diff --git a/.idea/scopes/infra.xml b/.idea/scopes/infra.xml new file mode 100644 index 000000000..75368dd13 --- /dev/null +++ b/.idea/scopes/infra.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/infra/.gitignore b/infra/.gitignore new file mode 100644 index 000000000..dd436f562 --- /dev/null +++ b/infra/.gitignore @@ -0,0 +1,13 @@ +**/*-key.json + +**/.terraform/* +*.tfstate +*.tfstate.* +crash.log +crash.*.log +*.tfvars +*.tfvars.json +override.tf +override.tf.json +*_override.tf +*_override.tf.json diff --git a/infra/gcp/dev/.terraform.lock.hcl b/infra/gcp/dev/.terraform.lock.hcl new file mode 100644 index 000000000..506719ede --- /dev/null +++ b/infra/gcp/dev/.terraform.lock.hcl @@ -0,0 +1,42 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/google" { + version = "4.78.0" + constraints = "4.78.0" + hashes = [ + "h1:wxYr3Z7xTg+rugpIu/DKOW88nJ7V76lYvq50+auW5cc=", + "zh:09a09e79ac404ea9ce2030185973130ed5f25e7f2c1d46093ee67fcc8f94e220", + "zh:1dd579d1200fd9cb57b84b326f401674cc5c62670c85fff7bb90642fd2379d10", + "zh:2d03592a8b370c8409fdffb8b946cc1ce9bca7407b48208743adf0de4a65c20c", + "zh:5f29e3155b6d0378d30574ac0f1c21aeedbca851e76c26de69ada81987514345", + "zh:75da3bb0ee4d4cf45c578462442e757596c2633e1b12afc0ecaaaed30aed25f5", + "zh:78b81c837322d66730c9ca04a06580477bfefb8492277cd653874adf5a8a63dc", + "zh:895433b46bd1cb1a599641fc0a619a2b6f59166701e923bf56fa6f46dc6d82ad", + "zh:9a734177d6c4223d9d65ca80c12bbb1f594b8e97217df6c4f05aa5250e968654", + "zh:cd94c7dd81fa777e0552adbcf043951b44e62bcde6eee391aa4d92c67c371c4a", + "zh:ceb97e8d2f7c5687c8591f5f8226b3233cf7b728eebb612b5d916d2af6fbf988", + "zh:d8ff449fbf8bf317c57b1c9de4c7d15429d52d9aabc048ea7570c9b3ae9810a2", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/google-beta" { + version = "4.84.0" + constraints = ">= 4.64.0, < 5.0.0" + hashes = [ + "h1:BkdCMbyvAkOMslh6tGz+5K/bsziWUmZvt+4pfR3xtA4=", + "zh:0c17bd21a0d98a5063b5bbdad0feac559913061264953d96b3b82289b9938d83", + "zh:138dd45494953f6ce0f837ab29ca61ff91e2001e7cf49356021a962030ccf217", + "zh:1846d617cd39cc7da60280686e1ba63239a4a200f30386dd66a633ea5789e307", + "zh:38d715a828573923d0129fa258b64360f77fbb437c605e26dba95e6b8cf79b53", + "zh:4a041086cabbcaaf9982051297ab864003c7e042b4a8d47c2bfaa47fc83886cb", + "zh:78bfc252ad0e56f2fd10abc25d1e79acb7bd95383017ea4ee309e8c5b15a338b", + "zh:7f193c7b32851e3c704ecf713f93d3ab78031e82d47ac0b4ccf3ecd6be3dda2d", + "zh:8c0f381aee7d3029ec7f0bc1e80ae545a9a522ec764648a9a4e024cfaac3d6f5", + "zh:cae23495634d780f92f241fde2718ef627ed6485225241dd90ef375eb710c0ea", + "zh:d7ccfb67d072870a6c54e76f5ac5fc9a817bd1392dfac81a964bae4cb36ca096", + "zh:e0adbd1e0bf48224c3d352423df5f1bcd62f4e95fe26c981720fbf81f863f57e", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/infra/gcp/dev/README.md b/infra/gcp/dev/README.md new file mode 100644 index 000000000..5744c6102 --- /dev/null +++ b/infra/gcp/dev/README.md @@ -0,0 +1,42 @@ +# Terraform configs for an example GCP Development Environment + +This directory is an example set of terraform configs that allow us to provision the necessary resources for an example GCP deployment. It is not intended to be used as-is, but rather as an example. Setting up terraform configs for an entire GCP organization is outside the scope of this project. + +## Prerequisites + +1. [Terraform](https://www.terraform.io/downloads.html) installed + - See [GitHub Actions](/.github/workflows) workflow file for list of supported terraform versions +2. An account on Google Cloud (GCP) with the necessary IAM permissions for your project. + - ToDo: list necessary Permissions + +## Setup + +### Manual or pre-configured resources + +While haztrak encourages users store all infrastructure as code that can be checked into VCS, +for purposes of this demonstration, a pre-configured project and access is necessary. + +1. Create the project + - This could be bootstrapped from a separate terraform module/directory or manually created in the GCP console. +2. Ensure your account has sufficient permissions + - `roles/iam.serviceAccountCreater` +3. Enable the Google APIs that will allow us to manage our infrastructure with terraform. + - `gcloud config set project ` + - `gcloud services enable iamcredentials.googleapis.com cloudresourcemanager.googleapis.com storage.googleapis.com` +4. Create a [service account for terraform](https://cloud.google.com/iam/docs/service-accounts-create) + - `gcloud iam service-accounts create --display-name "Terraform Service Account"` + - The account will need the following permissions + - `roles/storage.objectAdmin` + - `gcloud projects add-iam-policy-binding -dev-test-123 --member="serviceAccount:@haztrak-epa-dev-test-123.iam.gserviceaccount.com" --role=roles/storage.objectAdmin --role=roles/serviceusage.serviceUsageAdmin` +5. [Create a GCP bucket](https://cloud.google.com/storage/docs/creating-buckets#storage-create-bucket-cli) to hold Terraform remote state + - `gcloud storage buckets create gc:// --project ` + +### Terraform Initialization and Apply + +1. Modify the location of the remote state bucket in `backend.tf` to match the id of the bucket you created in the previous step. +2. Initialize terraform + - `terraform init` +3. Create a `terraform.tfvars` file with the following contents: + - `project_id = ""` +4. Apply terraform + - `terraform apply` diff --git a/infra/gcp/dev/backend.tf b/infra/gcp/dev/backend.tf new file mode 100644 index 000000000..63db282d4 --- /dev/null +++ b/infra/gcp/dev/backend.tf @@ -0,0 +1,18 @@ +terraform { + backend "gcs" { + bucket = "haztrak-terraform-remote-state-epa-test-123" + prefix = "terraform/dev" + } + required_providers { + google = { + source = "hashicorp/google" + version = "4.78.0" + } + } +} + +provider "google" { + project = var.project + region = var.region + zone = var.zone +} diff --git a/infra/gcp/dev/main.tf b/infra/gcp/dev/main.tf new file mode 100644 index 000000000..3995fc8bf --- /dev/null +++ b/infra/gcp/dev/main.tf @@ -0,0 +1,15 @@ +module "gcp_apis" { + source = "../modules/gcp-apis" + + project = var.project + services = [ + "compute.googleapis.com", + ] +} + +module "vpc" { + source = "../modules/network" + project = var.project + region = var.region + depends_on = [module.gcp_apis] +} diff --git a/infra/gcp/dev/outputs.tf b/infra/gcp/dev/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/infra/gcp/dev/variables.tf b/infra/gcp/dev/variables.tf new file mode 100644 index 000000000..4358c73c4 --- /dev/null +++ b/infra/gcp/dev/variables.tf @@ -0,0 +1,35 @@ +variable "project" { + description = "The project ID to deploy to" + type = string + + validation { + condition = length(var.project) > 0 + error_message = "You must provide a project ID" + } + +} + +variable "region" { + description = "The default region to deploy to" + default = "us-east1" + type = string + validation { + condition = can(regex("^[a-z]{2,}-[a-z]*[1-9]{1}$", var.region)) + error_message = "Invalid GCP region format. See 'gcloud compute regions list' for available options" + } +} + +variable "environment" { + type = string + description = "The environment to deploy to" + default = "dev" + validation { + condition = contains(["dev", "prod", "test"], var.environment) + error_message = "Environment must be one of [dev, prod, test]" + } +} + +variable "zone" { + type = string + description = "the default zone to use for the terraform GCP provider" +} diff --git a/infra/gcp/modules/gcp-apis/main.tf b/infra/gcp/modules/gcp-apis/main.tf new file mode 100644 index 000000000..d93f169e0 --- /dev/null +++ b/infra/gcp/modules/gcp-apis/main.tf @@ -0,0 +1,7 @@ +# Loop through each GCP API, passed as a string, and enable it +resource "google_project_service" "enabled_services" { + for_each = toset(var.services) + + project = var.project + service = each.key +} diff --git a/infra/gcp/modules/gcp-apis/variables.tf b/infra/gcp/modules/gcp-apis/variables.tf new file mode 100644 index 000000000..0d9bdff9a --- /dev/null +++ b/infra/gcp/modules/gcp-apis/variables.tf @@ -0,0 +1,21 @@ +variable "services" { + description = "List of Google Cloud API names to enable." + type = list(string) + validation { + # condition = length([ + # for service in var.services : service if can(regex("[a-z.]*", service)) == true + # ]) + condition = can([for s in var.services : regex("^[a-zA-Z.]*googleapis.com$", s)]) + error_message = "Service names must follow the format: 'serviceusage.googleapis.com'" + } +} + +variable "project" { + description = "The Google Cloud project ID to enable services in." + type = string + validation { + error_message = "GCP project length must be greater than zero" + condition = length(var.project) > 0 + } + +} diff --git a/infra/gcp/modules/k8/main.tf b/infra/gcp/modules/k8/main.tf new file mode 100644 index 000000000..e69de29bb diff --git a/infra/gcp/modules/k8/outputs.tf b/infra/gcp/modules/k8/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/infra/gcp/modules/k8/variables.tf b/infra/gcp/modules/k8/variables.tf new file mode 100644 index 000000000..e69de29bb diff --git a/infra/gcp/modules/network/main.tf b/infra/gcp/modules/network/main.tf new file mode 100644 index 000000000..4d11b0509 --- /dev/null +++ b/infra/gcp/modules/network/main.tf @@ -0,0 +1,14 @@ +locals { + vpc_name = var.environment == "prod" ? var.vpc_name : "${var.vpc_name}-dev" + database_subnet_name = var.environment == "prod" ? "${var.project}-database-subnet-prod" : "${var.project}-database-subnet-dev" +} + +module "vpc" { + source = "terraform-google-modules/network/google" + version = "~> 7.1" + project_id = var.project + network_name = local.vpc_name + routing_mode = "GLOBAL" + auto_create_subnetworks = false + subnets = var.subnets +} diff --git a/infra/gcp/modules/network/outputs.tf b/infra/gcp/modules/network/outputs.tf new file mode 100644 index 000000000..cbd951b13 --- /dev/null +++ b/infra/gcp/modules/network/outputs.tf @@ -0,0 +1,7 @@ +output "network" { + value = module.vpc.network +} + +output "vpc" { + value = module.vpc.network_id +} diff --git a/infra/gcp/modules/network/variables.tf b/infra/gcp/modules/network/variables.tf new file mode 100644 index 000000000..879a8cc60 --- /dev/null +++ b/infra/gcp/modules/network/variables.tf @@ -0,0 +1,41 @@ +# network inputs + +variable "vpc_name" { + type = string + description = "The name of the VPC" + default = "haztrak" +} + +variable "project" { + description = "The GCP project to deploy to" + type = string +} + +variable "region" { + description = "The region to deploy to" + type = string +} + +variable "environment" { + description = "The environment to deploy to" + type = string + default = "dev" + validation { + condition = contains(["dev", "prod"], var.environment) + error_message = "Environment must be one of [dev, prod]" + } +} + +# Note: A VPC is a global resource, subnets are regional. +variable "subnets" { + description = "Any subnets of the VPC." + type = list(object({ + subnet_name = string + subnet_ip = string + subnet_region = string + subnet_private_access = optional(string) + subnet_private_ipv6_access = optional(string) + description = optional(string) + })) + default = [] +}