はじめに
今回は、個人開発で直面した問題を通して、コンテナ化やKubernetesを導入するメリットを紹介します。
題材は、自宅の玄関の鍵を自動で開け閉めする「Autolock」というシステムです。
開発を始めたきっかけは、玄関で毎回物理鍵を探し、2箇所の鍵を開け閉めするのが煩わしかったことです。それなら自分でオートロックを作ってしまおうと考え、開発を始めました。
仕組みとしては、Raspberry Piでドアの開閉をセンサーで検知し、サーボモーターを制御して鍵の開け閉めを行うというものです。
当初はシンプルな構成でしたが、LINEからの遠隔操作やWebアプリ、ユーザー認証などの機能を追加するにつれて、プログラムの責務や依存関係が複雑になってきました。
そこで、機能ごとにコンテナを分割することにしました。しかし、今度は複数のコンテナで共有する設定情報をどう管理するか、という問題に直面しました。
この記事では、Autolockを例に、コンテナ化するメリットと、Kubernetesを導入するまでの経緯を紹介します。詳しい実装はGitHubに載せています。
なお、Autolock自体の開発経緯や機能については、以前Qiitaに投稿した記事で紹介しています。興味がある方はこちらもご覧ください。
1. Autolockの開発と機能追加
Autolockの開発を始めた当初は、非常にシンプルなシステムでした。
スマートフォンにMQTTというプロトコルでメッセージを送信できるアプリをインストールし、自宅のWi-Fiに接続した状態で特定のメッセージをラズベリーパイに送信して、オートロックを制御するというものです。
しかし、使っていくうちに、さまざまな機能を追加したくなりました。
- LINEから外出先でもオートロックを制御したい
- Webアプリから設定を変更したい
- ユーザー認証を導入したい
こうして機能を増やしていくと、ライブラリやプログラム同士の依存関係が複雑になり、ある機能を変更すると別の機能に影響が出る可能性も高くなります。また、機能によって適したプログラミング言語も異なり、PythonやC++など複数の言語を使う構成になっていきました。
このまま機能を追加し続けると、いずれ自分でも管理しきれなくなる。そう考え、システムの構成を見直すことにしました。
2. コンテナ化することで何がうれしいのか
そこで考えたのが、機能ごとにコンテナを分割することです。
例えば、Autolockでは次のように機能を分けられます。
- メインの制御プログラム: センサーの状態を確認し、サーボモーターなどを制御する
- Webアプリ: メインの制御設定や通知設定を変更する
- MQTTブローカー: MQTTメッセージを中継する
- 認証用データベース: Autolockを利用できるユーザーの情報を管理する
- Webhook処理プログラム: LINEやSlackなどの外部サービスからのWebhookを処理する
ここで、そもそもコンテナとは何でしょうか。
コンテナとは、アプリケーションと、その実行に必要なライブラリなどをまとめて動かすための仕組みです。Dockerは、コンテナイメージの作成やコンテナの実行などを行う代表的なソフトウェアです。
例えば、Webアプリを動かすには、プログラム本体だけでなく、実行環境やライブラリなども必要になります。これらをコンテナイメージとしてまとめておけば、環境ごとの差異を減らしながらアプリケーションを動かせます。
コンテナ化のメリットは、単にアプリケーションをDockerで動かせるようにすることだけではありません。
2.1 内部実装を隠蔽できる
コンテナを分割するメリットの一つは、機能同士の依存関係を整理できることです。
例えば、Webアプリがメインの制御プログラムと連携するとき、メインの制御プログラムの内部実装を知っている必要はありません。決められたインターフェース、つまりAPIや通信プロトコルを通じて連携できればよいわけです。
Autolockでは、オートロックの制御にMQTTというIoTでよく利用されるPub/Sub型のプロトコルを使っています。送信側が特定のトピックにメッセージを送信し、受信側がそのトピックを購読することで、プログラム同士が連携します。メッセージにはJSON形式のデータを利用しています。
例えば、メインの制御プログラムをC++からGoに書き直したとしても、MQTTのトピックやメッセージ形式などのインターフェースを維持できれば、Webアプリ側を変更せずに置き換えられます。
極端にいえば、Webアプリが決められたトピックに「鍵を開ける」という指示を送信できれば、その指示を受け取ったメインの制御プログラムが内部でどのように処理するかは、Webアプリ側が知る必要はありません。
もちろん、設定ファイルや通信プロトコルなどの互換性を維持する必要はあります。しかし、内部実装と外部とのインターフェースを分離することで、変更の影響範囲を小さくできます。
2.2 権限を分離できる
コンテナを分割することは、セキュリティの観点からも有効です。
例えば、設定ファイルを読み込むだけのコンテナには読み取り専用でマウントし、設定を編集するWebアプリだけに書き込み権限を与える、といった構成にできます。
また、コンテナごとに実行ユーザーやLinuxの権限、ネットワークへのアクセスなどを適切に制限することもできます。
一つの大きなプログラムにすべての機能を詰め込む場合と比べて、各機能に必要な権限だけを与える設計にしやすくなります。
ただし、コンテナを分割しただけで自動的に安全になるわけではありません。それぞれのコンテナにどのような権限を与えるか、適切に設計することが重要です。
2.3 機能ごとに開発・更新できる
機能をコンテナ単位で分離すれば、Webアプリを修正したときに、メインの制御プログラムまで作り直す必要はありません。
機能ごとにビルドやデプロイ、再起動などを管理できるため、特定の機能に集中して開発しやすくなります。
このように、コンテナ化することで機能の責務を分離し、変更の影響範囲を限定しやすくなります。
一方で、複数のコンテナが連携する以上、コンテナ間で共有する設定情報をどのように管理するか、という新たな問題が発生します。
3. コンテナ間で設定を共有したい
Autolockでは、メインの制御プログラムとWebアプリの両方で、同じ設定情報を参照する必要があります。
例えば、Webアプリから設定を変更した場合、その変更をメインの制御プログラムにも反映させなければなりません。
ここで考えられるのが、設定ファイルを複数のコンテナから共有する方法です。
Dockerでは、ホスト上のファイルやディレクトリをコンテナ内から参照できるようにする「マウント」という仕組みがあります。同じ設定ファイルを複数のコンテナにマウントすれば、ファイルを共有できます。
しかし、ファイルを共有するだけでは問題は解決しません。
設定ファイルが更新されても、アプリケーションがその変更を自動的に読み直すとは限らないからです。起動時にしか設定を読み込まないプログラムなら、設定を反映するために再読み込みや再起動が必要になります。
つまり、設定ファイルを共有する仕組みだけでなく、設定変更をどのように検知し、必要な処理を実行するかも考えなければなりません。
3.1 MQTTで変更を通知する
最初に考えたのは、MQTTを利用する方法です。
MQTTは、メッセージを送信する側と受信する側を分離し、メッセージを仲介するブローカーを通じて通信するプロトコルです。IoT機器の制御や状態通知などでよく利用されます。
既に開発初期段階から制御用メッセージのやり取りにMQTTを利用していました。そのため、設定変更の通知にもMQTTを利用することを考えました。
例えば、次のような流れです。
- Webアプリで設定を変更する
- WebアプリがMQTTで設定変更の通知を送信する
- メインの制御プログラムが通知を受け取る
- メインの制御プログラムが設定を読み直す、または再起動する
一見すると、シンプルな仕組みに見えます。
しかし、この方法ではコンテナを増やすたびに、MQTTライブラリのインストールや変更通知を受信する処理、設定を再読み込みする処理などを各プログラムに実装しなければなりません。
機能ごとに使用している言語も違うため、ライブラリの選定や実装方法を検討する必要があります。
また、通知が届かなかった場合や、アプリケーションの起動前に通知が送られた場合なども考慮しなければなりません。確実な受信確認を行うには、ACK(確認応答)を返す仕組みだけでなく、応答後の処理や、応答がない場合の対応も実装する必要があります。
せっかく機能を分離したのに、設定管理のための処理が各コンテナに入り込んでしまうため、長期的な保守を考えると複雑になってしまいます。
3.2 設定ファイルを監視して再起動する
もう一つ考えられるのは、設定ファイルの変更を監視し、変更されたら対象のコンテナを再起動する方法です。
各アプリケーションにMQTTの変更通知を受け取る処理を実装する必要がなくなるため、こちらのほうがシンプルに見えます。
ただし、Docker環境でコンテナの状態を監視し、必要に応じて再起動する仕組みを自前で構築するには、それなりの工夫が必要です。
例えば、監視用のプログラムからDockerデーモンを操作する方法が考えられます。Dockerデーモンは、コンテナの作成や起動、停止などを管理するバックグラウンドのプロセスです。
しかし、監視用コンテナからDockerソケットを通じてDockerデーモンを操作する場合、強い権限を与えることにつながります。また、コンテナの監視や再起動を行うための独自のプログラムも必要になります。
設定管理のためだけに、こうした仕組みを自分で実装して保守し続けるのは避けたいところです。
そこで、コンテナの起動や更新、障害時の復旧を管理する仕組みを利用できないかと考えました。
4. Kubernetesとは何か
ここで登場するのがKubernetesです。
Kubernetesは、コンテナ化されたアプリケーションを管理するためのプラットフォームです。一般的には「K8s」と略されます。
Dockerなどを利用すればコンテナを作成して起動できますが、複数のコンテナを継続的に運用するには、起動状態の監視や障害時の復旧、アプリケーションの更新なども管理する必要があります。
Kubernetesは、こうしたコンテナの管理に必要な機能を提供してくれます。複数のサーバーで構成することもでき、適切に設計すれば、障害への耐性を高めたり、更新時の停止時間を抑えたりできます。
例えば、次のような状況を考えてみます。
- コンテナが異常終了したら、自動的に復旧させたい
- アプリケーションを更新するときに、停止時間をできるだけ短くしたい
- 設定情報を複数のコンテナから参照できるようにしたい
これらをすべて自前で実装するのは大変です。
Kubernetesの大きな特徴は、利用者が「どのような状態にしたいか」を指定すると、その状態に近づけるように動作することです。
例えば、「Webアプリを2つ起動しておきたい」と指定しておけば、Kubernetesは実際に動作しているPodの数を確認し、1つしか動いていなければ、指定した状態に戻そうとします。
※維持したいPodの数を「レプリカ数」と呼びます。例えば、レプリカ数を2に指定すると、KubernetesはPodが2つ動作する状態を維持しようとします。
4.1 Pod:コンテナを管理する単位
Kubernetesでは、コンテナをPodという単位にまとめて管理します。
1つのPodには1つ以上のコンテナを配置できます。Podは、Kubernetesがコンテナを実行するために管理する最小の単位です。
例えば、Autolockのメインの制御プログラムをコンテナとして動かす場合、そのコンテナをPodに配置して実行します。
今回の構成では、基本的に1つのPodに1つのアプリケーションコンテナを配置しています。そのため、イメージをつかむ段階では Pod=コンテナ と考えても問題ありません。
ただし、Podとコンテナは厳密には別のものです。1つのPodに複数のコンテナを配置することもできるため、Kubernetesが管理する単位はあくまでPodだと覚えておくとよいでしょう。
4.2 Deployment:Podの構成や更新を管理する
Podを作成したら、それで終わりなのでしょうか。
例えば、Webアプリを2つのPodで動かしたい場合、Podを2つ作成するだけでは、片方が異常終了したときの復旧や、アプリケーション更新時の入れ替えも考えなければなりません。
そこで利用するのがDeploymentです。
Deploymentは、Podのテンプレートやレプリカ数、更新方法などを管理するKubernetesのリソースです。
例えば、WebアプリのDeploymentに「レプリカ数を2にする」と指定すると、KubernetesはWebアプリのPodが2つ動作する状態を維持しようとします。
また、Deploymentでは、コンテナイメージや環境変数、ボリュームのマウントなど、Podをどのような構成で起動するかも指定できます。
役割としてはDocker Composeにも共通する部分がありますが、DeploymentはKubernetesの仕組みと連携し、Podのレプリカ数の維持や更新を管理します。
アプリケーションの更新方法の一つが、ローリングアップデートです。
例えば、Webアプリを2つのPodで動かしているとします。適切な設定を行えば、新しいPodを起動して利用可能になったことを確認してから古いPodを停止する、といった順序で更新できます。
これにより、更新中も別のPodで処理を継続できるため、停止時間を抑えられます。
4.3 ConfigMap:設定情報を管理する
次に、コンテナ間で設定を共有する問題を考えてみます。
Kubernetesでは、アプリケーションの設定情報をConfigMapというリソースに保存できます。
ConfigMapは、アプリケーションの設定ファイルや環境変数など、機密情報ではない設定を管理するための仕組みです。
例えば、Autolockの設定値をConfigMapに保存し、メインの制御プログラムやWebアプリから参照できるようにします。
これにより、設定情報をコンテナイメージに直接埋め込まずに済みます。設定値を変更したい場合も、アプリケーションのイメージを作り直さずに設定情報を更新できます。また、ファイルではなくkubernetes内に保存されるため、複数サーバーをまたいでマウントすることができます。
ただし、ConfigMapを更新しただけでは、プロセスが自動的に設定を読み直さないため、再読み込みや再起動が必要です。
また、ConfigMapを更新しても、DeploymentのPodが自動的に再起動するわけではありません。
つまり、ConfigMapは設定情報を管理する仕組みであり、設定変更をきっかけにアプリケーションを更新する仕組みは、別途必要になります。
4.4 Secret:機密情報を管理する
Autolockでは、設定情報だけでなく、LINEやSlackのトークン、データベースの認証情報なども扱います。
これらは一般的な設定値とは異なり、外部に漏れてはいけない機密情報です。
Kubernetesでは、こうした情報をSecretというリソースに分けて管理することができ、Kubernetes内に暗号化された状態で保存されます。
ConfigMapとSecretを使い分けることで、通常の設定値と機密情報を分離できます。また、Secretを参照できる権限を適切に制限することもできます。
5. Kubernetesだけでは解決しない設定管理の課題
ここまで、Kubernetesが提供する主な仕組みを紹介しました。
- Pod: コンテナを実行する単位
- Deployment: Podの構成やレプリカ数、更新方法を管理する仕組み
- ConfigMap: 通常の設定情報を管理する仕組み
- Secret: 機密情報を管理する仕組み
これらを利用すれば、コンテナの起動や更新、設定情報の管理などをKubernetesの仕組みで簡単にすることができます。
しかし、Autolockの設定変更を例に考えると、まだ課題が残っています。
Webアプリで設定が変更されたら、その内容をConfigMapに反映し、変更の影響を受けるPodを更新する必要があります。
この一連の処理を手作業で行うこともできます。ConfigMapを更新したらPodを削除すればKubernetesが、自動で新しいPodを作ってくれるので設定の読み直しはできます。
しかし、設定項目や管理対象のコンポーネントが増えると、どのPodを更新すべきか判断するのと手動で毎回削除するのが難しくなります。
では、こうしたアプリケーション固有の管理処理も、Kubernetesの仕組みを利用して自動化できないのでしょうか。
そこで登場するのが、Kubernetes Operatorです。
次回の記事で、Autolockの設定管理を自動化するためにOperatorを自作した話と、その仕組みを紹介します。












