うなすけとあれこれ

2020年12月12日

サーバーサイドエンジニアとして2020年に使った技術

使用したプログラミング言語統計

2020年のフロントエンドエンジニアの技術スタックの一例 | potato4d D(iary)

この記事と、TLで「これのバックエンド版が見たい」という発言に触発されたので書いてみます。口語体と文語体が入り乱れてるのは許してください。

冒頭のグラフはwakatimeで生成した今年1年間のプログラミング言語使用率です。2位はTypeScript、3位はTerraform、4位はYAMLでした。

立場

フリーランスで、主にRailsやAWSを使用しているサービスの運用、開発に関わっています。いくつもの会社を見てきた訳ではなく、数社に深く関わっている1都合上、視野が狭いかもしれません。

公開している成果としては クラウドゲーミング最新開発事例 - #CEDEC2020 - Speaker Deck があります。

長年RubyとRailsを書いてきたので、技術スタックがそのあたりに偏っています。

利用した技術一覧

太字は特記事項があるものです。また、挙げている技術は「ある程度触った」くらいで記載しており、全てに習熟している訳ではありません2

各項目について、上のほうがよく使っているものという傾向がありますが、厳密ではないです。

主軸としてはRuby/Rails/AWSの主要コンポーネント というところでした。

特記事項

Ruby

今年も僕の主戦場はRubyでした。あと数年はRuby以外を軸に仕事をしていくことになるんじゃないかな……?ただ2020年らしいことは全くしていなくて、例えばRuby 2.7で導入された機能、Pattern matchingやnumberd parameterは使った記憶がないです。(Pattern matchingは使おうとしたが、愚直に書いたほうが短かくなったので断念)

TypeScript

今年はJavaScriptをBrowser console以外で書いた記憶がありません。

Rails

キャリアがRailsから始まっているので、思い入れがあります。また、新しい現場でもすぐにコードを書けるところは助かっています。とは言えコードが肥大化してくると何がどうやって動いているのか把握するのには時間がかかりますが、これはまあどんなものでもそうでしょう。 業界においてRailsが優位にあるとすれば、その実装速度の早さと、コードやデータ量、ビジネスロジックがある程度成長しても耐えられるRubyの表現力及びActiveRecordの底力があるように思います。あと数年は新規実装としてのRailsの立場は残っていると思います。それ以降はまた状況が変わっていると思うのでわかりません。

React/Vue.js

これを1つにまとめたのは、主にRails appに組み込まれたものを書いていたためです。TypeScriptもそうなのですが、JavaScriptに関しては既存のコードベースに手を入れるということはできても、設計を含めた0から書き始めるということができていない、できないのが自分の弱みだと感じているので、プライベートの時間ではキャッチアップをしています。

Docker

コンテナ技術はもう前提条件と言えるくらいには利用が広がっていますね。ECSやKubernetesをはじめとしたエコシステムの発展もあり、開発環境から本番環境までDockerで環境が統一されていることの便利さ、docker-compose一発で開発環境が準備できる手軽さは一度体験するともう戻れないと言っていいです。

Docker for Macが遅い遅いと言われ続けている昨今ですが、僕の環境においては許容範囲というか、あまりそこで困らなかった印象があります。大分改善されているのではないでしょうか。それとも僕が鈍いのか。

Fargate/ECS

業務としては一切Kubernetesを使用しませんでした。関わっている範囲だとこの2つで十分要求を満たすことができていました。

IntelliJ Idea系

これは、今年春から JetBrains All Products Pack を契約して使い始めました。これについては1本ブログ記事を書くつもりで下書きを温めていたのですが、この際ですからそれから引用する形にします。

Rubyを第一言語としていますが、PythonやGoなどで書かれたAPI serverの開発を素早く完了させる必然性が増えました。 これまでは腰を据えてプロダクトの全容をあらかた把握した上で開発を進めることが多かったのですが、とにかく素早く問題点を発見して修正する、それもあまり習熟していない言語で、という状況に置かれることが増えてきました。 それに、習熟しているつもりのRubyですら、今までにない大規模なアプリケーションの機能開発においては全容を把握しておくということが困難です。

また、DB clientとしてのDataGripが非常に便利なのは嬉しい誤算でした。

おわりに

あまりRails界隈でこういう記事をみかけない気がするので、他の皆さんはどうなのか知りたいので書いてほしかったり……年の瀬に1年を振り返ってみるのは楽しかったです。


  1. おおっぴらに言わないだけで探せばわかります 

  2. インターネット・強・パーソンの皆さまは、あれがないこれがないこいつはショボいとお叩きになられるのでは……とビクビクしています。 

Tweet
2020年12月12日
2020年12月02日

ナ組Minecraftサーバーの監視について —マイクラサーバー監視2020—

はじめに

皆さん、Minecraftしてますか。サーバー、立ててますか。監視、してますか?

この記事では、2020年10月末に爆誕したMinecraftサーバー「ナ組サーバー」について、僕が勝手に監視している方法について現時点での構成をまとめておくものです。

なんか突然GCPを触りたくなったのでナ組マイクラ鯖を立てた

— 蜘蛛糸まな🕸️ / HolyGrail (@HolyGrail) October 30, 2020

注意

「マイクラサーバー監視2020」と題していますが、僕はこれまでにMinecraftのサーバーを運用した経験はありません。何ならここで言及するサーバーについても、構築したのは蜘蛛糸まな氏です。 単純に、今Minecraftサーバーの監視をするならどうするか、ということについて述べています。過去のベストプラクティスは知りません。

ナ組サーバーとは

ナナメさん (@7name_) とそのお友達が遊んでいるMinecraftサーバーです。構築は前述の通り、蜘蛛糸まな氏です。GCP上に構築されています。

#ナ組マイクラ - Twitter検索 / Twitter

この動画の冒頭でも成り立ちについては述べられています。

僕は、このサーバーへのアクセス権を頂いているので、そこから色々な作業を行い監視環境を構築してみました。以下は行なったことについてのまとめとなります。

監視ツールについて

まず、サーバーの監視を行うときに「何でモニタリングするか」というのが大きな部分を占めるでしょう。SaaSであればMackerelやDatadog、GCP上のサーバーであればCloud Monitoringを使っておくのもありでしょう。自分で構築するのであれば、PrometheusやZabbixやNagiosなどの選択肢もあります。

今回は、Grafanaとの連携で見た目が良いこと、新しめのものであることから、Prometheusで監視、Grafanaで可視化という構成を採用しています。

MinecraftへのPluginの導入

先ほどPrometheusを導入することに決めた、と書きましたが、そもそもMinecraftサーバーの状態をPrometheusから取得することができなければ無意味です。

Minecraftに導入できるPrometheus exporterはいつくか存在します。

star数、ドキュメントの量から、 sladkoff/minecraft-prometheus-exporter を導入することに決めました。このようなexporterが存在していたことも、Prometheus採用の一因です。

MinecraftサーバーへのPluginの導入については、各サーバーによってまちまちだと思うので詳しくは触れませんが、今回は特定のディレクトリ以下へplugin本体を展開しておくだけで済みました。

plugin自体の設定はYAMLで記述することができるので楽ですね。いい感じにやっておきましょう。

Prometheus, Grafana serverの構築

今回、MinecraftとPrometheus、及びGrafanaは同居させず、別のサーバーに分離することにしました。どちらもワンバイナリで動作するので、サーバーの適当なディレクトリに置いて起動させるだけで構築は大体完了です。追加でやることは、Prometheusのscraping_configsにMinecraftサーバーを追加で指定することと、Grafanaのdata sourceにPrometheusを追加すること程度です。

Webサーバー運用についてのHTTPS化などのエトセトラはここで詳細に記述することはしません。1

dashboardの作成

ここまででGrafanaを使用してダッシュボードを作成することができるようになっていはずなので、あとはチマチマと必要そうなグラフを並べたダッシュボードを作成します。ここで作成したダッシュボードの変化を見ているときが一番楽しいですね。

ダッシュボード

このダッシュボードについてですが、MinecraftサーバーのIPアドレスがわかってしまうため、一般に公開はしていません。2

アラートの設定

サーバーというものは、いつ止まってもおかしくありませんし、止まってしまっているときには何らかの方法で通知が欲しいものです。

Prometheusによるアラートといえば、Alertmanagerがあります。Alertmanagerは様々な手段でアラートを送信することができますが、一番手軽なものはWebhookによる通知なのではないでしょうか。そこで、Webhookによる通知をAlertmanagerから送信しようとしてみましたが、これがDiscordのWebhookには対応していませんでした。3

Alertmanager integration with Discord · Issue #1365 · prometheus/alertmanager

ナ組はコミュニケーションをDiscord上で行なっているので、そこに通知を集約できないのはつらいです。

しかし幸いなことに、Grafana自体のArert機能においては、DiscordのWebhookをサポートしていることが判明しました。

https://grafana.com/docs/grafana/latest/alerting/notifications/#list-of-supported-notifiers

そして、指定した閾値に達した場合に次のようにアラートをWebhook経由でDiscordに飛ばせることが確認できました。

