Feat/bootstrap example#102
Conversation
27671fb to
263dcd6
Compare
f36bc58 to
3bdc937
Compare
3bdc937 to
1ee823b
Compare
There was a problem hiding this comment.
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.pklandexisting-db.pklthe task role grants onlyssmmessages:*(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:PowerUserAccessplus an inline policy for IAM management and read-onlyiam:Get*/iam:List*(without the latter, discovery spams 403s oniam: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 ECSsecrets[].valueFrom = dbSecret.res.arninstead of an env var. That capability is already onmain(PRs #89 + #105); this branch predates it, so rebasing onto currentmainand switching to the ARN should drop the plaintext path entirely. Keep the self-managed, non-rotating secret you already generate; don't switch tomanageMasterUserPassword, 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 plusaws ecs update-service --force-new-deployment. -
The agent API is a plaintext, unauthenticated, internet-facing endpoint. Public subnets plus
assignPublicIp=ENABLEDplus an internet-facing ALB with0.0.0.0/0ingress on an HTTP listener shouldn't be the out-of-the-box default for a control plane. Model ingress as a choice the waymodules/lgtm.pkldoes with itsvisibilityflag, and have the public option terminate HTTPS plus require theauth-basicplugin: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. -
--regionis a no-op, and there's no--profile. Theregionproperty is declared but never wired in: the target readsmodule.regiondirectly and the AZs are hardcoded (us-east-2a/b), so passing--region eu-west-1changes 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.Configalready supports it (profile: String?), so folks can pick which local credentials provision the stack. Worth noting in the README that--region/--profilegovern 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/PklProjectpinsformae@0.83.0,vars.pklsets the image toformae:0.86.0, and the README config table saysformae:0.83.2. Pin one across all three:0.86.x(the plugin HEAD already requires0.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 asizet-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
discoveryFiltersblock 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 matchingdiscoveryFiltersblock into the agent config so the first discovery cycle is already clean. -
Structure & naming.
main.pklvsexisting-db.pklreads as confusing since they're peers, and the two files duplicate ~300 lines of VPC/roles/ALB/service. Suggest collapsing to a singlebootstrap.pklentrypoint with adatabase = "new" | "existing"flag, factoring the shared resources intolib/modules and lifting the scalars plus sizing table intovars.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 }
| // 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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
This is not a generated file?
No description provided.