Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions charts/db-sync/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: v2
name: db-sync
description: Helm chart for govuk-db-sync CronJobs (backup/restore for PostgreSQL, MySQL, DocumentDB)
type: application
version: 0.1.0
appVersion: "0.1.0"
keywords:
- backup
- restore
- database
- cronjob
maintainers:
- name: GOV.UK Platform Engineering
email: govuk-platform-engineering@digital.cabinet-office.gov.uk
173 changes: 173 additions & 0 deletions charts/db-sync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# db-sync Helm Chart

Helm chart for deploying `govuk-db-sync` CronJobs to manage database backups and restores across PostgreSQL, MySQL, and DocumentDB.

## Installation

```bash
helm install db-sync ./charts/db-sync \
-f values-staging.yaml \
-n database-sync
```

## Configuration

### Basic Job Configuration

Each job in `values.yaml` requires:

- **schedule**: Cron schedule (e.g., `"0 2 * * *"`)
- **operation**: `backup` or `restore`
- **dbType**: `postgres`, `mysql`, or `documentdb`
- **dbName**: Target database name
- **s3Bucket**: S3 bucket for backups (e.g., `s3://govuk-prod-backups`)
- **s3Path**: S3 path prefix (e.g., `myapp/db`)

### Example Configuration

```yaml
cronjobs:
production:
# PostgreSQL backup
account-api-postgres:
schedule: "37 23 * * *"
operation: backup
dbType: postgres
dbName: account-api_production
s3Bucket: s3://govuk-prod-database-backups
s3Path: account-api/db

# PostgreSQL restore with transformation
email-alert-api-postgres:
schedule: "54 3 * * 1"
operation: restore
dbType: postgres
dbName: email-alert-api_production
s3Bucket: s3://govuk-staging-database-backups
s3Path: email-alert-api/db
transformScript: email-alert-api.sql
extraEnv:
- name: DB_OWNER
value: email-alert-api

Comment thread
dj-maisy marked this conversation as resolved.
# MySQL backup
release-mysql:
schedule: "11 3 * * 1"
operation: backup
dbType: mysql
dbName: release_production
s3Bucket: s3://govuk-prod-database-backups
s3Path: release/db

# DocumentDB backup
publisher-documentdb:
schedule: "13 3 * * 1"
operation: backup
dbType: documentdb
dbName: publisher_production
s3Bucket: s3://govuk-prod-database-backups
s3Path: publisher/docdb
```

### Optional Job Configuration

- **suspend**: Set to `true` to disable a job (default: `false`)
- **maxJobRuntimeSeconds**: Maximum job runtime in seconds (default: 43200 = 12 hours)
- **transformScript**: Name of a transform script from the `scripts/` ConfigMap
- **extraEnv**: Job-specific environment variables
- **resources**: Override default resource limits/requests

## Secrets Management

This chart uses AWS Secrets Manager via the ExternalSecrets operator.

**Expected Secret Structure:**

Create a secret at `govuk/db-sync/passwd` with key/value pairs mapping job names to passwords:

```json
{
"account-api-postgres": "password123",
"email-alert-api-postgres": "password456",
"release-mysql": "password789",
"publisher-documentdb": "mongodb-connection-string"
}
```

The chart loads these credentials into Kubernetes Secret `db-sync-passwd`, which is referenced by CronJobs.

## IAM Permissions

The ServiceAccount uses AWS IAM Roles for Service Accounts (IRSA). Configure the annotation:

```yaml
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/db-sync-role
```

**Required IAM Permissions:**
- S3: `s3:GetObject`, `s3:PutObject` on backup buckets
- RDS (for backups): Database instance access
- Secrets Manager: `secretsmanager:GetSecretValue` for `govuk/db-sync/passwd`
- CloudWatch (optional): Metrics publishing

## Transform Scripts

Place SQL/JavaScript transformation scripts in the `scripts/` directory:

```
charts/db-sync/
├── scripts/
│ ├── email-alert-api.sql
│ ├── content-store.sql
│ └── custom-transform.js
```

Reference them in job config:

```yaml
transformScript: email-alert-api.sql
```

## Environment-Specific Values

Create separate values files for each environment:

```bash
values-production.yaml
values-staging.yaml
values-integration.yaml
```

Then deploy with:

```bash
helm install db-sync ./charts/db-sync -f values-production.yaml
```

## Monitoring

Each CronJob logs to stdout. Monitor failures via:
- Kubernetes Events: `kubectl describe cronjob db-sync-<job-name>`
- Pod Logs: `kubectl logs -l app.kubernetes.io/component=<job-name>`
- Failed Jobs: `kubectl get jobs --failed`

## Troubleshooting

**Job not running:**
- Check if suspended: `kubectl get cronjob db-sync-<job-name>`
- Verify schedule is valid: https://crontab.guru/

**Secret not found:**
- Ensure ExternalSecret is synced: `kubectl describe externalsecret db-sync-passwd`
- Check AWS Secrets Manager: `aws secretsmanager get-secret-value --secret-id govuk/db-sync/passwd`

