メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

Raspberry Pi + ChatGPT で会話ロボットを作った

会話の間が長く、スムーズでなくてすいません💦

長期休みに入る子供対策 R&Dの一環で子供達と会話してくれる AI ロボットを作りました。

github.com

会話の例

日々このような会話が繰り広げられる様になりました。

例1

  • 🤖「こんにちは。今日はどんなサッカー練習をしましたか?」
  • 👦「今日はダイレクトシュートの練習をしました」
  • 🤖「それは良いですね。ダイレクトシュートは難しいですよね。どんなダイレクトシュートの練習ですか?」
  • 👦「インサイドキックです」
  • 🤖「サッカーの試合で使われるキックの7割がインサイドキックと言われていますね。試合でもインサイドキックをしますか?」

例2

  • 🤖「こんにちは。どんなサッカー選手が好きですか?」
  • 👦「ドリブルやパスが上手な選手です。」
  • 🤖「どの選手が好きですか?」
  • 👦「モドリッチです。」

などなど、普段親子での会話では出ない様な回答が引き出せて面白いです。

ロボットには敬語使うんだなというのも興味深いです。

システム概要図

*1

大まかな処理の流れ

  1. モーションセンサー検知
  2. OpenAI に会話開始テキストを送信し、レスポンスを音声変換しスピーカーから再生
  3. 以下繰り返し
    1. ビープ音再生
    2. RPi 録音開始
    3. 子供しゃべる
    4. 録音内容を音声ファイルに保存
    5. 音声ファイルを Cloud Speech to Text API でテキスト変換*2
    6. テキストを OpenAI に送信しレスポンスを音声変換しスピーカーから再生

工夫点

  • 会話のきっかけはロボットから
    • 「話しかけられた」方が子供は嬉しいと思ったので 👦
  • 子供の話すターンの終了は無言3秒経過したら
    • 「最初は録音開始から10秒以内に話す」ルールにしたが、話したいことが言えなかった
    • pyaudio の無言時間計測できる仕様があったのでそれを利用した
  • 子供が話して良いタイミングにはビープ音を鳴らす
    • 話して良い合図がないとロボットとの会話は難しかった
    • ビープ音と一緒に話している間は LED を光らせる様に視覚的にも会話期間を認識させる
  • 会話は返答無視で自動終了
    • ある程度遊んだらロボットの返事待たずどこかへいってしまう為
    • 返答を 5秒無視で会話終了
      • 再度モーションセンサーが検知したら会話開始となる
  • 日本語・英語どちらでも話せる様にした
    • 私個人の英語学習の為に英会話もさせたかった

ChatGPT にお願いする会話の制約

英語・日本語で制約を指定しています。

先の動画では以下のような指示をしています。

ja:
  system_content: >-
    #指示:
    以下の制約の下でチャットしてください。

    #制約条件:
    私は日本のサッカーが大好きな小学生2年生です。
    会話のトピックはどんなサッカー選手が好きか、とか、今日はどんなサッカーの練習をしたとか、をお願いします。
    回答は極力短くお願いします。

使ったもの

  • RPi 4B
    • OS: Raspbian GNU/Linux 10 (buster)
  • OpenAI API
  • Cloud Speech to Text API
  • モーションセンサー
  • スピーカー
  • マイク

諸々購入しましたが、弊社エンジニアは実質 0 円です😄
弊社のテックサポート制度で購入をサポートしている為です 🎉

ブレッドボードの設定

SunFounder で各種モジュールの設定方法があったので参考にしました。

https://docs.sunfounder.com/projects/kepler-kit/en/latest/

難しかったこと

3歳の舌足らずなところはうまく聞き取ってもらえなかったです。 でも会話が継続されているのでこれはこれで良いのかもしれない😅

また、こちらの話終わりを「無言になってから3秒経過したら」としましたが、ノイズを拾ってしまい3秒以上かかることがありました。

マイクの性能にも依存するのかもしれませんが、スマートホームのようなスムーズな会話を実現するには録音ライブラリの微調整と機材の向上は必要かと感じました。*3

もしくは、視覚的にローディング待ち状態がわかる仕掛けがあると良いかなと思いました。

子供達の反応

  • 長男:
    • もっと自分に合ってるサッカーの練習方法を教えて欲しい⚽️
    • これいつまで話せばいいの?
  • 次男:
    • ロボットになりたい🤖

カスタマーからストイックな要求も飛び出し、改良の余地がありました。
会話を続ける限り続けられてしまうので質問攻めの苦痛もあった様です。*4
次男のサイボーグ化は家族会議します。

まとめ

シンプルにモジュールと API を繋ぎ合わせて Raspberry Pi + ChatGPT で会話ロボットを作ってみました。

今後、さらに会話した感が増すように唇が動いたり、表情筋を再現できるような仕組みを作りたいと思っています。(怖い)

以上
参考になれば幸いです。


是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

medpeer.co.jp

■エンジニア紹介ページはこちら

engineer.medpeer.co.jp

*1:シーケンス図は https://github.com/kenzo0107/raspi-talk#system-overview

*2:音声のテキスト変換について、julius: 日本語音声を認識できるものの、辞書の作成や各種ライブラリのインストールが必要で手間でした。 pocketsphinx はインストールの難易度が高く、解決しなかったので見送りました。最終的に音声ファイルからテキストに変換する際の精度と導入の簡易さから Cloud Speech to Text を選択しました。他良い選択があれば是非ともご教示ください🙇‍♂️

*3:動画では途中早送りしてます

*4:数ターンで会話終了する仕組みを入れても良さそう

お財布に優しいCI改善小ネタ集

こんにちは。サーバーサイドエンジニアの三村(@t_mimura)です。

主に保険薬局と患者さまを繋ぐ「かかりつけ薬局」化支援アプリ kakariのサーバーサイド開発(Ruby on Rails)を担当しています。

今回はRailsシステムのCI時間をコスト追加なしで半減した話をします。

目次

前提

対象プロジェクト

まず、今回対象となるRailsシステムの規模感について簡単にご紹介します。

