EXERCISE 120分

演習:フルスタック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
</details>

課題2: VPCモジュールの実装

VPC、サブネット(パブリック2つ、プライベート2つ)、IGW、NAT Gateway、ルートテーブル、セキュリティグループを含むVPCモジュールを実装してください。

要件

  • VPC CIDR: 変数で指定可能
  • パブリックサブネット: 2つ(異なるAZ)
  • プライベートサブネット: 2つ(異なるAZ)
  • Internet Gateway + パブリックルートテーブル
  • NAT Gateway + プライベートルートテーブル(オプション)
  • Web用・DB用のセキュリティグループ
<details> <summary>解答例(自分で実装してから確認しよう)</summary>
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 }
</details>

課題3: 環境定義の作成

staging環境の main.tf でモジュールを組み立ててください。

要件

  • VPCモジュール、EC2モジュール、RDSモジュールを呼び出す
  • S3バケット(アセット用、バックアップ用)を直接定義
  • 全リソースに共通タグを設定
<details> <summary>解答例(自分で実装してから確認しよう)</summary>
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" }
  }
}
</details>

課題4: CI/CDワークフローの作成

Terraform用のGitHub Actionsワークフローを作成してください。

要件

  • PR時: fmt check → validate → plan → PRコメント
  • mainマージ後: plan → apply
<details> <summary>解答例(自分で実装してから確認しよう)</summary>
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 }}
</details>

達成度チェック

課題内容完了
課題1プロジェクト構造の作成[ ]
課題2VPCモジュールの実装[ ]
課題3環境定義の作成[ ]
課題4CI/CDワークフローの作成[ ]

まとめ

ポイント内容
モジュール化VPC/EC2/RDSを再利用可能なモジュールに
環境分離environments/staging で環境固有の設定
セキュリティ暗号化、パブリックアクセスブロック、最小権限SG
CI/CDPR時にplan、マージ後にapply

チェックリスト

  • モジュール化されたTerraformプロジェクトを構築できた
  • VPC全体をコードで定義できた
  • 環境定義でモジュールを組み合わせて使えた
  • Terraform用のCI/CDワークフローを作成できた

次のステップへ

演習が完了したら、Step 5 のチェックポイントクイズに挑戦しましょう。


推定所要時間: 120分