satococoa's blog

主にサーバーサイド、Web 系エンジニアのブログです。Go, Ruby, React, GCP, ...etc.

転職して子どもが生まれて6ヶ月間の育児休業に入った

タイトルに詰め込んだ通りの半年を過ごしていました。

転職について

パルス株式会社 → 株式会社ROUTE06へと転職しました。

前職の思い出

パルス含めイグニスグループには2011年9月からお世話になっていたので、もう10年近く勤めさせていただいたことになります。 こんなに長い期間一つの会社に所属したことは今までもなかったので、たくさんの経験をさせていただき感謝の言葉がつきません。

TitaniumやPhoneGapを使ったいわゆるガワネイティブのアプリ開発の経験、RailsでのWebやAPIの開発、RubyMotionでのiOSアプリの開発からのObjective-C→Swiftへの変遷、WWDCへの参加。。など本当に枚挙にいとまがないくらいの技術的な経験をさせていただきましたし、with.isINSPIX LIVEといった0→1のサービス立ち上げの経験もとても刺激的で楽しい思い出ばかりです。

ここ3年ほどははもっぱらサーバサイドをGo、インフラをGCP (App Engine、GKE)、クライアントサイドをAngular、React、Next.jsなどその場その場で最適と思われる技術選定をしてきましたが、結果的にかなり自分の好みで好き勝手やりたいようにやらせていただいていました。

入社時には社員数10名程度だった小さな会社も、いつの間にかバックオフィスが充実し、劇的にホワイト企業に進化し、社員数も100名を超え、マザーズ上場し、そして上場廃止し。。という感じで本当に良い時期に入社し、成長をともにできたなと思っています。

前職でお世話になった方々に改めてお礼を言いたいと思います。ありがとうございました。

どうして転職したか

一つは自分のキャリアを考えてのことです。

前述の通りイグニスグループでとても心地よく働かせていただいていたのですが、ちょっと心地よくなりすぎました。 ふと振り返ってみたときに一つの会社に10年も在籍していて、このままこの会社でしか通用しない人材になるリスクを見過ごしてしまって大丈夫なのか?という不安と、もう一度外の世界や他の業界を見てチャレンジしてみたいという好奇心が高まった結果、徐々に一回他の会社に入って揉まれてみたいなーと思うようになりました。

また、小さな会社がどんどん仲間が増えて大きな仕事ができるようになっていく。。という成長の過程は本当にスリリングで、贅沢を言うのならばもう一度その急成長を経験してみたいなと考えていました。

もう一つの理由は掲題にもある通り子どもができたことです。 ライフスタイルの変化を目前にし、転職のような自分や家庭にストレスがかかる決断ができるタイミングは今なのではと考えていました。 前職も決して子育てがしづらい環境ではありませんでしたしコロナ禍以降はリモートワークで働けていたので問題はなかったのですが、第一子誕生を控えてコロナ禍以降もフルリモートワークで働けそうで、より子育てに優しい環境があれば良いなーという気持ちもありました。

転職活動について

とりあえず昨年11月ごろに転職ドラフトFindyLAPRASに登録してみて、そこからいただいたスカウトにお返事したり面談をさせていただいたりしながら徐々に情報集めを進めていきました。

とはいえ転職のタイミングがとても悩ましかったです。 6月が出産予定日かつそこから半年は育児休業を取りたいと思っていたので、育休前に転職するのかそれとも育休後に転職するのか、でだいぶ迷いました。

転職するつもりで前職で育休を取得するのもちょっと申し訳ない気持ちがしていたのでなるべくなら育休前に転職したいなーという希望は元々あったのですが、転職から1, 2ヶ月ですぐ育休突入になるのもそれはそれで申し訳ないなーという気持ちが拮抗しており、当時面談やメッセージさせていただいた方々には意向を固めきれずにふわふわとやりとりさせていただくことになってしまい、ご迷惑をおかけしてしまったかもしれません。

