emahiro/b.log

Drastically Repeat Yourself !!!!

『STAFF ENGINEER』を読んで自分のこれからについて少し考えた

Overview

『スタッフエンジニア マネジメントを超えるリーダーシップ』を読んで、この本に書かれていた内容が自分自身の「今どうやって行きたいのか?」というキャリアの方向性を考えるいいきっかけになったのでその備忘録です。

ref: https://amzn.to/3ukgpaV

読書メモ

頭に残ったところの抜粋。

  • スタッフエンジニアとは?

  • スタッフエンジニアの原型 (アーキタイプ

    • テックリード
      • 与えられたチームやプロジェクトを成功に導く。
      • スタッフエンジニアとして最も一般的なアーキタイプ
      • 多くの場合、チームを成功に導くために、テックリードがチームが置かれた状況を整理したり、部門間の関係維持に務める。
      • 複雑な問題に取り組むチームのまとめ役。
    • アーキテクト
      • ビジネスのニーズ、ユーザーの目的、それらに関連する技術的な制約などを深く理解することに多くのエネルギーを費やす。
    • ソルバー
      • 会社が信頼を置き、困難な問題に関わり、解決の責任を負う。
      • 他のフタッフエンジニアのアーキタイプと異なり、すり合わせ等に時間を割かない。(なぜなら組織が優先事項と定めた問題に取り組むから)
      • 最小単位は個人。
    • 右腕
      • 上級の組織リーダーでありながら経営責任を追わない。
  • スタッフエンジニアの仕事

    • 技術的な方向性の設定
    • メンター及びスポンサー
      • チームのメンターやスポンサーになって彼らの進む道を応援すること。
    • エンジニアリングの展望を伝える。
    • 探索。
    • 接着剤になる。
  • 重要なことに力を注ぐ

    • スナッキングを避ける。
      • インパクトに富、手軽な仕事はなくなっていく。
      • 難しいがインパクトがある、もしくは手軽でインパクトが薄い仕事が残る。
      • つまみ食いは達成感が得られるが、学べることは少ない。
      • スナッキングで時間を潰すのは良いが、インパクトの大きな仕事に時間をかけるのか、スナッキングに時間をかけるのか、については誠実である必要がある。
    • プリーニングをやめる
      • インパクトが小さいのに目立つ仕事はやめる。
      • 目立つ仕事がインパクトが大きい仕事ではない。
    • ゴーストチェイシングをやめる
      • 前職のゴーストを追いかけるのをやめる。
      • エゴを抑え、無意味な仕事に時間を注ぐのをやめる。
  • どんな仕事に注力するべきか?

    • 会社が直面してるリスクを探る -> 重要な仕事をする最初のステップ。
    • 将来の成功と今の生存のバランスを取り続ける。

キャリアの方向性について

書籍としてはもう少しトピックが多くありましたが、自分は「自分がこれからどうなっていきたいのか?」ということを考えながらこの本を読んでいました。
結論、自分はスタッフプラスを目指そう、というところに行き着いてるわけではありませんが、「インパクトのある仕事を探っていこう」とは考えるようになりました。

特に本書の半分以上を占める邦訳版に載っている各社のスタッフプラスのエンジニアのコラムも読み進める中でいくつか「こういうことをするか」みたいなところがまとまってきたのでまとめてみます。

スナッキングを避けること

今の自分にとって耳の痛い話だなと思いました。
一定現職の歴も長くなってきて、ある程度全社的にインパクトの大きな仕事をこなしてきたとも思っている一方で、ここ最近どうしても「細かいところ」が目についてしまう自分がいました。
自分の性分なのかもしれませんし、若干悪いところでもあるんですが、目につくと気になって手を出しちゃう癖があって、これがまるっきり本書で述べられている「スナッキング」に近しいことでした。
あと、これに関連して些細なことであっても「問題と捉えてしまう」という頭の使い方も、完全に「スナッキング」だなと思いました。

もちろん過去それで解決してきたこともありますが、一方で「それって今解決するべきことなのか?」、さらにいうと「自分が取り組むべきイシューなのか?」は頭の片隅において物事を見ないと、結局インパクトの薄い、アウトカムの薄い仕事にばかり認知が奪われ、本当に価値ある仕事、というものをすする時間が相対的にどんどん減っていくので、考えるというのは容易いですが、「これってスナッキングじゃないか?」という観点は物事を捉えるときに持っておきたいなと思いました。
ただ、こればかり言ってると「全然仕事しない人」みたいに見られてしまうので、一介のエンジニアとしてこの観点を第一に置くことは出来ないなとは感じます...w。

インパクトのある仕事をすること

上述の「スナッキングを避けること」の正反対になります。
今自分が関わってることについてレバレッジポイントはどこになるのか?そういうものを探っていくプロセス自体に価値を感じるのでこの点は自分でももう少しエンジニアとして過ごすなかで言語化していきたいなと思います。

肩書の重要性

多分この書籍で自分にとってのインパクトの大きな部分だったかなと思います。

正直なところ自分は今まで「肩書」になんの価値があるのかわかっておらず、またこの業界にいると「肩書の陳腐さ」みたいなネガティブイメージを植え付けられてしまう機会(SNSの発言等々の情報の濁流の中で)がたくさんあるので、「肩書」や「役職」といったものに重きをおいてきたことはなかったのですが、本書の後編にある各企業のスタッフプラスのエンジニアのコラムに書いてあったのは 肩書の価値 についてでした。

ちょっと話はそれますが、自分の最近の感じてる課題感に 「経営との距離」 というものがあります。
これは自分が経営者になりたいとか経営に携わりたい、という気持ちから出てくるのものではありません。

事業に関わる仕事をしてると誰もが経験することとして「アウトカムがあるのか不透明」だと感じてしまう業務に巻き込まれる事があると思いますが、このとき現場の人間の一人ではその意思決定者に対して適切にフィードバックを送れない、もしくはフィードバックを送るまでのパスが長くてフィードバックが正確に届かないという事象が発生してしまいます。(ともすれば現場の社員が思い通りにならないことに対してネガティブな愚痴を言ってるだけ、と捉えられてしまう可能性だってあります)
もちろん、現場の1社員が知ることのできる情報量には限界があり、そもそも意思決定をしてるレイヤーと持ちうる情報量の格差があるので、圧倒的に意思決定する側の見解が正しいことだってあります(し、実際そういう可能性が高いことのほうがしばしばあるでしょう)
ただ、仮にそうだったとしてもこの「経営との距離」があると、「その見解を教えてくれ」という声さえなかなか届かないこともあります。別に経営としては全ての意思決定を公開する必要性もないので、そんな事はあり前だろう、と言われればそれまですが、結局のところ距離があると知りたいことも知れない、という事象が発生してそれがダイレクトに従業員として働いてるときのUXを損ないます。(サラリーマンなのでやれといわれたことはやるけど、やるならインパクトのあることをやっていきたいと思うのは自然なことです)

スタッフプラスのいくつかのコラムの中で述べられていた内容ですが、スタッフプラスになると会社の戦略という部分に対して関わる機会を得たり、戦略部分の会話に呼ばれたり、そもそも「周りの見る目」が変わるということがあるそうです。これはなんとなく理解できる感覚でもあります。
結局のところ「CxO」みたいなわかりやすい肩書がある方が周囲も「そういう人だ」と思ってみてくれるので、肩書は情報を手に入れたり、手に入れた情報をもとにフィードバックを送る際のショートカットとして機能する側面は否定できないものなんだなと感じました。

それがほしいから肩書を取りに行くのは若干歪んだモチベーションにも感じますし、何より「やるべきことをやっていった先に職責を広げる、もしくは職位を上げる機会が訪れるものである」ということは一定理解しつつも、「肩書」というものが持つパワーについて理解し直すいい機会になりました。

結局のところ

上記にも書きましたが、スタッフプラスになりたいのか?に関しては自分自身はまだ「?」な部分が多いですが、キャリアの方向性としては全然ありえる路線だなと感じました。
一方でやはり中期的にはもう少し自分自身や自分の関わる仕事のレバレッジポイントはどこにあるのか?と探索する日々は続きそうな気がします。

今年は育児も例年ほど本を読めてませんがこの本を読む機会があったのは良かったです。

OpenAPI3 で外部ファイルの定義を参照する

Overview

業務で利用してる OpenAPI3 に置いて、Endpoint の定義間(api と admin みたいな)で共有したいコンポーネントがあり、それを外部ファイルに定義したときに、各定義で参照する方法を調べたので備忘録として記載します。

やり方

$ref にファイルパスを渡す。

ref: Using-$ref

至って簡単です。

例えば JSON Schema を利用していてファイル構成が以下のような場合

└ api.json
└ admin.json
└ component/
         └ common.json

JSON ファイル側で $ref に component に切った json のファイルパスと定義までのドキュメントのパスを指定します。

つまり以下のようになります。

定義側

// common.json

{
    "components: {
        "schemas": {
            "SampleComponent": {...}
        }
    }
}

