CloudFront + S3 + Lambda@EdgeでBasic認証付きお手軽 静的サイト ( ランディングページ ) 環境を作った

大島 雅人
187

こんにちは。開発支援Gでインフラ運用をしている大島です。

Lambda@Edgeとは、CloudFrontが配信するコンテンツをカスタマイズする関数を実行できる機能のことです。本記事では、これを使ってランディングページ ( 以下LP ) のような静的サイトに対してお手軽にBasic認証を設置する話です。

また、最後におまけ程度ですが、この構成に到るまでの背景も書いていますのでもしよろしければご覧になっていってください。

LP環境をCloudFrontで作るのにBasic認証はどうしよう ?

開発中はデバッグ用の要素や社内開発環境へのリンクなども含まれていたりすることを考えると、何かしらの方法でアクセスを制限する必要があります。nginxなどのwebサーバであればBasic認証の設置も簡単ですが、今回はCloudFront + S3なのでそうそう気軽には設置できません。

そんなときに便利なのが本題でもある、Lambda@EdgeというCloudFrontでLambdaの処理を噛ませられるサービスです。こちらが本構成を使った時の処理の流れのシーケンス図です。

viewerはいつも通りCloudFrontへコンテンツを取得しにいきますが、その際にLambda@EdgeでBasic認証処理を行っています。

さらに、CloudFront -> S3もオリジンアクセスアイディンティティというものを使うことでS3をサイトホスティングしなくても繋がるようにしています。これによりS3に直接リクエストを投げられても弾くことが出来ます。

S3へのデプロイは、CircleCIで自動化します。GitHubの任意のブランチへマージされたタイミングでS3へのアップロード処理が走ります。これによりLPのデザイン、マークアップから本番公開までのフローを、エンジニアの工数を割くことなく完結できます。

Lambda@EdgeのBasic認証ってどんなことしてるの?

Lambda@Edgeといっても中身はただのLambdaです。CloudFrontのエッジに置かれるから『Edge』と呼ぶわけです。Lambdaのコードもいつも通りで、こんな風にリクエストを処理出来ます。

S3 + CloudFront + Lambda@Edge でBasic認証を参考にさせていただきました。ありがとうございます。

