rubycfn 0.5.2 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ef7317d69104c7137bc0010d44e5170dcd5dd41057be8b9bc6a0c146cba8e4b
4
- data.tar.gz: 404876907399ca2637709f6be230a90159ad3555ea68435e38a47558df994487
3
+ metadata.gz: 337ae0668503c4e97c9a3b8a3ed2693d5611ecb592ad2802b83e158f1a2b32f5
4
+ data.tar.gz: 25e6d98d9f4ecb0f80fb8a1c55bd907ff2af293ca04e8186774ffec5375948ec
5
5
  SHA512:
6
- metadata.gz: 3f928a0f477cce0be833e586571df27861e41a5ce37c251f919ae712638d3653a2d9d5a25fea3d5a8f01b25bc36d730b4d63ddc8bdce003a9b3cfdfea7299031
7
- data.tar.gz: e9a9d01201c7fd0cf73e136820044951e48629f2752626203ce98bc3a22a12e4f1ddb7c2623cd6e3e45ad2523501df93a9c3f2e59679f7f7a33061e279ec68d9
6
+ metadata.gz: 5de8efbd6b5b242f5f63a336f82602dcb96d6824b4bbf763c62bba0661bf1c811a1c453dfbcef7e20b6650cba46660758a5f99e271b70481e9850c51ba3bf9f5
7
+ data.tar.gz: '08fdaabf55a7e3ed1e6bf60b66b038f8729433d257c59fe74323042d178c6f0e0a1d52fd12aae13ea482b14ea986326614f79efb7a412cd2b1eea6932fc0fc83'
@@ -2,7 +2,11 @@
2
2
  All notable changes to Rubycfn will be documented in this file.
