upinetree's memo

Web系技術の話題とかたまに日常も。Qiita: http://qiita.com/upinetree

Docker-in-Docker で複数の docker-compose を同時に立ち上げる

課題設定

  • やりたいこと
    • project-a, project-b それぞれが docker-compose.yml を持っている状況
    • どちらも一緒に立ち上げて、協調動作させたい
    • リクエストは nginx で受けられると最高(最終的に https でつなげたい)
  • 条件
    • このための各プロジェクトの設定変更はなるべくしたくない
    • 各プロジェクトの変更で動かなくなるリスクが低い
    • 各プロジェクトの変更に追従するコストが低い

この課題に対し、Docker-in-Docker で解決を試みる!

前提プロジェクト

以下のように compose file が配置されていて、

$ tree
.
├── project-a
│   └── docker-compose.yml
└── project-b
     └── docker-compose.yml

サブディレクトリの compose file はこうなっているとする。

# project-a/docker-compose.yml
version: "3"

services:
  backend:
    build:
      context: ./
      dockerfile: backend/Dockerfile
    ports:
      - 3000:3000

  web:
    image: node:12
    volumes:
      - ./web:/app
    working_dir: /app
    command: yarn

# project-a/docker-compose.yml
version: "3"

services:
  web:
    image: docker/getting-started
    ports:
      - 80:80

project-a, project-b を同時に docker-compose で立ち上げたい。どうする?

  • a) それぞれ docker-compose を実行する
  • b) docker-compose -f で両方の compose file を指定して実行する

どちらの方法も、以下の問題がある。

そこで、第3の選択肢として Docker-in-Docker (DinD) を使った方法を試す。

Docker-in-Docker 用の compose file

実際に動作するサンプルリポジトリを作成した。

https://github.com/upinetree/dind-with-compose

このサンプルでは、トップディレクトリに DinD 用の compose file を配置している。ファイルツリーで表せば、

$ tree
.
├── docker-compose.yml
├── project-a
│   └── docker-compose.yml
└── project-b
     └── docker-compose.yml

のようになる。DinD 用の docker-compose.yml の内容はこんな感じ。

version: "3"

services:
  project-a-dockerd:
    image: docker:20-dind
    privileged: true
    volumes:
      - dind-certs-client-a:/certs/client
      - dind-data-a:/var/lib/docker
      - ./project-a:/project-a

  project-a-compose:
    image: docker/compose:latest
    environment:
      - DOCKER_HOST=tcp://docker:2376
      - DOCKER_TLS_VERIFY=1
      - DOCKER_CERT_PATH=/certs/client
    volumes:
      - dind-certs-client-a:/certs/client
      - ./project-a:/project-a
    working_dir: /project-a
    links:
      - project-a-dockerd:docker # dind の TLS 証明書の SAN に該当するようにエイリアス設定
    command: up

  project-b-dockerd:
  project-b-compose:
    # (snip) project-a-xxx と同様

  nginx:
    image: nginx
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    ports:
      - 80:80
      - 443:443

volumes:
  dind-data-a:
  dind-data-b:
  dind-certs-client-a:
  dind-certs-client-b:

docker-compose up すると、両方のプロジェクトのコンテナが立ち上がっていることが確認できる。

$ docker-compose up -d
$ docker-compose ps
$ docker-compose exec project-a-compose docker-compose ps
$ docker-compose exec project-b-compose docker-compose ps

※たまにコンテナが立ち上がるタイミングの都合で project-x-compose コンテナが異常終了していることがあるが、再度 docker-compose up -d すれば成功する。このタイミング問題の解決方法は後述

立ち上がった状態で http://a.project.localhost, http://b.project.localhost にアクセスすれば、nginx 経由でそれぞれレスポンスが返ってくるはず。

概略

以下、簡単のため project-a に絞って説明する。 project-a の設定のうち、関係性を把握するための最小限のパーツを抜粋するとこんな感じ。

services:
  project-a-dockerd:
    volumes:
      - ./project-a:/project-a

  project-a-compose:
    environment:
      - DOCKER_HOST=tcp://docker:2376
    volumes:
      - ./project-a:/project-a
    working_dir: /project-a
    links:
      - project-a-dockerd:docker

