upinetree's memo

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

Kaigi on Rails 2021 で STORES への ID 基盤の導入について発表した

こちらになります

kaigionrails.org

スライドはこちら

動画がアップされたらそれもあとで貼ろうと思います。

発表内容について

とりあえず15分にしとくかと軽い気持ちで Proposal を出したのですが、いざ採択されて話を整理しはじめてみるとボリュームが膨らむ膨らむ。全然15分では足りませんでした。 大風呂敷を広げた状態のマインドマップがこちら(一応、業務のこと色々書いてあるので拡大しても文字は読めない大きさにしてます)。

マインドマップ

これをキュッと圧縮する作業が本当に大変でした。 当時のメモを読むとこういうことを考えてまとめていたみたいです。

  • やったことが類似事例として役に立つ一方で、類似しない仕事をしている人にも学びも持ち帰ってもらいたい
  • やったことの多くを技術的負債との戦いが占めるので、結局、技術的負債との向き合い方、みたいなところで着地しそうだが、そこで終わりにはしたくない
  • システムの一部を移行することに技術的負債はついてまわる課題だと思うので、うまく学びを抽出できると良い

結果、具体例とかも全部省いて、そこそこ抽象的な話にまとめる形で着地しました。具体的な話は質問タイムで直接聞いてね、という設計です。 もともと認証周りの話ということもあってセキュリティとかを考慮すると具体例を入れづらいテーマなのですが、それにしてもこの内容で本当に面白いんだろうか、みたいな不安がずっとありました。 幸い、当日聞いていただいた方の反応を見たり感想をいただいたりして好評だったのでホッとしたものです。

あとは要素技術の解説とかも軽く入れられると良かったんですけどね。 せっかく sylph01 さんの認証の話からの流れがあったので、そこにうまく繋げられるような形になっていたら、OIDC とかを知らない人もその目的や利点を理解するための事例にできたのではないかなーと。 まあこれは仕方ないですね。

事前収録

事前収録、はじめてでした。 発表の形式は動画提出と、ライブ登壇のどちらかを選べたのですが、あまり家の回線が強くないため当日の通信状態や機材のトラブルを避けたかったのと、せっかくならチャレンジしてみたいな、という動機で、事前収録での動画提出にしたのでした。

参考までに構成を残しておきます。

  • OS: macOS BigSur
  • プレゼンツール: Deckset
  • 画面収録: QuickTime (cmd + shift + 5)
  • 話者収録: 一眼レフ (EOS Kiss X7)
  • 動画編集: Shotcut

音声はいい感じのマイクがなかったので素朴に Mac のマイクで録音しました。ノイズが乗るので Krisp に頼ろうと思ったけど、音声もキャンセルされることがあったので諦めました。通話だと言い直せるからあまり問題ないんでしょうけど、こういう収録だとやっぱ難しいですね。 あと、画面送り用のリモコンを使いました。

ところで MacBook Pro のメインディスプレイ、 16:9 じゃないんですよね。プレゼンツールはちゃんと 16:9 で出力してくれるので、提出用に不要な部分を切り取る動画編集をすることになりました。

チャレンジの側面が大きいので、せっかくなら面白いことしたいなと考えて、無理ない範囲でいろいろ試してます。 こだわったのは、

  • 物理登壇をなるべく再現
    • 立って話す(腹に力も入るので滑舌が改善されるし)
    • 入場と退場をいれる
  • なるべくカメラ目線をキープ
  • 軽めの動画編集による演出

動画編集ははじめてだったので手探りで、iMovieAdobe Premiere Rush ではちょっと機能不足だったのですが、 Shotcut が本当に多機能で使いやすく採用しました。 音声の位置合わせしてプレゼン側の音声だけ使ってそれぞれ出力ということをやったり、キーフレーム使えるのが嬉しくて無駄に使ってアニメーションさせたりしました。

あと実は少しだけ発表時間をオーバーしてしまったのですが、編集で 1.02 倍速とかして収めてます。もう収録する体力が残ってなかったので…。 地味にこういう点が動画提出のメリットですね。

