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_REGION | AWSリージョン | すべての AWS CLI コマンドで利用 |
TEMPLATE_DIR | テンプレート格納ディレクトリのルート | CloudFormation テンプレートの検索対象ディレクトリ |
③ 環境から引き継がれる変数(buildspec外で設定されている)
スクリプト内で参照されているが、env: セクション内には定義されていない変数。
| 変数名 | 説明 | 定義場所想定 |
|---|---|---|
Environment | デプロイ環境名(例: prod / stg/ dev) | CodePipeline から Environment パラメータとして渡される想定 |
AWS::AccountId / AWS::Region | これは CloudFormation 内部用変数であり、ここでは直接使われていない | – |
④ スクリプト内で定義されている変数
ビルドフェーズ中に Bash の中で生成・使用されている変数です。
| 変数名 | 定義箇所 | 説明 |
|---|---|---|
CHANGED_TEMPLATES | pre_build | find "$TEMPLATE_DIR" -name "*.yaml" の結果(全テンプレート一覧) |
ANY_ERROR | build | どこかの stack deploy が失敗したかを記録するフラグ |
PARAMS_DIR | build | cfn/params/$Environment (環境ごとのパラメータ格納ディレクトリ) |
file | build | while ループ内の1テンプレートファイルパス |
FILENAME | build | $file から basename(ファイル名のみ)を抽出したもの |
CLEAN_FILENAME | build | 01-vpc.yaml → vpc.yaml のように先頭番号を除去したファイル名 |
STACK_NAME | build | スタック名。例:${Environment}-vpc |
PARAM_FILE | build | $PARAMS_DIR/params.json へのフルパス |
TEMPLATE_CHANGED | build | CloudFormation上のテンプレートとローカルとの差分があるか(0/1) |
PARAMS_CHANGED | build | パラメータ差分があるか(0/1) |
CURRENT_PARAMS | build | aws cloudformation describe-stacks で取得した現在のスタックパラメータ |
USED_KEYS | build | yq でテンプレートから抽出したパラメータキー配列(JSON形式) |
FILTERED_DESIRED_PARAMS | build | 実際にテンプレートで使われているキーに絞った params.json |
PARAM_OVERRIDES | build | deploy時に --parameter-overrides に渡す文字列 |
/tmp/template_list.txt | build | 処理対象テンプレートの一覧を一時保存 |
/tmp/current_template.yaml | build | 現在のスタックテンプレートを AWS から取得して比較するための一時ファイル |
/tmp/ANY_ERROR | build/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フェーズをまたいで変数を利用することはできないので、一時ファイルを使用しています。
