CloudFrontによるプライベートコンテンツ配信をTerraform管理する

この記事はトリスタinsideで書かれた記事です。
現在トリスタinsideはBOOK☆WALKER Tech Blogに統合されました。

こんにちは、バックエンド開発グループのシゲタです。 普段はRailsを使って、ニコニコ漫画バックエンドのリプレイス、新規機能の開発に携わっています。

CloudFrontでは署名付きURLを発行することで、プライベートコンテンツの配信が可能になります。 これまでは、署名付きURLの発行にはrootユーザのみが作成できるCloudFrontのキーペアが必要でした。そのため、キーペア作成の際にはrootユーザにサインインして作業する、というような運用上推奨されない手段を取るほかありませんでした。

しかし、昨年10月にCloudFrontに追加されたTrusted Key Groups1により、IAMユーザによって作成されたキーグループで署名付きURLを発行できるようになりました。そしてこれまた先日にリリースされたAWS-Providerのv3.37.0で、このTrusted Key GroupsがTerraformからでも利用できるようになりました。これによって、IAMユーザのみでプライベートコンテンツを配信するための作業が完結するだけでなく、そのためのリソースを全てTerraformで管理できるようになった訳です。

そこで今回は、TerraformでTrusted Key Groupsを利用して、プライベートコンテンツを配信できるようなCloudFrontディストリビューションを作成してみようと思います。

必要な手順

以下はCloudFrontでプライベートコンテンツを配信するための手順になります。

  1. 署名に必要なキーペアの作成
  2. キーグループの作成
  3. オリジンアクセスアイデンティティの作成
  4. コンテンツのオリジンを作成
  5. コンテンツを配信するディストリビューションの作成
  6. 署名付きURLの発行

それでは1つずつ見ていきましょう。

1. 署名に必要なキーペアの作成

CloudFrontの署名付きURLの発行に必要な公開鍵、秘密鍵のキーペアを作成します。

openssl genrsa -out private_key.pem 2048
openssl rsa -pubout -in private_key.pem -out public_key.pem

2. キーグループの作成

ここからTerraformでの作業に移ります。先ほど作成した公開鍵をTerraformの管理下におきます。

resource "aws_cloudfront_public_key" "sample_public_key" {
  name        = "sample_public_key"
  encoded_key = <<KEY
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuT3wWXcGLvCBea4CWYyc
YJyzP2teEXkV/+zqJ5RjJZq1kErzyAJtZKDON04m4YlA7eUTs9OjHH28pV/12bEq
YzAGWxVv4xwU2yQxZwbUpckES+72mbxOpHj1NAxzP+Fz9H7ZbYJl0lprrWAjQ6B1
hGfL4/hllg6rPXbbv4kibzEOOgFMCTCCad6kx6tWv+E+FLaD8CA/p9Kmj3ImByqG
yz2w/I1BZ8BBdMFfewm8C0NlK1rMd34LLAPP2C8BpmNS21nQV7aZN42fnpaYNQVC
3lHxriz4rAXblxwmfIURvbDCz4DxQ3WJKx0G77JlE5jA9wqq3Mg/0zEJzawK2rbj
HwIDAQAB
-----END PUBLIC KEY-----
KEY
}

次にキーグループを作成します。ここで使用するaws_cloudfront_key_groupはAWS Providerのv3.36.0で追加されたリソースです。

resource "aws_cloudfront_key_group" "sample_key_group" {
  name  = "sample_key_group"
  # 前工程で作成した公開鍵をキーグループに追加
  items = [aws_cloudfront_public_key.sample_public_key.id]
}

3. オリジンアクセスアイデンティティを作成

オリジンアクセスアイデンティティ(OAI)とは、コンテンツのオリジンにアクセス制限を設けるために必要なCloudFrontの認証情報になります。オリジンへの直接参照を制限してCloudFrontからのみ参照できるようにしたい場合には、OAIを作成してディストリビューションに紐づける必要があります。

resource "aws_cloudfront_origin_access_identity" "sample_origin_access_identity" {
  comment = "sample_origin_access_identity"
}

4. コンテンツのオリジンを作成

コンテンツのオリジンを作成します。今回はオリジンにS3を使用します。

data "aws_iam_policy_document" "sample_bucket_access_policy" {
  statement {
    actions = [
      "s3:GetObject",
    ]
    # 前工程で作成したオリジンアクセスアイデンティティをidentifiersに追加することで
    # CloudFront経由でS3にアクセスできるようになる
    principals {
      type = "AWS"
      identifiers = [
        aws_cloudfront_origin_access_identity.sample_origin_access_identity.iam_arn,
      ]
    }
    resources = [
      "arn:aws:s3:::sample-bucket/*"
    ]
  }
}

resource "aws_s3_bucket" "sample_bucket" {
  bucket = "sample-bucket"
  acl    = "private"
  policy = data.aws_iam_policy_document.sample_bucket_access_policy.json
}

5. コンテンツを配信するディストリビューションの作成

最後にコンテンツを配信するCloudFrontのディストリビューションを作成し、これまで作成したリソースを関連づけていきます。

resource "aws_cloudfront_distribution" "sample_distribution" {
  origin {
    domain_name = aws_s3_bucket.sample_bucket.bucket_regional_domain_name
    origin_id = "SampleBucketOrigin"
    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.sample_origin_access_identity.cloudfront_access_identity_path
    }
  }

  default_cache_behavior {
    allowed_methods = ["HEAD", "GET"]
    cached_methods = ["HEAD", "GET"]
    target_origin_id = "SampleBucketOrigin"
    # v3.37.0で追加されたtrusted_key_groupsオプションです
    trusted_key_groups = [
      aws_cloudfront_key_group.sample_key_group.id
    ]

    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }
    viewer_protocol_policy = "allow-all"
  }
  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
  viewer_certificate {
    cloudfront_default_certificate = true
  }
  enabled         = true
  is_ipv6_enabled = true
}

