アルファテックブログ

実践 CI/CD クラウドインフラ構築・運用省力化

カバー

はじめに

私はTerraformを用いたクラウドインフラ構築・GitHubによるコード管理や、IaaS上の仮想サーバー設定の業務を担当しています。
この記事では、私が担当する業務で発生する課題と、その対応策としてのGitHub Actionsを用いたCI/CD実装例をご紹介します。

※ ご紹介するGitHub Actionsのワークフロー実装例には、サードパーティ製のアクションが含まれています。
サードパーティ製のアクションはサプライチェーン攻撃の対象となるケースがあります。
利便性とセキュリティリスクのトレードオフを認識したうえで活用する必要があります。

課題

Terraformを用いたクラウドインフラ構築・GitHubによるコード管理やIaaS上の仮想サーバー設定の業務では、以下3つの課題が発生し得ます。

PullRequest(PR)でのコミュニケーション

PR上では、レビュアーとレビュイーがコミュニケーションを取りながら、コードの変更内容や意図について認識を合わせる必要があります。
PRのテンプレートを用意して記載ルールを整備する方法もありますが、担当者ごとに説明の粒度や精度が異なります。
レビュイーがどの観点でセルフチェックを行ったのか、レビュアーにどこを重点的に確認して欲しいのかが読み取れないケースも少なくありません。
その結果、コミュニケーションに摩擦が生じ、関係性が悪化してしまうこともあります。

terraform planの実行結果を取得する

PRを作成・レビューする際には、terraform planの実行結果が適切であることを確認することが必須となります。
しかし、PRのたびに手作業でterraform planを実行して結果を添付するのは大きな負担となりがちです。
作業の過程で誤ってterraform applyを実行してしまうといったヒューマンエラーのリスクも発生します。

サーバーに対する手動操作

特に運用フェーズでは、各種サーバーにログインして様々なコマンドを実行する必要があります。
事前に検証済みのコマンドであっても、接続先の指定ミスや実行順の誤りといったヒューマンエラーの余地が残されています。

また、サーバー管理のためにクライアントソフトなどのツールを利用する場合、多くの場合は接続元として踏み台サーバーを用意します。
踏み台サーバーは作業用環境であることから「必要なツールをすぐに導入したい」という要望も多く、勝手に設定を変更されてしまうこともあります。
災害対策やセキュリティ対策、保守運用などの維持コストも発生します。


以降、それぞれ以下を例に取って、各種課題の軽減につながるCI/CDの仕組みをご紹介します。

  • Terraformによるインフラ構築:AWSを用いた基盤構築
  • サーバーに対する手動操作:AWS環境でのDBマイグレーション作業

対応策

各課題に対して、それぞれ以下の対応策をご紹介します。

  1. 課題:PullRequest(PR)でのコミュニケーション
    • 対応策:PRの内容の説明や確認観点の抽出をAIに補助してもらう
  2. 課題:terraform planの実行結果を取得する
    • 対応策:PRを契機に自動的にterraform planを実行する
  3. 課題:サーバーに対する手動操作
    • 対応策:git pushを契機に自動的にECSタスクを起動してコマンドを実行する

対応策の全体像

構成図

arch_all

GitHub設定

ディレクトリ構成
GitHubリポジトリー/
├── .github/
| ├── workflows/
| | ├── pr_agent.yml
| | ├── tf_fmt.yml
| | ├── tf_plan.yml
| | └── db_migrate.yml
| └── actions/
| ├── call_aws_step_functions
| | └── action.yml
| └── generate_aws_credential_string
| └── action.yml
└── environments/
└── dev/
├── docker/
| ├── app/
| | ├── prisma/
| | | ├── .env
| | | └── schema.prisma
| | └── migrate.sh
| └── Dockerfile
├── lambda_function/
| └── index.js
├── main.tf
└── terraform.tf
シークレット設定
Repository secrets
├── OPENAI_KEY
├── AWS_BACKEND_EXTERNAL_ID
├── AWS_BACKEND_ROLE_ARN
├── AWS_DEPLOY_EXTERNAL_ID
├── AWS_DEPLOY_ROLE_ARN
├── AWS_ROLE_ARN
├── DB_MGR_ECR_IMAGE_TAG
├── DB_MGR_ECR_REGISTRY
├── DB_MGR_ECR_REPOSITORY
├── DB_MGR_ROLE_ARN
├── DB_MGR_ROLE_EXTERNAL_ID
└── DB_MGR_SFN_ARN


以降、個々の対応策の詳細についてご紹介します。

対応策:PRの内容の説明や確認観点の抽出をAIに補助してもらう