テストアラートの様子

ここでは、Minecraftが使用しているJVMのThread数が異常な値になった場合に通知をするという単純なルールになっています。

まとめとアドベントカレンダーについて

このようにしてナ組のMinecraftサーバーは監視されています。Minecraftは10年近く遊ばれているゲームであり、インターネット上の情報も豊富ではあるものの古くなっている記述が沢山あります。監視に関する情報も、Prometheusを使用しているような比較的新しいものは見当らなかったので、こうやって記事にしてみました。

また、この記事は whywaita Advent Calendar 2020 の12/2を担当する記事となります。whywaitaさんは現在プライベートクラウドに関する仕事に関わっていたり、過去にPrometheus clusterを破壊するなどの輝かしい功績のあることですから、今回僕の行なった作業についても、より良い方法を知っているものと思います。一度、対面で教えていただきたいものですね。


  1. certbotを使用しています。それ以外の詳細については面倒になったので書きません。 

  2. わからないようにできないか?とがんばってみましたが僕には無理でした。 

  3. 代替手段として https://github.com/benjojo/alertmanager-discord がありますが、コンポーネントを増やすことになるので導入はしませんでした。 

Tweet
2020年12月02日
2020年11月30日

このブログをS3 Static website hosting + CloudFrontからAWS Amplifyに移行しました

amplify console

2017年からS3 Static website hostingとCloudFrontの組み合わせで運用してきたこのブログを、AWS Amplifyによる運用へと変更しました。

理由

S3 syncが遅い

記事、画像が増えてくると、S3 syncに時間がかかるようになってきます。大体10分前後かかるということで、ちょっとなんとかしたいなと思っていました。また、deployに使用している middleman-s3_sync gem の開発が停滞しているように見えるのも脱S3 syncの要因のひとつです。良く言えば安定している、と言えなくもないし、嫌ならS3 syncによるdeployをやめればいいという話ではあるかもしれませんが……

管理が楽

大した管理をしていたわけでもないですが、S3とCloudFrontの2つのサービスによる構成から、Amplifyだけを見れば良くなるので少し楽になります。

裏側の構成は変化しない

Amplifyも裏はCloudFront+S3っぽいし、何度か計測してると逆転することもあったので有意差はなさそう https://t.co/wa6hgje5PH

— うなすけ (@yu_suke1994) July 24, 2020

という感じで、Amplify consoleでもCloudFrontのdistrinution URLがCNAMEとして指定する値と出てくるように、CloudFrontがリクエストを受けるという構成は変化しないことがわかっていました。なので大した影響はないだろうと判断しました。

ハマり

ダウンタイムが発生する

元々のCloudFrontで握っているTLS証明書との兼ね合いか、一度既存のCloudFrontのdistributionを削除(無効化ではだめだった)してからでないとAmplify console側で以前のものと同じドメインを設定させることができないようです。

Switching Cloudfront+S3 => Amplify with custom domain requires downtime · Issue #22 · aws-amplify/amplify-console

そのためにダウンタイムは必ず発生してしまいます。

それがどのくらいかかるかについてですが、ダウンタイムは体感では5分もなかったように思います。

(特に予告なくシレッと移行作業を行いました)

Tweet
2020年11月30日
2020年10月17日

Kaigi on Rails STAY HOME Edition 配信の裏側

配信構成

御礼

Kaigi on Railsという初めてのイベントに参加していただいた皆様、素晴らしい発表をしてくださった発表者の皆様、惜しくも採択とはいかなかったものの、Proposalを提出してくださった皆様、協賛していただいたスポンサーの皆様、本当にありがとうございます。

Kaigi on RailsではProposal提出まわりと、yrindaさんと一緒に配信・動画まわりを担当していたうなすけです。

https://kaigionrails.org/team/

この記事では、完全オンライン開催にあたっての配信まわりについて、裏側がどうだったかについて触れるものです。リアル開催からオンライン開催に切り替えた苦労話なんかはチーフオーガナイザー氏が書いてくれたのでここでは触れません。

Kaigi on Railsのチーフオーガナイザーを務めました - okuramasafumiのブログ

はじめに本番構成

全てをすっ飛ばして、本番の配信環境をまず紹介します。

本番配信はメインのyrinda houseとサブのunasuke houseの2系統を用意して冗長化していました。メインで何か問題が発生したらサブ側の配信URLを広報し、そちらを視聴してもらうイメージでした。そのために2人が同じ内容をメインとサブで配信する必要があり、シーンの切り替えタイミング等は当日全員が入っているDiscordでタイミングを合わせて行っていました。

ライブ発表の場合の構成は、アプリケーション側のZoomを発表者の映像用に、モニターを1枚占有した状態のブラウザからZoomに入り発表資料用に、そしてこれらをOBSに流しこみました。 その他に、それらの画面に重ねる発表情報の枠、Zoomからの音声出力キャプチャなどの設定をライブ発表用に用意しました。

配信構成図

動画発表の場合は、発表する方々から事前に提出いただいた動画を、同様の見た目になるよう発表用の枠をかぶせて書き出し、当日は再生するのみの運用になるようにしました。

当日のタイムテーブルの進行については、発表者、休憩それぞれでOBSのシーンを作成し、基本的にはそれを上から切り替えていくという省エネ運用になるように配信担当側で協力して作り込んでいきました。

OBS scene構成

それまでのストーリー

まず当初、Kaigi on Railsはリアル会場で開催するつもりでした。ところが時世が変化し、どうしてもリアルで集まっての開催ができなくなり、他の多くのイベントと同様にインターネット上での配信による開催に切り替えることとなりました。

オンライン開催となったとき、ではどのような形式で開催するのかということが問題になってきます。配信プラットフォームはどれにするのか、どのように配信を行うのか、日付、時間帯、各トークの長さ、配信を委託するのか自分達でやるのか……

何度か話し合いを行った結果、運営チームにある程度の設備があることから、配信は自分達で行うことに、プラットフォームは強い決め手がある訳ではないですが、YouTube Liveを選択しました。議事録を振り返っていたのですが、全部録画もしくは配信ではなく、録画と配信を希望制にした理由は残っていませんでした。議論するまでもなく、そう決まったのだと思います。

オンラインイベントにすると決まった訳ですが、我々には知見が全くありませんでした。そこで、Kaigi on Railsそのものの宣伝も兼ね、「Kaigi on Rails new」というイベントを開催し、そこでオンラインイベントの “感じ” を掴むことにしました。

Kaigi on Rails new - Kaigi on Rails | Doorkeeper

結果様々なKPTを洗い出すことができ、本番までに何を準備すればよいか、どういう運用にすればいいのかが見えてきました。

その後は提出してもらった動画を編集し、書き出したものを共有したりで動画の準備は進んでいきます。機材がない方のために、事前に録画会をやろうという企画もありましたが、希望者少数のため実現しませんでした。

配信勢の皆さんに対しては、直前に配信テスト会を開催してZoomでの登壇の感じをつかんでもらう一方、配信スタッフ側でも実際にどういうオペレーションになるかの細部を詰めました。具体的には登壇者交代がどうなるか、などです。

そして、前述のような構成になりました。

当日起こったトラブル

反省点

音量のバラつき

事前に頂いていた動画の音量は正規化していたのですが、配信側の音量が想定以上に大きく、相対的に動画のほうが小さく聞こえてしまう問題がありました。アーカイブ公開版では再度正規化を行って、できる限りバラつきがないようにしてあります。

そもそもの配信構成について

振り返ってみると、配信にあたって回線を冗長化するよりは、オペレーターを冗長構成にすべきだったかもしれません。要するに配信環境を1箇所に構築し、そこに配信担当者が集合するという体制です。今回は何事もなく終わりましたし、何事があってもイベントが進行するように準備をしましたが、次回どうするかはまた考える必要があります。何事もそうですが。

参考にした情報

今後

Kaigi on Railsは来年も開催することが決定となっていますが、「やる」以外は何も決まっていません(本当に、何も……)。配信環境についても、今回の反省を活かして何かしらの改善を行っていきたいですね。例えばクラウド上からの配信にしたり、配信用機材を導入したりといったことが考えられます。1年かけて、ゆっくり考えていきたいです。


この記事はHHKB Professional HYBRID Type-SとHelixPicoとChoco60で書かれました。

Tweet
2020年10月17日
2020年09月26日

ItamaeのCIを travis-ci.org から travis-ci.com に移行しました

読む前に

タイトルに書いてあるように、travisの org から com に移行した、という話なのですが、そこに至るまでに様々な苦労をしたのでお願いだから最後まで読んでねぎらってください。

そもそもの始まり

ItamaeのCIには、travis-ci.org を使用していました。

CIというものは、たまに落ちることがあります。その原因は内部のコードが悪い場合や外部要因である場合などが挙げられます。ItamaeのCIも、例によってたまに落ちることがありました。

落ちているログ

たまに落ちることはよくて、落ちたCIを再度実行して通れば良いのです。しかし問題は、「落ちたtestを再度実行することができない」 というところにありました。

