CDK8S Patterns¶
Understanding construct patterns and best practices in cdk8s-mailu.
Introduction¶
CDK8S (Cloud Development Kit for Kubernetes) allows you to define Kubernetes applications using familiar programming languages. cdk8s-mailu leverages CDK8S patterns to provide a type-safe, testable, and maintainable way to deploy Mailu.
Core Concepts¶
Constructs¶
What is a Construct?
A construct is a composable building block that encapsulates one or more Kubernetes resources. In cdk8s-mailu, each Mailu component is implemented as a construct.
export class AdminConstruct extends Construct {
constructor(scope: Construct, id: string, config: AdminConfig) {
super(scope, id);
// Create Deployment
new Deployment(this, 'deployment', { ... });
// Create Service
new Service(this, 'service', { ... });
}
}
Benefits:
Encapsulation - Hide complex resource definitions
Reusability - Use the same construct in multiple charts
Testability - Unit test each construct independently
Composability - Combine constructs to build applications
Charts¶
What is a Chart?
A chart is a top-level construct that represents a complete Kubernetes application. It contains all the constructs and resources needed for deployment.
export class MailuChart extends Chart {
constructor(scope: Construct, id: string, props: MailuChartProps) {
super(scope, id, props);
// Create namespace
const ns = new Namespace(this, 'namespace', { ... });
// Create constructs
new FrontConstruct(this, 'front', config);
new AdminConstruct(this, 'admin', config);
// ...
}
}
Characteristics:
Extends
Chartfrom cdk8sEntry point for synthesis
Manages shared resources (namespace, configmaps)
Coordinates multiple constructs
Apps¶
What is an App?
An app is the root of your CDK8S application. It contains one or more charts and is responsible for synthesizing Kubernetes manifests.
const app = new App();
new MailuChart(app, 'mailu', { ... });
app.synth(); // Generate YAML files
Design Patterns¶
1. Configuration Object Pattern¶
Pattern: Pass a single configuration object instead of many parameters
// Good: Single configuration object
new AdminConstruct(this, 'admin', {
namespace: 'mailu',
resources: { requests: { cpu: '100m' } },
image: { repository: 'mailu/admin', tag: '2.0' },
});
// Avoid: Many individual parameters
new AdminConstruct(this, 'admin',
'mailu',
{ cpu: '100m' },
'mailu/admin',
'2.0'
);
Benefits:
Type-safe with TypeScript interfaces
Easy to add optional parameters
Clear parameter names
IDE autocomplete support
3. Conditional Resource Creation¶
Pattern: Create resources based on configuration flags
export class MailuChart extends Chart {
constructor(scope: Construct, id: string, config: MailuConfig) {
super(scope, id);
// Always create core components
new FrontConstruct(this, 'front', config);
new AdminConstruct(this, 'admin', config);
// Conditionally create optional components
if (config.components.clamav) {
new ClamAVConstruct(this, 'clamav', config);
}
if (config.components.webdav) {
new WebdavConstruct(this, 'webdav', config);
}
}
}
Benefits:
Flexible deployments
Only create what’s needed
Clear intent in code
Reduces resource usage
4. Resource Requirements Pattern¶
Pattern: Provide sensible defaults with override capability
interface ComponentConfig {
resources?: {
requests?: { cpu: string; memory: string };
limits?: { cpu: string; memory: string };
};
}
// In construct
const resources = config.resources ?? {
requests: { cpu: '100m', memory: '256Mi' },
limits: { cpu: '300m', memory: '512Mi' },
};
Benefits:
Works out of the box
Production-ready defaults
Easy to customize
Explicit resource management
5. Secret Reference Pattern¶
Pattern: Reference existing secrets instead of inline values
interface SecretReference {
name: string; // Secret name
key: string; // Key within secret
}
// Usage
database: {
password: {
name: 'postgres-credentials',
key: 'password',
},
}
// In construct
const passwordEnv = EnvValue.fromSecretValue({
secret: Secret.fromSecretName(this, 'db-secret', config.password.name),
key: config.password.key,
});
Benefits:
Never expose secrets in code
Integrate with secret management tools
Type-safe secret references
Clear intent
Testing Patterns¶
1. Snapshot Testing¶
Pattern: Verify generated manifests haven’t changed unexpectedly
test('generates expected manifests', () => {
const app = Testing.app();
const chart = new MailuChart(app, 'test', { ... });
const results = Testing.synth(chart);
expect(results).toMatchSnapshot();
});
Benefits:
Catch unintended changes
Document expected output
Fast regression testing
2. Resource Count Testing¶
Pattern: Verify expected number of resources
test('creates correct number of resources', () => {
const app = Testing.app();
const chart = new MailuChart(app, 'test', { ... });
const results = Testing.synth(chart);
expect(results.filter(r => r.kind === 'Deployment')).toHaveLength(5);
expect(results.filter(r => r.kind === 'Service')).toHaveLength(5);
});
Benefits:
Verify resource creation
Catch missing/extra resources
Quick validation
3. Configuration Validation Testing¶
Pattern: Test that invalid configurations are rejected
test('rejects invalid domain', () => {
const app = Testing.app();
expect(() => {
new MailuChart(app, 'test', {
domain: 'invalid domain with spaces',
});
}).toThrow('Invalid domain format');
});
Benefits:
Fail fast on bad config
Clear error messages
Prevent invalid deployments
Best Practices¶
1. Type Everything¶
Use TypeScript interfaces for all configuration:
interface MailuConfig {
domain: string;
namespace: string;
components: ComponentToggles;
// ... all fields typed
}
2. Validate Early¶
Validate configuration in construct constructors:
if (!isValidDomain(config.domain)) {
throw new Error(`Invalid domain: ${config.domain}`);
}
3. Provide Defaults¶
Make common configurations work with minimal input:
const resources = config.resources ?? DEFAULT_RESOURCES;
const image = config.image ?? DEFAULT_IMAGE;
4. Document Decisions¶
Use JSDoc comments to explain choices:
/**
* Creates Front component with Nginx reverse proxy.
*
* Note: When using Traefik TLS termination, the nginx-patch-configmap
* is automatically applied to wrap backend connections properly.
*/
export class FrontConstruct extends Construct { ... }
5. Keep Constructs Focused¶
Each construct should have a single responsibility:
// Good: Single component
class AdminConstruct { ... }
class FrontConstruct { ... }
// Avoid: Multiple components in one construct
class AllMailuComponents { ... }
See Also¶
Architecture Overview - Overall system design
Configuration Reference - Complete API docs
Quick Start Tutorial - Hands-on learning
This is a placeholder explanation. Content will be expanded by the docwriter with:
More advanced patterns
Anti-patterns to avoid
Real-world examples
Performance considerations
Debugging techniques
Migration patterns