PR-Agentというツールを使用して、AIによるフィードバックを実現します。
利用するAIは、OpenAI、Azure OpenAI、Amazon Bedrockなどの中からユーザーが選択できます。
AIの利用には別途料金が発生しますが、PR-Agent自体は無料で使用することができます。

レビュイーはAIによるフィードバックを受けて、セルフチェックの質を高めた上でレビューを依頼します。
レビュアーはAIによるフィードバックを参考にして、レビューの観点を考えることができます。

構成図

arch_pr_agent

対応策導入後のシーケンス

展開
  sequenceDiagram
      actor レビュイー
      participant GitHub as GitHub
Repository participant pr_agent as pr_agent
(GHA) participant OpenAI actor レビュアー %% 初回プッシュとPR作成 rect rgba(224,242,254,0.3) note over レビュイー,GitHub: 初回プッシュとPR作成 レビュイー ->> GitHub: Terraformコードをgit push レビュイー ->> GitHub: PRを作成 GitHub ->> pr_agent: PR作成イベント通知 end %% PR-Agent によるレビュー rect rgba(224,242,254,0.3) note over pr_agent,OpenAI: PR-Agent によるレビュー pr_agent ->> OpenAI: PRの内容を送信して分析 OpenAI ->> pr_agent: 概要・難易度・レビューコメント Note left of pr_agent: PR内容の説明や確認観点をAIで補助 pr_agent ->> GitHub: レビューコメントを投稿 GitHub ->> レビュイー: コメント通知 loop 1, PR-Agentによる指摘数 レビュイー ->> GitHub: レビュー結果を確認 alt 修正が必要 レビュイー ->> レビュイー: Terraformコードを修正 レビュイー ->> GitHub: Terraformコードをgit push GitHub ->> pr_agent: PR更新イベント通知 pr_agent ->> OpenAI: レビュー依頼 OpenAI ->> pr_agent: レビューコメント pr_agent ->> GitHub: レビューコメントを更新 GitHub ->> レビュイー: コメント通知 else 修正不要 レビュイー ->> GitHub: 修正不要である旨コメントを投稿 end end end %% レビュアーによるレビュー rect rgba(224,242,254,0.3) note over レビュイー,レビュアー: レビュアーによるレビュー レビュイー ->> レビュアー: レビュー依頼 レビュアー ->> GitHub: PRを確認 alt 指摘事項あり レビュアー ->> GitHub: レビュー指摘コメント投稿 GitHub ->> レビュイー: コメント通知 loop 1, 指摘事項の数 レビュイー ->> GitHub: レビュー結果を確認 レビュイー ->> レビュイー: Terraformコードを修正 レビュイー ->> GitHub: Terraformコードをgit push レビュイー ->> レビュアー: レビュー依頼 レビュアー ->> GitHub: 指摘が解消されたことを確認 end end レビュアー ->> GitHub: PRを承認 end

導入方法

1. OpenAIのAPIキーを発行する@OpenAI

  • 前提
    • PR-AgentからAIにアクセスするためにAPIキーが必要です。
    • Azure OpenAIやAmazon Bedrockも使用可能ですが、この記事ではOpenAIを使用することとします。
      • 様々なモデルに対応していますが、以下のドキュメントに記載の通り、2025年10月現在ではデフォルトでGPT-5が使用されます。
      • 以降の手順は、OpenAIのアカウントを取得済みであることを前提とします。
  1. 以下にてAPIキーを発行する
  2. OpenAIの支払い情報を設定する

2. 取得したAPIキーをGitHubリポジトリーのシークレットに登録する@GitHub Repository

  1. 取得したAPIキーをGitHubリポジトリーのシークレットOPENAI_KEYに設定する

3. PR-Agentを実行するワークフローを作成する@GitHub Repository

  1. 以下のドキュメントを参照し、PR-Agentを実行するワークフローを作成する
    • https://qodo-merge-docs.qodo.ai/installation/github/#run-as-a-github-action

    • 今回は、.github/workflows/pr_agent.ymlを以下のように設定しました。

      .github/workflows/pr_agent.yml
      .github/workflows/pr_agent.yml
      name: Pr Agent
      on:
      pull_request:
      issue_comment:
      jobs:
      pr_agent_job:
      if: ${{ github.event.sender.type != 'Bot' }}
      runs-on: ubuntu-latest
      permissions:
      issues: write
      pull-requests: write
      contents: write
      name: Run pr agent on every pull request, respond to user comments
      steps:
      - name: PR Agent action step
      id: pragent
      uses: qodo-ai/pr-agent@main
      env:
      OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      github_action_config.auto_review: "true"
      github_action_config.auto_describe: "true"
      github_action_config.auto_improve: "true"
      github_action_config.pr_actions: '["opened", "reopened", "ready_for_review", "review_requested", "synchronize"]'

使用感

PR作成時には"example"というコメントしか記載していない

