Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Best approach to modify CDK to use additional services #481

Open
hiddengearz opened this issue Feb 25, 2024 · 7 comments
Open

Best approach to modify CDK to use additional services #481

hiddengearz opened this issue Feb 25, 2024 · 7 comments
Labels
need-opinion We need ideas from community

Comments

@hiddengearz
Copy link

Hello,

Svelte & cloudformation noob here, I've been working on an demo app that uses AWS bedrock and cognito. However after using this adapter I realized it generates a cloudformation template and not the CDK code, so I'm a bit stuck on the best way to incorporate bedrock and cognito.

What would you suggest?

  • The naive approach seems to be to manually modify the cloudformation template every time i build the app.
  • I'm currently looking into ways to "daisy chain" cloudformation templates, which seems to be the better approach?
@hiddengearz
Copy link
Author

hiddengearz commented Feb 25, 2024

TIL you can import cloudformation into CDK, seems like this may be my best bet as I'm far more familiar with CDK.

Would still love to hear others opinions!

@jill64 jill64 added the need-opinion We need ideas from community label Feb 26, 2024
@hiddengearz
Copy link
Author

Importing the cloudformation CDK does work, though because the names of the constructs like S3 buckets and etc change everytime you'll have to play with regex if you need to make any modifications to them e.g assign roles.

It honestly may be easier to fork this repo and make changes to the original cdk in sveltekit-adapter-aws/cdk/arch.

Here is an example if anyone decides to go down this route:

export class SveleteStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, {...props,analyticsReporting: false} );

    const filePath = path.resolve(__dirname, '../../build/cdk.out/faulty-bedrock.template.json');
    //Get template from Svelete build
    const template = new cfninc.CfnInclude(this, 'Template', { 
      templateFile: filePath,
    });


    const [lambdaName, s3BucketName, cloudDistributionName] =  getCFResources();

    //get lambda that was genereated by Svelete
    const cfnLambda = template.getResource(lambdaName) as lambda.CfnFunction;
    const lambdaFunc = lambda.Function.fromFunctionName(this, lambdaName, cfnLambda.ref);

    //create bedrock policy
    const bedrockAccessPolicy = new PolicyStatement({
      effect: Effect.ALLOW,
      actions: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"], // See: https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_amazonbedrock.html
      resources: ["arn:aws:bedrock:*::foundation-model/anthropic.claude-v2"],
    });

    //Add these policies to the role used by that lambda
    lambdaFunc.addToRolePolicy(bedrockAccessPolicy);

@hiddengearz
Copy link
Author

Ultimately ended up just copying the cdk from the adapter and modifying it. The above method seems nice but is too much of a headache, likely caused some of the issues I experienced in #485

@jill64
Copy link
Owner

jill64 commented Mar 1, 2024

It is believed that only manual resource creation and integration is being used at the moment.
However, integration with other AWS resources is an important issue.
I would like to add an optional CDK extension function that takes the resource created by the adapter as an argument.

// svelte.config.js
{
  // ...
  adapter: adapter({
    // ...
    architecture: 'lambda-s3',
    extends: (lambda, s3, cloudfront) => {
      new aws.cognito.UserPoolClient(this, 'UserPoolClient', {
        userPool: 'UserPool',
        generateSecret: false,
        userPoolClientName: 'UserPoolClient'
        // ...
      })

      // ...
    }
  })
}

Naturally, they should be separated into separate files.
We will also provide the necessary types.

// svelte.config.js
import { extendsCDK } from './extendsCDK.js'

{
  // ...
  adapter: adapter({
    // ...
    architecture: 'lambda-s3',
    extends: extendsCDK
  })
}
// extendsCDK.ts
import type { ExtendsLambdaS3 } from '@jill64/sveltekit-adapter-aws/types'

export const extendsCDK: ExtendsLambdaS3 = (lambda, s3, cloudfront) => {
  new aws.cognito.UserPoolClient(this, 'UserPoolClient', {
    userPool: 'UserPool',
    generateSecret: false,
    userPoolClientName: 'UserPoolClient'
    // ...
  })

  // ...
}

