第10章:Infrastructure as Code (IaC) と自動化
10.1 IaCの概念とメリット
Infrastructure as Codeという思想の革新性
Infrastructure as Code(IaC)は、インフラストラクチャ管理における最も重要なパラダイムシフトの一つです。手動でサーバーを設定し、GUIでクリックを繰り返していた時代から、プログラマブルで再現可能な方法への移行は、単なる効率化を超えて、インフラストラクチャの品質、信頼性、そして開発速度を根本的に向上させました。
本章は「概念 50% / 実装 50%」程度のバランスを想定しており、まずは宣言的/命令的アプローチや冪等性といった考え方を押さえ、そのうえでTerraformやPython/Boto3のコード例を必要に応じて参照する読み方を推奨します。コードは主にイメージ共有のためのサンプルであり、実際に適用する際は検証環境で試しつつ、自組織の運用ルールやセキュリティポリシーに合わせて調整してください。
宣言的アプローチと命令的アプローチの本質
IaCツールを理解する上で最も重要な概念は、宣言的(Declarative)アプローチと命令的(Imperative)アプローチの違いです。
宣言的アプローチ:望ましい状態の記述
宣言的アプローチでは、「どのような状態であるべきか」を記述します。
# Terraform - 宣言的アプローチの例
resource "aws_instance" "web_servers" {
count = 3 # 3台のインスタンスが存在すべき
instance_type = "t3.medium"
ami = data.aws_ami.amazon_linux_2.id
subnet_id = aws_subnet.public[count.index % length(aws_subnet.public)].id
vpc_security_group_ids = [aws_security_group.web.id]
tags = {
Name = "web-server-${count.index + 1}"
Environment = var.environment
ManagedBy = "Terraform"
}
# 現在2台しかない場合、Terraformは自動的に1台追加
# 現在4台ある場合、Terraformは自動的に1台削除
# 設定が異なる場合、Terraformは差分を適用
}
命令的アプローチ:手順の記述
命令的アプローチでは、「何をすべきか」の手順を記述します。
# Python/Boto3 - 命令的アプローチの例
import boto3
ec2 = boto3.resource('ec2')
# 現在のインスタンス数を確認
current_instances = list(ec2.instances.filter(
Filters=[
{'Name': 'tag:Name', 'Values': ['web-server-*']},
{'Name': 'instance-state-name', 'Values': ['running']}
]
))
desired_count = 3
current_count = len(current_instances)
# 不足分を追加
if current_count < desired_count:
for i in range(current_count, desired_count):
instance = ec2.create_instances(
ImageId='ami-0123456789abcdef0',
InstanceType='t3.medium',
MinCount=1,
MaxCount=1,
SubnetId=get_next_subnet(),
SecurityGroupIds=['sg-1234567890abcdef0'],
TagSpecifications=[{
'ResourceType': 'instance',
'Tags': [
{'Key': 'Name', 'Value': f'web-server-{i+1}'},
{'Key': 'Environment', 'Value': environment},
{'Key': 'ManagedBy', 'Value': 'Python Script'}
]
}]
)[0]
print(f"Created instance: {instance.id}")
# 過剰分を削除
elif current_count > desired_count:
for instance in current_instances[desired_count:]:
instance.terminate()
print(f"Terminated instance: {instance.id}")
IaCがもたらす本質的価値
1. 冪等性(Idempotency)の保証
冪等性とは、同じ操作を何度実行しても同じ結果が得られる性質です。宣言的IaCツールは、この冪等性を自動的に保証します。
# 冪等性の実例
初期状態: インスタンス0台
1回目実行: 3台作成 → 結果: 3台
2回目実行: 変更なし → 結果: 3台(変わらず)
3回目実行: 変更なし → 結果: 3台(変わらず)
# 設定変更時
設定変更: instance_type を t3.large に変更
4回目実行: 3台を更新 → 結果: 3台(t3.large)
2. バージョン管理による変更追跡
インフラストラクチャをコードとして管理することで、ソフトウェア開発で培われたベストプラクティスを適用できます。
# Git でのインフラ変更管理
git log --oneline terraform/
# 出力例:
# a5f3c21 feat: Add auto-scaling for web servers
# 82b9e44 fix: Correct security group ingress rules
# 3d7a891 refactor: Extract RDS configuration to module
# f2c6b55 chore: Update instance types for cost optimization
# 特定の変更の詳細確認
git show a5f3c21
# 変更内容、理由、影響範囲が明確に記録される
3. コラボレーションの促進
# プルリクエストでのインフラ変更レビュー
レビュープロセス:
1. 変更案の作成:
- ブランチで変更を実装
- terraform plan の結果を確認
2. プルリクエスト:
- 変更の意図を説明
- 影響範囲を明記
- コスト影響を記載
3. レビュー:
- セキュリティチェック
- ベストプラクティス確認
- コスト最適化の検討
4. 承認と適用:
- 複数人による承認
- 自動テストの通過
- 本番環境への適用
IaCの成熟度モデル
組織のIaC採用レベルを評価し、段階的な改善を図るための指標です。以下の成熟度モデルは、自組織の現在地をざっくり把握し、どこから改善を始めるかを検討するための一つの物差しとして利用します。
本書の想定読者の多くは、少なくともレベル3(IaCツール導入)を短期的な目標とし、レベル4〜5は中長期で目指す到達点として参考にしていただくとよいでしょう。すべてのレベルを一度に実現する必要はなく、自組織の状況に合わせて段階的に取り組むことを前提としてください。
レベル1 - 手動運用:
特徴:
- GUI/CLIでの手動設定
- ドキュメント化されていない
- 再現性なし
リスク:
- ヒューマンエラー
- 環境間の不整合
- 障害復旧の遅延
レベル2 - スクリプト化:
特徴:
- シェルスクリプトでの部分自動化
- 基本的なバージョン管理
- 限定的な再現性
改善点:
- 手動作業の削減
- 基本的な標準化
レベル3 - IaCツール導入:
特徴:
- Terraform/CloudFormation使用
- コードレビュー実施
- 環境別管理
利点:
- 宣言的管理
- 状態管理
- ドリフト検出
レベル4 - 完全自動化:
特徴:
- CI/CDパイプライン統合
- 自動テスト実装
- Policy as Code
成果:
- 継続的デリバリー
- コンプライアンス自動化
- セルフサービス化
レベル5 - GitOps:
特徴:
- Git as Single Source of Truth
- Pull型デプロイメント
- 継続的な同期
最終形:
- 完全な監査証跡
- 自動ロールバック
- 宣言的運用
IaCのアンチパターンと対策
1. 手動変更との混在(Configuration Drift)
最も一般的で危険なアンチパターンは、IaCで管理されているリソースを手動で変更することです。
# ドリフト検出と防止策
# 1. 定期的なドリフト検出
resource "null_resource" "drift_check" {
provisioner "local-exec" {
command = <<-EOT
terraform plan -detailed-exitcode > /dev/null
if [ $? -eq 2 ]; then
echo "ALERT: Configuration drift detected!"
# Slackやメールでアラート送信
fi
EOT
}
triggers = {
# 1時間ごとに実行
time = timestamp()
}
}
# 2. AWS Config Rules による監視
resource "aws_config_config_rule" "terraform_managed" {
name = "terraform-managed-resources"
source {
owner = "AWS"
source_identifier = "REQUIRED_TAGS"
}
input_parameters = jsonencode({
tag1Key = "ManagedBy"
tag1Value = "Terraform"
})
# タグがない(手動作成された)リソースを検出
}
# 3. IAMポリシーによる手動変更の防止
data "aws_iam_policy_document" "prevent_manual_changes" {
statement {
effect = "Deny"
actions = [
"ec2:*",
"rds:*",
"s3:*"
]
resources = ["*"]
condition {
test = "StringNotEquals"
variable = "aws:userid"
values = [data.aws_caller_identity.terraform.user_id]
}
# Terraform実行ユーザー以外の変更を拒否
}
}
2. 状態ファイルの不適切な管理
Terraformの状態ファイルは、実際のインフラストラクチャとコードをマッピングする重要な情報です。
# リモートバックエンドの適切な設定
terraform {
backend "s3" {
# 状態ファイルの保存先
bucket = "terraform-state-bucket"
key = "prod/infrastructure/terraform.tfstate"
region = "ap-northeast-1"
# 暗号化
encrypt = true
kms_key_id = "arn:aws:kms:ap-northeast-1:123456789012:key/abcd1234"
# 状態ロック
dynamodb_table = "terraform-state-lock"
# バージョニング(履歴保持)
versioning = true
# アクセス制御
acl = "bucket-owner-full-control"
}
}
# DynamoDBテーブル(状態ロック用)
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
server_side_encryption {
enabled = true
}
tags = {
Name = "Terraform State Lock Table"
Environment = "shared"
}
}
3. モノリシックな構成
すべてのインフラストラクチャを単一の巨大な構成で管理すると、様々な問題が発生します。
# 適切なモジュール分割
# ディレクトリ構造
terraform/
├── modules/
│ ├── networking/
│ │ ├── variables.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── README.md
│ ├── compute/
│ │ └── ...
│ ├── database/
│ │ └── ...
│ └── security/
│ └── ...
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ │ └── ...
│ └── prod/
│ └── ...
└── global/
├── iam/
└── route53/
# モジュールの使用例
module "network" {
source = "../../modules/networking"
environment = var.environment
cidr_block = var.vpc_cidr
availability_zones = data.aws_availability_zones.available.names
enable_nat_gateway = var.environment == "prod" ? true : false
single_nat_gateway = var.environment != "prod" ? true : false
}
module "compute" {
source = "../../modules/compute"
environment = var.environment
subnet_ids = module.network.private_subnet_ids
instance_type = var.instance_types[var.environment]
instance_count = var.instance_counts[var.environment]
depends_on = [module.network]
}
10.2 Terraformによるインフラ構築の実践
Terraformの設計哲学と内部動作
Terraformは、HashiCorpが開発した最も人気のあるIaCツールです。その設計哲学を深く理解することで、より効果的な利用が可能になります。
リソースグラフとプランニング
Terraformは内部的に、すべてのリソースとその依存関係を有向非巡回グラフ(DAG)として管理します。
# 依存関係の自動解決
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project}-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id # 暗黙的な依存関係
tags = {
Name = "${var.project}-igw"
}
}
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)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project}-public-${var.availability_zones[count.index]}"
Type = "Public"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.project}-public-rt"
}
}
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
# Terraformは自動的に以下の順序で作成:
# 1. VPC
# 2. Internet Gateway, Subnets (並列実行可能)
# 3. Route Table
# 4. Route Table Associations
}
実践的なモジュール設計
再利用可能で保守性の高いモジュールの設計は、大規模インフラストラクチャ管理の鍵です。
完全なVPCモジュールの実装
# modules/vpc/variables.tf
variable "project_name" {
description = "プロジェクト名"
type = string
validation {
condition = can(regex("^[a-z0-9-]+$", var.project_name))
error_message = "Project name must contain only lowercase letters, numbers, and hyphens."
}
}
variable "environment" {
description = "環境名"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "vpc_cidr" {
description = "VPCのCIDRブロック"
type = string
default = "10.0.0.0/16"
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "VPC CIDR must be a valid IPv4 CIDR block."
}
}
variable "availability_zones" {
description = "使用するAZ"
type = list(string)
default = []
}
variable "enable_nat_gateway" {
description = "NAT Gatewayを有効にするか"
type = bool
default = true
}
variable "single_nat_gateway" {
description = "NAT Gatewayを1つだけ作成するか"
type = bool
default = false
}
variable "enable_vpn_gateway" {
description = "VPN Gatewayを有効にするか"
type = bool
default = false
}
variable "enable_flow_logs" {
description = "VPC Flow Logsを有効にするか"
type = bool
default = true
}
# modules/vpc/main.tf
locals {
azs = length(var.availability_zones) > 0 ? var.availability_zones : slice(data.aws_availability_zones.available.names, 0, 3)
# サブネット計算
# パブリック: /24 × AZ数
# プライベート: /24 × AZ数
# データベース: /24 × AZ数
public_cidrs = [for i in range(length(local.azs)) : cidrsubnet(var.vpc_cidr, 8, i)]
private_cidrs = [for i in range(length(local.azs)) : cidrsubnet(var.vpc_cidr, 8, i + 10)]
database_cidrs = [for i in range(length(local.azs)) : cidrsubnet(var.vpc_cidr, 8, i + 20)]
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
}
}
data "aws_availability_zones" "available" {
state = "available"
}
# VPC
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-vpc"
})
}
# Internet Gateway
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-igw"
})
}
# Public Subnets
resource "aws_subnet" "public" {
count = length(local.azs)
vpc_id = aws_vpc.this.id
cidr_block = local.public_cidrs[count.index]
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-public-${local.azs[count.index]}"
Type = "Public"
"kubernetes.io/role/elb" = "1" # EKS用タグ
})
}
# Private Subnets
resource "aws_subnet" "private" {
count = length(local.azs)
vpc_id = aws_vpc.this.id
cidr_block = local.private_cidrs[count.index]
availability_zone = local.azs[count.index]
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-private-${local.azs[count.index]}"
Type = "Private"
"kubernetes.io/role/internal-elb" = "1" # EKS用タグ
})
}
# Database Subnets
resource "aws_subnet" "database" {
count = length(local.azs)
vpc_id = aws_vpc.this.id
cidr_block = local.database_cidrs[count.index]
availability_zone = local.azs[count.index]
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-database-${local.azs[count.index]}"
Type = "Database"
})
}
# Elastic IPs for NAT Gateways
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(local.azs)) : 0
domain = "vpc"
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-nat-eip-${count.index + 1}"
})
depends_on = [aws_internet_gateway.this]
}
# NAT Gateways
resource "aws_nat_gateway" "this" {
count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(local.azs)) : 0
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-nat-${count.index + 1}"
})
depends_on = [aws_internet_gateway.this]
}
# Route Tables
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-public-rt"
Type = "Public"
})
}
resource "aws_route" "public_internet" {
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
resource "aws_route_table" "private" {
count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(local.azs)) : 0
vpc_id = aws_vpc.this.id
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-private-rt-${count.index + 1}"
Type = "Private"
})
}
resource "aws_route" "private_nat" {
count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(local.azs)) : 0
route_table_id = aws_route_table.private[count.index].id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = var.single_nat_gateway ? aws_nat_gateway.this[0].id : aws_nat_gateway.this[count.index].id
}
# Route Table Associations
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "private" {
count = length(aws_subnet.private)
subnet_id = aws_subnet.private[count.index].id
route_table_id = var.enable_nat_gateway ? (var.single_nat_gateway ? aws_route_table.private[0].id : aws_route_table.private[count.index].id) : aws_route_table.public.id
}
resource "aws_route_table_association" "database" {
count = length(aws_subnet.database)
subnet_id = aws_subnet.database[count.index].id
route_table_id = var.enable_nat_gateway ? (var.single_nat_gateway ? aws_route_table.private[0].id : aws_route_table.private[count.index].id) : aws_route_table.public.id
}
# VPC Flow Logs
resource "aws_flow_log" "this" {
count = var.enable_flow_logs ? 1 : 0
iam_role_arn = aws_iam_role.flow_logs[0].arn
log_destination = aws_cloudwatch_log_group.flow_logs[0].arn
traffic_type = "ALL"
vpc_id = aws_vpc.this.id
tags = merge(local.common_tags, {
Name = "${var.project_name}-${var.environment}-flow-logs"
})
}
resource "aws_cloudwatch_log_group" "flow_logs" {
count = var.enable_flow_logs ? 1 : 0
name = "/aws/vpc/${var.project_name}-${var.environment}"
retention_in_days = 30
tags = local.common_tags
}
resource "aws_iam_role" "flow_logs" {
count = var.enable_flow_logs ? 1 : 0
name = "${var.project_name}-${var.environment}-flow-logs-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "vpc-flow-logs.amazonaws.com"
}
}]
})
tags = local.common_tags
}
resource "aws_iam_role_policy" "flow_logs" {
count = var.enable_flow_logs ? 1 : 0
name = "${var.project_name}-${var.environment}-flow-logs-policy"
role = aws_iam_role.flow_logs[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams"
]
Effect = "Allow"
Resource = "*"
}]
})
}
# modules/vpc/outputs.tf
output "vpc_id" {
description = "VPCのID"
value = aws_vpc.this.id
}
output "vpc_cidr" {
description = "VPCのCIDRブロック"
value = aws_vpc.this.cidr_block
}
output "public_subnet_ids" {
description = "パブリックサブネットのIDリスト"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "プライベートサブネットのIDリスト"
value = aws_subnet.private[*].id
}
output "database_subnet_ids" {
description = "データベースサブネットのIDリスト"
value = aws_subnet.database[*].id
}
output "nat_gateway_ids" {
description = "NAT GatewayのIDリスト"
value = aws_nat_gateway.this[*].id
}
output "availability_zones" {
description = "使用されているAZのリスト"
value = local.azs
}
環境別構成管理のベストプラクティス
Terraformワークスペース vs ディレクトリ構造
# ワークスペースを使った環境管理
# 利点:単一の設定ファイルで複数環境を管理
# 欠点:環境間の差異が大きい場合に複雑化
locals {
# ワークスペース名から環境を判定
environment = terraform.workspace
# 環境別設定
instance_types = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.large"
}
instance_counts = {
dev = 1
staging = 2
prod = 4
}
enable_monitoring = {
dev = false
staging = true
prod = true
}
}
# ディレクトリベースの環境管理(推奨)
# environments/prod/main.tf
module "vpc" {
source = "../../modules/vpc"
project_name = var.project_name
environment = "prod"
vpc_cidr = "10.0.0.0/16"
enable_nat_gateway = true
single_nat_gateway = false # 高可用性のため各AZにNAT Gateway
enable_flow_logs = true
}
module "compute" {
source = "../../modules/compute"
project_name = var.project_name
environment = "prod"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
instance_type = "t3.large"
instance_count = 4
# 本番環境固有の設定
enable_monitoring = true
enable_detailed_monitoring = true
enable_auto_recovery = true
}
# environments/dev/main.tf
module "vpc" {
source = "../../modules/vpc"
project_name = var.project_name
environment = "dev"
vpc_cidr = "10.100.0.0/16" # 本番と異なるCIDR
enable_nat_gateway = true
single_nat_gateway = true # コスト削減のため1つのみ
enable_flow_logs = false # 開発環境では不要
}
高度なTerraform機能の活用
Dynamic Blocksによる柔軟な設定
# セキュリティグループの動的ルール生成
variable "security_group_rules" {
description = "セキュリティグループルール"
type = list(object({
type = string
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
description = string
}))
default = []
}
resource "aws_security_group" "this" {
name = "${var.project_name}-${var.environment}-sg"
description = "Security group for ${var.project_name}"
vpc_id = var.vpc_id
# 動的なインバウンドルール
dynamic "ingress" {
for_each = [for rule in var.security_group_rules : rule if rule.type == "ingress"]
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
description = ingress.value.description
}
}
# 動的なアウトバウンドルール
dynamic "egress" {
for_each = [for rule in var.security_group_rules : rule if rule.type == "egress"]
content {
from_port = egress.value.from_port
to_port = egress.value.to_port
protocol = egress.value.protocol
cidr_blocks = egress.value.cidr_blocks
description = egress.value.description
}
}
# デフォルトのアウトバウンドルール
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound traffic"
}
tags = merge(var.common_tags, {
Name = "${var.project_name}-${var.environment}-sg"
})
}
条件式とfor式の組み合わせ
# 複雑な条件に基づくリソース作成
locals {
# 本番環境のみマルチAZ、それ以外はシングルAZ
db_azs = var.environment == "prod" ? var.availability_zones : [var.availability_zones[0]]
# インスタンスタグの生成
instance_tags = merge(
var.common_tags,
{
for i in range(var.instance_count) :
"Instance${i}" => "web-${var.environment}-${i + 1}"
}
)
# 環境別のバックアップスケジュール
backup_schedules = {
prod = {
frequency = "daily"
retention = 30
time = "03:00"
}
staging = {
frequency = "weekly"
retention = 7
time = "03:00"
}
dev = null # 開発環境はバックアップなし
}
}
# RDSクラスターの条件付き作成
resource "aws_rds_cluster" "this" {
count = var.create_database ? 1 : 0
cluster_identifier = "${var.project_name}-${var.environment}-cluster"
engine = "aurora-mysql"
engine_version = var.engine_version
# 環境によって異なる設定
availability_zones = local.db_azs
# バックアップ設定
backup_retention_period = try(local.backup_schedules[var.environment].retention, 1)
preferred_backup_window = try(local.backup_schedules[var.environment].time, "03:00-04:00")
enabled_cloudwatch_logs_exports = var.environment == "prod" ? ["error", "general", "slowquery"] : []
# 本番環境のみ暗号化
storage_encrypted = var.environment == "prod"
kms_key_id = var.environment == "prod" ? aws_kms_key.rds[0].arn : null
# 本番環境のみ削除保護
deletion_protection = var.environment == "prod"
dynamic "scaling_configuration" {
for_each = var.serverless ? [1] : []
content {
auto_pause = var.environment != "prod"
min_capacity = var.environment == "prod" ? 2 : 1
max_capacity = var.environment == "prod" ? 16 : 4
seconds_until_auto_pause = 300
}
}
tags = var.common_tags
}
Terraformの状態管理上級テクニック
状態の分割とリモート参照
# ネットワーク層の状態を参照
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "terraform-state-bucket"
key = "env/${var.environment}/network/terraform.tfstate"
region = "ap-northeast-1"
}
}
# 共有リソースの状態を参照
data "terraform_remote_state" "shared" {
backend = "s3"
config = {
bucket = "terraform-state-bucket"
key = "global/shared/terraform.tfstate"
region = "ap-northeast-1"
}
}
# 参照した状態からの値の使用
resource "aws_instance" "app" {
subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
vpc_security_group_ids = [data.terraform_remote_state.network.outputs.app_security_group_id]
iam_instance_profile = data.terraform_remote_state.shared.outputs.ec2_instance_profile_name
# ...
}
状態の移行とリファクタリング
# 既存リソースのインポート
terraform import aws_instance.legacy i-1234567890abcdef0
# リソースの移動(リファクタリング)
terraform state mv aws_instance.old aws_instance.new
terraform state mv aws_instance.web module.compute.aws_instance.web
# モジュール間の移動
terraform state mv module.old.aws_vpc.main module.network.aws_vpc.main
# 状態からの削除(実リソースは削除されない)
terraform state rm aws_instance.temp
# 状態の一覧表示
terraform state list
# 特定リソースの詳細表示
terraform state show aws_instance.web
10.3 Ansibleによる構成管理の基礎
構成管理の本質と必要性
Infrastructure as Codeがインフラストラクチャをプロビジョニングするのに対し、構成管理ツールはそのインフラストラクチャ上でアプリケーションを動作させるための設定を行います。
# IaCと構成管理の責任分担
Infrastructure as Code (Terraform):
- ネットワークの作成
- サーバーのプロビジョニング
- ロードバランサーの設定
- データベースの作成
- セキュリティグループの定義
Configuration Management (Ansible):
- OSの設定とハードニング
- ミドルウェアのインストール
- アプリケーションのデプロイ
- 設定ファイルの管理
- ユーザーと権限の管理
Ansibleのアーキテクチャと特徴
Ansibleは、エージェントレスで動作し、SSHを通じて管理対象ノードを制御します。この設計により、追加のソフトウェアインストールが不要で、既存環境への導入が容易です。
Playbookの構造と設計
---
# site.yml - マスターPlaybook
- name: Common configuration for all servers
hosts: all
become: yes
roles:
- common
- security
- name: Configure web servers
hosts: webservers
become: yes
roles:
- nginx
- app-deploy
vars:
app_version: "``{{ lookup('env', 'APP_VERSION') | default('latest', true) }}``"
- name: Configure database servers
hosts: databases
become: yes
roles:
- postgresql
- backup
# group_vars/all.yml - 全ホスト共通変数
---
ntp_servers:
- ntp.nict.jp
- ntp.jst.mfeed.ad.jp
timezone: Asia/Tokyo
security_ssh_port: 22
security_ssh_password_authentication: "no"
security_ssh_permit_root_login: "no"
# group_vars/webservers.yml - Webサーバー用変数
---
nginx_worker_processes: "``{{ ansible_processor_vcpus }}``"
nginx_worker_connections: 2048
app_user: webapp
app_group: webapp
app_home: /var/www/app
app_port: 3000
# group_vars/production.yml - 本番環境用変数
---
nginx_server_tokens: "off"
nginx_ssl_protocols: "TLSv1.2 TLSv1.3"
nginx_ssl_ciphers: "HIGH:!aNULL:!MD5"
enable_monitoring: true
enable_log_shipping: true
高度なRole設計
# roles/nginx/tasks/main.yml
---
- name: Include OS-specific variables
include_vars: "``{{ ansible_os_family }}``.yml"
- name: Install Nginx
package:
name: "``{{ nginx_package_name }}``"
state: present
notify: restart nginx
- name: Create Nginx directories
file:
path: "``{{ item }}``"
state: directory
owner: root
group: root
mode: '0755'
loop:
- /etc/nginx/sites-available
- /etc/nginx/sites-enabled
- /etc/nginx/ssl
- /var/log/nginx
- name: Generate DH parameters
openssl_dhparam:
path: /etc/nginx/ssl/dhparams.pem
size: 2048
when: nginx_use_ssl | default(false)
- name: Configure Nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
validate: 'nginx -t -c %s'
notify: reload nginx
- name: Configure virtual hosts
template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/``{{ item.name }}``"
owner: root
group: root
mode: '0644'
loop: "``{{ nginx_vhosts }}``"
when: nginx_vhosts is defined
notify: reload nginx
- name: Enable virtual hosts
file:
src: "/etc/nginx/sites-available/``{{ item.name }}``"
dest: "/etc/nginx/sites-enabled/``{{ item.name }}``"
state: link
loop: "``{{ nginx_vhosts }}``"
when: nginx_vhosts is defined
notify: reload nginx
- name: Remove default site
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: reload nginx
- name: Ensure Nginx is running
systemd:
name: nginx
state: started
enabled: yes
daemon_reload: yes
# roles/nginx/templates/nginx.conf.j2
user ``{{ nginx_user }}``;
worker_processes ``{{ nginx_worker_processes }}``;
pid /run/nginx.pid;
events {
worker_connections ``{{ nginx_worker_connections }}``;
multi_accept on;
use epoll;
}
http {
# 基本設定
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens ``{{ nginx_server_tokens | default('on') }}``;
# MIME types
include /etc/nginx/mime.types;
default_type application/octet-stream;
# SSL設定
{% if nginx_use_ssl | default(false) %}
ssl_protocols {{ nginx_ssl_protocols }};
ssl_ciphers {{ nginx_ssl_ciphers }};
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;
{% endif %}
# ログ設定
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Gzip圧縮
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml application/atom+xml image/svg+xml;
# バーチャルホスト設定
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
# roles/nginx/handlers/main.yml
---
- name: restart nginx
systemd:
name: nginx
state: restarted
when: nginx_restart_on_change | default(true)
- name: reload nginx
systemd:
name: nginx
state: reloaded
when: nginx_reload_on_change | default(true)
- name: validate nginx configuration
command: nginx -t
changed_when: false
動的インベントリとクラウド統合
クラウド環境では、インスタンスが動的に作成・削除されるため、静的なインベントリファイルでは管理が困難です。
#!/usr/bin/env python3
# dynamic_inventory_aws.py
import json
import boto3
from collections import defaultdict
class AWSInventory:
def __init__(self):
self.inventory = defaultdict(lambda: {'hosts': [], 'vars': {}})
self.inventory['_meta'] = {'hostvars': {}}
def get_instances(self):
"""EC2インスタンスを取得してインベントリを構築"""
ec2 = boto3.client('ec2', region_name='ap-northeast-1')
# 実行中のインスタンスを取得
response = ec2.describe_instances(
Filters=[
{'Name': 'instance-state-name', 'Values': ['running']}
]
)
for reservation in response['Reservations']:
for instance in reservation['Instances']:
self._add_instance(instance)
def _add_instance(self, instance):
"""インスタンスをインベントリに追加"""
instance_id = instance['InstanceId']
# プライベートIPアドレスを使用(VPC内通信)
private_ip = instance.get('PrivateIpAddress')
if not private_ip:
return
# タグからグループを決定
tags = {tag['Key']: tag['Value'] for tag in instance.get('Tags', [])}
# 環境グループ
environment = tags.get('Environment', 'unknown')
self.inventory[environment]['hosts'].append(private_ip)
# 役割グループ
role = tags.get('Role', 'unknown')
self.inventory[role]['hosts'].append(private_ip)
# 組み合わせグループ
combined_group = f"{environment}_{role}"
self.inventory[combined_group]['hosts'].append(private_ip)
# ホスト変数
self.inventory['_meta']['hostvars'][private_ip] = {
'instance_id': instance_id,
'instance_type': instance['InstanceType'],
'availability_zone': instance['Placement']['AvailabilityZone'],
'private_dns_name': instance.get('PrivateDnsName', ''),
'tags': tags,
'ansible_host': private_ip,
'ansible_ssh_private_key_file': f"~/.ssh/{environment}.pem"
}
# 特別な設定
if role == 'bastion':
self.inventory['_meta']['hostvars'][private_ip]['ansible_ssh_common_args'] = '-o ProxyCommand="none"'
else:
# 踏み台経由の接続
bastion_ip = self._get_bastion_ip(environment)
if bastion_ip:
self.inventory['_meta']['hostvars'][private_ip]['ansible_ssh_common_args'] = \
f'-o ProxyCommand="ssh -W %h:%p -q ubuntu@{bastion_ip}"'
def _get_bastion_ip(self, environment):
"""踏み台サーバーのIPを取得"""
bastion_group = f"{environment}_bastion"
if bastion_group in self.inventory and self.inventory[bastion_group]['hosts']:
return self.inventory[bastion_group]['hosts'][0]
return None
def get_inventory(self):
"""インベントリをJSON形式で返す"""
self.get_instances()
return json.dumps(self.inventory, indent=2)
if __name__ == '__main__':
inventory = AWSInventory()
print(inventory.get_inventory())
冪等性の確保とベストプラクティス
冪等性は構成管理において最も重要な概念です。同じPlaybookを何度実行しても、システムの状態が同じになることを保証します。
# 冪等性を保証する書き方の例
---
- name: 冪等性のあるタスク例
hosts: all
become: yes
tasks:
# GOOD: 冪等性あり - ファイルが既に存在すれば変更なし
- name: Create application directory
file:
path: /opt/myapp
state: directory
owner: myapp
group: myapp
mode: '0755'
# GOOD: 冪等性あり - 行が既に存在すれば追加しない
- name: Add configuration line
lineinfile:
path: /etc/sysctl.conf
line: 'vm.swappiness=10'
state: present
# GOOD: 冪等性あり - パッケージが既にインストール済みなら何もしない
- name: Install required packages
package:
name:
- git
- python3-pip
- nginx
state: present
# BAD: 冪等性なし - 実行するたびにファイルに追記される
- name: Append to log file (非推奨)
shell: echo "Deployment at $(date)" >> /var/log/deploy.log
# GOOD: 上記の冪等性のある代替案
- name: Record deployment
copy:
content: "Last deployment: ``{{ ansible_date_time.iso8601 }}``\n"
dest: /var/log/last_deploy.log
owner: root
group: root
mode: '0644'
# 条件付き実行で冪等性を確保
- name: Initialize database
command: /opt/myapp/bin/init_db.sh
args:
creates: /opt/myapp/db/.initialized
# .initializedファイルが存在する場合は実行しない
# チェックモードでの動作確認
- name: Configure service
template:
src: myapp.service.j2
dest: /etc/systemd/system/myapp.service
register: service_config
check_mode: yes
- name: Reload systemd if needed
systemd:
daemon_reload: yes
when: service_config.changed
セキュアな変数管理
機密情報を安全に管理するため、Ansible Vaultを活用します。
# Vault暗号化されたファイルの作成
# ansible-vault create vars/secrets.yml
# 平文の secrets.yml
---
database_password: "super-secret-password"
api_keys:
stripe: "sk_live_..."
aws_access_key: "AKIA..."
aws_secret_key: "..."
# 暗号化後
$ANSIBLE_VAULT;1.1;AES256
39613836386435386...(暗号化されたデータ)
# Playbookでの使用
---
- name: Deploy application with secrets
hosts: webservers
vars_files:
- vars/common.yml
- vars/secrets.yml # Vault暗号化されたファイル
tasks:
- name: Create database configuration
template:
src: database.yml.j2
dest: "``{{ app_home }}``/config/database.yml"
owner: "``{{ app_user }}``"
group: "``{{ app_group }}``"
mode: '0600' # 機密情報のため厳格な権限
no_log: true # ログに機密情報を出力しない
# templates/database.yml.j2
production:
adapter: postgresql
encoding: unicode
database: ``{{ database_name }}``
pool: ``{{ database_pool | default(5) }}``
username: ``{{ database_user }}``
password: ``{{ database_password }}`` # Vaultから取得
host: ``{{ database_host }}``
port: ``{{ database_port | default(5432) }}``
# 実行時
# ansible-playbook -i inventory site.yml --ask-vault-pass
# または
# ansible-playbook -i inventory site.yml --vault-password-file ~/.vault_pass
10.4 CI/CDパイプラインとデプロイ自動化
CI/CDの本質と価値
継続的インテグレーション(CI)と継続的デリバリー(CD)は、ソフトウェア開発のスピードと品質を両立させるための方法論です。インフラストラクチャのコード化により、これらの実践をインフラ管理にも適用できるようになりました。
CI/CDパイプラインの設計原則
パイプライン設計の原則:
高速フィードバック:
- 問題の早期発見
- 10分以内の基本的なフィードバック
- 段階的な詳細検証
自動化の徹底:
- 手動プロセスの排除
- 一貫性の確保
- ヒューマンエラーの防止
段階的なリスク管理:
- 環境を段階的に昇格
- 各段階での検証強化
- ロールバック可能性の確保
監査証跡:
- すべての変更の記録
- 承認プロセスの可視化
- コンプライアンス対応
包括的なCI/CDパイプラインの実装
GitHub Actionsによる完全自動化
# .github/workflows/infrastructure-pipeline.yml
name: Infrastructure CI/CD Pipeline
on:
pull_request:
branches: [main]
paths:
- 'terraform/**'
- 'ansible/**'
push:
branches: [main]
paths:
- 'terraform/**'
- 'ansible/**'
env:
TF_VERSION: '1.5.0'
ANSIBLE_VERSION: '2.15.0'
AWS_REGION: 'ap-northeast-1'
jobs:
# 1. 静的解析とlint
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: ``${{ env.TF_VERSION }}``
- name: Terraform Format Check
run: |
cd terraform
terraform fmt -check -recursive
- name: Terraform Validate
run: |
cd terraform
for dir in $(find . -type f -name "*.tf" -exec dirname {} \; | sort -u); do
echo "Validating $dir"
(cd "$dir" && terraform init -backend=false && terraform validate)
done
- name: TFLint
uses: terraform-linters/setup-tflint@v3
with:
tflint_version: latest
- name: Run TFLint
run: |
cd terraform
tflint --init
tflint --recursive
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install Ansible and ansible-lint
run: |
pip install ansible=``${{ env.ANSIBLE_VERSION }}`` ansible-lint
- name: Ansible Lint
run: |
cd ansible
ansible-lint
# 2. セキュリティスキャン
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkov Security Scan
id: checkov
uses: bridgecrewio/checkov-action@master
with:
directory: terraform/
framework: terraform
output_format: sarif
output_file_path: reports/checkov.sarif
- name: Upload Checkov results
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: reports/checkov.sarif
- name: Terrascan
run: |
wget https://github.com/tenable/terrascan/releases/latest/download/terrascan_Linux_x86_64.tar.gz
tar -xf terrascan_Linux_x86_64.tar.gz
./terrascan scan -i terraform -d terraform/
- name: Secrets Scan
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ``${{ github.event.repository.default_branch }}``
head: HEAD
# 3. コスト見積もり
cost-estimation:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Setup Infracost
uses: infracost/setup-infracost@v2
with:
api-key: ``${{ secrets.INFRACOST_API_KEY }}``
- name: Generate Infracost JSON
run: |
cd terraform/environments/prod
infracost breakdown --path . \
--format json \
--out-file /tmp/infracost.json
- name: Post Infracost comment
uses: infracost/infracost-comment@v1
with:
path: /tmp/infracost.json
behavior: update
# 4. Terraformプラン(PR時)
terraform-plan:
needs: [static-analysis, security-scan]
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
strategy:
matrix:
environment: [dev, staging, prod]
permissions:
contents: read
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ``${{ secrets[format('AWS_{0}_ROLE', matrix.environment)] }}``
aws-region: ``${{ env.AWS_REGION }}``
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: ``${{ env.TF_VERSION }}``
- name: Terraform Init
run: |
cd terraform/environments/``${{ matrix.environment }}``
terraform init
- name: Terraform Plan
id: plan
run: |
cd terraform/environments/``${{ matrix.environment }}``
terraform plan -out=tfplan -no-color | tee plan_output.txt
- name: Create Plan Summary
uses: actions/github-script@v6
if: github.event_name == 'pull_request'
with:
script: |
const output = `#### Terraform Plan - ``${{ matrix.environment }}`` 📋
<details><summary>Show Plan</summary>
\`\`\`terraform
${require('fs').readFileSync('terraform/environments/``${{ matrix.environment }}``/plan_output.txt', 'utf8')}
\`\`\`
</details>
*Pushed by: @``${{ github.actor }}``, Action: \``${{ github.event_name }}``\`*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
# 5. 統合テスト環境の構築
integration-test:
needs: terraform-plan
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ``${{ secrets.AWS_TEST_ROLE }}``
aws-region: ``${{ env.AWS_REGION }}``
- name: Create Test Environment
id: test-env
run: |
cd terraform/environments/test
terraform init
terraform apply -auto-approve \
-var="pr_number=``${{ github.event.pull_request.number }}``"
# 出力値を環境変数に設定
echo "test_url=$(terraform output -raw test_environment_url)" >> $GITHUB_OUTPUT
- name: Run Integration Tests
run: |
cd tests/integration
npm install
npm run test:integration -- \
--url "``${{ steps.test-env.outputs.test_url }}``"
- name: Run E2E Tests
uses: cypress-io/github-action@v5
with:
config: baseUrl=``${{ steps.test-env.outputs.test_url }}``
spec: tests/e2e/**/*.cy.js
- name: Cleanup Test Environment
if: always()
run: |
cd terraform/environments/test
terraform destroy -auto-approve \
-var="pr_number=``${{ github.event.pull_request.number }}``"
# 6. プロダクションデプロイ
deploy-infrastructure:
needs: [static-analysis, security-scan, cost-estimation]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
strategy:
matrix:
environment: [dev, staging, prod]
max-parallel: 1 # 環境を順番にデプロイ
environment: ``${{ matrix.environment }}``
steps:
- uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ``${{ secrets[format('AWS_{0}_ROLE', matrix.environment)] }}``
aws-region: ``${{ env.AWS_REGION }}``
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: ``${{ env.TF_VERSION }}``
- name: Terraform Apply
run: |
cd terraform/environments/``${{ matrix.environment }}``
terraform init
terraform apply -auto-approve
- name: Run Smoke Tests
run: |
cd tests/smoke
./run_smoke_tests.sh ``${{ matrix.environment }}``
- name: Update Documentation
if: matrix.environment == 'prod'
run: |
cd terraform/environments/``${{ matrix.environment }}``
terraform show -json > ../../../docs/infrastructure-state.json
terraform graph | dot -Tpng > ../../../docs/infrastructure-diagram.png
# 7. 構成管理の適用
configure-servers:
needs: deploy-infrastructure
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
strategy:
matrix:
environment: [dev, staging, prod]
max-parallel: 1
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install Ansible
run: |
pip install ansible=``${{ env.ANSIBLE_VERSION }}``
pip install boto3 # AWS動的インベントリ用
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ``${{ secrets[format('AWS_{0}_ROLE', matrix.environment)] }}``
aws-region: ``${{ env.AWS_REGION }}``
- name: Run Ansible Playbook
env:
ANSIBLE_HOST_KEY_CHECKING: False
ANSIBLE_VAULT_PASSWORD: ``${{ secrets.ANSIBLE_VAULT_PASSWORD }}``
run: |
cd ansible
# Vault パスワードファイルの作成
echo "$ANSIBLE_VAULT_PASSWORD" > .vault_pass
# 動的インベントリを使用してPlaybook実行
ansible-playbook -i inventory/aws_ec2.yml \
site.yml \
--limit "``${{ matrix.environment }}``" \
--vault-password-file .vault_pass
# クリーンアップ
rm -f .vault_pass
# 8. 監視とアラートの設定
setup-monitoring:
needs: configure-servers
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Configure Datadog
env:
DD_API_KEY: ``${{ secrets.DATADOG_API_KEY }}``
DD_APP_KEY: ``${{ secrets.DATADOG_APP_KEY }}``
run: |
cd monitoring/datadog
./setup_monitors.sh
- name: Configure PagerDuty
env:
PAGERDUTY_TOKEN: ``${{ secrets.PAGERDUTY_TOKEN }}``
run: |
cd monitoring/pagerduty
./setup_escalation_policies.sh
デプロイメント戦略の実装
Blue-Green デプロイメント
# terraform/modules/blue-green/main.tf
variable "active_environment" {
description = "現在アクティブな環境 (blue/green)"
type = string
default = "blue"
}
locals {
environments = toset(["blue", "green"])
inactive_environment = var.active_environment == "blue" ? "green" : "blue"
}
# 両環境のASG
resource "aws_autoscaling_group" "app" {
for_each = local.environments
name = "${var.project_name}-${each.key}"
vpc_zone_identifier = var.private_subnet_ids
target_group_arns = each.key == var.active_environment ? [aws_lb_target_group.app.arn] : []
health_check_type = "ELB"
health_check_grace_period = 300
min_size = each.key == var.active_environment ? var.min_size : 0
max_size = var.max_size
desired_capacity = each.key == var.active_environment ? var.desired_capacity : 0
launch_template {
id = aws_launch_template.app[each.key].id
version = "$Latest"
}
tag {
key = "Name"
value = "${var.project_name}-${each.key}"
propagate_at_launch = true
}
tag {
key = "Environment"
value = each.key
propagate_at_launch = true
}
}
# 切り替えスクリプト
resource "local_file" "switch_environment" {
filename = "${path.module}/switch_environment.sh"
content = <<-EOF
#!/bin/bash
set -e
CURRENT="${var.active_environment}"
TARGET="${local.inactive_environment}"
echo "Switching from $CURRENT to $TARGET environment..."
# 1. 新環境を起動
aws autoscaling set-desired-capacity \
--auto-scaling-group-name ${var.project_name}-$TARGET \
--desired-capacity ${var.desired_capacity}
# 2. ヘルスチェックを待つ
aws autoscaling wait \
--auto-scaling-group-name ${var.project_name}-$TARGET \
--query "length(AutoScalingGroups[0].Instances[?HealthStatus=='Healthy'])" \
--desired-value ${var.desired_capacity}
# 3. トラフィックを切り替え
terraform apply -var="active_environment=$TARGET" -auto-approve
# 4. 旧環境を停止
aws autoscaling set-desired-capacity \
--auto-scaling-group-name ${var.project_name}-$CURRENT \
--desired-capacity 0
echo "Environment switch completed!"
EOF
file_permission = "0755"
}
カナリアデプロイメント
# kubernetes/canary-deployment.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 8080
---
# 安定版(90%のトラフィック)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-stable
spec:
replicas: 9
selector:
matchLabels:
app: myapp
version: stable
template:
metadata:
labels:
app: myapp
version: stable
spec:
containers:
- name: myapp
image: myapp:v1.0.0
ports:
- containerPort: 8080
---
# カナリア版(10%のトラフィック)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-canary
spec:
replicas: 1
selector:
matchLabels:
app: myapp
version: canary
template:
metadata:
labels:
app: myapp
version: canary
spec:
containers:
- name: myapp
image: myapp:v2.0.0
ports:
- containerPort: 8080
---
# 自動化されたカナリア分析
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: myapp
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
service:
port: 80
analysis:
interval: 1m
threshold: 10
maxWeight: 50
stepWeight: 5
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 1m
- name: request-duration
thresholdRange:
max: 500
interval: 1m
webhooks:
- name: load-test
url: http://loadtester/
metadata:
cmd: "hey -z 1m -c 10 -q 20 http://myapp/"
GitOpsによる宣言的デプロイメント
# argocd/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/infrastructure
targetRevision: HEAD
path: kubernetes/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
# Terraform連携
initContainers:
- name: terraform-apply
image: hashicorp/terraform:1.5.0
command:
- sh
- -c
- |
cd /terraform
terraform init
terraform apply -auto-approve
volumeMounts:
- name: terraform-config
mountPath: /terraform
# Post-sync hooks
postSync:
- name: smoke-tests
container:
image: postman/newman
command: ["newman", "run", "smoke-tests.json"]
- name: notify-slack
container:
image: curlimages/curl
command:
- sh
- -c
- |
curl -X POST $SLACK_WEBHOOK \
-H 'Content-type: application/json' \
-d '{"text":"Deployment completed successfully"}'
Infrastructure as Code と自動化は、現代のクラウド運用の基盤です。宣言的な管理、バージョン管理、自動化を組み合わせることで、信頼性が高く、監査可能で、効率的なインフラストラクチャ運用が実現できます。
重要なのは、これらのツールと手法を段階的に導入し、組織の成熟度に合わせて進化させていくことです。完璧を求めるのではなく、継続的な改善を通じて、より良いインフラストラクチャ管理を実現していくことが成功への鍵となります。
第11章へ進む