古いAWSサービスのフルリプレイス

外山英幸氏:それでは、「DevOpsの劇的改善 〜古いAWSサービスから王道のマネージドサービスでフルリプレイス」ということで、株式会社ビズリーチの外山が発表させていただきます。よろしくお願いします。

(会場拍手)

まずは自己紹介なのですが、外山英幸と申します。

ご紹介にありましたように、ビズリーチという会社でキャリトレ事業部のプロダクト部部長をやっております。

写真にもありますが、現在2歳半の子どもがおりまして。最近の保育園はすごくて、2歳半からすでに英語や漢字を教えているんですね。エンジニアの父としては、そこに負けないように、そろそろもうAWSの設定方法を教えていくタイミングなんだろうと思っているので、きっと来年には彼が「3歳児でもわかるAWS構築方法」という講演をしてくれることを期待しています(笑)。

では、本日のアジェンダです。

まずは会社紹介です。

ビズリーチという会社は、ここ数年テレビCMをやらせていただいておりますので、社名は聞いたことのある方もいらっしゃるのではないかと思っております。2009年に創業し、今年で10周年を迎えた会社です。

この10年間、ありがたいことに急成長させていただいておりまして、それを象徴する数字が従業員数です。年々増加を続け、現在1,449人になっています。

わたしたちは多事業展開をしておりまして、サービスは10以上存在していますが、本日は「キャリトレ」というサービスのシステムの裏側についてお話しさせていただきます。

キャリトレについて簡単な紹介をさせていただきます。求職者様と企業様が出会う転職のプラットフォームを提供しています。よく「ビズリーチは人材紹介業をしている会社ではないのか?」と言われるのですが、実はビズリーチは人材紹介業はやっておりません。基本的に、HR領域を中心としたインターネットサービスを提供するテック・カンパニーです。

ですので、社員数が1,500名弱いる中でエンジニアも300名以上いる会社となっております。今自分が携わっているキャリトレのプロダクト部にも、エンジニアが33人所属しています。

職種としては、Product Manger、Designer、Front End Engineer、Mobile App Engineer、Server Side Engineer、Infra Engineerといった職能があるのですが、それぞれ専門でやっているというより、できる人は全部やるといったかたちで、フルスタックを重視している組織になっています。

いろいろな事業部がありますので、全サービスの品質を一定以上に保つために、全社の事業部横断組織として、QAやセキュリティ室、全社SREといった組織があり、ここで全社の品質を担保していいます。

「キャリトレ」はプロダクトをリプレイスしている最中です。本日はこちらのリプレイスのインフラ部分について発表させていただこうと思っております。よろしくお願いします。

リプレイスしている理由

まず「なぜリプレイスしているのか?」というところなのですが、創業事業のビズリーチだけでなく、ありがたいことにキャリトレも急成長させていただいています。その中で、矢継ぎ早にシステム・機能の追加を行ってきていました。インフラ部分も同じように対応してきました。

キャリトレは2014年からスタートしているサービスなのですが、アーキテクチャはその時点ですでに数年の運用実績があったものを採用しています。ですので、今のトレンドからすると7年分ぐらいの乖離がある状態なんですね。

7年前といいますと2012年頃。2012年頃からAWSを使われていた方もいらっしゃるとは思いますが、主要な機能はあったものの、運用面ではまだアナログな部分も多かったですし、王道の使い方も確立されていませんでした。そのため、キャリトレのシステムの運用コストが高くなっているという背景がありました。

エンジニアの数は年々増えていっていますので、年々リリース数が増えていくのが理想ではあるのですが、保守コストが増えている状況です。これは多くのプロダクトで負債を抱えだすと、よくある例なのではないかと思います。

サービスとしては10倍や20倍と、さらに大きくしていきたいと思っているので、今のうちからこの状況をちゃんと改善していきたいと考えています。今までもこの状況を改善するために「負債返済のためのプロジェクト」を立ち上げ、部分的に解消はしていたのですが、その状況を根本的に改善させたいと考えていました。システムをより成長させて、運用コストを低下させるために、インフラ基盤、データモデル、サーバアプリケーション、フロントエンドをすべてリプレイスすることを決断しました。

モジュール的には、3つのWebサービス、1つの管理画面、2つのアプリ(iOSとAndroid)が対象となります。Batchも50本以上。レコメンドエンジンや、外部システムとの連携API・連携Batch、ありとあらゆるものをすべてゼロから作り変える一大プロジェクトを遂行している最中です。

「フルリプレイスすれば、負債のたまらない理想のシステムになるか?」といわれますと、そんなことありません。エンジニアを15年続けるなかで、自分もフルリプレイスは過去に何度もやってきていますが、リリースしたものが理想的な状態になることは過去に一度も経験したことがありません。

