YAPC::Kyoto 2023に参加して『qron: Cloud Native Cron Alternativeの今』というトークをした #yapcjapan

qron: Cloud Native Cron Alternativeの今

掲題の通りのトークをした。元々YAPC::Kyoto 2020のトークとして採択されていたトピックを2023年版として話すことに。

オンサイトのカンファレンスに参加するのは2019年のbuilderscon以来なので2年半ぶりくらい。

#builderscon tokyo 2019で「自動作曲入門」について話した - Sexually Knowing

トークについて

speakerdeck.com

もともとあったアイデアを必要に駆られて仕事で作ったものの話。

2020年当時はできたてほやほやだったけど2023年はもう運用して数年が経とうとしているので「実際動かしてみてこうだったよ」という話も盛り込んだ。

結果として強気の40分枠で応募し、採択の運びとなった。自分で選んでおいてだけど40分のトークを黙って聞いているのはけっこう気合がいるので構成とか話し方とかに気を遣う。

最近は別のプロジェクトで絶賛がつがつ作っているところであるものの、ずっと世に出ていないしプロジェクトやチームはどんどん勝手にめちゃくちゃになっていくしで気が滅入っていたけど、感想ツイートとかでおもしろそうって言われているのを見ると自分の仕事がちゃんと見られた気がして嬉しかった。

あと質疑コーナー含めて突っ込んだやりとりができたのも良かった。ソフトウェアエンジニアとして生きているっていう実感がそこにある。

キーノートについて

はてなにいた頃ずっとお世話になっていた id:onishi さんではあるけれど、キーノートに登場するエピソードは以前に聞いたことはあってもその当時にどんな思いを抱いていたかといったことは意外と聞いたことがなくて「そういうことがあったんだ」という新鮮な驚きと共に、onishiさんのパーソナリティ・考え方の裏付けがとれたような気がして得心がいった。

onishiさんもnekokakさんも「やるべきことをやってくれる誰かを待つのではなく自分がやる」というようなことをトークで話していて、やっぱり手を動かした人だけが世界を変えるなんだなあ。

onishiさんはご自身をモブと謙遜・自虐されていたけれど、Web業界で花形とされているソフトウェアエンジニアの第一線とは少し距離があった・退いたというようなご自身の認識からしたらそういう表現は確かに的を射ていると評することもできるかもしれないと思う一方で「主人公」という生き方だけにこだわっていては果たし難かった功績を積み上げられてきたと思う。

イベントについて

前述の通りオンサイトのイベントは2年半ぶり。

もはやPerlを書いていないどころかエコシステムから遠ざかってからだいぶ経つけれど、YAPCに参加するタイプの人たちとはウマが合うなあと思った。

久しぶりに会う人・初めて会う人などなどと話しているうちに生粋のエンジニアとしての自分でいられているなあと感じてのびのびできた。

しかし2年半ぶりということで社交筋 *1 ががっつり落ちていて、日曜の夜にあったスピーカーディナーでは抜け殻になってただ蒸しカキを食べていた。

メンタルはともかくフィジカルの衰えも強く感じたのですぐに鍛えられるフィジカルは次に向けて鍛えておこう……。

仕事もプライベートも最近めちゃくちゃで果たして楽しめるのか不安だったけれど前日と併せて2日間楽しく過ごせた。


けっこうな数が知り合いが運営に関わっていることを知ってちょっと驚いたりもした。カンファレンスの運営ってものすごく大変と伺っているので。

よりカンファレンス運営が身近になったなと思う一方で、カンファレンスで日頃磨いてきた芸を披露することに改めてこだわりたいなという思いを改めた。 (別にスタッフになることと相反しないが……)

あらためてYAPC::Kyoto 2023開催おつかれさまでした、たのしかったです。

*1:社交するための筋肉

チケット販売締切が1/31に迫っているYAPC::Kyoto 2023で「qron: Cloud Native Cron Alternativeの今」というトークをします

blog.yapcjapan.org

元々YAPC::Kyoto 2020に応募していた「qron: Cloud Native Cron Alternativeの今」をベースにした話をする予定です。

2020年当時は運用を始めたてだったので「こういうコンセプトで作ったやで」っていう話を中心にするつもりだったのですが、2023年現在は運用して数年が経ったこともあってコンセプトの紹介に加え「実際こういうところがうまくいっている・うまくいっていない」という話をしたほうが現実味があってよさそうだなと思い、タイトルも「〜の今」としました。

久しぶりにオンサイト開催されるカンファレンスで喋れるということでワクワクしています。個人的には初春の京都に出かける口実ができたのも嬉しいですし、登壇予定の面々の中に見知った名前もあったので久しぶりに顔を合わせられること・カンファレンスの発表を通して知見や交流を深める機会がまた巡ってきたことも楽しみです。

開催日の3/17は日曜日で次の3/19が春分の日であるため、連休チャンスと見なされ既に宿は混み合っているそうです。自分含めて遠方から参加される方々は早速宿を取るのが良いでしょうね。 迷ったら予約、チケット購入が吉と今年のおみくじも言っていました、間違いありません。

ちなみにチケット販売は2023/1/31までです (refs. 【販売期間は2023年1月31日まで】 YAPC::Kyoto 2023 参加チケットの販売を開始します! - YAPC::Japan 運営ブログ)。墓地が無料の時代は終わった、生きて今すぐチケットを買おう!!!

2022年振り返り

作ったもの

他、ちょくちょく使っているライブラリに軽微なバグ修正の報告をしたりPRをしたり、などなど。

hoist-gql-errorsは最近仕事で必要に駆られて書いたんだけど、Datadog APMの仕様に詳しくなったのでそのうち改めてまとめておきたい。

振り返り

サラリーマンをやっている以上仕方ないのかもしれないが自分の思うようなことがやれなくてフラストレーションを感じ続けた1年だった。

別にそんなに独り善がりなことを考えているわけではないとは思っていて「組織が良い方向に向かうにはこういうことをやったほうが良いと思うし自分もやりたいからやらせてくれ」っていうような話で、マネージャーにも了承してもらえていたけれどやんごとなき事情でそれより優先してやらないといけないことが出てきて……みたいなかんじ。