ちなみに背景のぬいぐるみが喋っていたことに気付いた方はいたかな?

立って話すための環境構築、色々なものが必要になるんですよね。こういう感じの撮影環境を簡易的に組み立ててやりました。

ところで事前収録だと、当日は圧倒的に楽ですね。緊張も全然しなかった。もう動画提出した時点で腹は決まってますからね。 自分の発表が流れる様子を、hey ブースで他の人と一緒にわいわいやりながら鑑賞するという新鮮な体験ができました。 なかなか良い体験でした。

とはいえライブ登壇のように当日までにスライドに調整を入れるとか、前後のできごとを拾って話題に組み込むみたいなことができないので、そこはもどかしいところでした。 あと、単純に動画提出〆の 9/26 に間に合わせるように準備するのがけっこう大変でした。

他の発表の感想

初日は半分仕事しながらの参加で、自分の発表それ以外は人と話すみたいなことをしててあまり他の方の発表を見れなかったのが心残りですかね。なので動画が上がってきたら改めて見たいなあ。 2日目はメモが残っていたので振り返ってみると、こんな感じでした。

  • Safe Retry with Idempotency-Key Header
    • リトライ難しい…
    • こういうの自前で実装すると罠に無限に嵌りそうなので、標準に触れるのは改めて大事だなと
    • 質問タイムがとても盛り上がっていて立ち聞きが楽しかった
  • 真の要求を捉え、データベース設計に反映させた話
    • 徐々に、能動的に真の要求を捉えていって、ユビキタス言語やモデルを改善していく具体的な事例がとてもよかった
    • 気を抜いているとだらだらっと既存にならって進めてしまうのであらためて気が引き締まる。新しい要求が出た時点で視点をアップデートして考えられるようにできると良い
  • Railsバージョンを倍にしたサービスのそれまでとそれから
    • 偉業の話だった
    • 絶望の第一部から、根本改善の第二部
    • 負債が常時たまり続けるなら常時解消し続ける、そのための仕組みを構築していく、という実話が良かった
  • Keynote
    • 勇気をもらえる話だった
    • Rafael 氏でも英語でコミュニケーションすることに対する恐怖を乗り越えて、地道に努力して実力や自信を積み上げてきた。そしてかつての自分が憧れていた人たちと肩を並べるようになった、というのがとてもエモかった
    • 同時に Shopify の強さもすごいなあと。フルタイムコミッターの採用や生産性向上に投資し、全社に Sorbet のような挑戦的な仕組みを導入していく判断ができること、それを可能にする体力があること
    • そしてそれらを牽引してきた氏はやっぱすごい

reBako

あとこのカンファレンスについて触れずにいられないのは reBako の体験の良さですね。

holygrail.hatenablog.com

上記の記事で同僚の HolyGrail が触れているように、オフラインの「Kaigi感」が再現されていて、発表の合間に人と話したり、話している間につい発表を聞き逃したりみたいなことまで発生して、懐かしい感覚を取り戻したような感じでした。

あと立ち聞きはとてもいい機能でしたね。興味のある会話の周りに自然に人だかりができて、リアルな感じがありました。そこから会話に参加しやすくなるような仕組みがあるとより良かったのかな。座席数が限られてメンバーが固定化したり、遠慮があったりしてなかなか輪に入れない、みたいなことが減らせるといいですね。私の場合は性格的に後者が大きくて、リアルでもうまくできてないんですが。 そういう意味だと、質問ブースの座席数はもうちょっとあってよかったかもしれませんね。もしくは人の回転を促す仕組みがあると良いのかも。

総じて

発表のクオリティが高くゲストも豪華で刺激的な2日間でした。とてもたのしかった!運営の皆さん、おつかれさま&ありがとうございました。 次回も Proposal 出せたら出したいんですが、Ruby/Rails からしばらく離れそうなので、どうなるかなあ。といっても今回の発表は全然 Ruby/Rails の話じゃないんですが。 なんにせよ次回も楽しみです。

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:というか僕はもともとはこれについて考えていたんだった。大脱線というか、逆さまに考えだしたというか