OACでのCloudFrontからS3の接続+Lambda@Edgeでの認証をTerraformで作成してみた

2023.04.13

こんにちは、ゲームソリューショングループのsoraです。
今回は、OACでのCloudFrontからS3の接続+Lambda@Edgeでの認証をTerraformで作成してみたことについて書いていきます。

構成

CloudFrontでアクセスを受けると、Lambda@Edgeで認証して、認証が通ればS3にアクセスできるという構成です。

フォルダ構成は以下です。

$ tree
.
├── front
│   └── front.html
├── lambda
│   └── lambda.py
└── main.tf

AWSサービスの作成

タイトルにある通り、Terraformを使ってAWS側で必要なサービスを作成します。

解説もコード内のコメントにある程度は記載しています。
特にLambda@Edgeを使用できるリージョンが決まっていること(12-13行目)と、S3に配置するHTMLファイルのcontent-typeを指定すること(25-26行目)に注意してください。

main.tf

terraform {
    #AWSプロバイダーのバージョン指定
    required_providers {
        aws = {
            source  = "hashicorp/aws"
            version = "~> 4.51.0"
        }
    }
}
#AWSプロバイダーの定義
provider aws {
    #Lambda@Edgeがバージニアリージョンでのみ使用可能なため
    region = "us-east-1"
}

#S3
resource aws_s3_bucket origin_contents {
    bucket = "cloudfront-origin-contents"
}
#ファイルアップロード
resource aws_s3_object object {
    bucket = aws_s3_bucket.origin_contents.id
    key = "index.html"
    source = "front/front.html"
    #content-typeを指定しない場合、ページが表示されずにダウンロードになる場合があるため指定する
    content_type = "text/html"
}

#Lambda用IAMロールの信頼関係の定義
data aws_iam_policy_document assume_role {
    statement {
        effect = "Allow"
        principals {
            type = "Service"
            identifiers = [
                "lambda.amazonaws.com",
                "edgelambda.amazonaws.com"
            ]
        }
        actions = ["sts:AssumeRole"]
    }
}
#Lambda用IAMロールの作成
resource aws_iam_role iam_for_lambda {
    name               = "cloudfront_access_lambda"
    assume_role_policy = data.aws_iam_policy_document.assume_role.json
    inline_policy {
        name = "my_inline_policy"
        policy = jsonencode({
            Version = "2012-10-17"
            Statement = [
                {
                    Action   = [
                        "lambda:InvokeFunction",
                        "lambda:GetFunction",
                        "lambda:EnableReplication",
                        "cloudfront:UpdateDistribution"
                    ]
                    Effect   = "Allow"
                    Resource = "*"
                },
            ]
        })
    }
}
data archive_file lambda {
    type        = "zip"
    source_file = "lambda/lambda.py"
    output_path = "lambda_handler.zip"
}
resource aws_lambda_function lambda {
    filename      = "lambda_handler.zip"
    function_name = "IPAuth"
    role          = aws_iam_role.iam_for_lambda.arn
    handler       = "lambda.lambda_handler"
    source_code_hash = data.archive_file.lambda.output_base64sha256
    runtime = "python3.8"
}

#CloudFrontディストリビューション
resource aws_cloudfront_distribution cf_distribution {
    enabled = true
    default_root_object = "index.html"
    #オリジンの設定
    origin {
        domain_name = aws_s3_bucket.origin_contents.bucket_regional_domain_name
        origin_id = aws_s3_bucket.origin_contents.id
        origin_access_control_id = aws_cloudfront_origin_access_control.main.id
    }
    viewer_certificate {
        cloudfront_default_certificate = true
    }
    #キャッシュの設定
    default_cache_behavior {
        target_origin_id       = aws_s3_bucket.origin_contents.id
        viewer_protocol_policy = "redirect-to-https"
        cached_methods         = ["GET", "HEAD"]
        allowed_methods        = ["GET", "HEAD"]
        forwarded_values {
            query_string = false
            headers      = []
            cookies {
                forward = "none"
            }
        }
        #ビューワーリクエストにLambdaを設定する
        lambda_function_association {
            event_type   = "viewer-request"
            lambda_arn   = aws_lambda_function.lambda.qualified_arn
            include_body = false
        }
    }
    #国ごとのコンテンツ制限がある場合はここで設定(今回はなし)
    restrictions {
        geo_restriction {
            restriction_type = "none"
        }
    }
}
# OACを作成
resource aws_cloudfront_origin_access_control main {
    name                              = "cloudfront-oac"
    origin_access_control_origin_type = "s3"
    signing_behavior                  = "always"
    signing_protocol                  = "sigv4"
}

#S3バケットポリシー(OACのみから許可する)
##ポリシーの定義
data aws_iam_policy_document allow_access_from_cloudfront {
    statement {
        principals {
            type        = "Service"
            identifiers = ["cloudfront.amazonaws.com"]
        }
        actions = [
            "s3:GetObject"
            ]
        resources = [
            "${aws_s3_bucket.origin_contents.arn}/*"
        ]
        condition {
            test     = "StringEquals"
            variable = "AWS:SourceArn"
            values   = [aws_cloudfront_distribution.cf_distribution.arn]
        }
    }
}
##バケットポリシーのアタッチ
resource aws_s3_bucket_policy allow_access_from_cloudfront {
    bucket = aws_s3_bucket.origin_contents.id
    policy = data.aws_iam_policy_document.allow_access_from_cloudfront.json
}

Lambdaで使用するコード(Python)

IPアドレスにより制限をかける関数としています。
今回はGoでなくPythonを使います。

以下のページをとても参考にさせていただきました。
CloudFrontとLambda@Edge ( Python3 )とS3で静的ページにIPアドレス制限とBasic認証を設定する

lambda.py

import base64

#自分のIPアドレス
ALLOW_IP = ["X.X.X.X"]

ERROR_RESPONSE_AUTH = {
    'status': '401',
    'statusDescription': 'Unauthorized',
    'body': 'Authentication Failed',
    'headers': {
            'www-authenticate': [
                {
                    'key':'WWW-Authenticate',
                    'value':'IP address fault'
                }
            ]
    }
}

def lambda_handler(event, context):
    request = event['Records'][0]['cf']['request']
    client_ip = request['clientIp']

    if client_ip in ALLOW_IP:
        return request
    else:
        return ERROR_RESPONSE_AUTH

静的ページとして表示させるHTMLも簡単に作成します。

front.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
    <h1>sora</h1>
    <h2>所属</h2>
    <p>営業統括本部 ゲームソリューショングループ ソリューションアーキテクト</p>
    <h2>今後ブログにしようと思っていること</h2>
    <ul>
        <li>CDK(Python)で出たエラー</li>
        <li>Goで色々作ってみた成果物</li>
        <li>構築したことのない構成やサービスをCDKで構築</li>
    </ul>
</body>
</html>

デプロイ

作成したTerraformのコードを実行して構築します。

terraform init
terraform apply

CloudFrontで提供されているドメイン名を確認して、アクセスするとページが表示されています。

最後に

今回は、OACでのCloudFrontからS3の接続+Lambda@Edgeでの認証をTerraformで作成してみたことを記事にしました。
どなたかの参考になると幸いです。