Mac環境でgRPCのC++コードをDockerつかって生成
🖼 背景
Protocol Buffers のコード生成だけなら Homebrew でインストール可能な protoc
コマンドさえあればいいが、
$ brew install protobuf $ protoc --version libprotoc 3.6.0
gRPC コードを生成するなら grpc_cpp_plugin
が必要となる。
$ which grpc_cpp_plugin /usr/local/bin/grpc_cpp_plugin
残念なことに Homebrew
や go get
ではインストールできず、Mac で make build
, make install
しなければならない。
WARNING: After installing with make install there is no easy way to uninstall, which can cause issues if you later want to remove the grpc and/or protobuf installation or upgrade to a newer version.
make
を使う方式だと公式ドキュメントにも「一度インストールしてしまえば削除するのは難しいからアップグレードのときとか困るよ」と書いてある…。
🌿 環境を汚さずに Docker でコード生成を行う
上のやり方を試したところ確かに C++ コードの生成はできたが、イケてないので Docker を使ってコード生成することにする。
前提として下記のようなディレクトリ構成にした。protobuf
ディレクトリにいろんな .proto
ファイルが配置されていて、codegen
ディレクトリに生成コードを出力。Dockerfile とスクリプトは tools
ディレクトリに置いた。
project ├── protobuf │ └── rpc │ ├── auth │ │ └── auth.proto │ ├── rpc.proto │ ├── setting │ │ └── language.proto │ └── user │ └── user.proto ├── codegen │ └── .gitkeep └── tools └── Dockerfile │ └── gen_grpc_cpp.sh
🐳 Dockerfile
gRPC 公式が C++ 用の Docker イメージを公開しているのでそれを使う。
docker run
すると bash
が起動するので protoc
と grpc_cpp_plugin
が使えることがわかる。
$ docker run -it --rm grpc/cxx root@55d8450a784e:/# protoc --version libprotoc 3.5.0 root@55d8450a784e:/# which grpc_cpp_plugin /usr/local/bin/grpc_cpp_plugin
この Docker イメージを利用した Docker ファイルが下記。
ホストの ./protobuf
ディレクトリをコンテナの /workspace/protobuf
ディレクトリへすべてコピーし、コンパイルを実行している。gRPC コードを生成する場合は --grpc_out
オプションを使う必要がある。
末尾で対象ファイルを指定しているが **/*.proto
で再帰的に配下のディレクトリすべてを対象に加えてほしいところだけど、そうは動作してくれないので、階層数に応じて複数ディレクトリを愚直に指定している。
🐚 tools/gen_grpc_cpp.sh
上記の Docker イメージを使うシェルスクリプトを書いた。流れとしては、
- Docker ビルド
- Docker 実行
- コンテナのファイルをホストにコピー
- 後始末
3番のコンテナ内のファイルをホスト(Mac)にコピーするには docker cp を使うしかないため、このシェルスクリプトが必要となる。もしこのコピーが Dockerfile に記述できるようになれば Docker 実行だけでコード生成&コピーできるようになるが、今の所できないようだ。
JavaScriptのオブジェクトリテラルの前後にスペースを含めるべきか否か
波括弧(curly brace)のオブジェクトリテラルを 1 行で書く場合の話。
const withSpace = { name: 'yamada', locale: 'JP' }; // 空白あり const withoutSpace = {name: 'yamada', locale: 'JP'}; // 空白なし
🤔 実は規定がない
どれを見ても『オブジェクトリテラルを 1 行で書く場合』の規定がない。
ただし、いろいろなオープンソースのコードを見る限りでは、スペースありで書くケースの方が多いようだ。
🦉 スペースなしの方が一貫性がある
配列リテラルとオブジェクトリテラルを比べた場合、複数行で書く場合はこうなって同じ形式が使えるけど
const array = [ 'apple', 'google', 'facebook', 'amazon', ]; const obj = { name: 'apple', ceo: 'timothy donald cook', };
配列リテラルを単一行で書く場合は前後にスペースを含めないので、オブジェクトリテラルもこれに合わせた方が一貫性があるという考え。
const array = ['apple', 'google', 'facebook', 'amazon']; const obj = {name: 'apple', ceo: 'timothy donald cook'};
📝他の言語の場合
🍷 Ruby
Ruby Style Guide にはハッシュリテラル(JavaScriptでいうオブジェクトリテラルのこと)の書き方について記述されているが、好きな方を使えというスタイル。
Ruby 界隈でも JavaScript と同じようにスペースありで記述されているケースが多い。しかし、Ruby にはブロック構文が存在する。
array = ['apple', 'google', 'facebook', 'amazon'] array.each do |name| p name end array.each { |name| p name }
ブロックを複数行で書く場合は do...end
で記述することが推奨されるが、単一行で書く場合は波括弧が推奨される。
ブロック構文とハッシュリテラルの明確な区別のため、スペースなしの方が良いと考える。
🐍 Python
Python の場合は明確に「空白を入れるな」と規定されている。
今回のブログ記事も Python の書き方を学んだときに確かにそっちの方が一貫性あるな、と感じたので他の言語でも採用することにした。
🦋 ES6 と TypeScript の普及で使う機会が増えた
import 文で分割代入(Destructuring assignment)構文が多用されるので、単一行のオブジェクトリテラルを使うケースがかなり増えた。この機会に一貫させてスッキリさせようと思う。
import {DateUtil} from 'luxon'; import {Get, Controller, UseInterceptors} from '@nestjs/common';
🐹 Prettier でコードフォーマットする
ESLint や TSLint に単一行オブジェクトリテラルに対して警告を出す機能はないが、Prettier にはコードフォーマットさせるための bracketSpacing
オプションが存在する。
.prettierrc
{ "singleQuote": true, "trailingComma": "all", "bracketSpacing": false }
$ yarn global add prettier $ prettier --write '**/*.ts'
DockerのHEALTHCHECKをwgetでやる
Docker には HEALTHCHECK というコンテナが正常稼働しているか確認する機能がある。
何をもって『正常』と判断するかはコンテナを稼働する人が指定する。Nginx みたいな Web サービスのコンテナの場合は公式サンプルにもある通りcurl
を死活監視のためのコマンドとして利用している。
curl -f http://localhost/ || exit 1
自コンテナの Web サーバからステータスコード 200 が返れば、シェルの終了コードも 0
になるので正常と判定されるし、ステータスコード 404 とかが返れば終了コードは 1 以上
となるので(curl の場合は 22
だった)exit 1
が実行されて終了コード 1
となり異常と判定される仕組み。
ヘルスチェック結果は docker ps
時に healthy
なのか unhealty
なのか表示される。
AWS の ECS でもこれを利用して『異常』の場合は既存のインスタンスを自動停止して新たなインスタンスに差し替えてくれたりする。
⛰ Alpine Linux の場合
wget
は最初から入ってるけど curl
は入ってない。別にいれればいいだけの話なんだけど、コンテナサイズを最小化することでしのぎを削ってる人達(?)からすれば耐え難いことかもしれない。大丈夫 wget
でも同じことができる。
🚝 Nginx を使ったテスト
お試し用の Dockerfile はこちら。
nginx:mainline-alpine
は Nginx 公式の Alpine Linux でつくられた最新安定版のコンテナ。
ビルドして起動すると curl
を指定しているため、コマンドが存在せず起動後に unhealthy
となる。
$ docker build -t nginx-health-check . $ docker run -it --rm nginx-health-check
起動直後
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7661b8c061fe nginx-health-check "nginx -g 'daemon of…" 4 seconds ago Up 2 seconds (health: starting) 80/tcp eager_lichterman
起動から 8 秒後
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7661b8c061fe nginx-health-check "nginx -g 'daemon of…" 10 seconds ago Up 8 seconds (unhealthy) 80/tcp eager_lichterman
🚀 wget に書き換える
wget -q -O - http://localhost/ || exit 1
にする。-q
は標準出力を出さない指定。-O -
はファイル出力先を - 指定することでなしにする。つまり標準出力にするという意味。
FROM nginx:mainline-alpine HEALTHCHECK --interval=5s --timeout=3s --start-period=5s --retries=1 \ CMD wget -q -O - http://localhost/ || exit 1 CMD ["nginx", "-g", "daemon off;"]
コンテナを改めてビルド
$ docker build -t nginx-health-check . $ docker run -it --rm nginx-health-check
今度は healthy
となる。
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES feba790fbc78 nginx-health-check "nginx -g 'daemon of…" 6 seconds ago Up 5 seconds (healthy) 80/tcp distracted_chandrasekhar