[レポート] ARC327 – Hands-on SaaS: AWSにおけるマルチテナントソリューションの構築 #reinvent

こむろです。ワークショップ2本目のレポートです。

概要

SaaS presents developers with a unique blend of architectural challenges. Supporting a multi-tenant model often means re-thinking your approach to almost every layer of your architecture. Onboarding, security, data partitioning, tenant isolation, identity—these are areas that must be factored into how you design, build, and deploy your SaaS solution. Of course, the best way to wrap your mind around these SaaS architectural principles is to dig into a working example. In this workshop, we’ll expose you to the core concepts of SaaS architecture then dive into a reference SaaS architecture where you can see the moving parts of a SaaS solution in action. The goal here is to provide a series of activities that allow you to interact with a functional solution, introducing code and configuration that realizes and extends the capabilities of this SaaS environment. Through this combination of a brief lecture and hands-on exercises, you’ll get a healthy dose of SaaS best practices all through the lens of a working reference solution.

マルチテナントモデルをサポートすることですべてのアーキテクチャレイヤを再考することを意味します。新たなユーザーの管理やセキュリティ、データのパーティショニング、テナントの独立性や一意性, こういったものを主要な要素に取り入れながら設計を行う必要があります。

このワークショップではSaaSアーキテクチャの核となるコンセプトを明らかにし、SaaSソリューションのパーツがどのように変動していくのかを確認します。このワークショップのゴールは機能的なソリューションと関わる一連のアクティビティを通して、SaaS環境の性能を拡張するコード、設定を紹介します。

資料

ワークショップ

ワークショップは提供されたAWSアカウントを利用して行いました。

こちらのワークショップでもCloud9環境を利用し、事前に用意されているアプリケーションを徐々に進化させながら(新たなバージョンをDeployしていく)、機能の拡張に応じてインフラ側の設定変更や構成の変更を確認していきました。

序盤

こちらも序盤はマルチテナントを実現する上で必要な要素の解説から入ります。

  • Multi-tenantとは
    • 一つのApplicationで複数のユーザーが互いのデータソースを独立性を持って管理、アクセスができるアーキテクチャをここでは指す
    • Multi-tenant=SaaS
  • このワークショップで体験できる要素は以下
    • Onbording
    • Authentication
    • Application Services
    • Data partition
  • このワークショップに含まれないけど、Multi-tenantのソリューションを考える上で必要な要素
    • Management and monitoring
    • analytics
    • operations
    • Billing
    • SaaS DevOps

Amazon Cognito

ユーザーとポリシーを接続。User PoolとIdentityを利用します。Tenantに属するユーザーの管理やRole Mappingの機能による権限管理を行います。

Partition

データストアにはDynamoDBを利用。PartitionはMulti-tenantの変更により変更する必要があります。Multi-tenantへの変更前はProductionIdをPartition Keyに指定。Multi-tenantを実現するためには、Partition Keyを TenantIdへ変更する必要があります。

さらに進化。Multi-Tenantを実現するためにはPartition KeyがTenantIdだけでは足りません。SortKeyにProductIdに指定します。

Admin/User Role

ユーザーのRoleを分割しAdminユーザーを実現させます。Amazon CognitoのRole mappingでIAM Role Policyに突き合わせを行い、CognitoのユーザーとIAM Role Policyを紐づけます。DynamoDB LeadingKeyにIAM Role Policyで指定。これによりそのRoleに紐付いたユーザーのアクセスできるデータ項目を制限することができます。

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/specifying-conditions.html

参照リンク

一時的に利用できるAWSアカウントが発行されています。以下のリンクにアクセスしhashIDを入力してアカウント情報を取得しました。

Githubのリンクはワークショップで利用した資料です。

ワークショップ内容

事前に用意されたSPAをDeployしていき、アプリケーションの進化と共にインフラの構成も進化させていきます。

Lab1

はじめはまだ何もユーザー管理の機能がないため、ユーザー認証と登録のアプリケーションをDpeloyしていきます。まずはCognitoを操作していきます。

このワークショップでもCloud9を利用しています。このCloud9環境は事前に主催者側が用意してくれているもので、GithubはClone済み。様々な環境、ツールもインストール済みのCloud9が構築されていました。そのため、事前準備に必要な作業はすべてスキップされています。

Create Cognito User Pool

まずはCognitoでUser Identityを作成していきます。

