GenU のバックエンド (CDK) 詳細解説　 ⑨GenerativeAiUseCasesStack > Auth スタックの確認

## はじめに

皆さん、こんにちは。

私は業務でデータ利活用基盤を取り扱っていること、2024 AWS Japan Top Engineer に選出されたということから、[AWS GenU](https://aws-samples.github.io/generative-ai-use-cases-jp/) およびそれに必要なデータ基盤の探求 ([Snowflake](https://www.snowflake.com/ja/), [dbt](https://www.getdbt.com/), [Iceberg](https://iceberg.apache.org/), etc) に取り組む必要があると考えています。

本投稿では、[GenU のバックエンドである CDK コード](https://github.com/aws-samples/generative-ai-use-cases-jp/tree/main/packages/cdk)を詳細に解説します。
自身そして閲覧して頂いた皆様の GenU への理解が少しでも深まり、生成 AI の民主化につながっていければと考えています。

## 前回までのおさらい

前回までで、以下が完了しました。

- [①AWS CDK のセットアップ](https://qiita.com/siruko/items/fd25fdcf89615cb85262)
- [②AWS CDK の動作確認](https://qiita.com/siruko/items/73169f986b4173e3d3a5)
- [③GenU の概要](https://qiita.com/siruko/items/625801b9e1847b305c1e)
- [④GenU CDK スタックの概要](https://qiita.com/siruko/items/8570ed43f7162ea4b907)
- [⑤CloudFrontWafStack スタックの解説](https://qiita.com/siruko/items/30439576ee7c63165d21)
- [⑥RagKnowledgeBaseStack スタックの解説](https://qiita.com/siruko/items/1223c9d22e73168a8809)
- [⑦WebSearchAgentStack スタックの解説](https://qiita.com/siruko/items/aef0a9599df60d47eb1e)
- [⑧GuardrailStack スタックの解説](https://qiita.com/siruko/items/d16e9fe27df4673d7554)

GenU の CDK は最大で以下の 6 つの子スタックを作成します。

- `CloudFrontWafStack`
- `RagKnowledgeBaseStack`
- `AgentStack`
- `GuardrailStack`
- `GenerativeAiUseCasesStack` ※メインスタック
- `DashboardStack`

今回は GenU 内の本丸である `GenerativeAiUseCasesStack` スタックを解説していきたいと思います。
ボリュームが多いですが、焦らずに見ていきます。

### GenerativeAiUseCasesStack スタック

GenerativeAiUseCasesStack は GenU の本体となる生成 AI アプリケーションのスタックです。
アーキテクチャ図でいうと、以下の赤枠の部分にあたります。
GenU の本体ということもあり、様々なリソースを作成しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/74f4c435-a3ea-4c9c-8d93-3054fc873ade.png)

GenerativeAiUseCasesStack の実体は [`packages/cdk/lib/generative-ai-use-cases-stack.ts`](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/main/packages/cdk/lib/generative-ai-use-cases-stack.ts) にあります。

```typescript:packages/cdk/lib/generative-ai-use-cases-stack.ts
import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {
  Auth,
  Api,
  Web,
  Database,
  Rag,
  RagKnowledgeBase,
  Transcribe,
  CommonWebAcl,
} from './construct';
import { CfnWebACLAssociation } from 'aws-cdk-lib/aws-wafv2';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
import { Agent } from 'generative-ai-use-cases-jp';
import { UseCaseBuilder } from './construct/use-case-builder';
import { StackInput } from './stack-input';

export interface GenerativeAiUseCasesStackProps extends StackProps {
  params: StackInput;
  // RAG Knowledge Base
  knowledgeBaseId?: string;
  knowledgeBaseDataSourceBucketName?: string;
  // Agent
  agents?: Agent[];
  // Guardrail
  guardrailIdentifier?: string;
  guardrailVersion?: string;
  // WAF
  webAclId?: string;
  // Custom Domain
  cert?: ICertificate;
}

export class GenerativeAiUseCasesStack extends Stack {
  public readonly userPool: cognito.UserPool;
  public readonly userPoolClient: cognito.UserPoolClient;

  constructor(
    scope: Construct,
    id: string,
    props: GenerativeAiUseCasesStackProps
  ) {
    super(scope, id, props);
    process.env.overrideWarningsEnabled = 'false';

    const params = props.params;

    // Auth
    const auth = new Auth(this, 'Auth', {
      selfSignUpEnabled: params.selfSignUpEnabled,
      allowedIpV4AddressRanges: params.allowedIpV4AddressRanges,
      allowedIpV6AddressRanges: params.allowedIpV6AddressRanges,
      allowedSignUpEmailDomains: params.allowedSignUpEmailDomains,
      samlAuthEnabled: params.samlAuthEnabled,
    });

    // Database
    const database = new Database(this, 'Database');

    // API
    const api = new Api(this, 'API', {
      modelRegion: params.modelRegion,
      modelIds: params.modelIds,
      imageGenerationModelIds: params.imageGenerationModelIds,
      endpointNames: params.endpointNames,
      customAgents: params.agents,
      queryDecompositionEnabled: params.queryDecompositionEnabled,
      rerankingModelId: params.rerankingModelId,
      crossAccountBedrockRoleArn: params.crossAccountBedrockRoleArn,

      userPool: auth.userPool,
      idPool: auth.idPool,
      userPoolClient: auth.client,
      table: database.table,
      knowledgeBaseId: params.ragKnowledgeBaseId || props.knowledgeBaseId,
      agents: props.agents,
      guardrailIdentify: props.guardrailIdentifier,
      guardrailVersion: props.guardrailVersion,
    });

    // WAF
    if (
      params.allowedIpV4AddressRanges ||
      params.allowedIpV6AddressRanges ||
      params.allowedCountryCodes
    ) {
      const regionalWaf = new CommonWebAcl(this, 'RegionalWaf', {
        scope: 'REGIONAL',
        allowedIpV4AddressRanges: params.allowedIpV4AddressRanges,
        allowedIpV6AddressRanges: params.allowedIpV6AddressRanges,
        allowedCountryCodes: params.allowedCountryCodes,
      });
      new CfnWebACLAssociation(this, 'ApiWafAssociation', {
        resourceArn: api.api.deploymentStage.stageArn,
        webAclArn: regionalWaf.webAclArn,
      });
      new CfnWebACLAssociation(this, 'UserPoolWafAssociation', {
        resourceArn: auth.userPool.userPoolArn,
        webAclArn: regionalWaf.webAclArn,
      });
    }

    // Web Frontend
    const web = new Web(this, 'Api', {
      // Auth
      userPoolId: auth.userPool.userPoolId,
      userPoolClientId: auth.client.userPoolClientId,
      idPoolId: auth.idPool.identityPoolId,
      selfSignUpEnabled: params.selfSignUpEnabled,
      samlAuthEnabled: params.samlAuthEnabled,
      samlCognitoDomainName: params.samlCognitoDomainName,
      samlCognitoFederatedIdentityProviderName:
        params.samlCognitoFederatedIdentityProviderName,
      // Backend
      apiEndpointUrl: api.api.url,
      predictStreamFunctionArn: api.predictStreamFunction.functionArn,
      ragEnabled: params.ragEnabled,
      ragKnowledgeBaseEnabled: params.ragKnowledgeBaseEnabled,
      agentEnabled: params.agentEnabled || params.agents.length > 0,
      flows: params.flows,
      flowStreamFunctionArn: api.invokeFlowFunction.functionArn,
      optimizePromptFunctionArn: api.optimizePromptFunction.functionArn,
      webAclId: props.webAclId,
      modelRegion: api.modelRegion,
      modelIds: api.modelIds,
      imageGenerationModelIds: api.imageGenerationModelIds,
      endpointNames: api.endpointNames,
      agentNames: api.agentNames,
      inlineAgents: params.inlineAgents,
      useCaseBuilderEnabled: params.useCaseBuilderEnabled,
      // Frontend
      hiddenUseCases: params.hiddenUseCases,
      // Custom Domain
      cert: props.cert,
      hostName: params.hostName,
      domainName: params.domainName,
      hostedZoneId: params.hostedZoneId,
    });

    // RAG
    if (params.ragEnabled) {
      const rag = new Rag(this, 'Rag', {
        envSuffix: params.env,
        kendraIndexArnInCdkContext: params.kendraIndexArn,
        kendraDataSourceBucketName: params.kendraDataSourceBucketName,
        kendraIndexScheduleEnabled: params.kendraIndexScheduleEnabled,
        kendraIndexScheduleCreateCron: params.kendraIndexScheduleCreateCron,
        kendraIndexScheduleDeleteCron: params.kendraIndexScheduleDeleteCron,
        userPool: auth.userPool,
        api: api.api,
      });

      // File API から data source の Bucket のファイルをダウンロードできるようにする
      // 既存の Kendra を import している場合、data source が S3 ではない可能性がある
      // その際は rag.dataSourceBucketName が undefined になって権限は付与されない
      if (rag.dataSourceBucketName) {
        api.allowDownloadFile(rag.dataSourceBucketName);
      }
    }

    // RAG Knowledge Base
    if (params.ragKnowledgeBaseEnabled) {
      const knowledgeBaseId =
        params.ragKnowledgeBaseId || props.knowledgeBaseId;
      if (knowledgeBaseId) {
        new RagKnowledgeBase(this, 'RagKnowledgeBase', {
          modelRegion: params.modelRegion,
          knowledgeBaseId: knowledgeBaseId,
          userPool: auth.userPool,
          api: api.api,
        });
        // File API から data source の Bucket のファイルをダウンロードできるようにする
        if (props.knowledgeBaseDataSourceBucketName) {
          api.allowDownloadFile(props.knowledgeBaseDataSourceBucketName);
        }
      }
    }

    // Usecase builder
    if (params.useCaseBuilderEnabled) {
      new UseCaseBuilder(this, 'UseCaseBuilder', {
        userPool: auth.userPool,
        api: api.api,
      });
    }

    // Transcribe
    new Transcribe(this, 'Transcribe', {
      userPool: auth.userPool,
      idPool: auth.idPool,
      api: api.api,
    });

    // Cfn Outputs
    new CfnOutput(this, 'Region', {
      value: this.region,
    });

    if (params.hostName && params.domainName) {
      new CfnOutput(this, 'WebUrl', {
        value: `https://${params.hostName}.${params.domainName}`,
      });
    } else {
      new CfnOutput(this, 'WebUrl', {
        value: `https://${web.distribution.domainName}`,
      });
    }

    new CfnOutput(this, 'ApiEndpoint', {
      value: api.api.url,
    });

    new CfnOutput(this, 'UserPoolId', { value: auth.userPool.userPoolId });

    new CfnOutput(this, 'UserPoolClientId', {
      value: auth.client.userPoolClientId,
    });

    new CfnOutput(this, 'IdPoolId', { value: auth.idPool.identityPoolId });

    new CfnOutput(this, 'PredictStreamFunctionArn', {
      value: api.predictStreamFunction.functionArn,
    });

    new CfnOutput(this, 'OptimizePromptFunctionArn', {
      value: api.optimizePromptFunction.functionArn,
    });

    new CfnOutput(this, 'InvokeFlowFunctionArn', {
      value: api.invokeFlowFunction.functionArn,
    });

    new CfnOutput(this, 'Flows', {
      value: Buffer.from(JSON.stringify(params.flows)).toString('base64'),
    });

    new CfnOutput(this, 'RagEnabled', {
      value: params.ragEnabled.toString(),
    });

    new CfnOutput(this, 'RagKnowledgeBaseEnabled', {
      value: params.ragKnowledgeBaseEnabled.toString(),
    });

    new CfnOutput(this, 'AgentEnabled', {
      value: (params.agentEnabled || params.agents.length > 0).toString(),
    });

    new CfnOutput(this, 'SelfSignUpEnabled', {
      value: params.selfSignUpEnabled.toString(),
    });

    new CfnOutput(this, 'ModelRegion', {
      value: api.modelRegion,
    });

    new CfnOutput(this, 'ModelIds', {
      value: JSON.stringify(api.modelIds),
    });

    new CfnOutput(this, 'ImageGenerateModelIds', {
      value: JSON.stringify(api.imageGenerationModelIds),
    });

    new CfnOutput(this, 'EndpointNames', {
      value: JSON.stringify(api.endpointNames),
    });

    new CfnOutput(this, 'SamlAuthEnabled', {
      value: params.samlAuthEnabled.toString(),
    });

    new CfnOutput(this, 'SamlCognitoDomainName', {
      value: params.samlCognitoDomainName ?? '',
    });

    new CfnOutput(this, 'SamlCognitoFederatedIdentityProviderName', {
      value: params.samlCognitoFederatedIdentityProviderName ?? '',
    });

    new CfnOutput(this, 'AgentNames', {
      value: Buffer.from(JSON.stringify(api.agentNames)).toString('base64'),
    });

    new CfnOutput(this, 'InlineAgents', {
      value: params.inlineAgents.toString(),
    });

    new CfnOutput(this, 'UseCaseBuilderEnabled', {
      value: params.useCaseBuilderEnabled.toString(),
    });

    new CfnOutput(this, 'HiddenUseCases', {
      value: JSON.stringify(params.hiddenUseCases),
    });

    this.userPool = auth.userPool;
    this.userPoolClient = auth.client;

    this.exportValue(this.userPool.userPoolId);
    this.exportValue(this.userPoolClient.userPoolClientId);
  }
}
```

このスタックでは、以下のリソースを作成しています。

- `Auth`
- `Database`
- `Api`
- `CommonWebAcl`
- `Web`
- `Rag`
- `RagKnowledgeBase`
- `UseCaseBuilder`
- `Transcribe`

上から 1 つずつ見ていきます。

#### GenerativeAiUseCasesStack > Auth リソース

Auth リソースは、アーキテクチャ図でいうと以下の赤枠の部分にあたります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217144/5574457d-75b6-4c20-af4d-240157903b3e.png)

Auth リソースの実体は `packages/cdk/lib/construct/auth.ts` にあります。
以下のソースコードが Auth の定義です。

```typescript:packages/cdk/lib/construct/auth.ts
import { Duration } from 'aws-cdk-lib';
import {
  UserPool,
  UserPoolClient,
  UserPoolOperation,
} from 'aws-cdk-lib/aws-cognito';
import {
  IdentityPool,
  UserPoolAuthenticationProvider,
} from '@aws-cdk/aws-cognito-identitypool-alpha';
import { Effect, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';

export interface AuthProps {
  selfSignUpEnabled: boolean;
  allowedIpV4AddressRanges?: string[] | null;
  allowedIpV6AddressRanges?: string[] | null;
  allowedSignUpEmailDomains?: string[] | null;
  samlAuthEnabled: boolean;
}

export class Auth extends Construct {
  readonly userPool: UserPool;
  readonly client: UserPoolClient;
  readonly idPool: IdentityPool;

  constructor(scope: Construct, id: string, props: AuthProps) {
    super(scope, id);

    const userPool = new UserPool(this, 'UserPool', {
      // SAML 認証を有効化する場合、UserPool を利用したセルフサインアップは利用しない。セキュリティを意識して閉じる。
      selfSignUpEnabled: props.samlAuthEnabled
        ? false
        : props.selfSignUpEnabled,
      signInAliases: {
        username: false,
        email: true,
      },
      passwordPolicy: {
        requireUppercase: true,
        requireSymbols: true,
        requireDigits: true,
        minLength: 8,
      },
    });

    const client = userPool.addClient('client', {
      idTokenValidity: Duration.days(1),
    });

    const idPool = new IdentityPool(this, 'IdentityPool', {
      authenticationProviders: {
        userPools: [
          new UserPoolAuthenticationProvider({
            userPool,
            userPoolClient: client,
          }),
        ],
      },
    });

    if (props.allowedIpV4AddressRanges || props.allowedIpV6AddressRanges) {
      const ipRanges = [
        ...(props.allowedIpV4AddressRanges
          ? props.allowedIpV4AddressRanges
          : []),
        ...(props.allowedIpV6AddressRanges
          ? props.allowedIpV6AddressRanges
          : []),
      ];

      idPool.authenticatedRole.attachInlinePolicy(
        new Policy(this, 'SourceIpPolicy', {
          statements: [
            new PolicyStatement({
              effect: Effect.DENY,
              resources: ['*'],
              actions: ['*'],
              conditions: {
                NotIpAddress: {
                  'aws:SourceIp': ipRanges,
                },
              },
            }),
          ],
        })
      );
    }

    idPool.authenticatedRole.attachInlinePolicy(
      new Policy(this, 'PollyPolicy', {
        statements: [
          new PolicyStatement({
            effect: Effect.ALLOW,
            resources: ['*'],
            actions: ['polly:SynthesizeSpeech'],
          }),
        ],
      })
    );

    // Lambda
    if (props.allowedSignUpEmailDomains) {
      const checkEmailDomainFunction = new NodejsFunction(
        this,
        'CheckEmailDomain',
        {
          runtime: Runtime.NODEJS_LATEST,
          entry: './lambda/checkEmailDomain.ts',
          timeout: Duration.minutes(15),
          environment: {
            ALLOWED_SIGN_UP_EMAIL_DOMAINS_STR: JSON.stringify(
              props.allowedSignUpEmailDomains
            ),
          },
        }
      );

      userPool.addTrigger(
        UserPoolOperation.PRE_SIGN_UP,
        checkEmailDomainFunction
      );
    }

    this.client = client;
    this.userPool = userPool;
    this.idPool = idPool;
  }
}
```

この中では、以下の AWS リソースを生成するコンストラクトを定義しています。

- `UserPool`
- `IdentityPool`
- `NodejsFunction`

##### GenerativeAiUseCasesStack > Auth > UserPool リソース

[`UserPool`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPool.html) は Cognito ユーザプールのリソースです。

以下のソースコードが UserPool の定義です。

```typescript:packages/cdk/lib/construct/auth.ts (抜粋)
    const userPool = new UserPool(this, 'UserPool', {
      // SAML 認証を有効化する場合、UserPool を利用したセルフサインアップは利用しない。セキュリティを意識して閉じる。
      selfSignUpEnabled: props.samlAuthEnabled
        ? false
        : props.selfSignUpEnabled,
      signInAliases: {
        username: false,
        email: true,
      },
      passwordPolicy: {
        requireUppercase: true,
        requireSymbols: true,
        requireDigits: true,
        minLength: 8,
      },
    });

    const client = userPool.addClient('client', {
      idTokenValidity: Duration.days(1),
    });

```

この中では、Cognito ユーザプールおよびアプリケーションクライアントを生成しています。

- 名前は `UserPool`
- `selfSignUpEnabled`: ユーザ自らサインアップ (アカウント作成) することを許可するか
  - パラメータの `samlAuthEnabled` が true なら false (許可しない)
  - パラメータの `samlAuthEnabled` が false なら パラメータの `selfSignUpEnabled`
  - つまり、SAML 認証をする場合は一律許可せず、SAML 認証をしない場合はパラメータで制御可能
  - `samlAuthEnabled` の初期値は false ([stack-input.ts`](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/main/packages/cdk/lib/stack-input.ts)で設定)
  - `selfSignUpEnabled` の初期値は true ([stack-input.ts`](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/main/packages/cdk/lib/stack-input.ts)で設定)
- [`signInAliases`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.SignInAliases.html): ユーザープールに登録またはサインインする方法
  - ユーザ名は不可、メールアドレスは許可
- [`passwordPolicy`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.PasswordPolicy.html): パスワードポリシー
  - パスワードに大文字が必要
  - パスワードに記号が必要
  - パスワードに数字が必要
  - パスワードの最小文字数は 8 文字
- ユーザプールに `client` アプリケーションクライアントを追加
  - [`idTokenValidity`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPoolClientOptions.html): ID トークンの有効期限を 1 日に設定

##### GenerativeAiUseCasesStack > Auth > IdentityPool リソース

[`IdentityPool`](https://docs.aws.amazon.com/cdk/api/v2/docs/@aws-cdk_aws-cognito-identitypool-alpha.IdentityPool.html) は Cognito ID プールのリソースです。

以下のソースコードが IdentityPool の定義です。

```typescript:packages/cdk/lib/construct/auth.ts (抜粋)
    const idPool = new IdentityPool(this, 'IdentityPool', {
      authenticationProviders: {
        userPools: [
          new UserPoolAuthenticationProvider({
            userPool,
            userPoolClient: client,
          }),
        ],
      },
    });

    if (props.allowedIpV4AddressRanges || props.allowedIpV6AddressRanges) {
      const ipRanges = [
        ...(props.allowedIpV4AddressRanges
          ? props.allowedIpV4AddressRanges
          : []),
        ...(props.allowedIpV6AddressRanges
          ? props.allowedIpV6AddressRanges
          : []),
      ];

      idPool.authenticatedRole.attachInlinePolicy(
        new Policy(this, 'SourceIpPolicy', {
          statements: [
            new PolicyStatement({
              effect: Effect.DENY,
              resources: ['*'],
              actions: ['*'],
              conditions: {
                NotIpAddress: {
                  'aws:SourceIp': ipRanges,
                },
              },
            }),
          ],
        })
      );
    }

    idPool.authenticatedRole.attachInlinePolicy(
      new Policy(this, 'PollyPolicy', {
        statements: [
          new PolicyStatement({
            effect: Effect.ALLOW,
            resources: ['*'],
            actions: ['polly:SynthesizeSpeech'],
          }),
        ],
      })
    );
```

この中では、Cognito ID プールを生成し、認証済みユーザに与えるロールを指定しています。

- 名前は `IdentityPool`
- `authenticationProviders`: 認証プロバイダーに前述のユーザプール、アプリケーションクライアントを指定
- 認証済みユーザのロールをアタッチ
  - AWS WAF による IPv4、IPv6 制限をかけている場合、認証されたユーザのソース IP アドレスが該当の IP アドレスレンジでない場合、すべてのアクションを拒否する `SourceIpPolicy` ポリシー
  - Amazon Polly の合成音声を生成する `polly:SynthesizeSpeech` を許可するポリシー

##### GenerativeAiUseCasesStack > Auth > NodejsFunction リソース

[`NodejsFunction`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.NodejsFunction.html) は NodeJS ランタイムの Lambda 関数のリソースです。

以下のソースコードが NodejsFunction の定義です。

```typescript:packages/cdk/lib/construct/auth.ts (抜粋)
    // Lambda
    if (props.allowedSignUpEmailDomains) {
      const checkEmailDomainFunction = new NodejsFunction(
        this,
        'CheckEmailDomain',
        {
          runtime: Runtime.NODEJS_LATEST,
          entry: './lambda/checkEmailDomain.ts',
          timeout: Duration.minutes(15),
          environment: {
            ALLOWED_SIGN_UP_EMAIL_DOMAINS_STR: JSON.stringify(
              props.allowedSignUpEmailDomains
            ),
          },
        }
      );

      userPool.addTrigger(
        UserPoolOperation.PRE_SIGN_UP,
        checkEmailDomainFunction
      );
    }
```

この中では、Lambda 関数を生成し、ユーザプールのサインアップ前処理に設定しています。

- パラメータ `allowedSignUpEmailDomains` に値がある場合、Lambda 関数を作成する
  - `allowedSignUpEmailDomains` の初期値は null
  - 例えば、`allowedSignUpEmailDomains: ["amazon.com"]` とした場合、amazon.com ドメインのメールアドレスしかサインアップ (アカウント作成) できなくなる ([参照](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/main/docs/DEPLOY_OPTION.md#%E3%82%B5%E3%82%A4%E3%83%B3%E3%82%A2%E3%83%83%E3%83%97%E3%81%A7%E3%81%8D%E3%82%8B%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%81%AE%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E3%82%92%E5%88%B6%E9%99%90%E3%81%99%E3%82%8B))
- Lambda 関数
  - 名前は `CheckEmailDomain`
  - ソースコードは `./lambda/checkEmailDomain.ts` (後述)
  - タイムアウトは最長の 15 分
  - 環境変数 `ALLOWED_SIGN_UP_EMAIL_DOMAINS_STR` に `allowedSignUpEmailDomains` の値を設定
- 前述のユーザプールのトリガーを設定
  - 実行タイミング [`UserPoolOperation.PRE_SIGN_UP`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPoolOperation.html) はサインアップ要求前
  - 処理内容として Lambda 関数を指定する

###### checkEmailDomain.ts の処理

この `CheckEmailDomain` 関数の実体は [`packages/cdk/lambda/checkEmailDomain.ts`] にあります。

```typescript:packages/cdk/lambda/checkEmailDomain.ts
import { PreSignUpTriggerEvent, Context, Callback } from 'aws-lambda';

const ALLOWED_SIGN_UP_EMAIL_DOMAINS_STR =
  process.env.ALLOWED_SIGN_UP_EMAIL_DOMAINS_STR;
const ALLOWED_SIGN_UP_EMAIL_DOMAINS: string[] = JSON.parse(
  ALLOWED_SIGN_UP_EMAIL_DOMAINS_STR!
);

// メールアドレスのドメインを許可するかどうかを判定する
const checkEmailDomain = (email: string): boolean => {
  // メールアドレスの中の @ の数が1つでない場合は、常に許可しない
  if (email.split('@').length !== 2) {
    return false;
  }

  // メールアドレスのドメイン部分が、許可ドメインの"いずれか"と一致すれば許可する
  // それ以外の場合は、許可しない
  // (ALLOWED_SIGN_UP_EMAIL_DOMAINSが空の場合は、常に許可しない)
  const domain = email.split('@')[1];
  return ALLOWED_SIGN_UP_EMAIL_DOMAINS.includes(domain);
};

/**
 * Cognito Pre Sign-up Lambda Trigger.
 *
 * @param event - The event from Cognito.
 * @param context - The Lambda execution context.
 * @param callback - The callback function to return data or error.
 */
exports.handler = async (
  event: PreSignUpTriggerEvent,
  context: Context,
  callback: Callback
) => {
  try {
    console.log('Received event:', JSON.stringify(event, null, 2));

    const isAllowed = checkEmailDomain(event.request.userAttributes.email);
    if (isAllowed) {
      // 成功した場合、イベントオブジェクトをそのまま返す
      callback(null, event);
    } else {
      // 失敗した場合、エラーメッセージを返す
      callback(new Error('Invalid email domain'));
    }
  } catch (error) {
    console.log('Error ocurred:', error);
    // エラーがError型であるか確認し、適切なエラーメッセージを返す
    if (error instanceof Error) {
      callback(error);
    } else {
      // エラーがError型ではない場合、一般的なエラーメッセージを返す
      callback(new Error('An unknown error occurred.'));
    }
  }
};
```

この Lambda 関数では サインアップ (アカウント作成) 要求に含まれるメールアドレスのドメイン部分が、許可ドメインのいずれかと一致すれば許可、いずれも一致しない場合は拒否します。

- ドメイン検証が NG の場合、エラーメッセージ **Invalid email domain** を返します。

Auth だけでかなり長くなってきたため、記事を分割します。
次回は GenU 内の `GenerativeAiUseCasesStack` スタックの `Database` リソースから解説したいと思います。

## (参考) GenU のバックエンド (CDK) 詳細解説投稿一覧

- [①AWS CDK のセットアップ](https://qiita.com/siruko/items/fd25fdcf89615cb85262)
- [②AWS CDK の動作確認](https://qiita.com/siruko/items/73169f986b4173e3d3a5)
- [③GenU の概要](https://qiita.com/siruko/items/625801b9e1847b305c1e)
- [④GenU CDK スタックの概要](https://qiita.com/siruko/items/8570ed43f7162ea4b907)
- [⑤CloudFrontWafStack スタックの解説](https://qiita.com/siruko/items/30439576ee7c63165d21)
- [⑥RagKnowledgeBaseStack スタックの解説](https://qiita.com/siruko/items/1223c9d22e73168a8809)
- [⑦WebSearchAgentStack スタックの解説](https://qiita.com/siruko/items/aef0a9599df60d47eb1e)
- [⑧GuardrailStack スタックの解説](https://qiita.com/siruko/items/d16e9fe27df4673d7554)
- [⑨GenerativeAiUseCasesStack > Auth スタックの解説](https://qiita.com/siruko/items/0c14040a1af132f42382)
- [⑩GenerativeAiUseCasesStack > Database, Api スタックの解説](https://qiita.com/siruko/items/5f249f11847f3829c18a)
- [⑪GenerativeAiUseCasesStack > CommonWebAcl, Web, Rag スタックの解説](https://qiita.com/siruko/items/1063daa2efe0b374a3fc)
- [⑫GenerativeAiUseCasesStack > RagKnowledgeBase, UseCaseBuilder, Transcribe スタックの解説](https://qiita.com/siruko/items/73d6006004caf8648594)
- [⑬DashBoard スタックの解説](https://qiita.com/siruko/items/73a75c7d0146d12ecbc3)
- [⑭GenU の Outputs の解説](https://qiita.com/siruko/items/afc14128a5b1a15ab69c)