Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/hub-infrastructure.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Hub infrastructure

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

defaults:
run:
working-directory: infrastructure/azure

jobs:
terraform:
name: Terraform ${{ matrix.command }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
command: ["fmt -check -recursive", "validate", "test"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
with:
terraform_version: 1.13.4
terraform_wrapper: false
- run: terraform init -backend=false -input=false -lockfile=readonly
- run: terraform ${{ matrix.command }}

release:
name: Release orchestration tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: python3 -m unittest discover -s . -p 'test_*.py' -v
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Commits and pull requests follow the [contribution conventions](../CONTRIBUTING.
## Project guidance

- [Hub documentation](../hub/docs/README.md) covers starting and checking the TypeScript API and frontend, and what is not built yet.
- [Azure Hub installation](../infrastructure/README.md) covers Terraform, release ordering, ownership, and recovery.
- [Starter instructions](../starter/AGENTS.md) and [setup guide](../starter/SETUP.md) cover the copyable prototype app and skills.
- [Build action setup](../CONTRIBUTING.md#replica-build-action) covers the shared action.
- [Controls documentation](../hub/docs/prototype-controls.md) covers the shared engine and definition schema owned by Hub.
Expand Down
2 changes: 2 additions & 0 deletions hub/docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ The setup lock does not serialize the rest of a release or make jobs exactly-onc

CI checks the image against disposable PostgreSQL, including setup, frontend/API serving, readiness, and termination. This is not a supported local-production installation. The existing devcontainer remains the development environment.

The [Azure reference installation](../../infrastructure/README.md) uses Terraform and a serialized maintenance-window release. It consumes the same image and runtime settings without adding Azure deployment logic to Hub.

An actual cloud deployment must additionally verify credentials, networking, data survival, backup and restore, domain configuration, and release ordering in an authorized environment. Never commit secrets, tenant-specific parameter files, or generated service addresses.
9 changes: 9 additions & 0 deletions infrastructure/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.tools/
**/__pycache__/
**/.terraform/
**/*.tfstate*
**/*.tfplan
**/*.tfvars
**/*.tfvars.json
**/backend.hcl
**/crash.log
146 changes: 146 additions & 0 deletions infrastructure/README.md

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions infrastructure/azure/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions infrastructure/azure/backend.hcl.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
resource_group_name = "your-existing-terraform-state-group"
storage_account_name = "youruniquestateaccount"
container_name = "terraform"
key = "replica-hub.tfstate"
use_azuread_auth = true
use_oidc = true
197 changes: 197 additions & 0 deletions infrastructure/azure/compute.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
resource "azurerm_container_app_job" "setup" {
name = "${var.name}-setup"
location = var.location
resource_group_name = azurerm_resource_group.hub.name
container_app_environment_id = azurerm_container_app_environment.hub.id
workload_profile_name = "Consumption"
replica_timeout_in_seconds = 1800
replica_retry_limit = 0
tags = local.tags

manual_trigger_config {
parallelism = 1
replica_completion_count = 1
}

identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.hub.id]
}

dynamic "registry" {
for_each = var.acr_id == null ? [] : [var.acr_id]
content {
server = split("/", var.image)[0]
identity = azurerm_user_assigned_identity.hub.id
}
}

secret {
name = "database-url"
value = local.database_url
}

dynamic "secret" {
for_each = local.secret_names
content {
name = "runtime-${substr(sha256(secret.value), 0, 12)}"
value = var.runtime_secrets[secret.value]
}
}

template {
container {
name = "setup"
image = var.image
cpu = 0.5
memory = "1Gi"
command = ["node", "dist/server/scripts/setup/index.js"]

dynamic "env" {
for_each = merge(local.runtime_environment, { RUNTIME_MODE = "setup" })
content {
name = env.key
value = env.value
}
}

env {
name = "DATABASE_URL"
secret_name = "database-url"
}

dynamic "env" {
for_each = local.secret_names
content {
name = env.value
secret_name = "runtime-${substr(sha256(env.value), 0, 12)}"
}
}
}
}

depends_on = [
azurerm_postgresql_flexible_server_database.hub,
azurerm_postgresql_flexible_server_configuration.tls,
azurerm_role_assignment.artifacts,
azurerm_role_assignment.image_pull,
]
}

resource "azurerm_container_app" "hub" {
count = var.deploy_hub ? 1 : 0
name = "${var.name}-hub"
resource_group_name = azurerm_resource_group.hub.name
container_app_environment_id = azurerm_container_app_environment.hub.id
workload_profile_name = "Consumption"
revision_mode = "Multiple"
tags = local.tags

identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.hub.id]
}

dynamic "registry" {
for_each = var.acr_id == null ? [] : [var.acr_id]
content {
server = split("/", var.image)[0]
identity = azurerm_user_assigned_identity.hub.id
}
}

secret {
name = "database-url"
value = local.database_url
}

dynamic "secret" {
for_each = local.secret_names
content {
name = "runtime-${substr(sha256(secret.value), 0, 12)}"
value = var.runtime_secrets[secret.value]
}
}

ingress {
external_enabled = true
allow_insecure_connections = false
target_port = 3001
transport = "http"
traffic_weight {
latest_revision = true
percentage = 100
}
}

template {
min_replicas = 1
max_replicas = 1
termination_grace_period_seconds = 60

container {
name = "hub"
image = var.image
cpu = 0.5
memory = "1Gi"

dynamic "env" {
for_each = local.runtime_environment
content {
name = env.key
value = env.value
}
}

env {
name = "DATABASE_URL"
secret_name = "database-url"
}

dynamic "env" {
for_each = local.secret_names
content {
name = env.value
secret_name = "runtime-${substr(sha256(env.value), 0, 12)}"
}
}

startup_probe {
transport = "HTTP"
port = 3001
path = "/api/health"
interval_seconds = 5
failure_count_threshold = 30
}
readiness_probe {
transport = "HTTP"
port = 3001
path = "/api/ready"
interval_seconds = 5
success_count_threshold = 1
timeout = 3
}
liveness_probe {
transport = "HTTP"
port = 3001
path = "/api/health"
interval_seconds = 10
timeout = 3
}
}
}

lifecycle {
prevent_destroy = true
}

depends_on = [azurerm_container_app_job.setup]
}

resource "azurerm_container_app_custom_domain" "hub" {
count = var.deploy_hub && var.custom_hostname != null ? 1 : 0
name = var.custom_hostname
container_app_id = azurerm_container_app.hub[0].id
container_app_environment_certificate_id = var.certificate_id
certificate_binding_type = "SniEnabled"
}
Loading