gobridge

CDK Scenario 1: Quickstart with Default VPC

Overview

Deploy one GoBridge task on Amazon Elastic Container Service (ECS) Fargate. The example creates a Virtual Private Cloud (VPC) and a shared config filesystem.

Use Case

You are a developer evaluating GoBridge and want a running instance as quickly as possible. You have an AWS account and a VPC (or let your CDK app create one), but no ECS cluster or EFS filesystem. The gobridge.NewSingle facade construct creates the ECS service, EFS filesystem, mount, and Identity and Access Management (IAM) grants. The bridge process creates absent config using the initial document embedded in your image; no seeder container is needed.

Architecture

flowchart LR
    subgraph AWS Account
        subgraph VPC ["New VPC (2 AZs)"]
            subgraph Fargate ["ECS Fargate"]
                Task["gobridge task\n512 CPU / 1024 MiB"]
            end
            EFS["EFS\n/gobridge/bridge.yaml"]
        end
    end

    Client["Developer with VPC access"] -->|HTTP| Task
    Task -->|NFS mount\n/var/lib/gobridge| EFS

    style Task fill:#f96,stroke:#333
    style EFS fill:#6bf,stroke:#333

The construct provisions:

The single facade runs exactly one task (DesiredCount is hard-coded to 1, a runtime invariant of the single EFS RW writer) and has no autoscaling. Scale horizontally with the cluster facade instead (Scenario 5). You supply the VPC — the single facade does not create or look one up.

Prerequisites

Requirement Minimum version Check command
AWS account aws sts get-caller-identity
AWS CLI 2.x aws --version
AWS CDK CLI 2.x cdk --version
Go 1.25+ go version
Docker 20.x+ docker --version
CDK bootstrapped cdk bootstrap aws://ACCOUNT/REGION

Ensure your shell has valid AWS credentials:

export CDK_DEFAULT_ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
export CDK_DEFAULT_REGION=us-west-1

Set Up the Consumer Module

Your CDK app is an ordinary Go module. It does not clone this repository and it does not need a replace directive:

mkdir gobridge-quickstart && cd gobridge-quickstart
go mod init example.com/gobridge-quickstart
go get github.com/mariotoffia/gobridge/deployment/aws/cdk@vX.Y.Z

# The CDK CLI needs to know how to run your app.
printf '{"app": "go run ."}\n' > cdk.json

Run go mod tidy after writing the stack below, before cdk deploy. go get on a module path records the requirement but not the go.sum entries for what that module’s own code imports, and the build fails on every missing one. go mod tidy also adds the infra module the constructs depend on.

That one vX.Y.Z covers the constructs, the declaration types and the bridge binary: the stack builds GoBridge at the same version. Pick a version whose train includes the profile modules (RELEASE.md).

Container Image

Save the bridge configuration as bridge.yaml next to your CDK app before synthesizing. The same document describes the image’s initial config and the CDK declaration.

With Image left unset, the construct builds the image for you — no Git checkout and no docker build of your own. cdk synth only stages the build context: a generated Dockerfile plus the facade’s parsed BridgeConfig. Everything else happens during cdk deploy, when Docker downloads the profile command at the version your app depends on, copies its owning module to a writable directory, fills the fixed embed file, runs go build, and pushes the image to your CDK bootstrap asset repository. An app whose cdk module is replaced or not a released version fails at synth; a version that does not exist, or an unreachable module proxy, fails during cdk deploy — after a stack update has begun. An app built against a local replace sets Image: gobridge.ImageFromGoBuild(gobridge.GoBuild{Version: "vX.Y.Z"}). An AMQP or Azure Service Bus config needs no extra step: the build derives its gobridge_amqp091, gobridge_amqp10 or gobridge_azure tag from the config, and the binary links that transport.

The result is the same multi-stage, CGO_ENABLED=0 (pure-Go SQLite via modernc.org/sqlite), distroless/static-debian12 image running as nonroot UID 65532, with a HEALTHCHECK that runs the binary directly (-healthcheck, which probes the local monitor /live endpoint). It needs cdk bootstrap, a running Docker daemon that can pull the two digest-pinned base images, outbound network from the build container to the Go module proxy, and credentials that may push to the bootstrap asset repository in ECR.

The build verifies the binary’s -initial-config-digest output against the staged document, so an image can never disagree with the config the stack declares. Optional plugin families are derived from that config; a family must be wired into the version you build. Literal credentials may be embedded, but artifact readers can recover them; Base64 does not hide them.

On a version whose train predates the profile modules, or if you would rather run your own image — an air-gapped registry, a custom Package, or a build pipeline you already own — build it from this repository’s root Dockerfile (Container image) and use gobridge.ImageFromRegistry("...@sha256:<digest>") or gobridge.ImageFromEcr(repo, tag) instead. CDK cannot modify those images: they must carry their own initial document, find an existing target, or wait idle for operator creation, while ConfigFile still drives validation and grants. See CDK image sources.

Create SSM Parameter

The admin API requires an API key stored in AWS Systems Manager Parameter Store. The Fargate task reads it at startup via the AdminAPIKeyParam bootstrap field.

aws ssm put-parameter \
  --name /gobridge/admin-api-key \
  --type SecureString \
  --value "my-secret-admin-key-min16chars" \
  --region us-west-1

The value must be at least 16 characters. Choose a strong, random string for production use.

CDK Stack

There is no prebuilt env-driven CDK entrypoint; you write a small CDK app that instantiates the gobridge.NewSingle facade. The facade takes a *gobridge.SingleProps. Its required fields are Vpc, Bootstrap, and BridgeConfig; everything else falls back to documented defaults (Go-built image, CPU 512, MemoryMiB 1024, MountPath /var/lib/gobridge).