3
3
  This project uses [Semantic Versioning](http://semver.org/).
4
4
 
5
- ## 0.5.3 (Next Release)
5
+ ## 0.5.4 (Next Release)
6
+
7
+ ## 0.5.3
8
+
9
+ * Improved code quality in templated files -- [@dennisvink][@dennisvink]
6
10
 
7
11
  ## 0.5.2
8
12
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rubycfn (0.5.2)
4
+ rubycfn (0.5.3)
5
5
  activesupport (~> 5.1.5)
6
6
  dotenv (~> 2.4.0)
7
7
  json (~> 2.1.0)
@@ -77,7 +77,7 @@ GEM
77
77
  rspec-mocks (~> 3.9.0)
78
78
  rspec-core (3.9.1)
79
79
  rspec-support (~> 3.9.1)
80
- rspec-expectations (3.9.0)
80
+ rspec-expectations (3.9.1)
81
81
  diff-lcs (>= 1.2.0, < 2.0)
82
82
  rspec-support (~> 3.9.0)
83
83
  rspec-given (3.8.0)
data/README.md CHANGED
@@ -61,7 +61,7 @@ __________ ____ __________________.___._________ _____________________
61
61
  | _/ | /| | _// | |/ \ \/ | __) | | _/
62
62
  | | \ | / | | \\____ |\ \____| \ | | \
63
63
  |____|_ /______/ |______ // ______| \______ /\___ / |______ /
64
- \/ \/ \/ \/ \/ \/ [v0.5.2]
64
+ \/ \/ \/ \/ \/ \/ [v0.5.3]
65
65
  Project name? example
66
66
  Account ID? 1234567890
67
67
  Select region EU (Frankfurt)
@@ -159,12 +159,11 @@ You can reuse these environment variables in your project code.
159
159
  The `.env.rspec` is used when running unit tests. It contains mock variables
160
160
  so that you can test the resulting CloudFormation templates properly.
161
161
 
162
- #### The missing .env.private file
162
+ #### The .env.private file
163
163
 
164
- There is one file that is not generated by default but does need mentioning:
165
- the `.env.private` file. This is a special file that allows you to override
166
- environment variables. An environment variable set in .env.private always takes
167
- precedence over environment variables set in other .env files.
164
+ This is a special file that allows you to override environment variables.
165
+ An environment variable set in .env.private always takes precedence over
166
+ environment variables set in other .env files.
168
167
 
169
168
  #### .rubocop.yml
170
169
 
@@ -1,4 +1,4 @@
1
1
  # Rubycfn version
2
2
  module Rubycfn
3
- VERSION = "0.5.2".freeze
3
+ VERSION = "0.5.3".freeze
4
4
  end
@@ -83,5 +83,9 @@ attic/
83
83
  /venv
84
84
  /Gemfile.lock
85
85
  .env.dependencies
86
+ .env.dependencies.development
87
+ .env.dependencies.test
88
+ .env.dependencies.acceptance
89
+ .env.dependencies.production
86
90
  CloudFormationResourceSpecification.json
87
91
  config.json
@@ -102,3 +102,6 @@ Metrics/PerceivedComplexity:
102
102
 
103
103
  Style/EvalWithLocation:
104
104
  Enabled: false
105
+
106
+ Style/PerlBackrefs:
107
+ Enabled: false
@@ -21,7 +21,7 @@ __________ ____ __________________.___._________ _____________________
21
21
  `rake compile` - Compile the code into CloudFormation templates
22
22
  `rake spec` - Run unit tests
23
23
  `rake upload` - Upload the CloudFormation templates to s3
24
- `rake update` - Save required outputs from DependencyStack to .env.dependencies
24
+ `rake update` - Save required outputs from DependencyStack to .env.dependencies.<ENVIRONMENT>
25
25
  `rake apply` - Deploy the CloudFormation templates
26
26
 
27
27
  ## Stack configuration
@@ -36,7 +36,7 @@ task :clean do
36
36
  end
37
37
  end
38
38
 
39
- desc "Store dependencies of DependencyStack in .env.dependencies"
39
+ desc "Store dependencies of DependencyStack in .env.dependencies.<ENVIRONMENT>"
40
40
  task :dependencies do
41
41
  require_relative "lib/core/dependencies.rb"
42
42
  end
@@ -51,8 +51,7 @@ RuboCop::RakeTask.new(:rubocop) do |t|
51
51
  t.options = ["--display-cop-names"]
52
52
  end
53
53
 
54
- task apply: %i(dependencies apply_stack)
55
- task compile: %i(compile_stack)
56
54
  task default: %i(dependencies compile_stack spec)
57
- task update: %i(dependencies)
55
+ task compile: %i(dependencies compile_stack)
58
56
  task upload: %i(dependencies upload_stack)
57
+ task apply: %i(dependencies apply_stack)
@@ -52,6 +52,9 @@ applications:
52
52
  env:
53
53
  SOME_ENV_VAR: Exposed
54
54
  SOME_OTHER_VAR: desopxE
55
+ # SOME_REFFED_VAR: :foobar.ef
56
+ # SOME_OTHER_STACK_VAR: :vpc_stack.ref("Outputs.VpcId")
57
+ # SOME_ENV_VAR: ${MY_ENV_VAR}
55
58
  hello-world2:
56
59
  image: tutum/hello-world
57
60
  container_port: 80
@@ -2,7 +2,7 @@ require "git-revision"
2
2
 
3
3
  def update_references(contents, environment, _artifact_bucket)
4
4
  Dotenv.load(".env.private")
5
- Dotenv.load(".env.dependencies")
5
+ Dotenv.load(".env.dependencies.#{ENV["ENVIRONMENT"]}")
6
6
  contents["Resources"].map do |resource|
7
7
  resource_name = resource.shift
8
8
  resource_values = resource.shift
@@ -1,6 +1,14 @@
1
+ def infra_config
2
+ config = YAML.safe_load(File.read("config.yaml"), [Symbol])
3
+ config["applications"] ||= {}
4
+ config["environments"] ||= {}
5
+ config["subnets"] ||= {}
6
+ config
7
+ end
8
+
1
9
  def load_env_vars
2
10
  Dotenv.load(".env.private")
3
- Dotenv.load(".env.dependencies")
11
+ Dotenv.load(".env.dependencies.#{ENV["ENVIRONMENT"]}")
4
12
  Dotenv.load(".env")
5
13
  Dotenv.load(".env.#{ENV["ENVIRONMENT"]}")
6
14
 
@@ -12,7 +20,7 @@ def load_env_vars
12
20
  aws_secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
13
21
  cloudformation_bucket: ENV["CLOUDFORMATIONBUCKET"],
14
22
  environment: ENV["ENVIRONMENT"],
15
- stack_name: ENV["STACK_NAME"]
23
+ stack_name: infra_config["environments"][ENV["ENVIRONMENT"]]["stack_name"]
16
24
  }
