Skip to content

Build Your Bundle

A UDS Bundle is a single deployable artifact that captures your environment’s configuration alongside all packages and images. You’ll create two files: a uds-bundle.yaml that defines what to deploy and how to configure it, and a uds-config.yaml that supplies runtime values (credentials, certificates, domain names).

UDS Core is published in multiple flavors that differ in the source registry for container images:

FlavorImage SourceUse Case
upstreamPublic registries (Docker Hub, GHCR)Default; utilizes common upstream container images
registry1IronBank / Registry OneDoD environments requiring hardened, Iron Bank-sourced images
unicornDefense Unicorns private registryFIPS-compliant hardened images; reserved for Defense Unicorns customers

Choose the flavor that matches your environment’s registry access and compliance requirements. The bundle ref encodes the flavor:

Bundle ref format
0.X.Y-upstream # upstream flavor
0.X.Y-registry1 # registry1 flavor
0.X.Y-unicorn # unicorn flavor

Start with a minimal uds-bundle.yaml. You’ll add overrides to this in the sections below.

uds-bundle.yaml
kind: UDSBundle
metadata:
name: my-uds-core
description: Production UDS Core deployment
version: 0.1.0
packages:
# Enables Zarf in your cluster
- name: init
repository: ghcr.io/zarf-dev/packages/init
ref: v0.73.0
- name: core
repository: registry.defenseunicorns.com/public/core
ref: 0.62.0-upstream

Unlike the local demo bundle, the production bundle does not include a uds-k3d package; your cluster already exists and is managed separately.

The example below uses access keys, which work with AWS S3, MinIO, and any S3-compatible provider. For Azure and GCP, the override structure differs. See the Loki cloud deployment guides for provider-specific examples.

uds-bundle.yaml
packages:
- name: core
overrides:
loki:
loki:
variables:
- name: LOKI_CHUNKS_BUCKET
description: "Object storage bucket for Loki chunks"
path: loki.storage.bucketNames.chunks
- name: LOKI_ADMIN_BUCKET
description: "Object storage bucket for Loki admin"
path: loki.storage.bucketNames.admin
- name: LOKI_S3_REGION
description: "Object storage region"
path: loki.storage.s3.region
- name: LOKI_ACCESS_KEY_ID
description: "Object storage access key ID"
path: loki.storage.s3.accessKeyId
sensitive: true
- name: LOKI_SECRET_ACCESS_KEY
description: "Object storage secret access key"
path: loki.storage.s3.secretAccessKey
sensitive: true
values:
- path: loki.storage.type
value: "s3"
- path: loki.storage.s3.endpoint
value: "" # leave empty for AWS; set for MinIO or other S3-compatible providers
uds-config.yaml
variables:
core:
loki_chunks_bucket: "your-loki-chunks-bucket"
loki_admin_bucket: "your-loki-admin-bucket"
loki_s3_region: "us-east-1"
loki_access_key_id: "your-access-key-id"
loki_secret_access_key: "your-secret-access-key"

The example below uses AWS S3. For other providers (Azure, GCP), the override structure and credentials format differ. See Velero’s supported providers for provider-specific configuration.

uds-bundle.yaml
packages:
- name: core
overrides:
velero:
velero:
variables:
- name: VELERO_CLOUD_CREDENTIALS
description: "Velero cloud credentials file content"
path: credentials.secretContents.cloud
sensitive: true
values:
- path: "configuration.backupStorageLocation"
value:
- name: default
provider: aws
bucket: "<your-velero-bucket>"
config:
region: "<your-region>"
s3ForcePathStyle: true
s3Url: "<your-s3-endpoint>"
credential:
name: "velero-bucket-credentials"
key: "cloud"
uds-config.yaml
variables:
core:
velero_cloud_credentials: |
[default]
aws_access_key_id=your-access-key-id
aws_secret_access_key=your-secret-access-key

Expose the TLS certificate and key for each gateway as bundle variables so they can be supplied at deploy time without hardcoding them in the bundle.

