トレタ開発者ブログ

飲食店向け予約/顧客台帳サービス「トレタ」、モバイルオーダー「トレタO/X」などを運営するトレタの開発メンバーによるブログです。

Postmanを使ってAPIテストを行うために最低限理解しておきたい3つのこと

この記事は トレタ Advent Calendar 2019 の17日目です。

はじめまして!QAエンジニアの @gonkm です。先週末17年ぶりに再結成を果たしたバンドのライブに参加して筋肉痛です。

トレタでは、今年ローンチしたToreta nowとトレタの予約台帳のオプション機能のQAを担当しています。本日はToreta nowのテストでよく使っているGUIのAPIクライアントツールであるPostmanについて、最低限理解しておきたい3つのことを書きます。

1. Testsを使ってレスポンスの結果を変数にセットする

APIテストを行っていると、レスポンスの結果を次のリクエストのパラメータとして扱いたいことが多いです。
Postmanではレスポンスが返ってきた後に実行されるTestsという処理がありますので、下記のように記述することで、Postmanの環境変数に値をセットすることが可能です。

let data = JSON.parse(responseBody)

pm.environment.set("starts_from_at", data.reservable_times.starts_time);

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

2. Pre-request scriptでテストの前の事前処理を行う

これで {{starts_from_at}} という変数を作成できましたが、実際に次に実行するテストでは、この1時間後の値である {{starts_to_at}} という値が必要になります。
Postmanではテスト実行前に実行されるPre-request scriptという処理がありますので、これを使って1時間後の値を作成してみます。

let  an_hour_ago = pm.environment.get("starts_from_at"); 

an_hour_ago = new Date(an_hour_ago);

an_hour_ago.setHours(an_hour_ago.getHours() + 1);

pm.environment.set("starts_to_at", an_hour_ago);

このとおり starts_to_atstarts_from_at から1時間後の値をセットすることができました。

3. テスト実行時に外部ファイルから値を渡し、変数をセットする

テストによっては実行するIteration毎に値を変えたい場合があります。それに伴い、期待する結果が変わる場合もあるでしょう。
前述のPre-request scriptやTestsを使って頑張ることも可能ですが、PostmanでもData-driven testingをサポートするための外部ファイルの取り込み機能があるので、それを使います。

この機能を使って境界値確認のテストを行ってみましょう。
まず下記のようなCSVを用意します。seatsにはテスト条件となる値を、expected_status_codeにはテスト結果として期待するstatus codeを記述しました。
それぞれはPostman内で {{seats}}, {{expected_status_code}} としてパラメータにセットしておきます。

seats,expected_status_code
0,400
1,200
4,200
99,404
100,404

次にPostmanの実行モジュールになるCollectionRunnerを起動し、 Data からcsvファイルをインポートします。
レコード数によってIterationも自動でセットされるので大変気がきいてます。ナイスですね。

f:id:gonkm:20191217082430p:plain

実際の実行結果は以下の通りです。今回はTestsでのstatus_code チェックに {{expected_status_code}} を使っているので、seats の値によって期待の異なるテストを実行することができました。

f:id:gonkm:20191217082332p:plain

最後に

弊社では監視にDatadogを使っているのですが、PostmanProだとDatadogのSynthetics API TestとIntegrationが可能なようなので来年はテストだけでなく外形監視にも使っていければと思っています。

他にもToreta nowではXCUITestを使ったUIテストの自動化を実施したりしています。この先、Androidのテストの自動化やsnapshotテストの導入、アジャイル開発でのQAプロセスの整備など、やりたいことがたくさんあるのですか、如何せん人が足りていません!ご興味のある方は下記よりご応募頂けるととってもうれしいです。よろしくお願いします!

入社後にAWSアカウントの整理とAWS SSOを導入した話

こんにちは、2019年7月よりトレタにJOINした @aibou です。 本記事はトレタ Advent Calendar 2019の16日目の記事です。

趣味はNFL観戦とボルダリングです。NFLは今年11月にマイナス気温の屋外で現地観戦してきました。 最近リードクライミングの講習を受けまして、ガシガシと岩を登っております。

さて、今回はAWSアカウントとAWS SSOのお話をしようと思います。 既に社内エンジニアへの共有や社内WikiにAWS SSOの利用マニュアルを残していますが、経緯や変遷について記載していないので、トレタ社員の方にも読み物として読んでいただければなと思っています。

免責事項

