GitHub ActionsとAWS CDKによるPR環境の自動構築・運用システム

東京ガスiネット DXSP部所属の利根川です。

目次

本記事では、Pull Request単位(以下PR)で独立したプレビュー環境を自動構築し、レビュープロセスを効率化するシステムの実装について解説します。AWS ECS/Aurora/EFSを用いたインフラ構成、GitHub Actionsによる自動デプロイの仕組み、そしてコスト最適化のための自動クリーンアップ機能まで、実運用を見据えた他プロジェクトにも展開できる汎用的な設計をご紹介します。

参考:ECS / Aurora / EFS

今回の取り組み

背景

私たちのチームでは、Djangoベースの店舗設備を管理するシステムを開発しています。このシステムは以下の特徴を持ちます:

  • マルチテナント対応django-tenantsを用いた複数組織対応
  • メディアファイル管理:設備の写真やPDFドキュメントを大量に扱う
  • ステートフルな処理:データベース構造に強く依存する複雑なビジネスロジック
  • ユーザー権限管理:店舗スタッフ、本部ユーザー、テナント管理者など複数のロール

このような複雑なアプリケーションでは、コードレビュー時に実際の動作を確認することが特に重要です。

課題

従来の開発フローでは、以下のような課題がありました:

  1. レビュー時の動作確認が困難

    • コードレビューだけでは実際のUI/UX、動作を確認できない
    • レビュワーがローカルで環境構築する手間が大きい(Postgres、EFS相当のストレージ、環境変数設定など)
    • マイグレーションファイルやサンプルデータの整合性を確認するには、実際に動かす必要がある
    • マルチテナント機能やファイルアップロード機能など、本番に近い状態でないと確認できない
  2. 共用開発環境での競合

    • 複数のPRが同時に進行すると、共用のdev環境で機能が衝突
    • 他の開発者のマイグレーションやテストデータによって動作確認が妨げられる
    • メディアファイル(画像、PDF)の混在により、意図しないデータが表示される
  3. フィードバックサイクルの遅延

    • レビュワーが環境構築に時間を費やし、本質的なレビューに集中できない
    • 問題が発見された場合の「修正→ビルド→再レビュー」のサイクルが長い
    • 非エンジニア(デザイナー、PM)への共有が困難

解決策

GitHub ActionsとAWS CDKを使用して、PR単位で完全に独立したプレビュー環境を自動構築・自動削除する仕組みを導入しました。

参考:GitHub Actions / CDK

1. アーキテクチャ概要

各PR環境は、以下のAWSリソースで構成されます:

Internet
    ↓
Application Load Balancer (ALB)
    ↓ (HTTPS)
ECS Fargate Cluster
    ├─ Appコンテナ (Django/Gunicorn)
    └─ Staticコンテナ (Nginx)
    ↓
Aurora PostgreSQL Serverless v2 (独立したDBクラスター)
    └─ Writer + Reader インスタンス
    ↓
EFS (メディアファイル用共有ストレージ)

参考:Aurora Serverless v2 スケーリング