このあたりは、テキストに沿ってひたすらバシバシ入力していきます。customの項目を追加する際に一つ注意点がありました。ネットワークの調子からかSaveがうまくいかず、何度かSave動作を繰り返すと custom: というPrefixがネストして重複して登録されていく現象が見受けられました。このcustomのAttributeはあとから変更が効かないようなので、万が一Saveに失敗した場合は、再度作り直したほうが良いかもしれません。

作成完了。

Cognito User Identity Pool

続いてUser Identity Poolを作成します。先程のUser Poolで作成された UserPoolIdClient Id を控えておき、 Authentication providers を展開して入力します。

名称も入力し作成完了。

Application Code with Cloud9

Cloud9上でアプリケーションのコードを確認します。基本的にこのワークショップではコードを修正することはなく、この機能がDeployされるよー、動作の変化と共にコードはこんな風になってるよーという感じで進行します。

Check ALB

アプリケーションのフロントにいるALBについて確認します。curlで実行するためのエンドポイントを確認するのが目的です。この後も何度か利用するためメモをしておきます。

modul-LoadB-1WNSGIU177ZF5-1620093013.us-east-1.elb.amazonaws.com が今回利用するアカウントで構築されているALBのエンドポイントになるようです。

Invoke create user request with curl

curlコマンドで確認します。

$ curl --header "Content-Type: application/json" --request POST --data '{"userPoolId": "us-east-1_1vRhSKm3n", "tenant_id": "999", "userName": "test@test.com", "email": "test@test.com", "firstName": "Test", "lastName": "User", "role": "tenantAdmin", "tier": "Advanced"}' http://modul-LoadB-1WNSGIU177ZF5-1620093013.us-east-1.elb.amazonaws.com/user/create{"User":{"Username":"test@test.com","Attributes":[{"Name":"sub","Value":"e370a512-f732-4982-8849-2f4afa8baa10"},{"Name":"custom:tier","Value":"Advanced"},{"Name":"custom:tenant_id","Value":"999"},{"Name":"given_name","Value":"Test"},{"Name":"family_name","Value":"User"},{"Name":"custom:role","Value":"tenantAdmin"},{"Name":"email","Value":"test@test.com"}],"UserCreateDTeamRole:~/environment $ 

ユーザーが新たに作成されました。Cognitoの方でも確認します。

S3 Hosting Website

S3に静的なページが配置されています。

このエンドポイントを叩いてみるとLoginフォームが出力されます。

Registerを指定し、ユーザー情報登録画面を開きます。

必要な情報を埋めてRegistered。ここはメールを受け取る必要があるので受信可能なメールアドレスを指定します。

Cognitoの設定で指定したメールの文面でアカウント情報が送信されます。

ログインできることを確認します。

認証情報の確認

認証情報はヘッダにJWTとして格納されています。ログインする際にGoogleのDeveloperツールを有効にしておき、値を確認します。

デコードしてみるとユーザーの情報が格納されている事がわかります。

これでユーザーの認証とユーザーをIdentifyできる登録機能が追加されました。これでLab1は終了です。

Lab2

Lab2はいよいよMulti-tenant化です。

Deploy v1 App

まずは新たなMulti-tenantに対応したアプリケーションコードをDeployします。

Check API Gateway

V1アプリケーションではAPI Gatewayを利用してアプリケーションをホストしています。そのため、ここではLab1の手順とは異なりAPI Gatewayのエンドポイントを探してHealthのエンドポイントを叩いて確認します。

ここでは https://hg14b6kw0b.execute-api.us-east-1.amazonaws.com/prod が対象のエンドポイントになります。

Invoke Health Endpoint

API GatewayのStageは prod を選択しています。そのためパスに prod が追加されています。

$ curl -X GET 'https://hg14b6kw0b.execute-api.us-east-1.amazonaws.com/prod/product/health'
{"service":"Product Manager","isAlive":true}

正常にアプリケーションと導通されているのが確認できました。

Create DynamoDB Table

DynamoDBにTableを追加します。PartitionKeyは ProductId を指定します。

Create New Item

API Gatewayを通してアプリケーションへItemを追加するRequestを実行します。

$ curl --header "Content-Type: application/json" --request POST --data '{"tenantId": "123", "sku": "1234", "title": "My Product", "description": "A Great Product", "condition": "Brand New", "conditionDescription": "New", "numberInStock": "1"}' https://hg14b6kw0b.execute-api.us-east-1.amazonaws.com/prod/product
{"status":"success"}

