emahiro/b.log

Drastically Repeat Yourself !!!!

Amazon Pinpoint で S3 上のファイルからセグメントを作成する

Overview

Amazon Pinpoint という AWS が提供してるマネージドCRM ツールを使ってセグメント(配信対象)を S3 上のファイルから作成する手順について記載します。

用語の整理

  • セグメント
    • Amazon Pinpoint 上の用語。配信対象を指します。
    • APNS であればデバイストークン、SMS であればメールアドレス、というようにマッピングさせることでモバイル端末に通知を送ることができます。

実装方法

ざっくり S3 にファイル(csv) をアップロードして、そのファイルを元に Pinpoint のセグメント(endpoint) を作成するという流れを考えます。

import の定義は以下に詳細に記載されています。

docs.aws.amazon.com

具体的な実装に関しては以下のようなシーケンスになります。passRole されてる AWS の内部処理については多分こうしてるんじゃないか?という予想です。

import するファイルのサイズが小さい場合、CreateImportJob API の完了後に作成された SegmentID が返ってくることがありますが、大きい場合 Pinpoint 側でも Batch Job が動いていて都度ステータスを確認して Segment の作成確認を行う必要があるので、CreateImportJob を実行したあとに GetImportJob で Segment の作成状況を確認する必要があります。

なお作成状況のステータスについては以下のドキュメントに記載されています。

docs.aws.amazon.com

sequenceDiagram
    participant app
    participant sts
    participant pp as pinpoint
    participant s3
    
app ->> s3: upload csv
s3 -->> app: url
app ->> sts: assumeRole(pinpoint-role)
sts -->> app: cred
app -->> pp: create import job with with cred & s3URL
alt AWS 内部
    pp ->> sts: passRole to import segment role from s3
    sts -->> pp: role
end
pp ->> s3: get file
s3 -->> pp: data
pp ->> pp: import job
pp -->> app: job status
loop if status != completed 
    app ->> pp: get import job status
    pp -->> app: status and segment id
end

利用する API

ファイル形式

今回はCSV を利用します。 セグメントを作成するための CSV のフォーマットは決まっていて ChannelTypeAddress が必須です。

  • ChannelType は APNS や GCM、SMS などになります。
  • Address は Push であればデバイストークン、メールとなればemail アドレスになります。

ハマったところ

Pinpoint -> S3 に触る role を設定する

先結論だけ書いておくと、Amazon Pinpoint に今回説明しているような外部のファイル(今回は S3上)を読み込ませてセグメント(endpoint) を作成する際は 専用のロールが必要になります。詳細は以下の公式ドキュメントに書いてあります。

docs.aws.amazon.com

なお、上記サンプルで書かれてるCLI のPayload だとうまく動かず、以下のようにしました(JSON のテンプレが古そう?)

# 以下の JSON を用意する。
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Effect": "Allow",
      "Principal": {
        "Service": "pinpoint.amazonaws.com"
      }
    }
  ]
}

aws iam create-role --role-name PinpointSegmentImport --assume-role-policy-document file://PinpointImportTrustPolicy.json

# response で以下の結果が返ってきます。
{
    "Role": {
        "Path": "/",
        "RoleName": "PinpointSegmentImport",
        "RoleId": "$RoleID",
        "Arn": "arn:aws:iam::$AppName:role/PinpointSegmentImport",
        "CreateDate": "2024-01-29T15:37:51+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Action": "sts:AssumeRole",
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "pinpoint.amazonaws.com"
                    }
                }
            ]
        }
    }
}

# 作った role に AmazonS3ReadOnlyAccess というポリシーをアタッチします。
aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess --role-name PinpointSegmentImport

※ これが最初わからず、以下の記事で AWS の assumeRole から調べ始めました。

dev.classmethod.jp

今回は本業において、とあるアカウントAから別のアカウントBに用意された Pinpoint のリソースを触る、という特殊な方法をベースに考えることになったのはありますが、ある role に Amazon Pinpoint をさわれるポリシーを attach し、その role に assume(変身)して Pinpoint を操作する、というのが基本的な流れになるのは変わりません。

では今回はこの専用ロール(Pinpoint) が S3 上のファイルを指定して Create Import Job を作成するのですが、この 「PinpointがS3上のファイルを指定して」 という部分、ここで結論に記載した専用の role が必要になります。
さらに Pinpoint を触れる role(roleA) が CreateImportJob を発行するときに、この Pinpoint -> S3で触る特別な role に成り代わる( iam.PassRole) 権限がないと、CreateImportJob を発行できません。ここが一番つまづきました。

このため、pinpoint の roleA には S3 を触るための roleB への iam.passRole が許可されてる必要があります。

余談

作成したセグメントを使って Push 通知を配信するには CreateCampaign を利用します。
こちらの手順については別のエントリを書く予定です。

cloud.google.com/go/bigquery.Query の結果を go test の中で mock する

Overview

BigQuery を使った実装をしているときに、API リクエストを mock したいケースがあると思いますが、そういうときに安易に DI を使わずに実装する方法を考えます

やること

やることは難しくなくて BigQuery のクライアントを生成するときにテストでは API のリクエスト先を httptest.Server に向けるだけです。

実際のクライアントを生成する実装は以下のようになります。

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("Request URL: %v\n", r.URL.String())
    var res []byte
    if _, err := w.Write(res); err != nil {...}
})

