From 3b18a9193d06f23e6ff0ad6a2d5458fdc914589c Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Tue, 28 Jul 2020 23:01:23 +0100 Subject: [PATCH] Adding terraform --- .gitignore | 39 +++++ infrastructure/LICENSE | 19 +++ infrastructure/Makefile | 215 +++++++++++++++++++++++++++ infrastructure/Makefile.env | 4 + infrastructure/README.md | 11 ++ infrastructure/main.tf | 64 ++++++++ infrastructure/outputs.tf | 0 infrastructure/prod-eu-west-1.tfvars | 5 + infrastructure/variables.tf | 15 ++ 9 files changed, 372 insertions(+) create mode 100644 infrastructure/LICENSE create mode 100644 infrastructure/Makefile create mode 100644 infrastructure/Makefile.env create mode 100644 infrastructure/README.md create mode 100644 infrastructure/main.tf create mode 100644 infrastructure/outputs.tf create mode 100644 infrastructure/prod-eu-west-1.tfvars create mode 100644 infrastructure/variables.tf diff --git a/.gitignore b/.gitignore index 778b4b8..e4a4c2f 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,42 @@ build .elasticbeanstalk/* !.elasticbeanstalk/*.cfg.yml !.elasticbeanstalk/*.global.yml + +############################ +# Terraform +############################ + +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log + +# Exclude all .tfvars files, which are likely to contain sentitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +# +# *.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc diff --git a/infrastructure/LICENSE b/infrastructure/LICENSE new file mode 100644 index 0000000..204b93d --- /dev/null +++ b/infrastructure/LICENSE @@ -0,0 +1,19 @@ +MIT License Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/infrastructure/Makefile b/infrastructure/Makefile new file mode 100644 index 0000000..41847f5 --- /dev/null +++ b/infrastructure/Makefile @@ -0,0 +1,215 @@ +# Copyright 2016 Philip G. Porada +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +.ONESHELL: +.SHELL := /usr/bin/bash +.PHONY: apply destroy-backend destroy destroy-target plan-destroy plan plan-target prep + +-include Makefile.env +# VARS="variables/$(ENV)-$(REGION).tfvars" +VARS="$(ENV)-$(REGION).tfvars" +CURRENT_FOLDER=$(shell basename "$$(pwd)") +S3_BUCKET="$(ENV)-$(REGION)-$(PROJECT)-terraform" +DYNAMODB_TABLE="$(ENV)-$(REGION)-$(PROJECT)-terraform" +WORKSPACE="$(ENV)-$(REGION)" +BOLD=$(shell tput bold) +RED=$(shell tput setaf 1) +GREEN=$(shell tput setaf 2) +YELLOW=$(shell tput setaf 3) +RESET=$(shell tput sgr0) + +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +set-env: + @if [ -z $(ENV) ]; then \ + echo "$(BOLD)$(RED)ENV was not set$(RESET)"; \ + ERROR=1; \ + fi + @if [ -z $(REGION) ]; then \ + echo "$(BOLD)$(RED)REGION was not set$(RESET)"; \ + ERROR=1; \ + fi + @if [ -z $(AWS_PROFILE) ]; then \ + echo "$(BOLD)$(RED)AWS_PROFILE was not set.$(RESET)"; \ + ERROR=1; \ + fi + @if [ ! -z $${ERROR} ] && [ $${ERROR} -eq 1 ]; then \ + echo "$(BOLD)Example usage: \`AWS_PROFILE=whatever ENV=demo REGION=us-east-2 make plan\`$(RESET)"; \ + exit 1; \ + fi + @if [ ! -f "$(VARS)" ]; then \ + echo "$(BOLD)$(RED)Could not find variables file: $(VARS)$(RESET)"; \ + exit 1; \ + fi + +prep: set-env ## Prepare a new workspace (environment) if needed, configure the tfstate backend, update any modules, and switch to the workspace + @echo "$(BOLD)Verifying that the S3 bucket $(S3_BUCKET) for remote state exists$(RESET)" + @if ! aws --profile $(AWS_PROFILE) s3api head-bucket --region $(REGION) --bucket $(S3_BUCKET) > /dev/null 2>&1 ; then \ + echo "$(BOLD)S3 bucket $(S3_BUCKET) was not found, creating new bucket with versioning enabled to store tfstate$(RESET)"; \ + aws --profile $(AWS_PROFILE) s3api create-bucket \ + --bucket $(S3_BUCKET) \ + --acl private \ + --region $(REGION) \ + --create-bucket-configuration LocationConstraint=$(REGION) > /dev/null 2>&1 ; \ + aws --profile $(AWS_PROFILE) s3api put-bucket-versioning \ + --bucket $(S3_BUCKET) \ + --versioning-configuration Status=Enabled > /dev/null 2>&1 ; \ + echo "$(BOLD)$(GREEN)S3 bucket $(S3_BUCKET) created$(RESET)"; \ + else + echo "$(BOLD)$(GREEN)S3 bucket $(S3_BUCKET) exists$(RESET)"; \ + fi + @echo "$(BOLD)Verifying that the DynamoDB table exists for remote state locking$(RESET)" + @if ! aws --profile $(AWS_PROFILE) --region $(REGION) dynamodb describe-table --table-name $(DYNAMODB_TABLE) > /dev/null 2>&1 ; then \ + echo "$(BOLD)DynamoDB table $(DYNAMODB_TABLE) was not found, creating new DynamoDB table to maintain locks$(RESET)"; \ + aws --profile $(AWS_PROFILE) dynamodb create-table \ + --region $(REGION) \ + --table-name $(DYNAMODB_TABLE) \ + --attribute-definitions AttributeName=LockID,AttributeType=S \ + --key-schema AttributeName=LockID,KeyType=HASH \ + --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 > /dev/null 2>&1 ; \ + echo "$(BOLD)$(GREEN)DynamoDB table $(DYNAMODB_TABLE) created$(RESET)"; \ + echo "Sleeping for 10 seconds to allow DynamoDB state to propagate through AWS"; \ + sleep 10; \ + else + echo "$(BOLD)$(GREEN)DynamoDB Table $(DYNAMODB_TABLE) exists$(RESET)"; \ + fi + @aws ec2 --profile=$(AWS_PROFILE) describe-key-pairs | jq -r '.KeyPairs[].KeyName' | grep "$(ENV)_infra_key" > /dev/null 2>&1; \ + if [ $$? -ne 0 ]; then \ + echo "$(BOLD)$(RED)EC2 Key Pair $(INFRA_KEY)_infra_key was not found$(RESET)"; \ + read -p '$(BOLD)Do you want to generate a new keypair? [y/Y]: $(RESET)' ANSWER && \ + if [ "$${ANSWER}" == "y" ] || [ "$${ANSWER}" == "Y" ]; then \ + mkdir -p ~/.ssh; \ + ssh-keygen -t rsa -b 4096 -N '' -f ~/.ssh/$(ENV)_infra_key; \ + aws ec2 --profile=$(AWS_PROFILE) import-key-pair --key-name "$(ENV)_infra_key" --public-key-material "file://~/.ssh/$(ENV)_infra_key.pub"; \ + fi; \ + else \ + echo "$(BOLD)$(GREEN)EC2 Key Pair $(ENV)_infra_key exists$(RESET)";\ + fi + @echo "$(BOLD)Configuring the terraform backend$(RESET)" + @terraform init \ + -input=false \ + -force-copy \ + -lock=true \ + -upgrade \ + -verify-plugins=true \ + -backend=true \ + -backend-config="profile=$(AWS_PROFILE)" \ + -backend-config="region=$(REGION)" \ + -backend-config="bucket=$(S3_BUCKET)" \ + -backend-config="key=$(ENV)/$(CURRENT_FOLDER)/terraform.tfstate" \ + -backend-config="dynamodb_table=$(DYNAMODB_TABLE)"\ + -backend-config="acl=private" + @echo "$(BOLD)Switching to workspace $(WORKSPACE)$(RESET)" + @terraform workspace select $(WORKSPACE) || terraform workspace new $(WORKSPACE) + +plan: prep ## Show what terraform thinks it will do + @terraform plan \ + -lock=true \ + -input=false \ + -refresh=true \ + -var-file="$(VARS)" + +format: prep ## Rewrites all Terraform configuration files to a canonical format. + @terraform fmt \ + -write=true \ + -recursive + +# https://github.com/terraform-linters/tflint +lint: prep ## Check for possible errors, best practices, etc in current directory! + @tflint + +# https://github.com/liamg/tfsec +check-security: prep ## Static analysis of your terraform templates to spot potential security issues. + @tfsec . + +documentation: prep ## Generate README.md for a module + @terraform-docs \ + markdown table \ + --sort-by-required . > README.md + +plan-target: prep ## Shows what a plan looks like for applying a specific resource + @echo "$(YELLOW)$(BOLD)[INFO] $(RESET)"; echo "Example to type for the following question: module.rds.aws_route53_record.rds-master" + @read -p "PLAN target: " DATA && \ + terraform plan \ + -lock=true \ + -input=true \ + -refresh=true \ + -var-file="$(VARS)" \ + -target=$$DATA + +plan-destroy: prep ## Creates a destruction plan. + @terraform plan \ + -input=false \ + -refresh=true \ + -destroy \ + -var-file="$(VARS)" + +apply: prep ## Have terraform do the things. This will cost money. + @terraform apply \ + -lock=true \ + -input=false \ + -refresh=true \ + -var-file="$(VARS)" + +destroy: prep ## Destroy the things + @terraform destroy \ + -lock=true \ + -input=false \ + -refresh=true \ + -var-file="$(VARS)" + +destroy-target: prep ## Destroy a specific resource. Caution though, this destroys chained resources. + @echo "$(YELLOW)$(BOLD)[INFO] Specifically destroy a piece of Terraform data.$(RESET)"; echo "Example to type for the following question: module.rds.aws_route53_record.rds-master" + @read -p "Destroy target: " DATA && \ + terraform destroy \ + -lock=true \ + -input=false \ + -refresh=true \ + -var-file=$(VARS) \ + -target=$$DATA + +destroy-backend: ## Destroy S3 bucket and DynamoDB table + @if ! aws --profile $(AWS_PROFILE) dynamodb delete-table \ + --region $(REGION) \ + --table-name $(DYNAMODB_TABLE) > /dev/null 2>&1 ; then \ + echo "$(BOLD)$(RED)Unable to delete DynamoDB table $(DYNAMODB_TABLE)$(RESET)"; \ + else + echo "$(BOLD)$(RED)DynamoDB table $(DYNAMODB_TABLE) does not exist.$(RESET)"; \ + fi + @if ! aws --profile $(AWS_PROFILE) s3api delete-objects \ + --region $(REGION) \ + --bucket $(S3_BUCKET) \ + --delete "$$(aws --profile $(AWS_PROFILE) s3api list-object-versions \ + --region $(REGION) \ + --bucket $(S3_BUCKET) \ + --output=json \ + --query='{Objects: Versions[].{Key:Key,VersionId:VersionId}}')" > /dev/null 2>&1 ; then \ + echo "$(BOLD)$(RED)Unable to delete objects in S3 bucket $(S3_BUCKET)$(RESET)"; \ + fi + @if ! aws --profile $(AWS_PROFILE) s3api delete-objects \ + --region $(REGION) \ + --bucket $(S3_BUCKET) \ + --delete "$$(aws --profile $(AWS_PROFILE) s3api list-object-versions \ + --region $(REGION) \ + --bucket $(S3_BUCKET) \ + --output=json \ + --query='{Objects: DeleteMarkers[].{Key:Key,VersionId:VersionId}}')" > /dev/null 2>&1 ; then \ + echo "$(BOLD)$(RED)Unable to delete markers in S3 bucket $(S3_BUCKET)$(RESET)"; \ + fi + @if ! aws --profile $(AWS_PROFILE) s3api delete-bucket \ + --region $(REGION) \ + --bucket $(S3_BUCKET) > /dev/null 2>&1 ; then \ + echo "$(BOLD)$(RED)Unable to delete S3 bucket $(S3_BUCKET) itself$(RESET)"; \ + fi diff --git a/infrastructure/Makefile.env b/infrastructure/Makefile.env new file mode 100644 index 0000000..081352a --- /dev/null +++ b/infrastructure/Makefile.env @@ -0,0 +1,4 @@ +ENV="prod" +REGION="eu-west-1" +PROJECT="strapi-elb" +AWS_PROFILE="admin" diff --git a/infrastructure/README.md b/infrastructure/README.md new file mode 100644 index 0000000..efa691d --- /dev/null +++ b/infrastructure/README.md @@ -0,0 +1,11 @@ +# terraform + +Boilerplate for TF + +Usage: + +- Clone into a project at root level. +- Rename `./terraform` to `infrastructure` (if needed). +- Delete `./infrastructure/.git/` and `./infrastructure/.gitignore` + +Commit to project. \ No newline at end of file diff --git a/infrastructure/main.tf b/infrastructure/main.tf new file mode 100644 index 0000000..55d15c2 --- /dev/null +++ b/infrastructure/main.tf @@ -0,0 +1,64 @@ +# aws config +provider "aws" { + region = var.region + profile = var.profile + version = "~> 2.70.0" +} + +# tags +locals { + tags = { + "Project" = "strapi-elb" + "Description" = "Terraform resources for strapi in Elastic Beanstalk" + } +} + +# Network + +module "vpc" { + source = "git::https://github.com/cloudposse/terraform-aws-vpc?ref=tags/0.14.0" + stage = var.stage + name = var.name + + cidr_block = "172.16.0.0/16" + enable_default_security_group_with_custom_rules = false +} + +module "subnets" { + source = "git::https://github.com/cloudposse/terraform-aws-dynamic-subnets?ref=tags/0.23.0" + stage = var.stage + name = var.name + + availability_zones = ["eu-west-1a", "eu-west-1b", "eu-west-1c"] + vpc_id = module.vpc.vpc_id + igw_id = module.vpc.igw_id + cidr_block = module.vpc.vpc_cidr_block + nat_gateway_enabled = false + nat_instance_enabled = false +} + +# RDS instance + +module "rds_instance" { + source = "git::https://github.com/cloudposse/terraform-aws-rds.git?ref=tags/0.20.0" + stage = var.stage + name = var.name + + allocated_storage = 5 + database_name = "postgres" + database_user = "mainuser" + database_password = "password" + database_port = 5432 + db_parameter_group = "postgres12" + engine = "postgres" + engine_version = "12.3" + instance_class = "db.t2.micro" + subnet_ids = module.subnets.public_subnet_ids + vpc_id = module.vpc.vpc_id + publicly_accessible = true + tags = local.tags +} + +# Set maintenance window +# subnet_ids and vpc_id required +# need a security group for the DB with ingress rule allowing inbound from the autoscaler/EB security group (does a single instance have an SC?) - use 0.0.0.0 for initial creation then change the TF stack with the EB security group once it's created. diff --git a/infrastructure/outputs.tf b/infrastructure/outputs.tf new file mode 100644 index 0000000..e69de29 diff --git a/infrastructure/prod-eu-west-1.tfvars b/infrastructure/prod-eu-west-1.tfvars new file mode 100644 index 0000000..5d72c46 --- /dev/null +++ b/infrastructure/prod-eu-west-1.tfvars @@ -0,0 +1,5 @@ +# module +name = "strapi-elb" +region = "eu-west-1" +stage = "prod" +profile = "admin" diff --git a/infrastructure/variables.tf b/infrastructure/variables.tf new file mode 100644 index 0000000..c47410c --- /dev/null +++ b/infrastructure/variables.tf @@ -0,0 +1,15 @@ +variable "name" { + +} + +variable "region" { + +} + +variable "stage" { + +} + +variable "profile" { + +}