Skip to content

Instantly share code, notes, and snippets.

@johnmcdowall
Created November 16, 2023 03:34
Show Gist options
  • Save johnmcdowall/dd8414cc43b39f3a9c70d0e6093f246a to your computer and use it in GitHub Desktop.
Save johnmcdowall/dd8414cc43b39f3a9c70d0e6093f246a to your computer and use it in GitHub Desktop.
The Terraform config to setup a managed LB, Firewall, PSQL DB, Redis and droplets on DO, with bonus ECR on AWS.
resource "aws_ecr_repository" "repo" {
name = "${var.product_name}-app"
}
resource "aws_ecr_lifecycle_policy" "ecr-lifecycle-policy" {
repository = aws_ecr_repository.repo.name
policy = jsonencode({
rules = [
{
rulePriority = 1
description = "Keep last 20 images"
selection = {
tagStatus = "any",
countType = "imageCountMoreThan",
countNumber = 20
}
action = {
type = "expire"
}
}
]
})
}
# This config assumes Cloudflare is in front of the LB doing TLS termination.
variable "do_token" {
sensitive = true
}
variable "ssh_key_names" {
sensitive = true
}
variable "digitalocean_region" {
description = "For example: nyc1, nyc2, ams2, ams3, fra2"
default = "sfo3"
}
# Name of your project. Will be prepended to most resources
variable "product_name" {
type = string
default = "your-app"
}
# The size we want our droplets to be.
# Can view slugs (valid options) https://slugs.do-api.dev/
variable "droplet_size" {
type = string
default = "s-1vcpu-1gb"
}
# The size we want our database images to be to be.
variable "database_size" {
type = string
default = "db-s-1vcpu-1gb"
}
# The size we want our redis images to be to be.
variable "redis_size" {
type = string
default = "db-s-1vcpu-1gb"
}
data "digitalocean_ssh_keys" "keys" {
filter {
key = "name"
values = var.ssh_key_names
}
}
# Create a VPC
resource "digitalocean_vpc" "vpc" {
name = "${var.product_name}-vpc"
region = var.digitalocean_region
ip_range = "192.168.0.0/16"
}
resource "digitalocean_droplet" "web" {
count = 2
image = "docker-20-04"
name = "${var.product_name}-${var.digitalocean_region}-webworker-${count.index + 1}"
region = var.digitalocean_region
size = var.droplet_size
tags = ["${var.product_name}-web", "production"]
monitoring = true
vpc_uuid = digitalocean_vpc.vpc.id
ssh_keys = data.digitalocean_ssh_keys.keys.ssh_keys[*].id
#-----------------------------------------------------------------------------------------------#
# Ensures that we create the new resource before we destroy the old one #
# https://www.terraform.io/docs/configuration/resources.html#lifecycle-lifecycle-customizations #
#-----------------------------------------------------------------------------------------------#
lifecycle {
create_before_destroy = true
}
}
resource "digitalocean_droplet" "worker" {
count = 1
image = "docker-20-04"
name = "${var.product_name}-${var.digitalocean_region}-worker-${count.index + 1}"
region = var.digitalocean_region
size = var.droplet_size
tags = ["${var.product_name}-worker", "production"]
monitoring = true
vpc_uuid = digitalocean_vpc.vpc.id
ssh_keys = data.digitalocean_ssh_keys.keys.ssh_keys[*].id
#-----------------------------------------------------------------------------------------------#
# Ensures that we create the new resource before we destroy the old one #
# https://www.terraform.io/docs/configuration/resources.html#lifecycle-lifecycle-customizations #
#-----------------------------------------------------------------------------------------------#
lifecycle {
create_before_destroy = true
}
}
resource "digitalocean_firewall" "web" {
name = "production-firewall"
droplet_ids = flatten([digitalocean_droplet.web.*.id, digitalocean_droplet.worker.*.id])
#--------------------------------------------------------------------------#
# Internal VPC Rules. We have to let ourselves talk to each other #
#--------------------------------------------------------------------------#
inbound_rule {
protocol = "tcp"
port_range = "1-65535"
source_addresses = [digitalocean_vpc.vpc.ip_range]
}
inbound_rule {
protocol = "udp"
port_range = "1-65535"
source_addresses = [digitalocean_vpc.vpc.ip_range]
}
inbound_rule {
protocol = "icmp"
source_addresses = [digitalocean_vpc.vpc.ip_range]
}
inbound_rule {
protocol = "tcp"
port_range = "22"
source_addresses = ["0.0.0.0/0", "::/0"]
}
inbound_rule {
protocol = "tcp"
port_range = "80"
source_addresses = [digitalocean_vpc.vpc.ip_range]
}
outbound_rule {
protocol = "udp"
port_range = "1-65535"
destination_addresses = [digitalocean_vpc.vpc.ip_range]
}
outbound_rule {
protocol = "tcp"
port_range = "1-65535"
destination_addresses = [digitalocean_vpc.vpc.ip_range]
}
outbound_rule {
protocol = "icmp"
destination_addresses = [digitalocean_vpc.vpc.ip_range]
}
#--------------------------------------------------------------------------#
# Selective Outbound Traffic Rules #
#--------------------------------------------------------------------------#
# DNS
outbound_rule {
protocol = "udp"
port_range = "53"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
# HTTP
outbound_rule {
protocol = "tcp"
port_range = "80"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
# HTTPS
outbound_rule {
protocol = "tcp"
port_range = "443"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
# Email
outbound_rule {
protocol = "tcp"
port_range = "587"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
# ICMP (Ping)
outbound_rule {
protocol = "icmp"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
}
resource "digitalocean_loadbalancer" "public" {
name = "${var.product_name}-production-loadbalancer-1"
region = "${var.digitalocean_region}"
forwarding_rule {
entry_port = 80
entry_protocol = "http"
target_port = 80
target_protocol = "http"
}
healthcheck {
port = 80
protocol = "tcp"
}
droplet_ids = toset(digitalocean_droplet.web.*.id)
vpc_uuid = digitalocean_vpc.vpc.id
#-----------------------------------------------------------------------------------------------#
# Ensures that we create the new resource before we destroy the old one #
# https://www.terraform.io/docs/configuration/resources.html#lifecycle-lifecycle-customizations #
#-----------------------------------------------------------------------------------------------#
lifecycle {
create_before_destroy = true
}
}
resource "digitalocean_database_cluster" "primary" {
name = "${var.product_name}-primary-psql-cluster"
engine = "pg"
version = "15"
size = var.database_size
region = var.digitalocean_region
private_network_uuid = digitalocean_vpc.vpc.id
node_count = 1
}
resource "digitalocean_database_db" "primary" {
cluster_id = digitalocean_database_cluster.primary.id
name = "${var.product_name}-primary-db"
}
resource "digitalocean_database_firewall" "web" {
cluster_id = digitalocean_database_cluster.primary.id
rule {
type = "tag"
value = "${var.product_name}-web"
}
rule {
type = "tag"
value = "${var.product_name}-worker"
}
}
resource "digitalocean_database_cluster" "redis" {
name = "${var.product_name}-primary-redis-cluster"
engine = "redis"
version = "7"
size = var.redis_size
region = var.digitalocean_region
eviction_policy = "allkeys_lru"
private_network_uuid = digitalocean_vpc.vpc.id
node_count = 1
}
resource "digitalocean_database_firewall" "redis-firewall" {
cluster_id = digitalocean_database_cluster.redis.id
rule {
type = "tag"
value = "${var.product_name}-web"
}
rule {
type = "tag"
value = "${var.product_name}-worker"
}
}
resource "digitalocean_project" "your-app" {
name = "Your App"
description = "Your App Production Environment."
purpose = "Web Application"
environment = "Production"
}
resource "digitalocean_project_resources" "your-app" {
project = digitalocean_project.your-app.id
resources = flatten([
digitalocean_droplet.web.*.urn,
digitalocean_droplet.worker.*.urn,
digitalocean_loadbalancer.public.urn,
digitalocean_database_cluster.primary.urn,
digitalocean_database_cluster.redis.urn,
])
}
terraform {
required_version = "~> 1.4.6"
required_providers {
aws = "~> 3.63"
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.0"
}
}
}
provider "aws" {
region = "us-west-1"
shared_credentials_file = "$HOME/.aws/credentials"
profile = "default"
skip_region_validation = true
}
provider "digitalocean" {
token = var.do_token
}
output "ecr_repo_url" {
description = "AWS ECR Repo URL"
value = aws_ecr_repository.repo.repository_url
}
output "web_droplet_ip_address" {
description = "Digital Ocean Droplet IPs"
value = digitalocean_droplet.web.*.ipv4_address
}
output "worker_droplet_ip_address" {
description = "Digital Ocean Droplet IPs"
value = digitalocean_droplet.worker.*.ipv4_address
}
output "database_cluster_url" {
description = "Digital Ocean PGSQL Cluster URL"
sensitive = true
value = digitalocean_database_cluster.primary.uri
}
output "redis_cluster_url" {
description = "Digital Ocean Redis Cluster URL"
sensitive = true
value = digitalocean_database_cluster.redis.uri
}
do_token="<your do token>"
ssh_key_names=["<name of ssh key already enabled at DO>"]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment