1
/
5

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 にもデフォルトルートオブジェクトを書き換えるプルリクエストが出てますね。

Wantedly, Inc.からお誘い
この話題に共感したら、メンバーと話してみませんか?
Wantedly, Inc.では一緒に働く仲間を募集しています
5 いいね!
5 いいね!

同じタグの記事

今週のランキング

白鳥 昇治さんにいいねを伝えよう
白鳥 昇治さんや会社があなたに興味を持つかも