mac環境でSAM CLIと仮想環境LinuxのDockerを使用してLambdaをローカル実行する

AWS Serverless Application Model(SAM)のローカル実行時のコンテナ実行環境として、mac上のLinux仮想環境にインストールしたDocker エンジンを使用する手順を検証します。

# なぜ、このような環境が必要か

* 手元mac端末でvscodeとAmazon Q develoerを使ったlambda開発がしたい
* サクサク開発/デバッグするためSAMのローカル実行を使用したい
* SAMのローカル実行にはコンテナエンジンが必要だが諸事情によりmacで直接稼働するDocker(Docker Desktop)は使用したくない

# 環境

- 手元mac端末
    - m1 macbook air sonoma 14.7.1
- 仮想環境
    - UTM 4.5.4
- 仮想環境用 Linux
    - ubuntu 24.04.1 LTS(ARM64)
- Docker
    - Docker CE 5.27.4.0
- SAM CLI
    - 1.125.0

# 実現したいアーキテクチャ
全体イメージ図です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/03aa8ca2-08ae-064a-618f-e1283c32d9bd.png)

今回検証するのは「macのターミナルでsam invokeを実行すると、仮想環境LinuxのDockerでLambdaが実行される」の部分だけです。

:::note
図中に「sam code」とある箱の部分ですが、後述するようにmacでSAMを実行して作成されるファイルを、Dockerエンジンが稼働するLinuxから直接参照できるようにする必要があります。NFSやSambaなど参照のやり方はいろいろありますが、今回はUTMの共有ディレクトリ機能を使用しています。
:::

# 検証手順
## SAM CLI、UTMのインストール
SAM CLIのインストール、UTMのインストールに関しては一般的な話のため詳細割愛します。

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

UTMはhomebrewでインストールしました。

## UTM仮想マシン作成
母艦となるmacのディスク残量が厳しいため、ubuntuを最小構成でインストールします。Dockerを動かすだけなのでDesktop環境など追加のパッケージは不要です。

UTMの仮想マシンは「仮想化」で作成します。インストールするLinuxはARM版が必要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/1b6219b2-4d7c-d32c-e749-58e202d8cecb.png)

メモリー2GB、ディスク8GBで構成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/cbea296d-87d1-6f85-853b-2d440e1588c4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/9eb087b6-7334-5ad5-5b80-6eb368d40f31.png)


「共有ディレクトリ」を設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/c4d03432-4790-8091-3eb8-770cff65e7de.png)

後段でsamの実行ディレクトリとなるディレクトリを含むようにパスを指定します。
私はvscodeのワークスペース用に作ったフォルダを指定しました。

## Ubuntu 導入
導入オプションとしてUbuntu server(minimized)を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/534f12a4-e3d9-1ca1-9cca-c4228fad56b3.png)

ネットワークやディスク構成はデフォルトのまま進めます。

ログインユーザとしては"user01"を作成しました
パスワードなどは適宜設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/6e9230e5-9605-ca1b-7d8e-2a87b5ff0e45.png)

インストーラで選択できるDockerはインストールしませんでした。Dockerは個別にインストールします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/c35c399e-cf4b-df3b-6734-316fc1d1c8a3.png)

インストールが完了したらインストールイメージのISOをドライブから外して再起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245869/18d324ca-aa41-8c98-67e3-7d1cf7ce1937.png)

Linuxの初期導入が完了しました。

## Linuxの設定
### Docker CEインストール
Docker CEをインストールします。手順は公式手順に従います。
https://docs.docker.jp/engine/installation/linux/docker-ce/ubuntu.html

### ログインユーザuser01のDokcerグループへの追加

```console:グループへの追加
$ usermod -aG docker user01
```

### 設定ファイル編集用にvimインストール

```console:vimインストール
$ apt-get -y install vim
```

### Dockerの設定
Dockerにリモートのmacから接続できるように設定します。設定ファイルは`/usr/lib/systemd/system/docker.service`です。

```conf:/usr/lib/systemd/system/docker.service(更新前)
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
```

`ExecStart`の行に`-H tcp://(linux仮想マシンのIPアドレス)`を追記します。
私の環境では192.168.64.12でした。

```conf:/usr/lib/systemd/system/docker.service(更新後)
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://192.168.64.12 --containerd=/run/containerd/containerd.sock
```

### カーネルモジュールの設定
UTMの共有フォルダは9pという仕組みを使っているようです。
https://docs.getutm.app/guest-support/linux/#virtfs

