はじめに
Dockerを未だに良く理解できていないモバイルゲーム系エンジニア(主にこれまでクライアントサイド、ついでに開発環境整備もやる人)が、書籍「たった1日で基本が身に付く! Docker/Kubernetes超入門」を読みつつDockerに入門してみました。その書評と個人的な覚書です。
入門の動機
ローカル環境・テスト環境・CI/CD環境(主にCI)の構築をモダンなやり方でできるようにしたいためです。
ゲーム開発初期段階ではUnityであればCloud Buildが利用できるため、すぐにビルド・単体テストの実行・およびチーム内にビルドしたアプリを共有することができます。これはこれで開発初期段階では確かに便利なのですが、実際の開発では
- メンバーがPull Requestを作成
- チームメンバーのレビュー
- マージ前もしくはマージ後に自動テストを実行
- 結果をSlackに通知
といったワークフローに加えChatOps連携などのワークフローも必要になることがこれまで多かったため、Cloud Buildだけではちょっときつい。また、CIまわりは過去に以下のような体験やチーム内で発生した問題がありました。
- Mac MiniやMac Proに直接Jenkinsをインストールして運用されており、マシンが壊れた際の復旧に時間がかかった
- 直接インストールして運用せざるを得ない場合は最低限MacのTime Machineバックアップを取ること
- ビルドにはマシンパワーが必要でプロジェクト進行具合によっては混み合いがちになり易い
- マシン台数を調整したいが物理マシンだとスケールし辛い
- マシンの構成管理ができていない場合マシンの環境がバラついてしまう
- マシンの導入時期によってもバラつく
- Ansibleで構成管理することで少しマシに
- Jenkinsのバージョンアップやプラグインの更新など頻繁に発生するため、メンテナンスコストが高い・属人的になり易い
- 気づいた頃には警告だらけになる
- 業務の隙をみて都度潰すしかなかった
- JenkinsのバージョンはLTS版を使う
- ジョブの管理はJenkinsfileで管理できるようになったおかげでいくらかマシになった
- 気づいた頃には警告だらけになる
- ChatOps連携するためにプラグイン的にcoffeeスクリプトでHubot連携する方法が採られた
- 簡単にChatOps連携するための当時の流行り
- 突然のcoffeeスクリプト!
- これを嫌ってごりっとGoでbotを書く現場もあって妙な共感を覚えたり
- 上記を踏まえチーム内にメンテナーを育成する必要がある
- レガシーな環境だと誰も近づきたくなくなる
- 総じて辛い体験(うっ...頭が
一方で近年GitHub ActionsやAmazon EC2 Macインスタンスの登場・CI/CDサービスの発展によって以前より選択肢が広がりました。このようなエコシステムに乗るためにもDockerに触れていると入っていきやすいように感じたため、重い腰を上げて入門することにしました。←動機
書評
紹介した書籍はタイトルに「たった1日で」と書いてありますが、気にしてはいけません。集中して1〜2週間程度、仕事の合間を縫って読むのであれば2〜3週間程度は見た方が良いです。
一方でDockerの基本からWebアプリケーション開発、AnsibleによるDockerホストの構築・Dockerを利用したJenkinsサーバー構築、k8sはおまけ程度といった塩梅で広く浅く解説されているため、さくっと全体感を掴むにはちょうど良さそうでした。
ただし、私が読んだのは2021/11であり2020/6頃の情報のためか、記載されている説明に不足感があったりサンプルが既に動作しない構成となっているようです。特に第6章のCI/CDのサンプルの実行については、私は断念しました(後述)。あくまでも全体感を掴む目的で、ハンズオンしつつサンプルを調整していく過程を通じて入門した気になれました。
各章の感想や注意
第1章 Dockerを使ってみよう
Docker Desktopの導入や概念的な説明と簡単なコマンド実行例なので、特に躓くことはなく読めました。
書籍の動作検証はWindows 10で行れたようです。私の試行環境はmacOS(Big Sur)でした。そのため、Docker Desktop for Mac(以下、DDfM)で試行したのですが、HyperKitやVPNKitといった仮想環境上で動作するため特有の制約があります。この制約を理解するには併せてDockerドキュメントの特にネットワーク構築機能 - 既知の制限、利用例、回避方法に予め目を通した方が良さそうです(ということに後で気づきました)。
第2章 イメージの利用と開発を体験しよう
Ansibleはsshで疎通できること前提である一方で利用するイメージであるCentOS7でsshdがそのままだとインストール・起動していないため、どうにかする必要がありました。そこで、推奨はされないものの下記のような方法で乗り切ることにしました。
-
openssh-server,openssh-clients(ssh-copy-idを使用するため)をインスール -
sshdを起動するためにsystemctlを使う- そのままだと「Failed to get D-Bus connection: Operation not permitted」となるため、
--privilegedオプションを付け、/sbin/init(systemdへのシンボリックリンク)経由で起動
- そのままだと「Failed to get D-Bus connection: Operation not permitted」となるため、
- playbookの修正
- CentOS7に
firewalldがインストールされていないため、そのままplaybookを実行するとエラーになる -
firewalldをyumモジュールでインストールするよう修正 - 面倒な場合は直接
yum -y install firewalld
- CentOS7に
第3章 Dockerネットワークとストレージを理解しよう
Dockerエンジンのネットワーク・名前解決やbindやvolumeの使い方の章です。
Dockerネットワークに関しては明示的にブリッジを作成してコンテナ起動時に指定すればコンテナ間で疎通することを確認できましたが、試しにデフォルトのブリッジ(bridge)に所属している場合でも確認したところ疎通できませんでした(何故だろう)。
第4章 Dockerfileでイメージを作成しよう
コマンドベースではなくDockerfileを使ったイメージのビルドに関する章です。
躓きポイントはありませんでした。
第5章 Composeを使ってマルチコンテナアプリを作ろう
第1章〜第4章までの知識を総動員したWebアプリ開発であれば一番実践的な章だと思います。
各章の要素がミックスされていて面白い章です。
躓きポイントはありませんでした。
第6章 DockerアプリでCI/CDしよう
一番気になっていた章だったのですが、結論から言えば断念しました。以下、躓いた点。
Jenkinsのイメージはjenkins/jenkinsで公開されている公式イメージのCentOS版を利用するサンプルとなっています。
Jenkinsのイメージから作成されるコンテナは/sbin/init(systemdへのシンボリックリンク)ではなく、/sbin/tiniで起動されるようです。これはゾンビプロセスの刈り取りとプロセスのSIGTERMなどのシグナルを正しく扱えるようにするためだそうです(Jenkinsで実行されるスクリプトがゾンビプロセスを作る可能性があり、コンテナ終了時にゾンビプロセスが残らないよう綺麗に終了するため。いわゆるPID 1問題)。PID 1に関する解説はされているものの/sbin/tiniの説明がされていないため、理解に苦しみました。
また、公式イメージでは当然sshdの相乗りなんてしておらず、sbin/tiniで起動することからJenkinsサーバーのホスト側でsystemdが起動しないため、第2章で乗り切った方法のようにopenssh-serverをインストールしてsshdは起動しようとしても上手くいきませんでした。
一方で説明やサンプルではJenkinsサーバーのホストでsshdが起動していることが前提となっています。そのため、サンプルを試そうとしても途中で手詰まりとなってしまいました。入門レベルである私の理解不足かもしれませんが、そうではない気もします。
また、Jenkins初回起動時に推奨プラグインのインストールが多数失敗しました。この点に関してはJenkinsのバージョンに起因していそうだったので、使用するjenkins/jenkinsのイメージのタグを調整すれば良さそうです。
書籍で使用されていたイメージのバージョン: jenkins/jenkins:2.190.1-centos
成功したイメージのバージョン: jenkins/jenkins:2.319-centos7(ltsの方が良いかも)
なお、Jenkinsコンテナのビルドをやり直したい場合はdocker-compose.yml内でvolumes指定がlocalになっているため、そのままコンテナを停止・再ビルドしてもJenkinsの初回起動画面になりません。当該volumeを消して作業をやり直す必要があることに注意。
私個人としてはJenkinsに頼らずCI/CDする方向性を模索していますが、Jenkins on Dockerをやりたいのであれば次のステップとして別の書籍やリソースを参考にした方が良さそうです。
第7章 Kubernetesを理解しよう
私の関心を満たすためにはDocker入門までで十分だったのですが、「Kubernetesも最近よく聞くけどサッパリ分からん...」と思っていたこともあり、せっかくなので触っておくことにしました。その方がインフラエンジニアとのコミュニケーションが捗るということを期待しつつ。
この章に関しては、minikubeをCentOS7上に展開する際には下記のような修正をする必要がありました。ローカルで十分試すことができるためリモートにインストールする必要はありませんが、下記のようなことをすればインストール自体はできたため記載しておきます。
-
minikubeインストール時に失敗しないようDocker Desktopの設定からメモリ割り当てを2GB以上に変更した上でrun時に-m 4gなどと指定- minkubeの動作環境として2GB以上のメモリが必要
- DDfMの場合デフォルト設定だと2GBとなっているが、オーバーヘッドのためか少し足りなくなってしまいエラーとなる
- playbookの調整
-
sshまわりの設定はマニュアルで設定するためコメントアウト -
conntrack,sudo,iprouteがインストールされるよう調整 -
minikubeのバージョン調整-
latestになっているところ、うまくいかなかったためv1.16.0などにダウングレード
-
-
client.crt,client.keyのパスが.minikube直下ではなくなったようなので./minikube/profiles/minikube以下に変更
上記のplaybookの調整をした上でmacOS側から下記を実行。
# Minikubeをホストするコンテナを特権モードで起動
docker run -it --name minikube_host --privileged -d -m 4g centos:centos7 /sbin/init
# サンプルを転送
# 送信元パスを./で終わらせることにより、ディレクトリ自体ではなくディレクトリ以下の内容をコピーする
docker container cp ansible/minikube/. minikube_host:/root/
立ち上がったコンテナ(ここではminikube_hostとします)にログインして下記を実行。
# ログイン
docker container exec -it minikube_host bash
# 必要なパッケージのインストール
yum -y install epel-release
yum -y install openssh-server openssh-clients ansible
# ssh-copy-id時にパスワードを聞かれるので適当に変更(もしくはsshd_configを調整する)
# ここでは簡単に済ませてるためrootのパスワードを変更
echo 'root:a' | chpasswd
# sshの準備
cd ~
mkdir .ssh && chmod 700 .ssh
mv ./id_rsa* .ssh && chmod 600 .ssh/id_rsa && chmod 644 .ssh/id_rsa.pub
systemctl start sshd
ssh-copy-id 127.0.0.1
# 後ははAnsibleに任せる
ansible-playbook -i 127.0.0.1, ./pb_centos7.yml
上記手順でCentOS7上にminikubeをインストールできたので続いてnginxのPod/Serviceを展開しましたが、コンテナのポートフォワーディングやminikube start時に指定するポートフォワーディングを試行しても、localhost:8080やlocalhost:30000等からアクセスできませんでした。DDfMの制約に引っかかっているのかもしれません。イマイチ理解できなかったため一旦スルーして、DDfM同梱のkubectlを使ってローカル環境で実行する方法に切り替えました。
第8章 Kubernetesを利用しよう
セクション1は特に躓くことなく試せました。
セクション2に関しては、些細な事ですがサンプルのHTML内記載のVersionやdocker-compose.ymlおよびapp.yml内のイメージのタグは予め2となっており、一方バージョンやタグが1想定で解説が開始されているため、少し混乱しました(途中で1=>2にする例があり、その結果がサンプルコード)。サンプルから改変して試行する場合はバージョンやタグを2から3へと読み替えるなどして試せば良いです。
コマンド実行後にブラウザを連打することでローリングアップデートの特徴であるアプリのバージョンが徐々に切り替わっていく様が観察できて面白いです。
また、このセクションの冒頭にあったブルー/グリーンデプロイメントなどにも触れるとあったものの、実際紹介されていたのはローリングアップデートであり、ブルー/グリーンデプロイメントに関しては「興味があれば調べてください」と締めくくられてしまっているのが残念な点でした。
自分がk8sを使うことは当分ないもののDevOps界隈を垣間見ることはできたのではないかと思います。
コマンド覚書
以下、コマンドや覚書。
イメージのタグ一覧をAPIで取得
Docker Hubでタグを見れば良いが、コマンドラインでタグ一覧を取得した場合はRegistry APIを利用して下記で取得できる。
curl -s https://registry.hub.docker.com/v2/repositories/{USER}/{IMAGE}/tags | jq -r '.results[].name'
JSONのパースのためjqで整形している。
Docker コンテナ作成・イメージ作成
ベースイメージから新規イメージを作成する一連の流れ。
ベースイメージからコンテナ作成
docker container run --rm --name {CONTAINER_NAME} -d {BASE_IMAGE_NAME} tail -f /dev/null
rmオプションを付けておくと、コンテナ終了時に削除までしてくれる。付けない場合明示的にコンテナを削除しないと同名で再度コンテナ作成することができないので面倒。
tail -f /dev/nullはコンテナが即終了しないよう待機させるため。この方法は良く使われるらしい。
コンテナに対してコマンド実行
docker container exec {CONTAINER_NAME} {COMMAND}
-
COMMANDpythonでflaskをインストールするのであればpip install flask==1.1.1など。
コンテナにファイル転送
docker container cp {LOCAL_PATH} {CONTAINER_NAME}:{REMOTE_PATH}
-
LOCAL_PATH転送したいファイルへのパス -
REMOTE_PATH転送先のパス。rootユーザーのホームディレクトリに転送する場合はexample_host:/root/といった具合。
コンテナの停止
docker container stop {CONTAINER_NAME}
イメージの作成
docker container commit {CONTAINER_NAME} {IMAGE_NAME}
イメージをDocker HubにPush
docker image push {IMAGE_NAME}:{TAG}
コンテナの動作確認
docker container run --name {CONTAINER_NAME} -p {LOCAL_PORT}:{REMOTE_PORT} -d -e {ENV_NAME_1}={ENV_VALUE_1} -e {ENV_NAME_2}={ENV_VALUE_2} {IMAGE_NAME} {COMMAND}
-
-pポートフォワーディング- 例:
-p 8080:80 - Dockerホスト側の8080ポートをコンテナ内の80ポートに紐付け)
- 例:
-
-e環境変数の設定。複数指定可能。 -
COMMANDコンテナ上で実行したいコマンド
コンテナにログイン
Alpine Linuxの場合は下記。
docker container exec -it {CONTAINER_NAME} ash
Alpine Linuxのデフォルトシェルはbashではなくash。
centosであればbashが使えるため下記。
docker container exec -it {CONTAINER_NAME} bash
あるいは、
docker container exec -it {CONTAINER_NAME} /bin/sh
Dockerホストにログイン
DDfMの場合、HyperKit/LinuxKit(VM)上でDockerホストが動作するらしく混乱した。
例えばログを探す際にdocker container inspect {CONTAINER_ID} | grep "LogPath"すると/var/lib/docker以下のパスが取得できたが、macOS上のターミナルからls /var/lib/dockerとしても存在しない。
そこで下記のようにすると、Dockerホストにログインできる。この場合、/var/lib/dockerのディレクトリが確認できる。
docker run -it --rm --privileged --pid=host justincormack/nsenter1
screenコマンドで入ることもできるらしいが、上記が簡単とのこと。
CentOS7上でssd使用する
centos7イメージでsystemctlが使用できない。
そのため、下記でsshdを起動することができない。
systemctl start sshd
privilegedオプションを付けて/sbin/init経由で起動する必要がある。
docker container run --name ansible_host -d --privileged centos:centos7 /sbin/init
privilegedオプションなしでsysytemdを使いたい場合は公式のSystemd integrationが参考になる。
ボリューム作成
docker volume create {VOLUME_NAME}
ボリュームのマウント
docker container run --rm -d --name {CONTAINER_NAME} --mount source={VOLUME_NAME},target={DST_VOLUME_PATH} {IMAGE_NAME}
--mount source=...,target=...がポイント。
ボリュームのバインド
docker container run --rm -d --name {CONTAINER_NAME} -p 8080:80 --mount type=bind,source={SRC_VOLUME_PATH},target={DST_VOLUME_PATH} {IMAGE_NAME}
readonlyにする場合はtargetの後に,readonlyなどとすれば良い。
ボリューム一覧確認
docker volume ls
ボリュームのインスペクト
docker volume inspect {VOLUME_NAME}
ボリュームの削除
docker volume rm {VOLUME_NAME}
Dockerfileからのビルド
docker image build -t {IMAGE_NAME:TAG_NAME} {DOCKERFILE_PATH}
DOCKERFILE_PATH Dockerfileが配置されているディレクトリへのパス
Docker Network
同一ネットワークであるコンテナであれば名前解決される。
名前解決の流れは自分自身のコンテナに問い合わせ、さらにDocker Engineに問い合わせる流れ(コンテナはネームサーバーを内包する)。
docker network create {NETWORK_NAME}
ネットワークの一覧
docker network ls
ネットワークの削除
docker network rm {NETWORK_NAME}
ネットワークを指定してコンテナ起動
docker container run --network {NETWORK_NAME}
Composeで指定する場合はdocker-compose.ymlのnetworkキーで指定する。
名前解決の確認
ネームサーバーは下記で確認できる。
docker container exec {CONTAINER_NAME} cat /etc/resolv.conf
名前解決できるかはnslookupを利用して下記で確認可能。
docker container exec {CONTAINER_NAME_1} nslookup {CONTAINER_NAME_2} {NAME_SERVER_IP_ADDRESS}
Docker Compose
複数のコンテナのオーケストレーションのためにはComposeを利用する。
k8sはとても複雑だが、Composeは比較的簡単。
docker-compose.ymlで構成を定義する。
docker-compose.ymlで変数を埋め込む場合は別途.envファイルを利用する。
ビルド
docker-compose build --no-cache
ビルド時間を短くするにはno-cacheオプションを付けなくても良いが、付けておいた方が安全。
起動
docker-compose up -d --build
-
-dバックグラウンド実行(デタッチ) -
--buildイメージをビルドして実行
変更を反映するには都度ビルドする必要がある。開発のイテレーション速度を上げるためにホットリロードするためにはymlでvolumesを指定してbindする方法がある。その場合は開発用と本番用のymlを切り替える。-fでymlの指定が可能。
ymlで定義されたサービスの確認
docker-compose ps
docker container lsでも確認できるが、複数のコンテナにまたがる操作はdocker-composeのコマンド体系を利用した方が良い。
サービスの停止・再開
docker-compose stop
stopで停止した場合は、下記で再開可能。
docker-compose start
再開を必要とせず、コンテナの破棄までしたい場合は下記。
docker-compose down
downするとネットワークも破棄もされる一方でボリュームは残ることに注意。
ボリュームが不要の場合は別途破棄する必要がある。
Kubernetes
リソースの種類とリソース名を指定することがdockerコマンドラインの感覚と違う点。通常マニフェストファイル(yml)で管理するが、コマンドは覚えておくと開発時に便利。
なお、マニフェストファイルはPod自体の構成するマニフェストファイル(kindがPod)と外部にサービスを公開するマニフェストファイル(kindがService)で構成する。
サービスの種類としては例えば下記がある。
| リソースの種類 | 用途 |
|---|---|
| ClusterIP | クラスタ内通信をするためのサービス |
| NodePort | クラスタ内通信+外部からの通信を受け付けるサービス |
| LoadBalancer | クラウドサービスで割り振られるIPと連携するサービス |
LoadBalancerは現状クラウドによっては使えないケースがあるらしい。
podの作成
kubectl run {POD_NAME} --generator=run-pod/v1 --hostport=8080 --port=80 --image=nginx
podの一覧
kubectl get pods
podのログ確認
kubectl logs {POD_NAME}
podの詳細確認
kubectl describe pods {POD_NAME}
kubectl get pods {POD_NAME} -o yaml
podでコマンドを実行
kubectl exec {POD_NAME} {COMMAND}
podにログイン
kubectl exec -it {POD_NAME} bash
コマンドに実行
kubectl exec {POD_NAME} hostname
podの削除
kubectl delete pod {POD_NAME}
リソースの種類の指定でpodsであったりpodであったりと揺らぎが気になったため、違いがあるのか調べたところ特に違いはないらしい。
- https://stackoverflow.com/questions/55904235/kubectl-pod-vs-pods
- https://kubernetes.io/docs/reference/kubectl/overview/#syntax
マニフェストファイルの適用
kubectl apply -f {FILE_PATH}
-fは複数指定可能。
リソースの削除
kubectl delete -f {FILE_PATH}
-fは複数指定可能。
レプリカセットの確認
kubectl get replicaset
リソースDeploymentを使用した際に指定したreplicas分だけ余分にpodが起動する。
障害発生等でpodの数が減った場合に自動的に望ましいpod数に調整される。
ただしDBをこれで水平展開してしまうと整合性が無くなるため、従来通りマネージドサービスを利用するか仕組みを作り込む必要がある。
ローリングアップデートの結果確認
kubectl rollout status deployment {RESOURCE_NAME}
ロールバック
kubectl rollout undo deployment {RESOURCE_NAME}
終わりに
Dockerに入門することで「Dockerは、サーバー・インフラエンジニアが使っている便利なやつ」という認識から一歩脱け出すことができました。一方privilegedオプションに頼りつつ学習を進めてしまいましたが、実務で適用するのは怖いところです。
また調査中に気づいたのですが、CIに関してはGameCIというサービスも出てきたようです(執筆時点でv2.0 alpha)。なんと「Free for everyone, forever」とのことらしいです。以前までDocker Hubにgableroux/unity3dイメージがありUnity Test Runnerを(Linuxビルドで)実行する風潮があったようなのですが、その後継の様子。Unity開発でGitHub Actionsを導入したいという要件や、自前でパイプライン構築するコストがかけられない場合はこのサービスを利用した方が幸せかもしれません。動向に要注目です。
fastlaneでのアプリ配信自動化までやりたい場合は@mogmetさんのこちらの記事が参考になりそうです。