Skip to content

Manifest reference

All manifests share the same envelope. Multiple YAML documents per file are allowed (--- separated); files are discovered recursively.

apiVersion: andro-cd/v1
kind: ECSService | ECSScheduledTask | ECSServiceSet | ECSCluster
metadata:
  name: my-app                          # unique across all repos
  labels:                               # chips in the UI, searchable, propagated as AWS tags
    team: platform
    env: production
spec:
  region: us-east-1                     # precedence: manifest > profile default > AWS_REGION
  awsProfile: prod-account              # optional named profile (multi-account)
  cluster: prod                         # auto-created if missing
  wave: 0                               # sync wave — lower waves settle first
  syncPolicy: { ... }                   # see below

A machine-readable JSON Schema is served at GET /api/schema — wire it into your manifest repo's CI together with androcd validate.

Sync policy

spec:
  syncPolicy:
    autoSync: true          # overrides global AUTO_SYNC for this app
    selfHeal: false         # revert manual AWS drift (default: only Git changes sync)
    prune: false            # delete the resource when removed from Git
    syncWindows:            # UTC windows when auto-sync is allowed (empty = always)
      - days: [Mon, Tue, Wed, Thu, Fri]
        start: "07:00"
        end: "19:00"
  • autoSyncfalse for critical services that require a human click.
  • selfHeal — off by default: console changes show as OutOfSync but aren't reverted.
  • prune — resources persist after manifest removal unless true (or manual Prune).
  • syncWindows — deploy freeze outside the windows; start inclusive, end exclusive, 24:00 = end of day. Manual sync always works.

kind: ECSService

spec:
  service:
    desiredCount: 2                     # ignored when autoscaling is configured
    launchType: FARGATE                 # FARGATE | EC2
    assignPublicIp: true
    circuitBreaker: true                # ECS deployment circuit breaker (default true)
    rollbackOnFailure: true             # auto-rollback on failed rollout (default true)
    minimumHealthyPercent: 100
    maximumPercent: 200
    autoscaling:                        # target-tracking; autoscaler owns desiredCount
      minCount: 1
      maxCount: 10
      targetCpu: 60
      targetMemory: 70
    loadBalancer:
      containerName: web                # defaults to the first container
      containerPort: 8080
      # EITHER reference an existing target group:
      targetGroupArn: arn:aws:elasticloadbalancing:...
      # OR let Andro-CD create the TG + listener rule (managed mode):
      # create:
      #   listenerArn: arn:aws:elasticloadbalancing:...:listener/app/main/abc/def
      #   port: 8080                    # TG port; defaults to containerPort
      #   protocol: HTTP                # towards the targets: HTTP | HTTPS
      #   rule:
      #     priority: 10                # unique per listener
      #     hostHeader: api.example.com # and/or pathPattern
      #     pathPattern: /api/*
      #   healthCheck:
      #     path: /health
      #     interval: 30
      #     timeout: 5
      #     healthyThreshold: 3
      #     unhealthyThreshold: 3
      #     matcher: "200-399"
    capacityProviders:                  # weighted strategy instead of launchType
      - provider: FARGATE_SPOT
        weight: 3
      - provider: FARGATE
        weight: 1
        base: 1                         # always ≥1 on on-demand
  network:
    subnets: [subnet-aaa, subnet-bbb]
    securityGroups: [sg-0ccc]
  hooks:
    preSync:                            # one-off task before the rollout (migrations)
      command: ["python", "manage.py", "migrate"]
      container: web                    # defaults to the first container
      timeoutSeconds: 600
    postSync:
      command: ["curl", "-X", "POST", "https://hooks.example/deployed"]
  taskDefinition:
    family: web-app                     # defaults to metadata.name
    cpu: "256"
    memory: "512"
    networkMode: awsvpc
    executionRoleArn: arn:aws:iam::...:role/ecsTaskExecutionRole
    taskRoleArn: arn:aws:iam::...:role/appRole
    resolveImages: false                # true → pin ECR tags to immutable digests
    containers:
      - name: web
        image: nginx:1.27
        essential: true
        cpu: 0
        memory: 512                     # hard limit
        memoryReservation: 256          # soft limit
        portMappings: [80, 443]         # ints or {containerPort, protocol}
        environment:                    # map or list of {name, value}
          APP_ENV: production
        secrets:                        # name -> SSM / Secrets Manager ARN
          DB_PASSWORD: arn:aws:ssm:...:parameter/db-pass
        command: ["gunicorn", "app.wsgi"]
        entryPoint: ["/entrypoint.sh"]
        logGroup: /ecs/web-app          # awslogs driver; group auto-created
        healthCheck:                    # docker HEALTHCHECK semantics
          command: ["CMD-SHELL", "curl -f http://localhost/health || exit 1"]
          interval: 30
          timeout: 5
          retries: 3
          startPeriod: 15

