0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【IaC超入門】30日でAWS CloudFormationとTerraformをマスターするロードマップ - 19日目: TerraformでVPCとサブネットを作成する

Posted at

TerraformでVPCとサブネットを作成する

はじめに

前回は、Terraformの基本的なコマンドとワークフローを学びました。今回は、より実践的なインフラ構築に挑戦します。VPC(Virtual Private Cloud)と複数のサブネットをTerraformで作成し、AWS上でセキュアなネットワーク基盤を構築する方法を解説します。

VPCとサブネットの概念

VPC(Virtual Private Cloud)

VPCは、AWS クラウド内の論理的に隔離された仮想ネットワーク環境です。自社専用のデータセンターをクラウド上に構築するようなもので、以下の特徴があります:

  • ネットワーク分離: 他のAWSアカウントから完全に隔離
  • IPアドレス制御: CIDR ブロックで IP アドレス範囲を定義
  • セキュリティ制御: セキュリティグループとNACLで通信を制御

サブネット

サブネットは、VPC内をさらに細分化したネットワークセグメントです:

サブネット種類 特徴 用途 インターネット接続
パブリック インターネットゲートウェイへのルート Webサーバー、ロードバランサー 直接接続可能
プライベート インターネットゲートウェイへの直接ルートなし データベース、アプリケーション NATゲートウェイ経由

設計方針:高可用性を考慮したVPC構成

今回は、本格的な運用を想定して以下の構成を作成します:

VPC (10.0.0.0/16)
├── パブリックサブネット
│   ├── ap-northeast-1a (10.0.1.0/24)
│   └── ap-northeast-1c (10.0.2.0/24)
└── プライベートサブネット
    ├── ap-northeast-1a (10.0.11.0/24)
    └── ap-northeast-1c (10.0.12.0/24)

ステップ1:プロジェクト構成とファイル作成

ディレクトリ構成

vpc-terraform-project/
├── main.tf           # プロバイダー設定
├── vpc.tf            # VPC関連リソース
├── subnets.tf        # サブネット定義
├── gateways.tf       # ゲートウェイとルート
├── variables.tf      # 変数定義
├── outputs.tf        # 出力値
└── terraform.tfvars  # 変数値

1.1 基本設定(main.tf)

# main.tf
terraform {
  required_version = ">= 1.0"
  
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "terraform"
      CreatedAt   = timestamp()
    }
  }
}

1.2 変数定義(variables.tf)

# variables.tf
variable "aws_region" {
  description = "AWSリージョン"
  type        = string
  default     = "ap-northeast-1"
}

variable "project_name" {
  description = "プロジェクト名"
  type        = string
  default     = "terraform-vpc-demo"
}

variable "environment" {
  description = "環境名"
  type        = string
  default     = "dev"
  
  validation {
    condition = contains(["dev", "staging", "prod"], var.environment)
    error_message = "環境名は dev, staging, 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 = "有効なCIDR形式を入力してください。"
  }
}

variable "availability_zones" {
  description = "使用するアベイラビリティゾーン"
  type        = list(string)
  default     = ["ap-northeast-1a", "ap-northeast-1c"]
}

variable "public_subnet_cidrs" {
  description = "パブリックサブネットのCIDRブロック"
  type        = list(string)
  default     = ["10.0.1.0/24", "10.0.2.0/24"]
}

variable "private_subnet_cidrs" {
  description = "プライベートサブネットのCIDRブロック"
  type        = list(string)
  default     = ["10.0.11.0/24", "10.0.12.0/24"]
}

1.3 VPC定義(vpc.tf)

# vpc.tf
# VPCの作成
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${var.project_name}-${var.environment}-vpc"
  }
}

# データソース:利用可能なAZ
data "aws_availability_zones" "available" {
  state = "available"
}

# VPCフローログ(ネットワークトラフィックの監視)
resource "aws_flow_log" "vpc_flow_log" {
  iam_role_arn    = aws_iam_role.flow_log_role.arn
  log_destination = aws_cloudwatch_log_group.vpc_log_group.arn
  traffic_type    = "ALL"
  vpc_id          = aws_vpc.main.id
}

# CloudWatch Log Group(フローログ用)
resource "aws_cloudwatch_log_group" "vpc_log_group" {
  name              = "/aws/vpc/${var.project_name}-${var.environment}-flowlogs"
  retention_in_days = 30

  tags = {
    Name = "${var.project_name}-${var.environment}-vpc-logs"
  }
}

# IAMロール(フローログ用)
resource "aws_iam_role" "flow_log_role" {
  name = "${var.project_name}-${var.environment}-flow-log-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "vpc-flow-logs.amazonaws.com"
        }
      }
    ]
  })
}

# IAMロールポリシー(フローログ用)
resource "aws_iam_role_policy" "flow_log_policy" {
  name = "${var.project_name}-${var.environment}-flow-log-policy"
  role = aws_iam_role.flow_log_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents",
          "logs:DescribeLogGroups",
          "logs:DescribeLogStreams"
        ]
        Effect   = "Allow"
        Resource = "*"
      }
    ]
  })
}

1.4 サブネット定義(subnets.tf)

# subnets.tf
# パブリックサブネット
resource "aws_subnet" "public" {
  count = length(var.public_subnet_cidrs)

  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidrs[count.index]
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-${var.environment}-public-subnet-${count.index + 1}"
    Type = "Public"
    Tier = "Web"
  }
}

# プライベートサブネット
resource "aws_subnet" "private" {
  count = length(var.private_subnet_cidrs)

  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = {
    Name = "${var.project_name}-${var.environment}-private-subnet-${count.index + 1}"
    Type = "Private"
    Tier = "Application"
  }
}

# データベース用サブネット(オプション)
resource "aws_subnet" "database" {
  count = length(var.availability_zones)

  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, 20 + count.index)
  availability_zone = var.availability_zones[count.index]

  tags = {
    Name = "${var.project_name}-${var.environment}-db-subnet-${count.index + 1}"
    Type = "Database"
    Tier = "Database"
  }
}

# データベースサブネットグループ
resource "aws_db_subnet_group" "main" {
  name       = "${var.project_name}-${var.environment}-db-subnet-group"
  subnet_ids = aws_subnet.database[*].id

  tags = {
    Name = "${var.project_name}-${var.environment}-db-subnet-group"
  }
}

1.5 ゲートウェイとルーティング(gateways.tf)

# gateways.tf
# インターネットゲートウェイ
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.project_name}-${var.environment}-igw"
  }
}

# Elastic IP(NAT Gateway用)
resource "aws_eip" "nat_gateway" {
  count = length(aws_subnet.public)

  domain     = "vpc"
  depends_on = [aws_internet_gateway.main]

  tags = {
    Name = "${var.project_name}-${var.environment}-nat-eip-${count.index + 1}"
  }
}

# NAT Gateway
resource "aws_nat_gateway" "main" {
  count = length(aws_subnet.public)

  allocation_id = aws_eip.nat_gateway[count.index].id
  subnet_id     = aws_subnet.public[count.index].id
  depends_on    = [aws_internet_gateway.main]

  tags = {
    Name = "${var.project_name}-${var.environment}-nat-gw-${count.index + 1}"
  }
}

# パブリックルートテーブル
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_name}-${var.environment}-public-rt"
  }
}

# プライベートルートテーブル
resource "aws_route_table" "private" {
  count = length(aws_subnet.private)

  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main[count.index].id
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-private-rt-${count.index + 1}"
  }
}

# データベースルートテーブル
resource "aws_route_table" "database" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.project_name}-${var.environment}-db-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
}

# ルートテーブル関連付け:プライベート
resource "aws_route_table_association" "private" {
  count = length(aws_subnet.private)

  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}

# ルートテーブル関連付け:データベース
resource "aws_route_table_association" "database" {
  count = length(aws_subnet.database)

  subnet_id      = aws_subnet.database[count.index].id
  route_table_id = aws_route_table.database.id
}

1.6 出力定義(outputs.tf)

# outputs.tf
output "vpc_id" {
  description = "VPCのID"
  value       = aws_vpc.main.id
}