project-a を動かすために 2 つのサービスを用意している。

となっている。つまり、親となる dind コンテナの中で、サブディレクトリの compose file で定義される子コンテナたちが動作し、その動作の司令を compose コンテナから行うといった構図。

project-a-compose サービスでは DOCKER_HOSTtcp//docker:2376 を指定して、 docker-compose が参照する docker daemon がどこにあるかを教えている。

  • ホスト名 docker
  • ポート番号 2376
    • dind コンテナが docker デーモンを起動するデフォルトの番号

両コンテナには ./project-a ディレクトリをマウントしている。それぞれ project-a/docker-compose.yml で定義した子コンテナの立ち上げの過程で、次の目的で利用している。

  • project-a-dockerd では web 子コンテナが /app 上で yarn install するため
  • project-a-compose では backend 子コンテナの Dockerfile を参照するため

以上の概略を表した構成図がこちら(雑なメタモデリングなのはご容赦をば…)。

f:id:upinetree:20210221154607p:plain
dind-with-compose 概略図

dind の TLS 設定

docker:dind コンテナはデフォルトで TLS を利用することになる。以下、関わる設定を中心に説明する。

services:
  project-a-dockerd:
    volumes:
      - dind-certs-client-a:/certs/client

  project-a-compose:
    environment:
      - DOCKER_TLS_VERIFY=1
      - DOCKER_CERT_PATH=/certs/client
    volumes:
      - dind-certs-client-a:/certs/client

volumes:
  dind-certs-client-a:
  • TLS 証明書や鍵を project-a-compose に渡すため、/certs/client を volume にマウント
    • dind は TLS 証明書一式を DOCKER_TLS_CERTDIR に自動生成する
    • dind が親とする docker イメージは DOCKER_TLS_CERTDIR/certs指定する
    • クライアント用の証明書と鍵は /certs/client生成する
    • volume を定義するのではなく、ローカルディレクトリをマウントしてもよい
      • ホストから直接内部の docker を叩いて状況を確認したり実験したりするのに使える
  • project-a-compose の DOCKER_TLS_VERIFY, DOCKER_CERT_PATH は、docker-compose が dockerd を叩く際に、指定の鍵を使って TLS で接続するよということを教えている

開発環境にとっては正直面倒くさいし設定が複雑になるので、無効化しても良いかもしれない。と最初は思っていた。しかしこれはあまり良くないアイデアだった(後述)。

子コンテナのキャッシュ

volume の dind-data-a, dind-data-b は、 dind コンテナで動く docker イメージ等のキャッシュ用。親コンテナを作り直すたびに、小コンテナをダウンロードするところからやり直しになるのがつらいので作った。

project-a-dockerd:
  image: docker:20-dind
  volumes:
    - dind-data-a:/var/lib/docker

volume で定義したが、ホストにマウントして手軽にキャッシュ操作できるようにしてもよいかもしれない。キャッシュを削除するにはボリュームを削除すれば良い。

子コンテナのボリュームを削除したいなら、対象の compose コンテナから操作する。

docker-compose run --rm project-b-compose docker-compose down -v

これに限らず、子コンテナに対する操作を頻繁に行うようなら Makefileシェルスクリプトでコマンドを作成しておくと便利。

nginx 設定

# (snip)
http {
  server {
    listen      80;
    server_name a.project.localhost;
    access_log  /dev/stdout;

    location / {
      proxy_pass http://project-a-dockerd:3000;
      proxy_set_header Host $host;
    }
  }
    # (snip)
}

docker-compose が定義するデフォルトの network を使えば、proxy_pass は http://project-a-dockerd:3000 のように表せる。

dockerd の起動を待つ

状況によっては docker-compose の実行の前に dockerd が立ち上がっていないことがある。すると、以下のようなエラーとなって docker-compose が失敗する。

Couldn't connect to Docker daemon at https://docker:2376 - is it running?

dockerd が立ち上がってから再実行すれば問題なく起動できるのだが、高頻度で失敗するのが煩わしい場合がある。 解決策はシンプルで、dockerd が立ち上がってるのを確認できるまで docker-compose up を再実行すればよい。次のドキュメントが参考になる。

