2023/01/09

docker の permission denied (2)

docker で Permission denied が発生することがあるのはわかったけどよくわからんという記事を書いた。

hiro99ma blog: docker composeのvolumesでpermission deniedになったりならなかったりする
https://blog.hirokuma.work/2023/01/docker-composevolumespermission-denied.html

そしてファイルの削除については ファイルの権限よりも属しているディレクトリの権限の方が強そうだという記事も書いた。

hiro99ma blog: root権限のファイルもuserが削除できるのか
https://blog.hirokuma.work/2023/01/rootuser.html

そのとき確認した dockertest1 は docker-compose.yml で ./hello_dir/ を volumes で指定している。
docker compose up の実行前に hello_dir ディレクトリを作成していなかった場合、実行すると root.root で作成された。中のファイルも root.root である。
user 権限で hello_dir ディレクトリを作成(0755)していた場合も、特に問題なく中に root.root のファイルが作成される。
ディレクトリを user 権限かつ chmod 0000 にしていた場合も、問題なく root.root のファイルが作成されていた。
ディレクトリを 123.123 という適当な権限かつ chmod 0000 までしたが、それでも docker compose するとファイルが作成された。

 

今までのルールが通用しないのですが、なんなんだ docker ??


冷静になろう。
前回も user で rm をしてはいるが、それまでの設定は sudo などして root 権限で行っている。
つまり root 権限があればだいたいのことはできてしまうと思っていて良いだろう。

それに、そもそも私が docker で体験した Permission denied は書込みだったんだろうか?
なんとなくだが、作成していたテストデータを読み込めなかったとかだったような気がしてきた。

read も追加した dockertest1 を作った。
Dockerfile でコメントアウトしていた node ユーザに切り替えるところを有効にして、index.js でファイルを読むようにした。
docker compose build で作り直して docker compose up する。

$ docker compose up
[+] Running 1/1
 ⠿ Container docker-hello-1  Recreated                                                                   0.3s
Attaching to docker-hello-1
docker-hello-1  | [abc]: Hello, Docker!?
docker-hello-1  | [abc]: ReadMe!
docker-hello-1  |
docker-hello-1  | node:internal/fs/utils:344
docker-hello-1  |     throw err;
docker-hello-1  |     ^
docker-hello-1  |
docker-hello-1  | Error: EACCES: permission denied, open '/data/abc/test.txt'
docker-hello-1  |     at Object.openSync (node:fs:585:3)
docker-hello-1  |     at Object.writeFileSync (node:fs:2155:35)
docker-hello-1  |     at Object.<anonymous> (/usr/src/app/index.js:8:4)
docker-hello-1  |     at Module._compile (node:internal/modules/cjs/loader:1103:14)
docker-hello-1  |     at Object.Module._extensions..js (node:internal/modules/cjs/loader:1155:10)
docker-hello-1  |     at Module.load (node:internal/modules/cjs/loader:981:32)
docker-hello-1  |     at Function.Module._load (node:internal/modules/cjs/loader:822:12)
docker-hello-1  |     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
docker-hello-1  |     at node:internal/main/run_main_module:17:47 {
docker-hello-1  |   errno: -13,
docker-hello-1  |   syscall: 'open',
docker-hello-1  |   code: 'EACCES',
docker-hello-1  |   path: '/data/abc/test.txt'
docker-hello-1  | }
docker-hello-1 exited with code 1

うーん、読み込むファイル read.txt を置くためにディレクトリをあらかじめ作成していて、その権限がローカルユーザ(`id -u` == 1002 だった)になっていたので node ユーザでは書込みできなかったというところだろうか。

 

そもそも node ユーザってどういうユーザなのだ?
node:16.14.0-alpine3.14」であらかじめ準備されているユーザだと思うのだが(ちなみに vscode の dockerエクステンションを使うと node:lts-alpine だった)。
まあ alpine を使わずにもっと普通のディストリビューションを使えばいいやんという気もするが、組み込み Linux をやっていたので busybox は嫌いではないのだ。昔は newlibc とか使っていたような気がするけど今はどうなんだろうね。

addgroup しているのは 1000。
Linux をインストールして最初にユーザを作るとだいたい 1000 から始まるので、ローカルユーザがそういうアカウントであれば EACCES エラーにならなかったかもしれない。

この、アカウントというか id というかで変わってくるというのがやっかいなところだと思う。
Dockerfile でユーザを切り替えたとしても、今回で言えば git clone したユーザの id を使うようにしないと書き込みはできないだろう。

