詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

GitHub Actions から Cloud Datastore エミュレータを利用する

盆栽のテストを書くのに Cloud Datastore エミュレータ が必要になったので、GitHub Actions で利用してみた。

大きく分けて方法は2つある。

ジョブ中で明示的に gcloud beta emulators datastore start する

setup-gcloud というアクションで gcloud コマンドをインストールできる。これを使う方法。

このままでは必要なコンポーネントはついてこないので、加えて gcloud components install する必要がある。

また、エミュレータが起動したのを待つために、curl を --retry-connrefused つきで利用する、というわけで以下のようになる(全景はこちら)。

    - run: |
        gcloud components install cloud-datastore-emulator --quiet
        gcloud components install beta --quiet
    - run: |
        gcloud beta emulators datastore start --project prchecklist-test &
        curl http://localhost:8081 --silent --retry 30 --retry-connrefused --retry-delay 1
    - run: |
        eval $(gcloud beta emulators datastore env-init)
        ...

サービスコンテナを使う

上記の方法でもいいんだけど、本質的でないことに行数を費やしてしまっている気がしてしまってどうもよろしくない。

GitHub Actions にはサービスコンテナという概念があって、ジョブで利用したいサービスを宣言的に指定できるらしい。MySQL とか Redis とか使うならこれだろうから、Datastore もこれにしたい。

サービスコンテナは Docker イメージを起動するかたち。オフィシャルに google/cloud-sdk ってイメージがあって、これで gcloud コマンドを利用できるんだけど、見るかぎり GitHub Actions のサービスコンテナでは今のところ CMD を外から指定できないようなので、エミュレータを起動する CMD をもったイメージを作る必要がある。

雑に検索してみた感じだと、ベースのバージョンに追従しつつ信用できそうな(=リポジトリと連携して automated build が有効になってそうな)やつがないので自作する。ただ書いてみたかっただけかもしれない。

https://hub.docker.com/r/motemen/datastore-emulator

google/cloud-sdk に CMD と HEALTHCHECK を足しただけ。これを使うと、以下のように書けてスッキリした(全景)。

    services:
      datastore-emulator:
        image: motemen/datastore-emulator
        ports:
          - '8081:8081'
        env:
          CLOUDSDK_CORE_PROJECT: ...

そして

書いていたらまったく同じ課題を解決しようとしている記事をはっけんしました……(これを読んで自分の actions 設定を手直しした)。

budougumi0617.github.io

中間証明書のないサーバにアクセスする

不特定多数のウェブサイトにアクセスするアプリケーションを書いていると、ときおり SSL 証明書の検証エラーとなる URL に行き当たることがある。が、確認のためブラウザでアクセスしてみると、普通に見れてしまったりもする。そんな事例のひとつ、タイトルの通り中間CA証明書のないサーバについて。

https://incomplete-chain.badssl.com/ というわかりやすい例がある。これを curl してみると:

% docker run -it --rm buildpack-deps:buster bash
root@22f1788d53c7:/# curl --version
curl 7.64.0 (x86_64-pc-linux-gnu) libcurl/7.64.0 OpenSSL/1.1.1d zlib/1.2.11 libidn2/2.0.5 libpsl/0.20.2 (+libidn2/2.0.5) libssh2/1.8.0 nghttp2/1.36.0 librtmp/2.3
Release-Date: 2019-02-06
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets HTTPS-proxy PSL
root@22f1788d53c7:/# curl -sS https://www.example.com/ > /dev/null # 正常系として
root@22f1788d53c7:/# curl -sS https://incomplete-chain.badssl.com/ > /dev/null
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

となるわけだけど、手元のブラウザではたぶんとくにエラーもなく開ける(はず)。

この URL では、サーバはあえて中間証明書を配信していない。にもかかわらずブラウザで見えてしまうのは、badssl.com の説明によると、

  • ブラウザが中間証明書をキャッシュしている
  • または、ブラウザが AIA (= Authority Information Access) fetching に対応している

から、ということらしい。AIA は SSL 証明書の拡張仕様で、サーバの証明書の発行者の証明書へのアクセス方法が記述されるもの。ブラウザで証明書の情報を見てみると確認できるはず。見てみると証明書の URL が http: で記述されていることに最初おどろいたけど、平文で取得した証明書も検証されるので、安全性には問題ないのだった。それを自動的に取得してくるのが AIA fetching(なのだと思う)。AIA fetching は最近のメジャーなブラウザだと Firefox 以外は対応しているのだというがあったが、公式にアナウンスしているものはすぐには見つからなかった。ソースを読まないといけないだろう。

中間証明書をまとめて取得する

