Skip to content

Commit 61345cd

Browse files
authored
Simple EC2 based ECS blueprint (#14)
Probably some code cleanup/sharing I could do here that would make this a bit smaller, but I didn't have a ton of time to get into it. Probably something worth doing in the future though.
1 parent 5cfc0ea commit 61345cd

File tree

3 files changed

+343
-1
lines changed

3 files changed

+343
-1
lines changed

stacker_blueprints/ecs.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
)
1414

1515
from stacker.blueprints.base import Blueprint
16+
from stacker.blueprints.variables.types import TroposphereType
1617

1718
from .policies import ecs_task_execution_policy
1819

@@ -281,3 +282,188 @@ def create_template(self):
281282
self.create_task_execution_role_policy()
282283
self.create_task_definition()
283284
self.create_service()
285+
286+
287+
class SimpleECSService(Blueprint):
288+
VARIABLES = {
289+
"ServiceName": {
290+
"type": str,
291+
"description": "A simple name for the service.",
292+
},
293+
"Image": {
294+
"type": str,
295+
"description": "The docker image to use for the task.",
296+
},
297+
"Command": {
298+
"type": list,
299+
"description": "A list of the command and it's arguments to run "
300+
"inside the container. If not provided, will "
301+
"default to the default command defined in the "
302+
"image.",
303+
"default": [],
304+
},
305+
"Cluster": {
306+
"type": str,
307+
"description": "The name or Amazon Resource Name (ARN) of the "
308+
"ECS cluster that you want to run your tasks on.",
309+
},
310+
"CPU": {
311+
"type": int,
312+
"description": "The relative CPU shares used by each instance of "
313+
"the task.",
314+
},
315+
"Memory": {
316+
"type": int,
317+
"description": "The amount of memory (in megabytes) to reserve "
318+
"for each instance of the task.",
319+
},
320+
"Count": {
321+
"type": int,
322+
"description": "The number of instances of the task to create.",
323+
"default": 1,
324+
},
325+
"Environment": {
326+
"type": dict,
327+
"description": "A dictionary representing the environment of the "
328+
"task.",
329+
"default": {},
330+
},
331+
"LogConfiguration": {
332+
"type": TroposphereType(ecs.LogConfiguration, optional=True),
333+
"description": "An optional log configuration object.",
334+
"default": None,
335+
},
336+
}
337+
338+
@property
339+
def service_name(self):
340+
return self.get_variables()["ServiceName"]
341+
342+
@property
343+
def image(self):
344+
return self.get_variables()["Image"]
345+
346+
@property
347+
def command(self):
348+
return self.get_variables()["Command"] or NoValue
349+
350+
@property
351+
def cluster(self):
352+
return self.get_variables()["Cluster"]
353+
354+
@property
355+
def cpu(self):
356+
return self.get_variables()["CPU"]
357+
358+
@property
359+
def memory(self):
360+
return self.get_variables()["Memory"]
361+
362+
@property
363+
def count(self):
364+
return self.get_variables()["Count"]
365+
366+
@property
367+
def environment(self):
368+
env_dict = self.get_variables()["Environment"]
369+
if not env_dict:
370+
return NoValue
371+
372+
env_list = []
373+
for k, v in env_dict.items():
374+
env_list.append(ecs.Environment(Name=str(k), Value=str(v)))
375+
376+
return env_list
377+
378+
@property
379+
def log_configuration(self):
380+
log_config = self.get_variables()["LogConfiguration"]
381+
return log_config or NoValue
382+
383+
def add_output(self, key, value):
384+
self.template.add_output(Output(key, Value=value))
385+
386+
def create_role(self):
387+
t = self.template
388+
389+
self.role = t.add_resource(
390+
iam.Role(
391+
"Role",
392+
AssumeRolePolicyDocument=get_ecs_task_assumerole_policy(),
393+
Path="/",
394+
)
395+
)
396+
397+
self.add_output("RoleName", self.role.Ref())
398+
self.add_output("RoleArn", self.role.GetAtt("Arn"))
399+
self.add_output("RoleId", self.role.GetAtt("RoleId"))
400+
401+
def generate_policy_document(self):
402+
return None
403+
404+
def create_policy(self):
405+
t = self.template
406+
407+
policy_doc = self.generate_policy_document()
408+
if not policy_doc:
409+
return
410+
411+
self.policy = t.add_resource(
412+
iam.ManagedPolicy(
413+
"ManagedPolicy",
414+
PolicyDocument=self.generate_policy(),
415+
Roles=[self.role.Ref()],
416+
)
417+
)
418+
419+
self.add_output("ManagedPolicyArn", self.policy.Ref())
420+
421+
def generate_container_definition(self):
422+
return ecs.ContainerDefinition(
423+
Command=self.command,
424+
Cpu=self.cpu,
425+
Environment=self.environment,
426+
Essential=True,
427+
Image=self.image,
428+
LogConfiguration=self.log_configuration,
429+
Memory=self.memory,
430+
Name=self.service_name,
431+
)
432+
433+
def create_task_definition(self):
434+
t = self.template
435+
436+
self.task_definition = t.add_resource(
437+
ecs.TaskDefinition(
438+
"TaskDefinition",
439+
Cpu=str(self.cpu),
440+
Family=self.service_name,
441+
Memory=str(self.memory),
442+
TaskRoleArn=self.role.GetAtt("Arn"),
443+
ContainerDefinitions=[self.generate_container_definition()]
444+
)
445+
)
446+
447+
self.add_output("TaskDefinitionArn", self.task_definition.Ref())
448+
449+
def create_service(self):
450+
t = self.template
451+
self.service = t.add_resource(
452+
ecs.Service(
453+
"Service",
454+
Cluster=self.cluster,
455+
DesiredCount=self.count,
456+
LaunchType="EC2",
457+
ServiceName=self.service_name,
458+
TaskDefinition=self.task_definition.Ref(),
459+
)
460+
)
461+
462+
self.add_output("ServiceArn", self.service.Ref())
463+
self.add_output("ServiceName", self.service.GetAtt("Name"))
464+
465+
def create_template(self):
466+
self.create_role()
467+
self.create_policy()
468+
self.create_task_definition()
469+
self.create_service()
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
{
2+
"Outputs": {
3+
"RoleArn": {
4+
"Value": {
5+
"Fn::GetAtt": [
6+
"Role",
7+
"Arn"
8+
]
9+
}
10+
},
11+
"RoleId": {
12+
"Value": {
13+
"Fn::GetAtt": [
14+
"Role",
15+
"RoleId"
16+
]
17+
}
18+
},
19+
"RoleName": {
20+
"Value": {
21+
"Ref": "Role"
22+
}
23+
},
24+
"ServiceArn": {
25+
"Value": {
26+
"Ref": "Service"
27+
}
28+
},
29+
"ServiceName": {
30+
"Value": {
31+
"Fn::GetAtt": [
32+
"Service",
33+
"Name"
34+
]
35+
}
36+
},
37+
"TaskDefinitionArn": {
38+
"Value": {
39+
"Ref": "TaskDefinition"
40+
}
41+
}
42+
},
43+
"Resources": {
44+
"Role": {
45+
"Properties": {
46+
"AssumeRolePolicyDocument": {
47+
"Statement": [
48+
{
49+
"Action": [
50+
"sts:AssumeRole"
51+
],
52+
"Effect": "Allow",
53+
"Principal": {
54+
"Service": [
55+
"ecs-tasks.amazonaws.com"
56+
]
57+
}
58+
}
59+
]
60+
},
61+
"Path": "/"
62+
},
63+
"Type": "AWS::IAM::Role"
64+
},
65+
"Service": {
66+
"Properties": {
67+
"Cluster": "fake-fargate-cluster",
68+
"DesiredCount": 3,
69+
"LaunchType": "EC2",
70+
"ServiceName": "WorkerService",
71+
"TaskDefinition": {
72+
"Ref": "TaskDefinition"
73+
}
74+
},
75+
"Type": "AWS::ECS::Service"
76+
},
77+
"TaskDefinition": {
78+
"Properties": {
79+
"ContainerDefinitions": [
80+
{
81+
"Command": [
82+
"/bin/run",
83+
"--args 1"
84+
],
85+
"Cpu": 1024,
86+
"Environment": [
87+
{
88+
"Name": "DEBUG",
89+
"Value": "false"
90+
},
91+
{
92+
"Name": "DATABASE_URL",
93+
"Value": "sql://fake_db/fake_db"
94+
}
95+
],
96+
"Essential": "true",
97+
"Image": "fake_repo/image:12345",
98+
"LogConfiguration": {
99+
"Ref": "AWS::NoValue"
100+
},
101+
"Memory": 2048,
102+
"Name": "WorkerService"
103+
}
104+
],
105+
"Cpu": "1024",
106+
"Family": "WorkerService",
107+
"Memory": "2048",
108+
"TaskRoleArn": {
109+
"Fn::GetAtt": [
110+
"Role",
111+
"Arn"
112+
]
113+
}
114+
},
115+
"Type": "AWS::ECS::TaskDefinition"
116+
}
117+
}
118+
}

