先日は、コンテナ標準化の現状と Kubernetes の立ち位置について において、各種ドキュメントをベースにコンテナ標準についてまとめてみました。

このブログでは、実際に tool などに触れて手を動かすことで、コンテナ標準についてさらに理解を深めてみたいと思います。

なお、基本的にこのブログ内のコマンドは、Linux で実行するものとします(自分は MacOS で Vagrant で Ubuntu VM を立てて実験してます)。

OCI Image の中身を見てみる

skopeo と呼ばれる「container image に対して様々な操作を行えるツール」があります。このツールを利用することで、「docker image から OCI Image への変換」を行うことができます。このツールを利用して、実際に OCI Image の中身を見てみましょう。

まず、以下のコマンドを実行して ruby:2.7.2-slim という docker image を oci:ruby-oci:2.7.2 という名前の OCI Image に変換します。

vagrant@vagrant:~/oci-playground$ skopeo copy docker://ruby:2.7.2-slim oci:ruby-oci:2.7.2
Getting image source signatures
Copying blob 852e50cd189d done
Copying blob 6de4319615e2 done
Copying blob 150eb06190d1 done
Copying blob cf654ff9d9df done
Copying blob 0a529f6cf42e done
Copying config 3265430f5e done
Writing manifest to image destination
Storing signatures

上記コマンドを実行すると、ruby-oci という directory が出来ています。

vagrant@vagrant:~/oci-playground$ ls
ruby-oci

ruby-oci directory の中を見てみると、以下のように blobs という direcyory と index.json, oci-layout という file が出来ています。これは、OCI Image Format Specification で定められた Image Layout の内容に一致しています。

vagrant@vagrant:~/oci-playground/ruby-oci$ ls
blobs  index.json  oci-layout

oci-layout file には imageLayoutVersion だけが記載されています。現時点では 1.0.0 が記載されているだけなので、将来の拡張のための file と考えると良いでしょう。

vagrant@vagrant:~/oci-playground/ruby-oci$ cat oci-layout | jq .
{
  "imageLayoutVersion": "1.0.0"
}

index.json は OCI Image のエントリーポイントとも呼べる file で、ここには以下のように「manifest fileへの参照(= Image Manifest を指し示す Content Descriptor)」が記載されています。

vagrant@vagrant:~/oci-playground/ruby-oci$ cat index.json | jq .
{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:ad39959791540e6213fbe4675b9f3ee11e96456df3601b0936973ca7ae766bd7",
      "size": 976,
      "annotations": {
        "org.opencontainers.image.ref.name": "2.7.2"
      }
    }
  ]
}

ここで出てきた「Content Descriptor」というのが OCI Image Format において特徴的なもので、これは「mediaType, digest, size の 3 つ組 + optional な情報 (e.g. annotations)」となっています。 mediaType が参照先の情報の種類、digest が参照先の情報の path、size が参照先の情報のバイト数を表しています。

digest で示されているのは「blobs directory 以下の file path」になっていて、例えば上記の sha256:ad39959791540e6213fbe4675b9f3ee11e96456df3601b0936973ca7ae766bd7 という digest は blobs/sha256/ad39959791540e6213fbe4675b9f3ee11e96456df3601b0936973ca7ae766bd7 という path を表しています。実際に、file の中身を見てみると以下のような JSON になっています。

vagrant@vagrant:~/oci-playground/ruby-oci$ cat blobs/sha256/ad39959791540e6213fbe4675b9f3ee11e96456df3601b0936973ca7ae766bd7 | jq .
{
  "schemaVersion": 2,
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:3265430f5e5babe0664d7f7bcc77db2ef7d5feaa1625c06c10b1409ad2952133",
    "size": 4598
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:852e50cd189dfeb54d97680d9fa6bed21a6d7d18cfb56d6abfe2de9d7f173795",
      "size": 27105484
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:6de4319615e27e1aaaadc89b43db39ea0e118f47eeecfa4c8b910ca2fd810653",
      "size": 12539406
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:150eb06190d1ba56f7b998da25a140c21258bca436d33e2e77df679d77ab364a",
      "size": 198
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:cf654ff9d9df475122683b6bd070fa57a1e1969ced2a45f2c1f76a0678495ef2",
      "size": 22852677
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:0a529f6cf42e0fb49fe3fb4d12e232b26db923ab85a442563b0a7ae0a28c5971",
      "size": 143
    }
  ]
}

