Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions PR_DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#### Which issue(s) does this change fix?

Fixes #8933

#### Why is this change necessary?

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.

#### How does it address the issue?

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.

**Template example:**
```yaml
Resources:
MyAgent:
Type: AWS::BedrockAgentCore::Runtime
Metadata:
Dockerfile: Dockerfile
DockerContext: ./agent
Architecture: arm64
Properties:
AgentRuntimeArtifact:
ContainerConfiguration:
ContainerUri: placeholder

MyTask:
Type: AWS::ECS::TaskDefinition
Metadata:
Dockerfile: Dockerfile
DockerContext: ./app
ContainerName: web
Properties:
ContainerDefinitions:
- Name: web
Image: placeholder
```

**Key implementation details:**
- Reuses `_build_lambda_image()` — same Docker build logic, buildkit support included
- `--resolve-image-repos` auto-creates ECR repos via companion stack
- `ContainerName` metadata targets specific containers in multi-container TaskDefinitions
- `Architecture` metadata sets `--platform` (e.g., `arm64` for AgentCore)
- `ARTIFACT_TYPE = ZIP` to pass the `PackageType` filter (these resources don't have `PackageType`)
- No SAM Transform changes needed — uses native CloudFormation resource types

**Design document:** `designs/container_image_builds_ecs_agentcore.md`

#### What side effects does this change have?

- `sam build` logs "Found N container service resource(s) to build" when applicable resources are present. No behavior change for templates without these resources.
- `--resolve-image-repos` creates ECR repos for ECS/AgentCore in addition to Lambda Image functions.
- `_update_built_resource` adds an optional `metadata` parameter (backward compatible, defaults to `None`).

#### Mandatory Checklist
**PRs will only be reviewed after checklist is complete**

- [x] Review the [generative AI contribution guidelines](https://github.com/aws/aws-sam-cli/blob/develop/CONTRIBUTING.md#ai-usage)
- [x] Add input/output [type hints](https://docs.python.org/3/library/typing.html) to new functions/methods
- [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))
- [x] Write/update unit tests
- [x] Write/update integration tests
- [x] Write/update functional tests if needed
- [x] `make pr` passes
- [x] `make update-reproducible-reqs` if dependencies were changed
- [ ] Write documentation

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).
234 changes: 234 additions & 0 deletions designs/container_image_builds_ecs_agentcore.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
Container Image Builds for ECS and AgentCore
=============================================

This is the design for extending `sam build`, `sam package`, `sam deploy`, and `sam sync`
to support building and deploying container images for non-Lambda resources:
`AWS::ECS::TaskDefinition` and `AWS::BedrockAgentCore::Runtime`.

What is the problem?
--------------------

SAM CLI provides an excellent developer experience for Lambda Image functions: a single
`sam build && sam deploy` builds the Docker image, pushes to ECR, and deploys via
CloudFormation. However, 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 alongside Lambda functions.

This creates a fragmented workflow where developers need external tooling (shell scripts,
Makefiles, or CI/CD steps) for the identical operation: build image → push to ECR →
update template with ECR URI → deploy.

What will be changed?
---------------------

We extend the existing Lambda Image build pipeline to recognize `AWS::ECS::TaskDefinition`
and `AWS::BedrockAgentCore::Runtime` resources that have a `Metadata` block with
`Dockerfile` and `DockerContext`. No new commands are introduced — the existing
`sam build`, `sam package`, `sam deploy`, and `sam sync` gain awareness of these
resource types.

### Design Principles

1. **Same convention** — Uses the identical Metadata block as Lambda Image functions
(Dockerfile, DockerContext, DockerTag, DockerBuildArgs, DockerBuildTarget)
2. **No Transform changes** — Works with native CloudFormation resource types
3. **Opt-in** — Only resources with the Metadata block are affected; existing templates
work unchanged
4. **Reuse** — Delegates to the same `_build_lambda_image()` Docker build logic

Success criteria for the change
-------------------------------

1. `sam build` discovers and builds container images for ECS TaskDefinitions and
AgentCore Runtimes that have Dockerfile metadata
2. `sam deploy --resolve-image-repos` auto-creates ECR repos for these resources
3. `sam package` / `sam deploy` pushes images to ECR and rewrites the template with
the ECR URI at the correct property path
4. `sam sync` builds, pushes, and triggers redeployment for these resources
5. Multi-container ECS TaskDefinitions can target a specific container via `ContainerName`
6. Architecture can be specified via `Architecture` metadata (e.g., `arm64`)
7. Buildkit support works automatically (shared with Lambda Image builds)
8. No regressions for existing Lambda, Layer, or API builds

User Experience
---------------

### Template Format

```yaml
Resources:
# AgentCore Runtime
MyAgent:
Type: AWS::BedrockAgentCore::Runtime
Metadata:
Dockerfile: Dockerfile
DockerContext: ./agent
DockerTag: latest
Architecture: arm64
Properties:
AgentRuntimeName: my_agent
AgentRuntimeArtifact:
ContainerConfiguration:
ContainerUri: placeholder
NetworkConfiguration:
NetworkMode: PUBLIC
RoleArn: !GetAtt AgentRole.Arn

# ECS TaskDefinition (multi-container)
MyTask:
Type: AWS::ECS::TaskDefinition
Metadata:
Dockerfile: Dockerfile
DockerContext: ./app
DockerTag: latest
ContainerName: web
Properties:
Family: my-app
ContainerDefinitions:
- Name: envoy
Image: public.ecr.aws/envoy:latest
- Name: web
Image: placeholder
```

### CLI Usage

```bash
# Build container images
sam build

# Deploy with auto ECR repo creation
sam deploy --resolve-image-repos

# Or with explicit repo
sam deploy --image-repositories SimpleAgent=123456789012.dkr.ecr.us-east-1.amazonaws.com/repo

# Live sync
sam sync --stack-name my-stack --watch --resolve-image-repos
```

### Metadata Fields

| Field | Required | Description |
|-------|----------|-------------|
| `Dockerfile` | Yes | Path to Dockerfile relative to DockerContext |
| `DockerContext` | Yes | Build context directory relative to template |
| `DockerTag` | No | Image tag (default: `latest`) |
| `DockerBuildArgs` | No | Dict of build args |
| `DockerBuildTarget` | No | Multi-stage build target |
| `DockerBuildExtraParams` | No | List of extra docker build params |
| `Architecture` | No | Target platform: `x86_64` (default) or `arm64` |
| `ContainerName` | No | ECS only: target container in multi-container TaskDefinition |

Implementation
--------------

### Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│ sam build │
├─────────────────────────────────────────────────────────────────┤
│ BuildContext.run() │
│ ├── builder.build() → Lambda functions + layers │
│ └── _build_container_images() → ECS + AgentCore containers │
│ ├── SamContainerServiceProvider (discovery) │
│ ├── ContainerBuildDefinition (build graph) │
│ └── ApplicationBuilder.build_container_images() │
│ └── _build_lambda_image() (shared Docker logic) │
├─────────────────────────────────────────────────────────────────┤
│ sam package / sam deploy │
│ ├── sync_ecr_stack() → auto-creates ECR repos (companion) │
│ ├── ECSTaskDefinitionImageResource.export() → push + rewrite │
│ └── AgentCoreRuntimeImageResource.export() → push + rewrite │
├─────────────────────────────────────────────────────────────────┤
│ sam sync │
│ └── ECSContainerSyncFlow │
│ ├── gather_resources() → build image │
│ ├── sync() → push to ECR + force ECS deployment │
│ └── SyncFlowFactory (registered for both types) │
└─────────────────────────────────────────────────────────────────┘
```

### Key Components

**`samcli/lib/providers/sam_container_provider.py`** (new)
- `SamContainerServiceProvider`: Scans stacks for ECS/AgentCore resources with
Dockerfile+DockerContext metadata. Returns `ContainerService` NamedTuples.

**`samcli/lib/build/build_graph.py`** (modified)
- `ContainerBuildDefinition`: Parallel to `FunctionBuildDefinition`. Holds resource
identifier, type, metadata, and architecture. Reads `Architecture` from metadata.

**`samcli/lib/build/app_builder.py`** (modified)
- `build_container_images()`: Iterates container definitions and builds each.
- `_build_container_image()`: Delegates to `_build_lambda_image()` — same Docker logic.
- `_update_built_resource()`: Extended for ECS (`ContainerDefinitions[N].Image`) and
AgentCore (`AgentRuntimeArtifact.ContainerConfiguration.ContainerUri`). Accepts
optional `metadata` param for `ContainerName` targeting.

**`samcli/lib/package/packageable_resources.py`** (modified)
- `ECSTaskDefinitionImageResource`: Custom export for nested `ContainerDefinitions[0].Image`.
- `AgentCoreRuntimeImageResource`: Export using jmespath for deeply nested property path.
- Both use `ARTIFACT_TYPE = ZIP` to pass the `PackageType` filter (these resources
don't have a `PackageType` property).

**`samcli/lib/sync/flows/ecs_container_sync_flow.py`** (new)
- `ECSContainerSyncFlow`: Builds image, pushes to ECR, forces ECS service redeployment
by finding services using the task definition family.

**`samcli/lib/bootstrap/companion_stack/companion_stack_manager.py`** (modified)
- `sync_ecr_stack()`: Extended to include container service resources when creating
ECR repos via the companion stack.

### Property Path Mapping

| Resource Type | Property Path for Image URI |
|---------------|----------------------------|
| `AWS::Serverless::Function` | `ImageUri` |
| `AWS::Lambda::Function` | `Code.ImageUri` |
| `AWS::ECS::TaskDefinition` | `ContainerDefinitions[N].Image` |
| `AWS::BedrockAgentCore::Runtime` | `AgentRuntimeArtifact.ContainerConfiguration.ContainerUri` |

Alternatives Considered
-----------------------

### 1. New SAM Transform resource type (e.g., `AWS::Serverless::ContainerService`)

**Rejected because:**
- Requires changes to the SAM Transform (separate repo, separate approval process)
- Adds coupling between SAM CLI and the Transform
- Users would need to wait for Transform support in all regions
- Native CFN types already work and are well-understood

### 2. Separate `sam container build` command

**Rejected because:**
- Fragments the workflow — users would need to remember different commands
- Doesn't integrate with `sam deploy` and `sam sync` naturally
- The existing `sam build` already handles image builds for Lambda

### 3. Using `PackageType: Image` on ECS/AgentCore resources

**Rejected because:**
- `PackageType` is a Lambda-specific concept not present on ECS or AgentCore resources
- Would require CloudFormation schema changes
- The Metadata-based approach is already the established pattern

Breaking Changes
----------------

None. This is purely additive:
- Templates without ECS/AgentCore Metadata are unaffected
- The `_update_built_resource` signature change is backward compatible (optional param)
- No existing CLI flags or behaviors change

Future Extensions
-----------------

1. **Multiple Dockerfiles per ECS TaskDefinition** — Build multiple containers from
one resource using a list of Metadata entries
2. **`sam local start-ecs`** — Local testing of ECS containers (similar to `sam local start-api`)
3. **Health check integration** — Wait for container health before marking sync complete
4. **Build caching** — Layer-aware caching for container builds (currently rebuilds fully)
5. **`sam init` templates** — Starter templates for ECS+SAM and AgentCore+SAM projects
35 changes: 34 additions & 1 deletion samcli/commands/build/build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
BuildError,
UnsupportedBuilderLibraryVersionError,
)
from samcli.lib.build.build_graph import DEFAULT_DEPENDENCIES_DIR
from samcli.lib.build.build_graph import DEFAULT_DEPENDENCIES_DIR, ContainerBuildDefinition
from samcli.lib.build.bundler import EsbuildBundlerManager
from samcli.lib.build.exceptions import (
BuildInsideContainerError,
Expand All @@ -45,6 +45,7 @@
from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable
from samcli.lib.providers.provider import LayerVersion, ResourcesToBuildCollector, Stack
from samcli.lib.providers.sam_api_provider import SamApiProvider
from samcli.lib.providers.sam_container_provider import SamContainerServiceProvider
from samcli.lib.providers.sam_function_provider import SamFunctionProvider
from samcli.lib.providers.sam_layer_provider import SamLayerProvider
from samcli.lib.providers.sam_stack_provider import SamLocalStackProvider
Expand Down Expand Up @@ -302,6 +303,11 @@ def run(self) -> None:

self._build_result = builder.build()

# Build container images for ECS/AgentCore resources
container_artifacts = self._build_container_images(builder)
if container_artifacts:
self._build_result.artifacts.update(container_artifacts)

self._handle_build_post_processing(builder, self._build_result)

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

def _build_container_images(self, builder: ApplicationBuilder) -> Dict[str, str]:
"""
Discover and build container images for ECS/AgentCore resources.

Returns
-------
Dict[str, str]
Map of resource full_path to built image tag
"""
container_provider = SamContainerServiceProvider(self.stacks)
container_services = list(container_provider.get_all())
if not container_services:
return {}

LOG.info("Found %d container service resource(s) to build", len(container_services))

container_build_defs = []
for service in container_services:
build_def = ContainerBuildDefinition(
resource_identifier=service.full_path,
resource_type=service.resource_type,
metadata=service.metadata,
)
container_build_defs.append(build_def)

return builder.build_container_images(container_build_defs)

@property
def is_building_specific_resource(self) -> bool:
"""
Expand Down
2 changes: 2 additions & 0 deletions samcli/commands/build/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
2. AWS::Lambda::Function\n
3. AWS::Serverless::LayerVersion\n
4. AWS::Lambda::LayerVersion\n
5. AWS::ECS::TaskDefinition (container image)\n
6. AWS::BedrockAgentCore::Runtime (container image)\n
\b
Supported Runtimes
------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,14 @@ def sync_ecr_stack(
function_logical_ids = [
function.full_path for function in function_provider.get_all() if function.packagetype == IMAGE
]

# Also include ECS/AgentCore container resources that need ECR repos
from samcli.lib.providers.sam_container_provider import SamContainerServiceProvider

container_provider = SamContainerServiceProvider(stacks)
container_logical_ids = [service.full_path for service in container_provider.get_all()]
function_logical_ids.extend(container_logical_ids)

manager.set_functions(function_logical_ids, image_repositories)
image_repositories.update(manager.get_repository_mapping())
manager.sync_repos()
Expand Down
Loading
Loading