diff --git a/infra-examples/digitalocean/README.md b/infra-examples/digitalocean/README.md new file mode 100644 index 0000000..ac901d8 --- /dev/null +++ b/infra-examples/digitalocean/README.md @@ -0,0 +1,50 @@ +# DigitalOcean Infrastructure Example + +This is an example implementation to create a production grade infrastructure for hosting Open edX instances on DigitalOcean, using the Terraform modules provided by Harmony. + +## Requirements + +| Name | Version | +|------|---------| +| [digitalocean](#requirement\_digitalocean) | >=2.45 | +| [helm](#requirement\_helm) | >=2.16 | +| [kubectl](#requirement\_kubectl) | >=1.17 | +| [kubernetes](#requirement\_kubernetes) | >=2.34 | + +## Providers + +| Name | Version | +|------|---------| +| [digitalocean](#provider\_digitalocean) | 2.45.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [kubernetes\_cluster](#module\_kubernetes\_cluster) | ../../terraform/modules/digitalocean/doks | n/a | +| [main\_vpc](#module\_main\_vpc) | ../../terraform/modules/digitalocean/vpc | n/a | +| [mongodb\_database](#module\_mongodb\_database) | ../../terraform/modules/digitalocean/database | n/a | +| [mysql\_database](#module\_mysql\_database) | ../../terraform/modules/digitalocean/database | n/a | +| [spaces](#module\_spaces) | ../../terraform/modules/digitalocean/spaces | n/a | + +## Resources + +| Name | Type | +|------|------| +| [digitalocean_database_db.forum_database](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/database_db) | resource | +| [digitalocean_project.project](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/project) | resource | +| [digitalocean_kubernetes_cluster.cluster](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/data-sources/kubernetes_cluster) | data source | +| [digitalocean_kubernetes_versions.available_versions](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/data-sources/kubernetes_versions) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [do\_access\_token](#input\_do\_access\_token) | DitialOcean access token. | `string` | n/a | yes | +| [environment](#input\_environment) | The DigitalOcean project environment. (for example: production, staging, development, etc.) | `string` | n/a | yes | +| [kubernetes\_cluster\_name](#input\_kubernetes\_cluster\_name) | Name of the DigitalOcean Kubernetes cluster to create. | `string` | n/a | yes | +| [region](#input\_region) | DigitalOcean region to create the resources in. | `string` | n/a | yes | + +## Outputs + +No outputs. diff --git a/infra-examples/digitalocean/k8s-cluster/main.tf b/infra-examples/digitalocean/k8s-cluster/main.tf deleted file mode 100644 index 5bd2c5f..0000000 --- a/infra-examples/digitalocean/k8s-cluster/main.tf +++ /dev/null @@ -1,55 +0,0 @@ -terraform { - required_providers { - digitalocean = { - source = "digitalocean/digitalocean" - version = ">=2.23" - } - } -} - -variable "cluster_name" { type = string } -variable "do_region" { - type = string - default = "tor1" -} - -data "digitalocean_kubernetes_versions" "available_versions" {} - -resource "digitalocean_kubernetes_cluster" "cluster" { - name = var.cluster_name - region = var.do_region - version = data.digitalocean_kubernetes_versions.available_versions.latest_version - #vpc_uuid = var.vpc_uuid - auto_upgrade = true - # "Surge upgrade makes cluster upgrades fast and reliable by bringing up new nodes before destroying the outdated nodes." - surge_upgrade = true - - node_pool { - name = "${var.cluster_name}-nodes" - size = "s-4vcpu-8gb" - # At this size, at least 3 nodes are recommended to run 2 Open edX instances using the default Tutor images, because - # resources like MySQL/MongoDB are not shared. - min_nodes = 3 - max_nodes = 4 - auto_scale = true - } -} - -resource "local_sensitive_file" "kubeconfig" { - filename = "${path.root}/kubeconfig" - content = digitalocean_kubernetes_cluster.cluster.kube_config[0].raw_config - file_permission = "0400" -} - -output "kubeconfig" { - value = digitalocean_kubernetes_cluster.cluster.kube_config[0] - sensitive = true -} - -output "cluster_urn" { - value = digitalocean_kubernetes_cluster.cluster.urn -} - -output "cluster_id" { - value = digitalocean_kubernetes_cluster.cluster.id -} diff --git a/infra-examples/digitalocean/main.tf b/infra-examples/digitalocean/main.tf index c7f2b19..73dedaf 100644 --- a/infra-examples/digitalocean/main.tf +++ b/infra-examples/digitalocean/main.tf @@ -1,94 +1,93 @@ -# A cluster to test proof of concept on DigitalOcean -terraform { - required_providers { - digitalocean = { - source = "digitalocean/digitalocean" - version = ">=2.23" - } - kubernetes = { - source = "hashicorp/kubernetes" - version = "2.35.0" - } - kubectl = { - source = "gavinbunney/kubectl" - version = "1.18.0" - } - helm = { - source = "hashicorp/helm" - version = "2.16.1" - } - } -} +data "digitalocean_kubernetes_versions" "available_versions" {} -# Configure the DigitalOcean Provider -provider "digitalocean" { - token = var.do_token -} +module "main_vpc" { + source = "../../terraform/modules/digitalocean/vpc" -variable "cluster_name" { type = string } -variable "do_token" { - type = string - sensitive = true + region = var.region + environment = var.environment } -module "k8s_cluster" { - source = "./k8s-cluster" +module "kubernetes_cluster" { + source = "../../terraform/modules/digitalocean/doks" - cluster_name = var.cluster_name - # max_worker_node_count = var.max_worker_node_count - # min_worker_node_count = var.min_worker_node_count - # worker_node_size = var.worker_node_size - # region = var.do_region - # vpc_uuid = digitalocean_vpc.main_vpc.id - # vpc_ip_range = var.vpc_ip_range -} + region = var.region + environment = var.environment + vpc_id = module.main_vpc.vpc_id -# Pre-declare data sources that we can use to get the cluster ID and auth info, once it's created -data "digitalocean_kubernetes_cluster" "cluster" { - name = var.cluster_name - # Set the depends_on so that the data source doesn't - # try to read from a cluster that doesn't exist, causing - # failures when trying to run a `tofu plan`. - depends_on = [module.k8s_cluster.cluster_id] + cluster_name = var.kubernetes_cluster_name + kubernetes_version = data.digitalocean_kubernetes_versions.available_versions.latest_version } -# Configure Kubernetes provider -provider "kubernetes" { - host = data.digitalocean_kubernetes_cluster.cluster.endpoint - token = data.digitalocean_kubernetes_cluster.cluster.kube_config[0].token - cluster_ca_certificate = base64decode(data.digitalocean_kubernetes_cluster.cluster.kube_config[0].cluster_ca_certificate) -} +module "spaces" { + source = "../../terraform/modules/digitalocean/spaces" + + region = var.region + environment = var.environment -# Configure Helm provider -provider "helm" { - kubernetes { - host = data.digitalocean_kubernetes_cluster.cluster.endpoint - token = data.digitalocean_kubernetes_cluster.cluster.kube_config[0].token - cluster_ca_certificate = base64decode(data.digitalocean_kubernetes_cluster.cluster.kube_config[0].cluster_ca_certificate) - } + bucket_prefix = "my-institute" } -provider "kubectl" { - host = data.digitalocean_kubernetes_cluster.cluster.endpoint - token = data.digitalocean_kubernetes_cluster.cluster.kube_config[0].token - cluster_ca_certificate = base64decode(data.digitalocean_kubernetes_cluster.cluster.kube_config[0].cluster_ca_certificate) - load_config_file = false +module "mysql_database" { + source = "../../terraform/modules/digitalocean/database" + + region = var.region + environment = var.environment + access_token = var.do_access_token + vpc_id = module.main_vpc.vpc_id + kubernetes_cluster_name = var.kubernetes_cluster_name + + database_engine = "mysql" + database_engine_version = 8 + database_cluster_instances = 1 + database_cluster_instance_size = "db-s-1vcpu-1gb" + database_maintenance_window_day = "sunday" + database_maintenance_window_time = "01:00:00" + + # Database cluster firewalls cannot use VPC CIDR, therefore the access is + # limited to the k8s cluster + firewall_rules = [ + { + type = "k8s" + value = module.kubernetes_cluster.cluster_id + }, + ] } +module "mongodb_database" { + source = "../../terraform/modules/digitalocean/database" -# Declare the kubeconfig as an output - access it anytime with "tofu output -raw kubeconfig" -output "kubeconfig" { - value = module.k8s_cluster.kubeconfig.raw_config - sensitive = true + region = var.region + environment = var.environment + access_token = var.do_access_token + vpc_id = module.main_vpc.vpc_id + kubernetes_cluster_name = var.kubernetes_cluster_name + + database_engine = "mongodb" + database_engine_version = 7 + database_cluster_instances = 3 + database_cluster_instance_size = "db-s-1vcpu-1gb" + database_maintenance_window_day = "sunday" + database_maintenance_window_time = "1:00" + + # Database cluster firewalls cannot use VPC CIDR, therefore the access is + # limited to the k8s cluster + firewall_rules = [ + { + type = "k8s" + value = module.kubernetes_cluster.cluster_id + }, + ] } resource "digitalocean_project" "project" { - name = var.cluster_name - description = "Testing the use of Helm to provision a cluster for multi-instance tutor deployment" + name = var.kubernetes_cluster_name + description = "Open edX deployment using Harmony" purpose = "Web Application" - environment = "Production" resources = [ - module.k8s_cluster.cluster_urn, + module.kubernetes_cluster.cluster_urn, + module.spaces.bucket_urn, + module.mysql_database.cluster_urn, + module.mongodb_database.cluster_urn, ] } diff --git a/infra-examples/digitalocean/providers.tf b/infra-examples/digitalocean/providers.tf new file mode 100644 index 0000000..ac50f0a --- /dev/null +++ b/infra-examples/digitalocean/providers.tf @@ -0,0 +1,56 @@ +terraform { + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + version = ">=2.45" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">=2.34" + } + kubectl = { + source = "gavinbunney/kubectl" + version = ">=1.17" + } + helm = { + source = "hashicorp/helm" + version = ">=2.16" + } + } +} + +# Pre-declare data sources that we can use to get the cluster ID and auth info, +# once it's created. Set the `depends_on` so that the data source doesn't try +# to read from a cluster that doesn't exist, causing failures when trying to +# run a `terraform plan`. +data "digitalocean_kubernetes_cluster" "cluster" { + name = module.kubernetes_cluster.cluster_name + depends_on = [module.kubernetes_cluster.cluster_id] +} + +provider "digitalocean" { + token = var.do_access_token + spaces_access_id = "DO00M682HAUD3KXUQNL3" + spaces_secret_key = "fnoviZN11Y0NAoeAXxUrOU0liJyKcfP4yQboHJkJKY0" +} + +provider "kubernetes" { + host = data.digitalocean_kubernetes_cluster.cluster.endpoint + token = data.digitalocean_kubernetes_cluster.cluster.kube_config[0].token + cluster_ca_certificate = base64decode(data.digitalocean_kubernetes_cluster.cluster.kube_config[0].cluster_ca_certificate) +} + +provider "helm" { + kubernetes { + host = data.digitalocean_kubernetes_cluster.cluster.endpoint + token = data.digitalocean_kubernetes_cluster.cluster.kube_config[0].token + cluster_ca_certificate = base64decode(data.digitalocean_kubernetes_cluster.cluster.kube_config[0].cluster_ca_certificate) + } +} + +provider "kubectl" { + host = data.digitalocean_kubernetes_cluster.cluster.endpoint + token = data.digitalocean_kubernetes_cluster.cluster.kube_config[0].token + cluster_ca_certificate = base64decode(data.digitalocean_kubernetes_cluster.cluster.kube_config[0].cluster_ca_certificate) + load_config_file = false +} diff --git a/infra-examples/digitalocean/variables.tf b/infra-examples/digitalocean/variables.tf new file mode 100644 index 0000000..5a6a435 --- /dev/null +++ b/infra-examples/digitalocean/variables.tf @@ -0,0 +1,35 @@ +variable "do_access_token" { + type = string + description = "DitialOcean access token." + sensitive = true +} + +variable "kubernetes_cluster_name" { + type = string + description = "Name of the DigitalOcean Kubernetes cluster to create." +} + +variable "region" { + type = string + description = "DigitalOcean region to create the resources in." + validation { + condition = contains([ + "ams3", + "blr1", + "fra1", + "lon1", + "nyc3", + "sfo2", + "sfo3", + "sgp1", + "syd1", + "tor1", + ], var.region) + error_message = "The DigitalOcean region must be in the acceptable region list." + } +} + +variable "environment" { + type = string + description = "The DigitalOcean project environment. (for example: production, staging, development, etc.)" +} diff --git a/terraform/modules/digitalocean/database/README.md b/terraform/modules/digitalocean/database/README.md new file mode 100644 index 0000000..0b24285 --- /dev/null +++ b/terraform/modules/digitalocean/database/README.md @@ -0,0 +1,56 @@ +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [digitalocean](#provider\_digitalocean) | n/a | +| [null](#provider\_null) | n/a | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [digitalocean_database_cluster.database_cluster](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/database_cluster) | resource | +| [digitalocean_database_db.databases](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/database_db) | resource | +| [digitalocean_database_firewall.database_cluster_firewall](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/database_firewall) | resource | +| [digitalocean_database_user.users](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/database_user) | resource | +| [null_resource.no_primary_key_patch_database_cluster](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | +| [digitalocean_vpc.vpc](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/data-sources/vpc) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [access\_token](#input\_access\_token) | DigitalOcean access token in order to patch the database settings. | `string` | n/a | yes | +| [database\_cluster\_instance\_size](#input\_database\_cluster\_instance\_size) | Database instance size. | `string` | `"s-1vcpu-1gb"` | no | +| [database\_cluster\_instances](#input\_database\_cluster\_instances) | Number of nodes in the database cluster. | `number` | `1` | no | +| [database\_engine](#input\_database\_engine) | Database engine name. | `string` | n/a | yes | +| [database\_engine\_version](#input\_database\_engine\_version) | Database engine version. | `string` | n/a | yes | +| [database\_maintenance\_window\_day](#input\_database\_maintenance\_window\_day) | The day when maintenance can be executed on the database cluster. | `string` | `"sunday"` | no | +| [database\_maintenance\_window\_time](#input\_database\_maintenance\_window\_time) | The hour in UTC at which maintenance updates will be applied in 24 hour format. | `string` | n/a | yes | +| [database\_users](#input\_database\_users) | Map of overrides for the user and database names. |
map(object({
username = string
database = string
}))
| `{}` | no | +| [environment](#input\_environment) | The DigitalOcean project environment. (for example: production, staging, development, etc.) | `string` | n/a | yes | +| [firewall\_rules](#input\_firewall\_rules) | List of rules to apply on the related firewalls. |
list(object({
type = string
value = string
}))
| n/a | yes | +| [kubernetes\_cluster\_name](#input\_kubernetes\_cluster\_name) | The name of the Kubernetes cluster. | `string` | n/a | yes | +| [region](#input\_region) | DigitalOcean region to create the resources in. | `string` | n/a | yes | +| [vpc\_id](#input\_vpc\_id) | ID of the VPC to use for the Kubernetes cluster. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [cluster\_connection\_string](#output\_cluster\_connection\_string) | The URI to use as a connection string for the database cluster. | +| [cluster\_host](#output\_cluster\_host) | The hostname of the database cluster. | +| [cluster\_id](#output\_cluster\_id) | The unique resource ID of the database cluster. | +| [cluster\_port](#output\_cluster\_port) | The port on which the database cluster is waiting for client connections. | +| [cluster\_root\_password](#output\_cluster\_root\_password) | Database root user password | +| [cluster\_root\_user](#output\_cluster\_root\_user) | Database root user | +| [cluster\_urn](#output\_cluster\_urn) | The unique resource ID of the database cluster. | +| [database\_user\_credentials](#output\_database\_user\_credentials) | List of database and user credentials mapping. | diff --git a/terraform/modules/digitalocean/database/main.tf b/terraform/modules/digitalocean/database/main.tf new file mode 100644 index 0000000..40958b0 --- /dev/null +++ b/terraform/modules/digitalocean/database/main.tf @@ -0,0 +1,86 @@ +terraform { + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + } + } +} + +data "digitalocean_vpc" "vpc" { + id = var.vpc_id +} + +resource "digitalocean_database_cluster" "database_cluster" { + name = substr("${var.kubernetes_cluster_name}-${var.environment}-${var.database_engine}", 0, 30) + engine = var.database_engine + version = var.database_engine_version + size = var.database_cluster_instance_size + region = var.region + node_count = var.database_cluster_instances + private_network_uuid = var.vpc_id + tags = [] + + maintenance_window { + day = var.database_maintenance_window_day + hour = var.database_maintenance_window_time + } +} + +# DigitalOcean cannot set VPC CIDR blocks for rules, so we have to list the rules +# one by one. Since the rules are dynamic and depends on resources from other modules, +# we dynamically create those rules given as an input for the module. This allows higher +# chance for misconfiguration, but necessary to work with other modules' resources. +resource "digitalocean_database_firewall" "database_cluster_firewall" { + cluster_id = digitalocean_database_cluster.database_cluster.id + + dynamic "rule" { + for_each = var.firewall_rules + + content { + type = rule.value["type"] + value = rule.value["value"] + } + } +} + +resource "digitalocean_database_user" "users" { + for_each = toset([ + for _, user in var.database_users : + user.username + ]) + + cluster_id = digitalocean_database_cluster.database_cluster.id + name = each.value +} + +resource "digitalocean_database_db" "databases" { + for_each = toset([ + for _, user in var.database_users : + user.database + ]) + + cluster_id = digitalocean_database_cluster.database_cluster.id + name = each.value +} + +# This change is needed because of a foreign key constraint issue in Django-wiki version +# that was used by open edX. openedx/edx-platform#30709 +resource "null_resource" "no_primary_key_patch_database_cluster" { + triggers = { + cluster_id = digitalocean_database_cluster.database_cluster.id + } + + provisioner "local-exec" { + command = <<-EOT + curl -X PATCH \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $DO_TOKEN" \ + -d '{"config": {"sql_mode": "ANSI,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,STRICT_ALL_TABLES"}}' \ + "https://api.digitalocean.com/v2/databases/$CLUSTER_ID/config" + EOT + environment = { + DO_TOKEN = var.access_token + CLUSTER_ID = digitalocean_database_cluster.database_cluster.id + } + } +} diff --git a/terraform/modules/digitalocean/database/outputs.tf b/terraform/modules/digitalocean/database/outputs.tf new file mode 100644 index 0000000..ab0e919 --- /dev/null +++ b/terraform/modules/digitalocean/database/outputs.tf @@ -0,0 +1,49 @@ +output "cluster_id" { + value = digitalocean_database_cluster.database_cluster.id + description = "The unique resource ID of the database cluster." +} + +output "cluster_urn" { + value = digitalocean_database_cluster.database_cluster.urn + description = "The unique resource ID of the database cluster." +} + +output "cluster_root_user" { + value = digitalocean_database_cluster.database_cluster.user + description = "Database root user" + sensitive = true +} + +output "cluster_root_password" { + value = digitalocean_database_cluster.database_cluster.password + description = "Database root user password" + sensitive = true +} + +output "cluster_host" { + value = digitalocean_database_cluster.database_cluster.host + description = "The hostname of the database cluster." +} + +output "cluster_port" { + value = digitalocean_database_cluster.database_cluster.port + description = "The port on which the database cluster is waiting for client connections." +} + +output "cluster_connection_string" { + value = digitalocean_database_cluster.database_cluster.uri + description = "The URI to use as a connection string for the database cluster." +} + +output "database_user_credentials" { + value = { + for key, val in var.database_users : + key => { + username = val.username + password = try(digitalocean_database_user.users[val.username].password, "") + database = try(digitalocean_database_db.databases[val.username].name, "") + } + } + description = "List of database and user credentials mapping." + sensitive = true +} diff --git a/terraform/modules/digitalocean/database/variables.tf b/terraform/modules/digitalocean/database/variables.tf new file mode 100644 index 0000000..928ab72 --- /dev/null +++ b/terraform/modules/digitalocean/database/variables.tf @@ -0,0 +1,110 @@ +variable "access_token" { + type = string + description = "DigitalOcean access token in order to patch the database settings." +} + +variable "region" { + type = string + description = "DigitalOcean region to create the resources in." + validation { + condition = contains([ + "ams3", + "blr1", + "fra1", + "lon1", + "nyc3", + "sfo2", + "sfo3", + "sgp1", + "syd1", + "tor1", + ], var.region) + error_message = "The DigitalOcean region must be in the acceptable region list." + } +} + +variable "environment" { + type = string + description = "The DigitalOcean project environment. (for example: production, staging, development, etc.)" +} + +variable "database_engine" { + type = string + description = "Database engine name." +} + +variable "database_engine_version" { + type = string + description = "Database engine version." +} + + +variable "database_cluster_instances" { + type = number + default = 1 + description = "Number of nodes in the database cluster." +} + +variable "database_cluster_instance_size" { + type = string + default = "s-1vcpu-1gb" + description = "Database instance size." +} + +variable "database_maintenance_window_day" { + type = string + default = "sunday" + description = "The day when maintenance can be executed on the database cluster." + + validation { + condition = contains([ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + ], var.database_maintenance_window_day) + error_message = "The day of the week on which to apply maintenance updates." + } +} + +variable "database_maintenance_window_time" { + type = string + description = "The hour in UTC at which maintenance updates will be applied in 24 hour format." +} + +variable "database_users" { + type = map(object({ + username = string + database = string + })) + default = {} + description = "Map of overrides for the user and database names." +} + +variable "kubernetes_cluster_name" { + type = string + description = "The name of the Kubernetes cluster." +} + +variable "vpc_id" { + type = string + description = "ID of the VPC to use for the Kubernetes cluster." +} + +variable "firewall_rules" { + type = list(object({ + type = string + value = string + })) + description = "List of rules to apply on the related firewalls." + + validation { + condition = alltrue([ + for rule in var.firewall_rules : contains(["droplet", "k8s", "ip_addr", "tag", "app"], rule.type) + ]) + error_message = "The DigitalOcean database cluster's firewall rule must be one of \"droplet\", \"k8s\", \"ip_addr\", \"tag\", or \"app\"." + } +} diff --git a/terraform/modules/digitalocean/doks/README.md b/terraform/modules/digitalocean/doks/README.md new file mode 100644 index 0000000..bf83479 --- /dev/null +++ b/terraform/modules/digitalocean/doks/README.md @@ -0,0 +1,48 @@ +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [digitalocean](#provider\_digitalocean) | n/a | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [digitalocean_kubernetes_cluster.cluster](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/kubernetes_cluster) | resource | +| [digitalocean_kubernetes_node_pool.additional_node_pools](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/kubernetes_node_pool) | resource | +| [digitalocean_tag.worker_firewall](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/tag) | resource | +| [digitalocean_vpc.vpc](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/data-sources/vpc) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_node\_pools](#input\_additional\_node\_pools) | Additional node pools attached to the cluster. |
list(object({
name = string
size = string
min_node_count = number
max_node_count = number
labels = optional(map(any))
}))
| `[]` | no | +| [cluster\_name](#input\_cluster\_name) | The name of the Kubernetes cluster. | `string` | n/a | yes | +| [environment](#input\_environment) | The DigitalOcean project environment. (for example: production, staging, development, etc.) | `string` | n/a | yes | +| [is\_auto\_scaling\_enabled](#input\_is\_auto\_scaling\_enabled) | Whether auto scaling is enabled for the cluster or not. | `bool` | n/a | yes | +| [is\_auto\_upgrade\_enabled](#input\_is\_auto\_upgrade\_enabled) | Whether auto upgrade is enabled for the cluster or not. | `bool` | n/a | yes | +| [is\_surge\_upgrade\_enabled](#input\_is\_surge\_upgrade\_enabled) | Whether surge upgrade is enabled for the cluster or not. | `bool` | n/a | yes | +| [kubernetes\_version](#input\_kubernetes\_version) | The supported Kubernetes version to install for the cluster. | `string` | n/a | yes | +| [max\_worker\_node\_count](#input\_max\_worker\_node\_count) | Maximum number of running Kubernetes worker nodes. | `number` | `5` | no | +| [min\_worker\_node\_count](#input\_min\_worker\_node\_count) | Minimum number of running Kubernetes worker nodes. | `number` | `3` | no | +| [region](#input\_region) | DigitalOcean region to create the resources in. | `string` | n/a | yes | +| [vpc\_id](#input\_vpc\_id) | ID of the VPC to use for the Kubernetes cluster. | `string` | n/a | yes | +| [worker\_node\_size](#input\_worker\_node\_size) | Kubernetes worker node size. | `string` | `"s-2vcpu-4gb"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [cluster\_endpoint](#output\_cluster\_endpoint) | Endpoint of the Kubernetes cluster. | +| [cluster\_id](#output\_cluster\_id) | The ID of the Kubernetes cluster that is generated during creation. | +| [cluster\_ipv4](#output\_cluster\_ipv4) | IPv4 address of the Kubernetes cluster. | +| [cluster\_urn](#output\_cluster\_urn) | The unique resource ID of the Kubernetes cluster. | diff --git a/terraform/modules/digitalocean/doks/main.tf b/terraform/modules/digitalocean/doks/main.tf new file mode 100644 index 0000000..e78efac --- /dev/null +++ b/terraform/modules/digitalocean/doks/main.tf @@ -0,0 +1,53 @@ +terraform { + required_providers { + digitalocean = { + source = "digitalocean/digitalocean" + } + } +} + +data "digitalocean_vpc" "vpc" { + id = var.vpc_id +} + +resource "digitalocean_tag" "worker_firewall" { + name = "fw-${var.cluster_name}-${var.environment}-workers" +} + +resource "digitalocean_kubernetes_cluster" "cluster" { + name = "${var.cluster_name}-${var.environment}" + region = var.region + version = var.kubernetes_version + vpc_uuid = data.digitalocean_vpc.vpc.id + auto_upgrade = var.is_auto_upgrade_enabled + surge_upgrade = var.is_surge_upgrade_enabled + + lifecycle { + ignore_changes = [ + version, + ] + } + + node_pool { + name = "${var.cluster_name}-${var.environment}-workers" + size = var.worker_node_size + min_nodes = var.min_worker_node_count + max_nodes = var.max_worker_node_count + auto_scale = var.is_auto_scaling_enabled + tags = [digitalocean_tag.worker_firewall.name] + } +} + + +resource "digitalocean_kubernetes_node_pool" "additional_node_pools" { + for_each = { for pool in var.additional_node_pools : pool.name => pool } + + cluster_id = digitalocean_kubernetes_cluster.cluster.id + name = "${var.cluster_name}-${var.environment}-${each.key}-workers" + size = each.value.size + min_nodes = each.value.min_node_count + max_nodes = each.value.max_node_count + auto_scale = true + tags = [digitalocean_tag.worker_firewall.name] + labels = each.value.labels +} diff --git a/terraform/modules/digitalocean/doks/outputs.tf b/terraform/modules/digitalocean/doks/outputs.tf new file mode 100644 index 0000000..45d9084 --- /dev/null +++ b/terraform/modules/digitalocean/doks/outputs.tf @@ -0,0 +1,24 @@ +output "cluster_id" { + value = digitalocean_kubernetes_cluster.cluster.id + description = "The ID of the Kubernetes cluster that is generated during creation." +} + +output "cluster_urn" { + value = digitalocean_kubernetes_cluster.cluster.urn + description = "The unique resource ID of the Kubernetes cluster." +} + +output "cluster_name" { + value = digitalocean_kubernetes_cluster.cluster.name + description = "The name of the Kubernetes cluster." +} + +output "cluster_ipv4" { + value = digitalocean_kubernetes_cluster.cluster.ipv4_address + description = "IPv4 address of the Kubernetes cluster." +} + +output "cluster_endpoint" { + value = digitalocean_kubernetes_cluster.cluster.endpoint + description = "Endpoint of the Kubernetes cluster." +} diff --git a/terraform/modules/digitalocean/doks/variables.tf b/terraform/modules/digitalocean/doks/variables.tf new file mode 100644 index 0000000..8abba23 --- /dev/null +++ b/terraform/modules/digitalocean/doks/variables.tf @@ -0,0 +1,87 @@ +variable "region" { + type = string + description = "DigitalOcean region to create the resources in." + validation { + condition = contains([ + "ams3", + "blr1", + "fra1", + "lon1", + "nyc3", + "sfo2", + "sfo3", + "sgp1", + "syd1", + "tor1", + ], var.region) + error_message = "The DigitalOcean region must be in the acceptable region list." + } +} + +variable "environment" { + type = string + description = "The DigitalOcean project environment. (for example: production, staging, development, etc.)" +} + +variable "cluster_name" { + type = string + description = "The name of the Kubernetes cluster." +} + +variable "kubernetes_version" { + type = string + description = "The supported Kubernetes version to install for the cluster." +} + +variable "additional_node_pools" { + type = list(object({ + name = string + size = string + min_node_count = number + max_node_count = number + labels = optional(map(any)) + })) + default = [] + description = "Additional node pools attached to the cluster." +} + +variable "worker_node_size" { + type = string + default = "s-2vcpu-4gb" + description = "Kubernetes worker node size." +} + +variable "min_worker_node_count" { + type = number + default = 3 + description = "Minimum number of running Kubernetes worker nodes." +} + +variable "max_worker_node_count" { + type = number + default = 5 + description = "Maximum number of running Kubernetes worker nodes." +} + +variable "vpc_id" { + type = string + description = "ID of the VPC to use for the Kubernetes cluster." +} + +variable "is_auto_upgrade_enabled" { + type = bool + default = true + description = "Whether auto upgrade is enabled for the cluster or not." +} + +variable "is_surge_upgrade_enabled" { + type = bool + default = true + description = "Whether surge upgrade is enabled for the cluster or not." +} + +variable "is_auto_scaling_enabled" { + type = bool + default = true + description = "Whether auto scaling is enabled for the cluster or not." +} diff --git a/terraform/modules/digitalocean/spaces/README.md b/terraform/modules/digitalocean/spaces/README.md new file mode 100644 index 0000000..57fd070 --- /dev/null +++ b/terraform/modules/digitalocean/spaces/README.md @@ -0,0 +1,43 @@ +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [digitalocean](#provider\_digitalocean) | n/a | +| [random](#provider\_random) | n/a | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [digitalocean_spaces_bucket.spaces_bucket](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/spaces_bucket) | resource | +| [digitalocean_spaces_bucket_cors_configuration.spaces_bucket_policy](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/spaces_bucket_cors_configuration) | resource | +| [digitalocean_spaces_bucket_policy.public_root_object_policy](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/spaces_bucket_policy) | resource | +| [random_id.bucket_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [allowed\_cors\_origins](#input\_allowed\_cors\_origins) | Lists the CORS origins to allow CORS requests from. | `list(string)` |
[
"*"
]
| no | +| [bucket\_prefix](#input\_bucket\_prefix) | The prefix for the DigitalOcean spaces bucket for easier identification. | `string` | n/a | yes | +| [environment](#input\_environment) | The DigitalOcean project environment. (for example: production, staging, development, etc.) | `string` | n/a | yes | +| [is\_force\_destroy\_enabled](#input\_is\_force\_destroy\_enabled) | Determines if the DigitalOcean spaces bucket is force-destroyed or not upon deletion. | `bool` | `true` | no | +| [is\_public](#input\_is\_public) | Determines whether the DigitalOcean spaces bucket's root object is publicly available or not. | `bool` | `false` | no | +| [is\_versioning\_enabled](#input\_is\_versioning\_enabled) | Determines if versioning is allowed on the DigitalOcean spaces bucket or not. | `bool` | `true` | no | +| [region](#input\_region) | DigitalOcean region to create the resources in. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [bucket\_id](#output\_bucket\_id) | The ID of the bucket that is generated during creation. | +| [bucket\_name](#output\_bucket\_name) | The name of the bucket, including the generated suffix. | +| [bucket\_urn](#output\_bucket\_urn) | The unique resource ID of the bucket. | diff --git a/terraform/modules/digitalocean/spaces/main.tf b/terraform/modules/digitalocean/spaces/main.tf new file mode 100644 index 0000000..9888a63 --- /dev/null +++ b/terraform/modules/digitalocean/spaces/main.tf @@ -0,0 +1,75 @@ +terraform { + required_providers { + random = { + source = "hashicorp/random" + } + + digitalocean = { + source = "digitalocean/digitalocean" + } + } +} + +resource "random_id" "bucket_suffix" { + byte_length = 8 +} + +resource "digitalocean_spaces_bucket" "spaces_bucket" { + name = "${var.bucket_prefix}-${var.environment}-${random_id.bucket_suffix.dec}" + region = var.region + acl = "private" + force_destroy = var.is_force_destroy_enabled + + versioning { + enabled = var.is_versioning_enabled + } + + lifecycle_rule { + enabled = true + + expiration { + expired_object_delete_marker = true + } + + noncurrent_version_expiration { + days = 30 + } + } +} + +resource "digitalocean_spaces_bucket_cors_configuration" "spaces_bucket_policy" { + bucket = digitalocean_spaces_bucket.spaces_bucket.id + region = var.region + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["PUT", "POST", "GET"] + allowed_origins = var.allowed_cors_origins + expose_headers = ["ETag"] + max_age_seconds = 3000 + } +} + +resource "digitalocean_spaces_bucket_policy" "public_root_object_policy" { + count = var.is_public ? 1 : 0 + + bucket = digitalocean_spaces_bucket.spaces_bucket.name + region = var.region + + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Sid" : "ForumUploads", + "Effect" : "Allow", + "Principal" : "*", + "Action" : [ + "s3:GetObject" + ], + "Resource" : [ + "arn:aws:s3:::${digitalocean_spaces_bucket.spaces_bucket.name}/*", + ] + } + ] + }) +} diff --git a/terraform/modules/digitalocean/spaces/outputs.tf b/terraform/modules/digitalocean/spaces/outputs.tf new file mode 100644 index 0000000..b295825 --- /dev/null +++ b/terraform/modules/digitalocean/spaces/outputs.tf @@ -0,0 +1,14 @@ +output "bucket_id" { + value = digitalocean_spaces_bucket.spaces_bucket.id + description = "The ID of the bucket that is generated during creation." +} + +output "bucket_urn" { + value = digitalocean_spaces_bucket.spaces_bucket.urn + description = "The unique resource ID of the bucket." +} + +output "bucket_name" { + value = digitalocean_spaces_bucket.spaces_bucket.name + description = "The name of the bucket, including the generated suffix." +} diff --git a/terraform/modules/digitalocean/spaces/variables.tf b/terraform/modules/digitalocean/spaces/variables.tf new file mode 100644 index 0000000..faca329 --- /dev/null +++ b/terraform/modules/digitalocean/spaces/variables.tf @@ -0,0 +1,54 @@ +variable "region" { + type = string + description = "DigitalOcean region to create the resources in." + validation { + condition = contains([ + "ams3", + "blr1", + "fra1", + "lon1", + "nyc3", + "sfo2", + "sfo3", + "sgp1", + "syd1", + "tor1", + ], var.region) + error_message = "The DigitalOcean region must be in the acceptable region list." + } +} + +variable "environment" { + type = string + description = "The DigitalOcean project environment. (for example: production, staging, development, etc.)" +} + +variable "bucket_prefix" { + type = string + description = "The prefix for the DigitalOcean spaces bucket for easier identification." +} + +variable "allowed_cors_origins" { + type = list(string) + default = ["*"] + description = "Lists the CORS origins to allow CORS requests from." +} + +variable "is_public" { + type = bool + default = false + description = "Determines whether the DigitalOcean spaces bucket's root object is publicly available or not." +} + +variable "is_force_destroy_enabled" { + type = bool + default = true + description = "Determines if the DigitalOcean spaces bucket is force-destroyed or not upon deletion." +} + +variable "is_versioning_enabled" { + type = bool + default = true + description = "Determines if versioning is allowed on the DigitalOcean spaces bucket or not." +} + diff --git a/terraform/modules/digitalocean/vpc/README.md b/terraform/modules/digitalocean/vpc/README.md new file mode 100644 index 0000000..e17d88b --- /dev/null +++ b/terraform/modules/digitalocean/vpc/README.md @@ -0,0 +1,39 @@ +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [digitalocean](#provider\_digitalocean) | n/a | +| [random](#provider\_random) | n/a | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [digitalocean_vpc.vpc](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs/resources/vpc) | resource | +| [random_id.vpc_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [environment](#input\_environment) | The DigitalOcean project environment. (for example: production, staging, development, etc.) | `string` | n/a | yes | +| [region](#input\_region) | DigitalOcean region to create the resources in. | `string` | n/a | yes | +| [vpc\_ip\_range](#input\_vpc\_ip\_range) | IP range assigned to the VPC. | `string` | `"10.10.0.0/24"` | no | +| [vpc\_name](#input\_vpc\_name) | Optional custom name for the VPC. If not provided, a name will be generated. | `string` | `""` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [vpc\_id](#output\_vpc\_id) | The ID of the VPC that is generated during creation. | +| [vpc\_ip\_range](#output\_vpc\_ip\_range) | The IP range that is covered by the VPC. | +| [vpc\_name](#output\_vpc\_name) | The name of the VPC, including the generated suffix, if any. | +| [vpc\_urn](#output\_vpc\_urn) | The unique resource ID of the VPC. | diff --git a/terraform/modules/digitalocean/vpc/main.tf b/terraform/modules/digitalocean/vpc/main.tf new file mode 100644 index 0000000..f916d75 --- /dev/null +++ b/terraform/modules/digitalocean/vpc/main.tf @@ -0,0 +1,26 @@ +terraform { + required_providers { + random = { + source = "hashicorp/random" + } + + digitalocean = { + source = "digitalocean/digitalocean" + } + } +} + +resource "random_id" "vpc_suffix" { + count = var.vpc_name == "" ? 1 : 0 + byte_length = 8 +} + +resource "digitalocean_vpc" "vpc" { + name = var.vpc_name == "" ? "open-edx-${var.environment}-vpc-${random_id.vpc_suffix[0].dec}" : var.vpc_name + region = var.region + ip_range = var.vpc_ip_range + + timeouts { + delete = "3m" + } +} diff --git a/terraform/modules/digitalocean/vpc/outputs.tf b/terraform/modules/digitalocean/vpc/outputs.tf new file mode 100644 index 0000000..2845cf6 --- /dev/null +++ b/terraform/modules/digitalocean/vpc/outputs.tf @@ -0,0 +1,19 @@ +output "vpc_id" { + value = digitalocean_vpc.vpc.id + description = "The ID of the VPC that is generated during creation." +} + +output "vpc_urn" { + value = digitalocean_vpc.vpc.urn + description = "The unique resource ID of the VPC." +} + +output "vpc_name" { + value = digitalocean_vpc.vpc.name + description = "The name of the VPC, including the generated suffix, if any." +} + +output "vpc_ip_range" { + value = digitalocean_vpc.vpc.ip_range + description = "The IP range that is covered by the VPC." +} diff --git a/terraform/modules/digitalocean/vpc/variables.tf b/terraform/modules/digitalocean/vpc/variables.tf new file mode 100644 index 0000000..1ec0b69 --- /dev/null +++ b/terraform/modules/digitalocean/vpc/variables.tf @@ -0,0 +1,36 @@ +variable "region" { + type = string + description = "DigitalOcean region to create the resources in." + validation { + condition = contains([ + "ams3", + "blr1", + "fra1", + "lon1", + "nyc3", + "sfo2", + "sfo3", + "sgp1", + "syd1", + "tor1", + ], var.region) + error_message = "The DigitalOcean region must be in the acceptable region list." + } +} + +variable "environment" { + type = string + description = "The DigitalOcean project environment. (for example: production, staging, development, etc.)" +} + +variable "vpc_name" { + type = string + default = "" + description = "Optional custom name for the VPC. If not provided, a name will be generated." +} + +variable "vpc_ip_range" { + type = string + default = "10.10.0.0/24" + description = "IP range assigned to the VPC." +}