GitHub 認証付き静的 Web サイトホスティングを CloudFront と Lambda@Edge で作る
Photo by Raphael Lopes on Unsplash
社内におけるファイルの共有は Google ドライブを使うことが多いのですが、CI の coverage レポートや Storybook といった、社内向け静的コンテンツのホスティングにもチラホラ需要がありました。
認証なしの静的 Web サイトホスティングであれば Amazon S3 でシュッと作れますが、認証付きとなると少し面倒です。また認証もメンバーの入れ替わりに適切に対応するべく、Basic Auth などではなく社内で利用している認証プロバイダーに乗りたいところです。
調べたところ Widen/cloudfront-auth という OSS で CloudFront に GitHub 認証をかけつつコンテンツのホスティングができたので、そのメモを残しておきます。
CloudFront と Lambda@Edge
まず、Amazon S3 の静的 Web サイトホスティングでは独自の認証を入れ込むことはできません。そこで CloudFront へのリクエスト時に任意の Lambda 実行をインターセプトできる Lambda@Edge という AWS Lambda の拡張機能を使います。
参考
cloudfront-auth
Lambda@Edge に認証認可の関数を書くことで CloudFront に認証機能をもたせるのですが、その認証用の Lambda をサクッと用意できる OSS が Widen/cloudfront-auth です。
使い方はとても簡単で、スクリプトを実行して対話式に Lambda のパッケージ(ZIP) を生成できます。また、認証プロバイダは Google, Microsoft など複数対応されています。Wantedly では社内でよく使われている GitHub 認証を利用することにしました。
Lambda@Edge のパッケージビルド
今回は GitHub 認証を使うので、予め GitHub App を作って Client ID と Secret を控えておきましょう。
$ git clone https://github.com/Widen/cloudfront-auth.git
$ cd cloudfront-auth
$ ./build.sh
> cloudfront-auth@1.0.0 build /Users/shoji/Git/cloudfront-auth
> npm install && cd build && npm install && cd .. && node build/build.js
...
>: Authentication methods:
(1) Google
(2) Microsoft
(3) GitHub
(4) OKTA
(5) Auth0
(6) Centrify
(7) OKTA Native
Select an authentication method: 3
>>: Client ID: <Your GitHub App Client ID>
>>: Client Secret: <Your GitHub App Client ID>
>>: Redirect URI: https://<your hosting site domain>/callback
>>: Session Duration (hours): (0) 48
>>: Organization: wantedly
Done... created Lambda function distributions/cloudfront-auth-github-wantedly/cloudfront-auth-github-wantedly.zip
Terraform によるデプロイ
cloudfront-auth の Lambad 関数のデプロイは AWS SAM を使った方法が用意されていますが、Wantedly のインフラは Terraform で管理されているので、今回はこちらを書きます。
以下の Terraform では CloudFront, S3 , Lambda@Edge を設定しています。いくつか構築の際にハマったポイントを書いておきます。
- CloudFront に設定する Lambda@Edge は `us-east-1` リージョンである必要があります。
- CloudFront に設定する Lambda@Edge の ARN は Publish されたバージョンまでを含めないといけません。
- CloudFront に設定する Lambda@Edge の タイムアウトは CloudFront のリクエスターの制限である5秒以内である必要があります。
// Lambda は事前に S3 にアップロードした ZIP を Terraform でデプロイしています。
// ACM の設定、CloudFront のエイリアスの Route53 設定はここには含まれていません。
// <hosting bucket name> <Your alias> <Your amazon ACM arn> あたりは適当に読み替えてください。
provider "aws" {
region = "ap-northeast-1"
}
provider "aws" {
alias = "virginia"
region = "us-east-1"
}
resource "aws_lambda_function" "cloudfront-auth-lambda" {
// Lambda@Edge for CloudFront requires us-east-1 region Lambda
provider = aws.virginia
architectures = ["x86_64"]
description = "cloudfront-auth lambda for github wtd org."
function_name = "cloudfront-auth-github"
handler = "index.handler"
memory_size = "128"
package_type = "Zip"
s3_bucket = "lambda-archives-bucket-virginia"
s3_key = "cloudfront-auth-github-wantedly.zip"
reserved_concurrent_executions = "-1"
role = aws_iam_role.lambda-at-edge-execution-role.arn
runtime = "nodejs14.x"
publish = true
tags = {
"role" = "cloudfront-http-redirect"
}
// CloudFront request-viewer function maximum timeout is 5
timeout = "5"
tracing_config {
mode = "PassThrough"
}
}
// Lambda@Edge を動かすための IAM Policy
data "aws_iam_policy_document" "lambda-at-edge-assume-role-policy" {
statement {
actions = ["sts:AssumeRole"]
effect = "Allow"
principals {
identifiers = [
"edgelambda.amazonaws.com",
"lambda.amazonaws.com",
]
type = "Service"
}
}
}
resource "aws_iam_role" "lambda-at-edge-execution-role" {
assume_role_policy = data.aws_iam_policy_document.lambda-at-edge-assume-role-policy.json
max_session_duration = "3600"
name = "lambda-at-edge-execution-role"
path = "/service-role/"
}
resource "aws_iam_role_policy_attachment" "lambda-at-edge-execution-role-policy-attachment" {
policy_arn = aws_iam_policy.lambda-at-edge-execution-role.arn
role = aws_iam_role.lambda-at-edge-execution-role.name
}
data "aws_iam_policy_document" "lambda-at-edge-execution-policy" {
statement {
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
]
effect = "Allow"
resources = [
"arn:aws:logs:*:*:*"
]
}
}
resource "aws_iam_policy" "lambda-at-edge-execution-role" {
name = "lambda-at-edge-execution-policy"
path = "/service-role/"
policy = data.aws_iam_policy_document.lambda-at-edge-execution-policy.json
}
// コンテンツ配信用 S3 バケット
resource "aws_s3_bucket" "bucket" {
bucket = "<hosting bucket name>"
acl = "private"
cors_rule {
allowed_headers = ["*"]
allowed_methods = ["GET"]
allowed_origins = ["*"]
max_age_seconds = 3000
}
policy = data.aws_iam_policy_document.s3-policy.json
}
// コンテンツ配信用 CloudFront
resource "aws_cloudfront_origin_access_identity" "access-identify" {
comment = "access-identity-<hosting bucket name>.s3.amazonaws.com"
}
resource "aws_cloudfront_distribution" "cloudfront-with-github-auth" {
comment = "cloudfront-auth-github-wantedly"
aliases = [
"<Your alias>"
]
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
# AWS Managed-CachingOptimized Cache Policy
cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6"
cached_methods = ["GET", "HEAD"]
compress = "true"
default_ttl = "0"
lambda_function_association {
event_type = "viewer-request"
include_body = "false"
lambda_arn = "${aws_lambda_function.cloudfront-auth-lambda.arn}:${aws_lambda_function.cloudfront-auth-lambda.version}"
}
max_ttl = "0"
min_ttl = "0"
smooth_streaming = "false"
target_origin_id = "${aws_s3_bucket.bucket.id}.s3.ap-northeast-1.amazonaws.com"
viewer_protocol_policy = "https-only"
}
enabled = "true"
http_version = "http2"
is_ipv6_enabled = "true"
origin {
connection_attempts = "3"
connection_timeout = "10"
domain_name = "${aws_s3_bucket.bucket.id}.s3.ap-northeast-1.amazonaws.com"
origin_id = "${aws_s3_bucket.bucket.id}.s3.ap-northeast-1.amazonaws.com"
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.access-identify.cloudfront_access_identity_path
}
}
price_class = "PriceClass_100"
restrictions {
geo_restriction {
restriction_type = "none"
}
}
retain_on_delete = "false"
viewer_certificate {
// ACM of wantedly.com for CloudFront
acm_certificate_arn = "<Your amazon ACM arn>"
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1"
}
depends_on = [
aws_lambda_function.cloudfront-auth-lambda
]
}
data "aws_iam_policy_document" "s3-policy" {
statement {
principals {
type = "AWS"
identifiers = [aws_cloudfront_origin_access_identity.access-identity.iam_arn]
}
actions = ["s3:GetObject"]
resources = ["arn:aws:s3:::<hosting bucket name>/*"]
}
}
Note
CloudFront ではサブディレクトリのデフォルトルートオブジェクトは設定できません。AWS 公式からは 「Lambda@Edge を使って実装できるよ」というポストが出てますが、今回認証に Lambda@Edge を使っているので、cloudfront-auth のLambda 関数に同様の処理を付け加えてあげると実現できたりします。cloudfront-auth にもデフォルトルートオブジェクトを書き換えるプルリクエストが出てますね。
- Implementing Default Directory Indexes in Amazon S3-backed Amazon CloudFront Origins Using Lambda@Edge
- Rewrite default urls to ./index.html #61