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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 38 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -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!
Code available on [Github](https://github.com/RCM7/aws-factorio).
6 changes: 6 additions & 0 deletions healthchecker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM golang:1.11.1-alpine3.8

COPY ./healthchecker.go $GOPATH/src/healthchecker/healthchecker.go

RUN go install healthchecker

26 changes: 26 additions & 0 deletions healthchecker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Healthchecker

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

```
$ 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 .
$ 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

```
60 changes: 60 additions & 0 deletions healthchecker/healthchecker.go
Original file line number Diff line number Diff line change
@@ -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 <url>\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)
}

}
3 changes: 3 additions & 0 deletions packer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
variables.json
roles
playbook.retry
56 changes: 56 additions & 0 deletions packer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 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

Load your AWS credentials. E.g.

```
export AWS_ACCESS_KEY_ID=heresomeaccesskey
export AWS_SECRET_ACCESS_KEY=heresomesecretkey
```

Install Ansible requirements:

```
$ ansible-galaxy install -r ./requirements.yml -p ./roles
```

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.
35 changes: 35 additions & 0 deletions packer/packer.json
Original file line number Diff line number Diff line change
@@ -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": "{{user `region`}}",
"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'"]
}
]
}
17 changes: 17 additions & 0 deletions packer/playbook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---

- hosts: all
roles:
- role: geerlingguy.docker
become: true

tasks:
- name: Move timeapp to /opt
become: true
command: "mv /home/admin/timeapp /opt/"

- name: Build timeapp docker image
become: true
command: "docker-compose build"
args:
chdir: /opt/timeapp/
6 changes: 6 additions & 0 deletions packer/requirements.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---

- name: geerlingguy.docker
src: geerlingguy.docker
version: 2.5.1

8 changes: 8 additions & 0 deletions packer/variables.json.sample
Original file line number Diff line number Diff line change
@@ -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"
}
5 changes: 5 additions & 0 deletions terraform/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.terraform
*.plan
*.tfstate
*.tfstate.backup
timeapp.tfvars
45 changes: 45 additions & 0 deletions terraform/README.md
Original file line number Diff line number Diff line change
@@ -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 | - |
Loading