本記事を参考に実施したことで発生した金銭・セキュリティ等あらゆる問題について責任を負いかねますので、自己責任のもと実施していただくよう、よろしくお願いいたします。

また、誤り等あればはてブ等でご指摘いただけますと幸いです。

入社直後の状態

トレタはしばらくインフラ担当者がごく少数な時期が続いたこともあり、いろいろ改善しないといけないところがあるな、というのが入社直後の印象でした。 とくにAWSアカウント構成の改善はすぐに取りかかれそうだと思ったので、着手することにしました。 当時の状況はこんな感じです。

  • AWSアカウントは2つあり、本番環境用(以降本番アカウント)とステージング用(以降ステージングアカウント)という分け方
  • 本番アカウントが親アカウントとなり、ステージングアカウントと一緒に利用料金を支払っている
  • 監査ログは本番アカウントで保管されている
  • 検証用にリソースを作成する場合はステージングアカウントに作成する
  • インフラ担当者がそれぞれのアカウントに社員ごとにIAMユーザを作成し、クレデンシャルを払い出す

図に起こすとこんな感じです

f:id:yo_aibou:20191212142125p:plain

おそらくAWS利用初期の組織はこういった運用をされている企業が多いと思います。 ただ、ある程度規模が大きくなり運用年数も経っていたトレタでは、以下のような問題が起きていました。

  • ステージングアカウントで検証用のリソースが放置され、プロダクトとして利用されているのかどうかわからない
    • これにより、何人もうかつにAWSリソースを触ることができない
    • よって、AWSのオペレーションがインフラ担当者に集中し、権限移譲が難しい状態に陥っていた
  • IAMユーザについて、本番アカウントとステージングアカウントで管理上のズレが発生している
  • 本番環境と監査ログ保管が同居していることで様々なリスクがある

ちなみに本番アカウントのアカウント名も人の名前になってたりして歴史がありますね笑

f:id:yo_aibou:20191212142158p:plain

とくに「うかつにAWSリソースを触ることができない問題」「本番環境と監査ログ保管の同居」については早急になんとかするべきだと判断し、まずはAWSアカウントをきちんと構造化して、プロダクトが運用されているアカウントに検証用リソースを作らなくてもすむ、監査用のアカウントを作成し監査ログを別にできるような状態にしようと思いました。

AWSアカウントの整理

AWSのマルチアカウントベストプラクティスは、2017年のAWS Summit Tokyoにて「AWS におけるマルチアカウント管理の手法とベストプラクティス」というセッション名でAWSの高田様が登壇されております。
https://d0.awsstatic.com/events/jp/2017/summit/slide/D4T2-2.pdf

スライド終盤の「本日のまとめ」 以降がわかりやすくておすすめです。

今回の構造化もこのベストプラクティスに則り、AWS Organizationを利用し以下のアカウントに分けることにしました。

アカウント名 内容
支払い用アカウント 以下の全ての子アカウントの親として、AWSで発生した料金を支払うアカウント
本番アカウント トレタの台帳システムが稼働している本番環境のアカウント
ステージングアカウント トレタの台帳システムが稼働しているステージング環境のアカウント
検証用アカウント AWSのサービスをお試しで利用したいというときに使うアカウント。消されても基本文句言わない
監査用アカウント 各アカウントのCloudTrailログ等監査に関するログを保管するアカウント

f:id:yo_aibou:20191212142218p:plain

前職でのAWSアカウント運用経験から、将来情シス用のアカウントが必要になるだろうと予想していますが、近い将来に関してはニーズがないと判断し一旦作成対象から外しました。 AWS Organizationを利用すれば、秒でアカウントを作成できるのでそのときに作れば問題ありません。

プロダクトのアカウントに情シスのリソースを作ってしまうと移行がマジで大変なのでみなさん気をつけましょう

アカウント構造化に合わせてアカウント名やメールアドレスもきちんと規則性をもたせるようにし、誰がみても理解できるような状態を目指した結果、以下のようになりました。

f:id:yo_aibou:20191212142246p:plain
(メールアドレスは伏せさせていただきます)

これにより、当初望んでいた「プロダクトから隔離された好き勝手触れるAWSアカウント」と「監査用の専用のAWSアカウント」の用意が完了しました。

ここではCloudTrail等のログを中央集権アカウントに集約する設定については割愛します。ググってください

おまけ:支払いアカウント切り替え

