【Terraform】ALB+EC2を作成してNginxをインストールする
ゴール
要件
- 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 を使ってみてください!
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 の動きとしては
- 80 ポートでリクエストを受け取る
- ターゲットグループ内の 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!
参考記事
Discussion