**Pod crash:**
- Check pod logs: `kubectl logs <pod-name> -p` (for previous pod)
- Verify S3 bucket access and IAM permissions
- Check database connectivity and credentials

## Values Reference

See `values.yaml` for all available configuration options.
166 changes: 166 additions & 0 deletions charts/db-sync/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "db-sync.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
*/}}
{{- define "db-sync.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "db-sync.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.AppVersion | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Common labels
*/}}
{{- define "db-sync.labels" -}}
helm.sh/chart: {{ include "db-sync.chart" . }}
{{ include "db-sync.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels
*/}}
{{- define "db-sync.selectorLabels" -}}
app.kubernetes.io/name: {{ include "db-sync.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Create the name of the service account to use
*/}}
{{- define "db-sync.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "db-sync.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

{{/*
Default DB port for a dbType.
Usage: include "db-sync.defaultDbPort" "postgres"
*/}}
{{- define "db-sync.defaultDbPort" -}}
{{- if eq . "postgres" -}}
5432
{{- else if eq . "mysql" -}}
3306
{{- else if eq . "documentdb" -}}
27017
{{- end -}}
{{- end }}

{{/*
URI scheme for a dbType.
Usage: include "db-sync.dbUriScheme" "postgres"
*/}}
{{- define "db-sync.dbUriScheme" -}}
{{- if eq . "postgres" -}}
postgres
{{- else if eq . "mysql" -}}
mysql
{{- else if eq . "documentdb" -}}
mongodb
{{- end -}}
{{- end }}

{{/*
Transform DB user default by engine.
Expected dict keys: job
*/}}
{{- define "db-sync.transformUser" -}}
{{- $job := .job -}}
{{- if eq $job.dbType "documentdb" -}}
{{- default "mongoadmin" $job.transformUsername -}}
{{- else if eq $job.dbType "mysql" -}}
{{- default "root" $job.transformUsername -}}
{{- else -}}
{{- default "postgres" $job.transformUsername -}}
{{- end -}}
{{- end }}

{{/*
Build source URI from components unless sourceUri is explicitly set.
Expected dict keys: root, job
*/}}
{{- define "db-sync.sourceUri" -}}
{{- $root := .root -}}
{{- $job := .job -}}
{{- if $job.sourceUri -}}
{{- $job.sourceUri -}}
{{- else -}}
{{- $hostname := default $root.Values.defaultSourceDbHostname $job.sourceDbHostname -}}
{{- if $hostname -}}
{{- $port := default $root.Values.defaultSourceDbPort $job.sourceDbPort -}}
{{- if not $port -}}
{{- $port = include "db-sync.defaultDbPort" $job.dbType | trim -}}
{{- end -}}
{{- $username := default $root.Values.defaultDbUsername $job.sourceDbUsername -}}
{{- $scheme := include "db-sync.dbUriScheme" $job.dbType | trim -}}
{{- printf "%s://%s@%s:%s/%s" $scheme $username $hostname $port $job.dbName -}}
{{- end -}}
{{- end -}}
{{- end }}

{{/*
Build destination URI from components unless destUri is explicitly set.
Expected dict keys: root, job
*/}}
{{- define "db-sync.destUri" -}}
{{- $root := .root -}}
{{- $job := .job -}}
{{- if $job.destUri -}}
{{- $job.destUri -}}
{{- else if ne $job.dbType "documentdb" -}}
{{- $hostname := default $root.Values.defaultDestDbHostname $job.destDbHostname -}}
{{- if $hostname -}}
{{- $port := default $root.Values.defaultDestDbPort $job.destDbPort -}}
{{- if not $port -}}
{{- $port = include "db-sync.defaultDbPort" $job.dbType | trim -}}
{{- end -}}
{{- $username := default $root.Values.defaultDbUsername $job.destDbUsername -}}
{{- $scheme := include "db-sync.dbUriScheme" $job.dbType | trim -}}
{{- printf "%s://%s@%s:%s/%s" $scheme $username $hostname $port $job.dbName -}}
{{- end -}}
{{- end -}}
{{- end }}

{{/*
Build transform URI from components unless transformUri is explicitly set.
Expected dict keys: job, transformUser
*/}}
{{- define "db-sync.transformUri" -}}
{{- $job := .job -}}
{{- $transformUser := .transformUser -}}
{{- if $job.transformUri -}}
{{- $job.transformUri -}}
{{- else -}}
{{- $scheme := include "db-sync.dbUriScheme" $job.dbType | trim -}}
{{- $port := include "db-sync.defaultDbPort" $job.dbType | trim -}}
{{- $transformDbName := default $job.dbName $job.transformDbName -}}
{{- printf "%s://%s:password@127.0.0.1:%s/%s" $scheme $transformUser $port $transformDbName -}}
{{- end -}}
Comment thread
dj-maisy marked this conversation as resolved.
{{- end }}
Loading
Loading