昨年12月の時点でも気持ちや転職先の候補が固まりきらなかったのもあり、これは焦るよりも少し長期戦になることを覚悟してじっくり選ぶかーという気持ちで、追加でYOUTRUSTにも登録し、育休明けの転職を目指しての転職活動に入ろうとしてました。 そこでROUTE06のスカウトをいただいてそこから一気にことが運び、結果3/29入社となりました。

どうしてROUTE06に決めたのか

これはタイミングがドンピシャだったと言うのが1番の理由です。 こう言うのは結局巡り合いなのかなーというスピリチュアルな気持ちにもなりました。 事業内容も面白そうでしたし、育休についても前職並みの制度(ROUTE06、社員の育休取得を後押しする社内制度を制定|株式会社ROUTE06のプレスリリース)を整えていただけるというご提案もいただき、かつ子育て世帯がとても多いと言うのも第一子の誕生を間近に控えた自分にとってはとても魅力でした。 単純に転職先の会社を選ぶという観点だけではなく、今後の自分が所属する一つのコミュニティとしてROUTE06という会社は良さそうに見えました。

とはいえフルリモートでの転職活動というのは初めてだったので、会社の雰囲気に馴染めそうかどうか(いわゆるカルチャーフィット)は不安だったので、2週間ほどお試しで業務委託として参加してみたのですが、全く問題なさそうだった&スキル的にも多分役に立てそうと思ったのでそこでサクッと決心しました。

この辺の様子は近々インタビューしていただいた記事が公開されるのでもしよろしければご覧いただければ嬉しいです。

2021/06/22 追記: インタビュー記事公開されました note.com

育児休業について

そして妻の入院日からお休みを取らせていただき、そのまま6/13〜12/13の予定での育児休業に突入しました。 コロナ禍のため入院日に僕ができることはただ病院へ付き添い入り口で別れることしかなかったのですが、それでもお休みをとって病院までしっかり見送れて、そのあともビデオ通話で妻とずっと話をしながら出産前日を過ごせたのはとても良かったと思っています。 急な入院日の決定でチームの皆様にも多大なサポートをしていただきました。 タスクをスムーズに引き継いでいただき、そして家庭を100%大事にしてください、すぐ休みに入ってください、と言っていただいたおかげでとても気持ちよく出産日を迎えることができました。とても感謝しています。

まだ退院から4日目というタイミングでこの記事を書いていますが、安心した気持ちで育児休業を取得できて本当に良かったと思います。 100%の気持ちで妻とともに家庭にコミットできている実感があります。 日に日に変化していく子の仕草や表情を妻とともに見守れることはまさに幸せの一言です。

終わりに

今の感動や感謝の気持ちを残しておきたく、深夜テンションで思いっきり私事を書いてみました。 長文になってしまいましたが、どなたかの参考になったりすると幸いです。

zenn.devを使ってみた

zenn.dev

技術的なメモは個人esa.ioだったりこのブログだったりに書いてきたのですが、せっかくなので多くの人の目に触れるところに置く方がメリットがあると考え今話題の zenn を遅ればせながら使ってみました。

エディタもシンプルで必要な機能は揃っている印象ですし、見た目もスッキリしていて好みです。 しばらく使ってみて良い感触だったら人の目に触れさせたい技術メモはzennに積極的に書いていこうと思います。

こっちのブログは読書メモとか個人的な雑記とかに使う感じの棲み分けかなと思います。

WordPressの書籍を読んだ

仕事でまれにプロダクト等の公式サイトを作りたい、という要件が発生します。

コーダーさんが会社にいるわけではないので、ほとんどが「デザインはデザイナさんアサインするから、あとは任せた!」パターンでサーバエンジニアのチームに話が降りてきます。 またそういうサイトはある程度エンジニアなしで更新できることが必須の要件となるため、必然的にWordPressが候補に上がるわけです。

