コネヒト開発者ブログ

コネヒト開発者ブログ

ユーザーとの距離を近くするために、ユーザーインタビュー実施のプロセスを自動化し、リードタイムを減らした話

こんにちは。今年の 4 月からプロダクトマネージャーになった@TOCです。 私は 3 月まではエンジニアとしてプロダクトマネージャーと一緒にユーザーとの距離を近くするための取り組みをいくつか行っていました。 本記事ではその取り組みの一つとして週一ユーザーインタビューの実現のために行ったインタビュー実施までのプロセス改善についてご紹介します。

(現在はプロダクトマネージャーですが、この記事ではエンジニアとしての帽子を被って記載します)

はじめに

プロダクト開発において、ユーザーの声を直接聞くことはとても大事な機会です。開発チームが直接ユーザーと対話する機会を増やすことは、リアルなフィードバックを得る上で非常に効果的だと思います。

コネヒトでは以前から定期的にユーザーインタビューを行っていましたが、実施の頻度としては 2~3 ヶ月に 1 回行うような状態でした。この頻度では、ユーザーのニーズの変化に迅速に対応することは難しく、また新たなアイデアを試す機会も限られてしまうように感じていました。

そこで私たちは、直近のゴールとして「週に 1 回はユーザーと話せる状態を実現する」ことを目指しました。この目標を達成する上で、どこがハードルになっているのかを プロダクトマネージャー(以下 PM) と話してみると下記のような課題感がありました。

  1. インタビュー企画から実施までの流れが属人化しており、1 人の PM しかできない
  2. 日程調整など実施プロセスを手動で行っており、実施までの運用コストがかかる

ユーザーとの距離を近くしたい、そのためにユーザーと頻度高く会話できる状態を作りたいのに、リードタイムが長いが故に実現できていない状態となっていました。この状態を解決するために、プロセスを効率化する必要がありました。

まずは業務フローを洗い出し、体験する

プロセス改善をするためには、まずその実態を知るのが手っ取り早いと思っています。なので、実際のフローを自分で行ってみて「どこに課題感があるのか」、「改善できるポイントはどこにあるのか」を特定するところから始めました。

実際に行ってみると、かなり手間のかかる作業であることがわかりました。 その中でも特に改善できそうなポイントが下記でした。

  • メールを候補者ひとりひとりに手動送信している。
  • メールの送信文をコピペして手動で書き換えている。
  • インタビュー予定が入ったことが PM しかわからない。入ったことを PM が Slack で周知する。

また、当時利用していた日程調整サービスもサービス終了のタイミングを迎えたところでした。 以上を踏まえて以下の改善を行うことにしました。

  1. 日程調整を自動化し、ミスが極力起きない運用にする
  2. より使いやすい日程調整ツールを選択する
  3. インタビュー予約が入った時に全体に周知し、見える化する

以下でどのような改善を行ったかを説明します。

プロセスの改善を行う

プロセス改善のために行った取り組みを 3 つ紹介します。

1. GAS を使ってメール送付を自動化

まず、 1 人ずつ手動で送っていたメール送付を、Google App Script(以下 GAS) を利用して一括送信できるようにしました。 GAS を使ったメール送信の方法は世の中にたくさん情報が出ているので詳細は省きますが、スプレッドシート上にメールアドレス、名前、担当者名を記入し、メール送信ボタンを押すと各項目の値が挿入されたメールが一括送信されるものを作成しました。

ほとんど他サイトを参考にして、ざっと作ったものになりますがコード内容を記載しておきます。

これによりメール送信の工数削減だけでなく、コピペをする際のミス防止にも繋がります。 もちろんこれで 100%ミスを防げるかというとそうではないですが、必ずテスト送信を行ってから本送信を行う、慣れてない人がやる時はモブ作業で画面を映しながら行うなど、極力ミスが起こりにくいオペレーションを行っています。

2. 日程調整は専用ツールに任せる

元々日程調整ツールは利用していましたが、利用サービスの終了に伴い、日程調整ツールも見直しを行うことにしました。日程調整ツールも様々なツールが世にありますが、最終的には TimeRex というサービスを選択し、現在も利用しています。

かなり具体的な方法にはなりますが、どういった形で TimeRex を利用しているのかの大枠をご紹介いたします。

  1. ユーザーインタビュー用のカレンダーアカウントを作成する
  2. TimeRex のアカウントを作成し、連携カレンダーを 1.で作成したカレンダーにする
  3. TimeRex でインタビュー用のカレンダーを作成する
  4. 日程調整用の URL から日程調整を行う。ユーザーインタビュー用のカレンダーに予定が入れば OK

もし、上記を行う場合、TimeRex のカレンダー設定は状況に合わせて変えていただければと思います。 一つちょっとした工夫点でいうと、弊社では固定のユーザーインタビュー時間を決めて設定をしています。例えば、火曜 10:00~11:00、水曜 17:00~18:00 など決めて、インタビュアーを担当する社員の予定はブロックしておきます。TimeRex でも日程候補の日時は細かく時間単位で設定できるので、固定のブロック時間に合わせて予約できる時間を設定しています。この固定枠を決めておく方法はタイミーさんの事例を参考にさせていただきました。

speakerdeck.com

上記によって、1 週間の中で固定のユーザーインタビューの時間を確保して、インタビューが入れば対応できる状態を作りました。

3. 日程が決まったら Slack に自動で通知をし、同席者を決める

最後にインタビュー予定が入ったら通知が来るようにすれば、インタビューのアサインなどもスムーズに行えそうです。 これは TimeRex が Zapier 連携できるので、その機能を利用しました。 設定は至ってシンプルで、TimeRex でイベント作成されたら Slack 通知する、というワークフローを組んでいます。

Zapier のワークフロー

イベントが作成された場合は下記のような通知がされます。

イベント作成通知

また、キャンセルをした際にも通知できます。

キャンセルの通知

このように通知をすることでインタビュー予定にすぐ気づけるようになります。 また、この投稿を利用することでインタビュー同席者の募集や立候補もしやすくなりました。

インタビュー同席者の募集

Zapier 連携を利用することで簡単に自動化できますので、もしよければ試してみてください。

どう変わったか

3 つの取り組みを改めてまとめると下記のようになりました。

改善のまとめ

どの取り組みも比較的ライトに実現ができました。これにより募集からユーザーインタビューまでのリードタイムが削減されました。 例えばメールを送る作業は、 1 人に対して確認も含め 3 分ほど使っていたのが、人数が増えても全員に対して 5 分ほどでメールを送れるようになりました。 何より、作業が簡略化されたことにより、他の PM への展開もしやすくなったので、今後ユーザーインタビュー文化を広げる足がかりになったかと思います。

歩み寄ることの大切さ

この取り組みは同じチームの PM と会話しながら実現をしました。もともと結構面倒な作業をしていたのは感じていたのですが、実際に自分がやってみると「これは面倒だね。こうしてみたらどう?」という提案をすることができました。

エンジニアは面倒なことを効率化することに関して、ある程度の知識があるし、効率化が好きな人は結構多いんじゃないかな、と思ってます(自分もそう)。 そんな自分が他の職種の人の作業を見たり、話したりすると「言ってくれれば、この作業もっと簡単にできたのに」と思うことが結構あったりします。 「時間取っちゃって申し訳ないから」という話を聞くこともあるのですが、「面倒なことに時間使う方がもったいないから全然声かけてくれて大丈夫!」という気持ちになります。

でもこれはエンジニアからも歩み寄ることも大事なんだろうな、と。

僕もやりがちですが、改善したいことを聞くと「そもそも何でこれをやるの?」「これってどういう意味があるの?」って聞いちゃいがちです(言い回しはもっと柔らかくしますが)。言われた方は「え、なんか困らせてるかな」という印象を受けるかもしれないですが、僕としては「もっとよくできないか、もっといい解決策はないか」を考えて聞いてることが多いです。でもそれは言わないとなかなか伝わらないことでもあるので、僕(エンジニア)から伝えていくことが大事なのだと思っています。

