Open11

[CDK] ImageType=Image な Lambda をマルチステージ構成の Dockerfile を使ってデプロイしようとすると GHA でのデプロイがうまく動かないので調査

hassaku63hassaku63
  • CDK で PackageType = Image の Lambda を GitHub Actions からデプロイしたい
  • デプロイは GHA (GitHub Actions) から行いたい
  • デプロイするイメージの Dockerfile はマルチステージ構成

という前提条件でデプロイすると2回目以降(なのか?)のデプロイがコケてしまう。

何がどうなって再現するのかさっぱり見えないので調査する。

hassaku63hassaku63

再現実験用に作ったコードはこちら

https://github.com/hassaku63/cdk-multi-stage-image-lambda-gha

Dockerfile はこれ

# for Mac M1
FROM --platform=linux/arm64 public.ecr.aws/lambda/python:3.9 AS build-arm64

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

# CMD [ "handler.handler" ]

# for Lambda Runtime
FROM --platform=linux/amd64 public.ecr.aws/lambda/python:3.9 AS build-amd64

# Install the function's dependencies using file requirements.txt
# from your project folder.
COPY requirements.txt  .
RUN  pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"
COPY . ${LAMBDA_TASK_ROOT}

# CMD [ "handler.handler" ]

Lambda リソースを定義する CDK の実装はこれ

interface CreatePythonLambdaFromAssetProps {
  path: string
  handler: string
}

function createPythonLambdaFromAsset(scope: Construct, id: string, props: CreatePythonLambdaFromAssetProps) {
  const f = new aws_lambda.DockerImageFunction(scope, id, {
    code: aws_lambda.DockerImageCode.fromImageAsset(props.path, {
      cmd: [props.handler],
    }),
    timeout: cdk.Duration.minutes(1),
    tracing: aws_lambda.Tracing.ACTIVE,
  });

  new cdk.CfnOutput(scope, `${id}Arn`, {
    value: f.functionArn,
  });

  return f;
}

成功した初回デプロイはこれ

https://github.com/hassaku63/cdk-multi-stage-image-lambda-gha/actions/runs/5628739854/job/15252946824

2回目のデプロイで失敗したのがこれ

https://github.com/hassaku63/cdk-multi-stage-image-lambda-gha/actions/runs/5628872367

hassaku63hassaku63

docker build が失敗している。

デプロイ時のログ
[07:47:14] Checking for previously published assets
[07:47:14] Retrieved account ID *** from disk cache
[07:47:14] Assuming role 'arn:aws:iam::***:role/cdk-hnb659fds-deploy-role-***-ap-northeast-1'.
[07:47:14] Retrieved account ID *** from disk cache
[07:47:14] Assuming role 'arn:aws:iam::***:role/cdk-hnb659fds-deploy-role-***-ap-northeast-1'.
[07:47:15] Retrieved account ID *** from disk cache
[07:47:15] Assuming role 'arn:aws:iam::***:role/cdk-hnb659fds-file-publishing-role-***-ap-northeast-1'.
[07:47:15] Retrieved account ID *** from disk cache
[07:47:15] Assuming role 'arn:aws:iam::***:role/cdk-hnb659fds-image-publishing-role-***-ap-northeast-1'.
[07:47:15] CdkMultiStageImageLambdaGhaStack:  check: Check s3://cdk-hnb659fds-assets-***-ap-northeast-1/af892d23d92db68bb6faa363d4813d5724bf6fceded4cb8d201f4200cc4ffcff.json
[07:47:16] CdkMultiStageImageLambdaGhaStack:  found: Found s3://cdk-hnb659fds-assets-***-ap-northeast-1/af892d23d92db68bb6faa363d4813d5724bf6fceded4cb8d201f4200cc4ffcff.json
[07:47:16] CdkMultiStageImageLambdaGhaStack:  check: Check ***.dkr.ecr.ap-northeast-1.amazonaws.com/cdk-hnb659fds-container-assets-***-ap-northeast-1:19223eadef6724113659c318155a5065154bc16a8b58865b8d53038e4db83f11
[07:47:17] Call failed: describeImages({"repositoryName":"cdk-hnb659fds-container-assets-***-ap-northeast-1","imageIds":[{"imageTag":"19223eadef6724113659c318155a5065154bc16a8b58865b8d53038e4db83f11"}]}) => The image with imageId {imageDigest:'null', imageTag:'19223eadef6724113659c318155a5065154bc16a8b58865b8d53038e4db83f11'} does not exist within the repository with name 'cdk-hnb659fds-container-assets-***-ap-northeast-1' in the registry with id '***' (code=ImageNotFoundException)
[07:47:17] 2 total assets, 1 still need to be published
[07:47:17] Retrieved account ID *** from disk cache
[07:47:17] Waiting for stack CDKToolkit to finish creating or updating...
CdkMultiStageImageLambdaGhaStack:  start: Building 19223eadef6724113659c318155a5065154bc16a8b58865b8d53038e4db83f11:***-ap-northeast-1
[07:47:19] CdkMultiStageImageLambdaGhaStack:  debug: docker login --username AWS --password-stdin https://***.dkr.ecr.ap-northeast-1.amazonaws.com
[07:47:20] CdkMultiStageImageLambdaGhaStack:  debug: docker inspect cdkasset-19223eadef6724113659c318155a5065154bc16a8b58865b8d53038e4db83f11
[07:47:20] CdkMultiStageImageLambdaGhaStack:  build: Building Docker image at /home/runner/work/cdk-multi-stage-image-lambda-gha/cdk-multi-stage-image-lambda-gha/cdk.out/asset.19223eadef6724113659c318155a5065154bc16a8b58865b8d53038e4db83f11
[07:47:20] CdkMultiStageImageLambdaGhaStack:  debug: docker build --tag cdkasset-19223eadef6724113659c318155a5065154bc16a8b58865b8d53038e4db83f11 .
Sending build context to Docker daemon  4.096kB

