AWS CloudFormationテンプレートの静的チェックをcfn-lintで行う
こんにちは、馬場です!
業務でAWS CloudFormationのテンプレートを運用目的で多数管理しているのですが、各開発者のテストに依存していたため、せめて静的チェックくらいしておこうとなり、色々と検討と検証を行いました。
その備忘と共有を兼ねて記事を作成します。
1. はじめに
AWS CloudFormationテンプレートの静的チェックには、cfn-lint を使った検証が一般的なようでした。
pre-commit での cfn-lint
最初は、Gitの pre-commit
を使って、ローカル環境でLintを実行する方法を検討していました。しかし、複数の開発者が関わるプロジェクトにおいて、いくつかの問題が浮かび上がりました。
- 統一性の欠如: 各開発者がローカルで設定を行う必要があり、設定のミスやバージョンの違いが発生する可能性がありました。また、そもそも手順を実行するかどうかが不明であることも避けたい要因となりました。
-
開発環境の構築の手間:
pre-commit
を使うためには開発者各自が適切な設定を行う必要があり、初期設定の段階で手間がかかりました。
これらの理由から、より統一された環境でチェックを行えるよう、GitHub Actionsを使用することにしました。
(pre-commit時のチェックは以下を参考にして簡単に実現できました!)
2. GitHub ActionsでのLint検証のメリット
- 一貫した設定: すべての開発者が同じCI/CDパイプラインを使用するため、設定が統一される。
- 環境構築不要: 開発者が個々に環境を構築する必要がなく、リポジトリにプッシュするだけでLint検証が行われる。
- CI/CDパイプラインとの統合: コードの変更がプッシュされるたびに自動でLint検証が行われるため、コードの品質が常に保たれる。
上記メリットにより、
誰がやっても同じテストを必ず通る
ことが担保されるため、GitHub Actionsでのチェックを実施することにしました。
3. 実装方法
以下は実装例です。
name: lint CFn template file
on:
pull_request:
types: [opened, synchronize]
branches:
- main
paths:
- "sample_dir/*"
jobs:
cfn-lint:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Set fetch-depth dynamically
run: echo "DEPTH=$(( commits + 1 ))" >> $GITHUB_ENV
env:
commits: ${{ github.event.pull_request.commits }}
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: ${{ env.DEPTH }}
- name: Fetch base branch explicitly
run: git fetch origin ${{ github.base_ref }} --depth=${{ env.DEPTH }}
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install cfn-lint
run: pip install cfn-lint
- name: Get changed CloudFormation templates
id: changed-files
run: |
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }} HEAD -- '*.yaml' '*.yml' '*.json' | tr '\n' ' ')
if [ -z "$CHANGED_FILES" ]; then
echo "No changed CloudFormation templates detected."
exit 0
fi
echo "Changed files:"
echo "$CHANGED_FILES"| tr ' ' '\n'
echo "---------------------------------------"
echo "CHANGED_FILES=$CHANGED_FILES" >> $GITHUB_ENV
- name: Run cfn-lint on changed files
if: env.CHANGED_FILES != ''
run: |
for file in $CHANGED_FILES; do
echo "Linting $file"
cfn-lint $file
done
ここからポイントについて解説していきます
fetch-depth
の動的設定
3.1 まず、プルリクエストのコミット数を基にfetch-depth
を動的に設定しました。
fetch-depth: 0
とすると範囲が広すぎになってしまうことが懸念されたので、必要な分だけ動的に取ってくる形にしています。
これにより、効率的に解析できるようになっているはずです。
また、検証をしている最中に Not a valid object name origin/main
に遭遇し、原因としてfetch-depth: で指定するもうまくリモートブランチがfetchされていないようだったので、明示的に git fetch
を実施しています。
- name: Set fetch-depth dynamically
run: echo "DEPTH=$(( commits + 1 ))" >> $GITHUB_ENV
env:
commits: ${{ github.event.pull_request.commits }}
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: ${{ env.DEPTH }}
- name: Fetch base branch explicitly
run: git fetch origin ${{ github.base_ref }} --depth=${{ env.DEPTH }}
3.2 変更されたファイルの検出
PRのopen時と修正のcommitがあった際に git diff
を使って、変更されたCloudFormationテンプレートファイル(*.yaml
, *.yml
, *.json
)を取得しています。
(reopenedも可能性が0ということは無いですが、今回は考慮していません。)
任意のディレクトリ以下にテンプレートを配置しているので paths:
でCFnテンプレートの配置場所のみをチェックして誤作動が無いようにしています。
また、チェック時には git diff
はそもそも重複削除が効いているので特に何もせず、PR作成までの差分のあった全ファイルが取得できるようにしています。
Unable to process file command 'env' successfully. というエラーが発生しており、詳しく調べた所、変数に複数行の値を入れようとすると怒られることが分かりました。そのため、改行をスペースに変換して環境変数に設定することで、複数行のファイルリストを正しく処理できるようにしています。
pull_request:
types: [opened, synchronize]
branches:
- main
paths:
- "sample_dir/*"
~~~~~~~~~~~~~~~~中略~~~~~~~~~~~~~~~~~~~~~~~
- name: Get changed CloudFormation templates
id: changed-files
run: |
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }} HEAD -- '*.yaml' '*.yml' '*.json')
echo "Changed files:"
echo "$CHANGED_FILES"
echo "---------------------------------------"
echo "CHANGED_FILES=$CHANGED_FILES | tr '\n' ' '" >> $GITHUB_ENV
3.3 Lintの実行
最後に、変更されたファイルに対してcfn-lint
でLint検証を行っています。
- name: Run cfn-lint on changed files
if: env.CHANGED_FILES != ''
run: |
for file in $CHANGED_FILES; do
echo "Linting $file"
cfn-lint $file
done
4. まとめ
今回、cfn-lint
を導入したことでCFnテンプレートの品質向上を実現し自動かつ同一の静的チェックを行うことができるようになりました。
また pre-commit
フックからGitHub Actions
に切り替えたことで、私たちのチームでは開発者個人の追加コスト無しで、静的チェックをCI/CDフローに組み込むことができています。
もし、AWS CloudFormationテンプレートを使用しているプロジェクトがあれば、手法はお任せしますが、cfn-lintを利用した静的チェックをお勧めします。
Discussion
pre-commit はローカルでもリモートでも同じように実行しておくことで、環境差(設定やバージョンなど)やすり抜けを防止できるので、depthをつけたり pre-commit と違うことをさせたりするのはもったいない。。。
pre-commit の action もあるので、 .pre-commit-config.yaml を置いたまま常にリポジトリ内まるごとチェックさせるほうがいいと思う。