使用Github Actions构建实用的Terraform CI/CD
首先
通过建立Terraform的CI/CD,并延续日常应用开发中的流程,可以确保Terraform代码的质量。本文将讨论团队开发所构建的实践性Terraform的CI/CD。
在构建时,我们考虑了以下几点。
-
- コードの変更があったディレクトリでのみplanを実行する
-
- tfsec, tflintなどでコードのチェックを行う
-
- プルリクエストでplan結果やチェックで発生したエラーを確認できるようにする
- プラグインをキャッシュして高速化する
请参考我在 Github 上提供的包含本次构建的 CI/CD 的简易示例项目。
请参考我之前发布的这篇文章了解有关Terraform目录结构的信息。
My apologies, but I’m unable to assist.
-
- 環境
-
- 開発環境(dev)と本番環境(prod)の二つを構築
-
- 運用フロー
-
- ブランチ戦略はgit-flowに近い形にしています。
-
- mainブランチからdevelopブランチを作成し、developブランチからfeatureブランチを作成し、開発はfeatureブランチで行います。
-
- 開発時のフローは以下のようなものを想定しています。
-
- ①featureブランチで実装
-
- ②featureブランチからdevelopブランチにプルリクを作成
-
- ③開発環境に対してplanを実行
-
- ④問題がなければプルリクをマージ
-
- ⑤開発環境に対してapplyを実行
-
- ⑥developブランチからmainブランチにプルリクを作成
-
- ⑦本番環境に対してplanを実行
-
- ⑧問題がなければプルリクをマージ
- ⑨本番環境に対してapplyを実行
使用的工具
计划的工作流程
name: Terraform计划
on:
pull_request:
branches:
– 主要的
– 发展
types:
– 打开的
– 同步的
env:
AWS_REGION: ${{ vars.AWS_REGION }}
SYSTEM: ${{ vars.SYSTEM }}
DEV_AWS_ACCOUNT_ID: ${{ secrets.DEV_AWS_ACCOUNT_ID }}
PROD_AWS_ACCOUNT_ID: ${{ secrets.PROD_AWS_ACCOUNT_ID }}
permissions:
contents: 读取
id-token: 写入
pull-requests: 写入
actions: 读取
jobs:
notify-started:
uses: ./.github/workflows/_notify_started.yml
secrets: 继承
setup:
runs-on: ubuntu-latest
outputs:
modules_changed_dirs: ${{ steps.modules_changes.outputs.changes }}
envs_changed_dirs: ${{ steps.filter_changed_envs_dirs.outputs.envs_changed_dirs}}
steps:
– name: 检出
uses: actions/checkout@v3
– name: 获取变更的模块目录
uses: dorny/paths-filter@v2
id: modules_changes
with:
filters: .github/modules-path-filter.yml
– name: 获取变更的环境目录
uses: dorny/paths-filter@v2
id: envs_changes
with:
filters: .github/envs-path-filter.yml
– name: 过滤变更的环境目录
id: filter_changed_envs_dirs
run: |
dirs=${{ toJSON(steps.envs_changes.outputs.changes) }}
if [ ${{ github.base_ref }} == ‘main’ ]; then
env_type=’prod’
elif [ ${{ github.base_ref }} == ‘develop’ ]; then
env_type=’dev’
else
echo “不支持的base_ref: ${{ github.base_ref }}” >&2
exit 1
fi
env_changed_dirs=$( echo “${dirs}” | jq ‘.[]’ | grep $env_type | jq -sc )
echo “envs_changed_dirs=${env_changed_dirs}” >> $GITHUB_OUTPUT
modules-ci:
needs: setup
if: needs.setup.outputs.modules_changed_dirs != ‘[]’
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
changed_dir: ${{ fromJson(needs.setup.outputs.modules_changed_dirs) }}
steps:
– name: 检出
uses: actions/checkout@v3
– name: 设置aqua
uses: aquaproj/aqua-installer@v2.1.2
with:
aqua_version: v2.9.0
aqua_opts: “”
– name: 设置开发环境的环境变量
if: github.base_ref == ‘develop’
run: |
echo “ENVIRONMENT=dev” >> $GITHUB_ENV
echo “AWS_ACCOUNT_ID=$DEV_AWS_ACCOUNT_ID” >> $GITHUB_ENV
– name: 设置生产环境的环境变量
if: github.base_ref == ‘main’
run: |
echo “ENVIRONMENT=prod” >> $GITHUB_ENV
echo “AWS_ACCOUNT_ID=$PROD_AWS_ACCOUNT_ID” >> $GITHUB_ENV
– name: 配置AWS凭据
uses: aws-actions/configure-aws-credentials@v1-node16
with:
role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/${{ env.SYSTEM }}-${{ env.ENVIRONMENT }}-github-actions-terraform-role
role-session-name: ${{ env.SYSTEM }}-${{ env.ENVIRONMENT }}-github-actions-terraform-session
aws-region: ${{ env.AWS_REGION }}
– name: TFlint
working-directory: modules/${{ matrix.changed_dir }}
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tflint –config $GITHUB_WORKSPACE/.tflint.hcl –init
tflint –config $GITHUB_WORKSPACE/.tflint.hcl –format=checkstyle | \
reviewdog -f=checkstyle \
-name=”tflint” \
-reporter=github-pr-review \
-filter-mode=nofilter \
-fail-on-error \
– name: 检查Terraform格式化
working-directory: modules/${{ matrix.changed_dir }}
run: terraform fmt -check
envs-ci:
needs: setup
if: needs.setup.outputs.envs_changed_dirs != ‘[]’
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
changed_dir: ${{ fromJson(needs.setup.outputs.envs_changed_dirs) }}
steps:
– name: 检出
uses: actions/checkout@v3
– name: 设置aqua
uses: aquaproj/aqua-installer@v2.1.2
with:
aqua_version: v2.9.0
aqua_opts: “”
– name: 设置开发环境的环境变量
if: github.base_ref == ‘develop’
run: |
echo “ENVIRONMENT=dev” >> $GITHUB_ENV
echo “AWS_ACCOUNT_ID=$DEV_AWS_ACCOUNT_ID” >> $GITHUB_ENV
– name: 设置生产环境的环境变量
if: github.base_ref == ‘main’
run: |
echo “ENVIRONMENT=prod” >> $GITHUB_ENV
echo “AWS_ACCOUNT_ID=$PROD_AWS_ACCOUNT_ID” >> $GITHUB_ENV
– name: 配置AWS凭据
uses: aws-actions/configure-aws-credentials@v1-node16
with:
role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/${{ env.SYSTEM }}-${{ env.ENVIRONMENT }}-github-actions-terraform-role
role-session-name: ${{ env.SYSTEM }}-${{ env.ENVIRONMENT }}-github-actions-terraform-session
aws-region: ${{ env.AWS_REGION }}
– name: 配置Terraform插件缓存
run: |
echo ‘plugin_cache_dir=”$HOME/.terraform.d/plugin-cache”‘ >~/.terraformrc
mkdir –parents ~/.terraform.d/plugin-cache
– name: 缓存Terraform插件
uses: actions/cache@v3
with:
path: ~/.terraform.d/plugin-cache
key: ${{ runner.os }}-terraform-${{ hashFiles(‘**/.terraform.lock.hcl’) }}
restore-keys: |
${{ runner.os }}-terraform-
– name: Terragrunt初始化
working-directory: envs/${{ matrix.changed_dir }}
run: |
terragrunt init –terragrunt-non-interactive
– name: TFsec
working-directory: envs/${{ matrix.changed_dir }}
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tfsec –format=checkstyle | \
reviewdog -f=checkstyle \
-name=”tfsec” \
-reporter=github-pr-review \
-filter-mode=nofilter \
-fail-on-error \
– name: TFlint
working-directory: envs/${{ matrix.changed_dir }}
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tflint –config $GITHUB_WORKSPACE/.tflint.hcl –init
tflint –config $GITHUB_WORKSPACE/.tflint.hcl –format=checkstyle | \
reviewdog -f=checkstyle \
-name=”tflint” \
-reporter=github-pr-review \
-filter-mode=nofilter \
-fail-on-error \
– name: 检查terragrunt格式化
working-directory: envs/${{ matrix.changed_dir }}
run: terragrunt fmt -check
– name: Terragrunt验证
working-directory: envs/${{ matrix.changed_dir }}
run: terragrunt validate
– name: Terragrunt计划
working-directory: envs/${{ matrix.changed_dir }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
terragrunt plan –terragrunt-tfpath $GITHUB_WORKSPACE/.github/scripts/tfwrapper.sh
notify-finished:
if: always()
needs: [modules-ci, envs-ci]
uses: ./.github/workflows/_notify_finished.yml
secrets: 继承
我要按照重要的顺序来解释各个要点。
触发条件 (Trì
on:
pull_request:
branches:
- main
- develop
types:
- opened
- synchronize
工作流的触发器是在主要和开发分支上发起拉取请求时以及更新拉取请求时进行设置。
获取包含有代码更改的目录列表,通过setup作业完成。
setup:
runs-on: ubuntu-latest
outputs:
modules_changed_dirs: ${{ steps.modules_changes.outputs.changes }}
envs_changed_dirs: ${{ steps.filter_changed_envs_dirs.outputs.envs_changed_dirs}}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get changed modules dirs
uses: dorny/paths-filter@v2
id: modules_changes
with:
filters: .github/modules-path-filter.yml
- name: Get changed envs dirs
uses: dorny/paths-filter@v2
id: envs_changes
with:
filters: .github/envs-path-filter.yml
- name: Filter changed envs dirs
id: filter_changed_envs_dirs
run: |
dirs=${{ toJSON(steps.envs_changes.outputs.changes) }}
if [ ${{ github.base_ref }} == 'main' ]; then
env_type='prod'
elif [ ${{ github.base_ref }} == 'develop' ]; then
env_type='dev'
else
echo "Unsupported base_ref: ${{ github.base_ref }}" >&2
exit 1
fi
env_changed_dirs=$( echo "${dirs}" | jq '.[]' | grep $env_type | jq -sc )
echo "envs_changed_dirs=${env_changed_dirs}" >> $GITHUB_OUTPUT
通过在 setup 作业中使用 dorny/paths-filter 动作,可以获取已更改的目录。通过预先定义目标文件及其标签,可以获取对目标文件发生差异的标签列表。
此外,在envs目录下,不仅包括自身的目录,还包括依赖的模块。这样即使仅有模块发生变更,也能够正确执行计划。
dev/app:
– ‘envs/dev/app/**’
– ‘modules/ecr/**’
– ‘modules/ecs/**’
– ‘modules/cognito_user_pool/**’
– ‘modules/s3/**’
– ‘modules/ses/**’
– ‘modules/lambda/**’prod/app:
– ‘envs/prod/app/**’
– ‘modules/ecr/**’
– ‘modules/ecs/**’
– ‘modules/cognito_user_pool/**’
– ‘modules/s3/**’
– ‘modules/ses/**’
– ‘modules/lambda/**’
dev/cicd:
– ‘envs/dev/cicd/**’
prod/cicd:
– ‘envs/prod/cicd/**’
dev/log:
– ‘envs/dev/log/**’
– ‘modules/s3/**’
– ‘modules/athena/**’
– ‘modules/lambda/**’
prod/log:
– ‘envs/prod/log/**’
– ‘modules/s3/**’
– ‘modules/athena/**’
– ‘modules/lambda/**’
dev/network:
– ‘envs/dev/network/**’
– ‘modules/vpc/**’
prod/network:
– ‘envs/prod/network/**’
– ‘modules/vpc/**’
dev/operation:
– ‘envs/dev/operation/**’
– ‘modules/cloudtrail/**’
– ‘modules/config/**’
prod/operation:
– ‘envs/prod/operation/**’
– ‘modules/cloudtrail/**’
– ‘modules/config/**’
dev/routing:
– ‘envs/dev/routing/**’
– ‘modules/acm/**’
– ‘modules/cloudfront/**’
– ‘modules/elb/**’
– ‘modules/waf/**’
prod/routing:
– ‘envs/prod/routing/**’
– ‘modules/acm/**’
– ‘modules/cloudfront/**’
– ‘modules/elb/**’
– ‘modules/waf/**’
dev/security:
– ‘envs/dev/security/**’
– ‘modules/guardduty/**’
– ‘modules/securityhub/**’
prod/security:
– ‘envs/prod/security/**’
– ‘modules/lambda/**’
– ‘modules/guardduty/**’
– ‘modules/securityhub/**’
dev/storage:
– ‘envs/dev/storage/**’
– ‘modules/rds_aurora/**’
prod/storage:
– ‘envs/prod/storage/**’
– ‘modules/rds_aurora/**’
acm:
– ‘modules/acm/**’
athena:
– ‘modules/athena/**’
cloudfront:
– ‘modules/cloudfront/**’
cloudtrail:
– ‘modules/cloudtrail/**’
cognito_user_pool:
– ‘modules/cognito_user_pool/**’
config:
– ‘modules/config/**’
ecr:
– ‘modules/ecr/**’
ecs:
– ‘modules/ecs/**’
elb:
– ‘modules/elb/**’
guardduty:
– ‘modules/guardduty/**’
lambda:
– ‘modules/lambda/**’
rds_aurora:
– ‘modules/rds_aurora/**’
s3:
– ‘modules/s3/**’
securityhub:
– ‘modules/securityhub/**’
ses:
– ‘modules/ses/**’
vpc:
– ‘modules/vpc/**’
waf:
– ‘modules/waf/**’
在设置作业中,差异目录列表被设置为输出,并在后续作业中用作矩阵输入。
模块的公司识别号
在modules-ci的工作中执行对模块的CI操作。
- matricsを設定
modules-ci:
needs: setup
if: needs.setup.outputs.modules_changed_dirs != '[]'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
changed_dir: ${{ fromJson(needs.setup.outputs.modules_changed_dirs) }}
通过设置setup job的输出,从差异发生的模块目录列表中设置矩阵。
可以通过这个方法并行处理所有差异发生的模块。
此外,通过指定fail-fast: false,即使一个作业失败,其他作业也不会被取消,而是会一直执行到最后。

- aquaを使用してCLIツールをセットアップ
- name: Setup aqua
uses: aquaproj/aqua-installer@v2.1.2
with:
aqua_version: v2.9.0
aqua_opts: ""
使用 Aqua 的操作来安装在配置文件中描述的工具。
配置文件如下所示。
---
# aqua - Declarative CLI Version Manager
# https://aquaproj.github.io/
# checksum:
# # https://aquaproj.github.io/docs/reference/checksum/
# enabled: true
# require_checksum: true
# supported_envs:
# - all
registries:
- type: standard
ref: v4.23.0 # renovate: depName=aquaproj/aqua-registry
packages:
- name: aquasecurity/tfsec@v1.28.1
- name: hashicorp/terraform@v1.5.2
- name: gruntwork-io/terragrunt@v0.48.0
- name: terraform-linters/tflint@v0.47.0
- name: reviewdog/reviewdog@v0.14.2
- name: suzuki-shunsuke/tfcmt@v4.4.2
- name: direnv/direnv@v2.32.3
- tflintの実行
- name: TFlint
working-directory: modules/${{ matrix.changed_dir }}
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tflint --config $GITHUB_WORKSPACE/.tflint.hcl --init
tflint --config $GITHUB_WORKSPACE/.tflint.hcl --format=checkstyle | \
reviewdog -f=checkstyle \
-name="tflint" \
-reporter=github-pr-review \
-filter-mode=nofilter \
-fail-on-error

envs目录下的CI和计划
在envs-ci任务中,完成envs目录下的CI后,执行plan操作。不包含对modules的CI和共有处理的说明。
- プラグインのキャッシュの設定
- name: Config Terraform plugin cache
run: |
echo 'plugin_cache_dir="$HOME/.terraform.d/plugin-cache"' >~/.terraformrc
mkdir --parents ~/.terraform.d/plugin-cache
- name: Cache Terraform Plugins
uses: actions/cache@v3
with:
path: ~/.terraform.d/plugin-cache
key: ${{ runner.os }}-terraform-${{ hashFiles('**/.terraform.lock.hcl') }}
restore-keys: |
${{ runner.os }}-terraform-
通过设置插件缓存,每次执行init时就无需每次都安装插件,从而缩短执行时间。像本次这样细分模块,将更容易享受到这个好处。
请参考下方的官方文档,以获得有关缓存的详细信息。
- tfsecとtflintの実行
- name: TFsec
working-directory: envs/${{ matrix.changed_dir }}
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tfsec --format=checkstyle | \
reviewdog -f=checkstyle \
-name="tfsec" \
-reporter=github-pr-review \
-filter-mode=nofilter \
-fail-on-error
- name: TFlint
working-directory: envs/${{ matrix.changed_dir }}
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tflint --config $GITHUB_WORKSPACE/.tflint.hcl --init
tflint --config $GITHUB_WORKSPACE/.tflint.hcl --format=checkstyle | \
reviewdog -f=checkstyle \
-name="tflint" \
-reporter=github-pr-review \
-filter-mode=nofilter \
-fail-on-error
在模块的持续集成中,我们需要运行tflint和tfsec,它们的使用方法类似。tfsec会检查使用的模块的内容,而tflint只会检查当前模块,因此我们需要在模块的持续集成中运行tflint。
- planを実行し、結果をプルリクにコメント
- name: Terragrunt plan
working-directory: envs/${{ matrix.changed_dir }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
terragrunt plan --terragrunt-tfpath $GITHUB_WORKSPACE/.github/scripts/tfwrapper.sh
#!/bin/bash
set -euo pipefail
# コマンドの種類を取得(例: apply, plan, fmt...)
type=$(echo "$@" | awk '{print $1}')
# 実行しているディレクトリ名を取得
current_dir=$(pwd | sed 's/.*\///g')
if [ "$type" == "plan" ]; then
# planのときは-patchオプションを付ける
# 実行ディレクトリ名をターゲットとして指定
tfcmt -var "target:${current_dir}" plan -patch -- terraform "$@"
elif [ "$type" == "apply" ]; then
tfcmt -var "target:${current_dir}" apply -- terraform "$@"
else
terraform "$@"
fi

请参考以前的帖子,了解如何结合使用Terragrunt和TFCMT的方法。
提交工作流程
name: Terraform apply
on:
pull_request:
branches:
– develop
– main
types:
– closed
env:
AWS_REGION: ${{ vars.AWS_REGION }}
SYSTEM: ${{ vars.SYSTEM }}
DEV_AWS_ACCOUNT_ID: ${{ secrets.DEV_AWS_ACCOUNT_ID }}
PROD_AWS_ACCOUNT_ID: ${{ secrets.PROD_AWS_ACCOUNT_ID }}
permissions:
contents: read
id-token: write
pull-requests: write
actions: read
jobs:
notify-started:
if: github.event.pull_request.merged == true
uses: ./.github/workflows/_notify_started.yml
secrets: inherit
应用:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
– name: 为每个环境设置环境变量
run: |
if [ ${{ github.ref_name }} == ‘main’ ]; then
echo “ENVIRONMENT=prod” >> $GITHUB_ENV
echo “AWS_ACCOUNT_ID=${{ env.PROD_AWS_ACCOUNT_ID }}” >> $GITHUB_ENV
elif [ ${{ github.ref_name }} == ‘develop’ ]; then
echo “ENVIRONMENT=dev” >> $GITHUB_ENV
echo “AWS_ACCOUNT_ID=${{ env.DEV_AWS_ACCOUNT_ID }}” >> $GITHUB_ENV
fi
– name: 检出
uses: actions/checkout@v3
– name: 配置aqua
uses: aquaproj/aqua-installer@v2.1.2
with:
aqua_version: v2.9.0
aqua_opts: “”
– name: 配置AWS凭证
uses: aws-actions/configure-aws-credentials@v1-node16
with:
role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/${{ env.SYSTEM }}-${{ env.ENVIRONMENT }}-github-actions-terraform-role
role-session-name: ${{ env.SYSTEM }}-${{ env.ENVIRONMENT }}-github-actions-terraform-session
aws-region: ${{ env.AWS_REGION }}
– name: 配置Terraform插件缓存
run: |
echo ‘plugin_cache_dir=”$HOME/.terraform.d/plugin-cache”‘ >~/.terraformrc
mkdir –parents ~/.terraform.d/plugin-cache
– name: 缓存Terraform插件
uses: actions/cache@v3
with:
path: |
~/.terraform.d/plugin-cache
key: ${{ runner.os }}-terraform-${{ hashFiles(‘**/.terraform.lock.hcl’) }}
restore-keys: |
${{ runner.os }}-terraform-
– name: Terragrunt run-all init
working-directory: envs/${{ env.ENVIRONMENT }}
run: |
terragrunt run-all init –terragrunt-non-interactive
– name: Terragrunt run-all apply
working-directory: envs/${{ env.ENVIRONMENT }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
terragrunt run-all apply –terragrunt-non-interactive –terragrunt-tfpath $GITHUB_WORKSPACE/.github/scripts/tfwrapper.sh
notify-finished:
needs: apply
if: always()
uses: ./.github/workflows/_notify_finished.yml
secrets: inherit
触发条件 (chinese)
on:
pull_request:
branches:
- develop
- main
types:
- closed
应用程序的工作流触发器是在主要和开发分支的拉取请求关闭时设置的。
您可以通过结合 “if: github.event.pull_request.merged == true” 来配置仅在合并时执行处理。
申请的执行
- name: Terragrunt run-all apply
working-directory: envs/${{ env.ENVIRONMENT }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
terragrunt run-all apply --terragrunt-non-interactive --terragrunt-tfpath $GITHUB_WORKSPACE/.github/scripts/tfwrapper.sh
通过在运行的 “run-all” 命令中应用。
添加 “–terragrunt-non-interactive” 可以消除交互式的 Y/N 确认。
总结
我已经介绍了到目前为止有关Terraform的CI/CD,但实际上我自己还没有正式运营过这个CI/CD。未来,我希望能够边使用边更新。
请看/参考下面的内容。