async_profiler_agent/metadata/
aws.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use reqwest::Method;
5use serde::Deserialize;
6use thiserror::Error;
7
8use super::AgentMetadata;
9
10#[derive(Error, Debug)]
11pub enum FargateMetadataToAgentMetadataError {
12    #[error("unable to parse task ARN as a valid ARN")]
13    TaskArnInvalid(#[from] aws_arn::Error),
14    #[error("AWS account id not found in Fargate metadata")]
15    AccountIdNotFound,
16    #[error("AWS region not found in Fargate metadata")]
17    AwsRegionNotFound,
18}
19
20#[derive(Error, Debug)]
21#[error("profiler metadata error: {0}")]
22pub enum AwsProfilerMetadataError {
23    #[error("failed to create profiler metadata file: {0}")]
24    FailedToCreateFile(#[from] std::io::Error),
25
26    #[error("failed fetching valid Fargate metadata: {0}")]
27    FargateMetadataToAgentMetadataError(#[from] FargateMetadataToAgentMetadataError),
28
29    #[error("retrieved invalid endpoint URI: {0}")]
30    InvalidUri(String),
31
32    #[error("failed to fetch metadata from endpoint over HTTP: {0}")]
33    FailedToFetchMetadataFromEndpoint(reqwest::Error),
34
35    #[error("failed to fetch metadata from IMDS endpoint over HTTP: {0}")]
36    FailedToFetchMetadataFromImds(#[from] aws_config::imds::client::error::ImdsError),
37
38    #[error("failed to parse metadata as valid UTF-8 from endpoint over HTTP: {0}")]
39    FailedToParseMetadataFromEndpoint(reqwest::Error),
40
41    #[error("failed to serialize metadata file: {0}")]
42    FailedToSerializeMetadataFile(#[from] serde_json::Error),
43}
44
45#[derive(Debug, Deserialize, PartialEq, Eq)]
46#[serde(rename_all = "camelCase")]
47struct ImdsEc2InstanceMetadata {
48    account_id: String,
49    region: String,
50    #[allow(dead_code)]
51    instance_type: String,
52    instance_id: String,
53}
54
55async fn read_ec2_metadata() -> Result<ImdsEc2InstanceMetadata, AwsProfilerMetadataError> {
56    let imds = aws_config::imds::Client::builder().build();
57    let imds_document = imds
58        .get("/latest/dynamic/instance-identity/document")
59        .await?;
60
61    Ok(serde_json::from_str(imds_document.as_ref())?)
62}
63
64#[derive(Deserialize, Debug, PartialEq, Eq)]
65struct FargateMetadata {
66    #[serde(rename = "Cluster")]
67    cluster: String,
68    #[serde(rename = "TaskARN")]
69    task_arn: String,
70}
71
72async fn read_fargate_metadata(
73    http_client: &reqwest::Client,
74) -> Result<FargateMetadata, AwsProfilerMetadataError> {
75    let Ok(md_uri) = std::env::var("ECS_CONTAINER_METADATA_URI_V4") else {
76        return Err(AwsProfilerMetadataError::InvalidUri(
77            "not running on fargate".into(),
78        ));
79    };
80    let uri = format!(
81        "{}/task",
82        // Only available if running on Fargate. Something like
83        // `http://169.254.232.106/v4/5261e761e0e2a3d92da3f02c8e5bab1f-3356750833`.
84        md_uri
85    );
86
87    let req = http_client
88        .request(Method::GET, uri.clone())
89        .build()
90        // The only thing that can be invalid about this request is necessarily the URI.
91        .map_err(|_e| AwsProfilerMetadataError::InvalidUri(uri))?;
92    let res = http_client
93        .execute(req)
94        .await
95        .map_err(AwsProfilerMetadataError::FailedToFetchMetadataFromEndpoint)?;
96    let body_str = res
97        .text()
98        .await
99        .map_err(AwsProfilerMetadataError::FailedToParseMetadataFromEndpoint)?;
100
101    Ok(serde_json::from_str(&body_str)?)
102}
103
104impl super::AgentMetadata {
105    fn from_imds_ec2_instance_metadata(
106        imds_ec2_instance_metadata: ImdsEc2InstanceMetadata,
107    ) -> Self {
108        Self::Ec2AgentMetadata {
109            aws_account_id: imds_ec2_instance_metadata.account_id,
110            aws_region_id: imds_ec2_instance_metadata.region,
111            ec2_instance_id: imds_ec2_instance_metadata.instance_id,
112        }
113    }
114
115    fn try_from_fargate_metadata(
116        fargate_metadata: FargateMetadata,
117    ) -> Result<Self, FargateMetadataToAgentMetadataError> {
118        let ecs_task_arn: aws_arn::ResourceName = fargate_metadata.task_arn.parse()?;
119
120        Ok(Self::FargateAgentMetadata {
121            aws_account_id: ecs_task_arn
122                .account_id
123                .ok_or(FargateMetadataToAgentMetadataError::AccountIdNotFound)?
124                .to_string(),
125            aws_region_id: ecs_task_arn
126                .region
127                .ok_or(FargateMetadataToAgentMetadataError::AwsRegionNotFound)?
128                .to_string(),
129            ecs_task_arn: fargate_metadata.task_arn,
130            ecs_cluster_arn: fargate_metadata.cluster,
131        })
132    }
133}
134
135pub async fn load_agent_metadata() -> Result<AgentMetadata, AwsProfilerMetadataError> {
136    let agent_metadata: AgentMetadata = match read_ec2_metadata().await {
137        Ok(imds_ec2_instance_metadata) => {
138            AgentMetadata::from_imds_ec2_instance_metadata(imds_ec2_instance_metadata)
139        }
140        Err(_) => {
141            let http_client = reqwest::Client::new();
142            let fargate_metadata = read_fargate_metadata(&http_client).await?;
143
144            AgentMetadata::try_from_fargate_metadata(fargate_metadata)?
145        }
146    };
147    Ok(agent_metadata)
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    // these constants are "anonymized" (aka randomly generated in that format)
155
156    // AMI_ID = 52d4b310b0a459ff
157    // INSTANCE_ID = 92eba08c089f6325
158    // ACCOUNT_ID = 123456789012
159    // IP = 10.65.149.216
160    // DATE = 2025-03-20T16:41:24Z
161    // DATE1 = 2025-03-20T16:41:24.713942268Z
162    // DATE2 = 2025-03-20T16:41:25.623883595Z
163    // TASK_ARN = 5261e761e0e2a3d92da3f02c8e5bab1f
164    // DOCKER_ID = 3356750833
165    // PRIV_IP = 169.254.232.106
166    // IMAGE_ID = ad9d89a36c31afef34c79e05263b06087ad354796cfd90c66ced30f40ea2dbf4
167    // TASK_UUID = f4094744-1b40-4701-9f26-ad84ebb709d7
168    // CLOCK_ERROR = 0.3148955
169
170    #[test]
171    fn test_imds_ec2_metadata() {
172        let json_str = r#"
173{
174    "accountId" : "123456789012",
175    "architecture" : "x86_64",
176    "availabilityZone" : "eu-west-1b",
177    "billingProducts" : null,
178    "devpayProductCodes" : null,
179    "marketplaceProductCodes" : null,
180    "imageId" : "ami-052d4b310b0a459ff",
181    "instanceId" : "i-092eba08c089f6325",
182    "instanceType" : "c5.4xlarge",
183    "kernelId" : null,
184    "pendingTime" : "2025-03-20T16:41:24Z",
185    "privateIp" : "10.65.149.216",
186    "ramdiskId" : null,
187    "region" : "eu-west-1",
188    "version" : "2017-09-30"
189}"#;
190
191        let imds_ec2_instance_metadata: ImdsEc2InstanceMetadata =
192            serde_json::from_str(&json_str).unwrap();
193
194        assert_eq!(
195            imds_ec2_instance_metadata,
196            ImdsEc2InstanceMetadata {
197                account_id: "123456789012".to_owned(),
198                region: "eu-west-1".to_owned(),
199                instance_type: "c5.4xlarge".to_owned(),
200                instance_id: "i-092eba08c089f6325".to_owned(),
201            }
202        );
203
204        let agent_metadata =
205            AgentMetadata::from_imds_ec2_instance_metadata(imds_ec2_instance_metadata);
206
207        assert_eq!(
208            agent_metadata,
209            AgentMetadata::Ec2AgentMetadata {
210                aws_account_id: "123456789012".to_owned(),
211                aws_region_id: "eu-west-1".to_owned(),
212                ec2_instance_id: "i-092eba08c089f6325".to_owned(),
213            }
214        )
215    }
216
217    #[test]
218    fn test_fargate_metadata() {
219        let json_str = r#"
220{
221    "Cluster": "arn:aws:ecs:us-east-1:123456789012:cluster/profiler-metadata-cluster",
222    "TaskARN": "arn:aws:ecs:us-east-1:123456789012:task/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f",
223    "Family": "profiler-metadata",
224    "Revision": "1",
225    "DesiredStatus": "RUNNING",
226    "KnownStatus": "NONE",
227    "Limits": {
228        "CPU": 0.25,
229        "Memory": 2048
230    },
231    "PullStartedAt": "2025-03-20T16:41:24.713942268Z",
232    "PullStoppedAt": "2025-03-20T16:41:25.623883595Z",
233    "AvailabilityZone": "us-east-1f",
234    "LaunchType": "FARGATE",
235    "Containers": [
236        {
237            "DockerId": "5261e761e0e2a3d92da3f02c8e5bab1f-3356750833",
238            "Name": "profiler-metadata",
239            "DockerName": "profiler-metadata",
240            "Image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/profiler-metadata",
241            "ImageID": "sha256:ad9d89a36c31afef34c79e05263b06087ad354796cfd90c66ced30f40ea2dbf4",
242            "Labels": {
243                "com.amazonaws.ecs.cluster": "arn:aws:ecs:us-east-1:123456789012:cluster/profiler-metadata-cluster",
244                "com.amazonaws.ecs.container-name": "profiler-metadata",
245                "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-1:123456789012:task/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f",
246                "com.amazonaws.ecs.task-definition-family": "profiler-metadata",
247                "com.amazonaws.ecs.task-definition-version": "1"
248            },
249            "DesiredStatus": "RUNNING",
250            "KnownStatus": "PULLED",
251            "Limits": {
252                "CPU": 0
253            },
254            "Type": "NORMAL",
255            "LogDriver": "awslogs",
256            "LogOptions": {
257                "awslogs-create-group": "true",
258                "awslogs-group": "/ecs/profiler-metadata",
259                "awslogs-region": "us-east-1",
260                "awslogs-stream": "ecs/profiler-metadata/5261e761e0e2a3d92da3f02c8e5bab1f",
261                "max-buffer-size": "25m",
262                "mode": "non-blocking"
263            },
264            "ContainerARN": "arn:aws:ecs:us-east-1:123456789012:container/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f/f4094744-1b40-4701-9f26-ad84ebb709d7",
265            "Networks": [
266                {
267                    "NetworkMode": "awsvpc",
268                    "IPv4Addresses": [
269                        "172.31.233.169"
270                    ],
271                    "AttachmentIndex": 0,
272                    "MACAddress": "16:ff:d6:e1:dc:99",
273                    "IPv4SubnetCIDRBlock": "172.31.192.0/20",
274                    "DomainNameServers": [
275                        "172.31.0.2"
276                    ],
277                    "DomainNameSearchList": [
278                        "ec2.internal"
279                    ],
280                    "PrivateDNSName": "ip-172-31-233-169.ec2.internal",
281                    "SubnetGatewayIpv4Address": "172.31.192.1/20"
282                }
283            ],
284            "Snapshotter": "overlayfs"
285        }
286    ],
287    "ClockDrift": {
288        "ClockErrorBound": 0.3148955,
289        "ReferenceTimestamp": "2025-03-20T16:41:24Z",
290        "ClockSynchronizationStatus": "SYNCHRONIZED"
291    },
292    "EphemeralStorageMetrics": {
293        "Utilized": 208,
294        "Reserved": 20496
295    }
296}
297"#;
298
299        let fargate_metadata: FargateMetadata = serde_json::from_str(&json_str).unwrap();
300
301        assert_eq!(
302            fargate_metadata,
303            FargateMetadata {
304                cluster: "arn:aws:ecs:us-east-1:123456789012:cluster/profiler-metadata-cluster"
305                    .to_owned(),
306                task_arn: "arn:aws:ecs:us-east-1:123456789012:task/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f".to_owned(),
307            }
308        );
309
310        let agent_metadata = AgentMetadata::try_from_fargate_metadata(fargate_metadata).unwrap();
311
312        assert_eq!(
313            agent_metadata,
314            AgentMetadata::FargateAgentMetadata {
315                aws_account_id: "123456789012".to_owned(),
316                aws_region_id: "us-east-1".to_owned(),
317                ecs_task_arn: "arn:aws:ecs:us-east-1:123456789012:task/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f".to_owned(),
318                ecs_cluster_arn: "arn:aws:ecs:us-east-1:123456789012:cluster/profiler-metadata-cluster".to_owned(),
319            }
320        )
321    }
322}