Terraform+CSV+GitHub ActionsでGitHub Organizationのメンバー管理をやってみた-③仕組み編

Terraform+CSV+GitHub ActionsでGitHub Organizationのメンバー管理をやってみた-③仕組み編

Clock Icon2025.03.24

こんにちは!クラウド事業本部の吉田です。

皆さん、GitHub Organization利用していますか?

Terraform+CSV+GitHub Actionsを利用して、GitHub Organizationのメンバー管理をする方法とその初期構築を紹介させていただきました。

当記事は、その仕組みの詳細を解説した記事となります。
AWS環境、Terraform、GitHub Actionsに分けて解説します。

記事リンク

サンプルコード

サンプルコードは、下記のリポジトリで公開しております。
https://github.com/yoshida-takeshi-classmethod/sample-gh-org

サンプルコードのディレクトリ/ファイル構成

.
├── .github/
│   └── workflows/ #GitHub Actionsのワークフローファイルの格納フォルダ
│       ├── apply.yml # Pull Requestマージ時に実行されるterraform apply用ワークフローファイル
│       └── plan.yml # Pull Request時に実行されるterraform plan用ワークフローファイル
├── shell/
│   └── script.sh # GitHub Appsトークン生成用シェルスクリプト
├── terraform/ #tfファイル格納用フォルダ
│   ├── module/
│   │   └── terraform-github-organization/ # Organizationのメンバー管理用モジュールフォルダ
│   │       ├── main.tf # モジュールのメインtfファイル
│   │       └── variables.tf # モジュールの変数用tfファイル
│   ├── .terraform.lock.hcl # terraform init時に自動生成されるプロバイダロックファイル
│   ├── organization.tf # モジュールに渡す引数を定義するtfファイル。チーム追加時などに更新するファイル
│   ├── README.md # organization.tfの内容を説明したREADMEファイル
│   └── versions.tf # terraform version、プロパイダー、バックエンドを指定するファイル
├── users/ #メンバー管理用のCSVファイルを格納するフォルダ
│   └── *.csv
├── .gitignore
└── README.md

AWS環境

初期構築の記事のおさらいです。
Terraformにはtfstateファイルというリソースの状態を管理するファイルがあり、そのリモート管理する場所としてS3を利用します。
GitHubとAWSへのアクセス認証は、OIDCによる一時的なクレデンシャルを利用します。
アクセスキー・シークレットキーのような永続的なクレデンシャルを渡すことなく、セキュアにGitHub Actionsを実行することができます。
細かな設定内容は初期構築の記事を参照してください。

AWS構成.png

Terraform

最初の記事のおさらいです。
GitHubリソースを管理するためのGitHub Providerがあります。
このGitHub Providerを利用して、Organization・チームのメンバー管理を行います。
Organization・チームのメンバー情報はCSV、チーム情報はtfファイルで管理します。
Terraformのチュートリアルと同様にメンバー情報はCSVにまとめることでtfファイルの肥大化を防ぎます。
チーム情報に関しては、チュートリアルと異なりtfファイルで管理しています。

処理の流れ.png

organization.tf・version.tf

organization.tf

organization.tf
locals {
  owners     = csvdecode(file("../users/owners.csv"))
  members    = csvdecode(file("../users/members.csv"))
  test-team  = csvdecode(file("../users/test-team.csv"))
  exsit-team = csvdecode(file("../users/exsit-team.csv"))
}

module "organization" {
  source = "./module/terraform-github-organization"

  name = "example"

  owners  = local.owners[*].username
  members = local.members[*].username

  existing_teams = ["exist-team"]

  teams = [
    {
      name    = "Test Team"
      members = local.test-team[*].username
    },
    {
      name    = "exist-team"
      members = local.exsit-team[*].username
    }
  ]
}

organization.tfでは、モジュールに渡すOrganization・チームのメンバー情報の定義とモジュールの呼び出しを行っています。

チーム追加時などにこのorganization.tfを更新する必要があります。
詳細な利用方法は、organization.tfのREADME最初の記事を参照してください。
ポイントは、既存チームと新規チームを区別して管理できる点です。
teamsブロックで設定されたチームは基本的にTerraformによって新規作成されます。
existing_teamsパラメータで既存のTeamを指定することで、そのチームは新規作成せずメンバー管理のみを行う例外処理をモジュール側で設定しております。
既存のチームに関してはslug形式で指定する必要があります。
また、既存チームのメンバー管理ですが、既存のメンバーの情報をCSVに記載しますとそのメンバーはTerraform管理にすることができます。

