Skip to content

Deployment

Deployment has two scopes that should not be confused:

  • Local app development — running an app repo (Signals or Aggregator) on your machine for development. Covered in the Installation guides.
  • Cloud deployment — standing up the full stack on AWS EKS from the bluedots-automation repository. That is what this page covers.

For the architecture behind this flow, see Infrastructure & Deployment Architecture; for how images get built and pinned, see CI/CD & Build Pipeline.

ToolMinimumPurpose
aws CLI2.xAWS auth/STS, EKS kubeconfig
tofu (OpenTofu)≥ 1.6provision cloud resources
terragrunt≥ 0.90wraps tofu; per-module state
kubectl≥ 1.24talk to EKS
helm≥ 3.12install the charts
bash, git4.x / 2.xrun install.sh, clone

Also required: an AWS account that can create VPC/EKS/IAM/S3 (plus an S3 bucket for OpenTofu remote state), a GitHub PAT with read:packages (GHCR_PAT) to pull images from GHCR, and DNS control for the public hostnames.

yq and a Makefile are not used — opentofu/aws/<env>/install.sh is the single entrypoint for both infrastructure and Helm.

Terminal window
# pick / create an environment (template/ is the reference)
cp -R opentofu/aws/template opentofu/aws/<env>
cd opentofu/aws/<env>
# authenticate and confirm identity
aws sso login # or export static keys
aws sts get-caller-identity # must succeed
# review environment inputs (region, EKS sizing, hosts, network, SMTP/SMS)
$EDITOR global-values.yaml
# one-shot infra bootstrap:
# create_tf_backend → backup_configs → create_tf_resources → apply_gp3_default_sc
bash install.sh

This creates the remote-state bucket, runs terragrunt run --all apply (network → eks → iam → storage → random_passwords → output-file), writes the EKS kubeconfig, makes gp3 the default StorageClass, and generates the three per-chart values files into the env directory.

Validate:

Terminal window
kubectl config current-context # the new cluster
kubectl get nodes # Ready
kubectl get sc # gp3 (default)
ls common-services-values.yaml signals-values.yaml aggregator-values.yaml
bash install.sh preflight # tooling + cluster + values files

Some values are not auto-generated. Edit global-values.yaml before the final terragrunt apply (hosts, network), and the generated signals-values.yaml / aggregator-values.yaml after they’re written but before deploy (Google Maps key, SMTP user/app-password, MSG91 keys, OpenAI key, admin emails).

The aggregator’s global.signalstack.actingOrgId is a post-Signals step — see step 4.

Deploy in strict dependency order. Either run the whole stack at once, or step through it gating on health.

Terminal window
export GHCR_PAT=ghp_xxxxxxxxxxxx # read:packages
# optional static checks (install nothing)
bash install.sh lint
bash install.sh dry_run
# full ordered deploy
bash install.sh deploy_all_services

deploy_all_services chains: preflight → create_namespaces_and_secrets → deploy_monitoring → deploy_common_services → deploy_signals → deploy_aggregator → fix_acme_issuer_uri.

Step-by-step equivalent (inspect health between releases):

Terminal window
bash install.sh create_namespaces_and_secrets
bash install.sh deploy_common_services # Kong + cert-manager + Postgres + Redis
kubectl -n common-services get pods,pvc # confirm Ready + bound BEFORE continuing
bash install.sh deploy_signals
bash install.sh deploy_aggregator # Keycloak init runs after Postgres — slowest

Each deploy_* runs helm upgrade --install … --wait, so it blocks until that release’s pods are Ready. It does not check cross-namespace dependencies — confirm common-services Postgres/Redis are Ready yourself before deploying Signals/Aggregator.

Terminal window
cd opentofu/aws/<env>
ORG_ID=$(./get-signalstack-org-id.sh) # network_service org id from the seeded DB
# set in aggregator-values.yaml:
# global:
# signalstack:
# actingOrgId: "<ORG_ID>"
bash install.sh deploy_aggregator # redeploy with the value set

Without it, aggregator login fails with SIGNALSTACK_ORG_NOT_REGISTERED.

Terminal window
helm list -A # monitoring, common-services, signals, aggregator = deployed
kubectl get ingress -A # hosts match global-values.yaml
kubectl get clusterissuer letsencrypt-prod
kubectl get certificate -A # READY=True once ACME completes
# the Kong proxy LoadBalancer hostname to point DNS at:
kubectl -n common-services get svc common-services-kong-proxy

Point A/CNAME records for the signals API, signals UI and aggregator hosts at the Kong proxy LoadBalancer. Once DNS resolves and certs are READY=True, the public URLs are live.

Terminal window
bash install.sh cleanup_all_services # helm releases + namespaces, reverse order
bash install.sh destroy_tf_resources # terragrunt run --all destroy (needs AWS creds)

This site is Astro + Starlight, deployed to GitHub Pages via GitHub Actions.

  1. Push the bluedots-docs project to a repository.
  2. In Settings → Pages, set Source = GitHub Actions.
  3. Confirm astro.config.mjs has the right site (no base needed for a custom domain):
    site: 'https://docs-signals-dpg.bluedotseconomy.org',
    A public/CNAME file pins the custom domain across deploys. If instead deploying to a GitHub Pages project URL (<org>.github.io/<repo>), set site to that origin and add base: '/<repo>'.

The bundled workflow at .github/workflows/deploy.yml builds with the official withastro/action and publishes on every push to main. After the first successful run, the site is live at the configured URL.

Terminal window
pnpm install
pnpm dev # local dev server (under /)
pnpm build # production build to dist/
pnpm preview # preview the production build