0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Docker編 第13回】CPU・Memory・Log・Networkを制御してContainerのリソースと通信範囲を管理する

0
Posted at

はじめに

前回は、.dockerignore、バージョン固定、non-root、Multi-stage buildを使ってDocker Imageを改善しました。

今回は、Containerを実際に運用するときに重要になる次の3つを試します。

  • CPU・Memoryの使用量を制限する
  • Docker Logが増え続けないようにローテーションする
  • Docker Networkをfrontend / backendに分離する

これまで作ってきた環境では、Flask、Redis、Caddy、Uptime Kumaが動いています。

今回はこの環境を、

CPU      → Flask Containerは最大0.5 CPU
Memory   → Flask Containerは最大128MiB
Log      → 1ファイル10MB、最大3ファイル
Network  → 必要なContainer同士だけ通信

という構成へ変更します。


今回の構成

最終的な通信経路は次のようにします。

                 HTTPS
Windows PC ──────────────→ Caddy
                              │
                    docker-lab_frontend
                              │
                              ▼
                             web
                              │
                    docker-lab_backend
                              │
                              ▼
                            Redis

ポイントは、Flaskのwebだけがfrontendとbackendの両方に参加することです。

これによって、

Caddy → Flask   OK
Flask → Redis   OK
Caddy → Redis   NG

という通信範囲を作ります。


1. CPUとMemoryを制限する

まずFlask ContainerにCPUとMemoryの制限を設定します。

compose.yamlwebに次を追加しました。

services:
  web:
    build: .
    cpus: "0.50"
    mem_limit: 128m

cpus: "0.50"は、Containerが利用できるCPU時間を約0.5 CPU分に制限します。

mem_limit: 128mは、Memory上限を128MiBに設定します。

設定を確認します。

sudo docker compose config

Composeによって次のように解釈されました。

cpus: 0.5
mem_limit: "134217728"

134217728 byteは128MiBです。


2. Raspberry PiでMemory制限が効かなかった

設定を反映してみます。

sudo docker compose up -d --force-recreate web

すると、次のWarningが出ました。

Your kernel does not support memory limit capabilities or the cgroup is not mounted. Limitation discarded.

docker inspectでも確認しました。

sudo docker inspect docker-lab-web-1 \
  --format 'NanoCpus={{.HostConfig.NanoCpus}} Memory={{.HostConfig.Memory}}'

結果は、

NanoCpus=500000000 Memory=0

でした。

CPU制限は設定されていますが、Memory制限は0です。

つまり、

CPU     → 設定成功
Memory  → 設定が破棄された

という状態でした。


3. cgroupを調べる

Dockerのリソース制限にはLinux Kernelのcgroupが使われます。

現在利用できるcgroup controllerを確認します。

cat /sys/fs/cgroup/cgroup.controllers

結果は、

cpuset cpu io pids

でした。

memoryがありません。

さらにKernelの起動パラメータを確認します。

cat /proc/cmdline

その中に、

cgroup_disable=memory

が入っていました。

つまり、このRaspberry PiではMemory cgroupが無効になっていました。


4. Memory cgroupを有効化する

Raspberry Pi OSではKernelの起動パラメータを/boot/firmware/cmdline.txtで設定できます。

念のためバックアップを作ります。

sudo cp /boot/firmware/cmdline.txt \
  /boot/firmware/cmdline.txt.before-memory-cgroup

cmdline.txtは基本的に1行で記述されるため、その行の末尾へ次を追加しました。

cgroup_memory=1 cgroup_enable=memory

今回は次のコマンドで追加しました。

sudo sed -i '1 s/$/ cgroup_memory=1 cgroup_enable=memory/' \
  /boot/firmware/cmdline.txt

その後、再起動します。

sudo reboot

再起動後に確認します。

cat /sys/fs/cgroup/cgroup.controllers

今度は、

cpuset cpu io memory pids

となりました。

memoryが追加されています。


5. Memory制限が有効になった

Flask Containerを再作成します。

cd ~/docker-lab

sudo docker compose up -d --force-recreate web

再び確認します。

sudo docker inspect docker-lab-web-1 \
  --format 'NanoCpus={{.HostConfig.NanoCpus}} Memory={{.HostConfig.Memory}}'

結果は、

NanoCpus=500000000 Memory=134217728

となりました。

これで、

CPU     0.5 CPU
Memory  128MiB

の両方が有効になりました。

docker statsでも確認できます。

sudo docker stats --no-stream

