Browse Source

[awslogs] Auto-detect region on EC2 instances

Signed-off-by: Samuel Karp <skarp@amazon.com>
Samuel Karp 9 năm trước cách đây
mục cha
commit
8a6dfb26f3

+ 39 - 10
daemon/logger/awslogs/cloudwatchlogs.go

@@ -2,6 +2,7 @@
 package awslogs
 package awslogs
 
 
 import (
 import (
+	"errors"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 	"runtime"
 	"runtime"
@@ -14,6 +15,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/aws/awserr"
 	"github.com/aws/aws-sdk-go/aws/defaults"
 	"github.com/aws/aws-sdk-go/aws/defaults"
+	"github.com/aws/aws-sdk-go/aws/ec2metadata"
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
 	"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
 	"github.com/docker/docker/daemon/logger"
 	"github.com/docker/docker/daemon/logger"
@@ -58,6 +60,10 @@ type api interface {
 	PutLogEvents(*cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error)
 	PutLogEvents(*cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error)
 }
 }
 
 
+type regionFinder interface {
+	Region() (string, error)
+}
+
 type byTimestamp []*cloudwatchlogs.InputLogEvent
 type byTimestamp []*cloudwatchlogs.InputLogEvent
 
 
 // init registers the awslogs driver and sets the default region, if provided
 // init registers the awslogs driver and sets the default region, if provided
@@ -85,13 +91,17 @@ func New(ctx logger.Context) (logger.Logger, error) {
 	if ctx.Config[logStreamKey] != "" {
 	if ctx.Config[logStreamKey] != "" {
 		logStreamName = ctx.Config[logStreamKey]
 		logStreamName = ctx.Config[logStreamKey]
 	}
 	}
+	client, err := newAWSLogsClient(ctx)
+	if err != nil {
+		return nil, err
+	}
 	containerStream := &logStream{
 	containerStream := &logStream{
 		logStreamName: logStreamName,
 		logStreamName: logStreamName,
 		logGroupName:  logGroupName,
 		logGroupName:  logGroupName,
-		client:        newAWSLogsClient(ctx),
+		client:        client,
 		messages:      make(chan *logger.Message, 4096),
 		messages:      make(chan *logger.Message, 4096),
 	}
 	}
-	err := containerStream.create()
+	err = containerStream.create()
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -100,13 +110,38 @@ func New(ctx logger.Context) (logger.Logger, error) {
 	return containerStream, nil
 	return containerStream, nil
 }
 }
 
 
-func newAWSLogsClient(ctx logger.Context) api {
+// newRegionFinder is a variable such that the implementation
+// can be swapped out for unit tests.
+var newRegionFinder = func() regionFinder {
+	return ec2metadata.New(nil)
+}
+
+// newAWSLogsClient creates the service client for Amazon CloudWatch Logs.
+// Customizations to the default client from the SDK include a Docker-specific
+// User-Agent string and automatic region detection using the EC2 Instance
+// Metadata Service when region is otherwise unspecified.
+func newAWSLogsClient(ctx logger.Context) (api, error) {
 	config := defaults.DefaultConfig
 	config := defaults.DefaultConfig
 	if ctx.Config[regionKey] != "" {
 	if ctx.Config[regionKey] != "" {
 		config = defaults.DefaultConfig.Merge(&aws.Config{
 		config = defaults.DefaultConfig.Merge(&aws.Config{
 			Region: aws.String(ctx.Config[regionKey]),
 			Region: aws.String(ctx.Config[regionKey]),
 		})
 		})
 	}
 	}
+	if config.Region == nil || *config.Region == "" {
+		logrus.Info("Trying to get region from EC2 Metadata")
+		ec2MetadataClient := newRegionFinder()
+		region, err := ec2MetadataClient.Region()
+		if err != nil {
+			logrus.WithFields(logrus.Fields{
+				"error": err,
+			}).Error("Could not get region from EC2 metadata, environment, or log option")
+			return nil, errors.New("Cannot determine region for awslogs driver")
+		}
+		config.Region = &region
+	}
+	logrus.WithFields(logrus.Fields{
+		"region": *config.Region,
+	}).Debug("Created awslogs client")
 	client := cloudwatchlogs.New(config)
 	client := cloudwatchlogs.New(config)
 
 
 	client.Handlers.Build.PushBackNamed(request.NamedHandler{
 	client.Handlers.Build.PushBackNamed(request.NamedHandler{
@@ -118,7 +153,7 @@ func newAWSLogsClient(ctx logger.Context) api {
 					version.VERSION, runtime.GOOS, currentAgent))
 					version.VERSION, runtime.GOOS, currentAgent))
 		},
 		},
 	})
 	})
