cfn-bridge 0.0.2 → 0.0.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
  SHA1:
3
- metadata.gz: bb117f078381ae0cb260bfee9b04ea821b312091
4
- data.tar.gz: ee0d543be32bf3ac7785b40dd1f2a1700afae8f0
3
+ metadata.gz: 07f63fba4fe361c558d340ea3c548e2c5e18536b
4
+ data.tar.gz: 63b98a3bc9025d48eb149f9559bd30440efc89b7
5
5
  SHA512:
6
- metadata.gz: d95bf99fc45343d58c6ef18dc83e029d4c6af4b24119cf503694aa7c579a87b29aceccd51629f7ff5a5fec3ec5086ad921d969f9cd6e870e670974ea5556f81c
7
- data.tar.gz: a8e052cf554b8c41dc73c298e8f90585371e29e6afb0b0cd564d1775597b3c41d6400d1594a874494b86750870f408fcbe80ee65e0bff2fa3e6ed95abdecf331
6
+ metadata.gz: 2026e5c2357a70ad1c6015dcb7253b283bd4df0a269ccba516c5ca58b8cde86a16fa9bfeb0bb2a618934e75ba3c05a7bd7cc57f189f72a57195300a1cc4e0ab5
7
+ data.tar.gz: cc9f7b83bca39812c6aede1ff96ca2c24bdc8098bb76d6869f0ba0a8919345b870738eea0911ed45ad0c81e538bc4b251b618e97d72dfd3f9848aded19b8471f
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/README.md CHANGED
@@ -1,10 +1,33 @@
1
+ <!-- START doctoc generated TOC please keep comment here to allow auto update -->
2
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
3
+ **Table of Contents**
4
+
5
+ - [cfn-bridge](#cfn-bridge)
6
+ - [Installation](#installation)
7
+ - [Usage](#usage)
8
+ - [Building a custom resource](#building-a-custom-resource)
9
+ - [Implementing the `create` operation](#implementing-the-create-operation)
10
+ - [Implementing `update` is usually not a requirement](#implementing-update-is-usually-not-a-requirement)
11
+ - [Implementing the `delete` operation](#implementing-the-delete-operation)
12
+ - [Registering and using it](#registering-and-using-it)
13
+ - [Current custom resources](#current-custom-resources)
14
+ - [Custom::SubscribeSQSQueueToSNSTopic](#customsubscribesqsqueuetosnstopic)
15
+ - [Custom::CloudFormationOutputs](#customcloudformationoutputs)
16
+ - [Contributing](#contributing)
17
+
18
+ <!-- END doctoc generated TOC please keep comment here to allow auto update -->
19
+
1
20
  # cfn-bridge
2
21
 
3
- A bridge to allow you to build custom AWS cloud formation resources.
22
+ [This project is sponsored by Neat](http://www.neat.com/)
23
+
24
+ A bridge to allow you to build custom AWS cloud formation resources in Ruby.
4
25
 
5
26
  Check Amazon's page [on custom cloud formation resources](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-walkthrough.html)
6
27
  to get more info on how and why you would like to have them.
7
28
 
29
+ If you're into Python more than Ruby, there's an [AWS project for this as well](http://blogs.aws.amazon.com/application-management/post/Tx2FNAPE4YGYSRV/Customers-CloudFormation-and-Custom-Resources).
30
+
8
31
  ## Installation
9
32
 
10
33
  Add this line to your application's Gemfile:
@@ -25,7 +48,178 @@ Run:
25
48
 
26
49
  $ cfn-bridge start QUEUE_NAME
27
50
 
28
- This will start a worker consuming from `QUEUE_NAME` until the caller calls `CTRL-C` to stop it. `QUEUE_NAME` should be the SQS queue to where the SNS topic is publishing the custom resource messages. An example template that setups the topic and the queue [is available at the repo](spec/files/test-formation.json) and can be used to provide the base topic and queue to use this application.
51
+ This will start a worker consuming from `QUEUE_NAME` until the caller calls `CTRL-C` to stop it. `QUEUE_NAME` should be the SQS queue to where the SNS topic is publishing the custom resource messages. An example template that setups the topic and the queue [is available at the repo](spec/files/test-formation.json) and can be used to provide the base topic and queue to use this gem.
52
+
53
+ Since this gem uses the `aws-sdk` gem you have to setup your AWS keys as environment variables (or as an IAM profile if you're running on EC2) to make sure the gem has the right credentials to perform the operations. As usual, we recommend that you provide keys with access only to the operations you're going to perform, do not provide all access keys for this.
54
+
55
+ ## Building a custom resource
56
+
57
+ Building a custom resource is simple, all it requires is a class with two methods, `create` and `delete` (you can also have an `update` one if it makes sense for your resource to be updateable) that will take the message parameter. Your custom resource should also inherit from `CloudFormation::Bridge::Resources::Base` to simplify your job, it provides a default `update` method that always fails and includes a couple constants you will have to use at your responses.
58
+
59
+ All three operations take a [Request](lib/cloud_formation/bridge/request.rb) method as a parameter and you should check it to see the method the fields that are available there for you to use.
60
+
61
+ Let's look at our `SubscribeQueueToTopic` to get a sense of how custom resources could be implemented, starting with the constants:
62
+
63
+ ```ruby
64
+ ARN = 'Arn'
65
+ ENDPOINT = 'Endpoint'
66
+ PROTOCOL = 'Protocol'
67
+
68
+ TOPIC_ARN = 'TopicArn'
69
+ QUEUE_NAME = 'QueueName'
70
+ RAW_MESSAGE_DELIVERY = 'RawMessageDelivery'
71
+
72
+ REQUIRED_FIELDS = [
73
+ TOPIC_ARN,
74
+ QUEUE_NAME,
75
+ ]
76
+ ```
77
+
78
+ This list of constants are first the outputs produced by the resource (`ARN`, `ENDPOINT` and `PROTOCOL`) and then the inputs that are used to create it, `TOPIC_ARN`, `QUEUE_NAME` and `RAW_MESSAGE_DELIVERY`. The inputs are the fields we use to create the resource, since this resource is meant to subscribe an SQS queue to an SNS topic, we accept the topic ARN, the queue name and an optional `raw message delivery` field that instructs the subscription to send the raw message only and not the message with the other SNS fields.
79
+
80
+ ### Implementing the `create` operation
81
+
82
+ Now let's look at the first operation, the `create` method:
83
+
84
+ ```ruby
85
+ def create(request)
86
+ require_fields(request, REQUIRED_FIELDS)
87
+
88
+ queue = queues.named(request.resource_properties[QUEUE_NAME])
89
+ topic = topics[request.resource_properties[TOPIC_ARN]]
90
+
91
+ subscription = topic.subscribe(queue)
92
+
93
+ if request.resource_properties[RAW_MESSAGE_DELIVERY]
94
+ subscription.raw_message_delivery = true
95
+ end
96
+
97
+ {
98
+ FIELDS::PHYSICAL_RESOURCE_ID => subscription.arn,
99
+ FIELDS::DATA => {
100
+ ARN => subscription.arn,
101
+ ENDPOINT => subscription.endpoint,
102
+ PROTOCOL => subscription.protocol,
103
+ },
104
+ }
105
+ end
106
+ ```
107
+
108
+ First, we validate that we have all the fields required to create the resource (topic ARN and queue name), if any one of these fields is empty, we raise an exception with the field that was empty. The code at the gem that does the messaging and executes your resources will catch any exception that inherits from `StandardError` and will forward it's message as a failure to the Cloud Formation service, this errors will be visible at the cloud formation events so make sure you raise exceptions with useful error messages whenever you have to.
109
+
110
+ From that on, we just implement the operation, grab the queue, grab the topic, subscribe one to the other, set the raw message delivery field if it was set and then, here's the important part, return the response.
111
+
112
+ Let's look at an example return response in raw JSON:
113
+
114
+ ```javascript
115
+ {
116
+ "Status" : "SUCCESS",
117
+ "PhysicalResourceId" : "Tester1",
118
+ "StackId" : "arn:aws:cloudformation:us-east-1:EXAMPLE:stack/stack-name/guid",
119
+ "RequestId" : "unique id for this create request",
120
+ "LogicalResourceId" : "MySeleniumTester",
121
+ "Data" : {
122
+ "resultsPage" : "http://www.myexampledomain/test-results/guid",
123
+ "lastUpdate" : "2012-11-14T03:30Z",
124
+ }
125
+ }
126
+ ```
127
+
128
+ And let's look at the one we're returning at the current `create` method:
129
+
130
+ ```ruby
131
+ {
132
+ FIELDS::PHYSICAL_RESOURCE_ID => subscription.arn,
133
+ FIELDS::DATA => {
134
+ ARN => subscription.arn,
135
+ ENDPOINT => subscription.endpoint,
136
+ PROTOCOL => subscription.protocol,
137
+ },
138
+ }
139
+ ```
140
+
141
+ The fields `Status`, `StackId`, `LogicalResourceId` and `RequestId` are not present here because the code that executes your resource already knows how to fill them, so you only have to care about `PhysicalResourceId` and `Data`.
142
+
143
+ The physical resource id should be a unique value that you can use to find this resource later when you receive `update` and `delete` operations. When dealing with AWS resources your best bet is to always use the `ARN` for the resource you're creating, this guarantees it is unique for your cloud formation and that you can easily find it later on other requests.
144
+
145
+ The `Data` field should contain fields that might be useful for the cloud formation where the resource is included, you don't actually have to provide one, but all fields returned here are available to `Fn::GetAtt` operations at your cloud formation template, so make sure you send back some useful data back if possible.
146
+
147
+ ### Implementing `update` is usually not a requirement
148
+
149
+ Since it wouldn't make much sense to update a subscription (you're either subscribed to something or you're not) we don't have an `update` method here, but if it makes sense for your resource, the update method follows the same pattern as `create`, just make sure you're *not changing the physical resource id* as it will be ignored. It's usually much simpler not to implement updates and always require the resource to be deleted and created again.
150
+
151
+
152
+ ### Implementing the `delete` operation
153
+
154
+ The `delete` method is much simpler than `create`:
155
+
156
+ ```ruby
157
+ def delete(request)
158
+ subscription = subscriptions[request.physical_resource_id]
159
+ subscription.unsubscribe if subscription && subscription.exists?
160
+ end
161
+ ```
162
+
163
+ Here we find the subscription using it's physical resource id (we used the subscription's `ARN` for this) and, if it exists, it is destroyed. Checking for the existence here is important because if your resource fails to be created the cloud formation service will still issue a `delete` operation for it (in case it was created even after failing) so make sure your code ignores delete operations for resources that do not exist.
164
+
165
+ ### Registering and using it
166
+
167
+ Once you resource is implemented, you must either register it at the [Executor::DEFAULT_REGISTRY hash](lib/cloud_formation/bridge/executor.rb) or manually create an executor providing it at the hash. Make sure the name you use starts with `Custom::` and that it doesn't clash with other resources already registered there.
168
+
169
+ Once you have it registered, you can just declare your resource at any cloud formation template:
170
+
171
+ ```javascript
172
+ "Resources": {
173
+ "FirstQueue": {
174
+ "Type": "AWS::SQS::Queue",
175
+ "Properties": {
176
+ "ReceiveMessageWaitTimeSeconds": 20,
177
+ "VisibilityTimeout": 60
178
+ }
179
+ },
180
+ "FirstTopic": {
181
+ "Type": "AWS::SNS::Topic"
182
+ },
183
+ "SubscribeResource": {
184
+ "Type": "Custom::SubscribeSQSQueueToSNSTopic",
185
+ "Properties": {
186
+ "ServiceToken": {
187
+ "Ref": "EntryTopic"
188
+ },
189
+ "TopicArn": {
190
+ "Ref": "FirstTopic"
191
+ },
192
+ "QueueName": {
193
+ "Fn::GetAtt": ["FirstQueue", "QueueName"]
194
+ }
195
+ }
196
+ }
197
+ }
198
+ ```
199
+
200
+ It is declared just like any other custom resource with the name you have registered at the executor (`Custom::SubscribeSQSQueueToSNSTopic` in this case) and then you can include your properties as needed. The `ServiceToken` property must always be there and should point to the SNS topic that is being watched for custom resource messages, the other properties are the ones your resource will use to implement it's actions.
201
+
202
+ And with this you should be able to start creating your own custom cloud formation resources.
203
+
204
+ ## Current custom resources
205
+
206
+ ### Custom::SubscribeSQSQueueToSNSTopic
207
+
208
+ Subscribes an SQS queue to an SNS topic.
209
+
210
+ Parameters:
211
+
212
+ * `TopicArn` - the SNS topic that will be subscribed to - *required*;
213
+ * `QueueName` - the SQS queue that will receive the messages from the topic - *required*;
214
+ * `RawMessageDelivery` - if set, the SNS message will not include the JSON envelope, it will send the raw message to the queue;
215
+
216
+ ### Custom::CloudFormationOutputs
217
+
218
+ Makes all outputs from another cloud formation available to `Fn::GetAtt` calls.
219
+
220
+ Parameters:
221
+
222
+ * `Name` - the name of the cloud formation you want to get the outputs from - *required*;
29
223
 
30
224
  ## Contributing
31
225
 
@@ -1,3 +1,4 @@
1
+ require 'cloud_formation/bridge/util'
1
2
  require 'singleton'
2
3
  require 'rollbar'
3
4
 
@@ -15,7 +16,7 @@ module CloudFormation
15
16
  include Singleton
16
17
 
17
18
  def report_exception(exception, custom_data = {}, user_data = {})
18
- puts "#{exception.message} - #{custom_data.inspect} - #{user_data.inspect}\n#{exception.backtrace.join("\n")}"
19
+ Util::LOGGER.error("#{exception.message} - #{custom_data.inspect} - #{user_data.inspect}\n#{exception.backtrace.join("\n")}")
19
20
  end
20
21
 
21
22
  end
@@ -1,7 +1,7 @@
1
1
  require 'cloud_formation/bridge/executor'
2
2
  require 'cloud_formation/bridge/exception_notifier'
3
3
  require 'cloud_formation/bridge/request'
4
- require 'logger'
4
+ require 'cloud_formation/bridge/util'
5
5
 
6
6
  module CloudFormation
7
7
  module Bridge
@@ -9,7 +9,7 @@ module CloudFormation
9
9
 
10
10
  attr_reader :logger, :running
11
11
 
12
- def initialize(queue_name, executor = CloudFormation::Bridge::Executor.new, logger = Logger.new(STDOUT))
12
+ def initialize(queue_name, executor = CloudFormation::Bridge::Executor.new, logger = Util::LOGGER)
13
13
  @queue_name = queue_name
14
14
  @executor = executor
15
15
  @logger = logger
@@ -29,17 +29,20 @@ module CloudFormation
29
29
  def poll
30
30
  message = queue.receive_message
31
31
 
32
- return unless message
32
+ unless message
33
+ logger.info("No messages found, looping again")
34
+ return
35
+ end
33
36
 
34
37
  begin
35
38
  logger.info("Received message #{message.id} - #{message.body}")
36
39
  body = JSON.parse(message.body)
37
- request = CloudFormation::Bridge::Request.new(JSON.parse(body["Message"]))
40
+ request = CloudFormation::Bridge::Request.new(JSON.parse(body["Message"]), logger)
38
41
  @executor.execute(request)
39
42
  message.delete
40
43
  logger.info("Processed message #{message.id}")
41
44
  message
42
- rescue => ex
45
+ rescue Exception => ex
43
46
  logger.info("Failed to process message #{message.id} - #{ex.message}")
44
47
  ExceptionNotifier.report_exception(ex,
45
48
  message: message.body,
@@ -1,6 +1,7 @@
1
1
  require 'securerandom'
2
2
  require 'cloud_formation/bridge/http_bridge'
3
3
  require 'cloud_formation/bridge/names'
4
+ require 'cloud_formation/bridge/util'
4
5
 
5
6
  module CloudFormation
6
7
  module Bridge
@@ -8,10 +9,11 @@ module CloudFormation
8
9
 
9
10
  include CloudFormation::Bridge::Names
10
11
 
11
- attr_reader :request
12
+ attr_reader :request, :logger
12
13
 
13
- def initialize(request)
14
+ def initialize(request, logger = Util::LOGGER)
14
15
  @request = request
16
+ @logger = logger
15
17
  end
16
18
 
17
19
  def update?
@@ -68,11 +70,17 @@ module CloudFormation
68
70
  FIELDS::STATUS => RESULTS::FAILED,
69
71
  )
70
72
 
73
+ logger.error("Failing request #{request_url} - #{response.inspect}")
74
+
71
75
  HttpBridge.put(request_url, response)
72
76
  end
73
77
 
74
78
  def succeed!(response)
75
- HttpBridge.put(request_url, build_response(response || {}))
79
+ actual_response = build_response(response || {})
80
+
81
+ logger.info("Succeeding request #{request_url} - #{actual_response.inspect}")
82
+
83
+ HttpBridge.put(request_url, actual_response)
76
84
  end
77
85
 
78
86
  def build_response(response = {})
@@ -0,0 +1,9 @@
1
+ require 'logger'
2
+
3
+ module CloudFormation
4
+ module Bridge
5
+ module Util
6
+ LOGGER = Logger.new(STDOUT)
7
+ end
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  module CloudFormation
2
2
  module Bridge
3
- VERSION = "0.0.2"
3
+ VERSION = "0.0.3"
4
4
  end
5
5
  end
data/spec/spec_helper.rb CHANGED
@@ -88,5 +88,5 @@ RSpec.configure do |config|
88
88
  end
89
89
 
90
90
  ['cloud_formation_creator', 'file_support'].each do |file|
91
- require File.join(__dir__, 'support', file)
91
+ require File.join(File.dirname(__FILE__), 'support', file)
92
92
  end
@@ -1,7 +1,7 @@
1
1
  module FileSupport
2
2
 
3
3
  def read_file(name)
4
- IO.read(File.join(__dir__, '..', 'files', name))
4
+ IO.read(File.join(File.dirname(__FILE__), '..', 'files', name))
5
5
  end
6
6
 
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cfn-bridge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maurício Linhares
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-08 00:00:00.000000000 Z
11
+ date: 2014-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -173,6 +173,7 @@ extensions: []
173
173
  extra_rdoc_files: []
174
174
  files:
175
175
  - ".gitignore"
176
+ - ".rspec"
176
177
  - Gemfile
177
178
  - LICENSE.txt
178
179
  - README.md
@@ -191,6 +192,7 @@ files:
191
192
  - lib/cloud_formation/bridge/resources/base.rb
192
193
  - lib/cloud_formation/bridge/resources/cloud_formation_outputs.rb
193
194
  - lib/cloud_formation/bridge/resources/subscribe_queue_to_topic.rb
195
+ - lib/cloud_formation/bridge/util.rb
194
196
  - lib/cloud_formation/bridge/version.rb
195
197
  - spec/files/outputs-formation.json
196
198
  - spec/files/sample-create-message.json