
みなさん、こんにちは!
AWSグループの大澤 (@yukiblue63) です。
Terraform を運用するにあたり、作業者の端末が1人1人異なると、Terraformの実行環境を揃えるのが大変ではないでしょうか。
そういった場合に、Github Actions を用いて、Terraform のPlanと Apply を実行できるようにすることで、作業者の端末に依存せずに実行環境を整備することが可能となります。
今回は、AWSリソースの管理にTerraform を利用する場合を想定し、Github Actions でPlan と Apply を自動化させる場合の例とポイントをご紹介します。
イメージ図
今回ご紹介する方法について、Github Actions と Terraform と AWSリソースの関係性について示します。

①: Github Actions が OpenID Connect を利用して各AWSリソースにアクセスします。
②: User がGithub Repository にコードをPush の上、Pull Request (PR) を発行したタイミングで、 Terraform Planを自動的に実行し、PR のコメント欄に結果を追記します。
PRがMergeされると、Terraform Apply が自動的に実行されます。
Github Actions
AWS でOpenID Connect の設定
Github Actions がAWSリソースへアクセスするために、OpenID Connect (OIDC) によって、有効期間の長い GitHub シークレット を利用できるようにします。
GitHub Actionsと連携するためのIAMを設定する手順 については、下記のテックブログの記事 でもご紹介しております。
ID プロバイダを作成
IAM コンソールから ID プロバイダ → プロバイダを追加 の順に選択します。

プロバイダの設定
下記のように入力します。

| 項目 | 入力する値 |
|---|---|
| プロバイダのタイプ | OpenID Connect |
| プロバイダの URL | https://token.actions.githubusercontent.com |
| 対象者 | sts.amazonaws.com |
以上で、GitHub Actions 用の ID プロバイダが作成されます。
IAMロールの作成
IAM コンソールにおいて、新規にIAMロールを作成します。
IAM コンソールから ロール → ロールを作成 の順に選択します。
信頼ポリシーの設定では、信頼されたエンティティタイプ に カスタム信頼ポリシー を選択後、
カスタム信頼ポリシー を入力します。
(ここでは、カスタム信頼ポリシーの例を記載します)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<AWSアカウントID>:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:<GitHubユーザー名>/<GitHubレポジトリ名>:ref:refs/heads/<ブランチ名>"
}
}
}
]
}
ここでのPOINT として、Condition となります。
- Condition を設定しないと全ての GitHub リポジトリから認証できるようになってしまうため、必ず設定してください。
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
// 特定のレポジトリかつ特定のブランチからのみ認証を許可
"token.actions.githubusercontent.com:sub": "repo:<GitHubユーザー名>/<GitHubレポジトリ名>:ref:refs/heads/<ブランチ名>"
}
}
入力したら 次へを選択します。
IAM ロールにポリシーをアタッチ
IAMロールに任意のポリシーをアタッチします。
ここでは、Github Actions ・Terraform が作成するAWSサービスをコントロールできるようにポリシーを付与してください。
(例として、AmazonEC2ReadOnlyAccess を付与しています)

