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'