zaki work log

作業ログやら生活ログやらなんやら

[NetBox] 最新のfeatureブランチでDockerビルドしてコンテナとしてデプロイする

NetBoxの機能でまだリリースはされていないけどfeatureブランチには実装済みの新しい機能を試してみたい場合、feature版のコンテナイメージは用意されてないためOS上に素で構築する必要があるが、それよりはfeature版コンテナイメージを手元でビルドした方が楽なのでは?と思って試してみた。

なお、試してみるとコンテナ版のビルドスクリプトはfeature版には対応していないため、実装の差異がある場合は手直しが必要*1

以下は時期バージョンの2.11のリリースノート。

github.com

環境

[zaki@cloud-dev netbox-docker (release)]$ docker-compose --version
docker-compose version 1.28.6, build 5db8d86f
[zaki@cloud-dev netbox-docker (release)]$ docker --version
Docker version 20.10.5, build 55c4c88

準備

Docker Compose版のリポジトリcloneする。

github.com

$ git clone https://github.com/netbox-community/netbox-docker.git
$ cd netbox-docker/

NetBoxコンテナイメージをビルドするには、付属のbuild.shを使うと簡単にビルドできる。

イメージのbuild

ビルドスクリプトの使い方は--helpを指定して確認できる。

$ ./build.sh --help
▶️ ./build.sh --help
Usage: ./build.sh <branch> [--push|--push-only]

ブランチ名を引数に実行すれば、そのブランチのソースを使ってコンテナイメージがビルドされる。
デフォルトはビルドのみで、(試してないけど)--pushも付与すれば、おそらくDocker Hubへpushされる(未確認。デフォルトではnetboxcommunityリポジトリになるので個人環境では変更が必要かも)。

というわけで、NetBoxのfeatureブランチをビルドするには以下の通り。

$ ./build.sh feature

実行すると .netboxディレクトリにNetBox本体のリポジトリcloneされビルド処理が始まる。
ビルド中はステージごとの処理時間がログの右側に表示されるのでtimeは付けなくていいかも。手元の環境(i7のESXiの上の4vCPUs RAM8GBのVM)では約10分必要だった。
あとは、実行ユーザー権限でDockerが使用可能であること。

ビルドログはこんな感じ。

▶️ ./build.sh feature
🌐 Checking out 'feature' of NetBox from the url 'https://github.com/netbox-community/netbox.git' into '.netbox'
✅ Checked out NetBox
🏭 Building the following targets: main ldap
🏗 Building the target 'main'
🐳 Building the Docker image 'docker.io/netboxcommunity/netbox:feature'.
    Build reason set to: interactive
[+] Building 641.3s (22/22) FINISHED                                                                                                                          
 => [internal] load build definition from Dockerfile                                                                                                     0.0s
 => => transferring dockerfile: 4.03kB                                                                                                                   0.0s
 => [internal] load .dockerignore                                                                                                                        0.0s
 => => transferring context: 159B                                                                                                                        0.0s
 => [internal] load metadata for docker.io/library/alpine:3.13                                                                                           2.6s
 => [internal] load build context                                                                                                                        0.3s
 => => transferring context: 18.63MB                                                                                                                     0.3s
 => [builder 1/4] FROM docker.io/library/alpine:3.13@sha256:ec14c7992a97fc11425907e908340c6c3d6ff602f5f13d899e6b7027c9b4133a                             0.8s
 => => resolve docker.io/library/alpine:3.13@sha256:ec14c7992a97fc11425907e908340c6c3d6ff602f5f13d899e6b7027c9b4133a                                     0.0s
 => => sha256:49f356fa4513676c5e22e3a8404aad6c7262cc7aaed15341458265320786c58c 1.47kB / 1.47kB                                                           0.0s
 => => sha256:ca3cd42a7c9525f6ce3d64c1a70982613a8235f0cc057ec9244052921853ef15 2.81MB / 2.81MB                                                           0.7s
 => => sha256:ec14c7992a97fc11425907e908340c6c3d6ff602f5f13d899e6b7027c9b4133a 1.64kB / 1.64kB                                                           0.0s 
 => => sha256:e103c1b4bf019dc290bcc7aca538dc2bf7a9d0fc836e186f5fa34945c5168310 528B / 528B                                                               0.0s 
 => => extracting sha256:ca3cd42a7c9525f6ce3d64c1a70982613a8235f0cc057ec9244052921853ef15                                                                0.1s 
 => [builder 2/4] RUN apk add --no-cache       bash       build-base       cargo       ca-certificates       cyrus-sasl-dev       graphviz       jpeg-  46.7s
 => [main  2/14] RUN apk add --no-cache       bash       ca-certificates       curl       graphviz       libevent       libffi       libjpeg-turbo      11.0s
 => [main  3/14] WORKDIR /opt                                                                                                                            0.6s
 => [builder 3/4] COPY .netbox/requirements.txt requirements-container.txt /                                                                             0.0s
 => [builder 4/4] RUN /opt/netbox/venv/bin/pip install       -r /requirements.txt       -r /requirements-container.txt                                 569.6s
 => [main  4/14] COPY --from=builder /opt/netbox/venv /opt/netbox/venv                                                                                   4.4s
 => [main  5/14] COPY .netbox /opt/netbox                                                                                                                0.4s
 => [main  6/14] COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py                                                          0.0s
 => [main  7/14] COPY docker/docker-entrypoint.sh /opt/netbox/docker-entrypoint.sh                                                                       0.0s
 => [main  8/14] COPY docker/launch-netbox.sh /opt/netbox/launch-netbox.sh                                                                               0.0s
 => [main  9/14] COPY startup_scripts/ /opt/netbox/startup_scripts/                                                                                      0.0s
 => [main 10/14] COPY initializers/ /opt/netbox/initializers/                                                                                            0.0s
 => [main 11/14] COPY configuration/ /etc/netbox/config/                                                                                                 0.0s
 => [main 12/14] COPY docker/nginx-unit.json /etc/unit/                                                                                                  0.0s
 => [main 13/14] WORKDIR /opt/netbox/netbox                                                                                                              0.1s
 => [main 14/14] RUN mkdir -p static /opt/unit/state/ /opt/unit/tmp/       && chmod -R g+w media /opt/unit/       && SECRET_KEY="dummy" /opt/netbox/ven  2.8s
 => exporting to image                                                                                                                                  11.5s
 => => exporting layers                                                                                                                                 11.5s
 => => writing image sha256:71d6db96e4e172dfae2d1b0c68aad0283212936e17f0bc22aef00004f7f2e756                                                             0.0s
 => => naming to docker.io/netboxcommunity/netbox:feature                                                                                                0.0s
✅ Finished building the Docker images 'docker.io/netboxcommunity/netbox:feature'
🔎 Inspecting labels on 'docker.io/netboxcommunity/netbox:feature'
{"BUILD_REASON":"interactive","NETBOX_GIT_BRANCH":"HEAD","NETBOX_GIT_REF":"4883bc3dd445a8353ae1f204d0a1fe3dcc83cb73","NETBOX_GIT_URL":"https://github.com/netbox-community/netbox.git","ORIGINAL_TAG":"docker.io/netboxcommunity/netbox:feature","org.label-schema.build-date":"2021-04-03T02:05+00:00","org.label-schema.description":"A container based distribution of NetBox, the free and open IPAM and DCIM solution.","org.label-schema.name":"NetBox Docker","org.label-schema.schema-version":"1.0","org.label-schema.url":"https://github.com/netbox-community/netbox-docker","org.label-schema.usage":"https://github.com/netbox-community/netbox-docker/wiki","org.label-schema.vcs-ref":"cb5ffa0354016d7187395c591d17b2f0f5f0699a","org.label-schema.vcs-url":"https://github.com/netbox-community/netbox-docker.git","org.label-schema.vendor":"The netbox-docker contributors.","org.label-schema.version":"1.1.0","org.opencontainers.image.authors":"The netbox-docker contributors.","org.opencontainers.image.created":"2021-04-03T02:05+00:00","org.opencontainers.image.description":"A container based distribution of NetBox, the free and open IPAM and DCIM solution.","org.opencontainers.image.documentation":"https://github.com/netbox-community/netbox-docker/wiki","org.opencontainers.image.licenses":"Apache-2.0","org.opencontainers.image.revision":"cb5ffa0354016d7187395c591d17b2f0f5f0699a","org.opencontainers.image.source":"https://github.com/netbox-community/netbox-docker.git","org.opencontainers.image.title":"NetBox Docker","org.opencontainers.image.url":"https://github.com/netbox-community/netbox-docker","org.opencontainers.image.vendor":"The netbox-docker contributors.","org.opencontainers.image.version":"1.1.0"}
🏗 Building the target 'ldap'
🐳 Building the Docker image 'docker.io/netboxcommunity/netbox:feature-ldap'.
    Build reason set to: interactive
[+] Building 5.1s (24/24) FINISHED                                                                                                                            
 => [internal] load build definition from Dockerfile                                                                                                     0.1s
 => => transferring dockerfile: 38B                                                                                                                      0.0s
 => [internal] load .dockerignore                                                                                                                        0.0s
 => => transferring context: 34B                                                                                                                         0.0s
 => [internal] load metadata for docker.io/library/alpine:3.13                                                                                           1.7s
 => [internal] load build context                                                                                                                        0.1s
 => => transferring context: 176.93kB                                                                                                                    0.1s
 => [builder 1/4] FROM docker.io/library/alpine:3.13@sha256:ec14c7992a97fc11425907e908340c6c3d6ff602f5f13d899e6b7027c9b4133a                             0.0s
 => CACHED [main  2/14] RUN apk add --no-cache       bash       ca-certificates       curl       graphviz       libevent       libffi       libjpeg-tur  0.0s
 => CACHED [main  3/14] WORKDIR /opt                                                                                                                     0.0s
 => CACHED [builder 2/4] RUN apk add --no-cache       bash       build-base       cargo       ca-certificates       cyrus-sasl-dev       graphviz        0.0s
 => CACHED [builder 3/4] COPY .netbox/requirements.txt requirements-container.txt /                                                                      0.0s
 => CACHED [builder 4/4] RUN /opt/netbox/venv/bin/pip install       -r /requirements.txt       -r /requirements-container.txt                            0.0s
 => CACHED [main  4/14] COPY --from=builder /opt/netbox/venv /opt/netbox/venv                                                                            0.0s
 => CACHED [main  5/14] COPY .netbox /opt/netbox                                                                                                         0.0s
 => CACHED [main  6/14] COPY docker/configuration.docker.py /opt/netbox/netbox/netbox/configuration.py                                                   0.0s
 => CACHED [main  7/14] COPY docker/docker-entrypoint.sh /opt/netbox/docker-entrypoint.sh                                                                0.0s
 => CACHED [main  8/14] COPY docker/launch-netbox.sh /opt/netbox/launch-netbox.sh                                                                        0.0s
 => CACHED [main  9/14] COPY startup_scripts/ /opt/netbox/startup_scripts/                                                                               0.0s
 => CACHED [main 10/14] COPY initializers/ /opt/netbox/initializers/                                                                                     0.0s
 => CACHED [main 11/14] COPY configuration/ /etc/netbox/config/                                                                                          0.0s
 => CACHED [main 12/14] COPY docker/nginx-unit.json /etc/unit/                                                                                           0.0s
 => CACHED [main 13/14] WORKDIR /opt/netbox/netbox                                                                                                       0.0s
 => CACHED [main 14/14] RUN mkdir -p static /opt/unit/state/ /opt/unit/tmp/       && chmod -R g+w media /opt/unit/       && SECRET_KEY="dummy" /opt/net  0.0s
 => [ldap 1/2] RUN apk add --no-cache       libsasl       libldap       util-linux                                                                       1.6s
 => [ldap 2/2] COPY docker/ldap_config.docker.py /opt/netbox/netbox/netbox/ldap_config.py                                                                0.0s
 => exporting to image                                                                                                                                   1.7s
 => => exporting layers                                                                                                                                  1.7s
 => => writing image sha256:b6c008b1e41d1f122c051ebba982ed7de5c3f47f14ab9ddef154f1b693cc3e7e                                                             0.0s
 => => naming to docker.io/netboxcommunity/netbox:feature-ldap                                                                                           0.0s
✅ Finished building the Docker images 'docker.io/netboxcommunity/netbox:feature-ldap'
🔎 Inspecting labels on 'docker.io/netboxcommunity/netbox:feature-ldap'
{"BUILD_REASON":"interactive","NETBOX_GIT_BRANCH":"HEAD","NETBOX_GIT_REF":"4883bc3dd445a8353ae1f204d0a1fe3dcc83cb73","NETBOX_GIT_URL":"https://github.com/netbox-community/netbox.git","ORIGINAL_TAG":"docker.io/netboxcommunity/netbox:feature-ldap","org.label-schema.build-date":"2021-04-03T02:05+00:00","org.label-schema.description":"A container based distribution of NetBox, the free and open IPAM and DCIM solution.","org.label-schema.name":"NetBox Docker","org.label-schema.schema-version":"1.0","org.label-schema.url":"https://github.com/netbox-community/netbox-docker","org.label-schema.usage":"https://github.com/netbox-community/netbox-docker/wiki","org.label-schema.vcs-ref":"cb5ffa0354016d7187395c591d17b2f0f5f0699a","org.label-schema.vcs-url":"https://github.com/netbox-community/netbox-docker.git","org.label-schema.vendor":"The netbox-docker contributors.","org.label-schema.version":"1.1.0","org.opencontainers.image.authors":"The netbox-docker contributors.","org.opencontainers.image.created":"2021-04-03T02:05+00:00","org.opencontainers.image.description":"A container based distribution of NetBox, the free and open IPAM and DCIM solution.","org.opencontainers.image.documentation":"https://github.com/netbox-community/netbox-docker/wiki","org.opencontainers.image.licenses":"Apache-2.0","org.opencontainers.image.revision":"cb5ffa0354016d7187395c591d17b2f0f5f0699a","org.opencontainers.image.source":"https://github.com/netbox-community/netbox-docker.git","org.opencontainers.image.title":"NetBox Docker","org.opencontainers.image.url":"https://github.com/netbox-community/netbox-docker","org.opencontainers.image.vendor":"The netbox-docker contributors.","org.opencontainers.image.version":"1.1.0"}

