レシピページのOGP画像を動的に生成する

こんにちは、クックパッドでエンジニアをやっている @morishin です。入社してわりと長い間 iOS アプリやそのバックエンドの開発を中心にやってきましたが、最近は専らウェブフロントエンドとその基盤をいい感じにするというのをやっています。先日クックパッドウェブサイトのレシピページの OGP 画像を素敵に刷新したのでそのお話をしたいと思います。

※ ここで OGP 画像と呼んでいるのは Open Graph Protocol で定義されている og:image プロパティに指定する画像のことです。
※ OGP 画像と呼んでいますが厳密には今回変更したのは Twitter Card (twitter:image) 用の画像のみなので、その他の SNS に表示される画像 (og:image) は変わっていません。

できたもの

これまではレシピ作者さんがアップロードされた料理写真を単にクロップしたものが表示されていましたが、料理写真の横にクックパッドのロゴやレシピ・作者さんの名前を添えたいい感じの画像が表示されるようになりました!画像を見ただけでクックパッドのレシピページであること、なんという料理であるかなどの情報をパッと認識することができるようになっています。

Before After
f:id:morishin127:20220210160959p:plain f:id:morishin127:20220210161035p:plain

動機

SNS にシェアされたレシピがより魅力的に見えるようにしたいという思いと、クックパッドのレシピであることがひと目で伝わってほしいという気持ちから OGP 画像のデザインを変えられないかという話が挙がりました。開発はそれなりにかかりそうなので、まずはクックパッドの公式ツイッターアカウントから「画像ウェブサイトカード」(Twitter for Business の機能) を使って特定のレシピページに対して手作業で作ったいくつかのパターンの画像を当てて複数回プロモーションツイートを投稿し、パターンごとのエンゲージメント率などを見ながらどのデザインが良さそうかを検討しました。検証を経て現在のデザインに決まったので、次に実現方法の検討に移りました。

▼画像ウェブサイトカードの例

パターン例1 パターン例2

実装方針

OGP 画像用の URL はウェブページの HTML 中で次のような meta タグで指定します。

<meta property="og:image" content="<画像のURL>">

この content に指定する URL へのアクセスを受けるサーバは HTML でなく画像データを返さなければならないため、全てのレシピに OGP 画像を用意しようとするとレシピの数だけ画像データが必要になります。バッチ処理で事前に全レシピの画像を生成しておくことも不可能ではありませんが300万品以上のレシピに対して画像を生成するコストは重く、また実際に OGP 画像がリクエストされるのはそのうちのごく一部のレシピであるため無駄も大きいです。そのため今回は OGP 画像用の URL にリクエストがあったタイミングで動的に画像データを生成して返すアプリケーションを作成することにしました。

実現方法にはいくつか選択肢が考えられましたが、最終的にはこのようなアーキテクチャになりました。インフラには AWS のサービスを利用しています。

f:id:morishin127:20220210161124j:plain
アーキテクチャ図

OGP 画像として利用したいビューを HTML として生成するページをクックパッドのウェブアプリケーション上に作り、AWS Lambda 上で実行した puppeteer (headless chrome) でそのページへアクセスしてスクリーンキャプチャを撮ることで、HTML を画像データにします。Lambda のトリガーを API Gateway にして HTTP エンドポイントからアクセスできるようにし、レスポンスのキャッシュ用途でその前段に CloudFront を置いています。

処理がシンプルであること、メンテナンスコストが低いことからサーバレスなアーキテクチャを選択しましたが、リリース後に AWS の CostExplorer を確認したところ $1/day 未満のコストで済んでいるため、金銭面でも良い選択だったのではないかと思っています。

アーキテクチャの選定

上述の構成に決定するまでに検討した内容についてお話します。

他社のサービスで OGP 画像の動的生成を実現しているものを見かけてることはありましたが、どのように作っているのかは知りませんでした。しばらく調べてみた感じではどうやら次のような選択肢がありそうでした。

  • サードパーティの画像配信 CDN サービスを利用する
  • ImageMagick や node-canvas を使ってサーバーサイドで画像処理を行い生成する
  • HTML として描画したビューのキャプチャを撮ることで画像を生成する

