mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* feat(deploy): add Azure deployment templates (App Service + ACI) Adds a deploy/azure/ lane so Open Design can be deployed to Microsoft Azure from the published runtime image, alongside the existing Docker Compose and Helm options. Covers the App Service + ACI scope of #1028. Two Bicep templates run the same single-port Alpine image used by deploy/docker-compose.yml and charts/open-design: - app-service.bicep: App Service for Containers with managed HTTPS, Always On, and health checks on /api/health. - aci.bicep: a single Azure Container Instances group with a public FQDN and an /api/health liveness probe. These are evaluation deployments: state lives on the container's local disk and is ephemeral. Open Design stores SQLite under OD_DATA_DIR, and SQLite needs real file locking, which the Azure Files (SMB) storage behind both App Service and ACI persistence cannot provide without corruption. App Service sets WEBSITES_ENABLE_APP_SERVICE_STORAGE=false to keep the data dir on local disk. Durable self-hosting stays on the Compose named volume or the Helm PVC. Both wire the daemon's env contract (OD_BIND_HOST/OD_PORT/OD_WEB_PORT/ OD_DATA_DIR/OD_PUBLIC_BASE_URL/OD_ALLOWED_ORIGINS/OD_API_TOKEN) and take the API token as a secure parameter so it never appears in deployment outputs. deploy-azure.sh wraps `az` to create the resource group, generate a token when one isn't supplied, deploy a lane, and print the URL. README.md documents both lanes, the ephemeral-data caveat, and the security trade-offs. deploy/tests/azure-bicep.test.ts guards the runtime contract and that the data dir is never mounted to Azure Files; when the bicep CLI is present it also compiles both templates. * docs(deploy): add ACI health-check example to Azure quick start
148 lines
4.6 KiB
Bicep
148 lines
4.6 KiB
Bicep
// Open Design on Azure Container Instances (ACI) — evaluation deployment.
|
|
//
|
|
// Single serverless container group with a public FQDN. State lives on the
|
|
// container's local disk, which is EPHEMERAL — it is reset whenever the group
|
|
// is restarted or recreated. This lane is for evaluation and demos.
|
|
//
|
|
// Why no persistent volume: Open Design stores SQLite under OD_DATA_DIR, and
|
|
// SQLite needs real file locking. ACI's only persistent volume type is Azure
|
|
// Files (SMB), where SQLite WAL/locking is unsupported and corrupts. So we
|
|
// keep the data dir on the container's local filesystem.
|
|
//
|
|
// SECURITY: ACI exposes the port over plain HTTP (no managed TLS). Access is
|
|
// gated by OD_API_TOKEN and browser traffic by OD_ALLOWED_ORIGINS. For
|
|
// internet-facing HTTPS use app-service.bicep.
|
|
|
|
@description('Base name for the deployment. A globally-unique suffix is appended to the DNS label.')
|
|
param name string = 'open-design'
|
|
|
|
@description('Azure region. Defaults to the resource group location.')
|
|
param location string = resourceGroup().location
|
|
|
|
@description('Container image to run. Pin to a digest (image@sha256:...) for production.')
|
|
param image string = 'docker.io/vanjayak/open-design:latest'
|
|
|
|
@secure()
|
|
@minLength(32)
|
|
@description('Required API token guarding the daemon. Generate one with: openssl rand -hex 32')
|
|
param apiToken string
|
|
|
|
@description('vCPU cores allocated to the container.')
|
|
param cpuCores int = 1
|
|
|
|
@description('Memory (GiB) allocated to the container. Raise for large exports or concurrent agent runs.')
|
|
param memoryInGb string = '1.5'
|
|
|
|
@description('Node.js heap cap passed to the container via NODE_OPTIONS.')
|
|
param nodeOptions string = '--max-old-space-size=192'
|
|
|
|
@description('Extra browser origins allowed to call /api, comma-separated. The container FQDN is always allowed.')
|
|
param extraAllowedOrigins string = ''
|
|
|
|
// Runtime invariants of the image, not user-facing knobs.
|
|
var containerPort = 7456
|
|
var dataDir = '/app/.od'
|
|
|
|
// ACI exposes the port verbatim (no 80 -> 7456 mapping), so the base URL
|
|
// carries the container port.
|
|
var dnsNameLabel = '${name}-${uniqueString(resourceGroup().id)}'
|
|
var fqdn = '${dnsNameLabel}.${toLower(location)}.azurecontainer.io'
|
|
var publicBaseUrl = 'http://${fqdn}:${containerPort}'
|
|
var allowedOrigins = empty(extraAllowedOrigins) ? publicBaseUrl : '${publicBaseUrl},${extraAllowedOrigins}'
|
|
|
|
resource containerGroup 'Microsoft.ContainerInstance/containerGroups@2023-05-01' = {
|
|
name: '${name}-aci'
|
|
location: location
|
|
properties: {
|
|
osType: 'Linux'
|
|
restartPolicy: 'Always'
|
|
ipAddress: {
|
|
type: 'Public'
|
|
dnsNameLabel: dnsNameLabel
|
|
ports: [
|
|
{
|
|
protocol: 'TCP'
|
|
port: containerPort
|
|
}
|
|
]
|
|
}
|
|
containers: [
|
|
{
|
|
name: 'open-design'
|
|
properties: {
|
|
image: image
|
|
ports: [
|
|
{
|
|
protocol: 'TCP'
|
|
port: containerPort
|
|
}
|
|
]
|
|
resources: {
|
|
requests: {
|
|
cpu: cpuCores
|
|
memoryInGB: json(memoryInGb)
|
|
}
|
|
}
|
|
environmentVariables: [
|
|
{
|
|
name: 'OD_BIND_HOST'
|
|
value: '0.0.0.0'
|
|
}
|
|
{
|
|
name: 'OD_PORT'
|
|
value: '${containerPort}'
|
|
}
|
|
{
|
|
name: 'OD_WEB_PORT'
|
|
value: '${containerPort}'
|
|
}
|
|
{
|
|
name: 'OD_DATA_DIR'
|
|
value: dataDir
|
|
}
|
|
{
|
|
name: 'OD_PUBLIC_BASE_URL'
|
|
value: publicBaseUrl
|
|
}
|
|
{
|
|
name: 'OD_ALLOWED_ORIGINS'
|
|
value: allowedOrigins
|
|
}
|
|
{
|
|
name: 'NODE_ENV'
|
|
value: 'production'
|
|
}
|
|
{
|
|
name: 'NODE_OPTIONS'
|
|
value: nodeOptions
|
|
}
|
|
{
|
|
name: 'OD_API_TOKEN'
|
|
secureValue: apiToken
|
|
}
|
|
]
|
|
livenessProbe: {
|
|
httpGet: {
|
|
path: '/api/health'
|
|
port: containerPort
|
|
scheme: 'HTTP'
|
|
}
|
|
initialDelaySeconds: 20
|
|
periodSeconds: 30
|
|
failureThreshold: 3
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
@description('Public URL of the deployed container (plain HTTP, port included).')
|
|
output appUrl string = publicBaseUrl
|
|
|
|
@description('Health endpoint to poll after deployment.')
|
|
output healthUrl string = '${publicBaseUrl}/api/health'
|
|
|
|
@description('Fully-qualified domain name assigned to the container group.')
|
|
output fqdn string = fqdn
|