microvm-ctl 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/PKG-INFO +45 -8
  2. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/README.md +40 -7
  3. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/__init__.py +4 -1
  4. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/bootstrap.py +6 -1
  5. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/cli.py +190 -2
  6. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/endpoint.py +42 -0
  7. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/fleet.py +54 -2
  8. microvm_ctl-0.2.0/microvm/hooks/server.py +669 -0
  9. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/images.py +33 -5
  10. microvm_ctl-0.2.0/microvm/integrations/__init__.py +16 -0
  11. microvm_ctl-0.2.0/microvm/integrations/durable.py +181 -0
  12. microvm_ctl-0.2.0/microvm/integrations/stepfunctions.py +210 -0
  13. microvm_ctl-0.2.0/microvm/lease.py +133 -0
  14. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/monitor.py +22 -15
  15. microvm_ctl-0.2.0/microvm/playground/__init__.py +5 -0
  16. microvm_ctl-0.2.0/microvm/playground/server.py +1849 -0
  17. microvm_ctl-0.2.0/microvm/playground/static/index.html +862 -0
  18. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm_ctl.egg-info/PKG-INFO +45 -8
  19. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm_ctl.egg-info/SOURCES.txt +12 -0
  20. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm_ctl.egg-info/requires.txt +3 -0
  21. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/pyproject.toml +5 -2
  22. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/tests/test_endpoint.py +77 -1
  23. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/tests/test_fleet.py +17 -0
  24. microvm_ctl-0.2.0/tests/test_hooks_lease.py +532 -0
  25. microvm_ctl-0.2.0/tests/test_integrations.py +330 -0
  26. microvm_ctl-0.2.0/tests/test_integrations_durable.py +170 -0
  27. microvm_ctl-0.2.0/tests/test_lease.py +131 -0
  28. microvm_ctl-0.2.0/tests/test_playground.py +228 -0
  29. microvm_ctl-0.1.0/microvm/hooks/server.py +0 -168
  30. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/LICENSE +0 -0
  31. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/client.py +0 -0
  32. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/config.py +0 -0
  33. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/data/lambda-microvms/2025-09-09/endpoint-rule-set-1.json.gz +0 -0
  34. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/data/lambda-microvms/2025-09-09/paginators-1.json +0 -0
  35. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/data/lambda-microvms/2025-09-09/service-2.json.gz +0 -0
  36. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/hooks/__init__.py +0 -0
  37. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm/throttle.py +0 -0
  38. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm_ctl.egg-info/dependency_links.txt +0 -0
  39. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm_ctl.egg-info/entry_points.txt +0 -0
  40. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/microvm_ctl.egg-info/top_level.txt +0 -0
  41. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/setup.cfg +0 -0
  42. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/tests/test_config.py +0 -0
  43. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/tests/test_cost_model.py +0 -0
  44. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/tests/test_hooks.py +0 -0
  45. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/tests/test_hooks_validate.py +0 -0
  46. {microvm_ctl-0.1.0 → microvm_ctl-0.2.0}/tests/test_throttle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microvm-ctl
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Control and execution plane for AWS Lambda MicroVMs: build images, launch Firecracker microVMs, scale fleets inside your quotas, call into them securely, and watch it all live.
5
5
  Author: Vivek Raja P S
6
6
  License: Apache-2.0
@@ -9,6 +9,7 @@ Project-URL: Repository, https://github.com/Vivek0712/microvm-ctl
9
9
  Project-URL: Documentation, https://github.com/Vivek0712/microvm-ctl/tree/main/docs
10
10
  Project-URL: Changelog, https://github.com/Vivek0712/microvm-ctl/blob/main/CHANGELOG.md
11
11
  Project-URL: Examples, https://github.com/Vivek0712/awesome-microvm
12
+ Project-URL: Articles, https://builder.aws.com/content/3JIDTpz0ZgatSBv24drra3gEod9/control-and-scale-aws-lambda-microvms-with-microvm-ctl
12
13
  Keywords: aws,lambda,microvm,firecracker,sandbox,serverless,fleet,orchestrator
13
14
  Classifier: Development Status :: 4 - Beta
14
15
  Classifier: Intended Audience :: Developers
@@ -17,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.9
17
18
  Classifier: Programming Language :: Python :: 3.10
18
19
  Classifier: Programming Language :: Python :: 3.11
19
20
  Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
20
22
  Classifier: Topic :: System :: Distributed Computing
21
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
24
  Requires-Python: >=3.9
@@ -28,19 +30,21 @@ Requires-Dist: rich>=13.7
28
30
  Provides-Extra: dev
29
31
  Requires-Dist: pytest>=8; extra == "dev"
30
32
  Requires-Dist: ruff>=0.5; extra == "dev"
33
+ Provides-Extra: durable
34
+ Requires-Dist: aws-durable-execution-sdk-python>=2.0.0; extra == "durable"
31
35
  Dynamic: license-file
32
36
 
33
37
  # microvm-ctl
34
38
 
35
- **The control and execution plane for [AWS Lambda MicroVMs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-microvms-guide.html).** Build a snapshot image from a Dockerfile, launch Firecracker microVMs in seconds, scale a fleet inside the quotas your account actually has, call into every VM over its authenticated endpoint, and watch the whole thing live. One Python SDK, one `mvm` command.
39
+ **The control and execution plane for [AWS Lambda MicroVMs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-microvms-guide.html).** Build a snapshot image from a Dockerfile, launch Firecracker microVMs in seconds, scale a fleet inside the quotas your account actually has, call into every VM over its authenticated endpoint, watch the whole thing live, and hand a VM a task from Step Functions, Lambda durable functions, or any orchestrator through a lease the VM completes itself. One Python SDK, one `mvm` command.
36
40
 
