はじめに
前回の記事では、LocalStack + SAM + Docker Compose でローカル開発環境を作りました。
今回はその続きです。ローカルで動いたコードを「mainブランチにマージしたら自動で本番に届く」状態にします。
この記事でわかること
-
template.local.yml/template.stg.yml/template.prod.ymlの3ファイルで環境を分離する設計 - GitHub Actions で SAM のビルド〜デプロイを自動化する手順
- STGは自動デプロイ、本番は手動トリガーにする理由と実装
-
sam build --use-containerがCIでは不要になる理由 - スモークテストで「デプロイできたこと」を確認する仕組み
- 実際にハマった2つの落とし穴(依存関係注入・Authorizer TTLバグ)
完成形のパイプライン
STGは自動、本番は手動。この非対称な設計が重要で、後ほど詳しく説明します。
環境ごとのテンプレート設計
まず、テンプレートを環境ごとに分離します。
backend/
├── template.local.yml # ローカル開発用(LocalStack + sam local)
├── template.stg.yml # STG環境用(AWS Lambda + RDS)
└── template.prod.yml # 本番環境用(AWS Lambda + RDS)
3ファイルの主な違いはここです。
| 項目 | local | stg | prod |
|---|---|---|---|
| S3接続先 | LocalStack (host.docker.internal) | AWS S3 | AWS S3 |
| DB接続先 | postgres-db (Docker) | RDS (stg) | RDS (prod) |
| CORS許可オリジン | localhost:3000 | https://stg.example.com | https://www.example.com |
| スロットリング | なし | なし | あり(DoS対策) |
| CloudFront | なし(presigned URLフォールバック) | あり | あり |
template.stg.yml のGlobalsセクション(本番も構造は同じ):
# template.stg.yml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Parameters:
DatabasePassword:
Type: String
NoEcho: true # CloudFormationコンソールに値を表示しない
Default: ""
FrontendUrl:
Type: String
Default: https://stg.example.com
Globals:
Function:
Runtime: python3.12
Timeout: 10
MemorySize: 128
Architectures:
- x86_64
Environment:
Variables:
DATABASE_HOST: your-rds-stg.xxxx.ap-northeast-1.rds.amazonaws.com
DATABASE_NAME: mydb
USE_LOCAL_S3: "false" # 本番S3を使う
S3_BUCKET: myapp-uploads-stg
COOKIE_SAMESITE: "None" # クロスオリジンCookieに必要
CLOUDFRONT_DOMAIN: !GetAtt MyImageDistribution.DomainName
Resources:
MyAppApi:
Type: AWS::Serverless::Api
Properties:
StageName: stg
Cors:
AllowOrigin: !Sub "'${FrontendUrl}'"
AllowCredentials: true
USE_LOCAL_S3: "false" の一行でLambdaコードの向き先が切り替わります。ローカルでは true にしてLocalStackに接続していたのと同じ環境変数です。
GitHub Actions の設定
GitHub Actions は、GitHubに組み込まれたCI/CDサービスです。リポジトリの .github/workflows/ に YAML ファイルを置くだけで、「pushをトリガーにビルドする」「手動ボタンでデプロイする」といった自動化フローを定義できます。GitHubのプライベートリポジトリでも月2,000分まで無料で利用できます。
ワークフローファイルの基本構造はシンプルです。
on: # いつ実行するか(トリガー)
push:
branches: [develop]
jobs:
deploy:
runs-on: ubuntu-latest # 実行環境
steps: # 実行するステップを順に定義
- uses: actions/checkout@v4
- run: echo "hello"
uses: で公式・サードパーティのアクション(再利用可能な処理単位)を呼び出せます。SAM CLIのインストールやAWS認証設定も、専用アクションが用意されているため数行で済みます。
事前準備: シークレットの登録
GitHubリポジトリの Settings → Secrets and variables → Actions に以下を登録します。
| シークレット名 | 内容 |
|---|---|
AWS_ACCESS_KEY_ID |
デプロイ用IAMユーザーのアクセスキー |
AWS_SECRET_ACCESS_KEY |
同シークレットキー |
STG_DB_PASSWORD |
STG環境のDBパスワード |
PROD_DB_PASSWORD |
本番環境のDBパスワード |
IAMユーザーに必要なポリシーは最低限これだけです。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"cloudformation:*",
"lambda:*",
"apigateway:*",
"s3:*",
"iam:*",
"logs:*"
],
"Resource": "*"
}
]
}
本番運用では Resource にARNを絞るとより安全です。
STGワークフロー — developブランチへのpushで自動実行
.github/workflows/deploy-stg.yml:
name: Deploy Backend to STG
on:
push:
branches:
- develop
paths:
- "**.py"
- "**/requirements.txt"
- "template.stg.yml"
- ".github/workflows/deploy-stg.yml"
workflow_dispatch: # 手動実行も可能にしておく
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Install SAM CLI
uses: aws-actions/setup-sam@v2
with:
use-installer: true
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: SAM Build
run: sam build -t template.stg.yml # --use-container は不要(後述)
- name: SAM Deploy
run: |
sam deploy \
--stack-name myapp-stg \
--s3-bucket myapp-lambda-deploy \
--no-confirm-changeset \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \
--no-fail-on-empty-changeset \
--force-upload \
--parameter-overrides \
DatabasePassword=${{ secrets.STG_DB_PASSWORD }} \
FrontendUrl=https://stg.example.com
- name: Smoke test
run: |
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST https://your-api-id.execute-api.ap-northeast-1.amazonaws.com/stg/user/login \
-H "Content-Type: application/json" -d '{}')
echo "smoke test status: $STATUS"
if [[ "$STATUS" == 5* ]]; then
echo "Smoke test FAILED: got HTTP $STATUS"
exit 1
fi
echo "Smoke test passed"
pathフィルタの意図: .py ファイル・requirements.txt・template.stg.yml のいずれかが変更されたときだけ実行されます。ドキュメントの修正やフロントエンドのコミットでLambdaデプロイが走らないようにする設定です。差分のないデプロイでも --no-fail-on-empty-changeset で正常終了します。
本番ワークフロー — 手動トリガーのみ
.github/workflows/deploy-prod.yml:
name: Deploy Backend to PROD
# workflow_dispatch のみ(自動トリガーなし)
on:
workflow_dispatch:
inputs:
reason:
description: "手動実行の理由(例: v1.2.0 本番リリース)"
required: true
default: "手動デプロイ"
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Install SAM CLI
uses: aws-actions/setup-sam@v2
with:
use-installer: true
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: SAM Build
run: sam build -t template.prod.yml
- name: SAM Deploy
run: |
sam deploy \
--stack-name myapp-prod \
--s3-bucket myapp-lambda-deploy \
--no-confirm-changeset \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \
--no-fail-on-empty-changeset \
--force-upload \
--parameter-overrides \
DatabasePassword=${{ secrets.PROD_DB_PASSWORD }} \
FrontendUrl=https://www.example.com
- name: Smoke test
run: |
PROD_API_URL=$(aws cloudformation describe-stacks \
--stack-name myapp-prod \
--query "Stacks[0].Outputs[?OutputKey=='ApiGatewayUrl'].OutputValue" \
--output text)
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "${PROD_API_URL}/user/login" \
-H "Content-Type: application/json" -d '{}')
echo "smoke test status: $STATUS"
if [[ "$STATUS" == 5* ]]; then
echo "Smoke test FAILED: got HTTP $STATUS"
exit 1
fi
echo "Smoke test passed"
なぜ本番は手動トリガーにするのか
workflow_dispatch だけにした理由は、developへのpush = 本番リリース準備完了 ではないからです。
個人開発ではブランチ保護が甘くなりがちで、途中の実装が develop に入ることもあります。「STGで1〜2日動作確認した上で、意図を持って本番に押す」という人間の判断ステップを挟むことで、誤デプロイを防ぎます。
GitHub上の操作手順はシンプルです。
Actions → Deploy Backend to PROD → Run workflow
→ 理由を入力して実行
これだけで SAM build → deploy → smoke test が全自動で走ります。
まとめ
| 構成要素 | 役割 |
|---|---|
template.{env}.yml |
環境ごとのインフラ差分をIaCで管理 |
deploy-stg.yml |
develop push で自動STGデプロイ |
deploy-prod.yml |
workflow_dispatch で手動本番デプロイ |
| pathフィルタ | 無関係なpushでデプロイが走るのを防ぐ |
| 依存注入ステップ | SAM build の pip install が不安定な場合の保険 |
| スモークテスト | 5xxのみ検知、Lambda死活確認 |
ローカルでLocalStackを使って開発し、developにpushしたらSTGに自動デプロイされ、確認したら手動で本番に届ける。この3段構えのパイプラインが揃うと、コードを書くことに集中できる環境になります。
この記事の背景
このパイプラインは、グループ旅行のしおり管理・費用精算・旅行レポート共有ができる個人開発サービス 旅BASE(tabibase) の実際のデプロイ構成をベースにしています。
ローカル環境構築の前回記事はこちらです。
AI(Claude Code)をチームメンバーとして開発する全体像については、こちらをご覧ください。