I have considered merging CloudFormation templates, but it seems a bit too challenging for me.
What do you think about this idea?

@hiddengearz
Copy link
Author

hiddengearz commented Mar 3, 2024

I think the above would work well, especially for simple stacks.

The main issue I ran into (which I believe this method will run into aswell) is that it seems to be far more difficult to make modifications to a stack/cloud formation template, from another stack. I had to make changes to the constructs in the generated stack and trying to do that from another stack, was annoying and ultimately didn't work. I'm not a pro but that is my experience from my attempt above.

For advanced stacks If I was able to modify the generated stack and it not be overwritten every time I run pnpm run build that would be sufficient.

I'll upload the two CDK's I made as an example, both should of done basically the same thing (sorry for the messy code, I wasn't planning to post this publicly anytime soon)

Attempt 1 - Importing a cloudformation template

I believe if you go the route of using 2 stacks you'll ultimately end up importing two CDK's or cloud formation templates. Interactions between them is difficult (from my limited experience)

import * as cdk from 'aws-cdk-lib';
import * as cfninc from 'aws-cdk-lib/cloudformation-include';
import * as lambda from "aws-cdk-lib/aws-lambda";
import { Construct } from 'constructs';
import {
  PolicyStatement,
  Effect,
  Role,
  ServicePrincipal,
} from "aws-cdk-lib/aws-iam";
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as cr from 'aws-cdk-lib/custom-resources';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as path from 'path';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as fs from 'fs';
import * as cd from 'aws-cdk-lib/aws-cloudfront';

export class SveleteStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, {...props,analyticsReporting: false} );

    const filePath = path.resolve(__dirname, '../../build/cdk.out/faulty-bedrock.template.json');
    //Get template from Svelete build
    const template = new cfninc.CfnInclude(this, 'Template', { 
      templateFile: filePath,
    });

    const mySecret = new secretsmanager.Secret(this, 'MySecret', {
      secretName: 'AUTH_SECRET',
      generateSecretString: {
        passwordLength: 32,
        excludePunctuation: true,
      },

    });

    const [lambdaName, s3BucketName, cloudDistributionName, cdkDeploymentaName] =  getCFResources();

    //get CustomCDKBucketDeployment lambda that was genereated by Svelete
    const cfnCdkLambda = template.getResource(cdkDeploymentaName) as lambda.CfnFunction;
    const cdklambdaFunc = lambda.Function.fromFunctionName(this, cdkDeploymentaName, cfnCdkLambda.ref);

    

    //get server lambda that was genereated by Svelete
    const cfnLambda = template.getResource(lambdaName) as lambda.CfnFunction;
    const lambdaFunc = lambda.Function.fromFunctionName(this, lambdaName, cfnLambda.ref);
    cfnLambda.addDependency(cfnCdkLambda);

    //get S3 that was genereated by Svelete
    const cfnBucket = template.getResource(s3BucketName) as s3.CfnBucket;
    const bucket = s3.Bucket.fromBucketName(this, s3BucketName, cfnBucket.ref);
    cfnBucket.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);

    //get S3 that was genereated by Svelete
    const cfnCloudDistribution = template.getResource(cloudDistributionName) as cd.CfnDistribution;
    const callBackURL:string = "https://" + cfnCloudDistribution.attrDomainName + "/api/auth/callback/cognito"
    console.log("call back URL:", callBackURL);

    const userpool = new cognito.UserPool(this, 'user-pool', {
      signInAliases: {
        email: true,
      },
      selfSignUpEnabled: true,
      standardAttributes: {
        familyName: {
          mutable: false,
          required: false,
        },
        address: {
          mutable: true,
          required: false,
        },
      },
      customAttributes: {
        'createdAt': new cognito.DateTimeAttribute(),
        'isAdmin': new cognito.BooleanAttribute({
          mutable: false,
        }),
      },
      passwordPolicy: {
        minLength: 8,
        requireLowercase: true,
        requireUppercase: true,
        requireDigits: true,
        requireSymbols: false,
      },
      accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    const appClient = userpool.addClient('app-client', {
      
      userPoolClientName: 'app-client',
      authFlows: {
        userPassword: true,
      },
      generateSecret: true,
      oAuth: {
        callbackUrls: [
          callBackURL,
        ],
      },
    });

    //Cognito policy
    const cognitoPolicy = new PolicyStatement({
      effect: Effect.ALLOW,
      actions: ["cognito-idp:SignUp", "cognito-idp:InitiateAuth"], // See: https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazoncognitouserpools.html
      //resources: ["arn:aws:cognito-idp:*::userpool/YOUR_USER_POOL_ID"],
      resources: ["*"],
    });
    
    //create bedrock policy
    const bedrockAccessPolicy = new PolicyStatement({
      effect: Effect.ALLOW,
      actions: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"], // See: https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_amazonbedrock.html
      resources: ["arn:aws:bedrock:*::foundation-model/anthropic.claude-v2"],
    });

    //Add these policies to the role used by that lambda
    lambdaFunc.addToRolePolicy(bedrockAccessPolicy);
    lambdaFunc.addToRolePolicy(cognitoPolicy);

    // Grant read permissions to the Lambda function's execution role
    lambdaFunc.addToRolePolicy(new PolicyStatement({
      actions: ['s3:GetObject', "s3:ListBucket"],
      resources: ["*"],
    }));
    console.log("bucket arn:", bucket.bucketArn);
    

    // Define the Lambda function to update environment variables
    const updateEnvVarsLambda = new lambda.Function(this, 'UpdateEnvVarsLambda', {
      runtime: lambda.Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: lambda.Code.fromInline('exports.handler = async (event) => {}'),
    });

    // Grant necessary permissions to the updateEnvVarsLambda function
    updateEnvVarsLambda.addToRolePolicy(new PolicyStatement({
      actions: ['lambda:UpdateFunctionConfiguration'],
      resources: ['*'], // Replace with the ARN of the target Lambda function if more restrictive permissions are desired
    }));

    // Grant read permissions to the Lambda function's execution role
    updateEnvVarsLambda.addToRolePolicy(new PolicyStatement({
      actions: ['s3:GetObject'],
      resources: [bucket.arnForObjects('*')],
    }));
    console.log("bucket arn:", bucket.bucketArn);

    // Define the CloudFormation custom resource to trigger the Lambda function
    const updateEnvVars = new cr.AwsCustomResource(this, 'UpdateEnvVarsCustomResource', {
      onCreate: {
        service: 'Lambda',
        action: 'updateFunctionConfiguration',
        parameters: {
          FunctionName: lambdaFunc.functionName, // Replace with the name of the existing Lambda function
          Environment: {
            Variables: {
              COGNITO_USER_POOL_ID: userpool.userPoolId,
              COGNITO_CLIENT_ID: appClient.userPoolClientId,
              COGNITO_CLIENT_SECRET: appClient.userPoolClientSecret.unsafeUnwrap(),
              COGNITO_ISSUER: "https://cognito-idp." + this.region + ".amazonaws.com/" + userpool.userPoolId,
              AUTH_TRUST_HOST: "true",
              AUTH_SECRET: mySecret.secretValue.unsafeUnwrap() //should use secrets manager TODO

            },
          },
        },
        physicalResourceId: cr.PhysicalResourceId.of(Date.now().toString()),
      },
      onUpdate: {
        service: 'Lambda',
        action: 'updateFunctionConfiguration',
        parameters: {
          FunctionName: lambdaFunc.functionName, // Replace with the name of the existing Lambda function
          Environment: {
            Variables: {
              COGNITO_USER_POOL_ID: userpool.userPoolId,
              COGNITO_CLIENT_ID: appClient.userPoolClientId,
              COGNITO_CLIENT_SECRET: appClient.userPoolClientSecret.unsafeUnwrap(), //TODO: Bad practice, ctf app so not critical
              COGNITO_ISSUER: "https://cognito-idp." + this.region + ".amazonaws.com/" + userpool.userPoolId,
              AUTH_TRUST_HOST: "true",
              //AUTH_SECRET: mySecret.secretValue.unsafeUnwrap(), //TODO: Bad practice, ctf app so not critical
              AUTH_SECRET:"efb724599037a553a63ccf4aa24ce4f847b4eddff8ecf99da641379fcd924c36"
             

            },
          },
        },
        physicalResourceId: cr.PhysicalResourceId.of(Date.now().toString()),
      },
      policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE }),
    });
    
    updateEnvVars.node.addDependency(cfnCdkLambda);

    // Loop through all constructs in the stack
    template.node.findAll().forEach(construct => {
      if (construct.node.id !== cdkDeploymentaName){
          construct.node.addDependency(cdklambdaFunc);
        }
    });
    this.node.findAll().forEach(construct => {
      if (construct.node.id !== cdkDeploymentaName){
        construct.node.addDependency(cdklambdaFunc);
      }
    });

  }
}