mediaTypeapplication/vnd.oci.image.manifest.v1+json だったことから、これは Image Manifest であると分かります。実際に、Image Manifest の仕様で定義された内容と一致しており、config (Container Image のメタデータ)や layers (Container Image の Layer、Docker Image における Layer Cache の単位となるもの)を情報として持つことも分かります。また、それらの情報への参照も、先ほどと同様の Content Descriptor 形式で表されていることが分かります。

config の内容は、以下のような Image Configuration となっています。環境変数や Command など Container 実行時に必要な各種メタデータや、Container Image 作成時の history の情報が記載されています。

vagrant@vagrant:~/oci-playground/ruby-oci$ cat blobs/sha256/3265430f5e5babe0664d7f7bcc77db2ef7d5feaa1625c06c10b1409ad2952133 | jq .
{
  "created": "2020-11-18T15:35:15.373100656Z",
  "architecture": "amd64",
  "os": "linux",
  "config": {
    "Env": [
      "PATH=/usr/local/bundle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "LANG=C.UTF-8",
      "RUBY_MAJOR=2.7",
      "RUBY_VERSION=2.7.2",
      "RUBY_DOWNLOAD_SHA256=1b95ab193cc8f5b5e59d2686cb3d5dcf1ddf2a86cb6950e0b4bdaae5040ec0d6",
      "GEM_HOME=/usr/local/bundle",
      "BUNDLE_SILENCE_ROOT_WARNING=1",
      "BUNDLE_APP_CONFIG=/usr/local/bundle"
    ],
    "Cmd": [
      "irb"
    ]
  },
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:f5600c6330da7bb112776ba067a32a9c20842d6ecc8ee3289f1a713b644092f8",
      "sha256:70ca8ae918406dce7acc5fe0f49e45b9275a266b83e275922e67358976c2929e",
      "sha256:e8ace463e6f7085a5439cf3b578a080fbefc8ad8424b59b9f35590adb1509763",
      "sha256:71e4ad27368acf7dbb5c90aa65d67cc462267836aa220cbafb9bb62acd9d48de",
      "sha256:1946ed62a3cb062940077a7a1dbfc93d55be6ef3d4f605883b42f71970381662"
    ]
  },
  "history": [
    {
      "created": "2020-11-17T20:21:17.570073346Z",
      "created_by": "/bin/sh -c #(nop) ADD file:d2abb0e4e7ac1773741f51f57d3a0b8ffc7907348842d773f8c341ba17f856d5 in / "
    },
    {
      "created": "2020-11-17T20:21:17.865210281Z",
      "created_by": "/bin/sh -c #(nop)  CMD [\"bash\"]",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:21:22.717162717Z",
      "created_by": "/bin/sh -c set -eux; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tbzip2 \t\tca-certificates \t\tlibffi-dev \t\tlibgmp-dev \t\tlibssl-dev \t\tlibyaml-dev \t\tprocps \t\tzlib1g-dev \t; \trm -rf /var/lib/apt/lists/*"
    },
    {
      "created": "2020-11-18T15:21:23.811888513Z",
      "created_by": "/bin/sh -c set -eux; \tmkdir -p /usr/local/etc; \t{ \t\techo 'install: --no-document'; \t\techo 'update: --no-document'; \t} >> /usr/local/etc/gemrc"
    },
    {
      "created": "2020-11-18T15:21:24.004412503Z",
      "created_by": "/bin/sh -c #(nop)  ENV LANG=C.UTF-8",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:30:41.383881949Z",
      "created_by": "/bin/sh -c #(nop)  ENV RUBY_MAJOR=2.7",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:30:41.629378277Z",
      "created_by": "/bin/sh -c #(nop)  ENV RUBY_VERSION=2.7.2",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:30:41.868222399Z",
      "created_by": "/bin/sh -c #(nop)  ENV RUBY_DOWNLOAD_SHA256=1b95ab193cc8f5b5e59d2686cb3d5dcf1ddf2a86cb6950e0b4bdaae5040ec0d6",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:35:11.770005784Z",
      "created_by": "/bin/sh -c set -eux; \t\tsavedAptMark=\"$(apt-mark showmanual)\"; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tautoconf \t\tbison \t\tdpkg-dev \t\tgcc \t\tlibbz2-dev \t\tlibgdbm-compat-dev \t\tlibgdbm-dev \t\tlibglib2.0-dev \t\tlibncurses-dev \t\tlibreadline-dev \t\tlibxml2-dev \t\tlibxslt-dev \t\tmake \t\truby \t\twget \t\txz-utils \t; \trm -rf /var/lib/apt/lists/*; \t\twget -O ruby.tar.xz \"https://cache.ruby-lang.org/pub/ruby/${RUBY_MAJOR%-rc}/ruby-$RUBY_VERSION.tar.xz\"; \techo \"$RUBY_DOWNLOAD_SHA256 *ruby.tar.xz\" | sha256sum --check --strict; \t\tmkdir -p /usr/src/ruby; \ttar -xJf ruby.tar.xz -C /usr/src/ruby --strip-components=1; \trm ruby.tar.xz; \t\tcd /usr/src/ruby; \t\t{ \t\techo '#define ENABLE_PATH_CHECK 0'; \t\techo; \t\tcat file.c; \t} > file.c.new; \tmv file.c.new file.c; \t\tautoconf; \tgnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\"; \t./configure \t\t--build=\"$gnuArch\" \t\t--disable-install-doc \t\t--enable-shared \t; \tmake -j \"$(nproc)\"; \tmake install; \t\tapt-mark auto '.*' > /dev/null; \tapt-mark manual $savedAptMark > /dev/null; \tfind /usr/local -type f -executable -not \\( -name '*tkinter*' \\) -exec ldd '{}' ';' \t\t| awk '/=>/ { print $(NF-1) }' \t\t| sort -u \t\t| xargs -r dpkg-query --search \t\t| cut -d: -f1 \t\t| sort -u \t\t| xargs -r apt-mark manual \t; \tapt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \t\tcd /; \trm -r /usr/src/ruby; \t! dpkg -l | grep -i ruby; \t[ \"$(command -v ruby)\" = '/usr/local/bin/ruby' ]; \truby --version; \tgem --version; \tbundle --version"
    },
    {
      "created": "2020-11-18T15:35:12.227711802Z",
      "created_by": "/bin/sh -c #(nop)  ENV GEM_HOME=/usr/local/bundle",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:35:12.563337139Z",
      "created_by": "/bin/sh -c #(nop)  ENV BUNDLE_SILENCE_ROOT_WARNING=1 BUNDLE_APP_CONFIG=/usr/local/bundle",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:35:12.907595531Z",
      "created_by": "/bin/sh -c #(nop)  ENV PATH=/usr/local/bundle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "empty_layer": true
    },
    {
      "created": "2020-11-18T15:35:14.977063521Z",
      "created_by": "/bin/sh -c mkdir -p \"$GEM_HOME\" && chmod 777 \"$GEM_HOME\""
    },
    {
      "created": "2020-11-18T15:35:15.373100656Z",
      "created_by": "/bin/sh -c #(nop)  CMD [\"irb\"]",
      "empty_layer": true
    }
  ]
}

