Repository navigation
Conversation
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
| 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): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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 -.)
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 . |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Agreed. Addressed in b458174:
- Added a
MicrovmImageWithRunfixture whose Dockerfile hasRUN python -c "print(1)", plustest_build_microvm_image_with_run_step_succeedsthat builds it. - Since every runner in this workflow is x86, a
RUNstep needs emulation wherever it runs. I movedtest_build_cmd_microvm_image.pyfrombuild-x86-2to thebuild-arm64lane, 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.
|
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:
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 ( |
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.
|
Thanks for the quick fixes. One more suggestion: a test for MicrovmImage inside It already works. Registering the types in
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 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())
|
…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.
|
Thanks for writing and verifying the test. Added it as-is in 771ecc4, with the Locally (Docker) all 12 MicrovmImage integration tests pass. I also confirmed the regression check: with |
🧪 Hands-on test: No problems observed in 8 scenariosCommit Scenarios
🤖 Automated hands-on test — not a review approval · model claude-opus-5.5 · Kiro kiro-cli 2.33.1 · build History (8)
|
…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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
This mapping can collide:
- Logical IDs that differ only in case (
MyImagevsMyimage) both becomemyimage:latest. - CDK construct IDs like
My ImageandMy-Imageboth becomemy-image:latest. - The tag is global to the Docker daemon, so two SAM projects on the same machine that each have a
MyImageresource 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.
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| ) | ||
|
|
||
| return ApplicationBuildResult(build_graph, build_strategy.build()) | ||
| result = ApplicationBuildResult(build_graph, build_strategy.build()) |
There was a problem hiding this comment.
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.
| return True | ||
|
|
||
| @staticmethod | ||
| def is_microvm_image_buildable(image: MicrovmImage): |
There was a problem hiding this comment.
A few small consistency points:
- This returns a
(bool, str)tuple with no return type hint (-> Tuple[bool, str]), whileis_layer_buildableright above returns a plain bool. The tuple is useful for the skip reason, but a type hint would make the difference obvious. - The
.zipcheck is case-sensitive, soCodeUri: ./image.ZIPgets through (the layer check has the same behavior, so this is optional). - Functions emit
BUILD_FUNCTION_RUNTIMEtelemetry, but there's no event for MicrovmImage builds. Worth adding one if you want adoption numbers for this feature.
There was a problem hiding this comment.
All three are in d9fe65d:
is_microvm_image_buildablenow has-> Tuple[bool, str]and a docstring that says it returns the skip reason.- The
.zipcheck is case-insensitive for MicrovmImages (./image.ZIPis now skipped). I leftis_layer_buildableunchanged. - A build that includes MicrovmImages tracks
UsedFeaturewith the new valueMicrovmImageBuild, next toBUILD_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.
Which issue(s) does this change fix?
Part of #9283. That issue asks for local
CodeUrisupport acrosssam build,sam package, andsam deploy; this PR coverssam build. Packaging (zipping and uploading the source directory) is the remaining part of the issue.Why is this change necessary?
sam builddoes not recognizeAWS::Serverless::MicrovmImageorAWS::Lambda::MicrovmImage, so there is no local build step for MicroVM images.How does it address the issue?
sam buildnow builds MicrovmImage resources by runningdocker buildon the resource's source directory (CodeUri, orCodeArtifactfor the Lambda type) and tagging the image with its full path, lowercased with/replaced by-, plus:latest(for examplechild-myimage:latest), for local testing. The tag is reported asimage_tagin the JSON build output.SamMicrovmImageProviderextracts images from the template. Architecture comes fromCpuConfigurations[0].Architecture; onlyARM_64is supported and it is the default.CodeArtifactis 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.MicrovmImageBuildDefinitionis wired into the build graph and into the default, cached, incremental, and parallel strategies. It is not persisted tobuild.toml, because Docker/Finch has its own layer cache.--base-dirand an intrinsicCodeUri.sam build MyImagefails with a specific reason if the image is not buildable (for example, an unsupported architecture).sam buildwith no target warns and skips it.--output jsonincludes built MicrovmImages ("type": "microvm_image").What side effects does this change have?
AWS::Serverless::MicrovmImageandAWS::Lambda::MicrovmImageare registered inRESOURCES_WITH_LOCAL_PATHS(CodeUri;CodeArtifactandCodeArtifact.Uri) somove_templaterebases their paths.tests/unit/commands/buildcmd/test_build_context_web_functions.pynow also mocks the MicrovmImage provider.sam packagenow 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.sam buildwith no target warns and skips the images and still succeeds, since packaging uses the source directory;sam build MyImagefails.Mandatory Checklist
PRs will only be reviewed after checklist is complete
make prpassesmake update-reproducible-reqsif dependencies were changedBy submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.