tl;dr
個人的に Lambda のデプロイは Serverless Framework 一択のつもりだったけど, Terraform で CloudFront を作っておいて, Lambda@Edge だけ Serverless Framework でデプロイするというのは違うかな...と思って Terraform でデプロイすることにしたのでメモの第二弾です.
尚, Lambda@Edge の用途としては, サイトに Basic 認証を設定したいと思いますが, 以下のような要件を満たすことを想定しています.
- Lambda 以外の AWS リソースは使わないこと
- 認証情報は複数設定出来ること
- ソースコードに認証情報を埋め込んだ状態でリポジトリには保存しない
- 環境変数は使わない (Lambda@Edge の制限)
Lambda 関数を書き散らす
以下のような超シンプルな Lambda 関数を書きます.
'use strict'; function unauthorized_response() { const body = 'Unauthorized'; const response = { status: '401', statusDescription: 'Unauthorized', body: body, headers: { 'www-authenticate': [{key: 'WWW-Authenticate', value:'Basic'}] }, }; return response; } exports.handler = (event, context, callback) => { const request = event.Records[0].cf.request; const headers = request.headers; const users = [ [ '${basic_auth_user1}', '${basic_auth_pass1}' ], [ '${basic_auth_user2}', '${basic_auth_pass2}' ] ]; if (typeof headers.authorization == 'undefined') { let response = unauthorized_response(); callback(null, response); } for (const elem of users) { let authUser = elem[0]; let authPass = elem[1]; let authString = 'Basic ' + new Buffer(authUser + ':' + authPass).toString('base64'); if (headers.authorization[0].value == authString) { // Continue request processing if authentication passed callback(null, request); } } let response = unauthorized_response(); callback(null, response); };
要件を満たす (認証情報を複数設定出来ること) 為, 以下のように認証情報を配列で持つようにしています. ユーザー名とパスワードは後述の Terraform 内で定義します.
const users = [ [ '${basic_auth_user1}', '${basic_auth_pass1}' ], [ '${basic_auth_user2}', '${basic_auth_pass2}' ] ];
この関数を適当なディレクトリを作成して index.js
というファイル名で保存します. 今回は,
lambda-at-edge2/src/index.js
というパスとファイル名で保存しています.
尚, この Lambda 関数が適用されると,下図のような認証ダイアログが表示されます.
Lambda@Edge を設定する
必要なリソースとしては以下の通り.
- IAM Role (ポリシーはマネージドポリシーの
arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
を付与) - Lambda ファンクション
- パスワード文字列を生成する
random_string
- 関数のソースコードに
random_string
の結果を埋め込む為,template_file
これらデプロイする Terraform コードのサンプルは以下の通り. 尚, Terraform のバージョンは 0.12.x を利用していて, 且つ, Terraform workspace で開発環境, 本番環境を分けて apply することを想定しています.
provider "aws" { region = "us-east-1" alias = "virginia" } resource "aws_iam_role" "lambda-edge" { name = "lambda-edge-${terraform.workspace}" assume_role_policy = data.aws_iam_policy_document.lambda-assume-role.json } data "aws_iam_policy_document" "lambda-assume-role" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = [ "lambda.amazonaws.com", "edgelambda.amazonaws.com" ] } } } resource "aws_iam_role_policy_attachment" "lambda-edge-basic-role" { role = aws_iam_role.lambda-edge.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } resource "random_string" "basic_auth_password" { count = 2 length = 12 special = false } data "template_file" "lambda-edge-function2" { template = file("lambda-at-edge2/src/index.js") vars = { basic_auth_user1 = "user1" basic_auth_pass1 = random_string.basic_auth_password[0].result basic_auth_user2 = "user2" basic_auth_pass2 = random_string.basic_auth_password[1].result } } output "basic_auth_password" { value = random_string.basic_auth_password.*.result } data "archive_file" "lambda-edge-function2" { type = "zip" output_path = "lambda-at-edge2/dst/index_access.zip" source { content = data.template_file.lambda-edge-function2.rendered filename = "index.js" } } resource "aws_lambda_function" "lambda-edge-function2" { provider = aws.virginia filename = data.archive_file.lambda-edge-function2.output_path function_name = "basic-auth-function-${terraform.workspace}" role = aws_iam_role.lambda-edge.arn handler = "index.handler" source_code_hash = data.archive_file.lambda-edge-function2.output_base64sha256 runtime = "nodejs12.x" publish = true memory_size = 128 timeout = 3 }
- Lambda@Edge はバージニアリージョン (
us-east-1
) にデプロイする必要がある為,providor
の指定でaws.virginia
を指定 source_code_hash
はソースコードの変更を検知する為, アーカイブの Base64 を SHA256 でハッシュした値を設定しているpublish = true
にすることで関数にバージョンが付与される, Lambda@Edge の関数にはバージョン番号が付与されている必要がある
尚, 要件を満たす (ソースコードに認証情報を埋め込んだ状態でリポジトリには保存しない等) 為, 以下のように random_string
と template_file
を利用してユーザー名とパスワードを terraform apply
時にソースコードに埋め込んでいます.
... 略 ... resource "random_string" "basic_auth_password" { count = 2 length = 12 special = false } data "template_file" "lambda-edge-function2" { template = file("lambda-at-edge2/src/index.js") vars = { basic_auth_user1 = "user1" basic_auth_pass1 = random_string.basic_auth_password[0].result basic_auth_user2 = "user2" basic_auth_pass2 = random_string.basic_auth_password[1].result } } ... 略 ...
CloudFront ビヘイビアに Lambda@Edge を設定
あとは, CloudFrontビヘイビアに Lambda@Edge を設定します.
resource "aws_cloudfront_distribution" "aws_cloudfront_distribution" { ... 略 ... default_cache_behavior { ... 略 ... lambda_function_association { event_type = "viewer-request" lambda_arn = aws_lambda_function.lambda-edge-function2.qualified_arn include_body = false } } ... 略 ... }
上記の例では, デフォルトのキャッシュビヘイビアに設定しています. また, event_type
は viewer-request
を設定する必要があります.
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-edge.html より引用
上図は, どのタイミング (Event Type) で Lambda 関数を仕込めるか図示したものです. 尚, 前回, ディレクトリインデックスを設定した場合には origin-request
に設定しましたが, ビヘイビア毎に設定出来るイベントタイプは 1 つだけなので注意が必要です.
参考
以下を参考にさせて頂きました. ありがとうございました.