Skip to content

feat(build): Add sam build support for MicrovmImage - #9357

Draft
licjun wants to merge 20 commits into
aws:developfrom
licjun:feat-microvm-image-build
Draft

licjun wants to merge 20 commits into
aws:developfrom
licjun:feat-microvm-image-build

Conversation

@licjun

@licjun licjun commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Which issue(s) does this change fix?

Part of #9283. That issue asks for local CodeUri support across sam build, sam package, and sam deploy; this PR covers sam build. Packaging (zipping and uploading the source directory) is the remaining part of the issue.

Why is this change necessary?

sam build does not recognize AWS::Serverless::MicrovmImage or AWS::Lambda::MicrovmImage, so there is no local build step for MicroVM images.

How does it address the issue?

sam build now builds MicrovmImage resources by running docker build on the resource's source directory (CodeUri, or CodeArtifact for the Lambda type) and tagging the image with its full path, lowercased with / replaced by -, plus :latest (for example child-myimage:latest), for local testing. The tag is reported as image_tag in the JSON build output.

  • Provider: SamMicrovmImageProvider extracts images from the template. Architecture comes from CpuConfigurations[0].Architecture; only ARM_64 is supported and it is the default. CodeArtifact is accepted as a bare string (what CDK templates normalize to) or as {Uri: ...}. S3 sources are skipped with a warning, as for functions and layers.
  • Build graph and strategies: a new MicrovmImageBuildDefinition is wired into the build graph and into the default, cached, incremental, and parallel strategies. It is not persisted to build.toml, because Docker/Finch has its own layer cache.
  • Built template: the code path is not rewritten to a build artifact. The local image is only for local testing; the deployment artifact is the source directory, which is zipped and uploaded, and the MicroVM service builds it remotely. After a successful build, the code path in the built template is set to the directory that was built, relative to the built template, so it still points at the source directory for packaging. This also covers --base-dir and an intrinsic CodeUri.
  • Targeting: sam build MyImage fails with a specific reason if the image is not buildable (for example, an unsupported architecture). sam build with no target warns and skips it.
  • --output json includes built MicrovmImages ("type": "microvm_image").

What side effects does this change have?

  • AWS::Serverless::MicrovmImage and AWS::Lambda::MicrovmImage are registered in RESOURCES_WITH_LOCAL_PATHS (CodeUri; CodeArtifact and CodeArtifact.Uri) so move_template rebases their paths.
  • tests/unit/commands/buildcmd/test_build_context_web_functions.py now also mocks the MicrovmImage provider.
  • sam package now fails with a clear error for a MicrovmImage with local code, instead of leaving the local path in the packaged template. Packaging support is the follow-up for Feature request: sam build/deploy should zip and upload local CodeUri for AWS::Serverless::MicrovmImage, like it does for Function #9283. S3 and intrinsic values are passed through.
  • Building a MicrovmImage needs a container runtime (Docker or Finch). Without one, sam build with no target warns and skips the images and still succeeds, since packaging uses the source directory; sam build MyImage fails.

Mandatory Checklist

PRs will only be reviewed after checklist is complete

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Add sam build support for AWS::Serverless::MicrovmImage and
AWS::Lambda::MicrovmImage. sam build runs docker build on the
resource's source directory and tags the image <full-path>:latest
for local testing.

The built template's code path is not rewritten to a build artifact:
the local image is not the deployment artifact. The source directory
is what gets zipped and uploaded, and the MicroVM service builds it
remotely. move_template only rebases the relative path.

- SamMicrovmImageProvider extracts images from the template, maps
  CpuConfigurations[0].Architecture (only ARM_64 is supported), and
  accepts CodeArtifact as either a string or {Uri: ...}
- MicrovmImageBuildDefinition is wired into the build graph and all
  build strategies; it is not persisted to build.toml
- Explicit targets (sam build MyImage) fail on an unsupported
  architecture; the build-all path warns and skips
- --output json reports built MicrovmImages
@github-actions github-actions Bot added area/build sam build command area/schema JSON schema file pr/internal labels Oct 21, 2026
Comment thread samcli/lib/build/app_builder.py Outdated
if code_value and not isinstance(code_value, str):
resolved_props = normalized_resources.get(logical_id, {}).get("Properties", {})
resolved_code = resolved_props.get(code_prop)
if isinstance(resolved_code, str):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only handles the bare-string form. For the hand-written CodeArtifact: {Uri: !Ref Src}, the resolved value is a dict, so it is skipped. The built template keeps {"Uri": {"Ref": "Src"}}, and move_template cannot rebase a non-string path (samcli/commands/_utils/template.py:392). At package time the path then resolves against .aws-sam/build/ instead of the source template's directory.

This test fails today and passes once the dict form is handled:

class TestApplicationBuilder_update_template_resolves_microvm_intrinsic_code(TestCase):
    @patch("samcli.lib.build.app_builder.get_validated_container_client")
    def setUp(self, mock_get_validated_container_client):
        mock_get_validated_container_client.return_value = Mock()
        self.builder = ApplicationBuilder(
            Mock(), "builddir", "basedir", "cachedir", stream_writer=StreamWriter(sys.stderr)
        )

    @parameterized.expand(
        [
            ("serverless_code_uri", "AWS::Serverless::MicrovmImage", "CodeUri", {"Ref": "Src"}, "./app"),
            ("lambda_code_artifact_string", "AWS::Lambda::MicrovmImage", "CodeArtifact", {"Ref": "Src"}, "./app"),
            (
                "lambda_code_artifact_uri",
                "AWS::Lambda::MicrovmImage",
                "CodeArtifact",
                {"Uri": {"Ref": "Src"}},
                {"Uri": "./app"},
            ),
        ]
    )
    def test_intrinsic_code_path_is_replaced_with_resolved_literal(
        self, _name, resource_type, code_prop, raw_value, resolved_value
    ):
        template_dict = {"Resources": {"MyImage": {"Type": resource_type, "Properties": {code_prop: raw_value}}}}
        resolved_resources = {"MyImage": {"Type": resource_type, "Properties": {code_prop: resolved_value}}}
        stack = Mock(
            stack_path="", template_dict=template_dict, resources=resolved_resources, location="/proj/template.yaml"
        )

        actual = self.builder.update_template(stack, {"MyImage": "myimage:latest"}, {})

        self.assertEqual(actual["Resources"]["MyImage"]["Properties"][code_prop], resolved_value)

The third case fails with {'Uri': {'Ref': 'Src'}} != {'Uri': './app'}. This branch has no unit test today; only the integration test covers the CodeUri case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, confirmed. The nested form was added after this replacement logic and I didn't update it. Fixed in 8e42a40: a resolved {"Uri": <str>} is now accepted alongside a resolved string, so the built template gets {"Uri": "./app"} and move_template rebases it via the CodeArtifact.Uri entry in RESOURCES_WITH_LOCAL_PATHS.

I added your test as-is (TestApplicationBuilder_update_template_resolves_microvm_intrinsic_code). Before the fix the lambda_code_artifact_uri case failed with {'Uri': {'Ref': 'Src'}} != {'Uri': './app'}; all three cases pass now, and the full unit suite passes.

