こんにちは。齋藤です。
タイトルのよくありそうなケースをTerraformで構築して検証してみました。
実現したい構成
環境
# tfenv list
* 1.3.8
"registry.terraform.io/hashicorp/aws" {
version = "4.54.0"
...
}
要件・用意するもの
まず、Terraformで構築する前に必要な要件や手順を書きます。
- CloudFrontに登録するキーペア(秘密鍵、公開鍵)を作成する
- S3はパブリックアクセスブロックする
- S3はCloudFrontのアクセスのみ許可する(OAI or OAC)
- ドメインはCloudFrontのデフォルトを使用する
- aws cliで署名付きURLを発行する(手順簡略化のため)
構築
まず最初にキーペアを作成します。
# 秘密鍵
openssl genrsa -out cf_presigned_url_private.pem 2048
# 公開鍵
openssl rsa -pubout -in cf_presigned_url_private.pem -out cf_presigned_url_public.pem
鍵が準備できたらTerraformのコードを書きます。
CloudFrontディストリビューションを作成するために下記のファイルを作成します。
# Managed Cache Policy
data "aws_cloudfront_origin_request_policy" "this" {
name = "Managed-CORS-S3Origin"
}
data "aws_cloudfront_cache_policy" "this" {
name = "Managed-CachingOptimized"
}
# CloudFront S3 image bucket
resource "aws_cloudfront_distribution" "image" {
# managed cache policyを利用する場合に指定する
depends_on = [
data.aws_cloudfront_origin_request_policy.this,
data.aws_cloudfront_cache_policy.this
]
origin {
domain_name = aws_s3_bucket.this.bucket_regional_domain_name
origin_id = aws_s3_bucket.this.id
s3_origin_config {
// CloudFrontからのアクセスのみ許可する
origin_access_identity = aws_cloudfront_origin_access_identity.image.cloudfront_access_identity_path
}
}
enabled = true
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = aws_s3_bucket.this.id
origin_request_policy_id = data.aws_cloudfront_origin_request_policy.this.id
cache_policy_id = data.aws_cloudfront_cache_policy.this.id
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
trusted_key_groups = [aws_cloudfront_key_group.this.id]
}
restrictions {
geo_restriction {
restriction_type = "whitelist"
locations = ["JP"]
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
}
# CloudFront origin access id
resource "aws_cloudfront_origin_access_identity" "image" {}
# CloudFront public key
resource "aws_cloudfront_public_key" "this" {
encoded_key = file("cf_presigned_url_public.pem")
name = "s3-public-key"
}
# CloudFront key group
resource "aws_cloudfront_key_group" "this" {
items = [aws_cloudfront_public_key.this.id]
name = "s3-key-group"
}
次にS3側のコードも記載します。
# S3 image bucket
resource "aws_s3_bucket" "this" {
bucket_prefix = "image"
}
# S3 acl
resource "aws_s3_bucket_acl" "this" {
bucket = aws_s3_bucket.this.id
acl = "private"
}
# S3 public access block
resource "aws_s3_bucket_public_access_block" "this" {
bucket = aws_s3_bucket.this.id
block_public_acls = true
block_public_policy = true
restrict_public_buckets = true
ignore_public_acls = true
}
# S3 bucket policy
resource "aws_s3_bucket_policy" "this" {
bucket = aws_s3_bucket.this.id
policy = data.aws_iam_policy_document.this.json
}
# CloudFrontからのアクセスを許可するポリシー作成
data "aws_iam_policy_document" "this" {
statement {
sid = "Allow CloudFront"
effect = "Allow"
principals {
type = "AWS"
identifiers = [aws_cloudfront_origin_access_identity.image.iam_arn]
}
actions = [
"s3:GetObject"
]
resources = [
"${aws_s3_bucket.this.arn}/*"
]
}
}
# S3 versioning(検証用のためバージョニングなしにする)
resource "aws_s3_bucket_versioning" "versioning_example" {
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = "Disabled"
}
}
最後にoutputの定義をします。
output "cloud_front_distribution_domain_name" {
value = aws_cloudfront_distribution.image.domain_name
}
output "cloud_front_distribution_public_key" {
value = aws_cloudfront_public_key.this.id
}
リソースを用意する
ここまでできたら、Terraformを実行します。
$ terraform init
$ terraform apply
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
cloud_front_distribution_domain_name = "d1da0r53cnj0cr.cloudfront.net"
cloud_front_distribution_public_key = xxxxxxxxxxxxxxxx
画像を表示する
ここまでできたら、適当な画像をS3にアップロードして検証します。
まずは、S3の画像に直接アクセスしてみます。
意図した通り、アクセスが拒否されました。
次はcloudfrontから画像を表示してみます。
今度は、キーペアが見つからないという理由でアクセス拒否されました。これも意図した通りです。
最後に署名付きURLを発行して画像を表示してみます。
aws cliでterraform outputのcloudfront url+画像ファイル名とキーペアIDを指定して、残りは秘密鍵のパスとURL有効期限をパラメーターに与えます。
$ aws cloudfront sign \
--url https://d1da0r53cnj0cr.cloudfront.net/test.png \
--key-pair-id xxxxxxxxxxxxx \
--private-key file://~/.ssh/cf_presigned_url_private.pem \
--date-less-than 2023-02-13T10:45:00+09:00
https://d1da0r53cnj0cr.cloudfront.net/test.png?Expires=1676252700&Signature=K089HpJNF3FbqaovsubtT9-VVYo0TWpz3YJUAQq7MjB2myiFs~Tw-MYhcc1Y58pTELZWJnZv0H-VjR3PqMCpBPRXAvPNBrIDJ29sTDMI044K2V8Y4lnBJgzjgv3TfI~VUoEMACb3-6hpEE6VgBwzHwyV0WToGtulSSnd8zv5sxpC8sH9JLdDG7Xaf0LPi9sA61Yk9jvMks5cmk9Sok9yHeM5wsiRjW90TwiSru8lghY5bw2WjmKlsPZvnXhvPCCqR~tuWPw~zxftLBp6UueXdas7U8JePu12Nr9pHfnx1qt-zOQRXYWK28LbbUMLUkEVr9Deaqym18ReXdojflMaCw__&Key-Pair-Id=K3VHA20U71DJ05
正しいパラメーターだとURLが返ります。このURLにブラウザでアクセスします。
S3にあげたイラスト屋の画像が表示されました。有効期限が過ぎるとアクセス拒否されます。
最後に
Terraformでの構築自体は難しくないけど、実際のアプリケーションだと鍵を管理するための手法が必要になりそうです。
EC2,ECS,Lambdaに直接鍵を持たせるわけにはいかないので、SecretsManagerかパラメーターストアあたりで保管するのがベターなのかな。