This repository has been archived by the owner on May 6, 2024. It is now read-only.
/
service_create.go
365 lines (301 loc) · 13.1 KB
/
service_create.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
package cmd
import (
"fmt"
"regexp"
"strings"
CWL "github.com/jpignata/fargate/cloudwatchlogs"
"github.com/jpignata/fargate/console"
"github.com/jpignata/fargate/docker"
EC2 "github.com/jpignata/fargate/ec2"
ECR "github.com/jpignata/fargate/ecr"
ECS "github.com/jpignata/fargate/ecs"
ELBV2 "github.com/jpignata/fargate/elbv2"
"github.com/jpignata/fargate/git"
IAM "github.com/jpignata/fargate/iam"
"github.com/spf13/cobra"
)
const typeService = "service"
type ServiceCreateOperation struct {
Cpu string
EnvVars []ECS.EnvVar
Image string
LoadBalancerArn string
LoadBalancerName string
Memory string
Num int64
Port Port
Rules []ELBV2.Rule
SecurityGroupIds []string
ServiceName string
SubnetIds []string
TaskRole string
}
func (o *ServiceCreateOperation) SetPort(inputPort string) {
var msgs []string
port := inflatePort(inputPort)
validProtocols := regexp.MustCompile(validProtocolsPattern)
if !validProtocols.MatchString(port.Protocol) {
msgs = append(msgs, fmt.Sprintf("Invalid protocol %s [specify TCP, HTTP, or HTTPS]", port.Protocol))
}
if port.Port < 1 || port.Port > 65535 {
msgs = append(msgs, fmt.Sprintf("Invalid port %d [specify within 1 - 65535]", port.Port))
}
if len(msgs) > 0 {
console.ErrorExit(fmt.Errorf(strings.Join(msgs, ", ")), "Invalid command line flags")
}
o.Port = port
}
func (o *ServiceCreateOperation) Validate() {
err := validateCpuAndMemory(o.Cpu, o.Memory)
if err != nil {
console.ErrorExit(err, "Invalid settings: %s CPU units / %s MiB", o.Cpu, o.Memory)
}
if o.Num < 1 {
console.ErrorExit(err, "Invalid number of tasks to keep running: %d, num must be > 1", o.Num)
}
}
func (o *ServiceCreateOperation) SetLoadBalancer(lb string) {
if o.Port.Empty() {
console.IssueExit("Setting a load balancer requires a port")
}
elbv2 := ELBV2.New(sess)
loadBalancer := elbv2.DescribeLoadBalancer(lb)
if loadBalancer.Type == typeNetwork {
if o.Port.Protocol != protocolTcp {
console.ErrorExit(fmt.Errorf("network load balancer %s only supports TCP", lb), "Invalid load balancer and protocol")
}
}
if loadBalancer.Type == typeApplication {
if !(o.Port.Protocol == protocolHttp || o.Port.Protocol == protocolHttps) {
console.ErrorExit(fmt.Errorf("application load balancer %s only supports HTTP or HTTPS", lb), "Invalid load balancer and protocol")
}
}
o.LoadBalancerName = lb
o.LoadBalancerArn = loadBalancer.Arn
}
func (o *ServiceCreateOperation) SetRules(inputRules []string) {
var rules []ELBV2.Rule
var msgs []string
validRuleTypes := regexp.MustCompile(validRuleTypesPattern)
if len(inputRules) > 0 && o.LoadBalancerArn == "" {
msgs = append(msgs, "lb must be configured if rules are specified")
}
for _, inputRule := range inputRules {
splitInputRule := strings.SplitN(inputRule, "=", 2)
if len(splitInputRule) != 2 {
msgs = append(msgs, "rules must be in the form of type=value")
}
if !validRuleTypes.MatchString(splitInputRule[0]) {
msgs = append(msgs, fmt.Sprintf("Invalid rule type %s [must be path or host]", splitInputRule[0]))
}
rules = append(rules,
ELBV2.Rule{
Type: strings.ToUpper(splitInputRule[0]),
Value: splitInputRule[1],
},
)
}
if len(msgs) > 0 {
console.ErrorExit(fmt.Errorf(strings.Join(msgs, ", ")), "Invalid rule")
}
o.Rules = rules
}
func (o *ServiceCreateOperation) SetEnvVars(inputEnvVars []string) {
o.EnvVars = extractEnvVars(inputEnvVars)
}
func (o *ServiceCreateOperation) SetSecurityGroupIds(securityGroupIds []string) {
o.SecurityGroupIds = securityGroupIds
}
var (
flagServiceCreateCpu string
flagServiceCreateEnvVars []string
flagServiceCreateImage string
flagServiceCreateLb string
flagServiceCreateMemory string
flagServiceCreateNum int64
flagServiceCreatePort string
flagServiceCreateRules []string
flagServiceCreateSecurityGroupIds []string
flagServiceCreateSubnetIds []string
flagServiceCreateTaskRole string
)
var serviceCreateCmd = &cobra.Command{
Use: "create <service-name>",
Short: "Create a service",
Long: `Create a service
CPU and memory settings can be optionally specified as CPU units and mebibytes
respectively using the --cpu and --memory flags. Every 1024 CPU units is
equivilent to a single vCPU. AWS Fargate only supports certain combinations of
CPU and memory configurations:
| CPU (CPU Units) | Memory (MiB) |
| --------------- | ------------------------------------- |
| 256 | 512, 1024, or 2048 |
| 512 | 1024 through 4096 in 1GiB increments |
| 1024 | 2048 through 8192 in 1GiB increments |
| 2048 | 4096 through 16384 in 1GiB increments |
| 4096 | 8192 through 30720 in 1GiB increments |
If not specified, fargate will launch minimally sized tasks at 0.25 vCPU (256
CPU units) and 0.5GB (512 MiB) of memory.
The Docker container image to use in the service can be optionally specified
via the --image flag. If not specified, fargate will build a new Docker
container image from the current working directory and push it to Amazon ECR in
a repository named for the task group. If the current working directory is a
git repository, the container image will be tagged with the short ref of the
HEAD commit. If not, a timestamp in the format of YYYYMMDDHHMMSS will be used.
To use the service with a load balancer, a port must be specified when the
service is created. Specify a port by passing the --port flag and a port
expression of protocol:port-number. For example, if the service listens on port
80 and uses HTTP, specify HTTP:80. Valid protocols are HTTP, HTTPS, and TCP.
You can only specify a single port.
Services can optionally be configured to use a load balancer. To put a load
balancer in front a service, pass the --lb flag with the name of a load
balancer. If you specify a load balancer, you must also specify a port via the
--port flag to which the load balancer should forward requests. Optionally,
Application Load Balancers can be configured to route HTTP/HTTPS traffic to the
service based upon a rule. Rules are configured by passing one or more rules by
specifying the --rule flag along with a rule expression. Rule expressions are
in the format of TYPE=VALUE. Type can either be PATH or HOST. PATH matches the
PATH of the request and HOST matches the requested hostname in the HTTP
request. Both PATH and HOST types can include up to three wildcard characters:
* to match multiple characters and ? to match a single character. If rules are
omitted, the service will be the load balancer's default action.
Environment variables can be specified via the --env flag. Specify --env with a
key=value parameter multiple times to add multiple variables.
Specify the desired count of tasks the service should maintain by passing the
--num flag with a number. If you omit this flag, fargate will configure a
service with a desired number of tasks of 1.
Security groups can optionally be specified for the service by passing the
--security-group-id flag with a security group ID. To add multiple security
groups, pass --security-group-id with a security group ID multiple times. If
--security-group-id is omitted, a permissive security group will be applied to
the service.
By default, the service will be created in the default VPC and attached
to the default VPC subnets for each availability zone. You can override this by
specifying explicit subnets by passing the --subnet-id flag with a subnet ID.
A task role can be optionally specified via the --task-role flag by providing
eith a full IAM role ARN or the name of an IAM role. The tasks run by the
service will be able to assume this role.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
operation := &ServiceCreateOperation{
Cpu: flagServiceCreateCpu,
Image: flagServiceCreateImage,
Memory: flagServiceCreateMemory,
Num: flagServiceCreateNum,
SecurityGroupIds: flagServiceCreateSecurityGroupIds,
ServiceName: args[0],
SubnetIds: flagServiceCreateSubnetIds,
TaskRole: flagServiceCreateTaskRole,
}
if flagServiceCreatePort != "" {
operation.SetPort(flagServiceCreatePort)
}
if flagServiceCreateLb != "" {
operation.SetLoadBalancer(flagServiceCreateLb)
}
if len(flagServiceCreateRules) > 0 {
operation.SetRules(flagServiceCreateRules)
}
if len(flagServiceCreateEnvVars) > 0 {
operation.SetEnvVars(flagServiceCreateEnvVars)
}
operation.Validate()
createService(operation)
},
}
func init() {
serviceCreateCmd.Flags().StringVarP(&flagServiceCreateCpu, "cpu", "c", "256", "Amount of cpu units to allocate for each task")
serviceCreateCmd.Flags().StringVarP(&flagServiceCreateMemory, "memory", "m", "512", "Amount of MiB to allocate for each task")
serviceCreateCmd.Flags().StringSliceVarP(&flagServiceCreateEnvVars, "env", "e", []string{}, "Environment variables to set [e.g. KEY=value] (can be specified multiple times)")
serviceCreateCmd.Flags().StringVarP(&flagServiceCreatePort, "port", "p", "", "Port to listen on [e.g., 80, 443, http:8080, https:8443, tcp:1935]")
serviceCreateCmd.Flags().StringVarP(&flagServiceCreateImage, "image", "i", "", "Docker image to run in the service; if omitted Fargate will build an image from the Dockerfile in the current directory")
serviceCreateCmd.Flags().StringVarP(&flagServiceCreateLb, "lb", "l", "", "Name of a load balancer to use")
serviceCreateCmd.Flags().StringSliceVarP(&flagServiceCreateRules, "rule", "r", []string{}, "Routing rule for the load balancer [e.g. host=api.example.com, path=/api/*]; if omitted service will be the default route (can be specified multiple times)")
serviceCreateCmd.Flags().Int64VarP(&flagServiceCreateNum, "num", "n", 1, "Number of tasks instances to keep running")
serviceCreateCmd.Flags().StringSliceVar(&flagServiceCreateSecurityGroupIds, "security-group-id", []string{}, "ID of a security group to apply to the service (can be specified multiple times)")
serviceCreateCmd.Flags().StringSliceVar(&flagServiceCreateSubnetIds, "subnet-id", []string{}, "ID of a subnet in which to place the service (can be specified multiple times)")
serviceCreateCmd.Flags().StringVarP(&flagServiceCreateTaskRole, "task-role", "", "", "Name or ARN of an IAM role that the service's tasks can assume")
serviceCmd.AddCommand(serviceCreateCmd)
}
func createService(operation *ServiceCreateOperation) {
var targetGroupArn string
cwl := CWL.New(sess)
ec2 := EC2.New(sess)
ecr := ECR.New(sess)
elbv2 := ELBV2.New(sess)
ecs := ECS.New(sess, clusterName)
iam := IAM.New(sess)
ecsTaskExecutionRoleArn := iam.CreateEcsTaskExecutionRole()
logGroupName := cwl.CreateLogGroup(serviceLogGroupFormat, operation.ServiceName)
if len(operation.SecurityGroupIds) == 0 {
operation.SecurityGroupIds = []string{ec2.GetDefaultSecurityGroupId()}
}
if len(operation.SubnetIds) == 0 {
operation.SubnetIds = ec2.GetDefaultVpcSubnetIds()
}
if operation.Image == "" {
var tag, repositoryUri string
if ecr.IsRepositoryCreated(operation.ServiceName) {
repositoryUri = ecr.GetRepositoryUri(operation.ServiceName)
} else {
repositoryUri = ecr.CreateRepository(operation.ServiceName)
}
if git.IsCwdGitRepo() {
tag = git.GetShortSha()
} else {
tag = docker.GenerateTag()
}
repository := docker.NewRepository(repositoryUri)
username, password := ecr.GetUsernameAndPassword()
repository.Login(username, password)
repository.Build(tag)
repository.Push(tag)
operation.Image = repository.UriFor(tag)
}
if operation.LoadBalancerArn != "" {
vpcId := ec2.GetSubnetVpcId(operation.SubnetIds[0])
targetGroupArn = elbv2.CreateTargetGroup(
&ELBV2.CreateTargetGroupInput{
Name: fmt.Sprintf("%s-%s", clusterName, operation.ServiceName),
Port: operation.Port.Port,
Protocol: operation.Port.Protocol,
VpcId: vpcId,
},
)
if len(operation.Rules) > 0 {
for _, rule := range operation.Rules {
elbv2.AddRule(operation.LoadBalancerArn, targetGroupArn, rule)
}
} else {
elbv2.ModifyLoadBalancerDefaultAction(operation.LoadBalancerArn, targetGroupArn)
}
}
taskDefinitionArn := ecs.CreateTaskDefinition(
&ECS.CreateTaskDefinitionInput{
Cpu: operation.Cpu,
EnvVars: operation.EnvVars,
ExecutionRoleArn: ecsTaskExecutionRoleArn,
Image: operation.Image,
Memory: operation.Memory,
Name: operation.ServiceName,
Port: operation.Port.Port,
LogGroupName: logGroupName,
LogRegion: region,
TaskRole: operation.TaskRole,
Type: typeService,
},
)
ecs.CreateService(
&ECS.CreateServiceInput{
Cluster: clusterName,
DesiredCount: operation.Num,
Name: operation.ServiceName,
Port: operation.Port.Port,
SecurityGroupIds: operation.SecurityGroupIds,
SubnetIds: operation.SubnetIds,
TargetGroupArn: targetGroupArn,
TaskDefinitionArn: taskDefinitionArn,
},
)
console.Info("Created service %s", operation.ServiceName)
}