ちなみにこれが vscode のエクステンションを使って作ったデフォルトの Dockerfile だ。

FROM node:lts-alpine
ENV NODE_ENV=production
WORKDIR /usr/src/app
COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"]
RUN npm install --production --silent && mv node_modules ../
COPY . .
EXPOSE 3000
RUN chown -R node /usr/src/app
USER node
CMD ["npm", "start"]

node というユーザがある前提で chown したり USER で切り替えたりしている。
USER は docker run のオプション -u で上書きできるそうなので、それを使えばローカルユーザの id を使うことができると思う。あくまで USER はデフォルトのユーザを定義するだけらしい。

コンテナ起動時に -u オプションを使うと USER 命令を上書きできます。

ただ、USER を使っていない Dockerfile だとどうなるのだろう?
↑だと chown までが 1000 で行われて、USER のところだけ置き換えられる?? それだと意味が無いよなぁ。
でも chown コマンドがあるからそこで指定されたユーザも -u で置き換えて上げよう、と考えるのは難しいと思う。

Dockerfile を書くベスト・プラクティス — Docker-docs-ja 17.06.Beta ドキュメント
https://docs.docker.jp/engine/userguide/eng-image/dockerfile_best-practice.html#user

イメージ内で得られるユーザとグループは UID/GID に依存しないため、イメージの構築に関係なく次の UID/GID が割り当てられます。そのため、これが問題になるのであれば、UID/GID を明確に割り当ててください。

ここでいう UID/GID はコマンドを実行しているローカルユーザの id -u や id -g のことを指しているのだろう。

docker のバージョンが上がると説明が追加されていた。

Dockerfile のベストプラクティス — Docker-docs-ja 1.9.0b ドキュメント
https://docs.docker.jp/engine/articles/dockerfile_best-practice.html#user

サービスは特権ユーザで実行せずに、 USER を使えば非 root ユーザで実行できます。利用するには Dockerfile でユーザとグループを RUN groupadd -r postgres && useradd -r -g postgres postgres のように作成します。

記述はなくなっていても -u 自体はなくなってないと思うのだ。
どうなってるのだろうね?
日本語版が古い可能性もあるので、英語版も確認したが同じようだ。

Docker run reference | Docker Documentation
https://docs.docker.com/engine/reference/run/#user

  • デフォルトユーザは root (id = 0)
  • Dockerfile の USER でデフォルトユーザを指定することができる
  • docker run の -u(--user) オプションで USER を上書きできる

 

dockertest1 - tag:test2 で試そう。

docker compose を使わず、docker run を -u 無しで実行すると、これは↑と同じく EACCES エラーになる(カレントユーザの id -u が 1002 なので)。

$ docker build -t hello-test .
$ docker run -v "$PWD/hello_dir:/data" -it hello-test

-u でカレントユーザの UID:GID を指定するとエラーにならなかった。

$ docker run -u "`id -u`:`id -g`" -v "$PWD/hello_dir:/data" -it hello-test

これは Dockerfile の USER node がカレントユーザの UID/GID を使うようになったのだろうか?

image