今回の構造化に伴い、AWSの支払いアカウントが本番アカウントから支払い用アカウントに変更となりました。 AWSの営業担当の方にアドバイスをいただき、以下の手順に沿って実施しました。

  1. 支払い用アカウントの作成&AWS Organizationの有効化
  2. 本番アカウントのAWS Organization解散(このとき、ステージングアカウントに支払い方法を設定する必要があります)
  3. 支払い用アカウントのAWS Organizationに本番アカウント、ステージングアカウントを招聘、完了

(この情報については記事冒頭の免責事項も合わせてご確認ください)

2実行後〜3実行前まで、AWSアカウント単体で請求が発生するためご注意ください。 私は事前に社内経理担当者に連絡しており、スムーズに作業ができました。

AWS SSOの導入

さて、上述の通りAWSアカウントを細分化することができましたが、各AWSアカウントへのログインに関して問題が見え始めました。 社内のエンジニア全員に検証環境を触ってほしいと言いつつ、検証用アカウント用のIAMユーザを発行する運用負荷やAWSのアカウント切り替え作業など、煩雑な作業がさらに増えてしまいました。 一般的に、「ログイン専用アカウントを用意し、ログイン後にSwitchロールする」という方法が普及しており、今回もその方法を採用しようかと考えていましたが、何かしらのSingle Sign On(SSO)を導入できないかと検討し始めました。

前職ではとあるIDaaSのSSO機能を利用してAWSにログインしており、SSOの管理画面上で各AWSアカウントへのフェデレーションログインを設定することで、容易にAWSマネジメントコンソールにログインできるような環境ができていました。 つまり、「1度SSOのログイン時に認証するだけでSSOのポータル画面から様々なAWSアカウントにクリックするだけでログインできる」という状態です。 現在残念ながら社内にSSOを導入できていないため、こういった状況には至れていません。 欲を言えば上記SSOサービスを利用しAWSだけでなく様々なSaaSへのログインできる状態を望んでいるのですが、せめてAWSアカウントだけはどうにかしたいと考え、AWS SSOを試験導入(後に本採用)することにしました。

現状、社内でActiveDirectoryやLDAPを運用していないため、ミニマムスタートということでAWSのSimpleADを利用してSSOのユーザ管理を行い、利用を開始しました。 設定についてはAWSのドキュメントを参考にして作業した以上のことをやっていないので説明は割愛します。

AWS SSOの設定が完了し、AWS SSOにログインするとOrganizationsに紐づくログイン可能なアカウント一覧が表示され、マネジメントコンソールへのログインや一時クレデンシャルの払い出しをすることができます。 1度AWS SSOにログインすればセッションが切れるまでの間はアカウントの切り替えは容易です。都度IAMユーザログイン時の多要素認証をする必要はありません。

f:id:yo_aibou:20191212144842p:plain

アプリケーションの追加とIAMポリシー

前職で利用していたIDaaSのSSO機能では、「SAML認証するためにAWSにIDプロパイダーを作成し、ログイン後にどのIAMロールにAssumeRoleするか」という項目を設定する必要がありました。 そのため、新しいAWSアカウントを作成すると

  • ログイン後に適用するIAMロールを作成する
  • SSOにアプリケーションを作成する&SAMLメタデータドキュメントのダウンロードする
  • AWSにIDプロパイダーを作成する
  • IDプロパイダーにSAMLメタデータドキュメントを登録する
  • SSOのアプリケーションにIDプロバイダーおよびIAMロールを設定する

といった作業が必要でした。 例えばOneLoginの設定について、Serverworksさんの以下の記事をご覧いただくとイメージがわくかもしれません。

OneLoginを利用してAWSにSAMLでログインする – サーバーワークスエンジニアブログ http://blog.serverworks.co.jp/tech/2018/06/22/onelogin-aws-saml/

この作業はある程度自動化できるとはいえ、(IDaaS側のAPIが不十分なこともあり)SAMLメタデータドキュメントの交換は自動化できず、ちょっとした運用負荷がありました。 それに対してAWS SSOは、AWS SSO側でSAML認証の設定を自動化できているため、AWS Organizationに新しいAWSアカウントを追加したあとは「どのSSOグループに(どのSSOユーザに)」「どのアクセス権限セットでログイン権限を与えるか」という設定をAWS SSO管理画面上で30秒ほどポチポチするだけでAWS SSOポータル画面からAWSにログインすることができます。

