diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index daf11ac0..75b6c2b7 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -76,10 +76,10 @@ func ComposeCommand(clusteropts *clusterOptions) *cobra.Command { return cmd } -func UpCommand(clusteropts *clusterOptions, opts *compose.ProjectOptions) *cobra.Command { +func UpCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { cmd := &cobra.Command{ Use: "up", - RunE: compose.WithProject(opts, func(project *compose.Project, args []string) error { + RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) if err != nil { return err diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/client.go index 8e98a55f..247536a9 100644 --- a/ecs/pkg/amazon/client.go +++ b/ecs/pkg/amazon/client.go @@ -6,6 +6,7 @@ import ( "github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/elbv2" "github.com/aws/aws-sdk-go/service/iam" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -31,6 +32,7 @@ func NewClient(profile string, cluster string, region string) (compose.API, erro sess: sess, ECS: ecs.New(sess), EC2: ec2.New(sess), + ELB: elbv2.New(sess), CW: cloudwatchlogs.New(sess), IAM: iam.New(sess), }, nil @@ -42,6 +44,7 @@ type client struct { sess *session.Session ECS *ecs.ECS EC2 *ec2.EC2 + ELB *elbv2.ELBV2 CW *cloudwatchlogs.CloudWatchLogs IAM *iam.IAM } diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index 611d84f4..e68c3132 100644 --- a/ecs/pkg/amazon/down.go +++ b/ecs/pkg/amazon/down.go @@ -9,6 +9,11 @@ import ( ) func (c *client) ComposeDown(project *compose.Project) error { + err := c.DeleteLoadBalancer(project) + if err != nil { + return err + } + services := []*string{} // FIXME we should be able to retrieve services by tags, so we don't need the initial compose file to run "down" for _, service := range project.Services { @@ -23,18 +28,17 @@ func (c *client) ComposeDown(project *compose.Project) error { if err != nil { return err } - - logrus.Debugf("Service deleted %q\n", *out.Service.ServiceName) - services = append(services, out.Service.ServiceName) + services = append(services, out.Service.ServiceArn) } - logrus.Info("All services stopped") - err := c.ECS.WaitUntilServicesInactive(&ecs.DescribeServicesInput{ + logrus.Info("Stopping services...") + err = c.ECS.WaitUntilServicesInactive(&ecs.DescribeServicesInput{ Services: services, }) if err != nil { return err } + logrus.Info("All services stopped") logrus.Debug("Deleting security groups") groups, err := c.EC2.DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{ diff --git a/ecs/pkg/amazon/loadBalancer.go b/ecs/pkg/amazon/loadBalancer.go new file mode 100644 index 00000000..6edc7955 --- /dev/null +++ b/ecs/pkg/amazon/loadBalancer.go @@ -0,0 +1,133 @@ +package amazon + +import ( + "fmt" + "github.com/docker/ecs-plugin/pkg/compose" + "github.com/sirupsen/logrus" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/compose-spec/compose-go/types" +) + +func (c client) CreateLoadBalancer(project *compose.Project, subnets []*string) (*string, error) { + logrus.Debug("Create Load Balancer") + alb, err := c.ELB.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{ + IpAddressType: nil, + Name: aws.String(fmt.Sprintf("%s-LoadBalancer", project.Name)), + Subnets: subnets, + Type: aws.String(elbv2.LoadBalancerTypeEnumNetwork), + Tags: []*elbv2.Tag{ + { + Key: aws.String("com.docker.compose.project"), + Value: aws.String(project.Name), + }, + }, + }) + if err != nil { + return nil, err + } + return alb.LoadBalancers[0].LoadBalancerArn, nil +} + +func (c client) DeleteLoadBalancer(project *compose.Project) error { + logrus.Debug("Delete Load Balancer") + // FIXME We can tag LoadBalancer but not search by tag ? + loadBalancer, err := c.ELB.DescribeLoadBalancers(&elbv2.DescribeLoadBalancersInput{ + Names: aws.StringSlice([]string{fmt.Sprintf("%s-LoadBalancer", project.Name)}), + }) + if err != nil { + return err + } + arn := loadBalancer.LoadBalancers[0].LoadBalancerArn + + err = c.DeleteListeners(arn) + if err != nil { + return err + } + + err = c.DeleteTargetGroups(arn) + if err != nil { + return err + } + + _, err = c.ELB.DeleteLoadBalancer(&elbv2.DeleteLoadBalancerInput{LoadBalancerArn: arn}) + return err +} + +func (c client) CreateTargetGroup(name string, vpc *string, port types.ServicePortConfig) (*string, error) { + logrus.Debugf("Create Target Group %d/%s\n", port.Target, port.Protocol) + group, err := c.ELB.CreateTargetGroup(&elbv2.CreateTargetGroupInput{ + Name: aws.String(name), + Port: aws.Int64(int64(port.Target)), + Protocol: aws.String(strings.ToUpper(port.Protocol)), + TargetType: aws.String("ip"), + VpcId: vpc, + }) + if err != nil { + return nil, err + } + arn := group.TargetGroups[0].TargetGroupArn + return arn, nil +} + +func (c client) DeleteTargetGroups(loadBalancer *string) error { + groups, err := c.ELB.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{ + LoadBalancerArn: loadBalancer, + }) + if err != nil { + return err + } + for _, group := range groups.TargetGroups { + logrus.Debugf("Delete Target Group %s\n", *group.TargetGroupArn) + _, err := c.ELB.DeleteTargetGroup(&elbv2.DeleteTargetGroupInput{ + TargetGroupArn: group.TargetGroupArn, + }) + if err != nil { + return err + } + } + return nil +} + +func (c client) CreateListener(port types.ServicePortConfig, arn *string, target *string) error { + logrus.Debugf("Create Listener %d\n", port.Published) + _, err := c.ELB.CreateListener(&elbv2.CreateListenerInput{ + DefaultActions: []*elbv2.Action{ + { + ForwardConfig: &elbv2.ForwardActionConfig{ + TargetGroups: []*elbv2.TargetGroupTuple{ + { + TargetGroupArn: target, + }, + }, + }, + Type: aws.String(elbv2.ActionTypeEnumForward), + }, + }, + LoadBalancerArn: arn, + Port: aws.Int64(int64(port.Published)), + Protocol: aws.String(strings.ToUpper(port.Protocol)), + }) + return err +} + +func (c client) DeleteListeners(loadBalancer *string) error { + listeners, err := c.ELB.DescribeListeners(&elbv2.DescribeListenersInput{ + LoadBalancerArn: loadBalancer, + }) + if err != nil { + return err + } + for _, listener := range listeners.Listeners { + logrus.Debugf("Delete Listener %s\n", *listener.ListenerArn) + _, err := c.ELB.DeleteListener(&elbv2.DeleteListenerInput{ + ListenerArn: listener.ListenerArn, + }) + if err != nil { + return err + } + } + return nil +} \ No newline at end of file diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index e54aae14..d0b84b2a 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -1,6 +1,7 @@ package amazon import ( + "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ecs" "github.com/compose-spec/compose-go/types" @@ -40,13 +41,37 @@ func (c *client) ComposeUp(project *compose.Project) error { return err } + loadBalancer, err := c.CreateLoadBalancer(project, subnets) + if err != nil { + return err + } + logGroup, err := c.GetOrCreateLogGroup(project) if err != nil { return err } for _, mapping := range mappings { - _, err = c.CreateService(project, mapping.service, mapping.task, securityGroup, subnets, logGroup) + ingress := []*ecs.LoadBalancer{} + for _, port := range mapping.service.Ports { + name := fmt.Sprintf("%s-%s-%d-%s", project.Name, mapping.service.Name, port.Target, port.Protocol) + targetgroup, err := c.CreateTargetGroup(name, vpc, port) + if err != nil { + return err + } + ingress = append(ingress, &ecs.LoadBalancer{ + ContainerName: aws.String(mapping.service.Name), + ContainerPort: aws.Int64(int64(port.Target)), + TargetGroupArn: targetgroup, + }) + + err = c.CreateListener(port, loadBalancer, targetgroup) + if err != nil { + return err + } + } + + _, err = c.CreateService(project, mapping.service, mapping.task, securityGroup, subnets, logGroup, ingress) if err != nil { return err } @@ -54,7 +79,7 @@ func (c *client) ComposeUp(project *compose.Project) error { return nil } -func (c *client) CreateService(project *compose.Project, service *types.ServiceConfig, task *ecs.RegisterTaskDefinitionInput, securityGroup *string, subnets []*string, logGroup *string) (*string, error) { +func (c *client) CreateService(project *compose.Project, service *types.ServiceConfig, task *ecs.RegisterTaskDefinitionInput, securityGroup *string, subnets []*string, logGroup *string, ingress []*ecs.LoadBalancer) (*string, error) { role, err := c.GetEcsTaskExecutionRole(service) if err != nil { return nil, err @@ -88,6 +113,7 @@ func (c *client) CreateService(project *compose.Project, service *types.ServiceC ServiceName: aws.String(service.Name), SchedulingStrategy: aws.String(ecs.SchedulingStrategyReplica), TaskDefinition: arn, + LoadBalancers: ingress, }) for _, port := range service.Ports {