はじめに
こんにちは、こんばんは、どうもamaebiと申します。
今回は、Public IPv4を持たないデュアルスタック構成でWebサーバー環境をTerraformで構築しました。
本ブログの想定読者として、「Public IPv4が有料化したし、Public IPv4を使用しない構成をぱぱっと作りたい」という方向けになっておりますので、必要に応じてカスタマイズしていただけますと幸いです。
考慮していない点
本ブログで以下の内容は考慮しておりません。
こちらは、後日ブログにする予定です。
- バックアップ設定
- ログ情報 の保存
- 運用・保守 (CloudWatch等)
構成図
ディレクトリ構成
ディレクトリの構成内容は以下のようになっています。
.
├── providers.tf
├── vpc.tf
├── sg.tf
├── ec2.tf
├── alb.tf
├── route53.tf
├── acm.tf
├── key_pair
│ ├── private_key.pem
│ └── public_key.pub
├── terraform.tfstate
├── terraform.tfstate.backup
└── terraform.tfvars
Terraformコード
vpc.tf
ファイルについては、過去に自分が書いたブログから引用しております。
※Nat Gateway は不要でしたので、今回は作成しておりません。必要に応じて、下記ブログから参照してください。
#--------------------------------------
# VPC
#--------------------------------------
resource "aws_vpc" "vpc" {
cidr_block = "10.0.0.0/16"
instance_tenancy = "default"
enable_dns_support = true
enable_dns_hostnames = true
assign_generated_ipv6_cidr_block = true
tags = {
Name = "example-vpc"
}
}
#--------------------------------------
# Subnet
#--------------------------------------
resource "aws_subnet" "public_subnet_1a" {
vpc_id = aws_vpc.vpc.id
availability_zone = "ap-northeast-1a"
map_public_ip_on_launch = true
cidr_block = cidrsubnet(aws_vpc.vpc.cidr_block, 8, 1)
assign_ipv6_address_on_creation = true
ipv6_cidr_block = cidrsubnet(aws_vpc.vpc.ipv6_cidr_block, 8, 0)
tags = {
Name = "example-public-subnet-1a"
}
}
resource "aws_subnet" "private_subnet_1a" {
vpc_id = aws_vpc.vpc.id
availability_zone = "ap-northeast-1a"
map_public_ip_on_launch = true
cidr_block = cidrsubnet(aws_vpc.vpc.cidr_block, 8, 2)
assign_ipv6_address_on_creation = true
ipv6_cidr_block = cidrsubnet(aws_vpc.vpc.ipv6_cidr_block, 8, 1)
tags = {
Name = "example-private-subnet-1a"
}
}
resource "aws_subnet" "public_subnet_1c" {
vpc_id = aws_vpc.vpc.id
availability_zone = "ap-northeast-1c"
map_public_ip_on_launch = true
cidr_block = cidrsubnet(aws_vpc.vpc.cidr_block, 8, 3)
assign_ipv6_address_on_creation = true
ipv6_cidr_block = cidrsubnet(aws_vpc.vpc.ipv6_cidr_block, 8, 2)
tags = {
Name = "example-public-subnet-1c"
}
}
resource "aws_subnet" "private_subnet_1c" {
vpc_id = aws_vpc.vpc.id
availability_zone = "ap-northeast-1c"
map_public_ip_on_launch = true
cidr_block = cidrsubnet(aws_vpc.vpc.cidr_block, 8, 4)
assign_ipv6_address_on_creation = true
ipv6_cidr_block = cidrsubnet(aws_vpc.vpc.ipv6_cidr_block, 8, 3)
tags = {
Name = "example-private-subnet-1c"
}
}
#--------------------------------------
# Route table
#--------------------------------------
resource "aws_route_table" "public_rt_1a" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "example-rt-public-1a"
}
}
resource "aws_route_table_association" "public_rt_1a" {
route_table_id = aws_route_table.public_rt_1a.id
subnet_id = aws_subnet.public_subnet_1a.id
}
resource "aws_route_table" "private_rt_1a" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "example-rt-private-1a"
}
}
resource "aws_route_table_association" "private_rt_1a" {
route_table_id = aws_route_table.private_rt_1a.id
subnet_id = aws_subnet.private_subnet_1a.id
}
resource "aws_route_table" "public_rt_1c" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "example-rt-public-1c"
}
}
resource "aws_route_table_association" "public_rt_1c" {
route_table_id = aws_route_table.public_rt_1c.id
subnet_id = aws_subnet.public_subnet_1c.id
}
resource "aws_route_table" "private_rt_1c" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "example-rt-private-1c"
}
}
resource "aws_route_table_association" "private_rt_1c" {
route_table_id = aws_route_table.private_rt_1c.id
subnet_id = aws_subnet.private_subnet_1c.id
}
#--------------------------------------
# Internet Gateway
#--------------------------------------
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "example-igw"
}
}
resource "aws_route" "public_rt_igw_1a_ipv4" {
route_table_id = aws_route_table.public_rt_1a.id
gateway_id = aws_internet_gateway.igw.id
destination_cidr_block = "0.0.0.0/0"
}
resource "aws_route" "public_rt_igw_1a_ipv6" {
route_table_id = aws_route_table.public_rt_1a.id
gateway_id = aws_internet_gateway.igw.id
destination_ipv6_cidr_block = "::/0"
}
resource "aws_route" "public_rt_igw_1c_ipv4" {
route_table_id = aws_route_table.public_rt_1c.id
gateway_id = aws_internet_gateway.igw.id
destination_cidr_block = "0.0.0.0/0"
}
resource "aws_route" "public_rt_igw_1c_ipv6" {
route_table_id = aws_route_table.public_rt_1c.id
gateway_id = aws_internet_gateway.igw.id
destination_ipv6_cidr_block = "::/0"
}
#--------------------------------------
# Egress-Only Internet Gateway
#--------------------------------------
resource "aws_egress_only_internet_gateway" "egress_only_igw" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "example-egress-only-igw"
}
}
resource "aws_route" "private_rt_egress_only_igw_1a" {
route_table_id = aws_route_table.private_rt_1a.id
egress_only_gateway_id = aws_egress_only_internet_gateway.egress_only_igw.id
destination_ipv6_cidr_block = "::/0"
}
resource "aws_route" "private_rt_egress_only_igw_1c" {
route_table_id = aws_route_table.private_rt_1c.id
egress_only_gateway_id = aws_egress_only_internet_gateway.egress_only_igw.id
destination_ipv6_cidr_block = "::/0"
}
locals {
my_ipv6 = ["<My IPv6アドレス>"]
}
#--------------------------------------
# Security Group : EC2 (Web)
#--------------------------------------
resource "aws_security_group" "web_ec2_sg" {
name = "web-ec2-sg"
vpc_id = aws_vpc.vpc.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.bastion_ec2_sg.id]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb_sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
tags = {
Name = "web-ec2-sg"
}
}
#--------------------------------------
# Security Group : EC2 (Bastion)
#--------------------------------------
resource "aws_security_group" "bastion_ec2_sg" {
name = "bastion-ec2-sg"
vpc_id = aws_vpc.vpc.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
ipv6_cidr_blocks = local.my_ipv6
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
tags = {
Name = "bastion-ec2-sg"
}
}
#--------------------------------------
# Security Group : ALB
#--------------------------------------
resource "aws_security_group" "alb_sg" {
name = "alb-sg"
vpc_id = aws_vpc.vpc.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
ipv6_cidr_blocks = ["::/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
ipv6_cidr_blocks = ["::/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
tags = {
Name = "alb-sg"
}
}
# インスタンスタイプ
locals {
dastion_instance_type = "t2.micro"
web_1a_instance_type = "t2.micro"
web_1c_instance_type = "t2.micro"
}
#--------------------------------------
# AMI : AmazonLinux2023 (x86_64)
#--------------------------------------
data "aws_ami" "amazon_linux_2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-kernel-6.1-x86_64"]
}
}
#--------------------------------------
# key_pair
#--------------------------------------
resource "aws_key_pair" "key_pair" {
key_name = "example-key"
public_key = file("./key_pair/public_key.pub")
tags = {
Name = "example-key"
}
}
#--------------------------------------
# EC2 (Bastion)
#--------------------------------------
resource "aws_instance" "bastion_ec2" {
ami = data.aws_ami.amazon_linux_2023.id
instance_type = local.dastion_instance_type
subnet_id = aws_subnet.public_subnet_1a.id
vpc_security_group_ids = [aws_security_group.bastion_ec2_sg.id]
associate_public_ip_address = false
ipv6_address_count = 1
key_name = aws_key_pair.key_pair.key_name
root_block_device {
volume_size = 8
volume_type = "gp3"
iops = 3000
throughput = 125
delete_on_termination = true
tags = {
Name = "bastion-ebs"
}
}
tags = {
Name = "bastion-ec2"
}
}
#--------------------------------------
# EC2 (Web/App)
#--------------------------------------
resource "aws_instance" "web_1a_ec2" {
ami = data.aws_ami.amazon_linux_2023.id
instance_type = local.web_1a_instance_type
subnet_id = aws_subnet.private_subnet_1a.id
vpc_security_group_ids = [aws_security_group.web_ec2_sg.id]
associate_public_ip_address = false
ipv6_address_count = 1
key_name = aws_key_pair.key_pair.key_name
root_block_device {
volume_size = 8
volume_type = "gp3"
iops = 3000
throughput = 125
delete_on_termination = true
tags = {
Name = "web-1a-ebs"
}
}
tags = {
Name = "web-1a-ec2"
}
}
resource "aws_instance" "web_1c_ec2" {
ami = data.aws_ami.amazon_linux_2023.id
instance_type = local.web_1c_instance_type
subnet_id = aws_subnet.private_subnet_1c.id
vpc_security_group_ids = [aws_security_group.web_ec2_sg.id]
associate_public_ip_address = false
ipv6_address_count = 1
key_name = aws_key_pair.key_pair.key_name
root_block_device {
volume_size = 8
volume_type = "gp3"
iops = 3000
throughput = 125
delete_on_termination = true
tags = {
Name = "web-1c-ebs"
}
}
tags = {
Name = "web-1c-ec2"
}
}
#--------------------------------------
# ALB
#--------------------------------------
resource "aws_lb" "alb" {
name = "example-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb_sg.id]
subnets = [
aws_subnet.public_subnet_1a.id,
aws_subnet.public_subnet_1c.id
]
ip_address_type = "dualstack-without-public-ipv4"
enable_http2 = false # http2を有効化するかどうか
enable_deletion_protection = false # 削除保護
# アクセスログの保存設定(必要に応じて設定してください)
# access_logs {
# bucket = ""
# prefix = ""
# enable = ""
# }
tags = {
Name = "example-alb"
}
}
#--------------------------------------
# Listener Rule
#--------------------------------------
# リダイレクト設定
resource "aws_lb_listener" "alb_listener_http" {
load_balancer_arn = aws_lb.alb.arn
port = 80
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = 443
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
resource "aws_lb_listener" "alb_listener_https" {
load_balancer_arn = aws_lb.alb.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate.tokyo_cert.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.alb_target_group.arn
}
depends_on = [
aws_acm_certificate_validation.cert_valid
]
}
#--------------------------------------
# Target Group
#--------------------------------------
resource "aws_lb_target_group" "alb_target_group" {
name = "example-web-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.vpc.id
# ヘルスチェック先
health_check {
path = "/index.html"
}
}
resource "aws_lb_target_group_attachment" "instance1" {
target_group_arn = aws_lb_target_group.alb_target_group.arn
target_id = aws_instance.web_1a_ec2.id
port = 80
}
resource "aws_lb_target_group_attachment" "instance2" {
target_group_arn = aws_lb_target_group.alb_target_group.arn
target_id = aws_instance.web_1c_ec2.id
port = 80
}
locals {
domain = "<ドメイン名>"
}
#--------------------------------------
# Route53 : Host Zone
#--------------------------------------
resource "aws_route53_zone" "public_zone" {
name = local.domain
}
#--------------------------------------
# Route53 : Record (AAAA)
#--------------------------------------
resource "aws_route53_record" "ipv6_record" {
zone_id = aws_route53_zone.public_zone.zone_id
name = local.domain
type = "AAAA"
alias {
name = aws_lb.alb.dns_name
zone_id = aws_lb.alb.zone_id
evaluate_target_health = true
}
}
#--------------------------------------
# Route53 : Record (DNS検証用レコード)
#--------------------------------------
resource "aws_route53_record" "route53_acm_dns_resolve" {
for_each = {
for dvo in aws_acm_certificate.tokyo_cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 600
type = each.value.type
zone_id = aws_route53_zone.public_zone.id
}
#--------------------------------------
# ACM
#--------------------------------------
resource "aws_acm_certificate" "tokyo_cert" {
domain_name = local.domain
validation_method = "DNS"
tags = {
Name = "wildecard-sslcert"
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_acm_certificate_validation" "cert_valid" {
certificate_arn = aws_acm_certificate.tokyo_cert.arn
validation_record_fqdns = [for record in aws_route53_record.route53_acm_dns_resolve : record.fqdn]
}
インスタンスの確認
実際に作成されたインスタンス情報を確認していきます。
※VPCに関しては、過去の記事で動作確認も行っているため、本ブログでは取り上げません。
EC2
すべてのEC2インスタンスがPublic IPv4を持たないデュアルスタックのインスタンスとして作成できました。
ALB
こちらもpublic IPv4を持たないデュアルスタックのロードバランサーとして作成できました。
この時ALB名をつけることを忘れておりました...
Route53
こちらも問題なく作成されています。
ACM
証明書も問題なく発行できました。
動作確認
ドメイン名からHTTPS経由でサイトが見られるか確認します。
その際、ALBが各インスタンスに対してトラフィックを分散できているかどうか確認します。
踏み台サーバーから各webサーバーに対して、sshでログインし、テストデータを挿入します。
- web-1a-ec2
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
web-1a-ec2
</body>
</html>
- web-1c-ec2
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
web-1c-ec2
</body>
</html>
踏み台サーバーにwebサーバー用の秘密鍵を入れ、テスト用のapacheをインストールします。
$ touch /home/ec2-user/.ssh/example-key
$ vi /home/ec2-user/.ssh/example-key ←秘密鍵を入力する
$ cat /home/ec2-user/.ssh/example-key
$ chmod 600 /home/ec2-user/.ssh/example-key
$ ssh -i /home/ec2-user/.ssh/example-key ec2-user@<IPv6アドレス>
$ sudo su -
# yum update -y
# yum install -y httpd
# touch /var/www/html/index.html
# vi /var/www/html/index.html ←各webサーバー用のテストデータを挿入する。
# systemctl start httpd
# systemctl status httpd
では、実際にドメイン名からHTTPS経由でサイトが見られるか確認していきましょう。
- web-1a-ec2
- web-1c-ec2
問題なく、各webを確認、疎通確認することができました。
さいごに
以上で、「Public IPv4を持たないデュアルスタック構成でWebサーバー環境をTerraformで構築してみた」でした。
Public IPv4を持たないAWSサービスが少なく、より複雑な構成を作成するのには少し限界があるかなと感じました。
今後保守・運用を考慮した構成も考えておりますので、ブログ更新を気長にお待ちしていただけますと幸いです。