この「アクセス権限セット」というのはいわゆるIAMロールのようなもので、AWS SSO側でIAMポリシーを一括管理しています。 AWS SSOとAWSログインアカウントを紐付ける作業を実施すると、アクセス権限セットに登録されたポリシーを元にAWSログインアカウント側でIAMロールが生成され、IDプロバイダーの生成・紐付けもやってくれます。 細かなリソースの管理は難しいですが(後述)、SSOにログインするユーザのグループをある程度まとめられるならとても便利な機能となっています。

トレタでの現状のログイン先アカウントとアクセス権限セットについては、各職種ごとにグループを作って以下のように分けています。 (やや不正確なところはありますが概ねこのとおりです。)

AWSアカウント 職種 付与権限セット
本番アカウント 正社員インフラ担当 Admin用権限セット
本番アカウント 正社員開発者 Developer用権限セット
ステージングアカウント 正社員インフラ担当 Admin用権限セット
ステージングアカウント 正社員開発者 Developer用権限セット
検証用アカウント 正社員インフラ担当&開発者 Admin用権限セット
支払い用(SSO管理用)
監査アカウント
SREリーダーなど限られた人のみ Admin用権限セット
支払い用アカウント 正社員経理担当 支払情報閲覧権限セット

多要素認証方法の追加(Email → 認証アプリ)

導入当初、AWS SSOはユーザ管理DBにSimpleADを利用するとEmailで認証コードが送られる形式の多要素認証しか選択できませんでした。 ログインするたびにメールの受信箱を確認し番号をコピペするという作業が必要でした。

f:id:yo_aibou:20191212154531p:plain
こういったHTMLメールが送られてきます

が、今年11月に入ってから認証アプリ(Google Authenticatorなど)を利用した多要素認証を設定することができることを気づきました。

https://aws.amazon.com/jp/about-aws/whats-new/2019/10/increase-aws-single-sign-on-security-with-multi-factor-authentication-using-authenticator-apps/

AWS SSOを使い始めてから切実に願望していた機能のひとつです。 設定の変更はAWS SSO稼働アカウント(要は親アカウント)のマネジメントコンソールにログインし、ボタンポチですぐにできます。

f:id:yo_aibou:20191212145927p:plain
見慣れたMFA設定画面 in AWS SSO

しかも素晴らしいことに、

  • 認証アプリを利用しない場合でも多要素認証を強制でき、その場合はメールアドレス宛に認証コードを送信する
  • 管理者だけでなくユーザ自身が認証アプリを登録することが可能

という設定が織り込まれています。 新入社員用等に、SSOユーザ作成時はメールアドレスによる認証で設定しておき、本人が望めば認証アプリを自分自身で設定してもらうというルールにしておくことで、SSOユーザも管理者も楽な状態を維持することができました。

awscli v2(preview)がSSOログインに対応

2019年10月後半頃に「awscli v2のインストール方法」について紹介された記事が上がってきました。 このときは「ふーん、Pythonがバンドルされるのか」ぐらいの気持ちでしかなかったのですが、直後に驚きのリリースが発表されました。 そう、awscli v2とAWS SSOの連携です。

https://aws.amazon.com/jp/about-aws/whats-new/2019/11/use-the-aws-cli-v2-preview-with-aws-single-sign-on-to-increase-developer-productivity/

それまでは、AWS SSOの画面に表示されるクレデンシャルを ~/.aws/credentials にコピペするという運用をしており、作業の手間とプロファイル名書き換え等による事故のリスクを抱えた状態で運用していました。

f:id:yo_aibou:20191212142405p:plain
AWS SSOの画面にAWS_ACCESS_KEY_IDなどが表示される

できるだけ事故を減らすために ~/.aws/config で role_arn と source_profile を使うやや黒魔術的な取り組みをしてたりしました。 説明がめんどくさいので、気になる方は勉強会等で捕まえていただければお話します。

実際にawscli v2とAWS SSOの連携機能を使ってみて利便性は格段に向上しました。 イメージをお伝えすると firebase login:ci とほぼ同等のことを実現しています。 手元のコンソールで aws2 sso login --profile [プロファイル名] と打つことでブラウザの認可画面に遷移し、許可ボタンを押すとSSOのアクセストークンが発行され、そのアクセストークンを使ってフェデレーションログインを実現しています。

f:id:yo_aibou:20191212161925p:plain
SSOログインに対応した ~/.aws/config の設定

f:id:yo_aibou:20191212162032p:plain
aws2 sso login コマンドを打つとAWS SSOへのログイン画面を通過した後にこの認可画面が表示される。認可するとアクセストークンがファイルに保存される