で、その「やらないといけないこと」というのが有り体に言って楽しくない。

別に労働ってそんなもんでしょと言われたらそうかもしれないけれど、フルタイム労働者だったら1日の1/3の時間を捧げるわけで、週5日勤務だったら1年の1/4になる。 1年の1/4の時間をかけるならどうせなら楽しくてやりがいのあるものであってほしい。

もちろんフルタイムの労働をやめるという方向性も考えられるけれど、2つの点から直近ではあまり考えていない。

1つは収入の安定性の面。車を買って趣味になったので安定して (継続して) 今以上の水準の収入がほしいし必須だということ。

もう1つは感情的な話で、やっぱり事業会社の事業に継続して携わることで領域を問わずいろいろやれるのは自分の性格に合っている。 いろいろ関わることで苦しいところもあるけれど「ここが良くなればサービスの質が上がってユーザもハッピーになれるのに」というところが見える場面はどこにいてもあると思っていて、そういう時に自分の管掌外だからといって見てみぬ振りをするのは性格上、おそらくとても難しい。

けっきょく無視することで別のストレスを感じてしまうなら、自分の仕事の範囲を変に決めきらずにおいておきたい。

というところで年明けのYAPC Kyoto 2023でなにか話したいけれど、何を話そうか……と悶々としている2022年でした。

趣味とOKRと私

以下は過去の社内LT大会で発表した時の資料です。

友人とOKRの話になって思い出したので発掘しました。

趣味にOKRを取り入れてみた感想としては、とても遠い・あるいは抽象的なゴールに向かう際に「まず何をやればいいんだろう」と道筋を立てるためのフレームワークであり、またゴールにどれだけ近付けたかを観測するためのフレームワークでもある、というものです。

Objectiveがゴールで、Key resultsがパンくずのようなイメージを持っています。Key resultsを達成できたかどうかはパンくずを拾ったかどうかといえるでしょう。


今日する話

スマブラと私

  • 強くなりたい!
  • 勝てると楽しい、負けるとイラ……イラ……
  • 強いプレイはかっこいい
    • コンボがちゃんと決まる
    • 動きが早い
    • 大胆な判断

スマブラのオンライン対戦と問題

  • レートマッチみたいな仕組み
  • ルールがいわゆるガチルールに統一されていない
    • あくまで「優先ルール」という扱いなので、アイテムありのワイワイルールの人とマッチしたりする
  • 一期一会なので、負けた相手と連戦して対策を積むことが難しい
  • プレイヤー毎ではなくプレイアブルキャラクターごとのレートなので、かなり格上のプレイヤーが使う別キャラとあたることもある
  • プレイヤーの回線品質がまちまちでめちゃくちゃラグい人とかがいる
  • 勝っても負けても成長の糧となりにくい、ストレスが溜まる

コミュニティに入る

  • スマブラプレイヤー・配信者のYouTubeメンバーシップに入っている人が参加できるDiscordコミュニティがある
  • いわゆるガチ勢が集まっている
    • 回線の品質が高い
    • 一貫したルールで対戦できる
    • 連戦できる
    • 自分よりちょっと格上の人とマッチングできる

勝てない

  • 勝てない!!!
  • コミュニティにいるのはスマブラ大好きな学生がほとんど
  • かけられる時間の量で勝てない
  • 恋に仕事に大忙しな社会人が勝つには 効率を極めるしかない

OKR

  • Objectives and Key Results
  • 目標と成果指標

目標は、場合によっては若干気後れするくらいの高いレベルに設定します。 成果指標は、数値化して測定し、簡単に評価できるようにします(Google では 0~1.0 の範囲で設定しています)。

from https://rework.withgoogle.com/jp/guides/set-goals-with-okrs/steps/introduction/

設定した目標

  • 1年以内にコミュニティのプレイヤーAさんにBO5で勝つ
    • プレイヤーAさんは良くコミュニティで対戦してくれる格上の方
    • BO5 = Best of 5; 3本先取で勝敗を決める方式
      • BO3と比べて運の要素が減る = より実力が出やすい
    • これまでの戦績: BO3どころか10戦やって1本取れるくらい

指標

  • 戦績
    • 週あたり2本は取る
  • プレイ
    • コンボ精度を高める
      • コンボAを1試合あたり5回試行する
        • 成功確率を80%以上にする
      • コンボB……
    • etc.

余談: Notionの活用

表を使うことで、シチュエーションに応じたコンボの確認をしやすくしている

1年やってみて

  • 結果的に未達だったけど得るものは多かった
    • 細分化した成長指標ごとに振り返ることで「これはできた」「これはまだまだ」という成長の足がかりが得られる
    • 「これはできるようになったじゃん」と自分を励ます
    • 各指標は目標より向上・改善する難易度が低いのでインクリメンタルなフィードバック・手応えを得やすい
      • e.g. コンボの精度が50→80%に上がったぞ!
  • 実際は1ヶ月、四半期、半年ごとに振り返っています

git pushするだけでGo製アプリケーションをリリースするGitHub Actionsのワークフローを整えた

Go製アプリケーションのリリース自動化

Goで書いたアプリケーションをリリースする際にやらないといけないことはいろいろある。

まず当然のこととしてコンパイルして成果物を作り、それをリリースする。 せっかくクロスビルドが容易なGoを使うので配布する成果物も可能な限り幅広い環境に対応させたい。

また、リリースに含まれる変更の概要をまとめたいわゆるChangelogのようなものも簡単に作れるとなおよい。

GitHubでホストするリポジトリのリリースについて主に考えたいので、リリースのホスティングGitHubのReleasesになる。 各Releaseの説明をChangelogとし、変更がもたらされたPull Requestのリンクも添えたい。

リリースするということは新しいバージョンを決めて、Gitのタグを打つ必要もある。

ひとつひとつはよくある作業だし特段難しいことはないが、いちいち手でやっていられないのでCIに任せたい。 掲題の通りGitHub Actionsでこれら一連の作業を自動化させるという話。

