ECS deployment management CLI for CI/CD. ecsmate renders desired state from CUE, diffs it against live AWS resources, and applies changes with live tracking.
- Highlights
- Requirements
- IAM Permissions
- Build and run
- CLI basics
- How it works
- Diff output (differ)
- Apply tracker (tracker)
- Manifest format
- Values and overrides
- SSM parameter references
- Secrets management
- Examples
- Notes and limitations
- Declarative manifests in CUE with values overlays and
--setoverrides. - Diff output that groups resource changes and shows recreate reasons.
- Apply pipeline with live tracker (interactive TTY or log-friendly output).
- ECS services, task definitions, scheduled tasks, and ALB ingress support.
- Optional SSM parameter resolution with
{{ssm:/path}}placeholders. - Managed secrets with KMS envelope encryption and automatic SSM parameter sync.
- Built-in status, rollback, validate, and template commands.
- Go 1.25+ to build.
- AWS credentials with access to ECS, EventBridge Scheduler, ELBv2, Application Auto Scaling, IAM, CloudWatch Logs, SSM, KMS, and STS (as needed).
go build ./cmd/ecsmate
./ecsmate --helpYou can also run directly:
go run ./cmd/ecsmate --help-m, --manifest: manifest directory (default:.)-f, --values: values files (repeatable, merged in order)--set: override values withkey=value(repeatable)-c, --cluster: ECS cluster name/ARN-r, --region: AWS region--log-level:debug|info|warn|error--no-color: disable ANSI colors--no-ssm: skip SSM resolution
0success/no diff1error2diff detected (diff command only)3rollout failed (apply/rollback)
diff
Show desired vs current state. Exit code 2 if changes are detected.
apply
Apply the plan. Prompts for confirmation unless --auto-approve is set.
Flags:
--auto-approve: skip interactive confirmation--no-wait: return after submitting changes--timeout: wait timeout for deployments (default 15m)--log-lines: task log lines on failure (-1all,0none,Nlimit)
status
Show service status. Use --watch to stream updates.
Flags:
--service: check a specific service (requires--cluster)--watch: refresh in a loop--interval: watch interval seconds--events: number of recent events per service
rollback
Rollback a service to a previous task definition revision.
Flags:
--service: service name (required)--revision: negative for relative (-1previous), positive for absolute--list: list available revisions--limit: number of revisions to list--no-wait: do not wait for deployment completion
validate
Validate CUE syntax, schema constraints, and manifest content without AWS calls.
template
Render the fully resolved manifest (YAML or JSON) without diffing/applying.
Flags:
-o, --output:yaml(default) orjson
secrets
Manage encrypted secrets files using KMS envelope encryption.
Subcommands:
encrypt <file> --kms-arn <arn>: encrypt a plaintext YAML filedecrypt <file>: decrypt to stdoutset <file> <key> [--value <val>]: set or update a secret (prompts if no value)delete <file> <key>: remove a secret from the file
- Loads all
.cuefiles in the manifest directory plustaskdefs/andvalues/subdirectories. - Merges any
--valuesfiles and applies--setoverrides. - Resolves
{{ssm:...}}placeholders (unless--no-ssm). - Loads and decrypts managed secrets (if configured).
- Builds desired state and discovers current ECS/ALB/Scheduler resources.
- Generates a plan and renders a diff.
- Applies in order:
- SSM parameters (managed secrets)
- Log groups (from
awslogscontainer configs withcreateLogGroup: true) - Target groups (ingress)
- Task definitions
- Listener rules (ingress)
- ECS services
- Scheduled tasks
Service names are automatically prefixed with the manifest name (if set),
unless already prefixed.
The diff renderer groups changes by resource and shows a boxed, structured view. It marks actions as Create/Update/Delete/Recreate and includes recreate reasons.
Notable behaviors:
- ECS-assigned defaults like
hostPortare ignored if only present remotely. - Unchanged
environmententries are suppressed to reduce noise.
The tracker prints structured progress with sections and task statuses. In an interactive TTY, it re-renders service deployment progress in place, including:
- rollout state and reasons
- old vs new task definition
- per-task status (RUNNING/PENDING/STOPPED)
- progress bar and recent events
In non-interactive output, it prints events and state changes as log lines.
Manifests are defined in CUE. Import the schema from pkg/cue. The schema is
embedded in the binary, so ecsmate can be run from any directory without needing
access to the ecsmate source tree.
Basic shape:
import "github.com/x-qdo/ecsmate/pkg/cue:schema"
manifest: schema.#Manifest & {
name: "myapp"
taskDefinitions: { ... }
services: { ... }
scheduledTasks: { ... }
ingress: { ... }
}Three types:
managed: fully defined in CUEmerged: override an existing task definition ARNremote: use a task definition ARN as-is
Managed example:
taskDefinitions: {
web: {
type: "managed"
family: "myapp-web"
cpu: "256"
memory: "512"
requiresCompatibilities: ["FARGATE"]
containerDefinitions: [{
name: "web"
image: "123456789012.dkr.ecr.us-east-1.amazonaws.com/web:latest"
portMappings: [{ containerPort: 80 }]
logConfiguration: {
logDriver: "awslogs"
options: {
awslogs-group: "/ecs/myapp/web"
awslogs-region: "us-east-1"
awslogs-stream-prefix: "ecs"
}
createLogGroup: true
retentionInDays: 14
}
}]
}
}Merged example:
taskDefinitions: {
api: {
type: "merged"
baseArn: "arn:aws:ecs:us-east-1:123456789012:task-definition/api:12"
overrides: {
cpu: "512"
containerDefinitions: [{
name: "api"
image: "123456789012.dkr.ecr.us-east-1.amazonaws.com/api:v2"
}]
}
}
}Remote example:
taskDefinitions: {
worker: {
type: "remote"
arn: "arn:aws:ecs:us-east-1:123456789012:task-definition/worker:42"
}
}services: {
web: {
cluster: "my-ecs-cluster"
taskDefinition: "web"
desiredCount: 2
launchType: "FARGATE"
networkConfiguration: {
awsvpcConfiguration: {
subnets: ["subnet-123", "subnet-456"]
securityGroups: ["sg-123"]
assignPublicIp: "DISABLED"
}
}
deployment: {
strategy: "rolling" // or "gradual"
config: {
minimumHealthyPercent: 50
maximumPercent: 200
circuitBreaker: { enable: true, rollback: true }
alarms: ["my-alarm-name"]
alarmRollback: true
}
}
}
}Deployment strategies:
rolling: standard ECS update with deployment configuration.gradual: ECS-native staged rollout usingdeployment.config.stepswithpercentandwait(seconds).
blue-green and canary are present in the CUE schema but are not supported
by the executor; they will fail with an explicit error.
Gradual example:
deployment: {
strategy: "gradual"
config: {
steps: [
{ percent: 25, wait: 60 },
{ percent: 50, wait: 60 },
{ percent: 75, wait: 60 },
{ percent: 100, wait: 0 },
]
}
}Auto scaling:
autoScaling: {
minCapacity: 1
maxCapacity: 10
policies: [{
name: "cpu"
type: "TargetTrackingScaling"
targetValue: 50
predefinedMetric: "ECSServiceAverageCPUUtilization"
}]
}Service dependency ordering:
services: {
web: { ... }
worker: {
dependsOn: ["web"]
}
}Scheduled tasks are created via EventBridge Scheduler. When scheduled tasks are present, ecsmate ensures an EventBridge role exists for the scheduler.
scheduledTasks: {
nightly: {
taskDefinition: "cron"
cluster: "my-ecs-cluster"
taskCount: 1
platformVersion: "1.4.0"
group: "maintenance" // ECS task group
schedule: {
type: "cron"
expression: "0 2 * * ? *"
}
networkConfiguration: {
awsvpcConfiguration: {
subnets: ["subnet-123"]
securityGroups: ["sg-123"]
assignPublicIp: "DISABLED"
}
}
tags: [
{ key: "env", value: "prod" },
{ key: "team", value: "ops" },
]
deadLetterConfig: {
arn: "arn:aws:sqs:us-east-1:123456789012:dlq"
}
retryPolicy: {
maximumEventAgeInSeconds: 300
maximumRetryAttempts: 2
}
}
}Ingress rules generate target groups and listener rules.
ingress: {
listenerArn: "arn:aws:elasticloadbalancing:..."
vpcId: "vpc-123"
rules: [{
priority: 10
host: "app.example.com"
service: {
name: "web"
containerName: "web"
containerPort: 80
}
healthCheck: { path: "/health", matcher: "200" }
}]
}Manifests commonly split defaults and environment-specific values under
values/. Files directly in values/ (like default.cue, _ssm.cue) are
auto-loaded. Subdirectory files (values/envs/*.cue, values/tenants/*.cue)
must be explicitly provided with -f. Ad-hoc changes use --set.
To allow --set overrides, use CUE's default syntax (type | *"default")
instead of concrete values:
// Overridable (recommended)
images: {
tag: string | *"latest" // --set images.tag=v2.0 works
registry: string | *"ecr.aws"
}
// NOT overridable (concrete values cause conflicts)
images: {
tag: "latest" // --set images.tag=v2.0 fails
}The --set flag uses CUE unification, which can only fill in or narrow
constraints. Concrete values like tag: "latest" are already fully specified
and cannot be changed. The default syntax string | *"latest" defines a
constraint (string) with a default (*"latest") that can be overridden.
Examples:
ecsmate diff -m ./deploy -f values/staging.cue
ecsmate apply -m ./deploy --set images.tag=v1.2.3
ecsmate diff -m examples/cloudinsurance \
-f examples/cloudinsurance/values/envs/stage.cue \
-f examples/cloudinsurance/values/tenants/cal.cue \
--set images.tag=20260111-r4Any string field can include {{ssm:/path/to/param}}. ecsmate resolves these
in batch via SSM (and splits StringList values on commas for subnets/security
groups). Use --no-ssm to keep placeholders intact.
Example:
cluster: "{{ssm:/myapp/prod/ecs/cluster_name}}"ecsmate provides built-in secrets management using KMS envelope encryption. Secrets are encrypted locally and stored in version control, then synced to SSM Parameter Store as SecureString parameters during apply.
Create and manage encrypted secrets files:
# Create initial secrets file from plaintext YAML
echo "db_password: secret123" > secrets.yaml
ecsmate secrets encrypt secrets.yaml --kms-arn arn:aws:kms:us-east-1:123456789012:key/xxx
rm secrets.yaml # remove plaintext
# View decrypted contents
ecsmate secrets decrypt secrets.enc.yaml
# Add or update a secret
ecsmate secrets set secrets.enc.yaml api_key --value "newkey123"
ecsmate secrets set secrets.enc.yaml api_key # prompts for value
# Remove a secret
ecsmate secrets delete secrets.enc.yaml old_keyConfigure managed secrets in your manifest:
manifest: schema.#Manifest & {
name: "myapp"
secrets: {
managed: {
file: "secrets.enc.yaml"
kmsKeyArn: "arn:aws:kms:us-east-1:123456789012:key/xxx"
ssmPrefix: "/myapp/prod"
}
}
// ...
}Reference managed secrets by key name in container definitions:
containerDefinitions: [{
name: "api"
image: "myapp:latest"
environment: {
APP_ENV: "production"
LOG_LEVEL: "info"
}
secrets: {
DB_PASSWORD: "db_password" // key from managed secrets
API_KEY: "api_key" // resolved to SSM ARN automatically
}
}]During apply, ecsmate:
- Creates/updates SSM SecureString parameters at
{ssmPrefix}/{key} - Resolves secret key names to their SSM parameter ARNs
- Task definitions reference the SSM ARNs (values never in task definition)
For secrets managed outside ecsmate (Secrets Manager, existing SSM params):
secrets: {
external: {
LEGACY_KEY: "arn:aws:secretsmanager:us-east-1:123456789012:secret:legacy"
}
}
// Then reference in containers:
secrets: {
LEGACY_KEY: "arn:aws:secretsmanager:..." // use ARN directly
}- SSM parameters are tagged with
ManagedBy=ecsmate - ecsmate refuses to overwrite parameters not managed by it
- Diff output shows hash changes, never actual secret values
- Orphan detection: removes SSM params under prefix that are no longer in secrets file
Two sample manifests live under examples/:
examples/webapp: web + worker services with scheduled cron task.examples/cloudinsurance: ingress rules, service discovery, and SSM-backed infrastructure values.
Try:
ecsmate validate -m examples/webapp
ecsmate diff -m examples/webapp -f examples/webapp/values/staging.cue
ecsmate template -m examples/cloudinsurance -o yaml- Service names are prefixed with
manifest.nameunless already prefixed. blue-greenandcanarystrategies require CodeDeploy and are not supported by the executor.- Log groups are only created when using
awslogswithcreateLogGroup: true. - Managed secrets require KMS encrypt/decrypt permissions and SSM write access.
- Environment and secrets use map syntax (
KEY: "value") not array syntax.