17
25
  end
18
26
 
@@ -22,6 +30,6 @@ def check_dependencies
22
30
  raise "CLOUDFORMATIONBUCKET not set. Run `rake init` and `rake update` first!" unless ENV["CLOUDFORMATIONBUCKET"]
23
31
  raise "ARTIFACTBUCKET not set. Run `rake init` and `rake update` first!" unless ENV["ARTIFACTBUCKET"]
24
32
  raise "ENVIRONMENT not set." unless ENV["ENVIRONMENT"]
25
- raise "STACK_NAME not set" unless ENV["STACK_NAME"]
33
+ raise "`stack_name` not configured in config.yaml" unless infra_config["environments"][ENV["ENVIRONMENT"]]["stack_name"]
26
34
  raise "AWS CREDENTIALS NOT SET" unless ENV["AWS_ACCESS_KEY_ID"] && ENV["AWS_SECRET_ACCESS_KEY"]
27
35
  end
@@ -2,7 +2,7 @@ require_relative "../core/git"
2
2
 
3
3
  def upload_stacks
4
4
  Dotenv.load(".env.private")
5
- Dotenv.load(".env.dependencies")
5
+ Dotenv.load(".env.dependencies.#{ENV["ENVIRONMENT"]}")
6
6
  env_vars = load_env_vars
7
7
 
8
8
  set_aws_credentials(
@@ -11,17 +11,10 @@ def upload_stacks
11
11
  env_vars[:aws_secret_access_key]
12
12
  )
13
13
 
14
- begin
15
- s3 = create_bucket_if_not_exists(
16
- env_vars[:aws_region],
17
- env_vars[:artifact_bucket]
18
- )
19
- rescue => e
20
- puts "Exception create_bucket_if_not_exists #{e}"
21
- end
14
+ s3 = Aws::S3::Resource.new(region: env_vars[:aws_region])
22
15
 
23
16
  stacks = compile_stacks(true)
24
- raise "CLOUDFORMATIONBUCKET not found in DependencyStack outputs" unless ENV["CLOUDFORMATIONBUCKET"]
17
+ raise "CLOUDFORMATIONBUCKET not found in <%= project_name %>#{ENV["ENVIRONMENT"].capitalize}DependencyStack outputs" unless ENV["CLOUDFORMATIONBUCKET"]
25
18
  stacks.each do |stack_name, stack|
26
19
  next if JSON.parse(stack)["Resources"].nil?
27
20
  hash = @stack_hashes[stack_name.to_sym]
