Skip to content

AWS cdk deploy deletes the resources which is already created

0

am trying to create a stack which contains API Gateway and few lambdas based on a config.json file

When I Give CDK Deploy for the first time the stack and the resources are created successfully, but if I change the lambda code and give the cdk deploy it deletes the existing resources created with in the stack

below is the code what I have tried

**infra-config.json **

{
    "stackname":"hellostack",
    "apigateway":"hello_apigateway",
    "lambda":[
        {
            "lambdaName": "getresource",
            "lambdaPath": "../../lambda/get_resources",
            "endpoint": "getresources",
            "handler":"handler",
            "memorySize": 128,
            "method": "GET",
            "module":"ResourceManagement"
        },
        {
            "lambdaName": "updateclientmanaged",
            "lambdaPath": "../../lambda/update_client_managed",
            "endpoint": "updateclimngd",
            "handler":"handler",
            "memorySize": 256,
            "method": "PATCH",
            "module":"DealReview"
        }
    ] }

**bin/infra.ts **

    const account = '123456789';
const region = 'us-east-2';
const config: Config = getInfraConfig()

const app = new cdk.App();
const stackName = config.stackname;


// Main function to run cleanup and then deploy the stack
async function main() {
    try {
        const stack = new LambdaApiStack(app, stackName, {
            lambdaConfig: config,
            env: { account, region },

        });
        console.log(`Bundling process completed successfully for stack: ${stackName}`);
    } catch (error: any) {
        console.error(`Error during bundling process for stack: ${stackName}`, error);
        process.exit(1); // Terminate the process with an error code
    }
}

main();

**lib/infra-stack.ts **

interface LambdaApiStackProps extends cdk.StackProps {
  lambdaConfig: Config;
}
export class LambdaApiStack extends cdk.Stack {
  private apiEnv: string = 'dev'
  private apiEnvRegion: string = 'us-east-2'
  private apiSecretVersion: string = 'TESTVERSION'

  private readonly environmentVariables: any = {
    WHUB_AWS_REGION: this.apiEnvRegion,
    WHUB_ENV_INSTANCE: this.apiEnv,
    WHUB_AWS_SECRET_VERSION: this.apiSecretVersion
  };
  constructor(scope: Construct, id: string, props: LambdaApiStackProps) {
    super(scope, id, props);
    const config: Config = props?.lambdaConfig;
    console.log(`config ${JSON.stringify(config)}`)
    const apiGatewayClient = new APIGatewayClient({});
    this.checkAndCreateApiGateway(apiGatewayClient, props?.lambdaConfig);
  }


  async checkAndCreateApiGateway(apiGatewayClient: APIGatewayClient, config: Config) {
    // Check if the API Gateway exists
    const apis = await apiGatewayClient.send(new GetRestApisCommand({}));
    const existingApi = apis.items?.find((api: any) => api.name === config.apigateway);

    let api;
    if (existingApi) {
      console.log(`existingApi ${JSON.stringify(existingApi)}`)

      api = apigateway.RestApi.fromRestApiAttributes(this, config.apigateway, {
        restApiId: existingApi.id!,
        rootResourceId: existingApi.rootResourceId!
      });
    } else {
      console.log(`inside Create the API Gateway`)

      // Create the API Gateway
      api = new apigateway.RestApi(this, config.apigateway, {
        restApiName: config.apigateway,
        deploy: false
      });
      console.log(`New APIGateway ${api.restApiId}`)

      const existingStage = api.deploymentStage;

      if (!existingStage || existingStage.stageName.toLowerCase() !== 'prod') {
        console.log(`existingStage ${existingStage}`)

        const deployment = new apigateway.Deployment(this, `${config.apigateway}-${this.apiEnv}-deploy`, {
          api,
        });

        new apigateway.Stage(this, `${config.apigateway}-${this.apiEnv}`, {
          deployment
        });
      }
      const cfnApi = api.node.defaultChild as apigateway.CfnRestApi;
      cfnApi.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);
    }

