0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Terraform で VPC,EC2,RDS を構築してみた

Last updated at Posted at 2025-07-12

背景

コード化することで、同じ環境を正確に、何度でも再現できるようにするため。

インフラの構成をコードとして可視化することで、レビューや管理を容易にするため。

今回の取り組みで意識した点

・コスト削減のため、Nat ゲートウェイを配置せずに EC2 をパブリックサブネットに配置した。
・RDS をマルチ AZ として、可用性を高めるようにした。

ベストプラクティスは EC2 をプライベートサブネットに配置し、外部からアクセスできないようにしてセキュリティを高めます。

高可用性を実現するためには、マルチ AZ にします。
マルチ AZ にすると 2 つのアベイラビリティゾーンに展開され、片方が何らかの影響でダウンした場合、もう片方にフェイルオーバーされます

コスト比較(1ヶ月単位)

外部との通信
・Nat ゲートウェイ:固定 45 ドル+通信 0.005 ドル/GB
・EC2 単体:0.005 ドル/時間

データベース
RDS Multi-AZ:Single-AZ の約 2 倍(高可用性のトレードオフ)

要件整理

ネットワーク構成

項目
VPC CIDR 10.0.0.0/16
Internet Gateway 配置済み
ルートテーブル (0.0.0.0/0) Internet Gateway

サブネット

Availability Zone パブリックサブネット CIDR プライベートサブネット CIDR
1a 10.0.1.0/24 10.0.10.0/24
1c 10.0.2.0/24 10.0.20.0/24

アプリケーション構成

リソース 配置先 設定/詳細
ALB パブリックサブネット
EC2 パブリックサブネット - インスタンスタイプ:t3.micro
- AMI:無料枠のもの
- アクセス:SSM セッションマネージャー
- セキュリティグループ:インバウンドは ALB からの HTTP 通信のみ許可
Auto Scaling Group パブリックサブネット - 最小:1 台
- 最大:3 台
RDS プライベートサブネット - Multi AZ
- アクセス許可:EC2 の SG のみ

全体の構成図

今回作る構成は、コスト最適化したもので以下のようになります。
構成図(コスト最適化)

ベストプラクティスの構成は以下のようになります。
構成図(ベストプラクティス)

Terraform 構成

以下のフォルダ構成としました。

├── backend.tf
├── bootstrap
│   ├── remote-state.tf
│   ├── terraform.tfstate
│   └── terraform.tfstate.backup
├── envs
│   └── dev.tfvars #projectとdb_passwordをここで指定
├── main.tf
├── modules
│   ├── alb
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── compute
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── userdata.sh
│   ├── network
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── rds
│       ├── main.tf
│       └── variables.tf
├── terraform.tfstate.d
│   └── dev
│       ├── terraform.tfstate
│       └── terraform.tfstate.backup
└── variables.tf

作成する手順

  1. リモートステート用の S3 バケットと DynamoDB を作成する。
  2. VPC、サブネット、インターネットゲートウェイ、ルートテーブルを作成する。
  3. 起動テンプレート、オートスケーリンググループ、ウェブサーバー用のセキュリティグループを作成する。
  4. ALB を作成する。
  5. RDS を作成する。
  6. ルートの main.tf で呼び出す

1. リモートステート用の S3 バケットと DynamoDB を作成する。

まずはじめに、Terraform の構成情報をチームで安全に共有するための「リモートステート」環境を構築します。具
体的には、tfstate ファイルを保管する S3 バケットと、複数人での同時実行による意図しない上書きを防ぐ(ロックするための)DynamoDB テーブルを作成します。

 bootstrap
├── remote-state.tf
├── terraform.tfstate
└── terraform.tfstate.backup

プロジェクトのルートに bootstrap ディレクトリを作成します。
bootstrap ディレクトリの中に remote-state.tf をファイルを作成します。
以下 remote-state.tf の中身です。

remote-state.tf
provider "aws" {
  region = "ap-northeast-1"
}

# S3バケット
resource "aws_s3_bucket" "state" {
  bucket = "vpc-ec2-rds-statefile-〇〇〇〇" #リモートステート用のS3バケットの名前
}

# S3バケットのバージョニング設定
resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.state.id
  versioning_configuration {
    status = "Enabled"
  }
}

