Skip to content

Commit 79c5701

Browse files
[AWS] [EC2] enrich events with EC2 tags with add_cloud_metadata processor (#41477) (#41615)
* add support to extract ec2 tags from IMDS endpoint Signed-off-by: Kavindu Dodanduwa <[email protected]> * add dedicated tests for tag extractor Signed-off-by: Kavindu Dodanduwa <[email protected]> * expand test case and add documentation Signed-off-by: Kavindu Dodanduwa <[email protected]> * add changelog entry Signed-off-by: Kavindu Dodanduwa <[email protected]> * handle empty tags, add tests and close underlying body Signed-off-by: Kavindu Dodanduwa <[email protected]> * review change - use aws.tags as tag prefix Signed-off-by: Kavindu Dodanduwa <[email protected]> --------- Signed-off-by: Kavindu Dodanduwa <[email protected]> (cherry picked from commit c878397) Co-authored-by: Kavindu Dodanduwa <[email protected]>
1 parent f1c1b9f commit 79c5701

File tree

5 files changed

+367
-121
lines changed

5 files changed

+367
-121
lines changed

CHANGELOG.next.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff]
340340

341341
*Libbeat*
342342

343+
- enrich events with EC2 tags in add_cloud_metadata processor {pull}41477[41477]
343344

344345

345346
*Heartbeat*

libbeat/processors/add_cloud_metadata/docs/add_cloud_metadata.asciidoc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ examples for each of the supported providers.
8383

8484
_AWS_
8585

86+
Metadata given below are extracted from https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html[instance identity document],
87+
8688
[source,json]
8789
-------------------------------------------------------------------------------
8890
{
@@ -98,6 +100,22 @@ _AWS_
98100
}
99101
-------------------------------------------------------------------------------
100102

103+
If the EC2 instance has IMDS enabled and if tags are allowed through IMDS endpoint, the processor will further append tags in metadata.
104+
Please refer official documentation on https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html[IMDS endpoint] for further details.
105+
106+
[source,json]
107+
-------------------------------------------------------------------------------
108+
{
109+
"aws": {
110+
"tags": {
111+
"org" : "myOrg",
112+
"owner": "userID"
113+
}
114+
}
115+
}
116+
-------------------------------------------------------------------------------
117+
118+
101119
_Digital Ocean_
102120

103121
[source,json]

libbeat/processors/add_cloud_metadata/provider_aws_ec2.go

