概要
Lambdaをコード管理したいのですが、Lambdaのベストプラクティス で良い感じのものがないと思い困り調べてみました。
以下のデプロイ方法がありますが、どれも個人的にはいまいちなため悩んでいました。
- AWS CLIでのデプロイする
- 独自のデプロイツール(Lambdaroll)を使う
- ServerlessFrameworkを使う
- AWS CDKでデプロイする
課題感
サーバレス関連のツールはどうしても構成管理ツールと紐づいてしまっているのでどうしたものかなぁと思っていました。
例えば、AWS CDKでもServerlessFrameworkでもバックエンドではCloudFormationで動いてしまっています。このようなツールのメリットは1リポジトリでインフラの構成管理とコード管理が完結することがメリットだと思っています。ただ、既存インフラ環境のIaCにTerraformを使っている場合、TerraformとCloudFormationのダブルスタンダードになってしまい個人的には気持ち悪いなと思っています。
それを解消する方法をこの記事では考えていきたいです。
Lambdaリソースの基本設計
設計方針
Lambdaのデプロイ方法はソースコードをzipで固めてS3に置くか、ECRにDocker Imageをプッシュするかを現在は選ぶことができます。
保守性が高く、どの環境でもちゃんとアプリケーションのデバッグができるという点ではDocker Imageの方がいいので、LambdaコードはECRにプッシュすることを選びます。
ちなみにサーバレスアプリケーションを作るときは、アプリケーションごとにリポジトリを作る方が良いとAWSの中の人に聞いたことがありますが、今回の用途としてはヘッダー書替え用やIP制限等のLambdaを扱いたいため1リポジトリでディレクトリを分けてデプロイします。
Lambdaリソースの管理方針
- インフラリソースはTerraformで管理
- Lambdaへのデプロイはソースコード側で行う
- CI/CDはGitHub Actionsで行う
インフラリソースをTerraform管理にする
ここではLambda Functionの管理とIAM、ECRの管理をします。
# IAM
resource "aws_iam_role" "lambda_basic" {
name = "lambda-basic-role"
path = "/service-role/"
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_basic" {
role = aws_iam_role.lambda_basic.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
# ECR
resource "aws_ecr_repository" "lambda1" {
name = "lambda-function1"
image_tag_mutability = "IMMUTABLE"
image_scanning_configuration {
scan_on_push = true
}
}
# Lambda Function
resource "aws_lambda_function" "lambda1" {
function_name = "lambda-function1"
role = aws_iam_role.lambda_basic.arn
package_type = "Image"
image_uri = "${aws_ecr_repository.lambda1.repository_url}:latest"
timeout = 60
lifecycle {
ignore_changes = [image_uri]
}
}
# CloudWatch Logs
## ref https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/monitoring-cloudwatchlogs.html
resource "aws_cloudwatch_log_group" "lambda1" {
name = "/aws/lambda/${aws_lambda_function.lambda1.function_name}"
retention_in_days = 30
}
Lambdaコードを作成する
以下のようなディレクトリにコードを配置します。
言語はnode.jsにします。
$ tree
.
└── lambda-function1
├── Dockerfile
├── index.js
├── package-lock.json
└── package.json
各ファイルの中身は以下の通りです。
{
"name": "lambda-container-image-example",
"license": "MIT"
}
npm install
して package-lock.json
は作っておいてください。
index.js
のコード内容は以下の通り。
exports.handler = (event, context, callback) => {
const response = event.Records[0].cf.response;
const headers = response.headers;
const headerNameSrc = 'X-Amz-Meta-Last-Modified';
const headerNameDst = 'Last-Modified';
if (headers[headerNameSrc.toLowerCase()]) {
headers[headerNameDst.toLowerCase()] = [
headers[headerNameSrc.toLowerCase()][0],
];
console.log(`Response header "${headerNameDst}" was set to ` +
`"${headers[headerNameDst.toLowerCase()][0].value}"`);
}
callback(null, response);
};
Dockerfileはこの通り
# AWS ベースイメージを使用
FROM public.ecr.aws/lambda/nodejs:14
# ソースコードを関数のルートディレクトリにコピーします。
# 関数のルートディレクトリは `LAMBDA_TASK_ROOT` 環境変数を上書きすることで変更することができます ( デフォルトは `/var/task` ) 。
COPY index.js package.json package-lock.json /var/task/
RUN npm install
# CMD にハンドラを設定します。
# Node.js の場合は `{ファイル名(拡張子なし)}.{関数名}` のように指定します。
# 今回は `index.js` の `handler` 関数をハンドラとして用意しているので以下のようになります。
CMD ["index.handler"]
GitHub Actionsのworkflowを作成する
Lambdaのソースコードおいてあるリポジトリでこちらのコードを設定してください。
GitHub Actionsで特定のディレクトリで操作された時にデプロイされるように設定してます。
name: "Deploy"
on:
push:
branches:
- main
paths:
- "lambda-function1/**"
jobs:
lambda:
name: "Deploy Lambda"
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: ./lambda-function1/
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 1
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: docker build & docker push & lambda update
run: |
docker build -t XXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-function1:${{ github.sha }} .
docker push XXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-function1:${{ github.sha }}
aws lambda update-function-code --function-name lambda-function1 --image-uri XXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-function1:${{ github.sha }}
Tips
GitHub Actionsのマーケットプレイスにあるものは使えない
GitHubActionsの公式にLambdaデプロイ用のコードがありますが、これはimage対応していないのでエラーになります。
参考資料: lambda-action
大元はこちらのコードですが、ファイルのみの対応になってしまっています。 image_uri
だけ指定してデプロイを試みても400エラーになります。
参考資料: drone-lambda
結論
ServerlessFrameworkのようにそのフレームワークだけ使えばインフラのことを管理しなくても良いのですが、元々のインフラ設計がサーバーレスを中心とした設計でない場合、余計なインフラリソースが勝手に作られて運用が辛い経験がありました。
この方法を使えば、TerraformとLambdaのデプロイを分離できますし、ServerlessFrameworkでできるような余計なインフラリソースが増えてしまう心配がありませんし、不要になれば、Terraformから削除すればAWS環境も綺麗な状態を保てるのでお勧めです。