重要なポイント: - 各PR環境は完全に独立したVPC、Aurora DB、EFSを持つ - dev環境のECRリポジトリとRoute53ホストゾーンのみ共有(コスト削減) - PR番号ベースの命名規則で環境を識別(例:pr-123.example.com

参考:ECR / Route 53/ALB(Application Load Balancer) / Fargate

2. デプロイフロー(初回構築時)

PR作成・更新時のデプロイは、以下の段階で実行されます:

Phase 1: 環境数チェック

- CloudFormationスタック数を監視
- 上限(4環境)到達時はデプロイをスキップし、PRにコメント通知

参考:CloudFormation(Outputsやdescribe-stacks) / AWS CLI

Phase 2: コンテナイメージのビルドとプッシュ

- Appコンテナ(Django/Gunicorn)をビルド → ECRにプッシュ(タグ: pr-{PR番号})
- Staticコンテナ(Nginx)をビルド → ECRにプッシュ(タグ: pr-{PR番号})

参考:Docker

Phase 3: シークレット生成

- Django Secret Keyを自動生成
- AWS Secrets Managerに保存(環境ごとに独立)

Phase 4: CDKスタックデプロイ(初回はdesiredCount=0)

- VPC、サブネット、セキュリティグループ作成
- Aurora PostgreSQL Serverless v2クラスター作成(Writer + Reader)
- EFS作成(メディアファイル用)
- ALB、ターゲットグループ作成
- ECSクラスター、タスク定義、サービス作成(desiredCount=0で起動)
- Route53レコード登録(pr-{PR番号}.pr-123.example.com)
- ACM証明書の自動検証

参考:ACM(AWS Certificate Manager)

Phase 5: データベース初期化

- 初期化タスクをECS Run Taskで実行
  - Djangoマイグレーション(migrate)
  - Public Tenant作成(django-tenants用)
  - スーパーユーザー作成
- サンプルデータ投入タスクを実行
  - テスト用テナント作成
  - ユーザーアカウント作成(管理者、店舗スタッフ、本部ユーザーなど)
  - 設備マスタデータ投入

Phase 6: ECSサービス起動とデプロイ

- ECSサービスのdesiredCountを1に更新(初回のみ)
- 最新のタスク定義でサービスを更新
- サービスの安定化を待機(wait-for-service-stability)

Phase 7: PRへの通知

- GitHub APIでPRにコメント投稿
  - プレビューURL
  - 管理画面URL
  - ログイン情報(各ロールのユーザー名/パスワード)
  - 自動削除までの猶予期間(1日間)

参考:GitHub API / actions/github-script

4. 更新時のデプロイフロー

PR更新時(新しいコミットがプッシュされた時)は、既存環境を再利用します:

1. 新しいコンテナイメージをビルド・プッシュ
2. 既存のCDKスタックを更新(差分のみ)
3. データベース初期化タスクを再実行(マイグレーション適用)
4. ECSサービスに新しいタスク定義をデプロイ
5. ローリングアップデートで切り替え

このフローにより、インフラリソースは再作成せず、アプリケーションコードとDBスキーマのみ更新されます。

技術的ポイント

ここでは、他のプロジェクトにも応用可能な汎用的な技術パターンを中心に解説します。

1. 初回デプロイとDBマイグレーションの依存関係解決

課題: ECSサービスが起動する前にデータベースマイグレーションを実行する必要がある。しかし、CDKでdesiredCount: 1を指定すると、マイグレーション前にアプリケーションが起動してしまう。

解決策:2段階デプロイパターン

// CDK側: コンテキストパラメータで初回デプロイを判定
const isInitialDeploy = this.node.tryGetContext('initialDeploy') === 'true';
const desiredCount = isInitialDeploy ? 0 : 1;

const service = new ecs.FargateService(this, 'Service', {
  // ...
  desiredCount,
});
# GitHub Actions側: 初回デプロイ判定
- name: Check if stack exists
  id: check-stack
  run: |
    if aws cloudformation describe-stacks --stack-name ${{ env.STACK_NAME }} 2>&1 | grep -q "does not exist"; then
      echo "is_initial_deploy=true" >> $GITHUB_OUTPUT
    else
      echo "is_initial_deploy=false" >> $GITHUB_OUTPUT
    fi

# 初回デプロイ時はdesiredCount=0でデプロイ
- name: Deploy CDK stack
  run: |
    if [ "${{ steps.check-stack.outputs.is_initial_deploy }}" = "true" ]; then
      npx cdk deploy ${{ env.STACK_NAME }} \
        --context initialDeploy=true
    else
      npx cdk deploy ${{ env.STACK_NAME }}
    fi

# マイグレーション実行(ECS Run Task)
- name: Run initialization task
  run: |
    aws ecs run-task --cluster ${{ env.ECS_CLUSTER }} ...

# 初回デプロイ時のみ、サービスのdesiredCountを1に更新
- name: Update ECS service desired count (if initial deploy)
  if: steps.check-stack.outputs.is_initial_deploy == 'true'
  run: |
    aws ecs update-service \
      --cluster ${{ env.ECS_CLUSTER }} \
      --service ${{ steps.get-cfn-outputs.outputs.service_name }} \
      --desired-count 1

汎用性: - DBマイグレーション以外にも、初期化処理が必要な任意のアプリケーションに適用可能 - Lambda関数でのスキーマ初期化、キャッシュウォームアップなどにも応用できる

2. CloudFormation Outputsによる疎結合設計

課題: CDKで作成したリソース(ECSサービス名、サブネットIDなど)をGitHub Actionsで参照したい。ハードコードすると、CDKコードの変更時にワークフローも修正が必要になる。

解決策:CloudFormation Outputsを活用

// CDK側: 必要な情報をOutputsとして出力
new cdk.CfnOutput(this, 'ECSServiceName', {
  value: this.service.serviceName,
  exportName: `${props.environment}-ecs-service-name`,
});

new cdk.CfnOutput(this, 'TaskDefinitionFamily', {
  value: taskDefinition.family,
});

new cdk.CfnOutput(this, 'PrivateSubnetIds', {
  value: this.vpc.privateSubnets.map(s => s.subnetId).join(','),
});
# GitHub Actions側: Outputsから動的に取得
- name: Get CloudFormation outputs
  id: get-cfn-outputs
  run: |
    OUTPUTS=$(aws cloudformation describe-stacks \
      --stack-name ${{ env.STACK_NAME }} \
      --query 'Stacks[0].Outputs')
    
    SERVICE_NAME=$(echo $OUTPUTS | jq -r '.[] | select(.OutputKey=="ECSServiceName") | .OutputValue')
    SUBNET_IDS=$(echo $OUTPUTS | jq -r '.[] | select(.OutputKey=="PrivateSubnetIds") | .OutputValue')
    
    echo "service_name=$SERVICE_NAME" >> $GITHUB_OUTPUT
    echo "subnet_ids=$SUBNET_IDS" >> $GITHUB_OUTPUT

# 後続のステップで利用
- name: Run task
  run: |
    aws ecs run-task \
      --cluster ${{ env.ECS_CLUSTER }} \
      --service ${{ steps.get-cfn-outputs.outputs.service_name }} \
      --network-configuration "awsvpcConfiguration={subnets=[${{ steps.get-cfn-outputs.outputs.subnet_ids }}]}"

汎用性: - CloudFormation/CDKを使う全プロジェクトに適用可能 - Terraform OutputsやPulumi Stack Outputsでも同様のパターンが使える

参考:Terraform / Pulumi

3. 複数コンテナイメージの段階的更新パターン

課題: ECSタスク定義に複数のコンテナ(App + Static)が含まれる場合、両方のイメージを新しいバージョンに更新したい。

解決策:amazon-ecs-render-task-definitionアクションの連鎖

# 現在のタスク定義をダウンロード
- name: Download current task definition
  run: |
    aws ecs describe-task-definition \
      --task-definition ${{ steps.get-cfn-outputs.outputs.task_def_family }} \
      --query taskDefinition > task-definition.json

# 1つ目のコンテナイメージを更新
- name: Update app container image
  id: task-def-app
  uses: aws-actions/amazon-ecs-render-task-definition@v1
  with:
    task-definition: task-definition.json
    container-name: app
    image: ${{ steps.login-ecr.outputs.registry }}/app:pr-${{ github.event.pull_request.number }}

# 2つ目のコンテナイメージを更新(前ステップの出力を入力に)
- name: Update static container image
  id: task-def-static
  uses: aws-actions/amazon-ecs-render-task-definition@v1
  with:
    task-definition: ${{ steps.task-def-app.outputs.task-definition }}  # 重要: 前ステップの出力
    container-name: static
    image: ${{ steps.login-ecr.outputs.registry }}/static:pr-${{ github.event.pull_request.number }}

# 最終的なタスク定義でデプロイ
- name: Deploy to Amazon ECS
  uses: aws-actions/amazon-ecs-deploy-task-definition@v2
  with:
    task-definition: ${{ steps.task-def-static.outputs.task-definition }}
    service: ${{ steps.get-cfn-outputs.outputs.service_name }}
    cluster: ${{ env.ECS_CLUSTER }}

参考:aws-actions/amazon-ecs-render-task-definition / aws-actions/amazon-ecs-deploy-task-definition

汎用性: - 3つ以上のコンテナでも同様に連鎖可能 - サイドカーコンテナ(ログルーター、メトリクス収集など)のイメージ更新にも適用可能

4. GitHub ActionsとAWS CLIによる環境数制限パターン

課題: PR環境が増えすぎるとAWSコストが爆発する。デプロイ前に環境数を制限したい。

解決策:事前チェックジョブと条件付きデプロイ

jobs:
  check-pr-limit:
    runs-on: ubuntu-latest
    outputs:
      should_deploy: ${{ steps.check.outputs.should_deploy }}
      pr_count: ${{ steps.check.outputs.pr_count }}
    steps:
      - name: Check PR environment count
        id: check
        run: |
          # 特定のプレフィックスを持つスタック数をカウント
          PR_STACKS=$(aws cloudformation list-stacks \
            --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE \
            --query 'StackSummaries[?starts_with(StackName, `AppStack-pr-`)].StackName' \
            --output text | wc -w)
          
          echo "pr_count=$PR_STACKS" >> $GITHUB_OUTPUT
          
          # 上限判定(4環境)
          if [ "$PR_STACKS" -ge 4 ]; then
            echo "should_deploy=false" >> $GITHUB_OUTPUT
          else
            echo "should_deploy=true" >> $GITHUB_OUTPUT
          fi

  # 上限到達時の通知ジョブ
  comment-limit-reached:
    needs: check-pr-limit
    if: needs.check-pr-limit.outputs.should_deploy == 'false'
    steps:
      - name: Comment on PR
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              body: '⚠️ PR環境の上限に達しています\n現在: ${{ needs.check-pr-limit.outputs.pr_count }}/4'
            })

  # 実際のデプロイジョブ(条件付き実行)
  deploy:
    needs: check-pr-limit
    if: needs.check-pr-limit.outputs.should_deploy == 'true'
    steps:
      # デプロイ処理...

