タイトルにあるとおり、TerraformでALB+EC2を構築していきます。
基本的な構成だと思いますので、使う機会も多いのでは?
Terraformの勉強としても使えるので試してみてください!
想定読者
- Terraformの基本的な文法を理解している
- AWSに触れたことがある
- Terraformを使用してAWSにデプロイできる環境がある
ゴール
インフラ構成図
要件
- ALBはHTTP(80番ポート)でリクエストを受け、EC2(80番ポート)に流す
- ALBは2つのAZが必要なので、AZおよびサブネットは2つ用意する
- ALB、EC2ともにパブリックサブネットに構築する
- ALBからのみEC2にアクセスできる
- 構築するEC2は1台のみ
NAT Gateway を使用してEC2→インターネットに疎通可能
⇒なかなかコストがかかるのでNAT Gatewayは使わない- Amazon Linux 2023 を使用してEC2を構築し、Nginxをインストールする
動作確認方法
WEBブラウザからALBにアクセスし、
EC2にインストールされたNginxの画面が表示されることを確認します
ディレクトリ構成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
. ├── 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プロバイダーのバージョン、リージョンを定義します
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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>)は各自変更してください
1 2 3 4 5 6 7 8 9 10 11 |
terraform { # tfstateの保存先 backend "s3" { # 保存先のバケット名 bucket = "<bucket_name>" region = "ap-northeast-1" # バケット内の保存先 key = "tfstate/terraform.tfstate" encrypt = true } } |
variables.tf
VPCのCidrと作成するリソースに付与するNameタグで使用するPrefixを設定しておきます
Prefixは自分がわかるように好きな文字列を設定してください
1 2 3 4 |
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(無料枠なので、、)
となるようにモジュールの引数を指定しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# 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での構築でも使用できるようにしました
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# 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化しています
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# 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しておきます
1 2 3 |
output "public_subnet_id" { value = aws_subnet.public.id } |
ec2
EC2にキーペアを指定していないため、SSHはできません。
もしSSHしたい方はキーペアを指定するか、
↓を参考にSession Managerを使ってみてください!
variables.tf
EC2の構築に必要なパラメータを設定します
- AMI
- AZ
- Instance Type
- SubnetのID
を変数化し、複数インスタンスを構築できるようにしました
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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⇒インターネットの通信(アウトバウンド)を許可しています
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# 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をインストールし起動するものとなっています
1 2 3 4 5 6 7 8 9 10 |
#!/bin/bash # Nginxインストール dnf install -y nginx # 自動起動設定 systemctl enable nginx # 起動 systemctl start nginx |
outputs.tf
ALBの構築時に必要なインスタンスのIDとSecurity GroupのIDをoutputします
1 2 3 4 5 6 7 |
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はリスト型にし、複数指定できるようにします
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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による削除は不可 |
tags |
{
Name = “${var.prefix}-alb”
}
|
Nameタグを付与しておく |
・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 |
default_action |
{
type = “forward”
target_group_arn = aws_lb_target_group.tg.arn
}
|
リクエストが来た時にどうするか。 今回はターゲットグループに振り分けるため、typeはforward、ターゲットグループを指定 |
・aws_lb_target_group
プロパティ | 値 | 説明 |
name | “alb-tg” | ターゲットグループ名 |
port | 80 | ポート |
protocol | “HTTP” | プロトコル |
vpc_id | var.vpc_id | 属するVPCのID |
tags |
{
Name = “${var.prefix}-alb-tg”
}
|
Nameタグを付与しておく |
・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ポートで受け取る |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# 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します
1 2 3 |
output "alb_sg_id" { value = aws_security_group.alb_sg.id } |
必要なIAMポリシー
今回使用したIAMポリシーはこちらです
- AmazonEC2FullAccess
- AmazonS3FullAccess
- AmazonVPCFullAccess
- ElasticLoadBalancingFullAccess
もう少し権限を絞りたかった。。。
リソース作成&確認
ルートディレクトリで以下のコマンドを実行してデプロイしましょう
1 2 3 4 |
$ terraform init $ terraform validate $ terraform plan $ terraform apply -auto-approve |
デプロイしたらAWSコンソールで作成されているか確認しましょう
動作確認
ALBのDNS(×××.ap-northeast-1.elb.amazonaws.com) にアクセスして、
Nginxの画面が表示されたらOK!
参考記事
コメント