Skip to content

Cloud Governance with CDK using Aspects

Manage Cloud Governance with AWS CDK, Aspects and Open Policy Agent

Nowadays with the established concepts of cloud computing, infrastructure as code, and automation; the volume and complexity of environments are increasing exponentially.

This landscape makes it necessary to implement a clear set of rules and policies regarding the lifecycle of cloud resources, otherwise known as Cloud Governance.

In this article, the implementation of a real-life set of tools will be discussed in order to provide Cloud Governance using AWS CDK and Open Policy Agent (OPA) You can follow the code used in this article by cloning the repo: https://github.com/nearform/cdk-aspects-opa-example

Principles of Cloud Governance

What is Cloud Governance?

  • Compliance with company policies and standards
  • Alignment with business objectives
  • Collaboration
  • Change management
  • Dynamic Response for events

Cloud Governance Pillars

  • Cloud Financial Management: Financial Policies, Budgets, Cost reporting
  • Cloud Operations Management: A clear definition of resources allocated to the service over time, Performance SLAs, Ongoing monitoring, Access control requirements
  • Cloud Data Management: Automation of data lifecycle management, Ensuring all data is encrypted, Developing a tiering strategy
  • Cloud Security and Compliance Management: Risk assessment, Identity and access management, Data management and encryption, Application security

More information on good Cloud Governance Framework can be found at Imperva: https://www.imperva.com/learn/data-security/cloud-governance/

What’s CDK?

AWS CDK lets you build reliable, scalable, cost-effective applications in the cloud with the considerable expressive power of a programming language”

For building these examples you will need a working AWS Account and the AWS CDK CLI installed with the appropriate permissions on your AWS Account.

A simple CDK stack

This snippet shows a complete declaration of a stack containing an S3 bucket without encryption:

JavaScript
// => lib/simple-stack.ts
import { aws_s3, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
 
export class SimpleStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    new InsecureBucket(this, "MyBucket");
  }
}
export class InsecureBucket extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);  
    const insecureBucket = new aws_s3.Bucket(this, "InsecureBucket", {
      encryption: undefined
    });
  }
}
// => bin/simple.ts
import { App } from 'aws-cdk-lib';
import { SecureStack } from '../lib/secure-stack';
 
const app = new App();
const simpleStack = new SimpleStack(app, 'SimpleStack', {});

CDK Aspects

Aspects are a way to change constructs in a given scope based on any characteristics of that node.  CDK Aspects implements the Visitor pattern using the interface IAspect .

Workflow using Aspects

When using CDK Aspects, before the changes have been applied in the AWS environment, all registered aspects on a stack are visited and you can implement validations and make changes in nodes. You also have access to a complete tree of current node (stack, resources, etc). In this aspect, you can also insert information or error annotations in nodes.

A policy to block the creation of buckets without encryption

In this example we can see the declaration of a class that implements the IAspect interface. The visit method verifies if the node is an S3 Bucket with encryption. If the answer is negative, an error annotation is included in the node:

JavaScript
// => lib/s3-checker.ts
export class BucketEncryptionChecker implements cdk.IAspect {
  public visit(node: IConstruct): void {
    if (node instanceof cdk.aws_s3.CfnBucket && !node.bucketEncryption){    
      Annotations.of(node).addError('Bucket encryption is not enabled');
    }
  }
}
// => lib/simple-test.ts
test('S3 Bucket Not Created Without Encryption', () => {
  const app = new cdk.App();
   // WHEN
  let stack = new Simple.SimpleStack(app, 'MyTestStack');
  cdk.Aspects.of(stack).add(new BucketEncryptionChecker());
    // THEN 
  Annotations.fromStack(stack).hasError('/MyTestStack/MyBucket/InsecureBucket/Resource', 'AWS::S3::Bucket::NoEncryption');
});

A rich example using OPA

When we talk about dozens of policies and their validation, in some contexts it can turn the codebases into a mess and very hard to maintain and deal with. In this case the Open Policy Agent or just OPA can help us!

Policy-based control for cloud native environments

OPA brings us stable, simple and flexible fine-grained control for all aspects of elements in a stack. With this feature you can decouple your Cloud Governance policies and rules from your services and maintain a concise base for those without losing performance or availability.

OPA Architecture