汎用性: - CloudFormationスタック以外(ECSクラスター数、RDS数など)の制限にも応用可能 - Azure、GCPでも同様のリソースカウントロジックが実装可能

5. PR環境の自動クリーンアップパターン(2種類)

課題: PR環境を放置すると環境が残り続けコストがかさむ。人手での削除は運用負荷が高い。

解決策1:PRクローズ時の即座削除

PRがクローズ(マージまたは却下)された時点で、即座に環境を削除します:

name: Destroy PR Preview Environment

on:
  pull_request:
    types: [closed]  # PRクローズ時にトリガー

jobs:
  destroy:
    steps:
      # スタック存在確認
      - name: Check if stack exists
        id: check-stack
        run: |
          if aws cloudformation describe-stacks --stack-name ${{ env.STACK_NAME }} 2>/dev/null; then
            echo "exists=true" >> $GITHUB_OUTPUT
          else
            echo "exists=false" >> $GITHUB_OUTPUT
          fi
      
      # CDK Destroy(スタックが存在する場合のみ)
      - name: Destroy CDK stack
        if: steps.check-stack.outputs.exists == 'true'
        run: |
          npx cdk destroy ${{ env.STACK_NAME }} --force
      
      # 関連リソースの削除
      - name: Delete Django Secrets
        run: |
          aws secretsmanager delete-secret \
            --secret-id App-${{ env.ENVIRONMENT }}-DjangoSecrets \
            --force-delete-without-recovery || true
      
      - name: Delete ECR images
        run: |
          aws ecr batch-delete-image \
            --repository-name yourapp-dev-app \
            --image-ids imageTag=pr-${{ github.event.pull_request.number }} || true
          aws ecr batch-delete-image \
            --repository-name yourapp-dev-static \
            --image-ids imageTag=pr-${{ github.event.pull_request.number }} || true
      
      # PRにコメント
      - name: Comment on PR
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              body: '🗑️ プレビュー環境削除完了'
            })

