ようへいの日々精進XP

よかろうもん

Terraform で Lambda@Edge を設定する個人的なテンプレート (2) 〜 Basic 認証を設定する 〜

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 関数が適用されると,下図のような認証ダイアログが表示されます.

f:id:inokara:20200801112250p:plain

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
}

尚, 要件を満たす (ソースコードに認証情報を埋め込んだ状態でリポジトリには保存しない等) 為, 以下のように random_stringtemplate_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_typeviewer-request を設定する必要があります.

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-edge.html より引用

上図は, どのタイミング (Event Type) で Lambda 関数を仕込めるか図示したものです. 尚, 前回, ディレクトリインデックスを設定した場合には origin-request に設定しましたが, ビヘイビア毎に設定出来るイベントタイプは 1 つだけなので注意が必要です.

参考

以下を参考にさせて頂きました. ありがとうございました.

docs.aws.amazon.com

docs.aws.amazon.com