versions.tf

versions.tf
# see https://www.terraform.io/docs/configuration/terraform.html
terraform {
  required_version = "~> 1.10.5"

  required_providers {
    github = {
      source  = "hashicorp/github"
      version = "~> 6.5.0"
    }
  }

  backend "s3" {
    bucket = "" #S3バケット名を指定
    key    = "" #tfstateファイル用のKeyを指定。特に命名規則がなければ、「{Org名}/{リポジトリ名}/tfstate」で指定することを推奨
    region = "ap-northeast-1"
    acl    = "bucket-owner-full-control"
  }
}

versions.tfは以下の設定を定義しております。

  • Terraformバージョン
  • GitHub providerバージョン
  • backend(tfstateファイル保管場所)

backendに関してはAWS上で構築したS3バケットとtfstateファイル名を記載してください。

モジュール

モジュールでは、Organization全体のメンバー管理とチーム管理の具体的な処理を実装しています。
variables.tfで変数とローカル値の定義を行い、main.tfでGitHubリソースの実際の操作を行う形で分けています。

variables.tf

variables.tf
variable "name" {
  type        = string
  description = "The name of the organization."
}

variable "owners" {
  type        = set(string)
  default     = []
  description = "List of owners."
}

variable "blocked_users" {
  type        = set(string)
  default     = []
  description = "List of blocked users."
}

variable "members" {
  type        = set(string)
  default     = []
  description = "List of members."
}

variable "teams" {
  type        = any
  default     = []
  description = "List of teams. This should be `teams` object."
}

variable "existing_teams" {
  type        = set(string)
  default     = []
  description = "List of existing team names"
}

locals {
  memberships = merge(
    { for u in var.owners : u => "admin" },
    { for u in var.members : u => "member" },
    {
      for u in setunion(
        flatten(local.teams[*].maintainers),
        flatten(local.teams[*].members)
      ) : u => "member" if ! contains(var.owners, u)
    }
  )

  # チーム設定の基本形
  teams = [
    for t in var.teams : merge({
      name        = ""
      description = ""
      visible     = true
      maintainers = []
      members     = []
    }, t)
  ]

  # 既存のTeamと新規のTeamを分離
  new_teams = { 
    for team in local.teams : team.name => team 
    if !contains(var.existing_teams, team.name)
  }

  teams_maintainers = flatten([
    for t in local.teams : [
      for u in t.maintainers : {
        team_name = t.name
        username  = u
        role      = "maintainer"
      }
    ]
  ])

  teams_members = flatten([
    for t in local.teams : [
      for u in t.members : {
        team_name = t.name
        username  = u
        role      = "member"
      }
    ]
  ])

  team_memberships = {
    for m in concat(local.teams_maintainers, local.teams_members) :
    "${m.team_name} ${m.username}" => m
  }

  users = setunion(
    var.owners,
    var.blocked_users,
    var.members,
    flatten(local.teams[*].maintainers),
    flatten(local.teams[*].members)
  )
}

variables.tfでは、モジュールで使用する変数の定義と、それらの変数を元としたローカル値を定義しています。

変数は大きく分けて、以下の値を受け取れるように定義しています。

  • Organization名
  • Owner(組織ロール)のメンバー一覧
  • Member(組織ロール)のメンバー一覧
  • ブロックユーザー一覧
  • チーム設定
  • 既存チーム一覧

これらの変数は、先ほど説明したorganization.tfから値が渡されます。

ローカル値は、以下の用途で利用します。

  • memberships: 組織全体のメンバーシップを管理
    • ownersリストのメンバーは"admin"、membersリストのメンバーは"member"として設定されます。
    • 3つ目のマップに関しては、Maintainer(チームロール)メンバーとMember(チームロール)メンバーの集合から、Owner(組織ロール)のメンバー以外はMember(組織ロール)を付与するようにしています
    • この"memberships"は、GitHub Providerのgithub_membershipで利用されます。github_membershipでは、"admin"を指定することでOwner(組織ロール)を割り当てられます。紛らわしい箇所ですので、ご留意ください。
    • github_membership Argument Reference
  • teams:チームの基本設定を定義
    • 名前、説明、可視性、Maintainer(チームロール)のメンバー一覧、Member(チームロール)のメンバー一覧といった項目について、デフォルト値を設定しつつ、organization.tfから渡された値で上書きできるようにしています。
  • new_teams:既存チームと新規チームで分離
  • teams_maintainers・teams_members: チームメンバーシップを管理
    • チームのMaintainer(チームロール)メンバーとMember(チームロール)メンバーの情報を管理します。
    • teams_maintainersでは、各チームのmaintainersリストから、"maintainer"として設定
    • teams_membersでは、各チームのmembersリストから、"member"として設定
  • team_memberships: チームメンバーシップのマッピング
    • teams_maintainersとteams_membersを結合し、一意のキーでマッピングします
    • キーは"チーム名 ユーザー名"の形式で、チームとユーザーの組み合わせを一意に特定できます
    • このマップは、GitHub Providerのgithub_team_membershipで利用され、各チームのメンバーシップを設定します
  • users: 全GitHub ユーザー
    • Owner(組織ロール)、Member(組織ロール)、ブロックユーザー、全チームのMaintainer(チームロール)とMember(チームロール)を含むすべてのGitHub ユーザーを定義します
    • データソースでユーザー情報を取得する際に利用されます

