Search code examples
aws-cdkwatchaws-cdk-typescript

cdk watch command forces full deploy with unrelated error on file change


I'm developing a little CDKv2 script to instantiate a few AWS services. I have some lambda code deployed in the lambda/ folder and the frontend stored in a bucket populated using the frontend/ folder in the source.

I've noticed that whenever I make a change to any of the file inside these two, cdk watch return the following error and falls back to perform a full redeploy (which is significantly slow).

Could not perform a hotswap deployment, because the CloudFormation template could not be resolved: Parameter or resource 'DomainPrefix' could not be found for evaluation
Falling back to doing a full deployment

Is there any way to make changes in these folders only trigger updating the related bucket content or the related lambda?

Following here the stack.ts for quick reference, just in case here you can take a look at the repo.

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

    const domainPrefixParam = new CfnParameter(this, 'DomainPrefix', {
      type: 'String',
      description: 'You have to set it in google cloud as well', //(TODO: add link to explain properly)
      default: process.env.DOMAIN_NAME || ''
    })

    const googleClientIdParam = new CfnParameter(this, 'GoogleClientId', {
      type: 'String',
      description: 'From google project',
      noEcho: true,
      default: process.env.GOOGLE_CLIENT_ID || ''
    })

    const googleClientSecretParam = new CfnParameter(this, 'GoogleClientSecret', {
      type: 'String',
      description: 'From google project',
      noEcho: true,
      default: process.env.GOOGLE_CLIENT_SECRET || ''
    })

    if(!domainPrefixParam.value || !googleClientIdParam.value || !googleClientSecretParam.value){
      throw new Error('Make sure you initialized DomainPrefix, GoogleClientId and GoogleClientSecret in the stack parameters')
    }

    const s3frontend = new s3.Bucket(this, 'Bucket', {
      bucketName: domainPrefixParam.valueAsString+'-frontend-bucket',
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
      enforceSSL: true,
      versioned: false,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      websiteIndexDocument: "index.html",
    }); 

    //TODO: fare in modo che questa origin access identity non sia legacy quando deployo
    const cfdistributionoriginaccessidentity = new cloudfront.OriginAccessIdentity(this, 'CFOriginAccessIdentity', {
      comment: "Used to give bucket read to cloudfront"
    })

    const cfdistribution = new cloudfront.CloudFrontWebDistribution(this, 'CFDistributionFrontend', {
      originConfigs: [
        {
          s3OriginSource: {
            s3BucketSource: s3frontend,
            originAccessIdentity: cfdistributionoriginaccessidentity
          },
          behaviors: [{ 
            isDefaultBehavior: true,
            allowedMethods: cloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
            forwardedValues: {
              queryString: true,
              cookies: { forward: 'all' }
            },
            minTtl: cdk.Duration.seconds(0),
            defaultTtl: cdk.Duration.seconds(3600),
            maxTtl: cdk.Duration.seconds(86400)
          }]
        }
      ]
    })

    s3frontend.grantRead(cfdistributionoriginaccessidentity)

    const cfdistributionpolicy = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: ['cloudfront:CreateInvalidation'],
      resources: [`"arn:aws:cloudfront::${this.account}:distribution/${cfdistribution.distributionId}"`]
    });

    const userpool = new cognito.UserPool(this, 'WebAppUserPool', {
      userPoolName: 'web-app-user-pool',
      selfSignUpEnabled: false
    })

    const userpoolidentityprovidergoogle = new cognito.UserPoolIdentityProviderGoogle(this, 'WebAppUserPoolIdentityGoogle', {
      clientId: googleClientIdParam.valueAsString,
      clientSecret: googleClientSecretParam.valueAsString,
      userPool: userpool,
      attributeMapping: {
        email: cognito.ProviderAttribute.GOOGLE_EMAIL
      },
      scopes: [ 'email' ]
    })

    // this is used to make the hostedui reachable
    userpool.addDomain('Domain', {
      cognitoDomain: {
        domainPrefix: domainPrefixParam.valueAsString
      }
    })

    const CLOUDFRONT_PUBLIC_URL = `https://${cfdistribution.distributionDomainName}/`

    const client = userpool.addClient('Client', {
      oAuth: {
        flows: {
          authorizationCodeGrant: true
        },
        callbackUrls: [
          CLOUDFRONT_PUBLIC_URL
        ],
        logoutUrls: [
          CLOUDFRONT_PUBLIC_URL
        ],
        scopes: [
          cognito.OAuthScope.EMAIL,
          cognito.OAuthScope.OPENID,
          cognito.OAuthScope.PHONE
        ]
      },
      supportedIdentityProviders: [
        cognito.UserPoolClientIdentityProvider.GOOGLE
      ]
    })

    client.node.addDependency(userpoolidentityprovidergoogle)

    // defines an AWS Lambda resource
    const securedlambda = new lambda.Function(this, 'AuhtorizedRequestsHandler', {
      runtime: lambda.Runtime.NODEJS_14_X,
      code: lambda.Code.fromAsset('lambda'),
      handler: 'secured.handler'
    });

    const lambdaapiintegration = new apigw.LambdaIntegration(securedlambda)

    const backendapigw = new apigw.RestApi(this, 'AuthorizedRequestAPI', {
      restApiName: domainPrefixParam.valueAsString,
      defaultCorsPreflightOptions: {
        "allowOrigins": apigw.Cors.ALL_ORIGINS,
        "allowMethods": apigw.Cors.ALL_METHODS,
      }
    })

    const backendapiauthorizer = new apigw.CognitoUserPoolsAuthorizer(this, 'BackendAPIAuthorizer', {
      cognitoUserPools: [userpool]
    })

    const authorizedresource = backendapigw.root.addMethod('GET', lambdaapiintegration, {
      authorizer: backendapiauthorizer,
      authorizationType: apigw.AuthorizationType.COGNITO
    })

    const s3deploymentfrontend = new s3deployment.BucketDeployment(this, 'DeployFrontEnd', {
      sources: [
        s3deployment.Source.asset('./frontend'),
        s3deployment.Source.data('constants.js', `const constants = {domainPrefix:'${domainPrefixParam.valueAsString}', region:'${this.region}', cognito_client_id:'${client.userPoolClientId}', apigw_id:'${backendapigw.restApiId}'}`)
      ],
      destinationBucket: s3frontend,
      distribution: cfdistribution
    })

    new cdk.CfnOutput(this, 'YourPublicCloudFrontURL', {
      value: CLOUDFRONT_PUBLIC_URL,
      description: 'Navigate to the URL to access your deployed application'
    })
  }
}

Solution

  • Recording the solution from the comments:

    Cause:

    cdk watch apparently does not work with template parameters. I guess this is because the default --hotswap option bypasses CloudFormation and deploys instead via SDK commands.

    Solution:

    • Remove the CfnParamters from the template. CDK recommends not using parameters in any case.
    • Perhaps cdk watch --no-hotswap would also work?