@@ -1,44 +1,97 @@
1
+ def to_bool(val)
2
+ return false unless val.nil? || val.empty?
3
+ return false unless val != "true"
4
+ true
5
+ end
6
+
7
+ def env_vars_to_array(val)
8
+ if val.nil? || val.empty?
9
+ return [
10
+ {
11
+ Name: "WITHOUT_ENV",
12
+ Value: "true"
13
+ }
14
+ ]
15
+ end
16
+ vars = []
17
+
18
+ # Create an array of variables to pass to this application.
19
+ # Resolve any references to its intrinsic function.
20
+ val.keys.each_with_index do |object, index|
21
+ value = val.values[index].class == Symbol && Kernel.class_eval(":#{val.values[index]}") || val.values[index]
22
+
23
+ # Support ${ENV_VAR} in config_yaml
24
+ if value =~ /\$\{([^\}]*)\}/
25
+ capture = $1
26
+ value = ENV[capture]
27
+ end
28
+
29
+ vars.push(
30
+ Name: object,
31
+ Value: value
32
+ )
33
+ end
34
+ vars
35
+ end
36
+
37
+ # Return the mandatory Stack parameters and add all defined ENV vars from config.yaml
38
+ def parent_parameters(env_vars)
39
+ params = {
40
+ "Vpc": :vpc_stack.ref("Outputs.Vpc"),
41
+ "Cluster": :ecs_stack.ref("Outputs.EcsCluster"),
42
+ "Listener": :ecs_stack.ref("Outputs.EcsLoadBalancerListener"),
43
+ "EcsServiceAutoScalingRoleArn": :ecs_stack.ref("Outputs.EcsAutoScalingRoleArn"),
44
+ "HostedZoneId": "HOSTEDZONEID".ref,
45
+ "HostedZoneName": "HOSTEDZONENAME".ref,
46
+ "LoadBalancerDnsName": :ecs_stack.ref("Outputs.EcsLoadBalancerUrl"),
47
+ "CanonicalHostedZoneId": :ecs_stack.ref("Outputs.EcsLoadBalancerHostedZoneId"),
48
+ "CertificateProviderFunctionArn": :acm_stack.ref("Outputs.CertificateProviderFunctionArn"),
49
+ "ApplicationDeploymentFailureRollbackFunctionArn": :ecs_stack.ref("Outputs.ApplicationDeploymentFailureRollbackFunctionArn")
50
+ }
51
+
52
+ env_vars.each do |var|
53
+ params[var[:Name].cfnize] = var[:Value]
54
+ end
55
+ params
56
+ end
57
+
58
+ def application_parameters(env_vars)
59
+ vars = []
60
+
61
+ env_vars.each do |var|
62
+ vars.push(
63
+ Name: var[:Name],
64
+ Value: var[:Name].cfnize.ref
65
+ )
66
+ end
67
+ vars
68
+ end
69
+
1
70
  # Generate nested stacks for each defined application, and create module dynamically.
2
71
  def create_applications
72
+ return if infra_config["environments"][environment]["cluster_size"].nil? || infra_config["environments"][environment]["cluster_size"].to_i.zero?
3
73
  resource :applications_stack,
4
74
  amount: infra_config["applications"].count,
5
75
  type: "AWS::CloudFormation::Stack" do |r, index|
76
+ env_vars = env_vars_to_array(infra_config["applications"].values[index]["env"])
77
+ application_vars = application_parameters(env_vars) # rubocop:disable Lint/UselessAssignment
6
78
  name = infra_config["applications"].keys[index]
7
79
  resource_name = name.tr("-", "_").cfnize
8
80
  simple_name = resource_name.downcase
9
81
  app_config = infra_config["applications"].values[index]
10
- warn "App config is empty" unless app_config # Rubocop didn't understand that the variable is actually used.
11
- # request_certificate("Gateway", gateway_domains[environment], :asellion_com_hosted_zone_id.ref, "us-east-1")
82
+ is_essential = to_bool(app_config["essential"].to_s)
12
83
 
13
84
  r._id("#{resource_name}Stack")
14
85
  r.property(:template_url) { "#{simple_name}stack" }