main.tf

main.tf
data "github_user" "main" {
  for_each = local.users
  username = each.key
}

# 既存のTeamを取得するデータソース
data "github_team" "existing" {
  for_each = var.existing_teams
  slug     = each.key
}

provider "github" {
  owner = var.name
}

resource "github_membership" "main" {
  for_each = local.memberships
  username = each.key
  role     = each.value
}

resource "github_organization_block" "main" {
  for_each = var.blocked_users
  username = each.key
}

# 新規のTeamのみを作成するリソース
resource "github_team" "main" {
  for_each    = local.new_teams
  name        = each.key
  description = each.value.description
  privacy     = each.value.visible ? "closed" : "secret"
}

# Team membership の設定を既存・新規両方に対応
resource "github_team_membership" "main" {
  depends_on = [github_membership.main]

  for_each = local.team_memberships
  team_id  = contains(var.existing_teams, each.value.team_name) ? data.github_team.existing[each.value.team_name].id : github_team.main[each.value.team_name].id
  username = each.value.username
  role     = each.value.role
}

まず、データソースとして以下の情報を取得します。

data "github_user" "main" {
  for_each = local.users
  username = each.key
}

data "github_team" "existing" {
  for_each = var.existing_teams
  slug     = each.key
}
  • github_user: variables.tfで作成したlocal.usersに含まれる全ユーザーの情報をGitHub APIから取得します
  • github_team: 既存チームの情報をGitHub APIから取得します。slugを使用してチームを特定します

次に、GitHub Providerの設定を行います.

provider "github" {
  owner = var.name
}
  • organization.tfから渡されたOrganization名をownerとして設定します
  • この設定により、以降のリソース操作は全て指定されたOrganizationに対して実行されます

続いて、Organization全体のメンバーシップを管理します

resource "github_membership" "main" {
  for_each = local.memberships
  username = each.key
  role     = each.value
}
  • variables.tfで作成したlocal.membershipsを使用して、各ユーザーの組織ロールを設定します
  • roleが"admin"の場合はOwner権限、"member"の場合はMember権限が付与されます

ブロックされたユーザーの管理も行います。

resource "github_organization_block" "main" {
  for_each = var.blocked_users
  username = each.key
}
  • organization.tfから渡されたblocked_usersに含まれるユーザーをOrganizationからブロックします

新規チームの作成します。

resource "github_team" "main" {
  for_each    = local.new_teams
  name        = each.key
  description = each.value.description
  privacy     = each.value.visible ? "closed" : "secret"
}
  • variables.tfで作成したlocal.new_teamsに含まれるチームのみを作成します
  • チームの可視性は、visibleがtrueの場合は"closed"(Organization内で表示)、falseの場合は"secret"(チームに所属しているメンバーのみ表示)となります

最後に、全てのチームのメンバーシップを管理します。

resource "github_team_membership" "main" {
  depends_on = [github_membership.main]

  for_each = local.team_memberships
  team_id  = contains(var.existing_teams, each.value.team_name) ? data.github_team.existing[each.value.team_name].id : github_team.main[each.value.team_name].id
  username = each.value.username
  role     = each.value.role
}
  • depends_onにより、組織のメンバーシップの設定が完了してからチームメンバーシップの設定を行います。
  • variables.tfで作成したlocal.team_membershipsを使用して、各チームのメンバーシップを設定します
  • チームが既存か新規かによって、参照するチームIDを切り替えています
  • roleが"maintainer"の場合はMaintainer権限、"member"の場合はMember権限が付与されます