github_create_pr

PR Agentによる変更内容の要約が追記される

github_created_pr github_change_walkthrough

タイポを含むコードをpushした場合

pragent_code_suggest_detail_typo

機密情報を含むコードをpushした場合

pragent_code_suggest_detail_sensitive


パラメーターを設定することで、PR-Agentによるコメントを日本語化することができます。
.github/workflows/pr_agent.ymlの設定で、PR-Agentによるコメントを日本語化する
.github/workflows/pr_agent.yml
name: Pr gent
on:
pull_request:
issue_comment:
jobs:
pr_agent_job:
if: ${{ github.event.sender.type != 'Bot' }}
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
contents: write
name: Run pr agent on every pull request, respond to user comments
steps:
- name: PR Agent action step
id: pragent
uses: qodo-ai/pr-agent@main
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
github_action_config.auto_review: "true"
github_action_config.auto_describe: "true"
github_action_config.auto_improve: "true"
github_action_config.pr_actions: '["opened", "reopened", "ready_for_review", "review_requested", "synchronize"]'
CONFIG.RESPONSE_LANGUAGE: 'ja-JP' # 日本語化のための設定
日本語によるコメント

pragent_japanese_comment

効果

本対応策を導入すると、開発効率を向上させるのに十分なクオリティのコメントが素早く自動的に生成されます。
結果として、レビュアーとレビュイーの間にAIが入り込み、コミュニケーションの摩擦が軽減されます。

対応策:PRを契機に自動的にterraform planを実行する

PRの作成や変更のタイミングで、GitHub Actionsから自動的にterraform planを実行します。

構成図

arch_auto_tf_plan

対応策導入後のシーケンス

展開
  sequenceDiagram
      actor 作業者
      participant GitHub as GitHub
Repository participant tf_fmt as tf_fmt
(GHA) participant tf_plan as tf_plan
(GHA) participant AWS %% 初回プッシュとPR作成 rect rgba(224,242,254,0.3) note over 作業者,GitHub: 初回プッシュとPR作成 作業者 ->> GitHub: Terraformコードをgit push 作業者 ->> GitHub: PRを作成 end %% Terraform plan 自動実行 rect rgba(224,242,254,0.3) note over GitHub,AWS: Terraform plan 自動実行 par terraform fmt GitHub ->> tf_fmt: PR作成イベント通知 tf_fmt ->> tf_fmt: terraform fmt tf_fmt ->> GitHub: terraform fmt結果コメントを投稿 and terraform plan GitHub ->> tf_plan: PR作成イベント通知 tf_plan ->> AWS: terraform plan Note left of tf_plan: PRを契機に自動的に
`terraform plan`を実行 tf_plan ->> GitHub: terraform plan結果コメントを投稿 end end %% Terraform plan 自動実行結果確認 rect rgba(224,242,254,0.3) note over GitHub,作業者: 結果確認 GitHub ->> 作業者: コメント通知 作業者 ->> GitHub: コメント確認 end

導入方法

1. OIDC providerを作成する@AWSアカウント

  1. 以下のドキュメントを参照し、AWS IAMにてOIDC providerを作成する
  • 今回は以下のように設定しました。

    aws_iam_oidcp

2. IAMロールを作成する@AWSアカウント

  1. GitHub ActionsにてAssumeRoleするIAMロールを作成する
    • 以下のドキュメントを参考にIAMロールを作成します。
    • GitHub ActionsからAssumeRoleするIAMロールには、Terraformのバックエンドへのアクセスやデプロイに使用するIAMロールとStepFunctions実行用IAMロールへのスイッチロールを許可します。
      • GitHub Actionsのワークフロー内でAWS Profileを設定することで、それぞれのIAMロールにスイッチロールします。
      • OIDC providerを作成するAWSアカウントとTerraformでAWSリソースをデプロイしたいAWSアカウントが異なる場合もあるかと思いますので、このような方式にしています。
    • 今回は以下のように設定しました。

      ※ 各種ポリシーの内容は必要に応じて絞り込みや追加を行う必要があります。

      IAM Role 信頼ポリシー 許可ポリシー
      AWS_ROLE
      GitHub Actions が OIDC により AssumeRole
      展開
      {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Federated": "arn:aws:iam::${AWSAccountId}:oidc-provider/token.actions.githubusercontent.com"
              },
              "Action": "sts:AssumeRoleWithWebIdentity",
              "Condition": {
                "StringEquals": {
                  "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                  "token.actions.githubusercontent.com:sub": "repo:${GitHubOrganizationName}/${GitHubRepositoryName}:*"
                }
              }
            }
          ]
        }
        
      展開
      {
          "Version": "2012-10-17",
          "Statement": {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": [
              "${AWS_BACKEND_ROLEのARN}",
              "${AWS_DEPLOY_ROLEのARN}",
              "${AWS_MGR_ROLEのARN}"
            ]
          }
        }
        
      AWS_BACKEND_ROLE
      Terraform バックエンド用 S3
      展開
      {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "AWS": "${AWS_ROLEのARN}"
              },
              "Action": "sts:AssumeRole",
              "Condition": {
                "StringEquals": {
                  "sts:ExternalId": "${外部ID}"
                }
              }
            }
          ]
        }
        
      展開
      AmazonS3FullAccess
      AWS_DEPLOY_ROLE
      AWS リソースデプロイ用
      展開
      {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "AWS": "${AWS_ROLEのARN}"
              },
              "Action": "sts:AssumeRole",
              "Condition": {
                "StringEquals": {
                  "sts:ExternalId": "${外部ID}"
                }
              }
            }
          ]
        }
        
      展開
      AmazonS3FullAccess

