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 = []
+}