あなたのLambdaが動いているのはEC2の上?それともFirecrackerの上?

サーバーレス開発部@大阪の岩田です。 Lambdaの実行環境は

  • EC2モデル
  • Firecrackerモデル

2つのモデルに分かれることが知られていますが、Lambdaがどちらのモデル上で実行されているかを判断できるかもしれない方法を発見したのでご紹介します。

筆者の想像を多分に含む内容なので、実際の環境とは異なる可能性が高いことを事前にご了承ください。

環境

今回利用した環境です

  • EC2インスタンス: i3.metal
  • OS: Ubuntu Server 18.04 LTS (HVM) SSD Volume Type x86_64 (ami-024a64a6685d05041)
  • Firecracker: v0.17.0

EC2モデルとFirecrackerモデルの違い

まずはおさらいから。 AWS公式の資料Security Overview of AWS Lambdaに分かりやすくまとまっていますが、Lambdaの実行環境は

  • EC2モデル
  • Firecrackerモデル

2つのモデルに分かれます。

EC2モデル

EC2モデルはLambdaのサービス開始当初から利用されているモデルです。 EC2インスタンス上でMicroVMが稼働し、さらにMicroVMの上に構築されたサンドボックス環境内でLambdaが実行されます。EC2モデルではEC2インスタンスがAWSアカウントの境界となり、EC2インスタンスとMicroVMの関係は1:1の関係に、MicroVMとLambda実行環境は1:Nの関係になります。

Firecrackerモデル

対するFirecrackerモデルは2018年から導入されたモデルです。ベアメタルのEC2インスタンス上でFirecrackerを利用して何十万のMicroVMを稼働させるモデルです。FirecrackerモデルではMicroVMがAWSアカウント間の境界となり、MicroVMとLambda実行環境は1:1の関係になります。

両モデルの対比

EC2モデルとFIrecrackerモデルの対比は以下の画像のようになります。

Lambda実行環境の対比 EC2モデルとFirecrackerモデル

※Security Overview of AWS Lambdaより引用

この辺りの解説はこちらの記事もご参照下さい。 2019年VPC Lambdaが高速に!! AWS Lambdaの内部構造に迫るセッション 「SRV409 A Serverless Journey: AWS Lambda Under the Hood」 #reinvent

EC2モデルとFirecrackerモデルを判別できるかもしれない方法

ここからが本題です。ある日Lambda実行環境でOSコマンドを叩きながら色々分析していたところ、タイミング次第でfindmntコマンドの出力結果が変わることに気づきました。

パターン1

TARGET                              SOURCE
/                                   /dev/root[/opt/amazon/asc/worker/rtfs/cache/<ランダムな文字列>.flat]
├─/var/task                         /dev/loop1
├─/dev                              /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/sandbox-dev]
├─/tmp                              /dev/loop0
├─/proc                             none
│ └─/proc/sys/kernel/random/boot_id /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/boot_id-<ランダムな文字列>]
├─/etc/passwd                       /dev/root[/etc/passwd]
├─/var/runtime                      /dev/root[/opt/amazon/asc/worker/runtime/nodejs-8.x]
├─/var/lang                         /dev/root[/opt/amazon/asc/worker/lang/node-v8.10.x]
└─/etc/resolv.conf                  /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/resolv.conf<ランダムな文字列>]

パターン2

TARGET                              SOURCE
/                                   /dev/root[/opt/amazon/asc/worker/rtfs/cache/<ランダムな文字列>.flat]
├─/var/task                         /dev/vdb[/opt/amazon/asc/worker/tasks/<AWSアカウントID>/<Lambda関数名>/<UUIDらしきランダムな文字列>]
├─/dev                              /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/sandbox-dev]
├─/tmp                              /dev/loop0
├─/proc                             none
│ └─/proc/sys/kernel/random/boot_id /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/boot_id-<ランダムな文字列>]
├─/etc/passwd                       /dev/root[/etc/passwd]
├─/var/runtime                      /dev/root[/opt/amazon/asc/worker/runtime/nodejs-8.x]
├─/var/lang                         /dev/root[/opt/amazon/asc/worker/lang/node-v8.10.x]
└─/etc/resolv.conf                  /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/resolv.conf<ランダムな文字列>]