参照側

{
    "$ref": "component/common.json#components/schemas/SampleComponent"
}

これで参照できるようになります。案外簡単でした。

See Also

swagger.io

stackoverflow.com

gonew コマンドを試してみた

Overview

gonew コマンドを触ってみたので備忘録です。

go.dev

gonew とは?

既存の Go のプロジェクトからコピペで作らずに、そのプロジェクトをテンプレートとして新規の Go プロジェクトを始める事ができるツールです。
既存プロジェクトの中身をいちいちコピペしなくて良くなります。Git から Clone する必要もないので Git の履歴等も参照してくる必要もありません(クレデンシャル消した履歴等を意図せず漏らしたりすることはない)

できること

既存のプロジェクトをテンプレートとして新しいプロジェクトを作る。

できないこと

go.mod を持っていないプロジェクトはテンプレートとして指定できない。(module 名をテンプレート名として指定するため)
このため、特定の実装をコピペする、もしくは Go のファイルをコピーする、といったことはできない。

たとえば go.mod が存在しないプロジェクトの package を追加しようとしても以下のようなエラーが発生する。

gonew github.com/emahiro/qrurl/tree/main/server/service github.com/il/gonewSample/samples/myserver3
gonew: go mod download -json github.com/emahiro/qrurl/tree/main/server/service@latest: exit status 1
{
    "Path": "github.com/emahiro/qrurl/tree/main/server/service",
    "Version": "latest",
    "Error": "module github.com/emahiro/qrurl/tree/main/server/service: no matching versions for query \"latest\"",
    "Origin": {
        "VCS": "git",
        "URL": "https://github.com/emahiro/qrurl",
        "Subdir": "tree/main/server/service",
        "TagPrefix": "tree/main/server/service/",
        "TagSum": "t1:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
    }
}

gonew を使う

インストール方法は公式のドキュメント通りです。

go install golang.org/x/tools/cmd/gonew@latest
gonew --help
usage: gonew srcmod[@version] [dstmod [dir]]
See https://pkg.go.dev/golang.org/x/tools/cmd/gonew.

基本的な使い方は以下です。

gonew $TemplateModulePath $modulePath

Sample1: あるプロジェクトをテンプレートとして新しいプロジェクトを生成する

新しいプロジェクトを生成するときは例えば以下のようにします。

gonew github.com/emahiro/il/gonewSample emahiro.dev/myserver

Sample2: あるプロジェクトの特定のコミットをテンプレートとして新しいプロジェクトを生成する

テンプレートには latest だけでなく、特定に revision を指定することも可能です。

gonew github.com/emahiro/il/gonewSample@$revisionHash emahiro.dev/myserver

go.mod 以外にも go.mod のあるディレクトリ配下にあるファイルは丸っとコピーしてきてくれるので yaml ファイルや env ファイルと言った config 系のファイルも取ってこれます。環境変数等をいちいち設定しなくて良くなりそうです。

まとめ

まだ試験段階のツールなのでもうちょっと便利になることを期待。
今のままでも、ハンズオンとかを提供するときのテンプレートを配ったり、マイクロサービスのモジュールのテンプレート用意してチームに展開したりと行ったユースケースでは使えそうではありますが、もっと多様なユースケースに対応してほしいなと思います。

See Also

cloud.google.com

cockroachdb/errors でスタックトレースを取得するのが良さそうだった

Overview

Go でエラーハンドリングするときにどこでエラーが発生したのかわかるように StackTrace を取得して、エラーメッセージと一緒に出力したいケースがあると思いますが、Go でStackTrace を取得するのはひと手間かかります。
「Go スタックトレース」といったキーワードでググると数多くの先人たちの知見をお目にかかる事ができますが、今回はそんな中でも cockroachdb/errors で Stack Trace を取得するのがお手軽で便利そうだったので、その備忘録です。

cockroachdb/errors とは

github.com

pkg.go.dev

cockroachdb が作ってる Go の errors のスーパーセットの中の一つで、スタックトレース以外にも便利な機能が用意されています。

スタークトレースの出力の仕方

pkg.go を見ればだいたい使い方はわかりますが、今回タイトルにおいているスタックトレースに絞った場合は以下のようにします。

// errors に stacktrace を追加する。
errors.WithStack(err)

// 追加した stacktrace に何かしらのメッセージを追加してエラーとして出力する。
errors.WithMessagef(errors.WithStack(err), format, args...)

スタックトレースのフォーマットは以下のように表示されます。

# サンプルの出力です。(今回は connect-go を使ってるプロジェクトでのスタックトレースを出力してみました。)

$ErrorMessage

(1) $Error Message
Wraps: (2) attached stack trace
  -- stack trace:
  | github.com/$ErrorMethod
  |     $ErrorFiles...
  | github.com/bufbuild/connect-go.NewUnaryHandler[...].func1
  |     /go/pkg/mod/github.com/bufbuild/connect-go@v1.9.0/handler.go:52
  | github.com/emahiro/qrurl/server/intercepter.NewRequestLogIntercepter.func1.1
  |     /app/intercepter/request_log_intercepter.go:25
  | github.com/bufbuild/connect-go.NewUnaryHandler[...].func2
  |     /go/pkg/mod/github.com/bufbuild/connect-go@v1.9.0/handler.go:81
  | github.com/bufbuild/connect-go.(*Handler).ServeHTTP
  |     /go/pkg/mod/github.com/bufbuild/connect-go@v1.9.0/handler.go:239
  | net/http.(*ServeMux).ServeHTTP
  |     /usr/local/go/src/net/http/server.go:2500
  | net/http.(*ServeMux).ServeHTTP
  |     /usr/local/go/src/net/http/server.go:2500
  | golang.org/x/net/http2/h2c.h2cHandler.ServeHTTP
  |     /go/pkg/mod/golang.org/x/net@v0.12.0/http2/h2c/h2c.go:125
  | github.com/rs/cors.(*Cors).Handler.func1
  |     /go/pkg/mod/github.com/rs/cors@v1.9.0/cors.go:236
  | net/http.HandlerFunc.ServeHTTP
  |     /usr/local/go/src/net/http/server.go:2122
  | net/http.serverHandler.ServeHTTP
  |     /usr/local/go/src/net/http/server.go:2936
  | net/http.(*conn).serve
  |     /usr/local/go/src/net/http/server.go:1995
  | runtime.goexit
  |     /usr/local/go/src/runtime/asm_amd64.s:1598
Wraps: (3) $ErrorEethod
Error types: (1) *hintdetail.withHint (2) *withstack.withStack (3) *errutil.leafError"

まとめ

Go で開発をしているとエラーハンドリングの中でも頭を悩ませるポイントが StackTrace だと思いますが。runtime package などを使って自前で作ることもありますが、cockroachdb/errors はスタックトレースのライブラリとしては有用なのではないかなと使ってみて感じました。

See Also

zenn.dev

tech.kanmu.co.jp

遠出の練習をしてきた

Overview

娘の月齢がもうすぐ3ヶ月を超えるので、遠出の練習に越ヶ谷レイクタウンまで行ってきました。

首もだいぶ安定して体もがっしりしてきていて、秋に旅行に行きたいと思っていたので、その準備として実際乳児を連れて外出するということがどういうことがあるのかを親としてもシミュレーションしたかったのと、あまりに猛暑続きで最近外出できていたなかったのでそのついで出かけて来ました。

子連れの遠出について

結論いうと、思っていた以上に疲れました。

まだ乳児なので授乳タイミングとオムツ替えのタイミングが読めないのでそれを気にしつつ移動するっていうのは思った以上に気苦労がありました。

良かったのは、今回行ったのは越ヶ谷レイクタウンで郊外の大型ショッピングモールに行ったということです。

郊外のショッピングモールのいいところは

  • 休みの日にはほぼ家族連れしかおらず、子連れで気にすること(子供が泣いちゃったりとか、騒いだりとか)があまりない。基本お互い様なので。
  • ベビーカーで移動してもそれなりに通れるくらい通りが広い。というかそれ前提で整備されてる。※ もちろんお店の中にベビーカー連れて行くのはちょっと難しいですが。
  • 育児を助けるアメニティが充実してる。イオンは場所によると思いますが、レイクタウンくらいになると授乳室やオムツ替え室だけでなく、哺乳瓶を洗う場所や調乳用の熱湯が出るウォーターサーバーや消毒用のレンジまで用意してくれてたりして、ちょっと感動します。

逆によくないところは

  • めちゃくちゃ混む
  • 車前提かつそこに止められるかは運。電車移動も可能ですが、遠出において電車移動はあまり現実的でない(後述します)

くらいでしょうか。

