AWSWEB

[AWS]CloudFront(署名付きURL)+S3のコンテンツ配信をTerraformで構築してみた

この記事は約10分で読めます。

こんにちは。齋藤です。

タイトルのよくありそうなケースを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かパラメーターストアあたりで保管するのがベターなのかな。