Flask Containerは次のようになりました。

docker-lab-web-1   0.02%   30.71MiB / 128MiB   23.99%

以前はMemory欄が0B / 0Bになっていましたが、Memory cgroupを有効化したことで正常に取得できるようになりました。


6. CPU制限を実際に試す

設定値を見るだけではなく、本当にCPUが制限されるか試してみます。

Flask Container内でCPUを使い続けるPythonプログラムを別Processとして起動しました。

sudo docker compose exec -d web \
  python -c 'while True: pass'

その状態で、

sudo docker stats --no-stream

を実行します。

結果、Flask ContainerのCPU使用率は、

50.00%

になりました。

cpus: "0.50"で設定した制限付近で頭打ちになっています。

Container内で動いているProcessも確認しました。

sudo docker top docker-lab-web-1

Flask本体に加えて、

python app.py
python -c while True: pass

が動いていました。

実験用Processを停止します。

今回はslim Image内にpkillが入っていなかったため、Host側からPIDを指定して終了しました。

sudo kill <PID>

再びdocker topを確認すると、

python app.py

だけに戻りました。

CPU制限は設定ファイル上だけではなく、実際に機能していることを確認できました。


7. Docker Logはどこに保存されるのか

次はLogです。

Flask ContainerのLogging DriverとLog Pathを確認しました。

sudo docker inspect docker-lab-web-1 \
  --format 'Driver={{.HostConfig.LogConfig.Type}} Path={{.LogPath}}'

Logging Driverは、

json-file

でした。

Dockerのjson-file Logging Driverでは、Containerの標準出力・標準エラー出力がHost側のLogファイルとして保存されます。

Logを放置すると、長期間の運用でファイルが大きくなる可能性があります。

そこでLog Rotationを設定します。


8. Log Rotationを設定する

compose.yamlwebへ次を追加しました。

logging:
  driver: "json-file"
  options:
    max-size: "10m"
    max-file: "3"

意味は、

max-size: 10m
    1つのLogファイルを最大10MB程度にする

max-file: 3
    最大3ファイルまで保持する

です。

設定を反映します。

sudo docker compose up -d --force-recreate web

docker inspectで確認します。

sudo docker inspect docker-lab-web-1 \
  --format '{{json .HostConfig.LogConfig}}'

結果は、

{"Type":"json-file","Config":{"max-file":"3","max-size":"10m"}}

となりました。


9. Log Rotationを実際に発生させる

本番用Flask Containerで10MBのLogを大量発生させる必要はありません。

そこで、実験用Containerを作り、小さい上限でLog Rotationを試しました。

sudo docker run -d \
  --name log-test \
  --log-driver json-file \
  --log-opt max-size=10k \
  --log-opt max-file=3 \
  busybox \
  sh -c 'i=0; while true; do echo "log-test line $i xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; i=$((i+1)); done'

このContainerでは、

1ファイル最大 10KB
最大3ファイル

にしています。

Logディレクトリを確認しました。

sudo ls -lh \
  /var/lib/docker/containers/$(sudo docker inspect -f '{{.Id}}' log-test)/

結果は、

9.7K  ...-json.log
9.8K  ...-json.log.1
9.8K  ...-json.log.2

となりました。

約10KBごとにLogファイルが切り替わり、3ファイルまで保持されています。

つまりLog Rotationが実際に機能していることを確認できました。

実験用Containerは削除します。

sudo docker rm -f log-test

Log RotationはBackupや集中Log管理ではありません。

古いLogを無限に残すのではなく、HostのDiskを圧迫しないように上限を設ける仕組みとして考えるのがよさそうです。


10. Docker Networkを分離する

次はNetworkです。

これまではdocker-lab_defaultという1つのNetworkを使っていました。

確認すると、

sudo docker network inspect docker-lab_default \
  --format '{{range $id, $c := .Containers}}{{$c.Name}}{{"\n"}}{{end}}'

次の4つが同じNetworkに参加していました。

caddy
uptime-kuma
docker-lab-redis-1
docker-lab-web-1

構造としては、

docker-lab_default
├── caddy
├── uptime-kuma
├── web
└── redis

です。

CaddyはFlaskへ接続する必要があります。

しかし同じNetworkにRedisも存在するため、CaddyとRedisまでNetworkを共有しています。

そこでNetworkを役割ごとに分離します。


11. frontendとbackendを作る

docker-labcompose.yamlで、webを2つのNetworkへ参加させます。

