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編 第8回】Healthcheck・自動再起動・バックアップでDockerを運用する

0
Posted at

はじめに

前回はUptime KumaをDocker Composeで構築し、Flaskアプリを監視しました。

その中で、

  • Docker Network
  • External Network
  • Volume
  • Restart Policy
  • Raspberry Pi再起動後のContainer復旧

などを確認しました。

今回はさらに一歩進めて、

Dockerをどう運用するか

をテーマにします。

具体的には、

  • Healthcheck
  • depends_onとservice_healthy
  • Restart Policy
  • Imageの更新
  • Containerの再作成
  • Volumeのバックアップ
  • Volume削除後の復元

を実際に試します。

DockerではContainerがUpになっていても、アプリケーションが正常とは限りません。

また、VolumeがあるからといってBackupが不要になるわけでもありません。

今回はその違いを実験しながら確認していきます。


今回の構成

今回も、これまで作ってきたFlask + Redis環境を使います。

Webブラウザ
    |
    | :8082
    v
+---------+
|  Flask  |
+---------+
    |
    | redis:6379
    v
+---------+
|  Redis  |
+---------+
    |
    v
Named Volume
redis-data

FlaskへアクセスするたびにRedisのhitsが増える構成です。


ContainerがUpなら正常なのか?

まず現在の状態を確認します。

sudo docker compose ps

Containerが正常に起動していれば、

Up

と表示されます。

しかし、ここで疑問が出ます。

ContainerがUpなら、中のアプリケーションも正常なのか?

答えは必ずしもそうではありません。

ContainerのMain Processが動いていれば、Dockerから見るとContainerはrunningです。

しかし、

Flask Processは動いている
↓
Redisへ接続できない
↓
Webアプリとしては正常に動けない

という状態もあり得ます。

そこで使うのがHealthcheckです。


まず既存のHealthcheckを確認する

Uptime Kumaは、Image側ですでにHealthcheckが設定されていました。

確認します。

sudo docker inspect uptime-kuma

Healthcheck部分だけを見る場合は、

sudo docker inspect \
  -f '{{json .Config.Healthcheck}}' \
  uptime-kuma

と確認できます。

一方、FlaskとRedisには最初Healthcheckがありませんでした。

sudo docker inspect \
  -f '{{json .Config.Healthcheck}}' \
  docker-lab-web-1

結果は、

null

でした。

Redisも同様です。

つまり、

Containerが起動しているか

は分かっても、

中のサービスが正常に応答しているか

までは確認していない状態です。


FlaskへHealthcheckを追加する

Flaskのcompose.yamlへHealthcheckを追加します。

services:
  web:
    build: .
    ports:
      - "${WEB_PORT}:5000"
    depends_on:
      - redis
    restart: unless-stopped
    healthcheck:
      test:
        - CMD
        - python
        - -c
        - "import urllib.request; urllib.request.urlopen('http://localhost:5000/')"
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

今回はPython標準ライブラリのurllib.requestを使って、

http://localhost:5000/

へアクセスしています。

つまりDocker自身が定期的に、

FlaskはHTTPで応答しているか?

を確認します。


Healthcheckの各設定

今回設定した内容を見てみます。

interval: 30s

30秒ごとにHealthcheckを実行します。

timeout: 5s

5秒以内に結果が返らなければ失敗として扱います。

retries: 3

3回連続で失敗するとunhealthyになります。

start_period: 10s

Container起動直後はアプリケーションの準備に時間がかかる可能性があるため、10秒間の猶予を与えています。


Flaskがhealthyになった

設定を反映します。

sudo docker compose up -d

状態を確認します。

sudo docker compose ps

するとFlaskに、

healthy

が表示されました。

これで、

running

だけではなく、

HTTPで正常に応答できる

ところまでDockerが確認しています。


Healthcheckの詳細を見る

Healthcheckの結果はdocker inspectでも確認できます。

sudo docker inspect \
  -f '{{json .State.Health}}' \
  docker-lab-web-1

ここでは、

  • Status
  • FailingStreak
  • 各Healthcheckの実行結果
  • ExitCode

などを確認できます。

正常なら、

Status: healthy
FailingStreak: 0
ExitCode: 0

のような状態になります。


Redisを止めてみる

Healthcheckが本当に意味を持っているのか確認します。

Redisだけ停止します。

sudo docker compose stop redis

Flask Container自体はそのまま動いています。

再び確認します。

sudo docker compose ps

しばらく待つとFlaskが、

Up

のまま、

unhealthy

になりました。

ここが重要です。

Container
→ running

Flask Process
→ running

Redis
→ 停止

FlaskのHTTP応答
→ 正常に処理できない

Healthcheck
→ unhealthy

