複数リージョンへの Alexa Skill のデプロイを可能な限り自動化する

こんにちは、ボイスサービス部の ymd (@y_am_a_da) です。今年からは採用にも関わっています。

クックパッドは日本で No.1 のレシピサービス*1ですが、ここ数年は海外展開にも注力しており、世界 74 ヶ国 32 言語で展開しています。

ボイスサービス部は、もともとクックパッドのひとつのプロジェクトとして音声 UI を利用したサービスの開発だったり、デモを作成*2していたのですが、サービスの拡大などにより最近独立した部署として設立されました。 規模が一番大きい Amazon Alexa (以下 Alexa) 向けのサービスは、レシピサービスと比較するとまだまだ小規模ですが現在 9 ヶ国 3 言語で展開をしています。

Alexa 向けのサービスも、このくらいのサービス規模になってくるとコードのデプロイや、サービスのメタデータの更新など、手動でやるのはなかなかに大変な規模になってきています。 この記事では、 Alexa 向けサービスのデプロイを省力化するために、クックパッドで実際に行っている自動化のテクニックについて紹介をさせていただきます。

クックパッドスキルの全体像

Amazon Alexa では、いわゆるモバイルアプリケーションにおけるアプリに相当するものとしてスキルというものがあり、ユーザーはこれを有効化してサービスを利用します。クックパッドでもこのスキルを介してサービスを提供しています。 具体的なデプロイのプロセスのお話をする前に、まずはこのスキルの全体像を紹介し、この記事で紹介するデプロイとはそもそも何をすることなのかを説明します。

以下の画像が、クックパッドスキルの全体像になります。実際にはもう少し複雑なのですが、デプロイの説明をするために必要最小限のリソースを記載しています。

クックパッドスキルの全体像

Alexa は基本的にクラウドベースで稼働しており、 Amazon Echo デバイス上での処理は必要最小限です。

これはスキルに対しても言えることで、例えばユーザーがクックパッドスキルに何かを話しかけた場合、 Amazon Echo が取得した音声が Alexa Skill (この場合はクックパッドスキル) に送信され、 Intent と呼ばれるコマンドに変換されます。 Alexa Skill では、開発者は生の発話を扱い処理するのではなく、もう一段回抽象化された Intent と呼ばれるものを扱います。 Android の開発者には馴染みのある単語かもしれませんが、 Alexa における Intent の概念も非常に近いです。 この Intent などを含んだリクエストを AWS Lambda に送信し、処理の結果を返すことで Amazon Echo に応答の発話をさせることができます。

また、現在 Amazon Echo には液晶付きのデバイスも存在しており、スキル側で応答の発話と一緒に画面に何かを表示させることも可能です。

表示内容は JSON ベースの Amazon Presentation Language (APL) という言語で記述します*3。この記述は AWS Lambda からのレスポンスとして直接渡すこともできるのですが、ファイルとして置いておいてレスポンスではそのファイルの URL だけを渡し、別途デバイス側でアクセスしてインポートさせることもできます。 後者の仕組みは APL Package と呼ばれており、Lambda から返すレスポンスのサイズを節約できるだけでなく (最近まで 24KB 以内に抑える必要があったため切実)、ファイルのキャッシュにより表示も高速化されるため、クックパッドでは積極的に利用しています。

DynamoDB は表記の通り、セッションを越えて永続化したいデータを保存しています。主にいくつかのパーソナライズされた体験を提供するために利用しています。

クックパッドスキルのデプロイ

全体像がわかったところで、 クックパッドスキルでのデプロイはどういうことをやっているのか。について紹介したいと思います。 クックパッドスキルでは大まかに、デプロイによって以下の画像に記載のものを更新しています。

デプロイで更新するリソース

簡単なところからいうと、 AWS Lambda のコードや、 Lambda 関数の設定をアップデートしています。ここでいう設定は、Lambda 関数のランタイムだったり、環境変数だったりを指しています。また、S3 に存在する APL のファイルもアップデートしています。 Alexa Skill の Interaction Model は、発話と Intent のマッピングファイルのことで、開発者はこれを記述することでユーザーのどういった発話をどういった Intent に変換するべきかを定義することができます。 Skill Manifest はやってきたリクエストをどこに送信するのかのエンドポイント (クックパッドスキルでは Lambda の ARN を指定しています) や、スキルストアに公開する紹介文、パーミッション関連などスキルに関する様々なデータになります。

クックパッドスキルのデプロイという行為は、 PR のマージ後に走る CI と、その後にチャットボットを利用したデプロイコマンドの実行により上に述べたリソースを最新のものにアップデートし、必要に応じて審査にサブミットすることを意味します。