「困ったな、この作業面倒だな」と感じてる人は

  • 具体の解決方法はなくていい。「〇〇したい」レベルでいいので、声をかけてみよう。
    • 「さてさて、改善しちゃおうかな」と腕まくりしてくれる人がきっといる。

「困ったな、この作業面倒だな」と感じてる人を見た人は

  • 一緒に体験してみる。
  • 「こうしたら解決できるよ〜」を作っちゃって、動くもので見せちゃう。

こういったことをすると、「困ったときに解決できるかも」という雰囲気を作るきっかけになるかもしれません。

コネヒト経営の生成AI活用レベル向上の一環でGPTsとZapierアプリをつくりました

こんにちは。GWはずっと真夜中でいいのに。のライブをたのしんだキャプテンの@itoshoです。ちなみにずとまよで一番好きな曲は「お勉強しといてよ」です。

はじめに

コネヒトではTech Visionのもとテクノロジーやエンジニアリングの活用を全社的に推進しています。その中で昨年から「Run with Tech」(以下、ランテク)という取り組みの一環で生成AIの活用に力を入れています。*1

こういった新しい取り組みはボトムアップの"やっていき"も大切ですが、一方で経営の"のっていき"も重要です。そこで経営のコミットメントを引き上げるためにランテクチームから経営チームへ生成AIに関する"お勉強しといてよ"的な宿題がGWに課されました。

この記事では宿題のアウトプットのひとつとして、GPTsとiPaaSであるZapierのChatGPT利用アプリケーション(ワークフロー)をつくったのでその紹介ができればと思います。

なお、最初に断っておくと技術的な難易度が高いものではありませんし、既に先行事例がある取り組みです。しかし、やはり自分で手を動かすのと本や記事を読むだけでは理解の解像度に雲泥の差があると思います。また、似たような取り組みでも事例を積み重ねることが技術コミュニティを盛り上げることに繋がると僕は信じているので誰かの何かのヒントになれば幸いです。

GPTs編

まず、GPTsの紹介をします。

何をつくったか?

一言で言うと「Notionの検索結果からいい感じに要約や考察をくれるGPTs」です。ちなみに名前はNotion探偵と名付けました。具体的にはママリに関する調査系ドキュメントがまとまったNotionのデータベースを検索し、そのドキュメントのリンクや概要を教えてくれます。そして、その情報をもとに考察やインサイトを出力することに特化させています。社内情報が含まれるのでモザイク多めですが、こんな感じで出力されます。

Notion探偵の出力例
Notion探偵の出力例

何故つくったか?

コネヒトのNotionにはたくさんのナレッジが蓄積されているのですが、全てのドキュメントに目を通すのは現実的ではありません。しかし、経営として良質な意思決定をしたり、HiPPOと呼ばれる的はずれな意見を回避したりするためにはできる限り多くのインプットが必要です。*2

そこでそのインプットを効率化するためにこのNotion探偵をつくりました。また、ただ単に情報を出力するだけではなく、質の高いインサイトも出力してもらうことで知の高速道路的な役割も担ってもらいたいと考えました。

どうやってつくったか?

凝ったことはしておらず一般的なGPTsのつくり方をしています。凝ったことをしなくてもつくれるのがGPTsの良いところですね。GPTsのつくり方やNotion APIの連携方法は以下の記事をたくさん参考にさせていただきました(感謝)。

zenn.dev

zenn.dev

上記の記事にも記載されている通りNotion APIは独自のリクエストヘッダーの設定が必要なのですが、GPTs Actions上ではAuthenticationヘッダーしか追加することができない*3のでリバースプロキシを立てる必要がありました。

リバースプロキシは使い慣れているAWS Lambdaを採用しようかなと思ったのですがユースケース的にCloudflare Workersで充分だと考え、お勉強も兼ねてCloudflare Workersを採用しました。

以下にコード片を晒しておきます。プロダクションコードではないのでゆるく書いている箇所もありますが、NYSL的な感じで自由に使ってください。

gist.github.com

ついでにGPTs Actions側で設定するOpen API Schemeも晒しておきます。xxx部分は各自のサービスに合わせて置き換えてください。なお、Notionのワークスペースから全てのデータベースから検索してもらうこともできるのですが、セキュリティ面を考慮し、機密性が低いデータベースのみ今回は対象としました。

gist.github.com

最後にInstructionsの一部(社内情報が含まれるので加筆修正および省略しています)も貼っておきます。こちらも参考記事のプロンプトをベースに作成したのですが、対象のデータベースが大きいのかResponseTooLargeErrorが頻繁に返ってきたのでその際のエラーハンドリングを追加しています。

このGPTs(以下、Notion探偵)は、Notionデータベースを自然言語のクエリで検索するために設計されています。
検索するNotionデータベースにはコネヒト社のドキュメント情報が格納されおり、それらを効率的に検索します。

Notion探偵はNotionのドキュメント情報から正確で文脈に沿った情報を提供することに重点を置いており、迅速で信頼性の高いデータを求める経営者にとって、非常に貴重なツールとなっています。

また、必要に応じて、クエリを明確にして検索結果の精度を高めたり、Notionからのレスポンスがエラーだった場合は自らそのエラー内容を確認し、クエリを修正して実行することができます。
特にレスポンスが"ResponseTooLargeError"だった場合は、page_sizeを自動で調整することでそのエラーを解消することができます。なお、どうしても上手くいかない場合は、page_size=20で実行します。

また、Notion探偵は情報を検索するだけではなく、与えられた情報から考察や解釈を出力することもできます。
考察や解釈を求められた際はその根拠として、参照元を出力します。

加えて、Notion探偵は図や表を駆使することで簡潔に情報を伝えることができ、時に鋭い問いを我々に投げかけることもあり、とにかく気が利くコミュニケーションをしてくれます。

また、どれくらいアウトプットに影響を与えているかはまだ分からないのですがRAGでNotion外の情報も持たせています。

つくってみての感想

雑な指示でもいい感じに情報を引っ張ってきて、まとめてくれるのでインプットの効率化という目的は一定果たせているなと感じています。ただ、まだプロンプトの精度が高くないこともあり、考察のアウトプットは伸び代があるので使いながらチューニングしていくぞというお気持ちです。

ちなみに、Notoin探偵はNotion AIを使えば同じようなことができると思います。現時点ではコネヒトはChatGPTに全振りしているので今回はGPTsを使いましたが、今後より生成AIの活用が進んでいったタイミングではNotion AIの全社導入も検討したいと考えています。

Zapier編

次にZapierアプリの紹介です。

何をつくったか?

LayerXの松本さんが以下の記事で紹介していた雑パーソナライズ情報収集ツールを自分用につくりました(こちらも感謝)。

tech.layerx.co.jp

やっていることはほとんど一緒なのですが、興味関心を動的に生成している点は特徴のひとつかなと思います。

「NewsToy」という名前をつけて、毎日TickTickでチェックしています。

NewsToyのアウトプット例
NewsToyのアウトプット例

何故つくったか?

いま僕はコネヒトの中長期の会社方針をアップデートする経営企画的な仕事に取り組んでいます。中長期の会社方針を立てるためにはペースレイヤリング的な発想で社会の変化や潮流を捉える必要があります。なお、ペースレイヤリングについてはMIMIGURIの安斎勇樹さんの記事が参考になります。

note.com

その中で良くも悪くもスタートアップやインターネット業界は海外の方が進んでいることが多いため、経営者としていち早く海外の潮流やトレンドを把握することが重要だと考えています。そして、把握すること以上に重要なのはそれを議論の土壌として自分たちなりの解釈や意味付けをすることで、会社や事業の方針に反映していくことです。

そこで海外のニュースを効率的に集め、そこから思考をゆさぶる問いを投げかけてくれるNewsToyをつくりました。

どうやってつくったか?

Zapierのワークフローを以下のように設定しています。

Zapierのワークフロー
Zapierのワークフロー

興味関心を動的に生成しているのは、2〜3の処理です。国内のニュース情報ははてなブックマークで収集しているので、自分のRSSフィードをスプレッドシートのIMPORTFEED関数を利用して取得しています。直近20件のブックマーク情報を取得するのでこれが自分の最新の興味関心と捉えることができます。

2の処理ではそのスプレッドシートからブックマークリストのタイトルを取得しています。そして、3の処理ではChatGPTにそのリストを渡し、興味関心リストを出力してもらっています。ちなみにプロンプトは以下です。

## 指示
私が直近ブックマークしたサイトのタイトルリストをもとに私の興味関心リストを作成してください。
リスト名はタイトルそのままではなく、一般化したカテゴリーにしてください。
例えば、PHPに興味関心があれば、リスト名はプログラミングやエンジニアリングといった名前にしてください。
また、リストは最大5個で、カンマ区切りで出力してください。
結果は必ずリストのみを出力してください。

## タイトルリスト
{スプレッドシートから取得したタイトルリスト}

また、7の処理で以下のようなプロンプトでGPT4-turboから問いを投げかけてもらっています。

## 指示
あなたは優秀な経営者の参謀です。
時間がない経営者のために取得した記事の内容を以下のフォーマットで要約してください。
要約は日本語で400文字程度でお願いします。

## フォーマット
タイトル: 記事のタイトルを書いてください
要約: 論点や要点を絞って要点してください
締め: あなたなりの鋭い問いを経営者に投げかけてください

## 取得した記事
{フィードアイテムのタイトル}
{フィードアイテムのコンテンツ}

つくってみての感想

これまで海外のニュースはPullで情報を取りに行くことが多かったのですが、NewsToyをつくってからは移動中や寝る前にサクッと情報収集できるようになったので既にかなり重宝しています。TickTickでToDo化したのも地味に効果的でした。また、やはり人は問いを投げかけられると思考をはじめる生き物なので思考のトレーニングにもなっています。

一方で実装面の課題もあります。例えば、興味関心はRSSフィードの更新ほど頻度高く変わるものではありません。ですのでZapier Tablesを使って、更新頻度を月一くらいにしたほうがエコです。また、もっと楽にアンテナを広げるためにそもそも登録するRSSフィードを動的に追加したり、過度なフィルターバブルを防ぐために逆に一定の確率で興味関心とは無関係なニュースを取得したりといったチューニングを今後やっていく予定です。

また、いまはまだ個人用途ですが、経営チームで使えるようにすればインプットを高いレベルで均質化できそうなので複数人での活用も検討しています。

さいごに

これまでもChatGPTをはじめとした生成AIサービスは使っていましたが、今回自分で手をちゃんと動かして学ぶことで生成AIの活用スキルが身体知になった感覚があります。また、NewsToyのようなサービスはこれまでtoC向けソフトウェアとして商業的な価値がありましたが、これぐらいのレベルであれば誰でもすぐにつくれてしまうので「Software is eating the world」から「Generative AI is eating software」の時代が本格的にやってきたと感じました。

それに対してソフトウェアエンジニアとしての危機感を覚えることは正直ゼロではありません。しかし、それよりも社会や人間の価値がドラスティックに変わっていく環境で仕事ができることはめちゃくちゃ恵まれているなと思いますし、予測できない未来にワクワクすることのほうが圧倒的に多いです。

そして、今回の取り組みを通じて改めて生成AIの活用スキルはメールを書くとかExcelが使えるとかと同じような当たり前のスキルになっていくと確信したので、コネヒトを生成AI活用のリーディングカンパニーにするぞ!という気持ちを新たにしました。

というわけで、そんなコネヒトで働くことに興味がある方はぜひお気軽にXなどでお気軽にお声がけください。

※このブログはChatGPTと一緒に執筆しました。また、OGP画像もCanvaの生成AI機能を利用して作成しています。

*1:ランテクの取り組みの詳細については近々誰か|д゚)チラッが書いてくれるでしょう!

*2:もちろん、不完全な情報の中で意思決定することも経営者の重要な資質ですが。

*3:と思っていたのですが、実はできるみたいです…!(調査中)

iOS/Android 開発Tips共有会 potatotips #86 を開催しました

こんにちは!iOSエンジニアのyoshitakaです。

先日コネヒト主催でiOS/Android 開発Tips共有会 potatotips #86 を開催しました!

potatotips.connpass.com

前回コネヒトで主催した際のブログがこちらです。

tech.connehito.com

今回はオンライン/オフラインのハイブリッド開催という形を取りました。

現地参加22名、オンライン視聴も常時30名以上とたくさんの方に参加していただき感謝です。

会場の雰囲気

登壇者のスライドを一部ご紹介させていただきます。(当日xでも紹介しておりました

speakerdeck.com

speakerdeck.com

speakerdeck.com

speakerdeck.com

speakerdeck.com

speakerdeck.com

speakerdeck.com

speakerdeck.com

speakerdeck.com

コネヒトからもiOSとAndroidでそれぞれ1名LTさせていただきました!

www.docswell.com

当日は概ねタイムライン通りに進めることができました。

登壇された方々にはスムーズな運営にご協力頂きありがとうございました!

LT発表後の懇親会も盛り上がっておりました🎉

引き続き勉強会を開催していきたいと思っておりますので、コラボして頂ける企業様がございましたらぜひお声がけ頂けると嬉しいです!

Draft Releaseデプロイフローを作成してみました

こんにちは。サーバーサイドエンジニアの岡田です

みなさんはどんなデプロイフローを採用していますでしょうか?今回は自分が取り組んでいたデプロイフローのちょっとした改善を皆さんに紹介したいと思います!

きっかけ

現状コネヒトではgdpを使ったデプロイ方法を採用しており、手元のコマンドからデプロイするのが標準になっています。

tech.connehito.com

元々自分はgit-pr-releaseを使ったワンクリックデプロイを以前の現場で使っており、手元でコマンドを打たなければいけないのが地味にめんどうだなあと感じていました。 そこでワンクリックでもっとラクしてデプロイしたい!と思ったのが今回のデプロイフローを考案したきっかけになります!

それ以外にも手元のコマンドからデプロイする事に感じていた課題感

  • 手元でコマンド打つ場合、打ち間違いやコマンド履歴等から意図せずデプロイしてしまう可能性
  • devデプロイ〜本番リリースの間(ビルドの7〜8分)地味にブランチを切り替え辛い(リリース前にmainブランチにcheckoutが必要なため)
  • 新しい人が入社してきた場合にgdp&hubのインストールと設定が必要になる
  • 本番デプロイ後にリリースノートを公開(gdp publish)するのを忘れてしまう

そしてこのデプロイフローが生まれた背景

当初はgit-pr-releaseを導入すれば簡単にワンクリックデプロイ出来るようになる!と考えていました。しかしいざgit-pr-releaseを導入しようと調べてみると、難しい背景がいくつか見つかりました。

ちなみにgit-pr-releaseとは

git-pr-releaseとは、本番環境向けの対象ブランチへのリリースPRを作成&リリース内容を一覧にして作成してくれるgemライブラリです。導入も簡単なので使える環境であればぜひ使ってみて欲しいです

こちらの記事が非常に参考になります!

songmu.jp

まずgit-pr-releaseが想定しているブランチモデルは

  • 本番用のmainブランチと開発用のdevelopブランチが並走する形で、developブランチからfeatureブランチを切ってmainブランチへマージしていく、いわゆるgit flowの簡易版の様なフローが前提
  • mainブランチへのマージが本番デプロイへのトリガーとして想定されている
  • 環境構成:本番環境(main)、dev環境(develop)

git-pr-releaseが想定しているブランチモデル

対してコネヒトでは

  • GitHub Flowを踏襲しており、本番用のmainブランチからfeatureブランチを切ってmainブランチへマージしていくフローになっている
  • 最新のmainブランチから切ったtagのpushが本番デプロイへのトリガーになっている
  • 環境構成:本番環境(最新のtag)、dev環境(featureブランチ)

コネヒトのブランチモデル

なのでもしgit-pr-releaseを採用する場合はブランチ戦略自体を見直す必要があり、流石にそれは影響範囲が大きすぎるかつ現実的ではありませんでした。そこで今あるブランチ戦略を変えずにもっと簡単にデプロイを実現する方法として考えたのがこの「Draft Releaseデプロイフロー」です。

そして実際にできたデプロイフローがこちら

いつものようにPRをマージします

Slackにdev環境へのデプロイ完了とドラフトリリースの作成が通知されます

作成されたドラフトリリースを

Publishすると

tagが切られ本番デプロイが走ると言った感じです

ビフォーアフター

既存のgdpを使ったデプロイフロー

  1. mainブランチからfeatureブランチを切る
  2. feature PRをmainブランチへマージ(dev環境へデプロイ)
  3. dev環境へのデプロイが完了したら、手元でgdpコマンドからタグを切ってpush
    1. 手元のmainを最新化(git checkout main && git pull origin main && git pull --tags)
    2. リリース内容確認(gdp deploy -d)
    3. タグを切って本番デプロイ(gdp deploy)
  4. 本番環境へのデプロイが完了したら、手元でgdpを使ってリリースノートを公開
    1. リリースノート内容確認(gdp publish -d)
    2. リリースノート公開(gdp publish)

デプロイフローBefore

今回作成したDraft Releaseデプロイフロー

  1. mainブランチからfeatureブランチを切る
  2. feature PRをmainブランチへマージ(dev環境へデプロイ)
  3. dev環境へのデプロイが完了したら、GitHub上から下書きリリースノートの内容を確認し、問題なければ公開(合わせてタグも切られるのでそのまま本番デプロイ)

デプロイフローAfter

改善ポイント

  • 手元でコマンド打つ場合、打ち間違いやコマンド履歴等から意図せずデプロイしてしまう可能性
    • GUI上から確認できるので、意図せずデプロイしてしまうといったことを防げる
  • devデプロイ〜本番リリースの間(ビルドの7〜8分)地味にブランチを切り替え辛い(リリース前にmainブランチにcheckoutが必要なため)
    • devデプロイ後は別のブランチに切り替えてすぐ作業の続きができるように
  • 新しい人が入社してきた場合にgdp&hubのインストールと設定が必要になる
    • 今回の対応で不要に
  • 本番デプロイ後にリリースノートを公開(gdp publish)するのを忘れてしまう
    • 必ずリリースノートが作成されるフローに

実際のコード

呼び出し元

ドラフトリリース作成ワークフロー

下記コードを各PJのリポジトリに展開するだけでドラフトリリースが作成されます!

on:
  push:
    branches:
      - main

permissions:
  contents: write

jobs:
  deploy:
    # デプロイ処理
    ...

  create_draft_release:
    if: ${{ success() }}
    needs: [ deploy ]
    uses: {Reusable Workflow用リポジトリ}/reusable-workflow/.github/workflows/create_draft_release.yml@main
    secrets:
      slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}

