Skip to content

Commit d4764ef

Browse files
feat(serverless-init): add MicroVM CloudService implementation
Adds cloudservice.MicroVM, a CloudService implementation for AWS Lambda MicroVMs: GetTags parses the image ARN for region/account_id/image_name, Init constructs and starts the lifecycle server from a LifecycleContext, Run spawns the user process with OnAlive/OnDead hooks bound to the lifecycle server's child-liveness tracking, and Shutdown stops the lifecycle server with a bounded timeout. MicroVM supports both amd64 and arm64 (all other cloud services are amd64-only). TracingContext gains a LifecycleCtx field (nil for all non-MicroVM services) so MicroVM.Init can receive the telemetry dependencies it needs without cloudservice depending on main.go. MicroVM is not yet reachable via GetCloudServiceType — that wiring, together with main.go populating LifecycleCtx, lands in a later PR in this stack so the type is fully wired in one atomic step. Includes unit test coverage for GetTags/GetEnhancedMetricTags (ARN parsing and "unknown" fallback), parseMicroVMARN, isMicroVM, isSupportedArch, Run's threading of the child handle into RunInit's liveness hooks, Init (nil TracingContext/LifecycleCtx no-ops, server construction, sidecar vs init-container Child exposure), Shutdown (nil server, live server stop), and the LogsTagSetter/TraceTagSetter wiring that forwards the lifecycle server's /launch microvm_id into base log and trace tags.
1 parent ead2d71 commit d4764ef

3 files changed

Lines changed: 725 additions & 2 deletions

File tree

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

0 commit comments

Comments
 (0)