0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「再現できる気がしない」を卒業する:TerraformでAWSポートフォリオを0から構築した

0
Posted at

はじめに

この記事はZennにも掲載しています。

AWSでポートフォリオサイトを作ったり、チャットボットを作ったりしてきたけど、「再現できる気がしない」という感覚が拭えなかった。

コンソールをポチポチしたり、Amplifyに丸投げしたりしていたから、なぜ動くかわからないまま動いていたという状態だった。

そこでTerraformを使って、S3 + CloudFront + ACM + Route53の構成を0からコードで書いて再現できるところまでやってみた。

とは言ってもAIに手伝ってもらいながらではあるが。


作った構成

CloudFront (xxxx.cloudfront.net)
    ↓ OAC(Origin Access Control)
S3(静的サイトホスティング)

ACM(SSL証明書 / us-east-1)
Route53(ホストゾーン / 学習用)

本来はRoute53のNSを使って独自ドメインまで繋げる設計だったが、Cloudflareでドメインを取得するとNS変更ができないため、今回はCloudFrontの標準ドメインで動作確認している。本来の設計については末尾に記載。


環境

  • Mac(Apple Silicon)
  • Terraform v1.15.2
  • AWS CLI 2.34.45
  • 作業ディレクトリ:~/terraform-portfolio

ステップ0:環境構築

# Terraformインストール
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

# AWS CLIインストール
brew install awscli

# 確認
terraform --version
aws --version

AWS CLIの認証設定:

aws configure
# Access Key ID、Secret Access Key、リージョン(ap-northeast-1)を入力

設定確認:

aws sts get-caller-identity

アカウント情報が返ってきたらOK。


ファイル構成

terraform-portfolio/
├── main.tf          # リソース定義
├── variables.tf     # 変数定義
├── .gitignore
├── .terraform.lock.hcl
├── index.html       # テスト用
└── README.md

variables.tf

variable "bucket_name" {
  description = "S3バケット名"
  default     = "exobrainlab-portfolio-tftest"
}

variable "domain_name" {
  description = "ドメイン名"
  default     = "exobrainlab.com"
}

variable "aws_region" {
  description = "AWSリージョン"
  default     = "ap-northeast-1"
}

main.tf

プロバイダー設定

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# 東京リージョン(メイン)
provider "aws" {
  region = var.aws_region
}

# バージニア北部(ACM用)
# CloudFrontはus-east-1の証明書しか使えない
provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

ACMをus-east-1で作る理由:CloudFrontはグローバルサービスで、証明書をus-east-1からしか取得できない仕様になっている。ここは初見で必ずハマるポイント。

S3

resource "aws_s3_bucket" "portfolio" {
  bucket        = var.bucket_name
  force_destroy = true  # terraform destroy時に中身ごと削除
}

# パブリックアクセスを全部ブロック(CloudFront経由のみ許可するため)
resource "aws_s3_bucket_public_access_block" "portfolio" {
  bucket = aws_s3_bucket.portfolio.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# 静的ウェブサイトホスティングの設定
resource "aws_s3_bucket_website_configuration" "portfolio" {
  bucket = aws_s3_bucket.portfolio.id

  index_document {
    suffix = "index.html"
  }

  error_document {
    key = "error.html"
  }
}

# CloudFrontだけがS3を読めるバケットポリシー
resource "aws_s3_bucket_policy" "portfolio" {
  bucket = aws_s3_bucket.portfolio.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowCloudFrontServicePrincipal"
        Effect = "Allow"
        Principal = {
          Service = "cloudfront.amazonaws.com"
        }
        Action   = "s3:GetObject"
        Resource = "${aws_s3_bucket.portfolio.arn}/*"
        Condition = {
          StringEquals = {
            "AWS:SourceArn" = aws_cloudfront_distribution.portfolio.arn
          }
        }
      }
    ]
  })
}

ACLには「作る」と「読む」の2段階がある。

設定 役割
block_public_acls 新規ACLをブロック
ignore_public_acls 既存ACLを無視
block_public_policy 新規ポリシーをブロック
restrict_public_buckets 既存ポリシーを無効化

4つ全部 true にするのが「完全にパブリックアクセスを遮断する」定石。

ACM

resource "aws_acm_certificate" "portfolio" {
  provider          = aws.us_east_1
  domain_name       = var.domain_name
  validation_method = "DNS"

  subject_alternative_names = [
    "www.${var.domain_name}"
  ]

  lifecycle {
    create_before_destroy = true
  }
}