App

package main

import (
	"github.com/aws/aws-cdk-go/awscdk/v2"
	"github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
	"github.com/aws/jsii-runtime-go"

	"github.com/mariotoffia/gobridge/deployment/aws/cdk/gobridge"
)

func main() {
	app := awscdk.NewApp(nil)
	stack := awscdk.NewStack(app, jsii.String("GoBridgeQuickstart"), &awscdk.StackProps{
		Env: &awscdk.Environment{
			Account: jsii.String("<account>"),
			Region:  jsii.String("us-west-1"),
		},
	})

	// Provide a VPC (create one, or look up an existing VPC by ID/tags).
	vpc := awsec2.NewVpc(stack, jsii.String("Vpc"), &awsec2.VpcProps{MaxAzs: jsii.Number(2)})

	gobridge.NewSingle(stack, "Single", &gobridge.SingleProps{
		Vpc: vpc,
		Bootstrap: gobridge.Bootstrap{
			BridgeID:         "gobridge-main",
			ConfigFilePath:   "/var/lib/gobridge/bridge.yaml",
			AdminAPIKeyParam: "/gobridge/admin-api-key",
		},
		// This document is validated at synth and embedded into the image.
		BridgeConfig: gobridge.ConfigFile("bridge.yaml"),
	})

	app.Synth(nil)
}

Deploy

cdk deploy --require-approval broadening

The facade serializes the Bootstrap settings (after defaults are applied) into the task container as the GOBRIDGE_AWS_BOOTSTRAP_JSON environment variable:

{
  "bridge_id": "gobridge-main",
  "node_role": "control",
  "topology": "single",
  "config_file_path": "/var/lib/gobridge/bridge.yaml",
  "admin_addr": ":8080",
  "monitor_addr": ":8081",
  "transport_http_addr": ":8082",
  "admin_api_key_param": "/gobridge/admin-api-key"
}

Initial bridge config

The control process creates the file only when it is definitively absent. Existing operator edits win, and creation races reread the winning document. The Fargate task reads config at /var/lib/gobridge/bridge.yaml on the EFS mount. Create a minimal config that accepts HTTP POST requests and republishes them as Server-Sent Events for testing.

Bridge configuration

Save the following as bridge.yaml locally:

bridge:
  id: gobridge-main
  log_level: info

receivers:
  - id: http-in
    transport: http
    options:
      path: /ingest

senders:
  - id: sse-out
    transport: http
    options:
      path: /events
      mode: sse

bindings:
  - id: to-sse
    sender_id: sse-out
    address: events

stores:
  # The default policy dead-letters permanent failures; the quickstart keeps
  # them in memory and acknowledges that a restart loses them. Scenario 7
  # shows a durable DLQ.
  dlq:
    type: memory
    options:
      acknowledge_volatile: true

routes:
  - id: forward
    receiver_id: http-in
    bindings: [to-sse]

This config creates a single route: HTTP POST requests to /ingest on the transport HTTP port (8082) are republished as Server-Sent Events to clients streaming from /events.

Later config changes

Changing the embedded document does not update an existing target. Use an admin config transaction or an atomic external writer; see configuration updates. Do not delete the target to force an update: confirmed absence stops new intake and drains the runtime to idle. That process will not initialize it again. A fresh process may initialize an absent target.

Verify

After deployment, verify liveness and readiness separately. Missing config keeps a valid bootstrap live but not ready. A read failure before activation leaves the data plane idle with an error.

Health check

This stack does not create a load balancer. Run probes on a host with VPC routing to the task’s private address and permit that host’s narrow source range in the task security group. The monitor uses port 8081, separate from admin on 8080 and message transport on 8082.

TASK_IP=10.0.1.23 # Replace with your task's private address.
ADMIN_ENDPOINT="http://${TASK_IP}:8080"
MONITOR_ENDPOINT="http://${TASK_IP}:8081"
curl --fail-with-body "${MONITOR_ENDPOINT}/api/v1/monitor/live"
curl --fail-with-body "${MONITOR_ENDPOINT}/api/v1/monitor/ready"

Require successful readiness before sending messages. A successful liveness probe alone does not prove that the bridge has activated a configuration.

Admin config API

Retrieve the running configuration:

curl -s -H "X-API-Key: my-secret-admin-key-min16chars" \
  "${ADMIN_ENDPOINT}/api/v1/admin/config" | jq .

Send a test message

The receiver accepts an HTTP POST at /ingest; the SSE sender republishes it to clients streaming from /events. Stream the sender output in one terminal:

curl -N "http://${TASK_IP}:8082/events"

Then POST a message to the ingress in another:

curl -s -X POST \
  -H "Content-Type: application/json" \
  -d '{"sensor":"temp-1","value":23.5}' \
  "http://${TASK_IP}:8082/ingest"

The /events stream emits the posted message as a data: event.

Clean Up

Remove all provisioned resources:

cd <your-cdk-app>
cdk destroy

EFS retention warning – The EFS filesystem uses the default RETAIN removal policy. After cdk destroy, the filesystem and its data persist in your account. Identify the exact retained filesystem and its mount targets before deleting it. Deletion permanently removes its configuration and any stored message state.

Also clean up the SSM parameter:

aws ssm delete-parameter --name /gobridge/admin-api-key --region us-west-1

The Go-built image is published into the shared CDK bootstrap asset repository. Leave that repository in place — other CDK stacks in the account use it — and prune old image assets with an ECR lifecycle policy instead (Container image).

What’s Next