はじめに
メタップスアドベントカレンダー16日目の記事です。
インフラのコードをTerraformで管理しているリポジトリについて、Github Actionsを使ってTerraformのplanやapplyなど各種コマンドを実行できるようにしました。
Github Actionsの構成
以下のようなことを実行しています。
- Terraformでworkspaceを使う構成となっているため、workflow_dispatchでEnvironmentや実行コマンドを選択して実行する。
- Terraformのproviderやworkspaceごとに細かくディレクトリやtfstateが分かれているため、差分検出用のJobとTerraformコマンド実行用のJobを分割している。
- validateコマンドでTerraformの構文チェックを行う。
- trivy-actionを使ってTerraformのコード脆弱性をチェックする。
- tfnotifyを使って実行結果をslackに通知させる。
Github Actionsコードサンプル
ディレクトリ構成
├── .github
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows
│ ├── genova.yml
│ ├── terraform-manual.yml
│ ├── terraform-plan.yml
│ └── trivy.yml
├── .tfnotify
│ └── slack.yml
コード内で参照するenvの秘匿値は事前にリポジトリに登録しておいてください。
https://docs.github.com/ja/actions/security-guides/using-secrets-in-github-actions
terraform-manual.yml
default指定しているdevelopブランチとTerraformコマンド実行対象のbranchとで差分のあるリソースのみを抽出し、実行します。
name: TerraformManualRelease
run-name: TerraformManualRelease [${{ inputs.environment }}] [${{ inputs.plan_or_apply }}] From [${{ github.ref_name }}]
on:
workflow_dispatch:
inputs:
environment:
description: 'AWS Environment'
required: true
type: choice
default: '-'
options:
- '-'
- staging
- production
plan_or_apply:
description: 'Terraform plan or apply'
required: true
type: choice
options:
- plan
- apply
jobs:
setup:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
outputs:
plan_targets: ${{ steps.output-targets.outputs.plan_targets }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get branch name
id: branch-name
uses: tj-actions/branch-names@v6
- name: diff
id: diff
run: |
diff_files=$(git diff --name-only origin/${{ steps.branch-name.outputs.default_branch }}...origin/${{ steps.branch-name.outputs.current_branch }} -- '*.tf')
diff_files_json=$(echo "$diff_files" | jq -R . | jq -s . | tr -d '\n' | sed 's/,$//')
echo "diff_files=$diff_files_json" >> "$GITHUB_OUTPUT"
- name: Output target dirs
id: output-targets
uses: k1LoW/github-script-ruby@v2
with:
script: |
require 'json'
dirs = []
Dir.glob('**/*.tf').each do |f|
dirs.push(File.dirname(f))
end
valid_dirs = dirs.uniq
files = '${{ steps.diff.outputs.diff_files }}'.split
targets = valid_dirs.select { |v| files.select { |d| d.match?(v) }.size > 0 }
puts "Target files: #{files}"
plan_targets = targets.flatten.uniq
puts "Target dirs: #{files}"
puts "Plan target dirs: #{plan_targets}"
core.set_output('plan_targets', plan_targets.to_json)
terraform:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
id-token: write
contents: write
pull-requests: write
needs: setup
env:
TERRAFORM_VERSION: {TERRAFORM_VERSION}
AWS_ROLE_ARN: {AWS_ROLE_ARN}
AWS_DEFAULT_REGION: {AWS_DEFAULT_REGION}
TFSATTE_BUCKET_NAME: {TFSATTE_BUCKET_NAME}
GITHUB_OWNER: metaps
GITHUB_USER: {GITHUB_USER}
GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }}
SSH_KEY: ${{ secrets.SSH_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
TF_VAR_app_domain: {TF_VAR_app_domain}
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}
SLACK_BOT_NAME: tfnotify
TFNOTIFY_CONFIG: /home/runner/work/{pass}/.tfnotify/slack.yml
defaults:
run:
working-directory: ${{ matrix.target }}
strategy:
matrix:
target: ${{ fromJSON(needs.setup.outputs.plan_targets) }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Env check
if: ${{ github.event.inputs.environment == '-' && contains(matrix.target, 'providers/aws/application') }}
run: |
echo "Error: You are modifying an AWS resource with an Environment, but Environment is not specified"
exit 1
- name: Set env to staging
if: ${{ github.event.inputs.environment == 'staging' }}
run: |
echo "ENV=staging" >> $GITHUB_ENV
- name: Set env to production
if: ${{ github.event.inputs.environment == 'production' }}
run: |
echo "ENV=produciton" >> $GITHUB_ENV
- name: Set AWS credentials
uses: aws-actions/configure-aws-credentials@master
with:
role-to-assume: ${{ env.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Setup tfnotify
run: |
sudo curl -fL -o tfnotify.tar.gz https://github.com/mercari/tfnotify/releases/download/v0.7.0/tfnotify_linux_amd64.tar.gz
sudo tar -C /usr/bin -xzf ./tfnotify.tar.gz
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: ${{ env.TERRAFORM_VERSION }}
- name: Set env to staging
if: ${{ github.event.inputs.environment == 'staging' }}
run: |
echo "ENV=staging" >> $GITHUB_ENV
- name: Set env to production
if: ${{ github.event.inputs.environment == 'production' }}
run: |
echo "ENV=production" >> $GITHUB_ENV
- name: Terraform Applcation Init
if: contains(matrix.target, 'providers/aws/application')
run: |
export MATRIX_TARGET=$(echo "${{ matrix.target }}" | sed "s/providers\/aws\/application/providers\/aws\/application\/${{ env.ENV }}/g")
terraform init -reconfigure -backend-config="key=${MATRIX_TARGET}".tfstate
env:
GIT_SSH_COMMAND: "echo '${{ secrets.SSH_KEY }}' > id_rsa
&& ssh-keyscan github.com > known_hosts
&& chmod 600 id_rsa known_hosts
&& ssh -i ./id_rsa -o UserKnownHostsFile=./known_hosts"
- name: Terraform Init
if: "!contains(matrix.target, 'providers/aws/application')"
run: |
terraform init -reconfigure -backend-config="key=${{ matrix.target }}".tfstate
env:
GIT_SSH_COMMAND: "echo '${{ secrets.SSH_KEY }}' > id_rsa
&& ssh-keyscan github.com > known_hosts
&& chmod 600 id_rsa known_hosts
&& ssh -i ./id_rsa -o UserKnownHostsFile=./known_hosts"
- name: Terraform Validate
run: terraform validate -no-color
- name: Run Trivy
uses: aquasecurity/trivy-action@master
with:
scan-type: config
trivy-config: ../trivy.yml
continue-on-error: true
- name: Terraform Plan
if: ${{ github.event.inputs.plan_or_apply == 'plan' && contains(matrix.target, 'providers/aws/application') }}
run: |
terraform plan -lock=false -no-color -var="env=${{ env.ENV }}" >> apply_result.temp
cat apply_result.temp | tfnotify --config ${{ env.TFNOTIFY_CONFIG }} plan --message "$(date)"
continue-on-error: true
- name: Terraform Plan
if: ${{ github.event.inputs.plan_or_apply == 'plan' && !contains(matrix.target, 'providers/aws/application') }}
run: |
terraform plan -lock=false -no-color >> apply_result.temp
cat apply_result.temp | tfnotify --config ${{ env.TFNOTIFY_CONFIG }} plan --message "$(date)"
continue-on-error: true
- name: Terraform Applcation Apply
if: ${{ github.event.inputs.plan_or_apply == 'apply' && contains(matrix.target, 'providers/aws/application') }}
run: |
terraform apply -auto-approve -var="env=${{ env.ENV }}" >> apply_result.temp
cat apply_result.temp | tfnotify --config ${{ env.TFNOTIFY_CONFIG }} apply --message "$(date)"
- name: Terraform Apply
if: ${{ github.event.inputs.plan_or_apply == 'apply' && !contains(matrix.target, 'providers/aws/application') }}
run: |
terraform apply -auto-approve >> apply_result.temp
cat apply_result.temp | tfnotify --config ${{ env.TFNOTIFY_CONFIG }} apply --message "$(date)"
trivy.yml
exit-code: 1
severity:
- HIGH
- CRITICAL
slack.yml
ci: github-actions
notifier:
slack:
token: $SLACK_TOKEN
channel: $SLACK_CHANNEL_ID
bot: $SLACK_BOT_NAME
terraform:
plan:
template: |
{{ .Message }}
{{if .Result}}
```
{{ .Result }}
```
{{end}}
```
{{ .Body }}
```
apply:
template: |
{{ .Message }}
{{if .Result}}
```
{{ .Result }}
```
{{end}}
```
{{ .Body }}
```
実行結果
通知画面
trivyの実行結果
Github Actionsの中でterraformコードの脆弱性を出力させることができます。
git::https:/github.com/terraform-aws-modules/terraform-aws-s3-bucket?ref=v3.15.1/main.tf (terraform)
====================================================================================================
Tests: 10 (SUCCESSES: 8, FAILURES: 2, EXCEPTIONS: 0)
Failures: 2 (UNKNOWN: 0, LOW: 1, MEDIUM: 1, HIGH: 0, CRITICAL: 0)
LOW: Bucket has logging disabled
════════════════════════════════════════
Ensures S3 bucket logging is enabled for S3 buckets
See https://avd.aquasec.com/misconfig/avd-aws-0089
────────────────────────────────────────
git::https:/github.com/terraform-aws-modules/terraform-aws-s3-bucket?ref=v3.15.1/main.tf:25-34
via terraform/providers/aws/{pass}/main.tf:334-354 (module.s3)
────────────────────────────────────────
25 ┌ resource "aws_s3_bucket" "this" {
26 │ count = local.create_bucket ? 1 : 0
27 │
28 │ bucket = var.bucket
29 │ bucket_prefix = var.bucket_prefix
30 │
31 │ force_destroy = var.force_destroy
32 │ object_lock_enabled = var.object_lock_enabled
33 │ tags = var.tags
34 └ }
────────────────────────────────────────
まとめ
Github Actionsでの各種操作に関するpermissionsについてはまだ改善余地があると思われますが、Github ActionsでTerraformコマンドを実行させることができました。