GitHub Actionsによるリリース自動化それ自体は目新しいトピックではない。しかしこの記事で紹介するワークフローはgit pushする以外に開発者がとるべきアクションはない。 具体的には、世の中の先行事例はGitのタグ発行は開発者の手作業で、それを契機にパイプラインを開始する……というものばかりだがそれも不要になる。

goreleaserにビルドまわりを任せる

最近はgoreleaserというツールがよく使われているのでビルドまわりはこれに任せる。

goreleaserには:

  • アプリケーションをビルドする
  • ビルドした成果物をGitHub Releasesにアップロードする

……という2点を任せる。

アップロード対象のリリースは実行したリポジトリに存在する最新のタグから同定される。 言い換えるとタグを打つのはgoreleaserの責務外となる。

semantic-releaseにバージョニングを任せる

[semantic-releaser][]というツールがある。JavaScript (Node) で実装されたCLIツールでConventional Commitsに従ってコミットログを書いておくとよしなに次のリリースバージョンを決めてくれる。

Conventional CommitsはSemantic Versioningを参照し、たとえば feat: blah blah ... とか書くと新機能の追加なのでminorの更新を含意する、などのルールを定義している。 Conventional Commitsに従うとコミットログから次のバージョンを決めるルールがツールを越えて相互運用できる。

他にもプラグインGitHub Releasesを作ったりGitタグを打ったり、あるいはNPMにアップロードするなどができる。

JavaScriptで書かれていることからもわかるように元々NPMパッケージのリリースを主眼に置いてエコシステムが整えられたツールだが、前述のようにリリースバージョンの決定以外はプラグインとして実装されているので、NPMパッケージのリリース以外にも使える。 必要なのはConventional Commitsに従ってコミットすることだけ。

つまり semantic-releaseを使ってGitタグの発行を含むバージョンアップ作業を自動化し、発行された新しいタグに成果物を作成・紐付ける作業をgoreleaserで自動化する というのがワークフローのあらましになる。

ワークフロー例

実際のワークフローを例に説明する。

pkgboundaries/ci.yml at main · aereal/pkgboundaries

上記ファイルから抜粋・簡略化したYAMLが以下。

  determine_release:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    outputs:
      will_release: ${{ steps.determine_release.outputs.new_release_published }}
    steps:
      - uses: actions/checkout@v3
      - id: determine_release
        uses: cycjimmy/semantic-release-action@v3.0.0
        with:
          dry_run: true
        env:
          GITHUB_TOKEN: ${{ github.token }}
  release:
    runs-on: ubuntu-latest
    needs:
      # - test
      - determine_release
    if: ${{ needs.determine_release.outputs.will_release }}
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - uses: cycjimmy/semantic-release-action@v3.0.0
        env:
          GITHUB_TOKEN: ${{ github.token }}
      - uses: actions/setup-go@v3
        with:
          go-version: '1.18.x'
      - uses: actions/cache@v3
        with:
          path: ~/go/pkg/mod
          key: go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            go-
      - uses: goreleaser/goreleaser-action@v2.9.1
        with:
          version: latest
          args: release --rm-dist
        env:
          GITHUB_TOKEN: ${{ github.token }}

determine_releaseとreleaseという2つのジョブに分けている。

determine_releaseは次のリリース予定を調べるジョブで「実行時点で新しいバージョンが発行されそうか」を示す真偽値 will_release を出力する。 cycjimmy/semantic-release-actionというsemantic-releaseを実行するActionがあり、その outputs.new_release_published という真偽値を参照している。 determine_releaseでは dry_run: true を指定し実際のリリースは行わず、バージョンアップ予定だけを調べる。

releaseジョブは実際にリリースを行う。 まず needs にdetermine_releaseを指定する。これは単に依存関係を宣言することに加えて needs コンテキスト経由でdetermine_releaseジョブの出力にアクセスする目的もある。 if: ${{ needs.determine_release.outputs.will_release }} で「 needs.determine_release.outputs.will_release がtrueだったらreleaseジョブを実行する」という意味になる。 新しいバージョンアップを要する変更がコミットされていなければリリース作業は行われない。

参考: - Workflow syntax for GitHub Actions - GitHub Docs - Contexts - GitHub Docs

続いてsemantic-releaseを実行し、GitHub Releasesを作る。 以下にsemantic-releaseの設定を引用する:

{
  "branches": [
    "main"
  ],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    "@semantic-release/github"
  ]
}

from pkgboundaries/.releaserc.json at 8f58c3dd5880534c1fd8cc8f513795722b52b279 · aereal/pkgboundaries

特別な設定は必要としない。デフォルトでNPMパッケージを公開する @semantic-release/npm が有効になっているのでそれ以外の有用なプラグインだけを明示しているだけに留まる。

最後にgoreleaserを実行する。 goreleaser-actionというインストールから実行までをよしなにやってくれるActionがあるのでこれを使う。 goreleaser/goreleaser-action: GitHub Action for GoReleaser

goreleaserはYAMLでいろいろ挙動をカスタマイズできるのだが、今回重要なのは release.mode になる。

release:
  mode: keep-existing

from pkgboundaries/.goreleaser.yml at 8f58c3dd5880534c1fd8cc8f513795722b52b279 · aereal/pkgboundaries

goreleaserもリリースノートの作成やGitHub Releasesの作成ができるが、今回のワークフローではそれらはsemantic-releaseに任せている。 mode: keep-existing を選ぶとタグに対応するGitHub Releasesが既に存在したらタイトルや本文を置き換えず成果物だけアップロードするという挙動になる。

こうすることでsemantic-releaseとうまく共存できる。

今後

リリース時に人間による確認を挟みたいという場合には、Environmentsが使えるかもしれない。

参考: Using environments for deployment - GitHub Docs

EnvironmentsはDeploymentsに関連する概念で、その名の通りデプロイ対象の環境を表す。 Environmentsはそれぞれデプロイ時に必須となるcommit statusやレビュアーを指定できる。 これら機能はBranch protection rulesと似ていて、たとえばproduction環境では所定のチームやユーザのレビューなしにデプロイできない、といった設定ができる。

