AWS Lambda関数のローカル動作確認をDocker内Dockerで実行する

##はじめに
やりたいことはタイトルの通りですが、以下の通りです。

- ローカルでLambda関数の動作確認ができる
- VS Code Remote Containerで開発環境の構築を完結させる（ローカル環境を汚さない）
- もし可能ならデプロイも簡単に

なお、この要件マッチするツールとして、AWS Cloud9が一番手軽だと思われます。
VS Codeにこだわりがなく、Cloud9の料金に問題がなければCloud 9をお勧めします。

##環境
- 開発端末：macOS
- 開発IDE：VS CodeのRemote Containers

##前提
- AWS IAMアカウント作成済み
- Docker, Git, VS Code (Remote Container)は開発端末にインストール済み

##補足
Lambdaのローカル実行には、AWS SAM CLIが提供されていますが、こちらのローカル実行にはコンテナ環境が使われます。
つまりコンテナからコンテナを呼べないといけません。以下の構成を目指します。

|  コンテナ  |  概要  |  インストール  |
| ---- | ---- | ---- |
|  ー  |  開発端末：macOS  |　Docker, Git, VS Code (Remote Container)　|
|  コンテナ①  |  開発端末：macOSから起動するコンテナ  |　**Docker**, AWS SAM CLI　|
|  コンテナ②  |  **コンテナ①から起動するコンテナ**  |　Pythonなど利用するLambda関数に応じたもの|