リリースした時点から負債はたまるものだと思っているのですが、負債がたまったとしてもそれを返済しやすいスケーラブルなシステムにはできます。今回は最大限にそうなることを目指して、インフラも含めてそういった刷新をしている状態です。

認証基盤のリプレイス

ではどんなことをやったのかですが、本日は8章まで用意しています。

最初は「認証基盤のリプレイス」です。

Webサービスにおいて、認証という仕組みがないWebサービスはほぼないと思っています。会員登録やログインといったことは、どのようなサービスでも必ずあるのではないかと思います。

認証システムを構築するのは簡単なのですが、システムが大きくなってくると、認証システムの不具合に対するリスクが比例して大きくなります。そのため、運用コストが高くなっている会社はたくさんあるのではないかと思っています。その結果、一度作った認証システムに機能追加することに対して、ネガティブになりやすくなってしまいます。

基本的には認証のチェックが必要なのですが、パスワードを連続で間違えた際のアカンウトロックやメールアクティベーション、認証コード、SSOなど、これらを作った時にはトレンドを追いかけていたとしても、やはり年々新しいものが出てきますので、それを追随して作るのは大変です。

大変といってもやりたくないということではなくて、保守を考えたときにちょっとナーバスになるということをこのように表現しています。

また、昨今はさまざまな認証との連携が当たり前に行われる時代です。Amazonとの連携、Facebook、Google、Twitter、BearerやSAMLと連携しますので、そういった認証の連携も求められます。これもあとから増やすことは工数としては簡単なのですが、リリースのたびに全部テストし直さないといけません。

セキュリティオートメーション。「アカウントリスト攻撃」というのは、例えばパスワードを「password」という文字に固定して、いろんなメールアドレスでアタックを試す。「誰かしら『password』にはしているだろう」ということでさまざまなメールアドレスを試すような攻撃のことを指しているのですが、それを防げている会社はおそらくそれほど多くないのではないかと思います。

アプリケーションで防ぐのが難しい部分でもありまして、AWSでもCloudWatchでログインのログを拾って、それをLambdaで処理してサードパーティ製のなにかに投げて、ブロックすべきIPだと判断されればWAFにIPを追加するといった感じで、オートメーション化はできるにはできるのですが、この仕組みを作ったあとにちゃんとモニタリングするのも大変ですし、これも言ってしまえば大変な仕組みになっています。

新システムの構成

これを新システムではどのようにしたかといいますと、Amazon Cognitoを採用しました。

Cognitoというサービスがすばらしい仕組みでして、基本的な認証機能はすべて揃え、管理されています。

メールアクティベイト、認証コード認証、SSO、パスワード間違い時の一定時間ロック等もありますし、「パスワードを忘れた方へ」もありますし、そのほかさまざまな外部認証にも対応していますので、こちらもワンボタンでいろいろな外部認証が追加できます。

なによりこれがすばらしかったのですが、CognitoのAdvanced Security Features。こちらはまだベータの機能なのですが、充実のセキュリティ対応となっております。

先ほど説明しましたメールアドレスリスト攻撃のほうもASFは対応していますし、異なる端末や異なるIPでのログインをした場合に、そのイベントを通知してくれたり、TOTPによるMFAに対応していたり。これだけでもCognitoを導入するメリットがあると感じています。認証周りとしては本当にすばらしいマネージドのサービスだと思います。

Cognitoを新規のプロダクトで導入しようと思っている方は、そんなに苦労せず導入できると思います。ただ、キャリトレはもともと構築しいたサービスがあったので、その認証基盤をCognitoに置き換えようとすると、相性が悪い部分がいくつかありました。

例えばこれもよくあるパターンですが、既存のシステムですでに会員のIDを払い出す仕組みを持ってるんです。求職者様のIDやリクルーターのID、オペレーターのIDなど、さまざまなIDとそれに対するログインを持っているのですが、最初からCognitoを使うのであれば、それをUUIDで管理したりもできます。ですがすでに払い出しているIDがあるので、Cognitoを使ったとしても会員IDはCognito側にも渡せません。

Cognitoを導入するのであれば、メールアドレスも本来Cognitoだけに置けばいいのですが、そのあとの運用を考えたときに、弊社のサービスでもそうですが、やはり大量のメール配信を行いたいわけです。求職者様に一括メール配信を送りたいとなったとき、Cognitoにしかメールアドレスがない状態ですとそこもやりづらいので、できればRDBの方にメールアドレスの写しを保管しておきたい、というケースが出てきました。これはそれほど珍しくないのかなと思っています。