なんとなくWordPressってエンジニアからは避けられがちなイメージもありますが、これだけ普及していてプラグインやテーマなどのエコシステムも強力なOSSなのでせっかくだからきちんと使いこなせるようになっておこう。。という気持ちでこの書籍を手に取りました。

結論から言うと、この本は僕のニーズにぴったりでした。

"デザイン教科書" と言う名前ではありますが、一言で言うとこういう内容です。 → "HTMLとCSSですでに組まれた静的サイトがあり、それをWordPressでよく使われる機能やプラグイン等を使ってCMS化する手順を一通り体験できる。"

HTML、CSS、JS、サーバやPHPのある程度の知識がある人がサクッとWordPressでWebサイトを作るための基本的な知識を速習するのに最適という印象です。 ボリューム感もお手頃で、ささっと斜め読みで2日で一通り読み終えました。

逆に以下についてはあまり詳しく買いていないので、この辺を知りたい人は別の視点から書かれた書籍の方がおすすめかもしれません。

  • PHPの知識 (本書も付録に多少解説はありますが。。)
  • HTML, CSS, JS の知識
  • WordPressのインストールやメンテナンスなどのサーバ運用に関わる知識

少しWordPressへの苦手意識というか嫌悪感がなくなったので、また次にWebサイト構築案件がきたらもうちょいポジティブな気持ちでWordPressにも向き合えそうです。

おまけ: ローカルでWordPressをインストールしてテーマ作成を行うためのdocker-compose.yml

version: "3"
services:
  db:
    image: mysql:5.7
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress
  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    ports:
      - "8000:80"
    volumes:
      - ./wp-content:/var/www/html/wp-content
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_DEBUG: 1
volumes:
  db_data:

goでjsonの一部のデータだけを後から型を指定して取り出したい

例えばAPIなんかでたまにあるこう言う形。 これを受け取った場合、共通処理としてまずはerrorがあるかどうかを見て、もしerrorがなかった場合にはdataをそのAPIに合わせた型にして取り出したいとする。

{
  "error": null,
  "data": {
    "title": "fuga"
  }
}
{
  "error": null,
  "data": {
    "nickname": "foobar"
  }
}

json.RawMessage を使うと後からjsonの一部だけを型を指定して取り出すことができる。 まさに今回欲しかったものにぴったり。 golang.org

サンプルはこちら https://play.golang.org/p/e2lW0-15o3P

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

var userJSON = `{
  "error": null,
  "data": {
      "nickname": "hoge"
  }
}`
var todoJSON = `{
  "error": null,
  "data": {
      "title": "fuga"
  }
}`

type Response struct {
    Error *ResponseError  `json:"error,omitempty"`
    Data  json.RawMessage `json:"data,omitempty"` // <- User か Todo になる
}

type ResponseError struct {
    Code    int    `json:"code,omitempty"`
    Message string `json:"message,omitempty"`
}

type User struct {
    Nickname string `json:"nickname,omitempty"`
}

type Todo struct {
    Title string `json:"title,omitempty"`
}

func parseResponse(data []byte) (*Response, error) {
    res := &Response{}
    if err := json.Unmarshal(data, res); err != nil {
        return nil, err
    }
    if res.Error != nil {
        return nil, fmt.Errorf("error response: %v\n", res.Error)
    }
    return res, nil
}

func main() {
    // User APIにアクセスした
    userRes, err := parseResponse([]byte(userJSON))
    if err != nil {
        log.Fatalf("parse error: %v", err)
    }
    user := &User{}
    if err := json.Unmarshal(userRes.Data, user); err != nil {
        log.Fatalf("error: %v", err)
    }
    fmt.Printf("user: %+v\n", user)

    // TODO APIにアクセスした
    todoRes, err := parseResponse([]byte(todoJSON))
    if err != nil {
        log.Fatalf("parse error: %v", err)
    }
    todo := &Todo{}
    if err := json.Unmarshal(todoRes.Data, todo); err != nil {
        log.Fatalf("error: %v", err)
    }
    fmt.Printf("todo: %+v\n", todo)

}