デザインの自由度が高く作成・変更が容易であって欲しい、また金銭コストも気になるというところでまずはサードパーティサービスではなく自作ができないかを考えました。ImageMagick や node-canvas で画像を作るのは単純に実装が難しく消耗しそう、また変更も大変そうに思ったので3つ目の HTML を画像化する手法を第一に試すことにしました。Vercel 社の vercel/og-image がこの方針を取っていること、GitHub 社の新しい OGP に関するブログ記事でもこの方法でやっていることが窺えたので、その事実にも後押しされました。オープンな文化™️、めっちゃ助かる。

上述のブログ記事と OSS である vercel/og-image の実装を参考にすることで、puppeteer で HTML のキャプチャ画像を生成するところは実現できました。これが Lambda で動かせれば ok です。Lambda 上で puppeteer を実行するためには chromium のバイナリを含むコードを Lambda 上にデプロイする必要がありますが、バイナリのサイズが大きいため通常の Lambda のアップロードサイズ上限 (250MB) に引っかかってしまいます。幸い Docker イメージとしてデプロイする方式だとサイズ上限が 10GB まで引き上げられる仕様であったため、Lambda 上で動かすアプリケーションは Docker イメージとして作成しました。細かい話ですが vercel/og-image が依存している chrome-aws-lambda パッケージは Lambda の 250MB 制限に引っかからないように chromium バイナリを Brotli 圧縮したものが使われていましたが、今回はサイズ上限を気にしないでよくなったのでこのこのパッケージは使わず素の puppeteer を利用しています。

デプロイフロー

インフラリソースの構築とアプリケーションコードのデプロイには AWS CDK を利用しました。クックパッドではほとんどの AWS リソースは Terraform で管理されており (参考: AWS リソース管理の Terraform 移行 - クックパッド開発者ブログ) 個々のアプリケーションとは別に Terraform 定義用のリポジトリがあります。しかし今回作ったアプリケーションでは例えば API Gateway のエンドポイントの定義であったり CloudFront のキャッシュポリシーであったり Lambda 実行環境のスペックであったり、そういった AWS リソースの設定値をアプリケーションのソースコードと同列に扱いたくて、また頻繁に手を加えるようにも思ったので、アプリケーションコードと同一のリポジトリに置いておきたいと考えました。そこでインフラリソースの定義を CDK で書き、そのソースコードはアプリケーションと同一のリポジトリに置いて管理することにしました。実際、リリースした次の日にはキャッシュポリシーを変更したりしていて、変更・デプロイが楽にできたと感じました。

CDK のスタック定義の実装としては AWS が用意してくれている AWS Solutions Constructs@aws-solutions-constructs/aws-cloudfront-apigateway を使い回す形でほぼ実現できたのですが、細かいところで API Gateway の REST API ではなく HTTP API (参考) を利用したかったため一部実装に手を加えて利用する形になりました。典型的なインフラの構成がソースコードとして配布されていて利用することができる点が CDK の大きな利点に感じました。あと TypeScript などで記述した場合 JSON や YAML と違い型定義がありエディタの機能で定義にジャンプして、リソースがどういうプロパティを取りうるかがパッと分かるのが良いですね。毎度ググってドキュメントを見に行かなくても済みます。

まとめ

OGP 画像を動的に生成するアプリケーションをサーバレスな構成で実現してみました。結果として運用コストが低く金銭的コストも低い、またデザインの変更も容易な設計になったと思っていて、おおむね満足しています。仕組みとしては汎用的なものでレシピページに限らず他のページ向けにも OGP 画像を生成させることができるので、今後も活用していきたいと思っています。

クックパッドでは、技術とサービス作りが大好きなエンジニアを募集しています!
実装についてもっと詳しい話を聞きたい方、クックパッドでエンジニアとして働くことに興味のある方、よければオンラインでカジュアルにお話しませんか🙋‍♂️
🔜 カジュアル面談 申し込みフォーム

Twitter などで雑にお声がけいただいても大丈夫です。クックパッドはエンジニアを積極採用中でございます。

【宣伝】

https://www.youtube.com/playlist?list=PLGT7Exkshx4gQwDgEM1a2wRJgAv2yuzIB というサービス開発者向けのライブ配信イベントをやっていて、次回は 2月24日(木) 20:30~ に「数千万レコードをリアルタイムに捌くクックパッドマートの開発」というタイトルでやるのでよかったら観にきてください!
👇️👇️👇️👇️👇️👇️
cookpad.connpass.com

AWS CodeBuildでのRailsアプリのdocker buildを早くしたい

メディアプロダクト開発部の後藤(id:mtgto)です。