f:id:yo_aibou:20191216114952p:plain
aws cli(v2)の実行コマンド。SSOのアクセストークン発行のみなのでprofile毎にsso loginする必要はありません

しかし、普段よく利用するAWS操作系のツール(Terraformやeksctlなど)が残念ながらSSO用のプロファイル設定に対応していないため、引き続きAWS SSOの画面から一時クレデンシャルをコピペする作業が必要となります。

AWS Organization外のAWSアカウントをAWS SSOへ参画させる

株式会社トレタはトレタCC株式会社(以降トレタCC)という子会社をもち、トレタCCのサービス展開にもAWSを利用しています。 しかしながら、会計上の理由によってトレタCCのAWSアカウントを株式会社トレタが管理しているAWS Organizationには所属していません。 そのため、トレタCCにログインする必要のある社員にのみIAMユーザを発行しているという状況です。 ですが、トレタアカウントとトレタCCアカウントの両方を行き来する社員も存在するため、可能な限りログインの窓口を1つに絞りたいと考えています。 この状況は多くの会社に当てはまるのではないでしょうか。

AWS SSOでは、Organizationに紐付いたAWSアカウントだけでなく、Organization外のAWSアカウントにフェデレーションログインすることも可能のようです。 SAML ProviderやSAMLメタデータアップロードなど、通常のSAML連携の設定を実施すれば連携可能だと思われます。 (本当は設定済みのものをお見せしたかったのですが、現在トレタCCのアカウントとAWS SSOの連携は実施していません。社のポリシーや法務・経理周りの許可がおり次第実施する予定です。)

f:id:yo_aibou:20191212154324p:plain
SSO管理画面のアプリケーションより、External AWS Accountから連携設定できます

AWSタグを利用した社外協力者への限定的な権限付与

AWS IAMでは、これまで多くの利用者に利用されてきたRBAC(ロールベースアクセス制御)だけでなく、ABAC(属性ベースアクセス制御)を提唱しはじめました。 これは、今まで「ロールやユーザごとにポリシーをわける制御」であったのが、「ユーザやロールで同じポリシーを共有し、IAMとリソースに付与されたタグ(属性)の一致をもって制御する」という方式です。 ABACを実現するには、各ロールやユーザにタグを付与するか、re:Invent2019直前に発表されたSession Tagsを利用する必要があります。

RBAC、ABACやSession Tagsの説明はクラスメソッドさんのブログに詳しく記載されていますので、そちらをご参照ください。

[UPDATE]フェデレーションでタグを渡せるSTSの新機能Session Tagがリリースされました #reinvent | Developers.IO

https://dev.classmethod.jp/cloud/aws/session-tag-on-id-federation/

ただ、残念ながらAWS SSOでABACを実現することは難しいです。 何故ならば

  • アクセス権限セットによって作成されるIAMロールにタグを付与することが現状できない
  • SessionTagsを利用するためのSAMLアサーションの属性情報を設定できない

という状況のためです。 AWSアカウントが少なければ、自動生成されるIAMロールを追従してタグを付与することでABACを実現できますが、AWSアカウントやアクセス権限セットの組み合わせが多くなるととてもじゃないけど運用が回りません。

「おそらく近い将来AWS SSOのSession Tagsサポートがリリースされるだろう」という予想のもと、一旦リソースに特定のタグを付与することで「ABACっぽい記述のRBAC」を実現しました。 モデルとして「トレタNowチーム担当の業務委託にSSM Session Managerを特定のEC2にのみ実行可能」を考えます。

今回は SsoPermissionForContractEngineers:toreta-now というタグを対象のEC2インスタンスに付与し、アクセス権限セットのポリシーの一部(ステートメント)を以下のように設定しました。

{
    "Effect": "Allow",
    "Action": [
      "ssm:StartSession", "ssm:TerminateSession"
    ],
    "Resource": "*",
    "Condition": {
        "StringEquals": {
            "aws:ResourceTag/SsoPermissionForContractEngineers": "toreta-now"
        }
    }
}

これにより、SsoPermissionForContractEngineers:toreta-now が付与されたEC2インスタンスにはSession Managerでログインでき、そうでないインスタンスにはログインできないことを確認しました。 現状まだ実務での利用に至れていませんが、対象のリソースにこのタグを付与したり許可アクションを設定できれば、実運用を始めようと考えています。 (Session Tags対応がリリースされれば、ステートメントの toreta-now${aws:PrincipalTag/SsoPermissionForContractEngineers} に置き換えることで同様のことができると思われます。)