で、ブラウザで見られるページはアプリケーションからも取得できたい、というのは普通にありえる需要で、これをどのように実現するのか、という話。

AIA fetching を実装してしまう、というのも正攻法ではあって、そういう実装もある(未検証)。使えるライブラリがない場合や実装したくない場合に、すべてのサイトを尽くすことはできないかもしれないけど、多くをカバーできる方法として、あらかじめ中間証明書を手元に取得しておく、という手をここでは考える。

つまり、ルートCA証明書によって署名されている中間証明書をあらかじめダウンロードしておけばいいわけだ。そして、都合のいいことにそういうリストが作られている。

https://wiki.mozilla.org/CA/Intermediate_Certificates

Mozilla による Common CA Database (CCADB) プロジェクト のリソースのひとつで、Mozilla が信頼しているルート CA によって署名された中間証明書の一覧らしい。このページの権威は自分には確認できなかったけど、どのみちすでに信頼されている手元のルート証明書で検証するので、それっぽいものが網羅されていれば出自はなんでもいいのである。

先述のページから中間証明書を取得し、すでに手元にあるルート証明書で検証できたものだけ保存することにする。Dockerfile にするとこういう感じになる。楽したかったので、apt で入る csvtool というのを使ってみた。

FROM buildpack-deps:buster

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

RUN apt-get update && \
    apt-get install -y csvtool
RUN mkdir -p /opt/intermediate-certs/certs && \
    curl -sL https://ccadb-public.secure.force.com/mozilla/PublicAllIntermediateCertsWithPEMCSV | \
    csvtool namedcol 'PEM Info' - | \
    csvtool drop 1 - | \ # ヘッダを落とす
    csvtool call "printf '%q\n'" - | \ # 改行が含まれているので1行に
    while IFS= read -r e; do \
        pem=$(eval "echo $e" | sed "s/'//"); \ # ' を除く
        if echo "$pem" | openssl verify; then \ # 既存のルート証明書で検証
            echo "$pem" | \
                tee -a /opt/intermediate-certs/ca-bundle.crt \ # 証明書バンドルを作る
                > /opt/intermediate-certs/certs/"$(echo "$pem" | openssl x509 -subject -noout | perl -pe 's/\W/_/g')".pem; \ # 個別の証明書も保存する
        fi; \
    done && \
    c_rehash /opt/intermediate-certs/certs

openssl verify して成功したものだけ保存する。今気づいたけど、中間証明書によって署名されている中間証明書はここで跳ねられてしまうな……。

証明書バンドルは、単にテキストとして並べて記述すればよい(opensslRFC)。取り回しがしやすいように、すべての証明書を1ファイルにまとめたものとそれぞれ別々のファイルにしたものとを作っている。

今回取得した中間証明書をシステムにそのまま組み込んでしまいたいなら、システムのルート(debian なら /etc/ssl/certs)に置いてしまえばいいのだけど、アプリケーション側で制御したいことがほとんどだと思うので、別の場所に置いておくのだいいだろう。

さっそく試してみる。--cacert がポイント。--capath /opt/intermediate-certs/certs:/etc/ssl/certs としてもいい。

% docker run --rm -it $(docker build -q .) bash
root@39401ca59f18:/# curl --version
curl 7.64.0 (x86_64-pc-linux-gnu) libcurl/7.64.0 OpenSSL/1.1.1d zlib/1.2.11 libidn2/2.0.5 libpsl/0.20.2 (+libidn2/2.0.5) libssh2/1.8.0 nghttp2/1.36.0 librtmp/2.3
Release-Date: 2019-02-06
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets HTTPS-proxy PSL
root@39401ca59f18:/# curl --cacert /opt/intermediate-certs/ca-bundle.crt -sS https://www.example.com/ > /dev/n
ull
root@39401ca59f18:/# curl --cacert /opt/intermediate-certs/ca-bundle.crt -sS https://incomplete-chain.badssl.com/ > /dev/null

やったね! 成功です。

ちなみに

ブラウザの確認だけで済ませてしまうと、こういう設定漏れに気づきにくいみたいです。Mackerel のようなサービスを使えば、証明書の期限なども含めて監視できます。わたしも使っています! どうぞご利用下さい。

mackerel.io

f:id:motemen:20200422224025p:plain

参考文献

ghq など、github.com/motemen 下のリポジトリを x-motemen Org に移しています

もうけっこう前のことになりますが、github.com/motemen 下にあったリポジトリの一部を github.com/x-motemen organization に移管しました。コードやバイナリの配布形式によって、ユーザ側で対処する必要があったりなかったりしますが、それぞれのリポジトリにおける指示(あれば)に従ってもらえればと思います。