3. 作成したIAMロールの情報をGitHubリポジトリーのシークレットに登録する@GitHub Repository

  1. 各種IAMロールのARNと外部IDについて、以下のGitHubリポジトリーのシークレットに設定する
    • AWS_ROLE_ARN:AWS_ROLEのARN
    • AWS_BACKEND_ROLE_ARN:AWS_BACKEND_ROLEのARN
    • AWS_BACKEND_EXTERNAL_ID:AWS_BACKEND_ROLEの外部ID
    • AWS_DEPLOY_ROLE_ARN:AWS_DEPLOY_ROLEのARN
    • AWS_DEPLOY_EXTERNAL_ID:AWS_DEPLOY_ROLEの外部ID

4. terraform fmtと修正提案を実行するワークフローを作成する@GitHub Actions

  1. 以下のページを参照して、terraform fmt実行と提案を自動化します。
  • 今回は、.github/workflows/tf_fmt.ymlを以下のように設定しました。

    .github/workflows/tf_fmt.yml
    .github/workflows/tf_fmt.yml
    name: Terraform fmt
    on:
    pull_request:
    env:
    TF_VERSION: 1.12.1
    permissions:
    id-token: write
    contents: read
    pull-requests: write
    jobs:
    fmt:
    name: Terraform fmt
    runs-on: ubuntu-latest
    steps:
    - name: Check out repository
    uses: actions/checkout@v4
    with:
    ref: ${{ github.event.pull_request.head.sha }}
    - name: Setup Terraform
    uses: hashicorp/setup-terraform@v3
    with:
    terraform_version: ${{ env.TF_VERSION }}
    - name: Terraform fmt
    run: terraform fmt -recursive
    - name: suggest
    uses: reviewdog/action-suggester@v1
    with:
    toolname: terraform fmt
    fail_on_error: true

