notable work
CASE 01 · ALTERNETA sole infra author · 2025-06 → 2025-07

None of the apps, all of the infrastructure

5 min read
terraformgithub actionsoidcvpckmss3 backend
terraform github actions oidc vpc kms s3 backend

Alterneta runs a handful of internal apps — an AI financial-analysis tool, a real estate management app, a marketing site, a few smaller experiments — and for every one of them, I own the ground they stand on and none of what runs on it. Across the org’s repositories I’m the primary, in most cases the sole, author of the Terraform and the GitHub Actions pipelines that provision and deploy infrastructure. I’ve stayed out of the application codebases themselves almost entirely. That split isn’t an accident of how the work happened to land; it’s the actual shape of the role, and it’s held steady across every repo in the org.

Building the floor before the apps needed it

The first piece was a shared sandbox VPC: three availability zones, a clean /16 split into public and private halves, NAT gateway creation exposed as a variable and left off by default, because a sandbox environment doesn’t need three NAT gateways billing around the clock. When the org’s first real application stack needed compute, it didn’t get its own network — it reached into the sandbox VPC’s outputs and built on top of that. That’s the shared-VPC pattern this org runs on, and the reasoning behind it — plus the dependency-cycle bug that came from briefly getting it wrong — is its own story told elsewhere in this set. The short version is that “one VPC per app” didn’t survive contact with a second application.

The application’s security layer, in four small modules

On top of that shared network, I authored the compute stack for the org’s AI financial-analysis app as four composable modules rather than one monolithic file: a KMS key with rotation enabled for EBS encryption, an IAM assumable role and instance profile scoped to what the instance actually needs, a security group that allows SSH only from the VPC’s own CIDR, and a launch template wiring all three together. It’s deliberately unglamorous infrastructure — a single EC2 instance, not a fleet — but the identity and encryption layer around it is built the way I’d want it if the app grew into something bigger, because retrofitting KMS and least-privilege IAM onto a running instance is a worse afternoon than doing it up front.

flowchart TB
subgraph vpc["shared sandbox vpc — 3 az, public / private split"]
  sg["security group — ssh from vpc cidr only"]
  lt["launch template"]
  ec2["ec2 instance"]
  ebs["encrypted gp3 root volume"]
end
kms["kms key — rotation enabled"]
prof["iam role + instance profile"]
lt -->|"launches"| ec2
sg -->|"attached"| ec2
prof -->|"attached"| ec2
ec2 --> ebs
kms -.->|"encrypts"| ebs
as built — the app compute stack on the shared sandbox vpc

Pipelines that never see a credential

Every infra repo in the org calls into two reusable GitHub Actions workflows instead of duplicating pipeline logic: one for terraform plan, one for terraform apply. Both assume an AWS IAM role through GitHub’s OIDC provider — id-token: write and nothing else, no stored access keys anywhere in the org. Plan runs automatically on every pull request; apply is a deliberate workflow_dispatch, never a push trigger. The organization owns how Terraform runs; each infra repo only owns what it runs against. Getting the OIDC role and permissions wired correctly took a few iterations — a couple of path and permission fixes landed before the pipeline stabilized, which is normal for the first time you wire OIDC into a new AWS account, not a sign anything was wrong with the approach.

flowchart LR
subgraph shared["shared public repos"]
  rtm["terraform module library"]
  rwf["reusable workflows — plan / apply"]
end
subgraph infra["private infra repos"]
  net["network stack — shared vpc"]
  app["app stack — ec2 · kms · iam"]
end
role["aws iam role — no stored keys"]
rwf -->|"workflow_call"| net
rwf -->|"workflow_call"| app
rtm -->|"git source, ref-pinned"| net
net -->|"vpc outputs"| app
net -->|"assumes via oidc"| role
app -->|"assumes via oidc"| role
as built — module library, reusable workflows, and the oidc trust path

Moving off the registry, on purpose

The org’s Terraform started on third-party registry modules, because that’s the fastest way to get a working VPC or security group. It’s moving away from them now: the reusable module library other infra repos are meant to consume is a hand-rolled VPC module with dynamic, object-typed ingress and egress rules, published for git-source consumption so a repo can pin a specific ref instead of copying a file. It’s a small org, so this isn’t urgent — but every dependency on a registry module is a dependency on someone else’s release cadence, and I’d rather the org’s core network primitive be something I can change on a Tuesday.

What I’d change

State for every repo in this org lives in one S3 bucket with no DynamoDB lock table, and there’s no Terraform workspace or environment split beyond a single dev/sandbox tag — fine while I’m the only person running apply, and both of those stop being fine the moment a second engineer or a second pipeline touches the same state key. Neither is a hard problem; both are on the list specifically because they’re easy to fix now and expensive to fix after something actually collides. The other honest gap: the IAM policy on the finance app’s instance role already grants RDS and DynamoDB access that nothing in the Terraform provisions yet — permissions ahead of the resources they’re meant to guard, which I’d rather narrow than leave broad “just in case.”

filed as CASE 01 · notable work