つまり、

runningとhealthyは別物

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


unhealthyになったら自動再起動するのか?

ここでもう一つ疑問が出ます。

Flaskには、

restart: unless-stopped

を設定しています。

それならunhealthyになったとき、自動で再起動するのでしょうか?

今回の実験では、FlaskはunhealthyになってもContainer自体は動き続けました。

つまり、

restart policy

と、

healthcheck

は役割が違います。

Restart Policyは、主にContainerのMain Processが終了した場合などに再起動を行います。

Healthcheckは、

このContainer内のサービスは正常か?

という状態をDockerへ知らせる仕組みです。

そのため、

unhealthy
=
自動的にContainerを再起動

ではありません。


Redisを再起動する

Redisを再び起動します。

sudo docker compose start redis

Redisが戻るとFlaskも再び正常に応答できるようになり、Healthcheckも、

healthy

へ戻りました。

Uptime Kuma側の監視も復旧しました。


RedisにもHealthcheckを追加する

次はRedis自身にもHealthcheckを追加します。

Redisには、

redis-cli ping

という確認方法があります。

Redisが正常なら、

PONG

と返ります。

compose.yamlへ追加します。

redis:
  image: redis:latest
  restart: unless-stopped
  healthcheck:
    test:
      - CMD
      - redis-cli
      - ping
    interval: 5s
    timeout: 3s
    retries: 5
  volumes:
    - redis-data:/data

これでDockerがRedisへ定期的にPINGを送り、応答を確認します。


depends_onだけでは準備完了を待たない

これまではFlask側で、

depends_on:
  - redis

としていました。

これはRedisより後にWeb Containerを起動するための依存関係として使えます。

しかし、

Redis Containerが起動した

ことと、

Redisが接続を受け付けられる状態になった

ことは同じではありません。

そこでservice_healthyを使います。


service_healthyを使う

Flask側の設定を変更します。

depends_on:
  redis:
    condition: service_healthy

Redis側には先ほどHealthcheckを追加しています。

最終的には次のようになります。

services:
  web:
    build: .
    ports:
      - "${WEB_PORT}:5000"
    depends_on:
      redis:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test:
        - CMD
        - python
        - -c
        - "import urllib.request; urllib.request.urlopen('http://localhost:5000/')"
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

  redis:
    image: redis:latest
    restart: unless-stopped
    healthcheck:
      test:
        - CMD
        - redis-cli
        - ping
      interval: 5s
      timeout: 3s
      retries: 5
    volumes:
      - redis-data:/data

volumes:
  redis-data:

起動順を確認する

一度停止します。

sudo docker compose down

再び起動します。

sudo docker compose up -d

すると、

Redis起動
↓
Redis Healthcheck
↓
healthy
↓
Flask起動
↓
Flask Healthcheck
↓
healthy

という流れになりました。

docker compose psでも、Redisが先にhealthyになり、その後Flaskが起動したことを確認できました。


service_healthyの注意点

ここで注意したいのは、

condition: service_healthy

がすべての起動場面を完全に制御してくれるわけではないという点です。

これは主に、

docker compose up

でComposeがサービスを起動するときの依存関係です。

一方、Raspberry Pi再起動後にDocker daemonが、

restart: unless-stopped

によってContainerを再起動する動作は、Composeがupを実行しているのと同じではありません。

つまり、

Composeによる起動制御

と、

Docker daemonによるRestart Policy

は分けて考えた方が分かりやすそうです。


Restart Policyを実機で確認する

Flask、Redis、Uptime Kumaには、

restart: unless-stopped

を設定しています。

そこでRaspberry Pi自体を再起動しました。

sudo reboot

再起動後、SSHで接続して確認します。

sudo docker ps

結果として、

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

が再び起動していました。

つまり、

Raspberry Pi再起動
↓
Docker daemon起動
↓
Restart Policy
↓
Container再起動

という動きを確認できました。

自宅サーバーではかなり重要な機能です。


Docker Imageを更新する

次はImageの更新を試します。

Redisでは、

image: redis:latest

を使っています。

まず現在のImageを確認します。

sudo docker images redis

次にRegistryから最新のImageを確認します。

sudo docker compose pull redis

今回は、すでに同じImageがローカルに存在していたため、実際には新しいImageへの更新は発生しませんでした。

ここで重要なのは、

pullした
=
Containerが自動で新しくなる

ではないことです。

ImageとContainerは別物です。


ImageとContainerは別

Dockerの考え方を整理すると、

Image
  ↓
Containerを作る

です。

Registryから新しいImageを取得しても、すでに動いているContainerが自動的に新しいImageへ入れ替わるとは限りません。

そのため更新時には、

Image取得
↓
Container再作成

