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 Admin tier — the yolo-{env}-admin write policy and the yolo-{env}-admin-role that carries it (observer read policy + admin write policy), which yolo sync, yolo scale and yolo permissions assume to cap a provisioning 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}-admins) whose membership grants the read / 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 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 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 3306 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 3306 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 BYO 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 two more classes of object that are not yours to edit, both YOLO-owned:

  • apps/{app}.yml — each app's services file (the app's name and the services it uses). Every deploy and every sync:app rewrites it, so the environment always knows which apps are using which shared services.
  • 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.

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.

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, REDIS_PORT and REDIS_PREFIX — each only if your .env doesn't already set it. 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 / 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 Octane 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.