ここについてはわりとアナログな対応を入れました。クライアントからするとCognitoの認証をしているだけなのですが、裏ではLambdaを挟んで認証用のAPIを用意して、そこで処理をしています。この認証用のAPIは決して認証しているわけではなくて、付随する細かい簡単な機能のみを提供しているAPIになっています。

先ほどの仕組みを実現するために、サインアップ時にCognitoのメールアドレスをDBに登録してあげるし、DBで払い出されたIDをCognitoに返してあげて、Cognitoのカスタムフィールドで会員IDを保持するということをやっています。

メールアドレスの変更も、Cognitoに対してメアドの変更をするのですが、そこが変更された場合はDBもちゃんと更新してあげるという、同期を取る仕組みです。そういったところでケアしています。

Facebook連携における問題点

次に、これはキャリトレ独自のポイントだったのですが、キャリトレは現在Facebook認証を導入しているところです。この移行を色々と考えたのですが、結論としては、Cognitoにそのまま移行するのは無理です。

今導入しているFacebook認証の認証情報をCognitoに移行するのは無理だという結論になりまして、結果的に、Cognitoに投げる前に先ほどの独自の認証用APIで一定のチェックをして、クリアしたものだけFacebook連携を行うといったことをやっています。

認証基盤のリプレイス

今、Cognitoのところで認証部分をリプレイスしたのですが、認証はあくまで本人を特定するため、本人が正しい人であることを証明するものになっています。次は認可部分です。認可は、「本人が正しいことは証明されました。では、その本人がアクセスしていいAPIは〇〇ですよ」という許可を与える部分になっています。ここもリプレイスしました。

弊社では、CognitoとAPI Gatewayを組み合わせることによって、認証・認可の仕組みを提供しています。まず、Cognitoで本人を特定されたあとに、CognitoのIdentity Poolで「その本人が誰なのか」「管理者なのか、一般ユーザーなのか」といった情報を返して、その情報を基にSPA側で管理者しかアクセスできないコンポーネントを表示するなど、表示の切り分けをして、そこからAPI Gatewayにリクエストを投げています。

API Gatewayとしては、仮に一般ユーザーがadminユーザーしか叩けないAPIを叩いてきたとしても、そこは権限がないという制御をしていますので、API Gateway側で403エラーが返されます。許可されている者のみ、裏のJavaで書かれているAPIにリクエストが行きます。

この最大のメリットは、このアプリケーションから見たときに、認証と認可の仕組みが完全に疎結合になっていることです。ですので、アプリケーションを修正したときに、間違って使えてはいけないAPIが使えるといったことが、絶対にありません。そこが分離されている状態です。

あとは、アプリケーションの関係ないところを修正したときにTwitterのログインができなくなるということもありません。そこが疎結合になっている部分が最大のメリットだと感じています。

ロールの変更

ここは細かい話なのですが、そのロールの情報をどう持たせていくのかという部分はみなさんの会社をイメージしてほしいのですが、個人はなにかしらの組織に属しているという前提です。

自分はキャリトレのプロダクト部に属しているので、「プロダクト部に属していますよ」という情報をcognito:groupsに持たせて、プロダクト部がやれる作業はどれかというロールの情報をcognito:roleで持たせる。それが巡り巡って、そこに紐付いたIAM RoleがSTSで返りますので、API Gatewayではその情報を基にアクセスできるAPIの制御が可能となるという仕組みです。

ここは細かいのですが簡単に補足しておきますと、そのロールは最初に定義したきり更新しないものではないと思っておりまして、部署が移動したり役割が変わったり、最初は一般ユーザーだったものをadminに変えたり、ロールは変わりうるものだと思います。

そのロールの変更をどうしているのかというと、ロールの変更を権限として持ったユーザーは、ロール変更要求のAPIを叩く権利を持っています。そのAPIを叩くと、APIでデータベースに保管されているロールの情報を更新していきます。RDBの情報を更新すると、非同期でそのイベントを拾って、LambdaでCognitoのUser Poolの情報を更新していきますので、これでサイクルが1周回って、本人が変えたい対象のロールがアップデートされるという流れです。

これで構築していたのですが、これも一部問題がありました。Cognitoのgroupsやroleを使うパターンですと、そのグループやメンバーポリシーの変動性が高かったり、Gateway自体のエンドポイントを頻繁に増えるようなシステムだと、そのマッピングが複雑になってしまうという問題がありました。

 

ここを楽するために、弊社では、先ほどのCognitoのユーザーグループを使うのをやめて、単純にCognitoのUser Poolのカスタムフィールドにスコープ名を追加しております。スコープ名を受け渡していき、API Gatewayでは渡されたスコープ名を基に、DynamoDBでそのスコープがどのAPIにアクセスできるのかポリシーを持たせるようにしておいて、そこと突き合わせて叩けるのか・叩けないのかチェックしています。