This document describes how to deploy the helm charts for VegBank API and the VegBank Postgres Cluster. After installing the helm charts, you should see one or more instances of the VegBank python pod, which hosts the flask app that powers the API (ex. https://api-dev.vegbank.org/plant-concepts/pc.92413) and three CloudNative PostgreSQL (CNPG) pods which contain the postgres database used by the API. The CNPG pods consist of a primary read-write pod and two replica read-only pods.
- API Authorization Guide: api-authorization.md – Detailed authentication, token usage, and scope system
- Production Deployment to Kubernetes: prod-deployment.md - Details on production deployment
- Database Recovery from Backups: db-recovery.md - Details on database backup and recovery
- VegBank Database Bootstrap: README - Automated method of restoring from a data-only dump file taken from the original VegBank database.
From the vegbank2 repo root directory, either:
-
# Deploy the local helm chart to the `dev-vegbank` context: helm upgrade vegbankapi -n vegbank ./helm -f ./helm/examples/values-overrides-dev-vb.yaml \ --set image.tag="docker-img-tag-#" # set to the Docker image version you wish to deploy
...or:
-
# Deploy the local helm chart to the `dev-vegbank-dev` context: helm upgrade vegbankapi -n vegbank-dev ./helm -f ./helm/examples/values-overrides-dev-vb-dev.yaml \ --set image.tag="docker-img-tag-#" # set to the Docker image version you wish to deploy
- Introduction
- Quick Reference: NCEAS Dev Deployments
- Requirements
- Prerequisites
- API Application Deployment
- Parameters
- Packaging and Publishing the Helm Chart
- Appendix 1: Prerequisite: Install a PostgreSQL Database
- Appendix 2: Prerequisite: Create K8s Secrets
- Appendix 3: Initial Database Population with a Dump File
- Helm 4.x
- Kubernetes 1.26+
- A target Kubernetes cluster with:
- a suitable namespace (ex.
vegbank,vegbank-dev) - CloudNative PG Operator 1.27.0+ installed (or you can provide your own PostgreSQL database, but this approach is not tested or supported)
- a suitable namespace (ex.
Before the initial deployment of the VegBank API helm chart, these prerequisites must be met:
- A PostgreSQL database must be deployed and running in the cluster. At NCEAS, we use the
dataone-cnpghelm chart to deploy a CloudNative PostgreSQL cluster. See Appendix 1 for details. - The necessary Kubernetes secrets must be created. See Appendix 2.
- If you wish to pre-populate the database with data from an existing dump file, that file must be made accessible to the helm chart via a PVC, and the appropriate parameters in
values.yamlmust be overridden to enable the restore process. See Appendix 3 for details.
Tip
Values overrides for deployments on the NCEAS k8s dev cluster are kept in the helm/examples directory. These files show the necessary overrides for different dev deployment contexts (e.g. dev-vegbank vs dev-vegbank-dev), and can be used for reference when creating your own overrides files.
-
Values Overrides
It will be necessary to override a few of the default parameters in the values.yaml file, to match your deployment environment. The recommended approach is to create (and keep a versioned copy of) a YAML file that specifies only those values that need to be overridden. The Parameters section, below, lists the parameters that can be configured during installation.
-
For example, to deploy the VegBank instance in our
dev-vegbankK8s context, we use the overrides defined inhelm/examples/values-overrides-dev-vb.yaml:$ helm upgrade --install vegbankapi -n vegbank oci://ghcr.io/nceas/charts/vegbank \ -f ./helm/examples/values-overrides-dev-vb.yamlwhere
values-overrides-dev-vb.yamlcontains only the values we wish to override. Similarly, we would use thehelm/examples/values-overrides-dev-vb-dev.yamlfile when deploying in thedev-vegbank-devcontext. -
Parameters may also be overridden on the command line; e.g.
$ helm upgrade --install vegbankapi -n vegbank oci://ghcr.io/nceas/charts/vegbank \ -f ./helm/examples/values-overrides-dev-vb.yaml --set image.tag="2.0.0-beta01"
-
-
Values Overrides for Database Population
If you wish to populate the database from a
pg_dumpfile on startup, see Appendix 3 for the necessary values overrides.
This example uses the dev-vegbank context and the values-overrides-dev-vb.yaml overrides file:
-
Deploy the latest published helm chart
$ helm upgrade --install vegbankapi -n vegbank oci://ghcr.io/nceas/charts/vegbank \ -f ./helm/examples/values-overrides-dev-vb.yaml- Deploy a specific version by adding:
--version <version-#>(List of published chart versions) - See more info by adding:
--debug --dry-runcan be used to test the installation without actually deploying
- Deploy a specific version by adding:
-
Deploy from the local helm chart in this repo (e.g. for testing changes to the chart)
$ helm upgrade --install vegbankapi ./helm \ -f ./helm/examples/values-overrides-dev-vb.yaml --debug -
To uninstall:
$ helm uninstall vegbankapi -n vegbank
Tip
If you wish to access the API without ingress (i.e. if ingress.enabled is false), you can do so by port-forwarding to the API service.
# 1. Find the name of the API service
#
$ kubectl get svc | grep api # or grep for your vegbank api release name
vegbankapi ClusterIP 10.104.197.36 <none> 80/TCP 26d
# 2. Set up port forwarding using kubectl port-forward service/<svc-name> <local-port>:80
#
$ kubectl port-forward service/vegbankapi 8080:80
# 3. Access the API on localhost via the port you specified:
#
$ curl -s http://localhost:8080/plant-concepts/pc.92413There are two initContainers:
vegbank-reconcile-postgres- This waits until the
postgrespod is accepting connections before proceeding with the nextinitContainer - If
databaseRestore.enabledis set totrueinvalues.yaml, the dump file defined indatabaseRestoreis restored to the empty cluster
- This waits until the
vegbank-apply-flyway- This executes
flyway migrate, which applies the migration files found in/db/migrations
- This executes
Tip
To monitor what's happening, you can use the following commands:
# Monitor pod readiness:
$ kubectl get pods --watch
NAME READY STATUS RESTARTS AGE
vegbankapi-bb94bf498-6fpw4 0/1 Init:0/2 0 12s
vegbankdb-cnpg-1 1/1 Running 0 5m4s
vegbankdb-cnpg-2 1/1 Running 0 3m36s
vegbankdb-cnpg-3 1/1 Running 0 2m19s
# Watch the container logs:
$ kubectl logs -f vegbankapi-bb94bf498-6fpw4 -c vegbankapi-reconcile-postgres
# or:
$ kubectl logs -f vegbankapi-bb94bf498-6fpw4 -c vegbankapi-apply-flywayAfter the initContainers complete, you will now have an up-to-date copy of the current vegbank postgres database - which has applied all migrations found in helm/db/mgrations.
If you are testing new schema updates, add them to helm/db/migrations with the correct naming convention and run a helm upgrade command, as described above.
Important
Before upgrading or redeploying, don't forget to change the databaseRestore.enabled value back to false if you enabled it earlier.
| Name | Description | Value |
|---|---|---|
auth.accessMode |
Controls authentication & upload behavior (RW or RO) for API access | read_only |
auth.oidcSecretName |
Name of the Kubernetes secret containing client_secrets.json | vegbank-oidc-config |
auth.oidcDefaultScopes |
Space-separated standard OIDC scopes requested during login. | openid email profile |
auth.roleName.admin |
Scope name for the admin role | vegbank:admin |
auth.roleName.contributor |
Scope name for the contributor role | vegbank:contributor |
auth.roleName.user |
Scope name for the user role | vegbank:user |
| Name | Description | Value |
|---|---|---|
database.name |
The name of the postgres database to be used by VegBank | vegbank |
database.host |
hostname of database to be used by VegBank (RW svc name of CNPG or pooler) | vegbankdb-cnpg-rw |
database.port |
port to connect to the database (Must match CNPG or pooler port number) | 5432 |
database.existingSecret |
Secret containing the database username and password. | vegbankdbcreds |
databaseRestore.enabled |
Restores a full (schema+data) database dump file defined below | false |
databaseRestore.pvc |
PVC containing the (schema+data) database dump file to restore | vegbankdb-init-pgdata |
databaseRestore.mountpath |
Mount path inside the container for the pvc/dump file volume | /tmp/databaseRestore |
databaseRestore.filepath |
dump file path, relative to databaseRestore.mountpath | vegbank_full_fc_v1.9_pg16_20250924.dump |
databaseRestore.postgresImage |
postgres image used by initContainer (must match CNPG postgres version) | postgres:17 |
flyway.image.repository |
docker image repository for flyway, used in initContainer | flyway/flyway |
flyway.image.pullPolicy |
How often should flyway image be pulled from repository? | IfNotPresent |
flyway.image.tag |
The tag of the flyway image to use in the initContainer | 12.1 |
flyway.dbpath |
The path to the directory containing the flyway migration files | /opt/local/flyway/db |
flyway.dbHost |
hostname for flyway's direct connection to the database (not via pooler!) | vegbankdb-cnpg-rw |
flyway.dbPort |
port for flyway's direct connection to the database (not via pooler!) | 5432 |
| Name | Description | Value |
|---|---|---|
ezid.baseUrl |
Base URL of the EZID API | https://ezid.cdlib.org |
ezid.doiPrefix |
DOI prefix (e.g. doi:10.5072 for test, doi:10.xxxxx for production) | doi:10.5072 |
ezid.doiShoulder |
DOI shoulder for minting (e.g. FK2 for test) | FK2 |
ezid.defaultTargetUrl |
Default target URL for minted DOIs | https://vegbank.org/cite/ |
| Name | Description | Value |
|---|---|---|
image.repository |
GitHub remote image repository address for the VegBank Docker Image | ghcr.io/nceas/vegbank |
image.pullPolicy |
How often should the image be pulled from the repository? | IfNotPresent |
image.tag |
version/tag of the VB Docker Image to use. Leave blank to use Chart default | "" |
resources |
{} |
| Name | Description | Value |
|---|---|---|
startupProbe.enabled |
Enable startupProbe | true |
startupProbe.httpGet.path |
The url path to probe during startup | / |
startupProbe.httpGet.port |
The named containerPort to probe | http |
startupProbe.successThreshold |
Min consecutive successes for probe to be successful | 1 |
startupProbe.failureThreshold |
No. of consecutive failures before the container restarted | 36 |
startupProbe.periodSeconds |
Interval (in seconds) between startup checks | 5 |
startupProbe.timeoutSeconds |
Timeout (in seconds) for each startup check | 3 |
livenessProbe.enabled |
Enable livenessProbe | true |
livenessProbe.httpGet.path |
The url path to probe | / |
livenessProbe.httpGet.port |
The named containerPort to probe | http |
livenessProbe.periodSeconds |
Period seconds for livenessProbe | 20 |
livenessProbe.timeoutSeconds |
Timeout seconds for livenessProbe | 10 |
livenessProbe.successThreshold |
Min consecutive successes for probe to be successful | 1 |
livenessProbe.failureThreshold |
No. consecutive failures before container restarted | 15 |
readinessProbe.enabled |
Enable readinessProbe | true |
readinessProbe.httpGet.path |
The url path to probe | / |
readinessProbe.httpGet.port |
The named containerPort to probe | http |
readinessProbe.periodSeconds |
Period seconds for readinessProbe | 10 |
readinessProbe.timeoutSeconds |
Timeout seconds for readinessProbe | 5 |
readinessProbe.successThreshold |
Min consecutive successes for probe to be successful | 1 |
readinessProbe.failureThreshold |
No. consecutive failures before container marked unhealthy | 3 |
| Name | Description | Value |
|---|---|---|
service.type |
The type of service to create. | ClusterIP |
service.port |
The port on which the service will be exposed. | 8000 |
| Name | Description | Value |
|---|---|---|
gunicorn.workers |
Number of worker processes for handling requests. | 5 |
gunicorn.timeout |
Workers silent for more than this many seconds are killed and restarted | 2400 |
gunicorn.logLevel |
Granularity of gunicorn logs (debug | info |
gunicorn.limitRequestFieldSize |
The maximum size of HTTP request header fields in bytes. Default is 8KB, but this is upped to 16kb to match the limit set at the auth stage. | 16384 |
| Name | Description | Value |
|---|---|---|
ingress.enabled |
Enable ingress to allow web traffic. Ingress settings ignored if 'false' | false |
ingress.className |
The class of the ingress controller to use. | traefik |
ingress.hostname |
Simple hostname mode: ingress auto-generated if ingress.hosts empty |
localhost |
ingress.hosts |
Full ingress host/path subtree (advanced mode). | [] |
ingress.tlsEnabled |
Set to 'false', to disable rendering Ingress TLS (HTTP-only). | true |
ingress.tlsSecretName |
Secret name used by inferred TLS when ingress.tls is empty. |
ingress-nginx-tls-cert |
ingress.annotations.cert-manager.io/cluster-issuer |
cert-manager cluster issuer | letsencrypt-prod |
ingress.tls |
Full TLS subtree (advanced mode). Ignored unless ingress.enabled is true. | [] |
| Name | Description | Value |
|---|---|---|
serviceAccount.create |
Specifies whether a service account should be created | true |
serviceAccount.automount |
Automatically mount a ServiceAccount's API credentials? | true |
serviceAccount.annotations |
Annotations to add to the service account | {} |
serviceAccount.name |
The name of the service account to use. | "" |
autoscaling.enabled |
false |
|
autoscaling.minReplicas |
1 |
|
autoscaling.maxReplicas |
100 |
|
autoscaling.targetCPUUtilizationPercentage |
80 |
|
volumeMounts |
Additional volumeMounts on the output Deployment definition. | [] |
nodeSelector |
{} |
|
tolerations |
[] |
|
affinity |
{} |
|
imagePullSecrets |
[] |
|
nameOverride |
"" |
|
fullnameOverride |
"" |
|
podAnnotations |
currently unused | {} |
podLabels |
{} |
|
podSecurityContext |
{} |
|
securityContext |
{} |
-
Edit
Chart.yamlas follows:- Update the
versionfield to the new version number (e.g.0.1.0). This version number uses semantic versioning, and should be updated according to the type of changes made to the chart since the last version. - Ensure the
appVersionfield is set to match the version of the VegBank API that you wish to deploy with this chart. This should be a valid docker image tag for the VegBank API image in the GitHub Container Registry.
- Update the
-
Use the
helm packagecommand:helm package -u ./helm
...which will create a file named
vegbank-<x.x.x>.tgzin the current directory, where<x.x.x>is the version number you set inChart.yaml.
-
Prerequisite: you will need a GitHub Personal Access Token (PAT) with the appropriate permissions to publish to the GitHub Container Registry. See the Docker README for details on how to create a PAT and use it to log in.
-
Once you have authenticated with the GitHub Container Registry, you can use the
helm pushcommand to publish the packaged chart:helm push vegbank-<x.x.x>.tgz oci://ghcr.io/nceas/charts
Before deploying the VegBank API helm chart, first deploy a PostgreSQL database, using the cnpg helm chart. This will initialize 3 postgres pods - wait for all three pods to be ready - and check their logs are free of errors - before proceeding.
Caution
This is only a one-time deployment. DO NOT helm uninstall or helm delete this chart, unless you really need to! Doing so will result in the dynamically provisioned PVCs being deleted (You won't lose the PVs or the data, but re-binding new PVCs to the existing data is non-trivial.) If you chose to have CNPG auto-generate a DB credentials secret, that will also be deleted.
-
Database Credentials
You may manually create a K8s Secret containing the PostgreSQL username and password, or you can allow the
cnpgchart to automatically generate a secret for you. If you choose to create a secret manually, ensure thevalues-cnpg.yamlfile contains the name of your secret. See the dataone-cnpg chart documentation for details.
Important
Either way, be sure to save the credentials; if lost, you will need to uninstall and reinstall the chart which could result in data loss if you have not taken a backup. NCEAS production and shared-dev credentials should be kept GPG-encrypted in our security repo.
-
Install the cnpg chart with the appropriate overrides file
# Deploy the latest version of the chart by leaving out the --version parameter. # Using the deployment name 'vegbankdb': # $ helm install vegbankdb oci://ghcr.io/dataoneorg/charts/cnpg -f ./helm/admin/values-cnpg.yaml $ kubectl -n dev-vegbank get pods NAME READY STATUS RESTARTS AGE vegbankdb-cnpg-1 1/1 Running 0 5m vegbankdb-cnpg-2 1/1 Running 0 6m vegbankdb-cnpg-3 1/1 Running 0 7m
-
Database Backup & Recovery
Scheduled backups can be enabled as described in the
dataone-cnpghelm chart documentation. This is disabled by default, but can be overridden in./admin/values-cnpg.yamlSee the database recovery documentation at
./docs/db-recovery.mdfor details of how to recover the database from backups.
-
Flask Session-Signing Secret for the API Application
This secret MUST be stable across pod restarts so that user sessions remain valid after a deployment or pod reschedule. Without it, a new random key is generated every restart, invalidating all active sessions. The secret is consumed by the deployment as the
FLASK_SECRET_KEYenvironment variable. Steps:# 1. Generate a strong random key locally: RND_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))") # 2. Create the secret: kubectl create secret generic vegbank-flask-secret \ --from-literal=secret_key=$RND_KEY
-
OIDC Client Secret for the API Application
This secret will be mounted into the pod at
/etc/vegbank/oidc/client_secrets.jsonand read by the app via theOIDC_CLIENT_SECRETS_FILEenvironment variable. Steps:-
Either obtain the keycloak-client-secrets-prod.json file from our private NCEAS GH Enterprise security repo, or use the template
helm/admin/client-secrets.jsonas a starting point to fill in your own details (client_id,client_secret,server_metadata_url,redirect_uris) -
Create the secret from this file:
kubectl create secret generic vegbank-oidc-config \ --from-file=client_secrets.json=path/to/my-client-secrets.json
-
-
EZID API Credentials for DOI Minting
The EZID username and password are required for minting DOIs and are consumed by the application via the
EZID_USERNAMEandEZID_PASSWORDenvironment variables. They are stored in the K8s secret defined inhelm/admin/secret.yaml. Edit that file to add your credentials, then apply it to your cluster:RELEASE_NAME=vegbankapi envsubst < helm/admin/secret.yaml | kubectl apply -n <mynamespace> -f -
For development and test deployments, credentials can be found on our private NCEAS dev team documentation site. For production deployments, obtain credentials from the GPG-encrypted secrets file in our private NCEAS GH Enterprise security repo.
Note
The DOI prefix and shoulder for production are documented in
NCEAS/vegbank2#305. Configure them via the
ezid.doiPrefix and ezid.doiShoulder parameters in your values overrides file. The defaults
in values.yaml are the EZID test/sandbox values (doi:10.5072 / FK2).
Note
A data-only (DML) pg_dump from the original VegBank database was used to bootstrap the production deployment. This process is documented in the ./docs/prod-deployment.md file
-
If you only need a "clean" installation with an empty database, this step can be skipped.
-
If you wish to pre-populate with data from an existing database, you will need a
pg_dumpfile that contains both the data (DML) and the schema definition (DDL). Such files are available for development purposes on theknbvm (knbvm.nceas.ucsb.edu)VM, under/mnt/ceph/repos/vegbank/, and can be accessed via PV+PVC mounts in bothvegbank-devandvegbanknamespaces; e.g:$ kc get pvc -n vegbank-dev NAME STATUS VOLUME CAPACITY ACCESS MODES AGE vegbankdb-init-pgdata Bound cephfs-vegbankdb-init-pgdata 100Gi RWO 182d # Admin access is needed for additional PV creation -
Override the following values in
values.yamlto enable the restore process and specify the dump file to use:databaseRestore: enabled: true # (default is 'false') pvc: "vegbankdb-init-pgdata" # must match name of PVC containing dump file (see above) filepath: "vegbank_full_fc_v1.9_pg16_20250924.dump" # path to dump file from PVC mount-point postgresImage: postgres:17 # must match cnpg major version - see below
Important
The restore process will fail unless the major version of the postgres image defined in databaseRestore.postgresImage matches the cnpg major version.