🚀

【Terraform】ALB+EC2を作成してNginxをインストールする

2025/01/12に公開

ゴール

要件

  • ALB は HTTP(80 番ポート)でリクエストを受け、EC2(80 番ポート)に流す
  • ALB は 2 つの AZ が必要なので、AZ およびサブネットは 2 つ用意する
  • ALB、EC2 ともにパブリックサブネットに構築する
    • NAT GATEWAYはまあまあお金がかかるので今回は使わない
  • ALB からのみ EC2 にアクセスできる
  • 構築するEC2 は1台のみ
  • Amazon Linux 2023を使用して EC2 を構築し、Nginxをインストールする

動作確認方法

WEB ブラウザから ALB にアクセスし、

EC2 にインストールされた Nginx の画面が表示されることを確認します

ディレクトリ構成

.
├── modules
│   ├── alb
│   │   ├── README.md
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── ec2
│   │   ├── README.md
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── scripts
│   │   │   └── user_data.sh
│   │   └── variables.tf
│   └── subnet
│       ├── README.md
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
├── README.md
├── backend.tf
├── main.tf
├── outputs.tf
├── provider.tf
└── variables.tf

ルートディレクトリ

provider.tf

Terraform や AWS プロバイダーのバージョン、リージョンを定義します

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  required_version = ">= 1.6.0"
}

provider "aws" {
  region = "ap-northeast-1"
}

backend.tf

tfstate を保存する S3 バケットを定義します

保存先のバケット名(<bucket_name>)は各自変更してください

terraform {
  # tfstateの保存先
  backend "s3" {
    # 保存先のバケット名
    bucket = "<bucket_name>"
    region = "ap-northeast-1"
    # バケット内の保存先
    key     = "tfstate/terraform.tfstate"
    encrypt = true
  }
}

variables.tf

VPC の Cidrと作成するリソースに付与するName タグで使用する Prefixを設定しておきます

Prefix は自分がわかるように好きな文字列を設定してください

locals {
  prefix         = "prefix"
  vpc_cidr_block = "172.16.0.0/16"
}

main.tf

VPC と internet gateway、モジュールの呼び出しをします

VPC と internet gateway は一つだけあればいいのでモジュール化せずここで定義しておきます

ポイントは、

  • Subnet を 2 つの AZ で作成
  • EC2 の AMI はAmazon Linux 2023、インスタンスタイプはt2.micro(無料枠なので、、)

となるようにモジュールの引数を指定しています。

# VPC
resource "aws_vpc" "main" {
  cidr_block = "172.16.0.0/16"

  tags = {
    Name = "${local.prefix}-vpc"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "public" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${local.prefix}-internet_gateway"
  }
}


# Subnet
module "subnet_1a" {
  source              = "./modules/subnet"
  internet_gateway_id = aws_internet_gateway.public.id
  prefix              = local.prefix
  vpc_id              = aws_vpc.main.id
  # 他はデフォルト値でOK
}

module "subnet_1c" {
  source              = "./modules/subnet"
  az                  = "ap-northeast-1c"
  internet_gateway_id = aws_internet_gateway.public.id
  prefix              = local.prefix
  public_subnet_cidr  = "172.16.10.0/24"
  vpc_id              = aws_vpc.main.id
}


# EC2
module "ec2_1a" {
  source           = "./modules/ec2"
  alb_sg_id        = module.alb.alb_sg_id
  # Amazon Linux 2023 AMI
  ami              = "ami-0d48337b7d3c86f62"
  instance_type    = "t2.micro"
  prefix           = local.prefix
  public_subnet_id = module.subnet_1a.public_subnet_id
  vpc_id           = aws_vpc.main.id
}

# ALB
module "alb" {
  source = "./modules/alb"
  prefix = local.prefix
  subnets = [
    module.subnet_1a.public_subnet_id,
    module.subnet_1c.public_subnet_id
  ]
  vpc_id          = aws_vpc.main.id
  vpc_cidr_block  = local.vpc_cidr_block
  ec2_id_list     = [module.ec2_1a.instance_id]
}

outputs.tf

今回 output する値はないので、空ファイルになります

モジュール

subnet

パブリックサブネットを作成するモジュールです。

このモジュールをルートディレクトリで 2 回呼び出し、2 つの AZ に同様の Subnet を作成します

variables.tf

Subnet の構築に必要なパラメータを設定します

  • AZ
  • Subnet の Cidr

を変数化し、ほかの AZ での構築でも使用できるようにしました

# Subnetが属するAZ
variable "az" {
  default = "ap-northeast-1a"
}

variable "internet_gateway_id" {}