ここまでの作業分をterraform applyしてリソースを作成すれば準備は完了です。

6. CloudFrontの署名付きURLを発行する

それでは署名付きURLを発行してみて正常にコンテンツにアクセスできるか確認してみましょう。

aws cloudfront sign --url https://dpkkhtyl9a6q4.cloudfront.net/animal_black_sheep_hitsuji.png \
  --key-pair-id K2FC8JKVLFUFDG \
  --private-key file://private_key.pem \
  --date-less-than 2021-5-10T13:30:00+09:00

https://dpkkhtyl9a6q4.cloudfront.net/sweets_shiroi_taiyaki_white.png?Expires=1620621000&Signature=uSFQDiFlhtvvg0C-iL8MAmjXBEB3V4coAduXuUbpwWEuf5PqancAj1GzjmsU38OAeIoGR-jeYyoqRg9jmvs6kxzSRVI7jnE6-c2n2hnun6H6nad70E--DywJrdcTpytj6pHNx1gTzltjA-HoHbEEfagDIS-LgW9Wv93vUxQXp1EmSSAnL~LsCa55sRcratB86a5bizz47vzX057MIkHu1CYQ5aRafwIYjTslRPDMmRxZm85nA4Yuw-QnEcgMnnJerBq2u197xlzRgkaOJH6twqz-TYzWkdzlECSkNno2mfyC-kq4ds1UYQUuLsnbE05IBrvl8eqOxIrs6wdz~YI4yg__&Key-Pair-Id=K2FC8JKVLFUFDG

発行されたURLにアクセスしてみると

f:id:bookwalker_developers:20210930223910p:plain

はい、無事にファイルが表示されました^ ^
これでCloudFront経由でプライベートコンテンツの配信ができるようになりました。

ここまでの作業分は、そのままお手元で試していただける形で以下に纏めたので、参考にしていただければ幸いです。 なお、S3バケット名は全世界でユニークである必要があるので、サンプルを試される際にはバケット名を適当な名前に置き換えて下さい。

terraform {
  required_version = "0.15.3"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.37.0"
    }
  }
  backend "s3" {
    bucket = "sample-tfstate"
    key    = "sample.tfstate"
    region = "ap-northeast-1"
  }
}
provider "aws" {
  region = "ap-northeast-1"
}

# CloudFront
resource "aws_cloudfront_public_key" "sample_public_key" {
  name        = "sample_public_key"
  encoded_key = <<KEY
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuT3wWXcGLvCBea4CWYyc
YJyzP2teEXkV/+zqJ5RjJZq1kErzyAJtZKDON04m4YlA7eUTs9OjHH28pV/12bEq
YzAGWxVv4xwU2yQxZwbUpckES+72mbxOpHj1NAxzP+Fz9H7ZbYJl0lprrWAjQ6B1
hGfL4/hllg6rPXbbv4kibzEOOgFMCTCCad6kx6tWv+E+FLaD8CA/p9Kmj3ImByqG
yz2w/I1BZ8BBdMFfewm8C0NlK1rMd34LLAPP2C8BpmNS21nQV7aZN42fnpaYNQVC
3lHxriz4rAXblxwmfIURvbDCz4DxQ3WJKx0G77JlE5jA9wqq3Mg/0zEJzawK2rbj
HwIDAQAB
-----END PUBLIC KEY-----
KEY
}

resource "aws_cloudfront_key_group" "sample_key_group" {
  name  = "sample_key_group"
  items = [aws_cloudfront_public_key.sample_public_key.id]
}

resource "aws_cloudfront_origin_access_identity" "sample_origin_access_identity" {}

resource "aws_cloudfront_distribution" "sample_distribution" {
  origin {
    domain_name = aws_s3_bucket.sample_bucket.bucket_regional_domain_name
    origin_id = "SampleBucketOrigin"
    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.sample_origin_access_identity.cloudfront_access_identity_path
    }
  }
  default_cache_behavior {
    allowed_methods = ["HEAD", "GET"]
    cached_methods = ["HEAD", "GET"]
    target_origin_id = "SampleBucketOrigin"
    trusted_key_groups = [
      aws_cloudfront_key_group.sample_key_group.id
    ]
    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }
    viewer_protocol_policy = "allow-all"
  }
  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
  viewer_certificate {
    cloudfront_default_certificate = true
  }
  enabled         = true
  is_ipv6_enabled = true
}

# S3
data "aws_iam_policy_document" "sample_bucket_access_policy" {
  statement {
    actions = [
      "s3:GetObject",
    ]
    principals {
      type = "AWS"
      identifiers = [
        aws_cloudfront_origin_access_identity.sample_origin_access_identity.iam_arn,
      ]
    }
    resources = [
      "arn:aws:s3:::sample-bucket/*"
    ]
  }
}

resource "aws_s3_bucket" "sample_bucket" {
  bucket = "sample-bucket"
  acl    = "private"
  policy = data.aws_iam_policy_document.sample_bucket_access_policy.json
}

まとめ

Trusted Key Groupsの登場により、IAMユーザーで署名に必要なキーグループの作成ができるようになりました。 さらに、Terraformがこれをサポートしているので、署名付きURLの発行に必要なCloudFrontリソースをTerraformで一元管理できます。冒頭でも書いた通り、rootユーザでの作業は最小限に収めるべきなので、CloudFrontでプライベートコンテンツ配信を行う際にはTrusted Key Groupsを積極的に利用していきたいですね。


  1. Trusted Key Groupsについての概要や、後述するキーペアの作成、署名付きURL発行のコマンドはこちらを参考にさせていただきました。