dockerize 等を使ってもよいが、まずは単純なスクリプトで十分だろう。

#!/bin/sh

set -e

cmd="$@"

until docker ps 1>/dev/null; do
  >&2 echo "Docker daemon is unavailable - sleeping"
  sleep 1
done

echo "Docker daemon is up - executing command"
exec $cmd
project-a-dockerd:
  volumes:
    - ./wait-for-dockerd.sh:/wait-for-dockerd.sh
  command:
    - /wait-for-dockerd.sh
    - docker-compose
    - up

dind が特権モードを要求 (privileged: true) することについて

ローカル開発環境では dind コンテナ自体が露出することはないのでリスクは低い認識でいる。考えられるリスクは…

  • 内部でさらに特権モードで野良イメージを動かしたときにホスト環境にいたずらされる可能性
  • dind のアップデートで脆弱性が生じる可能性

くらいだろうか(本当?)。

ローカルに閉じておらず外部に露出しているコンテナであれば、インジェクション系の脆弱性を付いた攻撃の対策は必要になりそうだ。

dind の rootless 版があるので、もしそれで手っ取り早く対策できるなら、やらないよりやるほうがいいだろう。

https://docs.docker.jp/engine/security/rootless.html#rootless-docker-in-docker

なお、利用にはまだ experimental フラグが必要。しかし docker-compose での指定方法がわからん…よってまだ試せていない。config ファイルに指定する方法があるようなので、それを compose コンテナに配置すれば良いだろうか。

試したが微妙だったこと

dockerd への TLS 接続の無効化

ローカル開発環境のような、外部に露出しておらず、侵入される危険性も小さい環境では、TLS の設定を無効化して compose file をシンプルにしたい気持ちが高まる。 TLS 接続を無効化するには、README の次の記述通りにやればできそうだ。

To disable this image behavior, simply override the container command or entrypoint to run dockerd directly

しかし dockerd を直接起動するためには、適切に --host オプションを与えてやる必要がある。

デフォルトの entrypoint である dockerd-entrypoint.sh は、 DOCKER_TLS_CERTDIR が空のときにいい感じに TLS disabled な dockerd を立ち上げてくれるようだ。やったね。

https://github.com/docker-library/docker/blob/master/20.10/dind/dockerd-entrypoint.sh#L127-L133

env に DOCKER_TLS_CERTDIR= を指定して無効化してみると、以下のような waring とともに dockerd の起動が遅くなった。

"Binding to an IP address without --tlsverify is deprecated. Startup is intentionally being slowed down to show this message" "Please consider generating tls certificates with client validation to prevent exposing unauthenticated root access to your network" "You can override this by explicitly specifying '--tls=false' or '--tlsverify=false'" "Support for listening on TCP without authentication or explicit intent to run without authentication will be removed in the next release"

このあたりの処理によるものだろう。

warning 通りに --tls=false すれば通常通り動くのだろうが、将来 binding できなくなる可能性が高いので、ここは素直に TLS にしておいたほうが良さそうだ。安全第一である。

ホストから子コンテナの docker-compose を実行する方法

以下のような compose file が配置されているとする。

# ./docker-compose.yml
version: "3"

services:
  project-a:
    image: docker:20.10.2-dind
    privileged: true
    tty: true
    ports:
      - 5301:2376 # docker (tls)
      - 9000:80 # nginx
    volumes:
      - ./certs/ca:/certs/ca
      - ./certs/client:/certs/client
# project-a/docker-compose.yml
version: "3"

services:
  nginx:
    image: nginx:latest
    ports:
      - 80:80

トップディレクトリで起動する DinD コンテナが親コンテナ。サブディレクトリで定義されているコンテナが、親コンテナの中で起動する子コンテナ。

この方法では、親コンテナが 5301:2376 でポートフォワーディングしているのを利用して、ホストのサブディレクトリから親コンテナに接続して docker-compose を実行する。

そのために、ホストで必要な環境変数を設定する。ここでは direnv を使う。

# ./project-a/.envrc
export DOCKER_HOST="tcp://127.0.0.1:5301"
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH="../certs/client"

親→子の順で docker-compose を起動していく。