参考:

GitHubにおけるDeploymentsは単にイベント履歴とそれらに応じたWebhookでしかない。 最近追加されたGitHub Actionsとの統合では、ジョブに environment: production のように記述することである環境の利用を宣言できる。 このジョブ実行開始時にDeployments APIでdeploymentとdeployment_statusが作成される。完了するとsuccessもしくはfailureの記録がGitHub Actionsによって行われる。

実際のデプロイ処理はジョブのstepとして自由に定義できるので、この記事で紹介したワークフローをデプロイ処理として記述することもできる。

するとEnvironmentsの機能でDeploymentsの開始時にチーム・ユーザによる確認を挟んだ上でリリースする、といったことが実現できる。

これは規模の大きいチームで厳格な統制を図りたい時に便利かもしれない。

jsondiff: JSONの構造の一部を無視して差分をとれるGoのライブラリを書いた

github.com

背景

仕事でお世話になっているkayac/ecspressoの機能の中にローカルのタスク・サービス定義と現在使われている定義を比較して差分を出力してくれるものがある。

github.com

これから加えようとしている差分をプレビューできるだけではなく、たとえばデプロイしようとしているわけでもないのに差分があればローカルの定義が古びていることがわかるのでCIに組み込めると便利。

しかし実際に使おうとすると困る点が見つかった。

たとえばタスク定義にイメージタグを書く際に {{ must_env 'IMAGE_TAG' }} のように環境変数を参照している時に「イメージタグ 以外 に差分がない」ことを確認するのが難しいということ。

理想的には image を無視したJSONの構造を比較して差分が出せると良い。あるいは出力されるdiffをパースして image の差分は無視するとかが考えられる。

が、いずれもecspressoの機能とするにはだいぶ領分を外れているので、Pull Requestを送ろうにもましな実装が思いつかないのでどうしたものか。

ECSのタスク・サービス定義に限った問題ではなくJSONのdiffを取る時に一部構造を無視できれば良いので、JSONの差分を取るライブラリを作ることにした。 それがaereal/jsondiffになる。

紹介

ドキュメントのExampleを見てもらうとわかりやすい。

jsondiff.Ignore(query /* gojq.*Query: ".a" */) みたいに使う。

無視するJSON構造はgojqのクエリが使える。

github.com

gojqはid:itchynyさんによるjqのGo実装で、CLIコマンドだけではなくライブラリとしても使える。

他にJSONの構造を指定するクエリ言語のようなものというとJSON Pathが考えられたが、自分が普段JSONの構造を走査する用途ではjq (gojq) を使っているしライブラリ実装を提供していることを知っていたのでgojqを選んだ。

実装のポイント

.a, .b, .c みたいなクエリを .a = null | .b = null | .c = null のようなクエリに変換し、これを実行した結果を比較することで特定のキーを無視する機能を実現している。

元となる .a, .b, .c というクエリの構文木は以下のようになっている。

f:id:aereal:20220324001407p:plain

これを変換して .a = null | .b = null | .c = null というクエリにしたい。構文木としては以下の通り。

f:id:aereal:20220324001404p:plain

この木を作るには右端から作り、最後に処理したノードの左辺を更新していくのが合理的。

リストの右から処理していくといえばfoldRightである。しかしGoにそんな高級な関数はないのでfor文でデクリメントしていく実装になった。

see jsondiff/diff.go at 7e60f563b3601f48e6d4bf8c43210e4ac4614087 · aereal/jsondiff · GitHub

これを可視化すると以下のようになる。

f:id:aereal:20220324001415p:plain f:id:aereal:20220324001412p:plain f:id:aereal:20220324001409p:plain

こう淡々と書くと「ふーん」という感じだけれど、 len(xs) - 1 から開始してデクリメントしていけば末尾からイテレートできることに気がついた時はちょっと気持ち良くなれた。

むすび

当初欲しいと思えたものが作れて満足。

これをecspressoやlambrollに入れるかというと、どうしようかなと思っている。

というのもCLIコマンドも作ったので、 ecspresso render させてそれを比較させるのでも良いのではと思っている。

いずれにせよライブラリとCLIコマンドの両方を作っておくことで、外部からも単体でも利用しやすくなるというのはgojqのお世話になって実感したので良い物作りができた。

ソフトウェアエンジニアリングですべてを薙ぎ倒す2022

これまで

色んなチームにヘルプで入ってプロジェクトやチームを良い感じにする手伝いをすることが主で、他に基盤サービスを作って運用したり、あとは開発者ブログ編集部をやったりなどなど。

現職での1年半くらいのキャリアにおいてソフトウェアエンジニアリングは高々3割くらいで、あとはプロジェクトやピープルをマネジメントするような仕事が占めていた。

振り返り

前職でもWevoxを使っていて、その当時から振り返ってもありえないくらい低い「自己成長」スコアがここ最近ついていてクソワロタ (真顔) 状態だった。

このままだと退職 or dieしかないと感じたので、上司には「もうしばらくプロジェクトマネジメントはやりたくないでござる 絶対にやりたくないでござる」と上申して新しいチームへの配属を希望した。そしてそれは叶えられた。

Webサービスを作って届ける過程でプロジェクトやピープルをマネジメントすることの必要性は理解しているし、特に 今の 現職がそれら領域への梃入れを求めていることも理解している。

それでも 自分が やりたくないとは感じていたものの、義務感が勝っていたので受け入れていたが、我慢できなくなったという次第。

まずもって自分は人間を取り扱うのがとても苦手で、平均程度にはできるとは思うものの心的負担がめちゃくちゃ高い。

また自分はソフトウェア構築の道を究めたいと考えており、プロジェクト・ピープルマネジメントは近接する分野であるがそのものではない。 このため有限の時間を使いたい分野に割けていない状況に強いストレスを感じており、それがWevoxのスコアにも表れていたということだろう。

