Wanderland

Pipelines Pack: Infrastructure Deployment

This tutorial picks up where AWS Adapter Modes left off and adds a deployment route to the same project. Yesterday we wrote one boundary that wraps one adapter call. Today we wire a second route — /deploy — to a chain that ships pre-built in the wanderland-pipelines-pack gem. Instead of writing each step in our lib/ directory, we name a registered archetype, fill in its knobs from config, and watch the chain fire end-to-end.

The deliverable is a working /deploy route. Today's chain runs through four no-op stubs — each one logs that it ran and returns a small payload. That's enough to see the chain end-to-end, confirm the wiring, and set the stage for the next tutorial where we replace the stubs with real shell-executor calls and start writing crossings under a per-run address.

What an archetype is

A route's chain is the sequence of boundaries the engine walks when the route is hit. Yesterday's /readout/:instance_id route declared its chain inline by naming instance_readout as a single boundary. The chain was bespoke to our project.

An archetype is a registered template for that chain. It lives in a YAML file shipped by a pack gem and looks like this:

name: infrastructure_deployment

slots:
  - name: start_run
    boundary: start_run
    args:
      system: !UserConfig system
      environment: !UserConfig environment

  - name: provision
    boundary: !UserConfig provisioner
    args:
      template: !UserConfig template
      parameters: !UserConfig parameters

  - name: publish
    boundary: publish_stack

  - name: complete
    boundary: complete_run

Some slots name a fixed boundary (start_run, publish_stack, complete_run). One — the provision slot — names its boundary as a !UserConfig thunk, so the user's config decides which boundary runs there. The args carry !UserConfig thunks too, so the values that vary per deploy come from your config rather than being hard-coded into the archetype.

When you put archetype: infrastructure_deployment on a route, the engine looks up the registered archetype, resolves the !UserConfig thunks against your route plus config data, and uses the resolved slot list as the route's chain. The chain shape is the archetype's; the values are yours; the implementation behind each knob is the one you named.

What the pipelines pack ships

wanderland-pipelines-pack is a gem we add to the Gemfile. When Bundler requires it, the gem does two things automatically:

Adding the pack costs one Gemfile line. The pack handles registration. Other packs (wanderland-aws-pack from yesterday, future wanderland-deploy-cdk-pack, ...) follow the same convention: glob and self-register at gem load.

1. Add the pipelines pack

In Gemfile, alongside wanderland-core and the AWS pack from yesterday:

# frozen_string_literal: true

source "https://rubygems.org"

gem "puma", "~> 6.0"
gem "wanderland-core",          path: "../wanderland-core"
gem "wanderland-aws-pack",      path: "../wanderland-aws-pack"
gem "wanderland-pipelines-pack", path: "../wanderland-pipelines-pack"
gem "aws-sdk-ec2", "~> 1.0"
gem "rake"

group :development, :test do
  gem "rspec", "~> 3.0"
  gem "rack-test", "~> 2.0"
end

Run bundle install. The pack lands; wanderland-core is already there.

config.ru from yesterday already calls run Wanderland.boot(...). Bundler's default-group autoload handles the pipelines pack the same way it does the AWS pack — no require line to add.

2. Wire a /deploy route to the archetype

Open config.yml. Underneath the existing /readout/:instance_id route, add:

routes:
  /readout/:instance_id:
    method: get
    boundary: instance_readout
    name: instance-readout

  /deploy:
    method: post
    name: deploy
    archetype: infrastructure_deployment
    provisioner: shell_provisioner
    system: web
    environment: prod
    template: stacks/web.yml
    parameters:
      instance_type: t3.medium
    tags:
      Owner: vivarta

Anatomy:

3. Add a deploy scenario

To run the chain through wanderland --type test, declare a scenario for the route:

scenarios:
  deploy:
    happy_path:
      description: "infrastructure_deployment archetype runs end-to-end"
      input:
        params: {}
      expected:
        completed: true

Anatomy:

4. Run the chain

WANDERLAND_AWS_MODE=mock bundle exec wanderland --type test config.yml

WANDERLAND_AWS_MODE=mock keeps yesterday's instance-readout scenarios passing without hitting AWS. The deploy scenario doesn't use the AWS adapter, so the env var doesn't affect it.

The output (abbreviated):

[start_run] ▶ run 01KR42WHDM3XG90S89Y1RQ2959 web/prod
[shell_provisioner] ▶ template="stacks/web.yml" parameters={"instance_type" => "t3.medium"}
[publish_stack] ▶ publish phase ran
[complete_run] ▶ run complete

deploy
  ✓ happy_path  — infrastructure_deployment archetype runs end-to-end
instance-readout
  ✓ instance_is_running
  ✓ instance_not_found
  ✓ instance_is_lazarus
  ✓ instance_is_lazarus_but_precise

The four boundaries log in chain order. Each one returns its OK signal; the engine stitches the payloads together into the route's response context. The completed: true from complete_run is what the scenario asserts against.

5. What each step does

The chain's structure is the archetype's contribution. The values are yours. The choice of which boundary fills each !UserConfig slot is yours.

6. Inspect the resolved chain

The framework's introspection routes show what the engine mounted. Boot the HTTP runner in a second shell:

bundle exec puma config.ru -p 9298

Then ask for the route definition:

curl -s http://localhost:9298/inspect/route/deploy | python3 -m json.tool
{
  "name": "deploy",
  "method": "post",
  "path": "/deploy",
  "chain": ["start_run", "shell_provisioner", "publish_stack", "complete_run"]
}

The chain field shows the resolved boundary names. The archetype's !UserConfig provisioner thunk has been replaced with shell_provisioner because that's what your config named.

To see what changes when you swap implementations, change provisioner: shell_provisioner in config.yml to a different registered boundary, restart, and re-inspect. The same archetype yields a different chain because a different boundary fills the provision slot.

7. Top-level vs route-level archetype

The same archetype: field works at two scopes. At the top level it's the default for every route:

archetype: infrastructure_deployment
provisioner: shell_provisioner
system: web
environment: prod
template: stacks/web.yml
parameters:
  instance_type: t3.medium

routes:
  /deploy:
    method: post
    name: deploy
  /destroy:
    method: post
    name: destroy

Both /deploy and /destroy use infrastructure_deployment with shell_provisioner. Per-route configurations layer on top; route-level fields override the top-level ones with the same key:

archetype: infrastructure_deployment
provisioner: shell_provisioner
system: web
environment: prod
template: stacks/web.yml

routes:
  /deploy:
    method: post
    provisioner: cdk_provisioner   # this route uses cdk
  /destroy:
    method: post
                                    # falls back to shell_provisioner

/deploy runs the chain with cdk_provisioner filling the provision slot. /destroy falls back to the top-level shell_provisioner. Route-level overrides everywhere — provisioner, system, template, anything the archetype reads.

When every route uses the same archetype, declaring it once at the top level is cleaner than repeating it per route. When routes use different archetypes (one runs infra, another runs container deploys), declare per route and leave the top level open.

8. Directory shape now

instance-readout/
├── Gemfile
├── Gemfile.lock
├── Rakefile
├── config.ru
├── config.yml          # /readout from yesterday + /deploy from today
├── lib/
│   ├── instance_readout.rb
│   └── instance_readout/
│       └── boundaries/
│           └── instance_readout.rb
└── spec/
    ├── spec_helper.rb
    ├── scenarios_spec.rb
    └── fixtures/
        └── cassettes/
            └── ec2/
                └── ...yml

No new files in your project. The deploy chain's boundaries live in wanderland-pipelines-pack; your config picks them. The same shape extends to other archetypes the pack will ship — containerized_deployment, tool_invocation — by adding them to the gem and naming them in archetype:.

What's next

Site Audit

wanderland.dev

oculus-view: fence: fence execute HTTP 404