kakariは2019年にリリースされ、今現在も活発に開発が進んでいるプロジェクトです。 大まかに以下のような構成となっています。

  • バックエンド Ruby on Rails
    • フロントエンドとモバイルアプリ向けのAPIを提供
    • 一部システムではwebpackを利用しVue.jsが組み込まれているRails Viewも提供
  • フロントエンド Vue.js
    • Rails Viewに組み込まれている
  • モバイルアプリ Kotlin, Swift
    • ※ 今回のスコープ外

今回はこれらのうち「Ruby on Railsとそれに組み込まれているVue.js」を管理しているGit リポジトリのCIを改善した話となります。

$ rails stats
+----------------------+--------+--------+---------+---------+-----+-------+
| Name                 |  Lines |    LOC | Classes | Methods | M/C | LOC/M |
+----------------------+--------+--------+---------+---------+-----+-------+
| Controllers          |  25114 |  18962 |     700 |    2768 |   3 |     4 |
| Helpers              |    336 |    240 |       1 |      42 |  42 |     3 |
| Jobs                 |      6 |      3 |       1 |       0 |   0 |     0 |
| Models               |  68048 |  46214 |    1537 |    5532 |   3 |     6 |
| Mailers              |   1128 |    871 |      60 |     117 |   1 |     5 |
| Views                |    182 |    172 |       0 |       0 |   0 |     0 |
| Libraries            |   1473 |   1140 |      16 |      77 |   4 |    12 |
| Controller specs     |    459 |    389 |       0 |       2 |   0 |   192 |
| Decorator specs      |    135 |    111 |       0 |       0 |   0 |     0 |
| Haml_lint specs      |     39 |     26 |       0 |       0 |   0 |     0 |
| Helper specs         |    320 |    240 |       0 |       1 |   0 |   238 |
| Lib specs            |   1126 |    936 |       0 |       2 |   0 |   466 |
| Mailer specs         |   6797 |   6046 |       0 |       3 |   0 |  2013 |
| Model specs          |  77120 |  61488 |       3 |     140 |  46 |   437 |
| Push_notifier specs  |   2547 |   2173 |       0 |       6 |   0 |   360 |
| Request specs        |  54132 |  45976 |       0 |     407 |   0 |   110 |
| Serializer specs     |   1820 |   1509 |       0 |       0 |   0 |     0 |
| System specs         |  28542 |  22830 |       0 |      20 |   0 |  1139 |
| Task specs           |    203 |    169 |       0 |       0 |   0 |     0 |
| Validator specs      |   1355 |    957 |      20 |       6 |   0 |   157 |
| Worker specs         |   7776 |   6388 |       0 |      25 |   0 |   253 |
+----------------------+--------+--------+---------+---------+-----+-------+
| Total                | 278658 | 216840 |    2338 |    9148 |   3 |    21 |
+----------------------+--------+--------+---------+---------+-----+-------+
  Code LOC: 67602     Test LOC: 149238     Code to Test Ratio: 1:2.2

CIの状況

