Infrastructure as Code (IaC) has become a standard in cloud development, allowing for quick environment setups and compliance through versioning. Tools like Terraform and the Cloud Development Kit (CDK) simplify the process compared to traditional CloudFormation.
In a previous blog post, I discussed using a custom solution for pull request reporting with cfn-lint and cfn_nag. While cfn-lint is still valuable for creating your own compliance rules, keeping up with CDK and AWS recommendations can be challenging. Fortunately, cdk-nag can serve as a substitute for cfn-lint.
CDK-Nag: Making Infrastructure Decisions Visible
Most tools I've used have a common issue: they can check your infrastructure but often fall short on providing actionable insights. cdk-nag enhances visibility into the compliant aspects of your IaC deployment. It comes with prebuilt rule packs for various standards:
- AWS Solutions
 - HIPAA Security
 - NIST 800-53 Rev 4
 - NIST 800-53 Rev 5
 - PCI DSS 3.2.1
 
These rules are mapped to specific controls, making compliance evaluations straightforward, even for non-AWS exployees.
Sample Implementation of CDK-Nag in Python
For demonstration, we'll use a sample CDK application created with the command cdk init app --language python. If you're new to CDK, check out the official tutorial.
Here's a minimal example of creating an S3 bucket in your CDK stack:
from aws_cdk import Stack, aws_s3 as s3
from constructs import Construct
from src.load_env.config import CDKConfig
class CdkSampleRepoStack(Stack):
    """Create the actual deployment in each AWS account."""
    def __init__(self, scope: Construct, construct_id: str, stage: str, config: CDKConfig, **kwargs) -> None:
        """Initialize CDK stack class."""
        super().__init__(scope, construct_id, **kwargs)
        # Create the S3 bucket
        bucket = s3.Bucket(self, id="MyBucket")
After running cdk synth, you'll generate a CloudFormation template in the cdk.out folder. However, this bucket configuration may have implicit or missing settings, such as encryption or TLS policies.
To leverage AWS best practices, add cdk-nag to your app.py:
#!/usr/bin/env python3
import os
import cdk_nag
import aws_cdk as cdk
from hello_cdk.hello_cdk_stack import CdkSampleRepoStack
app = cdk.App()
CdkSampleRepoStack(app, "HelloCdkStack", env=cdk.Environment(account='123456789012', region='us-east-1'))
cdk_nag.Aspects.of(app).add(cdk_nag.AwsSolutionsChecks(verbose=True))
app.synth()
Running cdk synth now will display error messages in your terminal, such as:
[Error at /CdkSampleRepoStack/MyBucket/Resource] AwsSolutions-S1: The S3 Bucket has server access logs disabled.
[Error at /CdkSampleRepoStack/MyBucket/Resource] AwsSolutions-S10: The S3 Bucket or bucket policy does not require requests to use SSL.
Handling Best Practice Violations
Sometimes, you may need to explicitly violate a best practice, like disabling access logging for a non-critical bucket. You can suppress specific rules as follows:
# hello_cdk.hello_cdk_stack.py
bucket = s3.Bucket(
    self,
    id="MyBucket",
    block_public_access=s3.BlockPublicAccess(
        block_public_acls=True,
        block_public_policy=True,
        ignore_public_acls=True,
        restrict_public_buckets=True,
    ),
    versioned=True,
    enforce_ssl=True,
    encryption=s3.BucketEncryption.KMS_MANAGED,
)
nag_suppression.add_resource_suppressions(
    construct=bucket,
    suppressions=[
        {
            "id": "AwsSolutions-S1",
            "reason": "This bucket does not hold customer data",
        }
    ],
)
With this suppression, the CSV report will now indicate that the rule has been suppressed. But, cdk-nag not supports suppressions on constructs but also on resource paths and many more
Integrating Pull Request Reporting with cdk-nag
Integrating cdk-nag into your CI/CD pipeline can enhance your deployment process. For instance, in Azure DevOps, you can automate pull request comments based on the cdk synth results.
Here's a brief overview of how to set this up:
- Create a message class to handle comments.
 - Use pandas to read the generated CSV files.
 - Translate the CSV table to Markdown.
 - Post comments to your pull requests.
 
import pull_request_comment
import logging
import pandas as pd
import os
logging.basicConfig(
    level=logging.INFO, format="[%(levelname)s] - %(message)s", force=True
)
def main():
    """Create a Pull Request comment within azure-pipelines-pr.yml"."""
    for filename in os.listdir("./synth/templates/"):
        if filename.endswith(".csv"):
            try:
                df = pd.read_csv(f"./cdk.out/templates/{filename}")
            except FileNotFoundError as e:
                logging.exception(e)
                raise
            if df.empty is False:
                logging.info("Adding CDK Validation report")
                result = msg.add(comment=df.to_markdown())
if __name__ == "__main__":
    main()
In your Azure DevOps pipeline, add a task to run cdk synth and then execute the script to post comments:
- task: AWSShellScript@1
  inputs:
    awsCredentials: ${{ parameters.ServiceConnection }}
    regionName: ${{ parameters.AwsRegion }}
    scriptType: 'inline'
    inlineScript: |
      cdk synth
      python3 src/pull_requests/comment.py
Final Thoughts
Implementing cdk-nag in your workflow may require some effort, but it provides an easier way to ensure compliance before deploying to AWS. Shifting left in your development process is always a good idea, especially for beginners looking to ensure correct deployments.
Happy and save coding! :-)

    
Top comments (2)
I haven't gotten around to actually integrating cdk-nag yet, it seems like the barrier to entry is actually quite low.
I should really give it a try!
Thanks, Malte!
Yes, the integration effort is quite low. Taking care of the rule treatment can be tricky; especially with "hidden" CDK ressources like Inline Policies.