layersFilesystem Layer を表しています。tar+gzip という mediaType の suffix は「gzip 圧縮された tar archive」を表しています。試しに、最も root にあった sha256:852e50cd189dfeb54d97680d9fa6bed21a6d7d18cfb56d6abfe2de9d7f173795 の中身を見てみます。

vagrant@vagrant:~/oci-playground/ruby-oci$ mkdir rootfs
vagrant@vagrant:~/oci-playground/ruby-oci$ tar xvzf blobs/sha256/852e50cd189dfeb54d97680d9fa6bed21a6d7d18cfb56d6abfe2de9d7f173795 -C rootfs/
.
.
.

上記コマンドで、rootfs directory 以下に圧縮されていた中身が展開されます(注: tar: bin/uncompress: Cannot hard link to ‘bin/gunzip’: Operation not permitted など一部の file について error は出ていて、そのせいで tar: Exiting with failure status due to previous errors という失敗 message も出てしまいましたが、それはここでは無視します)。

rootfs の中身を見てみると、以下のようにいくつかの directyory が並んでいます。

vagrant@vagrant:~/oci-playground/ruby-oci$ ls rootfs/
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

試しに変換前の ruby:2.7.2-slim docker image を利用して container を起動してみると、root directory の中身がそっくりであることが確認できます。

$ docker run -it ruby:2.7.2-slim bash
root@f6be3c7c619d:/# ls /
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

実は、これらの layer が「どう apply されるのか」は Image Layer Filesystem Changeset の Applying Changesets というセクション の中で以下のように明確に定義されています。ざっくり言えば「layer の上から順に tar archive を展開したようなもの」になります。 「file や directory の削除」は Whiteouts と呼ばれる特別な仕様で表現されますが、特別に注意を払う必要があるのはそれくらいのようです。

