コネヒト開発者ブログ

コネヒト開発者ブログ

仮説→実験→検証→学び...プロダクト開発のループを実現するために行っていること

こんにちは! @TOC@takapy です。

最近は期の終わりも近づいてきたので、普段利用しているREALFORCEのキーボードを大掃除しました。めっちゃ綺麗になって気分も一新できたので、定期的にやらねばと思いました。いつもありがとうREALFORCE!

さて、今回はプロダクト開発においてMobius Outcome Deliveryの思想を取り入れた話をご紹介しようと思います。

私は7月にこのMobius Outcome Delivery研修を受け、実際にこの考えをプロダクト開発に取り入れてみることでプロダクト開発をする上で感じていた課題感を解消できるヒントになるのではないか、と思いました。

Mobius Outcome Deliveryとは何なのか、またその思想を実際にプロダクト開発においてどう取り入れたのかの具体的な事例について興味ある方の参考になれば幸いです。


目次


Mobius Outcome Deliveryとは

Mobius Navigator(https://www.mobiusloop.com/)

www.mobiusloop.com

Mobius Outcome Deliveryは価値を生み出すためのナビゲーターであり、戦略からデリバリーまでを繋ぎ、見えるようにするフレームワークです。「最小限のアウトプットで最大限のアウトカムを達成する」ことをゴールとしており、研修でも度々 アウトカム(価値) という単語が登場しました。 アイデアをいかにシンプルに小さく実験するか、を問いながら上図のようなメビウスの輪の形状をしたMobius Navigator(メビウスナビゲーター)に沿ってプロダクト開発を進めていきます。

プロダクト開発のループを大きくDiscover, Decide(Options), Deliverの3セクションで表現しており、プロダクト開発における羅針盤的なものになり得るフレームワークになっております。

弊社では「プロダクト開発は作って終わりではない」とよく言われています。Mobius Outcome Deliveryはその言葉を表現できるフレームワークである点が一番気に入っています。 ユーザーに届けて終わりではなく、届けた結果どうなったのか、そこから学びを得て次に何をやるのか、を意識できるようになっており、プロダクト改善におけるループを辿っていけるフレームワークとなっております。

当時の課題感、何がやりたかったのか

弊社では現在、アジリティが全社におけるキーワードとなっており、チームでもいかに小さく実験していけるか、が関心ごととして強くなっています。

その際に「仮説を立て、実験し、検証する」のサイクルを回しているのですが、下記のような課題感を感じておりました。

  • 各実験が点になっていて繋がっていないのではないか、繋がっていたとしてもそれが見えていないのではないか
  • なんとなく進みが遅いと感じるが、どこがボトルネックになっているのか特定がしづらいのではないか
  • やったことがチーム内に閉じていて、その知見をチーム間や将来の誰かのために活かせられてないのではないか

当時、プロダクトマネジメント本の輪読会を行ったメンバーとプロダクト開発におけるプロセスの改善を行おうと思ったときに課題感を擦り合わせたのですが、上記3点がほぼ同じ課題感として挙がっており、「仮説→実験→結果→学び→仮説 … のループを回したい、それを見える化したい」という共通の思いが一致したので、これを実現できる方法を模索することになりました。

解決に向けて、方法の模索

Mobius Outcome Deliveryが仮説→実験→結果→学び→仮説 … のループをうまく表現していると思ったので、この研修で得たものを共有し、どうやってこの思想を表現できるか、プロダクト開発に取り入れられるか、を考えてみました。

そこでこの思想をプロダクト開発に取り入れるためのツールとして、 Miro など色々な方法を模索したのですが、Notionのプロジェクト管理テンプレートが一番やりたいことを実現できるのではないか、と思いました。

www.notion.so

Notionのプロジェクト管理は1つのプロジェクトに対して複数のタスクが紐づく形で構成されています。

Notion Projectについて

これを弊社のプロダクト開発に応用して、1つのやりたいこと、達成したい価値に対して実験を紐づけることでやりたいことができるのではないか、と考えました。

実際に作成したものをもとにどう実現したのかを具体的に説明します。

Mobius Outcome Deliveryを実現するために、どうやってNotionを活用したのか

以下Mobius Outcome Deliveryを実現する上でポイントになる点に絞って説明していきます。

Discover:テーマを作成する

Discoverの道のり

まずは上図の左側、Discoverの部分です。ここでは「なぜやるのか、誰に向けてやるのか」といった問題設定を行い、それを解決した際に得られるアウトカム、を決定します。

この部分をNotion Projectsにおける プロジェクト として作成します。これを自分達は テーマ と呼ぶようにしました。

データベースから新規テーマを作成すると下記のようなテンプレートが表示されます。

テーマのテンプレート

このテンプレートを埋めていくことでMobius Outcome Deliveryの左側Discoverを辿っていく形式になります。

Options, Deliver:実験を作成する

Options, Deliverの道のり

実際にやりたいことが定まったら、実現するための実験を考えていきます。

どうやったら一番シンプルに簡単に実験できるかを考え、やることが決まったら実験を作成します。

実験の作成

実験を作成するボタンを押すと、このテーマに紐づいた実験が作成されます。

実験はテーマが紐づいた状態で作成されるので、この実験は何を実現するためのものなのか、といった紐付けが見えるようになります。

このドキュメントは弊社では実験ドキュメントと呼ばれており、どうやって実験するのか、この実験で得た学び、などを記載できるようになっています。

この実験ドキュメントについては後の章で詳しく説明します。

テーマと実験の紐付け

実験が終わり、学びを得たら、次の一手を決めていきます。

Decide:もっと作るのか、別の問題を見つけるのか

Decideの道のり

実験をしても必ずしもうまくいくとは限りません。やってみて実際に得た反応をもとにもっと作っていくのか、はたまた別の問題を見つけるのか、といった選択をする必要があります。

これはMobius Outcome DeliveryでいうDecideのフェーズであり、ここで分岐が発生します。 この次への繋がりが一番実現したかった部分であり、今回のこだわったポイントでもあります。

私たちはこの繋がりをNotionのリレーションを用いて表現しました。

1. もっと作るという判断をした場合

例えば実際にユーザーに使ってもらい反応をみた際に、ニーズはありそうだが、少し別の形で実装をしてみたい、と思ったとします。Mobius Navigatorでいうところの再度Createのループに入るイメージです。

もっと作る判断をした場合

この時は実験ドキュメントで「次の実験を作成する」ボタンを押します。

次につながる実験を作成する

これを押すと2回目の実験が作成され、 前の実験 プロパティに1回目の実験とのリレーションが作成されます。

このリレーションにより2回目の実験は何を根拠に生まれた実験なのか、の繋がりを表現できるようになりました

実験の繋がり

2. 別の問題を見つけるという判断をした場合

実際にユーザーに届けてみるとニーズがないことがわかったり、実験をすることで別の問題を見つけることもあります。次に繋がる実験だったということですね。

この時Mobius Navigatorでいうところの再度Discoverのループに入るイメージです。

別の問題を見つけた場合

テーマのドキュメントに戻るとネクストアクションが設定されています。ここで「次のテーマを作成する」ボタンを押すと、次に繋がるテーマが作成されます。

次に繋がるテーマを作成する

このリレーションが作成することで、テーマ間での繋がりも作成されます。

こうすることで下図のように1つ1つのループにおける繋がりも表現できるようになりました。

ループが繋がっていくイメージ

まとめると

テーマと実験の関係図をまとめると下記のようになります。

テーマと実験の関係図

また、一連の流れをMobius Navigatorのループに当てはめると下記のようになります。

Mobius Navigatorに沿ってやること

このようにNotionを利用することで、Mobius Outcome Deliveryのフローをできるだけ簡単に、かつテンプレートに沿っていくことで実現できるような仕組みを作成しました。

最終的に作成したものはコネヒトカンバンと命名し、PdMを中心に展開をしていきました。

コネヒトカンバン爆誕

補足: 実験ドキュメントについて

ここで、先程説明をスキップした実験ドキュメントの詳細について改めて紹介します。

上記で説明した「テーマ」に紐づく形で、「実験ドキュメント」が作成されます。

前述した通り、この実験ドキュメントは1つのテーマに対して複数紐づくものとなっています。また、実験同士の繋がり(前後関係)も表現することができます。

実験ドキュメントのテンプレートは、以前のブログでも紹介した「A/Bテスト標準化テンプレート」を土台として、少しブラッシュアップして作成しました。

tech.connehito.com

テンプレートに入れ込んだ最終的な項目は以下のようなものです。

  • 実験の背景
  • 実験の概要
  • モニタリング指標
  • 実験後のアクションプラン
  • 実験結果

以降ではブラッシュアップしたポイントを中心に紹介します。

ステータス管理を容易に

冒頭で述べた通り、課題感の1つに「なんとなく進みが遅いと感じるが、どこがボトルネックになっているのか特定がしづらいのではないか」というものがありました。

そこで

  • 各実験は今どのステータスなのか
  • ステータスが暫く変わっていないのであれば、何が原因で変わっていないのか

などの現状を知り、そこでボトルネックを共有して解決を図れるような仕組みにできるよう、ステータス管理を簡単に行えるようにしました。

Notionで以下のようなテンプレートを用意しておき、「開発開始」や「検証開始」などのボタンを押すだけで、ステータスが変更されるようにしました。(ボタンのロジックなどは後述)

ステータス変更を簡単にするためのボタン

テンプレートの末尾には、実験終了したタイミングでステータス更新 & 結果記入欄が表示されるボタンも追加しています。

実験が終了した際のボタン

実験のサマリを把握しやすく

実験ドキュメントは現在の状態を把握できるだけでなく「過去に何がうまくいって、何がうまくいかなかったのかを振り返ることができる」ドキュメントとしても効果を発揮します。

過去の実験を一覧で見たときに「どの実験がどんな目的で行われて、結果はどうだったのか」がパッと分かるとハッピーですよね。

そこで、実験のサマリをNotionのページプロパティに持たせることで、一覧で見たときに逐一ドキュメントの中身を開かずとも大まかな内容を知ることができるようにしました。

実験サマリプロパティのテンプレート

一覧で見るとこんな感じです(マスク部分が多くて分かりづらいかもしれませんが雰囲気だけでも感じていただければと)

実験一覧

カンバンを作成する上で工夫した点

以上が大まかなカンバンの仕組みと詳細になります。

今回このコネヒトカンバンを作成するにあたって、実際に使ってもらうようにする工夫をいくつか行ったのでご紹介します。

利用者はできるだけ流れに沿うだけで利用できるようにする

今回カンバンを作成するにあたって、とにかく流れに沿えば自動的にメビウスループに乗っているような設計を心がけました。

その際に役に立ったのがNotionのボタン機能です。

www.notion.so

この機能を使うことで、ボタンをクリックした時にプロパティの操作やページ作成などを自動で行ってくれます。

例えば「次のテーマを作成する」ボタンはクリックすると下記のような動作を自動でおこなってくれます。

操作するページのステータスを 完了 にする→繋がりを紐づけてページを作成する→新規ページを開く

ボタンを使ったプロパティの変更設定

このように利用者はボタンを押すだけで自然とループに乗っている状態を目指すことで利用側のハードルをできるだけ下げるようにしました。

目的や思想、使い方のドキュメントを用意する

今回カンバンを作るにあたって、目的や思想の設計を入念に行いました。

実際にこのカンバンを作った人はいいのですが、利用する人たちはやりたいことなどを理解しないと「これをやる目的ってなんだろう…」といった状態になってしまいます。

なので、このカンバンの目的やどういった思想のもとで作成しているのかを書いたドキュメントを用意しておきました。

カンバンの目的を書いたドキュメントの一部

ただ、用意しているだけでちゃんと読んでもらえるとは限りません。そこは読んでもらう工夫だったり、仕組み化を今後も考える必要があると思っています。

実際に運用してみてどうだったか

PdMを中心に実際に利用してもらい、数ヶ月が経ちました。実際に使ってみた感想をアンケートで取ってみたので一部ご紹介します。

全チームの状況が一箇所で見れるのが最高

ステータスが俯瞰でみれるのがけっこう好き

特に繋がりの部分はポジティブな意見が多かったです

前の繋がりがあるから文脈思い出しやすい(あーこのゴールに向かう施策ね〜的な)

前後の繋がりが圧倒的にわかりやすくなった!

みんな書いてるけど、つながりがみえるようになったのはとても良い!

嬉しい声もありつつ、下記のような課題感もあらためて発見できました。

説明されると理解できるが、自分だけだと構造や使い方を理解するのにハードルがあった

実験というワードが強く、1回きりの施策の時(実験じゃない場合)に書いていいのかちょい悩むことがある

前後のつながりが見えるようになったのはとても良いが、まだ運用して時間が経っていないので本当に効果的かは長い目で見たい

ある程度想定はしていたり、対策をしたつもりでしたが、やはりこの辺は難しいですね。

浸透に関しては辛抱強く、でも楽しみながらみんなで続けていって、より良いものになるようにしていきたいと思ってます!

今後の展望

このカンバンを導入して、約2ヶ月ほどが経過しました。 色々書きましたが、まだ道半ばであり実験的な取り組みです。

改めて自分達がやりたかったこと、達成したかったことができてるかと言われると、できつつあるけど、まだまだこれから、と言う状態です。なので、今後もブラッシュアップしていきたいと思っています。

例えば、実験で得た知見をPdMやチーム間で共有できるような仕組みを用意したり、もっと過去の繋がりを遡って追加したり…などなど。

やりたいことがたくさんありすぎ状態ですが、同時にワクワクもしているのでこの取り組みを今後も続けていって、Mobius Outcome Deliveryの知見と実践を積んでいきたいと思います。

並行処理と並列処理の違いをコンテキストから考える

こんにちは!バックエンドエンジニアのjunyaUと申します。

実は9月入社で入社してから1ヶ月も経っていない(執筆当時)のですが、テックブログを書かせていただけるのは本当にありがたい環境だな〜とヒシヒシと感じております。

最近、プライベートではもっぱら自作OSの開発をしており、時間があっという間に溶けてしまいご飯をちゃんと食べれていないのが悩みです。

今回は並行処理と並列処理の違いをコンテキストの観点から考えて、両者がどのように違うのかを考察していこうと思います〜!

はじめに

なぜ記事を書こうと思ったのか

数年前、私が学生だった頃に初めてGoを触り、そこで「並行処理」という言葉を初めて耳にしました。その言葉を初めて聞いたとき、「プログラムを同時に動かすことができるのか!」とワクワクしましたが、並行処理について調べてみると、

  • 実は高速に切り替えて実行しているだけで本当に同時に実行していない
  • 並列処理と並行処理があり、並列処理は本当に同時に実行している

というような内容が書かれており、当時は全く理解できずぼんやりとしたまま考えるのをやめてしまいました。

時は過ぎ、私はOSの自作やコンピューターサイエンスの勉強にハマりました。

そこで、プロセスの並行処理を調べたり自分で実装したりするうちに並行処理と並列処理への解像度が少し上がりました。

ネットで並列処理と並行処理の違いを調べてみても、この問題を考える上で重要なコンテキストやコンテキストスイッチの概念に触れている記事が少なかったので、今回はコンテキストの観点から両者の違いを考えていこうと思います。

並行処理と並列処理の違いの結論

結論から述べると、プロセスAとプロセスBの実行に際して、

  • 並行処理は、特にシングルコアの場合、1つのコアがAとBのコンテキストを高速に入れ替えながらプロセスを実行する方式
  • 並列処理は、複数コアを使って、AとBのプロセスを真に同時に実行する方式

となります。

この違いを理解するためには、コンテキストの概念の理解は不可欠です。

次節からコンテキストに焦点を当てて並行処理と並列処理の違いを見ていきます。

プロセスのコンテキストとは?

コンテキストの定義

コンテキストとは、プロセスの現在の実行状態を表す情報の集合を指します。

これは、プロセスが中断された後、その状態から継続して実行できるようにするための「状態のスナップショット」であると言えます。

実際には、プロセスの実行状態を保存するためのデータは様々ありますが、重要でわかりやすいデータとしては、以下のものがあります。

  • プログラムカウンタ : 次に実行する命令が格納されているアドレス
  • スタックポインタ : 変数や一時的な計算結果など、プログラムの実行に必要なデータが格納されているアドレス
  • フラグレジスタ:条件分岐や算術命令の結果に基づくフラグの値

これらのデータの多くがCPUのレジスタに格納されています。

なので、コンテキストとはレジスタの内容 と考えることができます。

では、このレジスタについて詳しく見ていきます。

レジスタとは?

CPUとメモリの簡略図

レジスタはCPUの内部に存在する、高速にアクセス可能である小さな記憶領域です。

CPUはメインメモリに直接アクセスするよりもレジスタへのアクセスの方が高速であるため、頻繁に使用されるデータや、実行中の命令の情報は一時的にレジスタに格納されます。

CPUの動作は、基本的に「命令のフェッチ」と「命令の実行」の繰り返しで、フェッチする際はメモリからデータや命令を取得し、レジスタに格納され、レジスタの値から処理が行われます。

このレジスタには先ほど述べた、プログラムカウンタやデータなどが格納されているわけですが、

もしプロセスAの実行中に、プロセスBのデータのアドレスやプログラムカウンタの情報でレジスタの内容を上書きした場合、どうなるでしょうか?

プロセスAの実行中にも関わらずいきなりプロセスBの実行に切り替わってしまいます。

これが、並行処理で行われていることの核心となる部分です。

次の節で詳しく見ていきます。

並行処理とは?

並行処理の定義

並行処理の定義は、「1つのCPUコアに対して複数のコンテキストを高速に入れ替えながらプロセスを実行する方式」と冒頭で述べました。

改めて、コンテキストにフォーカスして考えてみます。それぞれのプロセスはそれぞれのコンテキストを持っています。(メモリに格納されている場所やデータがそれぞれ違うので当たり前ですね)

メモリの簡略図(アドレスや配置は説明用なのでデタラメです)

上の簡単な図をもとに考えてみます。

プロセスAのコンテキスト

  • プログラムカウンタ: 0x1000
  • スタックポインタ: 0x3000

プロセスBのコンテキスト

  • プログラムカウンタ: 0x7000
  • スタックポインタ: 0x9000

プロセスAが実行され始めた時、レジスタにはプログラムカウンタの0x1000、スタックポインタの0x3000を始めとした様々な値が保存されています。

ここで、プロセスAの実行中に現在のレジスタの値をどこかに退避させ、プロセスBのコンテキストをレジスタに格納すると、プロセスAの処理が中断され、プロセスBに処理が切り替わります。

これを連続して高速に行うとどうなるでしょうか?

CPUから見ると、複数のプロセスを高速に1つずつ処理しているだけなのですが、

人間から見ると、プロセスAとプロセスBが同時に動いているように見えると思います。

これが並行処理なのです。

コンテキストスイッチ

コンテキストスイッチの簡略図

並行処理について調べていると「コンテキストスイッチ」というワードを目にすることがあると思います。当時の私にとってはこれが難しく理解できませんでした。

ですが、コンテキストスイッチの説明は既に上の節で述べられています。

プロセスAの実行中に現在のレジスタの値をどこかに退避させ、プロセスBのコンテキストをレジスタに格納すると、プロセスAの処理が中断され、プロセスBに処理が切り替わります。

この部分です。レジスタの値をプロセスAのコンテキストからプロセスBのコンテキストに切り替えるという処理がコンテキストスイッチとなります。

もう少し砕いた言い方をすると、コンテキストスイッチ = レジスタの値の入れ替え

ということがいえます。

また、高速にコンテキストスイッチを行うことが並行処理といえます。

並列処理とは?

並列処理の定義

並列処理の定義は、「並列処理は複数コアを使って、AとBのコンテキストをそれぞれ異なるコアに格納し、複数のプロセスを真に同時に実行する方式」と冒頭で述べました。

並行処理は1つのCPUコアが複数のプロセスを切り替えながら実行するものなので、実際には同時実行していませんが、並列処理は真に同時に実行しています。

なぜ並列処理は真に同時に実行することができるのでしょうか?

コンテキストとマルチコア

並列処理の真髄は、CPUが複数のコンテキストを同時に扱える能力にあります。

コンテキストの複数保持を実現してくれるのがマルチコアの存在です。

CPUコアはコアごとにそれぞれレジスタを持っており、複数のコンテキストを同時に保持、実行することができます。

プロセスAとプロセスBを並列実行している簡略図

並行処理はコア1つに対してAとBを同時に実行させようとしていたので、高速にAとBをコンテキストスイッチする必要があったのに対して、並列処理は複数のコアを用いるのでコンテキストスイッチをすることなくそれぞれのコアにコンテキストを保持させて真に同時に実行することができます。

まとめ

並行処理は1つのコアに高速にコンテキストを切り替えて実行していく方式で、

並列処理は複数コアが、同時に実行したいプロセスのコンテキストをそれぞれ保持して同時に実行をしていく方式でした。コンテキストからのアプローチで見ると思っていたより理解しやすいのではないでしょうか。

実際には、どちらの処理方式も単独で使用されることは少なく、一般的にはこれらを組み合わせて利用されます。今回は、それぞれの方式の懸念点やパフォーマンス上のトレードオフ、最適な使い所には触れていませんが、これらのテーマは非常に奥が深いので、機会があればまた書きたいなと思います。

なお、今回の説明はわかりやすさを重視して簡略化している点もあるため、その点をご了承ください。

Lambda も Chatbot も SNS も使わない!EventBridge だけで CloudWatch Alarm のメッセージを加工し Slack へ post する方法

こんにちは。開発部プラットフォームグループでインフラエンジニアをしている @sasashuuu です。先日、Custom notifications are now available for AWS Chatbot で AWS Chatbot でのカスタム通知が行えるアップデートが発表されました。 実はこの方法以外にも、AWS EventBridge だけで CloudWatch Alarm のメッセージをさらに柔軟に加工して Slack へ post する方法があることをご存知でしょうか。Chatbot のアップデートが発表される前に当社で導入していたのですが、SNS や Chatbot を組み合わせたアーキテクチャを組まなくても良い他、 Lambda のランタイムの管理や実装の煩雑さのリスクなども減るので意外と活用できる方法なのではないかと思っております(実は当時 Chatbot を利用した上でメッセージ加工ができないかを模索した結果、できないことを知りこの方法に辿り着きました。そしてその方法を紹介しようとこのブログの8割ほどを執筆し終えた時に Chatbot のアップデートが発表され、なんとも言えない気持ちになったことは内緒です...)。この記事ではその方法についてご紹介したいと思います!

Before/After

まずメッセージの Before/After からお見せします。

ECS のタスク数が一定時間に必要な数から基準を下回った際に発生するアラートを例にご紹介します。

Before

Before の状態は Chatbot を通じて post されたデフォルトのメッセージとなっています。内容は充実しているのですが、Slack 通知をする上では情報過多な印象で、見慣れていなければどういった内容のアラートなのかを瞬時に把握し、対応するのはハードルが高いような印象でした。

After

After の状態は EventBridge を通じて加工されたデフォルトのメッセージとなっています。

メッセージの変更内容ですが、以下のような点を工夫しました。

  • 端的に何を示すアラートなのかを日本語で表現
  • 弊社の現場において Slack で通知する上で不要と判断した情報を除去
  • アラートに対するアクションを促せるような文言を追加

アーキテクチャ概要

タイトルや冒頭でも触れていましたが、AWS EventBridge を使用します。

重要となる構成要素は以下のラインナップです。

  • イベントパターン
    • AWS で発生したイベントを検知するためのパターン
  • ルール
    • AWS サービスのイベントを検知しターゲットへ送信するためのリソース
  • 入力トランスフォーマー
    • ターゲットへ渡すためのイベント(テキスト)をカスタマイズできる機能
  • ターゲット
    • 対象のイベントおよびパターンにマッチした際に送信先となるエンドポイント
  • API Destination
    • ターゲットに HTTP エンドポイントを指定できる機能

上記を組み合わせて作成した処理の流れをざっくりと説明すると以下のようになります。

実装手順

ここからは具体的な実装手順について解説します。構築時はコンソールでの作業を中心に進めていましたが、ここでは最終的に IaC へ落とし込んだ Terraform のコードをメインに解説させていただきますのでご了承ください。

Slack App を作成

Slack App が必要になるので、なければ作成をしてください。

https://api.slack.com/apps へアクセス後、下記の動線から作成が行えます。

Incomming Webhooks の作成

Incomming Webhooks を使用しますので、作成し取得しておきます。

下記の動線から作成&取得可能です。

書き込み権限の追加

Slack App へ chat:write の権限を追加します。

下記の動線から設定可能です。

Terraform の実装

下記はルールに関する定義です。

resource "aws_cloudwatch_event_rule" "demo_alert_alarm" {
  name = "demo-alert-alarm"
  event_pattern = jsonencode(
    {
      source = [
        "aws.cloudwatch"
      ],
      detail-type = [
        "CloudWatch Alarm State Change"
      ],
      resources = [
        {
          prefix = "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev"
        }
      ],
      detail = {
        state = {
          value = [
            "ALARM"
          ]
        }
      }
    }
  )
}

マッチさせたい条件を HCL に記述し、event_pattern の値に定義しています。

下記はターゲットに関する定義です。

resource "aws_cloudwatch_event_target" "demo_alert_alarm" {
  arn      = aws_cloudwatch_event_api_destination.demo_alert.arn
  role_arn = aws_iam_role.demo_alert.arn
  rule     = aws_cloudwatch_event_rule.demo_alert_alarm.id

  input_transformer {
    input_paths = {
      account     = "$.account"
      alarmName   = "$.detail.alarmName"
      description = "$.detail.configuration.description"
      reason      = "$.state.reason"
      region      = "$.region"
      time        = "$.time"
    }
    input_template = <<EOF
    {
  "attachments": [
      {
          "color": "#E01D5A",
          "blocks": [
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "*<alarmName> - ECSの必要なタスク数が足りていません*"
                  }
              },
              {
                  "type": "section",
                  "fields": [
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarm名:*\n<alarmName>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarmの詳細:*\n *<https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/<alarmName>?|AWS Console URL>*"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*AWSアカウント:*\n<account>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*リージョン:*\n<region>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*発生日時:*\n<time>"
                      }
                  ]
              },
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "アラートの内容に注意してください。\\n必要であれば *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラート管理表 対応ログ>* から「 <alarmName> 」をアラーム名のプロパティでフィルターし、過去の記録を参考に対応してください。\\n( *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラートの対応記録方針>* を参考に今回の対応記録も残しましょう。)"
                  }
              }
          ]
      }
  ]
}
    EOF
  }
}

