├── .gitignore ├── Makefile ├── example ├── alb.yml ├── fargate-service.yml ├── fargate-task-definition.yml └── readme.md ├── go.mod ├── go.sum ├── main.go └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | /bin -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell git describe --tags --candidates=1 --dirty 2>/dev/null || echo "dev") 2 | FLAGS=-X main.Version=$(VERSION) 3 | 4 | all: clean 5 | mkdir -p bin 6 | gox -os="linux darwin windows" -arch="amd64 arm64" -ldflags="$(FLAGS)" -output="./bin/{{.Dir}}_{{.OS}}_{{.Arch}}" 7 | 8 | clean: 9 | rm -rf bin 10 | -------------------------------------------------------------------------------- /example/alb.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Parameters: 3 | Healthcheck: 4 | Type: String 5 | Description: A URL to probe 6 | Default: /healthcheck 7 | 8 | CertificateArn: 9 | Type: String 10 | Description: ARN of a SSL certificate to use for the ALB 11 | Default: arn:aws:acm:us-east-1:xxx/yyyy 12 | 13 | Resources: 14 | ALB: 15 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 16 | Properties: 17 | Subnets: [subnet-aaaaaaaa,subnet-bbbbbbbb] 18 | SecurityGroups: 19 | - sg-qqqqqqq 20 | 21 | ALBHTTPListener: 22 | Type: AWS::ElasticLoadBalancingV2::Listener 23 | Properties: 24 | DefaultActions: 25 | - Type: forward 26 | TargetGroupArn: !Ref 'ALBTargetGroup' 27 | LoadBalancerArn: !Ref 'ALB' 28 | Port: 80 29 | Protocol: HTTP 30 | DependsOn: 31 | - ALB 32 | 33 | ALBHTTPSListener: 34 | Type: AWS::ElasticLoadBalancingV2::Listener 35 | Properties: 36 | Certificates: 37 | - CertificateArn: !Ref 'CertificateArn' 38 | DefaultActions: 39 | - Type: forward 40 | TargetGroupArn: !Ref 'ALBTargetGroup' 41 | LoadBalancerArn: !Ref 'ALB' 42 | Port: 443 43 | Protocol: HTTPS 44 | DependsOn: 45 | - ALB 46 | 47 | ALBTargetGroup: 48 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 49 | Properties: 50 | Port: 80 51 | Protocol: HTTP 52 | VpcId: vpc-qqqqqqqq 53 | TargetType: ip 54 | HealthCheckPath: !Ref 'Healthcheck' 55 | HealthCheckProtocol: HTTP 56 | HealthCheckIntervalSeconds: 7 57 | HealthCheckTimeoutSeconds: 5 58 | HealthyThresholdCount: 2 59 | UnhealthyThresholdCount: 4 60 | # deregistration_delay is important for fast ECS deploys. Without it the ALB will wait 300 seconds 61 | # for the instance to be removed. 62 | TargetGroupAttributes: 63 | - Key: deregistration_delay.timeout_seconds 64 | Value: '10' 65 | DependsOn: 66 | - ALB 67 | 68 | -------------------------------------------------------------------------------- /example/fargate-service.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Parameters: 3 | ClusterName: 4 | Type: String 5 | Description: The identifier of the ECS Cluster to use 6 | Default: myapp 7 | 8 | ContainerName: 9 | Type: String 10 | Description: The container within the service to route traffic to 11 | Default: myapp 12 | 13 | TaskDefinition: 14 | Type: String 15 | Description: The task definition to run. Must have the current release number eg myapp:123 16 | 17 | DesiredCount: 18 | Type: Number 19 | Description: Number of tasks to run 20 | Default: 1 21 | 22 | ALBTargetGroup: 23 | Type: String 24 | Description: The ALB target group to join containers into 25 | Default: arn:aws:elasticloadbalancing:us-east-1:xxxx:targetgroup/yyyy/zzzz 26 | 27 | Resources: 28 | Prod: 29 | Type: AWS::ECS::Service 30 | Properties: 31 | Cluster: !Ref 'ClusterName' 32 | LaunchType: "FARGATE" 33 | NetworkConfiguration: 34 | AwsvpcConfiguration: 35 | AssignPublicIp: ENABLED 36 | SecurityGroups: 37 | - sg-xxxxxxxx 38 | Subnets: 39 | - subnet-yyyyyyyy 40 | - subnet-zzzzzzzz 41 | DesiredCount: !Ref 'DesiredCount' 42 | LoadBalancers: 43 | - ContainerName: !Ref 'ContainerName' 44 | ContainerPort: 80 45 | TargetGroupArn: !Ref 'ALBTargetGroup' 46 | TaskDefinition: !Ref 'TaskDefinition' 47 | 48 | ServiceScalingTarget: 49 | Type: AWS::ApplicationAutoScaling::ScalableTarget 50 | DependsOn: Prod 51 | Properties: 52 | MaxCapacity: 20 53 | MinCapacity: 2 54 | ResourceId: !Join ['', [service/, !Ref 'ClusterName', /, !GetAtt [Prod, Name]]] 55 | RoleARN: arn:aws:iam::yyyyy:role/ecsAutoscaleRole 56 | ScalableDimension: ecs:service:DesiredCount 57 | ServiceNamespace: ecs 58 | 59 | ScaleDownPolicy: 60 | Type: AWS::ApplicationAutoScaling::ScalingPolicy 61 | Properties: 62 | PolicyName: ScaleDown 63 | PolicyType: StepScaling 64 | ScalingTargetId: !Ref 'ServiceScalingTarget' 65 | StepScalingPolicyConfiguration: 66 | AdjustmentType: ChangeInCapacity 67 | Cooldown: 60 68 | StepAdjustments: 69 | - MetricIntervalLowerBound: 0 70 | ScalingAdjustment: -1 71 | 72 | ScaleUpPolicy: 73 | Type: AWS::ApplicationAutoScaling::ScalingPolicy 74 | Properties: 75 | PolicyName: ScaleUp 76 | PolicyType: StepScaling 77 | ScalingTargetId: !Ref 'ServiceScalingTarget' 78 | StepScalingPolicyConfiguration: 79 | AdjustmentType: ChangeInCapacity 80 | Cooldown: 60 81 | StepAdjustments: 82 | - MetricIntervalLowerBound: 0 83 | ScalingAdjustment: 1 84 | 85 | CPUAlarmHigh: 86 | Type: AWS::CloudWatch::Alarm 87 | Properties: 88 | AlarmDescription: Scale up if CPU > 50% for 5 minutes 89 | MetricName: CPUUtilization 90 | Namespace: AWS/ECS 91 | Statistic: Average 92 | Period: '60' 93 | EvaluationPeriods: '5' 94 | ComparisonOperator: GreaterThanThreshold 95 | Threshold: '50' 96 | AlarmActions: 97 | - !Ref 'ScaleUpPolicy' 98 | Dimensions: 99 | - Name: ServiceName 100 | Value: !Ref 'Prod' 101 | 102 | CPUAlarmLow: 103 | Type: AWS::CloudWatch::Alarm 104 | Properties: 105 | AlarmDescription: Scale down if CPU < 30% for 5 minutes 106 | MetricName: CPUUtilization 107 | Namespace: AWS/ECS 108 | Statistic: Average 109 | Period: '60' 110 | EvaluationPeriods: '5' 111 | ComparisonOperator: LessThanThreshold 112 | Threshold: '30' 113 | AlarmActions: 114 | - !Ref 'ScaleDownPolicy' 115 | Dimensions: 116 | - Name: ServiceName 117 | Value: !Ref 'Prod' 118 | -------------------------------------------------------------------------------- /example/fargate-task-definition.yml: -------------------------------------------------------------------------------- 1 | family: myapp 2 | taskRoleArn: arn:aws:iam::xxx:role/task-arn 3 | executionRoleArn: arn:aws:iam::xxxx:role/task-arn 4 | memory: "2GB" 5 | cpu: "1024" 6 | networkMode: awsvpc 7 | requiresCompatibilities: 8 | - FARGATE 9 | containerDefinitions: 10 | - essential: true 11 | image: ${IMAGE_NAME:-your/docker-image} 12 | command: ["/bin/startup.sh"] 13 | name: myapp 14 | memory: 2048 15 | cpu: 1024 16 | logConfiguration: 17 | logDriver: awslogs 18 | options: 19 | awslogs-group: /ecs/myapp 20 | awslogs-region: us-east-1 21 | awslogs-stream-prefix: ecs 22 | portMappings: 23 | - {containerPort: 80} 24 | environment: 25 | - {name: PORT, value: "80"} 26 | -------------------------------------------------------------------------------- /example/readme.md: -------------------------------------------------------------------------------- 1 | # example fargate ecs service 2 | 3 | 4 | ### networking 5 | Create a VPC and some subnets, or use the default VPC. This is a complicated topic covered by the [aws docs](http://docs.aws.amazon.com/AmazonVPC/latest/GettingStartedGuide/getting-started-ipv4.html) 6 | 7 | All of the following templates will need the vpc and subnets replaced. 8 | 9 | Additionally, they also need two security groups pre created. 10 | 11 | A security group for the ALB and a security group for the fagate containers. The ELBs should be able to access container ports. Update the security group references in the templates. 12 | 13 | ### upload [fargate-task-definition.yml](fargate-task-definition.yml) 14 | 15 | This will need to be modified to fit your use case. Most important is the task role. This should provide access to pull containers from ECR as well as write cloudwatch logs, as well as whatever AWS perms your app needs. 16 | 17 | ```bash 18 | ecs-upload-task --file fargate-task-definition.yml 19 | ``` 20 | 21 | ### create the ELB 22 | 23 | ```bash 24 | aws cloudformation create-stack \ 25 | --stack-name myApp-alb \ 26 | --template-body file://alb.yml 27 | ``` 28 | 29 | ### create the ECS service 30 | 31 | ``` 32 | aws cloudformation create-stack \ 33 | --stack-name myApp \ 34 | --template-body file://fargate-task-definition.yml \ 35 | --parameters '[{"ParameterKey":"TaskDefinition","ParameterValue":"{OUTPUT FROM ecs-upload-task}"}]' 36 | ``` 37 | 38 | At this point everything should be running. 39 | 40 | ### deploying 41 | 42 | Later on in your CI pipeline, after pushing a new container up: 43 | ```bash 44 | IMAGE_NAME=your/service:r1234 ecs-upload-task \ 45 | --file fargate-task-definition.yml 46 | --service myApp-prod-1234567 # you can find this in the ECS web ui 47 | ``` 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/99designs/ecs-upload-task 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.21.1 7 | github.com/buildkite/ecs-run-task v1.3.0 8 | github.com/stretchr/testify v1.3.0 // indirect 9 | github.com/urfave/cli/v2 v2.1.1 10 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect 11 | golang.org/x/text v0.3.2 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/aws/aws-sdk-go v1.15.81/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= 3 | github.com/aws/aws-sdk-go v1.21.1 h1:IOFDnCEDybcw4V8nbKqyyjBu+vpu7hFYSfZqNuogi7I= 4 | github.com/aws/aws-sdk-go v1.21.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 5 | github.com/buildkite/ecs-run-task v1.3.0 h1:2ZKSrv4kbEhjCUkRdCJmF0K7bB6TpM3RNZv1zsa37Fo= 6 | github.com/buildkite/ecs-run-task v1.3.0/go.mod h1:DbOWBzMzc0AuMx4IxCAonV9QebcYypPe5Y7iGBxnXJg= 7 | github.com/buildkite/interpolate v0.0.0-20181028012610-973457fa2b4c h1:rQKXSYBMFBpO+4lLT62/w3fABubWPdiXZI/H5W/JYeg= 8 | github.com/buildkite/interpolate v0.0.0-20181028012610-973457fa2b4c/go.mod h1:gbPR1gPu9dB96mucYIR7T3B7p/78hRVSOuzIWLHK2Y4= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 10 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 15 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 16 | github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 17 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= 18 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 19 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 20 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 21 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 22 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 23 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 24 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 25 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 29 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 30 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 31 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 34 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 35 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 36 | github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= 37 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 38 | github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= 39 | github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 40 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 41 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 42 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= 43 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 44 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 45 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 46 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 47 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 48 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 49 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 50 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 51 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 53 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 54 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 55 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | "github.com/aws/aws-sdk-go/aws" 11 | "github.com/aws/aws-sdk-go/aws/session" 12 | "github.com/aws/aws-sdk-go/service/ecs" 13 | "github.com/buildkite/ecs-run-task/parser" 14 | "github.com/urfave/cli/v2" 15 | ) 16 | 17 | const ECS_POLL_INTERVAL = 1 * time.Second 18 | const version = "dev" 19 | 20 | func main() { 21 | app := cli.NewApp() 22 | app.Name = "ecs-upload-task" 23 | app.Usage = "upload ecs task definitions and update ECS services" 24 | app.Action = run 25 | app.Version = version 26 | app.Flags = []cli.Flag{ 27 | &cli.StringFlag{Name: "file", Value: "taskdefinition.json", Usage: "the task definition to upload"}, 28 | &cli.StringFlag{Name: "cluster", Value: "default", Usage: "The cluster to update the services on"}, 29 | &cli.StringFlag{Name: "service", Usage: "Optional service name to update"}, 30 | &cli.BoolFlag{Name: "dry-run", Value: false, Usage: "Parse the template without running upload"}, 31 | } 32 | 33 | app.Run(os.Args) 34 | } 35 | 36 | func run(c *cli.Context) error { 37 | sess := session.Must(session.NewSession()) 38 | svc := ecs.New(sess) 39 | cluster := c.String("cluster") 40 | filename := c.String("file") 41 | dryrun := c.Bool("dry-run") 42 | 43 | if dryrun { 44 | _, err := parser.Parse(filename, os.Environ()) 45 | if err != nil { 46 | fmt.Fprintf(os.Stderr, "Unable to parse task definition: %s\n", err) 47 | os.Exit(1) 48 | } 49 | 50 | fmt.Fprintf(os.Stdout, "Template %s is parsed successfully\n", filename) 51 | os.Exit(0) 52 | } 53 | 54 | taskDefinition := uploadTask(svc, filename) 55 | 56 | if serviceName := c.String("service"); serviceName != "" { 57 | updateService(svc, serviceName, cluster, taskDefinition) 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func uploadTask(svc *ecs.ECS, filename string) string { 64 | taskDefinitionInput, err := parser.Parse(filename, os.Environ()) 65 | if err != nil { 66 | fmt.Fprintf(os.Stderr, "Unable to parse task definition: %s\n", err) 67 | os.Exit(1) 68 | } 69 | 70 | log.Printf("Registering a task for %s\n", *taskDefinitionInput.Family) 71 | resp, err := svc.RegisterTaskDefinition(taskDefinitionInput) 72 | if err != nil { 73 | fmt.Fprintf(os.Stderr, "Unable to register task definition: %s\n", err) 74 | os.Exit(1) 75 | } 76 | 77 | log.Printf("Created %s\n", *resp.TaskDefinition.TaskDefinitionArn) 78 | 79 | return *resp.TaskDefinition.TaskDefinitionArn 80 | } 81 | 82 | func updateService(svc *ecs.ECS, service, cluster, taskDefinition string) { 83 | log.Printf("Updating service %s\n", service) 84 | 85 | _, err := svc.UpdateService(&ecs.UpdateServiceInput{ 86 | Cluster: &cluster, 87 | Service: &service, 88 | TaskDefinition: &taskDefinition, 89 | }) 90 | if err != nil { 91 | fmt.Fprintf(os.Stderr, "Unable to update service %s on cluster %s: %s\n", service, cluster, err) 92 | os.Exit(1) 93 | } 94 | 95 | pollUntilTaskDeployed(svc, cluster, service, taskDefinition) 96 | } 97 | 98 | func getService(svc *ecs.ECS, service, cluster string) (*ecs.Service, error) { 99 | resp, err := svc.DescribeServices(&ecs.DescribeServicesInput{ 100 | Services: []*string{aws.String(service)}, 101 | Cluster: aws.String(cluster), 102 | }) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | if len(resp.Failures) > 0 { 108 | return nil, errors.New(*resp.Failures[0].Reason) 109 | } 110 | 111 | if len(resp.Services) != 1 { 112 | return nil, errors.New("multiple services with the same name") 113 | } 114 | 115 | return resp.Services[0], nil 116 | } 117 | 118 | func pollUntilTaskDeployed(svc *ecs.ECS, service string, cluster string, task string) { 119 | lastSeen := time.Now().Add(-1 * time.Minute) 120 | 121 | for { 122 | service, err := getService(svc, cluster, service) 123 | if err != nil { 124 | fmt.Fprintf(os.Stderr, "Unable to get service: %s\n", err) 125 | os.Exit(1) 126 | } 127 | 128 | for i := len(service.Events) - 1; i >= 0; i-- { 129 | event := service.Events[i] 130 | if event.CreatedAt.After(lastSeen) { 131 | log.Println(*event.Message) 132 | lastSeen = *event.CreatedAt 133 | } 134 | } 135 | 136 | if len(service.Deployments) == 1 && *service.Deployments[0].TaskDefinition == task { 137 | return 138 | } 139 | 140 | time.Sleep(ECS_POLL_INTERVAL) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # `ecs-upload-task` 2 | 3 | A very simple cli tool that will upload an ecs task and optionally update a service to reference it. 4 | 5 | Designed as a partner tool to https://github.com/buildkite/ecs-run-task and shares the same task definition parser with environment substitution. 6 | 7 | ## Example 8 | 9 | Install it 10 | ```bash 11 | go install github.com/99designs/ecs-upload-task@latest 12 | 13 | or 14 | 15 | curl -L --fail -o /usr/bin/ecs-upload-task \ 16 | 17 | ``` 18 | 19 | `taskdefinition.yml` 20 | ```yaml 21 | family: hello-world 22 | containerDefinitions: 23 | - essential: true 24 | image: ${IMAGE_NAME:-99designs/hello-world} 25 | memory: 64 26 | cpu: 10 27 | name: hello-world 28 | portMappings: 29 | - {containerPort: 1234, hostPort: 1234} 30 | environment: 31 | - {name: PORT, value: "1234"} 32 | 33 | ``` 34 | 35 | then take it for a dry-run to validate that the template is parsed successfully. 36 | 37 | ```bash 38 | ecs-upload-task --file taskdefinition.yml --dry-run 39 | Template taskdefinition.yml is parsed successfully 40 | ``` 41 | 42 | then register the task definition 43 | 44 | ```bash 45 | ecs-upload-task --file taskdefinition.yml 46 | ``` 47 | 48 | Create a new ECS service referencing the task definition. Service templates can use the family name without the version, simply `hello-world` is enough. 49 | 50 | 51 | Later, in your automated CI pipeline you can deploy by simply: 52 | 53 | ```bash 54 | export IMAGE_NAME=99designs/hello-world:$FRESH_BUILD_NUMBER 55 | ecs-upload-task --file taskdefinition.yml --service hello-world-2017-05-15-10-45 56 | 57 | 2017/11/14 18:02:32 Registering a task for hello-world 58 | 2017/11/14 18:02:34 Created arn:xxx:task-definition/hello-world:2 59 | 2017/11/14 18:02:34 Updating service hello-world-2017-05-15-10-45 60 | 2017/11/14 18:02:55 (service hello-world-2017-05-15-10-45) has started 1 tasks: (task 81b2963f-072a-479b-856f-26af2ec615f8). 61 | 2017/11/14 18:03:05 (service hello-world-2017-05-15-10-45) registered 1 instances in (elb hello-world-ELB-O5IUREC150O5) 62 | 2017/11/14 18:03:28 (service hello-world-2017-05-15-10-45) deregistered 1 instances in (elb hello-world-ELB-O5IUREC150O5) 63 | 2017/11/14 18:03:48 (service hello-world-2017-05-15-10-45) has stopped 1 running tasks: (task a3e3a91b-be05-4092-bcf6-47f2075933af). 64 | 65 | ``` 66 | 67 | ## Creating a new release 68 | 69 | 1. Check out the the commit you want to create a release for, and tag it with appropriate semver convention: 70 | 71 | ``` 72 | $ git tag vx.x.x 73 | $ git push --tags 74 | ``` 75 | 76 | 2. Create the binaries: 77 | 78 | Binaries are compiled using [`gox`](https://github.com/mitchellh/gox). This provides a simple way to compile for multiple CPU architectures (`amd64`, `arm64`), which allows us to provide support for devices such as M1 Macs. 79 | 80 | `gox` can be installed via `go install` 81 | 82 | ``` 83 | go install github.com/mitchellh/gox@latest 84 | ``` 85 | Then simply run the `Makefile` to compile. 86 | 87 | ``` 88 | $ make 89 | ``` 90 | 91 | 3. Go to https://github.com/99designs/ecs-upload-task/releases/new 92 | 93 | 4. Select the tag version you just created 94 | 95 | 5. Attach the binaries from `./bin/*` 96 | 97 | --------------------------------------------------------------------------------