cognitoを利用してstreamlitへのアクセス範囲を限定する
はじめに
みなさん、streamlitは利用されていますか? pandasやmatplotlibなどを利用してデータ可視化する際には便利なツールですよね。
その際に、「ネットに公開はしたいけれど、アクセス権をもつユーザーのみに公開したいな」と考えることはありませんか? streamlitそのものには認証などの機能はないため、独自に実装する必要があります。
そこで今回はAWSサービスのCognitoを利用して、ログイン機能があるstreamlitを作成する方法を紹介します。各リソースはCDKでデプロイし、作成と削除を簡単にできるようにしています。
料金・構成
構成
今回、AWSのリソースはCDKでデプロイ・作成します。
(ただし、手動での事前の準備は多少必要です)
Application LoadBalancerでCognitoの認証をしています。Developerはあらかじめ、Cognitoでログイン用のアカウントを作成しておき、そのアカウントをログインさせたいUserに配布します。配布されたアカウントのみでCognitoでログインできるようにしているので、安全にデータを見せることができます。
料金
料金は利用量にもよって変わるのですが、合計で4,000[円/月]程度かかる計算です。「アクセスの頻度は低い」・「Fargateのリソースは0.5vCPU, 256MiB
」の想定で計算しています。サービスごとにかかる料金は以下の通りです。
AWSサービス | 料金[円/月] |
---|---|
Application LoadBalancer | 2,500 |
Fargate | 1,100 |
Cognito | (無料) |
準備
事前に準備が必要な物としてはドメインと証明書です。
(今回は説明しませんが、環境にCDKをデプロイする準備も必要です)
Route53
ドメインはRoute53で準備します。Route53に利用するドメインを登録して、すぐに利用できる状態にします。
画像のように、NSタイプとSOAタイプが登録されていたら大丈夫です。CDKでのデプロイ時にALBへのエイリアスとしてAタイプを追加して、このドメインにアクセスがあった際にルーティングしてくれるようにします。
今回は例として、以下のようなドメイン設定を想定しています。
- 取得したルートドメイン:
your.domain.com
- 今回作成するアプリケーションに設定するサブドメイン:
streamlit.your.domain.com
上の画像に設定されているのは、streamlit.your.domain.com
を想定しています。
証明書(ACM)
作成したドメインにhttpsアクセスできるようにドメインに対してACMで証明書を発行します。証明書の発行は、全てのサブドメインに適用できる*.your.domain.com
を入力します。(下の画像の2つの項目のうち、上の*.
から始まっている方)
Route53を利用している場合は、your.domain.com
を管理しているホストゾーンに対してCNAMEタイプのレコードが追加されることになります。
プログラム
プログラムは適宜載せますが、詳細はリポジトリを参照してください。
ディレクトリ構成
ディレクトリ構成は次のようになっています。
infrastructure/
フォルダにて、CDKのソースを管理しています。streamlit/
フォルダにて、Fargate上で稼働させるアプリケーションをDockerで記述しています。
root
├── infrastructure/
│ ├── cdk.context.json
│ ├── cdk.json
│ ├── cdk.out
│ ├── index.ts
│ ├── params.ts
│ ├── paramsExample.ts
│ └── StreamlitEcsFargateStack.ts
├── streamlit/
│ ├── app.py
│ ├── config.toml
│ ├── Dockerfile
│ └── Makefile
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json
ソースコード
streamlitのコード解説はしないです。CDKで作成されるリソースについてのみちょっと見ていきます。
Cognitoリソースの作成
Cognitoの設定をしているコードです。パラメータの詳しい内容はCDKドキュメントをご覧ください。また利用する環境に応じて、適宜修正をしてください。
ポイントは以下の2点です。
-
selfSignUpEnabled=false
- ユーザーの自己サインアップを防いでいる
-
removalPolicy=cdk.RemovalPolicy.DESTROY
- このスタックを削除した際に登録内容ごとCognitoを削除するようにしている
/* ~~前略~~ */
const userPool = new cognito.UserPool(this, "userPool", {
userPoolName: "streamlit-user-pool-test",
// self signUp disabled
selfSignUpEnabled: false,
userVerification: {
emailSubject: "Verify email message",
emailBody: "Thanks for signing up! Your verification code is {####}",
emailStyle: cognito.VerificationEmailStyle.CODE,
smsMessage: "Thanks for signing up! Your verification code is {####}"
},
// sign in
signInAliases: {
username: true,
email: true
},
// user attributes
standardAttributes: {
nickname: {
required: true,
// `mutable` means changeable
mutable: true
}
},
// role, specify if you want
mfa: cognito.Mfa.OPTIONAL,
mfaSecondFactor: {
sms: true,
otp: true
},
passwordPolicy: {
minLength: 8,
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
requireSymbols: true,
tempPasswordValidity: cdk.Duration.days(3)
},
accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
removalPolicy: cdk.RemovalPolicy.DESTROY,
// emails, by default `no-reply@verificationemail.com` used
})
/* ~~後略~~ */
ALBにCognito認証の付与
ALBにCognito認証を追加した設定ポイントは以下の通りです。
- FargateとALBに設定するセキュリティグループをあえて作成・設定している
- ALBのセキュリティグループに443のegressを許可するため
- デフォルトで生成されるセキュリティグループには付与されない
- ALBのlistenerにCognitoの認証を加えている
/* ~~前略~~ */
const ecsServiceSecurityGroup = new ec2.SecurityGroup(this, "ecs-service-sg", {
vpc,
securityGroupName: "streamlit-service-sg",
description: "security group to allow IdP",
})
const service = new ecs.FargateService(this, "StreamlitService", {
cluster: cluster,
taskDefinition: taskDef,
deploymentController: {
type: ecs.DeploymentControllerType.CODE_DEPLOY
},
healthCheckGracePeriod: cdk.Duration.seconds(5),
assignPublicIp: true,
securityGroups: [ecsServiceSecurityGroup],
})
// https://<alb-domain>/oauth2/idpresponse
// requires allowing HTTPS egress-rule
const albSecurityGroup = new ec2.SecurityGroup(this, "alb-sg", {
vpc,
securityGroupName: "streamlit-alb-sg",
description: "security group to allow IdP",
allowAllOutbound: false
})
albSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), "allow HTTP")
albSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(8080), "allow alt HTTP")
albSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443), "allow HTTPS")
albSecurityGroup.addEgressRule(ecsServiceSecurityGroup, ec2.Port.tcp(80), "allow HTTP")
albSecurityGroup.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443), "allow HTTPS")
ecsServiceSecurityGroup.addIngressRule(albSecurityGroup, ec2.Port.tcp(80), "allow from alb-HTTP")
const alb = new elb.ApplicationLoadBalancer(this, "ApplicationLoadBalancer", {
loadBalancerName: "StreamlitALB",
vpc: vpc,
idleTimeout: cdk.Duration.seconds(30),
// scheme: true to access from external internet
internetFacing: true,
securityGroup: albSecurityGroup
})
const listenerHttp1 = alb.addListener("listener-https", {
protocol: elb.ApplicationProtocol.HTTPS,
certificates: [elb.ListenerCertificate.fromArn(params.alb.certificate)]
})
const targetGroupBlue = listenerHttp1.addTargets("http-blue-target", {
targetGroupName: "http-blue-target",
protocol: elb.ApplicationProtocol.HTTP,
deregistrationDelay: cdk.Duration.seconds(30),
targets: [service],
healthCheck: {
healthyThresholdCount: 2,
interval: cdk.Duration.seconds(10)
},
})
listenerHttp1.addAction("cognito-auth-elb-1", {
action: new elbActions.AuthenticateCognitoAction({
userPool: userPool,
userPoolClient: app1,
userPoolDomain: userPoolDomain,
scope: "openid",
onUnauthenticatedRequest: elb.UnauthenticatedAction.AUTHENTICATE,
next: elb.ListenerAction.forward([targetGroupBlue])
}),
conditions: [elb.ListenerCondition.pathPatterns(["*"])],
priority: 1
})
/* ~~後略~~ */
デプロイ
パッケージのインストール
ルートディレクトリにてパッケージのインストールを行います。
$ npm install
デプロイに必要なパラメータの付与
デプロイの前にパラメータを設定します。infrastructure/
に保存されているparamsExample.ts
をコピーしてparams.ts
を作成します。
$ cp infrastructure/lib/paramsExample.ts infrastructure/lib/params.ts
params.ts
にvpcId
や作成したリソースのARN
を設定します。注意点として、domainPrefix
はユニークである必要があるので、入力値によってはデプロイが失敗します。
import { IStreamlitEcsFargateCognito } from './StreamlitEcsFargateStack';
export const params: IStreamlitEcsFargateCognito = {
+ vpcId: "{your-aws-vpcId}",
- vpcId: "vpc-xxxxxxxx",
env: {
+ account: "{your-aws-accountId}",
- account: "123456789012",
region: "ap-northeast-1"
},
alb: {
+ certificate: "{your-acm-arn}",
+ route53DomainName: "streamlit.your.domain.com"
- certificate: "arn:aws:acm:ap-northeast-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
- route53DomainName: "your.domain.com"
},
cognito: {
+ domainPrefix: "streamlit", # ここの値はユニークである必要があるので注意
+ callbackUrls: ["https://streamlit.your.domain.com/oauth2/idpresponse"],
+ logoutUrls: ["https://streamlit.your.domain.com"]
- domainPrefix: "as-you-like",
- logoutUrls: ["httptps://your.domain.com/oauth2/idpresponse"],
- logoutUrls: ["https://your.domain.com"]
},
}
デプロイ実行
上記の準備が終わったら、デプロイをします。
infrastructure/
ディレクトリに移動後、デプロイコマンド実行します。
$ cd infrastructure
$ cdk deploy
以下のように、IAMやセキュリティグループに関する設定が表示されたのち、y
を入力してデプロイを実行します。
params.ts
などが正しく設定されていたら、10分程度でデプロイが完了します。
確認
Cognitoにユーザーを作成する
AWSのコンソールから、CDKで作成したCognitoにアクセスします。
ログイン用のアカウントを作成するために、「ユーザーグループ」→「ユーザーの作成」の順に押下します。
適当なユーザー名でアカウントを作成します。
アカウントが「有効」になり、ステータスが「FORCE_CHANGE_PASSWORD」になったらアカウントの準備完了です。
作成したページにアクセスする
アプリケーションのドメインstreamlit.your.domain.com
にアクセスしてみます。するとログインが求められ、初回のみパスワードの変更も求められます。
有効なパスワードを入力して「Send」を押下します。
すると、作成したstreamlitのアプリケーション画面を見ることができます。
リソースの削除
テストなどで作成する場合は、以下のコマンドを実行してリソースを削除してください。削除を忘れた場合は4,000[円/月]ほどかかります。
$ cdk destroy
おわりに
今回もCDKを利用して、streamlitにログイン機能を持たせてアクセス範囲を限定してみました。
「長期的な運用」というよりは「すぐに作って、すぐ削除する」ようなアプリケーションを想定しています(の割にはCognitoはオーバースペックかもですね…)。
きっかけは、ALBにCognito認証を付与できるということを最近知って、何か簡単なアプリケーション例を作ってみたいなと思ったことでした。
また、AWS CDK 2.0のrcがリリースされるようになって、IaC関連のアップデートの楽しみが止まりませんね!(記事投稿時にはまだリリースされていませんでしたが、もうリリースされて2年ほど経つので消しました)
参考
Discussion