いつからなのかは不明ですが、travis-ci.org では失敗したtestを再実行するUIが消えてしまっており、空commitを積むなどしなければ同じコードでのtestの再実行ができなくなっていました。

再実行ができない様子

この “More Options” 内の “Requests” は一見再buildのリクエストのように見えますがそのような挙動はしません。

これは非常につらい。ので、CIをtravis-ci.orgではない別の何かに乗り換えることにしました。

Itamaeのテストについて

CIについて話す前に、Itamaeのtestにおける対象の組み合わせについて触れておきます。CIでは以下のような組み合わせに対してテストを行っています。

unitなのかintegrationなのか、というのは、unitはItamae gemの実装に対してテストを行い、integrationは用意されているrecipeをDocker containerに適用して意図した状態にできているかを確認するテストとなっています。

(以前はDigitalOceanで起動したインスタンスに対してrecipeの適用を行っていました)

選択肢

さて、OSSなら無料で使用できるCIサービスというものはいくつかありますが、代表的なものに以下の2つが上げられると思います。

新しもの好きということもあり、まずGitHub Actionsを試してみることにしました。

GitHub Actions

まずGitHub Actionsでのunit testは以下のようなYAMLで実行できます。

name: "unit test on ubuntu"

on:
  push:
    branches: "*"
jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-16.04, ubuntu-18.04]
        ruby: [2.2, 2.3, 2.4, 2.5, 2.6, 2.7, head]
      fail-fast: false
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v2
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby }}
      - run: bundle install
      - run: bundle exec rake spec:unit

簡単ですね。これで問題なくunit testはpassするようになります。

GitHub Actionsでunit testが成功する様子

しかし、GitHub Actionsでは integration testがどうしても成功しないという問題に直面してしまいました。

GitHub Actionsでintegration testがランダムに落ちる様子

なぜintegration testが通らないのか

なぜ通らないのかの原因がわかればいいのですが、これが全くの不明でした。上記画像にあるように落ちているものもあれば通っているものもあり、全体的に不安定になっています。落ちている原因そのものはtmpディレクトリに書き込みができなくなって落ちているのですが、なぜそうなっているのかはわかりません。

友人に相談したところ、興味深い事実が明らかになりました。

Slack log1 Slack log2

「straceをonにするとpassする」 のです。

straceは何をするツールなのかと言うと、システムコールをトレースするツールです。それを有効にしただけで通るようになる、つまり実行に対してオーバーヘッドがかかるとtmpへの書き込みが成功してtestが成功する、という現象が発生しているようなのです。

なぜこのような状況になっているのかの調査はちょっとハードルが高すぎるので、GitHub Actionsの採用は見送ることにしました。

(self-hosted runnerを使用することでGitHub Actionsで使用されているインスタンスに特有の問題なのかを調査することもできますが、そこまでの元気は出ませんでした)

CircleCI

次に手を出したのはCircleCIです。CircleCIでは最近matrix jobを記述できるようになり、複数の条件を組み合わせたテストが書けるようになりました。

https://circleci.com/blog/circleci-matrix-jobs

とはいえ複雑なYAMLになってしまいました。

.circleci/config.yml を見る

version: 2.1
orbs:
  ruby: circleci/ruby@1.0.7

executors:
  docker:
    docker:
      - image: cimg/base:stable
  docker-1804:
    docker:
      - image: cimg/base:stable-18.04
  machine:
    machine:
      image: circleci/classic:201808-01

jobs:
  unit:
    parameters:
      ruby-version:
        type: string
      exec:
        type: executor
        default: ""
    executor: << parameters.exec >>
    steps:
      - checkout
      - ruby/install:
          version: << parameters.ruby-version >>
      - run: gem install bundler --version 1.17.3 --force
      - run: bundle install -j4
      - run: ruby -v
      - run: bundle exec rake spec:unit
  integration:
    parameters:
      ruby-version:
        type: string
    executor: machine
    # executor: docker
    steps:
      - checkout
      - setup_remote_docker:
          version: 18.06.0-ce
      - ruby/install:
          version: << parameters.ruby-version >>
      - run: gem install bundler --version 1.17.3 --force
      - run: bundle install -j4
      - run:
          command: |
            ruby -v
            export PATH=$HOME/.rvm/bin:$PATH
            ruby -v
      - run: bundle exec rake spec:integration:all
  unit-jit:
    parameters:
      ruby-version:
        type: string
    executor: docker
    steps:
      - checkout
      - ruby/install:
          version: << parameters.ruby-version >>
      - run: gem install bundler --version 1.17.3 --force
      - run: bundle install -j4
      - run: ruby -v
      - run: RUBYOPT=--jit bundle exec rake spec:unit
  integration-jit:
    parameters:
      ruby-version:
        type: string
    # executor: machine
    executor: docker
    steps:
      - checkout
      - setup_remote_docker:
          version: 18.06.0-ce
      - ruby/install:
          version: << parameters.ruby-version >>
      - run: gem install bundler --version 1.17.3 --force
      - run: bundle install -j4
      - run: ruby -v
      - run: RUBYOPT=--jit bundle exec rake spec:integration:all

workflows:
  version: 2
  all-test:
    jobs:
      - unit:
          exec:
            name: docker-1804
          matrix:
            parameters:
              ruby-version: ["2.3"]
      - unit:
          exec:
            name: docker
          matrix:
            parameters:
              ruby-version: ["2.4", "2.5", "2.6", "2.7"]
      - integration:
          matrix:
            parameters:
              ruby-version: ["2.3", "2.4", "2.5", "2.6", "2.7"]
  # all-test-with-jit:
  #   jobs:
      - unit-jit:
          matrix:
            parameters:
              ruby-version: ["2.6", "2.7"]
      - integration-jit:
          matrix:
            parameters:
              ruby-version: ["2.6", "2.7"]

CircleCIでItamaeのtestを実行するにあたり、いくつかの壁に突き当たったので紹介します。

ruby/install-depsの挙動

CircleCIにはOrbという仕組みがあり、汎用的な手順ならYAMLに記述しなくてもOrbを導入することによって記述を省略できる仕組みがあります。

Orb の概要 - CircleCI

Rubyの実行環境を用意するOrbとして、CircleCIが公式で用意しているのが circleci/ruby です。

このOrbですが、bundlerをインストールするのにGemfile.lockの存在をアテにしている部分があります。

https://github.com/CircleCI-Public/ruby-orb/blob/5fee9e2ae8fc7a88cce9ce4d9da4c562ead614b1/src/commands/install-deps.yml#L34-L41

Itamaeはgemであり、Gemfile.lockをリポジトリに含んではいません。ではこのOrbは使えないのかというとそうではなく、よく読むとわかるように bundler-version を渡すとGemfile.lockがなくても指定したversionのbundlerをインストールできるように見えます。 しかし既存のbundlerを上書いてしまってよいかの確認ダイアログが出るため、どちらにしろ上手く動きません。

bundlerを上書きできない

結局、bundlerのinstallは手で --force を付与したコマンドを実行させることで回避しました。

machine executorでRubyのversionが設定できない?

unit testはこれでうまくいくようになりましたが、dockerコマンドを使用する都合上、machine executorで実行しているintegration testでエラーが出るようになりました。それもJITを有効にしている場合のみ失敗します。

--jitが無効

エラーを見ると、--jit というオプションが不正というものでした。しかし、このスクリーンショットで使用しているRubyは2.6であり、--jit は有効なはずです。

不審に思い、Rubyのversionも出力するようにしたのが以下のスクリーンショットです。

Rubyのversionが一致しない

なんと、Ruby 2.6 をインストールしたはずなのに、使用されているのはRuby 2.3になっています。これはRVMのPATHをこねくりまわしてもどうしても解決することができませんでした。どうしてこんなことになるのでしょう。

setup_remote_dockerではvolume mountが使用できない

二進も三進もいかないので、remote dockerを使用してみることにしました。これは、unit testで使用しているDocker executorであればRubyのversionが正しく設定できているので、その環境においてdockerコマンドを使用できるようにするための仕組みです。

デプロイする Docker イメージを作成するには、セキュリティのために各ビルドに独立した環境を作成する特別な setup_remote_docker キーを使用する必要があります。 この環境はリモートで、完全に隔離され、Docker コマンドを実行するように構成されています。 ジョブで docker または docker-compose のコマンドが必要な場合は、.circleci/config.yml に setup_remote_docker ステップを追加します。

Docker コマンドの実行手順 - CircleCI

しかし、やはりエラーになってしまいます。これはテストの過程においてItamaeのコードをまるっとdocker container側にvolume mountしている部分があるのですが、remote dockerがvolume mountをサポートしていないためにエラーになります。

ジョブ空間からリモート Docker 内のコンテナにボリュームをマウントすること (およびその逆) はできません。 https://circleci.com/docs/ja/2.0/building-docker-images/#section=configuration

(docker cpはできますが)

という様々な躓きがあり、CircleCIを使うのはあきらめようと考えました。 (あきらめるまでに60回以上CIを回しています)

travis-ci は org から com へ移行できる

ではどこのCIを使おうか、となるのですが、そもそもtravis-ci.comへの統合が進められていることに気付きました。

2018年に、Travis CIはGitHub Appsとして導入できるようになり、OSSも travis-ci.com でCIを実行できるようになっています。

2020年の今でも travis-ci.org を使用しているので、試しに travis-ci.com に移行してみることにしました。

この移行はとても簡単で、 travis-ci.com にGitHub accountでログインしてMigrateボタンをクリックするだけです。

結果ですが、このように “Restart build” ボタンが出現しており、テストの再実行ができるようになりました!

Restartができる様子

まとめ

という諸々があり、ItamaeのCIは travis-ci.com に移行することになります。

これについて、第53回情報科学若手の会のLT枠で発表した資料を貼っておきます。

wakate2020
Tweet
2020年09月26日
2020年08月17日

UTF-8 の文字列をできる限り Shift_JIS に変換したい(実践編)

unicode nomalize by uconv

先日、きりきりやままさんがこのような記事を公開していました

UTF-8 の文字列をできる限り Shift_JIS に変換したい - きりきりやま

それでは実際にそのような文字列変換を行うにはどうすればよいのか、またコメントでiconvについて触れられていたので、この記事ではUnicodeにおけるNFKC正規化をどうやって行うのか試してみることにしました。

追記

Ruby

僕にとって文字列処理といえばRubyなので、まずは以下のようなscriptを書いてみました。

puts "\u304c"
puts "String#encode('Shift_JIS') => #{"\u304c".encode('Shift_JIS').inspect}"
puts "codepoints => #{"\u304c".codepoints}"
puts "NFKC normalized codepoints => #{"\u304c".unicode_normalize(:nfkc).codepoints}"
puts "Encode to SJIS with NFKC => #{"\u304c".unicode_normalize(:nfkc).encode('Shift_JIS').inspect}"

puts "=" * 20

puts "\u304b\u3099"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u304b\u3099".encode('Shift_JIS', undef: :replace).inspect}"
puts "codepoints => #{"\u304b\u3099".codepoints.inspect}"
puts "NFKC normalized codepoints => #{"\u304b\u3099".unicode_normalize(:nfkc).codepoints}"
puts "Encode to SJIS with NFKC => #{"\u304b\u3099".unicode_normalize(:nfkc).encode('Shift_JIS').inspect}"

puts "=" * 20

puts "\u0063\u006d"
puts "String#encode('Shift_JIS') => #{"\u0063\u006d".encode('Shift_JIS').inspect}"
puts "codepoints => #{"\u0063\u006d".codepoints}"
puts "NFKC normalized codepoints => #{"\u0063\u006d".unicode_normalize(:nfkc).codepoints}"

puts "=" * 20

puts "\u339d"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u339d".encode('Shift_JIS', undef: :replace).inspect}"
puts "codepoints => #{"\u339d".codepoints}"
puts "NFKC normalized codepoints => #{"\u339d".unicode_normalize(:nfkc).codepoints}"
$ ruby script.rb
が
String#encode('Shift_JIS') => "\x{82AA}"
codepoints => [12364]
NFKC normalized codepoints => [12364]
Encode to SJIS with NFKC => "\x{82AA}"
====================
が
String#encode('Shift_JIS', undef: :replace) => "\x{82A9}?"
codepoints => [12363, 12441]
NFKC normalized codepoints => [12364]
Encode to SJIS with NFKC => "\x{82AA}"
====================
cm
String#encode('Shift_JIS') => "cm"
codepoints => [99, 109]
NFKC normalized codepoints => [99, 109]
====================
㎝
String#encode('Shift_JIS', undef: :replace) => "?"
codepoints => [13213]
NFKC normalized codepoints => [99, 109]

https://wandbox.org/permlink/CQaSM6ffOHc0zLu6

Rubyにおいては、Unicode正規化を行うには String#unicode_normalize によって行うことができます。その際にoptionとして正規化の形式を指定することができます。とても簡単ですね。

https://docs.ruby-lang.org/ja/latest/class/String.html#I_UNICODE_NORMALIZE

Python

import unicodedata

print('\u304c (U+304c)')
print('codepoints => ', end='')
for char in '\u304c'.strip():
  print(hex(ord(char)) + ' ' , end='')
print()
print('NFKC normalized codepoints => ', end='')
for char in unicodedata.normalize('NFKC', '\u304c').strip():
  print(hex(ord(char)) + ' ' , end='')
print()
print('=' * 20)

print('\u304b\u3099 (U+304b U+3099)')
print('codepoints => ', end='')
for char in '\u304b\u3099'.strip():
  print(hex(ord(char)) + ' ' , end='')
print()
print('NFKC normalized codepoints => ', end='')
for char in unicodedata.normalize('NFKC', '\u304b\u3099').strip():
  print(hex(ord(char)), end='')
print()
print('=' * 20)

print('\u0063\u006d (U+0063 U+006d)')
print('codepoints => ', end='')
for char in '\u0063\u006d'.strip():
  print(hex(ord(char)) + ' ' , end='')
print()
print('NFKC normalized codepoints => ', end='')
for char in unicodedata.normalize('NFKC', '\u0063\u006d').strip():
  print(hex(ord(char)) + ' ' , end='')
print()
print('=' * 20)

print('\u339d (U+339d)')
print('codepoints => ', end='')
for char in '\u339d'.strip():
  print(hex(ord(char)) + ' ' , end='')
print()
print('NFKC normalized codepoints => ', end='')
for char in unicodedata.normalize('NFKC', '\u339d').strip():
  print(hex(ord(char)) + ' ' , end='')
print()

https://wandbox.org/permlink/cMc7S5blWLZLLObD

Pythonにおいては、unicodedata モジュールをインポートすることによって使用できる unicodedata.normalize により、形式を指定して正規化を行うことができます。

unicodedata — Unicode データベース — Python 3.8.5 ドキュメント

Go

package main

import (
    "fmt"

    "strings"

    "unicode/utf8"

    "golang.org/x/text/unicode/norm"
)

func printCodepoints(str string) {
    fmt.Print("codepoints => ")
    for i, w := 0, 0; i < len(str); i += w {
        runeValue, width := utf8.DecodeRuneInString(str[i:])
        fmt.Printf("%U ", runeValue)
        w = width
    }
    fmt.Print("\n")
}

func main() {
    fmt.Println("\u304c (U+304c)")
    printCodepoints("\u304c")

    fmt.Print("NFKC normalized ")
    printCodepoints(norm.NFKC.String("\u304c"))

    fmt.Println(strings.Repeat("=", 20))

    fmt.Println("\u304b\u3099 (U+204c u+3099)")
    printCodepoints("\u304b\u3099")

    fmt.Print("NFKC normalized ")
    printCodepoints(norm.NFKC.String("\u304b\u3099"))

    fmt.Println(strings.Repeat("=", 20))

    fmt.Println("\u0063\u006d (U+0063 U+006d)")
    printCodepoints("\u0063\u006d")

    fmt.Print("NFKC normalized ")
    printCodepoints(norm.NFKC.String("\u0063\u006d"))

    fmt.Println(strings.Repeat("=", 20))

    fmt.Println("\u339d (U+339d)")
    printCodepoints("\u339d")

    fmt.Print("NFKC normalized ")
    printCodepoints(norm.NFKC.String("\u339d"))
}

https://play.golang.org/p/xG255G32mlJ

Goでは、norm packageを使用することで正規化を行うことができます。

JavaScript

// function from https://jsprimer.net/basic/string-unicode/#code-point-is-not-code-unit
function convertCodeUnits(str) {
    const codeUnits = [];
    for (let i = 0; i < str.length; i++) {
        codeUnits.push(str.charCodeAt(i).toString(16));
    }
    return codeUnits;
}


console.log('\u304c (U+304c)')
console.log('codepoints => ' + convertCodeUnits('\u304c'))
console.log('NFKC normalized codepoints => ' + convertCodeUnits('\u304c'.normalize('NFKC')))
console.log('=' .repeat(20))


console.log('\u304b\u3099 (U+304b U+3099)')
console.log('codepoints => ' + convertCodeUnits('\u304b\u3099'))
console.log('NFKC normalized codepoints => ' + convertCodeUnits('\u304b\u3099'.normalize('NFKC')))
console.log('='.repeat(20))

console.log('\u0063\u006d (U+0063 U+006d)')
console.log('codepoints => ' + convertCodeUnits('\u0063\u006d'))
console.log('NFKC normalized codepoints => ' + convertCodeUnits('\u0063\u006d'.normalize('NFKC')))
console.log('='.repeat(20))


console.log('\u339d (U+339d)')
console.log('codepoints => ' + convertCodeUnits('\u339d'))
console.log('NFKC normalized codepoints => ' + convertCodeUnits('\u339d'.normalize('NFKC')))
console.log('='.repeat(20))

https://wandbox.org/permlink/JLQH8LasdQo9ewgS

JavaScriptでは、 String.prototype.normalize() によって正規化を行うことができます。