9pのマウントに必要な機能はUbuntuの場合カーネルモジュールになっていました。

UTMの共有フォルダを自動マウントできるように、カーネルモジュールの自動ロード設定をします。
設定ファイルは/etc/modules-load.d/modules.confです。

```conf:/etc/modules-load.d/modules.conf
# /etc/modules is obsolete and has been replaced by /etc/modules-load.d/.
# Please see modules-load.d(5) and modprobe.d(5) for details.
#
# Updating this file still works, but it is undocumented and unsupported.
9pnet_virtio
9p
```
`9pnet_virtio`、`9p`を追記します。

### UTMの共有フォルダ自動マウント

UTMの共有フォルダのマウントパスを作成します。ここは何でもよいですが、/mnt/utmshareとしました。

```console
$ mkdir /mnt/utmshare
```

OS起動時の自動マウントを設定します。
以下を`/etc/fstab`に追記します。

```conf:/etc/fstab
share	/mnt/utmshare	9p	trans=virtio,version=9p2000.L,rw,_netdev,nofail	0	0
```

設定が終わったら再起動します。

### 設定確認

dockerdが設定した引数(`-H tcp://192.168.64.12`)で起動していることを確認します。

```shell-session
user01@ubuntu01:~$ ps -ef | grep docker
root         706       1  0 03:41 ?        00:00:00 /usr/bin/dockerd -H fd:// -H tcp://192.168.64.12 --containerd=/run/containerd/containerd.sock
user01      1043     807  0 03:41 pts/0    00:00:00 grep --color=auto docker
```

UTMの共有ディレクトリ`/mnt/utmshare`がマウントされていることを確認します

```shell-session
user01@ubuntu01:~$ df
Filesystem                        1K-blocks      Used Available Use% Mounted on
tmpfs                                200340      1036    199304   1% /run
efivarfs                                256        25       232  10% /sys/firmware/efi/efivars
/dev/mapper/ubuntu--vg-ubuntu--lv   5813344   2639876   2857260  49% /
tmpfs                               1001684         0   1001684   0% /dev/shm
tmpfs                                  5120         0      5120   0% /run/lock
/dev/vda2                           1768056    100636   1559288   7% /boot
/dev/vda1                            549804      6508    543296   2% /boot/efi
share                             239362432 192441088  46921344  81% /mnt/utmshare
tmpfs                                200336        12    200324   1% /run/user/1000
```

## macの設定
Linux側の準備ができたので、mac側の設定を行います。

### DOCKER_HOSTの設定
まずSAM CLIを実行するターミナルで以下を設定します。
`export DOCKER_HOST=(Linux仮想マシンのIPアドレス):2375`
私の環境ではLinux仮想マシンのIPアドレスは`192.168.64.12`なので、以下のように設定します。

```shell-session
% export DOCKER_HOST=192.168.64.12:2375
```

### SAMのテンプレート作成
samで使用するディレクトリで以下を実行し、SAMの必要ファイルを作成します。
m1 macのarmアーキテクチャの仮想環境で動かすので、`-a arm64`が必要になります。

```console
% sam init -a arm64
```

今回は検証なので、Hello Worldのサンプルを使用します。言語はPythonとし、サンプルのコードも特に変更しません。

<details><summary>sam init実行結果</summary>

```shell-session
% sam init -a arm64
Which template source would you like to use?
1 - AWS Quick Start Templates
2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
1 - Hello World Example
2 - Data processing
3 - Hello World Example with Powertools for AWS Lambda
4 - Multi-step workflow
5 - Scheduled task
6 - Standalone function
7 - Serverless API
8 - Infrastructure event management
9 - Lambda Response Streaming
10 - Serverless Connector Hello World Example
11 - Multi-step workflow with Connectors
12 - GraphQLApi Hello World Example
13 - Full Stack
14 - Lambda EFS example
15 - DynamoDB Example
16 - Machine Learning
Template: 1

Use the most popular runtime and package type? (Python and zip) [y/N]: 

Which runtime would you like to use?
1 - dotnet8
2 - dotnet6
3 - go (provided.al2)
4 - go (provided.al2023)
5 - graalvm.java11 (provided.al2)
6 - graalvm.java17 (provided.al2)
7 - java21
8 - java17
9 - java11
10 - java8.al2
11 - nodejs20.x
12 - nodejs18.x
13 - nodejs16.x
14 - python3.9
15 - python3.8
16 - python3.12
17 - python3.11
18 - python3.10
19 - ruby3.3
20 - ruby3.2
21 - rust (provided.al2)
22 - rust (provided.al2023)
Runtime: 16

What package type would you like to use?
1 - Zip
2 - Image
Package type: 1

Based on your selections, the only dependency manager available is pip.
We will proceed copying the template using pip.

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: 

Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: 

Would you like to set Structured Logging in JSON format on your Lambda functions?  [y/N]: 

Project name [sam-app]: 

    -----------------------
    Generating application:
    -----------------------
    Name: sam-app
    Runtime: python3.12
    Architectures: arm64
    Dependency Manager: pip
    Application Template: hello-world
    Output Directory: .
    Configuration file: sam-app/samconfig.toml
    
    Next steps can be found in the README file at sam-app/README.md
        

Commands you can use next
=========================
[*] Create pipeline: cd sam-app && sam pipeline init --bootstrap
[*] Validate SAM template: cd sam-app && sam validate
[*] Test Function in the Cloud: cd sam-app && sam sync --stack-name {stack-name} --watch
```

