Skip to content

Provisioning ​

yolo sync reconciles the AWS resources your app needs with what you've declared in yolo.yml. It's idempotent: it looks at what already exists, computes the difference, and makes only the API calls needed to close the gap. Run it as often as you like.

bash
yolo sync production

Scope-first provisioning ​

YOLO groups every resource by ownership scope — the blast radius if it changes. Each scope has exactly one writer, so an app deploy can never mutate shared infrastructure:

CommandScopeBlast radiusProvisions
yolo sync:account <env>Accountthe whole AWS accountthe ECS / Application Auto Scaling / ElastiCache service-linked roles (created if missing — a greenfield account can't create its first cluster or scalable target without them), GitHub OIDC provider
yolo sync:environment <env>Environmentevery app in the environmentVPC, the public and private subnet tiers, internet gateway & routes (the network), the RDS security group and private-only DB subnet group, SNS alarm topic, the shared ECS execution IAM role, the env-shared yolo-{env}-observer read-only policy (the env-wide read surface for env-tier reads like status:environment and audit, carried by the env observer role and the admin role — a deployer instead carries the app-fenced per-app observer, see CI/CD) and the yolo-{env}-observer-role that carries it (YOLO's own read commands — status, audit and friends — assume this role to cap themselves to the read-only policy even when the developer's own identity is broader; an operator or agent can also assume it directly from a *-readonly profile), the data documents — yolo-{env}-data-read / yolo-{env}-data-write (objects in every app data bucket — the YOLO-named ones by wildcard plus every bring-your-own bucket a published app claim names, which only sync:app writes; a new app naming one needs a second env sync after its first sync:app — and on the write side the database dumps), kept apart from the observer document so the deployer role can carry infra reads without ever reaching user data, and the Developer tier — the yolo-{env}-developer-role (observer + both data documents: writes to app data, nothing on the infrastructure), the Admin tier — the yolo-{env}-admin write policy and the yolo-{env}-admin-role that carries it (observer read policy + both data documents + admin write policy), which yolo sync, yolo scale, yolo permissions, yolo services and the environment:* file commands assume to cap a provisioning or environment-management run to YOLO's own blast radius (the services YOLO provisions, IAM fenced to yolo-*) even when the operator's identity is account-admin — and whose trust requires MFA, so escalating to admin is always an explicit human act an agent can't perform. The env grant groups (yolo-{env}-observers, yolo-{env}-developers, yolo-{env}-admins) whose membership grants the read / developer / admin tier are provisioned here too. The tier guard is fail-closed: a command refuses if it can't assume its role, so a fresh environment is bootstrapped once with yolo sync <env> --dangerously-skip-permissions (which runs uncapped on your identity to create the roles); every run after that is capped. The env config bucket (yolo-{account-id}-{env}-config — the environment's declaration: env manifest + env-shared .env), the env logs bucket (yolo-{account-id}-{env}-logs — the shared ALB's access logs under alb/, everything expiring after 90 days), the env backups bucket (yolo-{account-id}-{env}-backups — the apps' database dumps, one write-only prefix per app), the IVS event-logging pipeline (while the env manifest declares services.ivs — see the service lifecycle), the ALB and its :80/:443 listeners, the WAF fronting the ALB
yolo sync:app <env>Appone appS3 buckets, app IAM (deployer role/policy, the per-app observer role/policy, the per-app data-read (when the app declares a bucket) / data-write policies fenced to this app's data bucket and dump prefix, the per-app developer role, the yolo-{env}-{app}-observers / -developers grant groups, the per-app ECS task role + any task-role-policies), ECS cluster/service/task definition, target group + listener rule, CloudFront distribution, hosted zone & ACM certificate, SQS queues, CloudWatch dashboard — plus the shared Valkey cache (default-on for any app with tasks; opt out via cache.store). Sessions ride the same Valkey cluster by default, so they need no provisioning of their own

The bare yolo sync runs all three in dependency order — account, then environment, then app:

bash
yolo sync production   # account → environment → app

sync:app only additively attaches to shared infrastructure (its SNI certificate and listener rule on the environment's :443 listener, its database-port ingress rule on the shared RDS security group, its 6379 ingress rule on the shared cache security group). It never modifies the shared resource itself, so the environment tier stays the single writer.

The shared Valkey cache is env-scoped but bootstrapped from sync:app by exception (like the RDS security group), because its security group needs this app's task SG to authorise. The first app to sync creates the cluster (cache defaults on); later apps find it and just wire their env. Sessions reuse the same cluster (on a separate logical database), so there's no extra session infrastructure to provision.

Why scopes matter

Several apps can share one environment's VPC and load balancer. Because sync:app only attaches and never mutates, deploying app B can't break app A's networking. When you're iterating on one app, sync:app is faster than a full sync — the account and environment tiers rarely change. Each environment, on the other hand, gets its own VPC in a distinct 10.N.0.0/16 (auto-selected at create as the lowest /16 free on the account), so two environments — a staging trial beside production — never overlap and stay peerable.

The network ​

Each environment's VPC carries two subnet tiers, one per availability zone across three AZs:

  • Public (10.N.0-2.0/24) — where compute lives. Fargate tasks and the ALB get public IPs so they reach the internet without a NAT gateway; ingress is still gated by security groups.
  • Private (10.N.10-12.0/24) — where the database lives. No public IPs and a route table carrying only the VPC-local route, so nothing in the tier is reachable from (or can reach) the internet.

The RDS DB subnet group (yolo-{env}-private-subnet-group) spans only the private tier — a database launched into it has no public path by design, and the only way in is the database-port ingress rule from each app's task security group (or your laptop, via yolo db:tunnel). YOLO never creates or manages the database itself; it provisions the network shell the database sits in.

YOLO owns the network layer — there are no adoption escape hatches. Every environment's VPC, subnets and routing are created and named by YOLO, so every environment looks exactly the same and every guarantee (the private tier's isolation, the audit's posture verdicts, teardown) holds by construction. Infrastructure that lives outside the YOLO network is reached by VPC peering, never by pointing YOLO at it.

That no-adoption rule holds account-wide, not just for the network. Sync never claims a resource it didn't create: an existing resource that matches one of YOLO's names but carries no yolo:scope ownership tag is treated as a stranger — another tool's live infrastructure sharing the account — and the sync refuses at plan time, naming the resource, rather than stamping YOLO tags on it. Remove or rename the conflicting resource (or tag it manually if it genuinely is YOLO's), then re-run. Security-group lookups are additionally scoped to the environment's VPC, since AWS group names are only unique per VPC — a same-named group in someone else's VPC is simply invisible. The deliberate exceptions are resources that legitimately pre-exist because they're singletons beyond YOLO's naming authority: a domain's hosted zone and the account's GitHub OIDC provider are adopted, and the app data bucket is never tagged at all.

A database hosted elsewhere — the transitional posture while migrating into the yolo VPC — keeps working: declare it via database: and declare the peering in the environment manifest's peering list — sync:environment reconciles the connection, the routes both ways and DNS resolution, and tears it all down when the entry is removed. yolo audit classifies where the database actually sits (managed / external / exposed) and warns when it's publicly accessible or unreachable from the app's tasks — audit-only, so an external database never blocks a deploy. The full database story — the three postures, launching into the managed end-state, peering, and tunnelling — lives on its own page: Databases.

The environment declaration ​

App manifests declare what each app needs. The environment-shared tier has a declaration of its own: two files in the env config bucket (yolo-{account-id}-{env}-config), living in S3 rather than any app's repo precisely because they're shared — no single repo owns them, and every syncing app must see the same truth.

  • yolo-environment-{environment}.yml — the env manifest: the environment's canonical service domain and its env-shared services. yolo.yml is the app; yolo-environment-production.yml is the production environment. Seeded with defaults by the environment's first sync and never touched by sync again — every later edit is yours, made through the pull/push flow below.
  • .env — the env-shared secrets channel, the environment-tier sibling of each app's .env.{environment}. It holds generated service secrets (created on demand by the services that need them) and anything an env-shared service should read at provision time.

The bucket carries three more classes of object that are not yours to edit, all YOLO-owned:

  • apps/{app}.yml — each app's published claim (the app's name, the services it uses, and its bucket). Only sync:app writes it — an admin act, never a deploy — so the environment always knows which apps use which shared services and which bring-your-own buckets the data tiers must reach; a claim that lags the manifest is drift the deploy gate refuses on.
  • env/.env.{app} — each app's environment-side .env: YOLO-minted per-app secrets, one file per app. Today that's the app's scoped Typesense key, minted by sync:app and merged into the app's build. It lives here, not in the app's developer .env, precisely so the admin tier running sync (fenced from the per-app config buckets) can mint it — and so each app's build reads only its own key, never the env-shared .env's admin key or a sibling's.
  • yolo-version — the environment's version-of-record: the newest tagged codinglabsau/yolo release that has run sync:environment here (a dev-* pin never advances it, and an older release never lowers it). Sync's drift checks are only as current as the CLI running them, so a checkout with a stale vendor can plan against a newer environment and read "in sync" simply because it doesn't know about newer checks — every sync compares its own version to the marker. When a provably older CLI's plan holds a pending change in the account or environment tier, sync, sync:environment and sync:account refuse to apply it (an older release reconciling those tiers could walk a newer default back to its old value, and its plan couldn't flag it) — a plan that's clean there, or pending only in the app tier, proceeds. sync:app only warns, because an app's pinned release lagging the environment is the normal state between releases and its deploy gate must keep passing. Update the checkout and re-run. Nothing refuses when the two can't be ordered — a dev-* pin, an environment never stamped, or a marker the tier can't read.

The edit flow mirrors app env files:

bash
yolo environment:manifest:pull production    # → yolo-environment-production.yml (gitignored)
# edit
yolo environment:manifest:push production    # validated, key-level diff, confirm
yolo sync:environment production             # from any app in the environment

The env-shared .env moves the same way via environment:env:pull / environment:env:push (local copy .env.environment.production, gitignored).

Because every sync:environment pulls the manifest fresh from S3 and converges toward it, the environment's desired state has a single source of truth — apps pinned to different codinglabsau/yolo releases reconcile toward the same declared state instead of fighting over compiled-in defaults, within the schema each release knows: a manifest carrying keys from a newer release hard-fails older binaries with an upgrade hint, so update yolo across the environment's app repos before pushing a new key. Changing an env service's size is a file edit and a sync, not a delete-and-recreate.

Access is the boundary

S3 read on the env config bucket is what gates env-secret control. Deploying an app never requires it — the barrier to mutate the environment is deliberately higher than the barrier to ship an app.

Services ​

Some of what sync:environment provisions belongs to environment-backed services — the IVS event pipeline and the Typesense search cluster — each created under a declaration-driven lifecycle: provisioned while declared in the env manifest, torn down once the entry is removed (a declared service no app uses is flagged as idle, not torn down). The full model, and the need-to-know for every service (the app-side ones too), lives on its own page — see Services.

Web application firewall ​

Every environment with a load balancer gets a managed AWS WAF web ACL on its ALB — automatically, with no manifest key. It's compulsory infrastructure, like the ALB itself: one web ACL protects every app sharing the load balancer.

YOLO owns the policy — a baseline of AWS-managed protections, a per-IP rate limit, and a high-risk-country block — and reconciles it on every sync. You own the operational lists:

  • The allow and block IP sets are seeded empty for you to fill (known-good IPs to allow; abusive sources to block). Their contents are create-only — an entry you add in the console survives every subsequent sync.
  • The country block is seeded with a sensible default and is likewise yours to re-scope; it's seed-only, so your edits stick.
  • Any rule you add by hand is preserved too — YOLO only ever rewrites the rules it owns.

Tune the rest in the AWS console; YOLO won't undo it.

Blocked and counted requests are logged to CloudWatch Logs — the aws-waf-logs-yolo-{env} log group (the aws-waf-logs- prefix is mandated by WAFv2, so this is the one YOLO log group not named yolo-{env}-…), retained for 30 days. Each entry names the rule that matched, so you can answer why an IP was rejected and see what a Count-mode rule would have blocked — and the app's task role has read access to the stream, so an application can look up whether a given IP is tripping a rule. Allowed traffic is deliberately not logged here: the ALB's own access logs (in the env logs bucket) already record every request including WAF rejections, and duplicating the allow stream would multiply the logging cost for no extra signal. Logging is reconciled on every sync like the rest of the policy.

Alert alarms ​

Every environment gets a family of "a human should look" alarms — named …-alert-… to keep them distinct from the autoscaling alarms, whose ALARM state is part of their control loop and means nothing by itself. Every alert fires to the env's yolo-{env}-alarms SNS topic (state changes in both directions), and none of them should be in ALARM under normal operation — if an alert is red, something is genuinely wrong:

AlertScopeFires when
alb-5xxEnvironmentThe load balancer itself is generating 5xx — no healthy targets, a dead target group, a broken rule
valkey-memory / valkey-evictionsEnvironmentThe shared cache/session node is above 85% memory, or evicting heavily and sustained (background LRU churn on a full node is by design and doesn't fire)
database-cpu / -memory / -connections / -buffer-cacheEnvironment (when the manifest declares an Aurora cluster database — a plain RDS instance gets no database alerts)The cluster writer is saturating: CPU ≥ 80% sustained, freeable memory under 5% of the instance, connections past 75% of the class ceiling, or the working set no longer fitting in memory. The absolute thresholds derive from the writer's instance class at sync time; a Serverless v2 writer keeps the percentage-based pair only
web-5xxAppThe app is serving 5xx to ≥ 5% of its own requests (rate, not count, with a traffic floor so one error on a trickle can't page)

Topic subscriptions are yours — YOLO creates the topic but never subscribes endpoints. Subscribe an email or chat webhook to start; promote to a pager integration once the signal has earned trust. Thresholds are hardcoded — these are "bad things are happening" bars, not tuning knobs.

Every alert's threshold is also drawn as a red line on the matching CloudWatch dashboard chart (the app dashboard gains # Cache and buffer-cache panels so every alarmed metric has one). The lines and the alarms read the same values from one source, and the dashboard is rebuilt on every sync — so what the chart shows as the alarm bar is always exactly what pages.

Plan, confirm, apply ​

sync never surprises you. It runs as a three-step flow:

  1. Plan — YOLO inspects live AWS state and computes what would change, rendering it grouped by scope. Brand-new resources are listed under Will create (one + line each); drift on existing resources is shown under Pending changes as per-attribute diffs (current → desired). The plan is read-only, so it fans out across up to 8 worker processes — a full-environment plan takes seconds, not the better part of a minute. (No pcntl, or YOLO_PLAN_SEQUENTIAL=1 set? It runs in-process with identical output.)
  2. Confirm — you're shown the plan and asked to approve. If nothing has drifted, it short-circuits with "Already in sync" and exits without touching anything.
  3. Apply — only the changed steps run, sequentially and in declaration order — once writes start, ordering is the dependency contract.

The plan is always shown before the confirm, so there's no separate preview mode — to see what a sync would do, just run it and read the plan, then decline (or Ctrl-C) instead of confirming. Nothing is written until you approve.

Gate CI on drift with --check ​

--check runs the same read-only plan pass and prints the same diff, but never applies and exits non-zero when the environment has drifted (and 0 when it's already in sync):

bash
yolo sync production --check

Wire it into CI to fail a pipeline the moment infrastructure drifts from the manifest — someone hand-edited a resource, or a sync was never run after a manifest change. A non-zero exit also covers a plan that errored (bad credentials, an AWS API failure, an invalid manifest); in every case CI should stop and a human should look at the printed plan.

Skip the prompt with --force ​

In automation, skip the interactive confirmation:

bash
yolo sync production --force

Narrow to one tenant ​

For a multi-tenant app, limit the per-tenant steps to a single tenant (e.g. during a single-tenant cutover):

bash
yolo sync:app production --tenant=acme

See the sync command reference for every option.

Cache and sessions ​

Fargate tasks share nothing on their local filesystem, so a cache or session that lives there breaks the moment an app runs more than one task. Every app that runs tasks therefore gets the shared cache by default (web apps get the session store too) — opt out per app, rather than opt in to not being broken.

Cache ​

Any app with tasks — web or web-less worker — defaults to cache.store: redis — a shared ElastiCache for Valkey cache, one cluster per environment, isolated per app by a REDIS_PREFIX. Workers need it as much as web apps do: atomic locks, rate limiters, and onOneServer all sit on the cache. It's a single cache.t4g.micro node (a standard single instance; auto-failover/Multi-AZ off, ~A$11/mo) with allkeys-lru eviction, locked by a security group that only allows 6379 from the Fargate task SG. The container env is wired with CACHE_STORE=redis, REDIS_HOST and REDIS_PORT — each only if your .env doesn't already set it — plus REDIS_PREFIX, which is enforced: every app in the environment shares one node on the same logical databases, so the per-app prefix is the only thing keeping one app's keys off another's, and a conflicting value in your .env fails the build rather than shipping two apps onto one keyspace. Set cache.store: file|database|array to opt out.

Scaling is a deliberate vertical resize (a brief ~60s endpoint blip, data retained), not autoscaling — a cache evicts rather than runs out, and at this size the dollars don't justify a control loop.

Sessions ​

A web app defaults to session.driver: redis; set the key to override. YOLO provisions only what the chosen driver needs:

  • redis (default) — reuses the Valkey cache (needs cache.store: redis, the default; YOLO hard-fails if you opt the cache out without re-pinning the session driver). YOLO injects SESSION_DRIVER=redis only. Strong read-after-write consistency (~1 ms) means a freshly written session is readable immediately — no stale-read flicker right after login. The single node has no session HA — a node loss logs users out.
  • database / cookie / file — no infrastructure; YOLO just pins SESSION_DRIVER.

Sessions and cache share the node, not the keyspace ​

With both on redis, sessions and cache run on the same Valkey instance but on separate Redis logical databases — so they never collide and a cache:clear never touches sessions:

BackendLaravel redis connectionLogical DBDatabase env (default)
Cache (cache.store: redis)cacheDB 1REDIS_CACHE_DB (1)
Sessions (session.driver: redis)defaultDB 0REDIS_DB (0)

You get this with no dedicated session connection because three stock Laravel defaults stack:

  1. config/database.php ships two redis connections out of the box — default (database REDIS_DB, default 0) and cache (database REDIS_CACHE_DB, default 1).
  2. config/cache.php's redis store uses the cache connection → DB 1.
  3. The redis session handler routes by session.connection, not the cache store's connection: SessionManager::createRedisDriver() resolves the redis cache store and then overrides its connection with config('session.connection'). With SESSION_CONNECTION unset that's null, which Laravel's redis manager resolves to the default connection → DB 0.

That's why YOLO injects SESSION_DRIVER=redis only and deliberately leaves SESSION_CONNECTION unset — and it relies on cluster-mode-disabled Valkey (the YOLO default), since logical databases don't exist in cluster mode.

The split is inherited from your app's config, not enforced by YOLO

YOLO injects REDIS_HOST / REDIS_PORT (and enforces REDIS_PREFIX) but not REDIS_DB, REDIS_CACHE_DB, or SESSION_CONNECTION — the DB-0/DB-1 separation comes entirely from stock config/database.php + config/cache.php. If your app has dropped the cache connection, pointed both connections at the same database, or set REDIS_DB/REDIS_CACHE_DB to the same value, sessions and cache collapse onto one keyspace and a cache:clear will flush live sessions. Keep the two stock connections — or set SESSION_CONNECTION to a dedicated connection if you want sessions on a specific DB.

Cache high availability ​

The single cache node is a sound default — a node loss flushes the cache (it repopulates from source) and, since sessions ride the same Valkey cluster, logs users out; both are rare and low-stakes for most apps. If you need session durability across a node loss, add a Multi-AZ replica to the cluster (IncreaseReplicaCount + automatic failover) — a follow-up, not the default. If you only need graceful cache degradation without paying for a replica, use Laravel's first-party failover cache store (CACHE_STORE=failover, stores: ['redis', 'database']) — it falls through when the node is unreachable.

Failover isn't write-back

Writes that land in the fallback store during an outage are not synced back to Valkey when it recovers, so it's a degradation cushion, not a replica. It also only covers the cache store — sessions on the redis driver still depend on the node being up, so for session HA add a Multi-AZ replica rather than relying on failover.

Auditing what's deployed ​

yolo audit is the read-only health check for an environment. It rolls up three things and exits non-zero if any of them turns up an error — a green/red gate you can wire into CI or run by hand:

  1. A tag inventory — every resource tagged yolo:environment=<env>, classified ok or unexpected (the ownership check below).
  2. A drift check — the whole-stack sync --check plan, so "what's drifted from the manifest?" is part of the same answer (it reuses the deploy gate's machinery, read-only, no MFA).
  3. An RDS deletion-protection probe — reads the manifest database: target and verifies deletion protection is on (an error if it's off), reporting the instance/cluster basics alongside.
bash
yolo audit production

Errors fail the run (exit 1): unexpected resources, drift, and a database with deletion protection off. Warnings never do: e.g. a database the tier couldn't read, where protection can't be confirmed either way. Findings print in one block at the end.

The drift check and RDS probe are bare audit only. The scoped verbs — audit:environment, audit:app — stay focused inventory tools (they still exit non-zero on an unexpected resource, but run no drift or RDS probe).

The tag inventory ​

The inventory is the ownership half — it asks "is this resource accounted for?", not "does its config match the manifest?" (that attribute-level comparison is the drift check's job, via sync). There are two statuses, and a Reason column explains every unexpected row:

StatusMeaning
okAccounted for — yolo:app points at a live app, or it carries a yolo:scope=env/=account marker (declared shared infra).
unexpectedIn the environment's tag namespace but not accounted for. See the Reason.
Reason (on unexpected)Meaning
no ownership tagNo YOLO ownership marker (yolo:app/yolo:scope) — hand-rolled infrastructure, or alpha-era debris in the namespace.
service no longer provisionedYOLO-owned, but of an AWS service YOLO no longer provisions — there's no Resources/ class for it, so a sync would never create it. Left behind when support for a service is removed (the DynamoDB sessions table after DynamoDB sessions were dropped is the canonical case). Safe to delete once confirmed.
app cluster goneYOLO-owned, managed service, but yolo:app points at an app whose ECS cluster no longer exists — leftover resources from a removed app.

The service no longer provisioned check is driven by the catalogue of services YOLO has resource classes for, which mirrors the src/Resources/* directories. That makes it correct by construction: a managed service is never false-flagged, and the day a service is dropped its leftover resources surface automatically — no allow-list to keep in sync by hand.

The per-app dashboard isn't audited

sync:app also generates a CloudWatch dashboard (yolo-<env>-<app>-dashboard) panelling each of the app's ECS services (web, plus any extracted queue/scheduler — CPU/memory/tasks each), the ALB, SQS backlog, CloudFront, S3 and logs, plus an RDS panel sourced from the manifest database: key — omitted when it isn't set (the panel reads the manifest, never the app's secret .env). On an autoscaling web tier it also charts a worker-saturation panel with the burst trip threshold drawn on it. CloudWatch dashboards can't carry tags, so it's a read-only convenience that won't show up in yolo audit.

Like sync, audit is scope-grouped — narrow it with audit:environment <env> or audit:app <env> <app>, and add --unexpected to show only the rows needing attention:

bash
yolo audit production --unexpected
yolo audit:app production myapp

Full details in the audit command reference.

Released under the MIT License.