Skip to content

Commit 7005371

Browse files
committed
Address review feedback on IAM scope, runner_ip, and validation retries
- CloudWatchAgentServerPolicy already covers the agent's OTLP trace writes, so xray:PutSpans is unnecessary. Removing it leaves the AKS role with no inline policy at all; the VM inline policy is now validation reads only. - runner_ip is required in both modules instead of defaulting to "". This removes the path where an unset value left the AKS API server reachable from any IP while reading as restricted, and drops the dynamic block that worked around it. - VM log validation now retries on the same schedule as the AKS path, so a slow first ingestion no longer reports as a delivery failure. - Scope AKS metric validation on a test_id datapoint attribute and assert cloud.platform=azure_aks. Datapoint attributes are the one surface no resource processor rewrites, and cloud.platform is emitted only by the aks detector, so the assertion covers detection rather than echoing back the injected payload. - Match the kubernetes provider constraint already used by eks/daemon/efa. - Move the load-generator payload rationale into the script, and drop the stale USE_DEFAULT_CONFIG references and all-merged prerequisite list from the README.
1 parent 69677cc commit 7005371

10 files changed

Lines changed: 63 additions & 90 deletions

File tree

terraform/azure/aks/main.tf

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,10 @@ resource "azurerm_kubernetes_cluster" "cwagent" {
2424
oidc_issuer_enabled = true
2525
workload_identity_enabled = true
2626

27-
# Terraform drives the cluster over the public API server, so restrict it to the runner that created
28-
# it. The block is omitted entirely when runner_ip is unset rather than emitted with an empty list --
29-
# an empty authorized_ip_ranges means "open to all", which would read as restricted while being open.
30-
dynamic "api_server_access_profile" {
31-
for_each = var.runner_ip != "" ? [var.runner_ip] : []
32-
content {
33-
authorized_ip_ranges = [api_server_access_profile.value]
34-
}
27+
# Terraform drives the cluster over the public API server, so restrict it to the runner that created it.
28+
# runner_ip is required, so there is no path where this silently ends up open to all.
29+
api_server_access_profile {
30+
authorized_ip_ranges = [var.runner_ip]
3531
}
3632

3733
identity {
@@ -114,25 +110,8 @@ resource "aws_iam_role_policy_attachment" "cwagent_server_policy" {
114110
policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
115111
}
116112

117-
# Unlike the VM test, the AKS test binary runs on the runner under the runner's own credentials, so this
118-
# role needs agent writes only -- no validation reads.
119-
data "aws_iam_policy_document" "cwagent_permissions" {
120-
statement {
121-
effect = "Allow"
122-
actions = [
123-
# Agent write omitted from CloudWatchAgentServerPolicy: the X-Ray OTLP endpoint needs PutSpans,
124-
# which is a different action from PutTraceSegments.
125-
"xray:PutSpans",
126-
]
127-
resources = ["*"]
128-
}
129-
}
130-
131-
resource "aws_iam_role_policy" "cwagent" {
132-
name = "cwa-aks-integ-policy-${module.common.testing_id}"
133-
role = aws_iam_role.cwagent.id
134-
policy = data.aws_iam_policy_document.cwagent_permissions.json
135-
}
113+
# No inline policy: the AKS test binary runs on the runner under its own credentials, so this role needs
114+
# agent writes only -- and CloudWatchAgentServerPolicy alone covers them, OTLP traces included.
136115

137116
#####################################################################
138117
# Kubernetes resources: deploy CWA DaemonSet from ECR image
@@ -331,18 +310,14 @@ resource "kubernetes_daemon_set_v1" "cwagent" {
331310

332311
depends_on = [
333312
kubernetes_cluster_role_binding.cwagent,
334-
aws_iam_role_policy.cwagent,
335313
aws_iam_role_policy_attachment.cwagent_server_policy,
336314
]
337315
}
338316

339317
#####################################################################
340318
# Load generator: pushes OTLP to localhost:4318 for 3 min via hostNetwork
341319
#####################################################################
342-
# The payloads carry k8s.cluster.name and k8s.namespace.name resource attributes so the
343-
# agent's k8s logs-routing template produces a deterministic per-cluster destination
344-
# (/aws/cwagent/<cluster>/otlp, stream amazon-cloudwatch/<service>); resourcedetection
345-
# only overrides keys it detects (e.g. host.id), so these pass through intact.
320+
# See otlp_load_generator.sh for what the payloads carry and why.
346321
resource "kubernetes_job_v1" "otlp_load" {
347322
metadata {
348323
name = "otlp-load-generator"

terraform/azure/aks/otlp_load_generator.sh

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
#
88
# Rendered by terraform via templatefile(), so a single-dollar brace expansion is a template
99
# variable and a double-dollar brace expansion is passed through as a literal shell expansion.
10-
# Every payload carries k8s.cluster.name and k8s.namespace.name so the agent's k8s logs-routing
11-
# template produces a deterministic per-cluster destination, and resourcedetection (which only
12-
# overrides keys it detects, e.g. host.id) leaves them intact.
10+
# Every payload carries k8s.cluster.name and k8s.namespace.name so the agent's k8s logs-routing template
11+
# produces a deterministic per-cluster destination (/aws/cwagent/<cluster>/otlp, stream
12+
# amazon-cloudwatch/<service>); resourcedetection only overrides keys it detects, so these pass through.
13+
#
14+
# Metric datapoints carry test_id (value = per-run cluster name) for the Go test to scope on:
15+
# datapoint attributes are the one surface no resource processor rewrites.
1316

1417
SERVICE_NAME="${service_name}"
1518
INSTANCE_ID="${instance_id}"
@@ -28,7 +31,7 @@ while [ $(date +%s) -lt $END ]; do
2831
SPAN_ID=$(printf '%016x' "$NOW_S$SEQ")
2932

3033
curl -sf -X POST "$ENDPOINT/v1/metrics" -H "Content-Type: application/json" \
31-
-d "{\"resourceMetrics\":[{\"resource\":{\"attributes\":[{\"key\":\"service.name\",\"value\":{\"stringValue\":\"$SERVICE_NAME\"}},{\"key\":\"host.id\",\"value\":{\"stringValue\":\"$INSTANCE_ID\"}},{\"key\":\"k8s.cluster.name\",\"value\":{\"stringValue\":\"$INSTANCE_ID\"}},{\"key\":\"k8s.namespace.name\",\"value\":{\"stringValue\":\"amazon-cloudwatch\"}}]},\"scopeMetrics\":[{\"scope\":{\"name\":\"aks-otlp-test\"},\"metrics\":[{\"name\":\"aks_otlp_counter\",\"unit\":\"1\",\"sum\":{\"aggregationTemporality\":2,\"isMonotonic\":true,\"dataPoints\":[{\"asInt\":\"$SEQ\",\"startTimeUnixNano\":\"$${START}000000000\",\"timeUnixNano\":\"$NOW_NS\",\"attributes\":[{\"key\":\"ClusterName\",\"value\":{\"stringValue\":\"$INSTANCE_ID\"}}]}]}}]}]}]}" || true
34+
-d "{\"resourceMetrics\":[{\"resource\":{\"attributes\":[{\"key\":\"service.name\",\"value\":{\"stringValue\":\"$SERVICE_NAME\"}},{\"key\":\"host.id\",\"value\":{\"stringValue\":\"$INSTANCE_ID\"}},{\"key\":\"k8s.cluster.name\",\"value\":{\"stringValue\":\"$INSTANCE_ID\"}},{\"key\":\"k8s.namespace.name\",\"value\":{\"stringValue\":\"amazon-cloudwatch\"}}]},\"scopeMetrics\":[{\"scope\":{\"name\":\"aks-otlp-test\"},\"metrics\":[{\"name\":\"aks_otlp_counter\",\"unit\":\"1\",\"sum\":{\"aggregationTemporality\":2,\"isMonotonic\":true,\"dataPoints\":[{\"asInt\":\"$SEQ\",\"startTimeUnixNano\":\"$${START}000000000\",\"timeUnixNano\":\"$NOW_NS\",\"attributes\":[{\"key\":\"test_id\",\"value\":{\"stringValue\":\"$INSTANCE_ID\"}}]}]}}]}]}]}" || true
3235

3336
curl -sf -X POST "$ENDPOINT/v1/logs" -H "Content-Type: application/json" \
3437
-d "{\"resourceLogs\":[{\"resource\":{\"attributes\":[{\"key\":\"service.name\",\"value\":{\"stringValue\":\"$SERVICE_NAME\"}},{\"key\":\"host.id\",\"value\":{\"stringValue\":\"$INSTANCE_ID\"}},{\"key\":\"k8s.cluster.name\",\"value\":{\"stringValue\":\"$INSTANCE_ID\"}},{\"key\":\"k8s.namespace.name\",\"value\":{\"stringValue\":\"amazon-cloudwatch\"}}]},\"scopeLogs\":[{\"scope\":{\"name\":\"aks-otlp-test\"},\"logRecords\":[{\"timeUnixNano\":\"$NOW_NS\",\"severityText\":\"INFO\",\"body\":{\"stringValue\":\"aks_otlp_log_$INSTANCE_ID\"},\"attributes\":[{\"key\":\"ClusterName\",\"value\":{\"stringValue\":\"$INSTANCE_ID\"}}]}]}]}]}" || true

terraform/azure/aks/providers.tf

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: MIT
33

4-
# aws is constrained the same way the rest of the repo constrains it. azurerm and kubernetes are
5-
# major-version pinned because they are new to this repo and both have breaking schema changes past
6-
# the majors this module targets: unpinning azurerm resolved 5.0.0, which fails with
7-
# "At least 1 node_provisioning_profile blocks are required" on azurerm_kubernetes_cluster, and
8-
# kubernetes resolved 3.2.1 across its own 2.x -> 3.x break.
4+
# aws and kubernetes match terraform/eks/daemon/efa. azurerm has no precedent in this repo and is
5+
# pinned to 4.x because 5.0.0 requires a node_provisioning_profile block on azurerm_kubernetes_cluster.
96
terraform {
107
required_providers {
118
aws = {
@@ -18,7 +15,7 @@ terraform {
1815
}
1916
kubernetes = {
2017
source = "hashicorp/kubernetes"
21-
version = "~> 2.30"
18+
version = "~> 2.0"
2219
}
2320
tls = {
2421
source = "hashicorp/tls"

terraform/azure/aks/variables.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ variable "azure_subnet_name" {
3434
default = "default"
3535
}
3636

37+
# Required, not defaulted: the API server is public and this is the only thing scoping it to the runner.
3738
variable "runner_ip" {
3839
type = string
39-
description = "Runner public IP, used to scope API server access to the runner. Empty leaves the API server open to all IPs."
40-
default = ""
40+
description = "Runner public IP CIDR (e.g. \"1.2.3.4/32\") allowed to reach the AKS API server."
4141
}
4242

4343
variable "aks_node_vm_size" {

terraform/azure/vm/iam.tf

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,12 @@ resource "aws_iam_role_policy_attachment" "cwagent_server_policy" {
5757
policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
5858
}
5959

60-
# Everything CloudWatchAgentServerPolicy does not cover. The test binary runs on the VM under this same
61-
# role, so the validation reads have to live here too.
60+
# Validation reads only -- the agent's own writes are fully covered by CloudWatchAgentServerPolicy. The
61+
# test binary runs on the VM under this same role, so these have to live here.
6262
data "aws_iam_policy_document" "cwagent_permissions" {
6363
statement {
6464
effect = "Allow"
6565
actions = [
66-
# Agent write omitted from CloudWatchAgentServerPolicy: the X-Ray OTLP endpoint needs PutSpans,
67-
# which is a different action from PutTraceSegments.
68-
"xray:PutSpans",
69-
# Reads used to assert delivery.
7066
"cloudwatch:ListMetrics",
7167
"cloudwatch:GetMetricData",
7268
"logs:GetLogEvents",

terraform/azure/vm/providers.tf

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: MIT
33

4-
# aws is constrained the same way the rest of the repo constrains it. azurerm is major-version pinned
5-
# because it is new to this repo and 5.x carries breaking schema changes relative to the 4.x resources
6-
# used here (unpinning it resolved 5.0.0 and broke the sibling AKS module).
4+
# aws matches terraform/eks/daemon/efa. azurerm has no precedent in this repo and is pinned to 4.x
5+
# because 5.0.0 carries breaking schema changes relative to the 4.x resources used here.
76
terraform {
87
required_providers {
98
aws = {

terraform/azure/vm/variables.tf

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ variable "admin_username" {
7777
default = "cwagent"
7878
}
7979

80-
# CIDR allowed inbound SSH to the VM (the CI runner's public IP, e.g. "1.2.3.4/32"); no default so it must be set.
80+
# CIDR allowed inbound SSH to the VM (the CI runner's public IP, e.g. "1.2.3.4/32").
81+
# Required, not defaulted: this is the only thing scoping the NSG's Allow-22 rule.
8182
variable "runner_ip" {
82-
type = string
83-
default = ""
83+
type = string
8484
}
8585

8686
# Ubuntu image the VM boots; matches the Debian-package install path used below.

test/azure/aks/aks_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,14 @@ func TestMain(m *testing.M) {
4747

4848
func TestAKS(t *testing.T) {
4949
t.Run("Metrics", func(t *testing.T) {
50-
// host.id is overwritten by resourcedetection with the node's VMSS instance ID,
51-
// so scope by the cluster-name resource attribute the load generator sends.
50+
// test_id is a datapoint attribute, the one surface no resource processor rewrites, so it
51+
// isolates this run. cloud.platform=azure_aks comes only from the aks detector: proves detection ran.
5252
group := otlpvalidation.ValidateOtlpMetricsWithLabels(
5353
"AKSDefaultOtel", env.Region, []string{"aks_otlp_counter"},
5454
map[string]string{
55-
"@resource.k8s.cluster.name": env.AKSClusterName,
56-
"@resource.cloud.provider": "azure",
55+
"test_id": env.AKSClusterName,
56+
"@resource.cloud.platform": "azure_aks",
57+
"@resource.cloud.provider": "azure",
5758
},
5859
)
5960
for _, r := range group.TestResults {

test/azure/vm/README.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# Azure VM integration test
22

33
Validates the CloudWatch Agent on a **real Azure VM** running the default OTel configuration
4-
(`USE_DEFAULT_CONFIG=otel` / `default:otel`). It exercises the full generated OTel pipeline
4+
(`default:otel`). It exercises the full generated OTel pipeline
55
(Azure IMDS detection → translation → collector startup) and verifies that metrics, logs, and traces
66
reach CloudWatch through the Azure → AWS credential chain (`oidctoken` extension → `sigv4auth` web identity).
77

88
## What it checks
99

10-
The agent is started with no JSON config and `USE_DEFAULT_CONFIG=otel`, so it emits the default OTel
11-
pipeline (OTLP receivers on `127.0.0.1:4317/4318` → native CloudWatch OTLP exporters). The test pushes OTLP
12-
and asserts delivery:
10+
The agent is started with no JSON config, via `amazon-cloudwatch-agent-ctl -a fetch-config -m auto -s -c
11+
default:otel`, so it emits the default OTel pipeline (OTLP receivers on `127.0.0.1:4317/4318` → native
12+
CloudWatch OTLP exporters). The test pushes OTLP and asserts delivery:
1313

1414
| Signal | Destination | Assertion |
1515
|---------|-----------------------------------------------|-----------|
@@ -21,14 +21,6 @@ Delivery of all three signals is itself the credential-chain assertion: nothing
2121
the Azure web-identity exchange succeeded. The `cloud.provider=azure` resource label additionally proves the
2222
telemetry traversed the Azure detection path rather than being attributed to stray AWS credentials.
2323

24-
## Agent prerequisites
25-
26-
- `#2179` Add default OTel config — **merged** (`USE_DEFAULT_CONFIG=otel`)
27-
- `#2183` Azure platform detection — **merged** (IMDS detection → translation)
28-
- `#2010` OIDC auth (`oidctoken`) — **merged**
29-
- `#2197` web-identity for `awscloudwatchlogsprovisioner`**merged** (log-group creation on Azure VM)
30-
- `#2211` `set-env` ctl action — **merged** (used to persist `AWS_REGION` / `CWAGENT_ROLE_ARN`)
31-
3224
## Prerequisites that MUST be satisfied before a live run
3325

3426
These were verified against a real Azure VM and the merged agent translator. Without them a live

test/azure/vm/azurevm_test.go

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -122,26 +122,36 @@ func measuredMetrics() []string { return []string{"azurevm_otlp_counter", "azure
122122
func validateLogs() status.TestResult {
123123
testResult := status.TestResult{Name: "AzureVM_Logs", Status: status.FAILED}
124124

125-
streams := awsservice.GetLogStreams(otlpLogGroup)
126-
if len(streams) == 0 {
127-
testResult.Reason = fmt.Errorf("no log streams found in %s", otlpLogGroup)
128-
return testResult
129-
}
130-
131-
since := time.Now().Add(-loadWindow - time.Minute)
132-
until := time.Now()
125+
// Retry on the same schedule as the AKS log path: the stream and its events can both lag the
126+
// load window, so a single attempt fails on ingestion delay rather than on delivery.
133127
marker := fmt.Sprintf("azurevm_otlp_log_%s", env.InstanceId)
134-
for _, stream := range streams {
135-
err := awsservice.ValidateLogs(
136-
otlpLogGroup, *stream.LogStreamName, &since, &until,
137-
awsservice.AssertLogsNotEmpty(),
138-
awsservice.AssertPerLog(awsservice.AssertLogContainsSubstring(marker)),
139-
)
140-
if err == nil {
141-
testResult.Status = status.SUCCESSFUL
142-
return testResult
128+
const maxRetries = 4
129+
const retryInterval = 30 * time.Second
130+
for attempt := 1; attempt <= maxRetries; attempt++ {
131+
since := time.Now().Add(-loadWindow - time.Minute)
132+
until := time.Now()
133+
134+
streams := awsservice.GetLogStreams(otlpLogGroup)
135+
if len(streams) == 0 {
136+
testResult.Reason = fmt.Errorf("attempt %d: no log streams found in %s", attempt, otlpLogGroup)
137+
}
138+
for _, stream := range streams {
139+
log.Printf("[AzureVM_Logs] attempt %d: checking %s/%s", attempt, otlpLogGroup, *stream.LogStreamName)
140+
err := awsservice.ValidateLogs(
141+
otlpLogGroup, *stream.LogStreamName, &since, &until,
142+
awsservice.AssertLogsNotEmpty(),
143+
awsservice.AssertPerLog(awsservice.AssertLogContainsSubstring(marker)),
144+
)
145+
if err == nil {
146+
testResult.Status = status.SUCCESSFUL
147+
return testResult
148+
}
149+
testResult.Reason = err
150+
}
151+
if attempt < maxRetries {
152+
log.Printf("[AzureVM_Logs] %v — retrying in %v", testResult.Reason, retryInterval)
153+
time.Sleep(retryInterval)
143154
}
144-
testResult.Reason = err
145155
}
146156
return testResult
147157
}

0 commit comments

Comments
 (0)