[a-z][a-z]?[a-z]|zh-hans)/(.*) /$lang/404/index.html;
+}
+
+server {
+ server_name _;
+ listen $VCAP_APP_PORT;
+
+ modsecurity on;
+ modsecurity_rules_file /home/vcap/app/nginx/snippets/owasp-modsecurity-main.conf;
+
+ set $cf_forwarded_host "$host";
+ #if ($http_x_cf_forwarded_url ~* ^(https?\:\/\/)(.*?)(\/(.*))?$) {
+ # set $cf_forwarded_host "$2";
+ #}
+
+ set $port 8881;
+ if ($cf_forwarded_host ~* \-cms\-) {
+ set $port 8882;
+ }
+
+ location @fourohfour_english {
+ allow all;
+ access_log off;
+
+ default_type text/plain;
+ return 404 'Not Found';
+ break;
+ # rewrite ^ /404/index.html;
+ include nginx/snippets/proxy-to-static.conf;
+ break;
+ }
+
+ location ^~ /s3/files {
+ set $port 8883;
+ proxy_redirect off;
+ proxy_connect_timeout 300;
+ chunked_transfer_encoding off;
+ proxy_pass http://127.0.0.1:$port;
+ proxy_cookie_flags ~SESS.* secure;
+ proxy_set_header Host $cf_forwarded_host;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-Real-IP $remote_addr;
+ error_page 403 = @fourohfour_english;
+ }
+
+ location / {
+ proxy_redirect off;
+ proxy_connect_timeout 300;
+ chunked_transfer_encoding off;
+ proxy_pass http://127.0.0.1:$port;
+ proxy_cookie_flags ~SESS.* secure;
+ proxy_set_header Host $cf_forwarded_host;
+ proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-Real-IP $remote_addr;
+ error_page 403 = @fourohfour_english;
+ }
+}
+
+server {
+ server_name 127.0.0.1;
+ listen 8881;
+
+ location @fourohfour {
+ allow all;
+ access_log off;
+
+ rewrite ^ $error_page;
+ include nginx/snippets/proxy-to-static.conf;
+ }
+
+ location / {
+ #rewrite ^/static/(.*) /$1;
+ rewrite ^([^.]*[^/])$ $1/;
+ rewrite (.*)/$ $1/index.html last;
+
+ #include nginx/dynamic/deny-by-domain.conf;
+ # include nginx/snippets/ip-restrict-static.conf;
+ include nginx/snippets/proxy-to-static.conf;
+ error_page 403 = @fourohfour;
+ }
+}
+
+server {
+ server_name 127.0.0.1;
+ listen 8882;
+
+ error_page 403 = @forbidden;
+
+ location @forbidden {
+ allow all;
+ access_log off;
+
+ default_type text/plain;
+ return 403 'Forbidden by USAGov';
+ break;
+ # redirect to homepage usa.gov
+ }
+
+ location / {
+ access_log on;
+ rewrite_log on;
+ #include nginx/dynamic/deny-by-domain.conf;
+ include nginx/snippets/ip-restrict-cms.conf;
+ include nginx/snippets/proxy-to-app.conf;
+ }
+}
+
+server {
+ server_name 127.0.0.1;
+ listen 8883;
+
+ #Rewrite all s3 file requests to cms path.
+ #Location blocks below will handle the rest.
+ rewrite ^/s3/files/(.*)$ /cms/public/$1 break;
+
+ location @fourohfour {
+ allow all;
+ access_log off;
+
+ default_type text/plain;
+ return 404 'Not Found';
+ break;
+ rewrite ^ $error_page;
+ include nginx/snippets/proxy-to-static.conf;
+ }
+
+ location / {
+ rewrite ^/s3/files/(.*) /cms/public/$1;
+ rewrite ^([^.]*[^/])$ $1/;
+ rewrite (.*)/$ $1/index.html last;
+
+ #include nginx/dynamic/deny-by-domain.conf;
+ # include nginx/snippets/ip-restrict-static.conf;
+ include nginx/snippets/proxy-to-storage.conf;
+ error_page 403 = @fourohfour;
+ }
+}
+
+server {
+ server_name 127.0.0.1;
+ listen 8884;
+
+
+ location @fourohfour {
+ allow all;
+ access_log off;
+
+ rewrite ^ $error_page;
+ include nginx/snippets/proxy-to-static.conf;
+ }
+
+ location / {
+ rewrite ^/static/(.*) /$1;
+ rewrite ^([^.]*[^/])$ $1/;
+ rewrite (.*)/$ $1/index.html last;
+
+ include nginx/snippets/proxy-to-static.conf;
+ error_page 403 = @fourohfour;
+ }
+}
diff --git a/terraform/applications/nginx-waf/nginx/dynamic/deny-by-domain.conf b/terraform/applications/nginx-waf/nginx/dynamic/deny-by-domain.conf
new file mode 100644
index 00000000..41c90fc2
--- /dev/null
+++ b/terraform/applications/nginx-waf/nginx/dynamic/deny-by-domain.conf
@@ -0,0 +1 @@
+# Restricted by domain (placeholder, to be replaced via cron)
diff --git a/terraform/applications/nginx-waf/nginx/dynamic/deny_domain_by_ip.sh b/terraform/applications/nginx-waf/nginx/dynamic/deny_domain_by_ip.sh
new file mode 100644
index 00000000..679a7954
--- /dev/null
+++ b/terraform/applications/nginx-waf/nginx/dynamic/deny_domain_by_ip.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+# Get IP addresses for domains in domains-deny.list.
+# If the results differ from deny-by-domain.conf, update that file.
+# If there were changes AND the flag --no_reload was not passed, reload nginx.
+# (--no-reload is only wanted during setup, before nginx has started.)
+
+BASEDIR=$(dirname $0)
+
+echo "# Restricted by domain (via cron job):" > ${BASEDIR}/deny-by-domain_new.conf
+while read -r line
+do
+ ddns_record="$line"
+ if [[ ! -z $ddns_record ]]; then
+ resolved_ip=`getent ahosts $line | awk '{ print $1 ; exit }'`
+ if [[ ! -z $resolved_ip ]]; then
+ echo " deny $resolved_ip; # from $ddns_record" >> ${BASEDIR}/deny-by-domain_new.conf
+ fi
+ fi
+done < ${BASEDIR}/domains-deny.list
+
+# Update deny-by-domain.conf only if there are changes.
+CHANGES=$(diff ${BASEDIR}/deny-by-domain.conf ${BASEDIR}/deny-by-domain_new.conf)
+if [[ ! -z "$CHANGES" ]]; then
+ cat ${BASEDIR}/deny-by-domain_new.conf > ${BASEDIR}/deny-by-domain.conf
+ if [ "$1" != "--no-reload" ]; then
+ /usr/sbin/nginx -s reload
+ fi
+fi
+rm ${BASEDIR}/deny-by-domain_new.conf
diff --git a/terraform/applications/nginx-waf/nginx/dynamic/domains-deny.list b/terraform/applications/nginx-waf/nginx/dynamic/domains-deny.list
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/terraform/applications/nginx-waf/nginx/dynamic/domains-deny.list
@@ -0,0 +1 @@
+
diff --git a/terraform/applications/nginx-waf/nginx/snippets/ip-restrict-cms.conf.tmpl b/terraform/applications/nginx-waf/nginx/snippets/ip-restrict-cms.conf.tmpl
new file mode 100644
index 00000000..3cb7e179
--- /dev/null
+++ b/terraform/applications/nginx-waf/nginx/snippets/ip-restrict-cms.conf.tmpl
@@ -0,0 +1,6 @@
+#allow 127.0.0.1/32;
+#allow 172.0.0.0/8;
+
+${IPS_ALLOWED_CMS}
+
+#deny all;
diff --git a/terraform/applications/nginx-waf/nginx/snippets/ip-restrict-static.conf.tmpl b/terraform/applications/nginx-waf/nginx/snippets/ip-restrict-static.conf.tmpl
new file mode 100644
index 00000000..088ffe0e
--- /dev/null
+++ b/terraform/applications/nginx-waf/nginx/snippets/ip-restrict-static.conf.tmpl
@@ -0,0 +1,3 @@
+${IPS_DENYED_STATIC}
+
+allow all;
diff --git a/terraform/applications/nginx-waf/nginx/snippets/owasp-modsecurity-main.conf b/terraform/applications/nginx-waf/nginx/snippets/owasp-modsecurity-main.conf
new file mode 100644
index 00000000..c529b5ea
--- /dev/null
+++ b/terraform/applications/nginx-waf/nginx/snippets/owasp-modsecurity-main.conf
@@ -0,0 +1,7 @@
+# Include the recommended configuration
+Include /home/vcap/app/modsecurity/modsecurity.conf
+Include /home/vcap/app/modsecurity/modsecurity-override.conf
+Include /home/vcap/app/modsecurity/crs-setup.conf
+Include /home/vcap/app/modsecurity/crs/*.conf
+# A test rule
+SecRule ARGS:testparam "@contains test" "id:1234,deny,log,status:403"
diff --git a/terraform/applications/nginx-waf/nginx/snippets/proxy-to-app.conf.tmpl b/terraform/applications/nginx-waf/nginx/snippets/proxy-to-app.conf.tmpl
new file mode 100644
index 00000000..b62bb7bd
--- /dev/null
+++ b/terraform/applications/nginx-waf/nginx/snippets/proxy-to-app.conf.tmpl
@@ -0,0 +1,24 @@
+set $cf_forwarded_host "$host";
+set $cf_forwarded_uri "$request_uri";
+
+set $cf_destination_host "${cms_internal_endpoint}";
+set $cf_destination_port "61443";
+
+set $base_host "$cf_forwarded_host";
+if ($cf_forwarded_host ~* ^(.*)-waf-(.*)\.app\.cloud\.gov$) {
+ set $base_host "$1-cms-$2";
+}
+
+proxy_http_version 1.1;
+proxy_set_header Connection "";
+proxy_redirect off;
+proxy_connect_timeout 300;
+chunked_transfer_encoding off;
+
+proxy_set_header Host $cf_forwarded_host;
+proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+proxy_set_header X-Real-IP $remote_addr;
+
+# Use XX-CF-APP-INSTANCE on the original request if you wish to target an instance
+proxy_set_header X-CF-APP-INSTANCE $http_xx_cf_app_instance;
+proxy_pass https://$cf_destination_host:$cf_destination_port$cf_forwarded_uri;
diff --git a/terraform/applications/nginx-waf/nginx/snippets/proxy-to-static.conf.tmpl b/terraform/applications/nginx-waf/nginx/snippets/proxy-to-static.conf.tmpl
new file mode 100644
index 00000000..e1bd02c9
--- /dev/null
+++ b/terraform/applications/nginx-waf/nginx/snippets/proxy-to-static.conf.tmpl
@@ -0,0 +1,14 @@
+proxy_http_version 1.1;
+proxy_set_header Connection \"\";
+proxy_set_header Authorization '';
+proxy_set_header Host ${static_bucket}.${static_fips_endpoint};
+proxy_hide_header x-amz-id-2;
+proxy_hide_header x-amz-request-id;
+proxy_hide_header x-amz-meta-server-side-encryption;
+proxy_hide_header x-amz-server-side-encryption;
+proxy_hide_header Set-Cookie;
+proxy_ignore_headers Set-Cookie;
+proxy_intercept_errors on;
+#add_header Cache-Control max-age=31536000;
+add_header Cache-Control max-age=60;
+proxy_pass https://${static_bucket}.${static_fips_endpoint};
diff --git a/terraform/applications/nginx-waf/nginx/snippets/proxy-to-storage.conf.tmpl b/terraform/applications/nginx-waf/nginx/snippets/proxy-to-storage.conf.tmpl
new file mode 100644
index 00000000..4bd2caef
--- /dev/null
+++ b/terraform/applications/nginx-waf/nginx/snippets/proxy-to-storage.conf.tmpl
@@ -0,0 +1,14 @@
+proxy_http_version 1.1;
+proxy_set_header Connection \"\";
+proxy_set_header Authorization '';
+proxy_set_header Host ${storage_bucket}.${storage_fips_endpoint};
+proxy_hide_header x-amz-id-2;
+proxy_hide_header x-amz-request-id;
+proxy_hide_header x-amz-meta-server-side-encryption;
+proxy_hide_header x-amz-server-side-encryption;
+proxy_hide_header Set-Cookie;
+proxy_ignore_headers Set-Cookie;
+proxy_intercept_errors on;
+#add_header Cache-Control max-age=31536000;
+add_header Cache-Control max-age=60;
+proxy_pass https://${storage_bucket}.${storage_fips_endpoint};
diff --git a/terraform/applications/nginx-waf/packages/.DS_Store b/terraform/applications/nginx-waf/packages/.DS_Store
new file mode 100644
index 00000000..cbcfe95b
Binary files /dev/null and b/terraform/applications/nginx-waf/packages/.DS_Store differ
diff --git a/terraform/applications/nginx-waf/packages/coreruleset-4.7.0-minimal.tar.gz b/terraform/applications/nginx-waf/packages/coreruleset-4.7.0-minimal.tar.gz
new file mode 100644
index 00000000..74efd045
Binary files /dev/null and b/terraform/applications/nginx-waf/packages/coreruleset-4.7.0-minimal.tar.gz differ
diff --git a/terraform/applications/nginx-waf/packages/libmodsecurity3_3.0.9-1_amd64.deb b/terraform/applications/nginx-waf/packages/libmodsecurity3_3.0.9-1_amd64.deb
new file mode 100644
index 00000000..59817f52
Binary files /dev/null and b/terraform/applications/nginx-waf/packages/libmodsecurity3_3.0.9-1_amd64.deb differ
diff --git a/terraform/applications/nginx-waf/packages/libmodsecurity3t64_3.0.12-1.1build2_amd64.deb b/terraform/applications/nginx-waf/packages/libmodsecurity3t64_3.0.12-1.1build2_amd64.deb
new file mode 100644
index 00000000..b92fd5cc
Binary files /dev/null and b/terraform/applications/nginx-waf/packages/libmodsecurity3t64_3.0.12-1.1build2_amd64.deb differ
diff --git a/terraform/applications/nginx-waf/public/index.html b/terraform/applications/nginx-waf/public/index.html
new file mode 100644
index 00000000..187a9be9
--- /dev/null
+++ b/terraform/applications/nginx-waf/public/index.html
@@ -0,0 +1 @@
+Welcome to cloud.gov!
\ No newline at end of file
diff --git a/terraform/applications/nginx-waf/start b/terraform/applications/nginx-waf/start
new file mode 100755
index 00000000..ee1c8e83
--- /dev/null
+++ b/terraform/applications/nginx-waf/start
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+home="/home/vcap"
+app_path="${home}/app"
+nginx_path="${home}/deps/1/nginx/sbin"
+
+echo "Intializing Nginx..."
+
+## Configure nginx.
+${app_path}/init
+[ $? -ne 0 ] && exit 1
+
+echo "Starting Nginx..."
+## Start nginx.
+${nginx_path}/nginx -p ${app_path} -c nginx.conf &
+
+echo "Done!"
+## Simple entrypoint to hold the container open.
+${app_path}/entrypoint
diff --git a/terraform/applications/tf-bastion/apt.yml b/terraform/applications/tf-bastion/apt.yml
new file mode 100755
index 00000000..5f6bd5ca
--- /dev/null
+++ b/terraform/applications/tf-bastion/apt.yml
@@ -0,0 +1,6 @@
+---
+packages:
+ - curl
+ - gettext
+ - git
+ - wget
diff --git a/terraform/applications/tf-bastion/exports.sh b/terraform/applications/tf-bastion/exports.sh
new file mode 100755
index 00000000..f52afeef
--- /dev/null
+++ b/terraform/applications/tf-bastion/exports.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+home="/home/vcap"
+
+#app_path="${home}/app"
+
+PG_CONN_STR=$(echo "${VCAP_SERVICES}" | jq '."aws-rds"[].credentials.uri')
+PGDATABASE=$(echo "${VCAP_SERVICES}" | jq '."aws-rds"[].credentials.db_name')
+PGHOST=$(echo "${VCAP_SERVICES}" | jq '."aws-rds"[].credentials.host')
+PGPASSWORD=$(echo "${VCAP_SERVICES}" | jq '."aws-rds"[].credentials.password')
+PGPORT=$(echo "${VCAP_SERVICES}" | jq '."aws-rds"[].credentials.port')
+PGUSER=$(echo "${VCAP_SERVICES}" | jq '."aws-rds"[].credentials.username')
+
+{
+ echo "export PATH=${PATH}:${home}/deps/0/bin" | tee "${home}/exports.sh"
+ echo "alias terraform=tofu" | tee -a "${home}/exports.sh"
+ echo "alias tf=tofu" | tee -a "${home}/exports.sh"
+
+ echo "export PG_CONN_STR=${PG_CONN_STR}" | tee -a "${home}/exports.sh"
+ echo "export PGDATABASE=${PGDATABASE}" | tee -a "${home}/exports.sh"
+ echo "export PGHOST=${PGHOST}" | tee -a "${home}/exports.sh"
+ echo "export PGPASSWORD=${PGPASSWORD}" | tee -a "${home}/exports.sh"
+ echo "export PGPORT=${PGPORT}" | tee -a "${home}/exports.sh"
+ echo "export PGUSER=${PGUSER}" | tee -a "${home}/exports.sh"
+} > /dev/null 2>&1
+
diff --git a/terraform/applications/tf-bastion/start b/terraform/applications/tf-bastion/start
new file mode 100755
index 00000000..bd3454db
--- /dev/null
+++ b/terraform/applications/tf-bastion/start
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+home="/home/vcap"
+
+#app_path="${home}/app"
+
+echo "Downloading OpenTofu v${OPENTOFU_VERSION}..."
+rm -f /home/vcap/deps/0/bin/tofu
+wget -q "https://github.com/opentofu/opentofu/releases/download/v${OPENTOFU_VERSION}/tofu_${OPENTOFU_VERSION}_amd64.deb"
+
+echo "Installing OpenTofu..."
+dpkg-deb -R "tofu_${OPENTOFU_VERSION}_amd64.deb" ${home}/deps/0/apt/
+ln -s "${home}/deps/0/apt/usr/bin/tofu" "${home}/deps/0/bin/tofu"
+rm -f "tofu_${OPENTOFU_VERSION}_amd64.deb"
+
+echo "Exporting aliases and environmental variables..."
+
+PG_CONN_STR=$(echo "${VCAP_SERVICES}" | jq '."aws-rds"[].credentials.uri')
+PGDATABASE=$(echo "${VCAP_SERVICES}" | jq '."aws-rds"[].credentials.db_name')
+PGHOST=$(echo "${VCAP_SERVICES}" | jq '."aws-rds"[].credentials.host')
+PGPASSWORD=$(echo "${VCAP_SERVICES}" | jq '."aws-rds"[].credentials.password')
+PGPORT=$(echo "${VCAP_SERVICES}" | jq '."aws-rds"[].credentials.port')
+PGUSER=$(echo "${VCAP_SERVICES}" | jq '."aws-rds"[].credentials.username')
+
+{
+ echo "export PATH=${PATH}:${home}/deps/0/bin" | tee "${home}/exports.sh"
+ echo "alias terraform=tofu" | tee -a "${home}/exports.sh"
+ echo "alias tf=tofu" | tee -a "${home}/exports.sh"
+
+ echo "export PG_CONN_STR=${PG_CONN_STR}" | tee -a "${home}/exports.sh"
+ echo "export PGDATABASE=${PGDATABASE}" | tee -a "${home}/exports.sh"
+ echo "export PGHOST=${PGHOST}" | tee -a "${home}/exports.sh"
+ echo "export PGPASSWORD=${PGPASSWORD}" | tee -a "${home}/exports.sh"
+ echo "export PGPORT=${PGPORT}" | tee -a "${home}/exports.sh"
+ echo "export PGUSER=${PGUSER}" | tee -a "${home}/exports.sh"
+ echo "source exports.sh" | tee -a "${home}/.bashrc"
+} > /dev/null 2>&1
+
+echo "Bastion ready!"
+while : ; do sleep 500 ; done
diff --git a/terraform/bootstrap/.terraform.lock.hcl b/terraform/bootstrap/.terraform.lock.hcl
new file mode 100644
index 00000000..d7b0b33e
--- /dev/null
+++ b/terraform/bootstrap/.terraform.lock.hcl
@@ -0,0 +1,138 @@
+# This file is maintained automatically by "tofu init".
+# Manual edits may be lost in future updates.
+
+provider "registry.opentofu.org/cloudfoundry-community/cloudfoundry" {
+ version = "0.53.1"
+ constraints = "~> 0.5"
+ hashes = [
+ "h1:o6nGtINonmkgsX810QianzlX+y+aJ7WzYRwAvhQu5qE=",
+ "zh:017a55cdbd444ccf8fe45a3c7cdbc08ddf4f0f13550fcd457c31df9b2cfdb767",
+ "zh:100e9bd10868547193134082427abebad9db6359f6139a882192232e8e6911e3",
+ "zh:34467f6504e8527bd3e18e372d5386a43f2bffd88abf54bb72d51f04ab3e4e23",
+ "zh:3a278f5f71e39d29c7db999e2a34e8135b79cee4f36510b0f2c2dfec47997cf1",
+ "zh:3be1fbe17382c91561b1985d372606d802513d94bae6368e1bafd8dd49494737",
+ "zh:3f12bd7a629d547c706c380d9499ff39eab7b8824a14662aa446f230304bdd3a",
+ "zh:404acaa9ad7f95e83baf2332be54c065c21053bf304e80ac41ae49719462b184",
+ "zh:5ac5f6159d1e0c989e739cf16aa8dede6cee3562a6262bf9f2c6b53f4da866fe",
+ "zh:7a440ee173e69fa153ea4baea47adfca34d7171ffc83e7a1c0ec319d28998cbc",
+ "zh:87e2200bf66443671e249108d1cfa4aa13a31b9fdf445cec88364db8ea6be623",
+ "zh:b1b20b2b751df7765225cee5b01290b06e245e50faa8053495c2ef5ebe316998",
+ "zh:c8ddda9cf7dff40d762ea4dc22941c993ae8e9b2388c8d421f43254a56c98482",
+ "zh:d6ce83f0077a9f6262ffa1f7d777e2b72feac7ea7c8735aa39a5f86b4f3f7084",
+ "zh:d74126b9189ab4ca137ca634eaa25c571491bdd2456ccd0f3276a6d49163e412",
+ "zh:db5d415346e03eac0c5e025f9c10afdebfff35487e8a8383b3c4cd867c422fe2",
+ ]
+}
+
+provider "registry.opentofu.org/hashicorp/archive" {
+ version = "2.6.0"
+ hashes = [
+ "h1:s1OObC0b95ceQkrAMqL4q6wMYDBWYt8swZbLup+UJXI=",
+ "zh:046b3ba4223002d1cd1c917e8c21b58a636fcd751073745e3db99beebe254dd8",
+ "zh:1c1ed2ea0927b491689c3c7d178880cd9902f2a5339da8f46c56279920329a27",
+ "zh:1f17b47ba1bf18bd7bd30ea35c2ba32eaa23f8d08b3a35126edb31daf6ae10fd",
+ "zh:4b58aaac88335bb2ca482766e2682514fed78ff8cabe5665b6e5dd7c22ff9c81",
+ "zh:6c7dd6d4ff061d350fc6eb76866905c47450b8b8c1d2e238aa737afd48b6a267",
+ "zh:7b376916c5b911a3f887fd296c25ced36d8ba742b8482f1e0f092bf8fb008146",
+ "zh:8661139125b1ea7b89e0084377863dc820cdcbc433bb9a7c445350480f83b2c2",
+ "zh:e17c9056f210ec9a8c9cfe8a13ecd09ae59ad0a0197c96589b86eb4f7cf5326d",
+ "zh:ee15bddc7a596cccd400a762b6dadf1c8889faff7c931ae4b39f2e5404188da1",
+ "zh:f74355e6588daf88ec210d2967fbf5d22fa18c448d2807b8a7049dc777a2dbcb",
+ ]
+}
+
+provider "registry.opentofu.org/hashicorp/github" {
+ version = "6.3.1"
+ hashes = [
+ "h1:vKWgfpIrSNic7pYVi4LKIDQ2MgUpHq7uSj8nA8xfrw0=",
+ "zh:25ae1cb97ec528e6b7e9330489f4a33acc0fa80b909c113a8445656bc524c5b9",
+ "zh:3e1f6300dc10e52a54f13352770ed79f25ff4ba9ac49b776c52a655a3488a20b",
+ "zh:4aaf2877ec22e63358d7c9cd48c7d7947d1a1dc4d03231f0af193d8975d5918a",
+ "zh:4b904a81fac12a2a7606c8d811cb9c4e13581adcaaa19e503a067ac95c515925",
+ "zh:54fe7e0dca04e698631a5b86bdd43ef09a31375e68f8f89970b4315cd5fc6312",
+ "zh:6b14f92cf62784eaf20f43ef58ce966735f30d43deeab077943bd410c0d8b8b2",
+ "zh:86c49a1c11c024b26b6750c446f104922a3fe8464d3706a5fb9a4a05c6ca0b0a",
+ "zh:8939fb6332c4a58c4e90245eb9f0110987ccafff06b45a7ed513f2759a2abe6a",
+ "zh:8b4068a78c1f357325d1151facdb1aff506b9cd79d2bab21a55651255a130e2f",
+ "zh:ae22f5e52f534f19811d7f9480b4eb442f12ff16367b3893abb4e449b029ff6b",
+ "zh:afae9cfd9d49002ddfea552aa4844074b9974bd56ff2c2458f2297fe0df56a5b",
+ "zh:bc7a434408eb16a4fbceec0bd86b108a491408b727071402ad572cdb1afa2eb7",
+ "zh:c8e4728ea2d2c6e3d2c1bc5e7d92ed1121c02bab687702ec2748e3a6a0844150",
+ "zh:f6314b2cff0c0a07a216501cda51b35e6a4c66a2418c7c9966ccfe701e01b6b0",
+ "zh:fbd1fee2c9df3aa19cf8851ce134dea6e45ea01cb85695c1726670c285797e25",
+ ]
+}
+
+provider "registry.opentofu.org/hashicorp/local" {
+ version = "2.5.2"
+ hashes = [
+ "h1:MBgBjJljfDl1i2JPcIoH4hW+2XLJ+D1l12iH/xd3uTo=",
+ "zh:25b95b76ceaa62b5c95f6de2fa6e6242edbf51e7fc6c057b7f7101aa4081f64f",
+ "zh:3c974fdf6b42ca6f93309cf50951f345bfc5726ec6013b8832bcd3be0eb3429e",
+ "zh:5de843bf6d903f5cca97ce1061e2e06b6441985c68d013eabd738a9e4b828278",
+ "zh:86beead37c7b4f149a54d2ae633c99ff92159c748acea93ff0f3603d6b4c9f4f",
+ "zh:8e52e81d3dc50c3f79305d257da7fde7af634fed65e6ab5b8e214166784a720e",
+ "zh:9882f444c087c69559873b2d72eec406a40ede21acb5ac334d6563bf3a2387df",
+ "zh:a4484193d110da4a06c7bffc44cc6b61d3b5e881cd51df2a83fdda1a36ea25d2",
+ "zh:a53342426d173e29d8ee3106cb68abecdf4be301a3f6589e4e8d42015befa7da",
+ "zh:d25ef2aef6a9004363fc6db80305d30673fc1f7dd0b980d41d863b12dacd382a",
+ "zh:fa2d522fb323e2121f65b79709fd596514b293d816a1d969af8f72d108888e4c",
+ ]
+}
+
+provider "registry.opentofu.org/hashicorp/random" {
+ version = "3.6.3"
+ hashes = [
+ "h1:32/UZofQoXk8zPj9vpIDiSEmERA3Mx2VPvk1lHTTHvw=",
+ "zh:1bfd2e54b4eee8c761a40b6d99d45880b3a71abc18a9a7a5319204da9c8363b2",
+ "zh:21a15ac74adb8ba499aab989a4248321b51946e5431219b56fc827e565776714",
+ "zh:221acfac3f7a5bcd6cb49f79a1fca99da7679bde01017334bad1f951a12d85ba",
+ "zh:3026fcdc0c1258e32ab519df878579160b1050b141d6f7883b39438244e08954",
+ "zh:50d07a7066ea46873b289548000229556908c3be746059969ab0d694e053ee4c",
+ "zh:54280cdac041f2c2986a585f62e102bc59ef412cad5f4ebf7387c2b3a357f6c0",
+ "zh:632adf40f1f63b0c5707182853c10ae23124c00869ffff05f310aef2ed26fcf3",
+ "zh:b8c2876cce9a38501d14880a47e59a5182ee98732ad7e576e9a9ce686a46d8f5",
+ "zh:f27e6995e1e9fe3914a2654791fc8d67cdce44f17bf06e614ead7dfd2b13d3ae",
+ "zh:f423f2b7e5c814799ad7580b5c8ae23359d8d342264902f821c357ff2b3c6d3d",
+ ]
+}
+
+provider "registry.opentofu.org/hashicorp/time" {
+ version = "0.12.1"
+ hashes = [
+ "h1:D4eN1hzoSjOkkBg1dD13M5bzppQWosH/tkqYkeKjQks=",
+ "zh:50a9b67d5f5f42adbdb7712f67858aa64b5670070f6710751239b535fb48a4df",
+ "zh:5a846fae035e363aed75b966d64a56f3489a38083e8407aaa656730437f53ed7",
+ "zh:6767f1fc8a679b48eaa4cd114da0d8185fb3546375f3a0fb3728f10fa3dbc551",
+ "zh:85d3da407c828bf057cbc0e86c75ef3d0f9f74a73c4ea1b4aef18e33f41092b1",
+ "zh:9180721325139431112c638f5382a740ff219782f81d6346cdff5bccc418a43f",
+ "zh:9ba9989f905a64db1409a9a57649549c89c7aedfb55ae399a7fa9411aafaadac",
+ "zh:b3d9e7afb6a742e9be0541bc434b00d849fdfab0b4b859ceb0296c26c541af15",
+ "zh:c87da712d718acd9dd03f544b020c320699cb29df197be4f74783e3c3d80fc17",
+ "zh:cb1abe07638ef6d7b41d0e86dfb12d60a513aca3395a5da7191947f7459821dd",
+ "zh:ecff2e823ef49eda03663fa8ee8bdc17d27cd419dbdacbf1719f38812dbf417e",
+ ]
+}
+
+provider "registry.opentofu.org/integrations/github" {
+ version = "6.3.1"
+ constraints = "~> 6.0"
+ hashes = [
+ "h1:vKWgfpIrSNic7pYVi4LKIDQ2MgUpHq7uSj8nA8xfrw0=",
+ "zh:25ae1cb97ec528e6b7e9330489f4a33acc0fa80b909c113a8445656bc524c5b9",
+ "zh:3e1f6300dc10e52a54f13352770ed79f25ff4ba9ac49b776c52a655a3488a20b",
+ "zh:4aaf2877ec22e63358d7c9cd48c7d7947d1a1dc4d03231f0af193d8975d5918a",
+ "zh:4b904a81fac12a2a7606c8d811cb9c4e13581adcaaa19e503a067ac95c515925",
+ "zh:54fe7e0dca04e698631a5b86bdd43ef09a31375e68f8f89970b4315cd5fc6312",
+ "zh:6b14f92cf62784eaf20f43ef58ce966735f30d43deeab077943bd410c0d8b8b2",
+ "zh:86c49a1c11c024b26b6750c446f104922a3fe8464d3706a5fb9a4a05c6ca0b0a",
+ "zh:8939fb6332c4a58c4e90245eb9f0110987ccafff06b45a7ed513f2759a2abe6a",
+ "zh:8b4068a78c1f357325d1151facdb1aff506b9cd79d2bab21a55651255a130e2f",
+ "zh:ae22f5e52f534f19811d7f9480b4eb442f12ff16367b3893abb4e449b029ff6b",
+ "zh:afae9cfd9d49002ddfea552aa4844074b9974bd56ff2c2458f2297fe0df56a5b",
+ "zh:bc7a434408eb16a4fbceec0bd86b108a491408b727071402ad572cdb1afa2eb7",
+ "zh:c8e4728ea2d2c6e3d2c1bc5e7d92ed1121c02bab687702ec2748e3a6a0844150",
+ "zh:f6314b2cff0c0a07a216501cda51b35e6a4c66a2418c7c9966ccfe701e01b6b0",
+ "zh:fbd1fee2c9df3aa19cf8851ce134dea6e45ea01cb85695c1726670c285797e25",
+ ]
+}
diff --git a/terraform/bootstrap/README.md b/terraform/bootstrap/README.md
new file mode 100644
index 00000000..c309a92d
--- /dev/null
+++ b/terraform/bootstrap/README.md
@@ -0,0 +1,52 @@
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | > 1.7 |
+| [cloudfoundry](#requirement\_cloudfoundry) | ~> 0.5 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [cloudfoundry](#provider\_cloudfoundry) | 0.53.1 |
+
+## Modules
+
+| Name | Source | Version |
+|------|--------|---------|
+| [applications](#module\_applications) | ../modules/application | n/a |
+| [github](#module\_github) | ../modules/github | n/a |
+| [random](#module\_random) | ../modules/random | n/a |
+| [services](#module\_services) | ../modules/service | n/a |
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [cloudfoundry_app.external_applications](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/data-sources/app) | data source |
+| [cloudfoundry_domain.external](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/data-sources/domain) | data source |
+| [cloudfoundry_domain.internal](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/data-sources/domain) | data source |
+| [cloudfoundry_org.this](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/data-sources/org) | data source |
+| [cloudfoundry_service.this](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/data-sources/service) | data source |
+| [cloudfoundry_space.this](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/data-sources/space) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [cloudgov\_organization](#input\_cloudgov\_organization) | The organization for the cloud.gov account. | `string` | n/a | yes |
+| [cloudgov\_password](#input\_cloudgov\_password) | The password for the cloud.gov account. | `string` | n/a | yes |
+| [cloudgov\_space](#input\_cloudgov\_space) | The organization for the cloud.gov account. | `string` | n/a | yes |
+| [cloudgov\_username](#input\_cloudgov\_username) | The username for the cloudfoundry account. | `string` | n/a | yes |
+| [github\_organization](#input\_github\_organization) | The organization to use with GitHub. | `string` | `"GSA"` | no |
+| [github\_token](#input\_github\_token) | The token used authenticate with GitHub. | `string` | n/a | yes |
+| [mtls\_port](#input\_mtls\_port) | The default port to direct traffic to. Envoy proxy listens on 61443 and redirects to 8080, which the application should listen on. | `number` | `61443` | no |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [name](#output\_name) | n/a |
+
\ No newline at end of file
diff --git a/terraform/bootstrap/data.tf b/terraform/bootstrap/data.tf
new file mode 100755
index 00000000..6dae4d0a
--- /dev/null
+++ b/terraform/bootstrap/data.tf
@@ -0,0 +1,51 @@
+locals {
+ cloudfoundry = {
+ external_applications = try(data.cloudfoundry_app.external_applications, null)
+ domain_external = try(data.cloudfoundry_domain.external, null)
+ domain_internal = try(data.cloudfoundry_domain.internal, null)
+ organization = try(data.cloudfoundry_org.this, null)
+ services = try(data.cloudfoundry_service.this, null)
+ space = try(data.cloudfoundry_space.this, null)
+ }
+}
+
+data "cloudfoundry_app" "external_applications" {
+ for_each = {
+ for key, value in try(local.env.external_applications, []) : value.name => value
+ if try(value.deployed, false) &&
+ try(data.cloudfoundry_space.this.id, null) != null
+ }
+ name_or_id = format(local.env.name_pattern, each.key)
+ space = try(data.cloudfoundry_space.this.id, null)
+}
+
+data "cloudfoundry_domain" "external" {
+ //domain = "${split(".", local.env.external_domain)[1]}.${split(".", local.env.external_domain)[2]}"
+ domain = join(",", slice(split(".", local.env.external_domain), 0, 0))
+ sub_domain = split(".", local.env.external_domain)[0]
+}
+
+data "cloudfoundry_domain" "internal" {
+ domain = join(",", slice(split(".", local.env.external_domain), 0, 0))
+ sub_domain = split(".", local.env.internal_domain)[0]
+}
+
+data "cloudfoundry_org" "this" {
+ name = local.env.organization
+}
+
+data "cloudfoundry_space" "this" {
+ name = try(format(local.space_pattern, local.env.space), terraform.workspace)
+ org = data.cloudfoundry_org.this.id
+}
+
+
+data "cloudfoundry_service" "this" {
+ for_each = {
+ for key, value in try(local.env.services, {}) : key => value
+ if value.service_type != "user-provided" && try(data.cloudfoundry_space.this.id, null) != null
+ }
+
+ name = each.value.service_type
+ space = try(data.cloudfoundry_space.this.id, null)
+}
\ No newline at end of file
diff --git a/terraform/bootstrap/locals.tf b/terraform/bootstrap/locals.tf
new file mode 100644
index 00000000..7e642e56
--- /dev/null
+++ b/terraform/bootstrap/locals.tf
@@ -0,0 +1,273 @@
+locals {
+
+ ## The name of the project. Used to name most applications and services.
+ ## Default naming convention: ${local.project}-application-name-${terraform.workspace}
+ project = "digital-gov"
+
+ ## The full name of the project. If their isn't a longer name, this can be set to
+ ## local.project.
+ project_full = "${local.project}"
+
+ production_space = "prod"
+
+ repository = "GSA/digital-gov-drupal"
+
+ space_pattern = "%s"
+
+## The various environment settings to be deployed.
+ envs = {
+
+ ## Every environment gets settings in 'all'.
+ all = {
+
+ ## The API URL for cloud.gov.
+ api_url = "https://api.fr.cloud.gov"
+
+ ## These values are defaults values when options aren't configured in the application block.
+ defaults = {
+
+ ## The default size of the containers ephemeral disk.
+ disk_quota = 2048
+
+ ## Is SSH enabled on the container by default?
+ enable_ssh = true
+
+ ## The default health check timeout.
+ health_check_timeout = 60
+
+ ## Default method of performing a health check.
+ ## Valid options: "port", "process", or "http"
+ ## https://docs.cloudfoundry.org/devguide/deploy-apps/healthchecks.html
+ health_check_type = "port"
+
+ ## Default number of application instances to deploy.
+ instances = 1
+
+ ## Default amount of memory to use memory to use for an application.
+ memory = 64
+
+ port = 8080
+
+ ## The default cloudfoundry stack to deploy.
+ ## https://docs.cloudfoundry.org/devguide/deploy-apps/stacks.html
+ stack = "cflinuxfs4"
+
+ ## Is the application stopped by default?
+ stopped = false
+
+ ## Default CloudFoundry deployment strategy.
+ ## Valid optons: "none", "standard", or "blue-green".
+ ## https://docs.cloudfoundry.org/devguide/deploy-apps/rolling-deploy.html
+ strategy = "none"
+
+ ## Default wait time for an application to start.
+ timeout = 300
+ }
+
+ ## Configuration settings for the egress proxy application.
+ # egress = local.egress
+
+ ## External application based on the Terraform workspace being used.
+ external_applications = {}
+
+ ## The domain name for applications accessable external of cloud.gov.
+ external_domain = "app.cloud.gov"
+
+ ## The domain name for applications accessable inside of cloud.gov.
+ internal_domain = "apps.internal"
+
+ ## The naming convention/pattern for deployed systems and subsystems.
+ ## %s is replaced with the name of the system.
+ name_pattern = "${local.project}-%s-${terraform.workspace}"
+
+ ## The name of the cloud.gov organization.
+ organization = var.cloudgov_organization
+
+ ## Passwords that are generated for workspaces. By default, it's an empty map.
+ ## If one is defined below in a workspace's settings, it will supersed this one.
+ passwords = {
+ # test = {length = 32}
+ }
+
+ ## A copy of the project name, so it gets added to this setting object.
+ project = local.project
+
+ ## The name of the current Cloud.gov space.
+ space = "${terraform.workspace}"
+ }
+
+ ##
+ ##
+ ## The bootstrap workspace.
+ ## Used to initialize gobal/project wide applications/services.
+ ##
+ ##
+
+ bootstrap = {
+ secrets = {
+ PGDATABASE = {
+ encrypted = false
+ key = "db_name"
+ }
+ PGHOST = {
+ encrypted = false
+ key = "host"
+ }
+ PGPASSWORD = {
+ encrypted = false
+ key = "password"
+ }
+ PGPORT = {
+ encrypted = false
+ key = "port"
+ }
+ PG_CONN_STR = {
+ encrypted = false
+ key = "uri"
+ }
+ PGUSER = {
+ encrypted = false
+ key = "pg_user"
+ }
+ CF_USER = {
+ encrypted = false
+ key = "cf_user"
+ value = var.cloudgov_username
+ }
+ CF_PASSWORD = {
+ encrypted = false
+ key = "cf_password"
+ value = var.cloudgov_password
+ }
+ CF_ORG = {
+ encrypted = false
+ key = "cf_org"
+ value = var.cloudgov_organization
+ }
+ PROJECT = {
+ encrypted = false
+ key = "project"
+ value = local.project
+ }
+ TF_BACKEND_SPACE = {
+ encrypted = false
+ key = "tf_backend_space"
+ value = local.production_space
+ }
+ TF_BASTION = {
+ encrypted = false
+ key = "tf_bastion"
+ value = "${local.project}-tf-bastion-bootstrap"
+ }
+ }
+
+ services = {
+ terraform-backend = {
+ ## Applications to bind to this service.
+ applications = [ "tf-bastion" ]
+
+ ## The size of the instance to deploy.
+ service_plan = "micro-psql"
+
+ ## The type of service to be deployed.
+ service_type = "aws-rds"
+
+ ## Tags to add to the service.
+ tags = [
+ terraform.workspace
+ ]
+ }
+ }
+ space = "prod"
+
+ variables = {
+ "UBUNTU_VERSION" = {
+ key = "UBUNTU_VERSION"
+ value = "jammy"
+ }
+ "MODSECURITY_NGINX_VERSION" = {
+ key = "MODSECURITY_NGINX_VERSION"
+ value = "1.0.3"
+ }
+ }
+ }
+
+ "${local.production_space}" = {
+ apps = {
+ tf-bastion = {
+
+ ## Should the application have access to the internet?
+ allow_egress = true
+
+ ## Buildpacks to use with this application.
+ ## List buildpacks avalible with: cf buildpacks
+ buildpacks = [
+ "https://github.com/cloudfoundry/apt-buildpack",
+ "binary_buildpack"
+ ]
+
+ ## Command to run when container starts.
+ command = "./start"
+
+ ## Ephemeral disk storage.
+ disk_quota = 1024
+
+ ## Should SSH be enabled?
+ enable_ssh = true
+
+ ## Environmental variables. Avoid sensitive variables.
+ environment = {
+ CF_ORG = var.cloudgov_organization
+ CF_PASSWORD = var.cloudgov_password
+ CF_SPACE = var.cloudgov_space
+ CF_USER = var.cloudgov_username
+ OPENTOFU_VERSION = "1.8.4"
+ }
+
+ ## Timeout for health checks, in seconds.
+ health_check_timeout = 180
+
+ ## Type of health check.
+ ## Options: port, process, http
+ health_check_type = "process"
+
+ ## Number of instances of application to deploy.
+ instances = 1
+
+ ## Labels to add to the application.
+ labels = {
+ environment = "prod"
+ }
+
+ ## Maximum amount of memory the application can use.
+ memory = 512
+
+ ## Addional network policies to add to the application.
+ ## Format: name of the application and the port it is listening on.
+ network_policies = {}
+
+ ## Port the application uses.
+ #port = 0
+
+ ## Can the application be accessed outside of cloud.gov?
+ public_route = false
+
+ ## The source file should be a directory or a zip file.
+ source = "../applications/tf-bastion"
+
+ space = local.production_space
+
+ ## Templates take templated files and fill them in with sensitive data.
+ templates = []
+ }
+ }
+ }
+ }
+
+ ## Map of the 'all' environement and the current workspace settings.
+ env = merge(try(local.envs.all, {}), try(local.envs.bootstrap, {}))
+}
+
+output "name" {
+ value = local.env.passwords
+}
\ No newline at end of file
diff --git a/terraform/bootstrap/main.tf b/terraform/bootstrap/main.tf
new file mode 100755
index 00000000..0a81ee40
--- /dev/null
+++ b/terraform/bootstrap/main.tf
@@ -0,0 +1,86 @@
+locals {
+ ## Merging of the various credentials and environmental variables.
+ service_secrets = merge(
+ flatten(
+ [
+ for service_key, service_value in try(local.env.services, {}) : [
+ for key, value in try(module.services.results.service_key[service_key].credentials, {}) : {
+ "${key}" = nonsensitive(value)
+ }
+ ] if try(module.services.results.service_key[service_key].credentials, null) != null
+ ]
+ )
+ ...)
+
+ local_secrets = merge(
+ flatten(
+ [
+ for key, value in try(local.env.secrets, {}) : {
+ "${value.key}" = nonsensitive(value.value)
+ } if can(value.value)
+ ]
+ )
+ ...)
+
+ secrets = merge(local.service_secrets, local.local_secrets)
+
+ variables = merge(
+ flatten(
+ [
+ for key, value in try(local.env.variables, {}) : {
+ "${value.key}" = nonsensitive(value.value)
+ } if can(value.value)
+ ]
+ )
+ ...)
+}
+
+output "secrets" {
+ value = nonsensitive(local.secrets)
+}
+
+module "random" {
+ source = "../modules/random"
+ names = [ "dev" ]
+ passwords = local.env.passwords
+}
+
+## The instanced services (i.e. RDS, S3, etc.) get created first.
+## This allows their credentials to be injected into "user-provided" services (JSON blobs), if needed.
+module "services" {
+ source = "../modules/service"
+
+ cloudfoundry = local.cloudfoundry
+ env = local.env
+ skip_user_provided_services = true
+}
+
+# module "secrets" {
+# source = "../modules/service"
+
+# cloudfoundry = local.cloudfoundry
+# env = local.env
+# skip_service_instances = true
+# secrets = local.secrets
+# }
+
+module "applications" {
+ #for_each = local.cloudfoundry.spaces
+ source = "../modules/application"
+
+ cloudfoundry = local.cloudfoundry
+ env = merge(local.envs.all, local.envs.bootstrap, local.envs[local.production_space])
+ secrets = local.secrets
+ services = module.services.results
+}
+
+module "github" {
+ source = "../modules/github"
+
+ env = local.env
+ github_organization = var.github_organization
+ github_token = var.github_token
+ repository = local.repository
+ secrets = local.secrets
+ variables = local.variables
+}
diff --git a/terraform/bootstrap/provider.tf b/terraform/bootstrap/provider.tf
new file mode 100644
index 00000000..e27de2fa
--- /dev/null
+++ b/terraform/bootstrap/provider.tf
@@ -0,0 +1,21 @@
+terraform {
+ required_providers {
+ cloudfoundry = {
+ source = "cloudfoundry-community/cloudfoundry"
+ version = "~> 0.5"
+ }
+ }
+ required_version = "> 1.7"
+}
+
+provider "cloudfoundry" {
+ api_url = local.env.api_url
+ user = var.cloudgov_username
+ password = var.cloudgov_password
+}
+
+# Configure the GitHub Provider
+provider "github" {
+ owner = var.github_organization
+ token = var.github_token
+}
\ No newline at end of file
diff --git a/terraform/bootstrap/variables.tf b/terraform/bootstrap/variables.tf
new file mode 100644
index 00000000..69420fc9
--- /dev/null
+++ b/terraform/bootstrap/variables.tf
@@ -0,0 +1,40 @@
+variable "cloudgov_username" {
+ description = "The username for the cloudfoundry account."
+ type = string
+ sensitive = true
+}
+
+variable "cloudgov_password" {
+ description = "The password for the cloud.gov account."
+ type = string
+ sensitive = true
+}
+
+variable "cloudgov_organization" {
+ description = "The organization for the cloud.gov account."
+ type = string
+ sensitive = true
+}
+
+variable "cloudgov_space" {
+ description = "The organization for the cloud.gov account."
+ type = string
+ sensitive = true
+}
+
+variable "github_organization" {
+ description = "The organization to use with GitHub."
+ type = string
+ default = "GSA"
+}
+variable "github_token" {
+ description = "The token used authenticate with GitHub."
+ type = string
+ sensitive = true
+}
+
+variable "mtls_port" {
+ description = "The default port to direct traffic to. Envoy proxy listens on 61443 and redirects to 8080, which the application should listen on."
+ type = number
+ default = 61443
+}
\ No newline at end of file
diff --git a/terraform/docs/locals.tf.MD b/terraform/docs/locals.tf.MD
new file mode 100644
index 00000000..026e5086
--- /dev/null
+++ b/terraform/docs/locals.tf.MD
@@ -0,0 +1,35 @@
+# locals.tf
+
+This is a high level overview of the `locals.tf` file. The locals.tf file itself is heavily commented and will go into detail about individual settings if further information is required.
+
+The locals.tf is the main file that needs to be edited to configure your infrastructure.
+
+### Global variables
+
+#### project
+
+This variable holds the prefix of your resource names. For example, this project uses `benefit-finder` as a prefix for service names.
+
+#### project_full
+
+This variable is a longer, alternative name used in the project. For example, CircleCI calls this project `benefit-finder-gov`.
+
+#### bootstrap_workspace
+
+The name of the `bootstrap` workspace in Terraform. By default, it's `bootstrap`.
+
+#### global
+
+An object that sets commonly used applications and services (i.e. the WAF and the database), making configuration easier.
+
+#### egress
+
+Settings for the egress proxy that is deployed to the DMZ space.
+
+#### external_applications
+
+Settings for applications that aren't managed by Terraform. This is used to save pipeline variables to dynamically configure the other application.
+
+#### envs
+
+Settings for the majority of the deployment, that is then merged into a single `object`. The sub-object, `all` are configurations for every environment. The other sub-objects should be the name of your Terraform workspaces.
\ No newline at end of file
diff --git a/terraform/docs/scripts.MD b/terraform/docs/scripts.MD
new file mode 100644
index 00000000..49904319
--- /dev/null
+++ b/terraform/docs/scripts.MD
@@ -0,0 +1,75 @@
+# Cloud.gov Scripts
+
+These are scripts that are located in the `scripts` directory.
+
+## cloudgov-aws-creds.sh
+
+This script will export credentials to `AWS_ACCESS_KEY_ID`, `AWS_BUCKET`, `AWS_DEFAULT_REGION`, and `AWS_SECRET_ACCESS_KEY`. The export below, `bucket_name` is different than `AWS_BUCKET`, as `bucket_name` is the name of the Cloud.gov service, while `AWS_BUCKET` is the name of the bucket in AWS.
+
+After exporting the credentials, running `aws s3 ls s3://${AWS_BUCKET}/` should list the files in the bucket.
+
+After using the script, running the script again will delete the credentials, cleaning them up.
+
+- `deploy_space`: the space where you would like the account to be provisioned at.
+- `bucket_name`: the name of the bucket to generate credentials for.
+
+```
+export deploy_space="space_name_prod"
+export bucket_name="bucket_name"
+source ./cloudgov-aws-creds.sh
+```
+
+## cloud-gov-create-service-account.sh
+
+This creates pipeline service account credentials for your spaces. If credentials need to be regenerated or rotated, be sure to `tf apply` to the Terraform `bootstrap` environment to update the CircleCI variables.
+
+- `deploy_space`: the space where you would like the account to be provisioned at.
+- `org`: the name of the Cloud.gov organization your account is under.
+- `prefix`: A name that can be used as a resource prefix for every resource. It is optional.
+- `spaces`: A space separated string with all the spaces the service account should have access to.
+
+```
+export deploy_space="space_name_prod"
+export org="org_name"
+export prefix="name_prefix"
+export spaces="space_name_dev space_name_stage space_name_prod"
+bash init.sh
+```
+
+## egress-network-policy.sh
+
+This script allows public internet access from the provided `deploy_space` variable.
+
+***NOTE: This should only need to be ran once, during project setup.***
+
+- `deploy_space`: the space where you would like the account to be provisioned at.
+- `org`: the name of the Cloud.gov organization your account is under.
+
+```
+export deploy_space="space_name_dmz"
+export org="org_name"
+bash egress-network-policy.sh
+```
+
+## init.sh
+
+The `init.sh` script is located in the scripts directory of this repository. This script creates the S3 buckets for the Terraform backend and backups.
+
+After creating the S3 Buckets, the script will also execute `cloud-gov-create-service-account.sh`. This will create a service account that is used to deploy infrastructure from the pipeline.
+
+***NOTE: This should only need to be ran once, during project setup.***
+
+Before running this script, make sure to login to Cloud.gov with `cf login -a api.fr.cloud.gov --sso`.
+
+- `deploy_space`: the space where you would like the account to be provisioned at.
+- `org`: the name of the Cloud.gov organization your account is under.
+- `prefix`: A name that can be used as a resource prefix for every resource. It is optional.
+- `spaces`: A space separated string with all the spaces the service account should have access to.
+
+```
+export deploy_space="space_name_prod"
+export org="org_name"
+export prefix="name_prefix"
+export spaces="space_name_dev space_name_stage space_name_prod"
+bash init.sh
+```
\ No newline at end of file
diff --git a/terraform/infra/.terraform-docs.yaml b/terraform/infra/.terraform-docs.yaml
new file mode 100755
index 00000000..b480122f
--- /dev/null
+++ b/terraform/infra/.terraform-docs.yaml
@@ -0,0 +1,2 @@
+header-from: .terraform-docs/header.md
+footer-from: .terraform-docs/footer.md
diff --git a/terraform/infra/.terraform-docs/footer.md b/terraform/infra/.terraform-docs/footer.md
new file mode 100755
index 00000000..5c6a9055
--- /dev/null
+++ b/terraform/infra/.terraform-docs/footer.md
@@ -0,0 +1,76 @@
+### locals.tf Overview
+
+This is a high level overview of the `locals.tf` file. The locals.tf file itself is heavily commented and will go into detail about individual settings if further information is required.
+
+The locals.tf is the main file that needs to be edited to configure your infrastructure.
+
+#### Global variables
+
+##### project
+
+This variable holds the prefix of your resource names. For example, this project uses `benefit-finder` as a prefix for service names.
+
+##### project_full
+
+This variable is a longer, alternative name used in the project. For example, CircleCI calls this project `benefit-finder-gov`.
+
+##### bootstrap_workspace
+
+The name of the `bootstrap` workspace in Terraform. By default, it's `bootstrap`.
+
+##### global
+
+An object that sets commonly used applications and services (i.e. the WAF and the database), making configuration easier.
+
+##### egress
+
+Settings for the egress proxy that is deployed to the DMZ space.
+
+##### external_applications
+
+Settings for applications that aren't managed by Terraform. This is used to save pipeline variables to dynamically configure the other application.
+
+##### envs
+
+Settings for the majority of the deployment, that is then merged into a single `object`. The sub-object, `all` are configurations for every environment. The other sub-objects should be the name of your Terraform workspaces.
+
+### local.env.apps
+This is a `map` of `objects`.
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| buildpack | The admin buildpack name or Git URL | `string` | `null` | no |
+| buildpacks | A list of buildpack names and/or Git URLs | `list(string)` | `null` | no |
+| command | A custom start command for the application. A custom start command for the application. | `string` | `null` | no |
+| disk_quota | The size of the buildpack's ephemeral disk in megabytes. | `number` | `1024` | no |
+| docker_credentials | A custom start command for the application. | `map` | `null` | no |
+| docker_image | The URL to the docker image with tag. | `string` | `null` | no |
+| enable_ssh | Whether to enable or disable SSH access to the container. | `bool` | `true` | no |
+| environment | Key/value pairs of custom environment variables to set in your app. | `map` | `null` | no |
+| health_check_http_endpoint | The endpoint for the http health check type. | `string` | `"/"` | no |
+| health_check_invocation_timeout | The timeout in seconds for individual health check requests for "http" and "port" health checks. | `number` | `5` | no |
+| health_check_timeout | The timeout in seconds for the health check. | `number` | `180` | no |
+| health_check_type | The timeout in seconds for individual health check requests for "http" and "port" health checks. | `string` | `"port"` | no |
+| instances | The number of app instances that you want to start. | `number` | `1` | no |
+| labels | Adds labels to the application. | `map` | `null` | no |
+| memory | The memory limit for each application instance in megabytes. | `number` | `64` | no |
+| name | The name of the application. | `string` | n/a | yes |
+| path | An URI or path to target a zip file. If the path is a directory, the module will create a zip file. | `string` | n/a | yes |
+| space | The GUID of the associated Cloud Foundry space. | `string` | n/a | yes |
+| stack | The name of the stack the application will be deployed to. `cf stacks` will list valid options. | `string` | `"cflinuxfs4"` | no |
+| stopped | Defines the desired application state. Set to true to have the application remain in a stopped state. | `bool` | `false` | no |
+| strategy | Strategy ("none", "blue-green", or "rolling") to use for creating/updating application. | `string` | `"none"` | no |
+| timeout | Max wait time for app instance startup, in seconds. | `number` | `60` | no |
+
+### local.env.services
+This is a `map` of `objects`.
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| name | The name of the service instance. | `string` | n/a | yes |
+| json_params | A json string of arbitrary parameters. | `string` | `null` | no |
+| replace_on_params_change | Cloud Foundry will replace the resource on any params change. | `bool` | `false` | no |
+| replace_on_service_plan_change | Cloud Foundry will replace the resource on any service plan changes | `bool` | `false` | no |
+| space | The ID of the space. | `string` | n/a | yes |
+| service_plan | The ID of the service plan. | `string` | n/a | yes |
+| tags | List of instance tags. | `list(string)` | `[]` | no |
\ No newline at end of file
diff --git a/terraform/infra/.terraform-docs/header.md b/terraform/infra/.terraform-docs/header.md
new file mode 100755
index 00000000..1575d6ef
--- /dev/null
+++ b/terraform/infra/.terraform-docs/header.md
@@ -0,0 +1 @@
+# Cloud.gov Drupal Infrastructure
diff --git a/terraform/infra/.terraform.lock.hcl b/terraform/infra/.terraform.lock.hcl
new file mode 100644
index 00000000..913e3ce1
--- /dev/null
+++ b/terraform/infra/.terraform.lock.hcl
@@ -0,0 +1,93 @@
+# This file is maintained automatically by "tofu init".
+# Manual edits may be lost in future updates.
+
+provider "registry.opentofu.org/cloudfoundry-community/cloudfoundry" {
+ version = "0.51.2"
+ constraints = "~> 0.5, 0.51.2"
+ hashes = [
+ "h1:DAAWn0QmE75d6agoavWvchV6Ec5yOsxprPMMU7Q+xfM=",
+ "zh:2c15c7fbc8f15f6c21935d21c1eb8bab3e1454aec3476bc6fcda2d59bbd235a5",
+ "zh:3efe88cd4c40f1e90d71ceb94088d3ec2260ff01e4a4d722182c042b958c61f0",
+ "zh:41ea39daf091516f08cf6a5bc1efb88f19ac17ab8c146bc503d5a44c3c0fdd5a",
+ "zh:5287f2aade8821211426c8eed0a9dacaf41ab0be5a39ecf1526be2f5b71ab5a2",
+ "zh:7438d2dca479ace7720125c02c24660d4e928ee8b0ebd1514e2841b95d3563ef",
+ "zh:7583edf26c3160c4271c5ea473e799bca1f65da249d2e2e96dca69f2dce82c40",
+ "zh:8003fd57163a259d8005c7efa79d1d4d1cd3d98c26f82c58fa84f1e27c0f50d1",
+ "zh:8a5c05e59f4078193db1ceabd5350863cea869791e8ee5765472c0f36579cdcd",
+ "zh:8c0ccf62206c242b116ade68fee48c425b897c53f3da40d233c9c9a4a5fb514f",
+ "zh:9cc6ba428f1cd8c9a2d9bb3333dd16944cafcd2d2ca5af6fc040e4207cef4ea6",
+ "zh:a0fd393db027f03bde2b0056bfb04ce54827394eb31498b20e9beb3cb8e198b3",
+ "zh:a3d5cce15f8f611494c510f3a2dde2bd2841ffb2351541ed7f3177ea17f47a35",
+ "zh:bcf6edbebeabb36bf9254a2b6cdd3ea5e2f7d26836bd1392e6867a901d34c1ee",
+ "zh:e5a909abf388aa15af09ea1c9b6ec6fea5dd27ba4cefdf247b3afa90bb97cd3c",
+ "zh:fc2a42a8dbc41e216f63359087bfdaa02fe98717f4b793436e356013097c907a",
+ ]
+}
+
+provider "registry.opentofu.org/hashicorp/archive" {
+ version = "2.4.2"
+ hashes = [
+ "h1:tZcueUOGqjDRRzW9b6BMwV++XRqABodQjgC/K3bRoXM=",
+ "zh:0fee4f61bc999b5174a1268295e04c91c3f6be0160022cb53943b6ec0a3f1055",
+ "zh:10a895ee751beec68727d3dc6bf8e670f499618bb4b02649544be2c73e89603e",
+ "zh:1118373dfc03cf524273573e3aff9c99e0bb7128ab3ce0be211fd30e3928dfb8",
+ "zh:19c1b4c785f1d864e4fcaec7d96045437494efc333f1e661ea9994cd5c969cdb",
+ "zh:23f0aa399394ce8aa918a6a16ca9f5451d9d5b021e1b08929eb7972f65cb27da",
+ "zh:27d5daeec1819019a4b94c4980c09626e9cf71de3f54128a621fddb1b94b9ece",
+ "zh:56244088a96ff9e3a04b23de0ce2fcfa92c1a5fe6c91c6357cceda4d6d441c17",
+ "zh:578fcb23e8ebde3c5be6c5c67377b5e0c404cc807a74d7087e70c8fb3bb59b92",
+ "zh:9709d108559da5066f24a6d28be661b65a02e908f89b91fe42fc493962a5f466",
+ "zh:ff2a6df5d22bda78ca284756801ba7c86504e4bf0b48b31c8f5af44eefd9d0e8",
+ ]
+}
+
+provider "registry.opentofu.org/hashicorp/local" {
+ version = "2.5.1"
+ hashes = [
+ "h1:87L+rpGao062xifb1VuG9YVFwp9vbDP6G2fgfYxUkQs=",
+ "zh:031c2c2070672b7e78e0aa15560839278dc57fe7cf1e58a617ac13c67b31d5fb",
+ "zh:1ef64ea4f8382cd538a76f3d319f405d18130dc3280f1c16d6aaa52a188ecaa4",
+ "zh:422ce45691b2f384dbd4596fdc8209d95cb43d85a82aaa0173089d38976d6e96",
+ "zh:7415fbd8da72d9363ba55dd8115837714f9534f5a9a518ec42268c2da1b9ed2f",
+ "zh:92aa22d071339c8ef595f18a9f9245c287266c80689f5746b26e10eaed04d542",
+ "zh:9cd0d99f5d3be835d6336c19c4057af6274e193e677ecf6370e5b0de12b4aafe",
+ "zh:a8c1525b389be5809a97f02aa7126e491ba518f97f57ed3095a3992f2134bb8f",
+ "zh:b336fa75f72643154b07c09b3968e417a41293358a54fe03efc0db715c5451e6",
+ "zh:c66529133599a419123ad2e42874afbd9aba82bd1de2b15cc68d2a1e665d4c8e",
+ "zh:c7568f75ba6cb7c3660b69eaab8b0e4278533bd9a7a4c33ee6590cc7e69743ea",
+ ]
+}
+
+provider "registry.opentofu.org/hashicorp/random" {
+ version = "3.6.1"
+ hashes = [
+ "h1:egGGMQ18ihxoFBTgL/6aRL2N5/0bTI738Mg+TTsvBHA=",
+ "zh:1208af24d1f66e858740812dd5da12e8951b1ca75cc6edb1975ba22bfdeefb1b",
+ "zh:19137e9b4d3c15e1d99d2352888b98ec0e69bd5b2e89049150379d7bbd115063",
+ "zh:26613834a1a8ac60390c7a4cbd4cb794b01dfe237d2b0c10f132f3e434a21e03",
+ "zh:2cbe4425918f3f401609d89e6381f7d120493d637a3d103d827f0c0fd00b1600",
+ "zh:44ef27a972540435efa88f323280f96d6ac77934079225e7fcc3560cc28aae59",
+ "zh:8c5d4ca7d1ce007f7c055807cde77aad4685eb807ff802c93ffbec8589068f17",
+ "zh:9a4fa908d6af48805c862cd4f3a1031d552b96d863a94263e390ac92915d74a9",
+ "zh:ba396849f0f6d488784f6039095634e1c84e67e31375f3d17218fcf8ce952cb8",
+ "zh:cb695db8798957bd64ce411f061307e39cb2baa69668b4d42ccf010db47d2e39",
+ "zh:d02704bf99a93dc0b1ca00bd6051df9c431fbe17cd662a1ab58db1b96264a26f",
+ ]
+}
+
+provider "registry.opentofu.org/hashicorp/time" {
+ version = "0.11.1"
+ hashes = [
+ "h1:+S9YvR/HeCxFGMS3ITjOFqlWrR6DdarWWowT9Cz18/M=",
+ "zh:048c56f9f810f67a7460363a26bf3ef939d64f0d7b7342b9e7f24cc85ee1491b",
+ "zh:49f949cc5cb50fbb65f7b4578b79fbe02b6bafe9e3f5f1c2936114dd445b84b3",
+ "zh:553174a4fa88f6e186800d7ee155a6b5b4c6c81793643f1a20eab26becc7f823",
+ "zh:5cae304e21f77091d4b50389c655afd5e4e2e8d4cd9c06de139a31b8e7d343a9",
+ "zh:7aae20832bd9885f034831aa44db3a6ffcec034a2d5a2815d92c42c40c14ca1d",
+ "zh:93d715610dce777474b5eff1d7dbe797e72ca0b679cd8636efb3aa45d1cb589e",
+ "zh:bd29e04645775851eb10e7f3b39104ae57ca3632dec4ae07328d33d4182e7fb5",
+ "zh:d6ad6a4d52a6989b8452466f2ec3dbcdb00cc44a96bd1ca618d91a5d74895f49",
+ "zh:e68cfad3ec526631410fa9406938d624fd56b9ab065c76525cb3f731d106fbfe",
+ "zh:ffee8aa6b7ce56f4b8fdc0c492404be0041137a278388eb1d1180b637fb5b3de",
+ ]
+}
diff --git a/terraform/infra/.tflint.hcl b/terraform/infra/.tflint.hcl
new file mode 100755
index 00000000..64a6ccc3
--- /dev/null
+++ b/terraform/infra/.tflint.hcl
@@ -0,0 +1,26 @@
+config {
+ format = "compact"
+ plugin_dir = "~/.tflint.d/plugins"
+
+ module = true
+ force = false
+ disabled_by_default = false
+
+ varfile = ["terraform.tfvars"]
+}
+
+rule "terraform_unused_declarations" {
+ enabled = false
+}
+
+plugin "opa" {
+ enabled = true
+ version = "0.2.0"
+ source = "github.com/terraform-linters/tflint-ruleset-opa"
+}
+
+plugin "terraform" {
+ enabled = true
+ version = "0.2.2"
+ source = "github.com/terraform-linters/tflint-ruleset-terraform"
+}
\ No newline at end of file
diff --git a/terraform/infra/TERRAFORM.md b/terraform/infra/TERRAFORM.md
new file mode 100644
index 00000000..7012bbd7
--- /dev/null
+++ b/terraform/infra/TERRAFORM.md
@@ -0,0 +1,132 @@
+
+# Cloud.gov Drupal Infrastructure
+
+## Requirements
+
+No requirements.
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [cloudfoundry](#provider\_cloudfoundry) | 0.51.2 |
+
+## Modules
+
+| Name | Source | Version |
+|------|--------|---------|
+| [applications](#module\_applications) | ./modules/application | n/a |
+| [certificates](#module\_certificates) | ./modules/certificate | n/a |
+| [circleci](#module\_circleci) | ./modules/circleci | n/a |
+| [random](#module\_random) | ./modules/random | n/a |
+| [secrets](#module\_secrets) | ./modules/service | n/a |
+| [services](#module\_services) | ./modules/service | n/a |
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [cloudfoundry_app.egress_proxy](https://registry.terraform.io/providers/hashicorp/cloudfoundry/latest/docs/data-sources/app) | data source |
+| [cloudfoundry_app.external_applications](https://registry.terraform.io/providers/hashicorp/cloudfoundry/latest/docs/data-sources/app) | data source |
+| [cloudfoundry_domain.external](https://registry.terraform.io/providers/hashicorp/cloudfoundry/latest/docs/data-sources/domain) | data source |
+| [cloudfoundry_domain.internal](https://registry.terraform.io/providers/hashicorp/cloudfoundry/latest/docs/data-sources/domain) | data source |
+| [cloudfoundry_org.this](https://registry.terraform.io/providers/hashicorp/cloudfoundry/latest/docs/data-sources/org) | data source |
+| [cloudfoundry_route.egress_proxy](https://registry.terraform.io/providers/hashicorp/cloudfoundry/latest/docs/data-sources/route) | data source |
+| [cloudfoundry_service.this](https://registry.terraform.io/providers/hashicorp/cloudfoundry/latest/docs/data-sources/service) | data source |
+| [cloudfoundry_space.egress_proxy](https://registry.terraform.io/providers/hashicorp/cloudfoundry/latest/docs/data-sources/space) | data source |
+| [cloudfoundry_space.this](https://registry.terraform.io/providers/hashicorp/cloudfoundry/latest/docs/data-sources/space) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [circleci\_token](#input\_circleci\_token) | CircleCI token. | `string` | n/a | yes |
+| [cloudgov\_password](#input\_cloudgov\_password) | The password for the cloud.gov account. | `string` | n/a | yes |
+| [cloudgov\_username](#input\_cloudgov\_username) | The username for the cloudfoundry account. | `string` | n/a | yes |
+| [mtls\_port](#input\_mtls\_port) | The default port to direct traffic to. Envoy proxy listens on 61443 and redirects to 8080, which the application should listen on. | `number` | `61443` | no |
+| [newrelic\_key](#input\_newrelic\_key) | The API key for New Relic. | `string` | n/a | yes |
+| [no\_proxy](#input\_no\_proxy) | URIs that shouldn't be using the proxy to communicate. | `string` | `"apps.internal"` | no |
+| [proxy\_password](#input\_proxy\_password) | The proxy password. | `string` | n/a | yes |
+| [proxy\_username](#input\_proxy\_username) | The proxy username. | `string` | n/a | yes |
+
+## Outputs
+
+No outputs.
+
+### locals.tf Overview
+
+This is a high level overview of the `locals.tf` file. The locals.tf file itself is heavily commented and will go into detail about individual settings if further information is required.
+
+The locals.tf is the main file that needs to be edited to configure your infrastructure.
+
+#### Global variables
+
+##### project
+
+This variable holds the prefix of your resource names. For example, this project uses `benefit-finder` as a prefix for service names.
+
+##### project\_full
+
+This variable is a longer, alternative name used in the project. For example, CircleCI calls this project `benefit-finder-gov`.
+
+##### bootstrap\_workspace
+
+The name of the `bootstrap` workspace in Terraform. By default, it's `bootstrap`.
+
+##### global
+
+An object that sets commonly used applications and services (i.e. the WAF and the database), making configuration easier.
+
+##### egress
+
+Settings for the egress proxy that is deployed to the DMZ space.
+
+##### external\_applications
+
+Settings for applications that aren't managed by Terraform. This is used to save pipeline variables to dynamically configure the other application.
+
+##### envs
+
+Settings for the majority of the deployment, that is then merged into a single `object`. The sub-object, `all` are configurations for every environment. The other sub-objects should be the name of your Terraform workspaces.
+
+### local.env.apps
+This is a `map` of `objects`.
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| buildpack | The admin buildpack name or Git URL | `string` | `null` | no |
+| buildpacks | A list of buildpack names and/or Git URLs | `list(string)` | `null` | no |
+| command | A custom start command for the application. A custom start command for the application. | `string` | `null` | no |
+| disk\_quota | The size of the buildpack's ephemeral disk in megabytes. | `number` | `1024` | no |
+| docker\_credentials | A custom start command for the application. | `map` | `null` | no |
+| docker\_image | The URL to the docker image with tag. | `string` | `null` | no |
+| enable\_ssh | Whether to enable or disable SSH access to the container. | `bool` | `true` | no |
+| environment | Key/value pairs of custom environment variables to set in your app. | `map` | `null` | no |
+| health\_check\_http\_endpoint | The endpoint for the http health check type. | `string` | `"/"` | no |
+| health\_check\_invocation\_timeout | The timeout in seconds for individual health check requests for "http" and "port" health checks. | `number` | `5` | no |
+| health\_check\_timeout | The timeout in seconds for the health check. | `number` | `180` | no |
+| health\_check\_type | The timeout in seconds for individual health check requests for "http" and "port" health checks. | `string` | `"port"` | no |
+| instances | The number of app instances that you want to start. | `number` | `1` | no |
+| labels | Adds labels to the application. | `map` | `null` | no |
+| memory | The memory limit for each application instance in megabytes. | `number` | `64` | no |
+| name | The name of the application. | `string` | n/a | yes |
+| path | An URI or path to target a zip file. If the path is a directory, the module will create a zip file. | `string` | n/a | yes |
+| space | The GUID of the associated Cloud Foundry space. | `string` | n/a | yes |
+| stack | The name of the stack the application will be deployed to. `cf stacks` will list valid options. | `string` | `"cflinuxfs4"` | no |
+| stopped | Defines the desired application state. Set to true to have the application remain in a stopped state. | `bool` | `false` | no |
+| strategy | Strategy ("none", "blue-green", or "rolling") to use for creating/updating application. | `string` | `"none"` | no |
+| timeout | Max wait time for app instance startup, in seconds. | `number` | `60` | no |
+
+### local.env.services
+This is a `map` of `objects`.
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| name | The name of the service instance. | `string` | n/a | yes |
+| json\_params | A json string of arbitrary parameters. | `string` | `null` | no |
+| replace\_on\_params\_change | Cloud Foundry will replace the resource on any params change. | `bool` | `false` | no |
+| replace\_on\_service\_plan\_change | Cloud Foundry will replace the resource on any service plan changes | `bool` | `false` | no |
+| space | The ID of the space. | `string` | n/a | yes |
+| service\_plan | The ID of the service plan. | `string` | n/a | yes |
+| tags | List of instance tags. | `list(string)` | `[]` | no |
+
\ No newline at end of file
diff --git a/terraform/infra/data.tf b/terraform/infra/data.tf
new file mode 100755
index 00000000..59bab4b6
--- /dev/null
+++ b/terraform/infra/data.tf
@@ -0,0 +1,59 @@
+locals {
+ cloudfoundry = {
+ external_applications = try(data.cloudfoundry_app.external_applications, null)
+ domain_external = try(data.cloudfoundry_domain.external, null)
+ domain_internal = try(data.cloudfoundry_domain.internal, null)
+ organization = try(data.cloudfoundry_org.this, null)
+ services = try(data.cloudfoundry_service.this, null)
+ space = try(data.cloudfoundry_space.this, null)
+ }
+}
+
+data "cloudfoundry_app" "external_applications" {
+ for_each = {
+ for key, value in try(local.env.external_applications, []) : key => value
+ if try(value.deployed, false) &&
+ try(data.cloudfoundry_space.this.id, null) != null
+ }
+ name_or_id = format(local.env.name_pattern, each.key)
+ space = try(data.cloudfoundry_space.this.id, null)
+}
+
+data "cloudfoundry_domain" "external" {
+ //domain = "${split(".", local.env.external_domain)[1]}.${split(".", local.env.external_domain)[2]}"
+ domain = join(",", slice(split(".", local.env.external_domain), 0, 0))
+ sub_domain = split(".", local.env.external_domain)[0]
+}
+
+data "cloudfoundry_domain" "internal" {
+ domain = join(",", slice(split(".", local.env.external_domain), 0, 0))
+ sub_domain = split(".", local.env.internal_domain)[0]
+}
+
+data "cloudfoundry_org" "this" {
+ name = local.env.organization
+}
+
+data "cloudfoundry_space" "this" {
+ name = try(local.env.space, terraform.workspace)
+ org = data.cloudfoundry_org.this.id
+}
+
+data "cloudfoundry_service" "this" {
+ for_each = {
+ for key, value in try(local.env.services, {}) : key => value
+ if value.service_type != "user-provided" &&
+ try(data.cloudfoundry_space.this.id, null) != null
+ }
+
+ name = each.value.service_type
+ space = try(data.cloudfoundry_space.this.id, null)
+}
+
+data "cloudfoundry_asg" "trusted_local_networks_egress" {
+ name = "trusted_local_networks_egress"
+}
+
+data "cloudfoundry_asg" "public_networks_egress" {
+ name = "public_networks_egress"
+}
\ No newline at end of file
diff --git a/terraform/infra/dynamic.tf b/terraform/infra/dynamic.tf
new file mode 100644
index 00000000..6404b22a
--- /dev/null
+++ b/terraform/infra/dynamic.tf
@@ -0,0 +1,45 @@
+locals {
+
+ ## Map of service instances and secrets merged together.
+ services = {
+ instance = merge(
+ module.services.results.instance,
+ module.secrets.results.instance
+ )
+ user_provided = merge(
+ module.services.results.user_provided,
+ module.secrets.results.user_provided
+ )
+ service_key = merge(
+ module.services.results.service_key,
+ module.secrets.results.service_key
+ )
+ }
+
+ ## Merging of the various credentials and environmental variables.
+ secrets = merge(
+ merge(
+ flatten([
+ for app in try(local.env.services, []) : [
+ for key, value in try(module.services.results.service_key[app.name].credentials, {}) : {
+ "${app.name}_${key}" = value
+ }
+ ] if try(module.services.results.service_key[app.name].credentials, null) != null
+ ])
+ ...),
+ merge(
+ flatten([
+ for key, value in try(module.random.results, {}) : {
+ "${key}" = value.result
+ }
+ ])
+ ...)
+ )
+
+ ## List of the workspaces defined in the configuration above.
+ workspaces = flatten([
+ for key, value in local.envs : [
+ key
+ ]
+ ])
+}
diff --git a/terraform/infra/locals.tf b/terraform/infra/locals.tf
new file mode 100755
index 00000000..1653cd5a
--- /dev/null
+++ b/terraform/infra/locals.tf
@@ -0,0 +1,499 @@
+locals {
+
+ ## The name of the project. Used to name most applications and services.
+ ## Default naming convention: ${local.project}-application-name-${terraform.workspace}
+ project = "digital-gov"
+
+ ## The full name of the project. If their isn't a longer name, this can be set to
+ ## local.project.
+ project_full = "${local.project}"
+
+ ## The names of the project's production workspaces. This is used to adjust
+ ## settings dynamically throughout this configuration file.
+ production_workspaces = ["main", "dev"]
+
+ cms_fqdn = "https://bf-cms-${terraform.workspace}.bxdev.net"
+ static_fqdn = "https://bf-static-${terraform.workspace}.bxdev.net"
+
+ tf_backend = {
+ type = "pg"
+ name_pattern_psql = "${local.project}-terraform-backend-bootstrap"
+ name_pattern_secrets = "${local.project}--pg-secrets-bootstrap"
+ space = "prod"
+ }
+
+ ## "Common" applications and services that are deployed to every space.
+ globals = {
+ apps = {
+ ## Nginx Web Application Firewall (WAF).
+ waf = {
+
+ ## Should the application have access to the internet?
+ allow_egress = true
+
+ ## Buildpacks to use with this application.
+ ## List buildpacks avalible with: cf buildpacks
+ buildpacks = [
+ "https://github.com/cloudfoundry/apt-buildpack",
+ "nginx_buildpack"
+ ]
+
+ ## Command to run when container starts.
+ command = "./start"
+
+ ## Ephemeral disk storage.
+ disk_quota = 1024
+
+ ## Should SSH be enabled?
+ enable_ssh = true
+
+ ## Environmental variables. Avoid sensitive variables.
+ environment = {
+
+ ## IP addresses allowed to connected to the CMS.
+ ALLOWED_IPS_CMS = base64encode(
+ jsonencode([
+ "allow 0.0.0.0/0;"
+ ])
+ )
+
+ ## The OWASP CRS rules for modsecurity.
+ CRS_RULES = "coreruleset-4.7.0-minimal.tar.gz"
+
+ ## IP address that are denied access from the static website.
+ DENYED_IPS_STATIC = base64encode(jsonencode([]))
+
+ ## The current environment the application is running in.
+ ENV = terraform.workspace
+
+ ## Linux "Load Library Path", where system libraries are located. (i.e. libzip, gd, etc)
+ LD_LIBRARY_PATH = "/home/vcap/deps/0/lib/"
+
+ ## Ubuntu patch for newer version of mod security.
+ MODSECURITY_UPDATE = "libmodsecurity3_3.0.9-1_amd64.deb"
+
+ ## Domains that shouldn't be passed to the egress proxy server (i.e. apps.internal).
+ #no_proxy = var.no_proxy
+ }
+
+ ## Timeout for health checks, in seconds.
+ health_check_timeout = 180
+
+ ## Type of health check.
+ ## Options: port, process, http
+ health_check_type = "port"
+
+ ## Number of instances of application to deploy.
+ instances = 1
+
+ ## Labels to add to the application.
+ labels = {
+ environment = terraform.workspace
+ }
+
+ ## Maximum amount of memory the application can use.
+ memory = 96
+
+ ## Addional network policies to add to the application.
+ ## Format: name of the application and the port it is listening on.
+ network_policies = {
+ drupal = 61443
+ }
+
+ ## Port the application uses.
+ port = 80
+
+ ## Can the application be accessed outside of cloud.gov?
+ public_route = true
+
+ ## The source file should be a directory or a zip file.
+ source = "${path.cwd}/${var.terraform_working_dir}/applications/nginx-waf"
+
+ ## Templates take templated files and fill them in with sensitive data.
+ ## The proxy-to-static.conf has the S3 bucket written to it during
+ ## the 'terraform apply' command, before it the files are zipped up and
+ ## uploaded to cloud.gov.
+ templates = [
+ {
+ source = "${path.cwd}/${var.terraform_working_dir}/applications/nginx-waf/nginx/snippets/proxy-to-storage.conf.tmpl"
+ destination = "${path.cwd}/${var.terraform_working_dir}/applications/nginx-waf/nginx/snippets/proxy-to-storage.conf"
+ },
+ {
+ source = "${path.cwd}/${var.terraform_working_dir}/applications/nginx-waf/nginx/snippets/proxy-to-static.conf.tmpl"
+ destination = "${path.cwd}/${var.terraform_working_dir}/applications/nginx-waf/nginx/snippets/proxy-to-static.conf"
+ },
+ {
+ source = "${path.cwd}/${var.terraform_working_dir}/applications/nginx-waf/nginx/snippets/proxy-to-app.conf.tmpl"
+ destination = "${path.cwd}/${var.terraform_working_dir}/applications/nginx-waf/nginx/snippets/proxy-to-app.conf"
+ }
+ ]
+ }
+ database-backup-bastion = {
+
+ ## Should the application have access to the internet?
+ allow_egress = true
+
+ ## Buildpacks to use with this application.
+ ## List buildpacks avalible with: cf buildpacks
+ buildpacks = [
+ "https://github.com/cloudfoundry/apt-buildpack",
+ "binary_buildpack"
+ ]
+
+ ## Command to run when container starts.
+ command = "./start"
+
+ ## Ephemeral disk storage.
+ disk_quota = 1024
+
+ ## Should SSH be enabled?
+ enable_ssh = true
+
+ ## Environmental variables. Avoid sensitive variables.
+ environment = {
+ ## Linux "Load Library Path", where system libraries are located. (i.e. libzip, gd, etc)
+ LD_LIBRARY_PATH = "/home/vcap/deps/0/lib/"
+ }
+
+ ## Timeout for health checks, in seconds.
+ health_check_timeout = 180
+
+ ## Type of health check.
+ ## Options: port, process, http
+ health_check_type = "process"
+
+ ## Number of instances of application to deploy.
+ instances = 1
+
+ ## Labels to add to the application.
+ labels = {
+ environment = terraform.workspace
+ }
+
+ ## Maximum amount of memory the application can use.
+ memory = 64
+
+ services_external = [
+ "${local.project}-mysql-${terraform.workspace}",
+ "${local.project}-backup-${terraform.workspace}",
+ terraform.workspace == local.tf_backend.space ? "${local.project}-terraform-backend-default" : null
+ ]
+
+ ## The source file should be a directory or a zip file.
+ source = "../applications/database-backup-bastion"
+
+ space = terraform.workspace
+
+ #stopped = true
+ }
+ }
+
+ ## Services to deploy in this environment.
+ services = {
+
+ ## S3 storage for backups.
+ "backup" = {
+
+ ## Applications to bind to this service.
+ applications = []
+
+ ## Should a service key be generated for other applications to use?
+ service_key = true
+
+ ## The size of the instance to deploy.
+ service_plan = "basic"
+
+ ## The type of service to be deployed.
+ service_type = "s3"
+
+ ## Tags to add to the service.
+ tags = [
+ terraform.workspace
+ ]
+ },
+
+ ## MySQL RDS database.
+ "mysql" = {
+
+ ## Applications to bind to this service.
+ applications = ["cms"]
+
+ ## The size of the instance to deploy.
+ service_plan = contains(local.production_workspaces, terraform.workspace) ? "micro-mysql" : "micro-mysql"
+
+ ## The type of service to be deployed.
+ service_type = "aws-rds"
+
+ ## Tags to add to the service.
+ tags = [
+ terraform.workspace
+ ]
+ },
+
+ ## Credentials and other sensitive variables.
+ "secrets" = {
+
+ ## Applications to bind to this service.
+ applications = ["cms", "waf"]
+
+ ## Credentials that should be added to the json blob.
+ credentials = [
+ "cron_key",
+ "hash_salt",
+ "static_bucket",
+ "static_fips_endpoint",
+ "static_access_key_id",
+ "static_secret_access_key",
+ "storage_bucket",
+ "storage_fips_endpoint",
+ "storage_access_key_id",
+ "storage_secret_access_key"
+ ]
+
+ ## The type of service to be deployed.
+ service_type = "user-provided"
+
+ ## Tags to add to the service.
+ tags = [
+ terraform.workspace
+ ]
+ },
+
+ ## S3 storage for public files for Drupal.
+ ## Typically "sites/default/files/"
+ "storage" = {
+
+ ## Applications to bind to this service.
+ applications = ["cms", "waf"]
+
+ ## Should a service key be generated for other applications to use?
+ service_key = true
+
+ ## The size of the instance to deploy.
+ service_plan = "basic-public-sandbox"
+
+ ## The type of service to be deployed.
+ service_type = "s3"
+
+ ## Tags to add to the service.
+ tags = [
+ terraform.workspace
+ ]
+ },
+
+ # S3 storage for the statically generated site.
+ "static" = {
+
+ ## Applications to bind to this service.
+ applications = ["waf", "cms"]
+
+ ## Should a service key be generated for other applications to use?
+ service_key = true
+
+ ## The size of the instance to deploy.
+ service_plan = "basic-public-sandbox"
+
+ ## The type of service to be deployed.
+ service_type = "s3"
+
+ ## Tags to add to the service.
+ tags = [
+ terraform.workspace
+ ]
+ }
+ }
+ }
+
+ ## The mTLS port the proxy application uses.
+ ## Cloudfoundry will automatically redirect connections on this port to local port 8080.
+ mtls_port = var.mtls_port
+
+ ## Any applications that are external to this Terraform infrastucture.
+ ## In this case, the Drupal application is deployed via a manifest.yml in the Drupal
+ ## Github repostitory.
+ external_applications = {
+ cms = {
+
+ environement = "dev"
+
+ ## Port is the application listening on.
+ port = var.mtls_port
+ },
+ cms = {
+
+ environement = "main"
+
+ ## Port is the application listening on.
+ port = var.mtls_port
+ }
+ }
+
+ ## The various environment settings to be deployed.
+ envs = {
+
+ ## Every environment gets settings in 'all'.
+ all = {
+
+ ## The API URL for cloud.gov.
+ api_url = "https://api.fr.cloud.gov"
+
+ ## These values are defaults values when options aren't configured in the application block.
+ defaults = {
+
+ ## The default size of the containers ephemeral disk.
+ disk_quota = 2048
+
+ ## Is SSH enabled on the container by default?
+ enable_ssh = true
+
+ ## The default health check timeout.
+ health_check_timeout = 60
+
+ ## Default method of performing a health check.
+ ## Valid options: "port", "process", or "http"
+ ## https://docs.cloudfoundry.org/devguide/deploy-apps/healthchecks.html
+ health_check_type = "port"
+
+ ## Default number of application instances to deploy.
+ instances = 1
+
+ ## Default amount of memory to use memory to use for an application.
+ memory = 64
+
+ port = 8080
+
+ ## The default cloudfoundry stack to deploy.
+ ## https://docs.cloudfoundry.org/devguide/deploy-apps/stacks.html
+ stack = "cflinuxfs4"
+
+ ## Is the application stopped by default?
+ stopped = false
+
+ ## Default CloudFoundry deployment strategy.
+ ## Valid optons: "none", "standard", or "blue-green".
+ ## https://docs.cloudfoundry.org/devguide/deploy-apps/rolling-deploy.html
+ strategy = "none"
+
+ ## Default wait time for an application to start.
+ timeout = 300
+ }
+
+ ## Configuration settings for the egress proxy application.
+ # egress = local.egress
+
+ ## External application based on the Terraform workspace being used.
+ external_applications = try(local.external_applications, [])
+
+ ## The domain name for applications accessable external of cloud.gov.
+ external_domain = "app.cloud.gov"
+
+ ## The domain name for applications accessable inside of cloud.gov.
+ internal_domain = "apps.internal"
+
+ ## The naming convention/pattern for deployed systems and subsystems.
+ ## %s is replaced with the name of the system.
+ name_pattern = "${local.project}-%s-${terraform.workspace}"
+
+ ## The name of the cloud.gov organization.
+ organization = "gsa-digitalgov-prototyping"
+
+ ## Passwords that are generated for workspaces. By default, it's an empty map.
+ ## If one is defined below in a workspace's settings, it will supersed this one.
+ passwords = {
+ hash_salt = {
+ length = 32
+ }
+ cron_key = {
+ length = 32
+ }
+ }
+
+ ## A copy of the project name, so it gets added to this setting object.
+ project = local.project
+
+ ## The name of the current Cloud.gov space.
+ space = terraform.workspace
+ }
+
+ #################################
+ ##
+ ## ____
+ ## | _ \ _____ __
+ ## | | | |/ _ \ \ / /
+ ## | |_| | __/\ V /
+ ## |____/ \___| \_/
+ ##
+ #################################
+
+ dev = merge(
+ {
+ ## Applications to deploy.
+ apps = local.globals.apps
+ services = local.globals.services
+ },
+ {
+
+ ## Passwords that need to be generated for this environment.
+ ## These will actually use the sha256 result from the random module.
+ passwords = {
+ hash_salt = {
+ length = 32
+ }
+ cron_key = {
+ length = 32
+ }
+ }
+ }
+ )
+
+ #################################
+ ##
+ ## ____ _
+ ## | _ \ _ __ ___ __| |
+ ## | |_) | '__/ _ \ / _` |
+ ## | __/| | | (_) | (_| |
+ ## |_| |_| \___/ \__,_|
+ ##
+ #################################
+
+
+ main = merge(
+ {
+ ## Applications to deploy.
+ apps = local.globals.apps
+ services = local.globals.services
+ },
+ {
+
+ ## Passwords that need to be generated for this environment.
+ ## These will actually use the sha256 result from the random module.
+ passwords = {
+ hash_salt = {
+ length = 32
+ }
+ cron_key = {
+ length = 32
+ }
+ }
+ }
+ )
+ }
+
+ ## Map of the 'all' environement and the current workspace settings.
+ env = merge(try(local.envs.all, {}), try(local.envs[terraform.workspace], {}))
+
+ service_bindings = merge(
+ flatten(
+ [
+ for key, value in try(local.env.services, {}) : {
+ #svc_value.name => svc_value
+ "${key}" = value
+ }
+ ]
+ )
+ ...)
+}
+
+# output "name" {
+# value = local.env
+# }
\ No newline at end of file
diff --git a/terraform/infra/main.tf b/terraform/infra/main.tf
new file mode 100755
index 00000000..ec186f4a
--- /dev/null
+++ b/terraform/infra/main.tf
@@ -0,0 +1,65 @@
+module "random" {
+ source = "../modules/random"
+ names = local.workspaces
+ passwords = local.env.passwords
+}
+
+## Currently broken in CF provider v0.53.1.
+# resource "cloudfoundry_space_asgs" "trusted_local_networks_egress" {
+# space = data.cloudfoundry_space.this.id
+# running_asgs = [
+# data.cloudfoundry_asg.trusted_local_networks_egress.id,
+# data.cloudfoundry_asg.public_networks_egress.id
+# ]
+# staging_asgs = []
+# }
+
+## The instanced services (i.e. RDS, S3, etc.) get created first.
+## This allows their credentials to be injected into "user-provided" services (JSON blobs), if needed.
+module "services" {
+ source = "../modules/service"
+
+ cloudfoundry = local.cloudfoundry
+ env = local.env
+ skip_user_provided_services = true
+
+ depends_on = [
+ module.random
+ ]
+}
+
+module "secrets" {
+ source = "../modules/service"
+
+ cloudfoundry = local.cloudfoundry
+ env = local.env
+ skip_service_instances = true
+ secrets = local.secrets
+
+ depends_on = [
+ module.random
+ ]
+}
+
+module "applications" {
+ source = "../modules/application"
+
+ cloudfoundry = local.cloudfoundry
+ env = local.env
+ secrets = local.secrets
+ services = local.services
+
+ depends_on = [ module.services ]
+}
+
+# output "name" {
+# value = merge(
+# flatten(
+# [
+# for service in try(local.env.services, {}) : {
+# "${service.name}" = service
+# }
+# ]
+# )
+# ...)
+# }
\ No newline at end of file
diff --git a/terraform/infra/provider.tf b/terraform/infra/provider.tf
new file mode 100644
index 00000000..cc8f39a4
--- /dev/null
+++ b/terraform/infra/provider.tf
@@ -0,0 +1,19 @@
+terraform {
+ required_providers {
+ cloudfoundry = {
+ source = "cloudfoundry-community/cloudfoundry"
+ version = "~> 0.5"
+ }
+ }
+ required_version = "> 1.7"
+}
+
+terraform {
+ backend "pg" { }
+}
+
+provider "cloudfoundry" {
+ api_url = local.env.api_url
+ user = var.cloudgov_username
+ password = var.cloudgov_password
+}
diff --git a/terraform/infra/scripts/cloudgov-aws-creds.sh b/terraform/infra/scripts/cloudgov-aws-creds.sh
new file mode 100755
index 00000000..3c72a6d6
--- /dev/null
+++ b/terraform/infra/scripts/cloudgov-aws-creds.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+current_path=$(pwd)
+
+[ -z "${bucket_name}" ] && echo "No bucket name!" && help && exit 1
+
+echo "Getting bucket credentials..."
+{
+ current_space=$(cf target | grep space | awk '{print $2}')
+
+ cf target -s "${deploy_space}"
+
+ service_key="${bucket_name}-key"
+ s3_credentials=$(cf service-key "${bucket_name}" "${service_key}" | tail -n +2)
+} >/dev/null 2>&1
+
+if [ "${s3_credentials}" = "FAILED" ] ; then
+ echo "Key not found. Creating..."
+ {
+ cf create-service-key "${bucket_name}" "${service_key}"
+ s3_credentials=$(cf service-key "${bucket_name}" "${service_key}" | tail -n +2)
+ aws_access_key=$(echo "${s3_credentials}" | jq -r '.credentials.access_key_id')
+ aws_bucket_name=$(echo "${s3_credentials}" | jq -r '.credentials.bucket')
+ aws_bucket_region=$(echo "${s3_credentials}" | jq -r '.credentials.region')
+ aws_secret_key=$(echo "${s3_credentials}" | jq -r '.credentials.secret_access_key')
+ export AWS_ACCESS_KEY_ID=${aws_access_key}
+ export AWS_BUCKET=${aws_bucket_name}
+ export AWS_DEFAULT_REGION=${aws_bucket_region}
+ export AWS_SECRET_ACCESS_KEY=${aws_secret_key}
+
+ cf target -s "${current_space}"
+
+ } >/dev/null 2>&1
+else
+ echo "Key found. Deleting..."
+ {
+ current_space=$(cf target | grep space | awk '{print $2}')
+
+ cf target -s "${cf_space}"
+
+ cf delete-service-key "${bucket_name}" "${service_key}" -f
+
+ cf target -s "${current_space}"
+
+ } >/dev/null 2>&1
+fi
\ No newline at end of file
diff --git a/terraform/infra/scripts/cloudgov-create-service-account.sh b/terraform/infra/scripts/cloudgov-create-service-account.sh
new file mode 100755
index 00000000..5022b68e
--- /dev/null
+++ b/terraform/infra/scripts/cloudgov-create-service-account.sh
@@ -0,0 +1,84 @@
+#!/bin/bash
+
+org=""
+prefix=""
+deploy_space=""
+spaces=""
+
+current_path=$(pwd)
+tfvars_file="terraform.tfvars"
+
+help(){
+ echo "Usage: $0 [options]" >&2
+ echo
+ echo " -d Space to create the service token in. Likely production."
+ echo " -o Name of the Cloud.gov organization."
+ echo " -p Name of the service account prefix."
+ echo " -s List of spaces in your project. This gives the service account developer access to them."
+}
+
+while getopts 'd:ho:p:s:' flag; do
+ case "${flag}" in
+ d) deploy_space="${OPTARG}" ;;
+ h) help && exit 0 ;;
+ o) org="${OPTARG}" ;;
+ p) prefix="${OPTARG}-" ;;
+ s) spaces=(${OPTARG}) ;;
+ *) help && exit 1 ;;
+ esac
+done
+
+[[ -z "${org}" ]] && help && exit 1
+[[ -z "${prefix}" ]] && help && exit 1
+[[ -z "${deploy_space}" ]] && help && exit 1
+[[ -z "${spaces}" ]] && help && exit 1
+
+current_space=$(cf target | grep space -A 1 | awk '{print $2}')
+
+echo "Changing target space to the deployment space..."
+{
+ cf target -s ${deploy_space}
+} >/dev/null 2>&1
+
+echo "Checking service key..."
+while : ; do
+ {
+ service_key=$(cf service-key ${prefix}svc ${prefix}svc-key | sed '1,2d')
+ } >/dev/null 2>&1
+
+ if [[ ${service_key} == "" ]]; then
+ echo "Service key is missing!"
+ echo "Creating service account..."
+ # {
+ cf create-service cloud-gov-service-account space-deployer ${prefix}svc
+ # } >/dev/null 2>&1
+ echo "Creating service key..."
+ # {
+ cf create-service-key ${prefix}svc ${prefix}svc-key
+ # } >/dev/null 2>&1
+ else
+ export cloudgov_password=$(echo ${service_key} | jq -r '.credentials.password')
+ export cloudgov_username=$(echo ${service_key} | jq -r '.credentials.username')
+
+ for space in ${spaces[@]}; do
+ echo "Adding '${space}' to service account..."
+ cf set-space-role ${cloudgov_username} ${org} ${space} SpaceDeveloper
+ # >/dev/null 2>&1
+
+ echo "Allowing internet access for '${space}' deployment staging..."
+ cf bind-security-group public_networks ${org} --space ${space} --lifecycle staging
+ # >/dev/null 2>&1
+ done
+ break
+ fi
+ sleep 1
+done
+
+echo "Changing target space to the previous space..."
+{
+ cf target -s ${current_space}
+} >/dev/null 2>&1
+
+cp terraform.tfvars terraform.tfvars.tmp
+envsubst '$cloudgov_password,$cloudgov_username' < "${tfvars_file}.tmp" > ${tfvars_file}
+rm ${tfvars_file}.tmp
\ No newline at end of file
diff --git a/terraform/infra/scripts/egress-network-policy.sh b/terraform/infra/scripts/egress-network-policy.sh
new file mode 100755
index 00000000..6bd1f3ca
--- /dev/null
+++ b/terraform/infra/scripts/egress-network-policy.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+
+echo "Allowing internet access from '${deploy_space}'..."
+
+space_current=$(cf spaces | grep $(terraform workspace show))
+
+cf target -s ${deploy_space}
+
+cf bind-security-group public_networks_egress ${org} --space ${deploy_space}
+
+cf target -s ${space_current}
\ No newline at end of file
diff --git a/terraform/infra/scripts/init.sh b/terraform/infra/scripts/init.sh
new file mode 100755
index 00000000..b7834ce7
--- /dev/null
+++ b/terraform/infra/scripts/init.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+## If a project name is just a dash, no project name was set, so remove the dash.
+[ "${prefix}" = "-" ] && prefix=""
+
+echo "Creating terraform backend bucket..."
+{
+ service="${prefix}terraform-backend"
+ service_key="${service}-key"
+ cf create-service s3 basic "${service}"
+ cf create-service-key "${service}" "${service_key}"
+ s3_credentials=$(cf service-key "${service}" "${service_key}" | tail -n +2)
+
+ export backend_aws_access_key=$(echo "${s3_credentials}" | jq -r '.credentials.access_key_id')
+ export backend_aws_secret_key=$(echo "${s3_credentials}" | jq -r '.credentials.secret_access_key')
+ export backend_aws_bucket_name=$(echo "${s3_credentials}" | jq -r '.credentials.bucket')
+ export backend_aws_bucket_region=$(echo "${s3_credentials}" | jq -r '.credentials.region')
+
+ envsubst '$backend_aws_bucket_name,$backend_aws_bucket_region' < provider.tf.tmpl > provider.tf
+} >/dev/null 2>&1
+
+echo "Creating backup bucket..."
+{
+ service_backup="${prefix}backup"
+ cf create-service s3 basic "${service_backup}"
+} >/dev/null 2>&1
+
+./scripts/cloudgov-create-service-account.sh -d ${deploy_space} -o ${org} -p ${prefix} -s ${spaces}
\ No newline at end of file
diff --git a/terraform/infra/terraform.tfvars.tmpl b/terraform/infra/terraform.tfvars.tmpl
new file mode 100644
index 00000000..4b0a21f2
--- /dev/null
+++ b/terraform/infra/terraform.tfvars.tmpl
@@ -0,0 +1,2 @@
+cloudgov_password="$CF_PASSWORD"
+cloudgov_username="$CF_USER"
\ No newline at end of file
diff --git a/terraform/infra/variables.tf b/terraform/infra/variables.tf
new file mode 100755
index 00000000..4720b440
--- /dev/null
+++ b/terraform/infra/variables.tf
@@ -0,0 +1,23 @@
+variable "cloudgov_username" {
+ description = "The username for the cloudfoundry account."
+ type = string
+ sensitive = true
+}
+
+variable "cloudgov_password" {
+ description = "The password for the cloud.gov account."
+ type = string
+ sensitive = true
+}
+
+variable "terraform_working_dir" {
+ description = "Working directory for Terraform."
+ type = string
+ default = "digital-gov-drupal/terraform"
+}
+
+variable "mtls_port" {
+ description = "The default port to direct traffic to. Envoy proxy listens on 61443 and redirects to 8080, which the application should listen on."
+ type = number
+ default = 61443
+}
\ No newline at end of file
diff --git a/terraform/modules/application/.terraform-docs.yaml b/terraform/modules/application/.terraform-docs.yaml
new file mode 100755
index 00000000..b480122f
--- /dev/null
+++ b/terraform/modules/application/.terraform-docs.yaml
@@ -0,0 +1,2 @@
+header-from: .terraform-docs/header.md
+footer-from: .terraform-docs/footer.md
diff --git a/terraform/modules/application/.terraform-docs/footer.md b/terraform/modules/application/.terraform-docs/footer.md
new file mode 100755
index 00000000..4ee1fd20
--- /dev/null
+++ b/terraform/modules/application/.terraform-docs/footer.md
@@ -0,0 +1,85 @@
+## Example
+
+```terraform
+module "applications" {
+ source = "./modules/application"
+
+ cloudfoundry = local.cloudfoundry
+ env = local.env
+ secrets = local.secrets
+ services = local.services
+}
+```
+
+## Variables
+
+### cloudfoundry
+
+A variable that contains a `map(string)` of data lookups for pre-existing resources from Cloud.gov. This includes thing such as the organization and space ids. These are defined in `data.tf` in the root directory.
+
+### env
+
+A mixed type `object` variable that contains application settings. It is passed as an `any` type to allow optional variables to be ommitted from the object. It is defined in `locals.tf`, in the root directory. The object `local.env[terraform.workspace].apps` stores the values for the specific application that is to be deployed.
+
+Valid options are the attributes for the [cloudfoundry_app](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/resources/app) resource.
+
+### secrets
+
+A variable that has secrets and other credentials that the application uses. The `local.secrets` variable is generated in `locals_dynamic.tf`, as it merges a variety of credentials from the random and services modules.
+
+### services
+
+A variable that contains a `map(map(string))` of the services deployed in the environment. `local.services` is generated in `locals_dynamic.tf`, due to needing to be generated after the creation of the services, after the instance id are known. The services are then bound to the application.
+
+See the [service module](../service/readme.MD) for more information.
+
+## Usage
+
+Here is an example of how to define an application in `locals.tf`.
+
+```terraform
+locals {
+ env = {
+ workspace1 = {
+ apps = {
+ application1 = {
+ buildpacks = [
+ "staticfile_buildpack"
+ ]
+ command = "./start"
+ disk_quota = 256
+ enable_ssh = true
+ environment = {
+ environment = terraform.workspace
+ LD_LIBRARY_PATH = "/home/vcap/deps/0/lib/"
+ }
+ health_check_timeout = 180
+ health_check_type = "port"
+ instances = 1
+ labels = {
+ environment = terraform.workspace
+ }
+ memory = 64
+ port = 8080
+ public_route = false
+
+ source = "/path/to/application/directory"
+
+ templates = [
+ {
+ source = "${path.cwd}/path/to/templates/template.tmpl"
+ destination = "${path.cwd}}/path/to/templates/file"
+ }
+ ]
+ }
+ }
+ }
+ }
+}
+```
+
+## Additional Notes
+
+- Buildpacks
+ - Valid built-in Cloud.gov buildpacks can be found by running `cf buildpacks` from the CLI.
+ - External buildpacks, such as the `apt-buildpack` by referencing the URL to the buildpack repository: [https://github.com/cloudfoundry/apt-buildpack](https://github.com/cloudfoundry/apt-buildpack).
\ No newline at end of file
diff --git a/terraform/modules/application/.terraform-docs/header.md b/terraform/modules/application/.terraform-docs/header.md
new file mode 100755
index 00000000..cb180d7c
--- /dev/null
+++ b/terraform/modules/application/.terraform-docs/header.md
@@ -0,0 +1 @@
+# CloudFoundry Application Module
diff --git a/terraform/modules/application/README.md b/terraform/modules/application/README.md
new file mode 100644
index 00000000..22607fe8
--- /dev/null
+++ b/terraform/modules/application/README.md
@@ -0,0 +1,137 @@
+
+# CloudFoundry Application Module
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | > 1.7 |
+| [cloudfoundry](#requirement\_cloudfoundry) | ~> 0.5 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [archive](#provider\_archive) | n/a |
+| [cloudfoundry](#provider\_cloudfoundry) | ~> 0.5 |
+| [local](#provider\_local) | n/a |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [cloudfoundry_app.this](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/resources/app) | resource |
+| [cloudfoundry_network_policy.egress_proxy](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/resources/network_policy) | resource |
+| [cloudfoundry_network_policy.ingress_proxy](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/resources/network_policy) | resource |
+| [cloudfoundry_route.external](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/resources/route) | resource |
+| [cloudfoundry_route.internal](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/resources/route) | resource |
+| [local_sensitive_file.this](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/sensitive_file) | resource |
+| [archive_file.this](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [cloudfoundry](#input\_cloudfoundry) | Cloudfoundry settings. | object(
{
domain_external = object(
{
domain = string
id = string
internal = bool
name = string
org = string
sub_domain = string
}
)
domain_internal = object(
{
domain = string
id = string
internal = bool
name = string
org = string
sub_domain = string
}
)
external_applications = optional(
map(
object(
{
name = string
environement = string
port = optional(number, 61443)
}
)
),{}
)
organization = object(
{
annotations = map(string)
id = string
labels = map(string)
name = string
}
)
services = map(
object(
{
id = string
name = string
service_broker_guid = string
service_broker_name = string
service_plans = map(string)
space = string
}
)
)
space = object(
{
annotations = map(string)
id = string
labels = map(string)
name = string
org = string
org_name = string
quota = string
}
)
}
)
| n/a | yes |
+| [env](#input\_env) | The settings object for this environment. | object({
api_url = optional(string, "https://api.fr.cloud.gov")
apps = optional(
map(
object({
allow_egress = optional(bool, true)
buildpacks = list(string)
command = optional(string, "entrypoint.sh")
disk_quota = optional(number, 1024)
enable_ssh = optional(bool, false)
environment = optional(map(string), {})
health_check_timeout = optional(number, 180)
health_check_type = optional(string, "port")
instances = optional(number, 1)
labels = optional(map(string), {})
memory = optional(number, 96)
network_policies = optional(map(number),{})
port = optional(number, 80)
public_route = optional(bool, false)
space = optional(string ,null)
source = optional(string, null)
templates = list(map(string))
})
), {}
)
bootstrap_workspace = optional(string, "bootstrap")
defaults = object(
{
disk_quota = optional(number, 2048)
enable_ssh = optional(bool, true)
health_check_timeout = optional(number, 60)
health_check_type = optional(string, "port")
instances = optional(number, 1)
memory = optional(number, 64)
port = optional(number, 8080)
stack = optional(string, "cflinuxfs4")
stopped = optional(bool, false)
strategy = optional(string, "none")
timeout = optional(number, 300)
}
)
external_applications = optional(
map(
object({
enable_ssh = optional(bool, false)
instances = optional(number, 1)
memory = optional(number, 96)
port = optional(number, 61443)
})
), {}
)
external_domain = optional(string, "app.cloud.gov")
internal_domain = optional(string, "apps.internal")
name_pattern = string
organization = optional(string, "gsa-tts-usagov")
passwords = optional(
list(
object(
{
length = optional(number, 32)
}
)
), []
)
project = string
secrets = optional(
map(
object(
{
encrypted = bool
key = string
}
)
), {}
)
services = optional(
map(
object(
{
applications = optional(list(string), [])
environement = optional(string, "dev")
service_key = optional(bool, true)
service_plan = optional(string, "basic")
service_type = optional(string, "s3")
tags = optional(list(string), [])
}
)
), {}
)
space = string
})
| n/a | yes |
+| [secrets](#input\_secrets) | Sensitive credentials to be used to set application environmental variables. | `map(string)` | `{}` | no |
+| [services](#input\_services) | Services generated from the service module. | object(
{
instance = map(
object(
{
annotations = optional(string, null)
id = optional(string, null)
json_params = optional(string, null)
labels = optional(map(string), {})
name = optional(string, null)
recursive_delete = optional(bool, null)
replace_on_params_change = optional(bool, false)
replace_on_service_plan_change = optional(bool, false)
service_plan = optional(string, null)
space = optional(string, null)
tags = optional(list(string), null)
}
)
)
user_provided = map(
object(
{
annotations = optional(string, null)
id = optional(string, null)
json_params = optional(string, null)
labels = optional(map(string), {})
name = optional(string, null)
recursive_delete = optional(bool, null)
replace_on_params_change = optional(bool, false)
replace_on_service_plan_change = optional(bool, false)
service_plan = optional(string, null)
space = optional(string, null)
tags = optional(list(string), null)
}
)
)
service_key = map(
object(
{
name = optional(string, null)
service_instance = optional(string, null)
params = optional(map(string), null)
params_json = optional(string, null)
credentials = optional(map(string), {})
}
)
)
}
)
| `null` | no |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [apps](#output\_apps) | A `map` of [cloudfoundry\_app](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/resources/app) resource outputs. The key is the app name. |
+| [external\_endpoints](#output\_external\_endpoints) | A map of external URL's (app.cloud.gov) to used to reach an application. The key is the app name. |
+| [internal\_endpoints](#output\_internal\_endpoints) | A map of internal URL's (apps.internal) to used to reach an application. The key is the app name. |
+
+## Example
+
+```terraform
+module "applications" {
+ source = "./modules/application"
+
+ cloudfoundry = local.cloudfoundry
+ env = local.env
+ secrets = local.secrets
+ services = local.services
+}
+```
+
+## Variables
+
+### cloudfoundry
+
+A variable that contains a `map(string)` of data lookups for pre-existing resources from Cloud.gov. This includes thing such as the organization and space ids. These are defined in `data.tf` in the root directory.
+
+### env
+
+A mixed type `object` variable that contains application settings. It is passed as an `any` type to allow optional variables to be ommitted from the object. It is defined in `locals.tf`, in the root directory. The object `local.env[terraform.workspace].apps` stores the values for the specific application that is to be deployed.
+
+Valid options are the attributes for the [cloudfoundry\_app](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/resources/app) resource.
+
+### secrets
+
+A variable that has secrets and other credentials that the application uses. The `local.secrets` variable is generated in `locals_dynamic.tf`, as it merges a variety of credentials from the random and services modules.
+
+### services
+
+A variable that contains a `map(map(string))` of the services deployed in the environment. `local.services` is generated in `locals_dynamic.tf`, due to needing to be generated after the creation of the services, after the instance id are known. The services are then bound to the application.
+
+See the [service module](../service/readme.MD) for more information.
+
+## Usage
+
+Here is an example of how to define an application in `locals.tf`.
+
+```terraform
+locals {
+ env = {
+ workspace1 = {
+ apps = {
+ application1 = {
+ buildpacks = [
+ "staticfile_buildpack"
+ ]
+ command = "./start"
+ disk_quota = 256
+ enable_ssh = true
+ environment = {
+ environment = terraform.workspace
+ LD_LIBRARY_PATH = "/home/vcap/deps/0/lib/"
+ }
+ health_check_timeout = 180
+ health_check_type = "port"
+ instances = 1
+ labels = {
+ environment = terraform.workspace
+ }
+ memory = 64
+ port = 8080
+ public_route = false
+
+ source = "/path/to/application/directory"
+
+ templates = [
+ {
+ source = "${path.cwd}/path/to/templates/template.tmpl"
+ destination = "${path.cwd}}/path/to/templates/file"
+ }
+ ]
+ }
+ }
+ }
+ }
+}
+```
+
+## Additional Notes
+
+- Buildpacks
+ - Valid built-in Cloud.gov buildpacks can be found by running `cf buildpacks` from the CLI.
+ - External buildpacks, such as the `apt-buildpack` by referencing the URL to the buildpack repository: [https://github.com/cloudfoundry/apt-buildpack](https://github.com/cloudfoundry/apt-buildpack).
+
\ No newline at end of file
diff --git a/terraform/modules/application/data.tf b/terraform/modules/application/data.tf
new file mode 100644
index 00000000..131a2254
--- /dev/null
+++ b/terraform/modules/application/data.tf
@@ -0,0 +1,27 @@
+locals {
+
+ ## Create a single list of external service names. Multiple applications
+ ## could reference the same service, but the GUID only needs to be looked up once.
+ services_external = toset(
+ compact(
+ distinct(
+ flatten(
+ [
+ for value in try(var.env.apps, {}) : [
+ try(value.services_external, [])
+ ]
+ ]
+ )
+ )
+ )
+ )
+}
+
+## Lookup up service instance GUID's for existing services.
+## These can be externally deployed services or services deployed from different code sources.
+## The GUID can then be refrenced by data.cloudfoundry_service_instance.this["service-name"].id
+data "cloudfoundry_service_instance" "this" {
+ for_each = try(local.services_external, [])
+ name_or_id = each.value
+ space = try(var.cloudfoundry.space.id, null)
+}
\ No newline at end of file
diff --git a/terraform/modules/application/main.tf b/terraform/modules/application/main.tf
new file mode 100755
index 00000000..4e9c76c9
--- /dev/null
+++ b/terraform/modules/application/main.tf
@@ -0,0 +1,147 @@
+locals {
+ domains = merge(
+ merge(
+ flatten([
+ for key, value in try(var.env.apps, {}) : {
+ "${key}_internal_endpoint" = try(value.public_route, false) ? "${format(var.env.name_pattern, key)}.${var.env.external_domain}" : "${format(var.env.name_pattern, key)}.${var.env.internal_domain}"
+ }
+ ])
+ ...),
+ merge(
+ flatten([
+ for key, value in try(var.env.external_applications, {}) : {
+ "${key}_internal_endpoint" = try(value.public_route, false) ? "${format(var.env.name_pattern, key)}.${var.env.external_domain}" : "${format(var.env.name_pattern, key)}.${var.env.internal_domain}"
+ }
+ ])
+ ...)
+ )
+
+ service_keys = merge(
+ flatten([
+ for service_key, service_value in try(var.env.services, {}) : [
+ for key, value in try(var.services.service_key[service_key].credentials, {}) : {
+ "${service_key}_${key}" = value
+ } if try(var.services.service_key[service_key].credentials, null) != null
+ ]
+ ])
+ ...)
+
+ service_bindings = merge(
+ flatten(
+ [
+ for key, value in try(var.env.services, {}) : {
+ #svc_value.name => svc_value
+ "${key}" = value
+ }
+ ]
+ )
+ ...)
+}
+
+resource "local_sensitive_file" "this" {
+ for_each = { for key, value in flatten([
+ for key, value in try(var.env.apps, {}) : [
+ for kt, vt in try(value.templates, []) : {
+ name = basename(vt.destination)
+ source = vt.source
+ destination = vt.destination
+ vars = value.environment
+ }
+ ]
+ ]) : basename(value.destination) => value
+ }
+
+ content = templatefile(
+ each.value.source,
+ merge(
+ var.secrets,
+ local.domains,
+ local.service_keys
+ )
+ )
+ filename = each.value.destination
+}
+
+data "archive_file" "this" {
+ for_each = {
+ for key, value in try(var.env.apps, {}) : key => value
+ if try(value.source, null) != null && !endswith(try(value.source, ""), ".zip")
+ }
+
+ type = "zip"
+ source_dir = each.value.source
+ output_path = "/tmp/${var.env.project}-${each.key}-${terraform.workspace}.zip"
+
+ depends_on = [
+ local_sensitive_file.this
+ ]
+}
+
+resource "cloudfoundry_app" "this" {
+ for_each = {
+ for key, value in try(var.env.apps, {}) : key => value
+ }
+
+ buildpack = try(each.value.buildpack, null)
+ buildpacks = try(each.value.buildpacks, null)
+ command = try(each.value.command, null)
+ disk_quota = try(each.value.disk_quota, try(var.env.defaults.disk_quota, 1024))
+ docker_credentials = try(each.value.docker_credentials, null)
+ docker_image = try(each.value.docker_image, null)
+ enable_ssh = try(each.value.enable_ssh, try(var.env.defaults.enable_ssh, true))
+ environment = try(each.value.environment, {})
+ health_check_http_endpoint = try(each.value.health_check_http_endpoint, try(var.env.defaults.health_check_http_endpoint, null))
+ health_check_invocation_timeout = try(each.value.health_check_invocation_timeout, try(var.env.defaults.health_check_invocation_timeout, 5))
+ health_check_timeout = try(each.value.health_check_timeout, try(var.env.defaults.health_check_timeout, 180))
+ health_check_type = try(each.value.health_check_type, try(var.env.defaults.health_check_type, "port"))
+ instances = try(each.value.instances, try(var.env.defaults.instances, 1))
+ labels = try(each.value.labels, {})
+ memory = try(each.value.memory, try(var.env.defaults.memory, 64))
+ name = format(var.env.name_pattern, each.key)
+ path = endswith(try(each.value.source, ""), ".zip") ? each.value.source : "/tmp/${var.env.project}-${each.key}-${terraform.workspace}.zip"
+ source_code_hash = endswith(try(each.value.source, ""), ".zip") ? filebase64sha256(each.value.source) : data.archive_file.this[each.key].output_base64sha256
+ space = var.cloudfoundry.space.id
+ stack = try(each.value.stack, try(var.env.defaults.stack, "cflinux4"))
+ stopped = try(each.value.stopped, try(var.env.defaults.stopped, false))
+ strategy = try(each.value.strategy, try(var.env.defaults.strategy, "none"))
+ timeout = try(each.value.timeout, try(var.env.defaults.timeout, 60))
+
+ dynamic "service_binding" {
+ for_each = {
+ for svc_key, svc_value in try(var.env.services, {}) : svc_key => svc_value
+ if contains(svc_value.applications, each.key) && svc_value.service_type != "user-provided"
+ }
+ content {
+ service_instance = var.services.instance[service_binding.key].id
+ params_json = try(var.env.services[service_binding.key].params_json, null)
+ params = try(var.env.services[service_binding.key].params, {})
+ }
+ }
+
+ dynamic "service_binding" {
+ for_each = {
+ for svc_key, svc_value in try(var.env.services, {}) : svc_key => svc_value
+ if contains(svc_value.applications, each.key) &&
+ svc_value.service_type == "user-provided"
+ }
+ content {
+ service_instance = var.services.user_provided[service_binding.key].id
+ params_json = try(var.env.services[service_binding.key].params_json, null)
+ params = try(var.env.services[service_binding.key].params, {})
+ }
+ }
+
+ ## Bind any external services, not deployed by the root code calling this module.
+ dynamic "service_binding" {
+ for_each = try(local.services_external, [])
+ content {
+ service_instance = data.cloudfoundry_service_instance.this[service_binding.value].id
+ params_json = try(var.env.services[service_binding.value].params_json, null)
+ params = try(var.env.services[service_binding.value].params, {})
+ }
+ }
+
+ depends_on = [
+ data.archive_file.this,
+ ]
+}
diff --git a/terraform/modules/application/networking.tf b/terraform/modules/application/networking.tf
new file mode 100755
index 00000000..b10803fe
--- /dev/null
+++ b/terraform/modules/application/networking.tf
@@ -0,0 +1,32 @@
+locals {
+ merged_applications = merge(cloudfoundry_app.this, var.cloudfoundry.external_applications)
+}
+
+resource "cloudfoundry_network_policy" "ingress_proxy" {
+ for_each = {
+ for key, value in try(var.env.apps, []) : value.name => value
+ if try(value.network_policy, null) != null &&
+ try(var.cloudfoundry.external_applications[value.network_policy.name].id, null) != null
+ }
+ policy {
+ source_app = cloudfoundry_app.this[each.key].id
+ destination_app = var.cloudfoundry.external_applications[each.value.network_policy.name].id
+ port = try(var.env.apps[each.key].network_policy_app.port, 8080)
+ protocol = try(var.env.apps[each.key].network_policy_app.protocol, "tcp")
+ }
+}
+
+resource "cloudfoundry_network_policy" "egress_proxy" {
+ for_each = {
+ for key, value in try(var.env.apps, []) : value.name => value
+ if try(var.cloudfoundry.egress_app.id, null) != null &&
+ terraform.workspace != try(var.env.egress.workspace, null)
+ }
+
+ policy {
+ source_app = cloudfoundry_app.this[each.key].id
+ destination_app = try(var.cloudfoundry.egress_app.id, null)
+ port = try(var.env.egress.mtls_port, 61443)
+ protocol = try(var.env.egress.protocol, "tcp")
+ }
+}
diff --git a/terraform/modules/application/outputs.tf b/terraform/modules/application/outputs.tf
new file mode 100755
index 00000000..d8e4a883
--- /dev/null
+++ b/terraform/modules/application/outputs.tf
@@ -0,0 +1,34 @@
+output "apps" {
+ description = "A `map` of [cloudfoundry_app](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/resources/app) resource outputs. The key is the app name."
+ value = merge(
+ flatten([
+ for key, value in try(var.env.apps, {}) : {
+ "${key}" = try(cloudfoundry_app.this[key], null)
+ }
+ ])
+ ...)
+}
+
+output "external_endpoints" {
+ description = "A map of external URL's (app.cloud.gov) to used to reach an application. The key is the app name."
+ sensitive = true
+ value = merge(
+ flatten([
+ for key, value in try(var.env.apps, {}) : {
+ "${key}" = try(cloudfoundry_route.external[key].endpoint, null)
+ } if value.public_route
+ ])
+ ...)
+}
+
+output "internal_endpoints" {
+ description = "A map of internal URL's (apps.internal) to used to reach an application. The key is the app name."
+ sensitive = true
+ value = merge(
+ flatten([
+ for key, value in try(var.env.apps, {}) : {
+ "${key}" = try(cloudfoundry_route.internal[key].endpoint, null)
+ } if !value.public_route
+ ])
+ ...)
+}
\ No newline at end of file
diff --git a/terraform/modules/application/providers.tf b/terraform/modules/application/providers.tf
new file mode 100644
index 00000000..d106004d
--- /dev/null
+++ b/terraform/modules/application/providers.tf
@@ -0,0 +1,9 @@
+terraform {
+ required_providers {
+ cloudfoundry = {
+ source = "cloudfoundry-community/cloudfoundry"
+ version = "~> 0.5"
+ }
+ }
+ required_version = "> 1.7"
+}
diff --git a/terraform/modules/application/routes.tf b/terraform/modules/application/routes.tf
new file mode 100755
index 00000000..f240b628
--- /dev/null
+++ b/terraform/modules/application/routes.tf
@@ -0,0 +1,34 @@
+resource "cloudfoundry_route" "external" {
+ for_each = { for key, value in try(var.env.apps, {}) : key => value
+ if value.public_route && try(value.port, -1) != -1
+ }
+
+ domain = var.cloudfoundry.domain_external.id
+ #space = var.cloudfoundry.space.id
+ space = try(var.cloudfoundry.spaces[each.value.space].id, var.cloudfoundry.space.id)
+ hostname = format(var.env.name_pattern, each.key)
+ port = try(cloudfoundry_app.this[each.key].port, null)
+
+ target {
+ app = cloudfoundry_app.this[each.key].id
+ port = 0
+ }
+}
+
+resource "cloudfoundry_route" "internal" {
+ for_each = {
+ for key, value in try(var.env.apps, {}) : key => value
+ if !value.public_route && try(value.port, -1) != -1
+ }
+
+ domain = var.cloudfoundry.domain_internal.id
+ #space = var.cloudfoundry.space.id
+ space = try(var.cloudfoundry.spaces[each.value.space].id, var.cloudfoundry.space.id)
+ hostname = format(var.env.name_pattern, each.key)
+ port = try(cloudfoundry_app.this[each.key].port, null)
+
+ target {
+ app = cloudfoundry_app.this[each.key].id
+ port = 0
+ }
+}
diff --git a/terraform/modules/application/variables.tf b/terraform/modules/application/variables.tf
new file mode 100755
index 00000000..3a4649cc
--- /dev/null
+++ b/terraform/modules/application/variables.tf
@@ -0,0 +1,236 @@
+variable "cloudfoundry" {
+ description = "Cloudfoundry settings."
+ type = object(
+ {
+ domain_external = object(
+ {
+ domain = string
+ id = string
+ internal = bool
+ name = string
+ org = string
+ sub_domain = string
+ }
+ )
+ domain_internal = object(
+ {
+ domain = string
+ id = string
+ internal = bool
+ name = string
+ org = string
+ sub_domain = string
+ }
+ )
+ external_applications = optional(
+ map(
+ object(
+ {
+ name = string
+ environement = string
+ port = optional(number, 61443)
+ }
+ )
+ ),{}
+ )
+ organization = object(
+ {
+ annotations = map(string)
+ id = string
+ labels = map(string)
+ name = string
+ }
+ )
+ services = map(
+ object(
+ {
+ id = string
+ name = string
+ service_broker_guid = string
+ service_broker_name = string
+ service_plans = map(string)
+ space = string
+ }
+ )
+ )
+ space = object(
+ {
+ annotations = map(string)
+ id = string
+ labels = map(string)
+ name = string
+ org = string
+ org_name = string
+ quota = string
+ }
+ )
+ }
+ )
+}
+
+variable "env" {
+ description = "The settings object for this environment."
+ type = object({
+ api_url = optional(string, "https://api.fr.cloud.gov")
+ apps = optional(
+ map(
+ object({
+ allow_egress = optional(bool, true)
+ buildpacks = list(string)
+ command = optional(string, "entrypoint.sh")
+ disk_quota = optional(number, 1024)
+ enable_ssh = optional(bool, false)
+ environment = optional(map(string), {})
+ health_check_timeout = optional(number, 180)
+ health_check_type = optional(string, "port")
+ instances = optional(number, 1)
+ labels = optional(map(string), {})
+ memory = optional(number, 96)
+ network_policies = optional(map(number),{})
+ port = optional(number, -1)
+ public_route = optional(bool, false)
+ services_external = optional(list(string), [])
+ space = optional(string ,null)
+ source = optional(string, null)
+ stopped = optional(bool, false)
+ templates = optional(list(map(string)), [])
+ })
+ ), {}
+ )
+ bootstrap_workspace = optional(string, "bootstrap")
+ defaults = object(
+ {
+ disk_quota = optional(number, 2048)
+ enable_ssh = optional(bool, true)
+ health_check_timeout = optional(number, 60)
+ health_check_type = optional(string, "port")
+ instances = optional(number, 1)
+ memory = optional(number, 64)
+ port = optional(number, 8080)
+ stack = optional(string, "cflinuxfs4")
+ stopped = optional(bool, false)
+ strategy = optional(string, "none")
+ timeout = optional(number, 300)
+ }
+ )
+ external_applications = optional(
+ map(
+ object({
+ enable_ssh = optional(bool, false)
+ instances = optional(number, 1)
+ memory = optional(number, 96)
+ port = optional(number, 61443)
+ })
+ ), {}
+ )
+ external_domain = optional(string, "app.cloud.gov")
+ internal_domain = optional(string, "apps.internal")
+ name_pattern = string
+ organization = optional(string, "gsa-tts-usagov")
+ passwords = optional(
+ map(
+ object(
+ {
+ experation_days = optional(number, 0)
+ length = number
+ lower = optional(bool, false)
+ min_lower = optional(number, 0)
+ min_numeric = optional(number, 0)
+ min_special = optional(number, 0)
+ min_upper = optional(number, 0)
+ numeric = optional(bool, true)
+ override_special = optional(string, "!@#$%&*()-_=+[]{}<>:?")
+ special = optional(bool, true)
+ upper = optional(bool, true)
+ }
+ )
+ ), {}
+ )
+ project = string
+ secrets = optional(
+ map(
+ object(
+ {
+ encrypted = bool
+ key = string
+ }
+ )
+ ), {}
+ )
+ services = optional(
+ map(
+ object(
+ {
+ applications = optional(list(string), [])
+ environement = optional(string, "dev")
+ service_key = optional(bool, true)
+ service_plan = optional(string, "basic")
+ service_type = optional(string, "s3")
+ tags = optional(list(string), [])
+ }
+ )
+ ), {}
+ )
+ space = string
+ })
+}
+
+variable "secrets" {
+ description = "Sensitive credentials to be used to set application environmental variables."
+ type = map(string)
+ default = {}
+}
+
+variable "services" {
+ description = "Services generated from the service module."
+ type = object(
+ {
+ instance = map(
+ object(
+ {
+ annotations = optional(string, null)
+ id = optional(string, null)
+ json_params = optional(string, null)
+ labels = optional(map(string), {})
+ name = optional(string, null)
+ recursive_delete = optional(bool, null)
+ replace_on_params_change = optional(bool, false)
+ replace_on_service_plan_change = optional(bool, false)
+ service_plan = optional(string, null)
+ space = optional(string, null)
+ tags = optional(list(string), null)
+ }
+ )
+ )
+ user_provided = map(
+ object(
+ {
+ annotations = optional(string, null)
+ id = optional(string, null)
+ json_params = optional(string, null)
+ labels = optional(map(string), {})
+ name = optional(string, null)
+ recursive_delete = optional(bool, null)
+ replace_on_params_change = optional(bool, false)
+ replace_on_service_plan_change = optional(bool, false)
+ service_plan = optional(string, null)
+ space = optional(string, null)
+ tags = optional(list(string), null)
+ }
+ )
+ )
+ service_key = map(
+ object(
+ {
+ name = optional(string, null)
+ service_instance = optional(string, null)
+ params = optional(map(string), null)
+ params_json = optional(string, null)
+ credentials = optional(map(string), {})
+ }
+ )
+ )
+ }
+ )
+ default = null
+}
\ No newline at end of file
diff --git a/terraform/modules/circleci/.terraform-docs.yaml b/terraform/modules/circleci/.terraform-docs.yaml
new file mode 100755
index 00000000..b480122f
--- /dev/null
+++ b/terraform/modules/circleci/.terraform-docs.yaml
@@ -0,0 +1,2 @@
+header-from: .terraform-docs/header.md
+footer-from: .terraform-docs/footer.md
diff --git a/terraform/modules/circleci/.terraform-docs/footer.md b/terraform/modules/circleci/.terraform-docs/footer.md
new file mode 100755
index 00000000..84137580
--- /dev/null
+++ b/terraform/modules/circleci/.terraform-docs/footer.md
@@ -0,0 +1,12 @@
+## Examples
+
+```terraform
+module "circleci" {
+ source = "./modules/circleci"
+
+ env = local.env
+ services = local.services
+ secrets = local.secrets
+ schedules = local.env.circleci.schedules
+}
+```
diff --git a/terraform/modules/circleci/.terraform-docs/header.md b/terraform/modules/circleci/.terraform-docs/header.md
new file mode 100755
index 00000000..c0e2e473
--- /dev/null
+++ b/terraform/modules/circleci/.terraform-docs/header.md
@@ -0,0 +1,7 @@
+# CircleCI Module
+
+## Introduction
+
+This terraform module creates and sets CircleCI project/context variables and scheduled (cron-like) pipelines.
+
+** NOTE: Unless specific permissions are granted to the GSA project, the project won't have access to contexts.
\ No newline at end of file
diff --git a/terraform/modules/circleci/README.md b/terraform/modules/circleci/README.md
new file mode 100644
index 00000000..b0aa4ea8
--- /dev/null
+++ b/terraform/modules/circleci/README.md
@@ -0,0 +1,60 @@
+
+# CircleCI Module
+
+## Introduction
+
+This terraform module creates and sets CircleCI project/context variables and scheduled (cron-like) pipelines.
+
+** NOTE: Unless specific permissions are granted to the GSA project, the project won't have access to contexts.
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [circleci](#requirement\_circleci) | 0.8.2 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [circleci](#provider\_circleci) | 0.8.2 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [circleci_context.this](https://registry.terraform.io/providers/healx/circleci/0.8.2/docs/resources/context) | resource |
+| [circleci_context_environment_variable.this](https://registry.terraform.io/providers/healx/circleci/0.8.2/docs/resources/context_environment_variable) | resource |
+| [circleci_environment_variable.this](https://registry.terraform.io/providers/healx/circleci/0.8.2/docs/resources/environment_variable) | resource |
+| [circleci_schedule.schedule](https://registry.terraform.io/providers/healx/circleci/0.8.2/docs/resources/schedule) | resource |
+| [circleci_context.this](https://registry.terraform.io/providers/healx/circleci/0.8.2/docs/data-sources/context) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [context\_name](#input\_context\_name) | The CircleCI context to add variables to. | `string` | `null` | no |
+| [env](#input\_env) | Project environmental variables. | `any` | n/a | yes |
+| [schedules](#input\_schedules) | Set a scheduled pipeline. | `any` | `{}` | no |
+| [secrets](#input\_secrets) | Sensitive credentials to be used with the application. | `map(string)` | `{}` | no |
+
+## Outputs
+
+No outputs.
+
+## Examples
+
+```terraform
+module "circleci" {
+ source = "./modules/circleci"
+ env = local.env
+ services = local.services
+ secrets = local.secrets
+ schedules = local.env.circleci.schedules
+}
+```
+
\ No newline at end of file
diff --git a/terraform/modules/circleci/main.tf b/terraform/modules/circleci/main.tf
new file mode 100755
index 00000000..d777b9b9
--- /dev/null
+++ b/terraform/modules/circleci/main.tf
@@ -0,0 +1,92 @@
+locals {
+
+ ## Used as an "alias" so when 'hours_of_day' is set to '["*"]', it will replace it with every hour in a day.
+ every_hour = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
+
+ ## Used as an "alias" so when 'days_of_week' is set to '["*"]', it will replace it with every day in a week.
+ every_day = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]
+
+ ## Is the current workspace the 'bootstrap' environment?
+ is_bootstrap = terraform.workspace == var.env.bootstrap_workspace ? true : false
+
+ ## Is the current workspace the 'dmz' environment?
+ is_dmz = terraform.workspace == "dmz" ? true : false
+
+ ## Ignore 'bootstrap' and 'dmz' workspaces.
+ is_special_space = local.is_bootstrap || local.is_dmz ? true : false
+
+ ## A map of secrets filtered through the list of CircleCI variables.
+ /* circleci_variables = merge(
+ flatten([
+ for value in try(var.env.circleci_variables, []) : [
+ {
+ "${value}" = "${var.secrets["${value}"]}"
+ }
+ ]
+ ])
+ ...) */
+}
+
+## Get the context if 'var.context_name' is set and we aren't in the bootstrap environment.
+## NOTE: The context gets created in the 'bootstrap' environment.
+data "circleci_context" "this" {
+ count = try(var.context_name, null) != null && terraform.workspace != var.env.bootstrap_workspace ? 1 : 0
+ name = var.context_name
+}
+
+# Create the context if 'var.context' is set and we are in the bootstrap environment.
+resource "circleci_context" "this" {
+ count = try(var.context_name, null) != null && terraform.workspace == var.env.bootstrap_workspace ? 1 : 0
+ organization = try(var.env.circleci.organization, null)
+ name = var.context_name
+}
+
+## Creates a new environmental variable that is assigned to a context, if 'var.context_name' is set.
+resource "circleci_context_environment_variable" "this" {
+ for_each = {
+ for key, value in try(var.env.circleci_variables, []) : value => value
+ if var.context_name != null
+ }
+
+ context_id = try(data.circleci_context.this[0].id, circleci_context.this[0].id)
+ variable = local.is_special_space ? format("%s", each.key) : format("%s_%s", terraform.workspace, each.key)
+ value = var.secrets[each.key]
+}
+
+## Creates a new environmental that is assigned to the project, if 'var.context_name' is NOT set.
+resource "circleci_environment_variable" "this" {
+ for_each = {
+ for key, value in try(var.env.circleci_variables, []) : value => value
+ if var.context_name == null
+ }
+
+ project = var.env.circleci.project
+ name = local.is_special_space ? format("%s", each.key) : format("%s_%s", terraform.workspace, each.key)
+ value = var.secrets[each.key]
+}
+
+## Creates a scheduled pipeline, which runs at reoccuring times.
+resource "circleci_schedule" "schedule" {
+ for_each = {
+ for key, value in var.schedules : key => value
+ if !contains(value.ignore_workspace, terraform.workspace)
+ }
+
+ name = format(var.env.name_pattern, each.key)
+ organization = try(var.env.circleci.organization, null)
+ project = var.env.circleci.project
+ description = try(each.value.description, null)
+ per_hour = try(each.value.per_hour, 1)
+ hours_of_day = try(
+ each.value.hours_of_day[0] == "*" ? local.every_hour : try(each.value.hours_of_day,
+ [9,23])
+ )
+ days_of_week = try(
+ each.value.days_of_week[0] == "*" ? local.every_day : try(each.value.days_of_week,
+ ["MON", "TUES"])
+ )
+ use_scheduling_system = try(each.value.scheduling_system, true)
+ parameters_json = jsonencode(
+ try(each.value.parameters, {})
+ )
+}
diff --git a/terraform/modules/circleci/providers.tf b/terraform/modules/circleci/providers.tf
new file mode 100644
index 00000000..3c44cb3e
--- /dev/null
+++ b/terraform/modules/circleci/providers.tf
@@ -0,0 +1,9 @@
+terraform {
+ required_providers {
+ circleci = {
+ source = "healx/circleci"
+ version = "0.8.2"
+ }
+ }
+ required_version = "> 1.7"
+}
diff --git a/terraform/modules/circleci/variables.tf b/terraform/modules/circleci/variables.tf
new file mode 100755
index 00000000..f4eef240
--- /dev/null
+++ b/terraform/modules/circleci/variables.tf
@@ -0,0 +1,22 @@
+variable "context_name" {
+ description = "The CircleCI context to add variables to."
+ type = string
+ default = null
+}
+
+variable "env" {
+ description = "Project environmental variables."
+ type = any
+}
+
+variable "schedules" {
+ description = "Set a scheduled pipeline."
+ type = any
+ default = {}
+}
+
+variable "secrets" {
+ description = "Sensitive credentials to be used with the application."
+ type = map(string)
+ default = {}
+}
diff --git a/terraform/modules/github/.terraform-docs.yaml b/terraform/modules/github/.terraform-docs.yaml
new file mode 100755
index 00000000..b480122f
--- /dev/null
+++ b/terraform/modules/github/.terraform-docs.yaml
@@ -0,0 +1,2 @@
+header-from: .terraform-docs/header.md
+footer-from: .terraform-docs/footer.md
diff --git a/terraform/modules/github/.terraform-docs/footer.md b/terraform/modules/github/.terraform-docs/footer.md
new file mode 100755
index 00000000..e69de29b
diff --git a/terraform/modules/github/.terraform-docs/header.md b/terraform/modules/github/.terraform-docs/header.md
new file mode 100755
index 00000000..70ad68af
--- /dev/null
+++ b/terraform/modules/github/.terraform-docs/header.md
@@ -0,0 +1 @@
+# Github Secrets and Variables
diff --git a/terraform/modules/github/README.md b/terraform/modules/github/README.md
new file mode 100644
index 00000000..82bd599a
--- /dev/null
+++ b/terraform/modules/github/README.md
@@ -0,0 +1,42 @@
+
+# Github Secrets and Variables
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | > 1.7 |
+| [github](#requirement\_github) | ~> 6.0 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [github](#provider\_github) | ~> 6.0 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [github_actions_secret.this](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_secret) | resource |
+| [github_actions_variable.this](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_variable) | resource |
+| [github_repository.this](https://registry.terraform.io/providers/integrations/github/latest/docs/data-sources/repository) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [env](#input\_env) | The settings object for this environment. | object({
api_url = optional(string, "https://api.fr.cloud.gov")
apps = optional(
map(
object({
allow_egress = optional(bool, true)
buildpacks = list(string)
command = optional(string, "entrypoint.sh")
disk_quota = optional(number, 1024)
enable_ssh = optional(bool, false)
environment = optional(map(string), {})
health_check_timeout = optional(number, 180)
health_check_type = optional(string, "port")
instances = optional(number, 1)
labels = optional(map(string), {})
memory = optional(number, 96)
network_policies = optional(map(number),{})
port = optional(number, 80)
public_route = optional(bool, false)
source = optional(string, null)
templates = list(map(string))
})
), {}
)
bootstrap_workspace = optional(string, "bootstrap")
defaults = object(
{
disk_quota = optional(number, 2048)
enable_ssh = optional(bool, true)
health_check_timeout = optional(number, 60)
health_check_type = optional(string, "port")
instances = optional(number, 1)
memory = optional(number, 64)
port = optional(number, 8080)
stack = optional(string, "cflinuxfs4")
stopped = optional(bool, false)
strategy = optional(string, "none")
timeout = optional(number, 300)
}
)
external_applications = optional(
map(
object(
{
name = string
environement = string
port = optional(number, 61443)
}
)
),{}
)
external_domain = optional(string, "app.cloud.gov")
internal_domain = optional(string, "apps.internal")
name_pattern = string
organization = optional(string, "gsa-tts-usagov")
passwords = optional(
list(
object(
{
length = optional(number, 32)
}
)
), []
)
project = string
secrets = optional(
map(
object(
{
encrypted = bool
key = string
}
)
), {}
)
services = optional(
map(
object(
{
applications = optional(list(string), [])
environement = optional(string, "dev")
service_key = optional(bool, true)
service_plan = optional(string, "basic")
service_type = optional(string, "s3")
tags = optional(list(string), [])
}
)
), {}
)
space = string
})
| n/a | yes |
+| [github\_organization](#input\_github\_organization) | The organization to use with GitHub. | `string` | `"GSA"` | no |
+| [github\_token](#input\_github\_token) | The token used authenticate with GitHub. | `string` | n/a | yes |
+| [repository](#input\_repository) | The GitHub respository. | `string` | n/a | yes |
+| [secrets](#input\_secrets) | Secrets to create in the respository. | `map(string)` | `{}` | no |
+
+## Outputs
+
+No outputs.
+
\ No newline at end of file
diff --git a/terraform/modules/github/main.tf b/terraform/modules/github/main.tf
new file mode 100644
index 00000000..d26c8242
--- /dev/null
+++ b/terraform/modules/github/main.tf
@@ -0,0 +1,18 @@
+data "github_repository" "this" {
+ full_name = var.repository
+}
+
+resource "github_actions_secret" "this" {
+ for_each = { for key, value in try(var.env.secrets, []) : key => value }
+ repository = data.github_repository.this.name
+ secret_name = each.key
+ plaintext_value = !try(each.value.encrypted, false) ? try(var.secrets[each.value.key], null) : null
+ encrypted_value = try(each.value.encrypted, false) ? try(var.secrets[each.value.key], null) : null
+}
+
+resource "github_actions_variable" "this" {
+ for_each = { for key, value in try(var.variables, []) : key => value }
+ repository = data.github_repository.this.name
+ variable_name = each.key
+ value = each.value
+}
diff --git a/terraform/modules/github/provider.tf b/terraform/modules/github/provider.tf
new file mode 100644
index 00000000..a4f1bf27
--- /dev/null
+++ b/terraform/modules/github/provider.tf
@@ -0,0 +1,15 @@
+terraform {
+ required_providers {
+ github = {
+ source = "integrations/github"
+ version = "~> 6.0"
+ }
+ }
+ required_version = "> 1.7"
+}
+
+# Configure the GitHub Provider
+provider "github" {
+ owner = var.github_organization
+ token = var.github_token
+}
\ No newline at end of file
diff --git a/terraform/modules/github/variables.tf b/terraform/modules/github/variables.tf
new file mode 100644
index 00000000..b4fd4c50
--- /dev/null
+++ b/terraform/modules/github/variables.tf
@@ -0,0 +1,132 @@
+variable "env" {
+ description = "The settings object for this environment."
+ type = object({
+ api_url = optional(string, "https://api.fr.cloud.gov")
+ apps = optional(
+ map(
+ object({
+ allow_egress = optional(bool, true)
+ buildpacks = list(string)
+ command = optional(string, "entrypoint.sh")
+ disk_quota = optional(number, 1024)
+ enable_ssh = optional(bool, false)
+ environment = optional(map(string), {})
+ health_check_timeout = optional(number, 180)
+ health_check_type = optional(string, "port")
+ instances = optional(number, 1)
+ labels = optional(map(string), {})
+ memory = optional(number, 96)
+ network_policies = optional(map(number),{})
+ port = optional(number, 80)
+ public_route = optional(bool, false)
+ source = optional(string, null)
+ templates = list(map(string))
+ })
+ ), {}
+ )
+ bootstrap_workspace = optional(string, "bootstrap")
+ defaults = object(
+ {
+ disk_quota = optional(number, 2048)
+ enable_ssh = optional(bool, true)
+ health_check_timeout = optional(number, 60)
+ health_check_type = optional(string, "port")
+ instances = optional(number, 1)
+ memory = optional(number, 64)
+ port = optional(number, 8080)
+ stack = optional(string, "cflinuxfs4")
+ stopped = optional(bool, false)
+ strategy = optional(string, "none")
+ timeout = optional(number, 300)
+ }
+ )
+ external_applications = optional(
+ map(
+ object(
+ {
+ name = string
+ environement = string
+ port = optional(number, 61443)
+ }
+ )
+ ),{}
+ )
+ external_domain = optional(string, "app.cloud.gov")
+ internal_domain = optional(string, "apps.internal")
+ name_pattern = string
+ organization = optional(string, "gsa-tts-usagov")
+ passwords = optional(
+ map(
+ object(
+ {
+ experation_days = optional(number, 0)
+ length = number
+ lower = optional(bool, false)
+ min_lower = optional(number, 0)
+ min_numeric = optional(number, 0)
+ min_special = optional(number, 0)
+ min_upper = optional(number, 0)
+ numeric = optional(bool, true)
+ override_special = optional(string, "!@#$%&*()-_=+[]{}<>:?")
+ special = optional(bool, true)
+ upper = optional(bool, true)
+ }
+ )
+ ), {}
+ )
+ project = string
+ secrets = optional(
+ map(
+ object(
+ {
+ encrypted = bool
+ key = string
+ }
+ )
+ ), {}
+ )
+ services = optional(
+ map(
+ object(
+ {
+ applications = optional(list(string), [])
+ environement = optional(string, "dev")
+ service_key = optional(bool, true)
+ service_plan = optional(string, "basic")
+ service_type = optional(string, "s3")
+ tags = optional(list(string), [])
+ }
+ )
+ ), {}
+ )
+ space = string
+ })
+}
+
+variable "github_organization" {
+ description = "The organization to use with GitHub."
+ type = string
+ default = "GSA"
+}
+variable "github_token" {
+ description = "The token used authenticate with GitHub."
+ type = string
+ sensitive = true
+}
+
+variable "repository" {
+ description = "The GitHub respository."
+ type = string
+}
+
+variable "secrets" {
+ default = {}
+ description = "Secrets to create in the respository."
+ type = map(string)
+}
+
+variable "variables" {
+ default = {}
+ description = "Variables to create in the respository."
+ type = map(string)
+}
diff --git a/terraform/modules/random/.terraform-docs.yaml b/terraform/modules/random/.terraform-docs.yaml
new file mode 100755
index 00000000..b480122f
--- /dev/null
+++ b/terraform/modules/random/.terraform-docs.yaml
@@ -0,0 +1,2 @@
+header-from: .terraform-docs/header.md
+footer-from: .terraform-docs/footer.md
diff --git a/terraform/modules/random/.terraform-docs/footer.md b/terraform/modules/random/.terraform-docs/footer.md
new file mode 100755
index 00000000..794502f2
--- /dev/null
+++ b/terraform/modules/random/.terraform-docs/footer.md
@@ -0,0 +1,55 @@
+## Example
+
+```terraform
+module "random" {
+ source = "./modules/random"
+
+ names = ["dev", "stage", "prod"]
+ passwords = local.env.passwords
+}
+```
+
+
+## Usage
+
+### locals.tf
+
+Passwords to be generated are set in `local.env.passwords`.
+
+```terraform
+locals {
+ env = {
+ ...
+ workspace_name = {
+ ...
+ passwords = {
+ password1 = {
+ length = 16
+ special = false
+ }
+ }
+ }
+ }
+}
+```
+
+If the attribute `per_workspace` is set for `true`, then `multiple` resources will be created. It will prefix each resource name with each workspace name. It is useful to set this in the `bootstrap` "environment", allowing the passwords to be added as pipeline variables for each environment.
+
+```terraform
+locals {
+ env = {
+ ...
+ bootstrap = {
+ ...
+ passwords = {
+ password2 = {
+ length = 32
+ per_workspace = true
+ }
+ }
+ }
+ }
+}
+```
+
+If the `per_workspace` value isn't set or is `false`, only `single` resource will be created.
diff --git a/terraform/modules/random/.terraform-docs/header.md b/terraform/modules/random/.terraform-docs/header.md
new file mode 100755
index 00000000..330c132f
--- /dev/null
+++ b/terraform/modules/random/.terraform-docs/header.md
@@ -0,0 +1,5 @@
+# Random Module
+
+## Introduction
+
+This module generates random credentials and hashes that can be used in various applications.
\ No newline at end of file
diff --git a/terraform/modules/random/README.md b/terraform/modules/random/README.md
new file mode 100644
index 00000000..2f99e8a5
--- /dev/null
+++ b/terraform/modules/random/README.md
@@ -0,0 +1,102 @@
+
+# Random Module
+
+## Introduction
+
+This module generates random credentials and hashes that can be used in various applications.
+
+## Requirements
+
+No requirements.
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [random](#provider\_random) | n/a |
+| [time](#provider\_time) | n/a |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [random_password.multiple](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource |
+| [random_password.single](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource |
+| [time_rotating.multiple](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/rotating) | resource |
+| [time_rotating.single](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/rotating) | resource |
+| [time_static.multiple](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/static) | resource |
+| [time_static.single](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/static) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [names](#input\_names) | List of unique names for the multiple resources. | `list(string)` | `[]` | no |
+| [passwords](#input\_passwords) | A map of objects with password settings. | map(
object(
{
experation_days = optional(number, 0)
length = number
lower = optional(bool, false)
min_lower = optional(number, 0)
min_numeric = optional(number, 0)
min_special = optional(number, 0)
min_upper = optional(number, 0)
numeric = optional(bool, true)
override_special = optional(string, "!@#$%&*()-_=+[]{}<>:?")
special = optional(bool, true)
upper = optional(bool, true)
}
)
)
| n/a | yes |
+| [per\_workspace](#input\_per\_workspace) | Generate a password for each workspace. | `bool` | `false` | no |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [results](#output\_results) | A map(string) with the following attributes: result, md5, sha1sha256, and sha512. |
+
+## Example
+
+```terraform
+module "random" {
+ source = "./modules/random"
+
+ names = ["dev", "stage", "prod"]
+ passwords = local.env.passwords
+}
+```
+
+## Usage
+
+### locals.tf
+
+Passwords to be generated are set in `local.env.passwords`.
+
+```terraform
+locals {
+ env = {
+ ...
+ workspace_name = {
+ ...
+ passwords = {
+ password1 = {
+ length = 16
+ special = false
+ }
+ }
+ }
+ }
+}
+```
+
+If the attribute `per_workspace` is set for `true`, then `multiple` resources will be created. It will prefix each resource name with each workspace name. It is useful to set this in the `bootstrap` "environment", allowing the passwords to be added as pipeline variables for each environment.
+
+```terraform
+locals {
+ env = {
+ ...
+ bootstrap = {
+ ...
+ passwords = {
+ password2 = {
+ length = 32
+ per_workspace = true
+ }
+ }
+ }
+ }
+}
+```
+
+If the `per_workspace` value isn't set or is `false`, only `single` resource will be created.
+
\ No newline at end of file
diff --git a/terraform/modules/random/main.tf b/terraform/modules/random/main.tf
new file mode 100755
index 00000000..0c63e241
--- /dev/null
+++ b/terraform/modules/random/main.tf
@@ -0,0 +1,85 @@
+locals {
+ passwords = merge(
+ flatten([
+ for name in var.names : [
+ for key, value in var.passwords : {
+ "${name}_${key}" = value
+ } if try(value.per_workspace, false)
+ ]
+ ])
+ ...)
+}
+
+resource "time_rotating" "single" {
+ for_each = {
+ for key, value in var.passwords : key => value
+ if try(value.expiration, 0) > 0
+ }
+ rotation_days = each.value.expiration_days
+}
+
+resource "time_static" "single" {
+ for_each = {
+ for key, value in var.passwords : key => value
+ if !var.per_workspace && try(value.expiration, 0) == 0
+ }
+}
+
+resource "random_password" "single" {
+ for_each = {
+ for key, value in var.passwords : key => value
+ if !var.per_workspace
+ }
+
+ length = try(each.value.length, 16)
+ lower = try(each.value.lower, true)
+ min_lower = try(each.value.min_lower, 0)
+ min_numeric = try(each.value.min_numeric, 0)
+ min_special = try(each.value.min_special, 0)
+ numeric = try(each.value.numeric, true)
+ override_special = try(each.value.override_special, "!#$%&*()-_=+[]{}<>:?")
+ special = try(each.value.special, false)
+ upper = try(each.value.upper, true)
+
+
+ keepers = {
+ id = try(time_rotating.single[each.key].id, null) != null ? time_rotating.single[each.key].id : time_static.single[each.key].id
+ }
+}
+
+resource "time_rotating" "multiple" {
+ for_each = {
+ for key, value in local.passwords : key => value
+ if var.per_workspace && try(value.expiration, 0) > 0
+ }
+ rotation_days = each.value.expiration_days
+}
+
+resource "time_static" "multiple" {
+ for_each = {
+ for key, value in local.passwords : key => value
+ if var.per_workspace && try(value.expiration, 0) == 0
+ }
+}
+
+resource "random_password" "multiple" {
+ for_each = {
+ for key, value in local.passwords : key => value
+ if var.per_workspace
+ }
+
+ length = try(each.value.length, 16)
+ lower = try(each.value.lower, true)
+ min_lower = try(each.value.min_lower, 0)
+ min_numeric = try(each.value.min_numeric, 0)
+ min_special = try(each.value.min_special, 0)
+ numeric = try(each.value.numeric, true)
+ override_special = try(each.value.override_special, "!#$%&*()-_=+[]{}<>:?")
+ special = try(each.value.special, false)
+ upper = try(each.value.upper, true)
+
+ keepers = {
+ id = try(time_rotating.multiple[each.key].id, null) != null ? time_rotating.multiple[each.key].id : time_static.multiple[each.key].id
+ }
+}
+
diff --git a/terraform/modules/random/outputs.tf b/terraform/modules/random/outputs.tf
new file mode 100755
index 00000000..9752a4fc
--- /dev/null
+++ b/terraform/modules/random/outputs.tf
@@ -0,0 +1,31 @@
+output "results" {
+ description = "A map(string) with the following attributes: result, md5, sha1sha256, and sha512."
+ value = merge(
+ merge(
+ flatten([
+ for key, value in var.passwords : {
+ "${key}" = {
+ md5 = md5(random_password.single[key].result)
+ result = random_password.single[key].result
+ sha1 = sha1(random_password.single[key].result)
+ sha256 = sha256(random_password.single[key].result)
+ sha512 = sha512(random_password.single[key].result)
+ }
+ } if !var.per_workspace
+ ])
+ ...),
+ merge(
+ flatten([
+ for key, value in local.passwords : {
+ "${key}" = {
+ md5 = md5(random_password.multiple[key].result)
+ result = random_password.multiple[key].result
+ sha1 = sha1(random_password.multiple[key].result)
+ sha256 = sha256(random_password.multiple[key].result)
+ sha512 = sha512(random_password.multiple[key].result)
+ }
+ } if var.per_workspace
+ ])
+ ...)
+ )
+}
\ No newline at end of file
diff --git a/terraform/modules/random/variables.tf b/terraform/modules/random/variables.tf
new file mode 100755
index 00000000..337e6a84
--- /dev/null
+++ b/terraform/modules/random/variables.tf
@@ -0,0 +1,32 @@
+variable "names" {
+ type = list(string)
+ description = "List of unique names for the multiple resources."
+ default = []
+}
+
+variable "passwords" {
+ description = "A map of objects with password settings."
+ type = map(
+ object(
+ {
+ experation_days = optional(number, 0)
+ length = number
+ lower = optional(bool, false)
+ min_lower = optional(number, 0)
+ min_numeric = optional(number, 0)
+ min_special = optional(number, 0)
+ min_upper = optional(number, 0)
+ numeric = optional(bool, true)
+ override_special = optional(string, "!@#$%&*()-_=+[]{}<>:?")
+ special = optional(bool, true)
+ upper = optional(bool, true)
+ }
+ )
+ )
+}
+
+variable "per_workspace" {
+ type = bool
+ description = "Generate a password for each workspace."
+ default = false
+}
\ No newline at end of file
diff --git a/terraform/modules/service/.terraform-docs.yaml b/terraform/modules/service/.terraform-docs.yaml
new file mode 100755
index 00000000..b480122f
--- /dev/null
+++ b/terraform/modules/service/.terraform-docs.yaml
@@ -0,0 +1,2 @@
+header-from: .terraform-docs/header.md
+footer-from: .terraform-docs/footer.md
diff --git a/terraform/modules/service/.terraform-docs/footer.md b/terraform/modules/service/.terraform-docs/footer.md
new file mode 100755
index 00000000..c5a416d6
--- /dev/null
+++ b/terraform/modules/service/.terraform-docs/footer.md
@@ -0,0 +1,42 @@
+## Examples
+
+### Basic
+```terraform
+module "services" {
+ source = "./modules/service"
+
+ cloudfoundry = local.cloudfoundry
+ env = local.env
+}
+```
+
+### Advanced
+
+This advanced example will first generate service instances, such as RDS, along with other defined services, except for the `user defined` services. `User defined` services are useful for providing variables at runtime to applications. The issue is that until a service, such as RDS is deployed, their isn't a username and password created for that instance.
+
+The first step is to initalize any services that are not `user defined`, but setting `skip_user_provided_services` to `true`.
+
+```terraform
+module "services" {
+ source = "./modules/service"
+
+ cloudfoundry = local.cloudfoundry
+ env = local.env
+
+ skip_user_provided_services = true
+}
+```
+
+After the services are generated, another module block can be defined, which will pass a merged `map(string)` called `secrets`, that have the various information that is to be added to the `user defined` service. Setting the `skip_service_instances` to `true` will prevent the module from trying to redploy any non `user defined` service.
+
+```terraform
+module "secrets" {
+ source = "./modules/service"
+
+ cloudfoundry = local.cloudfoundry
+ env = local.env
+
+ secrets = local.secrets
+ skip_service_instances = true
+}
+```
\ No newline at end of file
diff --git a/terraform/modules/service/.terraform-docs/header.md b/terraform/modules/service/.terraform-docs/header.md
new file mode 100755
index 00000000..b8adab08
--- /dev/null
+++ b/terraform/modules/service/.terraform-docs/header.md
@@ -0,0 +1 @@
+# CloudFoundry Service Module
diff --git a/terraform/modules/service/README.md b/terraform/modules/service/README.md
new file mode 100644
index 00000000..a212a6c4
--- /dev/null
+++ b/terraform/modules/service/README.md
@@ -0,0 +1,87 @@
+
+# CloudFoundry Service Module
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | > 1.7 |
+| [cloudfoundry](#requirement\_cloudfoundry) | ~> 0.5 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [cloudfoundry](#provider\_cloudfoundry) | ~> 0.5 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [cloudfoundry_service_instance.this](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/resources/service_instance) | resource |
+| [cloudfoundry_service_key.this](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/resources/service_key) | resource |
+| [cloudfoundry_user_provided_service.this](https://registry.terraform.io/providers/cloudfoundry-community/cloudfoundry/latest/docs/resources/user_provided_service) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [cloudfoundry](#input\_cloudfoundry) | Cloudfoundry settings. | object(
{
domain_external = object(
{
domain = string
id = string
internal = bool
name = string
org = string
sub_domain = string
}
)
domain_internal = object(
{
domain = string
id = string
internal = bool
name = string
org = string
sub_domain = string
}
)
external_applications = map(string)
organization = object(
{
annotations = map(string)
id = string
labels = map(string)
name = string
}
)
services = map(
object(
{
id = string
name = string
service_broker_guid = string
service_broker_name = string
service_plans = map(string)
space = string
}
)
)
space = object(
{
annotations = map(string)
id = string
labels = map(string)
name = string
org = string
org_name = string
quota = string
}
)
}
)
| n/a | yes |
+| [env](#input\_env) | The settings object for this environment. | object({
api_url = optional(string, "https://api.fr.cloud.gov")
apps = optional(
map(
object({
allow_egress = optional(bool, true)
buildpacks = list(string)
command = optional(string, "entrypoint.sh")
disk_quota = optional(number, 1024)
enable_ssh = optional(bool, false)
environment = optional(map(string), {})
health_check_timeout = optional(number, 180)
health_check_type = optional(string, "port")
instances = optional(number, 1)
labels = optional(map(string), {})
memory = optional(number, 96)
network_policies = optional(map(number),{})
port = optional(number, 80)
public_route = optional(bool, false)
source = optional(string, null)
templates = list(map(string))
})
), {}
)
bootstrap_workspace = optional(string, "bootstrap")
defaults = object(
{
disk_quota = optional(number, 2048)
enable_ssh = optional(bool, true)
health_check_timeout = optional(number, 60)
health_check_type = optional(string, "port")
instances = optional(number, 1)
memory = optional(number, 64)
port = optional(number, 8080)
stack = optional(string, "cflinuxfs4")
stopped = optional(bool, false)
strategy = optional(string, "none")
timeout = optional(number, 300)
}
)
external_applications = optional(
map(
object(
{
environement = string
port = optional(number, 61443)
}
)
),{}
)
external_domain = optional(string, "app.cloud.gov")
internal_domain = optional(string, "apps.internal")
name_pattern = string
organization = optional(string, "gsa-tts-usagov")
passwords = optional(
list(
object(
{
length = optional(number, 32)
}
)
), []
)
project = string
secrets = optional(
map(
object(
{
encrypted = bool
key = string
}
)
), {}
)
services = optional(
map(
object(
{
applications = optional(list(string), [])
environement = optional(string, "dev")
service_key = optional(bool, true)
service_plan = optional(string, "basic")
service_type = optional(string, "s3")
tags = optional(list(string), [])
}
)
), {}
)
space = string
})
| n/a | yes |
+| [secrets](#input\_secrets) | Sensitive strings to be added to the apps environmental variables. | `map` | `{}` | no |
+| [skip\_service\_instances](#input\_skip\_service\_instances) | Allows the skipping of service instances. Useful to inject service secrets into a user provided secret. | `bool` | `false` | no |
+| [skip\_user\_provided\_services](#input\_skip\_user\_provided\_services) | Allows the skipping of user provided services. Useful to inject service secrets into a user provided secret. | `bool` | `false` | no |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [results](#output\_results) | n/a |
+
+## Examples
+
+### Basic
+```terraform
+module "services" {
+ source = "./modules/service"
+
+ cloudfoundry = local.cloudfoundry
+ env = local.env
+}
+```
+
+### Advanced
+
+This advanced example will first generate service instances, such as RDS, along with other defined services, except for the `user defined` services. `User defined` services are useful for providing variables at runtime to applications. The issue is that until a service, such as RDS is deployed, their isn't a username and password created for that instance.
+
+The first step is to initalize any services that are not `user defined`, but setting `skip_user_provided_services` to `true`.
+
+```terraform
+module "services" {
+ source = "./modules/service"
+
+ cloudfoundry = local.cloudfoundry
+ env = local.env
+
+ skip_user_provided_services = true
+}
+```
+
+After the services are generated, another module block can be defined, which will pass a merged `map(string)` called `secrets`, that have the various information that is to be added to the `user defined` service. Setting the `skip_service_instances` to `true` will prevent the module from trying to redploy any non `user defined` service.
+
+```terraform
+module "secrets" {
+ source = "./modules/service"
+
+ cloudfoundry = local.cloudfoundry
+ env = local.env
+
+ secrets = local.secrets
+ skip_service_instances = true
+}
+```
+
\ No newline at end of file
diff --git a/terraform/modules/service/main.tf b/terraform/modules/service/main.tf
new file mode 100755
index 00000000..ea18b7c8
--- /dev/null
+++ b/terraform/modules/service/main.tf
@@ -0,0 +1,61 @@
+locals {
+ credentials = merge(
+ flatten([
+ for key, value in try(var.env.services,{}) : {
+ "${key}" = {
+ applications = value.applications
+ service_type = value.service_type
+ tags = value.tags
+ credentials = merge(
+ [
+ for name in try(value.credentials, {}) : {
+ name = try(var.secrets[name], null)
+ }
+ ]
+ ...)
+ }
+ } if !var.skip_user_provided_services &&
+ value.service_type == "user-provided"
+ ])
+ ...)
+}
+
+
+resource "cloudfoundry_service_key" "this" {
+ for_each = {
+ for key, value in try(var.env.services, {}) : key => value
+ if !var.skip_service_instances &&
+ value.service_type != "user-provided" &&
+ try(value.service_key, false)
+ }
+
+ name = format("%s-%s-%s", format(var.env.name_pattern, each.key), each.key, "svckey")
+ service_instance = cloudfoundry_service_instance.this[each.key].id
+}
+
+resource "cloudfoundry_service_instance" "this" {
+ for_each = {
+ for key, value in try(var.env.services, {}) : key => value
+ if !var.skip_service_instances &&
+ value.service_type != "user-provided"
+ }
+
+ name = format(var.env.name_pattern, each.key)
+ json_params = try(each.value.json_params, null)
+ replace_on_params_change = try(each.value.replace_on_service_plan_change, false)
+ replace_on_service_plan_change = try(each.value.replace_on_service_plan_change, false)
+ space = var.cloudfoundry.space.id
+ service_plan = var.cloudfoundry.services[each.key].service_plans[each.value.service_plan]
+ tags = try(each.value.tags, [])
+}
+
+resource "cloudfoundry_user_provided_service" "this" {
+ for_each = {
+ for key, value in local.credentials : key => value
+ }
+
+ name = format(var.env.name_pattern, each.key)
+ space = var.cloudfoundry.space.id
+ credentials_json = jsonencode(try(each.value.credentials, {}))
+ tags = try(each.value.tags, [])
+}
diff --git a/terraform/modules/service/output.tf b/terraform/modules/service/output.tf
new file mode 100755
index 00000000..8bb00463
--- /dev/null
+++ b/terraform/modules/service/output.tf
@@ -0,0 +1,7 @@
+output "results" {
+ value = {
+ instance = try(cloudfoundry_service_instance.this, null)
+ user_provided = try(cloudfoundry_user_provided_service.this, null)
+ service_key = try(cloudfoundry_service_key.this, {})
+ }
+}
diff --git a/terraform/modules/service/providers.tf b/terraform/modules/service/providers.tf
new file mode 100644
index 00000000..d106004d
--- /dev/null
+++ b/terraform/modules/service/providers.tf
@@ -0,0 +1,9 @@
+terraform {
+ required_providers {
+ cloudfoundry = {
+ source = "cloudfoundry-community/cloudfoundry"
+ version = "~> 0.5"
+ }
+ }
+ required_version = "> 1.7"
+}
diff --git a/terraform/modules/service/variables.tf b/terraform/modules/service/variables.tf
new file mode 100755
index 00000000..e4e64d73
--- /dev/null
+++ b/terraform/modules/service/variables.tf
@@ -0,0 +1,183 @@
+variable "cloudfoundry" {
+ description = "Cloudfoundry settings."
+ type = object(
+ {
+ domain_external = object(
+ {
+ domain = string
+ id = string
+ internal = bool
+ name = string
+ org = string
+ sub_domain = string
+ }
+ )
+ domain_internal = object(
+ {
+ domain = string
+ id = string
+ internal = bool
+ name = string
+ org = string
+ sub_domain = string
+ }
+ )
+ external_applications = map(string)
+ organization = object(
+ {
+ annotations = map(string)
+ id = string
+ labels = map(string)
+ name = string
+ }
+ )
+ services = map(
+ object(
+ {
+ id = string
+ name = string
+ service_broker_guid = string
+ service_broker_name = string
+ service_plans = map(string)
+ space = string
+ }
+ )
+ )
+ space = object(
+ {
+ annotations = map(string)
+ id = string
+ labels = map(string)
+ name = string
+ org = string
+ org_name = string
+ quota = string
+ }
+ )
+ }
+ )
+}
+
+variable "env" {
+ description = "The settings object for this environment."
+ type = object({
+ api_url = optional(string, "https://api.fr.cloud.gov")
+ apps = optional(
+ map(
+ object({
+ allow_egress = optional(bool, true)
+ buildpacks = list(string)
+ command = optional(string, "entrypoint.sh")
+ disk_quota = optional(number, 1024)
+ enable_ssh = optional(bool, false)
+ environment = optional(map(string), {})
+ health_check_timeout = optional(number, 180)
+ health_check_type = optional(string, "port")
+ instances = optional(number, 1)
+ labels = optional(map(string), {})
+ memory = optional(number, 96)
+ network_policies = optional(map(number),{})
+ port = optional(number, 80)
+ public_route = optional(bool, false)
+ source = optional(string, null)
+ stopped = optional(bool, false)
+ templates = optional(list(map(string)), [])
+ })
+ ), {}
+ )
+ bootstrap_workspace = optional(string, "bootstrap")
+ defaults = object(
+ {
+ disk_quota = optional(number, 2048)
+ enable_ssh = optional(bool, true)
+ health_check_timeout = optional(number, 60)
+ health_check_type = optional(string, "port")
+ instances = optional(number, 1)
+ memory = optional(number, 64)
+ port = optional(number, 8080)
+ stack = optional(string, "cflinuxfs4")
+ stopped = optional(bool, false)
+ strategy = optional(string, "none")
+ timeout = optional(number, 300)
+ }
+ )
+ external_applications = optional(
+ map(
+ object(
+ {
+ environement = string
+ port = optional(number, 61443)
+ }
+ )
+ ),{}
+ )
+ external_domain = optional(string, "app.cloud.gov")
+ internal_domain = optional(string, "apps.internal")
+ name_pattern = string
+ organization = optional(string, "gsa-tts-usagov")
+ passwords = optional(
+ map(
+ object(
+ {
+ experation_days = optional(number, 0)
+ length = number
+ lower = optional(bool, false)
+ min_lower = optional(number, 0)
+ min_numeric = optional(number, 0)
+ min_special = optional(number, 0)
+ min_upper = optional(number, 0)
+ numeric = optional(bool, true)
+ override_special = optional(string, "!@#$%&*()-_=+[]{}<>:?")
+ special = optional(bool, true)
+ upper = optional(bool, true)
+ }
+ )
+ ), {}
+ )
+ project = string
+ secrets = optional(
+ map(
+ object(
+ {
+ encrypted = bool
+ key = string
+ }
+ )
+ ), {}
+ )
+ services = optional(
+ map(
+ object(
+ {
+ applications = optional(list(string), [])
+ environement = optional(string, "dev")
+ service_key = optional(bool, true)
+ service_plan = optional(string, "basic")
+ service_type = optional(string, "s3")
+ tags = optional(list(string), [])
+ }
+ )
+ ), {}
+ )
+ space = string
+ })
+}
+
+variable "skip_service_instances" {
+ description = "Allows the skipping of service instances. Useful to inject service secrets into a user provided secret."
+ type = bool
+ default = false
+}
+
+variable "skip_user_provided_services" {
+ description = "Allows the skipping of user provided services. Useful to inject service secrets into a user provided secret."
+ type = bool
+ default = false
+}
+
+variable "secrets" {
+ default = {}
+ description = "Sensitive strings to be added to the apps environmental variables."
+ type = map
+ sensitive = true
+}
\ No newline at end of file