Route53(学習用)

resource "aws_route53_zone" "portfolio" {
  name = var.domain_name
}

# ACMのDNS検証レコード
resource "aws_route53_record" "acm_validation" {
  for_each = {
    for dvo in aws_acm_certificate.portfolio.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  zone_id = aws_route53_zone.portfolio.zone_id
  name    = each.value.name
  type    = each.value.type
  records = [each.value.record]
  ttl     = 60
}

for dvo in ... はPythonの内包表記と同じ発想。dvoDomain Validation Option の略として慣習的に使われているが、変数名は何でもいい。

CloudFront

resource "aws_cloudfront_origin_access_control" "portfolio" {
  name                              = "${var.bucket_name}-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "portfolio" {
  enabled             = true
  default_root_object = "index.html"

  origin {
    domain_name              = aws_s3_bucket.portfolio.bucket_regional_domain_name
    origin_id                = "S3Origin"
    origin_access_control_id = aws_cloudfront_origin_access_control.portfolio.id
  }

  default_cache_behavior {
    target_origin_id       = "S3Origin"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]

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

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

デプロイ

terraform init    # プロバイダーのダウンロード
terraform plan    # 何が作られるか確認
terraform apply   # 実際に作成

terraform plan の読み方:

  • + 緑:新しく作られるリソース
  • ~ 黄:変更されるリソース
  • - 赤:削除されるリソース
  • Plan: N to add, N to change, N to destroy. で全体サマリー

動作確認

index.htmlをS3にアップロードして確認:

aws s3 cp ./index.html s3://exobrainlab-portfolio-tftest/

CloudFrontのURLを確認:

terraform state show aws_cloudfront_distribution.portfolio | grep domain_name

ブラウザでアクセスしてページが表示されればOK。


全部消して再現できるか確認

terraform destroy
aws s3 cp ./index.html s3://exobrainlab-portfolio-tftest/  # 再アップロード

再度アクセスして同じページが表示されれば「コードで管理できている」状態。

terraform destroy 前にS3を空にしておく必要がある。または force_destroy = true をS3バケットに設定しておくと terraform destroy 一発で中身ごと削除できる。


ハマったポイント

1. ACMはus-east-1固定

CloudFrontで使うACM証明書は必ずus-east-1で作る必要がある。東京リージョンで作っても使えない。

2. 全角文字混入

日本語入力モードでコードを書くとハイフンが全角()になる。us-east-1us-east-1 になってエラーになることがある。コードを書くときは常に半角モードで。

3. S3バケットが空でないと削除できない

terraform destroy 時にS3にファイルが残っていると BucketNotEmpty エラーになる。force_destroy = true を設定しておくか、先に aws s3 rm s3://バケット名/ --recursive で空にする。

4. CloudflareドメインはRoute53に移管できない

Cloudflareで取得したドメインはネームサーバーの変更が制限されているため、Route53への完全移管ができない。Route53のホストゾーンを作っても、CloudflareのNSが向いていない限り世界のDNSには影響しない。


.gitignore

.terraform/
terraform.tfstate
terraform.tfstate.backup
*.tfvars

terraform.tfstate にはAWSのリソースIDや設定の詳細が含まれるためGitHubには上げない。


本来の設計

CloudflareのNS変更ができる場合、または別のレジストラでドメインを取得している場合は以下の構成が完全版:

Route53(DNS)
    ↓ Aliasレコード
CloudFront + ACM(独自ドメイン + HTTPS)
    ↓ OAC
S3

追加で必要なリソース:

  • aws_acm_certificate_validation(証明書の検証完了を待つ)
  • aws_route53_record(apex と www の Alias レコード)
  • CloudFrontの aliases に独自ドメインを設定
  • viewer_certificate に ACM証明書のARNを設定

まとめ

コンソールのポチポチと違って、Terraformを使うと:

  • 構成がコードとして残る → GitHubで管理できる
  • 再現性があるterraform destroy して terraform apply すれば同じ環境が戻る
  • 差分が見えるterraform plan で何が変わるかを事前に確認できる

「再現できる気がしない」という感覚の正体は「構成が頭に入っていない」ことだった。AIに手伝ってもらいながらも、写経してTerraformで書くことで、何が存在していて、どう繋がっているかが腹に落ちる。理解できたという手応え、これは気持ちいい。自分が成長している実感あり。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?