globaldatanetmenu

.Enhancing Your AWS CDK Projects with Testing

Oct 11th 2024-6 min read

Automated unit and integration testing validates system components and increases confidence in your infrastructure code, but unfortunately this is a very neglected topic at CDK. But let's start at the beginning. A few weeks ago, we decided to start a new version of a CDK construct library in a project - this meant taking our CDK testing to a new level. We wanted to test each custom construct with multiple unit and integration tests. Here is an explanation of the difference between a unit test and an integration test.

Unit testing in the CDK focuses on testing the logic within CDK constructs, which are reusable components that define and configure cloud resources. These tests simply synthesized CDK code to a CloudFormation template without actually deploying any AWS resources. In short, AWS services are simply mocked up and we compare the generated template against our checks.

Integration testing, on the other hand, deploys the actual resources to an AWS account to ensure that the deployed resources work together correctly. These tests verify the behavior of the entire AWS application, including permissions, networking, and service integrations. Unlike unit testing, integration testing requires a real AWS account and uses tools such as the AWS SDK, AWS CLI, and custom resources to verify functionality in a real-world environment. We use the integ-test module - part of the CDK library - for testing in CDK.

Blog Content

Since we're just starting out with this new topic and wanted to see how other people are testing their infrastructure, we decided to do some research. Unfortunately there's a lack of good examples on the web and the same example is copied across many blog posts on the web. đź« 

This situation is over now. After troubleshooting, testing, failing and gaining experience. We are now in a position to share our experiences and examples with you. 🙌🏻

Unit Testing

No, you don’t need to test every line of your CDK application - Yan Cui

I see it the same way as Yan Cui, if you only declare and use official CDK constructs, we don't necessarily have to test, I would only test to ensure compliance (security and regulatory compliance), because as in the shift left principle, we can prevent this before we deploy insecure resources by CDK testing. However, this should not be used as an alternative just as an addition to check if you comply with security and regulatory frameworks in your AWS environments. You should still use config and/or conformance packs that are rolled out organization-wide to check AWS resources against your compliance requirements. But now lets start with an easy example. Since Security is everyones job we should test if all RDS Instances will be encrypted. This can be easily done like that.

  template.allResourcesProperties("AWS::RDS::DBInstance", {
    StorageEncrypted: true,
  });
  

In addition, we should use a custom KMS key to encrypt the RDS Intention and this key should be rotated regularly.

// KMS Key rotation Check
  template.allResourcesProperties("AWS::KMS::Key", {
    EnableKeyRotation: true
  });


//RDS Instance Check
template.allResourcesProperties("AWS::RDS::DBInstance", {
  StorageEncrypted: true,
  KmsKeyId: Match.anyValue(),
});

Unit tests like this could be implemented in any CDK project when using tools like projen - or whenever you create your own custom construct library.

Imagine you have a platform team and a lot of application development teams, and you want to give them easily deployable infrastructure code that meets your company's security and regulatory frameworks, and includes resources that are commonly used for almost every application.

Now let's take it a step further, we want to provide our application teams with constructs that expose databases or other resources according to our corporate specifications and legal requirements. this often requires custom resources. to find these in the synthesized stack, we need a little more configuration to specify the resources and check that the references are set correctly. In the following example, we search for a KMS key using the properties in the synthesised template and check that the reference is set correctly in the following secret.

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    function logicalIdFromResource(resource: any) {
      try {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
        const resKeys = Object.keys(resource);
        if (resKeys.length === 0) {
          throw new Error("No Resource found.");
        }
        if (resKeys.length !== 1) {
          throw new Error("Resource is not unique.");
        }
        const [logicalId] = resKeys;
        return logicalId;
      } catch (err) {
        console.log(resource);
        throw err;
      }
    }

    const testKmsKey = template.findResources("AWS::KMS::Key", {
      Properties: {"Description": "DescriptionTestKey"}
    });


    const testKmsKeyLogicalId = logicalIdFromResource(testKmsKey);

    template.findResources("AWS::SecretsManager::Secret", {
      Properties: {
        "Name": "test",
        "KmsKeyId": {
          "Fn::GetAtt": [
            `${testKmsKeyLogicalId}`,
            "Arn"
          ]
        },}});

Integration Testing

As mentioned at the beginning, integration tests are concerned with deploying resources from a CDK application to a test environment to test the application from start to finish. These tests can take a little more time than a unit test, but they also ensure that the resources are used as intended. Integration tests allow you to use different assertions to check resources for properties or settings.

For example, there is an awsApiCall that can be executed to check whether certain values are in a resource. Here is an example where we check a secret content.

// Check whether the secret was created with the correct content
const assertion = integTest.assertions.awsApiCall("secrets-manager", "GetSecretValue", {
  SecretId: SECRETARN,
}, ["SecretString"]);
assertion.provider.addToRolePolicy({
  Effect: "Allow",
  Action: ["kms:Decrypt*"],
  // reference errors when using the key. 
  Resource: ["*"],
});
assertion.assertAtPath("SecretString.username", ExpectedResult.stringLikeRegexp("test"));
assertion.assertAtPath("SecretString.password", ExpectedResult.stringLikeRegexp(".*?"));

With this assertion we use a nice Feature, instead of checking the whole response object at the end, we use "assertAtPath" to get back only parts of the secret string to do a regex on it.

During the integration test of a CDK app, two stacks are deployed, one stack containing the resources and another stack containing the testing resources. CloudFormation Outputs can be used to pass arns or other variables between the stacks, for example to read a specific secret. This would look as follows in the example.

 
new cdk.CfnOutput(testStack, "AdminSecretArn", {
  value: adminSecret.secretArn,
  exportName: testStack.stackName+"-AdminSecretArn"
});


const SECRETARN = cdk.Fn.importValue(testStack.stackName+"-AdminSecretArn");

In addition to the assertions for “awsApiCalls”, there is also the “invokeFunction”. This triggers an AWS Lambda Function that you can define yourself within the assertionStack. Using a lambda function you can execute far more complex tests which return a result at the end which can be checked using the CDK testing framework. For example, if you do not know exactly how long the function will take or when an object will end up in an s3 bucket, you can use a "waitForAssertions" method to define at what interval and for how long a lambda function should be triggered.

// Invoke the assertion Lambda function to validate the received payload in s3
integTest.assertions.invokeFunction({
  functionName: assertionLambda.functionName,
}).expect(ExpectedResult.objectLike({
  Payload: "200"
})).waitForAssertions({
  totalTimeout: Duration.minutes(5),
  interval: Duration.seconds(30),
});

References

Conclusion

Automated unit and integration testing is critical for validating CDK constructs and ensuring the security and compliance of infrastructure code. By improving CDK testing-from unit tests that validate synthesized CloudFormation templates to integration tests that deploy and verify real AWS resources-we can build robust and secure cloud architectures. However, there are still challenges to overcome as the test constructs are still in alpha.

We hope that our insights into testing the CDK have helped you and made it easier for you to get started.

globaldatanetCloud Development, Optimization & Automation

.Navigation

.Social

  • follow globaldatanet on instagram
  • follow globaldatanet on facebook
  • follow globaldatanet on twitter
  • follow globaldatanet on linkendin
  • follow globaldatanet on twitch
  •  listen to our serverless world podcast
  • follow globaldatanet's tech rss feed
  • follow globaldatanet at github
© 2024 by globaldatanet. All Right Reserved
Your privacy is important to us!

We use cookies on our website. Some of them are essential,while others help us to improve our online offer.
You can find more information in our Privacy policy