5. terraform planを実行するワークフローを作成する@GitHub Actions

  1. terraform planの実行を自動化します。
  • 今回は、以下のように設定しました。

    .github/workflows/tf_plan.yml
    .github/workflows/tf_plan.yml
    name: Terraform plan
    on:
    pull_request:
    env:
    TF_VERSION: 1.12.1
    AWS_DEFAULT_REGION: ap-northeast-1
    permissions:
    id-token: write
    contents: read
    pull-requests: write
    issues: write
    jobs:
    tf_plan:
    name: Terraform plan
    runs-on: ubuntu-latest
    strategy:
    matrix:
    workdir:
    - environments/dev
    steps:
    - name: Git checkout
    uses: actions/checkout@v4
    with:
    ref: ${{ github.event.pull_request.head.sha }}
    - name: Generate aws Backend credential string
    id: gen_aws_backend_cred_str
    uses: ./.github/actions/generate_aws_credential_string
    with:
    profile: "terraform-backend"
    aws_role_arn: ${{ secrets.AWS_BACKEND_ROLE_ARN }}
    aws_role_external_id: ${{ secrets.AWS_BACKEND_EXTERNAL_ID }}
    - name: Generate aws deploy credential string
    id: gen_aws_deploy_cred_str
    uses: ./.github/actions/generate_aws_credential_string
    with:
    profile: "terraform-deploy"
    aws_role_arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
    aws_role_external_id: ${{ secrets.AWS_DEPLOY_EXTERNAL_ID }}
    - name: Write aws credential string
    run: |
    mkdir -p $HOME/.aws
    echo "${{ steps.gen_aws_backend_cred_str.outputs.aws_credential }}" >> $HOME/.aws/config
    echo "${{ steps.gen_aws_deploy_cred_str.outputs.aws_credential }}" >> $HOME/.aws/config
    - name: Get aws credential
    uses: aws-actions/configure-aws-credentials@v4
    with:
    role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
    role-session-name: iar-gha-con-${{ github.run_id }}
    aws-region: ${{ env.AWS_DEFAULT_REGION }}
    - run: aws sts get-caller-identity
    - name: Setup Terraform
    uses: hashicorp/setup-terraform@v3
    with:
    terraform_version: ${{ env.TF_VERSION }}
    - name: Setup tfcmt
    env:
    GH_TOKEN: ${{ github.token }}
    run: |
    gh release download -R suzuki-shunsuke/tfcmt v4.14.4 -p tfcmt_linux_amd64.tar.gz
    tar -xzf tfcmt_linux_amd64.tar.gz
    sudo mv tfcmt /usr/local/bin/tfcmt
    tfcmt --version
    - name: Terraform init
    run: terraform init
    working-directory: ${{ matrix.workdir }}
    - name: Terraform validate
    run: terraform validate
    working-directory: ${{ matrix.workdir }}
    - name: Terraform plan
    run: tfcmt -var "target:${{ matrix.workdir }}" plan -patch -- terraform plan -parallelism=30 -no-color
    working-directory: ${{ matrix.workdir }}
    env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    .github/actions/generate_aws_credential_string/action.yml
    .github/actions/generate_aws_credential_string/action.yml
    name: Generate aws credential string
    inputs:
    profile:
    required: true
    type: string
    aws_role_arn:
    required: true
    type: string
    aws_role_external_id:
    required: true
    type: string
    outputs:
    aws_credential:
    value: ${{ steps.gen_aws_cred_str.outputs.aws_credential }}
    runs:
    using: composite
    steps:
    - name: Generate aws credential string
    id: gen_aws_cred_str
    shell: bash
    run: |
    CREDENTIAL=$(
    echo -e "[profile $PROFILE]
    credential_source = Environment
    role_arn = $AWS_ROLE_ARN
    external_id = $AWS_ROLE_EXTERNAL_ID
    "
    )
    echo "aws_credential<<EOF" >> $GITHUB_OUTPUT
    echo -e "$CREDENTIAL" >> $GITHUB_OUTPUT
    echo "EOF" >> $GITHUB_OUTPUT
    env:
    PROFILE: ${{ inputs.profile }}
    AWS_ROLE_ARN: ${{ inputs.aws_role_arn }}
    AWS_ROLE_EXTERNAL_ID: ${{ inputs.aws_role_external_id }}

使用感

自動的にterraform plan結果をコメント

terraform_plan_result

formatされていないコードをpushした場合にはワンクリックで修正をコミットできる

reviewdog_fmt

効果

本対応策を導入すると、terraform planが成功するか否かを常に確認することができます。
結果として、Terraformファイルの作成が大幅に効率化されます。

対応策:git pushを契機に自動的にECSタスクを起動してコマンドを実行する

コンテナのビルドコンテキストが格納されているenvironments/dev/docker配下に変更をgit pushすると、自動的にdocker builddocker pushして、そのイメージを用いてECSタスクを起動します。

拡張性やセキュリティを考慮して、GitHub ActionsからStepFunctionsを経由してECSタスクを起動する構成としました。
StepFunctionsを経由することで、承認や通知の処理を簡単に追加できます。また、GitHub Actionsに許可する権限はStepFunctionsの呼び出し権限のみとシンプルになります。

構成図

arch_db_migrate

対応策導入後のシーケンス

展開
  sequenceDiagram
      actor 作業者
      participant GitHub as GitHub
Repository participant db_migrate as db_migrate
(GHA) participant StepFunctions participant ECS participant ECR participant Servers %% コンテナイメージ作成 rect rgba(224,242,254,0.3) note over 作業者,ECR: コンテナイメージ作成 作業者 ->> 作業者: Dockerfileや
build contextを作成 作業者 ->> GitHub: git push GitHub ->> db_migrate: Push作成イベント通知 db_migrate ->> ECR: docker login db_migrate ->> db_migrate: docker build db_migrate ->> ECR: docker push end %% ECSタスクからのコマンド実行 rect rgba(224,242,254,0.3) note over db_migrate,Servers: ECSタスクからのコマンド実行 db_migrate ->> StepFunctions: ECSタスク起動用
ステートマシン実行 StepFunctions ->> ECS: ECSタスクの最新リビジョン取得 StepFunctions ->> ECS: ECSタスク実行 ECS ->> ECR: docker pull ECS ->> ECS: コンテナ実行 ECS ->> Servers: 各種コマンド実行 Note left of ECS: git pushを契機に自動的にECSタスクを
起動してコマンドを実行 end

導入方法

1. OIDC providerを作成する@AWSアカウント

  1. 対応策:PRを契機に自動的にterraform planを実行するで作成済みのものを使用します。