// Function to find the first resource name that matches the specified criteria
function findResourceName(regex: RegExp, template: string, namePrefix: string): string{
  const match: RegExpMatchArray | null = template.match(regex);

  if (match) {
    return match[1].toString();
  }

  throw new Error(`Resource starting with prefix '${namePrefix}' not found`);
}

function getCFResources():[string, string, string, string] {
  // Load CloudFormation template file

  const filePath = path.resolve(__dirname, '../../build/cdk.out/faulty-bedrock.template.json');
  const template: string = fs.readFileSync(filePath, 'utf-8');

  // Regular expressions for matching Lambda function name and S3 bucket name
  const lambdaRegex: RegExp = /"(Server[\w\d]+)":\s*{\s*"Type"\s*:\s*"AWS::Lambda::Function"/i;
  const s3Regex: RegExp = /"(Bucket[\w\d]+)":\s*{\s*"Type"\s*:\s*"AWS::S3::Bucket"/i;
  const cdRegex: RegExp = /"(CloudFront[\w\d]+)":\s*{\s*"Type"\s*:\s*"AWS::CloudFront::Distribution"/i;
  const cdkDeploymentRex: RegExp = /"(CustomCDKBucketDeployment[\w\d]+)":\s*{\s*"Type"\s*:\s*"AWS::Lambda::Function"/i;
  
  // Find Lambda function name and S3 bucket name
  const lambdaName: string = findResourceName(lambdaRegex, template, "server");
  const s3BucketName: string = findResourceName(s3Regex, template, "cdk");
  const cloudDistributionName: string = findResourceName(cdRegex, template, "cdk");
  const cdkDeploymentaName: string = findResourceName(cdkDeploymentRex, template, "cdkDeployment");

  console.log("Lambda function name:", lambdaName);
  console.log("S3 bucket name:", s3BucketName);
  console.log("cloud Distribution name:", cloudDistributionName);
  console.log("CDK Deployment function name:", cdkDeploymentaName);
  
  return [lambdaName, s3BucketName, cloudDistributionName, cdkDeploymentaName]
}





Attempt 2 - Modifying the generated CDK
import {
  CfnOutput,
  Duration,
  Fn,
  Stack,
  StackProps,
  aws_certificatemanager,
  aws_cloudfront,
  aws_cloudfront_origins,
  aws_lambda,
  aws_s3,
  aws_s3_deployment,
  aws_cognito,
  custom_resources,
  aws_secretsmanager,
  aws_iam,
  RemovalPolicy
} from 'aws-cdk-lib'
import { Construct } from 'constructs'
import {
  appPath,
  bridgeAuthToken,
  certificateArn,
  domainName,
  environment,
  memorySize
} from '../../build/external/params';
import * as path from 'path';
import * as lambda from "aws-cdk-lib/aws-lambda";


export class CDKStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props)
   
    //Start of new code
    const buildDir = path.resolve(__dirname, '../../build/');
    
    const mySecret = new aws_secretsmanager.Secret(this, 'MySecret', {
      secretName: 'AUTH_SECRET',
      generateSecretString: {
        passwordLength: 32,
        excludePunctuation: true,
      },

    });

    const cognitoDomain = generateRandomString(12);

    const userpool = new aws_cognito.UserPool(this, 'user-pool', {
      signInAliases: {
        email: true,
      },
      selfSignUpEnabled: true,
      standardAttributes: {
        familyName: {
          mutable: false,
          required: false,
        },
        address: {
          mutable: true,
          required: false,
        },
      },
      customAttributes: {
        'createdAt': new aws_cognito.DateTimeAttribute(),
        'isAdmin': new aws_cognito.BooleanAttribute({
          mutable: false,
        }),
      },
      passwordPolicy: {
        minLength: 8,
        requireLowercase: true,
        requireUppercase: true,
        requireDigits: true,
        requireSymbols: false,
      },
      accountRecovery: aws_cognito.AccountRecovery.EMAIL_ONLY,
      removalPolicy: RemovalPolicy.DESTROY,
    });
    
    // Check if the user pool already has a domain configured
   const existingDomain = userpool.node.tryFindChild('MyUserPoolDomain') as aws_cognito.CfnUserPoolDomain | undefined;

   // If the domain doesn't exist, add it to the user pool
   if (!existingDomain) {
      userpool.addDomain('MyCognitoDomain',{
          cognitoDomain: {
              domainPrefix: cognitoDomain
          },
      });
  }
    

    const lambdaPolicy = new aws_iam.PolicyDocument({
      statements: [
        // Allow Lambda to invoke Cognito
        new aws_iam.PolicyStatement({
          actions: [
            //'cognito-idp:AdminCreateUser',
            //'cognito-idp:AdminUpdateUserAttributes',
            "cognito-idp:SignUp",
            "cognito-idp:InitiateAuth",
            "cognito-idp:RespondToAuthChallenge",
            "cognito-idp:ConfirmSignUp",
            "cognito-idp:GlobalSignOut",
            "cognito-idp:GetUser",
            "cognito-idp:UpdateUserAttributes",
            "cognito-idp:ForgotPassword",
            "cognito-idp:ConfirmForgotPassword"

             // See: https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazoncognitouserpools.html
          ],
          resources: ['*'], // Be cautious with using '*' as it grants access to all resources
        }),
        // Allow Lambda to invoke Bedrock
        new aws_iam.PolicyStatement({
          actions: [
             "bedrock:InvokeModel", 
             "bedrock:InvokeModelWithResponseStream",
          ], // See: https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazoncognitouserpools.html
          //resources: ["arn:aws:cognito-idp:*::userpool/YOUR_USER_POOL_ID"],
          resources: ['*'], //TODO lockdown to claude model Replace '*' with the ARN of the Bedrock resource if possible
        }),
       new aws_iam.PolicyStatement({
          actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
          resources: ['arn:aws:logs:*:*:*'],
        }),
      ],
    });
    
    // Step 2: Create an IAM role for the Lambda function
    const lambdaRole = new aws_iam.Role(this, 'LambdaRole', {
      assumedBy: new aws_iam.ServicePrincipal('lambda.amazonaws.com'),
    });
    
    // Step 3: Attach the IAM policy to the IAM role
    lambdaRole.attachInlinePolicy(new aws_iam.Policy(this, 'LambdaPolicy', { document: lambdaPolicy }));
  
    //end of new code

    const lambdaFunc = new aws_lambda.Function(this, 'Server', {
      runtime: aws_lambda.Runtime.NODEJS_18_X,
      code: aws_lambda.Code.fromAsset(buildDir.toString() + '/lambda'),
      handler: 'server.handler',
      architecture: aws_lambda.Architecture.ARM_64,
      memorySize,
      timeout: Duration.seconds(30),
      environment,
      role: lambdaRole
    });

    const lambdaURL = lambdaFunc.addFunctionUrl({
      authType: aws_lambda.FunctionUrlAuthType.NONE,
      invokeMode: aws_lambda.InvokeMode.RESPONSE_STREAM
    })


    const certificate = certificateArn
      ? aws_certificatemanager.Certificate.fromCertificateArn(
          this,
          'CertificateManagerCertificate',
          certificateArn
        )
      : undefined

    const s3 = new aws_s3.Bucket(this, 'Bucket', {
      transferAcceleration: true
    })

    const cf2 = new aws_cloudfront.Function(this, 'CF2', {
      code: aws_cloudfront.FunctionCode.fromFile({
        filePath: buildDir.toString() +'/cf2/index.js'
      })
    })

    const behaviorBase = {
      viewerProtocolPolicy:
        aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
      originRequestPolicy:
        aws_cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
      functionAssociations: [
        {
          function: cf2,
          eventType: aws_cloudfront.FunctionEventType.VIEWER_REQUEST
        }
      ]
    }

    const cdn = new aws_cloudfront.Distribution(this, 'CloudFront', {
      domainNames: domainName ? [domainName] : undefined,
      certificate,
      defaultBehavior: {
        ...behaviorBase,
        allowedMethods: aws_cloudfront.AllowedMethods.ALLOW_ALL,
        cachePolicy: aws_cloudfront.CachePolicy.CACHING_DISABLED,
        origin: new aws_cloudfront_origins.HttpOrigin(
          Fn.select(2, Fn.split('/', lambdaURL.url)),
          {
            protocolPolicy: aws_cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
            originSslProtocols: [aws_cloudfront.OriginSslPolicy.TLS_V1_2],
            customHeaders: {
              'Bridge-Authorization': `Plain ${bridgeAuthToken}`
            }
          }
        )
      },
      httpVersion: aws_cloudfront.HttpVersion.HTTP2_AND_3,
      additionalBehaviors: {
        [appPath]: {
          ...behaviorBase,
          origin: new aws_cloudfront_origins.S3Origin(s3)
        }
      }
    })

    new aws_s3_deployment.BucketDeployment(this, 'S3Deploy', {
      sources: [aws_s3_deployment.Source.asset(buildDir.toString() + '/s3')],
      destinationBucket: s3,
      distribution: cdn
    })  
    
    const cdnName: string = "https://" + cdn.domainName ?? cdn.distributionDomainName;
    console.log("cdnName = " + cdnName);

    const appClient = userpool.addClient('app-client', {
      userPoolClientName: 'app-client',
      authFlows: {
        userPassword: true,
      },
      generateSecret: true,
      oAuth: {
        callbackUrls: [
          cdnName + "/auth/callback/cognito",
          cdnName + ":5173/auth/callback/cognito",
          cdnName + ":5000/auth/callback/cognito",
          cdnName + ":3000/auth/callback/cognito",
          'http://localhost:5173/auth/callback/cognito',
          'https://localhost:5173/auth/callback/cognito',
          'http://localhost:5000/auth/callback/cognito',
          'https://localhost:5000/auth/callback/cognito',
          'http://localhost:3000/auth/callback/cognito',
          'https://localhost:3000/auth/callback/cognito',
        ],
        flows: {
          authorizationCodeGrant: true,
        }
      },
    });
    
    

    //Can't use this because it creates a circular dependency, have to use the "UpdateEnvVarsLambda" as a workaround
    /*
    lambdaFunc.addEnvironment("COGNITO_USER_POOL_ID", userpool.userPoolId);
    lambdaFunc.addEnvironment("COGNITO_CLIENT_ID", appClient.userPoolClientId);
    lambdaFunc.addEnvironment("COGNITO_CLIENT_SECRET", appClient.userPoolClientSecret.unsafeUnwrap());
    lambdaFunc.addEnvironment("COGNITO_ISSUER", "https://cognito-idp." + this.region + ".amazonaws.com/" + userpool.userPoolId);
    lambdaFunc.addEnvironment("AUTH_TRUST_HOST", "true");
    lambdaFunc.addEnvironment("AUTH_SECRET", "12344567890");
    //lambdaFunc.addEnvironment("AUTH_SECRET", mySecret.secretValue.unsafeUnwrap()); //TODO: Bad practice, ctf app so not critical
    */

    // Define the Lambda function to update environment variables
    const updateEnvVarsLambda = new lambda.Function(this, 'UpdateEnvVarsLambda', {
      runtime: lambda.Runtime.NODEJS_18_X,
      handler: 'index.handler',
      code: lambda.Code.fromInline('exports.handler = async (event) => {}'),
    });

    // Grant necessary permissions to the updateEnvVarsLambda function
    updateEnvVarsLambda.addToRolePolicy(new aws_iam.PolicyStatement({
      actions: ['lambda:UpdateFunctionConfiguration'],
      resources: ['*'], // Replace with the ARN of the target Lambda function if more restrictive permissions are desired
    }));

    const environmentVariables: { [key: string]: string } = {
      "COGNITO_USER_POOL_ID": userpool.userPoolId,
      "COGNITO_CLIENT_ID": appClient.userPoolClientId,
      "COGNITO_CLIENT_SECRET": appClient.userPoolClientSecret.unsafeUnwrap(),
      "COGNITO_ISSUER": "https://cognito-idp." + this.region + ".amazonaws.com/" + userpool.userPoolId,
      "AUTH_TRUST_HOST": "true",
      "AUTH_SECRET": "<REDACTED", //just an example, not a production app.
      //"AUTH_SECRET": mySecret.secretValue.unsafeUnwrap(), //TODO: Bad practice: ctf app so not critical
    };

    // Define the CloudFormation custom resource to trigger the Lambda function
    const updateEnvVars = new custom_resources.AwsCustomResource(this, 'UpdateEnvVarsCustomResource', {
      onCreate: {
        service: 'Lambda',
        action: 'updateFunctionConfiguration',
        parameters: {
          FunctionName: lambdaFunc.functionName, // Replace with the name of the existing Lambda function
          Environment: {
            Variables: environmentVariables,
          },
        },
        physicalResourceId: custom_resources.PhysicalResourceId.of(Date.now().toString()),
      },
      onUpdate: {
        service: 'Lambda',
        action: 'updateFunctionConfiguration',
        parameters: {
          FunctionName: lambdaFunc.functionName, // Replace with the name of the existing Lambda function
          Environment: {
            Variables: environmentVariables,
          },
        },
        physicalResourceId: custom_resources.PhysicalResourceId.of(Date.now().toString()),
      },
      policy: custom_resources.AwsCustomResourcePolicy.fromSdkCalls({ resources: custom_resources.AwsCustomResourcePolicy.ANY_RESOURCE }),
    });
    
    updateEnvVars.node.addDependency(lambdaFunc);


    if (domainName) {
      new CfnOutput(this, 'Deployed URL', {
        description: 'Deployed URL',
        value: `https://${domainName}`
      })
    }

    new CfnOutput(this, 'CloudFront URL', {
      description: 'CloudFront URL',
      value: `https://${cdn.distributionDomainName}`
    })
  }
}

function generateRandomString(length: number): string {
  const charset = 'abcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  for (let i = 0; i < length; i++) {
    const randomIndex = Math.floor(Math.random() * charset.length);
    result += charset[randomIndex];
  }
  return result;
}

@hiddengearz
Copy link
Author

hiddengearz commented Mar 3, 2024

In hindsight for attempt 1 I'm pretty sure I was using the wrong names to retrieve the constructs from the cloudformation template, so there would be no need for findResourceName and it probably would of worked... but I think this shows the difference between the approaches.

@b9n2038
Copy link

b9n2038 commented Jun 24, 2024

I think you would want to extend via CDK. Have some way to allow the adapter to inject other stacks / files into the one cdk application or maybe allow specification of an alternative cdk app file that can import the Svelte Stack

Also there are easy ways to share resources across stacks, you can export and import resources from separate stacks using CfnOutput and Fn.ImportValue. If you haven't tried this before this works pretty well except for:

  1. Stack dependencies are painful if you want to refactor/rename exports as you can't rename an export if it is imported by another stack, you'll have to drop the dependency first;
  2. The interface values from the import (eg. IBucket vs. Bucket) have constraints and I sometimes need the concrete class, in which case I'm re-factoring constructs/stacks.

cdk deploy can output json with output values for use in other build process.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
need-opinion We need ideas from community
Projects
None yet
Development

No branches or pull requests

3 participants