解決策2:非アクティブ環境の定期クリーンアップ

PRがクローズされずに放置された場合や、ワークフロー実行失敗で削除漏れがあった場合に備え、定期実行ジョブで時系列ベースの削除も実施します:

name: Cleanup Inactive PR Environments

on:
  schedule:
    - cron: '0 15 * * *'  # 毎日0:00 JST
  workflow_dispatch:      # 手動実行も可能

jobs:
  cleanup:
    steps:
      - name: Find stacks to delete
        id: find-stacks
        uses: actions/github-script@v7
        with:
          script: |
            const { execSync } = require('child_process');
            const ONE_DAY_MS = 24 * 60 * 60 * 1000;
            const now = new Date();
            
            // CloudFormation Stacksを取得
            const stacksJson = execSync(
              'aws cloudformation list-stacks ' +
              '--stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE ' +
              '--query "StackSummaries[?starts_with(StackName, \'AppStack-pr-\')].{Name:StackName,UpdateTime:LastUpdatedTime}" ' +
              '--output json'
            ).toString();
            const stacks = JSON.parse(stacksJson);
            
            const stacksToDelete = [];
            
            for (const stack of stacks) {
              const lastUpdateTime = new Date(stack.UpdateTime);
              const daysSinceUpdate = (now - lastUpdateTime) / (24 * 60 * 60 * 1000);
              
              // 1日間更新がない場合、削除対象に
              if (now - lastUpdateTime > ONE_DAY_MS) {
                stacksToDelete.push({
                  name: stack.Name,
                  reason: `No updates for ${daysSinceUpdate.toFixed(1)} days`
                });
              }
            }
            
            core.setOutput('stacks_to_delete', JSON.stringify(stacksToDelete));

      - name: Delete stacks
        run: |
          STACKS_JSON='${{ steps.find-stacks.outputs.stacks_to_delete }}'
          echo "$STACKS_JSON" | jq -c '.[]' | while read stack; do
            STACK_NAME=$(echo "$stack" | jq -r '.name')
            
            # CDK Destroy
            npx cdk destroy $STACK_NAME --force
            
            # 関連リソースの削除(ECRイメージ、Secrets Managerなど)
            # ...
          done

