AWS本番環境をTerraform化してみた:第三回 Terraform環境別設定管理術
この記事は全七回の連載予定です!
- 第一回: インポート戦略編
- 第二回: リソースのインポート-ネットワーク、IAM編
- 第三回: Terraform環境別設定管理術<このページ>
- 第四回: リソースのインポート-ECS、タスク定義管理編※執筆中
- 第五回: リソースのインポート-RDS、S3、CloudFront編※執筆中
- 第六回: リソースのインポート-Route53、SES、SNS編※執筆中
- 第七回: 総括※執筆中
第一回: インポート戦略編では移行方針を立て、第二回: リソースのインポート-ネットワーク、IAM編ではインポートを実践しました。
今回は、本番環境と他環境の差分を吸収しながら、開発・ステージング・本番環境を統一的に管理するための一工夫を行なっていきます。
背景と課題
本番環境は手動で構築されたため、命名規則が統一されていない問題があります。Terraformで今後立ち上げる環境にはきちんとした命名規則を適用したいものです。
しかし同じTerraformコードで管理しようとすると、以下の問題に直面しました。
- 本番環境のリソース名が統一されていない(
wish-prod-publicvswish-env) - CloudWatchログの大文字小文字問題(
/ecs/Wishvs/ecs/wish) - 開発環境のみALBにIP制限を適用したい
- 環境ごとに異なるS3バケット名やドメイン名の管理
これらを個別に対応すると、コードが煩雑になり保守性が著しく低下します。
しかし「リソース名が変わるとリソースが再作成される」というTerraformの特性上、正確に環境ごとのリソース名を生成する必要があります。RDSのクラスター名を変えたらデータベースが消えます。
対象読者
- Terraformで複数環境を管理しているエンジニア
- 既存環境の命名規則の不統一に悩んでいる方
この記事からわかること
- 条件付きリソース名を使った環境差分の吸収方法
- 本番環境だけ異なる命名規則への対処法
- 環境別のセキュリティ設定の実装
この記事では説明しないこと
- Terraformのworkspace機能の基本
- tfvarsファイルの基本的な使い方
方針検討
本番環境はプロジェクト初期に手動で構築され、例えば以下のような名前が付けられていました。
| リソース | 本番環境 | 理想形 |
|---|---|---|
| S3公開バケット | wish-prod-public | wish-production-public |
| RDSクラスター | wish-prod-dbcluster | wish-production-dbcluster |
| CloudWatchログ | /ecs/Wish | /ecs/wish/production |
「本番環境のみ特殊な名前を維持し、他の環境では理想的な命名規則を使う」ためにシンプルに条件付きリソース名で環境差分を吸収します。
理想でない命名規則のリソースだけ切り出して本番と他環境をわける手もなくはないですが、 依存関係の管理が破綻しそうなので見送りました。
環境判定と条件付きリソース名
環境差分を吸収するため、localsブロックにて環境ごとのリソース名を定義します。まず環境を判定し、その結果に基づいてリソース名を動的に決定します。
variables.tfでの実装:
variable "environment" {
description = "Environment name"
type = string
}
variable "project_name" {
description = "Project name"
type = string
default = "wish"
}
locals {
# 本番環境の判定フラグ
is_production = var.environment == "production"
# S3バケット名
# 本番: 既存の命名規則を維持(wish-prod-public)
# 他環境: 統一された命名規則を適用(wish-development-public)
s3_public_bucket_name = local.is_production ? "wish-prod-public" : "${var.project_name}-${var.environment}-public"
# RDSクラスター名
rds_cluster_name = local.is_production ? "wish-prod-dbcluster" : "${var.project_name}-${var.environment}-dbcluster"
# CloudWatchログプレフィックス
# 本番: 大文字の "Wish" で既に運用中のため変更不可
# 他環境: 小文字 + 環境名を含む階層構造
log_prefix = local.is_production ? "/ecs/Wish" : "/ecs/${var.project_name}/${var.environment}"
#...その他のリソース名の定義が続く
}
実際のリソース定義での使用
これらの条件付きリソース名を実際のリソース定義で使用します。リソース定義はlocal値を参照するだけで、こちらに分岐を書かないのがポイントです。
tfファイルに分岐を書くと式の評価結果としてのリソース名が既存のリソース名と同一であっても、異なるリソースと判定され再作成の対象になります<重要!これで小一時間つまりました>。
恐らくですがTerraformの実行順序が、
- リソースの識別
- 式の評価
なので、1の段階では「式そのもの」がリソース識別子として用いられているのだと思います。
それを踏まえ、tfファイルでの実装例:
resource "aws_s3_bucket" "public" {
bucket = local.s3_public_bucket_name
tags = {
Name = local.s3_public_bucket_name #local定義を参照
Environment = var.environment
Project = var.project_name
}
}
resource "aws_rds_cluster" "main" {
cluster_identifier = local.rds_cluster_name #local定義を参照
database_name = var.rds_database_name
master_username = var.rds_master_username
master_password = var.rds_master_password
# 他の設定...
}
環境別セキュリティ設定
環境ごとにセキュリティ要件が異なることは常です。今回のケースでは
- 開発環境:社内からのみアクセス可能(IP制限)
- 本番環境:一般公開
この条件分岐は、tfファイルに直接記載して問題ありません。
開発環境のみALBにIP制限を適用する例:
resource "aws_security_group_rule" "alb_ingress_https" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
# 開発環境のみIP制限、他は全開放
# development: var.alb_allowed_ips の値(社内IPリスト)を使用
cidr_blocks = var.environment == "development" ? var.alb_allowed_ips : ["0.0.0.0/0"]
security_group_id = aws_security_group.alb.id
description = "HTTPS from ${var.environment == "development" ? "allowed IPs" : "anywhere"}"
}
環境別変数ファイル
各環境の固有値はtfvarsファイルにまとめておきましょう。
development.tfvarsの例:
environment = "development"
project_name = "wish"
# ALB Access Control (development only)
# 開発環境では特定のIPアドレスからのみアクセス可能
alb_allowed_ips = [
"203.0.113.10/32",
"203.0.113.20/32",
"203.0.113.30/32"
]
# RDS Configuration
rds_instance_count = 1
rds_instance_class = "db.t3.small"
production.tfvarsの例:
environment = "production"
project_name = "wish"
# ALB Access Control (production is open)
alb_allowed_ips = []
# RDS Configuration
rds_instance_count = 2
rds_instance_class = "db.r5.large"
環境別サブドメインマッピング
環境ごとに異なるサブドメインを使用する場合、map型変数とlookup関数を組み合わせるとお手軽です。
variable "environment_subdomain" {
description = "Subdomain prefix for each environment"
type = map(string)
default = {
production = "app"
staging = "staging"
development = "dev"
}
}
locals {
subdomain = lookup(var.environment_subdomain, var.environment, "app")
full_domain = "${local.subdomain}.${var.base_domain}"
}
Workspaceの運用
環境差が吸収できるようになったので、作業時のworkspaceも分けておきます。workspaceとtfvarsファイルを組み合わせることで、環境の切り替えと設定の適用を安全に行えます。
# 開発環境
terraform workspace select development
terraform apply -var-file=environments/development.tfvars
# ステージング環境
terraform workspace select staging
terraform apply -var-file=environments/staging.tfvars
まとめと次回予告
環境差分の吸収はTerraform移行時の課題の一つでしたが、リソース名の条件分岐の書き方で詰まったものの、うまく落とし込むことができました。
次回は、ECS/Fargateの移行とタスク定義の管理について解説します。