# S3バケットの暗号化設定
resource "aws_s3_bucket_server_side_encryption_configuration" "sse" {
  bucket = aws_s3_bucket.state.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# DynamoDBのテーブルを作成
resource "aws_dynamodb_table" "lock" {
  name         = "vpc-ec2-rds-statelock-〇〇〇〇" #リモートステート用のテーブル名
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"
  attribute {
    name = "LockID"
    type = "S" #Stringの"S"
  }
}

# S3バケット名の出力
output "aws_s3_bucket_state" {
  value = aws_s3_bucket.state.id
}

# DynamoDBテーブル名の出力
output "aws_dynamodb_table_lock" {
  value = aws_dynamodb_table.lock.arn
}

以下をターミナルで実行し、S3 バケットと DynamoDB を作成します

cd ./bootstrap

terraform init
terraform plan
terraform apply

S3 バケットと DynamoDB テーブルを作成できたら、Terraform にその場所を教えるため、プロジェクトルートの backend.tf に以下の設定を記述します。

root/backend.tf
terraform {
  backend "s3" {
    bucket               = "〇〇〇〇" #作成したリモートステート用のS3バケット名
    key                  = "terraform.tfstate"
    workspace_key_prefix = "envs"
    region               = "ap-northeast-1" #東京リージョン
    dynamodb_table       = "〇〇〇〇" #作成したリモートステート(ロック)用のテーブル名
    encrypt              = true
  }
}

2.VPC を作成する。

次に、AWS 上にプライベートなネットワーク空間を構築します。VPC、サブネット、インターネットゲートウェイなど、すべてのインフラの基礎となるネットワーク関連のリソースを network モジュールとしてまとめていきます。

modules
   └── network
        ├── main.tf
        ├── outputs.tf
        └── variables.tf
modules/network/main.tf
##Availability_zoneの取得
data "aws_availability_zones" "available" {
  state = "available"
}

#################################
#vpc
resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr_block
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${var.project}-vpc"
  }
}

#################################

#Internet Gateway

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.this.id

  tags = {
    Name = "${var.project}-igw"
  }
}

#################################

#Public Subnets
resource "aws_subnet" "public" {
  for_each                = { for idx, cidr in var.public_subnet_cidrs : idx => cidr }
  vpc_id                  = aws_vpc.this.id
  cidr_block              = each.value
  availability_zone       = data.aws_availability_zones.available.names[each.key]
  map_public_ip_on_launch = true
  tags = {
    Name = "${var.project}-public-${each.value}"
  }
}

#################################

#Route Table
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.this.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }
}

resource "aws_route_table_association" "public" {
  route_table_id = aws_route_table.public.id
  for_each       = aws_subnet.public
  subnet_id      = each.value.id
}

################################

#Private Subnets
resource "aws_subnet" "private" {
  for_each          = { for idx, cidr in var.private_subnet_cidrs : idx => cidr }
  vpc_id            = aws_vpc.this.id
  cidr_block        = each.value
  availability_zone = data.aws_availability_zones.available.names[each.key]
  tags = {
    Name = "${var.project}-private-${each.value}"
  }
}

modules/network/variables.tf
variable "vpc_cidr_block" {
  type        = string
  description = "vpcのcidrブロック"
}

variable "public_subnet_cidrs" {
  type        = list(string)
  description = "パブリックサブネットのcidrリスト"
}

variable "private_subnet_cidrs" {
  type        = list(string)
  description = "プライベートサブネットのcidrリスト"
}

variable "project" {
  type        = string
  description = "リソースTagの名前"
}

modules/network/outputs.tf
#VPC_IDの出力
output "vpc_id" {
  value = aws_vpc.this.id
}

###############################################

#パブリックサブネットの出力
output "public_subnet_ids" {
  value = [for s in aws_subnet.public : s.id]
}

###############################################

#プライベートサブネットの出力
output "private_subnet_ids" {
  value = [for s in aws_subnet.private : s.id]
}

outputs.tf は、他のリソースで参照するために必要になります。
例)modules/outputs.tf →  ルートの main.tf(呼び出し) →  他の modules で参照