tests/test_ecs.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
from stacker.context import Context
22
from stacker.variables import Variable
3-
from stacker_blueprints.ecs import Cluster, SimpleFargateService
3+
from stacker_blueprints.ecs import (
4+
Cluster,
5+
SimpleFargateService,
6+
SimpleECSService,
7+
)
48
from stacker.blueprints.testutil import BlueprintTestCase
59

610

@@ -49,3 +53,37 @@ def test_ecs_simple_fargate_service(self):
4953
bp.resolve_variables(self.generate_variables())
5054
bp.create_template()
5155
self.assertRenderedBlueprint(bp)
56+
57+
58+
class TestSimpleECSService(BlueprintTestCase):
59+
def setUp(self):
60+
self.common_variables = {
61+
"ServiceName": "WorkerService",
62+
"Image": "fake_repo/image:12345",
63+
"Command": ["/bin/run", "--args 1"],
64+
"Cluster": "fake-fargate-cluster",
65+
"CPU": 1024,
66+
"Memory": 2048,
67+
"Count": 3,
68+
"Environment": {
69+
"DATABASE_URL": "sql://fake_db/fake_db",
70+
"DEBUG": "false",
71+
},
72+
}
73+
74+
self.ctx = Context({'namespace': 'test', 'environment': 'test'})
75+
76+
def create_blueprint(self, name):
77+
return SimpleECSService(name, self.ctx)
78+
79+
def generate_variables(self, variable_dict=None):
80+
variable_dict = variable_dict or {}
81+
self.common_variables.update(variable_dict)
82+
83+
return [Variable(k, v) for k, v in self.common_variables.items()]
84+
85+
def test_ecs_simple_fargate_service(self):
86+
bp = self.create_blueprint("ecs__simple_ecs_service")
87+
bp.resolve_variables(self.generate_variables())
88+
bp.create_template()
89+
self.assertRenderedBlueprint(bp)

0 commit comments

Comments
 (0)