のぴぴのメモ

自分用のLinuxとかの技術メモ

GitHub ActionsによるTerraform実行環境(更新ディレクトリ特定+checkovによるセキュリティチェック付き)

はじめに

GitHub ActionsによるTerraformのCIサンプルです。
実際のコードはGitHubのリポジトリを参照ください。
このサンプルの特徴は以下の通りです。

  • OpenID Connect(OIDC)でのAWS認証によるActions実行により、アクセスキー&シークレットキー管理が不要
  • Terraformのデファクトとなっているディレクトリ構成で作成
  • その上でActionsでは、更新があったディレクトリを特定し、そのディレクトリのみTerraformを実行
  • CIに、IaCコードの静的解析を行うcheckovを組み込み、Pull Requestで自動実行(最近流行りの、DevSecOpsぽいものを実現)


terrafrom/envs/xxxのように環境面でTerraform実行先が固定されている場合(実行面数が増加しない場合)は、こんな面倒なやり方をせずワークフローを分けてon:で、paths:条件で実行する構成にした方がシンプルで望ましいです。
一方で、アカウントやプロジェクトなどTerraform実行ディレクトリが単純増加するユースケースで、GitHub ActionsでTerraformを実行したいケースでは、このサンプルのワークフローが効果を発揮すると思います。

前提とするTerraformのディレクトリ構成

terraformのモジュールの標準的なディレクトリ構成を前提としています。
/terraform/accounts/配下にそれぞれのアカウント単位でterrafomを管理するイメージです。
アカウントを追加する場合は、_templateフォルダを任意の名称でコピーして適切に設定を変更してterraformでapplyします。

.
└── terraform
    ├── accounts
    │   ├── _template
    │   ├── user-a
    │   │   ├── backend.tf
    │   │   ├── main.tf
    │   │   ├── provider.tf
    │   │   └── terraform.tf
    │   ├── user-b
    │   └── user-c
    └── modules
        └── xxxxx

ActionsからのAWS認証

ActionsからAWS環境にアクセスする際の認証には、OIDCを利用しています。OIDC利用の詳細は下記ドキュメントを参照ください。

Actionsのワークフローコード

実際のコードはこちらのワークフローのYAMLファイルを確認ください。
ジョブは、(1)更新ディレクトリ特定ジョブと、(2)Terraform実行ジョブ、の2つで構成されています。

更新ディレクトリ特定ジョブ

シェル芸の塊です。ここではざっくりとは以下の処理をしています。

  1. git logによる更新一覧取得
  2. sedawkuniqなどを駆使して、更新ディレクトリ情報のみ抽出
  3. 抽出した情報をjpコマンドでJSONに変換
  4. 変換したJSONは、次のTerraform実行ジョブでのmatrixに利用

具体的な処理の中身については、GitHubのREADMEこちらの記事を参照ください。

 detect_dirs:
    name: "Detect modified directories"
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash
    outputs:
      TARGET_DIR: ${{ steps.detectddir.outputs.TARGET_DIR }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v2
      - name: Detect modified project directories
        id: detectddir
        run: |
          # git fetch
          echo "::group::git fetch"
          TARGET_BRANCH="${{ github.base_ref }}"  
          echo "TAGET_BRANCE = ${TARGET_BRANCH}"
          git fetch --depth 1 origin ${TARGET_BRANCH}

          # If modules is changed, execute terraform to all .
          echo "::group::check modules directory"
          LINES=$( git diff origin/${TARGET_BRANCH} HEAD --name-only -- ${{env.TERRAFORM_MODULES_DIR}} | wc -l )
          if [ ${LINES} -gt 0 ]; then
            flag_all_envs='true'
          else
            flag_all_envs='false'
          fi
          echo "::group::flag_all_envs = ${flag_all_envs}"

          # Detect target directories
          echo "::group::detect target directories"
          if [ "${flag_all_envs}" == 'true' ]; then
            TARGET_DIR=$( \
              find ${{env.TERRAFORM_TARGET_DIR}} -type d -not -name ${{env.TERRAFORM_ENVS_EXCLUDED_DIR}} -maxdepth 1 -mindepth 1 | \
              jq -scR 'split("\n") | .[:-1]' \
            );
          else
            TARGET_DIR=$( \
              git diff origin/${TARGET_BRANCH} HEAD --name-only -- ${{ env.TERRAFORM_TARGET_DIR }} | \
              sed -E 's:(^${{ env.TERRAFORM_TARGET_DIR }}/[^/]*/)(.*$):\1:' | \
              sort | uniq | \
              awk '{ if( system("[ -d "$1" ]") == 0 && $1 !~ /${{env.TERRAFORM_ENVS_EXCLUDED_DIR}}/ ){print $1} }'  | \
              jq -scR 'split("\n") | .[:-1]'
            );
          fi

          # Output target directories
          echo "::endgroup::"
          # Output results
          echo "::group::results"
          echo "TARGET_DIR = ${TARGET_DIR}"
          echo "::endgroup::"
          # End processing
          echo "::set-output name=TARGET_DIR::${TARGET_DIR}"
          exit 0

Terraform実行ジョブ

先の更新ディレクトリ特定ジョブから、jobのoutputsを利用し特定したディレクトリ一覧を取得し、Matrixを利用し並列実行しています。
checkovの詳細については、こちらの記事を参照ください。

  run_terraform:
    name: "Run terraform"
    needs: detect_dirs
    if: ${{ needs.detect_dirs.outputs.TARGET_DIR != '[]' }}
    strategy:
      fail-fast: false
      max-parallel: 2
      matrix:
        target: ${{fromJson(needs.detect_dirs.outputs.TARGET_DIR)}}
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash
    env:
      TERRAFORM_WORK_DIR: ${{ matrix.target }}
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Checkout repository
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{secrets.ASSUME_ROLE_ARN}}
          role-session-name: pullrequest
          aws-region: ap-northeast-1
      - name: AWS Sts Get Caller Identity
        run: aws sts get-caller-identity
      - name: Setup terraform
        uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: ${{env.TERRAFORM_VERSION}}
      - name: Set up Python 3.9
        uses: actions/setup-python@v4
        with:
          python-version: 3.9
      - name: Test with Checkov
        id: checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: ${{env.TERRAFORM_ROOT_DIR}}
          framework: terraform
          quiet: true
          skip_check: ${{env.CHECKOV_SKIP_CHECK}}
      - name: Terraform Format
        run: terraform fmt -recursive -check=true
      - name: Terraform Init
        run: terraform -chdir=${TERRAFORM_WORK_DIR} init
      - name: Terraform Validate
        run: terraform -chdir=${TERRAFORM_WORK_DIR} validate -no-color
      - name: Terraform Plan
        run: terraform -chdir=${TERRAFORM_WORK_DIR} plan

セットアップ手順

具体的手順は、GitHubのリポジトリのREADMEに記載しています。

  1. Terraform実行用にIODCプロバイダー、IAMロール、S3バケット、DynamoDB作成と設定
    1. 作成用のCloudFormationを準備しているので、これで作成します。
    2. IAMロールのANRをGitHubリポジトリに、ASSUME_ROLE_ARNという名称のシークレットに格納します。
    3. 作成したS3バケット、DynamoDBを、Terraformコードに設定
  2. GitHubリポジトリへmainブランチ以外の任意のブランチでPush
  3. pull requestを作成し、mainにマージする。