ソフトウェア構築のエキスパートとありたいのであって、プロ労働者になるつもりもなったつもりもない。 仕事は選びたいし仕事を選べる立場でありたいと思うし、自分がやりたくないことをやっているがためにこんな損失がありますよという主張に説得力を持たせるだけのスキルを備えておきたいと考えている。

そういうありかたを望んでいる自分にとってスキルを研鑽できない状態に陥ることは、まったく大仰ではなく自身の人生が破壊されることと同義である。

新しいチームでやりたいこと

掲題の通りソフトウェアエンジニアリングですべての障害を破壊していきたい。

「運用でカバー」に相当する作業を自分から徹底的に排除していきたい。 結果的にそうなるとしても、まずソフトウェアエンジニアリングで解決できないか問いたいし、それを端から諦めるのはエキスパートとしての怠慢だと叱咤する。

これがDigital Transformationや!!!! というのを見せ付けていきたい。

そういえば5年くらい前にも似たようなこと書いていたなあと思い出したので置いておく: 「ついカッとなって……」取り組んだ 開発者のための開発 で業務効率を改善させた話 - エンジニアHub|Webエンジニアのキャリアを考える!


以上の文章は社内esaに書いた文章を加筆修正しました。

定期的にプロ労働者になりたくないと言っているなと気付いたのでその再確認。

injecuetにTerraform stateを参照する機能を追加した

injecuetとは: injecuet: CUEに環境変数を注入する便利CLIツールを書いた - Sexually Knowing

これまでCUEに埋め込むデータソースとして環境変数だけだったのですが、新たにTerraform stateを参照する機能を追加しました。

こういうCUEがあって:

@inject(tfstate,stateURL=./terraform/ok/terraform.tfstate)

name: string @inject(tfstate,name=output.user.name)
age: int @inject(tfstate,name=output.user.age)

terraform/ok/terraform.tfstate がこういうかんじだとして:

{
  "version": 4,
  "terraform_version": "1.1.6",
  "serial": 2,
  "lineage": "3124ddff-8837-9bb1-a0d6-fe4fd14969aa",
  "outputs": {
    "user": {
      "value": {
        "age": 17,
        "name": "aereal"
      },
      "type": [
        "object",
        {
          "age": "number",
          "name": "string"
        }
      ]
    }
  },
  "resources": []
}

injecuet ./tfstate.cue を実行するとこんな風に解決されます:

{
    @inject(tfstate,stateURL=./terraform/ok/terraform.tfstate)
    name: "aereal" @inject(tfstate,name=output.user.name)
    age:  17       @inject(tfstate,name=output.user.age)
}

attributeの形式を変更

before:

@injectenv(USER)

after:

@inject(env,name=USER)

Terraform state対応を入れるにあたり、データソースを識別できるようより拡張性のあるフォーマットに変えました。

古い形式も現在サポートしていますが、そのうち消えるかもしれません。

実装の話を簡単にすると、元々は (*Attribute) Contents() string という関数でattributeの中身を得て、それを strings.Split していました。 Contents()@some_attr(ここだよ)ここだよ を返します。

が、よくよく *Attribute のメソッドを見て Arg(int) などが生えていることに気がつきます。 Arg reports the contents of the ith comma-separated argument of a. という説明の通り、attributeの括弧の中身をカンマ区切りのリストとみなす実装です。

このようにライブラリのサポートがより受けやすい形式にしようということで変えました。

tfstate-lookup最高

Terraform stateから値を取る処理はtfstate-lookupにおんぶにだっこです。とても助かっています。

fujiwara/tfstate-lookup: Lookup resource attributes in tfstate.

CUEとJSONにおける数値の扱い

ちょっとハックが必要だったのが数値の扱いです。具体的にはfloatとintで、ちょっと込み入ったややdirtyな実装になっています。

具体的にはここ: github.com

CUEはintとfloatという区別があるのですが、JSONはnumberというGoやCUEでいうintとfloatをまとめた型しかありません。 これは元になったECMAScriptの仕様に由来するものですね。

で、Goのencoding/jsonのunmarshalはデフォルトでJSONのnumberをfloat64に変換します。

Goのfloat64をCUEがintを求める式に埋めようとすると型の不一致でエラーになりますが、Terraform stateに数値が含まれているとCUEに埋められないのも不条理です。

なので以下の条件をすべて満たす際にint64へキャストするようにしています。

  • CUEの式がintを受け入れる
  • CUEの式が floatは受け入れない
  • Terraform stateから得た値 (interface{}) がfloat32かfloat64である

理想的にはTerraform stateから得た値の小数部が0であることまで見るとよいのでしょうが、だいぶ大変なのでここらへんで諦めています。

良い実装方法があればPull Requestしてもらえると嬉しいです。

金沢に引っ越して1年が経とうとしている

この記事は地方在住 Advent Calendar 2021の10日目の記事です。

筆者について

……と移り住んでいます。

大阪府は進学で、京都市は就職でそれぞれのタイミングに合わせて移住しました。

金沢移住は転職と機をほぼ同じくしていますが、勤務先は金沢近郊ではなくリモートワークです。このあたりの背景は以前に書いたのでそちらをご覧ください。 端的にいえば観光で訪れた金沢に惚れて住んでみたくなったからです。

this.aereal.org

変わったところ

車に乗るようになった

f:id:aereal:20210930140428j:plain

これは必要になったからという理由が2割くらい、趣味に目覚めたからという理由が8割くらいです。元々、自動車免許を持っていなかったのでまず教習所に通い、秋に交付されました。

教習所に通っていたころの日記はこちらをご覧ください: aereal カテゴリーの記事一覧 - karimenの日記

後述するように金沢は降雪・寒冷地帯です。また冬に雨がとても多い地域ですので、京都時代に日常の足としていたロードバイクでは勝手が悪くなります。

また、地下鉄やJR, 各種私鉄網が発達していた京阪神に比べると公共交通機関カバレッジは北陸三県に広げて見てみても低いと言わざるをえません。 身軽な旅行が好きな身としてはより強く時刻表に縛られるようになり窮屈に感じます。

