Wanderland

Vivarta ALB MVP Walkthrough

A concrete mock-up of an ALB deploy through the Vivarta substrate, end-to-end. One CLI call, one config file, one CFN template, three boundaries, and the table of every crossing the run produces.

The ALB is intentionally trivial: two listeners, default 404 fixed-response. The point isn't the deliverable. It's seeing every address, type, and payload the substrate emits, so we can audit the model before any boundary code lands.

1. The flow

One CLI call. The yaml file is the deploy specification. Everything else falls out of running it.

wanderland --type cli config.yml deploy

The engine boots from config.yml, dispatches to the /deploy route, walks its chain (start_run → create_stack → publish_stack), and exits with the run's exit-crossing summary on stdout. Exit code is zero on success, non-zero on failure.

For replay-on-failure, the same call with VIVARTA_RUN_ID set picks up where the previous run left off — covered in section 5.

A future evolution will split config.yml into two files: an engine config (stable across deploys) and a deployment config that varies per deploy. The CLI will bind them via a flag like --deployment-config <yaml> — the deployment yaml acts as the request body, and the same engine config drives many different deploys. For tonight, both halves live in one file.

2. config.yml

One file. Defines engine setup (boundary_path, storage, adapters, resolvers), identity values (system, env, sub_env), CFN inputs (template, parameters, tags), and the route that wires the chain.

service: proquest-alb-mvp
boundary_path: lib/proquest_alb/boundaries

resolvers:
  Fixture:
    fixture_dirs: [spec/fixtures]

storage:
  mounts:
    ":advaita:":
      driver: sqlite
      path: !Env { name: ADVAITA_DB, default: /var/wanderland/advaita.db }

adapters:
  cfn:
    kind: aws
    service: cloudformation
    mode: !Env { name: WANDERLAND_AWS_MODE, default: live }
    region: us-east-1

# Identity in the substrate tree
system: proquest-alb
environment: dev
sub_environment: main

# CFN inputs
template_path: templates/alb-with-404.yml
parameters:
  ALBScheme: internal
  AvailabilityZoneA: us-east-1a
  AvailabilityZoneB: us-east-1c
  AvailabilityZoneC: us-east-1f
  IdleTimeout: "300"
  VpcId: vpc-0abc123def456789
  SubnetA: subnet-0aaa111
  SubnetB: subnet-0bbb222
  SubnetC: subnet-0ccc333
  CertificateArn: arn:aws:acm:us-east-1:123456789012:certificate/abcd-1234

tags:
  business_unit:    proquest
  namespace:        pqp
  subsystem:        alb
  component:        edge
  environment:      dev
  sub_environment:  main
  version:          0.0.1-mvp

routes:
  /deploy:
    method: post
    name: deploy
    chain:
      - boundary: start_run
      - boundary: create_stack
        args:
          adapter: cfn
      - boundary: publish_stack
        args:
          adapter: cfn

Anatomy:

3. CFN template — templates/alb-with-404.yml

Internal ALB, two listeners (HTTP → HTTPS redirect, HTTPS → fixed 404). Three subnets across AZs.

AWSTemplateFormatVersion: "2010-09-09"
Description: Advaita ALB MVP — two listeners, default 404 fixed-response, nothing to see here.

Parameters:
  ALBScheme:        { Type: String, AllowedValues: [internal, internet-facing] }
  AvailabilityZoneA: { Type: AWS::EC2::AvailabilityZone::Name }
  AvailabilityZoneB: { Type: AWS::EC2::AvailabilityZone::Name }
  AvailabilityZoneC: { Type: AWS::EC2::AvailabilityZone::Name }
  IdleTimeout:      { Type: Number, Default: 300 }
  VpcId:            { Type: AWS::EC2::VPC::Id }
  SubnetA:          { Type: AWS::EC2::Subnet::Id }
  SubnetB:          { Type: AWS::EC2::Subnet::Id }
  SubnetC:          { Type: AWS::EC2::Subnet::Id }
  CertificateArn:   { Type: String }