この検証で思い当たる点、気をつける点についてわかったことがあるので以下にまとめます。

まずリソース側のタグ管理は、できるだけコードベース管理されていることが望ましいです。 最近だとTerraformやCloudFormationを利用してリソース管理をされている方が多いと思いますが、ABACを実運用する上で必要になると感じました。

また、ポリシーのステートメント文内で利用する条件キーが、AWSサービスによって異なる点に気をつけてください。 例えば、同じEC2にアタッチしているタグであったとしても、ssm:StartSessionaws:ResourceTag であるのに対し、 ec2:StartInstancesec2:ResourceTag だったりします。 各アクションと条件キーの対比については、以下のページに各サービスごとにまとまっていますので、ポリシーを作成するときに参考にしてください。

AWS のサービスのアクション、リソース、および条件キー AWS Identity and Access Management

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_actions-resources-contextkeys.html

とくにS3の条件キーはかなり複雑で、s3:GetObject の場合は s3:ExistingObjectTag 条件キーが必要で、s3:PutObject の場合は s3:RequestObjectTag 条件キーが必要になります。 この s3:RequestObjectTag 条件キーがなかなか厄介で、この条件に合致した場合アカウント内のあらゆるS3バケットにPutObjectできてしまうということを観測しました。

そのため、どうしてもポリシー側でリソース名を記述必要があると判断し、一旦PutObjectを否認するようにしています。 どうしても必要な場合は sts:AssumeRole で専用ロールになってから作業するという逃げ道を考えています。

考察と今後の展望

以上、AWS SSOを導入したお話について執筆しました。 とくにAWS SSOを導入するべきだと思われる状況について私見を述べます。

  • AWSへログインする人が20名を超えている
  • 管理しているAWSアカウントが複数(3以上)あり、それらは1つのAWS Organizationにまとめられる
  • それらのアカウントにログインする役割の人が3種類以上(例:AWS管理者、社員エンジニア、業務委託エンジニア)
  • OneLoginやOktaといったIDaaSのSSO機能を導入していない

これに当てはまる組織に対しては大きな運用負荷軽減効果を望むことができますので、ぜひAWS SSOをご検討されてはいかがでしょうか? とくに「AWSアカウントが5以上あり、ログイン用アカウントからSwitchロールして目的のアカウントにログインしている」という組織の方にはおすすめしたいです。

また、すでにIDaaSを導入されている組織におきましても、AWSアカウントを非常に多く管理されている場合(目安として10以上)、AWS SSOを利用することでコネクター設定の作業負荷が軽減できるかもしれません。 私の経験としても、前職で利用していたIDaaSよりAWS SSOの設定作業が非常に楽だと感じました。 AWSのみAWS SSOで他はIDaaSのSSO、といった使い分けをすることも視野に入れておけばよかった、という気持ちもあります。 AWS SSOでも既に設定されているActive Directoryと連携することができるため、ぜひ検証してみてください。


ところで、本記事を執筆中にFinatextさんが興味深いアドベントカレンダーを公開していました

FinatextにおけるAWSのガードレール戦略の紹介 - Finatext - Medium
https://medium.com/finatext/finatext-aws-guardrail-2705cd1e3679

この記事を拝見させていただき、すぐに「これはAWS SSOを導入したら相当楽になるはず!」と直感しました。 ぜひご検討いただけますと幸いです笑(と言いつつ免責事項をご確認の上、きちんとご調査いただきますようよろしくお願いいたします。)

逆に、「AWS OrganizationのSCPを導入した」ことについては、 その機能があったことを完全に失念しており はっとさせられました。 アカウントレベルで制限したいアクションをアクセス権限セットで封じるのはかなりめんどくさかったので、大変参考になりました。後日設定してみます。

これとABACを組み合わせることでいい感じのアクセス権限払い出しができそうな気がしています。


ここまでAWS SSOのポジティブな話を記載してきましたが、もちろん問題もあります。

まず、ひとつのAWS OrganizationにひとつしかAWS SSOを設定できません。 至極真っ当な仕様であるとは思いますが、これによりAWS SSOの検証・実験のハードルが高いことが問題点であると感じました。 完全に独立したアカウントを用意すれば検証は可能ですが、稟議・経費精算といったワークフロー上の手間が増えることになります。

もうひとつは、前述の通りABACが現状難しいことと、アクセス権限セットのポリシーをコードベース管理できないことです。 前者はおそらくそのうちリリースされるとして、後者は管理のためにAPIの提供を望みます。