注目して欲しいのが/var/taskのマウントソースです。 /dev/loop1の場合と/dev/vdb[/opt/amazon/asc/worker/tasks/<AWSアカウントID>/<Lambda関数名>/<UUIDらしきランダムな文字列>]の場合と2パターンに分かれるんです!! 最初はLambdaのランタイムによって変わるのかと思い、ランタイムを変えたりレイヤーを付け外ししたり色々試したのですが、ランタイムは同じでもタイミング次第で/var/taskのマウントソースが変わることを発見しました。これ、何かLambdaの実行モデルによる違いっぽくないですか??

EC2モデルと違ってFirecrackerモデルは1つのベアメタルインスタンス内に複数のAWSアカウントが同居するので、ベアメタルインスタンス上ではAWSアカウントごとにディレクトリを分けてるのでは? と考えました。

実際にFirecrackerを触ってみる

EC2モデルの裏側がどのようにMicroVMを構成しているか分かりませんが、Firecrackerに関してはOSSで公開されているので、実際に環境を構築しながら分析してみます。

環境構築

以下の記事を参考に執筆時点の最新バージョンFirecracker v0.17.0の環境を構築します。 Firecrackerをさわって大量のmicroVMを立ち上げてみた #reinvent

ベアメタルインスタンス上のディレクトリをMicroVMにマウントしてみる

FirecrackerのAPIを調べたところCreates or updates a drive.というAPIでMicroVM上に新しいドライブを作成できるようです。 ベアメタルインスタンス上で以下のコマンドを入力し、MicroVMにマウントするためのファイルを作成します。

$ sudo mkdir -p /opt/amazon/asc/worker/tasks/123456789012/lambdafunc/
$ sudo chown ubuntu:ubuntu -R /opt/amazon/
$ truncate -s  50M /opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a
$ mkfs.ext4 /opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a

準備ができたのでMicroVMの起動準備を行います

$ ./firecracker-v0.17.0 --api-sock /tmp/firecracker.sock
$ curl --unix-socket /tmp/firecracker.sock -i \
    -X PUT 'http://localhost/boot-source'   \
    -H 'Accept: application/json'           \
    -H 'Content-Type: application/json'     \
    -d '{
        "kernel_image_path": "./hello-vmlinux.bin",
        "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
    }'
HTTP/1.1 204 No Content
Date: Sun, 16 Jun 2019 14:29:23 GMT    
$ curl --unix-socket /tmp/firecracker.sock -i \
    -X PUT 'http://localhost/drives/rootfs' \
    -H 'Accept: application/json'           \
    -H 'Content-Type: application/json'     \
    -d '{
        "drive_id": "rootfs",
        "path_on_host": "./hello-rootfs.ext4",
        "is_root_device": true,
        "is_read_only": false
    }'
HTTP/1.1 204 No Content
Date: Sun, 16 Jun 2019 14:29:40 GMT    

先ほど作成したファイルをマウントするためにCreates or updates a drive.APIを実行します

$ curl --unix-socket /tmp/firecracker.sock -i \
     -X PUT 'http://localhost/drives/scratch' \
     -H 'accept: application/json' \
     -H 'Content-Type: application/json' \
     -d '{
            "drive_id": "scratch",
            "path_on_host": "/opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a",
            "is_root_device": false,
            "is_read_only": true
         }'
HTTP/1.1 204 No Content
Date: Sun, 16 Jun 2019 14:29:56 GMT         

MicroVMを起動します

$ curl --unix-socket /tmp/firecracker.sock -i \
    -X PUT 'http://localhost/actions'       \
    -H  'Accept: application/json'          \
    -H  'Content-Type: application/json'    \
    -d '{
        "action_type": "InstanceStart"
     }'
HTTP/1.1 204 No Content
Date: Sun, 16 Jun 2019 14:30:10 GMT     

MicroVMが起動したら、MicroVM内で先ほどのファイルをマウントしてfindmntを叩いてみます。

$ mkdir /var/task
$ mount -t ext4 /dev/vdb /var/task
$ findmnt
TARGET                   SOURCE      FSTYPE  OPTIONS
/                        /dev/vda    ext4    rw,relatime,data=ordered
├─/dev                   devtmpfs    devtmpf rw,nosuid,relatime,size=10240k,nr_i
│ ├─/dev/mqueue          mqueue      mqueue  rw,nosuid,nodev,noexec,relatime
│ ├─/dev/pts             devpts      devpts  rw,nosuid,noexec,relatime,gid=5,mod
│ └─/dev/shm             shm         tmpfs   rw,nosuid,nodev,noexec,relatime
├─/proc                  proc        proc    rw,nosuid,nodev,noexec,relatime
│ └─/proc/sys/fs/binfmt_misc
│                        binfmt_misc binfmt_ rw,nosuid,nodev,noexec,relatime
├─/run                   tmpfs       tmpfs   rw,nodev,relatime,size=11496k,mode=
├─/sys                   sysfs       sysfs   rw,nosuid,nodev,noexec,relatime
│ ├─/sys/kernel/security securityfs  securit rw,nosuid,nodev,noexec,relatime
│ ├─/sys/kernel/debug    debugfs     debugfs rw,nosuid,nodev,noexec,relatime
│ ├─/sys/fs/selinux      selinuxfs   selinux rw,relatime
│ └─/sys/fs/pstore       pstore      pstore  rw,nosuid,nodev,noexec,relatime
└─/var/task              /dev/vdb    ext4    ro,relatime,data=ordered

/var/taskのソースは/dev/vdbとなっています。そうです。。。 ベアメタルインスタンス上のファイルパスはMicroVM内では分からないのです。仮想的なデバイスとして渡してるので、当然ですね。。

ベアメタルインスタンス上はAWSアカウントでディレクトリを分けずに試してみる

ちょっと考え方を変えてやってみます。まずベアメタルインスタンス上にディスクイメージを作成し、最初に確認したディレクトリ構造を作成します。

$ truncate -s  50M /home/ubuntu/microvmdisk.img
$ sudo mkfs.ext4 /home/ubuntu/microvmdisk.img
$ sudo mount -t ext4 /home/ubuntu/microvmdisk.img /mnt
$ sudo mkdir -p /mnt/opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a
$ sudo sh -c "echo hoge > /mnt/opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a/hoge.txt"
$ sudo umount /mnt

今度はこのイメージをMicroVMにマウントしてみます。先ほどと同様の手順でドライブの指定だけ以下のコマンドに変更してMicroVMを起動します。

$ curl --unix-socket /tmp/firecracker.sock -i \
     -X PUT 'http://localhost/drives/scratch' \
     -H 'accept: application/json' \
     -H 'Content-Type: application/json' \
     -d '{
            "drive_id": "scratch",
            "path_on_host": "/home/ubuntu/microvmdisk.img",
            "is_root_device": false,
            "is_read_only": true
         }'
HTTP/1.1 204 No Content
Date: Sun, 16 Jun 2019 14:42:26 GMT         

MicroVMが起動できたら仮想ディスクをマウントします。

$ mount -t ext4 /dev/vdb /mnt
$ mount --bind /mnt/opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a /var/task
$ umount /mnt

この状態でfindmntを叩いてみます。

$ findmnt
TARGET                   SOURCE      FSTYPE  OPTIONS
/                        /dev/vda    ext4    rw,relatime,data=ordered
├─/dev                   devtmpfs    devtmpf rw,nosuid,relatime,size=10240k,nr_i
│ ├─/dev/mqueue          mqueue      mqueue  rw,nosuid,nodev,noexec,relatime
│ ├─/dev/pts             devpts      devpts  rw,nosuid,noexec,relatime,gid=5,mod
│ └─/dev/shm             shm         tmpfs   rw,nosuid,nodev,noexec,relatime
├─/proc                  proc        proc    rw,nosuid,nodev,noexec,relatime
│ └─/proc/sys/fs/binfmt_misc
│                        binfmt_misc binfmt_ rw,nosuid,nodev,noexec,relatime
├─/run                   tmpfs       tmpfs   rw,nodev,relatime,size=11496k,mode=
├─/sys                   sysfs       sysfs   rw,nosuid,nodev,noexec,relatime
│ ├─/sys/kernel/security securityfs  securit rw,nosuid,nodev,noexec,relatime
│ ├─/sys/kernel/debug    debugfs     debugfs rw,nosuid,nodev,noexec,relatime
│ ├─/sys/fs/selinux      selinuxfs   selinux rw,relatime
│ └─/sys/fs/pstore       pstore      pstore  rw,nosuid,nodev,noexec,relatime
└─/var/task              /dev/vdb[/opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a]
                                     ext4    ro,relatime,data=ordered

