Helm Chart

The FerrisKey chart is published as an OCI artifact at oci://ghcr.io/ferriskey/charts/ferriskey. It deploys the API, the web console, the database migration job, and, unless you turn it off, an embedded PostgreSQL.

Install

Install the chart

The chart is an OCI artifact, so there is no helm repo add step:

helm install ferriskey oci://ghcr.io/ferriskey/charts/ferriskey \
  --namespace ferriskey \
  --create-namespace

Out of the box this brings up an embedded PostgreSQL and generates the database and admin passwords into Secrets.

Check the rollout

kubectl get pods -n ferriskey

Wait for the migration job to complete and the API and webapp pods to reach Running.

Reach the console

The webapp service listens on port 80:

kubectl port-forward -n ferriskey svc/ferriskey-webapp 5555:80

Open http://localhost:5555 and sign in with the admin account.

The embedded PostgreSQL is for trying things out

postgresql.enabled defaults to true so a fresh install works with no dependencies. Do not ship that to production. Point database.host at a managed instance and set postgresql.enabled: false.

Install against an external PostgreSQL

helm install ferriskey oci://ghcr.io/ferriskey/charts/ferriskey \
  --namespace ferriskey --create-namespace \
  --set postgresql.enabled=false \
  --set database.host=postgres.database.svc.cluster.local \
  --set database.passwordSecret.create=false \
  --set database.passwordSecret.name=ferriskey-db-credentials

With database.passwordSecret.create=false, the chart reads the password from the Secret you name, at the key given by database.passwordSecret.key (password by default).

Exposing the deployment

The chart can render either an Ingress or a Gateway API HTTPRoute. Both need publicHost, which is the hostname the console and the API are reached at. Rendering fails with a clear message when it is missing.

publicHost: iam.yourorg.com

ingress:
  enabled: true
  class: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
  tls:
    - hosts:
        - iam.yourorg.com
      secretName: ferriskey-tls
publicHost: iam.yourorg.com

gatewayAPI:
  httpRoute:
    enabled: true
    parentRefs:
      - name: my-gateway
        namespace: gateway-system

Both route / to the webapp and api.server.rootPath (/api by default) to the API, on a single hostname. api.webapp.url and webapp.api.url are derived from that configuration, so you only set them when you serve the two on separate hosts.

Values reference

The chart exposes the full Kubernetes surface for every workload: probes, security contexts, affinity, tolerations, topology spread, extra volumes, and so on. The table below covers the values specific to FerrisKey. The chart README has the exhaustive list.

Top level

KeyDefaultDescription
nameOverridenilOverride the release-derived resource name
publicHostnilPublic hostname. Required when an Ingress or HTTPRoute is enabled

Database

KeyDefaultDescription
database.hostnilPostgreSQL host. Defaults to the embedded instance when it is enabled
database.port5432PostgreSQL port
database.nameferriskeyDatabase name
database.userferriskeyDatabase user
database.passwordSecret.createtrueGenerate the password Secret
database.passwordSecret.namenilExisting Secret to read the password from
database.passwordSecret.keypasswordKey inside that Secret

API

KeyDefaultDescription
api.replicas1API replica count
api.image.repositoryghcr.io/ferriskey/ferriskey-apiAPI image
api.image.tagchart appVersionAPI image tag
api.admin.usernameadminInitial admin username
api.admin.emailadmin@cluster.localInitial admin email
api.admin.passwordSecret.createtrueGenerate the admin password Secret
api.admin.passwordSecret.namenilExisting Secret holding the admin password
api.admin.passwordSecret.keypasswordKey inside that Secret
api.server.port3333Port the API listens on
api.server.rootPath/apiPath prefix. Change it and the probes need updating too
api.server.allowedOrigins[]Browser origins allowed on every route
api.webapp.urlnilConsole URL. Computed from the exposure configuration when unset
api.log.filterinfoLog filter directives
api.log.jsonfalseJSON log output
api.serviceMonitor.enabledfalseCreate a Prometheus Operator ServiceMonitor
api.resources128Mi request / 512Mi limitAPI container resources

Web console

KeyDefaultDescription
webapp.replicas1Console replica count
webapp.image.repositoryghcr.io/ferriskey/ferriskey-webappConsole image
webapp.api.urlnilAPI URL the browser calls. Computed from the exposure configuration when unset
webapp.resources32Mi request / 64Mi limitConsole container resources

Database migrations

KeyDefaultDescription
databaseMigrations.image.repositoryAPI imageImage running sqlx migrate run
databaseMigrations.backoffLimitnilJob backoff limit
databaseMigrations.ttlSecondsAfterFinishednilSet to 0 outside of ArgoCD to clean the job up

Under Helm the migration job runs as a post-install and pre-upgrade hook, so it is recreated on every upgrade rather than patched in place.

Embedded PostgreSQL

KeyDefaultDescription
postgresql.enabledtrueDeploy the bundled PostgreSQL
postgresql.image.repositorypostgresPostgreSQL image
postgresql.image.tag17PostgreSQL image tag
postgresql.persistence.enabledtrueBack the instance with a PVC
postgresql.persistence.resources.requests.storage5GiPVC size
postgresql.persistence.storageClassnilStorage class

Observability

KeyDefaultDescription
opentelemetry.enabledfalseSet ACTIVE_OBSERVABILITY and export traces
opentelemetry.otlpEndpointhttp://tempo:4317OTLP collector endpoint

A production values file

publicHost: iam.yourorg.com

postgresql:
  enabled: false

database:
  host: postgres.database.svc.cluster.local
  name: ferriskey
  user: ferriskey
  passwordSecret:
    create: false
    name: ferriskey-db-credentials
    key: password

api:
  replicas: 3
  admin:
    username: admin
    email: admin@yourorg.com
    passwordSecret:
      create: false
      name: ferriskey-admin
      key: password
  server:
    allowedOrigins:
      - https://iam.yourorg.com
  log:
    json: true
  serviceMonitor:
    enabled: true

webapp:
  replicas: 2

databaseMigrations:
  ttlSecondsAfterFinished: 0

ingress:
  enabled: true
  class: nginx
  tls:
    - hosts:
        - iam.yourorg.com
      secretName: ferriskey-tls
helm upgrade --install ferriskey oci://ghcr.io/ferriskey/charts/ferriskey \
  --namespace ferriskey --create-namespace \
  -f values.yaml

Installing with ArgoCD

ArgoCD does not run Helm hooks, so the ordering has to be expressed with sync waves. Give the Secrets and PostgreSQL wave 0 and the migration job wave 1:

database:
  passwordSecret:
    annotations:
      argocd.argoproj.io/hook: PreSync
      argocd.argoproj.io/sync-wave: "0"

databaseMigrations:
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/sync-wave: "1"

ArgoCD regenerates the admin password Secret on every sync, which shows up as permanent drift. Tell it to ignore that field:

ignoreDifferences:
  - group: ""
    kind: Secret
    name: ferriskey-api-admin
    jsonPointers:
      - /data/password