synapse 0.11.1 → 0.12.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.
- data/.gitignore +1 -0
- data/.travis.yml +3 -1
- data/Gemfile.lock +32 -16
- data/README.md +120 -95
- data/lib/synapse.rb +20 -6
- data/lib/synapse/file_output.rb +57 -0
- data/lib/synapse/haproxy.rb +122 -25
- data/lib/synapse/service_watcher/base.rb +40 -2
- data/lib/synapse/service_watcher/dns.rb +1 -15
- data/lib/synapse/service_watcher/docker.rb +1 -25
- data/lib/synapse/service_watcher/ec2tag.rb +91 -9
- data/lib/synapse/service_watcher/zookeeper.rb +53 -24
- data/lib/synapse/version.rb +1 -1
- data/spec/lib/synapse/haproxy_spec.rb +22 -3
- data/spec/lib/synapse/service_watcher_base_spec.rb +67 -9
- data/spec/lib/synapse/service_watcher_docker_spec.rb +1 -33
- data/spec/lib/synapse/service_watcher_ec2tags_spec.rb +187 -0
- data/spec/spec_helper.rb +6 -1
- data/spec/support/configuration.rb +0 -2
- data/synapse.gemspec +3 -2
- metadata +29 -10
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
synapse (0.
|
4
|
+
synapse (0.12.1)
|
5
|
+
aws-sdk (~> 1.39)
|
5
6
|
docker-api (~> 1.7.2)
|
6
7
|
zk (~> 1.9.4)
|
7
8
|
|
@@ -9,21 +10,29 @@ GEM
|
|
9
10
|
remote: https://rubygems.org/
|
10
11
|
specs:
|
11
12
|
archive-tar-minitar (0.5.2)
|
13
|
+
aws-sdk (1.64.0)
|
14
|
+
aws-sdk-v1 (= 1.64.0)
|
15
|
+
aws-sdk-v1 (1.64.0)
|
16
|
+
json (~> 1.4)
|
17
|
+
nokogiri (>= 1.4.4)
|
12
18
|
coderay (1.0.9)
|
13
|
-
diff-lcs (1.2.
|
19
|
+
diff-lcs (1.2.5)
|
14
20
|
docker-api (1.7.6)
|
15
21
|
archive-tar-minitar
|
16
22
|
excon (>= 0.28)
|
17
23
|
json
|
18
|
-
excon (0.
|
24
|
+
excon (0.45.4)
|
19
25
|
ffi (1.9.3-java)
|
20
|
-
json (1.8.
|
26
|
+
json (1.8.3)
|
21
27
|
little-plugger (1.1.3)
|
22
28
|
logging (1.8.2)
|
23
29
|
little-plugger (>= 1.1.3)
|
24
30
|
multi_json (>= 1.8.4)
|
25
31
|
method_source (0.8.2)
|
26
|
-
|
32
|
+
mini_portile (0.6.2)
|
33
|
+
multi_json (1.11.2)
|
34
|
+
nokogiri (1.6.6.2)
|
35
|
+
mini_portile (~> 0.6.0)
|
27
36
|
pry (0.9.12.2)
|
28
37
|
coderay (~> 1.0.5)
|
29
38
|
method_source (~> 0.8)
|
@@ -36,21 +45,25 @@ GEM
|
|
36
45
|
pry-nav (0.2.3)
|
37
46
|
pry (~> 0.9.10)
|
38
47
|
rake (10.1.1)
|
39
|
-
rspec (
|
40
|
-
rspec-core (~>
|
41
|
-
rspec-expectations (~>
|
42
|
-
rspec-mocks (~>
|
43
|
-
rspec-core (
|
44
|
-
|
45
|
-
|
46
|
-
|
48
|
+
rspec (3.1.0)
|
49
|
+
rspec-core (~> 3.1.0)
|
50
|
+
rspec-expectations (~> 3.1.0)
|
51
|
+
rspec-mocks (~> 3.1.0)
|
52
|
+
rspec-core (3.1.7)
|
53
|
+
rspec-support (~> 3.1.0)
|
54
|
+
rspec-expectations (3.1.2)
|
55
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
56
|
+
rspec-support (~> 3.1.0)
|
57
|
+
rspec-mocks (3.1.3)
|
58
|
+
rspec-support (~> 3.1.0)
|
59
|
+
rspec-support (3.1.2)
|
47
60
|
slop (3.4.6)
|
48
61
|
spoon (0.0.4)
|
49
62
|
ffi
|
50
|
-
zk (1.9.
|
63
|
+
zk (1.9.5)
|
51
64
|
logging (~> 1.8.2)
|
52
65
|
zookeeper (~> 1.4.0)
|
53
|
-
zookeeper (1.4.
|
66
|
+
zookeeper (1.4.10)
|
54
67
|
|
55
68
|
PLATFORMS
|
56
69
|
java
|
@@ -60,5 +73,8 @@ DEPENDENCIES
|
|
60
73
|
pry
|
61
74
|
pry-nav
|
62
75
|
rake
|
63
|
-
rspec
|
76
|
+
rspec (~> 3.1.0)
|
64
77
|
synapse!
|
78
|
+
|
79
|
+
BUNDLED WITH
|
80
|
+
1.10.5
|
data/README.md
CHANGED
@@ -52,36 +52,27 @@ production:
|
|
52
52
|
|
53
53
|
You would like to be able to fail over to a different database in case the original dies.
|
54
54
|
Let's suppose your instance is running in AWS and you're using the tag 'proddb' set to 'true' to indicate the prod DB.
|
55
|
-
You set up synapse to proxy the DB connection on `localhost:3219` in the `synapse.conf.
|
55
|
+
You set up synapse to proxy the DB connection on `localhost:3219` in the `synapse.conf.yaml` file.
|
56
56
|
Add a hash under `services` that looks like this:
|
57
57
|
|
58
|
-
```
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
"frontend": [
|
77
|
-
"mode tcp",
|
78
|
-
],
|
79
|
-
"backend": [
|
80
|
-
"mode tcp",
|
81
|
-
],
|
82
|
-
},
|
83
|
-
},
|
84
|
-
...
|
58
|
+
```yaml
|
59
|
+
---
|
60
|
+
services:
|
61
|
+
proddb:
|
62
|
+
default_servers:
|
63
|
+
-
|
64
|
+
name: "default-db"
|
65
|
+
host: "mydb.example.com"
|
66
|
+
port: 5432
|
67
|
+
discovery:
|
68
|
+
method: "awstag"
|
69
|
+
tag_name: "proddb"
|
70
|
+
tag_value: "true"
|
71
|
+
haproxy:
|
72
|
+
port: 3219
|
73
|
+
server_options: "check inter 2000 rise 3 fall 2"
|
74
|
+
frontend: mode tcp
|
75
|
+
backend: mode tcp
|
85
76
|
```
|
86
77
|
|
87
78
|
And then change your database.yaml file to look like this:
|
@@ -112,6 +103,9 @@ And then execute:
|
|
112
103
|
Or install it yourself as:
|
113
104
|
|
114
105
|
$ gem install synapse
|
106
|
+
|
107
|
+
|
108
|
+
Don't forget to install HAProxy prior to installing Synapse.
|
115
109
|
|
116
110
|
## Configuration ##
|
117
111
|
|
@@ -129,7 +123,6 @@ Each value in the services hash is also a hash, and should contain the following
|
|
129
123
|
* `discovery`: how synapse will discover hosts providing this service (see below)
|
130
124
|
* `default_servers`: the list of default servers providing this service; synapse uses these if no others can be discovered
|
131
125
|
* `haproxy`: how will the haproxy section for this service be configured
|
132
|
-
* `shared_frontend`: optional: haproxy configuration directives for a shared http frontend (see below)
|
133
126
|
|
134
127
|
#### Service Discovery ####
|
135
128
|
|
@@ -167,6 +160,29 @@ It takes the following options:
|
|
167
160
|
* `container_port`: find containers forwarding this port
|
168
161
|
* `check_interval`: how often to poll the docker API on each server. Default is 15s.
|
169
162
|
|
163
|
+
##### AWS EC2 tags #####
|
164
|
+
|
165
|
+
This watcher retrieves a list of Amazon EC2 instances that have a tag
|
166
|
+
with particular value using the AWS API.
|
167
|
+
It takes the following options:
|
168
|
+
|
169
|
+
* `method`: ec2tag
|
170
|
+
* `tag_name`: the name of the tag to inspect. As per the AWS docs,
|
171
|
+
this is case-sensitive.
|
172
|
+
* `tag_value`: the value to match on. Case-sensitive.
|
173
|
+
|
174
|
+
Additionally, you MUST supply `server_port_override` in the `haproxy`
|
175
|
+
section of the configuration as this watcher does not know which port
|
176
|
+
the backend service is listening on.
|
177
|
+
|
178
|
+
The following options are optional, provided the well-known `AWS_`
|
179
|
+
environment variables shown are set. If supplied, these options will
|
180
|
+
be used in preference to the `AWS_` environment variables.
|
181
|
+
|
182
|
+
* `aws_access_key_id`: AWS key or set `AWS_ACCESS_KEY_ID` in the environment.
|
183
|
+
* `aws_secret_access_key`: AWS secret key or set `AWS_SECRET_ACCESS_KEY` in the environment.
|
184
|
+
* `aws_region`: AWS region (i.e. `us-east-1`) or set `AWS_REGION` in the environment.
|
185
|
+
|
170
186
|
#### Listing Default Servers ####
|
171
187
|
|
172
188
|
You may list a number of default servers providing a service.
|
@@ -182,16 +198,30 @@ If you do not list any default servers, no proxy will be created. The
|
|
182
198
|
`default_servers` will also be used in addition to discovered servers if the
|
183
199
|
`keep_default_servers` option is set.
|
184
200
|
|
201
|
+
If you do not list any `default_servers`, and all backends for a service
|
202
|
+
disappear then the previous known backends will be used. Disable this behavior
|
203
|
+
by unsetting `use_previous_backends`.
|
204
|
+
|
205
|
+
#### The `file_output` Section ####
|
206
|
+
|
207
|
+
This section controls whether or not synapse will write out service state
|
208
|
+
to the filesystem in json format. This can be used for services that want to
|
209
|
+
use discovery information but not go through HAProxy.
|
210
|
+
|
211
|
+
* `output_directory`: the path to a directory on disk that service registrations
|
212
|
+
should be written to.
|
213
|
+
|
185
214
|
#### The `haproxy` Section ####
|
186
215
|
|
187
216
|
This section is its own hash, which should contain the following keys:
|
188
217
|
|
189
|
-
* `port`: the port (on localhost) where HAProxy will listen for connections to the service.
|
218
|
+
* `port`: the port (on localhost) where HAProxy will listen for connections to the service. If this is omitted, only a backend stanza (and no frontend stanza) will be generated for this service; you'll need to get traffic to your service yourself via the `shared_frontend` or manual frontends in `extra_sections`
|
190
219
|
* `server_port_override`: the port that discovered servers listen on; you should specify this if your discovery mechanism only discovers names or addresses (like the DNS watcher). If the discovery method discovers a port along with hostnames (like the zookeeper watcher) this option may be left out, but will be used in preference if given.
|
191
220
|
* `server_options`: the haproxy options for each `server` line of the service in HAProxy config; it may be left out.
|
192
221
|
* `frontend`: additional lines passed to the HAProxy config in the `frontend` stanza of this service
|
193
222
|
* `backend`: additional lines passed to the HAProxy config in the `backend` stanza of this service
|
194
223
|
* `listen`: these lines will be parsed and placed in the correct `frontend`/`backend` section as applicable; you can put lines which are the same for the frontend and backend here.
|
224
|
+
* `shared_frontend`: optional: haproxy configuration directives for a shared http frontend (see below)
|
195
225
|
|
196
226
|
### Configuring HAProxy ###
|
197
227
|
|
@@ -201,80 +231,75 @@ The `haproxy` section of the config file has the following options:
|
|
201
231
|
* `config_file_path`: where Synapse will write the HAProxy config file
|
202
232
|
* `do_writes`: whether or not the config file will be written (default to `true`)
|
203
233
|
* `do_reloads`: whether or not Synapse will reload HAProxy (default to `true`)
|
234
|
+
* `do_socket`: whether or not Synapse will use the HAProxy socket commands to prevent reloads (default to `true`)
|
204
235
|
* `global`: options listed here will be written into the `global` section of the HAProxy config
|
205
236
|
* `defaults`: options listed here will be written into the `defaults` section of the HAProxy config
|
237
|
+
* `extra_sections`: additional, manually-configured `frontend`, `backend`, or `listen` stanzas
|
206
238
|
* `bind_address`: force HAProxy to listen on this address (default is localhost)
|
207
|
-
* `
|
208
|
-
|
209
|
-
|
239
|
+
* `shared_frontend`: (OPTIONAL) additional lines passed to the HAProxy config used to configure a shared HTTP frontend (see below)
|
240
|
+
* `restart_interval`: number of seconds to wait between restarts of haproxy (default: 2)
|
241
|
+
* `restart_jitter`: percentage, expressed as a float, of jitter to multiply the `restart_interval` by when determining the next
|
242
|
+
restart time. Use this to help prevent healthcheck storms when HAProxy restarts. (default: 0.0)
|
243
|
+
* `state_file_path`: full path on disk (e.g. /tmp/synapse/state.json) for caching haproxy state between reloads.
|
244
|
+
If provided, synapse will store recently seen backends at this location and can "remember" backends across both synapse and
|
245
|
+
HAProxy restarts. Any backends that are "down" in the reporter but listed in the cache will be put into HAProxy disabled (default: nil)
|
246
|
+
* `state_file_ttl`: the number of seconds that backends should be kept in the state file cache.
|
247
|
+
This only applies if `state_file_path` is provided (default: 86400)
|
248
|
+
|
249
|
+
Note that a non-default `bind_address` can be dangerous.
|
250
|
+
If you configure an `address:port` combination that is already in use on the system, haproxy will fail to start.
|
210
251
|
|
211
252
|
### HAProxy shared HTTP Frontend ###
|
212
253
|
|
213
254
|
For HTTP-only services, it is not always necessary or desirable to dedicate a TCP port per service, since HAProxy can route traffic based on host headers.
|
214
|
-
To support this, the optional `
|
215
|
-
|
255
|
+
To support this, the optional `shared_frontend` section can be added to both the `haproxy` section and each indvidual service definition.
|
256
|
+
Synapse will concatenate them all into a single frontend section in the generated haproxy.cfg file.
|
257
|
+
Note that synapse does not assemble the routing ACLs for you; you have to do that yourself based on your needs.
|
216
258
|
This is probably most useful in combination with the `service_conf_dir` directive in a case where the individual service config files are being distributed by a configuration manager such as puppet or chef, or bundled into service packages.
|
217
259
|
For example:
|
218
260
|
|
219
261
|
```yaml
|
220
|
-
|
221
|
-
"
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
"
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
"
|
260
|
-
|
261
|
-
"method": "zookeeper",
|
262
|
-
"path": "/nerve/services/service2",
|
263
|
-
"hosts": [ "0.zookeeper.example.com:2181" ]
|
264
|
-
},
|
265
|
-
"haproxy": {
|
266
|
-
"server_options": "check inter 2s rise 3 fall 2",
|
267
|
-
"shared_frontend": [
|
268
|
-
"acl is_service1 hdr_dom(host) -i service2.lb.example.com",
|
269
|
-
"use_backend service2 if is_service2"
|
270
|
-
],
|
271
|
-
"backend": [
|
272
|
-
"mode http"
|
273
|
-
]
|
274
|
-
}
|
275
|
-
}
|
276
|
-
}
|
277
|
-
}
|
262
|
+
haproxy:
|
263
|
+
shared_frontend: "bind 127.0.0.1:8081"
|
264
|
+
reload_command: "service haproxy reload"
|
265
|
+
config_file_path: "/etc/haproxy/haproxy.cfg"
|
266
|
+
socket_file_path: "/var/run/haproxy.sock"
|
267
|
+
global:
|
268
|
+
- "daemon"
|
269
|
+
- "user haproxy"
|
270
|
+
- "group haproxy"
|
271
|
+
- "maxconn 4096"
|
272
|
+
- "log 127.0.0.1 local2 notice"
|
273
|
+
- "stats socket /var/run/haproxy.sock"
|
274
|
+
defaults:
|
275
|
+
- "log global"
|
276
|
+
- "balance roundrobin"
|
277
|
+
services:
|
278
|
+
service1:
|
279
|
+
discovery:
|
280
|
+
method: "zookeeper"
|
281
|
+
path: "/nerve/services/service1"
|
282
|
+
hosts: "0.zookeeper.example.com:2181"
|
283
|
+
haproxy:
|
284
|
+
server_options: "check inter 2s rise 3 fall 2"
|
285
|
+
shared_frontend:
|
286
|
+
- "acl is_service1 hdr_dom(host) -i service1.lb.example.com"
|
287
|
+
- "use_backend service1 if is_service1"
|
288
|
+
backend: "mode http"
|
289
|
+
|
290
|
+
service2:
|
291
|
+
discovery:
|
292
|
+
method: "zookeeper"
|
293
|
+
path: "/nerve/services/service2"
|
294
|
+
hosts: "0.zookeeper.example.com:2181"
|
295
|
+
|
296
|
+
haproxy:
|
297
|
+
server_options: "check inter 2s rise 3 fall 2"
|
298
|
+
shared_frontend:
|
299
|
+
- "acl is_service1 hdr_dom(host) -i service2.lb.example.com"
|
300
|
+
- "use_backend service2 if is_service2
|
301
|
+
backend: "mode http"
|
302
|
+
|
278
303
|
```
|
279
304
|
|
280
305
|
This would produce an haproxy.cfg much like the following:
|
@@ -332,5 +357,5 @@ end
|
|
332
357
|
3. Implement the `start` and `validate_discovery_opts` methods
|
333
358
|
4. Implement whatever additional methods your discovery requires
|
334
359
|
|
335
|
-
When your watcher detects a list of new backends,
|
336
|
-
|
360
|
+
When your watcher detects a list of new backends, you should call `set_backends` to
|
361
|
+
store the new backends and update the HAProxy config.
|
data/lib/synapse.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require "synapse/version"
|
2
2
|
require "synapse/service_watcher/base"
|
3
3
|
require "synapse/haproxy"
|
4
|
+
require "synapse/file_output"
|
4
5
|
require "synapse/service_watcher"
|
5
6
|
require "synapse/log"
|
6
7
|
|
@@ -17,9 +18,17 @@ module Synapse
|
|
17
18
|
raise "specify a list of services to connect in the config" unless opts.has_key?('services')
|
18
19
|
@service_watchers = create_service_watchers(opts['services'])
|
19
20
|
|
20
|
-
# create
|
21
|
+
# create objects that need to be notified of service changes
|
22
|
+
@config_generators = []
|
23
|
+
# create the haproxy config generator, this is mandatory
|
21
24
|
raise "haproxy config section is missing" unless opts.has_key?('haproxy')
|
22
|
-
@
|
25
|
+
@config_generators << Haproxy.new(opts['haproxy'])
|
26
|
+
|
27
|
+
# possibly create a file manifestation for services that do not
|
28
|
+
# want to communicate via haproxy, e.g. cassandra
|
29
|
+
if opts.has_key?('file_output')
|
30
|
+
@config_generators << FileOutput.new(opts['file_output'])
|
31
|
+
end
|
23
32
|
|
24
33
|
# configuration is initially enabled to configure on first loop
|
25
34
|
@config_updated = true
|
@@ -47,10 +56,15 @@ module Synapse
|
|
47
56
|
|
48
57
|
if @config_updated
|
49
58
|
@config_updated = false
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
59
|
+
@config_generators.each do |config_generator|
|
60
|
+
log.info "synapse: configuring #{config_generator.name}"
|
61
|
+
config_generator.update_config(@service_watchers)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
sleep 1
|
66
|
+
@config_generators.each do |config_generator|
|
67
|
+
config_generator.tick(@service_watchers)
|
54
68
|
end
|
55
69
|
|
56
70
|
loops += 1
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'synapse/log'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
module Synapse
|
6
|
+
class FileOutput
|
7
|
+
include Logging
|
8
|
+
attr_reader :opts, :name
|
9
|
+
|
10
|
+
def initialize(opts)
|
11
|
+
unless opts.has_key?("output_directory")
|
12
|
+
raise ArgumentError, "flat file generation requires an output_directory key"
|
13
|
+
end
|
14
|
+
|
15
|
+
begin
|
16
|
+
FileUtils.mkdir_p(opts['output_directory'])
|
17
|
+
rescue SystemCallError => err
|
18
|
+
raise ArgumentError, "provided output directory #{opts['output_directory']} is not present or creatable"
|
19
|
+
end
|
20
|
+
|
21
|
+
@opts = opts
|
22
|
+
@name = 'file_output'
|
23
|
+
end
|
24
|
+
|
25
|
+
def tick(watchers)
|
26
|
+
end
|
27
|
+
|
28
|
+
def update_config(watchers)
|
29
|
+
watchers.each do |watcher|
|
30
|
+
write_backends_to_file(watcher.name, watcher.backends)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def write_backends_to_file(service_name, new_backends)
|
35
|
+
data_path = File.join(@opts['output_directory'], "#{service_name}.json")
|
36
|
+
begin
|
37
|
+
old_backends = JSON.load(File.read(data_path))
|
38
|
+
rescue Errno::ENOENT
|
39
|
+
old_backends = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
if old_backends == new_backends
|
43
|
+
# Prevent modifying the file unless something has actually changed
|
44
|
+
# This way clients can set watches on this file and update their
|
45
|
+
# internal state only when the smartstack state has actually changed
|
46
|
+
return false
|
47
|
+
else
|
48
|
+
# Atomically write new sevice configuration file
|
49
|
+
temp_path = File.join(@opts['output_directory'],
|
50
|
+
".#{service_name}.json.tmp")
|
51
|
+
File.open(temp_path, 'w', 0644) {|f| f.write(new_backends.to_json)}
|
52
|
+
FileUtils.mv(temp_path, data_path)
|
53
|
+
return true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|