🗂

cognitoを利用してstreamlitへのアクセス範囲を限定する

11 min read

はじめに

みなさん、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タイプのレコードが追加されることになります。

プログラム

プログラムは適宜載せますが、詳細はリポジトリを参照してください。

ディレクトリ構成

ディレクトリ構成は次のようになっています。

infra/フォルダにて、CDKのソース管理を行っています。
streamlit/フォルダにて、Fargate上で稼働させるアプリケーションをDockerで記述しています。

root
├── infra/
│  ├── 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を削除するようにしている
StreamlitEcsFargateStack.ts
/* ~~前略~~ */

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の認証を加えている
StreamlitEcsFargateStack.ts
/* ~~前略~~ */

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

デプロイに必要なパラメータの付与

デプロイの前にパラメータの設定を行います。
infra/に保存されているparamsExample.tsをコピーしてparams.tsを作成します。

$ cd infra
$ cp paramsExample.ts params.ts

params.tsvpcIdや作成したリソースのARNを設定します。
注意点として、domainPrefixはユニークである必要があるので、
入力値によってはデプロイが失敗する可能性があります。

params.ts
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"]
	},
}

デプロイ実行

上記の準備が終わったら、デプロイをします。

infra/ディレクトリに移動後、デプロイコマンド実行します。

$ cd infra
$ 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関連のアップデートの楽しみが止まりませんね!

参考

GitHubで編集を提案

Discussion

ログインするとコメントできます