Step 1/8 : FROM --platform=linux/arm64 public.ecr.aws/lambda/python:3.9 AS build-arm64
3.9: Pulling from lambda/python
371a402111f6: Pulling fs layer
8ed46a04bcaa: Pulling fs layer
bd477373174d: Pulling fs layer
455bc9247f6c: Pulling fs layer
91d1ce2b26de: Pulling fs layer
9a72daca427d: Pulling fs layer
455bc9247f6c: Waiting
91d1ce2b26de: Waiting
9a72daca427d: Waiting
8ed46a04bcaa: Download complete
bd477373174d: Verifying Checksum
bd477373174d: Download complete
455bc9247f6c: Verifying Checksum
455bc9247f6c: Download complete
9a72daca427d: Verifying Checksum
9a72daca427d: Download complete
91d1ce2b26de: Verifying Checksum
91d1ce2b26de: Download complete
371a402111f6: Verifying Checksum
371a402111f6: Download complete
371a402111f6: Pull complete
8ed46a04bcaa: Pull complete
bd477373174d: Pull complete
455bc9247f6c: Pull complete
91d1ce2b26de: Pull complete
9a72daca427d: Pull complete
Digest: sha256:696a74214bac1cf4afe6427331c6fc609c8b58a343f62e0ed9e3a483f120d1f1
Status: Downloaded newer image for public.ecr.aws/lambda/python:3.9
 ---> 1a7025f31878
Step 2/8 : COPY requirements.txt  .
 ---> 3f9db90a2dd9
Step 3/8 : RUN  pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"
Warning: rning] The requested image's platform (linux/arm64/v8) does not match the detected host platform (linux/amd64) and no specific platform was requested
 ---> Running in 514ca6707cb5
exec /bin/sh: exec format error
The command '/bin/sh -c pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"' returned a non-zero code: 1
CdkMultiStageImageLambdaGhaStack:  fail: docker build --tag cdkasset-19223eadef6724113659c318155a5065154bc16a8b58865b8d53038e4db83f11 . exited with error code 1: The command '/bin/sh -c pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"' returned a non-zero code: 1

 ❌ Deployment failed: Error: Failed to build asset 19223eadef6724113659c318155a5065154bc16a8b58865b8d53038e4db83f11:***-ap-northeast-1
    at Deployments.buildSingleAsset (/home/runner/work/cdk-multi-stage-image-lambda-gha/cdk-multi-stage-image-lambda-gha/node_modules/aws-cdk/lib/index.js:415:11476)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Object.buildAsset (/home/runner/work/cdk-multi-stage-image-lambda-gha/cdk-multi-stage-image-lambda-gha/node_modules/aws-cdk/lib/index.js:415:150805)
    at async /home/runner/work/cdk-multi-stage-image-lambda-gha/cdk-multi-stage-image-lambda-gha/node_modules/aws-cdk/lib/index.js:415:137021
[07:47:33] Notices refreshed

Failed to build asset 19223eadef6724113659c318155a5065154bc16a8b58865b8d53038e4db83f11:***-ap-northeast-1
[07:47:33] Error: Failed to build asset 19223eadef6724113659c318155a5065154bc16a8b58865b8d53038e4db83f11:***-ap-northeast-1
    at Deployments.buildSingleAsset (/home/runner/work/cdk-multi-stage-image-lambda-gha/cdk-multi-stage-image-lambda-gha/node_modules/aws-cdk/lib/index.js:415:11476)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Object.buildAsset (/home/runner/work/cdk-multi-stage-image-lambda-gha/cdk-multi-stage-image-lambda-gha/node_modules/aws-cdk/lib/index.js:415:150805)
    at async /home/runner/work/cdk-multi-stage-image-lambda-gha/cdk-multi-stage-image-lambda-gha/node_modules/aws-cdk/lib/index.js:415:137021
Error: Process completed with exit code 1.

途中のログで docker build していることと、そこで build-arm64 のステージを見ていることがわかる。
このステージは2つ定義したステージのうちの1つ目であり、これは本来ビルドしてほしくない。

また、現状の私の認識では --target を省いた場合の docker build は最後に定義したステージ(とそのステージが依存する他のステージ)のみがビルドされる想定だった。

今回の Dockerfile の内容からすると build-arm64 がビルドされるのは想定外。

[07:47:20] CdkMultiStageImageLambdaGhaStack: debug: docker build --tag cdkasset-19223eadef6724113659c318155a5065154bc16a8b58865b8d53038e4db83f11 .
Sending build context to Docker daemon 4.096kB

Step 1/8 : FROM --platform=linux/arm64 public.ecr.aws/lambda/python:3.9 AS build-arm64

https://github.com/hassaku63/cdk-multi-stage-image-lambda-gha/commit/ca37b40102d54f463d64fe69d7d954e8d37683b9

hassaku63hassaku63

ログを見返すと

Step 2/8 : COPY requirements.txt .
---> 3f9db90a2dd9
Step 3/8 : RUN pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"
Warning: rning] The requested image's platform (linux/arm64/v8) does not match the detected host platform (linux/amd64) and no specific platform was requested
---> Running in 514ca6707cb5
exec /bin/sh: exec format error

とあり、warn は出てるが失敗の原因はまた別のところにありそうな気もしてきた・・・。

まずは pip3 install の方をしっかり見た方がよさげかも。それはそれとして意図してないステージが build されてるのは解消したいが

hassaku63hassaku63

現状整理する。

最初にデプロイ成功した際にできたイメージが ECR にいるので、それを inspect した

docker inspect .dkr.ecr.ap-northeast-1.amazonaws.com/cdk-hnb659fds-container-assets--ap-northeast-1:5ab4aa8d130d7c303a8c438dce746929ceed5fe4bd30246a16319dadac31706b | jq

ECR にいるデプロイ成功時のイメージを inspect
[
  {
    "Id": "sha256:3d209cefab3e8eb27ef412a8424d494d19d4de2503a0ee5b9b4f32b29abc8e04",
    "RepoTags": [
      "************.dkr.ecr.ap-northeast-1.amazonaws.com/cdk-hnb659fds-container-assets-************-ap-northeast-1:5ab4aa8d130d7c303a8c438dce746929ceed5fe4bd30246a16319dadac31706b",
      "cdkasset-5ab4aa8d130d7c303a8c438dce746929ceed5fe4bd30246a16319dadac31706b:latest"
    ],
    "RepoDigests": [
      "************.dkr.ecr.ap-northeast-1.amazonaws.com/cdk-hnb659fds-container-assets-************-ap-northeast-1@sha256:8a857443b29f6e82ebc69badfe3cf64ed499296e3cd1c4b205346adcda34ef3d"
    ],
    "Parent": "",
    "Comment": "buildkit.dockerfile.v0",
    "Created": "2023-07-21T14:22:44.480560465Z",
    "Container": "",
    "ContainerConfig": {
      "Hostname": "",
      "Domainname": "",
      "User": "",
      "AttachStdin": false,
      "AttachStdout": false,
      "AttachStderr": false,
      "Tty": false,
      "OpenStdin": false,
      "StdinOnce": false,
      "Env": null,
      "Cmd": null,
      "Image": "",
      "Volumes": null,
      "WorkingDir": "",
      "Entrypoint": null,
      "OnBuild": null,
      "Labels": null
    },
    "DockerVersion": "",
    "Author": "",
    "Config": {
      "Hostname": "",
      "Domainname": "",
      "User": "",
      "AttachStdin": false,
      "AttachStdout": false,
      "AttachStderr": false,
      "Tty": false,
      "OpenStdin": false,
      "StdinOnce": false,
      "Env": [
        "LANG=en_US.UTF-8",
        "TZ=:/etc/localtime",
        "PATH=/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin",
        "LD_LIBRARY_PATH=/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib",
        "LAMBDA_TASK_ROOT=/var/task",
        "LAMBDA_RUNTIME_DIR=/var/runtime"
      ],
      "Cmd": null,
      "Image": "",
      "Volumes": null,
      "WorkingDir": "/var/task",
      "Entrypoint": [
        "/lambda-entrypoint.sh"
      ],
      "OnBuild": null,
      "Labels": null
    },
    "Architecture": "amd64",
    "Os": "linux",
    "Size": 708311546,
    "VirtualSize": 708311546,
    "GraphDriver": {
      "Data": {
        "LowerDir": "/var/lib/docker/overlay2/lz4uign93bkb1b6rxpi5atj8q/diff:/var/lib/docker/overlay2/igvletwu9fvev7mwiq5sbzjqm/diff:/var/lib/docker/overlay2/6892cb14de351a224c35cbe159af671f8b50b08822bb8a325471c2ffacfb4cf6/diff:/var/lib/docker/overlay2/83d4f2a740c3cfe6cc85b8163c34da760784f60040fb7823d78a6434c1329bee/diff:/var/lib/docker/overlay2/b83af63cf8994a7e5ec84c7a1e97235f8ec40073a5bdce51b14cd7216da0181e/diff:/var/lib/docker/overlay2/6a8f15ae89e1bc17c481246d5fecffbbea999273f051873f90e61bc9ccc9960d/diff:/var/lib/docker/overlay2/10ff1b32ad2f5add0dce0d7bebbaa6f7b6a87d95c6e761c8b99ab48065f079e1/diff:/var/lib/docker/overlay2/ed404b15aa329ebb67f35512e0be2e2db080f652ec5003b6855fdc7260ea50de/diff",
        "MergedDir": "/var/lib/docker/overlay2/p8uw4o5v8zykk3zxszrbu4v2o/merged",
        "UpperDir": "/var/lib/docker/overlay2/p8uw4o5v8zykk3zxszrbu4v2o/diff",
        "WorkDir": "/var/lib/docker/overlay2/p8uw4o5v8zykk3zxszrbu4v2o/work"
      },
      "Name": "overlay2"
    },
    "RootFS": {
      "Type": "layers",
      "Layers": [
        "sha256:2a9a5b39a0415dc5625eb8704426badbbe9e6ec672abd2ae038edc7c1fbb0f28",
        "sha256:16a264f580b93a5ebd9313794f6331dc2a68af9abf9d3abf55bb427a07681e92",
        "sha256:b67819d500bceb343dfb29903093bbacb0507a2a41008c5522bf86dc89fde30c",
        "sha256:15dd6c63f3a24e6a848a62cbdbef587fde902e26d3b13cf50aed08bf988fdf7b",
        "sha256:6c25fad1f01114fd54cceca2ce3c6a1c284a17c88a7b91487abbf8084f7a8752",
        "sha256:4c2065c968c85ff39471c53d91b86d4fcd19e992a4a4238720df99169523dfdb",
        "sha256:7830dd83e76651d4be182e9ffb1e7206d80ddcb5b9b96ecde5df4d1f4ac99000",
        "sha256:ec8e58bda745a8edf608221e68326fa8fe313f932f01dfff64383448f6711812",
        "sha256:6e07e7e6bf43f01604bbaa1e7144e593ae0d4a50bb9124cbf6e6e6b389a8e4d0"
      ]
    },
    "Metadata": {
      "LastTagTime": "2023-07-22T05:21:36.601203588Z"
    }
  }
]

amd64 っぽい。

CloudFormation の AWS::Lambda::Function の方も Architecture が未指定なのでデフォルトの x86_64 になっている。なのでここは期待値と一致している。

2回目以降(という認識であっているのかどうかも定かではないが)のデプロイで arm64 用のステージが呼ばれている。

hassaku63hassaku63

CDK 側でターゲットを明示してみた。

https://github.com/hassaku63/cdk-multi-stage-image-lambda-gha/commit/3b084a99858940029a62ea467cb156dfb6d29c3c

結果はこれ。変わらず build-arm64 をビルドしようとしている。

https://github.com/hassaku63/cdk-multi-stage-image-lambda-gha/commit/3b084a99858940029a62ea467cb156dfb6d29c3c

[09:50:16] CdkMultiStageImageLambdaGhaStack: debug: docker build --tag cdkasset-605c8af913d031315061bc31acacbed6a30c43ba4494119bc6988a8736569872 --target build-amd64 .
Sending build context to Docker daemon 4.096kB

Step 1/8 : FROM --platform=linux/arm64 public.ecr.aws/lambda/python:3.9 AS build-arm64

<中略>

Step 2/8 : COPY requirements.txt .
---> fa8279846842
Step 3/8 : RUN pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"
Warning: rning] The requested image's platform (linux/arm64/v8) does not match the detected host platform (linux/amd64) and no specific platform was requested
---> Running in ed3bf830f430
exec /bin/sh: exec format error
The command '/bin/sh -c pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"' returned a non-zero code: 1
CdkMultiStageImageLambdaGhaStack: fail: docker build --tag cdkasset-605c8af913d031315061bc31acacbed6a30c43ba4494119bc6988a8736569872 --target build-amd64 . exited with error code 1: The command '/bin/sh -c pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"' returned a non-zero code: 1

hassaku63hassaku63

エラーに見えるテキストを拾うと、pip install が 1 で終了している様子。

あと、その手前で

exec /bin/sh: exec format error

このエラーが出ている。

ホストと対象ファイルで実行形式に互換性がないっぽい。

pip3 ってバイナリだったか・・・?bash から呼び出すスクリプトだったはず??? と思った。

https://tooljp.com/linux/faq/5A093B94D26FEDB849257800005735DC.html

ベースイメージに bash がいないのなら、確かにこのエラーになるのも納得できる。ローカルで docker build して入ってみた。

手元は M1 Mac なので arm64 で試す

$ docker build --target build-arm64 -t test .

$ docker run --entrypoint /bin/bash -it test 
bash-4.2# echo $SHELL
/bin/bash

普通に bash は動くっぽい。 sh -c を介して実行するとこれは動作せず pip3 のヘルプが出るだけ。

bash-4.2# sh -c pip3 install -r requirements.txt 

ただ、RUN コマンドの書き方自体は AWS のドキュメント にも同様な記載があるし、手元の環境で docker build が動作することからもここの指定の仕方が悪いとは考えにくい。

hassaku63hassaku63

結局、

FROM --platform=linux/arm64 public.ecr.aws/lambda/python:3.9 AS build-arm64

これがビルドされてることが想定外でありそこを解消するのが早そうな気はする

Warning: rning] The requested image's platform (linux/arm64/v8) does not match the detected host platform (linux/amd64) and no specific platform was requested

と書いてあるので、ホスト (GitHub Actions, github hosted runner) のアーキテクチャである amd64 とマッチしないものをビルドしようとしているのが悪い気がする

hassaku63hassaku63

↑の状態で、GHA 経由だけでなく手元の M1 Mac からデプロイして動作を見てみた。

自分の記憶ではこのケースだと exec /bin/sh: exec format error で Lambda の実行がコケる想定だったが、これもうまく行ってしまった

ということで、プラットフォームごとにステージを定義する Dockerfile はそもそも使わんでOKということになりそう

この scrap を立てる契機になった事象の原因は解明できてないが、Lambda のデプロイをする用事は満たせているので(すっきりしないが)問題は解消としておく

hassaku63hassaku63

M1 の手元でのちょっとした動作検証なら↓こんな感じで良さそう。CDK は無関係の話にできる。

https://twitter.com/hassaku_63/status/1683040517637169153

一方で、CDK 的には Lambda のアーキテクチャと同じプラットフォームで docker build が動くようにしておきたい。cdk deploy を実行するマシンの環境に関係なく、Lambda を動かしたいアーキテクチャで cdk deploy / docker build が動くように明示できてればOK

よって、Function に指定する Architecture と ImageAsset で指定する platform を供給するための StackProps を定義してあげればそれで良さそう。Lambda のアーキテクチャを変えるなんてそうそうないと思うので、 prop 指定するまでもなく個別にハードコード、でも良いかもしれない。