$ docker-compose up -d
$ pushd project-a; docker-compose up -d; popd
$ curl http://127.0.0.1:9000

レスポンスが期待通りなら成功。ここまではうまく動くはず。

次に、ホストのディレクトリを子コンテナまでマウントしたいときを考える。そのために、ホストのディレクトリを親コンテナにマウントし、それを子コンテナにマウントすることになる。

# 親コンテナ (docker-compose.yml)
services:
  project-a:
    image: docker:20.10.2-dind
    volumes:
      - ./project-a:/project-a

# 子コンテナ (project-a/docker-compose.yml)
services:
  web:
    image: node:12
    volumes:
      - ./web:/app
    working_dir: /app

これで子コンテナを立ち上げると、以下の問題が発生する。

  • 中身が空の /app が出現する。つまりマウントに失敗している
  • 子コンテナに指定している ./ が、親コンテナ上のどこになるのかが不明

相対パスではなく絶対パス指定だとマウントできるようだ。しかし冒頭に示した条件のとおり、DinD のために子 compose file の内容を変更しないことが理想なので、絶対パス指定はしたくない…。

      volumes:
-       - ./web:/app
+       - /project-a/web:/app # マウントに成功するがやりたくない

子コンテナを立ち上げる際に --project-directory オプションを渡して、親コンテナ上のマウント先を指定すれば?

$ cd project-a
$ docker-compose --project-directory /project-a up -d

うまくいったように思われる。ところが、これがこの構成の限界。

さらに、子コンテナ用の Dockerfile が親コンテナのマウント対象に含まれている場合を考える。docker-compose で独自の Dockerfile をビルドするケースはよくあるはず。しかし --project-directory /project-a を指定していると、そのパス解決がうまくいかずビルドできなくなる。

# 子コンテナ (project-a/docker-compose.yml)
services:
  backend:
    build:
      context: ./
      dockerfile: backend/Dockerfile
    ports:
      - 9000:9000
$ cd project-a
$ docker-compose --project-directory /project-a up -d
ERROR: build path /project-a either does not exist, is not accessible, or is not a valid URL.

ビルド時の Dockerfile 参照はホストで行われているため、 --project-directory の指定がホストにとっては誤りとなるようだ(ソースコード読んだわけじゃないので確定ではないが、動作からおそらくそういうことだと思われる)。

つまり、マウント時のコンテキストとイメージ作成時のコンテキストが同じ状況じゃないと docker-compose up はできない。したがってこの構成で続行するためには、dind コンテナのディレクトリ構成をホストと同じにするか、その逆の状況を作らないといけない。

macOS, Ruby 3.0.0 の native extension build で '__declspec' attributes are not enabled と怒られて gem がインストールできないとき

環境

  • macOS 10.15.7 (Catalina)
  • ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin19]
  • Apple clang version 12.0.0 (clang-1200.0.32.28)

結論

-fdeclspec オプションをコンパイラに渡してビルドすればOK

$ CC='clang -fdeclspec' gem i byebug

なぜ

事象

Ruby 3.0.0 ではじめて byebug 入れようとしたときに以下のエラーが発生。

$ gem i byebug
Fetching byebug-11.1.3.gem
Building native extensions. This could take a while...
ERROR:  Error installing byebug:
        ERROR: Failed to build gem native extension.

    current directory: /Users/USERNAME/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/byebug-11.1.3/ext/byebug
/Users/USERNAME/.rbenv/versions/3.0.0/bin/ruby -I /Users/USERNAME/.rbenv/versions/3.0.0/lib/ruby/3.0.0 -r ./siteconf20210114-74414-sq50tn.rb extconf.rb
creating Makefile

current directory: /Users/USERNAME/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/byebug-11.1.3/ext/byebug
make "DESTDIR=" clean