services:
  web:
    networks:
      - frontend
      - backend

Redisはbackendだけにします。

services:
  redis:
    networks:
      - backend

そしてNetworkを定義します。

networks:
  frontend:
    name: docker-lab_frontend

  backend:
    name: docker-lab_backend

これによって、

frontend
└── web

backend
├── web
└── redis

という構成になります。

Flaskは外側のCaddyとも内側のRedisとも通信する必要があるため、両方のNetworkへ参加します。

Redisはbackendだけです。


12. Caddyをfrontendへ接続する

Caddyは別のCompose Projectで管理しています。

Caddy側のcompose.yamlでは、外部NetworkとしてDocker LabのNetworkを参照しています。

以前は、

networks:
  docker-lab:
    external: true
    name: docker-lab_default

でした。

これを、

networks:
  docker-lab:
    external: true
    name: docker-lab_frontend

へ変更しました。

Caddy Service側では、

services:
  caddy:
    networks:
      - default
      - docker-lab
      - uptime-kuma

となっています。

Caddyを再作成します。

cd ~/caddy

sudo docker compose up -d --force-recreate caddy

参加Networkを確認します。

sudo docker inspect caddy \
  --format '{{range $name, $net := .NetworkSettings.Networks}}{{$name}}{{"\n"}}{{end}}'

結果は、

caddy_default
docker-lab_frontend
uptime-kuma_default

となりました。

Caddyはdocker-lab_backendには参加していません。


13. Caddy → Flaskを確認する

Windows PCから、

https://flask.home.arpa

へアクセスしました。

正常にFlaskページが表示され、アクセスカウンターも増加しました。

つまり、

Caddy → web

は正常です。

さらにカウンターがRedisに保存されているため、

web → Redis

も正常に通信できています。

Networkを分離しても、アプリケーションに必要な通信は維持できています。


14. Redisはbackendにしかいない

Redisの参加Networkを確認します。

sudo docker inspect docker-lab-redis-1 \
  --format '{{range $name, $net := .NetworkSettings.Networks}}{{$name}}{{"\n"}}{{end}}'

結果は、

docker-lab_backend

だけでした。

一方Caddyは、

caddy_default
docker-lab_frontend
uptime-kuma_default

です。

CaddyとRedisには共通するDocker Networkがありません。


15. frontendからRedisへ接続してみる

実際に通信できないことも確認します。

Caddy Imageにredis-cliが入っているとは限らないため、docker-lab_frontendだけに参加する一時Containerを使いました。

sudo docker run --rm \
  --network docker-lab_frontend \
  redis:latest \
  redis-cli -h redis -p 6379 ping

結果は、

Could not connect to Redis at redis:6379: Name or service not known

となりました。

これはdocker-lab_frontendではredisという名前を解決できないためです。

Redisはdocker-lab_backendにしか参加していません。

これで、

Caddy → Flask      OK
Flask → Redis      OK
frontend → Redis   NG

という構成を実際に確認できました。

Docker Networkを分けることで、必要なContainer同士だけが直接通信できる構成にできます。

ただし、Docker Networkを分けただけであらゆる通信経路を防げる万能なFirewallになるわけではありません。

今回のポイントは、Docker Bridge Network上で「どのContainer同士に直接通信させるか」を構成として明確にしたことです。


16. Uptime Kumaもfrontendへ移動する

Uptime KumaではFlaskを、

http://web:5000

で直接監視しています。

そのためUptime Kumaもdocker-lab_frontendへ参加させます。

Uptime Kuma側のComposeでは、

services:
  uptime-kuma:
    networks:
      - default
      - docker-lab

networks:
  docker-lab:
    external: true
    name: docker-lab_frontend

としました。

設定を反映します。

cd ~/uptime-kuma

sudo docker compose up -d --force-recreate uptime-kuma

確認します。

sudo docker inspect uptime-kuma \
  --format '{{range $name, $net := .NetworkSettings.Networks}}{{$name}}{{"\n"}}{{end}}'

結果は、

docker-lab_frontend
uptime-kuma_default

となりました。

これでUptime KumaからFlaskへの直接監視も維持できます。


17. 古いNetworkを削除する

移行前のdocker-lab_defaultを確認します。

sudo docker network inspect docker-lab_default \
  --format '{{range $id, $c := .Containers}}{{$c.Name}}{{"\n"}}{{end}}'

何も表示されませんでした。

つまり、もうこのNetworkを使っているContainerはいません。

削除します。

