diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..066b8c38 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,125 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Mac stupid DS_Store files +*.DS_Store + +# terraform +terraform/ +.idea +.terraform +*.plan +*.out + +# Docker +Dockerfile +docker-compose.yml +docker/ + +# docs +docs/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index ce1d6f9d..ff84b186 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,9 @@ dist # Mac stupid DS_Store files *.DS_Store + +# terraform +.idea +.terraform +*.plan +*.out \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..175af7e7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +- repo: git://github.com/antonbabenko/pre-commit-terraform + rev: v1.27.0 # Get the latest from: https://github.com/antonbabenko/pre-commit-terraform/releases + hooks: + - id: terraform_fmt + - id: terraform_docs diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 00000000..43cef097 --- /dev/null +++ b/bin/README.md @@ -0,0 +1,199 @@ +# Deployment Scripts + +## Overview + +The following scripts are intended to be used for the deployment of the application infrastructure. They have been developed with ease-of-use in mind and, as such, potential input to the script runtime has been significantly restricted. + +The scripts in this repository can be divided into two categories: **Invoked Scripts** which represent scripts that a developer would call to launch and administer the application infrastructure, and **Utility Scripts** which are leveraged by the Invoked Scripts to perform common tasks as needed. + +## Invoked Scripts + +The following scripts are used to control the lifecycle of the application infrastructure. For detailed information about how to launch the infrastructure, please see the [Deployment Documentation](../DEPLOYING.md) + +**All invoked scripts should be run from the root of this repository.** + +### deploy + +##### Usage + + ```bin/deploy.sh arn:aws:iam:::mfa/``` + +The addition of an MFA resource is optional and only required if you are using MFA on your AWS account. If this value is not included, the script will not attempt to generate multi-factor credentials. + +##### Description +The deploy script is the main driver for deployment. The usage of the script is described in [DEPLOYING.md](../DEPLOYING.md) and documentation on the individual steps performed in the script are detailed inline as comments. + +The most common usage scenario for the deploy script is to perform a full standup of the application infrastructure when newly launching the application or modifying the terraform component(s) that comprise the application runtime environment. + +The deploy script completes the following steps: +- Create the Build Agent Docker image (See the [Docker Documentation](../docker/README.md) for more information) +- Ensure that the AWS is configured correctly and attempt an MFA sign-in if the user has passed an MFA device in. +- Builds the API code. +- Creates the Terraform Bucket to hold infrastructure state. (Note: If the bucket exists already, this step does nothing.) +- Runs `terraform apply` to generate the application infrastructure +- Logs into the Amazon Container Registry +- Builds and pushes Docker image for the bmore-responsive api. +- Outputs the S3 Bucket and API addresses for use in interfacing with the APIs +- Cleans up Docker containers that were created as a result of the build process + +In the event that any of the above steps return a nonzero error code, the deploy script halts. + +### redeploy + +##### Usage + +`bin/redeploy.sh` + +##### Description + +The redeploy script is intended for use when any application code for the API or Docker image is modified. When this happens, developers are able to run this script which performs the following actions: + +- Rebuilds the Docker Build Agent +- Ensures that AWS is configured correctly +- Builds the API +- Logs into the Amazon Container Registry +- Builds and pushes Docker images for the bmore-responsive api. +- Triggers a container redeployment through the AWS CLI +- Cleans up Docker containers that were created as a result of the build process + +Once the redeploy script is run, the bmore-responsive containers will go through the standard AWS rolling redeployment process. In this process, existing containers are kept running while new ones are created and registered as healthy. If the new containers are successfully marked healthy, old containers are drained and removed. This process takes about 5 minutes on average. + +The redeploy script is only intended for the redeployment of API code. In the event of a change to any terraform component(s), the `deploy.sh` script should be used to ensure that changes are applied correctly and propagated to all infrastructure components. + +**Please note** that the redeploy script does not attempt to perform the MFA login again. It assumes that you have a valid AWS session available when trying to use the script. In the event that this is not the case, the `bin/mfa.sh` script has been supplied to allow multi-factor login. + +### cleanup + +##### Usage + +##### Description + +The cleanup script is intended to be used when the application infrastructure is no longer desired. The script will destroy all terraform resources, clear the S3 buckets, and delete the terraform state bucket. **Running this script to completion will render your infrastructure unrecoverable without a full run of `deploy.sh`** + +The cleanup script performs the following steps: + +- Rebuilds the Docker Build Agent +- Ensures that AWS is configured correctly +- Runs `terraform destroy` **Note:** The developer running this script will be prompted to accept the destruction of the environment. +- Empties the terraform state bucket in use by the infrastructure +- Deletes the terraform state bucket in use by the infrastructure +- Cleans up Docker containers that were created as a result of the build process + +### mfa + +##### Usage + +```bin/mfa.sh arn:aws:iam:::mfa/``` + +The addition of an MFA resource is required. If this value is not included, the script will not do anything. + +##### Description + +This script is a simple wrapper to enable developers to log into AWS MFA for use in the `redeploy.sh` and `cleanup.sh` scripts. It should only be used in the event that a user has not run the `deploy.sh` script recently (or at all) or if their AWS credentials have expired after the given period (36 hours). The script performs the following actions: + +- Rebuilds the Docker Build Agent +- Ensures that AWS is configured correctly +- Attempts AWS MFA sign in with the given user credentials +- Cleans up Docker containers that were created as a result of the build process + +### Notes + +- Every script used for major operations rebuilds the Docker Build Agent. This is to ensure that the agent is at its latest state if changes have occurred between the latest container build and the latest source code updates. + +## Utility Scripts + +Utility scripts are simple shell scripts designed to be run by the Docker Build Agent when performing application infrastructure administration tasks. They are largely simple scripts designed to strip some complexity from the Invoked Scripts and make them more readable. + +**Note:** Most of these scripts require an active AWS session in order to run correctly. Please view the documentation above for `mfa.sh` or `deploy.sh` for more information. + +### apply + +##### Usage + +`apply ` + +- Terraform Component is the name of the terraform component being deployed. The script will cd to that component's folder and run all build actions from there. +- Extra Args are any other arguments that are normally passed to `terraform apply`. A complete list can be found at the [Terraform Docs](https://www.terraform.io/docs/commands/apply.html) + +##### Description + +The apply script is designed to be used when Invoked Scripts require terraform to deploy new resources. Apply performs three actions: + +- Enters the correct directory for the specified terraform component +- Initializes terraform +- Runs `terraform apply` with any additional arguments + + +### create-bucket + +##### Usage + +`create-bucket` + +##### Description + +This script creates the S3 bucket required for maintaining terraform state. Currently, the target bucket name is hard-coded to `cfb-healthcare-rollcall-terraform-state`. The bucket is created in the `us-east-1` region by default. + +### destroy + +##### Usage + +`destroy ` + +- Terraform Component is the name of the terraform component being deployed. The script will cd to that component's folder and run all build actions from there. +- Extra Args are any other arguments that are normally passed to `terraform destroy`. A complete list can be found at the [Terraform Docs](https://www.terraform.io/docs/commands/destroy.html) + +##### Description + +The destroy script is designed to be used when Invoked Scripts require terraform to destroy resources. The script performs three actions: + +- Enters the correct directory for the specified terraform component +- Initializes terraform +- Runs `terraform destroy` with any additional arguments + +**Note:** Unless `--force` is specified as an extra argument, invoking the `destroy` script will prompt the user for confirmation before performing any actions. + +### ecr-login + +##### Usage + +`ecr-login` + +This script is commonly used, however, to invoke a docker login command as such: + +`$(ecr-login)` + +##### Description + +The `ecr-login` script is a simple helper script to wrap the action of logging into the Amazon Elastic Container Registry for your AWS account. This is a necessary step before the execution of any `docker push` activities to the remote AWS Container Registry. + +### npm-build + +##### Usage + +`npm-build ` + +##### Description + +- installs dependencies with `npm install` +- tests code with 'npm test' +- lints code with 'npm lint' + +### output + +##### Usage + +`output ` + +- Terraform Component is the name of the terraform component being deployed. The script will cd to that component's folder and run all build actions from there. +- Extra Args are any other arguments that are normally passed to `terraform output`. A complete list can be found at the [Terraform Docs](https://www.terraform.io/docs/commands/output.html) + +##### Description + +The output script is designed to be used when Invoked Scripts require access to any of the outputs from the terraform component. The script performs three actions: + +- Enters the correct directory for the specified terraform component +- Initializes terraform +- Runs `terraform output` with any additional arguments + +This script is primarily used to pass information about terraform-created resources to other commands in subsequent steps of Invoked Scripts. Additionally, it is used to output information to developers and end-users of the deployment script(s). diff --git a/bin/cleanup.sh b/bin/cleanup.sh new file mode 100755 index 00000000..da07d63e --- /dev/null +++ b/bin/cleanup.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Exit on all nonzero exit codes. +set -e + +# Create the builder agent to help us with the rest of the build process +# We run this on all scripts to make sure that the builder is always up-to-date. +docker build -f docker/Dockerfile-Builder -t cfb-build-agent . + +# Set AWS Profile to default +export AWS_PROFILE="default" + +# Terraform Destroy all of our resources +# We still want to prompt the user for confirmation, especially with an action like this. +docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent destroy full-cluster + +# Empty out the Terraform State Bucket +docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent aws s3 rm s3://cfb-healthcare-rollcall-terraform-state --recursive + +# Delete the Terraform State Bucket +docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent aws s3api delete-bucket --bucket cfb-healthcare-rollcall-terraform-state --region us-east-1 + +# Clean up the stopped build agent containers +docker rm $(docker ps -a -q --filter ancestor=cfb-build-agent) > /dev/null 2>&1 diff --git a/bin/deploy.sh b/bin/deploy.sh new file mode 100755 index 00000000..4ebcaa34 --- /dev/null +++ b/bin/deploy.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# Close the whole script after a nonzero exit code +set -e + +# Create the builder agent to help us with the rest of the build process +docker build -f docker/Dockerfile-Builder -t cfb-build-agent . + +# Set AWS Profile to default +export AWS_PROFILE="default" + +# Login to MFA only if the end user has passed in MFA credentials +if [ "$#" -gt 0 ] + then + # We have entered MFA Parameters + docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent aws-mfa --duration 129600 --device $1 +fi + +# Build and test the cfb healthcare rollcall api +# We mount the current directory into /app/ so the agent can see all code and scripts. +# docker run -it cfb-build-agent ./npm-build + + +# Run Terraform Apply to create the infrastructure +# We still want the end-user to have to accept the Terraform plan before executing. +docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent apply full-cluster + +# Get DB outputs +DB_NAME=$(docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent output full-cluster db_instance_name | tr -d '\r') +echo "Db name -> $DB_NAME" + +DB_PORT=$(docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent output full-cluster db_instance_port | tr -d '\r') +echo "Db port -> $DB_PORT" + +DB_USERNAME=$(docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent output full-cluster db_instance_username | tr -d '\r') +echo "Db username -> $DB_USERNAME" + +DB_PASSWORD=$(docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent output full-cluster db_instance_password | tr -d '\r') +echo "Db password -> $DB_PASSWORD" + +DB_ENDPOINT=$(docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent output full-cluster db_instance_endpoint | tr -d '\r') +echo "Db endpoint -> $DB_ENDPOINT" + +DB_ADDRESS=$(docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent output full-cluster db_instance_address | tr -d '\r') +echo "Db address -> $DB_ADDRESS" + +DB_URL="postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_ENDPOINT}:${DB_PORT}/${DB_NAME}" +echo "DB URL -> $DB_URL" +### Building and Pushing Docker Images ### +docker run -it -v $(pwd):/app/ cfb-build-agent ./db-create +# Log into the ECS Repository first +$(docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent ecr-login | tr -d '\r') + +# Build the container image for the API +docker build -f docker/Dockerfile-Bmore-Responsive -t bmore-responsive . +# Get the address of the repository in AWS +CFB_REPO=$(docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent output full-cluster bmore-responsive_registry | tr -d '\r') +# Tag the image for pushing +docker tag bmore-responsive $CFB_REPO:latest +# Push the new docker image +docker push $CFB_REPO + +### Get important outputs for the end-user ### +S3_BUCKET=$(docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent output full-cluster output_bucket_name | tr -d '\r') +echo "Output Bucket -> $S3_BUCKET" + +LB_DNS=$(docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent output full-cluster load_balancer_address | tr -d '\r') +echo "API URL -> http://${LB_DNS}/" + +# Clean up the stopped build agent containers +docker rm $(docker ps -a -q --filter ancestor=cfb-build-agent) > /dev/null 2>&1 diff --git a/bin/mfa.sh b/bin/mfa.sh new file mode 100755 index 00000000..b25f628e --- /dev/null +++ b/bin/mfa.sh @@ -0,0 +1,26 @@ +#! /bin/bash + +# Exit on all nonzero exit codes. +set -e + +# Set AWS Profile to default +export AWS_PROFILE="default" + +# Create the builder agent to help us with the rest of the build process +# We run this on all scripts to make sure that the builder is always up-to-date. +docker build -f docker/Dockerfile-Builder -t cfb-build-agent . + +# Login to MFA only if the end user has passed in MFA credentials +# If they have not, then there is nothing to do here. +if [ "$#" -gt 0 ] + then + # We have entered MFA Parameters + docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent aws-mfa --duration 129600 --device $1 + else + # No MFA Serial Number has been entered + echo "No MFA Serial Number entered. Command usage: bin/mfa.sh " + echo "Please view DEPLOYING.md to get information about how to find your MFA Serial Number." +fi + +# Clean up the stopped build agent containers +docker rm $(docker ps -a -q --filter ancestor=cfb-build-agent) > /dev/null 2>&1 diff --git a/bin/redeploy.sh b/bin/redeploy.sh new file mode 100755 index 00000000..d65ea7e0 --- /dev/null +++ b/bin/redeploy.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Exit on all nonzero exit codes. +set -e + +# Set AWS Profile to default +export AWS_PROFILE="default" + +# Create the builder agent to help us with the rest of the build process +# We run this on all scripts to make sure that the builder is always up-to-date. +docker build -f docker/Dockerfile-Builder -t cfb-build-agent . + + +# Rebuild the Java Projects +#docker run -it -v $(pwd):/app/ cfb-build-agent npm-build + + +### Building and Pushing Docker Images ### +# Log into the ECS Repository first +$(docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent ecr-login | tr -d '\r') + +# Build the container image +docker build -f docker/Dockerfile-Bmore-Responsive -t bmore-responsive . +# Get the address of the repository in AWS +CFB_REPO=$(docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent output full-cluster bmore-responsive_registry | tr -d '\r') +echo "CFB_REPO -> $CFB_REPO" + +# Tag the image for pushing +docker tag bmore-responsive:latest $CFB_REPO:latest +# Push the new docker image +docker push $CFB_REPO + + +# Trigger AWS to Redeploy the running containers. +docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE cfb-build-agent aws ecs update-service --service bmore-responsive --cluster bmore-responsive-cluster --region us-east-1 --force-new-deployment + + +# Clean up the stopped build agent containers +docker rm $(docker ps -a -q --filter ancestor=cfb-build-agent) > /dev/null 2>&1 diff --git a/bin/util/apply b/bin/util/apply new file mode 100755 index 00000000..c4b22d04 --- /dev/null +++ b/bin/util/apply @@ -0,0 +1,5 @@ +#!/bin/bash + +cd terraform/components/$1 +terraform init +terraform apply ${@:2} diff --git a/bin/util/create-bucket b/bin/util/create-bucket new file mode 100755 index 00000000..53c36d40 --- /dev/null +++ b/bin/util/create-bucket @@ -0,0 +1,3 @@ +#!/bin/bash + +aws s3api create-bucket --bucket cfb-healthcare-rollcall-terraform-state --region us-east-1 diff --git a/bin/util/db-create b/bin/util/db-create new file mode 100755 index 00000000..3d66b3cf --- /dev/null +++ b/bin/util/db-create @@ -0,0 +1,3 @@ +#!/bin/bash + +npm run db-create \ No newline at end of file diff --git a/bin/util/destroy b/bin/util/destroy new file mode 100755 index 00000000..cb6e65c8 --- /dev/null +++ b/bin/util/destroy @@ -0,0 +1,5 @@ +#!/bin/bash + +cd terraform/components/$1 +terraform init +terraform destroy ${@:2} diff --git a/bin/util/ecr-login b/bin/util/ecr-login new file mode 100755 index 00000000..503b26a0 --- /dev/null +++ b/bin/util/ecr-login @@ -0,0 +1,2 @@ +#!/bin/bash +aws ecr get-login --no-include-email --region us-east-1 diff --git a/bin/util/npm-build b/bin/util/npm-build new file mode 100755 index 00000000..0267110e --- /dev/null +++ b/bin/util/npm-build @@ -0,0 +1,6 @@ +#!/bin/bash + +npm install +npm test +npm run lint + diff --git a/bin/util/output b/bin/util/output new file mode 100755 index 00000000..edd585ca --- /dev/null +++ b/bin/util/output @@ -0,0 +1,5 @@ +#!/bin/bash + +cd terraform/components/$1 +terraform init > /dev/null 2>&1 +terraform output ${@:2} diff --git a/docker-compose.yml b/docker-compose.yml index b467e576..957f59ea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,13 @@ version: '3' services: api: - build: . + image: 180104022864.dkr.ecr.us-east-2.amazonaws.com/bmore-responsive depends_on: - db links: - "db: database" ports: - - '3000:3000' + - '8080:80' command: > sh -c "npm run db-delete && npm run db-create && diff --git a/docker/Dockerfile-Bmore-Responsive b/docker/Dockerfile-Bmore-Responsive new file mode 100644 index 00000000..a1f3ae01 --- /dev/null +++ b/docker/Dockerfile-Bmore-Responsive @@ -0,0 +1,23 @@ +# Build from Node Alpine image +FROM node:12.11.0-alpine + +# Create app directory +RUN mkdir -p /usr/src +WORKDIR /usr/src + +# Install app dependencies +COPY ./package*.json ./ +RUN npm install + +# Bundle app source +COPY . . + + +# Expose port (will not be respected by Heroku, must be defined in app) +EXPOSE $port + +ARG DB_URL +ENV DB_URL=$DB_URL + +# Run app +CMD ["npm","start"] diff --git a/docker/Dockerfile-Builder b/docker/Dockerfile-Builder new file mode 100644 index 00000000..9ae8c834 --- /dev/null +++ b/docker/Dockerfile-Builder @@ -0,0 +1,23 @@ +FROM maven:3.6.0-jdk-11-slim + +# Download package dependencies +RUN apt-get update && apt-get install wget python3-pip -y + +# Download Terraform and make it executable. +WORKDIR /tmp/ +RUN wget -O terraform.zip https://releases.hashicorp.com/terraform/0.12.24/terraform_0.12.24_linux_amd64.zip +RUN unzip terraform.zip +RUN mv terraform /usr/bin/terraform + +# Install the AWS CLI +RUN pip3 install awscli --upgrade + +# Install the AWS MFA Tool +RUN pip3 install aws-mfa + +# Add Executables from docker/bin folder to PATH +ENV PATH "$PATH:/app/bin/util" + +# Create the directory for volume mounting +RUN mkdir /app/ +WORKDIR /app/ \ No newline at end of file diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..e01ab9a8 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,85 @@ +# Docker Images + +## Docker Build Agent + +### Overview +The Docker Build Agent is a tool designed to satisfy the deployment dependencies for the Provider Matching and FQHC Pricer APIs. A common task, when modernizing systems and deploying resources to new environments such as AWS, is the installation of tools such as Terraform, Python, the AWS CLI, and Java. The introduction of these installation requirements and instructions leaves extensive room for human error or accidental misconfiguration as the result of unnoticed environmental differences. + +In order to solve this problem, the Docker Build Agent was developed in order to include all dependencies needed at known versions and configurations. All deployment scripts (as detailed in the [Deployment Script Documentation](../bin/README.md)) utilize this image to ensure that any development or evaluation machine is running the exact same version of every necessary tool with exactly the same configuration. As a direct result, developers interfacing with this repository only require Docker installed on their system, eliminating several steps in the onboarding process for new team members or modernization efforts. + +### Included Software + +The Build Agent includes the following software for development and deployment activities: + +- Java +- Apache Maven +- Terraform +- Python & pip +- AWS CLI & MFA Tools + +### Building the Agent + +The Build Agent is built during the normal operation of the `deploy`, `redeploy`, `cleanup`, and `mfa` scripts. This should ensure that the agent is always at the latest state when running infrastructure administration scripts. In the event that a build needs to be done outside of those scripts, the following command can be used: + +`docker build -f docker/Dockerfile-Builder -t mpsm-build-agent . ` + +This command assumes that you are running the build command from the root directory of the repository. If you are running it from any other directory, modify the `-f` statement with the relative path to the Dockerfile-Builder file. + +### Running the Agent + +As with the build process, the agent is most commonly run when using the Deployment Scripts in the `bin` folder. In the event, however, that the agent has to be run outside these scripts, the following command can be run: + +`docker run -it -v $(pwd):/app/ -v $(pwd)/docker/aws/:/root/.aws/ -e AWS_PROFILE=$AWS_PROFILE mpsm-build-agent ` + +Breaking down the command above, we get the following: +- `docker run -it` launches the container and enables it to log output to the console window for monitoring and accept user input as needed. +- `-v $(pwd):/app/` mounts the entire working directory to the `/app/` folder on the container. This enables the container to see all files in the project and interact with them. **Note:** Anything the container does to files in `/app/` happens on the host OS as well. Be careful with what you mount. +- `-v $(pwd)/docker/aws/:/root/.aws` is a special volume mount that enables developers to login to the AWS CLI with the files in `docker/aws/`. Additionally, the `mfa` script detailed in [Deployment Scripts](../bin/README.md) writes MFA credentials to this file which enables us to keep our MFA session across multiple Build Agent instances. +- `-e AWS_PROFILE=$AWS_PROFILE` sets the `AWS_PROFILE` environment variable on the container. This is needed for the AWS CLI if a value other than `default` is used. +- `mpsm-build-agent ` invokes the container image and a command to run. A List of helper scripts is found in the [Deployment Scripts](../bin/README.md) section. + +Please note that this example is for a run command launched from the root directory of the repository. + +### AWS Credentials + +In the `docker/` folder of the repository is a directory called `/aws/` which contains the files `config` and `credentials`. These files and directory represent an isolated AWS CLI credentials environment that is mounted to the Build Agent as needed for activities such as terraform invocations or AWS CLI calls. These files are passed to every running instance of the Build Agent and enable the developer to log into the AWS CLI without needing it installed on their local machine. + +**Warning:** The Credentials file has been added to source control for distribution purposes only. Please **DO NOT COMMIT ANY CHANGES TO THE CREDENTIALS FILE.** + +## Provider Matching and FQHC Pricer Containers + +### Overview +In addition to the Build Agent, there are also two files labeled `Dockerfile-Matching` and `Dockerfile-FQHC` which control the creation of Docker images for the Provider Matching and FQHC Pricer APIs respectively. These are both Java containers which have been configured to enable easy deployment of the application to any system running Docker. + +### Building the Images + +In the event that a build needs to be done outside of the `deploy` or `redeploy` scripts, the following command can be used: + +`docker build -f docker/Dockerfile-Matching -t provider-matching . ` + +or + +`docker build -f docker/Dockerfile-FQHC -t fqhc-pricer . ` + +This command assumes that you are running the build command from the root directory of the repository. If you are running it from any other directory, modify the `-f` statement with the relative path to the appropriate Dockerfile + +### Running the Images + +The application infrastructure for this challenge has been designed to deploy these containers into an AWS ECS cluster. This task is performed by the `deploy` and `redeploy` scripts. In the event that either container needs to be started locally, the following commands can be run: + +##### Provider Matching +The following command launches a daemonized version of the Provider Matching API listening on Port 8080: + +`docker run -d -e AWS_REGION=us-east-1 -e AWS_BUCKET= -v $(pwd)/docker/aws/:/root/.aws/ -p 8080:8080 provider-matching` + +- The environment variable `AWS_REGION` is set to `us-east-1` by default, however it can be changed to whichever region your S3 bucket is deployed to. +- The environment variable `AWS_BUCKET` is the address of the S3 bucket to output provider matching files to. +- The AWS environment variables are required for the container to output to the target S3 bucket after entries have been processed into .txt files. +- `-v $(pwd)/docker/aws/:/root/.aws/` enables the container to write to the target S3 bucket. **Note:** This assumes that you have a valid AWS session and permission to write to the target S3 bucket. +- `-p 8080:8080` binds local port 8080 to the internal docker port 8080. + +##### FQHC Pricer +The following command launches a daemonized version of the FQHC Pricer API listening on Port 8080: + +`docker run -d -p 8080:8080 fqhc-pricer` +- `-p 8080:8080` binds local port 8080 to the internal docker port 8080. diff --git a/docker/aws/config b/docker/aws/config new file mode 100644 index 00000000..acb21815 --- /dev/null +++ b/docker/aws/config @@ -0,0 +1,3 @@ +[default] +region=us-east-2 +output=json diff --git a/docker/aws/credentials b/docker/aws/credentials new file mode 100644 index 00000000..abba670b --- /dev/null +++ b/docker/aws/credentials @@ -0,0 +1,6 @@ +[default] + + +[default-long-term] +aws_access_key_id = +aws_secret_access_key = diff --git a/docs/swagger/.swagger-codegen-ignore b/docs/swagger/.swagger-codegen-ignore deleted file mode 100644 index c5fa491b..00000000 --- a/docs/swagger/.swagger-codegen-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# Swagger Codegen Ignore -# Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/docs/swagger/.swagger-codegen/VERSION b/docs/swagger/.swagger-codegen/VERSION deleted file mode 100644 index edd1851a..00000000 --- a/docs/swagger/.swagger-codegen/VERSION +++ /dev/null @@ -1 +0,0 @@ -3.0.18 \ No newline at end of file diff --git a/docs/swagger/README.md b/docs/swagger/README.md deleted file mode 100644 index b334639e..00000000 --- a/docs/swagger/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Swagger -To view our Swagger docs please visit [SwaggerHub](https://app.swaggerhub.com/apis/codeforbaltimore/bmoreResponsive/1.0.0#/) or http://localhost:3000 with the application running locally. \ No newline at end of file diff --git a/src/index.js b/src/index.js index 3a0ec53d..57abf6bd 100644 --- a/src/index.js +++ b/src/index.js @@ -8,7 +8,7 @@ import morgan from 'morgan'; import swaggerUi from 'swagger-ui-express'; import nunjucks from 'nunjucks'; -import swaggerDocument from '../docs/swagger/swagger.json'; +import swaggerDocument from '../swagger.json'; import models, { sequelize } from './models'; import routes from './routes'; import utils from './utils'; diff --git a/docs/swagger/swagger.json b/swagger.json similarity index 100% rename from docs/swagger/swagger.json rename to swagger.json diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 00000000..1b3e22be --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,27 @@ +# Terraform + +## Overview + +The infrastructure for this application has been designed to be launched and configured via terraform. This enables us to store the application infrastructure as code, share the infrastructure state via terraform's built-in support for AWS S3 state storage, and maintain infrastructure state easily though a series of scripts (as defined in [Deployment Scripts](../bin/README.md)). + +## Components + +### full-cluster + +The full-cluster component represents an ECS cluster running in its own, dedicated VPC. Please see the [README](components/full-cluster/README.md) for more details. + +## Modules +Each module contains its own README.md which includes information about its functionality. Please refer to individual module documentation for more information about inputs, outputs, and behaviors of each module. + +| Module | Documentation | +|-------------|------------------------------------------| +| ALB |[README.md](modules/alb/README.md) | +| ASG |[README.md](modules/asg/README.md) | +| CERTIFICATE |[README.md](modules/certificate/README.md)| +| DB |[README.md](modules/db/README.md) | +| DNS_RECORD |[README.md](modules/dns_record/README.md) | +| ECS |[README.md](modules/ecs/README.md) | +| S3 |[README.md](modules/s3/README.md) | +| SG |[README.md](modules/sg/README.md) | +| VPC |[README.md](modules/vpc/README.md) | +| WAF |[README.md](modules/waf/README.md) | diff --git a/terraform/components/full-cluster/README.md b/terraform/components/full-cluster/README.md new file mode 100644 index 00000000..99292b98 --- /dev/null +++ b/terraform/components/full-cluster/README.md @@ -0,0 +1,44 @@ +# full-cluster + +## Infrastructure Requirements + - [x] Public DNS Record + - [x] SSL/TLS termination at the ALB + - [x] IP whitelisting via WAF + - [ ] Auto db credential rotation via Lambda + + +## Providers + +| Name | Version | +|------|---------| +| aws | 2.54.0 | +| random | n/a | +| template | n/a | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:-----:| +| aws\_region | AWS Region to use. | `string` | `"us-east-2"` | no | +| create\_waf | n/a | `bool` | `false` | no | +| db\_password | n/a | `string` | n/a | yes | +| public\_hosted\_zone\_name | n/a | `any` | n/a | yes | +| waf\_whitelist\_cidrs | n/a | `list(string)` |
[
"0.0.0.0/0"
]
| no | +| zone\_id | n/a | `any` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| bmore-responsive\_registry | Name of the Bmore Response Registry | +| db\_instance\_address | The address of the RDS instance | +| db\_instance\_endpoint | The connection endpoint | +| db\_instance\_name | The database name | +| db\_instance\_password | The database password (this password may be old, because Terraform doesn't track it after initial creation) | +| db\_instance\_port | The database port | +| db\_instance\_username | The master username for the database | +| load\_balancer\_address | DNS Address of the Application Load Balancer | +| output\_bucket\_name | Name of the output s3 bucket | +| public\_hosted\_zone\_nameservers | n/a | + + \ No newline at end of file diff --git a/terraform/components/full-cluster/cfb-container.json.tpl b/terraform/components/full-cluster/cfb-container.json.tpl new file mode 100644 index 00000000..0791cbaa --- /dev/null +++ b/terraform/components/full-cluster/cfb-container.json.tpl @@ -0,0 +1,36 @@ +[ + { + "name": "bmore-responsive", + "image": "${image_address}", + "cpu": 128, + "memory": 512, + "essential": true, + "portMappings": [ + { + "containerPort": 3000, + "hostPort": 0 + } + ], + "logConfiguration": + { + "logDriver": "awslogs", + "options": + { + "awslogs-group": "cfb-api-logs", + "awslogs-region": "us-east-2", + "awslogs-stream-prefix": "cfb-api-" + } + }, + "environment": [ + { "name" : "VUE_APP_BASE_API_URL", "value" : "${vue_app_base_api_url}" }, + { "name" : "NODE_ENV", "value" : "${node_env}" }, + { "name" : "DATABASE_HOST", "value" : "${database_host}" }, + { "name" : "DATABASE_USERNAME", "value" : "${database_user}" }, + { "name" : "DATABASE_PORT", "value" : "${database_port}" }, + { "name" : "DATABASE_NAME", "value" : "${database_name}" }, + { "name" : "JWT_KEY", "value" : "${jwt_key}" }, + { "name" : "BYPASS_LOGIN", "value" : "${bypass_login}" }, + { "name" : "DATABASE_PASSWORD", "value" : "${database_password}" } + ] + } +] diff --git a/terraform/components/full-cluster/main.tf b/terraform/components/full-cluster/main.tf new file mode 100644 index 00000000..5958e6ce --- /dev/null +++ b/terraform/components/full-cluster/main.tf @@ -0,0 +1,153 @@ +terraform { + required_version = "0.12.6" + backend "s3" { + bucket = "cfb-healthcare-rollcall-us-east-2-terraform-state" + key = "cfb-healthcare-rollcall/terraform/terraform.tfstate" + region = "us-east-2" + encrypt = true + } +} + +provider "aws" { + version = "2.54.0" + region = var.aws_region +} + +# Set up output S3 Bucket +module "s3" { + source = "../../modules/s3" + resource_suffix = random_pet.random_pet.id + aws_region = var.aws_region +} + +# Set up template files + +data "template_file" "cfb_ecs_task_definition" { + template = file("cfb-container.json.tpl") + vars = { + image_address = "codeforbaltimore/bmore-responsive" + s3_bucket = module.s3.output_bucket_name + vue_app_base_api_url = "bmoreres.codeforbaltimore.org" + node_env = "development" + database_host = module.db.this_db_instance_address + database_user = module.db.this_db_instance_username + // database_password_arn = aws_secretsmanager_secret_version.db_password.arn + database_port = module.db.this_db_instance_port + database_name = "healthcareRollcallDB" + jwt_key = "abc123" + bypass_login = "false" + aws_region = var.aws_region + database_password = var.db_password + } +} + +resource "aws_secretsmanager_secret" "db_password" { + name_prefix = "db_password" + +} + +resource "aws_secretsmanager_secret_version" "db_password" { + secret_id = aws_secretsmanager_secret.db_password.id + secret_string = var.db_password +} + +data "template_file" "user_data" { + template = file("userdata.sh.tpl") + vars = { + cluster_name = "bmore-responsive-cluster" + } +} + +# Set up AWS Resources + +resource "random_pet" "random_pet" { + length = 2 +} + +module "vpc" { + source = "../../modules/vpc" + vpc_cidr = "10.0.0.0/16" +} + +module "sg" { + source = "../../modules/sg" + vpc_id = module.vpc.vpc-id + db_ingress_cidrs = module.vpc.private_subnet_cidrs +} + +data "aws_route53_zone" "hosted_zone" { + zone_id = var.zone_id +} + +module "certificate" { + source = "../../modules/certificate" + dns_zone_id = data.aws_route53_zone.hosted_zone.zone_id + domain_name = "api.${data.aws_route53_zone.hosted_zone.name}" +} + +module "dns_record" { + source = "../../modules/dns_record" + name = "api.${data.aws_route53_zone.hosted_zone.name}" + zone_id = data.aws_route53_zone.hosted_zone.zone_id + lb_dns_name = module.alb.lb-dns + lb_zone_id = module.alb.zone_id +} + +module "alb" { + source = "../../modules/alb" + vpc_id = module.vpc.vpc-id + vpc_subnets = module.vpc.public-subnet-ids + lb_sg = module.sg.alb-sg-id + cfb_app_port = 3000 + certificate_arn = module.certificate.certificate_arn +} + +module "waf" { + source = "../../modules/waf" + whitelist_cidrs = var.waf_whitelist_cidrs + lb_arn = module.alb.lb-arn + resource_suffix = random_pet.random_pet.id +} + +module "ecs_cluster" { + source = "../../modules/ecs" + cluster_name = "bmore-responsive-cluster" + output_bucket_arn = module.s3.output_bucket_arn + bmore-responsive_desired_count = "3" + bmore-responsive_target_group_arn = module.alb.tg-cfb-arn + bmore-responsive_container_name = "bmore-responsive" + bmore-responsive_container_port = "3000" + bmore-responsive_container_definitions = data.template_file.cfb_ecs_task_definition.rendered + aws_region = var.aws_region +} + +module "asg" { + source = "../../modules/asg" + min_size = 3 + max_size = 6 + instance_count = 3 + instance_type = "t3.medium" + user_data = data.template_file.user_data.rendered + cluster_name = "bmore-responsive-cluster" + subnet_ids = module.vpc.subnet_ids + asg_security_group_ids = [module.sg.ecs_sg_id] + ecs_role = module.ecs_cluster.ecs_role +} + +module "db" { + source = "../../modules/db" + resource_suffix = random_pet.random_pet.id + engine_version = "10.6" + instance_class = "db.t3.medium" + username = "cfb_user" + password = var.db_password + port = "5432" + allocated_storage = "20" + vpc_security_group_ids = [module.sg.sg_postgresql_id] + db_subnet_group_name = "CFB Subnets" + maintenance_window = "Mon:00:00-Mon:03:00" + backup_window = "03:00-06:00" + parameter_group_name = "db_parameter_group" + subnet_ids = module.vpc.subnet_ids +} + diff --git a/terraform/components/full-cluster/outputs.tf b/terraform/components/full-cluster/outputs.tf new file mode 100644 index 00000000..90b3146c --- /dev/null +++ b/terraform/components/full-cluster/outputs.tf @@ -0,0 +1,48 @@ +output "output_bucket_name" { + description = "Name of the output s3 bucket" + value = module.s3.output_bucket_name +} + +output "bmore-responsive_registry" { + description = "Name of the Bmore Response Registry" + value = module.ecs_cluster.cfb_registry +} + +output "load_balancer_address" { + description = "DNS Address of the Application Load Balancer" + value = module.alb.lb-dns +} + +output "db_instance_name" { + description = "The database name" + value = module.db.this_db_instance_name +} + +output "db_instance_port" { + description = "The database port" + value = module.db.this_db_instance_port +} + +output "db_instance_username" { + description = "The master username for the database" + value = module.db.this_db_instance_username +} + +output "db_instance_password" { + description = "The database password (this password may be old, because Terraform doesn't track it after initial creation)" + value = module.db.this_db_instance_password +} + +output "db_instance_address" { + description = "The address of the RDS instance" + value = module.db.this_db_instance_address +} + +output "db_instance_endpoint" { + description = "The connection endpoint" + value = module.db.this_db_instance_endpoint +} + +output "public_hosted_zone_nameservers" { + value = data.aws_route53_zone.hosted_zone.zone_id +} \ No newline at end of file diff --git a/terraform/components/full-cluster/policy.json b/terraform/components/full-cluster/policy.json new file mode 100644 index 00000000..56a0ffea --- /dev/null +++ b/terraform/components/full-cluster/policy.json @@ -0,0 +1,16 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ssm:GetParameters", + "secretsmanager:GetSecretValue", + "kms:Decrypt" + ], + "Resource": [ + "*" + ] + } + ] +} diff --git a/terraform/components/full-cluster/terraform.tfvars b/terraform/components/full-cluster/terraform.tfvars new file mode 100644 index 00000000..34a66263 --- /dev/null +++ b/terraform/components/full-cluster/terraform.tfvars @@ -0,0 +1,2 @@ +public_hosted_zone_name = "bmoreres.codeforbaltimore.org" +zone_id = "Z06944204X33SC5R1R7Z" \ No newline at end of file diff --git a/terraform/components/full-cluster/userdata.sh.tpl b/terraform/components/full-cluster/userdata.sh.tpl new file mode 100644 index 00000000..151a4fb6 --- /dev/null +++ b/terraform/components/full-cluster/userdata.sh.tpl @@ -0,0 +1,9 @@ +#!/bin/bash +curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/linux_64bit/session-manager-plugin.rpm" -o "session-manager-plugin.rpm" +sudo yum install -y session-manager-plugin.rpm +sudo systemctl enable amazon-ssm-agent +sudo systemctl start amazon-ssm-agent +yum install -y postgresql-server postgresql-devel + + +echo ECS_CLUSTER=${cluster_name} >> /etc/ecs/ecs.config diff --git a/terraform/components/full-cluster/variables.tf b/terraform/components/full-cluster/variables.tf new file mode 100644 index 00000000..f22a6976 --- /dev/null +++ b/terraform/components/full-cluster/variables.tf @@ -0,0 +1,26 @@ +variable "aws_region" { + description = "AWS Region to use." + type = string + default = "us-east-2" +} + +variable "db_password" { + type = string + default = null +} + +variable "public_hosted_zone_name" {} + +variable "waf_whitelist_cidrs" { + type = list(string) + default = [ + "0.0.0.0/0" + ] +} + +variable "create_waf" { + type = bool + default = false +} + +variable "zone_id" {} \ No newline at end of file diff --git a/terraform/components/full-cluster/versions.tf b/terraform/components/full-cluster/versions.tf new file mode 100644 index 00000000..ac97c6ac --- /dev/null +++ b/terraform/components/full-cluster/versions.tf @@ -0,0 +1,4 @@ + +terraform { + required_version = ">= 0.12" +} diff --git a/terraform/modules/alb/README.md b/terraform/modules/alb/README.md new file mode 100644 index 00000000..ecf083db --- /dev/null +++ b/terraform/modules/alb/README.md @@ -0,0 +1,32 @@ +# alb + + +## Providers + +| Name | Version | +|------|---------| +| aws | n/a | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:-----:| +| certificate\_arn | n/a | `any` | n/a | yes | +| cfb\_app\_port | Port number the app will be running on | `any` | n/a | yes | +| lb\_sg | Security group IDs for the lb | `any` | n/a | yes | +| mytags | Tags to include on the resources | `map(string)` | `{}` | no | +| vpc\_id | VPC ID that the lb will be placed in | `any` | n/a | yes | +| vpc\_subnets | VPC subnets the lb will use | `list(string)` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| lb-arn | ARN of the lb | +| lb-dns | DNS name for the lb | +| lb-id | ID of the lb | +| tg-cfb-arn | ARN of the Target Group | +| zone\_id | n/a | + + + diff --git a/terraform/modules/alb/main.tf b/terraform/modules/alb/main.tf new file mode 100644 index 00000000..d74b846e --- /dev/null +++ b/terraform/modules/alb/main.tf @@ -0,0 +1,58 @@ +terraform { + required_version = ">= 0.12" +} + +# TODO: Add Documentation about adding HTTPS +# TODO: Make sure APIs have health checks that can be used +# Create ALB +resource "aws_lb" "lb" { + name = "bmore-responsive-api-alb" + internal = "false" + load_balancer_type = "application" + security_groups = [var.lb_sg] + + subnets = var.vpc_subnets + + enable_cross_zone_load_balancing = true + + enable_deletion_protection = false + + tags = merge( + { + "Name" = "bmore-responsive-api-alb" + }, + var.mytags, + ) +} + +# Create ALB target group for both containers +resource "aws_lb_target_group" "tg-cfb" { + name_prefix = "cfb-" + port = var.cfb_app_port + protocol = "HTTP" + vpc_id = var.vpc_id + depends_on = [aws_lb.lb] + lifecycle { + create_before_destroy = true + } + + health_check { + path = "/health" + } +} + +# Create ALB listener +# TODO: Add ALB Routing rules +resource "aws_lb_listener" "api-listener" { + load_balancer_arn = aws_lb.lb.arn + port = "443" + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy-2016-08" + certificate_arn = var.certificate_arn + + default_action { + target_group_arn = aws_lb_target_group.tg-cfb.arn + type = "forward" + } +} + diff --git a/terraform/modules/alb/outputs.tf b/terraform/modules/alb/outputs.tf new file mode 100644 index 00000000..644c575f --- /dev/null +++ b/terraform/modules/alb/outputs.tf @@ -0,0 +1,24 @@ +output "lb-id" { + description = "ID of the lb" + value = aws_lb.lb.id +} + +output "lb-dns" { + description = "DNS name for the lb" + value = aws_lb.lb.dns_name +} + +output "lb-arn" { + description = "ARN of the lb" + value = aws_lb.lb.arn +} + +output "tg-cfb-arn" { + description = "ARN of the Target Group" + value = aws_lb_target_group.tg-cfb.arn +} + +output "zone_id" { + value = aws_lb.lb.zone_id +} + diff --git a/terraform/modules/alb/variables.tf b/terraform/modules/alb/variables.tf new file mode 100644 index 00000000..a998e4f3 --- /dev/null +++ b/terraform/modules/alb/variables.tf @@ -0,0 +1,24 @@ +variable "vpc_id" { + description = "VPC ID that the lb will be placed in" +} + +variable "vpc_subnets" { + description = "VPC subnets the lb will use" + type = list(string) +} + +variable "mytags" { + description = "Tags to include on the resources" + type = map(string) + default = {} +} + +variable "cfb_app_port" { + description = "Port number the app will be running on" +} + +variable "lb_sg" { + description = "Security group IDs for the lb" +} + +variable "certificate_arn" {} \ No newline at end of file diff --git a/terraform/modules/asg/README.md b/terraform/modules/asg/README.md new file mode 100644 index 00000000..d3b57e0a --- /dev/null +++ b/terraform/modules/asg/README.md @@ -0,0 +1,31 @@ +# asg + + +## Providers + +| Name | Version | +|------|---------| +| aws | n/a | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:-----:| +| asg\_security\_group\_ids | Additional security groups to attach to the ASG. | `list(string)` | n/a | yes | +| cluster\_name | The name to be given to the ASG | `string` | n/a | yes | +| default\_cooldown | Default cooldown for ASG. | `string` | `"300"` | no | +| ecs\_role | The ARN of the role attached to ECS Cluster instances | `string` | n/a | yes | +| instance\_count | The number of instances to provision in the ASG. | `string` | n/a | yes | +| instance\_type | The EC2 instance type. | `string` | n/a | yes | +| max\_size | Maximum size for ASG. Must set min, count and max. | `string` | n/a | yes | +| min\_size | Minimum size for ASG. Must set min, count and max. | `string` | n/a | yes | +| root\_block\_device | n/a | `list(map(string))` |
[
{
"volume_size": "30"
}
]
| no | +| subnet\_ids | The subnet IDs used by the Auto Scaling Group. | `list(string)` | n/a | yes | +| user\_data | The user data to provide when launching the instance. | `string` | n/a | yes | + +## Outputs + +No output. + + + diff --git a/terraform/modules/asg/main.tf b/terraform/modules/asg/main.tf new file mode 100644 index 00000000..bb38a6e6 --- /dev/null +++ b/terraform/modules/asg/main.tf @@ -0,0 +1,79 @@ +terraform { + required_version = ">= 0.12" +} + +// Fill in information about the ECS host node here. +data "aws_ami" "ecs_agent_ami" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["*amazon-ecs-optimized"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } +} + +resource "aws_autoscaling_group" "ecs_cluster_asg" { + lifecycle { + create_before_destroy = true + } + + default_cooldown = var.default_cooldown + desired_capacity = var.instance_count + health_check_grace_period = 300 + launch_configuration = aws_launch_configuration.ecs_cluster_config.name + max_size = var.max_size + min_size = var.min_size + name = "bmore-responsive-ecs-cluster-asg" + vpc_zone_identifier = var.subnet_ids + + tags = [ + { + key = "Name" + value = "bmore-responsive-ecs-cluster-asg" + propagate_at_launch = true + }, + { + key = "ECS Cluster" + value = var.cluster_name + propagate_at_launch = true + }, + ] +} + +resource "aws_launch_configuration" "ecs_cluster_config" { + lifecycle { + create_before_destroy = true + } + + dynamic "root_block_device" { + for_each = var.root_block_device + content { + # TF-UPGRADE-TODO: The automatic upgrade tool can't predict + # which keys might be set in maps assigned here, so it has + # produced a comprehensive set here. Consider simplifying + # this after confirming which keys can be set in practice. + + delete_on_termination = lookup(root_block_device.value, "delete_on_termination", null) + encrypted = lookup(root_block_device.value, "encrypted", null) + iops = lookup(root_block_device.value, "iops", null) + volume_size = lookup(root_block_device.value, "volume_size", null) + volume_type = lookup(root_block_device.value, "volume_type", null) + } + } + + enable_monitoring = "true" + iam_instance_profile = var.ecs_role + image_id = data.aws_ami.ecs_agent_ami.image_id + instance_type = var.instance_type + name_prefix = "bmore-responsive-ecs-cluster-" + security_groups = var.asg_security_group_ids + + user_data = var.user_data +} + diff --git a/terraform/modules/asg/variables.tf b/terraform/modules/asg/variables.tf new file mode 100644 index 00000000..691306bd --- /dev/null +++ b/terraform/modules/asg/variables.tf @@ -0,0 +1,57 @@ +variable "cluster_name" { + description = "The name to be given to the ASG" + type = string +} + +variable "asg_security_group_ids" { + description = "Additional security groups to attach to the ASG." + type = list(string) +} + +variable "instance_count" { + description = "The number of instances to provision in the ASG." + type = string +} + +variable "default_cooldown" { + description = "Default cooldown for ASG." + default = "300" +} + +variable "instance_type" { + description = "The EC2 instance type." + type = string +} + +variable "max_size" { + description = "Maximum size for ASG. Must set min, count and max." + type = string +} + +variable "min_size" { + description = "Minimum size for ASG. Must set min, count and max." + type = string +} + +variable "subnet_ids" { + description = "The subnet IDs used by the Auto Scaling Group." + type = list(string) +} + +variable "user_data" { + description = "The user data to provide when launching the instance." + type = string +} + +variable "root_block_device" { + type = list(map(string)) + default = [{ + volume_size = "30" + }] +} + +variable "ecs_role" { + description = "The ARN of the role attached to ECS Cluster instances" + type = string +} + diff --git a/terraform/modules/certificate/README.md b/terraform/modules/certificate/README.md new file mode 100644 index 00000000..4ea86a81 --- /dev/null +++ b/terraform/modules/certificate/README.md @@ -0,0 +1,27 @@ +# certificate + + +## Providers + +| Name | Version | +|------|---------| +| aws | n/a | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:-----:| +| dns\_ttl | DNS records TTL | `number` | `60` | no | +| dns\_zone\_id | Route53 Zone id handleling the domains on the certificate | `any` | n/a | yes | +| domain\_name | Main domain name for the SSL certificate | `any` | n/a | yes | +| subject\_alternative\_names | Alternate domain names for the SSL certificate | `list(string)` | `[]` | no | +| tags | Tags associated to the certificate | `map(string)` | `{}` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| certificate\_arn | n/a | + + + diff --git a/terraform/modules/certificate/main.tf b/terraform/modules/certificate/main.tf new file mode 100644 index 00000000..f54bc403 --- /dev/null +++ b/terraform/modules/certificate/main.tf @@ -0,0 +1,30 @@ +terraform { + required_version = ">= 0.12" +} + +resource "aws_acm_certificate" "acm_certificate" { + domain_name = var.domain_name + validation_method = "DNS" + subject_alternative_names = var.subject_alternative_names + tags = var.tags + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_route53_record" "acm_certificate_validation_record" { + count = length(var.subject_alternative_names) + 1 + name = aws_acm_certificate.acm_certificate.domain_validation_options[count.index]["resource_record_name"] + type = "CNAME" + zone_id = var.dns_zone_id + records = [aws_acm_certificate.acm_certificate.domain_validation_options[count.index]["resource_record_value"]] + ttl = var.dns_ttl + allow_overwrite = true +} + +resource "aws_acm_certificate_validation" "acm_certificate_validation_record" { + depends_on = [aws_acm_certificate.acm_certificate] + certificate_arn = aws_acm_certificate.acm_certificate.arn + validation_record_fqdns = aws_route53_record.acm_certificate_validation_record.*.fqdn +} diff --git a/terraform/modules/certificate/outputs.tf b/terraform/modules/certificate/outputs.tf new file mode 100644 index 00000000..e99d9371 --- /dev/null +++ b/terraform/modules/certificate/outputs.tf @@ -0,0 +1,3 @@ +output "certificate_arn" { + value = aws_acm_certificate_validation.acm_certificate_validation_record.certificate_arn +} diff --git a/terraform/modules/certificate/variables.tf b/terraform/modules/certificate/variables.tf new file mode 100644 index 00000000..03d04e0a --- /dev/null +++ b/terraform/modules/certificate/variables.tf @@ -0,0 +1,24 @@ +variable "dns_zone_id" { + description = "Route53 Zone id handleling the domains on the certificate" +} + +variable "domain_name" { + description = "Main domain name for the SSL certificate" +} + +variable "dns_ttl" { + description = "DNS records TTL" + default = 60 +} + +variable "tags" { + description = "Tags associated to the certificate" + type = map(string) + default = {} +} + +variable "subject_alternative_names" { + description = "Alternate domain names for the SSL certificate" + type = list(string) + default = [] +} diff --git a/terraform/modules/db/README.md b/terraform/modules/db/README.md new file mode 100644 index 00000000..b7c12561 --- /dev/null +++ b/terraform/modules/db/README.md @@ -0,0 +1,54 @@ +# db + + +## Providers + +| Name | Version | +|------|---------| +| aws | n/a | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:-----:| +| allocated\_storage | The allocated storage in gigabytes. For read replica, set the same value as master's | `string` | n/a | yes | +| apply\_immediately | Specifies whether any database modifications are applied immediately, or during the next maintenance window | `string` | `"false"` | no | +| auto\_minor\_version\_upgrade | Indicates that minor engine upgrades will be applied automatically to the DB instance during the maintenance window | `string` | `"false"` | no | +| availability\_zone | The AZ for the RDS instance. It is recommended to only use this when creating a read replica instance | `string` | `""` | no | +| backup\_retention\_period | The days to retain backups for | `string` | `7` | no | +| backup\_window | The daily time range (in UTC) during which automated backups are created if they are enabled. Before and not overlap with maintenance\_window | `string` | `""` | no | +| db\_subnet\_group\_name | Name of DB subnet group | `string` | `""` | no | +| engine\_version | The postgres engine version | `string` | `""` | no | +| instance\_class | The instance type of the RDS instance | `string` | n/a | yes | +| iops | The amount of provisioned IOPS. Setting this implies a storage\_type of io1 | `string` | `"0"` | no | +| kms\_key\_id | Specifies a custom KMS key to be used to encrypt | `string` | `""` | no | +| maintenance\_window | The window to perform maintenance in. Syntax: 'ddd:hh24:mi-ddd:hh24:mi' | `string` | n/a | yes | +| parameter\_group\_name | Name of the DB parameter group to associate | `string` | n/a | yes | +| password | password for the master DB user | `string` | n/a | yes | +| port | The port on which the DB accepts connections | `string` | `"5432"` | no | +| resource\_suffix | n/a | `string` | `"default"` | no | +| storage\_encrypted | Specifies whether the DB instance is encrypted | `string` | `"true"` | no | +| storage\_type | One of standard (magnetic), gp2 (general purpose SSD), or io1 (provisioned IOPS SSD) | `string` | `"gp2"` | no | +| subnet\_ids | n/a | `any` | n/a | yes | +| username | Username for the master DB user | `string` | `"postgres"` | no | +| vpc\_security\_group\_ids | List of VPC security groups to associate | `list(string)` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| this\_db\_instance\_address | The address of the RDS instance | +| this\_db\_instance\_arn | The ARN of the RDS instance | +| this\_db\_instance\_availability\_zone | The availability zone of the RDS instance | +| this\_db\_instance\_endpoint | The connection endpoint | +| this\_db\_instance\_hosted\_zone\_id | The canonical hosted zone ID of the DB instance (to be used in a Route 53 Alias record) | +| this\_db\_instance\_id | The RDS instance ID | +| this\_db\_instance\_name | The database name | +| this\_db\_instance\_password | The database password (this password may be old, because Terraform doesn't track it after initial creation) | +| this\_db\_instance\_port | The database port | +| this\_db\_instance\_resource\_id | The RDS Resource ID of this instance | +| this\_db\_instance\_status | The RDS instance status | +| this\_db\_instance\_username | The master username for the database | + + + diff --git a/terraform/modules/db/main.tf b/terraform/modules/db/main.tf new file mode 100644 index 00000000..6a8037ae --- /dev/null +++ b/terraform/modules/db/main.tf @@ -0,0 +1,62 @@ +terraform { + required_version = ">= 0.12" +} + +resource "aws_db_subnet_group" "subnet_group" { + subnet_ids = var.subnet_ids +} + +resource "aws_db_instance" "this" { + identifier = "healthcare-rollcall-postgres" + + engine = "postgres" + engine_version = var.engine_version + instance_class = var.instance_class + allocated_storage = var.allocated_storage + username = var.username + password = var.password + port = var.port + + // allocated_storage = "${var.allocated_storage}" + storage_type = var.storage_type + iops = var.iops + storage_encrypted = var.storage_encrypted + kms_key_id = var.kms_key_id + name = "healthcareRollcallDB" + + # NOTE: Do NOT use 'user' as the value for 'username' as it throws: + # "Error creating DB Instance: InvalidParameterValue: MasterUsername + # user cannot be used as it is a reserved word used by the engine" + + vpc_security_group_ids = var.vpc_security_group_ids + + publicly_accessible = false + + maintenance_window = var.maintenance_window + backup_window = var.backup_window + + # disable backups to create DB faster + backup_retention_period = 0 + + tags = { + Owner = "user" + Environment = "dev" + Name = "healthcare-rollcall_db" + } + + enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"] + + # DB subnet group + db_subnet_group_name = aws_db_subnet_group.subnet_group.name + + # Snapshot name upon DB deletion + final_snapshot_identifier = "healthcare-rollcall-db-${var.resource_suffix}" + + # Database Deletion Protection + deletion_protection = false + + lifecycle { + ignore_changes = [password] + } +} + diff --git a/terraform/modules/db/outputs.tf b/terraform/modules/db/outputs.tf new file mode 100644 index 00000000..05c48023 --- /dev/null +++ b/terraform/modules/db/outputs.tf @@ -0,0 +1,60 @@ +output "this_db_instance_address" { + description = "The address of the RDS instance" + value = aws_db_instance.this.address +} + +output "this_db_instance_arn" { + description = "The ARN of the RDS instance" + value = aws_db_instance.this.arn +} + +output "this_db_instance_availability_zone" { + description = "The availability zone of the RDS instance" + value = aws_db_instance.this.availability_zone +} + +output "this_db_instance_endpoint" { + description = "The connection endpoint" + value = aws_db_instance.this.endpoint +} + +output "this_db_instance_hosted_zone_id" { + description = "The canonical hosted zone ID of the DB instance (to be used in a Route 53 Alias record)" + value = aws_db_instance.this.hosted_zone_id +} + +output "this_db_instance_id" { + description = "The RDS instance ID" + value = aws_db_instance.this.id +} + +output "this_db_instance_resource_id" { + description = "The RDS Resource ID of this instance" + value = aws_db_instance.this.resource_id +} + +output "this_db_instance_status" { + description = "The RDS instance status" + value = aws_db_instance.this.status +} + +output "this_db_instance_name" { + description = "The database name" + value = aws_db_instance.this.name +} + +output "this_db_instance_username" { + description = "The master username for the database" + value = aws_db_instance.this.username +} + +output "this_db_instance_password" { + description = "The database password (this password may be old, because Terraform doesn't track it after initial creation)" + value = aws_db_instance.this.password +} + +output "this_db_instance_port" { + description = "The database port" + value = aws_db_instance.this.port +} + diff --git a/terraform/modules/db/variables.tf b/terraform/modules/db/variables.tf new file mode 100644 index 00000000..2b6290c0 --- /dev/null +++ b/terraform/modules/db/variables.tf @@ -0,0 +1,116 @@ +variable "engine_version" { + type = string + description = "The postgres engine version" + default = "" +} + +variable "instance_class" { + type = string + description = "The instance type of the RDS instance" +} + +variable "username" { + type = string + description = "Username for the master DB user" + default = "postgres" +} + +variable "password" { + type = string + description = "password for the master DB user" +} + +variable "port" { + type = string + description = "The port on which the DB accepts connections" + default = "5432" +} + +variable "allocated_storage" { + type = string + description = "The allocated storage in gigabytes. For read replica, set the same value as master's" +} + +variable "storage_type" { + type = string + description = "One of standard (magnetic), gp2 (general purpose SSD), or io1 (provisioned IOPS SSD)" + default = "gp2" +} + +variable "iops" { + type = string + description = "The amount of provisioned IOPS. Setting this implies a storage_type of io1" + default = "0" +} + +variable "storage_encrypted" { + type = string + description = "Specifies whether the DB instance is encrypted" + default = "true" +} + +variable "kms_key_id" { + type = string + description = "Specifies a custom KMS key to be used to encrypt" + default = "" +} + +variable "vpc_security_group_ids" { + type = list(string) + description = "List of VPC security groups to associate" +} + +variable "db_subnet_group_name" { + type = string + description = "Name of DB subnet group" + default = "" +} + +variable "parameter_group_name" { + type = string + description = "Name of the DB parameter group to associate" +} + +variable "availability_zone" { + type = string + description = "The AZ for the RDS instance. It is recommended to only use this when creating a read replica instance" + default = "" +} + +variable "auto_minor_version_upgrade" { + type = string + description = "Indicates that minor engine upgrades will be applied automatically to the DB instance during the maintenance window" + default = "false" +} + +variable "apply_immediately" { + type = string + description = "Specifies whether any database modifications are applied immediately, or during the next maintenance window" + default = "false" +} + +variable "maintenance_window" { + type = string + description = "The window to perform maintenance in. Syntax: 'ddd:hh24:mi-ddd:hh24:mi'" +} + +variable "backup_retention_period" { + type = string + description = "The days to retain backups for" + default = 7 +} + +variable "backup_window" { + type = string + description = "The daily time range (in UTC) during which automated backups are created if they are enabled. Before and not overlap with maintenance_window" + default = "" +} + +variable "subnet_ids" { + // type = list(string) +} + +variable "resource_suffix" { + default = "default" +} + diff --git a/terraform/modules/dns_record/README.md b/terraform/modules/dns_record/README.md new file mode 100644 index 00000000..0252d98a --- /dev/null +++ b/terraform/modules/dns_record/README.md @@ -0,0 +1,24 @@ +# dns_record + + +## Providers + +| Name | Version | +|------|---------| +| aws | n/a | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:-----:| +| lb\_dns\_name | n/a | `any` | n/a | yes | +| lb\_zone\_id | n/a | `any` | n/a | yes | +| name | n/a | `any` | n/a | yes | +| zone\_id | n/a | `any` | n/a | yes | + +## Outputs + +No output. + + + diff --git a/terraform/modules/dns_record/main.tf b/terraform/modules/dns_record/main.tf new file mode 100644 index 00000000..bcec285d --- /dev/null +++ b/terraform/modules/dns_record/main.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 0.12" +} + +resource "aws_route53_record" "www" { + zone_id = var.zone_id + name = var.name + type = "A" + + alias { + name = var.lb_dns_name + zone_id = var.lb_zone_id + evaluate_target_health = true + } +} \ No newline at end of file diff --git a/terraform/modules/dns_record/outputs.tf b/terraform/modules/dns_record/outputs.tf new file mode 100644 index 00000000..e69de29b diff --git a/terraform/modules/dns_record/variables.tf b/terraform/modules/dns_record/variables.tf new file mode 100644 index 00000000..d28bbe36 --- /dev/null +++ b/terraform/modules/dns_record/variables.tf @@ -0,0 +1,7 @@ +variable "name" {} + +variable "zone_id" {} + +variable "lb_dns_name" {} + +variable "lb_zone_id" {} \ No newline at end of file diff --git a/terraform/modules/ecs/README.md b/terraform/modules/ecs/README.md new file mode 100644 index 00000000..cb667086 --- /dev/null +++ b/terraform/modules/ecs/README.md @@ -0,0 +1,31 @@ +# ecs + + +## Providers + +| Name | Version | +|------|---------| +| aws | n/a | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:-----:| +| aws\_region | n/a | `any` | n/a | yes | +| bmore-responsive\_container\_definitions | The Rendered JSON of a container definition array. See example-container.json for a sample of valid JSON input. | `string` | n/a | yes | +| bmore-responsive\_container\_name | The name of the container to associate with the Load Balancer. Must equal the container name in the container definition JSON | `string` | n/a | yes | +| bmore-responsive\_container\_port | The port on the container to associate with the Load Balancer | `string` | n/a | yes | +| bmore-responsive\_desired\_count | The number of tasks to run in the service | `string` | n/a | yes | +| bmore-responsive\_target\_group\_arn | The ARN of the Target Group for Load Balancing | `string` | n/a | yes | +| cluster\_name | The name to be given to the ASG | `string` | n/a | yes | +| output\_bucket\_arn | ARN of the S3 bucket that contains the output files | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| cfb\_registry | Address for the Registry | +| ecs\_role | ARN for the role attached to ECS Cluster instances. | + + + diff --git a/terraform/modules/ecs/main.tf b/terraform/modules/ecs/main.tf new file mode 100644 index 00000000..55a4f307 --- /dev/null +++ b/terraform/modules/ecs/main.tf @@ -0,0 +1,243 @@ +terraform { + required_version = ">= 0.12" +} + +# Set up necessary IAM Roles for ECS Hosts + +data "aws_iam_policy_document" "ecs_cluster_asg_policy" { + statement { + actions = [ + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel" + ] + effect = "Allow" + + resources = ["*"] + } + + statement { + actions = [ + "s3:GetEncryptionConfiguration" + ] + effect = "Allow" + resources = ["*"] + } + + statement { + + actions = [ + "kms:Decrypt" + ] + + effect = "Allow" + + resources = ["*"] + } + + statement { + actions = [ + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "logs:CreateLogStream", + "logs:PutLogEvents", + ] + + resources = [ + "*", + ] + } + + statement { + actions = [ + "s3:*", + ] + + resources = [ + var.output_bucket_arn, + "${var.output_bucket_arn}/*", + ] + } +} + +data "aws_caller_identity" "current" {} + +resource "aws_iam_role" "ecs_cluster" { + path = "/" + name = "bmore-responsive_ecs_cluster_role" + + assume_role_policy = < +## Providers + +| Name | Version | +|------|---------| +| aws | n/a | +| random | n/a | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:-----:| +| aws\_region | n/a | `any` | n/a | yes | +| mytags | Tags to include on the resources | `map(string)` | `{}` | no | +| resource\_suffix | n/a | `string` | `"default"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| output\_bucket\_arn | ARN for the S3 output bucket | +| output\_bucket\_name | Name for the S3 Bucket | + + + diff --git a/terraform/modules/s3/main.tf b/terraform/modules/s3/main.tf new file mode 100644 index 00000000..f35eccb1 --- /dev/null +++ b/terraform/modules/s3/main.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 0.12" +} + +resource "random_pet" "random_pet" { + length = 2 +} + +resource "aws_s3_bucket" "output-bucket" { + bucket = "cfb-healthcare-rollcall-${var.aws_region}-${var.resource_suffix}" + acl = "private" + force_destroy = "true" + tags = merge(map("Name", "cfb-healthcare-rollcall-${var.aws_region}-${random_pet.random_pet.id}"), var.mytags) +} diff --git a/terraform/modules/s3/outputs.tf b/terraform/modules/s3/outputs.tf new file mode 100644 index 00000000..d6da2ab4 --- /dev/null +++ b/terraform/modules/s3/outputs.tf @@ -0,0 +1,9 @@ +output "output_bucket_arn" { + description = "ARN for the S3 output bucket" + value = aws_s3_bucket.output-bucket.arn +} + +output "output_bucket_name" { + description = "Name for the S3 Bucket" + value = aws_s3_bucket.output-bucket.id +} diff --git a/terraform/modules/s3/variables.tf b/terraform/modules/s3/variables.tf new file mode 100644 index 00000000..f4cd02d8 --- /dev/null +++ b/terraform/modules/s3/variables.tf @@ -0,0 +1,11 @@ +variable "mytags" { + description = "Tags to include on the resources" + type = map(string) + default = {} +} + +variable "aws_region" {} + +variable "resource_suffix" { + default = "default" +} \ No newline at end of file diff --git a/terraform/modules/sg/README.md b/terraform/modules/sg/README.md new file mode 100644 index 00000000..9d426f55 --- /dev/null +++ b/terraform/modules/sg/README.md @@ -0,0 +1,27 @@ +# sg + + +## Providers + +| Name | Version | +|------|---------| +| aws | n/a | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:-----:| +| db\_ingress\_cidrs | n/a | `list(string)` | n/a | yes | +| mytags | Tags to include on the resources | `map` | `{}` | no | +| vpc\_id | VPC ID | `any` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| alb-sg-id | ALB Security Group ID | +| ecs\_sg\_id | ECS Security Group ID | +| sg\_postgresql\_id | n/a | + + + diff --git a/terraform/modules/sg/main.tf b/terraform/modules/sg/main.tf new file mode 100644 index 00000000..6e0510e5 --- /dev/null +++ b/terraform/modules/sg/main.tf @@ -0,0 +1,84 @@ +terraform { + required_version = ">= 0.12" +} + +# Create security group + +resource "aws_security_group" "sg-alb" { + name = "bmore-responsive-alb-access" + vpc_id = "${var.vpc_id}" + + # Merge tags from environment tfvars and create name tag + tags = "${merge(map("Name", "bmore-responsive-alb-access"), var.mytags)}" + + ingress { + # TLS (change to whatever ports you need) + from_port = 443 + to_port = 443 + protocol = "tcp" + + # We open to 0.0.0.0/0 here to support the testing activities. + # In a production environment, these connections would be limited to + # approved internal IPs. (10.x.x.x/x block(s)) + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + + +resource "aws_security_group" "sg-ecs" { + name = "bmore-responsive-ecs-host-access" + vpc_id = "${var.vpc_id}" + + # Merge tags from environment tfvars and create name tag + tags = "${merge(map("Name", "bmore-responsive-ecs-host-access"), var.mytags)}" + + ingress { + from_port = 32768 + to_port = 61000 + protocol = "tcp" + security_groups = ["${aws_security_group.sg-alb.id}"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_security_group" "sg_postgresql" { + name = "bmore-responsive-db-access" + vpc_id = var.vpc_id + + # Merge tags from environment tfvars and create name tag + tags = merge(map("Name", "bmore-responsive-db-access"), var.mytags) + + ingress { + # TLS (change to whatever ports you need) + from_port = 5432 + to_port = 5432 + protocol = "tcp" + + # We open to 0.0.0.0/0 here to support the testing activities. + # In a production environment, these connections would be limited to + # approved internal IPs. (10.x.x.x/x block(s)) + cidr_blocks = var.db_ingress_cidrs + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +# TODO: Add SG Rules for ECS ASG and Load Balancer. diff --git a/terraform/modules/sg/outputs.tf b/terraform/modules/sg/outputs.tf new file mode 100644 index 00000000..2989da50 --- /dev/null +++ b/terraform/modules/sg/outputs.tf @@ -0,0 +1,13 @@ +output "ecs_sg_id" { + description = "ECS Security Group ID" + value = aws_security_group.sg-ecs.id +} + +output "alb-sg-id" { + description = "ALB Security Group ID" + value = aws_security_group.sg-alb.id +} + +output "sg_postgresql_id" { + value = aws_security_group.sg_postgresql.id +} \ No newline at end of file diff --git a/terraform/modules/sg/variables.tf b/terraform/modules/sg/variables.tf new file mode 100644 index 00000000..76031398 --- /dev/null +++ b/terraform/modules/sg/variables.tf @@ -0,0 +1,13 @@ +variable "vpc_id" { + description = "VPC ID" +} + +variable "mytags" { + description = "Tags to include on the resources" + type = "map" + default = {} +} + +variable "db_ingress_cidrs" { + type = list(string) +} \ No newline at end of file diff --git a/terraform/modules/vpc/README.md b/terraform/modules/vpc/README.md new file mode 100644 index 00000000..0dcff33c --- /dev/null +++ b/terraform/modules/vpc/README.md @@ -0,0 +1,30 @@ +# vpc + + +## Providers + +| Name | Version | +|------|---------| +| aws | n/a | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:-----:| +| availability\_zones | The Availability Zones to use in the VPC | `list(string)` |
[
"us-east-1a",
"us-east-1b",
"us-east-1c"
]
| no | +| mytags | Tags to include on the resources | `map(string)` | `{}` | no | +| private\_subnet\_cidrs | The CIDR Blocks of the Private Subnets | `list(string)` |
[
"10.0.0.0/24",
"10.0.1.0/24",
"10.0.2.0/24"
]
| no | +| public\_subnet\_cidrs | The CIDR Blocks of the Public Subnets | `list(string)` |
[
"10.0.3.0/24",
"10.0.4.0/24",
"10.0.5.0/24"
]
| no | +| vpc\_cidr | The CIDR block for the VPC | `any` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| private\_subnet\_cidrs | n/a | +| public-subnet-ids | Subnet IDs | +| subnet\_ids | Subnet IDs | +| vpc-id | VPC ID | + + + diff --git a/terraform/modules/vpc/main.tf b/terraform/modules/vpc/main.tf new file mode 100644 index 00000000..1eaffe55 --- /dev/null +++ b/terraform/modules/vpc/main.tf @@ -0,0 +1,98 @@ +terraform { + required_version = ">= 0.12" +} + +resource "aws_vpc" "vpc" { + cidr_block = var.vpc_cidr + tags = merge( + { + "Name" = "cfb-healthcare-rollcall-vpc" + }, + var.mytags, + ) +} + +data "aws_availability_zones" "available" { + state = "available" +} + +resource "aws_subnet" "public-subnet" { + count = 3 + vpc_id = aws_vpc.vpc.id + cidr_block = element(var.public_subnet_cidrs, count.index) + availability_zone = data.aws_availability_zones.available.names[count.index] + + tags = { + Name = "cfb-public-subnet-${count.index}" + } +} + +resource "aws_subnet" "private-subnet" { + count = 3 + vpc_id = aws_vpc.vpc.id + cidr_block = element(var.private_subnet_cidrs, count.index) + availability_zone = data.aws_availability_zones.available.names[count.index] + + tags = { + Name = "cfb-private-subnet-${count.index}" + } +} + +resource "aws_internet_gateway" "igw" { + vpc_id = aws_vpc.vpc.id + + tags = { + Name = "CFB VPC IGW" + } +} + +# Set up Elastic IPs and NAT Gateways +resource "aws_eip" "nat-gw" { + count = 3 +} + +resource "aws_nat_gateway" "nat-gw" { + count = 3 + allocation_id = aws_eip.nat-gw[count.index].id + subnet_id = aws_subnet.public-subnet[count.index].id +} + +# Associate public routing rules to their subnets. +resource "aws_route_table" "public-route-table" { + vpc_id = aws_vpc.vpc.id + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw.id + } + + tags = { + Name = "CFB Public Subnets" + } +} + +resource "aws_route_table_association" "public-table-association" { + count = 3 + subnet_id = aws_subnet.public-subnet[count.index].id + route_table_id = aws_route_table.public-route-table.id +} + +# Associate private routing rules to their subnets. +resource "aws_route_table" "private-route-table" { + count = 3 + vpc_id = aws_vpc.vpc.id + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.nat-gw[count.index].id + } + + tags = { + Name = "CFB Subnets" + } +} + +resource "aws_route_table_association" "private-table-association" { + count = 3 + subnet_id = aws_subnet.private-subnet[count.index].id + route_table_id = aws_route_table.private-route-table[count.index].id +} + diff --git a/terraform/modules/vpc/outputs.tf b/terraform/modules/vpc/outputs.tf new file mode 100644 index 00000000..c2bb0a9a --- /dev/null +++ b/terraform/modules/vpc/outputs.tf @@ -0,0 +1,17 @@ +output "vpc-id" { + description = "VPC ID" + value = aws_vpc.vpc.id +} +output "subnet_ids" { + description = "Subnet IDs" + value = aws_subnet.private-subnet.*.id +} + +output "public-subnet-ids" { + description = "Subnet IDs" + value = aws_subnet.public-subnet.*.id +} + +output "private_subnet_cidrs" { + value = var.private_subnet_cidrs +} \ No newline at end of file diff --git a/terraform/modules/vpc/variables.tf b/terraform/modules/vpc/variables.tf new file mode 100644 index 00000000..3e213cc5 --- /dev/null +++ b/terraform/modules/vpc/variables.tf @@ -0,0 +1,28 @@ +variable "vpc_cidr" { + description = "The CIDR block for the VPC" +} + +variable "mytags" { + description = "Tags to include on the resources" + type = map(string) + default = {} +} + +variable "private_subnet_cidrs" { + description = "The CIDR Blocks of the Private Subnets" + type = list(string) + default = ["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24"] +} + +variable "public_subnet_cidrs" { + description = "The CIDR Blocks of the Public Subnets" + type = list(string) + default = ["10.0.3.0/24", "10.0.4.0/24", "10.0.5.0/24"] +} + +variable "availability_zones" { + description = "The Availability Zones to use in the VPC" + type = list(string) + default = ["us-east-1a", "us-east-1b", "us-east-1c"] +} + diff --git a/terraform/modules/waf/README.md b/terraform/modules/waf/README.md new file mode 100644 index 00000000..1db32df5 --- /dev/null +++ b/terraform/modules/waf/README.md @@ -0,0 +1,26 @@ +# waf + + +## Providers + +| Name | Version | +|------|---------| +| aws | n/a | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:-----:| +| create | n/a | `bool` | `false` | no | +| lb\_arn | n/a | `any` | n/a | yes | +| resource\_suffix | n/a | `any` | n/a | yes | +| whitelist\_cidrs | n/a | `list(string)` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| web\_acl\_id | n/a | + + + diff --git a/terraform/modules/waf/main.tf b/terraform/modules/waf/main.tf new file mode 100644 index 00000000..1ffb2a64 --- /dev/null +++ b/terraform/modules/waf/main.tf @@ -0,0 +1,55 @@ +terraform { + required_version = ">= 0.12" +} + +resource "aws_wafregional_web_acl_association" "web_acl_association" { + count = var.create ? 1 : 0 + resource_arn = var.lb_arn + web_acl_id = aws_wafregional_web_acl.web_acl[0].id +} + +resource "aws_wafregional_ipset" "external_ipset" { + count = var.create ? 1 : 0 + name = "GenericMatchExternalIPs${var.resource_suffix}" + dynamic "ip_set_descriptor" { + for_each = var.whitelist_cidrs + content { + type = "IPV4" + value = ip_set_descriptor.value + } + } + lifecycle { + create_before_destroy = true + } +} + +resource "aws_wafregional_rule" "detect_external_access" { + count = var.create ? 1 : 0 + name = "DetectExternalAccess${var.resource_suffix}" + metric_name = "DetectExternalAccess${var.resource_suffix}" + + predicate { + data_id = aws_wafregional_ipset.external_ipset[0].id + negated = false + type = "IPMatch" + } +} + +resource "aws_wafregional_web_acl" "web_acl" { + count = var.create ? 1 : 0 + name = "WebAcl${var.resource_suffix}" + metric_name = "WebAcl${var.resource_suffix}" + + default_action { + type = "BLOCK" + } + + rule { + action { + type = "ALLOW" + } + + priority = 1 + rule_id = aws_wafregional_rule.detect_external_access[0].id + } +} diff --git a/terraform/modules/waf/outputs.tf b/terraform/modules/waf/outputs.tf new file mode 100644 index 00000000..78b9b1ab --- /dev/null +++ b/terraform/modules/waf/outputs.tf @@ -0,0 +1,3 @@ +output "web_acl_id" { + value = aws_wafregional_web_acl.web_acl[0].id +} diff --git a/terraform/modules/waf/variables.tf b/terraform/modules/waf/variables.tf new file mode 100644 index 00000000..4ef2d3c4 --- /dev/null +++ b/terraform/modules/waf/variables.tf @@ -0,0 +1,12 @@ +variable "whitelist_cidrs" { + type = list(string) +} + +variable "resource_suffix" {} + +variable "lb_arn" {} + +variable "create" { + type = bool + default = false +} \ No newline at end of file diff --git a/terraform/versions.tf b/terraform/versions.tf new file mode 100644 index 00000000..ac97c6ac --- /dev/null +++ b/terraform/versions.tf @@ -0,0 +1,4 @@ + +terraform { + required_version = ">= 0.12" +}