それでは他のツールはどうでしょうか。

nkf

nkfはNetwork Kanji Filterの略で、古くからある文字コード変換ツールです。

https://ja.osdn.net/projects/nkf/

Rubyはnkfを同梱しているので、手軽に試すことができます。今回は一度Shift_JISに変換してからUTF-8に戻すことで、正しく変換できているかを確認してみます。

require 'kconv' # kconvはnkfのラッパーです

puts "\u304c (U+304c)"
puts "Endoce to SJIS by nkf => #{"\u304c".tosjis.inspect}"
puts "=" * 20

puts "\u304b\u3099 (U+304b U+3099)"
puts "Endoce to SJIS to UTF-8 by nkf => #{"\u304b\u3099".tosjis.toutf8}"

puts "=" * 20

puts "\u0063\u006d (U+0063 U+006d)"
puts "Endoce to SJIS by nkf => #{"\u0063\u006d".tosjis.inspect}"

puts "=" * 20

puts "\u339d (U+339d)"
puts "Endoce to SJIS to UTF-8 by nkf => #{"\u339d".tosjis.toutf8}"
$ ruby script.rb
が (U+304c)
Endoce to SJIS by nkf => "\x{82AA}"
====================
が (U+304b U+3099)
Endoce to SJIS to UTF-8 by nkf => 縺九y
====================
cm (U+0063 U+006d)
Endoce to SJIS by nkf => "cm"
====================
㎝ (U+339d)
Endoce to SJIS to UTF-8 by nkf => ㎝

このように、「が」(U+304B U+3099) の変換に失敗していることがわかります。そもそもnkfはUnicodeにおける正規化形式を指定できるのでしょうか。

nkfは2006-03-27にリリースされた 2.0.6 以降 (正確には2.0.6-beta2以降) においてUnicodeの正規化に対応するようになりましたが、「UTF8-MACの範囲のみ」と明言されています。

ここでの UTF-8-MAC は、macOSがAPFS以前1に採用していた HFS+ というファイルシステムにおいて使用されている正規化形式の通称2で、一見NFD形式のようで互換性のない正規化3を行っています。

nkfは入力においてのみUTF-8-MACを受け付けるようになっているようで、他の正規化形式に対応していません。

nkfにオプションから文字コードを指定した変換をして確かめてみましょう。

# nkf.rbとして保存
require 'nkf'

puts "\u304b\u3099 : U+304b U+3099"
puts "nkf --ic=UTF-8 --oc=Shift_JIS"
ga_to_sjis_from_utf8 = NKF.nkf('--ic=UTF-8 --oc=Shift_JIS', "\u304b\u3099")
puts ga_to_sjis_from_utf8.inspect
puts ga_to_sjis_from_utf8.encode('UTF-8')

puts "=" * 20

puts "nkf --ic=UTF-8-MAC --oc=Shift_JIS"
ga_to_sjis_from_utf8mac = NKF.nkf('--ic=UTF-8-MAC --oc=Shift_JIS', "\u304b\u3099")
puts ga_to_sjis_from_utf8mac.inspect
puts ga_to_sjis_from_utf8mac.encode('UTF-8')

puts "=" * 20

puts "\ufa19 (U+fa19)"
puts "NFD normalized => #{"\ufa19".unicode_normalize(:nfd).inspect}"
puts "NFKC normalized => #{"\ufa19".unicode_normalize(:nfkc).inspect}"
puts "nkf convert => #{NKF.nkf('--ic=UTF-8-MAC --oc=UTF-8', "\ufa19").inspect}"
$ ruby nkf.rb 
が : U+304b U+3099
nkf --ic=UTF-8 --oc=Shift_JIS
"\x{82A9}"
か
====================
nkf --ic=UTF-8-MAC --oc=Shift_JIS
"\x{82AA}"
が
====================
神 (U+fa19)
NFD normalized => "\u795E"
NFKC normalized => "\u795E"
nkf convert => "\uFA19"

ところで、㎝ (U+339D)の変換にも失敗しそうな気がしますが、成功しています。これはどういうことなのでしょうか。 Shift_JISに含まれる文字列の集合はJIS X 0201とJIS X 0208です。このどちらにも1文字で"cm"となる字体は定義されていません。4ではこの「㎝」はどこからやってきたのでしょうか。

「㎝」はNEC特殊文字に含まれており、NECやIBMによるShift_JIS拡張が統合された文字コードであるWindows-31Jに含まれています。これをCP932と呼ぶこともあり5、CP932からUnicodeへの文字変換表には CP932における 0x8770 をUnicodeでの 0x339D に変換すると定義されています。

https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT

Shift_JISの規定において、0x8770 は「保留域」となっています。6 このことからも、JISに規定されているShift_JISにはなく、それの拡張であるWindows-31Jに含まれている文字であることがわかります。7

また余談として、JIS X 0213にて規定されたShift_JISX0213における 0x8770 に「㎝」の字形が含まれています。8

これもnkfで確認することができ、CP932において拡張された文字を扱わないオプション --no-cp932ext を指定することで文字が消えていることが確かめられます。

require 'nkf'
puts "\u339d : U+339d"
cm_to_sjis_from_utf8 = NKF.nkf('--ic=UTF-8 --oc=Shift_JIS --no-cp932ext', "\u339d")
puts cm_to_sjis_from_utf8.inspect
puts cm_to_sjis_from_utf8.encode('UTF-8').inspect
puts "=" * 20
$ ruby nkf.rb
㎝ : U+339d
""
""
====================

nkfでは、事前にNFKC正規化を行ってからでないと正しくShift_JISに変換できないことがわかりました。

iconv

iconvは、以前はRubyの標準添付ライブラリでしたが、2.0で削除されました。

https://www.ruby-lang.org/ja/news/2013/02/24/ruby-2-0-0-p0-is-released/

現在でもgemとしてインストールできるようにはなっていますが、String#encode を使用することが推奨されているので、今回はコマンドラインの結果をみることにします。

https://github.com/ruby/iconv

# iconv.rbとして保存
require 'open3'

puts "\u304c (U+304c)"
puts "String#encode('Shift_JIS') => #{"\u304c".encode('Shift_JIS').inspect}"
Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFT-JIS') do |stdin, stdout_e, _|
  stdin.print "\u304c"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by iconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end

puts "=" * 20

puts "\u304b\u3099 (U+304b U+3099)"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u304b\u3099".encode('Shift_JIS', undef: :replace).inspect}"
Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFT-JIS') do |stdin, stdout_e, _|
  stdin.print "\u304b\u3099"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by iconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end

puts "=" * 20

puts "\u0063\u006d (U+0063 U+006d)"
puts "String#encode('Shift_JIS') => #{"\u0063\u006d".encode('Shift_JIS').inspect}"
Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFT-JIS') do |stdin, stdout_e, _|
  stdin.print "\u0063\u006d"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by iconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end

puts "=" * 20

puts "\u339d (U+339d)"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u339d".encode('Shift_JIS', undef: :replace).inspect}"
Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFT-JIS') do |stdin, stdout_e, _|
  stdin.print "\u339d"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by iconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end

Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFTJISX0213') do |stdin, stdout_e, _|
  stdin.print "\u339d"
  stdin.close
  result = stdout_e.read
  puts "Convert to ShiftJISX0213 by iconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('CP932').encode('UTF-8')}"
end
$ bundle exec ruby iconv.rb
が (U+304c)
String#encode('Shift_JIS') => "\x{82AA}"
Convert to Shift_JIS by iconv => "\x82\xAA"
Re-convert to UTF-8 => が
====================
が (U+304b U+3099)
String#encode('Shift_JIS', undef: :replace) => "\x{82A9}?"
Convert to Shift_JIS by iconv => "\x82\xA9iconv: illegal input sequence at position 3\n"
Re-convert to UTF-8 => かiconv: illegal input sequence at position 3
====================
cm (U+0063 U+006d)
String#encode('Shift_JIS') => "cm"
Convert to Shift_JIS by iconv => "cm"
Re-convert to UTF-8 => cm
====================
㎝ (U+339d)
String#encode('Shift_JIS', undef: :replace) => "?"
Convert to Shift_JIS by iconv => "iconv: illegal input sequence at position 0\n"
Re-convert to UTF-8 => iconv: illegal input sequence at position 0
Convert to ShiftJISX0213 by iconv => "\x87p"
Re-convert to UTF-8 => ㎝

nkfと同様に「が」(U+304B U+3099) の変換に失敗している様子がわかります。「か」までの出力には成功していることから、濁点 U+3099 の変換に失敗していそうですね。 またnkfについての説明で触れた「㎝」 (U+339D) については、Shift_JISへの変換は失敗していますが、Shift_JISX0213への変換は成功していますね。

他に指定できそうなoptionもないので、iconvでも事前にNFKC正規化しておく必要がありそうです。

uconv

それでは、Rubyを使用せずコマンドラインから使用できる、Unicodeの正規化形式も扱うことのできるツールはないのでしょうか?

