TIGの伊藤真彦です。
普段からAWS Lambdaにはお世話になっているのですが、初めてカスタムランタイムを動かす仕事が舞い込んできました。
AWS Lambdaのカスタムランタイムとは
AWS Lambdaは2021.09.13時点で次のランタイムがサポートされています。
- Node.js
- Python
- Ruby
- Java
- Go
- .NET
2018年からDockerコンテナでカスタムランタイムを構築することが可能になりました。
Dockerコンテナを準備すれば上記以外の言語、実行バイナリなど自由なアプリケーションをAWS Lambdaで動かすことが可能ということになります。
今回私が作成したカスタムランタイムは、GoでOracle DBのクライアントライブラリを操作可能なカスタムLambdaランタイムです。
GoでOracle DBに接続する
GoでOracle DBを利用する方法を検討した結果mattn/go-oci8を利用することがメジャーな手法であることがわかりました。
mattn/go-oci8を利用する際はgo get github.com/mattn/go-oci8
コマンドを実行して必要な機能をインストールする必要がありますが、前提としてOracle Instant Client、C/C++コンパイラをインストールし、設定ファイルと環境変数を整備する必要があります。
上記前提があるため、AWS LambdaデフォルトのGoランタイムではGoのコードそのものは完璧に書いてあっても動作しません。
そこで、各種依存パッケージを準備済みのDockerコンテナを準備することにしました。
AWS Lambdaで動作するコンテナを作成する
理論上どんなコンテナでもAWS Lambdaの上で動かすことが可能です。
しかし、最終的な成果物はLambdaランタイムとして動くことが可能な仕様を満たしている必要があります。
アプリケーションがAWS Lambdaのsdkを利用し、HTTPリクエストに応答する仕様を満たすように作られているのと同じように、コンテナには規定の環境変数が存在することなど、コンテナとしての要求仕様が存在します。
これら前提を満たすDockerfileをフルスクラッチで書き起こし、アップデートに追従するのは現実的ではありません。ベースイメージにamazon/aws-lambda-providedを利用すると安心して前提条件を満たすことができます。
このベースイメージに必要なライブラリ、今回はOracleクライアント一式と設定ファイルを用意します。
go get github.com/mattn/go-oci8
コマンドの実行、およびmattn/go-oci8を利用したコードをビルドし、ビルドされたコードを実行するためには、Oracleクライアントと設定ファイル一式がいずれのタイミングでも必要になるため、マルチステージビルドは行わず、Oracleクライアントの準備を整えた後にGoをインストールし、実行バイナリのビルドが終わったらGoを削除するという仕組みを整えました。
FROM amazon/aws-lambda-provided:al2 |
参考までに、oci8.pc
の設定内容は下記の内容です。
prefix=/usr/local/instantclient_21_3/ |
作成したコンテナをAWS Lambdaで利用する
DockerをLambdaで動かすアーキテクチャ
GoやPythonなど、提供されているランタイムでは、ソースコードや、ビルドしたバイナリををLambdaリソースにアップロードする形でデプロイを行いました。
Dockerカスタムランタイムの場合は、コンテナイメージ自体はECRにpushし、AWS Lambdaにはそのコンテナイメージのarnを設定する、という仕組みに変わります。
ECR + Lambdaという構成になる、という概要だけでも覚えておくといざ実装するときに助けになると思います。
コンテナイメージをECRにPushする
実際にコンテナイメージをpushし、AWS Lambdaで実行する方法を説明します。
まずはECRリポジトリを用意します。
CLIで構築する場合は下記のコマンドでリポジトリを作成します。
aws ecr create-repository --repository-name myapp |
terraformで記載することも可能です。
resource "aws_ecr_repository" "myapp" { |
リポジトリが用意できたら、コンテナをビルドし、pushします。
docker build -t myapp:latest |
続いてLambdaリソースを構築します。
aws lambda create-function \ |
terraformではこのように書くことができます。(vpcの設定など一部省略しています)${data.aws_caller_identity.current.account_id}
で自分のAWSアカウントIDを取得できるのがミソですね。
resource "aws_lambda_function" "myapp" { |
コンテナイメージを更新した際に気をつけるポイントですが、latest
タグのコンテナイメージを更新しても、すぐにLambda関数の挙動には反映されません。一晩寝かしても古いイメージが参照されていました。
全く同じimage-uri
のまま更新コマンドを実行することで即時反映できます。
aws lambda update-function-code --profile myprofile --function-name myapp --image-uri ${AWS_ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com/myapp:latest |
コンテナイメージのタグやダイジェストをコンテナイメージの内容を更新する度に厳密に管理する事も可能ですが、今のところ常にlatest
での運用に落ち着いています。
コンテナイメージをローカル環境でデバッグする
AWS Lambdaで動く要件を満たしたコンテナが作成されているかをローカル環境で確認することは可能です。逆に作成したアプリケーションコンテナ単体では動作しません。
Lambda Runtime Interface Emulatorとの組み合わせでコンテナを起動することでローカル環境でのデバッグが可能になります。
RIEは下記のコマンドでインストールできます。
mkdir -p ~/.aws-lambda-rie && curl -Lo ~/.aws-lambda-rie/aws-lambda-rie \ |
インストール完了後、下記のコマンドで、RIE経由でビルドしたコンテナを起動します。
実際に試してみた環境では(2021年9月時点)Dockerfile
でENTORYPOINT
が明記されている場合も、コマンドでENTORYPOINT
に相当する部分を指定する必要がありました。
docker run -v ~/.aws-lambda-rie:/aws-lambda --entrypoint /aws-lambda/aws-lambda-rie -p 9000:8080 myapp:latest /lambda |
このコンテナを起動した状態で、curlコマンドを実行してlocalhostで起動されたLambdaエンドポイントを叩く形でコンテナに組み込んだアプリケーションを実行します。
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}' |
curlコマンド実行時に想定通りにアプリケーションが動けばコンテナイメージそのものは問題なく出来上がっていることになります。
まとめ
- AWS LambdaではカスタムDockerコンテナを起動できる
- aws-lambda-providedをベースイメージにすることがオススメ
- ECR + Lambdaの組み合わせで構築、運用する
- ローカル環境でデバッグすることも可能
どうしてもカスタムランタイムでないと困る状況になる事はそう頻繁にある事ではないため、若干参考情報が少ないかなと感じました。
参考になれば幸いです。