##設定していく
なお、GUIベースで操作していますが、最終的に設定ファイルへ落とし込みますので、経緯が不要な場合は、[こちら](#成果物)からどうぞ
取り合えず動くものを見たいため手元にあった`docker-compose.yml`を暫定利用します。
OSイメージがAWS CLI対応のものであればなんでもいいと思います。
`privileged: true`がないとdocker内でdockerが動きません。

```yml:docker-compose.yml
version: "3"
services:
  my_lambda:
    image: rust:latest
    privileged: true # docker内でdockerを動かすために必要
```
`docker-compose.yml`を指定して、VS CodeのRemote Containersを使いコンテナ起動。

###AWS Toolkitをインストール
![スクリーンショット 2021-09-03 16.42.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/640934f2-9183-f09c-b41d-c41cf056ac53.png)
VS Codeの左バーにAWSのマークが表示されれば成功です。
ただし、インストールの際押下したボタンの通りですが、コンテナの方にインストールされており、コンテナをリビルドするとこのインストールはなかったことになりますので、設定ファイルへこの拡張機能を追加します。
![スクリーンショット 2021-09-03 17.46.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/d8254bd0-647d-7e61-61df-febea7c793e8.png)
追加されました。
![スクリーンショット 2021-09-03 17.48.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/ddd37e6c-700e-87e1-8e84-ac441ea88a6f.png)

###AWS認証情報の設定
次に認証情報の設定。以下を入力します。

- 1/3 初期プロファイルの名前：任意
- 2/3 アクセスキー：IAMのアクセスキー
- 3/3 シークレットキー：IAMのシークレットキー
![スクリーンショット 2021-09-03 16.51.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/866427b7-2f74-4fc1-251c-c805f8c875e0.png)
コンテナ内で認証情報ファイルが作成されました。

```bash:
$ ls -la ~/.aws/
total 12
drwxr-xr-x 2 root root 4096 Sep  3 07:56 .
drwx------ 1 root root 4096 Sep  3 07:56 ..
-rw------- 1 root root 1244 Sep  3 07:56 credentials
-rw------- 1 root root 1244 Sep  3 07:56 config
```

ただ、こちらもマウント対象のディレクトリではないため、コンテナをリビルドすると消えてしまいますので対処します。
プロジェクトディレクトリ配下にキー情報をおくことでマウント対象のファイルにした後で、コンテナ起動時に`~/.aws/`へコピーするやり方にしました。

そのために、まずは`~/.aws/`からプロジェクトディレクトリはいかにキーをコピーしておきます。
順番が逆になりややこしいですが、今回はコンテナの`~/.aws/`に直接作ってしまったため、次回以降のためマウント配下に保持しておく準備をしています。

```bash:作成された認証情報をコピーしてマウント対象のディレクトリへ移動
$ cp -rf ~/.aws .
$ ls -la
total 16
drwxr-xr-x  9 root root  288 Sep  3 08:53 .
drwxr-xr-x  3 root root 4096 Sep  3 06:50 ..
drwxr-xr-x  3 root root   96 Sep  3 08:53 .aws
drwxr-xr-x  3 root root   96 Sep  3 06:50 .devcontainer
drwxr-xr-x 12 root root  384 Sep  3 06:32 .git
-rw-r--r--  1 root root  320 Sep  3 06:32 .gitignore
-rw-r--r--  1 root root  767 Sep  3 06:50 Dockerfile
-rw-r--r--  1 root root   15 Sep  3 06:32 README.md
drwxr-xr-x  6 root root  192 Sep  3 07:00 stock_data
```
```json:devcontainer.jsonへ1行追記
"postCreateCommand": "cp -rf /workspace/.aws ~/",
```
なお、ローカルで保持している、キー情報を直にマウントするのが一般的かと思います。
ただし、マウントはコンテナ側の問題があった時、ローカルのマスタファイルへ反映してしまう可能性があります。
基本的に操作しないファイルですので問題ないと思いますが、VS CodeのRemote Containerの挙動はやや不安定で、以前クラッシュしてローカルのファイル含めて消えてしまったことがあるので私はマスタファイルからのマウントは行いません。

私の場合、このままだとGitHubへawsのキー情報がアップされてしまうのでgitの管理対象から外します。

```bash:.gitignoreへ1行追記
# Confidentials
credentials
config
```

リージョンを東京へ変更
![スクリーンショット 2021-09-03 17.00.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/a666e6d7-708d-22eb-b8dd-f7bb1420bbb8.png)
つながりました。試しにLambdaを選択してみると過去にアップした関数が見えています。
![スクリーンショット 2021-09-03 17.03.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/fb273ced-8ca2-1714-72c9-d076d1700e5f.png)

###AWS SAM CLIのインストール
早速開発をしようと、Lambdaを右クリックし、「Create Lambda SAM Application」を選択したところSAM CLIがないとのことでリンク先でインストール手順を確認します。
![スクリーンショット 2021-09-03 17.06.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/e6fa0f86-c16b-a0ad-2a7e-fa9bd87bd8fc.png)

Linuxはzipファイルを使うようですが、「Docker および AWS CLI が必要です。インストール手順をご確認ください」とあるため、インストール手順を確認します。
参考：

https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install-linux.html


インストール手順は以下のとおりですが、1-2は済んでいるため、3（コンテナ①の中にコンテナ②を作るためにコンテナ①でDockerのインストールが必要）、4だけで良さそうです。
> 1. Create an AWS account.
>2. Configure AWS Identity and Access Management (IAM) permissions and AWS credentials.
>3. Install Docker. Note: Docker is a prerequisite only for testing your application locally or using the --use-container option.
>4. Install the AWS SAM CLI.

#####3. Install Docker.
長いので実行したコマンドを列挙します。
基本的には、LinuxにDockerをインストールする手順通りに実施しています。
参考：

https://docs.docker.com/engine/install/debian/


```bash:コンテナ①にて実行
$ apt-get update
$ apt-get install apt-transport-https ca-certificates curl gnupg lsb-release

$ curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

$ echo  "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null

$ apt-get update
$ apt-get install docker-ce docker-ce-cli containerd.io

$ apt-cache madison docker-ce
 docker-ce | 5:20.10.8~3-0~debian-bullseye | https://download.docker.com/linux/debian bullseye/stable amd64 Packages
 docker-ce | 5:20.10.7~3-0~debian-bullseye | https://download.docker.com/linux/debian bullseye/stable amd64 Packages
 docker-ce | 5:20.10.6~3-0~debian-bullseye | https://download.docker.com/linux/debian bullseye/stable amd64 Packages
// 上のコマンドで表示されたバージョンを選んで指定
$ apt-get install docker-ce=5:20.10.8~3-0~debian-bullseye docker-ce-cli=5:20.10.8~3-0~debian-bullseye containerd.io

$ service docker start
$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
```

#####4. Install the AWS SAM CLI.

```bash:コンテナ①にて実行
// DL
$ wget https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip

// 解凍
$ unzip aws-sam-cli-linux-x86_64.zip -d sam-installation

// インストール（既にルートユーザだったのでsudoなしで実行）
$ ./sam-installation/install
You can now run: /usr/local/bin/sam --version
```

ここで問題が出ました。
3, 4共にリビルド時にも再実行されるようDocker設定ファイルに記載する必要がありますが、`devcontainer.json`の`postCreateCommand`は1行しか書けず可読性が良くない。
![スクリーンショット 2021-09-03 19.15.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/fdcb565b-86e9-4221-5569-0acd92e1f339.png)
`docker-compose.yml`に書いたらいいのですが、開発環境で使う設定をdevcontainer.jsonにまとめておきたかったので[こちら](https://stackoverflow.com/questions/65909781/is-there-a-way-to-debug-the-postcreatecommand-in-vscode-devcontainers)を参考にして書き直してみます。

また、zipファイルと解凍をマウントディレクトリに置いておくには大きすぎる。zipは消せば良いですが「sam-installation」が大きいことには変わりないので設定ファイルでは、マウント対象外のディレクトリで作業するようにします。

これらの方針をもとに構成変更と設定を行なっていきます。
まずは、構成変更。

```bash:変更前構成
/workspace
　├──.devcontainer
　│　└──devcontainer.json
　├──aws-sam-cli-linux-x86_64.zip
　└──sam-installation
```
```bash:変更後構成
/workspace
　├──.devcontainer
　│　├──devcontainer.json
　│　└──postCreateCommand.sh // 新規作成
/tmp
　├──aws-sam-cli-linux-x86_64.zip // コンテナ作成時DLするようにする
　└──sam-installation // コンテナ上記zipを解凍して作成するようにする
```
では、この構成にするために設定ファイルを変えていきます。

```json:.devcontainer/devcontainer.json
// "postCreateCommand": "cp -rf /workspace/.aws ~/",
"postCreateCommand": "chmod 755 .devcontainer/postCreateCommand.sh && ./.devcontainer/postCreateCommand.sh",
"postStartCommand": "service docker start",
```
最終行はコンテナ②のプロセススタートですが、コンテナ①停止でコンテナ②が止まってしまうのでpostCreateCommandに書いてあると、手動でスタートしないといけない場面があったため、以下を参考にしてpostStartCommandに記載しました。：

https://vlike-vlife.netlify.app/posts/vscode_remote_container_command

```sh:.devcontainer/postCreateCommand.sh
#!/bin/bash
echo "Installing Developer Requirements"

# AWS KEY
cp -rf /workspace/.aws ~/

# Docker Install
apt-get update
apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo  "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io
#apt-cache madison docker-ce
apt-get install -y docker-ce=5:20.10.8~3-0~debian-bullseye docker-ce-cli=5:20.10.8~3-0~debian-bullseye containerd.io

# AWS SAM CLI Install
wget  -P /tmp https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip
unzip /tmp/aws-sam-cli-linux-x86_64.zip -d /tmp/sam-installation
/tmp/sam-installation/install
rm -rf /tmp/aws-sam-cli-linux-x86_64.zip
```

ここで、いったんDLしたものを削除して、リビルドします。

```bash
$ rm -rf aws-sam-cli-linux-x86_64.zip
$ rm -rf sam-installation
```
リビルドは、VS Codeの右下の緑をクリックして[Rebuild Container]を選択。
再度立ち上がりましたので、Lambdaで「Create Lambda SAM Application」が選択できることを確認。
![スクリーンショット 2021-09-03 20.11.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/65341b14-cba4-e9f8-cd94-cf9036f7e65a.png)

##Hello World
そのまま選択肢から選んでpythonのhelloworldアプリケーションを作成してみました。
`sam init`コマンドで進めた方がわかりやすいかもしれません。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started-hello-world.html

少し経つとアプリケーションフォルダが作成されます。
![スクリーンショット 2021-09-04 12.10.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/c5baed3b-2008-67da-0175-628a0113846b.png)
チュートリアルに従って、動作確認できるかみていきます。

####Lambda 関数を直接呼び出す

```bash
$ cd lambda-python3.9
$ sam build
$ sam local invoke
（略）
{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}
```
####API をローカルでホストする

```bash
$ cd lambda-python3.9
$ sam build
$ sam local start-api
Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2021-09-04 02:23:40  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)

// 別のターミナルを立ち上げて実行（レスポンスが表示されるまで時間かかります）
$ curl http://127.0.0.1:3000/hello
{"message": "hello world"}
```
ブラウザからも表示を確認できました。
![スクリーンショット 2021-09-04 12.31.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/e7dd1ef6-4f38-c7b6-0aee-05e2ac3fb148.png)

####デプロイ
※S3バケットが必要になりますので、ない方は作成してください。 
※（イメージのデプロイの場合）ECRのリポジトリが必要になりますので、ない方は作成してください。 
![スクリーンショット 2021-09-04 12.38.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/6efe5162-243a-db45-7ada-566125f74036.png)

 1/5 `lambda-python3.9/template.yaml`を選択
 2/5 既存のS3バケットからデプロイイメージをアップするS3を選択
 3/5 既存のECRのリポジトリからデプロイイメージをプッシュするECRを選択
 4/5 なし
 5/5 Stack名を入力します。新規デプロイならここで命名となります。　例）HelloWorld-Python

######デプロイエラーとその解決にIAMに必要だった権限
ここから権限エラーがたくさん出ました。
権限はAWSコンソールから利用しているIAMユーザに権限をアタッチするやり方で追加していきました。
ポリシーを直接編集した方が早い気がします。

######AWSLambda_FullAccess
本開発をするにあたりLambda操作用に作成したIAMユーザに追加した権限

######AmazonS3FullAccess
デプロイ時にS3バケットを選択する必要があったため

######AmazonEC2ContainerRegistryFullAccess
デプロイ時の以下エラー解消のため

```bash
User: arn:aws:iam::9999999999:user/{my_user} is not authorized to perform: ecr:DescribeRepositories on resource: {my-repo}
```

######AWSCloudFormationFullAccess
デプロイ時の以下エラー解消のため

```bash
Error: Failed to create changeset for the stack: HelloWorld-Python, An error occurred (AccessDenied) when calling the CreateChangeSet operation: User: arn:aws:iam::9999999999:user/{my_user} is not authorized to perform: cloudformation:CreateChangeSet on resource: arn:aws:cloudformation:{my-repo}:stack/HelloWorld-Python/*
```
######IAMFullAccess
デプロイ時、CloudFormationで以下が発生
> API: iam:CreateRole User: arn:aws:iam::9999999999:user/{my_user} is not authorized to perform: iam:CreateRole on resource: role arn:aws:iam::9999999999:role/HelloWorld-Python-HelloWorld-PythonFunctionRole-1XBRI8W1QTJFN

```bash:SAMのログからは詳細不明
Error: Failed to create/update the stack: HelloWorld-Python, Waiter StackCreateComplete failed: Waiter encountered a terminal failure state: For expression "Stacks[].StackStatus" we matched expected path: "ROLLBACK_COMPLETE" at least once
```
![スクリーンショット 2021-09-04 19.26.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/415634f0-bc2a-ab72-67c9-6624c1e96326.png)

######AmazonAPIGatewayAdministrator
デプロイ時、CloudFormationで以下が発生
> User: arn:aws:iam::9999999999:user/{my_user} is not authorized to perform: apigateway:POST on resource: arn:aws:apigateway:{my-repo}::/restapis (Service: AmazonApiGateway; Status Code: 403; Error Code: AccessDeniedException; Request ID: 02e82b5c-3703-49d0-bb6a-7ffc84a2420e; Proxy: null)

（IAMFullAccessと同様のエラーの出方のためログは略）

--

開発とデプロイで分けたり、そもそもユーザではなくグループにアタッチしたいですね。
もっというと既存ポリシーは不要な権限（本来許容したくない権限もセットで含まれている）が多いため、必要なポリシーを手打ちして作成すべきかと思います。
今回は、とりあえずhelloworldを動かすためと思いコンソールのボタン押下で済む進め方をしました、ものすごく時間がかかってしまいました。

####デプロイ成功
ラムダ関数が追加されました。
![スクリーンショット 2021-09-04 20.36.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/859733d0-a6ab-ab6a-3527-012d4b566945.png)
ログに表示されたURLにアクセスして、無事イベント発行と関数実行ができていることを確認しました。
![スクリーンショット 2021-09-04 20.44.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/380300/9897335a-6814-6ea1-c4cb-7f42c9162749.png)

デプロイの情報を`samconfig.toml`としてルートに置くと次回以降のデプロイが簡略化するようなので、ログに表示されていた内容をもとに作成します。

##成果物
色々とごちゃごちゃしたので最終的な構成と設定を整理します。

```bash:変更後構成
/workspace
　├──.aws
　│　  ├──config
　│　  └──credentials
　├──.devcontainer // VS Code用設定ファイル
　│　  ├──devcontainer.json
　│　  ├──docker-compose.yml
　│  　└──postCreateCommand.sh // コンテナ①の各種インストールコマンド
　├──lambda-python3.9 // sam initで作成したアプリ（コンテナ②のDockerfileはここ）
　│　  └──samconfig.toml
　└──docker-compose.yml // コンテナ①
/root
　└──.aws // コンテナ①のsamのインストーラ（コンテナ起動時に/workspace/.awsをコピーして作成）
/tmp
　└──sam-installation // コンテナ①のsamのインストーラ（コンテナ起動時にDL）
```
```:.aws/credentials
[default]
aws_access_key_id = xxxxxxxxxxx
aws_secret_access_key = xxxxxxxxxxx
```
```:.aws/config
[default]
region=ap-northeast-1
output=json
```
```json:.devcontainer/devcontainer.json
{
	"name": "Existing Docker Compose (Extend)",
	"dockerComposeFile": [
		"../docker-compose.yml",
		"docker-compose.yml"
	],
	"service": "my_lambda",
	"workspaceFolder": "/workspace",
	"settings": {},
	"extensions": [
		"amazonwebservices.aws-toolkit-vscode"
	], 
	"postCreateCommand": "chmod 755 .devcontainer/postCreateCommand.sh && ./.devcontainer/postCreateCommand.sh",
	"postStartCommand": "service docker start",
}
```
```yaml:.devcontainer/docker-compose.yml
version: '3'
services:
  my_lambda:
    volumes:
      - .:/workspace:cached
      -     command: /bin/sh -c "while sleep 1000; do :; done"
```
```sh:.devcontainer/postCreateCommand.sh
#!/bin/bash
echo "Installing Developer Requirements"

# AWS KEY
cp -rf /workspace/.aws ~/

# Docker Install
apt-get update
apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo  "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io
#apt-cache madison docker-ce
apt-get install -y docker-ce=5:20.10.8~3-0~debian-bullseye docker-ce-cli=5:20.10.8~3-0~debian-bullseye containerd.io

# AWS SAM CLI Install
wget  -P /tmp https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip
unzip /tmp/aws-sam-cli-linux-x86_64.zip -d /tmp/sam-installation
/tmp/sam-installation/install
rm -rf /tmp/aws-sam-cli-linux-x86_64.zip
```
```toml:lambda-python3.9/samconfig.toml
version = 0.1
[default]
[default.deploy]
[default.deploy.parameters]
stack_name = "HelloWorld-Python"
s3_bucket = "xxxxxxxxxxxxxxxxxxxxxx"
image_repository = "xxxxxxxxxxxxxxxx"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND"]
```

```yaml:docker-compose.yml
version: "3"
services:
  my_lambda:
    image: rust:latest
    privileged: true # docker内でdockerを動かすために必要
```

##おわりに
本当は、カスタムランタイムでRustのLambda関数を作りたかったのですが、Dockerにsamを入れて使っている開発環境の情報が見つけられなかったことと、サーバレス関連のリソースをほとんど触ったことがないのにエラー祭りだったため、Pythonのhello Worldを一気通貫するところをまず目指しました。
これから、少しずつアレンジしていければと思います。
ありがとうございました。

##追記
動作が不安定だったのでDockerではなくMacでも直接動かせるようにしました。。

```bash:rust系
$ brew install rustup rust-analyzer
$ rustup-init
$ rustup update
$ export PATH="$HOME/.cargo/bin:$PATH"
$ cargo install cargo-edit
$ rustup target add x86_64-unknown-linux-musl
```
```bash:sam&aws系
$ brew tap aws/tap
$ brew install aws-sam-cli
$ cp -prf ~/work/docker/my_rust/my_stock_data/.aws ~/.
```
```bash:リンカー
$ brew install filosottile/musl-cross/musl-cross
$ cd my_lambda_function
$ mkdir .cargo
$ echo '[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"' > .cargo/config
$ ln -s /usr/local/bin/x86_64-linux-musl-gcc /usr/local/bin/musl-gcc
```