コネヒト開発者ブログ

コネヒト開発者ブログ

コネヒトは PHP Conference 2022 に協賛します!

こんにちは!@TOC です。今回は弊社が協賛するイベントについて紹介します。

コネヒトは PHP Conference 2022 に協賛いたします

コネヒトではママリを始め、社内の多くのプロダクトが PHP で開発されております(その他、技術スタックを知りたい場合は弊社テックビジョンをご覧ください)

そんなコネヒトはこの度、 PHP Conference 2022 にシルバースポンサーとして協賛いたします!

イベント概要

今回はオフラインでの参加も可能ということで、盛り上がりそうですね!

台風が近づいておりますので、気をつけつつオンライン・オフラインで楽しみたいです🙌

また、最新情報は Twitter で告知されるので PHP Conference 公式 Twitter もチェックしてみてください。

@phpcon

最後に

会場では弊社デザイナーが作ってくれた素敵なジョブボードも掲載いたします🙆‍♂️

みんなで楽しんでいきましょ〜!

コネヒトでは PHPer を積極採用中です!

hrmos.co

AWS SESでハードバウンスのようなメールがソフトバウンス扱いで届いた場合のしくみと対処について

こんにちは、インフラエンジニアのささしゅう(@sasashuuu)です。 本日はAWS SESにおけるバウンスメール周りのお話をしようと思います。 タイトルにもあるように、ハードバウンスのようなメールがソフトバウンス扱いで届き、気になってサポートへ問い合わせた際のお話をご紹介します。

バウンスメールとは

そもそもバウンスメールとはなんらかの原因により配信できなかったメールのことで、その種類にはソフトバウンスとハードバウンスがあります。

違いは以下のような内容です。

  • ソフトバウンス
    • 一時的な原因による配信失敗のメール(送信先のメールボックスがいっぱいだったなど)
  • ハードバウンス
    • 恒久的な原因による配信失敗のメール(そもそも宛先のメールアドレスが存在しないなど)

詳細な内容は過去のブログでも取り上げておりますので、よろしければご覧ください。

tech.connehito.com

届いたメールの内容

今回届いたバウンスメールの一部は以下のような内容です。

