📝

ALB を Cognito と統合してユーザー認証する機能を CDK で実装してみた

に公開

ALB を Amazon Cognito と統合してユーザーを認証する | AWS re:Post
CDK でやってみました。

前提

  • CDK 実行環境: Cloud9
  • ドメインおよびホストゾーンは Route 53 に登録済み
  • ACM で証明書発行済み
  • CDK の言語は TypeScript
  • cdk bootstrap は実行済み

CDK プロジェクト作成

$ mkdir -p ~/.npm-global
$ npm config set prefix '~/.npm-global'
$ echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc
$ source ~/.bashrc
$ npm install -g typescript
$ tsc -v
Version 5.7.3

$ mkdir alb-cognito-ec2
$ cd alb-cognito-ec2
$ cdk init app --language typescript

CDK コード

lib/alb-cognito-ec2-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Stack, StackProps, Duration } from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as certificatemanager from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as route53targets from 'aws-cdk-lib/aws-route53-targets';
import { Construct } from 'constructs';
import { InstanceTarget } from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets';
import { AuthenticateCognitoAction } from 'aws-cdk-lib/aws-elasticloadbalancingv2-actions';

export class AlbCognitoEc2Stack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // ドメイン名と証明書ARN
    const domainName = 'your-domain';
    const certificateArn = 'your-certificateArn;

    // 既存の証明書をインポート
    const certificate = certificatemanager.Certificate.fromCertificateArn(
      this,
      'Certificate',
      certificateArn
    );

    // VPC(パブリックサブネットのみ、2つのAZ)
    const vpc = new ec2.Vpc(this, 'MyVpc', {
      maxAzs: 2, // 2つのAZ
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
      ],
      natGateways: 0, // NATゲートウェイなし
    });

    // EC2 セキュリティグループ
    const ec2SG = new ec2.SecurityGroup(this, 'EC2SG', {
      vpc,
      allowAllOutbound: true,
    });
    ec2SG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP');

    // EC2 インスタンス(パブリックサブネットに配置)
    const ec2Instance = new ec2.Instance(this, 'WebServer', {
      vpc,
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
      machineImage: ec2.MachineImage.latestAmazonLinux2023(),
      securityGroup: ec2SG,
      vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, // パブリックサブネットに明示的に配置
    });

    // Apache インストールとindex.html作成のユーザーデータ
    ec2Instance.addUserData(
      'sudo yum update -y',
      'sudo yum install -y httpd',
      'sudo systemctl enable httpd',
      'sudo systemctl start httpd',
      // index.htmlを作成
      'sudo cat > /var/www/html/index.html << EOF',
      '<!DOCTYPE html>',
      '<html lang="ja">',
      '<head>',
      '    <meta charset="UTF-8">',
      '    <meta name="viewport" content="width=device-width, initial-scale=1.0">',
      '    <title>ログイン成功</title>',
      '    <style>',
      '        body {',
      '            font-family: Arial, sans-serif;',
      '            display: flex;',
      '            justify-content: center;',
      '            align-items: center;',
      '            height: 100vh;',
      '            margin: 0;',
      '            background-color: #f0f8ff;',
      '        }',
      '        .container {',
      '            text-align: center;',
      '            padding: 40px;',
      '            background-color: white;',
      '            border-radius: 10px;',
      '            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);',
      '        }',
      '        h1 {',
      '            color: #28a745;',
      '            margin-bottom: 20px;',
      '        }',
      '        p {',
      '            color: #666;',
      '            font-size: 18px;',
      '        }',
      '    </style>',
      '</head>',
      '<body>',
      '    <div class="container">',
      '        <h1>🎉 ログイン成功!</h1>',
      '        <p>Cognito認証が正常に完了しました。</p>',
      '        <p>ALB + Cognito + EC2の連携テストが成功しています。</p>',
      '    </div>',
      '</body>',
      '</html>',
      'EOF',
      // Apacheの権限設定
      'sudo chown apache:apache /var/www/html/index.html',
      'sudo chmod 644 /var/www/html/index.html',
      // Apacheを再起動
      'sudo systemctl restart httpd'
    );

    // ALB セキュリティグループ
    const albSG = new ec2.SecurityGroup(this, 'AlbSG', {
      vpc,
      allowAllOutbound: true,
    });
    albSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP');
    albSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443), 'Allow HTTPS');

    // ALB(2つのパブリックサブネットに配置)
    const alb = new elbv2.ApplicationLoadBalancer(this, 'MyALB', {
      vpc,
      internetFacing: true,
      securityGroup: albSG,
      loadBalancerName: 'alb-cognito-demo',
      vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
    });

    // Route 53 ホストゾーンの取得(既存のものを参照)
    const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', {
      domainName: domainName,
    });

    // ALB用のALIASレコードを作成
    new route53.ARecord(this, 'AliasRecord', {
      zone: hostedZone,
      recordName: domainName,
      target: route53.RecordTarget.fromAlias(new route53targets.LoadBalancerTarget(alb)),
    });

    // HTTPSリスナー
    const httpsListener = alb.addListener('HttpsListener', {
      port: 443,
      certificates: [certificate],
      open: true,
    });

    // HTTPからHTTPSへのリダイレクト
    const httpListener = alb.addListener('HttpListener', {
      port: 80,
      open: true,
    });
    
    httpListener.addAction('RedirectToHttps', {
      action: elbv2.ListenerAction.redirect({
        protocol: 'HTTPS',
        port: '443',
        permanent: true,
      }),
    });

    // Cognito User Pool
    const userPool = new cognito.UserPool(this, 'MyUserPool', {
      selfSignUpEnabled: true,
      signInAliases: { email: true },
      autoVerify: { email: true },
    });

    // User Pool Client
    const userPoolClient = new cognito.UserPoolClient(this, 'MyUserPoolClient', {
      userPool,
      generateSecret: true,
      oAuth: {
        flows: {
          authorizationCodeGrant: true,
        },
        scopes: [
          cognito.OAuthScope.EMAIL,
          cognito.OAuthScope.OPENID,
          cognito.OAuthScope.PROFILE,
        ],
        callbackUrls: [
          `https://${domainName}/oauth2/idpresponse`,
        ],
        logoutUrls: [
          `https://${domainName}/oauth2/idpresponse`,
        ],
      },
      authFlows: {
        userPassword: true,
        userSrp: true,
      },
      supportedIdentityProviders: [
        cognito.UserPoolClientIdentityProvider.COGNITO,
      ],
    });

    // User Pool Domain
    const userPoolDomain = new cognito.UserPoolDomain(this, 'MyUserPoolDomain', {
      userPool,
      cognitoDomain: {
        domainPrefix: `alb-demo-${this.account}`,
      },
    });

    // Target Group
    const targetGroup = new elbv2.ApplicationTargetGroup(this, 'TargetGroup', {
      vpc,
      port: 80,
      protocol: elbv2.ApplicationProtocol.HTTP,
      targets: [new InstanceTarget(ec2Instance)],
      healthCheck: {
        path: '/index.html',
        interval: Duration.seconds(30),
        timeout: Duration.seconds(5),
        healthyThresholdCount: 2,
        unhealthyThresholdCount: 3,
        protocol: elbv2.Protocol.HTTP,
        port: '80',
        healthyHttpCodes: '200',
      },
    });

    // HTTPS リスナーに Cognito 認証アクションを追加
    httpsListener.addAction('CognitoAuth', {
      action: new AuthenticateCognitoAction({
        userPool,
        userPoolClient,
        userPoolDomain,
        next: elbv2.ListenerAction.forward([targetGroup]),
      }),
    });

    // 出力
    new cdk.CfnOutput(this, 'LoadBalancerDNS', {
      value: alb.loadBalancerDnsName,
      description: 'Load Balancer DNS Name',
    });

    new cdk.CfnOutput(this, 'AccessURL', {
      value: `https://${domainName}`,
      description: 'Access URL with HTTPS',
    });

    new cdk.CfnOutput(this, 'HostedZoneId', {
      value: hostedZone.hostedZoneId,
      description: 'Route 53 Hosted Zone ID',
    });
  }
}
bin/alb-cognito-ec2
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { AlbCognitoEc2Stack } from '../lib/alb-cognito-ec2-stack';

const app = new cdk.App();
new AlbCognitoEc2Stack(app, 'AlbCognitoEc2Stack', {
  env: {
    account: 'your-account-id', 
    region: 'ap-northeast-1', 
  },
});

コードの概要は以下の通りです。

  • パブリックサブネットを 2 つ作成
  • EC2 インスタンスを 1 つ作成
    • ユーザーデータで index.html を作成
  • ALB を作成
    • ターゲットは EC2 インスタンス
    • Cognito による認証を追加
  • Cognito ユーザープールを作成
    • 認証が成功したら ALB 経由で EC2 インスタンスにアクセス

デプロイ

$ cdk synth
$ cdk deploy

デプロイ後、CloudFormation スタックの出力から AccessURL のドメインにアクセスします。

ログイン画面からサインアップします。

認証コードを入力します。

以下の画面が表示されれば成功です。

まとめ

今回は ALB を Cognito と統合してユーザー認証する機能を CDK で実装してみました。
どなたかの参考になれば幸いです。

参考資料

Discussion