これを行うことのできる uconv というものがあります。これはUnicode Consortiumが保守しているInternational Components for Unicodeというコンポーネント(?)に含まれており、Debianにおいては icu-devtools というパッケージ名で入手できます。

https://packages.debian.org/buster/icu-devtools

uconvに対して -x nfkc というふうに正規化形式を指定する(正確には、適用したいTransliterationを指定する)ことによって、NFKC正規化がされた上で文字コードの変換ができます。

require 'open3'

puts "\u304c (U+304c)"
puts "String#encode('Shift_JIS') => #{"\u304c".encode('Shift_JIS').inspect}"
Open3.popen2e('uconv --from-code UTF-8 --to-code Shift_JIS -x nfkc') do |stdin, stdout_e, _|
  stdin.print "\u304c"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by uconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end

puts "=" * 20

puts "\u304b\u3099 (U+304b U+3099)"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u304b\u3099".encode('Shift_JIS', undef: :replace).inspect}"
Open3.popen2e('uconv --from-code UTF-8 --to-code Shift_JIS -x nfkc') do |stdin, stdout_e, _|
  stdin.print "\u304b\u3099"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by uconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end

puts "=" * 20

puts "\u0063\u006d (U+0063 U+006d)"
puts "String#encode('Shift_JIS') => #{"\u0063\u006d".encode('Shift_JIS').inspect}"
Open3.popen2e('uconv --from-code UTF-8 --to-code Shift_JIS -x nfkc') do |stdin, stdout_e, _|
  stdin.print "\u0063\u006d"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by uconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end

puts "=" * 20

puts "\u339d (U+339d)"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u339d".encode('Shift_JIS', undef: :replace).inspect}"
Open3.popen2e('uconv --from-code UTF-8 --to-code Shift_JIS -x nfkc') do |stdin, stdout_e, _|
  stdin.print "\u339d"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by uconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8').inspect}"
  puts "codepoints => #{result.codepoints}"
end

Open3.popen2e('uconv --from-code UTF-8 --to-code cp932') do |stdin, stdout_e, _|
  stdin.print "\u339d"
  stdin.close
  result = stdout_e.read
  puts "Convert to CP932 by uconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('CP932').encode('UTF-8')}"
  puts "codepoints => #{result.codepoints}"
end
$ bundle exec ruby uconv.rb
が (U+304c)
String#encode('Shift_JIS') => "\x{82AA}"
Convert to Shift_JIS by uconv => "\x82\xAA"
Re-convert to UTF-8 => が
====================
が (U+304b U+3099)
String#encode('Shift_JIS', undef: :replace) => "\x{82A9}?"
Convert to Shift_JIS by uconv => "\x82\xAA"
Re-convert to UTF-8 => が
====================
cm (U+0063 U+006d)
String#encode('Shift_JIS') => "cm"
Convert to Shift_JIS by uconv => "cm"
Re-convert to UTF-8 => cm
====================
㎝ (U+339d)
String#encode('Shift_JIS', undef: :replace) => "?"
Convert to Shift_JIS by uconv => "cm"
Re-convert to UTF-8 => "cm"
codepoints => [99, 109]
Convert to CP932 by uconv => "\x87p"
Re-convert to UTF-8 => ㎝
codepoints => [34672]

このように、NFKC正規化を行ったうえで、正しくShift_JISに変換できていることが、またCP932を指定したときには正規化を行わなくても「㎝」 (U+339D)を1文字のまま相互に変換できていることがわかります。

ちなみにTransliterationを活用するとこのようにひらがなをローマ字に変換するという面白いこともできます。

$ echo おはようございます | uconv -x '::hiragana-latin;'
ohayougozaimasu

余談 Unicodeにおける正規化形式について

元記事や現在において指定できる正規化形式、NFC、NFD、NFKC、NFKDの4つは、それぞれの名前が規格に登場するのはUnicode 3.0.1からであり、そのリリースは 2000-08-31 です。

https://web.archive.org/web/20050211134342/http://www.unicode.org/unicode/reports/tr15/tr15-19.html

それ以前のリリースにおいては、正規化形式について触れられている記述がなく、前述のUnicode Standard Annex #15から参照できる"Previous Version" においても、 “It is a stable document and may be used as reference material” などの記述が存在しないことから、正規化形式というものが存在するのはUnicode 3.0.1 以降ということになります。

https://web.archive.org/web/20050207015030/http://www.unicode.org/unicode/reports/tr15/tr15-18.html

このあたりの話は、技術評論社から出版されている[改訂新版]プログラマのための文字コード技術入門 の Appendix 4「Unicodeの諸問題」にて詳細に説明されています。この本はとても面白いのでぜひ読んでみてください。

まとめ

あるUnicode文字列に対してNFKCなどの正規化を適用したい場合、Rubyでは String#unicode_normalize にオプションとして、コマンドラインでは uconv を使用することで目的を達成できます。文字コード変換で良く知られるnkfやiconvでは適切に正規化が行われていない文字列を変換することができません。


  1. ではAPFSではどうなのかというと、規格書には j_drec_hashed_jey_t 構造体に格納される name_len_and_hash を計算するときにNFD正規化を行うとこが記載されていますが、ファイル名そのものの正規化についての記述は見付かりませんでした。 https://developer.apple.com/support/downloads/Apple-File-System-Reference.pdf 正規化が行われず、与えられたバイト列をそのまま保持するようになっているのか、同じ名前に見えるファイルを複数作成することができるという記事もあります。 https://eclecticlight.co/2017/04/06/apfs-is-currently-unusable-with-most-non-english-languages/ 

  2. 軽く目を通しましたが、Apple側でこの正規化形式に名前をつけたりはしていないようです。 https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFileSystem/BPFileSystem.html#//apple_ref/doc/uid/10000185-SW1 ただし、Appleの配布しているiconvのencodingには UTF-8-MAC を指定できるので、ほぼ公式だとしていいでしょう。 

  3. https://developer.apple.com/library/archive/qa/qa1173/_index.html より 

  4. JIS X 0208-1997 附属書3 表1より https://www.jisc.go.jp/pdfb6/PDFView/ShowPDF/5gMAAIAz9AGm_33fhhRp 

  5. 「呼ぶこともあり」というのは、当初定められたCP932をいくつかのベンダが独自拡張したあと、それをMicrosoftが統合したWindows-31JのこともCP932と呼ぶからです。現代においてはCP932 = Windows31J としていいと思いますが。 

  6. JIS X 0208-1997 附属書1より https://www.jisc.go.jp/pdfa8/PDFView/ShowPDF/7gIAAKR6fwk8ZKtaZttC 

  7. MSDNに記載されていたCP932の文字一覧より https://web.archive.org/web/20180405180457/https://msdn.microsoft.com/en-us/library/cc194892.aspx 

  8. JIS X 0213-2000 附属書4 表23より https://www.jisc.go.jp/pdfa5/PDFView/ShowPDF/5AIAAHLePslwm6mc6z4g 

Tweet
2020年08月17日
2020年07月19日

unasuke.fmの再始動

unasuke.fm

再始動

https://unasuke.fm を公開しました。ロゴは衰咲ふち(@otoroesaki)さんに作成していただきました。この場を借りてお礼申しあげます。ありがとうございます。

一旦は今までのエピソードを聞けるようにしただけになります。今後、エピソードごとのサマリやshownoteの記載、RSS feedの公開などの改善を進めていきます。

加えて、次の収録についても日程含め検討中になります。

プラットフォームに乗るかどうか

Podcastは、なぜだかまたブームが来ているようで、AnchorやTransistor、stand.fmなどのプラットフォームが登場しています。 プラットフォームに依存することで、労力の削減や便利な機能を使うことができますが、自分はいちから構築することにしました。

自分が発信する場を他人に委ねる怖さ、というものも多分どこかにあって、一度Twitterのアカウントが凍結されたときにはそれはもう気が気ではありませんでした。ブログをMiddlemanでbuildしてS3にホストしているのも、大して有効活用できていないMastodonを維持し続けているのも、そのためかもしれません。

Next.js を選択した理由と、時間がかかった理由

今回は今までの自分の選択であるMiddlemanによる構築ではなく、Next.jsによる構築を選択しました。

理由は、Webサイトをつくるのであれば、Webの一級市民言語であるJavaScriptでつくるほうがいいのではないかという判断、自分の技術の幅を広げたくて、まだ仕事でも触ったことのないReactに慣れておきたかったという学習目的の2つが主です。また、Next.js を選択するのであれば、素JavaScriptよりは、TypeScriptで書くほうがいいと思い、そのようにしました。

そうなると、TypeScriptとReactとNext.jsの3つを初めて触ることになるので、2つのことを同時に学ばない (by ところてん)ようにするどころか3つのことを同時に学ぼうとしてしまっています。 実際、書いては思うように動かず、何度も友人に助けを求めたり、ひたすらWeb上の記事を読んだりして、それでも全然わからず、多忙を言い訳にしてしばらく放置してしまったりしました。

image.png (23.1 kB)