bq, err := bigquery.NewClient(ctx, "$ProjectID", option.WithEndpoint(ts.URL)
if err != nil {
    return err
} 

httptest.Server を起動し、BigQuery の接続先をOptionで起動したテストサーバーに向けます。

Query Operation の場合

https://pkg.go.dev/cloud.google.com/go/bigquery@v1.59.1#Client.Query のオペレーションを例にサンプルでテストサーバーを実装します。

Query Operation は BigQuery ライブラリの中で job のリストを取得しており、当てはまる jobId を query の API をリクエストする、という振る舞いになっています。

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("Request URL: %v\n", r.URL.String())
    var res []byte

    if r.URL.Path == "/projects/eure-metis-dev/jobs" {
    res = []byte(`{"kind":"bigquery#job","etag":"\"abcdefghijklmnopqrstuvwxyz\"","id":"your-project-id:your-job-id","selfLink":"https://www.googleapis.com/bigquery/v2/projects/your-project-id/jobs/your-job-id","jobReference":{"projectId":"your-project-id","jobId":"your-job-id","location":"US"},"configuration":{"query":{"query":"SELECT user_id FROM test-data","destinationTable":{"projectId":"your-project-id","datasetId":"your-dataset-id","tableId":"your-table-id"},"writeDisposition":"WRITE_TRUNCATE","useQueryCache":true}},"status":{"state":"DONE","errorResult":null,"errors":null},"statistics":{"creationTime":"1600000000000","startTime":"1600000001000","endTime":"1600000005000","totalBytesProcessed":"123456","query":{"totalBytesBilled":"123456","cacheHit":false,"totalBytesProcessed":"123456","totalSlotMs":"1234","numDmlAffectedRows":"0"}},"user_email":"user@example.com"}`)}

    if r.URL.Path == "/projects/your-project-id/queries/your-job-id" {
    res = []byte(`{"kind":"bigquery#getQueryResultsResponse","etag":"\"abcdefghijklmn\"","schema":{"fields":[{"name":"user_id","type":"INTEGER","mode":"NULLABLE"}]},"jobReference":{"projectId":"your-project-id","jobId":"your-job-id"},"totalRows":"10","rows":[{"f":[{"v":"1"}]}],"totalBytesProcessed":"123456","jobComplete":true,"cacheHit":false}`)}

    if _, err := w.Write(res); err != nil {...}
})