# Prefix
variable "prefix" {
  type = string
}

# Public Subnet's Cidr
variable "public_subnet_cidr" {
  default = "172.16.0.0/24"
}

# Subnetが属するVPCのID
variable "vpc_id" {}

main.tf

Subnet を作成し、インターネットへの Route を定義している Route Table と紐づけることで、

インターネットへアクセスできる Public Subnet 化しています

# Public Subnet
# ALBが使用
resource "aws_subnet" "public" {
  vpc_id            = var.vpc_id
  cidr_block        = var.public_subnet_cidr
  availability_zone = var.az

  tags = {
    Name = "${var.prefix}-public_subnet-${var.az}"
  }
}

# Route Table
# Public Subnetが使用
resource "aws_route_table" "public" {
  vpc_id = var.vpc_id

  tags = {
    Name = "${var.prefix}-public-route_table-${var.az}"
  }
}

# Route
resource "aws_route" "public" {
  route_table_id = aws_route_table.public.id
  gateway_id     = var.internet_gateway_id
  # 通信先
  # インターネットへの疎通許可
  destination_cidr_block = "0.0.0.0/0"
}

# ルートテーブルとサブネットを関連付け
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

outputs.tf

EC2 や ALB の構築時に必要な Subnet の IDを output しておきます

output "public_subnet_id" {
  value = aws_subnet.public.id
}

ec2

EC2にキーペアを指定していないため、SSH はできません。

もし SSH したい方はキーペアを指定するか、
↓ を参考に Session Manager を使ってみてください!
https://zenn.dev/kuuki/articles/terraform-aws-ssh-to-ec2-by-session-manager/

variables.tf

EC2 の構築に必要なパラメータを設定します

  • AMI
  • AZ
  • Instance Type
  • Subnet の ID

を変数化し、複数インスタンスを構築できるようにしました

variable "ami" {}

variable "alb_sg_id" {}

variable "az" {
  default = "ap-northeast-1a"
}

variable "instance_type" {}

variable "prefix" {
  type = string
}

variable "public_subnet_id" {}

variable "vpc_id" {}

main.tf

EC2 インスタンス、Security Group、EIPを定義しています

・EC2 インスタンス

AMI やインスタンスタイプ、Subnet等を指定しています

ユーザーデータに sh ファイルを指定し、ファイルを読み込んでいます

・Security Group

ALB⇒EC2 の 80 ポートへの通信(インバウンド)と EC2⇒ インターネットの通信(アウトバウンド)を許可しています

# EC2
resource "aws_instance" "instance" {
  ami           = var.ami
  instance_type = var.instance_type
  subnet_id     = var.public_subnet_id
  user_data     = file("${path.module}/scripts/user_data.sh")

  vpc_security_group_ids = [aws_security_group.ec2_sg.id]

  tags = {
    Name = "${var.prefix}-ec2-${var.az}"
  }
}

resource "aws_security_group" "ec2_sg" {
  name        = "ec2-sg"
  vpc_id      = var.vpc_id

  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [var.alb_sg_id]
  }

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

  tags = {
    Name = "${var.prefix}-ec2-sg"
  }
}

# EC2用EIP
resource "aws_eip" "ec2" {
  instance = aws_instance.instance.id
  domain   = "vpc"
  tags = {
    Name = "${var.prefix}-eip-ec2-${var.az}"
  }
}

user_data.sh

EC2 のユーザーデータに設定するスクリプトです。

今回はNginx をインストールし起動するものとなっています

#!/bin/bash

# Nginxインストール
dnf install -y nginx

# 自動起動設定
systemctl enable nginx

# 起動
systemctl start nginx

outputs.tf

ALB の構築時に必要なインスタンスの ID と Security Group の IDを output します

output "instance_id" {
  value = aws_instance.instance.id
}

output "ec2_sg_id" {
  value = aws_security_group.ec2_sg.id
}

alb

variables.tf

ALB の構築に必要なパラメータを設定します

ターゲットグループに設定する EC2 の ID はリスト型にし、複数指定できるようにします

variable "prefix" {
  type = string
}

# ALBをおくSubnet
variable "subnets" {
  type = list(string)
}

variable "vpc_id" {}

variable "vpc_cidr_block" {}

variable "ec2_id_list" {
  type = list(string)
}

main.tf

ALB の動きとしては

  1. 80 ポートでリクエストを受け取る
  2. ターゲットグループ内の EC2(80 ポート)にリクエストを流す

を想定しており、パラメータを表にまとめました

・aws_lb

