概要
他記事【AWS】Terraformでアクセスを分散、WebサーバーをHTTPS化する その3(ACM)で下図のような構成を作成した際に、複数サービスを__まとめてデプロイすると実装できない箇所__が気になったので、ディレクトリ構造の整理をした。
下記を重視してディレクトリを整理してみた。
- 環境別ディレクトリの整理
- 一部サービスを別Terraformとして分離
- Data Sourceの活用
- Moduleの活用
最終的なディレクトリ構成はこちら。
.
├── domain # ドメイン管理
│ ├── acm
│ │ ├── acm.tf
│ │ ├── provider.tf
│ │ ├── terraform.tfstate
│ │ └── terraform.tfstate.backup
│ └── route53
│ ├── provider.tf
│ ├── route53.tf
│ ├── terraform.tfstate
│ └── terraform.tfstate.backup
├── env # 環境別管理
│ ├── dev
│ │ ├── alb.tf
│ │ ├── ec2.tf
│ │ ├── network.tf
│ │ ├── provider.tf
│ │ ├── security_group.tf
│ │ ├── terraform.tfstate
│ │ ├── terraform.tfstate.backup
│ │ └── variables.tf
│ ├── prod
│ │ └── # 今回は省略
│ └── stg
│ └── # 今回は省略
└── modules # モジュール管理
├── ec2
│ └── mod_ec2.tf
└── network
└── mod_network.tf
主なポイント
- envディレクトリ内の
dev
,stg
,prod
の環境別ディレクトリで管理する。 - Route 53とACMのtfファイルを別ディレクトリで管理して、それぞれのディレクトリで
terraform apply
を実行する。 - 一部のリソースについて
modules
ディレクトリにモジュールを作成し、ルートディレクトリのtfファイルから情報を渡してリソースを構築する。
variables.tf, provider.tf
env/dev/variables.tf
# 変数を定義
variable "region" {
default = "ap-northeast-1"
}
variable "az_1" {
default = "ap-northeast-1a"
}
variable "az_2" {
default = "ap-northeast-1c"
}
variable "amazon_linux_ami" {
default = "ami-09ebacdc178ae23b7"
}
variable "instance_type" {
default = "t2.micro"
}
env/dev/provider.tf
provider "aws" {
region = var.region
}
mod_network.tf, network.tf
VPC、サブネット、インターネットゲートウェイ、ルートテーブルをまとめて構築。
modules/network/mod_network.tf
# 変数定義
variable "vpc_cidr" {}
variable "subnet1_cidr" {}
variable "subnet2_cidr" {}
variable "az_1" {}
variable "az_2" {}
# VPC作成
resource "aws_vpc" "vpc" {
cidr_block = var.vpc_cidr
tags = {
Name = "my-vpc-001"
}
}
# インターネットゲートウェイ
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "my-igw-001"
}
}
# サブネット
## パブリックサブネット
resource "aws_subnet" "public" {
vpc_id = aws_vpc.vpc.id
cidr_block = var.subnet1_cidr
map_public_ip_on_launch = true
availability_zone = var.az_1
tags = {
Name = "my-public-subnet-001"
}
}
## パブリックサブネット2
resource "aws_subnet" "public_2" {
vpc_id = aws_vpc.vpc.id
cidr_block = var.subnet2_cidr
map_public_ip_on_launch = true
availability_zone = var.az_2
tags = {
Name = "my-public-subnet-002"
}
}
# ルートテーブル
resource "aws_route_table" "public" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "my-public-rtb-001"
}
}
## パブリックルートを設定
resource "aws_route" "public" {
route_table_id = aws_route_table.public.id
gateway_id = aws_internet_gateway.igw.id
destination_cidr_block = "0.0.0.0/0"
}
## サブネット1へのルートテーブルの関連付け
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
## サブネット2へのルートテーブルの関連付け
resource "aws_route_table_association" "public_2" {
subnet_id = aws_subnet.public_2.id
route_table_id = aws_route_table.public.id
}
# モジュール外へのアウトプット
output "vpc_mod" {
value = aws_vpc.vpc
}
output "subnet1_mod" {
value = aws_subnet.public
}
output "subnet2_mod" {
value = aws_subnet.public_2
}
network.tf
からmod_network.tf
で宣言している変数に値を渡す。
env/dev/network.tf
module "network" {
source = "../../modules/network"
# vpc
vpc_cidr = "10.0.0.0/16"
# subnet1
subnet1_cidr = "10.0.0.0/24"
az_1 = var.az_1
# subnet2
subnet2_cidr = "10.0.1.0/24"
az_2 = var.az_2
}
security_group.tf
env/dev/security_group.tf
# セキュリティグループ
resource "aws_security_group" "sg_for_ec2" {
vpc_id = module.network.vpc_mod.id
name = "my-sg-001"
tags = {
Name = "my-sg-001"
}
}
## HTTPインバウンドルール
resource "aws_security_group_rule" "ingress_http" {
security_group_id = aws_security_group.sg_for_ec2.id
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
## アウトバウンドルール
resource "aws_security_group_rule" "egress" {
security_group_id = aws_security_group.sg_for_ec2.id
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
-
vpc_id = module.network.vpc_mod.id
モジュールは「module.任意のモジュール名.アウトプット名」で参照できる。-
module.network
network.tf内で定義したmodule "network"
-
.vpc_mod
mod_network.tf内でアウトプットされたoutput "vpc_mod"
-
mod_ec2.tf, ec2.tf
modules/ec2/mod_ec2.tf
# 変数定義
variable "instance_ami" {}
variable "instance_type" {}
variable "subnet1_id" {}
variable "subnet2_id" {}
variable "az_1" {}
variable "az_2" {}
variable "security_group_id" {}
# ec2(1)
resource "aws_instance" "web-server-instance" {
ami = var.instance_ami
instance_type = var.instance_type
subnet_id = var.subnet1_id
availability_zone = var.az_1
vpc_security_group_ids = [var.security_group_id]
tags = {
Name = "my-ec2-001"
}
# httpd ここの中身は何でもok。2つのインスタンスの見分けをつけたいだけ
user_data = <<EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
chkconfig httpd on
echo "<h1 style='color: indigo;'>Hi there, I'm the first EC2 instance.</h1>" > var/www/html/index.html
EOF
}
# ec2(2)
resource "aws_instance" "web-server-instance_2" {
ami = var.instance_ami
instance_type = var.instance_type
subnet_id = var.subnet2_id
availability_zone = var.az_2
vpc_security_group_ids = [var.security_group_id]
tags = {
Name = "my-ec2-002"
}
# httpd ここの中身は何でもok。2つのインスタンスの見分けをつけたいだけ
user_data = <<EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
chkconfig httpd on
echo "<h1 style='color: orange;'>Yo, I'm the second EC2 instance.</h1>" > var/www/html/index.html
EOF
}
# モジュール外へのアウトプット
output "first_instance" {
value = aws_instance.web-server-instance
}
output "second_instance" {
value = aws_instance.web-server-instance_2
}
ec2.tf
からmod_ec2.tf
で宣言している変数に値を渡す。
env/dev/ec2.tf
module "ec2" {
source = "../../modules/ec2"
instance_ami = var.amazon_linux_ami
instance_type = var.instance_type
security_group_id = aws_security_group.sg_for_ec2.id
#ec2(1)
subnet1_id = module.network.subnet1_mod.id
az_1 = var.az_1
#ec2(2)
subnet2_id = module.network.subnet2_mod.id
az_2 = var.az_2
}
alb.tf
env/dev/alb.tf
# ALBを作成
resource "aws_lb" "alb" {
name = "my-alb-001"
load_balancer_type = "application"
internal = false
subnets = [
module.network.subnet1_mod.id,
module.network.subnet2_mod.id,
]
security_groups = [
aws_security_group.sg_for_ec2.id
]
}
# ターゲットグループの作成
resource "aws_lb_target_group" "alb_target_group" {
name = "my-target-group"
vpc_id = module.network.vpc_mod.id
port = 80
protocol = "HTTP"
}
# ターゲットの登録
## パブリックサブネット1のEC2インスタンスをターゲットに登録
resource "aws_lb_target_group_attachment" "target" {
target_group_arn = aws_lb_target_group.alb_target_group.arn
target_id = module.ec2.first_instance.id
port = 80
}
## パブリックサブネット2のEC2インスタンスをターゲットに登録
resource "aws_lb_target_group_attachment" "target_2" {
target_group_arn = aws_lb_target_group.alb_target_group.arn
target_id = module.ec2.second_instance.id
port = 80
}
# HTTPリスナー
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.alb.arn
port = "80"
protocol = "HTTP"
# httpアクセス確認用
# default_action {
# type = "forward"
# target_group_arn = aws_lb_target_group.alb_target_group.id
# }
# httpからhttpsへのリダイレクト
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
route53.tf
route53/route53.tf
variable "domain_name" {}
# 新規のホストゾーンを作成
resource "aws_route53_zone" "hostzone" {
name = var.domain_name
}
# DNSレコード
data "aws_lb" "alb" {
name = "my-alb-001"
}
resource "aws_route53_record" "dns_record" {
zone_id = aws_route53_zone.hostzone.zone_id
name = aws_route53_zone.hostzone.name
type = "A"
alias {
name = data.aws_lb.alb.dns_name
zone_id = data.aws_lb.alb.zone_id
evaluate_target_health = true
}
}
-
variable "domain_name" {}
ここでは変数domain_name
にデフォルトの値を指定していないので、terraform plan (apply)
実行時にCLIから値の入力を対話形式で求められる。
# terraform plan (apply)を実行時
var.domain_name
Enter a value: # ドメインを入力。var.domain_nameに代入される。
- Data Sourceを使用し、外部の情報や他Terraformで定義した情報を読み込むことができる。
devディレクトリとroute53ディレクトリは、__それぞれの.tfstateファイルを持つ(それぞれterraform initを実行した)__別Terraformディレクトリである。
ここでは、既存の(作成した)ALB情報をDNSレコードのエイリアスに設定したいため、dataブロックを使って情報を持ち込んでいる。
# alb.tf
resource "aws_lb" "alb" {
name = "my-alb-001" # ←ここの name = "my-alb-001"を使用して、
...
# route53.tf
data "aws_lb" "alb" {
name = "my-alb-001" # devディレクトリで作成されたALBを特定している。
}
...
alias { # dataブロックで指定したALBのdns_name、zone_idを設定。
name = data.aws_lb.alb.dns_name
zone_id = data.aws_lb.alb.zone_id
...
acm.tf
acm/acm.tf
# 既存の(作成した)ホストゾーンを取得する
variable "domain_name" {}
data "aws_route53_zone" "hostzone" {
name = var.domain_name
}
# SSL証明書の定義
resource "aws_acm_certificate" "cert" {
domain_name = data.aws_route53_zone.hostzone.name
subject_alternative_names = []
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
# 検証用のレコード定義
resource "aws_route53_record" "cert_validation" {
for_each = {
for dvo in aws_acm_certificate.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 = 60
type = each.value.type
zone_id = data.aws_route53_zone.hostzone.zone_id
}
# 検証完了までの待機
resource "aws_acm_certificate_validation" "cert" {
certificate_arn = aws_acm_certificate.cert.arn
validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}
# HTTPSリスナー
## 既存の(作成した)ALB、ターゲットグループを取得する
data "aws_lb" "alb" {
name = "my-alb-001"
}
data "aws_lb_target_group" "alb_target_group" {
name = "my-target-group"
}
resource "aws_lb_listener" "https" {
load_balancer_arn = data.aws_lb.alb.arn
port = "443"
protocol = "HTTPS"
certificate_arn = aws_acm_certificate.cert.arn
ssl_policy = "ELBSecurityPolicy-2016-08"
default_action {
type = "forward"
target_group_arn = data.aws_lb_target_group.alb_target_group.id
}
depends_on = [
aws_acm_certificate_validation.cert
]
}
# HTTPSインバウンドルール
## 既存の(作成した)セキュリティグループを取得する
data "aws_security_group" "sg_for_ec2" {
name = "my-sg-001"
}
resource "aws_security_group_rule" "ingress_http" {
security_group_id = data.aws_security_group.sg_for_ec2.id
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
こちらもRoute53同様に既存のリソース情報をData Sourceを使用して取得している。
terraform applyの実行順序
- devディレクトリで実行。(実行前に
terraform get
を実行しモジュールを取得する) - route53ディレクトリに移動し実行。その後ネームサーバーを設定。(反映に2~3分)
- acmディレクトリに移動し実行。
終わりに
最後に所感。
変数やモジュールを使うことによってなんとなく情報が整理されているような見通しにはなったが、__管理するファイルの増加や変数宣言でコードが長くなること__等を考えると、どこをモジュール化するか、別ファイルに分けるかの切り分けの判断が難しい。
状況によっては1つのmain.tfに全てを書く方がわかりやすいというケースもありそう。
今回のディレクトリ構成ももっと効率的で可読性の高いものにできると思う〜。奥が深い。