😸

Terraformで生成したLambda関数のコードをリポジトリで管理してデプロイはCIでやりたい

2024/06/24に公開

インフラリソースの準備

まず Lambda 自体を Terraform で作る。

この時点で適当にGitリポジトリを生成し、IaC関連のコードは infra というディレクトリに配置することとする。

以下のコードで terraform apply したら "HelloWorldLambda" という名前の関数ができる。

ソースコード
terraform {
  required_version = "~> 1.7"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.40"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

variable "bucket_name" {
  description = "The name of the S3 bucket to deploy functions"
  type        = string
}

resource "aws_s3_bucket" "lambda_code_bucket" {
  bucket = var.bucket_name
}

resource "aws_s3_bucket_public_access_block" "this" {
  bucket = aws_s3_bucket.lambda_code_bucket.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_iam_role" "lambda_execution_role" {
  name = "lambda_execution_role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole",
        Principal = {
          Service = "lambda.amazonaws.com"
        },
        Effect = "Allow",
      },
    ]
  })
}

resource "aws_iam_policy" "lambda_logging" {
  name        = "lambda_logging"
  description = "IAM policy for logging from a lambda"

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ],
        Resource = "arn:aws:logs:*:*:*"
      },
    ]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_logs" {
  role       = aws_iam_role.lambda_execution_role.name
  policy_arn = aws_iam_policy.lambda_logging.arn
}

data "archive_file" "lambda_zip" {
  type        = "zip"
  output_path = "${path.module}/lambda_function.zip"

  source {
    content  = "def handler(event, context):\n    print('Hello World')\n    return 'Hello World'"
    filename = "main.py"
  }
}

resource "aws_s3_object" "lambda_code" {
  bucket = aws_s3_bucket.lambda_code_bucket.bucket
  key    = "lambda_function.zip"
  source = data.archive_file.lambda_zip.output_path

  etag = filemd5(data.archive_file.lambda_zip.output_path)
}

resource "aws_lambda_function" "this" {
  function_name = "DeployExampleLambda"

  s3_bucket = aws_s3_bucket.lambda_code_bucket.bucket
  s3_key    = aws_s3_object.lambda_code.key

  handler = "main.handler"
  runtime = "python3.10"
  role    = aws_iam_role.lambda_execution_role.arn

  timeout = 30
}

※ プロファイルは適当なものを設定しておく (e.g. export AWS_PROFILE=handson-profile)

AWSマネージメントコンソールで Lambda 関数が存在することを確認し、テスト実行をしてみる。

上記のような出力になればこの節の内容は問題なし。

Pythonの環境構築

今回は Python で実装するのでまずは環境構築を行う。
関数のソースコードは functions 以下で管理するので、この時点でディレクトリを生成しておく。

Pipenv のインストール

Macの場合は以下

$ brew install pipenv

Pipenv を採用する理由は以下。

  • バージョン切り替えの利便性が上がるため
  • 仮想環境を用いて他の環境との干渉を防げるため
  • パッケージの管理が行いやすくなるため

同様のことができるものであれば Pipenv 以外で代替しても問題なし。

Pipenv による Python のインストール

今回はプロジェクトのルートから関数本体とIaCとでディレクトリが分けるようにしているので、前者に移動してインストールを実行する。

$ cd functions
$ pipenv --python 3.10

仮想環境のアクティベート

$ pipenv shell

終了するときは exit を実行する。
仮想環境自体を削除したい場合は pipenv --rm を実行する。

パッケージのインストール

依存関係のインストール

$ pipenv install pandas numpy

開発やテストで利用するパッケージのインストール

$ pipenv install --dev jupyter

関数コードの準備

まずサンプルとして適当なコードを用意する。
ただ、実践的なものは他のパッケージと依存関係を持つはずなので、適当に numpy や pandas を使うようにしている。

main.py
import json
import numpy as np
import pandas as pd

# NOTE: 依存パッケージを含めて関数をアーカイブして、それがデプロイできるかをチェックすることが肝要なので内容は適当
def handler(event, context):
    array = np.array([1, 2, 3, 4, 5])
    array_sum = np.sum(array).item()

    df = pd.DataFrame({
        'A': [1, 2, 3],
        'B': [4, 5, 6]
    })
    df_sum = df.sum().to_dict()

    return {
        'statusCode': 200,
        'body': json.dumps({
            'numpy_sum': array_sum,
            'pandas_sum': df_sum,
        })
    }

レイヤーの追加

pandas などを利用するには以下のようなレイヤーと呼ばれるものを追加する必要がある。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/chapter-layers.html

以下から用途にあったレイヤーを探せる。

https://github.com/keithrozario/Klayers/tree/master/deployments

今回だと、リージョンは ap-northeast-1 でランタイムのPythonバージョンである3.10と一致している arn:aws:lambda:ap-northeast-1:770693421928:layer:Klayers-p310-pandas:13 を選択した。

Terraform で、Lambdaの引数として上記を指定する。

$ git diff          
diff --git a/infra/lambda.tf b/infra/lambda.tf
index e23785a..39d162a 100644
--- a/infra/lambda.tf
+++ b/infra/lambda.tf
@@ -27,4 +27,5 @@ resource "aws_lambda_function" "this" {
   role    = aws_iam_role.lambda_execution_role.arn
 
   timeout = 30
+  layers = ["arn:aws:lambda:ap-northeast-1:770693421928:layer:Klayers-p310-pandas:13"]
 }

編集したら terraform apply して反映する。

手動デプロイによる動作確認

適当に package という ZIP ファイルを作るための作業用ディレクトリを作って、アーカイブを AWS CLI でデプロイする。

$ mkdir package
$ cd package
$ cp ../main.py .
$ zip -r lambda_function.zip .
$ aws lambda update-function-code \
    --function-name DeployExampleLambda \
    --zip-file fileb://lambda_function.zip

デプロイ後に、マネージメントコンソールで以下のような出力が得られたらOK.

Github Actions による自動デプロイ

Github Secrets の設定

以下を各々設定すること

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_REGION
  • LAMBDA_FUNCTION_NAME

このサンプルでは本番やステージングを区別しないので、"Repository secrets" を利用

ワークフローの定義

.github/workflows/deploy.yml
name: Build and Deploy Lambda Function

on:
  push:
    paths:
      - functions/main.py

jobs:
  build_and_deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Package Lambda function
        run: |
          cd functions
          zip -r lambda_function.zip main.py

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Deploy to AWS Lambda
        run: |
          aws lambda update-function-code --function-name ${{ secrets.LAMBDA_FUNCTION_NAME }} --zip-file fileb://functions/lambda_function.zip

これで push されたときに functions/main.py が変更されていたら自動でデプロイされるようになる。

Discussion