kakariでは主にCircleCIを利用しており、以下のようなJobを実行しています。

  • Ruby on Railsのテスト(RSpec
    • 各種モデル群の単体テスト
    • APIのRequest Spec
    • Rails Viewを提供しているエンドポイントに対してのSystem Spec
  • JavaScriptのテスト(Jest
    • JavaScriptで完結する各種Vue.js Componentの単体テストなど
  • 静的解析

他にも細かなJobが幾つかありますが、いずれも1分前後の小さなJobのため今回は割愛します。 また、Playwrightを活用したE2Eテストもありますが、これに関しても実行環境・タイミングなどが特殊なため今回の対象外とします。

改善結果

細かな改善内容はさておき、具体的な改善結果をお見せします。

改善前のCI Workflow 14分18秒

改善後のCI Workflow 6分35秒

「14分」から「6分半」への大幅な改善を達成することができました 🎉

また、 「お金💰の力で並列数を増加」のような対応はとらずに一定改善できたため副次的効果ですがコストの削減にも繋がりました。

改善内容

前提知識: CIのキャッシュ機能

具体的な改善内容紹介の前に、ここで「CIのキャッシュ機能」について前提知識として理解しておきましょう。

CircleCI公式ドキュメントが丁寧のためそちらをご参照ください。 circleci.com

例示されている通り、yarn packageやruby gemsなどのライブラリのインストール処理にてキャッシュを活用し、各Job内でのインストール時間を短縮することが典型的な活用事例かと思います。

また、今回はCircleCIを利用しましたがGitHub ActionGit Lab CI/CDなど大抵のCIサービスには同等の仕組みがあるため適宜読み替えてください。

それでは、改善内容を紹介します。

webpack buildのキャッシュを活用

出落ちになりますが、本改善がかなり大きなものとなります。

前述した通り、本Railsシステムには一部Vue.jsが組み込まれているエンドポイント(画面)が存在します。 そのエンドポイントに対してのSystem Specを動作させるにはwebpackのbuildが必要な構成となっています。

このwebpack buildの実行時間が毎回5分弱ほどでした。

この時間を短縮するためにwebpackのpersitent cachingという仕組みを活用しました。 これは「キャッシュ」という名から推測できる通りビルド結果をキャッシュし、ビルド時間を短縮してくれる仕組みです。

改善効果

「5分弱」から「1分弱」(4~5分)の時間短縮

※ キャッシュが適用される場合

対応方法

以下のようにwebpackの設定を記述してキャッシュ情報をファイル出力し、そのファイルをCircleCIのキャッシュ対象に含めるだけです。

  // webpack.config.js
  cache: {
    type: 'filesystem',
    cacheLocation: path.resolve(__dirname, 'tmp/cache/webpack'),
  },
  snapshot: {
    buildDependencies: { hash: true },
    module: { hash: true },
    resolve: { hash: true },
    resolvebuildDependencies: { hash: true },

ポイントはキャッシュデータ有効性の判定方法を「hash 方式」としている点です。 キャッシュの有効性判定の方法は以下の二種類が選べます。

  • hash(ファイルコンテンツハッシュ) 方式
    • ファイル内容のハッシュ値が同一かどうかで判定
    • 公式原文 Compare content hashes to determine invalidation (more expensive than timestamp, but changes less often).
  • timestamp(ファイルタイムスタンプ)方式 デフォルト
    • ファイルのタイムスタンプ(更新日時)が同一かどうかで判定
    • 公式原文 Compare timestamps to determine invalidation.

CIでは毎回git cloneする都合上ファイルタイムスタンプが更新されます。 そのため、timestamp方式を利用するとキャッシュされているスナップショットが不正と判定されてしまいキャッシュを活用することができなくなってしまいます。

参考資料

RuboCopのキャッシュを活用

RuboCopにも同様のキャッシュの仕組みがあります。

docs.rubocop.org

キャッシュの有効性判定は以下の通りで、「Ruby・RuboCopのバージョン、Cop設定が変わらない限り同一ファイル内容に対してのCop結果は不変」ということから、Cop結果をキャッシュとして記録し次回実行時にキャッシュされているCop結果を流用する仕組みです。

Cache Validity
Later runs will be able to retrieve this information and present the stored information instead of inspecting the file again. This will be done if the cache for the file is still valid, which it is if there are no changes in:

・the contents of the inspected file
・RuboCop configuration for the file
・the options given to rubocop, with some exceptions that have no bearing on which offenses are reported
・the Ruby version used to invoke rubocop
・version of the rubocop program (or to be precise, anything in the source code of the invoked rubocop program)

改善効果

「2分」から「10秒弱」(2分)の時間短縮

※ キャッシュが適用される場合

対応方法

実はRuboCopはデフォルトでキャッシュファイルを生成します。 生成されるキャッシュファイルをCircleCIのキャッシュ対象に含めるだけです。

以下のように --cache-root オプションでキャッシュファイルの生成先を指定することができます。

bundle exec rubocop --cache-root tmp/cache/rubocop

参考資料

ESLintのキャッシュを活用

そろそろキャッシュにも慣れてきた頃でしょうか。
冗長な説明となるため簡単にご紹介します。

eslint.org

改善効果

「30秒前後」から「3秒前後」(30秒)の時間短縮

※ キャッシュが適用される場合

対応方法

eslint app/bundles/javascripts spec/javascripts --ext js,vue,ts \
  --cache \ # キャッシュを有効化
  --cache-location tmp/cache/eslint/ \ # キャッシュファイル生成先を指定
  --cache-strategy content # キャッシュの有効性判定を「ファイル内容方式」とする(理由は上記webpack buildと同様)

参考資料

Jestのキャッシュを活用

キャッシュはもう飽きてきましたね。
これが最後のキャッシュ対応です。

jestjs.io

改善効果

「3分前後」から「2分前後」(1分)の時間短縮

※ キャッシュが適用される場合

対応方法

cacheDirectory を指定し、生成されたキャッシュファイルをCircleCIのキャッシュ対象に含めるだけです。

# jest.config.js
module.exports = {
  cacheDirectory: 'tmp/cache/jest',
};

参考資料

RSpec Jobをテスト特性ごとに分割

RSpec実行Jobを「webpack buildが必要なSystem Spec rspec_with_assets」と「それ以外(Model Specなど) rspec_without_assets」の二つに分割し、「時間がかかるWebPack Buildを待たずに rspec_without_assets を開始」することで全体の総時間を短縮する試みです。
こちらは同チームのサーバーサイドエンジニアの谷(@yuyat137)が対応しました。

RSpecを分割実行しJob依存関係を最適化(before)

RSpecを分割実行しJob依存関係を最適化(after)

改善効果

これまでの改善とは異なり特定のJobが改善されるようなものではないため改善効果の計測が難しいのですが、最大(webpack buildのキャッシュが適用されない場合)は4,5分の時間短縮が期待できます。
また、後述するリソースクラスと並列数の最適化にも繋がります。

対応方法

特別な工夫はなく、単にRSpec実行Jobを分割するだけです。 Jobを分割する分メンテナンスコストは増えるため、期待できる改善効果とのトレードオフで採用・不採用を検討すると良いかと思います。

CircleCIのリソースクラスと並列数の最適化

まず前提知識として、CircleCIには特定のJobを並列稼働させる仕組みがあります。 例えば以下はRSpec実行Jobを8並列で稼働している様子です。

CircleCIにてRSpecの実行Jobを8並列で稼働している様子

このように並列数を上げることでCIの総時間を短縮することができます。 ただし、CircleCIは「サーバー稼働時間の合計 ≒ コスト」となるため、並列数を上げるとその分コストが増えます。(※ Job実行のための事前処理が並列数分実行されるため)

circleci.com

もう一つの前提知識として、CircleCIでは各種Jobのリソースクラス(サーバーのスペックのようなものと解釈してください)を調整することが可能です。 例えばCPUやメモリをあまり消費しないようなJobの場合は小さめの small なんかを指定するとコスト削減に繋がります。

今回の改善は「並列数」と「リソースクラス」の両方を調整し、最適化しました。

端的に言うと「RSpec実行Jobのリソースクラスを下げて並列数を上げた」となります。 実際の最適化内容は以下です。

※ 消費クレジット= ここではCircleCI上のコストの単位と解釈してください。 (参照: https://circleci.com/docs/ja/credits/

最適化前

  • 「Medium(CPU=2, Mem=4, 消費クレジット=10/min)」を8並列

最適化後

前述の「RSpec Jobをテスト特性ごとに分割」が完了していたため、それらのJobの特性に応じてリソースクラスと並列数を最適化。

  • 重ためのSystem Spec系実行Job
    • 「Medium(CPU=2, Mem=4, 消費クレジット=10/min)」を4並列
  • テストケースが多いSystem Spec以外実行Job
    • 「Small(CPU=1, Mem=2, 消費クレジット=5/min)」を8並列

最適化前後で消費クレジットの変化なし(共に80/min)

改善効果

「10分30秒」から「7分弱」(4分弱)の時間短縮

細かなテストが多いRSpec群に対しては「スペック抑えめで並列数を増やす」というアプローチが良さそうでした。

採用しなかった・見送った改善候補

HAML-Lint, Fasterer, Brakemanのキャッシュを活用

RuboCopやESLint同様に静的解析のJobは極力キャッシュを活用したかったのですが、ツール側が機能提供していなかったので断念しました。
OSSコントリビュートチャンスですね。

Stylelintのキャッシュを活用

Stylelintにもキャッシュの仕組みがあるためこれを活用することができたのですが、元々のJob実行時間が10秒未満と短かったため今回は見送りました。

bootsnapを活用

Bootsnapは各種ファイルパス情報などをキャッシュすることで、Rubyアプリケーションの起動を高速化するGemです。
詳細は公式ドキュメントなどを参照ください。

github.com

Bootsnapが生成するファイルをCircleCIのキャッシュ対象に含めるだけでJob内のコマンド実行が高速化する可能性があります。 Bootsnapは巨大なアプリケーションほど効果を発揮しますが、kakariはせいぜい小〜中規模程度なため「Bootsnapで受けられる恩恵 < Jobの複雑さ」と判断し今回は見送りました。

Jestの並列実行化

kakariではJestをシリアル実行していたので、並列で稼働する(デフォルト)の設定に変更してみましたが、逆にCI時間が延びたため今回は見送りました。 JestのCI時間はそこまで支配的でなかったため、深く調べてはいませんが恐らくCPU/メモリリソースが十分に割り当てられていなかったことが原因かと思われます。 jestjs.io

まとめ

以上が最近kakariプロジェクトで取り組んだCI改善の小ネタ集でした。

繰り返しになりますが、追加コストなし でCIの実行時間を「14分」から「6分半」に短縮することができました 🎉

いずれも裏技のような改善ではなく、公式ドキュメントに記載されているような正攻法ばかりでした。
「重たいテストを一つ一つ修正する」ことも大事ですが、まずは落ち着いて公式ドキュメントを読み込むことも大事ですね。

本記事がきっかけで世界で動いているCI時間が少しでも短縮することを願っています。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら

medpeer.co.jp

■エンジニア紹介ページはこちら

engineer.medpeer.co.jp

Terraform Provider を自作し SendGrid リソースを管理した話

SRE の田中 @kenzo0107 です。
Terraform Provider kenzo0107/terraform-provider-sendgrid を作成し、SendGrid のリソースを terraform で管理する様にした話です。

https://registry.terraform.io/providers/kenzo0107/sendgrid

まず弊社の SendGrid リソース管理の Before/After をご覧ください。

弊社の SendGrid リソースの管理方法 Before

SendGrid コンソール上で手動でリソースの作成・更新・削除していました。

特にチームメイトは以下運用で工数が肥大化していました。

  • 定期的な棚卸し
  • メールアドレスを元にプロジェクト担当者(or 退職者)かをチェック

弊社の SendGrid リソースの管理方法 After - Terraform 管理

コード管理することで以下メリットがありました。

  • 運用工数削減
    • メンバーがどのプロジェクトに所属しているかコード上で確認できる
    • 運用フロー改善*1
    • 変更理由が commit history に記録される
  • 設定の知見が伝えやすい
    • 「あのプロジェクトと同じ設定にしたい!」をコードのコピペでできる

何故自作したか?

まず既に SendGrid の Terraform Provider あるんじゃないの?と思い探しました。

世の中には SendGrid 用 Terraform Provider が沢山あるではないですか?!

利用頻度の高いリソースをサポートしているかを軸に探してみました*2が、以下のことがわかりました。

  • チームメイトをサポートしているのは以下のみ
    • SyedK1987
      • テストをしていない
      • チームメイトの管理は問題なし
      • API Key リソースをサポートしていない
    • octoenergy
      • テストが少ない
      • チームメイトリソースを定義し terraform apply 後、terraform plan 実行するとエラーになる*3

上記踏まえて以下理由から新たに SendGrid 用の Terraform Provider を自作することとしました。

  • テストの少ないプロジェクトへの関与が大変そう 💦
  • Terraform Provider 制作の知見を得たい ✨
    • 今後 SendGrid 以外にも terraform 管理を推進したい場合がありそう
    • Terraform のこと、もっと知りたい 💕

Terraform Provider を作成する為にまずやったこと

Terraform Custom Provider 作成のチュートリアル で学ぶ 📝

  • Terraform Plugin Framework を使ってHashiCupsという架空のコーヒーショップアプリケーションの API に対して Provider を作っていくチュートリアル
  • 2時間以内で終わる!*4
  • Golang をインストールしておく

手順通り進めればできるのでやり遂げられる気になれます!

SendGrid API の利用方法 確認 📝

  • 管理したいリソースに対する API を実行しレスポンスを確認し検証*5

上記をまず実施した上で以下ステップに進みました。

Terraform Custom Provider 作成する際の Plugin の選定

Provider を作成するには Hashicorp 社が提供する Plugin を利用します。 Plugin は以下2種類あります。

  1. SDK
  2. Framework (後発)

公式ドキュメント で推奨されている Framework を採用しました。

We recommend using the framework to develop new providers because it offers significant advantages as compared to Terraform Plugin SDKv2.

Golang 製 API Client ライブラリの選定

Terraform Provider が Golang 製なので API Client も Golang 製にする必要があります。

sendgrid/sendgrid-go が公式のライブラリですが、
以下理由の為、回避しました。

  • API 実行時に毎回 API Key を渡す必要があり処理が冗長
  • API 実行時に必要なキーやパス、メソッド、レスポンスのパースは利用者が実装する必要がある
  • Terraform Provider の API Client として利用する場合、Provider 側で API Client について上記を実装・テストが必要になる

例) チームメイト招待

        apiKey := os.Getenv("SENDGRID_API_KEY")
        host := "https://api.sendgrid.com"
        request := sendgrid.GetRequest(apiKey, "/v3/teammates", host)
        request.Method = "POST"
        request.Body = []byte(`{
  "email": "teammate1@example.com",
  "scopes": [
    "user.profile.read",
    "user.profile.update"
  ],
  "is_admin": false
}`)
        response, err := sendgrid.API(request)
        if err != nil {
                log.Println(err)
        } else {
                fmt.Println(response.StatusCode)
                fmt.Println(response.Body)
                fmt.Println(response.Headers)
        }

上記解消の為、 SendGrid ライブラリを自作し採用することとしました。*6

github.com

自作することで公式と比較し以下メリットがありました。

  • API は関数名で把握できパスを意識する必要がなく、リクエストに必要なキーは引数から確認できる
  • API のレスポンスのパースはライブラリ側で実施しており、 利用者は意識する必要がない
  • ライブラリのテストをライブラリ側で完結できる*7

例) 自作 SendGrid ライブラリの チームメイト招待

    c := sendgrid.New(apiKey)
    u, err := c.InviteTeammate(context.TODO(), &sendgrid.InputInviteTeammate{
        Email:   "engineer-boshuchu@example.com",
        IsAdmin: false,
        Scopes: []string{
            "user.profile.read",
            "user.profile.update",
        },
    })
    if err != nil {
        return err
    }
    log.Printf("invite user: %#v\n", u)

他 SendGrid Provider はどの API Client ライブラリを使ってる?

Terraform Provider 管理リポジトリ内で sendgrid/sendgrid-go を利用し API の処理をサポートするケースが多かったです。

この場合、Terraform Provider 管理リポジトリ内でライブラリのテストを実施する必要があります。
そして、ほぼテストしてなかったです 😢

Terraform Provider 作成でわかったこと

開発関連

  • まずチュートリアルを終え、以下を学ぶことが大前提
    • 開発環境を整備する方法
    • Provider リソースの定義の仕方
    • スキーマの定義の仕方*8
    • API Client の設定方法
    • data, resource リソースの作り方
    • terraform init, plan, apply でリソースを管理する方法

チュートリアルでも利用しますが、
Terraform Plugin Framework のクイックスタート用のリポジトリがあるので、そこにテストやドキュメント生成、Terraform Registry へのリリース等の GitHub Actions が用意されています。

テスト関連

SendGrid API 関連

  • リソース作成時と取得時でレスポンスが異なる
    • API Key は作成時のみキー値が取得できるが、Read する API がない
      terraform import 時は API Key のキー値が tfstate に保存されない*9
  • SendGrid API はサーバエラーが時折発生する
    • terraform plan/apply 実行時にサーバエラー (502) で正常終了できない場合がある
    • GitHub Actions 上でテスト実行時に too many requests (429) が発生することが多い*10
  • チームメイトの権限は招待後に SendGrid 側が自動で付与する場合がある*11
    • 付与する権限により権限を更新できない場合がある

自作 SendGrid Terraform を運用してみて得た知見・問題

アカウント毎の管理方法

  • 親アカウントは親アカウント用の terraform で管理*12
  • 子アカウント(サブユーザ)は各種プロジェクト毎の terraform で管理

弊社では子アカウントはプロジェクトに紐づくのでプロジェクトの terraform に寄せることでプロジェクト毎のライフサイクルで管理できる様にしています。

provider で指定するもの

各ワークスペースで環境変数として以下設定しています。

  • 親アカウント
    • SENDGRID_API_KEY
  • 子アカウント(サブユーザ)
    • SENDGRID_API_KEY
    • SENDGRID_SUBUSER

親アカウント・子アカウントのワークスペースで利用する SendGrid API Key は親アカウントで発行した API Key を指定します。

上記について Terraform Cloud を管理する Terraform Provider を利用し Terraform で管理されています。

秘匿情報の扱いについて

これはあくまで回避策として実施したことです。
別途解決策があればご指南いただきたいです🙇‍♂️

  • 秘匿情報は data "aws_kms_secrets" を利用し暗号化し管理
    • 開発者がアクセスできる KMS 鍵を用意し暗号化・復号できる様にした
    • 利用用途: サブユーザ作成時に必須のパスワードの暗号化

SendGrid は秘匿情報を暗号化する仕組みがないので、 terraform-provider-aws の力を借りました。*13

まとめ

Terraform Provider - SendGridSendGrid Go ライブラリを自作し SendGrid の管理工数が削減され、Terraform・SendGrid への知見が高まりました。

これからサポートできるリソースを増やし、より管理しやすくしていきたいと思います。

また、個人 OSS を社内導入時にレビューいただいた皆様に感謝です。

以上 参考になれば幸いです。


是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

medpeer.co.jp

■エンジニア紹介ページはこちら

engineer.medpeer.co.jp

*1:以前はチケットを作成し、そのチケットを上長が承認した上で Sendgrid コンソール上でメンバー追加等をしていましたが、 GitHub の Pull Request で上長が Approve したら OK という簡易な運用になりました。

*2:各種 SendGrid Provider の対応状況 2023-08-31 時点

チームメイト サブユーザ API Key Sender Authentication GitHub
download count
at 20230831 14:00
GitHub
archived ?
GitHub
star count
anna-money x o o o 16108 no 4
daco-tech x o o o 848 no 0
davidji99 x x o x 21,191 no 0
indentinc x o o x 1765 no 0
Meuko x o o o 740 no 0
nanasess x x x x 23 no 0
octoenergy o o o o 1,093 no 0
phogolabs x x x x 2275 no 0
SpotOnInc x o o o 106 no 0
SyedK1987 o o x o 144 no 0
taharah x o o x 3711 no 2
tatsuo48 x x x x 26,232 no 2
theaox x o o x 21,198 no 0
Trois-Six x o o o 287,579 yes 13
yinzara x o o x 1,488 no 0

*3:チームメイトとして招待後、保留中のチームメイトとなり、チームメイト取得 API で取得できないことを考慮できていない。

*4:チュートリアルのセクション毎に掛かるおよその時間が記載されているのでその合計を取りました。また自身が試して2時間以内には終わりました。

*5:ドキュメントと実際の挙動が異なる API がいくつかあった

  • 例: https://docs.sendgrid.com/api-reference/domain-authentication/add-an-ip-to-an-authenticated-domain の response の dns キーの値
  • 例: https://docs.sendgrid.com/api-reference/domain-authentication/remove-an-ip-from-an-authenticated-domain の response の dns キーの値

    *6:以前 https://github.com/kenzo0107/backlog を再利用したので然程手間ではなかったです。エラー時のハンドリングが異なる程度でした。

    *7:Provider 側でライブラリのテストを実装する必要がない

    *8:スキーマ関連は一番苦労したのでここは知見を溜めて別途執筆したいと思います 📝

    *9:ドキュメントに記載

    *10:調査中。MacOS ローカルで実施した場合は然程発生しない

    *11:明確な仕様がドキュメント上に見当たらなかったので、意図せぬ動作を避けるべく ignore_changes = [scopes] で変更無視する運用にし、権限だけはコンソール上で変更する運用にしている

    *12:親アカウントが複数あるのは、請求を分けたい意図です

    *13:SendGrid のワークスペースであるのに AWS の credentials が必要なのは問題であると判断し、Vault の様な秘匿情報の管理の仕組みを別途用意する必要がある旨、社内の GitHub Issue に登録してます。

  • 毎年恒例の「Swift Quiz」(for Swift 5.8) を公開しました!

    Swift Quiz に挑戦してね!

    メドピアでアプリ開発を担当している高橋です。

    メドピアが iOSDC Japan 2023 にスポンサードする度に毎年恒例で掲載している Swift Quiz の 2023 年度版を公開しました!(メドピアは今年はゴールドスポンサーです!)

    今回は Swift 5.8 (Xcode 14.3 相当) 対応になります。社内の iOS エンジニア 3 名にクイズを作成いただきました!

    swiftquiz.medpeer.co.jp

    今年は例年にも増して難易度が高くなっていると思います・・・🧑‍💻

    この辺りは Swift の基礎的な範囲ですが、

    ヒント:struct に注目!

    少し複雑な例文があったり、

    ヒント:lazy は遅延評価

    UI についての理解が必要だったりします。

    ヒント:iPhone だけじゃないよ!

    個人的には Swift 5.8 の特性に着目したこの問題はとてもいい問題だと思っています!

    ヒント:Swift 6 を予習している人には簡単かも?

    iOS / Swift エンジニアな方もそうでない方も、 ぜひチャレンジしてみてください〜! 👍 👍 👍

    swiftquiz.medpeer.co.jp

    ちなみに回答後の 𝕏 ツイートボタン押下時に iOSDC トークン が表示されるようになっております🙏

    iOSDC Japan 2023 のメドピアセッションも見にきてくださいませ!


    是非読者になってください


    メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

    ■募集ポジションはこちら

    medpeer.co.jp

    ■エンジニア紹介ページはこちら

    engineer.medpeer.co.jp

    大規模サービスのデータベースエンジンを MySQLからAurora MySQLへの移行 〜リードレプリカ, DNSを利用した最小ダウンタイム移行方法〜

    バックエンドエンジニアの徳富(@yannKazu1)です。先日、メドピアのメインサービスであるmedpeer.jpで使われているデータベースエンジンを、MySQLからAurora MySQLへと移行しました。今回はその移行のプロセスについて詳しくお話しします。

    移行したデータベースの簡単なインフラ構成

    移行方針

    今回移行するデータベースは複数のアプリケーションから参照されており、ダウンタイムによるユーザー影響が大きいため、移行方針の検討の段階で重視したのは、ダウンタイムの最小化でした。これを達成するために、DNSのCNAMEレコードと、Auroraのリードレプリカを活用し、移行させることにしました。

    DNSのCNAMEレコードの使用

    データベースエンドポイントをアプリケーションに直接記述する代わりに、DNSのCNAMEレコードを利用して間接的に参照するようにしました。これにより、データベースエンドポイントの変更が必要な場合でも、DNSのCNAMEレコードを更新するだけで対応できます。 今回移行したデータベースは複数のアプリケーションで参照されており、すべてのアプリケーションでデータベースホストを手で変更していくのはかなり時間がかかるため、この方法で行うことに決めました。

    Auroraをリードレプリカとしての起動

    Auroraはリードレプリカとして起動し、必要に応じてプライマリーに昇格させることが可能です。これにより、既存のデータベースと新しいAurora データベースとのデータ同期を保ちつつ、最小限のダウンタイムでデータベースをAuroraに切り替えることができました。

    具体的な手順

    以下に、具体的な手順を説明します。

    ①Auroraをリードレプリカとして起動

    MySQLのリードレプリカとしてAurora MySQLを起動します。

    ②CNAMEレコードを作成

    データベースエンドポイントを参照するCNAMEレコードを作成し、データベースエンドポイントを参照している全てのアプリケーションをDNS経由に切り替えます。これにより、Auroraに移行後にDNSのエンドポイントを変更するだけで全てのアプリケーションの参照先を切り替えることができ、ダウンタイムが大幅に削減できます。

    ③既存のRDSを読み取り専用に設定

    既存のRDSにread_onlyパラメータを設定し、書き込みを停止します。 書き込みがなくなることで、Auroraレプリカのレプリケーションを追いつかせます。

    ④Auroraをプライマリーに昇格

    リードレプリカとして起動していたAuroraのレプリケーション設定を削除し、フェールオーバーすることでプライマリーに昇格させます。

    ⑤DNSレコードのエンドポイントをAuroraに変更

    DNSレコードの向き先をAuroraのデータベースエンドポイントに変更します。これにより、アプリケーション側で変更を加えることなく、エンドポイントが更新されます。

    移行に際して検証したこと

    本データベースは多くのアプリケーションが参照するメインデータベースであったため、慎重に検証を行った上で移行を実施しました。特に以下の2点を重視しました。

    データベースエンジンの変更による予期せぬエラーの発生

    まず、ステージング環境でAurora化を実施し、予期せぬエラーが発生しないかを検証しました。今回AuroraのエンジンはMySQLと互換性のあるバージョンに揃えたので、基本的には問題ないと予想されましたが、確認のためにステージングで検証を行いました。その結果、アプリケーション側の改修は必要ありませんでした。

    Auroraのフェールオーバー時の接続切り替えの検証

    AuroraとRailsを組み合わせて使用すると、Railsのコネクションプールの仕組み上、フェールオーバー時に新しいライターへのコネクション切り替えが自動的に行われず、リーダー(旧 ライター)にデータの書き込みを行ってしまう事象が知られています。これにより、予期せぬタイミングでデータ更新ができない状況が発生します。

    今回、Auroraに移行したデータベースはRails、Go、PHPで書かれた複数のアプリケーションからアクセスを受けており、この問題への対策が必要でした。

    対策方法としてはいくつかあることが知られていますが、今回書き込みエラーが発生した際に接続をリセットし、再接続を行うミドルウェアを開発しました。このミドルウェアは、RailsのActionDispatch::ContentSecurityPolicy::Middlewareより先に呼び出されるよう設定しました。

    以下に、ミドルウェアの読み込みとそのコードを示します:

    module RdsConnectionAdapters
      class Reconnect
        class RdsReadOnlyError < ActiveRecord::ActiveRecordError; end
        CONNECTION_ERROR = ['READ ONLY', '--read-only'].freeze
        CONNECTION_ERROR_REGULAR = /#{CONNECTION_ERROR.map { |w| Regexp.escape(w) }.join('|')}/
    
        def initialize(app)
          @app = app
        end
    
        def call(env)
          @app.call(env)
        rescue ActiveRecord::StatementInvalid => e
          raise e unless should_clear_all_connections?(e)
    
          ActiveRecord::Base.clear_all_connections!
          raise RdsReadOnlyError, 'DBが読み込み専用になっていたため、再接続を行いました'
        end
    
        private
    
        def should_clear_all_connections?(e)
          if e.kind_of?(ActiveRecord::StatementInvalid)
            return CONNECTION_ERROR_REGULAR === e.message
          end
    
          false
        end
      end
    end
    

    config/environments/production.rb(ミドルウェアの読み込み)

    config.middleware.insert_before ActionDispatch::ContentSecurityPolicy::Middleware,
                                      RdsConnectionAdapters::Reconnect
    

    移行の成果と今後の展望

    CNAMEレコードの活用とAuroraレプリカの特性をうまく組み合わせることで、1時間程度のダウンタイムで抑えることができました。 DNSの工夫やAuroraの活用など、新しいアプローチに挑戦することで、予想よりもスムーズな移行が実現できたのは大きな成果です。

    この経験から得た学びは、社内で共有し、会社全体の知見向上に努めたいと考えています。


    是非読者になってください!


    メドピアでは一緒に働く仲間を募集しています。
    ご応募をお待ちしております!

    ■募集ポジションはこちら

    medpeer.co.jp

    ■エンジニア紹介ページはこちら

    engineer.medpeer.co.jp

    AWS DevDay 2023 Tokyoへの参加レポート〜「セキュリティ/ガバナンス系サービスを使った安全なSandbox環境の作り方」の紹介

    はじめに

     今年の4月に新卒で入社したバックエンドエンジニアの冨家(@asahi05020934)です。現在は、全国の医師が経験やナレッジを 「集合知」として共有し合う医師・医学生専用のドクタープラットフォームの開発を行っています。
     新しいAWSのサービスをキャッチアップするときに、触ってみることは効果的です。 「自宅で学ぼう!AWS 初学者向けの勉強方法 6ステップ!」 によると、

    勉強したサービスをハンズオンなどを通じて実際に触ってみることで、知識と実践を結びつけて理解することができます。

    とあり、アウトプットの重要性が身に沁みます。
     しかし、AWSのサービスを実際に触っていると、想定外のコストが発生し後悔する可能性があります。私も以前使っていないリソースを消し忘れて、月数千円の想定外の費用が発生し後悔したことがあります。
     どのようにすれば、AWSの安全なSandbox環境を作ることができるのか知りたいと思っていたところ、先日 AWS Dev Day 2023 Tokyoというイベントに参加し、セキュリティ/ガバナンス系サービスを使った安全なSandbox環境の作り方というセッションを聞いたので紹介します。

    Sandbox環境構築に役立つサービス

     「セキュリティ/ガバナンス系サービスを使った安全なSandbox環境の作り方」では、主にコスト管理系のサービスで「AWS Budgets」「AWS Cost Anomaly Detection」、ガバナンス系のサービスで「AWS Organizations」「AWS IAM Identity Center」、発見的統制のサービスで「Amazon GuardDuty」「AWS CloudTrail」を紹介していました。

    表1 AWSのSandbox環境構築に役立つサービス

    コスト管理系 ガバナンス系 発見的統制
    AWS Budgets AWS Organizations AWS CloudTrail
    AWS Cost Anomaly Detection AWS IAM Identity Center Amazon GuardDuty

    出典: セキュリティ・ガバナンス系サービス を使った安全な Sandbox 環境の作り方をもとに作成

    コスト管理系

    AWS Budgets

    カスタム予算を設定してコストと使用状況を追跡し、しきい値を超えた場合に E メールまたは SNS 通知でアラートを受信できるサービス
    出典: AWS Budgets

      AWS Budgetsには「予測とアクション」という機能があり、「月末ではいくらになるか」を予測してアラートを出してくれます。 これによりコスト面で危険な状態になりそうなとき、より早く気づくことができます。
     私も「このまま使っていけば月末どのくらいかかるんだろう」と不安になることが多いので、月末の金額を予測できるのは便利だと思いました。

    AWS Cost Anomaly Detection

    高度な Machine Learning テクノロジーを活用して、異常な支出と根本原因を特定できるサービス
    出典: AWS Cost Anomaly Detection

     AWS Cost Anomaly Detectionを使用することで、想定外の請求リスクを減らすことができます。
     私も過去にどこでコストが発生しているかよくわからず特定するのに苦労したことがあったので、このサービスを使えばスマートに特定できそうで良いなと思いました!

    ガバナンス系

    AWS Organizations

    複数のアカウントをまとめるサービス
    出典: セキュリティ・ガバナンス系サービス を使った安全な Sandbox 環境の作り方

     AWS Organizationsが役に立つ理由は、2つあります。
     1つ目は、SCPが利用できるからです。SCPは、アカウントや組織単位にアタッチするポリシーです。SCPを使いSandbox環境のアカウントでも使用してはいけないサービスをあらかじめ禁止することで、Sandbox環境のアカウント利用者が本番アカウントでも使用できない事を事前に気づくことができます。
     2つ目は、アカウントの閉鎖による不要リソースの削除を行うことができるからです。 リソースを消し忘れる原因の1つは、消して良いリソースと消してはいけないリソースが混ざってしまうことです。そこで、本番環境のアカウントとSandbox環境のアカウントを分け、お試し的な用途はSandbox環境のアカウントを利用し、使わなくなったらSandbox環境のアカウントごと削除することでリソースの消し漏れを防ぐことができます。
     私はこのセッションを聞くまで「リソースの消し漏れを抑制するためにアカウントごと閉鎖してしまう」という発想に至らなかったので、とても斬新な方法だと感じました!

    AWS IAM Identity Center

    ワークフォースのアイデンティティを安全に作成または接続し、AWSアカウントとアプリケーション全体でそのアクセスを一元管理するサービス
    出典: AWS IAM Identity Center

     AWS IAM Identity Centerが役に立つ理由は、3つあります。
     1つ目は、AWS Organizations全体のユーザー管理をAWS IAM Identity Centerでまとめて行うことができるからです。AWS IAM Identity Centerの方にログインをすれば、複数のAWSアカウントに対して、シングルサインオンが可能になります。これにより、IAMユーザーを勝手に作ってID・パスワードをいい加減に使い、インシデントを起こすリスクを減らすことができます。
     2つ目は、AWS CLIとAWS IAM Identity Centerが連携可能だからです。AWS CLIとAWS IAM Identity Centerを連携することで、一時的な認証情報(アクセスキー)を必要に応じて発行することができます。これにより、永続的なアクセスキーを発行することが不要になり、アクセスキーをGitにコミットしてしまったときなどのセキュリティリスクを下げることができます。
     3つ目は、WebAuthn対応のTouch ID / Windows Hello カメラ などの MFA が利用可能だからです。MFAの1つに、TOTPがあります。TOTPとは

    ワンタイムパスワード(OTP)の生成手法の標準の一つで、秘密鍵と現在時刻からハッシュ値を求め、これを6桁程度の数値列に変換してパスワードとする方式

    です。TOTPは、パスワードの生成・入力する分手間がかかります。そこで、WebAuthn対応のTouch ID / Windows Hello カメラ などの MFAを使うことで、パスワードの生成・入力が不要になり、多要素認証の手間を減らすことができます。
     私は毎回多要素認証する時にすぐにスマホを開けられずに困っているので、WebAuthn対応のTouch IDのような多要素認証が使えるのはとても魅力的に感じました!

    発見的統制

    AWS CloudTrail

    AWS インフラストラクチャ全体のアカウントアクティビティをモニタリングして記録し、ストレージ、分析、および修復アクションをコントロールできるサービス
    出典: AWS CloudTrail

     AWS CloudTrailを使うことで、「いつ誰が操作したのか」や「どんな操作をしたのか」ということを追跡することができ、調査をするとき便利です。
     もし、SandBox 環境がハッキングされ侵入されていた場合、作業時刻から不審な操作に気付けるかもしれませんね!

    Amazon GuardDuty

    悪意のあるアクティビティのために AWS アカウントとワークロードを継続的にモニタリングし、可視化と修復のための詳細なセキュリティ調査結果を提供する脅威検出サービス
    出典: Amazon GuardDuty

     Amazon GuardDutyを使うことで、AWS アカウントの脅威を様々な情報ソースから機械学習を使って検知することができます。
     セキュリティ問題は自分の想定外な部分に潜んでいて、私はいつも「本当にこれで問題ないかな」と不安に思っているので、潜在的な脅威の検出に役立てられるAmazon GuardDutyはとても心強いツールだと感じました。

    最後に

     以上、「セキュリティ/ガバナンス系サービスを使った安全なSandbox環境の作り方」を紹介してきました。今後はこの話を聞いたことをもとに安全なAWSのSandbox環境を構築して、後悔なくAWSのサービスに触れていきたいと思います。


    是非読者になってください!


    メドピアでは一緒に働く仲間を募集しています。   ご応募をお待ちしております!

    ■募集ポジションはこちら

    medpeer.co.jp

    ■エンジニア紹介ページはこちら

    engineer.medpeer.co.jp

    potatotips主催させていただきました

    iOSエンジニアの小林(@imk2o)です。 メドピアでモバイルアプリのテックリーダーをしております。

    さる2023年7月21日、弊社でpotatotips #83を主催させていただきました。 以前にもオンラインで主催させていただいたことはあったのですが、オフラインを交えたハイブリッド開催は初めてでした!

    弊社のモバイルアプリエンジニア総勢5名がLT登壇しましたので、その内容を紹介させていただきます。

    speakerdeck.com speakerdeck.com speakerdeck.com speakerdeck.com speakerdeck.com

    発表内容には、実際の業務の中で生じた課題に対する取り組みもありますので、参考になれば幸いです。

    ハイブリッド開催の所感

    技術系イベントもオフライン開催が復活してきたこともあり、企画当初から社内を会場として実施したいという声があがっていました。 ただ実際のところエンジニアだけで開催準備や当日の進行をするのは難しく、弊社HRチームに協力を仰ぎました。 ここで多忙な中でも快諾いただいたのが、開催成功の一番の要因だったと思います。 この場を借りて、あらためてHRチームには御礼申し上げます🙇‍♂️

    また発表に関して、オンライン配信と会場での投影を同時に行うための機材準備についても色々工夫をしてもらいました。

    • 会場発表者を含め、すべての参加者にZoomへ入ってもらう
    • 発表者は投影したいスライドをZoomで画面共有する
    • ホスト用PCを会場のプロジェクターに接続し画面投影
    • 音声は別のマイクで収録し、ホスト用PCと会場スピーカーに接続

    とすることで発表者が機材への繋ぎ変えをすることなく、ハイブリッド配信を実現しました。 事前リハーサルでMacと機材の相性問題が起きていたため、その解消と手順の簡略化をも実現したナイスソリューションですね!

    LTの後に懇親会をさせていただきましたが、技術的な話から雑談まで色々なお話しをさせてもらい、とても楽しい時間を過ごせました。 エンジニア同士だと、初対面でもすぐ会話できるのが不思議ですよね! 対面でエンジニアの方々とコミュニケーションする機会となったのが、開催して一番よかった理由かなと思いました。

    一方でオンライン参加いただいた方にはたくさんの拍手をいただいたものの、こちらのコミュニケーション不足でやや置き去りにしてしまったのかなと反省しております。振り返りをした上で、今後のイベント企画・開催に生かしていきたいと思います。

    お知らせ: iOSDCスポンサーセッションに登壇します!

    メドピアは今年も iOSDC Japan 2023に協賛しております。 弊社iOSエンジニアの新井がスポンサーセッションに登壇しますのでよろしくお願いいたします!


    是非読者になってください


    メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

    ■募集ポジションはこちら

    medpeer.co.jp

    ■エンジニア紹介ページはこちら

    engineer.medpeer.co.jp