aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.37k stars 3.77k forks source link

`aws_cognito.UserPool` doesn't wait for a new `aws_ses.EmailIdentity` to be ready #28531

Open ppena-LiveData opened 6 months ago

ppena-LiveData commented 6 months ago

Describe the feature

I might be doing something wrong, but I'm using aws_cognito.UserPoolEmail.withSes(), and no matter what I tried, I couldn't get CloudFormation to wait until a new aws_ses.EmailIdentity was ready before trying to find it to use it in a Cognito UserPool.

Use Case

I'd like to use a single CDK stack to create both an aws_ses.EmailIdentity and a aws_cognito.UserPool that references the Simple Email Service's EmailIdentity. I'd also like to create an aws_ses.ReceiptRuleSet in the same stack as the aws_ses_EmailIdentity without having to add a lot of addDependency() calls.

Proposed Solution

If it's possible, aws_cognito.UserPoolEmail.withSes should allow an aws_ses.EmailIdentity to be passed in to make sure any dependencies are properly configured. The same would also be nice for an ses.ReceiptRuleSet.

Other Information

The below is what I tried, but no matter what add_dependency() calls I added, I couldn't get it to work with a single stack and had to split it up into two separate stacks. I kept getting errors like this:

CDK-Bug-Test-Cognito |  8/11 | 6:55:26 AM | CREATE_FAILED        | AWS::Cognito::UserPool   | CognitoUserPool (CognitoUserPool53E37E69) Resource handler returned message: "Cognito received the following error from Amazon SES when attempting to send email: Email address is not verified. The following identities failed the check in region US-EAST-2: arn:aws:ses:us-east-2:245161217798:identity/paulie.livedata.com (Service: CognitoIdentityProvider, Status Code: 400, Request ID: c9d4ffe5-7d80-4a30-ad50-fc3ee712a3bc)" (RequestToken: 6af80751-1d91-b041-aaf3-8d086fb31439, HandlerErrorCode: InvalidRequest)

Here's the Python code:

import aws_cdk as cdk
from aws_cdk import App, Environment, Stack
from aws_cdk import aws_cognito as cognito
from aws_cdk import aws_route53 as route_53
from aws_cdk import aws_ses as ses
from aws_cdk import aws_ses_actions as ses_actions
from constructs import Construct

class SesStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, route53_hosted_zone: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        ses.EmailIdentity(
            self, 'EmailIdentity',
            identity=ses.Identity.public_hosted_zone(
                route_53.HostedZone.from_hosted_zone_attributes(
                    self, 'HostedZone',
                    hosted_zone_id=self.node.try_get_context('route53_hosted_zone_id'),
                    zone_name=route53_hosted_zone,
                )
            ),
            mail_from_domain=f'mail.{route53_hosted_zone}'
        )

class CognitoStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, route53_hosted_zone: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        identity = ses.EmailIdentity(
            self, 'EmailIdentity',
            identity=ses.Identity.public_hosted_zone(
                route_53.HostedZone.from_lookup(self, 'HostedZone', domain_name=route53_hosted_zone)
            ),
            mail_from_domain=f'mail.{route53_hosted_zone}'
        )

        from_email = f'noreply@{route53_hosted_zone}'
        email_rule_set = ses.ReceiptRuleSet(self, 'EmailRuleSet')
        email_rule_set.add_rule('EmailReceiptNoReply', recipients=[from_email],
                                actions=[ses_actions.Bounce(
                                    sender=from_email, template=ses_actions.BounceTemplate.MAILBOX_DOES_NOT_EXIST)])

        # ### This did seem to work for the ReceiptRuleSet:
        # make sure all the SES identity parts are done before the ReceiptRuleSet tries referencing the identity
        for child in identity.node.children:
            if child.node.default_child:  # this is a higher-level construct, so depend on its children
                for grandchild in child.node.children:
                    email_rule_set.node.add_dependency(grandchild)
            else:
                email_rule_set.node.add_dependency(child)

        user_pool = cognito.UserPool(
            self, 'CognitoUserPool',
            user_pool_name='test-cdk-bug',
            removal_policy=cdk.RemovalPolicy.DESTROY,
            email=cognito.UserPoolEmail.with_ses(
                from_email=from_email,
                from_name='Administrator',
                reply_to='support@example.com',
                ses_verified_domain=route53_hosted_zone,
            )
        )

        # ### But this was not sufficient for the UserPool:
        # make sure all the SES identity parts are done before the UserPool tries referencing the identity
        for child in identity.node.children:
            if child.node.default_child:  # this is a higher-level construct, so depend on its children
                for grandchild in child.node.children:
                    user_pool.node.add_dependency(grandchild)
            else:
                user_pool.node.add_dependency(child)

app = App()
route53_hosted_zone: str = app.node.try_get_context('route53_hosted_zone')

# ses_stack = SesStack(
#     app, 'CDK-Bug-Test-SES', route53_hosted_zone,
#     env=Environment(account=app.node.try_get_context('account'), region=app.node.try_get_context('region')),
# )

stack = CognitoStack(
    app, 'CDK-Bug-Test-Cognito', route53_hosted_zone,
    env=Environment(account=app.node.try_get_context('account'), region=app.node.try_get_context('region')),
)
# stack.add_dependency(ses_stack)

app.synth()

Acknowledgements

CDK version used

2.117.0

Environment details (OS name and version, etc.)

Windows 11

ppena-LiveData commented 6 months ago

NOTE: this race condition doesn't happen all the time. I reproduced the problem at least half a dozen times while writing up this ticket, but I just tried it a couple times, and the above code worked without a problem, which of course is very annoying and means this might be hard to fix.