├── .gitignore ├── README.md ├── index.js ├── main.tf ├── package.json ├── policies └── lambda-role.json ├── sample-data ├── data.js └── data.json ├── template └── haproxy.cfg.njk └── variables.tf /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | 4 | terraform.tfstate 5 | terraform.tfstate.backup 6 | *.zip 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HAProxy Configuration Generator 2 | 3 | This project uses [AWS Lambda](https://aws.amazon.com/lambda/) and [API Gateway](https://aws.amazon.com/api-gateway/) to create an API endpoint that can be used to generate a `haproxy.cfg` file based on the parameters provided. 4 | 5 | One major pain point of using Lambda and API Gateway is the difficulty of setting things up. This project uses Terraform to ease that difficulty. 6 | 7 | You need to have [Terraform](https://www.terraform.io/) installed and a functioning [AWS](https://aws.amazon.com/) account to deploy this project. 8 | 9 | [Lambda-Registry](https://github.com/shuaibiyy/lambda-registry) is an iteration of this project that adds support for persisting the state of past services. Past services are services for which HAProxy configs have previously been generated. This state is then consolidated into future configs upon request. 10 | 11 | ## Usage 12 | 13 | Follow these steps to deploy: 14 | 15 | 1. Install NPM modules: `npm install` 16 | 2. Compress the project: `zip -r haproxy_config_generator.zip .`. 17 | 3. Deploy the project by simply invoking `terraform apply`. You'll be asked for your AWS credentials. If you don't want to be prompted, you can add your credentials to the `variables.tf` file or run the setup using: 18 | ```bash 19 | terraform apply -var 'aws_access_key={your_aws_access_key}' \ 20 | -var 'aws_secret_key={your_aws_secret_key}' 21 | ``` 22 | 23 | To tear down: 24 | ```bash 25 | terraform destroy 26 | ``` 27 | 28 | You can find the Invoke URL for the API endpoint created via the AWS console for API Gateway. The steps look like: `Amazon API Gateway | APIs > haproxy_config_generator > Stages > api`. 29 | 30 | You can generate the config file by running these commands: 31 | ```bash 32 | $ curl -o /tmp/haproxycfg -H "Content-Type: application/json" --data @sample-data/data.json /generate 33 | $ echo "$( haproxy.cfg 34 | $ rm /tmp/haproxycfg 35 | ``` 36 | 37 | ### Running Locally 38 | 39 | You can run Lambda functions locally using [Lambda-local](https://github.com/ashiina/lambda-local) with a command like: 40 | ```bash 41 | lambda-local -l index.js -h handler -e sample-data/data.js 42 | ``` 43 | 44 | ### Customizing the Project 45 | 46 | The Lambda handler expects an `event` with the structure documented in `index.js`. This structure is only relevant because the [Nunjucks](https://github.com/mozilla/nunjucks) template file (`template/haproxy.cfg.njk`) relies on it to interpolate values in the right places. You can pass in any `event` structure you want as long as you modify the Nunjucks template file to understand it. 47 | 48 | ## Notes 49 | 50 | There is a [known issue](https://forums.aws.amazon.com/message.jspa?messageID=678324) whereby a newly deployed API Gateway would fail to call a Lambda function throwing an error similar to this one: 51 | ```bash 52 | Execution failed due to configuration error: Invalid permissions on Lambda function 53 | Method completed with status: 500 54 | ``` 55 | Or: 56 | ```bash 57 | { 58 | "message": "Internal server error" 59 | } 60 | ``` 61 | The solution for this is straightforward and demonstrated in [this youtube video](https://www.youtube.com/watch?v=H4LM_jw5zzs). 62 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Provide an event that contains an array of objects with the following keys: 3 | * 4 | * - mode: type of routing. It can be either `path` or `host`. 5 | * In `path` mode, the URL path is used to determine which backend to forward the request to. 6 | * In `host` mode, the HTTP host header is used to determine which backend to forward the request to. 7 | * Defaults to `host` mode. 8 | * - name: name of cluster the servers will be grouped within. 9 | * - predicate: value used along with mode to determine which cluster the request will be forwarded to. 10 | * `path` mode example: `acl url_beg /`. 11 | * `host` mode example: `acl hdr(host) -i `. 12 | * - cookie: name of cookie to be used for sticky sessions. If not defined, sticky sessions will not be configured. 13 | * - servers: key-value pairs of server names and their corresponding IP addresses. 14 | * 15 | * Example: 16 | * ======= 17 | * [ 18 | * { 19 | * "mode": "host", 20 | * "name": "example", 21 | * "predicate": "example.com", 22 | * "cookie": "JSESSIONID", 23 | * "servers": [ 24 | * { 25 | * "name": "app1", 26 | * "ip" : "192.168.1.5:80" 27 | * }, 28 | * { 29 | * "name": "app2", 30 | * "ip" : "192.168.1.7:80" 31 | * } 32 | * ] 33 | * }, 34 | * { 35 | * "mode": "path", 36 | * "name": "multiservice", 37 | * "predicate": "service", 38 | * "servers": [ 39 | * { 40 | * "name": "service1", 41 | * "ip" : "10.0.0.5:80" 42 | * }, 43 | * { 44 | * "name": "service2", 45 | * "ip" : "10.0.0.6:80" 46 | * } 47 | * ] 48 | * } 49 | * ] 50 | * 51 | */ 52 | 53 | exports.handler = (event, context, callback) => { 54 | console.log('Received event:', JSON.stringify(event, null, 2)) 55 | 56 | const nunjucks = require('nunjucks') 57 | 58 | nunjucks.configure('template', { autoescape: true }) 59 | const computedConfig = nunjucks.render('haproxy.cfg.njk', {clusters: event}) 60 | 61 | context.done(null, computedConfig) 62 | } 63 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | access_key = "${var.aws_access_key}" 3 | secret_key = "${var.aws_secret_key}" 4 | region = "${var.region}" 5 | } 6 | 7 | resource "aws_iam_role" "generator_iam" { 8 | name = "generator_iam" 9 | assume_role_policy = "${file("policies/lambda-role.json")}" 10 | } 11 | 12 | resource "aws_lambda_function" "generator_lambda" { 13 | filename = "haproxy_config_generator.zip" 14 | function_name = "haproxy_config_generator" 15 | role = "${aws_iam_role.generator_iam.arn}" 16 | handler = "index.handler" 17 | runtime = "nodejs4.3" 18 | source_code_hash = "${base64sha256(file("haproxy_config_generator.zip"))}" 19 | } 20 | 21 | resource "aws_api_gateway_rest_api" "generator_api" { 22 | name = "haproxy_config_generator" 23 | description = "API for HAProxy Configuration Generator" 24 | depends_on = ["aws_lambda_function.generator_lambda"] 25 | } 26 | 27 | resource "aws_api_gateway_resource" "generator_resource" { 28 | rest_api_id = "${aws_api_gateway_rest_api.generator_api.id}" 29 | parent_id = "${aws_api_gateway_rest_api.generator_api.root_resource_id}" 30 | path_part = "generate" 31 | } 32 | 33 | resource "aws_api_gateway_method" "generator_method" { 34 | rest_api_id = "${aws_api_gateway_rest_api.generator_api.id}" 35 | resource_id = "${aws_api_gateway_resource.generator_resource.id}" 36 | http_method = "POST" 37 | authorization = "NONE" 38 | 39 | request_models = { 40 | "application/json" = "${aws_api_gateway_model.generator_request_model.name}" 41 | } 42 | } 43 | 44 | resource "aws_api_gateway_integration" "generator_integration" { 45 | rest_api_id = "${aws_api_gateway_rest_api.generator_api.id}" 46 | resource_id = "${aws_api_gateway_resource.generator_resource.id}" 47 | http_method = "${aws_api_gateway_method.generator_method.http_method}" 48 | type = "AWS" 49 | integration_http_method = "${aws_api_gateway_method.generator_method.http_method}" 50 | uri = "arn:aws:apigateway:${var.region}:lambda:path/2015-03-31/functions/${aws_lambda_function.generator_lambda.arn}/invocations" 51 | } 52 | 53 | resource "aws_api_gateway_model" "generator_request_model" { 54 | rest_api_id = "${aws_api_gateway_rest_api.generator_api.id}" 55 | name = "Configuration" 56 | description = "A configuration schema" 57 | content_type = "application/json" 58 | schema = <