みなさん、こんにちは!
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つではないでしょうか。
長い記事となりましたが、今後も汎用的に使えるテクニックなどをご紹介していきます。