【ServerlessFramework】slsコマンドをコンテナ化する

slsコマンドが環境に依存してしまうのがイヤで、slsコマンドをコンテナ化してみました。
サクッと作れるかと思いきや、意外と苦戦しました。。。
 [serverless-python-requirements](https://www.serverless.com/plugins/serverless-python-requirements) プラグインを利用すると、dockerを利用してパッケージをビルドするのですが、こいつが曲者で、slsコマンド自体をコンテナ化しようとすると、コンテナ内からDockerコマンドが実行できてかつ、別コンテナでビルドした資材を参照できなくてはなりません。

いろいろ試行錯誤したので、まとめておこうかと思います。

一応、今回のソースはGitHubにpushしていますので、ソースの確認はこちらからどうぞ。

https://github.com/ng3rdstmadgke/sls-container

# ディレクトリ構成


```
sls-sample/
| app/               // lambdaにデプロイするソースコード (今回はfastapiのwebアプリ)
| | __init__.py
| | main.py
| bin/
| | build.sh*        // イメージをビルドするスクリプト
| | sls.sh*          // コンテナを起動するコマンド
| docker/
| | sls/
| | | Dockerfile     // slsコンテナのDockerfile
| .dockerignore
| package-lock.json
| package.json
| requirements.txt   // lambdaの依存パッケージ定義(layerにする)
| serverless.yml
```



# イメージ作成
コンテナ内でDockerコマンドを実行する方法は、コンテナ内でDockerデーモンを起動する「Docker in Docker(DinD)」と、docker.sockをマウントしてホスト側のDockerを操作する「Docker outside of Docker(DooD)」があります。DinDはさすがにオーバーヘッドが大きすぎるので、今回はDooDで作っていきます。
とはいえ、コンテナ内でDockerを利用する以上、Dockerのインストールは必須ですので、FROMイメージは[公式のDinDのイメージ](https://hub.docker.com/_/docker)( `docker:20.10.12-dind` )を利用します。
このイメージは `alpine:3.15` が元になっていますので `apk` コマンドで必要なパッケージをインストールしていけば良さそうです。

※ DinD, DooDはこちらの記事が非常に参考になりました。
 [コンテナからコンテナを操作する | 二畳半堂](https://blog.nijohando.jp/post/docker-in-docker-docker-outside-of-docker/) 
[Dockerコンテナ内からDockerを使うことについて](https://esakat.github.io/esakat-blog/posts/docker-in-docker/)


```docker/sls/Dockerfile
FROM docker:20.10.12-dind
RUN printenv
RUN apk update && apk add --no-cache nodejs npm
RUN npm install -g serverless@3.7.9
RUN mkdir /opt/sls
WORKDIR /opt/sls
COPY . .
RUN npm install
```

ビルドスクリプトを作ります。
※ 私はいつもオプションを忘れてしまうので、このくらいでもスクリプト化してしまいます。。。
```bin/build.sh
#!/bin/bash
PROJECT_ROOT=$(cd $(dirname $0)/..; pwd)
cd $PROJECT_ROOT
docker build --rm -f docker/sls/Dockerfile -t sample-sls:latest .
```
# コンテナの起動

コンテナの起動は少しポイントがあります。
基本は `sls` コマンドでコンテナを起動させればいいのですが、マウントするディレクトリが結構重要です。

- `-v ${HOME}/.aws:/root/.aws:ro`
ホスト側のAWSのcredentialsを参照できるようにします。
- `-v /root/.cache:/root/.cache`
serverless-python-requirementsによって別コンテナでビルドされた資材はホストの `${HOME}/.cache/serverless-python-requirements` に配置されますので、slsコンテナからビルド資材を参照できるように `/root/.cache` をマウントします。
※ .serverlessディレクトリの中を見ると `~/.cache/serverless-python-requirements/...` へのシンボリックリンクがあります。
- `-v /var/run/docker.sock:/var/run/docker.sock` 
Dockerコマンドがホスト側のDocker環境で実行されるように、 `docker.sock` をマウントします。
- `-v ${PROJECT_ROOT}/app:/opt/sls/app`
lambdaにデプロイするディレクトリをマウントします。(複数ある場合はすべて)
- `-v ${PROJECT_ROOT}/requirements.txt:/opt/sls/requirements.txt`
pythonの依存パッケージ定義ファイルをマウントします。
- `-v ${PROJECT_ROOT}/serverless.yml:/opt/sls/serverless.yml`
ServerlessFrameworkの設定ファイルをマウントします。

```bin/sls.sh
#!/bin/bash
PROJECT_ROOT=$(cd $(dirname $0)/..; pwd)
docker run -ti --rm \
  -v ${HOME}/.aws:/root/.aws:ro \
  -v /root/.cache:/root/.cache \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v ${PROJECT_ROOT}/app:/opt/sls/app \
  -v ${PROJECT_ROOT}/requirements.txt:/opt/sls/requirements.txt \
  -v ${PROJECT_ROOT}/serverless.yml:/opt/sls/serverless.yml \
  sample-sls:latest sls $*

```

# ビルド & デプロイ手順
```bash
chmod 755 ./bin/*.sh

# イメージのビルド
./bin/build.sh

# デプロイ
./bin/sls.sh deploy

# ブラウザでAPIにアクセス
# https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/api/hello
```


# 参考

https://www.serverless.com/plugins/serverless-python-requirements

https://blog.nijohando.jp/post/docker-in-docker-docker-outside-of-docker/

https://esakat.github.io/esakat-blog/posts/docker-in-docker/