Resources:
  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Advaita ALB MVP — ports 80/443 inbound
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - { IpProtocol: tcp, FromPort: 80,  ToPort: 80,  CidrIp: 0.0.0.0/0 }
        - { IpProtocol: tcp, FromPort: 443, ToPort: 443, CidrIp: 0.0.0.0/0 }

  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: !Ref ALBScheme
      Type:   application
      SecurityGroups: [!Ref ALBSecurityGroup]
      Subnets: [!Ref SubnetA, !Ref SubnetB, !Ref SubnetC]
      LoadBalancerAttributes:
        - { Key: idle_timeout.timeout_seconds, Value: !Ref IdleTimeout }

  ListenerHTTP:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref LoadBalancer
      Port: 80
      Protocol: HTTP
      DefaultActions:
        - Type: redirect
          RedirectConfig: { Protocol: HTTPS, Port: "443", StatusCode: HTTP_301 }

  ListenerHTTPS:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref LoadBalancer
      Port: 443
      Protocol: HTTPS
      Certificates: [{ CertificateArn: !Ref CertificateArn }]
      DefaultActions:
        - Type: fixed-response
          FixedResponseConfig:
            StatusCode:  "404"
            ContentType: text/plain
            MessageBody: "nothing to see here, move along"

Outputs:
  LoadBalancerArn:          { Value: !Ref LoadBalancer }
  LoadBalancerDNSName:      { Value: !GetAtt LoadBalancer.DNSName }
  LoadBalancerHostedZoneID: { Value: !GetAtt LoadBalancer.CanonicalHostedZoneID }
  ListenerHTTPArn:          { Value: !Ref ListenerHTTP }
  ListenerHTTPSArn:         { Value: !Ref ListenerHTTPS }
  SecurityGroupId:          { Value: !Ref ALBSecurityGroup }

4. The chain — three boundaries

Each boundary is a small class that reads the deploy values from runtime.config.domain and writes crossings under the run-tree address. They live in the wanderland-pipelines library when that gem is broken out (or in lib/proquest_alb/boundaries/ for now).

start_run

Opens the run record. Generates a ULID for the run-id (or reads VIVARTA_RUN_ID from env for replay), computes the run-tree address (:advaita:<system>:<env>:<sub_env>:runs:<run_id>), and writes metadata, tags, and an initial lifecycle:new crossing under it. Returns the run-id and run-tree address in the result so downstream boundaries know where to write.

create_stack

Reads template path, parameters, and tags from runtime.config.domain. Pulls the configured CFN adapter via input.adapter(:cfn). Auto-detects capabilities from the template (CAPABILITY_IAM, CAPABILITY_NAMED_IAM if the template creates IAM resources). Generates a deterministic stack name from tags. Calls the CFN change-set probe to inspect the diff, then execute_change_set (or the direct create_stack path for a fresh stack). Polls describe_stacks until completion. Writes provision crossings under the run-tree address for each provider event.

The boundary checks for its own completion crossing before doing work. If create-complete is already there from a prior run with the same run-id, it skips and returns immediately. The replay model in section 5 covers the contract.

publish_stack

Reads the stack identifier from prior crossings (the create-complete written by create_stack). Calls describe_stacks for outputs and describe_stack_resources for the resource list. Writes each CFN output as a crossing at <run_addr>:outputs:<key>. Writes each resource as a crossing at <run_addr>:resources:<logical_id>. Writes a lifecycle:active crossing and an active-pointer crossing at <slot_addr>:active so the deploy is externally identifiable. Closes with a run:complete crossing to mark the run done.

In the old peregrine model, what publish_stack does here was split across nimbus publish_resources, nimbus publish (deployment record), nimbus activate, and nimbus complete_deployment. Folded into one boundary because all four happen at the same logical moment: the deploy succeeded, this run is now the active one in its slot.

5. Crossing table

Every crossing a successful run produces. Run-id mocked as 01HZJ9X8K5YQR4T6V8B2WFGM3Q.

Address abbreviations:

# to_addr from_addr type_addr Payload
1 R:metadata B:start_run :types:run:metadata {run_id, system, environment, sub_environment, started_at}
2 R:tags B:start_run :types:run:tags full tag bundle from config
3 R:lifecycle:new B:start_run :types:lifecycle:new {at, reason: "run_started"}
4 R:provision:requested B:create_stack :types:cfn:create-requested {stack_name, template_ref, parameters, tags, capabilities} (capabilities auto-detected)
5 R:provision:change-set:created B:create_stack :types:cfn:change-set-created {change_set_id, summary, changes_count}
6 R:provision:change-set:executed B:create_stack :types:cfn:change-set-executed {change_set_id, executed_at}
7 R:provision:waiting B:create_stack :types:cfn:wait {state: "CREATE_IN_PROGRESS", elapsed_s} (one or more, periodic)
8 R:provision:complete B:create_stack :types:cfn:create-complete {stack_id, stack_name, status: "CREATE_COMPLETE", duration_s}
9 R:outputs:LoadBalancerArn B:publish_stack :types:cfn:output {key, value}
10 R:outputs:LoadBalancerDNSName B:publish_stack :types:cfn:output {key, value}
11 R:outputs:LoadBalancerHostedZoneID B:publish_stack :types:cfn:output {key, value}
12 R:outputs:ListenerHTTPArn B:publish_stack :types:cfn:output {key, value}
13 R:outputs:ListenerHTTPSArn B:publish_stack :types:cfn:output {key, value}
14 R:outputs:SecurityGroupId B:publish_stack :types:cfn:output {key, value}
15 R:resources:LoadBalancer B:publish_stack :types:cfn:resource {logical_id, physical_id, resource_type: "AWS::ElasticLoadBalancingV2::LoadBalancer"}
16 R:resources:ListenerHTTP B:publish_stack :types:cfn:resource (same shape, per resource)
17 R:resources:ListenerHTTPS B:publish_stack :types:cfn:resource (same shape)
18 R:resources:ALBSecurityGroup B:publish_stack :types:cfn:resource (same shape)
19 R:lifecycle:active B:publish_stack :types:lifecycle:active {at, prior_state: "new"}
20 S:active B:publish_stack :types:slot:active-pointer {run_id, at, prior_run_id: null}
21 R:complete B:publish_stack :types:run:complete {at, status: "ok", crossings_count: 20, duration_s}

Notes on the table:

6. Replay model — Jenkins, Docker, idempotent boundaries

The substrate's append-only nature plus run-id-keyed addressing gives us a clean replay-on-failure flow. Each Jenkins job is small, the deploy runtime is a Docker container, and boundaries skip themselves if the completion crossing they would have produced is already in the run's sub-stream.

The Jenkins side

Each pipeline job:

Jenkins reads the result. On success, the job passes. On failure, Jenkins re-fires the same job with the same VIVARTA_RUN_ID env var.

docker run --rm \\
  -e VIVARTA_RUN_ID=01HZJ9X8K5YQR4T6V8B2WFGM3Q \\
  -v $PWD:/repo \\
  -v advaita-data:/var/wanderland \\
  vivarta-runner:latest --type cli /repo/config.yml deploy

The :advaita: SQLite mount is a named Docker volume (or EFS mount) shared across runs so prior crossings are visible.

Replay-by-run-id

The replay contract: each boundary checks for its completion crossing before doing work; if present, the boundary skips. Re-running with the same run-id picks up where the previous run failed.

The skip check — (from_addr, type_addr)

Every pipeline boundary declares an idempotency signature: a (from_addr, type_addr) pair identifying the crossing it produces when complete. The check is:

Query the substrate for crossings under the run's sub-stream where from_addr == this_boundary.identity and type_addr == this_boundary.completion_type. If any exist, skip; otherwise run.

The query uses the existing wanderland-core context surface (context.signed.from_boundary(name).with_type_addr(type)), but reads against runtime.storage directly so prior crossings outside the current request context are visible.

Per-boundary completion signatures for the ALB walkthrough:

Boundary Completion signature (from_addr, type_addr) Skip means
start_run (B:start_run, :types:run:metadata) run record already exists
create_stack (B:create_stack, :types:cfn:create-complete) stack create already finished
publish_stack (B:publish_stack, :types:run:complete) publish + lifecycle + active-pointer all done

Boundaries that emit multiple intermediate crossings (e.g. create_stack writes change-set-created, change-set-executed, wait, create-complete) check only the terminal type. Mid-stream re-entry of a single boundary isn't supported — if it failed mid-way, the next replay starts that boundary fresh and writes new intermediate crossings. The terminal crossing is what marks the boundary skippable on later replays.

Mid-stream resumption within a boundary

For boundaries that touch external state (CFN stack create), the inline-skip-on-completion isn't enough — the external call may have started but not finished. The boundary's own first action (after the skip check) handles this. For create_stack:

This pattern is per-boundary; it's not a substrate concern. Each boundary's author chooses the right reattachment strategy for its provider. The substrate just provides the addressable history.

Implementation shape — three options, deferred

The skip check uses the same (from, type) signature regardless of where it lives:

Pick when the first boundary lands. The contract — (from, type) is sufficient, signatures declared per-boundary — is what's load-bearing.

Review prompts

What to scrutinize before code lands: