AWS Lambdaの構成管理のためにterraformを導入してみた

FASTALERT開発チームバックエンドエンジニアの鈴木(泰)です。

本記事は、AWS Lambdaの構成管理のためにTerraformを導入してみたというお話です。

TL;DR

  • FASTALERTチームの開発文化とTerraformの導入に至った背景
  • AWS Lambda関数をTerraformでどう管理しているか

もくじ

本対応の背景

具体的なAWS Lambda x Terraformの話に入る前に、FASTALERTチーム、workerレポジトリ、今回Terraformの導入に至った動機について、少しだけお話しさせてください。

FASTALERTとチーム文化

FASTALERTは、事件・事故・災害などのリスク情報をAIが自動収集し、収集された情報をお客様にとって使いやすい形で提供するBtoB向けのSaaSです。まだまだ成長過程のサービスであるため、顧客からのフィードバックを含め、寄せられる要望へ迅速に対応しなければなりません。

上記のような事情から、FASTALERTのバックグラウンド処理の内容は規模が小さく多岐に渡り(広く浅い)、「開発速度」が重要となってきます。

様々なバックグラウンド処理

  • データやシステムの監視とメトリクス算出、データベース中の防災情報の管理、防災情報へのメタ情報、社内システム連携
  • トリガーも様々。イベント駆動、Web API、運用者が不定期的に手動で実行する、等。

開発速度を重要視するチームの慣習

  • ほぼ全てのコンポネントがCD化されている、本番稼働するシステムの全ソースコードはGitで管理されている、Gitのタグを付与するだけのお手軽リリース、IaCの導入、スクラム開発

workerレポジトリ

FASTALERTチームにはworkerという名前のレポジトリがあります。 このレポジトリは、様々なバックグラウンド処理を小さい工数で提供し、かつ、ソースコード管理システムでソースコードを管理し、CI/CDを完備することを目的とし、Lambda製の様々なバックグラウンド処理を一元的に管理しており、Lambda関数を簡単に追加できるようにしています。具体的には(1)ソースコードを書く(2)デプロイ用のコードを追加(3)Gitのタグを付与する。これだけで新しいLambda関数をリリース可能です。本記事執筆時においては約50個のLambda関数が管理されており、ソースコードの追加・更新が頻繁に行なわれています。

apexのメンテナンス停止

workerレポジトリでは、Lambda関数の構成管理ツールとしてこれまではapexを利用していました。apexが2019年にメンテナンスを停止しました。今回のTerraform導入は、このapexのEOLを受けてのことです。

Lambdaの構成管理ツールとしてTerraformを採用した理由

Terraform以外の構成管理ツールの候補としてAWS SAMがありました。Terraformを採用することとなった決め手となったのは、以下の点です。

  • AWS上の実際のリソース設定とTerraformのstateを柔軟に解消できる。
  • FASTALERT開発チームのメンバーはTerraformを使い慣れている。

AWS上の実際のリソース設定とTerraformのstateを柔軟に解消できる

過去の経験上、SAMはリソース定義(yamlファイル)と実際のインフラ設定の間に生じた差分の解決がとても面倒臭い、という印象があります(昔、SAMを利用していたとき、差分が生じて、CloudFormationが動かなくなり、CloudFormationに紐づくリソースを丸ごと削除して新しく作り直す、というような危険な運用をしたことが苦い経験として記憶に焼き付いていたりします)。

IaCを進める過程において、構成管理用のソースコードと実際のインフラ設定の間に差分が生まれるということはよくあります。FASTALERTチームのように開発速度が求められる現場においては、そのような差分が発生する可能性はとても高いです。

Terraformは、CloudFormationを使用しておらず、かつ、実際のインフラ設定との差分を解消するためのimportコマンドがあるため、差分の発生に対して柔軟に対処することができます。

FASTALERT開発チームのメンバーはTerraformを使い慣れている

Terraformを採用したもう1つの理由は、FASTALERT開発チームメンバーがTerraformに慣れているという点です。最近FASTALERTチームでは、構成管理をTerraform化することを推奨しており、Terraformを使えるメンバーが増えつつあります。

AWS Lambdaの構成管理のためにTerraformを導入(詳細)

それでは、我々がどのようにしてTerraform化を実現したかについてお伝えいたします。

本記事で紹介している詳細実装は、Python3.8、Terraform1.1.7で動作することを前提としています。

コードサンプルはこちらにあります。

ディレクトリ構造

workerレポジトリは以下のようなディレクトリ構造です。

functions/
terraform/
build-pkg.sh
Pipfile
Pipfile.lock

Pipfile, Pipfile.lockは依存ライブラリを管理します。

本記事の要となる、functions/, terraform/ディレクトリについての詳細です。build-pkg.shはLambda用のzipを作るためのスクリプトです(後述)。

# functionsディレクトリ配下にはLambda関数のエントリーポイント群があります。
functions/
functions/lambda_process_checker.py
functions/lambda_export_api_log.py
...以下省略...

# 複数のLambda関数から共通して使われるソースコードは
# functions/worker_lib/ ディレクトリ配下にあります
functions/worker_lib/
functions/worker_lib/common.py
...以下省略...

# Terraformディレクトリ配下にはTerraformのコードがあります。
terraform/

## 共通するリソース(IAMとか)はmain.tfに書いています。
terraform/main.tf

## Lambda関数のリソース定義は
## 可読性を向上させるために
## 基本的にはLambda関数1つにつき、1つのファイルです。
## ファイル名は /functions ディレクトリ配下のpythonのソースコードと同じ。
terraform/lambda_process_checker.tf
terraform/lambda_export_api_log.tf
...以下省略...