最後の趣味というのは車それ自体を楽しむことが目的になった、ということです。元々、ロードバイクでツーリングやヒルクライムは好きだったのでスポーツ走行に対する関心の芽はあったのだと思います。

そんな折にMAZDA3という車を知り、一目惚れしたので免許を取ることを決意し最近乗りはじめました。

d.aereal.org

車が無くとも一人暮らししている分にはさほど困りませんが、地域の特徴を見るに少なからず不便は被るでしょう。

雪が降る、冷え込む

f:id:aereal:20211210224934j:plain

降雪・寒冷地帯ですと述べました。京都市は年間を通して比較的温暖です。冬は緯度の割に冷えたり雪がちらつくことはありますが、少なくとも市街地においては大しことはないです。 京の底冷えという表現がありますが、少なくともここ最近では際立って低温というほどでもありません。

ただし夏はとても蒸し暑い地域で、そのため住宅の構造もそれに応じたものになっています。二重窓はまず市街地では設定されていません。なので、実は屋内の体感温度は金沢の方が温かく感じます。

また京都市より明らかに降雪量がピーク・平均ともに多いです。が、それゆえに公営の除雪車や融雪設備は比較的充実していますし、車両のスタッドレスタイヤ等の装備率も高いとされているので交通が麻痺するほどではないようです。

ただ北海道出身なのであまり積もらずシャーベット状の路面が広がる様は新鮮です。どちらが良いというようなものではありませんが、根雪になる北海道の方がさまざまな点で歩きやすいようには思います。

変わらないところ

荷物の配達

北陸といえど本州なので、各種運送業のみなさまのおかげで特段の不自由を感じずに暮らしています。

ただ2021年1月の大雪などで交通が麻痺した時はさすがに遅れが生じていましたが、これは仕方のないことですし、予想できたことなので大事ではありません。

良いところ

のと里山海道という最高の自動車専用道がある

石川県/のと里山海道の紹介

のと里山海道金沢市能登半島を結ぶ自動車専用道です。無料化された自動車専用道というと片側一車線ずつで線形もよろしくない一般道と大差ないようなところを想像しがちですが、日本海沿岸を走るおよそ35kmの区間は二車線ずつで速度制限は80km/hのかなり高規格な道路です。

晴れた日に走ると視界の片隅に日本海と水平線を入れながら穏やかにドライブできとても快適です。前述したように免許とりたて・マイカーを買いたての身なので運転に慣れる格好の機会です。

元々有料道路だったのですが、北陸新幹線延伸に伴い無料化されました。

土日は兼六園に入り放題

毎週末は石川県民であることを公的証明書で示せば入園料が免除されます。

兼六園の県民観賞の日(毎週土・日曜日) 石川県民は、毎週土・日曜日は入園料免除となります。

ご利用案内|兼六園より

のと里山海道と併せて住んでいる自治体に税金を払う甲斐があるというものです。

微妙なところ

舗装が荒れている道路が散見される

特に157号線・159号線のそれぞれ尾山神社付近や尾張町付近が荒れていてロードバイクで走っていると振動で手が痺れてきますし、ハンドルがとられそうになります。

車通りが多いエリアでもあるのでかなり緊張感があります。できれば走るのは避けたいほどです。

冬に冷え込み、夏は猛暑日にもなるので寒暖差が激しく厳しい気候ではありますが安全のためメンテナンスは十全に行ってほしいです。

総括

1年近く金沢に住んでみて、概ね予想していた体験が得られています。 公共交通機関を頼った生活はできなくなることは想定済みですし、スーパーやコンビニ、そしてAmazonがあれば変わりのない生活が送れるだろうという期待もほぼその通りです。

意外な点としては、自身が実感を持てるほどにこの地域に愛着が湧いているということです。ひとえに払った税金が体感できるかたちで還元されているからでしょう。 そういった意味では京都市は「どうせインバウンドの観光客のために費されるのだろうな」という怒りとも諦めともつかない感情で見るしかありませんでした。

ふるさと納税のような制度が敷かれていますが、けっきょくのところ人生の時々の曲面において長大な時間を費す居住地域に気持ちよく納税できることが最も望ましいと考えているので、そういった意味でも今の金沢の生活は気に入っています。

一方、金沢に骨を埋めたいかというとはっきりとした答えはもっていません。 せっかくフルリモート勤務でそれなりにやっていけるということがわかりましたし車を持ったので、もっといろんな地域に住んでみたいという気持ちがあります。

転職から1年が経った

this.aereal.org

this.aereal.org

所属組織についての感想とか意気込みみたいなのは社内のesaに書いたので、転職体験に関する個人的な感想を書く。

意外とやれている気がする

ノンバーバルコミュニケーションなどに重心があった人々は、特に最近限界を迎えたり消耗しきったりなどしている声を聞くようになっているけれど、自分はかなり適応できている……というか自分の性質にあった環境に周りが変わってきていると思う。

自分は、おそらく考えていることが顔に出やすいほうなのでイラついてくるとあからさまに相手に伝わりやすい……と思う。 オンラインミーティングやテキストチャットへの偏心は、この自分の性質をうまいこと補ってくれるので助かっている。

また、自分は簡潔さよりも誤解を招かないことを重視したコミュニケーションをとる傾向があるので、口頭で同期的に話していると長くなりがち。 その点、テキストベースだと非同期にやりとりしやすくて「あ〜長い話してすまん〜」って気持ちが減ってやりやすい。

一方、自分ひとりのパフォーマンスが良くても仕方がないのでうまいこといくと良いなと思っている。いいなと思っているけど、現状、自分が能動的になにか手を打つつもりはあまりない。 自分より得意な人が考えてくれているし、そもそういうのは得意じゃないどころか下手まである。それに、自分が得意でかつやりたいことでまだまだやるべきことがあるのでそちらに目を向けたい。

「元同僚」ができて嬉しい

今回が初めての転職なので「元同僚」がたくさんできてとても嬉しい。

