From 49428930d40c321cd67408ebc62651c7a0172470 Mon Sep 17 00:00:00 2001 From: Quinn Grier Date: Sun, 2 Feb 2025 19:07:49 -0800 Subject: [PATCH] Add the "Bring Your Own" functionality The basic setup is with Docker Compose, and Terraform is added on top for doing a public cloud setup. Each component is given its own Compose and Terraform setup. HTTP-only can be used for private setups, and HTTPS with automatic certificate renewal can be used for public cloud setups. The code is structured to allow support for multiple cloud providers. This commit only adds support for Amazon AWS as a starting point. The directory gets packaged as divviup-bring-your-own.tar.gz and added to the release artifacts via the existing GitHub Actions workflow. The Docker image versions for divviup* and janus* sit on :latest in the repository, but get pinned in the release artifact. There is also a companion commit for the divviup/public-docs repository with documentation on how to use divviup-bring-your-own.tar.gz. --- .github/workflows/cli-release.yml | 80 ++++++ bring-your-own/aggregator/.env | 40 +++ bring-your-own/aggregator/docker-compose.yml | 215 ++++++++++++++++ bring-your-own/aggregator/generate-keys.sh | 58 +++++ bring-your-own/application/.env | 48 ++++ bring-your-own/application/docker-compose.yml | 152 ++++++++++++ bring-your-own/application/generate-keys.sh | 58 +++++ bring-your-own/terraform/aws.tf | 231 ++++++++++++++++++ bring-your-own/terraform/aws.tfvars | 9 + bring-your-own/terraform/provision.bash | 53 ++++ 10 files changed, 944 insertions(+) create mode 100644 bring-your-own/aggregator/.env create mode 100644 bring-your-own/aggregator/docker-compose.yml create mode 100644 bring-your-own/aggregator/generate-keys.sh create mode 100644 bring-your-own/application/.env create mode 100644 bring-your-own/application/docker-compose.yml create mode 100644 bring-your-own/application/generate-keys.sh create mode 100644 bring-your-own/terraform/aws.tf create mode 100644 bring-your-own/terraform/aws.tfvars create mode 100644 bring-your-own/terraform/provision.bash diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index 00d556d87..c403b1874 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -15,6 +15,86 @@ env: CARGO_TERM_COLOR: always jobs: + get-api-version: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Get API version + id: get-v + run: | + set -E -e -u -o pipefail || exit $? + trap exit ERR + v=${{ inputs.tag }} + if [[ ! $v ]]; then + if [[ $GITHUB_REF_TYPE != tag ]]; then + printf '%s\n' "GITHUB_REF=${GITHUB_REF@Q} is not a tag" >&2 + exit 1 + fi + v=$GITHUB_REF_NAME + fi + printf '%s\n' v=$v >>$GITHUB_OUTPUT + outputs: + v: ${{ steps.get-v.outputs.v }} + + get-janus-version: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Get Janus version + id: get-v + run: | + set -E -e -u -o pipefail || exit $? + trap exit ERR + git clone https://github.com/divviup/janus.git + cd janus + v=$(git describe --tags --abbrev=0) + printf '%s\n' v=$v >>$GITHUB_OUTPUT + outputs: + v: ${{ steps.get-v.outputs.v }} + + upload-bring-your-own: + needs: + - get-api-version + - get-janus-version + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Pick directory name + id: pick-d + run: | + d=divviup-bring-your-own + printf '%s\n' d=$d >>$GITHUB_OUTPUT + - name: Create archive + run: | + set -E -e -u -o pipefail || exit $? + trap exit ERR + d=${{ steps.pick-d.outputs.d }} + v=${{ needs.get-api-version.outputs.v }} + jv=${{ needs.get-janus-version.outputs.v }} + mv bring-your-own $d + sed -i " + /DIVVIUP/ s|:latest\$|:$v| + /JANUS/ s|:latest\$|:$jv| + " $d/*/.env + tar czf $d.tar.gz $d + - name: Publish archive + env: + GH_TOKEN: ${{ secrets.DIVVIUP_GITHUB_AUTOMATION_RELEASE_PAT }} + run: | + set -E -e -u -o pipefail || exit $? + trap exit ERR + d=${{ steps.pick-d.outputs.d }} + v=${{ needs.get-api-version.outputs.v }} + gh release upload $v $d.tar.gz + upload-compose-yaml: runs-on: ubuntu-latest steps: diff --git a/bring-your-own/aggregator/.env b/bring-your-own/aggregator/.env new file mode 100644 index 000000000..1bd8332f8 --- /dev/null +++ b/bring-your-own/aggregator/.env @@ -0,0 +1,40 @@ +# Profile +COMPOSE_PROFILES=http + +# HTTP +LISTEN_HOST=0.0.0.0 +LISTEN_PORT_HTTP=9001 +PUBLIC_HOST=host.docker.internal +PUBLIC_PORT_HTTP=9001 + +# HTTPS +ACME_CA_URI=https://acme-v02.api.letsencrypt.org/directory +HTTPS_ADMIN_EMAIL=example@example.com +LISTEN_PORT_HTTPS=443 +PUBLIC_PORT_HTTPS=443 + +# Docker images +JANUS_AGGREGATOR_IMAGE=us-west2-docker.pkg.dev/divviup-artifacts-public/janus/janus_aggregator:latest +JANUS_MIGRATOR_IMAGE=us-west2-docker.pkg.dev/divviup-artifacts-public/janus/janus_db_migrator:latest +POSTGRES_IMAGE=postgres:latest +NGINX_PROXY_IMAGE=nginxproxy/nginx-proxy:latest +ACME_COMPANION_IMAGE=nginxproxy/acme-companion:latest + +# Postgres +POSTGRES_DB=janus +POSTGRES_USER=postgres +#POSTGRES_PASSWORD= + +# Database key +#DATASTORE_KEYS= + +# Aggregator API keys +#AGGREGATOR_API_AUTH_TOKENS= + +# Aggregator settings +AGGREGATOR_API_PATH_PREFIX=api +MIN_AGGREGATION_JOB_SIZE=10 +MAX_AGGREGATION_JOB_SIZE=500 + +# Restart policy +RESTART_POLICY=unless-stopped diff --git a/bring-your-own/aggregator/docker-compose.yml b/bring-your-own/aggregator/docker-compose.yml new file mode 100644 index 000000000..e9f573403 --- /dev/null +++ b/bring-your-own/aggregator/docker-compose.yml @@ -0,0 +1,215 @@ +x-janus-common: &janus-common + depends_on: + janus-migrator: + condition: service_completed_successfully + image: ${JANUS_AGGREGATOR_IMAGE?} + healthcheck: + test: wget -O - http://127.0.0.1:8000/healthz + start_period: 60s + restart: ${RESTART_POLICY?} + +x-janus-environment: &janus-environment + RUST_LOG: info + DATASTORE_KEYS: ${DATASTORE_KEYS?} + AGGREGATOR_API_AUTH_TOKENS: ${AGGREGATOR_API_AUTH_TOKENS?} + +services: + + postgres: + image: ${POSTGRES_IMAGE} + environment: + POSTGRES_DB: ${POSTGRES_DB?} + POSTGRES_USER: ${POSTGRES_USER?} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD?} + healthcheck: + test: pg_isready -U ${POSTGRES_USER?} -d ${POSTGRES_DB?} + start_period: 60s + restart: ${RESTART_POLICY?} + + janus-migrator: + depends_on: + postgres: + condition: service_healthy + image: ${JANUS_MIGRATOR_IMAGE?} + environment: + DATABASE_URL: postgres://${POSTGRES_USER?}:${POSTGRES_PASSWORD?}@postgres:5432/${POSTGRES_DB?} + command: migrate run --source /migrations --connect-timeout 60 + + janus-aggregator-http: + <<: *janus-common + profiles: + - http + configs: + - janus-aggregator-http.yml + extra_hosts: + host.docker.internal: host-gateway + environment: + <<: *janus-environment + CONFIG_FILE: /janus-aggregator-http.yml + entrypoint: /janus_aggregator aggregator + ports: + - "${LISTEN_HOST?}:${LISTEN_PORT_HTTP?}:80" + + janus-aggregator-https: + <<: *janus-common + profiles: + - https + configs: + - janus-aggregator-https.yml + environment: + <<: *janus-environment + CONFIG_FILE: /janus-aggregator-https.yml + LETSENCRYPT_EMAIL: ${HTTPS_ADMIN_EMAIL?} + LETSENCRYPT_HOST: ${PUBLIC_HOST?} + VIRTUAL_HOST: ${PUBLIC_HOST?} + entrypoint: /janus_aggregator aggregator + + janus-aggregation-job-creator: + <<: *janus-common + configs: + - janus-aggregation-job-creator.yml + extra_hosts: + host.docker.internal: host-gateway + environment: + <<: *janus-environment + CONFIG_FILE: /janus-aggregation-job-creator.yml + entrypoint: /janus_aggregator aggregation_job_creator + + janus-aggregation-job-driver: + <<: *janus-common + configs: + - janus-aggregation-job-driver.yml + extra_hosts: + host.docker.internal: host-gateway + environment: + <<: *janus-environment + CONFIG_FILE: /janus-aggregation-job-driver.yml + entrypoint: /janus_aggregator aggregation_job_driver + + janus-collection-job-driver: + <<: *janus-common + configs: + - janus-collection-job-driver.yml + extra_hosts: + host.docker.internal: host-gateway + environment: + <<: *janus-environment + CONFIG_FILE: /janus-collection-job-driver.yml + entrypoint: /janus_aggregator collection_job_driver + + janus-garbage-collector: + <<: *janus-common + configs: + - janus-garbage-collector.yml + extra_hosts: + host.docker.internal: host-gateway + environment: + <<: *janus-environment + CONFIG_FILE: /janus-garbage-collector.yml + entrypoint: /janus_aggregator garbage_collector + + nginx-proxy: + profiles: + - https + image: ${NGINX_PROXY_IMAGE?} + ports: + - "${LISTEN_HOST?}:${LISTEN_PORT_HTTP?}:80" + - "${LISTEN_HOST?}:${LISTEN_PORT_HTTPS?}:443" + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - certs:/etc/nginx/certs + - html:/usr/share/nginx/html + restart: ${RESTART_POLICY?} + + acme-companion: + profiles: + - https + image: ${ACME_COMPANION_IMAGE?} + environment: + ACME_CA_URI: ${ACME_CA_URI?} + DEFAULT_EMAIL: ${HTTPS_ADMIN_EMAIL?} + volumes_from: + - nginx-proxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - acme:/etc/acme.sh + restart: ${RESTART_POLICY?} + +volumes: + acme: + certs: + html: + +configs: + + janus-aggregator-http.yml: + content: | + database: + url: postgres://${POSTGRES_USER?}:${POSTGRES_PASSWORD?}@postgres:5432/${POSTGRES_DB?} + health_check_listen_address: 0.0.0.0:8000 + listen_address: 0.0.0.0:80 + max_upload_batch_size: 100 + max_upload_batch_write_delay_ms: 250 + batch_aggregation_shard_count: 32 + aggregator_api: + public_dap_url: http://${PUBLIC_HOST?}:${PUBLIC_PORT_HTTP?} + path_prefix: ${AGGREGATOR_API_PATH_PREFIX?} + + janus-aggregator-https.yml: + content: | + database: + url: postgres://${POSTGRES_USER?}:${POSTGRES_PASSWORD?}@postgres:5432/${POSTGRES_DB?} + health_check_listen_address: 0.0.0.0:8000 + listen_address: 0.0.0.0:80 + max_upload_batch_size: 100 + max_upload_batch_write_delay_ms: 250 + batch_aggregation_shard_count: 32 + aggregator_api: + public_dap_url: https://${PUBLIC_HOST?}:${PUBLIC_PORT_HTTPS?} + path_prefix: ${AGGREGATOR_API_PATH_PREFIX?} + + janus-aggregation-job-creator.yml: + content: | + database: + url: postgres://${POSTGRES_USER?}:${POSTGRES_PASSWORD?}@postgres:5432/${POSTGRES_DB?} + health_check_listen_address: 0.0.0.0:8000 + batch_aggregation_shard_count: 32 + tasks_update_frequency_secs: 10 + aggregation_job_creation_interval_secs: 10 + min_aggregation_job_size: ${MIN_AGGREGATION_JOB_SIZE?} + max_aggregation_job_size: ${MAX_AGGREGATION_JOB_SIZE?} + + janus-aggregation-job-driver.yml: + content: | + database: + url: postgres://${POSTGRES_USER?}:${POSTGRES_PASSWORD?}@postgres:5432/${POSTGRES_DB?} + health_check_listen_address: 0.0.0.0:8000 + job_discovery_interval_secs: 10 + max_concurrent_job_workers: 10 + worker_lease_duration_secs: 600 + worker_lease_clock_skew_allowance_secs: 60 + maximum_attempts_before_failure: 10 + batch_aggregation_shard_count: 32 + + janus-collection-job-driver.yml: + content: | + database: + url: postgres://${POSTGRES_USER?}:${POSTGRES_PASSWORD?}@postgres:5432/${POSTGRES_DB?} + health_check_listen_address: 0.0.0.0:8000 + job_discovery_interval_secs: 10 + max_concurrent_job_workers: 10 + worker_lease_duration_secs: 600 + worker_lease_clock_skew_allowance_secs: 60 + maximum_attempts_before_failure: 10 + batch_aggregation_shard_count: 32 + + janus-garbage-collector.yml: + content: | + database: + url: postgres://${POSTGRES_USER?}:${POSTGRES_PASSWORD?}@postgres:5432/${POSTGRES_DB?} + health_check_listen_address: 0.0.0.0:8000 + garbage_collection: + gc_frequency_s: 60 + report_limit: 5000 + aggregation_limit: 500 + collection_limit: 50 diff --git a/bring-your-own/aggregator/generate-keys.sh b/bring-your-own/aggregator/generate-keys.sh new file mode 100644 index 000000000..93422ab12 --- /dev/null +++ b/bring-your-own/aggregator/generate-keys.sh @@ -0,0 +1,58 @@ +# +# This shell script is written to be as portable as possible. It even +# avoids defining any shell functions, as the work to be done is quite +# straightforward. +# +# Each generated key will be one of the following types: +# +# Type 1: 128 random bits (16 random bytes) written in +# URL-safe Base64 with no padding. +# +# Type 2: 256 random bits (32 random bytes) written in +# URL-safe Base64 with no padding. +# + +chmod 600 .env || exit $? + +tmp=generate-keys.sh.tmp + +to_url_safe=' + s|=||g + s|+|-|g + s|/|_|g +' + +# AGGREGATOR_API_AUTH_TOKENS: A single type 1 key. +head -c 16 /dev/urandom >${tmp?}1 || exit $? +base64 -w 0 ${tmp?}1 >${tmp?}2 || exit $? +x=`sed "${to_url_safe?}" ${tmp?}2` || exit $? +sed -i.bak " + /^#AGGREGATOR_API_AUTH_TOKENS=/ { + s|=.*|=${x?}| + s|#|| + } +" .env || exit $? + +# DATASTORE_KEYS: A single type 1 key. +head -c 16 /dev/urandom >${tmp?}1 || exit $? +base64 -w 0 ${tmp?}1 >${tmp?}2 || exit $? +x=`sed "${to_url_safe?}" ${tmp?}2` || exit $? +sed -i.bak " + /^#DATASTORE_KEYS=/ { + s|=.*|=${x?}| + s|#|| + } +" .env || exit $? + +# POSTGRES_PASSWORD: A single type 1 key. +head -c 16 /dev/urandom >${tmp?}1 || exit $? +base64 -w 0 ${tmp?}1 >${tmp?}2 || exit $? +x=`sed "${to_url_safe?}" ${tmp?}2` || exit $? +sed -i.bak " + /^#POSTGRES_PASSWORD=/ { + s|=.*|=${x?}| + s|#|| + } +" .env || exit $? + +rm -f ${tmp?}* || exit $? diff --git a/bring-your-own/application/.env b/bring-your-own/application/.env new file mode 100644 index 000000000..f98752b77 --- /dev/null +++ b/bring-your-own/application/.env @@ -0,0 +1,48 @@ +# Profile +COMPOSE_PROFILES=http-two-host + +# HTTP +LISTEN_HOST=0.0.0.0 +LISTEN_PORT_HTTP_API=8080 +LISTEN_PORT_HTTP_APP=8080 +PUBLIC_HOST_API=127.0.0.2 +PUBLIC_HOST_APP=127.0.0.1 +PUBLIC_PORT_HTTP_API=8080 +PUBLIC_PORT_HTTP_APP=8080 + +# HTTPS +ACME_CA_URI=https://acme-v02.api.letsencrypt.org/directory +HTTPS_ADMIN_EMAIL=example@example.com +LISTEN_PORT_HTTPS=443 +PUBLIC_PORT_HTTPS=443 + +# Docker images +DIVVIUP_API_IMAGE=us-west2-docker.pkg.dev/divviup-artifacts-public/divviup-api/divviup_api_integration_test:latest +DIVVIUP_API_MIGRATOR_IMAGE=us-west2-docker.pkg.dev/divviup-artifacts-public/divviup-api/divviup_api:latest +POSTGRES_IMAGE=postgres:latest +NGINX_IMAGE=nginx:latest +NGINX_PROXY_IMAGE=nginxproxy/nginx-proxy:latest +ACME_COMPANION_IMAGE=nginxproxy/acme-companion:latest + +# Postgres +POSTGRES_DB=divviup +POSTGRES_USER=postgres +#POSTGRES_PASSWORD= + +# Database key +#DATABASE_ENCRYPTION_KEYS= + +# Other keys +#SESSION_SECRETS= + +# Auth0 +AUTH_URL=https://example.auth0.com +AUTH_CLIENT_ID=example +AUTH_CLIENT_SECRET=example + +# Postmark +POSTMARK_TOKEN=example +EMAIL_ADDRESS=example@example.com + +# Restart policy +RESTART_POLICY=unless-stopped diff --git a/bring-your-own/application/docker-compose.yml b/bring-your-own/application/docker-compose.yml new file mode 100644 index 000000000..b4d32c54f --- /dev/null +++ b/bring-your-own/application/docker-compose.yml @@ -0,0 +1,152 @@ +x-divviup_common: &divviup_common + depends_on: + divviup-api-migrator: + condition: service_completed_successfully + image: ${DIVVIUP_API_IMAGE?} + environment: &divviup_environment + AUTH_AUDIENCE: https://${PUBLIC_HOST_APP?} + AUTH_CLIENT_ID: ${AUTH_CLIENT_ID?} + AUTH_CLIENT_SECRET: ${AUTH_CLIENT_SECRET?} + AUTH_URL: ${AUTH_URL?} + DATABASE_ENCRYPTION_KEYS: ${DATABASE_ENCRYPTION_KEYS?} + DATABASE_URL: postgres://${POSTGRES_USER?}:${POSTGRES_PASSWORD?}@postgres:5432/${POSTGRES_DB?} + EMAIL_ADDRESS: ${EMAIL_ADDRESS?} + POSTMARK_TOKEN: ${POSTMARK_TOKEN?} + RUST_LOG: info + SESSION_SECRETS: ${SESSION_SECRETS?} + healthcheck: + test: wget -O - http://127.0.0.1:8080/health + start_period: 60s + restart: ${RESTART_POLICY?} + +services: + + postgres: + image: ${POSTGRES_IMAGE} + environment: + POSTGRES_DB: ${POSTGRES_DB?} + POSTGRES_USER: ${POSTGRES_USER?} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD?} + healthcheck: + test: pg_isready -U ${POSTGRES_USER?} -d ${POSTGRES_DB?} + start_period: 60s + restart: ${RESTART_POLICY?} + + divviup-api-migrator: + depends_on: + postgres: + condition: service_healthy + image: ${DIVVIUP_API_MIGRATOR_IMAGE?} + environment: + DATABASE_URL: postgres://${POSTGRES_USER?}:${POSTGRES_PASSWORD?}@postgres:5432/${POSTGRES_DB?} + entrypoint: /migration up + + divviup-api-http: + <<: *divviup_common + profiles: + - http-one-host + - http-two-host + extra_hosts: + host.docker.internal: host-gateway + entrypoint: | + sh -c ' + case ${PUBLIC_PORT_HTTP_API?} in 80) + API_URL=http://${PUBLIC_HOST_API?} + ;; *) + API_URL=http://${PUBLIC_HOST_API?}:${PUBLIC_PORT_HTTP_API?} + esac + case ${PUBLIC_PORT_HTTP_APP?} in 80) + APP_URL=http://${PUBLIC_HOST_APP?} + ;; *) + APP_URL=http://${PUBLIC_HOST_APP?}:${PUBLIC_PORT_HTTP_APP?} + esac + export API_URL + export APP_URL + /divviup_api_bin + ' + ports: + - "${LISTEN_HOST?}:${LISTEN_PORT_HTTP_API?}:8080" + + divviup-app-http: + profiles: + - http-one-host + depends_on: + divviup-api-http: + condition: service_healthy + image: ${NGINX_IMAGE?} + configs: + - source: divviup-app-http-nginx.conf + target: /etc/nginx/nginx.conf + ports: + - "${LISTEN_HOST?}:${LISTEN_PORT_HTTP_APP?}:80" + + divviup-api-https: + <<: *divviup_common + profiles: + - https + environment: + <<: *divviup_environment + LETSENCRYPT_EMAIL: ${HTTPS_ADMIN_EMAIL?} + LETSENCRYPT_HOST: ${PUBLIC_HOST_API?},${PUBLIC_HOST_APP?} + VIRTUAL_HOST: ${PUBLIC_HOST_API?},${PUBLIC_HOST_APP?} + entrypoint: | + sh -c ' + case ${PUBLIC_PORT_HTTPS?} in 443) + API_URL=https://${PUBLIC_HOST_API?} + APP_URL=https://${PUBLIC_HOST_APP?} + ;; *) + API_URL=https://${PUBLIC_HOST_API?}:${PUBLIC_PORT_HTTPS?} + APP_URL=https://${PUBLIC_HOST_APP?}:${PUBLIC_PORT_HTTPS?} + esac + export API_URL + export APP_URL + /divviup_api_bin + ' + + nginx-proxy: + profiles: + - https + image: ${NGINX_PROXY_IMAGE?} + ports: + - "${LISTEN_HOST?}:${LISTEN_PORT_HTTP_API?}:80" + - "${LISTEN_HOST?}:${LISTEN_PORT_HTTPS?}:443" + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - certs:/etc/nginx/certs + - html:/usr/share/nginx/html + restart: ${RESTART_POLICY?} + + acme-companion: + profiles: + - https + image: ${ACME_COMPANION_IMAGE?} + environment: + ACME_CA_URI: ${ACME_CA_URI?} + DEFAULT_EMAIL: ${HTTPS_ADMIN_EMAIL?} + volumes_from: + - nginx-proxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - acme:/etc/acme.sh + restart: ${RESTART_POLICY?} + +volumes: + acme: + certs: + html: + +configs: + + divviup-app-http-nginx.conf: + content: | + events { + } + http { + server { + listen 80 default_server; + location / { + proxy_set_header Host ${PUBLIC_HOST_APP?}:${PUBLIC_PORT_HTTP_APP?}; + proxy_pass http://divviup-api-http:8080; + } + } + } diff --git a/bring-your-own/application/generate-keys.sh b/bring-your-own/application/generate-keys.sh new file mode 100644 index 000000000..0f7aa233d --- /dev/null +++ b/bring-your-own/application/generate-keys.sh @@ -0,0 +1,58 @@ +# +# This shell script is written to be as portable as possible. It even +# avoids defining any shell functions, as the work to be done is quite +# straightforward. +# +# Each generated key will be one of the following types: +# +# Type 1: 128 random bits (16 random bytes) written in +# URL-safe Base64 with no padding. +# +# Type 2: 256 random bits (32 random bytes) written in +# URL-safe Base64 with no padding. +# + +chmod 600 .env || exit $? + +tmp=generate-keys.sh.tmp + +to_url_safe=' + s|=||g + s|+|-|g + s|/|_|g +' + +# DATABASE_ENCRYPTION_KEYS: A single type 1 key. +head -c 16 /dev/urandom >${tmp?}1 || exit $? +base64 -w 0 ${tmp?}1 >${tmp?}2 || exit $? +x=`sed "${to_url_safe?}" ${tmp?}2` || exit $? +sed -i.bak " + /^#DATABASE_ENCRYPTION_KEYS=/ { + s|=.*|=${x?}| + s|#|| + } +" .env || exit $? + +# POSTGRES_PASSWORD: A single type 1 key. +head -c 16 /dev/urandom >${tmp?}1 || exit $? +base64 -w 0 ${tmp?}1 >${tmp?}2 || exit $? +x=`sed "${to_url_safe?}" ${tmp?}2` || exit $? +sed -i.bak " + /^#POSTGRES_PASSWORD=/ { + s|=.*|=${x?}| + s|#|| + } +" .env || exit $? + +# SESSION_SECRETS: A single type 2 key. +head -c 32 /dev/urandom >${tmp?}1 || exit $? +base64 -w 0 ${tmp?}1 >${tmp?}2 || exit $? +x=`sed "${to_url_safe?}" ${tmp?}2` || exit $? +sed -i.bak " + /^#SESSION_SECRETS=/ { + s|=.*|=${x?}| + s|#|| + } +" .env || exit $? + +rm -f ${tmp?}* || exit $? diff --git a/bring-your-own/terraform/aws.tf b/bring-your-own/terraform/aws.tf new file mode 100644 index 000000000..13fb81ebb --- /dev/null +++ b/bring-your-own/terraform/aws.tf @@ -0,0 +1,231 @@ +terraform { + required_version = ">= 1.9.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.73" + } + } +} + +variable "region" { + type = string +} + +variable "name_prefix" { + type = string +} + +variable "resource_group_key" { + type = string +} + +variable "resource_group_value" { + type = string +} + +variable "elastic_ip_id" { + type = string +} + +variable "instance_type" { + type = string +} + +variable "ubuntu_version" { + type = string +} + +variable "volume_size_gb" { + type = number +} + +variable "component" { + type = string +} + +provider "aws" { + region = var.region + default_tags { + tags = { + (var.resource_group_key) = var.resource_group_value + } + } +} + +resource "aws_resourcegroups_group" "main" { + name = "${var.name_prefix}_resource_group" + resource_query { + query = jsonencode({ + ResourceTypeFilters = ["AWS::AllSupported"], + TagFilters = [{ + Key = var.resource_group_key, + Values = [var.resource_group_value] + }] + }) + } +} + +resource "tls_private_key" "main" { + algorithm = "RSA" + rsa_bits = 4096 +} + +resource "local_file" "ssh_key_pk_txt" { + filename = "ssh_key_pk.txt" + file_permission = "0600" + content = tls_private_key.main.public_key_openssh +} + +resource "local_file" "ssh_key_sk_txt" { + filename = "ssh_key_sk.txt" + file_permission = "0600" + content = tls_private_key.main.private_key_openssh +} + +resource "aws_key_pair" "main" { + key_name = "${var.name_prefix}_ssh_key" + public_key = tls_private_key.main.public_key_openssh +} + +resource "aws_security_group" "main" { + tags = { + Name = "${var.name_prefix}_sg" + } + name = "${var.name_prefix}_sg" + description = "${var.name_prefix}_sg" +} + +resource "aws_vpc_security_group_egress_rule" "main_tx_self" { + tags = { + Name = "${var.name_prefix}_sg_tx_self" + } + description = "${var.name_prefix}_sg_tx_self" + security_group_id = aws_security_group.main.id + referenced_security_group_id = aws_security_group.main.id + ip_protocol = "-1" +} + +resource "aws_vpc_security_group_ingress_rule" "main_rx_self" { + tags = { + Name = "${var.name_prefix}_sg_rx_self" + } + description = "${var.name_prefix}_sg_rx_self" + security_group_id = aws_security_group.main.id + referenced_security_group_id = aws_security_group.main.id + ip_protocol = "-1" +} + +resource "aws_vpc_security_group_egress_rule" "main_tx_any" { + tags = { + Name = "${var.name_prefix}_sg_tx_any" + } + description = "${var.name_prefix}_sg_tx_any" + security_group_id = aws_security_group.main.id + cidr_ipv4 = "0.0.0.0/0" + ip_protocol = "-1" +} + +resource "aws_vpc_security_group_ingress_rule" "main_rx_any_tcp_22" { + tags = { + Name = "${var.name_prefix}_sg_rx_any_tcp_22" + } + description = "${var.name_prefix}_sg_rx_any_tcp_22" + security_group_id = aws_security_group.main.id + cidr_ipv4 = "0.0.0.0/0" + from_port = 22 + to_port = 22 + ip_protocol = "tcp" +} + +resource "aws_vpc_security_group_ingress_rule" "main_rx_any_tcp_80" { + tags = { + Name = "${var.name_prefix}_sg_rx_any_tcp_80" + } + description = "${var.name_prefix}_sg_rx_any_tcp_80" + security_group_id = aws_security_group.main.id + cidr_ipv4 = "0.0.0.0/0" + from_port = 80 + to_port = 80 + ip_protocol = "tcp" +} + +resource "aws_vpc_security_group_ingress_rule" "main_rx_any_tcp_443" { + tags = { + Name = "${var.name_prefix}_sg_rx_any_tcp_443" + } + description = "${var.name_prefix}_sg_rx_any_tcp_443" + security_group_id = aws_security_group.main.id + cidr_ipv4 = "0.0.0.0/0" + from_port = 443 + to_port = 443 + ip_protocol = "tcp" +} + +data "aws_ami" "main" { + owners = ["099720109477"] + filter { + name = "name" + values = ["ubuntu/images/*-${var.ubuntu_version}-amd64-server-*"] + } + most_recent = true +} + +resource "aws_instance" "main" { + tags = { + Name = "${var.name_prefix}_instance" + } + ami = data.aws_ami.main.id + instance_type = var.instance_type + key_name = aws_key_pair.main.key_name + vpc_security_group_ids = [aws_security_group.main.id] + root_block_device { + volume_size = var.volume_size_gb + } +} + +data "aws_eip" "main" { + id = var.elastic_ip_id +} + +resource "aws_eip_association" "main" { + allocation_id = data.aws_eip.main.id + instance_id = aws_instance.main.id + allow_reassociation = false +} + +resource "terraform_data" "provisioner" { + depends_on = [ + aws_eip_association.main, + ] + connection { + type = "ssh" + agent = false + user = "ubuntu" + host = data.aws_eip.main.public_ip + private_key = tls_private_key.main.private_key_openssh + } + provisioner "file" { + source = "./${var.component}/" + destination = "/home/ubuntu" + } + provisioner "file" { + source = "./terraform/provision.bash" + destination = "/home/ubuntu/provision.bash" + } + provisioner "remote-exec" { + inline = [ + "cd /home/ubuntu || exit $?", + "bash provision.bash || exit $?", + ] + } +} + +resource "local_file" "instance_id_txt" { + depends_on = [ + terraform_data.provisioner, + ] + filename = "instance_id.txt" + file_permission = "0600" + content = "${aws_instance.main.id}\n" +} diff --git a/bring-your-own/terraform/aws.tfvars b/bring-your-own/terraform/aws.tfvars new file mode 100644 index 000000000..34da2fe0d --- /dev/null +++ b/bring-your-own/terraform/aws.tfvars @@ -0,0 +1,9 @@ +region = "us-west-1" +name_prefix = "divviup" +resource_group_key = "resource_group" +resource_group_value = "divviup" +elastic_ip_id = "eipalloc-XXXXXXXXXXXXXXXXX" +instance_type = "t3.small" +ubuntu_version = "24.04" +volume_size_gb = 20 +component = "aggregator" diff --git a/bring-your-own/terraform/provision.bash b/bring-your-own/terraform/provision.bash new file mode 100644 index 000000000..81fa0e7af --- /dev/null +++ b/bring-your-own/terraform/provision.bash @@ -0,0 +1,53 @@ +set -E -e -u -o pipefail || exit $? +trap exit ERR + +set -x + +#----------------------------------------------------------------------- +# Sleep a bit before doing anything else +#----------------------------------------------------------------------- +# +# Terraform sometimes gets into the host so fast that "apt-get update" +# fails with weird "No such file or directory" errors. Doing a sleep +# before doing anything else seems to help. +# + +sleep 30 + +#----------------------------------------------------------------------- +# Make apt-get noninteractive +#----------------------------------------------------------------------- + +DEBIAN_FRONTEND=noninteractive +readonly DEBIAN_FRONTEND +export DEBIAN_FRONTEND + +#----------------------------------------------------------------------- +# Install some packages +#----------------------------------------------------------------------- + +sudo apt-get -q -y update + +sudo apt-get -q -y install \ + bash \ + jq \ +; + +#----------------------------------------------------------------------- +# Install Docker +#----------------------------------------------------------------------- + +curl -L -S -f -s https://get.docker.com/ | sudo sh + +x=$(sed -n '/^docker:/ p' /etc/group) +if [[ ! $x ]]; then + sudo groupadd docker +fi + +sudo usermod -G docker -a $USER + +#----------------------------------------------------------------------- +# Start the Docker Compose deployment +#----------------------------------------------------------------------- + +sg docker 'docker compose up -d'