-	return client
+	return client, nil
 }
 }
 
 
 // Name returns the name of the awslogs logging driver
 // Name returns the name of the awslogs logging driver
@@ -312,12 +347,6 @@ func ValidateLogOpt(cfg map[string]string) error {
 	if cfg[logGroupKey] == "" {
 	if cfg[logGroupKey] == "" {
 		return fmt.Errorf("must specify a value for log opt '%s'", logGroupKey)
 		return fmt.Errorf("must specify a value for log opt '%s'", logGroupKey)
 	}
 	}
-	if cfg[regionKey] == "" && os.Getenv(regionEnvKey) == "" {
-		return fmt.Errorf(
-			"must specify a value for environment variable '%s' or log opt '%s'",
-			regionEnvKey,
-			regionKey)
-	}
 	return nil
 	return nil
 }
 }
 
 

+ 23 - 1
daemon/logger/awslogs/cloudwatchlogs_test.go

@@ -32,7 +32,10 @@ func TestNewAWSLogsClientUserAgentHandler(t *testing.T) {
 		},
 		},
 	}
 	}
 
 
-	client := newAWSLogsClient(ctx)
+	client, err := newAWSLogsClient(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
 	realClient, ok := client.(*cloudwatchlogs.CloudWatchLogs)
 	realClient, ok := client.(*cloudwatchlogs.CloudWatchLogs)
 	if !ok {
 	if !ok {
 		t.Fatal("Could not cast client to cloudwatchlogs.CloudWatchLogs")
 		t.Fatal("Could not cast client to cloudwatchlogs.CloudWatchLogs")
@@ -53,6 +56,25 @@ func TestNewAWSLogsClientUserAgentHandler(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestNewAWSLogsClientRegionDetect(t *testing.T) {
+	ctx := logger.Context{
+		Config: map[string]string{},
+	}
+
+	mockMetadata := newMockMetadataClient()
+	newRegionFinder = func() regionFinder {
+		return mockMetadata
+	}
+	mockMetadata.regionResult <- &regionResult{
+		successResult: "us-east-1",
+	}
+
+	_, err := newAWSLogsClient(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
 func TestCreateSuccess(t *testing.T) {
 func TestCreateSuccess(t *testing.T) {
 	mockClient := newMockClient()
 	mockClient := newMockClient()
 	stream := &logStream{
 	stream := &logStream{

+ 20 - 0
daemon/logger/awslogs/cwlogsiface_mock_test.go

@@ -49,6 +49,26 @@ func (m *mockcwlogsclient) PutLogEvents(input *cloudwatchlogs.PutLogEventsInput)
 	return output.successResult, output.errorResult
 	return output.successResult, output.errorResult
 }
 }
 
 
+type mockmetadataclient struct {
+	regionResult chan *regionResult
+}
+
+type regionResult struct {
+	successResult string
+	errorResult   error
+}
+
+func newMockMetadataClient() *mockmetadataclient {
+	return &mockmetadataclient{
+		regionResult: make(chan *regionResult, 1),
+	}
+}
+
+func (m *mockmetadataclient) Region() (string, error) {
+	output := <-m.regionResult
+	return output.successResult, output.errorResult
+}
+
 func test() {
 func test() {
 	_ = &logStream{
 	_ = &logStream{
 		client: newMockClient(),
 		client: newMockClient(),

+ 4 - 3
docs/reference/logging/awslogs.md

@@ -34,9 +34,10 @@ You can use the `--log-opt NAME=VALUE` flag to specify Amazon CloudWatch Logs lo
 
 
 ### awslogs-region
 ### awslogs-region
 
 
-You must specify a region for the `awslogs` logging driver. You can specify the
-region with either the `awslogs-region` log option or `AWS_REGION` environment
-variable:
+The `awslogs` logging driver sends your Docker logs to a specific region. Use
+the `awslogs-region` log option or the `AWS_REGION` environment variable to set
+the region.  By default, if your Docker daemon is running on an EC2 instance
+and no region is set, the driver uses the instance's region.
 
 
     docker run --log-driver=awslogs --log-opt awslogs-region=us-east-1 ...
     docker run --log-driver=awslogs --log-opt awslogs-region=us-east-1 ...