sudo docker network rm docker-lab_default

Network一覧を確認します。

sudo docker network ls | grep docker-lab

結果は、

docker-lab_backend
docker-lab_frontend

だけになりました。

古いNetworkを残さず、移行完了です。


18. 最終確認

最後にContainer一覧を確認しました。

sudo docker ps

結果、次の4つが起動しています。

uptime-kuma          healthy
caddy                Up
docker-lab-web-1     healthy
docker-lab-redis-1   healthy

Hostへ公開しているPortを見ると、Caddyだけが、

80
443

を公開しています。

Flask、Redis、Uptime KumaはHost側へPortを公開していません。

外部からの入口はCaddyにまとめつつ、内部ではDocker Networkを使って必要な通信だけを行う構成になりました。


最終的なdocker-labのcompose.yaml

今回の変更を含めると、Docker Lab側はおおよそ次の構成になりました。

services:
  web:
    build: .
    cpus: "0.50"
    mem_limit: 128m
    environment:
      APP_MESSAGE: "${APP_MESSAGE}"
    depends_on:
      redis:
        condition: service_healthy
    restart: unless-stopped
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test:
        - CMD
        - python
        - -c
        - "import urllib.request; urllib.request.urlopen('http://localhost:5000/')"
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    secrets:
      - redis_password
    networks:
      - frontend
      - backend

  redis:
    image: redis:latest
    restart: unless-stopped
    command:
      - sh
      - -c
      - 'redis-server --requirepass "$$(cat /run/secrets/redis_password)"'
    healthcheck:
      test:
        - CMD-SHELL
        - 'redis-cli -a "$$(cat /run/secrets/redis_password)" ping'
      interval: 5s
      timeout: 3s
      retries: 5
    volumes:
      - redis-data:/data
    secrets:
      - redis_password
    networks:
      - backend

volumes:
  redis-data:

secrets:
  redis_password:
    file: ./redis_password.txt

networks:
  frontend:
    name: docker-lab_frontend

  backend:
    name: docker-lab_backend

今回理解できたこと

今回の実験では、Containerを「動かす」だけではなく、「どこまで使わせるか」「どこまで通信させるか」を設定しました。

CPU制限

cpus: "0.50"

CPUを使い続けるProcessを動かしても、Container全体が約50%で頭打ちになることを確認できました。

Memory制限

mem_limit: 128m

Memory cgroupが無効だと設定そのものが破棄されることも分かりました。

docker inspectで、

Memory=134217728

となるところまで確認することが重要でした。

Log Rotation

logging:
  driver: "json-file"
  options:
    max-size: "10m"
    max-file: "3"

Logを無制限に増やすのではなく、サイズと世代数に上限を設けられます。

実験用Containerでは実際に、

-json.log
-json.log.1
-json.log.2

へローテーションするところまで確認できました。

Network分離

frontend
├── Caddy
├── Uptime Kuma
└── Flask

backend
├── Flask
└── Redis

とすることで、

Caddy → Flask      OK
Flask → Redis      OK
frontend → Redis   NG

という通信範囲を作れました。


Containerを動かすだけでは運用にならない

Dockerを触り始めた頃は、

docker run

でContainerが起動してWebページが表示されれば、それでDockerを使えているように感じました。

しかし実際に長期間動かすことを考えると、

CPUを使い切ったらどうする?
Memoryを使い切ったらどうする?
Logが何GBにもなったらどうする?
本来通信する必要がないContainer同士まで通信できてよい?

といった問題が出てきます。

今回設定した、

Resource Limit
Log Rotation
Network Segmentation

は、そうした「動いた後」の問題に対する設定でした。

Docker ComposeはContainerの起動手順を書くだけではなく、

どれだけResourceを使えるか
どのNetworkへ参加するか
どのContainerと通信できるか
Logをどう保持するか

までコードとして残せます。

ここまで来ると、Docker Composeが単なる「Containerをまとめて起動するコマンド」ではなく、アプリケーションの実行環境そのものを定義するファイルだということがかなり見えてきました。


次回

次回はDocker環境の更新と復旧を試します。

予定している内容は、

Imageの更新
Containerの再作成
Version変更
Healthcheckによる確認
問題が起きた場合のRollback
GitとBackupの役割
Raspberry PiやSD Cardが壊れた場合の復旧

です。

これまで作ってきたDocker環境を、

「壊れないようにする」のではなく、「壊れても再構築できるようにする」

という視点で整理していきます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?