Why I Gave Up on AWS Copilot CLI for Preview Environments
I wanted to automatically build an isolated preview environment for each pull request. The first candidate that came to mind was AWS Copilot CLI, AWS’s official ECS deployment tool. Copilot CLI is appealing for its simplicity—you can deploy an application to ECS Fargate with a single copilot svc deploy command—and the official documentation provides environment management mechanisms.
The bottom line: in my case, Copilot CLI was not a good fit. After a full day of PoC work, I couldn’t even get the application to start, and I concluded that Copilot CLI’s abstractions were more of a hindrance than a help.
This article documents what I tried, where I got stuck, and why I gave up. I hope it serves as a useful reference for anyone considering Copilot CLI.
PoC Goals
I created a new AWS account and aimed to perform the following end-to-end using only AWS Copilot CLI:
copilot app initto initialize the applicationcopilot svc initto initialize the API service (Load Balanced Web Service) and Worker service (background jobs)copilot env init+copilot env deployto set up the environmentcopilot svc deployto deploy (including Aurora Serverless v2 Addon)- Verify application startup, DB migration, and authentication integration
The goal was to quickly determine whether “Copilot CLI can run this app” before investing in manifest refinement or GitHub Actions automation. Cognito integration was the biggest risk, so I wanted to validate it early.
PoC Results
| Task | Result |
|---|---|
Application initialization (app init) | Success |
Service initialization (svc init) | Success |
Environment setup (env init + env deploy) | Success |
Service deployment (svc deploy) | Failed—secrets/env var injection issues prevented app startup |
| DB migration | Not attempted (blocked by above) |
| Cognito integration | Not attempted |
Full resource cleanup (app delete) | Success |
Environment setup went smoothly, but at the service deployment stage, secrets/environment variable injection became a wall, and the application never started.
Issues Discovered
Issue 1: Secrets/Environment Variable Injection (The Biggest Blocker)
Copilot CLI does not automatically grant the ECS Execution Role read permissions for SSM Parameter Store or Secrets Manager. I tried every approach below, and all failed:
| Method | Result |
|---|---|
secrets + SSM parameter path | ssm:GetParameters AccessDenied |
secrets + secretsmanager: prefix | secretsmanager:GetSecretValue AccessDenied |
from_cfn to reference Addon output (secrets) | No export named ... found |
from_cfn to reference Addon output (variables) | Same as above |
The fundamental problem is that there are extremely limited ways to pass connection information from Addon-created resources (such as Aurora) to containers. There is also no way to add policies to the Execution Role from an Addon.
Issue 2: Addon Constraints
Copilot CLI’s Addon feature adds CloudFormation templates, but it came with the following constraints:
YAML syntax constraints: Copilot CLI’s parser does not work well with CloudFormation shorthand notation like !Sub and !Ref. Everything must be written in long form—Fn::Sub, Ref, etc. This is a particularly frustrating gotcha for anyone experienced with CloudFormation.
from_cfn doesn’t work: This is the official feature for referencing Addon outputs from the Manifest, but internally it uses Fn::ImportValue (which requires Exports), making it impossible to reference nested stack outputs. Since Addons are managed as nested stacks within Copilot CLI, there is a fundamental mismatch.
Issue 3: Compatibility with Real-World Complexity
Copilot CLI is designed for “simple apps that follow conventions.” When confronted with real-world complexity, here’s where the conflicts arose:
| Project Characteristic | Conflict with Copilot CLI |
|---|---|
docker-entrypoint.sh depends on JSON-formatted DB secrets | Cannot be injected via Copilot CLI’s secrets mechanism |
ManageMasterUserPassword Secret format doesn’t match app expectations | Even custom Secrets can’t be passed to the container |
| Multiple services (API / Admin / Worker) + DB + Redis | Managing dependencies and resource sharing between Addons is complex |
| Schema management via DB migration tool | Faces the same DB connection information passing problem |
Issue 4: Slow Debug Loop
Trial and error with Copilot CLI was extremely time-consuming:
| Step | Duration |
|---|---|
| Aurora Serverless v2 creation | 15–20 min |
| Stack deletion | 10–15 min |
| One trial cycle | 30+ min |
The following issues made debugging even harder:
- When a service is removed due to
ROLLBACK_COMPLETE,copilot svc logs --previousbecomes unavailable - Nested stack (Addon) errors are inaccessible after deletion
- You end up having to check CloudFormation event logs directly, negating the benefits of Copilot CLI’s abstractions
Issue 5: Customization Barriers
Once you try to go beyond Copilot CLI’s abstractions, you need CloudFormation knowledge anyway:
- Customizing the Execution Role → No way to do it
- Fine-grained control over Task Definitions (entrypoint override, etc.) → Requires
taskdef_overrides - Sharing resources between Addons (e.g., API and Worker sharing the same Aurora) → Requires custom workarounds
- Integrating with existing Secrets management patterns → Doesn’t fit Copilot CLI’s conventions
Overall Assessment
| Aspect | Evaluation |
|---|---|
| Deploying simple apps | Copilot CLI is excellent |
| Deploying complex apps | Not a good fit |
| Learning & debugging cost | CloudFormation knowledge is required anyway; the abstraction becomes a barrier |
Notably, bypassing docker-entrypoint.sh required entrypoint overriding via taskdef_overrides. On the other hand, copilot app delete for full resource cleanup worked without issues—that’s a point in its favor.
Copilot CLI is a great tool for “deploying a Hello World-level app to ECS as fast as possible.” However, once real-world requirements like DB connections, Secrets management, and multi-service coordination come into play, the abstraction layer becomes a constraint.
The most critical issue was that there is effectively no way to inject connection information from Addon-created resources into containers. I believe this is a problem that anyone would face when trying to run a production-level application with Copilot CLI, not just for preview environments.
Direction After Giving Up
Deploying to ECS Fargate itself is a valid approach. The problem was IaC tool selection. Based on lessons learned from the Copilot CLI PoC, I evaluated the following alternatives:
| Option | Advantages | Concerns |
|---|---|---|
| Terraform | Mature HCL ecosystem, rich module library | Dynamic per-PR environment management tends to be verbose |
| AWS CDK | Can be written in TypeScript, superset of CloudFormation | Indirectly subject to CloudFormation constraints |
| Pulumi | TypeScript, Review Stacks feature, Stack Reference | Pulumi Cloud costs |
Summary
AWS Copilot CLI is an appealing tool for its simplicity, but watch out if your project involves:
- Passing connection information from Addon-created resources to containers (DB, Redis, etc.)
- Customizing the Execution Role (access to SSM, Secrets Manager, etc.)
- Sharing resources across multiple services (e.g., API and Worker using the same DB)
- Long debug trial cycles (Aurora creation time + stack deletion time)
The PoC approach of “let’s first see if Copilot CLI works” was the right call. Being able to reach a “not a good fit” conclusion in a single day allowed me to smoothly pivot to Pulumi. If you’re unsure about tool selection, I recommend running a small PoC to make an early decision.
That’s all for today—when it comes to IaC tool selection, try small and decide fast. That’s all from the Gemba.