From 66d32b91351d613ebad2ff169500476914b1941c Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Wed, 24 Oct 2018 00:57:34 +0800 Subject: [PATCH 01/10] Add timeapp Go app Dockerfile Docker compose with nginx proxy --- timeapp/Dockerfile | 9 +++++++++ timeapp/docker-compose.yml | 26 ++++++++++++++++++++++++++ timeapp/timeapp.env | 1 + timeapp/timeapp.go | 19 +++++++++++++++++++ 4 files changed, 55 insertions(+) create mode 100644 timeapp/Dockerfile create mode 100644 timeapp/docker-compose.yml create mode 100644 timeapp/timeapp.env create mode 100644 timeapp/timeapp.go diff --git a/timeapp/Dockerfile b/timeapp/Dockerfile new file mode 100644 index 0000000..dde6072 --- /dev/null +++ b/timeapp/Dockerfile @@ -0,0 +1,9 @@ +FROM golang:1.11.1-alpine3.8 + +COPY ./timeapp.go $GOPATH/src/timeapp/timeapp.go + +RUN go install timeapp + +EXPOSE 8080 + +CMD $GOPATH/bin/timeapp diff --git a/timeapp/docker-compose.yml b/timeapp/docker-compose.yml new file mode 100644 index 0000000..bd89388 --- /dev/null +++ b/timeapp/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3' + +services: + + timeapp: + build: ./ + restart: always + environment: + - VIRTUAL_PORT=8080 + env_file: + - ./timeapp.env + networks: + - proxy + + proxy: + image: jwilder/nginx-proxy:alpine-0.7.0 + restart: always + ports: + - 80:80 + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + networks: + - proxy + +networks: + proxy: diff --git a/timeapp/timeapp.env b/timeapp/timeapp.env new file mode 100644 index 0000000..3eeb1d0 --- /dev/null +++ b/timeapp/timeapp.env @@ -0,0 +1 @@ +VIRTUAL_HOST=localhost diff --git a/timeapp/timeapp.go b/timeapp/timeapp.go new file mode 100644 index 0000000..d9c9fa5 --- /dev/null +++ b/timeapp/timeapp.go @@ -0,0 +1,19 @@ +package main + +import ( + "time" + "net/http" + "io" + "log" +) + +func main() { + http.HandleFunc("/now", nowTime) + if err := http.ListenAndServe(":8080", nil); err != nil { + log.Printf("An error ocurred: %s\n", err) + } +} + +func nowTime(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, time.Now().UTC().String()) +} From 426f2224d090d6baec50464230949c9def6e7606 Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Wed, 24 Oct 2018 02:08:11 +0800 Subject: [PATCH 02/10] Add packer scripts Provision image using ansible: - install docker - pull and build necessary images to run timeapp --- packer/.gitignore | 3 +++ packer/README.md | 40 ++++++++++++++++++++++++++++++++++++ packer/packer.json | 35 +++++++++++++++++++++++++++++++ packer/playbook.yml | 23 +++++++++++++++++++++ packer/requirements.yml | 6 ++++++ packer/variables.json.sample | 8 ++++++++ 6 files changed, 115 insertions(+) create mode 100644 packer/.gitignore create mode 100644 packer/README.md create mode 100644 packer/packer.json create mode 100644 packer/playbook.yml create mode 100644 packer/requirements.yml create mode 100644 packer/variables.json.sample diff --git a/packer/.gitignore b/packer/.gitignore new file mode 100644 index 0000000..8999871 --- /dev/null +++ b/packer/.gitignore @@ -0,0 +1,3 @@ +variables.json +roles +playbook.retry diff --git a/packer/README.md b/packer/README.md new file mode 100644 index 0000000..7505b7d --- /dev/null +++ b/packer/README.md @@ -0,0 +1,40 @@ +# Timeapp Packer + +Packer scripts to create the Timeapp base image. + +## Requirements + + - Ansible - (used 2.6.2) - [Installation Docs](http://docs.ansible.com/ansible/latest/intro_installation.html) + - Packer - (used 1.3.1) - [Installation Docs](https://www.packer.io/downloads.html) + +## Configuration + +Copy the `variables.json.sample` file and customize accordingly. + +``` +$ cp variables.json.sample variables.json +$ vim variables.json +$ cat variables.json +{ + "source_ami": "ami-0483f1cc1c483803f", + "vpc_id": "", + "subnet_id": "", + "region": "eu-west-1", + "docker_version": "18.06.1", + "timeapp_version": "0.0.1" +} +``` + +## Usage + +Install Ansible requirements: + +``` +$ ansible-galaxy install -r ./requirements.yml -p ./roles +``` + +Build Timeapp image: + +``` +$ packer build -var-file=variables.json ./packer.json +``` diff --git a/packer/packer.json b/packer/packer.json new file mode 100644 index 0000000..0860bec --- /dev/null +++ b/packer/packer.json @@ -0,0 +1,35 @@ +{ + "variables": { + "source_ami": "{{env `source_ami`}}", + "vpc_id": "{{env `vpc_id`}}", + "subnet_id": "{{env `subnet_id`}}", + "region": "{{env `region`}}", + "docker_version": "{{env `docker_version`}}", + "timeapp_version": "{{env `timeapp_version`}}" + }, + + "builders": [{ + "type": "amazon-ebs", + "region": "eu-west-1", + "source_ami": "{{user `source_ami`}}", + "instance_type": "t2.micro", + "ssh_username": "admin", + "associate_public_ip_address": true, + "vpc_id": "{{user `vpc_id`}}", + "subnet_id": "{{user `subnet_id`}}", + "ami_name": "timeapp-{{user `timeapp_version`}}-({{isotime \"20060102150405\"}})" + }], + + "provisioners": [ + { + "type": "file", + "source": "../timeapp", + "destination": "/home/admin/timeapp" + }, + { + "type": "ansible", + "playbook_file": "./playbook.yml", + "extra_arguments": [ "--extra-vars", "docker_package='docker-ce={{user `docker_version`}}~ce~3-0~debian'"] + } + ] +} diff --git a/packer/playbook.yml b/packer/playbook.yml new file mode 100644 index 0000000..3c27985 --- /dev/null +++ b/packer/playbook.yml @@ -0,0 +1,23 @@ +--- + +- hosts: all + roles: + - role: geerlingguy.docker + become: true + + tasks: + - name: Move timeapp to /opt + become: true + command: "mv /home/admin/timeapp /opt/" + + - name: Pull docker images + become: true + command: "docker-compose pull" + args: + chdir: /opt/timeapp/ + + - name: Build timeapp docker image + become: true + command: "docker-compose build" + args: + chdir: /opt/timeapp/ diff --git a/packer/requirements.yml b/packer/requirements.yml new file mode 100644 index 0000000..4870bfe --- /dev/null +++ b/packer/requirements.yml @@ -0,0 +1,6 @@ +--- + +- name: geerlingguy.docker + src: geerlingguy.docker + version: 2.5.1 + diff --git a/packer/variables.json.sample b/packer/variables.json.sample new file mode 100644 index 0000000..cf7f35e --- /dev/null +++ b/packer/variables.json.sample @@ -0,0 +1,8 @@ +{ + "source_ami": "ami-0483f1cc1c483803f", + "vpc_id": "", + "subnet_id": "", + "region": "eu-west-1", + "docker_version": "18.06.1", + "timeapp_version": "0.0.1" +} From d299533c0aebf624333786871271238c6b95443f Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Fri, 26 Oct 2018 04:10:44 +0800 Subject: [PATCH 03/10] Add terraform vpc module --- terraform/aws-vpc-module/README.md | 53 +++++++++ terraform/aws-vpc-module/main.tf | 163 ++++++++++++++++++++++++++ terraform/aws-vpc-module/outputs.tf | 43 +++++++ terraform/aws-vpc-module/variables.tf | 45 +++++++ 4 files changed, 304 insertions(+) create mode 100644 terraform/aws-vpc-module/README.md create mode 100644 terraform/aws-vpc-module/main.tf create mode 100644 terraform/aws-vpc-module/outputs.tf create mode 100644 terraform/aws-vpc-module/variables.tf diff --git a/terraform/aws-vpc-module/README.md b/terraform/aws-vpc-module/README.md new file mode 100644 index 0000000..6f4ab61 --- /dev/null +++ b/terraform/aws-vpc-module/README.md @@ -0,0 +1,53 @@ +# VPC Terraform Module + +A terraform module to create and manage a VPC in AWS. + + +## Module Input Variables + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|:----:|:-----:|:-----:| +| aws_azs | List of Availability Zones to use for this VPC subnets. | string | - | yes | +| dns_domain | The domain to which this VPC belongs to. Managed throught the 'environment' variable. | map | `` | no | +| dns_ttl | - | string | `600` | no | +| enable\_dns_hostnames | Should be true if you want to have custom DNS hostnames within the VPC. | string | `true` | no | +| enable\_dns_support | Should be true if you want to have DNS support whitin the VPC. | string | `true` | no | +| environment | The environment designation for which this VPC will be created. Can only be one of: 'stg' or 'prd'. | string | - | yes | +| service_name | The name/reference for this VPC. | string | `default` | no | +| vpc\_cidr_block | - | string | `10.0.0.0/24` | no | +| vpc\_public\_subnet\_cidr_blocks | The CIDR block for the VPC. | string | `10.0.0.0/28` | no | + +## Usage + +```hcl + +module "vpc" { + source = "./aws-vpc-module/" + + aws_azs = "eu-west-1a,eu-west-1b,eu-west-1c" + environment = "stg" + service_name = "engage" + vpc_cidr_block = "10.0.0.0/24" + vpc_public_subnet_cidr_blocks = "10.0.0.0/28,10.0.0.16/28,10.0.0.32/28" +} + +``` + + +## Outputs + +| Name | Description | +|------|-------------| +| dns_resolvers | - | +| dns\_zone_id | - | +| vpc_cidr | - | +| vpc\_default\_sg_id | - | +| vpc_id | - | +| vpc\_igw_id | - | +| vpc\_nat_eips | - | +| vpc\_nat\_gw_ids | - | +| vpc\_private\_route\_table_ids | - | +| vpc\_public\_route\_table_id | - | +| vpc\_public\_subnet_ids | - | diff --git a/terraform/aws-vpc-module/main.tf b/terraform/aws-vpc-module/main.tf new file mode 100644 index 0000000..b905572 --- /dev/null +++ b/terraform/aws-vpc-module/main.tf @@ -0,0 +1,163 @@ +# +# VPC for the infrastructure. +# +resource "aws_vpc" "main" { + cidr_block = "${var.vpc_cidr_block}" + enable_dns_hostnames = "${var.enable_dns_hostnames}" + enable_dns_support = "${var.enable_dns_support}" + + tags { + Name = "${lower(var.environment)}-${lower(var.service_name)}" + environment = "${lower(var.environment)}" + } +} + +# +# Gateway for outside world access (internet access). +# +resource "aws_internet_gateway" "main" { + vpc_id = "${aws_vpc.main.id}" + + tags { + Name = "${lower(var.environment)}-${lower(var.service_name)}" + environment = "${lower(var.environment)}" + } +} + +resource "aws_eip" "nat" { + count = "${length(split(",", var.vpc_public_subnet_cidr_blocks))}" + vpc = true +} + +resource "aws_nat_gateway" "main" { + count = "${length(split(",", var.vpc_public_subnet_cidr_blocks))}" + allocation_id = "${element(aws_eip.nat.*.id, count.index)}" + subnet_id = "${element(aws_subnet.public.*.id, count.index)}" + + lifecycle { + create_before_destroy = true + } +} + +# +# DHCP configurations. +# +resource "aws_vpc_dhcp_options" "main" { + domain_name_servers = ["AmazonProvidedDNS"] + domain_name = "${lower(lookup(var.dns_domain, lower(var.environment)))}" + + tags { + Name = "${lower(var.environment)}-${lower(var.service_name)}" + environment = "${lower(var.environment)}" + } +} + +resource "aws_vpc_dhcp_options_association" "main" { + vpc_id = "${aws_vpc.main.id}" + dhcp_options_id = "${aws_vpc_dhcp_options.main.id}" +} + +# +# Base network routing. +# + +# using nat gateway +resource "aws_route_table" "private_nat_gw" { + count = "${length(split(",", var.vpc_public_subnet_cidr_blocks))}" + + vpc_id = "${aws_vpc.main.id}" + + tags { + Name = "${lower(var.environment)}-${lower(var.service_name)}-private-${element(split(",", var.aws_azs), count.index)}" + environment = "${lower(var.environment)}" + } +} + +resource "aws_route" "private_nat_gw" { + count = "${length(split(",", var.vpc_public_subnet_cidr_blocks))}" + route_table_id = "${element(aws_route_table.private_nat_gw.*.id, count.index)}" + destination_cidr_block = "0.0.0.0/0" + nat_gateway_id = "${element(aws_nat_gateway.main.*.id, count.index)}" +} + +resource "aws_route_table" "public" { + vpc_id = "${aws_vpc.main.id}" + + tags { + Name = "${lower(var.environment)}-${lower(var.service_name)}-public" + environment = "${lower(var.environment)}" + } +} + +resource "aws_route" "public" { + route_table_id = "${aws_route_table.public.id}" + destination_cidr_block = "0.0.0.0/0" + gateway_id = "${aws_internet_gateway.main.id}" +} + +# +# Base subnets and respective routing associations. +# + +resource "aws_subnet" "public" { + vpc_id = "${aws_vpc.main.id}" + + count = "${length(split(",", var.vpc_public_subnet_cidr_blocks))}" + + cidr_block = "${element(split(",", var.vpc_public_subnet_cidr_blocks), count.index)}" + availability_zone = "${element(split(",", var.aws_azs), count.index)}" + map_public_ip_on_launch = true + + tags { + Name = "${lower(var.environment)}-${lower(var.service_name)}-public-${element(split(",", var.aws_azs), count.index)}" + environment = "${lower(var.environment)}" + } +} + +resource "aws_route_table_association" "public" { + count = "${length(split(",", var.vpc_public_subnet_cidr_blocks))}" + subnet_id = "${element(aws_subnet.public.*.id, count.index)}" + route_table_id = "${aws_route_table.public.id}" +} + +# +# Default Security Group (allows traffic between instances). +# +resource "aws_security_group" "default" { + name = "${lower(var.environment)}-${lower(var.service_name)}-sg" + vpc_id = "${aws_vpc.main.id}" + + ingress { + from_port = "0" + to_port = "0" + protocol = "-1" + self = true + } + + egress { + from_port = "0" + to_port = "0" + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags { + Name = "${lower(var.environment)}-${lower(var.service_name)}-sg" + environment = "${lower(var.environment)}" + } +} + +# +# DNS record. +# + +resource "aws_route53_zone" "default" { + name = "${lookup(var.dns_domain, var.environment)}" + vpc_id = "${aws_vpc.main.id}" + comment = "${lower(var.environment)} ${var.service_name} private DNS" + + tags { + Name = "${lower(var.environment)}-${lower(var.service_name)}" + environment = "${lower(var.environment)}" + } +} diff --git a/terraform/aws-vpc-module/outputs.tf b/terraform/aws-vpc-module/outputs.tf new file mode 100644 index 0000000..7b0a1a6 --- /dev/null +++ b/terraform/aws-vpc-module/outputs.tf @@ -0,0 +1,43 @@ +output "vpc_public_subnet_ids" { + value = "${join(",", aws_subnet.public.*.id)}" +} + +output "vpc_id" { + value = "${aws_vpc.main.id}" +} + +output "vpc_public_route_table_id" { + value = "${aws_route_table.public.id}" +} + +output "vpc_nat_eips" { + value = "${join(",", aws_eip.nat.*.public_ip)}" +} + +output "vpc_igw_id" { + value = "${aws_internet_gateway.main.id}" +} + +output "vpc_nat_gw_ids" { + value = "${join(",", aws_nat_gateway.main.*.id)}" +} + +output "vpc_cidr" { + value = "${aws_vpc.main.cidr_block}" +} + +output "vpc_default_sg_id" { + value = "${aws_security_group.default.id}" +} + +output "vpc_private_route_table_ids" { + value = "${join(",", aws_route_table.private_nat_gw.*.id)}" +} + +output "dns_zone_id" { + value = "${aws_route53_zone.default.id}" +} + +output "dns_resolvers" { + value = "${join(",", aws_route53_zone.default.name_servers)}" +} diff --git a/terraform/aws-vpc-module/variables.tf b/terraform/aws-vpc-module/variables.tf new file mode 100644 index 0000000..373396b --- /dev/null +++ b/terraform/aws-vpc-module/variables.tf @@ -0,0 +1,45 @@ +variable "service_name" { + description = "The name/reference for this VPC." + default = "default" +} + +variable "environment" { + description = "The environment designation for which this VPC will be created. Can only be one of: 'stg' or 'prd'." +} + +variable "vpc_cidr_block" { + default = "10.0.0.0/24" +} + +variable "vpc_public_subnet_cidr_blocks" { + description = "The CIDR block for the VPC." + default = "10.0.0.0/28" +} + +variable "dns_domain" { + description = "The domain to which this VPC belongs to. Managed throught the 'environment' variable." + type = "map" + + default = { + stg = "stg.engagetech.capsule.one" + prd = "prd.engagetech.capsule.one" + } +} + +variable "dns_ttl" { + default = "600" +} + +variable "aws_azs" { + description = "List of Availability Zones to use for this VPC subnets." +} + +variable "enable_dns_hostnames" { + description = "Should be true if you want to have custom DNS hostnames within the VPC." + default = true +} + +variable "enable_dns_support" { + description = "Should be true if you want to have DNS support whitin the VPC." + default = true +} From 4c8830bbbd8f03b35c971c864e69924c811900b8 Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Fri, 26 Oct 2018 04:11:15 +0800 Subject: [PATCH 04/10] Add terraform vpc subnet module --- terraform/aws-vpc-subnet-module/README.md | 38 ++++++++++++++++++++ terraform/aws-vpc-subnet-module/main.tf | 18 ++++++++++ terraform/aws-vpc-subnet-module/outputs.tf | 3 ++ terraform/aws-vpc-subnet-module/variables.tf | 28 +++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 terraform/aws-vpc-subnet-module/README.md create mode 100644 terraform/aws-vpc-subnet-module/main.tf create mode 100644 terraform/aws-vpc-subnet-module/outputs.tf create mode 100644 terraform/aws-vpc-subnet-module/variables.tf diff --git a/terraform/aws-vpc-subnet-module/README.md b/terraform/aws-vpc-subnet-module/README.md new file mode 100644 index 0000000..060510d --- /dev/null +++ b/terraform/aws-vpc-subnet-module/README.md @@ -0,0 +1,38 @@ +# Subnet Terraform Module + +A terraform module to create and manage a collection of Subnets in a VPC in AWS. + + +## Module Input Variables + +| Name | Description | Type | Default | Required | +|------|-------------|:----:|:-----:|:-----:| +| aws_azs | List with the Availability Zones for the subnet(s). | string | - | yes | +| environment | The environment designation for which this VPC will be created. Can only be one of: 'stg' or 'prd'. | string | - | yes | +| service_name | The name/reference for this subnet(s). | string | `general` | no | +| subnet_type | For naming purposes only. Indicate 'private' or 'public', according to the route tables specified. | string | - | yes | +| vpc_id | The id of the VPC that the subnet(s) will belong to. | string | - | yes | +| vpc\_route\_table_ids | Route table ids. | string | - | yes | +| vpc\_subnet\_cidr_blocks | List with the The CIDR block(s) for the subnet(s). | string | - | yes | + +## Usage + +```hcl +module "timeapp_subnets" { + source = "./aws-vpc-subnet-module/" + service_name = "timeapp" + environment = "stg" + aws_azs = "eu-west-1a,eu-west-1b,eu-west-1c" + vpc_id = "vpc-04793656e61730a" + vpc_subnet_cidr_blocks = "10.0.0.128/28,10.0.0.144/28,10.0.0.160/28" + vpc_route_table_ids = "rtb-022ea3c7c3c4366cb,rtb-042ec3c7c9c436ac2,rtb-723eb3d7c3e436ec7" + subnet_type = "private" +} +``` + + +## Outputs + +| Name | Description | +|------|-------------| +| subnet_ids | - | diff --git a/terraform/aws-vpc-subnet-module/main.tf b/terraform/aws-vpc-subnet-module/main.tf new file mode 100644 index 0000000..c1ed720 --- /dev/null +++ b/terraform/aws-vpc-subnet-module/main.tf @@ -0,0 +1,18 @@ +resource "aws_subnet" "default" { + vpc_id = "${var.vpc_id}" + count = "${length(compact(split(",", var.vpc_subnet_cidr_blocks)))}" + cidr_block = "${element(split(",", var.vpc_subnet_cidr_blocks), count.index)}" + availability_zone = "${element(split(",", var.aws_azs), count.index)}" + map_public_ip_on_launch = false + + tags { + Name = "${lower(var.environment)}-${lower(var.service_name)}-${var.subnet_type}-${element(split(",", var.aws_azs), count.index)}" + environment = "${lower(var.environment)}" + } +} + +resource "aws_route_table_association" "default" { + count = "${length(compact(split(",", var.vpc_subnet_cidr_blocks)))}" + subnet_id = "${element(aws_subnet.default.*.id, count.index)}" + route_table_id = "${element(split(",", var.vpc_route_table_ids), count.index)}" +} diff --git a/terraform/aws-vpc-subnet-module/outputs.tf b/terraform/aws-vpc-subnet-module/outputs.tf new file mode 100644 index 0000000..868d399 --- /dev/null +++ b/terraform/aws-vpc-subnet-module/outputs.tf @@ -0,0 +1,3 @@ +output "subnet_ids" { + value = "${join(",", aws_subnet.default.*.id)}" +} diff --git a/terraform/aws-vpc-subnet-module/variables.tf b/terraform/aws-vpc-subnet-module/variables.tf new file mode 100644 index 0000000..28fadd2 --- /dev/null +++ b/terraform/aws-vpc-subnet-module/variables.tf @@ -0,0 +1,28 @@ +variable "service_name" { + description = "The name/reference for this subnet(s)." + default = "general" +} + +variable "environment" { + description = "The environment designation for which this VPC will be created. Can only be one of: 'stg' or 'prd'." +} + +variable "aws_azs" { + description = "List with the Availability Zones for the subnet(s)." +} + +variable "vpc_id" { + description = "The id of the VPC that the subnet(s) will belong to." +} + +variable "vpc_subnet_cidr_blocks" { + description = "List with the The CIDR block(s) for the subnet(s)." +} + +variable "vpc_route_table_ids" { + description = "Route table ids." +} + +variable "subnet_type" { + description = "For naming purposes only. Indicate 'private' or 'public', according to the route tables specified." +} From 7b7a3f6ac81562debe75bd9edcdb7102b46d6074 Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Fri, 26 Oct 2018 04:12:46 +0800 Subject: [PATCH 05/10] Add timeapp module Add terraform scripts to deploy the whole infrastructure Remove proxy nginx from timeapp docker-compose Update ansible to only build images locally --- packer/README.md | 16 +++ packer/playbook.yml | 6 - terraform/.gitignore | 5 + terraform/README.md | 45 ++++++++ terraform/main.tf | 37 ++++++ terraform/outputs.tf | 3 + terraform/timeapp-module/README.md | 61 ++++++++++ terraform/timeapp-module/init.tpl | 13 +++ terraform/timeapp-module/main.tf | 160 ++++++++++++++++++++++++++ terraform/timeapp-module/outputs.tf | 3 + terraform/timeapp-module/variables.tf | 106 +++++++++++++++++ terraform/timeapp.tfvars.sample | 3 + terraform/variables.tf | 28 +++++ timeapp/README.md | 31 +++++ timeapp/docker-compose.yml | 18 +-- timeapp/timeapp.env | 1 - 16 files changed, 512 insertions(+), 24 deletions(-) create mode 100644 terraform/.gitignore create mode 100644 terraform/README.md create mode 100644 terraform/main.tf create mode 100644 terraform/outputs.tf create mode 100644 terraform/timeapp-module/README.md create mode 100644 terraform/timeapp-module/init.tpl create mode 100644 terraform/timeapp-module/main.tf create mode 100644 terraform/timeapp-module/outputs.tf create mode 100644 terraform/timeapp-module/variables.tf create mode 100644 terraform/timeapp.tfvars.sample create mode 100644 terraform/variables.tf create mode 100644 timeapp/README.md delete mode 100644 timeapp/timeapp.env diff --git a/packer/README.md b/packer/README.md index 7505b7d..5240cb2 100644 --- a/packer/README.md +++ b/packer/README.md @@ -27,6 +27,13 @@ $ cat variables.json ## Usage +Load your AWS credentials. E.g. + +``` +export AWS_ACCESS_KEY_ID=heresomeaccesskey +export AWS_SECRET_ACCESS_KEY=heresomesecretkey +``` + Install Ansible requirements: ``` @@ -38,3 +45,12 @@ Build Timeapp image: ``` $ packer build -var-file=variables.json ./packer.json ``` + +If everything goes as expected, at the end of the previous command output you'll get the new ami: + +``` +==> Builds finished. The artifacts of successful builds are: +--> amazon-ebs: AMIs were created: +eu-west-1: ami-09cf0fb8ff8269c8f +``` +This will be used on the next step with Terraform. \ No newline at end of file diff --git a/packer/playbook.yml b/packer/playbook.yml index 3c27985..9392615 100644 --- a/packer/playbook.yml +++ b/packer/playbook.yml @@ -10,12 +10,6 @@ become: true command: "mv /home/admin/timeapp /opt/" - - name: Pull docker images - become: true - command: "docker-compose pull" - args: - chdir: /opt/timeapp/ - - name: Build timeapp docker image become: true command: "docker-compose build" diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 0000000..27b6327 --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,5 @@ +.terraform +*.plan +*.tfstate +*.tfstate.backup +timeapp.tfvars diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 0000000..d71b478 --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,45 @@ +# Terraform Timeapp + +The terraform component includes the following modules: + + - [aws-vpc-module](./aws-vpc-module/README.md) - Create VPC where we'll deploy all resources + - [aws-vpc-subnet-module](./aws-vpc-subnet-module/README.md) - Create private subnets where Timeapp instances will be deployed + - [timeapp-module](./timeapp-module/README.md) - Create Timeapp deployment + +## Requirements + + - Terraform - (used 0.11.7) - [Installation Docs](https://www.terraform.io/downloads.html) + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|:----:|:-----:|:-----:| +| aws\_access_key | - | string | - | yes | +| aws_azs | List with the Availability Zones for the subnet(s). | string | `eu-west-1a,eu-west-1b,eu-west-1c` | no | +| aws_region | - | string | `eu-west-1` | no | +| aws\_secret_key | - | string | - | yes | +| dns\_public\_zone_id | The id of the public Route53 Zone where the public DNS record for the timeapp will be created. | string | - | yes | +| ec2\_ami_id | The AMI that will be used to provision new instances. | string | - | yes | +| ec2_keypair | The SSH key to be used when provisioning new instances. | string | - | yes | +| environment | The environment designation for which this deployment will be created. Can only be one of: 'stg' or 'prd'. | string | `stg` | no | + +## Usage +``` +# export env vars for terraform to use +$ export TF_VAR_aws_access_key=$AWS_ACCESS_KEY_ID +$ export TF_VAR_aws_secret_key=$AWS_SECRET_ACCESS_KEY + +# copy and edit input variables +$ cp timeapp.tfvars.sample timeapp.tfvars +$ vim timeapp.tfvars + +$ terraform init +$ terraform plan -var-file=timeapp.tfvars -out terraform.plan +$ terraform apply terraform.plan +``` + +## Outputs + +| Name | Description | +|------|-------------| +| timeapp_url | - | \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..01dc9a8 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,37 @@ +provider "aws" { + access_key = "${var.aws_access_key}" + secret_key = "${var.aws_secret_key}" + region = "${var.aws_region}" +} + +module "vpc" { + source = "./aws-vpc-module/" + aws_azs = "${var.aws_azs}" + environment = "${var.environment}" + service_name = "engage" + vpc_cidr_block = "10.0.0.0/24" + vpc_public_subnet_cidr_blocks = "10.0.0.0/28,10.0.0.16/28,10.0.0.32/28" +} + +module "timeapp_subnets" { + source = "./aws-vpc-subnet-module/" + service_name = "timeapp" + environment = "${var.environment}" + aws_azs = "${var.aws_azs}" + vpc_id = "${module.vpc.vpc_id}" + vpc_subnet_cidr_blocks = "10.0.0.128/28,10.0.0.144/28,10.0.0.160/28" + vpc_route_table_ids = "${module.vpc.vpc_private_route_table_ids}" + subnet_type = "private" +} + +module "timeapp" { + source = "./timeapp-module/" + service_name = "timeapp" + environment = "${var.environment}" + vpc_id = "${module.vpc.vpc_id}" + ec2_keypair = "${var.ec2_keypair}" + ec2_ami_id = "${var.ec2_ami_id}" + vpc_private_subnet_ids = "${module.timeapp_subnets.subnet_ids}" + vpc_public_subnet_ids = "${module.vpc.vpc_public_subnet_ids}" + dns_public_zone_id = "${var.dns_public_zone_id}" +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..7191fe5 --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,3 @@ +output "timeapp_url" { + value = "http://${module.timeapp.timeapp_fqdn}/now" +} diff --git a/terraform/timeapp-module/README.md b/terraform/timeapp-module/README.md new file mode 100644 index 0000000..7932730 --- /dev/null +++ b/terraform/timeapp-module/README.md @@ -0,0 +1,61 @@ +# Timeapp Terraform Module + +A terraform module to create and manage timeapp deployments in AWS. + +This module will create an Autoscaling Group with multiple instances of Timeapp across different Availability Zones. + +Inbound requests go to an Application Load Balancer and are forwarded to the Timeapp instances. + +The Timeapp endpoint `/now` is also used as healthcheck for the Load Balancer to drain Unhealthy instances, and for the Auto Scaling Group to terminate them and create new ones. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|:----:|:-----:|:-----:| +| asg\_default_cooldown | Time (in seconds) between a scaling activity and the succeeding scaling activity. | string | `300` | no | +| asg\_health\_check\_grace_period | Time (in seconds) after instance comes into service before checking health. | string | `300` | no | +| asg\_health\_check_type | Type of health check to be perform. Must be one of 'EC2' or 'ELB'. | string | `ELB` | no | +| desired_instances | - | string | `3` | no | +| dns\_domain | The domain to which this VPC belongs to. Managed throught the 'environment' variable. | map | `` | no | +| dns\_public\_zone_id | The id of the DNS public Zone to place new dns records | string | - | yes | +| ebs\_root\_volume_iops | - | string | `0` | no | +| ebs\_root\_volume_size | - | string | `16` | no | +| ebs\_root\_volume_type | - | string | `gp2` | no | +| ec2\_ami_id | - | string | - | yes | +| ec2\_instance_type | - | string | `t2.small` | no | +| ec2_keypair | - | string | - | yes | +| environment | - | string | - | yes | +| lb\_health\_check\_healthy_threshold | The number of consecutive LB Health Checks successes required before considering an unhealthy target healthy. | string | `5` | no | +| lb\_health\_check_interval | The inverval for LB Health Checks, in seconds. | string | `10` | no | +| lb\_health\_check_matcher | The status codes to use when checking for a successful response from a target. | string | `200` | no | +| lb\_health\_check_path | Path to use for health checks. | string | `/now` | no | +| lb\_health\_check_timeout | The connection timeout for LB Health Checks, in seconds. | string | `5` | no | +| lb\_health\_check\_unhealthy_threshold | The number of consecutive Health Check failures required before considering the target unhealthy. | string | `2` | no | +| max_instances | - | string | `5` | no | +| min_instances | - | string | `2` | no | +| service_name | The name of the app | string | `timeapp` | no | +| vpc_id | - | string | - | yes | +| vpc\_private\_subnet_ids | Comma separated list of the private subnet ids | string | - | yes | +| vpc\_public\_subnet_ids | Comma separated list of the private subnet ids | string | - | yes | + +## Usage + +``` +module "timeapp" { + source = "./timeapp-module/" + service_name = "timeapp" + environment = "stg" + vpc_id = "vpc-04793656e61730a" + ec2_keypair = "your-ssh-key" + ec2_ami_id = "ami-09cf0fb8ff8269c8f" + vpc_private_subnet_ids = "subnet-0b91069166f1d21a1,subnet-0c90c69cc6fcd28aa,subnet-0797067768f9928c9" + vpc_public_subnet_ids = "subnet-0b91c69c66c1dc1a1,subnet-0ca0ca9ca6fad2aaa,subnet-2794065768a992cb9" + dns_public_zone_id = "ZXW99LFDMOAMRV" +} +``` + +## Outputs + +| Name | Description | +|------|-------------| +| timeapp_fqdn | - | diff --git a/terraform/timeapp-module/init.tpl b/terraform/timeapp-module/init.tpl new file mode 100644 index 0000000..3ded299 --- /dev/null +++ b/terraform/timeapp-module/init.tpl @@ -0,0 +1,13 @@ +#cloud-config +manage_etc_hosts: false + +write_files: + - content: | + set -x + sudo docker-compose -f /opt/timeapp/docker-compose.yml up -d + path: /opt/start_timeapp.sh + permissions: '0750' + +runcmd: + - /opt/start_timeapp.sh + diff --git a/terraform/timeapp-module/main.tf b/terraform/timeapp-module/main.tf new file mode 100644 index 0000000..8f2dffd --- /dev/null +++ b/terraform/timeapp-module/main.tf @@ -0,0 +1,160 @@ +# Instance templates +data "template_file" "init" { + template = "${file("${path.module}/init.tpl")}" +} + +# Security groups +resource "aws_security_group" "timeapp-sg" { + name = "${lower(var.environment)}-${lower(var.service_name)}-sg" + vpc_id = "${var.vpc_id}" + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags { + Name = "${lower(var.environment)}-${lower(var.service_name)}-sg" + environment = "${lower(var.environment)}" + } +} + +resource "aws_security_group" "public-lb-sg" { + name = "${var.environment}-${var.service_name}-elb-pub-sg" + vpc_id = "${var.vpc_id}" + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags { + Name = "${var.environment}-${var.service_name}-lb-pub-sg" + environment = "${var.environment}" + } +} + +resource "aws_security_group_rule" "timeapp-allow-lb" { + type = "ingress" + from_port = 80 + to_port = 80 + protocol = "tcp" + source_security_group_id = "${aws_security_group.public-lb-sg.id}" + + security_group_id = "${aws_security_group.timeapp-sg.id}" +} + +# ALB , listener and target group + +resource "aws_lb" "timeapp" { + name = "${lower(var.environment)}-${lower(var.service_name)}" + internal = false + load_balancer_type = "application" + security_groups = ["${aws_security_group.public-lb-sg.id}"] + subnets = ["${compact(split(",",var.vpc_public_subnet_ids))}"] + + tags { + Name = "${lower(var.environment)}-${lower(var.service_name)}" + environment = "${lower(var.environment)}" + } +} + +resource "aws_lb_target_group" "timeapp" { + name = "${var.environment}-${var.service_name}" + port = 80 + protocol = "HTTP" + vpc_id = "${var.vpc_id}" + + health_check { + interval = "${var.lb_health_check_interval}" + timeout = "${var.lb_health_check_timeout}" + path = "${var.lb_health_check_path}" + healthy_threshold = "${var.lb_health_check_healthy_threshold}" + unhealthy_threshold = "${var.lb_health_check_unhealthy_threshold}" + matcher = "${var.lb_health_check_matcher}" + } + + tags { + Name = "${var.environment}-${var.service_name}" + Environment = "${var.environment}" + } +} + +resource "aws_lb_listener" "timeapp" { + load_balancer_arn = "${aws_lb.timeapp.arn}" + port = 80 + protocol = "HTTP" + + default_action { + target_group_arn = "${aws_lb_target_group.timeapp.arn}" + type = "forward" + } +} + +# Launch configuration and Auto scaling group + +resource "aws_launch_configuration" "timeapp" { + name_prefix = "${lower(var.environment)}-${lower(var.service_name)}-" + instance_type = "${var.ec2_instance_type}" + security_groups = ["${aws_security_group.timeapp-sg.id}"] + image_id = "${var.ec2_ami_id}" + key_name = "${var.ec2_keypair}" + + user_data = "${data.template_file.init.rendered}" + + root_block_device { + volume_type = "${var.ebs_root_volume_type}" + volume_size = "${var.ebs_root_volume_size}" + iops = "${var.ebs_root_volume_iops}" + } +} + +resource "aws_autoscaling_group" "timeapp" { + name = "${lower(var.environment)}-${lower(var.service_name)}" + vpc_zone_identifier = ["${element(compact(split(",",var.vpc_private_subnet_ids)), count.index)}"] + launch_configuration = "${aws_launch_configuration.timeapp.name}" + default_cooldown = "${var.asg_default_cooldown}" + health_check_grace_period = "${var.asg_health_check_grace_period}" + health_check_type = "${var.asg_health_check_type}" + + min_size = "${var.min_instances}" + max_size = "${var.max_instances}" + desired_capacity = "${var.desired_instances}" + + target_group_arns = ["${aws_lb_target_group.timeapp.arn}"] + + tag { + key = "Name" + value = "${lower(var.environment)}-${lower(var.service_name)}" + propagate_at_launch = true + } + + tag { + key = "environment" + value = "${lower(var.environment)}" + propagate_at_launch = true + } +} + +resource "aws_route53_record" "public_elb" { + zone_id = "${var.dns_public_zone_id}" + name = "${lower(var.service_name)}" + type = "A" + + alias { + name = "${aws_lb.timeapp.dns_name}" + zone_id = "${aws_lb.timeapp.zone_id}" + evaluate_target_health = false + } +} diff --git a/terraform/timeapp-module/outputs.tf b/terraform/timeapp-module/outputs.tf new file mode 100644 index 0000000..a57a08f --- /dev/null +++ b/terraform/timeapp-module/outputs.tf @@ -0,0 +1,3 @@ +output "timeapp_fqdn" { + value = "${aws_route53_record.public_elb.fqdn}" +} diff --git a/terraform/timeapp-module/variables.tf b/terraform/timeapp-module/variables.tf new file mode 100644 index 0000000..d0c7f1e --- /dev/null +++ b/terraform/timeapp-module/variables.tf @@ -0,0 +1,106 @@ +variable "environment" {} + +variable "service_name" { + default = "timeapp" + description = "The name of the app" +} + +variable "dns_domain" { + description = "The domain to which this VPC belongs to. Managed throught the 'environment' variable." + type = "map" + + default = { + stg = "stg.engagetech.capsule.one" + prd = "prd.engagetech.capsule.one" + } +} + +variable "dns_public_zone_id" { + description = "The id of the DNS public Zone to place new dns records" +} + +variable "vpc_id" {} + +variable "vpc_public_subnet_ids" { + description = "Comma separated list of the private subnet ids" +} + +variable "vpc_private_subnet_ids" { + description = "Comma separated list of the private subnet ids" +} + +variable "ec2_instance_type" { + default = "t2.small" +} + +variable "ec2_keypair" {} +variable "ec2_ami_id" {} + +variable "ebs_root_volume_type" { + default = "gp2" +} + +variable "ebs_root_volume_size" { + default = 16 +} + +variable "ebs_root_volume_iops" { + default = 0 +} + +variable "asg_default_cooldown" { + default = 300 + description = "Time (in seconds) between a scaling activity and the succeeding scaling activity." +} + +variable "asg_health_check_grace_period" { + default = 300 + description = "Time (in seconds) after instance comes into service before checking health." +} + +variable "asg_health_check_type" { + default = "ELB" + description = "Type of health check to be perform. Must be one of 'EC2' or 'ELB'." +} + +variable "max_instances" { + default = 5 +} + +variable "min_instances" { + default = 2 +} + +variable "desired_instances" { + default = 3 +} + +variable "lb_health_check_interval" { + description = "The inverval for LB Health Checks, in seconds." + default = 10 +} + +variable "lb_health_check_timeout" { + description = "The connection timeout for LB Health Checks, in seconds." + default = 5 +} + +variable "lb_health_check_healthy_threshold" { + description = "The number of consecutive LB Health Checks successes required before considering an unhealthy target healthy." + default = 5 +} + +variable "lb_health_check_unhealthy_threshold" { + description = "The number of consecutive Health Check failures required before considering the target unhealthy." + default = 2 +} + +variable "lb_health_check_matcher" { + description = "The status codes to use when checking for a successful response from a target." + default = 200 +} + +variable "lb_health_check_path" { + description = "Path to use for health checks." + default = "/now" +} diff --git a/terraform/timeapp.tfvars.sample b/terraform/timeapp.tfvars.sample new file mode 100644 index 0000000..770b123 --- /dev/null +++ b/terraform/timeapp.tfvars.sample @@ -0,0 +1,3 @@ +dns_public_zone_id="" +ec2_keypair="" +ec2_ami_id="" diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..39a4970 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,28 @@ +variable "aws_access_key" {} +variable "aws_secret_key" {} + +variable "aws_region" { + default = "eu-west-1" +} + +variable "aws_azs" { + default = "eu-west-1a,eu-west-1b,eu-west-1c" + description = "List with the Availability Zones for the subnet(s)." +} + +variable "environment" { + default = "stg" + description = "The environment designation for which this deployment will be created. Can only be one of: 'stg' or 'prd'." +} + +variable "dns_public_zone_id" { + description = "The id of the public Route53 Zone where the public DNS record for the timeapp will be created." +} + +variable "ec2_ami_id" { + description = "The AMI that will be used to provision new instances." +} + +variable "ec2_keypair" { + description = "The SSH key to be used when provisioning new instances." +} diff --git a/timeapp/README.md b/timeapp/README.md new file mode 100644 index 0000000..dde725d --- /dev/null +++ b/timeapp/README.md @@ -0,0 +1,31 @@ +# Timeapp + +Timeapp is a small application that serves HTTP requests on the endpoint `/now` and returns the current time. + +## Build and Run + +``` +$ go build timeapp.go +$ ./timeapp +``` + +### Docker + +``` +$ docker build --rm -t timeapp . +$ docker run -d -p 80:8080 timeapp +``` + +### Docker-compose + +``` +$ docker-compose build +$ docker-compose up -d +``` + +## Usage + +``` +$ curl localhost/now +2018-10-25 19:40:46.926302223 +0000 UTC +``` \ No newline at end of file diff --git a/timeapp/docker-compose.yml b/timeapp/docker-compose.yml index bd89388..d445ae5 100644 --- a/timeapp/docker-compose.yml +++ b/timeapp/docker-compose.yml @@ -5,22 +5,6 @@ services: timeapp: build: ./ restart: always - environment: - - VIRTUAL_PORT=8080 - env_file: - - ./timeapp.env - networks: - - proxy - - proxy: - image: jwilder/nginx-proxy:alpine-0.7.0 - restart: always ports: - - 80:80 - volumes: - - /var/run/docker.sock:/tmp/docker.sock:ro - networks: - - proxy + - "80:8080" -networks: - proxy: diff --git a/timeapp/timeapp.env b/timeapp/timeapp.env deleted file mode 100644 index 3eeb1d0..0000000 --- a/timeapp/timeapp.env +++ /dev/null @@ -1 +0,0 @@ -VIRTUAL_HOST=localhost From 4c532c1be49fb2df91760117f796f91d64820d69 Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Fri, 26 Oct 2018 04:15:39 +0800 Subject: [PATCH 06/10] Add healthchecker --- healthchecker/Dockerfile | 6 ++++ healthchecker/README.md | 27 +++++++++++++++ healthchecker/healthchecker.go | 60 ++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 healthchecker/Dockerfile create mode 100644 healthchecker/README.md create mode 100644 healthchecker/healthchecker.go diff --git a/healthchecker/Dockerfile b/healthchecker/Dockerfile new file mode 100644 index 0000000..1d670d3 --- /dev/null +++ b/healthchecker/Dockerfile @@ -0,0 +1,6 @@ +FROM golang:1.11.1-alpine3.8 + +COPY ./healthchecker.go $GOPATH/src/healthchecker/healthchecker.go + +RUN go install healthchecker + diff --git a/healthchecker/README.md b/healthchecker/README.md new file mode 100644 index 0000000..82f5c6d --- /dev/null +++ b/healthchecker/README.md @@ -0,0 +1,27 @@ +# Healthchecker + +Healthchecker is a small application that makes HTTP GET requests to [Timeapp's](../timeapp/README.md) every 2 seconds and prints whether the clock is synchronized or not, and the respective time difference in seconds. + +## Build and Run + +``` +$ go build healthchecker.go +$ ./healthchecker.go http://timeapp.stg.engagetech.capsule.one/now +``` + +### Docker + +``` +$ docker build --rm -t hcker . +``` + +## Usage + +``` +$ docker run --rm hcker healthchecker http://timeapp.stg.engagetech.capsule.one/now +Clock synchronized. Time diff: 0.431881s +Clock synchronized. Time diff: 0.101007s +Clock desynchronized. Time diff: 1.422374s +Clock synchronized. Time diff: 0.096573s + +``` \ No newline at end of file diff --git a/healthchecker/healthchecker.go b/healthchecker/healthchecker.go new file mode 100644 index 0000000..60602b3 --- /dev/null +++ b/healthchecker/healthchecker.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "time" +) + +func check(url string) (time.Duration, error) { + var err error + var resp *http.Response + now := time.Now().UTC() + + resp, err = http.Get(url) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + var body []byte + body, err = ioutil.ReadAll(resp.Body) + if err != nil { + return 0, err + } + + const layout = "2006-01-02 15:04:05.999999999 -0700 MST" + var remoteTime time.Time + remoteTime, err = time.Parse(layout, string(body)) + if err != nil { + return 0, err + } + + return remoteTime.Sub(now), nil +} + +func main() { + args := os.Args + if len(os.Args) != 2 { + fmt.Printf("Usage:\n%s \n", os.Args[0]) + os.Exit(1) + } + + for { + diff, err := check(args[1]) + if err != nil { + log.Printf("An error occurred: %s", err) + } else { + if diff.Seconds() > 1 { + fmt.Printf("Clock desynchronized. Time diff: %fs\n", diff.Seconds()) + } else { + fmt.Printf("Clock synchronized. Time diff: %fs\n", diff.Seconds()) + } + } + time.Sleep(2 * time.Second) + } + +} From deed234e432f917a868049fb12935e7fa320749a Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Fri, 26 Oct 2018 04:16:07 +0800 Subject: [PATCH 07/10] Fix timeapp.go with go fmt --- timeapp/timeapp.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/timeapp/timeapp.go b/timeapp/timeapp.go index d9c9fa5..65dfe5c 100644 --- a/timeapp/timeapp.go +++ b/timeapp/timeapp.go @@ -1,19 +1,19 @@ package main import ( - "time" - "net/http" - "io" - "log" + "io" + "log" + "net/http" + "time" ) func main() { - http.HandleFunc("/now", nowTime) - if err := http.ListenAndServe(":8080", nil); err != nil { - log.Printf("An error ocurred: %s\n", err) - } + http.HandleFunc("/now", nowTime) + if err := http.ListenAndServe(":8080", nil); err != nil { + log.Printf("An error ocurred: %s\n", err) + } } func nowTime(w http.ResponseWriter, r *http.Request) { - io.WriteString(w, time.Now().UTC().String()) + io.WriteString(w, time.Now().UTC().String()) } From 367beaec62fbd4472028137585c3b2d4cf7df0d2 Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Fri, 26 Oct 2018 04:19:59 +0800 Subject: [PATCH 08/10] Improve healthchecker README --- healthchecker/README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/healthchecker/README.md b/healthchecker/README.md index 82f5c6d..245acc4 100644 --- a/healthchecker/README.md +++ b/healthchecker/README.md @@ -7,21 +7,20 @@ Healthchecker is a small application that makes HTTP GET requests to [Timeapp's] ``` $ go build healthchecker.go $ ./healthchecker.go http://timeapp.stg.engagetech.capsule.one/now +Clock synchronized. Time diff: 0.431881s +Clock synchronized. Time diff: 0.101007s +Clock desynchronized. Time diff: 1.422374s +Clock synchronized. Time diff: 0.096573s ``` ### Docker ``` $ docker build --rm -t hcker . -``` - -## Usage - -``` $ docker run --rm hcker healthchecker http://timeapp.stg.engagetech.capsule.one/now Clock synchronized. Time diff: 0.431881s Clock synchronized. Time diff: 0.101007s Clock desynchronized. Time diff: 1.422374s Clock synchronized. Time diff: 0.096573s -``` \ No newline at end of file +``` From 67a4c4c1d08287b4745e6f96c00bbbcabeb37956 Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Fri, 26 Oct 2018 15:01:30 +0800 Subject: [PATCH 09/10] Replace root README.md Fix typo in healthchecker README --- README.md | 82 +++++++++++++++++++---------------------- healthchecker/README.md | 2 +- 2 files changed, 39 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index b4af648..eb816d9 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,59 @@ DevOps Coding Test ================== -# Goal +The goal of this coding test was _"Script the creation of a service, and a healthcheck script to verify it is up and responding correctly"_. -Script the creation of a service, and a healthcheck script to verify it is up and responding correctly. +## Packer and Ansible -# Prerequisites +In my proposed solution I've implemented an immutable infrastructure approach, easily achievable using [Packer](https://packer.io). The setup of this image is done using [Ansible](https://www.ansible.com), which allows a more flexible and developer friendly syntax when compared to regular shell scripts, specially if you leverage Ansible Roles. -You will need an AWS account. Create one if you don't own one already. You can use free-tier resources for this test. +In this case I've used Ansible to install Docker via a community Ansible Role, and added a playbook task that will prepare the Timeapp to be ready to run. -# The Task -You are required to provision and deploy a new service in AWS. It must: +## Terraform -* Be publicly accessible, but *only* on port 80. -* Return the current time on `/now`. +When the AMI is ready, it can be used by [Terraform](https://www.terraform.io) scripts to create all necessary AWS resources: -# Mandatory Work + - VPC + - Public Subnets + - Private Subnets + - Auto Scaling Group + - Security Groups + - Application Load Balancer + - DNS Record -Fork this repository. + The setup is as follows: + + - Timeapp instances are spinned through the Auto Scaling Group, in private subnets across different Availability Zones. These instances are not accessible to the internet, and only allow connections to the port `80` that are originated from the Aplication Load Balancer. + - The Application Load Balancer is associated with the public subnets created with the VPC, and redirects requests made to port `80` to the Timeapp instances, via Target Group that is associated to the Timeapp Autoscaling Group. + - The Load Balancer assures the traffic is distributed through all Healthy instances and drains Unhealthy ones, resulting in the Auto Scaling Group terminating the Unhealthy and provisioning new ones. + - The Auto Scaling Group assures there is always more than one instance running, providing us with high availability. + +With the use of Terraform Modules, this setup is highly configurable and flexible to all sorts of changes, as well as easily reproducible. -* Script your service using your configuration management and/or infrastructure-as-code tool of choice. -* Provision the service in your AWS account. -* Write a healthcheck script that can be run externally to periodically check if the service is up and its clock is not desynchronised by more than 1 second. -* Alter the README to contain instructions required to: - * Provision the service. - * Run the healthcheck script. -* Provide us IAM credentials to login to the AWS account. If you have other resources in it make sure we can only access what is related to this test. +## Timeapp and Healthchecker -Once done, give us access to your fork. Feel free to ask questions as you go if anything is unclear, confusing, or just plain missing. +Timeapp and healthchecker apps were both developed with Golang in very short files, accompanied by a respective Dockerfile for easiness of deployment. -# Extra Credit +## Usage Instructions -We know time is precious, we won't mark you down for not doing the extra credits, but if you want to give them a go... + - [Timeapp](./timeapp/README.md) + - [Healthchecker](./healthchecker/README.md) + - [Packer scripts](./packer/README.md) + - [Terraform scripts](./terraform/README.md) -* Run the service inside a Docker container. -* Make it highly available. -* We value CloudFormation and rely on it heavily. If you already know CF, we’d love to see you use it. +## Possible improvements -# Questions +There is always room for improvement, and possible steps I see to improve this solution even further are: -#### What scripting languages can I use? + - Add serverspec tests to Packer + - Have the Docker image built by a different pipeline and available in a Docker Registry instead of building it everytime + - Increase and decrease number of Auto Scaling Group instances based on CPU system load (cpu usage, etc..) + - Add logging and monitoring tooling to Timeapp instances -Anyone you like. You’ll have to justify your decision. We use CloudFormation, Puppet and Python internally. Please pick something you're familiar with, as you'll need to be able to discuss it. +## Related work -#### Will I have to pay for the AWS charges? +You can check previous work I did using this kind of approach in my blogpost [Automating infrastructure: playing factorio on AWS +](https://capsule.one/blog/2017/09/28/automating-infrastructure-playing-factorio-on-aws/) where I leverage the same tools to deploy a Factorio server, this time using AWS API Gateway and AWS Lambda. -No. You are expected to use free-tier resources only and not generate any charges. Please remember to delete your resources once the review process is over so you are not charged by AWS. - -#### What will you be grading me on? - -Scripting skills, security, elegance, understanding of the technologies you use, documentation. - -#### What will you not take into account? - -Brevity. We know there are very simple ways of solving this exercise, but we need to see your skills. We will not be able to evaluate you if you provide five lines of code. - -#### Will I have a chance to explain my choices? - -If we proceed to a phone interview, we’ll be asking questions about why you made the choices you made. Comments in the code are also very helpful. - -#### Why doesn't the test include X? - -Good question. Feel free to tell us how to make the test better. Or, you know, fork it and improve it! \ No newline at end of file +Code available on [Github](https://github.com/RCM7/aws-factorio). \ No newline at end of file diff --git a/healthchecker/README.md b/healthchecker/README.md index 245acc4..aa28ca2 100644 --- a/healthchecker/README.md +++ b/healthchecker/README.md @@ -1,6 +1,6 @@ # Healthchecker -Healthchecker is a small application that makes HTTP GET requests to [Timeapp's](../timeapp/README.md) every 2 seconds and prints whether the clock is synchronized or not, and the respective time difference in seconds. +Healthchecker is a small application that makes HTTP GET requests to [Timeapp](../timeapp/README.md) every 2 seconds and prints whether the clock is synchronized or not, and the respective time difference in seconds. ## Build and Run From da715cd1e7debb931d94f1cd4dc217e183640407 Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Fri, 26 Oct 2018 16:37:25 +0800 Subject: [PATCH 10/10] Fix packer region to use env var --- packer/packer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packer/packer.json b/packer/packer.json index 0860bec..2924f8d 100644 --- a/packer/packer.json +++ b/packer/packer.json @@ -10,7 +10,7 @@ "builders": [{ "type": "amazon-ebs", - "region": "eu-west-1", + "region": "{{user `region`}}", "source_ami": "{{user `source_ami`}}", "instance_type": "t2.micro", "ssh_username": "admin",