Workflow のyaml ファイルの配置
レポジトリのトップに、.github/workflows ディレクトリを配置し、その配下に、yamlファイルを設置します。
// .github ディレクトリの配下のツリー図
.
├
└── workflows
├── terraform_apply_common.yml
├── terraform_apply_production.yml
├── terraform_apply_staging.yml
└── terraform_plan.yml
ロールを作成
ロール名 に任意のロール名を入力して ロールを作成します。
ここまで完了すると、Github Actions から OIDC による AWS認証の準備が完了します。
terraform plan の自動実行
Terraform Plan をPR (Pull Request) が作成されたときとPRを作成後に対象ブランチにpushしたときのそれぞれのタイミングで実行させる場合を想定します。
---
name: "Workflow for Terraform Plan"
on:
workflow_call:
inputs:
TF_VERSION:
type: string
required: true
pull_request: ①
branches:
- (Planを自動実行させたいブランチ名を指定)
types: [opened, synchronize]
permissions:
id-token: write
contents: read
pull-requests: write
env:
ROLE_TO_ASSUME: arn:aws:iam::(略):role/(OpenID Connect でGithub Actions が利用するIAMロール名)
AWS_REGION: ap-northeast-1
jobs:
terraform:
name: "Terraform Directory Checks"
runs-on: ubuntu-latest
strategy:
matrix:
directory:
[
"(同じレポジトリ内にあるTerraformのコードがある場所を指定)"
]
defaults:
run:
working-directory: ${{ matrix.directory }}
steps:
- name: Checkout
uses: actions/checkout@v4
# .terraform-version で定義しているTerraformバージョンを取得 ②
- name: Get Terraform version
id: terraform-version
uses: bigwheel/get-terraform-version-action@v1.2.0
with:
path: ${{ matrix.directory }}
# .terraform-version で定義しているTerraformバージョンでinit ②
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ steps.terraform-version.outputs.terraform-version }}
- name: Setup AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.ROLE_TO_ASSUME }}
aws-region: ${{ env.AWS_REGION }}
- name: Check if directory is not empty
id: check
run: |
if [ "$(ls -A .)" ]; then
echo "Directory is not empty"
else
echo "Directory is empty"
exit 1
fi
- name: Terraform Init
id: init
run: terraform init
- name: Terraform Plan
id: plan
run: terraform plan -no-color
continue-on-error: true
- name: truncate terraform plan result
run: |
plan=$(cat <<'EOF'
${{ format('{0}{1}', steps.plan.outputs.stdout, steps.plan.outputs.stderr) }}
EOF
)
echo "PLAN<<EOF" >> $GITHUB_ENV
echo "${plan}" | grep -v 'Refreshing state' | tail -c 65000 >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: create comment from plan result ③
uses: actions/github-script@0.9.0
if: github.event_name == 'pull_request'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`\n
${ process.env.PLAN }
\`\`\`
</details>
*Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Working Directory: \`${{ matrix.directory }}\`, Workflow: \`${{ github.workflow }}\`*`;
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
上記のコード内に今回ポイントとなる箇所を番号で示しています。
①: 指定したブランチに取り込まれたプッシュイベントをトリガーにワークフローが実行されます。
②: 実行の際にTerraformのバージョンを指定させるために、tfenv で利用する、
.terraform-versionファイルに実行させたいTerraformバージョンを記載しておき、それを読み取り、指定バージョンで実行します。③: マージ元のPRにapply結果をコメントで投稿させます。これにより、plan結果を自動的に1つのPR上でまとめて確認することができ、見通しが良くなります。
Terraform Apply の自動実行
Terraform Apply をPR (Pull Request) がCloseされたときに自動実行、対象ブランチを指定し、
手動実行させる場合を想定します。
---
name: "Workflow Terraform Apply"
on:
pull_request: ①
branches:
- (Planを自動実行させたいブランチ名を指定)
types:
- closed ②
workflow_dispatch: ③
workflow_call:
inputs:
TF_VERSION:
type: string
required: true
permissions:
id-token: write
contents: read
pull-requests: write
env:
ROLE_TO_ASSUME: arn:aws:iam::(略):role/(OpenID Connect でGithub Actions が利用するIAMロール名)
jobs:
terraform:
name: "Terraform Directory Checks"
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true ②
strategy:
matrix:
directory:
[
"(同じレポジトリ内にあるTerraformのコードがある場所を指定)"
]
defaults:
run:
working-directory: ${{ matrix.directory }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
# .terraform-version で定義しているTerraformバージョンを取得
- name: Get Terraform version
id: terraform-version
uses: bigwheel/get-terraform-version-action@v1.2.0
with:
path: ${{ matrix.directory }}
# .terraform-version で定義しているTerraformバージョンでinit
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ steps.terraform-version.outputs.terraform-version }}
- name: Setup AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.ROLE_TO_ASSUME }}
aws-region: ${{ env.AWS_REGION }}
- name: Check if directory is not empty
id: check
run: |
if [ "$(ls -A .)" ]; then
echo "Directory is not empty"
else
echo "Directory is empty"
exit 1
fi
- name: Terraform Init
id: init
run: terraform init
- name: Terraform Apply
id: apply
working-directory: ${{ matrix.directory }}
run: |
terraform apply -auto-approve -no-color
- name: truncate terraform apply result
run: |
apply=$(cat <<'EOF'
${{ format('{0}{1}', steps.apply.outputs.stdout, steps.apply.outputs.stderr) }}
EOF
)
echo "PLAN<<EOF" >> $GITHUB_ENV
echo "${apply}" | grep -v 'Refreshing state' | tail -c 65000 >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
上記のコード内に今回ポイントとなる箇所を番号で示しています。
①: 指定したブランチに対するPRのcloseイベントをトリガーにワークフローが実行されます。
②: Pull Request がMergeかつClose された場合は、「PRがクローズされた際のイベントとPRがマージされているかどうかの条件を組み合わせる」必要がある点に注意が必要となります。
③: PRにMergeする前に、検証等でApplyを実行させるような場合を想定し、
workflow_dispatchにより手動実行も可能としています。
Terraform
Terraform のコードやディレクトリ構成として、下記のような構成を準備しました。
想定として、AWSリソースの環境として、staging環境とprod環境の2つがあると仮定し、両環境で共通で必要となるリソースをcommon ディレクトリにまとめています。
└── terraform
├── common
│ └── aws
│ ├── <共通環境で必要なtfファイル群>
├── modules
│ └── aws
│ ├── <AWSリソース名>
│ │ ├── main.tf
│ │ ├── output.tf
│ └── └── variables.tf
| (以下、略)
├── production
│ └── aws
│ ├── <prod環境で必要なtfファイル群>
└── staging
└── aws
├── <staging環境で必要なtfファイル群>
.terraform-version で定義しているTerraformバージョンを取得 するための、.terraform-version は、
common、staging、production のそれぞれのディレクトリに配置します。
Terraform のバージョン管理ツールの tfenv を使う上で、実行バージョンを指定するファイルを活用しています。
また、それぞれのディレクトリに、provider の指定、stateファイルを s3バケットに置く場合のbackend 設定 などもそれぞれ用意します。
実行例
実際に、Github Actions で実行させた場合のサンプル例です。
Github の 対象レポジトリ内の Actions の画面ですが、画像内の矢印の通り、
ブランチを指定して、Terraform Apply ができるようになっています。

まとめ
Terraform を運用するにあたり、作業者の端末の環境やOSが1人1人異なると、Terraformの実行環境を揃えるのが大変になってきてしまう
Github Actions やその他のCI/CDツールを活用することで、Terraform の実行環境を整備することが可能
事前準備として、OpenID Connect でGithub Actions がAWSリソースへアクセスするために設定が必要
OIDC用のIAMロールの信頼ポリシーでは、Condition を設定しないと全ての GitHub リポジトリから認証できるようになってしまうため、必ず設定する
今回の例では、PRが作成されると、Terraform Plan を自動実行し、PR内のコメントとして追記されるように設定
tfenv で利用する、
.terraform-versionファイルに実行させたいTerraformバージョンを記載しておくことで、指定したTerraformのバージョンで実行させることが可能Pull Request がMergeかつClose された場合は、「PRがクローズされた際のイベントとPRがマージされているかどうかの条件を組み合わせる」必要がある
レポジトリの初期設定の際に合わせて、Gtihub Actions による自動化も実施いただくと、実際に運用時の煩雑さ軽減に寄与できる方法の1つではないでしょうか。
長い記事となりましたが、今後も汎用的に使えるテクニックなどをご紹介していきます。
