🤖

AWS CloudFormationテンプレートの静的チェックをcfn-lintで行う

2025/03/03に公開1

こんにちは、馬場です!
業務でAWS CloudFormationのテンプレートを運用目的で多数管理しているのですが、各開発者のテストに依存していたため、せめて静的チェックくらいしておこうとなり、色々と検討と検証を行いました。
その備忘と共有を兼ねて記事を作成します。

1. はじめに

AWS CloudFormationテンプレートの静的チェックには、cfn-lint を使った検証が一般的なようでした。
https://github.com/aws-cloudformation/cfn-lint

pre-commit での cfn-lint

最初は、Gitの pre-commit を使って、ローカル環境でLintを実行する方法を検討していました。しかし、複数の開発者が関わるプロジェクトにおいて、いくつかの問題が浮かび上がりました。

  • 統一性の欠如: 各開発者がローカルで設定を行う必要があり、設定のミスやバージョンの違いが発生する可能性がありました。また、そもそも手順を実行するかどうかが不明であることも避けたい要因となりました。
  • 開発環境の構築の手間: pre-commitを使うためには開発者各自が適切な設定を行う必要があり、初期設定の段階で手間がかかりました。

これらの理由から、より統一された環境でチェックを行えるよう、GitHub Actionsを使用することにしました。

(pre-commit時のチェックは以下を参考にして簡単に実現できました!)
https://aws.amazon.com/jp/blogs/news/git-pre-commit-validation-of-aws-cloudformation-templates-with-cfn-lint/

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

ここからポイントについて解説していきます

3.1 fetch-depthの動的設定

まず、プルリクエストのコミット数を基に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を利用した静的チェックをお勧めします。

DELTAテックブログ

Discussion

rakiraki

pre-commit はローカルでもリモートでも同じように実行しておくことで、環境差(設定やバージョンなど)やすり抜けを防止できるので、depthをつけたり pre-commit と違うことをさせたりするのはもったいない。。。
pre-commit の action もあるので、 .pre-commit-config.yaml を置いたまま常にリポジトリ内まるごとチェックさせるほうがいいと思う。

https://github.com/pre-commit/action

https://pre-commit.com/#usage-in-continuous-integration