input_transformer では、local.input_path_settings で local values の EventBridge の変数用の定義を指定し、input_template では post する Slack のメッセージのヒアドキュメントを定義しています。このヒアドキュメントの中で、EventBridge の変数を のように使用し、メッセージで展開が行えます。

下記はターゲットに付随するリソースに関する定義です。

resource "aws_cloudwatch_event_api_destination" "demo_alert" {
  connection_arn                   = aws_cloudwatch_event_connection.demo_alert.arn
  http_method                      = "POST"
  invocation_endpoint              = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" # webhook URL はここでは管理しない
  invocation_rate_limit_per_second = 300
  name                             = "demo-alert"
  lifecycle {
    ignore_changes = [
      invocation_endpoint
    ]
  }
}

resource "aws_cloudwatch_event_connection" "demo_alert" {
  authorization_type = "API_KEY"
  name               = "demo-alert"
  auth_parameters {
    api_key {
      key   = "Authorization"
      value = "dummy"
    }
  }
}

aws_cloudwatch_event_api_destination, aws_cloudwatch_event_connection では イベントの送信先と接続設定に関する設定を行なっています。aws_cloudwatch_event_api_destination の invocation_endpoint には Webhook URL の値が設定されます。aws_cloudwatch_event_connection の auth_parameters.api_key の設定は使用しないため、適当な文字列の設定で大丈夫です。また、注意点としてここでは説明をわかりやすくするために、invocation_endpoint をマスクした値でハードコードしているような実装になっていますが、セキュアな情報となりますのでこの辺りの管理や参照などは適宜安全な方法で行ってください。