世間ではバレンタインですね。最近私はハンドメイドスイーツオークションというWebサービスの立ち上げをやっていました。ライブ配信でバレンタインのスイーツを作っていただき、ライバーのファンがスイーツをオークション形式で実際に購入できるというサービスです。 私のチームでは仮想DOMを扱うのにVue 2を使うことが多いのですが、今回は期日がずらせないイベントだったことや必要なライブラリがReact版しか提供されていなかったこともあり私がVueより使い慣れているReactで作りました。

本記事ではAWS CodeBuildでのRailsアプリのDocker buildを早くするための工夫を紹介します。

docker buildを早くしたい理由

クックパッドでは多くのアプリケーションを運用していますが、その多くはAWS ECS上で動いています(ref. ECS インフラの変遷)。 デプロイにかかる時間の大部分を占めるのはCIおよびDocker imageのビルド時間なのでコード修正→ステージング環境へのデプロイ→動作確認→デプロイのデベロップルーティンを日に何度もくり返すような生活をしているときに発生する待ち時間を減らすためにもdocker buildにかかる時間を減らすことができれば幸せになります(主に私が)。

まずは現状どれだけかかっているかを見てみましょう。今回使うのは生まれてから数年経っておりVue.jsを含むフロントエンド実装がそこそこあるRailsアプリです。

f:id:mtgto:20220214133414p:plain

CodeBuildのフェーズ詳細で確認すると、現状のDocker buildのためのCodeBuildジョブには10分弱ほどかかっていることがわかりました。

今回はこれを半分の時間にするのを目標としてみます。

今回のRailsアプリケーション

今回の実験に使用したRailsアプリケーションです。数年の歴史を経てかなりのページ数を持っています。

  • Ruby 420ファイル
  • TypeScript 180ファイル

PROVISIONINGフェーズにかかる時間が長い

まずはAWS CodeBuild Console上の最近のビルド履歴のフェーズ詳細からどのフェーズに時間がかかっているかを確認しましょう。すると PROVISIONING というフェーズに232秒かかっていることがぱっと目につきます。「CodeBuild PROVISIONING 遅い」で検索すると、CodeBuildに利用しているDockerイメージが古いと環境構築にかかる時間が長くなってしまいそれがPROVISIONINGフェーズが長くなる原因となるようです。実際、今回実験に使用したプロジェクトでは aws/codebuild/standard:2.0 という大変古いバージョン 1 を使用していました。 これを「ビルドの詳細」→「環境」から現行の最新の aws/codebuild/standard:5.0 に変更し、ビルドしてみます。同時にDockerのバージョンも18から20に変更します。

f:id:mtgto:20220214133450p:plain

PRIVISIONINGフェーズで232秒かかっていたのが95秒になりました。これだけで2分以上改善できました。

BUILDフェーズの地道な改善

次になんとかしたいのは309秒かかっているBUILDフェーズです。このフェーズではDockerfileをもとにdocker build && docker pushを行なっているため、工夫次第で改善できそうです。

今回のプロジェクトのDockerfileはマルチステージビルドを使っており、後述の yarn installbundle install をDockerのレイヤーキャッシュでなるべくスキップするような工夫はすでにされていました。それにもかかわらず5分近くかかっているのであれば感覚的にはこれは短縮できそうです。さっそくプロジェクトを見ていきましょう。

babel-loader → swc-loaderを使う

swc (Speedy Web Compiler) はRustで書かれたJavaScript / TypeScriptのトランスパイラで、babelよりも早いという特徴があります。Parcel v2ではTypeScriptのトランスパイルにデフォルトでswcを使ってくれたりするので、知らない間にswcのお世話になっているかもしれません。

今回実験に使用したプロジェクトではCodeBuildでのwebpack buildに約192秒かかっていました (JavaScript/TypeScriptの他にscss/cssの処理やコピーだけですが画像アセットの処理を含みます)。これならbabelからswcに変更するメリットがありそうです。

実際にbabel-loaderからswc-loaderに切り替えたところCodeBuildでのwebpack buildは192秒→174秒に改善しました。

チャンク分割

swc-loaderの導入により早くはなりましたが劇的には早くなりませんでした。この背景として今回のプロジェクトでは元々出力JSファイル数が多く、またチャンク分割 (splitChunk) の設定もしてないことが原因と思われました。

