Declarative, GitOps-based application management for Azure Container Apps. Deploy ACA apps from Git with automatic drift detection, health-aware reconciliation, and safety guardrails.
SynosCD brings Flux-like GitOps to Azure Container Apps:
- β Git as Source of Truth - YAML manifests define desired state
- β Automatic Drift Healing - Operator continuously reconciles desiredβlive state
- β Health-Aware Reconciliation - Failed/unready apps trigger automatic re-apply
- β Safe by Default - Managed ownership tagging + protected app list prevent accidental deletion
- β
Local Development - Full support for
az loginbased testing - β Production Grade - Structured JSON logging, ARM validation, LRO polling
- Pull-based reconciliation loop (configurable interval)
- GitHub App authentication for config repo access
- Real ARM create/update/delete/list/get calls for ACA resources
- Health-aware reconciliation (re-apply if app is not healthy)
- Managed ownership tagging on apps (
synoscd-managed=true) - Optional prune mode for managed apps missing from Git
- Protected app list to avoid deleting critical apps (e.g.
synoscd-operator)
For most users, the fastest path is:
- Download a release artifact from GitHub Releases
- Set the required environment variables
- Run a read-only command such as
synos get apps
If you are running from a repo checkout or source install and your GitHub App secrets are already stored in Azure Key Vault, you can also use the bootstrap helper:
python scripts/bootstrap_env.py --run synos get appsThat command loads the required environment variables and runs the CLI without needing source or eval.
SynosCD releases are published in GitHub Releases.
Each tagged release includes:
synoscd-<version>.tar.gzsource distributionsynoscd-<version>-py3-none-any.whlwheelsynos-linux-x86_64.tar.gzsynos-macos-x86_64.tar.gzsynos.exe
Choose the format that matches how you want to run it:
Download the release artifact you want, then install it with pip:
pip install synoscd-<version>-py3-none-any.whl
# or
pip install synoscd-<version>.tar.gz- Windows: use
synos.exe - Linux/macOS: extract the tarball and run
synos
Example on Linux/macOS:
tar -xzf synos-linux-x86_64.tar.gz
./synos --helpExample on Windows PowerShell:
.\synos.exe --helpSynosCD is configured entirely through environment variables.
SYNOSCD_GITHUB_APP_IDSYNOSCD_GITHUB_APP_PRIVATE_KEYSYNOSCD_GITHUB_APP_INSTALLATION_IDSYNOSCD_GITHUB_REPO_OWNERSYNOSCD_GITHUB_REPO_NAMESYNOSCD_AZURE_SUBSCRIPTION_IDSYNOSCD_AZURE_RESOURCE_GROUPSYNOSCD_AZURE_CONTAINER_APP_ENVIRONMENT
SYNOSCD_GITHUB_CONFIG_PATH(default:apps)SYNOSCD_RECONCILE_INTERVAL_SECONDS(default:300)SYNOSCD_AZURE_MANAGED_IDENTITY_CLIENT_ID(needed for user-assigned MI in ACA)SYNOSCD_PRUNE_ENABLED(default:false)SYNOSCD_PROTECTED_APPS_CSV(default:synoscd-operator)
If you are running locally, authenticate first:
az login
az account set --subscription <your-subscription-id>export SYNOSCD_GITHUB_APP_ID=<app-id>
export SYNOSCD_GITHUB_APP_PRIVATE_KEY="$(cat private-key.pem)"
export SYNOSCD_GITHUB_APP_INSTALLATION_ID=<installation-id>
export SYNOSCD_GITHUB_REPO_OWNER=<owner>
export SYNOSCD_GITHUB_REPO_NAME=<repo>
export SYNOSCD_AZURE_SUBSCRIPTION_ID=<sub-id>
export SYNOSCD_AZURE_RESOURCE_GROUP=<resource-group>
export SYNOSCD_AZURE_CONTAINER_APP_ENVIRONMENT=<ace-name>
synos get apps$env:SYNOSCD_GITHUB_APP_ID = '<app-id>'
$env:SYNOSCD_GITHUB_APP_PRIVATE_KEY = Get-Content .\private-key.pem -Raw
$env:SYNOSCD_GITHUB_APP_INSTALLATION_ID = '<installation-id>'
$env:SYNOSCD_GITHUB_REPO_OWNER = '<owner>'
$env:SYNOSCD_GITHUB_REPO_NAME = '<repo>'
$env:SYNOSCD_AZURE_SUBSCRIPTION_ID = '<sub-id>'
$env:SYNOSCD_AZURE_RESOURCE_GROUP = '<resource-group>'
$env:SYNOSCD_AZURE_CONTAINER_APP_ENVIRONMENT = '<ace-name>'
synos get appsIf you are running from a repo checkout or source install and your GitHub App secrets are in Key Vault (for example synoscd-vault), you can auto-load everything with one command:
python scripts/bootstrap_env.py --run synos get appsOther common examples:
python scripts/bootstrap_env.py --run synos config
python scripts/bootstrap_env.py --run synos get source
python scripts/bootstrap_env.py --run synos get appsYou can also generate shell exports if needed:
python scripts/bootstrap_env.py --print-exportsThis script auto-detects subscription, resource group, ACE, and loads GitHub App secrets from AKV:
github-app-idgithub-app-installation-idgithub-app-private-key
Optional overrides:
python scripts/bootstrap_env.py \
--vault-name synoscd-vault \
--rg-hint synoscd-dev \
--ace-hint <ace-name> \
--config-repo-url https://github.com/<owner>/<repo>.git \
--run synos get sourceConvenience wrappers:
# Git Bash / Linux / macOS
./scripts/synos-env.sh get apps
# PowerShell
./scripts/synos-env.ps1 get appssynos configβ show active SynosCD configurationsynos get apps [-o table|json|yaml]β list desired Apps with live health contextsynos get source [-o table|json|yaml]β show Git source details and latest commitsynos get status [-o table|json|yaml]β show reconciliation summarysynos status app <name> [-o table|json|yaml] [--watch]β show detailed live status for one appsynos status all [-o table|json|yaml] [--watch]β show detailed live status for all appssynos describe <name> [-o table|json|yaml] [--watch]β alias forsynos status app <name>synos diff [--app <name>] [-o table|json|yaml]β show desired vs live state differences
synos reconcile source [--watch]β reconcile from Git source nowsynos reconcile app <name>β reconcile one app nowsynos sync [--dry-run]β run one reconciliation pass nowsynos operator [--interval <seconds>]β run the continuous operator loop
synos logs app <name> [--tail N] [--follow]β show or stream logs for one ACA appsynos logs operator [--tail N] [--follow]β show or stream logs for the SynosCD operator
synos set --showβ show current operator runtime settingssynos set --interval <seconds> [--yes]β change reconcile interval on the running operatorsynos set --prune/--no-prune [--yes]β enable or disable prune on the running operatorsynos set --protected-apps <csv> [--yes]β update protected apps on the running operatorsynos set --max-concurrent <n> [--yes]β update max concurrent reconciles on the running operatorsynos suspend <name> [--yes]β suspend reconciliation for one app at runtimesynos resume <name> [--yes]β resume reconciliation for one app at runtime
synos bootstrap ...β validate/setup bootstrap values for operator configuration
--verboseor-vβ increase log verbosity--debugβ enable debug logging
A typical user flow looks like this:
- Confirm configuration with
synos config - Check source connectivity with
synos get source - List managed apps with
synos get apps - Inspect one app with
synos status app <name>orsynos describe <name> - Force a refresh with
synos reconcile app <name>orsynos reconcile source
If you need temporary operational control without changing Git:
- use
synos suspend <name>to pause reconciliation for one app - use
synos resume <name>to re-enable reconciliation - use
synos set --showto inspect live operator settings - use
synos set --interval ...or related flags to change runtime operator behavior
- One operator per ACE/environment
- One ops repo as source of truth (recommended)
- Apps defined as YAML under
apps/(or custom config path) - Terraform/Bicep owns platform foundation (ACE, identity, network, etc.)
- SynosCD owns app-level reconciliation
apiVersion: synoscd.io/v1alpha1
kind: App
metadata:
name: demo-app
labels:
synoscd.io/managed: "true"
spec:
containers:
- name: app
image: mcr.microsoft.com/azuredocs/containerapps-helloworld:latest
cpu: 0.25
memory: 0.5Gi
ingress:
enabled: true
external: true
targetPort: 80
scale:
minReplicas: 1
maxReplicas: 3
suspend: falseLocal run is supported via DefaultAzureCredential.
python -m venv venv
source venv/bin/activate # Windows Git Bash: source venv/Scripts/activate
pip install -e .You can run SynosCD in any of these ways:
synos --help
python -m synoscd --help
python src/synoscd/cli.py --helpThe package is configured for standard installation on Windows, Linux, and macOS.
This section is mainly for maintainers or platform admins who build and deploy the SynosCD operator image.
ACR_NAME="synoscdacr"
IMAGE="synoscdacr.azurecr.io/synoscd:v0.2.6"
podman build -t "$IMAGE" .
TOKEN=$(az acr login -n "$ACR_NAME" --expose-token --query accessToken -o tsv)
podman login synoscdacr.azurecr.io -u 00000000-0000-0000-0000-000000000000 -p "$TOKEN"
podman push "$IMAGE"
MI_CLIENT_ID=$(az identity show -g synoscd-dev -n synoscd-identity --query clientId -o tsv)
az containerapp update -n synoscd-operator -g synoscd-dev \
--image "$IMAGE" \
--set-env-vars \
SYNOSCD_AZURE_MANAGED_IDENTITY_CLIENT_ID="$MI_CLIENT_ID" \
SYNOSCD_RECONCILE_INTERVAL_SECONDS=30 \
SYNOSCD_PRUNE_ENABLED=false \
SYNOSCD_PROTECTED_APPS_CSV="synoscd-operator"Each cycle:
- Fetch YAML resources from Git (
apps/by default) - Parse into SynosCD resources (
App,Project) - List live ACA apps from Azure
- For each managed
App:- If missing or drifted, apply desired state via ARM
- If live app health is failed/not ready, force reconcile
- Optionally prune managed apps missing from Git (if enabled)
- Drift heal: Enabled by default via desired-vs-live comparison
- Health-aware reconcile: Failed/unready app is re-applied even when spec is unchanged
- Ownership tag: SynosCD writes
synoscd-managed=trueon managed apps - Prune mode: Disabled by default (
SYNOSCD_PRUNE_ENABLED=false) - Protected apps: Never pruned if listed in
SYNOSCD_PROTECTED_APPS_CSV
Prune deletes only apps that are:
- tagged as managed by SynosCD, and
- not present in desired Git state, and
- not in protected app list
SynosCD publishes multiple distribution formats from GitHub Releases:
| Format | Best for |
|---|---|
Source tarball (.tar.gz) |
Contributors and source installs |
Wheel (.whl) |
Standard Python installs and pip mirrors |
Standalone binary (synos / synos.exe) |
Direct download and run with env vars already configured |
Recommended usage:
- Most users: download a GitHub Release artifact
- Python environments: install the wheel or source distribution with
pip - No-Python client machines: download the standalone release binary
If you use a standalone binary, you still need to set the same SynosCD env vars before running the CLI.
flux reconcile source gitβsynos reconcile sourceflux reconcile kustomization <name>βsynos reconcile app <name>flux get source gitβsynos get sourceflux get ks/flux get hrβsynos get apps(App-centric model)
SynosCD is intentionally simpler than Flux.
- Flux uses separate resource types such as
GitRepository,Kustomization, andHelmRelease - SynosCD uses a single app-centric model
- Git connectivity is configured on the operator itself
- The operator pulls manifests directly and reconciles them to Azure Container Apps
Today, the primary Git contract is the App manifest. You do not need separate Source or Kustomization manifests unless SynosCD later grows into a multi-source or multi-tenant platform.
Image tag not found in ACR. Verify tags:
az acr repository show-tags -n synoscdacr --repository synos -o tableUse ARM-safe tags (SynosCD now uses synoscd-managed=true).
Check app resource provisioning + revision state:
az containerapp show -n <app> -g <rg> --query "{provisioning:properties.provisioningState,latest:properties.latestRevisionName,ready:properties.latestReadyRevisionName}" -o jsonc
az containerapp revision list -n <app> -g <rg> -o tableIf Azure CLI log streaming has extension issues, use activity log:
APP_ID=$(az containerapp show -n <app> -g <rg> --query id -o tsv)
az monitor activity-log list --resource-id "$APP_ID" --status Failed --offset 2h -o jsoncACA requires valid CPU/memory combinations (for example 0.25 + 0.5Gi, 0.5 + 1Gi).
src/synoscd/
__init__.py
schema.py
config.py
logger.py
github.py
aca.py
reconciler.py
cli.py
examples/
docs/
- Core GitOps loop: implemented
- ARM app create/update/list/get/delete: implemented
- Drift heal + health-aware retry: implemented
- Safe ownership tagging + optional prune: implemented
- Multi-repo orchestration: not implemented (single repo per operator recommended)