前職の同僚は頼れるし話していて楽しいけれど、同じ会社で働く同僚となるといろいろ思うところがあったり、そも目にしたり耳にしたりするものが同質になりがちな人とだけ話していて、本当にこれでいいのかな? と感じることがあった。 特に組織とかの話をする際がそう。

転職して所属も変わり、いろいろ見聞きするものごとが違ってくるにつれ、前職の同僚とたまに話すことが良い刺激になっていると感じる。 もともと交友範囲が広くなかったのですごくありがたい。

もちろんやりようによっては転職という手段をとらずともなんとかできたとは思うけれど、自分の場合はこうなったという話。

次のN年

前職に入社した時は30歳までには転職するだろうという漠然とした見立てがあった。

今はどうかというと、よくわからない。ただまた10年所属しつづけるかというと、あまりそういう未来は見えない。 所属組織がどうこうではなく、自分が10年以上同じ場所でなにかをやりつづけるイメージが持てないだけ。

this.aereal.org

そもそも10年先も現役でソフトウェアエンジニアをやりつづけられるのか、けっこう怪しいとも思う。 自分が世間で通用しつづけられるか、という視線を持つために転職というエコシステムは意識しつづけるだろう。

一方で、背中を追い続けるだけではなく、背中を見せるということもそれなりに意識しなければならない気配は漂ってきている。 いいかんじの環境を得るためにとる自分の動きのレパートリーを増やすというのも当然考えなければいけないだろう。

まあ遠い将来のことはよくわからないので、元気に楽しくやっていきたいですね。以上です。

生きているのならシェルスクリプトにだってなってみせる、そうPerlならね

シェルスクリプトを書くのをやめる - blog.8-p.info

これを見て:

Shell - run shell commands transparently within perl - metacpan.org

use Shell qw(cat ps cp);
$passwd = cat('</etc/passwd');
@pslines = ps('-ww'),
cp("/etc/passwd", "/tmp/passwd");

Synopsisより。

もうちょっとそれっぽく書くとこう:

cp '/etc/passwd', '/tmp/passwd';

仕組み

Shell.pm - metacpan.org

コメントを除いて150行に満たない小さいコードです。答えを言うとメタプログラミングで実現されています。

オリジナルはLarry WallによってPerlの強力さをデモンストレートするために書かれたそうです。興味深いですね。

use

Perlを知らない人向けに解説すると、Perluse Shell qw(cp mv);BEGIN { require Shell; Shell->import('cp', 'mv'); } のsyntax sugarです。 qw(cp mv) はリストリテラルの一種で ('cp', 'mv') と等価です。

requireはパス上からShellというパッケージを探して読み込む処理で、それを行った後にimportというサブルーチンの呼び出しを行います。

呼び出されるShellパッケージでこのimportサブルーチン *1 を定義しておくと、読み込まれた際に任意の処理を実行できます。

シンボルのインポート・エクスポートとシンボルテーブル

ところでこういうPerlのコードを見たことがあるかもしれません:

use Carp qw(croak);

croak('oops');

Carpというモジュールをuseするとcroakというサブルーチンが使えるようになりました。不思議ですね。

これは専用の言語機能があるわけではなくて上述のimportで何かをしています。

Perlにはシンボルテーブルというものがあり、他の言語でいうスコープとか環境 (environment) と呼ばれるものとだいたい一緒です。

変数やサブルーチンはこのシンボルテーブルに属しており、パッケージごとにシンボルテーブルが別れています。言い換えるとシンボルテーブルの境界をなすのがパッケージです。

Perlのおもしろいところはこのシンボルテーブルをランタイムにわりと自由に触れるということです。触れるというのは読み出すだけではなく、書き換えることもできます。

MyPackage::my_symbol など パッケージ名::シンボル名 という風にアクセスできます。

Shellのコードでいうと *{"${callpack}::$sym"} = \&{"Shell::$sym"}; がシンボルテーブルを操作するコードになります。

$callpack は呼び出し元のパッケージ、つまり use を書いたパッケージにあたるので、呼び出し元のシンボルテーブルに対して $sym の値を \&{"Shell::$sym"} に書き換えています。

$symimport の引数リストの要素なので、 'cp''mv' になります。つまり Shell::cp などのサブルーチンを指すのですが、これらの定義はどこにあるんでしょうか。

AUTOLOAD: Perl版method_missing

答えはAUTOLOADという関数です。これはRubyでいうmethod_missingで、AUTOLOADが定義されたパッケージに存在しないシンボルを参照しようとした時に呼ばれます。

ShellのAUTOLOADは呼び出そうとしたシンボル名を取り出し、 _make_cmd に渡しています。この _make_cmd がコマンドを実行するサブルーチンを作るので、それをシンボルテーブルに書き加えることで任意のコマンドを実行できるサブルーチンが無から湧き上がったようにみえるのでした。

補足

シンボルテーブル操作とか、スコープによるガードレールを破壊するような危険な行為じゃん! と思ったあなた。そうですね。

Perlはpragmaという仕組みで、一部の言語機能の使用を制限したり警告することができます。strict refs でこのシンボルテーブル操作を制限できます。

use strict; とだけ書くとこのstrict refsも有効になります。

ちなみにこのpragmaのon/offはレキシカルに行えるので、これらをどうしても使いたい時には no strict 'refs'; と書くと一時的に無効にできたりします。

Perl後方互換性を保つだけではなく書き手をある程度信頼する作りであるとも感じます。 これをもって大規模チームでの開発に向かないと評価する向きは理解できますが、2021年だしこういう「大いなる力には責任が伴う」を地で行くようなファンキーな言語がひとつくらいあっても良いのではないでしょうか。

おわり

scrapbox.io

私はちょっとした処理もGoで書くようになりました。

シェルスクリプトの一番の問題点はテスタビリティに欠ける言語設計だと思っているので、GoじゃなくてPerlでもRubyでもなんでも良いがテスタビリティが担保される言語ならなんでも良いと思います。

