プライベートサブネットからCognitoにアクセスする
前回の記事の最後で、考えていた構成では問題があると締めくくっていました。
React×Spring Bootな構成をAWS Fargateで動かす(4)
バックエンドサービスは、認可のためにJWT検証のURLにアクセスする必要があります。
しかし元々考えていた構成では、以下のようにプライベートサブネットにあるバックエンドサービスからCognitoのJWT検証URLへのアクセス経路がなかったのです。
今回の記事は、この構成上の問題の解決方法を模索してみようというものになっています。
構成のパターン
まずはどんな構成が考えられそうか調べてみようということでググってみると、いくつか検索することができました。
Amazon Cognito ユーザープールエンドポイントへのアクセスパターンを考える
VPC Endpoint サポートされてないけど閉域から Cognito で認証したい!
これらを今回の構成に当てはめて、まとめると以下のようになります。
NAT Gateway経由でアクセスする構成
一番最初に思いつきそうなのがこの構成なのではないでしょうか。
プライベートサブネットからインターネットへのアクセスをNAT Gateway経由でアクセスするという構成です。
しかし、以下の理由から今回は見送りました。
- NAT Gatewayの冗長化も考えるとコスト高
- CognitoのJWT検証URLへのアクセスをするためだけにしてはオーバースペック
- 任意のURLにアクセスできてしまう
フォワードProxy経由でアクセスする構成
パブリックサブネットにEC2とプロキシソフトウェアを構築して、プロキシサーバ経由でアクセスするという構成です。
そもそもFargateをベースとした構成の中にEC2が混ざるのもいやですし、EC2の運用もしなければならないということで、こちらも見送りました。
パブリックサブネットで起動する構成
Fargateをパブリックサブネットで起動することによって、インターネットにアクセスできる経路を確保しようという構成です。
こちらもNAT Gateway同様、任意のURLにアクセスできてしまうというのと、サブネット分離できずインターネットにさらされる危険性を排除できない、ということから見送りました。
API Gateway経由でアクセスする構成
プライベートなAPI Gatewayのエンドポイントを立て、API GatewayからCognitoにプロキシさせるという構成です。
こちらで実現できそうだったのですが、VPCエンドポイントがさらに増えてしまうのと、Cognito全体にプライベートからアクセスしたいわけでもないため、何か別の方法はないか考えることにしました。
別の構成の考察
そもそもJWT検証URLから返ってくるのは以下のようなJSONファイル。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ "keys": [{ "kid": "1234example=", "alg": "RS256", "kty": "RSA", "e": "AQAB", "n": "1234567890", "use": "sig" }, { "kid": "5678example=", "alg": "RS256", "kty": "RSA", "e": "AQAB", "n": "987654321", "use": "sig" }] } |
このファイルを持っておいて、それを読ませればいいのでは?と考えたのですが、公式ドキュメントによると
ということで値は定期的にローテーションされる可能性があるということです。
当然と言えば当然ですね。
そこでフォワードProxy経由でアクセスする構成からヒントを得て、少々トリッキーではあるのですが、以下のような構成ならば実現できるのでは?と思いました。
まず、CognitoのJWT検証URLにアクセスして、そのままレスポンスを返す非VPCのLambdaを作成します。
ALBからこのLambdaに振り分けるようにします。
ALBはすでにあるはずなので、パスベースでルーティングすれば追加のコストはかかりません。
そしてバックエンドサービスからALB経由でLambdaにアクセスできさえすれば、実現できるのではないかと考えたのです。
ここで少々変更点があるのですが、元々の構成ではALBをパブリックに置いていたのですが、プライベートのALBにしています。
理由としては、バックエンドサービスから呼び出すときにプライベートALBでないとプライベートのIPアドレスが解決されず、結局アクセスできないためです。
元々は社内システムが想定ということで勘弁してくださいね。
構成の検証
思いついたら早速検証してみよう!
ということで検証のゴールは、プライベートなALB経由で非VPC Lambdaに振り分け、CognitoのJWT検証URLの内容をレスポンスできるか、と決めました。
検証のためのアプリももちろんCDKで作っていきます。
まずはお決まりのコマンドでCDKプロジェクトを作成します。
1 2 3 4 |
mkdir -p cognito-gateway cd cognito-gateway cdk init --language typescript npm install dotenv |
環境変数から値を渡すため、cognito-gateway.tsを以下のように修正します。
1 2 3 4 5 6 7 8 9 10 |
#!/usr/bin/env node import * as cdk from 'aws-cdk-lib'; import { CognitoGatewayStack } from '../lib/cognito-gateway-stack'; import * as dotenv from 'dotenv'; dotenv.config(); const app = new cdk.App(); new CognitoGatewayStack(app, 'CognitoGatewayStack', { }); |
プロジェクトのルートにlambdaディレクトリを作成し、以下ののソースをlambda_function.pyとして保存しておきます。
このPythonプログラムが、CognitoのJWT検証URLの内容をレスポンスするLambdaになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import os import json import urllib.request import urllib.error def lambda_handler(event, context): # 環境変数からJWKのURLを取得 jwk_set_uri = os.getenv('COGNITO_JWK_SET_URI') if not jwk_set_uri: return { 'statusCode': 500, 'body': json.dumps('COGNITO_JWK_SET_URI environment variable is not set'), 'headers': { 'Content-Type': 'application/json' } } try: # JWKのURLにリクエストを投げる with urllib.request.urlopen(jwk_set_uri) as response: jwk_data = response.read().decode('utf-8') # レスポンスをそのまま返す return { 'statusCode': 200, 'body': jwk_data, 'headers': { 'Content-Type': 'application/json' } } except urllib.error.URLError as e: return { 'statusCode': 500, 'body': json.dumps(f'Error fetching JWK: {str(e)}'), 'headers': { 'Content-Type': 'application/json' } } |
最後にcognito-gateway-stack.tsを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as path from 'path'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import * as targets from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets'; import * as lambda from 'aws-cdk-lib/aws-lambda'; export class CognitoGatewayStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // VPCを作成 const vpc = new ec2.Vpc(this, 'CognitoGatewayVpc', { vpcName: 'cognito-gateway-vpc', ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'), maxAzs: 2, subnetConfiguration: [ { cidrMask: 24, name: 'PrivateSubnet', subnetType: ec2.SubnetType.PRIVATE_ISOLATED, }, ], natGateways: 0, }); // Session Manager用VPCエンドポイントを作成 vpc.addInterfaceEndpoint('Ec2MessagesEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.EC2_MESSAGES, }); vpc.addInterfaceEndpoint('SsmEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.SSM, }); vpc.addInterfaceEndpoint('SsmMessagesEndpoint', { service: ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES, }); // Lambdaの作成 const lambdaForCognito = new lambda.Function(this, 'CognitoJwkLambda', { runtime: lambda.Runtime.PYTHON_3_12, handler: 'lambda_function.lambda_handler', code: lambda.Code.fromAsset(path.join(__dirname, '../lambda')), environment: { COGNITO_JWK_SET_URI: `https://cognito-idp.ap-northeast-1.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`, }, }); // ロードバランサ用セキュリティグループの作成 const securityGroup = new ec2.SecurityGroup(this, 'AlbSecurityGroup', { securityGroupName: 'sec-cognito-gateway-alb', vpc, description: 'Allow http traffic', }); securityGroup.addIngressRule(ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(80), 'Allow from internal'); // ロードバランサの作成 const lb = new elbv2.ApplicationLoadBalancer(this, 'ApplicationLoadBalancer', { vpc, loadBalancerName: 'cognito-gateway-alb', vpcSubnets: { subnetGroupName: 'PrivateSubnet', }, internetFacing: false, securityGroup: securityGroup, }); // リスナーの追加 const listener = lb.addListener('Listener', { port: 80, protocol: elbv2.ApplicationProtocol.HTTP, open: false, }); // Cognito用Lambdaターゲットグループの追加 listener.addTargets('TargetGroupLambda', { targetGroupName: 'tg-miso-cognito-jwk', targets: [new targets.LambdaTarget(lambdaForCognito)], healthCheck: { enabled: true, }, }); // テスト用EC2の作成 const testInstance = new ec2.Instance(this, 'TestEC2Instance', { instanceName: 'test-instance', vpc: vpc, vpcSubnets: { subnetGroupName: 'PrivateSubnet', }, instanceType: new ec2.InstanceType('t3.nano'), machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023, }), associatePublicIpAddress: false, ssmSessionPermissions: true, securityGroup: securityGroup, }); } } |
プライベートサブネットのみのVPCを作成し、そこにプライベートなALBを置いています。
テストのために同じサブネットにEC2インスタンスを作成し、Session Manager経由でログインできるようにしておきます。
.envファイルにUSER_POOL_IDの値をセットして、いざデプロイ!
テスト用のEC2インスタンスから、curlコマンドでALBのURLを叩いたところ、無事にJSONの値を表示することができました😆
これで構成上の問題は解消できそうです。
次回はいよいよすべての構成をCDKでデプロイしていきたいと思います!
それでは、また次回に👋
シリーズ記事
- React×Spring Bootな構成をAWS Fargateで動かす(1) ~ Cognitoの構築
- React×Spring Bootな構成をAWS Fargateで動かす(2) ~認証付きフロントエンドの作成
- React×Spring Bootな構成をAWS Fargateで動かす(3) ~バックエンドの作成
- React×Spring Bootな構成をAWS Fargateで動かす(4) ~認可機能の追加
- React×Spring Bootな構成をAWS Fargateで動かす(5) ~構成上の問題の解消方法
- React×Spring Bootな構成をAWS Fargateで動かす(6) ~ Fargateのデプロイ(準備編)
- React×Spring Bootな構成をAWS Fargateで動かす(7) ~ Fargateのデプロイ(実装・デプロイ編)
執筆者プロフィール

- tdi デジタルイノベーション技術部
-
昔も今も新しいものが大好き!
インフラからアプリまで縦横無尽にトータルサポートや新技術の探求を行っています。
週末はときどきキャンプ場に出没します。
この執筆者の最新記事
Pick UP!2025.03.18React×Spring Bootな構成をAWS Fargateで動かす(7) ~ Fargateのデプロイ(実装・デプロイ編)
Pick UP!2025.02.27React×Spring Bootな構成をAWS Fargateで動かす(6) ~ Fargateのデプロイ(準備編)
Pick UP!2025.02.19React×Spring Bootな構成をAWS Fargateで動かす(5) ~ 構成上の問題の解消方法
Pick UP!2025.02.07React×Spring Bootな構成をAWS Fargateで動かす(4) ~ 認可機能の追加