current directory: /Users/USERNAME/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/byebug-11.1.3/ext/byebug
make "DESTDIR="
compiling breakpoint.c
In file included from breakpoint.c:1:
In file included from ./byebug.h:4:
In file included from /Users/USERNAME/.rbenv/versions/3.0.0/include/ruby-3.0.0/ruby.h:38:
In file included from /Users/USERNAME/.rbenv/versions/3.0.0/include/ruby-3.0.0/ruby/ruby.h:23:
In file included from /Users/USERNAME/.rbenv/versions/3.0.0/include/ruby-3.0.0/ruby/defines.h:73:
In file included from /Users/USERNAME/.rbenv/versions/3.0.0/include/ruby-3.0.0/ruby/backward/2/attributes.h:43:
In file included from /Users/USERNAME/.rbenv/versions/3.0.0/include/ruby-3.0.0/ruby/internal/attr/pure.h:25:
/Users/USERNAME/.rbenv/versions/3.0.0/include/ruby-3.0.0/ruby/assert.h:132:1: error: '__declspec' attributes are not enabled; use '-fdeclspec' or '-fms-extensions' to enable support for __declspec attributes
RBIMPL_ATTR_NORETURN()
^
/Users/USERNAME/.rbenv/versions/3.0.0/include/ruby-3.0.0/ruby/internal/attr/noreturn.h:29:33: note: expanded from macro 'RBIMPL_ATTR_NORETURN'
# define RBIMPL_ATTR_NORETURN() __declspec(noreturn)
                                ^
...(snip)...

In file included from breakpoint.c:1:
In file included from ./byebug.h:4:
In file included from /Users/USERNAME/.rbenv/versions/3.0.0/include/ruby-3.0.0/ruby.h:38:
In file included from /Users/USERNAME/.rbenv/versions/3.0.0/include/ruby-3.0.0/ruby/ruby.h:26:
In file included from /Users/USERNAME/.rbenv/versions/3.0.0/include/ruby-3.0.0/ruby/internal/core.h:23:
/Users/USERNAME/.rbenv/versions/3.0.0/include/ruby-3.0.0/ruby/internal/core/rarray.h:88:19: error: field has incomplete type 'struct RBasic'
    struct RBasic basic;
                  ^
/Users/USERNAME/.rbenv/versions/3.0.0/include/ruby-3.0.0/ruby/internal/core/rstring.h:74:12: note: forward declaration of 'struct RBasic'
    struct RBasic basic;
           ^
fatal error: too many errors emitted, stopping now [-ferror-limit=]
6 warnings and 20 errors generated.
make: *** [breakpoint.o] Error 1

make failed, exit code 2

こまった。

解決法

エラーメッセージ読めば書いてある。 どう -fdeclspec を適用するという話だが、gem が Makefile つくるときに extconf.rb で渡せるようにしてあげれば良い。

byebug 見に行くとこうなっているもんで

https://github.com/deivid-rodriguez/byebug/blob/58bb5fde36e33fe9436067014c67351cd8d89cfc/ext/byebug/extconf.rb#L7

じゃあ CC 指定すりゃ良いなってことがわかった。

なお sqlite3 も同様にコケたので同じ方法で解決できた。

根本的には?

これ、 gem 側でオプション渡すような改修入れたほうが良いのかな?どうなっているのが理想かよくわからん。

2020年の振り返り

あまり年末の振り返りというのはしてこなかったのだが、ふと思いついたのでやってみる。 熱燗を飲みつつガキ使と紅白を行ったり来たりしながら書いているので、どんな内容になっているのかかなり不安である。

f:id:upinetree:20201231184417j:plain

これは年越しケーキ。

技術的なこと

前半はでかい Rails アプリケーションをチームで持続的に開発していくためにどのように分割すべきかという、いわゆるモジュラモノリスについてずっと考えていた。 クリーンアーキテクチャや Design It! や 進化的アーキテクチャの内容を参考にしつつ、Rials アプリケーションで現実的に実施するにはどうしたら?を考えた。なお、マイクロサービスは最初からスコープ外としていた。 Shopify の事例は大いに参考になった。 プロトタイプまでは作れたが、並行で進めていた仕事に比重を取られていったり、途中で自分のロールの変化によって中断せざるを得なくなりと、中途半端な取り組みになってしまったのが残念だった。 そのプロトタイプに至る思考過程は文書に残せたし、次に(自分でない人であっても)考える人の踏み台になるような成果は残せたと思う。 このテーマは個人的に興味があるので、2021年も引き続きやっていきたい気持ちがある。