/bin/sh でログインして確認すると、COPY したファイルや hello_dir/abc/* も含めて node.root になっていた。
whoami で 1002 と出ているから、alpine の node ユーザで動いているわけでもないようだ。

$ docker run -u "`id -u`:`id -g`" -v "$PWD/hello_dir:/data" -it hello-test /bin/sh
/usr/src/app $ ls -l
total 16
drwxrwxr-x    1 node     root          4096 Jan  8 10:35 hello_dir
-rw-r--r--    1 node     root           259 Jan  8 12:48 index.js
-rw-r--r--    1 node     root          1414 Jan  5 12:28 package-lock.json
-rw-r--r--    1 node     root           278 Jan  5 13:51 package.json
$ id -u
1002
$ id -g
1002
$ whoami
whoami: unknown uid 1002

となると、chown した node はそのまま nodeユーザのアカウントで実行され、その次の USER が docker run -u で上書きされたと考えてよさそうだ。

が・・・ hello_dir/abc/test.txt は node.root で生成されているのだ。 USER が上書きされているのであれば 1002.1002 になっているのではないだろうか?

USER の説明を見てみると current stage という説明がある。

The USER instruction sets the user name (or UID) and optionally the user group (or GID) to use as the default user and group for the remainder of the current stage.

ステージはビルドステージのことのようで FROM から始まるようだ。ということは・・・USER を書く位置はあまり関係ない?
だいたい、イメージのビルドとコンテナの実行への指示が同じ Dockerfile に書かれているのがわかりづらいと思うのだ。

Understand how CMD and ENTRYPOINT interact
https://docs.docker.com/engine/reference/builder/#understand-how-cmd-and-entrypoint-interact

  • CMD か ENTRYPOINT はどちらかは少なくとも必要。
  • コンテナを「実行可能」として定義するなら ENTRYPOINT が必要。
  • CMD は ENTRYPOINT のデフォルト引数

両方書くと ENTRYPOINT を並べた最後に CMD で書いたものが追加される。
ENTRYPOINT がない場合も同様だが、そのときは CMD が ENTRYPOINT の引数と等しくなるので CMD の内容が実行されたかのように見えるだろう。

  • ENTRYPOINT も CMD も、
    • 配列形式: シェル無しでそのまま実行される(たぶん exec系のシステムコールにそのまま渡される)
    • 単独形式: /bin/sh -c の引数になる
  • ENTRYPOINT が配列、CMD が配列
    • ENTRYPOINT 配列 + CMD 配列が実行される
  • ENTRYPOINT が配列、CMD が単独
    • ENTRYPOINT 配列 + /bin/sh -c CMD が実行される
  • ENTRYPOINT が単独、CMD が配列
    • /bin/sh -c ENTRYPOINT + CMD配列 が実行される
  • ENTRYPOINT が単独、CMD が単独
    • /bin/sh -c ENTRYPOINT + CMD が実行される

わかりづらい・・・。特に ENTRYPOINT が配列で CMD が単独の場合は後者に /bin/sh が付くので混乱しそうだ。
CMD は実行用というよりも ENTRYPOINT の引数として使った方がわかりやすいようにも思うが、名前が CMD だけにコマンドを書きたくなるな。

FROM で使った image の中に CMD が既に入っていた場合、image の中で使われていた CMD は空になるそうだ。
なので、FROM で使われる前提の image であれば ENTRYPOINT は環境を作るのに専念して(node では docker-entrypoint.sh を実行するようになっていた)、CMD はどうでもよい

そして CMD は別の引数で上書きされる(overridden when running the container with alternative arguments)というのは?
これは docker run しかあるまい。

Docker run リファレンス — Docker-docs-ja 20.10 ドキュメント
https://docs.docker.jp/engine/reference/run.html#dockerfile

以下が上書き可能なようだ。

  • CMD
  • ENTRYPOINT
  • EXPOSE
  • ENV
  • HEALTHCHECK
  • VOLUME
  • USER
  • WORKDIR

CMD の置き換えは、docker run の一番最後に付けるコマンドがそれに当たるらしい。
いつも実行中のコンテナにログインするときに /bin/sh を付けている。

フォアグラウンド
https://docs.docker.jp/engine/reference/run.html#id36

あまり気にしていなかったが、このときも ENTRYPOINT も実行されているのだろうか? あるいは ENTRYPOINT という名前の通り、ENTRY し終わったコンテナの場合には実行されないのだろうか。

ENTRYPOINT
https://docs.docker.com/engine/reference/builder/#entrypoint

dockertest1 の CMD を ENTRYPOINT に置き換えると、docker run ... /bin/sh としてもログインできずに ENTRYPOINT の中身が実行された。CMD のままだと /bin/sh でログインできる。

 

いかん、脱線しすぎた。
ともかく 1つの FROM に対して USER は 1つだけで、順番はあまり関係ないというか、ENTRYPOINT か CMD にしか影響を及ぼさないのだろう。docker run -u による上書きがマルチステージの Dockerfile だとどうなのかわからないが、最後のステージにしか影響しないんじゃないだろうか。

それと、docker compose の user は Dockerfile の上書きになるそうだ。つまり立ち位置としては docker run -u と同じということだろう。まあ、docker compose を使うと docker run -u は使えないからその代わりだと認識しておけば良いか。

https://docs.docker.jp/compose/compose-file/index.html#user

処理を root 権限のまま実行するというのはあまりやらないことだと思うので、Dockerfile には USER を書いておくのがよいと思う。

0 件のコメント:

コメントを投稿

コメントありがとうございます。
スパムかもしれない、と私が思ったら、
申し訳ないですが勝手に削除することもあります。

注: コメントを投稿できるのは、このブログのメンバーだけです。