+ [
{
"resource_id": image.full_path,
"type": "microvm_image",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this entry include the built tag, for example "image_tag": "myimage:latest"? The image exists for local testing, but nothing reports its name: it isn't in the JSON output or the built template. A tool that wants to run it has to copy the lowercase-and-dash rule from _build_microvm_image, and breaks if that rule changes. The JSON output is the machine contract, so it seems the right place.

(Small one: the PR description says the tag is <full-path>:latest, but the code lowercases it and replaces / with -.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Added image_tag to the entry, taken from the build result so it's the tag that was actually built, with a test (09e410c). Also fixed the description: the tag is the full path lowercased with / replaced by -, plus :latest.

"dockerfile": "Dockerfile",
"tag": docker_tag,
"buildargs": {},
"platform": get_docker_platform(architecture),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image is always built for linux/arm64, but nothing warns on an x86 host. warn_cross_architecture_build only covers functions, and only with --use-container (app_builder.py:239-242). On x86 Linux without QEMU set up, any RUN step fails with exec format error and no hint why. Could we warn when the host architecture differs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Fixed in 1f48ed3: _build_microvm_image now compares the host architecture (_get_host_architecture, same helper the function warning uses) with the image architecture and logs a warning when they differ, explaining that RUN steps need emulation (e.g. QEMU via binfmt_misc) and otherwise fail with exec format error. It doesn't depend on --use-container, since a MicrovmImage build is always a Docker build. It stays a warning rather than an error because hosts with emulation (e.g. Docker Desktop) can complete the build. Added unit tests for the x86_64-host and arm64-host cases.


WORKDIR /app

COPY app.py .

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Dockerfile has no RUN step, so the arm64 build never runs arm64 code. The green ubuntu x86 lanes don't show cross-arch builds work. A trivial RUN python -c "print(1)" would make the test exercise emulation.

@licjun licjun Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Addressed in b458174:

  • Added a MicrovmImageWithRun fixture whose Dockerfile has RUN python -c "print(1)", plus test_build_microvm_image_with_run_step_succeeds that builds it.
  • Since every runner in this workflow is x86, a RUN step needs emulation wherever it runs. I moved test_build_cmd_microvm_image.py from build-x86-2 to the build-arm64 lane, which already sets up QEMU, so the new test exercises the x86 + emulated arm64 path.
  • The tier1 smoke test keeps the COPY-only fixture. The tier1 lanes (Finch, Windows) don't set up QEMU, and that test is meant to cover the container runtime rather than emulation.

@bnusunny

Copy link
Copy Markdown
Contributor

Nice work. The model is clear: the image is local-only, the built template keeps pointing at the source directory, and the image stays out of build.toml.

Two things to fix before this leaves draft:

  1. CodeArtifact: {Uri: !Ref X} leaves an unresolved intrinsic in the built template, so its path is never rebased. There is a failing test in the inline comment.
  2. Nothing reports the image tag, so a tool that wants to run the local image has to re-derive the naming rule.

The inline comments cover these two, plus arm64 builds on x86 hosts. CI: the only red lane is durable-functions on 3.10, which failed pulling the emulator image from public ECR (toomanyrequests). It is unrelated to this change; a rerun should clear it.

update_template only replaced an intrinsic MicrovmImage code path when
the resolved value was a string. A hand-written
CodeArtifact: {Uri: !Ref Src} resolves to {"Uri": "./app"}, so the built
template kept {"Uri": {"Ref": "Src"}}. move_template cannot rebase a
non-string path, so packaging would resolve it against .aws-sam/build/.

Also accept a resolved {"Uri": <str>} and add unit tests for the
intrinsic replacement.
r4340571595: the local image sam build creates for a MicrovmImage is
for local testing, but nothing reported its tag. The built template
keeps the source directory, and the JSON entry had only the resource ID
and architecture, so a tool had to copy the tagging rule from
_build_microvm_image.

Add image_tag to the entry, read from the build result so it is the tag
that was actually built.
MicrovmImage is built for linux/arm64, but Dockerfile RUN steps execute
on the host CPU. On an x86 host without emulation they fail with
"exec format error" and no hint why. The existing cross-architecture
warning only covers functions built with --use-container.

Warn when the host architecture differs from the image architecture,
reusing _get_host_architecture. The build still proceeds, since hosts
with QEMU (e.g. Docker Desktop) can complete it.
The existing fixture only has COPY, so building it for linux/arm64 never
runs arm64 code and the green x86 lanes did not show cross-arch builds
work. Add a fixture with a RUN step and a test that builds it.

Move the MicrovmImage integration tests from build-x86-2 to the
build-arm64 lane, which sets up QEMU. The tier1 smoke test keeps the
COPY-only fixture, since tier1 lanes do not set up emulation.
@bnusunny

Copy link
Copy Markdown
Contributor

Thanks for the quick fixes. One more suggestion: a test for MicrovmImage inside Fn::ForEach with --language-extensions.

It already works. Registering the types in RESOURCES_WITH_LOCAL_PATHS is enough, because the language-extensions code builds its artifact-property and Mapping tables from that dict. But nothing pins it, so a later change to either side could break it silently. I ran the test below against b458174: it passes, and it fails with KeyError: 'Mappings' when AWS_SERVERLESS_MICROVMIMAGE is removed from RESOURCES_WITH_LOCAL_PATHS.

tests/integration/testdata/buildcmd/microvm-image-foreach/template.yaml, plus Alpha/Dockerfile and Beta/Dockerfile (each FROM python:3.12-slim + COPY . /app):

AWSTemplateFormatVersion: '2010-09-09'
Transform:
  - AWS::LanguageExtensions
  - AWS::Serverless-2016-10-31

Resources:
  Fn::ForEach::Images:
    - Name
    - [Alpha, Beta]
    - ${Name}Image:
        Type: AWS::Serverless::MicrovmImage
        Properties:
          Name: !Sub "${Name}-image"
          CodeUri: ./${Name}
          BaseImageArn: arn:aws:lambda:us-east-2:aws:microvm-image:al2023-1
          BaseImageVersion: "0"

In TestBuildMicrovmImage:

    def test_build_foreach_dynamic_code_uri(self):
        """With --language-extensions, each Fn::ForEach image builds from its own directory and the
        built template keeps Fn::ForEach, with per-image paths in a generated Mapping."""
        source_dir = Path(self.test_data_path, "microvm-image-foreach")
        self.template_path = str(source_dir.joinpath("template.yaml"))
        cmdlist = self.get_command_list(use_buildkit=self.use_buildkit) + ["--language-extensions"]
        command_result = run_command(cmdlist, cwd=self.working_dir)

        self.assertEqual(command_result.process.returncode, 0, command_result.stderr.decode())

        container_client = get_validated_container_client()
        for tag in ("alphaimage:latest", "betaimage:latest"):
            self.assertTrue(container_client.images.list(name=tag), f"Docker image '{tag}' should exist")

        with open(str(self.built_template), "r") as f:
            built_template = yaml_parse(f.read())
        self.assertIn("Fn::ForEach::Images", built_template["Resources"])
        mapping = built_template["Mappings"]["SAMCodeUriImages"]
        for name in ("Alpha", "Beta"):
            built_path = Path(self.default_build_dir, mapping[name]["CodeUri"]).resolve()
            self.assertEqual(built_path, source_dir.joinpath(name).resolve())

tearDown would also need to remove alphaimage:latest and betaimage:latest.

…nsions

MicrovmImage inside Fn::ForEach already builds with --language-extensions,
because the language-extensions code derives its artifact-property and
Mapping tables from RESOURCES_WITH_LOCAL_PATHS. Nothing pinned it, so add
an integration test: each image builds from its own directory, and the
built template keeps Fn::ForEach with per-image paths in the generated
SAMCodeUriImages Mapping.

The test fails with KeyError: 'Mappings' if AWS_SERVERLESS_MICROVMIMAGE is
removed from RESOURCES_WITH_LOCAL_PATHS.
@licjun

licjun commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for writing and verifying the test. Added it as-is in 771ecc4, with the microvm-image-foreach fixture and tearDown cleanup for alphaimage:latest and betaimage:latest.

Locally (Docker) all 12 MicrovmImage integration tests pass. I also confirmed the regression check: with AWS_SERVERLESS_MICROVMIMAGE removed from RESOURCES_WITH_LOCAL_PATHS, the new test fails with KeyError: 'Mappings'. It runs in the build-arm64 lane with the rest of the file, and it isn't tier1-marked.

@aws-sam-tooling-bot

aws-sam-tooling-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

🧪 Hands-on test: No problems observed in 8 scenarios

Commit d9fe65d · build LambdaToolingHandsOn-Docker:1d07c265-5d82-4956-aa14-2f985bef41b5. The plan's scenarios ran on an isolated build host; a problem is reported only if it reproduced at least twice. Passing scenarios show only that these checks behaved as expected, not that the change is correct.

Scenarios

Scenario Planned check Result Runner's note
S1 Main path: sam build --output json on an AWS::Serverless::MicrovmImage builds a linux/arm64 image tagged myimage:latest… ✅ pass Head: sam build --output json exits 0. The output has {resource_id: MyImage, type: microvm_image, architecture: arm64, image_tag: myimage:latest}. docker image inspect myimage:latest prints linux/arm…
S2 AWS::Lambda::MicrovmImage with CodeArtifact as a {Uri} dict and as a bare string, plus a tag collision warning for logi… ✅ pass The plan's template left out Name/BaseImageArn on the Serverless resources, so the SAM translator rejected it (InvalidSamDocumentException) on both head and base; this is not caused by the PR. Adding…
S3 Error and skip paths the PR adds. Targeted builds of bad images fail with specific reasons: missing dir, file CodeUri, … ✅ pass Name/BaseImageArn/BaseImageVersion were added so the SAM translator accepts the templates. Every targeted build exits 1 with no traceback: MissingImg 'does not exist', FileImg 'is a file', NoDockerfi…
S4 No container runtime: a build-all warns, skips the images and still succeeds; a targeted image build fails cleanly. Doc… ✅ pass Docker was made unreachable with DOCKER_HOST=unix:///tmp/no-such-docker.sock. Build-all exits 0 with 'Skipping MicrovmImage MyImage: no container runtime (Docker or Finch) is available...' and 'Build…
S5 sam package now fails with a clear ExportFailedError for a MicrovmImage with local code, and passes through an S3 CodeU… ✅ pass Run with fake credentials, AWS_ENDPOINT_URL set to unreachable 127.0.0.1:9, and IMDS disabled, so nothing could reach AWS. Head: local.yaml exits 1 with 'Unable to upload artifact ./src referenced by…
S6 Nested stack: the image in the child stack gets the tag child-myimage:latest, the built child template's CodeUri points… ✅ pass Name/BaseImageArn/BaseImageVersion were added to the child template. 'sam build --parallel --cached', 'sam build --cached' and 'sam build Child/MyImage' all exit 0 and list 'Child/MyImage -> child-my…
S7 --base-dir and an intrinsic CodeUri: the source is resolved from base_dir, and the built template's CodeUri is a litera… ✅ pass 'sam build -t infra/template.yaml --base-dir . --parameter-overrides Src=./alt' exits 0 and builds baseimg:latest and refimg:latest. In the built template, BaseImg.CodeUri is '../../app' (resolves to…
S8 Run the PR's unit tests for the provider, build graph, strategies, app builder, build context and package exporter, plu… ✅ pass Unit tests on head (8 files: provider, build graph, strategy, app builder, build context, web functions, artifact exporter): 646 passed, 6 subtests passed, 0 failed. Integration tests in tests/integr…

🤖 Automated hands-on test — not a review approval · model claude-opus-5.5 · Kiro kiro-cli 2.33.1 · build LambdaToolingHandsOn-Docker:1d07c265-5d82-4956-aa14-2f985bef41b5 · commit d9fe65d

History (8)
Commit Result When (UTC) Build
d9fe65d No problems observed 2026-10-27 17:15 LambdaToolingHandsOn-Docker:1d07c265-5d82-4956-aa14-2f985bef41b5
05e9ed3 Problems reproduced 2026-10-26 20:58 LambdaToolingHandsOn-Docker:980bc0d4-632b-411c-9bbb-ef37771ad45a
67b32bf Superseded 2026-10-26 20:33 LambdaToolingHandsOn-Docker:6005ac04-f619-4a21-bb44-bb34fd78e55a
749383c Problems reproduced 2026-10-24 00:03 LambdaToolingHandsOn-Docker:65ea523c-9d72-4da5-9a4c-9a2ebbb67373
93f98ac Superseded 2026-10-23 23:46 LambdaToolingHandsOn-Docker:8a2567ff-059e-456c-a3a9-73e297c54235
49c5b31 Problems reproduced 2026-10-23 23:25 LambdaToolingHandsOn-Docker:3b2dd16d-0a00-43a6-b19d-ac3201d2fd02
81f5c14 Problems reproduced 2026-10-23 21:43 LambdaToolingHandsOn-Docker:54fae771-de2e-478a-916b-c1cfc4fd78f4
771ecc4 Plan only (not run) 2026-10-23 18:04

licjun added 10 commits October 23, 2026 11:32
…t from

With --base-dir, the MicrovmImage source is resolved from base_dir, but
the built template kept the original CodeUri and move_template rebased
it from the template's directory. For sam build -t infra/template.yaml
--base-dir . with CodeUri: src/app, Docker built ./src/app while the
built template pointed at infra/src/app, which does not exist.

Record the absolute directory each image is built from, and have
update_template write it relative to the template's directory so
move_template rebases it correctly. This also covers intrinsic code
paths and CodeArtifact: {Uri: ...}, so the separate intrinsic
resolution is removed. update_template becomes an instance method;
its only caller already calls it on an instance.
…orted

MicrovmImage types are in RESOURCES_WITH_LOCAL_PATHS so sam build can
rebase their source path, but sam package has no exporter for them. On an
image-only project it reported "Successfully packaged artifacts", left
the local path in the packaged template, and suggested sam deploy.

Register placeholder exporters that fail with a clear message when the
code path is local. S3 URIs and unresolved intrinsics pass through
unchanged. The upcoming sam package support replaces these with real
exporters.
The text output never named the local image a MicrovmImage build
creates; the tag only appeared in the Docker stream, and "Built
Artifacts" holds nothing for it. The suggested next steps also offered
sam local invoke, which fails on a project with no functions.

Add a "Built MicrovmImages" section listing each image's tag and
platform, read from the build result. When only MicrovmImages were
built, drop the sam local invoke suggestion. Output is unchanged when no
MicrovmImage is built.
…rors

A CDK construct ID can contain characters Docker rejects in an image
name, so a resource ID such as "ucb space img" produced the invalid tag
"ucb space img:latest" and an unhandled traceback. Build the tag from
the full path with every run of non-alphanumeric characters replaced by
"-". Existing tags are unchanged.

_build_microvm_image re-raised any docker APIError other than a
Dockerfile-not-found error, which surfaced as an unexpected error asking
for a bug report. Raise DockerBuildFailed with the daemon's explanation
instead, so it is reported as a build failure attributed to the resource.
"Dockerfile not found in CodeUri directory" was shown when the
directory did not exist (including an unresolved parameter such as
'.../CodeUriParam'), when CodeUri was a file such as a .zip, and when
the Dockerfile was missing. It was also raised as DockerBuildFailed,
although no Docker build had run.

- Distinguish a missing directory (with a --parameter-overrides hint),
  a file, and a missing Dockerfile
- Raise a new MicrovmImageSourceError for these pre-build checks
- Record why the provider skipped an image (S3 source, missing or
  invalid code property), so sam build <image> reports that reason
  instead of "Unable to find resource"
Lambda image functions are built from Metadata Dockerfile, DockerContext,
DockerTag and DockerBuildArgs, so users may set them on a MicrovmImage
too. They have no effect: a MicrovmImage is built from the Dockerfile at
the root of CodeUri, as the MicroVM service builds it. Warn when any of
them is set.
Both the build strategy and _build_microvm_image logged a "Building
MicrovmImage" line for the same image. Keep the strategy's line, which
includes the source directory and is where function and layer progress
lines are logged.
run() collected the resources to build, and then
_check_build_method_experimental_flag collected them again. Collecting
logs a warning for each skipped MicrovmImage, so each warning printed
twice. Pass the already collected resources to the check instead.
…ure parameter

The unsupported Architecture warning always suggested
--parameter-overrides, including for a literal such as X86_64. An
unresolvable !Ref is rewritten to the parameter name, so add the hint
only when the value is the name of a template parameter, and name that
parameter.
With --cached, functions and layers report whether their cache was
valid, but MicrovmImages, which are always rebuilt with Docker's layer
cache, printed nothing. Log that SAM CLI does not cache them.

The build description and the --parallel help still described only
functions and layers. Include MicroVM images, and regenerate the
schema.
for image in self.microvm_image_provider.get_all():
if image.name in excludes:
continue
buildable, non_buildable_reason = BuildContext.is_microvm_image_buildable(image)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, a plain sam build (no target) on any template that contains a MicrovmImage now needs Docker/Finch, and so do sam sync / sam deploy flows that build first. Before this PR the image was ignored and the build succeeded. Since the local image is only for local testing and the deployment artifact is the source directory, would it make sense to warn and skip MicrovmImages on the build-all path when no container runtime is available, and only hard-fail when the image is explicitly targeted? If the hard requirement is intentional, it's worth calling out in the release notes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Unlike a PackageType: Image function, the local MicrovmImage is not the deployment artifact, so failing the whole build blocks a deploy that does not need it. Fixed in 0fc2c67: on the build-all path (sam build with no target, and flows such as sam sync that build everything), if no container runtime is available, each MicrovmImage is skipped with a warning and the build continues. sam build MyImage still fails. The built template still points the skipped image at its source directory, so packaging is unaffected. I also updated the side effects in the PR description.

return working_directory

@staticmethod
def _microvm_image_tag(full_path: str) -> str:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mapping can collide:

  • Logical IDs that differ only in case (MyImage vs Myimage) both become myimage:latest.
  • CDK construct IDs like My Image and My-Image both become my-image:latest.
  • The tag is global to the Docker daemon, so two SAM projects on the same machine that each have a MyImage resource overwrite each other's image.

Appending a short hash (e.g. of the full path, or of the resolved source dir) would avoid this. Alternatively, at least detect and warn on collisions within a single build.

@licjun licjun Oct 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I kept the tag derived from the full path without a hash, so the local tag stays predictable (for docker run myimage:latest and for a future sam local command that looks the image up by convention). This matches PackageType: Image functions, which are tagged <lowercased logical ID>:<DockerTag> and already have the case and cross-project collisions. I added your second option in e28fcc0: a build now warns when two MicrovmImages map to the same tag, for example:

MicrovmImages MyImage, Myimage all map to the local image tag 'myimage:latest', so the image built last overwrites the others. Rename the resources so their IDs differ by more than case or punctuation.

The cross-project case is not detected, since it involves images outside this build.

Comment thread samcli/lib/build/app_builder.py Outdated
self._image_build_client = CLIBuildClient(engine_type=engine_type)
LOG.debug(f"Using CLIBuildClient with engine_type {engine_type}")
else:
self._image_build_client = SDKBuildClient(self._container_client)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lazy _image_build_client setup (buildkit availability check, CLIBuildClient vs SDKBuildClient) is a copy of the block in _build_lambda_image (~line 570). Could it move into a small helper, e.g. _get_image_build_client(), that both call? That keeps the two paths from drifting when the buildkit logic changes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 789e74a: the setup is now _get_image_build_client(), and both _build_lambda_image and _build_microvm_image call it. It is still called inside the existing try blocks, so error handling is unchanged.

Comment thread samcli/lib/build/app_builder.py Outdated
)

return ApplicationBuildResult(build_graph, build_strategy.build())
result = ApplicationBuildResult(build_graph, build_strategy.build())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the temporary result variable doesn't add anything here. This could go back to return ApplicationBuildResult(build_graph, build_strategy.build()) to keep the diff minimal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in d9fe65d.

Comment thread samcli/commands/build/build_context.py Outdated
return True

@staticmethod
def is_microvm_image_buildable(image: MicrovmImage):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few small consistency points:

  • This returns a (bool, str) tuple with no return type hint (-> Tuple[bool, str]), while is_layer_buildable right above returns a plain bool. The tuple is useful for the skip reason, but a type hint would make the difference obvious.
  • The .zip check is case-sensitive, so CodeUri: ./image.ZIP gets through (the layer check has the same behavior, so this is optional).
  • Functions emit BUILD_FUNCTION_RUNTIME telemetry, but there's no event for MicrovmImage builds. Worth adding one if you want adoption numbers for this feature.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three are in d9fe65d:

  • is_microvm_image_buildable now has -> Tuple[bool, str] and a docstring that says it returns the skip reason.
  • The .zip check is case-insensitive for MicrovmImages (./image.ZIP is now skipped). I left is_layer_buildable unchanged.
  • A build that includes MicrovmImages tracks UsedFeature with the new value MicrovmImageBuild, next to BUILD_FUNCTION_RUNTIME.

A plain sam build on a template with a MicrovmImage failed when no
container runtime was available, although the local image is only for
local testing and packaging uses the source directory. On the build-all
path, warn and skip the images instead. Building a targeted image still
fails.
The local tag is derived from the full path, so IDs that differ only in
case or punctuation map to the same tag, and the image built last
overwrites the others. Warn about such collisions within a build.
_build_lambda_image and _build_microvm_image each had the same lazy
setup that picks the BuildKit CLI client or the Docker SDK client. Move
it into _get_image_build_client so the two paths cannot drift.
- Return ApplicationBuildResult directly, without a temporary variable.
- Add a return type to is_microvm_image_buildable, which returns the
  skip reason with the result, unlike is_layer_buildable.
- Treat a CodeUri ending in .ZIP or other casing as a .zip file.
- Track a UsedFeature telemetry event when a build includes
  MicrovmImages.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/build sam build command area/schema JSON schema file pr/internal

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants