バージョン管理された人

subversionで管理されてます

golang で書かれた Lambda Docker を ARM で動かす CDK

AWS Lambda も ARM で動かす方が若干料金が安くなる。 また、 AWS Lambda はミリ秒課金になってから料金を安くするために実行速度の早い言語である golang 書きたくなるだろう。

今回はこの golang アプリを動かす Docker を ARM な AWS Lambda で動かすインフラを定義する CDK コードについて解説する。

Dockerfile

何はともあれ Lambda Docker を動かすには Docker 環境を用意しなければならないため、 Lambda Docker で動かすイメージを作る Dockerfile を作る。 実は、 x86_64 環境であれば、何も考えずに golang アプリを生成し、 ECR Public Garelly にある lambda/go の Usage 通りに利用するような Dockerfile を書くだけでいい。 しかし、 ARM はこれでは 動かない 。 lambda/go には OS/Arch の部分に ARM 64 と立派に記載されているが、このイメージは ARM 環境だと 利用できずx86_64 環境のみが提供されている。 証拠とは言えないが、傍証としては このような Issue に対する 回答 が存在する。

docker image pull --platform linux/arm64 public.ecr.aws/lambda/go:1 をして docker inspect しても Architectureamd64 のものしか落ちてこない

なのでこのイメージはそのままでは使えない。 かといってローカルでも走らせられるように RIE を自前で組込もうとするとダウンロード URL が arm64 と x86_64 で異なるため、自前でダウンロード先を動的に変更するようにしたりする必要がある等マルチアーキテクチャで動かすには手間が増えてしまう。 もし、 AWS が新しいアーキテクチャを増やしてきた場合はそこのメンテナンスも自前で必要になるなど Docker でアーキテクチャ関係なく動かしたいのにアーキテクチャのことを意識して Dockerfile をメンテナンスしなければならなくなる。 したがって、これとは違うイメージを利用しなければならない。

実は ECR Public Garelly には lambda/provided というのが存在し、こちらのイメージを使えば lambda に対応したバイナリであればなんでも動かせる。

先程の Issue の回答にも lambda/provided を使えと書いてある。

ゆえに、 golang アプリを ARM64 な Lambda Docker として動かしたい場合はこの lambda/provided を使うことになる。 ここではこちらが試して動いた lambda/provided:al2 を使って Lambda Docker を構築する Dockerfile を示す。

FROM --platform=$TARGETPLATFORM golang:1 as build
WORKDIR /usr/src/app
ARG TARGETOS
ARG TARGETARCH
COPY go.mod go.sum ./
RUN go mod download && go mod tidy
COPY . .
RUN env CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -v -o /main ./...

FROM --platform=$TARGETPLATFORM public.ecr.aws/lambda/provided:al2
COPY --from=build /main ${LAMBDA_RUNTIME_DIR}/bootstrap
CMD [ "main" ]

この Dockerfile ではマルチステージビルドを利用して golang アプリの生成をやってからそれを実際に動かすイメージへコピーして Lambda Docker 用のコンテナをつくるということをやっている。

少々だけ解説すると、 go コマンドでビルドする際に

を指定してあげて、しっかり動かしたい環境向けの golang アプリを生成する必要がある。 これらは GOOSGOARCH で指定できる。 また、 Docker にはいくつか 自動で設定されるグローバル ARG 変数 が存在し、これらに OS やアーキテクチャ、プラットフォームの情報などが格納されている。 この情報を利用してこれらを go コンパインラに伝えて動かしたいアークテキチャ向けに golang アプリをビルドしている。 CGO_ENABLEDcgo を使って C ライブラリをビルドしないようにする同じみの設定。 たったこれだけにはなるが、 lambda/go が ARM で動かないことを知らないと Lambda Docker を動かしたときにアーキテクチャが違うことによって動かないというエラーに悩まされることになる。

lambda/go を使っても ARM 向けのビルドだけは通るが、実態は自分がビルドした golang アプリ以外 x86_64 向けにビルドされたものしか入っていないため、 docker inspect すると Arcitecture は ARM なのに Lambda で動かそうとしてもアーキテクチャが噛み合わずに動くことはない。 しかし、ローカルだと qemu が挟まって正常に動いてしまうため、なぜか Lambda でだけ動かない Docker イメージができあがる。

最後にビルドした golang アプリをコピーしなければならないわけだが、これは ${LAMBDA_RUNTIME_DIR}/bootstrap というディレクトリに設定すれば OK。 この bootstrap から Lambda 関数を起動するようになっているので、ここにプログラムをコピーし、それを CMD に指定して走らせるという手順になっている。

他の言語でも同じ

CDK

Docker イメージは用意できたので、あとは AWS 上にこれをデプロイすれば終わりだ。 やることは

  1. DockerImageCode::fromImageAsset のプロパティに platform を指定する
  2. DockerImageFunctionarchitecture プロパティを設定

の2つになる。 つまり、まとめて書けば

import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";

export class Component extends cdk.Resource {
  constructor(scope: Construct, id: string) {
    // ...
    new cdk.aws_lambda.DockerImageFunction(this, "Function", {
      code: cdk.aws_lambda.DockerImageCode.fromImageAsset(
        "<lambda コードへのパス>",
        { platform: cdk.aws_ecr_assets.Platform.ARM_64 },
      ),
      architecture: cdk.aws_lambda.Architecture.ARM_64,
    });
  }
}

になる。

まとめ

lambda/go という Lambda Docker を golang で動かすイメージが提供されているものの、こちらは使えないため、 lambda/provided を利用する。 最後にデプロイ先のアーキテクチャを指定すれば ARM 上で Lambda 関数を動かせる。