Applying Changesets

  • Layer Changesets of media type application/vnd.oci.image.layer.v1.tar are applied, rather than simply extracted as tar archives.
  • Applying a layer changeset requires special consideration for the whiteout files.
  • In the absence of any whiteout files in a layer changeset, the archive is extracted like a regular tar archive.

cf. https://github.com/opencontainers/image-spec/blob/v1.0.1/layer.md#applying-changesets

ということで、OCI image の中身に目を通してみました。「Conatainer を走らせるために必要な情報(= 実行時のメタデータ + Layer 化された filesystem の情報)」が格納されてることがわかったかと思います。

Container Registry との通信内容を見てみる

ここまでで、「Container Image の内容」については把握できました。次に、「Container Registry から Container Image をどのように pull しているのか」を調べてみましょう。

現在、各種 Container Registry は Docker 社が公開している Docker Registry HTTP API V2 と呼ばれる仕様に従う形で Container Image の Pull を出来るようにしています。実は、「Container Image の Pull」にあたる操作はただの HTTP request であるため、$ curl を利用して実行する事ができます。ここでは、実際に $ curl で request してみることで、Container Registry との通信内容を見てみる事にしましょう。

なお、自分が試した範囲では、どの Container Image も OCI Image Format ではなく Docker Image Manifest V 2, Schema 2 に従う形の response を返してきました。ただ、OCI Image Format と Docker Image V2.2 は一部の mediaType 名を除いてほぼ同一なので、先ほど眺めた内容は理解に役立つはずです。

さて、実際に curl で request を送ってみましょう。対象 Container Image は何でも良いのですが、ここでは https://github.com/GoogleContainerTools/base-images-docker に記載されてる Debian の Container Image である gcr.io/google-appengine/debian9 を対象にしてみます。

まず、以下のように Container Registry の Authentication に必要な Token を取得します。この時、「google-appengine/debian9 の pull」という形で scope を指定しておきます。

$ export TOKEN=$(curl "https://gcr.io/v2/token?service=gcr.io&scope=repository:google-appengine/debian9:pull" | jq -r '.token')
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   453    0   453    0     0   1088      0 --:--:-- --:--:-- --:--:--  1088

次に、https://gcr.io/v2/<name>/manifests/<reference> へ先ほど取得した Token 付きで GET request を送ります。こうすると、Docker Image V2.2 における manifest file が取得できます。

$ curl -H "Authorization: Bearer ${TOKEN}" https://gcr.io/v2/google-appengine/debian9/manifests/latest | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   528  100   528    0     0    469      0  0:00:01  0:00:01 --:--:--   469
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
  "config": {
    "mediaType": "application/vnd.docker.container.image.v1+json",
    "size": 463,
    "digest": "sha256:18c47921b263ac67af3d654e3b485c998d1e6bab56edc5a15b6b7a8fad3ac18a"
  },
  "layers": [
    {
      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
      "size": 47965538,
      "digest": "sha256:faa9d9046d25e5fd30ac4444c7b6c30a1a6fff7c461410547156aed2001668a1"
    }
  ]
}

まず、config の中身を見てみましょう。

digest を利用して参照を辿る際は https://gcr.io/v2/<name>/blobs/<digest> へ request すれば良いです。実際に request してみると、以下のような response が返ってきます。先ほど OCI Image の中身を見てみた時と同様に、Container 実行に必要なメタデータが格納されていることが分かります。

$ curl -L -H "Authorization: Bearer ${TOKEN}" https://gcr.io/v2/google-appengine/debian9/blobs/sha256:18c47921b263ac67af3d654e3b485c998d1e6bab56edc5a15b6b7a8fad3ac18a | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    13    0    13    0     0     23      0 --:--:-- --:--:-- --:--:--    23
100   463  100   463    0     0    750      0 --:--:-- --:--:-- --:--:--   750
{
  "architecture": "amd64",
  "author": "Bazel",
  "created": "1970-01-01T00:00:00Z",
  "history": [
    {
      "author": "Bazel",
      "created": "1970-01-01T00:00:00Z",
      "created_by": "bazel build ..."
    }
  ],
  "os": "linux",
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:0a3dcb016bd8a852985044291de00ad6a6b94dcb0eac01b34b56afed409b9999"
    ]
  },
  "config": {
    "Cmd": [
      "/bin/sh",
      "-c",
      "/bin/bash"
    ],
    "Env": [
      "DEBIAN_FRONTEND=noninteractive",
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "PORT=8080"
    ]
  }
}

なお、注意点として、どうやら GCR は https://gcr.io/v2/<name>/blobs/<digest> への request では Google Cloud Storage への redirect response を返すようです。-L オプションを付けない場合は以下のような結果になることには留意してください。

