新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:Docker/今後の予定

第1章 Docker入門

Dockerの利用体系

アプリケーション開発の世界では,コードに問題があれば以前のコードに戻して開発を再開するのはごく普通なことである。
現代のアプリケーションはそのほとんどがアプリケーションのコードだけで動作するものではなく,バックエンドのDBやアプリケーションを実行するフレームワーク,OSの提供するライブラリなどの実行環境と密接に連携して動く。
Dockerfileで開発環境の構築を自動化しておけば,開発中のコードだけでなくアプリケーションを揮発・テストする環境そのものも,必要に応じて変更・巻き戻しすることが容易になる。

Dockerを活用したアプリケーションの開発・テストの流れは次のようになる。
まず,構成管理(ここではGitを使用する)上にアプリケーションのコードとDockerfileを保存しておく。
インフラ管理者がGitリポジトリ上のDockerfileを更新すると,イメージビルド用のサーバが自動的にイメージを作成し,完成したイメージはDockerレジストリに保存して公開される。
続いて,それぞれの開発者は自身の開発用マシンにこれらのイメージをダウンロードし,コンテナを起動することで開発環境を整える。
この部分はシェルスクリプトでまとめて自動化すると良い。例えばnode.js, MySQL, memcachedなどのコンテナがあったとすると,それらのイメージのダウンロードと起動,そしてMySQLに対して開発用のDBを作成する部分まで自動化しておくと,開発者は指定のシェルスクリプト(これもリポジトリに入れておく)を実行するだけで同一の開発環境を手に入れることができる。
開発者はGitからアプリケーションのコードを取得し,更新して再度Gitリポジトリに保存する。
このとき,Jenkinsなどのインテグレーションツールによって追加されたコードに問題がないかどうか,テストサーバでテストを行うが,テストを実施する環境にもDockerによってコンテナを用意する。開発者がダウンロードしたものと同じイメージをテストサーバにもダウンロードし,テスト用のコンテナを起動する=開発者と同じ環境でテストするので,環境の違いに起因する問題を回避できる。また,テストを実行するごとに新しいコンテナを起動するので,毎回クリーンな環境でテストを実行できる。
なお,テスト環境の構築には,アプリケーションのコードをコンテナ内に展開する,DBをアプリケーション用に初期化するなどの処理が必要である。(方法については第2章で)

Dockerfileで実行環境を管理する利点として,脆弱性対応などによる環境の変更が容易になるという点が挙げられる。
現代のアプリケーションは実行環境と密接に関係しているため,実行環境を変更したい場合はすべての開発用マシンとテスト環境で同じように環境を変更しなければならない。このため気軽に変更することができず,脆弱性があるとわかっていながら古いバージョンのDBを使い続けるといったような塩漬け環境が発生してしまうこともしばしばあった。
しかし,Dockerを活用することで実行環境を構成管理ツール下に置いて更新および取得が容易になり,最近のアプリケーション開発に求められている,サービス提供中も継続的に開発を続けて積極的にソフトウェアをバージョンアップしていくような開発も可能になる。

また,テスト環境だけでなく,サービス提供環境もコンテナで構築し,継続的にデプロイを行う継続的デリバリーの実現も容易になる。
新機能を追加したアプリケーションは最終的にテストサーバ上で結合テストを実施し,テストが完了した状態のコンテナをそのままサービス用イメージとして固めてDockerレジストリに保存する。このイメージをサービス提供用のサーバにダウンロードしてコンテナを起動することで,新バージョンによるサービスの提供が開始される。
しかし,この説明はかなり単純化されており,サービスの提供を継続したまま稼働中のアプリケーションを入れ替えるにはさまざまな工夫が必要になる。

Dockerが解決する課題

DockerはもともとdotCloud社のPaaSを実現する内部の仕組みとして開発され,今ではOSSとして利用者がプライベートなPaaS環境を構築するコンポーネントとして利用できるようになっている。
「独自なPaaS環境」にも色々な形態が考えられるが,大きくは継続的インテグレーションや継続的デリバリーを実現する環境と捉えることができる。