37
41
  [![ci](https://github.com/Vivek0712/microvm-ctl/actions/workflows/ci.yml/badge.svg)](https://github.com/Vivek0712/microvm-ctl/actions/workflows/ci.yml)
38
42
  [![pypi](https://img.shields.io/pypi/v/microvm-ctl)](https://pypi.org/project/microvm-ctl/)
39
- ![python](https://img.shields.io/badge/python-3.9%20to%203.12-blue)
43
+ ![python](https://img.shields.io/badge/python-3.9%20to%203.13-blue)
40
44
  ![license](https://img.shields.io/badge/license-Apache--2.0-green)
41
45
 
42
46
  ```console
43
- pip install microvm-ctl
47
+ pip install microvm-ctl # https://pypi.org/project/microvm-ctl/
44
48
 
45
49
  mvm bootstrap # one time: S3 artifact bucket + build/execution IAM roles
46
50
  mvm image build my-sandbox ./my-app # Dockerfile at ./my-app root -> runnable snapshot
@@ -50,6 +54,8 @@ mvm scale my-sandbox 10 # converge the fleet, throttled to yo
50
54
  mvm top --watch # live state table
51
55
  ```
52
56
 
57
+ To see it used for real first, jump to [the eight examples](#see-it-working-eight-examples-and-the-article-series).
58
+
53
59
  ## Why this exists
54
60
 
55
61
  Lambda MicroVMs exposes the primitive under Lambda itself: a Firecracker VM with a full AL2023 userland, a dedicated HTTPS endpoint, and a lifecycle you control (run, suspend, resume, terminate). The service deliberately stops there. There is no load balancer (one endpoint per VM), no fleet abstraction, no token management, no dashboard, and a fresh account runs quotas far below the published defaults. microvm-ctl fills that gap.
@@ -63,6 +69,8 @@ Lambda MicroVMs exposes the primitive under Lambda itself: a Firecracker VM with
63
69
  | In-VM hook runtime | zero-dependency server for `/ready`, `/validate`, `/run`, `/resume`, `/suspend`, `/terminate` plus your own routes | `microvm/hooks/server.py` |
64
70
  | Observability and cost | live fleet table, CloudWatch tail, a cost model that prices a session shape before you commit to it | `microvm/monitor.py` |
65
71
  | Account bootstrap | artifact bucket plus separate build and execution roles | `microvm/bootstrap.py` |
72
+ | Leases | `Lease`, `LeasePolicy`, `FleetManager.lease`: one task per VM, completed by the VM through a task token, callback id, HTTP, SQS, or EventBridge | `microvm/lease.py` |
73
+ | Orchestrator integrations | generated Step Functions state machine and IAM (`mvm lease asl`, `mvm lease policy`), `lease_microvm` for Lambda durable functions | `microvm/integrations/` |
66
74
 
67
75
  The `lambda-microvms` botocore service model ships inside the package, so the plane works on whatever boto3 you already have.
68
76
 
@@ -142,6 +150,14 @@ app.serve(port=8080)
142
150
 
143
151
  ![mvm cost](docs/img/mvm-cost.png)
144
152
 
153
+ ## The playground
154
+
155
+ `mvm playground` opens a local web app that drives everything above against the live service: build images, run and scale fleets, call VMs, tail logs, price sessions, run a parameterised benchmark, and watch every AWS API call the process makes in a trace. A dry-run switch turns each mutating action into a printout of the exact request it would send. See [docs/playground.md](docs/playground.md).
156
+
157
+ ```console
158
+ mvm playground # http://127.0.0.1:8765
159
+ ```
160
+
145
161
  ## Documentation
146
162
 
147
163
  - [Quickstart](docs/quickstart.md): from an empty account to a serving VM, with the environment variables explained.
@@ -150,11 +166,32 @@ app.serve(port=8080)
150
166
  - [Architecture](docs/architecture.md): the two planes, the lifecycle state machine, the fleet design decisions, and the security model.
151
167
  - [The hook contract](docs/hooks.md): what each hook is for and what breaks when you ignore it.
152
168
  - [Quotas and cost](docs/quotas-and-cost.md): the quota walls, what counts against them, and the cost model with worked examples.
153
- - [Troubleshooting](docs/troubleshooting.md): the errors we hit on the live service and what each one meant.
169
+ - [Troubleshooting](docs/troubleshooting.md): the errors I hit on the live service and what each one meant.
170
+ - [The playground](docs/playground.md): the local web app over the whole SDK, and how to host it.
171
+ - [Integrations](docs/integrations.md): the lease contract for handing a VM to Step Functions, Lambda durable functions, or your own orchestrator, and what the plane should own.
172
+
173
+ ## See it working: eight examples and the article series
174
+
175
+ The fastest way to understand the plane is to read the apps built on it. The companion repo [awesome-microvm](https://github.com/Vivek0712/awesome-microvm) holds eight production-shaped examples, each a Dockerfile plus a single-file app, deployed and recorded on the live service:
176
+
177
+ | Example | What it shows |
178
+ |---|---|
179
+ | [code-sandbox](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/code-sandbox) | untrusted or AI-written Python per session; state persists across calls and suspend |
180
+ | [ai-code-runner](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/ai-code-runner) | Bedrock writes code, the VM runs it, tracebacks drive a self-repair loop |
181
+ | [agent-eval](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/agent-eval) | `Fleet.scale_to` fan-out over byte-identical clones, scoreboard, drain |
182
+ | [notebook](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/notebook) | a kernel whose namespace survives suspend and resume with the same PID |
183
+ | [data-analytics](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/data-analytics) | DuckDB over S3 through the execution role; bulk data off the endpoint |
184
+ | [ci-runner](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/ci-runner) | clone, test, report, terminate; `--max-duration` as the runaway cap |
185
+ | [pdf-service](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/pdf-service) | untrusted HTML rendered in the VM; idle policy sleeps it between bursts |
186
+ | [multi-tenant-agents](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/multi-tenant-agents) | one VM per tenant, identity via `runHookPayload`, `run_payload_factory` on a `Fleet` |
187
+
188
+ The three-part article series **Building on AWS Lambda MicroVMs** on the AWS Builder Center walks through them:
154
189
 
155
- ## Examples and the article series
190
+ 1. [Control and scale AWS Lambda MicroVMs with microvm-ctl](https://builder.aws.com/content/3JIDTpz0ZgatSBv24drra3gEod9/control-and-scale-aws-lambda-microvms-with-microvm-ctl): this plane and its measurements.
191
+ 2. [Seven workloads Lambda could never run, until MicroVMs](https://builder.aws.com/content/3JJ2oNWY9EsZzivMMx044cSlrFQ/seven-workloads-lambda-could-never-run-until-microvms): the first seven examples through build, run, cost, and gotchas.
192
+ 3. [A kernel for every customer: scaling AI agents to 1,000 tenants on AWS Lambda MicroVMs with microvm-ctl](https://builder.aws.com/content/3JJ7tASPSSUUTrcpWnWtxOuu8g3/a-kernel-for-every-customer-scaling-ai-agents-to-1000-tenants-on-aws-lambda-microvms-with-microvm-ctl): the multi-tenant finale with the decision guide.
156
193
 
157
- Eight production-shaped apps run on this plane: a code execution sandbox, an AI code runner with a self-repair loop, an agent evaluation fleet, a stateful notebook kernel, sandboxed DuckDB analytics, an ephemeral CI runner, an HTML to PDF service, and multi-tenant AI agents. Each has a Dockerfile, a single-file app, a recorded live transcript, and an article in the series **Building on AWS Lambda MicroVMs**. They live in the companion repo [awesome-microvm](https://github.com/Vivek0712/awesome-microvm).
194
+ The article sources and a long-form deep dive per example live under [awesome-microvm/blog](https://github.com/Vivek0712/awesome-microvm/tree/main/blog).
158
195
 
159
196
  ## Requirements and regions
160
197
 
@@ -162,7 +199,7 @@ Python 3.9 or newer on the machine running the plane. The VMs themselves are ARM
162
199
 
163
200
  ## Credits and inspiration
164
201
 
165
- This project grew out of [lambda-microvm-starter](https://github.com/vidanov/lambda-microvm-starter) by [Alexey Vidanov](https://github.com/vidanov), the one-command on-ramp that deploys any Dockerfile to a Lambda MicroVM behind a public CloudFront URL. His starter kit and its troubleshooting notes were the first working map of the service we had, and several of the gotchas documented here were first written down there. microvm-ctl takes the next step from one deployed app to fleets, tokens, quotas, and cost, and we are grateful for the ground he covered first.
202
+ This project grew out of [lambda-microvm-starter](https://github.com/vidanov/lambda-microvm-starter) by [Alexey Vidanov](https://github.com/vidanov), the one-command on-ramp that deploys any Dockerfile to a Lambda MicroVM behind a public CloudFront URL. His starter kit and its troubleshooting notes were the first working map of the service I had, and several of the gotchas documented here were first written down there. microvm-ctl takes the next step from one deployed app to fleets, tokens, quotas, and cost, and I am grateful for the ground he covered first.
166
203
 
167
204
  ## Contributing
168
205
 
@@ -1,14 +1,14 @@
1
1
  # microvm-ctl
2
2
 
3
- **The control and execution plane for [AWS Lambda MicroVMs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-microvms-guide.html).** Build a snapshot image from a Dockerfile, launch Firecracker microVMs in seconds, scale a fleet inside the quotas your account actually has, call into every VM over its authenticated endpoint, and watch the whole thing live. One Python SDK, one `mvm` command.
3
+ **The control and execution plane for [AWS Lambda MicroVMs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-microvms-guide.html).** Build a snapshot image from a Dockerfile, launch Firecracker microVMs in seconds, scale a fleet inside the quotas your account actually has, call into every VM over its authenticated endpoint, watch the whole thing live, and hand a VM a task from Step Functions, Lambda durable functions, or any orchestrator through a lease the VM completes itself. One Python SDK, one `mvm` command.
4
4
 
5
5
  [![ci](https://github.com/Vivek0712/microvm-ctl/actions/workflows/ci.yml/badge.svg)](https://github.com/Vivek0712/microvm-ctl/actions/workflows/ci.yml)
6
6
  [![pypi](https://img.shields.io/pypi/v/microvm-ctl)](https://pypi.org/project/microvm-ctl/)
7
- ![python](https://img.shields.io/badge/python-3.9%20to%203.12-blue)
7
+ ![python](https://img.shields.io/badge/python-3.9%20to%203.13-blue)
8
8
  ![license](https://img.shields.io/badge/license-Apache--2.0-green)
9
9
 
10
10
  ```console
11
- pip install microvm-ctl
11
+ pip install microvm-ctl # https://pypi.org/project/microvm-ctl/
12
12
 
13
13
  mvm bootstrap # one time: S3 artifact bucket + build/execution IAM roles
14
14
  mvm image build my-sandbox ./my-app # Dockerfile at ./my-app root -> runnable snapshot
@@ -18,6 +18,8 @@ mvm scale my-sandbox 10 # converge the fleet, throttled to yo
18
18
  mvm top --watch # live state table
19
19
  ```
20
20
 
21
+ To see it used for real first, jump to [the eight examples](#see-it-working-eight-examples-and-the-article-series).
22
+
21
23
  ## Why this exists
22
24
 
23
25
  Lambda MicroVMs exposes the primitive under Lambda itself: a Firecracker VM with a full AL2023 userland, a dedicated HTTPS endpoint, and a lifecycle you control (run, suspend, resume, terminate). The service deliberately stops there. There is no load balancer (one endpoint per VM), no fleet abstraction, no token management, no dashboard, and a fresh account runs quotas far below the published defaults. microvm-ctl fills that gap.
@@ -31,6 +33,8 @@ Lambda MicroVMs exposes the primitive under Lambda itself: a Firecracker VM with
31
33
  | In-VM hook runtime | zero-dependency server for `/ready`, `/validate`, `/run`, `/resume`, `/suspend`, `/terminate` plus your own routes | `microvm/hooks/server.py` |
32
34
  | Observability and cost | live fleet table, CloudWatch tail, a cost model that prices a session shape before you commit to it | `microvm/monitor.py` |
33
35
  | Account bootstrap | artifact bucket plus separate build and execution roles | `microvm/bootstrap.py` |
36
+ | Leases | `Lease`, `LeasePolicy`, `FleetManager.lease`: one task per VM, completed by the VM through a task token, callback id, HTTP, SQS, or EventBridge | `microvm/lease.py` |
37
+ | Orchestrator integrations | generated Step Functions state machine and IAM (`mvm lease asl`, `mvm lease policy`), `lease_microvm` for Lambda durable functions | `microvm/integrations/` |
34
38
 
35
39
  The `lambda-microvms` botocore service model ships inside the package, so the plane works on whatever boto3 you already have.
36
40
 
@@ -110,6 +114,14 @@ app.serve(port=8080)
110
114
 
111
115
  ![mvm cost](docs/img/mvm-cost.png)
112
116
 
117
+ ## The playground
118
+
119
+ `mvm playground` opens a local web app that drives everything above against the live service: build images, run and scale fleets, call VMs, tail logs, price sessions, run a parameterised benchmark, and watch every AWS API call the process makes in a trace. A dry-run switch turns each mutating action into a printout of the exact request it would send. See [docs/playground.md](docs/playground.md).
120
+
121
+ ```console
122
+ mvm playground # http://127.0.0.1:8765
123
+ ```
124
+
113
125
  ## Documentation
114
126
 
115
127
  - [Quickstart](docs/quickstart.md): from an empty account to a serving VM, with the environment variables explained.
@@ -118,11 +130,32 @@ app.serve(port=8080)
118
130
  - [Architecture](docs/architecture.md): the two planes, the lifecycle state machine, the fleet design decisions, and the security model.
119
131
  - [The hook contract](docs/hooks.md): what each hook is for and what breaks when you ignore it.
120
132
  - [Quotas and cost](docs/quotas-and-cost.md): the quota walls, what counts against them, and the cost model with worked examples.
121
- - [Troubleshooting](docs/troubleshooting.md): the errors we hit on the live service and what each one meant.
133
+ - [Troubleshooting](docs/troubleshooting.md): the errors I hit on the live service and what each one meant.
134
+ - [The playground](docs/playground.md): the local web app over the whole SDK, and how to host it.
135
+ - [Integrations](docs/integrations.md): the lease contract for handing a VM to Step Functions, Lambda durable functions, or your own orchestrator, and what the plane should own.
136
+
137
+ ## See it working: eight examples and the article series
138
+
139
+ The fastest way to understand the plane is to read the apps built on it. The companion repo [awesome-microvm](https://github.com/Vivek0712/awesome-microvm) holds eight production-shaped examples, each a Dockerfile plus a single-file app, deployed and recorded on the live service:
140
+
141
+ | Example | What it shows |
142
+ |---|---|
143
+ | [code-sandbox](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/code-sandbox) | untrusted or AI-written Python per session; state persists across calls and suspend |
144
+ | [ai-code-runner](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/ai-code-runner) | Bedrock writes code, the VM runs it, tracebacks drive a self-repair loop |
145
+ | [agent-eval](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/agent-eval) | `Fleet.scale_to` fan-out over byte-identical clones, scoreboard, drain |
146
+ | [notebook](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/notebook) | a kernel whose namespace survives suspend and resume with the same PID |
147
+ | [data-analytics](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/data-analytics) | DuckDB over S3 through the execution role; bulk data off the endpoint |
148
+ | [ci-runner](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/ci-runner) | clone, test, report, terminate; `--max-duration` as the runaway cap |
149
+ | [pdf-service](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/pdf-service) | untrusted HTML rendered in the VM; idle policy sleeps it between bursts |
150
+ | [multi-tenant-agents](https://github.com/Vivek0712/awesome-microvm/tree/main/examples/multi-tenant-agents) | one VM per tenant, identity via `runHookPayload`, `run_payload_factory` on a `Fleet` |
151
+
152
+ The three-part article series **Building on AWS Lambda MicroVMs** on the AWS Builder Center walks through them:
122
153
 
123
- ## Examples and the article series
154
+ 1. [Control and scale AWS Lambda MicroVMs with microvm-ctl](https://builder.aws.com/content/3JIDTpz0ZgatSBv24drra3gEod9/control-and-scale-aws-lambda-microvms-with-microvm-ctl): this plane and its measurements.
155
+ 2. [Seven workloads Lambda could never run, until MicroVMs](https://builder.aws.com/content/3JJ2oNWY9EsZzivMMx044cSlrFQ/seven-workloads-lambda-could-never-run-until-microvms): the first seven examples through build, run, cost, and gotchas.
156
+ 3. [A kernel for every customer: scaling AI agents to 1,000 tenants on AWS Lambda MicroVMs with microvm-ctl](https://builder.aws.com/content/3JJ7tASPSSUUTrcpWnWtxOuu8g3/a-kernel-for-every-customer-scaling-ai-agents-to-1000-tenants-on-aws-lambda-microvms-with-microvm-ctl): the multi-tenant finale with the decision guide.
124
157
 
125
- Eight production-shaped apps run on this plane: a code execution sandbox, an AI code runner with a self-repair loop, an agent evaluation fleet, a stateful notebook kernel, sandboxed DuckDB analytics, an ephemeral CI runner, an HTML to PDF service, and multi-tenant AI agents. Each has a Dockerfile, a single-file app, a recorded live transcript, and an article in the series **Building on AWS Lambda MicroVMs**. They live in the companion repo [awesome-microvm](https://github.com/Vivek0712/awesome-microvm).
158
+ The article sources and a long-form deep dive per example live under [awesome-microvm/blog](https://github.com/Vivek0712/awesome-microvm/tree/main/blog).
126
159
 
127
160
  ## Requirements and regions
128
161
 
@@ -130,7 +163,7 @@ Python 3.9 or newer on the machine running the plane. The VMs themselves are ARM
130
163
 
131
164
  ## Credits and inspiration
132
165
 
133
- This project grew out of [lambda-microvm-starter](https://github.com/vidanov/lambda-microvm-starter) by [Alexey Vidanov](https://github.com/vidanov), the one-command on-ramp that deploys any Dockerfile to a Lambda MicroVM behind a public CloudFront URL. His starter kit and its troubleshooting notes were the first working map of the service we had, and several of the gotchas documented here were first written down there. microvm-ctl takes the next step from one deployed app to fleets, tokens, quotas, and cost, and we are grateful for the ground he covered first.
166
+ This project grew out of [lambda-microvm-starter](https://github.com/vidanov/lambda-microvm-starter) by [Alexey Vidanov](https://github.com/vidanov), the one-command on-ramp that deploys any Dockerfile to a Lambda MicroVM behind a public CloudFront URL. His starter kit and its troubleshooting notes were the first working map of the service I had, and several of the gotchas documented here were first written down there. microvm-ctl takes the next step from one deployed app to fleets, tokens, quotas, and cost, and I am grateful for the ground he covered first.
134
167
 
135
168
  ## Contributing
136
169
 
@@ -9,9 +9,10 @@ from microvm.config import PlaneConfig
9
9
  from microvm.endpoint import EndpointClient, EndpointError
10
10
  from microvm.fleet import Fleet, FleetManager
11
11
  from microvm.images import ImageBuilder, ImageBuildError
12
+ from microvm.lease import Lease, LeasePolicy
12
13
  from microvm.monitor import CostModel, FleetMonitor
13
14
 
14
- __version__ = "0.1.0"
15
+ __version__ = "0.2.0"
15
16
 
16
17
  __all__ = [
17
18
  "microvm_client",
@@ -21,6 +22,8 @@ __all__ = [
21
22
  "ImageBuildError",
22
23
  "Fleet",
23
24
  "FleetManager",
25
+ "Lease",
26
+ "LeasePolicy",
24
27
  "EndpointClient",
25
28
  "EndpointError",
26
29
  "FleetMonitor",
@@ -74,7 +74,12 @@ def bootstrap(cfg: PlaneConfig, prefix: str = "microvm-ctl") -> dict:
74
74
  logs_stmt = {
75
75
  "Effect": "Allow",
76
76
  "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
77
- "Resource": f"arn:aws:logs:{cfg.region}:{account}:log-group:/aws/lambda/microvms/*",
77
+ # the service writes to /aws/lambda-microvms/<image>; the old prefix is kept
78
+ # so roles bootstrapped by earlier releases keep working
79
+ "Resource": [
80
+ f"arn:aws:logs:{cfg.region}:{account}:log-group:/aws/lambda-microvms/*",
81
+ f"arn:aws:logs:{cfg.region}:{account}:log-group:/aws/lambda/microvms/*",
82
+ ],
78
83
  }
79
84
  build_role = _ensure_role(
80
85
  iam,
@@ -10,9 +10,12 @@
10
10
  mvm suspend|resume|terminate ID... lifecycle control
11
11
  mvm drain IMAGE terminate the whole fleet
12
12
  mvm call ID /path [-X POST -d '{}'] authenticated request into the VM
13
+ mvm status ID | watch ID job telemetry from the hook runtime (/status, /events)
14
+ mvm lease asl|policy|run Step Functions ASL, IAM statements, one manual lease
13
15
  mvm top [--image NAME] [--watch] live fleet dashboard
14
16
  mvm logs IMAGE CloudWatch tail
15
17
  mvm cost [--memory-gb 2 ...] session economics
18
+ mvm playground [--port 8765] browser UI over everything above, live against AWS
16
19
  """
17
20
 
18
21
  from __future__ import annotations
@@ -253,11 +256,138 @@ def cmd_logs(args):
253
256
  mon = FleetMonitor(_cfg(args))
254
257
  events = mon.tail_logs(args.image, minutes=args.minutes)
255
258
  if not events:
256
- console.print(f"[dim]no events in /aws/lambda/microvms/{args.image}[/]")
259
+ console.print(f"[dim]no events in {' or '.join(mon.log_groups(args.image))}[/]")
257
260
  for e in events:
258
261
  console.print(f"[dim]{e['stream'][:20]}[/] {e['message']}")
259
262
 
260
263
 
264
+ # ---------------------------------------------------------------- job telemetry
265
+ def _snapshot_table(snap: dict, title: str) -> Table:
266
+ t = Table(title=title, header_style="bold magenta", show_header=False)
267
+ t.add_column("field", style="cyan")
268
+ t.add_column("value")
269
+ prog = snap.get("progress") or {}
270
+ done, total = prog.get("done"), prog.get("total")
271
+ t.add_row("phase", str(snap.get("phase") or "-"))
272
+ t.add_row("elapsed", f"{snap.get('elapsed_s', 0):.0f} s")
273
+ t.add_row("progress", f"{done}/{total}" if total else str(done if done is not None else "-"))
274
+ for k, v in sorted((snap.get("counters") or {}).items()):
275
+ t.add_row(f"counter {k}", str(v))
276
+ lease = snap.get("lease")
277
+ if lease:
278
+ state = "done" if lease.get("done") else ("lost" if lease.get("lost") else "active")
279
+ err = lease.get("error")
280
+ colour = "red" if err else "green"
281
+ t.add_row("lease", f"{lease.get('kind')} {lease.get('id') or ''} "
282
+ f"heartbeats={lease.get('heartbeats', 0)} [{colour}]{state}[/]")
283
+ if err:
284
+ t.add_row("error", f"[red]{err.get('error_type')}[/] {err.get('message')}")
285
+ else:
286
+ t.add_row("lease", "[dim]none[/]")
287
+ return t
288
+
289
+
290
+ def _log_line(entry: dict) -> str:
291
+ level = entry.get("level", "info")
292
+ style = {"error": "red", "warning": "yellow", "warn": "yellow"}.get(level, "dim")
293
+ extra = {k: v for k, v in entry.items() if k not in ("t", "level", "msg", "phase", "microvm_id", "seq")}
294
+ tail = f" [dim]{json.dumps(extra, default=str)}[/]" if extra else ""
295
+ return f"[{style}]{level:<7}[/] [cyan]{entry.get('phase') or ''}[/] {entry.get('msg', '')}{tail}"
296
+
297
+
298
+ def cmd_status(args):
299
+ client = EndpointClient(_cfg(args), args.id, ports=[args.port] if args.port else None)
300
+ snap = client.status()
301
+ console.print(_snapshot_table(snap, f"job on {args.id}"))
302
+ for entry in snap.get("log_tail") or []:
303
+ console.print(_log_line(entry) if isinstance(entry, dict) else str(entry))
304
+
305
+
306
+ def cmd_watch(args):
307
+ from rich.live import Live
308
+ client = EndpointClient(_cfg(args), args.id, ports=[args.port] if args.port else None)
309
+ snap = client.status()
310
+ with Live(_snapshot_table(snap, f"job on {args.id}"), refresh_per_second=2, console=console) as live:
311
+ for obj in client.watch(timeout=args.timeout):
312
+ if not isinstance(obj, dict):
313
+ continue
314
+ if "msg" in obj:
315
+ live.console.print(_log_line(obj))
316
+ else:
317
+ live.update(_snapshot_table(obj, f"job on {args.id}"))
318
+
319
+
320
+ # ---------------------------------------------------------------- leases
321
+ def _lease_policy(args):
322
+ from microvm.lease import LeasePolicy
323
+ return LeasePolicy(budget_s=args.budget, heartbeat_timeout_s=args.heartbeat, slack_s=args.slack)
324
+
325
+
326
+ def _image_arn_offline(image: str, region: str, role_arn: str | None, profile) -> str:
327
+ """Resolve a bare image name without calling STS when the account is readable
328
+ from the execution role ARN, so `mvm lease asl` works without credentials."""
329
+ from microvm.client import image_arn
330
+ if image.startswith("arn:"):
331
+ return image
332
+ parts = (role_arn or "").split(":")
333
+ if role_arn and role_arn.startswith("arn:") and len(parts) > 4 and parts[4]:
334
+ return f"arn:aws:lambda:{region}:{parts[4]}:microvm-image:{image}"
335
+ return image_arn(image, region, profile)
336
+
337
+
338
+ def cmd_lease_asl(args):
339
+ from microvm.integrations.stepfunctions import lease_state_machine
340
+ cfg = _cfg(args)
341
+ role = args.execution_role or cfg.execution_role_arn
342
+ if not role:
343
+ sys.exit("mvm lease asl: --execution-role ARN (or MVM_EXECUTION_ROLE_ARN) is required")
344
+ asl = lease_state_machine(
345
+ image_arn=_image_arn_offline(args.image, cfg.region, role, cfg.profile),
346
+ execution_role_arn=role, policy=_lease_policy(args), region=cfg.region,
347
+ name=args.name, task_expr=args.task_expr, heartbeat_s=args.heartbeat_every,
348
+ )
349
+ print(json.dumps(asl, indent=2))
350
+
351
+
352
+ def cmd_lease_policy(args):
353
+ from microvm.integrations.stepfunctions import iam_statements, orchestrator_statements, policy_document
354
+ cfg = _cfg(args)
355
+ out = {
356
+ "execution_role": policy_document(iam_statements(args.kind, orchestrator_arn=args.orchestrator)),
357
+ "orchestrator_role": policy_document(
358
+ orchestrator_statements(args.execution_role or cfg.execution_role_arn)),
359
+ }
360
+ print(json.dumps(out, indent=2))
361
+
362
+
363
+ def cmd_lease_run(args):
364
+ from microvm.lease import Lease
365
+ cfg = _cfg(args)
366
+ if args.kind != "none" and not args.token:
367
+ sys.exit(f"mvm lease run: --token is required for kind {args.kind}")
368
+ try:
369
+ task = json.loads(args.task) if args.task else {}
370
+ except ValueError as e:
371
+ sys.exit(f"mvm lease run: --task is not valid JSON: {e}")
372
+ lease = Lease(kind=args.kind, token=args.token or "", region=cfg.region, target=args.target,
373
+ heartbeat_s=args.heartbeat_every, id=args.id)
374
+ fm = FleetManager(cfg)
375
+ vm = fm.lease(args.image, lease, task, _lease_policy(args), version=args.version,
376
+ execution_role=args.execution_role)
377
+ console.print(
378
+ f"[bold green]✓[/] {vm.microvm_id} {_state(vm.state)} "
379
+ f"[link=https://{vm.endpoint}]{vm.endpoint}[/link] lease {lease.kind}"
380
+ )
381
+ if args.wait:
382
+ vm = fm.wait_until(vm.microvm_id, "RUNNING")
383
+ console.print(f" now {_state(vm.state)}; follow it with: mvm watch {vm.microvm_id}")
384
+
385
+
386
+ def cmd_playground(args):
387
+ from microvm.playground import serve
388
+ serve(host=args.host, port=args.port, dry_run=args.dry_run, open_browser=not args.no_open, cfg=_cfg(args))
389
+
390
+
261
391
  def cmd_cost(args):
262
392
  model = CostModel(memory_gb=args.memory_gb, snapshot_gb=args.snapshot_gb)
263
393
  s = model.session(args.active * 60, args.suspended * 60, args.cycles)
@@ -351,13 +481,63 @@ def main(argv: list[str] | None = None):
351
481
  c.add_argument("--port", type=int, default=None, help="non-default app port")
352
482
  c.set_defaults(fn=cmd_call)
353
483
 
484
+ st = sub.add_parser("status", help="job snapshot from the hook runtime (GET /status)")
485
+ st.add_argument("id")
486
+ st.add_argument("--port", type=int, default=None, help="non-default app port")
487
+ st.set_defaults(fn=cmd_status)
488
+
489
+ w = sub.add_parser("watch", help="live job table plus streamed log lines (GET /events)")
490
+ w.add_argument("id")
491
+ w.add_argument("--port", type=int, default=None, help="non-default app port")
492
+ w.add_argument("--timeout", type=float, default=600, help="stop after N seconds")
493
+ w.set_defaults(fn=cmd_watch)
494
+
495
+ le = sub.add_parser("lease", help="hand a VM a task through a lease").add_subparsers(
496
+ dest="sub", required=True)
497
+
498
+ def _policy_flags(sp):
499
+ sp.add_argument("--budget", type=int, default=900,
500
+ help="seconds the orchestrator waits (task timeout)")
501
+ sp.add_argument("--heartbeat", type=int, default=120, help="heartbeat timeout in seconds")
502
+ sp.add_argument("--slack", type=int, default=120, help="VM outlives the budget by this many seconds")
503
+ sp.add_argument("--heartbeat-every", type=int, default=30, help="seconds between VM heartbeats")
504
+
505
+ la = le.add_parser("asl", help="print the Step Functions state machine (JSONata) for one lease")
506
+ la.add_argument("--image", required=True, help="image name or ARN")
507
+ la.add_argument("--execution-role", default=None,
508
+ help="VM execution role ARN (default: MVM_EXECUTION_ROLE_ARN)")
509
+ la.add_argument("--name", default="Lease", help="name of the lease state")
510
+ la.add_argument("--task-expr", default="$states.input", help="JSONata expression for the task")
511
+ _policy_flags(la)
512
+ la.set_defaults(fn=cmd_lease_asl)
513
+
514
+ lp = le.add_parser("policy", help="print the IAM statements for the VM role and the orchestrator role")
515
+ lp.add_argument("--kind", required=True, choices=["sfn", "durable", "sqs", "eventbridge", "http"])
516
+ lp.add_argument("--orchestrator", default=None,
517
+ help="state machine, function, queue, or bus ARN the VM completes to")
518
+ lp.add_argument("--execution-role", default=None, help="VM execution role for iam:PassRole")
519
+ lp.set_defaults(fn=cmd_lease_policy)
520
+
521
+ lr = le.add_parser("run", help="launch one lease by hand (manual tests)")
522
+ lr.add_argument("image")
523
+ lr.add_argument("--kind", required=True, choices=["sfn", "durable", "http", "sqs", "eventbridge", "none"])
524
+ lr.add_argument("--token", default=None, help="task token, callback id, or bearer (not for kind none)")
525
+ lr.add_argument("--target", default=None, help="http URL, SQS queue URL, or event bus name")
526
+ lr.add_argument("--task", default=None, help="task JSON (pointers, not bodies; 4096 chars total)")
527
+ lr.add_argument("--id", default=None, help="human label carried as lease.id")
528
+ lr.add_argument("--version", help="image version (default: latest ACTIVE)")
529
+ lr.add_argument("--execution-role", default=None)
530
+ lr.add_argument("--wait", action="store_true", help="poll until RUNNING")
531
+ _policy_flags(lr)
532
+ lr.set_defaults(fn=cmd_lease_run)
533
+
354
534
  tp = sub.add_parser("top", help="live fleet dashboard")
355
535
  tp.add_argument("--image")
356
536
  tp.add_argument("--watch", action="store_true")
357
537
  tp.add_argument("--interval", type=float, default=3)
358
538
  tp.set_defaults(fn=cmd_top)
359
539
 
360
- lg = sub.add_parser("logs", help="tail CloudWatch logs for an image")
540
+ lg = sub.add_parser("logs", help="tail CloudWatch logs for an image (/aws/lambda-microvms/<image>)")
361
541
  lg.add_argument("image")
362
542
  lg.add_argument("--minutes", type=int, default=15)
363
543
  lg.set_defaults(fn=cmd_logs)
@@ -371,6 +551,14 @@ def main(argv: list[str] | None = None):
371
551
  co.add_argument("--cycles", type=int, default=1, help="suspend/resume cycles")
372
552
  co.set_defaults(fn=cmd_cost)
373
553
 
554
+ pgp = sub.add_parser("playground", help="local browser UI that drives the SDK against the live service")
555
+ pgp.add_argument("--host", default="127.0.0.1")
556
+ pgp.add_argument("--port", type=int, default=8765)
557
+ pgp.add_argument("--dry-run", action="store_true",
558
+ help="start with dry run on: mutating actions are recorded, not sent")
559
+ pgp.add_argument("--no-open", action="store_true", help="do not open a browser tab")
560
+ pgp.set_defaults(fn=cmd_playground)
561
+
374
562
  args = p.parse_args(argv)
375
563
  try:
376
564
  args.fn(args)
@@ -15,6 +15,7 @@ retries the two endpoint errors you must design for:
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ import json
18
19
  import random
19
20
  import time
20
21
 
@@ -127,6 +128,47 @@ class EndpointClient:
127
128
  time.sleep(1)
128
129
  raise EndpointError(f"{self.microvm_id} not serving {path} after {timeout}s")
129
130
 
131
+ # -- job telemetry (HookApp built-in /status and /events) -------------------
132
+ def status(self, since: int | None = None) -> dict:
133
+ """The hook runtime's job snapshot (`GET /status`): phase, progress, counters,
134
+ the log tail, and the lease state. `since` returns only log lines with a
135
+ sequence number above it."""
136
+ path = "/status" if since is None else f"/status?since={int(since)}"
137
+ resp = self.get(path, timeout=10)
138
+ if resp.status_code != 200:
139
+ raise EndpointError(f"{self.microvm_id} answered {resp.status_code} on {path}")
140
+ return resp.json()
141
+
142
+ def watch(self, timeout: float = 600):
143
+ """Stream `GET /events` (Server-Sent Events) and yield each parsed JSON object:
144
+ log lines as they happen plus a snapshot every few seconds. Stops when a
145
+ snapshot reports the lease done, on `timeout` seconds, or when the VM closes
146
+ the stream."""
147
+ deadline = time.time() + timeout
148
+ resp = self.request("GET", "/events", timeout=(10, 30), max_attempts=1, stream=True)
149
+ if resp.status_code != 200:
150
+ raise EndpointError(f"{self.microvm_id} answered {resp.status_code} on /events")
151
+ try:
152
+ for line in resp.iter_lines():
153
+ if time.time() > deadline:
154
+ return
155
+ if isinstance(line, bytes):
156
+ line = line.decode("utf-8", "replace")
157
+ if not line.startswith("data:"):
158
+ continue
159
+ try:
160
+ obj = json.loads(line[5:].strip())
161
+ except ValueError:
162
+ continue
163
+ yield obj
164
+ lease = obj.get("lease") if isinstance(obj, dict) else None
165
+ if isinstance(lease, dict) and lease.get("done"):
166
+ return
167
+ except requests.RequestException:
168
+ return
169
+ finally:
170
+ resp.close()
171
+
130
172
  def shell_token(self, minutes: int = 15) -> dict[str, str]:
131
173
  """Token for interactive shell access (VM must run with SHELL_INGRESS)."""
132
174
  return dict(
@@ -18,6 +18,7 @@ from typing import Callable
18
18
 
19
19
  from microvm.client import image_arn, microvm_client
20
20
  from microvm.config import TPS, PlaneConfig
21
+ from microvm.lease import Lease, LeasePolicy, client_token, encode_payload
21
22
  from microvm.throttle import Throttled
22
23
 
23
24
  ACTIVE_STATES = {"PENDING", "RUNNING", "SUSPENDING", "SUSPENDED"}
@@ -122,7 +123,7 @@ class FleetManager:
122
123
  return self.quotas.get("MaxMemoryGb")
123
124
 
124
125
  # -- single VM ---------------------------------------------------------------
125
- def run(
126
+ def run_params(
126
127
  self,
127
128
  image: str,
128
129
  *,
@@ -133,7 +134,14 @@ class FleetManager:
133
134
  ingress: list[str] | None = None,
134
135
  egress: list[str] | None = None,
135
136
  execution_role: str | None = None,
136
- ) -> Microvm:
137
+ client_token: str | None = None,
138
+ ) -> dict:
139
+ """The exact RunMicrovm request `run` would send, for inspection or dry runs.
140
+
141
+ `client_token` (1 to 128 chars) makes the launch idempotent on the service
142
+ side: a retried request with the same token returns the same microVM instead
143
+ of a second one. Use it whenever the caller may replay, such as a durable
144
+ function step."""
137
145
  params: dict = {"imageIdentifier": image_arn(image, self.cfg.region, self.cfg.profile)}
138
146
  if version:
139
147
  params["imageVersion"] = version
@@ -149,8 +157,52 @@ class FleetManager:
149
157
  role = execution_role or self.cfg.execution_role_arn
150
158
  if role:
151
159
  params["executionRoleArn"] = role
160
+ if client_token:
161
+ params["clientToken"] = client_token
162
+ return params
163
+
164
+ def run(
165
+ self,
166
+ image: str,
167
+ *,
168
+ version: str | None = None,
169
+ idle_policy: IdlePolicy | None = None,
170
+ run_payload: str | None = None,
171
+ max_duration: int | None = None,
172
+ ingress: list[str] | None = None,
173
+ egress: list[str] | None = None,
174
+ execution_role: str | None = None,
175
+ client_token: str | None = None,
176
+ ) -> Microvm:
177
+ params = self.run_params(
178
+ image, version=version, idle_policy=idle_policy, run_payload=run_payload,
179
+ max_duration=max_duration, ingress=ingress, egress=egress, execution_role=execution_role,
180
+ client_token=client_token,
181
+ )
152
182
  return Microvm.from_api(self._run(**params))
153
183
 
184
+ def lease(
185
+ self,
186
+ image: str,
187
+ lease: Lease,
188
+ task: dict,
189
+ policy: LeasePolicy | None = None,
190
+ *,
191
+ version: str | None = None,
192
+ execution_role: str | None = None,
193
+ ingress: list[str] | None = None,
194
+ egress: list[str] | None = None,
195
+ ) -> Microvm:
196
+ """RunMicrovm with the lease in runHookPayload, the policy's idle policy and duration cap,
197
+ and clientToken = client_token(lease). One call, idempotent on the token."""
198
+ policy = policy or LeasePolicy()
199
+ return self.run(
200
+ image, version=version, idle_policy=policy.idle_policy(),
201
+ run_payload=encode_payload(lease, task), max_duration=policy.max_duration(),
202
+ ingress=ingress, egress=egress, execution_role=execution_role,
203
+ client_token=client_token(lease),
204
+ )
205
+
154
206
  def get(self, microvm_id: str) -> Microvm:
155
207
  return Microvm.from_api(self.api.get_microvm(microvmIdentifier=microvm_id))
156
208