2. IAMロールを作成する@AWSアカウント

  1. GitHub ActionsにてAssumeRoleするIAMロール(DB_MGR_ROLE)を作成する
    • 今回は以下のように設定しました。

      ※ 各種ポリシーの内容は必要に応じて絞り込みや追加を行う必要があります。

      信頼ポリシー
      ECSタスクに関連づけるIAMロールの信頼ポリシー
      {
      "Version": "2012-10-17",
      "Statement": [
      {
      "Effect": "Allow",
      "Principal": {
      "AWS": "${AWS_ROLEのARN}"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
      "StringEquals": {
      "sts:ExternalId": "${外部ID}"
      }
      }
      }
      ]
      }
      許可ポリシー
      ECSタスクに関連づけるIAMロールの許可ポリシー
      {
      "Version": "2012-10-17",
      "Statement": [
      {
      "Sid": "PublishEcsLogs",
      "Effect": "Allow",
      "Action": [
      "logs:CreateLogStream",
      "logs:CreateLogGroup",
      "logs:PutLogEvents"
      ],
      "Resource": [
      "arn:aws:logs:ap-northeast-1:${AWSAccountId}:log-group:/ecs/${TaskDefinitionName}:*"
      ]
      },
      {
      "Sid": "ExecuteStepFunctions",
      "Effect": "Allow",
      "Action": [
      "states:ListStateMachines",
      "states:DescribeStateMachine",
      "states:StartExecution",
      "states:DescribeExecution"
      ],
      "Resource": "*"
      },
      {
      "Sid": "EcrPush",
      "Effect": "Allow",
      "Action": [
      "ecr:CompleteLayerUpload",
      "ecr:UploadLayerPart",
      "ecr:InitiateLayerUpload",
      "ecr:BatchCheckLayerAvailability",
      "ecr:PutImage",
      "ecr:BatchGetImage"
      ],
      "Resource": "arn:aws:ecr:ap-northeast-1:${AWSAccountId}:repository/${RepositoryName}"
      },
      {
      "Sid": "EcrLogin",
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
      }
      ]
      }

3. 作成したIAMロールの情報をGitHubリポジトリーのシークレットに登録する@GitHub Repository

  1. 各種IAMロールのARNと外部IDについて、以下のGitHubリポジトリーのシークレットに設定する
    • DB_MGR_ROLE_ARN:AWS_MGR_ROLEのARN
    • DB_MGR_ROLE_EXTERNAL_ID:AWS_MGR_ROLEの外部ID

4. ECSタスクを起動するVPCを作成する@AWSアカウント

  1. ECSタスクを起動するVPC・サブネットを作成する
    • 前提として、VPCやサブネット、Aurora等のデータベースサーバーが存在している必要があります。
    • ECSタスクで使用するコンテナレジストリにアクセスするための経路が必要になります。
  2. ECSタスクに関連付けるセキュリティグループを作成する
    • ECSタスクからコマンドを実行する対象のデータベースサーバーにアクセスできるようにする必要があります。

5. ECS関連リソースを作成する@AWSアカウント

  1. ECRリポジトリーを作成する

    • 今回はDBマイグレーションを想定しているので、dev/db-migrateというリポジトリーを作成しました。

    • latest運用を想定しているため、ミュータブルとしています。

      • latest運用とは、コンテナイメージを変更する際常にlatestタグを付与することで、ECSタスク定義を更新せずとも常に最新のコンテナイメージを使用する運用を指しています。

      ecr_repository

  2. ECSクラスターを作成する

    • 今回は、ECSクラスターのインフラとしてFargateを使用することとしました。

    ecs_cluster

  3. ECSタスクに関連づけるIAMロールを作成する

    信頼ポリシー
    ECSタスクに関連づけるIAMロールの信頼ポリシー
    {
    "Version": "2012-10-17",
    "Statement": [
    {
    "Sid": "",
    "Effect": "Allow",
    "Principal": {
    "Service": "ecs-tasks.amazonaws.com"
    },
    "Action": "sts:AssumeRole"
    }
    ]
    }
    許可ポリシー
    AmazonEC2ContainerRegistryReadOnly
    AmazonECSTaskExecutionRolePolicy
    RDSのDBユーザーのパスワードをシークレットマネージャで管理する場合には許可ポリシーを追加
    シークレットマネージャアクセス用ポリシー
    {
    "Version": "2012-10-17",
    "Statement": [
    {
    "Effect": "Allow",
    "Action": [
    "secretsmanager:GetSecretValue"
    ],
    "Resource": "*" # 必要に応じて制限する
    }
    ]
    }
  4. ECSタスクを作成する

    • ECRリポジトリーdev/db-migratelatestタグのイメージを実行するECSタスクを作成します。

    ecs_task

