Skip to content

Feat/bootstrap example#102

Draft
browdues wants to merge 4 commits into
mainfrom
feat/bootstrap-example
Draft

Feat/bootstrap example#102
browdues wants to merge 4 commits into
mainfrom
feat/bootstrap-example

Conversation

@browdues

Copy link
Copy Markdown
Contributor

No description provided.

@browdues browdues force-pushed the feat/bootstrap-example branch 3 times, most recently from 27671fb to 263dcd6 Compare June 16, 2026 14:39
@browdues browdues force-pushed the feat/bootstrap-example branch from f36bc58 to 3bdc937 Compare June 18, 2026 04:57
@browdues browdues force-pushed the feat/bootstrap-example branch from 3bdc937 to 1ee823b Compare June 18, 2026 16:38

@JeroenSoeters JeroenSoeters left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! This will be a much better experience for users than hand-rolling commands, and hopefully we can also use this to replace the bootstrap rollbook with manual steps for our own prod agent. A couple of things before we can merge this:

  • The task role has no AWS permissions. In both main.pkl and existing-db.pkl the task role grants only ssmmessages:* (ECS Exec). The agent boots but 403s on the first discovery/apply. Take a look in the public docs where we recently updated the guidance on permissions: PowerUserAccess plus an inline policy for IAM management and read-only iam:Get*/iam:List* (without the latter, discovery spams 403s on iam:ListServerCertificates, iam:ListSAMLProviders, etc.).

  • The generated DB password lands in plaintext in the task definition (main.pkl). The from-scratch path passes it as a plain env var (the inline comment already flags this trade-off), so it's exposed in the task def, formae eval, and CloudWatch. The fix is to inject it via ECS secrets[].valueFrom = dbSecret.res.arn instead of an env var. That capability is already on main (PRs #89 + #105); this branch predates it, so rebasing onto current main and switching to the ARN should drop the plaintext path entirely. Keep the self-managed, non-rotating secret you already generate; don't switch to manageMasterUserPassword, since RDS rotates that secret on a schedule and ECS only injects secrets at task start, so the running agent would keep a stale password and break on the first rotation until it's redeployed. Worth a README line that rotating the DB password means updating the secret plus aws ecs update-service --force-new-deployment.

  • The agent API is a plaintext, unauthenticated, internet-facing endpoint. Public subnets plus assignPublicIp=ENABLED plus an internet-facing ALB with 0.0.0.0/0 ingress on an HTTP listener shouldn't be the out-of-the-box default for a control plane. Model ingress as a choice the way modules/lgtm.pkl does with its visibility flag, and have the public option terminate HTTPS plus require the auth-basic plugin:

    visibility: "public" | "internal" = "public"
    local albScheme      = if (visibility == "internal") "internal" else "internet-facing"
    local externalCidr   = if (visibility == "internal") vpcCidr else "0.0.0.0/0"
    local assignPublicIp = if (visibility == "internal") "DISABLED" else "ENABLED"

    "internal" flips the ALB scheme, scopes the SG ingress to the VPC CIDR, and drops public task IPs (reach it via VPN/peering); "public" stays laptop-reachable but over HTTPS plus auth instead of plaintext-open.

  • --region is a no-op, and there's no --profile. The region property is declared but never wired in: the target reads module.region directly and the AZs are hardcoded (us-east-2a/b), so passing --region eu-west-1 changes nothing and would try to put subnets in the wrong region anyway. Route the property through the target config, and derive the AZs from it ("\(region)a" / "\(region)b", with overrides for odd regions). While you're there, add an optional --profile. aws.Config already supports it (profile: String?), so folks can pick which local credentials provision the stack. Worth noting in the README that --region/--profile govern the local apply (the creds used to build the infra), not the deployed agent, which authenticates via its ECS task role.

  • Three different formae versions in one PR. examples/PklProject pins formae@0.83.0, vars.pkl sets the image to formae:0.86.0, and the README config table says formae:0.83.2. Pin one across all three: 0.86.x (the plugin HEAD already requires 0.86.2).

  • The task is undersized and hardcoded. cpu="512" / memory="1024" is below even the smallest tier in the agent sizing guide and will likely OOM under real discovery. Add a size t-shirt property mapped to Fargate-valid CPU/memory pairs so users can scale it without hand-picking values:

    // small .5vCPU/1GB · medium 1vCPU/2GB · large 2vCPU/4GB · xlarge 4vCPU/8GB
    size = new formae.Prop { flag = "size"; default = "small" }

    (Heads-up: the doc table's raw numbers aren't all Fargate-legal. 1 vCPU needs >=2 GB, 2 vCPU needs >=4 GB, so round up to valid combos. Happy to reconcile the docs table separately.)

  • No discovery filter, so the agent will surface its own substrate. Once a target is registered against the new agent, discovery enumerates the very VPC/RDS/ECS/ALB it runs on and reports them as unmanaged. The public docs already prescribe the fix: the discoveryFilters block in the Express install guide excludes the agent's own infra for exactly this reason. Tag every bootstrap resource with a consistent key (e.g. app=formae-agent) and emit a matching discoveryFilters block into the agent config so the first discovery cycle is already clean.

  • Structure & naming. main.pkl vs existing-db.pkl reads as confusing since they're peers, and the two files duplicate ~300 lines of VPC/roles/ALB/service. Suggest collapsing to a single bootstrap.pkl entrypoint with a database = "new" | "existing" flag, factoring the shared resources into lib/ modules and lifting the scalars plus sizing table into vars.pkl / sizing.pkl, so the sizing table and shared properties have one home and the security-critical bits (task role, discovery filter) live in one place instead of duplicated.

    Sketch of the end state
    examples/bootstrap/
      bootstrap.pkl        # single entrypoint
      vars.pkl             # scalars + mkTarget(region, profile) + azFor(region)
      sizing.pkl           # t-shirt size -> Fargate-valid cpu/mem
      lib/
        network.pkl        # VPC + subnets + routes + SGs
        database.pkl       # RDS + self-managed secret   (imported only in "new" mode)
        ingress.pkl        # ALB + visibility flag
        agent.pkl          # task def + service + roles + config (incl. discoveryFilters)
    
    // bootstrap.pkl, the entrypoint becomes thin wiring
    local dbMode = properties.database.value   // "new" | "existing"
    
    local net   = new network.Network { name = n; vpcCidr = vars.vpcCidr; /* azs from region */ }
    local db    = if (dbMode == "new") new database.Database { name = n; subnetIds = net.subnetIds } else null
    local front = new ingress.Ingress { name = n; vpcId = net.vpcId; visibility = properties.visibility.value }
    local app   = new agent.Agent {
      name = n
      size = sizing.forSize(properties.size.value)
      dbHost      = if (dbMode == "new") db.endpointAddress else properties.dbHost.value
      dbSecretArn = if (dbMode == "new") db.secretArn      else properties.dbPasswordSecretArn.value
      targetGroupArn = front.targetGroupArn
      discoveryFilterTag = "formae-agent"
    }
    
    forma {
      vars.stack
      vars.mkTarget(properties.region.value, properties.profile.value)
      ...net.resources
      ...(if (dbMode == "new") db.resources else new Listing {})
      ...front.resources
      ...app.resources
    }

Comment thread pkg/ccx/client.go
// concurrently with an apply. Resolve-reads (resolvable resolution) do NOT get
// PluginOperator-level retries, so the SDK retryer is their only protection;
// 2 attempts was too aggressive for that path.
o.MaxAttempts = 8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's drop this. formae code already retries resolve-reads at 2 layers: in the ResolveCache and in the PluginOperator. Seems like this comment is stale.

name: String?
// formae patch: accept Resolvable so ECS container env vars can reference a
// provisioned resource's output (e.g. an RDS endpoint address) by `.res`.
// NOTE: re-apply if `make gen-aws-pkl-types` regenerates this file.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a generated file?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants