github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/scrape/discovery/aws/ec2.go (about) 1 // Copyright 2021 The Prometheus Authors 2 // Copyright 2021 The Pyroscope Authors 3 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 package aws // revive:disable-line:import-shadowing package name is not referenced 17 18 import ( 19 "context" 20 "fmt" 21 "github.com/sirupsen/logrus" 22 "net" 23 "strings" 24 "time" 25 26 "github.com/aws/aws-sdk-go/aws" 27 "github.com/aws/aws-sdk-go/aws/awserr" 28 "github.com/aws/aws-sdk-go/aws/credentials" 29 "github.com/aws/aws-sdk-go/aws/credentials/stscreds" 30 "github.com/aws/aws-sdk-go/aws/ec2metadata" 31 "github.com/aws/aws-sdk-go/aws/session" 32 "github.com/aws/aws-sdk-go/service/ec2" 33 "github.com/pkg/errors" 34 "github.com/pyroscope-io/pyroscope/pkg/scrape/discovery" 35 "github.com/pyroscope-io/pyroscope/pkg/scrape/discovery/refresh" 36 "github.com/pyroscope-io/pyroscope/pkg/scrape/discovery/targetgroup" 37 "github.com/pyroscope-io/pyroscope/pkg/scrape/model" 38 "github.com/pyroscope-io/pyroscope/pkg/util/strutil" 39 ) 40 41 const ( 42 ec2Label = model.MetaLabelPrefix + "ec2_" 43 ec2LabelAMI = ec2Label + "ami" 44 ec2LabelAZ = ec2Label + "availability_zone" 45 ec2LabelAZID = ec2Label + "availability_zone_id" 46 ec2LabelArch = ec2Label + "architecture" 47 ec2LabelIPv6Addresses = ec2Label + "ipv6_addresses" 48 ec2LabelInstanceID = ec2Label + "instance_id" 49 ec2LabelInstanceLifecycle = ec2Label + "instance_lifecycle" 50 ec2LabelInstanceState = ec2Label + "instance_state" 51 ec2LabelInstanceType = ec2Label + "instance_type" 52 ec2LabelOwnerID = ec2Label + "owner_id" 53 ec2LabelPlatform = ec2Label + "platform" 54 ec2LabelPrimarySubnetID = ec2Label + "primary_subnet_id" 55 ec2LabelPrivateDNS = ec2Label + "private_dns_name" 56 ec2LabelPrivateIP = ec2Label + "private_ip" 57 ec2LabelPublicDNS = ec2Label + "public_dns_name" 58 ec2LabelPublicIP = ec2Label + "public_ip" 59 ec2LabelSubnetID = ec2Label + "subnet_id" 60 ec2LabelTag = ec2Label + "tag_" 61 ec2LabelVPCID = ec2Label + "vpc_id" 62 ec2LabelSeparator = "," 63 ) 64 65 // DefaultEC2SDConfig is the default EC2 SD configuration. 66 var DefaultEC2SDConfig = EC2SDConfig{ 67 Port: 80, 68 RefreshInterval: 60 * time.Second, 69 } 70 71 func init() { 72 discovery.RegisterConfig(&EC2SDConfig{}) 73 } 74 75 // EC2Filter is the configuration for filtering EC2 instances. 76 type EC2Filter struct { 77 Name string `yaml:"name"` 78 Values []string `yaml:"values"` 79 } 80 81 // EC2SDConfig is the configuration for EC2 based service discovery. 82 type EC2SDConfig struct { 83 Endpoint string `yaml:"endpoint"` 84 Region string `yaml:"region"` 85 AccessKey string `yaml:"access-key,omitempty"` 86 SecretKey string `yaml:"secret-key,omitempty"` 87 Profile string `yaml:"profile,omitempty"` 88 RoleARN string `yaml:"role-arn,omitempty"` 89 Application string `yaml:"application,omitempty"` 90 RefreshInterval time.Duration `yaml:"refresh-interval,omitempty"` 91 Port int `yaml:"port"` 92 Filters []*EC2Filter `yaml:"filters"` 93 } 94 95 // Name returns the name of the EC2 Config. 96 func (*EC2SDConfig) Name() string { return "ec2" } 97 98 // NewDiscoverer returns a Discoverer for the EC2 Config. 99 func (c *EC2SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) { 100 return NewEC2Discovery(c, opts.Logger), nil 101 } 102 103 // UnmarshalYAML implements the yaml.Unmarshaler interface for the EC2 Config. 104 func (c *EC2SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { 105 *c = DefaultEC2SDConfig 106 type plain EC2SDConfig 107 err := unmarshal((*plain)(c)) 108 if err != nil { 109 return err 110 } 111 if c.Region == "" { 112 sess, err := session.NewSession() 113 if err != nil { 114 return err 115 } 116 metadata := ec2metadata.New(sess) 117 region, err := metadata.Region() 118 if err != nil { 119 return errors.New("EC2 SD configuration requires a region") 120 } 121 c.Region = region 122 } 123 for _, f := range c.Filters { 124 if len(f.Values) == 0 { 125 return errors.New("EC2 SD configuration filter values cannot be empty") 126 } 127 } 128 return nil 129 } 130 131 // EC2Discovery periodically performs EC2-SD requests. It implements 132 // the Discoverer interface. 133 type EC2Discovery struct { 134 *refresh.Discovery 135 logger logrus.FieldLogger 136 cfg *EC2SDConfig 137 ec2 *ec2.EC2 138 139 // azToAZID maps this account's availability zones to their underlying AZ 140 // ID, e.g. eu-west-2a -> euw2-az2. Refreshes are performed sequentially, so 141 // no locking is required. 142 azToAZID map[string]string 143 } 144 145 // NewEC2Discovery returns a new EC2Discovery which periodically refreshes its targets. 146 func NewEC2Discovery(conf *EC2SDConfig, logger logrus.FieldLogger) *EC2Discovery { 147 d := &EC2Discovery{ 148 logger: logger, 149 cfg: conf, 150 } 151 d.Discovery = refresh.NewDiscovery( 152 logger, 153 "ec2", 154 d.cfg.RefreshInterval, 155 d.refresh, 156 ) 157 return d 158 } 159 160 func (d *EC2Discovery) ec2Client(_ context.Context) (*ec2.EC2, error) { 161 if d.ec2 != nil { 162 return d.ec2, nil 163 } 164 165 creds := credentials.NewStaticCredentials(d.cfg.AccessKey, d.cfg.SecretKey, "") 166 if d.cfg.AccessKey == "" && d.cfg.SecretKey == "" { 167 creds = nil 168 } 169 170 sess, err := session.NewSessionWithOptions(session.Options{ 171 Config: aws.Config{ 172 Endpoint: &d.cfg.Endpoint, 173 Region: &d.cfg.Region, 174 Credentials: creds, 175 }, 176 SharedConfigState: session.SharedConfigEnable, 177 Profile: d.cfg.Profile, 178 }) 179 if err != nil { 180 return nil, errors.Wrap(err, "could not create aws session") 181 } 182 183 if d.cfg.RoleARN != "" { 184 creds := stscreds.NewCredentials(sess, d.cfg.RoleARN) 185 d.ec2 = ec2.New(sess, &aws.Config{Credentials: creds}) 186 } else { 187 d.ec2 = ec2.New(sess) 188 } 189 190 return d.ec2, nil 191 } 192 193 func (d *EC2Discovery) refreshAZIDs(ctx context.Context) error { 194 azs, err := d.ec2.DescribeAvailabilityZonesWithContext(ctx, &ec2.DescribeAvailabilityZonesInput{}) 195 if err != nil { 196 return err 197 } 198 d.azToAZID = make(map[string]string, len(azs.AvailabilityZones)) 199 for _, az := range azs.AvailabilityZones { 200 d.azToAZID[*az.ZoneName] = *az.ZoneId 201 } 202 return nil 203 } 204 205 func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { 206 ec2Client, err := d.ec2Client(ctx) 207 if err != nil { 208 return nil, err 209 } 210 211 tg := &targetgroup.Group{ 212 Source: d.cfg.Region, 213 } 214 215 var filters []*ec2.Filter 216 for _, f := range d.cfg.Filters { 217 filters = append(filters, &ec2.Filter{ 218 Name: aws.String(f.Name), 219 Values: aws.StringSlice(f.Values), 220 }) 221 } 222 223 // Only refresh the AZ ID map if we have never been able to build one. 224 // Prometheus requires a reload if AWS adds a new AZ to the region. 225 if d.azToAZID == nil { 226 if err := d.refreshAZIDs(ctx); err != nil { 227 d.logger.WithError(err).Debug("Unable to describe availability zones") 228 } 229 } 230 231 input := &ec2.DescribeInstancesInput{Filters: filters} 232 if err := ec2Client.DescribeInstancesPagesWithContext(ctx, input, func(p *ec2.DescribeInstancesOutput, lastPage bool) bool { 233 for _, r := range p.Reservations { 234 for _, inst := range r.Instances { 235 if inst.PrivateIpAddress == nil { 236 continue 237 } 238 239 labels := model.LabelSet{ 240 ec2LabelInstanceID: model.LabelValue(*inst.InstanceId), 241 model.AppNameLabel: model.LabelValue(d.cfg.Application), 242 } 243 244 if r.OwnerId != nil { 245 labels[ec2LabelOwnerID] = model.LabelValue(*r.OwnerId) 246 } 247 248 labels[ec2LabelPrivateIP] = model.LabelValue(*inst.PrivateIpAddress) 249 if inst.PrivateDnsName != nil { 250 labels[ec2LabelPrivateDNS] = model.LabelValue(*inst.PrivateDnsName) 251 } 252 addr := net.JoinHostPort(*inst.PrivateIpAddress, fmt.Sprintf("%d", d.cfg.Port)) 253 labels[model.AddressLabel] = model.LabelValue(addr) 254 255 if inst.Platform != nil { 256 labels[ec2LabelPlatform] = model.LabelValue(*inst.Platform) 257 } 258 259 if inst.PublicIpAddress != nil { 260 labels[ec2LabelPublicIP] = model.LabelValue(*inst.PublicIpAddress) 261 labels[ec2LabelPublicDNS] = model.LabelValue(*inst.PublicDnsName) 262 } 263 labels[ec2LabelAMI] = model.LabelValue(*inst.ImageId) 264 labels[ec2LabelAZ] = model.LabelValue(*inst.Placement.AvailabilityZone) 265 azID, ok := d.azToAZID[*inst.Placement.AvailabilityZone] 266 if !ok && d.azToAZID != nil { 267 d.logger.WithField("az", *inst.Placement.AvailabilityZone).Debug("Availability zone ID not found") 268 } 269 labels[ec2LabelAZID] = model.LabelValue(azID) 270 labels[ec2LabelInstanceState] = model.LabelValue(*inst.State.Name) 271 labels[ec2LabelInstanceType] = model.LabelValue(*inst.InstanceType) 272 273 if inst.InstanceLifecycle != nil { 274 labels[ec2LabelInstanceLifecycle] = model.LabelValue(*inst.InstanceLifecycle) 275 } 276 277 if inst.Architecture != nil { 278 labels[ec2LabelArch] = model.LabelValue(*inst.Architecture) 279 } 280 281 if inst.VpcId != nil { 282 labels[ec2LabelVPCID] = model.LabelValue(*inst.VpcId) 283 labels[ec2LabelPrimarySubnetID] = model.LabelValue(*inst.SubnetId) 284 285 var subnets []string 286 var ipv6addrs []string 287 subnetsMap := make(map[string]struct{}) 288 for _, eni := range inst.NetworkInterfaces { 289 if eni.SubnetId == nil { 290 continue 291 } 292 // Deduplicate VPC Subnet IDs maintaining the order of the subnets returned by EC2. 293 if _, ok := subnetsMap[*eni.SubnetId]; !ok { 294 subnetsMap[*eni.SubnetId] = struct{}{} 295 subnets = append(subnets, *eni.SubnetId) 296 } 297 298 for _, ipv6addr := range eni.Ipv6Addresses { 299 ipv6addrs = append(ipv6addrs, *ipv6addr.Ipv6Address) 300 } 301 } 302 labels[ec2LabelSubnetID] = model.LabelValue( 303 ec2LabelSeparator + 304 strings.Join(subnets, ec2LabelSeparator) + 305 ec2LabelSeparator) 306 if len(ipv6addrs) > 0 { 307 labels[ec2LabelIPv6Addresses] = model.LabelValue( 308 ec2LabelSeparator + 309 strings.Join(ipv6addrs, ec2LabelSeparator) + 310 ec2LabelSeparator) 311 } 312 } 313 314 for _, t := range inst.Tags { 315 if t == nil || t.Key == nil || t.Value == nil { 316 continue 317 } 318 name := strutil.SanitizeLabelName(*t.Key) 319 labels[ec2LabelTag+model.LabelName(name)] = model.LabelValue(*t.Value) 320 } 321 tg.Targets = append(tg.Targets, labels) 322 } 323 } 324 return true 325 }); err != nil { 326 if awsErr, ok := err.(awserr.Error); ok && (awsErr.Code() == "AuthFailure" || awsErr.Code() == "UnauthorizedOperation") { 327 d.ec2 = nil 328 } 329 return nil, errors.Wrap(err, "could not describe instances") 330 } 331 return []*targetgroup.Group{tg}, nil 332 }