ここまでの説明だけを見ると、登場するリソースも少ないですし、ただ CI とチャットボットがあるだけで仕組みも非常に簡単そうに見えますが、 Alexa 固有の仕様によって工夫が必要な部分もいくつかありますのでここで紹介をしていきます。

Lambda 関数の管理について

スキルストアに公開されている Alexa Skill は、1 つのスキルに 2 つの実体が存在します。Live バージョンと Development バージョンです。 Live バージョンは名前の通り一般公開され実際に使用されているバージョンのスキルです。 Development バージョンはこちらも名前の通り開発用に使用するスキルです。

Live バージョンは基本的にデータの一切の変更が不可能です。開発者は Development バージョンのスキルに必要な変更を加え、審査に出し、パスすることで変更を Live バージョンに反映することができます。

以下の図が Development バージョンのスキルを審査に提出した際のフローになります。それぞれのバージョンが持つメタデータやバージョンの変化に対するの理解を簡易にするためにこのような書き方をしておりますが、実際の挙動は明らかではありません。 すなわち、図では Development バージョンのスキルが審査を経て新しい Live バージョンになっていますが、ただ単に Development バージョンのスキルのデータで Live バージョンのスキルを上書きしていることもありえます。

スキルのライフサイクル 上記の図から以下のことが言えます。

  • Development バージョンのスキルは、少なくとも審査提出時には本番環境の Lambda 関数をエンドポイントに設定する必要がある
  • Lambda 関数に互換性の無い変更をする場合、審査に提出する Development バージョンのスキルは Live バージョンと異なる Lambda 関数を利用する必要がある

後者が少し厄介です。もう少し詳しく説明すると、例えば新しい発話への対応や、インタラクションフローの変更をする場合、 Live バージョンで利用している Lambda 関数と互換性がないため、異なるエンドポイントにコードをデプロイをし、それを Development バージョンのエンドポイントとして設定し、審査に提出する必要があります。

こういった仕組みを開発者が簡単に使える形で実現するにはどうすれば良いでしょうか。クックパッドでは以下のことをやっています。

1. 開発用のスキルを別途用意する

Development バージョンのスキルは少なくとも審査提出時には本番環境の Lambda 関数をエンドポイントとして設定する必要があります。うっかり開発環境の Lambda を設定したまま提出すると酷いことになります。

スキルのエンドポイントに開発版 Lambda 関数をセットし審査に出した場合

このエンドポイントの切り替えを自動でよしなにやるようにしても良いのですが、とはいえ事故が怖いので基本的には審査提出前の最終動作確認以外では使わず、開発時には別の開発用スキルを利用するようにしています。

2. Lambda のバージョニングとエイリアスを活用する

これが一番重要です。上に述べている通り、互換性の無い変更を加えたい場合、審査に提出する Development バージョンのスキルの Lambda 関数は Live バージョンのものと別でなければならず、かつ審査後そのまま新たな Live バージョンになるため本番環境のものである必要があります。

互換性の無い Lambda 関数をエンドポイントに設定する場合

これを解決するにあたって、 Lambda 関数のバージョニングやエイリアスを作成する仕組みを利用します。バージョニングは、最新の Lambda 関数のコードや設定をもとにバージョンを発行し、独立した関数として使用できる機能です。エイリアスは、ある Lambda 関数のバージョンに紐づくポインタのようなものを作成出来る機能です。

これを利用することで、例えば互換性の無い変更をしたい場合には新しくエイリアスを発行し、新しいバージョンの関数と紐付け、それを審査に提出するスキルのエンドポイントとすることで安全に本番環境の Lambda 関数を利用できます。

このエイリアスは、コードを管理している GitHub リポジトリの tag から自動的に生成できるようにしています。tag は セマンティックバージョニングに倣ったルールで各 PR ごとに開発者が付与します。 エイリアスは、ここで付与された tag のメジャーバージョンをもとに必要に応じて作成します。 開発者は破壊的な変更をした時にのみメジャーバージョンを上げるだけで自動的に最適なエイリアスにコードがアップロードされるようになっています。

スキルのエンドポイントは、このエイリアスを参照させるようにすることで互換性の無い場合でも安全にそれぞれのコードを参照できるようになっています。

バージョニングとエイリアスを利用して互換性の無い変更を安全にデプロイする

Alexa Skill の管理について

クックパッドでは、展開している国や言語ごとにスキルを別で分けています。複数スキルがあると管理が大変なので、 1 つのスキルで複数リージョンへ展開することも選択肢としてはあったのですが、 1 つにした場合、スキルの審査は展開している全てのリージョンで行われ、その全てでパスをしないと公開できないようなので複数に分けています。 例えば、あるリージョンに向けただけの変更をしただけなのに、全く関係のない別のリージョンで審査に落ちて公開ができない。というのは避けたいですし、そもそも国や時期によって審査のスケジュール感もばらつきが大きいのでこのようにしています。

