案件が増えるたびにRoute53・S3・Cloudfront・ACM作るのがだるいのでTerraformを使って自動作成するようにしてみた

どうもこんにちは。

先日はTerraformerのインストール方法なんて書いてみましたが、結局使ってません。
きっと既存のインフラをIaCする時だけでいいんでしょうね。（初めてなのでよくわかりませんでした。）


さて、表題の通り、案件が増えるたびに手作業でやってましたがさすがにだるすぎるのでTerraformを覚えて
以降はterraformファイルのコピペだけでできるようにしました。

#いつも手作業でやっていたこと
## 前提
　　本番URL: **anken.co.jp**
　　案件名: **anken**

を既存のALB、その配下の既存のEC2に相乗りで運用するとします。

環境は
　・テスト環境（**test.anken.co.jp**）
　・本番環境（**anken.co.jp**）
を用意します。

##手順
###1. URLの取得
諸所ありMUUMUU辺りで取ってます。

###2. ホストゾーンの作成
取得したURLでALB配下ないし既存のEC2で応答させたいです。
その為にRoute53でホストゾーンを作成し、以下のレコードを作成しています。

  - anken.co.jpを既存ALBへ転送するAレコード
  - test.anken.co.jpを既存ALBへ転送するAレコード（ALBのルールでテスト環境へ振り分けている）
  - mail.anken.co.jpをメールサーバーのEC2へ紐づけるAレコード
  - anken.co.jp宛のメールをmail.anken.co.jpで受け付けるためのMXレコード
  - test.anken.co.jp宛のメールをmail.anken.co.jpで受け付けるためのMXレコード
  - anken.co.jpからのメール送信元を定義するTXTレコード
  - img.anken.co.jpという、CDN用S3へのパブリックアクセス用のURLをcloudfrontへ紐づけるAレコード

###3. S3バケットの作成
CDN的な扱いのバケットを作成します。

###4. SSL証明書の取得
ACMを使って証明書を作成しています。
anken.co.jp、test.anken.co.jpをそれぞれSSLにするため、

  - *.anken.co.jp
  - anken.co.jp　（追加）

でACMを取得しています。

また、cloudfrontからS3バケットにアクセスするときの独自URLとして

  - img.anken.co.jp

でのSSLも取得しています。（DNS確認レコードも作成します。）

###5. Cloudfrontの作成
3.　で作成したCDN用S3バケットへアクセスするためのCloudfrontを作成します。

###6. ターゲットグループの作成
ALBからの振り分け先インスタンスグループを作成します。


###7. ALBの振り分け設定を追加
anken.co.jp、test.anken.co.jpの場合の振り分け先ターゲットグループを設定します。

###8. SSL証明書をALBに追加
4.　で作成した証明書をALBに追加します。 
　
**まぁこんな感じです。**


#Terraformのフォルダ構成
*こんな感じにしました。*

```shell
/terraform    
|--modules    #既存リソース
|  |--ec2.tf
|  |--vpc.tf
|--anken      #!ここが作業ファルダ!
|  |--acm.tf
|  |--cloudfront.tf
|  |--ec2.tf
|  |--init.tf
|  |--main.tf
|  |--route53.tf
|  |--s3.tf
|--anken2     #案件が増えたら...
|  |--acm.tf
|  |--cloudfront.tf
|  |--ec2.tf
|  |--init.tf
|  |--main.tf
|  |--route53.tf
|  |--s3.tf
```

増えるリソースだけはankenフォルダに切り分け、既存のリソースなどはmodulesフォルダにおきました。
（modulesという名前は相応しくないか。）

##余談
初心者故、Terraformはルートディレクトリからplanとかapplyとかするもんだと思っていたのでわかるまで時間がかかってしまった。
また、フォルダ構成に正解はないようだが、案件がどんどん増えていくパターンのディレクトリ構成をどのようにググればいいかわからず大変だった。


#tfファイルソース
##まず既存資源のソース
###ec2周り
冗長構成の為、インスタンス2台の定義をします。
また、既存のロードバランサーとそのリスナー（80/443ポート）も定義しておきます。

```terraform:/terraform/modules/ec2.tf
data "aws_instance" "ap_1" {
  instance_id = "i-(インスタンスID)"
}

data "aws_instance" "ap_2" {
  instance_id = "i-(インスタンスID)"
}

output ap_1 {
  value = data.aws_instance.ap_1
}

output ap_2 {
  value = data.aws_instance.ap_2
}

data "aws_alb" "main-lb" {
  name = "main-lb"
  arn  = "(ALBのarn)"
}

output main-lb {
  value = data.aws_alb.main-lb
}

data "aws_lb_listener" "main_lb_listener_80" {
  arn  = "(ALBのリスナー（80ポート）のarn)"
}

data "aws_lb_listener" "main_lb_listener_443" {
  arn  = "(ALBのリスナー（443ポート）のarn)"
}

output main_lb_listener_80 {
  value = "${data.aws_lb_listener.main_lb_listener_80}"
}

output main_lb_listener_443 {
  value = "${data.aws_lb_listener.main_lb_listener_443}"
}
```
outputを書き忘れるとmodule外から参照できません。

###vpc周り
```terraform:/terraform/modules/vpc.tf
data "aws_vpc" "vpc" {
  id = "vpc-*******"
}

output "vpc" {
  value = "${data.aws_vpc.vpc}"
}
```
関係ないと思うかもしれませんがターゲットグループを設定するのにvpcの情報を求められるので定義しないといけません。
（この芋づる感がめんどくさい）

##案件ごとに複製するリソース部分のソース
###まずinit.tfとmain.tf

```terraform:/terraform/anken/init.tf
provider "aws" {
  region = "ap-northeast-1"
}
# S3用のcloudfrontの証明書取るときに使います。
provider "aws" {
  alias  = "virginia"
  region = "us-east-1"
}
```
```terraform:/terraform/anken/main.tf
module "main" {
  source = "../modules/"
}

# ターゲットグループ名などに使います。
variable product_name {
  default = "anken"
}

# Route53などへ設定する値に使います。
variable domain {
  default = "anken.co.jp"
}

```
Variableを使って案件固有の値を定義します。

###手順2. ホストゾーンの作成　のソース
```terraform:/terraform/anken/route53.tf
resource "aws_route53_zone" "zone_for_prod" {
  name = "${var.domain}"
}

resource "aws_route53_record" "prod_env_A" {
  zone_id = "${aws_route53_zone.zone_for_prod.zone_id}"
  name    = "${aws_route53_zone.zone_for_prod.name}"
  type    = "A"
  alias {
    name                   = "${module.main.main-lb.dns_name}"
    zone_id                = "${module.main.main-lb.zone_id}"
    evaluate_target_health = false
  }
}

resource "aws_route53_record" "prod_env_MX" {
  zone_id = "${aws_route53_zone.zone_for_prod.zone_id}"
  name    = "${aws_route53_zone.zone_for_prod.name}"
  type    = "MX"
  records = [
    "10 ${aws_route53_record.prod_env_mail_A.name}",
  ]
  ttl = 300
}

resource "aws_route53_record" "prod_env_TXT" {
  zone_id = "${aws_route53_zone.zone_for_prod.zone_id}"
  name    = "${aws_route53_zone.zone_for_prod.name}"
  type    = "TXT"
  records = [
    "v=spf1 +mx +ip4:***.***.***.*** ~all"
  ]
  ttl = 300
}

resource "aws_route53_record" "prod_env_mail_A" {
  zone_id = "${aws_route53_zone.zone_for_prod.zone_id}"
  name    = "mail.${aws_route53_zone.zone_for_prod.name}"
  type    = "A"
  records = [
    "***.***.***.***"
  ]
  ttl = 300
}

resource "aws_route53_record" "prod_env_cdn_A" {
  zone_id = "${aws_route53_zone.zone_for_prod.zone_id}"
  name    = "img.${aws_route53_zone.zone_for_prod.name}"
  type    = "A"

  alias {
    name                   = "${aws_cloudfront_distribution.cdn.domain_name}"
    zone_id                = "${aws_cloudfront_distribution.cdn.hosted_zone_id}"
    evaluate_target_health = false
  }
}

resource "aws_route53_record" "test_env_A" {
  zone_id = "${aws_route53_zone.zone_for_prod.zone_id}"
  name    = "test.${aws_route53_zone.zone_for_prod.name}"
  type    = "A"
  alias {
    name                   = "${module.main.main-lb.dns_name}"
    zone_id                = "${module.main.main-lb.zone_id}"
    evaluate_target_health = false
  }
}

resource "aws_route53_record" "test_env_MX" {
  zone_id = "${aws_route53_zone.zone_for_prod.zone_id}"
  name    = "${aws_route53_record.test_env_A.name}"
  type    = "MX"
  records = [
    "10 ${aws_route53_record.prod_env_mail_A.name}",
  ]
  ttl = 300
}
```
***はIPなので伏せてます。