Check DynamoDB Table (ProductBootcamp)

DynamoDBにある ProductBootcamp というテーブルにItemが追加されているかを確認します。

Itemが追加されていることが分かります。

Deploy V2 App

V2アプリケーションをDeployしていきます。これはMulti-tenantに対応したアプリケーションです。そのため、Deploy後にデータストアのPartition構成を変更する必要があります。

Update DynamoDB Table

これまでのPartition構造は以下の通りでした。

  • Partition Key: ProductId

これだと複数のテナントで同じProductIdを登録することができません。そのため、Partition構造を以下のように変更します。DynamoDBのテーブルなのであまりアイテム数は登録されていませんがセオリー通りにTableを削除して再作成します。

  • PartitionKey: tenantId
  • SortKey: productId

Invoke Health Endpoint

再度Healthのエンドポイントを叩いて正常に動作しているかを確認します。

$ curl -X GET 'https://hg14b6kw0b.execute-api.us-east-1.amazonaws.com/prod/product/health'
{"service":"Product Manager","isAlive":true}

問題なさそうです。

Create New Item

再作成したテーブルにProduction Itemを登録します。

$ curl --header "Content-Type: application/json" --request POST --data '{"tenantId": "123", "sku": "1234", "title": "My Product", "description": "A Great Product", "condition": "Brand New", "conditionDescription": "New", "numberInStock": "1"}' https://hg14b6kw0b.execute-api.us-east-1.amazonaws.com/prod/product
{"status":"success"}

登録完了。さらに同じ sku でもう一つアイテムを追加します。

$ curl --header "Content-Type: application/json" --request POST --data '{"tenantId": "456", "sku": "1234", "title": "My Product", "description": "A Great Product", "condition": "Brand New", "conditionDescription": "New", "numberInStock": "1"}' https://hg14b6kw0b.execute-api.us-east-1.amazonaws.com/prod/product
{"status":"success"}

先程とほぼ同じRequestですが、tenantId のみ異なります。先程のDynamoDBのPartition構造の変更により、tenantIdsku の組み合わせで一意と判定されます。そのためこちらのRequestも正常に登録が可能です。

DynamoDBの該当テーブルの登録されているアイテムを確認しましょう。

Applying Tenant Conext

ここまででTenantによるユーザーとItemデータの分離を実践してきました。ユーザーは特定のTenantに属しているため、ログインした時点でTenantが決定されます。そこでItemの登録画面を追加したアプリケーションをDeployし、明示的にユーザーがTenantIdを指定しなくても正常にItemが登録できることを確認します。

TenantIdはJWT Tokenの中からcustomで定義したPropertyとして取得します。以下が該当コード

module.exports.getTenantId = function (req) {
    var tenantId = '';
    var bearerToken = req.get('Authorization');
    if (bearerToken) {
        bearerToken = bearerToken.substring(bearerToken.indexOf(' ') + 1);
        var decodedIdToken = jwtDecode(bearerToken);
        if (decodedIdToken)
            tenantId = decodedIdToken['custom:tenant_id'];
    }
    return tenantId;
};

これでユーザーが認証を行うと自動的にTenantIdが取得されるアプリケーションになります。これをDeploy。

Deploy完了したら新たにユーザー登録をしてItemを登録してみます。

と、このワークショップもここで時間切れになりました。

Lab3

時間内に実践はできませんでしたが、この後ユーザー情報とIAM Role Policyとの突き合わせによって、自身に関係ないデータにはアクセスできないPermissionの機能などをDeployし、データの独立性を確保するための変更のようです(他のユーザーやテナントの情報にアクセスできちゃマズイですし)

まとめ

このワークショップでは既存のアプリケーションを進化させ、認証、ユーザー管理、Multi-tenantといった機能を加えていき、AWSで構成されたアーキテクチャがどのように変化していくかを体験しました。

Multi-tenant化に伴いデータストアのPartition構造を大きく変更する必要があるなど、既存の構造を壊す必要がありましたが、サービスごとがうまい具合に疎結合された構成であれば、変更に伴う影響は比較的小さいことを実感できました。SaaSを構成する上でMulti-tenant化は避けて通れない道です。今回のワークショップでは限られた時間の中で少ないながらも、Multi-tenant化に必要不可欠なエッセンスを確認できました。