ローカルでTerraformを管理していると、ステートファイルの共有や環境別デプロイが課題になりがちです。
本記事では、GitLabのステート管理機能とCI/CDを活用して、
安全なステート管理とマルチ環境デプロイを実現する方法を紹介します。
具体的な実装例を交えながら、同じ課題に悩む方へのヒントとして共有します。
公式ドキュメントを参考にしながら進めてみましたが、意外と面倒で理解が進まなかったので、備忘録としても残しておこうかと思った次第です。
環境
- GitLab: SaaS版 Ultimate(Terraform State管理、CI/CDパイプライン実行環境)
- Terraform: >= 1.6.0
- AWS: S3, Lambda(python), DynamoDB などを使用した環境
*AWSの環境はTerraformで作れるものであれば、何でも応用が利くと思います。
*GitLabのTerraform State管理機能は、SaaS版ではFreeプランから利用可能です。セキュリティスキャン機能の一部はUltimateプランで利用できます。
現状と課題
ローカルでのTerraform管理の問題点
プロジェクト開始当初、Terraformをローカル環境で実行していました。この運用には以下の課題がありました:
-
ステートファイルの共有問題
- チームメンバー間でステートファイルを共有する仕組みがない
- 誰かが最新のステートを持っているか不明確
- 同時実行によるステート破損のリスク
-
環境別デプロイの煩雑さ
- dev/stg/prod環境ごとに手動で変数を切り替え
- 環境間での設定ミスが発生しやすい
- デプロイ手順が属人化
自分が特に困ったのは、ステートファイルの共有問題で、チーム内でメンバーがステートファイルをローカルに持ってしまうと引継ぎできず、修正加えられなかったりCIに移行できなかったりで、なかなか苦労した経緯もあります。
また、実際には起きませんでしたが、環境が破損しステートファイルを失ってしまうとどうにもならないため、早いところ移行しなければと思っていました。
GitLabのセキュリティ機能も活用したい
ステート管理の移行先としてGitLabを調査していたところ、CI/CDと統合されたセキュリティスキャン機能が充実していることがわかりました:
- SAST (Static Application Security Testing): コードの静的解析
- Dependency Scanning: 依存ライブラリの脆弱性チェック
- Secret Detection: シークレット情報の誤コミット検出
- SAST-IaC: Terraformコードのセキュリティチェック
これまでは手動でのコードレビューに頼っていましたが、せっかくGitLabに移行するなら、これらの機能も活用してセキュリティ品質を向上させたいと考えました。
やりたいこと(解決したい課題)
上記の課題を解決するため、以下を実現したいと考えました:
-
GitLabでの集中ステート管理
- チーム全体で安全にステートを共有
- ロック機能による同時実行の防止
-
マルチ環境対応の自動化
- ブランチ/タグ戦略による環境の自動判定
- 環境ごとの変数管理の自動化
-
CI/CDパイプラインの構築
- MRでの自動plan実行
- mainブランチでのapply実行
-
セキュリティ品質の向上(せっかくなので)
- SAST、Dependency Scanning、Secret Detection の統合
- IaCコードのセキュリティチェック
- 脆弱性の早期発見と自動レポート
GitLabを使わない場合であれば、リモート(S3 + DynamoDB)に配置するとか、TerraformCloudを使うなど、代替案もあるかと思います。ですが、今回は、環境ごとのデプロイとか、脆弱性試験の実行、それを同じツールで管理したいということで、GitLabを使っている次第です。
GitLabのステート管理機能とは
GitLabは HTTP Backend 機能を提供しており、Terraformのステートファイルを安全に管理できます。
主な特徴
- プロジェクトごとのState管理: GitLabプロジェクト内でステートを管理
- ロック機能: 同時実行を防止し、ステートの整合性を保証
- バージョン管理: ステートの履歴を保持
- アクセス制御: GitLabの権限管理を活用
backend設定例
GitLabのHTTP Backendを使用してステートを管理します。
Backend URLの構成要素:
https://gitlab.com/api/v4/projects/{PROJECT_ID}/terraform/state/{STATE_NAME}
-
https://gitlab.com/api/v4: GitLab APIのベースURL(固定) -
{PROJECT_ID}: GitLabプロジェクトのID(数値) -
{STATE_NAME}: ステートファイルの名前(任意の文字列)
設定手順:
-
GitLabプロジェクトIDを確認
- GitLab UI でプロジェクトを開く
- プロジェクト名の下に表示されている
Project ID: 12345678を確認 - または
Settings>Generalで確認可能
-
State名を決定
- 環境ごとに異なる名前を使用(例:
IaC-dev,IaC-stg,IaC-prod) - 英数字、ハイフン、アンダースコアが使用可能
- プロジェクト内で一意である必要がある
- 環境ごとに異なる名前を使用(例:
-
GitLab UIでBackend設定を確認(推奨)
- GitLab UI で
操作>環境>Terraform ステートを開く - 右上の
Terraform init コマンドをコピーをクリック - 表示されるコマンドに、プロジェクトIDを含む正しいURLが記載されています
- このURLを参考に
backend.tfを作成できます
- GitLab UI で
-
backend.tfを作成
例: プロジェクトID が 12345678、State名を IaC-dev とする場合
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.50"
}
}
backend "http" {
# State取得・更新用のURL
address = "https://gitlab.com/api/v4/projects/12345678/terraform/state/IaC-dev"
# State ロック用のURL
lock_address = "https://gitlab.com/api/v4/projects/12345678/terraform/state/IaC-dev/lock"
unlock_address = "https://gitlab.com/api/v4/projects/12345678/terraform/state/IaC-dev/lock"
lock_method = "POST"
unlock_method = "DELETE"
retry_wait_min = 5
}
}
重要:
- このURLは GitLab APIのエンドポイント であり、事前にGitLab UIで作成する必要はありません
-
terraform init実行時に、指定したState名で自動的にステートが作成されます - 作成後は
操作>環境>Terraform ステートで確認できます
参考: GitLab-managed Terraform state | GitLab
ステートファイルのGitLabへの移行手順
1. backend.tf の作成
環境ごとに backend.tf を作成します。
# ディレクトリ構成
spa-infra/iac/
├── envs/
│ ├── dev/
│ │ ├── backend.tf # 開発環境用
│ │ ├── main.tf
│ │ └── terraform.tfvars
│ ├── stg/
│ │ └── backend.tf # ステージング環境用
│ └── prod/
│ └── backend.tf # 本番環境用
└── modules/
2. GitLab認証情報の設定
ローカル環境での実行時
GitLabのPersonal Access Tokenを作成し、環境変数に設定します。
Personal Access Tokenの作成手順:
- GitLab UI で右上のアバター >
Edit profileを開く - 左メニューから
Access Tokensを選択 - 以下を入力:
- Token name:
terraform-local(任意の名前) - Expiration date: 有効期限を設定(推奨: 90日以内)
- Scopes:
apiにチェック
- Token name:
-
Create personal access tokenをクリック - 表示されたトークンをコピー(再表示できないため必ず保存)
参考: Personal access tokens | GitLab
環境変数への設定:
# ユーザー名は任意の文字列(GitLabのユーザー名を推奨)
# パスワードには作成したPersonal Access Tokenを設定
export TF_HTTP_USERNAME="your-gitlab-username"
export TF_HTTP_PASSWORD="glpat-xxxxxxxxxxxxxxxxxxxx" # 作成したトークン
補足:
-
TF_HTTP_USERNAMEは認証に使用されますが、GitLabのユーザー名である必要はありません -
TF_HTTP_PASSWORDに設定するPersonal Access Tokenが実際の認証情報です - CI/CD環境では後述の
CI_JOB_TOKENを使用するため、この設定は不要です
CI/CD環境での実行時
CI/CD環境では、GitLabが自動的に CI_JOB_TOKEN を提供するため、Personal Access Tokenの作成は不要です。
terraform init \
-backend-config=username=gitlab-ci-token \
-backend-config=password=${CI_JOB_TOKEN}
補足:
-
gitlab-ci-tokenは固定値(GitLabのCI/CD専用ユーザー名) -
CI_JOB_TOKENはGitLabが自動的に生成・提供する一時トークン
3. ステートの移行
既存のローカルステートがある場合、以下のコマンドで移行します:
cd spa-infra/iac/envs/dev
# 初期化(移行を確認)
terraform init -migrate-state
# 確認
terraform state list
4. 動作確認
# plan実行
terraform plan
# ステートがGitLabに保存されていることを確認
# GitLab UI: Project > Infrastructure > Terraform states
CI/CDパイプラインの構築
注記:
本記事では .gitlab-ci.yml の主要部分を抜粋して紹介しています。
実際の統合時は、プロジェクトの要件に合わせて各セクションを組み合わせてください。
ブランチ/タグ戦略
環境ごとに異なるトリガーを設定し、自動的に適切な環境へデプロイします。
| 環境 | トリガー | 自動apply |
|---|---|---|
| dev |
main ブランチ |
手動 |
| stg |
v*.*.*-stg タグ |
手動 |
| prod |
v*.*.*-prod タグ |
手動 |
workflow設定
.gitlab-ci.yml でworkflowルールを定義します(抜粋):
workflow:
rules:
# マージリクエスト: dev環境でplanまでチェック
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
variables:
TF_ROOT: spa-infra/iac/envs/dev
TF_REGION: "ap-northeast-1"
TF_AWS_ACCOUNT_ID: XXXXXXXXXX # 開発環境
TF_AUTO_DEPLOY: "false"
TF_STATE_NAME: "IaC-dev"
# mainブランチ: dev環境
- if: $CI_COMMIT_REF_NAME == "main"
variables:
TF_ROOT: spa-infra/iac/envs/dev
TF_REGION: "ap-northeast-1"
TF_AWS_ACCOUNT_ID: XXXXXXXXXX
TF_AUTO_DEPLOY: "false"
TF_STATE_NAME: "IaC-dev"
# タグベース: staging環境(v*.*.*-stg)
- if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+-stg$/'
variables:
TF_ROOT: spa-infra/iac/envs/stg
TF_AWS_ACCOUNT_ID: YYYYYYYYYY
TF_STATE_NAME: "IaC-stg"
# タグベース: production環境(v*.*.*-prod)
- if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+-prod$/'
variables:
TF_ROOT: spa-infra/iac/envs/prod
TF_AWS_ACCOUNT_ID: ZZZZZZZZZZ
TF_STATE_NAME: "IaC-prod"
Terraformジョブの定義
.gitlab-ci.yml のジョブ定義部分(抜粋):
image:
name: hashicorp/terraform:latest
entrypoint: [""]
stages:
- test
- get-aws-session
- terraform_plan
- terraform_apply
# Plan実行
terraform_plan:
stage: terraform_plan
resource_group: ${TF_STATE_NAME}
script:
- terraform --version
- cd ${TF_ROOT}
- terraform init -backend-config=username=gitlab-ci-token -backend-config=password=${CI_JOB_TOKEN}
- terraform plan -var-file="terraform.tfvars" -out=tfplan
# Apply実行(手動トリガー)
terraform_apply:
stage: terraform_apply
resource_group: ${TF_STATE_NAME}
script:
- cd ${TF_ROOT}
- terraform init -backend-config=username=gitlab-ci-token -backend-config=password=${CI_JOB_TOKEN}
- terraform apply -var-file="terraform.tfvars" --auto-approve
rules:
- if: '$TF_AUTO_DEPLOY == "true"'
when: on_success
- when: manual # 基本的に手動実行
セキュリティテストの統合
GitLabの組み込みセキュリティスキャンを活用します(抜粋):
include:
- template: Security/Dependency-Scanning.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml
- template: Security/SAST-IaC.gitlab-ci.yml
variables:
GIT_DEPTH: "0" # SAST系の推奨設定
SAST_DISABLED: "false"
DEPENDENCY_SCANNING_DISABLED: "false"
SECRET_DETECTION_DISABLED: "false"
SAST_IAC_DISABLED: "false"
各スキャンの役割
- SAST (Static Application Security Testing): Pythonコードの静的解析
- Dependency Scanning: 依存ライブラリの脆弱性チェック
- Secret Detection: シークレット情報の誤コミット検出
- SAST-IaC: Terraformコードのセキュリティチェック
セキュリティテストは、GitLabのマニュアルページを見ると、まず初めにこの辺をやっておけ的なバイブルに基づき、以下4項目のテストを追加します。他は、必要に応じて追加してください。
動作確認
1. マージリクエストでのplan実行
MRを作成すると、自動的に以下が実行されます:
- セキュリティスキャン(SAST, Dependency Scanning等)
- Terraform plan(dev環境)
# MR作成
git checkout -b feature/add-new-resource
git add .
git commit -m "feat: Add new Lambda function"
git push origin feature/add-new-resource
GitLab UIでMRを作成すると、パイプラインが自動実行されます。
2. mainブランチでのapply
MRがマージされると、mainブランチでパイプラインが実行されます:
-
terraform_planが自動実行 -
terraform_applyは手動トリガー(安全のため)
# GitLab UI でパイプラインを確認
# terraform_apply ジョブを手動実行
3. 本番環境へのデプロイ
タグを作成すると、対応する環境へデプロイされます:
コマンドで実行するには、以下のようなコマンド構成になります。
GitLab UIを活用して、慣れるまで画面を見ながらタグ作成と実行でも良いと思います。
# ステージング環境
git tag v1.0.0-stg
git push origin v1.0.0-stg
# 本番環境
git tag v1.0.0-prod
git push origin v1.0.0-prod
4. セキュリティスキャン結果の確認
GitLab UI で以下を確認できます:
- Security Dashboard: 検出された脆弱性の一覧
- Merge Request: MRごとのセキュリティレポート
- Pipeline: 各スキャンの実行結果
導入後の効果と今後の改善点
導入効果
-
ステート管理の安全性向上
- チーム全体で最新のステートを共有
- ロック機能により同時実行を防止
- ステート破損のリスクが大幅に減少
-
デプロイの自動化と標準化
- ブランチ/タグ戦略により環境を自動判定
- 手動での変数切り替えが不要に
- デプロイ手順の属人化を解消
-
セキュリティ品質の向上
- 全てのMRで自動的にセキュリティスキャン
- 脆弱性の早期発見
- シークレット情報の誤コミット防止
-
開発速度の向上
- MRでのplan自動実行により、レビュー時に変更内容を確認可能
- 環境構築の手間が削減
- チームメンバー全員が同じ手順でデプロイ可能
今後の改善点
-
テスト環境の追加
- E2Eテストを実行する専用環境の構築
- テスト結果に基づく自動デプロイ
CIの中に脆弱性テストまでは盛り込めました。次のステップとしてE2Eテストの導入を検討しています。
GitLab自体の機能も豊富なので、勉強しながら、使える機能を増やしていこうと思います。
まとめ
ステート管理の移行に合わせて、いくつかの機能を追加しました。
GitLabのステート管理機能とCI/CDを活用することで、以下を実現できました:
- 安全なステート管理: チーム全体での共有とロック機能
- マルチ環境対応: ブランチ/タグ戦略による自動化
- セキュリティ品質向上: 自動スキャンによる脆弱性の早期発見
もともと目的としていた、個人にステートが張り付いて、複数人で管理できないという問題を解決することができました。
また、タグ管理を追加することによって、ステージング環境や本番環境にどのコミットがデプロイされているかということもわかりやすくなったのは大きな収穫です。
それだけでなく、セキュリティスキャンの統合により、あまり手間をかけることなくレビュー時に脆弱性を発見できるようになったことは大きな成果です。
今後は、テスト環境の追加やコスト最適化など、さらなる改善を進めていきます。
本記事が、Terraformのステート管理やCI/CD構築の参考になれば幸いです。