- Published on
IaCパターン&ベストプラクティス2025:Terraformモジュール、Pulumi、Crossplane、状態管理
- Authors

- Name
- Youngju Kim
- @fjvbn20031
目次
1. IaCランドスケープ2025:ツール比較(ひかく)と選択基準(せんたくきじゅん)
Infrastructure as Code(IaC)は、インフラをコードで定義(ていぎ)しバージョン管理(かんり)するDevOpsの核心的(かくしんてき)実践法(じっせんほう)です。2025年現在、IaCエコシステムは多様(たよう)なツールが共存(きょうぞん)し、それぞれ固有(こゆう)の強みを持っています。
1.1 主要IaCツール比較
| ツール | 言語 | 状態管理 | クラウド対応 | 特徴 |
|---|---|---|---|---|
| Terraform/OpenTofu | HCL | リモート状態ファイル | マルチクラウド | 最大のエコシステム、豊富なプロバイダー |
| Pulumi | TS/Python/Go/C# | Pulumi Cloud/自前バックエンド | マルチクラウド | 汎用プログラミング言語使用 |
| CDKTF | TS/Python/Go/C# | Terraformバックエンド | マルチクラウド | CDK構文+Terraformプロバイダー |
| AWS CDK | TS/Python/Go/C# | CloudFormation | AWS専用 | AWSネイティブ、L2/L3コンストラクト |
| Crossplane | YAML(K8s CRD) | K8s etcd | マルチクラウド | K8sネイティブ、GitOps親和的 |
| Ansible | YAML | ステートレス(手続き型) | マルチクラウド | 構成管理+プロビジョニング |
1.2 宣言的(せんげんてき)vs 命令的(めいれいてき)IaC
宣言的(Declarative)IaC:
┌─────────────────────────────────────────────────┐
│ 「EC2インスタンスが3台欲しい」 │
│ → ツールが現在の状態と比較し、必要な変更を計算 │
│ → Terraform, Pulumi, CloudFormation │
└─────────────────────────────────────────────────┘
命令的(Imperative)IaC:
┌─────────────────────────────────────────────────┐
│ 「EC2インスタンスを作成せよ、SGをアタッチせよ」 │
│ → 順番にコマンドを実行 │
│ → Ansible, Shell Scripts │
└─────────────────────────────────────────────────┘
1.3 OpenTofu vs Terraform
2023年のHashiCorpライセンス変更(へんこう)(BSL)後、OpenTofu がLinux Foundationプロジェクトとして誕生(たんじょう)しました。
# OpenTofu と Terraform は同一の HCL 構文を使用
# opentofu init / terraform init どちらも同様に動作
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
2. Terraformモジュール設計(せっけい)パターン
Terraformモジュールは再利用可能(さいりようかのう)なインフラコードの核心単位(たんい)です。正しいモジュール設計は、組織全体(そしきぜんたい)のインフラ一貫性(いっかんせい)と生産性(せいさんせい)を決定します。
2.1 フラットモジュール vs ネストモジュール
フラットモジュール構造(推奨):
modules/
├── vpc/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── versions.tf
├── ecs-cluster/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── versions.tf
└── rds/
├── main.tf
├── variables.tf
├── outputs.tf
└── versions.tf
ネストモジュール構造(複雑性増加):
modules/
└── platform/
├── main.tf # vpc, ecs, rds モジュールを呼び出し
├── modules/
│ ├── vpc/
│ ├── ecs-cluster/
│ └── rds/
└── outputs.tf
2.2 コンポジションパターン
モジュールコンポジションは、小さなモジュールを組み合わせてより大きなインフラを構成(こうせい)するパターンです。
# environments/prod/main.tf
# コンポジションパターン:小さなモジュールを組み合わせ
module "vpc" {
source = "../../modules/vpc"
name = "prod-vpc"
cidr_block = "10.0.0.0/16"
azs = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = false # 本番環境はAZごとにNAT
}
module "ecs_cluster" {
source = "../../modules/ecs-cluster"
name = "prod-cluster"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
capacity_providers = ["FARGATE", "FARGATE_SPOT"]
}
module "rds" {
source = "../../modules/rds"
name = "prod-db"
engine = "aurora-postgresql"
engine_version = "15.4"
instance_class = "db.r6g.xlarge"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.database_subnet_ids
security_group_ids = [module.ecs_cluster.security_group_id]
}
2.3 ファクトリーパターン
同じモジュールを複数回(ふくすうかい)インスタンス化するパターンです。
# サービスファクトリーパターン
variable "services" {
type = map(object({
cpu = number
memory = number
port = number
count = number
health_check_path = string
}))
default = {
"api-gateway" = {
cpu = 512
memory = 1024
port = 8080
count = 3
health_check_path = "/health"
}
"user-service" = {
cpu = 256
memory = 512
port = 8081
count = 2
health_check_path = "/actuator/health"
}
"order-service" = {
cpu = 512
memory = 1024
port = 8082
count = 2
health_check_path = "/health"
}
}
}
module "ecs_services" {
source = "../../modules/ecs-service"
for_each = var.services
name = each.key
cluster_id = module.ecs_cluster.cluster_id
cpu = each.value.cpu
memory = each.value.memory
container_port = each.value.port
desired_count = each.value.count
health_check_path = each.value.health_check_path
subnet_ids = module.vpc.private_subnet_ids
}
2.4 ラッパーモジュールパターン
コミュニティモジュールをラップして組織標準(ひょうじゅん)を強制(きょうせい)するパターンです。
# modules/org-s3-bucket/main.tf
# 組織標準を強制するラッパーモジュール
module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
version = "~> 4.0"
bucket = var.bucket_name
# 組織標準:常に暗号化
server_side_encryption_configuration = {
rule = {
apply_server_side_encryption_by_default = {
sse_algorithm = "aws:kms"
kms_master_key_id = var.kms_key_id
}
}
}
# 組織標準:パブリックアクセスブロック
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
# 組織標準:バージョニング有効化
versioning = {
enabled = true
}
# 組織標準:タグ
tags = merge(var.tags, {
ManagedBy = "terraform"
Team = var.team
Environment = var.environment
CostCenter = var.cost_center
})
}
3. Terraformベストプラクティス
3.1 リモート状態管理
# 状態管理インフラのブートストラップ
# bootstrap/main.tf
resource "aws_s3_bucket" "terraform_state" {
bucket = "myorg-terraform-state-${data.aws_caller_identity.current.account_id}"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
tags = {
Name = "Terraform State Lock Table"
ManagedBy = "terraform"
}
}
3.2 ワークスペース vs ディレクトリ戦略(せんりゃく)
ディレクトリ戦略(推奨):
infrastructure/
├── modules/ # 共有モジュール
│ ├── vpc/
│ ├── ecs/
│ └── rds/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf # dev専用の状態ファイル
│ ├── staging/
│ └── prod/
└── global/ # 共有リソース(IAM, Route53)
ワークスペース戦略(注意が必要):
- 同じコード、異なる状態 → 環境間の差異が大きくなると管理困難
- terraform workspace select dev / prod
- 状態ファイルが同じバックエンドに保存 → 権限分離が困難
3.3 プロバイダーバージョン管理
# versions.tf
terraform {
required_version = ">= 1.6.0, < 2.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.30" # 5.30.x を許可、6.0 は不可
}
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.24, < 3.0"
}
helm = {
source = "hashicorp/helm"
version = "~> 2.12"
}
}
}
# .terraform.lock.hcl は必ず Git にコミット
# terraform init -upgrade でプロバイダーを更新
3.4 変数(へんすう)バリデーションと型安全性(かたあんぜんせい)
variable "environment" {
type = string
description = "デプロイ環境"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environmentはdev、staging、prodのいずれかである必要があります。"
}
}
variable "instance_type" {
type = string
description = "EC2インスタンスタイプ"
validation {
condition = can(regex("^(t3|m6i|c6i|r6i)\\.", var.instance_type))
error_message = "許可されたインスタンスファミリー:t3, m6i, c6i, r6i"
}
}
variable "cidr_block" {
type = string
description = "VPC CIDRブロック"
validation {
condition = can(cidrhost(var.cidr_block, 0))
error_message = "有効なCIDRブロックを入力してください。"
}
}
# 複合型変数
variable "scaling_config" {
type = object({
min_size = number
max_size = number
desired_size = number
})
validation {
condition = var.scaling_config.min_size <= var.scaling_config.desired_size
error_message = "min_sizeはdesired_size以下である必要があります。"
}
validation {
condition = var.scaling_config.desired_size <= var.scaling_config.max_size
error_message = "desired_sizeはmax_size以下である必要があります。"
}
}
4. Pulumiディープダイブ:プログラミング言語によるIaC
Pulumiは、TypeScript、Python、Go、C#などの汎用(はんよう)プログラミング言語でインフラを定義します。条件分岐(じょうけんぶんき)、ループ、抽象化(ちゅうしょうか)など、言語のすべての機能(きのう)を活用(かつよう)できます。
4.1 Pulumi TypeScript 基本構造
// index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
const environment = config.require("environment");
const vpcCidr = config.get("vpcCidr") || "10.0.0.0/16";
// VPC 作成
const vpc = new aws.ec2.Vpc("main-vpc", {
cidrBlock: vpcCidr,
enableDnsHostnames: true,
enableDnsSupport: true,
tags: {
Name: `${environment}-vpc`,
Environment: environment,
ManagedBy: "pulumi",
},
});
// パブリックサブネット(プログラミング言語のループを活用)
const azs = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"];
const publicSubnets = azs.map((az, index) => {
return new aws.ec2.Subnet(`public-subnet-${index}`, {
vpcId: vpc.id,
cidrBlock: `10.0.${index + 1}.0/24`,
availabilityZone: az,
mapPublicIpOnLaunch: true,
tags: {
Name: `${environment}-public-${az}`,
Type: "public",
},
});
});
// 出力値
export const vpcId = vpc.id;
export const publicSubnetIds = publicSubnets.map(s => s.id);
4.2 Pulumiコンポーネントリソース
// components/ecs-service.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
interface EcsServiceArgs {
clusterArn: pulumi.Input<string>;
vpcId: pulumi.Input<string>;
subnetIds: pulumi.Input<string>[];
containerImage: string;
cpu: number;
memory: number;
port: number;
desiredCount: number;
healthCheckPath: string;
}
export class EcsService extends pulumi.ComponentResource {
public readonly serviceUrl: pulumi.Output<string>;
public readonly taskDefinitionArn: pulumi.Output<string>;
constructor(
name: string,
args: EcsServiceArgs,
opts?: pulumi.ComponentResourceOptions
) {
super("custom:app:EcsService", name, {}, opts);
const logGroup = new aws.cloudwatch.LogGroup(`${name}-logs`, {
retentionInDays: 30,
tags: { Service: name },
}, { parent: this });
const taskRole = new aws.iam.Role(`${name}-task-role`, {
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [{
Action: "sts:AssumeRole",
Effect: "Allow",
Principal: { Service: "ecs-tasks.amazonaws.com" },
}],
}),
}, { parent: this });
const taskDefinition = new aws.ecs.TaskDefinition(`${name}-task`, {
family: name,
cpu: args.cpu.toString(),
memory: args.memory.toString(),
networkMode: "awsvpc",
requiresCompatibilities: ["FARGATE"],
executionRoleArn: taskRole.arn,
taskRoleArn: taskRole.arn,
containerDefinitions: JSON.stringify([{
name: name,
image: args.containerImage,
cpu: args.cpu,
memory: args.memory,
portMappings: [{ containerPort: args.port }],
logConfiguration: {
logDriver: "awslogs",
options: {
"awslogs-group": logGroup.name,
"awslogs-region": "ap-northeast-2",
"awslogs-stream-prefix": "ecs",
},
},
}]),
}, { parent: this });
this.taskDefinitionArn = taskDefinition.arn;
this.registerOutputs({ taskDefinitionArn: this.taskDefinitionArn });
}
}
4.3 Pulumiスタックリファレンス
// スタック間のデータ共有
// infrastructure/index.ts で VPC 情報を export
export const vpcId = vpc.id;
export const privateSubnetIds = privateSubnets.map(s => s.id);
// application/index.ts で参照
const infraStack = new pulumi.StackReference("org/infrastructure/prod");
const vpcId = infraStack.getOutput("vpcId");
const subnetIds = infraStack.getOutput("privateSubnetIds");
4.4 Pulumi Policy as Code(CrossGuard)
// policy-pack/index.ts
import { PolicyPack, validateResourceOfType } from "@pulumi/policy";
import * as aws from "@pulumi/aws";
new PolicyPack("aws-security", {
policies: [
{
name: "s3-no-public-read",
description: "S3バケットはパブリック読み取りを許可してはなりません",
enforcementLevel: "mandatory",
validateResource: validateResourceOfType(aws.s3.BucketAclV2, (acl, args, reportViolation) => {
if (acl.acl === "public-read" || acl.acl === "public-read-write") {
reportViolation("S3バケットにパブリックACLが設定されています。");
}
}),
},
{
name: "ec2-require-tags",
description: "EC2インスタンスは必須タグを持つ必要があります",
enforcementLevel: "mandatory",
validateResource: validateResourceOfType(aws.ec2.Instance, (instance, args, reportViolation) => {
const requiredTags = ["Name", "Environment", "Team", "CostCenter"];
const tags = instance.tags || {};
for (const tag of requiredTags) {
if (!(tag in tags)) {
reportViolation(`必須タグ '${tag}' が不足しています。`);
}
}
}),
},
],
});
5. Crossplane:KubernetesネイティブIaC
Crossplaneは、Kubernetes CRD(Custom Resource Definition)を使用してクラウドリソースを管理(かんり)します。kubectlでAWS、GCP、Azureリソースを作成(さくせい)・管理できます。
5.1 Crossplaneアーキテクチャ
Crossplane アーキテクチャ:
┌─────────────────────────────────────────────────┐
│ K8s Cluster │
│ ┌───────────────────────────────────────────┐ │
│ │ Crossplane Core │ │
│ │ ┌─────────┐ ┌──────────┐ ┌─────────┐ │ │
│ │ │Composite│ │Composition│ │ XRD │ │ │
│ │ │Resource │ │ │ │ │ │ │
│ │ └────┬────┘ └────┬─────┘ └────┬────┘ │ │
│ │ └─────────────┼─────────────┘ │ │
│ └───────────────────┬─┤─────────────────────┘ │
│ │ │ │
│ ┌───────────────────┴─┴─────────────────────┐ │
│ │ Providers │ │
│ │ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ │
│ │ │AWS Prov. │ │GCP Prov. │ │Azure P. │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬────┘ │ │
│ └───────┼──────────────┼─────────────┼──────┘ │
└──────────┼──────────────┼─────────────┼──────────┘
▼ ▼ ▼
AWS Cloud GCP Cloud Azure Cloud
5.2 XRD(Composite Resource Definition)定義
# xrd.yaml - プラットフォームチームが定義するAPIスキーマ
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xdatabases.platform.example.com
spec:
group: platform.example.com
names:
kind: XDatabase
plural: xdatabases
claimNames:
kind: Database
plural: databases
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
engine:
type: string
enum: ["postgresql", "mysql"]
description: "データベースエンジン"
size:
type: string
enum: ["small", "medium", "large"]
description: "インスタンスサイズ"
region:
type: string
default: "ap-northeast-2"
required:
- engine
- size
5.3 Composition定義
# composition.yaml - 実際のリソースマッピング
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xdatabases.aws.platform.example.com
labels:
provider: aws
spec:
compositeTypeRef:
apiVersion: platform.example.com/v1alpha1
kind: XDatabase
resources:
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
forProvider:
engine: postgresql
engineVersion: "15"
instanceClass: db.t3.medium
allocatedStorage: 20
publiclyAccessible: false
skipFinalSnapshot: true
patches:
- type: FromCompositeFieldPath
fromFieldPath: "spec.engine"
toFieldPath: "spec.forProvider.engine"
- type: FromCompositeFieldPath
fromFieldPath: "spec.size"
toFieldPath: "spec.forProvider.instanceClass"
transforms:
- type: map
map:
small: db.t3.medium
medium: db.r6g.large
large: db.r6g.xlarge
5.4 Claimベースプロビジョニング
# claim.yaml - 開発者がリクエストするリソース
apiVersion: platform.example.com/v1alpha1
kind: Database
metadata:
name: orders-db
namespace: orders-team
spec:
engine: postgresql
size: medium
region: ap-northeast-2
# 開発者の使用体験
kubectl apply -f claim.yaml
kubectl get databases -n orders-team
# NAME ENGINE SIZE READY AGE
# orders-db postgresql medium True 5m
6. 状態管理(じょうたいかんり)の深掘(ふかぼ)り
6.1 状態バックエンド比較
S3 + DynamoDB(AWS):
┌─────────────┐ ┌──────────────┐
│ S3 Bucket │ │ DynamoDB │
│(状態保存) │ │(ロック管理) │
│ バージョン有 │ │ LockIDハッシュ│
│ 暗号化有効 │ │ キーテーブル │
└─────────────┘ └──────────────┘
GCS(GCP):
┌─────────────────────────────┐
│ GCS Bucket │
│(状態保存+組み込みロック) │
│ バージョン有効、暗号化有効 │
└─────────────────────────────┘
Terraform Cloud / Spacelift:
┌─────────────────────────────┐
│ マネージド状態保存 │
│ 組み込みロック、状態履歴 │
│ アクセス制御、監査ログ │
└─────────────────────────────┘
6.2 状態ロックと同時実行制御(どうじじっこうせいぎょ)
# 状態ロックの強制解除(注意:他の操作が実行中でないことを確認)
terraform force-unlock LOCK_ID
# 状態ファイルの照会
terraform state list
terraform state show aws_instance.web
# 状態からリソースを除去(削除なし)
terraform state rm aws_instance.legacy
# 既存リソースを状態に取り込み
terraform import aws_instance.web i-1234567890abcdef0
6.3 状態マイグレーション
# moved ブロックでリソース移動(Terraform 1.1+)
moved {
from = aws_instance.web
to = module.compute.aws_instance.web
}
moved {
from = aws_security_group.web_sg
to = module.networking.aws_security_group.web_sg
}
# import ブロックで既存リソースの取り込み(Terraform 1.5+)
import {
to = aws_instance.legacy_server
id = "i-0abc123def456789"
}
import {
to = aws_s3_bucket.existing_bucket
id = "my-existing-bucket-name"
}
7. IaCテスト戦略
7.1 Terratest
// test/vpc_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestVpcModule(t *testing.T) {
t.Parallel()
terraformOptions := &terraform.Options{
TerraformDir: "../modules/vpc",
Vars: map[string]interface{}{
"name": "test-vpc",
"cidr_block": "10.0.0.0/16",
"azs": []string{"us-east-1a", "us-east-1b"},
},
NoColor: true,
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcId)
privateSubnetIds := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
assert.Equal(t, 2, len(privateSubnetIds))
}
7.2 Checkov静的解析(せいてきかいせき)
# Checkov 実行
checkov -d . --framework terraform
# 特定チェックのスキップ
checkov -d . --skip-check CKV_AWS_18,CKV_AWS_21
# JSONレポート生成
checkov -d . -o json > checkov-report.json
# custom_policy.yaml
metadata:
id: "CUSTOM_001"
name: "Ensure S3 bucket has lifecycle policy"
category: "general"
definition:
cond_type: "attribute"
resource_types:
- "aws_s3_bucket"
attribute: "lifecycle_rule"
operator: "exists"
7.3 OPA Conftest
# policy/terraform.rego
package terraform
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_instance"
not resource.change.after.tags.Environment
msg := sprintf("EC2インスタンス '%s' にEnvironmentタグがありません", [resource.address])
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
resource.change.after.acl == "public-read"
msg := sprintf("S3バケット '%s' にパブリック読み取りACLが設定されています", [resource.address])
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_security_group_rule"
resource.change.after.cidr_blocks[_] == "0.0.0.0/0"
resource.change.after.from_port == 22
msg := "SSH(22)ポートを0.0.0.0/0に開放してはなりません"
}
# Conftest 実行
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
conftest test tfplan.json -p policy/
8. モノレポ vs ポリレポ戦略
8.1 モノレポ構造
infrastructure/ # モノレポ
├── .github/
│ └── workflows/
│ ├── terraform-plan.yml # PRでplan
│ └── terraform-apply.yml # マージ後apply
├── modules/ # 共有モジュール
│ ├── vpc/
│ ├── ecs/
│ ├── rds/
│ └── monitoring/
├── environments/
│ ├── shared/ # 共有リソース(IAM, DNS)
│ ├── dev/
│ ├── staging/
│ └── prod/
├── terragrunt.hcl # ルート設定
└── Makefile
8.2 TerragruntでDRYを実現(じつげん)
# environments/prod/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../modules/vpc"
}
inputs = {
environment = "prod"
cidr_block = "10.0.0.0/16"
azs = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
enable_nat_gateway = true
single_nat_gateway = false
}
# ルート terragrunt.hcl
remote_state {
backend = "s3"
config = {
bucket = "myorg-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
9. GitOps for IaC
9.1 Atlantis
# atlantis.yaml
version: 3
automerge: false
parallel_plan: true
parallel_apply: true
projects:
- name: prod-vpc
dir: environments/prod
workspace: default
terraform_version: v1.7.0
autoplan:
when_modified:
- "*.tf"
- "../../modules/vpc/**"
enabled: true
apply_requirements:
- approved
- mergeable
- undiverged
9.2 Spacelift設定
# .spacelift/config.yml
version: "1"
stacks:
prod-infra:
space: production
project_root: environments/prod
terraform_version: "1.7.0"
autodeploy: false
labels:
- "env:prod"
- "team:platform"
drift_detection:
enabled: true
schedule:
- "0 */6 * * *" # 6時間ごとにドリフト検知
reconcile: false # 自動修正しない
10. ドリフト検知(けんち)と修復(しゅうふく)
10.1 ドリフト検知戦略
# Terraform組み込みドリフト検知
terraform plan -detailed-exitcode
# 終了コード:
# 0 - 変更なし
# 1 - エラー
# 2 - 変更あり(ドリフト検知)
# 自動ドリフト検知スクリプト
#!/bin/bash
set -e
ENVIRONMENTS=("dev" "staging" "prod")
for env in "${ENVIRONMENTS[@]}"; do
echo "=== Checking drift for $env ==="
cd "environments/$env"
terraform init -no-color > /dev/null
if ! terraform plan -detailed-exitcode -no-color > "/tmp/drift-${env}.txt" 2>&1; then
EXIT_CODE=$?
if [ $EXIT_CODE -eq 2 ]; then
echo "DRIFT DETECTED in $env"
curl -X POST "$SLACK_WEBHOOK" \
-H 'Content-Type: application/json' \
-d "{\"text\": \"Drift detected in ${env} environment\"}"
fi
fi
cd ../..
done
11. IaCにおけるシークレット管理
11.1 HashiCorp Vault連携(れんけい)
# Vault プロバイダー
provider "vault" {
address = "https://vault.example.com"
}
# Vault からシークレットを読み取り
data "vault_generic_secret" "db_credentials" {
path = "secret/data/prod/database"
}
resource "aws_db_instance" "main" {
engine = "postgres"
engine_version = "15"
instance_class = "db.r6g.large"
username = data.vault_generic_secret.db_credentials.data["username"]
password = data.vault_generic_secret.db_credentials.data["password"]
}
11.2 SOPS(Secrets OPerationS)
# SOPS で暗号化
sops --encrypt --age age1xxxxx secrets.yaml > secrets.enc.yaml
# Terraform で SOPS を使用
data "sops_file" "secrets" {
source_file = "secrets.enc.yaml"
}
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db.id
secret_string = data.sops_file.secrets.data["db_password"]
}
12. クイズ
この記事で取り上げたIaCパターンについて理解(りかい)を確認(かくにん)しましょう。
Q1. Terraformモジュール設計
質問:Terraformラッパーモジュールパターンの主な目的は何ですか?
正解:コミュニティモジュールをラップして、組織標準(暗号化、タグ付け、アクセス制御)を強制することです。
ラッパーモジュールは内部的にコミュニティモジュールを呼び出しながら、組織で必須とされる設定(S3暗号化、パブリックアクセスブロック、タグポリシー)をデフォルト値として適用します。開発者はラッパーモジュールを使用するだけで、自動的にセキュリティおよびガバナンスポリシーに準拠できます。
Q2. Pulumi vs Terraform
質問:PulumiがTerraformに対して持つ最大の利点は何ですか?
正解:TypeScript、Python、Goなどの汎用プログラミング言語を使用するため、条件分岐、ループ、抽象化など言語のすべての機能をインフラコードに活用できます。
HCLは宣言的DSLであり、複雑なロジックの実装に制約があります。Pulumiは既存のIDE、デバッガー、テストフレームワーク、パッケージマネージャーをそのまま使用でき、開発者の生産性が向上します。
Q3. Crossplaneアーキテクチャ
質問:CrossplaneにおけるXRD、Composition、Claimの役割をそれぞれ説明してください。
正解:
- XRD(Composite Resource Definition):プラットフォームチームが定義するAPIスキーマ。開発者に公開するフィールドと型を定義します。
- Composition:XRDに対する実際のリソースマッピング。1つのXRDに複数のComposition(AWS用、GCP用など)を紐付けできます。
- Claim:開発者がNamespace単位でリクエストするリソース。XRDスキーマに沿ったシンプルなYAMLを書くと、Compositionが実際のクラウドリソースを作成します。
Q4. 状態管理
質問:Terraform状態ファイルにおけるmovedブロックとimportブロックの違いは何ですか?
正解:
movedブロック:すでに状態にあるリソースのアドレスを変更します。モジュールリファクタリング時にリソースを削除/再作成せずに移動できます。importブロック:状態ファイルにないが、実際のクラウドに存在するリソースをTerraform管理下に取り込みます。Terraform 1.5からコード内で宣言的にimportできるようになりました。
Q5. GitOps for IaC
質問:AtlantisとSpaceliftのドリフト検知方式の違いを説明してください。
正解:
- Atlantis:PRベースのワークフローに焦点。ドリフト検知機能は組み込まれておらず、別途cronスクリプトやCIパイプラインで
terraform plan -detailed-exitcodeを実行する必要があります。 - Spacelift:組み込みドリフト検知機能を提供。スケジュール(例:6時間ごと)に従って自動的にplanを実行し、ドリフトが検出されたら通知を送信するか、自動修復(reconcile)オプションを提供します。ポリシーでドリフト対応をコード化できます。
13. 参考資料(さんこうしりょう)
- HashiCorp Terraform Documentation - https://developer.hashicorp.com/terraform/docs
- Pulumi Documentation - https://www.pulumi.com/docs/
- Crossplane Documentation - https://docs.crossplane.io/
- OpenTofu Documentation - https://opentofu.org/docs/
- Terragrunt Documentation - https://terragrunt.gruntwork.io/docs/
- Terratest - https://terratest.gruntwork.io/
- Checkov by Bridgecrew - https://www.checkov.io/
- Infracost Documentation - https://www.infracost.io/docs/
- Atlantis Documentation - https://www.runatlantis.io/docs/
- Spacelift Documentation - https://docs.spacelift.io/
- SOPS (Secrets OPerationS) - https://github.com/getsops/sops
- OPA Conftest - https://www.conftest.dev/
- tfsec by Aqua Security - https://aquasecurity.github.io/tfsec/
この記事では、IaCの主要ツール(Terraform、Pulumi、Crossplane)と設計パターン(コンポジション、ファクトリー、ラッパー)、状態管理、テスト、GitOps、ドリフト検知まで包括的(ほうかつてき)に取り上げました。組織の規模と要件に合ったツールとパターンを選択し、テストとセキュリティスキャンをCI/CDに統合(とうごう)することが、成功するIaC運用の鍵(かぎ)です。