育児の当事者になると、郊外の大型施設のアメニティの充実ぶりを肌で感じることになり、もはや休日のお出かけはこうした郊外のショッピング施設(イオン、ララポ、アウトレットモール)以外にできない気もしてきます。
都内のショッピング施設とかも行かないわけではないので、実は行ったときにそういうアメニティ周りも最近目を向けることがあるのですが、新しめの施設(駅構内含む)ならまだしも、どうでないところはまだまだ整備中なんだなと感じます。特に電車移動が前提になる都内の移動についてはは駅構内に充実してないとちょっと移動がしんどいなと感じそうです(大きな駅の比較的新しい駅校舎の中はだいたい完備されてますが)

移動について

子連れの週末のお出かけは自宅の周囲の散歩なり、行って返ってくるまでに15~30min 前後、という移動距離の範囲を除くと基本的に 「車移動が圧倒的に楽、というか必須」になると思います。

子連れの外出には思った以上の荷物が必要になります。(特に月齢が浅い子供ほど、授乳、オムツのタイミングは読めないので足りないときのことを考えて持っていくものが多くなりがちです。)

先輩パパさんの話の中に「ベビーカーセット荷物運ぶのがしんどいので外出が近場の公園とかになりがち」ということを聞いていたのですが、これは事実あると思います。

車であれば子供がぐずっても別に誰に迷惑かけるわけでもないですし、何より上述した荷物一式をすべて詰め込んで移動できます。車は倉庫になるので。

あまり周りの声を気にしてもしょうがないじゃないか、育児中なんだから、と思いこそすれ、やはり公共交通機関を使った移動であれば、両親にかかる負担は子供以外にもあるのは事実で、そういうのが積み重なって出不精になりがちになるのかなと思いました。家庭によると思いますが、うちの場合は夫婦揃って面倒くさがりなのでそういうことが重なると出かけなるのは想像に難くないです笑

まとめ

郊外のショッピングモールは偉大。
イオンと三井不動産はファミリーの味方。

育休から復帰しました

Overview

4月末に第一子である娘が生まれてから3ヶ月間の育休を頂いており、昨日から現職に復帰しました。

娘が生まれたときのエントリは以下。

ema-hiro.hatenablog.com

初めての育児だったので、感じたことと買ってよかったもの、使っていたものたちをつらつらと書きます。

育休について

まず育休について。

先の娘が生まれたエントリにも記載してますが、育休を取る/取らない、取るとしてどれくらい取るか、というのは家庭の選択の問題でもあるので、「絶対取るべし」とは思いませんが、取得した人がおおよそ口を揃えて言うことと自分も同じように「取ってよかった」と思います。

理由は育児に参加できるということもあるんですが、子供の成長速度は自分が思っていた以上に早く、見逃してしまうことがもったいない瞬間がたくさんあったので、それらを一緒に体験することができた、ということが一番大きかったです。

育休におけるキャリア云々の話については、現職は当たり前のように育休を取得する男性が多かったので普通に取るものだと思ってて、キャリアのリスクみたいなことはなーんも考えてませんでした。取った直後は育休のこと「ポケモンゼルダとスプラやるための休暇」くらいに思ってました。全然そんなことなかったですが...。
そういえば、育休を取ることを両親、親族に伝えると割りと概ね好評だった一方で「令和だね〜」みたいな感想をもらったりもしたので、男性が育休を取得すること自体の意識は進み始めているとは言え、ある年齢より上に行くとまだまだ珍しいことのようでした。

1点反省があるとすれば、育休の取得のためには ある程度のキャッシュ(現金)が必要になる ということについてもう少し事前に目を向けておくべきだったかなということです。
育休にはその手当が健康保険から支給されますが、上限があり、お金の面だけ持ち出すと手取りが多い人ほど育休を取得することそれ自体が「キャッシュフローの観点では損」になります。また手当の支給にはタイムラグ(大体2~3ヶ月)があるので、育休スタートから短くとも2ヶ月は各種支払(家賃orローン、クレカ、光熱費、食費、育児関連費)に耐える現金をプールしておく必要があります。特に5月6月は各種税金(自動車税、固定資産税 etc..)の納付時期であり、さらに6月を跨いてしまうと住民税の普通徴収に一時的に変更(*1)されて、前述のコストに加えて税金でお金がごっそりなくなる事象が発生します。
自分の場合住民税の普通徴収が完全にノーマークで、かつ戸建のローンもあり、事前にローン口座に幾ばくか現金を移していたのもあって結構ヒヤヒヤしました。

3末が現職の賞与タイミングだったのもあり、それを全部育児関連費用に当てるように予算を組んでいて、ある程度キャッシュがある状態だったのでなんとかやりくりできましたが、この育児休業手当の支給のタイムラグと支給上限があることで給与の手取りが多い人ほど損をする現状の仕組みがある限り「お金が理由で育休を取得しない」という人がいても不思議ではないと思います。
男性の育休を推進したいのであればこういったキャッシュフローが一時的に悪化するような状況は早晩改善されて欲しいなと思います。

※1. 税金の先払いなのでもともと特別徴収の場合、復職後に特別徴収に切り替わります。事前に払い込んでる金額が多い場合、翌年の6月まで若干手取りが増えます。

育児について

次に育児について。

同年代ですでに経験してる人も多い中、自分にとっては初めての経験だったのでハマりどころにちゃんとハマりながらの試行錯誤した日々でした。
結果としては「奥さんにおんぶにだっこ」状態でした。やはり母体の中で10ヶ月かけて準備する母親と、いきなり育児が始まる父親では土台からまるで違うので、スキルゼロの状態から始まって、しばらくはほぼ戦力外と言っても過言ではありませんでした。
今となっては慣れましたが、育児の実情を今まで雰囲気で知った気になっていて自分事として捉えたことはなかったので、今回体験してみてその大変さを改めて痛感しました。

育児それ自体は大なり小なりのタスクの連続で、それら一つ一つをこなしていくこと自体はさほど難しいことではない、というのが自分の感じているところではあるんですが、問題は「そのタスクのタイミングがいつ来るか全くわからない」ので、常に脳内のリソースの一部を子に向けなければならず、そしてその状態が24時間続く生活の中で徐々に体と心が疲れていきました。
僕自身は体力と気力にはそれなりに自信があるほうだと思っていたのですが、それは単なる驕りで、1週間経過したくらいから体調とメンタルが崩れ、結果として育休最初の1ヶ月は育児するか寝るかの2択という生活をしていました。数少ない娯楽はポケモンする、アニメ見る、漫画読む、ジムで体動かしてサウナ入ってくるくらいででそれ以外のことをやる気力は一切湧きませんでした。育休中に積読消化!みたいなことをほざいていた過去の自分は結構アホだったなと思います。疲れ果てて活字なんか読めませんでした。
そう言えば、育児始めたてでメンタル的にしんどいときに母親側のケアは行政がサポートしてるサービスも含めていくつかありますが、父親側のケアは探してもすぐには出てこないなということに気づき、こういうところでも男性が育児に参加していくための土壌はまだまだ整備途中であることを感じました(自治体ごとにまちまちだとは思いますが)

まぁでもどんなに疲れていようと我が子は可愛いもので、自分でもこういう感情を持つんだなーと思うと人間の遺伝子レベルの本能の強さを感じました。寝てれば天使、起きたらただの怪獣なんですけど可愛いから許す、みたいな感じなります。

少し楽になったのは1ヶ月過ぎたくらいからで、うちは奥さんの意向で退院してすぐに夜間消灯する生活を始めていたので、2ヶ月目くらいから徐々に夜寝るリズムが作られて、ミルク対応はあれど深夜は自分の時間を取ることができるようになりました。
その結果、徹夜してるのに自分の時間を持てることが嬉しく、体は疲れててもメンタルはだいぶ回復しました。
今思うと夜よく寝てくれるというのは単に娘の特徴でしかなく、個人差はある問題かと思いますが1度寝たらまとまって寝てくれるリズムを先んじて作れたのは生活の面ではプラスでした。

ここで作れた深夜の自由時間の殆どはゼルダとスプラやってましたが、ゼルダが1段落した時期くらいからプログラミングの禁断症状が出て、毎日深夜にコードを書く生活をしており、多分エンジニアキャリアの中で一番と言ってもいいくらいプライベートでコード書く期間を過ごしてました。間違いなく仕事してるとき以上にコードに触っていたと思います。

先週くらいからもう夜は子供と一緒の時間に寝るようになったので、ものごころついてから変わることなかった夜型の生活リズムがあっというまに朝方に変わりました。育児の効用が凄すぎます。

買って良かったもの ~育児編~

Amazon Echo

もともとスマートホームにしようと思って購入してたのですが、現在はほぼぴよログの入力インターフェースおよび CuboAi のモニターとして使ってます。
Amazon Echo が1番育児で活躍してるガジェットかも知れません。
あまりに夫婦で「アレクサ」と言いまくってるので「ママ」「パパ」と呼ばれる前に娘に「アレクサ」と呼ばれる可能性があります。

調乳ポッド

80度以上のお湯をキープしてくれます。
そろそろミルクの時間だなーという時間の少し前くらいからお湯沸かしてキープしておけるので多少時間がズレたときに沸かし直す手間が省けて良いです。