呼び出される側

ドラフトリリース作成ワークフロー

ドラフトリリース作成actionの実行と、Slack通知を行っています。

name: Create Draft Release
on:
  workflow_call:
    secrets:
      slack-webhook-url:
        required: true

jobs:
  start:
    runs-on: ubuntu-latest
    steps:
      - name: Action create-draft-release
        id: create-draft-release
        uses: {Reusable Workflow用リポジトリ}/reusable-workflow/.github/actions/create-draft-release@main

      - name: Notify slack of create draft release
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          fields: repo,workflow
          author_name: ${{ github.actor }}
          text: |
            ドラフトリリースが作成されました。<${{ steps.create-draft-release.outputs.draft-release-link }}|${{ steps.create-draft-release.outputs.draft-release-tag }}>
            <${{ github.server_url }}/${{ github.repository }}/releases|リリース一覧>
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.slack-webhook-url }}

実装時のポイント

Reusable Workflowとはいわゆる再利用可能なワークフローの事で、各リポジトリで共通のワークフローを実行することができます! 作成方法はシンプルでon.workflow_callを定義したymlファイルを作成するだけです。

on:
  workflow_call:

今回Reusable Workflowとして実装した理由ですが、Reusable Workflowと後述のaction.ymlの大きな違いとしては実行環境に左右されるかどうかだと考えています。

Reusable Workflowの場合はワークフロー(1つ以上のジョブ)でしか実行できないので前後の処理や実行環境に左右されません。逆にaction.ymlではジョブの一ステップとして実行されるので、必要であれば actions/checkout@v2actions/setup-node@v2 など使って実行環境を整える必要があります。

そして今回のDraft Release作成処理は完全に処理が独立しているので、リポジトリ毎の実行環境に左右されずに横展開しやすいReusable Workflowで実装しました

Reusable Workflow使用時の注意点としては実行されるコードはあくまで呼び出し元で実行されるので

  • actions/checkout を使用する場合は呼び出し元のブランチにチェックアウトされる
  • Reusable Workflowのリポジトリ内にあるファイルは参照できない

と言った点に注意が必要です!上記点を解決するために次のaction.ymlへとつながります

詳しくは公式ドキュメントを確認してください!

docs.github.com

ドラフトリリース作成処理本体

こちらがメインのドラフトリリース作成処理です。実行している処理はシンプルにGitHub CLIを使ったリリースノートの作成と削除を行なっています。(削除はリリースノートの自動生成を使いたいため一度削除→再作成しています)

name: Create Draft Release Action
description: Create draft release with calver tag

outputs:
  draft-release-link:
    description: Create draft release link.
    value: ${{ steps.create-draft-release.outputs.draft-release-link }}
  draft-release-tag:
    description: Create draft release tag.
    value: ${{ steps.create-draft-release.outputs.draft-release-tag }}

runs:
  using: "composite"
  steps:
    - name: Create Draft Release
      id: create-draft-release
      env:
        TZ: "Asia/Tokyo"
        GH_TOKEN: ${{ github.token }}
      shell: bash
      run: |
        # 最新のタグ取得
        latest_tag=$(gh release list --repo '${{ github.repository }}' | awk -F'\\t' '$2=="Latest" { print $3 }')
        echo "latest_tag:${latest_tag}"

        # calver形式での次バージョンのタグ取得
        next_tag=$(/bin/bash ${{ github.action_path }}/calver.sh $latest_tag)
        echo "next_tag:${next_tag}"

        # draft release取得
        draft_tag=$(gh release list --repo '${{ github.repository }}' | awk -F'\\t' '$2=="Draft" { print $3 }' | head -n 1)
        echo "draft_tag:${draft_tag}"

        # draft release が既に存在していれば、削除してから再作成(リリースノートの自動生成を使いたいため)
        if [ "$draft_tag" != "" ]; then
          gh release delete $draft_tag --repo '${{ github.repository }}' -y
        fi
        draft_release_link=$(
          gh release create $next_tag \\
            --repo '${{ github.repository }}' \\
            --title "Release ${next_tag}" \\
            --latest --draft --generate-notes
        )
        echo "draft-release-link=${draft_release_link}" >> "${GITHUB_OUTPUT}"
        echo "draft-release-tag=${next_tag}" >> "${GITHUB_OUTPUT}"
        echo $draft_release_link

実装時のポイント

action.ymlとはいわゆるメタデータ構文と言われているもので、ワークフロー内の処理を切り出したり共通化することができます! 普段よく使っているようなactions/checkoutのようなアクションを作成できるイメージです。作成方法はこちらもシンプルでaction.ymlまたはaction.yamlのファイル名で作成するだけです

action.ymlではパブリックリポジトリにあるアクションはもちろん、同一リポジトリ内のアクションからプライベートリポジトリのアクションも実行可能です。

    # パブリックのaction
    uses: actions/create-draft-release@main
    # 同一リポジトリ内のaction
    uses: ./.github/actions/create-draft-release
    # プライベートリポジトリのaction
    uses: {owner}/{repo}/.github/actions/create-draft-release@main