後半は引き続き基盤的な仕事ではあったが、複数アプリケーションが絡むような、より考える範囲が広い課題に取り組んだ。 認証部分をキャッチアップするにあたって、OAuth 2.1 の RFC を全部読んだり、OpenID Connect まわりを勉強した。 それらを現実的な状況に落とすためにはどうするかを考え、どんどん発掘されていく課題に地道に対処していく日々だった。

また、EMとしての振る舞いにも苦戦した年だった。 何分経験があまりないため、最初はどう動いたらよいか勘所がわからなったのだが、上司の的確なサポートのおかげで徐々にうまく動かけるようになってきた。 チームの生産性を上げるためにどうしたらよいか、スピードと品質のバランスを上手くを取るために、開発フローにどんな装置を設けたら良いのか、みたいなことを考え実施してきた。 プロジェクトとしても不確実性がめちゃくちゃ高いこともあって、はやめに不確実な部分を潰すように動くようにしていた。 それでもこぼしてしまったこともあって、それがもとで混乱を生じさせてしまったりして反省も多かった。 この分野の能力も引き続き高めていきたい。

新しい技術や流行りの技術はあまり触れられていない一年だったように思う。 でも複雑でリアルな問題に対して、将来を踏まえどう効率よく解決するのか、という点は今までよりも考えられた一年だった。 プログラミング筋力的には衰えを感じるので、機会を増やせるよう動くとか、個人でガッと時間をとるとか、来年は意識してやっていきたい。

個人的なこと

なんといっても住宅を購入したことが一番大きな出来事だった。 これについては色々学びがあったので、独立したブログを書こうと思っていたのであったが、結局バタバタして年内にはかなわなかった。 少なくない額の35年の住宅ローンを背負う身となってしまったのだが、今まで感じていた暮らしのストレスは解消され、生活のワクワク感も伴い、結果としてよい選択だったと思っている。 そういうこともあり、来年は財テクをがんばっていきたい。

そして話題として外せないのが感染症による世の中の変化。 弊社もほぼリモートワーク前提となり、働き方が大きく変わった。 住宅購入の動機(のひとつ)となったのはそれが発端である。 最初は慣れず疲れもたまる一方であったが、最近はむしろ以前の働き方を思い出せないくらいに慣れてしまった。 一方で対面でのコミュニケーションにはそれにしかない価値があるとも考えているので、来年は選択的に働き方を決められるような世の中になっているとよいなと思う。

今年は楽器はあんまりできなかったので、来年こそはという思いがある。 しばらくはワイワイやるのは難しそうなので、家でリハビリを続けようと思う。 宅録とかはじめて見るのも良いかもしれない。昔DTMをかじったこともあり、深めてみたい分野でもある。

そういえば、Twitter から距離を取るようになった。 どうも先鋭化した情報に出会うことが多くなり、平穏に過ごすことが難しくなった。 といっても書かなくなっただけで、眺めることはやめられていないのだが…無駄に情報の深追いをしないように気をつけている。 技術トレンドをキャッチアップする情報源として使っていたこともあり、代わりに何を見るのが良いのかなというのが最近の関心である。

来年

あまり年始の目標は立てないのであるが、楽しいことをやっていきたいと思う。

VSCode 拡張で TabNine 使ってみた

TabNine というのを使ってみたのでメモ

概要

  • コード補完を機械学習モデルを使って良い感じにしてくれるやつ
  • 主要エディタの拡張として利用可能
  • デフォルトではローカルに機械学習モデルをダウンロードしてきて、ローカルで処理を完結するので外部にコードが漏れることはない
    • 落としてくるモデルは 700MB 弱くらいだった
    • ローカルだと当然結構メモリを食うようで、2G弱くらい消費するよう…
  • メモリ消費が気になる場合はクラウドサーバーの GPU に任せることができる。それが TabNine Cloud と呼ばれているやつで、課金要素でもある
    • 一応コードを送っても補完処理を行う以外には使わないしログにも残さないとのこと
    • もしポリシー上だめそうならエンタープライズ向けのプランにセルフホスティング版も提供しているそうな