この2イメージがビルドされた。

[zaki@cloud-dev netbox-docker (release)]$ docker images
REPOSITORY               TAG            IMAGE ID       CREATED         SIZE
netboxcommunity/netbox   feature-ldap   1e775790d163   2 minutes ago   404MB
netboxcommunity/netbox   feature        ef2733470d41   2 minutes ago   399MB

デプロイ

デプロイは通常通り、必要に応じて docker-compose.override.yml を作成する。
このとき、指定コンテナイメージのtagにfeatureを指定し、今ビルドしたイメージを使うように指定する。
(※これはdocker-compose.override.ymlで指定しなくても、環境変数VERSIONfeatureを指定しても良いようにdocker-compose.ymlが作成されている。)

version: '3.4'
services:
  netbox:
    ports:
      - 9900:8080
    image: netboxcommunity/netbox:feature

デプロイ開始。

[zaki@cloud-dev netbox-docker (release)]$ docker-compose up -d
Creating network "netbox-docker_default" with the default driver
Creating volume "netbox-docker_netbox-media-files" with local driver
Creating volume "netbox-docker_netbox-postgres-data" with local driver
Creating volume "netbox-docker_netbox-redis-data" with local driver
Pulling postgres (postgres:12-alpine)...
12-alpine: Pulling from library/postgres
ca3cd42a7c95: Already exists
a0d003399a5b: Pull complete
0a08de1ad3ba: Pull complete
36fedf58ee26: Pull complete
733693f76814: Pull complete
14068c3cc9f8: Pull complete
0e1916273d9a: Pull complete
163f3880181d: Pull complete
Digest: sha256:56ccea5941111d7cfd22894c6ab7eb7dd9c0fff1ffa489194e87d4f5372160ec
Status: Downloaded newer image for postgres:12-alpine
Pulling redis (redis:6-alpine)...
6-alpine: Pulling from library/redis
ca3cd42a7c95: Already exists
8ea57f684bcd: Pull complete
c99865ad8d20: Pull complete
e2b874e1c121: Pull complete
4419ed1bbecc: Pull complete
662c8cef3675: Pull complete
Digest: sha256:142f857eb675a6a817dff1ae4d2138fc0228db596db2849ad1c38023a9dea7a4
Status: Downloaded newer image for redis:6-alpine
Creating netbox-docker_redis-cache_1 ... done
Creating netbox-docker_redis_1       ... done
Creating netbox-docker_postgres_1    ... done
Creating netbox-docker_netbox-worker_1 ... done
Creating netbox-docker_netbox_1        ... done

実行状態。

[zaki@cloud-dev netbox-docker (release)]$ docker-compose ps
            Name                           Command               State     Ports  
----------------------------------------------------------------------------------
netbox-docker_netbox-worker_1   /opt/netbox/venv/bin/pytho ...   Up               
netbox-docker_netbox_1          /opt/netbox/docker-entrypo ...   Exit 1           
netbox-docker_postgres_1        docker-entrypoint.sh postgres    Up       5432/tcp
netbox-docker_redis-cache_1     docker-entrypoint.sh sh -c ...   Up       6379/tcp
netbox-docker_redis_1           docker-entrypoint.sh sh -c ...   Up       6379/tcp

おや、、、

実行エラー

ログを確認してみると以下の通り。

[zaki@cloud-dev netbox-docker (release)]$ docker-compose logs 

:
:

netbox_1         | 🧬 loaded config '/etc/netbox/config/configuration.py'
netbox_1         | 🧬 loaded config '/etc/netbox/config/extra.py'
netbox_1         | 💡 Superuser Username: admin, E-Mail: admin@example.com
netbox_1         | 🧬 loaded config '/etc/netbox/config/configuration.py'
netbox_1         | 🧬 loaded config '/etc/netbox/config/extra.py'
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/000_users.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/010_groups.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/020_custom_fields.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/020_tags.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/030_regions.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/040_sites.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/050_manufacturers.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/060_device_types.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/070_rack_roles.py
netbox_1         | ▶️  Running the startup script /opt/netbox/startup_scripts/075_rack_groups.py
netbox_1         | Traceback (most recent call last):
netbox_1         |   File "./manage.py", line 10, in <module>
netbox_1         |     execute_from_command_line(sys.argv)
netbox_1         |   File "/opt/netbox/venv/lib/python3.8/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
netbox_1         |     utility.execute()
netbox_1         |   File "/opt/netbox/venv/lib/python3.8/site-packages/django/core/management/__init__.py", line 413, in execute
netbox_1         |     self.fetch_command(subcommand).run_from_argv(self.argv)
netbox_1         |   File "/opt/netbox/venv/lib/python3.8/site-packages/django/core/management/base.py", line 354, in run_from_argv
netbox_1         |     self.execute(*args, **cmd_options)
netbox_1         |   File "/opt/netbox/venv/lib/python3.8/site-packages/django/core/management/base.py", line 398, in execute
netbox_1         |     output = self.handle(*args, **options)
netbox_1         |   File "/opt/netbox/venv/lib/python3.8/site-packages/django/core/management/commands/shell.py", line 93, in handle
netbox_1         |     exec(sys.stdin.read(), globals())
netbox_1         |   File "<string>", line 1, in <module>
netbox_1         |   File "/usr/lib/python3.8/runpy.py", line 282, in run_path
netbox_1         |     return _run_code(code, mod_globals, init_globals,
netbox_1         |   File "/usr/lib/python3.8/runpy.py", line 87, in _run_code
netbox_1         |     exec(code, run_globals)
netbox_1         |   File "../startup_scripts/__main__.py", line 27, in <module>
netbox_1         |     runpy.run_path(f.path)
netbox_1         |   File "/usr/lib/python3.8/runpy.py", line 265, in run_path
netbox_1         |     return _run_module_code(code, init_globals, run_name,
netbox_1         |   File "/usr/lib/python3.8/runpy.py", line 97, in _run_module_code
netbox_1         |     _run_code(code, mod_globals, init_globals,
netbox_1         |   File "/usr/lib/python3.8/runpy.py", line 87, in _run_code
netbox_1         |     exec(code, run_globals)
netbox_1         |   File "/opt/netbox/startup_scripts/075_rack_groups.py", line 3, in <module>
netbox_1         |     from dcim.models import RackGroup, Site
netbox_1         | ImportError: cannot import name 'RackGroup' from 'dcim.models' (/opt/netbox/netbox/dcim/models/__init__.py)
netbox-docker_netbox_1 exited with code 1

エラーになっている。

先に答えを書くと、NetBox本体はfeatureブランチを今回対象としているけど、イメージビルドに使っているNetBox Dockerはfeatureブランチは存在しないのでデフォルトのreleaseブランチを使っているので、実装の差異があるとそこでエラーが発生する。

NetBox Dockerのコードを本体のfeatureブランチの内容に合わせる

以下は2021.04.03 11時(JST)時点での修正ポイント。

最初はfeatureブランチの内容が実装されてないから実装されるまで待とうかなーと思ったけど、あきらめきれずにソースを追ってみる。
(最近こんなのばっかだな)

最初に見るエラーの内容は以下。

Running the startup script /opt/netbox/startup_scripts/075_rack_groups.py
:
:
ImportError: cannot import name 'RackGroup' from 'dcim.models' (/opt/netbox/netbox/dcim/models/__init__.py)

エラー発生個所のコードを確認すると、featureブランチはLocationになってる

masterやdevelopだとRackGroupになっている

そして、実行中スクリプト075_rack_groups.pyのあるnetbox-dockerのリリースブランチでは、RackGroupを参照してる、と。

ということで、startup_scripts以下で RackGroup を参照しているところを Location に変更すれば一歩進める(少なくともimportはうまくいく)と予測が立つ。
ただ、パッケージ名変更だけで動くのかというとそれはあやしいのでもうblameをチェックすると、以下のissueで「名前の変更」が行われたことが確認できる。

github.com

変更点は以下。

github.com

grepと脳内インタプリタでざっと見た感じでは、netbox-docker側のスクリプトもリネームでとりあえず大丈夫そうなので、startup_scripts内で参照しているRackGroupLocationに変更してやる。
以下の通り。

diff --git a/startup_scripts/075_rack_groups.py b/startup_scripts/075_rack_groups.py
index 3974b56..d2bf249 100644
--- a/startup_scripts/075_rack_groups.py
+++ b/startup_scripts/075_rack_groups.py
@@ -1,6 +1,6 @@
 import sys
 
-from dcim.models import RackGroup, Site
+from dcim.models import Location, Site
 from startup_script_utils import load_yaml
 
 rack_groups = load_yaml("/opt/netbox/initializers/rack_groups.yml")
@@ -17,7 +17,7 @@ for params in rack_groups:
         query = {field: params.pop(assoc)}
         params[assoc] = model.objects.get(**query)
 
-    rack_group, created = RackGroup.objects.get_or_create(**params)
+    rack_group, created = Location.objects.get_or_create(**params)
 
     if created:
         print("🎨 Created rack group", rack_group.name)
diff --git a/startup_scripts/080_racks.py b/startup_scripts/080_racks.py
index 087b3f9..e27b268 100644
--- a/startup_scripts/080_racks.py
+++ b/startup_scripts/080_racks.py
@@ -1,6 +1,6 @@
 import sys
 
-from dcim.models import Rack, RackGroup, RackRole, Site
+from dcim.models import Rack, Location, RackRole, Site
 from startup_script_utils import load_yaml, pop_custom_fields, set_custom_fields_values
 from tenancy.models import Tenant
 
@@ -14,7 +14,7 @@ required_assocs = {"site": (Site, "name")}
 optional_assocs = {
     "role": (RackRole, "name"),
     "tenant": (Tenant, "name"),
-    "group": (RackGroup, "name"),
+    "group": (Location, "name"),
 }
 
 for params in racks:
diff --git a/startup_scripts/330_power_panels.py b/startup_scripts/330_power_panels.py
index bfde18f..db11a4d 100644
--- a/startup_scripts/330_power_panels.py
+++ b/startup_scripts/330_power_panels.py
@@ -1,6 +1,6 @@
 import sys
 
-from dcim.models import PowerPanel, RackGroup, Site
+from dcim.models import PowerPanel, Location, Site
 from startup_script_utils import load_yaml, pop_custom_fields, set_custom_fields_values
 
 power_panels = load_yaml("/opt/netbox/initializers/power_panels.yml")
@@ -10,7 +10,7 @@ if power_panels is None:
 
 required_assocs = {"site": (Site, "name")}
 
-optional_assocs = {"rack_group": (RackGroup, "name")}
+optional_assocs = {"rack_group": (Location, "name")}
 
 for params in power_panels:
     custom_field_data = pop_custom_fields(params)

再デプロイ

変更が済んだら一度down --volumeでボリューム含めて全て削除(これはトラブル防止のために環境をクリアにするという趣旨)し、再度デプロイする。

[zaki@cloud-dev netbox-docker (release)]$ docker-compose down --volume 
Stopping netbox-docker_netbox-worker_1 ... done
Stopping netbox-docker_postgres_1      ... done
Stopping netbox-docker_redis-cache_1   ... done
Stopping netbox-docker_redis_1         ... done
Removing netbox-docker_netbox_1        ... done
Removing netbox-docker_netbox-worker_1 ... done
Removing netbox-docker_postgres_1      ... done
Removing netbox-docker_redis-cache_1   ... done
Removing netbox-docker_redis_1         ... done
Removing network netbox-docker_default
Removing volume netbox-docker_netbox-media-files
Removing volume netbox-docker_netbox-postgres-data
Removing volume netbox-docker_netbox-redis-data
[zaki@cloud-dev netbox-docker (release)]$ docker-compose up -d
Creating network "netbox-docker_default" with the default driver
Creating volume "netbox-docker_netbox-media-files" with local driver
Creating volume "netbox-docker_netbox-postgres-data" with local driver
Creating volume "netbox-docker_netbox-redis-data" with local driver
Creating netbox-docker_redis-cache_1   ... done
Creating netbox-docker_postgres_1      ... done
Creating netbox-docker_redis_1       ... done
Creating netbox-docker_netbox-worker_1 ... done
Creating netbox-docker_netbox_1        ... done
[zaki@cloud-dev netbox-docker (release)]$ docker-compose ps
            Name                           Command               State                       Ports                     
-----------------------------------------------------------------------------------------------------------------------
netbox-docker_netbox-worker_1   /opt/netbox/venv/bin/pytho ...   Up                                                    
netbox-docker_netbox_1          /opt/netbox/docker-entrypo ...   Up      0.0.0.0:9900->8080/tcp,0.0.0.0:49164->8080/tcp
netbox-docker_postgres_1        docker-entrypoint.sh postgres    Up      5432/tcp                                      
netbox-docker_redis-cache_1     docker-entrypoint.sh sh -c ...   Up      6379/tcp                                      
netbox-docker_redis_1           docker-entrypoint.sh sh -c ...   Up      6379/tcp           

ログを見た感じでも正常に起動した模様。

webアクセス

この通り、featureブランチで開発されている2.11の表示になっている。(リリース版は現在2.10)

f:id:zaki-hmkc:20210403114740p:plain

2.11のリリースノートはこちら

github.com

#5451 - Add support for multiple-selection custom fields

たとえばカスタムフィールドの「multi-select」とかがサポートされるようになっている。 画面右上Adminメニューの「Custom fields」で、以下のように項目が追加されているのを確認できる。

v2.11 f:id:zaki-hmkc:20210403115250p:plain

以下はv2.10の場合 f:id:zaki-hmkc:20210403115401p:plain

まとめ

featureブランチの内容でも手元でイメージビルドすればDocker Composeを使ってNetBoxをデプロイできることを確認できた。
ただし実装の内容(次期バージョンと現バージョンの実装の差異)によっては修正が必要。
なので、「構築は楽だけどこれでまぁ大丈夫かな?」くらいだろうか。。

今回は露見してないけど「全くの新しいパッケージimportが必要」なパターンであればデプロイのタイミングではエラーにならずに画面から参照しようとするとランタイムエラーになる、というパターンもあるかもしれないので注意。

用途としては「壊して作り直す」が容易なコンテナのメリットを活かせるので、新機能を確認するための環境の用意の手段の一つとして、やりかたをチェックしておくと良いと思う。

*1:あくまで2021.04.03時点の2.11バージョンの場合

NetBoxのSecretを使って暗号化された情報を登録・参照する

NetBoxは、デバイスVMの情報に暗号化されたSecret情報を持たせることができるので、例えばログイン用のアカウント情報などもNetBoxに登録しておくことができます。
このSecretはbase64エンコードされるだけのもの、、ではなく、256ビットのAES共通鍵で暗号化されて保存されます。(ドキュメントより)

The plaintext value of a secret is encrypted to a ciphertext immediately prior to storage within the database using a 256-bit AES master key

netbox.readthedocs.io

鍵作成

鍵形式は以下。

Supported Key Format

Public key formats supported

  • PKCS#1 RSAPublicKey* (PEM header: BEGIN RSA PUBLIC KEY)
  • X.509 SubjectPublicKeyInfo** (PEM header: BEGIN PUBLIC KEY)
  • OpenSSH line format is not supported.

Private key formats supported (unencrypted) - PKCS#1 RSAPrivateKey* (PEM header: BEGIN RSA PRIVATE KEY) - PKCS#8 PrivateKeyInfo (PEM header: BEGIN PRIVATE KEY)

https://netbox.readthedocs.io/en/stable/core-functionality/secrets/#supported-key-format

PKCS#1形式のRSAキーペアを用意するのが簡単でopenssl genrsaで作れる、、、と思ったけど、NetBoxのUIでもキーペアが作成できます。

鍵の作成は画面一番右上ユーザーメニューの[Profile]から。

f:id:zaki-hmkc:20210402070824p:plain

User Profile画面のサイドメニューの[User Key]で鍵管理の画面になります。

f:id:zaki-hmkc:20210402071030p:plain

初回アクセス時はまだ登録された鍵情報がないため、[Create User Key]押下し、鍵の作成を行います。
別途作成したキーペアの公開鍵の内容をテキストフィールドに入力するか、[Generate a New Key Pair]押下してここでキーペアを作成します。
今回はここでキーペアを作成してみます。

f:id:zaki-hmkc:20210402071224p:plain

[Generate a New Key Pair]押下するとキーペアが作成され、その情報が表示されます。

f:id:zaki-hmkc:20210402071409p:plain

ここで表示された秘密鍵はこの画面を閉じると再表示はできない(NetBox上には残らない)ので、テキストの内容を手元にコピーし、netbox_private.keyなどのファイル名で保存しておきます。
保存したら[I have saved my new private key]押下。 すると元の鍵管理画面に戻って作成された公開鍵情報が表示された状態になるので[Save]を押下。

f:id:zaki-hmkc:20210402071738p:plain

これで鍵情報が登録されました。

Secretの暗号化は共通鍵で暗号化されると書かれてたのに何でRSA公開鍵作るのかというと、暗号化・復号に使うAES共通鍵を、ここで作ったRSAキーペアの公開鍵で暗号化したものがNetBox上に保存され、Secret情報の暗号化・復号にはRSAキーペアの秘密鍵で復号したAES共通鍵を使用するみたい。(ドキュメントを読む限り、そうなる)
webの画面で作成されたキーペアは見た感じ2048ビット長になる模様(NetBox v2.10.4時点)。

web画面でSecretの作成

Secret Roleの作成

Secretを作成するにはその準備としてSecret Roleを作成しておく必要があります。 (Secretの必須パラメタにSecret Roleがある)

Secret Roleの作成は、メニューの[Secrets] -> [Secret Roles]から。

f:id:zaki-hmkc:20210402070727p:plain

Secret Roleには、暗号化するSecretが何の情報なのかメタ情報的な名称を付けてやるとよさげ。 例えば「login credentials」など。

f:id:zaki-hmkc:20210402070711p:plain

Secretの作成

VM情報を開くとダッシュボードに「Secrets」のパートの[+ Add secret]を押下。

f:id:zaki-hmkc:20210402071948p:plain

Secretの作成画面になるので、Secret Roleを選択しデータを入力、[Create]押下。

f:id:zaki-hmkc:20210402072713p:plain

そうすると、現在のNetBoxのweb操作のためのセッション情報に秘密鍵が保持されてない場合、秘密鍵を要求されるので、ここにキーペア作成時に保存した秘密鍵のテキストデータを入力し[Request session key]を押下します。

f:id:zaki-hmkc:20210402072844p:plain

秘密鍵を入力するとセッション情報として保持されたという通知がブラウザのダイアログで表示されるので[OK]押下。

f:id:zaki-hmkc:20210402073223p:plain

で、これで秘密鍵を使用可能になったので、[Create]押下してSecretの登録を再開します。

f:id:zaki-hmkc:20210402073452p:plain

このとき、秘密鍵がセッション情報に登録されているので、[Unlock]を押下すると内容を確認できます。(内容はダミーですw)

f:id:zaki-hmkc:20210402073637p:plain

RESTでSecretの参照

RESTの仕様は例によってNetBoxのweb画面下部の「{} API」で見れるswaggerから。
secretsのセクションにエンドポイント一覧があるのでそこで確認できます。

登録したSecretの情報を取得するには、/secrets/secrets/{id}/を使用します。(IDはwebで見たときのURLで確認)

[zaki@cloud-dev netbox-key]$ curl -X GET http://192.168.0.19:28080/api/secrets/secrets/1/ -H "Accept: application/json; indent=4" -H "Authorization: Token ${NETBOX_TOKEN}"
{
    "id": 1,
    "url": "http://192.168.0.19:28080/api/secrets/secrets/1/",
    "assigned_object_type": "virtualization.virtualmachine",
    "assigned_object_id": 6,
    "assigned_object": {
        "id": 6,
        "url": "http://192.168.0.19:28080/api/virtualization/virtual-machines/6/",
        "name": "client-dev"
    },
    "role": {
        "id": 1,
        "url": "http://192.168.0.19:28080/api/secrets/secret-roles/1/",
        "name": "login credentials",
        "slug": "login-credentials"
    },
    "name": "zaki",
    "plaintext": null,
    "hash": "pbkdf2_sha256$1000$jA10MuGv10l3$K586baQvjZQCl10idEA0WbeuWwHqotrOuG0Oz/7pAzQ=",
    "tags": [],
    "custom_fields": {},
    "created": "2021-04-01",
    "last_updated": "2021-04-01T22:32:14.236537Z"
}

ただし、普通にやってもデータが入っているplaintextのところは(暗号化データを復号できずに)nullとなっている。
この値を取得するには、webの画面でセッションとして秘密鍵情報を登録したのと同じように、RESTで秘密鍵を送信してセッションキーを発行しておく必要があります。

[zaki@cloud-dev netbox-key]$ ls -F
gen_by_openssl/  netbox_register_private.key

カレントディレクトリに秘密鍵を保存したnetbox_register_private.keyファイルがある場合、--data-urlencode@でファイル指定して以下のRESTでセッションキーを取得します。

[zaki@cloud-dev netbox-key]$ curl -X POST -H "Authorization: Token ${NETBOX_TOKEN}" -H "Accept: application/json; indent=4" --data-urlencode "private_key@netbox_register_private.key" http://192.168.0.19:28080/api/secrets/get-session-key/
{
    "session_key": "8zJfic3TpR/WcJhDtuIWChZQrdkuJeIsfVI3JmDmqhc="
}

これでセッションキーを取得できたので、X-Session-Keyヘッダにこの値を追加して再度SecretのAPIを叩く。

[zaki@cloud-dev netbox-key]$ curl -X GET http://192.168.0.19:28080/api/secrets/secrets/1/ -H "Accept: application/json; indent=4" -H "Authorization: Token ${NETBOX_TOKEN}" -H "X-Session-Key: 8zJfic3TpR/WcJhDtuIWChZQrdkuJeIsfVI3JmDmqhc="
{
    "id": 1,
    "url": "http://192.168.0.19:28080/api/secrets/secrets/1/",
    "assigned_object_type": "virtualization.virtualmachine",
    "assigned_object_id": 6,
    "assigned_object": {
        "id": 6,
        "url": "http://192.168.0.19:28080/api/virtualization/virtual-machines/6/",
        "name": "client-dev"
    },
    "role": {
        "id": 1,
        "url": "http://192.168.0.19:28080/api/secrets/secret-roles/1/",
        "name": "login credentials",
        "slug": "login-credentials"
    },
    "name": "zaki",
    "plaintext": "curry_tabetai",
    "hash": "pbkdf2_sha256$1000$jA10MuGv10l3$K586baQvjZQCl10idEA0WbeuWwHqotrOuG0Oz/7pAzQ=",
    "tags": [],
    "custom_fields": {},
    "created": "2021-04-01",
    "last_updated": "2021-04-01T22:32:14.236537Z"
}

無事に"plaintext": "curry_tabetai" という値を取得できました。
ちなみにAnsibleのlookup pluginを使う場合はkey_fileに秘密鍵ファイルのパスを指定すれば、セッション処理は自動でやってくれるので楽です。(あとでまとめる)

curlのオプションはこの辺も参照。 qiita.com

環境

Docker版NetBox (v2.10.4)で確認。

[Ansible] そのtag設定、想定通りに動いてますか? (継承機能とその実装を確認する)

playbook内のtask定義にtagを設定しておくことで、指定tagのtaskのみ実行したり、逆に指定tagのtaskを除外してansible-playbookを実行することができます。
開発中のtaskのみピンポイントで実行したい場合や、逆に、共有のDBのデータを更新したりするtaskはほかのユーザーやチームと調整してからでないと実行が難しかったり、Blue-Greenデプロイメントの実装で環境Aの機能をオフにしてもう片方の環境Bをオンにするような処理だけど開発中は環境Bだけ確認したかったり、大量データのダウンロードや冪等の確認を伴い処理に時間がかかるため開発中は実行したくないなど特定のtaskは実行したくない場合に利用できます。

また、特殊tagとして、常に実行するalwaysと実行しないneverというtagが予約語として用意されています。
neverは特に「通常は実行したくない場合」にtaskに設定しておくと、簡単に除外設定できます。

ただし、tag設定はtaskの呼び出し元のtag設定も継承されるという特徴があります。

docs.ansible.com

tag設定をちゃんと設計・把握していないと、例えばrole単体で作成してて自前のplaybookから呼んでいるときは問題なかったけど、チームの他の人が作成したplaybookと結合したら想定してなかったtagが追加されて、期待しない動作をすることがあるので注意が必要です。

なお、本文中の「tagの設定」はplaybookに対するtagsディレクティブを使ったtagの使用を表し、「tagの指定」はansible-playbook実行時のオプション--tags--skip-tagsによるtagの使用を表します。(この記事内でのローカルルール)

また、Ansibleバージョンは2.10.5で確認しています。

TL;DR

本記事の内容は、importでtaskを読み込んだ場合の話です。

  • playbookのtag設定は、「tagの継承」によってそのtaskの呼び出し元定義のtag設定も有効
  • tag設定によってtaskが実行されるか評価されるのはtask単位
    • 呼び出し元のplayやroleのtag設定と除外指定だけで配下の全taskが除外されたりはしない
  • 動かしたくないtaskがある場合はnever設定だけに頼らず--skip-tagsを指定
    • --tags neverのみ指定するとnevertag設定されたtaskは動作する
    • nevertagと他のtagを複数設定されたtaskは、他に設定されているtagを--tagsで指定されると動作する
    • --skip-tags nevernevertagが設定されたtaskは他の有効なtagがあっても動かない
  • ansible-playbook実行時は、まず--tagsで処理対象taskかチェックされてから--skip-tagsで除外対象かをチェックされる
    • --skip-tagsの指定が優先される
    • --skip-tags alwaysalwaystag設定されたtaskは動かない
  • 実行前にansible-playbook --list-tasksで処理対象taskとそのtag設定を確認しよう

tag設定の継承例

playbook定義

tag設定の継承のされかたは、ちょっとわざとらしい上に一部無意味だけど以下のplaybookを例で説明します。

# playbook
---
- hosts: localhost
  gather_facts: false
  tags: __play

  tasks:
    - import_role:
        name: other
      tags: __other
    - name: sample task
      debug:
        msg: "sample tasks"

  roles:
    - role: sample
      tags: __role1

    - role: other
      tags: __role2
  • playに__playを設定
  • playはtasksにtaskを2つ定義
    • task1は__othertagを設定してotherroleを設定
    • task2はtag設定無し
  • playはrolesも使ってroleを2つ定義
    • role1は__role1tagを設定してsampleroleを設定
    • role2は__role2tagを設定して(taskと同じ)otherroleを設定

tasksrolesを同時指定した場合の処理順はroles->tasks

docs.ansible.com

role定義

上記playbookが参照している一つ目のsampleroleは以下の通り。

# roles/sample/tasks/main.yml
---
- name: task1
  debug:
    msg: use __tag1
  tags: __tag1

- name: task2
  debug:
    msg: use __tag2 and never
  tags:
    - __tag2
    - never

- name: task3
  debug:
    msg: use never
  tags:
    - never

もう一つのotherroleは以下の通り。

# roles/other/tasks/main.yml
---
- name: other
  debug:
    msg: "other"
  tags: __other_task
  
- name: import
  import_tasks: other_sub.yml

そしてさらにotherroleでは、import_tasksを使って別のtaskファイルを読んでいる。内容は以下の通り。

# roles/other/tasks/other_sub.yml
---
- name: other_sub
  debug:
    msg: other_sub

構造を整理

上記のplaybookのplay/task/roleの呼び出し構成とtagの設定場所の概要は以下の通り。
playやtaskのように要素に対して設定されてるものはブロックの中へ、rolesimport_roleimport_tasksのように別定義の呼び出し時に設定されるものは矢印にtagを記載しています。

f:id:zaki-hmkc:20210324093715p:plain

ここで、playbookのファイル上でtaskに設定されてるだけのtagを整理すると以下の通り。

No task tags
(1) sample task N/A
(2) other __other_task
(3) other_sub N/A
(4) task1 __tag1
(5) task2 __tag2, never
(6) task3 never

しかし、実際は「tagの継承機能」が働くため、ansible-playbook実行時のtaskごとのtag設定としては以下の通りになります。(順不同)

No task tags
(1) sample task __play_tag
(2)-a other __other_task, __other, __play_tag
(2)-b other __other_task, __parent_role_tag2, __play_tag
(3)-a other_sub __other, __play_tag
(3)-b other_sub __parent_role_tag2, __play_tag
(4) task1 __tag1, __parent_role_tag1, __play_tag
(5) task2 __tag2, never, __parent_role_tag1, __play_tag
(6) task3 never, __parent_role_tag1, __play_tag

ここまで来るとマトリクス表の方がわかりやすいかもなのではみ出るけど併記。(はてなブログのスタイルなんか良いのないかなー)

No task __play __other __role1 __role2 __other_task __tag1 __tag2 never
(1) sample task
(2)-a other
(2)-b other
(3)-a other_sub
(3)-b other_sub
(4) task1
(5) task2
(6) task3

(2)のotherと(3)のother_subは、playで定義されたtasksから呼ばれるパターンと、rolesから呼ばれるパターンで、上位の定義が異なるため、同じファイルでも継承されるtagの情報が異なります。
(tasksからimport_roleで呼ばれるときは__otherが設定され、rolesから呼ばれるときは_role2が設定される)

呼び出し元の判定だけでは終了しない

大抵のプログラミング言語には制御構文ifを使った条件分岐を実装できます。
以下のようなコードを記述すると、条件Aに合致しなかった場合はcode-a以下入れ子になっている条件Bifは評価されずに処理は進みます。

if (条件A) {
  // code-a
  if (条件B) {
    // code-b
    if (条件C) {
      // code-c
    }
  }
}

それに対してAnsibleのplaybookにおけるroleやimport_tasksなどのimport系の外部taskの呼び出しは、呼び出し元のtag条件に合致しなくても、一旦すべてのtaskの内容をロードし、継承によるtag設定も含めて全tagを評価した上で、そのtaskが処理対象か否かを判定します。

前述の例で、例えばsamplerole内に定義されたtask1(No 4)を見てみると、呼び出し階層は以下のようになります。

f:id:zaki-hmkc:20210324094012p:plain

コマンドラインオプションの--tags指定の特徴として、「指定されたtagがplaybookで設定されているtaskを実行する。tagが設定されなければ除外」という動きになりますが、--tags __tag1を指定してansible-playbookを実行する場合、「プログラミングの制御構文ifのように呼び出し元のplayのtags: __playの時点でtagの条件に合致しないため処理を終了する」という動きにはなりません

オプションは後述しますが、--tags__tag1を指定して--list-tasksを使ってtaskとtagの一覧を出力すると、以下の通りtask1が処理対象となることが確認できます。

$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __tag1

playbook: playbook.yml

  play #1 (localhost): localhost        TAGS: [__play]
    tasks:
      sample : task1    TAGS: [__play, __role1, __tag1]

(追記) importとinclude

なおこの動作は、import系を使った静的なtaskの読み込みの場合になります。実行時に動的にtask読み込みを行うincludeは、事前の読み込み処理の時点ではtaskを読み込まないため、include_* を行うtask自体がtag条件に合致しなかった場合、対象の(もしimportだったら処理対象だったとしても)taskは読み込まれないため処理対象になりません。逆に、include_* 自体がtag条件に合致して外部のtask情報を読み込んでも、読み込んだtaskへtag情報は継承されません

本記事も基本的にすべてimport系のtask読み込みを前提とした説明になります。(task読み込みの前処理段階でincludeの場合はtaskを読み込まないため)

Comparing includes and imports: dynamic and static re-use

docs.ansible.com

taskとtagを確認するオプション

1つのplaybookで完結しているなど小規模で目視できるならともかく、この例のように呼び出し階層も複数あると、tagsで文字列検索しても構造を把握するのは正直難しいです。
ですが、ansible-playbookコマンドには対象playbookのtaskとtagの構造をリストアップする便利なオプションがあります。

--list-tags: 実行予定taskで使用される全tagを出力

--list-tagsを指定します。
これを使うと「実行対象のtaskに設定されている全てのtag」をまとめて出力できます。
neverなどによって対象外となっているtaskについては表示されません。

※ 正確には「実行されるかどうか評価の対象となるtask」であり、例えばwhenの評価結果で実行されない場合のtaskでも出力の対象となります。
事前処理としてplaybookの構文解析を行い処理対象となるtask一覧を静的にリストアップできた内容になります。

$ ansible-playbook -i localhost, playbook.yml --list-tags

playbook: playbook.yml

  play #1 (localhost): localhost        TAGS: [__play]
      TASK TAGS: [__other, __other_task, __play, __role1, __role2, __tag1]

もしnever設定のtaskを処理するつもりがないのにneverが含まれている場合は注意してください。

--list-tasks: 実行予定の全taskとそれぞれのtagを出力

実行されるtaskとそのtag設定は、ansible-playbookのオプション--list-tasksで確認できます。

$ ansible-playbook -i localhost, playbook.yml --list-tasks

playbook: playbook.yml

  play #1 (localhost): localhost        TAGS: [__play]
    tasks:
      sample : task1    TAGS: [__play, __role1, __tag1]
      other : other     TAGS: [__other_task, __play, __role2]
      other : other_sub TAGS: [__play, __role2]
      other : other     TAGS: [__other, __other_task, __play]
      other : other_sub TAGS: [__other, __play]
      sample task       TAGS: [__play]

これらのオプションは、--tag--skip-tagでtag指定した場合は、tag設定によって実行対象となるtaskのみが表示されます。
--checkのさらに1段階前の実行対象のtaskが何になるかを確認するのに利用すると良いです。

例えば--tagを指定すると以下の通り、指定されたtagが設定されている特定taskのみ実行対象になることを確認できます。

$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __role2

playbook: playbook.yml

  play #1 (localhost): localhost        TAGS: [__play]
    tasks:
      other : other     TAGS: [__other_task, __play, __role2]
      other : other_sub TAGS: [__play, __role2]

neverが設定されたtaskが実行される条件

上記の実行例ではneverが設定されたtaskはリストアップの対象になりません。

--tagsが未指定(あるいはデフォルトのall指定)の場合は、never設定のtaskは処理対象となりません。 neverが設定されたtaskを実行対象にするには以下の通り。

  • --tags neverを指定する
  • taskにnever以外にもtagを設定しておき、そのtagを--tagsで指定する
  • neveralwaysをtaskに同時設定する (--tags指定は不要)

よってnevertagを設定して通常実行されないように設定されているtaskでも、--tagsで明示的に指定されたり、併記・あるいは継承されたtagを--tagsで指定しても実行されます。
言い換えると、taskに設定されたneverを含むtagのうち、どれか一つでも--tagsによって対象と判定されれば(never設定されていても)処理対象となります。

以下のように--tags __role1を指定すると、__role1tagの設定を上位で行ったsampleroleはneverが設定されていても全て実行対象となります。

$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __role1

playbook: playbook.yml

  play #1 (localhost): localhost        TAGS: [__play]
    tasks:
      sample : task1    TAGS: [__play, __role1, __tag1]
      sample : task2    TAGS: [__play, __role1, __tag2, never]
      sample : task3    TAGS: [__play, __role1, never]

ただしこれは--tagsのみが指定された場合。
--skip-tagsの指定がある場合は、--tagsの判定後に除外判定が行われます。

never設定されたtaskを確実に動かしたくない場合は、--skip-tags neverを指定することで、「taskに設定されたneverを含むtagのうちどれか一つでも--tagsで処理対象と判定」された結果を、除外対象に上書きできます。

$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __role1 --skip-tags never
playbook: playbook.yml

  play #1 (localhost): localhost        TAGS: [__play]
    tasks:
      sample : task1    TAGS: [__play, __role1, __tag1]

実装の調査

というわけで長い前置きでしたが、ここからさらに長くなるおまけコンテンツ(本題)です。
playbookからどうやって設定されているtag情報を読み取って、継承処理を行っているか、その実装を確認してみます。

この辺りはとくにドキュメントで規定されているわけでは(たぶん)ないので、少なくとも現在のAnsible 2.10.5の場合のアーキテクチャの話になります。
今後このあたりの(仕様は変わらなくても)実装が変更されるメジャーバージョンアップなどがあれば話は変わってくるかもしれません。

リスト出力部分

まずは--list-tasks--list-tagsが指定された場合の「taskとtag情報を保持しつつ実際には処理せずにその情報のみprint」がどうなってるか確認してみます。

リスト表示のみの実行

--list-tasks--list-tagsが指定された場合の出力箇所はPlaybookCLIのrun()以下。
results変数にplayの情報がセットされており、ループで逐次内容をprintしています。

                    msg = "\n  play #%d (%s): %s" % (idx + 1, ','.join(play.hosts), play.name)
                    mytags = set(play.tags)
                    msg += '\tTAGS: [%s]' % (','.join(mytags))
                                    if context.CLIARGS['listtasks']:
                                        cur_tags = list(mytags.union(set(task.tags)))
                                        cur_tags.sort()
                                        if task.name:
                                            taskmsg += "      %s" % task.get_name()
                                        else:
                                            taskmsg += "      %s" % task.action
                                        taskmsg += "\tTAGS: [%s]\n" % ', '.join(cur_tags)

ここでtask情報のprintを行いreturnするとこのままreturn 0でプログラムは終了するので、リスト表示指定を行わず通常実行の場合では、results変数を取得する前段の以下の時点でターゲットノードに対する各処理は完了していることになります。

        results = pbex.run()

通常実行

前述のリスト表示の場合results変数にはplayの情報がセットされていました。
それに対して--list-tasksなどのリスト表示指定をせずに普通に実行すると、run()の戻り値はplayの実行結果(正常なら0、異常なら非0のint)となります。

そのため、リスト表示するかどうか判定するif isinstance(results, list)で処理対象外となり、そのelseでその戻り値をreturnして終了します。

つまり、--list-tasksなど指定してtask一覧を表示する場合、実際に処理を実行するrun()メソッドはコールされてから(処理が諸々スキップされたのちに)、Ansible実行の最後に出力する実装になっています。

処理分岐の実装箇所

pbex.run()のコールは以下で、前述の通り--list-tagsなどの指定による情報表示の前に行われます。

        # create the playbook executor, which manages running the plays via a task queue manager
        pbex = PlaybookExecutor(playbooks=context.CLIARGS['args'], inventory=inventory,
                                variable_manager=variable_manager, loader=loader,
                                passwords=passwords)

        results = pbex.run()

コールされているPlaybookExecutor()の実装は以下で、--list-tasksなどの指定が行われている場合、コンストラクタの時点TaskQueueManagerの初期化を行わずに、このオブジェクトをNoneにセットしています。

        if context.CLIARGS.get('listhosts') or context.CLIARGS.get('listtasks') or \
                context.CLIARGS.get('listtags') or context.CLIARGS.get('syntax'):
            self._tqm = None

この後の実処理(run())では、このself._tqmの値がNoneであればリストアップのみとして処理されていきます。

playbookの実行時の処理

playbookファイルのリストを1個ずつ処理していく処理の開始場所は以下の部分。

            for playbook_path in self._playbooks:
                pb = Playbook.load(playbook_path, variable_manager=self._variable_manager, loader=self._loader)

ただし、リスト表示のみの動作の場合は、以下の部分になります。

                if self._tqm is None:  # we are doing a listing
                    entry = {'playbook': playbook_path}
                    entry['plays'] = []

entryという辞書に、playbookplaysというキーを持つ変数をセットしています。
この辞書に変数が追加されるのは上記と、以下のもう1か所
これはplayごとの処理のループ内で、対象playの情報を追加しています。
(このelse節で実処理が走るため、リスト表示時はtaskの実処理が行われない)

                    if self._tqm is None:
                        # we are just doing a listing
                        entry['plays'].append(play)

play毎の処理のループを抜けたら以下の部分で配列entrylistentryを追加しています。

                if entry:
                    entrylist.append(entry)  # per playbook

そしてplaybook毎のループの処理が完了したら、リスト表示時は処理完了。play情報の一覧になっているentrylistreturnします
(リスト表示ではなく実処理の場合はこのentrylistは空のままになっているため、ここではreturnされずに処理続行し、最終的にはtask実行が成功した0かエラー時の非0がreturnされるため型が異なる)

            if entrylist:
                return entrylist

よって呼び出し元であるPlaybookCLI#run()は、このreturn値がlist形式か否かで「処理済み」か「処理予定の情報を収集しただけ」かを、isinstance()で判断できます。

tag情報は、playのtagはここで取り出しているplay.tagsで参照しています。

                    mytags = set(play.tags)
                    msg += '\tTAGS: [%s]' % (','.join(mytags))

同じようにtaskのtagは以下
(list tasksと list tagsで表示形式が異なるので取得方法も若干異なる)

                                    all_tags.update(task.tags)
                                    if context.CLIARGS['listtasks']:
                                        cur_tags = list(mytags.union(set(task.tags)))

tag情報取得箇所

tag情報のprint箇所はわかったので、playbookで設定されているtagをどこから読み取ってるのか調べてみます。
そうすると、そもそもtagの前にどうやってplayやtaskの情報をロードしているのか、という話になるわけで、そこから追ってみます。

playbookをソースからロード

エントリポイントは、playbook_executor.pyの前述の以下の部分でplaybookの情報をロード。

                pb = Playbook.load(playbook_path, variable_manager=self._variable_manager, loader=self._loader)

ここからコールされるのは、以下のplaybook/__init__.pyで実装してあるload()スタティックメソッド。

    @staticmethod
    def load(file_name, variable_manager=None, loader=None):
        pb = Playbook(loader=loader)
        pb._load_playbook_data(file_name=file_name, variable_manager=variable_manager)
        return pb

ここでPlaybookインスタンスを作成し_load_playbook_data()メソッドをコール、そこからplaybookの内容をロードしているのは以下の箇所。

        try:
            ds = self._loader.load_from_file(os.path.basename(file_name))
        except UnicodeDecodeError as e:
            raise AnsibleParserError("Could not read playbook (%s) due to encoding issues: %s" % (file_name, to_native(e)))

ここでコールしているのは、呼び出し元で指定しているloaderインスタンスload_from_file()メソッド。
これが何かと言うと、playbook_executor.pyのPlaybook.load()コール時の引数のloader=self._loaderの部分
さらにこれはPlaybookExecutorインスタンス作成時の引数で指定されています。

    def __init__(self, playbooks, inventory, variable_manager, loader, passwords):
        self._playbooks = playbooks
        self._inventory = inventory
        self._variable_manager = variable_manager
        self._loader = loader

じゃあさらに呼び出し元のcli/playbook.pyを見ると、laoderインスタンスは_play_prereqs()で取得しています。

        loader, inventory, variable_manager = self._play_prereqs()

_play_prereqs()の実装はここ
この中でloaderDataLoader()をコールして取得しています。

    def _play_prereqs():
        options = context.CLIARGS

        # all needs loader
        loader = DataLoader()

そしてこのDataLoader()はこちら

斜め読みする限り、Ansibleとしての処理ではなくYAMLを読んでいます。
上位からコールされるメソッドのload_from_file()を追っていくと、self.load() -> from_yaml() -> json.loads() と呼ばれて、Pythonの標準パッケージのjsonで処理されています。
(※ YAML形式のplaybookもjsonで処理している)

なので、この時点ではYAMLとして文法が合っていれば、例えば存在しないモジュールや未定義のディレクティブを書いてたりしても読み込み処理までは動作します。
逆に、YAMLとして文法誤りがあれば、例えばインデント誤りなどあればこの時点でエラーになります。
ただし、Ansibleとしては処理されていないため、rolesimport_playbookのようにAnsibleとしての外部ファイル読み込みもまだ処理されないため、外部ファイルに文法誤りがあってもここではエラー判定は発生しません。

そのままplaybookの処理に戻って内容を追っていくと、呼び出し元で使えるようにplay情報としてリストへ追加しているのはPlaybookクラスの_load_playbook_data()メソッド内下記の2か所で行われます。

                pb = PlaybookInclude.load(entry, basedir=self._basedir, variable_manager=variable_manager, loader=self._loader)
                if pb is not None:
                    self._entries.extend(pb._entries)
                entry_obj = Play.load(entry, variable_manager=variable_manager, loader=self._loader, vars=vars)
                self._entries.append(entry_obj)

前者はimport_playbookでplaybookをさらに読み込んでいる場合で、最終的にplaybook内に記述されているplayの読み込みは後者のPlay.load()で、ここで読み取った一つのplay分のYAML情報(entry)をentry_objとして取得し、self._entriesリストに追加されていきます。

playのロード

Play#load()は以下のスタティックメソッドで、Playインスタンスを生成し、load_data()をコールしています。

        p = Play()
        if vars:
            p.vars = vars.copy()
        return p.load_data(data, variable_manager=variable_manager, loader=loader)

load_data()の実装はplaybook/base.pyFieldAttributeBaseクラス

play以下全taskのロード

load_data()メソッドの下記コードで、全てのplayの情報が再帰的に処理され読み込まれます。

        for name, attr in sorted(iteritems(self._valid_attrs), key=operator.itemgetter(1)):
            # copy the value over unless a _load_field method is defined
            target_name = name
            if name in self._alias_attrs:
                target_name = self._alias_attrs[name]
            if name in ds:
                method = getattr(self, '_load_%s' % name, None)
                if method:
                    self._attributes[target_name] = method(name, ds[name])
                else:
                    self._attributes[target_name] = ds[name]

nameにはtasksroles・そしてtagsなどの属性値が順番にセットされ、_load_tasks()_load_roles()_load_tasks()というメソッドが定義されていればそれがコールされ、その戻り値をself._attributesにセットしていきます。
この処理で、taskやroleあるいやimport系の定義による子要素も読み込まれていきます。

よって、この時点で保持されるtag情報は、まだ該当のtaskに設定されているもののみがセットされます。
(呼び出し元のtag情報は継承されていない)

そして実は、全playのデータロードが完了しても、保持されているデータ構造はこのままで、taskに紐づくtag情報はそのtaskに設定されたtagのみとなっています。
(呼び出し元のtag情報をマージした情報が保持される構成にはならない)

継承の仕組み

前述の通り、読み込まれた全taskの情報はtask単位でオブジェクトとして保持されます。
そしてこの個々のtaskオブジェクトは、そのtaskが持つ使用モジュールやwhenuntilなどの条件、registerなど、各ディレクティブ設定の情報によって構成されています。 (前述の通りself._attributes[key]で参照できる)

設定が継承される動作は、これらの値を参照するタイミングで親情報と結合された内容となる処理によって実装されています。

taskのtag情報などは前述の通りself._attributes['tags']に保持されていますが、実際のコードは.tagsというプロパティ値でアクセスしています。
参照箇所は例えば、--list-tasksで実行した際には以下の通り。

# https://github.com/ansible/ansible/blob/stable-2.10/lib/ansible/cli/playbook.py#L143
                    mytags = set(play.tags)
# https://github.com/ansible/ansible/blob/stable-2.10/lib/ansible/cli/playbook.py#L171
                                        cur_tags = list(mytags.union(set(task.tags)))

この.tagsというプロパティアクセスは、Python組み込み関数property()を使ったメンバアクセス関数設定とfunctools#partial()を使った引数固定の関数指定を使用し、getterのメソッドとして_generic_g_parent()をコールするように、tagsに限らず各属性に対する定義をメタクラスで行っています

                    method = "_get_attr_%s" % attr_name
                    if method in src_dict or method in dst_dict:
                        getter = partial(_generic_g_method, attr_name)
                    elif ('_get_parent_attribute' in dst_dict or '_get_parent_attribute' in src_dict) and value.inherit:
                        getter = partial(_generic_g_parent, attr_name)
                    else:
                        getter = partial(_generic_g, attr_name)

                    setter = partial(_generic_s, attr_name)
                    deleter = partial(_generic_d, attr_name)

                    dst_dict[attr_name] = property(getter, setter, deleter)

このアクセサメソッド設定を、Taggableクラスで定義されている以下の_tagsという名前のプロパティ名を使用し、

class Taggable:

    untagged = frozenset(['untagged'])
    _tags = FieldAttribute(isa='list', default=list, listof=(string_types, int), extend=True)

メタクラスの以下のコード部分tagsという先頭の_を除いた名前でプロパティを動的に設定しています。

            keys = list(src_dict.keys())
            for attr_name in keys:
                value = src_dict[attr_name]
                if isinstance(value, Attribute):
                    if attr_name.startswith('_'):
                        attr_name = attr_name[1:]

これでattr_nameには_tagsの先頭_が取り除かれtagsがセットされ、前述の_generic_g_parent+partial+propertyの設定をdst_dict[attr_name]にセットすることで、.tagsというプロパティアクセスを実装しています。

value.inherit:についてはAtrributeクラスのデフォルト値Trueとなるように実装されます。
※ なので、ここを改造してFalseにセットして動作させると、tagの継承は発生しません。

    def __init__(
        self,
        isa=None,
        private=False,
        default=None,
        required=False,
        listof=None,
        priority=0,
        class_type=None,
        always_post_validate=False,
        inherit=True,
        alias=None,
        extend=False,
        prepend=False,
        static=False,
    ):

以上の実装によって、.tagsプロパティに参照すると、_generic_g_parent(prop_name=tags, self)がコールされるようになります。

_generic_g_parent()の内容についてはメタクラスと同じbase.pyにて実装されています。

def _generic_g_parent(prop_name, self):
    try:
        if self._squashed or self._finalized:
            value = self._attributes[prop_name]
        else:
            try:
                value = self._get_parent_attribute(prop_name)
            except AttributeError:
                value = self._attributes[prop_name]

        # _snip_

(条件によって異なりますが基本的には)_get_parent_attribute()をコールして戻り値を取り出しています。
ではこのメソッドがどこにあるかと言うと、これは呼び出し元(self)が実装しているもので、以下の2クラスで現状は実装されています。

Blockクラス側はRoleの場合の実装の違いなどはありますが、「親情報(_parent)がある場合は、_get_parent_attribute()再帰的に呼び出す」という処理によって、そのtaskが持つ呼び出し元の設定も含めた情報が得られるようになっています。

playbookにおけるtag設定が呼び出し元の設定も継承するのはこの仕組みによって実装されています。

実行対象のtagかどうかを評価する箇所

ansible-playbookの実行時、playbookにおけるtagsの設定に加え、コマンドラインオプションの--tagsおよび--skip-tagsの指定によって、対象taskが実行されるかどうかが決定します。

指定したオプションは以下のPlay#__init__の実装の通り、only_tags--tags指定のもの、skip_tags--skip_tags指定のものとして処理されます。
そして、--tags指定を行わなかった場合は、自動的にallが指定されます。

        self.only_tags = set(context.CLIARGS.get('tags', [])) or frozenset(('all',))
        self.skip_tags = set(context.CLIARGS.get('skip_tags', []))

判定のロジックは以下のTaggable#evaluate_tags()の通りで、まず--tagsによって処理対象かどうかが決定し、その後に--skip-tagsで除外設定かを判定します。(--skip-tagsの指定が優先される)

        should_run = True  # default, tasks to run

        if only_tags:
            if 'always' in tags:
                should_run = True
            elif ('all' in only_tags and 'never' not in tags):
                should_run = True
            elif not tags.isdisjoint(only_tags):
                should_run = True
            elif 'tagged' in only_tags and tags != self.untagged and 'never' not in tags:
                should_run = True
            else:
                should_run = False

        if should_run and skip_tags:

            # Check for tags that we need to skip
            if 'all' in skip_tags:
                if 'always' not in tags or 'always' in skip_tags:
                    should_run = False
            elif not tags.isdisjoint(skip_tags):
                should_run = False
            elif 'tagged' in skip_tags and tags != self.untagged:
                should_run = False

        return should_run

大原則としては以下の通り。

  • --tagsで指定されなくても動かしたければalwaysを設定 (ただし--skip-tags指定で上書きされる)
  • tagが設定されたtaskを動かしたければ--tagsで指定 (ただし--skip-tags指定で上書きされる)
  • tagが設定されたtaskを動かしたくなければ--skip-tagsで指定

気を付けるべきは、指定tag以外のtagが設定されたtaskや、--tags--skip-tagsで指定しなかったtagのtaskがどうなるか。 単純に「nevertagを設定しているから動かないだろう」「--tagsで指定してないから動かないだろう」は継承によって認識外のtagが付与される可能性もあるので危険。

tag以外 (クイズ)

例えばwhenによる条件も継承されます。

playbook.yml

---
- hosts: localhost
  gather_facts: false
  vars:
    cond1: true
    cond2: true

  roles:
    - role: sample
      when:
        - cond1 is defined and cond1|bool
        - cond2 is defined and cond2|bool
    - other

roles/sample/tasks/main.yml

---
- name: roled task 1
  debug:
    msg: "cond1 {{cond1}} , cond2 {{cond2}}"

- name: set cond2 to false
  set_fact:
    cond2: 0

- name: roled task 2
  debug:
    msg: "cond1 {{cond1}} , cond2 {{cond2}}"

- name: set cond2 to true
  set_fact:
    cond2: 1

- name: roled task 3
  debug:
    msg: "cond1 {{cond1}} , cond2 {{cond2}}"

どうでしょう、5つのうちどのtaskが動作するかわかりますか?

正解は以下の通り。

$ ansible-playbook -i localhost, playbook.yml 

PLAY [localhost] *****************************************************************************************************************

TASK [sample : roled task 1] *****************************************************************************************************
ok: [localhost] => 
  msg: cond1 True , cond2 True

TASK [sample : set cond2 to false] ***********************************************************************************************
ok: [localhost]

TASK [sample : roled task 2] *****************************************************************************************************
skipping: [localhost]

TASK [sample : set cond2 to true] ************************************************************************************************
skipping: [localhost]

TASK [sample : roled task 3] *****************************************************************************************************
skipping: [localhost]

PLAY RECAP ***********************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=3    rescued=0    ignored=0   

docs.ansible.com

まとまった情報が見当たらないのですが、多くのディレクティブ/playbookキーワードは継承されます。
例えばこのあたりの指定は呼び出し先のtaskに継承されます。

以下のtask.pyloop_controlのアクセサメソッドの実装のように、inherit=Falseというパラメタを指定しているものは機能は継承されませんが、前述のリストのようにそうでないものは機能が継承される実装になっているようです。

    _loop_control = FieldAttribute(isa='class', class_type=LoopControl, inherit=False)

まとめ

  • playbookにおけるtagの設定は、そのtaskに設定されてるものが全てではないので気を付けましょう。
  • --list-tasks--list-tagsを活用しましょう。

「Kubernetes Novice Tokyo #9」でkindをPodman環境で使う内容でLT登壇した振り返り

Kubernetes Novice Tokyo #9」で「kind on Podman」というタイトルでLTしたので、内容について振り返ります。

k8s-novice-jp.connpass.com

発表資料

speakerdeck.com

内容

Kubernetesのノードとして動作するDockerコンテナを使うことでDocker上でKubernetesクラスタを作成できるkindというツールを、DockerではなくPodmanで動かす、という内容を紹介しました。

LTタイトルは、、、ちょっと端的過ぎたかもしれない。。(反省点)
「DockerではなくPodmanでkindを~」とかの方がわかりやすかったですね。

ベースの情報はLT内でも何度か触れたContainer Runtime Meetup #3のPodmanに関する内容になっていて、その後に割とすぐ手元の環境でもPodman環境でkindを動かす、というのは確認してたりします。

zaki-hmkc.hatenablog.com

以前書いたブログは、Podmanで動かす手順に終始してましたが、Noviceということもあって、「なんでPodman使ったの?」という背景の説明(CentOS開発終了→RHELが使いやすくなった→RHELだとDockerよりPodmanの方が使いやすい)も混ぜることにしました。

登壇タイミングと準備

Container Runtime Meetup #3が1月末だったので、本当は2月のKubernetes Novice Tokyo #8で話せれば(CentOS開発終了のニュースからもさほど時間経ってなくて)タイムリーだったのですが、準備の時間がちょっとなかったので別の機会で…と思ってました。ですが、3月のNoviceの募集が始まったタイミング(2月下旬)も微妙に忙しく、このネタはもう流そうと思ってましたw
そう思ってたところ、LTの抽選締め切りの3月5日直前あたりからちょっと時間取れるようになって、「あれ?週末準備すれば行けるんじゃね?」と思い、抽選前日に枠も1個空いていたので申し込みました。

なので、ネタとしては以前検証した内容だったけど、発表としては割と突貫で準備がんばりました。

とはいえ、LTの時間が5分だと絞り切れなかったと思うので、10分枠だったのは幸いでした。
(といいつつ、10分枠でも微妙にオーバーしそうな感じでしたが)

LTで入らなかったデモ

10分もあるのでデモできるだろーと最初は考えてましたが、無理でした。
kindクラスタの作成はLTの時間内で余裕で完了するので、LTの最初の方でクラスタ作成を行ってスライドの説明を進めつつ頃合いを見計らってターミナルに戻る、というヤツをやってみたかったのですが、コマンドの入出力だけじゃなくて環境や前提など最低限の説明を入れると思ったより時間使ってしまいますね。

5分LTでデモやってる方、スゴイです。。

副産物

(以下、kind version 0.10.0の場合)

rootlessモードの説明をする際に、なんだかんだでDockerのrootlessは試したことが無かったのでいい機会になりました。

zaki-hmkc.hatenablog.com

rootlessのDockerでもkind試しましたが、やっぱり「rootlessコンテナ環境でもsudoが必要」を確認できました。

ちなみに動作は微妙に異なり、rootlessのDocker環境でsudoを使わずにユーザー権限でクラスタ作成しようとすると以下の通りで、「ノードのコンテナをデプロイしたあとのクラスタ作成処理でエラー」となり、rootless実行時のガードは特に入っていない模様。
実際にコンテナをデプロイしてからエラーになるので、失敗まで時間がかかります。

zaki@ubuntu-node:~/local/kind$ ./kind create cluster
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.20.2) 🖼 
 ✓ Preparing nodes 📦  
 ✗ Writing configuration 📜 
ERROR: failed to create cluster: failed to generate kubeadm config content: failed to get kubernetes version from node: failed to get file: command "docker exec --privileged kind-control-plane cat /kind/version" failed with error: exit status 1
Command Output: Error response from daemon: Container 38056cbe6654f6e0c5e0f71c162aff811985eb207c2d3dd807cdb62f6ee9a88f is not running

それに対してPodmanの場合は以下の通りで、クラスター作成前にpodman provider does not work properly in rootless modeとメッセージが出力されている通り「Podmanの場合はrootlessなので一般ユーザー権限だと動かさない」という処理が入っているような雰囲気。(想像です)

[zaki@rhel8 kind]$ kind create cluster
enabling experimental podman provider
Creating cluster "kind" ...
podman provider does not work properly in rootless mode

また、環境変数KIND_EXPERIMENTAL_PROVIDERについては、「Dockerが未インストールでPodmanがインストール済みの場合」は、指定しなくてもPodmanでkindが動作します。
「DockerとPodmanが両方インストール済みの場合」はデフォルトではDockerでkindがデプロイされます。
両方インストール済みの場合にPodmanでkindを動作させたい場合は、明示的にKIND_EXPERIMENTAL_PROVIDER=podmanを指定すればPodmanでkindが使用されます。

関連リンク

スライドのページにも載せてますが再掲。

rheb.hatenablog.com

speakerdeck.com

rheb.hatenablog.com

medium.com

speakerdeck.com

speakerdeck.com

Rootless modeのDockerをUbuntu 20.04へインストール (Debian 10では依存パッケージ不足で失敗)

DockerのRootless modeのセットアップを試してみた。
設定すればデフォルトではroot権限が必要なDockerを一般ユーザーでも使えるモードで動作させることができる。

現在はDockerをパッケージインストールすると、Rootless mode用のセットアップスクリプトも依存でインストールされるようになっている。

docs.docker.com

ちなみにPodmanはデフォルトでRootlessで動作する。

前提

Ubuntu 20.04であれば特に無いが、Debian Busterだと必要なパッケージが公式リポジトリに無いためインストールに失敗する。(後述)

今回の環境は以下の通り。

zaki@ubuntu-node:~$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.2 LTS"

準備

Prerequisitesの通り、uidmapをインストールする。

zaki@ubuntu-node:~$ apt search uidmap
Sorting... Done
Full Text Search... Done
uidmap/focal-updates 1:4.8.1-1ubuntu5.20.04 amd64
  programs to help use subuids

入ってないので入れる。

$ sudo apt install uidmap
zaki@ubuntu-node:~$ id -u
1000
zaki@ubuntu-node:~$ whoami
zaki
zaki@ubuntu-node:~$ grep ^$(whoami): /etc/subuid
zaki:100000:65536
zaki@ubuntu-node:~$ grep ^$(whoami): /etc/subgid
zaki:100000:65536

ディストリビューション毎の設定

Ubuntuの場合は特に準備無し。

その他はドキュメントのDistribution-specific hintの内容を確認する。

例えばDebianの場合は以下。カーネルパラメタを設定する。

Add kernel.unprivileged_userns_clone=1 to /etc/sysctl.conf (or /etc/sysctl.d) and run sudo sysctl --system.

https://docs.docker.com/engine/security/rootless/#distribution-specific-hint

要は以下のコマンドを実行すればOK.

# echo "kernel.unprivileged_userns_clone=1" > /etc/sysctl.d/rootless-docker.conf

設定したカーネルパラメタを確認。

zaki@debian-node:~$ cat /proc/sys/kernel/unprivileged_userns_clone 
0
zaki@debian-node:~$ sudo sh -c 'echo "kernel.unprivileged_userns_clone=1" > /etc/sysctl.d/rootless-docker.conf'
zaki@debian-node:~$ cat /proc/sys/kernel/unprivileged_userns_clone 
1

インストール

通常のDocker未インストールの場合

docker-ce-rootless-extrasをインストールする。 インストールするにはDockerインストール用のリポジトリを追加する必要があるため、各ディストリビューション用のインストール手順を確認する。

docs.docker.com

Ubuntuであれば以下の通り。
(ほかのディストリビューションも、docker-ceをinstallする直前まで進めれば良い)

$ sudo apt-get update
$ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo 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/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
$ sudo apt-get update

設定すればaptでインストールできるようになる。

zaki@debian-node:~$ apt search docker-ce-rootless-extras
ソート中... 完了
全文検索... 完了  
docker-ce-rootless-extras/buster 5:20.10.5~3-0~debian-buster amd64
  Rootless support for Docker.

ただし、docker-ce-rootless-extrasをインストールすると依存でdocker-ceも入るため、以下の「通常のDockerがインストール済みの場合」のサービス停止を確認する。

zaki@ubuntu-node:~$ which docker
/usr/bin/docker

通常のDockerがインストール済みの場合

Dockerは止める。

Note

If the system-wide Docker daemon is already running, consider disabling it: $ sudo systemctl disable --now docker.service

https://docs.docker.com/engine/security/rootless/#install

zaki@ubuntu-node:~$ systemctl is-active docker
active
zaki@ubuntu-node:~$ systemctl is-enabled docker
enabled

Ubuntuだとインストールしただけでenabledになってるので。

zaki@ubuntu-node:~$ sudo systemctl disable --now docker
Synchronizing state of docker.service with SysV service script with /lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install disable docker
Removed /etc/systemd/system/multi-user.target.wants/docker.service.
Warning: Stopping docker.service, but it can still be activated by:
  docker.socket
zaki@ubuntu-node:~$ systemctl is-active docker
inactive
zaki@ubuntu-node:~$ systemctl is-enabled docker
disabled

install

パッケージ版Dockerをインストールすると、Rootless modeのDockerインストールのためのスクリプト/usr/bin/dockerd-rootless-setuptool.sh にある。

zaki@ubuntu-node:~$ which dockerd-rootless-setuptool.sh
/usr/bin/dockerd-rootless-setuptool.sh

このスクリプトをRootlessで実行したい一般ユーザーでinstallを引数に指定して実行する。

zaki@ubuntu-node:~$ dockerd-rootless-setuptool.sh install
[INFO] Creating /home/zaki/.config/systemd/user/docker.service
[INFO] starting systemd service docker.service
+ systemctl --user start docker.service
+ sleep 3
+ systemctl --user --no-pager --full status docker.service
● docker.service - Docker Application Container Engine (Rootless)
     Loaded: loaded (/home/zaki/.config/systemd/user/docker.service; disabled; vendor preset: enabled)
     Active: active (running) since Fri 2021-03-05 00:12:24 UTC; 3s ago
       Docs: https://docs.docker.com/engine/security/rootless/
   Main PID: 75116 (rootlesskit)
     CGroup: /user.slice/user-1000.slice/user@1000.service/docker.service
             ├─75116 rootlesskit --net=slirp4netns --mtu=65520 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run --propagation=rslave /usr/bin/dockerd-rootless.sh
             ├─75129 /proc/self/exe --net=slirp4netns --mtu=65520 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run --propagation=rslave /usr/bin/dockerd-rootless.sh
             ├─75146 slirp4netns --mtu 65520 -r 3 --disable-host-loopback --enable-sandbox --enable-seccomp 75129 tap0
             ├─75153 dockerd
             └─75166 containerd --config /run/user/1000/docker/containerd/containerd.toml --log-level info

Mar 05 00:12:24 ubuntu-node dockerd-rootless.sh[75153]: time="2021-03-05T00:12:24.859622362Z" level=warning msg="Your kernel does not support CPU realtime scheduler"
Mar 05 00:12:24 ubuntu-node dockerd-rootless.sh[75153]: time="2021-03-05T00:12:24.859699390Z" level=warning msg="Your kernel does not support cgroup blkio weight"
Mar 05 00:12:24 ubuntu-node dockerd-rootless.sh[75153]: time="2021-03-05T00:12:24.859760290Z" level=warning msg="Your kernel does not support cgroup blkio weight_device"
Mar 05 00:12:24 ubuntu-node dockerd-rootless.sh[75153]: time="2021-03-05T00:12:24.859931829Z" level=info msg="Loading containers: start."
Mar 05 00:12:25 ubuntu-node dockerd-rootless.sh[75153]: time="2021-03-05T00:12:25.105917170Z" level=info msg="Default bridge (docker0) is assigned with an IP address 172.17.0.0/16. Daemon option --bip can be used to set a preferred IP address"
Mar 05 00:12:25 ubuntu-node dockerd-rootless.sh[75153]: time="2021-03-05T00:12:25.315884736Z" level=info msg="Loading containers: done."
Mar 05 00:12:25 ubuntu-node dockerd-rootless.sh[75153]: time="2021-03-05T00:12:25.324807056Z" level=warning msg="Not using native diff for overlay2, this may cause degraded performance for building images: failed to set opaque flag on middle layer: operation not permitted" storage-driver=overlay2
Mar 05 00:12:25 ubuntu-node dockerd-rootless.sh[75153]: time="2021-03-05T00:12:25.325060434Z" level=info msg="Docker daemon" commit=363e9a8 graphdriver(s)=overlay2 version=20.10.5
Mar 05 00:12:25 ubuntu-node dockerd-rootless.sh[75153]: time="2021-03-05T00:12:25.325172424Z" level=info msg="Daemon has completed initialization"
Mar 05 00:12:25 ubuntu-node dockerd-rootless.sh[75153]: time="2021-03-05T00:12:25.337548054Z" level=info msg="API listen on /run/user/1000/docker.sock"
+ DOCKER_HOST=unix:///run/user/1000/docker.sock /usr/bin/docker version
Client: Docker Engine - Community
 Version:           20.10.5
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        55c4c88
 Built:             Tue Mar  2 20:18:20 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.5
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       363e9a8
  Built:            Tue Mar  2 20:16:15 2021
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.3
  GitCommit:        269548fa27e0089a8b8278fc4fc781d7f65a939b
 runc:
  Version:          1.0.0-rc92
  GitCommit:        ff819c7e9184c13b7c2607fe6c30ae19403a7aff
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
+ systemctl --user enable docker.service
Created symlink /home/zaki/.config/systemd/user/default.target.wants/docker.service → /home/zaki/.config/systemd/user/docker.service.
[INFO] Installed docker.service successfully.
[INFO] To control docker.service, run: `systemctl --user (start|stop|restart) docker.service`
[INFO] To run docker.service on system startup, run: `sudo loginctl enable-linger zaki`

[INFO] Make sure the following environment variables are set (or add them to ~/.bashrc):

export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix:///run/user/1000/docker.sock

timeつけ忘れたけど10秒もかからなかった。
実行ログの最後に、環境変数設定のメッセージが出ているので、これを設定する。
(/usr/binは設定済みなので省略)

zaki@ubuntu-node:~$ export DOCKER_HOST=unix:///run/user/1000/docker.sock

なお、この環境変数は設定するパスを見ても分かる通り、使用するユーザーID毎に異なるため、別のユーザーでも使用する場合は都度dockerd-rootless-setuptool.shを設定する。

zaki@ubuntu-node:~$ docker version
Client: Docker Engine - Community
 Version:           20.10.5
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        55c4c88
 Built:             Tue Mar  2 20:18:20 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.5
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       363e9a8
  Built:            Tue Mar  2 20:16:15 2021
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.3
  GitCommit:        269548fa27e0089a8b8278fc4fc781d7f65a939b
 runc:
  Version:          1.0.0-rc92
  GitCommit:        ff819c7e9184c13b7c2607fe6c30ae19403a7aff
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

永続的に設定するには、この環境変数~/.bashrcに追記しておく。

コンテナをデプロイ

rootでなく通常ユーザーで、とりあえずwebサーバーをローカルで8080/TCPでlistenする設定で実行。

zaki@ubuntu-node:~$ docker run -d -p 8080:80 --rm --name httpd httpd
Unable to find image 'httpd:latest' locally
latest: Pulling from library/httpd
45b42c59be33: Pull complete 
83ac8490fcc3: Pull complete 
bdb2d204d86d: Pull complete 
243acf75a504: Pull complete 
8fc1ad93a9b1: Pull complete 
Digest: sha256:3c252c919ef2445a6a41dde913a56202754821af87c049c4312bf81bdbc6df4b
Status: Downloaded newer image for httpd:latest
0bd290f5a999bf82a3b71cac0d7faf1c9b15ee88567eaa11240ebe870299f379

webアクセス確認。

zaki@ubuntu-node:~$ curl localhost:8080
<html><body><h1>It works!</h1></body></html>

listenしてるポートとプロセスを確認すると、(通常のDockerであればdocker-proxyのところが)rootlesskitがlistenしていることを確認できる。

State   Recv-Q  Send-Q   Local Address:Port    Peer Address:Port  Process                                     
LISTEN  0       4096     127.0.0.53%lo:53           0.0.0.0:*      users:(("systemd-resolve",pid=688,fd=13))  
LISTEN  0       128            0.0.0.0:22           0.0.0.0:*      users:(("sshd",pid=751,fd=3))              
LISTEN  0       4096                 *:8080               *:*      users:(("rootlesskit",pid=983,fd=9))       
LISTEN  0       128               [::]:22              [::]:*      users:(("sshd",pid=751,fd=4))    

プロセス確認すると、rootでなく実行ユーザー権限で動作していることが確認できる。

zaki@ubuntu-node:~$ ps aux | grep docker
zaki         983  0.0  0.3 633996 13360 ?        Ssl  23:23   0:00 rootlesskit --net=slirp4netns --mtu=65520 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run --propagation=rslave /usr/bin/dockerd-rootless.sh
zaki        1011  0.0  0.3 779716 12660 ?        Sl   23:23   0:00 /proc/self/exe --net=slirp4netns --mtu=65520 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run --propagation=rslave /usr/bin/dockerd-rootless.sh
zaki        1034  0.0  2.2 1020492 89740 ?       Sl   23:23   0:00 dockerd
zaki        1151  0.0  1.1 898976 47252 ?        Ssl  23:23   0:00 containerd --config /run/user/1000/docker/containerd/containerd.toml --log-level info
zaki        1582  0.0  0.1 553704  7616 ?        Sl   23:32   0:00 /usr/bin/rootlesskit-docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.2 -container-port 80
zaki        1588  0.0  0.1 696508  4452 ?        Sl   23:32   0:00 docker-proxy -container-ip 172.17.0.2 -container-port 80 -host-ip 127.0.0.1 -host-port 8080 -proto tcp
zaki        1607  0.0  0.1 111908  7752 ?        Sl   23:32   0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 0bd290f5a999bf82a3b71cac0d7faf1c9b15ee88567eaa11240ebe870299f379 -address /run/user/1000/docker/containerd/containerd.sock
zaki        1769  0.0  0.0   6432   672 pts/0    S+   23:32   0:00 grep --color=auto docker

コンテナの実行状態を確認すると、通常ユーザーでは一覧は見えて、rootユーザーだと何も見えないことを確認できる。

zaki@ubuntu-node:~$ docker ps
CONTAINER ID   IMAGE     COMMAND              CREATED              STATUS          PORTS                  NAMES
0bd290f5a999   httpd     "httpd-foreground"   About a minute ago   Up 59 seconds   0.0.0.0:8080->80/tcp   httpd
zaki@ubuntu-node:~$ sudo docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

inspectを使うと以下の通りユーザーのホームディレクトリ以下が設定されていることがわかる。

zaki@ubuntu-node:~$ docker inspect 0bd2
[
    {
        "Id": "0bd290f5a999bf82a3b71cac0d7faf1c9b15ee88567eaa11240ebe870299f379",

        ...

        "Image": "sha256:464fdc577ef4d4ba06050b76a95ffee72d280f7aaa4291f7f4827fca7a00ed0f",
        "ResolvConfPath": "/home/zaki/.local/share/docker/containers/0bd290f5a999bf82a3b71cac0d7faf1c9b15ee88567eaa11240ebe870299f379/resolv.conf",
        "HostnamePath": "/home/zaki/.local/share/docker/containers/0bd290f5a999bf82a3b71cac0d7faf1c9b15ee88567eaa11240ebe870299f379/hostname",
        "HostsPath": "/home/zaki/.local/share/docker/containers/0bd290f5a999bf82a3b71cac0d7faf1c9b15ee88567eaa11240ebe870299f379/hosts",
        "LogPath": "/home/zaki/.local/share/docker/containers/0bd290f5a999bf82a3b71cac0d7faf1c9b15ee88567eaa11240ebe870299f379/0bd290f5a999bf82a3b71cac0d7faf1c9b15ee88567eaa11240ebe870299f379-json.log",
        "Name": "/httpd",

    ...

なお、一般ユーザー権限で動作するので、1024未満のポートは使用できない。(従来からのOSの制限)

zaki@ubuntu-node:~$ docker run -d -p 80:80 --rm httpd
557e475710bab1688172ffc7ed26bf94439551bcb28d2fe564fc5ac6f8e29f6d
docker: Error response from daemon: driver failed programming external connectivity on endpoint nostalgic_shamir (4ce60fe6e15f3f5f3af4b85e2eb06d0928a9acb2153f48e7903ba6932ba4d699): Error starting userland proxy: error while calling PortManager.AddPort(): cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or set CAP_NET_BIND_SERVICE on rootlesskit binary, or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission denied.

メッセージの通りカーネルパラメタを設定して許可を与えることは可能。

デーモン

ユーザーごとの以下のパスにファイルが作成されている。

zaki@ubuntu-node:~$ cat ~/.config/systemd/user/docker.service 
[Unit]
Description=Docker Application Container Engine (Rootless)
Documentation=https://docs.docker.com/engine/security/rootless/

[Service]
Environment=PATH=/usr/bin:/sbin:/usr/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
ExecStart=/usr/bin/dockerd-rootless.sh 
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutSec=0
RestartSec=2
Restart=always
StartLimitBurst=3
StartLimitInterval=60s
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
Delegate=yes
Type=simple
KillMode=mixed

[Install]
WantedBy=default.target

systemctl--userオプションを付けて確認可能

zaki@ubuntu-node:~$ systemctl --user status docker
● docker.service - Docker Application Container Engine (Rootless)
     Loaded: loaded (/home/zaki/.config/systemd/user/docker.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2021-03-05 00:12:24 UTC; 40min ago
       Docs: https://docs.docker.com/engine/security/rootless/
   Main PID: 75116 (rootlesskit)
     CGroup: /user.slice/user-1000.slice/user@1000.service/docker.service
             ├─75116 rootlesskit --net=slirp4netns --mtu=65520 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/r>
             ├─75129 /proc/self/exe --net=slirp4netns --mtu=65520 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up>
             ├─75146 slirp4netns --mtu 65520 -r 3 --disable-host-loopback --enable-sandbox --enable-seccomp 75129 tap0
             ├─75153 dockerd
    :
    :
zaki@ubuntu-node:~$ systemctl --user is-active docker
active
zaki@ubuntu-node:~$ systemctl --user is-enabled docker
enabled

この通り、OSをリブートしても有効。


Debian 10 (Buster)だと起動エラー

zaki@debian-node:~$ dockerd-rootless-setuptool.sh install
[INFO] Creating /home/zaki/.config/systemd/user/docker.service
[INFO] starting systemd service docker.service
+ systemctl --user start docker.service
+ sleep 3
+ systemctl --user --no-pager --full status docker.service
● docker.service - Docker Application Container Engine (Rootless)
   Loaded: loaded (/home/zaki/.config/systemd/user/docker.service; disabled; vendor preset: enabled)
   Active: activating (auto-restart) (Result: exit-code) since Sun 2021-03-07 23:51:41 JST; 973ms ago
     Docs: https://docs.docker.com/engine/security/rootless/
  Process: 6490 ExecStart=/usr/bin/dockerd-rootless.sh (code=exited, status=1/FAILURE)
 Main PID: 6490 (code=exited, status=1/FAILURE)
+ set +x
[ERROR] Failed to start docker.service. Run `journalctl -n 20 --no-pager --user --unit docker.service` to show the error log.
[ERROR] Before retrying installation, you might need to uninstall the current setup: `/usr/bin/dockerd-rootless-setuptool.sh uninstall -f ; /usr/bin/rootlesskit rm -rf /home/zaki/.local/share/docker`
No journal files were opened due to insufficient permissions.
zaki@debian-node:~$

謎エラーがでた。

journalctl -n 20 --no-pager --user --unit docker.serviceを実行してもエラーが何も記録されていない。

zaki@debian-node:~$ journalctl -n 20 --no-pager --user --unit docker.service
Hint: You are currently not seeing messages from the system.
      Users in the 'systemd-journal' group can see all messages. Pass -q to
      turn off this notice.
No journal files were opened due to insufficient permissions.
zaki@debian-node:~$

情報が何も無さすぎるので、とりあえずログの方を何とかしようと検索してみると、以下の情報がヒット。

stackoverflow.com

/etc/systemd/journald.confで以下の変更を行ってsystemd-journaldをリスタート。

@@ -13,6 +13,7 @@
 
 [Journal]
 #Storage=auto
+Storage=persistent
 #Compress=yes
 #Seal=yes
 #SplitMode=uid

これで再度dockerd-rootless-setuptool.sh installすると、以下のようにエラーが記録された。

zaki@debian-node:~$ journalctl -n 20 --no-pager --user --unit docker.service
-- Logs begin at Mon 2021-03-08 00:42:16 JST, end at Mon 2021-03-08 00:42:27 JST. --
 3月 08 00:42:25 debian-node dockerd-rootless.sh[1061]: + : builtin
 3月 08 00:42:25 debian-node dockerd-rootless.sh[1061]: + : auto
 3月 08 00:42:25 debian-node dockerd-rootless.sh[1061]: + : auto
 3月 08 00:42:25 debian-node dockerd-rootless.sh[1061]: + net=
 3月 08 00:42:25 debian-node dockerd-rootless.sh[1061]: + mtu=
 3月 08 00:42:25 debian-node dockerd-rootless.sh[1061]: + [ -z ]
 3月 08 00:42:25 debian-node dockerd-rootless.sh[1061]: + which slirp4netns
 3月 08 00:42:25 debian-node dockerd-rootless.sh[1061]: + [ -z ]
 3月 08 00:42:25 debian-node dockerd-rootless.sh[1061]: + which vpnkit
 3月 08 00:42:25 debian-node dockerd-rootless.sh[1061]: + echo Either slirp4netns (>= v0.4.0) or vpnkit needs to be installed
 3月 08 00:42:25 debian-node dockerd-rootless.sh[1061]: Either slirp4netns (>= v0.4.0) or vpnkit needs to be installed
 3月 08 00:42:25 debian-node dockerd-rootless.sh[1061]: + exit 1
 3月 08 00:42:25 debian-node systemd[549]: docker.service: Main process exited, code=exited, status=1/FAILURE
 3月 08 00:42:25 debian-node systemd[549]: docker.service: Failed with result 'exit-code'.
 3月 08 00:42:27 debian-node systemd[549]: docker.service: Service RestartSec=2s expired, scheduling restart.
 3月 08 00:42:27 debian-node systemd[549]: docker.service: Scheduled restart job, restart counter is at 3.
 3月 08 00:42:27 debian-node systemd[549]: Stopped Docker Application Container Engine (Rootless).
 3月 08 00:42:27 debian-node systemd[549]: docker.service: Start request repeated too quickly.
 3月 08 00:42:27 debian-node systemd[549]: docker.service: Failed with result 'exit-code'.
 3月 08 00:42:27 debian-node systemd[549]: Failed to start Docker Application Container Engine (Rootless).

slirp4netnsがインストールされてないのでインストールする。

packages.debian.org

と思ったのだけど、現在のstableであるDebian Busterだとslirp4netnsのバージョンは0.2.3のため、要件が合わない。(ログを見る限り0.4.0以上が必要)
なので、替わりにvpnkitを用意すれば良さそう。
ただしパッケージはなさそうなので自前で用意する必要があるかも。(未確認)

github.com

なお、sidや時期stableバージョンのBullseyeであれば1.0.1なので使用可能と思われる。(未確認確認 )

packages.debian.org

Debian11で確認は以下

zaki-hmkc.hatenablog.com

このパッケージはCentOS 7であればバージョン0.4.3をインストール可能なので、CentOS であれば通常のパッケージインストールで使用可能だと思われる。(未確認)

[zaki@cloud-dev ~]$ yum info slirp4netns
読み込んだプラグイン:fastestmirror
Loading mirror speeds from cached hostfile
 * base: ty1.mirror.newmediaexpress.com
 * epel: ftp.jaist.ac.jp
 * extras: ty1.mirror.newmediaexpress.com
 * updates: ty1.mirror.newmediaexpress.com
インストール済みパッケージ
名前                : slirp4netns
アーキテクチャー    : x86_64
バージョン          : 0.4.3
リリース            : 4.el7_8
容量                : 169 k
リポジトリー        : installed
提供元リポジトリー  : extras
要約                : slirp for network namespaces
URL                 : https://github.com/rootless-containers/slirp4netns
ライセンス          : GPLv2
説明                : slirp for network namespaces, without copying buffers across the namespaces.

情報源

[Linux] ターミナルでコマンド履歴の検索とsttyのstop設定

bashzshはシェル上でCtrl-r押下してからコマンド名やオプションの文字列を入力すると、コマンド履歴からインクリメンタルサーチできる。

(reverse-i-search)`':

Ctrl-rを押下すると↑のようにプロンプトが変化するので、例えばここにkubectlと入力すれば、直近で実行したkbuectlコマンドを検索できる。(2つ目以前の候補検索は都度Ctrl-rを押下)

ただ、行き過ぎた場合はCtrl-sで戻れる設定になっているが、デフォルトではCtrl-sは端末のstopに割り当てられているため、コマンド検索でなくターミナルの入出力がロックされる。(解除はCtrl-q)
Ctrl-sは例えばリアルタイムで大量に流れ続けるログをtail -fで見ているときに一時停止したい場合などに使用する
※※ うっかりどこか変なキーコンビネーションを押してターミナルがまったく動かなくなった場合は(ネットワークが中途半端に切断された場合を除いて)これが原因のことが大半なのでとりあえずCtrl-qすれば動くかも

stty 設定

確認

この「Ctrl-sは端末のstop」という設定は stty -aで確認できる。

$ stty -a
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = M-^?; eol2 = M-^?; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo -echoe -echok -echonl -noflsh -xcase -tostop -echoprt -echoctl -echoke

使用頻度の高いCtrl-cCtrl-zなどもここで確認できる。

設定変更

設定を変更する場合、例えばstopCtrl-xにバインドするには以下を実行。
(^xは、Ctrl-vの後に制御コード入力でなく、ハット・スモールエックスの2文字でOK)

$ stty stop ^x

この後に確認すると、以下の通り、設定が変更される。

$ stty -a

... start = ^Q; stop = ^X; ...

設定無効化

「いや、stopなんか不要だ!一瞬でも目に入ればわかる!」って場合は、オフにもできる。

$ stty stop undef

この設定を行うと、内容は以下の通り。

$ stty -a
speed 38400 baud; rows 30; columns 246; line = 0;
... start = ^Q; stop = <undef>; ...

:

詳しくはman stty参照。

永続的に設定したい場合は、$HOME/.bashrcにこれらのコマンドを書いておけばOK

まとめ

コマンド履歴を新->旧方向へインクリメンタルサーチするにはCtrl-rをタイプする。
プロンプトは以下。

(reverse-i-search)`':

コマンド履歴を旧->新方向へインクリメンタルサーチするにはCtrl-sをタイプする。
ただしデフォルトで端末制御がCtrl-sを喰ってしまうので使いたい場合はそちらは無効化するか別キーを割り当てる。
プロンプトは以下。

(i-search)`':

いずれもEmacsキーバインドと同じで、Ctrl-risearch-backwardCtrl-sisearch-forwardと、逆サーチ、順サーチがそれぞれ割り当てられている。

履歴のインクリメンタルサーチとCtrl-sによるstop / [unix][shell] | 戯術者の日記

ちなみにfzfを使えば、Ctrl-rで検索モードになったあと↑や↓で検索結果を選べるのでCtrl-s無くてもよかったりするけどね。

[Docker / Docker Compose] コンテナのIPアドレスを固定する方法

以前「コンテナ若葉マーク」で「コンテナ環境ではIPアドレスじゃなくてコンテナ名を使って通信しろ!(IPアドレスは意識するな!)」みたいなことを話したりしたことあったんですが、何らかの理由でコンテナ名(ホスト名)でなくIPアドレスを使って(イコールIPアドレスを固定して)コンテナを使いたい場合について。

コンテナ単体で固定IPアドレスを使いたいユースケースはさすがに無いと思うので、複数のコンテナをデプロイしてお互いのコンテナ間通信の際にIPアドレスを固定したい、という場合について、Docker単体(docker実行)の場合と、Docker Compose使用時それぞれについて説明します。

まずDocker Networkを作成してから、そのネットワーク上にコンテナをデプロイ、という構成は同じです。

Docker単体

ここではコンテナを10.254.253.0/24のアドレスのネットワーク上にデプロイしてみます。

Network作成

Dockerでコンテナ間通信をしたい場合は、まずDocker Networkを作成します。

その際、IPアドレス固定にしたい場合はDocker Networkのサブネットアドレスを指定することでアドレス固定することができます。

$ docker network create --subnet 10.254.253.0/24 fixed_container_network
e79041fae649459e9172e078ef81bb33b1adf33c5ee0fe2270de5dcb47cfd57e

↑で作成したDocker Networkは↓で確認できます。

$ docker network ls | grep fixed_container_network
e79041fae649   fixed_container_network      bridge    local

ホストOSのip aでネットワークインタフェースを確認すると以下の通り。

$ ip a
:
:
643: br-e79041fae649: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:58:bf:b6:14 brd ff:ff:ff:ff:ff:ff
    inet 10.254.253.1/24 brd 10.254.253.255 scope global br-e79041fae649
       valid_lft forever preferred_lft forever

fixed_container_network という名前のDocker Networkが作成されました。

ネットワークのみ指定のコンテナ

IPアドレスは固定せずにネットワークのみ指定する場合は以下の通り。

$ docker run --network fixed_container_network -d --rm --name debian-container debian:latest tail -f /dev/null 
416bea2cddb4d9245e5d9d25fae60861b005de4fcf73faa344ab8c57d428987d

コンテナのIPアドレスを指定しない場合、割り当てられるIPアドレスは以下の通り。

$ docker inspect debian-container --format "{{ .NetworkSettings.Networks.fixed_container_network.IPAddress }}"
10.254.253.2

指定したDocker Networkのサブネットのアドレスのうち、空いているものが割り当てられます。

IPアドレス指定

アドレスを固定するには--ipオプションを使用します。

$ docker run --network fixed_container_network -d --rm --name debian-container-fixed-addr --ip 10.254.253.199 debian:latest tail -f /dev/null 
3ad749fbc5783ad9182439eba66fcef08c9422428b31170d30a40013011ce44b

デプロイされたコンテナのIPアドレスは以下の通り。

$ docker inspect debian-container-fixed-addr --format "{{ .NetworkSettings.Networks.fixed_container_network.IPAddress }}"
10.254.253.199

Docker Compose

Docker Composeを使ってもネットワークのサブネットアドレス固定と、コンテナのIPアドレス固定の指定ができます。

Docker Compose使用時はネットワークの定義は特に追加設定が不要な場合は定義そのものを省略できる(省略時はオートで作成される)けど、前述と同じようにサブネットアドレスを指定する場合は明示的に定義します。

ここではコンテナを10.254.249.0/24のアドレスでデプロイしてみます。

Network定義

固定サブネットアドレスでネットワークを定義するには以下の通り。

networks:
  fixed_compose_network:
    ipam:
      driver: default
      config:
        - subnet: 10.254.249.0/24

fixed_compose_networkはネットワーク名なので任意。
そのパラメタのipam以降の定義で、サブネットアドレスを指定します。

ネットワークのみ指定のコンテナ

services:
  sample_container1:
    image: debian:latest
    command: tail -f /dev/null
    networks:
      fixed_compose_network:

前述の10.254.249.0/24を設定したネットワークを指定したコンテナをデプロイするにはこの通り。

コンテナに対するIPアドレスは指定していないけど、docker-compose up -dすると、以下の通り。 (fixed_address_sampleは実行ディレクトリ名が使われている)

$ docker inspect fixed_address_sample_sample_container1_1 --format "{{ .NetworkSettings.Networks.fixed_address_sample_fixed_compose_network.IPAddress }}"
10.254.249.2

IPアドレス指定

前述の10.254.249.0/24を設定したネットワークを指定しつつ、さらにipv4_addressIPアドレス指定でコンテナをデプロイするにはこの通り。

  sample_container2:
    image: ubuntu:latest
    command: tail -f /dev/null
    networks:
      fixed_compose_network:
        ipv4_address: 10.254.249.89

この内容を追加してdocker-compose up -dすると(既存Compose Fileに追加したあとは停止→起動せずにupのみでOK)、

$ docker inspect fixed_address_sample_sample_container2_1 --format "{{ .NetworkSettings.Networks.fixed_address_sample_fixed_compose_network.IPAddress }}"
10.254.249.89

この通り、指定したIPアドレスが割り当てられています。

see: networks / Compose file version 3 reference | Docker Documentation


Compose File全体は以下の通りです。

version: '3'
services:
  sample_container1:
    image: debian:latest
    command: tail -f /dev/null
    networks:
      fixed_compose_network:

  sample_container2:
    image: ubuntu:latest
    command: tail -f /dev/null
    networks:
      fixed_compose_network:
        ipv4_address: 10.254.249.89

networks:
  fixed_compose_network:
    ipam:
      driver: default
      config:
        - subnet: 10.254.249.0/24

まとめ

アプリケーションがホスト名でなくIPアドレスを指定する必要があるなどネットワーク回りで固有の事情がある場合、IPアドレス固定を行うためのコンテナ実行についてまとめました。

環境

$ docker --version
Docker version 20.10.3, build 48d30b5
$ docker-compose version
docker-compose version 1.27.4, build unknown
docker-py version: 4.4.1
CPython version: 3.6.8
OpenSSL version: OpenSSL 1.0.2k-fips  26 Jan 2017
$ cat /etc/centos-release
CentOS Linux release 7.9.2009 (Core)

参考

docs.docker.com

このページの以下をチェック。