汎用性:

  • 解決策1(PRクローズ時削除):GitLab CI/CDのmerge_requestイベント、Azure PipelinesのPRトリガーでも同様に実装可能
  • 解決策2(定期クリーンアップ):CloudFormation以外(EC2インスタンス、S3バケットなど)の自動削除にも応用可能
  • 削除条件を変更可能:例:3日間、1週間など
  • 組み合わせ利点:2つの削除方式を組み合わせることで、削除漏れを防止できる

6. ECS Run Taskによる初期化処理パターン

課題: データベースマイグレーションやサンプルデータ投入など、デプロイ時に一度だけ実行したい処理がある。

解決策:専用の初期化タスク定義 + ECS Run Task

# 初期化用タスク定義を取得
- name: Download init task definition
  run: |
    aws ecs describe-task-definition \
      --task-definition ${{ steps.get-cfn-outputs.outputs.init_task_family }} \
      --query taskDefinition > init-task-definition.json

# 最新イメージで初期化タスク定義を更新
- name: Update init task definition with new image
  id: init-task-def
  uses: aws-actions/amazon-ecs-render-task-definition@v1
  with:
    task-definition: init-task-definition.json
    container-name: init
    image: ${{ steps.login-ecr.outputs.registry }}/app:pr-${{ github.event.pull_request.number }}