/var/taskに関してLambda実行環境と同様の出力が得られました。 一応ファイルの中身を確認します。

$ cat /var/task/hoge.txt
hoge

ベアメタルインスタンス上で書き込んだhogeという文字列が見えています。

一応MicroVM内のコンテナをシミュレートするためにMicroVM内で新しいネームスペースでシェルを起動して確認します。

unshare --mount /bin/ash
$ findmnt
TARGET                   SOURCE      FSTYPE  OPTIONS
/                        /dev/vda    ext4    rw,relatime,data=ordered
├─/dev                   devtmpfs    devtmpf rw,nosuid,relatime,size=10240k,nr_i
│ ├─/dev/mqueue          mqueue      mqueue  rw,nosuid,nodev,noexec,relatime
│ ├─/dev/pts             devpts      devpts  rw,nosuid,noexec,relatime,gid=5,mod
│ └─/dev/shm             shm         tmpfs   rw,nosuid,nodev,noexec,relatime
├─/proc                  proc        proc    rw,nosuid,nodev,noexec,relatime
│ └─/proc/sys/fs/binfmt_misc
│                        binfmt_misc binfmt_ rw,nosuid,nodev,noexec,relatime
├─/run                   tmpfs       tmpfs   rw,nodev,relatime,size=11496k,mode=
├─/sys                   sysfs       sysfs   rw,nosuid,nodev,noexec,relatime
│ ├─/sys/kernel/security securityfs  securit rw,nosuid,nodev,noexec,relatime
│ ├─/sys/kernel/debug    debugfs     debugfs rw,nosuid,nodev,noexec,relatime
│ ├─/sys/fs/selinux      selinuxfs   selinux rw,relatime
│ └─/sys/fs/pstore       pstore      pstore  rw,nosuid,nodev,noexec,relatime
└─/var/task              /dev/vdb[/opt/amazon/asc/worker/tasks/123456789012/lambdafunc/3cd7e9c3-499e-4c08-98e4-dd7e2f348d0a]
                                     ext4    ro,relatime,data=ordered

OKですね。

結局Lambda実行環境はどうなの??

一応findmntの出力結果をLambda実行環境と同等にすることができました。が、普通に考えたらMicroVMにマウントするイメージファイル内でAWSアカウントごとにディレクトリを分けずに、MicroVMにマウントするイメージファイルそのものをAWSアカウントで分けますよね?さらにもっと言えば別にFirecracker環境でも/var/taskのマウントソースが/dev/loop1になるような環境も簡単に構築できます。

ということで、Firecracker環境上でLambdaが実行されている場合、/var/taskのマウントソースが/dev/vdb[/opt/amazon/asc/worker/tasks/<AWSアカウントID>/<Lambda関数名>/<UUIDらしきランダムな文字列>になるという仮説を裏付けるような結果は何も出ませんでした。。。

まとめ

Lambda実行環境には/var/taskのマウントソースが

  • /dev/loop1になる場合
  • /dev/vdb[/opt/amazon/asc/worker/tasks/<AWSアカウントID>/<Lambda関数名>/<UUIDらしきランダムな文字列>になる場合

2つのパターンが存在することをご紹介しました。この2つのパターンの違いによってLambda実行環境がEC2モデルかFirecrackerモデルかを判別できると考えたのですが、残念ながら仮説の裏付けとなる検証結果が特に得られませんでした。とりあえず今日のところはLambda実行環境の/var/taskマウントソースが2パターンあるという紹介までです。

もし、今回紹介した内容からLambda実行環境の裏側についてピンと来た方がいれば、是非意見をお聞かせ下さい!!