ファイルの中身

functions/process_checker/main.pyの中には、Lambdaのエントリーポイントがあります。

# coding: utf-8
def handler(event, context):
    print('start process_checker')
    ...以下省略...

terraform/main.tfの中には、共通するリソース定義があります。

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.0"
    }
  }
}

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

resource "aws_iam_role" "worker-role" {
  name        = "worker-role"
  description = "Do not edit manually! This is auto generated by Terraform. Allows Lambda Function to call AWS services on your behalf."
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      ...以下省略...

terraform/lambda_process_checker.tfの中には、Lambda関数リソースの定義があります。dists.zipは、build-pkg.sh(後述)が生成するLambda関数のパッケージです。

resource "aws_lambda_function" "aws_lambda_process_checker" {
  function_name    = "aws_lambda_process_checker"
  filename         = "dists.zip"
  source_code_hash = filebase64sha256("dists.zip")
  runtime          = "python3.8"
  role             = aws_iam_role.worker-role.arn
  handler          = "lambda_process_checker.handler"
}

Lambdaのデプロイ方法

  1. Lambda関数のパッケージを作る。
  2. terraform applyコマンドを実行する。

Lambda関数のパッケージを作る

Lambda関数用のzipファイルを作ります。全てのLambda関数のエントリーポイント、その実行に必要となる依存ライブラリを全て1つのzipファイルの中に格納します。参考 .zip ファイルアーカイブで Python Lambda 関数をデプロイする

workerレポジトリ上のbuild-pkg.shがzipファイルを作ります。build-pkg.shが正常終了するとdists.zipというファイルが生成されます。

#!/bin/bash

set -e

WORK_DIR="/tmp/lambda-pkg-$(date +%s)"
DIST_DIR="${WORK_DIR}/dists"
OUT_DIR=$(pwd)/terraform

mkdir $WORK_DIR
mkdir $DIST_DIR

pipenv lock -r > requirements.txt
pip install -r requirements.txt -t ${DIST_DIR}/
cp -r ./functions/* ${DIST_DIR}/

cd ${DIST_DIR}/ && zip -q -r dists.zip * && mv dists.zip ${OUT_DIR}/

注意点

この実装方法ですと、Lambda関数の数が増えていくのに伴い、dists.zipファイルのサイズが大きくなることが懸念されます。dists.zipファイルのサイズが大きくなることを回避するための実装方針として、サイズが大きくなりがちな変数の定義(長い文字列や配列、連想配列の定数値)はDynamoDBやS3等の永続化層へ配置するようにしています。Pythonのソースコードが増えるだけであれば、ファイルのサイズが大きくなることは回避できます。(それでも、Pythonの依存パッケージのサイズが大きくなってしまう懸念点は依然として残されています。。。もしそうなってしまった場合、AWS Lambda Layers等の利用を検討すべきかもしれません。)

terraform applyコマンドを実行する

dists.zipが生成された後、以下のコマンドを実行し、Lambda関数をデプロイします。

cd terraform && terraform init && terraform apply

以上です。

新しいLambda関数を作成する場合、Pythonのソースコード、Terraformの定義ファイルを追加するだけです。とてもお手軽に作れます。例えば、new_func1という新しいLambdaを追加する場合、以下の2つを追加するだけです。逆に既存のLambdaを削除する場合、2つのファイルを削除するだけです。

  • functions/new_func1/main.py
  • terraform/lambda_new_func1.tf

コードサンプルはこちらにあります。

所感

AWS Lambdaの構成管理ツールとしてTerraformを導入してから3ヶ月ほど経っています。本対応を終えて、振り返って思うことをお話しします。

本記事で紹介した実装方法のメリットは、シンプルな構成であることかなと思います。ソースコードとインフラのリソース定義が1つのレポジトリで完結し、全体の見通しが良いです。Lambdaの追加や削除がとても簡単にできます。

実務的に困るようなデメリットは今のところは顕在化していません。しかしながら本記事で紹介した、規模の小さなプログラムを一元管理するようなタイプのレポジトリ(上記のworkerレポジトリ)の注意点として以下のことがあげられます。本来マイクロサービスとして独立して作るべき処理を、処理の追加とリリースが簡単という理由だけで、追加してはならないということです。このタイプのレポジトリに追加して良い処理は、緊急性が必要とされ、かつ、マイクロサービスとして切り出すことの費用対効果が得られないもの(規模の小さな処理、マイクロサービスとして切り出すべきかどうか、その境界が曖昧な処理)、に限定すべきです。

あとがき

本記事では書いていないこと

  • 実際には、zipファイルの作成、terraform applyコマンドの実行は、CD環境上で自動化されています。
  • Pythonソースコードのユニットテストの実行は、CI環境上で自動化されています。
  • 我々の本番環境では、workerレポジトリ上で管理されているLambda関数は約50個あるため、functions/ディレクトリ配下には50個のLambdaのエントリーポイント、terraform/ディレクトリ配下には50個のLambda関数リソース定義があります。
  • 実際には、Terraformのリソース定義ファイルはもう少し作り込んであります(Lambdaのトリガーリソースの定義、モジュール機能、-var-fileオプション、stateファイル管理周り)。今回の記事では省略しました。
  • 50個全てのLambdaを、無停止でapexからTerraformへ切り替える作業が本当に大変なところであり、話したいところでもあるのですが(汗)、記事がとても長くなってしまうので省略しました。