使用感

  • まだそんなに使ってないからわからんけど、文脈に応じてよくあるフレーズを出してくれている感じある
  • コード中の似たパターンもなにげに拾ってくれていそう
  • 名前付けとかそれっぽいのが出てくるので地味に便利かも
  • 指筋の節約になるので疲れづらい
  • 補完候補を見て選択してエンター、という操作をやる前にタイピングしたほうが早いのではという場面もある
    • 候補が妥当かどうか脳が判断するのに多少時間かかる場合とか
    • 気にせずタイピングして使えそうなときに使うみたいな運用になるだろうか
  • 最近ゴリゴリコード書く機会が減っているので活躍する機会があるかはわからん…しばらく有効にしておいて、メモリ消費が気になるようならオフにする感じで

ジョブカン出勤簿にバーンダウンチャートを表示する Chrome 拡張を作った

これ。

upinetree/jobcan_burndown

こういうのがでてくるようになる。

f:id:upinetree:20200704014814p:plain
チャート例

今のところデベロッパーモードのみ。

メモ

  • 自分の労働ペースがどんなもんかよくわからなかったので作った
  • あとここ最近 JS 触ってなかったのでなんでもいいので作りたかったというのもある…
    • なんだかんだ React Hooks 触る機会なかったなとか
    • チャート出してるだけなので React 使う必要まったくないんだけど、使いたかったから仕方がない
    • 使ったら満足したので雑目
    • Recharts 見た目すっきりして好きだけど使いやすいかというとそうではなかった
  • こういう用途だと Parcel サクッと使えてめちゃ便利
  • ジョブカンのマークアップが少しでも変わると壊れる
    • むしろ壊れてもいいのでそろそろ良いアプデ期待してます

継続的デプロイの次の課題はなんだろう

Feature Toggle について考えていた流れで、デプロイシステムのことが気になってきた。 ちょっと前にどうやったらデプロイをもっと楽できるだろうかとか考えていたせいもあって、どうも気になると落ち着かない。 軽く思考を整理してブログという形で放流しておく*1

一言でいうと、たとえ理想的なデプロイシステムが得られたとしても、きっと別のところがボトルネックになるので一緒に解消していかないとだよなあみたいな話。

CI/CDがうまく回り始めたら

たとえば master にマージしたら自動でデプロイしてすぐに反映されるみたいな。いわゆる継続的デプロイ (not デリバリ) が達成できた状態。 なんかもう何も考えなくてもデプロイできちゃう。

こうなった状態で何が発生するかと考えると、僕のような人間としてはやはり master にマージするのが怖くなっちゃう気がする。 レビュアーがマージするとしたら、レビュアーは Approve するのが怖くなる。 レビュイーがマージするとしても、Approve もらったあとにマージするには一定の自信と覚悟がいる。

ほかには、機能のリリーススケジュールが決まっているときなんかは結局マージせずにその日を待つこともあるだろう。

一方で、デプロイとは別問題として feature branch を運用する上での課題にも考えを巡らす。

  • master の差分取り込みコスト
  • 他のブランチとの衝突
  • 予期せぬリグレッション
  • コードフリーズ

マージが遅れるとかマージできないってことはこれらの課題を相変わらず抱えたままということ。 デプロイが楽になっても、結局色々考えることあるんじゃないか。

じゃあどうする

トランクベース開発の推進

このあたりの話。

できる限り小さく変更を分けてこまめに master にマージすれば、怖さも最小限で済む。 マージが「機能の開発完了」単位でなく、小さな変更単位になっている状態を目指す。そうした文化づくりをしていく。

とはいえ一度に出したい機能もあるのが実際のところ。 そこでデプロイとリリースの分離が必要になってくる。

デプロイとリリースの分離

  • リリースのレバーをビジネス側が持つ
  • デプロイのレバーを開発側が持つ

こうできたら理想的だし、リリーススケジュール都合でデプロイを待つ必要もない。 (「XX側」といった構造が適切かどうかは検討の余地がある)

とはいえ、現実的にはデプロイ=リリースを意味することが多いので、分離する方法を探すことになる。

この分離のためには Feature Toggle がよく知られているかと思う*2。 特定の機能を無効にしたままリリースできるトグルを実装してしまう方法。

