Explanation
CDK8S Approach for GitLab BDA¶
This document explains how GitLab BDA uses CDK8S with deployment-specific constructs and patterns. For general CDK8S concepts, architecture, and benefits, see CDK8S Infrastructure as Code.
Overview¶
GitLab BDA deployment consists of 14 reusable TypeScript constructs that generate 80+ Kubernetes resources, including:
GitLab platform (via Helm chart integration)
Harbor container registry
PostgreSQL database (CloudNativePG)
Redis cache
S3 buckets for artifacts, uploads, registry, backups
ExternalSecrets for credential management
RBAC and networking
Project Structure¶
dp-infra/gitlabbda/
├── charts/
│ ├── gitlab-chart.ts # Main chart (orchestrator)
│ └── constructs/ # 14 reusable constructs
│ ├── namespace.ts # Namespace + labels
│ ├── rbac.ts # ServiceAccounts, Roles
│ ├── s3-provider.ts # Crossplane ProviderConfig
│ ├── s3-buckets.ts # 8 S3 buckets
│ ├── database.ts # PostgreSQL (CNPG)
│ ├── redis.ts # Redis cache
│ ├── gitlab-helm.ts # GitLab Helm chart integration
│ ├── harbor.ts # Harbor registry
│ ├── app-secrets.ts # ExternalSecrets
│ └── ...
├── main.ts # Entry point (loads config)
├── config.yaml # Configuration (not code!)
├── .env # Secrets (git-ignored)
├── manifests/
│ └── gitlab.k8s.yaml # Generated YAML (committed to git)
├── package.json # Node.js dependencies
└── tsconfig.json # TypeScript configuration
GitLab BDA Constructs¶
1. Namespace and Infrastructure¶
NamespaceConstruct (namespace.ts)
Creates
gitlabbdanamespaceAdds standard labels (managed-by, part-of, version)
RbacConstruct (rbac.ts)
ServiceAccounts for GitLab, Harbor
Roles and RoleBindings
2. Storage and S3¶
S3ProviderConfigConstruct (s3-provider.ts)
Crossplane ProviderConfig for Hetzner S3
Credentials from ExternalSecret
S3BucketsConstruct (s3-buckets.ts)
Creates 8 S3 buckets:
gitlab-artifacts-gitlabbda-kup6s- CI/CD artifactsgitlab-uploads-gitlabbda-kup6s- User uploadsgitlab-lfs-gitlabbda-kup6s- Git LFS objectsgitlab-pages-gitlabbda-kup6s- GitLab Pagesgitlab-registry-gitlabbda-kup6s- Container registrygitlab-backups-gitlabbda-kup6s- GitLab backupsgitlab-postgresbackups-gitlabbda-kup6s- PostgreSQL backupsgitlab-cache-gitlabbda-kup6s- Build cache
3. Databases and Caches¶
DatabaseConstruct (database.ts)
CloudNativePG Cluster (2 replicas)
Storage class:
longhorn-redundant-app(1 replica, PostgreSQL has own replication)Barman backup to S3
PgBouncer pooler
Bootstrap from existing cluster
RedisConstruct (redis.ts)
Redis StatefulSet
Persistent storage
Service for GitLab
4. Applications¶
GitlabHelmConstruct (gitlab-helm.ts)
Key pattern: Integrates existing Helm chart with CDK8S
Generates Helm values from TypeScript config
Renders Helm chart to YAML
Includes rendered resources in CDK8S output
Example:
export class GitlabHelmConstruct extends Construct {
constructor(scope: Construct, id: string, config: AppConfig) {
super(scope, id);
// Generate Helm values from config (type-safe!)
const helmValues = {
global: {
hosts: {
domain: config.domains.gitlab,
},
psql: {
host: 'gitlab-postgres-pooler',
password: {
secret: 'gitlab-postgres-app',
key: 'password',
},
},
minio: {
enabled: false, // Use Hetzner S3 instead
},
registry: {
bucket: 'gitlab-registry-gitlabbda-kup6s',
},
},
gitlab: {
webservice: {
replicas: config.replicas.webservice,
},
},
};
// Render Helm chart (no Helm runtime needed in cluster!)
const rendered = helmRender('gitlab/gitlab', helmValues);
// Include in CDK8S output as standard K8S resources
new kplus.ApiObject(this, 'gitlab-resources', {
apiVersion: 'v1',
kind: 'List',
spec: rendered,
});
}
}
Benefits of this pattern:
Reuse complex Helm charts (GitLab has 100+ resources)
Type-safe values generation (config → Helm values)
No Helm runtime in cluster (ArgoCD sees plain YAML)
Full control over Helm values via TypeScript
See gitlab-helm.ts for implementation.
HarborConstruct (harbor.ts)
Harbor container registry
Similar Helm integration pattern
5. Secret Management¶
AppSecretsConstruct (app-secrets.ts)
Key pattern: ExternalSecret resources via CDK8S
Syncs secrets from ClusterSecretStore
Adds sync wave annotations programmatically
Example:
export class AppSecretsConstruct extends Construct {
constructor(scope: Construct, id: string, config: AppConfig) {
super(scope, id);
// ExternalSecret for GitLab secrets
new ExternalSecret(this, 'gitlab-secrets', {
metadata: {
namespace: config.namespace,
annotations: {
'argocd.argoproj.io/sync-wave': '2', // After ClusterSecretStore (wave 1)
},
},
spec: {
refreshInterval: '1h',
secretStoreRef: {
name: 'gitlabbda-app-secrets-store',
kind: 'ClusterSecretStore',
},
target: {
name: 'gitlab-secrets',
creationPolicy: 'Owner',
},
dataFrom: [{
extract: {
key: 'gitlabbda-app-secrets-source',
},
}],
},
});
}
}
Benefits:
ESO resources generated from type-safe code
Sync waves enforced programmatically
Consistent secret patterns across constructs
Configuration Pattern¶
GitLab BDA separates configuration from code:
# config.yaml (data, committed to git)
versions:
gitlab: v18.5.1
harbor: v2.14.0
domains:
gitlab: gitlab.staging.bluedynamics.eu
harbor: harbor.staging.bluedynamics.eu
replicas:
webservice: 2
sidekiq: 1
storage:
postgresql: 10Gi
redis: 5Gi
// main.ts (loads and validates config)
import * as yaml from 'yaml';
import * as fs from 'fs';
const configFile = fs.readFileSync('config.yaml', 'utf-8');
const config: AppConfig = yaml.parse(configFile);
// Override with environment variables
if (process.env.GITLAB_VERSION) {
config.versions.gitlab = process.env.GITLAB_VERSION;
}
// Create app and chart
const app = new App();
new GitlabChart(app, 'gitlab', config);
app.synth();
Benefits:
Config is pure data (no code)
Type-safe via
AppConfiginterfaceEnvironment-specific configs (staging vs production)
IDE autocomplete for config fields
Build Workflow¶
# CRITICAL: Source .env to load secrets
bash -c 'source .env && npm run build'
What happens:
main.tsreadsconfig.yamlandprocess.env(.env vars)TypeScript compiles to JavaScript (
npm run compile)CDK8S constructs instantiated with config
CDK8S generates
manifests/gitlab.k8s.yaml(npm run synth)Commit manifests to git
ArgoCD deploys to cluster
Construct Composition¶
The main chart composes all 14 constructs:
// charts/gitlab-chart.ts
export class GitlabChart extends Chart {
constructor(scope: Construct, id: string, config: AppConfig) {
super(scope, id);
// Infrastructure (wave 1)
new NamespaceConstruct(this, 'namespace', config);
new RbacConstruct(this, 'rbac', config);
new S3ProviderConfigConstruct(this, 's3-provider', config);
new S3BucketsConstruct(this, 's3-buckets', config);
// Secrets (wave 2)
new AppSecretsConstruct(this, 'secrets', config);
// Databases and apps (wave 3)
new DatabaseConstruct(this, 'database', config);
new RedisConstruct(this, 'redis', config);
new GitlabHelmConstruct(this, 'gitlab', config);
new HarborConstruct(this, 'harbor', config);
}
}
Benefits:
Clear dependency order (waves)
Each construct independently testable
Easy to add/remove components
High-level view of deployment
Unique GitLab BDA Patterns¶
1. Helm Chart Integration¶
Challenge: GitLab Helm chart has 100+ resources, complex configuration
Solution: Render Helm chart via CDK8S, output as plain YAML
Best of both worlds: Helm expertise + CDK8S type safety
ArgoCD sees plain YAML (no Helm runtime needed)
Full control over Helm values via TypeScript
2. Multi-Bucket S3 Strategy¶
Challenge: GitLab needs 8 different S3 buckets for different purposes
Solution: Single construct creates all buckets with consistent naming
All follow pattern:
gitlab-{purpose}-gitlabbda-kup6sAll use same Crossplane ProviderConfig
All configured with
managementPoliciesto skip tagging (Hetzner limitation)
3. PostgreSQL with Barman Backup¶
Challenge: PostgreSQL backups to S3 with CNPG
Solution: DatabaseConstruct configures Barman cloud backups
backup: {
barmanObjectStore: {
destinationPath: `s3://gitlab-postgresbackups-gitlabbda-kup6s/`,
s3Credentials: {
accessKeyId: { /* from secret */ },
secretAccessKey: { /* from secret */ },
},
wal: {
compression: 'gzip',
encryption: 'AES256',
},
},
retentionPolicy: '30d',
}
4. Environment-Specific Builds¶
Staging:
cp config.yaml config-active.yaml
bash -c 'source .env && npm run build'
Production:
cp config-production.yaml config-active.yaml
bash -c 'source .env && npm run build'
Different domains, replica counts, storage sizes via configuration.
Testing¶
GitLab BDA constructs are unit-tested:
// tests/constructs/database.test.ts
describe('DatabaseConstruct', () => {
it('should create CNPG cluster with correct storage class', () => {
const chart = Testing.chart();
const config = createTestConfig();
new DatabaseConstruct(chart, 'test', { config });
const manifests = Testing.synth(chart);
const cluster = findResource(manifests, 'Cluster');
expect(cluster.spec.storage.storageClass).toBe('longhorn-redundant-app');
});
it('should configure Barman S3 backups', () => {
const chart = Testing.chart();
const config = createTestConfig();
new DatabaseConstruct(chart, 'test', { config });
const manifests = Testing.synth(chart);
const cluster = findResource(manifests, 'Cluster');
expect(cluster.spec.backup.barmanObjectStore.destinationPath)
.toContain('s3://gitlab-postgresbackups-gitlabbda-kup6s');
});
});
Troubleshooting¶
Missing .env File¶
Symptoms: S3 credentials show undefined in manifests
Solution:
# Verify .env exists
cat .env | grep HETZNER_S3_ACCESS_KEY
# Ensure using bash -c 'source .env'
bash -c 'source .env && npm run build'
Helm Values Not Applied¶
Symptoms: GitLab deployed with default values, ignoring config
Solution: Check GitlabHelmConstruct generates correct Helm values
# Inspect generated manifest
grep -A 20 'global:' manifests/gitlab.k8s.yaml
CNPG Cluster Not Starting¶
Symptoms: PostgreSQL pods in Pending
Common causes:
PVC not created (check Longhorn)
Barman credentials invalid (check ExternalSecret)
Sync wave out of order (database before secrets)
See CLAUDE.md for comprehensive troubleshooting.