AWSリソースをCI/CDでデプロイする

tech article

CloudFormationテンプレートをCI/CDでデプロイできるようにしてみました。
ネット上で調べた限りではベストプラクティス的なものが見当たらなかったのでかなり我流というか、スクリプト頼りになっているかと思います。

もっとこうしたらいいんじゃない?という改善案、知見、アドバイス等あればぜひ教えてほしいです。

ポイント

  • CloudFormationはoutputsで各スタック間の変数を利用できるのが長所なので、デプロイする順序を制御できるようにしました。
  • 後からデプロイ順を変更できるように、スタック名とテンプレートファイル名を区別するようにしました。
  • 新たにファイルが追加された場合か、既存のスタックに対してParametersが更新された場合のみスタックを更新し、それ以外の場合はスキップするようにしました。

① buildspec.yaml

version: 0.2
env:
  variables:
    AWS_REGION: ap-northeast-1
    TEMPLATE_DIR: cfn/templates/automatic
 
phases:
  install:
    commands:
      - echo "Installing dependencies..."
      - yum install -y jq curl || (apt-get update && apt-get install -y jq curl)
      # yq を追加インストール(yq v4 系を使用)
      - curl -L https://github.com/mikefarah/yq/releases/download/v4.44.1/yq_linux_amd64 -o /usr/local/bin/yq
      - chmod +x /usr/local/bin/yq
  pre_build:
    commands:
      - echo "Detecting all templates in $TEMPLATE_DIR..."
      - export CHANGED_TEMPLATES=$(find "$TEMPLATE_DIR" -name "*.yaml")
      - echo "Templates to check:"
      - echo "$CHANGED_TEMPLATES"
      - |
        if [ -z "$CHANGED_TEMPLATES" ]; then
          echo "No templates found. Skipping build phase."
          exit 0
        fi
 
  build:
    commands:
      - echo "デプロイ対象環境は ${Environment} です"
      - |
        ANY_ERROR=0
        export PARAMS_DIR="cfn/params/$Environment"
        rm -f /tmp/ANY_ERROR || true
        find "$TEMPLATE_DIR" -name "*.yaml" | sort > /tmp/template_list.txt || true
        while IFS= read -r file; do
          echo "Processing file: $file"
          FILENAME=$(basename "$file")
          # プレフィックス(先頭からハイフンまで)を除去してスタック名を作成
          CLEAN_FILENAME=$(echo "$FILENAME" | sed 's/^[^-]*-//')
          STACK_NAME="${Environment}-${CLEAN_FILENAME%.*}"
          PARAM_FILE="$PARAMS_DIR/params.json"
          cat $PARAM_FILE
          echo "Checking stack: $STACK_NAME"
 
          aws cloudformation get-template \
            --region "$AWS_REGION" \
            --stack-name "$STACK_NAME" \
            --query 'TemplateBody' \
            --output text > /tmp/current_template.yaml || echo "" > /tmp/current_template.yaml
          TEMPLATE_CHANGED=0
          PARAMS_CHANGED=0
 
          # テンプレート差分チェック
          if ! diff -q "$file" /tmp/current_template.yaml > /dev/null; then
            TEMPLATE_CHANGED=1
          fi
 
          # パラメータ差分チェック(使用されているキーのみ比較)
          if [ -f "$PARAM_FILE" ]; then
            CURRENT_PARAMS=$(aws cloudformation describe-stacks \
              --region "$AWS_REGION" \
              --stack-name "$STACK_NAME" \
              --query "Stacks[0].Parameters" \
              --output json 2>/dev/null || echo "[]")
 
            # yqでParametersキーを抽出
            USED_KEYS=$(yq -r '.Parameters | keys | @json' "$file")
            echo "USED_KEYS: $USED_KEYS"
 
            FILTERED_DESIRED_PARAMS=$(jq -c --argjson keys "$USED_KEYS" '
              to_entries
              | map(select(.key as $k | $keys | index($k)))
              | map({ParameterKey: .key, ParameterValue: .value})
            ' "$PARAM_FILE" 2>/dev/null || echo "[]")
 
            if [ "$CURRENT_PARAMS" != "$FILTERED_DESIRED_PARAMS" ]; then
              PARAMS_CHANGED=1
            fi
          fi
 
          if [ "$TEMPLATE_CHANGED" -eq 1 ] || [ "$PARAMS_CHANGED" -eq 1 ]; then
            echo "Changes detected. Deploying stack: $STACK_NAME"
 
            # パラメータ整形(使用されているキーのみ)
            if [ -f "$PARAM_FILE" ]; then
              echo "Using parameters from $PARAM_FILE"
              PARAM_OVERRIDES=$(jq -r --argjson keys "$USED_KEYS" '
                to_entries
                | map(select(.key as $k | $keys | index($k)))
                | map("\(.key)=\(.value)")
                | join(" ")
              ' "$PARAM_FILE" 2>/dev/null || echo "")
              echo "PARAM_OVERRIDES: $PARAM_OVERRIDES"
            else
              echo "No parameter file found. Proceeding without parameters."
              PARAM_OVERRIDES=""
            fi
 
            # デプロイ
            if [ -n "$PARAM_OVERRIDES" ]; then
              if ! aws cloudformation deploy \
                   --region "$AWS_REGION" \
                   --template-file "$file" \
                   --stack-name "$STACK_NAME" \
                   --parameter-overrides $PARAM_OVERRIDES \
                   --capabilities CAPABILITY_NAMED_IAM; then
                echo "[ERROR] Deploy failed for stack: $STACK_NAME" >&2
                ANY_ERROR=1
                continue
              fi
            else
              if ! aws cloudformation deploy \
                   --region "$AWS_REGION" \
                   --template-file "$file" \
                   --stack-name "$STACK_NAME" \
                   --capabilities CAPABILITY_NAMED_IAM; then
                echo "[ERROR] Deploy failed for stack: $STACK_NAME" >&2
                ANY_ERROR=1
                continue
              fi
            fi
            echo "[OK] Deployed: $STACK_NAME"
          else
            echo "No changes detected for $STACK_NAME. Skipping."
          fi
        done < /tmp/template_list.txt
 
        # フェーズをまたいで変数を共有するために一時ファイルを使用
        if [ $ANY_ERROR -ne 0 ]; then
          touch /tmp/ANY_ERROR
        fi
 
  post_build:
    commands:
      - echo "Finished deploying changed templates."
      - |
        if [ -f /tmp/ANY_ERROR ]; then
          echo "[SUMMARY] Some stacks failed to deploy." >&2
          exit 1
        else
          echo "[SUMMARY] All target stacks processed successfully."
        f

② CodeBuild の環境変数(env: セクションで定義)

変数名意味備考
AWS_REGIONAWSリージョンすべての AWS CLI コマンドで利用
TEMPLATE_DIRテンプレート格納ディレクトリのルートCloudFormation テンプレートの検索対象ディレクトリ

③ 環境から引き継がれる変数(buildspec外で設定されている)

スクリプト内で参照されているが、
env: セクション内には定義されていない変数。

変数名説明定義場所想定
Environmentデプロイ環境名(例: prod / stg/ dev)CodePipeline から Environment パラメータとして渡される想定
AWS::AccountId / AWS::Regionこれは CloudFormation 内部用変数であり、ここでは直接使われていない

④ スクリプト内で定義されている変数

ビルドフェーズ中に Bash の中で生成・使用されている変数です。

変数名定義箇所説明
CHANGED_TEMPLATESpre_buildfind "$TEMPLATE_DIR" -name "*.yaml" の結果(全テンプレート一覧)
ANY_ERRORbuildどこかの stack deploy が失敗したかを記録するフラグ
PARAMS_DIRbuildcfn/params/$Environment (環境ごとのパラメータ格納ディレクトリ)
filebuildwhile ループ内の1テンプレートファイルパス
FILENAMEbuild$file から basename(ファイル名のみ)を抽出したもの
CLEAN_FILENAMEbuild01-vpc.yamlvpc.yaml のように先頭番号を除去したファイル名
STACK_NAMEbuildスタック名。例:${Environment}-vpc
PARAM_FILEbuild$PARAMS_DIR/params.json へのフルパス
TEMPLATE_CHANGEDbuildCloudFormation上のテンプレートとローカルとの差分があるか(0/1)
PARAMS_CHANGEDbuildパラメータ差分があるか(0/1)
CURRENT_PARAMSbuildaws cloudformation describe-stacks で取得した現在のスタックパラメータ
USED_KEYSbuildyq でテンプレートから抽出したパラメータキー配列(JSON形式)
FILTERED_DESIRED_PARAMSbuild実際にテンプレートで使われているキーに絞った params.json
PARAM_OVERRIDESbuilddeploy時に --parameter-overrides に渡す文字列
/tmp/template_list.txtbuild処理対象テンプレートの一覧を一時保存
/tmp/current_template.yamlbuild現在のスタックテンプレートを AWS から取得して比較するための一時ファイル
/tmp/ANY_ERRORbuild/post_buildエラー発生時に touch され、post_build で検知するためのファイル

⑤ 参照するファイル

cfn/
├── params/
│   ├── prod/
│   │   └── params.json
│   ├── stg/
│   │   └── params.json
│   └── dev/
│       └── params.json
├── templates/
│   ├── automatic/
│   │   ├── 01-vpc.yaml
│   │   ├── 02-securitygroup.yaml
│   │   ├── ...
│   │   └── xx-xxxx.yaml
└── buildspec.yml

⑥ buildspecのポイント解説

        find "$TEMPLATE_DIR" -name "*.yaml" | sort > /tmp/template_list.txt || true

$TEMPLATE_DIR配下に配置されている各テンプレートファイルをソートして/tmp/template_list.txtに保持します。

        while IFS= read -r file; do
        ...
        done < /tmp/template_list.txt

/tmp/template_list.txt に書かれたファイルパスを1行ずつ読み込み、
その1つ1つに対してループ内の処理(CloudFormationデプロイなど)を実行します。

          STACK_NAME="${Environment}-${CLEAN_FILENAME%.*}"

Bash の「パターン削除」構文

${var%pattern}
末尾から 最短一致 する pattern を削除
${file%.*} → example(example.yaml → example)

            # yqでParametersキーを抽出
            USED_KEYS=$(yq -r '.Parameters | keys | @json' "$file")
            echo "USED_KEYS: $USED_KEYS"

テンプレートファイルからParametersのキーだけを抽出しています。
ここをjqで処理しようとしたのですが、うまくいかなかったので泣く泣くyqを導入することにしました。

            FILTERED_DESIRED_PARAMS=$(jq -c --argjson keys "$USED_KEYS" '
              to_entries
              | map(select(.key as $k | $keys | index($k)))
              | map({ParameterKey: .key, ParameterValue: .value})
            ' "$PARAM_FILE" 2>/dev/null || echo "[]")

params.json の中からCloudFormation テンプレートで使っているキー (USED_KEYS) だけを取り出し、
ParameterKey / ParameterValue 形式に整形してJSON 配列にしています

params.jsonには、各テンプレートファイルで利用するParametersを一括でまとめて記載しています。

        # フェーズをまたいで変数を共有するために一時ファイルを使用
        if [ $ANY_ERROR -ne 0 ]; then
          touch /tmp/ANY_ERROR
        fi

フェーズをまたいで変数を利用することはできないので、一時ファイルを使用しています。

タイトルとURLをコピーしました