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
| Module | What it provisions | Hosts |
|---|---|---|
modules/api | App 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 records | api.dev.logbook.social, api.logbook.social |
modules/web | Static Web App for the landing page, Clerk DNS records | dev.logbook.social, logbook.social |
modules/docs | Static Web App for this site | docs.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.
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 outputsAlways 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/apicreateslogbook-api-<env>-kvand 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.bashaz 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, andAZURE_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.
| Workflow | Trigger | What it does |
|---|---|---|
api-pr-tests.yml | pull request touching apps/api | installs and runs the API test suite |
api-deploy.yml | push to main touching apps/api | tests, builds the image, pushes it to the registry, tags a release candidate, migrates and deploys dev |
api-release.yml | push to main | release-please opens or merges the release PR; on merge it promotes the RC image, migrates and deploys prod |
web-deploy.yml | push to main touching apps/web | builds the landing page and uploads it to the dev Static Web App; prod by manual dispatch |
docs-deploy.yml | push to main touching apps/docs | same 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.