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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions infra/aws/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Local .terraform directories
**/.terraform/*

# private configuration files
terraform/invocation/main.tf

# .tfstate files
*.tfstate
*.tfstate.*
.terraform.lock.hcl

# Crash log files
crash.log
crash.*.log

# Exclude all .tfvars files, which are likely to contain sensitive 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
*.tfvars.json

# 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
116 changes: 116 additions & 0 deletions infra/aws/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Recipe: Set up and manage OSMT infrastructure on AWS

This recipe is intended to be a starting point for organizations that want to deploy OSMT in an AWS cloud environment. It is not the only way to deploy OSMT, but it shows a workable approach that aims to be simple, secure, and production-ready. This approach can be copied and modified for specific organizational needs.

This directory includes code that enables an implementer to quickly provision the OSMT application into a new production environment on the AWS cloud. The approach uses Terraform to provision infrastructure on a specified AWS account including: Route53, ECS/Fargate, RDS, Elasticache, and Cloudwatch. Configuration is synced with a S3 Bucket and DynamoDB table.

This is not the only way to deploy OSMT, but it shows a workable approach that aims to be simple, secure, and production-ready. This approach can be copied and modified for specific organizational needs. Some runtime secrets are accessible within privileged access on the AWS Console, so maintaining the security of the AWS account is important.

The workflows documented here have been developed and tested on MacOS but use tools supported more broadly. As an admin user of an AWS account, these instructions will enable you to set up infrastructure from a local computer.

Advanced installations will likely extend and customize the infrastructure beyond what is mentioned here. For example, teams can populate settings templates with secrets in secure automated environments. This approach is not covered here, but discussion and further development is welcome within the OSMT community.

**Known Issues and potential improvements for additional security and functionality**:

- WARNING! Migrations are not yet working against RDS due to `utf8mb3_unicode_ci` encoded into migrations. To discuss with maintainer team.
- App containers are set to run migrations and reindex elasticsearch. There should be a container usually not running that is used to run migrations and reindexing. This is a TODO.
- Application is using root database credentials. A clever system for swapping out credentials for different procedures would be a good improvement.
- CloudWatch logs are not encrypted.
- Application environment variables, including some application secrets are made available to the application as environment variables. Follow the principles of least privilege and secure access to the AWS account to keep these secrets secure. Only allow need to know access to the IAM roles necessary to access this information. An integration with AWS Secrets Manager would be a well-documented additional layer of improvement from here.
- High availability: This approach is about one and a half steps away from high-availability.
- By enabling multi-AZ capability on RDS, it becomes high availability
- Elasticsearch is running on only one AZ, representing a single point of failure. Replicating this would be a tricky endeavor using ECS. A managed Elasticsearch service would be an option for an organization that found it needed a highly available Elasticsearch. The connection string could easily be configured on the app container.
- Additional ALB access logs could be added by creating an additional S3 bucket and configuring the ALB to write logs to it.

## Install dependencies on your local computer

- Terraform CLI ([See instructions](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli))
- Optional: AWS CLI ([See instructions](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html))

## Create an AWS Account and relevant access roles

- Go to the [AWS Console](https://console.aws.amazon.com/console/home) and select "Create a new AWS account". Or use an existing account, but make sure that the account is secure, because this approach encodes some application secrets into environment variables. Only system admins authorized to access this secure configuration should have relevant permissions in the account.

- Sign up and confirm email addresses, real addresses, and payment method
- Create IAM Role for `osmt-admin`
- Go to the Identity & Access Management (IAM) service and select Users
- Add IAM user: select "Add user" -> IAM -> attach policies manually -> add policy `AdministratorAccess`
- Obtain Access key and save in your local workspace

- Go to IAM console
- Go to user you created
- Go to security credentials
- Create access key
- Select CLI option
Create a description tag value for the access key (OSMT infra)
- Download CSV of the access key, store securely
- Make `~/.aws/credentials` file and fill values from CSV:

```
[osmt]
aws_access_key_id = YOURKEY
aws_secret_access_key = SECRET
```

- Test credentials: `aws sts get-caller-identity --profile osmt`

- Choose an AWS region that best serves your userbase and 2 availability zones within that region you want to deploy your app to. For example, select the `us-west-2` region with AZs `us-west-2a` and `us-west-2b`. This will be used in the Terraform configuration `config.auto.tfvars.json`.

### Set up an S3 bucket and Dynamo DB to store terraform state for the deployment.

This recipe uses S3 to back up the terraform state, to enable the infrastructure to be managed by multiple users or the same user across devices over time. The Terraform state is stored in a S3 bucket with a key name identified in a DynamoDB table. These resources are created manually in the AWS console.

- Go to S3 within the AWS console: Create new bucket with settings:
name: `osmt-test` (It is recommended to use this format to indicate application and environment, e.g. `osmt-prod`. This value will be the name of the environment you'll deploy and will be used later in the Terraform configuration).
- ACLs Disabled (object ownership: bucket owner enforced)
- Block all public access
- Bucket versioning: enable
- Default encryption (use defaults: AWS S3 managed keys, leave bucket key enabled)
- Go to DynamoDB within the AWS Console
- create table `osmt-test`
- Partition key: `LockID`
- No sort key is needed
- Table settings: default

A future upgrade to this guide could replace these manual steps with AWS CLI commands.

## Configure your domain name and SSL certificate

- Buy a domain at your registrar of choice (you can use AWS Route53 for easiest setup), or identify a subdomain that you want to direct to the OSMT app
- Note: to serve the app at the root of a domain, your registrar needs to support the `ALIAS` record type. Route53 supports this, but some registrars do not.
- Create a hosted zone matching your domain name. Add a description, and choose type public. If you used Route53 to register, this will be created automatically
- Configure certificate: go to AWS Certificate manager in the console. Request certificate -> Public. Follow any instructions to verify your domain name.
- Add your domain name (and `*.yourdomainname.net`) to the domain names. DNS validation, default key algorithm.
- Go into certificate (probably pending). Click Create Records in Route53
- Copy the ARN from the certificate detail page. Paste it into `config.auto.tfvars.json` as the value for `config.alb.certificate_arn`

## Generate configuration files

To get the app ready to deploy into your AWS account, generate settings files from templates and populate them with your custom environment settings.

- In the `terraform/invocation` directory, copy the template file `main.tf.tpl` to `main.tf`.
- In the `terraform/invocation` directory, copy the template file `config.auto.tfvars.json.tpl` to `config.auto.tfvars.json`.
- Customize `config.alb.certificate_arn` with the ARN of the certificate you generated for your domain.
- In the `terraform/invocation` directory, copy the template file `secrets.auto.tfvars.json.tpl` to `secrets.auto.tfvars.json`.
- Fill out values including your OKTA.
- Input correct values for each template variable in `main.tf` (see comments in the file for details).
- Include the bucket ID and table information from the step above.
- Generate `config.auto.tfvars.json` and `secrets.auto.tfvars.json` in the same directory.
- Begin populating templates with data.

## Run terraform to create cloud resources

- The goal of the user is to generate terraform code starting point (`main.tf`) that is ready to run against their AWS account.
- Terraform needs to look at your config before it's able to do fancy terraform stuff, like generating "var.config" from stored state.
- Navigate to `/infra/terraform/invocation`. Run command `terraform init`. Troubleshoot any errors that appear (typically means missing or incorrect configuration)
- Observe resources to be created: `terraform plan`
- Apply plan: `terraform apply`

Observe the resources being created in your AWS Account. This will result in a version of the application

## Cleanup: Take down the infrastructure (don't pay for something you're not using)

For test environments, it's important to not pay for resources you're not using. Here is how to turn off your AWS resources:

- Remove `enable_deletion_protection` (e.g. in `config.auto.tfvars.json`)
- Invoke terraform to tear down resources: `terraform destroy`
58 changes: 58 additions & 0 deletions infra/aws/terraform/invocation/config.auto.tfvars.json.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"config": {
"alb": {
"container_name": "app",
"enable_deletion_protection": true,
"certificate_arn": "<Copy from Route53, the certificate for your domain>"
},
"ecs": {
"task": {
"app": {
"cpu": 2048,
"desired_count": 2,
"docker_image": "wguopensource/osmt-app",
"docker_tag": "latest",
"memory": 8192
},
"elasticsearch": {
"cpu": 2048,
"desired_count": 1,
"docker_tag": "7.17.4",
"docker_image": "docker.elastic.co/elasticsearch/elasticsearch",
"memory": 4096
}
}
},
"env": "test",
"elasticache": {
"engine_version": "6.x",
"node_type": "cache.t3.small",
"parameter_group_name": "default.redis6.x"
},
"rds": {
"db_name": "osmt",
"engine_version": "8.0.32",
"instance_class": "db.t3.small",
"multi_az": true,
"parameter_group_family": "mysql8.0",
"skip_final_snapshot": false
},
"vpc": {
"azs": [
"us-west-2a",
"us-west-2b"
],
"subnets": {
"private": [
"192.168.100.0/27",
"192.168.100.64/27"
],
"public": [
"192.168.100.128/27",
"192.168.100.192/27"
]
},
"vpc_cidr": "192.168.100.0/24"
}
}
}
28 changes: 28 additions & 0 deletions infra/aws/terraform/invocation/main.tf.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
terraform {
backend "s3" {
bucket = "<your_s3_bucket_name>"
dynamodb_table = "<your_dynamodb_table_name>"
encrypt = true
key = "<state_file_s3_object_key>"
profile = "<your_aws_profile_name>"
region = "<aws_region>"
}
}

provider "aws" {
profile = "<your_aws_profile_name>"
region = "<aws_region>"

default_tags {
tags = {
TerraformModule = "OSMT"
}
}
}

module "osmt" {
source = "../module/"

config = var.config
secrets = var.secrets
}
20 changes: 20 additions & 0 deletions infra/aws/terraform/invocation/secrets.auto.tfvars.json.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"secrets": {
"rds": {
"master_password": "<mysql_8.0_password>",
"master_username": "<mysql_8.0_username>"
},
"app": {
"base_domain": "<e.g. base_domain.net>",
"environment": "apiserver,oauth2-okta",
"frontend_url": "<e.g. https://base_domain.net>",
"oauth_issuer": "<oauth_issuer e.g. https://dev-82064468.okta.com/oauth2/default>",
"oauth_clientid": "<oauch_clientid>",
"oauth_secret": "<oauth_secret>>",
"oauth_audience": "<oauth_audience>",
"migrations_enabled": "true",
"reindex_elasticsearch": "true",
"skip_metadata_import": "false"
}
}
}
3 changes: 3 additions & 0 deletions infra/aws/terraform/invocation/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
variable "config" {}

variable "secrets" {}
58 changes: 58 additions & 0 deletions infra/aws/terraform/module/alb.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
resource "aws_lb" "this" {
name = local.identity-prefix
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = module.vpc.public_subnets

enable_deletion_protection = var.config.alb.enable_deletion_protection
}


resource "aws_lb_target_group" "this" {
name = local.identity-prefix
port = 80
protocol = "HTTP"
target_type = "ip"
vpc_id = module.vpc.vpc_id

health_check {
healthy_threshold = 3
interval = 15
path = "/"
port = 80
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 2
}
}


resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.this.arn
port = 80
protocol = "HTTP"

default_action {
type = "redirect"

redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}


resource "aws_lb_listener" "https" {
certificate_arn = var.config.alb.certificate_arn
load_balancer_arn = aws_lb.this.arn
port = 443
protocol = "HTTPS"

default_action {
type = "forward"
target_group_arn = aws_lb_target_group.this.arn
}
}
7 changes: 7 additions & 0 deletions infra/aws/terraform/module/cloudwatch.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "aws_cloudwatch_log_group" "app" {
name = "${local.identity-prefix}-app"
}

resource "aws_cloudwatch_log_group" "elasticsearch" {
name = "${local.identity-prefix}-elasticsearch"
}
49 changes: 49 additions & 0 deletions infra/aws/terraform/module/config.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
variable "config" {
type = object({
alb = object({
certificate_arn = string
container_name = string
enable_deletion_protection = bool
})
ecs = object({
task = object({
app = object({
cpu = number
desired_count = number
docker_image = string
docker_tag = string
memory = number
})
elasticsearch = object({
cpu = number
desired_count = number
docker_image = string
docker_tag = string
memory = number
})
})
})
env = string
elasticache = object({
engine_version = string
node_type = string
parameter_group_name = string
})
rds = object({
db_name = string
engine_version = string
instance_class = string
multi_az = bool
parameter_group_family = string
skip_final_snapshot = bool
})
vpc = object({
azs = list(string)
subnets = object({
private = list(string)
public = list(string)
})
vpc_cidr = string
})
})
}
6 changes: 6 additions & 0 deletions infra/aws/terraform/module/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
data "aws_region" "current" {}


data "aws_iam_policy" "ecs_task_execution" {
name = "AmazonECSTaskExecutionRolePolicy"
}
Loading