├── terraform ├── output.tf ├── terraform.tfvars ├── vpc.tf ├── security-groups.tf ├── public-subnet.tf ├── variables.tf ├── private-subnet.tf ├── user_data │ └── app-server.tpl └── app-servers.tf ├── .gitignore ├── Dockerfile ├── requirements.txt ├── docker ├── nginx.conf └── docker-compose.yaml ├── scripts └── deploy.sh ├── .travis.yml ├── LICENSE ├── Makefile ├── app.py └── README.md /terraform/output.tf: -------------------------------------------------------------------------------- 1 | output "elb_address" { 2 | value = "${aws_elb.elb_app.dns_name}" 3 | } 4 | 5 | output "app_version" { 6 | value = "${var.api_version}" 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore pyenv 2 | bin/ 3 | include/ 4 | pip-selfcheck.json 5 | lib 6 | .Python 7 | 8 | # Ignore terraform 9 | terraform/.terraform 10 | terraform/terraform.tfstate* 11 | -------------------------------------------------------------------------------- /terraform/terraform.tfvars: -------------------------------------------------------------------------------- 1 | terragrunt = { 2 | remote_state { 3 | backend = "s3" 4 | config { 5 | bucket = "tf-flask-api" 6 | key = "api-app/terraform.tfstate" 7 | dynamodb_table = "tf-lock-state" 8 | region = "eu-west-2" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2-alpine 2 | LABEL maintainer="ember@pfragoso.org" 3 | 4 | ENV REDIS_URL localhost 5 | 6 | RUN pip install --upgrade pip 7 | RUN mkdir -p /app 8 | 9 | ADD app.py requirements.txt /app/ 10 | 11 | WORKDIR /app 12 | RUN pip install -r /app/requirements.txt 13 | 14 | EXPOSE 5000 15 | 16 | CMD ["python", "/app/app.py"] 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==3.0.0 2 | certifi==2018.4.16 3 | chardet==3.0.4 4 | click==6.7 5 | Flask==1.0.2 6 | Flask-Redis==0.3.0 7 | Flask-RESTful==0.3.6 8 | idna==2.6 9 | itsdangerous==0.24 10 | Jinja2==2.10 11 | MarkupSafe==1.0 12 | pytz==2018.4 13 | redis==2.10.6 14 | requests==2.20.0 15 | six==1.11.0 16 | urllib3==1.22 17 | Werkzeug==0.14.1 18 | -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | location / { 5 | proxy_pass http://api:5000; 6 | proxy_redirect off; 7 | proxy_set_header Host $host; 8 | proxy_set_header X-Real-IP $remote_addr; 9 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 10 | proxy_set_header X-Forwarded-Host $server_name; 11 | } 12 | 13 | location /health { 14 | access_log off; 15 | return 200 'OK!'; 16 | add_header Content-Type text/plain; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | web: 4 | image: nginx:alpine 5 | restart: always 6 | links: 7 | - api 8 | ports: 9 | - "8080:80" 10 | volumes: 11 | - "./nginx.conf:/etc/nginx/conf.d/default.conf" 12 | api: 13 | build: ../ 14 | links: 15 | - redis 16 | restart: always 17 | environment: 18 | - REDIS_URL=redis://api-redis:6379/0 19 | depends_on: 20 | - redis 21 | redis: 22 | container_name: api-redis 23 | image: redis 24 | restart: always 25 | 26 | -------------------------------------------------------------------------------- /terraform/vpc.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "s3" { 3 | bucket = "tf-flask-api" 4 | key = "api-app/terraform.tfstate" 5 | dynamodb_table = "tf-lock-state" 6 | region = "eu-west-2" 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = "${var.aws_region}" 12 | access_key = "${var.aws_access_key}" 13 | secret_key = "${var.aws_secret_key}" 14 | version = "~> 1.10" 15 | } 16 | 17 | resource "aws_vpc" "vpc_app" { 18 | cidr_block = "${var.vpc_cidr}" 19 | enable_dns_hostnames = "true" 20 | 21 | tags { 22 | Name = "${var.app_name}-vpc" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | docker run \ 6 | --rm \ 7 | --workdir /terraform \ 8 | --volume $(pwd)/terraform:/terraform \ 9 | -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \ 10 | chrisns/docker-terragrunt init -reconfigure --terragrunt-non-interactive 11 | 12 | docker run \ 13 | --rm \ 14 | --workdir /terraform \ 15 | --volume $(pwd)/terraform:/terraform \ 16 | -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \ 17 | chrisns/docker-terragrunt plan -out plan -var "api_version=$TRAVIS_COMMIT" 18 | 19 | docker run \ 20 | --rm \ 21 | --workdir /terraform \ 22 | --volume $(pwd)/terraform:/terraform \ 23 | -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \ 24 | chrisns/docker-terragrunt apply -auto-approve plan 25 | -------------------------------------------------------------------------------- /terraform/security-groups.tf: -------------------------------------------------------------------------------- 1 | resource "aws_security_group" "app-web" { 2 | name = "${var.app_name}" 3 | description = "${var.app_name}-app-web" 4 | vpc_id = "${aws_vpc.vpc_app.id}" 5 | 6 | ingress { 7 | from_port = "${var.app_port}" 8 | to_port = "${var.app_port}" 9 | protocol = "tcp" 10 | cidr_blocks = ["${aws_vpc.vpc_app.cidr_block}"] 11 | } 12 | 13 | egress { 14 | from_port = 0 15 | to_port = 0 16 | protocol = "-1" 17 | cidr_blocks = ["0.0.0.0/0"] 18 | } 19 | } 20 | 21 | resource "aws_security_group" "elb_web" { 22 | name = "${var.app_name}-elb" 23 | description = "${var.app_name}-elb" 24 | vpc_id = "${aws_vpc.vpc_app.id}" 25 | 26 | ingress { 27 | from_port = "80" 28 | to_port = "80" 29 | protocol = "tcp" 30 | cidr_blocks = ["0.0.0.0/0"] 31 | } 32 | 33 | egress { 34 | from_port = 0 35 | to_port = 0 36 | protocol = "-1" 37 | cidr_blocks = ["0.0.0.0/0"] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: bash 4 | 5 | services: 6 | - docker 7 | 8 | env: 9 | global: 10 | - DOCKER_IMAGE=pfragoso/flask-api 11 | 12 | stages: 13 | - name: build 14 | if: branch = master 15 | - name: deploy 16 | if: branch = master 17 | 18 | jobs: 19 | include: 20 | - stage: build 21 | script: 22 | - docker login --username "$DOCKER_USERNAME" --password "$DOCKER_PASSWORD" 23 | - docker build --rm --tag "$DOCKER_IMAGE":"$TRAVIS_COMMIT" . 24 | - docker build --rm --tag "$DOCKER_IMAGE":latest . 25 | - docker images 26 | - docker network create app 27 | - docker run --name redis --rm --detach -p 6379:6379 --network app redis 28 | - docker run --name flask-api --rm --detach -p 5000:5000 --network app -e "REDIS_URL=redis://redis:6379/0" "$DOCKER_IMAGE":"$TRAVIS_COMMIT" 29 | - docker ps 30 | - sleep 20 31 | - curl -I http://127.0.0.1:5000/api/kubernetes 32 | - docker push "$DOCKER_IMAGE":"$TRAVIS_COMMIT" 33 | - docker push "$DOCKER_IMAGE":latest 34 | - stage: deploy 35 | script: ./scripts/deploy.sh 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pedro Fragoso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /terraform/public-subnet.tf: -------------------------------------------------------------------------------- 1 | resource "aws_internet_gateway" "vpc_app" { 2 | vpc_id = "${aws_vpc.vpc_app.id}" 3 | } 4 | 5 | resource "aws_subnet" "public_az1" { 6 | vpc_id = "${aws_vpc.vpc_app.id}" 7 | cidr_block = "${var.public_subnet_az1_cidr}" 8 | availability_zone = "${var.aws_region}a" 9 | map_public_ip_on_launch = true 10 | 11 | tags { 12 | Name = "public az1" 13 | } 14 | } 15 | 16 | resource "aws_subnet" "public_az2" { 17 | vpc_id = "${aws_vpc.vpc_app.id}" 18 | cidr_block = "${var.public_subnet_az2_cidr}" 19 | availability_zone = "${var.aws_region}b" 20 | map_public_ip_on_launch = true 21 | 22 | tags { 23 | Name = "public az2" 24 | } 25 | } 26 | 27 | resource "aws_subnet" "public_az3" { 28 | vpc_id = "${aws_vpc.vpc_app.id}" 29 | cidr_block = "${var.public_subnet_az3_cidr}" 30 | availability_zone = "${var.aws_region}c" 31 | map_public_ip_on_launch = true 32 | 33 | tags { 34 | Name = "public az3" 35 | } 36 | } 37 | 38 | resource "aws_route_table" "public" { 39 | vpc_id = "${aws_vpc.vpc_app.id}" 40 | 41 | route { 42 | cidr_block = "0.0.0.0/0" 43 | gateway_id = "${aws_internet_gateway.vpc_app.id}" 44 | } 45 | } 46 | 47 | resource "aws_route_table_association" "public_az1" { 48 | subnet_id = "${aws_subnet.public_az1.id}" 49 | route_table_id = "${aws_route_table.public.id}" 50 | } 51 | 52 | resource "aws_route_table_association" "public_az2" { 53 | subnet_id = "${aws_subnet.public_az2.id}" 54 | route_table_id = "${aws_route_table.public.id}" 55 | } 56 | 57 | resource "aws_route_table_association" "public_az3" { 58 | subnet_id = "${aws_subnet.public_az3.id}" 59 | route_table_id = "${aws_route_table.public.id}" 60 | } 61 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ROOT_DIR:=$(dir $(abspath $(lastword $(MAKEFILE_LIST)))) 2 | TERRAFORM = cd terraform && terragrunt 3 | DOCKER_IMAGE:=pfragoso/flask-api 4 | APP_VERSION:=latest 5 | 6 | 7 | .PHONY: build-docker publish-docker run-local test 8 | build-docker: 9 | ifeq ($(APP_VERSION),latest) 10 | @docker build --rm --tag ${DOCKER_IMAGE}:latest . 11 | else 12 | @docker build --rm --tag ${DOCKER_IMAGE}:latest . 13 | @docker build --rm --tag ${DOCKER_IMAGE}:$(APP_VERSION) . 14 | endif 15 | 16 | 17 | publish-docker: check-docker-env build-docker 18 | docker login --username "${DOCKER_USERNAME}" --password "${DOCKER_PASSWORD}" 19 | ifeq ($(APP_VERSION),latest) 20 | @docker push ${DOCKER_IMAGE}:latest 21 | else 22 | @docker push ${DOCKER_IMAGE}:$(APP_VERSION) 23 | @docker push ${DOCKER_IMAGE}:latest 24 | endif 25 | 26 | run-local: 27 | @cd docker && docker-compose up -d 28 | 29 | check-docker-env: 30 | @test -n "$(DOCKER_USERNAME)" || \ 31 | (echo "DOCKER_USER env not set"; exit 1) 32 | @test -n "$(DOCKER_PASSWORD)" || \ 33 | (echo "DOCKER_PASSWORD env not set"; exit 1) 34 | 35 | .PHONY: init plan apply destroy infra deploy 36 | 37 | plan: check-aws-env 38 | @$(TERRAFORM) plan 39 | 40 | apply: check-aws-env plan 41 | @$(TERRAFORM) apply -auto-approve 42 | 43 | destroy: check-aws-env 44 | @$(TERRAFORM) destroy 45 | 46 | init: 47 | @$(TERRAFORM) init --terragrunt-non-interactive 48 | 49 | infra: init apply 50 | 51 | deploy: check-docker-env check-aws-env publish-docker 52 | @$(TERRAFORM) apply -var 'api_version=$(APP_VERSION)' -auto-approve 53 | 54 | #create-all: publish-docker infra 55 | create-all: infra 56 | 57 | check-aws-env: 58 | @test -n "$(AWS_ACCESS_KEY_ID)" || \ 59 | (echo "AWS_ACCESS_KEY_ID env not set"; exit 1) 60 | @test -n "$(AWS_SECRET_ACCESS_KEY)" || \ 61 | (echo "AWS_SECRET_ACCESS_KEY env not set"; exit 1) 62 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | // AWS specific vars 2 | variable "aws_access_key" { 3 | default = "" 4 | description = "AWS access key (AWS_ACCESS_KEY_ID)." 5 | } 6 | 7 | variable "aws_secret_key" { 8 | default = "" 9 | description = "AWS secret key (AWS_SECRET_ACCESS_KEY)." 10 | } 11 | 12 | variable "aws_region" { 13 | description = "AWS region" 14 | default = "eu-west-2" 15 | } 16 | 17 | // VPC vars 18 | variable "availability_zones" { 19 | default = "eu-west-2a,eu-west-2b,eu-west-2c" 20 | description = "List of availability zones" 21 | } 22 | 23 | variable "vpc_cidr" { 24 | description = "CIDR for VPC" 25 | default = "10.0.0.0/16" 26 | } 27 | 28 | variable "public_subnet_az1_cidr" { 29 | description = "CIDR for az1 public subnet" 30 | default = "10.0.10.0/24" 31 | } 32 | 33 | variable "public_subnet_az2_cidr" { 34 | description = "CIDR for az2 public subnet" 35 | default = "10.0.11.0/24" 36 | } 37 | 38 | variable "public_subnet_az3_cidr" { 39 | description = "CIDR for az3 public subnet" 40 | default = "10.0.12.0/24" 41 | } 42 | 43 | variable "private_subnet_az1_cidr" { 44 | description = "CIDR for az1 private subnet" 45 | default = "10.0.20.0/24" 46 | } 47 | 48 | variable "private_subnet_az2_cidr" { 49 | description = "CIDR for az2 private subnet" 50 | default = "10.0.21.0/24" 51 | } 52 | 53 | variable "private_subnet_az3_cidr" { 54 | description = "CIDR for az3 private subnet" 55 | default = "10.0.22.0/24" 56 | } 57 | 58 | // EC2 vars 59 | 60 | variable "instance_type" { 61 | description = "Instance type" 62 | default = "t1.micro" 63 | } 64 | 65 | variable "app_name" { 66 | description = "app name to be deployed" 67 | default = "flask-api" 68 | } 69 | 70 | variable "app_port" { 71 | description = "app port to be served" 72 | default = "80" 73 | } 74 | 75 | variable "api_version" { 76 | description = "build version from containers" 77 | default = "latest" 78 | } 79 | -------------------------------------------------------------------------------- /terraform/private-subnet.tf: -------------------------------------------------------------------------------- 1 | resource "aws_eip" "nat_gw_ip" { 2 | vpc = true 3 | } 4 | 5 | resource "aws_nat_gateway" "nat" { 6 | allocation_id = "${aws_eip.nat_gw_ip.id}" 7 | subnet_id = "${aws_subnet.public_az1.id}" 8 | } 9 | 10 | resource "aws_subnet" "private_az1" { 11 | vpc_id = "${aws_vpc.vpc_app.id}" 12 | cidr_block = "${var.private_subnet_az1_cidr}" 13 | availability_zone = "${var.aws_region}a" 14 | map_public_ip_on_launch = false 15 | 16 | tags { 17 | Name = "private az1" 18 | } 19 | } 20 | 21 | resource "aws_subnet" "private_az2" { 22 | vpc_id = "${aws_vpc.vpc_app.id}" 23 | cidr_block = "${var.private_subnet_az2_cidr}" 24 | availability_zone = "${var.aws_region}b" 25 | map_public_ip_on_launch = false 26 | 27 | tags { 28 | Name = "private az2" 29 | } 30 | } 31 | 32 | resource "aws_subnet" "private_az3" { 33 | vpc_id = "${aws_vpc.vpc_app.id}" 34 | cidr_block = "${var.private_subnet_az3_cidr}" 35 | availability_zone = "${var.aws_region}c" 36 | map_public_ip_on_launch = false 37 | 38 | tags { 39 | Name = "private az3" 40 | } 41 | } 42 | 43 | resource "aws_route_table" "private" { 44 | vpc_id = "${aws_vpc.vpc_app.id}" 45 | 46 | tags { 47 | Name = "Private route table" 48 | } 49 | } 50 | 51 | resource "aws_route" "private_route" { 52 | route_table_id = "${aws_route_table.private.id}" 53 | destination_cidr_block = "0.0.0.0/0" 54 | nat_gateway_id = "${aws_nat_gateway.nat.id}" 55 | } 56 | 57 | resource "aws_route_table_association" "private_az1" { 58 | subnet_id = "${aws_subnet.private_az1.id}" 59 | route_table_id = "${aws_route_table.private.id}" 60 | } 61 | 62 | resource "aws_route_table_association" "private_az2" { 63 | subnet_id = "${aws_subnet.private_az2.id}" 64 | route_table_id = "${aws_route_table.private.id}" 65 | } 66 | 67 | resource "aws_route_table_association" "private_az3" { 68 | subnet_id = "${aws_subnet.private_az3.id}" 69 | route_table_id = "${aws_route_table.private.id}" 70 | } 71 | -------------------------------------------------------------------------------- /terraform/user_data/app-server.tpl: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | repo_update: true 3 | repo_upgrade: all 4 | 5 | write_files: 6 | - content: | 7 | server { 8 | listen 80; 9 | 10 | location / { 11 | proxy_pass http://api:5000; 12 | proxy_redirect off; 13 | proxy_set_header Host $host; 14 | proxy_set_header X-Real-IP $remote_addr; 15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 16 | proxy_set_header X-Forwarded-Host $server_name; 17 | } 18 | 19 | location /health { 20 | access_log off; 21 | return 200 'OK!'; 22 | add_header Content-Type text/plain; 23 | } 24 | } 25 | path: /tmp/nginx.conf 26 | - content: | 27 | version: "3" 28 | services: 29 | web: 30 | image: nginx:alpine 31 | restart: unless-stopped 32 | links: 33 | - api 34 | ports: 35 | - "80:80" 36 | volumes: 37 | - "./nginx.conf:/etc/nginx/conf.d/default.conf" 38 | api: 39 | container_name: flask-api-${api_version} 40 | image: pfragoso/flask-api:${api_version} 41 | links: 42 | - redis 43 | restart: unless-stopped 44 | environment: 45 | - REDIS_URL=redis://api-redis:6379/0 46 | depends_on: 47 | - redis 48 | redis: 49 | container_name: api-redis 50 | image: redis 51 | restart: unless-stopped 52 | path: /tmp/docker-compose.yml 53 | runcmd: 54 | - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 55 | - add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 56 | - apt-get update 57 | - apt-get -y install docker-ce 58 | - usermod -aG docker ubuntu 59 | - curl -L https://github.com/docker/compose/releases/download/1.19.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose 60 | - chmod +x /usr/local/bin/docker-compose 61 | - systemctl start docker 62 | - mkdir /app 63 | - mv /tmp/docker-compose.yml /tmp/nginx.conf /app 64 | - chown ubuntu /app 65 | - su -l ubuntu -c "cd /app && docker-compose up -d" -------------------------------------------------------------------------------- /terraform/app-servers.tf: -------------------------------------------------------------------------------- 1 | data "template_file" "user_data_api" { 2 | template = "${file("${path.module}/user_data/app-server.tpl")}" 3 | 4 | vars { 5 | api_version = "${var.api_version}" 6 | } 7 | } 8 | 9 | resource "aws_elb" "elb_app" { 10 | name = "${var.app_name}" 11 | subnets = ["${aws_subnet.public_az1.id}", "${aws_subnet.public_az2.id}", "${aws_subnet.public_az3.id}"] 12 | security_groups = ["${aws_security_group.elb_web.id}"] 13 | 14 | listener { 15 | instance_port = "${var.app_port}" 16 | instance_protocol = "http" 17 | lb_port = "${var.app_port}" 18 | lb_protocol = "http" 19 | } 20 | 21 | health_check { 22 | healthy_threshold = 2 23 | unhealthy_threshold = 2 24 | timeout = 3 25 | target = "HTTP:${var.app_port}/api/kubernetes" 26 | interval = 5 27 | } 28 | 29 | cross_zone_load_balancing = true 30 | idle_timeout = 5 31 | connection_draining = true 32 | connection_draining_timeout = 60 33 | } 34 | 35 | data "aws_ami" "ubuntu_ami" { 36 | most_recent = true 37 | owners = ["099720109477"] 38 | 39 | filter { 40 | name = "name" 41 | values = ["ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*"] 42 | } 43 | 44 | filter { 45 | name = "virtualization-type" 46 | values = ["hvm"] 47 | } 48 | } 49 | 50 | resource "aws_launch_configuration" "lc_web_app" { 51 | lifecycle { 52 | create_before_destroy = true 53 | } 54 | 55 | image_id = "${data.aws_ami.ubuntu_ami.id}" 56 | instance_type = "t2.micro" 57 | security_groups = ["${aws_security_group.app-web.id}"] 58 | user_data = "${data.template_file.user_data_api.rendered}" 59 | } 60 | 61 | resource "aws_autoscaling_group" "asg_web_app" { 62 | lifecycle { 63 | create_before_destroy = true 64 | } 65 | 66 | name = "${aws_launch_configuration.lc_web_app.name}" 67 | load_balancers = ["${aws_elb.elb_app.name}"] 68 | vpc_zone_identifier = ["${aws_subnet.private_az1.id}", "${aws_subnet.private_az2.id}", "${aws_subnet.private_az3.id}"] 69 | min_size = 1 70 | max_size = 2 71 | desired_capacity = 2 72 | min_elb_capacity = 1 73 | 74 | launch_configuration = "${aws_launch_configuration.lc_web_app.name}" 75 | 76 | depends_on = ["aws_nat_gateway.nat"] 77 | 78 | tags { 79 | key = "Name" 80 | value = "${var.app_name}" 81 | propagate_at_launch = true 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from os import getenv 4 | from flask import Flask, request, jsonify 5 | from flask_restful import Resource, Api 6 | from flask_redis import FlaskRedis 7 | 8 | app = Flask(__name__) 9 | app.config['REDIS_URL'] = getenv('REDIS_URL', 'redis://localhost:6379/0') 10 | 11 | api = Api(app) 12 | redis_store = FlaskRedis(app) 13 | 14 | def populate_data(): 15 | if "indices" not in redis_store.keys(): 16 | pipe = redis_store.pipeline() 17 | obj = {} 18 | 19 | for i in range(1,6): 20 | r = requests.get('https://api.github.com/search/repositories?q=kubernetes&page={}&per_page=100'.format(i)) 21 | data = r.json()['items'] 22 | 23 | for j in range(len(data)): 24 | obj['id'] = data[j]['id'] 25 | obj['name'] = data[j]['name'] 26 | obj['full_name'] = data[j]['full_name'] 27 | obj['html_url'] = data[j]['html_url'] 28 | obj['language'] = data[j]['language'] 29 | obj['updated_at'] = data[j]['updated_at'] 30 | obj['pushed_at'] = data[j]['pushed_at'] 31 | obj['stargazers_count'] = data[j]['stargazers_count'] 32 | 33 | pipe.hmset(data[j]['id'], obj) 34 | pipe.expire(data[j]['id'], 3600) 35 | pipe.sadd("indices", data[j]['id']) 36 | 37 | pipe.expire("indices", 3600) 38 | pipe.execute() 39 | 40 | def query_all(): 41 | populate_data() 42 | 43 | obj = [] 44 | for keys in redis_store.smembers("indices"): 45 | obj.append(redis_store.hgetall(keys)) 46 | 47 | return obj 48 | 49 | def query_sort(sort_by): 50 | populate_data() 51 | 52 | obj = [] 53 | for keys in redis_store.sort("indices", by="*->{}".format(sort_by),desc=True): 54 | obj.append(redis_store.hgetall(keys)) 55 | 56 | return obj 57 | 58 | def paginate(results_list, page, per_page): 59 | count = len(results_list) 60 | 61 | start_items = (int(per_page) * int(page)) - int(per_page) 62 | end_items = start_items + int(per_page) 63 | 64 | obj = {} 65 | obj['page'] = page 66 | obj['total_count'] = count 67 | obj['results'] = results_list[start_items:end_items] 68 | 69 | return obj 70 | 71 | 72 | class Kubernetes(Resource): 73 | def get(self): 74 | q = query_all() 75 | 76 | return jsonify(paginate(q, page=request.args.get('page', 1), 77 | per_page=request.args.get('per_page', 500))) 78 | 79 | 80 | class KubernetesPop(Resource): 81 | def get(self): 82 | return jsonify(paginate(query_sort("stargazers_count"), 83 | page=request.args.get('page', 1), 84 | per_page=request.args.get('per_page', 10))) 85 | 86 | class KubernetesAct(Resource): 87 | def get(self): 88 | q = query_all() 89 | n = sorted(q, key=lambda k: k['updated_at']) 90 | 91 | return jsonify(paginate(n, 92 | page=request.args.get('page', 1), 93 | per_page=request.args.get('per_page', 10))) 94 | 95 | 96 | api.add_resource(Kubernetes, '/api/kubernetes') 97 | api.add_resource(KubernetesPop, '/api/popularity/kubernetes') 98 | api.add_resource(KubernetesAct, '/api/activity/kubernetes') 99 | 100 | if __name__ == '__main__': 101 | populate_data() 102 | app.run(host='0.0.0.0',port=5000) 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flask-api with Terraform [![Build Status](https://travis-ci.org/ember/flask-app-terraform-aws.svg?branch=master)](https://travis-ci.org/ember/flask-app-terraform-aws) 2 | 3 | ## Goal 4 | The goal is to setup a PoC with simple API rest writen in Flask and deploy the infraestructure using Terraform, Docker and using immutable deployments. 5 | 6 | ## How it works 7 | ### The application 8 | The application (app.py) is a simple REST API that queries the Github API and caches the result that can be accessable via endpoints. 9 | 10 | ### Endpoints 11 | * /api/kubernetes - returns 500 repositories with the topic "kubernetes", without any filter or sorting. 12 | * /api/popularity/kubernetes - returns the 500 repositories sorted by popularity (stargazers_count), showing 10 results per page. 13 | * /api/activity/kubernetes - returns the 500 repositories sorted by activity (updated_at) showing 10 results per page. 14 | 15 | All endpoints have pagination. For example /api/activity/kubernetes?page=1&per_page=200 will show 200 results. 16 | 17 | ```bash 18 | curl -sS "http://endpoint/api/popularity/kubernetes?page=1&per_page=1" | jq 19 | { 20 | "page": "1", 21 | "results": [ 22 | { 23 | "full_name": "kubernetes/kubernetes", 24 | "html_url": "https://github.com/kubernetes/kubernetes", 25 | "id": "20580498", 26 | "language": "Go", 27 | "name": "kubernetes", 28 | "pushed_at": "2018-05-22T10:09:43Z", 29 | "stargazers_count": "36608", 30 | "updated_at": "2018-05-22T10:02:44Z" 31 | } 32 | ], 33 | "total_count": 500 34 | } 35 | ``` 36 | 37 | ## How to run 38 | This assumes that you have an AWS IAM user with API access and have installed Docker Terraform and Terragrunt. 39 | 40 | All the complexity is masked with a Makefile. 41 | 42 | ## Run all the stack 43 | To create all the containers, ift and run the ansible you first need to setup the environment variables 44 | ```bash 45 | export DOCKER_USERNAME=someuser 46 | export DOCKER_PASSWORD=somepassword 47 | export AWS_ACCESS_KEY_ID=aws_key 48 | export AWS_SECRET_ACCESS_KEY=aws_secret 49 | make create-all 50 | ``` 51 | ### Run locally 52 | To try the app with redis locally you can run 53 | ```bash 54 | make run-local 55 | ``` 56 | 57 | This will run the compose file with the application and a redis instance running in a container. 58 | 59 | ### Build the container and push it 60 | For container image exists the Docker Hub public registry but it can also be easily change to use a private registry. 61 | 62 | To build the image 63 | ```bash 64 | make build-docker 65 | ```` 66 | 67 | To build and publish the image will require that you have the env vars for your registry set up. 68 | ```bash 69 | export DOCKER_USERNAME=someuser 70 | export DOCKER_PASSWORD=somepassword 71 | make publish-docker 72 | ``` 73 | 74 | This also accepts publishing tags 75 | ```bash 76 | make publish-docker APP_VERSION=2 77 | ``` 78 | 79 | ## Deploy the infrastructure 80 | This will use Terragrunt/Terraform to deploy the initial infraesctruture. Terraform will set the VPC, Public subnets, Instances in eu-west-2. 81 | 82 | ### Run Terraform 83 | To run the Terraform you will need to have the env vars for your credentials. 84 | ```bash 85 | export AWS_ACCESS_KEY_ID=aws_key 86 | export AWS_SECRET_ACCESS_KEY=aws_secret 87 | make infra 88 | ``` 89 | 90 | To see the ELB that was created you can run 91 | ```bash 92 | cd terraform && terraform output elb_address 93 | ``` 94 | 95 | ## Deploy flask-api 96 | The application will be packaged in a container and terraform will re-create a new environment for each deploy. 97 | 98 | For the Terraform you also need the AWS env vars. 99 | ```bash 100 | export AWS_ACCESS_KEY_ID=aws_key 101 | export AWS_SECRET_ACCESS_KEY=aws_secret 102 | make deploy 103 | ``` 104 | 105 | To deploy specific versions of the application and assuming you already publish the new tag into the Docker Registry you can: 106 | 107 | ```bash 108 | make deploy APP_VERSION=2 109 | ``` 110 | 111 | After all is working you can destroy all infrastructure with 112 | ```bash 113 | make destroy 114 | ``` 115 | 116 | 117 | ## Continuous deployment 118 | This can be used easily in a CI/CD you can see a working example using Travis in .travis.yml. 119 | 120 | --------------------------------------------------------------------------------