KubernetesとIstioを使ったDX改善
Recruit Engineers Advent Calendar 2020
本記事は Recruit Engineers Advent Calendar 2020 17日目の記事です。
16日目の記事はsangotaroさんの
でした。
目次
- Recruit Engineers Advent Calendar 2020
- 目次
- 前置き
- KubernetesとIstioを使ったシステムのアーキテクチャ
- システムの機能を並行開発するときの悩み
- 目指した理想の開発環境の姿
- 自動化したこと
- Istio Gatewayの別の問題
- 今後やりたいこと
- まとめ
前置き
この記事では、KubernetesとIstioを利用したPR環境(プルリクエスト環境, プレビュー環境)の自動デプロイをするための基盤整備を通したDX改善(developer experience, 開発体験)の取り組みについてお話しします。
PR環境というのは「プルリクエストごとに用意されている個別環境」というようなニュアンスで、今回の取り組みでは「機能開発をしているfeatureブランチをremoteにpushした際に自動的に立ち上がるプレビュー用環境」のことを指しています。
何故このような仕組みを整備するに至ったのか、どのように実装されているのか、どのような問題にぶつかったのか、を背景などを踏まえて説明します。
話すこと
- Istioの一部のコンポーネントに関する説明
- 背景・経緯
- Kubernetes, Istioを使ってプレビュー環境の自動デプロイを実現する方法
話さないこと
- Docker, Kubernetesに関する説明
- Istio, サービスメッシュの全体に関する説明
- Kubernetes, Istioを採用している理由
- 完璧に動作するソースコード(参考としてのソースコードは掲載しますが、動作は保証しません)
KubernetesとIstioを使ったシステムのアーキテクチャ
下記のGCPの公式ドキュメントでは、Anthos Service Mesh(Istioベースのマネージドサービスメッシュ)とGoogle Cloud Load Balancingを組み合わせて、アプリケーションをGoogle Kubernetes Engine上でホスティングする方法について説明されています。
Kubernetes上にデプロイしたサービスをKubernetesのネットワーク外のHTTPクライアントからアクセスできるようにする場合、一般的にはk8s Service*1のNodePortやLoadBalancerを利用して外部に露出させたエンドポイントを通してHTTPクライアントからのアクセスを受け付けるようにします。
一方でIstioを併用する場合は、Kuberenetesクラスタ内にはIstio IngressGatewayという「クラスタの外部」と「クラスタの内部」の間に立つルーターのような役割をするコンポーネントが存在します。
Istio IngressGatewayは、k8s ServiceのNodePortやLoadBalancerなどの代わりにKuberenetesクラスタ外からのトラフィックを受けた上で、そのトラフィックをKuberenetesクラスタ内の適切なk8s Serviceに対して転送します。トラフィックの適切な転送先を判別するために、Istioは下で説明するIstio GatewayとIstio VirtualServiceという2つのリソースを利用します。
Istio Gateway
Istio IngressGatewayが受けたHTTPリクエストは、ホスト名, URL, User Agent, プロトコル, ポート番号, クエリパラメータ, その他HTTPヘッダーなどの情報を元に適切なIstio Gatewayに振り分けられます。
1つのIstio Gatewayには、リクエストの振り分けを行うための詳細な条件を1つ以上設定することができます。例えば、下の例のように example.com
と foobar.com
の2つのホスト名を設定すれば複数のホスト名に宛てたリクエストを1つのIstio Gatewayに集約することも可能です。
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: sample-gateway spec: servers: - port: number: 80 name: http protocol: HTTP hosts: - example.com - foobar.com
Istio VirtualService
Istio VirtualServiceは、Istio Gateway, ホスト名, k8s Serviceをもとにルーティング先を決める役割を持ちます。
以下の例では、
- Istio Gatway
sample-gateway
で受けた、 - ホスト名
example.com
,foobar.com
に宛てたHTTPリクエストを、 - k8s Service
nginx-service
の80番ポートにルーティングする。
といったような挙動をします。
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: sample-virtual-service spec: gateways: - sample-gateway hosts: - example.com - foobar.com http: - route: - destination: host: nginx-service.default.svc.cluster.local port: number: 80
システムの機能を並行開発するときの悩み
ある開発現場での話です。
この開発現場では、GitHubに対して変更をpushするとCI/CDのワーカーが自動でテストを実行し、Dockerイメージをビルドしてdev(開発)環境のDeploymentにローリングアップデートをかけるような仕組みが整備されています。
手元の変更をすぐにdev環境に反映できることによって、ローカルでのモックよりも更にprd(本番)環境に近い状況で簡単に動作検証をできるようになりました。また、実際に稼働しているKubernetesクラスタ上に変更が反映されると、エンジニア以外のメンバーに対して簡単に新しいUIや機能を触ってもらうようなことも可能になります。
今となってはCI/CDツールの普及によってこういった開発プロセスを円滑に進めるための仕組みも容易に導入できるようになり、この一連の仕組みやプロセスはGitOpsという言葉でCloudNative時代のDX向上を目指すものとして表現されています。
しかし今回の開発現場では、GitOpsを導入し容易にdev環境に対するデプロイができるようになったことにより、別の課題が発生してしまいました。
Bob「俺はこのイケてる新UIをdev環境にデプロイするぜ!」
Alice「私はこの最高に便利な新機能をdev環境にデプロイするわ!」
〜 5分後 〜
Bob「あれ、俺が開発した新UIがdev環境に反映されていないぞ?」
Alice「あっ、私が後追いでデプロイしたから上書きされちゃったのかも…!」
何がどうなったかは二人の会話を見れば一目瞭然です。
CDは同じdev環境に対するローリングアップデートを続けるので、BobとAliceがそれぞれ別々のブランチで作業をしているとBobの変更はAliceの変更によって上書きされてdev環境からは消えてしまうわけです。
これに対応するためには、例えばdev環境をナンバリングして2つ以上並べて用意しておくとか、事前に他のエンジニアと連携をとった上でdev環境にデプロイするタイミングを調整するとか色々な方法があるわけですが、これを更にスマートに解決してみようというのが今回の本題になります。
目指した理想の開発環境の姿
BobとAliceの例のように、五月雨式にそれぞれのブランチに変更を重ねたとしてもお互いの変更が上書きしないようにするためには、やはり環境自体が分離されている必要があります。
かといってブランチの数だけdev環境を複製しようものなら、(特にKubernetesを使っている場合には)莫大なパブリッククラウドの利用料金と果てしない手間がかかることは間違いないでしょう。
やはり理想は、
- ブランチの数だけ
- 動作確認に必要な最低限のコンポーネントが
- 自動的に作成/削除される
という状態です。
機能開発をしているブランチをGitHubにpushすると自動的にPR環境が立ち上がり、PullRequestがマージされてブランチが削除されると自動的にPR環境が削除される、といったようなことが実現できればGitOpsにおけるDXもかなり改善できるのではないかと考えました。
理想のPR環境環境を目指すにあたって、動作確認に必要な最低限のコンポーネントをどうやってデプロイするのかというのは、システム全体のアーキテクチャやネットワークの話も絡んできそうで少し難しい問題かに思えました。
しかし今回のようにKubernetesを使っている例では、
- リバースプロキシサーバー(必要であれば)
- フロントエンドサーバー
- バックエンド(API)サーバー
あたりをk8s Service + Deploymentのベーシックな構成で複製することができるなら、あとはロードバランサーから適切にルーティングさえできればOKです。
PR環境に割り当てられたホスト名からどのようにルーティングするかどうかについては、Istio GatewayとIstio VirtualServiceを設定すればIstio IngressGatewayが適切なk8s Serviceにルーティングをしてくれるでしょう。
そう、実はそんなに難しくないのです。
自動化したこと
今回の取り組みによって、以下のことが自動化されました。
- PR環境の立ち上げ
- PR環境の更新
- featureブランチに変更を加えてpushしたときに、立ち上げ時に作成したリソースを全てローリングアップデートして、変更がすぐに反映されるようにする。
- PR環境の削除
- GitHub上に存在するfeatureブランチの一覧と、Kubernetesクラスタ上にデプロイされているPR環境用のリソースを一覧で比較し、「featureブランチは存在しないけどPR環境用のリソースは存在する」という条件に合うPR環境のリソースを全て削除する。これをCronのような形で一日数回実行する。
fig2の図では、このPR環境を構成しているリソースを表しています。オレンジ色の部分がfeatureブランチの数だけ複製されて同じKubernetesクラスタ内にデプロイされるリソースです。
どう実現したのか、具体的なサンプルソースコードや注意点なども含めて説明していきます。
k8s Service, Deploymentを作成する
アプリケーションの本体を担うこの2つについて考えることは、そんなに多くありません。
同じnamespace内にデプロイするのであればk8s ServiceとDeploymentの metadata.name
さえ重複しないような仕組みになっていれば、dev環境に適用されているmanifestをそのままコピーして使えます。
以下は deployment.yaml
の例です。%FEATURE%
, %DOCKER_TAG%
というのは、CI/CD上で実行する直前に sed
コマンドなどでブランチ名と置き換えることを想定しています。(例: sed -e s/%FEATURE%/new-ui/g -e s/%DOCKER_TAG%/${DOCKER_TAG}/g ./deployment.yaml | kubectl apply -f - --record
)
apiVersion: apps/v1 kind: Deployment metadata: name: app-%FEATURE% namespace: default labels: component: app feature: "%FEATURE%" spec: replicas: 1 selector: matchLabels: name: app-%FEATURE% template: metadata: labels: name: app-%FEATURE% spec: restartPolicy: Always containers: - name: app-%FEATURE% image: asia.gcr.io/sample-project/app:%DOCKER_TAG% env: - name: PROJECT_ENV value: dev imagePullPolicy: Always livenessProbe: httpGet: path: /healthz port: 80 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 15 readinessProbe: httpGet: path: /healthz port: 80 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 15 ports: - containerPort: 80 protocol: TCP resources: limits: cpu: 200m memory: 400Mi requests: cpu: 100m memory: 200Mi
同様に、以下は service.yaml
の例です。こちらも特に気にせずDeploymentと同じようにほぼコピーで大丈夫ですが、k8s ServiceとDeploymentを正常に繋ぐために spec.selector.name
が動的に差し替えられていることに注意が必要です。
apiVersion: v1 kind: Service metadata: type: NodePort name: app-%FEATURE% namespace: default labels: component: app feature: "%FEATURE%" spec: ports: - name: http port: 80 protocol: TCP targetPort: 80 selector: name: app-%FEATURE%
Istio Gateway, Istio VirtualServiceを作成する
作成したk8s ServiceとDeploymentに対して正常にトラフィックをルーティングできるようにするために、Istio GatewayとIstio VirtualServiceのルールをそれぞれ用意する必要があります。
new-ui
ブランチでPR環境を立ち上げようとする場合を例にとると、
- Istio Gateway
new-ui.prenv.example.com
を受け付けるGatewaynew-ui-gateway
- Istio VirtualService
new-ui-gateway
で受け付けたnew-ui.prenv.example.com
をk8s Serviceapp-new-ui
にルーティングするVirtualService
の2つを作成することになります。
以下は gateway.yaml
と virtualservice.yaml
の例です。k8s Service, Deploymentのmanifestと同じように、適用時にsedコマンドなどでブランチ名を差し込んでやるようにしましょう。
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: %FEATURE%-gateway spec: servers: - port: number: 80 name: http protocol: HTTP hosts: - %FEATURE%.prenv.example.com
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: app-%FEATURE% namespace: istio-system labels: component: app feature: "%FEATURE%" spec: hosts: - "%FEATURE%.prenv.example.com" gateways: - %FEATURE%-gateway http: - route: - destination: port: number: 80 host: app-%FEATURE%.default.svc.cluster.local
ドメイン名のレコードをDNSサーバーに登録する
これは2つの方法があります。
1つは、ワイルドカードでDNSレコードを作成してしまうことです。 *.prenv.example.com
などでロードバランサーに向けたAレコードやCNAMEレコードを作成してしまえば、どのような名前のfeatureブランチが作成されたとしても prenv.example.com
の任意のサブドメインに対するアクセスをロードバランサーを経由してIstio IngressGatewayにルーティングできます。
ワイルドカードのDNSレコードが作成できるのであれば、この方法が一番手軽でよいでしょう。
もう1つは、ExternalDNSを利用して動的にDNSレコードの追加/削除をすることです。
ExternalDNSは、Kubernetesに対してサードパーティーのDNSサービスに対するアクセス権限を付与すると、k8s ServiceやIngressリソースにAnnotationを追加するだけで自動的にDNSサーバーに必要なレコードを作成してくれます。
以下はIstio GatewayとIstio VirtualServiceで設定されているホスト名を自動的にGoogle Cloud DNSに登録する deployment.yaml
の例です。RBAC(=Role-based access control)が有効化されているかどうかによってセットアップの方法は異なりますが、基本的に公式のチュートリアルに従って進めると正常に動作するようになります。
apiVersion: apps/v1 kind: Deployment metadata: name: external-dns spec: strategy: type: Recreate selector: matchLabels: app: external-dns template: metadata: labels: app: external-dns spec: serviceAccountName: external-dns containers: - name: external-dns image: registry.opensource.zalan.do/teapot/external-dns:latest args: - --log-level=debug - --source=service - --source=ingress - --source=istio-gateway - --source=istio-virtualservice - --domain-filter=prenv.example.com - --provider=google - --registry=txt - --txt-owner-id=prenv-external-dns
ただし、ExternalDNSを利用する場合はオンデマンドでDNSレコードを作成するため、一番最初にPR環境を立ち上げるときにはDNSレコードを作成してから疎通するようになるまで少し時間がかかってしまうのが難点です。
証明書を取得・更新する
PR環境はfeatureブランチの名前ごとにサブドメインを増殖させるので、証明書もそれに応じたものを用意してロードバランサーにアタッチする必要があります。オンデマンドで1つずつ取得していたのでは大変なので、ここではワイルドカード証明書を取得してそれを全てのPR環境で使い回すようにしました。本番環境では適切ではない方法かもしれませんが、これはdev環境なので気にすることではないでしょう。
GCPの場合でいうと、Google-managed SSL certificatesはワイルドカード証明書を取得することができません。そこで、PR環境ではcert-managerを利用することにしました。
cert-managerは、Let's Encrptでの証明書の取得から更新を全て自動で行ってくれる画期的なKubernetesアドオンです。公式ドキュメントのマニュアル通りにインストールとセットアップを進めると、HTTP-01やDNS-01チャレンジ方式で取得された証明書のキーペアがKubernetesのSecretsとして作成されます。あとはIstio IngressGatewayにマウントすればOKです。
この証明書の取得については、更新は確かに自動で行われますが取得自体を特に自動化する必要はなく、最初の一回だけ実行すればあとは放置しておくだけで大丈夫です。
apiVersion: cert-manager.io/v1alpha2 kind: Issuer metadata: name: letsencrypt-prod namespace: istio-system spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: user@example.com privateKeySecretRef: name: letsencrypt-prod solvers: - dns01: clouddns: project: sample-project serviceAccountSecretRef: name: cert-manager-credentials key: prenv-certmanager.json --- apiVersion: cert-manager.io/v1alpha2 kind: Certificate metadata: name: istio-gateway namespace: istio-system spec: secretName: istio-ingressgateway-certs-prenv issuerRef: name: letsencrypt-prod dnsNames: - "*.prenv.example.com" - "prenv.example.com"
環境を自動削除する
これに関しては、そこまで開発サイクルが激しくないのであれば手作業で必要がなくなったPR環境を削除してもOKですし、Kubernetes CronJobやArgo CronWorkflowなどを使って自動的にクリーンアップするような仕組みを作ってもよいでしょう。
今回この記事で紹介したmanifestには、全て共通してPR環境用のリソースであることを判別するためのlabelを設定してあります。これを使って以下のような流れで動くジョブを定期的に実行すれば、使われなくなったPR環境がKubernetesクラスタの計算資源を食いつぶすようなこともなくなるでしょう。
- GitHubのAPIを使用して、リモートリポジトリに存在するfeatureブランチを全て取得する。
kubectl
コマンドを使用して、labelでフィルタリングしてKubernetesクラスタ上に存在するPR環境のリソースを全て取得する。- 1と2で取得したfeatureブランチ名の集合の差分を計算し、削除するべきPR環境の一覧を取得する。
- 3で取得したPR環境を構成するリソース(k8s Service, Deployment, Istio Gateway, Istio VirtualService)を全て削除する。
Istio Gatewayの別の問題
さて、これらの仕組みを自動化したことによって、開発現場のBobとAliceはPR環境に各々の変更をデプロイして動作確認をできるようになりました。
Michael「2人とも、いいニュースだ!featureブランチをremoteにpushすると自動的にPR環境が立ち上がる仕組みを作ったぞ!」
Bob「本当かい!?じゃあ早速俺のnew-uiブランチをpushしてみるよ!」
Alice「わあ、すごい!私のnew-featブランチもそのPR環境で確認してみるわ!」
〜 5分後 〜
Bob「これはすごい。ブランチ名のサブドメインが勝手に切られて、そこに俺が開発した新UIがデプロイされているぞ!」
Alice「私の新機能もちゃんと動いているわ!見てちょうだい、Bob!」
Bob「どれどれ、 https://new-feat.prenv.example.com っと…。あれ?404エラーになるぞ?」
Alice「えっ?どうしてかしら…。私のブラウザでは正常にアクセスできるのに…。」
Alice「おかしいわ、私のブラウザだと逆にBobがデプロイした https://new-ui.prenv.example.com にアクセスできないみたい!」
Michael「何だって!?」
PR環境をハシゴできない
このPR環境の仕組みを作ってしばらくしてから、ある問題が発生していることに気が付きました。
それは、「複数のPR環境をハシゴしてアクセスしようとしたときに、最初にアクセスしたPR環境以外は全て404エラーになってしまう」という問題。
どの環境にアクセスができてどの環境にアクセスできなくなるかが完全に各人が最初にアクセスしたPR環境に依存しているということ、プライベートブラウズモードでアクセスし直すとアクセスできることから、恐らくブラウザキャッシュかセッション周りで何かよく分からないことが起きているという目星はつけてはいたのですが、詳しい原因についてはもう少し調査をする必要がありました。
404を返却しているHTTPレスポンスヘッダを見たところレスポンスの返却元がenvoyであることが分かったので、ロードバランサーからIstio VirtualServiceまでのどこかでこのエラーが返却されていると仮定し、唯一アクセスログを吐いているIstio IngressGatewayのenvoyのログを見てみることにしました。
[2020-12-07T07:56:51.762Z] \"GET / HTTP/2\" 404 NR \"-\" \"-\" 0 0 0 - \"xxx.xxx.xxx.xxx\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X ...)" \"xxxxx-xxxxx-xxxxx-xxxxx\" \"new-feat.prenv.example.com\" \"-\" - - xxx.xxx.xxx.xxx:80 xxx.xxx.xxx.xxx:1234 new-ui.prenv.example.com -\n
上記がそのアクセスログです。Istioの公式ドキュメントによると、 NR
というのは no route configured
を意味しており、Istio VirtualServiceの設定が適切な状態ではないことを示しています。また、ログの中に2つの違うホスト名が記録されている点も気になります。
ワイルドカード証明書は複数のIstio Gatewayとの相性が良くない
これまでに得た情報をもとに調査をしていると、調査に協力してくださっていた orisanoさん と int-ttさん から公式ドキュメント内のある情報を提供していただきました。Istioの公式ドキュメントには、このように書かれています。
404 errors occur when multiple gateways configured with same TLS certificate
Configuring more than one gateway using the same TLS certificate will cause browsers that leverage HTTP/2 connection reuse (i.e., most browsers) to produce 404 errors when accessing a second host after a connection to another host has already been established.
For example, let’s say you have 2 hosts that share the same TLS certificate like this:
- Wildcard certificate *.test.com installed in istio-ingressgateway
- Gateway configuration gw1 with host service1.test.com, selector istio: ingressgateway, and TLS using gateway’s mounted (wildcard) certificate
- Gateway configuration gw2 with host service2.test.com, selector istio: ingressgateway, and TLS using gateway’s mounted (wildcard) certificate
- VirtualService configuration vs1 with host service1.test.com and gateway gw1
- VirtualService configuration vs2 with host service2.test.com and gateway gw2
Since both gateways are served by the same workload (i.e., selector istio: ingressgateway) requests to both services (service1.test.com and service2.test.com) will resolve to the same IP. If service1.test.com is accessed first, it will return the wildcard certificate (*.test.com) indicating that connections to service2.test.com can use the same certificate. Browsers like Chrome and Firefox will consequently reuse the existing connection for requests to service2.test.com. Since the gateway (gw1) has no route for service2.test.com, it will then return a 404 (Not Found) response.
要約すると、
「同じTLS証明書を利用して2つ以上のIstio Gatewayを構成する場合に、2回目以降のアクセスが正しいIstio GatewayとIstio VirtualServiceにルーティングされずに404 Not Foundエラーが起きる」
ということです。
つまり、 new-ui.prenv.example.com
を受け付けるIstio Gateway new-ui-gateway
と、new-feat.prenv.example.com
を受け付けるIstio Gateway new-feat-gateway
があって、同じIstio IngressGatewayで同じワイルドカードのTLS証明書 *.prenv.example.com
を使っているようなケース。このようなケースにおいてHTTP/2のconnection reuseの仕様の影響で、 new-ui.prenv.example.com
にアクセスしたあとに new-feat.prenv.example.com
にアクセスしようとすると本来ルーティングされるべきIstio Gateway new-feat-gateway
ではなく、Istio Gateway new-ui-gateway
にルーティングされてしまってエラーとなってしまいます。
envoyのログに no route configured
のエラーが記録されていたのはそういうことで、 new-feat.prenv.example.com
にアクセスをしたのに Istio Gateway new-ui-gateway
にルーティングされてしまい、そこからルーティング可能なVirtualServiceには new-feat.prenv.example.com
に対応したk8s Serviceが設定されていなかったためにこのエラーが返却されてしまったということなのです。
解決方法
この問題を解消するためには、上記のドキュメント内で
You can avoid this problem by configuring a single wildcard Gateway, instead of two (gw1 and gw2). Then, simply bind both VirtualServices to it like this:
と書かれているように、
- 1つのIstio Gatewayでワイルドカードのドメインで受け付けるように設定する。
- 全てのIstio VirtualServiceをそのIstio Gatewayからルーティングするように設定する。
という2つの設定をすることで回避できます。
以下は回避手段を講じた gateway.yaml
の例です。注意する必要があるのは、上記で例示した gateway.yaml
は全てのfeatureブランチについて別個に適用する必要があったのに対し、以下の gateway.yaml
は1つのKuberenetesクラスタにつき一度だけ適用すればOKという点です。
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: sample-prenv-gateway spec: servers: - port: number: 80 name: http protocol: HTTP hosts: - *.prenv.example.com
Bob「Aliceの新機能が俺のブラウザでも見れるようになったよ!」
Alice「私のブラウザでもBobの作ったUIが見れたわ!」
今後やりたいこと
勘の良い方なら疑問に思ったかもしれません。
「あれ、データベースってどうするの?」
実は、ここはまだ取り組めていない部分です。現状はdev環境に存在する1つのデータベースに対して全てのPR環境から接続するような形になっています。
「どうやってデータベースを複製するのか?」「複製したときにかかるコストは?」「複製にかかる時間は?」
など壁も多いですが、データベースのスキーマ構造の変更に対しても耐えられる柔軟なPR環境の整備を次の目標にして改善活動に励んでいこうと思います。
まとめ
こういった取り組みは前例もあまりないことから、やってみようと足を踏み出すのは中々難しいかもしれません。
ですが今回このPR環境を整備したことによって、開発チーム内でのエンハンスのスピードは目に見えて向上したように感じます。ピーク時は10個近いPR環境が並行稼働していることもありました。
この記事を通して伝えたかったのは、一見難しいように思える仕組みづくりもエコシステムの恩恵を受けて簡単に実現できる可能性があること、その取り組みを実現して不の部分を解消したときにそのソリューションがスケールすることが分かっているならば、それは長期的に見ると大きな価値を生み出す可能性があるということです。
KubernetesとIstioという開発現場において採用事例のあまり多くなさそうなケースでの紹介となってしまいましたが、この仕組みを実現するための取り組みのエッセンスが読者の方の開発現場でも役に立つことを願っています。
18日目の記事はka2jun8さんが執筆予定です。乞うご期待!