$ curl --include -H "Authorization: Bearer ${TOKEN}" https://gcr.io/v2/google-appengine/debian9/blobs/sha256:18c47921b263ac67af3d654e3b485c998d1e6bab56edc5a15b6b7a8fad3ac18a
HTTP/2 302
docker-distribution-api-version: registry/2.0
location: https://storage.googleapis.com/artifacts.google-appengine.appspot.com/containers/images/sha256:18c47921b263ac67af3d654e3b485c998d1e6bab56edc5a15b6b7a8fad3ac18a
content-type: application/json
date: Wed, 09 Dec 2020 13:38:51 GMT
server: Docker Registry
cache-control: private
x-xss-protection: 0
x-frame-options: SAMEORIGIN
alt-svc: h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
accept-ranges: none
vary: Accept-Encoding

{"errors":[]}%

上記では config の取得を行いましたが、Layer (mediaType: application/vnd.docker.image.rootfs.diff.tar.gzip のデータ)についても同様にhttps://gcr.io/v2/<name>/blobs/<digest> への request によって取得する事ができます。先ほどと同様に、tar コマンドで展開すると container 実行に利用される file を取得することが出来ます。

$ curl -L -H "Authorization: Bearer ${TOKEN}" https://gcr.io/v2/google-appengine/debian9/blobs/sha256:faa9d9046d25e5fd30ac4444c7b6c30a1a6fff7c461410547156aed2001668a1 --output /tmp/faa9d9046d25e5fd30ac4444c7b6c30a1a6fff7c461410547156aed2001668a1
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    13    0    13    0     0     28      0 --:--:-- --:--:-- --:--:--    28
100 45.7M  100 45.7M    0     0  13.6M      0  0:00:03  0:00:03 --:--:-- 20.1M

$ ls -la /tmp/faa9d9046d25e5fd30ac4444c7b6c30a1a6fff7c461410547156aed2001668a1
-rw-r--r--  1 minami  wheel  47965538 Dec  9 23:48 /tmp/faa9d9046d25e5fd30ac4444c7b6c30a1a6fff7c461410547156aed2001668a1

$ mkdir /tmp/rootfs
$ tar xvzf /tmp/faa9d9046d25e5fd30ac4444c7b6c30a1a6fff7c461410547156aed2001668a1 -C /tmp/rootfs/
$ ls /tmp/rootfs
bin   boot  dev   etc   home  lib   lib64 media mnt   opt   proc  root  run   sbin  srv   sys   tmp   usr   var

という事で、Container Registry との通信について、特に「Container Image の Pull」に絞って通信内容を見てみました。Docker Image V2.2 をベースにした通信である事、特に config や layer などがそれぞれの単位で通信できることなどが分かったかと思います。より詳しい内容が気になる場合は、Docker Registry HTTP API V2 を参照してみてください。

なお、「Container Image 全てをまとめた file を一括でダウンロードしないのは何故なのか?」という疑問についてですが、これは自分の理解では「Layer Cache を効かせた形での Image Pull を実現するため」だと捉えています。Layer のデータは巨大であるため最低限の通信で済ませたいというのが大前提にあり、そのために「コンテンツの中身を反映した digest 値を用いて、Layer ごとに通信する」という振る舞いになっているのだと思われます。

まとめ

OCI image を実際に作成して眺めて見ることで、Container Image について理解を深めました。また、curl で Container Registry との通信を行うことで、Container Registry との通信内容についても理解を深める事が出来ました。

ドキュメントを読むだけだとどうしても理解が曖昧になってしまいがちですが、実際に手を動かす事で具体的な動作をイメージ出来るようになります。このブログ自体は自分の理解のために試したことをまとめたものですが、誰か他の人にとっても理解を助けるものになっていれば幸いです。

なお、今回は Container Image + Container Registry 編でしたが、後日 Container Runtime についても「手を動かして調べた内容」についてまとめたいと思っています。特に、「Container Image から Container Runtime が利用する Filesystem Bundle への Conversion」や、「runc などの low-level Container Runtime の動作」、「containerd や CRI-O などの high-level Container Runtime の動作」について試したことをまとめる予定です。

補足: Vagrant での実験環境

以下のような Vagrantfile を使ってます。ubuntu-20.04 を使ってます。

# -*- mode: ruby -*-
# vi: set ft=ruby :

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "bento/ubuntu-20.04"
end

参考文献

以下のブログは、ツールやコマンド、内容において大幅に参考にさせて頂きました。ありがとうございました。