6. ステートマシンを作成する@AWSアカウント

  1. ステートマシンに関連づけるIAMロールを作成する

    信頼ポリシー
    ステートマシンに関連づけるIAMロールの信頼ポリシー
    {
    "Version": "2012-10-17",
    "Statement": [
    {
    "Sid": "",
    "Effect": "Allow",
    "Principal": {
    "Service": "states.amazonaws.com"
    },
    "Action": "sts:AssumeRole"
    }
    ]
    }
    許可ポリシー
    ステートマシンに関連づけるIAMロールの許可ポリシー
    {
    "Version": "2012-10-17",
    "Statement": [
    {
    "Effect": "Allow",
    "Action": [
    "ecs:RunTask",
    "ecs:StopTask",
    "ecs:DescribeTasks",
    "ecs:DescribeTaskDefinition"
    ],
    "Resource": "*"
    },
    {
    "Effect": "Allow",
    "Action": [
    "events:PutTargets",
    "events:PutRule",
    "events:DescribeRule"
    ],
    "Resource": [
    "arn:aws:events:ap-northeast-1:${AWSAccountId}:rule/StepFunctionsGetEventsForECSTaskRule"
    ]
    },
    {
    "Effect": "Allow",
    "Action": [
    "iam:PassRole"
    ],
    "Resource": [
    "arn:aws:iam::${AWSAccountId}:role/${ステートマシンに関連づけるIAMロール名}"
    ]
    }
    ]
    }
  2. ステートマシンを作成する

    ステートマシン定義
    ステートマシン定義
    {
    "Comment": "A description of my state machine",
    "StartAt": "DescribeTaskDefinition",
    "States": {
    "DescribeTaskDefinition": {
    "Type": "Task",
    "Arguments": {
    "TaskDefinition": "arn:aws:ecs:ap-northeast-1:${AWSAccountId}:task-definition/${ECSタスク名}"
    },
    "Resource": "arn:aws:states:::aws-sdk:ecs:describeTaskDefinition",
    "Next": "ECS RunTask",
    "Assign": {
    "ecsTaskDefArn": "{% $states.result.TaskDefinition.TaskDefinitionArn %}"
    }
    },
    "ECS RunTask": {
    "Type": "Task",
    "Resource": "arn:aws:states:::ecs:runTask.sync",
    "End": true,
    "Arguments": {
    "LaunchType": "FARGATE",
    "Cluster": "arn:aws:ecs:ap-northeast-1:${AWSAccountId}:cluster/${ECSクラスター名}",
    "TaskDefinition": "{% $ecsTaskDefArn %}",
    "NetworkConfiguration": {
    "AwsvpcConfiguration": {
    "AssignPublicIp": "ENABLED", # コンテナリポジトリにアクセスするために付与しているが、NAT GatewayやVPC Endpointでアクセスできる場合には不要
    "SecurityGroups": [
    "${ECSタスクに関連付けるSGのID}"
    ],
    "Subnets": [
    "${ECSタスクを配置するサブネットのID}"
    ]
    }
    }
    }
    }
    },
    "QueryLanguage": "JSONata"
    }

7. DBマイグレーション用AWSリソースの情報をGitHubリポジトリーのシークレットに登録する@GitHub Repository

  1. ECSとStepFunctions関連の情報について、以下のGitHubリポジトリーのシークレットに設定する
    • DB_MGR_ECR_IMAGE_TAG:latest
    • DB_MGR_ECR_REGISTRY:ECRレジストリURL (${AWSAccountId}.dkr.ecr.${Region}.amazonaws.com)
    • DB_MGR_ECR_REPOSITORY:ECRリポジトリー名
    • DB_MGR_SFN_ARN:ステートマシンのARN