という流れを考える必要があります。


Redis Containerを再作成する

まず現在のContainer IDを確認します。

sudo docker compose ps

または、

sudo docker ps

で確認できます。

その後、Redisだけ強制的に再作成します。

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

再びContainer IDを確認すると、RedisのContainer IDが変わっていました。

つまり、

古いRedis Container
↓
削除
↓
新しいRedis Container

になっています。


Containerを作り直してもCounterは残る

Redis Containerは新しくなりました。

しかしブラウザでFlaskへアクセスすると、これまでのCounterは残っていました。

なぜならRedisのデータはContainer内部ではなく、

volumes:
  - redis-data:/data

としてNamed Volumeへ保存しているからです。

旧Redis Container
       |
       v
redis-data Volume
       ^
       |
新Redis Container

Containerが変わっても、同じVolumeをMountすればデータを引き継げます。


Image更新では互換性にも注意する

ただし、

Volumeがある
↓
どんなバージョンへ更新しても安全

という意味ではありません。

Databaseなどでは、バージョンによってデータ形式や互換性が変わることがあります。

そのため実運用では、

  • Release Notes
  • Upgrade手順
  • Databaseの互換性
  • Backup

などを確認してから更新する必要があります。

今回の--force-recreateは、Container再作成とVolumeの関係を理解するための実験です。


VolumeがあればBackupはいらない?

ここまでVolumeによってデータが残ることを何度も確認しました。

では、

VolumeがあるならBackupはいらないのでは?

という疑問が出ます。

Volumeは、

Containerを削除してもデータを残す

ための仕組みです。

しかしVolumeそのものを削除すると、当然データはなくなります。

つまり、

Volume
=
永続化

Backup
=
消失したデータを復旧するためのコピー

で、役割が違います。

実際に試してみます。


RedisのBackupを作る

RedisにはRDB形式でデータを保存する仕組みがあります。

まずRedisへ明示的に保存させます。

sudo docker compose exec redis redis-cli SAVE

成功すると、

OK

と表示されます。

Redis Container内部では、

/data/dump.rdb

が作成されています。

確認します。

sudo docker compose exec redis ls -lh /data/dump.rdb

Raspberry Pi側へBackupをコピーする

Backup保存用ディレクトリを作ります。

mkdir -p ~/docker-backup

Container内のdump.rdbをホストへコピーします。

sudo docker cp \
  docker-lab-redis-1:/data/dump.rdb \
  ~/docker-backup/dump.rdb

確認します。

ls -lh ~/docker-backup/dump.rdb

これで、

Redis Volume

とは別に、

~/docker-backup/dump.rdb

というBackupができました。


Volumeを本当に削除する

ここからはデータ削除を伴う実験です。

Backupが作成できていることを確認したうえで、Compose環境とVolumeを削除します。

sudo docker compose down -v

-vを付けるとComposeで管理しているVolumeも削除されます。

これまで何度も使った、

sudo docker compose down

とは意味が違います。

docker compose down
→ ContainerやNetworkを削除
→ Named Volumeは残る

docker compose down -v
→ ContainerやNetworkを削除
→ Named Volumeも削除

となります。


再構築するとCounterがリセットされた

再び起動します。

sudo docker compose up -d

新しいredis-data Volumeが作成されます。

ブラウザからFlaskへアクセスすると、Counterが最初からになりました。

つまり、

Volume削除
↓
Redisデータ消失
↓
新しいVolume作成
↓
Counterリセット

となりました。

これで、

Containerを削除してもVolumeがあれば残る

と、

Volume自体を削除したらデータは消える

の違いを実際に確認できました。


BackupからRedisを復元する

次は先ほど保存した、

~/docker-backup/dump.rdb

から復元します。

Redisを停止します。

sudo docker compose stop redis

現在のRedis ContainerへBackupをコピーします。

sudo docker cp \
  ~/docker-backup/dump.rdb \
  docker-lab-redis-1:/data/dump.rdb

その後Redisを起動します。

sudo docker compose start redis

必要に応じてFlaskも再起動します。

sudo docker compose restart web

ブラウザから確認すると、削除前のCounterへ戻りました。

今回の環境では、Backup前の値である200回台のCounterが復元されました。

つまり、

Volume削除
↓
データ消失
↓
Backupからdump.rdbを戻す
↓
Redis起動
↓
Counter復元

まで実際に確認できました。


VolumeとBackupの違い

今回の実験で、この2つの違いがかなり分かりやすくなりました。

Volume

Container削除
↓
データは残る

用途は、

Containerとデータを分離する

ことです。

Backup

Volume削除
↓
データ消失
↓
Backupから復元

用途は、

元データが失われても復旧できるようにする

ことです。