# 新しいタスク定義を登録
- name: Register new init task definition
  id: register-init-task
  run: |
    NEW_TASK_DEF=$(aws ecs register-task-definition \
      --cli-input-json file://${{ steps.init-task-def.outputs.task-definition }} \
      --query 'taskDefinition.taskDefinitionArn' \
      --output text)
    echo "task_definition=$NEW_TASK_DEF" >> $GITHUB_OUTPUT

# 初期化タスクを実行(マイグレーション)
- name: Run initialization task
  id: run-init
  run: |
    TASK_ARN=$(aws ecs run-task \
      --cluster ${{ env.ECS_CLUSTER }} \
      --task-definition ${{ steps.register-init-task.outputs.task_definition }} \
      --launch-type FARGATE \
      --network-configuration "awsvpcConfiguration={...}" \
      --query 'tasks[0].taskArn' \
      --output text)
    echo "task_arn=$TASK_ARN" >> $GITHUB_OUTPUT

# タスク完了を待機
- name: Wait for initialization to complete
  run: |
    aws ecs wait tasks-stopped \
      --cluster ${{ env.ECS_CLUSTER }} \
      --tasks ${{ steps.run-init.outputs.task_arn }}
    
    # 終了コードを確認
    EXIT_CODE=$(aws ecs describe-tasks \
      --cluster ${{ env.ECS_CLUSTER }} \
      --tasks ${{ steps.run-init.outputs.task_arn }} \
      --query 'tasks[0].containers[0].exitCode' \
      --output text)
    
    if [ "$EXIT_CODE" != "0" ]; then
      exit 1
    fi

# 別のコマンドを実行する場合(サンプルデータ投入など)
- name: Run sample data seeding task
  run: |
    aws ecs run-task \
      --cluster ${{ env.ECS_CLUSTER }} \
      --task-definition ${{ steps.register-init-task.outputs.task_definition }} \
      --overrides '{"containerOverrides":[{"name":"init","command":["sh","-c","python manage.py seed_sample_data"]}]}'

汎用性:

  • 任意のワンタイム処理に適用可能:キャッシュクリア、インデックス再構築などに適用できます
  • 同じタスク定義で異なるコマンドを実行可能--overrides を活用してコマンドを切り替えられます
  • Kubernetesパターン:Kubernetesの`Job`/`CronJob`と同様の運用パターンで運用可能です

メリット

  1. レビュー品質の向上

    • 実際の動作を確認しながらレビューできる
    • UI/UXの問題を早期に発見できる
    • データベースに依存する機能も確認可能
  2. 開発速度の向上

    • レビュワーの環境構築コストがゼロ
    • フィードバックサイクルが高速化
    • 並行開発時の競合がなくなる
  3. 運用コストの最適化

    • 同時稼働環境数を制限(上限4環境)
    • 非アクティブ環境を自動削除(1日間更新なし)
    • 不要なリソースの課金を防止
  4. チーム全体の生産性向上

    • 非エンジニアでも簡単に最新の変更を確認できる
    • デザイナーやプロダクトマネージャーとの協業が円滑に
    • ステークホルダーへのデモが容易