ECR digest pinning

With resolveImages: true, mutable tags (app:latest) are resolved via ecr:DescribeImages and pinned to app@sha256:… at sync time — immutable deploys and reliable drift detection. Non-ECR images pass through unchanged.

kind: ECSScheduledTask

Cron-style task backed by EventBridge Scheduler:

apiVersion: andro-cd/v1
kind: ECSScheduledTask
metadata:
  name: nightly-report
spec:
  cluster: batch
  schedule:
    expression: cron(0 3 * * ? *)       # cron(...), rate(...) or at(...)
    roleArn: arn:aws:iam::...:role/androcdSchedulerRole
    enabled: true
  network:
    subnets: [subnet-aaa]
    securityGroups: [sg-0ccc]
  taskDefinition:
    containers:
      - name: report
        image: 123.dkr.ecr.us-east-1.amazonaws.com/reports:v2
        command: ["python", "-m", "reports.nightly"]

The scheduler role needs ecs:RunTask plus iam:PassRole for the task's roles.

kind: ECSServiceSet (app-of-apps)

Generate N applications from one template:

apiVersion: andro-cd/v1
kind: ECSServiceSet
metadata:
  name: api-environments
spec:
  generators:
    - values: {env: dev,  count: 1}
    - values: {env: prod, count: 3}
  template:
    apiVersion: andro-cd/v1
    kind: ECSService
    metadata:
      name: api-${env}
    spec:
      cluster: ${env}
      service: {desiredCount: "${count}"}
      # ...

${var} placeholders substitute verbatim; each generator produces one app.

kind: ECSCluster

Manage the cluster itself from Git — insights, capacity providers, default strategy, Service Connect namespace and tags:

apiVersion: andro-cd/v1
kind: ECSCluster
metadata:
  name: production
  labels: {team: platform}
spec:
  region: eu-central-1
  wave: 0                              # create before wave-1 services target it
  containerInsights: enhanced          # disabled | enabled | enhanced
  capacityProviders: [FARGATE, FARGATE_SPOT]
  defaultCapacityProviderStrategy:
    - {provider: FARGATE_SPOT, weight: 3}
    - {provider: FARGATE, weight: 1, base: 1}
  serviceConnectNamespace: internal    # Cloud Map namespace (name or ARN)
  • No network/taskDefinition — those belong to services.
  • Omitted fields are left untouched on the live cluster (no churn).
  • Prune is safe: deletion is refused while the cluster still has active services or running tasks.
  • Healthy when the cluster is ACTIVE; pairs naturally with waves (cluster wave 0, services wave 1+).

Values files (templating)

values.yaml / values.yml files are not manifests — they define ${key} substitutions for every manifest in their directory subtree:

manifests/
├── values.yaml              # tag: stable, team: platform
├── web.yaml                 # image: repo/app:${tag}  → repo/app:stable
└── envs/prod/
    ├── values.yaml          # tag: v42
    └── web.yaml             # image: repo/app:${tag}  → repo/app:v42
  • Files layer from the repo root down; the closest file wins on conflicts.
  • Nested mappings flatten to dotted keys: image: {tag: v1}${image.tag}.
  • The CLI validator applies the same substitution, so CI matches the server exactly.