まずはこの仮説が正しいかを調べてみましょう。 webpack-bundle-analyzerを使って出力されるJavaScriptにどのようなパッケージが含まれているかを見てみたところ、このプロジェクトでは32個のJavaScriptファイルが生成され、そのうち4ファイルが1MBを越えていました。同じnpmパッケージを複数のJSがもっていることもわかったため、webpackのチャンク分割の設定を行い共通部分をまとめるようにしたところCodeBuildでのwebpack buildの実行時間は174秒から50秒に一気に短縮されました。

今回は時間の都合で行っていませんが、webpack-bundle-analyzerで見たところかなりの部分をaws-sdkが占めていることがわかりました。aws-sdkをv2からv3にアップデートすることで必要なサービス用のライブラリだけをインストールすることができるようになるためTree shakingなどビルド時間や生成ファイルサイズのさらなる改善も得られそうです。

AWS SDK for JavaScript v3

それでもだめなときの最終手段「金」

これまでいくつかの改善を行ってきましたがあとすこしだけCodeBuildの実行時間を半分にするには足りませんでした。そこで最後の手段である「お金で殴る」を使ってみることにしました。

CodeBuildではビルドの設定で利用するマシンスペックを変更できます。これまで使用していた「3GBメモリ / 2vCPU」で不足なのであればその上の「7GBメモリ / 4vCPU」を使用してみましょう。

これによりBUILDフェーズが56秒短縮されました。まさに「時は金なり」です。

ちなみにAWS CodeBuild の料金はビルド一分あたりの料金で計算され、だいたいvCPUが倍になれば料金も倍になります(分単位に切り上げ)。今回のプロジェクトのようにアセットのコンパイルにかかる時間がボトルネックな場合には強いマシンスペックを選ぶことでビルド時間が短縮されることが期待できます。コスパを考えつつスペックを選択しましょう。

実施済みの改善ポイント

今回実験に利用したプロジェクトでは既に実施済みでしたが、以下のような設定もビルド時間削減が期待できます。

webpackerをやめる

docker buildの高速化とは直接つながりませんが、webpackerをやめて直接webpackを使えるようにすることで、RubyGemsのインストールをアセットコンパイルの依存から外すことができます。request specやfeature specを実行するためにはRailsからアセットが利用できないといけないため、アセットコンパイルをBundle installと並列して行うことによりCIの時間短縮も期待できます。

Rails 7からはWebpackerは標準で入らなくなり、2022/1/19にはwebpackerは以降セキュリティパッチのみの対応で機能追加は後続のShakapackerへの移行が必要になりました。

Webpacker自体は悪ではないとは思いますが、なにをやっているかわからずRailsやwebpackのバージョンアップのたびにwebpackerへのマイグレーションで苦労していたので私の周りでは脱webpackerすることが多いです。

Webpackerを外すときには config/webpacker.yml および config/webpack の git logやgit blame を見てどんな修正をしているか確認します。大した修正をしてなさそうだとわかったらwebpackerを外してしまって1からwebpackの設定をしてしまうのが楽なんじゃないかなと思っています (これはWebpackおじさんがチームにいる場合なので異論は認めます)。

一度webpackerを外してしまえば今回やったようなbabel => swcの導入などもしやすくなるでしょう。

Dockerレイヤーキャッシュを活用する

CodeBuildでもDockerレイヤーキャッシュを利用する設定ができるためライブラリのインストールをDockerのレイヤーキャッシュを使ってスキップすることを期待できます。

Webアセットを含むRailsアプリの場合、

  • package.json, package-lock.json (or yarn.lock) だけを先にCOPYしてからnpm install && webpackする
  • Gemfile, Gemfile.lock だけを先にCOPYしてからbundle installする

のようにライブラリのインストールに必要なファイルだけを先にCOPYしておくことで、それ以外のファイルを変更しても上記のファイルが更新されない限りはキャッシュが有効になることを期待できます。

まとめ

今回実施した工夫によりCodeBuild実行時間は9分半から4分44秒になり、目標とした半分の時間にすることができました。

  • 最適化前にかかっていた時間 570秒
  • 利用イメージのバージョンアップによりPROVISIONINGフェーズの改善 -137秒
  • babelからswcに変更 -18秒
  • webpackのチャンク分割 -124秒
  • CodeBuildのスペック変更 -56秒
  • トータル 570 - 335 = 235秒
改善前 改善後
f:id:mtgto:20220214133414p:plain f:id:mtgto:20220214133607p:plain

これ以上の最適化は今回のプロジェクトではコスパが悪そうなので、まずは社内の別のプロジェクトでもCodeBuildで古いイメージを使ってないかを確認していこうと思います。

この記事がCodeBuildユーザーやDocker Buildが遅くて困っている方の参考になれば幸いです。


  1. 古すぎても4.0未満はもはやAWS Console上で選択できません。

クックパッドの基盤をフル活用して新卒が新規アプリケーションを作った話

はじめに

こんにちは。クックパッドレシピサービス開発部の宮崎(HN:どや)です。 私は2021年新卒としてクックパッドに入社し、そろそろ1年が経とうとしています。時の流れははやいですね。

さて、表題にも記しましたが、去年の末に新しくサーバーサイドのアプリケーションを作成しました。

クックパッドではサービスメッシュを用いたマイクロサービスアーキテクチャを採用しており、ドメインやチームに応じてアプリケーションが小さく分割されています。今回、クックパッドで利用されるマイクロサービスの1つとして、新しいアプリケーションサーバーを立ち上げました。

社内のマイクロサービスの状況については以下の記事が詳しいので、ぜひ読んでみてください。

techlife.cookpad.com

「アプリケーションサーバーを立ち上げる」と一口に言っても、新しいアプリケーションを作るのは意外と大変です。そもそも、新しいアプリケーションをリリースできる状態に持っていくには、考慮すべき点や手を動かす内容が結構あります。

たとえば、アプリケーションを動かすインフラを用意するのは当然のこと、ログの適切な保存も必要ですし、マイクロサービスアーキテクチャを採用していれば他のサービスとの協調なども考えなければなりません。加えて、クックパッドのサービスの裏側に新しくアプリケーションを追加するとなれば大きな負荷にも対応する必要があります。習熟した人間であればいさ知らず、未習熟の人間がやろうとすれば非常に大きな工数が必要となるでしょう。

本記事では、アプリケーションの立ち上げの話を通じてクックパッドの強力な基盤を紹介します。クックパッドの基盤パワーは恐ろしく、rails new すらまともにしたことが無かった新卒が学習を進めながら2ヶ月足らずでアプリケーションの作成を完遂できました。

アプリケーション作成の経緯

今回作成したアプリケーションはクックパッドのアプリにおける「買い物機能」のドメインにまつわるロジックやリソースを置くサーバーです。

買い物機能は、クックパッドアプリ上で Cookpad Mart のプラットフォームを利用して食材などを購入できる機能です。Cookpad Mart というのは、クックパッドが提供している生鮮 EC のサービスで、クックパッドとは全く別のアプリとして Android/iOS でリリースされています。

ややこしいのですが、Cookpad Mart という独立した生鮮 EC サービスがあり、そのプラットフォームをクックパッドアプリからも利用できるようにしたのが買い物機能*1です。

さて、買い物機能は比較的新しく開発が始まった機能で、元々そのドメイン固有の API サーバーを持っていませんでした。購入や決済などの基本的な機能は既に Cookpad Mart のアプリケーションサーバーにあったため、そこに相乗りする形で開発をしていました。

すなわち、「Cookpad Mart では利用しないが、買い物機能では利用したい」API などがあった場合は Cookpad Mart のアプリケーションサーバーに強引に乗せるか、BFF などのオーケストレーション層に乗せるほかない状況でした。

このような決定をした理由としては以下のようなものが挙げられます。

  • 初期は Cookpad Mart の既存機能しか使わないため、相乗りする形でも大きく困らなかった。
  • 開発チーム内でサーバーサイドエンジニアが不足していた。
  • 新規事業であり撤退する可能性もあったので大きな工数を取らなかった。

特に2つ目が大きく、アプリケーションは開発するだけでなく保守運用なども発生します。当時、サーバーサイドエンジニアが新卒1人しかいない状況で、新しくアプリケーションを作成するのは難しいという判断がなされました。

しかし、買い物機能の機能や体験の拡充に伴い、以下のような問題が発生してきました。

  • どうしても既存のアプリケーションサーバーに置くには難しいデータやロジックなどが出てきた。
  • 買い物機能は大きく体験を変える新規機能であることから開発チーム自体も Cookpad アプリ本体の開発チームとは分かれていたため、Cookpad Mart のアプリケーションを触る際に Cookpad Mart の開発チームとのコミュニケーションコストが増大していた。

