feat: add new scaletest infrastructure (#15573)

Closes https://github.com/coder/internal/issues/148

This has been validated to have working proxies with the `small`
scenario.

- [x] multi-region gcp infrastructure
- [x] use cloudflare provider to automate dns entries
- [x]  automate proxy registration
- [x] multi-regional proxies
- [x] move scenarios into locals

This excludes the infrastructure for `cert-manager` and `otel-collector`
and those will be included in followup PRs.
This commit is contained in:
Garrett Delfosse
2024-12-12 09:28:49 -05:00
committed by GitHub
parent d31c2f1fe7
commit 08f0eaaf3b
14 changed files with 1327 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
resource "cloudflare_record" "coder" {
for_each = local.deployments
zone_id = var.cloudflare_zone_id
name = each.value.subdomain
content = google_compute_address.coder[each.key].address
type = "A"
ttl = 3600
}
@@ -0,0 +1,105 @@
coder:
workspaceProxy: ${workspace_proxy}
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: "cloud.google.com/gke-nodepool"
operator: "In"
values: ["${node_pool}"]
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
podAffinityTerm:
topologyKey: "kubernetes.io/hostname"
labelSelector:
matchExpressions:
- key: "app.kubernetes.io/instance"
operator: "In"
values: ["${release_name}"]
env:
%{~ if workspace_proxy ~}
- name: "CODER_ACCESS_URL"
value: "${access_url}"
- name: CODER_PRIMARY_ACCESS_URL
value: "${primary_url}"
- name: CODER_PROXY_SESSION_TOKEN
valueFrom:
secretKeyRef:
key: token
name: "${proxy_token}"
%{~ endif ~}
%{~ if provisionerd ~}
- name: "CODER_URL"
value: "${access_url}"
- name: "CODER_PROVISIONERD_TAGS"
value: "scope=organization"
- name: "CODER_CONFIG_DIR"
value: "/tmp/config"
%{~ endif ~}
%{~ if !workspace_proxy && !provisionerd ~}
- name: "CODER_ACCESS_URL"
value: "${access_url}"
- name: "CODER_PG_CONNECTION_URL"
valueFrom:
secretKeyRef:
name: "${db_secret}"
key: url
- name: "CODER_PROVISIONER_DAEMONS"
value: "0"
- name: CODER_PROVISIONER_DAEMON_PSK
valueFrom:
secretKeyRef:
key: psk
name: "${provisionerd_psk}"
- name: "CODER_PROMETHEUS_COLLECT_AGENT_STATS"
value: "true"
- name: "CODER_PROMETHEUS_COLLECT_DB_METRICS"
value: "true"
- name: "CODER_PPROF_ENABLE"
value: "true"
%{~ endif ~}
- name: "CODER_CACHE_DIRECTORY"
value: "/tmp/coder"
- name: "CODER_TELEMETRY_ENABLE"
value: "false"
- name: "CODER_LOGGING_HUMAN"
value: "/dev/null"
- name: "CODER_LOGGING_STACKDRIVER"
value: "/dev/stderr"
- name: "CODER_PROMETHEUS_ENABLE"
value: "true"
- name: "CODER_VERBOSE"
value: "true"
- name: "CODER_EXPERIMENTS"
value: "${experiments}"
- name: "CODER_DANGEROUS_DISABLE_RATE_LIMITS"
value: "true"
image:
repo: ${image_repo}
tag: ${image_tag}
replicaCount: "${replicas}"
resources:
requests:
cpu: "${cpu_request}"
memory: "${mem_request}"
limits:
cpu: "${cpu_limit}"
memory: "${mem_limit}"
securityContext:
readOnlyRootFilesystem: true
%{~ if !provisionerd ~}
service:
enable: true
sessionAffinity: None
loadBalancerIP: "${ip_address}"
%{~ endif ~}
volumeMounts:
- mountPath: "/tmp"
name: cache
readOnly: false
volumes:
- emptyDir:
sizeLimit: 1024Mi
name: cache
+102
View File
@@ -0,0 +1,102 @@
data "http" "coder_healthy" {
url = local.deployments.primary.url
// Wait up to 5 minutes for DNS to propagate
retry {
attempts = 30
min_delay_ms = 10000
}
lifecycle {
postcondition {
condition = self.status_code == 200
error_message = "${self.url} returned an unhealthy status code"
}
}
depends_on = [helm_release.coder_primary, cloudflare_record.coder["primary"]]
}
resource "null_resource" "api_key" {
provisioner "local-exec" {
interpreter = ["/bin/bash", "-c"]
command = <<EOF
set -e
curl '${local.deployments.primary.url}/api/v2/users/first' \
--data-raw $'{"email":"${local.coder_admin_email}","password":"${local.coder_admin_password}","username":"${local.coder_admin_user}","name":"${local.coder_admin_full_name}","trial":false}' \
--insecure --silent --output /dev/null
session_token=$(curl '${local.deployments.primary.url}/api/v2/users/login' \
--data-raw $'{"email":"${local.coder_admin_email}","password":"${local.coder_admin_password}"}' \
--insecure --silent | jq -r .session_token)
echo -n $${session_token} > ${path.module}/.coderv2/session_token
api_key=$(curl '${local.deployments.primary.url}/api/v2/users/me/keys/tokens' \
-H "Coder-Session-Token: $${session_token}" \
--data-raw '{"token_name":"terraform","scope":"all"}' \
--insecure --silent | jq -r .key)
echo -n $${api_key} > ${path.module}/.coderv2/api_key
EOF
}
depends_on = [data.http.coder_healthy]
}
data "local_file" "api_key" {
filename = "${path.module}/.coderv2/api_key"
depends_on = [null_resource.api_key]
}
resource "null_resource" "license" {
provisioner "local-exec" {
interpreter = ["/bin/bash", "-c"]
command = <<EOF
curl '${local.deployments.primary.url}/api/v2/licenses' \
-H "Coder-Session-Token: ${trimspace(data.local_file.api_key.content)}" \
--data-raw '{"license":"${var.coder_license}"}' \
--insecure --silent --output /dev/null
EOF
}
}
resource "null_resource" "europe_proxy_token" {
provisioner "local-exec" {
interpreter = ["/bin/bash", "-c"]
command = <<EOF
curl '${local.deployments.primary.url}/api/v2/workspaceproxies' \
-H "Coder-Session-Token: ${trimspace(data.local_file.api_key.content)}" \
--data-raw '{"name":"europe","display_name":"Europe","icon":"/emojis/1f950.png"}' \
--insecure --silent \
| jq -r .proxy_token > ${path.module}/.coderv2/europe_proxy_token
EOF
}
depends_on = [null_resource.license]
}
data "local_file" "europe_proxy_token" {
filename = "${path.module}/.coderv2/europe_proxy_token"
depends_on = [null_resource.europe_proxy_token]
}
resource "null_resource" "asia_proxy_token" {
provisioner "local-exec" {
interpreter = ["/bin/bash", "-c"]
command = <<EOF
curl '${local.deployments.primary.url}/api/v2/workspaceproxies' \
-H "Coder-Session-Token: ${trimspace(data.local_file.api_key.content)}" \
--data-raw '{"name":"asia","display_name":"Asia","icon":"/emojis/1f35b.png"}' \
--insecure --silent \
| jq -r .proxy_token > ${path.module}/.coderv2/asia_proxy_token
EOF
}
depends_on = [null_resource.license]
}
data "local_file" "asia_proxy_token" {
filename = "${path.module}/.coderv2/asia_proxy_token"
depends_on = [null_resource.asia_proxy_token]
}
@@ -0,0 +1,160 @@
resource "local_file" "kubernetes_template" {
filename = "${path.module}/.coderv2/templates/kubernetes/main.tf"
content = <<EOF
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "~> 0.23.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.30"
}
}
}
provider "coder" {}
provider "kubernetes" {
config_path = null # always use host
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
os = "linux"
arch = "amd64"
}
resource "kubernetes_pod" "main" {
count = data.coder_workspace.me.start_count
metadata {
name = "coder-$${lower(data.coder_workspace_owner.me.name)}-$${lower(data.coder_workspace.me.name)}"
namespace = "${local.coder_namespace}"
labels = {
"app.kubernetes.io/name" = "coder-workspace"
"app.kubernetes.io/instance" = "coder-workspace-$${lower(data.coder_workspace_owner.me.name)}-$${lower(data.coder_workspace.me.name)}"
}
}
spec {
security_context {
run_as_user = "1000"
fs_group = "1000"
}
container {
name = "dev"
image = "${var.workspace_image}"
image_pull_policy = "Always"
command = ["sh", "-c", coder_agent.main.init_script]
security_context {
run_as_user = "1000"
}
env {
name = "CODER_AGENT_TOKEN"
value = coder_agent.main.token
}
resources {
requests = {
"cpu" = "${local.scenarios[var.scenario].workspaces.cpu_request}"
"memory" = "${local.scenarios[var.scenario].workspaces.mem_request}"
}
limits = {
"cpu" = "${local.scenarios[var.scenario].workspaces.cpu_limit}"
"memory" = "${local.scenarios[var.scenario].workspaces.mem_limit}"
}
}
}
affinity {
node_affinity {
required_during_scheduling_ignored_during_execution {
node_selector_term {
match_expressions {
key = "cloud.google.com/gke-nodepool"
operator = "In"
values = ["${google_container_node_pool.node_pool["primary_workspaces"].name}","${google_container_node_pool.node_pool["europe_workspaces"].name}","${google_container_node_pool.node_pool["asia_workspaces"].name}"]
}
}
}
}
}
}
}
EOF
}
resource "kubernetes_config_map" "template" {
provider = kubernetes.primary
metadata {
name = "coder-template"
namespace = kubernetes_namespace.coder_primary.metadata.0.name
}
data = {
"main.tf" = local_file.kubernetes_template.content
}
}
resource "kubernetes_job" "push_template" {
provider = kubernetes.primary
metadata {
name = "${var.name}-push-template"
namespace = kubernetes_namespace.coder_primary.metadata.0.name
labels = {
"app.kubernetes.io/name" = "${var.name}-push-template"
}
}
spec {
completions = 1
template {
metadata {}
spec {
affinity {
node_affinity {
required_during_scheduling_ignored_during_execution {
node_selector_term {
match_expressions {
key = "cloud.google.com/gke-nodepool"
operator = "In"
values = ["${google_container_node_pool.node_pool["primary_misc"].name}"]
}
}
}
}
}
container {
name = "cli"
image = "${var.coder_image_repo}:${var.coder_image_tag}"
command = [
"/opt/coder",
"--verbose",
"--url=${local.deployments.primary.url}",
"--token=${trimspace(data.local_file.api_key.content)}",
"templates",
"push",
"--directory=/home/coder/template",
"--yes",
"kubernetes"
]
volume_mount {
name = "coder-template"
mount_path = "/home/coder/template/main.tf"
sub_path = "main.tf"
}
}
volume {
name = "coder-template"
config_map {
name = kubernetes_config_map.template.metadata.0.name
}
}
restart_policy = "Never"
}
}
}
wait_for_completion = true
}
+146
View File
@@ -0,0 +1,146 @@
data "google_compute_default_service_account" "default" {
project = var.project_id
depends_on = [google_project_service.api["compute.googleapis.com"]]
}
locals {
deployments = {
primary = {
subdomain = "${var.name}-scaletest"
url = "http://${var.name}-scaletest.${var.cloudflare_domain}"
region = "us-east1"
zone = "us-east1-c"
cidr = "10.200.0.0/24"
}
europe = {
subdomain = "${var.name}-europe-scaletest"
url = "http://${var.name}-europe-scaletest.${var.cloudflare_domain}"
region = "europe-west1"
zone = "europe-west1-b"
cidr = "10.201.0.0/24"
}
asia = {
subdomain = "${var.name}-asia-scaletest"
url = "http://${var.name}-asia-scaletest.${var.cloudflare_domain}"
region = "asia-southeast1"
zone = "asia-southeast1-a"
cidr = "10.202.0.0/24"
}
}
node_pools = {
primary_coder = {
name = "coder"
cluster = "primary"
}
primary_workspaces = {
name = "workspaces"
cluster = "primary"
}
primary_misc = {
name = "misc"
cluster = "primary"
}
europe_coder = {
name = "coder"
cluster = "europe"
}
europe_workspaces = {
name = "workspaces"
cluster = "europe"
}
europe_misc = {
name = "misc"
cluster = "europe"
}
asia_coder = {
name = "coder"
cluster = "asia"
}
asia_workspaces = {
name = "workspaces"
cluster = "asia"
}
asia_misc = {
name = "misc"
cluster = "asia"
}
}
}
resource "google_container_cluster" "cluster" {
for_each = local.deployments
name = "${var.name}-${each.key}"
location = each.value.zone
project = var.project_id
network = google_compute_network.vpc.name
subnetwork = google_compute_subnetwork.subnet[each.key].name
networking_mode = "VPC_NATIVE"
default_max_pods_per_node = 256
ip_allocation_policy { # Required with networking_mode=VPC_NATIVE
}
release_channel {
# Setting release channel as STABLE can cause unexpected cluster upgrades.
channel = "UNSPECIFIED"
}
initial_node_count = 1
remove_default_node_pool = true
network_policy {
enabled = true
}
depends_on = [
google_project_service.api["container.googleapis.com"]
]
monitoring_config {
enable_components = ["SYSTEM_COMPONENTS"]
managed_prometheus {
enabled = false
}
}
workload_identity_config {
workload_pool = "${data.google_project.project.project_id}.svc.id.goog"
}
lifecycle {
ignore_changes = [
maintenance_policy,
release_channel,
remove_default_node_pool
]
}
}
resource "google_container_node_pool" "node_pool" {
for_each = local.node_pools
name = each.value.name
location = local.deployments[each.value.cluster].zone
project = var.project_id
cluster = google_container_cluster.cluster[each.value.cluster].name
node_count = local.scenarios[var.scenario][each.value.name].nodepool_size
node_config {
oauth_scopes = [
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/monitoring",
"https://www.googleapis.com/auth/trace.append",
"https://www.googleapis.com/auth/devstorage.read_only",
"https://www.googleapis.com/auth/service.management.readonly",
"https://www.googleapis.com/auth/servicecontrol",
]
disk_size_gb = 100
machine_type = local.scenarios[var.scenario][each.value.name].machine_type
image_type = "cos_containerd"
service_account = data.google_compute_default_service_account.default.email
tags = ["gke-node", "${var.project_id}-gke"]
labels = {
env = var.project_id
}
metadata = {
disable-legacy-endpoints = "true"
}
}
lifecycle {
ignore_changes = [management[0].auto_repair, management[0].auto_upgrade, timeouts]
}
}
+89
View File
@@ -0,0 +1,89 @@
resource "google_sql_database_instance" "db" {
name = "${var.name}-coder"
project = var.project_id
region = local.deployments.primary.region
database_version = "POSTGRES_14"
deletion_protection = false
depends_on = [google_service_networking_connection.private_vpc_connection]
settings {
tier = local.scenarios[var.scenario].cloudsql.tier
activation_policy = "ALWAYS"
availability_type = "ZONAL"
location_preference {
zone = local.deployments.primary.zone
}
database_flags {
name = "max_connections"
value = local.scenarios[var.scenario].cloudsql.max_connections
}
ip_configuration {
ipv4_enabled = false
private_network = google_compute_network.vpc.id
}
insights_config {
query_insights_enabled = true
query_string_length = 1024
record_application_tags = false
record_client_address = false
}
}
lifecycle {
ignore_changes = [deletion_protection, timeouts]
}
}
resource "google_sql_database" "coder" {
project = var.project_id
instance = google_sql_database_instance.db.id
name = "${var.name}-coder"
# required for postgres, otherwise db fails to delete
deletion_policy = "ABANDON"
lifecycle {
ignore_changes = [deletion_policy]
}
}
resource "random_password" "coder_postgres_password" {
length = 12
}
resource "random_password" "prometheus_postgres_password" {
length = 12
}
resource "google_sql_user" "coder" {
project = var.project_id
instance = google_sql_database_instance.db.id
name = "${var.name}-coder"
type = "BUILT_IN"
password = random_password.coder_postgres_password.result
# required for postgres, otherwise user fails to delete
deletion_policy = "ABANDON"
lifecycle {
ignore_changes = [deletion_policy, password]
}
}
resource "google_sql_user" "prometheus" {
project = var.project_id
instance = google_sql_database_instance.db.id
name = "${var.name}-prometheus"
type = "BUILT_IN"
password = random_password.prometheus_postgres_password.result
# required for postgres, otherwise user fails to delete
deletion_policy = "ABANDON"
lifecycle {
ignore_changes = [deletion_policy, password]
}
}
locals {
coder_db_url = "postgres://${google_sql_user.coder.name}:${urlencode(random_password.coder_postgres_password.result)}@${google_sql_database_instance.db.private_ip_address}/${google_sql_database.coder.name}?sslmode=disable"
}
+27
View File
@@ -0,0 +1,27 @@
locals {
project_apis = [
"cloudtrace",
"compute",
"container",
"logging",
"monitoring",
"servicemanagement",
"servicenetworking",
"sqladmin",
"stackdriver",
"storage-api",
]
}
data "google_project" "project" {
project_id = var.project_id
}
resource "google_project_service" "api" {
for_each = toset(local.project_apis)
project = data.google_project.project.project_id
service = "${each.value}.googleapis.com"
disable_dependent_services = false
disable_on_destroy = false
}
+42
View File
@@ -0,0 +1,42 @@
resource "google_compute_network" "vpc" {
project = var.project_id
name = var.name
auto_create_subnetworks = "false"
depends_on = [
google_project_service.api["compute.googleapis.com"]
]
}
resource "google_compute_subnetwork" "subnet" {
for_each = local.deployments
name = "${var.name}-${each.key}"
project = var.project_id
region = each.value.region
network = google_compute_network.vpc.name
ip_cidr_range = each.value.cidr
}
resource "google_compute_address" "coder" {
for_each = local.deployments
project = var.project_id
region = each.value.region
name = "${var.name}-${each.key}-coder"
address_type = "EXTERNAL"
network_tier = "PREMIUM"
}
resource "google_compute_global_address" "sql_peering" {
project = var.project_id
name = "${var.name}-sql-peering"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
prefix_length = 16
network = google_compute_network.vpc.id
}
resource "google_service_networking_connection" "private_vpc_connection" {
network = google_compute_network.vpc.id
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.sql_peering.name]
}
@@ -0,0 +1,104 @@
resource "kubernetes_namespace" "coder_asia" {
provider = kubernetes.asia
metadata {
name = local.coder_namespace
}
lifecycle {
ignore_changes = [timeouts, wait_for_default_service_account]
}
depends_on = [google_container_node_pool.node_pool["asia_misc"]]
}
resource "kubernetes_secret" "provisionerd_psk_asia" {
provider = kubernetes.asia
type = "Opaque"
metadata {
name = "coder-provisioner-psk"
namespace = kubernetes_namespace.coder_asia.metadata.0.name
}
data = {
psk = random_password.provisionerd_psk.result
}
lifecycle {
ignore_changes = [timeouts, wait_for_service_account_token]
}
}
resource "kubernetes_secret" "proxy_token_asia" {
provider = kubernetes.asia
type = "Opaque"
metadata {
name = "coder-proxy-token"
namespace = kubernetes_namespace.coder_asia.metadata.0.name
}
data = {
token = trimspace(data.local_file.asia_proxy_token.content)
}
lifecycle {
ignore_changes = [timeouts, wait_for_service_account_token]
}
}
resource "helm_release" "coder_asia" {
provider = helm.asia
repository = local.coder_helm_repo
chart = local.coder_helm_chart
name = local.coder_release_name
version = var.coder_chart_version
namespace = kubernetes_namespace.coder_asia.metadata.0.name
values = [templatefile("${path.module}/coder_helm_values.tftpl", {
workspace_proxy = true,
provisionerd = false,
primary_url = local.deployments.primary.url,
proxy_token = kubernetes_secret.proxy_token_asia.metadata.0.name,
db_secret = null,
ip_address = google_compute_address.coder["asia"].address,
provisionerd_psk = null,
access_url = local.deployments.asia.url,
node_pool = google_container_node_pool.node_pool["asia_coder"].name,
release_name = local.coder_release_name,
experiments = var.coder_experiments,
image_repo = var.coder_image_repo,
image_tag = var.coder_image_tag,
replicas = local.scenarios[var.scenario].coder.replicas,
cpu_request = local.scenarios[var.scenario].coder.cpu_request,
mem_request = local.scenarios[var.scenario].coder.mem_request,
cpu_limit = local.scenarios[var.scenario].coder.cpu_limit,
mem_limit = local.scenarios[var.scenario].coder.mem_limit,
})]
}
resource "helm_release" "provisionerd_asia" {
provider = helm.asia
repository = local.coder_helm_repo
chart = local.provisionerd_helm_chart
name = local.provisionerd_release_name
version = var.provisionerd_chart_version
namespace = kubernetes_namespace.coder_asia.metadata.0.name
values = [templatefile("${path.module}/coder_helm_values.tftpl", {
workspace_proxy = false,
provisionerd = true,
primary_url = null,
proxy_token = null,
db_secret = null,
ip_address = null,
provisionerd_psk = kubernetes_secret.provisionerd_psk_asia.metadata.0.name,
access_url = local.deployments.primary.url,
node_pool = google_container_node_pool.node_pool["asia_coder"].name,
release_name = local.coder_release_name,
experiments = var.coder_experiments,
image_repo = var.coder_image_repo,
image_tag = var.coder_image_tag,
replicas = local.scenarios[var.scenario].provisionerd.replicas,
cpu_request = local.scenarios[var.scenario].provisionerd.cpu_request,
mem_request = local.scenarios[var.scenario].provisionerd.mem_request,
cpu_limit = local.scenarios[var.scenario].provisionerd.cpu_limit,
mem_limit = local.scenarios[var.scenario].provisionerd.mem_limit,
})]
}
@@ -0,0 +1,104 @@
resource "kubernetes_namespace" "coder_europe" {
provider = kubernetes.europe
metadata {
name = local.coder_namespace
}
lifecycle {
ignore_changes = [timeouts, wait_for_default_service_account]
}
depends_on = [google_container_node_pool.node_pool["europe_misc"]]
}
resource "kubernetes_secret" "provisionerd_psk_europe" {
provider = kubernetes.europe
type = "Opaque"
metadata {
name = "coder-provisioner-psk"
namespace = kubernetes_namespace.coder_europe.metadata.0.name
}
data = {
psk = random_password.provisionerd_psk.result
}
lifecycle {
ignore_changes = [timeouts, wait_for_service_account_token]
}
}
resource "kubernetes_secret" "proxy_token_europe" {
provider = kubernetes.europe
type = "Opaque"
metadata {
name = "coder-proxy-token"
namespace = kubernetes_namespace.coder_europe.metadata.0.name
}
data = {
token = trimspace(data.local_file.europe_proxy_token.content)
}
lifecycle {
ignore_changes = [timeouts, wait_for_service_account_token]
}
}
resource "helm_release" "coder_europe" {
provider = helm.europe
repository = local.coder_helm_repo
chart = local.coder_helm_chart
name = local.coder_release_name
version = var.coder_chart_version
namespace = kubernetes_namespace.coder_europe.metadata.0.name
values = [templatefile("${path.module}/coder_helm_values.tftpl", {
workspace_proxy = true,
provisionerd = false,
primary_url = local.deployments.primary.url,
proxy_token = kubernetes_secret.proxy_token_europe.metadata.0.name,
db_secret = null,
ip_address = google_compute_address.coder["europe"].address,
provisionerd_psk = null,
access_url = local.deployments.europe.url,
node_pool = google_container_node_pool.node_pool["europe_coder"].name,
release_name = local.coder_release_name,
experiments = var.coder_experiments,
image_repo = var.coder_image_repo,
image_tag = var.coder_image_tag,
replicas = local.scenarios[var.scenario].coder.replicas,
cpu_request = local.scenarios[var.scenario].coder.cpu_request,
mem_request = local.scenarios[var.scenario].coder.mem_request,
cpu_limit = local.scenarios[var.scenario].coder.cpu_limit,
mem_limit = local.scenarios[var.scenario].coder.mem_limit,
})]
}
resource "helm_release" "provisionerd_europe" {
provider = helm.europe
repository = local.coder_helm_repo
chart = local.provisionerd_helm_chart
name = local.provisionerd_release_name
version = var.provisionerd_chart_version
namespace = kubernetes_namespace.coder_europe.metadata.0.name
values = [templatefile("${path.module}/coder_helm_values.tftpl", {
workspace_proxy = false,
provisionerd = true,
primary_url = null,
proxy_token = null,
db_secret = null,
ip_address = null,
provisionerd_psk = kubernetes_secret.provisionerd_psk_europe.metadata.0.name,
access_url = local.deployments.primary.url,
node_pool = google_container_node_pool.node_pool["europe_coder"].name,
release_name = local.coder_release_name,
experiments = var.coder_experiments,
image_repo = var.coder_image_repo,
image_tag = var.coder_image_tag,
replicas = local.scenarios[var.scenario].provisionerd.replicas,
cpu_request = local.scenarios[var.scenario].provisionerd.cpu_request,
mem_request = local.scenarios[var.scenario].provisionerd.mem_request,
cpu_limit = local.scenarios[var.scenario].provisionerd.cpu_limit,
mem_limit = local.scenarios[var.scenario].provisionerd.mem_limit,
})]
}
@@ -0,0 +1,124 @@
data "google_client_config" "default" {}
locals {
coder_admin_email = "admin@coder.com"
coder_admin_full_name = "Coder Admin"
coder_admin_user = "coder"
coder_admin_password = "SomeSecurePassword!"
coder_helm_repo = "https://helm.coder.com/v2"
coder_helm_chart = "coder"
coder_namespace = "coder"
coder_release_name = "${var.name}-coder"
provisionerd_helm_chart = "coder-provisioner"
provisionerd_release_name = "${var.name}-provisionerd"
}
resource "random_password" "provisionerd_psk" {
length = 26
}
resource "kubernetes_namespace" "coder_primary" {
provider = kubernetes.primary
metadata {
name = local.coder_namespace
}
lifecycle {
ignore_changes = [timeouts, wait_for_default_service_account]
}
depends_on = [google_container_node_pool.node_pool["primary_misc"]]
}
resource "kubernetes_secret" "coder_db" {
provider = kubernetes.primary
type = "Opaque"
metadata {
name = "coder-db-url"
namespace = kubernetes_namespace.coder_primary.metadata.0.name
}
data = {
url = local.coder_db_url
}
lifecycle {
ignore_changes = [timeouts, wait_for_service_account_token]
}
}
resource "kubernetes_secret" "provisionerd_psk_primary" {
provider = kubernetes.primary
type = "Opaque"
metadata {
name = "coder-provisioner-psk"
namespace = kubernetes_namespace.coder_primary.metadata.0.name
}
data = {
psk = random_password.provisionerd_psk.result
}
lifecycle {
ignore_changes = [timeouts, wait_for_service_account_token]
}
}
resource "helm_release" "coder_primary" {
provider = helm.primary
repository = local.coder_helm_repo
chart = local.coder_helm_chart
name = local.coder_release_name
version = var.coder_chart_version
namespace = kubernetes_namespace.coder_primary.metadata.0.name
values = [templatefile("${path.module}/coder_helm_values.tftpl", {
workspace_proxy = false,
provisionerd = false,
primary_url = null,
proxy_token = null,
db_secret = kubernetes_secret.coder_db.metadata.0.name,
ip_address = google_compute_address.coder["primary"].address,
provisionerd_psk = kubernetes_secret.provisionerd_psk_primary.metadata.0.name,
access_url = local.deployments.primary.url,
node_pool = google_container_node_pool.node_pool["primary_coder"].name,
release_name = local.coder_release_name,
experiments = var.coder_experiments,
image_repo = var.coder_image_repo,
image_tag = var.coder_image_tag,
replicas = local.scenarios[var.scenario].coder.replicas,
cpu_request = local.scenarios[var.scenario].coder.cpu_request,
mem_request = local.scenarios[var.scenario].coder.mem_request,
cpu_limit = local.scenarios[var.scenario].coder.cpu_limit,
mem_limit = local.scenarios[var.scenario].coder.mem_limit,
})]
}
resource "helm_release" "provisionerd_chart" {
provider = helm.primary
repository = local.coder_helm_repo
chart = local.provisionerd_helm_chart
name = local.provisionerd_release_name
version = var.provisionerd_chart_version
namespace = kubernetes_namespace.coder_primary.metadata.0.name
values = [templatefile("${path.module}/coder_helm_values.tftpl", {
workspace_proxy = false,
provisionerd = true,
primary_url = null,
proxy_token = null,
db_secret = null,
ip_address = null,
provisionerd_psk = kubernetes_secret.provisionerd_psk_primary.metadata.0.name,
access_url = local.deployments.primary.url,
node_pool = google_container_node_pool.node_pool["primary_coder"].name,
release_name = local.coder_release_name,
experiments = var.coder_experiments,
image_repo = var.coder_image_repo,
image_tag = var.coder_image_tag,
replicas = local.scenarios[var.scenario].provisionerd.replicas,
cpu_request = local.scenarios[var.scenario].provisionerd.cpu_request,
mem_request = local.scenarios[var.scenario].provisionerd.mem_request,
cpu_limit = local.scenarios[var.scenario].provisionerd.cpu_limit,
mem_limit = local.scenarios[var.scenario].provisionerd.mem_limit,
})]
}
+123
View File
@@ -0,0 +1,123 @@
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 4.36"
}
random = {
source = "hashicorp/random"
version = "~> 3.5"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.20"
}
// We use the kubectl provider to apply Custom Resources.
// The kubernetes provider requires the CRD is already present
// and would require a separate apply step beforehand.
// https://github.com/hashicorp/terraform-provider-kubernetes/issues/1367
kubectl = {
source = "alekc/kubectl"
version = ">= 2.0.0"
}
helm = {
source = "hashicorp/helm"
version = "~> 2.9"
}
tls = {
source = "hashicorp/tls"
version = "~> 4.0"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
required_version = "~> 1.9.0"
}
provider "google" {
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
provider "kubernetes" {
alias = "primary"
host = "https://${google_container_cluster.cluster["primary"].endpoint}"
cluster_ca_certificate = base64decode(google_container_cluster.cluster["primary"].master_auth.0.cluster_ca_certificate)
token = data.google_client_config.default.access_token
}
provider "kubernetes" {
alias = "europe"
host = "https://${google_container_cluster.cluster["europe"].endpoint}"
cluster_ca_certificate = base64decode(google_container_cluster.cluster["europe"].master_auth.0.cluster_ca_certificate)
token = data.google_client_config.default.access_token
}
provider "kubernetes" {
alias = "asia"
host = "https://${google_container_cluster.cluster["asia"].endpoint}"
cluster_ca_certificate = base64decode(google_container_cluster.cluster["asia"].master_auth.0.cluster_ca_certificate)
token = data.google_client_config.default.access_token
}
provider "kubectl" {
alias = "primary"
host = "https://${google_container_cluster.cluster["primary"].endpoint}"
cluster_ca_certificate = base64decode(google_container_cluster.cluster["primary"].master_auth.0.cluster_ca_certificate)
token = data.google_client_config.default.access_token
load_config_file = false
}
provider "kubectl" {
alias = "europe"
host = "https://${google_container_cluster.cluster["europe"].endpoint}"
cluster_ca_certificate = base64decode(google_container_cluster.cluster["europe"].master_auth.0.cluster_ca_certificate)
token = data.google_client_config.default.access_token
load_config_file = false
}
provider "kubectl" {
alias = "asia"
host = "https://${google_container_cluster.cluster["asia"].endpoint}"
cluster_ca_certificate = base64decode(google_container_cluster.cluster["asia"].master_auth.0.cluster_ca_certificate)
token = data.google_client_config.default.access_token
load_config_file = false
}
provider "helm" {
alias = "primary"
kubernetes {
host = "https://${google_container_cluster.cluster["primary"].endpoint}"
cluster_ca_certificate = base64decode(google_container_cluster.cluster["primary"].master_auth.0.cluster_ca_certificate)
token = data.google_client_config.default.access_token
}
}
provider "helm" {
alias = "europe"
kubernetes {
host = "https://${google_container_cluster.cluster["europe"].endpoint}"
cluster_ca_certificate = base64decode(google_container_cluster.cluster["europe"].master_auth.0.cluster_ca_certificate)
token = data.google_client_config.default.access_token
}
}
provider "helm" {
alias = "asia"
kubernetes {
host = "https://${google_container_cluster.cluster["asia"].endpoint}"
cluster_ca_certificate = base64decode(google_container_cluster.cluster["asia"].master_auth.0.cluster_ca_certificate)
token = data.google_client_config.default.access_token
}
}
+106
View File
@@ -0,0 +1,106 @@
locals {
scenarios = {
small = {
coder = {
nodepool_size = 1
machine_type = "t2d-standard-4"
replicas = 1
cpu_request = "1000m"
mem_request = "6Gi"
cpu_limit = "2000m"
mem_limit = "12Gi"
}
provisionerd = {
replicas = 1
cpu_request = "100m"
mem_request = "1Gi"
cpu_limit = "1000m"
mem_limit = "1Gi"
}
workspaces = {
nodepool_size = 1
machine_type = "t2d-standard-4"
cpu_request = "100m"
mem_request = "128Mi"
cpu_limit = "100m"
mem_limit = "128Mi"
}
misc = {
nodepool_size = 1
machine_type = "t2d-standard-4"
}
cloudsql = {
tier = "db-f1-micro"
max_connections = 500
}
}
medium = {
coder = {
nodepool_size = 1
machine_type = "t2d-standard-8"
replicas = 1
cpu_request = "3000m"
mem_request = "12Gi"
cpu_limit = "6000m"
mem_limit = "24Gi"
}
provisionerd = {
replicas = 1
cpu_request = "100m"
mem_request = "1Gi"
cpu_limit = "1000m"
mem_limit = "1Gi"
}
workspaces = {
nodepool_size = 1
machine_type = "t2d-standard-8"
cpu_request = "100m"
mem_request = "128Mi"
cpu_limit = "100m"
mem_limit = "128Mi"
}
misc = {
nodepool_size = 1
machine_type = "t2d-standard-4"
}
cloudsql = {
tier = "db-custom-1-3840"
max_connections = 500
}
}
large = {
coder = {
nodepool_size = 3
machine_type = "t2d-standard-8"
replicas = 3
cpu_request = "1000m"
mem_request = "6Gi"
cpu_limit = "2000m"
mem_limit = "12Gi"
}
provisionerd = {
replicas = 1
cpu_request = "100m"
mem_request = "1Gi"
cpu_limit = "1000m"
mem_limit = "1Gi"
}
workspaces = {
nodepool_size = 1
machine_type = "t2d-standard-8"
cpu_request = "100m"
mem_request = "128Mi"
cpu_limit = "100m"
mem_limit = "128Mi"
}
misc = {
nodepool_size = 1
machine_type = "t2d-standard-4"
}
cloudsql = {
tier = "db-custom-2-7680"
max_connections = 500
}
}
}
}
+87
View File
@@ -0,0 +1,87 @@
variable "name" {
description = "The name all resources will be prefixed with"
}
variable "scenario" {
description = "The scenario to deploy"
validation {
condition = contains(["small", "medium", "large"], var.scenario)
error_message = "Scenario must be one of small, medium, or large"
}
}
// GCP
variable "project_id" {
description = "The project in which to provision resources"
}
variable "k8s_version" {
description = "Kubernetes version to provision."
default = "1.24"
}
// Cloudflare
variable "cloudflare_api_token" {
description = "Cloudflare API token."
sensitive = true
}
variable "cloudflare_email" {
description = "Cloudflare email address."
sensitive = true
}
variable "cloudflare_domain" {
description = "Cloudflare coder domain."
}
variable "cloudflare_zone_id" {
description = "Cloudflare zone ID."
}
// Coder
variable "coder_license" {
description = "Coder license key."
sensitive = true
}
variable "coder_chart_version" {
description = "Version of the Coder Helm chart to install. Defaults to latest."
default = null
}
variable "coder_image_tag" {
description = "Tag to use for Coder image."
default = "latest"
}
variable "coder_image_repo" {
description = "Repository to use for Coder image."
default = "ghcr.io/coder/coder"
}
variable "coder_experiments" {
description = "Coder Experiments to enable."
default = ""
}
// Workspaces
variable "workspace_image" {
description = "Image and tag to use for workspaces."
default = "docker.io/codercom/enterprise-minimal:ubuntu"
}
variable "provisionerd_chart_version" {
description = "Version of the Provisionerd Helm chart to install. Defaults to latest."
default = null
}
variable "provisionerd_image_repo" {
description = "Repository to use for Provisionerd image."
default = "ghcr.io/coder/coder"
}
variable "provisionerd_image_tag" {
description = "Tag to use for Provisionerd image."
default = "latest"
}