半分エタりかけていましたが、公式ドキュメントを何度も何度も読んでいくうちになんだかある程度「わかる」ようになりました。結局自分は、悩んで泥臭く試行錯誤を繰り返して手を動かさないと技術を身に付けることはできないんじゃないかと思います。

正直まだまだ機能面や見た目の面でも足りていない部分が多く構築中ですが、オリジナルのファイルがSoundCloud上にもう存在しないので、とりあえずWeb上に参照可能な形で復活させることを優先しました。RSSもないのでPodcastとしては片手落ちですが、"Done is better than perfect" と言いますし、まずは公開することにしました。

使用している技術

まず、Next.jsを静的サイトジェネレーターとして採用しました。そうなると必然、Viewを記述するためにReactを採用することになります。また前述のように、主にTypeScriptで記述しています。ESLintとPrettierを用いてコードフォーマットをしています。

音声ファイルのホスティングについては、転送量による課金のないWasabiを、WebサイトそのもののホスティングについてはFirebase Hostingを選択し、DeployはGitHub Actions経由で行うようにしました。確認用にmaster branchをNetlifyにてHostingしています。

また Content Security Policy を有効化にし、Report Onlyの状態にして report-uri.io に収集させるようにしました。

余談ですが、開発はほぼ全てをWindows 10(not WSL)上で行いました。開発環境において特にハマることはありませんでしたが、ESLintのruleに改行コードをUnix styleかWindows Styleかに設定するものがあり、これの扱いが悩ましいところでした。

参考にした記事・コード

Tweet
2020年07月19日
2020年06月30日

パソコンが壊れて、直した話

windows

前提条件

起こったこと

pic.twitter.com/Fm7KgLQe77

— うなすけ (@yu_suke1994) June 28, 2020

まずはこの動画の状況になるまでの出来事を書いていきます。

  1. Windows Updateが来ているので実行する
  2. Windows Updateの途中で何度か再起動される
  3. 再起動のどこかのタイミングで、「Synaptics ドライバーのアンインストールに失敗しました」というエラーメッセージが出て、そこで止まってしまった
    1. 文言はあやふや、今思えばこの写真を撮っておくべきだった。
  4. どのような操作もできないまま固まってしまったので泣く泣く電源ボタン長押しによる中断
    1. 多分これが原因だが、他にどうすればよかったのだろうか。
  5. 再度起動すると、「前回のWindows Updateを修復しています」、のようなメッセージが出る
  6. ログイン画面に遷移するが、一切の操作ができない状態になる
    1. マウス操作不可能、キー入力不可能
  7. 電源ボタン操作で終了、起動し、UEFIメニューから回復オプションに入る
  8. システムの復元ポイントからWindows Updateを実行する前の状態に戻すも状況変化せず
  9. 「インストールされている Windows Update を削除する」を実行するも状況変化せず
  10. 「インストール メディアを使用して PC を復元する」も効果なし
  11. 別のマウス、キーボードを接続するも認識せず
  12. セーフモードで起動するも状況変化せず
  13. セーフモードで起動した結果、UEFIメニューに入れなくなる
    1. 起動直後のキー入力がすっ飛ばされるようになる。これ仕様なの?
  14. 詰み……

一度セーフモードに入ると抜けられなくなるというのは結構な絶望感がありました。

どう直したか

  1. セーフモード画面のまま放置して電池が切れるのを待つ
    1. ここで一旦寝る
  2. 起動し、UEFIメニューに入れること、回復オプションに入れることを確認
    1. これは賭けだったが、入れてよかった。
  3. 「個人用ファイルを保持する」設定で「PC を初期状態に戻す」を実行
  4. キー入力を受け付けるようになり、復活!

という流れだったのでした。イチから環境構築をやりなおしになってしまいましたが、ひとまず直ってよかったです。

※ この記事は復旧させたWindowsのWSL上で書かれました。

Tweet
2020年06月30日
2020年05月31日

Itamae v1.10.8 リリース

itamae v1.10.8

久々に自分がReleaseしたので、なにが入ったのかを軽くまとめます。

mergeしたもの

Print “(in action_XXX)” as a debug log by pocke · #315

debug logにどのactionによる実行なのかを表示するものです。descriptionに貼ってあるlogを見るとわかりやすいですね。

Reduce check_package_is_installed calling when package version is not specified by pocke · #314

package resourceにおいて、そのresourceのversionが指定されていない場合には不要となる処理を実行せずfast returnすることによって処理の高速化を図るものです。具体的には。installするactionにおいては、versionが指定されていない場合にはどのversionが存在するかの確認はしなくていいですし、removeするactionにおいてはinstallされているかどうかのみ確認すればよいだけです。 patch authorによるbenchmarkでは1.3倍高速になったとか。

Simplify Git resource’s get_revision method by pocke · #313

git resourceにおいて、git rev-list の最初行を取得するのではなく、git rev-parse の結果を使用するように変更するものです。無駄な出力をさせなくてよくなり、実行速度も少し向上するようです。

mergeしなかったもの

Fix to set “verifyhostkey” based on “stricthostkey_checking” value by musaprg · #312

SSH時の設定について、 StrictHostKeyCheckingno に設定されている場合にはその設定値を削除し、代わりにverify_host_key:never に設定するものです。 net-ssh gemの v6.0.2 において ArgumentError: invalid option(s): strict_host_key_checking が出てしまったことからいただいたpull requestではあるのですが、itamaeが依存しているSpecinfra側で対応がされたため、こちらでは特に対処しないこととしました。

Remove stricthostkey_checking option when net-ssh does not support it by mizzy · Pull Request #717 · mizzy/specinfra

蛇足

基本的に僕のレスが遅れがちでちょっと申し訳なさがあります。そして6月もリリースする予定です。

Tweet
2020年05月31日
2020年04月17日

Rubyのdistroless imageをmagicpakで構築できるのか?

distroless-ruby

こんにちは、趣味でこのようなものを作っている者です。

https://github.com/unasuke/distroless-ruby

作るだけ作って、実用はしていませんけど。

Distrolessとは何か、ということについては以下の記事をご参照ください。

distrolessイメージを使って、ランタイムDockerイメージを作ってみる - Qiita

さて私がこれまでこのimageをどのように作成していたのかと言いますと、この記事に書いたように人力で依存関係を調べて頑張っていました。 Rubyが動くdistroless image は作ることができるのか - Qiita

Ruby 2.6までは順調だったのですが、2.7ではリンクされるライブラリが増えたのかなんなのか、これまでのように単純なバージョンの変更では上手くいかずに手が止まってしまっていました。

そこに登場したのがこのツール、magicpakです。

これを使えば、Rubyが実行するために必要なファイルを列挙でき、distrolessなimageの作成が楽になるのではないかと考え、試してみました。

magicpak -v $(which ruby)

では早速次のようなDockerfileを作成し、buildの様子を眺めてみます。

FROM ruby:2.7.0-buster
ADD https://github.com/coord-e/magicpak/releases/latest/download/magicpak-x86_64-unknown-linux-musl /usr/bin/magicpak
RUN chmod +x /usr/bin/magicpak
RUN /usr/bin/magicpak -v $(which ruby) /bundle

docker buildが……

Sending build context to Docker daemon   2.09MB
Step 1/4 : FROM ruby:2.7.0-buster as ruby
 ---> ea1d77821a3c
Step 2/4 : ADD https://github.com/coord-e/magicpak/releases/latest/download/magicpak-x86_64-unknown-linux-musl /usr/bin/magicpak
Downloading [==================================================>]  4.222MB/4.222MB
 ---> bd6ae35e14fe
Step 3/4 : RUN chmod +x /usr/bin/magicpak
 ---> Running in e75094fba6fd
Removing intermediate container e75094fba6fd
 ---> 8280a91988f3
Step 4/4 : RUN /usr/bin/magicpak -v $(which ruby) /bundle
 ---> Running in 818a39c7466f
[INFO] exe: loading /usr/local/bin/ruby
[INFO] action: bundle shared object dependencies of /usr/local/bin/ruby
[INFO] exe: loading /usr/local/lib/libruby.so.2.7

################ snip #############################