3. 起動テンプレート、オートスケーリンググループを作成する。

ウェブサーバーとなる EC2 インスタンスを定義します。ここでは、どのような EC2 を起動するかの設計図である「起動テンプレート」と、負荷に応じてインスタンス数を自動で増減させる「Auto ScalingGroup」を作成します。また、セキュアなアクセスを実現するための IAM ロールやセキュリティグループもこの compute モジュールで一括して設定します。

modules
   └── compute
        ├── main.tf
        ├── outputs.tf
        ├── variables.tf
        └── userdata.sh
modules/compute/main.tf
##############################
#IAMロール
resource "aws_iam_role" "ssm" {
  name = "${var.project}-ssm-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect = "Allow",
      Principal = {
        Service = ["ec2.amazonaws.com"]
      },
      Action = ["sts:AssumeRole"]
    }]

  })
}

resource "aws_iam_role_policy_attachment" "ssm_core" {
  role       = aws_iam_role.ssm.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "ssm" {
  name = "${var.project}-ssm-profile"
  role = aws_iam_role.ssm.name
}


##############################

#セキュリティグループ
resource "aws_security_group" "web" {
  name        = "${var.project}-web"
  vpc_id      = var.vpc_id
  description = "Allow HTTP ALB"


  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [var.alb_sg_id] #ALBからのアクセスを許可
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }


}


##################################################

#起動テンプレート
resource "aws_launch_template" "web" {
  name_prefix            = "${var.project}-lt-"
  image_id               = var.ami_id
  instance_type          = var.instance_type
  vpc_security_group_ids = [aws_security_group.web.id]

  iam_instance_profile {
    name = aws_iam_instance_profile.ssm.name
  }

  monitoring {
    enabled = true
  }

  user_data = base64encode(file("${path.module}/userdata.sh"))
}



##################################################

#AutoScalingGroup
resource "aws_autoscaling_group" "web" {
  name             = "${var.project}-asg"
  desired_capacity = 1
  max_size         = 3
  min_size         = 1
  launch_template {
    id      = aws_launch_template.web.id
    version = "$Latest"
  }

  vpc_zone_identifier = var.public_subnet_ids
  target_group_arns   = [var.alb_target_group_arn]

  health_check_type         = "ELB"
  health_check_grace_period = 300


  tag {
    key                 = "Name"
    value               = "${var.project}-web"
    propagate_at_launch = true
  }
}

modules/compute/variables.tf
variable "instance_type" {
  type        = string
  description = "ec2インスタンスのタイプ"
}

variable "ami_id" {
  type        = string
  description = "AMIのID"
}

variable "vpc_id" {
  type        = string
  description = "VPCのID"
}

variable "public_subnet_ids" {
  type        = list(string)
  description = "パブリックサブネットのCIDRリスト"
}

variable "project" {
  type        = string
  description = "プロジェクトの名前"
}

variable "alb_target_group_arn" {
  type        = string
  description = "ALBのターゲットグループのARN"
}

variable "alb_sg_id" {
  type        = string
  description = "ALBのセキュリティグループのID"
}

modules/compute/outputs.tf
# セキュリティグループの出力
output "web_sg_id" {
  value = aws_security_group.web.id
}

modules/compute ディレクトリに、EC2 インスタンス起動時に自動で実行されるスクリプト userdata.sh を作成します。
このスクリプトでは、ALB からのヘルスチェックに応答したり、デプロイが成功したことをブラウザで確認したりするために、Web サーバー(Nginx)をインストールしています。

modules/compute/userdata.sh
#!/bin/bash
set -eux
# nginx をインストールして Listen
amazon-linux-extras install -y nginx1
systemctl enable --now nginx

# シンプルなヘルス用ページ
echo ok >/usr/share/nginx/html/health.html

set -eux
エラー発生時に即座にスクリプトを停止させるなど、デバッグに非常に役立ちます。

4. ALB を作成する。

ユーザーからの HTTP リクエストを受け付け、EC2 インスタンスにトラフィックを振り分けるための Application LoadBalancer (ALB) を作成します。この alb モジュールには、ALB 本体に加えて、トラフィックの転送先を定義するターゲットグループや、外部からのアクセスを制御するセキュリティグループの設定が含まれます。

modules
   └── alb
        ├── main.tf
        ├── outputs.tf
        └── variables.tf
modules/alb/main.tf
######################################################

#アプリケーションロードバランサー

resource "aws_lb" "this" {
  name               = "${var.project}-alb"
  internal           = false
  load_balancer_type = "application"
  subnets            = var.public_subnet_ids
  security_groups    = [aws_security_group.alb.id]
}

######################################################

#ALBのセキュリティグループ

resource "aws_security_group" "alb" {
  name   = "${var.project}-alb-sg"
  vpc_id = var.vpc_id

  ingress {
    from_port   = 80
    to_port     = 80
    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"]
  }

}

######################################################

#ALBのターゲットグループ

resource "aws_lb_target_group" "web" {
  name     = "${var.project}-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = var.vpc_id
  health_check {
    path                = var.health_check_path
    interval            = 30
    matcher             = "200"
    healthy_threshold   = 2
    unhealthy_threshold = 2
    timeout             = 5
  }
}

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.this.arn
  port              = 80
  protocol          = "HTTP"
  default_action {
    type = "forward"
    forward {
      target_group {
        arn = aws_lb_target_group.web.arn
      }
    }
  }

}
modules/alb/variables.tf
variable "project" {
  type        = string
  description = "プロジェクトの名前"
}

variable "public_subnet_ids" {
  type        = list(string)
  description = "パブリックサブネットのCIDRリスト"
}

variable "vpc_id" {
  type        = string
  description = "VPCのID"
}

variable "health_check_path" {
  type    = string
  default = "/health.html"
}
modules/alb/outputs.tf
#ロードバランサーのdnsの出力
output "aws_dns" {
  value = aws_lb.this.dns_name
}

#ALBのセキュリティグループの出力
output "alb_sg_id" {
  value = aws_security_group.alb.id
}

#ALBのターゲットグループARNの出力
output "alb_target_group_arn" {
  value = aws_lb_target_group.web.arn
}

5. RDS を作成する。

アプリケーションのデータを永続的に保存するためのデータベースを構築します。今回は可用性を高めるため、MySQL 互換の RDS を Multi-AZ 構成でプライベートサブネットに配置します。EC2 インスタンスからのみアクセスできるよう、セキュリティグループも厳密に設定します。

modules
   └── rds
        ├── main.tf
        └── variables.tf
modules/rds/main.tf
#DBサブネットグループ
resource "aws_db_subnet_group" "this" {
  name       = "${var.project}-db-subnets"
  subnet_ids = var.private_subnet_ids
}

###########################################
#RDSセキュリティグループ
resource "aws_security_group" "rds" {
  name   = "${var.project}-rds"
  vpc_id = var.vpc_id
  ingress {
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [var.web_sg_id]
  }

}

###########################################
#RDSインスタンス
resource "aws_db_instance" "primary" {
  identifier              = "${var.project}-primary"
  engine                  = "mysql"
  engine_version          = "8.0"
  instance_class          = "db.t3.micro"
  allocated_storage       = 20
  username                = "admin"
  password                = var.db_password
  db_subnet_group_name    = aws_db_subnet_group.this.name
  vpc_security_group_ids  = [aws_security_group.rds.id]
  multi_az                = true
  skip_final_snapshot     = true
  backup_retention_period = 1
}
modules/rds/variables.tf
variable "project" {
  type        = string
  description = "プロジェクトの名前"
}

variable "private_subnet_ids" {
  type        = list(string)
  description = "プライベートサブネットのCIDRリスト"
}

variable "vpc_id" {
  type        = string
  description = "VPCのID"
}

variable "web_sg_id" {
  type        = string
  description = "セキュリティグループのID"
}

variable "db_password" {
  type        = string
  description = "データベースのパスワード"
}

6. ルートの main.tf で呼び出す

最後に、これまで作成してきた各モジュール(network, compute, alb, rds)を、プロジェクトのルートにある main.tf で組み合わせて、インフラ全体を定義します。これにより、モジュール間のデータの受け渡し(VPCID やサブネット ID など)が行われ、一つのまとまったシステムとして機能します。

root/main.tf
provider "aws" {
  region = var.region
}

####################################################