15
- r.property(:parameters) do
16
- {
17
- "Vpc": :vpc_stack.ref("Outputs.SfsVpc"),
18
- "Cluster": :ecs_stack.ref("Outputs.SfsEcsCluster"),
19
- "Listener": :ecs_stack.ref("Outputs.EcsLoadBalancerListener"),
20
- "EcsServiceAutoScalingRoleArn": :ecs_stack.ref("Outputs.SfsEcsAutoScalingRoleArn"),
21
- "HostedZoneId": :route53_stack.ref("Outputs.HostedZoneId"),
22
- "HostedZoneName": :route53_stack.ref("Outputs.HostedZoneName"),
23
- "LoadBalancerDnsName": :ecs_stack.ref("Outputs.EcsLoadBalancerUrl"),
24
- "CanonicalHostedZoneId": :ecs_stack.ref("Outputs.EcsLoadBalancerHostedZoneId"),
25
- "CertificateProviderFunctionArn": :acm_stack.ref("Outputs.CertificateProviderFunctionArn")
26
- }
27
- end
86
+ r.property(:parameters) { parent_parameters(env_vars) }
28
87
  Object.const_set("#{resource_name}Stack", Module.new).class_eval <<-RUBY
29
88
  extend ActiveSupport::Concern
30
89
  include Rubycfn
31
90
 
32
91
  included do
33
- def env_vars_to_array(val)
34
- vars = []
35
- val.keys.each_with_index do |object, index|
36
- vars.push(
37
- Name: object,
38
- Value: val.values[index]
39
- )
40
- end
41
- vars
92
+
93
+ application_vars.each do |var|
94
+ Object.class_eval("parameter var[:Name].to_sym, description: 'Value for ENV var \#{var[:Name]}'")
42
95
  end
43
96
 
44
97
  parameter :vpc,
@@ -77,6 +130,10 @@ def create_applications
77
130
  description: "ARN of certificate provider",
78
131
  type: "String"
79
132
 
133
+ parameter :application_deployment_failure_rollback_function_arn,
134
+ description: "ARN of ECS deployment failure Lambda",
135
+ type: "String"
136
+
80
137
  variable :min,
81
138
  value: app_config["min"].to_s
82
139
 
@@ -103,7 +160,7 @@ def create_applications
103
160
  description generate_stack_description("#{resource_name}Stack")
104
161
  resource :service,
105
162
  type: "AWS::ECS::Service" do |r|
106
-
163
+ r.property(:service_name) { "#{environment}-<%= project_name %>-#{simple_name}" }
107
164
  r.property(:cluster) { :cluster.ref }
108
165
  r.property(:role) { :service_role.ref }
109
166
  r.property(:desired_count) { min.to_i }
@@ -117,6 +174,14 @@ def create_applications
117
174
  end
118
175
  end
119
176
 
177
+ resource :application_deployment_health,
178
+ type: "Custom::EcsDeploymentCheck" do |r|
179
+ r.property(:service_token) { :application_deployment_failure_rollback_function_arn.ref }
180
+ r.property("AWSRegion") { "${AWS::Region}".fnsub }
181
+ r.property(:service) { "#{environment}-<%= project_name %>-#{simple_name}" }
182
+ r.property(:cluster) { :cluster.ref }
183
+ end
184
+
120
185
  resource :task_definition,
121
186
  type: "AWS::ECS::TaskDefinition" do |r|
122
187
  r.property(:family) { "#{simple_name}-service" }