8. StepFunctionsを実行するワークフローを作成する@GitHub Actions

  • 以下を参考に、コンテナイメージの準備とStepFunction実行自動化を行います。

  • 今回は、以下のように設定しました。

    .github/workflows/db_migrate.yml
    .github/workflows/db_migrate.yml
    name: DB migration
    on:
    push:
    branches:
    - main
    path:
    - 'environments/dev/docker/**'
    env:
    AWS_DEFAULT_REGION: ap-northeast-1
    permissions:
    id-token: write
    contents: read
    jobs:
    tf_plan:
    name: DB Migration
    runs-on: ubuntu-latest
    strategy:
    matrix:
    build_context:
    - ./environments/dev/docker
    steps:
    - name: Git checkout
    uses: actions/checkout@v4
    with:
    ref: ${{ github.event.pull_request.head.sha }}
    - name: Get aws credential
    uses: aws-actions/configure-aws-credentials@v4
    with:
    role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
    role-session-name: iar-gha-con-${{ github.run_id }}
    aws-region: ${{ env.AWS_DEFAULT_REGION }}
    - run: aws sts get-caller-identity
    - name: Switch to db-migrate role
    uses: aws-actions/configure-aws-credentials@v4
    with:
    role-to-assume: ${{ secrets.DB_MGR_ROLE_ARN }}
    role-external-id: ${{ secrets.DB_MGR_ROLE_EXTERNAL_ID }}
    role-session-name: iar-gha-mgr-${{ github.run_id }}
    role-chaining: true
    aws-region: ${{ env.AWS_DEFAULT_REGION }}
    role-skip-session-tagging: true
    - run: aws sts get-caller-identity
    - name: Set up Docker Buildx
    uses: docker/setup-buildx-action@v3
    with:
    install: true
    - name: Login to Amazon ECR
    uses: aws-actions/amazon-ecr-login@v2
    - name: Build and push docker iamge
    uses: docker/build-push-action@v2
    with:
    context: ${{ matrix.build_context }}
    push: true
    no-cache: true
    tags: ${{ secrets.DB_MGR_ECR_REGISTRY }}/${{ secrets.DB_MGR_ECR_REPOSITORY }}:${{ secrets.DB_MGR_ECR_IMAGE_TAG }}
    - name: Call aws stepfunctions
    uses: ./.github/actions/call_aws_step_functions
    with:
    aws_sfn_arn: ${{ secrets.DB_MGR_SFN_ARN }}
    .github/actions/call_aws_step_functions/action.yml
    .github/actions/call_aws_step_functions/action.yml
    name: Build and push docker image
    inputs:
    aws_sfn_arn:
    required: true
    type: string
    runs:
    using: composite
    steps:
    - name: Execute StepFunctions
    shell: bash
    run: |
    SFN_EXEC_ARN=$(aws stepfunctions start-execution \
    --state-machine-arn "$STATE_MACHINE" \
    --no-cli-pager \
    | jq -r '.executionArn')
    if [ -z "$SFN_EXEC_ARN" ]; then
    echo "Error: 実行 ARN の取得に失敗しました。"
    exit 1
    fi
    echo "Step Functions 実行開始: $SFN_EXEC_ARN"
    while true; do
    STATUS=$(aws stepfunctions describe-execution \
    --execution-arn "$SFN_EXEC_ARN" --no-cli-pager \
    | jq -r '.status')
    if [ "$STATUS" != "RUNNING" ]; then
    break
    fi
    sleep 5
    done
    echo "実行完了。最終ステータス: $STATUS"
    env:
    STATE_MACHINE: ${{ inputs.aws_sfn_arn }}

※ dockerコンテナのbuild cacheについて
今回は運用時に必要なワンショットのコマンド実行が目的の、ビルドに長時間を要さないコンテナを想定しています。
そこで、build cacheの有無でビルド結果が変わることを避けるため、build cacheを使用しない設定としました。
ECRを用いたremote cacheには料金がかかりますし、GitHubのキャッシュ機能は期限を過ぎると削除される仕様です。
ビルドに時間がかかりコストとメリットのバランスが取れる場合には、何らかの方法でbuild cacheを導入することが推奨されます。

使用感

GitHub Actionsワークフロー実行ログ

db_migrate_workflow

StepFunctions実行ログ

db_migrate_ecs_sfn_result

JavascriptのORMツールであるPrismaを用いて、ECSタスクからAuroraの情報を取得する例

db_migrate_ecs_task_result

効果

本対応策を導入すると、コンテナで実行するよう事前に記述したコマンドのみが実行されます。
結果として、踏み台サーバーの維持コストやオペレーションミスのリスクを軽減することができます。

おわりに

この記事では、GitHub Actionsを用いたCI/CDによるインフラ構築・運用の省力化についてご紹介しました。

PR-Agentの導入によって、AIによるレビューを手軽に導入することができます。
PRのテンプレートを充実させることやコーディング規約をチームに浸透させることも重要ですが、別視点でのアプローチとして有効であると感じています。

terraform planの自動実行は非常に便利で、手動実行にはもう戻れないと感じています。
「いざterraform applyを実行したところでエラーが発覚し、手戻りが発生してしまった」といった事態が起きにくくなります。

ECSタスク起動によるコマンドの実行については、セキュリティ強化やオペレーションミスへの有効な対策になります。
「作業者の判断で手順書外のオペレーションを実施し、システム障害を招いてしまった」といった事態が非常に起きにくくなります。

CI/CDの仕組みが整っていないケースは少ないかもしれませんが、この記事が快適な開発体験の一助となれば幸いです。


TOP
アルファロゴ 株式会社アルファシステムズは、ITサービス事業を展開しています。このブログでは、技術的な取り組みを紹介しています。X(旧Twitter)で更新通知をしています。