From cbf4e851e24fb8b1d2321224073fd2588804a451 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 9 Feb 2020 22:28:45 -0500 Subject: [PATCH] Porting over most of the things --- app.env.tpl | 1 + main.tf | 188 ++++++++++++++++++++++++++++++++++++++++++++ outputs.tf | 19 +++++ spoke-app-provision | 123 +++++++++++++++++++++++++++++ spoke-app-run | 12 +++ spoke.service | 14 ++++ variables.tf | 61 ++++++++++++++ 7 files changed, 418 insertions(+) create mode 100644 app.env.tpl create mode 100755 spoke-app-provision create mode 100755 spoke-app-run create mode 100644 spoke.service diff --git a/app.env.tpl b/app.env.tpl new file mode 100644 index 0000000..4640904 --- /dev/null +++ b/app.env.tpl @@ -0,0 +1 @@ +# TODO diff --git a/main.tf b/main.tf index e69de29..3584f49 100644 --- a/main.tf +++ b/main.tf @@ -0,0 +1,188 @@ +resource "digitalocean_ssh_key" "app" { + count = length(var.ssh_keys) + name = "${var.resource_prefix}app-${count.index}" + public_key = element(var.ssh_keys, count.index) +} + +resource "digitalocean_database_cluster" "pg" { + name = "${var.resource_prefix}pg" + engine = "pg" + version = "11" + size = var.database_cluster_size + region = var.region + node_count = 1 +} + +resource "digitalocean_droplet" "app" { + image = "ubuntu-18-04-x64" + name = "${var.resource_prefix}app" + region = var.region + size = var.droplet_size + + ssh_keys = [digitalocean_ssh_key.app.*.id] +} + +resource "digitalocean_certificate" "app" { + name = "${var.resource_prefix}app" + private_key = var.cert_private_key + leaf_certificate = var.cert_leaf_certificate + + lifecycle { + create_before_destroy = true + } +} + +resource "digitalocean_loadbalancer" "app" { + name = "${var.resource_prefix}lb-app" + region = var.region + droplet_ids = [digitalocean_droplet.app.id] + redirect_http_to_https = true + + forwarding_rule { + entry_port = 80 + entry_protocol = "http" + + target_port = var.port + target_protocol = "http" + } + + forwarding_rule { + entry_port = 443 + entry_protocol = "https" + + target_port = var.port + target_protocol = "http" + + certificate_id = digitalocean_certificate.app.id + } + + healthcheck { + port = var.port + protocol = "tcp" + } +} + +resource "digitalocean_firewall" "app" { + name = "${var.resource_prefix}app" + + droplet_ids = [digitalocean_droplet.app.id] + + inbound_rule { + protocol = "tcp" + port_range = "22" + source_addresses = ["0.0.0.0/0", "::/0"] + } + + inbound_rule { + protocol = "tcp" + port_range = "1-65535" + # FIXME: what + #port_range = var.port + source_load_balancer_uids = [digitalocean_loadbalancer.app.id] + } + + inbound_rule { + protocol = "icmp" + source_addresses = ["0.0.0.0/0", "::/0"] + } + + outbound_rule { + protocol = "tcp" + port_range = "1-65535" + destination_addresses = ["0.0.0.0/0", "::/0"] + } + + outbound_rule { + protocol = "udp" + port_range = "1-65535" + destination_addresses = ["0.0.0.0/0", "::/0"] + } + + outbound_rule { + protocol = "icmp" + destination_addresses = ["0.0.0.0/0", "::/0"] + } +} + +resource "random_string" "session_secret" { + length = 199 + special = false +} + +resource "null_resource" "app_provision" { + triggers = { + droplet_id = digitalocean_droplet.app.id + database_cluster_id = digitalocean_database_cluster.pg.id + provision_script_sha1 = filesha1("spoke-app-provision") + run_script_sha1 = filesha1("spoke-app-run") + service_sha1 = filesha1("spoke.service") + env_sha1 = sha1(join(";", [ + jsonencode(var.env), + random_string.session_secret.result, + var.base_url, + var.node_env, + var.node_options, + var.port, + ])) + } + + connection { + host = digitalocean_droplet.app.ipv4_address + } + + provisioner "file" { + source = "spoke-app-provision" + destination = "/tmp/spoke-app-provision" + } + + provisioner "file" { + source = "spoke-app-run" + destination = "/tmp/spoke-app-run" + } + + provisioner "file" { + content = templatefile("app.env.tpl", merge({ + ASSETS_MAP_FILE = "assets.json", + ASSETS_DIR = "./build/client/assets", + BASE_URL = var.base_url, + DATABASE_URL = digitalocean_database_cluster.pg.uri, + DB_HOST = digitalocean_database_cluster.pg.host, + DB_NAME = digitalocean_database_cluster.pg.database, + DB_PASSWORD = digitalocean_database_cluster.pg.password, + DB_PORT = digitalocean_database_cluster.pg.port, + DB_TYPE = "pg", + DB_USER = digitalocean_database_cluster.pg.user, + DB_USE_SSL = "true", + JOBS_SAME_PROCESS = "1", + NODE_ENV = var.node_env, + NODE_OPTIONS = var.node_options, + OUTPUT_DIR = "./build", + PORT = var.port, + REDIS_URL = "redis://127.0.0.1:6379/0", + SESSION_SECRET = random_string.session_secret.result, + }, var.env)) + destination = "/tmp/app.env" + } + + provisioner "file" { + source = "spoke.service" + destination = "/tmp/spoke.service" + } + + provisioner "remote-exec" { + inline = [ + "bash /tmp/spoke-app-provision system0", + "sudo -H -u spoke bash /tmp/spoke-app-provision spoke0", + "bash /tmp/spoke-app-provision system1", + ] + } +} + +resource "digitalocean_database_firewall" "app_pg" { + cluster_id = digitalocean_database_cluster.pg.id + + rule { + type = "droplet" + value = digitalocean_droplet.app.id + } +} diff --git a/outputs.tf b/outputs.tf index e69de29..4877721 100644 --- a/outputs.tf +++ b/outputs.tf @@ -0,0 +1,19 @@ +output "loadbalancer_ip" { + value = digitalocean_loadbalancer.app.ip +} + +output "droplet_urn" { + value = digitalocean_droplet.app.urn +} + +output "loadbalancer_urn" { + value = digitalocean_loadbalancer.app.urn +} + +output "database_cluster_urn" { + value = digitalocean_database_cluster.pg.urn +} + +output "droplet_ipv4_address" { + value = digitalocean_droplet.app.ipv4_address +} diff --git a/spoke-app-provision b/spoke-app-provision new file mode 100755 index 0000000..5c8a008 --- /dev/null +++ b/spoke-app-provision @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +set -o errexit +set -o pipefail + +main() { + local target="${1:-system0}" + "_run_${target}" +} + +_run_system0() { + set -o xtrace + + sudo swapon --show | if ! grep -q /swap; then + sudo fallocate -l 8G /swap + sudo chmod 600 /swap + sudo mkswap -L swap /swap + sudo swapon /swap + fi + + if ! grep -q ^LABEL=swap /etc/fstab &>/dev/null; then + echo 'LABEL=swap none swap sw 0 0' | sudo tee -a /etc/fstab + fi + + sudo sysctl vm.swappiness=10 + echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swappiness.conf + + sudo sysctl vm.vfs_cache_pressure=50 + echo 'vm.vfs_cache_pressure=50' | + sudo tee /etc/sysctl.d/99-cache-pressure.conf + + sudo apt-get update -y + sudo apt-get install -y \ + build-essential \ + ca-certificates \ + curl \ + git \ + gnupg \ + redis + + sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" >/etc/apt/sources.list.d/pgdg.list' + curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - + sudo apt-get update -y + sudo apt-get install -y postgresql-client-11 + + curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - + echo "deb https://dl.yarnpkg.com/debian/ stable main" | + sudo tee /etc/apt/sources.list.d/yarn.list + sudo apt-get update -y + sudo apt-get install -y --no-install-recommends yarn + + if ! getent passwd spoke; then + sudo useradd --create-home --comment 'Spoke app' spoke + fi + + sudo chsh -s /bin/bash spoke + sudo chown -R spoke:spoke /home/spoke +} + +_run_system1() { + set -o xtrace + sudo cp -v /tmp/spoke.service /etc/systemd/system/spoke.service + + sudo systemctl enable spoke + sudo systemctl stop spoke || true + sudo systemctl start spoke || true +} + +_run_spoke0() { + set -o xtrace + + git --version + if [[ ! -d /home/spoke/app/.git ]]; then + git clone https://github.com/MoveOnOrg/Spoke.git /home/spoke/app + fi + cd /home/spoke/app + git checkout -qf 'v5.1' + + cp -v /tmp/spoke-app-run /home/spoke/spoke-app-run + chmod +x /home/spoke/spoke-app-run + + if ! command -v nvm; then + curl -fsSL \ + https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash + set +o errexit + set +o xtrace + source ~/.nvm/nvm.sh + set -o xtrace + set -o errexit + fi + nvm --version 2>/dev/null + nvm install 2>/dev/null + nvm use 2>/dev/null + + if [[ -f /tmp/app.env ]]; then + cp -v /tmp/app.env /home/spoke/app/.env + fi + sha1sum /home/spoke/app/.env + + set -o allexport + source /home/spoke/app/.env + set +o allexport + + yarn --version + yarn install --ignore-scripts --non-interactive --frozen-lockfile + + local git_head + git_head="$(cat .git/HEAD || true)" + local yarn_prod_build_ref + yarn_prod_build_ref="$( + cat /home/spoke/yarn_prod_build_ref 2>/dev/null || true + )" + if [[ "${git_head}" == "${yarn_prod_build_ref}" ]]; then + echo "skipping yarn run prod-build" + return + fi + + yarn run prod-build + rm -rf ./node_modules + yarn install --production --ignore-scripts + echo "${git_ref}" >/home/spoke/yarn_prod_build_ref +} + +main "${@}" diff --git a/spoke-app-run b/spoke-app-run new file mode 100755 index 0000000..3c772b5 --- /dev/null +++ b/spoke-app-run @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -o errexit +set -o pipefail + +main() { + cd /home/spoke/app + source ~/.nvm/nvm.sh + nvm use + exec npm start +} + +main "${@}" diff --git a/spoke.service b/spoke.service new file mode 100644 index 0000000..3f6db44 --- /dev/null +++ b/spoke.service @@ -0,0 +1,14 @@ +# vim:filetype=systemd +[Unit] +Description=Spoke + +[Service] +Type=simple +User=spoke +Group=spoke +EnvironmentFile=-/home/spoke/app/.env +ExecStart=/home/spoke/spoke-app-run +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/variables.tf b/variables.tf index e69de29..0aa9b32 100644 --- a/variables.tf +++ b/variables.tf @@ -0,0 +1,61 @@ +variable "base_url" { + description = "Fully qualified https URL of the app" +} + +variable "resource_prefix" { + description = "Prefix prepended to resource names" + default = "spoke-" +} + +variable "node_options" { + description = "Value defined at build time and run time as NODE_OPTIONS" + default = "--max_old_space_size=8192" +} + +variable "node_env" { + description = "Value defined at build time and run time as NODE_ENV" + default = "production" +} + +variable "port" { + description = "TCP port used to communicate between droplet and load balancer" + default = "3000" +} + +variable "droplet_size" { + description = "Size value passed when provisioning app droplet" + default = "s-1vcpu-1gb" +} + +variable "database_cluster_size" { + description = "Size value passed when provisioning database cluster" + default = "db-s-1vcpu-1gb" +} + +variable "database_cluster_node_count" { + default = 1 +} + +variable "region" { + description = "Region at which all resources will be provisioned" + default = "nyc1" +} + +variable "ssh_keys" { + type = "list" + description = "List of ssh public keys to pass to droplet provisioning" +} + +variable "cert_private_key" { + description = "Certificate key to use when defining th cert used with the load balancer" +} + +variable "cert_leaf_certificate" { + description = "Leaf certificate to use when defining the cert used with the load balancer" +} + +variable "env" { + type = "map" + description = "Arbitrary *additional* environment variables passed at build time and run time" + default = {} +}