ciinabox-ecs 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,340 @@
1
+ require 'cfndsl'
2
+
3
+ CloudFormation {
4
+
5
+ # Template metadata
6
+ AWSTemplateFormatVersion "2010-09-09"
7
+ Description "ciinabox - ECS Services v#{ciinabox_version}"
8
+
9
+ # Parameters
10
+ Parameter("ECSCluster") { Type 'String' }
11
+ Parameter("VPC") { Type 'String' }
12
+ Parameter("SubnetPublicA") { Type 'String' }
13
+ Parameter("SubnetPublicB") { Type 'String' }
14
+ Parameter("ECSSubnetPrivateA") { Type 'String' }
15
+ Parameter("ECSSubnetPrivateB") { Type 'String' }
16
+ Parameter("ECSENIPrivateIpAddress") { Type 'String' }
17
+ Parameter("SecurityGroupBackplane") { Type 'String' }
18
+ Parameter("SecurityGroupOps") { Type 'String' }
19
+ Parameter("SecurityGroupDev") { Type 'String' }
20
+ Parameter('SecurityGroupNatGateway') { Type 'String' }
21
+
22
+ # Lambda function ARN for CR that creates and validates ACM
23
+ Parameter('CRAcmCertArn') { Type 'String' }
24
+
25
+ Resource("ECSRole") {
26
+ Type 'AWS::IAM::Role'
27
+ Property('AssumeRolePolicyDocument', {
28
+ Statement: [
29
+ Effect: 'Allow',
30
+ Principal: { Service: ['ecs.amazonaws.com'] },
31
+ Action: ['sts:AssumeRole']
32
+ ]
33
+ })
34
+ Property('Path', '/')
35
+ Property('Policies', [
36
+ {
37
+ PolicyName: 'read-only',
38
+ PolicyDocument: {
39
+ Statement: [
40
+ {
41
+ Effect: 'Allow',
42
+ Action: ['ec2:Describe*', 's3:Get*', 's3:List*'],
43
+ Resource: '*'
44
+ }
45
+ ]
46
+ }
47
+ },
48
+ {
49
+ PolicyName: 's3-write',
50
+ PolicyDocument: {
51
+ Statement: [
52
+ {
53
+ Effect: 'Allow',
54
+ Action: ['s3:PutObject', 's3:PutObject*'],
55
+ Resource: '*'
56
+ }
57
+ ]
58
+ }
59
+ },
60
+ #http://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html
61
+ {
62
+ PolicyName: 'ecsServiceRole',
63
+ PolicyDocument: {
64
+ Statement: [
65
+ {
66
+ Effect: 'Allow',
67
+ Action: [
68
+ "ec2:AttachNetworkInterface",
69
+ "ec2:CreateNetworkInterface",
70
+ "ec2:CreateNetworkInterfacePermission",
71
+ "ec2:DeleteNetworkInterface",
72
+ "ec2:DeleteNetworkInterfacePermission",
73
+ "ec2:Describe*",
74
+ "ec2:DetachNetworkInterface",
75
+ "ecs:CreateCluster",
76
+ "ecs:DeregisterContainerInstance",
77
+ "ecs:DiscoverPollEndpoint",
78
+ "ecs:Poll",
79
+ "ecs:RegisterContainerInstance",
80
+ "ecs:StartTelemetrySession",
81
+ "ecs:Submit*",
82
+ "ec2:AuthorizeSecurityGroupIngress",
83
+ "ec2:Describe*",
84
+ "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
85
+ "elasticloadbalancing:DeregisterTargets",
86
+ "elasticloadbalancing:Describe*",
87
+ "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
88
+ "elasticloadbalancing:RegisterTargets",
89
+ "ecr:GetAuthorizationToken",
90
+ "ecr:BatchCheckLayerAvailability",
91
+ "ecr:GetDownloadUrlForLayer",
92
+ "ecr:BatchGetImage",
93
+ "logs:CreateLogStream",
94
+ "logs:PutLogEvents"
95
+ ],
96
+ Resource: '*'
97
+ }
98
+ ]
99
+ }
100
+ },
101
+ {
102
+ PolicyName: 'packer',
103
+ PolicyDocument: {
104
+ Statement: [
105
+ {
106
+ Effect: 'Allow',
107
+ Action: [
108
+ 'ec2:AttachVolume',
109
+ 'ec2:CreateVolume',
110
+ 'ec2:DeleteVolume',
111
+ 'ec2:CreateKeypair',
112
+ 'ec2:DeleteKeypair',
113
+ 'ec2:CreateSecurityGroup',
114
+ 'ec2:DeleteSecurityGroup',
115
+ 'ec2:AuthorizeSecurityGroupIngress',
116
+ 'ec2:CreateImage',
117
+ 'ec2:RunInstances',
118
+ 'ec2:TerminateInstances',
119
+ 'ec2:StopInstances',
120
+ 'ec2:DescribeVolumes',
121
+ 'ec2:DetachVolume',
122
+ 'ec2:DescribeInstances',
123
+ 'ec2:CreateSnapshot',
124
+ 'ec2:DeleteSnapshot',
125
+ 'ec2:DescribeSnapshots',
126
+ 'ec2:DescribeImages',
127
+ 'ec2:RegisterImage',
128
+ 'ec2:CreateTags',
129
+ 'ec2:ModifyImageAttribute',
130
+ 'dynamodb:*'
131
+ ],
132
+ Resource: '*'
133
+ }
134
+ ]
135
+ }
136
+ }
137
+ ])
138
+ }
139
+
140
+ if defined? webHooks
141
+ rules = []
142
+ webHooks.each do |ip|
143
+ rules << { IpProtocol: 'tcp', FromPort: '443', ToPort: '443', CidrIp: ip }
144
+ end
145
+ else
146
+ rules = [{ IpProtocol: 'tcp', FromPort: '443', ToPort: '443', CidrIp: '192.168.1.1/32' }]
147
+ end
148
+
149
+ Resource("SecurityGroupWebHooks") {
150
+ Type 'AWS::EC2::SecurityGroup'
151
+ Property('VpcId', Ref('VPC'))
152
+ Property('GroupDescription', 'WebHooks like github')
153
+ Property('SecurityGroupIngress', rules)
154
+ }
155
+
156
+ Resource('ToolsSSLCertificate') {
157
+ Type 'Custom::AwsAcmIssueValidator'
158
+ Property('ServiceToken', Ref('CRAcmCertArn'))
159
+ Property('DomainName', "*.#{dns_domain}")
160
+ Property('FallbackCertificateArn', default_ssl_cert_id)
161
+ } if acm_auto_issue_validate
162
+
163
+ certificate_arn = acm_auto_issue_validate ?
164
+ FnGetAtt('ToolsSSLCertificate', 'CertificateArn') : default_ssl_cert_id
165
+
166
+ Output('DefaultSSLCertificate') {
167
+ Value(certificate_arn)
168
+ }
169
+
170
+ elb_listners = []
171
+ elb_listners << { LoadBalancerPort: '80', InstancePort: '8080', Protocol: 'HTTP' }
172
+ elb_listners << { LoadBalancerPort: '443', InstancePort: '8080', Protocol: 'HTTPS', SSLCertificateId: certificate_arn }
173
+ services.each do |service|
174
+ if service.is_a?(Hash) && (!service.values.include? nil)
175
+ service.each do |name, properties|
176
+ unless properties['LoadBalancerPort'].nil? || properties['InstancePort'].nil? || properties['Protocol'].nil?
177
+ elb_listners << { LoadBalancerPort: properties['LoadBalancerPort'], InstancePort: properties['InstancePort'], Protocol: properties['Protocol'] }
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ proxy_elb_sgs = [
184
+ Ref('SecurityGroupBackplane'),
185
+ Ref('SecurityGroupOps'),
186
+ Ref('SecurityGroupDev'),
187
+ Ref('SecurityGroupWebHooks')
188
+ ]
189
+
190
+ proxy_elb_sgs << Ref('SecurityGroupNatGateway') if allow_nat_connections
191
+
192
+ Resource('CiinaboxProxyELB') {
193
+ Type 'AWS::ElasticLoadBalancing::LoadBalancer'
194
+ Property('Listeners', elb_listners)
195
+ Property('HealthCheck', {
196
+ Target: "TCP:8080",
197
+ HealthyThreshold: '3',
198
+ UnhealthyThreshold: '2',
199
+ Interval: '15',
200
+ Timeout: '5'
201
+ })
202
+ Property('CrossZone', true)
203
+ Property('SecurityGroups', proxy_elb_sgs)
204
+ Property('Subnets', [
205
+ Ref('SubnetPublicA'), Ref('SubnetPublicB')
206
+ ])
207
+ }
208
+
209
+ Resource("CiinaboxProxyDNS") {
210
+ Type 'AWS::Route53::RecordSet'
211
+ Property('HostedZoneName', FnJoin('', [dns_domain, '.']))
212
+ Property('Name', FnJoin('', ['*.', dns_domain, '.']))
213
+ Property('Type', 'A')
214
+ Property('AliasTarget', {
215
+ 'DNSName' => FnGetAtt('CiinaboxProxyELB', 'DNSName'),
216
+ 'HostedZoneId' => FnGetAtt('CiinaboxProxyELB', 'CanonicalHostedZoneNameID')
217
+ })
218
+ }
219
+
220
+ if defined? internal_elb and internal_elb
221
+ Resource('CiinaboxProxyELBInternal') {
222
+ Type 'AWS::ElasticLoadBalancing::LoadBalancer'
223
+ Property('Listeners', elb_listners)
224
+ Property('Scheme', 'internal')
225
+ Property('HealthCheck', {
226
+ Target: "TCP:8080",
227
+ HealthyThreshold: '3',
228
+ UnhealthyThreshold: '2',
229
+ Interval: '15',
230
+ Timeout: '5'
231
+ })
232
+ Property('CrossZone', true)
233
+ Property('SecurityGroups', [
234
+ Ref('SecurityGroupBackplane'),
235
+ Ref('SecurityGroupOps'),
236
+ Ref('SecurityGroupDev'),
237
+ Ref('SecurityGroupWebHooks')
238
+ ])
239
+ Property('Subnets', [
240
+ Ref('ECSSubnetPrivateA'), Ref('ECSSubnetPrivateB')
241
+ ])
242
+ }
243
+
244
+ services.each do |service|
245
+ #Services look like this:
246
+ #[
247
+ # {\"jenkins\"=>{\"LoadBalancerPort\"=>50000, \"InstancePort\"=>50000, \"Protocol\"=>\"TCP\"}}",
248
+ # {\"bitbucket\"=>{\"LoadBalancerPort\"=>22, \"InstancePort\"=>7999, \"Protocol\"=>\"TCP\"}}"
249
+ #]
250
+ name, details = service.first
251
+ Resource("CiinaboxProxyDNSInternal") {
252
+ Type 'AWS::Route53::RecordSet'
253
+ Property('HostedZoneName', FnJoin('', [dns_domain, '.']))
254
+ Property('Name', FnJoin('', ["internal-#{name}.", dns_domain, '.']))
255
+ Property('Type', 'A')
256
+ Property('AliasTarget', {
257
+ 'DNSName' => FnGetAtt('CiinaboxProxyELBInternal', 'DNSName'),
258
+ 'HostedZoneId' => FnGetAtt('CiinaboxProxyELB', 'CanonicalHostedZoneNameID')
259
+ })
260
+ }
261
+ end
262
+ end
263
+
264
+ Resource('ProxyTask') {
265
+ Type "AWS::ECS::TaskDefinition"
266
+ Property('ContainerDefinitions', [
267
+ {
268
+ Name: 'proxy',
269
+ Memory: 256,
270
+ Cpu: 100,
271
+ Image: 'jwilder/nginx-proxy',
272
+ PortMappings: [{
273
+ HostPort: 8080,
274
+ ContainerPort: 80
275
+ }],
276
+ Essential: true,
277
+ MountPoints: [
278
+ {
279
+ ContainerPath: '/etc/localtime',
280
+ SourceVolume: 'timezone',
281
+ ReadOnly: true
282
+ },
283
+ {
284
+ ContainerPath: '/tmp/docker.sock',
285
+ SourceVolume: 'docker_sock',
286
+ ReadOnly: false
287
+ }
288
+ ]
289
+ }
290
+ ])
291
+ Property('Volumes', [
292
+ {
293
+ Name: 'timezone',
294
+ Host: {
295
+ SourcePath: '/etc/localtime'
296
+ }
297
+ },
298
+ {
299
+ Name: 'docker_sock',
300
+ Host: {
301
+ SourcePath: '/var/run/docker.sock'
302
+ }
303
+ }
304
+ ])
305
+ }
306
+
307
+ Resource('ProxyService') {
308
+ Type 'AWS::ECS::Service'
309
+ Property('Cluster', Ref('ECSCluster'))
310
+ Property('DesiredCount', 1)
311
+ Property('Role', Ref('ECSRole'))
312
+ Property('TaskDefinition', Ref('ProxyTask'))
313
+ Property('LoadBalancers', [
314
+ { ContainerName: 'proxy', ContainerPort: '80', LoadBalancerName: Ref('CiinaboxProxyELB') }
315
+ ])
316
+ }
317
+
318
+ services.each do |name|
319
+ name.each do |service_name, service|
320
+ params = {
321
+ ECSCluster: Ref('ECSCluster'),
322
+ ECSRole: Ref('ECSRole'),
323
+ ServiceELB: Ref('CiinaboxProxyELB')
324
+ }
325
+ params['InternalELB'] = Ref('CiinaboxProxyELBInternal') if defined? internal_elb and internal_elb
326
+ if (defined? service['params']) and service['params'].kind_of?(Array)
327
+ service['params'].each do |param|
328
+ params.merge!(param)
329
+ end
330
+ end
331
+ # ECS Task Def and Service Stack
332
+ Resource("#{service_name}Stack") {
333
+ Type 'AWS::CloudFormation::Stack'
334
+ Property('TemplateURL', "https://#{source_bucket}.s3.amazonaws.com/ciinabox/#{ciinabox_version}/services/#{service_name}.json")
335
+ Property('TimeoutInMinutes', 5)
336
+ Property('Parameters', params)
337
+ }
338
+ end
339
+ end
340
+ }
@@ -0,0 +1,172 @@
1
+ require 'cfndsl'
2
+ require 'digest'
3
+ require 'base64'
4
+ require_relative '../ext/policies'
5
+ class Lambdas
6
+
7
+
8
+ def initialize(config)
9
+ puts config
10
+ @config = config
11
+ end
12
+
13
+ def create_stack()
14
+ ciinaboxes_dir = ENV['CIINABOXES_DIR'] || 'ciinaboxes'
15
+ source_bucket = @config['source_bucket']
16
+ config = @config
17
+ CloudFormation do
18
+
19
+ # Template metadata
20
+ AWSTemplateFormatVersion "2010-09-09"
21
+ Description "ciinabox - Lambda Functions v#{config['ciinabox_version']}"
22
+
23
+ # Parameters
24
+ Parameter("EnvironmentType") { Type 'String' }
25
+ Parameter("EnvironmentName") { Type 'String' }
26
+ Parameter("VPC") { Type 'String' }
27
+
28
+ # Route Tables
29
+ Parameter("RouteTablePrivateA") { Type 'String' }
30
+ Parameter("RouteTablePrivateB") { Type 'String' }
31
+
32
+ # Public Subnets
33
+ Parameter("SubnetPublicA") { Type 'String' }
34
+ Parameter("SubnetPublicB") { Type 'String' }
35
+
36
+ # Security Groups
37
+ Parameter("SecurityGroupBackplane") { Type 'String' }
38
+ Parameter("SecurityGroupOps") { Type 'String' }
39
+ Parameter("SecurityGroupDev") { Type 'String' }
40
+
41
+ Mapping('EnvironmentType', config['Mappings']['EnvironmentType'])
42
+
43
+ ## Subnets for Lambdas
44
+ config['availability_zones'].each do |az|
45
+ Resource("SubnetPrivate#{az}") {
46
+ Type 'AWS::EC2::Subnet'
47
+ Property('VpcId', Ref('VPC'))
48
+ Property('CidrBlock', FnJoin("", [FnFindInMap('EnvironmentType', 'ciinabox', 'NetworkPrefix'), ".", FnFindInMap('EnvironmentType', 'ciinabox', 'StackOctet'), ".", config['lambdaSubnets']["SubnetOctet#{az}"], ".0/", FnFindInMap('EnvironmentType', 'ciinabox', 'SubnetMask')]))
49
+ Property('AvailabilityZone', FnSelect(config['azId'][az], FnGetAZs(Ref("AWS::Region"))))
50
+ Property('Tags', [{ Key: 'Name', Value: "ciinabox-lambda-private-#{az}" }])
51
+ }
52
+ end
53
+
54
+ config['availability_zones'].each do |az|
55
+ Resource("SubnetRouteTableAssociationPrivate#{az}") {
56
+ Type 'AWS::EC2::SubnetRouteTableAssociation'
57
+ Property('SubnetId', Ref("SubnetPrivate#{az}"))
58
+ Property('RouteTableId', Ref("RouteTablePrivate#{az}"))
59
+ }
60
+ end
61
+
62
+ config['lambdas']['functions'].each do |name, lambda_config|
63
+
64
+
65
+ environment = lambda_config['environment'] || {}
66
+ environment['LAMBDA_PACKAGE_TIMESTAMP'] = lambda_config['timestamp']
67
+ environment['LAMBDA_PACKAGE_SHA256'] = lambda_config['code_sha256']
68
+
69
+ # Create Role for Lambda function
70
+ role_name = lambda_config['role']
71
+ role_config = config['lambdas']['roles'][role_name]
72
+ Resource("LambdaRole#{role_name}") do
73
+ Type 'AWS::IAM::Role'
74
+ Property('AssumeRolePolicyDocument', Statement: [
75
+ Effect: 'Allow',
76
+ Principal: { Service: ['lambda.amazonaws.com'] },
77
+ Action: ['sts:AssumeRole']
78
+ ])
79
+ Property('Path', '/')
80
+ unless role_config['policies_inline'].nil?
81
+ Property('Policies', Policies.new.create_policies(role_config['policies_inline']))
82
+ end
83
+
84
+ unless role_config['policies_managed'].nil?
85
+ Property('ManagedPolicyArns', role_config['policies_managed'])
86
+ end
87
+ end
88
+
89
+ # Create Lambda function
90
+ function_name = name
91
+ Resource(function_name) do
92
+ Type 'AWS::Lambda::Function'
93
+ Property('Code', S3Bucket: source_bucket,
94
+ S3Key: "ciinabox/#{config['ciinabox_version']}/lambdas/#{name}/#{lambda_config['timestamp']}/src.zip")
95
+ Property('Environment', Variables: Hash[environment.collect { |k, v| [k, v] }])
96
+ Property('Handler', lambda_config['handler'] || 'index.handler')
97
+ Property('MemorySize', lambda_config['memory'] || 128)
98
+ Property('Role', FnGetAtt("LambdaRole#{lambda_config['role']}", 'Arn'))
99
+ Property('Runtime', lambda_config['runtime'])
100
+ Property('Timeout', lambda_config['timeout'] || 10)
101
+ if (lambda_config['vpc'] != nil && lambda_config['vpc'])
102
+ Property('VpcConfig', {
103
+ SubnetIds: config['availability_zones'].collect { |az| Ref("SubnetPrivate#{az}") },
104
+ SecurityGroupIds: [Ref('SecurityGroupBackplane')]
105
+ })
106
+ end
107
+ if !lambda_config['named'].nil? && lambda_config['named']
108
+ Property('FunctionName', name)
109
+ end
110
+ end
111
+
112
+ Output("Lambda#{function_name}Arn") {
113
+ Value(
114
+ FnGetAtt(function_name, 'Arn')
115
+ )
116
+ }
117
+
118
+ # Create Lambda version
119
+ sha256 = lambda_config['code_sha256']
120
+ Resource("#{name}Version#{lambda_config['timestamp']}") do
121
+ Type 'AWS::Lambda::Version'
122
+ DeletionPolicy 'Retain'
123
+ Property('FunctionName', Ref(name))
124
+ Property('CodeSha256', sha256)
125
+ end
126
+
127
+ lambda_config['allowed_sources'] = [] if lambda_config['allowed_sources'].nil?
128
+
129
+ # if lambda has schedule defined
130
+ if lambda_config.key?('schedules')
131
+ lambda_config['allowed_sources'] << { 'principal' => 'events.amazonaws.com' }
132
+ lambda_config['schedules'].each_with_index do |schedule, index|
133
+ Resource("Lambda#{name}Schedule#{index}") do
134
+ Type 'AWS::Events::Rule'
135
+ Condition(schedule['condition']) if schedule.key?('condition')
136
+ Property('ScheduleExpression', "cron(#{schedule['cronExpression']})")
137
+ Property('State', 'ENABLED')
138
+ target = {
139
+ 'Arn' => FnGetAtt(name, 'Arn'), 'Id' => "lambda#{name}"
140
+ }
141
+ target['Input'] = schedule['payload'] if schedule.key?('payload')
142
+ Property('Targets', [target])
143
+ end
144
+ end
145
+ end
146
+
147
+ # Generate lambda function Policy
148
+ unless lambda_config['allowed_sources'].nil?
149
+ i = 1
150
+ lambda_config['allowed_sources'].each do |source|
151
+ Resource("#{name}Permissions#{i}") do
152
+ Type 'AWS::Lambda::Permission'
153
+ Property('FunctionName', Ref(name))
154
+ Property('Action', 'lambda:InvokeFunction')
155
+ Property('Principal', source['principal'])
156
+ end
157
+ i += 1
158
+ end
159
+ end
160
+
161
+ end
162
+
163
+ end
164
+ end
165
+
166
+ end
167
+
168
+ if defined? lambdas or config.key? 'lambdas'
169
+ lambdas = Lambdas.new(config)
170
+ lambdas.create_stack()
171
+ end
172
+