Skip to content

Commit 0b84fe0

Browse files
feat(serverless-init): add MicroVM CloudService implementation (#53093)
Adds `cloudservice.MicroVM`, a `CloudService` implementation for AWS Lambda MicroVMs, and a `LifecycleContext` type carried on `TracingContext.LifecycleCtx`: - `GetTags` parses `DD_AWS_MICROVM_IMAGE_ARN` for `region`/`account_id`/`image_name` (falling back to `"unknown"` for any field it can't parse). - `GetEnhancedMetricTags` derives base/usage tag sets from those tags. - `Init` reads a `LifecycleContext` (metric/log flushers, trace-tag/log-tag setters, flush timeout, sidecar flag) and constructs + starts the lifecycle hook server. - `Run` spawns the user process via `mode.RunInit`, binding `ProcessHooks.OnAlive`/`OnDead` to the lifecycle server's child so its `/ready` check reflects real liveness. Sidecar mode is fatal for MicroVM — there's no child process to track, so `/ready` would silently return 503 forever instead of surfacing the misconfiguration. - `Shutdown` stops the lifecycle server within a bounded timeout so in-flight `/suspend`/`/terminate` requests can complete before the metric/trace agents tear down. - MicroVM supports both amd64 and arm64 (every other cloud service here is amd64-only). `MicroVM` is a complete, self-contained type in this PR but is **not yet reachable**: `GetCloudServiceType` still doesn't know about it, so nothing in the running agent changes yet. This is the core piece of the MicroVM integration: everything needed to answer the lifecycle server's HTTP hooks and track the user process's liveness, in one type that satisfies `CloudService` end-to-end. Landing it as its own PR — before wiring it into `main.go` or registering it in `GetCloudServiceType` (PR 5) — lets it be reviewed as a unit without also having to reason about the wiring changes, and avoids any intermediate state where `MicroVM` could be selected by `GetCloudServiceType` before `main.go` populates its `LifecycleContext` (which would leave `Init` a no-op and `Run` dereferencing a nil child). This is PR 2 of 5 in the split of #53036 (`tianning.li/microvm-07-microvm-service-wiring`). See PR 1 (#53092) for the full stack list. ``` dda inv test --targets=./cmd/serverless-init/... ``` All 242 tests pass (3 platform-skips) — this PR adds no new tests of its own; `MicroVM`'s tests land in PRs 3 and 4 so each stays under ~300 lines. Existing tests confirm nothing else regresses with the new type and field present but unused. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent dc00020 commit 0b84fe0

9 files changed

Lines changed: 933 additions & 12 deletions

File tree

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016-present Datadog, Inc.
5+
6+
package cloudservice
7+
8+
import (
9+
"context"
10+
"maps"
11+
"os"
12+
"runtime"
13+
"strings"
14+
"time"
15+
16+
"log"
17+
18+
"github.com/DataDog/datadog-agent/cmd/serverless-init/lifecycle"
19+
serverlessInitLog "github.com/DataDog/datadog-agent/cmd/serverless-init/log"
20+
"github.com/DataDog/datadog-agent/cmd/serverless-init/mode"
21+
"github.com/DataDog/datadog-agent/pkg/metrics"
22+
serverlessenv "github.com/DataDog/datadog-agent/pkg/serverless/env"
23+
serverlessMetrics "github.com/DataDog/datadog-agent/pkg/serverless/metrics"
24+
)
25+
26+
const (
27+
// MicroVM's resource_type tag value
28+
MicroVMResourceType = "lambdamicrovm"
29+
30+
// MicroVMOrigin origin tag value
31+
MicroVMOrigin = MicroVMResourceType
32+
33+
// MicroVM resource_provider tag value
34+
MicroVMResourceProvider = "aws"
35+
36+
// Microvm metric prefix
37+
MicroVMPrefix = "aws.lambda.microvm."
38+
39+
// MicroVm usage metric name suffix
40+
MicroVMUsageMetricSuffix = "instance"
41+
)
42+
43+
// LifecycleContext carries the telemetry dependencies needed by MicroVM.Init to
44+
// construct and start the lifecycle hook server. Populated by main.go before
45+
// calling CloudService.Init; nil (and ignored) for all non-MicroVM services.
46+
type LifecycleContext struct {
47+
MetricFlusher lifecycle.Flusher
48+
LogsFlusher lifecycle.LogsFlusher
49+
MetricEmitter lifecycle.MetricEmitter
50+
SampleDrainer lifecycle.SampleDrainer
51+
FlushTimeout time.Duration
52+
SidecarMode bool
53+
LogsTagSetter lifecycle.LogsTagSetter // nil-safe; applied via server.SetLogsTagSetter after /run
54+
BaseTags []string // startup log tag snapshot passed alongside LogsTagSetter
55+
TraceTagSetter lifecycle.TraceTagSetter // nil-safe; applied via server.SetTraceTagSetter after /run
56+
BaseTraceTags map[string]string // startup trace tag snapshot passed alongside TraceTagSetter
57+
}
58+
59+
// MicroVM implements CloudService for AWS Lambda MicroVMs.
60+
type MicroVM struct {
61+
server *lifecycle.Server
62+
child *lifecycle.Child
63+
flushTimeout time.Duration
64+
}
65+
66+
// GetTags returns MicroVM-specific tags parsed from the image ARN env var.
67+
func (m *MicroVM) GetTags() map[string]string {
68+
tags := map[string]string{
69+
"origin": MicroVMOrigin,
70+
"_dd.origin": MicroVMOrigin,
71+
"resource_type": MicroVMResourceType,
72+
"resource_provider": MicroVMResourceProvider,
73+
}
74+
75+
arn := os.Getenv(serverlessenv.MicroVMImageARNEnvVar)
76+
if arn == "" {
77+
tags["region"] = "unknown"
78+
tags["account_id"] = "unknown"
79+
tags["image_name"] = "unknown"
80+
tags["resource_id"] = "unknown"
81+
return tags
82+
}
83+
84+
region, accountID, imageName := parseMicroVMARN(arn)
85+
tags["region"] = region
86+
tags["account_id"] = accountID
87+
tags["image_name"] = imageName
88+
tags["resource_id"] = arn
89+
90+
return tags
91+
}
92+
93+
// GetEnhancedMetricTags returns base (low-cardinality) and usage tags.
94+
// instance_id is absent from Usage tags at startup because the MicroVM ID is
95+
// not known until the /run lifecycle hook fires.
96+
func (m *MicroVM) GetEnhancedMetricTags(tags map[string]string) EnhancedMetricTags {
97+
baseTags := map[string]string{
98+
"account_id": tagValueOrUnknown(tags["account_id"]),
99+
"image_name": tagValueOrUnknown(tags["image_name"]),
100+
"origin": tagValueOrUnknown(tags["origin"]),
101+
"region": tagValueOrUnknown(tags["region"]),
102+
"resource_type": tagValueOrUnknown(tags["resource_type"]),
103+
"resource_provider": tagValueOrUnknown(tags["resource_provider"]),
104+
"resource_id": tagValueOrUnknown(tags["resource_id"]),
105+
}
106+
return EnhancedMetricTags{Base: baseTags, Usage: maps.Clone(baseTags)}
107+
}
108+
109+
// GetDefaultLogsSource returns the default logs source.
110+
func (m *MicroVM) GetDefaultLogsSource() string { return MicroVMOrigin }
111+
112+
// GetMetricPrefix returns the AWS MicroVM metric prefix.
113+
func (m *MicroVM) GetMetricPrefix() string { return MicroVMPrefix }
114+
115+
// GetUsageMetricSuffix returns the usage metric suffix.
116+
func (m *MicroVM) GetUsageMetricSuffix() string { return MicroVMUsageMetricSuffix }
117+
118+
// GetOrigin returns the origin tag value.
119+
func (m *MicroVM) GetOrigin() string { return MicroVMOrigin }
120+
121+
// GetSource returns the metrics source.
122+
func (m *MicroVM) GetSource() metrics.MetricSource {
123+
return metrics.MetricSourceAWSMicroVMEnhanced
124+
}
125+
126+
// isSupportedArch reports whether arch is supported by MicroVM.
127+
// MicroVM supports both amd64 and arm64; all other cloud services are amd64-only.
128+
func isSupportedArch(arch string) bool {
129+
return arch == archAMD64 || arch == archARM64
130+
}
131+
132+
// Init starts the MicroVM lifecycle hook server.
133+
func (m *MicroVM) Init(ctx *TracingContext) error {
134+
if arch := runtime.GOARCH; !isSupportedArch(arch) {
135+
log.Fatalf(unsupportedArchMsg, arch)
136+
}
137+
if ctx == nil || ctx.LifecycleCtx == nil {
138+
return nil
139+
}
140+
lc := ctx.LifecycleCtx
141+
m.flushTimeout = lc.FlushTimeout
142+
143+
components, err := lifecycle.SetupFromEnv(lc.SidecarMode)
144+
if err != nil {
145+
log.Printf("Invalid lifecycle env-var config (%v); starting with defaults", err)
146+
components = lifecycle.SetupFallback(lc.SidecarMode)
147+
}
148+
m.child = components.Child
149+
150+
arn := os.Getenv(serverlessenv.MicroVMImageARNEnvVar)
151+
if arn == "" {
152+
arn = "unknown"
153+
}
154+
heartbeat := lifecycle.NewHeartbeat(
155+
lifecycle.DefaultHeartbeatInterval,
156+
lc.MetricEmitter,
157+
m.GetSource(),
158+
[]string{"microvm_image_arn:" + arn},
159+
)
160+
m.server = lifecycle.NewServer(
161+
components.Port,
162+
lc.MetricFlusher,
163+
ctx.TraceAgent, // satisfies lifecycle.Flusher via TraceAgent.Flush()
164+
lc.LogsFlusher,
165+
lc.MetricEmitter,
166+
lc.SampleDrainer,
167+
m.GetSource(),
168+
lc.FlushTimeout,
169+
components.Handle,
170+
components.Forwarder,
171+
heartbeat,
172+
)
173+
if lc.LogsTagSetter != nil {
174+
m.server.SetLogsTagSetter(lc.LogsTagSetter, lc.BaseTags)
175+
}
176+
if lc.TraceTagSetter != nil {
177+
m.server.SetTraceTagSetter(lc.TraceTagSetter, lc.BaseTraceTags)
178+
}
179+
l, err := m.server.Listen()
180+
if err != nil {
181+
log.Fatalf("MicroVM lifecycle server failed to bind: %v", err)
182+
}
183+
go m.server.Serve(l)
184+
return nil
185+
}
186+
187+
// Child returns the *lifecycle.Child that mode.RunInit uses for /ready
188+
// alive-checking. Nil in sidecar mode or when Init has not been called.
189+
func (m *MicroVM) Child() *lifecycle.Child { return m.child }
190+
191+
// Run spawns the user process in init-container mode. ProcessHooks bind
192+
// m.child.MarkAlive/MarkDead so the lifecycle server's /ready alive-check
193+
// reflects the user app's state without exposing *lifecycle.Child to the
194+
// mode package. MicroVM is exclusively an init-container deployment; sidecar
195+
// mode is a wiring error and is treated as fatal.
196+
func (m *MicroVM) Run(modeConf mode.Conf, logConfig *serverlessInitLog.Config) error {
197+
if modeConf.SidecarMode {
198+
log.Fatalf("MicroVM does not support sidecar mode")
199+
}
200+
return mode.RunInit(logConfig, &mode.ProcessHooks{
201+
OnAlive: m.child.MarkAlive,
202+
OnDead: m.child.MarkDead,
203+
})
204+
}
205+
206+
// Shutdown stops the MicroVM lifecycle hook server so that any in-flight
207+
// /suspend or /terminate request can complete before the metric and trace
208+
// agents are torn down.
209+
func (m *MicroVM) Shutdown(_ *serverlessMetrics.ServerlessMetricAgent, _ bool, _ error) {
210+
if m.server == nil {
211+
return
212+
}
213+
ctx, cancel := context.WithTimeout(context.Background(), m.flushTimeout)
214+
defer cancel()
215+
if err := m.server.Stop(ctx); err != nil {
216+
log.Printf("MicroVM lifecycle server shutdown error: %v", err)
217+
}
218+
}
219+
220+
// AddStartMetric is a no-op for MicroVM. The lifecycle server emits the run
221+
// metric when the /run hook fires; emitting it here would double-count.
222+
func (m *MicroVM) AddStartMetric(_ *serverlessMetrics.ServerlessMetricAgent) {}
223+
224+
// ShouldForceFlushAllOnForceFlushToSerializer returns false for MicroVM.
225+
func (m *MicroVM) ShouldForceFlushAllOnForceFlushToSerializer() bool { return false }
226+
227+
// isMicroVM returns true when running inside an AWS Lambda MicroVM.
228+
func isMicroVM() bool {
229+
_, exists := os.LookupEnv(serverlessenv.MicroVMImageARNEnvVar)
230+
return exists
231+
}
232+
233+
// parseMicroVMARN extracts region, accountID, and imageName from an ARN of the
234+
// form arn:aws:lambda:<region>:<account>:microvm-image:<name>.
235+
// Returns "unknown" for any field that cannot be parsed.
236+
func parseMicroVMARN(arn string) (region, accountID, imageName string) {
237+
parts := strings.Split(arn, ":")
238+
region = "unknown"
239+
accountID = "unknown"
240+
imageName = "unknown"
241+
// ARN format: arn:aws:lambda:region:account:microvm-image:name
242+
if len(parts) >= 5 {
243+
if parts[3] != "" {
244+
region = parts[3]
245+
}
246+
if parts[4] != "" {
247+
accountID = parts[4]
248+
}
249+
}
250+
if len(parts) >= 7 && parts[6] != "" {
251+
imageName = strings.Join(parts[6:], ":")
252+
}
253+
return region, accountID, imageName
254+
}

0 commit comments

Comments
 (0)