LoginSignup
14
6

More than 1 year has passed since last update.

俺流AWS Lambdaデプロイ+IaCのベストプラクティスを考える

Last updated at Posted at 2021-07-27

概要

Lambdaをコード管理したいのですが、Lambdaのベストプラクティス で良い感じのものがないと思い困り調べてみました。

以下のデプロイ方法がありますが、どれも個人的にはいまいちなため悩んでいました。

課題感

サーバレス関連のツールはどうしても構成管理ツールと紐づいてしまっているのでどうしたものかなぁと思っていました。

例えば、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環境も綺麗な状態を保てるのでお勧めです。

14
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
6