Infrastructure as Code (IaC)
インフラの構成をコードとして管理し、バージョン管理・レビュー・自動適用を可能にする手法
Infrastructure as Code (IaC)
インフラの構成をコードとして管理し、バージョン管理・レビュー・自動適用を可能にする手法
この章で学ぶこと
- IaCの基本概念と宣言的/命令的アプローチの違いを理解する
- Terraform、CloudFormation、CDK、Pulumiの特徴と使い分けを習得する
- IaCのベストプラクティスとGitOps連携パターンを把握する
- IaCのテスト戦略とCI/CDパイプラインへの統合方法を理解する
- 実運用におけるモジュール設計とマルチ環境管理を実践できる
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
- CI/CD概念 の内容を理解していること
1. IaC とは何か
1.1 従来のインフラ管理 vs IaC
従来のインフラ管理:
管理者がGUIコンソールで手動設定
→ ドキュメントと実態が乖離
→ 再現性がない
→ 変更履歴が追えない
→ 複数人の作業で設定が衝突
→ 障害時の復旧に時間がかかる
IaC:
コードでインフラを定義
→ Git でバージョン管理
→ PR でレビュー
→ CI/CD で自動適用
→ 環境の完全な再現が可能
→ 障害時はコードから即座に復元
1.2 IaC の利点
+----------------------------------------------------------+
| IaC の価値 |
+----------------------------------------------------------+
| |
| 再現性 同じコードで同じ環境を何度でも構築 |
| ┌──────────────────────────────────────────┐ |
| │ code → dev環境 / staging環境 / prod環境 │ |
| └──────────────────────────────────────────┘ |
| |
| 追跡可能性 Git の履歴 = インフラの変更履歴 |
| ┌──────────────────────────────────────────┐ |
| │ commit log = "いつ誰が何を変更したか" │ |
| └──────────────────────────────────────────┘ |
| |
| レビュー可能性 PR でインフラ変更をレビュー |
| ┌──────────────────────────────────────────┐ |
| │ terraform plan の差分を PR コメントに表示 │ |
| └──────────────────────────────────────────┘ |
| |
| 一貫性 全環境が同じコードから生成 |
| ┌──────────────────────────────────────────┐ |
| │ 環境間の差分 = 変数の違いのみ │ |
| └──────────────────────────────────────────┘ |
| |
| 速度 新環境を数分で構築可能 |
| ┌──────────────────────────────────────────┐ |
| │ terraform apply → 5分で完全な環境が完成 │ |
| └──────────────────────────────────────────┘ |
| |
+----------------------------------------------------------+
1.3 IaC の適用範囲
IaCはクラウドインフラだけでなく、幅広い領域に適用される。
| 対象 | ツール例 | 管理対象 |
|---|---|---|
| クラウドインフラ | Terraform, CDK, Pulumi | VPC, EC2, RDS, S3 等 |
| コンテナオーケストレーション | Kubernetes マニフェスト, Helm | Pod, Service, Deployment 等 |
| 構成管理 | Ansible, Chef, Puppet | OS設定, パッケージ, ユーザー |
| ネットワーク | Terraform, Ansible | ファイアウォール, DNS, CDN |
| モニタリング | Terraform (Datadog/PagerDuty provider) | ダッシュボード, アラート |
| CI/CD | GitHub Actions YAML, GitLab CI | パイプライン, ワークフロー |
| アクセス制御 | Terraform (IAM), Vault | ポリシー, ロール, シークレット |
2. 宣言的 vs 命令的アプローチ
2.1 比較表
| 項目 | 宣言的 (Declarative) | 命令的 (Imperative) |
|---|---|---|
| 定義方法 | 「あるべき状態」を記述 | 「手順」を記述 |
| 冪等性 | 組み込み | 自分で担保 |
| 代表ツール | Terraform, CloudFormation | Ansible (一部), シェルスクリプト |
| 差分検知 | 自動 (plan) | 困難 |
| 学習コスト | DSL の習得が必要 | プログラミングスキルで対応可能 |
| 適用場面 | インフラ構築 | 構成管理、プロビジョニング |
| ドリフト検知 | 容易 | 困難 |
| 並行実行 | ツールが依存関係を解決 | 手動で順序を制御 |
2.2 宣言的の例(Terraform)
# 「こうあるべき」を記述 → Terraform が差分を検出して適用
resource "aws_s3_bucket" "data" {
bucket = "my-app-data-bucket"
versioning {
enabled = true
}
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
# ライフサイクルルール
resource "aws_s3_bucket_lifecycle_configuration" "data" {
bucket = aws_s3_bucket.data.id
rule {
id = "archive-old-objects"
status = "Enabled"
transition {
days = 90
storage_class = "GLACIER"
}
expiration {
days = 365
}
}
}2.3 命令的の例(シェルスクリプト)
#!/bin/bash
# 「手順」を記述 → 冪等性は自分で担保する必要がある
# バケットが存在しなければ作成
if ! aws s3api head-bucket --bucket my-app-data-bucket 2>/dev/null; then
aws s3api create-bucket --bucket my-app-data-bucket
fi
# バージョニングを有効化
aws s3api put-bucket-versioning \
--bucket my-app-data-bucket \
--versioning-configuration Status=Enabled
# 暗号化を設定
aws s3api put-bucket-encryption \
--bucket my-app-data-bucket \
--server-side-encryption-configuration '{
"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]
}'
# ライフサイクルルールを設定
aws s3api put-bucket-lifecycle-configuration \
--bucket my-app-data-bucket \
--lifecycle-configuration '{
"Rules": [{
"ID": "archive-old-objects",
"Status": "Enabled",
"Transitions": [{"Days": 90, "StorageClass": "GLACIER"}],
"Expiration": {"Days": 365}
}]
}'2.4 ハイブリッドアプローチ
実際のプロジェクトでは、宣言的と命令的を組み合わせることが多い。
典型的な組み合わせ:
1. Terraform (宣言的) + Ansible (命令的)
Terraform: VPC, EC2 インスタンスを構築
Ansible: EC2 内のOS設定、パッケージインストール
2. Terraform (宣言的) + User Data スクリプト (命令的)
Terraform: インフラ構築 + User Data で初期化スクリプト実行
3. CDK (宣言的/プログラマティック) + Custom Resource (命令的)
CDK: 標準リソース定義
Custom Resource: CDKが対応していないリソースをLambdaで管理
3. 主要 IaC ツール
3.1 Terraform
HashiCorp が開発した、マルチクラウド対応の IaC ツール。HCL (HashiCorp Configuration Language) で記述する。最も広く使われているIaCツールであり、1,000以上のプロバイダーが利用可能。
# Terraform の基本構成
terraform {
required_version = ">= 1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = "ap-northeast-1"
default_tags {
tags = {
ManagedBy = "terraform"
Project = "my-app"
Environment = var.environment
}
}
}
# VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project}-${var.environment}-vpc"
}
}
# サブネット
resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.project}-${var.environment}-private-${count.index}"
Type = "private"
}
}
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 100)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project}-${var.environment}-public-${count.index}"
Type = "public"
}
}
# ECS Fargate サービス
resource "aws_ecs_service" "app" {
name = "my-app"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = var.desired_count
launch_type = "FARGATE"
network_configuration {
subnets = aws_subnet.private[*].id
security_groups = [aws_security_group.app.id]
}
deployment_circuit_breaker {
enable = true
rollback = true
}
lifecycle {
ignore_changes = [desired_count] # オートスケーリングと競合防止
}
}Terraform ワークフロー:
terraform init → プロバイダー・モジュールのダウンロード
↓
terraform plan → 現在の状態と定義の差分を表示
↓
terraform apply → 変更を適用
↓
terraform destroy → リソースを削除 (開発環境のクリーンアップ)
状態管理:
+-----------+ +----------------+ +-----------+
| .tf ファイル | ←→ | terraform.tfstate | ←→ | 実インフラ |
| (あるべき姿) | | (現在の状態) | | (AWS等) |
+-----------+ +----------------+ +-----------+
OpenTofu (Terraformのオープンソースフォーク):
- HashiCorp のライセンス変更(BSL)を受けて2023年に誕生
- Terraform 1.5.x からフォーク
- Linux Foundation 管轄
- 既存の Terraform コードとほぼ互換
3.2 AWS CloudFormation
# CloudFormation テンプレート
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Complete application stack with VPC, ECS, and RDS'
Parameters:
Environment:
Type: String
AllowedValues: [dev, staging, prod]
InstanceClass:
Type: String
Default: db.t3.medium
AllowedValues: [db.t3.micro, db.t3.small, db.t3.medium, db.t3.large]
Conditions:
IsProduction: !Equals [!Ref Environment, prod]
Resources:
DataBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain # スタック削除時もバケットを保持
Properties:
BucketName: !Sub 'my-app-${Environment}-data'
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
LifecycleConfiguration:
Rules:
- Id: ArchiveOldObjects
Status: Enabled
Transitions:
- TransitionInDays: 90
StorageClass: GLACIER
Database:
Type: AWS::RDS::DBInstance
Properties:
Engine: postgres
EngineVersion: '16.1'
DBInstanceClass: !If [IsProduction, db.t3.large, !Ref InstanceClass]
AllocatedStorage: !If [IsProduction, 100, 20]
MultiAZ: !If [IsProduction, true, false]
StorageEncrypted: true
DeletionProtection: !If [IsProduction, true, false]
BackupRetentionPeriod: !If [IsProduction, 30, 7]
Outputs:
BucketArn:
Value: !GetAtt DataBucket.Arn
Export:
Name: !Sub '${Environment}-data-bucket-arn'
DatabaseEndpoint:
Value: !GetAtt Database.Endpoint.Address
Export:
Name: !Sub '${Environment}-db-endpoint'3.3 AWS CDK
// AWS CDK (TypeScript) - プログラミング言語でインフラを定義
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns';
interface AppStackProps extends cdk.StackProps {
environment: string;
isProduction: boolean;
}
export class AppStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: AppStackProps) {
super(scope, id, props);
// VPC
const vpc = new ec2.Vpc(this, 'AppVpc', {
maxAzs: props.isProduction ? 3 : 2,
natGateways: props.isProduction ? 2 : 1,
subnetConfiguration: [
{
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
cidrMask: 24,
},
{
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
cidrMask: 24,
},
{
name: 'Isolated',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
cidrMask: 24,
},
],
});
// RDS
const database = new rds.DatabaseInstance(this, 'Database', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_16_1,
}),
instanceType: props.isProduction
? ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.LARGE)
: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
multiAz: props.isProduction,
deletionProtection: props.isProduction,
backupRetention: cdk.Duration.days(props.isProduction ? 30 : 7),
});
// ECS クラスター
const cluster = new ecs.Cluster(this, 'AppCluster', { vpc });
// Fargate サービス (L3 Construct: ALB + Fargate を一括定義)
const service = new ecs_patterns.ApplicationLoadBalancedFargateService(
this, 'AppService', {
cluster,
cpu: props.isProduction ? 1024 : 256,
memoryLimitMiB: props.isProduction ? 2048 : 512,
desiredCount: props.isProduction ? 3 : 1,
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry('my-app:latest'),
containerPort: 3000,
environment: {
NODE_ENV: props.environment,
DB_HOST: database.instanceEndpoint.hostname,
},
},
circuitBreaker: { rollback: true },
},
);
// オートスケーリング
if (props.isProduction) {
const scaling = service.service.autoScaleTaskCount({
minCapacity: 3,
maxCapacity: 10,
});
scaling.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
});
scaling.scaleOnMemoryUtilization('MemoryScaling', {
targetUtilizationPercent: 80,
});
}
// DB への接続許可
database.connections.allowFrom(
service.service,
ec2.Port.tcp(5432),
'Allow ECS to access RDS',
);
}
}
// アプリケーションのエントリポイント
const app = new cdk.App();
new AppStack(app, 'AppDev', {
environment: 'dev',
isProduction: false,
env: { account: '123456789012', region: 'ap-northeast-1' },
});
new AppStack(app, 'AppProd', {
environment: 'prod',
isProduction: true,
env: { account: '987654321098', region: 'ap-northeast-1' },
});3.4 Pulumi
// Pulumi (TypeScript) - 汎用プログラミング言語でマルチクラウド対応
import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';
import * as awsx from '@pulumi/awsx';
const config = new pulumi.Config();
const env = config.require('environment');
const isProduction = env === 'prod';
// VPC (Crosswalk: 高レベル抽象化)
const vpc = new awsx.ec2.Vpc('app-vpc', {
numberOfAvailabilityZones: isProduction ? 3 : 2,
natGateways: isProduction ? { strategy: awsx.ec2.NatGatewayStrategy.OnePerAz } : { strategy: awsx.ec2.NatGatewayStrategy.Single },
});
// S3 バケット
const bucket = new aws.s3.Bucket('data-bucket', {
bucket: `my-app-${env}-data`,
versioning: { enabled: true },
serverSideEncryptionConfiguration: {
rule: {
applyServerSideEncryptionByDefault: {
sseAlgorithm: 'AES256',
},
},
},
lifecycleRules: [{
enabled: true,
transitions: [{
days: 90,
storageClass: 'GLACIER',
}],
expiration: { days: 365 },
}],
});
// ECS クラスター + Fargate サービス
const cluster = new aws.ecs.Cluster('app-cluster');
const service = new awsx.ecs.FargateService('app-service', {
cluster: cluster.arn,
desiredCount: isProduction ? 3 : 1,
networkConfiguration: {
subnets: vpc.privateSubnetIds,
securityGroups: [],
},
taskDefinitionArgs: {
container: {
name: 'app',
image: 'my-app:latest',
cpu: isProduction ? 1024 : 256,
memory: isProduction ? 2048 : 512,
portMappings: [{ containerPort: 3000 }],
environment: [
{ name: 'NODE_ENV', value: env },
],
},
},
});
// 出力
export const bucketArn = bucket.arn;
export const bucketName = bucket.bucket;
export const vpcId = vpc.vpcId;
export const serviceUrl = pulumi.interpolate`http://${service.service.name}`;3.5 Crossplane
Kubernetes CRD としてクラウドリソースを管理するIaCツール。
# Crossplane: Kubernetes マニフェストでAWSリソースを管理
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
name: my-app-data
spec:
forProvider:
region: ap-northeast-1
providerConfigRef:
name: aws-provider
---
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
metadata:
name: my-app-db
spec:
forProvider:
region: ap-northeast-1
engine: postgres
engineVersion: "16.1"
instanceClass: db.t3.medium
allocatedStorage: 20
storageEncrypted: true
providerConfigRef:
name: aws-provider4. IaC ツール比較
| 項目 | Terraform | CloudFormation | CDK | Pulumi | Crossplane |
|---|---|---|---|---|---|
| 言語 | HCL | YAML/JSON | TypeScript等 | TypeScript/Python/Go等 | YAML (K8s CRD) |
| マルチクラウド | はい | AWS のみ | AWS のみ | はい | はい |
| 状態管理 | tfstate (S3等) | AWS管理 | CloudFormation経由 | Pulumi Cloud / S3 | Kubernetes etcd |
| 学習コスト | 中 | 中 | 低(開発者向き) | 低(開発者向き) | 中(K8s知識必要) |
| エコシステム | 最大 | AWS限定 | AWS限定 | 成長中 | 成長中 |
| ドリフト検知 | plan で検知 | drift detection | CloudFormation経由 | preview で検知 | 継続的リコンサイル |
| 推奨場面 | マルチクラウド | AWS専用 | AWS + TypeScript | マルチクラウド + 開発者 | K8s中心の組織 |
| ライセンス | BSL 1.1 | AWS サービス | Apache 2.0 | Apache 2.0 | Apache 2.0 |
| OSS代替 | OpenTofu | - | - | - | - |
4.1 ツール選定ディシジョンツリー
IaC ツール選定:
Kubernetes 中心の組織?
├── Yes → Crossplane を検討
│ ├── クラウドリソースもK8sで管理したい → Crossplane
│ └── K8sマニフェストのみ管理 → Kustomize / Helm
└── No → マルチクラウド要件がある?
├── Yes → Terraform or Pulumi
│ ├── DSLが好み / 大きなコミュニティ → Terraform
│ └── プログラミング言語で書きたい → Pulumi
└── No → AWS のみ?
├── Yes → CDK or CloudFormation
│ ├── TypeScript/Python チーム → CDK
│ └── YAML シンプルに → CloudFormation
└── No → 他クラウド → Terraform / Pulumi
5. IaC のベストプラクティス
5.1 モジュール化
# modules/ecs-service/main.tf - 再利用可能なモジュール
variable "service_name" {
type = string
description = "サービス名"
}
variable "image" {
type = string
description = "コンテナイメージ"
}
variable "cpu" {
type = number
default = 256
description = "CPU ユニット (256 = 0.25 vCPU)"
}
variable "memory" {
type = number
default = 512
description = "メモリ (MiB)"
}
variable "desired_count" {
type = number
default = 1
description = "希望するタスク数"
}
variable "environment_variables" {
type = map(string)
default = {}
description = "環境変数"
}
variable "cluster_id" {
type = string
description = "ECS クラスター ID"
}
variable "subnet_ids" {
type = list(string)
description = "サブネット ID リスト"
}
variable "security_group_ids" {
type = list(string)
description = "セキュリティグループ ID リスト"
}
# タスク定義
resource "aws_ecs_task_definition" "this" {
family = var.service_name
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = var.cpu
memory = var.memory
execution_role_arn = aws_iam_role.execution.arn
task_role_arn = aws_iam_role.task.arn
container_definitions = jsonencode([{
name = var.service_name
image = var.image
portMappings = [{
containerPort = 3000
protocol = "tcp"
}]
environment = [
for k, v in var.environment_variables : {
name = k
value = v
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.this.name
"awslogs-region" = data.aws_region.current.name
"awslogs-stream-prefix" = var.service_name
}
}
}])
}
# ECS サービス
resource "aws_ecs_service" "this" {
name = var.service_name
cluster = var.cluster_id
task_definition = aws_ecs_task_definition.this.arn
desired_count = var.desired_count
launch_type = "FARGATE"
network_configuration {
subnets = var.subnet_ids
security_groups = var.security_group_ids
}
deployment_circuit_breaker {
enable = true
rollback = true
}
}
# 出力
output "service_name" {
value = aws_ecs_service.this.name
}
output "task_definition_arn" {
value = aws_ecs_task_definition.this.arn
}# 利用側: environments/prod/main.tf
module "api_service" {
source = "../../modules/ecs-service"
service_name = "api"
image = "my-api:v1.2.3"
cpu = 512
memory = 1024
desired_count = 3
cluster_id = module.ecs_cluster.id
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.security.app_sg_id]
environment_variables = {
NODE_ENV = "production"
DATABASE_URL = module.database.connection_string
}
}
module "worker_service" {
source = "../../modules/ecs-service"
service_name = "worker"
image = "my-worker:v1.2.3"
cpu = 1024
memory = 2048
desired_count = 2
cluster_id = module.ecs_cluster.id
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.security.worker_sg_id]
environment_variables = {
NODE_ENV = "production"
QUEUE_URL = module.sqs.queue_url
}
}5.2 ディレクトリ構成
terraform/
├── modules/ # 再利用可能モジュール
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── ecs-service/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── iam.tf
│ ├── rds/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── monitoring/
│ ├── main.tf
│ ├── variables.tf
│ └── dashboards.tf
├── environments/ # 環境別設定
│ ├── dev/
│ │ ├── main.tf # モジュール呼び出し
│ │ ├── variables.tf # 変数定義
│ │ ├── terraform.tfvars # 環境固有の値
│ │ ├── backend.tf # 状態ファイルの保存先
│ │ └── providers.tf # プロバイダー設定
│ ├── staging/
│ │ ├── main.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ └── prod/
│ ├── main.tf
│ ├── terraform.tfvars
│ └── backend.tf
├── global/ # 環境共通リソース
│ ├── iam/
│ │ └── main.tf
│ ├── dns/
│ │ └── main.tf
│ └── ecr/
│ └── main.tf
└── scripts/ # ヘルパースクリプト
├── init.sh
└── plan.sh
5.3 変数管理のベストプラクティス
# variables.tf - 型・説明・バリデーション付き変数定義
variable "environment" {
type = string
description = "デプロイ環境 (dev, staging, prod)"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment は dev, staging, prod のいずれかを指定してください"
}
}
variable "instance_type" {
type = string
default = "t3.medium"
description = "EC2 インスタンスタイプ"
validation {
condition = can(regex("^t3\\.", var.instance_type))
error_message = "t3 ファミリーのインスタンスタイプを指定してください"
}
}
variable "alert_email" {
type = string
description = "アラート通知先メールアドレス"
sensitive = false
validation {
condition = can(regex("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", var.alert_email))
error_message = "有効なメールアドレスを指定してください"
}
}
variable "database_password" {
type = string
description = "データベースパスワード"
sensitive = true # plan/apply の出力でマスクされる
}# terraform.tfvars - 環境固有の値
# (Git にコミット可能、シークレットは含めない)
environment = "prod"
instance_type = "t3.large"
alert_email = "ops@example.com"
# シークレットは別ファイルまたは環境変数で管理
# export TF_VAR_database_password="xxx"
# または terraform.tfvars.secret (.gitignore に追加)6. IaC のテスト戦略
6.1 テストピラミッド
/\
/ \
/E2E \ terraform apply + 検証 + destroy
/------\ (terratest, kitchen-terraform)
/ 統合 \ terraform plan の検証
/テスト \ (tfplan JSON 解析)
/----------\
/ 静的解析 \ lint, validate, security scan
/ ユニット \ (tflint, checkov, tfsec, OPA)
/--------------\
6.2 静的解析
# CI での IaC 静的解析
name: Terraform Lint & Security
on:
pull_request:
paths: ['terraform/**']
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: '1.7'
# 構文チェック
- name: Terraform fmt
run: terraform fmt -check -recursive
working-directory: terraform/
- name: Terraform validate
run: |
cd terraform/environments/dev
terraform init -backend=false
terraform validate
# ベストプラクティスチェック
- name: TFLint
uses: terraform-linters/setup-tflint@v4
- run: |
tflint --init
tflint --recursive
working-directory: terraform/
# セキュリティスキャン
- name: Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: terraform/
framework: terraform
output_format: sarif
output_file_path: checkov-results.sarif
- name: tfsec
uses: aquasecurity/tfsec-action@v1.0.3
with:
working_directory: terraform/
# OPA (Open Policy Agent) によるカスタムポリシー
- name: OPA Policy Check
run: |
cd terraform/environments/dev
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
opa eval --data policies/ --input tfplan.json "data.terraform.deny[msg]"6.3 Plan テスト
# terraform plan を PR にコメント
name: Terraform Plan
on:
pull_request:
paths: ['terraform/**']
jobs:
plan:
runs-on: ubuntu-latest
permissions:
pull-requests: write
strategy:
matrix:
environment: [dev, staging, prod]
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Terraform Init
run: terraform init
working-directory: terraform/environments/${{ matrix.environment }}
- name: Terraform Plan
id: plan
run: terraform plan -no-color -out=tfplan
working-directory: terraform/environments/${{ matrix.environment }}
continue-on-error: true
- name: Comment PR with Plan
uses: actions/github-script@v7
with:
script: |
const plan = `${{ steps.plan.outputs.stdout }}`;
const truncated = plan.length > 60000
? plan.substring(0, 60000) + '\n... (truncated)'
: plan;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `### Terraform Plan - ${{ matrix.environment }}
\`\`\`
${truncated}
\`\`\`
`
});6.4 E2E テスト (Terratest)
// test/ecs_service_test.go
package test
import (
"testing"
"fmt"
"time"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/http-helper"
"github.com/stretchr/testify/assert"
)
func TestEcsService(t *testing.T) {
t.Parallel()
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../modules/ecs-service",
Vars: map[string]interface{}{
"service_name": fmt.Sprintf("test-%d", time.Now().Unix()),
"image": "nginx:latest",
"cpu": 256,
"memory": 512,
"desired_count": 1,
"environment": "test",
},
})
// テスト終了時にリソースを確実に削除
defer terraform.Destroy(t, terraformOptions)
// リソースを作成
terraform.InitAndApply(t, terraformOptions)
// 出力を検証
serviceName := terraform.Output(t, terraformOptions, "service_name")
assert.Contains(t, serviceName, "test-")
// ECS サービスが Running か確認
serviceArn := terraform.Output(t, terraformOptions, "service_arn")
assert.NotEmpty(t, serviceArn)
// ALB エンドポイントのヘルスチェック
albDns := terraform.Output(t, terraformOptions, "alb_dns_name")
url := fmt.Sprintf("http://%s/health", albDns)
http_helper.HttpGetWithRetry(t, url, nil, 200, "OK", 30, 10*time.Second)
}7. CI/CD との統合
7.1 Terraform CI/CD パイプライン
# 完全な Terraform CI/CD パイプライン
name: Terraform CI/CD
on:
push:
branches: [main]
paths: ['terraform/**']
pull_request:
paths: ['terraform/**']
permissions:
id-token: write # OIDC 認証用
contents: read
pull-requests: write
jobs:
# PR 時: lint + plan
lint:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform fmt -check -recursive
- run: |
cd terraform/environments/prod
terraform init -backend=false
terraform validate
plan:
if: github.event_name == 'pull_request'
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform-plan
aws-region: ap-northeast-1
- name: Terraform Plan
run: |
cd terraform/environments/prod
terraform init
terraform plan -no-color -out=tfplan 2>&1 | tee plan.txt
- name: Post Plan to PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const plan = fs.readFileSync('terraform/environments/prod/plan.txt', 'utf8');
const body = `### Terraform Plan
\`\`\`hcl
${plan.substring(0, 60000)}
\`\`\``;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body
});
# main マージ時: apply
apply:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production # 手動承認が必要
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform-apply
aws-region: ap-northeast-1
- name: Terraform Apply
run: |
cd terraform/environments/prod
terraform init
terraform apply -auto-approve7.2 Atlantis (Terraform PR 自動化)
# atlantis.yaml - リポジトリ設定
version: 3
automerge: false
delete_source_branch_on_merge: true
parallel_plan: true
parallel_apply: false
projects:
- name: prod
dir: terraform/environments/prod
workspace: default
autoplan:
when_modified: ["*.tf", "../modules/**/*.tf"]
enabled: true
apply_requirements: [approved, mergeable]
workflow: production
- name: dev
dir: terraform/environments/dev
workspace: default
autoplan:
when_modified: ["*.tf", "../modules/**/*.tf"]
enabled: true
apply_requirements: [mergeable]
workflow: default
workflows:
production:
plan:
steps:
- init
- run: tflint --recursive
- run: checkov -d .
- plan
apply:
steps:
- apply8. 状態管理
8.1 リモートバックエンドの設定
# S3 + DynamoDB バックエンド (推奨)
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "terraform-locks" # ロック用
encrypt = true
kms_key_id = "alias/terraform-state-key"
}
}# バックエンドインフラ自体の定義 (bootstrap)
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-terraform-state"
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"
kms_master_key_id = aws_kms_key.terraform_state.id
}
}
}
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}8.2 状態ファイルの分割
状態ファイル分割戦略:
1. 環境別分割 (最低限)
terraform/environments/dev/ → dev.tfstate
terraform/environments/prod/ → prod.tfstate
2. レイヤー別分割 (推奨)
terraform/layers/networking/ → networking.tfstate
terraform/layers/database/ → database.tfstate
terraform/layers/application/ → application.tfstate
3. サービス別分割 (マイクロサービス)
terraform/services/user/ → user-service.tfstate
terraform/services/order/ → order-service.tfstate
利点:
- blast radius (影響範囲) の最小化
- plan/apply の高速化
- チーム間の並行作業が可能
- ロック競合の削減
注意:
- 分割間のデータ共有は data source / remote state で行う
# レイヤー間のデータ共有
# application レイヤーから networking の出力を参照
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "prod/networking/terraform.tfstate"
region = "ap-northeast-1"
}
}
resource "aws_ecs_service" "app" {
# networking レイヤーの出力を使用
network_configuration {
subnets = data.terraform_remote_state.networking.outputs.private_subnet_ids
}
}9. アンチパターン
アンチパターン1: 状態ファイルのローカル管理
悪い例:
terraform.tfstate をローカルに保存し、
Git にコミットしてしまう。
問題:
- 機密情報(パスワード等)が Git に入る
- 複数人での並行作業で状態が衝突
- 状態ファイルの紛失 = インフラ管理不能
改善:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "terraform-locks" # ロック用
encrypt = true
}
}
# .gitignore に *.tfstate を追加
アンチパターン2: ハードコードされた値
# 悪い例: 値をハードコード
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890" # マジックナンバー
instance_type = "t3.large" # 環境で変わるべき
subnet_id = "subnet-12345" # 環境依存
}
# 改善: 変数化 + データソース
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = var.subnet_id
}アンチパターン3: 巨大な単一状態ファイル
悪い例:
1つの terraform.tfstate に全リソース(VPC, RDS, ECS, S3, IAM, ...)を管理
→ plan に5分以上かかる
→ 1つの変更が全リソースに影響するリスク
→ チーム間でロック競合が頻発
改善:
レイヤー別 or サービス別に状態ファイルを分割
→ 各 plan は30秒以内
→ blast radius が限定される
→ チームが並行作業可能
アンチパターン4: ドリフトの放置
問題:
手動でAWSコンソールから変更を行い、
コードと実態が乖離(ドリフト)する。
コード: instance_type = "t3.medium"
実インフラ: instance_type = "t3.large" (コンソールで変更)
→ terraform plan で "変更あり" と表示され続ける
→ plan の結果が信頼できなくなる
→ 次の terraform apply で意図しない変更が入る
改善:
1. 全変更を PR 経由で行うルールを徹底
2. AWS Config / CloudTrail でコンソール操作を検知
3. 定期的に terraform plan を実行してドリフトを検出
4. CI でドリフト検出を自動化
# ドリフト検出の自動化
name: Drift Detection
on:
schedule:
- cron: '0 9 * * *' # 毎日9時
jobs:
detect-drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/terraform-plan
aws-region: ap-northeast-1
- name: Check for drift
run: |
cd terraform/environments/prod
terraform init
terraform plan -detailed-exitcode -out=tfplan 2>&1 | tee plan.txt
EXIT_CODE=$?
if [ $EXIT_CODE -eq 2 ]; then
echo "DRIFT DETECTED!"
# Slack 通知
curl -X POST "$SLACK_WEBHOOK" \
-d "{\"text\":\"Terraform drift detected in production!\"}"
fi
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}実践演習
演習1: 基本的な実装
以下の要件を満たすコードを実装してください。
要件:
- 入力データの検証を行うこと
- エラーハンドリングを適切に実装すること
- テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
"""基本的な実装パターンの演習"""
def __init__(self):
self.data = []
def validate_input(self, value):
"""入力値の検証"""
if value is None:
raise ValueError("入力値がNoneです")
return True
def process(self, value):
"""データ処理のメインロジック"""
self.validate_input(value)
self.data.append(value)
return self.data
def get_results(self):
"""処理結果の取得"""
return {
'count': len(self.data),
'data': self.data
}
# テスト
def test_exercise1():
ex = Exercise1()
assert ex.process(1) == [1]
assert ex.process(2) == [1, 2]
assert ex.get_results()['count'] == 2
try:
ex.process(None)
assert False, "例外が発生するべき"
except ValueError:
pass
print("全テスト合格!")
test_exercise1()演習2: 応用パターン
基本実装を拡張して、以下の機能を追加してください。
# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
class AdvancedExercise:
"""応用パターンの演習"""
def __init__(self, max_size: int = 100):
self._items: List[Dict] = []
self._max_size = max_size
self._created_at = datetime.now()
def add(self, key: str, value: any) -> bool:
"""アイテムの追加(サイズ制限付き)"""
if len(self._items) >= self._max_size:
return False
self._items.append({
'key': key,
'value': value,
'timestamp': datetime.now().isoformat()
})
return True
def find(self, key: str) -> Optional[Dict]:
"""キーによる検索"""
for item in reversed(self._items):
if item['key'] == key:
return item
return None
def remove(self, key: str) -> bool:
"""キーによる削除"""
for i, item in enumerate(self._items):
if item['key'] == key:
self._items.pop(i)
return True
return False
def stats(self) -> Dict:
"""統計情報"""
return {
'total_items': len(self._items),
'max_size': self._max_size,
'usage_percent': len(self._items) / self._max_size * 100,
'uptime': str(datetime.now() - self._created_at)
}
# テスト
def test_advanced():
ex = AdvancedExercise(max_size=3)
assert ex.add("a", 1) == True
assert ex.add("b", 2) == True
assert ex.add("c", 3) == True
assert ex.add("d", 4) == False # サイズ制限
assert ex.find("b")['value'] == 2
assert ex.remove("b") == True
assert ex.find("b") is None
stats = ex.stats()
assert stats['total_items'] == 2
print("応用テスト全合格!")
test_advanced()演習3: パフォーマンス最適化
以下のコードのパフォーマンスを改善してください。
# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
"""非効率な検索"""
for i in range(len(data)):
for j in range(i + 1, len(data)):
if data[i] + data[j] == target:
return (i, j)
return (-1, -1)
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
"""ハッシュマップを使った効率的な検索"""
seen = {}
for i, num in enumerate(data):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return (-1, -1)
# ベンチマーク
def benchmark():
import random
data = list(range(5000))
random.shuffle(data)
target = data[100] + data[4000]
start = time.time()
result1 = slow_search(data, target)
slow_time = time.time() - start
start = time.time()
result2 = fast_search(data, target)
fast_time = time.time() - start
print(f"非効率版: {slow_time:.4f}秒")
print(f"効率版: {fast_time:.6f}秒")
print(f"高速化率: {slow_time/fast_time:.0f}倍")
benchmark()ポイント:
- アルゴリズムの計算量を意識する
- 適切なデータ構造を選択する
- ベンチマークで効果を測定する
10. FAQ
Q1: Terraform と Pulumi のどちらを選ぶべきか?
チームのスキルセットと要件による。インフラ専任チームがいる場合はTerraformのエコシステムの広さが有利。アプリケーション開発者がインフラも管理する場合は、Pulumi/CDKのプログラミング言語アプローチが学習コストを下げる。マルチクラウドが要件ならTerraformかPulumiを選ぶ。2023年のHashiCorpライセンス変更により、OSSライセンスを重視する場合はOpenTofuも選択肢に入る。
Q2: IaC のテストはどうするか?
Terraformの場合、terraform plan による差分確認が基本。加えて、terraform validate で構文チェック、tflint でベストプラクティスチェック、checkov / tfsec でセキュリティスキャンを行う。統合テストには terratest (Go) を使い、実際にリソースを作成・検証・破棄するE2Eテストが可能。OPA (Open Policy Agent) でカスタムポリシーを定義し、組織のガバナンスルールを自動チェックすることもできる。
Q3: 既存のインフラを IaC に移行するには?
Terraform の場合、terraform import コマンドで既存リソースを状態ファイルに取り込む。terraformer や former2 (CloudFormation) といった逆生成ツールも活用できる。段階的に移行し、新規リソースは必ずIaCで作成するルールを設けるのが現実的。AWS の場合、AWS Application Composer や IaC Generator も利用できる。
Q4: Terraform の状態ファイルが壊れた場合の対処法は?
まず terraform state list で現在の状態を確認する。S3 バックエンドの場合、バージョニングが有効なら過去の状態ファイルを復元できる。最悪の場合、terraform import で全リソースを再インポートする必要がある。状態ファイルのバックアップは必ずバージョニング付きS3で管理し、DynamoDBでロックをかけることが重要。
Q5: IaCで管理すべきでないリソースは?
一時的なリソース(デバッグ用EC2など)、データベースの中身(テーブル・レコード)、アプリケーション設定(Feature Flag等)はIaCの対象外とすることが多い。また、頻繁に変更されるリソース(Auto Scalingの desired_count 等)は lifecycle { ignore_changes } で除外するか、別の仕組みで管理する。
Q6: マルチアカウント環境でのIaCはどう設計するか?
AWS Organizations を使ったマルチアカウント環境では、(1) 管理アカウントでOrganization/OU/SCPを管理、(2) 共有サービスアカウントでRoute53/Transit Gatewayを管理、(3) 各環境アカウントでアプリケーションインフラを管理、という3層構成が一般的。Terraform の provider エイリアスや assume_role でクロスアカウント操作を行う。
FAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
まとめ
| 項目 | 要点 |
|---|---|
| IaC の本質 | インフラをコードで定義し、バージョン管理する |
| 宣言的 vs 命令的 | 宣言的(Terraform等)が主流、冪等性が組み込み |
| 主要ツール | Terraform(マルチクラウド)、CDK(AWS+TS)、Pulumi(マルチ+言語) |
| 状態管理 | リモートバックエンド必須(S3+DynamoDB等) |
| モジュール化 | 再利用可能なモジュールで DRY 原則を実現 |
| テスト | 静的解析 + plan テスト + E2E テスト (terratest) |
| CI/CD 統合 | PR で plan、マージで apply、OIDC 認証 |
| ドリフト検出 | 定期的な plan 実行で乖離を自動検知 |
| ベストプラクティス | 変数化、環境分離、テスト、最小権限 |
| 必須スキル | plan の読み方、モジュール設計、セキュリティ |
次に読むべきガイド
- GitOps -- IaCとGitを組み合わせた運用手法
- クラウドデプロイ -- IaCで構築したインフラへのデプロイ
- GitHub Actions基礎 -- IaCをCIで自動適用
- CI/CD概念 -- パイプライン設計の基礎
参考文献
- Kief Morris. Infrastructure as Code, 2nd Edition. O'Reilly Media, 2020.
- HashiCorp. "Terraform Documentation." https://developer.hashicorp.com/terraform/docs
- AWS. "AWS CDK Developer Guide." https://docs.aws.amazon.com/cdk/v2/guide/
- Pulumi. "Pulumi Documentation." https://www.pulumi.com/docs/
- Yevgeniy Brikman. Terraform: Up & Running, 3rd Edition. O'Reilly Media, 2022.
- OpenTofu. "OpenTofu Documentation." https://opentofu.org/docs/
- Gruntwork. "Terratest Documentation." https://terratest.gruntwork.io/
- Bridgecrew. "Checkov Documentation." https://www.checkov.io/