前提条件
- 第1回から第8回までの構築が完了していること
- フロントエンド(React、Vue、Angular等)のビルド環境があること
- こちらからDockerコンテナでの実行環境が準備できていること
フロントエンドのサンプルコードはこちらから
Terraform実行環境の起動
前回と同様に、Dockerコンテナ内で作業を進めます。
# コンテナを起動してbashに入る
docker-compose run --rm terraform
これ以降のコマンドは、すべてこのコンテナ内で実行します。
基礎知識
S3静的ウェブサイトホスティングとは
S3(Simple Storage Service)は、オブジェクトストレージサービスです。静的ウェブサイトホスティング機能を使用することで、HTML、CSS、JavaScript、画像などの静的ファイルをウェブサイトとして公開できます。
WEBアプリケーション開発者の視点から見ると
ローカル開発では一般的にdevサーバー(例えばlocalhost:3000
)で確認しますが、クライアントサイドで動作するアプリの場合、本番環境では静的ファイルをどこかにホスティングする必要があります。S3は、これらのファイルを保存し、Web Serverとして機能させることができるサービスです。
主な特徴
特徴 | 説明 |
---|---|
高可用性 | 99.999999999%(イレブンナイン)の耐久性 |
スケーラビリティ | 自動的にスケールし、トラフィック増加に対応 |
低コスト | 使用した分だけの従量課金 |
簡単な運用 | サーバー管理不要 |
CloudFrontとCDN
CloudFrontは、AWSのCDN(Content Delivery Network)サービスです。世界中のエッジロケーションにコンテンツをキャッシュし、ユーザーに一番近い場所から配信します。
なぜCloudFrontが必要か
課題 | CloudFrontによる解決 |
---|---|
S3は対象リージョンのみ | 世界中のエッジロケーションから配信 |
S3への直接アクセスはセキュリティリスク | OACにより、CloudFront経由のみアクセス可能 |
HTTPSの設定が複雑 | CloudFrontで簡単にHTTPS化 |
キャッシュ制御が困難 | 詳細なキャッシュポリシー設定可能 |
Origin Access Control (OAC)とは
OACは、CloudFrontからS3バケットへの安全なアクセスを提供する仕組みです。2022年に導入された新しい方式で、従来のOAI(Origin Access Identity)よりセキュアです。
OACの仕組み
- S3バケットのパブリックアクセスを完全にブロック
- CloudFrontにOACを設定
- S3バケットポリシーでOACからのアクセスのみ許可
- ユーザーはCloudFront経由でのみコンテンツにアクセス可能
今回作成するネットワーク構成
リソース | 説明 | 数量 |
---|---|---|
S3バケット | 静的ファイルを保存 | 1個 |
S3バケットポリシー | OACからのアクセスを許可 | 1個 |
CloudFrontディストリビューション | CDN配信 | 1個 |
CloudFront OAC | S3への安全なアクセス | 1個 |
CloudFront関数 | SPAのルーティング対応(オプション) | 1個 |
Terraformコードの実装
変数の追加
フロントエンド用の変数を追加します。
# terraform/variables.tf に追加
variable "frontend_domain_name" {
description = "フロントエンドのドメイン名(オプション)"
type = string
default = ""
}
variable "frontend_certificate_arn" {
description = "CloudFront用のSSL証明書ARN(us-east-1リージョン)"
type = string
default = ""
}
variable "enable_cloudfront_logging" {
description = "CloudFrontのアクセスログを有効にするか"
type = bool
default = false
}
variable "cloudfront_price_class" {
description = "CloudFrontの価格クラス"
type = string
default = "PriceClass_100" # 日本を指定してコスト削減
}
S3バケット
静的ファイルを保存するS3バケットを作成します。
# terraform/s3_frontend.tf
# フロントエンド用S3バケット
resource "aws_s3_bucket" "frontend" {
bucket = "${var.project_name}-${var.environment}-frontend"
force_destroy = true # 実際の運用では外してもいい
tags = {
Name = "${var.project_name}-${var.environment}-frontend"
}
}
# バケットのパブリックアクセスをブロック
resource "aws_s3_bucket_public_access_block" "frontend" {
bucket = aws_s3_bucket.frontend.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# バケットの暗号化設定
resource "aws_s3_bucket_server_side_encryption_configuration" "frontend" {
bucket = aws_s3_bucket.frontend.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
# バケットのバージョニング設定
resource "aws_s3_bucket_versioning" "frontend" {
bucket = aws_s3_bucket.frontend.id
versioning_configuration {
status = "Enabled"
}
}
# CloudFrontアクセスログ用バケット(オプション)
resource "aws_s3_bucket" "cloudfront_logs" {
count = var.enable_cloudfront_logging ? 1 : 0
bucket = "${var.project_name}-${var.environment}-cf-logs"
force_destroy = true # 実際の運用では外してもいい
tags = {
Name = "${var.project_name}-${var.environment}-cf-logs"
}
}
# Logバケットのライフサイクルルール
resource "aws_s3_bucket_lifecycle_configuration" "cloudfront_logs" {
count = var.enable_cloudfront_logging ? 1 : 0
bucket = aws_s3_bucket.cloudfront_logs[0].id
rule {
id = "delete-old-logs"
status = "Enabled"
expiration {
days = 30
}
}
}
S3バケット設定の説明
パラメータ | 設定値 | 設定理由 |
---|---|---|
bucket名 | プロジェクト名-環境名-frontend | グローバルで一意である必要があるため、識別しやすい命名規則を採用 |
block_public_acls | true | パブリックACLの設定を防ぎ、意図しない公開を防止 |
block_public_policy | true | パブリックなバケットポリシーの適用を防止 |
ignore_public_acls | true | 既存のパブリックACLを無視してセキュリティを強化 |
restrict_public_buckets | true | バケットのパブリックアクセスを完全に制限 |
sse_algorithm | AES256 | 保存時の暗号化でデータを保護 |
versioning status | Enabled | 誤削除や上書きからの復旧を可能にする |
CloudFront Origin Access Control
CloudFrontからS3への安全なアクセスを設定します。
# terraform/cloudfront.tf
# Origin Access Control
resource "aws_cloudfront_origin_access_control" "frontend" {
name = "${var.project_name}-${var.environment}-frontend-oac"
description = "OAC for ${var.project_name} frontend"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
OAC設定の説明
パラメータ | 設定値 | 設定理由 |
---|---|---|
origin_access_control_origin_type | s3 | S3オリジン用のOACであることを指定 |
signing_behavior | always | すべてのリクエストに署名を付与してセキュリティを確保 |
signing_protocol | sigv4 | AWS Signature Version 4を使用(最新かつ最もセキュア) |
CloudFrontディストリビューション
CDN配信の設定を行います。
# terraform/cloudfront.tf
# CloudFront Distribution
resource "aws_cloudfront_distribution" "frontend" {
enabled = true
is_ipv6_enabled = true
comment = "${var.project_name} frontend distribution"
default_root_object = "index.html"
price_class = var.cloudfront_price_class
# エイリアス設定(カスタムドメインを使用する場合)
aliases = var.frontend_domain_name != "" ? [var.frontend_domain_name] : []
# オリジン設定
origin {
domain_name = aws_s3_bucket.frontend.bucket_regional_domain_name
origin_access_control_id = aws_cloudfront_origin_access_control.frontend.id
origin_id = "S3-${aws_s3_bucket.frontend.id}"
}
# デフォルトキャッシュ動作
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-${aws_s3_bucket.frontend.id}"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
compress = true
}
# 追加のキャッシュ動作(静的アセット用)
ordered_cache_behavior {
path_pattern = "/static/*"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-${aws_s3_bucket.frontend.id}"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 86400
max_ttl = 31536000
compress = true
}
# SPAのルーティング対応
custom_error_response {
error_code = 404
response_code = 200
response_page_path = "/index.html"
}
custom_error_response {
error_code = 403
response_code = 200
response_page_path = "/index.html"
}
# 地理的制限(必要に応じて設定)
restrictions {
geo_restriction {
restriction_type = "none"
}
}
# SSL証明書設定
viewer_certificate {
cloudfront_default_certificate = var.frontend_certificate_arn == ""
acm_certificate_arn = var.frontend_certificate_arn != "" ? var.frontend_certificate_arn : null
ssl_support_method = var.frontend_certificate_arn != "" ? "sni-only" : null
minimum_protocol_version = "TLSv1.2_2021"
}
# ログ設定
dynamic "logging_config" {
for_each = var.enable_cloudfront_logging ? [1] : []
content {
include_cookies = false
bucket = aws_s3_bucket.cloudfront_logs[0].bucket_domain_name
prefix = "cloudfront/"
}
}
tags = {
Name = "${var.project_name}-${var.environment}-frontend-cf"
}
}
CloudFront設定の説明
カテゴリ | パラメータ | 設定値 | 設定理由 |
---|---|---|---|
基本設定 | enabled | true | ディストリビューションを有効化 |
is_ipv6_enabled | true | IPv6サポートを有効化して将来性を確保 | |
default_root_object | index.html | ルートアクセス時のデフォルトファイル | |
price_class | PriceClass_200 | コストと配信エリアのバランスを考慮 | |
キャッシュ設定 | viewer_protocol_policy | redirect-to-https | HTTPアクセスを自動的にHTTPSにリダイレクト |
compress | true | gzip圧縮を有効化して転送量を削減 | |
default_ttl | 3600(1時間) | 頻繁に更新されるHTMLファイル用の短めのキャッシュ | |
max_ttl(静的アセット) | 31536000(1年) | 変更頻度の低い静的ファイルは長期キャッシュ | |
SPA対応 | custom_error_response | 404→200 | クライアントサイドルーティングに対応 |
セキュリティ | minimum_protocol_version | TLSv1.2_2021 | 最新のセキュリティ基準に準拠 |
S3バケットポリシー
CloudFrontからのアクセスのみを許可するポリシーを設定します。
# terraform/s3_frontend.tf に追加
# S3バケットポリシー
resource "aws_s3_bucket_policy" "frontend" {
bucket = aws_s3_bucket.frontend.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowCloudFrontServicePrincipal"
Effect = "Allow"
Principal = {
Service = "cloudfront.amazonaws.com"
}
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.frontend.arn}/*"
Condition = {
StringEquals = {
"AWS:SourceArn" = aws_cloudfront_distribution.frontend.arn
}
}
}
]
})
}
バケットポリシー設定の説明
パラメータ | 設定値 | 設定理由 |
---|---|---|
Principal.Service | cloudfront.amazonaws.com | CloudFrontサービスプリンシパルを指定 |
Action | s3:GetObject | 読み取りのみ許可(最小権限の原則) |
Resource | バケット内の全オブジェクト(/*) | すべてのファイルへのアクセスを許可 |
Condition | AWS:SourceArn | 特定のCloudFrontディストリビューションからのみアクセスを許可 |
出力の追加
作成したリソースの情報を出力に追加します。
# terraform/outputs.tf に追加
# S3関連の出力
output "frontend_s3_bucket_name" {
description = "フロントエンド用S3バケット名"
value = aws_s3_bucket.frontend.id
}
output "frontend_s3_bucket_arn" {
description = "フロントエンド用S3バケットARN"
value = aws_s3_bucket.frontend.arn
}
output "frontend_s3_bucket_domain_name" {
description = "S3バケットのドメイン名"
value = aws_s3_bucket.frontend.bucket_domain_name
}
# CloudFront関連の出力
output "cloudfront_distribution_id" {
description = "CloudFrontディストリビューションID"
value = aws_cloudfront_distribution.frontend.id
}
output "cloudfront_distribution_arn" {
description = "CloudFrontディストリビューションARN"
value = aws_cloudfront_distribution.frontend.arn
}
output "cloudfront_distribution_domain_name" {
description = "CloudFrontディストリビューションのドメイン名"
value = aws_cloudfront_distribution.frontend.domain_name
}
output "cloudfront_oac_id" {
description = "CloudFront OAC ID"
value = aws_cloudfront_origin_access_control.frontend.id
}
リソースを作成
Terraformコンテナ内で以下のコマンドを実行します。
# terraformディレクトリへ移動
cd terraform
# 実行計画の確認
terraform plan
# リソースの作成
terraform apply
CloudFrontディストリビューションの作成には15〜20分程度かかります。
結果の確認
# S3バケット名の確認
terraform output frontend_s3_bucket_name
# CloudFrontドメイン名の確認(重要:アクセスURLとして使用)
terraform output cloudfront_distribution_domain_name
# CloudFrontディストリビューションIDの確認(デプロイ時に使用)
terraform output cloudfront_distribution_id
動作確認
AWS Management Consoleでの確認
S3 > バケット
作成されたS3バケットを選択し、以下を確認します。
カテゴリ | 項目 | 設定値 |
---|---|---|
アクセス許可 | バケットポリシー | CloudFrontからのアクセスのみ許可 |
パブリックアクセス | すべてブロック | |
プロパティ | バージョニング | 有効 |
暗号化 | AES256 |
CloudFront > ディストリビューション
作成されたディストリビューションを選択し、以下を確認します。
カテゴリ | 項目 | 設定値 |
---|---|---|
一般 | 料金クラス | 北米と欧州のみを使用 |
ディストリビューションドメイン名 | xxxxxx.cloudfront.net | |
オリジン | オリジンタイプ | S3 |
ビヘイビア | ビューワープロトコルポリシー | HTTP を HTTPS にリダイレクト |
初期コンテンツのアップロード
サンプルコードを使わない場合は、簡単なHTMLファイルをアップロードします。
# テスト用HTMLファイルの作成
cat > index.html << EOF
<!DOCTYPE html>
<html>
<head>
<title>AWS CloudFront Test</title>
</head>
<body>
<h1>CloudFront配信テスト</h1>
<p>S3 + CloudFrontの構築が完了しました!</p>
</body>
</html>
EOF
# S3にアップロード
aws s3 cp index.html s3://$(terraform output -raw frontend_s3_bucket_name)/
# CloudFrontのURLで確認
echo "https://$(terraform output -raw cloudfront_distribution_domain_name)"
ブラウザでCloudFrontのURLにアクセスし、HTMLが表示されることを確認します。
サンプルコードから実行
以下、サンプルコードのルートから実行
# 実行権限を付与
chmod +x scripts/deploy-frontend.sh
# 環境変数を設定
export S3_BUCKET_NAME=出力したS3バケット名
export CLOUDFRONT_DISTRIBUTION_ID=CloudFrontディストリビューションID
# デプロイ実行
./scripts/deploy-frontend.sh
キャッシュ
何にどのくらいのキャッシュを適用させるか、目安として以下をご確認ください。
もちろんプロジェクトにより全く違うこともあり得ますが…
ファイルタイプ | キャッシュ期間 | 理由 |
---|---|---|
HTML(index.html) | 5分 | 頻繁に更新される可能性があるため短時間 |
JavaScript/CSS(ハッシュ付き) | 1年 | ファイル名にハッシュが含まれるため長期間 |
画像/フォント | 1年 | 変更頻度が低いため長期間 |
JSON(manifest等) | 5分 | アプリケーション設定のため短時間 |
コスト管理
サービス | 項目 | 料金(東京リージョン) |
---|---|---|
S3 | ストレージ | $0.025/GB・月 |
リクエスト(GET) | $0.00037/1,000リクエスト | |
CloudFront | データ転送(インターネットへ) | $0.114/GB(最初の10TB) |
HTTPリクエスト | $0.0075/10,000リクエスト | |
無効化リクエスト | 月1,000パスまで無料 |
トラブルシューティング
アクセスが403エラーになる場合
- S3バケットポリシーが正しく設定されているか確認
- CloudFront OACのIDが正しいか確認
キャッシュが更新されない場合
- CloudFrontの無効化を実行
- ブラウザのキャッシュをクリア
- Cache-Controlヘッダーの設定を確認
既存のロググループが原因でapplyできない
なんらかの理由でロググループが重複すると言うエラーが発生したりしなかったり…
おそらく前回削除しきれていなかっただけと仮定し、既存のロググループをimportします
terraform import 'aws_cloudwatch_log_group.vpc_flow_logs[0]' '/aws/vpc/sample-sys-dev'
次のステップ
次回は、CORS設定とセキュリティ強化を行います。
- ALB Listener RuleのCORS対応
- セキュリティヘッダーの実装
- WAF(Web Application Firewall)の設定
- CSRF対策の実装