はじめに
こんにちは、タイミーでPlatform Engineerをしている近藤です。
タイミーでは、インフラ管理においてTerraformを積極的に活用しています。 当初はAWSリソースの管理が中心でしたが、事業や組織の拡大に伴い、管理対象は多岐にわたるようになりました。
現在では、以下のような様々な用途でTerraformリポジトリが運用されています。
- AWSインフラ: メインとなるサービス基盤の構築・運用
- GCPインフラ: BigQueryなどのデータ分析基盤や、特定のGoogle Cloudリソース管理
- SaaSアカウント管理: GitHubやDatadogなどのユーザー・権限管理
- その他: Elastic Cloudなどの専用リソース管理
このように、クラウドプロバイダーも用途も異なる複数のリポジトリが存在する状況において、私たちは「Terraformワークフローの共通化」に取り組みました。
共通化前の課題
リポジトリが増えるにつれて、開発・運用チームは次のような課題に直面していました。
- ワークフローの実装と機能のバラつき リポジトリごとにCI/CDの設定がコピペと独自改変を繰り返して作られていたため、「あるリポジトリではLintが走るが、別のリポジトリでは走らない」「あるリポジトリではSlack通知が飛ぶが、別のリポジトリでは通知自体がない」といった不整合が起きていました。
- メンテナンスコストの増大 新しいセキュリティチェックツールの導入や、Terraformのバージョンアップに伴うCI修正を行おうとすると、全てのリポジトリに対して個別に修正PRを送る必要がありました。
- 新規リポジトリ作成への心理的ハードル 「リポジトリを分けたほうが責務が明確になる」と分かっていても、「またあのCI設定をコピーしてメンテナンスするのか」という負担が頭をよぎり、既存のリポジトリに無理やりリソースを追加してしまうケースがありました。
結果として、ワークフローの維持管理が大変そうで、新しい用途での活用を躊躇してしまうという状態になっていました。
解決策:共通Terraformワークフロー基盤の開発
これらの課題を解決するために、社内のTerraformリポジトリで利用するためのCI/CDワークフローを共通化し、単一のプロダクトとして管理する共通Terraformワークフロー基盤を開発しました。
安全かつ自動化された配布の仕組み
共通化にあたっては、変更が全リポジトリに波及するリスクを考慮し、安全に開発・配布できる仕組みを構築しました。
- サンドボックス用リポジトリでの開発 まず、Terraform用の実験リポジトリで機能追加や修正を行います。ここで実際にTerraformを動かし、動作を検証します。
- 共通ワークフロー基盤リポジトリへの自動PR
サンドボックスでの検証が完了して
mainブランチにマージされると、自動的に共通Terraformワークフロー基盤リポジトリに対してPull Requestが作成されます。 - 各リポジトリへの自動配布 共通Terraformワークフロー基盤側でPRをマージすると、その変更が対象となる全ての実利用リポジトリ(AWS用、GCP用、SaaS用など)に対して自動的に配布されます。
このサイクルにより、開発者は安心して共通基盤を改善でき、利用者も常に検証済みの最新ワークフローを享受できるようになりました。