もともとこれらは id:Songmuid:itchynyid:onk らにアクティブにメンテされているもので、motemen 個人はほぼ完全にノータッチとなっているのが現状です。そういうリポジトリは他にもあるので、今後も随時このような移管をやっていくつもりです。よろしくお願いします。

Organization 移管のメリット

で、実際的な話として、個人リポジトリにコラボレータとして追加することと organization のリポジトリとすることの差なんだけど、与えられる権限が違うっぽい。

とくに個人リポジトリではコラボレータに admin 権限を与えることができず、そうすると webhook の設定ができないので、メンテナがリポジトリを積極的に改善しよう! と思ったときにいきなり出鼻をくじかれることになってしまう(とくに最近だと GitHub Actions 化しようとしてやられる)。

こちらは org 化していないリポジトリ(gore)のコラボレータ管理画面。Write 権限のあるなししかない。

f:id:motemen:20200302133140p:plain

こちらは org 化したリポジトリ(ghq)のコラボレータ管理画面。songmu さんに admin 権限をあげて、自分は Write みたいにできている。

f:id:motemen:20200302133159p:plain

移管の経緯

大きなきっかけは ghq の v1 リリース。メンテナの songmu さんと会ったときに、この際 ghq organization みたいなのを作ってそこに置きます? というような話をしたんだけど、motemen/ghq でひとつの名前として認知されてるからなー、という話もあり、その時はそのままの名前で行くことになったのでした。

OSS(とも思ってないが)を別の名前の下に移す……ということにはなかなかアンビバレントな感情があって、大方の人間と同じく自分もいわゆる承認欲求を燃料にしてこれまで生きてきたので、自分の作ったものが使われたり、言及されることはとても嬉しい。そういう面では、自分の名前から剥がしてしまうことには心理的な抵抗が長いことあった。

しかし幸いなことに自分のソフトウェアが多くの人に使われるようになってくると、自分にとって負の影響も大きくなってくる。つまりイシューや pull request がやってくるようになる。はじめの頃はこういう直接の反応がやってくるのが嬉しいので丁寧に応対しようとするけど、数は多くなくても、一つひとつがその裏に人間の意図や要望のあることであるし、否応なくコミュニケーションを必要とするので、疲れてしまう。そもそも自分は人間とアレコレしたくないからコンピュータを触っているのであった(今どきこういう考えをしている人は少ないのかもしれない)。それなのになせこんなことに……。他人に認められたいのに、他人とのコミュニケーションを拒む、という姿勢自体がはらんでいる歪みだったのかもしれない。

もうひとつ重要なのは、リポジトリを公開した時点で、そのソフトウェアによって解決したかった自分の課題はほぼ満たされているので、それ以上に手を加えるモチベーションがない、ということだった。公開したソフトウェアはそこで硬直してしまっていて、そこに変更を加えることの喜びは、他人にどんな風に使われているか分からないものを変更するコストを上回らない。自分で書いててひでーなとも思うが、そういうことである。こういうのをひっくるめて、自分の書いたものがOSSとも呼べない(というかバザール的でない)な、と思っている。

それでも使われているものは身近な人がメンテナになってくれていて、イシューに回答したりリリースまでやってくれていたりする。それはありがたいことで、コミュニケーションを拒否している罪悪感はいくらか和らぐものの、もうおれの持ち物じゃなくね? という感覚は大きくなっていく。

とまあこんなふうに、自分のものであるという主張には固執しつつ、しかし他者からの要望に応えもしないでいる、ということにだんだんと嫌気が差してきて、冒頭に書いたように org 化の話が出てくるわけです。結局その時は見送ったわけだけど、それでもそのアイデアは頭の中に残っていて、ghq 以外のソフトウェアも他の人にメンテしてもらってるものはひとつの org に移してしまおうと画策はしていた。Apache Incubator に倣って motemen-incubated とか考えてたんだけど、x-motemen という名前を思いついて、試しに organization 作成フォームに入力していたら作れてしまったので、そのまま利用したという流れであった。x- には卒業感があって、いいなあと自画自賛する次第である。

結局自分の名前残っとるやんけ、との指摘はその通りで、しかし自分の中ではもはやあまり重要でもない。

それにしても、移管と直接関係するわけではないけれど、メンテナンスされるソフトウェアの価値というものをひしひしと感じている昨今であり、brew upgrade ghq して新しいバージョンが手元に降ってくることには、自分が最初のユーザであるからこその喜びがあると思う。こればかりは特別な体験です。

はてなで一緒に働きませんか?