From ef6c45e95e4f3fe0d400a0bc2f209ff65a4b0821 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sat, 3 Oct 2026 12:14:26 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor(containers):=20base=20=E3=81=8B?= =?UTF-8?q?=E3=82=89=20dockerd=E3=83=BBcontainerd=E3=83=BBdind=E3=83=BBaws?= =?UTF-8?q?-cdk-lib=E3=83=BBroot=20=E3=81=AE=20uv=E3=83=BB=E6=96=87?= =?UTF-8?q?=E6=9B=B8=E3=82=92=E5=A4=96=E3=81=99=20(#400)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apt から docker-ce と containerd.io を外す(docker-ce-cli・buildx・compose は残す) - containers/base/dind とその COPY を消し、ShellCheck の対象の並びから外す - npm のグローバルから aws-cdk-lib を、root の RUN から uv を外し、PATH から /root/.local/bin を外す - dpkg の除外設定 excludes-devbase を最初の RUN より前に置き、changelog・man・info・/usr/include/node を展開しない - 最初の RUN と利用者の層で /usr/share/doc の copyright でない物を消し、利用者の層で apt の一覧を消す - 外した物を Dockerfile へ戻すと名前を挙げて落ちる試験を足す Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 1 - containers/base/Dockerfile | 25 ++- containers/base/dind | 90 -------- containers/base/dpkg-excludes | 14 ++ .../containers/test_base_dockerfile_fonts.py | 2 +- .../test_base_dockerfile_shellcheck.py | 2 +- tests/containers/test_base_dockerfile_slim.py | 205 ++++++++++++++++++ tests/containers/test_base_shellcheck_ci.py | 2 +- 8 files changed, 238 insertions(+), 103 deletions(-) delete mode 100644 containers/base/dind create mode 100644 containers/base/dpkg-excludes create mode 100644 tests/containers/test_base_dockerfile_slim.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 899f0904..d6b1f25f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,6 @@ jobs: run: | shellcheck --version | sed -n 2p && shellcheck \ containers/base/ai-cli-aliases.sh \ - containers/base/dind \ containers/base/entrypoint.sh \ containers/base/shellrc-dir.sh \ containers/base/tmux-clean \ diff --git a/containers/base/Dockerfile b/containers/base/Dockerfile index 528a4fce..be12c522 100644 --- a/containers/base/Dockerfile +++ b/containers/base/Dockerfile @@ -9,6 +9,10 @@ FROM ubuntu:26.04 ENV DEBIAN_FRONTEND=noninteractive +# 文書の除外 (#400)。Ubuntu の excludes より後に読まれ、copyright を残して changelog・info・man・ +# Node.js のヘッダを展開しない。最初の apt-get install より前に置き、派生イメージにも効かせる +COPY --chmod=0644 dpkg-excludes /etc/dpkg/dpkg.cfg.d/excludes-devbase + # APT パッケージ + Node.js + ロケール(1レイヤーにまとめる) RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ @@ -68,11 +72,14 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ apt-get update; \ # パッケージインストール apt-get install -y --no-install-recommends \ - docker-ce docker-ce-cli containerd.io \ + docker-ce-cli \ docker-buildx-plugin docker-compose-plugin \ terraform gh nodejs $BROWSER_PKG; \ - # クリーンアップ + # クリーンアップ。/usr/share/doc は copyright とディレクトリを指す symlink + # (copyright への経路) だけを残す。除外の設定より前から ubuntu:26.04 にある物も消す (#400) apt-get clean; \ + find /usr/share/doc -mindepth 1 ! -type d ! -xtype d ! -name copyright -delete; \ + rm -rf /usr/share/info/*; \ rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* /tmp/* /var/tmp/* # ユーザー設定 @@ -112,7 +119,7 @@ ENV PATH="/usr/local/share/npm-global/bin:${PATH}" # 非対話の処理からも同じ置き場を使うため。 ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright -# AWS CLI + gcloud SDK + uv + npm globals(1レイヤーにまとめる) +# AWS CLI + gcloud SDK + npm globals(1レイヤーにまとめる) RUN set -eux; \ # 所有を $USERNAME:npm、ディレクトリへ setgid を付けて作る。 # 後から誰が入れてもグループが npm のまま揃う @@ -151,11 +158,9 @@ RUN set -eux; \ cloud-datastore-emulator pubsub-emulator \ kubectl kustomize skaffold minikube \ docker-credential-gcr cloud-run-proxy 2>&1 || true; \ - # uv (Python パッケージマネージャー) - curl -LsSf https://astral.sh/uv/install.sh | sh; \ # npm グローバルパッケージ(umask 0002 で npm グループにも書き込み権を残す) umask 0002; \ - npm i -g yarn @playwright/test aws-cdk aws-cdk-lib typescript @google/gemini-cli @openai/codex @googleworkspace/cli; \ + npm i -g yarn @playwright/test aws-cdk typescript @google/gemini-cli @openai/codex @googleworkspace/cli; \ # root が作ったファイルも ubuntu が上書きできるように揃える。 # install レイヤーと同じ RUN なのでレイヤーの複製は発生しない。 chown -R "$USERNAME":npm "$NPM_CONFIG_PREFIX"; \ @@ -173,7 +178,7 @@ RUN set -eux; \ /var/cache/* \ /var/log/* -ENV PATH="/root/.local/bin:/opt/google-cloud-sdk/bin:${PATH}" +ENV PATH="/opt/google-cloud-sdk/bin:${PATH}" # OpenBao CLI (bao)。devbase の機密のサーバ backend と同じ 2.6 系に固定する (PLAN54)。 # 起動中のコンテナから自分の機密を読み書きし、管理者は管理スクリプトをここから実行する。 @@ -235,8 +240,11 @@ RUN set -eux; \ # クリーンアップ。--with-deps の sudo apt や Node24/npm11 の # /tmp/node-compile-cache が root 所有ファイルを残すため sudo で削除する # (非 root の rm では Permission denied になりビルドが失敗する)。 + # --with-deps の apt-get update が残す apt の一覧と、/usr/share/doc の copyright でない物も消す (#400)。 # ブラウザの置き場 ($PLAYWRIGHT_BROWSERS_PATH) は消さない。 + sudo find /usr/share/doc -mindepth 1 ! -type d ! -xtype d ! -name copyright -delete; \ sudo rm -rf /tmp/* \ + /var/lib/apt/lists/* \ ~/.cache \ ~/.npm \ ~/.config/yarn \ @@ -302,9 +310,8 @@ COPY --chmod=0644 fonts-local.conf /etc/fonts/local.conf # ここで /var/cache/fontconfig を作り直してコンテナ初回起動時の生成を避ける。 RUN sudo fc-cache -f -# entrypoint と dind +# entrypoint COPY --chmod=755 entrypoint.sh /entrypoint.sh -COPY --chmod=755 dind /usr/local/bin/dind WORKDIR /work ENTRYPOINT ["/entrypoint.sh"] diff --git a/containers/base/dind b/containers/base/dind deleted file mode 100644 index f82d3aca..00000000 --- a/containers/base/dind +++ /dev/null @@ -1,90 +0,0 @@ -#!/bin/sh -set -e - -# DinD: a wrapper script which allows docker to be run inside a docker container. -# Original version by Jerome Petazzoni -# See the blog post: https://www.docker.com/blog/docker-can-now-run-within-docker/ -# -# This script should be executed inside a docker container in privileged mode -# ('docker run --privileged', introduced in docker 0.6). - -# Usage: dind CMD [ARG...] - -# apparmor sucks and Docker needs to know that it's in a container (c) @tianon -# -# Set the container env-var, so that AppArmor is enabled in the daemon and -# containerd when running docker-in-docker. -# -# see: https://github.com/containerd/containerd/blob/787943dc1027a67f3b52631e084db0d4a6be2ccc/pkg/apparmor/apparmor_linux.go#L29-L45 -# see: https://github.com/moby/moby/commit/de191e86321f7d3136ff42ff75826b8107399497 -export container=docker - -# Allow AppArmor to work inside the container; -# -# aa-status -# apparmor filesystem is not mounted. -# apparmor module is loaded. -# -# mount -t securityfs none /sys/kernel/security -# -# aa-status -# apparmor module is loaded. -# 30 profiles are loaded. -# 30 profiles are in enforce mode. -# /snap/snapd/18357/usr/lib/snapd/snap-confine -# ... -# -# Note: https://0xn3va.gitbook.io/cheat-sheets/container/escaping/sensitive-mounts#sys-kernel-security -# -# ## /sys/kernel/security -# -# In /sys/kernel/security mounted the securityfs interface, which allows -# configuration of Linux Security Modules. This allows configuration of -# AppArmor policies, and so access to this may allow a container to disable -# its MAC system. -# -# Given that we're running privileged already, this should not be an issue. -if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security; then - mount -t securityfs none /sys/kernel/security || { - echo >&2 'Could not mount /sys/kernel/security.' - echo >&2 'AppArmor detection and --privileged mode might break.' - } -fi - -# Mount /tmp (conditionally) -# /tmp must be 'exec,rw', and 'dev' to allow mknod to work for the -# pkg/archive/archive_linux_test.go tests. -if ! mountpoint -q /tmp; then - mount -t tmpfs none /tmp -fi - -# cgroup v2: enable nesting -if [ -f /sys/fs/cgroup/cgroup.controllers ]; then - # move the processes from the root group to the /init group, - # otherwise writing subtree_control fails with EBUSY. - # An error during moving non-existent process (i.e., "cat") is ignored. - mkdir -p /sys/fs/cgroup/init - # this happens in a loop because things like "docker exec" on our dind - # container will create new processes, which creates a race between our - # moving everything to "init" and enabling subtree_control - while ! { - # move the processes from the root group to the /init group, - # otherwise writing subtree_control fails with EBUSY. - # An error during moving non-existent process (i.e., "cat") is ignored. - xargs -rn1 < /sys/fs/cgroup/cgroup.procs > /sys/fs/cgroup/init/cgroup.procs || : - # enable controllers - sed -e 's/ / +/g' -e 's/^/+/' < /sys/fs/cgroup/cgroup.controllers \ - > /sys/fs/cgroup/cgroup.subtree_control - }; do true; done -fi - -# Change mount propagation to shared to make the environment more similar to a -# modern Linux system, e.g. with SystemD as PID 1. -mount --make-rshared / - -if [ $# -gt 0 ]; then - exec "$@" -fi - -echo >&2 'ERROR: No command specified.' -echo >&2 'You probably want to run hack/make.sh, or maybe a shell?' diff --git a/containers/base/dpkg-excludes b/containers/base/dpkg-excludes new file mode 100644 index 00000000..5a47d0e9 --- /dev/null +++ b/containers/base/dpkg-excludes @@ -0,0 +1,14 @@ +# devbase の文書の除外 (#400)。/etc/dpkg/dpkg.cfg.d/excludes-devbase として置く。 +# Ubuntu の excludes より後に読まれ、dpkg は後に読んだ規則を優先する。 +# base を FROM で継ぐ派生イメージが apt で入れるパッケージにも効く。 + +# 文書を外し直し、copyright だけを残す (Ubuntu が残す changelog も外す) +path-exclude=/usr/share/doc/* +path-include=/usr/share/doc/*/copyright + +# man と info +path-exclude=/usr/share/man/* +path-exclude=/usr/share/info/* + +# Node.js のヘッダ (ネイティブアドオンのビルドでは node-gyp が取得する) +path-exclude=/usr/include/node/* diff --git a/tests/containers/test_base_dockerfile_fonts.py b/tests/containers/test_base_dockerfile_fonts.py index 64a06ea7..ac40925e 100644 --- a/tests/containers/test_base_dockerfile_fonts.py +++ b/tests/containers/test_base_dockerfile_fonts.py @@ -97,7 +97,7 @@ def _first_apt_install(block: str) -> str: """1 つ目の RUN の**1 回目**の apt-get install の一覧だけを取り出す 1 つ目の RUN は apt-get install を 2 回呼ぶ。1 回目は Ubuntu の標準のアーカイブから、 - 2 回目は後から足したリポジトリ (docker-ce / terraform / gh / nodejs) からである。 + 2 回目は後から足したリポジトリ (docker-ce-cli / terraform / gh / nodejs) からである。 """ calls = [m.start() for m in re.finditer(r"apt-get install", block)] assert len(calls) >= 2, "1 つ目の RUN に apt-get install が 2 回無い" diff --git a/tests/containers/test_base_dockerfile_shellcheck.py b/tests/containers/test_base_dockerfile_shellcheck.py index b41405d4..9b9d8b44 100644 --- a/tests/containers/test_base_dockerfile_shellcheck.py +++ b/tests/containers/test_base_dockerfile_shellcheck.py @@ -59,7 +59,7 @@ def _first_apt_install(block: str) -> str: """1 つ目の RUN の**1 回目**の apt-get install の一覧だけを取り出す 1 つ目の RUN は apt-get install を 2 回呼ぶ。RUN の本文全体で探すと、2 回目の一覧 - (後から足したリポジトリの docker-ce / gh / nodejs など) にあっても通ってしまう。 + (後から足したリポジトリの docker-ce-cli / gh / nodejs など) にあっても通ってしまう。 範囲は 1 回目の apt-get install から最初の ``;`` まで。2 回目の直前までにすると、 間にある locale-gen やリポジトリの設定の語でも通ってしまう。 """ diff --git a/tests/containers/test_base_dockerfile_slim.py b/tests/containers/test_base_dockerfile_slim.py new file mode 100644 index 00000000..ccb9b6e5 --- /dev/null +++ b/tests/containers/test_base_dockerfile_slim.py @@ -0,0 +1,205 @@ +"""base イメージから使う者の無い中身を外した形 (#400) + +Docker を起動せず、``containers/base/Dockerfile`` と ``containers/base/dpkg-excludes`` を読んで固定する。 +外した物 (dockerd・containerd・グローバルの aws-cdk-lib・root の uv・apt の一覧・文書・``dind``) を +戻すと、その名前を挙げて落ちる。 +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +from .dockerfile_parse import Instruction, parse + +BASE_DIR = Path(__file__).resolve().parents[2] / "containers" / "base" +DOCKERFILE = BASE_DIR / "Dockerfile" +DPKG_EXCLUDES = BASE_DIR / "dpkg-excludes" +UBUNTU_EXCLUDES = "/etc/dpkg/dpkg.cfg.d/excludes" +UV_INSTALLER = "astral.sh/uv/install.sh" + + +@pytest.fixture(scope="module") +def instructions() -> list[Instruction]: + return parse(DOCKERFILE.read_text()) + + +def _runs(instructions: list[Instruction]) -> list[tuple[int, Instruction]]: + return [(i, ins) for i, ins in enumerate(instructions) if ins.keyword == "RUN"] + + +def _first_run(instructions: list[Instruction]) -> tuple[int, Instruction]: + return _runs(instructions)[0] + + +def _user_index(instructions: list[Instruction]) -> int: + """root でない利用者へ切り替える最初の USER の位置""" + for i, ins in enumerate(instructions): + if ins.keyword == "USER" and ins.args not in ("root", "0"): + return i + raise AssertionError("USER で利用者へ切り替えていない") + + +def _user_layer_run(instructions: list[Instruction]) -> Instruction: + """USER の直後の、利用者の道具を入れる RUN""" + user = _user_index(instructions) + return next(ins for i, ins in _runs(instructions) if i > user) + + +def _words(args: str) -> set[str]: + return set(re.split(r"[\s;\\]+", args)) + + +def _commands(args: str) -> list[str]: + return [c.strip() for c in re.split(r";|&&", args)] + + +# --- I1: dockerd と containerd を入れず、CLI・buildx・compose は入れる --- + + +@pytest.mark.parametrize("package", ["docker-ce", "containerd.io"]) +def test_daemon_packages_are_not_installed(instructions, package): + for _, run in _runs(instructions): + assert package not in _words(run.args), f"{package} を apt で入れている" + + +@pytest.mark.parametrize("package", ["docker-ce-cli", "docker-buildx-plugin", + "docker-compose-plugin"]) +def test_docker_client_packages_are_installed(instructions, package): + _, first = _first_run(instructions) + assert package in _words(first.args), f"{package} を apt で入れていない" + + +# --- I2: グローバルの aws-cdk-lib を入れず、aws-cdk は入れる --- + + +def _npm_global_installs(instructions: list[Instruction]) -> list[set[str]]: + found = [] + for _, run in _runs(instructions): + for command in _commands(run.args): + if re.match(r"npm (i|install) -g\b", command): + found.append(_words(command)) + assert found, "npm のグローバルの install が無い" + return found + + +def test_aws_cdk_lib_is_not_installed_globally(instructions): + for words in _npm_global_installs(instructions): + assert "aws-cdk-lib" not in words, "aws-cdk-lib を npm のグローバルへ入れている" + + +def test_aws_cdk_cli_is_installed_globally(instructions): + assert any("aws-cdk" in words for words in _npm_global_installs(instructions)), \ + "aws-cdk を npm のグローバルへ入れていない" + + +# --- I3: uv は ubuntu の利用者にだけ入れ、PATH に /root/.local/bin を置かない --- + + +def test_uv_is_installed_only_for_user(instructions): + user = _user_index(instructions) + positions = [i for i, run in _runs(instructions) if UV_INSTALLER in run.args] + assert positions, "uv のインストーラを呼ぶ RUN が無い" + root_runs = [i for i in positions if i < user] + assert not root_runs, "root の RUN で uv を入れている" + + +def test_path_has_no_root_local_bin(instructions): + for ins in instructions: + if ins.keyword == "ENV": + assert "/root/.local/bin" not in ins.args, "ENV PATH に /root/.local/bin がある" + + +# --- I4: apt の一覧を取得する RUN は、同じ RUN で一覧を消す --- + + +def test_runs_fetching_apt_lists_remove_them(instructions): + for i, run in _runs(instructions): + fetches = "apt-get update" in run.args or "--with-deps" in run.args + cached = re.search(r"--mount=type=cache,target=/var/lib/apt\b", run.args) + if fetches and not cached: + assert "/var/lib/apt/lists/*" in run.args, \ + f"{i} 番目の命令 (RUN) が apt の一覧を残す: {run.args[:80]}" + + +# --- I5: 文書の除外の設定と、残る文書の削除 --- + + +def _excludes_copy(instructions: list[Instruction]) -> tuple[int, str]: + for i, ins in enumerate(instructions): + if ins.keyword == "COPY" and ins.args.split()[-2:-1] == ["dpkg-excludes"]: + return i, ins.args.split()[-1] + raise AssertionError("dpkg-excludes を COPY していない") + + +def test_excludes_are_copied_before_first_run(instructions): + copy_at, _ = _excludes_copy(instructions) + first_at, _ = _first_run(instructions) + assert copy_at < first_at, "文書の除外の設定が最初の RUN より後にある" + + +def test_excludes_are_read_after_ubuntu_excludes(instructions): + _, dest = _excludes_copy(instructions) + directory, _, name = dest.rpartition("/") + assert directory == "/etc/dpkg/dpkg.cfg.d" + assert name > UBUNTU_EXCLUDES.rpartition("/")[2], \ + f"{name} は Ubuntu の excludes より先に読まれる" + + +def _rules() -> list[str]: + return [line.strip() for line in DPKG_EXCLUDES.read_text().splitlines() + if line.strip() and not line.lstrip().startswith("#")] + + +def test_doc_is_excluded_except_copyright(): + rules = _rules() + exclude = rules.index("path-exclude=/usr/share/doc/*") + include = rules.index("path-include=/usr/share/doc/*/copyright") + assert exclude < include, "copyright の取り込みが doc の除外より前にある" + assert not [r for r in rules if r.startswith("path-include=") and "copyright" not in r], \ + "copyright のほかを取り込んでいる" + + +@pytest.mark.parametrize("path", ["/usr/share/man/*", "/usr/share/info/*", "/usr/include/node/*"]) +def test_paths_are_excluded(path): + assert f"path-exclude={path}" in _rules(), f"{path} を外していない" + + +def _doc_cleanup(run: Instruction) -> str: + found = [c for c in _commands(run.args) if re.search(r"\bfind /usr/share/doc\b", c)] + assert found, f"/usr/share/doc の copyright でない物を消していない: {run.args[:80]}" + return found[0] + + +def _assert_doc_cleanup(command: str) -> None: + for part in ["! -type d", "! -xtype d", "! -name copyright", "-delete"]: + assert part in command, f"/usr/share/doc の削除に {part} が無い: {command}" + assert "-type f" not in command, f"/usr/share/doc の削除が symlink を残す: {command}" + + +def test_first_run_removes_non_copyright_docs(instructions): + _, first = _first_run(instructions) + _assert_doc_cleanup(_doc_cleanup(first)) + assert "/usr/share/info/*" in first.args, "最初の RUN が /usr/share/info を消していない" + + +def test_user_layer_removes_non_copyright_docs_with_sudo(instructions): + command = _doc_cleanup(_user_layer_run(instructions)) + _assert_doc_cleanup(command) + assert command.startswith("sudo find "), f"利用者の層の削除が sudo でない: {command}" + + +# --- I6: dind を置かない --- + + +def test_dind_script_is_gone(): + assert not (BASE_DIR / "dind").exists(), "containers/base/dind が残っている" + + +def test_dockerfile_does_not_install_dind(instructions): + for ins in instructions: + assert "/usr/local/bin/dind" not in ins.args, f"{ins.keyword} が dind を置いている" + if ins.keyword == "COPY": + assert "dind" not in ins.args.split(), "dind を COPY している" diff --git a/tests/containers/test_base_shellcheck_ci.py b/tests/containers/test_base_shellcheck_ci.py index d766921b..0b44b830 100644 --- a/tests/containers/test_base_shellcheck_ci.py +++ b/tests/containers/test_base_shellcheck_ci.py @@ -125,7 +125,7 @@ def ci() -> dict: def test_base_scripts_are_counted(): scripts = base_shell_scripts(BASE_DIR) - for name in ["ai-cli-aliases.sh", "dind", "entrypoint.sh", "shellrc-dir.sh", + for name in ["ai-cli-aliases.sh", "entrypoint.sh", "shellrc-dir.sh", "tmux-clean", "tmux-first", "tmux-session"]: assert name in scripts for name in ["Dockerfile", "fonts-local.conf", "tmux.conf"]: From f05dace3640effae358f882dfe43cab08bd9e47a Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sat, 3 Oct 2026 12:14:26 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor(containers):=20entrypoint=20?= =?UTF-8?q?=E3=81=AE=20DinD=20=E3=81=AE=E5=88=86=E5=B2=90=E3=82=92?= =?UTF-8?q?=E6=B6=88=E3=81=97=E3=80=81ENABLE=5FDIND=20=E3=81=AF=E7=9F=A5?= =?UTF-8?q?=E3=82=89=E3=81=9B=E3=82=92=201=20=E8=A1=8C=E5=87=BA=E3=81=99?= =?UTF-8?q?=E3=81=A0=E3=81=91=E3=81=AB=E3=81=99=E3=82=8B=20(#400)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit devbase_notice_dind_removed を足し、ENABLE_DIND が true か 1 なら廃止を 1 行出して起動を続ける。 docker.sock・dockerd・docker info には触れない。 Co-Authored-By: Claude Opus 5.5 --- containers/base/entrypoint.sh | 82 ++++------------ .../test_entrypoint_dind_removed.py | 93 +++++++++++++++++++ 2 files changed, 109 insertions(+), 66 deletions(-) create mode 100644 tests/containers/test_entrypoint_dind_removed.py diff --git a/containers/base/entrypoint.sh b/containers/base/entrypoint.sh index 06e0eeeb..4e2d2d32 100644 --- a/containers/base/entrypoint.sh +++ b/containers/base/entrypoint.sh @@ -744,6 +744,20 @@ devbase_install_git_credentials() { rm -f "$tmp_cred" } +# DinD (ENABLE_DIND) の廃止を知らせる (#400)。 +# +# base は dockerd を入れなくなった。ENABLE_DIND が true か 1 なら廃止を 1 行出し、それ以外は +# 何も出さない。どちらも 0 を返し、docker.sock・dockerd・docker info には触れない。 +# 設定の残りのためにコンテナを使えなくしないよう、起動は止めない。 +devbase_notice_dind_removed() { + case "${ENABLE_DIND:-}" in + true|1) + echo "NOTICE: ENABLE_DIND は廃止しました (DinD は起動しません)。ホストの docker.sock を mount して docker を使ってください" + ;; + esac + return 0 +} + # テストは関数定義だけを使う (source 時のみ有効な return で以降を読み飛ばす)。 if [ -n "${DEVBASE_ENTRYPOINT_LIB_ONLY:-}" ]; then # 実行したときは return が失敗して exit へ進む。shellcheck は source を想定せず exit を届かないと読む @@ -859,72 +873,8 @@ EOF fi fi -# 5. Docker-in-Docker (DinD) Setup - enabled by ENABLE_DIND=true -if [ "$ENABLE_DIND" = "true" ] || [ "$ENABLE_DIND" = "1" ]; then - echo "Starting Docker-in-Docker..." - # Docker公式パターンに準拠したDinD起動スクリプト - # 参考: https://github.com/docker-library/docker/blob/master/dockerd-entrypoint.sh - - # 1. 古いPIDファイルのクリーンアップ - echo "Cleaning up stale PID files..." - find /run /var/run -iname 'docker*.pid' -delete 2>/dev/null || true - find /run /var/run -iname 'containerd*.pid' -delete 2>/dev/null || true - - # 2. Stale socketのクリーンアップ - echo "Cleaning up stale socket files..." - rm -f /var/run/docker.sock 2>/dev/null || true - rm -rf /var/run/docker/containerd/*.sock 2>/dev/null || true - - # 3. dockerdプロセスチェック - if pgrep -x dockerd > /dev/null; then - echo "Docker daemon already running (PID: $(pgrep -x dockerd))" - else - echo "Starting Docker daemon..." - - # dind wrapper script使用(mount操作にroot権限が必要) - if [ -x '/usr/local/bin/dind' ]; then - echo "Using dind wrapper script" - # tini (docker-init) が利用可能ならPID 1問題を解決 - if command -v docker-init >/dev/null 2>&1; then - echo "Using docker-init (tini) for proper signal handling" - sudo docker-init /usr/local/bin/dind dockerd & - else - sudo /usr/local/bin/dind dockerd & - fi - else - echo "Using dockerd directly" - if command -v docker-init >/dev/null 2>&1; then - sudo docker-init dockerd & - else - sudo dockerd & - fi - fi - - echo "Docker daemon started (PID: $!)" - - # 起動確認(最大30秒待機) - echo "Waiting for Docker daemon to be ready..." - for _ in {1..30}; do - if docker info > /dev/null 2>&1; then - echo "Docker daemon is ready" - break - fi - sleep 1 - done - - # 起動失敗チェック - if ! docker info > /dev/null 2>&1; then - echo "ERROR: Docker daemon failed to start within 30 seconds" - echo "Dockerd process:" - # 診断として利用者と起動の引数を含む全列を出したい。pgrep は PID (-a でも別の列) だけを出す - # shellcheck disable=SC2009 - ps aux | grep dockerd || echo " No dockerd process found" - echo "Docker socket:" - ls -la /var/run/docker.sock 2>/dev/null || echo " Docker socket not found" - exit 1 - fi - fi -fi +# 5. DinD は廃止した (#400)。ENABLE_DIND を残した利用者へ 1 行で知らせ、起動は続ける +devbase_notice_dind_removed # ======================================== # AI Agent Settings Symlink Setup (PLAN39: 共通 / グループの 2 層) diff --git a/tests/containers/test_entrypoint_dind_removed.py b/tests/containers/test_entrypoint_dind_removed.py new file mode 100644 index 00000000..4e07c3cc --- /dev/null +++ b/tests/containers/test_entrypoint_dind_removed.py @@ -0,0 +1,93 @@ +"""entrypoint の DinD の廃止の知らせ (#400) + +base は dockerd を入れなくなり、DinD を廃止した。``ENABLE_DIND`` を残した利用者には 1 行で知らせ、 +起動は止めない。``containers/base/entrypoint.sh`` を ``DEVBASE_ENTRYPOINT_LIB_ONLY=1`` で source し、 +``devbase_notice_dind_removed`` が docker.sock・dockerd・``docker info`` に触れないことを固定する。 +""" + +from __future__ import annotations + +import os +import re +import subprocess +from pathlib import Path + +import pytest + +ENTRYPOINT = Path(__file__).resolve().parents[2] / "containers" / "base" / "entrypoint.sh" + +# 知らせが呼んではならないコマンド。呼ばれたら名前を記録する +FORBIDDEN = ["sudo", "dockerd", "docker", "docker-init", "rm", "pgrep", "sleep"] + + +def run_notice(tmp_path: Path, env: dict) -> tuple[subprocess.CompletedProcess, list[str]]: + stub_dir = tmp_path / "bin" + stub_dir.mkdir() + calls = tmp_path / "calls" + for name in FORBIDDEN: + stub = stub_dir / name + stub.write_text(f'#!/bin/sh\necho {name} >> "{calls}"\n') + stub.chmod(0o755) + base = {k: v for k, v in os.environ.items() + if not k.startswith("DEVBASE_") and k != "ENABLE_DIND"} + base["PATH"] = f"{stub_dir}:{base.get('PATH', '')}" + script = ( + f'set -e\nDEVBASE_ENTRYPOINT_LIB_ONLY=1 . "{ENTRYPOINT}"\n' + "devbase_notice_dind_removed\n" + ) + result = subprocess.run( + ["bash", "-c", script], cwd=tmp_path, env={**base, **env}, + capture_output=True, text=True, + ) + called = calls.read_text().split() if calls.exists() else [] + return result, called + + +@pytest.mark.parametrize("value", ["true", "1"]) +def test_enabled_prints_one_notice_and_touches_nothing(tmp_path, value): + result, called = run_notice(tmp_path, {"ENABLE_DIND": value}) + assert result.returncode == 0, result.stderr + lines = result.stdout.splitlines() + assert len(lines) == 1, result.stdout + assert "ENABLE_DIND" in lines[0] + assert "docker.sock" in lines[0] + assert result.stderr == "" + assert called == [] + + +@pytest.mark.parametrize("env", [{}, {"ENABLE_DIND": "false"}, {"ENABLE_DIND": ""}, + {"ENABLE_DIND": "yes"}]) +def test_not_enabled_prints_nothing(tmp_path, env): + result, called = run_notice(tmp_path, env) + assert result.returncode == 0, result.stderr + assert result.stdout == "" + assert result.stderr == "" + assert called == [] + + +def _main_lines() -> list[str]: + """LIB_ONLY の return より後の、起動のときだけ走る本体""" + lines = ENTRYPOINT.read_text().splitlines() + guard = next(i for i, line in enumerate(lines) if "DEVBASE_ENTRYPOINT_LIB_ONLY:-" in line) + return lines[guard:] + + +def _index(lines: list[str], pattern: str) -> int: + for i, line in enumerate(lines): + if re.search(pattern, line): + return i + raise AssertionError(f"pattern not found: {pattern}") + + +def test_notice_runs_between_aws_and_ai_settings(): + lines = _main_lines() + aws = _index(lines, r'^if \[ -n "\$AWS_CONFIG_BASE64" \]') + notice = _index(lines, r"^devbase_notice_dind_removed$") + ai = _index(lines, r"^devbase_require_account_group ") + assert aws < notice < ai + + +def test_main_does_not_start_dockerd(): + body = "\n".join(line for line in _main_lines() if not line.lstrip().startswith("#")) + for word in ["dockerd", "/usr/local/bin/dind", "docker info", "/var/run/docker.sock"]: + assert word not in body, f"entrypoint の本体に {word} が残っている" From af320b2a46bd1cb07db0108792b6ed72b831bdad Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sat, 3 Oct 2026 12:14:26 +0900 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20DinD=20=E3=81=AE=E5=BB=83=E6=AD=A2?= =?UTF-8?q?=E3=81=A8=20base=20=E3=81=8B=E3=82=89=E5=A4=96=E3=81=97?= =?UTF-8?q?=E3=81=9F=E4=B8=AD=E8=BA=AB=E3=82=92=E4=BB=95=E6=A7=98=E3=83=BB?= =?UTF-8?q?=E9=96=8B=E7=99=BA=E8=80=85=E3=81=AE=E6=96=87=E6=9B=B8=E3=83=BB?= =?UTF-8?q?CHANGELOG=20=E3=81=AB=E6=9B=B8=E3=81=8F=20(#400)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 12 ++++++++++++ docs/developer/contributing.md | 2 +- docs/specifications/base-image-shellcheck.md | 2 +- docs/specifications/ci-checks.md | 5 ++--- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c6c521a..f44b925e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,18 @@ プロジェクトは、GPU を使うものも含めて `containers/general` へ移すか、`FROM devbase-general:latest` で Rust・gfortran・MeCab など要る道具を足す自前の Dockerfile をプロジェクトに置いてください。 手元に残った `devbase-lfm` のイメージは `docker image rm devbase-lfm:latest` で消せます。 +- **DinD(`ENABLE_DIND`)を廃止しました(#400、破壊的変更)。** base イメージは dockerd と containerd + (`docker-ce`・`containerd.io`)を入れず、コンテナの中で dockerd を起こす手順と `/usr/local/bin/dind` を + 外しました。`ENABLE_DIND` に `true` か `1` を設定していても DinD は起きず、起動の記録(`docker logs`)に + 廃止を知らせる `NOTICE:` の行を 1 行出して起動を続けます。docker はホストの docker.sock を mount して + 使ってください。docker CLI・`docker buildx`・`docker compose` は今までどおり使えます。 +- **base イメージから、使う者の無い中身を外しました(#400)。** グローバルの `aws-cdk-lib`(CDK のアプリは + 自分の `package.json` に持ちます。CLI の `cdk` は残ります)・root の uv(ubuntu の uv は残ります。 + `sudo uv` は使えなくなります)・ビルドで残っていた apt の一覧です。また apt で入れるパッケージの文書 + (`/usr/share/doc` の copyright のほか・man・info)と Node.js のヘッダ(`/usr/include/node`)を展開 + しなくなりました。この設定は base を `FROM` で継ぐ派生イメージにも効きます。ネイティブアドオンの + ビルドでは node-gyp がヘッダを取得します(ネットワークが要ります)。反映には `devbase build base` で + base を建て直し、派生イメージも建て直してください。 ### Fixed - **スナップショットの差分バックアップが、毎回ボリューム全体を控えていたのを直しました。** diff --git a/docs/developer/contributing.md b/docs/developer/contributing.md index a8d1cffd..caef264a 100644 --- a/docs/developer/contributing.md +++ b/docs/developer/contributing.md @@ -291,7 +291,7 @@ SHA-256 で照合して入れ、既定の水準(style まで)で走らせる ```bash docker run --rm -v "$PWD":/w -w /w --entrypoint shellcheck devbase-base:latest bin/devbase bin/rc install.sh docker run --rm -v "$PWD":/w -w /w --entrypoint shellcheck devbase-base:latest containers/base/ai-cli-aliases.sh \ - containers/base/dind containers/base/entrypoint.sh containers/base/shellrc-dir.sh \ + containers/base/entrypoint.sh containers/base/shellrc-dir.sh \ containers/base/tmux-clean containers/base/tmux-first containers/base/tmux-session ``` diff --git a/docs/specifications/base-image-shellcheck.md b/docs/specifications/base-image-shellcheck.md index 89d3fb29..6f85b965 100644 --- a/docs/specifications/base-image-shellcheck.md +++ b/docs/specifications/base-image-shellcheck.md @@ -39,7 +39,7 @@ bash-language-server などの言語サーバは Bash の診断を `shellcheck` `shellcheck` は Ubuntu の標準のアーカイブ(`universe`)にあり、外部のリポジトリを要さない。 そのため 1 つ目の `RUN` の**1 回目**の `apt-get install` の一覧に置く。2 回目の一覧は後から -足したリポジトリのパッケージ(`docker-ce` / `terraform` / `gh` / `nodejs`。amd64 では `google-chrome-stable` も) +足したリポジトリのパッケージ(`docker-ce-cli` / `terraform` / `gh` / `nodejs`。amd64 では `google-chrome-stable` も) のためにあり、標準のアーカイブのパッケージを混ぜない。 shellcheck のための `RUN` は立てない。立てると派生イメージ 7 つが積む層が 1 つ増え、 diff --git a/docs/specifications/ci-checks.md b/docs/specifications/ci-checks.md index f05dcc65..f9b8e995 100644 --- a/docs/specifications/ci-checks.md +++ b/docs/specifications/ci-checks.md @@ -161,7 +161,7 @@ on: - **`bin/` は glob(`bin/*`)で渡す。** 足したファイルも自動で検査に入り、シェルでないファイルが入れば その手順が失敗して気づける。`bin/` の中身は `devbase` と `rc` の 2 本で、どちらも Bash である - **`containers/base/` はファイル名を並べて渡す。** 何を検査したかが `ci.yml` とジョブのログだけで読める。 - `*.sh` の glob では拡張子の無い `dind` と `tmux-*` を拾えないため使わない。並べ忘れは pytest が止める + `*.sh` の glob では拡張子の無い `tmux-*` を拾えないため使わない。並べ忘れは pytest が止める (下の「検査の対象の漏れ」) - `ludeeus/action-shellcheck` は使わない @@ -171,7 +171,7 @@ on: | --- | --- | | `Run ShellCheck on bin/` | `bin/devbase`・`bin/rc` | | `Run ShellCheck on install.sh` | `install.sh` | -| `Run ShellCheck on containers/base/` | `containers/base/ai-cli-aliases.sh`・`dind`・`entrypoint.sh`・`shellrc-dir.sh`・`tmux-clean`・`tmux-first`・`tmux-session` | +| `Run ShellCheck on containers/base/` | `containers/base/ai-cli-aliases.sh`・`entrypoint.sh`・`shellrc-dir.sh`・`tmux-clean`・`tmux-first`・`tmux-session` | base のシェルスクリプトは、`containers/base/` の直下の項目のうち次のすべてを満たすものである。 @@ -206,7 +206,6 @@ base のシェルスクリプトは、`containers/base/` の直下の項目の | `bin/rc` | `source=/dev/null` | 補完の定義の読み込み | 補完の定義は `DEVBASE_ROOT` の下にあり、場所は実行時にしか決まらない | | `containers/base/entrypoint.sh` | `disable=SC2046` | `devbase_install_git_credentials` の `sudo install -m 600 -o $(id -u) -g $(id -g) ...` | `id` が失敗して空を出したとき、引用があると uutils の `install` が空の持ち主を「変えない」と読み root の持ち主で書くため、引用しない | | `containers/base/entrypoint.sh` | `disable=SC2317` | `DEVBASE_ENTRYPOINT_LIB_ONLY` のときの `return 0 2>/dev/null \|\| exit 0` | source したときは `return` で抜け、実行したときは `exit` へ進む。shellcheck は source を想定せず `exit` を届かないと読む | -| `containers/base/entrypoint.sh` | `disable=SC2009` | dockerd が起動しなかったときの `ps aux \| grep dockerd` | 診断として利用者と起動の引数を含む全列を出す。`pgrep` は PID(`-a` でも別の列)だけを出す | | `containers/base/shellrc-dir.sh` | `source=/dev/null` | 置き場所のファイルを `.` で読む行 | 読むのは利用者が置くファイルで、検査の時点では存在しない | | `containers/base/ai-cli-aliases.sh`・`shellrc-dir.sh` | `shell=bash` | 先頭 | 抑止ではない。bash が source するファイルで shebang を置かないため |