microvm-ctl 0.1.0__py3-none-any.whl

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.
microvm/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ """microvm-ctl: control and execution plane for AWS Lambda MicroVMs.
2
+
3
+ Control plane : build images, run/suspend/resume/terminate, scale fleets.
4
+ Execution plane: authenticated calls into the microVM endpoint, in-VM hook server.
5
+ """
6
+
7
+ from microvm.client import lambda_client, microvm_client
8
+ from microvm.config import PlaneConfig
9
+ from microvm.endpoint import EndpointClient, EndpointError
10
+ from microvm.fleet import Fleet, FleetManager
11
+ from microvm.images import ImageBuilder, ImageBuildError
12
+ from microvm.monitor import CostModel, FleetMonitor
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ __all__ = [
17
+ "microvm_client",
18
+ "lambda_client",
19
+ "PlaneConfig",
20
+ "ImageBuilder",
21
+ "ImageBuildError",
22
+ "Fleet",
23
+ "FleetManager",
24
+ "EndpointClient",
25
+ "EndpointError",
26
+ "FleetMonitor",
27
+ "CostModel",
28
+ ]
microvm/bootstrap.py ADDED
@@ -0,0 +1,99 @@
1
+ """One-time account bootstrap: artifact bucket + build/execution IAM roles.
2
+
3
+ Build role — assumed by the image build infrastructure: reads the code
4
+ artifact zip from S3 and writes build logs.
5
+ Execution — assumed by the running microVM (credentials are vended inside
6
+ role the VM): runtime logs plus whatever your workload needs.
7
+ Build vs execution role separation is deliberate — never let
8
+ the sandbox role read your artifact bucket.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import time
15
+
16
+ from microvm.client import lambda_client
17
+ from microvm.config import PlaneConfig
18
+
19
+ TRUST = {
20
+ "Version": "2012-10-17",
21
+ "Statement": [
22
+ {
23
+ "Effect": "Allow",
24
+ "Principal": {"Service": "lambda.amazonaws.com"},
25
+ "Action": ["sts:AssumeRole", "sts:TagSession"],
26
+ }
27
+ ],
28
+ }
29
+
30
+
31
+ def _ensure_role(iam, name: str, policy_doc: dict) -> str:
32
+ try:
33
+ arn = iam.get_role(RoleName=name)["Role"]["Arn"]
34
+ except iam.exceptions.NoSuchEntityException:
35
+ arn = iam.create_role(
36
+ RoleName=name,
37
+ AssumeRolePolicyDocument=json.dumps(TRUST),
38
+ Description="microvm-ctl control plane",
39
+ )["Role"]["Arn"]
40
+ time.sleep(8) # IAM eventual consistency before first use
41
+ iam.put_role_policy(
42
+ RoleName=name, PolicyName=f"{name}-inline", PolicyDocument=json.dumps(policy_doc)
43
+ )
44
+ return arn
45
+
46
+
47
+ def bootstrap(cfg: PlaneConfig, prefix: str = "microvm-ctl") -> dict:
48
+ """Create (idempotently) the artifact bucket and the two IAM roles.
49
+
50
+ Returns the three values the rest of the plane needs, ready to export as
51
+ MVM_ARTIFACT_BUCKET / MVM_BUILD_ROLE_ARN / MVM_EXECUTION_ROLE_ARN.
52
+ """
53
+ sts = lambda_client("sts", cfg.region, cfg.profile)
54
+ account = sts.get_caller_identity()["Account"]
55
+ bucket = cfg.artifact_bucket or f"{prefix}-artifacts-{account}-{cfg.region}"
56
+
57
+ s3 = lambda_client("s3", cfg.region, cfg.profile)
58
+ try:
59
+ s3.head_bucket(Bucket=bucket)
60
+ except Exception:
61
+ params = {"Bucket": bucket}
62
+ if cfg.region != "us-east-1":
63
+ params["CreateBucketConfiguration"] = {"LocationConstraint": cfg.region}
64
+ s3.create_bucket(**params)
65
+ s3.put_public_access_block(
66
+ Bucket=bucket,
67
+ PublicAccessBlockConfiguration={
68
+ "BlockPublicAcls": True, "IgnorePublicAcls": True,
69
+ "BlockPublicPolicy": True, "RestrictPublicBuckets": True,
70
+ },
71
+ )
72
+
73
+ iam = lambda_client("iam", cfg.region, cfg.profile)
74
+ logs_stmt = {
75
+ "Effect": "Allow",
76
+ "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
77
+ "Resource": f"arn:aws:logs:{cfg.region}:{account}:log-group:/aws/lambda/microvms/*",
78
+ }
79
+ build_role = _ensure_role(
80
+ iam,
81
+ f"{prefix}-build-role",
82
+ {
83
+ "Version": "2012-10-17",
84
+ "Statement": [
85
+ {"Effect": "Allow", "Action": "s3:GetObject", "Resource": f"arn:aws:s3:::{bucket}/*"},
86
+ logs_stmt,
87
+ ],
88
+ },
89
+ )
90
+ execution_role = _ensure_role(
91
+ iam,
92
+ f"{prefix}-execution-role",
93
+ {"Version": "2012-10-17", "Statement": [logs_stmt]},
94
+ )
95
+ return {
96
+ "artifact_bucket": bucket,
97
+ "build_role_arn": build_role,
98
+ "execution_role_arn": execution_role,
99
+ }
microvm/cli.py ADDED
@@ -0,0 +1,382 @@
1
+ """`mvm`: the microvm-ctl command line.
2
+
3
+ mvm bootstrap one-time: S3 artifact bucket + build/execution roles
4
+ mvm quotas applied vs published quotas for this account
5
+ mvm image build NAME DIR zip -> S3 -> build -> ACTIVE version
6
+ mvm image ls | versions NAME
7
+ mvm run IMAGE [-n 5] spin up microVM(s)
8
+ mvm ls [--image NAME] fleet listing
9
+ mvm scale IMAGE N converge fleet to N
10
+ mvm suspend|resume|terminate ID... lifecycle control
11
+ mvm drain IMAGE terminate the whole fleet
12
+ mvm call ID /path [-X POST -d '{}'] authenticated request into the VM
13
+ mvm top [--image NAME] [--watch] live fleet dashboard
14
+ mvm logs IMAGE CloudWatch tail
15
+ mvm cost [--memory-gb 2 ...] session economics
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import json
22
+ import sys
23
+ import time
24
+
25
+ from rich.console import Console
26
+ from rich.table import Table
27
+
28
+ from microvm import __version__
29
+ from microvm.config import TPS, PlaneConfig
30
+ from microvm.endpoint import EndpointClient
31
+ from microvm.fleet import Fleet, FleetManager, IdlePolicy, applied_quotas
32
+ from microvm.images import ImageBuilder
33
+ from microvm.monitor import CostModel, FleetMonitor
34
+
35
+ console = Console()
36
+
37
+ STATE_STYLE = {
38
+ "PENDING": "yellow",
39
+ "RUNNING": "bold green",
40
+ "SUSPENDING": "yellow",
41
+ "SUSPENDED": "cyan",
42
+ "TERMINATING": "red",
43
+ "TERMINATED": "dim",
44
+ }
45
+
46
+
47
+ def _cfg(args) -> PlaneConfig:
48
+ kw = {}
49
+ if args.region:
50
+ kw["region"] = args.region
51
+ if args.profile:
52
+ kw["profile"] = args.profile
53
+ return PlaneConfig(**kw)
54
+
55
+
56
+ def _state(s: str) -> str:
57
+ return f"[{STATE_STYLE.get(s, 'white')}]{s}[/]"
58
+
59
+
60
+ # ---------------------------------------------------------------- bootstrap
61
+ def cmd_bootstrap(args):
62
+ from microvm.bootstrap import bootstrap
63
+ out = bootstrap(_cfg(args), prefix=args.prefix)
64
+ console.print("[bold green]✓ control plane bootstrapped[/]")
65
+ for k, v in out.items():
66
+ console.print(f" [cyan]{k}[/] = {v}")
67
+ console.print(
68
+ "\nExport these (or put them in your shell profile):\n"
69
+ f" export MVM_ARTIFACT_BUCKET={out['artifact_bucket']}\n"
70
+ f" export MVM_BUILD_ROLE_ARN={out['build_role_arn']}\n"
71
+ f" export MVM_EXECUTION_ROLE_ARN={out['execution_role_arn']}"
72
+ )
73
+
74
+
75
+ def cmd_quotas(args):
76
+ cfg = _cfg(args)
77
+ applied = applied_quotas(cfg)
78
+ t = Table(title=f"Lambda MicroVMs quotas in {cfg.region}", header_style="bold magenta")
79
+ for c in ("quota", "published default", "applied to this account", "plane throttles at"):
80
+ t.add_column(c)
81
+ for op, default in TPS.items():
82
+ if op in ("GetMicrovm", "CreateMicrovmAuthToken"):
83
+ continue
84
+ got = applied.get(op)
85
+ eff = (got or default) * FleetManager.QUOTA_HEADROOM
86
+ t.add_row(f"{op} (TPS)", str(default), str(got) if got is not None else "[dim]unknown[/]",
87
+ f"{eff:g}/s")
88
+ mem = applied.get("MaxMemoryGb")
89
+ t.add_row("Max allocated microVM memory (GB)", "1024",
90
+ str(mem) if mem is not None else "[dim]unknown[/]", "-")
91
+ console.print(t)
92
+ if not applied:
93
+ console.print("[dim]Service Quotas did not answer (missing servicequotas:GetServiceQuota?); "
94
+ "showing published defaults.[/]")
95
+
96
+
97
+ # ---------------------------------------------------------------- images
98
+ def cmd_image_build(args):
99
+ cfg = _cfg(args)
100
+ builder = ImageBuilder(cfg)
101
+ env = dict(kv.split("=", 1) for kv in args.env) if args.env else None
102
+ with console.status(f"[bold]building image [cyan]{args.name}[/] from {args.dir}…"):
103
+ built = builder.build(
104
+ args.name,
105
+ args.dir,
106
+ memory_mib=args.memory,
107
+ environment=env,
108
+ os_capabilities_all=args.caps_all,
109
+ description=args.description,
110
+ )
111
+ console.print(f"[bold green]✓ {built.name}:{built.version}[/] ({built.build_seconds}s)")
112
+ if built.memory_snapshot_bytes:
113
+ console.print(
114
+ f" memory snapshot: {built.memory_snapshot_bytes/1e6:.0f} MB "
115
+ f"disk snapshot: {(built.disk_snapshot_bytes or 0)/1e6:.0f} MB"
116
+ )
117
+
118
+
119
+ def cmd_image_ls(args):
120
+ t = Table(title="MicroVM images", header_style="bold magenta")
121
+ for c in ("name", "state", "active version", "created"):
122
+ t.add_column(c)
123
+ for img in ImageBuilder(_cfg(args)).list_images():
124
+ t.add_row(
125
+ img["name"], img["state"], img.get("latestActiveImageVersion", "-"),
126
+ str(img["createdAt"])[:19],
127
+ )
128
+ console.print(t)
129
+
130
+
131
+ def cmd_image_versions(args):
132
+ t = Table(title=f"versions of {args.name}", header_style="bold magenta")
133
+ for c in ("version", "build state", "status", "created"):
134
+ t.add_column(c)
135
+ for v in ImageBuilder(_cfg(args)).list_versions(args.name):
136
+ t.add_row(v["imageVersion"], v["state"], v["status"], str(v["createdAt"])[:19])
137
+ console.print(t)
138
+
139
+
140
+ # ---------------------------------------------------------------- lifecycle
141
+ def cmd_run(args):
142
+ cfg = _cfg(args)
143
+ fm = FleetManager(cfg)
144
+ policy = IdlePolicy(max_idle=args.idle, suspended_for=args.suspended_ttl)
145
+ for _ in range(args.count):
146
+ vm = fm.run(
147
+ args.image,
148
+ version=args.version,
149
+ idle_policy=policy,
150
+ run_payload=args.payload,
151
+ max_duration=args.max_duration,
152
+ )
153
+ console.print(
154
+ f"[bold green]✓[/] {vm.microvm_id} {_state(vm.state)} "
155
+ f"[link=https://{vm.endpoint}]{vm.endpoint}[/link]"
156
+ )
157
+ if args.wait:
158
+ vm = fm.wait_until(vm.microvm_id, "RUNNING")
159
+ console.print(f" now {_state(vm.state)}")
160
+
161
+
162
+ def cmd_ls(args):
163
+ fm = FleetManager(_cfg(args))
164
+ vms = fm.list(args.image)
165
+ if not args.all:
166
+ vms = [v for v in vms if v.state != "TERMINATED"]
167
+ t = Table(title="microVMs", header_style="bold magenta")
168
+ for c in ("microvm id", "state", "image", "version", "started"):
169
+ t.add_column(c)
170
+ for vm in vms:
171
+ t.add_row(
172
+ vm.microvm_id, _state(vm.state),
173
+ vm.image_arn.split(":")[-1], vm.image_version, str(vm.started_at)[:19],
174
+ )
175
+ console.print(t)
176
+
177
+
178
+ def cmd_get(args):
179
+ vm = FleetManager(_cfg(args)).api.get_microvm(microvmIdentifier=args.id)
180
+ vm.pop("ResponseMetadata", None)
181
+ console.print_json(json.dumps(vm, default=str))
182
+
183
+
184
+ def _lifecycle(args, verb: str):
185
+ fm = FleetManager(_cfg(args))
186
+ for vid in args.ids:
187
+ getattr(fm, verb)(vid)
188
+ console.print(f"[bold green]✓[/] {verb} {vid}")
189
+
190
+
191
+ def cmd_scale(args):
192
+ cfg = _cfg(args)
193
+ fleet = Fleet(FleetManager(cfg), args.image, idle_policy=IdlePolicy(max_idle=args.idle))
194
+ before = fleet.size()
195
+ with console.status(f"[bold]scaling [cyan]{args.image}[/] {before} → {args.n}…"):
196
+ fleet.scale_to(args.n, wait_running=args.wait)
197
+ console.print(f"[bold green]✓[/] fleet [cyan]{args.image}[/]: {before} → {fleet.size()} microVMs")
198
+
199
+
200
+ def cmd_drain(args):
201
+ n = Fleet(FleetManager(_cfg(args)), args.image).drain()
202
+ console.print(f"[bold green]✓[/] terminated {n} microVMs of [cyan]{args.image}[/]")
203
+
204
+
205
+ # ---------------------------------------------------------------- execution plane
206
+ def cmd_call(args):
207
+ cfg = _cfg(args)
208
+ client = EndpointClient(cfg, args.id, ports=[args.port] if args.port else None)
209
+ resp = client.request(
210
+ args.method, args.path,
211
+ port=args.port,
212
+ data=args.data.encode() if args.data else None,
213
+ headers={"Content-Type": "application/json"} if args.data else {},
214
+ )
215
+ console.print(f"[bold]{resp.status_code}[/] in {resp.elapsed.total_seconds()*1000:.0f} ms")
216
+ try:
217
+ console.print_json(json.dumps(resp.json()))
218
+ except ValueError:
219
+ console.print(resp.text[:2000])
220
+
221
+
222
+ # ---------------------------------------------------------------- monitor
223
+ def _top_table(mon: FleetMonitor, image: str | None) -> Table:
224
+ snap = mon.snapshot(image)
225
+ counts = " ".join(f"{_state(s)}×{n}" for s, n in sorted(snap["by_state"].items()))
226
+ t = Table(
227
+ title=f"fleet: {image or 'all images'} — {snap['total']} VMs {counts}",
228
+ header_style="bold magenta",
229
+ )
230
+ for c in ("microvm id", "state", "image", "started"):
231
+ t.add_column(c)
232
+ for vm in snap["members"]:
233
+ t.add_row(
234
+ vm["microvmId"], _state(vm["state"]),
235
+ vm["imageArn"].split(":")[-1], str(vm["startedAt"])[:19],
236
+ )
237
+ return t
238
+
239
+
240
+ def cmd_top(args):
241
+ mon = FleetMonitor(_cfg(args))
242
+ if not args.watch:
243
+ console.print(_top_table(mon, args.image))
244
+ return
245
+ from rich.live import Live
246
+ with Live(_top_table(mon, args.image), refresh_per_second=0.5, console=console) as live:
247
+ while True:
248
+ time.sleep(args.interval)
249
+ live.update(_top_table(mon, args.image))
250
+
251
+
252
+ def cmd_logs(args):
253
+ mon = FleetMonitor(_cfg(args))
254
+ events = mon.tail_logs(args.image, minutes=args.minutes)
255
+ if not events:
256
+ console.print(f"[dim]no events in /aws/lambda/microvms/{args.image}[/]")
257
+ for e in events:
258
+ console.print(f"[dim]{e['stream'][:20]}[/] {e['message']}")
259
+
260
+
261
+ def cmd_cost(args):
262
+ model = CostModel(memory_gb=args.memory_gb, snapshot_gb=args.snapshot_gb)
263
+ s = model.session(args.active * 60, args.suspended * 60, args.cycles)
264
+ t = Table(title=f"session economics: {args.memory_gb} GB / {model.vcpu:g} vCPU",
265
+ header_style="bold magenta")
266
+ t.add_column("dimension")
267
+ t.add_column("USD", justify="right")
268
+ t.add_row("running compute", f"${s['running_usd']:.6f}")
269
+ t.add_row(f"suspend cycles x{args.cycles}", f"${s['suspend_cycles_usd']:.6f}")
270
+ t.add_row("suspended storage", f"${s['suspended_storage_usd']:.6f}")
271
+ t.add_row("[bold]total[/]", f"[bold]${s['total_usd']:.6f}[/]")
272
+ t.add_row("[dim]always-on equivalent[/]", f"[dim]${s['vs_always_on_usd']:.6f}[/]")
273
+ t.add_row("[bold green]savings[/]", f"[bold green]{s['savings_pct']}%[/]")
274
+ console.print(t)
275
+
276
+
277
+ # ---------------------------------------------------------------- parser
278
+ def main(argv: list[str] | None = None):
279
+ p = argparse.ArgumentParser(prog="mvm",
280
+ description="Control and execution plane for AWS Lambda MicroVMs")
281
+ p.add_argument("--version", action="version", version=f"microvm-ctl {__version__}")
282
+ p.add_argument("--region", default=None, help="service region (default: MVM_REGION or us-east-1)")
283
+ p.add_argument("--profile", default=None, help="AWS profile (default: MVM_PROFILE or AWS_PROFILE)")
284
+ sub = p.add_subparsers(dest="cmd", required=True)
285
+
286
+ b = sub.add_parser("bootstrap", help="provision S3 bucket + IAM roles")
287
+ b.add_argument("--prefix", default="microvm-ctl", help="name prefix for the bucket and roles")
288
+ b.set_defaults(fn=cmd_bootstrap)
289
+
290
+ q = sub.add_parser("quotas", help="applied vs published quotas for this account")
291
+ q.set_defaults(fn=cmd_quotas)
292
+
293
+ img = sub.add_parser("image", help="image factory").add_subparsers(dest="sub", required=True)
294
+ ib = img.add_parser("build", help="zip -> S3 -> build -> ACTIVE version")
295
+ ib.add_argument("name")
296
+ ib.add_argument("dir")
297
+ ib.add_argument("--memory", type=int, default=2048, help="minimum memory in MiB")
298
+ ib.add_argument("--env", action="append", metavar="K=V",
299
+ help="image-level env var (shared by every clone)")
300
+ ib.add_argument("--caps-all", action="store_true",
301
+ help="additionalOsCapabilities=ALL (EFS/FUSE/containerd/eBPF)")
302
+ ib.add_argument("--description")
303
+ ib.set_defaults(fn=cmd_image_build)
304
+ il = img.add_parser("ls", help="list images")
305
+ il.set_defaults(fn=cmd_image_ls)
306
+ iv = img.add_parser("versions", help="list versions of one image")
307
+ iv.add_argument("name")
308
+ iv.set_defaults(fn=cmd_image_versions)
309
+
310
+ r = sub.add_parser("run", help="spin up microVM(s)")
311
+ r.add_argument("image")
312
+ r.add_argument("--version", help="image version (default: latest ACTIVE)")
313
+ r.add_argument("-n", "--count", type=int, default=1)
314
+ r.add_argument("--idle", type=int, default=300, help="suspend after N idle seconds")
315
+ r.add_argument("--suspended-ttl", type=int, default=3600, help="auto-terminate after N suspended seconds")
316
+ r.add_argument("--payload", help="runHookPayload (per-VM context)")
317
+ r.add_argument("--max-duration", type=int, default=None, help="hard lifetime cap in seconds")
318
+ r.add_argument("--wait", action="store_true", help="poll until RUNNING")
319
+ r.set_defaults(fn=cmd_run)
320
+
321
+ ls = sub.add_parser("ls", help="list microVMs")
322
+ ls.add_argument("--image")
323
+ ls.add_argument("--all", action="store_true", help="include TERMINATED")
324
+ ls.set_defaults(fn=cmd_ls)
325
+
326
+ g = sub.add_parser("get", help="raw GetMicrovm JSON")
327
+ g.add_argument("id")
328
+ g.set_defaults(fn=cmd_get)
329
+
330
+ for verb in ("suspend", "resume", "terminate"):
331
+ v = sub.add_parser(verb, help=f"{verb} one or more microVMs")
332
+ v.add_argument("ids", nargs="+")
333
+ v.set_defaults(fn=lambda a, _v=verb: _lifecycle(a, _v))
334
+
335
+ sc = sub.add_parser("scale", help="converge fleet to N")
336
+ sc.add_argument("image")
337
+ sc.add_argument("n", type=int)
338
+ sc.add_argument("--idle", type=int, default=300)
339
+ sc.add_argument("--wait", action="store_true", help="wait for new members to reach RUNNING")
340
+ sc.set_defaults(fn=cmd_scale)
341
+
342
+ d = sub.add_parser("drain", help="terminate every microVM of an image")
343
+ d.add_argument("image")
344
+ d.set_defaults(fn=cmd_drain)
345
+
346
+ c = sub.add_parser("call", help="authenticated request into a microVM")
347
+ c.add_argument("id")
348
+ c.add_argument("path")
349
+ c.add_argument("-X", "--method", default="GET")
350
+ c.add_argument("-d", "--data", help="request body (sent as application/json)")
351
+ c.add_argument("--port", type=int, default=None, help="non-default app port")
352
+ c.set_defaults(fn=cmd_call)
353
+
354
+ tp = sub.add_parser("top", help="live fleet dashboard")
355
+ tp.add_argument("--image")
356
+ tp.add_argument("--watch", action="store_true")
357
+ tp.add_argument("--interval", type=float, default=3)
358
+ tp.set_defaults(fn=cmd_top)
359
+
360
+ lg = sub.add_parser("logs", help="tail CloudWatch logs for an image")
361
+ lg.add_argument("image")
362
+ lg.add_argument("--minutes", type=int, default=15)
363
+ lg.set_defaults(fn=cmd_logs)
364
+
365
+ co = sub.add_parser("cost", help="session economics")
366
+ co.add_argument("--memory-gb", type=float, default=2)
367
+ co.add_argument("--snapshot-gb", type=float, default=None,
368
+ help="suspended snapshot size (default: memory)")
369
+ co.add_argument("--active", type=float, default=30, help="active minutes")
370
+ co.add_argument("--suspended", type=float, default=480, help="suspended minutes")
371
+ co.add_argument("--cycles", type=int, default=1, help="suspend/resume cycles")
372
+ co.set_defaults(fn=cmd_cost)
373
+
374
+ args = p.parse_args(argv)
375
+ try:
376
+ args.fn(args)
377
+ except KeyboardInterrupt:
378
+ sys.exit(130)
379
+
380
+
381
+ if __name__ == "__main__":
382
+ main()
microvm/client.py ADDED
@@ -0,0 +1,60 @@
1
+ """boto3 client factory with a vendored `lambda-microvms` service model.
2
+
3
+ The service model ships inside this package (microvm/data), so the control
4
+ plane works even on boto3/botocore versions that predate the service —
5
+ botocore's loader picks up our data directory first.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import threading
11
+ from pathlib import Path
12
+
13
+ import boto3
14
+ import botocore.session
15
+
16
+ _DATA_DIR = str(Path(__file__).parent / "data")
17
+ _lock = threading.Lock()
18
+ _sessions: dict[tuple[str | None, str], boto3.Session] = {}
19
+
20
+
21
+ def _session(profile: str | None, region: str) -> boto3.Session:
22
+ """One boto3 Session per (profile, region), with our model dir on the loader path."""
23
+ key = (profile, region)
24
+ with _lock:
25
+ if key not in _sessions:
26
+ bc = botocore.session.Session()
27
+ loader = bc.get_component("data_loader")
28
+ if _DATA_DIR not in loader.search_paths:
29
+ loader.search_paths.insert(0, _DATA_DIR)
30
+ _sessions[key] = boto3.Session(
31
+ profile_name=profile, region_name=region, botocore_session=bc
32
+ )
33
+ return _sessions[key]
34
+
35
+
36
+ def microvm_client(region: str = "us-east-1", profile: str | None = None):
37
+ """A `lambda-microvms` client (RunMicrovm, SuspendMicrovm, images, tokens…)."""
38
+ return _session(profile, region).client("lambda-microvms")
39
+
40
+
41
+ def lambda_client(service: str, region: str = "us-east-1", profile: str | None = None):
42
+ """Any other client (s3, iam, logs, cloudwatch…) on the same session/credentials."""
43
+ return _session(profile, region).client(service)
44
+
45
+
46
+ _accounts: dict[tuple[str | None, str], str] = {}
47
+
48
+
49
+ def account_id(region: str = "us-east-1", profile: str | None = None) -> str:
50
+ key = (profile, region)
51
+ if key not in _accounts:
52
+ _accounts[key] = _session(profile, region).client("sts").get_caller_identity()["Account"]
53
+ return _accounts[key]
54
+
55
+
56
+ def image_arn(name_or_arn: str, region: str, profile: str | None = None) -> str:
57
+ """The API's imageIdentifier params want full ARNs; accept a bare name anywhere."""
58
+ if name_or_arn.startswith("arn:"):
59
+ return name_or_arn
60
+ return f"arn:aws:lambda:{region}:{account_id(region, profile)}:microvm-image:{name_or_arn}"
microvm/config.py ADDED
@@ -0,0 +1,81 @@
1
+ """Shared configuration for the control plane.
2
+
3
+ Lambda MicroVMs is available in: us-east-1, us-east-2, us-west-2,
4
+ eu-west-1, ap-northeast-1. All microVMs are ARM64 (Graviton).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from dataclasses import dataclass, field
11
+
12
+ SUPPORTED_REGIONS = ("us-east-1", "us-east-2", "us-west-2", "eu-west-1", "ap-northeast-1")
13
+
14
+ # AWS-managed network connector ARNs (account-agnostic, per region).
15
+ INGRESS_ALL = "arn:aws:lambda:{region}:aws:network-connector:aws-network-connector:ALL_INGRESS"
16
+ INGRESS_NONE = "arn:aws:lambda:{region}:aws:network-connector:aws-network-connector:NO_INGRESS"
17
+ INGRESS_SHELL = "arn:aws:lambda:{region}:aws:network-connector:aws-network-connector:SHELL_INGRESS"
18
+ EGRESS_INTERNET = "arn:aws:lambda:{region}:aws:network-connector:aws-network-connector:INTERNET_EGRESS"
19
+
20
+ # Lambda-managed base image every microVM image builds on.
21
+ MANAGED_BASE_IMAGE = "arn:aws:lambda:{region}:aws:microvm-image:al2023-1"
22
+
23
+ # Reserved proxy headers on the microVM endpoint.
24
+ AUTH_HEADER = "X-aws-proxy-auth"
25
+ PORT_HEADER = "X-aws-proxy-port"
26
+ FORCE_H2_HEADER = "X-aws-proxy-force-h2"
27
+
28
+ # In-VM runtime hook base path (served BY your app, called by Lambda).
29
+ HOOK_BASE_PATH = "/aws/lambda-microvms/runtime/v1"
30
+
31
+ # Published TPS quotas the control plane throttles around (soft, raisable).
32
+ TPS = {
33
+ "RunMicrovm": 5,
34
+ "SuspendMicrovm": 2,
35
+ "ResumeMicrovm": 5,
36
+ "TerminateMicrovm": 10,
37
+ "GetMicrovm": 100,
38
+ "CreateMicrovmAuthToken": 50,
39
+ }
40
+
41
+ MAX_DURATION_SECONDS = 28_800 # 8 h hard ceiling across RUNNING + SUSPENDED
42
+
43
+
44
+ @dataclass
45
+ class PlaneConfig:
46
+ """Everything the control plane needs to know about one deployment."""
47
+
48
+ region: str = field(default_factory=lambda: os.environ.get("MVM_REGION", "us-east-1"))
49
+ profile: str | None = field(
50
+ default_factory=lambda: os.environ.get("MVM_PROFILE") or os.environ.get("AWS_PROFILE")
51
+ )
52
+ artifact_bucket: str | None = field(default_factory=lambda: os.environ.get("MVM_ARTIFACT_BUCKET"))
53
+ build_role_arn: str | None = field(default_factory=lambda: os.environ.get("MVM_BUILD_ROLE_ARN"))
54
+ execution_role_arn: str | None = field(default_factory=lambda: os.environ.get("MVM_EXECUTION_ROLE_ARN"))
55
+
56
+ def __post_init__(self) -> None:
57
+ if self.region not in SUPPORTED_REGIONS:
58
+ raise ValueError(
59
+ f"Lambda MicroVMs is not available in {self.region}; pick one of {SUPPORTED_REGIONS}"
60
+ )
61
+
62
+ # Convenience per-region ARNs -------------------------------------------------
63
+ @property
64
+ def base_image_arn(self) -> str:
65
+ return MANAGED_BASE_IMAGE.format(region=self.region)
66
+
67
+ @property
68
+ def ingress_all(self) -> str:
69
+ return INGRESS_ALL.format(region=self.region)
70
+
71
+ @property
72
+ def ingress_none(self) -> str:
73
+ return INGRESS_NONE.format(region=self.region)
74
+
75
+ @property
76
+ def ingress_shell(self) -> str:
77
+ return INGRESS_SHELL.format(region=self.region)
78
+
79
+ @property
80
+ def egress_internet(self) -> str:
81
+ return EGRESS_INTERNET.format(region=self.region)