mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
Merge 4270bea576 into 53fb175855
This commit is contained in:
commit
7a98d2d71d
3 changed files with 406 additions and 0 deletions
58
deploy/azure/azure-pipelines.yml
Normal file
58
deploy/azure/azure-pipelines.yml
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
trigger: none
|
||||
|
||||
parameters:
|
||||
- name: azureServiceConnection
|
||||
type: string
|
||||
default: '<your-azure-service-connection>'
|
||||
|
||||
variables:
|
||||
vmImageName: ubuntu-latest
|
||||
resourceGroupName: open-design-aci
|
||||
deploymentName: open-design-aci
|
||||
location: eastus
|
||||
templateFile: deploy/azure/container-instance.bicep
|
||||
openDesignImage: docker.io/vanjayak/open-design:latest
|
||||
dnsNameLabel: open-design-$(Build.BuildId)
|
||||
browserOrigin: https://od.example.com
|
||||
|
||||
pool:
|
||||
vmImage: $(vmImageName)
|
||||
|
||||
steps:
|
||||
- checkout: self
|
||||
|
||||
- task: AzureCLI@2
|
||||
displayName: Deploy Open Design to Azure Container Instances
|
||||
inputs:
|
||||
azureSubscription: ${{ parameters.azureServiceConnection }}
|
||||
scriptType: bash
|
||||
scriptLocation: inlineScript
|
||||
useGlobalConfig: false
|
||||
inlineScript: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${OD_API_TOKEN:-}" ]; then
|
||||
echo "Set OD_API_TOKEN as a secret pipeline variable before running this pipeline." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(browserOrigin)" = "https://od.example.com" ]; then
|
||||
echo "Set browserOrigin to the TLS origin served by your authenticated reverse proxy." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
az group create \
|
||||
--name "$(resourceGroupName)" \
|
||||
--location "$(location)"
|
||||
|
||||
az deployment group create \
|
||||
--name "$(deploymentName)" \
|
||||
--resource-group "$(resourceGroupName)" \
|
||||
--template-file "$(templateFile)" \
|
||||
--parameters \
|
||||
odApiToken="$OD_API_TOKEN" \
|
||||
image="$(openDesignImage)" \
|
||||
dnsNameLabel="$(dnsNameLabel)" \
|
||||
allowedOrigins="$(browserOrigin)"
|
||||
env:
|
||||
OD_API_TOKEN: $(OD_API_TOKEN)
|
||||
176
deploy/azure/container-instance.bicep
Normal file
176
deploy/azure/container-instance.bicep
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
targetScope = 'resourceGroup'
|
||||
|
||||
@description('Azure region for the Open Design container group and storage account.')
|
||||
param location string = resourceGroup().location
|
||||
|
||||
@description('Container group name.')
|
||||
param containerGroupName string = 'open-design'
|
||||
|
||||
@description('DNS label for the Azure Container Instances upstream endpoint. Must be unique in the selected region.')
|
||||
param dnsNameLabel string = toLower('open-design-${uniqueString(resourceGroup().id, location)}')
|
||||
|
||||
@description('Open Design container image.')
|
||||
param image string = 'docker.io/vanjayak/open-design:latest'
|
||||
|
||||
@secure()
|
||||
@description('Required Open Design API token. Generate with: openssl rand -hex 32')
|
||||
param odApiToken string
|
||||
|
||||
@description('Comma-separated browser-visible origins allowed by the daemon. Set this to the authenticated reverse proxy origin, for example https://od.example.com.')
|
||||
param allowedOrigins string = ''
|
||||
|
||||
@description('Node.js heap cap inside the container.')
|
||||
param nodeOptions string = '--max-old-space-size=192'
|
||||
|
||||
@description('CPU cores requested by the container.')
|
||||
@minValue(1)
|
||||
param cpuCores int = 1
|
||||
|
||||
@description('Memory requested by the container in GiB.')
|
||||
@minValue(1)
|
||||
param memoryInGB int = 1
|
||||
|
||||
@description('Azure Files share quota in GiB for persistent Open Design data.')
|
||||
@minValue(1)
|
||||
@maxValue(5120)
|
||||
param fileShareQuotaGB int = 10
|
||||
|
||||
var storageAccountName = take(toLower('od${uniqueString(resourceGroup().id, location)}'), 24)
|
||||
var fileShareName = 'opendesigndata'
|
||||
var appPort = 7456
|
||||
|
||||
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
|
||||
name: storageAccountName
|
||||
location: location
|
||||
sku: {
|
||||
name: 'Standard_LRS'
|
||||
}
|
||||
kind: 'StorageV2'
|
||||
properties: {
|
||||
allowBlobPublicAccess: false
|
||||
minimumTlsVersion: 'TLS1_2'
|
||||
supportsHttpsTrafficOnly: true
|
||||
}
|
||||
}
|
||||
|
||||
resource fileService 'Microsoft.Storage/storageAccounts/fileServices@2023-05-01' = {
|
||||
parent: storageAccount
|
||||
name: 'default'
|
||||
}
|
||||
|
||||
resource dataShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2023-05-01' = {
|
||||
parent: fileService
|
||||
name: fileShareName
|
||||
properties: {
|
||||
accessTier: 'TransactionOptimized'
|
||||
shareQuota: fileShareQuotaGB
|
||||
}
|
||||
}
|
||||
|
||||
resource containerGroup 'Microsoft.ContainerInstance/containerGroups@2023-05-01' = {
|
||||
name: containerGroupName
|
||||
location: location
|
||||
properties: {
|
||||
osType: 'Linux'
|
||||
restartPolicy: 'Always'
|
||||
sku: 'Standard'
|
||||
ipAddress: {
|
||||
type: 'Public'
|
||||
dnsNameLabel: dnsNameLabel
|
||||
ports: [
|
||||
{
|
||||
protocol: 'TCP'
|
||||
port: appPort
|
||||
}
|
||||
]
|
||||
}
|
||||
containers: [
|
||||
{
|
||||
name: 'open-design'
|
||||
properties: {
|
||||
image: image
|
||||
ports: [
|
||||
{
|
||||
protocol: 'TCP'
|
||||
port: appPort
|
||||
}
|
||||
]
|
||||
environmentVariables: [
|
||||
{
|
||||
name: 'NODE_ENV'
|
||||
value: 'production'
|
||||
}
|
||||
{
|
||||
name: 'NODE_OPTIONS'
|
||||
value: nodeOptions
|
||||
}
|
||||
{
|
||||
name: 'OD_BIND_HOST'
|
||||
value: '0.0.0.0'
|
||||
}
|
||||
{
|
||||
name: 'OD_PORT'
|
||||
value: string(appPort)
|
||||
}
|
||||
{
|
||||
name: 'OD_WEB_PORT'
|
||||
value: string(appPort)
|
||||
}
|
||||
{
|
||||
name: 'OD_DATA_DIR'
|
||||
value: '/app/.od'
|
||||
}
|
||||
{
|
||||
name: 'OD_ALLOWED_ORIGINS'
|
||||
value: allowedOrigins
|
||||
}
|
||||
{
|
||||
name: 'OD_API_TOKEN'
|
||||
secureValue: odApiToken
|
||||
}
|
||||
]
|
||||
resources: {
|
||||
requests: {
|
||||
cpu: cpuCores
|
||||
memoryInGB: memoryInGB
|
||||
}
|
||||
}
|
||||
volumeMounts: [
|
||||
{
|
||||
name: 'open-design-data'
|
||||
mountPath: '/app/.od'
|
||||
readOnly: false
|
||||
}
|
||||
]
|
||||
livenessProbe: {
|
||||
httpGet: {
|
||||
path: '/api/health'
|
||||
port: appPort
|
||||
scheme: 'http'
|
||||
}
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
failureThreshold: 3
|
||||
timeoutSeconds: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
volumes: [
|
||||
{
|
||||
name: 'open-design-data'
|
||||
azureFile: {
|
||||
shareName: dataShare.name
|
||||
storageAccountName: storageAccount.name
|
||||
storageAccountKey: listKeys(storageAccount.id, storageAccount.apiVersion).keys[0].value
|
||||
readOnly: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
output daemonFqdn string = containerGroup.properties.ipAddress.fqdn
|
||||
output proxyUpstreamUrl string = 'http://${containerGroup.properties.ipAddress.fqdn}:${appPort}'
|
||||
output storageAccountName string = storageAccount.name
|
||||
output fileShareName string = dataShare.name
|
||||
172
docs/deployment/cloud/azure.md
Normal file
172
docs/deployment/cloud/azure.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
# Azure Container Instances
|
||||
|
||||
This guide deploys the Docker image to Azure Container Instances (ACI) with an Azure Files share mounted at `/app/.od` for persistent Open Design data.
|
||||
|
||||
ACI is the daemon upstream in this topology. The browser-facing app URL must be served by an authenticated TLS reverse proxy that forwards traffic to ACI, injects the daemon bearer token on `/api/*` requests, and sends a browser origin listed in `OD_ALLOWED_ORIGINS`.
|
||||
|
||||
## Before You Start
|
||||
|
||||
- Azure CLI installed and signed in
|
||||
- Permission to create a resource group, storage account, file share, and container group
|
||||
- A public Docker image, or an image in a registry that ACI can pull
|
||||
|
||||
## Step 1: Choose Names
|
||||
|
||||
```bash
|
||||
export RESOURCE_GROUP=open-design-aci
|
||||
export LOCATION=eastus
|
||||
export DEPLOYMENT_NAME=open-design-aci
|
||||
export DNS_LABEL=open-design-$RANDOM
|
||||
export BROWSER_ORIGIN=https://od.example.com
|
||||
export OD_API_TOKEN="$(openssl rand -hex 32)"
|
||||
```
|
||||
|
||||
The DNS label must be unique in the Azure region. `BROWSER_ORIGIN` is the HTTPS origin users will open after a trusted proxy is in front of the daemon. The API token is required because the daemon binds to `0.0.0.0` inside the container; keep this token in your proxy or deployment secrets and do not expose it to browser code.
|
||||
|
||||
## Step 2: Create The Resource Group
|
||||
|
||||
```bash
|
||||
az group create \
|
||||
--name "$RESOURCE_GROUP" \
|
||||
--location "$LOCATION"
|
||||
```
|
||||
|
||||
## Step 3: Deploy The Bicep Template
|
||||
|
||||
Run this from the repository root:
|
||||
|
||||
```bash
|
||||
az deployment group create \
|
||||
--name "$DEPLOYMENT_NAME" \
|
||||
--resource-group "$RESOURCE_GROUP" \
|
||||
--template-file deploy/azure/container-instance.bicep \
|
||||
--parameters \
|
||||
location="$LOCATION" \
|
||||
dnsNameLabel="$DNS_LABEL" \
|
||||
allowedOrigins="$BROWSER_ORIGIN" \
|
||||
odApiToken="$OD_API_TOKEN"
|
||||
```
|
||||
|
||||
The template creates:
|
||||
|
||||
- Azure Storage account
|
||||
- Azure Files share for `/app/.od`
|
||||
- Linux Azure Container Instances container group
|
||||
- Public upstream DNS name and TCP port `7456`
|
||||
- Liveness probe against `/api/health`
|
||||
|
||||
## Step 4: Fetch The ACI Upstream
|
||||
|
||||
Fetch the daemon upstream host for your reverse proxy:
|
||||
|
||||
```bash
|
||||
export ACI_FQDN="$(az deployment group show \
|
||||
--resource-group "$RESOURCE_GROUP" \
|
||||
--name "$DEPLOYMENT_NAME" \
|
||||
--query "properties.outputs.daemonFqdn.value" \
|
||||
--output tsv)"
|
||||
export ACI_UPSTREAM_URL="http://${ACI_FQDN}:7456"
|
||||
```
|
||||
|
||||
Do not open this URL directly in a browser. The daemon requires `Authorization: Bearer <OD_API_TOKEN>` on non-loopback `/api/*` requests, and the web UI does not put that secret in browser requests.
|
||||
|
||||
## Step 5: Put A Trusted Proxy In Front
|
||||
|
||||
Serve `BROWSER_ORIGIN` from a TLS reverse proxy that authenticates users before forwarding traffic to the ACI upstream. The proxy must add the daemon token to API requests:
|
||||
|
||||
```nginx
|
||||
upstream open_design_aci {
|
||||
server <aci-fqdn>:7456;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name od.example.com;
|
||||
|
||||
# Add your organization's authentication layer here before proxying.
|
||||
|
||||
location /api/ {
|
||||
proxy_set_header Authorization "Bearer <OD_API_TOKEN>";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 1h;
|
||||
proxy_send_timeout 1h;
|
||||
gzip off;
|
||||
proxy_pass http://open_design_aci;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://open_design_aci;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace `<aci-fqdn>` with `ACI_FQDN`, replace `<OD_API_TOKEN>` with the same secret passed to the Bicep deployment, and keep `BROWSER_ORIGIN` equal to the origin served by the proxy. Keep the `/api/` buffering, gzip, HTTP/1.1, and timeout directives in place so streamed generation responses are not cut off by nginx. After the proxy is configured, open `BROWSER_ORIGIN` in your browser.
|
||||
|
||||
## Optional Parameters
|
||||
|
||||
```bash
|
||||
az deployment group create \
|
||||
--name "$DEPLOYMENT_NAME" \
|
||||
--resource-group "$RESOURCE_GROUP" \
|
||||
--template-file deploy/azure/container-instance.bicep \
|
||||
--parameters \
|
||||
odApiToken="$OD_API_TOKEN" \
|
||||
dnsNameLabel="$DNS_LABEL" \
|
||||
allowedOrigins="$BROWSER_ORIGIN" \
|
||||
image="docker.io/vanjayak/open-design:latest" \
|
||||
cpuCores=1 \
|
||||
memoryInGB=1 \
|
||||
fileShareQuotaGB=10
|
||||
```
|
||||
|
||||
Set `allowedOrigins` to the comma-separated browser origins served by trusted proxies. A direct public ACI browser URL is not supported because browser API calls cannot safely carry the daemon token.
|
||||
|
||||
## Azure DevOps
|
||||
|
||||
Use `deploy/azure/azure-pipelines.yml` as a starting point.
|
||||
|
||||
Before running it:
|
||||
|
||||
- Create an Azure Resource Manager service connection.
|
||||
- Set `OD_API_TOKEN` as a secret pipeline variable.
|
||||
- Update `resourceGroupName`, `location`, `openDesignImage`, and `browserOrigin`.
|
||||
- Replace `<your-azure-service-connection>` with your service connection name.
|
||||
|
||||
## Operations
|
||||
|
||||
View logs:
|
||||
|
||||
```bash
|
||||
az container logs \
|
||||
--resource-group "$RESOURCE_GROUP" \
|
||||
--name open-design
|
||||
```
|
||||
|
||||
Restart the container group:
|
||||
|
||||
```bash
|
||||
az container restart \
|
||||
--resource-group "$RESOURCE_GROUP" \
|
||||
--name open-design
|
||||
```
|
||||
|
||||
Delete all Azure resources created by this guide:
|
||||
|
||||
```bash
|
||||
az group delete \
|
||||
--name "$RESOURCE_GROUP"
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Do not expose the raw ACI endpoint as the browser-facing app URL. Use it as the upstream for a trusted proxy or for token-authenticated operational checks.
|
||||
- Keep `OD_API_TOKEN` secret. The proxy may use it upstream, but browser clients must not receive it. Rotate it by redeploying with a new value.
|
||||
- Keep `allowedOrigins` aligned with the browser-visible proxy origin; otherwise the daemon origin guard will reject browser API requests.
|
||||
- The Azure Files share persists project data after container restarts and image updates.
|
||||
Loading…
Reference in a new issue