テストのないプロダクションコードはおぞましいと言う人が多くコモンセンスと言って差し支えないと思いますが、それが開発用の便利スクリプトとかになると途端に緩むのはどうしてなのでしょうか。 認証情報などが環境変数に露出していて、間違えると本番データを消してしまったり数百万の請求がやってくるかもしれない環境で、テストもない・dry-run機構が壊れているかもしれないスクリプトを実行することが恐しくないのでしょうか。

私は他人の書いた古びたシェルスクリプトほど怖いものはないです。

以上です。

Rubyのshellについて (2021-09-17追記)

miyagawaさんから情報提供いただきました。

Ruby演算子オーバーロードができるのでよりそれっぽい見た目になって最高ですね。

*1:関数のこと

prpl: AWS SSMパラメータストアの値を環境変数に設定するツールを作った

github.com

作った。

prpl = parameters pull toolです。

使い方

go run github.com/aereal/prpl/cmd/prpl -path /app/staging env

こういう風に使う。 -path はパラメータストアのパラメータパス。このパス以下のパラメータをすべて取得し、環境変数として設定、コマンドを実行する。 SecretStringもdecryptされて設定される。

injecuetと組み合わせるために作った。もちろんこれに限らずSSMパラメータストアと連携させてコマンドを実行するのにも使える。

this.aereal.org

ちなみに横着してバイナリの配布はしていない。自分の用途では go run で問題ないので。気が向いたらバイナリ配布します。

環境変数命名規則

READMEにも書いてあるけど、 -path に指定した文字列を取り除いた残りを使う。英数字以外はすべてアンダースコアにして大文字になる。

なぜこうしているかというと、 /app/staging/creds/id/app/production/creds/id のようにパスの先頭に環境などを含めているケースを想定しているため。 環境変数を参照するアプリは環境を意識せずに CREDS_ID と参照し、prplを実行する際に -path を切り替えて実行することを想定している。

ssmwrapとの違い

非常によく似たツールにssmwrapがあって、というかもともとssmwrapを使おうとしていたけれどバグがあり修正PRを送ったものの応答がなかったのと、上記のようなもっと合理的な命名規則を採用したく、では別のものを作ろうと思い立った。

ssmwrapと違いオプションは -debug-path だけで、コマンドを実行する機能しかない。 正直、ssmwrapのオプションは -paths とか -names とか -prefix とか -env とかかなりややこしくて所望する結果を得るのにどういうオプションを渡したら良いのかかなりとっつきにくかったので、しょっちゅうオプションを変えるわけではないとはいえ、UNIX的世界観のツールらしくもっとsimpleかつeasyにしようという思いもあった。

また、ファイルへの書き出し機能は、おそらくECSやDockerのenvironmentFileのために追加されたのだろうけれど、これって env(1) で良いよなという思いもあり削りたかった。

いかがでしたか?

最近ちまちましたツール作りが捗っていて楽しい。どうぞご利用ください。

ISUCON 11予選に参加して敗退した

id:karupaneruraid:Sixeight とチームを組んでISUCON 11予選に参加した。
再試験スコアは25746点、ベストスコアは記録をちゃんと残せてなかったけど3万ちょっとくらい。

去年の思い出:
ISUCON 10の予選に参加しました - Sexually Knowing

使ったリポジトリ:
GitHub - aereal/isucon11-qualifier: 一発あてるぞ


id:karupaneruraさんの: ISUCON11予選に参加しました - 時計を壊せ

id:Sixeightさんの: ISUCON 11でISUCONに初出場して予選敗退した - ちなみに

参加するまで

仕事でバタバタしているうちにチーム編成がおぼつかず応募が締め切られたので、今年はお流れかなーと思っていたら既に応募していたid:karupaneruraメン募していたので声をかけ組んだという流れ。
そこにid:Sixeightも合流してチームにゃんこ選抜になった。

縁がなければ参加していなかっただろうから感謝です。

前日まで

過去問を解く練習を2回、過去問の講評を一緒に眺める回を1回やった。

AWSアカウントの準備とかはid:karupaneruraがサクサク進めてくれて助かる、ありがとうございます。

ISUCON 10の予選問題を解いたりして、まあまあ調子よくスコアを出せたりしたものの自分とid:karupaneruraは参加していたので当然といえば当然でもある。

また、集まってやる練習日以外にもScrapboxプロジェクトを作ってそこにカンペを溜めていくなどした。これはISUCON 10の時にもやっていて、いわゆるWiki的な使い方ができて本当に助かっています、ありがとうScrapbox.

当日

やったことは二人が既に書いてあるとおり。

自分は、conditionの挿入をバックグラウンドのGoroutineにやるように書き換えたり、trendのクエリ改善をしたり。

trendのクエリ改善は、order byの条件をミスって出してはfail続きでスコアを伸ばしきることができず、前述のスコアでフィニッシュ。

感想と反省

writeとreadにそれぞれボトルネックが設定され、かつ相互に関係しあうので思考停止キャッシュやKVS退避では解決しない解きごたえのある内容でとても楽しかった。

それだけに競技中に自分が納得のいくパフォーマンスを発揮できなかったことは悔しいし、はっきりと実力の不足を感じた。

競技後に自分が最後まで担当していたN+1クエリを解決させて、それがブロックしていた他の小さい変更を入れたら予選通過ボーダーくらいにまで上がったのでなおさら悔しい。

それだけではなく、その他のコード修正でもチームメイトにGoのコードレビューをしてもらった時に知らないことが多かったし「window関数を使ったらいけそう」って言われても書けなかった。

自惚れるほど自信があったわけではないけれど実力は足りていないことは改めて実感したし、この調子だとまた来年のISUCON (あれば) の1〜2ヶ月前から準備してもぜんぜん足りなさそうなので、今からしこしこ準備することにした。
とりあえず講評で出てきた知らない新機能や使用の自信のない機能とかは一通り調べていくことにした (MySQLのlateral句とかnginxのtry_filesやinternal redirectとか)。

ISUCONという競技自体は楽しめたけれども、これまでよりずっと悔しいし反省点の多い回だったので次回は絶対に本戦に出場するぞ。