プロパティ 説明
name alb LBの名前
internal false VPC外からアクセスしたいのでfalse
load_balancer_type application ELBのタイプ。今回はALB
security_groups aws_security_group.alb_sg.id ELBで使用するSecurity Group リストで指定する
subnets var.subnets ELBが属するSubnet。 リストで指定する。 ALBの場合は2つ以上指定する必要あり。
enable_deletion_protection true ELBが削除可能かどうか trueなので、Terraformによる削除は不可

・aws_security_group

名前と VPC、Name タグの付与のみ

・aws_security_group_rule(インバウンド)

プロパティ 説明
security_group_id aws_security_group.alb_sg.id ルールを紐づけるSecurity GroupのID
type ingress インバウンドなので、ingress
from_port 80 80ポートへのアクセスを許可する
to_port 80
protocol tcp 通信プロトコルはTCP
cidr_blocks "0.0.0.0/0" どのIPからでもアクセスを許可

・aws_security_group_rule(アウトバウンド)

プロパティ 説明
security_group_id aws_security_group.alb_sg.id ルールを紐づけるSecurity GroupのID
type egress アウトバウンドなので、egress
from_port 80 80ポートへのアクセスを許可する
to_port 80
protocol tcp 通信プロトコルはTCP
cidr_blocks var.vpc_cidr_block VPC内のIPアドレスへの通信を許可

・aws_lb_listener

プロパティ 説明
load_balancer_arn aws_lb.alb.arn 紐づけるELBのARN
port "80" 80ポートでリクエストを受け付ける
protocol "HTTP" 受け取るプロトコルはHTTP

・aws_lb_target_group

プロパティ 説明
name alb-tg ターゲットグループ名
port 80 ポート
protocol HTTP プロトコル
vpc_id var.vpc_id 属するVPCのID

・aws_lb_target_group_attachment

プロパティ 説明
for_each toset(var.ec2_id_list) リスト内の要素を一つずつ取り出し、resourceを作成していく
target_group_arn aws_lb_target_group.tg.arn 紐づけるターゲットグループのARN。
target_id each.value ターゲットグループにアタッチするインスタンスのID。 each.valueでループ中のリスト内の値がとれる
port 80 インスタンスの80ポートで受け取る
# ALB
resource "aws_lb" "alb" {
  name               = "alb"
  internal           = false
  load_balancer_type = "application"

  security_groups = [aws_security_group.alb_sg.id]
  subnets         = var.subnets

  # 削除保護
  enable_deletion_protection = true

  tags = {
    Name = "${var.prefix}-alb"
  }
}

resource "aws_security_group" "alb_sg" {
  name        = "alb_sg"
  vpc_id      = var.vpc_id

  tags = {
    Name = "${var.prefix}-alb-sg"
  }
}

resource "aws_security_group_rule" "alb_sg_ingress_http" {
  security_group_id = aws_security_group.alb_sg.id
  type              = "ingress"
  from_port         = "80"
  to_port           = "80"
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
}

resource "aws_security_group_rule" "alb_sg_egress" {
  security_group_id = aws_security_group.alb_sg.id
  type              = "egress"
  from_port         = 80
  to_port           = 80
  protocol          = "tcp"
  cidr_blocks       = [var.vpc_cidr_block]
}

# HTTPリスナー
resource "aws_lb_listener" "alb_http" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.tg.arn
  }
}

# ターゲットグループ
resource "aws_lb_target_group" "tg" {
  name     = "alb-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = var.vpc_id

  tags = {
    Name = "${var.prefix}-alb-tg"
  }
}

resource "aws_lb_target_group_attachment" "tg_attachment" {
  for_each = toset(var.ec2_id_list)

  target_group_arn = aws_lb_target_group.tg.arn
  target_id        = each.value
  port             = 80
}

outputs.tf

EC2 の Security Group の Ingress に必要な ALB の Security Group の IDを output します

output "alb_sg_id" {
  value = aws_security_group.alb_sg.id
}

必要な IAM ポリシー

今回使用したIAM ポリシーはこちらです

  • AmazonEC2FullAccess
  • AmazonS3FullAccess
  • AmazonVPCFullAccess
  • ElasticLoadBalancingFullAccess

もう少し権限を絞りたかった。。。

リソース作成&確認

ルートディレクトリで以下のコマンドを実行してデプロイしましょう

$ terraform init
$ terraform validate
$ terraform plan
$ terraform apply -auto-approve

デプロイしたら AWS コンソールで作成されているか確認しましょう

動作確認

ALB の DNS(×××.ap-northeast-1.elb.amazonaws.com) にアクセスして、 Nginx の画面が表示されたら OK!

参考記事

https://registry.terraform.io/providers/hashicorp/aws/latest/docs

Discussion