diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..aa75c7fb0e3f78b26e846b911433cbda408aae20
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,34 @@
+### Terraform ###
+# Local .terraform directories
+**/.terraform/*
+
+# .tfstate files
+*.tfstate
+*.tfstate.*
+
+# Crash log files
+crash.log
+
+# Exclude all .tfvars files, which are likely to contain sentitive data, such as
+# password, private keys, and other secrets. These should not be part of version
+# control as they are data points which are potentially sensitive and subject
+# to change depending on the environment.
+#
+*.tfvars
+
+# Ignore override files as they are usually used to override resources locally and so
+# are not checked in
+override.tf
+override.tf.json
+*_override.tf
+*_override.tf.json
+
+# Include override files you do wish to add to version control using negated pattern
+# !example_override.tf
+
+# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
+# example: *tfplan*
+
+# Ignore CLI configuration files
+.terraformrc
+terraform.rc
diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl
new file mode 100644
index 0000000000000000000000000000000000000000..2962f391eddd7300bc3978690da294fa8d7b0d87
--- /dev/null
+++ b/.terraform.lock.hcl
@@ -0,0 +1,21 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/terraform-provider-openstack/openstack" {
+  version     = "1.35.0"
+  constraints = "~> 1.35.0"
+  hashes = [
+    "h1:k1SCosvSICWAgRkswl83KtCycN7iP9asejWDDEQEtuk=",
+    "zh:04cf8800c83289a28619ac9925bc03e0ccd624f0ed68284f8bd473cf48f05ef0",
+    "zh:15fc2e1ea6f87d11e15aad075f3bfb7013eb63f31637f1ee317c94686c9650ec",
+    "zh:1b6625ce80e6d8f192c984dcf6ba7ab303e4fdab6fa5a0a5651a8a01521aa879",
+    "zh:4eb60013433f45fa3ef3fca314f68202e40ea3a12b0a5ccf75d8708002bbd8cd",
+    "zh:505a7b22c813874090ceaee1a3e7f29961d0fe5a854d90f313aec5aa4900f44a",
+    "zh:7be239b7e5672bd8fc3f3744bfe58dff73f8317ede349228a92dbd92b291a832",
+    "zh:97b21c64da2700a69b5a3d30e514d76b52fc1089a45a0e02611aadb071b6fcc1",
+    "zh:b45be0621b08f16236893a5bcc0e7fa1552176b299f6dbafa806f8b0ba5e4096",
+    "zh:cd3b4387766ab33fbf504b22e73196d5984bc647f5c8ac9483ad604cf5cd2ceb",
+    "zh:e3b69afe0cab18521ee11e283f783e76c02d248de40f7a25d54576f2b1643003",
+    "zh:f43d52db66544065040b40db6334a1dfe6c9084d87104af1b1d133f90bbf3a66",
+  ]
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..50c300cf6866f5f9f262cd20502844965ee3a4de
--- /dev/null
+++ b/README.md
@@ -0,0 +1,11 @@
+# Prerequisites
+
+* Install [terraform](https://www.terraform.io/downloads.html) 1.0 or greater
+* Configure some application credentials in the [RC Openstack dashboard](https://dashboard.cloud.rc.uab.edu/identity/application_credentials/)
+* Add a key pair (in the [Key Pairs](https://dashboard.cloud.rc.uab.edu/project/key_pairs) section)
+
+# Quickstart
+
+* Copy `terraform.tfvars.example` to `terraform.tfvars` and edit in appropriate values
+* Run `terraform init`
+* Run `terraform apply`
diff --git a/compute.tf b/compute.tf
new file mode 100644
index 0000000000000000000000000000000000000000..d721ae690c3ae3e2a0f037e78be31842e1263135
--- /dev/null
+++ b/compute.tf
@@ -0,0 +1,162 @@
+# data for reference image
+data "openstack_images_image_v2" "base_image" {
+  name = var.base_image_name
+}
+
+# data for different instance sizes 
+
+data "openstack_compute_flavor_v2" "m1_small" {
+  name = "m1.small"
+}
+
+# bastion node
+
+resource "openstack_compute_instance_v2" "bastion" {
+  name      = "bastion"
+  image_id  = data.openstack_images_image_v2.base_image.id
+  flavor_id = data.openstack_compute_flavor_v2.m1_small.id
+  key_pair  = var.ssh_keypair
+  security_groups = [
+    "default",
+    openstack_compute_secgroup_v2.allow_ssh.name
+  ]
+
+  block_device {
+    # this is the image to clone from
+    uuid                  = data.openstack_images_image_v2.base_image.id
+    source_type           = "image"
+    destination_type      = "local"
+    boot_index            = 0
+    delete_on_termination = true
+  }
+
+  block_device {
+    # and the volume to copy to
+    source_type           = "blank"
+    destination_type      = "volume"
+    volume_size           = 10
+    boot_index            = 1
+    delete_on_termination = true
+  }
+
+  network {
+    uuid = openstack_networking_network_v2.public_network.id
+  }
+}
+
+resource "openstack_compute_floatingip_associate_v2" "bastion_association" {
+  floating_ip = openstack_compute_floatingip_v2.floating_ip.address
+  instance_id = openstack_compute_instance_v2.bastion.id
+}
+
+# OSD nodes
+
+resource "openstack_compute_instance_v2" "osd" {
+  count = var.osd_node_count
+
+  name      = format("osd%02d", count.index + 1)
+  image_id  = data.openstack_images_image_v2.base_image.id
+  flavor_id = data.openstack_compute_flavor_v2.m1_small.id
+  key_pair  = var.ssh_keypair
+  security_groups = [
+    "default"
+  ]
+
+  block_device {
+    # this is the image to clone from
+    uuid                  = data.openstack_images_image_v2.base_image.id
+    source_type           = "image"
+    destination_type      = "local"
+    boot_index            = 0
+    delete_on_termination = true
+  }
+
+  block_device {
+    # and the volume to copy to
+    source_type           = "blank"
+    destination_type      = "volume"
+    volume_size           = 10
+    boot_index            = 1
+    delete_on_termination = true
+  }
+
+  network {
+    uuid = openstack_networking_network_v2.public_network.id
+  }
+
+  network {
+    uuid = openstack_networking_network_v2.cluster_network.id
+  }
+}
+
+# MDS node
+
+resource "openstack_compute_instance_v2" "mds" {
+  count = 1 # TODO: add variables for this later
+
+  name      = format("mds%02d", count.index + 1)
+  image_id  = data.openstack_images_image_v2.base_image.id
+  flavor_id = data.openstack_compute_flavor_v2.m1_small.id
+  key_pair  = var.ssh_keypair
+  security_groups = [
+    "default"
+  ]
+
+  block_device {
+    # this is the image to clone from
+    uuid                  = data.openstack_images_image_v2.base_image.id
+    source_type           = "image"
+    destination_type      = "local"
+    boot_index            = 0
+    delete_on_termination = true
+  }
+
+  block_device {
+    # and the volume to copy to
+    source_type           = "blank"
+    destination_type      = "volume"
+    volume_size           = 10
+    boot_index            = 1
+    delete_on_termination = true
+  }
+
+  network {
+    uuid = openstack_networking_network_v2.public_network.id
+  }
+}
+
+# MON node
+
+resource "openstack_compute_instance_v2" "mon" {
+  count = 1 # TODO: add variables for this later
+
+  name      = format("mon%02d", count.index + 1)
+  image_id  = data.openstack_images_image_v2.base_image.id
+  flavor_id = data.openstack_compute_flavor_v2.m1_small.id
+  key_pair  = var.ssh_keypair
+  security_groups = [
+    "default"
+  ]
+
+  block_device {
+    # this is the image to clone from
+    uuid                  = data.openstack_images_image_v2.base_image.id
+    source_type           = "image"
+    destination_type      = "local"
+    boot_index            = 0
+    delete_on_termination = true
+  }
+
+  block_device {
+    # and the volume to copy to
+    source_type           = "blank"
+    destination_type      = "volume"
+    volume_size           = 10
+    boot_index            = 1
+    delete_on_termination = true
+  }
+
+  network {
+    uuid = openstack_networking_network_v2.public_network.id
+  }
+}
diff --git a/networks.tf b/networks.tf
new file mode 100644
index 0000000000000000000000000000000000000000..ba81251f76d2983ff2f8e8981f46bfc0e8d85706
--- /dev/null
+++ b/networks.tf
@@ -0,0 +1,48 @@
+# data source for external network
+
+data "openstack_networking_network_v2" "external" {
+  name = "uab-campus" # TODO: variable-itize this
+}
+
+# cluster network - intercommunication for OSDs
+
+resource "openstack_networking_network_v2" "cluster_network" {
+  name           = "ceph-cluster-network"
+  admin_state_up = "true"
+}
+
+resource "openstack_networking_subnet_v2" "cluster_subnet" {
+  network_id = openstack_networking_network_v2.cluster_network.id
+  cidr       = "10.0.100.0/24"
+}
+
+# public network - management/filesystem network
+
+resource "openstack_networking_network_v2" "public_network" {
+  name           = "ceph-public-network"
+  admin_state_up = "true"
+}
+
+resource "openstack_networking_subnet_v2" "public_subnet" {
+  network_id = openstack_networking_network_v2.public_network.id
+  cidr       = "10.0.0.0/24"
+}
+
+# router
+
+resource "openstack_networking_router_v2" "router" {
+  name                = "public-router"
+  admin_state_up      = true
+  external_network_id = data.openstack_networking_network_v2.external.id
+}
+
+resource "openstack_networking_router_interface_v2" "router_interface_public" {
+  router_id = openstack_networking_router_v2.router.id
+  subnet_id = openstack_networking_subnet_v2.public_subnet.id
+}
+
+# floating ip
+
+resource "openstack_compute_floatingip_v2" "floating_ip" {
+  pool = data.openstack_networking_network_v2.external.name
+}
diff --git a/openstack.tf b/openstack.tf
new file mode 100644
index 0000000000000000000000000000000000000000..63d5bdad57937689c664bff36fd745a5f1a730f7
--- /dev/null
+++ b/openstack.tf
@@ -0,0 +1,10 @@
+provider "openstack" {
+  auth_url                      = "https://keystone.cloud.rc.uab.edu:5000/v3"
+  user_name                     = var.user_name
+  user_domain_name              = var.user_domain_name
+  application_credential_name   = var.appcred_name
+  application_credential_id     = var.appcred_id
+  application_credential_secret = var.appcred_secret
+
+  insecure = true
+}
diff --git a/outputs.tf b/outputs.tf
new file mode 100644
index 0000000000000000000000000000000000000000..118a6e53bfd7fb229cc7831b865248defb6d68ac
--- /dev/null
+++ b/outputs.tf
@@ -0,0 +1,3 @@
+output "bastion_ip_address" {
+  value = openstack_compute_floatingip_v2.floating_ip.address
+}
diff --git a/securitygroups.tf b/securitygroups.tf
new file mode 100644
index 0000000000000000000000000000000000000000..737af1d00ebe0777493dcb014752d25e6ae9ec67
--- /dev/null
+++ b/securitygroups.tf
@@ -0,0 +1,11 @@
+resource "openstack_compute_secgroup_v2" "allow_ssh" {
+  name        = "allow ssh"
+  description = "allow ssh to hosts"
+
+  rule {
+    from_port   = 22
+    to_port     = 22
+    ip_protocol = "tcp"
+    cidr        = "0.0.0.0/0"
+  }
+}
diff --git a/terraform.tf b/terraform.tf
new file mode 100644
index 0000000000000000000000000000000000000000..5767904ac4d8c0454f7d829698f767744450f44a
--- /dev/null
+++ b/terraform.tf
@@ -0,0 +1,10 @@
+terraform {
+  required_version = ">= 1.0.1"
+
+  required_providers {
+    openstack = {
+      source  = "terraform-provider-openstack/openstack"
+      version = "~> 1.35.0"
+    }
+  }
+}
diff --git a/terraform.tfvars.example b/terraform.tfvars.example
new file mode 100644
index 0000000000000000000000000000000000000000..9ecb6c766a69ffdca063a2e92e81e751bbc24ba9
--- /dev/null
+++ b/terraform.tfvars.example
@@ -0,0 +1,6 @@
+user_name        = "openstack_user_name"
+user_domain_name = "openstack_domain_name"
+appcred_id       = "application_credential_id"
+appcred_name     = "application_credential_name"
+appcred_secret   = "application_credential_secret"
+ssh_keypair      = "ssh_keypair_name"
diff --git a/variables.tf b/variables.tf
new file mode 100644
index 0000000000000000000000000000000000000000..e0b694a6c469b00b997cef4b58606543f0234d0b
--- /dev/null
+++ b/variables.tf
@@ -0,0 +1,42 @@
+variable "user_name" {
+  type        = string
+  description = "user name for use with openstack"
+}
+
+variable "user_domain_name" {
+  type        = string
+  default     = "uab"
+  description = "the domain to use for the user"
+}
+
+variable "appcred_id" {
+  type        = string
+  description = "The app credential id. Get one from https://dashboard.cloud.rc.uab.edu/identity/application_credentials/"
+}
+
+variable "appcred_name" {
+  type        = string
+  description = "The name for your application credential"
+}
+
+variable "appcred_secret" {
+  type        = string
+  description = "The secret key for your application credential"
+}
+
+variable "ssh_keypair" {
+  type        = string
+  description = "ssh keypair name to use for authentication"
+}
+
+variable "base_image_name" {
+  type        = string
+  default     = "sles-15-sp3-x86_64"
+  description = "base image to use for the cluster"
+}
+
+variable "osd_node_count" {
+  type        = number
+  default     = 3
+  description = "amount of OSD nodes to create"
+}