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 +4 -4
- data/.rspec +2 -0
- data/README.md +196 -2
- data/lib/cloud_formation/bridge/exception_notifier.rb +2 -1
- data/lib/cloud_formation/bridge/poller.rb +8 -5
- data/lib/cloud_formation/bridge/request.rb +11 -3
- data/lib/cloud_formation/bridge/util.rb +9 -0
- data/lib/cloud_formation/bridge/version.rb +1 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/support/file_support.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 07f63fba4fe361c558d340ea3c548e2c5e18536b
|
4
|
+
data.tar.gz: 63b98a3bc9025d48eb149f9559bd30440efc89b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2026e5c2357a70ad1c6015dcb7253b283bd4df0a269ccba516c5ca58b8cde86a16fa9bfeb0bb2a618934e75ba3c05a7bd7cc57f189f72a57195300a1cc4e0ab5
|
7
|
+
data.tar.gz: cc9f7b83bca39812c6aede1ff96ca2c24bdc8098bb76d6869f0ba0a8919345b870738eea0911ed45ad0c81e538bc4b251b618e97d72dfd3f9848aded19b8471f
|
data/.rspec
ADDED
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
|
-
|
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
|
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
|
-
|
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 '
|
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 =
|
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
|
-
|
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
|
-
|
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 = {})
|
data/spec/spec_helper.rb
CHANGED
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.
|
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-
|
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
|