- 1. Infrastructure as Codeとは
- 2. Terraform基礎
- 3. HCL構文の深掘り
- 4. 状態管理
- 5. モジュール
- 6. Terragrunt
- 7. ワークフロー
- 8. AWS実践例 - VPC + EC2 + RDS + ALB
- 9. セキュリティ
- 10. ベストプラクティス
- まとめ
1. Infrastructure as Codeとは
なぜインフラをコードで管理するのか
従来のインフラ管理方式は、管理者がコンソールにアクセスして手動でサーバーをプロビジョニングし、ネットワークを設定するものでした。この方式にはいくつかの問題があります。
- 再現不可能: 同一の環境を再度構築することが困難です。
- 変更追跡不可: 誰が、いつ、何を変更したか把握できません。
- スケーリングの限界: サーバー10台までは手動で管理できますが、100台以上は事実上不可能です。
- 環境の不一致: 開発、ステージング、本番環境が微妙に異なるConfiguration Driftが発生します。
Infrastructure as Code(IaC)は、インフラの望ましい状態をコードファイルで定義し、ツールが自動的にその状態を実現する方法論です。コードであるためGitでバージョン管理し、コードレビューを行い、CI/CDパイプラインでデプロイできます。
IaCツール比較
| ツール | 言語 | アプローチ | 状態管理 | クラウド対応 |
|---|---|---|---|---|
| Terraform | HCL | 宣言的 | 自己管理Stateファイル | マルチクラウド |
| CloudFormation | JSON/YAML | 宣言的 | AWS管理 | AWS専用 |
| Pulumi | TypeScript/Python/Go | 命令的 + 宣言的 | Pulumi Cloud | マルチクラウド |
| AWS CDK | TypeScript/Python/Go | 命令的 | CloudFormationスタック | AWS専用 |
| Crossplane | YAML | 宣言的 (K8s CRD) | K8s etcd | マルチクラウド |
TerraformはHCLという専用の宣言的言語を使用し、最も広いプロバイダエコシステムを持っています。AWS、GCP、Azureはもちろん、Datadog、PagerDuty、GitHubのようなSaaSも管理できます。
CloudFormationはAWSネイティブツールで、AWSサービスとの統合が最も早いです。新しいAWSサービスのリリースと同時にサポートされます。
Pulumiは汎用プログラミング言語を使用するため、IDE自動補完、型チェック、ユニットテストをそのまま活用できます。
AWS CDKはCloudFormationの上に抽象化レイヤーを追加したツールで、L2/L3 Constructで複雑なパターンを簡潔に表現します。
宣言的 vs 命令的アプローチ
IaCツールは大きく2つのアプローチに分かれます。
宣言的(Declarative): 望ましい最終状態を定義すると、ツールが現在の状態との差分を計算して変更を実行します。TerraformとCloudFormationがこの方式です。
命令的(Imperative): 実行する手順を順番に記述します。シェルスクリプト、Ansible(一部)、Pulumiがこの方式に近いです。
Terraformは宣言的アプローチを採用しており、インフラの「何を(What)」を定義すれば「どのように(How)」はTerraformが処理します。
2. Terraform基礎
Provider
ProviderはTerraformが特定のインフラプラットフォームと通信するためのプラグインです。AWS、GCP、Azureのようなクラウドプロバイダだけでなく、Kubernetes、Helm、Datadog、GitHubなど数千のプロバイダが存在します。
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-2"
default_tags {
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
}
required_providersブロックでプロバイダのソースとバージョン制約を明示します。~> 5.0は5.x範囲の最新バージョンを使用するが6.0以上は許可しないという意味です。
Resource
Resourceは Terraformで管理するインフラオブジェクトを宣言します。リソースタイプとローカル名の組み合わせで一意に識別されます。
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "main-vpc"
}
}
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-${count.index + 1}"
}
}
リソース間の参照はリソースタイプ.ローカル名.属性の形式です。上記の例でaws_vpc.main.idはVPCリソースのIDを参照しています。
Data Source
Data SourceはTerraform外部で既に存在するリソースの情報を読み取ります。読み取り専用のため、インフラを変更しません。
data "aws_availability_zones" "available" {
state = "available"
}
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
VariableとOutput
Variableはモジュールの入力パラメータで、Outputはモジュールの出力値です。
# variables.tf
variable "environment" {
description = "デプロイ環境 (dev, staging, prod)"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environmentはdev、staging、prodのいずれかでなければなりません。"
}
}
variable "instance_type" {
description = "EC2インスタンスタイプ"
type = string
default = "t3.medium"
}
variable "db_password" {
description = "データベースパスワード"
type = string
sensitive = true
}
# outputs.tf
output "vpc_id" {
description = "作成されたVPCのID"
value = aws_vpc.main.id
}
output "alb_dns_name" {
description = "ALBのDNS名"
value = aws_lb.main.dns_name
}
3. HCL構文の深掘り
ブロック構造
HCL(HashiCorp Configuration Language)の基本構造はブロックです。ブロックはタイプ、ラベル、ボディで構成されます。
# ブロックタイプ "ラベル1" "ラベル2" {
# 属性 = 値
# }
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
root_block_device {
volume_size = 20
volume_type = "gp3"
}
}
型システム
HCLは豊富な型システムをサポートしています。
# プリミティブ型
variable "name" {
type = string
}
variable "port" {
type = number
}
variable "enabled" {
type = bool
}
# コレクション型
variable "availability_zones" {
type = list(string)
}
variable "instance_tags" {
type = map(string)
}
variable "allowed_ports" {
type = set(number)
}
# 構造体型
variable "database_config" {
type = object({
engine = string
engine_version = string
instance_class = string
allocated_storage = number
multi_az = bool
})
}
条件式
条件式は三項演算子の形式で記述します。
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
monitoring = var.environment == "prod" ? true : false
}
ループ - countとfor_each
countは数値ベースの反復に適しています。
resource "aws_subnet" "private" {
count = 3
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 10)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "private-subnet-${count.index + 1}"
}
}
for_eachはマップやセットベースの反復に適しており、途中の項目を削除してもインデックスがずれません。
variable "subnets" {
type = map(object({
cidr_block = string
availability_zone = string
public = bool
}))
}
resource "aws_subnet" "this" {
for_each = var.subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
map_public_ip_on_launch = each.value.public
tags = {
Name = each.key
}
}
for式
locals {
# リスト変換
subnet_ids = [for s in aws_subnet.this : s.id]
# 条件付きフィルタリング
public_subnet_ids = [for k, s in aws_subnet.this : s.id if s.map_public_ip_on_launch]
# マップ変換
subnet_id_map = { for k, s in aws_subnet.this : k => s.id }
}
ローカル変数
ローカル変数は、モジュール内で繰り返し使用する値を一箇所で定義します。
locals {
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
Team = "platform"
}
name_prefix = "${var.project_name}-${var.environment}"
is_production = var.environment == "prod"
}
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = local.is_production ? "t3.large" : "t3.micro"
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web"
Role = "webserver"
})
}
4. 状態管理
terraform.tfstateとは
Terraformは管理中のインフラの現在の状態をStateファイルに記録します。planコマンドはStateファイルの状態とコードで宣言された状態を比較して変更点を計算します。
Stateファイルには機密情報(パスワード、キーなど)が含まれる可能性があるため、ローカルファイルシステムに保存するのは危険です。
リモートバックエンド (S3 + DynamoDB)
チーム環境ではリモートバックエンドを使用してStateを中央で管理する必要があります。AWSではS3 + DynamoDBの組み合わせが標準です。
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "prod/vpc/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "terraform-lock"
}
}
S3バケットには必ずバージョニングを有効化し、サーバーサイド暗号化を適用する必要があります。
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-terraform-state-bucket"
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_lock" {
name = "terraform-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
State Locking
DynamoDBテーブルはStateロックを提供します。2人が同時にterraform applyを実行すると、先に実行した人がロックを取得し、後から実行した人は待機します。
ロックが解放されない場合(プロセスの異常終了など)は、terraform force-unlock LOCK_IDコマンドで強制解除できます。ただし、他の人が実際に作業中でないことを必ず確認してください。
State管理コマンド
# Stateのリソース一覧確認
terraform state list
# 特定リソースの状態確認
terraform state show aws_vpc.main
# リソース名の変更(コードで名前を変えた場合)
terraform state mv aws_vpc.main aws_vpc.primary
# Stateからリソースを除去(実際のインフラは維持)
terraform state rm aws_instance.temp
# 既存インフラをStateにインポート
terraform import aws_vpc.existing vpc-0123456789abcdef0
5. モジュール
モジュールとは
モジュールは関連するリソースを1つのパッケージにまとめたものです。コードの再利用性を高め、関心事を分離し、チーム間で標準化されたインフラパターンを共有できるようにします。
モジュールのディレクトリ構造
modules/
vpc/
main.tf
variables.tf
outputs.tf
README.md
ec2/
main.tf
variables.tf
outputs.tf
rds/
main.tf
variables.tf
outputs.tf
モジュールの作成
# modules/vpc/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
enable_dns_support = true
enable_dns_hostnames = true
tags = merge(var.tags, {
Name = "${var.name_prefix}-vpc"
})
}
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = merge(var.tags, {
Name = "${var.name_prefix}-igw"
})
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = merge(var.tags, {
Name = "${var.name_prefix}-public-${count.index + 1}"
Tier = "public"
})
}
resource "aws_subnet" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
tags = merge(var.tags, {
Name = "${var.name_prefix}-private-${count.index + 1}"
Tier = "private"
})
}
# modules/vpc/variables.tf
variable "name_prefix" {
description = "リソース名のプレフィックス"
type = string
}
variable "cidr_block" {
description = "VPC CIDRブロック"
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_cidrs" {
description = "パブリックサブネットCIDRリスト"
type = list(string)
}
variable "private_subnet_cidrs" {
description = "プライベートサブネットCIDRリスト"
type = list(string)
}
variable "availability_zones" {
description = "アベイラビリティゾーンリスト"
type = list(string)
}
variable "tags" {
description = "共通タグ"
type = map(string)
default = {}
}
# modules/vpc/outputs.tf
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.this.id
}
output "public_subnet_ids" {
description = "パブリックサブネットIDリスト"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "プライベートサブネットIDリスト"
value = aws_subnet.private[*].id
}
モジュールの呼び出し
module "vpc" {
source = "./modules/vpc"
name_prefix = "myapp-prod"
cidr_block = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
availability_zones = ["ap-northeast-2a", "ap-northeast-2c"]
tags = local.common_tags
}
# モジュール出力の参照
resource "aws_instance" "web" {
subnet_id = module.vpc.public_subnet_ids[0]
# ...
}
Terraform Registryモジュール
Terraform RegistryにはコミュニティとHashiCorpが検証したモジュールが公開されています。
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.5.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["ap-northeast-2a", "ap-northeast-2c"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnets = ["10.0.11.0/24", "10.0.12.0/24"]
enable_nat_gateway = true
single_nat_gateway = false
enable_dns_hostnames = true
}
6. Terragrunt
Terragruntとは
TerragruntはTerraformのラッパーツールで、DRY(Don't Repeat Yourself)原則を適用して重複設定を排除します。複数の環境(dev、staging、prod)を管理する際に特に有用です。
ディレクトリ構造
infrastructure/
terragrunt.hcl # ルート設定
environments/
dev/
terragrunt.hcl # dev環境共通設定
vpc/
terragrunt.hcl
ec2/
terragrunt.hcl
rds/
terragrunt.hcl
staging/
terragrunt.hcl
vpc/
terragrunt.hcl
ec2/
terragrunt.hcl
prod/
terragrunt.hcl
vpc/
terragrunt.hcl
ec2/
terragrunt.hcl
rds/
terragrunt.hcl
modules/
vpc/
ec2/
rds/
ルート設定
# infrastructure/terragrunt.hcl
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "my-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "terraform-lock"
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "ap-northeast-2"
}
EOF
}
環境別設定
# environments/prod/vpc/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules/vpc"
}
inputs = {
name_prefix = "myapp-prod"
cidr_block = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
availability_zones = ["ap-northeast-2a", "ap-northeast-2c"]
}
依存関係管理
Terragruntはモジュール間の依存関係を明示的に宣言できます。
# environments/prod/ec2/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules/ec2"
}
dependency "vpc" {
config_path = "../vpc"
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_id = dependency.vpc.outputs.public_subnet_ids[0]
}
terragrunt run-all applyを実行すると、依存関係の順序に従ってVPCを先に作成し、EC2を後から作成します。
7. ワークフロー
コアコマンド
# プロバイダプラグインのダウンロードと初期化
terraform init
# 変更計画の確認(実際の変更なし)
terraform plan
# 変更の適用
terraform apply
# 特定リソースのみ適用
terraform apply -target=aws_vpc.main
# インフラ全体の削除
terraform destroy
# コードフォーマット
terraform fmt -recursive
# 設定の妥当性検証
terraform validate
# 依存関係グラフの出力
terraform graph | dot -Tpng > graph.png
Planファイルの保存
# plan結果をファイルに保存
terraform plan -out=tfplan
# 保存されたplanをそのまま適用(追加確認なし)
terraform apply tfplan
planファイルを使用すると、plan時点とapply時点の間にコードが変更されてもplan時点の変更のみが適用されます。
CI/CD連携
GitHub Actionsを使用したTerraform CI/CDパイプラインの例です。
name: Terraform CI/CD
on:
pull_request:
paths:
- 'infrastructure/**'
push:
branches:
- main
paths:
- 'infrastructure/**'
jobs:
plan:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.0
- name: Terraform Init
working-directory: infrastructure
run: terraform init
- name: Terraform Format Check
working-directory: infrastructure
run: terraform fmt -check -recursive
- name: Terraform Validate
working-directory: infrastructure
run: terraform validate
- name: Terraform Plan
working-directory: infrastructure
run: terraform plan -no-color -out=tfplan
- name: Comment Plan on PR
uses: actions/github-script@v7
with:
script: |
const output = `#### Terraform Plan
\`\`\`
Plan output here
\`\`\`
`;
apply:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.0
- name: Terraform Init
working-directory: infrastructure
run: terraform init
- name: Terraform Apply
working-directory: infrastructure
run: terraform apply -auto-approve
8. AWS実践例 - VPC + EC2 + RDS + ALB
全体アーキテクチャ
この例では以下のインフラを構築します。
- VPC(パブリックサブネット2つ + プライベートサブネット2つ)
- Application Load Balancer (ALB)
- EC2インスタンス (Auto Scaling Group)
- RDS PostgreSQL (Multi-AZ)
- Security Groupチェーン
VPCとネットワーク
# vpc.tf
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.5.0"
name = "${local.name_prefix}-vpc"
cidr = "10.0.0.0/16"
azs = ["ap-northeast-2a", "ap-northeast-2c"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnets = ["10.0.11.0/24", "10.0.12.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
enable_dns_hostnames = true
public_subnet_tags = {
Tier = "public"
}
private_subnet_tags = {
Tier = "private"
}
tags = local.common_tags
}
Security Groups
# security_groups.tf
resource "aws_security_group" "alb" {
name_prefix = "${local.name_prefix}-alb-"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-alb-sg"
})
lifecycle {
create_before_destroy = true
}
}
resource "aws_security_group" "app" {
name_prefix = "${local.name_prefix}-app-"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-app-sg"
})
lifecycle {
create_before_destroy = true
}
}
resource "aws_security_group" "rds" {
name_prefix = "${local.name_prefix}-rds-"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-rds-sg"
})
lifecycle {
create_before_destroy = true
}
}
ALB
# alb.tf
resource "aws_lb" "main" {
name = "${local.name_prefix}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = module.vpc.public_subnets
tags = local.common_tags
}
resource "aws_lb_target_group" "app" {
name = "${local.name_prefix}-app-tg"
port = 8080
protocol = "HTTP"
vpc_id = module.vpc.vpc_id
health_check {
path = "/health"
healthy_threshold = 2
unhealthy_threshold = 3
timeout = 5
interval = 30
}
tags = local.common_tags
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
Auto Scaling Group
# asg.tf
resource "aws_launch_template" "app" {
name_prefix = "${local.name_prefix}-app-"
image_id = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.app.id]
user_data = base64encode(<<-SCRIPT
#!/bin/bash
yum update -y
yum install -y docker
systemctl start docker
systemctl enable docker
docker run -d -p 8080:8080 myapp:latest
SCRIPT
)
tag_specifications {
resource_type = "instance"
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-app"
})
}
}
resource "aws_autoscaling_group" "app" {
name = "${local.name_prefix}-app-asg"
desired_capacity = 2
max_size = 4
min_size = 1
target_group_arns = [aws_lb_target_group.app.arn]
vpc_zone_identifier = module.vpc.private_subnets
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
tag {
key = "Name"
value = "${local.name_prefix}-app"
propagate_at_launch = true
}
}
RDS
# rds.tf
resource "aws_db_subnet_group" "main" {
name = "${local.name_prefix}-db-subnet"
subnet_ids = module.vpc.private_subnets
tags = local.common_tags
}
resource "aws_db_instance" "main" {
identifier = "${local.name_prefix}-postgres"
engine = "postgres"
engine_version = "15.4"
instance_class = "db.t3.medium"
allocated_storage = 20
max_allocated_storage = 100
storage_encrypted = true
db_name = "myapp"
username = "admin"
password = var.db_password
multi_az = true
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
backup_retention_period = 7
skip_final_snapshot = false
final_snapshot_identifier = "${local.name_prefix}-final-snapshot"
tags = local.common_tags
}
9. セキュリティ
Sensitive変数
sensitiveとしてマークされた変数はplan/apply出力でマスキングされます。
variable "db_password" {
type = string
sensitive = true
}
output "db_endpoint" {
value = aws_db_instance.main.endpoint
}
output "db_password" {
value = var.db_password
sensitive = true
}
機密値の管理方法
- 環境変数:
TF_VAR_db_password環境変数を通じて注入します。 - terraform.tfvarsファイル:
.gitignoreに追加してバージョン管理から除外します。 - Vault連携: HashiCorp Vaultから動的にシークレットを取得します。
# VaultからDBパスワードを取得
data "vault_generic_secret" "db" {
path = "secret/data/production/database"
}
resource "aws_db_instance" "main" {
password = data.vault_generic_secret.db.data["password"]
# ...
}
- AWS Secrets Manager連携
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "production/database/password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
# ...
}
ポリシー検査 - OPA (Open Policy Agent)
OPAを使用してTerraform planにポリシーを強制できます。
# policy/terraform.rego
package terraform
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
not resource.change.after.server_side_encryption_configuration
msg := "S3バケットには必ず暗号化を設定する必要があります。"
}
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)に開放することはできません。"
}
# planをJSONで出力
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
# OPAでポリシー検査
opa eval --data policy/ --input tfplan.json "data.terraform.deny"
tfsec / Trivy 静的解析
# tfsecでセキュリティスキャン
tfsec .
# TrivyでIaCスキャン
trivy config .
10. ベストプラクティス
ディレクトリ構造
project/
environments/
dev/
main.tf
variables.tf
terraform.tfvars
backend.tf
staging/
main.tf
variables.tf
terraform.tfvars
backend.tf
prod/
main.tf
variables.tf
terraform.tfvars
backend.tf
modules/
vpc/
ec2/
rds/
alb/
global/
iam/
route53/
命名規則
- リソース名: snake_caseを使用 (
aws_security_group.web_server) - 変数名: snake_caseを使用 (
instance_type,db_password) - ファイル名: リソースタイプ別に分離 (
vpc.tf,ec2.tf,rds.tf) - タグ値: 環境-サービス-ロールパターン (
prod-myapp-web)
コードレビューチェックリスト
- セキュリティ: シークレットがハードコードされていないか?Security Groupが過度に開放されていないか?
- コスト: インスタンスタイプは適切か?未使用のリソースはないか?
- 可用性: Multi-AZが適用されているか?Auto Scalingが設定されているか?
- 状態管理: Stateキーは適切か?リモートバックエンドが設定されているか?
- モジュール化: 重複コードをモジュールに抽出できるか?
- タグ付け: すべてのリソースに必須タグがあるか?
よくある間違いと解決策
| 間違い | 解決策 |
|---|---|
| StateファイルをGitにコミット | .gitignoreに追加、リモートバックエンド使用 |
| シークレットのハードコード | sensitive変数、Vault、Secrets Manager使用 |
| countで作成したリソースの途中項目削除 | for_eachを使用 |
| プロバイダバージョン未固定 | required_providersにバージョン明示 |
| plan無しで直接apply | CI/CDでplanレビューを必須化 |
| モジュール無しですべてのコードを1ファイルに記述 | モジュールに分離、関心事別にファイル分離 |
まとめ
TerraformとIaCは現代のクラウドインフラ管理の基盤です。コードでインフラを定義することで、再現可能で、変更追跡が可能で、コードレビューを通じて品質を高めることができます。核心原則をまとめると以下の通りです。
- 宣言的コードでインフラを定義し、Gitでバージョン管理します。
- リモートバックエンドでStateを安全に管理し、ロックで同時変更を防止します。
- モジュールでコードを再利用し、チーム標準を確立します。
- CI/CDパイプラインでplanレビューと自動デプロイを実現します。
- ポリシー検査でセキュリティとコンプライアンスを自動化します。
このガイドの例をベースに、自分のプロジェクトに合わせて修正して活用してください。
현재 단락 (1/791)
従来のインフラ管理方式は、管理者がコンソールにアクセスして手動でサーバーをプロビジョニングし、ネットワークを設定するものでした。この方式にはいくつかの問題があります。