フューチャー技術ブログ

GoでDockerのAPIを叩いてみる

Future Tech Night #7で「GoでDockerのAPIを叩いてみる」という発表をしてきました。

他の登壇者のレポートはこちらです。

近年、コンテナの利用はますます増えています。実行環境としても、クラウドサービスでコンテナをホストするサービスは増えています。コンテナを動かすサービスもあれば、K8Sの利用も増えています。Kubernetesも最小のビルディングブロックはコンテナです。K8SのKnativeベースのGCP Cloud Runが僕の最近のお気に入りです。

Clloud Runロゴ

AWS Lambdaもコンテナを実行できるようになりました

実行環境だけではなく、開発環境としても必要不可欠なツールになってきています。OSやバージョンが違っても同じ環境を再現できます。データベースなどのミドルウェアもOSにインストールすることなく、プロジェクトごとに個別の環境を作るのも簡単になりました。

そのコンテナのデファクトとなっているのがDockerです。Dockerはコンテナのオールインワンツールで、コンテナのビルドから実行までできますし、複数のコンテナを連携して動かす機能(オーケストレーション)もdocker-composeコマンドで提供されています。

コンテナ自体はDockerだけに限定されるされるものではありません。ビルドはdocker build以外にも、Bazelでも、Buildpacksでも、作成する手段は他にもあります。実行する部分はOCI Runtime Specificationという規格があり、Dockerもその1つです。

とはいえDockerは便利です。WindowsでもmacでもLinuxでもインストーラで環境が整うので、Dockerの環境構築でトラブル、という例は聞いたことがありません。Docker Hubでさまざまなイメージが1コマンドでダウンロードして起動できるのも良いですし、何よりも情報が多いというメリットがあります。

M1 Macの互換性情報のメモをQiitaに公開したときも感想として一番多かったのが、「Dockerが動くなら買おうかな」というものでした。このエントリーではDockerをもっと活用する方法について紹介します。

Dockerの仕組み

Dockerをインストールして実際にサーバーなりを起動する場合、操作はdockerコマンドで行います。このdockerコマンドは単に命令を送るだけで、実態はWindowsなりLinuxなりmacOSで常駐プログラムとして実装されているサーバー(dockerd)が行います。

dockerdがdockerコマンドを受け取る図

Linuxはプロセスやファイルシステムを隔離してそのプロセスだけが動いているように見える状態で動きます。Linuxカーネルが持つ機能を使います。WindowsとmacはHyperVやHypervisor.FrameworkといったOSが持つ仮想PC機能を使い、Linuxを動かし、その中でLinuxカーネルの機能を使って動かします。コンテナごとに独立したOSが起動しているわけではなく、1つのLinuxの中で隔離機能を使って作った環境の中でそれぞれのプロセスが起動します。

コマンドからサーバーへのアクセスは、通常は/var/run/docker.sockというUnixドメインソケットを通じて動かします。これはHTTPサーバーがこのUDPの中で動いています。別ホストであればDOCKER_HOST環境変数を設定することでTCP/IPを使った連携ができます。

このサーバーにアクセスすると、コンテナを起動したり止めたりといったコンテナの操作ができます。また、このUDPはDockerのボリュームマウントを使ってコンテナの中に持ち込むことができます。そうすると、コンテナの中からあらゆるコンテナ操作ができる、特権コンテナのようなことも可能になります。

UDPベースのHTTPなので、curlコマンドでコンテナを操作できます。次のサンプルはhello-worldイメージを実行してそのログをコンソールに出力するコマンドです(CIDは最初のコマンド実行後に出力されるコンテナのIDが代入されているものとします)。

$ curl --unix-socket /var/run/docker.sock -H "Content-Type: application/json" \
-d '{"Image": "hello-world"}' \
-X POST http://localhost/containers/create

$ curl --unix-socket /var/run/docker.sock -X POST http://localhost/containers/${CID}/start

$ curl --unix-socket /var/run/docker.sock -X POST http://localhost/containers/${CID}/wait

$ curl --unix-socket /var/run/docker.sock "http://localhost/containers/${CID}/logs?stdout=1"

dockerコマンドなどはこの命令を自分で組み立てているわけではなく、GoやPythonのSDKを使って実装されています。このSDKを使うことでこれらの公式コマンドと同じことができます。

https://docs.docker.com/engine/api/sdk/

Docker Engine SDKsページのキャプチャ

Dockerのログビューアを作ってみる

近年のサービス開発では、オブザーバビリティが大事という機運が高まっています。ただ、数多くのSaaSなサービスはあるものの、手元で開発環境を用意するのは意外と面倒だったりします。オブザーバビリティでは次のような項目が技術要素として挙げられています。

  • 構造化ログ
  • トレーシング
  • メトリックス

オブザーバビリティではこれらのルールに従ったログを出すアプリケーションと、それを閲覧するビューアが二人三脚で必要となります。とりあえず先頭の要素を実現するものを実装してみます。ログ出力はJSONを出力する構造化ロガーが各言語にあったりしますので、そのあたりを使って行区切りのJSON(JSONL)として出力したものをパースして色付けして出力します。

このブログのフューチャーOSS推進タスクフォース始めますの記事の中で、ログビューアというものがこっそり書かれていましたが、それがこれにあたります。

構造ログビューアをやると書いているブログ記事

最終的に出来上がったコードがこれです。

https://gitlab.com/osaki-lab/secondsight

アーキテクチャはこんな感じです。

DockerログをオンメモリDBに保存、GoでWebAPIをたててgo:embedでHTML/JSを配信してするReactアプリ

Docker APIの利用

前述のドキュメントページのリファレンスなどをまず確認し、実行したい機能をまずは探します。dockerコマンドと必ずしも1:1になっているとは限らないので注意が必要です。ログビューアを作るには次のAPIを利用してみると良さそうです。

なお、Docker SDKを網羅するサンプルコードとして、dockerコマンド自身があります。コードなんかを探索するとパラメータや返り値の加工方法が一発で理解できます。ためしにContainerStats()などを検索してみてください。

https://github.com/docker/cli

完成したのが次のプログラムです。半年ぐらい前に作って放置していたものを、発表の一週間前ぐらいからいじり初めてGoのコードをゼロから作り直して、動くようにしてみました(ので実践投入はまだ)。壮大な構想のために複雑化していたところをバサッと切り捨ててシンプルにしました。

ログビューアの動作イメージ動画

なお、これの実装中に調べて書いたのが次のエントリーです。

それ以外の実装部分の理解の助けになるエントリーもいくつもあります。

他にも、サーバーレス連載などでDockerやCloud Runについて書いた記事も多数あります

軽く作ってみると、構造化ログが見えるのは便利です。色がつくとわかりやすいです。今後も、暇を見つけていろいろ機能を足していきたいです。フィルタや検索機能OpenCensus/OpenTelemetryのトレースログ、メトリクスの表示などです。ログ出力ライブラリによっていろいろクセがあったりするので、それぞれの出力をパースしてみやすくするのもいいですね。

まとめ

Dockerは今時な開発を支える重要なツールですが、Go SDKでいじってみるのは簡単でカスタマイズが可能です。実際にそれらを使うサンプルコードも紹介しました。クラウドサービスを使えば便利なものをローカルで気軽に実現するツールとかは作ってみるチャンスな予感(大手は投資しないだろうし)

今後、フューチャーでもいろいろOSSを作ったりしていきたいと思っています