From aa57b4da2111c5e40479b8f5a3aca62834986620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20SEBAN?= Date: Tue, 5 Jan 2016 16:52:25 +0100 Subject: [PATCH] Init repo with initial version 1.0.0 --- .gitignore | 2 + Dockerfile | 43 +++++++ LICENSE | 9 ++ README.md | 148 +++++++++++++++++++++++ bin/entrypoint | 9 ++ bin/superwhale | 289 +++++++++++++++++++++++++++++++++++++++++++++ doc/superwhale.png | Bin 0 -> 29883 bytes lib/header.cfg | 8 ++ 8 files changed, 508 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bin/entrypoint create mode 100755 bin/superwhale create mode 100644 doc/superwhale.png create mode 100644 lib/header.cfg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af56f61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.idea/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f86eede --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +# The aim is to create a lightweight docker image +# that provides a smart http reverse proxy using haproxy +# for docker network usage. +# +# Read the README.md file for more informations. +# +# Find more here : https://github.com/Bahaika/whale-haproxy + +FROM alpine:3.3 +MAINTAINER Jérémy SEBAN + +# Installing haproxy, ruby +RUN apk add --update ruby haproxy + +# Installing superwhale dependency +RUN gem install filewatcher --no-ri --no-rdoc + +# Cleaning downloaded packages from image +RUN rm -rf /var/cache/apk/* + +# Adding superwhale libraries files +RUN mkdir -p /usr/lib/superwhale +COPY ./lib/header.cfg /usr/lib/superwhale/header.cfg + +# Adding superwhale binary +COPY ./bin/superwhale /bin/superwhale +RUN chmod +x /bin/superwhale + +# Adding entrypoint +COPY ./bin/entrypoint /bin/entrypoint +RUN chmod +x /bin/entrypoint + +# Exposing HTTP and HTTPS ports +EXPOSE 80 443 + +# Setting /etc/superwhale.d as VOLUME +VOLUME ["/etc/superwhale.d"] + +# Entrypoint to dispatch parameters +ENTRYPOINT ["/bin/entrypoint"] + +# Setting the starting command +CMD ["/bin/superwhale"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e2521aa --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2016 Ingensi + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b9d3629 --- /dev/null +++ b/README.md @@ -0,0 +1,148 @@ +![SuperWhale Logo](https://raw.githubusercontent.com/Ingensi/superwhale/develop/doc/superwhale.png) +*Logo by Camille BRIZARD* + +### Superwhale +*This project is licensed under the terms of the MIT license.* + +#### Aim +Docker introduced Docker Networks with version [1.9](https://github.com/docker/docker/blob/master/CHANGELOG.md#190-2015-11-03). With that update you can now have multiple web servers linked to the same network and a reverse proxy in front of them all without having to manage links manually. First, I was using HAProxy to do this role but HAProxy wasn't flexible enough : I was facing an issue, sometimes my containers weren't running while I was starting up my reverse and because HAProxy force name resolving at startup it was crashing almost every time. So I decided to create a system that add a smart layer upon HAProxy using a simple ruby script. + +#### How it works +It uses alpine as a base distribution to provide a lightweight image. It includes ruby (with 1 gem : [FileWatcher](https://github.com/thomasfl/filewatcher)) and [HAProxy](http://www.haproxy.org/), that's all. At startup, SuperWhale will launch `/bin/superwhale` : the main process of the container. + +`superwhale` will search for services inside the `/etc/superwhale.d` folder, create HAProxies configurations and then start HAProxies. It will also watch for file modifications inside `/etc/hosts` or `/etc/superwhale.d` and will gracefully reload HAProxies if there is any. + +#### How to use it + +##### Defining services + +A service is the `superwhale` representation of a backend webserver that needs to be reverse proxied. To declare a service create an `YAML` file and put it inside the `/etc/superwhale.d` folder. For instance : + +``` +git: + domain_name: git.mydomain.tld + backends: + - host: git_container + port: 80 + options: + - option forwardfor + - http-request set-header X-Client-IP %[src] + - http-request set-header Host git.mydomain.tld +``` + +Will output this inside HAProxies configuration ONLY if a `git_container` is present on the relevant `docker network` : + +``` +[...] + +frontend public + [...] + acl host_git hdr(host) -i git.mydomain.tld + use_backend git_backend if host_git + +backend git_backend + server git1 git:80 + option forwardfor + http-request set-header X-Client-IP %[src] + http-request set-header Host git.mydomain.tld + +[...] +``` + +You can define multiple backends to create a load-balanced backend and the load-balancing algorithm used between them. + +Here is an exhaustive list of what you can define for a service : + + +| Option | Type | Usage | +|---------------|---------------------|-------| +| *domain_name* | `string` | Define the domain name used to determine the backend | +| *backends* | `{host: 'hostname',port: port_int}[]` | Address of the backend server | +| *balance* | `string` | Define the load-balance algorithm for the backend pool | +| *options* | `string[]` | Options added to the backend block | +| *is_default* | `bool` | If true, add `default_backend` with this backend. Only one service can define this option. | + +###### Gracefull reload + +When you modify services if we restart HAProxy on-the-fly it will interrupt all active connections, breaking current downloads, streamings etc... To avoid this, there is not one HAProxy, but three. There is one in the front named `dispatcher`, and 2 behinds : `master` and `slave`. When configuration is changed, slave is restarted, then master is. Using the capability of HAProxy to exit the process only when all connections are closed, there is no lost of connections. + +Here is what HAProxy documentation says about soft-stop : +``` +2.4) Soft stop +-------------- +It is possible to stop services without breaking existing connections by the +sending of the SIGUSR1 signal to the process. All services are then put into +soft-stop state, which means that they will refuse to accept new connections, +except for those which have a non-zero value in the 'grace' parameter, in which +case they will still accept connections for the specified amount of time, in +milliseconds. This makes it possible to tell a load-balancer that the service +is failing, while still doing the job during the time it needs to detect it. +``` + +##### Setting `haproxy.cfg` header + +By default, this `defaults` section will be added at the top of the HAProxies configuration files : + +``` +defaults + maxconn 4096 + log global + mode http + retries 3 + timeout connect 5s + timeout client 15min + timeout server 15min +``` + +You can override this simply by adding a `header.cfg` file in `/etc/superwhale.d`. + +##### Using HTTPS + +You can use HTTPS by simply adding certificate file : `/etc/superwhale.d/https.pem`. This certificate is the concatenation of the certificate and the private key : + +``` +$ cat server.crt server.key > /etc/superwhale.d/https.pem +``` + +If you want to redirect all HTTP traffic to HTTPS, add a modifier to the container `run` command : `docker run [...] bahaika/whale-haproxy --force-ssl`. + +##### Debugging container + +If you want a verbose output for debugging purposes, add a modifier to the container `run` command : `docker run [...] bahaika/whale-haproxy --debug`. You can display less informations with a `--info` modifier. + +##### Launching the container + +There is two way of using this container : + +###### With volumes + +While launching the container, use the `-v` argument : +``` +docker run -d \ + -v /mnt/volumes/superwhale/:/etc/superwhale.d \ + -p 80:80 -p 443:443 --net=dockernet bahaika/whale-haproxy +``` + +###### With inheritance + +Create a `Dockerfile` and inherits from `bahaika/whale-haproxy` : + +``` +FROM bahaika/whale-haproxy:latest + +COPY ./service1.yml /etc/superwhale.d/service1.yml +``` + +#### Contributions + +##### Want to contribute ? + +* Fork the project. +* Make your feature addition or bug fix. +* Commit, do not mess with version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull). +* Send me a pull request. + +##### Contibutors + +Jérémy SEBAN - Main contributor - (GitHub: https://github.com/HipsterWhale) + diff --git a/bin/entrypoint b/bin/entrypoint new file mode 100644 index 0000000..bc211c7 --- /dev/null +++ b/bin/entrypoint @@ -0,0 +1,9 @@ +#!/bin/sh + +if [ -z "$1" ]; then + exec /bin/superwhale +elif [ "${1:0:1}" = '-' ]; then + exec /bin/superwhale $@ +else + exec $@ +fi \ No newline at end of file diff --git a/bin/superwhale b/bin/superwhale new file mode 100755 index 0000000..dcbae69 --- /dev/null +++ b/bin/superwhale @@ -0,0 +1,289 @@ +#!/usr/bin/ruby + +# Dependencies +require 'yaml' +require 'filewatcher' +require 'logger' + +# Folders constants +PATHS = { + superwhale: { + custom: '/etc/superwhale.d', + defaults: '/usr/lib/superwhale', + }, + haproxy: '/etc/haproxy', + hosts: '/etc/hosts' +} + +# SuperWhale constants +SUPERWHALE = { + version: '1.0.0', + debug: ARGV.include?('--debug'), + info: ARGV.include?('--info'), + services: "#{PATHS[:superwhale][:custom]}/*.yml", + defaults: { + header: "#{PATHS[:superwhale][:defaults]}/header.cfg" + }, + custom: { + header: "#{PATHS[:superwhale][:custom]}/header.cfg", + https_cert: "#{PATHS[:superwhale][:custom]}/https.pem" + }, + failed_services: [] +} + +# HAProxies constants +HAPROXY = { + binary: '/usr/sbin/haproxy', + debug_flag: '-d', + dispatcher: { + config: "#{PATHS[:haproxy]}/haproxy_dispatcher.cfg" + }, + master: { + config: "#{PATHS[:haproxy]}/haproxy_master.cfg", + port: 10080 + }, + slave: { + config: "#{PATHS[:haproxy]}/haproxy_slave.cfg", + port: 11080 + } +} + +# Logging helper +LOGGER = Logger.new(STDOUT) +LOGGER.level = if SUPERWHALE[:debug] + Logger::DEBUG + elsif SUPERWHALE[:info] + Logger::INFO + else + Logger::WARN + end + +# Checks if a host exists in /etc/hosts file +def hosts_exists?(hostname) + hosts = File.read PATHS[:hosts] + hosts.include? "\t#{hostname}\n" +end + +def compile_haproxy_config(frontend_directives, backend_blocks, instance_port) + # Loading header of haproxy configuration file + haproxy_cfg = if File.exist? SUPERWHALE[:custom][:header] + LOGGER.info 'Loaded custom haproxy config header' + File.read SUPERWHALE[:custom][:header] + else + LOGGER.info 'Loaded default haproxy config header' + File.read SUPERWHALE[:defaults][:header] + end + + # Compiling frontend block + haproxy_cfg << "\n\nfrontend public\n" + frontend_directives.each do |directive| + haproxy_cfg << " #{directive}\n".gsub('%INSTANCE_PORT%', instance_port.to_s) + end + + # Compiling backend blocks + backend_blocks.each do |name, directives| + haproxy_cfg << "\nbackend #{name}_backend\n" + directives.each do |directive| + haproxy_cfg << " #{directive}\n" + end + end + + haproxy_cfg +end + +# Create HAProxies configuration from superwhale services +def create_haproxy_config(generate_dispatcher) + + # Generating dispatcher configuration + if generate_dispatcher + # Dispatcher defaults + dispatcher_config = "defaults\n" + dispatcher_config << " maxconn 4096\n" + dispatcher_config << " log global\n" + dispatcher_config << " mode http\n" + dispatcher_config << " retries 3\n" + dispatcher_config << " timeout connect 5s\n" + dispatcher_config << " timeout client 15m\n" + dispatcher_config << " timeout server 15m\n\n" + + # Dispatcher frontend + dispatcher_config << "frontend public\n" + dispatcher_config << " bind *:80\n" + + # Frontend HTTPS management + if File.exist? SUPERWHALE[:custom][:https_cert] + LOGGER.info 'Certificate found, activating SSL' + dispatcher_config << " bind *:443 ssl crt #{SUPERWHALE[:custom][:https_cert]}\n" + if ARGV.include? '--force-ssl' + dispatcher_config << " redirect scheme https if !{ ssl_fc }\n" + end + else + LOGGER.warn 'No certificate found, disabling SSL capabilities' + end + dispatcher_config << " default_backend dispatched\n\n" + + # Dispatcher backend + dispatcher_config << "backend dispatched\n" + dispatcher_config << " server master 127.0.0.1:#{HAPROXY[:master][:port]} check\n" + dispatcher_config << " server slave 127.0.0.1:#{HAPROXY[:slave][:port]} check backup\n" + + LOGGER.debug "Dispatcher configuration:\n\n#{dispatcher_config}" + + File.write HAPROXY[:dispatcher][:config], dispatcher_config + end + + + LOGGER.info 'Parsing configurations' + + # Frontend block + frontend_directives = [] + frontend_directives << 'bind *:%INSTANCE_PORT%' + + # Backend blocks + backend_blocks = {} + + # Iterating on services + default_backend_found = false + Dir[SUPERWHALE[:services]].each do |file| + begin + services = YAML.load_file file + services.each do |name, options| + LOGGER.debug "Parsing service #{name} : #{options.to_s}" + + # Checking if a service is using the same name + if backend_blocks.has_key?(name) + LOGGER.fatal 'Multiple services are using the same name, can\'t continue !' + exit 11 + end + + # Parsing and iterating on hosts of current service + hosts = [] + LOGGER.info "Configuring and adding #{name} service : " + options['backends'].each do |backend| + if hosts_exists? backend['host'] + hosts << backend + LOGGER.info "[Service/#{name}] Adding #{name}/#{backend['host']}" + else + LOGGER.warn "[Service/#{name}] Skipping #{name}/#{backend['host']}, can't found it in /etc/hosts" + end + end + if hosts.count == 0 + LOGGER.warn "No host available for service #{name} : skipping it" + SUPERWHALE[:failed_services] << name unless SUPERWHALE[:failed_services].include? name + next + else + if SUPERWHALE[:failed_services].include? name + LOGGER.warn "Service #{name} is now available again" + SUPERWHALE[:failed_services].delete name + else + LOGGER.info "Service #{name} loaded" + end + end + + # Configuring service in frontend block + frontend_directives << "acl host_#{name} hdr(host) -i #{options['domain_name']}" + frontend_directives << "use_backend #{name}_backend if host_#{name}" + if options['is_default'] + if default_backend_found + LOGGER.fatal 'Multiple default backend in config file, can\'t continue' + exit 10 + end + frontend_directives << "default_backend #{name}_backend" + default_backend_found = true + end + + # Defining service backend block + backend_blocks[name] = [] + + # Adding load-balancing algorithm if needed + if hosts.count > 1 + backend_blocks[name] << (options['balance'] || 'roundrobin') + end + + # Iterating on backend hosts + current_index = 0 + hosts.each do |host| + backend_blocks[name] << "server #{name}#{current_index+=1} #{host['host']}:#{host['port']}" + end + + # Adding custom options + options['options'].each do |custom_option| + backend_blocks[name] << custom_option + end if options.has_key? 'options' + end + rescue + # Something wrong happened while parsing service file, ignoring it. + LOGGER.warn "Incorrect service file : #{File.basename(file)}" + end + end + + # Generating and writing final configuration files, for master... + master_config = compile_haproxy_config frontend_directives, backend_blocks, HAPROXY[:master][:port] + File.write HAPROXY[:master][:config], master_config + # ...and for slave + slave_config = compile_haproxy_config frontend_directives, backend_blocks, HAPROXY[:slave][:port] + File.write HAPROXY[:slave][:config], slave_config + + LOGGER.debug "Master HAProxy configuration file generated :\n\n#{master_config}\n" + LOGGER.debug "Slave HAProxy configuration file generated :\n\n#{slave_config}\n" + + LOGGER.info 'HAProxy configuration file created' +end + +# Fork HAProxy and return its PID +def start_haproxy(config_file) + LOGGER.info 'Starting up HAProxy...' + pid = fork do + exec "#{HAPROXY[:binary]} -f #{config_file} #{HAPROXY[:debug_flag] if SUPERWHALE[:debug]}" + end + LOGGER.info "Forked haproxy (#{config_file}) with PID : #{pid}" + pid +end + +# Entry point +def main + LOGGER.warn "Starting up SuperWhale, version #{PATHS[:superwhale][:version]} !" + + # Creating haproxy configuration file including dispatcher config + create_haproxy_config true + + # Starting master HAProxy process... + haproxy_master_pid = start_haproxy HAPROXY[:master][:config] + haproxy_slave_pid = start_haproxy HAPROXY[:slave][:config] + + sleep 2 + + # Starting dispatcher HAProxy process... + start_haproxy HAPROXY[:dispatcher][:config] + + # Watching for file changes, (blocking) + FileWatcher.new([PATHS[:hosts], "#{PATHS[:superwhale][:custom]}/*"]).watch do + LOGGER.warn 'File modification detected !' + + # Recreating haproxy configuration file without dispatcher config + create_haproxy_config false + + # Killing slave + LOGGER.info 'Killing slave' + Process.kill 'USR1', haproxy_slave_pid + Process.wait haproxy_slave_pid + LOGGER.info 'Killing slave done' + + # Restarting slave with new configuration + LOGGER.info 'Restarting slave' + haproxy_slave_pid = start_haproxy HAPROXY[:slave][:config] + + # Killing master + LOGGER.info 'Killing master' + Process.kill 'USR1', haproxy_master_pid + Process.wait haproxy_master_pid + LOGGER.info 'Killing master done' + + # Restarting master with new configuration + LOGGER.info 'Restarting master' + haproxy_master_pid = start_haproxy HAPROXY[:master][:config] + end +end + +# Calling entry point +main \ No newline at end of file diff --git a/doc/superwhale.png b/doc/superwhale.png new file mode 100644 index 0000000000000000000000000000000000000000..6f098870fcca54823af329d3cfae246995863908 GIT binary patch literal 29883 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 G81#Pyc?$Oc literal 0 HcmV?d00001 diff --git a/lib/header.cfg b/lib/header.cfg new file mode 100644 index 0000000..33d9d1c --- /dev/null +++ b/lib/header.cfg @@ -0,0 +1,8 @@ +defaults + maxconn 4096 + log global + mode http + retries 3 + timeout connect 5s + timeout client 15min + timeout server 15min \ No newline at end of file