exports.handler = (event, context, callback) => {

    // リクエストヘッダを取得してる。このあとBase64とかとってBasic認証してあげる
    const request = event.Records[0].cf.request;
    const headers = request.headers;

あとはLambda@Edge、S3やCloudFrontなどの要素をTerraformでmodule化しておけば、importするだけで増やし放題です。

CloudFrontとLambda@Edgeを設定する部分

ここでは、CloudFrontにLambda@Edgeを指定する部分のterraformのコードを紹介します。

まず、CloudFrontにLambda@Edgeを設定する部分ですが、このようにlambda_function_associationを設定するだけです。たったこれだけ。

工夫するポイントとしては、var.lambda_function_associationsをlistで宣言しておくことです。例えば本番環境はBasic認証が不要などの場合に、moduleのimport時に何も渡さなければdefaultで空のlistが設定されるので無効化出来ます。

#
# module側のコード
#
resource "aws_cloudfront_distribution" "cf" {
// 中略
  default_cache_behavior = {
    // 中略

    // たったこれだけでできる
    lambda_function_association = "${var.lambda_function_associations}"
  }
}

// module側でこのように変数を宣言しておく
variable "lambda_function_associations" {
  type        = "list"
  default     = []
  description = "Lambda@Edgeを設定するための変数。リストで設定する。"
}

#
# moduleをimportする側のコード
#
module "demo_cloudfront" {
  source = "../module/cloudfront_s3"

  lambda_function_associations = [
    {
      event_type = "viewer-request"
      // 注意: lambda_arnはpublishされたものしか使えない
      // またlambda_arnをterraformのinterpolation経由で持ってこようとするとうまくいかないのでベタがきしている
      lambda_arn = "arn:aws:lambda:us-east-1:${var.account_id}:function:cloudfront_s3_basic_auth_dev:2"
    },
  ]
}

次にデプロイするLambda@Edgeの部分です。本当にただのLambdaなので特別なことはしていませんが、必ずvirginiaにデプロイする必要があります

// Lambda@Edgeは必ずvirginiaにdeployする必要がある
provider "aws" {
  region = "us-east-1"
  alias  = "virginia"
}

resource "aws_lambda_function" "basic_auth" {
  provider         = "aws.virginia"
  function_name    = "cloudfront_s3_basic_auth_${var.env}"
  role             = "${aws_iam_role.lambda.arn}"
  handler          = "cloudfront_s3_basic_auth.handler"
  memory_size      = 128
  publish          = true
  source_code_hash = "${base64sha256(file("${path.module}/lambda_source/cloudfront_s3_basic_auth.zip"))}"
  s3_bucket        = "${var.lambda_bucket_name}"
  s3_key           = "cloudfront_s3_basic_auth/cloudfront_s3_basic_auth.zip"
  runtime          = "nodejs6.10"
}

構築したときのちょっとしたハマりポイント

Lambda@Edgeのハマりどころ

Lambda@EdgeはCloudFrontと共に動くので、いくつか制限があります。要はエッジ(Edge)サーバーで動いているので重い処理や動的なコードはあまり使えないということです。以下のポイントは特につまづきやすいので、始める前に確認しておいてもらえるとよいと思います!

  • Lambdaは環境変数が使えない
  • us-east-1にLambdaを置かなくてはならない
  • S3もus-east-1にzipを置かなくてはならない
  • LambdaのLATESTが使えないので、publishしてversion番号を付けなくてはならない
  • iam_roleでlambda.amazonaws.comだけじゃなくedgelambda.amazonaws.comも必要

CloudFrontのハマりどころ

  • 設定変更してから反映されるまで1時間弱ぐらいかかることもあった
    • 自分の設定が悪いんだと思ってハマりまくってしまった…
  • キャッシュのInvalidationをするIAM policyを書くときにdistributionごとに設定できなかった

おまけ:LP環境とwebアプリケーション環境は分けておこう

本記事ではLP環境をCloudFrontで構築する話を紹介してきましたが、実際は紆余曲折があってこの構成にたどり着いています。誰かの参考になればと思い、背景を書いておきます。

もともとLP環境とwebアプリケーション環境は同じだった

弊社で運営しているスタディサプリENGLISHの場合ですが、豊富なアニメーションや英会話音声を再生するなど従来の教育系サービスとは一線を画するインタラクティブな要素が多いことから、webのフロントエンドはSPA(Single Page Application)で作られていました。本サービスをリリースした当初(2015年秋頃)はリリース優先ということもあり、LPとログイン後の英語学習を行うアプリケーション部分は同じSPAとして動いていました。

リリースから時を経てプロモーションが本格化してくると、「ちょっとこの部分の文言変えたい」、「xxxの広告コードをLP部分にだけ仕込みたい」、「集客用にいろいろなデザイン試したい」などの要望が出てきます。これはwebサービスを運用している会社ではよくあることだと思います。

当時はLPとwebアプリケーション部分がひとつのプロジェクト(リポジトリ)で運用されていたため、ちょっとした文言変更であってもエンジニアがwebフロントエンドアプリケーション部分のコードを修正してPRレビュー、マージをしてデプロイをするというフローになっていました。

マーケティング側はA/Bテストや広告の効果確認など自分たちのペースでどんどん回していきたい、一方でエンジニア側は品質を保つためにもそれ相応の確認手順を踏んでからでないとデプロイできないというジレンマに陥っていました。

LP環境の分離

そこで、2017年の夏にTOEIC対策講座をリリースする際に、LPとwebアプリケーションの分離を併せてて行いました。

構成としては、AWS S3にjsやcssなどをの静的リソースをアップロードしておき、S3にproxyするnginxをAWS ECS上でDockerコンテナとして起動させています。このnginxで、特定のURLはリダイレクトさせたり、開発環境やステージング環境用にbasic認証の設定などもしています。

さらに別のLPを作りたいという要望が来た

提供するサービスの数に比例するように、それぞれに対応したサブドメインやLP環境が欲しいという要望が挙がってくるようになってきました。

以前の構成で考えれば、同じようにS3にproxyするnginxをデプロイするためのECS serviceをたててデプロイすればいいかなと考えていたんですが、チームメンバーと話しているうちに、「CloudFront + S3でいいのではないか」という話になりました。

LP環境が今後も増え続けていくと仮定すると、ALBが増えたりインスタンス増やしたりクラスターのキャパシティの計算も考えないといけなくなってくるので、メンテ不要になるのは大きなメリットであると判断しました。

当時のSlackでのやりとりの様子

まとめ

弊社ではインフラエンジニアの数が少ないので、なるべくAWSのマネージドServiceを活用して手間を減らしていく方針で進めています。AWSのコンポーネントを駆使して楽していく環境を作りたい!!という方がいましたらお声がけしていただけると幸いです!