バウンサー

原因不明のぐずりで全然泣き止まないときでもバウンサーに突っ込んで揺らしてると、8割くらいの確率で大人しくなります。

ベビーモニター

jp.getcubo.com

娘の寝返り監視のために導入しました。
またベビーモニター導入と同じタイミングで娘の寝室をリビングから2Fの寝室に移したので、娘と同じ空間にいる時間をちょっと減らせて気が楽になる、という副作用もありました。
娘が同じ空間で寝てるとちょっとした物音でも起きちゃうことを気にして、なるべく音を立てないようにしながら活動するストレスがありましたが、一時的に寝る部屋を別部屋に移せたのでそういった懸念はなくなりました。

リンサークリーナー

ソファに娘がゲロってもすぐにゲロった部分を掃除できるので、ゲロが怖くなくなりました。

番外編1 - あって良かったもの

これは確信を持って言えるのですが、育児には自家用車がある方が圧倒的に楽です。
母子の退院時に始まり、1ヶ月診断、予防接種、ちょっとしたおでかけなどなど全てにおいて自分達専用の動く倉庫があるとないとでは外出時のストレスが段違いで、もはや自家用車がない状態で子連れ外出のやり方がわかりません。

ドラム式洗濯乾燥機

ミルクはこぼすわ、吐き戻しはするわ、季節柄すごい量の汗かくわでとんでもない量の洗濯物が日々排出されてて、とてもじゃないけど洗濯して外で干すという作業は無理of無理でした。

これはたまたまなのですが戸建がある状態で育児スタートしたので、集合住宅だと感じる煩わしさみたいなものとほぼ無縁でいられて精神衛生上良いことしかありませんでした。

Ex.

  • 子供が泣いても近所に迷惑かけない。
  • 戸建で部屋が余ってるのでとりあえず困ったものは余ってる部屋に突っ込んでおける。
  • 駐車場から家の玄関が近いので、ベビーカー始め荷物の積み下ろしが楽。

etc...

番外編2 - 使ってるアプリ・サービスなど

ぴよログ

www.piyolog.com

子供のアクティビティを記録できるアプリです。
夫婦で子供の寝起き、ミルク、うんち、おしっこなどの各種アクティビティの記録を共有できるので、余計な確認(ミルクいつあげた?みたいな会話) が減りますし、もうぴよログなしには育児のコミュニケーションが成立しなくなっています。
体重や身長、予防接種の記録などもできるので、特に検診時の質問事項にすぐ答えられるのも良きでした。
もはや子育ての健全な運用にもログが欠かせない時代になっているんですね。

ぴよログは Alexa にも対応しており、授乳中やおむつ替えてる最中など、両手が塞がってスマホ使えないときでも入力できるインターフェースを用意してくれてるあたり「わかってる」アプリだと思います。

作ってるみなさん本当にありがとうございます。課金はしてないんですけどね。

みてね

mitene.us

娘の写真は「みてね」を使って夫婦の両親に共有しています。
家族みんなが入ってるLINEグループとか作ってそこで投稿しても良かったんですが、まぁLINEはお互い色々気を使うし、写真や動画見る以外の余計なコミュニケーション発生させたくなかったので専用のアプリ使うことにしました。

「みてね」が良かったところは管理者権限でメンバーのやれることを絞れること(*1)と、アプリの登録導線がめちゃくちゃわかりやすかったことです。
あんまりスマホに詳しくない夫婦それぞれの両親にも LINE でリンク送るだけですぐに登録してもらうことができました。

あと「みてね」には誰が写真を見たのかを確認できる機能があるのですが、普段連絡をよこさずスマホもそんなに得意じゃない自分の父が毎日必ず1度はログインしてる形跡を確認できたりして、この時代にこういうサービスがあって良かったなと思います。

*1. 両親のコメントを許可してしまうと特に姑(僕の母)からのコメント対応に奥さん側が苦慮するという話を聞いていたので家では僕たち夫婦以外のコメント権限を剥奪して運用してます。

メルカリ

メルカリは育児を通して一番価値の再評価をしたサービスかも知れません。

今までガジェットを出品することでしか使ってなかったのですが、育児に必要なものは単価がちょい高めなのに寿命が早くて3ヶ月、ベビーカーみたいなものでも数年というスパンでいらなくなるので、中古を嫌う以外に新品を買うメリットがあまりありません。
多少の型落ちでも気にしないのであれば、メルカリで調達するのが一番コスパ良く、調達価格によってはレンタルするより安く済みます。
また、使い潰したところでメルカリで買ったものなので特に気になることはなく、ちょっときれいに使えたらまたメルカリに出品できちゃうね〜とか夫婦で話してて、必要なものの半分以上はメルカリで調達しました。

自分は買う側としてメルカリをちゃんと使ってこず、全然その辺の土地勘がなかったのですが、奥さんがメルカリヘビーユーザーで売るのも買うのも自分とは比較にならないくらいのプロだったので、チャイルドシートとベビーカーをセットで3万弱で調達してきた(型落ち度合いが2年程度)のは流石に驚きました。
(しかも出品者が近くに住んでいたので車で取りに行ったので配送料も梱包解く手間もゼロ。メルカリってジモティーみたいに使えちゃうんですね...w)

まいにちのたまひよ

st.benesse.ne.jp

奥さんに伝えて登録してもらったんですが、同じ月齢の部屋作ってくれるので、同じ月齢の子の成長段階を見つつ、我が子の状況を客観的に見ることができてよかったです。
ミルクの量を増やすタイミングとかも、基本は個人差があるものなのですが、目安として同じ月齢の子がどれくらい飲んでいるのかを知ると、じゃあ増やそうか、とかもうちょっと同じ量でいくか、みたいなことを決める1つの指標にできました。

なお、奥さんはここで友達作ってて、インターネットで仕事してる自分以上にインターネットをインターネットとして使っていて尊敬しました。(こういう掲示板ではすぐROM専になる自分とは大違いでした)

こどもチャレンジ

おもちゃのサブスク程度のつもりで始めました。

その他の感想

  • 今までも友達の子供に会わせてもらう機会があったのですが、それはだいたい生後半年過ぎたあたりのタイミングが多く、この育児最初のしんどい時期を過ぎた状態で会っていて、かつこの時期を経たあとの話をメインで聞いていたので、実際にスタート時期の話は身の回りでも聞く機会が少ないんだなと、今更ながらに気づきました。
  • 育児関連の行政サポートは東京都が圧倒的で、東京近郊だと市区町村によってまちまちでした。たまたま今自分が住んでる地域はサポートが少なく、郊外だからといって子育てがし易いということはなかったので、地域ごとに明確に分かれるポイントになりそうです。この辺は自分もリサーチ不足だったところで「できるなら都内」というのは同意するところがあります。まぁそれに耐えるだけの稼ぎがあればの話ですけど。
  • あまりに機械やテクノロジーの恩恵を受けすぎてて、これらがなかった自分の両親以上の世代はどうやって育児してたのかマジで疑問です。(そもそも核家族じゃないというのはありそう)
  • 育休始めとした職場の育児に関するサポートは相当充実しており、現状自分はかなり恵まれてる方だなと思いました。
  • 税金ヤバイ。育休中は住民税を非課税にしてほしいです。
  • 子供のマイナンバーカードは個人番号が通知されたら即作ったほうが良い です。
    • 子のKYC及び親子であることの証明が必要になる手続きにおいて住民票を使わず親子それぞれマイナンバーカードがあれば事足りるケースがあり、いくつかの手続きが超簡単になります。僕はこの恩恵を子のゆうちょ口座を作るときに受けていて、親子それぞれのマイナンバーカードの提出のみで口座作成できました。他にどんな手続がこの方式で対応できるかを調べ切っている訳では無いですが、もう本人確認のデファクトマイナンバーカードに移行してるので、子供の本人確認の面倒臭さから開放されるためにもマイナンバーカードを作らない理由はなさそうです。
    • 保険証としての機能も付いたので便利さは増していくはず。
    • マイナポイントは出生タイミングの関係で受け取れなかったです。悲しい...。
  • パパ育休の仕組みが難しく、厚生省のドキュメント見てもいまいち手当の振り込まれるタイミング等々が分からずだったので、もうちょっとわかりやすい資料を用意してほしいなと思います。
  • 行政の主催してるパパママ向けのイベントには参加して損はない。
    • 自宅のすぐ近くに公民館の分館があり、そこで親子イベントが毎月開催されてることを知ったので参加してみたのですが、親同士繋がりが作れたりしたので参加しておいてよかったなと思います。企画の内容によっては月齢的に合わないケースがあるので毎月参加していたわけではありませんが、参加したときに同じ班になった自分たちよりちょっと月齢の進んだ子の保護者に話を聞くだけでも結構学びになることはありました。
    • 特に保育園前の育児をしてると家族と時々会える友人を除いて、全く知らない人と知り合う機会というのはほぼなくないので、気分転換に地域のコミュニティをを利用するのもなかなか良いものでした。
  • 男性側が突発的なワンオペに対応できるくらいの最低限のタスク消化スキルを付けておくと、奥さんが整体に行ったり美容院に行ったりするなどの外出する時間を作ってあげられます。子供は可愛くても何かとストレスがかかる存在でもあるので、適度な息抜きは夫婦ともに必要ですね。