GitHub Actions

GitHub ActionsによってPull Request作成時にterraform plan、Pull Requestマージ時にterraform applyが実行されます。

vscode-drop-1742714661068-gwqx1iieel.png

それぞれのワークフローの詳細な内容を解説します。

terraform plan用ワークフロー

plan.yml
name: terraform plan

on:
  pull_request:
    paths:
      - 'terraform/*.tf'
      - 'users/*.csv'
      - '.github/workflows/*.yml'

jobs:
  plan:
    name: terraform plan
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: write
      pull-requests: write
    steps:
      - name: checkout
        uses: actions/checkout@v3
        with:
          ref: ${{ github.head_ref }}

      - name: setup terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.10.5

      - uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: ap-northeast-1
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}

      - run: terraform init
        working-directory: terraform

      - id: fmt
        run: terraform fmt
        working-directory: terraform

      # terraform fmtでフォーマットされた場合、コミットしてプッシュ
      - name: Commit changes
        run: |
          if [[ -n "$(git status -s)" ]]; then
            git config --global user.name 'github-actions[bot]'
            git config --global user.email 'github-actions[bot]@users.noreply.github.com'
            git add .
            git commit -m "terraform fmt"
            git push origin ${{ github.head_ref }}
          else
            echo "No changes to commit"
          fi
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - id: validate
        run: terraform validate -no-color
        working-directory: terraform
        continue-on-error: true

      # terraform validate失敗時の結果をプルリクエストに出力
      - uses: actions/github-script@v6
        if: steps.validate.outcome == 'failure'
        env:
          STDERR: "```${{ steps.validate.outputs.stderr }}```"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: process.env.STDERR
            });
            core.setFailed('terraform validate failed');

      - name: Make script executable
        run: chmod +x ./script.sh
        working-directory: shell

      - name: Generate GitHub Apps token
        id: generate
        env:
          APP_ID: ${{ secrets.APP_ID }}
          PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
        run: |
          ./script.sh
        working-directory: shell

      - id: plan
        run: terraform plan -no-color
        working-directory: terraform
        continue-on-error: true
        env:
          GITHUB_TOKEN: ${{ steps.generate.outputs.token }}

      # terraform plan成功時・失敗時それぞれの結果をプルリクエストに出力
      - uses: actions/github-script@v6
        if: steps.plan.outcome == 'failure'
        env:
          STDERR: "```${{ steps.plan.outputs.stderr }}```"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: process.env.STDERR
            });
            core.setFailed('terraform plan failed');

      - uses: actions/github-script@v6
        if: steps.plan.outcome == 'success'
        env:
          STDOUT: "```${{ steps.plan.outputs.stdout }}```"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: process.env.STDOUT
            })

      - name: Revoke GitHub Apps token
        if: ${{ always() }}
        env:
          GITHUB_TOKEN: ${{ steps.generate.outputs.token }}
        run: |
          curl --location --silent --request DELETE \
            --url "${GITHUB_API_URL}/installation/token" \
            --header "Accept: application/vnd.github+json" \
            --header "X-GitHub-Api-Version: 2022-11-28" \
            --header "Authorization: Bearer ${GITHUB_TOKEN}"

まず最初に、ワークフローのトリガーを見ていきます。

on:
  pull_request:
    paths:
      - 'terraform/*.tf'
      - 'users/*.csv'
      - '.github/workflows/*.yml'

terraformやCSV、ワークフロー用ファイルの更新に対してプルリクエストが作成された際に、このterraform plan用のワークフローが実行されます。

次に、権限設定です。

jobs:
  plan:
    name: terraform plan
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: write
      pull-requests: write

基本的にはGITHUB_TOKENを利用します。
ただしGITHUB_TOKENはOrganizationのPermissionが設定できないため、Organizationのチームメンバーの管理の処理をするterraform planterraform applyのステップに関してはGitHub Appsトークンを利用します。
GITHUB_TOKEN のアクセス許可

各権限の利用用途は下記の通りです。

  • id-token: write
    • GitHubとAWSのOIDC認証用
  • contents: write
    • terraform fmtでフォーマットされた際にその変更をコミット・プッシュするためwrite権限を利用します。
  • pull-requests: write
    • Pull Requestへのコメント用

stepsの処理の流れを説明していきます

  1. 環境準備
      - name: checkout
        uses: actions/checkout@v3
        with:
          ref: ${{ github.head_ref }}

      - name: setup terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.10.5

      - uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: ap-northeast-1
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}