[INFO] exe: using default interpreter /lib64/ld-linux-x86-64.so.2
[INFO] action: bundle executable /usr/local/bin/ruby as None
[INFO] action: emit /bundle
[INFO] action: emit: creating /bundle as it does not exist
[INFO] emit: link /lib/x86_64-linux-gnu/libpthread-2.28.so => /bundle/lib/x86_64-linux-gnu/libpthread.so.0
[INFO] emit: copy /lib/x86_64-linux-gnu/libpthread-2.28.so => /bundle/lib/x86_64-linux-gnu/libpthread-2.28.so
[INFO] emit: link /usr/local/lib/libruby.so.2.7.0 => /bundle/usr/local/lib/libruby.so.2.7
[INFO] emit: copy /usr/local/lib/libruby.so.2.7.0 => /bundle/usr/local/lib/libruby.so.2.7.0
[INFO] emit: link /lib/x86_64-linux-gnu/libdl-2.28.so => /bundle/lib/x86_64-linux-gnu/libdl.so.2
[INFO] emit: copy /lib/x86_64-linux-gnu/libdl-2.28.so => /bundle/lib/x86_64-linux-gnu/libdl-2.28.so
[INFO] emit: link /lib/x86_64-linux-gnu/libm-2.28.so => /bundle/lib/x86_64-linux-gnu/libm.so.6
[INFO] emit: copy /lib/x86_64-linux-gnu/libm-2.28.so => /bundle/lib/x86_64-linux-gnu/libm-2.28.so
[INFO] emit: link /lib/x86_64-linux-gnu/ld-2.28.so => /bundle/lib64/ld-linux-x86-64.so.2
[INFO] emit: copy /lib/x86_64-linux-gnu/ld-2.28.so => /bundle/lib/x86_64-linux-gnu/ld-2.28.so
[INFO] emit: link /lib/x86_64-linux-gnu/libz.so.1.2.11 => /bundle/lib/x86_64-linux-gnu/libz.so.1
[INFO] emit: copy /lib/x86_64-linux-gnu/libz.so.1.2.11 => /bundle/lib/x86_64-linux-gnu/libz.so.1.2.11
[INFO] emit: link /lib/x86_64-linux-gnu/libcrypt-2.28.so => /bundle/lib/x86_64-linux-gnu/libcrypt.so.1
[INFO] emit: copy /lib/x86_64-linux-gnu/libcrypt-2.28.so => /bundle/lib/x86_64-linux-gnu/libcrypt-2.28.so
[INFO] emit: link /lib/x86_64-linux-gnu/librt-2.28.so => /bundle/lib/x86_64-linux-gnu/librt.so.1
[INFO] emit: copy /lib/x86_64-linux-gnu/librt-2.28.so => /bundle/lib/x86_64-linux-gnu/librt-2.28.so
[INFO] emit: link /usr/lib/x86_64-linux-gnu/libgmp.so.10.3.2 => /bundle/usr/lib/x86_64-linux-gnu/libgmp.so.10
[INFO] emit: copy /usr/lib/x86_64-linux-gnu/libgmp.so.10.3.2 => /bundle/usr/lib/x86_64-linux-gnu/libgmp.so.10.3.2
[INFO] emit: copy /usr/local/bin/ruby => /bundle/usr/local/bin/ruby
[INFO] emit: link /lib/x86_64-linux-gnu/libc-2.28.so => /bundle/lib/x86_64-linux-gnu/libc.so.6
[INFO] emit: copy /lib/x86_64-linux-gnu/libc-2.28.so => /bundle/lib/x86_64-linux-gnu/libc-2.28.so
Removing intermediate container 818a39c7466f
 ---> 077d681487ae
Successfully built 077d681487ae

多いですね。これを /bundle にまとめて展開すれば動くのでしょうか?次のようなDockerfileを使って試してみましょう。

FROM ruby:2.7.0-buster as ruby

ADD https://github.com/coord-e/magicpak/releases/latest/download/magicpak-x86_64-unknown-linux-musl /usr/bin/magicpak
RUN chmod +x /usr/bin/magicpak
RUN /usr/bin/magicpak -v $(which ruby) /bundle

FROM gcr.io/distroless/base-debian10

COPY --from=ruby /bundle /.
RUN ["/usr/local/bin/ruby", "-v"]
RUN ["/usr/local/bin/gem", "install", "sinatra"]

buildしてみると……

# ...snip...
Step 5/8 : FROM gcr.io/distroless/base-debian10
 ---> 5bb0e81ff6e4
Step 6/8 : COPY --from=ruby /bundle /.
 ---> f2a8875bdfdc
Step 7/8 : RUN ["/usr/local/bin/ruby", "-v"]
 ---> Running in 2cce67b240b1
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]
Removing intermediate container 2cce67b240b1
 ---> 145cba6957e9
Step 8/8 : RUN ["/usr/local/bin/gem", "install", "sinatra"]
 ---> Running in e445421dbc07
OCI runtime create failed: container_linux.go:349: starting container process caused "exec: \"/usr/local/bin/gem\": stat /usr/local/bin/gem: no such file or directory": unknown

/usr/local/bin/gem がないようです。確かにmagicpakの対象にしたのはrubyコマンドのみで、gemやbundlerに対しては何もしていません。これらも含めてあげましょう。

FROM ruby:2.7.0-buster as ruby

ADD https://github.com/coord-e/magicpak/releases/latest/download/magicpak-x86_64-unknown-linux-musl /usr/bin/magicpak
RUN chmod +x /usr/bin/magicpak
RUN /usr/bin/magicpak -v $(which ruby) /bundle

FROM gcr.io/distroless/base-debian10

COPY --from=ruby /bundle /.
COPY --from=ruby /usr/local/bin/ /usr/local/bin # ここを追加
RUN ["/usr/local/bin/ruby", "-v"]
RUN ["/usr/local/bin/gem", "install", "sinatra"]

結果は……

Step 9/9 : RUN ["/usr/local/bin/gem", "install", "sinatra"]
 ---> Running in e638dbdb65fd
<internal:gem_prelude>:1:in `require': cannot load such file -- rubygems.rb (LoadError)
        from <internal:gem_prelude>:1:in `<internal:gem_prelude>'
The command '/usr/local/bin/gem install sinatra' returned a non-zero code: 1

こんどは rubygems.rb がみつかりません。これは /usr/local/lib/ruby/以下にあります。これも含めます。

FROM ruby:2.7.0-buster as ruby

ADD https://github.com/coord-e/magicpak/releases/latest/download/magicpak-x86_64-unknown-linux-musl /usr/bin/magicpak
RUN chmod +x /usr/bin/magicpak
RUN /usr/bin/magicpak -v $(which ruby) /bundle

FROM gcr.io/distroless/base-debian10

COPY --from=ruby /bundle /.
COPY --from=ruby /usr/local/bin/ /usr/local/bin
COPY --from=ruby /usr/local/lib/ruby/ /usr/local/lib/ruby # ここを追加
RUN ["/usr/local/bin/ruby", "-v"]
RUN ["/usr/local/bin/gem", "install", "sinatra"]

さてどうか……

Step 10/10 : RUN ["/usr/local/bin/gem", "install", "sinatra"]
 ---> Running in 888f7427612a
/usr/local/lib/ruby/2.7.0/yaml.rb:3: warning: It seems your ruby installation is missing psych (for YAML output).
To eliminate this warning, please install libyaml and reinstall your ruby.
/usr/local/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:92:in `require': libyaml-0.so.2: cannot open shared object file: No such file or directory - /usr/local/lib/ruby/2.7.0/x86_64-linux/psych.so (LoadError)

libyamlが見付からないというエラーが出ました。gemコマンドに対してはmagicpakを通してないからでしょうか。ちなみにそれをやってもエラーが出ます。

root@4033859f93d6:/# ./magicpak -v /usr/local/bin/gem /bundle
[INFO] exe: loading /usr/local/bin/gem
[ERROR] error: The executable is malformed: unknown magic number: 7795575320214446371

とりあえず、libyamlを足してやると……

FROM ruby:2.7.0-buster as ruby

ADD https://github.com/coord-e/magicpak/releases/latest/download/magicpak-x86_64-unknown-linux-musl /usr/bin/magicpak
RUN chmod +x /usr/bin/magicpak
RUN /usr/bin/magicpak -v $(which ruby) /bundle

FROM gcr.io/distroless/base-debian10

COPY --from=ruby /bundle /.
COPY --from=ruby /usr/lib/x86_64-linux-gnu/libyaml* /usr/lib/x86_64-linux-gnu/ # ここを追加
COPY --from=ruby /usr/local/bin/ /usr/local/bin
COPY --from=ruby /usr/local/lib/ruby/ /usr/local/lib/ruby
RUN ["/usr/local/bin/ruby", "-v"]
RUN ["/usr/local/bin/gem", "install", "sinatra"]

今度は……

6 gems installed
Removing intermediate container 1a7823880c0d
 ---> 680de9256898
Successfully built 680de9256898
Successfully tagged unasuke/distroless-ruby:2.7.0-buster

sinatraがインストールできました!ここでは省きますが、ちゃんとリクエストも受けつけるようになっています。imageのサイズは次のようになりました。

image size
ruby:2.7.0-buster 842MB
ruby:2.7.0-slim-buster 149MB
ruby:2.7.0-alpine 51.4MB
rubylang/ruby:2.7.0-bionic 359MB
unasuke/distroless-ruby:2.7.0-buster 59.4MB

alpineには敵わないものの、slim-busterのおよそ半分のサイズになりました。まあ、実用性については疑問点が残りますが。

まとめ

magickpakを使うことで、比較的楽にdistroless-rubyのDocker imageを作成することができました(それまでのtry-and-errorはこの比ではなかったので……)。distroless自体は、Goなどのシングルバイナリが動けばいいimageを作るときには便利に使えるのではないかと思います。

GoogleContainerTools/distroless: 🥑 Language focused docker images, minus the operating system.

Tweet
2020年04月17日
新しい投稿
古い投稿