Private module を含む go サーバの Dockerfile

FROM golang as builder
ENV GOFLAGS=-mod=vendor
WORKDIR /work
COPY go.* ./
RUN go mod download
COPY . ./
RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -v -o app

FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY --from=builder /work/app /app
CMD ["/app"]

事前に go mod vendor しておき、 ENV GOFLAGS=-mod=vendor を付与することで private リポジトリを get しようとしない。

参考: github.com

mysql クライアントのみをインストールした状態で mysql2 gem をビルドする

この記事の続きです。

satococoa.hatenablog.com

よく考えると mysql サーバをローカルにインストールする必要はないので、以下のようにやってみました。

brew uninstall mysql
brew install mysql-client
# mysql との競合を避けるため /usr/local/opt/mysql-client 以下に入るので、以下のようにオプションを調整
bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/mysql-client/lib --with-cppflags=-I/usr/local/opt/mysql-client/include"

これで入りました。

Firebase Authentication の ID トークンを Ruby で検証する

ID トークンを確認する | Firebase にあるように、Fireabse Authentication によって発行された ID トークンを正しく検証することにより、そのユーザの user_id を確認することができます。

Firebase Admin SDK が提供されていればそれを使うことで簡単に検証できるのですが、Ruby 版は提供されていないので Rails から使いたい場合などは自分で検証処理を書くことになります。

検証すべき内容は ID トークンを確認する | Firebase に書いてあるのでそれに沿って書いていきます。

要: JWT gem

# @see https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl=ja
#
# Usage:
#    validator = FirebaseAuth::TokenValidator.new(token)
#    payload = validator.validate!
#
class FirebaseAuth::TokenValidator
  class InvalidTokenError < StandardError; end

  ALG = 'RS256'
  CERTS_URI = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'
  CERTS_CACHE_KEY = 'firebase_auth_certificates'
  PROJECT_ID = 'YOUR_PROJECT_ID'
  ISSUER_URI_BASE = 'https://securetoken.google.com/'

  def initialize(token)
    @token = token
  end

  #
  # Validates firebase authentication token
  #
  # @raise [InvalidTokenError] validation error
  # @return [Hash] valid payload
  #
  def validate!
    options = {
      algorithm: ALG,
      iss: ISSUER_URI_BASE + PROJECT_ID,
      verify_iss: true,
      aud: PROJECT_ID,
      verify_aud: true,
      verify_iat: true,
    }
    payload, _ = JWT.decode(@token, nil, true, options) do |header|
      cert = fetch_certificates[header['kid']]
      if cert.present?
        OpenSSL::X509::Certificate.new(cert).public_key
      else
        nil
      end
    end

    # JWT.decode でチェックされない項目のチェック
    raise InvalidTokenError.new('Invalid auth_time') unless Time.zone.at(payload['auth_time']).past?
    raise InvalidTokenError.new('Invalid sub') if payload['sub'].empty?

    payload
  rescue JWT::DecodeError => e
    Rails.logger.error e.message
    Rails.logger.error e.backtrace.join("\n")

    raise InvalidTokenError.new(e.message)
  end

  private

  # 証明書は毎回取得せずにキャッシュする (要: Rails.cache)
  def fetch_certificates
    cached = Rails.cache.read(CERTS_CACHE_KEY)
    return cached if cached.present?

    res = Net::HTTP.get_response(URI(CERTS_URI))
    raise 'Fetch certificates error' unless res.is_a?(Net::HTTPSuccess)

    body = JSON.parse(res.body)
    expires_at = Time.zone.parse(res.header['expires'])
    Rails.cache.write(CERTS_CACHE_KEY, body, expires_in: expires_at - Time.current)

    body
  end
end