Cloud Functions で ImageMagick 使って OGP 画像を生成・表示する

Overview

タイトルのとおりです。
2023年時点で Next を使って実装してある自分のポートフォリオサイトの Note ページに書かれた各ページのエントリのタイトルの OGP 画像を表示させます。エントリの本体は Notion にサーブしており、API 経由で Notion の Article 情報を取得してタイトルのOGP画像を生成する、という流れです。

今回は自前で OGP の画像を生成して Next.js 側で参照する方法を記載します。

自前で OGP を生成する

自前で OGP 画像を生成するときに考慮することは以下です。

  1. 何を使って画像を生成するか。
  2. 生成した画像の保存先

何を使って画像を生成するか

これが一番頭を悩ませました。

今回はカスタマイズしたOGP画像を生成する上で考慮したパターンは以下の3つです。

  1. 自前で実装せず Next.js で用意されている OGP の機能を利用して画像を生成する。
  2. Puppeteer を利用して生成した HTML をキャプチャした結果を OGP として利用する。
  3. ImageMagick を利用して画像を生成する。

Next.js で用意されてる OGP 向けの機能を利用する

公式のドキュメントで記載されてる以下の方法で実装する方法です。Generate images using code を使ってタイトル部分を動的に変更することが可能です。

この方法ではOGP画像でタイトルをエントリごとにカスタマイズするときにフォントが日本語である必要がありますが、公式ドキュメントで指定されている方法だと漢字混じりのタイトルで Unsupported OpenType が発生してしまい、ImageResponse を使った OGP の作成ができないことがネックになり ImageResponse での実装を見送りました。
※ ちなみにこの ImageResponse というのは過去バージョンの Next では vercel/og 呼ばれていた機能の現行バージョンの機能であって、ほぼほぼ vercel/og と同じことができます(便利機能をそのまま本体が取り込んだっぽい)

ref: @vercel/ogで記号文字を入れて"Unsupported OpenType signature"になって困ったけど解決したメモ

漢字対応してるフォントを ImageResponse で登録したときに OpenType のエラーが発生する問題については、自分の実装方法が間違っているかもしれないということもも否めませんが、そもそも Vercel 以外に乗っかる場合には Next.js を使うときにいくつか公式ドキュメントで紹介されている実装方法がそのまま使えないユースケースがありそうでした。結局のところ、Next を使う場合には Vercel に乗っかったほうが 100% 楽で、Static export などせずにランタイムごと Edge 環境にデプロイしてしまう、という技術選択が正しいという状況が現時点では生まれているんでしょうかね。

Puppeteer を利用してキャプチャする

Next.js で標準で用意されてる ImageResponse では日本語フォントの対応に難儀したので、FaaS で画像を生成する方針に切り替えます。

いくつか調べましたが、FaaS 上で画像を生成し、それを Strage に保存してクライアントから参照するありがちなパターンを採用します。

その上で何を使って FaaS 上で画像を生成するのかを考え、最初は puppeteer を使って FaaS 上でタイトルのみを表示させた HTML でページを組んでそれをキャプチャして画像として保存する、という方針を採用しました。
実装の詳細は以下のエントリが詳しいです。

yamahitsuji.medium.com

依存するライブラリも、実装方法もほぼここで記載されている内容そのままで実装し、動作させることもできたのですが、いくつかハマったところがあり、結論としてはこの方法は不採用にしました。

以下ハマったところです。

  • 日本語フォントが反映されず、真っ白なキャプチャを撮影してしまう。

  • medium で書かれた時期の puppeteer が古く、最新の puppeteer では chrome-aws-lambda のライブラリも互換がない。

    • これ自体は高速化するツールというだけなので chrome-aws-lambda を外しても動作はしました。
  • 動作が安定しない

    • 上記2つのトラブルを解消し、実際に puppeteer での HTML ページのキャプチャを動作させた所、動作するときとしないときがあり、挙動が安定しませんでした。原因ついては結局わかっていませんでしたが、puppeteer を動かすということは FaaS 上でブラウザを起動しキャプチャをするということになり、FaaS でやるには重たい処理にであることはわかります。実際 CloudFunctions のリソースの設定も結構モリモリの構成にしないと、そもそもブラウザが起動しないということもありました。

結局のところ最後の動作が安定しないことが決め手になって puppeteerの採用は諦めました。

ImageMagick を利用する

準備

Firebase Functions (CloudFunctions) が動作してるコンテナは現在デフォルトで ImageMagick のコマンドが使えます。
実行環境が nodejs である Firebase Functions 上で ImageMagick のコマンドが利用できるかどうかは以下のコマンドを使って確認できます。

await spawn("convert", ["-h"])

// or

await.spawn("which convert")

これで標準出力の結果でエラーが返却されない(両者ともに conver コマンドは存在することがわかる結果を出力する)ので Firebase Functions 上で convert コマンドを利用して画像を生成します。

ImageMagick での実装

実装自体は単純で convert コマンドを使用するときの CLI の中身を node の子プロセスの中で実行するだけです。具体的な実装については以下になります。

const createOgpImage = async (title: string): Promise<string> => {
  const outFile = path.join(os.tmpdir(), `ogp.png`);
  // create ogp image
  await spawnSync("convert", [
    "-font",
    "./font/NotoSansCJKjp-Medium.otf",
    "-pointsize",
    "38",
    "-fill",
    "black",
    "-gravity",
    "Center",
    "-annotate",
    "+0+0",
    title,
    "./ogp_src.png",
    outFile,
  ]);
  return outFile;
};

日本語フォントの設定

ここで ImageMagick を使うまでにハマっていたところとして日本語フォントの設定があります。これは package.json と同階層から指定した相対パスにフォントデータを入れて、convert コマンドを実行するときにその相対パスfont option で指定します。
実際のフォルダ構成は以下のようになります。

├ package.json
├ font
     └ $フォントデータ 

これで convert -font "./toFontPath で日本語フォントを指定して画像を生成します。

補足

ImageMagick を利用する前にもう少し軽量な画像編集ライブラリである sharp や nodejs 上で image magick を使うための npm package (公式ドキュメント にも記載されている)の gm を利用することを検討しましたが、sharp には画像作成の機能はなく、gm を使った場合は Functions codebase could not be analyzed successfully. というエラーが発生してデプロイすることができなくなってしまったので、ImageMagick のコマンドを CloudFunctions でそのまま使う方法を採用しました。

生成した画像の保存先

今回は Firebase を利用してるので保存先は Firebase Strage (Google Cloud Strage) に保存します。

生成タイミングは Next のアプリケーションをビルドするときに、Server Component 内で Notion の API にアクセスして記事情報を取得し、タイトル情報を取得後、CloudFunctions を HTTP 経由で Trigger して、CloudFunctions で生成した画像を保存します。図に書くと以下のようなピタゴラスイッチを作るイメージです。

ビルド時に CloudFunctions を Trigger する。

Next.js のアプリケーションのビルド時に Notion API にアクセスしてタイトル情報を取得し、OGP を生成したあとにブログエントリの各ページを SSG で生成して Firebase Hosting にデプロイする、という方法を採用しました。
今回 SSG を利用したのは、OGP の生成だけでなく、エントリのリストやエントリの中身を取得するのに都度、Notion の API を Call する場合、クライアント側にクレデンシャルを露出させないと行けない可能性があったので、ビルド時にエントリ本体を SSG で生成し、 OGP の生成・保存も Next のサーバーコンポーネントで行いました。
サーバーコンポーネントを利用したのは上記の API アクセスや CloudFunctions へのリクエスト情報をクライアント側に露出させるなく、クレデンシャル情報やアクセス先の URL をクライアントと同じリポジトリの中で管理することができるからです。
ビルドプロセスに組み込むためには以下の Next 側で提供されてる generate 用の関数を利用します。

ざっくり説明すると generateStaticParams は SSG で生成されるページの Index を生成し、generateMetadata でその生成された Index を受け取って metadata の生成を行います。metadata の生成時にエントリのタイトル情報が必要になるので、ここで Notion API に Index (Notion API で言うところの Article の PrimaryKey) を渡してエントリの情報を取得します。

サンプルコードは以下です。

// app/note/[id]/page.tsx

export async function generateStaticParams() {
  const resp = await FetchNotionArticles({ params: {} });
  return resp.results.map((result) => {
    return {
      id: result?.id,
    };
  });
}