また、現場のエンジニアが片手間でSSOを設定することは負担が大きく、せめてユーザ管理をActiveDirectoryやLDAP等を利用して情シス側に移管できればと思っています(何卒宜しくお願いします)。


最後にアドバイスとして、組織の運用を大きく変えるような仕組みを導入したいときは「まず自分を生贄に捧げる」という覚悟を持って取り組むと、それを運用し始めたときの利点欠点の理解が進みます。 私の場合、それまで使っていたIAMユーザを捨ててすべてAWS SSOに寄せることで良い点・悪い点を実感として得ることができ、これらを同僚に共有することで本当に導入すべきかどうかをきちんと議論できたと思います。 また、良い点ばかりに目を向けていると導入後に困難な事象に遭遇した場合に不幸を生む可能性があるので、「きちんと悪い面にも目を向ける」「どうしてもやめたくなったらどうするか」ということをきちんと考えて、共有しておけば、自分がいなくなったとしても

終わりに

この記事が投稿される頃には Bears @ Packers が終わっているはずで、その試合の結果次第では地区優勝に大きく近づきます。 Packers勝利&Rams敗北でプレーオフが確定しました!残り2戦のうち1勝すれば地区優勝です。 ただ、プレーオフに向けて不安要素がかなり多い(とくにラン守備とフリーになれないWR陣)ですが、1発勝負ぜひ勝ってスーパーボウルに行けたらと思ってます。 (でもWeek12でボコボコにされた49ersと飛び道具テイサム・ヒル擁するセインツには勝てる気がしない)

Go Pack Go!

店舗検索APIにページネーションを追加した話

トレタ Advent Calendar 2019 の14日目の記事です。

こんにちは。ニュージーランドで1週間息抜きをしてきた2日後に、ボケボケの頭のまま 社内テックトーク で決して得意とは言えない競技プログラミングに関して話すことになってしまい、自分の計画性のなさを恨んでいるモバイルアプリエンジニアの大野です。 帰国した2日後に火山が噴火して驚きました。
本Advent Calendarには実に3年ぶりの登場となります。


私が主に関わっている Toreta now では、先日公開されたver1.2.0 から店舗を検索する機能にページネーションが追加されました。ページネーションが必要なほどサービスが成長してきたんだなーと思うと嬉しい限りです。
が、そのページネーションを実現するためのAPIの設計で一悶着あったので、本記事ではそのことについて紹介します。

登場人物

  • サーバサイドエンジニア (以下 「サ」)
  • 私とは別のモバイルアプリエンジニア (以下「モ」)

なお、GETのAPIなので実際にはリクエストパラメータはqueryで渡していますが、可読性のためにここではjson形式で表示することとします。 また、実際のAPIとはパラメータやデータのフォーマットなどが異なります。

最初期のバージョン

私「店舗検索は(中略) な感じだし、offsetベースよりはcursorベースのページネーションのほうが良いですよね。ってなわけで、こんな感じでリクエストパラメータとレスポンスを考えてみたんですがどうでしょ。」


北緯45°, 東経135°の位置にいるユーザーが、付近の店舗を検索する場合

最初のリクエストには、席数と予約の開始時間とユーザーの現在地を入れる。

# 1回目のリクエスト
{
    "seats": 4,
    "starts_from": "2019-12-14T21:00:00+09:00",
    "location": {
        "latitude": 45.0,
        "longitude": 135.0
    },
    "next_token": null
}

最初のリクエストへのレスポンスには、店舗一覧の情報と次回の検索用の next_token が入る。 next_token がnullだったら、ページネーションが完了したと判断する。 next_token には次回の検索の先頭のindexが入る。

# レスポンス
{
    "restaurants": [
        {
            "name": "レストラン1"
        },
        {
            "name": "レストラン2"
        }
    ],
    "next_token": "5"
}

2回目以降のリクエストでは、next_token 以外には最初のリクエストと同じ値を入れる。 next_token には前回のリクエストへのレスポンスに入っていた値をそのまま入れる。

# 2回目以降のリクエスト
{
    "seats": 4,
    "starts_from": "2019-12-14T21:00:00+09:00",
    "location": {
        "latitude": 45.0,
        "longitude": 135.0
    },
    "next_token": "5"
}

next_token の実装がどうなっているかはクライアント側では意識せず、単に受け取った値をそのまま使うだけです。 こうすると、UI以外のページネーションのロジックを全てサーバ側に押し付けることが出来るので、クライアント側では楽ができます。

