Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# osx
.DS_Store
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
sudo: required

language: generic

services:
- docker

script:
- docker build -t ecs-nginx-proxy:latest .
13 changes: 13 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM nginx:latest

WORKDIR /root/

RUN apt-get update && apt-get install -y -q --no-install-recommends curl unzip && apt-get clean

# download release of ecs-gen
ENV ECS_GEN_VERSION 0.2.0
RUN curl -OL https://github.com/codesuki/ecs-gen/releases/download/$ECS_GEN_VERSION/ecs-gen-linux-amd64.zip && unzip ecs-gen-linux-amd64.zip && cp ecs-gen-linux-amd64 /usr/local/bin/ecs-gen

COPY nginx.tmpl nginx.tmpl

CMD nginx && ecs-gen --signal="nginx -s reload" --template=nginx.tmpl --output=/etc/nginx/conf.d/default.conf
77 changes: 76 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,77 @@
# ecs-nginx-proxy
Reverse proxy for AWS ECS. Let's you address your docker containers by sub domain.
[![License](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](./LICENSE)
[![Build Status](http://img.shields.io/travis/codesuki/ecs-nginx-proxy.svg?style=flat)](https://travis-ci.org/codesuki/ecs-nginx-proxy)
[![nginx latest](https://img.shields.io/badge/nginx-latest-brightgreen.svg?style=flat)](https://hub.docker.com/_/nginx/)

ecs-nginx-proxy lets you run a nginx reverse proxy in an AWS ECS cluster. <br/>
Uses [ecs-gen](https://github.com/codesuki/ecs-gen) to automatically make containers accessible by subdomain as they are started. <br/>
My use case is using a wildcard domain to make per branch test environments accessible by branch.domain.com. Heavily inspired by [nginx-proxy](https://github.com/jwilder/nginx-proxy).

## Sample use case
You want to spin up development environments on AWS ECS for each pull request on your project.
How do you make this easy to use? Do you look up the instance IP and connect directly? <br/>
The easiest, at least for me, is to setup a wildcard DNS record and route to each deployed branch based on the subdomain, e.g. `*.domain.com`, `branch.domain.com`. <br/>
This projects enables you to do that.

## Usage
### Requirements
* Wildcard domain like `*.domain.com`
* ELB/ALB for this domain
* ECS Cluster
* EC2 instances in the cluster need a role including `ecs:Describe*` and `ecs:List*`
* Easiest is to use `AmazonEC2ContainerServiceFullAccess` although that gives more permissions than needed

### Setup
* Create a new ECS task
* Add a container using the `codesuki/ecs-nginx-proxy` docker image and make port 80 accessible
* Create a new service using the above task and a ELB
* Connect to the ELB serving the wildcard domain

### Adding containers
Each container you want to make accessible needs to have its corresponding port mapped (can be random mapping) and the environment variable `VIRTUAL_HOST` set to the hostname it should respond to.

## Sample ECS task and service description
For reference JSON descriptions for the ecs-nginx-proxy [task](./examples/task.json), [service](./examples/service.json) and a [sample task](./examples/sample_task.json) can be found in the `examples/` folder.
Check out the commands below or just the sample descriptions if you already know how to work with AWS ECS.

To register the sample tasks and services with your AWS ECS cluster run the following commands.
### Register task
#### Requirements
* ECS Cluster
* Cluster EC2 instances need `ecs:Describe*` and `ecs:List*` permissions (see [Requirements](#usage) above)
```
aws ecs register-task-definition --cli-input-json file://./examples/task.json
```

### Register service
#### Requirements
* ELB or ALB + Target Group
* Service role for the ELB/ALB containing `AmazonEC2ContainerServiceRole`

#### If you use ELB
You need to supply the load balancer name.
```
aws ecs create-service --cluster <NAME> --role <NAME> --load-balancers loadBalancerName=<NAME>,containerName=ecs-nginx-proxy,containerPort=80 --cli-input-json file://./examples/service.json
```

#### If you use ALB
You need to supply the target group ARN.
```
aws ecs create-service --cluster <NAME> --role <NAME> --load-balancers targetGroupArn=<ARN>,containerName=ecs-nginx-proxy,containerPort=80 --cli-input-json file://./examples/service.json
```

### Register sample task
Before running the commands below change the `VIRTUAL_HOST` environment variable in [examples/samples_task.json](./examples/sample_task.json) to a domain corresponding to your load balancer setup.

```
aws ecs register-task-definition --cli-input-json file://./examples/sample_task.json
```

### Register sample service
```
aws ecs create-service --cluster <NAME> --service-name sample-service --task-definition sample-task --desired-count 1
```

## TODO
* Support SSL connections
* Support path based routing (e.g. domain.com/service)
24 changes: 24 additions & 0 deletions examples/sample_task.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"networkMode": "bridge",
"containerDefinitions": [
{
"memory": 100,
"portMappings": [
{
"containerPort": 80,
"protocol": "tcp"
}
],
"environment": [
{
"name": "VIRTUAL_HOST",
"value": "sample.domain.com"
}
],
"essential": true,
"name": "sample-container",
"image": "nginx:latest"
}
],
"family": "sample-task"
}
7 changes: 7 additions & 0 deletions examples/service.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"cluster": "ecs-nginx-proxy-cluster",
"serviceName": "ecs-nginx-proxy",
"taskDefinition": "ecs-nginx-proxy",
"desiredCount": 1,
"role": "ecsServiceRole"
}
19 changes: 19 additions & 0 deletions examples/task.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"networkMode": "bridge",
"containerDefinitions": [
{
"memory": 200,
"portMappings": [
{
"hostPort": 80,
"containerPort": 80,
"protocol": "tcp"
}
],
"essential": true,
"name": "ecs-nginx-proxy",
"image": "codesuki/ecs-nginx-proxy:latest"
}
],
"family": "ecs-nginx-proxy"
}
48 changes: 48 additions & 0 deletions nginx.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the
# scheme used to connect to this server
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
# If we receive Upgrade, set Connection to "upgrade"; otherwise, delete any
# Connection header that may have been passed to this server
map $http_upgrade $proxy_connection {
default upgrade;
'' close;
}
gzip_types text/plain text/css application/javascript application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
log_format vhost '$host $remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log off;
# HTTP 1.1 support
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $proxy_connection;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
# Mitigate httpoxy attack (see https://github.com/jwilder/nginx-proxy for details)
proxy_set_header Proxy "";
server {
server_name _; # This is just an invalid value which will never trigger on a real hostname.
listen 80;
access_log /var/log/nginx/access.log vhost;
return 503;
}

{{ range $index, $value := . }}
upstream {{ $value.Host }} {
server {{ $value.Address }}:{{ $value.Port }};
}
server {
server_name {{ $value.Host }};
listen 80;
access_log /var/log/nginx/access.log vhost;
location / {
proxy_pass http://{{ $value.Host }};
}
}
{{ end }}