デメリット

  1. 初期構築コスト

    • CDKスタック、GitHub Actionsワークフローの実装が必要
    • AWSリソースの権限設定、ネットワーク設計が複雑
    • 初期デプロイ時の2段階フローなど、考慮事項が多い
    • チームメンバーへの教育・ドキュメント整備が必要
  2. デプロイ時間

    • PR環境の初回構築には5〜10分程度かかる(VPC、Aurora、EFSの作成)
    • 更新時も3〜5分程度必要(コンテナイメージのビルド+デプロイ)
    • 小さな修正でもすぐに確認できるわけではない
  3. 運用コストの増加

    • Aurora Serverless v2(Writer + Reader)が環境ごとに必要
    • EFSの利用料金(メディアファイルが多い場合は増加)
    • 上限設定(4環境)でも、フル稼働時は月額数万円のコスト増
  4. トラブルシューティングの難しさ

    • デプロイ失敗時の原因特定が難しい場合がある(CDK、CloudFormation、ECS、GitHub Actionsと複数レイヤー)
    • ログの確認が複数のサービスに分散(CloudWatch Logs、GitHub Actions、CloudFormation Events)
    • ログの確認が複数のサービスに分散(CloudWatch Logs、GitHub Actions、CloudFormation Events)

参考:CloudWatch / CloudWatch Logs - Aurora接続エラー、EFSマウント失敗など、インフラ固有の問題が発生する可能性

  1. 環境間の一貫性維持

    • PR環境とproduction環境の差異が発生する可能性(リソーススペック、設定値など)
    • インフラコードの同期が重要(CDKコードの変更忘れ)
    • 環境変数やシークレットの管理が煩雑(Secrets Managerの手動更新が必要な場合も)
  2. プロジェクト固有の課題

    • マルチテナント構成:django-tenantsのPublic Tenantを毎回作成する必要がある
    • メディアファイルの肥大化:EFSの容量とコストが増加しやすい
    • サンプルデータの整合性:テナント、ユーザー、設備データの依存関係が複雑

導入・展開のポイント

1. 環境変数とシークレットの設計

PR番号をベースにした動的な環境名を使用することで、環境ごとに独立したリソースを管理できます:

env:
  ENVIRONMENT: pr-${{ github.event.pull_request.number }}
  ECS_CLUSTER: yourapp-pr-${{ github.event.pull_request.number }}
  STACK_NAME: AppStack-pr-${{ github.event.pull_request.number }}
  DOMAIN_NAME: pr-${{ github.event.pull_request.number }}.pr-123.example.com

Django Secret Keyなどの機密情報は、AWS Secrets Managerで環境ごとに自動生成:

SECRET_KEY=$(python3 -c 'import secrets; import string; chars = string.ascii_letters + string.digits + "!@#$%^&*(-_=+)"; print("".join(secrets.choice(chars) for i in range(50)))')
aws secretsmanager create-secret \
  --name App-${{ env.ENVIRONMENT }}-DjangoSecrets \
  --secret-string "{\"SECRET_KEY\":\"$SECRET_KEY\",\"DJANGO_SUPERUSER_PASSWORD\":\"admin\"}"

2. リソース制限の設定

コスト爆発を防ぐため、同時稼働環境数を制限する仕組みは必須です。詳細な実装は4. GitHub ActionsとAWS CLIによる環境数制限パターンを参照してください。

3. 自動クリーンアップの必須化

放置された環境が溜まり続けないよう、自動削除の仕組みは初期段階から導入することを強く推奨します。詳細な実装は5. PR環境の自動クリーンアップパターン(2種類)を参照してください。

4. CDKでのコンテキスト活用

初回デプロイと更新デプロイで異なる設定が必要な場合、CDKのコンテキスト機能を活用できます。詳細な実装は1. 初回デプロイとDBマイグレーションの依存関係解決を参照してください。

5. PRへのコメント通知

デプロイ完了時に、アクセスURLとログイン情報をPRにコメントすることで、レビュワーの利便性が大幅に向上します:

- name: Comment on PR
  uses: actions/github-script@v7
  with:
    script: |
      github.rest.issues.createComment({
        owner: context.repo.owner,
        repo: context.repo.repo,
        issue_number: context.issue.number,
        body: '## 🚀 プレビュー環境デプロイ完了\n\n**URL**: https://${{ env.DOMAIN_NAME }}'
      })

