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  }