LoginSignup
24
6

はじめに

メタップスアドベントカレンダー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 }}
      ```

実行結果

実行時にEnvironmentと実行コマンドを選択します。
image.png

通知画面

実行結果をslackに通知させることができます。
スクリーンショット 2023-12-16 11.31.53.png

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コマンドを実行させることができました。

24
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
6