export async function generateMetadata({
  params,
}: {
  params: {
    id: string;
  };
}): Promise<Metadata> {
  const data = await FetchNotionArticleDetail({
    params: { id: params.id, tags: [] },
  });
// 略
  return {
      title: data.title,
      openGraph: {
        // ref: [https://nextjs.org/docs/app/api-reference/functions/generate-metadata#metadata-fields] を参考に OG の必要な値を追加する。
      },
      images: [
          {
            url: `保存した画像の CloudStrage上の URL を入れる`,
            width: 1200,
            height: 630,
            type: "image/png",
          },
        ],
})

まとめ

Next.js の OGP の機構をそのまま使えると思ってましたが、日本語フォントとのかみ合わせがうまく行かない問題があって、ちょっと遠回りですが、GCPピタゴラスイッチを実装しました。
Next.js を使うなら Vercel に完全に乗っかったほうが楽だなーと思いましたが、元々 Firebase に乗っかってるプロダクトの上での実装だったので今回のように生の ImageMagick を使う方法を採用しました。

SeeAlso

GitHub Actions で CloudRun にデプロイする

Overview

CloudRun で開発したアプリケーションを GitHub Actions 経由で CloudRun にデプロイします。
202307時点の公式のドキュメントの記載だと不十分な部分があったので、現時点でのGitHub Actions 経由でのデプロイ方法を記載します。

準備

以下の API を有効化しておいてください。

  • IAM Service Account
  • Cloud Build
  • Artifact Registry

Cloud Run のデプロイ方法

GitHub Actions を経由する前に Cloud Run にはいくつかのデプロイ方法があります。これは以下の公式ドキュメントに記載してあります。

cloud.google.com

Cloud Run にアプリケーションをデプロイする方法は以下の3つがあり、どれを採用しても大丈夫です。

  1. Docker Image をデプロイする
  2. GitHub と連携する
  3. ソースコードからデプロイする
    • ソースコードからデプロイするときにデプロイするソースのディレクトリと同じ階層(ex. Go の場合は go.mod と同じ階層)に Dockerfile がある場合、その Dockerfile を元に Image をビルドするようになります。そのため、デプロイコマンド (gcloud run delpy) 実行時に image option を指定する必要はありません。

今回自分は 3 のソースコードからデプロイする前提で進めます。理由は最新の main ブランチの状態をデプロイするフローにするには、main にマージ時点での最新版のソースコードをベースにしてデプロイするのが、シンプルかなと考えたからです。
1 のDocker Image は毎回作成して Push するのがめんどくさかったので、すべての処理を Cloud Build に任せて乗っかる、という方法を採用した、という流れです。

環境変数を追加する

実行環境で環境変数を追加する場合は公式ドキュメントでいくつか紹介されてます。 ref: 環境変数を構成する  |  Google Cloud Functions に関するドキュメント

簡単に利用できるのは以下かなと思います。

  1. デプロイコマンド (gcloud run deploy) で環境変数を追加する。
  2. Docker Image をビルドするときに環境変数を差し込む。
  3. .env.yaml を追加する。

環境変数の中身にいくつかクレデンシャルの情報を入れますが、それらを GitHub のコミットに追加したくなかったので 1 or 3 で、今回は 3 を採用して、GitHub Actions の実行時に .env.yaml を作成していく方法を採用しました。

権限について

CloudRun に GitHub Actions (外部サービス)経由でデプロイするには専用のサービスアカウントが必要になります。
このサービスアカウントにどの権限を付与するのか、がドキュメントに書いてないことが原因で色々ハマったので、その権限について書いている、というのがこのエントリの内容になるのですが、 結論から先に書いてしまうとデプロイ用のサービスアカウントに対して以下の権限を追加することで GitHub Actions からデプロイできるようになります。

  • roles/run.admin
  • roles/iam.serviceAccountUser
  • roles/cloudbuild.builds.builder
  • roles/artifactregistry.reader

これは最後に載せている参照先の中の 【Cloud Run】 デプロイするために必要なパーミッション(GCP) - Qiita に書いてある内容そのままでした。

権限付与手順

サービスアカウントの作成と借用権限の設定

$ gcloud iam service-accounts create $ServiceAccount