最後は IAM に関する定義です。

EventBridge が ApiDestination へイベントを送信するための権限などを設定しています。

resource "aws_iam_role" "demo_alert" {
  assume_role_policy = jsonencode({
    "Statement" : [
      {
        "Action" : "sts:AssumeRole",
        "Effect" : "Allow",
        "Principal" : {
          "Service" : "events.amazonaws.com"
        }
      }
    ],
    "Version" : "2012-10-17"
  })
  managed_policy_arns = [
    aws_iam_policy.demo_alert.arn,
  ]
  max_session_duration = 3600
  name                 = "demo-alert"
}

resource "aws_iam_policy" "demo_alert" {
  name = "demo-alert"
  policy = jsonencode({
    "Statement" : [
      {
        "Action" : [
          "events:InvokeApiDestination"
        ],
        "Effect" : "Allow",
        "Resource" : [
          "arn:aws:events:ap-northeast-1:xxxxxxxxxxxx:api-destination/demo-alert/*"
        ]
      }
    ],
    "Version" : "2012-10-17"
  })
}

ここまで解説した内容の実装は、CloudWatch Alarm の state だと ALARM に関連する内容のものとなっています。ですが、実際にはアラートが復旧した場合の OK 通知も必要となる場合がほとんどでしょう。そのため下記のリソースは state ごとに 1セットで作成するので必要に応じて追加してください。(その他は共用で利用)

  • aws_cloudwatch_event_rule
  • aws_cloudwatch_event_target

