こしあん
2023-07-16

docker-composeを使ってLambda用のAWSプロファイルを切り替える


1.7k{icon} {views}


Dockerを使ってLambdaをデプロイします。ローカルではAWSの名前付きプロファイルを使って、コードに変更を加えずに、AWSにデプロイする場合は工夫が必要になります。docker-composeのenvironmentを使うとそこをよしなにできました。

はじめに

やりたいこと

  • AWS LambdaにDockerイメージからデプロイしたい
  • ローカルでLambdaのイメージをテストしたときのみ特定プロファイルを読み込ませたいが、AWS環境上ではデフォルトプロファイルを読み込ませたい
  • ローカル限定でアドホックな環境変数を定義するときに、docker-composeのenvironmentでやったら便利だった

作るもの

AWS LambdaからAWS SNSを経由してメールを送信するプログラム

正直この程度だと、DockerでなくLmabda上でコードを直書きしたほうが早いが、もっと複雑なプログラムだとDockerの旨味が出てくるのでサンプル程度に使う。

前提知識

前提知識として、ロールやポリシーを設定した上で、Lambdaにコード直書きしてSNS経由でメール送信できるようにしておきます。以下の記事がわかりやすいです。

AWS Lambdaで遊ぼう #3 LambdaからSNSでメール送信
https://zenn.dev/nakam_aws/articles/6562e07cfae4fd

LambdaのベースのDockerイメージ

LambdaをDockerイメージからデプロイしたい場合、ベースのDockerイメージの選択肢が2つあります。

Tag見ればわかりますが、Pythonの場合ものすごい勢いで更新されています、結論から言うとこっちが楽でおすすめです。

  • スクラッチビルド
    UbuntuのようなLambdaに対応していないDockerイメージを使って、1からビルドする方法です。後で確認しますが、ローカルで動かすためにRIEのインストールや取り回しが必要になるので、ややめんどいです。

今回紹介するdocker-composeを使ったやり方はどちらのベースイメージでも使えます。

AWS公式のlambda/pythonの場合

以下のようなディレクトリ構成にしました。

- app.py
- docker-compose.yaml
- Dockerfile
- requirements.txt

コード

※requirements.txtは何も書いていないので割愛

app.py

import boto3
client = boto3.client('sns')

def lambda_handler(event, context):
    params = {
        'TopicArn': '<your-sns-arn>',
        'Subject': 'Lambda -> SNS送信テスト',
        'Message': 'Message\n\nLambda -> SNSでの送信テスト'
    }

    client.publish(**params)

Lambdaにコードを埋め込む場合と同じです。boto3に特にプロファイル名を指定しないで各サービスのクライアントを起動すると、デフォルトプロファイルが読み込まれるというのがポイントです。

デフォルトプロファイルとは、aws configureをしたときに登録されるプロファイルで、「~/.aws/config」や「~/.aws/credentials」では[default]と書かれています。後で見るように、このデフォルトプロファイルの扱いがトリッキーなので注意が必要です。

Dockefile

FROM public.ecr.aws/lambda/python:3.10.2023.07.13.16

# Install requirements
COPY requirements.txt .
RUN  pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"

# Copy function code
COPY app.py ${LAMBDA_TASK_ROOT}

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "app.lambda_handler" ] 

AWS公式のサンプルをほぼコピペです。ベースのイメージ名や、CMDの引数を変えています。app.lambda_handlerのコンマ以降はエントリーポイントの関数名を登録するので適宜変更しましょう。

docker-compose.yaml

services:
  lambda:
    build: .
    image: lambda_email_pre
    ports:
      - 9000:8080
    volumes:
      - ~/.aws/:/root/.aws
    environment:
      - AWS_PROFILE=develop

主にローカルで動かすためのdocker-composeです。environment以外の部分は、docker-composeを使わない書き方だと、以下のようになるはずです。

# ビルド
docker build -t lambda_email_pre .
# 起動
docker run --rm -it -p 9000:8080 -v ~/.aws/:/root/.aws --env AWS_PROFILE=develop lambda_email_pre

長ったらしいのでいちいちコマンドコピペするの面倒くさいですよね。docker-composeだとビルドも起動も簡単です。

# ビルド
docker-compose build
# 起動
docker-compose up
# 終了
docker-compose down

volumeオプションでのローカルからの認証情報のマウントは、Lambdaであっても機能します。このケースでは認証情報が必要なのは、実質的にPythonコードで呼んでいるboto3なので。

Windowsでの実行の注意点

Windowsの場合はWSLを2ウィンドウ立ち上げておきます。AWS公式にあるとおり、curlを実行します。

curl http://127.0.0.1:9000/2015-03-31/functions/function/invocations -s -d '{}'

コマンドプロンプトで実行しようとすると、以下のようなエラーになります。これは実装がおかしいのではなく、コマンドプロンプトの特有のエラーなので、WSLで実行すれば正常に実行できます。

curl http://127.0.0.1:9000/2015-03-31/functions/function/invocations -s -d ‘{}’
{“errorMessage”: “Unable to unmarshal input: Expecting value: line 1 column 1 (char 0)”, “errorType”: “Runtime.UnmarshalError”, “requestId”: “f260a31c-bb64-4971-a0d4-8bc3fb6ee48b”, “stackTrace”: []}

ポイント

実際にこれはECRにプッシュ→AWSで実行しても、ローカルで実行しても動きます(デプロイ方法はネット上にいくつも記事あるので割愛)

docker-compose.yamlのenvironmentはdocker runに有効な環境変数です。docker buildのときは反映されません

したがって、ローカルの実行はdocker-composeで行ってしまい、DockerイメージだけECRに投げて、AWS上ではあとはよしなにしてもらえば、docker-compose.yamlのenvironmentで設定した環境変数は無視されるということです。

これはAWSプロファイルの切り替えに便利で、ローカル環境では名前つきプロファイルを使っている(この例だとdevelop)に対し、AWS環境だとAWSが用意したデフォルトプロファイルを使うということが、ソースファイルの書き換えなしでできます。

AWSで使うプロファイルは、環境変数AWS_PROFILEで任意の名前つきプロファイルに切り替えできますが、例えばAWS_PROFILEを空文字(””)にしても、aws configureで指定したプロファイルには一致しません(AWS側で実行しようとするとエラーになりました)。aws configureで指定されるデフォルトプロファイルは、これらの名前付きプロファイルの設定がないときに読み込まれるプロファイルなので、愚直にやろうとすると、環境変数の設定ではなく、

  • unsetのような形でビルド時の環境変数を消去するか
  • runのときに動的に環境変数を付与するか

のいずれかになりますが、実行時の引数に応じたif的な処理はDockerfileの仕様上あまり得意ではありません。

なので、そもそも発想を変えて、docker-composeはローカル用と割り切って、docker-compose経由でrunしたときに付与される環境変数を定義してしまうというのが一つのやり方です(CI/CD考え始めるとまた面倒くさくなりますが)

Dockerイメージをスクラッチビルドする場合

スクラッチビルドの場合は考えることが増えます。ディレクトリ構成は以下のようになります。

+ src
  - app.py
  - entry_script.sh
+ docker-compose.yaml
+ Dockerfile
+ requirements.txt

ローカルではLambdaのエミュレートのためにRIEを使い、それ以外では普通に実行するということを行うため、別途entry_script.shを用意します。

RIEのセットアップも別途必要になります。

src/app.py

import boto3
client = boto3.client('sns')

def lambda_handler(event, context):
    params = {
        'TopicArn': '<your-sns-arn>',
        'Subject': 'Lambda -> SNS送信テスト',
        'Message': 'Message\n\nLambda -> SNSでの送信テスト'
    }

    client.publish(**params)

AWSが用意したベースイメージを使う場合と変わりません

src/entry_script.sh

#!/bin/sh
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
  echo AWS_PROFILE=${AWS_PROFILE}
  exec /usr/bin/aws-lambda-rie /usr/bin/python3 -m awslambdaric $@
else
  exec /usr/bin/python3 -m awslambdaric $@
fi

LAMBDA_RUNTIME_APIが用意されていない(ローカル環境の場合は)RIEを使い、それ以外は普通にPythonを実行しています。

Dockerfile

Ubuntu:22.04をベースイメージとした場合です。Lambda RIEの組み込みも行っています。正直そこまで大した容量ではないので組み込んでもいいと思います。

FROM ubuntu:22.04

RUN apt-get update
ENV TZ=Asia/Tokyo
ENV LANG=en_US.UTF-8
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get install -yq --no-install-recommends python3-pip \
        python3-dev \
        curl \
        tzdata && apt-get upgrade -y && apt-get clean

RUN ln -s /usr/bin/python3 /usr/bin/python

# Install RIE
RUN curl -Lo /usr/bin/aws-lambda-rie \
    https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie && \
    chmod +x /usr/bin/aws-lambda-rie

# Install requirements
COPY requirements.txt .
RUN pip install -U pip &&\
  pip install --no-cache-dir -r requirements.txt
COPY src .

ENTRYPOINT  [ "/entry_script.sh"]
CMD ["app.lambda_handler"]

requirements.txt

awslambdaric==2.0.4
boto3==1.28.3

docker-compose.yaml

services:
  lambda:
    build: .
    image: lambda_email
    ports:
      - 9000:8080
    volumes:
      - ~/.aws/:/root/.aws
    environment:
      - AWS_PROFILE=develop

スクラッチビルドと公式イメージの比較

Dockerイメージをスクラッチビルドするのと、公式イメージを使うのどう変わるのか比較してみました。

手軽さ

公式イメージのほうが楽です。

イメージ容量

今回のケースでは、スクラッチビルドのほうが軽かったです

REPOSITORY              TAG                                 IMAGE ID       CREATED          SIZE
lambda_email            latest                              4f10beeb059d   19 minutes ago   328MB
lambda_email_pre        latest                              1246275decbc   3 hours ago      619MB

lambda_emailがスクラッチビルド、lambda_email_preが公式イメージです。無圧縮状態で倍程度の容量差がありました。今回はDynamoDBなど使っていませんでしたが、公式イメージでそこらへんカバーされているためちょっと重いのかもしれません。

ただ、ECRにプッシュしたらかなり微妙な差となりました。

  • 328MB -> 114.61MB
  • 619MB -> 184.24MB

ECRの料金がGB/月あたり0.10USDとかなり安いので、正直1~2円程度の差でしかないです。無料枠(月500MB)が使えれば0円になります。

Lambdaの速度

Lambdaの実行速度は、公式イメージのほうが速い結果となりました。コンソールから3回テストケースを叩いた場合です。

公式イメージ

START RequestId: 30593aff-6c12-487b-ac14-326acc4673c2 Version: $LATEST
END RequestId: 30593aff-6c12-487b-ac14-326acc4673c2
REPORT RequestId: 30593aff-6c12-487b-ac14-326acc4673c2  Duration: 249.23 ms Billed Duration: 734 ms Memory Size: 128 MB Max Memory Used: 67 MB  Init Duration: 484.38 ms    

START RequestId: aeab0ced-3ff7-40ce-92c6-6fd2eae1f77f Version: $LATEST
END RequestId: aeab0ced-3ff7-40ce-92c6-6fd2eae1f77f
REPORT RequestId: aeab0ced-3ff7-40ce-92c6-6fd2eae1f77f  Duration: 187.79 ms Billed Duration: 188 ms Memory Size: 128 MB Max Memory Used: 68 MB  

START RequestId: 8c7b54bf-378e-4b66-af7b-e7b0925b762e Version: $LATEST
END RequestId: 8c7b54bf-378e-4b66-af7b-e7b0925b762e
REPORT RequestId: 8c7b54bf-378e-4b66-af7b-e7b0925b762e  Duration: 137.53 ms Billed Duration: 138 ms Memory Size: 128 MB Max Memory Used: 68 MB  

1回目が2.2秒、それ以降が1秒でした。

スクラッチビルド

START RequestId: a7f62aa0-77b3-470c-8845-3ee5b11bdcbd Version: $LATEST
END RequestId: a7f62aa0-77b3-470c-8845-3ee5b11bdcbd
REPORT RequestId: a7f62aa0-77b3-470c-8845-3ee5b11bdcbd  Duration: 1118.60 ms    Billed Duration: 2200 ms    Memory Size: 128 MB Max Memory Used: 62 MB  Init Duration: 1080.64 ms   

START RequestId: 431c18a7-2b33-422d-9b88-c6db8f352608 Version: $LATEST
END RequestId: 431c18a7-2b33-422d-9b88-c6db8f352608
REPORT RequestId: 431c18a7-2b33-422d-9b88-c6db8f352608  Duration: 1075.32 ms    Billed Duration: 1076 ms    Memory Size: 128 MB Max Memory Used: 63 MB  

START RequestId: 01c843d3-5311-4382-8da0-a41e4258cafa Version: $LATEST
END RequestId: 01c843d3-5311-4382-8da0-a41e4258cafa
REPORT RequestId: 01c843d3-5311-4382-8da0-a41e4258cafa  Duration: 1048.83 ms    Billed Duration: 1049 ms    Memory Size: 128 MB Max Memory Used: 64 MB  

1回目が0.7秒、2回目以降が0.1~0.2秒でした。

Dockerイメージサイズだけ見るとスクラッチビルドのほうが速いですが、公式イメージは何らかの最適化がかかっているのでしょうね。特に問題なければ公式イメージ使うでいいと思います。

感想

LambdaのDocker周り簡単かなと思ったら結構クセがあった。AWSのプロファイル周りはハマりがち



Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です