├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── main.go └── tf ├── ec2_user_data.tmpl ├── main.tf └── task-definition.json.tmpl /.dockerignore: -------------------------------------------------------------------------------- 1 | tf/ 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tf/variables.tf 2 | 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | *.prof 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.6.1 2 | 3 | # Copy the local package files to the container's workspace. 4 | ADD . /go/src/tmp/hello-go-ecs-terraform 5 | 6 | # Build the command inside the container. 7 | RUN go install tmp/hello-go-ecs-terraform 8 | 9 | # Run the command by default when the container starts. 10 | ENTRYPOINT ["/go/bin/hello-go-ecs-terraform", "-port", "8080"] 11 | 12 | EXPOSE 8080 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Gregory Trubetskoy 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: all help deps plan apply destroy show force_deploy 3 | 4 | ENV ?= ${USER} 5 | 6 | all: help 7 | 8 | help: 9 | @echo "Usage:" 10 | @echo " make plan" 11 | @echo " make apply" 12 | @echo " make show" 13 | @echo " make force_deploy" 14 | @echo " make destroy" 15 | 16 | deps: 17 | @hash terraform > /dev/null 2>&1 || (echo "Install terraform to continue"; exit 1) 18 | @echo Environment: ${ENV} 19 | 20 | plan: deps 21 | @cd tf; \ 22 | terraform get; \ 23 | TF_VAR_environ="${ENV}" terraform plan --state=${ENV}.tfstate 24 | 25 | apply: deps 26 | @cd tf; \ 27 | terraform get; \ 28 | TF_VAR_environ="${ENV}" terraform apply --state=${ENV}.tfstate 29 | 30 | destroy: deps 31 | @cd tf; \ 32 | TF_VAR_environ="${ENV}" terraform destroy --state=${ENV}.tfstate 33 | 34 | show: deps 35 | @cd tf; \ 36 | TF_VAR_environ="${ENV}" terraform show ${ENV}.tfstate 37 | 38 | # Force deploy of the code as it presently is 39 | force_deploy: deps 40 | @cd tf; \ 41 | terraform get; \ 42 | terraform taint --state=${ENV}.tfstate null_resource.docker; \ 43 | TF_VAR_environ="${ENV}" terraform apply --state=${ENV}.tfstate 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | A "Hello World" in Golang deployed to AWS ECS (Docker) using Terraform 3 | 4 | Big thanks to [Tadas Vilkeliskis](http://vilkeliskis.com/about/) for 5 | [Bootstrapping Docker Infrastructure With Terraform](http://vilkeliskis.com/blog/2016/02/10/bootstrapping-docker-with-terraform.html) 6 | write up. 7 | 8 | # How to use this 9 | 1. Make sure you have the following installed and/or created/configured/working: 10 | * [Docker](http://www.docker.com) 11 | * [Docker Hub](https://hub.docker.com/) account 12 | * [Golang](https://golang.org/doc/install) 13 | * [Terraform](https://www.terraform.io/) 14 | * [AWS access](https://console.aws.amazon.com/) with admin priviliges and your [AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) is working 15 | * [AWS Key Pair](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html) 16 | 17 | 1. Allow Terraform to use your credentials in `~/.aws/config` by symlinking it to `credentials`: 18 | 19 | ```sh 20 | ln -s ~/.aws/confic ~/.aws/credentials 21 | ``` 22 | 1. Install this example: 23 | 24 | ```sh 25 | go get github.com/grisha/hello-go-ecs-terraform 26 | ``` 27 | 1. Change into the project directory: 28 | 29 | ```sh 30 | cd $GOPATH/src/grisha/hello-go-ecs-terraform 31 | ``` 32 | 33 | 1. Edit `tf/variables.tf` file so that it has the following: 34 | 35 | ``` 36 | variable "key_name" { default = "YOUR-AWS-KEY-PAIR-NAME" } 37 | variable "dockerimg" { default = "YOUR-DOCKER-HUB-USERNAME/IMAGE-NAME" } 38 | ``` 39 | 1. The docker hub username must be correct (though the actual image doesn not need to exist, it will be created for you), and you should be authenticated with: 40 | 41 | ```sh 42 | docker login 43 | ``` 44 | 1. Check that terraform works with: 45 | ```sh 46 | make plan 47 | ``` 48 | 49 | 1. If the above produces no errors, give it a try. Note that due to timing of AWS object creations sometimes you have to run this twice, it succeeds on the second try: 50 | ```sh 51 | make apply 52 | ``` 53 | 54 | 1. You should now see a load balancer in the AWS console, where it should list its DNS name. You should also see an ECS service and its tasks and associated EC2 instances. The whole thing will take a few minutes to create. 55 | 56 | 1. Once it's all created, you should be able to hit the ELB DNS name with your browser and see the app in action. 57 | 58 | 1. In this set up Terraform uses the `.git/logs/HEAD` file as the indicator that code has changed, but this file only changes when you commit something (The idea being that your CI, e.g. Jenkins would actually perform the `make apply`). If you want to force deploy the code that you currently have, you can do this: 59 | ```sh 60 | make force_deploy 61 | ``` 62 | Once you do this, you should see the ECS gradually replace your tasks with the new version. 63 | 64 | 1. When finished, you can destroy everything with: 65 | 66 | ```sh 67 | make destroy 68 | ``` 69 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // This simple program serves a plain text HTTP page displaying which 2 | // port it's listening on, when the process was started and the 3 | // hostname. 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "net/http" 11 | "os" 12 | "time" 13 | ) 14 | 15 | func hello(w http.ResponseWriter, r *http.Request, listenSpec string, start string, hostname string) { 16 | fmt.Fprintf(w, "Hello world!\n"+ 17 | "Listening on: %s\n"+ 18 | "Started on: %s\n"+ 19 | "Hostname: %s\n", listenSpec, start, hostname) 20 | } 21 | 22 | func main() { 23 | port := flag.Int("port", 8080, "HTTP port number") 24 | flag.Parse() 25 | 26 | listenSpec := fmt.Sprintf(":%d", *port) 27 | start := time.Now().String() 28 | hostname, _ := os.Hostname() 29 | 30 | fmt.Printf("Serving on %s.....\n", listenSpec) 31 | 32 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 33 | hello(w, r, listenSpec, start, hostname) 34 | }) 35 | http.ListenAndServe(listenSpec, nil) 36 | } 37 | -------------------------------------------------------------------------------- /tf/ec2_user_data.tmpl: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | bootcmd: 3 | - cloud-init-per instance $(echo "ECS_CLUSTER=${cluster_name}" >> /etc/ecs/ecs.config) 4 | -------------------------------------------------------------------------------- /tf/main.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "environ" {default = "UNKNOWN" } 3 | variable "appname" {default = "HelloGoEcsTerraform" } 4 | variable "host_port" { default = 8080 } 5 | variable "docker_port" { default = 8080 } 6 | variable "lb_port" { default = 80 } 7 | variable "aws_region" { default = "us-east-1" } 8 | variable "key_name" {} 9 | variable "dockerimg" {} 10 | 11 | # From https://github.com/aws/amazon-ecs-cli/blob/d566823dc716a83cf97bf93490f6e5c3c757a98a/ecs-cli/modules/config/ami/ami.go#L31 12 | variable "ami" { 13 | description = "AWS ECS AMI id" 14 | default = { 15 | us-east-1 = "ami-67a3a90d" 16 | us-west-1 = "ami-b7d5a8d7" 17 | us-west-2 = "ami-c7a451a7" 18 | eu-west-1 = "ami-9c9819ef" 19 | eu-central-1 = "ami-9aeb0af5" 20 | ap-northeast-1 = "ami-7e4a5b10" 21 | ap-southeast-1 = "ami-be63a9dd" 22 | ap-southeast-2 = "ami-b8cbe8db" 23 | } 24 | } 25 | 26 | provider "aws" { 27 | region = "${var.aws_region}" 28 | } 29 | 30 | module "vpc" { 31 | source = "github.com/terraform-community-modules/tf_aws_vpc" 32 | name = "${var.appname}-${var.environ}-vpc" 33 | cidr = "10.100.0.0/16" 34 | public_subnets = "10.100.101.0/24,10.100.102.0/24" 35 | azs = "us-east-1c,us-east-1b" 36 | } 37 | 38 | resource "aws_security_group" "allow_all_outbound" { 39 | name_prefix = "${var.appname}-${var.environ}-${module.vpc.vpc_id}-" 40 | description = "Allow all outbound traffic" 41 | vpc_id = "${module.vpc.vpc_id}" 42 | 43 | egress = { 44 | from_port = 0 45 | to_port = 0 46 | protocol = "-1" 47 | cidr_blocks = ["0.0.0.0/0"] 48 | } 49 | } 50 | 51 | resource "aws_security_group" "allow_all_inbound" { 52 | name_prefix = "${var.appname}-${var.environ}-${module.vpc.vpc_id}-" 53 | description = "Allow all inbound traffic" 54 | vpc_id = "${module.vpc.vpc_id}" 55 | 56 | ingress = { 57 | from_port = 0 58 | to_port = 0 59 | protocol = "-1" 60 | cidr_blocks = ["0.0.0.0/0"] 61 | } 62 | } 63 | 64 | resource "aws_security_group" "allow_cluster" { 65 | name_prefix = "${var.appname}-${var.environ}-${module.vpc.vpc_id}-" 66 | description = "Allow all traffic within cluster" 67 | vpc_id = "${module.vpc.vpc_id}" 68 | 69 | ingress = { 70 | from_port = 0 71 | to_port = 65535 72 | protocol = "tcp" 73 | self = true 74 | } 75 | 76 | egress = { 77 | from_port = 0 78 | to_port = 65535 79 | protocol = "tcp" 80 | self = true 81 | } 82 | } 83 | 84 | resource "aws_security_group" "allow_all_ssh" { 85 | name_prefix = "${var.appname}-${var.environ}-${module.vpc.vpc_id}-" 86 | description = "Allow all inbound SSH traffic" 87 | vpc_id = "${module.vpc.vpc_id}" 88 | 89 | ingress = { 90 | from_port = 22 91 | to_port = 22 92 | protocol = "tcp" 93 | cidr_blocks = ["0.0.0.0/0"] 94 | } 95 | } 96 | 97 | # This role has a trust relationship which allows 98 | # to assume the role of ec2 99 | resource "aws_iam_role" "ecs" { 100 | name = "${var.appname}_ecs_${var.environ}" 101 | assume_role_policy = <