6. 初回デプロイの考慮

データベースマイグレーションなど、アプリケーション起動前に必要な処理がある場合は、2段階デプロイを検討してください。ECS ServiceのdesiredCountを制御することで実現できます。

7. エラーハンドリング

サンプルデータ投入など、失敗してもデプロイ自体は成功させたい処理については、適切なエラーハンドリングを実装:

if [ "$EXIT_CODE" != "0" ]; then
  echo "⚠️ サンプルデータ投入に失敗しましたが、デプロイは継続します"
else
  echo "✅ サンプルデータ投入が完了しました"
fi

8. タグ戦略

ECRイメージにはpr-{PR番号}というタグを使用することで、環境とイメージの対応が明確になり、削除時の処理も簡潔になります:

docker build -t $ECR_REGISTRY/yourapp-dev-app:pr-${{ github.event.pull_request.number }} .

9. モニタリングとアラート

  • CloudWatchでコストを監視し、想定外の課金を早期検知
  • デプロイ失敗時のSlack/Teams通知を設定
  • 環境数の推移をグラフ化して可視化

10. ドキュメント整備

  • チーム向けのセットアップガイド
  • トラブルシューティングドキュメント
  • AWSリソースの命名規則と権限設計の文書化

11. プロジェクト固有の考慮事項

私たちのプロジェクトでは、以下のようなアプリケーション固有の課題にも対応しました:

django-tenantsのマルチテナント初期化:

# init-task-definition内でPublic Tenantを作成
uv run python manage.py migrate_schemas --shared
uv run python manage.py create_public_tenant

複数ロールのサンプルユーザー作成:

# seed_sample_dataコマンド内で、各ロールのユーザーを作成
User.objects.create_user('admin', password='***', is_superuser=True)
User.objects.create_user('staff_store', password='***', role='STORE_STAFF')
User.objects.create_user('staff_hq', password='***', role='HQ_STAFF')
User.objects.create_user('tenant_admin', password='***', role='TENANT_ADMIN')

これにより、レビュワーはすぐに各ロールでログインし、権限制御を確認できます。


まとめ

  • 定量的な効果:
    • レビュー時の環境構築時間:30分 → 0分(完全自動化)
    • フィードバックサイクル:平均2日 → 平均半日(迅速化)
    • PR環境の削除忘れ:月3〜5件 → 0件(自動クリーンアップ)
  • 定性的な効果:
    • デザイナーやPMが直接プレビュー環境を確認でき、コミュニケーションが円滑に
    • マイグレーションファイルの不具合を早期に発見できるようになった
    • マルチテナント機能の動作確認が容易になり、バグ混入が減少
  • コスト面:
    • PR環境の運用コスト:月額約1.5万円(概算、2025年12月時点)。詳細は各サービスの料金ページを参照:Aurora Serverless v2 料金 / EFS 料金
    • 上限4環境の制限により、想定外のコスト増加を防止
    • 開発者の時間削減効果を考慮すると、ROIは高い
  • 今後の改善点:
    • デプロイ時間の短縮(現在5〜10分 → 目標3分以内)
    • コスト削減(Aurora Serverless v2のACU設定最適化)
    • 自動テストとの統合(E2Eテストの自動実行)

本記事で紹介した技術パターンは、ECS以外のコンテナ基盤(EKS、Cloud Run、Azure Container Instancesなど)でも応用可能です。特に以下のパターンは汎用性が高く、多くのプロジェクトで活用できます:

  1. 2段階デプロイパターン(初期化処理とアプリ起動の分離)
  2. CloudFormation Outputsによる疎結合設計
  3. 環境数制限パターン(コスト爆発の防止)
  4. 時系列ベースの自動クリーンアップ

初期構築コストはかかりますが、チーム全体の生産性向上とレビュー品質の改善に大きく貢献する仕組みです。ぜひ参考にしていただければ幸いです。