Terraform で SPA を配信する HTTPS 静的サイトのインフラを構築する[Route53 + ACM + CloudFront + S3 + CloudFront Functions(Basic 認証 + α)]
Terraform で Vue.js や Rect などで作られた SPA サイトを独自ドメインで HTTPS 配信するためのインフラを構築していきます。
サンプルソースをリポジトリにまとめてみたのでこれを参考に紹介していきます。
サンプルリポジトリ
サンプルとなるソースをリポジトリにまとめてみたのでこれを参考に紹介していきます。
※本記事の内容と基本的に一緒ですが、一気に作るのでなく Route53 と ACM、CloudFront の3ディレクトリに分けてます。
その為 remote state を使用した構成になっています。因みに リソース名の定義が下手だったり、access_key などの扱いが雑ですので使ってみる場合は使いやすい形に直してください。
前提
- 独自ドメインでのアクセスを行うために、お名前.com などでドメインを購入してください。
- この記事ではお名前.com で
example.com
というドメインを購入していると想定でいます
- この記事ではお名前.com で
- アクセス URL は
example.com
になります。www.example.com
でのアクセスは想定していません。
Terraform
Terraform の Provider 定義
基本的には region
は ap-northeast-1
ですが、一部のリソース(ACM, CloudFunctions)の作成は us-east-1
で行うので、以下のように定義している想定です。
provider "aws" {
access_key = var.aws_access_key
secret_key = var.aws_secret_key
region = var.region
}
provider "aws" {
alias = "us_east"
access_key = var.aws_access_key
secret_key = var.aws_secret_key
region = "us-east-1"
}
Route53 に Terraform で ホストゾーンを作成
まず Route53 でホストゾーンを作成します。
作成するホストゾーン名はパラメータで指定します。
resource "aws_route53_zone" "main" {
name = var.zone_name
}
terraform apply
を実行し、AWS マネジメントコンソールなどでホストゾーンが作成されていることと、NS レコードが作成されていることを確認しましょう。
もし、ドメインを Route53 で購入または Route 53 に移管していないようであれば、この NS レコードをお名前.com などのドメインを購入したところで、Route53 に追加された NS レコードの値を使い、そのドメインに対する NS(ネームサーバー)の変更をしましょう。
Terraform で ACM による SSL 証明書を発行
Route53 へのホストゾーンの作成と必要であれば NS 変更が終わったら、ACM で SSL 証明書を発行します。
resource "aws_acm_certificate" "main_cert" {
provider = aws.us_east
domain_name = data.terraform_remote_state.route53.outputs.main_zone_name
# wwwありのドメインも対象にしたい場合は以下を追加、使用予定はないけどリダイレクトしやすい環境に移行などした時のことを考え一応入れとく
subject_alternative_names = ["www.${data.terraform_remote_state.route53.outputs.main_zone_name}"]
# DNSを使ったドメイン認証
validation_method = "DNS"
}
# DNSを使ったドメイン認証のためにRoute53にcnameレコードを作成
resource "aws_route53_record" "main_cnames" {
for_each = {
for dvo in aws_acm_certificate.main_cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = data.terraform_remote_state.route53.outputs.main_zone_id
name = each.value.name
records = [each.value.record]
type = each.value.type
ttl = 60
}
resource "aws_acm_certificate_validation" "main_cert_validation" {
provider = aws.us_east
certificate_arn = aws_acm_certificate.main_cert.arn
validation_record_fqdns = [for record in aws_route53_record.main_cnames : record.fqdn]
}
Terraform で CloudFront と OAI と S3 を作成
ここから一気に色々作成していきます。
Terraform で S3 を作成
# vueなど配信コンテンツ用のバケット
resource "aws_s3_bucket" "origin_bucket" {
bucket = local.domain_name
force_destroy = true
}
# CloudFront 以外からアクセスできる必要はないので、ACL をプライベートにします
resource "aws_s3_bucket_acl" "origin_bucket_acl" {
bucket = aws_s3_bucket.origin_bucket.id
acl = "private"
}
# CloudFront 以外からアクセスできる必要はないので、パブリックアクセスを全てオフにします
resource "aws_s3_bucket_public_access_block" "origin_bucket" {
bucket = aws_s3_bucket.origin_bucket.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# CloudFrontのアクセスログ用のバケット
resource "aws_s3_bucket" "logging_bucket" {
bucket = "cloudfront-logs.${local.domain_name}"
force_destroy = true
}
# パブリックアクセスを全てオフにします
resource "aws_s3_bucket_public_access_block" "logging_bucket" {
bucket = aws_s3_bucket.logging_bucket.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# ログのバケットのライフサイクルを設定します
# この辺はお好みで変更してください
resource "aws_s3_bucket_lifecycle_configuration" "logging_bucket_lifecycle_config" {
bucket = aws_s3_bucket.logging_bucket.bucket
rule {
id = "s3-cloudfront-logs-transitions"
status = "Enabled"
expiration {
days = 365
}
transition {
days = 90
storage_class = "STANDARD_IA"
}
transition {
days = 180
storage_class = "GLACIER"
}
}
}
Terraform で OAI (Origin Access Identity) を作成
パブリックアクセスでない S3 に CloudFront がアクセスできるようにするために、OAI を作成します
# OAIの作成
resource "aws_cloudfront_origin_access_identity" "oai" {
comment = "access-identity-ap-northeast-1-cloudfront-resource-periosprint-dev"
}
data "aws_iam_policy_document" "origin_access_identity_policy" {
statement {
sid = "OriginAccessIdentity"
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.origin_bucket.arn}/*"]
principals {
type = "AWS"
identifiers = [aws_cloudfront_origin_access_identity.oai.iam_arn]
}
}
}
# OAIを利用したCloudFrontからのアクセスを許可するポリシーをS3のバケットに設定
resource "aws_s3_bucket_policy" "origin_bucket_policy" {
bucket = aws_s3_bucket.origin_bucket.id
policy = data.aws_iam_policy_document.origin_access_identity_policy.json
}
Terraform で CloudFront を作成
# キャッシュポリシーの作成
resource "aws_cloudfront_cache_policy" "this" {
name = local.uppercase_domain_name
comment = local.uppercase_domain_name
# default_ttl と max_ttl min_ttl は開発環境なら0にしてキャッシュしない方が開発しやすいです
default_ttl = 86400
max_ttl = 31536000
min_ttl = 1
parameters_in_cache_key_and_forwarded_to_origin {
cookies_config {
cookie_behavior = "none"
}
headers_config {
header_behavior = "none"
}
query_strings_config {
query_string_behavior = "none"
}
enable_accept_encoding_brotli = true
enable_accept_encoding_gzip = true
}
}
# オリジンリクエストポリシーの作成
resource "aws_cloudfront_origin_request_policy" "this" {
name = local.uppercase_domain_name
comment = local.uppercase_domain_name
cookies_config {
cookie_behavior = "none"
}
headers_config {
header_behavior = "none"
}
query_strings_config {
query_string_behavior = "none"
}
}
resource "aws_cloudfront_distribution" "cfd" {
aliases = [local.domain_name]
comment = local.domain_name
enabled = true
is_ipv6_enabled = true
price_class = "PriceClass_200"
default_root_object = "index.html"
origin {
domain_name = aws_s3_bucket.origin_bucket.bucket_regional_domain_name
origin_id = local.domain_name
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.oai.cloudfront_access_identity_path
}
}
logging_config {
include_cookies = false
bucket = aws_s3_bucket.logging_bucket.bucket_domain_name
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = local.domain_name
# httpでアクセスされた場合、httpsにリダイレクト
viewer_protocol_policy = "redirect-to-https"
compress = true
cache_policy_id = aws_cloudfront_cache_policy.this.id
origin_request_policy_id = aws_cloudfront_origin_request_policy.this.id
}
custom_error_response {
error_caching_min_ttl = "300"
error_code = "403"
response_code = "200"
# Nuxt や Next.js で SSG を用いていて 404.html など専用ページがあれば置き換えてください。
response_page_path = "/index.html"
}
custom_error_response {
error_caching_min_ttl = "300"
error_code = "404"
response_code = "200"
# Nuxt や Next.js で SSG を用いていて 404.html など専用ページがあれば置き換えてください。
response_page_path = "/index.html"
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = data.terraform_remote_state.acm.outputs.acm_main_cert_arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2019"
}
}
Terraform で Route53 (A レコード) を作成
Route53 に A レコードを追加して、独自ドメインにアクセスされた場合、CloudFront にアクセスされるようにします
resource "aws_route53_record" "www" {
zone_id = local.zone_id
name = local.domain_name
type = "A"
alias {
name = aws_cloudfront_distribution.cfd.domain_name
zone_id = aws_cloudfront_distribution.cfd.hosted_zone_id
evaluate_target_health = false
}
}
おまけ (Basic 認証と URL の書き換え)
開発環境などアクセスされたくない場合に便利
Terraform で CloudFront Functions (Basic 認証用の処理) を作成
function handler(event) {
var request = event.request
var uri = request.uri
var headers = request.headers
// Basic 認証に用いる文字列(dXNlcjpwYXNzd29yZA==)の部分は shell などで以下のコマンドで生成します。
// echo -n user:password | base64
var basicAuth = 'Basic dXNlcjpwYXNzd29yZA=='
if (
typeof headers.authorization === 'undefined' ||
headers.authorization.value !== basicAuth
) {
return {
statusCode: 401,
statusDescription: 'Unauthorized',
headers: { 'www-authenticate': { value: 'Basic' } },
}
}
if (uri === '/') {
return request
}
if (uri.endsWith('/')) {
// リクエスト URL が https://hogehoge.com/foo/ の様に "/" で終わる場合は、この分岐に入ります。
// その際の扱いは要件によって変えてください。
// パターン1: Nuxt の例ですが、pages/bar/index.vue のような構成の場合は、
// リクエスト URL が https://hogehoge.com/bar/ であれば、
// https://hogehoge.com/bar/index.html にルーティングして欲しいので request.uri の末尾に index.html を付与します。
request.uri = request.uri.concat('index.html')
return request
// パターン2: "/" で終わる場合は "/" を削ってリダイレクト
return {
statusCode: 302,
statusDescription: 'Found',
headers: {
location: {
value: request.uri.slice(0, -1),
},
},
}
} else if (!uri.includes('.')) {
// ファイル名に "." が含まれていない = 拡張子がない場合、request.uri の末尾に ".html" をつける
request.uri = request.uri.concat('.html')
}
return request
}
Terraform で CloudFront Functions を CloudFront に設定
resource "aws_cloudfront_function" "basic_auth" {
name = "basic_auth"
runtime = "cloudfront-js-1.0"
comment = "basic_auth function"
publish = true
code = file("${path.module}/basic_auth/function.js")
}
resource "aws_cloudfront_distribution" "cfd" {
default_cache_behavior {
function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.basic_auth.arn
}
}
}
個人開発したサービス