Redis on SAP BTP, Hyperscaler Optionとは
Redis on SAP BTP, Hyperscaler Optionは、アプリケーションのキャッシュやセッション情報を格納するために使用するサービスです。AWS, Azure, GCPといったハイパースケーラーが提供するRedisキャッシュサービスを、BTPのサービスとして利用できるようになっています。開発者はハイパースケーラーの違いを意識する必要はありません。
出典:What Is Redis, Hyperscaler Option?
※以下、この記事ではRedis on SAP BTP, Hyperscaler Optionを"Redis"と記載します。
スタンドアロンApplication RouterでのRedisの役割
Application Routerについて
Application RouterはBTPのアプリケーションへユーザがアクセスする際の入り口となるサービスで、ユーザの認証やバックエンドサービスなどへのルーティングを行います。
Application Routerには二種類あります。一つはスタンドアロンのApplication Router(自分で作成する)で、もう一つはSAP Build Work Zoneなどに組み込まれたManaged Approuterです。
スタンドアロンApplication Routerを使用するケースとしては、SAP Build Work Zoneを使用しないケース、およびマルチテナントなアプリケーションを開発するケースなどがあります。
Application Routerについて詳しく知りたい場合は、以下のブログが参考になります。
スタンドアロンApplication RouterでのRedisの必要性
私がRedisについて初めて知ったのは、以下のブログがきっかけでした。
それによると、
- BTPのアプリケーションでランダムに401(認証)エラーが発生する事象が発生していた
- 調査の結果、Cloud FoundryのEvacuation(Diago Cellのメンテナンスなどのために、アプリケーションインスタンスを別インスタンスに移行させるプロセス)によってApplication Routerのインスタンスが再作成されたときに、保持していたセッション情報などが失われたと考えられた
ブログでは、Application Routerを使用する場合には以下が必要としています。
- Redisのような状態保持メカニズムを使用することが本番環境では必須。そうしないと、セルがEvacuateされたときにセッションが失われてしまう
- 本番環境ではApplication Routerインスタンスを少なくとも2つ、理想的には3つにスケールする。これにより、セルが更新のためEvacuateされるときに、他のApplication Router(セル)がその情報を引き継ぎ、状態を失うことがないようにすることができる
本番環境でスタンドアロンApplication Routerを使用するときは、Redisを使うことが必須ということです。
Service to Application RouterシナリオでのCSRFトークンを使用したいとき
私がRedisの検証をしてみようと思った直接のきっかけは、CAPで作ったODataサービスにCSRFトークンチェックを追加したいと思ったことでした。CAP自体にはCSRFトークンを扱う仕組みはなく、自前の実装かApplication Routerを使用した実装が紹介されています。
Application RouterでCSRFトークンチェックを有効化するには、xs-app.jsonに"csrfProtection": true
の設定を追加します。
{
"routes": [
{
"source": "^/odata/(.*)$",
"target": "/odata/$1",
"destination": "srv-api",
"csrfProtection": true
}
]
}
しかし、単純にこの設定をしただけだと、PostmanからCSRFトークンを設定しなくてもPOSTリクエストが実行できました。
ドキュメントを読んだところ、Service to Application Routerシナリオのシナリオでは、外部(Redis)でセッション管理をしないとCSRFトークンは有効にならないことがわかりました。そこで、Application RouterでRedisを使用してCSRFトークンハンドリングが有効化になることを検証してみようと思いました。
検証内容
- Redis使用前の状態を確認
- Application RouterでRedisを有効化する
- Redisに格納されたセッション情報を確認
検証環境
BTPのトライアル環境を使用しています。
※トライアル環境ではRedisのサービスインスタンスが1つしか作れませんでした
コンポーネントの構成
ソースコード
プロジェクトの構成
.
├── app
│ ├── booksui2 // Fiori elements
│ ├── router // Application Router
│ └── services.cds
├── db
│ ├── data
│ └──data-model.cds
├── srv
│ └── cat-service.cds
└── package.json
/app/router/xs-app.jsonの設定は以下のようになっています。
{
"welcomeFile": "nsbooksui2/index.html",
"routes": [
...,
{
"source": "^/odata/(.*)$",
"target": "/odata/$1",
"destination": "srv-api",
"csrfProtection": true
},
{
"source": "^(.*)$",
"target": "$1",
"service": "html5-apps-repo-rt",
"authenticationType": "xsuaa"
}
]
}
検証手順
1. Redis使用前の状態を確認
アプリケーションをデプロイし、PostmanからApplication Routerにリクエストを送ってみます。
2. Application RouterでRedisを有効化する
以下のステップでRedisを有効化します。
2.1. mta.yamlにRedisのリソースを追加
resourcesセクションに以下の設定を追加します。
resources:
- name: cap-redis-redis
type: org.cloudfoundry.managed-service
parameters:
config:
engine_version: "6.0"
eviction_policy: noeviction
service: redis-cache
service-plan: trial
Redisのサービスインスタンスを作成するには数十分かかりました。
2.2. Application RouterでRedisを使用する設定
mta.yamlでApplication Routerのモジュールに以下の設定を追加します。
- name: cap-redis
type: approuter.nodejs
path: app/router
properties:
EXT_SESSION_MGT: |
{
"instanceName": "cap-redis-redis", //redisのサービスインスタンス名
"storageType": "redis",
"sessionSecret": "G7h2xP8RjWmQ9uV3bLfZ1cDnJ4oB5sY6kMtH0aE2wX9vFi8zK7yCq3R",
"defaultRetryTimeout": 10000,
"backOffMultiplier": 10
}
SVC2AR_STORE_CSRF_IN_EXTERNAL_SESSION: true
requires:
...
- name: cap-redis-redis
EXT_SESSION_MGT
ユーザセッションをRedisで保管するための設定です。設定内容はドキュメントに記載されています。sessionSecret
はセッションクッキーを生成するためのシークレットで、自分で64文字以上の文字列を作成して設定します。
SVC2AR_STORE_CSRF_IN_EXTERNAL_SESSION
Service to Application RouterシナリオでCSRFトークンを使用したい場合、trueを設定します。ブラウザからログインするシナリオの場合、この設定は不要です。
2.3. CSRFトークンチェックが有効化されたことを確認
Postmanからステップ1と同じリクエストを送信すると、403 (Forbidden) エラーになります。CSRFトークンチェックが有効化されたためです。
POSTリクエストを送るには、まずGet(またはHead)リクエストによりCSRFトークンを取得します。x-csrf-token
ヘッダにfetch
を指定することで、レスポンスヘッダにトークンが返ります。
取得したトークンをPOSTリクエストのx-csrf-token
ヘッダに設定して実行すると、リクエストは成功します。
3. Redisに格納されたセッション情報を確認
Redisのキャッシュは以下のステップで確認できます。
- redis-cliをインストール
- Application RouterにSSHで接続
- redis-cliでセッション情報を確認
- セッション情報の中身を確認
3.1. redis-cliをインストール
linux (Ubuntu) の場合、以下のコマンドでインストールできます。
sudo apt-get install redis-tools
redis-cli --version //インストールされたことを確認
3.2. Application RouterにSSHで接続
ドキュメントを参考に、Application RouterにSSHで接続します。
- SSHを有効化
cf enable-ssh <app name> //SSHを有効化
cf restart <app name> //アプリケーションを再起動
- Reidsインスタンスのサービスキーを作成して表示
cf service-key <instance name> <key name>
- SSHトンネルを設定(以下ではローカルポート6666を使用)
cf ssh -L 6666:<instance hostname>:<instance port> <host app name>
例)
cf ssh -L 6666:master.rg-00b26854-749a-47eb-a6d1-41f6ecb16c9d.9u04ff.use1.cache.amazonaws.com:1460 cap-redis
vcap@740f7311-ba59-4946-66d6-52cc:~$
- 新規のターミナルを立ち上げ、Redisのインスタンスに接続
redis-cli --tls -c -p 6666 -a <instance password>
127.0.0.1:6666>
3.3. redis-cliでセッション情報を確認
Redisに格納されたキーの一覧を確認します。Postmanから接続したセッションが{external-session}
として登録されています。
KEYS *
1) "{external-sessions}:91006ab733d64a139d8ecf637c258320"
※UIから接続した場合は、{approuter-sessions}
で始まるキーが登録されます。
1) "{approuter-sessions}:ktS8iAaHA3NNjRojQET6gDODc_KEA9nV"
キーに対応する値を取得します。
GET <キー値>
例)
GET {external-sessions}:91006ab733d64a139d8ecf637c258320
"H4sIAAAAAAAEA61WWZOqSBr9Kz283rKKfamIeUBFTYRUUGSZmriRQCprgiwqdNz/3oFVHTHd/XK7Y3ghkvzW8y2HXyn86HBDUHHAbZtWBMTUO6UwNC2iUOK4WOQRwymxjKOzyEkRK8gcS1Mvf1bTHnXaoC6tCPXOSIzICzwtKS9Udu9sfG5wm・・・
このままでば解読できないので、ChatGPTにこのデータ形式は何か聞いたところ、以下の回答でした。
提供されたデータはGzip圧縮されたバイナリデータのようです。これを読める形式にするためには、Gzip圧縮を解凍する必要があります
4.4. セッション情報の中身を確認
ChatGPTの助けを借りて、セッション情報を解凍するスクリプトを作成しました。
#!/bin/bash
# Redisからデータを取得してBase64デコード
compressed_data='3.3で取得した値'
echo $compressed_data | base64 --decode > compressed_data.gz
# Gzip解凍
gzip -d -c compressed_data.gz > decompressed_data.txt
# エスケープされたJSON文字列を読み込み、正しいJSON形式に変換して保存
escaped_json=$(cat decompressed_data.txt)
echo $escaped_json | jq -r . > decompressed_data.json
# 正しいJSON形式のデータを表示
cat decompressed_data.json
# 一時ファイルを削除
rm compressed_data.gz decompressed_data.txt
新規ターミナルを立ち上げ、スクリプトを実行します。
chmod +x decode_redis_data.sh
./decode_redis_data.sh
解凍結果は以下のようになりました。セッション情報としてaccessTokenやxsrf-tokenが保存されていることがわかります。
{
"externalSessionId": "91006ab733d64a139d8ecf637c258320",
"externalSessionExpiration": 1716454079,
"jwtRefreshStarted": true,
"user": {
"userId": "n/a",
"name": "n/a",
"token": {
"accessToken": "XXXX",
"expiryDate": 1718129515764382,
"oauthOptions": {
"tenantmode": "dedicated",
"sburl": "https://internal-xsuaa.authentication.us10.hana.ondemand.com",
"subaccountid": "75ba9a19-b434-4743-89e6-4a9d9ab3e5e8",
"credential-type": "instance-secret",
"clientid": "XXXX",
"xsappname": "cap-redis-a2c7c84etrial-dev!t266030",
"clientsecret": "XXXX",
"serviceInstanceId": "29fb9fcd-376f-4631-ad8f-f250913c757a",
"url": "https://a2c7c84etrial.authentication.us10.hana.ondemand.com",
"uaadomain": "authentication.us10.hana.ondemand.com",
"verificationkey": "-----BEGIN PUBLIC KEY-----\nXXXX\n-----END PUBLIC KEY-----",
"apiurl": "https://api.authentication.us10.hana.ondemand.com",
"identityzone": "a2c7c84etrial",
"identityzoneid": "75ba9a19-b434-4743-89e6-4a9d9ab3e5e8",
"tenantid": "75ba9a19-b434-4743-89e6-4a9d9ab3e5e8",
"zoneid": "75ba9a19-b434-4743-89e6-4a9d9ab3e5e8"
}
},
"tenantid": "75ba9a19-b434-4743-89e6-4a9d9ab3e5e8",
"scopes": [
"uaa.resource"
],
"tenant": "a2c7c84etrial"
},
"xsrf": {
"token": "05a99e23a2e36bf7-dSPc2omar80L9EWLRQJ4rZQ5eAI",
"secret": "gUasKun6U8eMapRThKpyIbNWX5HF3h1juEIH8P7ynos"
}
}
まとめ
- スタンドアロンApplication Routerを本番環境で使用する場合、Redisを使用する
- Application RouterでRedisを使用するには、環境変数
EXT_SESSION_MGT
を設定する - Service to Application RouterシナリオでCSRFトークンを使用したい場合は、上記に加えて
SVC2AR_STORE_CSRF_IN_EXTERNAL_SESSION
も設定する