#VPC
module "vpc" {
  source               = "./modules/network"
  project              = var.project #envsで指定
  vpc_cidr_block       = var.vpc_cidr
  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnet_cidrs = ["10.0.10.0/24", "10.0.20.0/24"]
}

######################################################

#ALB
module "alb" {
  source            = "./modules/alb"
  project           = var.project
  vpc_id            = module.vpc.vpc_id
  public_subnet_ids = module.vpc.public_subnet_ids

}

####################################################

#起動テンプレートとAuto Scaling Group
module "compute" {
  source               = "./modules/compute"
  project              = var.project
  ami_id               = var.ami_id
  instance_type        = var.instance_type
  vpc_id               = module.vpc.vpc_id
  public_subnet_ids    = module.vpc.public_subnet_ids
  alb_sg_id            = module.alb.alb_sg_id
  alb_target_group_arn = module.alb.alb_target_group_arn
}

####################################################

#RDS

module "rds" {
  source             = "./modules/rds"
  project            = var.project
  private_subnet_ids = module.vpc.private_subnet_ids
  vpc_id             = module.vpc.vpc_id
  web_sg_id          = module.compute.web_sg_id
  db_password        = var.db_password #envsで指定
}

root/variables.tf
variable "project" {
  type        = string
  description = "プロジェクトの名前"
}

variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}

variable "region" {
  type    = string
  default = "ap-northeast-1"
}

variable "ami_id" {
  type        = string
  default     = "ami-01ead1eca9a200e01"
  description = "AMIのID"
}

variable "instance_type" {
  type        = string
  default     = "t3.micro"
  description = "EC2のインスタンスタイプ"
}

variable "db_password" {
  type        = string
  description = "データベースのパスワード"
}

(発展)workspace を切り分け、変数を管理する。

変数として以下のようにすると開発環境、本番環境を分けられます。

envs
 ├── dev.tfvars #開発環境
 └── prod.tfvars #本番環境
dev.tfvars
project = "〇〇" #プロジェクトの名前など、ルートmain.tfで変数化したが、variabels.tfでデフォルト値を設定していない場合

db_password = "〇〇〇〇〇〇" #データベースのパスワード

今回の例では dev.tfvars
に直接データベースのパスワードを記述しましたが、実際のプロジェクトでは、パスワードなどの機密情
報を Git で管理するのは非常に危険です。
より安全な方法として、AWS Secrets Manager や AWS Systems Manager Parameter Store
を使って機密情報を管理し、Terraform からはデータソースとしてそれを参照する方法があります。

デプロイ!!

最後に以下のコマンドをターミナルで実行し、AWS 環境にデプロイします。
今回は workspace を使い、dev という名前の開発環境用の作業領域を作成・選択してから、dev.tfvars を読み込んで実行します。
これにより将来 prod 環境などの別の環境が増えた場合でも、tfstate ファイルを安全に分離・管理することができます。

terraform init

terraform workspace new dev #①workspaceの作成

terraform workspace select dev #②workspaceの選択

terraform plan -var-file=envs/dev.tfvars #もしworkspace使用しない場合は、上記①,②をスキップ、-var-file以下を削除して実行

terraform apply -var-file=envs/dev.tfvars #もしworkspace使用しない場合は、上記①,②をスキップ、-var-file以下を削除して実行

デプロイしたリソースを削除するには以下のコマンドをターミナルで実行します

terraform destroy -var-file=envs/dev.tfvars #もしworkspace使用しない場合は、-var-file以下を削除して実行

まとめ

本記事では、Terraform を使い、一般的な Web システムのインフラ(VPC, ALB, EC2, RDS)をコードで構築する方法を解説しました。

今回の構築を通して、

  • Terraform を用いた実践的なシステム構築の流れ
  • modules を利用したコードの再利用性を高める重要性
  • 不明点を解決するための公式ドキュメントの活用

など多くのことを学ぶことができました。この記事が、これから Terraform を学ぶ方々の助けになれば幸いです。

GitHubリポジトリはこちら

次に取り組む構成

今後は、サーバーレスアーキテクチャにも挑戦し、API Gateway, Lambda,DynamoDB を用いた API の構築にも取り組んでいきたいと考えています!

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?