Skip to content

Commit 03fd9e6

Browse files
committed
feat: add ECS and AgentCore container build/package/deploy support
Extends SAM CLI to build, package, and deploy container images for AWS::ECS::TaskDefinition and AWS::BedrockAgentCore::Runtime resources using the same Metadata convention as Lambda Image functions. Fixes #8933
1 parent 9ffa5ed commit 03fd9e6

19 files changed

Lines changed: 1560 additions & 4 deletions

PR_DESCRIPTION.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#### Which issue(s) does this change fix?
2+
3+
Fixes #8933
4+
5+
#### Why is this change necessary?
6+
7+
SAM CLI provides an excellent developer experience for Lambda Image functions (`sam build && sam deploy`), but users deploying containerized workloads to ECS (Fargate) or Bedrock AgentCore must manage their Docker build/push/deploy pipeline separately — even when these resources live in the same CloudFormation template. This creates a fragmented workflow requiring external tooling for an identical operation: build image → push to ECR → deploy.
8+
9+
#### How does it address the issue?
10+
11+
Extends the existing Lambda Image build pipeline to recognize `AWS::ECS::TaskDefinition` and `AWS::BedrockAgentCore::Runtime` resources with a `Metadata` block containing `Dockerfile` and `DockerContext`. No new commands — `sam build`, `sam package`, `sam deploy`, and `sam sync` gain awareness of these resource types.
12+
13+
**Template example:**
14+
```yaml
15+
Resources:
16+
MyAgent:
17+
Type: AWS::BedrockAgentCore::Runtime
18+
Metadata:
19+
Dockerfile: Dockerfile
20+
DockerContext: ./agent
21+
Architecture: arm64
22+
Properties:
23+
AgentRuntimeArtifact:
24+
ContainerConfiguration:
25+
ContainerUri: placeholder
26+
27+
MyTask:
28+
Type: AWS::ECS::TaskDefinition
29+
Metadata:
30+
Dockerfile: Dockerfile
31+
DockerContext: ./app
32+
ContainerName: web
33+
Properties:
34+
ContainerDefinitions:
35+
- Name: web
36+
Image: placeholder
37+
```
38+
39+
**Key implementation details:**
40+
- Reuses `_build_lambda_image()` — same Docker build logic, buildkit support included
41+
- `--resolve-image-repos` auto-creates ECR repos via companion stack
42+
- `ContainerName` metadata targets specific containers in multi-container TaskDefinitions
43+
- `Architecture` metadata sets `--platform` (e.g., `arm64` for AgentCore)
44+
- `ARTIFACT_TYPE = ZIP` to pass the `PackageType` filter (these resources don't have `PackageType`)
45+
- No SAM Transform changes needed — uses native CloudFormation resource types
46+
47+
**Design document:** `designs/container_image_builds_ecs_agentcore.md`
48+
49+
#### What side effects does this change have?
50+
51+
- `sam build` logs "Found N container service resource(s) to build" when applicable resources are present. No behavior change for templates without these resources.
52+
- `--resolve-image-repos` creates ECR repos for ECS/AgentCore in addition to Lambda Image functions.
53+
- `_update_built_resource` adds an optional `metadata` parameter (backward compatible, defaults to `None`).
54+
55+
#### Mandatory Checklist
56+
**PRs will only be reviewed after checklist is complete**
57+
58+
- [x] Review the [generative AI contribution guidelines](https://github.com/aws/aws-sam-cli/blob/develop/CONTRIBUTING.md#ai-usage)
59+
- [x] Add input/output [type hints](https://docs.python.org/3/library/typing.html) to new functions/methods
60+
- [x] Write design document if needed ([Do I need to write a design document?](https://github.com/aws/aws-sam-cli/blob/develop/DEVELOPMENT_GUIDE.md#design-document))
61+
- [x] Write/update unit tests
62+
- [x] Write/update integration tests
63+
- [x] Write/update functional tests if needed
64+
- [x] `make pr` passes
65+
- [x] `make update-reproducible-reqs` if dependencies were changed
66+
- [ ] Write documentation
67+
68+
By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0).
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
Container Image Builds for ECS and AgentCore
2+
=============================================
3+
4+
This is the design for extending `sam build`, `sam package`, `sam deploy`, and `sam sync`
5+
to support building and deploying container images for non-Lambda resources:
6+
`AWS::ECS::TaskDefinition` and `AWS::BedrockAgentCore::Runtime`.
7+
8+
What is the problem?
9+
--------------------
10+
11+
SAM CLI provides an excellent developer experience for Lambda Image functions: a single
12+
`sam build && sam deploy` builds the Docker image, pushes to ECR, and deploys via
13+
CloudFormation. However, users deploying containerized workloads to ECS (Fargate) or
14+
Bedrock AgentCore must manage their Docker build/push/deploy pipeline separately, even
15+
when these resources live in the same CloudFormation template alongside Lambda functions.
16+
17+
This creates a fragmented workflow where developers need external tooling (shell scripts,
18+
Makefiles, or CI/CD steps) for the identical operation: build image → push to ECR →
19+
update template with ECR URI → deploy.
20+
21+
What will be changed?
22+
---------------------
23+
24+
We extend the existing Lambda Image build pipeline to recognize `AWS::ECS::TaskDefinition`
25+
and `AWS::BedrockAgentCore::Runtime` resources that have a `Metadata` block with
26+
`Dockerfile` and `DockerContext`. No new commands are introduced — the existing
27+
`sam build`, `sam package`, `sam deploy`, and `sam sync` gain awareness of these
28+
resource types.
29+
30+
### Design Principles
31+
32+
1. **Same convention** — Uses the identical Metadata block as Lambda Image functions
33+
(Dockerfile, DockerContext, DockerTag, DockerBuildArgs, DockerBuildTarget)
34+
2. **No Transform changes** — Works with native CloudFormation resource types
35+
3. **Opt-in** — Only resources with the Metadata block are affected; existing templates
36+
work unchanged
37+
4. **Reuse** — Delegates to the same `_build_lambda_image()` Docker build logic
38+
39+
Success criteria for the change
40+
-------------------------------
41+
42+
1. `sam build` discovers and builds container images for ECS TaskDefinitions and
43+
AgentCore Runtimes that have Dockerfile metadata
44+
2. `sam deploy --resolve-image-repos` auto-creates ECR repos for these resources
45+
3. `sam package` / `sam deploy` pushes images to ECR and rewrites the template with
46+
the ECR URI at the correct property path
47+
4. `sam sync` builds, pushes, and triggers redeployment for these resources
48+
5. Multi-container ECS TaskDefinitions can target a specific container via `ContainerName`
49+
6. Architecture can be specified via `Architecture` metadata (e.g., `arm64`)
50+
7. Buildkit support works automatically (shared with Lambda Image builds)
51+
8. No regressions for existing Lambda, Layer, or API builds
52+
53+
User Experience
54+
---------------
55+
56+
### Template Format
57+
58+
```yaml
59+
Resources:
60+
# AgentCore Runtime
61+
MyAgent:
62+
Type: AWS::BedrockAgentCore::Runtime
63+
Metadata:
64+
Dockerfile: Dockerfile
65+
DockerContext: ./agent
66+
DockerTag: latest
67+
Architecture: arm64
68+
Properties:
69+
AgentRuntimeName: my_agent
70+
AgentRuntimeArtifact:
71+
ContainerConfiguration:
72+
ContainerUri: placeholder
73+
NetworkConfiguration:
74+
NetworkMode: PUBLIC
75+
RoleArn: !GetAtt AgentRole.Arn
76+
77+
# ECS TaskDefinition (multi-container)
78+
MyTask:
79+
Type: AWS::ECS::TaskDefinition
80+
Metadata:
81+
Dockerfile: Dockerfile
82+
DockerContext: ./app
83+
DockerTag: latest
84+
ContainerName: web
85+
Properties:
86+
Family: my-app
87+
ContainerDefinitions:
88+
- Name: envoy
89+
Image: public.ecr.aws/envoy:latest
90+
- Name: web
91+
Image: placeholder
92+
```
93+
94+
### CLI Usage
95+
96+
```bash
97+
# Build container images
98+
sam build
99+
100+
# Deploy with auto ECR repo creation
101+
sam deploy --resolve-image-repos
102+
103+
# Or with explicit repo
104+
sam deploy --image-repositories SimpleAgent=123456789012.dkr.ecr.us-east-1.amazonaws.com/repo
105+
106+
# Live sync
107+
sam sync --stack-name my-stack --watch --resolve-image-repos
108+
```
109+
110+
### Metadata Fields
111+
112+
| Field | Required | Description |
113+
|-------|----------|-------------|
114+
| `Dockerfile` | Yes | Path to Dockerfile relative to DockerContext |
115+
| `DockerContext` | Yes | Build context directory relative to template |
116+
| `DockerTag` | No | Image tag (default: `latest`) |
117+
| `DockerBuildArgs` | No | Dict of build args |
118+
| `DockerBuildTarget` | No | Multi-stage build target |
119+
| `DockerBuildExtraParams` | No | List of extra docker build params |
120+
| `Architecture` | No | Target platform: `x86_64` (default) or `arm64` |
121+
| `ContainerName` | No | ECS only: target container in multi-container TaskDefinition |
122+
123+
Implementation
124+
--------------
125+
126+
### Architecture
127+
128+
```
129+
┌─────────────────────────────────────────────────────────────────┐
130+
│ sam build │
131+
├─────────────────────────────────────────────────────────────────┤
132+
│ BuildContext.run() │
133+
│ ├── builder.build() → Lambda functions + layers │
134+
│ └── _build_container_images() → ECS + AgentCore containers │
135+
│ ├── SamContainerServiceProvider (discovery) │
136+
│ ├── ContainerBuildDefinition (build graph) │
137+
│ └── ApplicationBuilder.build_container_images() │
138+
│ └── _build_lambda_image() (shared Docker logic) │
139+
├─────────────────────────────────────────────────────────────────┤
140+
│ sam package / sam deploy │
141+
│ ├── sync_ecr_stack() → auto-creates ECR repos (companion) │
142+
│ ├── ECSTaskDefinitionImageResource.export() → push + rewrite │
143+
│ └── AgentCoreRuntimeImageResource.export() → push + rewrite │
144+
├─────────────────────────────────────────────────────────────────┤
145+
│ sam sync │
146+
│ └── ECSContainerSyncFlow │
147+
│ ├── gather_resources() → build image │
148+
│ ├── sync() → push to ECR + force ECS deployment │
149+
│ └── SyncFlowFactory (registered for both types) │
150+
└─────────────────────────────────────────────────────────────────┘
151+
```
152+
153+
### Key Components
154+
155+
**`samcli/lib/providers/sam_container_provider.py`** (new)
156+
- `SamContainerServiceProvider`: Scans stacks for ECS/AgentCore resources with
157+
Dockerfile+DockerContext metadata. Returns `ContainerService` NamedTuples.
158+
159+
**`samcli/lib/build/build_graph.py`** (modified)
160+
- `ContainerBuildDefinition`: Parallel to `FunctionBuildDefinition`. Holds resource
161+
identifier, type, metadata, and architecture. Reads `Architecture` from metadata.
162+
163+
**`samcli/lib/build/app_builder.py`** (modified)
164+
- `build_container_images()`: Iterates container definitions and builds each.
165+
- `_build_container_image()`: Delegates to `_build_lambda_image()` — same Docker logic.
166+
- `_update_built_resource()`: Extended for ECS (`ContainerDefinitions[N].Image`) and
167+
AgentCore (`AgentRuntimeArtifact.ContainerConfiguration.ContainerUri`). Accepts
168+
optional `metadata` param for `ContainerName` targeting.
169+
170+
**`samcli/lib/package/packageable_resources.py`** (modified)
171+
- `ECSTaskDefinitionImageResource`: Custom export for nested `ContainerDefinitions[0].Image`.
172+
- `AgentCoreRuntimeImageResource`: Export using jmespath for deeply nested property path.
173+
- Both use `ARTIFACT_TYPE = ZIP` to pass the `PackageType` filter (these resources
174+
don't have a `PackageType` property).
175+
176+
**`samcli/lib/sync/flows/ecs_container_sync_flow.py`** (new)
177+
- `ECSContainerSyncFlow`: Builds image, pushes to ECR, forces ECS service redeployment
178+
by finding services using the task definition family.
179+
180+
**`samcli/lib/bootstrap/companion_stack/companion_stack_manager.py`** (modified)
181+
- `sync_ecr_stack()`: Extended to include container service resources when creating
182+
ECR repos via the companion stack.
183+
184+
### Property Path Mapping
185+
186+
| Resource Type | Property Path for Image URI |
187+
|---------------|----------------------------|
188+
| `AWS::Serverless::Function` | `ImageUri` |
189+
| `AWS::Lambda::Function` | `Code.ImageUri` |
190+
| `AWS::ECS::TaskDefinition` | `ContainerDefinitions[N].Image` |
191+
| `AWS::BedrockAgentCore::Runtime` | `AgentRuntimeArtifact.ContainerConfiguration.ContainerUri` |
192+
193+
Alternatives Considered
194+
-----------------------
195+
196+
### 1. New SAM Transform resource type (e.g., `AWS::Serverless::ContainerService`)
197+
198+
**Rejected because:**
199+
- Requires changes to the SAM Transform (separate repo, separate approval process)
200+
- Adds coupling between SAM CLI and the Transform
201+
- Users would need to wait for Transform support in all regions
202+
- Native CFN types already work and are well-understood
203+
204+
### 2. Separate `sam container build` command
205+
206+
**Rejected because:**
207+
- Fragments the workflow — users would need to remember different commands
208+
- Doesn't integrate with `sam deploy` and `sam sync` naturally
209+
- The existing `sam build` already handles image builds for Lambda
210+
211+
### 3. Using `PackageType: Image` on ECS/AgentCore resources
212+
213+
**Rejected because:**
214+
- `PackageType` is a Lambda-specific concept not present on ECS or AgentCore resources
215+
- Would require CloudFormation schema changes
216+
- The Metadata-based approach is already the established pattern
217+
218+
Breaking Changes
219+
----------------
220+
221+
None. This is purely additive:
222+
- Templates without ECS/AgentCore Metadata are unaffected
223+
- The `_update_built_resource` signature change is backward compatible (optional param)
224+
- No existing CLI flags or behaviors change
225+
226+
Future Extensions
227+
-----------------
228+
229+
1. **Multiple Dockerfiles per ECS TaskDefinition** — Build multiple containers from
230+
one resource using a list of Metadata entries
231+
2. **`sam local start-ecs`** — Local testing of ECS containers (similar to `sam local start-api`)
232+
3. **Health check integration** — Wait for container health before marking sync complete
233+
4. **Build caching** — Layer-aware caching for container builds (currently rebuilds fully)
234+
5. **`sam init` templates** — Starter templates for ECS+SAM and AgentCore+SAM projects

samcli/commands/build/build_context.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
BuildError,
3030
UnsupportedBuilderLibraryVersionError,
3131
)
32-
from samcli.lib.build.build_graph import DEFAULT_DEPENDENCIES_DIR
32+
from samcli.lib.build.build_graph import DEFAULT_DEPENDENCIES_DIR, ContainerBuildDefinition
3333
from samcli.lib.build.bundler import EsbuildBundlerManager
3434
from samcli.lib.build.exceptions import (
3535
BuildInsideContainerError,
@@ -45,6 +45,7 @@
4545
from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable
4646
from samcli.lib.providers.provider import LayerVersion, ResourcesToBuildCollector, Stack
4747
from samcli.lib.providers.sam_api_provider import SamApiProvider
48+
from samcli.lib.providers.sam_container_provider import SamContainerServiceProvider
4849
from samcli.lib.providers.sam_function_provider import SamFunctionProvider
4950
from samcli.lib.providers.sam_layer_provider import SamLayerProvider
5051
from samcli.lib.providers.sam_stack_provider import SamLocalStackProvider
@@ -302,6 +303,11 @@ def run(self) -> None:
302303

303304
self._build_result = builder.build()
304305

306+
# Build container images for ECS/AgentCore resources
307+
container_artifacts = self._build_container_images(builder)
308+
if container_artifacts:
309+
self._build_result.artifacts.update(container_artifacts)
310+
305311
self._handle_build_post_processing(builder, self._build_result)
306312

307313
click.secho("\nBuild Succeeded", fg="green")
@@ -1192,6 +1198,33 @@ def collect_all_build_resources(self) -> ResourcesToBuildCollector:
11921198
)
11931199
return result
11941200

1201+
def _build_container_images(self, builder: ApplicationBuilder) -> Dict[str, str]:
1202+
"""
1203+
Discover and build container images for ECS/AgentCore resources.
1204+
1205+
Returns
1206+
-------
1207+
Dict[str, str]
1208+
Map of resource full_path to built image tag
1209+
"""
1210+
container_provider = SamContainerServiceProvider(self.stacks)
1211+
container_services = list(container_provider.get_all())
1212+
if not container_services:
1213+
return {}
1214+
1215+
LOG.info("Found %d container service resource(s) to build", len(container_services))
1216+
1217+
container_build_defs = []
1218+
for service in container_services:
1219+
build_def = ContainerBuildDefinition(
1220+
resource_identifier=service.full_path,
1221+
resource_type=service.resource_type,
1222+
metadata=service.metadata,
1223+
)
1224+
container_build_defs.append(build_def)
1225+
1226+
return builder.build_container_images(container_build_defs)
1227+
11951228
@property
11961229
def is_building_specific_resource(self) -> bool:
11971230
"""

samcli/commands/build/command.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
2. AWS::Lambda::Function\n
5555
3. AWS::Serverless::LayerVersion\n
5656
4. AWS::Lambda::LayerVersion\n
57+
5. AWS::ECS::TaskDefinition (container image)\n
58+
6. AWS::BedrockAgentCore::Runtime (container image)\n
5759
\b
5860
Supported Runtimes
5961
------------------

samcli/lib/bootstrap/companion_stack/companion_stack_manager.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,14 @@ def sync_ecr_stack(
312312
function_logical_ids = [
313313
function.full_path for function in function_provider.get_all() if function.packagetype == IMAGE
314314
]
315+
316+
# Also include ECS/AgentCore container resources that need ECR repos
317+
from samcli.lib.providers.sam_container_provider import SamContainerServiceProvider
318+
319+
container_provider = SamContainerServiceProvider(stacks)
320+
container_logical_ids = [service.full_path for service in container_provider.get_all()]
321+
function_logical_ids.extend(container_logical_ids)
322+
315323
manager.set_functions(function_logical_ids, image_repositories)
316324
image_repositories.update(manager.get_repository_mapping())
317325
manager.sync_repos()

0 commit comments

Comments
 (0)