詳しくは公式ドキュメントと

docs.github.com

こちらの記事が参考になりました!

developer.mamezou-tech.com

calver形式でのタグ作成シェルスクリプト

このシェルスクリプトを作成した背景ですが、前提としてコネヒトではcalver形式でのバージョン管理を採用しています。

タグ作成にあたってできるだけ既存と同じようなcalver形式のタグを作成する必要があったのですが、現状これと同じようなタグを作成するツールが見つかりませんでした。そこで元々のgdpコマンドで行なっていたタグ作成ロジックをそのままコピーしてきて、このシェルスクリプトを作成することにしました。(GitHub Actionsとの相性や実行の手軽さも考えてシェルスクリプトにしました)

#!/bin/bash
set -eu

##############################
## Get next calver
## @ref <https://github.com/Connehito/gdp/blob/main/format.go>
##############################
today=$(date +'%Y%m%d')
regexp="(.*)([0-9]{8})\\.(.+)"

version=${1:-$today}

if [[ $version =~ $regexp ]]; then
    if [[ $today = ${BASH_REMATCH[2]} ]]; then
        minor=${BASH_REMATCH[3]}
        next=$(($minor + 1))
        echo "${BASH_REMATCH[1]}${today}.${next}"
        exit 0
    fi
    echo "${BASH_REMATCH[1]}${today}.1"
    exit 0
fi
echo "${today}.1"
exit 0

おわりに

同じような環境でデプロイフローに悩みを感じでいる方の参考になれば嬉しいです

AWSコスト削減の一環でecrmを使ってECRの不要イメージを削除した話

こんにちは、コネヒト プラットフォームグループの@yosshiです。

昨今、円安の進行によりAWSのコスト増が無視できない状況となっています。
このような状況下で、コスト削減関連のイベントに注目が集まるなど、コスト削減への関心が徐々に高まってきているように感じます。

弊社でもさまざまな方法でコスト削減施策を進めており、今回はその一環で「ecrm」というツールを使用しECRイメージを削除した話をしたいと思います。

弊社では、コンテナのオーケストレーションにAmazon ECSを主に使用しています。 ECSにタスクをデプロイする場合は、イメージをビルドした上でAmazon ECRにpushし、そのECRのイメージを使用しタスクを起動しています。 ECRはイメージが増えれば増えるほど、容量に応じてコストがかかるので、この機会に対応したいと考えていました。

ECRで発生するコスト

ECRではデータストレージの容量に応じて料金がかかります。
ap-northeast-1(アジアパシフィック(東京))のリージョンでは、GB/月当たり 0.10USDの料金が発生します。
※2024/01/17時点の情報。ECRでは上記とは別に転送コストなどが発生します。

参考:https://aws.amazon.com/jp/ecr/pricing/

ecrmとは?

ecrmはAmazon ECRから不要なイメージを安全に削除するOSSです。
github.com

こちらのブログに詳細が書いてありますので、詳しくはこちらをご確認ください。
techblog.kayac.com

ECRには元々ライフサイクルポリシーという機能があり、世代数やイメージプッシュからの日数に基づき古いイメージを自動的に削除することができるのですが、「現在のECSタスクで使用しているもの」という観点でチェックをしてくれるわけではないので、使用中のイメージを削除してしまうリスクがあると感じていました。

そこで調べていたところecrmの存在を知り、試してみることにしました。

試してみる

インストール(macOS で brew を使う場合)

$ brew install fujiwara/tap/ecrm

イメージ削除にあたり、削除対象を決めるための設定ファイルが必要になります。
AWSアカウントのcredentialを適切に環境変数などで設定した状態で 以下コマンドを実行することで、アカウントに存在するECRやECS、Lambdaのリソースを元に、設定ファイル(ecrm.yaml)を自動生成してくれます。

$ ecrm generate

実行すると設定ファイル(ecrm.yaml)が作成されました。
クラスタやタスク定義はワイルドカードのパターンで指定でき、prefixが記号([_/-])で区切れてまとめられそうなものは、適宜まとめたパターンで自動生成してくれます。

ecrm.yaml

clusters:
  - name_pattern: <cluster1>-*
  - name_pattern: <cluster2>-*
    ・
    ・
task_definitions:
  - name_pattern: <task_definition1>-*
    keep_count: 5
  - name_pattern: <task_definition2>-*
    keep_count: 5
    ・
    ・
lambda_functions:
  - name_pattern: <lambda_function1>-*
    keep_count: 5
    keep_aliase: true
  - name_pattern: <lambda_function2>-*
    keep_count: 5
    keep_aliase: true
    ・
    ・
repositories:
  - name_pattern: <repository1>-*
    expires: 30d
    keep_count: 5
    keep_tag_patterns:
      - latest
  - name_pattern: <repository2>-*
    expires: 30d
    keep_count: 5
    keep_tag_patterns:
      - latest
    ・
    ・

この設定ファイル(ecrm.yaml)を弊社の都合に合わせて修正していきます。

設定した条件 (削除対象外とするもの)

  • 現在使用中のイメージ
    • ECSクラスタで現在実行中のタスクに含まれるイメージ
    • ECSサービスで指定されているタスク定義に含まれるイメージ
  • ECSタスク定義で指定されているイメージ(タスク定義最新リビジョンから5世代分
  • Lambda関数で指定されているイメージ(Lambda最新バージョンから5世代分)
  • 90日以内に作成されたイメージ
  • タグ名にlatest, releaseがついているイメージ ※弊社ではリリース対象に左記のようなタグ名をつけています

上記の下線部は可変にできるので、適宜適切な内容に置き換えてください。 設定ファイル(ecrm.yaml)は以下のようになりました

clusters:
  - name_pattern: <cluster1>-*
  - name_pattern: <cluster2>-*
    ・
    ・
task_definitions:
  - name_pattern: <task_definition1>-*
    keep_count: 5
  - name_pattern: <task_definition2>-*
    keep_count: 5
    ・
    ・
lambda_functions:
  - name_pattern: <lambda_function1>-*
    keep_count: 5
    keep_aliase: true
  - name_pattern: <lambda_function2>-*
    keep_count: 5
    keep_aliase: true
    ・
    ・
repositories:
  - name_pattern: <repository1>-*
    expires: 90d
    keep_count: 5
    keep_tag_patterns:
      - *latest*
      - *release*
  - name_pattern: <repository2>-*
    expires: 90d
    keep_count: 5
    keep_tag_patterns:
      - *latest*
      - *release*
    ・
    ・

設定ファイル(ecrm.yaml)の編集が完了したら、どの程度イメージが削減できそうか確認します。

削除対象は以下コマンドで確認できます。

$ ecrm plan

実行したところ以下のようになりました。

REPOSITORY     |   TYPE    |    TOTAL     |    EXPIRED    |    KEEP      
---------------------------+-------------+--------------+---------------+--------------
<repository1>  | Image     | 256 (38 GB)  | -187 (27 GB)  | 69 (11 GB)      
<repository2>  | Image     | 227 (44 GB)  | -209 (41 GB)  | 18 (3.2 GB)  
<repository3>  | Image     | 92 (13 GB)   | -82 (12 GB)   | 10 (1.5 GB)  
<repository4>  | Image     | 109 (307 GB) | -57 (181 GB)  | 52 (126 GB)  
<repository5>  | Image     | 63 (67 GB)   | -52 (53 GB)   | 11 (14 GB)   
<repository6>  | Image     | 251 (2.2 GB) | -236 (2.0 GB) | 15 (190 MB)  
<repository7>  | Image     | 771 (67 GB)  | -760 (66 GB)  | 11 (862 MB)  
<repository8>  | Image     | 150 (75 GB)  | -135 (67 GB)  | 15 (7.8 GB)  
<repository9>  | Image     | 65 (22 GB)   | -60 (20 GB)   | 5 (1.7 GB)   
<repository10> | Image     | 138 (29 GB)  | -128 (27 GB)  | 10 (2.1 GB)  
<repository11> | Image     | 116 (21 GB)  | -104 (19 GB)  | 12 (2.2 GB)  
<repository12> | Image     | 927 (129 GB) | -910 (126 GB) | 17 (2.8 GB)  
<repository13> | Image     | 284 (55 GB)  | -231 (44 GB)  | 53 (11 GB)   
<repository14> | Image     | 57 (546 MB)  | -21 (186 MB)  | 36 (360 MB) 

トータルのイメージに対して削除対象がどの程度あるか(EXPIRED)、残るイメージがどの程度あるか(KEEP)がわかります。

実際に削除を実行する際には以下コマンドを実行します。

$ ecrm delete

実行した後、実際に削除されたかどうかを確認したいので再度ecrm planを実行します。

$ ecrm plan

実行結果

REPOSITORY       |    TYPE     |    TOTAL    | EXPIRED |    KEEP      
-----------------+-------------+-------------+---------+--------------
<repository1>    | Image       | 69 (11 GB)  |         | 69 (11 GB)   
<repository2>    | Image       | 18 (3.2 GB) |         | 18 (3.2 GB)  
<repository3>    | Image       | 10 (1.5 GB) |         | 10 (1.5 GB)  
<repository4>    | Image       | 52 (126 GB) |         | 52 (126 GB)  
<repository5>    | Image       | 11 (14 GB)  |         | 11 (14 GB)   
<repository6>    | Image       | 15 (190 MB) |         | 15 (190 MB)  
<repository7>    | Image       | 11 (862 MB) |         | 11 (862 MB)  
<repository8>    | Image       | 15 (7.8 GB) |         | 15 (7.8 GB)  
<repository9>    | Image       | 5 (1.7 GB)  |         | 5 (1.7 GB)   
<repository10>   | Image       | 10 (2.1 GB) |         | 10 (2.1 GB)  
<repository11>   | Image       | 12 (2.2 GB) |         | 12 (2.2 GB)  
<repository12>   | Image       | 17 (2.8 GB) |         | 17 (2.8 GB)  
<repository13>   | Image       | 53 (11 GB)  |         | 53 (11 GB)   
<repository14>   | Image       | 36 (360 MB) |         | 36 (360 MB)  

EXPIREDの列がブランクとなり、対象の不要イメージが削除されたことがわかります。

結果

今回のケースでは685GBのイメージの削減に成功しました。 現時点のAWSのコストベース(GB/月当たり 0.10USD)で換算すると、月間68.5USDの削減となります。
※2024/01時点のAWS ECRのコスト

感想

今回はecrmを使用してECRイメージを削除した話をしました。

金額としては小さいかもしれませんが、もともと使っていないイメージに対する余分なコストだったので、このタイミングで簡単に削減できてよかったなと思います。

コスト削減に注目が集まっている状況だと思うので、各社のコスト削減の参考になれば幸いです。

今回は単発でイメージの削除を行いましたが、今後は自動で定期実行する仕組みなども合わせて検討していきたいと考えています。

2023年のコネヒト開発組織を振り返る

こんにちは。CTOの永井(shnagai)です。

早いもので今年も残すところ数日ですね。

アドベントカレンダー最終日ということで、技術的なトピックやチームでの工夫はこれまでの記事でみんなが思う存分書いてくれたので、今回は2023年のコネヒト開発組織の1年を自分の視点で振り返っていこうと思います。

この記事は、コネヒトAdvent Calendar2023の25日目の記事です。

adventar.org

開発組織のアップデートと特に目立った技術的なトピックという構成で書いてます。

開発組織のリフレーミングと頼れる多くの仲間のジョイン

FY23のスタートに合わせて、開発組織のリフレーミングを行いました。

それぞれのチームの関係を可視化

期初のタイミングでチームトポロジーを参考に、それぞれのチームの構造を改めて可視化しました。

自分はチームトポロジーの、ストリームアラインドチームが出す価値の総量が開発組織のバリューという考えに非常に感銘を受けており、プロダクトの先にいるユーザーファーストの考えをうまく組織に当てはめた考えだなと思っています。

ママリや自治体向け事業等の事業毎にストリームアラインドチームを配置して、事業部と密に連携しながら開発するスタイルをコネヒトでは採用しています。

コンプリケイテッドサブシステムチームは技術的難易度の高い領域を受け持ち、プラットフォームチームでは、ストリームアラインドチームの認知負荷を減らしプラットフォームエンジニアリングでプロダクトの価値最大化に貢献することをミッションにしています。

下記は、期初戦略で全社に説明した資料の一部です。

※現在はメンバー増によりストリームアラインドが増えています。

ピープルマネジメントをEMに集約 〜エンジニアがよりプロダクト・事業に集中出来る体制へ〜

元々コネヒトでは、各開発グループにグループリーダー(以後、GL)を配置し、そのGLがチームのマネジメント4象限(ピープル/プロダクト/プロジェクト/テクノロジー)を全て担う構造にしていました。

だいたい2,3名のチームにそれぞれGLがいる構造だったのですが、今の組織フェーズ的にはエンジニアがよりプロダクトや事業に集中する時間を作るほうが良いと判断して、ピープルマネジメント業務をチーム横断でEM 2名に集約しました。

全てがうまくいっているわけではありませんが、ピープルマネジメントを集約したことでメンバーからはよりプロダクトや事業に集中出来ているというフィードバックをもらっており、一定うまくいっている施策だなと思っています。

制度やロールは常に見直しながらアップデートしていくのが良いと思っており、これからも組織拡大に合わせて柔軟に見直していき、アジリティの高い開発が出来るようにしていきたいと思っています。

きれいなことばかり書きましたが、テックリードという役割を設けるトライもしたのですが、中々イメージの言語化や定義が追いつかずに、役割を担ってくれたメンバーと対話しながら、半年でまた別のロールにしたりと頼れる同僚とともに試行錯誤しながらやっています。

3人に1人を目指して 多くの仲間のジョイン

今年は、7名の新しいエンジニアがコネヒトにジョインしてくれました。

コネヒトでは、テックビジョンにおいて「3人に1人」という戦術を掲げており、会社としてやりたい事業を全て実現し、テクノロジーやエンジニアリングの恩恵を社内も受け、そしてエンジニア以外でもテクノロジーを使いこなせるような世界を目指しています。

今年ジョインしてくれた仲間の職種も多岐に渡るのですが、それぞれが所属するチームや開発組織でバリューを発揮してくれており全体として活気づいています。

社内のエンジニア比率が高まっていく中で、生成AIの技術革新もあいまってテックビジョンに掲げる世界をより実現フェーズに持っていくために新たな動きも走り始めています。

全社向けに生成AIの活用とテクノロジーを利用した業務改善を推進する「Run with Tech 」プロジェクトで、こちらは、また、どこかの機会で紹介出来ればと思っています。

続いて、今年特に印象的に残った技術的なトピックについても紹介していければと思います。

検索システムの内製化完了

今年技術的なトピックで一番大きなトピックといえば検索システムの内製化が完了したことがまず挙げられます。

これまで、SaaSの検索システムを内部のAPIから呼び出す形式で使っていたものを、フルリプレイスする形で0から検索システムを構築しました。

時系列で見る検索システム内製化の軌跡

  • 2021年10月 構想開始
  • 2022年3月 経営承認
  • 2022年4月 開発開始
  • 2022年8月 最初の機能(新着質問順)のリプレイス完了
  • 2023年6月 全ての機能のリプレイス完了

主な狙いは、検索システムを内部で持つことによるユーザー体験の強化とSaaS入れ替えによる費用削減の2点でした。

時系列で書くとすんなり言ったように見えますが、すべての機能において既存とのABテストを行いママリにおける検索体験のユーザー毀損がないことを細かく確認しながらリプレイスを行いました。

新着順、人気順、サジェスト、関連キーワードと多岐に渡る機能のABテストはSaaS提供と同品質をゴールにしている関係で中々にヒリヒリするもので、メインで担当していた同僚達のやり切る力には脱帽です。

技術的な要素を少し解説すると、検索システム内製化には下記技術要素があります。

  • 内部APIから呼ばれる検索API(Go)
  • 検索エンジンにはOpenSearchを採用
  • 技術的難易度が一番高かったデータ同期の仕組みにはAWS GlueとStepFunctionsでニアリアルタイムな基盤を構築   tech.connehito.com

担当したメンバーは全員検索システムは未経験だったので、みんなでペンギン本を読んで輪読会をしたり、壁にぶつかるたびに議論して一歩ずつ前に進んでいきました。私自身もメンバーとして当初コミットしていて、検索システムは奥深くエンジニアとしてまた一つ成長できた良い機会だったと感じています。

tech.connehito.com

もちろん、リプレイスをして終了ではなく、ようやく検索システムをママリの武器に出来るスタートラインに立てたので、昨今のLLM全盛の時勢も鑑みながら、キーワード検索にとらわれずよりユーザーにとってママリの検索がより使いやすいものであり続けられるように技術的な検証や新たなトライも既に始まっています。

tech.connehito.com

「終わらせることを始めよう」を合言葉に最終的に3ヶ月計画を前倒しし、検索システム内製化という成果に対して社内表彰もされたプロジェクトとなりました。

ユーザーへの価値提供という点では、まさにこれから真価を問われることになりますが、テクノロジーでプロダクトを伸ばす先端事例になるような取組みなので今後がより楽しみです。

Let’s Go

コネヒトのテックビジョンでは、「バックエンドのシステムにGoを積極的に導入する」という戦略を掲げていおりその戦術名が「Let's Go」です。

これまでも、前述の検索APIしかり新規のシステムでのGoの採用は何度か行ってきました。それらを通しての、組織としての知見の習得はそこそこ進んできたかと感じています。

このLet’s Goの作戦は、aboyがボールを持って中心に進めてくれているのですが、今年の大きな進歩として、既存のPHPで書かれたコードベースをGoに置き換えるというプロジェクトが走り出しました。

まずは、Goの特徴である並列処理やワーカー実装のしやすさを活かせる非同期のバッチ処理のリプレイスを実施していますが、メインシステムを適材適所でGoに置き換える動きがスタートしているのは開発組織として大きな一歩だなと感じています。

中期戦略では、2024年中にバッチ処理を全てGoに置き換えるというチャレンジングな目標を掲げており、その実現に向けて日々開発を進めています。

また、Let’s Go Talkという社主催のイベントも定期的に実施しており、Go好きがゆるく繋がれるようなコミュニティも作っていきたいと思っているので興味のある方は是非ご参加ください。

コネヒト/Connehito Inc. - connpass

まとめ

2024年も引き続きプロダクトや事業を伸ばしていくことに注力しつつ、テックビジョンに掲げる「Beyond a Tech Company」の実現に向けてCTOというラベルを活かして色々と策を打っていこうと思います。

テックビジョンの存在が羅針盤からより会社の中で身近なものになっていけば、コネヒトとしてユーザーや社会に約束するありたい世界観の実現に近づいていくと自分は信じています。

最後にですが、エンジニアはもちろん、幅広い職種で採用もしていますので興味持たれた方は是非ご応募いただきカジュアルにお話出来るとうれしいです。

hrmos.co

trivyとGithub Actionsを使用しTerraform設定ファイルのセキュリティスキャンを実行する仕組みを作りました

この記事はコネヒトアドベントカレンダー21日目の記事です。

コネヒト Advent Calendar 2023って?
コネヒトのエンジニアやデザイナーやPdMがお送りするアドベント カレンダーです。
コネヒトは「家族像」というテーマを取りまく様々な課題の解決を 目指す会社で、
ママの一歩を支えるアプリ「ママリ」などを 運営しています。

adventar.org

はじめに

コネヒトのプラットフォームグループでインフラ関連を担当している@yosshiです。 今年の7月に入社してから早いもので半年が経ちました。時が経つのは本当に早いですね。

今回のブログでは、セキュリティスキャンツールであるtrivyを使って、自動的にIaC (Infrastructure as Code)スキャンを実行する仕組みを構築した話をしたいと思います。

弊社ではインフラ構成をTerraform利用して管理するようにしており、それをモノレポの構成で運用しています。

インフラリソースの作成・変更・削除をする際には必ず相互レビューを必須としているものの、人的なチェックのみに依存しているためファイルの設定ミスやセキュリティ上の見落としが潜在的なリスクとなっていました。

これらを事前に検知するツールを調べていたところtrivyの存在を知り、今回導入に至りました。

trivyとは

  • コンテナイメージやアプリケーションの依存ライブラリ・OSのパッケージなどを迅速にスキャンし、セキュリティリスクを効率的に検出するセキュリティツールです。
  • 当初はコンテナのセキュリティ問題に焦点を当てたツールとして開発されたようですが、後にTerraformやKubernetesなどの設定ファイルのチェック機能も追加されました。
  • 設定ファイルのチェックでは、設定ミスやセキュリティのベストプラクティスに沿っていない構成などを検知することができます。

参考:https://github.com/aquasecurity/trivy

(参考)スキャン可能な対象 2023/12時点

  • コンテナイメージ
  • ファイルシステム
  • リモートGitリポジトリ
  • 仮想マシンイメージ
  • Kubernetes
  • AWS

(参考)検出可能な内容 2023/12時点

  • OSパッケージとソフトウェア依存関係(SBOM)
  • 既知の脆弱性(CVE)
  • IaCの問題と設定ミス
  • 機密情報と秘密
  • ソフトウェアライセンス

上記の通りtrivyでは、Terraformのコードだけでなくさまざまなセキュリティスキャン行うことができます。

trivy自体の詳しい説明はここでは割愛するので、詳しくは公式サイトや他の方の記事などをご確認いただけると幸いです。

Github Actionsでの実装

では早速ですが実装内容の説明に移りたいと思います。 今回はTerraformコードのスキャンをCIに組み込んでいます。 弊社では CIツールとしてGithub Actionsを利用しているため、今回もこちらを利用します。

スキャン実行は、trivy公式で用意しているGithub Actions用のツール(tricy-action)があるので、こちらをそのまま利用しています。

背景

まず、前提条件となる弊社のディレクトリ構造を説明します。

弊社では、サービスで共通利用するリソース(base_system)と各サービスで利用するリソース(product_system)とでディレクトリを分けており、 product_sytem以下にはサービスごと関連するリソースが紐づいています。以下のようなイメージです。

.
├── base_system
│   ├── common
│   │   └── terraform
│   ├── privilege
│   │   └── terraform
│   .
│   .
└── product_system
    ├── (サービス1)
    │   └── terraform
    ├── (サービス2)
    │   └── terraform
    .
    .
    .

実装方針と内容

実装したGithub Actionsのコードは以下の通りです。

主に以下のことをやっています。

  1. シェルスクリプト(sync-updated-dirs-to-work-dir.sh)の実行
    • mainブランチとプルリクエスト中のブランチのコードの差分を検知
    • 差分となっているディレクトリを作業用のディレクトリに同期
  2. 作業ディレクトリに対してスキャン実行
  3. 検出されたスキャン結果をPRのコメントに残す。

Github Actionsのコードは以下の通りです。

name: trivy-scan

on:
  pull_request:
    types: [opened, reopened, synchronize]

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  trivy_scan:
    name: Run Trivy Scan
    runs-on: ubuntu-latest
    steps:
      - name: Clone repo
        uses: actions/checkout@v4

      - name: fetch origin/main for getting diff
        run: git fetch --depth 1 origin $GITHUB_BASE_REF

      - name: Sync Updated Dir to Work Dir
        env:
          GITHUB_TOKEN: ${{ secrets.github_token }}
        run: |
          bash utils/scripts/sync-updated-dirs-to-work-dir.sh

      - name: Trivy Scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'config'
          severity: 'HIGH,CRITICAL'
          scan-ref: scan_work_dir
          output: trivy-scan-result.txt

      - name: Format Trivy Scan Result
        run: |
          if [ -s trivy-scan-result.txt ]; then
            # ファイルに内容がある場合
            echo -e "## 脆弱性スキャン結果\n<details><summary>詳細</summary>\n\n\`\`\`\n$(cat trivy-scan-result.txt)\n\`\`\`\n</details>" > formatted-trivy-result.md
          else
            # ファイルが空の場合
            echo -e "## 脆弱性スキャン結果\n脆弱性が検知されませんでした。" > formatted-trivy-result.md
          fi

      - name: Comment PR with Trivy scan results
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          recreate: true
          GITHUB_TOKEN: ${{ secrets.github_token }}
          path: formatted-trivy-result.md

詳しく見ていきます。

まずは変更差分となったディレクトリを調べるため、Github Actionsの中でシェルスクリプトを実行しています。

ここでは一時的なディレクトリを用意し、プルリクエスト上で変更が発生したディレクトリの内容を作業用のディレクトリに同期するという作業を行なっています。

Github Actionsの関連部分は以下です。

- name: Sync Updated Dir to Work Dir
   run: |
     bash utils/scripts/sync-updated-dirs-to-work-dir.sh

sync-updated-dirs-to-work-dir.shはrsyncをwrapしたもので、前述の通りプルリクエスト上で変更が発生したterraformディレクトリを、スキャン実行する一時的な作業用ディレクトリに同期する処理をしています。1

細かな実装は内部事情に特化したものとなっているため割愛しますが、例えば、base_system/common/terraform, base_system/privilege/terraform, product_system/(サービス1)/terraformのディレクトリで変更が発生している場合、このスクリプトを実行することで作業用ディレクトリ(scan_work_dir)が作成され、以下のようなディレクトリ構造となります。

.
├── base_system
│   ├── common
│   │   └── terraform
│   └── privilege
│   │   └── terraform
│   .
│   .
├── product_system
│   ├── (サービス1)
│   │   └── terraform
│   ├── (サービス2)
│   │   └── terraform
│   .
│   .
└── scan_work_dir (ここのディレクトリにtrivyによるスキャンを実行する)
    ├── base_system
    │   ├── common
    │   │   └── terraform
    │   └── privilege
    │   │   └── terraform
    └── product_sytem
        └── (サービス1)
            └── terraform

次に同期してきた作業用ディレクトリに対してスキャンを実行します。
ここでは前述の通り、公式が用意しているtricy-actionを利用しています。

- name: Trivy Scan
  uses: aquasecurity/trivy-action@master
  with:
   scan-type: 'config'
   severity: 'HIGH,CRITICAL'
   scan-ref: scan_work_dir
   output: trivy-scan-result.txt

オプションの内容は以下の通りです。

  • scan-type: 'config'
    • configはTerraformなどの設定ファイルをスキャンする際に使用する。
  • scan-ref: scan_work_dir
    • 作業用ディレクトリ scan_work_dir を指定している。
  • output: trivy-scan-result.txt
    • スキャンした結果をテキストファイルtrivy-scan-result.txtに出力する。
    • このテキストファイルは次のアクションでフォーマットを整形して出力するために使用している。
  • serverity
    • 脆弱性の深刻度で'CLITICAL'と'HIGH'を指定しています。
    • ここのレベルは、CVSS2によって定量化された脆弱性の深刻度をもとに設定されています。

他のオプションなどについてはtricy-actionのページをご確認ください。

次に、前のアクションで出力したテキストファイルを見やすく整形した上で、marocchino/sticky-pull-request-commentを使用しプルリクエストのコメント欄に出力しています。

プルリクエスト更新時には、既存の脆弱性に関するコメントを削除した上で新規コメントを残して欲しかったので、その点でmarocchino/sticky-pull-request-comment(recreate: trueのオプション指定)を利用することで楽に実装することができました。

# スキャンした結果を整える
- name: Format Trivy Scan Result
  run: |
    if [ -s trivy-scan-result.txt ]; then
      # ファイルに内容がある場合
      echo -e "## 脆弱性スキャン結果\n<details><summary>詳細</summary>\n\n\`\`\`\n$(cat trivy-scan-result.txt)\n\`\`\`\n</details>" > formatted-trivy-result.md
    else
      # ファイルが空の場合
      echo -e "## 脆弱性スキャン結果\n脆弱性が検知されませんでした。" > formatted-trivy-result.md
    fi

- name: Comment PR with Trivy scan results
  uses: marocchino/sticky-pull-request-comment@v2
  with:
    recreate: true
    GITHUB_TOKEN: ${{ secrets.github_token }}
    path: formatted-trivy-result.md

スキャン結果

  • 脆弱性が検知されなかった場合

  • 脆弱性が検知された場合

「詳細」部分を開くと以下のような形で表示されています。

今回のケースだとCritical0件、High61件の脆弱性が検知されていることがわかります。

base_system/privilege/terraform/xxx.tf (terraform)
=====================================================================
Tests: 75 (SUCCESSES: 14, FAILURES: 61, EXCEPTIONS: 0)
Failures: 61 (HIGH: 61, CRITICAL: 0)

HIGH: IAM policy document uses sensitive action 'autoscaling:Describe*' on wildcarded resource '*'
════════════════════════════════════════
You should use the principle of least privilege when defining your IAM policies. This means you should specify each exact permission required without using wildcards, as this could cause the granting of access to certain undesired actions, resources and principals.

See https://avd.aquasec.com/misconfig/avd-aws-0057
────────────────────────────────────────
 base_system/privilege/terraform/xxx.tf:376
   via base_system/privilege/terraform/xxx.tf:376 (aws_iam_policy.xxx.xxx)
    via base_system/privilege/terraform/xxx.tf:375-438 (aws_iam_policy.xxx.xxx)
     via base_system/privilege/terraform/xxx.tf:373-439 (aws_iam_policy.xxx)
────────────────────────────────────────
 373   resource "aws_iam_policy" "xxx" {
 ...
 376 [     Version = "2012-10-17",
 ...
 439   }
────────────────────────────────────────
・
・
・(以下省略)

上記の例では、重要度「HIGH」の脆弱性が検知されています。

IAMポリシーは最小特権で付与するべきなので、ワイルドカード使うのはリスクがありますよ、という指摘のようです。

補足

検出された脆弱性の中で、この内容は指摘する対象から除外したい、というケースもあると思います。

その場合は.trivyignoreというファイルをトップディレクトリに置くことで勝手に参照して検出対象から除外してくれます。

以下のような形で記載します。

.trivyignore

AVD-AWS-0057
AVD-AWS-XXXX

(参考)

以下のようにtrivyignoresのオプションを利用することで、トップディレクトリに配置するだけでなく別のディレクトリに配置しているファイルを参照させたり、.trivyignore以外の別のファイル名を指定することもできるようです。

- name: Trivy Scan
  uses: aquasecurity/trivy-action@master
  with:
   scan-type: 'config'
   severity: 'HIGH,CRITICAL'
   scan-ref: trivy_temp_dir
   output: trivy-scan-result.txt
   trivyignores: test-trivyignore

まとめ

今回はtrivyとGitHub Actionsを活用し、Terraformでのセキュリティ上のリスクを効果的に検知する仕組みを構築しました。

今回検知されたものについては、優先度の高いものから順次改善していきたいと思います。

また、冒頭に説明した通り、trivyでは他にも様々なものを検知してくれる機能があるので、 Terraformの設定だけでなく色々な場面での活用を検討していきたいと思います。


  1. 変更が発生したディレクトリの抽出には git diff origin/main --name-only の結果をパースしています。
  2. CVSS( Common Vulnerability Scoring System ) = 共通脆弱性評価システム