    for (const lambdaConfig of config.lambda) {
      await this.processLambdaConfig(api, lambdaConfig);
    }
  }

  async processLambdaConfig(api: apigateway.IRestApi, lambdaConfig: LambdaConfig) {
    console.log(`inside processLambdaConfig`)
    const lambdaAssetPath = path.join(__dirname, lambdaConfig.lambdaPath + '\\index.ts');
    const packageLockPath = path.join(__dirname, '../../', '\\package-lock.json');
    const lambdaClient = new LambdaClient({ region: this.apiEnvRegion });

    const lambdaName = `${this.apiEnv}_${lambdaConfig.lambdaName}`;
    console.log(`packageLockPath ${packageLockPath}`)

    console.log(`lambdaAssetPath ${lambdaAssetPath}`)
    if (!fs.existsSync(lambdaAssetPath)) {
      throw new Error(`Lambda asset path does not exist: ${lambdaAssetPath}`);
    }

    let lambdaRole = this.node.tryFindChild('dev-testexecrole-wa-hubb') as iam.IRole;
    if (!lambdaRole) {
      lambdaRole = iam.Role.fromRoleArn(this, `dev-testexecrole-wa-hubb`, `arn:aws:iam::1234567889:role/dev-testexecrole-wa-hubb`);

    }
    console.log(`Before lambdaExist`)
    try {
      await lambdaClient.send(new GetFunctionCommand({ FunctionName: lambdaName }));
      await lambdaClient.send(new UpdateFunctionConfigurationCommand({
        FunctionName: lambdaName,
        MemorySize: lambdaConfig.memorySize,
        Handler: lambdaConfig.handler,
        Environment: {
          Variables: this.environmentVariables
        }
      }));

      await lambdaClient.send(new UpdateFunctionCodeCommand({
        FunctionName: lambdaName,
        ZipFile: fs.readFileSync(lambdaAssetPath)
      }));

    }
    catch (err: any) {
      if (err.name === 'ResourceNotFoundException') {
        console.log(`After lambdaExist`)

        const lambdaFunction = new NodejsFunction(this, `${this.apiEnv}_${lambdaConfig.lambdaName}`, {
          functionName: `${this.apiEnv}_${lambdaConfig.lambdaName}`,
          memorySize: lambdaConfig.memorySize,
          timeout: cdk.Duration.minutes(2),
          runtime: lambda.Runtime.NODEJS_20_X,
          handler: lambdaConfig.handler,
          role: lambdaRole,
          entry: lambdaAssetPath,
          bundling: {
            externalModules: ['aws-lambda'],
            nodeModules: ['pg']
          },
          depsLockFilePath: packageLockPath,
          environment: this.environmentVariables
        });


        const integration = new apigateway.LambdaIntegration(lambdaFunction, {
          requestTemplates: { 'application/json': '{ "statusCode": "200" }' },
        });
        let rootResource = api.root.getResource(this.apiEnv);
        if (!rootResource) {
          rootResource = api.root.addResource(this.apiEnv);
          console.log("Root Res Created", rootResource)
        }
        let moduleResource = rootResource.getResource(`${lambdaConfig.module}`);
        if (!moduleResource) {
          moduleResource = rootResource.addResource(`${lambdaConfig.module}`);
          console.log("Module Res Created", moduleResource)
        }
        let resource = moduleResource.getResource(lambdaConfig.endpoint);
        if (!resource) {
          resource = moduleResource.addResource(lambdaConfig.endpoint);
          resource.addMethod(lambdaConfig.method, integration);
          console.log("Resource Created", resource)
        }
        else {
          resource.addMethod(lambdaConfig.method, integration);
          console.log("Resource Already Exist")
        }

        lambdaFunction.grantInvoke(new iam.ServicePrincipal('apigateway.amazonaws.com'));
        const cfnLambdaFunction = lambdaFunction.node.defaultChild as CfnResource;
        cfnLambdaFunction.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);

      }
    }
  }
}
2 Answers
1
Accepted Answer

The reason resources are being deleted is because you are checking if the resources exist and only instantiating them in your CDK application if they do not. This an imperative method whereas CloudFormation is declarative. When you do not declare the resources, the resulting CloudFormation template will not contain the definition of the resources and CloudFormation will delete the resources. You can override this by applying the RemovalPolicy.RETAIN, but you have only done so for the Lambda function and the API Gateway. All the other resources such as the API Gateway deployment, stage, resources do not have the removal policy applied to them so they will be deleted.

There are a lot of direct AWS SDK calls being made in your example. Unless you have a specific use case to be doing this, you generally should not be checking for the existence of resources within your CDK application or updating with directly AWS SDK. You should simply declare the resources and configuration you want to deploy and CloudFormation handles creating new resources or updating existing ones to match the desired configuration.

Here is a rough draft attempt at rewriting your lib/infra-stack.ts file. This is not fully validated or tested, so you should validate that the code runs and is suitable for your use case and environment. This intended just a start to give you an idea of how to work with the declarative method:

async createApiGateway(apiGatewayClient: APIGatewayClient, config: Config) {
  // Create the API Gateway
  const api = new apigateway.RestApi(this, config.apigateway, {
    restApiName: config.apigateway,
    deploy: false
  });
  console.log(`New APIGateway ${api.restApiId}`)

  const deployment = new apigateway.Deployment(this, `${config.apigateway}-${this.apiEnv}-deploy`, {
    api,
  });

  new apigateway.Stage(this, `${config.apigateway}-${this.apiEnv}`, {
    deployment
  });
  const cfnApi = api.node.defaultChild as apigateway.CfnRestApi;
  cfnApi.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);

  for (const lambdaConfig of config.lambda) {
    await this.processLambdaConfig(api, lambdaConfig);
  }
}

async processLambdaConfig(api: apigateway.IRestApi, lambdaConfig: LambdaConfig) {
  console.log(`inside processLambdaConfig`)
  const lambdaAssetPath = path.join(__dirname, lambdaConfig.lambdaPath + '\\index.ts');
  const packageLockPath = path.join(__dirname, '../../', '\\package-lock.json');

  const lambdaName = `${this.apiEnv}_${lambdaConfig.lambdaName}`;
  console.log(`packageLockPath ${packageLockPath}`)

  console.log(`lambdaAssetPath ${lambdaAssetPath}`)
  if (!fs.existsSync(lambdaAssetPath)) {
    throw new Error(`Lambda asset path does not exist: ${lambdaAssetPath}`);
  }

  const lambdaRole = iam.Role.fromRoleArn(this, `dev-testexecrole-wa-hubb`, `arn:aws:iam::1234567889:role/dev-testexecrole-wa-hubb`);

  const lambdaFunction = new NodejsFunction(this, `${this.apiEnv}_${lambdaConfig.lambdaName}`, {
    functionName: `${this.apiEnv}_${lambdaConfig.lambdaName}`,
    memorySize: lambdaConfig.memorySize,
    timeout: cdk.Duration.minutes(2),
    runtime: lambda.Runtime.NODEJS_20_X,
    handler: lambdaConfig.handler,
    role: lambdaRole,
    entry: lambdaAssetPath,
    bundling: {
      externalModules: ['aws-lambda'],
      nodeModules: ['pg']
    },
    depsLockFilePath: packageLockPath,
    environment: this.environmentVariables
  });

  const integration = new apigateway.LambdaIntegration(lambdaFunction, {
    requestTemplates: { 'application/json': '{ "statusCode": "200" }' },
  });

  const rootResource = api.root.addResource(this.apiEnv);
  const moduleResource = rootResource.addResource(`${lambdaConfig.module}`);

  const resource = moduleResource.addResource(lambdaConfig.endpoint);
  resource.addMethod(lambdaConfig.method, integration);
  console.log("Resource Created", resource)

  lambdaFunction.grantInvoke(new iam.ServicePrincipal('apigateway.amazonaws.com'));
}
AWS

answered 2 years ago

EXPERT

reviewed a year ago

0

To update the resources in-place instead of replacing them, you need to use the aws_cdk.RemovalPolicy.RETAIN policy for the resources you want to update without replacement.

In your code, you are already applying the RemovalPolicy.RETAIN for the CfnRestApi resource, which might prevent the deletion of the API Gateway during updates. However, you need to apply the same policy for the Lambda functions as well.

In the processLambdaConfig method, after creating the lambdaFunction instance, you can apply the RemovalPolicy.RETAIN policy to the underlying CfnResource of the Lambda function:

const cfnLambdaFunction = lambdaFunction.node.defaultChild as CfnResource;
cfnLambdaFunction.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN);

f you're updating the Lambda function code, you can use the aws_lambda.Code.fromAsset method instead of fs.readFileSync to package and upload the new code. This ensures that the Lambda function is updated with the new code without replacing the entire function:

const lambdaCode = lambda.Code.fromAsset(lambdaConfig.lambdaPath, {
  bundling: {
    externalModules: ['aws-lambda'],
    nodeModules: ['pg'],
  },
});

await lambdaClient.send(new UpdateFunctionCodeCommand({
  FunctionName: lambdaName,
  CodeUri: lambdaCode.location,
  Publish: true, // Publish a new version of the Lambda function
}));

By applying the RemovalPolicy.RETAIN policy and using the appropriate methods to update the Lambda function code, you should be able to update your Lambda functions without deleting and recreating them during subsequent deployments.

answered 2 years ago

  • does lambda.Code.fromAsset take care of compiling the ts to js file ?

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.

Relevant content