しかし、複数スキルがあると、 Skill Manifest や Interaction Model の管理が大変なので、リポジトリでファイルベースで管理し、 ask-cli という Alexa Skill 用の CLI ツールを利用して自動で更新出来るようにしています。

具体的には、 Alexa Skill には Skill Package という概念があり、これは Skill Manifest や Interaction Model などスキルを構成する要素をまるごと含んだものなのですが、 ask-cli にはこの単位でスキルをアップデートするコマンドがあるため、それを利用してスキルごとにまとめてアップデート出来るようにしています。

また、 ask-cli には少し前に CI 上からでも簡単に利用できるようになったため、 PR がマージされるごとに CI で Skill Package を更新するようにし、意識しなくても常にそれぞれのスキルが最新の情報に更新されるようにしています。 https://github.com/alexa/ask-cli/blob/develop/docs/concepts/CI-CD.md

ちなみに、スキルは細かく分けていますが、 Lambda のエンドポイントは AWS のリージョンごとにしか分けていないのでいくつかのスキルでは同じ関数を利用しているものもあります。今後展開する国が増えてきたらまた変わるかもしれませんが、少なくとも今はこのエイリアスやバージョニングのおかげで特に不便はしていません。

APL ファイルの管理について

最後は APL ファイルの管理についてです。上で述べたとおり、 APL は URL から別の APL ファイルを取得し、それを利用する仕組み (APL Package) を備えています。クックパッドでも APL Package を利用していくつかのファイルは Amazon S3 上に保管するようにしています。

基本的には、ただ S3 上に APL のファイルを置いておき、その URL を渡せば良いだけなのですが、少しだけ厄介な部分があります。

公式でも説明されているのですが、 APL Package で取得されたファイルは強くキャッシュされるため、ただ新しくファイルを更新してもそれが反映されるまで大変長い時間がかかります。

これを回避する方法はいくつかあるんですが、クックパッドではリージョンや Lambda のバージョンごとにディレクトリを分けるようにし、デプロイする度に新しい APL ファイルを参照させるようにしています。 AWS Lambda は AWS_LAMBDA_FUNCTION_VERSION という環境変数で関数のバージョンを取得することが出来るため、これをもとに APL ファイルの URL を動的に生成させることで、自動的に適切な APL ファイルを指す URL を渡すことができるようになっています。

この仕組みのもう一つのメリットとしては、デプロイした後に問題が発覚して切り戻したい場合でも、 Lambda のバージョンを戻すだけで自動的に APL ファイルも一つ前のバージョンに戻せることです。キャッシュも気にする必要がありません。

リージョンを分けている理由としては、上に述べた通り AWS Lambda はリージョンごとに用意しているため、それに合わせるためです。 言い換えると、 Lambda 関数ごとにディレクトリを用意していて、その中でさらにバージョンごとにディレクトリを用意し、そこに APL ファイルをアップロードしている。という運用になります。

S3 にアップロードする APL ファイルは、リポジトリで全て管理をしており、デプロイのタイミングで AWS S3 にアップロードするようにしています。

まとめ

今回は Alexa 向けのサービスデプロイに関わる色々な仕組みや工夫について紹介をさせていただきました。

今回は紹介しきれませんでしたが、昔と比べるとかなり整備はされてきているものの、まだまだ管理や運用が難しい部分も多く、かつ展開するリージョンが多くなるほど大変になるものもあるため、これらを省力化できるようにすることは非常に意義のあることだと思っています。

まだまだ複数リージョンに展開をしているスキルの数は少なく、言い換えると事例や実践的な情報も少ないため、ここで紹介した内容が皆さまの日々の開発に役立てばと思います。

弊社ではこのように色々な技術スタックを持ったエンジニアが数多く在籍しております。絶賛エンジニア募集しておりますのでご興味ありましたらぜひこちらのサイトをご覧ください。

info.cookpad.com

*1:2021 年 12 月 31 日時点/iOS,Androidアプリ 1日あたりのアクティブユーザー数(2021年10月〜12月 App Annie)

*2:こちらに作成したデモの紹介記事があります https://techlife.cookpad.com/entry/2020/10/26/090000

*3:今回のテーマとは少し異なりますが、クックパッドにおける APL の tips はこちらの記事で他にも色々と紹介しているのでぜひ合わせてご覧ください。 https://techlife.cookpad.com/entry/2021/05/28/110000

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/