こうした課題が無視できなくなってきたため、「アプリケーション分割」という形で、買い物機能用のアプリケーションサーバーを新しく作成する決定をしました。チームのサーバーサイドエンジニアが増えたのも、この意思決定を後押ししています。

このアプリケーション分割において、入ってまだ半年も経たない新卒が旗振りをすることになります。もちろん押し付けられたとかではなく、私が経験も知識もないのに「やりたい!」と言ったらやらせてもらえました。新卒がこういったことをやらせてもらえるのもクックパッドの良いところですね(宣伝)。

逆に言えば、未熟な新卒1人でもアプリケーション分割が許容可能な工数で実現できるほどに、基盤やベストプラクティスが整っているとも言えます。今回、買い物機能の開発速度を落とさないままにアプリケーション作成を行わなければならないため、1、2ヶ月ほどで少ない人数でアプリケーション分割を完了させる必要がありました。もし、後述する開発基盤が整っていなければ必要な工数はもっと大きくなっており、分割をするコストを捻出できず相乗りしたままつらい状況を続けていたかもしれません。

しかしながら、クックパッドの基盤を用いた開発によって工数や複雑性を大幅に縮小でき、許容可能な工数に収めることができました。ここからは、アプリケーション分割の道筋を述べながら、そんなクックパッドの便利な基盤の一端に触れていただきたいと思います。

アプリケーション作成までの道筋

具体的にアプリケーション分割をどのような手順で行なっていったかを順番に見ていきます。その中で、クックパッドの開発を支える優れた基盤を紹介します。

API のインターフェース決定

アプリケーションサーバーを作成するにあたって、要求を元にアプリケーションの要件を定義しました。今回定義したアプリケーションの要件は機能的にも非機能的にも比較的シンプルでした。

今回はまずアプリケーションのフレームワークとして社内で多くの採用実績がある Ruby on Rails を採用しました。クックパッドでは多くのケースでサーバーの実装に Ruby on Rails が採用されており、社内にも大量の知見やライブラリなどの資産があることが決定の主な理由です。

また、新規作成するアプリケーションの API の方式として、以下の2つを検討しました。

  • RESTful
    • Garage なる Restful API をサポートする gem がある。
    • 認証を設定すればクライアントから直接叩くことができる。
  • gRPC
    • Griffin なる自前の gRPC サーバー実装がある。
    • Schema を BFF 層などと共有できる。

それぞれ、Garage や Griffin といった社内資産があり、1から自分で構築をする必要もなく Rails の手軽さと合わせてシュッと API サーバーを作ることができました。

今回は BFF 層の裏に回すユースケースが多そうだったことから、社内のスタンダードにもなっている gRPC を採用しました。クックパッドではスマートフォン向けの BFF 層として Orcha というものが存在しており、これは Java で書かれています。gRPC を採用したことで Schema から Java 向けの型定義を生成できるのも、gRPC を選択した1つの理由でした。

gRPC サーバーの立ち上げを既存の gem などを使って自前で実装しようとすると、マルチプロセス対応やインターセプターの導入など考慮することが多くありますが、Griffin や既存のインターセプター実装である griffin-interceptors のおかげで手軽に作成が完了しました。

インフラ・ミドルウェアなどの準備

クックパッドではアプリケーションにまつわるインフラの管理は主に以下の2つを通して行なっています。

  • Terraform
    • RDS、ElastiCache、VPC、CloudWatch などのリソースの管理・デプロイを行う。
  • Hako
    • 主に実装したアプリケーションの管理・デプロイなどを行う。

この Terraform や Hako といった基盤が既にあるため、レビュープロセスを経ながら安心かつ手軽に各リソースを準備することができました。

社内の Hako と ECS を組み合わせたエコシステムは強力で、開発者はタスク設定を記述するだけでこのエコシステムに乗ることができます。設定ファイルを書いておくことでサイドカーコンテナの定義や環境変数の注入、ALB との紐付け、Autoscale 設定など ECS の設定 + α が簡単に実現でき、後述する運用面の力強いサポートも受けることができます。これにより EC2 インスタンスを用意して、Ruby をインストールして、ミドルウェアも入れて……といったような煩雑かつ工数の多い操作が不要なため、簡単にアプリケーションの追加・修正が行えます。

サービスメッシュでアプリケーション間通信できるようにする

今回新規作成したアプリケーションは、BFF の裏側にあります。つまり、BFF との疎通を考える必要があり、またマイクロサービスにおけるサービス間通信はエラー時の対応など色々と考えることが多く大変です。クックパッドでは Envoy を採用してサービスメッシュを実現することでこの課題を解決しています。