つまり、

Volume != Backup

です。


同じRaspberry PiへのBackupで十分なのか?

今回は、

~/docker-backup/

へBackupを保存しました。

学習用としてはこれで十分ですが、実運用ではこれだけでは不十分です。

例えばmicroSDカード自体が故障すると、

Docker Volume

も、

~/docker-backup/

も同時に失われる可能性があります。

本当にBackupとして使うなら、

Raspberry Pi
   |
   +-- Docker Volume
   |
   +-- ローカルBackup
          |
          v
さらに別媒体

という構成を考える必要があります。

例えば、

  • USBストレージ
  • NAS
  • 別PC
  • クラウドストレージ

など、Raspberry Pi本体とは別の場所へコピーする方が安全です。


DatabaseのBackupはファイルコピーだけでよいのか?

今回Redisでは、

redis-cli SAVE

を実行してからdump.rdbをコピーしました。

これはRedis自身に整合性の取れたRDBファイルを作らせています。

Databaseによっては、動作中のデータファイルをそのままコピーすると整合性が取れない可能性があります。

例えば、

PostgreSQL
→ pg_dump

MySQL
→ mysqldump

など、Database自身が持つBackup機能を使うことが一般的です。

つまり、

Volumeのディレクトリをコピーすれば、どんなDatabaseでも安全にBackupできる

とは限りません。

Databaseごとの正しいBackup方法を使う必要があります。


今回の運用を整理する

ここまでの内容を整理すると、Docker運用にはいくつか違う仕組みがあります。

restart policy
    |
    +-- Containerが停止したときの再起動

healthcheck
    |
    +-- Serviceが正常か確認

depends_on + service_healthy
    |
    +-- Compose起動時に依存Serviceの準備を待つ

docker compose pull
    |
    +-- 新しいImageを取得

Container再作成
    |
    +-- 新しいImageや設定を反映

Volume
    |
    +-- Container削除後もデータを残す

Backup
    |
    +-- Volumeそのものを失っても復元できるようにする

それぞれ目的が違います。


「動いている」と「正常」は違う

今回特に印象に残ったのが、

running

と、

healthy

の違いです。

Redisを停止したとき、FlaskのContainer自体は動いていました。

しかしFlaskはRedisを必要としているため、アプリケーションとしては正常ではありません。

Container Process
→ running

Service
→ unhealthy

という状態があり得ます。

これはDockerを運用するときにかなり重要な考え方だと思います。


「永続化」と「Backup」も違う

もう一つ重要だったのが、

Volume

と、

Backup

の違いです。

VolumeがあればContainerを自由に作り直せます。

しかし、

docker compose down -v

のようにVolumeそのものを失えばデータは消えます。

そこでBackupが必要になります。

Container
→ Disposable

Volume
→ Persistent

Backup
→ Recoverable

と考えると分かりやすそうです。


今回理解できたこと

今回の実験では、

  • ContainerがrunningでもServiceが正常とは限らない
  • HealthcheckでServiceの状態を確認できる
  • unhealthyだけでは自動再起動しない
  • RedisにもHealthcheckを設定できる
  • depends_onとservice_healthyでCompose起動時の依存関係を制御できる
  • Restart PolicyでRaspberry Pi再起動後にContainerを復旧できる
  • ImageをPullしても既存Containerが自動で置き換わるとは限らない
  • Containerを再作成してもVolumeがあればデータは残る
  • VolumeとBackupは別物
  • BackupからVolumeのデータを復元できる

ことを確認できました。


まとめ

今回はDockerを「構築する」だけではなく、「運用する」という視点で実験しました。

特に重要だったのは、

Containerが起動していることと、サービスが正常であることは別

ということです。

Healthcheckを使うことで、

running

だけでは分からないServiceの状態を確認できました。

また、

VolumeはBackupではない

ということも実際にVolumeを削除して確認しました。

Containerを作り直せるようにするためにVolumeを使い、

Volumeそのものを失ったときに備えてBackupを取る。

この2つは別の役割です。

Dockerでは、

Containerは作り直せる
Imageも再取得できる
設定もComposeから再現できる
データはVolumeへ保存する
Volumeを失ったときはBackupから復旧する

という形に近づけるほど、環境を再構築しやすくなります。

次回はさらにセキュリティの視点を加えて、

  • .env
  • Containerへの環境変数
  • Docker Compose Secrets
  • RedisへのPassword設定
  • Port公開
  • 127.0.0.1へのBind
  • Firewall
  • DockerのNATとDOCKER-USER

を実際に確認します。

「設定値と秘密情報はどう分けるのか?」

「ContainerのPortはどこまで公開されているのか?」

をRaspberry Pi上で実験していきます。

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?