リポジトリのチェックアウト、Terraformのセットアップ、AWS認証の設定を行います。
AWS認証に関しては、初期構築で作成したS3アクセス用IAMロールを利用します。

  1. Terraformの初期化・フォーマット・検証
      - run: terraform init
        working-directory: terraform

      - id: fmt
        run: terraform fmt
        working-directory: terraform

      # terraform fmtでフォーマットされた場合、コミットしてプッシュ
      - name: Commit changes
        run: |
          if [[ -n "$(git status -s)" ]]; then
            git config --global user.name 'github-actions[bot]'
            git config --global user.email 'github-actions[bot]@users.noreply.github.com'
            git add .
            git commit -m "terraform fmt"
            git push origin ${{ github.head_ref }}
          else
            echo "No changes to commit"
          fi
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - id: validate
        run: terraform validate -no-color
        working-directory: terraform
        continue-on-error: true

      # terraform validate失敗時の結果をプルリクエストに出力
      - uses: actions/github-script@v6
        if: steps.validate.outcome == 'failure'
        env:
          STDERR: "```${{ steps.validate.outputs.stderr }}```"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: process.env.STDERR
            });
            core.setFailed('terraform validate failed');

Terraformの初期化、コードのフォーマット、構文検証を行います。
terraform fmtによってフォーマットされた場合は、botによるコミット・プッシュが実行されます。
teraform varidateで失敗した場合は、teraform varidateの出力結果をPull Requestにコメントとして投稿します。
terraform fmtteraform varidateを組み込むことで、基本的なコマンドをGitHub ホステッド ランナー上で完結するようにしています。
(ローカルでこれらのコマンドを実行する必要がなくなる)

  1. GitHub Apps トークンの生成
      - name: Make script executable
        run: chmod +x ./script.sh
        working-directory: shell

      - name: Generate GitHub Apps token
        id: generate
        env:
          APP_ID: ${{ secrets.APP_ID }}
          PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
        run: |
          ./script.sh
        working-directory: shell

先ほどの繰り返しとなりますが、Organizationのチームメンバーの管理の処理をするterraform planterraform applyのステップに関してはGitHub Appsトークンを利用します。
shellフォルダに格納されているGitHub Appsトークン生成用のシェルを実行することで、GitHub Appsトークンを生成しております。
今までの記事で紹介しました通り、GitHub Appsトークン生成スクリプトとGitHub Actionsワークフロー上のGitHub Appsトークン生成に関するステップは、下記の参考記事から引用させていただいております。
GitHub Appsトークン解体新書:GitHub ActionsからPATを駆逐する技術

  1. terraform planの実行と結果の通知
      - id: plan
        run: terraform plan -no-color
        working-directory: terraform
        continue-on-error: true
        env:
          GITHUB_TOKEN: ${{ steps.generate.outputs.token }}

      # terraform plan成功時・失敗時それぞれの結果をプルリクエストに出力
      - uses: actions/github-script@v6
        if: steps.plan.outcome == 'failure'
        env:
          STDERR: "```${{ steps.plan.outputs.stderr }}```"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: process.env.STDERR
            });
            core.setFailed('terraform plan failed');

      - uses: actions/github-script@v6
        if: steps.plan.outcome == 'success'
        env:
          STDOUT: "```${{ steps.plan.outputs.stdout }}```"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: process.env.STDOUT
            })

そして、生成したGitHub Apps トークンを利用してterraform planを実行します。
terraform planに関しては、成功事も失敗時も出力結果をPull Requestにコメントとして投稿します。
成功時は、リソースの変更内容が表示されますので、承認者はその変更内容を確認してPull Requestをマージするか判断します。

  1. GitHub Apps トークンの失効
      - name: Revoke GitHub Apps token
        if: ${{ always() }}
        env:
          GITHUB_TOKEN: ${{ steps.generate.outputs.token }}
        run: |
          curl --location --silent --request DELETE \
            --url "${GITHUB_API_URL}/installation/token" \
            --header "Accept: application/vnd.github+json" \
            --header "X-GitHub-Api-Version: 2022-11-28" \
            --header "Authorization: Bearer ${GITHUB_TOKEN}"

最後に不要となったGitHub Apps トークンを失効します。
こちらも下記の参考記事から引用させていただいております。
GitHub Appsトークン解体新書:GitHub ActionsからPATを駆逐する技術

terraform apply用ワークフロー

apply.yml
name: terraform apply