継続的インテグレーションはもともとソフトウェア品質の向上を目的と考えだされた開発プロセスで,ソフトウェアの開発中から常にテストを実施し,動作しない部分があれば即座にそれを発見・修正するようにすることで,後でまとめてテストする場合に比べて根本原因の発見・修正を容易にするといったものである。
ソフトウェアの開発が進むにつれて単体テストの数は増えていくので,新しいコードをリポジトリに保存する度にテストを自動実行する仕組みが必要になる。これを実現するのが,Jenkinsなどのインテグレーションツールである。

実際の開発で継続的インテグレーションを実現する上での技術的課題として,インテグレーションツールと連携する開発環境・テスト環境の維持管理という課題があった。
開発者によってはコードをコミットする前に自身の開発用PCで動作確認をしたいと思うだろうが,その際には開発用PCにテストサーバと同一の実行環境を用意する必要がある。
これまではこのような環境を用意するために,Vagrantなどのサーバ仮想化による仮想マシン環境を利用することがあったが,仮想マシンの起動・セットアップに時間が掛かる,仮想マシンの数に限界があるなどの課題があった。
Dockerであれば,コンテナの起動は仮想マシンに比べて早く,物理サーバ上で多数のコンテナも起動することができる。

また,継続的インテグレーションにはソフトウェアの品質向上だけでなく,デプロイの効率化といった目的もある。
これまでのウォーターフォール型開発では,要件定義からアプリケーションの完成まで数年掛かることもしばしばあるが,Webに関連する技術の進化の早さ・利用者の求める機能の変化に対応できず,要件定義したシステムがサービス提供時には時代遅れになってしまうという恐れがある。
そこで,開発する機能を小さくすることで要件定義からテストまでの期間を短縮し,完成したアプリケーションを継続的にサービス提供環境にデプロイしていくという継続的デリバリーという考え方が誕生した。
しかし,このプロセスの実現には,サービス提供環境へのデプロイという作業を大幅に効率化する必要がある。
継続的インテグレーションによってテスト環境へのアプリケーションの導入が自動化されていれば,そこでテストされた「確実に動作するアプリケーション」をそのままサービス提供環境に持ってこれれば良い。
ここでもDockerを活用し,結合テストが完了したコンテナをそのままイメージに固めてサービス提供用サーバに展開することで上記の継続的デリバリーを実現することができる。
とはいえ,継続的デリバリーにはまだ解決されるべき課題もある。例えば,データを保持するDBサーバはバージョンアップ後の環境に既存のデータを移行する必要がある。折衷案としては,サービス提供環境のDBについてはコンテナを使用せずに,頻繁に機能追加されるアプリケーション部分だけをDockerのコンテナで提供するといった方法も考えられる。

Dockerの基礎技術

Linuxコンテナ

Linuxが提供するコンテナ機能はDockerの開発が始まる以前から存在した。
いわゆるサーバ仮想化,例えばLinux KVMでは,ホストLinuxカーネルモジュールとして提供されるハイパーバイザの機能によって仮想マシンを作成する。
つまり,物理サーバの上にホストLinuxが乗っかり,その上にハイパーバイザが,そして仮想マシンとゲストOSが乗っかるという構造である。

一方,Linuxコンテナでは仮想マシンやゲストOSといった考え方はない。
Linuxに限らず,一般的なOS環境では,物理サーバ上にCPUやメモリなどを制御するカーネルが稼働し,その上に複数のユーザプロセスが稼働するユーザ空間が存在する。
Linuxマルチタスク型のOSなので,1台のサーバ上で複数のユーザプロセスを稼働させることができる。わざわざサーバ仮想化を利用しなくても,WebサーバとAPサーバ,DBサーバを同時に稼働させることが可能である。
ただし,同じLinux上で稼働するプロセスは,OSレベルの環境設定は基本的に全て共通になる。つまり,全てのプロセスから同じディスクとファイル群が見え,NICIPアドレスなどのネットワーク設定も同じであるので,例えばTCP80番ポートで接続を受けるWebサーバを複数同時に起動することはできない。
Linuxコンテナは,このような課題を解決する。Linuxカーネルが提供する機能によって,Linux上で稼働するプロセスを複数のグループに分割して,それぞれのグループごとに異なる環境設定を実現する。実行環境が分離された複数のユーザ空間(=コンテナ)を用意して,それぞれについて異なるディレクトリ構成やネットワーク設定を割り当てるようなことが可能になる。
また,稼働中のプロセスの一覧表となるプロセステーブルもコンテナごとに割り当てられる。つまり,特定のコンテナに属さない外部の環境(ホストのLinux)からはコンテナの内部を含め全てのプロセスが見えるが,コンテナ内部のプロセスからは同じコンテナ内のプロセスだけが見えるようになる。