{
    "notificationType": "Bounce",
    "bounce": {
        "feedbackId": "xxxx-xxx-xxx-xxx-xxx-xxx-xxxxxx",
        "bounceType": "Transient",
        "bounceSubType": "General",
        "bouncedRecipients": [
            {
                "emailAddress": "xxxxxxxxxxxx",
                "action": "failed",
                "status": "5.1.1",
                "diagnosticCode": "smtp;550 5.1.1 RESOLVER.ADR.RecipNotFound; not found"
            },
            ...

SESによってハンドリングされたバウンスメールは上記のようなJSONオブジェクトの形で配信されるのですが、いくつか項目をピックアップして見ていきましょう。(※詳細は公式ドキュメントを参照。)

  • notificationType

    • 通知のタイプです。
    • Bounceが入っているのでバウンスメール扱いとなります。
  • bounceType

    • SESによって決定されるバウンスタイプです。
    • Transientがソフトバウンスになります。

      ソフトバウンスであるかどうかを判断するには、SNS 通知の内容を確認します。bounceType  が Transient  の場合はソフトバウンスです。ソフトバウンスのタイプを確認するには、bounceSubType の値を確認します。

      aws.amazon.com

  • diagnosticCode

    • DSNのDiagnostic-Codeフィールドの値です。
    • 配信が不可であった場合に返ってくる応答メッセージかな(?)と解釈しております。
    • ちなみにsmtp;550 5.1.1 RESOLVER.ADR.RecipNotFound; not foundとなっています。いかにも宛先などが見つからなかったようなメッセージですね。
    • なお、SESのデベロッパーズガイドのメールボックスシミュレーターのシュミレートシナリオや、バウンス応答アクションのテンプレート説明などでは、不明なユーザーやメールボックスが見つからなかったことなどを示唆するメッセージが見て取れます。

      バウンス – 受取人のEメールプロバイダー は、SMTP 550 5.1.1 レスポンスコード (「不明な ユーザー」) レスポンスコードで E メールを拒否 します。

      https://docs.aws.amazon.com/ja_jp/ses/latest/dg/ses-dg.pdf

      Mailbox Does Not Exist - SMTP 応答コード = 550、SMTP ステータスコード = 5.1.1

      docs.aws.amazon.com

詳細は後述しますが、今回はこの smtp;550 5.1.1 RESOLVER.ADR.RecipNotFound; not found のレスポンスメッセージがSES側で未知のものであったことがソフトバウンス認定されてしまっていた原因でした。

問い合わせと回答

これまでの内容を踏まえ、以下2点について内容をまとめサポートへ問い合わせをしました。

  • なぜこのようなハードバウンスのような内容のメールがソフトバウンス扱いで届くのか。
  • アカウントレベルのサプレッションリストの使用 のように、任意のソフトバウンスメールのメールついて2回目以降の送信が行われないような機能、または良い方法はあるのか。

得られた回答の要点をざっくりとご紹介します。

まず1点目の「なぜこのようなハードバウンスのような内容のメールがソフトバウンス扱いで届くのか」については以下の内容です。

  • SES は受信MTAからのレスポンスからハードバウンスが発生したかどうかを判定する。
  • しかしながら受信MTAによって異なるレスポンスメッセージを利用するため、SES が把握していないレスポンスメッセージが受信MTAから返却される場合がある。
  • その場合、顧客のバウンス率の評価に悪影響を与えないようにSESではハードバウンスではなくソフトバウンスが発生したものとして扱う。

このようにSESが把握していないレスポンスメッセージに関しては意図的にソフトバウンス扱いとして対処しているとのことでした。

また、本件に関するレスポンスメッセージに関しては問い合わせ後、ハードバウンスと判定されるような修正を反映してくださったようで、今後本件のような事象(ハードバウンスのような内容がソフトバウンスで届く件)が発生した場合は同じようにフィードバックが欲しいとのことでした。(バウンス判定の質の向上のために使用したいとのことでした)

そして2点目の「アカウントレベルのサプレッションリストの使用 のように、任意のソフトバウンスメールのメールついて2回目以降の送信が行われないような機能、または良い方法があるか」については以下の内容です。

  • SES のサービスのみでは実現が難しいため以下のような対応が案としてあげられる。
  • 対象のアドレスについて、手動でサプレッションリストに登録する。
  • Lambda関数にて特定の条件を満たした場合に対象のアドレスをサプレッションリストに登録するコードを実装し、SESのSNS通知と連携する。

問い合わせ時点ではSESの機能で要件を満たすものはないそうで、手動でのオペレーションや他のAWSサービスを組み併せて対応する必要があるみたいでした。

まとめ

最後に本記事の要点を簡単にまとめておきます。 AWS SESで把握していないMTAからのレスポンスメッセージはバウンスレートに影響しないようにハードバウンスであってもソフトバウンスとして扱われます。もしこのような状況に遭遇した場合はサポートへフィードバックを送り、解消を図るのが良いと思います。また、そうでなくとも任意のソフトバウンスメールを2回目以降に送らせないようにするには手動でサプレッションリストに登録する、もしくはSES・Lambda・SNSなどのサービスを組み合わせて独自のシステムを構築するのが良いと思います。 もし同じような現象に遭遇し、お困りの方がいれば本記事の内容をご活用いただければと思います。

AWS Step FunctionsのRestoreDBClusterFromSnapshotだけではクラスタに紐づくインスタンスは作成されない

こんにちは、インフラエンジニアのささしゅう(@sasashuuu)です。

本日は、Step Functionsを用いてデータの同期更新システムを構築した際に少しだけハマったポイントについてご紹介です。

Step Functionsとは

Step FunctionsとはAWSの各種サービスを組み合わせた一連の処理の自動化などがローコードで行えるAWSのサービスです。

aws.amazon.com

弊社でも過去にいくつかStep Functionsを使用した導入事例を記事にしているので、もしご興味があればご覧ください。いくつかピックアップしておきます。

tech.connehito.com tech.connehito.com tech.connehito.com

やりたかったこと

前述したように、Step Functionsのワークフローを用いてデータの同期更新をするシステムを構築していました。その過程で同期元のRDSのスナップショットからAuroraクラスタおよびそれに紐づくインスタンスを復元しようとワークフローを組んでいました。

以下はそのフローの一部分です。

ここでは、DescribeDBClusterSnapshotsのステートで処理を実行し、取得したスナップショットの識別子を元にRestoreDBClusterFromSnapshotのステートでクラスターおよびクラスタに紐づくインスタンスを復元しようとしていました。

しかし、待てど暮らせどインスタンスは現れてくれません…。

以前検証のためにコンソールからスナップショットを使用して、クラスターの復元をおこなったことがありました。その際は対象のスナップショットの画面から「アクション」>「スナップショットを復元」>「DBクラスターを復元」で「えいっ、ポチッ」とやればインスタンスも丸っと綺麗に復元できたんだけどな…。(※以下、オペレーション時のイメージのキャプチャ)

今回Step Functions上で同じようなことを再現したかったのですが、どうもうまくいきません。

何が起こっていたか

結論、叩くAPIが足りていませんでした。

Step Functions上でRDSのスナップショットからの復元という要件を満たすには、RestoreDBClusterFromSnapshotとはまた別のステートでインスタンス作成を行う処理が必要でした。

前述したAWSコンソールからの実行時の例では「DBクラスターを復元」というsubmitボタンを押下した後、裏でよしなにクラスターの作成とインスタンスの作成のAPIの実行が両方走っていたのです。コンソールから「DBクラスターを復元」を実行した際に実行されるAPIは次の通りです。

この辺りは実行後、Cloud Trailのログなどを見るとよくわかります。

やらなければいけなかったこと

下記のようにワークフローを修正すれば良いです。

CreateDBInstanceのステートを追加しました。

これで晴れて、クラスターおよびインスタンスを含めスナップショットからの復元を行うことができます。

補足

上記の挙動は公式ドキュメントにも明記されていました。

コンソールを使用して DB クラスターを復元する場合、Amazon RDS は自動的に使用する DB クラスターのプライマリインスタンス (ライター) を作成します。RDS API を使用して DB クラスターを復元する場合は、DB クラスターのプライマリインスタンスを明示的に作成する必要があります。

docs.aws.amazon.com

現時点では、コンソールを使用したオペレーション時のみプライマリインスタンスを作成する挙動となっているようです。

今回はStep Functionsを用いたスナップショットからのRDS復元時に少しだけハマったポイントについてご紹介しました。また、ハマりポイントの小ネタなどがあれば更新しようと思います

ニアリアルタイムで同期される検索基盤 ~パイプライン構築編~

皆さん,こんにちは!MLエンジニアの柏木(@asteriam)です.

今回は前回のエントリーに続いてその後編,パイプライン構築の話になります. tech.connehito.com

はじめに

再掲になりますが,我々は以下の構成で今回の検索基盤を構築しています.

  • 検索エンジン:Amazon OpenSearch Service
  • データベース:Amazon Aurora
  • データ同期(ETL):AWS Glue
  • ワークフロー・パイプライン:AWS Step Functions・Lambda・EventBridge

後編は,検索エンジンに定期的に安定してデータを同期するために構築しているワークフロー・パイプラインに関する内容になります(下図の全量データ同期パイプラインの部分).

検索基盤全体のアーキテクチャー概略図

もう少し具体的には,OpenSearchのindexの管理方法について説明した後に,パイプラインの話とパイプラインのコード管理の方法の順番で紹介していきたいと思います.


目次


OpenSearchのindex管理

我々は2つのindexを用意し,その上段にaliasを置く方法でOpenSearchのindexを管理しています.aliasを置く理由としては,以下の理由が一番大きいです.

  • アプリケーション側で接続先を毎回変更する必要がない

また,接続先の変更ミスなどが発生すると適切なドキュメントにアクセスできなくなるといったこともあり,アプリケーション側からは常に同じ接続先にすることで上記問題を気にせずにリクエストを検索エンジンに投げることができます.

OpenSearch内でのindexの構成

indexに入るデータはGlueから同期されるようになっており,後述するパイプラインを用いてデータが安全に入る仕組みを構築しています.

一方で,我々はOpenSearchのReindex APIを使ってindexの作り替えをしているのではなく,全量データを数日(サービス仕様のためボカしています)に1回洗い替え作業を実施し,indexを一度削除してから新規で作り替えを行っています.そのタイミングで,裏側でaliasを切り替えている状況になります.

なぜindexの作り替えを毎回実施しているのか

数日に1回全量データの洗い替え作業を実施しており,このタイミングでindexの作り替えを行っています.洗い替え作業を行う理由としては,

  • データの漏れや欠損が発生していた場合にそれらを洗い替え時に拾うことができるため

この処理はデータの取りこぼしがあった時の保険的な役割が大きいです.そのため,Reindex APIは既存のindexのコピーになるので,それだけだと不十分で毎回indexを作り替えることをしています.

indexの切り替えフローの紹介

洗い替え時のindexの切り替えフローを紹介しようと思います.流れとしては下図のようになっています.

洗い替え時のindexの切り替えフロー

今,indexAとindexBの2つがあるとして,aliasはindexBを向いている(alias→indexB)とします.差分データの更新がindexBで続いており,indexAはスタンバイ状態になります.

まず,indexAの削除をした後,AWS Glueによる全量データの同期をindexAに対して行います.これによりindexAに最新のデータが入った状態になります.その後aliasの向き先をindexBからindexAに変更します(alias→indexA).これで取りこぼしがあった場合でも,補完することができ,データをindex化することができます.あとは次回洗い替え時にAとBが入れ替わった形でまた処理が実行されます(indexAとBで交互にループしている状態).

これらの処理をStep Functionsのパイプライン上で実現して日々実行されています.

ワークフローとパイプラインの役割

では,実際にパイプラインの処理はどんな感じになっているかを紹介していきます.

今回,ワークフローとパイプラインというように分けていますが,それぞれの役割をまずは説明します.ちなみにこれらはどちらもStep Functionsを用いて構築しています.

  • ワークフロー
    • 2つのindexに対応したパイプラインがあり,そのどちらを使用するかを制御する
    • EventBridgeによるスケジュール実行の対象となり動作する

ワークフローの構成図

  • パイプライン
    • 上図にあるStep Functionsの実体で,辞書更新とデータ同期の処理を行う

全量データ同期のパイプライン構成図

なぜワークフローとパイプラインで分けたのか?

なぜこの2つに分けたのかを説明していくと,問題点と解決策は以下のようになりました.

  • 問題点:数日に1回のスケジュール設定をEventBridgeで行えなかった
    • 開発当初は2つindexに対して用意したパイプラインに対して,それぞれスケジュールを割り当てて実行しようとしていたのですが,数日に1回のスケジュール設定を上手くそれぞれのindexのパイプラインが交互に実行するのができないことがわかった
      • 月によって31日がなかったりなどの影響があり
  • 解決策:それぞれのパイプラインを管理するワークフロー的な役割を用意
    • 2つのパイプラインを制御する役割として,上段にワークフロー的な役割をするStep Functionsを用意することで,上手くこの問題を回避した
    • EventBridgeによるスケジュール設定をこのワークフローに割り当て,現在どのindexに対してaliasが接続しているかをチェックすることで,その後にどちらのパイプラインを流せば良いかを判断している存在とした

この結果,管理するEventBridgeも1つになったのと,ワークフローを確認することでどちらのパイプラインに処理が流れたかを一目で確認することができるようになりました.

全量データ同期パイプラインの詳細

次にパイプラインの詳細を見ていこうと思います.このパイプラインの中には大きく2つの処理パートがあります.

  • 辞書更新パート
  • データ同期パート

全量データ同期のパイプライン構成図

辞書更新への対応

パイプラインの最初の処理に辞書更新の有無を判断しているステップがあります.これはS3に置いてある辞書ファイルが直近で更新があったかをチェックし,更新があればこの辞書更新のパートに処理が進んでいきます.無ければこの処理はスキップされます.

辞書更新のフローを別に切り出すことも可能でしたが,少ない数のパイプラインで管理したいのと,運用を考えた場合に,意識せず上段のワークフローを再実行することで辞書更新も取り込んだ形でindexの更新が行えるのがベストだと思ったのもあり,メインのパイプラインの中に組み込んだ形を取っています.

辞書更新のパートでは,「ユーザー辞書」や「同義語辞書」の更新を行える処理になっています.これらの辞書を更新するためには,OpenSearchの更新を行う必要があり,更新された辞書情報はindexの作り替え時に適用されることになります.

OpenSearchの更新をするためには,パッケージの更新とその関連付けの処理を行う必要があり,こちらの公式ドキュメントが一部参考になるかと思います.

上記処理はStep Functionsのアクションに登録されているので,それらを用いて処理を組み立てています.

OpenSearchのindexの切り替え方法

先述した「indexの切り替えフローの紹介」の章で説明した内容をデータ同期パートでStep Functionsのフローに落とし込んでいます.

OpenSearchへはLambdaを用いてAPIを叩いてindexのdeleteやaliasの切り替えを行っています.

ここでのindexの切り替えステップは,検索システム-実務者のための開発改善ガイドブックの「8.5.1 インデクサの更新」の章でも書かれているように,Blue/Greenデプロイの形になっていて,新しいindexへのデータ投入が完了した後に,aliasの向き先を古いindexから新しいものに変更することで安全にデプロイをしています.その際に,新しいindexへの差分データ同期のパイプラインを有効化,古いindexへの同期処理は無効化することでパイプラインの完全な切り替えが完了します.

データパイプラインのコード管理

最後に,これらの複数のパイプラインをコード管理する方法ですが,AWS SAMによるパイプラインのコード管理を行っています.

AWS SAMとは,AWS Serverless Application Model (AWS SAM) といい,サーバレスなAWSリソースを管理するツールになります.サーバレスに特化したCloudFormationを拡張したものと言え,今回使用したLambda・Glue・Step Functionsなどのリソースは全てコード管理することができます.

ディレクトリ構成は以下のような感じになります.

.
├── README.md
└── sam
    ├── env
    │   ├── dev
    │   │   ├── samconfig.toml
    │   │   └── template.yaml
    │   └── prd
    │       ├── samconfig.toml
    │       └── template.yaml
    ├── functions/(Lambdaの定義)
    ├── glue
    │   ├── dev/(GlueJobの定義)
    │   └── prd/(GlueJobの定義)
    └── statemachine/(StepFunctionsの定義)
  • Step FunctionsとLambdaは共通化し,Glue Jobはdev/prdで個別にスクリプトを用意し,環境差分はtemplate.yamlの定数で定義
  • 共通変数はtemplate.yamlのParametersに記載

コード管理することで,いくつか利点があります.

  • 設定した内容を別環境に簡単に適用することができる
  • コードレビューが可能になり,設定ミスや漏れなどに気づきやすい
  • 環境依存部分をパラメータ化することで,テンプレートファイルで簡単に管理・切り替えが可能

ABテストを実施する際など,同一の環境をもう1セット用意する必要がある場合でもコード管理しておくこと,コマンド一発で環境を用意できるので,コード管理の恩恵を多分に受けています.

細かい設定ファイルの中身についてはここでは記載しないですが,今度登壇させて頂くJAWS DAYS 2022(2022-10-08 (土))でもう少し説明しようと思っていますので,気になる方はこちらのイベントを確認して貰えると嬉しいです!(資料は公開予定ですので,そちらでも可能です)

おわりに

今回構築したパイプラインは再実行を行いたい場合でも,input情報の設定をすること無く実施できるので,属人性もなく,indexの向き先なども気にせずに実行できるので運用がかなり楽になった仕組みだと感じています.

また,パイプラインもコード管理しておくことでチーム内の他のメンバーが環境構築する際にも簡単に実行できたり,スクラップ&ビルドも容易だったりして整理しておくと色々と恩恵があるなと感じています.

今後もより安心安全で信頼性が高いパイプライン構築のために,改善できる部分はよりブラッシュアップしていこうと思っています.

再掲になりますが,JAWS DAYS 2022でも今回のテックブログの内容を紹介する予定になっていますので,是非興味がある方はイベント登録(無料)して頂いて見て貰えると嬉しいです.

  • 登壇時間:2022-10-08(土)14:20~15:00
  • タイトル:AWSのマネージドサービスで実現するニアリアルタイムな検索基盤

jawsdays2022.jaws-ug.jp

最後に,コネヒトではプロダクトを成長させたいMLエンジニアを募集しています!!(切実に募集しています!)
もっと話を聞いてみたい方や,少しでも興味を持たれた方は,ぜひ一度カジュアルにお話させてもらえると嬉しいです.(僕宛@asteriamにTwitterDM経由でご連絡いただいてもOKです!)

www.wantedly.com

GitHub Actions & ecspresso を用いたデプロイフローの改善

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

本日はコネヒト株式会社で取り組んでいる Super X という活動の一環である「デプロイフロー改善」の取り組みを紹介しようと思います。

本記事で紹介した改善以外にも、デプロイフロー改善チームで取り組んだことは別エントリでお届けする予定です✨


目次


はじめに

そもそも Super X って何?と思われるかと思うので、取り組みのご紹介をしようと思います!

Super X はエンジニア組織として追っている目標のプロジェクト名で、開発組織の中で課題となってる負債の解消などに取り組んでいく活動です!

今回私は 「デプロイフロー改善」を上期のテーマとして選び、活動を行ってまいりました。

抱えていたデプロイフローでの課題

※以下、「開発環境」は一般的に言うステージング環境を指しています。

弊社では過去に ecs-deploy を用いたデプロイを ecspresso に変更する取り組みがありました。(ref. GitHub Actions & ecspressoによるデプロイフロー構築

上記取り組みで ecspresso 導入がされたものの、以下の課題が残っていました。

  1. 開発環境と本番環境で別々の Docker イメージが利用されている
  2. ロールバックの方法が確立してない

1.については開発環境で検証した Docker イメージを本番環境でも利用した方が安心であるのと、本番環境デプロイ時に再ビルドが発生するので余計な時間がかかっているのを解消したいと思っていました。

2.については、ecspresso を使えば簡単にロールバックできるものの、その運用方法が社内で確立していなかったので、この際に他リポジトリに展開できるまで運用方法を確立しようという話になりました。

各課題の解決方法

開発環境で検証した Docker イメージを本番環境のデプロイで利用する

前提として、ecspresso を用いたデプロイフローはざっくり下記のようになっております。

1. 最新コードのチェックアウト
2. イメージをビルドし、ECR へプッシュ
3. ecspresso を利用して ECS タスク定義の更新・デプロイ

今回の改善では開発環境で上記のデプロイフローを行い、本番環境では下記のデプロイフローを行うことを目指します。

1. 最新コードのチェックアウト
2. ecspresso を利用して、開発環境で作成したイメージを参照するように ECS タスク定義の更新・デプロイ

このとき Docker イメージにつけるタグ名としては以下を満たす必要があると考えました。

  • 開発環境デプロイ時に利用できるものであること
  • タグ名が :latest などに固定されないこと(ecspresso のロールバックを考慮する)

上記2点を考慮すると、コミットハッシュをタグにするのがいいのではないか、という話になりました。

コミットハッシュならば、Github Actions 内で github.sha で取得ができます。 取得したコミットハッシュをイメージのタグ名として、イメージのビルド・ECR へのプッシュを行い、ecspresso でそのイメージを利用する形にすれば、開発環境で検証したイメージを本番環境デプロイ時に流用できる設計となりました。

デプロイフローのイメージ図

これにより、本番デプロイの actions は以下のようになり、だいぶシンプルになりました(一部抜粋)。

on:
  push:
    tags:
      - '*'

env:
  AWS_ROLE_ARN: hogehoge

permissions:
  id-token: write
  contents: read
  actions: read
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      # 通知など前処理
      - name: Checkout
        uses: actions/checkout@v2
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{ env.AWS_ROLE_ARN }}
          aws-region: aws-region
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
      - uses: kayac/ecspresso@v1
        with:
          version: latest
      - name: Deploy to Amazon ECS
        env:
          IMAGE_TAG: ${{ github.sha }}
        run: |
          ecspresso deploy --config .ecspresso/production/config.yaml
      # 後続処理

ロールバック方法の確立

デプロイが完了し、タスクは正常に起動したもののアプリケーションの動作に問題が発生したので戻したい場合には、ecspresso rollback コマンドを使用します。

ドキュメントによると、ロールバックをすると下記動作が走ります。

  1. 現在サービスに設定されているタスク定義の「ひとつ前」のリビジョンを見つける
  2. ひとつ前のリビジョンのタスク定義をサービスに設定する

ref. ecspresso advent calendar 2020 day 6 - rollback

なので理論上、有効なリビジョンのタスク定義がある場合は無限にロールバック可能です。

このとき、--deregister-task-definition オプションをつけるとロールバックを行ったタスク定義を登録解除 します。 つまり、1回異常が起きたタスク定義はもう使わない、という思想のようで、ecspresso 作者の方も特別な事情がない場合はこのオプションをつけることをおすすめしています。

このオプションをつけることで、無限に戻れはするが、戻った際に過去ロールバックをした定義は使われない状態が作れます。 なのでロールバック時はオプションを指定したコマンドを実行するように修正しました。

ecspresso rollback --deregister-task-definition --config .ecspresso/production/config.yaml

ちなみに ecspresso Roadmap to v2 を見ると、このオプションがデフォルトで true になるようなので、いずれオプション指定しなくても良くなりそうですね🙌

その他工夫した点

デプロイジョブの並列化

場合によっては複数の ECS 環境にデプロイを行いたい場合があるかもしれません。その際にジョブを直列で書くと、1つの環境でデプロイが終わらないと次のデプロイが始まりません。

そんな時はジョブを並列化するとデプロイ時間が短縮されます。

deploy_1:
  needs: build
  runs-on: ubuntu-latest
  steps:
    # デプロイ
deploy_2:
  needs: build
  runs-on: ubuntu-latest
  steps:
    # デプロイ
deploy_3:
  needs: build
  runs-on: ubuntu-latest
  steps:
    # デプロイ

上記のように needs: build でビルド作業が終わったら各ジョブが走るようにします。

デプロイ並列化

actions の詳細を見ても上図のように並列実行できていることがわかります。

並列化したジョブの結果を取得する workflow-conclusion-action の利用

デプロイをする actions では最後にデプロイ結果を通知するジョブを用意しています。デプロイのジョブを並列化した場合、どれか1つでもジョブが失敗したら通知としては失敗と通知してほしい気持ちになります。

通常ジョブの成功/失敗は job.status で取得できますが、これは1つのジョブの成否になるので、ジョブが分かれた場合、前段のジョブが成功したのか、失敗したのかはわかりません。

そんな時は workflow-conclusion-action が便利です。

notify_end:
  needs: [ deploy_1, deploy_2, deploy_3 ]
  runs-on: ubuntu-latest
  if: always()
  steps:
    - uses: technote-space/workflow-conclusion-action@v2
    - name: Notify slack of deployment result
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ env.WORKFLOW_CONCLUSION }}
        fields: repo,commit,ref,workflow,message
        author_name: ${{ github.actor }}
        text: |
          Deployment has ${{
            (env.WORKFLOW_CONCLUSION == 'success' && 'succeeded') ||
            (env.WORKFLOW_CONCLUSION == 'failure' && 'failed') ||
            'cancelled'
          }} to hogehoge.
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

このように各デプロイ全て終わった後に実行し、結果を env.WORKFLOW_CONCLUSION で取得すると、どれか一つでもデプロイが失敗した場合は失敗の通知をしてくれます。

まとめ

今回は ecspresso を用いたデプロイの更なる改善方法をご紹介しました。

仕組みが整っていると、普段あまり意識しなくても流れに乗ってしまえばできてしまうデプロイですが、これを機に CI 周りの理解だけでなく、ECR、ECS などインフラ側の理解も進んだので、非常に良い機会でした。

今回ご紹介した方法が、日頃の開発の何か参考になれば幸いです。

PR

コネヒト株式会社では絶賛エンジニア募集しております!

今回のような活動に興味を持った方、ぜひ一度お話させてもらえるとうれしいです。

hrmos.co

参考

ecspresso handbook

オンボーディング改善に機械学習を活用する〜トピックモデルによる興味選択編〜

みなさんこんにちは。MLチームのたかぱい(@takapy0210)です。

ここ1年くらいPokémon UNITE というゲームにハマっていまして、何回か大会にも出場しているのですが、先日出場した大会の「おじさんの部 26歳以上の部」で準優勝することができました🎉
若い頃の部活に近い感覚で、チームメンバーで勝利の喜びを噛み締めたり、負けた悔しさを共有したりなど、生活に刺激を与えてくれる存在になっています。

さて本日は、コネヒトの運営するママリのオンボーディング改善に機械学習を活用した事例をお話をしようと思います。

今回実施したオンボーディング改善には大きく分けて以下2つのステップがあります。

ステップ1:興味選択にどのようなトピックを掲示したら良いか?
 → 後述するTwitterの例でいうところの「Pokémon」や「Business news」など

ステップ2:選択したトピックに関連するアイテムをどのように計算(推薦)するか?
 → 「Pokémon」を選んだユーザーに対して、どんなアイテムを推薦するのか

本エントリでは主にステップ1の内容についてお話しできればと思います。(ステップ2に関しては別エントリでお届けする予定です!)


目次


はじめに

世の中にリリースされているサービスの多くは、登録時に何かしらのオンボーディングがあると思います。

例えばTwitterのオンボーディングでは、以下のように「Twitterで見たいものは何か?」を選ぶフローがあり、ここでユーザーごとに興味関心のあるトピックを選択することで、パーソナライズを実現しようとしています。

このように、サービスを使い始めたばかりのユーザーに対しては、順当にユーザーの興味関心に適合したアイテムを推薦することで、サービスへの信頼性を高める戦略をとることは非常に重要だと考えています。

そこでママリにおいても、オンボーディング時にユーザーの興味関心を教えてもらうことで、新規ユーザーのUX向上に貢献できるのではないか、という仮説のもと、今回のプロジェクトが始まりました。(以降、このプロジェクトのことを”興味選択”と呼びます)

オンボーディング改善に取り組んだ背景

これまでのママリでは、新規ユーザーに対してはお子さんの年齢や妊娠週数別にルールベースでアイテムの推薦を行っていました。

これにはいくつか課題がありますが、中でもユーザーの興味関心を拾えていない部分が大きいと考えていました。

例えば、妊娠初期のユーザーでも、以下のように興味関心は1人1人異なります。

  • 妊娠中、母体に訪れる症状(例:つわりなど)に関心のあるユーザー
  • 仕事関連(例:産休など)に関心のあるユーザー
  • お金関連(例:出産にかかる費用や保険など)に関心のあるユーザー etc …

上記のような興味関心は今までのルールベースの推薦では考慮できておらず、同じ属性(妊娠初期など)の新規ユーザーには一様なアイテムが推薦されている状態でした。

このような課題を解消するために、オンボーディングに興味選択を組み込むことになりました。

なぜ機械学習を使う必要があったのか

例えば、妊娠初期のユーザーがどのようなアイテムに興味関心があるのか?を知る方法はいくつかあると思います。

一番手軽に知る方法、検索のログを分析することでしょうか。
検索のログを妊娠初期ユーザーでフィルタリングし、検索頻度の高い単語 = 妊娠初期に気になるトピック、と仮定することができます。

しかし、検索のログで拾えるものは一般的に、顕在化されている関心であることが多く、潜在的な関心を拾うのは難しいと考えられます。

そこで、検索だけでなくアイテムのクリックログから、それぞれのクラスタごとによく見られているアイテムを抽出し、潜在的なニーズを探るためにトピックモデルを活用しました。

顕在的なニーズは検索ログベースで抽出し、潜在的なニーズは機械学習を使って抽出することで、興味選択で表示するトピックに網羅性を持たせ、さまざまな悩みを持つユーザーに寄り添ったオンボーディングを目指しました。

分析手順

今回はユーザーを複数のクラスタ(妊娠初期や妊娠中期、生後Nヶ月、など)に分割し、それぞれのクラスタが閲覧しているアイテム(=質問)毎にトピックモデルを活用し、各クラスタでどのようなトピックが現れるのかを分析しました。

トピックモデルとは

文書が複数の潜在的なトピックから確率的に生成されると仮定したモデルです。

ここでいう「トピック」とは話の主題のことで、同じ話題について話していても、人によって解釈が変わることもあります。
特徴として、トピックモデルの「トピックの数」と「出力されたトピックのラベル」は人間が決める必要があるということが挙げられます。

例えば、トピックの数を3と指定してモデリングした場合、そのモデルに何かしらの文書を渡すと、出力として3つのトピックそれぞれの確率分布が得られます。
また、トピックモデルの出力が以下のようになった場合、Topic1はスポーツ、Topic2は経済、Topic3はエンタメといった解釈は人間が行う必要もあります。

こうした「トピックへの意味づけ」や「最適なトピックの数」に関しても、併せて紹介していこうと思います。

トピックモデルのイメージ図

今回はトピックモデルの代表とも言えるLDAを、gensimというライブラリを用いて実装しました。

トピックモデルの概要についてはAlbertさんの記事を見ていただくとよりイメージがしやすいと思います。

www.albert2005.co.jp

gensimを用いたトピックモデリング

モデリングする際は日本語であればトークナイズ済みのデータが必要になります。(以下のようなイメージです)

LDAの学習に使用するデータイメージ

データが用意できれば、LDAの学習は以下のように実装できます

import gensim
import multiprocessing

texts = df['content_token']
dic = gensim.corpora.Dictionary(texts)
bow_corpus = [dic.doc2bow(doc) for doc in texts]

lda_model = gensim.models.LdaMulticore(
    bow_corpus,
    num_topics=18,
    id2word=dic,
    workers=multiprocessing.cpu_count(),
    passes=10,
    random_state=0
)

前述したように学習後のトピックに対する意味(ラベル)づけは人間が行う必要があります。
その際は以下のようにすることで、トピックに紐づく単語を取得することが可能です。

num_words = 30
topic_list = []
word_list = []
weight_list = []

for n, values in lda_model.show_topics(num_topics=18, num_words=num_words, formatted=False): 
    for word, weight in values: 
        topic_list.append(n)
        word_list.append(word)
        weight_list.append(round(float(weight) * 100, 2))
        
topic_df = pd.DataFrame()
topic_df['topic'] = topic_list
topic_df['word'] = word_list
topic_df['weight'] = weight_list

このようなDataFrameを取得することができ、トピックの理解に役立ちます。

取得できるDataFrame

可視化してみると、より分かりやすくなると思います。

import matplotlib.pyplot as plt
import japanize_matplotlib
import seaborn as sns

fig, axes = plt.subplots(2, 5, figsize=(36, 20))
topic = 0
for ax in axes.ravel()[0:]:
    sns.barplot(x="weight", y="word", data=topic_df.query('topic==@topic'), color='#da8886', ax=ax)
    ax.set_title(f'Topic: {topic}')
    topic += 1
plt.show()

トピックに紐づく単語(一部)

最適なトピック数を探索する

前述したように、トピックの数は人間が指定しなければなりません。

トピックモデルのトピック数は、一般的にCoherenceとPerplexityの値から最適なトピック数に当たりをつけることができます。

  • Coherence :トピックが人間にとって分かりやすいかといった品質を表す指標。高い方が良いとされている。
  • Perplexity:モデルの予測性能を表す指標。低い方が良いとされている。

上の2つの値は、以下のようにして探索することができます。

# 探索
start = 10
limit = 50
step = 1
coherence_vals = []
perplexity_vals = []
for n_topic in tqdm(range(start, limit, step)):
    lda_model = gensim.models.LdaMulticore(bow_corpus,
                                           num_topics=n_topic,
                                           id2word=dic,
                                           workers=multiprocessing.cpu_count(),
                                           passes=10,
                                           random_state=0)
    perplexity_vals.append(np.exp2(-lda_model.log_perplexity(bow_corpus)))
    coherence_model_lda = gensim.models.CoherenceModel(model=lda_model, texts=texts, dictionary=dic, coherence='c_v')
    coherence_vals.append(coherence_model_lda.get_coherence())

# グラフの描画
fig, ax1 = plt.subplots(figsize=(15,8))
# coherence & perplexity
x = range(start, limit, step)
ax1.plot(x, coherence_vals, 'o-', color=c1)
ax1.set_xlabel('Num Topics')
ax1.set_ylabel('Coherence', color='darkturquoise'); ax1.tick_params('y', colors='darkturquoise')
ax2 = ax1.twinx()
ax2.plot(x, perplexity_vals, 'o-', color=c2)
ax2.set_ylabel('Perplexity', color='slategray'); ax2.tick_params('y', colors='slategray')
# 表示
ax1.set_xticks(x)
fig.tight_layout()
plt.show()

coherenceとperplexityのプロット

上記の例だと26あたりのトピック数が良さそうだ、という判断をすることができます。

プロダクトへの活用

上記で分析した結果などを参考に、PdMと議論しながら最終的にユーザーに表示するトピックを選定していきました。

2022年09月時点では以下のようなトピックがオンボーディングで表示されています(※ユーザーの属性情報によって表示されるトピックは異なります)

オンボーディング時のスクリーンショット

最後に

今回はユーザーをクラスタ別で分析したときに、どのようなコンテンツの興味があるのか?を分析することで、オンボーディングを改善した事例をご紹介しました。

冒頭で述べた通り、続編として「選択したトピックに関連するアイテムをどのように計算(推薦)するか?」の内容についても、後日紹介できればと思います。

オンボーディング改善の内容は、PyCon 2022でも詳細をお話する予定なので、興味がある方は是非観にきてください! (登壇日時は10月14日(金)の17時10分〜17時40分に決まりました!)

2022.pycon.jp

We Are Hiring !!

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

www.wantedly.com

機械学習に関しては、過去の取り組み事例などを以下にまとめていますので、是非見てみてください!

tech.connehito.com

そして興味持っていただけた方はカジュアルにお話しましょう! (TwitterのDMでもMeety経由でも、気軽にご連絡ください)

参考文献

コネヒトはiOSDC Japan 2022に協賛いたします!

こんにちは、iOSアプリエンジニアのyanamura(@yanamura_)です!

本日は、iOSアプリ開発者の祭典iOSDC Japan 2022に協賛するお知らせです。

コネヒトはiOSDC Japan 2022に協賛いたします!

iOSDC Japan 2022に、シルバースポンサーとして協賛いたします。

iosdc.jp

スポンサーするにあたって、コネヒトは「人の生活になくてはならないものをつくる」というミッションを掲げているので、技術コミュニティについても同様に、サポートして一緒に盛り上げていくことができたら、と思っております。

イベント概要

  • 開催 2022年9月10日(土)~9月12日(月)
  • 場所 早稲田大学 理工学部西早稲田キャンパス63号館・オンライン(ニコニコ生放送)
  • 対象 iOS関連技術およびすべてのソフトウェア技術者
  • タイムテーブル https://fortee.jp/iosdc-japan-2022/timetable
  • 主催 iOSDC Japan 2022 実行委員会 (実行委員長 長谷川智希) / WASEDA-EDGE人材育成プログラム
  • 共催 GTIE (Greater Tokyo Innovation Ecosystem), 早稲田大学 総合研究機構 グローバル科学知融合研究所

iOSDCトークンはこちら!

#connehito-techvision

これはiOSDCチャレンジで使うトークンです!

今年はオンラインだけではなく、オフラインでも開催される予定です。久しぶりのオフライン開催なので私も会場に足を運びたいと思っています!会場でお会いした際にはぜひお話しましょう!