Skip to content

Infrastructure

Logbook runs on Azure. Infrastructure is one OpenTofu root in the monorepo's infra/ directory with a module per deployable, applied by hand with a small wrapper script. One plan shows a whole environment. Shared, organisation-level resources come from a separate repository.

The modules

ModuleWhat it provisionsHosts
modules/apiApp Service (container from the shared registry), PostgreSQL Flexible Server, Redis, Service Bus, Neo4j on Container Instances, Blob storage, Log Analytics and Application Insights, Key Vault, DNS recordsapi.dev.logbook.social, api.logbook.social
modules/webStatic Web App for the landing page, Clerk DNS recordsdev.logbook.social, logbook.social
modules/docsStatic Web App for this sitedocs.dev.logbook.social, docs.logbook.social

dev and prod are OpenTofu workspaces over one state key (logbook.tfstate) in the shared state storage account. modules/shared/ holds pieces the app modules reuse, such as ci-secret (a Key Vault secret plus a read grant for CI). Resource names follow logbook-<app>-<env>-<thing>, for example logbook-api-dev-kv.

What comes from elsewhere

The apptic/devops repository owns the things shared across Apptic Labs services: the per-environment resource groups (logbook-dev-rg, logbook-prod-rg), the logbook.social DNS zone, the container registry appticlabs, and the GitHub OIDC application that CI logs in with. The Logbook stacks read those by name and never create them. The Static Web App module is also sourced from that repository by relative path, so apptic/devops needs to be checked out next to apptic/logbook.

Running OpenTofu

The tofu wrapper logs in to Azure if needed, selects the workspace, and drives plan and apply for everything at once.

bash
cd infra

./tofu init             # once per clone
./tofu -e dev plan      # writes plan.out
./tofu -e dev apply     # applies plan.out, then deletes it
./tofu -e dev output    # show outputs

Always plan before apply and read the plan. apply only ever consumes a saved plan. Static Web App applies take a few minutes because custom-domain validation waits for DNS to propagate.

Secrets

Secrets live in Key Vault, not in OpenTofu variables and not in GitHub.

  • modules/api creates logbook-api-<env>-kv and the App Service reads its secrets as Key Vault references in app settings.

  • Third-party keys (Stripe, Resend, Clerk, PostHog) are created as placeholders with the value PLACEHOLDER_REPLACE_ME. Seed them once with the Azure CLI. CI fails with a clear message if it ever reads a placeholder.

    bash
    az keyvault secret set --vault-name logbook-api-dev-kv --name web-clerk-publishable-key --value pk_test_...
  • The web and docs modules write their Static Web App deployment tokens into the same vault (web-swa-deployment-token, docs-swa-deployment-token) and grant the CI identity read access to exactly those secrets.

  • GitHub holds only AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_SUBSCRIPTION_ID, which are enough for the OIDC login.

CI workflows

All workflows are in .github/workflows at the repo root and are path-filtered so a change to one app does not deploy the others.

WorkflowTriggerWhat it does
api-pr-tests.ymlpull request touching apps/apiinstalls and runs the API test suite
api-deploy.ymlpush to main touching apps/apitests, builds the image, pushes it to the registry, tags a release candidate, migrates and deploys dev
api-release.ymlpush to mainrelease-please opens or merges the release PR; on merge it promotes the RC image, migrates and deploys prod
web-deploy.ymlpush to main touching apps/webbuilds the landing page and uploads it to the dev Static Web App; prod by manual dispatch
docs-deploy.ymlpush to main touching apps/docssame for this site

Deploy jobs log in with azure/login, read what they need from Key Vault through the keyvault-secrets composite action, and then build and upload. See .github/README.md in the repo for the full API release flow.

Built with VitePress