lono 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ require 'json'
2
+
3
+ $:.unshift File.dirname(__FILE__)
4
+ require 'lono/cli'
5
+ require 'lono/task'
6
+ require 'lono/dsl'
@@ -0,0 +1,25 @@
1
+ require 'thor'
2
+
3
+ module Lono
4
+ class CLI < Thor
5
+
6
+ desc "init", "Setup lono project"
7
+ long_desc "Sets up config/lono.rb"
8
+ def init
9
+ Lono::Task.init
10
+ end
11
+
12
+ desc "generate", "Generate the cloud formation templates"
13
+ long_desc <<EOL
14
+ Examples:
15
+
16
+ 1. lono generate
17
+
18
+ Builds the cloud formation templates files based on config/lono.rb and writes them to the output folder on the filesystem.
19
+ EOL
20
+ def generate
21
+ Lono::Task.generate(options.dup.merge(:verbose => true))
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,69 @@
1
+ require 'erb'
2
+
3
+ module Lono
4
+ class DSL
5
+ def initialize(options={})
6
+ @options = options
7
+ @path = options[:config_path] || 'config/lono.rb'
8
+ @templates = []
9
+ @results = {}
10
+ end
11
+
12
+ def evaluate
13
+ instance_eval(File.read(@path), @path)
14
+ end
15
+
16
+ def template(name, &block)
17
+ @templates << {:name => name, :block => block}
18
+ end
19
+
20
+ def build
21
+ @templates.each do |t|
22
+ @results[t[:name]] = Template.new(t[:name], t[:block], @options).build
23
+ end
24
+ end
25
+
26
+ def output(options={})
27
+ output_path = options[:output_path] || 'output'
28
+ FileUtils.mkdir(output_path) unless File.exist?(output_path)
29
+ puts "Generating Cloud Formation templates:" if options[:verbose]
30
+ @results.each do |name,json|
31
+ path = "#{output_path}/#{name}"
32
+ puts " #{path}" if options[:verbose]
33
+ File.open(path, 'w') {|f| f.write(json) }
34
+ end
35
+ end
36
+
37
+ def run(options={})
38
+ evaluate
39
+ build
40
+ options.empty? ? output : output(options)
41
+ end
42
+ end
43
+
44
+ class Template
45
+ include ERB::Util
46
+ def initialize(name, block, options={})
47
+ @name = name
48
+ @block = block
49
+ @options = options
50
+ @options[:project_root] ||= '.'
51
+ end
52
+
53
+ def build
54
+ instance_eval(&@block)
55
+ template = IO.read(@source)
56
+ ERB.new(template).result(binding)
57
+ end
58
+
59
+ def source(path)
60
+ @source = "#{@options[:project_root]}/templates/#{path}"
61
+ end
62
+
63
+ def variables(vars={})
64
+ vars.each do |var,value|
65
+ instance_variable_set("@#{var}", value)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,36 @@
1
+ module Lono
2
+ class Task
3
+ def self.init(options={})
4
+ project_root = options[:project_root] || '.'
5
+ puts "Settin up lono project" unless options[:quiet]
6
+ %w[Guardfile config/lono.rb templates/app.json.erb].each do |name|
7
+ source = File.expand_path("../../files/#{name}", __FILE__)
8
+ dirname = File.dirname(name)
9
+ FileUtils.mkdir(dirname) unless File.exist?(dirname)
10
+ dest = "#{project_root}/#{name}"
11
+
12
+ if File.exist?(dest)
13
+ puts "already exists: #{dest}" unless options[:quiet]
14
+ else
15
+ puts "creating: #{dest}" unless options[:quiet]
16
+ FileUtils.cp(source, dest)
17
+ end
18
+ end
19
+ end
20
+ def self.generate(options)
21
+ new(options).generate
22
+ end
23
+
24
+ def initialize(options={})
25
+ @options = options
26
+ if options.empty?
27
+ @dsl = DSL.new
28
+ else
29
+ @dsl = DSL.new(options)
30
+ end
31
+ end
32
+ def generate
33
+ @dsl.run(@options)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ module Lono
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/lono/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Tung Nguyen"]
6
+ gem.email = ["tongueroo@gmail.com"]
7
+ gem.description = %q{Lono generates cloud formation templates based on erb templates.}
8
+ gem.summary = %q{Lono generates cloud formation templates based on erb templates.}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "lono"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Lono::VERSION
17
+
18
+ gem.add_dependency "rake"
19
+ gem.add_dependency "json"
20
+ gem.add_dependency "thor"
21
+ gem.add_dependency "aws-sdk"
22
+ gem.add_dependency 'guard'
23
+ gem.add_dependency 'rb-fsevent'
24
+ gem.add_dependency "guard-cloudformation"
25
+ gem.add_dependency "guard-lono"
26
+
27
+ gem.add_development_dependency 'rspec'
28
+ gem.add_development_dependency 'guard-rspec'
29
+ gem.add_development_dependency 'guard-bundler'
30
+
31
+ end
@@ -0,0 +1,47 @@
1
+ require File.expand_path("../../spec_helper", __FILE__)
2
+
3
+ describe Lono do
4
+ before(:each) do
5
+ @project_root = File.expand_path("../../project", __FILE__)
6
+ @dsl = Lono::DSL.new(
7
+ :config_path => "#{@project_root}/config/lono.rb",
8
+ :project_root => @project_root
9
+ )
10
+ @dsl.evaluate
11
+ end
12
+
13
+ after(:each) do
14
+ FileUtils.rm_rf("#{@project_root}/output")
15
+ end
16
+
17
+ it "should generate cloud formation template" do
18
+ @dsl.build
19
+ @dsl.output(:output_path => "#{@project_root}/output")
20
+ raw = IO.read("#{@project_root}/output/prod-api-app.json")
21
+ json = JSON.load(raw)
22
+ json['Description'].should == "Api Stack"
23
+ json['Mappings']['AWSRegionArch2AMI']['us-east-1']['64'].should == 'ami-123'
24
+ end
25
+ end
26
+
27
+ describe Lono::Task do
28
+ before(:each) do
29
+ @project_root = File.expand_path("../../project", __FILE__)
30
+ end
31
+
32
+ after(:each) do
33
+ FileUtils.rm_rf("#{@project_root}/output")
34
+ end
35
+
36
+ it "task should generate cloud formation templates" do
37
+ Lono::Task.generate(
38
+ :project_root => @project_root,
39
+ :config_path => "#{@project_root}/config/lono.rb",
40
+ :output_path => "#{@project_root}/output"
41
+ )
42
+ raw = IO.read("#{@project_root}/output/prod-api-app.json")
43
+ json = JSON.load(raw)
44
+ json['Description'].should == "Api Stack"
45
+ json['Mappings']['AWSRegionArch2AMI']['us-east-1']['64'].should == 'ami-123'
46
+ end
47
+ end
@@ -0,0 +1,39 @@
1
+ template "prod-api-app.json" do
2
+ source "app.json.erb"
3
+ variables(
4
+ :env => 'prod',
5
+ :app => 'api',
6
+ :role => "app",
7
+ :ami => "ami-123",
8
+ :instance_type => "c1.xlarge",
9
+ :port => "80",
10
+ :high_threshold => "15",
11
+ :high_periods => "4",
12
+ :low_threshold => "5",
13
+ :low_periods => "10",
14
+ :max_size => "24",
15
+ :min_size => "6",
16
+ :down_adjustment => "-3",
17
+ :up_adjustment => "3",
18
+ :ssl_cert => "arn:aws:iam::12345:server-certificate/wildcard"
19
+ )
20
+ end
21
+ template "prod-br-app.json" do
22
+ source "app.json.erb"
23
+ variables(
24
+ :env => "prod",
25
+ :app => 'br',
26
+ :role => "app",
27
+ :ami => "ami-456",
28
+ :instance_type => "m1.medium",
29
+ :port => "80",
30
+ :high_threshold => "35",
31
+ :high_periods => "4",
32
+ :low_threshold => "20",
33
+ :low_periods => "2",
34
+ :max_size => "6",
35
+ :min_size => "3",
36
+ :down_adjustment => "-1",
37
+ :up_adjustment => "2"
38
+ )
39
+ end
@@ -0,0 +1,424 @@
1
+ {
2
+ "AWSTemplateFormatVersion": "2010-09-09",
3
+ "Description": "<%= @app.capitalize %> Stack",
4
+ "Mappings": {
5
+ "AWSInstanceType2Arch": {
6
+ "c1.medium": {
7
+ "Arch": "64"
8
+ },
9
+ "c1.xlarge": {
10
+ "Arch": "64"
11
+ },
12
+ "cc1.4xlarge": {
13
+ "Arch": "64"
14
+ },
15
+ "cc2.8xlarge": {
16
+ "Arch": "64"
17
+ },
18
+ "cg1.4xlarge": {
19
+ "Arch": "64"
20
+ },
21
+ "m1.large": {
22
+ "Arch": "64"
23
+ },
24
+ "m1.medium": {
25
+ "Arch": "64"
26
+ },
27
+ "m1.small": {
28
+ "Arch": "64"
29
+ },
30
+ "m1.xlarge": {
31
+ "Arch": "64"
32
+ },
33
+ "m2.2xlarge": {
34
+ "Arch": "64"
35
+ },
36
+ "m2.4xlarge": {
37
+ "Arch": "64"
38
+ },
39
+ "m2.xlarge": {
40
+ "Arch": "64"
41
+ },
42
+ "t1.micro": {
43
+ "Arch": "64"
44
+ }
45
+ },
46
+ "AWSRegionArch2AMI": {
47
+ "us-east-1": {
48
+ "32": "",
49
+ "64": "<%= @ami %>"
50
+ },
51
+ "us-west-1": {
52
+ "32": "",
53
+ "64": ""
54
+ }
55
+ }
56
+ },
57
+ "Outputs": {
58
+ "ELBHostname": {
59
+ "Description": "The URL of the website",
60
+ "Value": {
61
+ "Fn::Join": [
62
+ "",
63
+ [
64
+ "http://",
65
+ {
66
+ "Fn::GetAtt": [
67
+ "elb",
68
+ "DNSName"
69
+ ]
70
+ }
71
+ ]
72
+ ]
73
+ }
74
+ }
75
+ },
76
+ "Parameters": {
77
+ "Application": {
78
+ "Default": "<%= @app %>",
79
+ "Description": "Application name",
80
+ "Type": "String"
81
+ },
82
+ "Environment": {
83
+ "Default": "<%= @env %>",
84
+ "Description": "stag, prod etc",
85
+ "Type": "String"
86
+ },
87
+ "InstanceType": {
88
+ "AllowedValues": [
89
+ "t1.micro",
90
+ "m1.small",
91
+ "m1.medium",
92
+ "m1.large",
93
+ "m1.xlarge",
94
+ "m2.xlarge",
95
+ "m2.2xlarge",
96
+ "m2.4xlarge",
97
+ "c1.medium",
98
+ "c1.xlarge",
99
+ "cc1.4xlarge",
100
+ "cc2.8xlarge",
101
+ "cg1.4xlarge"
102
+ ],
103
+ "ConstraintDescription": "must be a valid EC2 instance type.",
104
+ "Default": "<%= @instance_type %>",
105
+ "Description": "WebServer EC2 instance type",
106
+ "Type": "String"
107
+ },
108
+ "KeyName": {
109
+ "Default": "default",
110
+ "Description": "The EC2 Key Pair to allow SSH access to the instances",
111
+ "Type": "String"
112
+ },
113
+ "Role": {
114
+ "Default": "<%= @role %>",
115
+ "Description": "redis, psql, app, etc",
116
+ "Type": "String"
117
+ },
118
+ "StackNumber": {
119
+ "Description": "s1, s2, s3, etc",
120
+ "Type": "String"
121
+ },
122
+ "WebServerPort": {
123
+ "Default": "<%= @port %>",
124
+ "Description": "The TCP port for the Web Server",
125
+ "Type": "Number"
126
+ }
127
+ },
128
+ "Resources": {
129
+ "CPUAlarmHigh": {
130
+ "Properties": {
131
+ "AlarmActions": [
132
+ {
133
+ "Ref": "WebServerScaleUpPolicy"
134
+ }
135
+ ],
136
+ "AlarmDescription": "Scale-up if CPU > <%= @high_threshold %>% for <%= @high_mins %> minutes",
137
+ "ComparisonOperator": "GreaterThanThreshold",
138
+ "Dimensions": [
139
+ {
140
+ "Name": "AutoScalingGroupName",
141
+ "Value": {
142
+ "Ref": "WebServerGroup"
143
+ }
144
+ }
145
+ ],
146
+ "EvaluationPeriods": "<%= @high_periods %>",
147
+ "MetricName": "CPUUtilization",
148
+ "Namespace": "AWS/EC2",
149
+ "Period": "60",
150
+ "Statistic": "Average",
151
+ "Threshold": "<%= @high_threshold %>"
152
+ },
153
+ "Type": "AWS::CloudWatch::Alarm"
154
+ },
155
+ "CPUAlarmLow": {
156
+ "Properties": {
157
+ "AlarmActions": [
158
+ {
159
+ "Ref": "WebServerScaleDownPolicy"
160
+ }
161
+ ],
162
+ "AlarmDescription": "Scale-down if CPU < <%= @low_threshold %>% for 10 minutes",
163
+ "ComparisonOperator": "LessThanThreshold",
164
+ "Dimensions": [
165
+ {
166
+ "Name": "AutoScalingGroupName",
167
+ "Value": {
168
+ "Ref": "WebServerGroup"
169
+ }
170
+ }
171
+ ],
172
+ "EvaluationPeriods": "<%= @low_periods %>",
173
+ "MetricName": "CPUUtilization",
174
+ "Namespace": "AWS/EC2",
175
+ "Period": "60",
176
+ "Statistic": "Average",
177
+ "Threshold": "<%= @low_threshold %>"
178
+ },
179
+ "Type": "AWS::CloudWatch::Alarm"
180
+ },
181
+ "HostRecord": {
182
+ "Properties": {
183
+ "Comment": "DNS name for my stack.",
184
+ "HostedZoneName": "mydomain.net.",
185
+ "Name": {
186
+ "Fn::Join": [
187
+ "",
188
+ [
189
+ {
190
+ "Ref": "AWS::StackName"
191
+ },
192
+ ".mydomain.net"
193
+ ]
194
+ ]
195
+ },
196
+ "ResourceRecords": [
197
+ {
198
+ "Fn::GetAtt": [
199
+ "elb",
200
+ "DNSName"
201
+ ]
202
+ }
203
+ ],
204
+ "TTL": "60",
205
+ "Type": "CNAME"
206
+ },
207
+ "Type": "AWS::Route53::RecordSet"
208
+ },
209
+ "LaunchConfig": {
210
+ "Properties": {
211
+ "BlockDeviceMappings": [
212
+ {
213
+ "DeviceName": "/dev/sdb",
214
+ "VirtualName": "ephemeral0"
215
+ }
216
+ ],
217
+ "ImageId": {
218
+ "Fn::FindInMap": [
219
+ "AWSRegionArch2AMI",
220
+ {
221
+ "Ref": "AWS::Region"
222
+ },
223
+ {
224
+ "Fn::FindInMap": [
225
+ "AWSInstanceType2Arch",
226
+ {
227
+ "Ref": "InstanceType"
228
+ },
229
+ "Arch"
230
+ ]
231
+ }
232
+ ]
233
+ },
234
+ "InstanceType": {
235
+ "Ref": "InstanceType"
236
+ },
237
+ "KeyName": {
238
+ "Ref": "KeyName"
239
+ },
240
+ "SecurityGroups": [
241
+ "global",
242
+ {
243
+ "Fn::Join": [
244
+ "-",
245
+ [
246
+ {
247
+ "Ref": "Environment"
248
+ },
249
+ {
250
+ "Ref": "Application"
251
+ }
252
+ ]
253
+ ]
254
+ },
255
+ {
256
+ "Ref": "ServiceSecurityGroup"
257
+ }
258
+ ],
259
+ "UserData": {
260
+ "Fn::Base64": {
261
+ "Fn::Join": [
262
+ "",
263
+ [
264
+ "#!/bin/bash -lexv\n",
265
+ "exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1\n",
266
+ "echo ",
267
+ {
268
+ "Ref": "AWS::StackName"
269
+ },
270
+ " > /tmp/stackname\n",
271
+ "echo \"\" >> /etc/profile.d/base.sh\n",
272
+ "'\" >> /etc/profile.d/base.sh\n",
273
+ "source /etc/profile.d/base.sh\n",
274
+ "set +e\n",
275
+ "rvm use system --default\n",
276
+ "set -e\n",
277
+ "echo 'running' > /tmp/type-of-instance\n",
278
+ "cat /proc/uptime | cut -f1 -d'.' > /tmp/time-to-boot\n"
279
+ ]
280
+ ]
281
+ }
282
+ }
283
+ },
284
+ "Type": "AWS::AutoScaling::LaunchConfiguration"
285
+ },
286
+ "ServiceSecurityGroup": {
287
+ "Properties": {
288
+ "GroupDescription": "Enable SSH access and HTTP from the load balancer only",
289
+ "SecurityGroupIngress": [
290
+ {
291
+ "CidrIp": "0.0.0.0/0",
292
+ "FromPort": "22",
293
+ "IpProtocol": "tcp",
294
+ "ToPort": "22"
295
+ },
296
+ {
297
+ "FromPort": {
298
+ "Ref": "WebServerPort"
299
+ },
300
+ "IpProtocol": "tcp",
301
+ "SourceSecurityGroupName": {
302
+ "Fn::GetAtt": [
303
+ "elb",
304
+ "SourceSecurityGroup.GroupName"
305
+ ]
306
+ },
307
+ "SourceSecurityGroupOwnerId": {
308
+ "Fn::GetAtt": [
309
+ "elb",
310
+ "SourceSecurityGroup.OwnerAlias"
311
+ ]
312
+ },
313
+ "ToPort": {
314
+ "Ref": "WebServerPort"
315
+ }
316
+ }
317
+ ]
318
+ },
319
+ "Type": "AWS::EC2::SecurityGroup"
320
+ },
321
+ "WebServerGroup": {
322
+ "Properties": {
323
+ "AvailabilityZones": [
324
+ "us-east-1a",
325
+ "us-east-1d",
326
+ "us-east-1e"
327
+ ],
328
+ "HealthCheckGracePeriod": "300",
329
+ "HealthCheckType": "ELB",
330
+ "LaunchConfigurationName": {
331
+ "Ref": "LaunchConfig"
332
+ },
333
+ "LoadBalancerNames": [
334
+ {
335
+ "Ref": "elb"
336
+ }
337
+ ],
338
+ "MaxSize": "<%= @max_size %>",
339
+ "MinSize": "<%= @min_size %>",
340
+ "NotificationConfiguration": {
341
+ "NotificationTypes": [
342
+ "autoscaling:EC2_INSTANCE_LAUNCH",
343
+ "autoscaling:EC2_INSTANCE_LAUNCH_ERROR",
344
+ "autoscaling:EC2_INSTANCE_TERMINATE",
345
+ "autoscaling:EC2_INSTANCE_TERMINATE_ERROR"
346
+ ],
347
+ "TopicARN": [
348
+ "arn:aws:sns:us-east-1:867690557112:ops"
349
+ ]
350
+ }
351
+ },
352
+ "Type": "AWS::AutoScaling::AutoScalingGroup"
353
+ },
354
+ "WebServerScaleDownPolicy": {
355
+ "Properties": {
356
+ "AdjustmentType": "ChangeInCapacity",
357
+ "AutoScalingGroupName": {
358
+ "Ref": "WebServerGroup"
359
+ },
360
+ "Cooldown": "120",
361
+ "ScalingAdjustment": "<%= @down_adjustment %>"
362
+ },
363
+ "Type": "AWS::AutoScaling::ScalingPolicy"
364
+ },
365
+ "WebServerScaleUpPolicy": {
366
+ "Properties": {
367
+ "AdjustmentType": "ChangeInCapacity",
368
+ "AutoScalingGroupName": {
369
+ "Ref": "WebServerGroup"
370
+ },
371
+ "Cooldown": "120",
372
+ "ScalingAdjustment": "<%= @up_adjustment %>"
373
+ },
374
+ "Type": "AWS::AutoScaling::ScalingPolicy"
375
+ },
376
+ "elb": {
377
+ "Properties": {
378
+ "AvailabilityZones": [
379
+ "us-east-1a",
380
+ "us-east-1d",
381
+ "us-east-1e"
382
+ ],
383
+ "HealthCheck": {
384
+ "HealthyThreshold": "3",
385
+ "Interval": "6",
386
+ "Target": {
387
+ "Fn::Join": [
388
+ "",
389
+ [
390
+ "HTTP:",
391
+ {
392
+ "Ref": "WebServerPort"
393
+ },
394
+ "/up/elb"
395
+ ]
396
+ ]
397
+ },
398
+ "Timeout": "5",
399
+ "UnhealthyThreshold": "5"
400
+ },
401
+ "Listeners": [
402
+ {
403
+ "InstancePort": {
404
+ "Ref": "WebServerPort"
405
+ },
406
+ "LoadBalancerPort": "80",
407
+ "Protocol": "HTTP"
408
+ },
409
+ {
410
+ "InstancePort": {
411
+ "Ref": "WebServerPort"
412
+ },
413
+ "LoadBalancerPort": "443",
414
+ "PolicyNames": [],
415
+ "Protocol": "HTTPS",
416
+ "SSLCertificateId": "<%= @ssl_cert %>"
417
+ }
418
+
419
+ ]
420
+ },
421
+ "Type": "AWS::ElasticLoadBalancing::LoadBalancer"
422
+ }
423
+ }
424
+ }