A CTO choosing cloud infrastructure faces a lopsided bet: buy a managed runtime and accept its constraints, or build an internal platform and own its behavior. I would buy the runtime by default. Build in-house only when a specific constraint has survived a production-shaped test and its value exceeds the cost of operating the platform. A forecast of future scale is not enough.
Buy the runtime when your application is the source of uncertainty
The meaningful comparison is AWS ECS on Fargate versus a self-managed Kubernetes platform on EC2. ECS on Fargate wins when traffic, deployment patterns, and team needs are still changing, because AWS operates the hosts while engineers can spend their time changing the application. You still pay for allocated compute, networking, observability, and application operations. Self-managed Kubernetes wins when you need control over scheduling, node configuration, or workload placement that the managed runtime cannot provide economically. It costs engineering time as well as EC2 capacity, because your team must operate clusters, nodes, upgrades, and the tools around them.
That comparison is narrower than “buy versus build everything.” A purchased runtime does not design your PostgreSQL schema, resolve a bad deployment, or decide whether an alert warrants waking an engineer. The hidden-maintenance argument—Buying Cloud Infrastructure Means Owning the Hidden Maintenance Load—is right to challenge the idea that procurement eliminates work, but it does not follow that building the runtime eliminates more of it. In-house Kubernetes adds work unless its extra control solves a problem you can name and price.
I would not build a self-managed cluster to avoid an anticipated Fargate bill, because an anticipated bill is easier to revise than an operational commitment made before the workload is understood. Ask first whether the application can run behind an AWS Application Load Balancer with stateless tasks, externalized sessions, and data in a managed store. If it can, the managed option preserves a useful freedom: the team can change its architecture without also changing a platform it has built around an earlier guess.
“Managed” does not mean “portable.” IAM policies, task definitions, load-balancer behavior, and networking choices can make migration expensive because they become part of the deployment path. Keep Terraform modules for the runtime separate from application configuration, and use OpenTelemetry for traces so a runtime change does not require rewriting every instrumentation call. Those choices do not make a switch free; they make its boundaries visible.
Build only when the missing control has a measured value
A request for “more flexibility” is not a build case, because flexibility has no return until someone identifies the constraint it removes. A stronger case sounds like this: a required workload needs a node-level agent or placement policy that the managed runtime cannot support, and a representative test shows that the limitation changes latency or cost enough to matter. Kubernetes, Karpenter, and the Horizontal Pod Autoscaler can then be evaluated as a specific solution rather than as a standard platform to adopt.
Before approving that solution, require the proposing team to demonstrate the behavior. For example, configure an HPA using autoscaling/v2, apply the intended resource requests, and show what happens when Karpenter provisions replacement nodes during a traffic step-up. Resource requests matter because the HPA’s utilization calculation depends on them; a scaling graph without that configuration can give a misleading account of how the system will respond. Test a node drain during the same run, because steady-state throughput says little about capacity available while nodes are being replaced.
Set a provisional service objective—say, 99.9% monthly availability—as a value to tune with the product owner, not as a claim that either runtime delivers it. Pair it with an explicit recovery-time objective and recovery-point objective for the data store. A cluster cannot satisfy a database recovery promise by itself, because application availability after data loss depends on backups, replication, and a practiced restore. The CTO should reject any comparison that credits Kubernetes for reliability while leaving those procedures outside its cost.
Security review also needs to compare actual responsibilities. With Fargate, engineers still control container images, IAM permissions, secrets, and network access; with self-managed Kubernetes, they additionally take responsibility for the cluster components and node lifecycle. Use an OIDC-based deployment identity rather than long-lived CI credentials where the chosen platform supports it, because short-lived credentials reduce the consequences of a leaked pipeline secret. The point is not that one option is inherently secure: each changes which team must detect and repair a failure.
There is a legitimate build case when requirements are stable and expensive to meet through the purchased abstraction. There is a weak one when engineers prefer Kubernetes APIs or assume that owning EC2 instances will automatically lower spend, because neither preference pays for cluster operations. Put the constraint, its observed impact, and the expected monthly benefit in the same decision record.
A paired trial should decide the argument before a cost spreadsheet does
Run the same container image and PostgreSQL workload on both candidates, with comparable CPU and memory allocations and the same external dependencies. Keep a small production-shaped traffic trace, then use k6 to replay its request mix; a synthetic peak alone can favor the option tuned specifically for that peak. Instrument both deployments with OpenTelemetry and inspect Prometheus histograms for p95 latency, because an average can conceal the slow requests that determine whether users notice scaling delays.
Suppose a pilot records p95 latency of 180 ms at 400 requests per second on Fargate. That observation is useful only alongside error rate, task utilization, and the duration of the run, because a short test can miss queue growth or scaling lag. Repeat it while deploying a new image and while removing capacity. If the self-managed option wins only during an ideal steady state, its result does not yet justify ownership of upgrades and recovery.
The scaling-checklist framing—Cloud Infrastructure Best Practices for Scalable Apps—helps keep the application test honest, but a list of scaling practices cannot choose an owner for the runtime. Make the trial produce a decision artifact instead: the observed constraint, the configuration used, the bill for the test, the operator time spent, and the failure that would change the recommendation. That record lets a skeptical engineering lead reproduce the conclusion instead of accepting an architecture diagram.
Price the alternatives with fully loaded people costs rather than cloud invoices alone. The following runnable Python 3 model uses illustrative planning inputs, not vendor prices or measured results. Replace the monthly baselines and cost per million requests with figures from your trial and staffing plan:
python3 - <<'PY'
managed_base = 18000
managed_per_million = 120
build_base = 62000
build_per_million = 35
for million_requests in (20, 100, 300, 600):
managed = managed_base + managed_per_million * million_requests
build = build_base + build_per_million * million_requests
winner = "managed" if managed < build else "in-house"
print(f"{million_requests}M: managed=${managed:,.0f}; build=${build:,.0f}; {winner}")
PY
In this model, the in-house option becomes cheaper only above roughly 518 million requests a month, because its lower assumed marginal cost must first repay a higher fixed cost. That crossover is not a recommendation to migrate at that volume: request count is a poor proxy if requests vary in CPU time, memory use, or data transfer. It is a prompt to identify which measured cost driver would actually make the build option win.
The winning option needs an exit condition before approval
Approving Fargate should include a threshold for reconsidering it. Name the constraint that would trigger another trial—such as an inability to place a required workload, or a sustained marginal-cost difference after operator time is included—because otherwise each rising bill becomes a fresh argument from intuition. Review that threshold on a fixed cadence and after a material workload change; do not rebuild the platform in response to a single expensive month.
Approving self-managed Kubernetes needs an equally clear condition for stopping. Require an owner for version upgrades, a tested node-replacement procedure, and an estimate of time spent on the platform. Track that time in the same decision record as compute spend, because a lower EC2 bill can conceal an expensive internal service. If the team cannot maintain the cluster while delivering application changes, the platform is failing its original economic case even if the workload runs well.
Keep application deployment and data migration separable from the runtime choice. A container image, an explicit HTTP health endpoint, and a documented PostgreSQL restore procedure give both options a common test surface. They do not guarantee portability, because differences in networking, identity, and persistent storage still require work; they do prevent a later decision from being blocked by undocumented assumptions.
Start with the constraint you can test this month
Open one decision ticket: name the workload, the suspected managed-runtime constraint, and the result that would make you build instead. Assign an engineer to run the paired trial and someone outside the platform proposal to check its costs. If the constraint does not appear under production-shaped load, buy the runtime for now. Revisit the choice when evidence changes, not when the architecture feels too simple.


