Terraformを使いAWSにSSL通信と独自ドメインを使ったStatic websiteを構築する話し(GitHub Actionsもあるよ)

ざっくりしたいこと

Vue.js でできたStatic websiteを独自ドメイン with SSLで公開する

SSL証明書の発行、サブドメイン作成などもすべて、できる限りTerraformで作成する

使用技術:

・Vue.js

・Terraform(v0.12.26)

・GitHub Actions

その他実行環境:

  • aws-cli/2.0.19
  • Python/3.8.3
  • Linux/5.4.0–37-generic botocore/2.0.0dev23

Vue.js

Static websiteの作成

Terraform

前提: State用のS3は作成済み。ドメインはRoute53で取得済み。
Deployで作ったものはすべて壊せるようにしたい

S3,Cloud Front,ACM,Route53あたりをTerraformで触っていく。

以下のことをすべてTerraformで自動化する

S3:Privateで作成&Force Deleteの有効化

blog-terraform-aws-static-site-s3.tf
GitHub Gist: instantly share code, notes, and snippets.
]
resource "aws_s3_bucket" "site" {
  bucket = var.bucket_name
  acl    = "private"
  tags = {
    name = var.tag
  }
  force_destroy = true
  versioning {
    enabled = true
  }
}

resource "aws_s3_bucket_policy" "site" {
  bucket = aws_s3_bucket.site.id
  policy = data.aws_iam_policy_document.s3_site_policy.json
}

data "aws_iam_policy_document" "s3_site_policy" {
  statement {
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.site.arn}/*"]

    principals {
      type        = "AWS"
      identifiers = [aws_cloudfront_origin_access_identity.site.iam_arn]
    }
  }
}

CloudFront:作成した(S3,証明書,Domain)と紐づけていく、存在しないページはすべて/index.htmlにリダイレクト

blog-terraform-aws-static-site-cloud-front.tf
GitHub Gist: instantly share code, notes, and snippets.
locals {
  s3_origin_id = "s3-origin-${var.site_domain}"
}

resource "aws_cloudfront_origin_access_identity" "site" {
  comment = var.site_domain
}

resource "aws_cloudfront_distribution" "site" {
  tags = {
    name = var.tag
  }
  origin {

    domain_name = aws_s3_bucket.site.bucket_regional_domain_name
    origin_id   = local.s3_origin_id

    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.site.cloudfront_access_identity_path
    }
  }

  enabled             = true
  is_ipv6_enabled     = true
  comment             = var.site_domain
  default_root_object = "index.html"

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = local.s3_origin_id

    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  custom_error_response {
    error_caching_min_ttl = 3000
    error_code            = 404
    response_code         = 200
    response_page_path    = "/index.html"
  }

  custom_error_response {
    error_caching_min_ttl = 3000
    error_code            = 403
    response_code         = 200
    response_page_path    = "/index.html"
  }



  price_class = "PriceClass_200"

  # CloudFrontドメインの証明書を利用
  //viewer_certificate {
  //  cloudfront_default_certificate = true
  //}


  aliases = [var.site_domain]

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate_validation.acm_cert.certificate_arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1"
  }
}

Route53: 取得済みの独自ドメインから指定のサブドメインのHost Zoneを作成し、NSレコードを追加する。その後、作成したCloud Frontへ

blog-terraform-aws-static-site-dns.tf
GitHub Gist: instantly share code, notes, and snippets.
data "aws_route53_zone" "root_domain" {
  name = var.root_domain
}

resource "aws_route53_zone" "sub_domain" {
  name = var.site_domain
  tags = {
    name = var.tag
  }
}

resource "aws_route53_record" "root_domain" {
  depends_on      = [aws_route53_zone.sub_domain]
  allow_overwrite = true
  name            = var.site_domain
  ttl             = 30
  type            = "NS"
  zone_id         = data.aws_route53_zone.root_domain.zone_id

  records = [
    aws_route53_zone.sub_domain.name_servers.0,
    aws_route53_zone.sub_domain.name_servers.1,
    aws_route53_zone.sub_domain.name_servers.2,
    aws_route53_zone.sub_domain.name_servers.3,
  ]
}

resource "aws_route53_record" "sub_domain" {
  zone_id = aws_route53_zone.sub_domain.zone_id
  name    = aws_route53_zone.sub_domain.name
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.site.domain_name
    zone_id                = aws_cloudfront_distribution.site.hosted_zone_id
    evaluate_target_health = false
  }
}

ACM:ワイルドカード証明書を発行し、認証する

blog-terraform-aws-static-site-acm.tf
GitHub Gist: instantly share code, notes, and snippets.

resource "aws_acm_certificate" "acm_cert" {
  provider                  = aws.us-east-1
  domain_name               = var.root_domain
  subject_alternative_names = ["*.${var.root_domain}"]
  validation_method         = "DNS"

  lifecycle {
    create_before_destroy = true
  }

  tags = {
    name = var.tag
  }
}


resource "aws_route53_record" "cert_validation" {
  allow_overwrite = true
  zone_id         = data.aws_route53_zone.root_domain.id
  name            = aws_acm_certificate.acm_cert.domain_validation_options.0.resource_record_name
  type            = aws_acm_certificate.acm_cert.domain_validation_options.0.resource_record_type
  records         = [aws_acm_certificate.acm_cert.domain_validation_options.0.resource_record_value]
  ttl             = 60
}

resource "aws_route53_record" "cert_validation_alt" {
  allow_overwrite = true
  zone_id         = data.aws_route53_zone.root_domain.id
  name            = aws_acm_certificate.acm_cert.domain_validation_options.1.resource_record_name
  type            = aws_acm_certificate.acm_cert.domain_validation_options.1.resource_record_type
  records         = [aws_acm_certificate.acm_cert.domain_validation_options.1.resource_record_value]
  ttl             = 60
}



resource "aws_acm_certificate_validation" "acm_cert" {
  provider                = aws.us-east-1
  certificate_arn         = aws_acm_certificate.acm_cert.arn
  validation_record_fqdns = [aws_route53_record.cert_validation.fqdn, aws_route53_record.cert_validation_alt.fqdn]
}

完成物

GitHub - kooooohe/aws-static-site-terraform
Contribute to kooooohe/aws-static-site-terraform development by creating an account on GitHub.

Stateはローカルに保存するように作ってあるので、下記のようなファイルを作れば作成済みのS3に保存されるようになる

main.tf

```json
terraform {
  backend "s3" {
      bucket = "mybucket"
      key    = "path/to/my/key"
      region = "us-east-1"
      }
   }
```

特にModule化とかはしていないが、variables.tfの値を変更すればそれぞれのドメインで同等の環境を誰でも作ることができるようにはしてある。

GitHub Actions

S3のファイル更新とCloud Frontのキャッシュ削除をGitHub Actionsで行う。

※S3の更新からのLambda発火でinvalidation作成とかもやろうと思ったのだが、こっちのほうがシンプルなので採用

blog-terraform-aws-static-site-actions.yml
GitHub Gist: instantly share code, notes, and snippets.
name: Deploy Production

on:
  push:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [14.x]

    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: yarn install
    - run: yarn build

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1
    - run: aws s3 sync ./dist/ s3://${{ secrets.BUCKET_NAME }}/ --delete --exact-timestamps
    - run: aws cloudfront create-invalidation --distribution-id ${{ secrets.DISTRIBUTION_ID}} --paths "/*"