You can use OPA as Daemon receiving inputs from an HTTP API or as a library. You can build and distribute compiled policies in webassembly to be used as well.

Image extracted from: https://www.openpolicyagent.org

Workflow using Aspects + OPA Policies

A workflow with an Aspect calling OPA to validate the node through one or more policies:

An example of Financial Policy

This is an example of a policy that just returns allow: true when the input object has the properties active and hasBudget filled with “yes”:

Plain Text
# => test/policies/financial.rego
package policy.financial
default allow = false
allow {
   input.tags.active == "yes"
   input.tags.hasBudget == "yes"
}
Controlling Resources Life Cycle

Let's imagine that we want to enforce that the applied stacks have an explicit repository tag and the current Error Budget of the project is greater than 0.  We can do that by declaring a policy like this:

Plain Text
# => test/policies/change.rego
package policy.change
import future.keywords.in
default allow = false
allow { 
   some repoTag in input.stackMetadata
   repoTag.type == "repoTag"
   repoTag.data != ""
 
   some errorBudget in input.stackMetadata
   errorBudget.type == "errorBudget"
   errorBudget.data > 0
}

An OPA Checker that implements IAspect

In this implementation, we create a data object with information about node/resource and submit that to OPA. If the response is different than { result: { allow: true } } an error annotation is created blocking the process of deployment:

JavaScript
// => lib/opa-checker.ts
export class OpaChecker implements IAspect {
 
  private id = ''
  private opClient: IOpaClient
  constructor(id: string, opClient: IOpaClient){
    this.id = id
    this.opClient = opClient
  }
 
  public visit(node: IConstruct): void {
    const data = this.buildData(node)
 
    this.loadTagsData(node, data)
    this.loadCnfResourceData(node, data)
 
    const opaResult = this.opClient.submit(
      {
        input: data
      }
    )
 
    if(opaResult.error){
      Annotations.of(node).addError(`OpaChecker::${this.id}::${opaResult.errorMsg}`);
    }
 
    if(opaResult.result.allow){
      Annotations.of(node).addInfo(`OpaChecker::${this.id}::Allowed`);
    }else{
      Annotations.of(node).addError(`OpaChecker::${this.id}::NotAllowed`);
    }
  }
  // ...methods omitted

Bringing it all together

Here is a complete implementation of a stack that has three Aspect Checkers:

  • BucketEncryptionChecker
  • OpaChecker for Change Policy
  • OpaChecker for Financial Policy
JavaScript
// => bin/simple.ts
const app = new App();
const stack = new SecureStack(app, 'SimpleStack', {});
 
Tags.of(stack).add("projectId", "my-project")
Tags.of(stack).add('active', 'yes')
Tags.of(stack).add('hasBudget', 'yes')
 
stack.node.addMetadata("repoTag", "v0.0.0")
stack.node.addMetadata("errorBudget", 0.1)
 
Aspects.of(stack).add(new BucketEncryptionChecker());
 
const opaServerEndpoing = "http://localhost:8181/v1/data"
const policyChangeClient = new DefaultOpaClient(`${opaServerEndpoing}/policy/change`)
const policyFinancialClient = new DefaultOpaClient(`${opaServerEndpoing}/policy/financial`)
 
Aspects.of(stack).add(new OpaChecker('PolicyChange', policyChangeClient))
Aspects.of(stack).add(new OpaChecker('PolicyFinancial', policyFinancialClient))
 
// => generated payload that will be sent to OPA
{
   "input": {
       "id": "Resource",
       "addr": "c8400522ef47ac4b17b2337c6266232ef0f0bec571",
       "path": "SimpleStack/MyBucket/SecureBucket/Resource",
       "type": "AWS::S3::Bucket",
       "stackMetadata": [
           {
               "type": "repoTag",
               "data": "v0.0.0"
           },
           {
               "type": "errorBudget",
               "data": 0.1
           }
       ],
       "isStack": false,
       "tags": {
           "active": "yes",
           "hasBudget": "yes",
           "projectId": "my-project"
       }
   }
}

Conclusion

Aspects is an interesting way to control DevOps flows based on project characteristics and business rules. The combination of this tool with OPA brings us flexibility and power to implement Cloud Governance rules and policies in a clear and centralized way, and it’s pretty good!

Insight, imagination and expertly engineered solutions to accelerate and sustain progress.

Contact