サ「頭の中では大丈夫そうという気持ちになっています」 (原文ママ)
私「あざっす」

モさんによる指摘

モ「ちょっと待ってそれだと最初のリクエストと2回目以降のリクエストで location の値が違ったらどうなんの。ユーザーの位置が変わったら出てくる店舗も変わっちゃうでしょ。」

私「確かに。必ず同じ値のlocationを入れるようにする、って制限を付けるのもAPIとしては微妙っすね。じゃあこんな感じでどうでしょ。」


北緯45°, 東経135°の位置にいるユーザーが、付近の店舗を検索する場合 改

最初のリクエストには、席数と予約の開始時間とユーザーの現在地を入れる。

# 1回目のリクエスト 改
{
    "seats": 4,
    "starts_from": "2019-12-14T21:00:00+09:00",
    "location": {
        "latitude": 45.0,
        "longitude": 135.0
    },
    "next_token": null
}

最初のリクエストへのレスポンスには、店舗一覧の情報と次回の検索用の next_token が入る。 next_token がnullだったら、ページネーションが完了したと判断する。 next_token には、次回の検索の先頭のindexと検索のlocationが入る。

# レスポンス 改
{
    "restaurants": [
        {
            "name": "レストラン1"
        },
        {
            "name": "レストラン2"
        }
    ],
    "next_token": "5, latitude=45.0, longitude=135.0"
}

2回目以降のリクエストでは、seats, starts_from には最初のリクエストと同じ値を入れる。 next_token には前回のリクエストへのレスポンスに入っていた値をそのまま入れる。 サーバ側では next_token の値をparseして使う。

# 2回目以降のリクエスト 改
{
    "seats": 4,
    "starts_from": "2019-12-14T21:00:00+09:00",
    "next_token": "5, latitude=45.0, longitude=135.0"

}

私「というかこれだと2回目以降のリクエストに必要なパラメータがどれかわかりにくいし、いっそのこと全部 next_token に入れちゃっていいんじゃね? 」

サ, モ「確かに」


北緯45°, 東経135°の位置にいるユーザーが、付近の店舗を検索する場合 改々

最初のリクエストには、席数と予約の開始時間とユーザーの現在地を入れる。

# 1回目のリクエスト 改々
{
    "seats": 4,
    "starts_from": "2019-12-14T21:00:00+09:00",
    "location": {
        "latitude": 45.0,
        "longitude": 135.0
    },
    "next_token": null
}

最初のリクエストへのレスポンスには、店舗一覧の情報と次回の検索用の next_token が入る。 next_token がnullだったら、ページネーションが完了したと判断する。 next_token には、次回の検索の先頭のindexと全ての検索条件が入る。

# レスポンス 改々
{
    "restaurants": [
        {
            "name": "レストラン1"
        },
        {
            "name": "レストラン2"
        }
    ],
    "next_token": "5, latitude=45.0, longitude=135.0, seats=4, starts_from=2019-12-14T21:00:00+09:00"
}

2回目以降のリクエストでは、next_token に前回のリクエストへのレスポンスに入っていた値をそのまま入れる。 サーバ側では next_token の値をparseして使う。

# 2回目以降のリクエスト 改々
{
    "next_token": "5, latitude=45.0, longitude=135.0, seats=4, starts_from=2019-12-14T21:00:00+09:00"
}

私, サ, モ「良さそう」


というわけで最終的にはこの内容で落ち着きました。

サーバ側のロジックがちょっとややこしいことになってしまい、実際に実装途中でいろいろバグりましたが、仮にバグがあった場合サーバ側はすぐに直して反映させることが出来るのに対し、モバイルアプリは審査の期間が挟まれたり何よりユーザーが最新版にアップデートしてくれるとは限らなかったりするため、クライアントにモバイルアプリが含まれることがわかっている場合にはややこしいロジックをサーバー側に押し付けるのは正しい設計だと信じています。

加えて、こうすることでサーバ側、クライアント側ともに状態を持たずにやりとりが出来るので何かと都合が良いことに気付きました。 (クライアント側は一旦 next_token を保存しないといけませんが。) 何となく、関数型プログラミングと近い考え方なのかな、と思ったりしています。

終わりに

ややこしいロジックを正しく実装してくださったサさん、及び的確な指摘でver1.2.0をリリースまで導いてくださったモさんに、この場を借りてお礼申し上げます。 明日はフロントエンドエンジニアの上垣がなにか書いてくれます。

© Toreta, Inc.

Powered by Hatena Blog