###手順3. S3バケットの作成 のソース
```terraform:/terraform/anken/s3.tf
resource "aws_s3_bucket" "b" {
  bucket = "${var.product_name}"
}

resource "aws_s3_bucket_policy" "b" {
  bucket = "${aws_s3_bucket.b.id}"
  policy = jsonencode(
    {
      Id = "PolicyForCloudFrontPrivateContent"
      Statement = [
        {
          Action = "s3:GetObject"
          Effect = "Allow"
          Principal = {
            AWS = "${aws_cloudfront_origin_access_identity.origin_access.iam_arn}"
          }
          Resource = "${aws_s3_bucket.b.arn}/*"
          Sid      = "1"
        },
      ]
      Version = "2008-10-17"
    }
  )
}

```
バケットとcloudfrontからのアクセスポリシーを定義します。

###手順4. SSL証明書の取得 のソース
```terraform/terraform/anken/acm.tf
resource "aws_acm_certificate" "cert" {
  domain_name               = "*.${var.domain}"
  subject_alternative_names = ["${var.domain}"]
  validation_method         = "DNS"
  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_route53_record" "cert_validation" {
  name    = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_name}"
  type    = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_type}"
  zone_id = "${aws_route53_zone.zone_for_prod.id}"
  records = ["${aws_acm_certificate.cert.domain_validation_options.0.resource_record_value}"]
  ttl     = 300
}

resource "aws_acm_certificate_validation" "cert" {
  certificate_arn = "${aws_acm_certificate.cert.arn}"

  validation_record_fqdns = [
    "${aws_route53_record.cert_validation.fqdn}",
  ]
}

# 証明書発行リクエスト
resource "aws_acm_certificate" "cdn_cert" {
  domain_name       = "img.${var.domain}"
  validation_method = "DNS"
  provider          = "aws.virginia"

  lifecycle {
    create_before_destroy = true
  }
}

# DNSによる検証用レコードの登録
resource "aws_route53_record" "cdn_cert_validation" {
  name    = "${aws_acm_certificate.cdn_cert.domain_validation_options.0.resource_record_name}"
  type    = "${aws_acm_certificate.cdn_cert.domain_validation_options.0.resource_record_type}"
  zone_id = "${aws_route53_zone.zone_for_prod.id}"
  records = ["${aws_acm_certificate.cdn_cert.domain_validation_options.0.resource_record_value}"]
  ttl     = 300
}

# 検証
resource "aws_acm_certificate_validation" "cdn_cert" {
  certificate_arn         = "${aws_acm_certificate.cdn_cert.arn}"
  validation_record_fqdns = ["${aws_route53_record.cdn_cert_validation.fqdn}"]
  provider                = "aws.virginia"
}

```
providerでリージョンを明確に定義することでterraformがそのリージョンで動いてくれます。