on:
  pull_request:
    types: [closed]
    branches:
      - main
    paths:
      - 'terraform/*.tf'
      - 'users/*.csv'
      - '.github/workflows/*.yml'

jobs:
  apply:
    name: terraform apply
    runs-on: ubuntu-latest
    if: github.event.pull_request.merged == true
    permissions:
      id-token: write
      contents: read
      pull-requests: write
    steps:
      - name: checkout
        uses: actions/checkout@v3

      - name: setup terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.10.5

      - uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: ap-northeast-1
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}

      - run: terraform init
        working-directory: terraform

      - name: Make script executable
        run: chmod +x ./script.sh
        working-directory: shell

      - name: Generate GitHub Apps token
        id: generate
        env:
          APP_ID: ${{ secrets.APP_ID }}
          PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
        run: |
          ./script.sh
        working-directory: shell

      - id: apply
        run: terraform apply -auto-approve -no-color
        working-directory: terraform
        continue-on-error: true
        env:
          GITHUB_TOKEN: ${{ steps.generate.outputs.token }}

      # terraform apply成功時・失敗時それぞれの結果をプルリクエストに出力
      - uses: actions/github-script@v6
        if: steps.apply.outcome == 'failure'
        env:
          STDERR: "```${{ steps.apply.outputs.stderr }}```"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: process.env.STDERR
            });
            core.setFailed('terraform apply failed');

      - uses: actions/github-script@v6
        if: steps.apply.outcome == 'success'
        env:
          STDOUT: "```${{ steps.apply.outputs.stdout }}```"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: process.env.STDOUT
            })

      - name: Revoke GitHub Apps token
        if: ${{ always() }}
        env:
          GITHUB_TOKEN: ${{ steps.generate.outputs.token }}
        run: |
          curl --location --silent --request DELETE \
            --url "${GITHUB_API_URL}/installation/token" \
            --header "Accept: application/vnd.github+json" \
            --header "X-GitHub-Api-Version: 2022-11-28" \
            --header "Authorization: Bearer ${GITHUB_TOKEN}"

terraform apply用ワークフローは、terraform plan用ワークフローとほぼ内容が同じです。
重複することは説明を割愛させていただきます。

最初にワークフローのトリガーを見ていきます。

on:
  pull_request:
    types: [closed]
    branches:
      - main
    paths:
      - 'terraform/*.tf'
      - 'users/*.csv'
      - '.github/workflows/*.yml'

jobs:
  apply:
    name: terraform apply
    runs-on: ubuntu-latest
    if: github.event.pull_request.merged == true

terraformやCSV、ワークフロー用ファイルの更新に対するプルリクエストがmainブランチにマージされた際に、このterraform apply用のワークフローが実行されます。

次に、権限設定です。

jobs:
  apply:
    name: terraform apply
    runs-on: ubuntu-latest
    if: github.event.pull_request.merged == true
    permissions:
      id-token: write
      contents: read
      pull-requests: write

terraform plan用のワークフローと違いファイル更新がないため、contentsをread権限にしております。
それ以外の権限はterraform plan用のワークフローと同じです。

stepsの処理の流れは以下の通りです。
terraform applyを実行するstep以外はほぼ同じです。
(terraform fmtterraform validateを実行するステップがない)

  1. 環境準備
  2. Terraformの初期化
  3. GitHub Apps トークンの生成
  4. terraform applyの実行と結果の通知
  5. GitHub Apps トークンの失効

4の「terraform applyの実行と結果の通知」に関しても、
terraform planと同様に成功事も失敗時も出力結果をPull Requestにコメントとして投稿します。

      - id: apply
        run: terraform apply -auto-approve -no-color
        working-directory: terraform
        continue-on-error: true
        env:
          GITHUB_TOKEN: ${{ steps.generate.outputs.token }}

      # terraform apply成功時・失敗時それぞれの結果をプルリクエストに出力
      - uses: actions/github-script@v6
        if: steps.apply.outcome == 'failure'
        env:
          STDERR: "```${{ steps.apply.outputs.stderr }}```"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: process.env.STDERR
            });
            core.setFailed('terraform apply failed');

      - uses: actions/github-script@v6
        if: steps.apply.outcome == 'success'
        env:
          STDOUT: "```${{ steps.apply.outputs.stdout }}```"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: process.env.STDOUT
            })

最後に

3つの記事に渡る長い長い内容となってしまいましたが、ここまで読んでいただきありがとうございます!

以上、クラウド事業本部の吉田でした!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.