【AWS】Terraformディレクトリ構造の整理（tfファイル配置方式の見直し）

## 概要
他記事[【AWS】Terraformでアクセスを分散、WebサーバーをHTTPS化する その3(ACM)](https://qiita.com/ramunauna/items/766282526080b928c338)で下図のような構成を作成した際に、複数サービスを__まとめてデプロイすると実装できない箇所__が気になったので、ディレクトリ構造の整理をした。
![ALB_Route53_ACM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1813991/ac0da77b-d9fa-f1cc-5fd0-63f304929274.png)

下記を重視してディレクトリを整理してみた。

- 環境別ディレクトリの整理
- 一部サービスを別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

```terraform: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"
}
```
```terraform:env/dev/provider.tf
provider "aws" {
  region = var.region
}
```

## mod_network.tf, network.tf
VPC、サブネット、インターネットゲートウェイ、ルートテーブルをまとめて構築。

```terraform: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`で宣言している変数に値を渡す。

```terraform: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

```terraform: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

```terraform: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`で宣言している変数に値を渡す。

```terraform: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

```terraform: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

```terraform: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から値の入力を対話形式で求められる。

```bash
# terraform plan (apply)を実行時
var.domain_name
  Enter a value: # ドメインを入力。var.domain_nameに代入される。
```

- Data Sourceを使用し、外部の情報や他Terraformで定義した情報を読み込むことができる。  
devディレクトリとroute53ディレクトリは、__それぞれの.tfstateファイルを持つ(それぞれterraform initを実行した）__別Terraformディレクトリである。  
ここでは、既存の（作成した）ALB情報をDNSレコードのエイリアスに設定したいため、dataブロックを使って情報を持ち込んでいる。

```terraform
# 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

```terraform: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の実行順序

1. devディレクトリで実行。(実行前に`terraform get`を実行しモジュールを取得する）
1. route53ディレクトリに移動し実行。その後ネームサーバーを設定。（反映に2~3分）
1. acmディレクトリに移動し実行。

## 終わりに
最後に所感。

変数やモジュールを使うことによってなんとなく情報が整理されているような見通しにはなったが、__管理するファイルの増加や変数宣言でコードが長くなること__等を考えると、どこをモジュール化するか、別ファイルに分けるかの切り分けの判断が難しい。
状況によっては1つのmain.tfに全てを書く方がわかりやすいというケースもありそう。

今回のディレクトリ構成ももっと効率的で可読性の高いものにできると思う〜。奥が深い。