最終的に出来上がった全体のコードはこちらになります。

resource "aws_cloudwatch_event_rule" "demo_alert_alarm" {
  name = "demo-alert-alarm"
  event_pattern = jsonencode(
    {
      source = [
        "aws.cloudwatch"
      ],
      detail-type = [
        "CloudWatch Alarm State Change"
      ],
      resources = [
        {
          prefix = "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev"
        }
      ],
      detail = {
        state = {
          value = [
            "ALARM"
          ]
        }
      }
    }
  )
}

resource "aws_cloudwatch_event_rule" "demo_alert_ok" {
  name = "demo-alert-ok"
  event_pattern = jsonencode(
    {
      source = [
        "aws.cloudwatch"
      ],
      detail-type = [
        "CloudWatch Alarm State Change"
      ],
      resources = [
        {
          prefix = "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev"
        }
      ],
      detail = {
        state = {
          value = [
            "OK"
          ]
        }
      }
    }
  )
}

resource "aws_cloudwatch_event_target" "demo_alert_alarm" {
  arn      = aws_cloudwatch_event_api_destination.demo_alert.arn
  role_arn = aws_iam_role.demo_alert.arn
  rule     = aws_cloudwatch_event_rule.demo_alert_alarm.id

  input_transformer {
    input_paths = {
      account     = "$.account"
      alarmName   = "$.detail.alarmName"
      description = "$.detail.configuration.description"
      reason      = "$.state.reason"
      region      = "$.region"
      time        = "$.time"
    }
    input_template = <<EOF
    {
  "attachments": [
      {
          "color": "#E01D5A",
          "blocks": [
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "*<alarmName> - ECSの必要なタスク数が足りていません*"
                  }
              },
              {
                  "type": "section",
                  "fields": [
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarm名:*\n<alarmName>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarmの詳細:*\n *<https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/<alarmName>?|AWS Console URL>*"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*AWSアカウント:*\n<account>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*リージョン:*\n<region>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*発生日時:*\n<time>"
                      }
                  ]
              },
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "アラートの内容に注意してください。\\n必要であれば *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラート管理表 対応ログ>* から「 <alarmName> 」をアラーム名のプロパティでフィルターし、過去の記録を参考に対応してください。\\n( *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラートの対応記録方針>* を参考に今回の対応記録も残しましょう。)"
                  }
              }
          ]
      }
  ]
}
    EOF
  }
}

resource "aws_cloudwatch_event_target" "demo_alert_ok" {
  arn      = aws_cloudwatch_event_api_destination.demo_alert.arn
  role_arn = aws_iam_role.demo_alert.arn
  rule     = aws_cloudwatch_event_rule.demo_alert_ok.id

  input_transformer {
    input_paths = {
      account     = "$.account"
      alarmName   = "$.detail.alarmName"
      description = "$.detail.configuration.description"
      reason      = "$.state.reason"
      region      = "$.region"
      time        = "$.time"
    }
    input_template = <<EOF
    {
  "attachments": [
      {
          "color": "#2DB57C",
          "blocks": [
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "*<alarmName> - ECSの必要なタスク数が足りていません*"
                  }
              },
              {
                  "type": "section",
                  "fields": [
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarm名:*\n<alarmName>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarmの詳細:*\n *<https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/<alarmName>?|AWS Console URL>*"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*AWSアカウント:*\n<account>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*リージョン:*\n<region>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*発生日時:*\n<time>"
                      }
                  ]
              },
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "引き続きアラートに注意してください"
                  }
              }
          ]
      }
  ]
}
    EOF
  }
}

resource "aws_cloudwatch_event_api_destination" "demo_alert" {
  connection_arn                   = aws_cloudwatch_event_connection.demo_alert.arn
  http_method                      = "POST"
  invocation_endpoint              = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" # webhook URL はここでは管理しない
  invocation_rate_limit_per_second = 300
  name                             = "demo-alert"
  lifecycle {
    ignore_changes = [
      invocation_endpoint
    ]
  }
}

resource "aws_cloudwatch_event_connection" "demo_alert" {
  authorization_type = "API_KEY"
  name               = "demo-alert"
  auth_parameters {
    api_key {
      key   = "Authorization"
      value = "dummy"
    }
  }
}

resource "aws_iam_role" "demo_alert" {
  assume_role_policy = jsonencode({
    "Statement" : [
      {
        "Action" : "sts:AssumeRole",
        "Effect" : "Allow",
        "Principal" : {
          "Service" : "events.amazonaws.com"
        }
      }
    ],
    "Version" : "2012-10-17"
  })
  managed_policy_arns = [
    aws_iam_policy.demo_alert.arn,
  ]
  max_session_duration = 3600
  name                 = "demo-alert"
}

resource "aws_iam_policy" "demo_alert" {
  name = "demo-alert"
  policy = jsonencode({
    "Statement" : [
      {
        "Action" : [
          "events:InvokeApiDestination"
        ],
        "Effect" : "Allow",
        "Resource" : [
          "arn:aws:events:ap-northeast-1:xxxxxxxxxxxx:api-destination/demo-alert/*"
        ]
      }
    ],
    "Version" : "2012-10-17"
  })
}

additional

ここまでは基本的な実装方法をお伝えしましたが、対象の CloudWatch Alarm リソースが増える度に定義が増えてしまう冗長な設計となっていました。応用として下記のように local values に CloudWatch Alarm ごとの固有の設定に関する定義を切り出し、loop させて DRY に実装する方法もおすすめです。ここでは詳しくは解説しませんが、一部のサンプルコードをご紹介しておきます。

locals {
  input_paths = {
    account     = "$.account"
    alarmName   = "$.detail.alarmName"
    description = "$.detail.configuration.description"
    reason      = "$.state.reason"
    region      = "$.region"
    time        = "$.time"
  }

  config = [
    {
      target_arn     = aws_cloudwatch_event_api_destination.demo_alert.arn
      name_prefix    = "demo-alert"
      prefix_pattern = "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev"
      alert = {
        alarm = {
          main_message = "*<alarmName> - ECSの必要なタスク数が足りていません*"
          sub_message  = "アラートの内容に注意してください。\\n必要であれば *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラート管理表 対応ログ>* から「 <alarmName> 」をアラーム名のプロパティでフィルターし、過去の記録を参考に対応してください。\\n( *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラートの対応記録方針>* を参考に今回の対応記録も残しましょう。)"
          color        = "#E01D5A"
          state_type   = "ALARM"
        }
        ok = {
          main_message = "*<alarmName> - ECSの必要なタスク数が足りていません*"
          sub_message  = "引き続きアラートに注意してください"
          color        = "#2DB57C"
          state_type   = "OK"
        }
      }
    }
  ]
}

resource "aws_cloudwatch_event_rule" "demo_alert" {
  for_each = {
    for config in local.config : config.name_prefix => {
      name_prefix    = config.name_prefix
      prefix_pattern = config.prefix_pattern
      alert          = config.alert
    }
  }

  name = format("%s-%s", each.value.name_prefix, "alarm")
  event_pattern = jsonencode(
    {
      source = [
        "aws.cloudwatch"
      ],
      detail-type = [
        "CloudWatch Alarm State Change"
      ],
      resources = [
        {
          prefix = each.value.prefix_pattern
        }
      ],
      detail = {
        state = {
          value = [each.value.alert.alarm.state_type]
        }
      }
    }
  )
}

resource "aws_cloudwatch_event_target" "demo_alert" {

  for_each = {
    for config in local.config : config.name_prefix => {
      name_prefix    = config.name_prefix
      prefix_pattern = config.prefix_pattern
      alert          = config.alert
      target_arn     = config.target_arn
    }
  }

  arn      = each.value.target_arn
  role_arn = aws_iam_role.demo_alert.arn
  rule     = aws_cloudwatch_event_rule.alarm[each.value.name_prefix].id

  input_transformer {
    input_paths    = local.input_paths
    input_template = <<EOF
    {
  "attachments": [
      {
          "color": "${each.value.alert.alarm.color}",
          "blocks": [
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "${each.value.alert.alarm.main_message}"
                  }
              },
              {
                  "type": "section",
                  "fields": [
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarm名:*\n<alarmName>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarmの詳細:*\n *<https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/<alarmName>?|AWS Console URL>*"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*AWSアカウント:*\n<account>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*リージョン:*\n<region>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*発生日時:*\n<time>"
                      }
                  ]
              },
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "${each.value.alert.alarm.sub_message}"
                  }
              }
          ]
      }
  ]
}
    EOF
  }
}

おわりに

今回は、EventBridge で CloudWatch Alarm のメッセージを加工し、Slack へ post する方法をご紹介しました。Lambda などを使わずとも柔軟にメッセージを加工できるのは個人的に発見でした。皆さんも機会があればぜひ試してみてください!

Go言語による並行処理の輪読会で行った、輪読会でワイワイするための工夫

こんにちは!@TOC です! 最近は「ゾン100〜ゾンビになるまでにしたい100のこと〜」というアニメにハマっており、見るたびに「やりたいことをやるんだ!」という気持ちになります🙌 キャラも魅力的なのでこれからが楽しみですね!

さて、今回は社内で行った「Go言語による並行処理」本の輪読会が最近完走したので、その様子をご紹介しながら、輪読会をより良いものにするために行った工夫点についてご紹介します。


目次


輪読会で読む本の選定

今回はオライリーから出版されている「Go言語による並行処理」という本を読む本として選定しました。

www.oreilly.co.jp

Goに関する本は数多く出版されていますが、この本はGo言語における並行処理に特化した本で、Goの並行処理を学ぼうと思った人は一度目にしたことがあるのではないでしょうか。

数あるGoの本の中からこの本を選定した理由は2つあります。

  1. 本の内容として難易度が高く、一人で読むと心が折れそうだから
  2. Goの強みでもある並行処理を業務で活かせるレベルに引き上げたいから

一つ目は少し弱気な理由にはなりますが、今回輪読会をやった感想としてはかなり大事なポイントかなと思っております。「Go言語による並行処理」本はかなり丁寧に解説されてるので、読んだらなんとなくわかった気になりますが、理解を深める・自分の中で昇華するまで一人で行うにはかなり骨が折れます。

以前、GoのLT会でこの本が紹介された時も「一人で読んで心が折れた」と言った意見が多く見られました。

その点、輪読会で一緒に読む仲間がいると進めようという気持ちも高まりますし、議論することで理解が深まりやすいです。なので、この本に限らず、興味あるけどなかなか読み進められない本は一緒に読む仲間を見つけるのは一つ良い方法かなと思います。

二つ目に関しては、現在コネヒトではテックビジョンとしてLet's Goというものを掲げております。

tech-vision.connehito.com

新たな武器としてGoを選定したわけですが、せっかくならGoの強みである並行処理についてちゃんと理解したい、業務で使えるようになりたい、という思いもあり、この本を選定しました。

どういう形式でやったのか

頻度としては隔週くらいで行いました。事前に読む章を決めて読んでおき、学びになったこと、疑問に思ったことを付箋に書いて各付箋について議論していきます。

輪読会の様子
わからない箇所について議論をしたり、後述するようにコードを実際に動かしてみて理解を深めたりしました。

こうやって議論をすることで一人だけでやるよりも理解を深められるのが輪読会のいいところですね!

輪読会を進める上での工夫

今回輪読した本は丁寧に書いてありつつも、理解に至るには難易度も高く進めるのも一苦労でした。そのため輪読会を進める上で工夫した点を紹介できればと思います。

1. 写経当番を決めて、実際に手を動かす

「Go言語による並行処理」は3章くらいからコード表記が多くなり、実践的な内容になってきます。並行処理は実際に動かしてみたり、コードを変えてみたりして挙動を確かめないと全然動きが想像できなかったりするので、3章以降では各パートで写経当番を決めて、各自写経してくることにしました。

写経したコードをコミットできるリポジトリを用意し、自分の担当した分のコードをコミットしていきます。

これは実際にコードを動かしながら学べるし、負担を分担できるため今回のケースとしてはかなり有用だったかなと思います。みんなやってるんだから自分もやらなきゃって気持ちになりますしね!

やはりある程度イメージしづらい内容だとコードで語る方が理解しやすくなる側面があるかと思うので、難易度が高かったりイメージしづらい内容の輪読会の場合、この方法はおすすめです。

2. 学んだことを実践的にアウトプットしてみる勉強会でワイワイ

輪読会を完走し、実際に写経しながら進めていたとはいえ、やはり 知っている使える には大きなギャップがあります。なので、テーマはなんでもいいから、実際に並行処理を書いてみてみんなでワイワイする勉強会を行おうという話になりました。

せっかくならガッツリやって、最後みんなで飲みにでも行こうという話になったので、有志で日曜日に会社に集まって、各々がテーマを決めて並行処理を書いてみる会を行いました。

テーマは以下のようなものがありました

  • PHPのEnumからGoのEnumに変換するConverterを並行処理を使って作成してみる
  • 並行処理を使って、複数のAPIレスポンスをまとめる処理を書いてみる
  • 並列処理のアルゴリズムを自分で作ってみる
  • プッシュ通知処理をゴルーチンを使って実装してみる

それぞれテーマが興味深く、かつ具体性もあったので取り掛かりやすかったのかなとも思います!

まずはみんなでワイワイランチ!

ランチの様子
お腹を満たしたら、それぞれもくもく作業!

2時間やったら中間進捗発表して、さらに2時間やって最終発表といった感じでメリハリをつけてもくもくすることができました。

ちょっとした発表時間があると、進めようという程よい緊張感もあり、集中して取り組めたと思います。 発表は実際にデモをしてみたり、ホワイトボードの前で解説したりとみんな進捗でてて最高でした!(写真撮り忘れた…)

勉強会も終わってパシャリ
ちょっとしたプチ開発合宿みたいな感じで楽しかったです!

最近はオフラインの場も増えてきたので、こういった機会をたまに設けると学びが加速しそうですね!

最後に

今回は「Go言語による並行処理」本の輪読会を有意義にするために行ったことを紹介させていただきました。

この輪読会は途中で日程が合わずに継続が危ぶまれる時もありましたが、なんとか完走することができました(実際開始から終了までのリードタイムとしては10ヶ月ほどかかっています…笑)。

難しい部分もありつつ、工夫もしながら完走できたのはすごい自信にも繋がったかなと思うので、また10月からも新しい本の輪読会に取り組む予定です!

みなさんも一人で読むと心が折れそうになる本は仲間を見つけてワイワイしながら取り組んでみるのはどうでしょうか?🙌

みんなで打ち上げ!
お疲れ様でした!!

DroidKaigi 2023 Day3 参加レポート

こんにちは!Androidエンジニアの関根です。

2023/09/14から3日間、DroidKaigi 2023が開催されています。 弊社でもスポンサーをさせていただきオフライン参加しているので、僭越ながらレポートをします。 少しでもAndroid開発の盛り上がりに貢献できたら嬉しく思います。

3日目は、これまでと趣向を変えてコミュニケーションを中心にしたコンテンツが行われました。 一覧にすると以下の通りです。

  • Codelabs / コードラボ -
  • Career Panel Discussion / キャリア・パネルディスカッション -
  • Career Advice Sessions / キャリア相談会 -
  • Meetups on different topics / 特定のトピックについてのミートアップ -

わたしは、コードラボとキャリア・パネルディスカッションに参加しましたので、2つのコンテンツを中心に感想を書かせていただきます。 補足ですが、バリスタの方が作るカフェラテの提供があり、リラックスして参加できました。

※Day1、Day2の様子と、バリスタの方のお店のWebサイトをAppendixに載せているので、そちらもお読みください。

キャリアパネルディスカッション

さまざまなバックボーンを持つパネリスト4名の、パネルディスカッションです。

2023.droidkaigi.jp

  • キャリアプランを立てるかどうか
  • キャリア形成をする上での行動や心構え
  • プライベートとキャリア

など、日常的には聞けないテーマが多く取り上げられました。slidoを使った質疑応答もあり、技術とは異なる観点で、コミュニティを支えるコンテンツだったと感じています。内容に共感しながら、業界を支えている方々の姿勢に触れ、背筋が伸びる思いがしました。

Codelab

Codelabsの中から選定された、3つのコースが用意されており、完了するとプレゼントがもらえるというコンテンツです*1

2023.droidkaigi.jp

わたしは「Jetpack Composeの基本」というコースを選んで取り組みました。

developer.android.com

じっくりとJetpackComposeに触れる機会を作れていなかったので、純粋に良い機会になりましたし、ランチタイムから同席になった方々と、お互いの作業内容の共有をしながら取り組めました。JetpackComposeやマルチモジュールの導入状況など、業務上での裏話を情報交換をできたので、貴重な時間になりました。

参加したコンテンツ以外にも、魅力的なコンテンツがありましたので、紹介しておきます。

Meetup

いくつかのテーブルに分かれ、特定のテーマを元に交流するコンテンツです。

2023.droidkaigi.jp

わたしはCodelabsに集中していたため、時間切れとなり、参加できませんでしたが、終始人が集まり交流が行われていました。オンライン主流の日常では、得難い機会であり、オフラインならではのコンテンツだと感じました。

キャリア相談会

パネルディスカッションのパネリストの方々に、少人数で、キャリア相談ができるコンテンツです。

2023.droidkaigi.jp

キャリアの悩みは、所属会社だけでは解決できないケースもあると思うので、コミュニティで相談できることは心強いと感じました。満席のアナウンスもあり、盛況だったようです。

オフィスツアー

スカラーシッププログラムの参加者向けのコンテンツで、DroidKaigiに協賛している企業のオフィスを見学するツアーです。

medium.com

コミュニティの一員として、学生を支援していることに、強く意義を感じます。オフィスツアーから戻った学生の方々を、拍手で迎えることもAndroidコミュニティの温かさを実感しました。

まとめ

スピーカーの皆様、スタッフの皆様、そして参加された皆様、3日間お疲れ様でした!

1日目、2日目では、技術的な見識を深く得られましたし、企業ブースではさまざまな業種の方々から、貴重な開発事例をお聞きできました。3日目には、Androidコミュニティの方々と、情報交換やもくもくコーディングできたので、有意義に過ごさせていただきました。

スポンサー一覧を見ると、モバイルアプリ以外への広がりを感じますし、キャリアについて相談したり、スカラーシップでの学生支援など、Androidコミュニティを支えるイベントに、より一層進化していることを実感しております。

DroidKaigiを支えてくれている皆様に、心から感謝です!また来年お会いしましょう!

Appendix

tech.connehito.com

tech.connehito.com

alphabetticafe.com

*1:完了しましたが、プレゼントをもらい忘れてしまいました・・・

DroidKaigi 2023 Day2 参加レポート

こんにちは!Androidエンジニアの関根です。

2023/09/14から3日間、DroidKaigi 2023が開催されています。 弊社でもスポンサーをさせていただきオフライン参加しているので、僭越ながらレポートをします。

本日は、2日目にわたしが聴講したセッションを紹介します。後日アーカイブ動画公開後に更新していく予定です。 少しでもAndroid開発の盛り上がりに貢献できたら嬉しく思います。

ビジネス向けアプリを開発するときに知っておくべきAndroid Enterpriseの世界

最初は、Yusaku Tanakaさんによるモバイルデバイス管理(MDM)を利用した開発についてのセッションです。

MDMに関して、システム構成や配布方法、実装方法など始まりから終わりまでを理解できる内容になっていました。 相対的に事例を多く聞ける事例ではないので、初めての知見ばかりで、新鮮に聞かせていただきました。 すぐにでもMDMを利用して社内ツールの提供をすることもできそうに感じています。

speakerdeck.com

ビジネス向けアプリの開発を予定している方、社内用のアプリを作りたい方におすすめしたいです。

Flutterにおけるアプリ内課金実装 -Android/iOS 完全なる統一-

弊社の中島さんのFlutterを用いたアプリ内課金の定期購入についてのセッションです。 Androidの関数と対比しながら解説して頂き、Flutter未経験の方にも理解しやすい内容でした。 終始落ち着いて発表されていて、改めて一緒に働けることを心強く思いました。中島さん登壇お疲れ様でした!

speakerdeck.com

Flutterで定期購入機能の導入を検討中、あるいはアプリ内課金機能があるアプリのFlutterへのリプレイス を検討中の方におすすめしたいです。

Androidアプリの良いユニットテストを考える

Nozomi Takumaさんによる、Androidアプリ開発でのユニットテストに関するセッションです。

良いユニットテストの定義を提示して頂いた上で、テストの種類ごとにおける実行速度の対比や、 テストダブル利用時の考慮すべきトレードオフ、テスト実装時の手法など、網羅的でわかりやすい内容でした。テストコードはプロダクトコードとは違う知識や思考が必要だと思うので、解説いただいた内容をもとにチームでの議論もしやすくなると感じました。

speakerdeck.com

ユニットテストを書いていない、もしくは書いているがもう一歩踏み込んで取り組みたい方におすすめです。

レイヤードアーキテクチャーでの例外との向き合い方

Yukihiro MoriさんによるMVVMをベースにした例外の扱いに関する発表です

レイヤーごとの例外の対処方法や、ユーザーへの例外の伝え方のパターンと実装方法など、開発上の関心を総合的に解説した内容です。 実装方法とアーキテクチャ図を合わせて紹介していただき、理解しやすい内容で、実装内容を具体度高く聞けたので、弊社にも取り入れていきたいと思います。

speakerdeck.com

なんとなく例外を扱っている、例外の種類が多くハンドリングに困っているという方にお勧めしたいです

できる!アクセシビリティ向上

Ogura Yuriさんによるアプリのアクセシビリティに関するセッションです。

そもそものアクセシビリティの定義や、TalkBackとスイッチアクセスの機能を、実際のデバイスで操作するデモから始まり、知識がない状態でも安心して聞けました。 デモの後は施策立案〜リリースまでのプロセスと、明日にでもできる容易な改善手法を解説いただいたので、導入時に参考にできることを多く知れたと思います。

speakerdeck.com

アクセシビリティに対して見識がない方にお勧めしたいです。

まとめ

スピーカーの皆様、スタッフの皆様、2日目もお疲れ様でした!

今日は企業ブースもいくつか回らせていただき、久しぶりにオフラインでの情報交換ができ、楽しませてもらいました。 今回は、NISSANさんの企業ブースでモビリティアプリを触らせていただくなど、スマートフォン以外を扱う企業参加も増えている印象で、Androidコミュニティのさまざまな広がりを感じました。 明日もよろしくお願いします!

最後にコネヒトでは一緒に働く仲間を募集しています! 興味持っていただけた方は気軽にご連絡ください!

www.wantedly.com

CakePHP4.3から非推奨になったFixtureのテーブル定義の問題を解決しました

こんにちは。

今回はCakePHPのバージョン4.3から非推奨機能に追加されたTestFixture の対応をしたのでバックエンドエンジニアの共同制作でブログに書いてみました。

冒頭〜導入手順5までは高橋で、手順6以降は西中が書いております。

以前PHP8.1にアップデートした際にCakePHPのバージョンも4.2から4.3にアップデートしました。

その時のブログはこちら↓

https://tech.connehito.com/entry/2023/03/31/195819

この時からテストを実行する度に以下のような警告が出るようになりました。

Deprecated Error: You are using the listener based PHPUnit integration. This fixture system is deprecated, and we recommend you upgrade to the extension based PHPUnit integration. See https://book.cakephp.org/4/en/appendices/fixture-upgrade.html
/var/www/html/server/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php, line: 79

対象のリポジトリは元々CakePHP3系で作られていたので、テストに使うテーブル定義はFixtureクラスの中に定義されていました。 ですが、CakePHPのマイグレーションを利用しておらず、いわゆるマスタとなるようなテーブル定義は別のリポジトリで管理されています。

そのため、テーブル定義が二つの場所にある二重管理状態になっていました。

CakePHPの公式ドキュメントではCakePHPのマイグレーションを使った方法については詳細に記述されているのですが、DDLファイルを使った方法は簡潔に記述しかありません。ネットで探してもあまり見つからなかったので、同じように躓いた人の役に立てれたら嬉しいなと思っています。


目次


前提

対応方法をご紹介する前に前提条件をお伝えします。

  • PHP: 8.1
    • CakePHP: 4.3
    • PHPUnit: 9.6.5
  • AWS
    • ECS
    • Amazon Aurora MySQL v2(MySQL5.7)
  • DBのスキーマ管理はアプリケーションコードとは別リポジトリ
    • マイグレーションツールは Ridgepole
  • GitHub Actions

導入手順

mysqldumpによって取得したDDLファイルからテストのテーブル定義を使うように変更しました。

1. アプリケーションリポジトリにDDLファイルを/config/schema配下に置く

DDLファイルは以下のSQLコマンドで生成しました。

mysqldump -u[ユーザー] -h[ホスト] -P[ポート] -p[パスワード] --no-data --skip-column-statistics --no-create-db [データベース名] > base_test.sql

FixtureやFactoryでレコードを追加する際に、AUTO_INCREMENTの値が1ではない場合主キーが重複してしまうため、awkコマンドを使って初期値を変換しました。

awk '{ gsub(/AUTO_INCREMENT=[0-9]+/, "AUTO_INCREMENT=1"); print }' "base_test.sql" > "test.sql"

2. phpunit.xmlを変更する

phpunit.xml から <listeners> ブロックを削除し、以下の内容を phpunit.xml に追加しました。

ref: https://book.cakephp.org/4/en/appendices/fixture-upgrade.html

<extensions>
    <extension class="\Cake**\T**estSuite\Fixture\PHPUnitExtension" />
</extensions>

3. tests/boostrap.phpに追記する

1で置いたDDLファイルを使用してテーブル定義を取得します。

ref: https://book.cakephp.org/4/ja/development/testing.html#creating-test-database-schema

$testSqlFile = dirname(__DIR__) . '/config/schema/test.sql';
(new SchemaLoader())->loadSqlFiles($testSqlFile, 'test');

4. Fixtureクラスからテーブル定義を削除する

実施したリポジトリは元々CakePHP3系で作られていたので、テストに使うテーブル定義はFixtureの中に定義されていました。 段階的なアップグレードを行っていたため、Fixtuer内にまだテーブル定義は残っている状態です。 そのため、まずは各Fixtureクラスからごっそりテーブル定義ブロックを削除していきます。

<?php
namespace App\Test\Fixture;

use Cake\TestSuite\Fixture\TestFixture;

/**
 * ArticlesFixture
 *
 */
class ArticlesFixture extends TestFixture
{
    /**
     * Table name
     *
     * @var string
     */
    public $table = 'articles';
    public $connection = 'test_hoge';

-    /**
-     * Fields
-     *
-     * @var array
-     */
-    // @codingStandardsIgnoreStart
-    public $fields = [
-        'id' => ['type' => 'integer', 'length' => 11, 'unsigned' => true, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null],
-        'title' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => '', 'collate' => 'utf8mb4_general_ci', 'comment' => '', 'precision' => null, 'fixed' => null],
-        'description' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => '', 'collate' => 'utf8mb4_general_ci', 'comment' => '', 'precision' => null, 'fixed' => null],
-        'status' => ['type' => 'integer', 'length' => 3, 'unsigned' => false, 'null' => false, 'default' => '0', 'comment' => '', 'precision' => null, 'autoIncrement' => null],
-        'created' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
-        'modified' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
-        '_indexes' => [
-            'index_status' => ['type' => 'index', 'columns' => ['status'], 'length' => []],
-        ],
-        '_constraints' => [
-            'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
-        ],
-        '_options' => [
-            'engine' => 'InnoDB',
-            'collation' => 'utf8_general_ci',
-        ],
-    ];

    /**
     * Records
     *
     * @var array
     */
    public $records = [
        ︙
    ];
}

また、$recordsの中に配列がある場合、今まで$fieldsのtype指定していたのでこのように独自でjson形式にしなければなりません。

$records = [
    [
        'id' => 1,
        'urls' => [
            0 => [
          'type' => 1,
        'url' => 'https://www.example1.co.jp',
      ],
      1 => [
          'type' => 2,
        'url' => 'https://www.example2.co.jp',
      ],
        ],
    ],
    [
        'id' => 2,
        'urls' => [
            0 => [
          'type' => 1,
        'url' => 'https://www.example1.co.jp',
      ],
      1 => [
          'type' => 2,
        'url' => 'https://www.example2.co.jp',
      ],
        ],
    ],
];
// テストで使用するのにjson形式に変換する
foreach ($records as $seq => $record) {
    if (is_array($record['urls'])) {
       $records[$seq]['urls'] = json_encode($record['urls']);
    }
}
$this->records = $records;

以下のように直接json形式にするでも大丈夫です。

$records = [
    [
        'id' => 1,
        'urls' => '[{"type": 1,"url": "https://www.example1.co.jp"},{"type": 2,"url": "https://www.example2.co.jp"}',
    ]
    [
        'id' => 2,
        'urls' => '[{"type": 1,"url": "https://www.example1.co.jp"},{"type": 2,"url": "https://www.example2.co.jp"}',
    ]
];

5. ユニットテストを回してみる

今回実施したリポジトリではDockerコンテナを利用しているので、コンテナの中に入ってPHPUnitを回すようにしていました。

元々、composer.json 内でPHPUnitを定義しているため、composer から呼び出せるようになっています。

    "scripts": {
        "post-install-cmd": [
            "App\\Console\\Installer::postInstall"
        ],
        "post-create-project-cmd": "App\\Console\\Installer::postInstall",
        ︙
        "test": "phpunit --colors=always"
    },

テストコードの件数が少ない場合は気にしなくて良いのですが、テスト件数が多くテスト実行後に結果を見ようとするとターミナル上で見切れてしまうことが多々あったので実行結果はテキストファイルに書き出すようにしていました。 その時、composerのタイムアウトが発生してしまうことがあるので予め環境変数を上書きしてタイムアウトが起きないように設定した上でテストを実行し、出力したファイルを見比べながらテストコードを実情にあった形で直していきます。

$ export COMPOSER_PROCESS_TIMEOUT=0
$ composer test > text.log

CakePHPのFixtureはどうやら主キーやユニークキーが重複してしまうFixtureのレコードも許してしまい、レコード重複エラーが発生してしまうという事象が頻発してしまいました。

このリポジトリではFixture Factoriesを利用しているので、重複が出ないようにテストケース内でFactoryクラスを使ってレコードを作成するように書き換えることで既存のテストに影響が出ないように修正を行いました。

ref: https://tech.connehito.com/entry/2022/07/22/100000

これで一応Fixtureの警告は消えましたが、これだとテーブル定義を変更した場合にアプリケーションリポジトリのDDLファイルを手動で更新しなくてはなりません。

テーブル定義を管理しているリポジトリに変更があった場合に自動でAWSのS3にDDLファイルをアップロードし、アプリケーションリポジトリでそのDDLファイルをダウンロードしてくるという仕組みを作ることになりました。

6. 自動化

6.1. DDLファイルをS3のバケットにアップロードする

テーブル定義変更の度にリポジトリ内のDDLファイルの変更を行わないといけないだけでなく、弊社の場合、複数のリポジトリで同じDBを参照しているということが多々あるため、スキーマ定義の変更の度に各リポジトリ内のDDLファイルを書き換えるのは手間だという問題もありました。

今回の本題であるスキーマ定義のDDLファイルをダウンロードする仕組みは、正にこの問題を解決する手段として考えたものでした。

弊社では前述の通り、 Ridgepole を使ってスキーマ定義を管理しているのでスキーマ定義の変更が発生したタイミングでDDLファイルを作成し、S3のバケットにアップロードする仕組みを作成しました。

簡単に流れを説明するとこのようになっています。

  1. 開発者がテーブル定義を変更し、スキーマ定義管理用のリポジトリに push します。
  2. スキーマ定義管理用のリポジトリに設定されているGitHub ActionsがDDL反映処理の実行のためのECSを起動します。 この一連の処理については過去にブログで投稿しているので、そちらを参考にしてください。 refs: https://tech.connehito.com/entry/2019/10/08/165500
  3. 前述のDDL反映を行います。
  4. ECS内でmysqldumpコマンドを実行します。
  5. 各スキーマのDDLファイルを取得し、ローカルに保存します。 この記事の冒頭で紹介させていただいたawkコマンドを使ってDDLファイルの一部を書き換える処理も行っています。
  6. S3にファイルをアップロードします。

S3へのアップロードフロー

6.2. S3のバケットからDDLファイルをダウンロードする

「S3のバケットからDDLファイルをダウンロードする」ということは決まったのですが、「じゃあどのタイミングでDDLファイルをダウンロードするのが適切なんだろう?」という課題が出てきました。

そこで考えたのがこの3つのタイミングでした。

  • テスト実行時にDDLファイルをダウンロードする
  • DockerビルドのタイミングでDDLファイルをダウンロードする
  • Dockerコンテナを立ち上げたタイミングでDDLファイルをダウンロードする

テスト実行時にDDLファイルをダウンロードする

最初に思いついたのはこの方法です。 毎回テストの度に最新のDDLファイルをダウンロードすれば最新のスキーマ定義でテストが実行できるというメリットはあります。

ですが、

  • 通信エラーでS3のバケットからDDLファイルをダウンロードできなかった場合にテストが動かなくなってしまう
  • スキーマ定義の変更は頻繁に行われるわけではないので、毎回DDLファイルをダウンロードするのは無駄

という問題があったため、却下となりました。

DockerビルドのタイミングでDDLファイルをダウンロードする

Dockerコンテナをビルドするのはそんなに頻繁ではないので一見良さそうに思えます。

ですが、

  • テストコードは本番環境には不要なため、不要なファイルがDockerイメージに含まれてしまう
  • DDLファイルを含める分、Dockerイメージが大きくなってしまう
  • 本番環境には不要なファイルをダウンロードするために、DockerイメージをビルドするためのCIの設定にAWSのキーを設定しないといけない

という問題があったため、こちらも却下となりました。

Dockerコンテナを立ち上げたタイミングでDDLファイルをダウンロードする

ローカルでDockerコンテナを立ち上げる際に必ず実行するシェルファイルがあるため、そちらにDDLファイルをダウンロードする処理を追加しました。

ローカルのDockerコンテナは何かライブラリの更新・変更のような大きな変更があったらビルドし直すということもあり、いまの開発スタイルではこのタイミングが適切だろうということになり、このタイミングでDDLファイルをダウンロードすることになりました。

また、「なんかスキーマ定義合ってないかも?」となった時でも「まず最初にDockerコンテナを立ち上げ直してみよう」という手段が取れるので、問題解決の第一歩の負担が重くないことも良さそうだねという話になりました。

実際のシェルの処理とは異なりますが、以下のように aws-cli の aws s3 cp コマンドでS3のバケットからダウンロードし、 tests/bootstrap.php で読み込めるようにしました。

# copy table schema files from s3
aws s3 cp s3://[DDLファイルのあるバケット]/メインで使うDB.sql /tmp/ && \
aws s3 cp s3://[DDLファイルのあるバケット]/連携して使うDB1.sql /tmp/ && \
aws s3 cp s3://[DDLファイルのあるバケット]/連携して使うDB2.sql /tmp/ && \
aws s3 cp s3://[DDLファイルのあるバケット]/連携して使うDB3.sql /tmp/

cp /tmp/*.sql /var/www/html/config/schema/

弊社のリポジトリはローカルのディレクトリを /var/www/html ディレクトリにマウントする形にしているので、マウントの対象外の /tmp ディレクトリにDDLファイルをダウンロードし、実際に読み込むファイルはマウント後のディレクトリにコピーしたものを利用する形にしています。

// Load one or more SQL files.
$ddlFiles = [
    'test' => 'メインで使うDB.sql',
    'test_sub1' => '連携して使うDB1.sql',
    'test_sub2' => '連携して使うDB2.sql',
    'test_sub3' => '連携して使うDB3.sql',
];

$schemaLoader = new \Cake\TestSuite\Fixture\SchemaLoader();
foreach ($ddlFiles as $schema => $file) {
$fullPath = dirname(__DIR__) . '/config/schema/' . $file;
    if (!file_exists($fullPath)) {
        // ファイルが存在しない場合は /tmp ディレクトリからコピーしてくる
        copy('/tmp/' . $file, $fullPath);
    }
    $schemaLoader->loadSqlFiles($fullPath, $schema);
}

まとめ

スキーマ定義をダウンロードできる仕組みを入れ、単体テストで利用できるようにしました。

これにより、最新のスキーマ定義でテストを実行できるようになり、テストコードとデータベース構造の整合性を維持できるようになりました。また、スキーマ定義の変更に柔軟に対応できるため、開発のスピードアップにもつながっていくことが期待できます。

他のリポジトリでも同じDBを参照しているため、横展開していくことで開発効率・開発者体験の向上が見込めそうです!

コネヒトでは一緒に働く仲間を募集しています!

そして興味持っていただけた方は気軽にご連絡ください!

https://www.wantedly.com/companies/connehito/projects