ディスクイメージ管理機能

Linuxコンテナでは,コンテナごとに異なるディスク領域を割り当てることができる。これは以前から利用されているchrootとほぼ同じ技術で,ホストLinux上の特定ディレクトリがコンテナ内部でルートディレクトリとして認識される形になる。
ただし,Dockerの場合はホストLinuxのディレクトリをそのままコンテナに割り当てるわけではない。Dockerは独自のディスクイメージ管理機能を提供しており,事前に用意したディスクイメージの内容をホストLinuxにマウントした上で,それをコンテナのルートファイルシステムとして割り当てている。

ネットワーク管理機能

Dockerがコンテナに提供するネットワークの構成は,次のようになっている。
異なるコンテナのプロセスは,ホストLinux上に用意された仮想ブリッジdocker0を通じてネットワーク通信を行う。コンテナ内部と仮想ブリッジの間は,vethによって接続されている。
vethはLinuxが提供する機能で,仮想NICのペアを用意してそれらを直結した状態にするというものである。一方の仮想NICに送り込んだパケットは,そのままもう一方の仮想NICから出てくる。Dockerはコンテナごとにvethを用意し,一方の仮想NICをコンテナ内部に,もう一方をdocker0に接続する。コンテナ内部では仮想NICのデバイス名はeth0となり,コンテナ内部のプロセスはeth0を通じて仮想ブリッジを経由したネットワーク通信が可能となる。デフォルトでは仮想ブリッジには172.17.0.0/16というサブネットが割り当てられ,仮想ブリッジ自身にはIPアドレス172.17.42.1が割り当てられる。

Dockerがコンテナを起動すると,コンテナ内部の仮想NICには該当のサブネットからIPアドレスを自動的に割り当てる。
コンテナ内のプロセスが外部ネットワークと通信する際には,ホストLinuxiptablesよりIPマスカレードの処理が行われる。(IPマスカレード=NAPT。複数のプライベートアドレスを一つのグローバルアドレスに割り当てる。IPアドレスだけではどのプライベートアドレスから来たかわからないので,ポート番号と対応付けを行う)つまり,ホストLinuxの物理NICIPアドレスを代表アドレスとして外部ネットワークに接続することになる。
一方,外部ネットワークからコンテナ内のプロセスに接続する場合,ホストLinuxiptablesによってポートフォワーディングが行われる。コンテナを起動する際に8080番ポートから80番ポートに転送するといった指定を行うと,ホストLinuxの8080番ポートで受け取ったパケットがコンテナ内の80番ポートに転送されるようになる。つまり,外部ネットワークから接続する場合は,コンテナ内のIPアドレスは気にせずホストLinuxに対して接続すればよいことになる。複数のコンテナでApacheを起動するような場合も,コンテナごとに転送元のポート番号さえ分ければ,コンテナ内部のhttpdは全て同じ80番ポートを使用していても良いことになる。

CPUとメモリの制御

Dockerでは,それぞれのコンテナに割り当てるCPUとメモリの制御が可能である。CPUについては,コンテナ内のプロセスが利用可能なCPUコアを明示的に指定したり,CPU割当ての優先度を設定したりすることができる。

これらの制御はLinuxのcgroupsの機能で行われる。cgroupsは,Linux上に稼働するプロセス群をいくつかのグループに分けて,それぞれのグループに対してCPUやメモリの割当てを制御する機能である。Dockerでコンテナを起動すると,そのコンテナに対応するグループが用意されて,起動時のオプションに従ってCPU・メモリに対する割当ての設定を行う。コンテナ内で起動するプロセスは自動的にこのグループに所属することになる。

今後の予定

業務の忙しかったり試験があったりでだいぶサボってしまったので,そろそろお勉強を再開。
リハビリがてらこの辺を読んでいく予定。(Scalaはちょっとお休み…)

Dockerコンテナ実践検証(Think IT Books)

Dockerコンテナ実践検証(Think IT Books)