###手順5. Cloudfrontの作成 のソース
```terraform:/terraform/anken/cloudfront.tf
# Origin Accessも作ります！
resource "aws_cloudfront_origin_access_identity" "origin_access" {

}

resource "aws_cloudfront_distribution" "cdn" {
  aliases = ["${aws_acm_certificate.cdn_cert.domain_name}"]

  default_cache_behavior {
    allowed_methods = ["GET", "HEAD"]
    cached_methods  = ["GET", "HEAD"]
    compress        = "false"
    default_ttl     = "86400"

    forwarded_values {
      cookies {
        forward = "none"
      }

      query_string = "false"
    }

    max_ttl                = "31536000"
    min_ttl                = "0"
    smooth_streaming       = "false"
    target_origin_id       = "S3-${aws_s3_bucket.b.id}"
    trusted_signers        = ["self"]
    viewer_protocol_policy = "allow-all"
  }

  enabled         = "true"
  http_version    = "http1.1"
  is_ipv6_enabled = "false"

  logging_config {
    bucket          = "${aws_s3_bucket.b.bucket_domain_name}"
    include_cookies = "false"
    prefix          = "logs/"
  }

  origin {
    origin_id   = "S3-${aws_s3_bucket.b.id}"
    domain_name = "${aws_s3_bucket.b.bucket_domain_name}"
    s3_origin_config {
      origin_access_identity = "${aws_cloudfront_origin_access_identity.origin_access.cloudfront_access_identity_path}"
    }

  }

  price_class = "PriceClass_All"

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  retain_on_delete = "false"

  viewer_certificate {
    acm_certificate_arn            = "${aws_acm_certificate.cdn_cert.arn}"
    cloudfront_default_certificate = "false"
    minimum_protocol_version       = "TLSv1.1_2016"
    ssl_support_method             = "sni-only"
  }
}

```
設定内容がちょっと恥ずかしい...

###手順6、7、8　のソース
（ec2でまとめたためファイルが1つなので...）

```terraform:/terraform/anken/ec2.tf
# ターゲットグループ作成
resource "aws_lb_target_group" "prod_group" {
  deregistration_delay = "300"

  health_check {
    enabled             = "true"
    healthy_threshold   = "5"
    interval            = "30"
    matcher             = "200"
    path                = "/"
    port                = "traffic-port"
    protocol            = "HTTP"
    timeout             = "5"
    unhealthy_threshold = "2"
  }

  name       = "${var.product_name}"
  port       = "80"
  protocol   = "HTTP"
  slow_start = "0"

  stickiness {
    cookie_duration = "86400"
    enabled         = "false"
    type            = "lb_cookie"
  }

  target_type = "instance"
  vpc_id      = "${module.main.vpc.id}"
}

# ターゲットグループにインスタンスを登録
resource "aws_alb_target_group_attachment" "ap_1_attachment" {
  target_group_arn = "${aws_lb_target_group.prod_group.arn}"
  target_id        = "${module.main.ap_1.id}"
  port             = 80
}

# ターゲットグループにインスタンスを登録
resource "aws_alb_target_group_attachment" "ap_2_attachment" {
  target_group_arn = "${aws_lb_target_group.prod_group.arn}"
  target_id        = "${module.main.ap_2.id}"
  port             = 80
}

# 80番ポートリクエスト時のルールを定義（443にリダイレクトさせています。）
resource "aws_lb_listener_rule" "rule_80" {
  action {
    order = "1"

    redirect {
      host        = "#{host}"
      path        = "/#{path}"
      port        = "443"
      protocol    = "HTTPS"
      query       = "#{query}"
      status_code = "HTTP_301"
    }

    type = "redirect"
  }

  condition {
    field  = "host-header"
    values = ["${var.domain}"]
  }

  listener_arn = "${module.main.main_lb_listener_80.arn}"
}

# 443リクエスト時にどのターゲットグループに振るかの設定
resource "aws_lb_listener_rule" "rule_443" {
  action {
    order            = "1"
    target_group_arn = "${aws_lb_target_group.prod_group.arn}"
    type             = "forward"
  }

  condition {
    field  = "host-header"
    values = ["${var.domain}"]
  }

  listener_arn = "${module.main.main_lb_listener_443.arn}"
}

# SSL証明書の登録
resource "aws_lb_listener_certificate" "cert" {
  listener_arn    = "${module.main.main_lb_listener_443.arn}"
  certificate_arn = "${aws_acm_certificate.cert.arn}"
}

```


#終わりに
Terraformは習うより慣れろだと思いました。
この複雑なJSONを見るだけで頭がくらくらするし、説明を見てもよくわからないんですよね。

また、これらをコピペすれば多分動きますが、ファイルごとにリソースの参照があるため全部持って行かないとまず動かないと思います。
部分的にでも参考になる部分があると嬉しいです。

それでは。