bq, err := bigquery.NewClient(ctx, "$ProjectID", option.WithEndpoint(ts.URL)
if err != nil {
    return err
} 

これで2つ目の query のレスポンスに入っている、field key の結果が返ってきます。

ハマったところ

BigQuery のライブラリでどの API を Call しているのかわからなかった。

API のリクエストとレスポンスを調べて httptest.Server の中でレスポンスを生成して w.Write すれば終わりかと思っていたのですが、BigQuery のライブラリでは job の一覧を call したあと、該当する job の query を取得する API をリクエストしてることがわかりました。

これは httptest.Server で起動したサーバーでリクエストを dump してみて、job list -> query と順番に Call してることがわかりました。これで httptest.Server の中で call された URL に応じてレスポンスを変えることで、BigQuery の API ライブラリの振る舞いをトレースすることができるようになりました。

BigQuery の API のレスポンスを生成する

API のドキュメントは以下の2つを読めば Query オペレーションの振る舞いはトレースできるようになります。

ただし、この API ドキュメントもそんなに優しいわけではなく、例えば struct みたいな感じで中身が具体的に分からないフィールドもあるのに、API のレスポンスが期待されるレスポンスでないと空のレスポンスを作っているにも関わらず、ライブラリ側の Unmarshal 処理でエラーになっていまう、ということがありました。(レスポンスそれ自体に意味はないけど、ライブラリ側のバリデーションで落とされては振る舞いが完全なものになりません。

ちょっとどうしようか悩んでましたが、ChatGPT に上記のドキュメントを渡して「ダミーのレスポンスを生成して」という雑なプロンプトを投げるだけでサンプルレスポンスを生成してくれたのでこれはこれですぐに解決しました。

GoogleAPI のリクエストを mock するときのサンプルレスポンスの生成は今後全部 ChatGPT に投げれば解決しそうでした。

現職で5年目を迎えた

3/1 で現職に努めて 4年を終え、今年から 5 年目に入る。

今までのキャリアで3年以上同じ会社に努めたことはないので、現在進行系で最長在籍記録を更新中でもある。

3年もいると色々飽きてくるタイプではあるけど、毎年少しずつ違ったことにチャレンジさせてもらえるというのはありがたい限りである。

キャリアとしても20代は幾つかの会社を転々をしつつも、30代になって腰を据えて一つの会社でキャリアを積める、というのは履歴書的にも綺麗だな〜と思ったりも最近している。

今までほど開発そのものに関われる(コードを書くことそのものにコミットする)わけではないけれど、会社として求められてることを着実にこなしていくことにも面白さを感じている。こういうのをいわゆる「目線が変わった」というのかも知れない。
実装や要素技術そのものにコミットするのは自分より得意な人がいるので、適材適所ではないけれど、うまく知恵を借りながら結果を追求していく、ということそのものに面白みを感じるようになった、と言えるのかも知れない。ずっと「技術にこだわったうえで技術は手段と割り切りたい」と思っていたけど少しそこに近づけているような間隔も覚える。まだ道半ばだけど。

まぁ一方で在籍期間も長くなってきて、老害ゾーンにも足を踏み込みつつあるので、身の振り方などは意識していかないとなとも思う。仕事は多くの場合「何をいうかではなく、誰が言うか」という側面が大きく、ここを否定しては大きな結果は得ることができない。自分の影響力なんて微々たるもんだけど、「古株の言ってること」みたいなものには上はともかく現場はどうしても左右されてしまう人もいるので、そうならないように気をつけたい。誰が言ったかでなく、何を言ったかが大事で、そういった感情で動かされない人ももちろんいるけれど、人間は感情で動いてしまう生き物でもあるので、それを理解したうえでコトに向かっていくスタンスは続けていきたい。

正直、ここまで長く在籍すること想像していなかったので、次のキャリアを自分はどうするのか?というのは目下悩んでることである。

この仕事を始めた頃にフワッと思い描いていた社会的に意義のあるプロダクトに関わるということは現職で叶ってしまっているし、そもそも社会の動きが変わっている中の本流で仕事をできることはキャリアとしてもなかなかない体験でもあり、これ自体にはやりがいと関われてることの満足感は強く感じる。

ただ、これを経験してしまった自分の次のキャリアというのは、どこに向かっていくべきなのか?最近よく考えている。尚まだこれといって答えは出てない。
色々お話を聞く機会もあるけれど、ある程度ふわっと固まってるのは「2B よりは 2C の方が肌にあってそう」ということや「組織もいいけどプロダクトの強さ、面白さを語れるところに魅力を感じる」くらいの粒度の粗さを再確認をするのみで、これも明日には考え方が変わっているかも知れない。

まだまだやれることは多いし現職を頑張るつもりでいるけど、「その次」みたいなところは少しずつ考えていきたいなと思う。

VSCode のターミナルで間違ったコマンドを叩くと beep 音が鳴る

気づいたら突如 VSCode に integrate されている terminal で間違ったコマンドを実行したりすると beep 音が鳴るようになっていたのがうるさいので off にします。

これ、a11y のサポートが入って音が鳴るらしいのですが、なんでこの設定が強制的に on になっているのかはわかりませんでした。

stackoverflow-com.translate.goog

"editor.accessibilitySupport": "off" に更新すれば OK でした。

勝手に a11y が有効化されるのも困る...。アプデか何かで影響したのかも知れないです。

actions/setup-go で Go のバージョンを指定する

actions/setup-go は Go のバージョンを指定しないとデフォルトで Go1.20 が使われているっぽい挙動に出くわしたので Go のバージョンを指定する方法のメモ。

github.com

気づいたきっかけは local で使用してる Go のバージョンと CI (GitHub Actions 上)で使用してる Go のバージョンがズレていると、setup-go が落ちる、という事象に出くわしたこと。

Go のバージョン指定方法は公式の Readme に書いてあるとおり。

https://github.com/actions/setup-go?tab=readme-ov-file#basic

go-version というディレクティブに利用したい Go のバージョンを記載すれば良い。

ただこれだと CI を 2つ以上動かしているときに Go のバージョンが上がる度毎回 CI の設定ファイルを更新する必要があり、漏れが発生しやすい。
go.mod を利用するオプションがあるのでこちらを利用しておくと local で利用してる Go のバージョンと CI で利用する Go のバージョンをあわせる事ができそう。

ドキュメントとしては以下に記載してある内容。

https://github.com/actions/setup-go?tab=readme-ov-file#getting-go-version-from-the-gomod-file

BigQuery のクライアントを Go で実装する

Overview

BigQuery のクライアントを Go で実装する手順とハマったことについてまとめます。

基本的には 公式ドキュメント に記載されている内容そのままです。

cloud.google.com

Go で BigQuery のクライアントを実装する

公式ドキュメントのとおりであれば以下

ctx := context.Background()
client, err := bigquery.NewClient(ctx, projectID)
if err != nil {
        return fmt.Errorf("bigquery.NewClient: %v", err)
}
defer client.Close()

q := client.Query(...)
job := q.Run(ctx)

itr, err := job.Read(ctx)
for {
        var row []bigquery.Value
         err := it.Next(&row)
        if err == iterator.Done {
                break
        }
        if err != nil {
                return err
        }
        fmt.Fprintln(w, row)
}

独自の HTTP Client を使いたいときはクライアントの生成のときに Option に差し込みます。

hcl := custom.NewHTTPClient()
ctx := context.Background()
client, err := bigquery.NewClient(ctx, projectID, option.WithHTTPClient(hcl))
if err != nil {
        return fmt.Errorf("bigquery.NewClient: %v", err)
}
defer client.Close()

ここまではドキュメントどおりに実装すれば普通に動く実装です。

以下実装時に気づいたことまとめます。

BigQuery のライブラリは 2種類ある

BigQuery に限りませんが、GCP をサービスを利用するときには利用する Google のライブラリの種類を見る必要があります。多くの場合(ドキュメントのサンプルコードでも)は cloud.google.com/go/... というライブラリを使うことになります。
ただ実は他にも google.golang.org/api/... というライブラリもあり、一見するとどっちを使うべきなのか迷いますが cloud.google.com/go/... のライブラリは google.golang.org/api/... をより使いやすくしたライブラリでもあるので cloud 接頭辞の方のライブラリを使えばよいです。

例えば今回の BigQuery のケースだと、 cloud.google.com/go/bigquery の Query は iterator 型を採用してるので、クエリの結果が終わるまで値を取得することができますが、 google.golang.org/api/bigquery にはありません。一回のクエリのレスポンスはライブラリの側で最大 10MB に制限されてしまい、正しくすべてのクエリ結果を返すことができないシグネチャになっています。

// MaxResults: [Optional] The maximum number of rows of data to return
// per page of results. Setting this flag to a small value such as 1000
// and then paging through results might improve reliability when the
// query result set is large. In addition to this limit, responses are
// also limited to 10 MB. By default, there is no maximum row count, and
// only the byte limit applies.

BigQuery の型はそれぞれ Go で対応する形が決まっている

Next メソッドの GoDoc を見るとわかるのですが、Next メソッドには 1レコードを Struct で表現することでクエリの結果を自動で Struct にマッピングしてくれる という振る舞いがあります。
詳細は上記のメソッドの Example 参照。

ただしこの Struct にマッピングするときは実際の BigQuery の Schema がどういう Schema であるかによって対応する型が異なります。
例えば BigQuery の Schema が Nullable な String のとき Struct に string を割り当てても上手く以下ないことがあります(文字列が入ってくるときは問題ありませんが、カラムが NULL のときにエラーになる)

このため、Nullable なカラムのスキーマの事も考え、BigQuery の対応する Type の方を Struct に割り当てておくと安全です。対応表は上記 Next メソッドの GoDoc に記載してあります。

STRING      string
BOOL        bool
INTEGER     int, int8, int16, int32, int64, uint8, uint16, uint32
FLOAT       float32, float64
BYTES       []byte
TIMESTAMP   time.Time
DATE        civil.Date
TIME        civil.Time
DATETIME    civil.DateTime
NUMERIC     *big.Rat
BIGNUMERIC  *big.Rat

ref: https://github.com/googleapis/google-cloud-go/blob/bigquery/v1.59.0/bigquery/iterator.go#L124-L137

具体的には以下のような Struct を定義することになります。

type Record struct {
    UserID int64 // Null が許容されていない Number 型のカラム
    HogeID bigquery.NullInt64 // Nullable な Number 型のカラム 
}

q := client.Query("select userID, HogeID from $dataset ...")
job := q.Run(ctx)


itr, err := job.Read(ctx)
for {
        record := Record{}
        err := it.Next(&record)
        if err == iterator.Done {
                break
        }
        if err != nil {
                return err
        }
        fmt.Fprintln(w, row)
}

カラム名とフィールド名は一致させる必要がある

最後にこれにハマりました。 Next メソッドの Godoc を読むと書いてあるのですが、

  1. struct にマッピングさせるときに Struct のフィールド名は Query で Select するカラム名と一致してる必要がある(tag 等での縛りがないので)
  2. struct のフィールド名と Query で Select するカラム名が異なっているときは parse は失敗してフィールドは空の状態になる。

書いてあるのは以下の部分

If dst is pointer to a struct, each column in the schema will be matched with an exported field of the struct that has the same name, ignoring case. Unmatched schema columns and struct fields will be ignored.

ref: https://pkg.go.dev/cloud.google.com/go/bigquery#RowIterator.Next

つまり

  • mapping 対象(dst) の struct はテーブルのスキーマと同じ名前のフィールドを保つ必要がある。
  • フィールドは公開状態にしておく(先頭は大文字)
  • マッチしないケース(ex struct のフィールドが UserID で BQ のテーブルスキーマカラム名が user_id などのとき)にフィールドの値は ignore される。

ということになります。

もしスキーマがずれるケースはクエリを書くときにエイリアスを当てる必要があります。サンプルは以下。

clinet.Query("select user_id as UserID ... from $dataset"

余談: ValueLoader を実装すると独自型を BigQuery の型を parse することができる

bigquery.Value 型は ValueLoader という interface を実装しており、これで Next メソッドの中で自動で与えられた変数(or struct) にクエリの結果をマッピングできるようになります。

ref: https://pkg.go.dev/cloud.google.com/go/bigquery#ValueLoader

これの interface を実装した type は Next メソッドの中で bigquery.Value を parse できるようになり、以下のような書換えが可能になります。

before: もともとは struct をマッピングする実装だったが、返却されるのが []int64

type row struct {
    userID int64
}

var userIDs []int64
q := client.Query("select userID from $dataset ...")
job := q.Run(ctx)
itr, err := job.Read(ctx)
for {
        r := row{}
        err := it.Next(&r)
        if err == iterator.Done {
                break
        }
        if err != nil {
                return err
        }
        userIDs = append(userIDs, r.UserID)
}

可能であれば struct にマッピングせずに int64 の配列をそのままほしいところではあるが、そのままの int64 は ValueLoader を実装していない。

after: 新しい Slice 型を独自型として定義して Next にわたす。

type (
    Int64Slice []*int64
)

func (sl *Int64Slice) Load(v []bigquery.Value, _ bigquery.Schema) error {
    for _, vv := range v {
        switch t := vv.(type) {
        case int64:
            *sl = append(*sl, &t)
        default:
            // ignore if value type is not int64.
            *sl = append(*sl, nil)
        }
    }
    return nil
}


var userIDs []int64
q := client.Query("select userID from $dataset ...")
job := q.Run(ctx)
itr, err := job.Read(ctx)
for {
        var s int64Slice
        err := it.Next(&s)
        if err == iterator.Done {
                break
        }
        if err != nil {
                return err
        }
        userIDs = append(userIDs, s[0])
}

まぁ書いててどちらも大差ないですが、独自型を使えるということの説明をしたかった、ということです...笑

余談2: Generics を使って汎用的な wrapper メソッドを作る

Generics を使った Query メソッドの wrapper が作れるなと思ったので簡単にまとめてみました。

type queryOption func(b *bigquery.Query) *bigquery.Query

func QueryAll[T any](ctx context.Context, queryStr string, opts ...queryOption) ([]T, error) {
    q := bq.Query(queryStr)
    for _, opt := range opts {
        q = opt(q)
    }
    job, err := q.Run(ctx)
    if err != nil {
        return nil, err
    }
    itr, err := job.Read(ctx)
    if err != nil {
        return nil, err
    }

    var rows []T
    for {
        var row T
        err := itr.Next(&row)
        if err == iterator.Done {
            break
        }
        if err != nil {
            return nil, err
        }
        rows = append(rows, row)
    }
    return rows, nil
}

使うときは以下のように使います。

type row struct {
    userID bigquery.NullString
}

q := "select userID from $dataset ..."
rows, err := bigquery.QueryAll[row](ctx, q)

独自型を定義したときは以下です。

q := "select userID from $dataset ...."
rows, err := bigquery.QueryAll[bigquery.Int64Slice](ctx, q)

これもわざわざ Generics で抽象化する必要もないかなとも居ますが、冗長な実装を何度も書くことないのと Generics を使えそうというフィードバックをもらったので試してみました。
特に T には何型が入ってもよく、呼び出し元で record を表現した struct とフィールド名をエイリアスにしたカラム名を使う Select 文を考えるのみでよいでの若干コードはスッキリするかなと思います。

git コマンドが重たくなったら git gc コマンドを使ってみる

Overview

git gc が便利という話です。

git push が重たい

ここ1年くらい git コマンドがものすごく重たい事象に直面しており、特に git push したときに 10秒くらい待たないと push が完了しないという事象が発生してました。
github も頻繁にアプデ繰り返してるので当初は github が重たくなったのかな?と完全に人のせいにしてましたが、 周りは層でもないらしくどうやら「おま環事案」という感じがしました。

git gc

ちょうど git が重たいなら git gc コマンド試してみれば〜ということを教えてもらったので試したところ、手元のゴミファイルやゴミデータを丸っと削除してくれて git push が快速になりました。

git-scm.com

git にはまだまだ知らない機能がありますね。

git が重たいなと感じたら git gc コマンド試してみてください。

子供と過ごす初めての冬休み

まえがき

人生で初めて子供と年末年始を共に過ごしたので思ったことを備忘録として書いておく。

子供過ごす初めての年末年始だった

自分は今までに30回以上(物心ついてから考えると20数回か)年末年始というイベントを過ごしてることになるけど、我が子にとっては当たり前だけど初めての年末年始なわけで、家族として過ごす年末年始も初めてなわけである。

不思議と子供がいるだけでなんとなく初めて年末年始を過ごす、みたいな錯覚に陥った。家族集まって過ごせるのも初めてであるし、そもそも奥さんはカレンダー通りのお休みの職業ではなく、一般的なサラリーマン、公務員が休日のときに仕事があるタイプの職種なので、一昨年結婚してるのに奥さんも含めて家族揃って年末年始を過ごすというのが人生で初めてだった事に気づいた。
そういえば、大学で上京して以降、誰かと年末年始の全てを自分以外の誰かと過ごすというのはこれまた自分でも初めてだった。(帰省はしていたが、実家にいても飽きるし、地元で時間を過ごす相手は歳を重ねるに連れて少なくなって行って今ではほぼいないので実家への滞在期間は長くて2泊程度だったりするのが結婚するまでのここ数年だったりしたので)

話は戻って子供と過ごす年末年始というのは新鮮ではあったものの、基本的な過ごし方はいい意味でいつも通りと変わらなかったので、実を言うと年末年始感はあまりなかった。

ただクリスマスだったり、大晦日だったりといったイベント類はやっぱり家族揃っていることでちょっと豪華に過ごしたりとかそういうことに気が回るようになった。自分ひとりで過ごしていたときはだいぶ異なったことを考えるものだなと結構驚いた。
まぁでも冷静になって考えてみると、今はまだ0歳の我が子も自分たち両親と一緒にこういったイベントを過ごしてくれる機会も数えてみるとそう多くないのかもしれない(*)と思うと、これから先の数々の節目やイベントはそれなりに記憶に残るものにしたいな、ということも考えたりもした。

※ 大学に行くまでは18回。もし付き合ってる人とか出来たらもっと早く両親と過ごしてくれる機会がなくなるのは早いかもしれない。

その他

家族と過ごすお正月だったので他にも気づいたことを書いてみる。

年末年始のテレビ番組は結構面白い

ある年齢を境に正月特番と行ったものをほとんど見なくなってしまっていたけど、今年は結構見ていた。
現職のプロダクトのCMがあるかも、ということもあってTVを意識的につけっぱにしていたことも関係してるが、子供がいると何かしら BGM をかけていたくなるが、動画サービス流しっぱでも同じコンテンツばかりが流れてくることもあって飽きるので、案外 TV というのは BGM としても優秀だなと思ったりもした。
子供が寝静まった後にも見ていたけど、自分たちが物心ついていたときにみていた番組の復刻版とか特番とかもやっていて案外面白いものだなと感じた。思えば子供の頃はお小学特番をめっちゃ見てるタイプの人間だったので、そういえば正月ってこんな過ごし方だったなと懐かしさも感じた。

特に紅白とか見ながら我が子は「どんなアーティストを好きになるんだろうね?」とか「何が好きになるのか?」みたいな会話に花が咲いて、今までの自分では考えもしないことを考えるものだなと自分が一番驚いた。

余談だが、正月早々気が重たくなるニュースが重なったけどテレビ局は比較的いつも通りのテンションで番組を放映してくれたので気の滅入るようなコンテンツを目にする機会を減らせてよかったかもしれない。その分 X (旧 Twitter) はだいぶ地獄な様相だったが。
こういうところも東日本のときの反省が活かされている?のかもしれないなと感じた(災害ニュースばかりを流さない的な)

年末年始は断捨離が捗る

年末年始は大掃除の機会といったけれど確かに家族が揃っていると今まで手を付けてこなかったゴミを捨てたりといった断捨離は格段にはかどりました。やっぱり普段から「これ邪魔だよね?」とか「これもう必要なくない?」といったことを会話していたので、今回は夫婦揃う初めての年末年始だったので一気に片付けたり、捨てた。

ただ、捨てたぶんだけ子育て用品が増えているので捨てたもののプラスマイナスはゼロかなと思う。

年賀状を書く

ある年を堺に年賀状なんて書かなくなっていたけど、子供の写真を載せた年賀状を親戚に送った。結構喜んでもらえた(と母親から連絡があった)

親戚の成長というのは老後の楽しみの一つらしい。まぁ結婚して以降実家に帰る機会もめっきり減ってしまったのでそういう意味では年に一回子供の成長を知らせるというのは親の義務なのかもしれないなと思った。
これも今までの自分では考えもしなかったことだなと思う。

2023 年の振り返り

まえがき

年の瀬なので今年も1年の振り返りをします。

子供が生まれた & 育休を取った

今年一番の大きな出来事は子供が生まれ、父親になったことでした。

私生活のすべてが一変し、また年の半ばに3ヶ月ほど育休取得したことで、今後のキャリアをはじめ自分自身のことや働き方のことを考え直すいい機会にもなりました。

ema-hiro.hatenablog.com

ema-hiro.hatenablog.com

ema-hiro.hatenablog.com

正直育児は楽しいことばかりではなく、子供が生まれたことでできなくなったこともありますが、一方で得たものも多いですし、また個人としては仕事でどこにフォーカスしていくのか、ということが方向性をある程度クリアにできた1年になりました。

筋トレの頻度が増えた

週1程度で4年ほど続けている筋トレですが、これが週2になりました。

子供が生まれて自分1人の時間というものが著しくなくなったのもあり、自分としては結構ストレスが溜まっていたので、そのストレス解消のために週末の数時間1人の時間を作るためにジムに行くようになりました。

家族も増えて今まで以上に体が資本であるのと同時に、1人でできる自分なりのストレス発散方法を持っているというのは大事だなと改めて思いました。

自分の作ったプロダクトが TVCM で放映された

現職で開発に関わっている Pairs のテレビ CM が開始されました。

この業界に入って以降一度は経験したいと思っていた「自分が仕事で関わってるプロダクトがテレビ で CM される」という機会を得ることが出来ました。

この年末年始にも新しい CM が放映されており、こちらはちょうど自分が育休明けに開発に関わった機能がフューチャーされています。

自分の書いたコードで動いてるプロダクトが特集されてそれが日本全国で見られてるかと思うと、正直なかなかエモい気持ちになります。エモい。

数年ぶりに個人開発した

自分は週末にコードを書いたり、個人開発をしたりするタイプのエンジニアではないのですが、育休期間中に自分のポートフォリオのサイトを Nuxt -> Next に書き換えたり、気になっていた Connect を使ってツール作ったりといったことをしてました。

過酷な育児に耐えかねてのほとんど息抜きというか現実逃避というか、そういう感じで久しぶりに仕事以外でコードを書くことを楽しんでいたのですが、仕事してる時よりコード書いてて自分でも驚きました。

コードを書いたり新しい知識や技術を身につけるんだというパワーに少し陰りが見え始めていたなと自分でも自覚があったのですが、時間があれば「息抜きにコード書く」人種なんだなと知ることが出来て自分でも少しホッとしました。なお、仕事に復帰してからは全然週末そういう時間は取れてません...涙

ブログ継続した

気づいたら今年は30記事くらい書いてました。自分でも結構なペースで書いてたんだなと思います。
技術ネタだけじゃなく、育児のことも残すようになったので単純にネタが増えたことも影響してそうです。
毎年思いますが、エンジニアキャリアの半分以上をブログ書いて過ごしてるわけで、完全にこのブログを書くことが趣味というかこれもまた一つの息抜きになっています。

来年もちゃんと継続します。

来年の話

2023 年は1年のちょうど間に育休が挟まっていたので、1年の中で前半と後半での180度生活が変わりました。
来年は今年の後半をベースしないといけないのでそんなに大きな目標はないです。毎年そうなんですが、仕事以外でこれと言ってやりたい目標がないので、育児しながらのらりくらりと過ごしてそうな気がします。
あ、スプラトゥーンはもっと上達したい(XP2000の壁は今年超えられませんでした) のと、サッカーを今年は1試合しか現地観戦できなかったので来年はもう少し現地観戦できる機会を増やしたいなと思っています。
あと、今年息抜きにコード書いたり競プロ(Leetcode) 始めたりしてやっぱりコード書くの楽しいなと思ったので、週末にコード書ける時間は少しでも取れると嬉しいなと思ったりしています。

2023年の買って良かったもの

今年買ったものは育児関連グッズが多すぎるので、育児関連グッズ以外で買ってよかったものを上げておきます。

HHKB Studio

HHKB の新作を買いました。実は Keychron Q2 を買ったり、MISTEL の分割キーボードを買ったりと結構キーボードにお金を使った年だなと思ったんですが、結局 HHKB の配列から逃れられずに HHKB に帰って来たところでこちらの新作が発売されたので買いました。
色々言われてますが、打鍵感が今までの HHKB の中では一番好みで、かつカスタマイズ可能というところが良きです。
ポインティングスティックやジェスチャーパッドは正直感度が微妙なのであってもなくてもどっちでもいいかなと思っていますが、キースイッチだけでも買ってもいいかなと思わされるガジェットでした。正直追加された機能群なしでカスタマイズ可能かつ Studio で採用されたキースイッチを採用した Professional シリーズももう少し安価で出してくれてもいいのでは?と思ったりもしています(そしたら自分はきっとそちらを買うんだと思います。)

https://amzn.to/3RD8Zac

happyhackingkb.com

大きな本棚

最近ですが技術書が縦置きで2列入る奥行きの大きな本棚を買いました。
僕は本を買うなら物理本派(漫画は電子書籍)なので、とにかく本を置く場所に困っていて、部屋の中に積んだりうまく収納を作って置いたりを繰り返してたのですが、今回買った本棚に全て収納仕切れて、部屋も少し広くなったのでとても満足でした。

大体奥行きが50cm くらいある本棚だと技術書を贅沢に置くことが出来て良い感じです。

やはりものを置くものはデカいに限りますね。

www.nitori-net.jp

オーブンレンジ

自宅の電子レンジをオーブンレンジに買い替えました。
ヘルシオとかも考えましたけど、置く場所の寸法の問題で断念して普通のオーブンレンジを買いました。僕の使う範囲では電子レンジの時代と何も変わってませんが奥さんは料理のレパートリーが増えたりあと離乳食作るのに役立ってるみたいです。

www.toshiba-lifestyle.com

SIXPAD パワーガン

夫婦共々育児で深刻な肩こりと腰痛に悩まされるようになっていたので対処療法として購入しました。
僕は筋トレ後も使って翌日のダメージ軽減等にも使っています。
結構充電が持つのでそんなに頻繁に充電しなくても大丈夫なのも良いポイントです。

https://amzn.to/3H15Vjk

www.mtgec.jp

iPhone15 Pro

今年もiPhoneを買い替えました。
毎年似たりよったりのマイナーアプデばっかりで今年も買い替えるか悩んでいたのですが、子供を撮影するコンパクトなカメラだと思ってるのでカメラ買う感覚で買いました。
今年は軽くなったり、Type-C になったりとユーザーが欲しいと思っていたアップデートがあり、買い替える価値はありました。

www.apple.com

Nishikawa Air 4DX

枕を買い替えました。
数年来眠りに投資しよう、まずは枕を買い換えようと思っていて、なかなか出来ていなかったのですが、今年満を持して買い替えてみて、眠りに投資しなかったここ数年を後悔しました。

www.airsleep.jp

go-cmp/cmpopts を使って map の特定の key のみをフィルタする

Overview

go-cmp を使って実際取得した値と期待値を比較するときに go-cmp/cmpopts を使って、二値の比較のオプションを設定することが出来ますが、この二値が map のときに map の中の特定の key に紐づく value のみを比較したいというユースケースがあって、既存の options だけでは満たせなかったのでやり方を考えました。

cmpopts.IgnoreMapEntries を利用する

IgnoreMapEntries を利用します。

IgnoreMapEntries は map 同士を比較、もしくは map と比較するときに特定の key-value のセットを ignore するという options になります。

discardFunc は interface を取っていますが、ここは func(k string, v any) bool という func 型を取り、指定した key を指定して返り値が true となる key-value のセットを ignore します。

繰り返しですが、このメソッドは func(k string, v any) bool の返り値が true のときの key-value のセットを ignore する、というオプションになるので、逆に false の場合は Ignore しない(=残る)というものになります。そのため、以下のようなメソッドを用意して特定の key-value のセットのみを残す(= フィルタする)ということが可能です。

// any のところは比較したい map の型定義に合わせる。例えば map[string]int 同士を比較したい場合は `want map[string]int` をとります。
func FilterWantMapEntires(want map[string]any) cmp.Option {
    return cmpopts.IgnoreMapEntries(func(k string, _ any) bool {
        _, ok := want[k]
        return !ok
    })
}

使い方としては以下のような感じで option の中に組み込むことを想定しています。

func main() {
    got := map[string]int{"1": 1, "2": 2, "3": 3}
    want := map[string]int{"1": 1, "2": 2} // 1 と 2 のみ比較したい。

    opts := []cmp.Option{
        FilterWantMapEntries(want),
    }

    if diff := cmp.Diff(got, want, opts...); diff != "" {
        fmt.Println(diff)
    }
}

ref: https://go.dev/play/p/4eU9w2nTgBp

Ref

あとから調べたのですが、go-cmp の cmpopts の使い方で実際にユースケースでよく使うものをベースに以下がよくまとまっていました。

zenn.dev

育休から復帰してみて変わったこと

はじめに

これは「子育てエンジニアAdvent Calendar 2023」 7日目の記事です。

adventar.org

About Me

  • 都内勤務。
  • 住まいは郊外。会社までは大体ドアドアで1.5時間くらいの通勤時間。
  • 今年の4月に第一子の娘が生まれた。
  • 核家族構成で夫婦共働き(今は奥さんが育休中)
  • 5月~7月末まで3ヶ月間育児休暇をもらった。

生まれた直後や、育休取った話は自分のブログで別でエントリを残していたりします。

ema-hiro.hatenablog.com

ema-hiro.hatenablog.com

今回は育休も明けて復職した今現在、以前と比較して変わったことや、復職してしばらく仕事をしてみたタイミングでどんなことを感じてるのか、ということを言語化してみました。

週末の過ごし方が変わった

元々週末に外に出るタイプでもなかったですが、旅行や家族のお出かけを除いて週末は基本的には家で、しかも子供のいるリビングで過ごす時間が増えました。
奥さんも土日に仕事があるタイプというのもあり、以前は自室で過ごすことが多かったのですが、ちょうど寝返りを打ち始めたくらいから放っておくとどこに行くかわからないので、リビングで過ごす時間は増えました。(ほぼ監視要員ですが)
ただ、ずっとリビングから身動き取れないのもしんどいので、1日にうちどこかは自室に籠らせてもらってます。

出かける先が変わった

週末のお出かけ先が大型ショッピングモールか奥さんの実家、がメインになりました。どちらも車で行ける範囲でかつ、子育て関連(特に乳幼児)のアメニティが充実していることが条件で、これを満たせないお出かけ先はいけなくなりました。
※ 実は生まれてからまだ一度も公共交通機関に乗ってません。

この辺もブログに書いてみました。

ema-hiro.hatenablog.com

もっと子供を連れて都内のイベントとか行くのかな〜とか思っていたんですが、まぁ現実問題かなり厳しいな、、というのが感想です。
都内に住んでいればまた話は変わってくるのかもしれませんが、郊外に暮らしてて都内に子連れで、しかも公共交通機関使う、というのはだいぶ億劫になってしました。

通勤が嫌いじゃなくなる

現職はフルリモートが許可されてはおらず、週に何日か出社する必要がある会社であり、ちょうど育休明けのタイミングでこの出社(RTO)がルール化されたのもあって最初は正直めんどいな〜と思っていたんですが、そうは言ってもサラリーマンなので定常的に出社し続けけてみて「通勤時間、実は一日の中で唯一1人になれる時間」ということに気づきました。
このことに気づいてから、毎日出社は流石にしんどいですが、定期的に出社する理由があるというのは、子供が出来て以降なかなか取ることのできなかった「一人の時間」というものを強制的に確保することに繋がってて、そのおかげか育児中少しずつ削られてたメンタル面が回復しました。
色々言われる RTO ですが、こういうのも考え方1つで前向きになれるものだなと思いました。
ちなみに通勤中はひたすらスプラ3の動画見るか、シーズンごとのアニメを消化しています。
(通勤時間は勉強時間?…え?なんのことですか?)

一人の時間というのは自分たち夫婦でも度々話に出るないようなのですが、子供は可愛いものの、やはり1日中拘束されざる得ないことで多少なりしんどい時間があるのは事実であり、定期的に子供と離れる時間は必要で、自分の場合はそれがたまたま「出社」だった、という感じです。
なお、出社日は丸っと奥さんにワンオペ依頼をしてる身でもあるので、そこは本当に感謝してもしきれません。

スキルアップのスタンスが変わる

これもよく言われてる話ですが、いわゆる「子供ができるとスキルアップの時間が取れなくなる」というこれに自分もぶち当たりました。
自分自身はもともと週末にコードを書いてたりするタイプではありませんでしたが、気になるトピックをつまみ食いしたりとか、本読んだりということに当てていた時間はほぼなくなりました(気分転換の読書はしますが、何かを学んでやる!みたいなモチベではなくなりました)

これに対して正直言うと葛藤はあったのですが、今はたまたまそういう時間が取れないタイミングというだけで、手のかからなくなるタイミングはそのうちくるので、子供の成長に合わせて調整していけばいいかな?くらいの考えでいます。
ただ、スキルアップに限らずなんでもそうですが、好きなことに自分の時間をフルで使える期間というのは思いの外短く、そしてありがたい時間だったんだな、と今思い返すと感じます。

一日のやることを極限まで絞る

上記のスキルアップのスタンスを変える話とも似てるのですが、育児周りのタスクをこなしていると一日の過ぎ去る速度が本当に一瞬で自分自身のやりたいことをする時間は子供が起きてる時間は殆どできないので、一日の中で「これやりたい」ということを普段ものすごく絞っています。
例えば子供向けの椅子を買った週末なんかは、それを「組み立てる」という目標だけ立ててそれさえ週末できれば「偉い!よくやった」と思うようにしてます。一番疲れてるときなんかは「メルカリの発送する」というタスクだけを週末にすることで自分のことを褒めてました。
できる限りハードルを下げて過ごしていないと、突発的なイベント(子供の対象不良など)に対応しきれないし、メンタルの余裕もなくなってしまうので、とにかく自分自身のやること(やりたいこと)を極限まで絞りつつ、かつサボれるところはうまくサボってと、元々決めてたこと以上のことができればすべて儲けものと思える状態を作れると、それだけで前向きになれました。

その他

政治に興味を持つようになった

政治家になりたいとかそういう意味じゃないです。
もともと経済状況とかを軽く眺めるくらいの興味しか持ってなかったですし、どちらかといえば子育て支援に予算回した方がいいんじゃないの〜?くらいの薄い関心しか持っていなかったのですが、子供を持った瞬間に見るポイントが明確に変わりました。
やはり子育てに関わる論点を挙げてる候補者に目が行きますし、政治そのものも子育てのトピックに対してどういうスタンスを取っているのか?ということを真っ先に見るようになり、当事者意識は上がったなと感じます。

良い父親像みたいな理想を捨てる

インターネットに触れてると「父親でもこれくらいできないと」といった発言を偶に目にしますし、自分も当初はそれくらい頑張らなければいけないのかな、、と気張ってもいたのですが、まぁやっていくうちにできることとできないことがある事に気づき、無理しても理想とのギャップでしんどくなるだけなので、そういうものを一切気にしないことにしました。
育児に関しては「最低限の冗長化」を目標としていて、奥さんだけしかできないことがないようにするということは担保しつつ、それ以上のことは自分にも期待しないようにしてますし、家庭でも期待しないで、ということを伝えてます。

おわりに

まとまりのない文章でしたが、書き出してみると「いかにメンタルを安定させるか」ということをフォーカスして自分は復職以降の生活を続けてきたことがわかりました。
自分の子供はもちろん可愛いです。ただ、育児が始まって以降公私問わず気の休まる時間がかなり減って、しんどい面はあるので夫婦でうまくサボれるところはサボりつつ、自分は自分で自分自身のご機嫌を取りつつ、心の余裕を作りながら子供と向き合っていこうと思います。

AWS Go SDK でのエラーハンドリングについて

AWS の Go SDK はその本体のコードを読もうとすると実装がものすごく抽象化されていて、慣れていないと実際のコードを追いかけることも結構難しい。

Go の実装でエラーの中身(型)を見て処理を変えたいケースやそもそもどういうエラーを返すのかを知りたいときは、以下の Go SDK のドキュメントや、コマンドのドキュメントを見に行くと API がどういう振る舞いをするのかを調べることができる。

aws.github.io

ex. これは DeleteObjects のコマンドリファレンスだが、API の仕様に準拠してるので AWSAPI リファレンスを読むよりAPIの振る舞いをつかみやすい。

docs.aws.amazon.com

配列形式のクエリパラメータを受け取る実装について考えてみる

Overview

ID=1, ID=2,... のようにクエリパラメータで指定したパラメータを key にしてリソースを取得する GET の API、というのは一般的かと思いますが、このクエリパラメータで同じ key を複数取って複数のリソースを取得する実装パターンについて API を実装するときのベターなプラクティスとそれに伴う Go の実装について考えてみます。

Motivation

API を書いていたときに RDB 上の PrimaryKey をクエリパラメータに指定してあるリソースを複数取得する Endpoint を実装するときに、クエリパラメータの取り方で実装のフィードバックを受けたのがきっかけです。

実装パターンについて

以下のパターンをベースに考えます。

  1. GET /user?id=1,2,3 という 取得したいパラメータのリストをカンマ区切りの文字列 で表現する方法。
  2. GET /user?id=1&id=2&id=3 という 取得したいパラメータを1つずつ & で繋げて配列形式を表現する 方法。
  3. GET /user?id[]=1&id[]=2&... という 2 と似ているが取得したいパラメータが「配列である」ことを表現するために [] のブラケットをつける 方法。

GET /user?id=1,2,3 のパターン

ほぼ癖ですが、僕は自分が Overview に記載されているような GET の Endpoint を実装するときはほとんどこのパターンを採用してました。強い理由はないですが、URL からもあるリソースに対してどういうフィルタが適用されているのかが直感的でわかりやすいと考えていたからです。

Go で実装するときも以下のようにクエリパラメータを取り出して、split 分割で配列に変換して後続の DB レイヤの処理に回す、という実装をしていました。

var ids []int
query := r.URL.Query()
if q := query.Get("ids"); q != "" {
    split := strings.Split(q, ",")
    if len(split) != 0 {
        for _ , idstr := range split {
            id, _ := strconv.Atoi(idstr)
            ids = appned(ids, id)
        }
    }
}

ただ、このようなクエリパラメータのパターンは iOS で一般的に使用されている Alamofire のような HTTP クライアントのライブラリではそのままで対応してないです ※ ※ 配列を表現したいときは ids[] というようなブラケットが付いてしまいます。

dev.classmethod.jp

Go の実装面でもカンマ区切りの部分を Split して文字列を分割する必要があり、ちょいと工夫が必要です。

GET /user?id=1&id=2&id=3&id=... のパターン

URL 上は若干読みづらい(直感的ではない)ですが、配列にしたい key を複数クエリパラメータに指定することで「配列である」ことを表現します。

この場合、Go 側の実装では以下のようになります。

var ids []int
query := r.URL.Query()
if idsStr = query["id"]; len(idsStr) > 0 {
    for _ , idStr := range idsStr {
        id, _ := strconv.Atoi(idStr)
        ids = append(ids, id)
    }
}

これだと Go 側では文字列の分割処理をする必要がないです。

パターンとしても直感的ではないものの、 Endpoint のクエリパラメータで配列を表現するには一般的らしいです。

stackoverflow.com

GET /user?id[]=1&id[]=2&id[]=3&... のパターン

2 のパターンと同じですが、先に紹介した iOS のライブラリのようにクエリパラメータの中で、クエリパラメータが配列であることを表現します。

Go 側の実装は 2 のときとあまり変わりません。

var ids []int
query := r.URL.Query()
if idsStr = query["id[]"]; len(idsStr) > 0 {
    for _ , idStr := range idsStr {
        id, _ := strconv.Atoi(idStr)
        ids = append(ids, id)
    }
}

RFC 3986 を見る

もともと カンマ区切りの文字列 を採用してるときに iOS との連携時に、クエリパラメータがうまくデコードできないということに気づいて今回のことを考えました。

iOS としては元々 3 のパターンを前提としてクエリパラメータをデコードしていたのですが、どうやら iOS 側で利用してるメジャーなライブラリ(現職の iOS チームが使っていたもの)が RFC 3986 に対応したことで 3 のパターンが使えなくなり、結果としては 2 のパターンで実装する事になりました。

なお、この RFC3986 に対応したことでデコードエラーになるようになったのは、 []予約語扱いになったので、ライブラリ側でデコードエラーとするようになったっぽいです。
ref: https://tex2e.github.io/rfc-translater/html/rfc3986.html#2-2--Reserved-Characters

まとめ

クエリパラメータの中での配列の表現について自分の実装の癖を見直すいいきっかけになりました。
動作としては同じでもモバイル側の都合等々、考慮しないといけないポイントがありますね。

VSCode で CUE を書く

Overview

VSCode で CUE を書くときの設定です。

手順

CUE の Install

cuelang.org

上記の公式ドキュメントに書かれてる通りに進めればよいです。
僕個人は go コマンドでインストールするのが好みです。

CUE を入れたら cueimports を使うために PATH を通さないといけません。 Go で入れた場合は GoPath がそのまま cueimports の PATH として機能してくれます。

VSCode 側の設定

CUE の拡張をインストールします。

marketplace.visualstudio.com

以下の設定を設定に追記します。

"cue.toolsPath": "$HOME/go/bin"
"cue.lintOnSave": "off", // or "off"
"cue.lintFlags": [], // e.g. ["-c"]
"cue.moduleRoot": "${workspaceFolder}", // used to resolve modules

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

Overview

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

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

読書メモ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

肩書の重要性

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

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

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

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

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

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

結局のところ

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

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