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