diff --git a/go.mod b/go.mod index 3ca616ec68..2b3a441dbf 100644 --- a/go.mod +++ b/go.mod @@ -10,13 +10,14 @@ require ( github.com/DataDog/datadog-api-client-go v1.0.0-beta.16 github.com/Masterminds/sprig/v3 v3.2.2 github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46 - github.com/aws/aws-sdk-go-v2 v1.17.7 + github.com/aws/aws-sdk-go-v2 v1.23.0 github.com/aws/aws-sdk-go-v2/config v1.18.19 github.com/aws/aws-sdk-go-v2/credentials v1.13.18 github.com/aws/aws-sdk-go-v2/service/ecs v1.24.2 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.19.7 github.com/aws/aws-sdk-go-v2/service/lambda v1.30.2 github.com/aws/aws-sdk-go-v2/service/s3 v1.31.0 + github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.26.2 github.com/creasty/defaults v1.6.0 github.com/envoyproxy/protoc-gen-validate v0.10.1 github.com/fsouza/fake-gcs-server v1.21.0 @@ -82,8 +83,8 @@ require ( github.com/apparentlymart/go-textseg v1.0.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect @@ -93,7 +94,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 // indirect - github.com/aws/smithy-go v1.13.5 // indirect + github.com/aws/smithy-go v1.17.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/go.sum b/go.sum index 177423350d..3d8864c134 100644 --- a/go.sum +++ b/go.sum @@ -113,8 +113,9 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aslakhellesoy/gox v1.0.100/go.mod h1:AJl542QsKKG96COVsv0N74HHzVQgDIQPceVUh1aeU2M= -github.com/aws/aws-sdk-go-v2 v1.17.7 h1:CLSjnhJSTSogvqUGhIC6LqFKATMRexcxLZ0i/Nzk9Eg= github.com/aws/aws-sdk-go-v2 v1.17.7/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= +github.com/aws/aws-sdk-go-v2 v1.23.0 h1:PiHAzmiQQr6JULBUdvR8fKlA+UPKLT/8KbiqpFBWiAo= +github.com/aws/aws-sdk-go-v2 v1.23.0/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno= github.com/aws/aws-sdk-go-v2/config v1.18.19 h1:AqFK6zFNtq4i1EYu+eC7lcKHYnZagMn6SW171la0bGw= @@ -123,10 +124,12 @@ github.com/aws/aws-sdk-go-v2/credentials v1.13.18 h1:EQMdtHwz0ILTW1hoP+EwuWhwCG1 github.com/aws/aws-sdk-go-v2/credentials v1.13.18/go.mod h1:vnwlwjIe+3XJPBYKu1et30ZPABG3VaXJYr8ryohpIyM= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 h1:gt57MN3liKiyGopcqgNzJb2+d9MJaKT/q1OksHNXVE4= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1/go.mod h1:lfUx8puBRdM5lVVMQlwt2v+ofiG/X6Ms+dy0UkG/kXw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 h1:sJLYcS+eZn5EeNINGHSCRAwUJMFVqklwkH36Vbyai7M= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31/go.mod h1:QT0BqUvX1Bh2ABdTGnjqEjvjzrCfIniM9Sc8zn9Yndo= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 h1:1mnRASEKnkqsntcxHaysxwgVoUUp5dkiB+l3llKnqyg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3 h1:DUwbD79T8gyQ23qVXFUthjzVMTviSHi3y4z58KvghhM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.3/go.mod h1:7sGSz1JCKHWWBHq98m6sMtWQikmYPpxjqOydDemiVoM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25/go.mod h1:zBHOPwhBc3FlQjQJE/D3IfPWiWaQmT06Vq9aNukDo0k= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3 h1:AplLJCtIaUZDCbr6+gLYdsYNxne4iuaboJhVt9d+WXI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.3/go.mod h1:ify42Rb7nKeDDPkFjKn7q1bPscVPu/+gmHH8d2c+anU= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 h1:p5luUImdIqywn6JpQsW3tq5GNOxKmOnEpybzPx+d1lk= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32/go.mod h1:XGhIBZDEgfqmFIugclZ6FU7v75nHhBDtzuB4xB/tEi4= github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 h1:DWYZIsyqagnWL00f8M/SOr9fN063OEQWn9LLTbdYXsk= @@ -147,14 +150,17 @@ github.com/aws/aws-sdk-go-v2/service/lambda v1.30.2 h1:JEUEgBM8HZ27ahhZsIlgfj7xP github.com/aws/aws-sdk-go-v2/service/lambda v1.30.2/go.mod h1:PmNd6f36wPbp2+B3ZSuvHqqSwggfagEdI18tIb8s91o= github.com/aws/aws-sdk-go-v2/service/s3 v1.31.0 h1:B1G2pSPvbAtQjilPq+Y7jLIzCOwKzuVEl+aBBaNG0AQ= github.com/aws/aws-sdk-go-v2/service/s3 v1.31.0/go.mod h1:ncltU6n4Nof5uJttDtcNQ537uNuwYqsZZQcpkd2/GUQ= +github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.26.2 h1:Cq/W87yJuXpGahxRyX+1jo/ChzwI6/fBnC+gE0o2WJk= +github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.26.2/go.mod h1:m8lJjajGWGC1CSllESM3kez9431m+bRpEmkfiyuIsiQ= github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 h1:5V7DWLBd7wTELVz5bPpwzYy/sikk0gsgZfj40X+l5OI= github.com/aws/aws-sdk-go-v2/service/sso v1.12.6/go.mod h1:Y1VOmit/Fn6Tz1uFAeCO6Q7M2fmfXSCLeL5INVYsLuY= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 h1:B8cauxOH1W1v7rd8RdI/MWnoR4Ze0wIHWrb90qczxj4= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6/go.mod h1:Lh/bc9XUf8CfOY6Jp5aIkQtN+j1mc+nExc+KXj9jx2s= github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 h1:bWNgNdRko2x6gqa0blfATqAZKZokPIeM1vfmQt2pnvM= github.com/aws/aws-sdk-go-v2/service/sts v1.18.7/go.mod h1:JuTnSoeePXmMVe9G8NcjjwgOKEfZ4cOjMuT2IBT/2eI= -github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/aws/smithy-go v1.17.0 h1:wWJD7LX6PBV6etBUwO0zElG0nWN9rUhp0WdYeHSHAaI= +github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/pkg/app/piped/executor/ecs/deploy.go b/pkg/app/piped/executor/ecs/deploy.go index ca40ecabdc..31da87edb6 100644 --- a/pkg/app/piped/executor/ecs/deploy.go +++ b/pkg/app/piped/executor/ecs/deploy.go @@ -17,6 +17,7 @@ package ecs import ( "context" + "github.com/aws/aws-sdk-go-v2/service/ecs/types" "github.com/pipe-cd/pipecd/pkg/app/piped/deploysource" "github.com/pipe-cd/pipecd/pkg/app/piped/executor" "github.com/pipe-cd/pipecd/pkg/config" @@ -97,9 +98,13 @@ func (e *deployExecutor) ensureSync(ctx context.Context) model.StageStatus { return model.StageStatus_STAGE_FAILURE } - primary, _, ok := loadTargetGroups(&e.Input, e.appCfg, e.deploySource) - if !ok { - return model.StageStatus_STAGE_FAILURE + // When the service is not under ELB, target groups are not used. + var primary *types.LoadBalancer = nil + if e.appCfg.Input.IsAccessedViaELB() { + primary, _, ok = loadTargetGroups(&e.Input, e.appCfg, e.deploySource) + if !ok { + return model.StageStatus_STAGE_FAILURE + } } recreate := e.appCfg.QuickSync.Recreate @@ -115,7 +120,7 @@ func (e *deployExecutor) ensurePrimaryRollout(ctx context.Context) model.StageSt if !ok { return model.StageStatus_STAGE_FAILURE } - servicedefinition, ok := loadServiceDefinition(&e.Input, e.appCfg.Input.ServiceDefinitionFile, e.deploySource) + serviceDefinition, ok := loadServiceDefinition(&e.Input, e.appCfg.Input.ServiceDefinitionFile, e.deploySource) if !ok { return model.StageStatus_STAGE_FAILURE } @@ -129,7 +134,7 @@ func (e *deployExecutor) ensurePrimaryRollout(ctx context.Context) model.StageSt return model.StageStatus_STAGE_FAILURE } - if !rollout(ctx, &e.Input, e.platformProviderName, e.platformProviderCfg, taskDefinition, servicedefinition, primary) { + if !rollout(ctx, &e.Input, e.platformProviderName, e.platformProviderCfg, taskDefinition, serviceDefinition, primary) { return model.StageStatus_STAGE_FAILURE } @@ -141,7 +146,7 @@ func (e *deployExecutor) ensureCanaryRollout(ctx context.Context) model.StageSta if !ok { return model.StageStatus_STAGE_FAILURE } - servicedefinition, ok := loadServiceDefinition(&e.Input, e.appCfg.Input.ServiceDefinitionFile, e.deploySource) + serviceDefinition, ok := loadServiceDefinition(&e.Input, e.appCfg.Input.ServiceDefinitionFile, e.deploySource) if !ok { return model.StageStatus_STAGE_FAILURE } @@ -155,7 +160,7 @@ func (e *deployExecutor) ensureCanaryRollout(ctx context.Context) model.StageSta return model.StageStatus_STAGE_FAILURE } - if !rollout(ctx, &e.Input, e.platformProviderName, e.platformProviderCfg, taskDefinition, servicedefinition, canary) { + if !rollout(ctx, &e.Input, e.platformProviderName, e.platformProviderCfg, taskDefinition, serviceDefinition, canary) { return model.StageStatus_STAGE_FAILURE } diff --git a/pkg/app/piped/platformprovider/ecs/client.go b/pkg/app/piped/platformprovider/ecs/client.go index 3b16819f80..5732469341 100644 --- a/pkg/app/piped/platformprovider/ecs/client.go +++ b/pkg/app/piped/platformprovider/ecs/client.go @@ -18,6 +18,7 @@ import ( "context" "errors" "fmt" + "strings" "time" "github.com/aws/aws-sdk-go-v2/aws" @@ -27,6 +28,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ecs/types" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" elbtypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" + "github.com/aws/aws-sdk-go-v2/service/servicediscovery" + "go.uber.org/zap" "github.com/pipe-cd/pipecd/pkg/app/piped/platformprovider" @@ -47,6 +50,7 @@ const ( type client struct { ecsClient *ecs.Client elbClient *elasticloadbalancingv2.Client + sdClient *servicediscovery.Client logger *zap.Logger } @@ -79,6 +83,7 @@ func newClient(region, profile, credentialsFile, roleARN, tokenPath string, logg } c.ecsClient = ecs.NewFromConfig(cfg) c.elbClient = elasticloadbalancingv2.NewFromConfig(cfg) + c.sdClient = servicediscovery.NewFromConfig(cfg) return c, nil } @@ -481,3 +486,86 @@ func (c *client) TagResource(ctx context.Context, resourceArn string, tags []typ } return nil } + +func (c *client) deregisterInstanceFromServiceDiscovery(ctx context.Context, serviceId string, instanceId string) error { + input := &servicediscovery.DeregisterInstanceInput{ + ServiceId: aws.String(serviceId), + InstanceId: aws.String(instanceId), + } + _, err := c.sdClient.DeregisterInstance(ctx, input) + if err != nil { + return fmt.Errorf("failed to deregister ECS instance(%s) of service(%s) from Service Discovery: %w", instanceId, serviceId, err) + } + return nil +} + +func (c *client) registerInstanceToServiceDiscovery(ctx context.Context, serviceId string, serviceName string, taskArn string) error { + clusterName := clusterName(taskArn) + input1 := &ecs.DescribeTasksInput{ + Cluster: aws.String(clusterName), + Tasks: []string{taskArn}, + } + output, err := c.ecsClient.DescribeTasks(ctx, input1) + if err != nil || len(output.Tasks) == 0 { + return fmt.Errorf("failed to describe ECS task(%s): %w", taskArn, err) + } + + task := output.Tasks[0] + taskId := afterLastSlash(*task.TaskArn) + + ipv4, err := ipv4OfTask(task) + if err != nil { + return fmt.Errorf("failed to get privateIPv4Address of task(%s): %w", taskId, err) + } + + input := &servicediscovery.RegisterInstanceInput{ + ServiceId: aws.String(serviceId), + InstanceId: aws.String(taskId), + Attributes: map[string]string{ + "AVAILABILITY_ZONE": *task.AvailabilityZone, + "AWS_INIT_HEALTH_STATUS": "HEALTHY", // the default status in SDK + "AWS_INSTANCE_IPV4": *ipv4, + "ECS_CLUSTER_NAME": clusterName, + "ECS_SERVICE_NAME": serviceName, + "ECS_TASK_DEFINITION_FAMILY": afterLastSlash(*task.TaskDefinitionArn), + "REGION": regionName(taskArn), + }, + } + + _, err = c.sdClient.RegisterInstance(ctx, input) + if err != nil { + return fmt.Errorf("failed to register ECS instance(%s) of service(%s) to Service Discovery: %w", taskId, serviceId, err) + } + return nil +} + +// HACK: it depends on the ARN structure. It may be changed in the future. +func afterLastSlash(str string) string { + strSplit := strings.Split(str, "/") + return strSplit[len(strSplit)-1] +} + +// HACK: it depends on the ARN structure. It may be changed in the future. +func clusterName(taskArn string) string { + strSplit := strings.Split(taskArn, "/") + return strSplit[len(strSplit)-2] +} + +// HACK: it depends on the ARN structure. It may be changed in the future. +func regionName(taskArn string) string { + return strings.Split(taskArn, "/")[3] +} + +// HACK: it depends on the Task's Attatchments and Details structures. It may be changed in the future. +func ipv4OfTask(t types.Task) (*string, error) { + if len(t.Attachments) == 0 { + return nil, fmt.Errorf("failed to get privateIPv4Address of task(%s): the task has no attatchments", *t.TaskArn) + } + + for _, detail := range t.Attachments[0].Details { + if *detail.Name == "privateIPv4Address" { + return detail.Value, nil + } + } + return nil, fmt.Errorf("failed to get privateIPv4Address of task(%s): the task has no details of 'privateIPv4Address'", *t.TaskArn) +} diff --git a/pkg/config/application_ecs.go b/pkg/config/application_ecs.go index fb0e5e72d3..56213a9667 100644 --- a/pkg/config/application_ecs.go +++ b/pkg/config/application_ecs.go @@ -57,12 +57,23 @@ type ECSDeploymentInput struct { // Run standalone task during deployment. // Default is true. RunStandaloneTask *bool `json:"runStandaloneTask" default:"true"` + // How the ECS service is accessed. + // Default is ELB. + AccessedVia string `json:"AccessedVia" default:"ELB"` } func (in *ECSDeploymentInput) IsStandaloneTask() bool { return in.ServiceDefinitionFile == "" } +func (in *ECSDeploymentInput) IsAccessedViaELB() bool { + return in.AccessedVia == "ELB" +} + +func (in *ECSDeploymentInput) IsAccessedViaServiceDiscovery() bool { + return in.AccessedVia == "SERVICE_DISCOVERY" +} + type ECSVpcConfiguration struct { Subnets []string AssignPublicIP string