Lines changed: 114 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ package add_cloud_metadata
2020
import (
2121
"context"
2222
"fmt"
23+
"io"
2324
"net/http"
25+
"strings"
2426

2527
"github.com/elastic/elastic-agent-libs/logp"
2628

2729
awssdk "github.com/aws/aws-sdk-go-v2/aws"
2830
awscfg "github.com/aws/aws-sdk-go-v2/config"
31+
"github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds"
2932
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
3033
"github.com/aws/aws-sdk-go-v2/service/ec2"
3134
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
@@ -35,7 +38,14 @@ import (
3538
conf "github.com/elastic/elastic-agent-libs/config"
3639
)
3740

41+
const (
42+
eksClusterNameTagKey = "eks:cluster-name"
43+
tagsCategory = "tags/instance"
44+
tagPrefix = "aws.tags"
45+
)
46+
3847
type IMDSClient interface {
48+
ec2rolecreds.GetMetadataAPIClient
3949
GetInstanceIdentityDocument(ctx context.Context, params *imds.GetInstanceIdentityDocumentInput, optFns ...func(*imds.Options)) (*imds.GetInstanceIdentityDocumentOutput, error)
4050
}
4151

@@ -90,30 +100,17 @@ func fetchRawProviderMetadata(
90100
result.err = fmt.Errorf("failed loading AWS default configuration: %w", err)
91101
return
92102
}
93-
awsClient := NewIMDSClient(awsConfig)
94103

95-
instanceIdentity, err := awsClient.GetInstanceIdentityDocument(context.TODO(), &imds.GetInstanceIdentityDocumentInput{})
104+
imdsClient := NewIMDSClient(awsConfig)
105+
instanceIdentity, err := imdsClient.GetInstanceIdentityDocument(ctx, &imds.GetInstanceIdentityDocumentInput{})
96106
if err != nil {
97107
result.err = fmt.Errorf("failed fetching EC2 Identity Document: %w", err)
98108
return
99109
}
100110

101-
// AWS Region must be set to be able to get EC2 Tags
102111
awsRegion := instanceIdentity.InstanceIdentityDocument.Region
103-
awsConfig.Region = awsRegion
104112
accountID := instanceIdentity.InstanceIdentityDocument.AccountID
105-
106-
clusterName, err := fetchEC2ClusterNameTag(awsConfig, instanceIdentity.InstanceIdentityDocument.InstanceID)
107-
if err != nil {
108-
logger.Warnf("error fetching cluster name metadata: %s.", err)
109-
} else if clusterName != "" {
110-
// for AWS cluster ID is used cluster ARN: arn:partition:service:region:account-id:resource-type/resource-id, example:
111-
// arn:aws:eks:us-east-2:627286350134:cluster/cluster-name
112-
clusterARN := fmt.Sprintf("arn:aws:eks:%s:%s:cluster/%v", awsRegion, accountID, clusterName)
113-
114-
_, _ = result.metadata.Put("orchestrator.cluster.id", clusterARN)
115-
_, _ = result.metadata.Put("orchestrator.cluster.name", clusterName)
116-
}
113+
instanceID := instanceIdentity.InstanceIdentityDocument.InstanceID
117114

118115
_, _ = result.metadata.Put("cloud.instance.id", instanceIdentity.InstanceIdentityDocument.InstanceID)
119116
_, _ = result.metadata.Put("cloud.machine.type", instanceIdentity.InstanceIdentityDocument.InstanceType)
@@ -122,10 +119,106 @@ func fetchRawProviderMetadata(
122119
_, _ = result.metadata.Put("cloud.account.id", accountID)
123120
_, _ = result.metadata.Put("cloud.image.id", instanceIdentity.InstanceIdentityDocument.ImageID)
124121

122+
// AWS Region must be set to be able to get EC2 Tags
123+
awsConfig.Region = awsRegion
124+
tags := getTags(ctx, imdsClient, NewEC2Client(awsConfig), instanceID, logger)
125+
126+
if tags[eksClusterNameTagKey] != "" {
127+
// for AWS cluster ID is used cluster ARN: arn:partition:service:region:account-id:resource-type/resource-id, example:
128+
// arn:aws:eks:us-east-2:627286350134:cluster/cluster-name
129+
clusterARN := fmt.Sprintf("arn:aws:eks:%s:%s:cluster/%v", awsRegion, accountID, tags[eksClusterNameTagKey])
130+
131+
_, _ = result.metadata.Put("orchestrator.cluster.id", clusterARN)
132+
_, _ = result.metadata.Put("orchestrator.cluster.name", tags[eksClusterNameTagKey])
133+
}
134+
135+
if len(tags) == 0 {
136+
return
137+
}
138+
139+
logger.Infof("Adding retrieved tags with key: %s", tagPrefix)
140+
for k, v := range tags {
141+
_, _ = result.metadata.Put(fmt.Sprintf("%s.%s", tagPrefix, k), v)
142+
}
143+
}
144+
145+
// getTags is a helper to extract EC2 tags. Internally it utilize multiple extraction methods.
146+
func getTags(ctx context.Context, imdsClient IMDSClient, ec2Client EC2Client, instanceId string, logger *logp.Logger) map[string]string {
147+
logger.Info("Extracting EC2 tags from IMDS endpoint")
148+
tags, ok := getTagsFromIMDS(ctx, imdsClient, logger)
149+
if ok {
150+
return tags
151+
}
152+
153+
logger.Info("Tag extraction from IMDS failed, fallback to DescribeTags API to obtain EKS cluster name.")
154+
clusterName, err := clusterNameFromDescribeTag(ctx, ec2Client, instanceId)
155+
if err != nil {
156+
logger.Warnf("error obtaining cluster name: %v.", err)
157+
return tags
158+
}
159+
160+
if clusterName != "" {
161+
tags[eksClusterNameTagKey] = clusterName
162+
}
163+
return tags
164+
}
165+
166+
// getTagsFromIMDS is a helper to extract EC2 tags using instance metadata service.
167+
// Note that this call could get throttled and currently does not implement a retry mechanism.
168+
// See - https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html#instancedata-throttling
169+
func getTagsFromIMDS(ctx context.Context, client IMDSClient, logger *logp.Logger) (tags map[string]string, ok bool) {
170+
tags = make(map[string]string)
171+
172+
b, err := getMetadataHelper(ctx, client, tagsCategory, logger)
173+
if err != nil {
174+
logger.Warnf("error obtaining tags category: %v", err)
175+
return tags, false
176+
}
177+
178+
for _, tag := range strings.Split(string(b), "\n") {
179+
tagPath := fmt.Sprintf("%s/%s", tagsCategory, tag)
180+
b, err := getMetadataHelper(ctx, client, tagPath, logger)
181+
if err != nil {
182+
logger.Warnf("error extracting tag value of %s: %v", tag, err)
183+
return tags, false
184+
}
185+
186+
tagValue := string(b)
187+
if tagValue == "" {
188+
logger.Infof("Ignoring tag key %s as value is empty", tag)
189+
continue
190+
}
191+
192+
tags[tag] = tagValue
193+
}
194+
195+
return tags, true
196+
}
197+
198+
// getMetadataHelper performs the IMDS call for the given path and returns the response content after closing the underlying content reader.
199+
func getMetadataHelper(ctx context.Context, client IMDSClient, path string, logger *logp.Logger) (content []byte, err error) {
200+
metadata, err := client.GetMetadata(ctx, &imds.GetMetadataInput{Path: path})
201+
if err != nil {
202+
return nil, fmt.Errorf("error from IMDS metadata request: %w", err)
203+
}
204+
205+
defer func(Content io.ReadCloser) {
206+
err := Content.Close()
207+
if err != nil {
208+
logger.Warnf("error closing IMDS metadata response body: %v", err)
209+
}
210+
}(metadata.Content)
211+
212+
content, err = io.ReadAll(metadata.Content)
213+
if err != nil {
214+
return nil, fmt.Errorf("error extracting metadata from the IMDS response: %w", err)
215+
}
216+
217+
return content, nil
125218
}
126219

127-
func fetchEC2ClusterNameTag(awsConfig awssdk.Config, instanceID string) (string, error) {
128-
svc := NewEC2Client(awsConfig)
220+
// clusterNameFromDescribeTag is a helper to extract EKS cluster name using DescribeTag.
221+
func clusterNameFromDescribeTag(ctx context.Context, ec2Client EC2Client, instanceID string) (string, error) {
129222
input := &ec2.DescribeTagsInput{
130223
Filters: []types.Filter{
131224
{
@@ -135,15 +228,13 @@ func fetchEC2ClusterNameTag(awsConfig awssdk.Config, instanceID string) (string,
135228
},
136229
},
137230
{
138-
Name: awssdk.String("key"),
139-
Values: []string{
140-
"eks:cluster-name",
141-
},
231+
Name: awssdk.String("key"),
232+
Values: []string{eksClusterNameTagKey},
142233
},
143234
},
144235
}
145236

146-
tagsResult, err := svc.DescribeTags(context.TODO(), input)
237+
tagsResult, err := ec2Client.DescribeTags(ctx, input)
147238
if err != nil {
148239
return "", fmt.Errorf("error fetching EC2 Tags: %w", err)
149240
}

0 commit comments

Comments
 (0)