【AWS SAM】デプロイとローカル確認用のコンテナイメージを一つのDockerfileで使い分ける

# 初めに

LambdaアプリケーションをSAMで開発するにあたりコンテナでデプロイを初めて取り扱うにあたり色々試行していたのですが、
せっかくコンテナで環境をそのまま持ってけるんだからローカル環境も同じコンテナで処理したい！
でもDockerfileの多重管理はしたくない！となっていい方法がないかと模索していたところ
割と良さげな方法がありましたので備忘録がてらまとめます。

AWSのサポートに問い合わせてみたら記事執筆時点(2021/08/27)では
まだ公式のドキュメントが追いついてなく記載がなく追記予定らしいです。

こんなことしなくてももっと簡単にできるよっていうのがあったらご教示いただければと思います。

# 何がしたかったか

今回はAWS公式で提供されているpython3.8のイメージをベースとします。

大元のDockerfileはドキュメントに記載の通り以下で
`ENTRYPOINT ["/lambda-entrypoint.sh"]` が実装されており、これに対して
`CMD` で引数を渡すような作りをすることで対象のスクリプトを実行できます。
https://github.com/aws/aws-lambda-base-images/blob/python3.8/Dockerfile.python3.8

ただしこのコンテナをローカル開発で利用するにあたり以下の点が不満でした

- sam経由で実行する場合`sam build` と `sam local invoke` が必要
    - せっかくのスクリプト言語なのにいちいちビルドが必要で確認がめんどくさい
    - build問題はsamを経由せずに
    　ボリュームをマウントdockerコマンド経由で実行してやれば解決はするが次項の問題がある
- コンテナは起動するとスクリプトの実行が終わるのでコンテナを立ち上げっぱなしにできない
    - ちょっと確認のためにコンテナ内で作業したいとかがやりづらい

なのでローカル用には`ENTRYPOINT ["tail", "-f"]`を実装したDockerfileを利用し、
通常時は内部でpythonコマンド経由で実行しある程度整理できたところで、
sam経由で実行のようなことを考えていました。

ただしこのためにDockerfileを複数作って管理というのは共通部分を多重に管理しないといけないため回避したい問題でした。

# 解決方法

Dockerのマルチステージビルドを使うことで解決できました。
今回調べるまで知らなかったのですが`docker build` の`--target`オプションで指定したステージを最終成果物としてできることを知りました。

ただ`docker build --target=xxx`と同じ挙動をするものがSAMに見当たらないという点でつまづきました。
[sam buildのドキュメント](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-build.html)にもなく[SAMのアプリケーションビルド](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-using-build.html#build-container-image)のページにもなかったのでAWSに問い合わせしてみたのですが、
ドキュメントが追いついていないだけで`SAM CLI 1.12.0`で実装されていたようです。
(SAM側のテンプレートファイルの`DockerBuildTarget`で利用するステージの指定が可能)
https://github.com/aws/aws-sam-cli/issues/2578


# 各種実装

## ファイルの配置

testフォルダとか一部省略してます。
`docker-compose.yml`が増えている以外は`sam init`で作られるのと同じです。

```
.
├── src
│   ├── Dockerfile
│   ├── docker-compose.yml
│   ├── __init__.py
│   ├── requirements.txt
│   └── app.py
└── template.yaml
```

## Dockerfileの実装

最初にデプロイ用の`prod`イメージを作り、
その後にそこからローカル開発用に一部を書き換えた`dev`イメージを作成します。

```Dockerfile:Dockerfile
FROM public.ecr.aws/lambda/python:3.8 as prod

#ソースコード類をコピー
COPY ./* ./
RUN python3.8 -m pip install -r requirements.txt
CMD ["app.lambda_handler"]

FROM prod AS dev
ENTRYPOINT [ "tail", "-f"]
```

## docker-compose.yml

個人的には`docker-compose`経由の起動が楽なので`docker-compose.yml`も作っておきます。
この`docker-compose.yml`はローカルの開発専用でSAMでデプロイする場合は後述のSAMテンプレート側が読まれます。
`target: dev`を指定することでdocker-compose経由で起動する場合はdev側のイメージを利用します。

```yaml:docker-compose.yml
version: "3"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: dev
    volumes:
      - .:/var/task
```


## template.yml
SAM側のコンテナの設定系は`Metadata`で設定します(詳しくは[SAMのアプリケーションビルド](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-using-build.html#build-container-image)を参照)
このMetadataに`DockerBuildTarget`で対象のステージを指定することができるようです。
(2021/08/27時点で記載がなかったのはこのパラメータ)

なおこのパラメータをなしで実行すると最終の`dev`まで実行されたものが利用されました。

`template.yml`は以下のような形で実装します(一部省略)

```yaml:template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
...
Resources:
  CreateMntFolder:
    Type: AWS::Serverless::Function
    Properties:
      PackageType: Image
...
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./src
      DockerTag: python3.8-v1
      DockerBuildTarget: prod
```

こうすることでDockerfileを多重管理する必要もなくなり、
ちょっと確認したい時は`docker-compose up -d`経由でコンテナを立ち上げ、
`docker-compose exec app bash`で内部に入って実行もできますし、
`sam build`からの`sam local invoke`や`sam local start-api`等の各種ローカル実行系のコマンドでの確認もできます。


# 終わりに

マルチステージビルドはDocker公式チュートリアルちょっと読む限りは中間成果物があるような環境のために
使い捨ての作業イメージが作れるくらいのイメージでした。

実際はそうではなくSAMに限らずこれまで環境ごとに複数Dockerfileを作っていたのが、
一つのファイルで共通の部分の中間イメージを作りそこから個別実装ということができたり、
今回みたいに用途によって途中のステージのものを利用することもできたりと
少し視野を広く持っていればいろいろなことができそうです。


一つのDockerfileにアプリとDBのイメージを両方記載して`docker-compose.yml`で`target`パラメータをうまく指定することで
Dockerfileを一つにまとめられそうなんですがどうなんでしょうか。
同じこと考えている人はいそうなのでこの辺りはまたチュートリアルとかフォーラムとか色々みてみようかと思います。