Feature toggle をちゃんとした仕組みとして、プロダクトや運用の性質にあわせていかに上手に設計するかがキーになるのだろうな。 仕組みがない状態で愚直にやり始めると if/else が散りばめられて、保守性の低下を招くことになりかねないし、そうでなくても普通に面倒。 トランクベース開発を可能にするくらいだとさらにその傾向が強くなりそうだ。

独立して適用できるか?

トランクベース開発や、デプロイとリリースの分離は、開発者個人からチーム全体へとコードの所有権を渡していって早期にフィードバックを得る方法のように思う。 そのフィードバックによって、コードを、ひいてはプロダクトを良くしていくことができるんだろう。

そうすると、冒頭の master マージのハードル解消とは独立したメリットがあるプラクティスとして考えることができる。 なのでそういう課題感がなくてもやってもいいんだけど、CI/CDがうまく回っている状態でなければこれらの実現は難しいように思う。 相互補完的な立ち位置にあるのではないだろうか。

とはいえ、できる範囲で部分的に適用していくのは大賛成。 あとはニーズとコストとの綱引きになるのかな。

*1:継続的デリバリ読めば書いてあるかもしれないけど、まだ読んでないのでこの思考自体無駄かもしれない

*2:というか僕はもともとはこれについて考えていたんだった。大脱線というか、逆さまに考えだしたというか

macOS で自作キーボードからジョブカン打刻

様子

用意するもの

jobcan_slack_electron

  • Electron 製自作ツール
  • ジョブカンを Slack 連携させると使えるようになるスラッシュコマンド (/jobcan_touch) を、 Slack API 経由で実行
  • レガシートークンが必要、かつコマンド実行のための command.chat APIは非公開なのでそのうち使えなくなるかもしれない…
    • ジョブカンが API を提供していないのでこうするのが一番楽そう
    • スクレイピングはつらい
  • オマケ機能で出勤、退勤時に好きな画像をランダムで表示してくれる

打刻用ショートカットを登録

macOS のショートカットキー設定ではアプリケーションを直接登録することができないので、 Automator でサービスを作ってからショートカットキーとして設定する。

  • jobcan_slack_electron をパスの通った場所に配置
  • Automator を起動

f:id:upinetree:20200301151649p:plain
Spotlight から探すと楽

  • 「ファイル」→「新規」→「クイックアクション」
  • 「アプリケーションを起動」アクションを追加して、起動アプリケーションに jobcan_slack_electron を選択

f:id:upinetree:20200301151755p:plain
設定が完了した様子

  • 保存(ここでは jobcan_touch という名前を設定)して終了
  • macOS の「システム環境設定」→「キーボード」→「ショートカット」→「サービス」に先程保存したクイックアクションのサービスが出現しているはずなので、好きなショートカットキーを設定する。このあとキーボードの専用キーに登録するショートカットになるので、誤爆しないよう複雑なものにすると良い

f:id:upinetree:20200301151849p:plain
jobcan_touch サービスをショートカットキーとして登録

  • 上記で設定したショートカットキーを自作キーボードから叩くために、QMKのキー設定を書く。以下は meishi の例で、誤爆防止のため One Shot Layer 機能を使っている
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
  [0] = LAYOUT(
    XXXXXXX, XXXXXXX, XXXXXXX, OSL(1) \
  ),
  [1] = LAYOUT(
    HYPR(KC_BSLASH), XXXXXXX, XXXXXXX, XXXXXXX \
  ),
};
  • QMKの設定をファームウェアに焼いたら完成。設定したキーを入力すれば、ジョブカン打刻ができるようなった! 🎉

おまけ: Electron Fiddle でアプリを初めて作った感想

  • 最小限のElectronアプリをシュッと作れて良い体験だった
    • ただ、本当に最小限で、 main.js, render.js, index.html しか編集できない
    • require() を書くだけで自動的にモジュールがインストール、補完なども有効になる
    • 保存すれば普通の Electron プロジェクトなので、普通の開発フローに乗せられる
  • ボイラープレートの作成から、ビルド、実行、パッケージ化がワンタッチでできる
    • Gist への Save/Load もワンタッチ
  • Gist からリポジトリ化する際には、以下の対応を行った。らくちん
    • electron-builder の導入と設定
    • GitHub Actions でのビルド自動化