output "vpc_cidr_block" {
  description = "VPCのCIDRブロック"
  value       = aws_vpc.main.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 "internet_gateway_id" {
  description = "インターネットゲートウェイのID"
  value       = aws_internet_gateway.main.id
}

output "nat_gateway_ids" {
  description = "NATゲートウェイのIDリスト"
  value       = aws_nat_gateway.main[*].id
}

output "db_subnet_group_name" {
  description = "データベースサブネットグループ名"
  value       = aws_db_subnet_group.main.name
}

# ネットワーク設定の概要
output "network_summary" {
  description = "ネットワーク設定の概要"
  value = {
    vpc_id             = aws_vpc.main.id
    availability_zones = var.availability_zones
    public_subnets = {
      for i, subnet in aws_subnet.public :
      subnet.availability_zone => {
        id         = subnet.id
        cidr_block = subnet.cidr_block
      }
    }
    private_subnets = {
      for i, subnet in aws_subnet.private :
      subnet.availability_zone => {
        id         = subnet.id
        cidr_block = subnet.cidr_block
      }
    }
  }
}

ステップ2:設定ファイルの作成(terraform.tfvars)

# terraform.tfvars
aws_region               = "ap-northeast-1"
project_name             = "my-webapp"
environment              = "dev"
vpc_cidr                 = "10.0.0.0/16"
availability_zones       = ["ap-northeast-1a", "ap-northeast-1c"]
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"]

ステップ3:デプロイ手順

3.1 初期化と検証

# プロジェクトディレクトリに移動
cd vpc-terraform-project

# 初期化
terraform init

# 構文チェック
terraform validate

# フォーマットチェック
terraform fmt -check

3.2 実行計画の確認

# 実行計画の生成
terraform plan -var-file="terraform.tfvars" -out=vpc.tfplan

# 計画の詳細確認
terraform show vpc.tfplan

3.3 デプロイの実行

# 計画の適用
terraform apply vpc.tfplan

# 結果の確認
terraform output

ステップ4:作成されたリソースの確認

AWSコンソールでの確認

  1. VPCダッシュボード: 作成されたVPCと設定を確認
  2. サブネット: 各サブネットの配置とCIDRを確認
  3. ルートテーブル: ルーティング設定を確認
  4. インターネットゲートウェイ: インターネット接続設定を確認
  5. NATゲートウェイ: プライベートサブネット用の設定を確認

CLI での確認

# VPC情報の確認
aws ec2 describe-vpcs --vpc-ids $(terraform output -raw vpc_id)

# サブネット情報の確認
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$(terraform output -raw vpc_id)"

# ルートテーブル情報の確認
aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$(terraform output -raw vpc_id)"

ステップ5:リソースの理解

依存関係の解析

Terraformは以下の順序でリソースを作成します:

  1. VPC → 基盤となるネットワーク
  2. サブネット → VPCに依存
  3. インターネットゲートウェイ → VPCに依存
  4. Elastic IP → インターネットゲートウェイに依存
  5. NATゲートウェイ → サブネットとElastic IPに依存
  6. ルートテーブル → VPC、ゲートウェイに依存

グラフ可視化

依存関係をグラフで確認できます:

terraform graph | dot -Tpng > dependency-graph.png

セキュリティ考慮事項

実装したセキュリティ対策

  1. VPCフローログ: ネットワークトラフィックの監視
  2. プライベートサブネット: データベースの直接アクセス防止
  3. NATゲートウェイ: プライベートリソースの安全なインターネットアクセス
  4. 最小権限の原則: IAMロールは必要最小限の権限のみ

追加推奨事項

# セキュリティグループの例(別ファイル security_groups.tf)
resource "aws_security_group" "web" {
  name        = "${var.project_name}-${var.environment}-web-sg"
  description = "Web層用のセキュリティグループ"
  vpc_id      = aws_vpc.main.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 = {
    Name = "${var.project_name}-${var.environment}-web-sg"
  }
}

トラブルシューティング

よくある問題と解決法

問題 原因 解決法
CIDRの重複 サブネットCIDRがVPCの範囲外 cidrsubnet関数で動的計算
AZ不足 指定したAZが存在しない data.aws_availability_zonesで動的取得
依存関係エラー リソース作成順序の問題 depends_onで明示的に依存関係を指定

パフォーマンスとコスト最適化

コスト考慮事項

  • NATゲートウェイ: 高可用性が不要な開発環境では1つに削減可能
  • フローログ: 必要に応じて無効化してCloudWatchコストを削減
  • Elastic IP: 使用していないEIPは料金が発生するため注意
# 開発環境用の条件付きリソース
resource "aws_nat_gateway" "main" {
  count = var.environment == "prod" ? length(aws_subnet.public) : 1
  
  # ... 他の設定
}

まとめ

今回構築したVPC環境の特徴:

主要な改善点

  1. 高可用性: 複数AZでの冗長化
  2. セキュリティ: 層別セキュリティの実装
  3. 運用性: フローログによる監視機能
  4. スケーラビリティ: 変数による柔軟な設定
  5. 保守性: ファイル分割による管理しやすさ

学習のポイント

  • リソース間の参照: aws_vpc.main.idによる依存関係の表現
  • 配列とループ: countを使った複数リソースの効率的な作成
  • 関数の活用: cidrsubnetなどの組み込み関数の利用
  • 条件分岐: 環境に応じたリソース作成の制御

この基盤があれば、EC2インスタンス、ロードバランサー、データベースなどのアプリケーションリソースを安全に配置できます。

次回は、この VPC 上にセキュリティグループとEC2インスタンスをデプロイし、より実践的なWebアプリケーション環境を構築します。お楽しみに!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?