Diary of a Perpetual Student

Perpetual Student: A person who remains at university far beyond the normal period

安価に手間なく readonly な GraphQL API を作る

私は個人開発で、読み込みのみが求められる(ユーザーの行動起因でのデータ更新が起こらない) GraphQL API を作っています。情報サイトや個人ブログのバックエンドとしての利用が挙げられます。

今回は趣味の個人開発向けにインフラ費用を抑え、そして手間なく GraphQL API を構成するための流れをご紹介します。

構成のポイント

安価に手間なく readonly な GraphQL API を作るためのポイントは以下の3点です:

Go 言語の ORM である ent の GraphQL インテグレーション機能を用いる

ent という Go 言語の ORM があります。ent には GraphQL インテグレーションという機能があり、これを使うと、ent 上のスキーマ定義を用いて簡単に RDB をデータストアとした GraphQL API サーバを作れます。GraphQL のスキーマを手作りする必要もないし、リゾルバ―を実装するために必要なメソッドもコード生成されます。

entgo.io

実は、この話は過去にもブログに書いたのですが、今回は具体的なコードや手順を紹介できればと思います。

DB には SQLite を用いて、db ファイルをランタイムに同梱する

今回は前述の通り、ユーザーの行動起因でのデータ更新が起こらない API を想定しています。API サーバとは別にデータストアの RDB を用意しなければなりません。

SQLite を用いることで、 API サーバの裏に RDB サーバを用意する必要がなくなり、インフラの構成がシンプルになります。

インフラには Amazon API Gateway (HTTP API) と AWS Lambda を用いる

作った API サーバを公開・運用するためのインフラには、 Amazon API GatewayAWS Lambda を選択します。

Amazon API Gateway の HTTP API は最初の12か月に無料枠があるほか、無料枠がなくても1.29USD/100万リクエストと非常に安価です*1REST API のほうが自由度は高いですが料金は高くなります。今回の要件では HTTP API で十分です。

Lambda は月あたり100万リクエスト分の永年無料枠があるほか、アーキテクチャに arm64 を選択したりメモリサイズを適切に設定したりすることで料金を抑えることができます。コールドスタートに掛かる時間が気になりますが、実用に足るパフォーマンスが出せるのでしょうか。記事の最後にリクエストに掛かる時間の計測結果を掲載しておくので、お楽しみに。

サンプルコード

サンプルコードを以下のリポジトリに掲載しています。

github.com

サンプルコードでは、楽曲(Song)とアーティスト(Artist)の情報を提供する GraphQL API を構成しています。

例えば、以下のようなクエリを投げると、

{
  songs(first: 3) {
    edges {
      node {
        title
        releasedYear
        artist {
          name
        }
      }
    }
    pageInfo {
      startCursor
      endCursor
    }
    totalCount
  }
}

このようなレスポンスが帰ってきます*2

{
  "data": {
    "songs": {
      "edges": [
        {
          "node": {
            "title": "北ウイング",
            "releasedYear": 1984,
            "artist": {
              "name": "中森明菜"
            }
          }
        },
        {
          "node": {
            "title": "悲しみがとまらない",
            "releasedYear": 1983,
            "artist": {
              "name": "杏里"
            }
          }
        },
        {
          "node": {
            "title": "シンデレラ・ハネムーン",
            "releasedYear": 1978,
            "artist": {
              "name": "岩崎宏美"
            }
          }
        }
      ],
      "pageInfo": {
        "startCursor": "gaFp0wAAAAAAAAAB",
        "endCursor": "gaFp0wAAAAAAAAAD"
      },
      "totalCount": 19
    }
  }
}

サンプルコードの解説

手間なく、と書いたものの、すべてをこの記事で解説するのは難しいです。サンプルコードの commit を丁寧に分けておいたので、適宜差分を確認して読み進めていくと良いでしょう。ent や gqlgen のドキュメントも合わせて参照してください。

Step 1: ent 上でスキーマを定義する

まずは ent でスキーマを定義するためのファイルを作りましょう。

go run -mod=mod entgo.io/ent/cmd/ent new Song

のように go run コマンドを実行すると、ent のスキーマ定義ファイルが生成されます。(commit

できたファイルに具体的なフィールド名などを書き込んでいきます。(commit

ent のスキーマ定義のやり方については他の記事をあたってほしいのですが、GraphQL インテグレーションを使う際のポイントとして、Annotation を付与する必要があります。

func (Artist) Annotations() []schema.Annotation {
    return []schema.Annotation{
        entgql.QueryField(),
        entgql.RelayConnection(),
    }
}

entgql.QueryField() は、検索条件などが指定できる GraphQL スキーマを生成するために必要です。entgql.RelayConnection() は必須ではないですが、Relay Cursor Connections と呼ばれるページングに対応した GraphQL スキーマを生成するために指定しています。

Step 2: ent の Client と GraphQL の schema やサーバの雛形をコード生成する

ent のスキーマ定義から、ent の Client と GraphQL スキーマを生成します。また、生成された GraphQL スキーマgqlgen に渡すことで GraphQL リゾルバ―を生成します。

go generate コマンドでこれらのコード生成を行うための準備をします。

go run -mod=mod entgo.io/ent/cmd/ent したときに ent/ ディレクトリに generate.go が作成されているのですが、こちらは消してしまいます。ent の GraphQL インテグレーションを行うために設定を指定する必要があるのと、gqlgen によるコード生成が ent に依存しているのでコード生成の順番を制御したいからです。

代わりに、ent/entc.go ファイルを作り、ent によるコード生成の設定と、実際にコード生成を行うためのコードを書いていきます。また、ent が書き出した GraphQL スキーマからさらにコード生成するための gqlgen の設定ファイルも用意します。

コード生成の準備を行った commit はこちらです。

ここまでできたら go generate ./... でコード生成していきましょう。途中で失敗した場合は go mod tidy を実行してから再度 go generate ./... すると成功するはずです。(commit

Step 3: ent Client を用いて、SQLite の db ファイルを生成するプログラムを作る

ent の ORM Client がコード生成できたので、DB スキーマやマスタデータを SQLite の db ファイルに書き出すプログラムを作っていきます。

準備として、ent で SQLite を利用する際に cgo に依存しないようにしていきましょう。(commit)詳しくは以下の記事を読んでください。

zenn.dev

準備ができたら、ent Client を用いて DB スキーマを適用し初期データを挿入するプログラムを書きます(commit)。

//go:embed "csv/artists.csv"
var artistsBytes []byte

type Artist struct {
    ID   int    `csv:"id"`
    Name string `csv:"name"`
}

func initArtists(ctx context.Context, tx *ent.Tx) error {
    artists := []*Artist{}
    if err := csvutil.Unmarshal(artistsBytes, &artists); err != nil {
        return err
    }

    builders := make([](*ent.ArtistCreate), 0, len(artists))
    for _, a := range artists {
        builder := tx.Artist.Create().
            SetID(a.ID).
            SetName(a.Name)
        builders = append(builders, builder)
    }
    return tx.Artist.CreateBulk(builders...).Exec(ctx)
}

初期データ挿入では、csv ファイルを構造体のスライスに変換し、ent の CreateBulk メソッドを使うことで bulk insert を実現しています。CSV ファイルの読み込みには Go の標準パッケージである embed を利用しています。プログラムに付随させたいファイルを同じバイナリにまとめて同梱できるのが便利です。

ここまでできたら、 go run cmd/gen-db-file を実行することで、スキーマと初期データが反映された SQLite のファイルが書き出されます。

Step 4: GraphQL Resolver を ent Client で実装する

次は GraphQL の Resolver を実装していきます。Step 2 でのコード生成で Resolver はできたのですが、実装が空っぽになっています。

func (r *queryResolver) Node(ctx context.Context, id int) (ent.Noder, error) {
    panic(fmt.Errorf("not implemented"))
}

func (r *queryResolver) Nodes(ctx context.Context, ids []int) ([]ent.Noder, error) {
    panic(fmt.Errorf("not implemented"))
}

func (r *queryResolver) Artists(ctx context.Context, after *entgql.Cursor[int], first *int, before *entgql.Cursor[int], last *int, where *ent.ArtistWhereInput) (*ent.ArtistConnection, error) {
    panic(fmt.Errorf("not implemented"))
}

func (r *queryResolver) Songs(ctx context.Context, after *entgql.Cursor[int], first *int, before *entgql.Cursor[int], last *int, where *ent.SongWhereInput) (*ent.SongConnection, error) {
    panic(fmt.Errorf("not implemented"))
}

この panic 部分を消して、ent Client を用いた実装で埋めていきます。(commit

難しいところとして、 node クエリを実装するために、グローバルID から型を特定する仕組みを用意しなければなりません。今回のデータは、1-1000 までを楽曲 ID、1001- をアーティスト ID としているので、以下のような関数を作ります。

func getTypeFromID(id int) string {
    if id > 1000 {
        return artist.Table
    } else {
        return song.Table
    }
}

ent.WithFixedNodeType() と上記関数を用いることで、resolver がグローバル ID から対応するエンティティを取得して node として返せるようになります。

func (r *queryResolver) Node(ctx context.Context, id int) (ent.Noder, error) {
    return r.Client.Noder(ctx, id, ent.WithFixedNodeType(getTypeFromID(id)))
}

Step 5: ローカルで動くサーバを作る

プログラムの大半がこれまでに完成しました。あとは HTTP サーバを作って GraphQL ハンドラを呼べるようにするだけです。

cmd/server/main.go を実装していきます(commit)。

go run cmd/server でサーバが立ち上がるので、http://localhost:8080 にアクセスしましょう。PlayGround が表示されるはずです。

Step 6: Lambda で動くサーバを作る

最後に、このサーバを Lambda + API Gateway で動くようにしていきましょう。

まずは、Go の様々な HTTP サーバを Lambda のハンドラーに変換するライブラリ awslabs/aws-lambda-go-api-proxy を用いて Lambda 用のサーバを実装します(commit)。基本的には http.ListenAndServe() する代わりに lambda.Start(httpadapter.NewV2(http.DefaultServeMux).ProxyWithContext) するだけです。

Makefile も整備して、Lambda 向けのバイナリと db ファイルを同梱した zip ファイルを作れるようにしておきます。make server-lambda.zip を実行すると、Lambda 向けの zip ファイルが生成されます。

API Gateway や Lambda、Lambda を実行するための IAM ロールなどを作る terraform のコードを用意しておきました(commit)。AWS_PROFILE を設定した上で、(cd terraform; terraform init; terraform apply) すると、AWS 上でリソースが生成され、用意した zip ファイルを用いて deploy されます。

AWS のコンソールから API Gateway の endpoint URL を確認し、アクセスをして PlayGround が表示されたら無事完成です!クエリも叩いてみて動くかどうかを確認してみましょう。

パフォーマンス

今回のサンプルとは別のものになりますが、同じ構成で提供している GraphQL API のレスポンスに掛かる時間をお見せします。サンプルよりも多いレコード(数千レコード)が永続化されているものです。

計測のために、Mackerel の外形監視を設定してみました。リクエストボディやヘッダを以下のように設定することで、外形監視で GraphQL のクエリを実際に投げることができます。

外形監視ルールをサービスに紐付けることで、レスポンスタイムをメトリックにして投稿することができます。以下にレスポンスタイムのグラフを掲載します。

ログを見るとコールドスタートが発生しているようですが、グラフに現れている通り、50 ms ほどでレスポンスを返せているようです。十分なパフォーマンスだと言えるでしょう。

まとめ

以上のようにして、readonly な GraphQL API を安価に手間なく構築することができました。パフォーマンスに関しても満足いく水準で得られています。

今回採用したアーキテクチャやライブラリをみなさんもぜひ使ってみてください。

*1:AWSの無料枠・課金体系に関する情報は執筆当時のものであり、今後変更される可能性があります。

*2:サンプルコードのマスタデータには、id:arthur-1 が某日にカラオケで歌った曲を用いています。

PHP Conference Japan 2023 で学生時代の昔話をします

本日、2023/10/08 に開催される PHP Conference Japan 2023 のタイムテーブルが公開されました。

id:arthur-1 は 15:55~16:20 のレギュラートーク枠で、「学園祭Web開発の現場とPHPのこれまでとこれから──技術選定と教育から語る」というタイトルで登壇することになりました。

fortee.jp

学生時代の記憶が風化していくので、話せるタイミングを逃してしまいそうだったのですが、発表の機会をいただけて大変嬉しいです。当日はぜひ大田区産業プラザPiOに聞きにきてください。

仮説検証型アジャイル開発に携わり始めたエンジニアの思考(と Mackerel Drink Up の宣伝)

新卒入社から1年ちょっと経ったころ、開発チーム内での席替え*1があり、Mackerel の OpenTelemetry 対応という大きな開発プロジェクトからは離れることとなった。それまで自分が携わってきた OpenTelemetry 対応については Mackerel 公式ブログに記事を書いたのでぜひ読んでほしい。

mackerel.io

このプロジェクトを離れた代わりに、Mackerel チーム内で新たに発足した、大小様々な顧客要望に優先度をつけて仮説検証型アジャイル開発のサイクルを回し、インパクトの大きなものを高い頻度でユーザーに提供し続けることを目指したユニットに所属し、エンジニアをすることになった。このユニットでの取り組みによって、最近だと以下のような機能の提供が実現された:

この中で、良いなと思った機能・こうするともっと良くなると思った機能があれば、ぜひ私にフィードバックして欲しい。開発者に会って話せる機会もどんどん設けていく予定で、直近だと Mackerel Meetup #10 Tokyo が 8/30(水)に行われる。ユーザーの要望を拾ったり膨らませたりするために自分も積極的に顔を出す予定だ。

mackerelio.connpass.com

さて、このユニットは職種混合で構成されており、その中でエンジニアである自分に課せられた使命として、

  • サービスの開発者でありユーザーでもある2つの目線を活かし、MVP(Minimum Viable Product)の発見に必要な意見を提供すること
  • 取り組む優先順位を決め、アジャイル開発の予測可能性を高めるために、ストーリーポイントを見積もること
  • MVP を手早く開発してリリースすること
  • 仮説検証を行うためのデータを観測可能にすること

などが挙げられる。また、このユニットに属するエンジニアをリードしていくことも期待されている(と勝手に思っている)。

そんな状況下で、自分がどういう思考をして仕事に臨んでいるかを書き綴っていく。仮説検証型開発と絡めて書いたつもりだが、一例にすぎず一般的なエンジニアリングの話をしているような気もする。

使命を果たすために、自分に足りないスキルは何か

このプロジェクトの肝である仮説検証型サイクルに必要な要素として、リリースした MVP を検証するステップがある。ここで、ユーザーの行動や機能の利用状況を分析するためのデータ基盤への理解が浅いことに気づいた。

データ基盤を触れるエンジニアが固定化されているというチームの状況下で、すでにスキルを持っているエンジニアに丸投げするのではなく、まずは教えてもらい自分の手でデータ基盤のタスクに取り組むことにした。今では典型的なものであれば自走して取り組めるようになっている。

スキルマップを見つめて、自分は次に何をできるようになるべきか悩むことがあるだろう。できるマネージャーは各個人に対する期待感を言語化して説明してくれるのだが、その期待に応えるための具体的な手段は自分で見繕わなければならない。触れたい技術ややりたい仕事を見つける手段として、当たり前ではあるが個人への期待から深掘る方向性もあるよという話である。

目標を達成するために解決したい技術的課題は何か

ユニットの目標は「インパクトの大きなものを高い頻度でユーザーに提供し続ける」である。これを実現するために足枷になっている障害は何か、技術的課題であればどうやったら解決できるのか、というのを考えている。

見つけた課題についてはまず Slack に書き込みチームメイトの反応を伺うことにしている。よりコストパフォーマンスの良い解決方法が見つかるかもしれないし、適切な批評がもらえることもあるだろう。なんなら、自分が手をつけるより早く問題解決をしてくれる場合もある。

声を上げた上で、概ね以下のような分類で取り組んでいる:

  • 自分がやればすぐ解決できそうなこと
    • まずは勝手にやってみる。上手くいきそうであれば共有しチームに展開する
    • 例) ドキュメントの整備、壊れにくいテストを書く、壊れにくいスタイル定義を書く、CI をちょっと直すなどの規模感
  • 自分がある程度の時間かければ解決できそうなこと
    • スプリントタスクではなくやりたいことをやっていい時間が確保されているので、その枠で取り組む
    • 例) テストの高速化など、答えが見えきっていない規模感
  • チーム全体の合意を取って進めたいこと
    • 作文して開発チームの会議に挙げる。場合によっては ADR(Architecture Decision Record) を書く

あるべき理想を掲げるだけでも一仕事だが、実現できるように動くのはもっと大変だ。腕力を高めていくのと同時に、腕力で解決しきれない問題をどうチームで取り組めるように持っていくかという2軸の難しさがある。自分はどちらにもまだ伸び代が大分あると自覚していて、この半期で何か大きな実績を積めればいいなと考えている。

MVP の実装における、少し先の未来を見据えた設計

MVP はその名の通り Minimum な Product であるから、それを出したきりで終わりということはなく、さらなる拡張や方向性の修正が後に行われることがある。対応コストとのバランスという問題もあるが、ある程度未来を見据えた設計を最初のうちにしておくというのは大事なことで、長い目で見たときの工数を節約できると考えている。

未来の見通しがはっきりしているなら、その余地を作れば良い。地下鉄の延伸計画があるなら、今は不要でもホームを余分に作っておく、といった具合である。API を提供しているサービスでは、その破壊的な変更を行いにくいという難しさがある。段階的な移行手順を用意しなければならず、段取りを立てたり関係者とコミュニケーションを取ったりするだけで一苦労であるからだ。スキーマモデリングに関しては、将来的に破壊的な変更が起こりにくいような堅牢な設計であることをレビューで確認したい。ただ、こういう筋の良い設計スキルというのは日々の経験で身に付いていくものだと思う。自分もまだまだ経験が浅く微妙な設計をしてしまったと後悔することがある。

別解としては、破壊的な変更を行うことをユーザーに了解してもらう方法も考えられる。各種サービスで beta ラベルや experimental フラグをつけて提供されている機能が思い浮かぶだろうか。

見通しがついていなくても最低限、後にそのコードを改修する際に嫌だなと思われないような開発は心がけたい。レガシーという了解がある技術・手法にわざわざ触れないようにしている。これはコピペコーディングに頼っているとついついやってしまいがちである。テックリードやある技術に長けた人の啓蒙をちゃんと聞いて納得し、自分のスキルとする動きを続けると良い。

未来を描き説得力を持って語る

前述の話題に関連して、未来を見据えるためには、そもそも未来を描かなければならない。自分は不定期でチーム内に向けてエントリを書き、プロダクトやそれを構成するアーキテクチャについてこんな夢を持ってるという話を共有している。時には夢と夢同士が結合し一つの一般化された夢になることもある。アイデアが自分の頭の中だけにあっても、その未来は自分にしか見えず、チームとしての仕事に寄与しない。

ただ、何でもかんでもやりたいです!と叫ぶだけでは、その夢はきっと実現できないだろう。チームとして取り組む優先度を見積もるための材料(理屈)も用意することも必要だ。プロダクトのミッション・ビジョン・バリューに紐づけるだとか、ユニットや事業の目標達成に寄与することを説明するだとか、そういったコミュニケーションから逃げずに向き合っている。こういった説得力というのは本当に馬鹿にならない大事なスキルなのだと思う。これは、プロダクトに関することでも、直接は価値提供に繋がらない開発基盤に関することでも同じである。

*1:はてなでは、仕事で出てくるアレコレを学校の用語に例えて呼称することが多い

OpenTelemetry Metrics をコマンド一発で投稿できるツール、作りました

OpenTelemetry Protocol (OTLP) を受ける endpoint を開発する際、デバッグのために簡単にテレメトリデータを送りたいという需要があります。OpenTelemetry Collector をいちいち立てたり、何らかのアプリケーションを作ってそれを計装したりするのは、デバッグのためにする手順として手間がかかります。

そこで、コマンドラインで一発叩くだけでテレメトリデータを送れるツールを作ってみました。現段階では Metrics のみに対応しています。

github.com

インストール

macOSLinux であれば、 Homebrew で簡単にインストールできます。

brew install Arthur1/tap/otlc

また、以下のような設定ファイルを適当なところに置いてください。

# 送り先
endpoint: localhost:4317
# 認証のためなどでヘッダが必要であれば設定
headers:
  Authorization: Bearer your_secret_key

Metrics の投稿

以下のようなコマンドで、awesome_gauge という名前のメトリックが 123.45 という値で投稿されます。--resource-attrs--datapoint-attrs オプションでメトリックに属性をつけることもできます。

otlc metrics post --conf ./otlc.yaml --name awesome_gauge --value 123.45 \
--resource-attrs service.name=otlc --datapoint-attrs hoge=poyo,fuga=1

実装の中身

otlc/metrics/post.go at 27866d6f826d6aa267f1f0de1687fddfe50b89b9 · Arthur1/otlc · GitHub

実際に計装する時に利用する OpenTelemetry の APISDK を呼んで実現しています。より低いレイヤーのメソッドを利用して組み立てるとより自由なコマンドラインツールに仕上げられるかもしれません。

まとめ

仕事で使う道具が揃っていないと感じたら、手作りして OSS にすると良いと思います。Go 言語はコマンドラインツールを作ってさまざまな環境に向けて公開するためのエコシステムが整っていて、こういった用途に向いています。

GoReleaser を使って Homebrew でインストール可能な CLI ツールを公開する時にも GitHub Apps トークンが使える

3行

  • GoReleaser を使うと、作ったツールを Homebrew でインストールするための formula を書き出してくれて便利
  • GitHub Actions でこれを行う際、GitHub の PAT が必要とされているが、代わりに GitHub Apps トークンも使える
  • PAT を利用する方法と比較して、GitHub Apps トークンを使うと有効期限での強制的なローテーション業務が発生せず運用が楽になる

前置き

GoReleaser を使っていますか?設定ファイルを用意すると、ソースコードを様々な環境に向けてクロスコンパイルした上で、GitHub などの Release に載せられるツールです。

GitHub Actions と合わせて使うことでリリース作業を自動化でき、バージョンタグを打つだけでバイナリや checksum ファイルを生成して Release に載せることができます。

この GoReleaser には便利な機能があり、設定ファイル .goreleaser.yaml に以下のように追記することで、指定したリポジトリに Homebrew の formula ファイルを push することができます。簡単に自分の作った CLI ツールを Homebrew でインストール可能にできるわけです。

brews:
  - repository:
      owner: Arthur1
      name: homebrew-tap
      token: "{{ .Env.TAP_GITHUB_TOKEN }}"

※2023-06-13 にリリースされた v1.19.0 より、旧来 brews: - tap: と書いていた記法が deprecated になっています。cf.) https://goreleaser.com/deprecations/#brewstap

ここまでの内容についてのより詳しい説明は他の記事に譲ります:

zenn.dev

本題

さて、GoReleaser を GitHub Actions で動かす際に必要な、tap リポジトリへの push を行うための token をどのように用意していますか?多くの解説記事では GitHub の Personal Access Token (PAT) を用いる方法が紹介されています。また、GoReleaser の公式ドキュメントの例でも PAT を用いているような表現があります。

goreleaser.com

しかし、世の中には運用上の面でより優れている代わりの方法があります。それは GitHub Apps トークンを用いる方法です。GitHub App の秘密鍵から temporary なトークンを都度生成して利用します。PAT(正確には Fine-grained PAT)と異なり有効期限がないため定期的なローテーションが must にならないこと、一時トークンが流出しても秘密鍵さえ流出しなければ影響範囲を絞れること、といったメリットがあります。

PAT の代わりに GitHub Apps トークンを GitHub Apps で運用する方法について知りたい方は以下の記事を読むと良いでしょう。少々混み入ったところまで踏み込んでいますが、丁寧に説明されています。

zenn.dev

GoReleaser の Homebrew 機能のために GitHub Apps トークンを用いるための設定ポイントは以下の 2つです:

  • App の permission を設定するとき、Repository permissions の 「Contents」を 「Access: Read and Write」にセットする
  • App をインストールするとき、Only select repositories で、Homebrew tap のためのリポジトリを指定する *1

というわけで、GitHub Actions で GoReleaser を動かし、Homebrew formula を自動で書き出すときにも GitHub Apps が使えるというお話でした。

*1:All repositories でも実現可能ですが、App が持つ権限は最低限に絞りたいですよね?

ファッションを武器にする

迷ったら〜〜な方を選べ、という座右の銘テンプレートが知られているが、自分だったら「迷ったら美しい方を選べ」と埋める。

今日はあるイベントに出ていたのだが、イベント用のオリジナルTシャツを着て来て欲しいと言われていた。そのTシャツは淡いピンク色で、一般男性が電車に乗って外に出かける格好にするにはコーディネートが難しいものであった。たまたま同じ色がアクセントで入った靴を持っていたのでこれを合わせてみるといい感じになった。

イベントに行くと、参加者の方から「Tシャツと靴の色合わせたんですか?」と声を掛けてもらった。初対面の人とのコミュニケーションは苦手なので、こういう形で会話のきっかけを作れるのはありがたい。色を揃えることでイベントへの前向きな姿勢を視覚からアピールできたのもよかった。

同じような取り組みは他にもやっている。最近の例を挙げると、来週は Mackerel Meetup #14 で司会進行役をするのだが、今回のイベントのテーマカラー(バナーや配布するノベルティTシャツに使われている基調色)に合わせて髪色をネイビーにしてみた。

形から入るタイプというのはよく言ったものだが、自分は見た目から入るタイプなのかもしれない。