演習:フルスタックIaCを構築しよう
ストーリー
「いよいよ本格的なインフラをTerraformで構築する演習だ」
木村先輩がアーキテクチャ図を広げた。
「VPC、EC2、RDS、S3 の全てをTerraformで管理する。 さらにGitHub Actionsと連携して、PRベースでインフラ変更を管理する。 これが完成すれば、インフラのコード化は一通りマスターしたことになる」
演習の概要
Webアプリケーション用のAWSインフラ全体をTerraformでコード化してください。
完成形のアーキテクチャ
Internet
│
┌─────┴─────┐
│ IGW │
└─────┬─────┘
│
┌──── VPC (10.0.0.0/16) ──────────────┐
│ │
│ ┌─ Public Subnet ─┐ ┌─ Public ─┐ │
│ │ 10.0.1.0/24 │ │ 10.0.2.0 │ │
│ │ [EC2 Web-1] │ │ [EC2 Web-2]│ │
│ └─────────────────┘ └───────────┘ │
│ │ │ │
│ ┌────┴────┐ │ │
│ │ NAT │ │ │
│ └────┬────┘ │ │
│ │ │ │
│ ┌─ Private Subnet ┐ ┌─ Private ─┐ │
│ │ 10.0.101.0/24 │ │ 10.0.102.0 │ │
│ │ [RDS Primary] │ │ [RDS Standby]│ │
│ └─────────────────┘ └────────────┘ │
│ │
└───────────────────────────────────────┘
┌──────────────────────┐
│ S3: Assets Bucket │
│ S3: Backup Bucket │
└──────────────────────┘
課題1: プロジェクト構造の作成
以下のディレクトリ構造を作成してください。
infrastructure/
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── ec2/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── rds/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── environments/
│ └── staging/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── backend.tf
│ └── terraform.tfvars
└── .github/
└── workflows/
└── terraform.yml
<details>
<summary>解答例(自分で実装してから確認しよう)</summary>
bash
# ディレクトリ作成
mkdir -p infrastructure/modules/{vpc,ec2,rds}
mkdir -p infrastructure/environments/staging
mkdir -p .github/workflows
# 各ファイルを作成
touch infrastructure/modules/vpc/{main.tf,variables.tf,outputs.tf}
touch infrastructure/modules/ec2/{main.tf,variables.tf,outputs.tf}
touch infrastructure/modules/rds/{main.tf,variables.tf,outputs.tf}
touch infrastructure/environments/staging/{main.tf,variables.tf,outputs.tf,backend.tf,terraform.tfvars}
touch .github/workflows/terraform.yml課題2: VPCモジュールの実装
VPC、サブネット(パブリック2つ、プライベート2つ)、IGW、NAT Gateway、ルートテーブル、セキュリティグループを含むVPCモジュールを実装してください。
要件
- VPC CIDR: 変数で指定可能
- パブリックサブネット: 2つ(異なるAZ)
- プライベートサブネット: 2つ(異なるAZ)
- Internet Gateway + パブリックルートテーブル
- NAT Gateway + プライベートルートテーブル(オプション)
- Web用・DB用のセキュリティグループ
hcl
# modules/vpc/variables.tf
variable "name" {
type = string
}
variable "cidr_block" {
type = string
default = "10.0.0.0/16"
}
variable "public_subnets" {
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
variable "private_subnets" {
type = list(string)
default = ["10.0.101.0/24", "10.0.102.0/24"]
}
variable "enable_nat_gateway" {
type = bool
default = true
}
variable "tags" {
type = map(string)
default = {}
}hcl
# modules/vpc/main.tf
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(var.tags, { Name = var.name })
}
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = merge(var.tags, { Name = "${var.name}-igw" })
}
resource "aws_subnet" "public" {
count = length(var.public_subnets)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnets[count.index]
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = merge(var.tags, { Name = "${var.name}-public-${count.index + 1}" })
}
resource "aws_subnet" "private" {
count = length(var.private_subnets)
vpc_id = aws_vpc.this.id
cidr_block = var.private_subnets[count.index]
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = merge(var.tags, { Name = "${var.name}-private-${count.index + 1}" })
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
tags = merge(var.tags, { Name = "${var.name}-public-rt" })
}
resource "aws_route_table_association" "public" {
count = length(var.public_subnets)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? 1 : 0
domain = "vpc"
tags = merge(var.tags, { Name = "${var.name}-nat-eip" })
}
resource "aws_nat_gateway" "this" {
count = var.enable_nat_gateway ? 1 : 0
allocation_id = aws_eip.nat[0].id
subnet_id = aws_subnet.public[0].id
tags = merge(var.tags, { Name = "${var.name}-nat" })
depends_on = [aws_internet_gateway.this]
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.this.id
dynamic "route" {
for_each = var.enable_nat_gateway ? [1] : []
content {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.this[0].id
}
}
tags = merge(var.tags, { Name = "${var.name}-private-rt" })
}
resource "aws_route_table_association" "private" {
count = length(var.private_subnets)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private.id
}
resource "aws_security_group" "web" {
name = "${var.name}-web-sg"
vpc_id = aws_vpc.this.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(var.tags, { Name = "${var.name}-web-sg" })
}
resource "aws_security_group" "db" {
name = "${var.name}-db-sg"
vpc_id = aws_vpc.this.id
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(var.tags, { Name = "${var.name}-db-sg" })
}hcl
# modules/vpc/outputs.tf
output "vpc_id" { value = aws_vpc.this.id }
output "public_subnet_ids" { value = aws_subnet.public[*].id }
output "private_subnet_ids" { value = aws_subnet.private[*].id }
output "web_security_group_id" { value = aws_security_group.web.id }
output "db_security_group_id" { value = aws_security_group.db.id }課題3: 環境定義の作成
staging環境の main.tf でモジュールを組み立ててください。
要件
- VPCモジュール、EC2モジュール、RDSモジュールを呼び出す
- S3バケット(アセット用、バックアップ用)を直接定義
- 全リソースに共通タグを設定
hcl
# environments/staging/main.tf
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "terraform"
}
}
}
locals {
name_prefix = "${var.project_name}-${var.environment}"
}
module "vpc" {
source = "../../modules/vpc"
name = local.name_prefix
cidr_block = var.vpc_cidr
public_subnets = var.public_subnet_cidrs
private_subnets = var.private_subnet_cidrs
enable_nat_gateway = true
}
module "web" {
source = "../../modules/ec2"
name_prefix = "${local.name_prefix}-web"
instance_type = var.web_instance_type
instance_count = var.web_instance_count
subnet_ids = module.vpc.public_subnet_ids
security_group_ids = [module.vpc.web_security_group_id]
root_volume_size = 20
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>${var.project_name} - ${var.environment}</h1>" > /var/www/html/index.html
EOF
}
module "database" {
source = "../../modules/rds"
name_prefix = local.name_prefix
environment = var.environment
engine = "mysql"
engine_version = "8.0"
instance_class = var.db_instance_class
allocated_storage = 20
database_name = "app"
master_username = "admin"
master_password = var.db_password
multi_az = false
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.vpc.db_security_group_id]
deletion_protection = false
}
resource "aws_s3_bucket" "assets" {
bucket = "${local.name_prefix}-assets"
}
resource "aws_s3_bucket_server_side_encryption_configuration" "assets" {
bucket = aws_s3_bucket.assets.id
rule {
apply_server_side_encryption_by_default { sse_algorithm = "AES256" }
}
}
resource "aws_s3_bucket_public_access_block" "assets" {
bucket = aws_s3_bucket.assets.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket" "backups" {
bucket = "${local.name_prefix}-backups"
}
resource "aws_s3_bucket_versioning" "backups" {
bucket = aws_s3_bucket.backups.id
versioning_configuration { status = "Enabled" }
}
resource "aws_s3_bucket_server_side_encryption_configuration" "backups" {
bucket = aws_s3_bucket.backups.id
rule {
apply_server_side_encryption_by_default { sse_algorithm = "AES256" }
}
}課題4: CI/CDワークフローの作成
Terraform用のGitHub Actionsワークフローを作成してください。
要件
- PR時: fmt check → validate → plan → PRコメント
- mainマージ後: plan → apply
yaml
# .github/workflows/terraform.yml
name: Terraform
on:
pull_request:
branches: [main]
paths: ['infrastructure/**']
push:
branches: [main]
paths: ['infrastructure/**']
permissions:
contents: read
pull-requests: write
id-token: write
env:
TF_VERSION: '1.7.0'
WORKING_DIR: 'infrastructure/environments/staging'
jobs:
plan:
name: Terraform Plan
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-northeast-1
- run: terraform fmt -check -recursive
working-directory: ${{ env.WORKING_DIR }}
- run: terraform init
working-directory: ${{ env.WORKING_DIR }}
- run: terraform validate
working-directory: ${{ env.WORKING_DIR }}
- id: plan
run: terraform plan -no-color
working-directory: ${{ env.WORKING_DIR }}
continue-on-error: true
- uses: actions/github-script@v7
with:
script: |
const body = `### Terraform Plan
\`\`\`
${{ steps.plan.outputs.stdout }}
\`\`\``;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
})
apply:
name: Terraform Apply
runs-on: ubuntu-latest
if: github.event_name == 'push'
environment: staging
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-northeast-1
- run: terraform init
working-directory: ${{ env.WORKING_DIR }}
- run: terraform apply -auto-approve
working-directory: ${{ env.WORKING_DIR }}達成度チェック
| 課題 | 内容 | 完了 |
|---|---|---|
| 課題1 | プロジェクト構造の作成 | [ ] |
| 課題2 | VPCモジュールの実装 | [ ] |
| 課題3 | 環境定義の作成 | [ ] |
| 課題4 | CI/CDワークフローの作成 | [ ] |
まとめ
| ポイント | 内容 |
|---|---|
| モジュール化 | VPC/EC2/RDSを再利用可能なモジュールに |
| 環境分離 | environments/staging で環境固有の設定 |
| セキュリティ | 暗号化、パブリックアクセスブロック、最小権限SG |
| CI/CD | PR時にplan、マージ後にapply |
チェックリスト
- モジュール化されたTerraformプロジェクトを構築できた
- VPC全体をコードで定義できた
- 環境定義でモジュールを組み合わせて使えた
- Terraform用のCI/CDワークフローを作成できた
次のステップへ
演習が完了したら、Step 5 のチェックポイントクイズに挑戦しましょう。
推定所要時間: 120分