サービスメッシュの設定を記述する共通のレポジトリがあり、そこに設定を追加するだけで簡単にサービスをサービスメッシュに乗せることができます。新規アプリケーションを upstream として登録し、BFF の設定に upstream のアプリケーション名を追加するだけで、Control Plane である Itacho が設定を読み出してよしなにサービス間通信を制御してくれます。

いざデプロイ

CI/CD の実現のための基盤も色々と整っています。

CI については、クックパッドは Jenkins と AWS CodeBuild を採用しています。

PR に対してテストを実行するであったり、ブランチがマージされたタイミングで CI を回すであったりといった設定も、Jenkins のテンプレート設定を元にして簡単に作成できます。

デプロイは Rundeck でデプロイ用のスクリプトを呼び出しています。この Rundeck のジョブを起動すると、デプロイが走り ECS の task や service が作成・更新されます。あとは ECS がよしなにやってくれるので、デプロイ完了です。Rundeck のジョブの起動も Slack や後述する hako-console 上から簡単に実行できます。

リリースに向けて

デプロイが完了したからと言ってすぐさまリリースができるわけではありません。たとえば、以下のような観点は考慮しておくべきでしょう。

  • 監視などのためにログやメトリクスが適切に取られているか。
  • リリース後に負荷に耐え切れるのか。

これについても問題ありません。

まず、ログについては ECS タスクが出力するものは Hako のエコシステムが自動で処理しており、hako-console と S3 で閲覧や検索が可能になっています。hako-console はアプリケーションのメトリクスやシステムについての情報を一覧することができる社内向けの便利なコンソールツールです。

techlife.cookpad.com

アプリケーションレベルのログは fluentd を利用して送信することもでき、Hako のエコシステムを活用すれば fluentd の設定も簡便に行えます。

また、メトリクス については ECS や ALB から CloudWatch に出力されたものや、自前で運用している Prometheus、cadvisor 由来のものがあります。これらのアプリケーションで収集された rps やレイテンシーといったメトリクスは Grafana Dashboard で閲覧することができ、このダッシュボードについても hako-console によってサービスごとに自動で提供されます。

また、リリース後の負荷についても、負荷試験の基盤が整っています。シナリオを作成して、Web コンソール上から負荷試験を実施することができます。

techlife.cookpad.com

ログやメトリクスの収集や可視化は、保守運用の上でとても大切です。またリリース時に負荷に耐え切れるのか負荷試験を行なってシステムを検証することも大規模サービスでは必要になります。しかし、これらの設定や実施のための手順が煩雑だと、後手に回ってしまう可能性があります。上述したようにそのハードルを下げることで、保守運用や安全なリリースのために必要なことを簡単に実現できるようにしています。

こうしてログ・メトリクスの整備や負荷試験も行い、無事にアプリケーションをリリースすることができました。 当日のリリース時にも監視を行なっていましたが、上で用意したメトリクスやログを元に容易に監視が行え、特に障害等は発生せずリリースが完了しました。

まとめ

クックパッドの基盤をフル活用して新卒でもアプリケーションを作成、デプロイ、リリースまで持っていけたよという事例を紹介しました。作業の流れを通じて、クックパッド社内の基盤を紹介しました。

クックパッドには数多くの基盤があり、生産性の向上やサービス品質の安定に一役買っています。今回紹介したのはその一部で、他にも色々な基盤が存在しています。それらは過去のクックパッドの歴史の中で試行錯誤の中で生み出されてきた知識の結晶とも呼べ、いまのクックパッドの開発の大きな支えとなっています。

特に、複雑なアーキテクチャや高い負荷などが見込まれる中で、サービスのために必要な開発が比較的低コストかつ安全に行えるというのは、非常に恵まれていると感じました。

さて、クックパッドでは「毎日の料理を楽しみにする」というミッションを実現するため、一緒にチャレンジする仲間を募集しています。 本記事で紹介した基盤は日々改善が重ねられています。本記事を読んでクックパッドの基盤開発やそれを活かしたサービス開発に興味を持った方は、ぜひカジュアル面談などで一度お話ししましょう!

info.cookpad.com

*1:買い物機能の詳細や技術的な挑戦は以下の記事も参考にしてください。 https://techlife.cookpad.com/entry/2021/01/18/kaimono-swift-ui