$ gcloud iam workload-identity-pools create  $PoolName --location="global"`

$ gcloud iam workload-identity-pools providers create-oidc $ProviderName \
   --location="global" \
   --workload-identity-pool=$PoolName \
   --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.actor=assertion.actor,attribute.aud=assertion.aud" \
   --issuer-uri="https://token.actions.githubusercontent.com"

# 権限の借用
$ PoolID=$(
gcloud iam workload-identity-pools describe $PoolName \
      --location="global" \
      --format="value(name)" \
      --project=$ProjectName )
 
$ gcloud iam service-accounts add-iam-policy-binding "${ServiceAccount}@${ProjectName}.iam.gserviceaccount.com" \
   --role="roles/iam.workloadIdentityUser" \
   --member="principalSet://iam.googleapis.com/${PoolID}/attribute.repository/${GitHubRepo}"

権限の追加

gcloud projects add-iam-policy-binding ${PROJECT_ID} \
 --member="serviceAccount:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
 --role="roles/run.admin"

※ 上記の role をそれぞれ追加する。

GitHub Actions の設定ファイル

source からデプロイする場合

jobs:
  deploy:
    strategy:
      matrix:
        os: [ubuntu-latest]
    runs-on: ${{ matrix.os }}

    permissions:
      contents: "read"
      id-token: "write"

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - id: "auth"
        name: "Authenticate to Google Cloud"
        uses: "google-github-actions/auth@v1"
        with:
          workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ secrets.GCP_DEPLOY_SERVICE_ACCOUNT }}@${{ secrets.GCP_PROJECT_ID }}.iam.gserviceaccount.com

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v1

      - name: Deploy to Cloud Run
        run: |-
          echo "GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}" > .env.yaml
          echo "LINE_MESSAGE_CHANNEL_SECRET: ${{ secrets.LINE_MESSAGE_CHANNEL_SECRET }}" >> .env.yaml
          echo "LINE_PUBLIC_KEY_ID: ${{ secrets.LINE_PUBLIC_KEY_ID }}" >> .env.yaml
          echo "LINE_CHANNEL_ID: '${{ secrets.LINE_CHANNEL_ID }}'" >> .env.yaml
          echo "LINE_PRIVATE_KEY: ${{ secrets.LINE_PRIVATE_KEY }}" >> .env.yaml
          echo "LINE_CHANNEL_ACCESS_TOKEN: ${{ secrets.LINE_CHANNEL_ACCESS_TOKEN }}" >> .env.yaml
          gcloud run deploy $SERVICE_NAME \
            --project=$GCP_PROJECT_ID \
            --region=$REGION \
            --service-account=$GCP_DEPLOY_SERVICE_ACCOUNT@$GCP_PROJECT_ID.iam.gserviceaccount.com \
            --allow-unauthenticated \
            --cpu=2 \
            --memory=512Mi \
            --timeout=60 \
            --platform managed \
            --env-vars-file ./.env.yaml \
            --source ./$SourceRepo

Docker Image からデプロイする場合

jobs:
  deploy:
    strategy:
      matrix:
        os: [ubuntu-latest]
    runs-on: ${{ matrix.os }}
    defaults:
      run:
        working-directory: ./$SourceRepoName

    permissions:
      contents: "read"
      id-token: "write"

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - id: "auth"
        name: "Authenticate to Google Cloud"
        uses: "google-github-actions/auth@v1"
        with:
          workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ secrets.GCP_DEPLOY_SERVICE_ACCOUNT }}@${{ secrets.GCP_PROJECT_ID }}.iam.gserviceaccount.com

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v1

      - name: Authorize Docker push
        run: gcloud auth configure-docker

      - name: Build Images
        run: |
          echo $EnvNameKey=$EnvNameVal > .env
          docker build -t asia.gcr.io/${{ secrets.GCP_PROJECT_ID }}/$ImageName:$TagName --platform linux/amd64 .

      - name: Push Images
        run: docker push asia.gcr.io/${{ secrets.GCP_PROJECT_ID }}/$ImageName:$TagName

      - name: Deploy to Cloud Run
        run: |
          gcloud run deploy $SERVICE_NAME \
          --project=$GCP_PROJECT_ID \
          --region=$REGION \
          --service-account=$GCP_DEPLOY_SERVICE_ACCOUNT@$GCP_PROJECT_ID.iam.gserviceaccount.com \
          --image asia.gcr.io/${{ secrets.GCP_PROJECT_ID }}/$ImageName:$TagName \
          --allow-unauthenticated \
          --cpu=2 \
          --memory=512Mi \
          --timeout=60 \
          --platform managed

See Also

以下のエントリをいくつか参考にさせてもらいました。

http.Request をコピーする

Overview

Go の http.Request を二度読み込みしたいケースでハマったのでその備忘録です。

どういうケースか?

API を書いていると認証・認可等のAPI全体に関わる共通の前処理を middleware で行い、ユースケースの処理を後続で行うというパターンがよくあると思います。
この際、大体のケースはリクエストヘッダーの情報のみで処理を行い、Body 本体に触ることは少ないかと思いますが、Body の中の情報も認証に利用するケース(*1) に遭遇したときに、前処理で Request Body を読み込み、Close してしまうと後続の本処理で Body を読み込んでも中身が空、という状況に出くわした、というケースです。

サンプルは以下のようなケースです。この場合 HogeHandler で Body を Read してもすでに Close された Request Body を Read してるので Body は空です。
これに対応するために middleware で利用する http Request を別の変数に渡して処理したりもしましたが、http Request はポインタ型で指し示す実体が同じなので、変数を入れ替えて Body を読み込んでもポインタなので original のリクエストのBodyも読み込みが完了してしまいます。

// middleware.go
func VerifyToken(http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        b, err := io.ReadAll(r.Body)
        if err := nil { // error handling }
        defer r.Body.Close()
        // 読み込んだ body の byte 配列を使っての処理
    }
}

// handler.go

func HogeHandler(w http.ResponseWriter, r *http.Request) {
    var v Hoge{}
    decorder := json.NewDecoder(r.Body)
    if err := decorder.Decode(&v); err != nil { // error handling }
    // ※ output -> v is empty
}

// main.go

mux := http.NewServeMux()
mux.HandleFunc("/hoge", VerifyToken(HogeHandler))

*1. 今回のユースケースは LINE Bot を実装してときのケースです。LINE MessagingAPI では webhook に登録した Endpoint において LINE MessagingAPI からのアクセスであることを検証することが推奨されており、署名を検証する ときに Body の中身も検証対象に含めています。

結論: Clone してから Body に詰め直す

以下の Stack Overflow 回答が書いてあります。

stackoverflow.com

上記の発生した対応への修正としては以下です。

  • middleware ではリクエストを Clone してこの Clone したものを使用する。
  • Clone したリクエストから Body を読み込む。(このとき context が同じ場合は元の Request の Body も読み込まれてしまっている)
  • Clone したリクエスト Body の値を middleware では利用する。
  • 読み込んだリクエストBodyを再度 io.Reader に変換して、Clone 元のリクエスト(original) の Body に差し込む。

サンプルコードは以下です。

// middleware.go
func VerifyToken(http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, origReq *http.Request) {
        ctx := origReq.Context()
        copy := origReq.Clone(ctx)
        b, err := io.ReadAll(copy.Body)
        if err := nil { // error handling }
        defer copy.Body.Close()
        origReq.Body = io.NopCloser(bytes.NewBuffer(b)) // 読み込んだ byte を元のリクエストの Body に入れ直す
    }
}

まとめ

Stack Overflow に助けられましたが、1 Request の中で Body を2度読み込むという、珍しいユースケースに当たったので http.Request のコピーとその取り回しの仕方の勉強になりました。

ポートフォリオサイトを Nuxt3 から Next に乗り換えた

Overview

自分のポートフォリオサイト ( emahiro.dev ) の技術スタックを Nuxt3 -> Next に更新しました。
フレームワークの部分のみ置き換え、その他の要素(ホスティング先等)はそのままにしてます。

なぜ変えようと思ったか

個人で作ってるものなのでほとんど気分にはなるのですが、あえて理由づけするなら以下になります。

  1. Vue のパラダイムのみで生きていたので React の空気を感じてみたかった。
  2. 仕事で使う上ではエコシステムが充実してる React を今後技術選択の上で採用していこうと考えていたので触っておきたかった。

特に2つ目の理由についてですが、自分は Vue でも React もどちらがいい、みたいな議論にはあまり興味がなく、技術は選択する人のスキルレベルや好みで選択をすればいいと思っているタイプなのですが、Vue のパラダイムが 3系になってある程度枯れるまでに React のエコシステムが充実して、React の方が現時点ではより枯れてしまったという現象が発生してると感じています。
実際、Vue のパラダイムの中で動いてるプロダクト(Nuxt, Vuetifyなど)は Vue2とVue3 の間で断絶が発生しており、メジャーバージョンが上がる部分で既存の資産が使えない事象が発生しており、ちょっとした負債になりつつあります。( Vue2 系のサポートは 今年まで
実際現職の業務であるツールをフルスクラッチで作り直したときに Vuetify (Vue2系) を採用したのですが、Vue3 に対応した最新版の調査をしたところ Vuetify が 2系でサポートしていたコンポーネントをほぼサポートせず、 Vue3 対応した Vuetify を使うというのはほぼプロダクト作り直しと同義、という事象にぶち当たりました。

結局プロダクトとして採用するには、サポート体制やエコシステム、コミュニティの充実度(= どれだけ枯れているか?)というのが採用の一つの基準になると考えており、その観点では Vercel という企業がそのエコシステムの上で事業をしているので、3系がようやく枯れ始めてきた Vue と比べると現段階では Next に寄せておくほうが無難、という考えを持っています。
ちょっと前に以下のようなブログもバズってましたが、CompositionAPI よりも僕個人の見解としては、このエコシステムのサポート(どれだけ枯れているか度)というものが大きな違いとして発生してしまったのではないかなと考えています。
※ 実際 Nuxt3 はほぼ React を書いてるような雰囲気を感じることもありますし、ある一点に向かって収斂しつつあるような状況にもなっています。

qiita.com

移行手順

  1. もともとの package.json を削除。
  2. Get Started をベースに Next のアプリケーションを生成する。
  3. Firebase Hosting で Hosting するディレクトリを Next が生成する Static page のディレクトリに合わせる (*1)

※1. Firebase Hosting は、設定にある public に指定したディレクトリの直下に index.html があればページを Hosting することができます。

Memo

ここからは移行したときのメモです。

Font を変更する

next/font を使います。
使い方は Font に関する 公式ドキュメント を参照しました。
個人的には以下のサイトもわかりやすくてよかったです。

chocolat5.com

スタイルをあてる

CSS について色々忘れてるところがありましたが css のスタイルを import して className 二指定してスタイルを当てる、というのはわかりやすい設定方法でした。
この辺のスタイル定義のプラクティスがあれば調べたいなと思いました。

個人的には routing(ディレクトリ構造)ごとに css module を用意するのが今のところ良いのかなと思ってますが、いい感じに scoped にする方法を調べたいなと思います。

Static page を生成する。

Static Page の生成について Static Export を参照することで index.html を生成するようになります。
出力されるディレクトリは Nuxt3 のときはディレクトリ名をカスタマイズすることができましたが、Next で Static export を使うケースでは out ディレクトリになります。
なのでこの out ディレクトリを firebase の hosting が参照する public 先に指定します。

この static page の生成モードへの変更は config で以下のように設定します。

const nextConfig = {
  output: "export",
};

Static export を設定してる状態では Image をそのまま使うには最適化モードを false にする。

※ 最適化モードにした状態で画像を扱う方法は自分はまだ調べてません。

Static export の設定してる状態でが画像の最適化モードを off にしないと以下のエラーが発生します。

Unhandled Runtime Error
Error: Image Optimization using the default loader is not compatible with `{ output: 'export' }`.
  Possible solutions:
    - Remove `{ output: 'export' }` and run "next start" to run server mode including the Image Optimization API.
    - Configure `{ images: { unoptimized: true } }` in `next.config.js` to disable the Image Optimization API.
  Read more: https://nextjs.org/docs/messages/export-image-api

最適化モードはすぐには必要ないので一旦 off にしました。next config ファイルで非最適化モードを true にします。

const nextConfig = {
  images: {
    unoptimized: true,
  },
};

favicon を設定する

favicon の設定もほぼ 公式のドキュメント 通りに進めました。
favicon の作成はオンラインで生成してくれるツールがあるので特に困ることはありませんでした。

雑感

Nuxt3 を触るようになってから従来の Vue 感はあまり感じていなかったので、Next を使い始めてもあまり違和感なく触ることができました。
functions の返り値に html を記述していく方法は割と今だと一般的なんですかね?
HTML を雰囲気で書けて、かつ SFC の中で CSS を scoped にできるとうい点で自分は Vue を選択しがちだった、というのがあるので Next に触ってみて似たようなことができるというのは移行のしやすさの観点から考えても学びがありました。

まだ全機能使えてるわけではないのでもうちょっと機能追加しつつ色んな機能を触ってみようと思います。

Nuxt3 での static page の生成には nuxi generate を使う

Overview

タイトルの通りですが、Nuxt3 で静的ページの機能しか使っていないケースにおいては nuxi build じゃなくて nuxi generate を使おうね、という話です。

Firebase とのかみ合わせの問題

Firebase Hosting でページをホストしてる場合、Hosting 側で指定する公開ディレクトリ(public) に生成された静的なコンテンツを入れて置かなければならないのですが、ここに index.html が存在する必要ありますが、 nuxi build は nuxtConfig の設定関係なく SSR 前提の生成物しか吐き出しません(というか nuxi build + nuxi start = nuxi dev みたいなものです。)

nuxtConfig の設定を反映して SSRCSR それぞれに対応した成果物を生成してくれるコマンドは nuxi generate になります。

Firebase Hosting を使ってる場合は上記の通り index.html が生成されている必要があるので nuxi generate を基本的に使おうね、ということになります。

とはいえ

実はこれ、nuxt3 のプロジェクトを作ると自動生成される README に書いてあります。

# 略
# serve with hot reload at localhost:3000
$ npm run dev

# build for production and launch server
$ npm run build
$ npm run start

# generate static project
$ npm run generate

久しぶりに Nuxt 触ると色々忘れてる....。

Firebase Deploy で Failed to get details for project というエラーが出たのでその調査

Overview

自分のポートフォリオサイトをメンテナンスしたときに Firebase 周りの設定も最新の状況に合わせて色々更新したところ、Deploy 時にエラーが発生するようになっていたので、その調査と修正の備忘録です。

主に必要だった内容は以下

  • Firebase を Github Actions から Deploy するときに FIREBASE_TOKEN が使えなくなっていた。
  • Deploy 時に RealTime Database が有効化されていないのでプロジェクトが見つからない。

対応

See Also

zenn.dev

  • 基本的にはこの zenn のエントリの通りに進めた。

qiita.com

  • これは参考にしたけど CI 上でわざわざ JSON にして取り扱う必要はなかった。

firebase.google.com

  • Firebase Hosting に限れば GitHub Actions とのインテグレーションが公式のドキュメントに記載されているが、このインテグレーションは Firebase の他のサービスは対応しておらず、この方式を取るとそれまでは CI 上でコマンド1つで Firebase のすべてのサービスにデプロイで来ていた状態が Hosting のみでプロイできる状態に変わってしまい、他のサービスのデプロイは別途考える必要ができてしまうので採用を見送りました。

父になった

サマリ

  • 家族が増えました。
  • また1つ Pairs に人生を変えてもらいました。
  • 育休取ります。
  • 育児は大変。本当に大変。

家族が増えました

4/23 日に娘が生まれて、人生で初めて「父親」になりました。

自分たちは Pairs で出会って結婚した夫婦であるので、Pairs がこの世になければ娘は生まれてこなかった命であると思うと、Pairs によってまた1つ「人生を変えてもらった」ということになります。
そして自分自身がその仕事に関われていることを考えると、これはかなりエモい気持ちになりました。
Pairs は自分たち夫婦にとっては本当に 「人生にあってよかったもの」 になってます。

入籍報告のときも同じことを書きましたが、「自分が関わっている仕事で、自分以外の誰かだけでなく、自分の人生そのものを変える機会」をもらえていること自体、本当に貴重な時間を過ごしてるなと実感しています。

育休取ります

4月末より3ヶ月の間育休をもらっています。

現職は育休にめちゃくちゃ前向きで、1on1 で奥さんが妊娠したことを報告したときも開口一番「育休取るでしょ?」と当たり前のように聞かれる環境でもあったので、育休を安心して取得でき、この文化を作ってくれた会社と当たり前のように取得していった先輩パパの皆さんには感謝しかありません。

この育休を取得すること自体は、家庭ごとの選択の問題でもあるので育休絶対取るべし!とは思わないのですが、「まぁずっと仕事してきたしここいらで一旦休むか!」という安易な気持ちと、育児に慣れる期間がある程度必要かなと思ったので取得をしました。

この業界にいると男性でも長期の育休を取得することが当たり前になっている風潮を感じますし、僕自信も半年くらい取ろうかなとも考えたのですが、どうしてもお金周りのこと(※1) を考えるとそんなに長くも取っていられないなという現実がそこにはあったため、出産からの母体の回復期間 + 育休中の収入の減少分(手取りがエグいくらい減る)を考慮(※2) して一旦3ヶ月 (予定) としました。

※1. 実際の育休中のお金周りについては厚生労働省のドキュメントを結構読み込みましたが、取り急ぎの脳内マップ(必要な前提知識をぱぱっと頭に入れておく)作成のために以下のスライドに助けてもらいました。

speakerdeck.com

※2. この辺、企業によってはこの手取りの減収分を補填してくれる制度を提供してくれる企業さんもちらほら出てきていていいなと思ってたりします。

育児について

育児は思っていた数倍しんどいです(現在進行系)
基本的なタスク(ミルク、おむつ替え、沐浴、寝かしつけ)の連続で1つ1つはやればできるんですが、ミルク以外はタイミングが読めず、また常に自分のリソースの半分以上が専有されてるような状態なので、落ち着いて自分の事ができないストレスと相まって結構メンタルに来ます。
出産を経てない父親でもかなりきついので出産を経て体ボロボロの母体は言わずもがなです。これをワンオペでやるのは無理ゲーというのは事実でした。

まだ生まれて1週間ちょっとなのでわからないところも多く、未経験のこともまだまだたくさんありますが、少なくともこの数日育児をしてみて思ったのは、自分自身のアンラーニングが絶対に必要で、僕自身は初日にしてその環境変化に体がついていけずお腹を下しました。 そもそもいくら育児が夫婦で一緒に進めていくものとは言っても 10ヶ月の準備期間がある母親と、言葉の通じないモンスターが急に家にやってくる父親とでは育児のスタートラインがまるで違うという前提を忘れており、これに気づいたら多少気が楽になったとともに、大好きな漫画である『アオアシ』の中で司馬選手がアシトにかけた「頭作りかえろ」という言葉がものすごく身にしみました。

最後に

周りからよく言われますが、一昨年入籍して(結婚式をしたのは去年)、今年1人家族が増えてと、人生のスピード感がすごいです。
苦労は増える(増えた)し、これからもっと苦労はあるだろうし、その反面自分の自由は完全に失われ、育児タスクをこなすだけで1日が秒で終わるというのはありますけど、自分以外の誰かの成長を毎日少しずつでも間近で見れるというのは今までにはない感覚で、日々発見があって面白いです。

こういうときに恒例となっている「欲しい物リスト」を一応作ったのですが、直接の知り合い以外からもらうのは気がひけるので「祝ってやってもいいぜ!」という方がいらっしゃいましたら emahiro@(Gメール) までアマゾンギフト券をお気持ち程度いただけると嬉しいです。(全てミルクとおむつとお尻ふきに錬成される予定です。)

Go で重複を削除する処理のパフォーマンスについて

Overview

※ 過去何度か上げた内容のエントリの更新版です。

Go で Slice から重複してる要素を排除する unique の処理について実装方法でパフォーマンスが異なるのでこのエントリを書いています。

最近暇つぶしに少し手を出してる leetCode の配列操作の問題に触発されてこのエントリを書いています。

leetcode.com

なお過去上げた重複削除関連のエントリは以下

ema-hiro.hatenablog.com

ema-hiro.hatenablog.com

具体的な実装方法

今日のネタにする重複削除の実装についてはいかにコードを挙げています。

github.com

パフォーマンスについて

ざっくりパフォーマンスを測定したら以下のような結果になりました。
これだけ見ると Sort した状態の Slice を対象にしてるとはいえ Generics を使った実装は効率がいいことがわかります。
Generics が出てくるまでの実装と比べて for loop 一度で重複を削除できる処理と同じくらいのパフォーマンスになりました。

※ 一方でこんかいのベンチは Flatten かつ Sorted な Slice に限るので、このパフォーマンスを引き出すには条件があるベンチの結果でもあります。

go test -v -bench=. -benchmem
goos: darwin
goarch: arm64
pkg: sample.com/go-sandbox
Benchmark
Benchmark/unique_normal
Benchmark/unique_normal-10          11301798           90.41 ns/op         48 B/op         2 allocs/op
Benchmark/unique_faster
Benchmark/unique_faster-10          856136389           1.404 ns/op         0 B/op         0 allocs/op
Benchmark/unique_generic
Benchmark/unique_generic-10         853362406           1.409 ns/op         0 B/op         0 allocs/op
PASS
ok      sample.com/go-sandbox   4.124s

time.Now() を使ってテスト結果の比較をするときにCreatedAt に幅を持たせる

知らなかったので備忘録です。

Overview

go-cmp を使用して DB の Insert のテストを記述するとき time.Now() を使ってしまうと生成したデータと比較対象のデータでレコードの作成時刻( created_at )で誤差レベルの Diff が生じてしまいます。
※ 生成時刻の time.Now() と比較したい対象(Goでは慣習的に want を使うことが多い)が生成されるタイミングの time.Now() で Call されるタイミングが微妙に異なるから(と言っても ns, ms レベル)

どうやるのか

以下の zenn のエントリに書いてあるとおりなのですが、go-cmp の cmpopts.EquateApproxTime という option を利用します。

zenn.dev

option の引数に指定した duration の分は誤差が合っても同じものとして判定される( Diff の判定をゆるくする)ので Insert や Update のテストで利用することができます。

SeeAlso

pkg.go.dev