uds-bundle.yaml
packages:
- name: core
overrides:
istio-admin-gateway:
uds-istio-config:
variables:
- name: ADMIN_TLS_CERT
description: "Base64-encoded TLS cert chain for admin gateway"
path: tls.cert
- name: ADMIN_TLS_KEY
description: "Base64-encoded TLS key for admin gateway"
path: tls.key
sensitive: true
istio-tenant-gateway:
uds-istio-config:
variables:
- name: TENANT_TLS_CERT
description: "Base64-encoded TLS cert chain for tenant gateway"
path: tls.cert
- name: TENANT_TLS_KEY
description: "Base64-encoded TLS key for tenant gateway"
path: tls.key
sensitive: true
uds-config.yaml
variables:
core:
admin_tls_cert: "LS0t..." # base64-encoded full cert chain
admin_tls_key: "LS0t..." # base64-encoded private key
tenant_tls_cert: "LS0t..."
tenant_tls_key: "LS0t..."

Restrict the admin gateway to internal networks

Section titled “Restrict the admin gateway to internal networks”

UDS Core deploys two ingress gateways by default: the Tenant Gateway for end-user application traffic and the Admin Gateway for platform-administration interfaces such as Grafana and the Keycloak admin console. The Admin Gateway should not be reachable from the public internet.

To enforce this at the load balancer layer, most load balancer controllers require you to override service annotations. The exact annotations will vary depending on your cloud provider; the example below uses the AWS Load Balancer Controller to provision an internal NLB:

uds-bundle.yaml
packages:
- name: core
overrides:
istio-admin-gateway:
gateway:
values:
# Provision the admin gateway's load balancer as an internal NLB
# so the admin domain is reachable only from inside the VPC.
- path: service.annotations
value:
# Configure to use an internal (not internet-facing) loadbalancer
service.beta.kubernetes.io/aws-load-balancer-scheme: "internal"
# Add additional annotations as necessary for your configuration

Use the equivalent annotations for your provider:

  • AWS Load Balancer Controller - set service.beta.kubernetes.io/aws-load-balancer-scheme: "internal"
  • Azure Kubernetes Service - set service.beta.kubernetes.io/azure-load-balancer-internal: "true"
  • Google Kubernetes Engine - set networking.gke.io/load-balancer-type: "Internal"
  • MetalLB - set metallb.io/address-pool: <pool-name> to pin the gateway’s load balancer IP to a dedicated internal address pool (configure the pool itself via IPAddressPool)

Disable Keycloak’s embedded dev-mode database and connect it to your external database. Pass the connection details as variables.

uds-bundle.yaml
packages:
- name: core
overrides:
keycloak:
keycloak:
values:
- path: devMode
value: false
variables:
- name: KEYCLOAK_DB_HOST
path: postgresql.host
- name: KEYCLOAK_DB_USERNAME
path: postgresql.username
- name: KEYCLOAK_DB_DATABASE
path: postgresql.database
- name: KEYCLOAK_DB_PASSWORD
path: postgresql.password
sensitive: true
uds-config.yaml
variables:
core:
keycloak_db_host: "your-db-host" # hostname or IP of your database server
keycloak_db_username: "keycloak" # database user created in provision-services step
keycloak_db_database: "keycloak" # database name created in provision-services step
keycloak_db_password: "your-db-password" # password for the database user

Some UDS Core components are disabled by default and must be explicitly enabled:

Enable if your distribution does not include a metrics server (e.g., a bare RKE2 cluster without built-in metrics):

uds-bundle.yaml
packages:
- name: core
optionalComponents:
- metrics-server

With all overrides combined, here are the final files:

uds-bundle.yaml
kind: UDSBundle
metadata:
name: my-uds-core
description: Production UDS Core deployment
version: 0.1.0
packages:
- name: init
repository: ghcr.io/zarf-dev/packages/init
ref: v0.73.0
- name: core
repository: registry.defenseunicorns.com/public/core
ref: 0.62.0-upstream
overrides:
loki:
loki:
variables:
- name: LOKI_CHUNKS_BUCKET
description: "Object storage bucket for Loki chunks"
path: loki.storage.bucketNames.chunks
- name: LOKI_ADMIN_BUCKET
description: "Object storage bucket for Loki admin"
path: loki.storage.bucketNames.admin
- name: LOKI_S3_REGION
description: "Object storage region"
path: loki.storage.s3.region
- name: LOKI_ACCESS_KEY_ID
description: "Object storage access key ID"
path: loki.storage.s3.accessKeyId
sensitive: true
- name: LOKI_SECRET_ACCESS_KEY
description: "Object storage secret access key"
path: loki.storage.s3.secretAccessKey
sensitive: true
values:
- path: loki.storage.type
value: "s3"
- path: loki.storage.s3.endpoint
value: ""
velero:
velero:
variables:
- name: VELERO_CLOUD_CREDENTIALS
description: "Velero cloud credentials file content"
path: credentials.secretContents.cloud
sensitive: true
values:
- path: "configuration.backupStorageLocation"
value:
- name: default
provider: aws
bucket: "<your-velero-bucket>"
config:
region: "<your-region>"
s3ForcePathStyle: true
s3Url: "<your-s3-endpoint>"
credential:
name: "velero-bucket-credentials"
key: "cloud"
istio-admin-gateway:
uds-istio-config:
variables:
- name: ADMIN_TLS_CERT
description: "Base64-encoded TLS cert chain for admin gateway"
path: tls.cert
- name: ADMIN_TLS_KEY
description: "Base64-encoded TLS key for admin gateway"
path: tls.key
sensitive: true
istio-tenant-gateway:
uds-istio-config:
variables:
- name: TENANT_TLS_CERT
description: "Base64-encoded TLS cert chain for tenant gateway"
path: tls.cert
- name: TENANT_TLS_KEY
description: "Base64-encoded TLS key for tenant gateway"
path: tls.key
sensitive: true
keycloak:
keycloak:
values:
- path: devMode
value: false
variables:
- name: KEYCLOAK_DB_HOST
path: postgresql.host
- name: KEYCLOAK_DB_USERNAME
path: postgresql.username
- name: KEYCLOAK_DB_DATABASE
path: postgresql.database
- name: KEYCLOAK_DB_PASSWORD
path: postgresql.password
sensitive: true
uds-config.yaml
shared:
domain: "yourdomain.com"
variables:
core:
# TLS (base64-encoded full cert chains)
admin_tls_cert: "LS0t..."
admin_tls_key: "LS0t..."
tenant_tls_cert: "LS0t..."
tenant_tls_key: "LS0t..."
# Loki object storage
loki_chunks_bucket: "your-loki-chunks-bucket"
loki_admin_bucket: "your-loki-admin-bucket"
loki_s3_region: "us-east-1"
loki_access_key_id: "your-access-key-id"
loki_secret_access_key: "your-secret-access-key"
# Velero backup storage
velero_cloud_credentials: |
[default]
aws_access_key_id=your-access-key-id
aws_secret_access_key=your-secret-access-key
# Keycloak database
keycloak_db_host: "your-db-host" # hostname or IP of your database server
keycloak_db_username: "keycloak" # database user created in provision-services step
keycloak_db_database: "keycloak" # database name created in provision-services step
keycloak_db_password: "your-db-password" # password for the database user

Once your configuration files are ready, create the deployable bundle artifact.

  1. Create the bundle

    Terminal window
    uds create --confirm

    This command pulls all referenced packages and their images, then packages them into a single archive. Depending on network speed and package sizes, this can take several minutes on first run.

    The output is a file named:

    Output
    uds-bundle-<name>-<arch>-<version>.tar.zst
  2. Inspect the bundle (optional)

    Terminal window
    uds inspect uds-bundle-my-uds-core-*.tar.zst

    This lists the packages included in the bundle and their versions, letting you confirm the contents before deploying.