</details>

### SAMビルド実行
`sam build`を実行します

```
% sam build
(略)                                     

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
[*] Deploy: sam deploy --guided
```

"Built Artifacts"のディレクトリ配下にテンプレートやアプリケーションコード(app.py)が出力されます。このパスをローカル実行時の引数で使用します。

```shell-session
% ls .aws-sam/build
HelloWorldFunction	template.yaml
% ls .aws-sam/build/HelloWorldFunction 
__init__.py				charset_normalizer-3.4.0.dist-info	requirements.txt
app.py					idna					urllib3
certifi					idna-3.10.dist-info			urllib3-2.2.3.dist-info
certifi-2024.8.30.dist-info		requests
charset_normalizer			requests-2.32.3.dist-info
```

### SAM ローカル実行

ローカル実行のコマンドを実行します

```console
sam local invoke --container-host 192.168.64.12 --container-host-interface 0.0.0.0 --docker-volume-basedir /mnt/utmshare/test01/sam-app/.aws-sam/build
```

:::note
` --docker-volume-basedir`には **「Linux仮想マシン側から見えるBuilt Artifactsのディレクトリ」** を指定します。samを実行するmac側のディレクトリではないことに注意です。
:::

:::note
`--container-host`と`--container-host-interface`はローカルマシンで稼働していないDockerを使用する場合に必要なようです。
:::

```shell-session:sam local invoke実行結果
% sam local invoke --container-host 192.168.64.12 --container-host-interface 0.0.0.0 --docker-volume-basedir /mnt/utmshare/test01/sam-app/.aws-sam/build
Invoking app.lambda_handler (python3.12)                                                                                                       
Local image was not found.                                                                                                                     
Removing rapid images for repo public.ecr.aws/sam/emulation-python3.12                                                                         
Building image................................................................................................................................................................................................................................................................................................................................................................................................................................................................
Using local image: public.ecr.aws/lambda/python:3.12-rapid-arm64.                                                                              
                                                                                                                                               
Mounting /mnt/utmshare/test01/sam-app/.aws-sam/build/HelloWorldFunction as /var/task:ro,delegated, inside runtime container                    
START RequestId: 12113ae4-b112-4d6a-853d-0e9564d2bfed Version: $LATEST
END RequestId: 21972317-d3d8-41c6-abf8-b1ec673ee401
REPORT RequestId: 21972317-d3d8-41c6-abf8-b1ec673ee401 Init Duration: 0.02 ms Duration: 54.31 ms Billed Duration: 55 ms Memory Size: 128 MB Max Memory Used: 128 MB 
{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}
```

無事仮想環境LinuxのDockerを使ってローカル実行のLambdaが実行されました。

Linux側の`docker ps`でコンテナが実行されていることが確認できます。

```shell-session:
$ docker ps
CONTAINER ID   IMAGE                                           COMMAND                  CREATED        STATUS                  PORTS                    NAMES
1c4533c23445   public.ecr.aws/lambda/python:3.12-rapid-arm64   "/var/rapid/aws-lamb…"   1 second ago   Up Less than a second   0.0.0.0:6708->8080/tcp   distracted_joliot
```

# 最後に
本稿では、macからリモートのDockerを使用してSAMローカル実行を行う手順を検証しました。

今回は検証なので簡易に実装できる方法として仮想環境のLinuxを使用しましたが
同様の手順で別物理PCで稼働しているDocker環境を仕様し、コードの共有にsamba/NFSを使ったSAMローカル実行も可能と考えます。

同じようなケースで困っていることは少ないかもしれませんが、ぜひお役立てください。