diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index c55303f..f035b87 100644 --- a/.terraform.lock.hcl +++ b/.terraform.lock.hcl @@ -1,6 +1,25 @@ # This file is maintained automatically by "terraform init". # Manual edits may be lost in future updates. +provider "registry.terraform.io/hashicorp/archive" { + version = "2.5.0" + hashes = [ + "h1:OTk41JfiDc1TVFTcRZ//4+jwPBIcWHXOwN29mjdOyug=", + "zh:3b5774d20e87058d6d67d9ad4ce3fc4a5f7ea7748d345fa6721e24a0cbb0a3d4", + "zh:3b94e706ac0f5151880ccc9e63d33c4113361f27e64224a942caa04a5a19cd44", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7d7201858fa9376029818c9d017b4b53a933cea75480306b1122663d1e8eea2b", + "zh:8c8c7537978adf12271fe143f93b3587bb5dbabf8202ff49d0e3955b7bddc24b", + "zh:a5942584665a2689e73f3a3c43296adeaeb7e8698631d157419aa931ff856907", + "zh:a63673abdba624d60c84b819184fe86422bdbdf6bc73f68d903a7191aed32c00", + "zh:bcd1586cc32b263265e09e78f56dba3a6b6b19f5371c099a9d7a1bfe0b0667cc", + "zh:cc9e70e186e4dcef60208b4a64b42e6813b197e21ea106a96bb4eb23b54c3e44", + "zh:d4c8a0f69412892507a2c9ec0e334bcc2812a54b81212420d4f2c96ef58f713a", + "zh:e91e6d90bbc15252310eca6400d4188b29260aab0539480a3fc7b45e4d19c446", + "zh:fc468449c0dbda56aae6cb924e4a67578d18504b5b06e8989783182c6b4a5f73", + ] +} + provider "registry.terraform.io/hashicorp/google" { version = "4.51.0" constraints = "4.51.0" diff --git a/cloudfunctions/main.tf b/cloudfunctions/main.tf new file mode 100644 index 0000000..ef3e9e4 --- /dev/null +++ b/cloudfunctions/main.tf @@ -0,0 +1,43 @@ +resource "google_cloudfunctions_function" "verify_email_function" { + name = "verify_email_function" + description = "Sends verification emails and tracks them in CloudSQL" + runtime = "nodejs20" + available_memory_mb = 256 + source_archive_bucket = var.function_source_bucket + source_archive_object = var.function_source_object + entry_point = "verifyEmail" + environment_variables = { + CLOUD_SQL_USER = var.db_user + CLOUD_SQL_PASSWORD = var.db_password + CLOUD_SQL_DATABASE = var.db_name + CLOUD_SQL_INSTANCE = var.db_host + POSTMARK_FROM_EMAIL = var.postmark_from_email + GCP_PROJECT_ID = var.project_id + POSTMARK_API_KEY_SECRET = "projects/${var.project_id}/secrets/postmark-api-key/versions/latest" + DOMAIN= var.domain + } + service_account_email = var.service_account_email + event_trigger { + event_type = "providers/cloud.pubsub/eventTypes/topic.publish" + resource = var.pubsub_topic + } +} + +resource "google_project_iam_member" "function_pubsub_invoker" { + project = var.project_id + role = "roles/pubsub.subscriber" + member = "serviceAccount:${google_cloudfunctions_function.verify_email_function.service_account_email}" +} + +resource "google_project_iam_member" "secretmanager_access" { + project = var.project_id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_cloudfunctions_function.verify_email_function.service_account_email}" +} + +resource "google_project_iam_member" "cloudsql_client" { + project = var.project_id + role = "roles/cloudsql.client" + member = "serviceAccount:${google_cloudfunctions_function.verify_email_function.service_account_email}" +} + diff --git a/cloudfunctions/outputs.tf b/cloudfunctions/outputs.tf new file mode 100644 index 0000000..60c573a --- /dev/null +++ b/cloudfunctions/outputs.tf @@ -0,0 +1,4 @@ +output "cloud_function_url" { + description = "The URL of the deployed Cloud Function" + value = google_cloudfunctions_function.verify_email_function.https_trigger_url +} diff --git a/cloudfunctions/variables.tf b/cloudfunctions/variables.tf new file mode 100644 index 0000000..790ded1 --- /dev/null +++ b/cloudfunctions/variables.tf @@ -0,0 +1,72 @@ +variable "project_id" { + description = "The project ID to deploy resources into" + type = string +} + +variable "region" { + description = "The region to deploy resources into" + type = string + default = "us-central1" +} + +# variable "function_source" { +# description = "Path to the Cloud Function source code" +# type = string +# } + +variable "db_user" { + description = "Database user" + type = string +} + +variable "db_password" { + description = "Database password" + type = string +} + +variable "db_name" { + description = "Database name" + type = string +} + +variable "db_host" { + description = "Cloud SQL instance connection name" + type = string +} + +variable "postmark_from_email" { + description = "The Postmark from email" + type = string +} + +variable "service_account_email" { + description = "Service account email for the Cloud Function" + type = string +} + +variable "pubsub_topic" { + description = "Pub/Sub topic to trigger the Cloud Function" + type = string +} + +# variable "postmark_api_key_secret" { +# description = "The resource name of the Postmark API key secret in Secret Manager" +# type = string +# } + + +variable "function_source_bucket" { + description = "Pub/Sub topic to trigger the Cloud Function" + type = string +} + +variable "function_source_object" { + description = "Pub/Sub topic to trigger the Cloud Function" + type = string +} + +variable "domain" { + description = "domain to send emails from" + type = string + default = "http://kashyabcloudapp.me:8080" +} diff --git a/cloudsql/main.tf b/cloudsql/main.tf index b58bd18..890e363 100644 --- a/cloudsql/main.tf +++ b/cloudsql/main.tf @@ -29,7 +29,7 @@ resource "google_sql_database" "default" { instance = google_sql_database_instance.default.name } -resource "random_password" "default" { +resource "random_password" "generated_password" { length = 16 special = true } @@ -37,5 +37,5 @@ resource "random_password" "default" { resource "google_sql_user" "default" { name = var.db_user instance = google_sql_database_instance.default.name - password = random_password.default.result + password = random_password.generated_password.result } \ No newline at end of file diff --git a/cloudsql/outputs.tf b/cloudsql/outputs.tf index 0a21353..c9e8fd1 100644 --- a/cloudsql/outputs.tf +++ b/cloudsql/outputs.tf @@ -11,6 +11,14 @@ output "sql_user_name" { } output "sql_user_password" { - value = random_password.default.result + value = random_password.generated_password.result sensitive = true } + +output "sql_instance_connection_name" { + value = google_sql_database_instance.default.connection_name +} + +output "sql_instance_ip" { + value = google_sql_database_instance.default.ip_address.0.ip_address +} \ No newline at end of file diff --git a/cloudsql/variables.tf b/cloudsql/variables.tf index ce454aa..d4edba1 100644 --- a/cloudsql/variables.tf +++ b/cloudsql/variables.tf @@ -61,4 +61,4 @@ variable "private_vpc_connection" { variable "project_id" { description = "Project ID" type = string -} +} \ No newline at end of file diff --git a/firewall/main.tf b/firewall/main.tf index 2fc84b6..e53e4a0 100644 --- a/firewall/main.tf +++ b/firewall/main.tf @@ -4,19 +4,33 @@ resource "google_compute_firewall" "allow_http" { allow { protocol = "tcp" - ports = ["80"] # Replace 80 with your application port if different + ports = ["8080", "22"] # Replace 80 with your application port if different } source_ranges = ["0.0.0.0/0"] } +resource "google_compute_firewall" "allow-webapp-firewall" { + name = "allow-firewall-db-${0}" + network = var.network + + allow { + protocol = "tcp" + ports = ["5432"] + + } + source_ranges = ["10.62.0.0/16"] + direction = "EGRESS" + priority = 500 +} + resource "google_compute_firewall" "deny_ssh" { name = "deny-ssh" network = var.network deny { protocol = "tcp" - ports = ["22"] + ports = [""] } source_ranges = ["0.0.0.0/0"] diff --git a/iam/main.tf b/iam/main.tf index 059baf1..c17ca34 100644 --- a/iam/main.tf +++ b/iam/main.tf @@ -15,3 +15,41 @@ resource "google_project_iam_binding" "monitoring_metric_writer" { "serviceAccount:${var.service_account_email}" ] } + +resource "google_project_iam_binding" "webapp_secret_accessor" { + project = var.project_id + role = "roles/secretmanager.secretAccessor" + members = [ + "serviceAccount:${var.service_account_email}" + ] +} + +resource "google_project_iam_binding" "cloudfunctions_developer" { + project = var.project_id + role = "roles/cloudfunctions.developer" + + members = [ + "serviceAccount:${var.service_account_email}" + ] +} + +resource "google_project_iam_binding" "storage_admin" { + project = var.project_id + role = "roles/storage.admin" + + members = [ + "serviceAccount:${var.service_account_email}" + ] +} + +resource "google_project_iam_member" "function_pubsub_invoker" { + project = var.project_id + role = "roles/pubsub.subscriber" + member = "serviceAccount:${var.service_account_email}" +} + +resource "google_project_iam_member" "secretmanager_access" { + project = var.project_id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${var.service_account_email}" +} diff --git a/main.tf b/main.tf index b2b72d5..d394482 100644 --- a/main.tf +++ b/main.tf @@ -3,6 +3,7 @@ provider "google" { region = var.region } + module "vpc" { source = "./vpc" vpc_name = var.vpc_name @@ -52,10 +53,18 @@ module "compute_instance" { zone = var.zone service_account_email = module.service_account.email startup_script = templatefile("${path.module}/startup_script.tpl", { + APPLICATION_NAME = "webapp" + SERVICE_NAME = "webapp.service" + GCP_PROJECT_ID = var.project_id DB_USER = module.cloudsql.sql_user_name - DB_PASS = module.cloudsql.sql_user_password + DB_PASS = "tZ-.5aP}j@+fY:z(" DB_NAME = module.cloudsql.sql_database_name - DB_HOST = module.cloudsql.sql_instance_name + DB_HOST = module.cloudsql.sql_instance_ip + JWT_SECRET = "" + DB_PORT=var.port + PUBSUB_TOPIC="verify_email" + # Construct the DATABASE_URL + #DATABASE_URL = "postgres://${module.cloudsql.sql_user_name}:${module.cloudsql.sql_user_password}@/${module.cloudsql.sql_database_name}?host=/cloudsql/${module.cloudsql.sql_instance_connection_name}" }) } @@ -65,7 +74,7 @@ module "dns" { webapp_dnsrecord_type = "A" webapp_dns_ttl = 300 managed_zone_webapp = "my-new-zone" # The name of your existing managed zone - global_ip = module.vpc.private_service_connect_ip + global_ip = module.compute_instance.instance_ip } module "service_account" { @@ -79,4 +88,48 @@ module "iam" { source = "./iam" project_id = var.project_id service_account_email = module.service_account.email +} + +module "secret_manager" { + source = "./secret_manager" + project_id = var.project_id +} + +module "pubsub" { + source = "./pubsub" + # project_id = var.project_id + # region = var.region +} + +resource "google_storage_bucket" "function_bucket" { + name = "numeric-gcf-source" + location = var.region + uniform_bucket_level_access = true +} + +data "archive_file" "function_zip" { + type = "zip" + output_path = "/tmp/function-source.zip" + source_dir = "../serverless-fork/" +} + +resource "google_storage_bucket_object" "function_zip" { + name = "function-source.zip" + bucket = google_storage_bucket.function_bucket.name + source = data.archive_file.function_zip.output_path +} + +module "cloudfunctions" { + source = "./cloudfunctions" + project_id = var.project_id + region = var.region + function_source_bucket = google_storage_bucket.function_bucket.name + function_source_object = google_storage_bucket_object.function_zip.name + db_user = module.cloudsql.sql_user_name + db_password = "tZ-.5aP}j@+fY:z(" + db_name = module.cloudsql.sql_database_name + db_host = module.cloudsql.sql_instance_ip + postmark_from_email = "murali.k@northeastern.edu" + service_account_email = module.service_account.email + pubsub_topic = module.pubsub.pubsub_topic_name } \ No newline at end of file diff --git a/outputs.tf b/outputs.tf index 7944967..3b67d67 100644 --- a/outputs.tf +++ b/outputs.tf @@ -45,3 +45,27 @@ output "instance_name" { output "instance_self_link" { value = module.compute_instance.instance_self_link } + +output "sql_instance_connection_name" { + value = module.cloudsql.sql_instance_connection_name +} + +# output "database_url_output" { +# value = module.compute_instance.database_url +# } + + +output "cloud_function_url" { + description = "The URL of the deployed Cloud Function" + value = module.cloudfunctions.cloud_function_url +} + +output "pubsub_topic_name" { + description = "The name of the Pub/Sub topic" + value = module.pubsub.pubsub_topic_name +} + +output "service_account_email" { + description = "The email of the service account" + value = module.service_account.email +} \ No newline at end of file diff --git a/pubsub/main.tf b/pubsub/main.tf new file mode 100644 index 0000000..01d9f29 --- /dev/null +++ b/pubsub/main.tf @@ -0,0 +1,8 @@ +resource "google_pubsub_topic" "verify_email" { + name = "verify_email" +} + +resource "google_pubsub_subscription" "verify_email_subscription" { + name = "verify_email_subscription" + topic = google_pubsub_topic.verify_email.name +} diff --git a/pubsub/outputs.tf b/pubsub/outputs.tf new file mode 100644 index 0000000..1ca4061 --- /dev/null +++ b/pubsub/outputs.tf @@ -0,0 +1,3 @@ +output "pubsub_topic_name" { + value = google_pubsub_topic.verify_email.name +} \ No newline at end of file diff --git a/pubsub/variables.tf b/pubsub/variables.tf new file mode 100644 index 0000000..7a2584a --- /dev/null +++ b/pubsub/variables.tf @@ -0,0 +1,9 @@ +variable "pubsub_topic" { + type = string + default = "verify_email" +} + +variable "pubsub_subscription" { + type = string + default = "verify_email_subscription" +} diff --git a/secret_manager/main.tf b/secret_manager/main.tf new file mode 100644 index 0000000..72addf5 --- /dev/null +++ b/secret_manager/main.tf @@ -0,0 +1,20 @@ +resource "random_password" "jwt_secret" { + length = 32 + special = true +} + +resource "google_secret_manager_secret" "jwt_secret" { + secret_id = "jwt-secret" + replication { + user_managed { + replicas { + location = "us-central1" + } + } + } +} + +resource "google_secret_manager_secret_version" "jwt_secret_version" { + secret = google_secret_manager_secret.jwt_secret.id + secret_data = random_password.jwt_secret.result +} diff --git a/secret_manager/outputs.tf b/secret_manager/outputs.tf new file mode 100644 index 0000000..e363a36 --- /dev/null +++ b/secret_manager/outputs.tf @@ -0,0 +1,4 @@ + +output "jwt_secret_version_name" { + value = google_secret_manager_secret_version.jwt_secret_version.name +} diff --git a/secret_manager/variables.tf b/secret_manager/variables.tf new file mode 100644 index 0000000..6d2c53d --- /dev/null +++ b/secret_manager/variables.tf @@ -0,0 +1,4 @@ +variable "project_id" { + description = "The project ID where the resources will be created" + type = string +} diff --git a/startup_script.tpl b/startup_script.tpl index b0aabe0..7c4e19a 100644 --- a/startup_script.tpl +++ b/startup_script.tpl @@ -1,14 +1,77 @@ #!/bin/bash -DB_USER='${DB_USER}' -DB_PASS='${DB_PASS}' -DB_NAME='${DB_NAME}' -DB_HOST='${DB_HOST}' + +# Install Google Cloud SDK if not already installed +if ! command -v gcloud &> /dev/null; then + echo "Google Cloud SDK not found, installing..." + export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)" + echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list + curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add - + sudo apt-get update && sudo apt-get install -y google-cloud-sdk +fi + +# Retrieve secrets from Google Secret Manager +JWT_SECRET=$(gcloud secrets versions access latest --secret=jwt-secret) + +# Set environment variables +#To-do: Replace the password with rand +DB_USER="${DB_USER}" +DB_PASS="tZ-.5aP}j@+fY:z(" +DB_NAME="${DB_NAME}" +DB_HOST="${DB_HOST}" +APPLICATION_NAME="${APPLICATION_NAME}" +SERVICE_NAME="${SERVICE_NAME}" +GCP_PROJECT_ID="${GCP_PROJECT_ID}" +PUBSUB_TOPIC="verify_email" # Update application configuration -echo "DB_USER=${DB_USER}" >> /etc/environment -echo "DB_PASS=${DB_PASS}" >> /etc/environment -echo "DB_NAME=${DB_NAME}" >> /etc/environment -echo "DB_HOST=${DB_HOST}" >> /etc/environment +echo "DB_USER=${DB_USER}" | sudo tee -a /etc/environment +echo "DB_PASS=tZ-.5aP}j@+fY:z(" | sudo tee -a /etc/environment +echo "DB_NAME=${DB_NAME}" | sudo tee -a /etc/environment +echo "DB_HOST=${DB_HOST}" | sudo tee -a /etc/environment +echo "DB_PORT=${DB_PORT}" | sudo tee -a /etc/environment +echo "JWT_SECRET_KEY=${JWT_SECRET}" | sudo tee -a /etc/environment +echo "APPLICATION_NAME=${APPLICATION_NAME}" | sudo tee -a /etc/environment +echo "SERVICE_NAME=${SERVICE_NAME}" | sudo tee -a /etc/environment +echo "GCP_PROJECT_ID=${GCP_PROJECT_ID}" | sudo tee -a /etc/environment +echo "PUBSUB_TOPIC=verify_email" | sudo tee -a /etc/environment + + +# Create the systemd service file with environment variables +sudo bash -c 'cat < /etc/systemd/system/${SERVICE_NAME} +[Unit] +Description=Go Web App Service by Kashyab - Systemd from TF +After=network.target + +[Service] +Environment=DB_USER=${DB_USER} +Environment=DB_PASS=tZ-.5aP}j@+fY:z( +Environment=DB_NAME=${DB_NAME} +Environment=DB_HOST=${DB_HOST} +Environment=DB_PORT=${DB_PORT} +Environment=JWT_SECRET_KEY=${JWT_SECRET} +Environment=APPLICATION_NAME=${APPLICATION_NAME} +Environment=SERVICE_NAME=${SERVICE_NAME} +Environment=GCP_PROJECT_ID=${GCP_PROJECT_ID} +Environment=PUBSUB_TOPIC=verify_email +ExecStart=/usr/local/bin/${APPLICATION_NAME} +Restart=on-failure +User=csye6225 +Group=csye6225 +WorkingDirectory=/usr/local/bin + +[Install] +WantedBy=multi-user.target +EOF' + + +sudo chmod 644 /etc/systemd/system/webapp.service +sudo sed -i 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/config +sudo restorecon -rv /etc/systemd/system/webapp.service + +# Reload systemd manager configuration +sudo systemctl daemon-reload + -# Start your web application -sudo systemctl start webapp.service \ No newline at end of file +# Enable and start the service +sudo systemctl enable ${SERVICE_NAME} +sudo systemctl restart ${SERVICE_NAME} diff --git a/variables.tf b/variables.tf index 26c0cb3..6b489af 100644 --- a/variables.tf +++ b/variables.tf @@ -45,4 +45,70 @@ variable "zone" { default = "us-central1-a" } +variable "application_name" { + description = "The name of the web application" + type = string + default = "webapp" +} + +variable "database_url" { + description = "Database URL that is constructed in the startup script" + type = string + default = "localhost" +} + +variable "jwt_secret"{ + description = "JWT Secret is also built in the startup script" + type = string + default = "value" +} + +variable "port" { + description = "Port Number" + type = string + default = "5432" +} + + +variable "service_name" { + description = "The name of the service" + type = string + default = "webapp.service" +} + + +variable "postmark_from_email" { + description = "The Postmark from email" + type = string + default = "murali.k@northeastern.edu" +} + +variable "service_account_email" { + description = "Service account email for the Cloud Function" + type = string + default = "" +} + +variable "pubsub_topic" { + description = "Pub/Sub topic to trigger the Cloud Function" + type = string + default = "" +} + +# variable "function_source" { +# description = "Path to the Cloud Function source code from main" +# type = string +# default = "../function.zip" +# } +variable "function_source_bucket" { + description = "Pub/Sub topic to trigger the Cloud Function main" + type = string + default = "" +} + +variable "function_source_object" { + description = "Pub/Sub topic to trigger the Cloud Function main" + type = string + default = "" +} diff --git a/vpc/outputs.tf b/vpc/outputs.tf index bbaca6f..0f6aaeb 100644 --- a/vpc/outputs.tf +++ b/vpc/outputs.tf @@ -33,3 +33,7 @@ output "private_vpc_connection_name" { output "private_service_connect_ip" { value = google_compute_global_address.private_service_connect_ip.address } + +output "vpc_network" { + value = google_compute_network.vpc +} \ No newline at end of file