Introduction
As organizations scale their AWS infrastructure, maintaining consistent security, compliance, and best practices across multiple CDK projects becomes challenging. Each team might implement their own governance rules, leading to inconsistencies, security gaps, and compliance violations.
The solution? Build a reusable governance library that can be shared across all your CDK projects.
In this article, I'll show you how to build a production-ready AWS governance library using three powerful CDK concepts:
- Constructs - For organizational structure
- Blueprints - For property injection (enforcing defaults)
- Aspects - For validation and compliance checking
Why a Governance Library?
Before diving into implementation, let's understand the problem:
Without a Governance Library
// Project A const bucket = new s3.Bucket(this, 'Bucket', { encryption: s3.BucketEncryption.S3_MANAGED, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, // ... repeated security config }); // Project B (different team) const bucket = new s3.Bucket(this, 'Bucket', { // Oops! Forgot encryption, no public access block }); // Project C const bucket = new s3.Bucket(this, 'Bucket', { encryption: s3.BucketEncryption.KMS, // Inconsistent encryption method }); Problems:
- ❌ Inconsistent security configurations
- ❌ Copy-paste errors
- ❌ No centralized compliance enforcement
- ❌ Difficult to audit
- ❌ Hard to update org-wide
With a Governance Library
// Just one line - consistent governance across all projects CommonGovernancePatterns.soc2(app, 'myorg', 'my-project', 'production'); // All S3 buckets automatically get: // ✅ Encryption enabled // ✅ Public access blocked // ✅ SSL enforced // ✅ Versioning enabled (production) // ✅ Organizational tags applied Architecture Overview
Our governance library uses a three-layer architecture:
┌─────────────────────────────────────┐ │ GovernanceFactory (Entry Point) │ │ - applyToApp() │ │ - applyToStack() │ └──────────────┬──────────────────────┘ │ ┌───────┴────────┐ │ │ ▼ ▼ ┌─────────────┐ ┌──────────────┐ │ Blueprints │ │ Aspects │ │ (Inject) │ │ (Validate) │ └─────────────┘ └──────────────┘ │ │ └────────┬───────┘ ▼ ┌──────────────────────┐ │ Your CDK Resources │ │ (S3, IAM, RDS...) │ └──────────────────────┘ Core Concepts Explained
1. Constructs
Constructs are CDK's building blocks. In our governance library, we use Constructs to organize and apply tags.
export class OrganizationalTags extends Construct { constructor(scope: Construct, id: string, private readonly config: GovernanceConfig) { super(scope, id); // Apply tags at scope level - they inherit to all children Tags.of(scope).add('Organization', config.organization); Tags.of(scope).add('Project', config.project); Tags.of(scope).add('Environment', config.environment); Tags.of(scope).add('ManagedBy', 'CDK'); Tags.of(scope).add('CostCenter', `${config.organization}-${config.project}-${config.environment}`); // Apply additional organizational tags if (config.organizationalTags) { Object.entries(config.organizationalTags).forEach(([key, value]) => { Tags.of(scope).add(key, value); }); } } } Key Points:
- Constructs organize code into logical units
- Tags applied at App level propagate to all stacks and resources
- Single source of truth for organizational metadata
2. Blueprints (Property Injection)
Blueprints use CDK's Property Injection feature to enforce defaults before resources are created. They implement the IPropertyInjector interface.
How Property Injection Works
When you create a resource like new s3.Bucket(this, 'MyBucket', props), CDK:
- Takes your
props - Passes them through all registered property injectors
- Injectors modify the props
- Creates the resource with modified props
Example: S3 Security Blueprint
export class SecureS3Blueprint implements IPropertyInjector { public readonly constructUniqueId: string; constructor(private readonly config: GovernanceConfig) { // This ID tells CDK which construct type to inject into this.constructUniqueId = Bucket.PROPERTY_INJECTION_ID; } public inject(originalProps: BucketProps, context: InjectionContext): BucketProps { const enforcedProps: Partial<BucketProps> = {}; // Block public access (override user's setting if needed) if (this.config.security?.blockPublicAccess !== false) { enforcedProps.blockPublicAccess = BlockPublicAccess.BLOCK_ALL; // Warn if we're overriding if (originalProps.blockPublicAccess !== BlockPublicAccess.BLOCK_ALL) { Annotations.of(context.scope).addWarning( `S3 bucket public access setting overridden for security compliance` ); } } // Enforce SSL if (this.config.security?.enforceSSL !== false) { enforcedProps.enforceSSL = true; } // Enforce encryption if (this.config.security?.enforceEncryption !== false) { enforcedProps.encryption = BucketEncryption.S3_MANAGED; } // Enable versioning in production if (this.config.environment === 'production' && this.config.security?.enableVersioning !== false) { enforcedProps.versioned = true; } // Merge original props with enforced props return { ...originalProps, ...enforcedProps, }; } } Example: IAM Role Blueprint
export class SecureIAMRoleBlueprint implements IPropertyInjector { public readonly constructUniqueId: string; constructor(private readonly config: GovernanceConfig) { this.constructUniqueId = Role.PROPERTY_INJECTION_ID; } public inject(originalProps: RoleProps, context: InjectionContext): RoleProps { const additionalPolicies: any[] = []; // Add CloudWatch logging in production if (this.config.environment === 'production') { additionalPolicies.push( ManagedPolicy.fromAwsManagedPolicyName('CloudWatchAgentServerPolicy') ); } // Enforce session duration limits based on compliance const maxSessionDuration = this.getMaxSessionDuration(); return { ...originalProps, managedPolicies: [ ...(originalProps.managedPolicies || []), ...additionalPolicies ], maxSessionDuration: originalProps.maxSessionDuration || maxSessionDuration, }; } private getMaxSessionDuration(): Duration { switch (this.config.complianceProfile) { case 'SOC2': case 'HIPAA': return Duration.hours(1); // Strict compliance case 'PCI': return Duration.hours(2); default: return this.config.environment === 'production' ? Duration.hours(4) : Duration.hours(12); } } } Blueprint Benefits:
- ✅ Enforces defaults before resource creation
- ✅ Can override user-provided properties
- ✅ Works transparently - developers don't need to change code
- ✅ Centralized security enforcement
3. Aspects (Validation)
Aspects validate resources after they're created but before synthesis. They implement the IAspect interface and visit every construct in the tree.
How Aspects Work
Aspects use the Visitor pattern:
- CDK calls
visit(node)for every construct - You check if the construct is a resource you care about
- You validate and add errors/warnings/info annotations
- Annotations appear in CDK output during synthesis
Example: Security Compliance Aspect
export class SecurityComplianceAspect implements IAspect { constructor(private readonly config: GovernanceConfig) {} public visit(node: IConstruct): void { // Check different resource types if (node instanceof s3.Bucket) { this.validateS3Security(node); } if (node instanceof iam.Role) { this.validateIAMRoleSecurity(node); } if (node instanceof rds.DatabaseInstance) { this.validateRDSInstanceSecurity(node); } if (node instanceof ec2.SecurityGroup) { this.validateSecurityGroupRules(node); } } private validateS3Security(bucket: s3.Bucket): void { const bucketResource = bucket.node.defaultChild as s3.CfnBucket; // Check encryption if (this.config.security?.enforceEncryption && !bucketResource.bucketEncryption) { Annotations.of(bucket).addError( `S3 bucket "${bucket.node.id}" must have encryption enabled` ); } // Check versioning in production if (this.config.environment === 'production') { const versioningConfig = bucketResource.versioningConfiguration; if (!versioningConfig || versioningConfig.status !== 'Enabled') { Annotations.of(bucket).addError( `S3 bucket "${bucket.node.id}" must have versioning enabled in production` ); } } } private validateIAMRoleSecurity(role: iam.Role): void { const roleResource = role.node.defaultChild as iam.CfnRole; // Check for overly permissive policies const policies = roleResource.policies; if (policies && Array.isArray(policies)) { policies.forEach((policy: any) => { const statements = policy.policyDocument?.Statement; if (Array.isArray(statements)) { statements.forEach((statement: any) => { if (statement.Effect === 'Allow' && statement.Resource === '*' && statement.Action === '*') { Annotations.of(role).addError( `IAM role "${role.roleName}" has overly permissive policy with Action: "*" and Resource: "*"` ); } }); } }); } } private validateSecurityGroupRules(sg: ec2.SecurityGroup): void { const sgResource = sg.node.defaultChild as ec2.CfnSecurityGroup; if (sgResource.securityGroupIngress) { const ingress = Array.isArray(sgResource.securityGroupIngress) ? sgResource.securityGroupIngress : [sgResource.securityGroupIngress]; ingress.forEach((rule: any) => { // Check for SSH/RDP from 0.0.0.0/0 if (rule.cidrIp === '0.0.0.0/0') { if (rule.fromPort === 22 || rule.fromPort === 3389) { Annotations.of(sg).addError( `Security group allows SSH/RDP access from 0.0.0.0/0 - critical security risk!` ); } } }); } } } Example: Naming Convention Aspect
export class NamingConventionAspect implements IAspect { constructor(private readonly config: GovernanceConfig) {} public visit(node: IConstruct): void { if (!this.config.naming?.enforce) return; const expectedPrefix = this.config.naming.prefix || `${this.config.organization}-${this.config.environment}`; // Validate S3 bucket names if (node instanceof s3.Bucket) { const bucketName = node.bucketName; if (bucketName && !bucketName.startsWith(expectedPrefix)) { Annotations.of(node).addError( `S3 bucket name "${bucketName}" must start with "${expectedPrefix}"` ); } } // Validate IAM role names if (node instanceof iam.Role) { const roleName = node.roleName; if (roleName && !roleName.includes(this.config.organization)) { Annotations.of(node).addWarning( `IAM role name "${roleName}" should include organization "${this.config.organization}"` ); } } } } Aspect Benefits:
- ✅ Validates resources after creation
- ✅ Catches configuration issues before deployment
- ✅ Provides clear error messages
- ✅ Non-invasive - doesn't modify resources
Configuration System
Create a flexible configuration interface:
export interface GovernanceConfig { // Core identifiers organization: string; project: string; environment: 'development' | 'staging' | 'production'; // Compliance profiles complianceProfile?: 'SOC2' | 'HIPAA' | 'PCI' | 'GDPR' | 'basic'; // Custom tags requiredTags?: string[]; organizationalTags?: Record<string, string>; // Security settings security?: { enforceEncryption?: boolean; enforceSSL?: boolean; blockPublicAccess?: boolean; enableVersioning?: boolean; }; // Naming conventions naming?: { prefix?: string; pattern?: RegExp; enforce?: boolean; }; // Tagging strategy tagging?: { autoApply?: boolean; enforceRequired?: boolean; }; } Compliance Profiles
Pre-configure settings for common compliance frameworks:
export const COMPLIANCE_PROFILES: Record<string, Partial<GovernanceConfig>> = { basic: { security: { enforceEncryption: true, enforceSSL: true, blockPublicAccess: true, }, requiredTags: ['Owner', 'Environment'], }, SOC2: { security: { enforceEncryption: true, enforceSSL: true, blockPublicAccess: true, enableVersioning: true, }, requiredTags: ['Owner', 'Team', 'DataClassification', 'Compliance'], tagging: { enforceRequired: true, }, }, HIPAA: { security: { enforceEncryption: true, enforceSSL: true, blockPublicAccess: true, enableVersioning: true, }, requiredTags: ['Owner', 'Team', 'DataClassification', 'PHI', 'Compliance'], tagging: { enforceRequired: true, }, }, }; export function mergeGovernanceConfig(config: GovernanceConfig): GovernanceConfig { const profile = config.complianceProfile ? COMPLIANCE_PROFILES[config.complianceProfile] : {}; return { ...DEFAULT_GOVERNANCE_CONFIG, ...profile, ...config, security: { ...DEFAULT_GOVERNANCE_CONFIG.security, ...profile.security, ...config.security, }, } as GovernanceConfig; } Governance Factory
Create a factory class to orchestrate everything:
export class GovernanceFactory { /** * Apply governance to an entire CDK App */ static applyToApp(app: App, config: GovernanceConfig): void { const mergedConfig = mergeGovernanceConfig(config); // 1. Apply blueprints (property injection) this.applyBlueprints(app, mergedConfig); // 2. Apply organizational tags new OrganizationalTags(app, 'AppGovernance', mergedConfig); // 3. Apply aspects (validation) this.applyAspects(app, mergedConfig); } /** * Apply governance to a specific stack */ static applyToStack(stack: Stack, config: GovernanceConfig): void { const mergedConfig = mergeGovernanceConfig(config); new OrganizationalTags(stack, 'StackGovernance', mergedConfig); this.applyAspects(stack, mergedConfig); } /** * Apply blueprints via property injection */ private static applyBlueprints(app: App, config: GovernanceConfig): void { const blueprints = [ new SecureS3Blueprint(config), new SecureIAMRoleBlueprint(config), new SecureRDSBlueprint(config), ]; // Register blueprints with the App const existingInjectors = (app as any).propertyInjectors || []; (app as any).propertyInjectors = [...existingInjectors, ...blueprints]; } /** * Apply aspects for validation */ private static applyAspects(scope: Construct, config: GovernanceConfig): void { // Security compliance Aspects.of(scope).add(new SecurityComplianceAspect(config)); // Naming conventions if (config.naming?.enforce) { Aspects.of(scope).add(new NamingConventionAspect(config)); } // Required tags validation if (config.requiredTags && config.requiredTags.length > 0) { Aspects.of(scope).add(new RequiredTagsValidationAspect(config)); } // Compliance tag enforcement if (config.complianceProfile && config.complianceProfile !== 'basic') { Aspects.of(scope).add(new ComplianceTagEnforcementAspect(config)); } } } Convenience Patterns
Add common usage patterns:
export class CommonGovernancePatterns { /** * Apply basic governance (suitable for most projects) */ static basic( app: App, organization: string, project: string, environment: 'development' | 'staging' | 'production' ): void { GovernanceFactory.applyToApp(app, { organization, project, environment, complianceProfile: 'basic', }); } /** * Apply SOC2 compliance governance */ static soc2( app: App, organization: string, project: string, environment: 'development' | 'staging' | 'production' ): void { GovernanceFactory.applyToApp(app, { organization, project, environment, complianceProfile: 'SOC2', }); } /** * Development-friendly governance (less strict) */ static development(app: App, organization: string, project: string): void { GovernanceFactory.applyToApp(app, { organization, project, environment: 'development', complianceProfile: 'basic', security: { enforceEncryption: false, // More flexible for dev enforceSSL: true, blockPublicAccess: true, enableVersioning: false, }, tagging: { enforceRequired: false, // Warnings instead of errors }, }); } /** * Production-ready governance (strict) */ static production( app: App, organization: string, project: string, complianceProfile: 'SOC2' | 'HIPAA' | 'PCI' | 'GDPR' = 'SOC2' ): void { GovernanceFactory.applyToApp(app, { organization, project, environment: 'production', complianceProfile, security: { enforceEncryption: true, enforceSSL: true, blockPublicAccess: true, enableVersioning: true, }, tagging: { enforceRequired: true, // Strict enforcement }, naming: { enforce: true, }, }); } } Package Structure
Organize your library for reusability:
aws-governance-lib/ ├── src/ │ ├── config/ │ │ └── governance-config.ts # Configuration interfaces │ ├── constructs/ │ │ └── governance-factory.ts # Main factory class │ ├── blueprints/ │ │ └── security-blueprints.ts # Property injectors │ ├── aspects/ │ │ ├── core-governance.ts # Tags, naming, etc. │ │ ├── security-compliance.ts # Security validation │ │ └── infrastructure-validation.ts # Resource-specific │ ├── utils/ │ │ └── blueprint-helpers.ts # Helper functions │ └── index.ts # Public exports ├── test/ │ └── governance.test.ts ├── package.json ├── tsconfig.json └── README.md package.json
{ "name": "@myorg/aws-governance-lib", "version": "1.0.0", "description": "Reusable AWS CDK governance library", "main": "lib/index.js", "types": "lib/index.d.ts", "scripts": { "build": "tsc", "test": "jest", "prepublishOnly": "npm run build" }, "peerDependencies": { "aws-cdk-lib": "^2.100.0", "constructs": "^10.0.0" }, "keywords": [ "aws", "cdk", "governance", "compliance", "security" ] } Using the Library
Installation
npm install @myorg/aws-governance-lib Basic Usage
import { App } from 'aws-cdk-lib'; import { CommonGovernancePatterns } from '@myorg/aws-governance-lib'; import { MyStack } from './stacks/MyStack'; const app = new App(); // Apply governance with one line CommonGovernancePatterns.basic(app, 'myorg', 'my-project', 'production'); // Create your stacks normally new MyStack(app, 'MyStack', {}); app.synth(); Advanced Usage
import { App } from 'aws-cdk-lib'; import { GovernanceFactory } from '@myorg/aws-governance-lib'; const app = new App(); // Custom configuration GovernanceFactory.applyToApp(app, { organization: 'myorg', project: 'data-platform', environment: 'production', complianceProfile: 'SOC2', organizationalTags: { 'BusinessUnit': 'Engineering', 'Department': 'Platform', 'CostCenter': 'CC-1234' }, security: { enforceEncryption: true, enforceSSL: true, blockPublicAccess: true, enableVersioning: true, }, naming: { prefix: 'myorg-prod', enforce: true, } }); Per-Stack Governance
const app = new App(); // Different governance per stack const devStack = new MyStack(app, 'DevStack', { env: 'dev' }); const prodStack = new MyStack(app, 'ProdStack', { env: 'prod' }); GovernanceFactory.applyToStack(devStack, { organization: 'myorg', project: 'my-project', environment: 'development', complianceProfile: 'basic', }); GovernanceFactory.applyToStack(prodStack, { organization: 'myorg', project: 'my-project', environment: 'production', complianceProfile: 'SOC2', }); Real-World Example
Let's see a complete example with validation output:
// bin/app.ts import { App } from 'aws-cdk-lib'; import { CommonGovernancePatterns } from '@myorg/aws-governance-lib'; import { DataPipelineStack } from './stacks/DataPipelineStack'; const app = new App(); // Apply SOC2 governance CommonGovernancePatterns.soc2(app, 'myorg', 'data-platform', 'production'); // Create stack new DataPipelineStack(app, 'DataPipelineStack', { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }, }); app.synth(); // stacks/DataPipelineStack.ts import { Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as iam from 'aws-cdk-lib/aws-iam'; export class DataPipelineStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // Create S3 bucket - governance applied automatically! const bucket = new s3.Bucket(this, 'DataLake', { // You don't need to specify security settings // The governance library handles it }); // Blueprint will inject: // - blockPublicAccess: BlockPublicAccess.BLOCK_ALL // - encryption: BucketEncryption.S3_MANAGED // - enforceSSL: true // - versioned: true (production) // Aspect will validate: // - All security settings are correct // - Naming convention follows org standards // - Required tags are present // Create IAM role const role = new iam.Role(this, 'DataPipelineRole', { assumedBy: new iam.ServicePrincipal('glue.amazonaws.com'), }); // Blueprint will inject: // - CloudWatch managed policies (production) // - Maximum session duration (1 hour for SOC2) } } Synthesis Output
When you run cdk synth, you'll see:
[WARNING] S3 bucket "DataLake" missing required tag "DataClassification" - required for SOC2 compliance [INFO] Ensure S3 bucket "DataLake" has SSL enforcement via bucket policy ✅ Successfully applied organizational tags: Organization=myorg, Project=data-platform, Environment=production ✅ Security compliance checks passed for 5 resources Testing the Library
Create comprehensive tests:
import { App, Stack } from 'aws-cdk-lib'; import { Template, Annotations, Match } from 'aws-cdk-lib/assertions'; import * as s3 from 'aws-cdk-lib/aws-s3'; import { GovernanceFactory } from '../src'; describe('Governance Library', () => { test('applies S3 security defaults', () => { const app = new App(); const stack = new Stack(app, 'TestStack'); GovernanceFactory.applyToStack(stack, { organization: 'test', project: 'test-project', environment: 'production', complianceProfile: 'SOC2', }); new s3.Bucket(stack, 'TestBucket', {}); const template = Template.fromStack(stack); // Verify security settings were injected template.hasResourceProperties('AWS::S3::Bucket', { PublicAccessBlockConfiguration: { BlockPublicAcls: true, BlockPublicPolicy: true, IgnorePublicAcls: true, RestrictPublicBuckets: true, }, BucketEncryption: { ServerSideEncryptionConfiguration: Match.anyValue(), }, VersioningConfiguration: { Status: 'Enabled', }, }); }); test('validates naming conventions', () => { const app = new App(); const stack = new Stack(app, 'TestStack'); GovernanceFactory.applyToStack(stack, { organization: 'test', project: 'test-project', environment: 'production', naming: { prefix: 'test-prod', enforce: true, }, }); new s3.Bucket(stack, 'TestBucket', { bucketName: 'wrong-prefix-bucket', }); app.synth(); // Check for naming convention errors const annotations = Annotations.fromStack(stack).findError( '*', Match.stringLikeRegexp('.*must start with "test-prod".*') ); expect(annotations.length).toBeGreaterThan(0); }); test('applies organizational tags', () => { const app = new App(); const stack = new Stack(app, 'TestStack'); GovernanceFactory.applyToStack(stack, { organization: 'test', project: 'test-project', environment: 'development', }); new s3.Bucket(stack, 'TestBucket', {}); const template = Template.fromStack(stack); // Verify tags were applied template.hasResourceProperties('AWS::S3::Bucket', { Tags: Match.arrayWith([ { Key: 'Organization', Value: 'test' }, { Key: 'Project', Value: 'test-project' }, { Key: 'Environment', Value: 'development' }, { Key: 'ManagedBy', Value: 'CDK' }, ]), }); }); }); Best Practices
1. Start Simple, Expand Gradually
// Phase 1: Just tagging GovernanceFactory.applyToApp(app, { organization: 'myorg', project: 'my-project', environment: 'production', }); // Phase 2: Add basic security // (update library, users automatically get new features) // Phase 3: Add compliance profiles // (rollout with migration guide) 2. Make Warnings Non-Breaking
// Use info/warnings for new validations if (config.strictMode) { Annotations.of(node).addError('Must have encryption'); } else { Annotations.of(node).addWarning('Should have encryption'); } 3. Allow Opt-Out Mechanisms
// Let teams opt-out if needed if (this.config.security?.enforceEncryption !== false) { // Apply encryption } // Usage: GovernanceFactory.applyToApp(app, { // ... security: { enforceEncryption: false, // Opt out }, }); 4. Document Governance Decisions
/** * Enforces S3 bucket encryption for SOC2 compliance * * Required by: SOC2 CC6.1 - Encryption at Rest * Exceptions: Must be approved by Security team * Contact: security-team@myorg.com */ if (this.config.complianceProfile === 'SOC2') { enforcedProps.encryption = BucketEncryption.S3_MANAGED; } 5. Version Your Library Carefully
Use semantic versioning:
- Patch (1.0.1): Bug fixes, non-breaking
- Minor (1.1.0): New features, non-breaking
- Major (2.0.0): Breaking changes (new enforcements)
Migration Strategy
Step 1: Install Library
npm install @myorg/aws-governance-lib Step 2: Replace Existing Governance
// BEFORE import { applyStackGovernance } from './factories/stack-factory'; applyStackGovernance(stack, 'production'); // AFTER import { CommonGovernancePatterns } from '@myorg/aws-governance-lib'; CommonGovernancePatterns.production(app, 'myorg', 'my-project', 'SOC2'); Step 3: Keep Project-Specific Aspects
import { GovernanceFactory } from '@myorg/aws-governance-lib'; import { MyCustomAspect } from './aspects/custom-aspects'; // Apply library governance GovernanceFactory.applyToApp(app, config); // Add project-specific aspects Aspects.of(app).add(new MyCustomAspect()); Advanced: Resource-Specific Blueprints
Add blueprints for other AWS services:
// Lambda function blueprint export class SecureLambdaBlueprint implements IPropertyInjector { public readonly constructUniqueId = Function.PROPERTY_INJECTION_ID; public inject(originalProps: FunctionProps, context: InjectionContext): FunctionProps { return { ...originalProps, // Enable X-Ray tracing tracing: Tracing.ACTIVE, // Set environment encryption environmentEncryption: new kms.Key(context.scope, 'LambdaKey'), // Set timeout limits timeout: originalProps.timeout || Duration.seconds(30), }; } } // RDS blueprint export class SecureRDSBlueprint implements IPropertyInjector { public readonly constructUniqueId = DatabaseInstance.PROPERTY_INJECTION_ID; public inject(originalProps: DatabaseInstanceProps, context: InjectionContext): DatabaseInstanceProps { return { ...originalProps, storageEncrypted: true, backupRetention: Duration.days(7), multiAz: this.config.environment === 'production', deletionProtection: this.config.environment === 'production', publiclyAccessible: false, }; } } Conclusion
Building a reusable governance library transforms how you manage AWS infrastructure:
Benefits Recap
✅ Consistency - All projects follow the same standards
✅ Security - Enforce security by default
✅ Compliance - Built-in SOC2, HIPAA, PCI, GDPR profiles
✅ Maintainability - Update governance in one place
✅ Developer Experience - One-line setup, transparent enforcement
✅ Auditability - Clear validation output
✅ Scalability - Works across unlimited projects
Key Takeaways
- Use Blueprints for property injection (enforce defaults)
- Use Aspects for validation (catch issues)
- Use Constructs for organization (apply tags)
- Make it configurable - different needs for dev/prod
- Version carefully - breaking changes need migration guides
- Document everything - explain why, not just what
Next Steps
- Start with tagging and basic security
- Add compliance profiles based on your needs
- Expand to cover more AWS services
- Get feedback from teams
- Iterate and improve
With this architecture, you can ensure every CDK project in your organization follows best practices automatically, freeing developers to focus on building features instead of configuring security.
Resources
This architecture is battle-tested across multiple production environments with 100+ stacks and ensures consistent governance at scale.
Top comments (0)