はじめに
始めまして、NTT ドコモサービスイノベーション部の小川です。 aws の lambda でサーバーレスの仕組みを作りたいとなったときに、往々にして lambda にデフォで入ってるライブラリでは足りない!となることはあると思います。 本記記事では、ライブラリ足りないとなったときに aws cdk を使ったデプロイ方法 4 つ+外伝を 1 つ紹介していきます。 では、早速内容に入っていきましょう!
自己紹介
NTT ドコモでは R&D のサービスイノベーション部に所属しています。
業務は
- 大量制御信号データを有益なデータにパースするための基盤開発チーム
- NW 情報をどんどん見える化していくチーム
の 2 チームに所属しており、色々と NW の知識を蓄えていってます。
基盤開発チームの方は、同チームメンバーが体外発表をしている資料もあるので、興味あればぜひ眺めてみてください!
サマリ
本記事では aws cdk を使って lambda をデプロイする方法に
5 つの方法を紹介
- cdk 上でコードを完結させたければ、2,3,4 の方法が良さげ
- 個人的におすすめなのは、github actions を使う方法
実行環境
- aws cdk typescript 2.134
- Amazon Linux 2023.3.20240304
- docker
1. 愚直に zip ファイルを作ってデプロイ
まず、1つ目は一番シンプルな layer 化したいライブラリを zip 化してデプロイするというものです。 zip 化するうえでは
amazon linux 2023
で zip 化すること- python や node のバージョンを lambda で実行する環境に合わせること
- zip 化するフォルダ構成をお作法にあわせること(python だと
/python/
配下にライブラリをインストールして zip 化する)
などを注意すればいいです。 ただ、少し手間なので docker を使って zip 化するまでやってしまう方が楽ではあります。 参考までに python で zip 化する docker ファイル共有します。
FROM public.ecr.aws/lambda/python:3.12 WORKDIR /work RUN mkdir /work/require COPY requirements.txt /work/require # システム更新と必要なパッケージのインストール # pyscopg2を使いたかったのでlibpq-devel, gccをインストールしている RUN dnf update && dnf install -y zip libpq-devel RUN dnf install -y make gcc RUN pip3 install --upgrade pip RUN pip3 install -r /work/require/requirements.txt -t /python/ ENTRYPOINT ["zip", "-r", "/work/lambda.zip", "/python/"]
この docker ファイルを使った image ビルド後の作業として、私はローカルで実行したときはローカルフォルダーと/work/
をマウントして docker run したら zip ファイルがローカルのフォルダに保存されるようにしていました。
zip ファイルを作成した後は 任意の s3 に zip ファイルをアップロードして、cdk を使って以下のように書いてあげれば layer のデプロイができます。
import * as lambda from 'aws-cdk-lib/aws-lambda' const lambdaLayer = new lambda.CfnLayerVersion(scope, 'LambdaLayer', { layerName: 'test-layer', compatibleRuntimes: [lambda.Runtime.PYTHON_3_12.toString()], content: { s3Bucket: 'hogehoge', s3Key: 'fugafuga/lambda.zip' }, compatibleArchitectures: ['x86_64'], description: 'Test layer' })
こちらのデプロイの流れをまとめると
- zip ファイル作成
- s3 にアップロード
- デプロイ という流れになります。
チーム開発をするときには、どの layer を使ったかを残しておくためにも layer の zip も github に上げておきたいという欲がでてくるかもしれません。そのときは github の 100MB 制限には注意が必要です。(そもそも pip のライブラリを zip 化して github に push するのはイケてないですよね)
2. cdk deploy 中に zip ファイルを作成してデプロイ
2 番目の方法は cdk の s3_asset メソッドを使って zip ファイル作成から layer のデプロイまでやってしまう方法です。
こちらの方法はlayer の容量が 250MB を超えなければ一番オススメする方法です。
フォルダ構成はこちら
lib | | - lambda | | - main.py | - requirements.txt
コードはこちらです。
const lambdaLayerDir = path.join(__dirname, './lambda') const lambdaLayerConfig = new assets.Asset( scope, 'LambdaLayerConfig', { path: lambdaLayerDir, bundling: { image: lambda.Runtime.PYTHON_3_12.bundlingImage, command: [ 'bash', '-c', 'pip install -r requirements.txt -t /asset-output/python -q' ], outputType: cdk.BundlingOutput.NOT_ARCHIVED } } ) const lambdaLayer = new lambda.CfnLayerVersion( scope, 'LambdaLayer', { layerName: 'test-layer', compatibleRuntimes: [lambda.Runtime.PYTHON_3_12.toString()], content: { s3Bucket: lambdaLayerConfig.s3BucketName, s3Key: lambdaLayerConfig.s3ObjectKey }, compatibleArchitectures: ['x86_64'], description: 'test layer' } )
cdk だけで layer に使う zip の作成から layer のデプロイまでできるので便利ですよね。
用意するのは requirements.txt だけで良いので、1の方法に比べてチーム開発もしやすいなと個人的には感じてます。
3. DockerImageAsset + cdk-ecr-deployment を使ってデプロイ
3 番目の方法として、aws cdklabsにて開発されているcdk-ecr-deploymentを使う方法を紹介します。
cdklabs は有志が開発しているライブラリだったり、cdk チームのメンバーが開発しているライブラリがあったりします。人気があるライブラリはaws-cdk-lib
に同胞されるとか。
と前置きはここまでにして、このcdk-ecr-deployment
はlayer が 250MB を越してしまったときに使うと良いものになります。
そうなると、
- image をビルド
- ecr に push
- push した image タグを lambda のcodeパラメータに設定
というステップを踏まないといけないと思います。
そのステップを良い感じに解決してくれるのが、cdk-ecr-deployment
になります。
このライブラリは image を指定の ecr に push してくれる役割を果たしています。
(別途 image をビルドするステップの記載は必要です)
aws-cdk-lib
とは別ライブラリなので別途インストールは必要です。
npm install cdk-ecr-deployment
コード
import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets' import * as ecrdeploy from 'cdk-ecr-deployment' // imageビルド // cdkが作成するecrにimageがpushされる const testImage = new DockerImageAsset(scope, 'TestImage', { directory: path.join(__dirname, './path/to/Dockerfile'), // Dockerfileがあるフォルダ platform: Platform.LINUX_AMD64 }) const image-tag = 'hogehoge-latest' const ecrTestImage = new ecrdeploy.ECRDeployment( scope, 'TestImageDeployment', { src: new ecrdeploy.DockerImageName(testImage.imageUri), dest: new ecrdeploy.DockerImageName( `${accuntId}.dkr.ecr.ap-northeast-1.amazonaws.com/${repoName}:${image-tag}` // imageをpushしたいrepoを指定 ) } ) const testApiLambda = new lambda.CfnFunction(scope, 'CfnTestLambda', { functionName: 'test-lambda', packageType: 'Image', code: { imageUri: `${accuntId}.dkr.ecr.ap-northeast-1.amazonaws.com/${repoName}:${image-tag}` }, role: lambdaRole, timeout: 60, memorySize: 128 })
実行するとき(サンプルの実行方法に習ってる)
NO_PREBUILT_LAMBDA=1 cdk deploy
DockerImageAsset
+cdk-ecr-deployment
を使えば cdk だけで image のビルドから lambda のデプロイまですることができます。
注意点
このライブラリとても便利だという反面、個人的に気になるポイントがあります。 それは
- 予期していないリソースが作成される
- 内部的に使用している lambda のランタイムが EOL していることがある
- 開発が途中で終わることがある
という点です。
予期していないリソースが作成される
1 点目は、cdk-ecr-deployment
の中でも動作に必要なリソースが作成されます
作成されるリソースは、deploy してみたところ以下みたいです。特に、iam policy についてはこのポリシーが OK かは一度確認しても損はないかなと思います。
AWS::Lambda::Function AWS::IAM::Role AWS::IAM::Policy Custom::CDKBucketDeployment
内部的に使用している lambda のランタイムが EOL していることがある
cdk でビルドされた image を指定した ecr にコピーするための lambda が裏側で作成されデプロイされています。開発者がしっかりと EOL に対応していれば良いのですが、されていないパターンもちらほらあります。
cdklabs の中のライブラリを使う場合は気にかけておきましょう。
開発が途中で終わることがある
cdk-ecr-deployment
については現状 issue も PR も立ってるので大丈夫なのかなと思います。
cdk の document でも紹介されてるのでしばらくは大丈夫そうというのが自分の所感です。
4. cdk-docker-image-deployment を使ってデプロイ
4点目はcdk-ecr-deployment
から派生したcdk-docker-image-deploymentを使う方法です。
※2024/09/15 現在このライブラリが内部的に使用している lambda のバージョンが一部 node16 になっています。
この方法もlayer が 250MB を越してしまったときに使うと良いものになります。
- image をビルド
- ecr に push
- push した image タグを lambda のcodeパラメータに設定
の 1,2 のステップをよしなにやってくれます。
aws-cdk-lib
とは別ライブラリなので別途インストールは必要です。
npm install cdk-docker-image-deployment
コード
import * as imagedeploy from 'cdk-docker-image-deployment' // ecrのリポジトリ情報をDockerImageDeploymentの型に合わせて取得 const repo = ecr.Repository.fromRepositoryName( scope, 'Ecr', 'ecr' // imageをpushしたいリポジトリを指定 ) const testImage = new imagedeploy.DockerImageDeployment( scope, 'testImage', { source: imagedeploy.Source.directory( path.join(__dirname, './lambda') // lambdaのコードがあるフォルダ ), destination: imagedeploy.Destination.ecr(repo, { tag: ImageTag }) // 任意のimage tagを指定可能 } )
注意点
こちらもDockerImageAsset + cdk-ecr-deployment を使ってデプロイと同様に個人的に気になるポイントがあります。
気になるポイントとしては、
- 内部的に動作している lambda のランタイムの node16 がすでに EOL
- 開発が止まってそう
の 2 点が追加で気になるポイントです。
個人的に node18 に対応したコードに書き直し、動作したのは確認できたので開発者がアップデートする気があれば、、
と便利なライブラリゆえにちょっと残念な気持ちはあります。
外伝「github actions を使う」
開発者の方はコードの管理をするのに github などのバージョン管理ツールを使われてる方は多いですよね。
github はバージョン管理などだけではなく、github actionsを使って CICD パイプラインも組むことができ、
私が所属しているチームでも非常に多く活用しています。
(ただの独り言ですが、github actions って自分が学生だった 2018 年くらいの頃にあったのかなとふと気になりました、やべ年齢バレる)
とても便利な github actions を有効活用して lambda デプロイを cdk でしてあげようというのが外伝の内容になります。
github actions のあれやこれやは今回の記事では割愛するので参考資料乗せておきます
デプロイのステップとしては
- github actions 上で lambda image をビルド and ECR に push
- cdk の環境変数で image tag 名を受取・デプロイ
という流れになります。
どうでしょう、意外にシンプルな感じがしますよね。
後述しますが、こちらの方法
に比べて個人的に好みな方法です。
1. github actions 上で image ビルド
ファーストステップとして github actions 上で image ビルドできるように github の workflow yaml を用意します。
本記事ではビルドする yaml ファイルの紹介します。(長くなるので折りたたんでます)
PRマージ時にimageビルドするyamlファイル
name: build-and-push-docker-image on: workflow_call: inputs: build-context: description: Path to the Docker build context directory type: string required: true dockerfile: description: Path to the Dockerfile type: string required: true docker-image-ecr: description: URI and tag for the Docker image, e.g. ACCOUNT.dkr.ecr.ap-northeast-1.amazonaws.com/my-repo:latest type: string required: true env: description: 環境名 type: string required: true add-suffix-to-tag: description: Add a suffix to the tag, e.g. latest -> latest-20210101-123456-abc123a type: boolean default: false required: false aws-region: description: aws region type: string default: ap-northeast-1 required: false tz: description: add-suffix-to-tagで付与する時刻のタイムゾーン type: string default: Asia/Tokyo required: false secrets: iam-role-arn: description: ECRにpushするためのIAMロールのARN required: true outputs: image-tag: description: URI and tag for the Docker image, e.g. ACCOUNT.dkr.ecr.ap-northeast-1.amazonaws.com/my-repo:latest value: ${{ jobs.build-and-push-docker-image.outputs.image-tag }} permissions: id-token: write contents: read jobs: build-and-push-docker-image: runs-on: ubuntu-latest outputs: # ビルドする変更がない場合にlatestタグを返すようにする image-tag: ${{ steps.get-image-tag.outputs.image-tag != '' && steps.get-image-tag.outputs.image-tag || steps.set-default-image-tag.outputs.image-tag }} latest-image-tag: ${{ steps.get-image-tag.outputs.latest-image-tag }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # 履歴も含めて読み込む。これがないと最新のコミットのみ取得する動作になり、diffが取れない - id: check git diff # pushイベントの、push前の最新のコミットと現在の最新のコミットで差分があったファイル数を取得する run: | N_CHANGED_FILES=$(git diff --name-only ${{ github.event.before }} ${{ github.ref }} -- ${{ inputs.build-context }}/ | wc -l) echo "n-changed-files=$N_CHANGED_FILES" >> "$GITHUB_OUTPUT" - run: echo "${{ steps.diff.outputs.n-changed-files }} file(s) have changed." - if: steps.diff.outputs.n-changed-files == '0' id: set-default-image-tag run: | echo "image-tag=${{ inputs.docker-image-ecr }}:${{ inputs.env }}-latest" echo "image-tag=${{ inputs.docker-image-ecr }}:${{ inputs.env }}-latest" >> $GITHUB_OUTPUT - if: steps.diff.outputs.n-changed-files != '0' id: get-image-tag name: Get image tag run: | SHORT_COMMIT_HASH=$(echo ${GITHUB_SHA} | cut -c1-7) DATE_TIME=$(TZ=${{ inputs.tz }} date +%Y%m%d-%H%M%S) if [[ "${{ inputs.add-suffix-to-tag }}" = "true" ]]; then IMAGE_TAG_SUFFIX="${{ inputs.env }}-${DATE_TIME}-${SHORT_COMMIT_HASH}" else IMAGE_TAG_SUFFIX="${{ inputs.env }}" fi IMAGE_TAG="${{ inputs.docker-image-ecr }}:${IMAGE_TAG_SUFFIX}" echo "IMAGE_TAG: $IMAGE_TAG" echo "image-tag=$IMAGE_TAG" >> $GITHUB_OUTPUT echo "latest-image-tag=${{ inputs.docker-image-ecr }}:${{ inputs.env }}-latest" >> $GITHUB_OUTPUT - if: steps.diff.outputs.n-changed-files != '0' uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.iam-role-arn }} role-session-name: github-actions-b-dash-infra-realviz-${{ github.run_id }} aws-region: ${{ inputs.aws-region }} - if: steps.diff.outputs.n-changed-files != '0' uses: aws-actions/amazon-ecr-login@v1 - if: steps.diff.outputs.n-changed-files != '0' uses: docker/build-push-action@v4 with: context: ${{ inputs.build-context }} file: ${{ inputs.dockerfile }} platforms: linux/amd64 load: true push: true tags: ${{ steps.get-image-tag.outputs.image-tag }}, ${{ steps.get-image-tag.outputs.latest-image-tag }} # 最新のimageを参照できるようにlatestタグもつける
上記の yaml の output は
- ビルドした image の ecr image タグ名
- ファイルに変更がないときには、image タグが最新の image を参照し続けるために、hogehoge-latest とついた tag 名を作成する
という出力になります。
私達のチームがgitFeatureFlowを採用しているのもあり、
- id: check git diff # pushイベントの、push前の最新のコミットと現在の最新のコミットで差分があったファイル数を取得する run: | N_CHANGED_FILES=$(git diff --name-only ${{ github.event.before }} ${{ github.ref }} -- ${{ inputs.build-context }}/ | wc -l) echo "n-changed-files=$N_CHANGED_FILES" >> "$GITHUB_OUTPUT"
の step でファイル差分を検知するために、git diff で push 前の最新のコミットと現在の最新のコミットで差分を検知するようにしています。
ブランチ戦略によっては${{ github.event.before }}
の部分はorigin/main
のように書き換えて使用するほうがわかりやすいと思います。
2,3,4 の方法に比べてどうなの?
私達のチームではセキュリティ対策の関係もあり、github actions の方法を使っています。
当初は 4 の#4-cdk-docker-image-deployment-を使ってデプロイを使ってたのですが、内部的に lambda のバージョンが古い -> AWS のセキュリティ対策に引っかかるという経緯があり、ライブラリ側が対応する前に移行しました。
ただ、それぞれにメリット・デメリットはあるので 2,3,4 の方法に比べて外伝のメリット・デメリットを思いつく範囲で箇条書きします。
メリット
- 余分な AWS リソースが作成されず、github actions 上の ubuntu 上でビルドができる
- 特定のディレクトリ内のファイルが変更されたら、ビルドするのように制御が可能 -> cdk diff 時にビルドされないようにできる
- docker 公式が github actions のモジュールを提供しているため、ググると色々と知見がでてくる
デメリット
- 2,3,4 は cdk コード内で完結することができるが、github actions を使う場合は別途 yaml の作成が必要になる
- 後任の人が困らないようにちゃんと引き継ぎ、README・マニュアルに記載をしましょう
終わりに
本記事では、aws cdk(+ github actions)を活用して lambda デプロイする方法を紹介しました。
今回は lambda だけでしたが、IaC を活用して皆が幸せになる AWS を作っていきましょう。