同期ワークフローの実装
上記の配布フローは、GitHub Actions と GitHub App を使った同期ワークフローによって実現されています。具体的な実装を見ていきましょう。
同期ワークフローの全体像
name: Sync Workflow on: push: branches: [main] workflow_dispatch: jobs: # 配布先リポジトリ一覧を設定ファイルから読み込み load_repositories: outputs: repositories: ${{ steps.load.outputs.repositories }} steps: - uses: actions/checkout@v4 - id: load run: | REPOS=$(yq -o=json '.repositories' sync-repositories.yaml | jq -c) echo "repositories=$REPOS" >> "$GITHUB_OUTPUT" # 各リポジトリに対してマトリクス実行で同期 sync_workflow: needs: load_repositories strategy: matrix: include: ${{ fromJson(needs.load_repositories.outputs.repositories) }} steps: # GitHub Appで配布先リポジトリ用のトークンを発行 - uses: actions/create-github-app-token@v2 id: app-token with: app-id: ${{ vars.BOT_APP_ID }} private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} owner: ${{ matrix.owner }} repositories: ${{ matrix.name }} # ソースと配布先の両方をチェックアウト - uses: actions/checkout@v4 with: repository: ${{ matrix.owner }}/${{ matrix.name }} token: ${{ steps.app-token.outputs.token }} path: target-repo # sync-files.yaml に基づいてファイルをコピー - name: Copy workflow files run: .github/scripts/sync-copy-files.sh # 差分があればPRを作成 - name: Create PR if changed run: .github/scripts/sync-create-pr.sh env: GH_TOKEN: ${{ steps.app-token.outputs.token }}
配布先リポジトリの設定(sync-repositories.yaml)
配布先となるリポジトリは、設定ファイルで管理します。共通基盤リポジトリでは、実利用リポジトリすべてを配布先として定義しています。
# 共通基盤リポジトリの sync-repositories.yaml repositories: - owner: your-org name: repo-a # 利用リポジトリA - owner: your-org name: repo-b # 利用リポジトリB - owner: your-org name: repo-c # 利用リポジトリC # ... 他の利用リポジトリ
一方、サンドボックスリポジトリでは、共通基盤リポジトリのみを配布先として定義します。
# サンドボックスの sync-repositories.yaml repositories: - owner: your-org name: workflow-base # 共通基盤リポジトリのみ
同期対象ファイルの設定(sync-files.yaml)
同期するファイルと除外するファイルは、設定ファイルで明確に管理します。
# .github/config/tf/sync-files.yaml # 同期対象のファイル・ディレクトリ files: - .github/config/tf/ # 共通設定ファイル - .github/workflows/ # ワークフローファイル - .github/actions/tf_* # カスタムアクション # 除外対象(リポジトリ固有の設定) exclude: - .github/workflows/tf_sync_workflow.yml # 同期WF自体 - .github/config/tf/sync-files.yaml # 同期設定 - .github/config/tf/sync-repositories.yaml - .github/config/tf/.env.local # ローカル環境変数 - .github/config/tf/dirs.json # 対象ディレクトリ一覧 - .github/config/tf/filters.local.yaml # ローカルフィルタ - .github/config/tf/.tflint.hcl # tflint設定 - .github/config/tf/trivy.yaml # trivy設定
このように、共通化すべきファイルと、リポジトリ固有であるべきファイルを明確に分離することで、柔軟性を保ちながら一貫性を維持しています。
設定ファイルによる振る舞いの制御
共通ワークフローの振る舞いは、階層化された設定ファイルによって柔軟に制御できます。
設定ファイルの階層構造
.github/config/tf/
├── .env # グローバル設定(共通)
├── .env.local # リポジトリ固有の設定
├── dirs.json # 対象ディレクトリ一覧
├── filters.yaml # グローバル変更検出フィルタ
├── filters.local.yaml # ローカル変更検出フィルタ
├── .tflint.hcl # tflint設定
└── trivy.yaml # trivy設定
envs/
├── production/
│ └── .env # ディレクトリ固有の設定
└── staging/
└── .env
グローバル設定(.env)
全リポジトリで共通の設定です。同期によって配布されます。主な設定項目は以下の通りです。
- Terraform実行設定: 並列数(
TF_MAX_PARALLEL_JOBS)、CLI引数(TF_CLI_ARGS_plan)など - Git設定: コミット時のユーザー名・メールアドレス
- Slack通知設定: ステータスごとの色・メッセージ・絵文字・アイコンを
SLACK_STATUS_{STATUS}_{PROPERTY}形式で定義
Slack通知の設定はデータとして定義し、ワークフロー側ではシェルの間接変数展開を使ってジョブのステータス(success / failure / cancelled / maintenance)に応じた値を動的に参照する設計にしています。
リポジトリ固有・ディレクトリ固有の設定
グローバル設定を上書きする設定は、以下の2箇所で定義できます。
.env.local: リポジトリ全体に適用される設定(同期の除外対象)envs/{env}/.env: 特定のディレクトリにのみ適用される設定
主な設定項目:
TF_MAX_PARALLEL_JOBS: APIレート制限を考慮した並列数の上書きAWS_ROLE_TO_ASSUME/GCP_WORKLOAD_IDENTITY_PROVIDER: 認証設定SLACK_CHANNEL_ID/SLACK_MENTION_NAME: 通知先チャンネルとメンション
対象ディレクトリ設定(dirs.json)
terraform plan/apply の対象ディレクトリを定義します。
[ "envs/production", "envs/staging", "envs/development" ]
変更検出フィルタ(filters.yaml)
共通モジュールや設定ファイルの変更時に全ディレクトリを再実行するためのグローバルフィルタです。
# .github/config/tf/filters.yaml(共通) terraform_trigger: - modules/ - .github/config/tf/ - .github/actions/tf_*/** - .github/workflows/tf_* - .github/workflows/*tf** - .terraform-version
# .github/config/tf/filters.local.yaml(リポジトリ固有) terraform_trigger: - shared/ # リポジトリ固有の共有モジュール
認証処理の共通化(tf_init アクション)
認証処理はComposite Actionとして共通化し、AWS OIDC / GCP Workload Identity Federation の両方に対応しています。
# .github/actions/tf_init/action.yml name: Terraform Init inputs: directory: required: true credentials: required: false runs: using: composite steps: # リポジトリ固有のパッチがあれば適用 - name: Check if tf_patch exists id: check-patch shell: bash run: | if [ -d ".github/actions/tf_patch" ]; then echo "exists=true" >> "$GITHUB_OUTPUT" fi - name: Apply patches if: steps.check-patch.outputs.exists == 'true' uses: ./.github/actions/tf_patch with: directory: ${{ inputs.directory }} # Terraformバージョンのセットアップ - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_wrapper: false terraform_version: ${{ steps.var.outputs.terraform-version }} # 環境変数の読み込み(グローバル → ローカル → ディレクトリ固有) - name: Load environment variables shell: bash run: | for env_file in \ ".github/config/tf/.env" \ ".github/config/tf/.env.local" \ "${{ inputs.directory }}/.env"; do if [ -f "$env_file" ]; then envsubst < "$env_file" >> "$GITHUB_ENV" fi done # AWS OIDC認証(設定がある場合) - name: Configure AWS credentials if: env.AWS_ROLE_TO_ASSUME != '' uses: aws-actions/configure-aws-credentials@v4 with: aws-region: ${{ env.AWS_REGION }} role-to-assume: ${{ env.AWS_ROLE_TO_ASSUME }} # GCP Workload Identity認証(設定がある場合) - name: Authenticate to Google Cloud if: env.GCP_WORKLOAD_IDENTITY_PROVIDER != '' uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ env.GCP_SERVICE_ACCOUNT }} # Terraform init - name: Terraform init shell: bash run: | terraform -chdir=${{ inputs.directory }} init
導入成果
運用面での改善
共通基盤を1箇所修正するだけで、全リポジトリに改善が行き渡るようになりました。セキュリティスキャンの追加やツールのバージョンアップも、一括で適用可能です。
また、どのリポジトリを触っても、「PRを出せばPlanが走り、結果がコメントされる」「マージすればApplyされる」という同じ挙動が保証されるようになりました。これにより、エンジニアが新しいリポジトリを触る際の学習コストが下がりました。
技術的に実現できたこと
共通Terraformワークフロー基盤では、インフラ運用に必要な仕組みを次のような形でまとめて提供しています。
- Terraform実行パイプラインの共通化: PRごとに
terraform plan、mainマージ時にterraform applyを自動実行し、複数環境をマトリクス実行しながら並列数を制御。 - 実行の安全性とレートリミット対策: ワークフローのコンカレンシーやキャンセルポリシーを設定し、不要な再実行を避けつつ、GitHub API やクラウドプロバイダAPIのレートリミットにかからないように実行を制御。
- 品質・セキュリティチェックの標準化:
terraform fmt/validate/tflint/trivyに加え、各種Lintを一括で走らせ、結果をPRレビューとしてフィードバック。 - ドリフト検出と自動PR: 定期的なPlanで実環境とのドリフトを検出し、差分があれば自動でPRを作成・管理。
- PRレビュー体験の強化: Plan/Apply結果や診断結果に加え、AIベースのPRエージェントによる要約・レビューコメント生成でレビュー負荷を軽減。
- 認証と権限管理の一貫性: AWS OIDC / GCP Workload Identity Federation とアクセスキー認証の両方に対応し、
plan/applyごとに適切な認証情報を選択できるよう統一。
設計時に意識したこと
Terraformワークフローを「単なるCI設定」ではなく、継続的に拡張・改善していける共通基盤にするために、次のような設計方針と実装上の工夫を取り入れています。
1つのソースから複数リポジトリへ安全に配布できること
共通ワークフローの更新がそのまま全リポジトリに波及するため、必ずサンドボックスで検証してから共通基盤に取り込む二段階フローにしています。その上で、GitHub App を使ったトークン発行と同期専用ワークフローにより、「人手でコピペせずに、レビュー済みの変更だけを各リポジトリへ配布する」という流れを自動化しました。
配布方法としては、reusable workflow を使って直接呼び出す案も検討しました。しかし、ユースケースが社内限定であること、配布時の差分をPRとして確認できたほうがレビューしやすいこと、呼び出し元ワークフロー側の設定も含めて将来的に変更する可能性があることから、GitHub App で各リポジトリにPRを作成する方式を選びました。
設定ファイルで振る舞いを切り替えられること
Terraform の実行対象やトリガーとなるファイルパターンなどは、ワークフロー内にベタ書きせず、グローバル設定とリポジトリごとの設定ファイルをマージして解決する構造にしています。これにより、「共通のベースラインは維持しつつ、プロジェクトごとに監視対象や実行頻度だけ変える」といった調整をコード変更なしで行えるようにしました。
認証・権限まわりをワークフロー側で肩代わりすること
AWS OIDC / GCP Workload Identity Federation によるキーレス認証と、従来のアクセスキー認証の両方をラップし、plan / apply などのアクションごとに適切な権限を選び分けられるようにしています。個々のリポジトリでは「どのロール/サービスアカウントを使うか」だけを意識すればよく、認証フローそのものの実装は共通基盤に閉じ込めています。
共通ワークフローと各リポジトリの実装を柔軟につなげられること
ワークフローの中で、Terraformファイルに対して小さなパッチを当てる専用アクションtf_patchを用意しています。
例えば、各リポジトリの providers.tf には、過去の経緯からローカル開発用のAWS認証設定(プロファイル指定やロール引き受け設定)が含まれています。CI環境ではOIDC認証を使用するためこれらの設定は不要であり、むしろ干渉してしまいます。tf_patch アクションは terraform initの前にこれらの行を自動的に削除し、どのリポジトリでも同じOIDC認証の前提で共通ワークフローを実行できるようにしています。
このように、共通ワークフローと各リポジトリ固有の実装の間をつなぐアダプタとして機能させることで、既存コードへの変更は最小限に抑えたまま共通化を進められるようにしています。
現状の課題
共通化によって多くのメリットを得られましたが、運用を続ける中でいくつかの改善点も見えてきました。
同期除外対象の管理
現在、同期対象から除外するファイルは sync-files.yaml にファイル名を個別で定義しています。しかし、除外対象が増えるにつれてこのリストの管理が煩雑になってきました。ディレクトリ構造を整理し、「共通化するもの」と「リポジトリ固有のもの」を明確にディレクトリで分離することで、除外設定をシンプルにできる余地があります。
ファイルの差分管理
同期ワークフローは「ソースリポジトリにあるファイルを配布先にコピーする」という動作をしますが、ソースリポジトリでファイルを削除・移動した場合に、配布先の古いファイルを自動で削除するような仕組みにはなっていません。そのため、ファイル構成を変更した際には、配布先リポジトリに不要なファイルが残ってしまうことがあります。この問題も、上述のディレクトリ構造の見直しによって対応できると考えています。現状では、AIコーディングエージェントの Devin を活用して、不要になったファイルのお掃除PRを作成してもらうなど、人力での対応を行っています。
おわりに
共通Terraformワークフロー基盤によって、インフラや各種 SaaS アカウント管理のワークフローは一貫性を持って運用できるようになり、個々のリポジトリごとに CI を実装・メンテナンスする負担は大きく減りました。そのうえで、開発者は「リポジトリごとの Terraform の中身」に集中しやすくなり、ワークフロー自体は共通基盤として継続的に改善していける状態になっています。
また、この記事で紹介したのと同じような考え方で、アプリケーションのモノレポに対するデプロイワークフローも共通化しています。こちらについても、機会があれば別のエントリとして詳しく紹介できればと思います。