@@ -124,10 +189,10 @@ def create_applications
124
189
  [
125
190
  {
126
191
  "Name": "#{simple_name}-service",
127
- "Essential": true,
192
+ "Essential": #{is_essential},
128
193
  "Image": image,
129
194
  "Memory": memory.to_i,
130
- "Environment": env_vars,
195
+ "Environment": application_vars,
131
196
  "PortMappings": [
132
197
  {
133
198
  "ContainerPort": container_port.to_i
@@ -235,6 +300,27 @@ def create_applications
235
300
  "elasticloadbalancing:RegisterTargets"
236
301
  ],
237
302
  "Resource": "*"
303
+ },
304
+ {
305
+ "Effect": "Allow",
306
+ "Action": [
307
+ "ec2:DescribeTags",
308
+ "ecs:CreateCluster",
309
+ "ecs:DeregisterContainerInstance",
310
+ "ecs:DiscoverPollEndpoint",
311
+ "ecs:Poll",
312
+ "ecs:RegisterContainerInstance",
313
+ "ecs:StartTelemetrySession",
314
+ "ecs:UpdateContainerInstancesState",
315
+ "ecs:Submit*",
316
+ "ecr:GetAuthorizationToken",
317
+ "ecr:BatchCheckLayerAvailability",
318
+ "ecr:GetDownloadUrlForLayer",
319
+ "ecr:BatchGetImage",
320
+ "logs:CreateLogStream",
321
+ "logs:PutLogEvents"
322
+ ],
323
+ "Resource": "*"
238
324
  }
239
325
  ]
240
326
  }
@@ -402,7 +488,10 @@ def create_applications
402
488
  end
403
489
 
404
490
  resource :cloudfront_distribution,
405
- depends_on: :ecs_application_issued_certificate,
491
+ depends_on: [
492
+ :ecs_application_issued_certificate,
493
+ :application_deployment_health
494
+ ],
406
495
  type: "AWS::CloudFront::Distribution" do |r|
407
496
  r.property(:distribution_config) do
408
497
  {
@@ -13,15 +13,15 @@ client = Aws::CloudFormation::Client.new(region: ENV["AWS_REGION"])
13
13
 
14
14
  begin
15
15
  res = client.describe_stacks(
16
- stack_name: "DependencyStack"
16
+ stack_name: "<%= project_name %>#{ENV["ENVIRONMENT"].capitalize}DependencyStack"
17
17
  )
18
18
  rescue Aws::CloudFormation::Errors::InvalidClientTokenId, Aws::CloudFormation::Errors::ValidationError => e
19
19
  puts "E: #{e.class}"
20
20
  raise "ERROR: Your AWS credentials are not set or invalid." if e.class == Aws::CloudFormation::Errors::InvalidClientTokenId
21
- raise "ERROR: DependencyStack does not exist. Run `rake init` first!" if e.class == Aws::CloudFormation::Errors::ValidationError
21
+ raise "ERROR: <%= project_name %>#{ENV["ENVIRONMENT"].capitalize}DependencyStack does not exist. Run `rake init` first!" if e.class == Aws::CloudFormation::Errors::ValidationError
22
22
  end
23
23
 
24
- dep_file = File.open(".env.dependencies", "w")
24
+ dep_file = File.open(".env.dependencies.#{ENV["ENVIRONMENT"]}", "w")
25
25
  res[:stacks].each do |stack|
26
26
  stack[:outputs].each do |output|
27
27
  dep_file.puts "#{output[:output_key].upcase}=#{output[:output_value]}"
@@ -7,7 +7,7 @@ require "aws-sdk"
7
7
  require_relative "../aws_helper/main"
8
8
 
9
9
  Dotenv.load(".env.private")
10
- Dotenv.load(".env.dependencies")
10
+ Dotenv.load(".env.dependencies.#{ENV["ENVIRONMENT"]}")
11
11
  raise "CLOUDFORMATIONBUCKET not set. Run `rake init` and `rake update` first!" unless ENV["CLOUDFORMATIONBUCKET"]
12
12
 
13
13
  env_vars = load_env_vars
@@ -26,7 +26,7 @@ s3_filename = get_parent_stack_s3_location(
26
26
  client = Aws::CloudFormation::Client.new
27
27
 
28
28
  parent_parameters = []
29
- File.open(".env.dependencies").read.each_line do |line|
29
+ File.open(".env.dependencies.#{ENV["ENVIRONMENT"]}").read.each_line do |line|
30
30
  line.strip!
31
31
  param, value = line.split("=")
32
32
  parent_parameters.push(