diff --git a/.gitignore b/.gitignore index 839afa9..31988be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,22 @@ +# Other +*.zip + # Terraform -.terraform/ -.terraform.lock.hcl +.terraform** terraform.tfstate* **.tfvars** tf.plan +# Terragrunt +backend.tf +provider.tf + # Helm + Kubernetes infra/aws/us-east-2/apps/coder-ws/experiment/prometheus.yaml infra/aws/us-east-2/apps/coder-devel/build-and-push infra/aws/us-east-2/apps/cert-manager coder-observability/ observability/ -charts/ secrets/ # Python diff --git a/coder/agent-providers.tf b/coder/agent-providers.tf new file mode 100644 index 0000000..2fcaae7 --- /dev/null +++ b/coder/agent-providers.tf @@ -0,0 +1,466 @@ +## +# Agents Provider Configuration +## + +locals { + credential_age = 30 +} + +resource "time_rotating" "bedrock" { + rotation_days = local.credential_age +} + +resource "aws_iam_user" "agent" { + name = "coder-bedrock-provider" + path = "/${var.region}/" +} + +resource "aws_iam_user_policy_attachment" "test-attach" { + user = aws_iam_user.agent.name + policy_arn = "arn:aws:iam::aws:policy/AmazonBedrockLimitedAccess" +} + +resource "aws_iam_access_key" "agent" { + user = aws_iam_user.agent.name + lifecycle { + replace_triggered_by = [time_rotating.bedrock] + } +} + +resource "aws_iam_service_specific_credential" "bedrock" { + service_name = "bedrock.amazonaws.com" + user_name = aws_iam_user.agent.name + credential_age_days = local.credential_age + lifecycle { + replace_triggered_by = [time_rotating.bedrock] + } +} + +resource "coderd_ai_provider" "openai-gpt-oss" { + type = "openai" + name = "aws-bedrock-openai-gpt-oss" + display_name = "AWS Bedrock - OpenAI GPT OSS" + enabled = true + base_url = "https://bedrock-mantle.${var.region}.api.aws/v1" + + api_key_wo = aws_iam_service_specific_credential.bedrock.service_credential_secret + api_key_wo_version = time_rotating.bedrock.unix +} + +resource "coderd_ai_provider" "openai-gpt" { + type = "openai" + name = "aws-bedrock-openai-gpt" + display_name = "AWS Bedrock - OpenAI GPT" + enabled = true + # https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-55.html#model-card-openai-gpt-55-programmatic-access + # https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-54.html#model-card-openai-gpt-54-programmatic-access + base_url = "https://bedrock-mantle.${var.region}.api.aws/openai/v1" + + api_key_wo = aws_iam_service_specific_credential.bedrock.service_credential_secret + api_key_wo_version = time_rotating.bedrock.unix +} + +resource "coderd_ai_provider" "openai-compatible" { + type = "openai-compat" + name = "aws-bedrock-openai-compatible" + display_name = "AWS Bedrock - OpenAI Compatible" + enabled = true + base_url = "https://bedrock-mantle.${var.region}.api.aws" + + api_key_wo = aws_iam_service_specific_credential.bedrock.service_credential_secret + api_key_wo_version = time_rotating.bedrock.unix +} + +resource "coderd_ai_provider" "openai-compatible-mantle" { + type = "openai-compat" + name = "aws-bedrock-openai-mantle-compatible" + display_name = "AWS Bedrock Mantle - OpenAI Compatible" + enabled = true + base_url = "https://bedrock-mantle.${var.region}.api.aws/v1" + + api_key_wo = aws_iam_service_specific_credential.bedrock.service_credential_secret + api_key_wo_version = time_rotating.bedrock.unix +} + +resource "coderd_ai_provider" "openai-compatible-runtime" { + type = "openai-compat" + name = "aws-bedrock-openai-runtime-compatible" + display_name = "AWS Bedrock Runtime - OpenAI Compatible" + enabled = true + base_url = "https://bedrock-runtime.${var.region}.amazonaws.com/openai/v1" + + api_key_wo = aws_iam_service_specific_credential.bedrock.service_credential_secret + api_key_wo_version = time_rotating.bedrock.unix +} + +resource "coderd_ai_provider" "bedrock" { + type = "bedrock" + name = "aws-bedrock" + display_name = "AWS Bedrock" + enabled = true + base_url = "https://bedrock-runtime.${var.region}.amazonaws.com" + + settings = { + bedrock = { + model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" + small_fast_model = "global.anthropic.claude-haiku-4-5-20251001-v1:0" + access_key_wo = aws_iam_access_key.agent.id + access_key_secret_wo = aws_iam_access_key.agent.secret + credentials_wo_version = time_rotating.bedrock.unix + } + } +} + +locals { + anthropic = { + "global.anthropic.claude-opus-4-8" = { + name = "aws-anthropic-opus-4-8" + display_name = "Claude Opus 4.8" + enabled = false + model_config = { + max_output_tokens = 8192 + temperature = 1 + cost = { + input_price_per_million_tokens = "3" + output_price_per_million_tokens = "15" + } + provider_options = { + anthropic = {} + } + } + } + "global.anthropic.claude-opus-4-7" = { + name = "aws-anthropic-opus-4-7" + display_name = "Claude Opus 4.7" + enabled = false + model_config = { + max_output_tokens = 8192 + temperature = 1 + cost = { + input_price_per_million_tokens = "3" + output_price_per_million_tokens = "15" + } + provider_options = { + anthropic = {} + } + } + } + "global.anthropic.claude-opus-4-6-v1" = { + name = "aws-anthropic-opus-4-6" + display_name = "Claude Opus 4.6" + enabled = false + model_config = { + max_output_tokens = 8192 + temperature = 1 + cost = { + input_price_per_million_tokens = "3" + output_price_per_million_tokens = "15" + } + provider_options = { + anthropic = {} + } + } + } + "global.anthropic.claude-sonnet-4-6" = { + name = "aws-anthropic-sonnet-4-6" + display_name = "Claude Sonnet 4.6" + model_config = { + max_output_tokens = 8192 + temperature = 1 + cost = { + input_price_per_million_tokens = "3" + output_price_per_million_tokens = "15" + } + provider_options = { + anthropic = {} + } + } + } + # "global.anthropic.claude-fable-5" = { + # name = "aws-anthropic-fable-5" + # display_name = "Claude Fable 5" + # model_config = { + # max_output_tokens = 8192 + # temperature = 1 + # cost = { + # input_price_per_million_tokens = "3" + # output_price_per_million_tokens = "15" + # } + # provider_options = { + # anthropic = {} + # } + # } + # } + "global.anthropic.claude-sonnet-5" = { + name = "aws-anthropic-sonnet-5" + display_name = "Claude Sonnet 5" + model_config = { + max_output_tokens = 8192 + temperature = 1 + cost = { + input_price_per_million_tokens = "3" + output_price_per_million_tokens = "15" + } + provider_options = { + anthropic = {} + } + } + } + } + +} + +resource "coderd_ai_provider" "anthropic" { + + for_each = local.anthropic + + type = "bedrock" + name = each.value.name + display_name = "AWS Bedrock - ${each.value.display_name}" + enabled = try(each.value.enabled, true) + base_url = "https://bedrock-runtime.${var.region}.amazonaws.com" + + settings = { + bedrock = { + model = each.key + small_fast_model = "global.anthropic.claude-haiku-4-5-20251001-v1:0" + access_key_wo = aws_iam_access_key.agent.id + access_key_secret_wo = aws_iam_access_key.agent.secret + credentials_wo_version = time_rotating.bedrock.unix + } + } +} + +## +# Agent Models +## + +resource "coderd_agents_model" "sonnet-4-5" { + ai_provider_id = coderd_ai_provider.bedrock.id + model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" + display_name = "Claude Sonnet 4.5" + enabled = true + context_limit = 200000 + + model_config = jsonencode({ + max_output_tokens = 8192 + temperature = 1 + # POST "https://bedrock-runtime.${var.region}.amazonaws.com/v1/messages": 400 Bad Request + # { + # "message":"`temperature` may only be set to 1 when thinking is enabled. Please consult our documentation at + # https://docs.claude.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking" + # } + cost = { + input_price_per_million_tokens = "3" + output_price_per_million_tokens = "15" + } + provider_options = { + anthropic = { + effort = "low" + thinking = { budget_tokens = 4096 } + } + } + }) +} + +# Mark the Sonnet model as the deployment-wide default for Coder Agents. +# Setting a new default automatically demotes the previous one, so only a single +# coderd_default_agents_model resource should exist per deployment. +resource "coderd_default_agents_model" "default" { + model_id = coderd_agents_model.sonnet-4-5.id +} + +resource "coderd_agents_model" "haiku-4-5" { + ai_provider_id = coderd_ai_provider.bedrock.id + model = "global.anthropic.claude-haiku-4-5-20251001-v1:0" + display_name = "Claude Haiku 4.5" + enabled = true + context_limit = 200000 + + model_config = jsonencode({ + max_output_tokens = 8192 + temperature = 1 + cost = { + input_price_per_million_tokens = "3" + output_price_per_million_tokens = "15" + } + provider_options = { + anthropic = { + effort = "low" + thinking = { budget_tokens = 4096 } + } + } + }) +} + +resource "coderd_agents_model" "anthropic-models" { + + for_each = local.anthropic + + ai_provider_id = coderd_ai_provider.anthropic[each.key].id + model = each.key + display_name = each.value.display_name + enabled = true + context_limit = 200000 + + model_config = jsonencode(each.value.model_config) +} + +resource "coderd_agents_model" "gpt-oss-120b" { + ai_provider_id = coderd_ai_provider.openai-compatible-runtime.id + model = "openai.gpt-oss-120b-1:0" + display_name = "GPT OSS 120b" + enabled = true + context_limit = 200000 + + model_config = jsonencode({ + max_output_tokens = 8192 + temperature = 0.7 + cost = { + input_price_per_million_tokens = "3" + output_price_per_million_tokens = "15" + } + provider_options = { + anthropic = { + effort = "low" + thinking = { budget_tokens = 4096 } + } + } + }) +} + +resource "coderd_agents_model" "gpt-oss-20b" { + ai_provider_id = coderd_ai_provider.openai-compatible-runtime.id + model = "openai.gpt-oss-20b-1:0" + display_name = "GPT OSS 20b" + enabled = true + context_limit = 200000 + + model_config = jsonencode({ + max_output_tokens = 8192 + temperature = 0.7 + cost = { + input_price_per_million_tokens = "3" + output_price_per_million_tokens = "15" + } + provider_options = { + anthropic = { + effort = "low" + thinking = { budget_tokens = 4096 } + } + } + }) +} + +resource "coderd_agents_model" "gpt-oss-safeguard-120b" { + ai_provider_id = coderd_ai_provider.openai-gpt-oss.id + model = "openai.gpt-oss-safeguard-120b" + display_name = "GPT OSS Safeguard 120b" + enabled = true + context_limit = 200000 + + model_config = jsonencode({ + max_output_tokens = 8192 + temperature = 0.7 + cost = { + input_price_per_million_tokens = "3" + output_price_per_million_tokens = "15" + } + provider_options = { + anthropic = { + effort = "low" + thinking = { budget_tokens = 4096 } + } + } + }) +} + +resource "coderd_agents_model" "gpt-oss-safeguard-20b" { + ai_provider_id = coderd_ai_provider.openai-gpt-oss.id + model = "openai.gpt-oss-safeguard-20b" + display_name = "GPT OSS Safeguard 20b" + enabled = true + context_limit = 200000 + + model_config = jsonencode({ + max_output_tokens = 8192 + temperature = 0.7 + cost = { + input_price_per_million_tokens = "3" + output_price_per_million_tokens = "15" + } + provider_options = { + anthropic = { + effort = "low" + thinking = { budget_tokens = 4096 } + } + } + }) +} + +resource "coderd_agents_model" "gpt-5-5" { + ai_provider_id = coderd_ai_provider.openai-gpt.id + model = "openai.gpt-5.5" + display_name = "GPT 5.5" + enabled = false + context_limit = 200000 + + model_config = jsonencode({ + max_output_tokens = 8192 + temperature = 0.7 + cost = { + input_price_per_million_tokens = "3" + output_price_per_million_tokens = "15" + } + provider_options = { + openai = { + reasoning_effort = "low" + max_completion_tokens = 8192 + parallel_tool_calls = true + } + } + }) +} + +resource "coderd_agents_model" "gpt-5-4" { + ai_provider_id = coderd_ai_provider.openai-gpt.id + model = "openai.gpt-5.4" + display_name = "GPT 5.4" + enabled = false + context_limit = 200000 + + model_config = jsonencode({ + max_output_tokens = 8192 + temperature = 0.7 + cost = { + input_price_per_million_tokens = "3" + output_price_per_million_tokens = "15" + } + provider_options = { + openai = { + reasoning_effort = "low" + max_completion_tokens = 8192 + parallel_tool_calls = true + } + } + }) +} + +resource "coderd_agents_model" "nova-2-lite" { + # https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-amazon-nova-2-lite.html#model-card-amazon-nova-2-lite-programmatic-access + ai_provider_id = coderd_ai_provider.openai-compatible.id + model = "amazon.nova-2-lite-v1:0" + display_name = "Nova 2 Lite" + enabled = false + context_limit = 200000 + + model_config = jsonencode({ + max_output_tokens = 8192 + temperature = 0.7 + cost = { + input_price_per_million_tokens = "3" + output_price_per_million_tokens = "15" + } + provider_options = {} + }) +} \ No newline at end of file diff --git a/coder/main.tf b/coder/main.tf index f9e94e2..9b6bcea 100644 --- a/coder/main.tf +++ b/coder/main.tf @@ -3,8 +3,11 @@ terraform { coderd = { source = "coder/coderd" } - random = { - source = "hashicorp/random" + aws = { + source = "hashicorp/aws" + } + time = { + source = "hashicorp/time" } } backend "s3" {} @@ -24,11 +27,26 @@ variable "coder_username" { sensitive = true } +variable "region" { + type = string + default = "us-east-2" +} + +variable "profile" { + type = string + default = "demo-coder" +} + provider "coderd" { url = var.coder_primary_url token = var.coder_token } +provider "aws" { + region = var.region + profile = var.profile +} + module "experiment-org" { source = "../modules/coder/org" provisioner_key_name = "default" diff --git a/coder/providers.tf b/coder/providers.tf new file mode 100644 index 0000000..e69de29 diff --git a/infra/1-click/0-infra/0-vpc.tf b/infra/1-click/0-infra/0-vpc.tf new file mode 100644 index 0000000..056f2aa --- /dev/null +++ b/infra/1-click/0-infra/0-vpc.tf @@ -0,0 +1,49 @@ +## +# Networking Infrastructure +## + +locals { + # Subnet Discovery - https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/deploy/subnet_discovery/#subnet-filtering + tags_lb_subnet_discovery = { + "kubernetes.io/cluster/${local.formatted_name}" = "owned" + } + # Internet-Facing LB - https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/deploy/subnet_discovery/#subnet-role-tag + tags_public_lb = { + "kubernetes.io/role/elb" = 1 + } + tags_public_subnet = merge( + local.tags_lb_subnet_discovery, + local.tags_public_lb + ) + tags_private_subnet = { + "karpenter.sh/discovery" = "${local.formatted_name}" + } + # Use variable to configure AZ just in case 1 AZ isn't accessible + # https://repost.aws/questions/QUgdQev4KETKG_Bwev9tMtRQ/is-it-possible-to-enable-3rd-availability-zone-in-us-west-1#AN9eAH55FwTC-NSSp-FjIfTQ + availability_zones = [for az in var.azs : "${var.region}${az}"] + public_subnet_cidrs = ["10.0.8.0/21", "10.0.4.0/22", "10.0.0.0/22"] + private_subnet_cidrs = ["10.0.64.0/20", "10.0.80.0/20", "10.0.96.0/20"] +} + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 6.6.0" + + name = local.formatted_name + + enable_nat_gateway = true + enable_dns_hostnames = true + + cidr = "10.0.0.0/16" + azs = local.availability_zones + + public_subnet_suffix = "public" + public_subnets = [for index, _ in var.azs : local.public_subnet_cidrs[index]] + public_subnet_tags = local.tags_public_subnet + + private_subnet_suffix = "private" + private_subnets = [for index, _ in var.azs : local.private_subnet_cidrs[index]] + private_subnet_tags = local.tags_private_subnet + + tags = {} +} \ No newline at end of file diff --git a/infra/1-click/0-infra/1-db.tf b/infra/1-click/0-infra/1-db.tf new file mode 100644 index 0000000..fafff67 --- /dev/null +++ b/infra/1-click/0-infra/1-db.tf @@ -0,0 +1,99 @@ +## +# Database Infrastructure +## + +resource "aws_security_group" "postgres" { + vpc_id = module.vpc.vpc_id + name = "${local.formatted_name}-pgsql" + description = "security group for postgres all egress traffic" + tags = { + Name = "PostgreSQL" + } +} + +resource "aws_vpc_security_group_ingress_rule" "postgres" { + security_group_id = aws_security_group.postgres.id + cidr_ipv4 = module.vpc.vpc_cidr_block + ip_protocol = "tcp" + from_port = 5432 + to_port = 5432 +} + +resource "aws_vpc_security_group_egress_rule" "postgres" { + security_group_id = aws_security_group.postgres.id + cidr_ipv4 = "0.0.0.0/0" + ip_protocol = -1 +} + +resource "aws_db_subnet_group" "coder" { + name = "${local.formatted_name}-coder" + subnet_ids = module.vpc.private_subnets + + tags = { + Name = "${local.formatted_name}-coder" + } +} + +resource "time_static" "coder_snapshot" { + triggers = { + run_on_ip_change = "${local.formatted_name}-coder" + } +} + +resource "aws_db_instance" "coder" { + identifier = "${local.formatted_name}-coder" + instance_class = "db.t4g.medium" + allocated_storage = 50 + engine = "postgres" + engine_version = "15.12" + username = var.coder_username + password = var.coder_password + db_name = "coder" + db_subnet_group_name = aws_db_subnet_group.coder.name + vpc_security_group_ids = [aws_security_group.postgres.id] + publicly_accessible = false + snapshot_identifier = null + skip_final_snapshot = false + final_snapshot_identifier = "coder-${replace(time_static.coder_snapshot.rfc3339, ":", "-")}" + + tags = { + Name = "${local.formatted_name}-coder" + } + lifecycle { + ignore_changes = [ + snapshot_identifier + ] + } +} + +resource "time_static" "grafana_snapshot" { + triggers = { + run_on_ip_change = "${local.formatted_name}-grafana" + } +} + +resource "aws_db_instance" "grafana" { + identifier = "${local.formatted_name}-grafana" + instance_class = "db.t4g.medium" + allocated_storage = 50 + engine = "postgres" + engine_version = "15.12" + username = var.grafana_username + password = var.grafana_password + db_name = "grafana" + db_subnet_group_name = aws_db_subnet_group.coder.name + vpc_security_group_ids = [aws_security_group.postgres.id] + publicly_accessible = false + snapshot_identifier = null + skip_final_snapshot = false + final_snapshot_identifier = "grafana-${replace(time_static.coder_snapshot.rfc3339, ":", "-")}" + + tags = { + Name = "${local.formatted_name}-grafana" + } + lifecycle { + ignore_changes = [ + snapshot_identifier + ] + } +} \ No newline at end of file diff --git a/infra/1-click/0-infra/2-s3.tf b/infra/1-click/0-infra/2-s3.tf new file mode 100644 index 0000000..67b73c6 --- /dev/null +++ b/infra/1-click/0-infra/2-s3.tf @@ -0,0 +1,28 @@ +## +# Bucket Infrastructure +## + +## +# Loki Inputs +## + +resource "aws_s3_bucket" "loki" { + bucket = "${local.formatted_name}-grafana" + tags = var.loki_s3_bucket_tags +} + +resource "aws_s3_bucket_lifecycle_configuration" "loki" { + bucket = aws_s3_bucket.loki.id + + rule { + id = "logs" + + filter {} # Target all objects in bucket. + + expiration { + days = 365 + } + + status = "Enabled" + } +} \ No newline at end of file diff --git a/infra/1-click/0-infra/3-eks.tf b/infra/1-click/0-infra/3-eks.tf new file mode 100644 index 0000000..e3cd839 --- /dev/null +++ b/infra/1-click/0-infra/3-eks.tf @@ -0,0 +1,110 @@ +## +# Kubernetes Infrastructure +## + +locals { + # Karpenter Security Group Discovery - https://karpenter.sh/v1.0/concepts/nodeclasses/#specsecuritygroupselectorterms + tags_kptr_sg_discovery = { + "karpenter.sh/discovery" = "${local.formatted_name}-karpenter" + } + labels_system_node = { + "scheduling.coder.com/pool" = "system" + } + taints_system = { + key = "CriticalAddonsOnly" + effect = "NO_SCHEDULE" + } + tolerations_system = [{ + key = "CriticalAddonsOnly" + operator = "Exists" + }] +} + +module "eks" { + source = "terraform-aws-modules/eks/aws" + version = "~> 21.15.1" + + vpc_id = module.vpc.vpc_id + + # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_cluster#vpc_config-1 + # https://docs.aws.amazon.com/eks/latest/userguide/network-reqs.html#network-requirements-subnets + subnet_ids = toset(concat( + module.vpc.private_subnets + )) + + name = local.formatted_name + + kubernetes_version = "1.34" + endpoint_public_access = true + endpoint_private_access = true + + create_security_group = true + create_node_security_group = true + create_iam_role = true + node_security_group_tags = local.tags_kptr_sg_discovery + create_node_iam_role = true + node_iam_role_use_name_prefix = true + + compute_config = { + enabled = true + node_pools = ["system"] + } + + attach_encryption_policy = false + create_kms_key = false # Enable unless needed + encryption_config = null + enable_cluster_creator_admin_permissions = true + enable_irsa = true + + addons = { + coredns = { + before_compute = true + } + kube-proxy = { + before_compute = true + } + vpc-cni = { + before_compute = true + configuration_values = jsonencode({ + enableNetworkPolicy = "true" + nodeAgent = { + enablePolicyEventLogs = "true" + } + env = { + ENABLE_PREFIX_DELEGATION = "true" + WARM_PREFIX_TARGET = "1" + WARM_IP_TARGET = "0" + AWS_VPC_K8S_CNI_LOGLEVEL = "DEBUG" + } + }) + } + } + + tags = {} +} + +module "karpenter" { + + source = "../../../modules/k8s/bootstrap/karpenter" + + cluster_name = module.eks.cluster_name + cluster_oidc_provider_arn = module.eks.oidc_provider_arn + cluster_oidc_provider = module.eks.oidc_provider + + namespace = "karpenter" + chart_version = "1.9.0" + + node_selector = { + "beta.kubernetes.io/os" = "linux" + "kubernetes.io/os" = null + } + tolerations = local.tolerations_system + topology_spread = [] + + iam_role_use_name_prefix = true + node_iam_role_use_name_prefix = true + replicas = 2 + karpenter_controller_role_policies = { + "AmazonEFSCSIDriverPolicy" = "arn:aws:iam::aws:policy/service-role/AmazonEFSCSIDriverPolicy" + } +} \ No newline at end of file diff --git a/infra/1-click/0-infra/4-bootstrap.tf b/infra/1-click/0-infra/4-bootstrap.tf new file mode 100644 index 0000000..077fa95 --- /dev/null +++ b/infra/1-click/0-infra/4-bootstrap.tf @@ -0,0 +1,37 @@ +## +# System-Level Addons +## + +module "metrics-server" { + + depends_on = [module.eks] + source = "../../../modules/k8s/bootstrap/metrics-server" + + namespace = "metrics-server" + chart_version = "3.13.0" + tolerations = local.tolerations_system +} + +module "lb-ctrl" { + + source = "../../../modules/k8s/bootstrap/lb-controller" + cluster_name = module.eks.cluster_name + cluster_oidc_provider_arn = module.eks.oidc_provider_arn + + namespace = "lb-ctrl" + chart_version = "3.0.0" + vpc_id = module.vpc.vpc_id + tolerations = local.tolerations_system +} + +module "ebs-csi" { + + source = "../../../modules/k8s/bootstrap/ebs-csi" + cluster_name = module.eks.cluster_name + cluster_oidc_provider_arn = module.eks.oidc_provider_arn + + namespace = "ebs-csi" + chart_version = "2.55.0" + tolerations = local.tolerations_system + replace = true +} \ No newline at end of file diff --git a/infra/1-click/0-infra/main.tf b/infra/1-click/0-infra/main.tf new file mode 100644 index 0000000..e2b5a0d --- /dev/null +++ b/infra/1-click/0-infra/main.tf @@ -0,0 +1,49 @@ +## +# Global Inputs + Providers +## + +locals { + normalized_domain_name = split(".", var.domain_name)[0] + formatted_name = "${var.name}-${local.normalized_domain_name}" +} + +data "aws_eks_cluster_auth" "coder" { + name = module.eks.cluster_name +} + +provider "aws" { + region = var.region + profile = var.profile +} + +provider "helm" { + kubernetes = { + host = module.eks.cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) + exec = { + api_version = "client.authentication.k8s.io/v1beta1" + command = "aws" + args = [ + "eks", "get-token", + "--cluster-name", module.eks.cluster_name, + "--profile", var.profile, + "--region", var.region + ] + } + } +} + +provider "kubernetes" { + host = module.eks.cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "aws" + args = [ + "eks", "get-token", + "--cluster-name", module.eks.cluster_name, + "--profile", var.profile, + "--region", var.region + ] + } +} \ No newline at end of file diff --git a/infra/1-click/0-infra/terragrunt.hcl b/infra/1-click/0-infra/terragrunt.hcl new file mode 100644 index 0000000..6f1ca3b --- /dev/null +++ b/infra/1-click/0-infra/terragrunt.hcl @@ -0,0 +1,19 @@ +## +# Base Module +## + +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +inputs = { + profile = include.root.locals.CODER_AWS_PROFILE + region = include.root.locals.CODER_AWS_REGION + domain_name = include.root.locals.CODER_DOMAIN_NAME + azs = jsondecode(include.root.locals.CODER_AWS_AZS) + coder_username = include.root.locals.CODER_DB_USERNAME + coder_password = include.root.locals.CODER_DB_PASSWORD + grafana_username = include.root.locals.GRAFANA_DB_USERNAME + grafana_password = include.root.locals.GRAFANA_DB_PASSWORD +} \ No newline at end of file diff --git a/infra/1-click/0-infra/variables.tf b/infra/1-click/0-infra/variables.tf new file mode 100644 index 0000000..be3922c --- /dev/null +++ b/infra/1-click/0-infra/variables.tf @@ -0,0 +1,58 @@ +variable "region" { + description = "The AWS region to deploy AWS-resources to." + type = string + default = "us-east-2" +} + +variable "name" { + description = "Name for created resources and it's tag prefix" + type = string + default = "coder" +} + +variable "profile" { + description = "The local AWS profile to use." + type = string + default = "default" +} + +variable "domain_name" { + description = "Your domain name (i.e. coder-example.com)." + type = string +} + +variable "azs" { + type = list(string) + default = ["a", "b", "c"] +} + +variable "coder_username" { + description = "Coder DB's username." + type = string + default = "coder" +} + +variable "coder_password" { + description = "Coder DB's password." + type = string + sensitive = true + default = "th1s1sn0tas3cur3pass0wrd" +} + +variable "grafana_username" { + description = "Grafana DB's username." + type = string + default = "grafana" +} + +variable "grafana_password" { + description = "Grafana DB's password." + type = string + sensitive = true + default = "th1s1sn0tas3cur3pass0wrd" +} + +variable "loki_s3_bucket_tags" { + type = map(string) + default = {} +} \ No newline at end of file diff --git a/infra/1-click/0-init.sh b/infra/1-click/0-init.sh new file mode 100755 index 0000000..83cb72f --- /dev/null +++ b/infra/1-click/0-init.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -ae -o pipefail + +source coder.env + +terragrunt run --all --non-interactive --config root.hcl init -- -migrate-state -upgrade \ No newline at end of file diff --git a/infra/1-click/1-apply.sh b/infra/1-click/1-apply.sh new file mode 100755 index 0000000..a36c589 --- /dev/null +++ b/infra/1-click/1-apply.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -ae -o pipefail + +source coder.env + +terragrunt run --all --non-interactive --config root.hcl apply \ No newline at end of file diff --git a/infra/1-click/1-setup/5-manifests.tf b/infra/1-click/1-setup/5-manifests.tf new file mode 100644 index 0000000..0561540 --- /dev/null +++ b/infra/1-click/1-setup/5-manifests.tf @@ -0,0 +1,314 @@ +## +# Manifest Setup Post Addon-Deployment +# Includes auxiliary resources depending on CRDs +## + +## +# EBS CSI StorageClasses +## + +resource "kubernetes_manifest" "default-sc" { + manifest = { + apiVersion = "storage.k8s.io/v1" + kind = "StorageClass" + metadata = { + name = "default" + annotations = { + "storageclass.kubernetes.io/is-default-class" = "true" + } + } + provisioner = "ebs.csi.aws.com" + volumeBindingMode = "WaitForFirstConsumer" + allowedTopologies = [{ + matchLabelExpressions = [{ + key = "topology.ebs.csi.aws.com/zone" + values = [for az in var.azs : "${data.aws_region.this.region}${az}"] + }] + }] + parameters = { + type = "gp3" + encrypted = "true" + } + } +} + +resource "kubernetes_manifest" "automode-sc" { + manifest = { + apiVersion = "storage.k8s.io/v1" + kind = "StorageClass" + metadata = { + name = "automode" + } + provisioner = "ebs.csi.eks.amazonaws.com" + volumeBindingMode = "WaitForFirstConsumer" + allowedTopologies = [{ + matchLabelExpressions = [{ + key = "eks.amazonaws.com/compute-type" + values = ["auto"] + }] + }] + parameters = { + type = "gp3" + encrypted = "true" + } + } +} + +## +# NodeClass(es) for Coder (Server, Provisioner, & Workspaces) +## + +data "kubernetes_service_account_v1" "kptr" { + metadata { + name = "node-role" + namespace = "karpenter" + } +} + +locals { + nodeclass_configs = { + "coder" = { + api_version = "karpenter.k8s.aws/v1" + kind = "EC2NodeClass" + user_data = <<-EOT + MIME-Version: 1.0 + Content-Type: multipart/mixed; boundary="//" + + --// + Content-Type: application/node.eks.aws + + apiVersion: node.eks.aws/v1alpha1 + kind: NodeConfig + spec: + kubelet: + config: + registryPullQPS: 30 + + --//-- + EOT + subnet_selector = [{ + tags = { + "karpenter.sh/discovery" = "${local.formatted_name}" + } + }] + sg_selector = [{ + # Use for EKS AutoMode. AWS manages this, not TF: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_cluster#cluster_security_group_id-1 + id = data.aws_eks_cluster.coder.vpc_config[0].cluster_security_group_id + # tags = { + # # If AutoMode is enabled, THIS BREAKS. Communication to CoreDNS locked behind system nodes. Kptr SG cant talk to AutoMode's SGs (only trusts itself and another AWS-managed SG) + # "karpenter.sh/discovery" = "${local.formatted_name}-karpenter" + # } + }] + block_device_mappings = [{ + deviceName = "/dev/xvda" + ebs = { + volumeSize = "500Gi" + volumeType = "gp3" + encrypted = false + deleteOnTermination = true + } + }] + } + } +} + +resource "kubernetes_manifest" "nodeclass" { + + for_each = local.nodeclass_configs + + manifest = { + apiVersion = each.value.api_version + kind = each.value.kind + metadata = { + name = each.key + } + spec = { + role = data.kubernetes_service_account_v1.kptr.metadata[0].annotations["eks.amazonaws.com/role-arn"] + amiSelectorTerms = [{ + alias = "al2023@latest" + }] + subnetSelectorTerms = each.value.subnet_selector + securityGroupSelectorTerms = each.value.sg_selector + blockDeviceMappings = each.value.block_device_mappings + userData = each.value.user_data + } + } +} + +## +# NodePool(s) for Coder (Server, Provisioner, & Workspaces) +## + +locals { + nodepool_configs = { + # "automode" = { + # node_expires_after = "24h" + # disruption_consolidation_policy = "WhenEmpty" + # disruption_consolidate_after = "1m" + # instance_type = "t3a.large" + # node_class_ref = { + # group = "eks.amazonaws.com" + # kind = "NodeClass" + # name = "default" + # } + # taints = [] + # } + "karpenter" = { + node_expires_after = "24h" + disruption_consolidation_policy = "WhenEmpty" + disruption_consolidate_after = "1m" + instance_type = "t3a.large" + node_class_ref = { + group = "karpenter.k8s.aws" + kind = "EC2NodeClass" + name = "coder" + } + taints = [] + } + } +} + +resource "kubernetes_manifest" "nodepool" { + + depends_on = [kubernetes_manifest.nodeclass] + for_each = local.nodepool_configs + + field_manager { + force_conflicts = true + } + + wait { + condition { + type = "Ready" + status = "True" + } + } + + timeouts { + create = "10m" + update = "10m" + delete = "30s" + } + + manifest = { + apiVersion = "karpenter.sh/v1" + kind = "NodePool" + metadata = { + name = each.key + } + spec = { + template = { + metadata = { + labels = { + "node.coder.io/instance" = "coder-v2" + "node.coder.io/managed-by" = "karpenter" + "node.coder.io/name" = "coder" + "node.coder.io/part-of" = "coder" + # "eks.amazonaws.com/compute-type" = "auto" + "node.coder.io/used-for" = each.key + } + } + spec = { + taints = each.value.taints == null ? [{ + key = "dedicated" + value = each.key + effect = "NoSchedule" + }] : each.value.taints + requirements = [{ + key = "kubernetes.io/arch" + operator = "In" + values = ["amd64"] + }, { + key = "kubernetes.io/os" + operator = "In" + values = ["linux"] + }, { + key = "kubernetes.sh/capacity-type" + operator = "In" + values = ["spot", "on-demand"] + }, { + key = "node.kubernetes.io/instance-type" + operator = "In" + values = [each.value.instance_type] + }] + nodeClassRef = each.value.node_class_ref + expireAfter = each.value.node_expires_after + } + } + disruption = { + consolidationPolicy = each.value.disruption_consolidation_policy + consolidateAfter = each.value.disruption_consolidate_after + } + } + } +} + +## +# Image Prefetch DaemonSet. Add images to warm new Coder nodes with workspace image. +## + +resource "kubernetes_daemon_set_v1" "img-fetch" { + + for_each = local.nodepool_configs + + metadata { + name = "imgs-for-${each.key}" + namespace = "kube-system" + labels = { + "app.kubernetes.io/name" = "img-fetch" + "app.kubernetes.io/part-of" = "coder-workspaces" + } + } + + spec { + selector { + match_labels = { + "app.kubernetes.io/name" = "img-fetch" + } + } + + template { + metadata { + labels = { + "app.kubernetes.io/name" = "img-fetch" + } + } + + spec { + + # Select's Coder-specific nodes. Do not pull on system nodes. + node_selector = kubernetes_manifest.nodepool[each.key].manifest.spec.template.metadata.labels + + toleration { + key = "dedicated" + value = each.key + effect = "NoSchedule" + } + + termination_grace_period_seconds = 5 + + init_container { + name = "enterprise-base" + image = "docker.io/codercom/enterprise-base:ubuntu" + command = [] + } + + container { + name = "pause" + image = "registry.k8s.io/pause:3.9" + + resources { + requests = { + cpu = "1m" + memory = "1Mi" + } + limits = { + cpu = "10m" + memory = "10Mi" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/infra/1-click/1-setup/6-coder-server.tf b/infra/1-click/1-setup/6-coder-server.tf new file mode 100644 index 0000000..b5fcef8 --- /dev/null +++ b/infra/1-click/1-setup/6-coder-server.tf @@ -0,0 +1,265 @@ +## +# Fetch DNS Zone +## + +data "aws_route53_zone" "coder" { + name = local.apex_domain +} + +## +# SSL Certificate +## + +resource "aws_acm_certificate" "coder" { + domain_name = var.domain_name + subject_alternative_names = ["*.${var.domain_name}"] + validation_method = "DNS" +} + +resource "aws_route53_record" "coder_validation" { + + for_each = { + for rec in aws_acm_certificate.coder.domain_validation_options : rec.domain_name => { + name = rec.resource_record_name + record = rec.resource_record_value + type = rec.resource_record_type + } + } + + allow_overwrite = true + name = each.value.name + records = [each.value.record] + ttl = 60 + type = each.value.type + zone_id = data.aws_route53_zone.coder.zone_id +} + +resource "aws_acm_certificate_validation" "coder" { + certificate_arn = aws_acm_certificate.coder.arn + validation_record_fqdns = [for record in aws_route53_record.coder_validation : record.fqdn] + + timeouts { + create = "30m" + } +} + +resource "aws_acm_certificate" "grafana" { + domain_name = "grafana.${var.domain_name}" + subject_alternative_names = ["*.grafana.${var.domain_name}"] + validation_method = "DNS" +} + +resource "aws_route53_record" "grafana_validation" { + + for_each = { + for rec in aws_acm_certificate.grafana.domain_validation_options : rec.domain_name => { + name = rec.resource_record_name + record = rec.resource_record_value + type = rec.resource_record_type + } + } + + allow_overwrite = true + name = each.value.name + records = [each.value.record] + ttl = 60 + type = each.value.type + zone_id = data.aws_route53_zone.coder.zone_id +} + +resource "aws_acm_certificate_validation" "grafana" { + certificate_arn = aws_acm_certificate.grafana.arn + validation_record_fqdns = [for record in aws_route53_record.grafana_validation : record.fqdn] + + timeouts { + create = "30m" + } +} + +## +# ------ +## + +resource "aws_iam_user" "bedrock" { + name = "bedrock-access" + path = "/${local.normalized_domain_name}/${data.aws_region.this.region}/" +} + +resource "aws_iam_access_key" "bedrock" { + user = aws_iam_user.bedrock.name +} + +resource "aws_iam_user_policy_attachment" "bedrock" { + user = aws_iam_user.bedrock.name + # https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonBedrockLimitedAccess.html + policy_arn = "arn:aws:iam::aws:policy/AmazonBedrockLimitedAccess" +} + +locals { + pub_subs = [for az in var.azs : "${local.formatted_name}-public-${data.aws_region.this.region}${az}"] +} + +## +# EIP Mapping +## + +resource "aws_eip" "coder" { + count = length(var.azs) + domain = "vpc" + public_ipv4_pool = "amazon" + tags = { + Name = "${local.formatted_name}-coder-${count.index}" + } +} + +resource "aws_eip" "grafana" { + count = 1 + domain = "vpc" + public_ipv4_pool = "amazon" + tags = { + Name = "${local.formatted_name}-grafana-${count.index}" + } +} + +resource "aws_route53_record" "coder-primary" { + name = var.domain_name + records = aws_eip.coder.*.public_ip + ttl = 60 + type = "A" + zone_id = data.aws_route53_zone.coder.zone_id +} + +resource "aws_route53_record" "coder-wildcard" { + name = "*.${var.domain_name}" + records = aws_eip.coder.*.public_ip + ttl = 60 + type = "A" + zone_id = data.aws_route53_zone.coder.zone_id +} + +resource "aws_route53_record" "grafana-primary" { + name = "grafana.${var.domain_name}" + records = aws_eip.grafana.*.public_ip + ttl = 60 + type = "A" + zone_id = data.aws_route53_zone.coder.zone_id +} + +resource "aws_route53_record" "grafana-wildcard" { + name = "*.grafana.${var.domain_name}" + records = aws_eip.grafana.*.public_ip + ttl = 60 + type = "A" + zone_id = data.aws_route53_zone.coder.zone_id +} + +## +# Helm Installs +## + +locals { + coder_release_name = "coder" + coder_ns = "coder" +} + +module "coder-server" { + + source = "../../../modules/k8s/bootstrap/coder-server" + + release_name = local.coder_release_name + chart_version = var.coder_version + cluster_name = data.aws_eks_cluster.coder.id + cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.coder.arn + + namespace = local.coder_ns + # lb_class = "eks.amazonaws.com/nlb" + lb_class = "service.k8s.aws/nlb" + + coder = { + access_url = "https://${var.domain_name}" + wildcard_url = "*.${var.domain_name}" + image_repo = "ghcr.io/coder/coder" + image_tag = "v${var.coder_version}" + rep_cnt = 1 + # External Provisioners will be used + prov_rep_cnt = var.coder_license != "" ? 0 : 5 + } + + prometheus = { + enable = true + } + + db = { + url = data.aws_db_instance.coder.endpoint + username = var.coder_username + password = var.coder_password + } + + aibridge = { + enabled = var.coder_license != "" + bedrock = var.coder_license != "" ? { + region = data.aws_region.this.region + model = "global.anthropic.claude-opus-4-5-20251101-v1:0" + access_id = aws_iam_access_key.bedrock.id + secret_id = aws_iam_access_key.bedrock.secret + } : null + } + + resource_request = { + cpu = "1000m" + memory = "2Gi" + } + resource_limit = { + cpu = "1000m" + memory = "2Gi" + } + + tags = {} + + svc_annot = { + "service.beta.kubernetes.io/aws-load-balancer-nlb-target-type" = "instance" + "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing" + "service.beta.kubernetes.io/aws-load-balancer-attributes" = "deletion_protection.enabled=false" + "service.beta.kubernetes.io/aws-load-balancer-eip-allocations" = join(",", aws_eip.coder.*.allocation_id) + "service.beta.kubernetes.io/aws-load-balancer-subnets" = join(",", local.pub_subs) + "service.beta.kubernetes.io/aws-load-balancer-ssl-cert" = aws_acm_certificate.coder.arn + "service.beta.kubernetes.io/aws-load-balancer-ssl-ports" = 443 + } + + node_selector = kubernetes_manifest.nodepool["karpenter"].manifest.spec.template.metadata.labels + tolerations = [for toleration in kubernetes_manifest.nodepool["karpenter"].manifest.spec.template.spec.taints : { + key = toleration.key + operator = "Equal" + value = toleration.value + effect = toleration.effect + }] + + topology_spread = [{ + max_skew = 1 + topology_key = "kubernetes.io/hostname" + when_unsatisfiable = "ScheduleAnyway" + label_selector = { + match_labels = { + "app.kubernetes.io/name" = local.coder_release_name + "app.kubernetes.io/part-of" = "coder" + } + } + match_label_keys = [ + "app.kubernetes.io/instance" + ] + }] + + pod_aaf_pref_sched_ie = [{ + weight = 100 + pod_affinity_term = { + label_selector = { + match_labels = { + "app.kubernetes.io/instance" = "coder" + "app.kubernetes.io/name" = local.coder_release_name + "app.kubernetes.io/part-of" = "coder" + } + } + topology_key = "kubernetes.io/hostname" + } + }] +} \ No newline at end of file diff --git a/infra/1-click/1-setup/7-coder-init.tf b/infra/1-click/1-setup/7-coder-init.tf new file mode 100644 index 0000000..03be15e --- /dev/null +++ b/infra/1-click/1-setup/7-coder-init.tf @@ -0,0 +1,111 @@ +# Wait for DNS propagation. May require multiple requests + +data "http" "first-user" { + + depends_on = [module.coder-server] + + url = "http://${aws_eip.coder.0.public_ip}/api/v2/users/first" + method = "POST" + request_headers = { + Host = var.domain_name + Accept = "application/json" + } + request_body = jsonencode({ + email = var.coder_admin_email + username = var.coder_admin_username + password = var.coder_admin_password + trial = false + }) + retry { + attempts = 60 + 1 + max_delay_ms = 5000 + min_delay_ms = 5000 + } +} + +locals { + coder_svc_url = "http://${local.coder_release_name}.${local.coder_ns}.svc.cluster.local" +} + +module "coder-kube-logstream" { + + depends_on = [module.coder-server] + source = "../../../modules/k8s/bootstrap/coder-logstream" + + coder = { + access_url = local.coder_svc_url + ws_ns = [local.coder_ns] + } +} + +module "monitoring" { + + source = "../../../modules/k8s/bootstrap/monitoring" + + chart_version = "0.7.0-rc.1" + chart_timeout = 360 + cluster_name = "${var.name}-${local.normalized_domain_name}" + cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.coder.arn + + lb_class = "service.k8s.aws/nlb" + storage_class = kubernetes_manifest.automode-sc.manifest.metadata.name + + domain_name = "grafana.${var.domain_name}" + tolerations = concat([{ + key = "CriticalAddonsOnly" + operator = "Exists" + }], [for toleration in kubernetes_manifest.nodepool["karpenter"].manifest.spec.template.spec.taints : { + key = toleration.key + operator = "Equal" + value = toleration.value + effect = toleration.effect + }]) + + coder = { + db = { + host = data.aws_db_instance.coder.address + password = var.coder_password + username = var.coder_username + database = data.aws_db_instance.coder.db_name + } + selector = { + coderd = "pod=~`coder.*`, pod!~`.*provisioner.*`, namespace=~`(coder)`" + provisionerd = "pod=~`coder-provisioner.*`, namespace=~`(coder)`" + workspaces = "pod!~`coder.*`, namespace=~`(coder)`" + ctrl_plane_ns = "coder" + ext_prov_ns = "coder" + } + } + grafana = { + admin = { + username = var.grafana_admin_username + password = var.grafana_admin_password + } + db = { + host = data.aws_db_instance.grafana.address + password = var.grafana_password + username = var.grafana_username + database = data.aws_db_instance.grafana.db_name + } + svc = { + annots = { + "service.beta.kubernetes.io/aws-load-balancer-nlb-target-type" = "instance" + "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing" + "service.beta.kubernetes.io/aws-load-balancer-attributes" = "deletion_protection.enabled=false" + "service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol" = "tcp" + "service.beta.kubernetes.io/aws-load-balancer-healthcheck-path" = "/api/health" + "service.beta.kubernetes.io/aws-load-balancer-ssl-cert" = aws_acm_certificate.grafana.arn + "service.beta.kubernetes.io/aws-load-balancer-ssl-ports" = 443 + "service.beta.kubernetes.io/aws-load-balancer-eip-allocations" = join(",", aws_eip.grafana.*.allocation_id) + "service.beta.kubernetes.io/aws-load-balancer-subnets" = join(",", [local.pub_subs[0]]) + } + } + } + loki = { + s3 = { + chunks_bucket = data.aws_s3_bucket.loki.id + ruler_bucket = data.aws_s3_bucket.loki.id + region = data.aws_s3_bucket.loki.bucket_region + } + } +} \ No newline at end of file diff --git a/infra/1-click/1-setup/main.tf b/infra/1-click/1-setup/main.tf new file mode 100644 index 0000000..3e960e9 --- /dev/null +++ b/infra/1-click/1-setup/main.tf @@ -0,0 +1,67 @@ +## +# Remote State Resources +## + +provider "aws" { + region = var.region + profile = var.profile +} + +provider "helm" { + kubernetes = { + host = data.aws_eks_cluster.coder.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.coder.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.coder.token + } +} + +provider "kubernetes" { + host = data.aws_eks_cluster.coder.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.coder.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.coder.token +} + +data "aws_region" "this" {} + +locals { + apex_domain = join(".", slice(split(".", var.domain_name), length(split(".", var.domain_name)) - 2, length(split(".", var.domain_name)))) + normalized_domain_name = split(".", var.domain_name)[0] + formatted_name = "${var.name}-${local.normalized_domain_name}" +} + +data "aws_vpc" "this" { + tags = { + "Name" = local.formatted_name + } +} + +data "aws_s3_bucket" "loki" { + bucket = "${local.formatted_name}-grafana" +} + +data "aws_db_instance" "coder" { + db_instance_identifier = "${local.formatted_name}-coder" +} + +data "aws_db_instance" "grafana" { + db_instance_identifier = "${local.formatted_name}-grafana" +} + +data "aws_security_group" "coder" { + name = "${local.formatted_name}-pgsql" + vpc_id = data.aws_vpc.this.id +} + +data "aws_eks_cluster" "coder" { + name = local.formatted_name + region = var.region +} + +data "aws_eks_cluster_auth" "coder" { + name = local.formatted_name + region = var.region +} + +data "aws_iam_openid_connect_provider" "coder" { + url = data.aws_eks_cluster.coder.identity[0].oidc[0].issuer +} \ No newline at end of file diff --git a/infra/1-click/1-setup/terragrunt.hcl b/infra/1-click/1-setup/terragrunt.hcl new file mode 100644 index 0000000..e633e43 --- /dev/null +++ b/infra/1-click/1-setup/terragrunt.hcl @@ -0,0 +1,23 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = ["../0-infra"] +} + +inputs = { + # Optional. Set if using Terragrunt. + profile = include.root.locals.CODER_AWS_PROFILE + region = include.root.locals.CODER_AWS_REGION + domain_name = include.root.locals.CODER_DOMAIN_NAME + azs = jsondecode(include.root.locals.CODER_AWS_AZS) + coder_version = include.root.locals.CODER_VERSION + coder_username = include.root.locals.CODER_DB_USERNAME + coder_password = include.root.locals.CODER_DB_PASSWORD + coder_license = include.root.locals.CODER_LICENSE + coder_admin_email = include.root.locals.CODER_EMAIL + coder_admin_username = include.root.locals.CODER_USERNAME + coder_admin_password = include.root.locals.CODER_PASSWORD +} \ No newline at end of file diff --git a/infra/1-click/1-setup/variables.tf b/infra/1-click/1-setup/variables.tf new file mode 100644 index 0000000..0cac0e0 --- /dev/null +++ b/infra/1-click/1-setup/variables.tf @@ -0,0 +1,98 @@ + +## +# Global Inputs + Providers +## + +variable "region" { + description = "The AWS region of the deployment." + type = string + default = "us-east-2" +} + +variable "name" { + description = "Name for created resources and tag prefix." + type = string + default = "coder" +} + +variable "profile" { + type = string + default = "default" +} + +variable "domain_name" { + type = string +} + +## +# Coder K8s Inputs +## + +variable "coder_username" { + description = "Coder DB's username." + type = string + default = "coder" +} + +variable "coder_password" { + description = "Coder DB's password." + type = string + default = "th1s1sn0tas3cur3pass0wrd" + sensitive = true +} + +variable "coder_version" { + type = string + default = "2.30.0" +} + +variable "coder_license" { + type = string + default = "" + sensitive = true +} + +variable "coder_admin_email" { + type = string + default = "admin@coder.com" +} + +variable "coder_admin_username" { + type = string + default = "admin" +} + +variable "coder_admin_password" { + type = string + default = "Th1s1sN0TS3CuR3!!" + sensitive = true +} + +variable "grafana_username" { + description = "Grafana DB's username." + type = string + default = "grafana" +} + +variable "grafana_password" { + description = "Grafana DB's password." + type = string + default = "th1s1sn0tas3cur3pass0wrd" + sensitive = true +} + +variable "grafana_admin_username" { + type = string + default = "admin" +} + +variable "grafana_admin_password" { + type = string + default = "Th1s1sN0TS3CuR3!!" + sensitive = true +} + +variable "azs" { + type = list(string) + default = ["a", "b", "c"] +} \ No newline at end of file diff --git a/infra/1-click/2-clean.sh b/infra/1-click/2-clean.sh new file mode 100755 index 0000000..73c1463 --- /dev/null +++ b/infra/1-click/2-clean.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -ae -o pipefail + +source coder.env + +terragrunt run --all --non-interactive --config root.hcl destroy \ No newline at end of file diff --git a/infra/1-click/2-coder/8-backend.tf b/infra/1-click/2-coder/8-backend.tf new file mode 100644 index 0000000..a23d278 --- /dev/null +++ b/infra/1-click/2-coder/8-backend.tf @@ -0,0 +1,150 @@ +## +# Setup main provisioner + proxy if license is added. +## + +## +# External Provisioner Setup +## + +resource "coderd_license" "enterprise" { + count = var.coder_license != "" ? 1 : 0 + license = var.coder_license +} + +locals { + coder_ns = "coder" + coder_release_name = "coder" + coder_svc_url = "http://${local.coder_release_name}.${local.coder_ns}.svc.cluster.local" +} + +module "coder-ext-prov" { + + count = length(coderd_license.enterprise) + depends_on = [ coderd_license.enterprise ] + source = "../../../modules/k8s/bootstrap/coder-provisioner" + + release_name = "coder-provisioner" + namespace = "coder-provisioner" + chart_name = "coder-provisioner" + chart_version = var.coder_version + + cluster_name = "${local.formatted_name}" + cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.coder.arn + + coder = { + access_url = local.coder_svc_url + ws_ns = [local.coder_ns] + image_tag = "v${var.coder_version}" + rep_cnt = 5 + } +} + +## +# Proxy Setup +## + +locals { + apex_domain = join(".", slice(split(".", var.domain_name), length(split(".", var.domain_name)) - 2, length(split(".", var.domain_name)))) + pub_subs = [for az in var.azs : "${local.formatted_name}-public-${data.aws_region.this.region}${az}"] + proxy_access_url = "proxy.${var.domain_name}" + proxy_wildcard_url = "*.proxy.${var.domain_name}" +} + +## +# Fetch AWS Hosted Zone +## + +data "aws_route53_zone" "coder" { + name = local.apex_domain +} + +resource "aws_eip" "proxy" { + count = 1 + domain = "vpc" + public_ipv4_pool = "amazon" + tags = { + Name = "${local.formatted_name}-proxy-${count.index}" + } +} + +resource "aws_route53_record" "proxy-primary" { + name = local.proxy_access_url + records = aws_eip.proxy.*.public_ip + ttl = 60 + type = "A" + zone_id = data.aws_route53_zone.coder.zone_id +} + +resource "aws_route53_record" "proxy-wildcard" { + name = local.proxy_wildcard_url + records = aws_eip.proxy.*.public_ip + ttl = 60 + type = "A" + zone_id = data.aws_route53_zone.coder.zone_id +} + +resource "aws_acm_certificate" "proxy" { + domain_name = local.proxy_access_url + subject_alternative_names = [local.proxy_wildcard_url] + validation_method = "DNS" +} + +resource "aws_route53_record" "proxy_validation" { + + for_each = { + for rec in aws_acm_certificate.proxy.domain_validation_options : rec.domain_name => { + name = rec.resource_record_name + record = rec.resource_record_value + type = rec.resource_record_type + } + } + + allow_overwrite = true + name = each.value.name + records = [each.value.record] + ttl = 60 + type = each.value.type + zone_id = data.aws_route53_zone.coder.zone_id +} + +resource "aws_acm_certificate_validation" "proxy" { + certificate_arn = aws_acm_certificate.proxy.arn + validation_record_fqdns = [for record in aws_route53_record.proxy_validation : record.fqdn] + + timeouts { + create = "30m" + } +} + +# module "coder-proxy" { + +# count = length(coderd_license.enterprise) +# depends_on = [ coderd_license.enterprise, module.coder-ext-prov ] +# source = "../../../modules/k8s/bootstrap/coder-proxy" + +# release_name = "coder-proxy" +# namespace = "coder-proxy" +# chart_name = "coder" +# chart_version = var.coder_version + +# proxy = { +# name = "proxy" +# display_name = "Coder Proxy" +# access_url = "https://${local.proxy_access_url}" +# icon = "/emojis/1f4a1.png" # 💡 +# wildcard_url = local.proxy_wildcard_url +# coder_access_url = "https://${var.domain_name}" +# image_tag = "v${var.coder_version}" +# rep_cnt = 1 +# } + +# svc_annot = { +# "service.beta.kubernetes.io/aws-load-balancer-nlb-target-type" = "instance" +# "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing" +# "service.beta.kubernetes.io/aws-load-balancer-attributes" = "deletion_protection.enabled=false" +# "service.beta.kubernetes.io/aws-load-balancer-eip-allocations" = join(",", aws_eip.proxy.*.allocation_id) +# "service.beta.kubernetes.io/aws-load-balancer-subnets" = join(",", [local.pub_subs[0]]) +# "service.beta.kubernetes.io/aws-load-balancer-ssl-cert" = aws_acm_certificate.proxy.arn +# "service.beta.kubernetes.io/aws-load-balancer-ssl-ports" = 443 +# } +# } \ No newline at end of file diff --git a/infra/1-click/2-coder/9-templates.tf b/infra/1-click/2-coder/9-templates.tf new file mode 100644 index 0000000..77a81e9 --- /dev/null +++ b/infra/1-click/2-coder/9-templates.tf @@ -0,0 +1,222 @@ +## +# Coder Template Push +## + +data "coderd_organization" "default" { + is_default = true +} + +## +# Standalone Kubernetes Template +## + +data "archive_file" "k8s-default" { + type = "zip" + excludes = ["${path.module}/templates/kubernetes/.terraform"] + source_dir = "${path.module}/templates/kubernetes" + output_path = "${path.module}/templates/kubernetes.zip" +} + +resource "time_static" "k8s-default" { + triggers = { + run_on_ip_changes = data.aws_eip.coder.private_ip + run_on_checksum = "${data.archive_file.k8s-default.id}" + } +} + +resource "coderd_template" "k8s-default" { + + depends_on = [module.coder-ext-prov] + + name = "kubernetes" + organization_id = data.coderd_organization.default.id + display_name = "Kubernetes (Deployment)" + description = "Provision Kubernetes Deployments as Coder workspaces" + icon = "https://${var.domain_name}/icon/k8s.png" + versions = [ + { + name = "stable-${formatdate("YYYY-MM-DD_hh-mm-ss", time_static.k8s-default.rfc3339)}" + description = "The stable version of the template." + directory = "${path.module}/templates/kubernetes" + active = true + tf_vars = [{ + name = "namespace" + value = "coder" + }, { + name = "use_kubeconfig" + value = tostring(false) + }, { + name = "host_ip" + value = data.aws_eip.coder.private_ip + }] + } + ] +} + +## +# Claude-Code Kubernetes Template +## + +## +# NOTICE: This template requires a Premium license as it includes the following: +# - AI Bridge +# +# NOTE: AI Boundary's will not work in EKS Auto Mode. +## + +data "archive_file" "k8s-claude" { + + count = var.coder_license != "" && length(module.coder-ext-prov) > 0 ? 1 : 0 + + type = "zip" + excludes = ["${path.module}/templates/kubernetes-claude/.terraform"] + source_dir = "${path.module}/templates/kubernetes-claude" + output_path = "${path.module}/templates/kubernetes-claude.zip" +} + +resource "time_static" "k8s-claude" { + + count = var.coder_license != "" && length(module.coder-ext-prov) > 0 ? 1 : 0 + + triggers = { + run_on_ip_changes = data.aws_eip.coder.private_ip + run_on_checksum = "${data.archive_file.k8s-claude[0].id}" + } +} + +resource "coderd_template" "k8s-claude" { + + count = var.coder_license != "" && length(module.coder-ext-prov) > 0 ? 1 : 0 + + name = "kubernetes-claude" + organization_id = data.coderd_organization.default.id + display_name = "Claude Code on Coder" + description = "Provision a Kubernetes Deployments with Claude installed." + icon = "https://${var.domain_name}/icon/claude.svg" + versions = [ + { + name = "stable-${formatdate("YYYY-MM-DD_hh-mm-ss", time_static.k8s-claude[0].rfc3339)}" + description = "The stable version of the template." + directory = "${path.module}/templates/kubernetes-claude" + active = true + tf_vars = [{ + name = "namespace" + value = "coder" + }, { + name = "use_kubeconfig" + value = tostring(false) + }, { + name = "host_ip" + value = data.aws_eip.coder.private_ip + }] + } + ] +} + +## +# AWS EC2 Linux +## + +data "archive_file" "aws-linux" { + type = "zip" + excludes = ["${path.module}/templates/aws-linux/.terraform"] + source_dir = "${path.module}/templates/aws-linux" + output_path = "${path.module}/templates/aws-linux.zip" +} + +resource "time_static" "aws-linux" { + triggers = { + run_on_checksum = "${data.archive_file.aws-linux.id}" + } +} + +resource "coderd_template" "aws-linux" { + + depends_on = [module.coder-ext-prov] + + name = "aws-linux" + organization_id = data.coderd_organization.default.id + display_name = "AWS EC2 (Linux)" + description = "Provision an AWS EC2 Linux VM." + icon = "https://${var.domain_name}/icon/aws.svg" + versions = [ + { + name = "stable-${formatdate("YYYY-MM-DD_hh-mm-ss", time_static.aws-linux.rfc3339)}" + description = "The stable version of the template." + directory = "${path.module}/templates/aws-linux" + active = true + } + ] +} + +## +# AWS EC2 Windows +## + +data "archive_file" "aws-windows" { + type = "zip" + excludes = ["${path.module}/templates/aws-windows/.terraform"] + source_dir = "${path.module}/templates/aws-windows" + output_path = "${path.module}/templates/aws-windows.zip" +} + +resource "time_static" "aws-windows" { + triggers = { + run_on_checksum = "${data.archive_file.aws-windows.id}" + } +} + +resource "coderd_template" "aws-windows" { + + depends_on = [module.coder-ext-prov] + + name = "aws-windows" + organization_id = data.coderd_organization.default.id + display_name = "AWS EC2 (Windows)" + description = "Provision an AWS EC2 Windows VM." + icon = "https://${var.domain_name}/icon/windows.svg" + versions = [ + { + name = "stable-${formatdate("YYYY-MM-DD_hh-mm-ss", time_static.aws-windows.rfc3339)}" + description = "The stable version of the template." + directory = "${path.module}/templates/aws-windows" + active = true + } + ] +} + +## +# AWS EC2 DevContainer +## + +data "archive_file" "aws-devcontainer" { + type = "zip" + excludes = ["${path.module}/templates/aws-devcontainer/.terraform"] + source_dir = "${path.module}/templates/aws-devcontainer" + output_path = "${path.module}/templates/aws-devcontainer.zip" +} + +resource "time_static" "aws-devcontainer" { + triggers = { + run_on_checksum = "${data.archive_file.aws-devcontainer.id}" + } +} + +resource "coderd_template" "aws-devcontainer" { + + depends_on = [module.coder-ext-prov] + + name = "aws-devcontainer" + organization_id = data.coderd_organization.default.id + display_name = "AWS EC2 (Linux with Devcontainer)" + description = "Provision an AWS EC2 Linux VM running DevContainers." + icon = "https://${var.domain_name}/icon/aws.svg" + versions = [ + { + name = "stable-${formatdate("YYYY-MM-DD_hh-mm-ss", time_static.aws-devcontainer.rfc3339)}" + description = "The stable version of the template." + directory = "${path.module}/templates/aws-devcontainer" + active = true + } + ] +} \ No newline at end of file diff --git a/infra/1-click/2-coder/main.tf b/infra/1-click/2-coder/main.tf new file mode 100644 index 0000000..be172b6 --- /dev/null +++ b/infra/1-click/2-coder/main.tf @@ -0,0 +1,72 @@ +## +# Global Inputs + Providers +## + +locals { + formatted_name = "${var.name}-${local.normalized_domain_name}" + normalized_domain_name = split(".", var.domain_name)[0] +} + +provider "aws" { + region = var.region + profile = var.profile +} + +data "aws_region" "this" {} + +data "aws_eks_cluster" "coder" { + name = local.formatted_name + region = var.region +} + +data "aws_eks_cluster_auth" "coder" { + name = local.formatted_name + region = var.region +} + +data "aws_iam_openid_connect_provider" "coder" { + url = data.aws_eks_cluster.coder.identity[0].oidc[0].issuer +} + +## +# Coder MUST be in a reachable state by now +## + +data "aws_eip" "coder" { + region = var.region + tags = { + Name = "${local.formatted_name}-coder-0" + } +} + +data "http" "login" { + url = "http://${data.aws_eip.coder.public_ip}/api/v2/users/login" + method = "POST" + request_headers = { + Host = var.domain_name + Accept = "application/json" + } + request_body = jsonencode({ + email = var.coder_admin_email + password = var.coder_admin_password + }) +} + +provider "helm" { + kubernetes = { + host = data.aws_eks_cluster.coder.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.coder.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.coder.token + } +} + +provider "kubernetes" { + host = data.aws_eks_cluster.coder.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.coder.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.coder.token +} + +provider "coderd" { + url = "http://${data.aws_eip.coder.public_ip}" + token = jsondecode(data.http.login.response_body).session_token +} \ No newline at end of file diff --git a/infra/1-click/2-coder/templates/aws-devcontainer/README.md b/infra/1-click/2-coder/templates/aws-devcontainer/README.md new file mode 100644 index 0000000..3b58d73 --- /dev/null +++ b/infra/1-click/2-coder/templates/aws-devcontainer/README.md @@ -0,0 +1,111 @@ +--- +display_name: AWS EC2 (Devcontainer) +description: Provision AWS EC2 VMs with a devcontainer as Coder workspaces +icon: ../../../site/static/icon/aws.svg +maintainer_github: coder +verified: true +tags: [vm, linux, aws, persistent, devcontainer] +--- + +# Remote Development on AWS EC2 VMs using a Devcontainer + +Provision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs) with this example template. +![Architecture Diagram](./architecture.svg) + + + +## Prerequisites + +### Authentication + +By default, this template authenticates to AWS using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration). + +The simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`. + +To use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template. + +## Required permissions / policy + +The following sample policy allows Coder to create EC2 instances and modify +instances provisioned by Coder: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "ec2:GetDefaultCreditSpecification", + "ec2:DescribeIamInstanceProfileAssociations", + "ec2:DescribeTags", + "ec2:DescribeInstances", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceStatus", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:DescribeInstanceCreditSpecifications", + "ec2:DescribeImages", + "ec2:ModifyDefaultCreditSpecification", + "ec2:DescribeVolumes" + ], + "Resource": "*" + }, + { + "Sid": "CoderResources", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstanceAttribute", + "ec2:UnmonitorInstances", + "ec2:TerminateInstances", + "ec2:StartInstances", + "ec2:StopInstances", + "ec2:DeleteTags", + "ec2:MonitorInstances", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:ModifyInstanceAttribute", + "ec2:ModifyInstanceCreditSpecification" + ], + "Resource": "arn:aws:ec2:*:*:instance/*", + "Condition": { + "StringEquals": { + "aws:ResourceTag/Coder_Provisioned": "true" + } + } + } + ] +} +``` + +## Architecture + +This template provisions the following resources: + +- AWS Instance + +Coder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance. + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +## Caching + +To speed up your builds, you can use a container registry as a cache. +When creating the template, set the parameter `cache_repo` to a valid Docker repository in the form `host.tld/path/to/repo`. + +See the [Envbuilder Terraform Provider Examples](https://github.com/coder/terraform-provider-envbuilder/blob/main/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf/) for a more complete example of how the provider works. + +> [!NOTE] +> We recommend using a registry cache with authentication enabled. +> To allow Envbuilder to authenticate with a registry cache hosted on ECR, specify an IAM instance +> profile that has read and write access to the given registry. For more information, see the +> [AWS documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html). +> +> Alternatively, you can specify the variable `cache_repo_docker_config_path` +> with the path to a Docker config `.json` on disk containing valid credentials for the registry. + +## code-server + +`code-server` is installed via the [`code-server`](https://registry.coder.com/modules/code-server) registry module. For a list of all modules and templates pplease check [Coder Registry](https://registry.coder.com). \ No newline at end of file diff --git a/infra/1-click/2-coder/templates/aws-devcontainer/architecture.svg b/infra/1-click/2-coder/templates/aws-devcontainer/architecture.svg new file mode 100644 index 0000000..737db74 --- /dev/null +++ b/infra/1-click/2-coder/templates/aws-devcontainer/architecture.svg @@ -0,0 +1,8 @@ +AWSAWSHostingHostingVirtual MachineVirtual MachineLinux HardwareLinux HardwareCoder WorkspaceCoder WorkspaceDevcontainerDevcontainerenvbuilder created filesytemenvbuilder created filesytemA Clone of your repoA Clone of your repoSource codeSource codeLanguagesLanguagesPython. Go, etcPython. Go, etcToolingToolingExtensions, linting, formatting, etcExtensions, linting, formatting, etcCPUsCPUsDisk StorageDisk StorageCode EditorCode EditorVS Code DesktopVS Code DesktopLocal InstallationLocal InstallationVS Code DesktopVS Code DesktopLocal InstallationLocal Installationcode-servercode-serverA web IDEA web IDEJetBrains GatewayJetBrains GatewayLocal InstallationLocal InstallationCommand LineCommand LineSSH via Coder CLISSH via Coder CLI \ No newline at end of file diff --git a/infra/1-click/2-coder/templates/aws-devcontainer/cloud-init/cloud-config.yaml.tftpl b/infra/1-click/2-coder/templates/aws-devcontainer/cloud-init/cloud-config.yaml.tftpl new file mode 100644 index 0000000..3aaefb4 --- /dev/null +++ b/infra/1-click/2-coder/templates/aws-devcontainer/cloud-init/cloud-config.yaml.tftpl @@ -0,0 +1,15 @@ +#cloud-config +cloud_final_modules: + - [scripts-user, always] +hostname: ${hostname} +users: + - name: ${linux_user} + sudo: ALL=(ALL) NOPASSWD:ALL + shell: /bin/bash + ssh_authorized_keys: + - "${ssh_pubkey}" +# Automatically grow the partition +growpart: + mode: auto + devices: ['/'] + ignore_growroot_disabled: false \ No newline at end of file diff --git a/infra/1-click/2-coder/templates/aws-devcontainer/cloud-init/userdata.sh.tftpl b/infra/1-click/2-coder/templates/aws-devcontainer/cloud-init/userdata.sh.tftpl new file mode 100644 index 0000000..9fb8117 --- /dev/null +++ b/infra/1-click/2-coder/templates/aws-devcontainer/cloud-init/userdata.sh.tftpl @@ -0,0 +1,37 @@ +#!/bin/bash +# Install Docker +if ! command -v docker &> /dev/null +then + echo "Docker not found, installing..." + curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh 2>&1 >/dev/null + usermod -aG docker ${linux_user} + newgrp docker +else + echo "Docker is already installed." +fi + +# Set up Docker credentials +mkdir -p "/home/${linux_user}/.docker" + +if [ -n "${docker_config_json_base64}" ]; then + # Write the Docker config JSON to disk if it is provided. + printf "%s" "${docker_config_json_base64}" | base64 -d | tee "/home/${linux_user}/.docker/config.json" +else + # Assume that we're going to use the instance IAM role to pull from the cache repo if we need to. + # Set up the ecr credential helper. + apt-get update -y && apt-get install -y amazon-ecr-credential-helper + mkdir -p .docker + printf '{"credsStore": "ecr-login"}' | tee "/home/${linux_user}/.docker/config.json" +fi +chown -R ${linux_user}:${linux_user} "/home/${linux_user}/.docker" + +# Start envbuilder +sudo -u coder docker run \ + --rm \ + --net=host \ + -h ${hostname} \ + -v /home/${linux_user}/envbuilder:/workspaces \ + %{ for key, value in environment ~} + -e ${key}="${value}" \ + %{ endfor ~} + ${builder_image} \ No newline at end of file diff --git a/infra/1-click/2-coder/templates/aws-devcontainer/main.tf b/infra/1-click/2-coder/templates/aws-devcontainer/main.tf new file mode 100644 index 0000000..703c1b3 --- /dev/null +++ b/infra/1-click/2-coder/templates/aws-devcontainer/main.tf @@ -0,0 +1,331 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + aws = { + source = "hashicorp/aws" + } + cloudinit = { + source = "hashicorp/cloudinit" + } + envbuilder = { + source = "coder/envbuilder" + } + } +} + +module "aws_region" { + source = "https://registry.coder.com/modules/aws-region" + default = "us-east-1" +} + +provider "aws" { + region = module.aws_region.value +} + +variable "cache_repo" { + default = "" + description = "(Optional) Use a container registry as a cache to speed up builds. Example: host.tld/path/to/repo." + type = string +} + +variable "cache_repo_docker_config_path" { + default = "" + description = "(Optional) Path to a docker config.json containing credentials to the provided cache repo, if required. This will depend on your Coder setup. Example: `/home/coder/.docker/config.json`." + sensitive = true + type = string +} + +variable "iam_instance_profile" { + default = "" + description = "(Optional) Name of an IAM instance profile to assign to the instance." + type = string +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +data "aws_ami" "ubuntu" { + most_recent = true + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"] + } + filter { + name = "virtualization-type" + values = ["hvm"] + } + owners = ["099720109477"] # Canonical +} + +data "coder_parameter" "instance_type" { + name = "instance_type" + display_name = "Instance type" + description = "What instance type should your workspace use?" + default = "t3.micro" + mutable = false + option { + name = "2 vCPU, 1 GiB RAM" + value = "t3.micro" + } + option { + name = "2 vCPU, 2 GiB RAM" + value = "t3.small" + } + option { + name = "2 vCPU, 4 GiB RAM" + value = "t3.medium" + } + option { + name = "2 vCPU, 8 GiB RAM" + value = "t3.large" + } + option { + name = "4 vCPU, 16 GiB RAM" + value = "t3.xlarge" + } + option { + name = "8 vCPU, 32 GiB RAM" + value = "t3.2xlarge" + } +} + +data "coder_parameter" "root_volume_size_gb" { + name = "root_volume_size_gb" + display_name = "Root Volume Size (GB)" + description = "How large should the root volume for the instance be?" + default = 30 + type = "number" + mutable = true + validation { + min = 1 + monotonic = "increasing" + } +} + +data "coder_parameter" "fallback_image" { + default = "codercom/enterprise-base:ubuntu" + description = "This image runs if the devcontainer fails to build." + display_name = "Fallback Image" + mutable = true + name = "fallback_image" + order = 3 +} + +data "coder_parameter" "devcontainer_builder" { + description = <<-EOF +Image that will build the devcontainer. +Find the latest version of Envbuilder here: https://ghcr.io/coder/envbuilder +Be aware that using the `:latest` tag may expose you to breaking changes. +EOF + display_name = "Devcontainer Builder" + mutable = true + name = "devcontainer_builder" + default = "ghcr.io/coder/envbuilder:latest" + order = 4 +} + +data "coder_parameter" "repo_url" { + name = "repo_url" + display_name = "Repository URL" + default = "https://github.com/coder/envbuilder-starter-devcontainer" + description = "Repository URL" + mutable = true +} + +data "coder_parameter" "ssh_pubkey" { + name = "ssh_pubkey" + display_name = "SSH Public Key" + default = "" + description = "(Optional) Add an SSH public key to the `coder` user's authorized_keys. Useful for troubleshooting. You may need to add a security group to the instance." + mutable = false +} + +data "local_sensitive_file" "cache_repo_dockerconfigjson" { + count = var.cache_repo_docker_config_path == "" ? 0 : 1 + filename = var.cache_repo_docker_config_path +} + +data "aws_iam_instance_profile" "vm_instance_profile" { + count = var.iam_instance_profile == "" ? 0 : 1 + name = var.iam_instance_profile +} + +# Be careful when modifying the below locals! +locals { + # TODO: provide a way to pick the availability zone. + aws_availability_zone = "${module.aws_region.value}a" + + hostname = lower(data.coder_workspace.me.name) + linux_user = "coder" + + # The devcontainer builder image is the image that will build the devcontainer. + devcontainer_builder_image = data.coder_parameter.devcontainer_builder.value + + # We may need to authenticate with a registry. If so, the user will provide a path to a docker config.json. + docker_config_json_base64 = try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, "") + + # The envbuilder provider requires a key-value map of environment variables. Build this here. + envbuilder_env = { + # ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider + # if the cache repo is enabled. + "ENVBUILDER_GIT_URL" : data.coder_parameter.repo_url.value, + # The agent token is required for the agent to connect to the Coder platform. + "CODER_AGENT_TOKEN" : try(coder_agent.dev.0.token, ""), + # The agent URL is required for the agent to connect to the Coder platform. + "CODER_AGENT_URL" : data.coder_workspace.me.access_url, + # The agent init script is required for the agent to start up. We base64 encode it here + # to avoid quoting issues. + "ENVBUILDER_INIT_SCRIPT" : "echo ${base64encode(try(coder_agent.dev[0].init_script, ""))} | base64 -d | sh", + "ENVBUILDER_DOCKER_CONFIG_BASE64" : local.docker_config_json_base64, + # The fallback image is the image that will run if the devcontainer fails to build. + "ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value, + # The following are used to push the image to the cache repo, if defined. + "ENVBUILDER_CACHE_REPO" : var.cache_repo, + "ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true", + # You can add other required environment variables here. + # See: https://github.com/coder/envbuilder/?tab=readme-ov-file#environment-variables + } +} + +# Check for the presence of a prebuilt image in the cache repo +# that we can use instead. +resource "envbuilder_cached_image" "cached" { + count = var.cache_repo == "" ? 0 : data.coder_workspace.me.start_count + builder_image = local.devcontainer_builder_image + git_url = data.coder_parameter.repo_url.value + cache_repo = var.cache_repo + extra_env = local.envbuilder_env +} + +data "cloudinit_config" "user_data" { + gzip = false + base64_encode = false + + boundary = "//" + + part { + filename = "cloud-config.yaml" + content_type = "text/cloud-config" + + content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", { + hostname = local.hostname + linux_user = local.linux_user + + ssh_pubkey = data.coder_parameter.ssh_pubkey.value + }) + } + + part { + filename = "userdata.sh" + content_type = "text/x-shellscript" + + content = templatefile("${path.module}/cloud-init/userdata.sh.tftpl", { + hostname = local.hostname + linux_user = local.linux_user + + # If we have a cached image, use the cached image's environment variables. + # Otherwise, just use the environment variables we've defined in locals. + environment = try(envbuilder_cached_image.cached[0].env_map, local.envbuilder_env) + + # Builder image will either be the builder image parameter, or the cached image, if cache is provided. + builder_image = try(envbuilder_cached_image.cached[0].image, data.coder_parameter.devcontainer_builder.value) + + docker_config_json_base64 = local.docker_config_json_base64 + }) + } +} + +# This is useful for debugging the startup script. Left here for reference. +# resource local_file "startup_script" { +# content = data.cloudinit_config.user_data.rendered +# filename = "${path.module}/user_data.txt" +# } + +resource "aws_instance" "vm" { + ami = data.aws_ami.ubuntu.id + availability_zone = local.aws_availability_zone + instance_type = data.coder_parameter.instance_type.value + iam_instance_profile = try(data.aws_iam_instance_profile.vm_instance_profile[0].name, null) + root_block_device { + volume_size = data.coder_parameter.root_volume_size_gb.value + } + + user_data = data.cloudinit_config.user_data.rendered + tags = { + Name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" + # Required if you are using our example policy, see template README + Coder_Provisioned = "true" + } + lifecycle { + ignore_changes = [ami] + } +} + +resource "aws_ec2_instance_state" "vm" { + instance_id = aws_instance.vm.id + state = data.coder_workspace.me.transition == "start" ? "running" : "stopped" +} + +resource "coder_agent" "dev" { + count = data.coder_workspace.me.start_count + arch = "amd64" + auth = "token" + os = "linux" + dir = "/workspaces/${trimsuffix(basename(data.coder_parameter.repo_url.value), ".git")}" + connection_timeout = 0 + + metadata { + key = "cpu" + display_name = "CPU Usage" + interval = 5 + timeout = 5 + script = "coder stat cpu" + } + metadata { + key = "memory" + display_name = "Memory Usage" + interval = 5 + timeout = 5 + script = "coder stat mem" + } +} + +resource "coder_metadata" "info" { + count = data.coder_workspace.me.start_count + resource_id = coder_agent.dev[0].id + item { + key = "ami" + value = aws_instance.vm.ami + } + item { + key = "availability_zone" + value = local.aws_availability_zone + } + item { + key = "instance_type" + value = data.coder_parameter.instance_type.value + } + item { + key = "ssh_pubkey" + value = data.coder_parameter.ssh_pubkey.value + } + item { + key = "repo_url" + value = data.coder_parameter.repo_url.value + } + item { + key = "devcontainer_builder" + value = data.coder_parameter.devcontainer_builder.value + } +} + +# See https://registry.coder.com/modules/coder/code-server +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + agent_id = coder_agent.dev[0].id +} \ No newline at end of file diff --git a/infra/1-click/2-coder/templates/aws-linux/README.md b/infra/1-click/2-coder/templates/aws-linux/README.md new file mode 100644 index 0000000..3575640 --- /dev/null +++ b/infra/1-click/2-coder/templates/aws-linux/README.md @@ -0,0 +1,94 @@ +--- +display_name: AWS EC2 (Linux) +description: Provision AWS EC2 VMs as Coder workspaces +icon: ../../../site/static/icon/aws.svg +maintainer_github: coder +verified: true +tags: [vm, linux, aws, persistent-vm] +--- + +# Remote Development on AWS EC2 VMs (Linux) + +Provision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template. + +## Prerequisites + +### Authentication + +By default, this template authenticates to AWS using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration). + +The simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`. + +To use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template. + +## Required permissions / policy + +The following sample policy allows Coder to create EC2 instances and modify +instances provisioned by Coder: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "ec2:GetDefaultCreditSpecification", + "ec2:DescribeIamInstanceProfileAssociations", + "ec2:DescribeTags", + "ec2:DescribeInstances", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceStatus", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:DescribeInstanceCreditSpecifications", + "ec2:DescribeImages", + "ec2:ModifyDefaultCreditSpecification", + "ec2:DescribeVolumes" + ], + "Resource": "*" + }, + { + "Sid": "CoderResources", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstanceAttribute", + "ec2:UnmonitorInstances", + "ec2:TerminateInstances", + "ec2:StartInstances", + "ec2:StopInstances", + "ec2:DeleteTags", + "ec2:MonitorInstances", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:ModifyInstanceAttribute", + "ec2:ModifyInstanceCreditSpecification" + ], + "Resource": "arn:aws:ec2:*:*:instance/*", + "Condition": { + "StringEquals": { + "aws:ResourceTag/Coder_Provisioned": "true" + } + } + } + ] +} +``` + +## Architecture + +This template provisions the following resources: + +- AWS Instance + +Coder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance. + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +## code-server + +`code-server` is installed via the `startup_script` argument in the `coder_agent` +resource block. The `coder_app` resource is defined to access `code-server` through +the dashboard UI over `localhost:13337`. \ No newline at end of file diff --git a/infra/1-click/2-coder/templates/aws-linux/cloud-init/cloud-config.yaml.tftpl b/infra/1-click/2-coder/templates/aws-linux/cloud-init/cloud-config.yaml.tftpl new file mode 100644 index 0000000..b0ae9b2 --- /dev/null +++ b/infra/1-click/2-coder/templates/aws-linux/cloud-init/cloud-config.yaml.tftpl @@ -0,0 +1,8 @@ +#cloud-config +cloud_final_modules: + - [scripts-user, always] +hostname: ${hostname} +users: + - name: ${linux_user} + sudo: ALL=(ALL) NOPASSWD:ALL + shell: /bin/bash \ No newline at end of file diff --git a/infra/1-click/2-coder/templates/aws-linux/cloud-init/userdata.sh.tftpl b/infra/1-click/2-coder/templates/aws-linux/cloud-init/userdata.sh.tftpl new file mode 100644 index 0000000..0045120 --- /dev/null +++ b/infra/1-click/2-coder/templates/aws-linux/cloud-init/userdata.sh.tftpl @@ -0,0 +1,2 @@ +#!/bin/bash +sudo -u '${linux_user}' sh -c '${init_script}' \ No newline at end of file diff --git a/infra/1-click/2-coder/templates/aws-linux/main.tf b/infra/1-click/2-coder/templates/aws-linux/main.tf new file mode 100644 index 0000000..ba22558 --- /dev/null +++ b/infra/1-click/2-coder/templates/aws-linux/main.tf @@ -0,0 +1,286 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + cloudinit = { + source = "hashicorp/cloudinit" + } + aws = { + source = "hashicorp/aws" + } + } +} + +# Last updated 2023-03-14 +# aws ec2 describe-regions | jq -r '[.Regions[].RegionName] | sort' +data "coder_parameter" "region" { + name = "region" + display_name = "Region" + description = "The region to deploy the workspace in." + default = "us-east-1" + mutable = false + option { + name = "Asia Pacific (Tokyo)" + value = "ap-northeast-1" + icon = "/emojis/1f1ef-1f1f5.png" + } + option { + name = "Asia Pacific (Seoul)" + value = "ap-northeast-2" + icon = "/emojis/1f1f0-1f1f7.png" + } + option { + name = "Asia Pacific (Osaka)" + value = "ap-northeast-3" + icon = "/emojis/1f1ef-1f1f5.png" + } + option { + name = "Asia Pacific (Mumbai)" + value = "ap-south-1" + icon = "/emojis/1f1ee-1f1f3.png" + } + option { + name = "Asia Pacific (Singapore)" + value = "ap-southeast-1" + icon = "/emojis/1f1f8-1f1ec.png" + } + option { + name = "Asia Pacific (Sydney)" + value = "ap-southeast-2" + icon = "/emojis/1f1e6-1f1fa.png" + } + option { + name = "Canada (Central)" + value = "ca-central-1" + icon = "/emojis/1f1e8-1f1e6.png" + } + option { + name = "EU (Frankfurt)" + value = "eu-central-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (Stockholm)" + value = "eu-north-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (Ireland)" + value = "eu-west-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (London)" + value = "eu-west-2" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (Paris)" + value = "eu-west-3" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "South America (São Paulo)" + value = "sa-east-1" + icon = "/emojis/1f1e7-1f1f7.png" + } + option { + name = "US East (N. Virginia)" + value = "us-east-1" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "US East (Ohio)" + value = "us-east-2" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "US West (N. California)" + value = "us-west-1" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "US West (Oregon)" + value = "us-west-2" + icon = "/emojis/1f1fa-1f1f8.png" + } +} + +data "coder_parameter" "instance_type" { + name = "instance_type" + display_name = "Instance type" + description = "What instance type should your workspace use?" + default = "t3.micro" + mutable = false + option { + name = "2 vCPU, 1 GiB RAM" + value = "t3.micro" + } + option { + name = "2 vCPU, 2 GiB RAM" + value = "t3.small" + } + option { + name = "2 vCPU, 4 GiB RAM" + value = "t3.medium" + } + option { + name = "2 vCPU, 8 GiB RAM" + value = "t3.large" + } + option { + name = "4 vCPU, 16 GiB RAM" + value = "t3.xlarge" + } + option { + name = "8 vCPU, 32 GiB RAM" + value = "t3.2xlarge" + } +} + +provider "aws" { + region = data.coder_parameter.region.value +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +data "aws_ami" "ubuntu" { + most_recent = true + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] + } + filter { + name = "virtualization-type" + values = ["hvm"] + } + owners = ["099720109477"] # Canonical +} + +resource "coder_agent" "dev" { + count = data.coder_workspace.me.start_count + arch = "amd64" + auth = "aws-instance-identity" + os = "linux" + startup_script = <<-EOT + set -e + + # Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here + EOT + + metadata { + key = "cpu" + display_name = "CPU Usage" + interval = 5 + timeout = 5 + script = "coder stat cpu" + } + metadata { + key = "memory" + display_name = "Memory Usage" + interval = 5 + timeout = 5 + script = "coder stat mem" + } + metadata { + key = "disk" + display_name = "Disk Usage" + interval = 600 # every 10 minutes + timeout = 30 # df can take a while on large filesystems + script = "coder stat disk --path $HOME" + } +} + +# See https://registry.coder.com/modules/coder/code-server +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/code-server/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + agent_id = coder_agent.dev[0].id + order = 1 +} + +# See https://registry.coder.com/modules/coder/jetbrains +module "jetbrains" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains/coder" + version = "~> 1.0" + agent_id = coder_agent.dev[0].id + agent_name = "dev" + folder = "/home/coder" +} + +locals { + hostname = lower(data.coder_workspace.me.name) + linux_user = "coder" +} + +data "cloudinit_config" "user_data" { + gzip = false + base64_encode = false + + boundary = "//" + + part { + filename = "cloud-config.yaml" + content_type = "text/cloud-config" + + content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", { + hostname = local.hostname + linux_user = local.linux_user + }) + } + + part { + filename = "userdata.sh" + content_type = "text/x-shellscript" + + content = templatefile("${path.module}/cloud-init/userdata.sh.tftpl", { + linux_user = local.linux_user + + init_script = try(coder_agent.dev[0].init_script, "") + }) + } +} + +resource "aws_instance" "dev" { + ami = data.aws_ami.ubuntu.id + availability_zone = "${data.coder_parameter.region.value}a" + instance_type = data.coder_parameter.instance_type.value + + user_data = data.cloudinit_config.user_data.rendered + tags = { + Name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}" + # Required if you are using our example policy, see template README + Coder_Provisioned = "true" + } + lifecycle { + ignore_changes = [ami] + } +} + +resource "coder_metadata" "workspace_info" { + resource_id = aws_instance.dev.id + item { + key = "region" + value = data.coder_parameter.region.value + } + item { + key = "instance type" + value = aws_instance.dev.instance_type + } + item { + key = "disk" + value = "${aws_instance.dev.root_block_device[0].volume_size} GiB" + } +} + +resource "aws_ec2_instance_state" "dev" { + instance_id = aws_instance.dev.id + state = data.coder_workspace.me.transition == "start" ? "running" : "stopped" +} \ No newline at end of file diff --git a/infra/1-click/2-coder/templates/aws-windows/README.md b/infra/1-click/2-coder/templates/aws-windows/README.md new file mode 100644 index 0000000..b5af8cb --- /dev/null +++ b/infra/1-click/2-coder/templates/aws-windows/README.md @@ -0,0 +1,96 @@ +--- +display_name: AWS EC2 (Windows) +description: Provision AWS EC2 VMs as Coder workspaces +icon: ../../../site/static/icon/aws.svg +maintainer_github: coder +verified: true +tags: [vm, windows, aws] +--- + +# Remote Development on AWS EC2 VMs (Windows) + +Provision AWS EC2 Windows VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template. + + + +## Prerequisites + +### Authentication + +By default, this template authenticates to AWS with using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration). + +The simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`. + +To use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template. + +## Required permissions / policy + +The following sample policy allows Coder to create EC2 instances and modify +instances provisioned by Coder: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "ec2:GetDefaultCreditSpecification", + "ec2:DescribeIamInstanceProfileAssociations", + "ec2:DescribeTags", + "ec2:DescribeInstances", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceStatus", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:DescribeInstanceCreditSpecifications", + "ec2:DescribeImages", + "ec2:ModifyDefaultCreditSpecification", + "ec2:DescribeVolumes" + ], + "Resource": "*" + }, + { + "Sid": "CoderResources", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstanceAttribute", + "ec2:UnmonitorInstances", + "ec2:TerminateInstances", + "ec2:StartInstances", + "ec2:StopInstances", + "ec2:DeleteTags", + "ec2:MonitorInstances", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:ModifyInstanceAttribute", + "ec2:ModifyInstanceCreditSpecification" + ], + "Resource": "arn:aws:ec2:*:*:instance/*", + "Condition": { + "StringEquals": { + "aws:ResourceTag/Coder_Provisioned": "true" + } + } + } + ] +} +``` + +## Architecture + +This template provisions the following resources: + +- AWS Instance + +Coder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance. + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +## code-server + +`code-server` is installed via the `startup_script` argument in the `coder_agent` +resource block. The `coder_app` resource is defined to access `code-server` through +the dashboard UI over `localhost:13337`. \ No newline at end of file diff --git a/infra/1-click/2-coder/templates/aws-windows/main.tf b/infra/1-click/2-coder/templates/aws-windows/main.tf new file mode 100644 index 0000000..d157b24 --- /dev/null +++ b/infra/1-click/2-coder/templates/aws-windows/main.tf @@ -0,0 +1,214 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + aws = { + source = "hashicorp/aws" + } + } +} + +# Last updated 2023-03-14 +# aws ec2 describe-regions | jq -r '[.Regions[].RegionName] | sort' +data "coder_parameter" "region" { + name = "region" + display_name = "Region" + description = "The region to deploy the workspace in." + default = "us-east-1" + mutable = false + option { + name = "Asia Pacific (Tokyo)" + value = "ap-northeast-1" + icon = "/emojis/1f1ef-1f1f5.png" + } + option { + name = "Asia Pacific (Seoul)" + value = "ap-northeast-2" + icon = "/emojis/1f1f0-1f1f7.png" + } + option { + name = "Asia Pacific (Osaka-Local)" + value = "ap-northeast-3" + icon = "/emojis/1f1f0-1f1f7.png" + } + option { + name = "Asia Pacific (Mumbai)" + value = "ap-south-1" + icon = "/emojis/1f1f0-1f1f7.png" + } + option { + name = "Asia Pacific (Singapore)" + value = "ap-southeast-1" + icon = "/emojis/1f1f0-1f1f7.png" + } + option { + name = "Asia Pacific (Sydney)" + value = "ap-southeast-2" + icon = "/emojis/1f1f0-1f1f7.png" + } + option { + name = "Canada (Central)" + value = "ca-central-1" + icon = "/emojis/1f1e8-1f1e6.png" + } + option { + name = "EU (Frankfurt)" + value = "eu-central-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (Stockholm)" + value = "eu-north-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (Ireland)" + value = "eu-west-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (London)" + value = "eu-west-2" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (Paris)" + value = "eu-west-3" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "South America (São Paulo)" + value = "sa-east-1" + icon = "/emojis/1f1e7-1f1f7.png" + } + option { + name = "US East (N. Virginia)" + value = "us-east-1" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "US East (Ohio)" + value = "us-east-2" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "US West (N. California)" + value = "us-west-1" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "US West (Oregon)" + value = "us-west-2" + icon = "/emojis/1f1fa-1f1f8.png" + } +} + +data "coder_parameter" "instance_type" { + name = "instance_type" + display_name = "Instance type" + description = "What instance type should your workspace use?" + default = "t3.micro" + mutable = false + option { + name = "2 vCPU, 1 GiB RAM" + value = "t3.micro" + } + option { + name = "2 vCPU, 2 GiB RAM" + value = "t3.small" + } + option { + name = "2 vCPU, 4 GiB RAM" + value = "t3.medium" + } + option { + name = "2 vCPU, 8 GiB RAM" + value = "t3.large" + } + option { + name = "4 vCPU, 16 GiB RAM" + value = "t3.xlarge" + } + option { + name = "8 vCPU, 32 GiB RAM" + value = "t3.2xlarge" + } +} + +provider "aws" { + region = data.coder_parameter.region.value +} + +data "coder_workspace" "me" { +} +data "coder_workspace_owner" "me" {} + +data "aws_ami" "windows" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["Windows_Server-2019-English-Full-Base-*"] + } +} + +resource "coder_agent" "main" { + arch = "amd64" + auth = "aws-instance-identity" + os = "windows" +} + +locals { + + # User data is used to stop/start AWS instances. See: + # https://github.com/hashicorp/terraform-provider-aws/issues/22 + + user_data_start = < +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +${coder_agent.main.init_script} + +true +EOT + + user_data_end = < +shutdown /s + +true +EOT +} + +resource "aws_instance" "dev" { + ami = data.aws_ami.windows.id + availability_zone = "${data.coder_parameter.region.value}a" + instance_type = data.coder_parameter.instance_type.value + + user_data = data.coder_workspace.me.transition == "start" ? local.user_data_start : local.user_data_end + tags = { + Name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}" + # Required if you are using our example policy, see template README + Coder_Provisioned = "true" + } + lifecycle { + ignore_changes = [ami] + } +} + +resource "coder_metadata" "workspace_info" { + resource_id = aws_instance.dev.id + item { + key = "region" + value = data.coder_parameter.region.value + } + item { + key = "instance type" + value = aws_instance.dev.instance_type + } + item { + key = "disk" + value = "${aws_instance.dev.root_block_device[0].volume_size} GiB" + } +} \ No newline at end of file diff --git a/infra/1-click/2-coder/templates/kubernetes-claude/README.md b/infra/1-click/2-coder/templates/kubernetes-claude/README.md new file mode 100644 index 0000000..e687f92 --- /dev/null +++ b/infra/1-click/2-coder/templates/kubernetes-claude/README.md @@ -0,0 +1,105 @@ +--- +display_name: Claude Code on Coder +description: Provision Kubernetes workspaces with Claude Code and code-server +icon: ../../../site/static/icon/claude.svg +maintainer_github: coder +verified: true +tags: [kubernetes, container, claude, ai] +--- + +# Kubernetes Workspaces with Claude Code + +Provision Kubernetes-based [Coder workspaces](https://coder.com/docs/workspaces) with Claude Code and code-server pre-configured. + +## Features + +- **Claude Code**: AI-powered coding assistant via the [claude-code module](https://registry.coder.com/modules/claude-code) +- **code-server**: Browser-based VS Code experience +- **Persistent Storage**: Home directory persists across workspace restarts +- **Resource Control**: Configurable CPU, memory, and disk allocation +- **Workspace Metrics**: Real-time CPU, memory, and disk usage displayed in the dashboard + +## Prerequisites + +### Infrastructure + +- **Kubernetes Cluster**: An existing Kubernetes cluster with sufficient resources +- **Namespace**: A pre-existing namespace for workspace deployments +- **Storage Class**: A default storage class for persistent volume claims + +### Authentication + +This template supports two authentication methods: + +1. **In-cluster authentication** (default): When Coder runs as a pod in the same cluster, it uses the ServiceAccount for authentication. Set `use_kubeconfig = false`. + +2. **Kubeconfig authentication**: When Coder runs outside the cluster, provide a valid `~/.kube/config` on the Coder host. Set `use_kubeconfig = true`. + +## Configuration + +### Template Variables + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `use_kubeconfig` | bool | `false` | Use host kubeconfig for authentication | +| `namespace` | string | required | Kubernetes namespace for workspaces | + +### Workspace Parameters + +Users can customize these settings when creating a workspace: + +| Parameter | Options | Default | Description | +|-----------|---------|---------|-------------| +| CPU | 2, 4, 6, 8 cores | 2 | CPU cores allocated to the workspace | +| Memory | 2, 4, 6, 8 GB | 2 | Memory allocated to the workspace | +| Home Disk Size | 1-99999 GB | 10 | Persistent storage for `/home/coder` | + +## Architecture + +This template provisions the following Kubernetes resources: + +- **Deployment**: A single-replica deployment running the workspace container +- **Persistent Volume Claim**: Storage for the `/home/coder` directory + +### Container Image + +Uses `codercom/enterprise-base:ubuntu` which includes common development tools. To customize: + +- Extend the image with additional tools +- Build your own image based on the [enterprise-images repository](https://github.com/coder/enterprise-images) + +### Included Applications + +| Application | Access | Description | +|-------------|--------|-------------| +| Claude Code | Subdomain | AI coding assistant with AI Bridge enabled | +| code-server | Subdomain | Browser-based VS Code | + +### Workspace Metadata + +The dashboard displays real-time metrics: + +- CPU Usage (container and host) +- RAM Usage (container and host) +- Home Disk Usage +- Load Average (host) + +## Pod Scheduling + +Workspaces use pod anti-affinity to distribute pods across nodes, improving cluster utilization and resilience. + +## Security + +- Containers run as non-root user (UID 1000) +- File system group set to 1000 +- Resource limits enforced based on user-selected parameters + +## Customization + +This template is designed as a starting point. Common modifications include: + +- Adding environment variables +- Mounting secrets or configmaps +- Changing the container image +- Adding init containers +- Configuring node selectors or tolerations diff --git a/infra/1-click/2-coder/templates/kubernetes-claude/main.tf b/infra/1-click/2-coder/templates/kubernetes-claude/main.tf new file mode 100644 index 0000000..28817a2 --- /dev/null +++ b/infra/1-click/2-coder/templates/kubernetes-claude/main.tf @@ -0,0 +1,383 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + kubernetes = { + source = "hashicorp/kubernetes" + } + } +} + +provider "coder" {} + +variable "use_kubeconfig" { + type = bool + description = <<-EOF + Use host kubeconfig? (true/false) + + Set this to false if the Coder host is itself running as a Pod on the same + Kubernetes cluster as you are deploying workspaces to. + + Set this to true if the Coder host is running outside the Kubernetes cluster + for workspaces. A valid "~/.kube/config" must be present on the Coder host. + EOF + default = false +} + +variable "namespace" { + type = string + description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces). If the Coder host is itself running as a Pod on the same Kubernetes cluster as you are deploying workspaces to, set this to the same namespace." +} + +variable "host_ip" { + type = string + description = "The IP address to use for hostAliases in workspace pods. If empty, no hostAliases will be added." + default = "" +} + +data "coder_parameter" "cpu" { + name = "cpu" + display_name = "CPU" + description = "The number of CPU cores" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 Cores" + value = "2" + } + option { + name = "4 Cores" + value = "4" + } + option { + name = "6 Cores" + value = "6" + } + option { + name = "8 Cores" + value = "8" + } +} + +data "coder_parameter" "memory" { + name = "memory" + display_name = "Memory" + description = "The amount of memory in GB" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 GB" + value = "2" + } + option { + name = "4 GB" + value = "4" + } + option { + name = "6 GB" + value = "6" + } + option { + name = "8 GB" + value = "8" + } +} + +data "coder_parameter" "home_disk_size" { + name = "home_disk_size" + display_name = "Home disk size" + description = "The size of the home disk in GB" + default = "10" + type = "number" + icon = "/emojis/1f4be.png" + mutable = false + validation { + min = 1 + max = 99999 + } +} + +provider "kubernetes" { + # Authenticate via ~/.kube/config or a Coder-specific ServiceAccount, depending on admin preferences + config_path = var.use_kubeconfig == true ? "~/.kube/config" : null +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_agent" "main" { + os = "linux" + arch = "amd64" + + # The following metadata blocks are optional. They are used to display + # information about your workspace in the dashboard. You can remove them + # if you don't want to display any information. + # For basic resources, you can use the `coder stat` command. + # If you need more control, you can write your own script. + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "3_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + # get load avg scaled by number of cores + script = <` +- Check pod events: `kubectl describe pod -n coder-` +- Verify StorageClass is available: `kubectl get sc` + +### Cannot connect to workspace + +- Ensure the Coder agent is running: check pod logs +- Verify network policies allow traffic to the workspace + +### Persistent volume not binding + +- Check PVC status: `kubectl get pvc -n ` +- Verify StorageClass supports dynamic provisioning diff --git a/infra/1-click/2-coder/templates/kubernetes/main.tf b/infra/1-click/2-coder/templates/kubernetes/main.tf new file mode 100644 index 0000000..e63cd73 --- /dev/null +++ b/infra/1-click/2-coder/templates/kubernetes/main.tf @@ -0,0 +1,365 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + kubernetes = { + source = "hashicorp/kubernetes" + } + } +} + +provider "coder" {} + +variable "use_kubeconfig" { + type = bool + description = <<-EOF + Use host kubeconfig? (true/false) + + Set this to false if the Coder host is itself running as a Pod on the same + Kubernetes cluster as you are deploying workspaces to. + + Set this to true if the Coder host is running outside the Kubernetes cluster + for workspaces. A valid "~/.kube/config" must be present on the Coder host. + EOF + default = false +} + +variable "namespace" { + type = string + description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces). If the Coder host is itself running as a Pod on the same Kubernetes cluster as you are deploying workspaces to, set this to the same namespace." +} + +variable "host_ip" { + type = string + description = "The IP address to use for hostAliases in workspace pods. If empty, no hostAliases will be added." + default = "" +} + +data "coder_parameter" "cpu" { + name = "cpu" + display_name = "CPU" + description = "The number of CPU cores" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 Cores" + value = "2" + } + option { + name = "4 Cores" + value = "4" + } + option { + name = "6 Cores" + value = "6" + } + option { + name = "8 Cores" + value = "8" + } +} + +data "coder_parameter" "memory" { + name = "memory" + display_name = "Memory" + description = "The amount of memory in GB" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 GB" + value = "2" + } + option { + name = "4 GB" + value = "4" + } + option { + name = "6 GB" + value = "6" + } + option { + name = "8 GB" + value = "8" + } +} + +data "coder_parameter" "home_disk_size" { + name = "home_disk_size" + display_name = "Home disk size" + description = "The size of the home disk in GB" + default = "10" + type = "number" + icon = "/emojis/1f4be.png" + mutable = false + validation { + min = 1 + max = 99999 + } +} + +provider "kubernetes" { + # Authenticate via ~/.kube/config or a Coder-specific ServiceAccount, depending on admin preferences + config_path = var.use_kubeconfig == true ? "~/.kube/config" : null +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_agent" "main" { + os = "linux" + arch = "amd64" + startup_script = <<-EOT + set -e + + # Install the latest code-server. + # Append "--version x.x.x" to install a specific version of code-server. + curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server + + # Start code-server in the background. + /tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 & + EOT + + # The following metadata blocks are optional. They are used to display + # information about your workspace in the dashboard. You can remove them + # if you don't want to display any information. + # For basic resources, you can use the `coder stat` command. + # If you need more control, you can write your own script. + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "3_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + # get load avg scaled by number of cores + script = <= v1.14.1](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli) +- [Terragrunt (>= v0.78.0)](https://terragrunt.gruntwork.io/docs/getting-started/install/) +- [AWS CLI (>= v2.33.23)](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) +- [AWS Account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-creating.html) +- [AWS Bedrock/Anthropic Agreement Completed](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html). +- [AWS Route53 Domain](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-register.html#domain-register-procedure-section) + +### Enterprise +- Same OSS requirements. +- An [Enterprise Coder License](https://coder.com/docs/admin/licensing). + +# Getting Started + +1. Create or acquire an AWS account +2. Login as a user with AdministratorAccess +3. Setup your local machine to have an [AWS profile](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). + - When running `aws configure`, this will automatically setup a `default` profile for you. + - If using `aws configure sso` you must manually set the profile name to `default`. Afterwards, you can login with `aws sso login` + - If using `aws login` follow this [thread](https://github.com/hashicorp/terraform-provider-aws/issues/45316) to see the fix. + +4. Purchase/Register a domain in Route53 and tie it to a public zone. When purchasing, it'll create a zone in a few minutes, wait for this. If using an existing domain, cleanup any conflicting A/AAAA/CNAME/TXT records. + +5. Fill the `coder.env` file with the following environment variables: + +```env +CODER_AWS_PROFILE=default # (Change profile if needed) +CODER_AWS_REGION=us-east-2 # (Change region if needed) +CODER_DOMAIN_NAME=put.your.domain.here.com +CODER_LICENSE=abcde1234..... # (Optional) +``` + +> [!IMPORTANT] +> Upon creating the initial deployment, DO NOT CHANGE THE DOMAIN. Clean up first, then recreate it. The entire infrastructure depends on the name. + +6. Initialize the project: + +```bash +./0-init.sh +``` + +7. Finally, deploy: + +```bash +./1-apply.sh +``` + +## Logging In + +1. On your browser, go to "https://put.your.domain.here.com" +2. Enter the below login details (if you didn't override it): + +``` +Email: admin@coder.com +Password: Th1s1sN0TS3CuR3!! +``` + +3. You're done! + +## Cleaning Up + +1. [Delete all Coder workspaces](https://coder.com/docs/user-guides/workspace-lifecycle#deleting-workspaces). + +2. Run this script to tear down the deployment: + +```bash +./2-clean.sh +``` + +## Advanced Configuration + +### Using a Remote State + +You can enable the deployment to store your terraform state remotely if you need to interact with this outside your local desktop. + +This is useful if you plan on running this deployment within an ephemeral environment and need to persist it's state. + +Otherwise, if you're running this on your laptop or on a machine that persists state, then feel free to keep the state local. + +If using a remote state, then follow this to setup the S3 backend: https://spacelift.io/blog/terraform-s3-backend#how-to-create-a-terraform-s3-backend + +Follow this link if you need more details on Terraform's S3 backend: https://developer.hashicorp.com/terraform/language/backend/s3 + +Once the AWS S3 Bucket is setup, set the following environment variables on your `coder.env` file: + +``` +CODER_TF_USE_REMOTE_STATE=true +CODER_TF_BACKEND_AWS_BUCKET_NAME= +CODER_TF_BACKEND_AWS_REGION= +``` + +### Changing the Coder Version + +There's no rolling back in Coder, only updating forward. If you need to change the Coder version, change the following environment variable: + +``` +CODER_VERSION= +``` + +Make sure that the version is ALWAYS greater than the previous. + +# Troubleshooting + +- When logging in to AWS you might run into issues such as: `An error occurred (InvalidRequestException)`. Make sure that: + - Your spelling is correct. + - The correct region for your SSO Start URL is set. + - You're consuming the correct SSO URL. + +- When initializing the project, and if you ran `aws login`, you might run into: `Error: failed to refresh cached credentials, no EC2 IMDS role found`. + - View this [thread]((https://github.com/hashicorp/terraform-provider-aws/issues/45316)) to see how to workaround this. Otherwise, try the other options to login (manually setting the profile/credentials, or use AWS SSO). + +- To verify the state of the infrastructure, visit the AWS Console and look at the pages of following AWS services that this solution deploys/manages: + - EC2 + - VPC (NAT Gateway, Subnets, Security Groups, EIPs) + - RDS (Database, Snapshots, and SubnetGroups) + - EKS + - Route53 + - CloudWatch Logs + - Bedrock + +- To verify the cluster state, run `aws eks update-kubeconfig --name --region --profile ` and execute kubectl commands. + +- To verify the cluster addons, inspect the status/logs of the following cluster addons: + - vpc-cni + - coredns + - metrics-server + - karpenter + - aws-load-balancer-controller (lb-ctrl) + - aws-ebs-csi (ebs-ctrl) + +- To verify the status of resources tied to CRDs, you can inspect the following: + - Service (check the aws-load-balancer-controller deployment for logs) + - PersistentVolumeClaim (check the ebs-csi deployment for logs) + - NodePool (EKS AutoMode + Karpenter) + - EC2NodeClass (Karpenter) + - NodeClass (EKS AutoMode) + + +- Be careful with running this deployment multiple times in succession. You may be getting rate-limited by AWS. + +- If the deployment fails on "data.external.first-user", then you might be running into: + - DNS hasn't propagated yet (you may need to re-run this after waiting a few more minutes) + - Domain name records not set properly. Check Route53 for your domain to see if the A record was properly set to the correct IP. + - Local DNS may not also be refreshed yet either (i.e. curl may fail, but browser still accessible) + - Load Balancer health checks may be failing (i.e. Availability Zone is temporarily unavailable, or Coder failed to start.) + +- EKS Addons may fail installation due to "taking too long". Try re-running the deployment script to verify if changes have finished applying \ No newline at end of file diff --git a/infra/1-click/coder.env b/infra/1-click/coder.env new file mode 100644 index 0000000..c0849e2 --- /dev/null +++ b/infra/1-click/coder.env @@ -0,0 +1,14 @@ +CODER_TF_USE_REMOTE_STATE=false +CODER_TF_BACKEND_AWS_BUCKET_NAME= +CODER_TF_BACKEND_AWS_REGION= + +CODER_AWS_REGION=us-east-2 + +CODER_VERSION=2.30.0 +CODER_LICENSE= + +CODER_USERNAME=admin +CODER_EMAIL=admin@coder.com +CODER_PASSWORD=Th1s1sN0TS3CuR3!! + +CODER_DOMAIN_NAME= \ No newline at end of file diff --git a/infra/1-click/root.hcl b/infra/1-click/root.hcl new file mode 100644 index 0000000..4d1168f --- /dev/null +++ b/infra/1-click/root.hcl @@ -0,0 +1,99 @@ +locals { + CODER_TF_BACKEND_AWS_BUCKET_NAME = get_env("CODER_TF_BACKEND_AWS_BUCKET_NAME") + CODER_TF_BACKEND_AWS_REGION = get_env("CODER_TF_BACKEND_AWS_REGION", "us-east-2") + CODER_TF_BACKEND_AWS_PROFILE = get_env("CODER_TF_BACKEND_AWS_PROFILE", "default") + CODER_TF_BACKEND_ENCRYPT = get_env("CODER_TF_BACKEND_ENCRYPT", "false") + CODER_TF_USE_REMOTE_STATE = get_env("CODER_TF_USE_REMOTE_STATE", "false") == "true" + + CODER_AWS_PROFILE = get_env("CODER_AWS_PROFILE", "default") + CODER_AWS_REGION = get_env("CODER_AWS_REGION", "us-east-2") + CODER_AWS_AZS = get_env("CODER_AWS_AZS", jsonencode(["a", "c"])) + + CODER_DB_USERNAME = get_env("CODER_DB_USERNAME", "coder") + CODER_DB_PASSWORD = get_env("CODER_DB_PASSWORD", "th1s1sn0tas3cur3pass0wrd") + GRAFANA_DB_USERNAME = get_env("GRAFANA_DB_USERNAME", "grafana") + GRAFANA_DB_PASSWORD = get_env("GRAFANA_DB_PASSWORD", "th1s1sn0tas3cur3pass0wrd") + + CODER_DOMAIN_NAME = get_env("CODER_DOMAIN_NAME") + CODER_LICENSE = get_env("CODER_LICENSE", "") + CODER_VERSION = get_env("CODER_VERSION", "2.30.0") + + CODER_USERNAME = get_env("CODER_USERNAME", "admin") + CODER_EMAIL = get_env("CODER_EMAIL", "admin@coder.com") + CODER_PASSWORD = get_env("CODER_PASSWORD", "Th1s1sN0TS3CuR3!!") + + GRAFANA_USERNAME = get_env("GRAFANA_ADMIN_USERNAME", "admin") + GRAFANA_PASSWORD = get_env("GRAFANA_ADMIN_PASSWORD", "Th1s1sN0TS3CuR3!!") +} + +generate "backend" { + path = "backend.tf" + if_exists = "overwrite" + disable = !local.CODER_TF_USE_REMOTE_STATE + if_disabled = "remove_terragrunt" + + contents = <<-EOF + terraform { + backend "s3" { + bucket = "${local.CODER_TF_BACKEND_AWS_BUCKET_NAME}" + key = "${path_relative_to_include()}/terraform.tfstate" + region = "${local.CODER_TF_BACKEND_AWS_REGION}" + profile = "${local.CODER_TF_BACKEND_AWS_PROFILE}" + encrypt = "${local.CODER_TF_BACKEND_ENCRYPT}" + } + } + EOF +} + +generate "provider" { + path = "provider.tf" + if_exists = "overwrite" + + contents = <<-EOF + terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.32.1" + } + helm = { + source = "hashicorp/helm" + version = "~> 3.1.1" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 3.0.1" + } + external = { + source = "hashicorp/external" + version = "~> 2.3.5" + } + time = { + source = "hashicorp/time" + version = "~> 0.13.1" + } + http = { + source = "hashicorp/http" + version = "~> 3.5.0" + } + dns = { + source = "hashicorp/dns" + version = "~> 3.5.0" + } + archive = { + source = "hashicorp/archive" + version = "~> 2.7.1" + } + random = { + source = "hashicorp/random" + version = "~> 3.8.1" + } + coderd = { + source = "coder/coderd" + version = "~> 0.0.12" + } + } + } + EOF +} \ No newline at end of file diff --git a/infra/aws/0-init.sh b/infra/aws/0-init.sh new file mode 100755 index 0000000..5b00bc9 --- /dev/null +++ b/infra/aws/0-init.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -ae -o pipefail + +source coder.env + +terragrunt run --all --non-interactive --config root.hcl $@ init -- -migrate-state -upgrade \ No newline at end of file diff --git a/infra/aws/1-plan.sh b/infra/aws/1-plan.sh new file mode 100755 index 0000000..82fe3a5 --- /dev/null +++ b/infra/aws/1-plan.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -ae -o pipefail + +source coder.env + +echo $@ + +TG_ARGS=() +TF_ARGS=() + +while [[ $# -gt 0 ]]; do + if [[ "$1" == "--" ]]; then + shift + TF_ARGS=("$@") + break + fi + + TG_ARGS+=("$1") + shift +done + +terragrunt run --all \ + --config root.hcl \ + "${TG_ARGS[@]}" \ + -- \ + plan \ + "${TF_ARGS[@]}" \ No newline at end of file diff --git a/infra/aws/2-apply.sh b/infra/aws/2-apply.sh new file mode 100755 index 0000000..0322d9e --- /dev/null +++ b/infra/aws/2-apply.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -ae -o pipefail + +source coder.env + +echo $@ + +TG_ARGS=() +TF_ARGS=() + +while [[ $# -gt 0 ]]; do + if [[ "$1" == "--" ]]; then + shift + TF_ARGS=("$@") + break + fi + + TG_ARGS+=("$1") + shift +done + +terragrunt run --all \ + --config root.hcl \ + "${TG_ARGS[@]}" \ + -- \ + apply \ + "${TF_ARGS[@]}" \ No newline at end of file diff --git a/infra/aws/3-destroy.sh b/infra/aws/3-destroy.sh new file mode 100755 index 0000000..a9ae45a --- /dev/null +++ b/infra/aws/3-destroy.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -ae -o pipefail + +source coder.env + +terragrunt run --all --non-interactive --config root.hcl $@ destroy \ No newline at end of file diff --git a/infra/aws/eu-west-2/eks/main.tf b/infra/aws/eu-west-2/eks/main.tf index 2bffa33..ae33c20 100644 --- a/infra/aws/eu-west-2/eks/main.tf +++ b/infra/aws/eu-west-2/eks/main.tf @@ -1,129 +1,56 @@ -terraform { - required_version = ">= 1.0" - required_providers { - aws = { - source = "hashicorp/aws" - version = ">= 5.100.0" - } - } - backend "s3" {} -} - -variable "name" { - description = "The resource name and tag prefix" - type = string -} - -variable "profile" { - type = string -} - -variable "region" { - description = "The aws region to deploy eks cluster" - type = string -} - -variable "cluster_version" { - description = "The eks version" - type = string -} - -variable "cluster_instance_type" { - description = "EKS Instance Size/Type" - default = "t3.xlarge" - type = string -} - provider "aws" { region = var.region profile = var.profile } -## -# Cluster Infrastructure -## - -locals { - system_subnet_tags = { - "subnet.amazonaws.io/system/owned-by" = var.name - } - provisioner_subnet_tags = { - "subnet.amazonaws.io/coder-provisioner/owned-by" = var.name - } - ws_all_subnet_tags = { - "subnet.amazonaws.io/coder-ws-all/owned-by" = var.name - } - system_sg_tags = { - "subnet.amazonaws.io/system/owned-by" = var.name - } - provisioner_sg_tags = { - "sg.amazonaws.io/coder-provisioner/owned-by" = var.name - } - ws_all_sg_tags = { - "sg.amazonaws.io/coder-ws-all/owned-by" = var.name - } - cluster_asg_node_labels = { - "node.amazonaws.io/managed-by" = "asg" +data "aws_vpc" "this" { + tags = { + Name = var.vpc_name } } -data "aws_region" "this" {} +data "aws_subnets" "public" { + filter { + name = "vpc-id" + values = [data.aws_vpc.this.id] + } -module "eks-network" { - source = "../../../../modules/network/eks-vpc" + tags = { + Name = "*${var.public_subnet_suffix}*" + } +} - name = var.name - vpc_cidr_block = "10.0.0.0/16" - public_subnets = { - "system0" = { - cidr_block = "10.0.10.0/24" - availability_zone = "${data.aws_region.this.name}a" - map_public_ip_on_launch = true - private_dns_hostname_type_on_launch = "ip-name" - } - "system1" = { - cidr_block = "10.0.11.0/24" - availability_zone = "${data.aws_region.this.name}b" - map_public_ip_on_launch = true - private_dns_hostname_type_on_launch = "ip-name" - } +data "aws_subnets" "private" { + filter { + name = "vpc-id" + values = [data.aws_vpc.this.id] } - private_subnets = { - "system0" = { - cidr_block = "10.0.20.0/24" - availability_zone = "${data.aws_region.this.name}a" - private_dns_hostname_type_on_launch = "ip-name" - tags = local.system_subnet_tags - } - "system1" = { - cidr_block = "10.0.21.0/24" - availability_zone = "${data.aws_region.this.name}b" - private_dns_hostname_type_on_launch = "ip-name" - tags = local.system_subnet_tags - } - "provisioner" = { - cidr_block = "10.0.22.0/24" - availability_zone = "${data.aws_region.this.name}a" - map_public_ip_on_launch = true - private_dns_hostname_type_on_launch = "ip-name" - tags = local.provisioner_subnet_tags - } - "ws-all" = { - cidr_block = "10.0.16.0/22" - availability_zone = "${data.aws_region.this.name}b" - map_public_ip_on_launch = true - private_dns_hostname_type_on_launch = "ip-name" - tags = local.ws_all_subnet_tags - } + + tags = { + Name = "*${var.private_subnet_suffix}*" } } -data "aws_iam_policy_document" "sts" { - statement { - effect = "Allow" - actions = ["sts:*"] - resources = ["*"] +locals { + tags = { + Name = var.name + "karpenter.sh/discovery" = var.name + } + # Karpenter Security Group Discovery - https://karpenter.sh/v1.0/concepts/nodeclasses/#specsecuritygroupselectorterms + tags_kptr_sg_discovery = { + "karpenter.sh/discovery" = var.name + } + labels_system_node = { + "scheduling.coder.com/pool" = "system" } + taints_system = { + key = "CriticalAddonsOnly" + effect = "NO_SCHEDULE" + } + tolerations_system = [{ + key = "CriticalAddonsOnly" + operator = "Exists" + }] } resource "aws_iam_policy" "sts" { @@ -131,47 +58,62 @@ resource "aws_iam_policy" "sts" { path = "/" description = "Assume Role Policy" policy = data.aws_iam_policy_document.sts.json + tags = local.tags } -module "cluster" { +module "eks" { source = "terraform-aws-modules/eks/aws" - version = "~> 20.0" + version = "~> 21.15.1" - vpc_id = module.eks-network.vpc_id - subnet_ids = toset(concat(concat( - module.eks-network.public_subnet_ids, - module.eks-network.private_subnet_ids), - module.eks-network.intra_subnet_ids + vpc_id = data.aws_vpc.this.id + subnet_ids = toset(concat( + data.aws_subnets.private.ids )) - cluster_name = var.name - cluster_version = var.cluster_version - cluster_endpoint_public_access = true - cluster_endpoint_private_access = true + name = var.name + kubernetes_version = var.eks_version + endpoint_public_access = true + endpoint_private_access = true - create_cluster_security_group = true + create_security_group = true create_node_security_group = true - node_security_group_tags = merge( - local.system_sg_tags, - merge(local.provisioner_sg_tags, local.ws_all_sg_tags) - ) - create_iam_role = true - cluster_addons = { + create_iam_role = true + node_security_group_tags = local.tags_kptr_sg_discovery + create_node_iam_role = true + node_iam_role_use_name_prefix = true + + create_auto_mode_iam_resources = true + compute_config = { + enabled = true + } + + addons = { coredns = { - most_recent = true + before_compute = true } kube-proxy = { - most_recent = true + before_compute = true } vpc-cni = { - most_recent = true + before_compute = true + configuration_values = jsonencode({ + enableNetworkPolicy = "true" + nodeAgent = { + enablePolicyEventLogs = "true" + } + env = { + ENABLE_PREFIX_DELEGATION = "true" + WARM_PREFIX_TARGET = "1" + WARM_IP_TARGET = "0" + AWS_VPC_K8S_CNI_LOGLEVEL = "DEBUG" + } + }) } } - # Disable KMS, speed up cluster creation times. Enable if encryption is necessary. - attach_cluster_encryption_policy = false - create_kms_key = false - cluster_encryption_config = {} + attach_encryption_policy = false + create_kms_key = false # Enable unless needed + encryption_config = null enable_cluster_creator_admin_permissions = true enable_irsa = true @@ -180,9 +122,9 @@ module "cluster" { min_size = 0 max_size = 10 desired_size = 0 # Cant be modified after creation. Override from AWS Console - labels = local.cluster_asg_node_labels + labels = local.labels_system_node - instance_types = [var.cluster_instance_type] + instance_types = [var.instance_type] capacity_type = "ON_DEMAND" iam_role_additional_policies = { AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" @@ -190,7 +132,152 @@ module "cluster" { } # System Nodes should not be public - subnet_ids = module.eks-network.private_subnet_ids + subnet_ids = data.aws_subnets.private.ids } } + + tags = local.tags +} + +provider "kubernetes" { + host = module.eks.cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) + exec { + api_version = "client.authentication.k8s.io/v1beta1" + args = ["eks", "get-token", "--cluster-name", var.name, "--region", var.region, "--profile", var.profile] + command = "aws" + } +} + +resource "kubernetes_manifest" "default-class" { + + depends_on = [module.eks] + + manifest = { + apiVersion = "eks.amazonaws.com/v1" + kind = "NodeClass" + + metadata = { + name = "coder-default" + labels = { + "app.kubernetes.io/managed-by" = "terraform" + } + } + + spec = { + ephemeralStorage = { + iops = 3000 + size = "80Gi" + throughput = 125 + } + + networkPolicy = "DefaultAllow" + networkPolicyEventLogs = "Disabled" + snatPolicy = "Disabled" + + role = module.eks.node_iam_role_name + + securityGroupSelectorTerms = [ + { + id = module.eks.cluster_primary_security_group_id + } + ] + + subnetSelectorTerms = [for subnet_id in data.aws_subnets.private.ids : { id = subnet_id }] + } + } +} + +# Override the System NodePool to restrict number of nodess +resource "kubernetes_manifest" "system-pool" { + + depends_on = [module.eks] + + manifest = { + apiVersion = "karpenter.sh/v1" + kind = "NodePool" + + metadata = { + name = "coder-system" + labels = { + "app.kubernetes.io/managed-by" = "terraform" + } + } + + spec = { + disruption = { + budgets = [ + { + nodes = "10%" + } + ] + consolidateAfter = "30s" + consolidationPolicy = "WhenEmptyOrUnderutilized" + } + + limits = { + nodes = 4 + } + + template = { + metadata = {} + + spec = { + expireAfter = "336h" + + nodeClassRef = { + group = split("/", kubernetes_manifest.default-class.manifest.apiVersion)[0] + kind = kubernetes_manifest.default-class.manifest.kind + name = kubernetes_manifest.default-class.manifest.metadata.name + } + + requirements = [ + { + key = "karpenter.sh/capacity-type" + operator = "In" + values = ["on-demand"] + }, + { + key = "eks.amazonaws.com/instance-category" + operator = "In" + values = ["c", "m", "r"] + }, + { + key = "eks.amazonaws.com/instance-generation" + operator = "Gt" + values = ["4"] + }, + { + key = "kubernetes.io/arch" + operator = "In" + values = ["amd64", "arm64"] + }, + { + key = "topology.kubernetes.io/zone" + operator = "In" + values = slice([for az in var.azs : "${var.region}${az}"], 0, 1) + }, + { + key = "kubernetes.io/os" + operator = "In" + values = ["linux"] + } + ] + + taints = [ + { + key = "CriticalAddonsOnly" + effect = "NoSchedule" + } + ] + + terminationGracePeriod = "24h0m0s" + } + } + } + } +} + +output "azs" { + value = join(", ", slice([for az in var.azs : "${var.region}${az}"], 0, 1)) } \ No newline at end of file diff --git a/infra/aws/eu-west-2/eks/terragrunt.hcl b/infra/aws/eu-west-2/eks/terragrunt.hcl new file mode 100644 index 0000000..d77a8c1 --- /dev/null +++ b/infra/aws/eu-west-2/eks/terragrunt.hcl @@ -0,0 +1,21 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = ["../vpc"] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region="eu-west-2" + azs=jsondecode(include.root.locals.CODER_AWS_AZS) + + name=include.root.locals.CODER_CLUSTER_NAME + vpc_name=include.root.locals.CODER_VPC_NAME + eks_version=include.root.locals.CODER_CLUSTER_VERSION + instance_type=include.root.locals.CODER_CLUSTER_INSTANCE_TYPE + public_subnet_suffix=include.root.locals.CODER_PUBLIC_SUBNET_SUFFIX + private_subnet_suffix=include.root.locals.CODER_PRIVATE_SUBNET_SUFFIX +} \ No newline at end of file diff --git a/infra/aws/eu-west-2/eks/variables.tf b/infra/aws/eu-west-2/eks/variables.tf new file mode 100644 index 0000000..4e5b465 --- /dev/null +++ b/infra/aws/eu-west-2/eks/variables.tf @@ -0,0 +1,43 @@ +variable "profile" { + type = string +} + +variable "region" { + description = "The aws region to deploy eks cluster" + type = string +} + +variable "azs" { + description = "The aws availability zones to deploy eks cluster" + type = list(string) +} + +variable "name" { + description = "The resource name and tag prefix" + type = string +} + +variable "eks_version" { + description = "The eks version" + type = string +} + +variable "instance_type" { + description = "EKS Instance Size/Type" + default = "t3.xlarge" + type = string +} + +variable "vpc_name" { + type = string +} + +variable "private_subnet_suffix" { + type = string + default = "private" +} + +variable "public_subnet_suffix" { + type = string + default = "public" +} \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/cert-manager/main.tf b/infra/aws/eu-west-2/k8s/cert-manager/main.tf index c095843..7bc7507 100644 --- a/infra/aws/eu-west-2/k8s/cert-manager/main.tf +++ b/infra/aws/eu-west-2/k8s/cert-manager/main.tf @@ -1,59 +1,6 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - kubernetes = { - source = "hashicorp/kubernetes" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "cluster_oidc_provider_arn" { - type = string -} - -variable "addon_namespace" { - type = string - default = "cert-manager" -} - -variable "addon_version" { - type = string - default = "v1.18.2" -} - -variable "cloudflare_api_token" { - type = string - sensitive = true -} - -variable "cloudflare_email" { - type = string - sensitive = true -} - provider "aws" { - region = var.cluster_region - profile = var.cluster_profile + region = var.region + profile = var.profile } data "aws_eks_cluster" "this" { @@ -64,6 +11,10 @@ data "aws_eks_cluster_auth" "this" { name = var.cluster_name } +data "aws_iam_openid_connect_provider" "this" { + url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer +} + provider "helm" { kubernetes = { host = data.aws_eks_cluster.this.endpoint @@ -79,12 +30,17 @@ provider "kubernetes" { } module "cert-manager" { - source = "../../../../../modules/k8s/bootstrap/cert-manager" + + source = "../../../../../modules/k8s/bootstrap/cert-manager" + cluster_name = var.cluster_name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn + cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.this.arn + + namespace = var.addon_namespace + helm_version = var.addon_version - namespace = var.addon_namespace - helm_version = var.addon_version - cloudflare_token_secret = var.cloudflare_api_token - cloudflare_token_secret_email = var.cloudflare_email + tolerations = [{ + key = "CriticalAddonsOnly" + operator = "Exists" + }] } \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/cert-manager/terragrunt.hcl b/infra/aws/eu-west-2/k8s/cert-manager/terragrunt.hcl new file mode 100644 index 0000000..dc8cb8e --- /dev/null +++ b/infra/aws/eu-west-2/k8s/cert-manager/terragrunt.hcl @@ -0,0 +1,20 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks" + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region="eu-west-2" + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + addon_namespace=include.root.locals.CRTMGR_ADDON_NAMESPACE + addon_version=include.root.locals.CRTMGR_ADDON_VERSION +} \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/cert-manager/variables.tf b/infra/aws/eu-west-2/k8s/cert-manager/variables.tf new file mode 100644 index 0000000..f93b94c --- /dev/null +++ b/infra/aws/eu-west-2/k8s/cert-manager/variables.tf @@ -0,0 +1,22 @@ +variable "cluster_name" { + type = string +} + +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "addon_namespace" { + type = string + default = "cert-manager" +} + +variable "addon_version" { + type = string + default = "v1.18.2" +} \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/coder-proxy/main.tf b/infra/aws/eu-west-2/k8s/coder-proxy/main.tf index 3b5cf7b..e6f60af 100644 --- a/infra/aws/eu-west-2/k8s/coder-proxy/main.tf +++ b/infra/aws/eu-west-2/k8s/coder-proxy/main.tf @@ -1,114 +1,6 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - kubernetes = { - source = "hashicorp/kubernetes" - } - coderd = { - source = "coder/coderd" - } - acme = { - source = "vancluever/acme" - } - tls = { - source = "hashicorp/tls" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "acme_server_url" { - type = string - default = "https://acme-v02.api.letsencrypt.org/directory" -} - -variable "acme_registration_email" { - type = string -} - -variable "addon_version" { - type = string - default = "2.25.1" -} - -variable "coder_proxy_name" { - type = string -} - -variable "coder_proxy_display_name" { - type = string -} - -variable "coder_proxy_icon" { - type = string -} - -variable "coder_access_url" { - type = string - # sensitive = true -} - -variable "coder_proxy_url" { - type = string - # sensitive = true -} - -variable "coder_proxy_wildcard_url" { - type = string - # sensitive = true -} - -variable "coder_token" { - type = string - sensitive = true -} - -variable "image_repo" { - type = string - sensitive = true -} - -variable "image_tag" { - type = string - default = "latest" -} - -variable "kubernetes_ssl_secret_name" { - type = string -} - -variable "kubernetes_create_ssl_secret" { - type = bool - default = true -} - -variable "cloudflare_api_token" { - type = string - sensitive = true -} - provider "aws" { - region = var.cluster_region - profile = var.cluster_profile + region = var.region + profile = var.profile } data "aws_eks_cluster" "this" { @@ -119,6 +11,8 @@ data "aws_eks_cluster_auth" "this" { name = var.cluster_name } +data "aws_region" "this" {} + provider "helm" { kubernetes = { host = data.aws_eks_cluster.this.endpoint @@ -133,79 +27,176 @@ provider "kubernetes" { token = data.aws_eks_cluster_auth.this.token } +data "http" "login" { + url = "${var.coder_access_url}/api/v2/users/login" + method = "POST" + request_headers = { + Accept = "application/json" + } + request_body = jsonencode({ + email = var.coder_admin_email + password = var.coder_admin_password + }) + + retry { + attempts = 5 + min_delay_ms = (5 * 1000) # 5 seconds + } +} + provider "coderd" { url = var.coder_access_url - token = var.coder_token + token = jsondecode(data.http.login.response_body).session_token } -provider "acme" { - server_url = var.acme_server_url +locals { + azs = slice(var.azs, 0, 1) + pub_subs = [for az in local.azs : "${var.vpc_name}-public-${data.aws_region.this.region}${az}"] + release_name = "coder" + chart_name = "coder" + namespace = "coder" + + common_name = trimprefix(trimprefix(var.coder_proxy_url, "https://"), "http://") + wildcard_name = trimprefix(trimprefix(var.coder_proxy_wildcard_url, "https://"), "http://") + ssl_vol_friendly_name = replace(local.common_name, ".", "-") +} + +resource "kubernetes_manifest" "certificate" { + + field_manager { + force_conflicts = true + } + + wait { + condition { + type = "Ready" + status = "True" + } + } + + timeouts { + create = "10m" + update = "10m" + delete = "30s" + } + + manifest = { + apiVersion = "cert-manager.io/v1" + kind = "Certificate" + metadata = { + name = local.ssl_vol_friendly_name + namespace = module.coder-proxy.namespace + } + spec = { + commonName = local.common_name + dnsNames = [ + local.common_name, + local.wildcard_name + ] + duration = "2160h" # 90 days + renewBefore = "360h" # 15 days + issuerRef = { + kind = "ClusterIssuer" + name = "issuer" + } + secretName = local.ssl_vol_friendly_name + privateKey = { + rotationPolicy = "Never" + algorithm = "RSA" + encoding = "PKCS1" + size = "2048" + } + } + } +} + +resource "aws_eip" "coder" { + count = length(local.pub_subs) + domain = "vpc" + public_ipv4_pool = "amazon" + tags = { + Name = "coder-eip-${count.index}" + } } module "coder-proxy" { + source = "../../../../../modules/k8s/bootstrap/coder-proxy" - namespace = "coder-proxy" - acme_registration_email = var.acme_registration_email - acme_days_until_renewal = 90 - replica_count = 2 - helm_version = var.addon_version - image_repo = var.image_repo - image_tag = var.image_tag - primary_access_url = var.coder_access_url - proxy_access_url = var.coder_proxy_url - proxy_wildcard_url = var.coder_proxy_wildcard_url - coder_proxy_name = var.coder_proxy_name - coder_proxy_display_name = var.coder_proxy_display_name - coder_proxy_icon = var.coder_proxy_icon - proxy_token_config = { - name = "coder-proxy" + proxy = { + access_url = var.coder_proxy_url + wildcard_url = var.coder_proxy_wildcard_url + coder_access_url = var.coder_access_url + mount_ssl = true + mount_ssl_name = kubernetes_manifest.certificate.manifest.spec.secretName + name = var.coder_proxy_name + display_name = var.coder_proxy_display_name + icon = var.coder_proxy_icon + rep_cnt = 2 + image_repo = var.image_repo + image_tag = var.image_tag } - cloudflare_api_token = var.cloudflare_api_token - ssl_cert_config = { - name = var.kubernetes_ssl_secret_name - create_secret = var.kubernetes_create_ssl_secret + + namespace = local.namespace + resource_limit = { + cpu = "2" + memory = "4Gi" } - service_annotations = { - "service.beta.kubernetes.io/aws-load-balancer-nlb-target-type" = "instance" - "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing" - "service.beta.kubernetes.io/aws-load-balancer-attributes" = "deletion_protection.enabled=true" + resource_request = { + cpu = "500m" + memory = "4Gi" } - node_selector = { - "node.coder.io/managed-by" = "karpenter" - "node.coder.io/used-for" = "coder-proxy" + svc_annot = { + "service.beta.kubernetes.io/aws-load-balancer-nlb-target-type" = "ip" + "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing" + "service.beta.kubernetes.io/aws-load-balancer-attributes" = "deletion_protection.enabled=false" + "service.beta.kubernetes.io/aws-load-balancer-eip-allocations" = join(",", aws_eip.coder.*.allocation_id) + "service.beta.kubernetes.io/aws-load-balancer-subnets" = join(",", local.pub_subs) } tolerations = [{ - key = "dedicated" - operator = "Equal" - value = "coder-proxy" - effect = "NoSchedule" + key = "CriticalAddonsOnly" + operator = "Exists" }] - topology_spread_constraints = [{ + topology_spread = [{ max_skew = 1 topology_key = "kubernetes.io/hostname" when_unsatisfiable = "ScheduleAnyway" label_selector = { match_labels = { - "app.kubernetes.io/name" = "coder" - "app.kubernetes.io/part-of" = "coder" + "app.kubernetes.io/name" = local.chart_name + "app.kubernetes.io/part-of" = local.chart_name } } match_label_keys = [ "app.kubernetes.io/instance" ] }] - pod_anti_affinity_preferred_during_scheduling_ignored_during_execution = [{ - weight = 100 - pod_affinity_term = { - label_selector = { - match_labels = { - "app.kubernetes.io/instance" = "coder-v2" - "app.kubernetes.io/name" = "coder" - "app.kubernetes.io/part-of" = "coder" - } + affinity = { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [{ + key = "topology.kubernetes.io/zone" + operator = "In" + values = [for az in local.azs : "${data.aws_region.this.region}${az}"] + }] + }] } - topology_key = "kubernetes.io/hostname" } - }] + # podAntiAffinity = { + # preferredDuringSchedulingIgnoredDuringExecution = [{ + # weight = 100 + # podAffinityTerm = { + # topologyKey = "kubernetes.io/hostname" + # labelSelector = { + # matchLabels = { + # "app.kubernetes.io/instance" = local.release_name + # "app.kubernetes.io/name" = local.chart_name + # "app.kubernetes.io/part-of" = local.chart_name + # } + # } + # } + # }] + # } + } } \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/coder-proxy/terragrunt.hcl b/infra/aws/eu-west-2/k8s/coder-proxy/terragrunt.hcl new file mode 100644 index 0000000..a661dbb --- /dev/null +++ b/infra/aws/eu-west-2/k8s/coder-proxy/terragrunt.hcl @@ -0,0 +1,40 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks", + "../lb-controller", + "../cert-manager", + "../other", + "../../../us-east-2/k8s/coder-server" + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region="eu-west-2" + + azs=jsondecode(include.root.locals.CODER_AWS_AZS) + vpc_name=include.root.locals.CODER_VPC_NAME + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + coder_access_url=include.root.locals.CODER_DOMAIN_NAME + coder_wildcard_access_url=include.root.locals.CODER_WILDCARD_URL + + coder_proxy_url="https://emea-proxy.ai.coder.com" + coder_proxy_wildcard_url="*.emea-proxy.ai.coder.com" + coder_proxy_name="eu-west-2" + coder_proxy_display_name="Europe (London)" + coder_proxy_icon="/emojis/1f1ec-1f1e7.png" + + image_repo=include.root.locals.CODER_IMAGE_REPO + image_tag=include.root.locals.CODER_IMAGE_TAG + addon_version=include.root.locals.CODER_ADDON_VERSION + + coder_admin_email=include.root.locals.CODER_EMAIL + coder_admin_password=include.root.locals.CODER_PASSWORD +} + diff --git a/infra/aws/eu-west-2/k8s/coder-proxy/variables.tf b/infra/aws/eu-west-2/k8s/coder-proxy/variables.tf new file mode 100644 index 0000000..49b092a --- /dev/null +++ b/infra/aws/eu-west-2/k8s/coder-proxy/variables.tf @@ -0,0 +1,69 @@ +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "azs" { + type = list(string) + default = ["a", "b", "c"] +} + +variable "vpc_name" { + type = string +} + +variable "cluster_name" { + type = string +} + +variable "addon_version" { + type = string + default = "2.25.1" +} + +variable "coder_proxy_name" { + type = string +} + +variable "coder_proxy_display_name" { + type = string +} + +variable "coder_proxy_icon" { + type = string +} + +variable "coder_access_url" { + type = string +} + +variable "coder_proxy_url" { + type = string +} + +variable "coder_proxy_wildcard_url" { + type = string +} + +variable "image_repo" { + type = string + sensitive = true +} + +variable "image_tag" { + type = string + default = "latest" +} + +variable "coder_admin_email" { + type = string +} + +variable "coder_admin_password" { + type = string + sensitive = true +} \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/coder-ws/main.tf b/infra/aws/eu-west-2/k8s/coder-ws/main.tf deleted file mode 100644 index 31c49ac..0000000 --- a/infra/aws/eu-west-2/k8s/coder-ws/main.tf +++ /dev/null @@ -1,209 +0,0 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - kubernetes = { - source = "hashicorp/kubernetes" - } - coderd = { - source = "coder/coderd" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "cluster_oidc_provider_arn" { - type = string -} - -variable "provisioner_addon_version" { - type = string - default = "2.23.0" -} - -variable "logstream_addon_version" { - type = string - default = "0.0.11" -} - -variable "coder_access_url" { - type = string - sensitive = true -} - -variable "coder_token" { - type = string - sensitive = true -} - -variable "image_repo" { - type = string - sensitive = true -} - -variable "image_tag" { - type = string - default = "latest" -} - -variable "rotate_key_image_repo" { - type = string - sensitive = true -} - -variable "rotate_key_image_tag" { - type = string - sensitive = true -} - -variable "aws_secret_id" { - type = string - sensitive = true -} - -variable "aws_secret_region" { - type = string - sensitive = true -} - -provider "aws" { - region = var.cluster_region - profile = var.cluster_profile -} - -data "aws_eks_cluster" "this" { - name = var.cluster_name -} - -data "aws_eks_cluster_auth" "this" { - name = var.cluster_name -} - -provider "helm" { - kubernetes = { - host = data.aws_eks_cluster.this.endpoint - cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) - token = data.aws_eks_cluster_auth.this.token - } -} - -provider "kubernetes" { - host = data.aws_eks_cluster.this.endpoint - cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) - token = data.aws_eks_cluster_auth.this.token -} - -provider "coderd" { - url = var.coder_access_url - token = var.coder_token -} - -locals { - service_account_labels = { - "app.kubernetes.io/instance" = "coder-provisioner" - "app.kubernetes.io/name" = "coder-provisioner" - "app.kubernetes.io/part-of" = "coder-provisioner" - } - node_selector = { - "node.coder.io/managed-by" = "karpenter" - "node.coder.io/used-for" = "coder-provisioner" - } - tolerations = [{ - key = "dedicated" - operator = "Equal" - value = "coder-provisioner" - effect = "NoSchedule" - }] -} - -module "default-ws" { - source = "../../../../../modules/k8s/bootstrap/coder-provisioner" - cluster_name = var.cluster_name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - - coder_organization_name = "coder" - - namespace = "coder-ws" - image_repo = var.image_repo - image_tag = var.image_tag - ws_service_account_name = "coder-ws" - ws_service_account_labels = local.service_account_labels - provisioner_service_account_name = "coder" - replica_count = 6 - primary_access_url = var.coder_access_url - env_vars = { - CODER_PROMETHEUS_ENABLE = "false" - CODER_PROMETHEUS_COLLECT_AGENT_STATS = "false" - CODER_PROMETHEUS_COLLECT_DB_METRICS = "false" - } - node_selector = local.node_selector - tolerations = local.tolerations -} - -module "experiment-ws" { - source = "../../../../../modules/k8s/bootstrap/coder-provisioner" - - cluster_name = var.cluster_name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - - coder_organization_name = "experiment" - - namespace = "coder-ws-experiment" - image_repo = var.image_repo - image_tag = var.image_tag - ws_service_account_name = "coder-ws-experiment" - ws_service_account_labels = local.service_account_labels - provisioner_service_account_name = "coder" - replica_count = 2 - primary_access_url = var.coder_access_url - env_vars = { - CODER_PROMETHEUS_ENABLE = "false" - CODER_PROMETHEUS_COLLECT_AGENT_STATS = "false" - CODER_PROMETHEUS_COLLECT_DB_METRICS = "false" - } - node_selector = local.node_selector - tolerations = local.tolerations -} - -module "demo-ws" { - source = "../../../../../modules/k8s/bootstrap/coder-provisioner" - - cluster_name = var.cluster_name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - - coder_organization_name = "demo" - - namespace = "coder-ws-demo" - image_repo = var.image_repo - image_tag = var.image_tag - ws_service_account_name = "coder-ws-demo" - ws_service_account_labels = local.service_account_labels - provisioner_service_account_name = "coder" - replica_count = 2 - primary_access_url = var.coder_access_url - env_vars = { - CODER_PROMETHEUS_ENABLE = "false" - CODER_PROMETHEUS_COLLECT_AGENT_STATS = "false" - CODER_PROMETHEUS_COLLECT_DB_METRICS = "false" - } - node_selector = local.node_selector - tolerations = local.tolerations -} \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/ebs-controller/main.tf b/infra/aws/eu-west-2/k8s/ebs-controller/main.tf deleted file mode 100644 index e882e1d..0000000 --- a/infra/aws/eu-west-2/k8s/ebs-controller/main.tf +++ /dev/null @@ -1,72 +0,0 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - kubernetes = { - source = "hashicorp/kubernetes" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "cluster_oidc_provider_arn" { - type = string -} - -variable "addon_version" { - type = string - default = "2.22.1" -} - -variable "addon_namespace" { - type = string - default = "default" -} - -provider "aws" { - region = var.cluster_region - profile = var.cluster_profile -} - -data "aws_eks_cluster" "this" { - name = var.cluster_name -} - -data "aws_eks_cluster_auth" "this" { - name = var.cluster_name -} - -provider "helm" { - kubernetes = { - host = data.aws_eks_cluster.this.endpoint - cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) - token = data.aws_eks_cluster_auth.this.token - } -} - -module "ebs-controller" { - source = "../../../../../modules/k8s/bootstrap/ebs-controller" - cluster_name = data.aws_eks_cluster.this.name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - - namespace = var.addon_namespace - chart_version = var.addon_version -} \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/karpenter/main.tf b/infra/aws/eu-west-2/k8s/karpenter/main.tf deleted file mode 100644 index a997bdf..0000000 --- a/infra/aws/eu-west-2/k8s/karpenter/main.tf +++ /dev/null @@ -1,203 +0,0 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - kubernetes = { - source = "hashicorp/kubernetes" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_oidc_provider_arn" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "addon_version" { - type = string -} - -variable "addon_namespace" { - type = string - default = "default" -} - -provider "aws" { - region = var.cluster_region - profile = var.cluster_profile -} - -data "aws_eks_cluster" "this" { - name = var.cluster_name -} - -data "aws_eks_cluster_auth" "this" { - name = var.cluster_name -} - -provider "helm" { - kubernetes = { - host = data.aws_eks_cluster.this.endpoint - cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) - token = data.aws_eks_cluster_auth.this.token - } -} - -provider "kubernetes" { - host = data.aws_eks_cluster.this.endpoint - cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) - token = data.aws_eks_cluster_auth.this.token -} - -locals { - global_node_labels = { - "node.coder.io/instance" = "coder-v2" - "node.coder.io/managed-by" = "karpenter" - } - global_node_reqs = [{ - key = "kubernetes.io/arch" - operator = "In" - values = ["amd64"] - }, { - key = "kubernetes.io/os" - operator = "In" - values = ["linux"] - }, { - key = "kubernetes.sh/capacity-type" - operator = "In" - values = ["spot", "on-demand"] - }] - provisioner_subnet_tags = { - "subnet.amazonaws.io/coder-provisioner/owned-by" = var.cluster_name - } - ws_all_subnet_tags = { - "subnet.amazonaws.io/coder-ws-all/owned-by" = var.cluster_name - } - provisioner_sg_tags = { - "sg.amazonaws.io/coder-provisioner/owned-by" = var.cluster_name - } - ws_all_sg_tags = { - "sg.amazonaws.io/coder-ws-all/owned-by" = var.cluster_name - } -} - -locals { - nodepool_configs = [{ - name = "coder-proxy" - node_labels = merge(local.global_node_labels, { - "node.coder.io/name" = "coder" - "node.coder.io/part-of" = "coder" - "node.coder.io/used-for" = "coder-proxy" - }) - node_taints = [{ - key = "dedicated" - value = "coder-proxy" - effect = "NoSchedule" - }] - node_requirements = concat(local.global_node_reqs, [{ - key = "node.kubernetes.io/instance-type" - operator = "In" - values = ["m5a.xlarge", "m6a.xlarge"] - }]) - node_class_ref_name = "coder-proxy-class" - }, { - name = "coder-provisioner" - node_labels = merge(local.global_node_labels, { - "node.coder.io/name" = "coder" - "node.coder.io/part-of" = "coder" - "node.coder.io/used-for" = "coder-provisioner" - }) - node_taints = [{ - key = "dedicated" - value = "coder-provisioner" - effect = "NoSchedule" - }] - node_requirements = concat(local.global_node_reqs, [{ - key = "node.kubernetes.io/instance-type" - operator = "In" - values = ["m5a.4xlarge", "m6a.4xlarge"] - }]) - node_class_ref_name = "coder-provisioner-class" - }, { - name = "coder-ws-all" - node_labels = merge(local.global_node_labels, { - "node.coder.io/name" = "coder" - "node.coder.io/part-of" = "coder" - "node.coder.io/used-for" = "coder-ws-all" - }) - node_taints = [{ - key = "dedicated" - value = "coder-ws" - effect = "NoSchedule" - }] - node_requirements = concat(local.global_node_reqs, [{ - key = "node.kubernetes.io/instance-type" - operator = "In" - values = ["c6a.32xlarge", "c5a.32xlarge"] - }]) - node_class_ref_name = "coder-ws-class" - disruption_consolidate_after = "30m" - }] -} - -module "karpenter-addon" { - source = "../../../../../modules/k8s/bootstrap/karpenter" - cluster_name = var.cluster_name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - - namespace = var.addon_namespace - chart_version = var.addon_version - node_selector = { - "node.amazonaws.io/managed-by" : "asg" - } - ec2nodeclass_configs = [{ - name = "coder-proxy-class" - subnet_selector_tags = local.provisioner_subnet_tags - sg_selector_tags = local.provisioner_sg_tags - }, { - name = "coder-ws-class" - subnet_selector_tags = local.ws_all_subnet_tags - # ami_alias = "al2023@latest" # Use /dev/xvda - ami_alias = "bottlerocket@latest" # Use /dev/xvda + /dev/xvdb - sg_selector_tags = local.ws_all_sg_tags - user_data = <<-EOF - [settings.kubernetes] - 'registry-qps' = 20 - EOF - block_device_mappings = [{ - device_name = "/dev/xvda" - ebs = { - volume_size = "1000Gi" - volume_type = "gp3" - } - }, { - device_name = "/dev/xvdb" - ebs = { - volume_size = "1000Gi" - volume_type = "gp3" - } - }] - }, { - name = "coder-provisioner-class" - subnet_selector_tags = local.provisioner_subnet_tags - sg_selector_tags = local.provisioner_sg_tags - }] -} \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/lb-controller/main.tf b/infra/aws/eu-west-2/k8s/lb-controller/main.tf index 6bab002..c5721e0 100644 --- a/infra/aws/eu-west-2/k8s/lb-controller/main.tf +++ b/infra/aws/eu-west-2/k8s/lb-controller/main.tf @@ -1,54 +1,6 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - kubernetes = { - source = "hashicorp/kubernetes" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "cluster_oidc_provider_arn" { - type = string -} - -variable "addon_namespace" { - type = string - default = "default" -} - -variable "addon_version" { - type = string - default = "1.13.2" -} - -variable "use_cert_manager" { - type = bool - default = false -} - provider "aws" { - region = var.cluster_region - profile = var.cluster_profile + region = var.region + profile = var.profile } data "aws_eks_cluster" "this" { @@ -59,6 +11,16 @@ data "aws_eks_cluster_auth" "this" { name = var.cluster_name } +data "aws_iam_openid_connect_provider" "this" { + url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer +} + +data "aws_vpc" "this" { + tags = { + Name = var.vpc_name + } +} + provider "helm" { kubernetes = { host = data.aws_eks_cluster.this.endpoint @@ -67,18 +29,62 @@ provider "helm" { } } +provider "kubernetes" { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token +} + +locals { + release_name = "aws-load-balancer-controller" +} + module "lb-controller" { source = "../../../../../modules/k8s/bootstrap/lb-controller" cluster_name = data.aws_eks_cluster.this.name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn + cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.this.arn + release_name = local.release_name namespace = var.addon_namespace chart_version = var.addon_version enable_cert_manager = var.use_cert_manager - service_target_eni_sg_tags = { - Name = "aidemo-node" + vpc_id = data.aws_vpc.this.id + + tolerations = [{ + key = "CriticalAddonsOnly" + operator = "Exists" + }] + affinity = { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [ + { + key = "eks.amazonaws.com/compute-type", + operator = "In", + values = ["auto"] + } + ] + }] + } + } + podAntiAffinity = { + preferredDuringSchedulingIgnoredDuringExecution = [{ + weight = 100 + podAffinityTerm = { + labelSelector = { + matchExpressions = [{ + key = "app.kubernetes.io/name" + operator = "In" + values = [local.release_name] + }] + } + topologyKey = "kubernetes.io/hostname" + } + }] + } } - cluster_asg_node_labels = { # var.karpenter_node_labels - "node.amazonaws.io/managed-by" = "asg" + service_target_eni_sg_tags = { + Name = "aienv-node" } } \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/lb-controller/terragrunt.hcl b/infra/aws/eu-west-2/k8s/lb-controller/terragrunt.hcl new file mode 100644 index 0000000..670a7d7 --- /dev/null +++ b/infra/aws/eu-west-2/k8s/lb-controller/terragrunt.hcl @@ -0,0 +1,23 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks", + "../cert-manager" + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region="eu-west-2" + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + addon_namespace=include.root.locals.LB_ADDON_NAMESPACE + addon_version=include.root.locals.LB_ADDON_VERSION + vpc_name=include.root.locals.CODER_VPC_NAME + use_cert_manager=include.root.locals.LB_ADDON_USE_CERTMGR +} \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/lb-controller/variables.tf b/infra/aws/eu-west-2/k8s/lb-controller/variables.tf new file mode 100644 index 0000000..1b505c0 --- /dev/null +++ b/infra/aws/eu-west-2/k8s/lb-controller/variables.tf @@ -0,0 +1,31 @@ +variable "cluster_name" { + type = string +} + +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "addon_namespace" { + type = string + default = "default" +} + +variable "addon_version" { + type = string + default = "1.13.2" +} + +variable "vpc_name" { + type = string +} + +variable "use_cert_manager" { + type = bool + default = false +} \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/metrics-server/main.tf b/infra/aws/eu-west-2/k8s/metrics-server/main.tf index 8b50a90..a4a69e2 100644 --- a/infra/aws/eu-west-2/k8s/metrics-server/main.tf +++ b/infra/aws/eu-west-2/k8s/metrics-server/main.tf @@ -1,42 +1,6 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "addon_namespace" { - type = string - default = "kube-system" -} - -variable "addon_version" { - type = string - default = "3.13.0" -} - provider "aws" { - region = var.cluster_region - profile = var.cluster_profile + region = var.region + profile = var.profile } data "aws_eks_cluster" "this" { @@ -60,7 +24,8 @@ module "metrics-server" { namespace = var.addon_namespace chart_version = var.addon_version - node_selector = { - "node.amazonaws.io/managed-by" = "asg" - } + tolerations = [{ + key = "CriticalAddonsOnly" + operator = "Exists" + }] } \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/metrics-server/terragrunt.hcl b/infra/aws/eu-west-2/k8s/metrics-server/terragrunt.hcl new file mode 100644 index 0000000..6c0c0d2 --- /dev/null +++ b/infra/aws/eu-west-2/k8s/metrics-server/terragrunt.hcl @@ -0,0 +1,20 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks" + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region="eu-west-2" + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + addon_namespace=include.root.locals.METRICS_SRV_ADDON_NAMESPACE + addon_version=include.root.locals.METRICS_SRV_ADDON_VERSION +} \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/metrics-server/variables.tf b/infra/aws/eu-west-2/k8s/metrics-server/variables.tf new file mode 100644 index 0000000..6e7dfc0 --- /dev/null +++ b/infra/aws/eu-west-2/k8s/metrics-server/variables.tf @@ -0,0 +1,22 @@ +variable "cluster_name" { + type = string +} + +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "addon_namespace" { + type = string + default = "kube-system" +} + +variable "addon_version" { + type = string + default = "3.13.0" +} \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/other/main.tf b/infra/aws/eu-west-2/k8s/other/main.tf new file mode 100644 index 0000000..ecb5627 --- /dev/null +++ b/infra/aws/eu-west-2/k8s/other/main.tf @@ -0,0 +1,135 @@ +provider "aws" { + region = var.region + profile = var.profile +} + +data "aws_eks_cluster" "this" { + name = var.cluster_name +} + +data "aws_eks_cluster_auth" "this" { + name = var.cluster_name +} + +data "aws_iam_openid_connect_provider" "this" { + url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer +} + +data "aws_region" "this" {} + +provider "helm" { + kubernetes = { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token + } +} + +provider "kubernetes" { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token +} + +## +# Manifest Setup Post Addon-Deployment +# Includes auxiliary resources depending on CRDs +## + +## +# EBS CSI StorageClasses +## + +resource "kubernetes_manifest" "automode-gp3" { + manifest = { + apiVersion = "storage.k8s.io/v1" + kind = "StorageClass" + metadata = { + name = "gp3-automode" + } + provisioner = "ebs.csi.eks.amazonaws.com" + volumeBindingMode = "WaitForFirstConsumer" + allowedTopologies = [{ + matchLabelExpressions = [{ + key = "eks.amazonaws.com/compute-type" + values = ["auto"] + }] + }] + parameters = { + type = "gp3" + encrypted = "true" + } + } +} + +## +# Setup Cert-Manager ClusterIssuer +## + +locals { + cf_secret_key = "key" +} + +resource "kubernetes_secret_v1" "cf" { + metadata { + name = "cloudflare-token" + namespace = var.cloudflare_secret_namespace + annotations = { + "custom.kubernetes.secret/key" = local.cf_secret_key + "custom.kubernetes.secret/email" = var.cloudflare_email + } + } + data = { + (local.cf_secret_key) = var.cloudflare_api_token + } +} + +resource "kubernetes_manifest" "issuer" { + + field_manager { + force_conflicts = true + } + + wait { + condition { + type = "Ready" + status = "True" + } + } + + timeouts { + create = "10m" + update = "10m" + delete = "30s" + } + + manifest = { + apiVersion = "cert-manager.io/v1" + kind = "ClusterIssuer" + metadata = { + labels = {} + name = var.cluster_issuer_name + } + spec = { + acme = { + privateKeySecretRef = { + name = var.cluster_issuer_priv_key_ref + } + server = "https://acme-v02.api.letsencrypt.org/directory" + solvers = [ + { + dns01 = { + cloudflare = { + apiTokenSecretRef = { + key = kubernetes_secret_v1.cf.metadata[0].annotations["custom.kubernetes.secret/key"] + name = kubernetes_secret_v1.cf.metadata[0].name + } + email = kubernetes_secret_v1.cf.metadata[0].annotations["custom.kubernetes.secret/email"] + } + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/other/terragrunt.hcl b/infra/aws/eu-west-2/k8s/other/terragrunt.hcl new file mode 100644 index 0000000..3268609 --- /dev/null +++ b/infra/aws/eu-west-2/k8s/other/terragrunt.hcl @@ -0,0 +1,25 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks", + "../lb-controller", + "../cert-manager", + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region="eu-west-2" + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + azs=include.root.locals.CODER_VPC_AZS + + cloudflare_api_token=include.root.locals.CF_TOKEN + cloudflare_secret_namespace=include.root.locals.CRTMGR_ADDON_NAMESPACE + cloudflare_email=include.root.locals.CF_EMAIL +} \ No newline at end of file diff --git a/infra/aws/eu-west-2/k8s/other/variables.tf b/infra/aws/eu-west-2/k8s/other/variables.tf new file mode 100644 index 0000000..74f086c --- /dev/null +++ b/infra/aws/eu-west-2/k8s/other/variables.tf @@ -0,0 +1,42 @@ +variable "profile" { + type = string + default = "default" +} + +variable "region" { + type = string + default = "us-east-2" +} + +variable "cluster_name" { + type = string +} + +variable "cluster_issuer_name" { + type = string + default = "issuer" +} + +variable "cluster_issuer_priv_key_ref" { + type = string + default = "issuer-account-key" +} + +variable "azs" { + type = list(string) + default = ["a", "b", "c"] +} + +variable "cloudflare_api_token" { + type = string + sensitive = true +} + +variable "cloudflare_secret_namespace" { + type = string + default = "cert-manager" +} + +variable "cloudflare_email" { + type = string +} diff --git a/infra/aws/eu-west-2/vpc/main.tf b/infra/aws/eu-west-2/vpc/main.tf new file mode 100644 index 0000000..4114767 --- /dev/null +++ b/infra/aws/eu-west-2/vpc/main.tf @@ -0,0 +1,67 @@ +provider "aws" { + region = var.region + profile = var.profile +} + +locals { + # Subnet Discovery - https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/deploy/subnet_discovery/#subnet-filtering + tags_lb_subnet_discovery = { + "kubernetes.io/cluster/${var.cluster_name}" = "owned" + } + # Internet-Facing LB - https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/deploy/subnet_discovery/#subnet-role-tag + tags_public_lb = { + "kubernetes.io/role/elb" = 1 + } + tags_public_subnet = merge( + local.tags_lb_subnet_discovery, + local.tags_public_lb + ) + tags_private_subnet = { + "karpenter.sh/discovery" = "${var.cluster_name}" + } + # Use variable to configure AZ just in case 1 AZ isn't accessible + # https://repost.aws/questions/QUgdQev4KETKG_Bwev9tMtRQ/is-it-possible-to-enable-3rd-availability-zone-in-us-west-1#AN9eAH55FwTC-NSSp-FjIfTQ + availability_zones = [for az in var.azs : "${var.region}${az}"] + public_subnet_cidrs = [for index, _ in var.azs : cidrsubnet(var.vpc_cidr, 8, index + 1)] + private_subnet_cidrs = [for index, _ in var.azs : cidrsubnet(var.vpc_cidr, 2, index + 1)] +} + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 6.6.0" + + name = var.vpc_name + + enable_nat_gateway = false + enable_dns_hostnames = true + + cidr = var.vpc_cidr + azs = local.availability_zones + + public_subnet_suffix = var.public_subnet_suffix + public_subnets = [for index, _ in var.azs : local.public_subnet_cidrs[index]] + public_subnet_tags = local.tags_public_subnet + + private_subnet_suffix = var.private_subnet_suffix + private_subnets = [for index, _ in var.azs : local.private_subnet_cidrs[index]] + private_subnet_tags = local.tags_private_subnet + + tags = {} +} + +module "nat" { + + source = "RaJiska/fck-nat/aws" + version = "~> 1.4.0" + + name = "${var.region}-${var.nat_name}" + vpc_id = module.vpc.vpc_id + subnet_id = module.vpc.public_subnets[0] + ha_mode = true # Enables high-availability mode + use_cloudwatch_agent = true # Enables Cloudwatch agent and have metrics reported + + update_route_tables = true + route_tables_ids = { + for index, _ in var.azs : "rtb-${index}" => module.vpc.private_route_table_ids[index] + } +} \ No newline at end of file diff --git a/infra/aws/eu-west-2/vpc/terragrunt.hcl b/infra/aws/eu-west-2/vpc/terragrunt.hcl new file mode 100644 index 0000000..7858497 --- /dev/null +++ b/infra/aws/eu-west-2/vpc/terragrunt.hcl @@ -0,0 +1,21 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region="eu-west-2" + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + vpc_name=include.root.locals.CODER_VPC_NAME + vpc_cidr=include.root.locals.CODER_VPC_CIDR + azs=include.root.locals.CODER_VPC_AZS + nat_name=include.root.locals.CODER_VPC_NAT_NAME + public_subnet_suffix=include.root.locals.CODER_PUBLIC_SUBNET_SUFFIX + private_subnet_suffix=include.root.locals.CODER_PRIVATE_SUBNET_SUFFIX +} \ No newline at end of file diff --git a/infra/aws/eu-west-2/vpc/variables.tf b/infra/aws/eu-west-2/vpc/variables.tf new file mode 100644 index 0000000..6ff8d7d --- /dev/null +++ b/infra/aws/eu-west-2/vpc/variables.tf @@ -0,0 +1,48 @@ +variable "profile" { + type = string +} + +variable "region" { + description = "The aws region for the vpc" + type = string +} + +variable "cluster_name" { + description = "Cluster name" + type = string +} + +variable "azs" { + type = list(string) + default = ["a", "b", "c"] + + validation { + condition = length(var.azs) <= 3 + error_message = "There should only be at most 3 availability zones specified." + } +} + +variable "vpc_name" { + description = "Name for created resources and tag prefix" + type = string +} + +variable "vpc_cidr" { + type = string + default = "10.0.0.0/16" +} + +variable "private_subnet_suffix" { + type = string + default = "private" +} + +variable "public_subnet_suffix" { + type = string + default = "public" +} + +variable "nat_name" { + description = "Name for created resources and tag prefix" + type = string +} \ No newline at end of file diff --git a/infra/aws/root.hcl b/infra/aws/root.hcl new file mode 100644 index 0000000..484b2ab --- /dev/null +++ b/infra/aws/root.hcl @@ -0,0 +1,187 @@ +locals { + CODER_TF_BACKEND_AWS_BUCKET_NAME = get_env("CODER_TF_BACKEND_AWS_BUCKET_NAME") + CODER_TF_BACKEND_AWS_REGION = get_env("CODER_TF_BACKEND_AWS_REGION", "us-east-2") + CODER_TF_BACKEND_AWS_PROFILE = get_env("CODER_TF_BACKEND_AWS_PROFILE", "default") + CODER_TF_BACKEND_ENCRYPT = get_env("CODER_TF_BACKEND_ENCRYPT", "false") + + CODER_AWS_PROFILE = get_env("CODER_AWS_PROFILE", "default") + CODER_AWS_REGION = get_env("CODER_AWS_REGION", "us-east-2") + CODER_AWS_AZS = get_env("CODER_AWS_AZS", jsonencode(["a", "c"])) + + CODER_CLUSTER_NAME = get_env("CODER_CLUSTER_NAME", "coder") + CODER_CLUSTER_VERSION = get_env("CODER_CLUSTER_VERSION", "1.35") + CODER_CLUSTER_INSTANCE_TYPE = get_env("CODER_CLUSTER_INSTANCE_TYPE", "c6a.large") + CODER_VPC_NAME = get_env("CODER_VPC_NAME", "coder") + CODER_VPC_CIDR = get_env("CODER_VPC_CIDR", "10.0.0.0/16") + CODER_VPC_AZS = get_env("CODER_VPC_AZS", jsonencode(["a", "b", "c"])) + CODER_VPC_NAT_NAME = get_env("CODER_VPC_NAT_NAME", "nat-instance") + CODER_DB_SUBNET_GROUP_NAME = get_env("CODER_DB_SUBNET_GROUP_NAME", "coder-db-subnet-group") + CODER_PUBLIC_SUBNET_SUFFIX = get_env("CODER_PUBLIC_SUBNET_SUFFIX", "public") + CODER_PRIVATE_SUBNET_SUFFIX = get_env("CODER_PRIVATE_SUBNET_SUFFIX", "private") + + CODER_DB_RDS_ID = get_env("CODER_DB_RDS_ID", "coder") + CODER_DB_USERNAME = get_env("CODER_DB_USERNAME", "coder") + CODER_DB_PASSWORD = get_env("CODER_DB_PASSWORD", "th1s1sn0tas3cur3pass0wrd") + CODER_DB_NAME = get_env("CODER_DB_NAME", "coder") + + LITELLM_DOMAIN_NAME = get_env("LITELLM_DOMAIN_NAME") + LITELLM_ADDON_NAMESPACE = get_env("LITELLM_ADDON_NAMESPACE", "litellm") + LITELLM_ADDON_VERSION = get_env("LITELLM_ADDON_VERSION", "0.1.830") + LITELLM_DB_RDS_ID = get_env("LITELLM_DB_RDS_ID", "litellm") + LITELLM_DB_NAME = get_env("LITELLM_DB_NAME", "litellm") + LITELLM_DB_USERNAME = get_env("LITELLM_DB_USERNAME", "litellm") + LITELLM_DB_USER_PASSWORD = get_env("LITELLM_DB_USER_PASSWORD", "th1s1sn0tas3cur3pass0wrd") + LITELLM_DB_ADMIN_PASSWORD = get_env("LITELLM_DB_ADMIN_PASSWORD", "th1s1sn0tas3cur3pass0wrd") + LITELLM_GCLOUD_AUTH = get_env("LITELLM_GCLOUD_AUTH", "th1s1sn0tas3cur3pass0wrd") + LITELLM_MASTER_KEY = get_env("LITELLM_MASTER_KEY", "th1s1sn0tas3cur3pass0wrd") + + CODER_IMAGE_REPO = get_env("CODER_IMAGE_REPO", "ghcr.io/coder/coder") + CODER_IMAGE_TAG = get_env("CODER_IMAGE_TAG", "v2.28.6") + CODER_ADDON_VERSION = get_env("CODER_ADDON_VERSION", "2.25.1") + CODER_LOGSTREAM_ADDON_VERSION = get_env("CODER_LOGSTREAM_ADDON_VERSION", "0.0.11") + + KPTR_ADDON_NAMESPACE = get_env("KPTR_ADDON_NAMESPACE", "karpenter") + KPTR_ADDON_VERSION = get_env("KPTR_ADDON_VERSION", "1.9.0") + + CRTMGR_ADDON_VERSION = get_env("CRTMGR_ADDON_VERSION", "v1.18.2") + CRTMGR_ADDON_NAMESPACE = get_env("CRTMGR_ADDON_NAMESPACE", "cert-manager") + + EBS_ADDON_NAMESPACE = get_env("EBS_ADDON_NAMESPACE", "ebs-controller") + EBS_ADDON_VERSION = get_env("EBS_ADDON_VERSION", "2.22.1") + EBS_ADDON_REPLACE = get_env("EBS_ADDON_REPLACE", "true") + + LB_ADDON_NAMESPACE = get_env("LB_ADDON_NAMESPACE", "lb-controller") + LB_ADDON_VERSION = get_env("LB_ADDON_VERSION", "1.13.2") + LB_ADDON_USE_CERTMGR = get_env("LB_ADDON_USE_CERTMGR", "true") + + METRICS_SRV_ADDON_NAMESPACE = get_env("METRICS_SRV_ADDON_NAMESPACE", "kube-system") + METRICS_SRV_ADDON_VERSION = get_env("METRICS_SRV_ADDON_VERSION", "3.13.0") + + CODER_OBSRV_CHART_VERSION = get_env("CODER_OBSRV_CHART_VERSION", "0.7.0-rc.1") + CODER_OBSRV_CHART_NAMESPACE = get_env("CODER_OBSRV_CHART_NAMESPACE", "observability") + + LOKI_S3_BUCKET_NAME = get_env("LOKI_S3_BUCKET_NAME", "loki") + LOKI_S3_BUCKET_REGION = get_env("LOKI_S3_BUCKET_REGION", "us-east-2") + + GRAFANA_DOMAIN_NAME = get_env("GRAFANA_DOMAIN_NAME") + + GRAFANA_DB_RDS_ID = get_env("GRAFANA_DB_RDS_ID", "grafana") + GRAFANA_DB_NAME = get_env("GRAFANA_DB_NAME", "grafana") + GRAFANA_DB_USERNAME = get_env("GRAFANA_DB_USERNAME", "grafana") + GRAFANA_DB_PASSWORD = get_env("GRAFANA_DB_PASSWORD", "Th1s1sN0TS3CuR3!!") + + GRAFANA_USERNAME = get_env("GRAFANA_USERNAME", "admin") + GRAFANA_PASSWORD = get_env("GRAFANA_PASSWORD", "Th1s1sN0TS3CuR3!!") + + GRAFANA_ADMIN_USERNAME = get_env("GRAFANA_ADMIN_USERNAME", "admin") + GRAFANA_ADMIN_PASSWORD = get_env("GRAFANA_ADMIN_PASSWORD", "Th1s1sN0TS3CuR3!!") + + CODER_DOMAIN_NAME = get_env("CODER_DOMAIN_NAME") + CODER_WILDCARD_URL = get_env("CODER_WILDCARD_URL") + CODER_LICENSE = get_env("CODER_LICENSE", "") + CODER_EXPERIMENTS = get_env("CODER_EXPERIMENTS", jsonencode([])) + CODER_BUILT_IN_PROVISIONER_COUNT = get_env("CODER_BUILT_IN_PROVISIONER_COUNT", "0") + + CODER_USERNAME = get_env("CODER_USERNAME", "admin") + CODER_EMAIL = get_env("CODER_EMAIL", "admin@coder.com") + CODER_PASSWORD = get_env("CODER_PASSWORD", "Th1s1sN0TS3CuR3!!") + + CF_EMAIL = get_env("CF_EMAIL") + CF_TOKEN = get_env("CF_TOKEN") + + CODER_OIDC_SIGN_IN_TEXT = get_env("CODER_OIDC_SIGN_IN_TEXT") + CODER_OIDC_ICON_URL = get_env("CODER_OIDC_ICON_URL") + CODER_OIDC_SCOPES = get_env("CODER_OIDC_SCOPES", "[]") + CODER_OIDC_EMAIL_DOMAIN = get_env("CODER_OIDC_EMAIL_DOMAIN") + CODER_OIDC_ISSUER_URL = get_env("CODER_OIDC_ISSUER_URL") + CODER_OIDC_CLIENT_ID = get_env("CODER_OIDC_CLIENT_ID") + CODER_OIDC_CLIENT_SECRET = get_env("CODER_OIDC_CLIENT_SECRET") + + CODER_GITHUB_OAUTH_CLIENT_ID = get_env("CODER_GITHUB_OAUTH_CLIENT_ID") + CODER_GITHUB_OAUTH_CLIENT_SECRET = get_env("CODER_GITHUB_OAUTH_CLIENT_SECRET") + CODER_GITHUB_EXTERN_AUTH_CLIENT_ID = get_env("CODER_GITHUB_EXTERN_AUTH_CLIENT_ID") + CODER_GITHUB_EXTERN_AUTH_CLIENT_SECRET = get_env("CODER_GITHUB_EXTERN_AUTH_CLIENT_SECRET") + + CODER_ANTHROPIC_LLM_ENDPOINT = get_env("CODER_ANTHROPIC_LLM_ENDPOINT") + CODER_ANTHROPIC_LLM_KEY = get_env("CODER_ANTHROPIC_LLM_KEY") + CODER_OPENAI_LLM_ENDPOINT = get_env("CODER_OPENAI_LLM_ENDPOINT") + CODER_OPENAI_LLM_KEY = get_env("CODER_OPENAI_LLM_KEY") +} + +generate "backend" { + path = "backend.tf" + if_exists = "overwrite" + + contents = <<-EOF + terraform { + backend "s3" { + bucket = "${local.CODER_TF_BACKEND_AWS_BUCKET_NAME}" + key = "ai.coder.com/infra/aws/${path_relative_to_include()}/terraform.tfstate" + region = "${local.CODER_TF_BACKEND_AWS_REGION}" + profile = "${local.CODER_TF_BACKEND_AWS_PROFILE}" + encrypt = "${local.CODER_TF_BACKEND_ENCRYPT}" + } + } + EOF +} + +generate "provider" { + path = "provider.tf" + if_exists = "overwrite" + + contents = <<-EOF + terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.52.0" + } + helm = { + source = "hashicorp/helm" + version = "~> 3.1.1" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 3.0.1" + } + grafana = { + source = "grafana/grafana" + version = "4.28.1" + } + external = { + source = "hashicorp/external" + version = "~> 2.3.5" + } + time = { + source = "hashicorp/time" + version = "~> 0.13.1" + } + http = { + source = "hashicorp/http" + version = "~> 3.5.0" + } + dns = { + source = "hashicorp/dns" + version = "~> 3.5.0" + } + archive = { + source = "hashicorp/archive" + version = "~> 2.7.1" + } + random = { + source = "hashicorp/random" + version = "~> 3.8.1" + } + coderd = { + source = "coder/coderd" + version = "~> 0.0.12" + } + argocd = { + source = "argoproj-labs/argocd" + version = "~> 7.15.3" + } + } + } + EOF +} \ No newline at end of file diff --git a/infra/aws/us-east-2/eks/data.tf b/infra/aws/us-east-2/eks/data.tf new file mode 100644 index 0000000..7dccbf0 --- /dev/null +++ b/infra/aws/us-east-2/eks/data.tf @@ -0,0 +1,70 @@ +data "aws_caller_identity" "me" {} + +data "aws_vpc" "this" { + tags = { + Name = var.vpc_name + } +} + +data "aws_subnets" "public" { + filter { + name = "vpc-id" + values = [data.aws_vpc.this.id] + } + + tags = { + Name = "*${var.public_subnet_suffix}*" + } +} + +data "aws_subnets" "private" { + filter { + name = "vpc-id" + values = [data.aws_vpc.this.id] + } + + tags = { + Name = "*${var.private_subnet_suffix}*" + } +} + +data "aws_ssoadmin_instances" "this" { + region = "us-east-1" +} + +data "aws_identitystore_group" "aws_administrator" { + identity_store_id = one(data.aws_ssoadmin_instances.this.identity_store_ids) + region = "us-east-1" + + alternate_identifier { + unique_attribute { + attribute_path = "DisplayName" + attribute_value = "CoderCSAWSAdmin" + } + } +} + +data "aws_iam_policy_document" "ecr-mirror" { + + statement { + effect = "Allow" + actions = ["ecr:CreateRepository"] + resources = ["*"] + } + + statement { + effect = "Allow" + actions = ["ecr:GetAuthorizationToken"] + resources = ["*"] + } + + statement { + effect = "Allow" + actions = [ + "ecr:BatchImportUpstreamImage", + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer" + ] + resources = [ "arn:aws:ecr:${var.region}:${data.aws_caller_identity.me.account_id}:repository/cache/*" ] + } +} \ No newline at end of file diff --git a/infra/aws/us-east-2/eks/main.tf b/infra/aws/us-east-2/eks/main.tf index e23a5bf..5269a67 100644 --- a/infra/aws/us-east-2/eks/main.tf +++ b/infra/aws/us-east-2/eks/main.tf @@ -1,56 +1,3 @@ -terraform { - required_version = ">= 1.0" - required_providers { - aws = { - source = "hashicorp/aws" - version = ">= 5.46" - } - } - backend "s3" {} -} - -variable "name" { - description = "The resource name and tag prefix" - type = string -} - -variable "profile" { - type = string -} - -variable "region" { - description = "The aws region to deploy eks cluster" - type = string -} - -variable "cluster_version" { - description = "The eks version" - type = string -} - -variable "cluster_instance_type" { - description = "EKS Instance Size/Type" - default = "t3.xlarge" - type = string -} - -variable "vpc_id" { - type = string - sensitive = true -} - -variable "private_subnet_ids" { - type = list(string) - default = [] - sensitive = true -} - -variable "public_subnet_ids" { - type = list(string) - default = [] - sensitive = true -} - provider "aws" { region = var.region profile = var.profile @@ -58,106 +5,330 @@ provider "aws" { locals { tags = { - Environment = "prod" - Name = "${var.name}-eks-cluster" + Name = var.name "karpenter.sh/discovery" = var.name } - system_subnet_tags = { - "subnet.amazonaws.io/system/owned-by" = var.name - } - provisioner_subnet_tags = { - "subnet.amazonaws.io/coder-provisioner/owned-by" = var.name - } - ws_all_subnet_tags = { - "subnet.amazonaws.io/coder-ws-all/owned-by" = var.name - } - system_sg_tags = { - "subnet.amazonaws.io/system/owned-by" = var.name - } - provisioner_sg_tags = { - "sg.amazonaws.io/coder-provisioner/owned-by" = var.name + # Karpenter Security Group Discovery - https://karpenter.sh/v1.0/concepts/nodeclasses/#specsecuritygroupselectorterms + tags_kptr_sg_discovery = { + "karpenter.sh/discovery" = var.name } - ws_all_sg_tags = { - "sg.amazonaws.io/coder-ws-all/owned-by" = var.name + labels_system_node = { + "scheduling.coder.com/pool" = "system" } - cluster_asg_node_labels = { - "node.amazonaws.io/managed-by" = "asg" + taints_system = { + key = "CriticalAddonsOnly" + effect = "NO_SCHEDULE" } + tolerations_system = [{ + key = "CriticalAddonsOnly" + operator = "Exists" + }] } -data "aws_iam_policy_document" "sts" { - statement { - effect = "Allow" - actions = ["sts:*"] - resources = ["*"] - } -} - -resource "aws_iam_policy" "sts" { - name_prefix = "sts" - path = "/" - description = "Assume Role Policy" - policy = data.aws_iam_policy_document.sts.json - tags = local.tags +resource "aws_iam_policy" "ecr-mirror" { + name_prefix = "ecr-mirror-auto" + description = "Allows ECR pull-through cache automation including repository creation" + path = "/${var.region}/auto-mode/" + policy = data.aws_iam_policy_document.ecr-mirror.json } module "eks" { source = "terraform-aws-modules/eks/aws" - version = "~> 20.0" + version = "~> 21.24.0" - vpc_id = var.vpc_id - subnet_ids = toset(concat(var.public_subnet_ids, var.private_subnet_ids)) + vpc_id = data.aws_vpc.this.id + subnet_ids = toset(concat( + data.aws_subnets.private.ids + )) - cluster_name = var.name - cluster_version = var.cluster_version - cluster_endpoint_public_access = true - cluster_endpoint_private_access = true + name = var.name + kubernetes_version = var.eks_version + endpoint_public_access = true + endpoint_private_access = true - create_cluster_security_group = true + create_security_group = true create_node_security_group = true create_iam_role = true - cluster_addons = { + node_security_group_tags = local.tags_kptr_sg_discovery + create_node_iam_role = true + node_iam_role_use_name_prefix = true + + node_iam_role_additional_policies = { + "ECRMirrorPolicy" = aws_iam_policy.ecr-mirror.arn + } + + create_auto_mode_iam_resources = true + compute_config = { + enabled = true + } + + addons = { coredns = { - most_recent = true + before_compute = true } kube-proxy = { - most_recent = true + before_compute = true } vpc-cni = { - most_recent = true + before_compute = true configuration_values = jsonencode({ enableNetworkPolicy = "true" nodeAgent = { enablePolicyEventLogs = "true" } + env = { + ENABLE_PREFIX_DELEGATION = "true" + WARM_PREFIX_TARGET = "1" + WARM_IP_TARGET = "0" + AWS_VPC_K8S_CNI_LOGLEVEL = "DEBUG" + } }) } } - attach_cluster_encryption_policy = false - create_kms_key = false - cluster_encryption_config = {} + attach_encryption_policy = false + create_kms_key = false # Enable unless needed + encryption_config = null enable_cluster_creator_admin_permissions = true enable_irsa = true - eks_managed_node_groups = { - system = { - min_size = 0 - max_size = 10 - desired_size = 0 # Cant be modified after creation. Override from AWS Console - labels = local.cluster_asg_node_labels - - instance_types = [var.cluster_instance_type] - capacity_type = "ON_DEMAND" - iam_role_additional_policies = { - AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" - STSAssumeRole = aws_iam_policy.sts.arn + tags = local.tags +} + +# https://aws-controllers-k8s.github.io/docs/intro +# https://github.com/aws-controllers-k8s +# https://docs.aws.amazon.com/eks/latest/userguide/ack.html +module "eks-ack-capability" { + source = "terraform-aws-modules/eks/aws//modules/capability" + version = "~> 21.24.0" + + name = "eks-ack" + cluster_name = module.eks.cluster_name + type = "ACK" + + # IAM Role/Policy + iam_role_policies = { + AdministratorAccess = "arn:aws:iam::aws:policy/AdministratorAccess" + } + + tags = local.tags +} + +# https://docs.aws.amazon.com/eks/latest/userguide/argocd.html +module "eks-argocd-capability" { + source = "terraform-aws-modules/eks/aws//modules/capability" + version = "~> 21.24.0" + + type = "ARGOCD" + cluster_name = module.eks.cluster_name + + configuration = { + argo_cd = { + aws_idc = { + idc_instance_arn = one(data.aws_ssoadmin_instances.this.arns) + idc_region = "us-east-1" } + namespace = "argocd" + rbac_role_mapping = [{ + role = "ADMIN" + identity = [{ + id = data.aws_identitystore_group.aws_administrator.group_id + type = "SSO_GROUP" + }] + }] + } + } - # System Nodes should not be public - subnet_ids = var.private_subnet_ids + # IAM Role/Policy + iam_policy_statements = { + ECRRead = { + actions = [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + ] + resources = ["*"] } } tags = local.tags +} + +resource "aws_eks_access_entry" "auto-mode" { + principal_arn = module.eks.node_iam_role_arn + cluster_name = module.eks.cluster_name + type = "EC2" +} + +resource "aws_eks_access_policy_association" "attach" { + + cluster_name = module.eks.cluster_name + policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSAutoNodePolicy" + principal_arn = module.eks.node_iam_role_arn + + access_scope { + type = "cluster" + } +} + +resource "aws_iam_instance_profile" "auto-mode" { + name = module.eks.node_iam_role_name + role = module.eks.node_iam_role_name +} + +provider "kubernetes" { + host = module.eks.cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) + exec { + api_version = "client.authentication.k8s.io/v1beta1" + args = ["eks", "get-token", "--cluster-name", var.name, "--region", var.region, "--profile", var.profile] + command = "aws" + } +} + +resource "kubernetes_service_account_v1" "auto-mode-node-role" { + + metadata { + name = "auto-mode-node-role" + namespace = "default" + annotations = { + "eks.amazonaws.com/role-arn" = module.eks.node_iam_role_arn + "eks.amazonaws.com/role-name" = module.eks.node_iam_role_name + } + } +} + +resource "kubernetes_manifest" "sys-default-cls" { + + depends_on = [ + module.eks, + aws_eks_access_entry.auto-mode, + aws_eks_access_policy_association.attach + ] + + manifest = { + apiVersion = "eks.amazonaws.com/v1" + kind = "NodeClass" + + metadata = { + name = "sys-default" + labels = { + "app.kubernetes.io/managed-by" = "terraform" + } + } + + spec = { + ephemeralStorage = { + iops = 3000 + size = "80Gi" + throughput = 125 + } + + networkPolicy = "DefaultAllow" + networkPolicyEventLogs = "Disabled" + + role = module.eks.node_iam_role_name + + securityGroupSelectorTerms = [ + { + id = module.eks.cluster_primary_security_group_id + } + ] + + snatPolicy = "Random" + + subnetSelectorTerms = [for subnet_id in data.aws_subnets.private.ids : { id = subnet_id }] + } + } +} + +# Override the System NodePool to restrict number of nodess +resource "kubernetes_manifest" "system-pool" { + + depends_on = [ + module.eks, + aws_eks_access_entry.auto-mode, + aws_eks_access_policy_association.attach + ] + + manifest = { + apiVersion = "karpenter.sh/v1" + kind = "NodePool" + + metadata = { + name = "coder-system" + labels = { + "app.kubernetes.io/managed-by" = "terraform" + } + } + + spec = { + disruption = { + budgets = [ + { + nodes = "10%" + } + ] + consolidateAfter = "30s" + consolidationPolicy = "WhenEmptyOrUnderutilized" + } + + template = { + metadata = {} + + spec = { + expireAfter = "336h" + + nodeClassRef = { + group = split("/", kubernetes_manifest.sys-default-cls.manifest.apiVersion)[0] + kind = kubernetes_manifest.sys-default-cls.manifest.kind + name = kubernetes_manifest.sys-default-cls.manifest.metadata.name + } + + requirements = [ + { + key = "karpenter.sh/capacity-type" + operator = "In" + values = ["spot", "on-demand"] + }, + { + key = "eks.amazonaws.com/instance-category" + operator = "In" + values = ["c", "m", "r"] + }, + { + key = "eks.amazonaws.com/instance-generation" + operator = "Gt" + values = ["4"] + }, + { + key = "kubernetes.io/arch" + operator = "In" + values = ["amd64", "arm64"] + }, + { + key = "topology.kubernetes.io/zone" + operator = "In" + values = slice([for az in var.azs : "${var.region}${az}"], 0, 1) + }, + { + key = "kubernetes.io/os" + operator = "In" + values = ["linux"] + } + ] + + taints = [ + { + key = "CriticalAddonsOnly" + effect = "NoSchedule" + } + ] + + terminationGracePeriod = "24h0m0s" + } + } + } + } } \ No newline at end of file diff --git a/infra/aws/us-east-2/eks/terragrunt.hcl b/infra/aws/us-east-2/eks/terragrunt.hcl new file mode 100644 index 0000000..827f4bc --- /dev/null +++ b/infra/aws/us-east-2/eks/terragrunt.hcl @@ -0,0 +1,21 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = ["../vpc"] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region=include.root.locals.CODER_AWS_REGION + azs=jsondecode(include.root.locals.CODER_AWS_AZS) + + name=include.root.locals.CODER_CLUSTER_NAME + vpc_name=include.root.locals.CODER_VPC_NAME + eks_version=include.root.locals.CODER_CLUSTER_VERSION + instance_type=include.root.locals.CODER_CLUSTER_INSTANCE_TYPE + public_subnet_suffix=include.root.locals.CODER_PUBLIC_SUBNET_SUFFIX + private_subnet_suffix=include.root.locals.CODER_PRIVATE_SUBNET_SUFFIX +} \ No newline at end of file diff --git a/infra/aws/us-east-2/eks/variables.tf b/infra/aws/us-east-2/eks/variables.tf new file mode 100644 index 0000000..4e5b465 --- /dev/null +++ b/infra/aws/us-east-2/eks/variables.tf @@ -0,0 +1,43 @@ +variable "profile" { + type = string +} + +variable "region" { + description = "The aws region to deploy eks cluster" + type = string +} + +variable "azs" { + description = "The aws availability zones to deploy eks cluster" + type = list(string) +} + +variable "name" { + description = "The resource name and tag prefix" + type = string +} + +variable "eks_version" { + description = "The eks version" + type = string +} + +variable "instance_type" { + description = "EKS Instance Size/Type" + default = "t3.xlarge" + type = string +} + +variable "vpc_name" { + type = string +} + +variable "private_subnet_suffix" { + type = string + default = "private" +} + +variable "public_subnet_suffix" { + type = string + default = "public" +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/acm-controller/main.tf b/infra/aws/us-east-2/k8s/acm-controller/main.tf new file mode 100644 index 0000000..a7f8d0a --- /dev/null +++ b/infra/aws/us-east-2/k8s/acm-controller/main.tf @@ -0,0 +1,111 @@ +provider "aws" { + region = var.region + profile = var.profile +} + +data "aws_eks_cluster" "this" { + name = var.cluster_name +} + +data "aws_eks_cluster_auth" "this" { + name = var.cluster_name +} + +data "aws_iam_openid_connect_provider" "this" { + url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer +} + +provider "helm" { + kubernetes = { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token + } +} + +provider "kubernetes" { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token +} + +module "oidc-role" { + source = "../../../../../modules/security/role/access-entry" + name = "acm-ctrl" + path = "/${var.cluster_name}/${var.region}/" + cluster_name = var.cluster_name + policy_arns = { + "AmazonS3ReadOnlyAccess" = "arn:aws:iam::aws:policy/AWSCertificateManagerFullAccess" + } + cluster_policy_arns = { + "AmazonEKSClusterAdminPolicy" = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy", + } + oidc_principals = { + "${data.aws_iam_openid_connect_provider.this.arn}" = ["system:serviceaccount:*:*"] + } + tags = {} +} + +resource "kubernetes_namespace_v1" "this" { + + count = var.create_namespace ? 1 : 0 + + metadata { + name = var.namespace + } +} + +resource "kubernetes_service_account_v1" "acm-ctrl" { + metadata { + name = "ack-acm-controller" + namespace = try(kubernetes_namespace_v1.this[0].metadata[0].name, var.namespace) + annotations = { + "eks.amazonaws.com/role-arn" = module.oidc-role.role_arn + } + } + automount_service_account_token = true +} + +resource "helm_release" "acm-ctrl" { + name = var.release_name + namespace = try(kubernetes_namespace_v1.this[0].metadata[0].name, var.namespace) + chart = var.chart_name + repository = "oci://public.ecr.aws/aws-controllers-k8s/" + create_namespace = false + upgrade_install = true + skip_crds = false + wait = true + wait_for_jobs = true + version = var.chart_version + timeout = 600 + + values = [yamlencode({ + aws = { + region = "us-east-1" # Required by CloudFront + } + serviceAccount = { + create = false + name = kubernetes_service_account_v1.acm-ctrl.metadata[0].name + } + deployment = { + tolerations = [{ + effect = "NoSchedule" + key = "CriticalAddonsOnly" + operator = "Exists" + }] + affinity = { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [{ + key = "karpenter.sh/nodepool" + operator = "In" + values = ["system"] + }] + }] + } + } + } + } + })] +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/acm-controller/terragrunt.hcl b/infra/aws/us-east-2/k8s/acm-controller/terragrunt.hcl new file mode 100644 index 0000000..1d7058d --- /dev/null +++ b/infra/aws/us-east-2/k8s/acm-controller/terragrunt.hcl @@ -0,0 +1,20 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks" + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region=include.root.locals.CODER_AWS_REGION + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + # addon_namespace=include.root.locals.KYVERNO_ADDON_NAMESPACE + # addon_version=include.root.locals.KYVERNO_ADDON_VERSION +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/acm-controller/variables.tf b/infra/aws/us-east-2/k8s/acm-controller/variables.tf new file mode 100644 index 0000000..189def8 --- /dev/null +++ b/infra/aws/us-east-2/k8s/acm-controller/variables.tf @@ -0,0 +1,37 @@ +variable "cluster_name" { + type = string +} + +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "release_name" { + type = string + default = "acm-chart" +} + +variable "chart_name" { + type = string + default = "acm-chart" +} + +variable "chart_version" { + type = string + default = "1.3.4" +} + +variable "create_namespace" { + type = bool + default = true +} + +variable "namespace" { + type = string + default = "acm-ctrl" +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-server/chart/Chart.lock b/infra/aws/us-east-2/k8s/argo/coder-server/chart/Chart.lock new file mode 100644 index 0000000..e9c61f1 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-server/chart/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: coder + repository: https://helm.coder.com/v2 + version: 2.25.1 +digest: sha256:c339760603080970eae8717ddf19b6475a4278a73724036eed0d5a9e4188c406 +generated: "2026-06-26T11:47:33.29437-07:00" diff --git a/infra/aws/us-east-2/k8s/argo/coder-server/chart/Chart.yaml b/infra/aws/us-east-2/k8s/argo/coder-server/chart/Chart.yaml new file mode 100644 index 0000000..44ade60 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-server/chart/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: coder +description: Wrapper chart that deploys the upstream Coder Helm chart with opinionated defaults +type: application +version: 0.1.0 +appVersion: "2.25.1" +dependencies: + - name: coder + version: 2.25.1 + repository: https://helm.coder.com/v2 diff --git a/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/_helpers.tpl b/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/_helpers.tpl new file mode 100644 index 0000000..776818c --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/_helpers.tpl @@ -0,0 +1,51 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "coder.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Fully qualified app name. Truncated to 63 chars (K8s label limit). +*/}} +{{- define "coder.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Common labels for wrapper-managed resources. +*/}} +{{- define "coder.labels" -}} +app.kubernetes.io/name: {{ include "coder.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Values.coder.coder.image.tag | default .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Selector labels matching the upstream coder chart's pod labels. +The upstream chart uses: + app.kubernetes.io/name: coder + app.kubernetes.io/instance: +*/}} +{{- define "coder.upstreamSelectorLabels" -}} +app.kubernetes.io/name: coder +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Namespace helper. +*/}} +{{- define "coder.namespace" -}} +{{- .Values.namespace | default .Release.Namespace }} +{{- end }} diff --git a/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/certificate.yaml b/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/certificate.yaml new file mode 100644 index 0000000..9c8b425 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/certificate.yaml @@ -0,0 +1,24 @@ +{{- if .Values.certificate.enabled }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ .Values.tls.secretName | default (include "coder.fullname" .) }} + namespace: {{ include "coder.namespace" . }} + labels: + {{- include "coder.labels" . | nindent 4 }} +spec: + commonName: {{ .Values.certificate.commonName | quote }} + dnsNames: + {{- toYaml .Values.certificate.dnsNames | nindent 4 }} + duration: {{ .Values.certificate.duration | quote }} + renewBefore: {{ .Values.certificate.renewBefore | quote }} + issuerRef: + kind: {{ .Values.certificate.issuerRef.kind }} + name: {{ .Values.certificate.issuerRef.name }} + secretName: {{ .Values.tls.secretName | default (include "coder.fullname" .) }} + privateKey: + rotationPolicy: {{ .Values.certificate.privateKey.rotationPolicy }} + algorithm: {{ .Values.certificate.privateKey.algorithm }} + encoding: {{ .Values.certificate.privateKey.encoding }} + size: {{ .Values.certificate.privateKey.size }} +{{- end }} diff --git a/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/env-configmap.yaml b/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/env-configmap.yaml new file mode 100644 index 0000000..643ba36 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/env-configmap.yaml @@ -0,0 +1,107 @@ +{{/* +ConfigMap containing all non-secret CODER_* environment variables. +Referenced by the upstream coder chart via coder.envFrom. +*/}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-env + namespace: {{ include "coder.namespace" . }} + labels: + {{- include "coder.labels" . | nindent 4 }} +data: + CODER_ACCESS_URL: {{ .Values.accessUrl | quote }} + CODER_WILDCARD_ACCESS_URL: {{ .Values.wildcardUrl | quote }} + CODER_REDIRECT_TO_ACCESS_URL: {{ .Values.tls.enabled | quote }} + CODER_ENABLE_TERRAFORM_DEBUG_MODE: {{ .Values.terraformDebugMode | quote }} + CODER_TRACE_LOGS: {{ .Values.traceLogs | quote }} + CODER_TRACE_ENABLE: {{ .Values.traceEnable | quote }} + CODER_LOG_FILTER: {{ .Values.logFilter | quote }} + CODER_SWAGGER_ENABLE: {{ .Values.swaggerEnable | quote }} + CODER_UPDATE_CHECK: {{ .Values.updateCheck | quote }} + CODER_CLI_UPGRADE_MESSAGE: {{ .Values.cliUpgradeMessage | quote }} + CODER_PROVISIONER_DAEMONS: {{ .Values.provisionerDaemons | quote }} + CODER_PROVISIONER_FORCE_CANCEL_INTERVAL: {{ .Values.provisionerForceCancelInterval | quote }} + CODER_QUIET_HOURS_DEFAULT_SCHEDULE: {{ .Values.quietHoursDefaultSchedule | quote }} + CODER_ALLOW_CUSTOM_QUIET_HOURS: {{ .Values.allowCustomQuietHours | quote }} + {{- if .Values.experiments }} + CODER_EXPERIMENTS: {{ .Values.experiments | quote }} + {{- end }} + + # Database + CODER_PG_AUTH: {{ .Values.db.pgAuth | quote }} + + # Prometheus + CODER_PROMETHEUS_ENABLE: {{ .Values.prometheus.enabled | quote }} + CODER_PROMETHEUS_COLLECT_AGENT_STATS: {{ .Values.prometheus.collectAgentStats | quote }} + CODER_PROMETHEUS_COLLECT_DB_METRICS: {{ .Values.prometheus.collectDbMetrics | quote }} + + # OIDC + {{- if .Values.oidc.enabled }} + CODER_OIDC_ISSUER_URL: {{ .Values.oidc.issuerUrl | quote }} + CODER_OIDC_CLIENT_ID: {{ .Values.oidc.clientId | quote }} + CODER_OIDC_SIGN_IN_TEXT: {{ .Values.oidc.signInText | quote }} + CODER_OIDC_ICON_URL: {{ .Values.oidc.iconUrl | quote }} + {{- if .Values.oidc.scopes }} + CODER_OIDC_SCOPES: {{ join "," .Values.oidc.scopes | quote }} + {{- end }} + CODER_OIDC_EMAIL_DOMAIN: {{ .Values.oidc.emailDomain | quote }} + {{- end }} + + # GitHub OAuth2 + {{- if .Values.oauth2.enabled }} + CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE: {{ .Values.oauth2.defaultProviderEnable | quote }} + CODER_OAUTH2_GITHUB_CLIENT_ID: {{ .Values.oauth2.clientId | quote }} + CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS: {{ .Values.oauth2.allowSignups | quote }} + CODER_OAUTH2_GITHUB_DEVICE_FLOW: {{ .Values.oauth2.deviceFlow | quote }} + CODER_OAUTH2_GITHUB_ALLOW_EVERYONE: {{ eq (len .Values.oauth2.allowedOrgs) 0 | quote }} + {{- if .Values.oauth2.allowedOrgs }} + CODER_OAUTH2_GITHUB_ALLOWED_ORGS: {{ join "," .Values.oauth2.allowedOrgs | quote }} + {{- end }} + {{- else }} + CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE: "false" + {{- end }} + + # External auth providers + {{- range $i, $auth := .Values.externalAuth }} + CODER_EXTERNAL_AUTH_{{ $i }}_ID: {{ $auth.id | quote }} + CODER_EXTERNAL_AUTH_{{ $i }}_TYPE: {{ $auth.type | quote }} + CODER_EXTERNAL_AUTH_{{ $i }}_CLIENT_ID: {{ $auth.clientId | quote }} + {{- if $auth.authUrl }} + CODER_EXTERNAL_AUTH_{{ $i }}_AUTH_URL: {{ $auth.authUrl | quote }} + {{- end }} + {{- if $auth.tokenUrl }} + CODER_EXTERNAL_AUTH_{{ $i }}_TOKEN_URL: {{ $auth.tokenUrl | quote }} + {{- end }} + {{- if $auth.revokeUrl }} + CODER_EXTERNAL_AUTH_{{ $i }}_REVOKE_URL: {{ $auth.revokeUrl | quote }} + {{- end }} + {{- if $auth.validateUrl }} + CODER_EXTERNAL_AUTH_{{ $i }}_VALIDATE_URL: {{ $auth.validateUrl | quote }} + {{- end }} + {{- if $auth.regex }} + CODER_EXTERNAL_AUTH_{{ $i }}_REGEX: {{ $auth.regex | quote }} + {{- end }} + {{- end }} + + # AI Bridge + CODER_AIBRIDGE_ENABLED: {{ .Values.aibridge.enabled | quote }} + CODER_AIBRIDGE_STRUCTURED_LOGGING: {{ .Values.aibridge.structuredLogging | quote }} + {{- if .Values.aibridge.anthropic.enabled }} + CODER_AIBRIDGE_ANTHROPIC_BASE_URL: {{ .Values.aibridge.anthropic.url | quote }} + {{- end }} + {{- if .Values.aibridge.openai.enabled }} + CODER_AIBRIDGE_OPENAI_BASE_URL: {{ .Values.aibridge.openai.url | quote }} + {{- end }} + {{- if .Values.aibridge.bedrock.enabled }} + CODER_AIBRIDGE_BEDROCK_BASE_URL: {{ .Values.aibridge.bedrock.url | quote }} + CODER_AIBRIDGE_BEDROCK_REGION: {{ .Values.aibridge.bedrock.region | quote }} + CODER_AIBRIDGE_BEDROCK_ACCESS_KEY: {{ .Values.aibridge.bedrock.accessKey | quote }} + CODER_AIBRIDGE_BEDROCK_MODEL: {{ .Values.aibridge.bedrock.model | quote }} + CODER_AIBRIDGE_BEDROCK_SMALL_FAST_MODEL: {{ .Values.aibridge.bedrock.fastModel | quote }} + {{- end }} + + # Extra env vars + {{- range $k, $v := .Values.extraEnvVars }} + {{ $k }}: {{ $v | quote }} + {{- end }} diff --git a/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/namespace.yaml b/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/namespace.yaml new file mode 100644 index 0000000..f601e25 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/namespace.yaml @@ -0,0 +1,8 @@ +{{- if .Values.createNamespace }} +apiVersion: v1 +kind: Namespace +metadata: + name: {{ include "coder.namespace" . }} + labels: + {{- include "coder.labels" . | nindent 4 }} +{{- end }} diff --git a/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/pdb.yaml b/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/pdb.yaml new file mode 100644 index 0000000..e409fcf --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/pdb.yaml @@ -0,0 +1,14 @@ +{{- if .Values.pdb.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ .Release.Name }} + namespace: {{ include "coder.namespace" . }} + labels: + {{- include "coder.labels" . | nindent 4 }} +spec: + maxUnavailable: {{ .Values.pdb.maxUnavailable }} + selector: + matchLabels: + {{- include "coder.upstreamSelectorLabels" . | nindent 6 }} +{{- end }} diff --git a/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/secrets.yaml b/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/secrets.yaml new file mode 100644 index 0000000..67e856f --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/secrets.yaml @@ -0,0 +1,42 @@ +{{/* +Aggregate Secret containing all sensitive CODER_* environment variables. +Referenced by the upstream coder chart via coder.envFrom. +Individual per-key secrets are also created (matching the TF module pattern) +for any external consumers that reference them by name. +*/}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-env-secrets + namespace: {{ include "coder.namespace" . }} + labels: + {{- include "coder.labels" . | nindent 4 }} +type: Opaque +stringData: + {{- if .Values.db.url }} + {{- if eq .Values.db.pgAuth "awsiamrds" }} + CODER_PG_CONNECTION_URL: {{ printf "postgresql://%s@%s/%s" .Values.db.username .Values.db.url .Values.db.name | quote }} + {{- else }} + CODER_PG_CONNECTION_URL: {{ printf "postgresql://%s:%s@%s/%s" .Values.db.username .Values.db.password .Values.db.url .Values.db.name | quote }} + {{- end }} + {{- end }} + {{- if and .Values.oidc.enabled .Values.oidc.clientSecret }} + CODER_OIDC_CLIENT_SECRET: {{ .Values.oidc.clientSecret | quote }} + {{- end }} + {{- if and .Values.oauth2.enabled .Values.oauth2.clientSecret }} + CODER_OAUTH2_GITHUB_CLIENT_SECRET: {{ .Values.oauth2.clientSecret | quote }} + {{- end }} + {{- range $i, $auth := .Values.externalAuth }} + {{- if $auth.clientSecret }} + CODER_EXTERNAL_AUTH_{{ $i }}_CLIENT_SECRET: {{ $auth.clientSecret | quote }} + {{- end }} + {{- end }} + {{- if and .Values.aibridge.anthropic.enabled .Values.aibridge.anthropic.key }} + CODER_AIBRIDGE_ANTHROPIC_KEY: {{ .Values.aibridge.anthropic.key | quote }} + {{- end }} + {{- if and .Values.aibridge.openai.enabled .Values.aibridge.openai.key }} + CODER_AIBRIDGE_OPENAI_KEY: {{ .Values.aibridge.openai.key | quote }} + {{- end }} + {{- if and .Values.aibridge.bedrock.enabled .Values.aibridge.bedrock.secretKey }} + CODER_AIBRIDGE_BEDROCK_ACCESS_KEY_SECRET: {{ .Values.aibridge.bedrock.secretKey | quote }} + {{- end }} diff --git a/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/service.yaml b/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/service.yaml new file mode 100644 index 0000000..0c95d06 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-server/chart/templates/service.yaml @@ -0,0 +1,47 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} + namespace: {{ include "coder.namespace" . }} + labels: + {{- include "coder.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + {{- if and (eq .Values.service.type "LoadBalancer") .Values.service.loadBalancerClass }} + loadBalancerClass: {{ .Values.service.loadBalancerClass }} + {{- end }} + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + - name: https + port: 443 + protocol: TCP + targetPort: {{ if .Values.tls.enabled }}https{{ else }}http{{ end }} + selector: + {{- include "coder.upstreamSelectorLabels" . | nindent 4 }} +--- +{{- if .Values.prometheus.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-prometheus + namespace: {{ include "coder.namespace" . }} + labels: + {{- include "coder.labels" . | nindent 4 }} +spec: + type: ClusterIP + clusterIP: None + ports: + - name: http + protocol: TCP + port: 2112 + targetPort: 2112 + selector: + {{- include "coder.upstreamSelectorLabels" . | nindent 4 }} +{{- end }} diff --git a/infra/aws/us-east-2/k8s/argo/coder-server/chart/values.yaml b/infra/aws/us-east-2/k8s/argo/coder-server/chart/values.yaml new file mode 100644 index 0000000..078b3de --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-server/chart/values.yaml @@ -0,0 +1,212 @@ +# -- Namespace to deploy into. +namespace: coder +createNamespace: true + +# --------------------------------------------------------------------------- +# Wrapper-level configuration (translated into coder.env for the subchart) +# --------------------------------------------------------------------------- + +# -- Coder access URL (e.g. https://ai.coder.com). +accessUrl: "" +# -- Coder wildcard access URL (e.g. https://*.ai.coder.com). +wildcardUrl: "" + +# -- Comma-separated list of experiments to enable. +experiments: "" + +provisionerDaemons: 0 +provisionerForceCancelInterval: "10m0s" +quietHoursDefaultSchedule: "CRON_TZ=America/Los_Angeles 50 23 * * *" +allowCustomQuietHours: true + +terraformDebugMode: true +traceLogs: true +traceEnable: true +logFilter: ".*" +swaggerEnable: true +updateCheck: true +cliUpgradeMessage: true + +# -- Arbitrary extra CODER_* env vars. Keys are the env-var name. +extraEnvVars: {} +# CODER_EXPERIMENTS: "multi-organization" + +# --------------------------------------------------------------------------- +# TLS / cert-manager certificate +# --------------------------------------------------------------------------- +tls: + enabled: false + # -- Name of the TLS secret. When certificate.enabled=true this is also + # used as the Certificate secretName. + secretName: "" + +certificate: + enabled: false + commonName: "" + dnsNames: [] + duration: "2160h" # 90 days + renewBefore: "360h" # 15 days + issuerRef: + kind: ClusterIssuer + name: issuer + privateKey: + rotationPolicy: Never + algorithm: RSA + encoding: PKCS1 + size: 2048 + +# --------------------------------------------------------------------------- +# Database +# --------------------------------------------------------------------------- +db: + # -- Full host:port endpoint. + url: "" + username: "" + password: "" + name: coder + # -- Authentication mode: "password" or "awsiamrds". + pgAuth: password + +# --------------------------------------------------------------------------- +# OIDC +# --------------------------------------------------------------------------- +oidc: + enabled: false + issuerUrl: "" + clientId: "" + clientSecret: "" + signInText: "" + iconUrl: "" + scopes: [] + emailDomain: "" + +# --------------------------------------------------------------------------- +# GitHub OAuth2 +# --------------------------------------------------------------------------- +oauth2: + enabled: false + defaultProviderEnable: false + allowSignups: true + deviceFlow: false + allowedOrgs: [] + clientId: "" + clientSecret: "" + +# --------------------------------------------------------------------------- +# External auth providers +# --------------------------------------------------------------------------- +externalAuth: [] +# - id: primary-github +# type: github +# clientId: "" +# clientSecret: "" + +# --------------------------------------------------------------------------- +# AI Bridge +# --------------------------------------------------------------------------- +aibridge: + enabled: false + structuredLogging: true + anthropic: + enabled: false + url: "" + key: "" + openai: + enabled: false + url: "" + key: "" + bedrock: + enabled: false + url: "" + region: "" + accessKey: "" + secretKey: "" + model: "global.anthropic.claude-sonnet-4-5-20250929-v1:0" + fastModel: "global.anthropic.claude-haiku-4-5-20251001-v1:0" + +# --------------------------------------------------------------------------- +# Prometheus metrics +# --------------------------------------------------------------------------- +prometheus: + enabled: false + collectAgentStats: true + collectDbMetrics: true + +# --------------------------------------------------------------------------- +# Service (managed outside the upstream chart, matching the TF module) +# --------------------------------------------------------------------------- +service: + type: LoadBalancer + loadBalancerClass: "service.k8s.aws/nlb" + annotations: {} + +# --------------------------------------------------------------------------- +# Pod Disruption Budget +# --------------------------------------------------------------------------- +pdb: + enabled: false + maxUnavailable: 1 + +# --------------------------------------------------------------------------- +# Upstream coder subchart overrides +# +# The subchart is named "coder" and its values live under ".Values.coder". +# Inside the subchart the values are nested under ".Values.coder.*", so from +# the parent chart we address them as "coder.coder.*". +# +# Non-secret env vars are loaded from a ConfigMap (-env) via envFrom. +# Secret env vars are loaded from a Secret (-env-secrets) via envFrom. +# Both resources are created by this wrapper chart's templates. +# --------------------------------------------------------------------------- +coder: + coder: + image: + repo: ghcr.io/coder/coder + tag: latest + pullPolicy: IfNotPresent + pullSecrets: [] + + # Additional env entries beyond what envFrom provides. + env: [] + + # Load all CODER_* variables from the wrapper-managed ConfigMap and Secret. + # IMPORTANT: The names below must match the template output names. If you + # change the release name from "coder", update these accordingly. + envFrom: + - configMapRef: + name: coder-env + - secretRef: + name: coder-env-secrets + envUseClusterAccessURL: false + + # Disable the upstream chart's Service — we manage our own. + service: + enable: false + + tls: + secretNames: [] + + replicaCount: 1 + + resources: + requests: + cpu: "500m" + memory: "2Gi" + limits: + cpu: "2" + memory: "2Gi" + + serviceAccount: + annotations: {} + name: coder + + annotations: {} + podAnnotations: {} + + tolerations: [] + nodeSelector: {} + affinity: {} + topologySpreadConstraints: [] + + # NOTE: terminationGracePeriodSeconds is not configurable in the + # upstream coder chart v2.25.1 (hardcoded to 60s). diff --git a/infra/aws/us-east-2/k8s/argo/coder-server/main.tf b/infra/aws/us-east-2/k8s/argo/coder-server/main.tf new file mode 100644 index 0000000..b570007 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-server/main.tf @@ -0,0 +1,343 @@ +provider "aws" { + region = var.region + profile = var.profile +} + +data "aws_region" "this" {} + +data "aws_caller_identity" "this" {} + +data "aws_eks_cluster" "this" { + name = var.cluster_name +} + +data "aws_eks_cluster_auth" "this" { + name = var.cluster_name +} + +data "aws_iam_openid_connect_provider" "this" { + url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer +} + +data "aws_db_instance" "coder" { + db_instance_identifier = var.coder_db_rds_name +} + +data "aws_vpc" "this" { + tags = { + Name = var.vpc_name + } +} + +provider "kubernetes" { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token +} + +# --------------------------------------------------------------------------- +# Locals +# --------------------------------------------------------------------------- + +locals { + region = data.aws_region.this.region + account_id = data.aws_caller_identity.this.account_id + azs = var.azs + pub_subs = [for az in local.azs : "${var.vpc_name}-public-${local.region}${az}"] + release_name = "coder" + namespace = "coder" + rds_db_name = split(".", data.aws_db_instance.coder.endpoint)[0] + + common_name = trimprefix(trimprefix(var.coder_access_url, "https://"), "http://") + wildcard_name = trimprefix(trimprefix(var.coder_wildcard_access_url, "https://"), "http://") + ssl_vol_friendly_name = replace(local.common_name, ".", "-") +} + +# --------------------------------------------------------------------------- +# Elastic IPs for NLB +# --------------------------------------------------------------------------- + +resource "aws_eip" "coder" { + count = length(local.pub_subs) + domain = "vpc" + public_ipv4_pool = "amazon" + tags = { + Name = "coder-eip-${count.index}" + } +} + +# --------------------------------------------------------------------------- +# IRSA: Provisioner policy (EC2 permissions for Terraform provisioners) +# --------------------------------------------------------------------------- + +data "aws_iam_policy_document" "provisioner" { + statement { + effect = "Allow" + actions = [ + "ec2:GetDefaultCreditSpecification", + "ec2:DescribeIamInstanceProfileAssociations", + "ec2:DescribeTags", + "ec2:DescribeInstances", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceStatus", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:DescribeInstanceCreditSpecifications", + "ec2:DescribeImages", + "ec2:ModifyDefaultCreditSpecification", + "ec2:DescribeVolumes" + ] + resources = ["*"] + } + + statement { + effect = "Allow" + actions = [ + "ec2:DescribeInstanceAttribute", + "ec2:UnmonitorInstances", + "ec2:TerminateInstances", + "ec2:StartInstances", + "ec2:StopInstances", + "ec2:DeleteTags", + "ec2:MonitorInstances", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:ModifyInstanceAttribute", + "ec2:ModifyInstanceCreditSpecification" + ] + resources = ["arn:aws:ec2:*:*:instance/*"] + } +} + +module "provisioner-policy" { + count = var.coder_builtin_provisioner_count == 0 ? 0 : 1 + + source = "../../../../../../modules/security/policy" + name = "coder-srv" + path = "/${var.cluster_name}/${local.region}/" + description = "Coder Terraform External Provisioner Policy" + policy_json = data.aws_iam_policy_document.provisioner.json +} + +# --------------------------------------------------------------------------- +# IRSA: RDS IAM auth policy +# --------------------------------------------------------------------------- + +data "aws_iam_policy_document" "rds" { + statement { + effect = "Allow" + actions = ["rds-db:connect"] + resources = [ + "arn:aws:rds-db:${local.region}:${local.account_id}:dbuser:${data.aws_db_instance.coder.resource_id}/${var.coder_db_username}" + ] + } +} + +module "rds-policy" { + source = "../../../../../../modules/security/policy" + name = "coder-srv-${local.rds_db_name}" + path = "/${var.cluster_name}/${local.region}/" + description = "Coder DB IAM Access Policy" + policy_json = data.aws_iam_policy_document.rds.json +} + +# --------------------------------------------------------------------------- +# IRSA: OIDC role for the Coder ServiceAccount +# --------------------------------------------------------------------------- + +module "provisioner-oidc-role" { + source = "../../../../../../modules/security/role/access-entry" + name = "coder-srv" + path = "/${var.cluster_name}/${local.region}/" + cluster_name = var.cluster_name + policy_arns = merge({ + "AmazonEC2ReadOnlyAccess" = "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" + "CoderRDSDBPolicy" = module.rds-policy.policy_arn + }, var.coder_builtin_provisioner_count == 0 ? {} : { + "TFProvisionerPolicy" = module.provisioner-policy[0].policy_arn + }) + cluster_policy_arns = {} + oidc_principals = { + "${data.aws_iam_openid_connect_provider.this.arn}" = ["system:serviceaccount:*:*"] + } +} + +# --------------------------------------------------------------------------- +# ArgoCD Application — deploys the local wrapper Helm chart +# --------------------------------------------------------------------------- + +resource "argocd_application" "coder" { + metadata { + name = local.release_name + namespace = "argocd" + } + + spec { + project = "default" + + destination { + server = "https://kubernetes.default.svc" + namespace = local.namespace + } + + source { + repo_url = "https://helm.coder.com/v2" + chart = "coder" + target_revision = var.addon_version + + helm { + release_name = local.release_name + + values = yamlencode({ + # Wrapper-level values + namespace = local.namespace + createNamespace = true + + accessUrl = var.coder_access_url + wildcardUrl = var.coder_wildcard_access_url + experiments = join(",", var.coder_experiments) + + provisionerDaemons = var.coder_builtin_provisioner_count + + tls = { + enabled = true + secretName = local.ssl_vol_friendly_name + } + + certificate = { + enabled = true + commonName = local.common_name + dnsNames = [local.common_name, local.wildcard_name] + } + + db = { + url = data.aws_db_instance.coder.endpoint + username = var.coder_db_username + password = var.coder_db_password + name = var.coder_db_name + pgAuth = "awsiamrds" + } + + oidc = { + enabled = true + issuerUrl = var.coder_oidc_secret_issuer_url + clientId = var.coder_oidc_secret_client_id + clientSecret = var.coder_oidc_secret_client_secret + signInText = var.oidc_sign_in_text + iconUrl = var.oidc_icon_url + scopes = var.oidc_scopes + emailDomain = var.oidc_email_domain + } + + oauth2 = { + enabled = true + defaultProviderEnable = false + allowSignups = true + deviceFlow = false + allowedOrgs = var.coder_github_allowed_orgs + clientId = var.coder_oauth_secret_client_id + clientSecret = var.coder_oauth_secret_client_secret + } + + externalAuth = [{ + id = "primary-github" + type = "github" + clientId = var.coder_github_external_auth_secret_client_id + clientSecret = var.coder_github_external_auth_secret_client_secret + }] + + aibridge = { + enabled = true + } + + prometheus = { + enabled = true + } + + service = { + type = "LoadBalancer" + loadBalancerClass = "service.k8s.aws/nlb" + annotations = { + "service.beta.kubernetes.io/aws-load-balancer-nlb-target-type" = "ip" + "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing" + "service.beta.kubernetes.io/aws-load-balancer-attributes" = "deletion_protection.enabled=false,load_balancing.cross_zone.enabled=true" + "service.beta.kubernetes.io/aws-load-balancer-eip-allocations" = join(",", aws_eip.coder[*].allocation_id) + "service.beta.kubernetes.io/aws-load-balancer-subnets" = join(",", local.pub_subs) + } + } + + coder = { + coder = { + image = { + repo = var.image_repo + tag = var.image_tag + } + replicaCount = length(local.pub_subs) + resources = { + requests = { cpu = "500m", memory = "2Gi" } + limits = { cpu = "2", memory = "2Gi" } + } + serviceAccount = { + annotations = { + "eks.amazonaws.com/role-arn" = module.provisioner-oidc-role.role_arn + } + name = local.release_name + } + service = { + enable = false + } + tls = { + secretNames = [local.ssl_vol_friendly_name] + } + tolerations = [{ + key = "platform" + value = "coder-server" + effect = "NoSchedule" + }] + affinity = { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [ + { + key = "topology.kubernetes.io/zone" + operator = "In" + values = [for az in local.azs : "${local.region}${az}"] + }, + { + key = "node.coder.io/used-for" + operator = "In" + values = ["coder-server"] + } + ] + }] + } + } + podAntiAffinity = { + preferredDuringSchedulingIgnoredDuringExecution = [] + requiredDuringSchedulingIgnoredDuringExecution = [] + } + } + } + } + }) + } + } + + sync_policy { + # automated { + # prune = true + # self_heal = true + # } + # sync_options = ["CreateNamespace=true"] + # retry { + # limit = "5" + # backoff { + # duration = "30s" + # max_duration = "2m" + # factor = "2" + # } + # } + } + } +} diff --git a/infra/aws/us-east-2/k8s/argo/coder-server/variables.tf b/infra/aws/us-east-2/k8s/argo/coder-server/variables.tf new file mode 100644 index 0000000..41ca65d --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-server/variables.tf @@ -0,0 +1,147 @@ +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "azs" { + type = list(string) + default = ["a", "c"] +} + +variable "vpc_name" { + type = string +} + +variable "cluster_name" { + type = string +} + +variable "addon_version" { + type = string + default = "2.25.1" +} + +variable "coder_access_url" { + type = string +} + +variable "coder_wildcard_access_url" { + type = string +} + +variable "coder_experiments" { + type = list(string) + default = [] +} + +variable "coder_github_allowed_orgs" { + type = list(string) + default = [] +} + +variable "coder_builtin_provisioner_count" { + type = number + default = 0 +} + +variable "coder_github_external_auth_secret_client_secret" { + type = string + sensitive = true +} + +variable "coder_github_external_auth_secret_client_id" { + type = string + sensitive = true +} + +variable "coder_oauth_secret_client_secret" { + type = string + sensitive = true +} + +variable "coder_oauth_secret_client_id" { + type = string + sensitive = true +} + +variable "coder_oidc_secret_client_secret" { + type = string + sensitive = true +} + +variable "coder_oidc_secret_client_id" { + type = string + sensitive = true +} + +variable "coder_oidc_secret_issuer_url" { + type = string + sensitive = true +} + +variable "coder_db_rds_name" { + type = string +} + +variable "coder_db_username" { + type = string +} + +variable "coder_db_password" { + type = string + sensitive = true +} + +variable "coder_db_name" { + type = string +} + +variable "image_repo" { + type = string + sensitive = true +} + +variable "image_tag" { + type = string + default = "latest" +} + +variable "oidc_sign_in_text" { + type = string +} + +variable "oidc_icon_url" { + type = string +} + +variable "oidc_scopes" { + type = list(string) +} + +variable "oidc_email_domain" { + type = string +} + +variable "anthropic_llm_endpoint" { + type = string + sensitive = true +} + +variable "anthropic_llm_key" { + type = string + sensitive = true +} + +variable "openai_llm_endpoint" { + type = string + sensitive = true +} + +variable "openai_llm_key" { + type = string + sensitive = true +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/Chart.lock b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/Chart.lock new file mode 100644 index 0000000..9b6d067 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: libcoder + repository: file://charts/libcoder + version: 0.1.0 +digest: sha256:8788012975e83c0a94502ae0cca1159feb805460431b6dcbbdbd57c4a2d29e1b +generated: "2026-06-02T06:34:10.952532379Z" \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/Chart.yaml b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/Chart.yaml new file mode 100644 index 0000000..536bd18 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +appVersion: 2.34.5 +dependencies: +- name: libcoder + repository: file://charts/libcoder + version: 0.1.0 +description: External provisioner daemon for Coder. This is an Enterprise feature; + contact sales@coder.com. +home: https://github.com/coder/coder +icon: https://helm.coder.com/coder_logo_black.png +keywords: +- coder +- terraform +kubeVersion: '>= 1.19.0-0' +maintainers: +- email: support@coder.com + name: Coder Technologies, Inc. + url: https://coder.com/contact +name: coder-provisioner +sources: +- https://github.com/coder/coder/tree/main/helm/provisioner +type: application +version: 2.34.5 \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/README.md b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/README.md new file mode 100644 index 0000000..a5d6ffd --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/README.md @@ -0,0 +1,149 @@ +# Coder Helm Chart + +This directory contains the Helm chart used to deploy Coder provisioner daemons onto a Kubernetes +cluster. + +External provisioner daemons are a Premium feature. Contact sales@coder.com. + +## Getting Started + +> **Warning**: The main branch in this repository does not represent the +> latest release of Coder. Please reference our installation docs for +> instructions on a tagged release. + +View +[our docs](https://coder.com/docs/admin/provisioners) +for detailed installation instructions. + +## Values + +Please refer to [values.yaml](values.yaml) for available Helm values and their +defaults. + +A good starting point for your values file is: + +```yaml +coder: + env: + - name: CODER_URL + value: "https://coder.example.com" + # This env enables the Prometheus metrics endpoint. + - name: CODER_PROMETHEUS_ADDRESS + value: "0.0.0.0:2112" + replicaCount: 10 +provisionerDaemon: + keySecretName: "coder-provisionerd-key" + keySecretKey: "provisionerd-key" +``` + +## Specific Examples + +Below are some common specific use-cases when deploying a Coder provisioner. + +### Set Labels and Annotations + +If you need to set deployment- or pod-level labels and annotations, set `coder.{annotations,labels}` or `coder.{podAnnotations,podLabels}`. + +Example: + +```yaml +coder: + # ... + annotations: + com.coder/annotation/foo: bar + com.coder/annotation/baz: qux + labels: + com.coder/label/foo: bar + com.coder/label/baz: qux + podAnnotations: + com.coder/podAnnotation/foo: bar + com.coder/podAnnotation/baz: qux + podLabels: + com.coder/podLabel/foo: bar + com.coder/podLabel/baz: qux +``` + +### Additional Templates + +You can include extra Kubernetes manifests in `extraTemplates`. + +The below example will also create a `ConfigMap` along with the Helm release: + +```yaml +coder: + # ... +provisionerDaemon: + # ... +extraTemplates: + - | + apiVersion: v1 + kind: ConfigMap + metadata: + name: some-config + namespace: {{ .Release.Namespace }} + data: + key: some-value +``` + +### Disable Service Account Creation + +### Deploying multiple provisioners in the same namespace + +To deploy multiple provisioners in the same namespace, set the following values explicitly to avoid conflicts: + +- `nameOverride`: controls the name of the provisioner deployment +- `serviceAccount.name`: controls the name of the service account. + +Note that `nameOverride` does not apply to `extraTemplates`, as illustrated below: + +```yaml +coder: + # ... + serviceAccount: + name: other-coder-provisioner +provisionerDaemon: + # ... +nameOverride: "other-coder-provisioner" +extraTemplates: + - | + apiVersion: v1 + kind: ConfigMap + metadata: + name: some-other-config + namespace: {{ .Release.Namespace }} + data: + key: some-other-value +``` + +If you wish to deploy a second provisioner that references an existing service account, you can do so as follows: + +- Set `coder.serviceAccount.disableCreate=true` to disable service account creation, +- Set `coder.serviceAccount.workspacePerms=false` to disable creation of a role and role binding, +- Set `coder.serviceAccount.nameOverride` to the name of an existing service account. + +See below for a concrete example: + +```yaml +coder: + # ... + serviceAccount: + name: preexisting-service-account + disableCreate: true + workspacePerms: false +provisionerDaemon: + # ... +nameOverride: "other-coder-provisioner" +``` + +## Testing + +The test suite for this chart lives in `./tests/chart_test.go`. + +Each test case runs `helm template` against the corresponding `test_case.yaml`, and compares the output with that of the corresponding `test_case.golden` in `./tests/testdata`. +If `expectedError` is not empty for that specific test case, no corresponding `.golden` file is required. + +To add a new test case: + +- Create an appropriately named `.yaml` file in `testdata/` along with a corresponding `.golden` file, if required. +- Add the test case to the array in `chart_test.go`, setting `name` to the name of the files you added previously (without the extension). If appropriate, set `expectedError`. +- Run the tests and ensure that no regressions in existing test cases occur: `go test ./tests`. \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/Chart.yaml b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/Chart.yaml new file mode 100644 index 0000000..9b692ad --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +appVersion: 0.1.0 +description: Coder library chart +home: https://github.com/coder/coder +maintainers: +- email: support@coder.com + name: Coder Technologies, Inc. + url: https://coder.com/contact +name: libcoder +type: library +version: 0.1.0 \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/templates/_coder.yaml b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/templates/_coder.yaml new file mode 100644 index 0000000..71d38f6 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/templates/_coder.yaml @@ -0,0 +1,201 @@ +{{- define "libcoder.deployment.tpl" -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "coder.name" .}} + namespace: {{ .Release.Namespace }} + labels: + {{- include "coder.labels" . | nindent 4 }} + {{- with .Values.coder.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + annotations: {{ toYaml .Values.coder.annotations | nindent 4}} +spec: + replicas: {{ .Values.coder.replicaCount }} + selector: + matchLabels: + {{- include "coder.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "coder.labels" . | nindent 8 }} + {{- with .Values.coder.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + annotations: + {{- include "coder.componentAnnotation" . | nindent 8 }} + {{- with .Values.coder.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + + spec: + serviceAccountName: {{ .Values.coder.serviceAccount.name | quote }} + {{- with .Values.coder.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with .Values.coder.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + restartPolicy: Always + {{- with .Values.coder.image.pullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + terminationGracePeriodSeconds: 60 + {{- with .Values.coder.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.coder.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.coder.nodeSelector }} + nodeSelector: + {{ toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.coder.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.coder.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.coder.initContainers }} + initContainers: + {{ toYaml . | nindent 8 }} + {{- end }} + containers: [] + {{- include "coder.volumes" . | nindent 6 }} +{{- end -}} +{{- define "libcoder.deployment" -}} +{{- include "libcoder.util.merge" (append . "libcoder.deployment.tpl") -}} +{{- end -}} + +{{- define "libcoder.statefulset.tpl" -}} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "coder.name" .}} + namespace: {{ .Release.Namespace }} + labels: + {{- include "coder.labels" . | nindent 4 }} + {{- with .Values.coder.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + annotations: {{ toYaml .Values.coder.annotations | nindent 4}} +spec: + serviceName: {{ include "coder.name" . }} + replicas: {{ .Values.coder.replicaCount }} + selector: + matchLabels: + {{- include "coder.selectorLabels" . | nindent 6 }} + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + {{- include "coder.labels" . | nindent 8 }} + {{- with .Values.coder.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + annotations: + {{- include "coder.componentAnnotation" . | nindent 8 }} + {{- with .Values.coder.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + + spec: + serviceAccountName: {{ .Values.coder.serviceAccount.name | quote }} + {{- with .Values.coder.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with .Values.coder.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + restartPolicy: Always + {{- with .Values.coder.image.pullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + terminationGracePeriodSeconds: 60 + {{- with .Values.coder.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.coder.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.coder.nodeSelector }} + nodeSelector: + {{ toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.coder.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.coder.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.coder.initContainers }} + initContainers: + {{ toYaml . | nindent 8 }} + {{- end }} + containers: [] + {{- include "coder.volumes" . | nindent 6 }} + {{- with .Values.coder.volumeClaimTemplates }} + volumeClaimTemplates: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end -}} +{{- define "libcoder.statefulset" -}} +{{- include "libcoder.util.merge" (append . "libcoder.statefulset.tpl") -}} +{{- end -}} + +{{- define "libcoder.containerspec.tpl" -}} +name: coder +image: {{ include "coder.image" . | quote }} +imagePullPolicy: {{ .Values.coder.image.pullPolicy }} +command: + {{- toYaml .Values.coder.command | nindent 2 }} +resources: + {{- if and (hasKey .Values.coder "resources") (not (empty .Values.coder.resources)) }} + {{- toYaml .Values.coder.resources | nindent 2 }} + {{- else }} + limits: + cpu: 2000m + memory: 4096Mi + requests: + cpu: 2000m + memory: 4096Mi + {{- end }} +lifecycle: + {{- toYaml .Values.coder.lifecycle | nindent 2 }} +securityContext: {{ toYaml .Values.coder.securityContext | nindent 2 }} +{{ include "coder.volumeMounts" . }} +{{- end -}} +{{- define "libcoder.containerspec" -}} +{{- include "libcoder.util.merge" (append . "libcoder.containerspec.tpl") -}} +{{- end -}} + +{{- define "libcoder.serviceaccount.tpl" -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.coder.serviceAccount.name | quote }} + namespace: {{ .Release.Namespace }} + annotations: {{ toYaml .Values.coder.serviceAccount.annotations | nindent 4 }} + labels: + {{- include "coder.labels" . | nindent 4 }} + {{- with .Values.coder.serviceAccount.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end -}} +{{- define "libcoder.serviceaccount" -}} +{{- include "libcoder.util.merge" (append . "libcoder.serviceaccount.tpl") -}} +{{- end -}} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/templates/_helpers.tpl b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/templates/_helpers.tpl new file mode 100644 index 0000000..4999ec4 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/templates/_helpers.tpl @@ -0,0 +1,248 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "coder.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "coder.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Selector labels + +!!!!! DO NOT ADD ANY MORE SELECTORS. IT IS A BREAKING CHANGE !!!!! +*/}} +{{- define "coder.selectorLabels" -}} +app.kubernetes.io/name: {{ include "coder.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "coder.labels" -}} +helm.sh/chart: {{ include "coder.chart" . }} +{{ include "coder.selectorLabels" . }} +app.kubernetes.io/part-of: {{ include "coder.name" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Coder Docker image URI +*/}} +{{- define "coder.image" -}} +{{- if and (eq .Values.coder.image.tag "") (eq .Chart.AppVersion "0.1.0") -}} +{{ fail "You must specify the coder.image.tag value if you're installing the Helm chart directly from Git." }} +{{- end -}} +{{ .Values.coder.image.repo }}:{{ .Values.coder.image.tag | default (printf "v%v" .Chart.AppVersion) }} +{{- end }} + +{{/* +Coder TLS enabled. +*/}} +{{- define "coder.tlsEnabled" -}} + {{- if hasKey .Values.coder "tls" -}} + {{- if .Values.coder.tls.secretNames -}} + true + {{- else -}} + false + {{- end -}} + {{- else -}} + false + {{- end -}} +{{- end }} + +{{/* +Coder TLS environment variables. +*/}} +{{- define "coder.tlsEnv" }} +{{- if eq (include "coder.tlsEnabled" .) "true" }} +- name: CODER_TLS_ENABLE + value: "true" +- name: CODER_TLS_ADDRESS + value: "0.0.0.0:8443" +- name: CODER_TLS_CERT_FILE + value: "{{ range $idx, $secretName := .Values.coder.tls.secretNames -}}{{ if $idx }},{{ end }}/etc/ssl/certs/coder/{{ $secretName }}/tls.crt{{- end }}" +- name: CODER_TLS_KEY_FILE + value: "{{ range $idx, $secretName := .Values.coder.tls.secretNames -}}{{ if $idx }},{{ end }}/etc/ssl/certs/coder/{{ $secretName }}/tls.key{{- end }}" +{{- end }} +{{- end }} + +{{/* +Coder default access URL +*/}} +{{- define "coder.defaultAccessURL" }} +{{- if eq (include "coder.tlsEnabled" .) "true" -}} +https +{{- else -}} +http +{{- end -}} +://coder.{{ .Release.Namespace }}.svc.cluster.local +{{- end }} + +{{/* +Coder volume definitions. +*/}} +{{- define "coder.volumeList" }} +{{- if hasKey .Values.coder "tls" -}} +{{- range $secretName := .Values.coder.tls.secretNames }} +- name: "tls-{{ $secretName }}" + secret: + secretName: {{ $secretName | quote }} +{{ end -}} +{{- end }} +{{ range $secret := .Values.coder.certs.secrets -}} +- name: "ca-cert-{{ $secret.name }}" + secret: + secretName: {{ $secret.name | quote }} +{{ end -}} +{{ if gt (len .Values.coder.volumes) 0 -}} +{{ toYaml .Values.coder.volumes }} +{{ end -}} +{{- end }} + +{{/* +Coder volumes yaml. +*/}} +{{- define "coder.volumes" }} +{{- if trim (include "coder.volumeList" .) -}} +volumes: +{{- include "coder.volumeList" . -}} +{{- else -}} +volumes: [] +{{- end -}} +{{- end }} + +{{/* +Coder volume mounts. +*/}} +{{- define "coder.volumeMountList" }} +{{- if hasKey .Values.coder "tls" }} +{{ range $secretName := .Values.coder.tls.secretNames -}} +- name: "tls-{{ $secretName }}" + mountPath: "/etc/ssl/certs/coder/{{ $secretName }}" + readOnly: true +{{ end -}} +{{- end }} +{{ range $secret := .Values.coder.certs.secrets -}} +- name: "ca-cert-{{ $secret.name }}" + mountPath: "/etc/ssl/certs/{{ $secret.name }}.crt" + subPath: {{ $secret.key | quote }} + readOnly: true +{{ end -}} +{{ if gt (len .Values.coder.volumeMounts) 0 -}} +{{ toYaml .Values.coder.volumeMounts }} +{{ end -}} +{{- end }} + +{{/* +Coder volume mounts yaml. +*/}} +{{- define "coder.volumeMounts" }} +{{- if trim (include "coder.volumeMountList" .) -}} +volumeMounts: +{{- include "coder.volumeMountList" . -}} +{{- else -}} +volumeMounts: [] +{{- end -}} +{{- end }} + +{{/* +Coder ingress wildcard hostname with the wildcard suffix stripped. +*/}} +{{- define "coder.ingressWildcardHost" -}} +{{/* This regex replace is required as the original input including the suffix + * is not a legal ingress host. We need to remove the suffix and keep the + * wildcard '*'. + * + * - '\\*' Starts with '*' + * - '[^.]*' Suffix is 0 or more characters, '-suffix' + * - '(' Start domain capture group + * - '\\.' The domain should be separated with a '.' from the subdomain + * - '.*' Rest of the domain. + * - ')' $1 is the ''.example.com' + */}} +{{- regexReplaceAll "\\*[^.]*(\\..*)" .Values.coder.ingress.wildcardHost "*${1}" -}} +{{- end }} + +{{/* +Fail on fully deprecated values or deprecated value combinations. This is +included at the top of coder.yaml. +*/}} +{{- define "coder.verifyDeprecated" }} +{{/* +Deprecated value coder.tls.secretName must not be used. +*/}} +{{- if .Values.coder.tls.secretName }} +{{ fail "coder.tls.secretName is deprecated, use coder.tls.secretNames instead." }} +{{- end }} +{{- end }} + +{{/* +Renders a value that contains a template. +Usage: +{{ include "coder.renderTemplate" ( dict "value" .Values.path.to.the.Value "context" $) }} +*/}} +{{- define "coder.renderTemplate" -}} + {{- if typeIs "string" .value }} + {{- tpl .value .context }} + {{- else }} + {{- tpl (.value | toYaml) .context }} + {{- end }} +{{- end -}} + +{{- define "libcoder.rbac.rules.basic" -}} +- apiGroups: [""] + resources: ["pods"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +- apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +{{- end }} + +{{- define "libcoder.rbac.rules.deployments" -}} +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +{{- end }} + +{{/* +Component annotation for pod metadata. +This should be overridden in each chart to specify the component type. +*/}} +{{- define "coder.componentAnnotation" -}} +{{- end }} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/templates/_rbac.yaml b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/templates/_rbac.yaml new file mode 100644 index 0000000..e8e0a98 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/templates/_rbac.yaml @@ -0,0 +1,91 @@ +{{- define "libcoder.rbac.forNamespace" -}} + {{- $nsPerms := ternary .workspacePerms .Top.Values.coder.serviceAccount.workspacePerms (hasKey . "workspacePerms") -}} + {{- $nsDeployRaw := ternary .enableDeployments .Top.Values.coder.serviceAccount.enableDeployments (hasKey . "enableDeployments") -}} + {{- $nsExtraRaw := ternary .extraRules .Top.Values.coder.serviceAccount.extraRules (hasKey . "extraRules") -}} + {{- $nsDeploy := and $nsPerms $nsDeployRaw -}} + {{- $nsExtra := ternary $nsExtraRaw (list) $nsPerms -}} + + {{- if or $nsPerms (or $nsDeploy $nsExtra) }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Top.Values.coder.serviceAccount.name }}-workspace-perms + namespace: {{ .NS }} +rules: +{{- if $nsPerms }} +{{ include "libcoder.rbac.rules.basic" .Top | trimPrefix "\n" | indent 2 }} +{{- end }} +{{- if $nsDeploy }} +{{ include "libcoder.rbac.rules.deployments" .Top | trimPrefix "\n" | indent 2 }} +{{- end }} +{{- if $nsExtra }} + {{- if kindIs "slice" $nsExtra }} +{{ toYaml $nsExtra | trimPrefix "\n" | indent 2 }} + {{- else }} +{{ toYaml (list $nsExtra) | trimPrefix "\n" | indent 2 }} + {{- end }} +{{- end }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Top.Values.coder.serviceAccount.name | quote }} + namespace: {{ .NS }} +subjects: + - kind: ServiceAccount + name: {{ .Top.Values.coder.serviceAccount.name | quote }} + {{- if ne .NS .Top.Release.Namespace }} + namespace: {{ .Top.Release.Namespace }} + {{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Top.Values.coder.serviceAccount.name }}-workspace-perms + {{- end }} +{{- end -}} + +{{- define "libcoder.rbac.core" -}} + {{- $top := . -}} + {{- $rootPerms := $top.Values.coder.serviceAccount.workspacePerms | default false -}} + {{- $rootDeploy := $top.Values.coder.serviceAccount.enableDeployments | default false -}} + {{- $rootExtra := $top.Values.coder.serviceAccount.extraRules | default (list) -}} + + {{- $rootParams := dict + "Top" $top + "NS" $top.Release.Namespace + "workspacePerms" $rootPerms + "enableDeployments" $rootDeploy + "extraRules" $rootExtra -}} + {{ include "libcoder.rbac.forNamespace" $rootParams }} + + {{- $wsnsRaw := get $top.Values.coder.serviceAccount "workspaceNamespaces" -}} + {{- $extra := default (list) $wsnsRaw -}} + + {{- range $_, $ns := $extra }} + {{- $nsName := ternary $ns.name $ns (kindIs "map" $ns) -}} + {{- if $nsName }} + {{- $params := dict "Top" $top "NS" $nsName -}} + {{- if kindIs "map" $ns }} + {{- if hasKey $ns "workspacePerms" }}{{- $_ := set $params "workspacePerms" $ns.workspacePerms }}{{- else }}{{- $_ := set $params "workspacePerms" $rootPerms }}{{- end }} + {{- if hasKey $ns "enableDeployments" }}{{- $_ := set $params "enableDeployments" $ns.enableDeployments }}{{- else }}{{- $_ := set $params "enableDeployments" $rootDeploy }}{{- end }} + {{- if hasKey $ns "extraRules" }}{{- $_ := set $params "extraRules" $ns.extraRules }}{{- else }}{{- $_ := set $params "extraRules" $rootExtra }}{{- end }} + {{- else }} + {{- $_ := set $params "workspacePerms" $rootPerms -}} + {{- $_ := set $params "enableDeployments" $rootDeploy -}} + {{- $_ := set $params "extraRules" $rootExtra -}} + {{- end }} + {{ include "libcoder.rbac.forNamespace" $params }} + {{- end }} + {{- end }} +{{- end -}} + +{{- define "libcoder.rbac.tpl" -}} + {{- if not .Values.coder.serviceAccount.disableCreate -}} + {{ include "libcoder.rbac.core" . }} + {{- end }} +{{- end -}} + +{{- define "libcoder.namespace.rbac.tpl" -}} + {{ include "libcoder.rbac.core" . }} +{{- end -}} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/templates/_util.yaml b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/templates/_util.yaml new file mode 100644 index 0000000..d1b2742 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/charts/libcoder/templates/_util.yaml @@ -0,0 +1,13 @@ +{{- /* + libcoder.util.merge will merge two YAML templates and output the result. + This takes an array of three values: + - the top context + - the template name of the overrides (destination) + - the template name of the base (source) +*/}} +{{- define "libcoder.util.merge" -}} +{{- $top := first . -}} +{{- $overrides := fromYaml (include (index . 1) $top) | default (dict ) -}} +{{- $tpl := fromYaml (include (index . 2) $top) | default (dict ) -}} +{{- toYaml (merge $overrides $tpl) -}} +{{- end -}} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/NOTES.txt b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/NOTES.txt new file mode 100644 index 0000000..fab4573 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/NOTES.txt @@ -0,0 +1,12 @@ +{{/* +Deprecation notices: +*/}} + +{{- if .Values.provisionerDaemon.pskSecretName }} +* Provisioner Daemon PSKs are no longer recommended for use with external + provisioners. Consider migrating to scoped provisioner keys instead. For more + information, see: https://coder.com/docs/admin/provisioners#authentication +{{- end }} + +Enjoy Coder! Please create an issue at https://github.com/coder/coder if you run +into any problems! :) \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/_coder.tpl b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/_coder.tpl new file mode 100644 index 0000000..d0ba44f --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/_coder.tpl @@ -0,0 +1,108 @@ +{{/* +Service account to merge into the libcoder template +*/}} +{{- define "coder.serviceaccount" -}} +{{- end }} + +{{/* +Component annotation for pod metadata. +*/}} +{{- define "coder.componentAnnotation" -}} +app.kubernetes.io/component: provisionerd +{{- end }} + +{{/* +StatefulSet to merge into the libcoder template +*/}} +{{- define "coder.statefulset" -}} +spec: + template: + spec: + terminationGracePeriodSeconds: {{ .Values.provisionerDaemon.terminationGracePeriodSeconds }} + containers: + - +{{ include "libcoder.containerspec" (list . "coder.containerspec") | indent 8}} + +{{- end }} + +{{/* +ContainerSpec for the Coder container of the Coder provisioner daemon +*/}} +{{- define "coder.containerspec" -}} +args: +{{- if .Values.coder.commandArgs }} + {{- toYaml .Values.coder.commandArgs | nindent 12 }} +{{- else }} +- provisionerd +- start +{{- end }} +env: +- name: CODER_PROMETHEUS_ADDRESS + value: "0.0.0.0:2112" +{{- if and (empty .Values.provisionerDaemon.pskSecretName) (empty .Values.provisionerDaemon.keySecretName) }} +{{ fail "Either provisionerDaemon.pskSecretName or provisionerDaemon.keySecretName must be specified." }} +{{- else if and .Values.provisionerDaemon.keySecretName .Values.provisionerDaemon.keySecretKey }} + {{- if and (not (empty .Values.provisionerDaemon.pskSecretName)) (ne .Values.provisionerDaemon.pskSecretName "coder-provisioner-psk") }} + {{ fail "Either provisionerDaemon.pskSecretName or provisionerDaemon.keySecretName must be specified, but not both." }} + {{- else if .Values.provisionerDaemon.tags }} + {{ fail "provisionerDaemon.tags may not be specified with provisionerDaemon.keySecretName." }} + {{- end }} +- name: CODER_PROVISIONER_DAEMON_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.provisionerDaemon.keySecretName | quote }} + key: {{ .Values.provisionerDaemon.keySecretKey | quote }} +{{- else }} +- name: CODER_PROVISIONER_DAEMON_PSK + valueFrom: + secretKeyRef: + name: {{ .Values.provisionerDaemon.pskSecretName | quote }} + key: psk +{{- end }} +{{- if include "provisioner.tags" . }} +- name: CODER_PROVISIONERD_TAGS + value: {{ include "provisioner.tags" . }} +{{- end }} + # Set the default access URL so a `helm apply` works by default. + # See: https://github.com/coder/coder/issues/5024 +{{- $hasAccessURL := false }} +{{- range .Values.coder.env }} +{{- if eq .name "CODER_URL" }} +{{- $hasAccessURL = true }} +{{- end }} +{{- end }} +{{- if not $hasAccessURL }} +- name: CODER_URL + value: {{ include "coder.defaultAccessURL" . | quote }} +{{- end }} +{{- with .Values.coder.env }} +{{ toYaml . }} +{{- end }} +ports: + {{- range .Values.coder.env }} + {{- if eq .name "CODER_PROMETHEUS_ENABLE" }} + {{/* + This sadly has to be nested to avoid evaluating the second part + of the condition too early and potentially getting type errors if + the value is not a string (like a `valueFrom`). We do not support + `valueFrom` for this env var specifically. + */}} + {{- if eq .value "true" }} +- name: "prometheus-http" + containerPort: 2112 + protocol: TCP + {{- end }} + {{- end }} + {{- end }} +{{- end }} + +{{/* +Convert provisioner tags to the environment variable format +*/}} +{{- define "provisioner.tags" -}} + {{- $keys := keys .Values.provisionerDaemon.tags | sortAlpha -}} + {{- range $i, $key := $keys -}} + {{- $val := get $.Values.provisionerDaemon.tags $key -}} + {{- if ne $i 0 -}},{{- end -}}{{ $key }}={{ $val }} + {{- end -}} +{{- end -}} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/coder.yaml b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/coder.yaml new file mode 100644 index 0000000..796950e --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/coder.yaml @@ -0,0 +1,20 @@ +--- +{{- if not .Values.coder.serviceAccount.disableCreate }} +{{ include "libcoder.serviceaccount" (list . "coder.serviceaccount") }} +{{- end }} + +--- +{{ include "libcoder.statefulset" (list . "coder.statefulset") }} + +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "coder.name" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "coder.labels" . | nindent 4 }} +spec: + clusterIP: None + selector: + {{- include "coder.selectorLabels" . | nindent 4 }} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/extra-templates.yaml b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/extra-templates.yaml new file mode 100644 index 0000000..3d31333 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/extra-templates.yaml @@ -0,0 +1,4 @@ +{{- range .Values.extraTemplates }} +--- +{{ include "coder.renderTemplate" (dict "value" . "context" $) }} +{{- end }} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/rbac.yaml b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/rbac.yaml new file mode 100644 index 0000000..1ddef21 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/templates/rbac.yaml @@ -0,0 +1 @@ +{{ include "libcoder.rbac.tpl" . }} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/values.yaml b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/values.yaml new file mode 100644 index 0000000..042e5c9 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/charts/coder-provisioner/values.yaml @@ -0,0 +1,260 @@ +# coder -- Common configuration options. +coder: + # coder.env -- The environment variables to set. These can be used to + # configure all aspects of Coder provisioner daemon. Please see + # `coder provisionerd start --help for information about what environment + # variables can be set. + # Note: The following environment variables are set by default and cannot be + # overridden: + # - CODER_PROMETHEUS_ADDRESS: set to 0.0.0.0:2112 and cannot be changed. + # Prometheus must still be enabled by setting CODER_PROMETHEUS_ENABLE. + # + # We will additionally set CODER_URL, if unset, to the cluster service + # URL. + env: [] + # - name: "CODER_URL" + # value: "https://coder.example.com" + + # coder.image -- The image to use for Coder provisioner daemon. + image: + # coder.image.repo -- The repository of the image. + repo: "ghcr.io/coder/coder" + # coder.image.tag -- The tag of the image, defaults to {{.Chart.AppVersion}} + # if not set. If you're using the chart directly from git, the default + # app version will not work and you'll need to set this value. The helm + # chart helpfully fails quickly in this case. + tag: "" + # coder.image.pullPolicy -- The pull policy to use for the image. See: + # https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy + pullPolicy: IfNotPresent + # coder.image.pullSecrets -- The secrets used for pulling the Coder image from + # a private registry. + pullSecrets: [] + # - name: "pull-secret" + + # coder.initContainers -- Init containers for the deployment. See: + # https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ + initContainers: + [] + # - name: init-container + # image: busybox:1.28 + # command: ['sh', '-c', "sleep 2"] + + # coder.annotations -- The StatefulSet annotations. See: + # https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + annotations: {} + + # coder.labels -- The StatefulSet labels. See: + # https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + labels: {} + + # coder.podAnnotations -- The Coder pod annotations. See: + # https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + # The annotation `app.kubernetes.io/component` is automatically added to identify + # the component type (coderd, wsproxy, or provisionerd). + podAnnotations: {} + + # coder.podLabels -- The Coder pod labels. See: + # https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + podLabels: {} + + # coder.serviceAccount -- Configuration for the automatically created service + # account. Creation of the service account cannot be disabled. + serviceAccount: + # coder.serviceAccount.workspacePerms -- Whether or not to grant the + # service account permissions to manage workspaces. This includes + # permission to manage pods and persistent volume claims in the deployment + # namespace. + # + # It is recommended to keep this on if you are using Kubernetes templates + # within Coder. + workspacePerms: true + # coder.serviceAccount.enableDeployments -- Provides the service account permission + # to manage Kubernetes deployments. + enableDeployments: true + # coder.serviceAccount.annotations -- The Coder service account annotations. + annotations: {} + # coder.serviceAccount.name -- The service account name + name: coder-provisioner + # coder.serviceAccount.disableCreate -- Whether to create the service account or use existing service account. + disableCreate: false + + # coder.securityContext -- Fields related to the container's security + # context (as opposed to the pod). Some fields are also present in the pod + # security context, in which case these values will take precedence. + securityContext: + # coder.securityContext.runAsNonRoot -- Requires that the coder container + # runs as an unprivileged user. If setting runAsUser to 0 (root), this + # will need to be set to false. + runAsNonRoot: true + # coder.securityContext.runAsUser -- Sets the user id of the container. + # For security reasons, we recommend using a non-root user. + runAsUser: 1000 + # coder.securityContext.runAsGroup -- Sets the group id of the container. + # For security reasons, we recommend using a non-root group. + runAsGroup: 1000 + # coder.securityContext.readOnlyRootFilesystem -- Mounts the container's + # root filesystem as read-only. + readOnlyRootFilesystem: null + # coder.securityContext.seccompProfile -- Sets the seccomp profile for + # the coder container. + seccompProfile: + type: RuntimeDefault + # coder.securityContext.allowPrivilegeEscalation -- Controls whether + # the container can gain additional privileges, such as escalating to + # root. It is recommended to leave this setting disabled in production. + allowPrivilegeEscalation: false + + # coder.volumes -- A list of extra volumes to add to the Coder provisioner daemon pod. + volumes: [] + # - name: "my-volume" + # emptyDir: {} + + # coder.volumeMounts -- A list of extra volume mounts to add to the Coder provisioner daemon pod. + volumeMounts: [] + # - name: "my-volume" + # mountPath: "/mnt/my-volume" + + # coder.replicaCount -- The number of StatefulSet replicas. This + # should only be increased if High Availability is enabled. + # + # This is an Enterprise feature. Contact sales@coder.com. + replicaCount: 1 + + # coder.volumeClaimTemplates -- Volume claim templates for the StatefulSet. + # Each provisioner pod gets its own persistent volume, useful for caching + # Terraform providers, modules, etc. + volumeClaimTemplates: [] + # - metadata: + # name: provisioner-cache + # spec: + # accessModes: ["ReadWriteOnce"] + # resources: + # requests: + # storage: 10Gi + + # coder.lifecycle -- container lifecycle handlers for the Coder container, allowing + # for lifecycle events such as postStart and preStop events + # See: https://kubernetes.io/docs/tasks/configure-pod-container/attach-handler-lifecycle-event/ + lifecycle: + {} + # postStart: + # exec: + # command: ["/bin/sh", "-c", "echo postStart"] + # preStop: + # exec: + # command: ["/bin/sh","-c","echo preStart"] + + # coder.resources -- The resources to request for Coder. These are optional + # and are not set by default. + resources: + {} + # limits: + # cpu: 2000m + # memory: 4096Mi + # requests: + # cpu: 2000m + # memory: 4096Mi + + # coder.certs -- CA bundles to mount inside the Coder pod. + certs: + # coder.certs.secrets -- A list of CA bundle secrets to mount into the + # pod. The secrets should exist in the same namespace as the Helm + # deployment. + # + # The given key in each secret is mounted at + # `/etc/ssl/certs/{secret_name}.crt`. + secrets: + [] + # - name: "my-ca-bundle" + # key: "ca-bundle.crt" + + # coder.affinity -- Allows specifying an affinity rule for the deployment. + affinity: + {} + # podAntiAffinity: + # preferredDuringSchedulingIgnoredDuringExecution: + # - podAffinityTerm: + # labelSelector: + # matchExpressions: + # - key: app.kubernetes.io/instance + # operator: In + # values: + # - "coder" + # topologyKey: kubernetes.io/hostname + # weight: 1 + + # coder.tolerations -- Tolerations for tainted nodes. + # See: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ + tolerations: + [] + # - key: "key" + # operator: "Equal" + # value: "value" + # effect: "NoSchedule" + + # coder.nodeSelector -- Node labels for constraining coder pods to nodes. + # See: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector + nodeSelector: {} + # kubernetes.io/os: linux + + # coder.command -- The command to use when running the container. Used + # for customizing the location of the `coder` binary in your image. + command: + - /opt/coder + + # coder.commandArgs -- Set arguments for the entrypoint command of the Coder pod. + commandArgs: [] + +# provisionerDaemon -- Provisioner Daemon configuration options +provisionerDaemon: + # provisionerDaemon.pskSecretName -- The name of the Kubernetes secret that contains the + # Pre-Shared Key (PSK) to use to authenticate with Coder. The secret must be + # in the same namespace as the Helm deployment, and contain an item called + # "psk" which contains the pre-shared key. + # NOTE: We no longer recommend using PSKs. Please consider using provisioner + # keys instead. They have a number of benefits, including the ability to + # rotate them easily. + pskSecretName: "coder-provisioner-psk" + + # provisionerDaemon.keySecretName -- The name of the Kubernetes + # secret that contains a provisioner key to use to authenticate with Coder. + # See: https://coder.com/docs/admin/provisioners#authentication + # NOTE: it is not permitted to specify both provisionerDaemon.keySecretName + # and provisionerDaemon.pskSecretName. An exception is made for the purposes + # of backwards-compatibility: if provisionerDaemon.pskSecretName is unchanged + # from the default value and provisionerDaemon.keySecretName is set, then + # provisionerDaemon.keySecretName and provisionerDaemon.keySecretKey will take + # precedence over provisionerDaemon.pskSecretName. + keySecretName: "" + # provisionerDaemon.keySecretKey -- The key of the Kubernetes + # secret specified in provisionerDaemon.keySecretName that contains + # the provisioner key. Defaults to "key". + keySecretKey: "key" + + # provisionerDaemon.tags -- If using a PSK, specify the set of provisioner + # job tags for which this provisioner daemon is responsible. + # See: https://coder.com/docs/admin/provisioners#provisioner-tags + # NOTE: it is not permitted to specify both provisionerDaemon.tags and + # provsionerDaemon.keySecretName. + tags: + {} + # location: usa + # provider: kubernetes + + # provisionerDaemon.terminationGracePeriodSeconds -- Time in seconds that Kubernetes should wait before forcibly + # terminating the provisioner daemon. You should set this to be longer than your longest expected build time so that + # redeployments do not interrupt builds in progress. + terminationGracePeriodSeconds: 600 + +# extraTemplates -- Array of extra objects to deploy with the release. Strings +# are evaluated as a template and can use template expansions and functions. All +# other objects are used as yaml. +extraTemplates: + #- | + # apiVersion: v1 + # kind: ConfigMap + # metadata: + # name: my-configmap + # data: + # key: {{ .Values.myCustomValue | quote }} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/main.tf b/infra/aws/us-east-2/k8s/argo/coder-workspace/main.tf new file mode 100644 index 0000000..f48c153 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/main.tf @@ -0,0 +1,541 @@ +provider "aws" { + region = var.region + profile = var.profile +} + +data "aws_region" "this" {} + +data "aws_eks_cluster" "this" { + name = var.cluster_name +} + +data "aws_eks_cluster_auth" "this" { + name = var.cluster_name +} + +data "aws_iam_openid_connect_provider" "this" { + url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer +} + +data "http" "login" { + url = "${var.coder_access_url}/api/v2/users/login" + method = "POST" + request_headers = { + Accept = "application/json" + } + request_body = jsonencode({ + email = var.coder_admin_email + password = var.coder_admin_password + }) + + retry { + attempts = 5 + min_delay_ms = (5 * 1000) # 5 seconds + } +} + +provider "helm" { + kubernetes = { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token + } +} + +provider "argocd" { + kubernetes { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token + } +} + +provider "kubernetes" { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token +} + +provider "coderd" { + url = var.coder_access_url + token = jsondecode(data.http.login.response_body).session_token +} + +locals { + release_name = "coder" + chart_name = "coder-provisioner" + namespace = "coder" + + node_selector = {} + topology_spread = [] + # topology_spread = [{ + # max_skew = 2 + # topology_key = "kubernetes.io/hostname" + # when_unsatisfiable = "ScheduleAnyway" + # label_selector = { + # match_labels = { + # "app.kubernetes.io/name" = local.chart_name + # "app.kubernetes.io/part-of" = local.chart_name + # } + # } + # match_label_keys = [ + # "app.kubernetes.io/instance" + # ] + # }] + tolerations = [{ + key = "coder" + operator = "Exists" + values = ["provisioner"] + }] + affinity = { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [ + { + key = "node.coder.io/used-for", + operator = "In", + values = ["coder-provisioner"] + } + ] + }] + } + } + podAntiAffinity = {} + # podAntiAffinity = { + # preferredDuringSchedulingIgnoredDuringExecution = [{ + # weight = 100 + # podAffinityTerm = { + # labelSelector = { + # match_labels = { + # "app.kubernetes.io/name" = local.chart_name + # "app.kubernetes.io/part-of" = local.chart_name + # } + # } + # topologyKey = "kubernetes.io/hostname" + # } + # }] + # } + } +} + +# module "default-ws" { + +# source = "../../../../../modules/k8s/bootstrap/coder-provisioner" + +# release_name = local.release_name +# chart_version = var.addon_version +# chart_name = local.chart_name +# cluster_name = data.aws_eks_cluster.this.id +# cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.this.arn + +# namespace = "coder-ws" + +# coder = { +# access_url = var.coder_access_url +# org_name = "coder" +# image_repo = var.image_repo +# image_tag = var.image_tag +# rep_cnt = 1 +# ws_extra_rules = [{ +# apiGroups = [""] +# resources = ["configmaps"] +# verbs = [ +# "create", +# "delete", +# "deletecollection", +# "get", +# "list", +# "patch", +# "update", +# "watch" +# ] +# }] +# env_vars = { +# CODER_PROMETHEUS_ENABLE = "true" +# CODER_PROMETHEUS_COLLECT_AGENT_STATS = "true" +# CODER_PROMETHEUS_COLLECT_DB_METRICS = "true" +# # TF_VAR_namespace = "coder-ws" +# } +# } + +# svc_acc = { +# create = true +# name = "coder" +# } + +# node_selector = local.node_selector +# tolerations = local.tolerations +# topology_spread = local.topology_spread +# affinity = local.affinity +# } + +# module "experiment-ws" { + +# source = "../../../../../modules/k8s/bootstrap/coder-provisioner" + +# release_name = local.release_name +# chart_version = var.addon_version +# chart_name = local.chart_name +# cluster_name = var.cluster_name +# cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.this.arn + +# namespace = "coder-ws-experiment" + +# coder = { +# access_url = var.coder_access_url +# org_name = "experiment" +# image_repo = var.image_repo +# image_tag = var.image_tag +# rep_cnt = 1 +# ws_extra_rules = [{ +# apiGroups = [""] +# resources = ["configmaps"] +# verbs = [ +# "create", +# "delete", +# "deletecollection", +# "get", +# "list", +# "patch", +# "update", +# "watch" +# ] +# },{ +# apiGroups = [""] +# resources = ["serviceaccounts"] +# verbs = [ +# "create", +# "delete", +# "deletecollection", +# "get", +# "list", +# "patch", +# "update", +# "watch" +# ] +# },{ +# apiGroups = ["rbac.authorization.k8s.io"] +# resources = ["clusterrolebindings"] +# verbs = [ +# "create", +# "delete", +# "deletecollection", +# "get", +# "list", +# "patch", +# "update", +# "watch" +# ] +# }] +# env_vars = { +# CODER_PROMETHEUS_ENABLE = "true" +# CODER_PROMETHEUS_COLLECT_AGENT_STATS = "true" +# CODER_PROMETHEUS_COLLECT_DB_METRICS = "true" +# # TF_VAR_namespace = "coder-ws-experiment" +# } +# } + +# svc_acc = { +# create = true +# name = "coder" +# } + +# node_selector = local.node_selector +# tolerations = local.tolerations +# topology_spread = local.topology_spread +# affinity = local.affinity +# } + +data "aws_iam_policy_document" "eks" { + statement { + effect = "Allow" + actions = [ + "eks:*", + "iam:*" + ] + resources = ["*"] + } +} + +module "eks-admin-policy" { + source = "../../../../../../modules/security/policy" + name = "demo-ws-eks-policy" + path = "/" + description = "Coder External Demo-Provisioner Policy (EKS)" + policy_json = data.aws_iam_policy_document.eks.json +} + +# module "demo-ws" { + +# source = "../../../../../modules/k8s/bootstrap/coder-provisioner" + +# release_name = local.release_name +# chart_version = var.addon_version +# chart_name = local.chart_name +# cluster_name = var.cluster_name +# cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.this.arn + +# namespace = "coder-ws-demo" + +# coder = { +# access_url = var.coder_access_url +# org_name = "demo" +# image_repo = var.image_repo +# image_tag = var.image_tag +# rep_cnt = 1 +# ws_extra_rules = [{ +# apiGroups = [""] +# resources = ["configmaps"] +# verbs = [ +# "create", +# "delete", +# "deletecollection", +# "get", +# "list", +# "patch", +# "update", +# "watch" +# ] +# },{ +# apiGroups = [""] +# resources = ["serviceaccounts"] +# verbs = [ +# "create", +# "delete", +# "deletecollection", +# "get", +# "list", +# "patch", +# "update", +# "watch" +# ] +# },{ +# apiGroups = ["rbac.authorization.k8s.io"] +# resources = ["clusterrolebindings"] +# verbs = [ +# "create", +# "delete", +# "deletecollection", +# "get", +# "list", +# "patch", +# "update", +# "watch" +# ] +# }] +# env_vars = { +# CODER_PROMETHEUS_ENABLE = "true" +# CODER_PROMETHEUS_COLLECT_AGENT_STATS = "true" +# CODER_PROMETHEUS_COLLECT_DB_METRICS = "true" +# # TF_VAR_namespace = "coder-ws-experiment" +# } +# } + +# svc_acc = { +# create = true +# name = "coder" +# iam_policy_arns = { +# "EKSAdminPolicy" = module.eks-admin-policy.policy_arn +# } +# } + +# node_selector = local.node_selector +# tolerations = local.tolerations +# topology_spread = local.topology_spread +# affinity = local.affinity +# } + +locals { + # coder-provisioner-values = yamlencode({ + # coder = { + # image = { + # repo = var.coder.image_repo + # tag = var.coder.image_tag + # pullPolicy = var.coder.image_pull_policy + # pullSecrets = var.coder.image_pull_secrets + # } + # serviceAccount = { + # workspacePerms = true + # enableDeployments = true + # name = var.svc_acc.name + # disableCreate = !var.svc_acc.create + # workspaceNamespaces = [ for v in var.coder.ws_ns : { name = v } ] + # extraRules = var.coder.ws_extra_rules + # annotations = merge({ + # "eks.amazonaws.com/role-arn" = module.oidc-role.role_arn + # }, var.svc_acc.annots) + # } + # podAnnotations = { + # "prometheus.io/scrape" = "true" + # "prometheus.io/port" = "2112" + # } + # env = [ + # for k, v in merge({ + # CODER_URL = var.coder.access_url + # }, var.coder.env_vars) : { name = k, value = v } + # ] + # volumeClaimTemplates = [{ + # metadata = { + # name = "cache" + # } + # spec = { + # accessModes = ["ReadWriteOnce"] + # storageClassName = "gp3-automode" + # resources = { + # requests = { + # storage = "10Gi" + # } + # } + # } + # }] + # # volumes = [{ + # # name = "cache" + # # persistentVolumeClaim = { + # # claimName = kubernetes_persistent_volume_claim_v1.cache.metadata[0].name + # # readOnly = false + # # } + # # }] + # volumeMounts = [{ + # mountPath = "/home/coder/.cache/coder" + # name = "cache" + # readOnly = false + # }] + # podSecurityContext = { + # fsGroup = 1000 + # } + # securityContext = { + # runAsNonRoot = true + # runAsUser = 1000 + # runAsGroup = 1000 + # readOnlyRootFilesystem = null + # seccompProfile = { + # type = "RuntimeDefault" + # } + # allowPrivilegeEscalation = false + # } + # resources = { + # requests = var.rsrc_req + # limits = var.rsrc_lim + # } + # nodeSelector = {} # var.node_selector + # replicaCount = # var.coder.rep_cnt + # tolerations = # var.tolerations + # topologySpreadConstraints = local.topology_spread + # affinity = var.affinity + # } + # provisionerDaemon = { + # keySecretKey = kubernetes_secret_v1.ext-prov.metadata[0].annotations["custom.kubernetes.secret/key"] + # keySecretName = kubernetes_secret_v1.ext-prov.metadata[0].name + # terminationGracePeriodSeconds = 600 + # } + # }) +} + +resource "argocd_application_set" "coder-workspaces" { + metadata { + name = "coder-workspaces" + namespace = "argocd" + labels = {} + annotations = {} + } + spec { + generator { + list { + elements = [{ + name = "coder-ws" + namespace = "coder-ws-test" + repoURL = "https://github.com/coder/ai.coder.com.git" + path = "infra/aws/us-east-2/k8s/argo/coder-ws/charts/coder-provisioner" + chart = "coder-provisioner" + revision = "main" + values = yamlencode({}) + template = yamlencode({ + spec = { + sync_policy = { + sync_options = ["CreateNamespace=true"] + } + } + }) + },{ + name = "coder-ws-experiment" + namespace = "coder-ws-experiment-test" + repoURL = "https://github.com/coder/ai.coder.com.git" + path = "infra/aws/us-east-2/k8s/argo/coder-ws/charts/coder-provisioner" + chart = "coder-provisioner" + revision = "main" + values = yamlencode({}) + template = yamlencode({ + spec = { + sync_policy = { + sync_options = ["CreateNamespace=true"] + } + } + }) + },{ + name = "coder-ws-demo" + namespace = "coder-ws-demo-test" + repoURL = "https://github.com/coder/ai.coder.com.git" + path = "infra/aws/us-east-2/k8s/argo/coder-ws/charts/coder-provisioner" + chart = "coder-provisioner" + revision = "main" + values = yamlencode({}) + template = yamlencode({ + spec = { + sync_policy = { + sync_options = ["CreateNamespace=true"] + } + } + }) + },{ + name = "coder-logstream-kube" + namespace = "coder-logstream-kube-test" + repoURL = "https://github.com/coder/ai.coder.com.git" + path = "infra/aws/us-east-2/k8s/argo/coder-ws/charts/coder-provisioner" + chart = "coder-logstream-kube" + revision = "main" + values = yamlencode({ + url = var.coder_access_url + namespaces = ["coder-ws", "coder-ws-demo", "coder-ws-experiment"] + image = { + repo = "ghcr.io/coder/coder-logstream-kube" + tag = "v0.0.15" + pullPolicy = "IfNotPresent" + } + + nodeSelector = {} # var.node_selector + affinity = {} # var.affinity + tolerations = {} # var.tolerations + }) + template = yamlencode({ + spec = { + sync_policy = { + sync_options = ["CreateNamespace=true"] + } + } + }) + }] + } + } + + template { + metadata { + name = "{{name}}" + } + spec { + source { + repo_url = "{{repoURL}}" + path = "{{path}}" + target_revision = "{{version}}" + } + # repo_url = "{{repoURL}}" + # chart = "{{chart}}" + # target_revision = "{{version}}" + destination { + server = "https://kubernetes.default.svc" + namespace = "{{namespace}}" + } + } + } + } +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/terragrunt.hcl b/infra/aws/us-east-2/k8s/argo/coder-workspace/terragrunt.hcl new file mode 100644 index 0000000..2577291 --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/terragrunt.hcl @@ -0,0 +1,29 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../../eks", + "../../coder-server", + "../../kyverno", + "../../other" # Deploy's auxillary manifests + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region=include.root.locals.CODER_AWS_REGION + + coder_access_url=include.root.locals.CODER_DOMAIN_NAME + coder_admin_email=include.root.locals.CODER_EMAIL + coder_admin_password=include.root.locals.CODER_PASSWORD + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + image_repo=include.root.locals.CODER_IMAGE_REPO + image_tag=include.root.locals.CODER_IMAGE_TAG + addon_version=include.root.locals.CODER_ADDON_VERSION + logstream_addon_version=include.root.locals.CODER_LOGSTREAM_ADDON_VERSION +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/argo/coder-workspace/variables.tf b/infra/aws/us-east-2/k8s/argo/coder-workspace/variables.tf new file mode 100644 index 0000000..edc2d5d --- /dev/null +++ b/infra/aws/us-east-2/k8s/argo/coder-workspace/variables.tf @@ -0,0 +1,45 @@ +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "cluster_name" { + type = string +} + +variable "addon_version" { + type = string + default = "2.23.0" +} + +variable "logstream_addon_version" { + type = string + default = "0.0.11" +} + +variable "coder_access_url" { + type = string +} + +variable "image_repo" { + type = string + sensitive = true +} + +variable "image_tag" { + type = string + default = "latest" +} + +variable "coder_admin_email" { + type = string +} + +variable "coder_admin_password" { + type = string + sensitive = true +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/cert-manager/main.tf b/infra/aws/us-east-2/k8s/cert-manager/main.tf index c095843..226cbec 100644 --- a/infra/aws/us-east-2/k8s/cert-manager/main.tf +++ b/infra/aws/us-east-2/k8s/cert-manager/main.tf @@ -1,59 +1,6 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - kubernetes = { - source = "hashicorp/kubernetes" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "cluster_oidc_provider_arn" { - type = string -} - -variable "addon_namespace" { - type = string - default = "cert-manager" -} - -variable "addon_version" { - type = string - default = "v1.18.2" -} - -variable "cloudflare_api_token" { - type = string - sensitive = true -} - -variable "cloudflare_email" { - type = string - sensitive = true -} - provider "aws" { - region = var.cluster_region - profile = var.cluster_profile + region = var.region + profile = var.profile } data "aws_eks_cluster" "this" { @@ -64,6 +11,10 @@ data "aws_eks_cluster_auth" "this" { name = var.cluster_name } +data "aws_iam_openid_connect_provider" "this" { + url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer +} + provider "helm" { kubernetes = { host = data.aws_eks_cluster.this.endpoint @@ -79,12 +30,30 @@ provider "kubernetes" { } module "cert-manager" { - source = "../../../../../modules/k8s/bootstrap/cert-manager" - cluster_name = var.cluster_name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - namespace = var.addon_namespace - helm_version = var.addon_version - cloudflare_token_secret = var.cloudflare_api_token - cloudflare_token_secret_email = var.cloudflare_email + source = "../../../../../modules/k8s/bootstrap/cert-manager" + + cluster_name = var.cluster_name + cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.this.arn + + namespace = var.addon_namespace + helm_version = var.addon_version + + tolerations = [{ + key = "CriticalAddonsOnly" + operator = "Exists" + }] + affinity = { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [{ + key = "karpenter.sh/nodepool" + operator = "In" + values = ["system"] + }] + }] + } + } + } } \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/cert-manager/terragrunt.hcl b/infra/aws/us-east-2/k8s/cert-manager/terragrunt.hcl new file mode 100644 index 0000000..27da9ba --- /dev/null +++ b/infra/aws/us-east-2/k8s/cert-manager/terragrunt.hcl @@ -0,0 +1,20 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks" + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region=include.root.locals.CODER_AWS_REGION + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + addon_namespace=include.root.locals.CRTMGR_ADDON_NAMESPACE + addon_version=include.root.locals.CRTMGR_ADDON_VERSION +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/cert-manager/variables.tf b/infra/aws/us-east-2/k8s/cert-manager/variables.tf new file mode 100644 index 0000000..f93b94c --- /dev/null +++ b/infra/aws/us-east-2/k8s/cert-manager/variables.tf @@ -0,0 +1,22 @@ +variable "cluster_name" { + type = string +} + +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "addon_namespace" { + type = string + default = "cert-manager" +} + +variable "addon_version" { + type = string + default = "v1.18.2" +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/coder-server/main.tf b/infra/aws/us-east-2/k8s/coder-server/main.tf index 10da024..006555a 100644 --- a/infra/aws/us-east-2/k8s/coder-server/main.tf +++ b/infra/aws/us-east-2/k8s/coder-server/main.tf @@ -1,305 +1,324 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - kubernetes = { - source = "hashicorp/kubernetes" - } - coderd = { - source = "coder/coderd" - } - acme = { - source = "vancluever/acme" - } - tls = { - source = "hashicorp/tls" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "cluster_oidc_provider_arn" { - type = string -} - -variable "acme_server_url" { - type = string - default = "https://acme-v02.api.letsencrypt.org/directory" -} - -variable "acme_registration_email" { - type = string -} - -variable "addon_version" { - type = string - default = "2.25.1" -} - -variable "coder_access_url" { - type = string +provider "aws" { + region = var.region + profile = var.profile } -variable "coder_wildcard_access_url" { - type = string -} +data "aws_region" "this" {} -variable "coder_experiments" { - type = list(string) - default = [] -} - -variable "coder_github_allowed_orgs" { - type = list(string) - default = [] +data "aws_eks_cluster" "this" { + name = var.cluster_name } -variable "coder_builtin_provisioner_count" { - type = number - default = 0 +data "aws_eks_cluster_auth" "this" { + name = var.cluster_name } -variable "coder_github_external_auth_secret_client_secret" { - type = string - sensitive = true +data "aws_iam_openid_connect_provider" "this" { + url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer } -variable "coder_github_external_auth_secret_client_id" { - type = string - sensitive = true +data "aws_db_instance" "coder" { + db_instance_identifier = var.coder_db_rds_name } -variable "coder_oauth_secret_client_secret" { - type = string - sensitive = true +data "aws_vpc" "this" { + tags = { + Name = var.vpc_name + } } -variable "coder_oauth_secret_client_id" { - type = string - sensitive = true +provider "helm" { + kubernetes = { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token + } } -variable "coder_oidc_secret_client_secret" { - type = string - sensitive = true +provider "kubernetes" { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token } -variable "coder_oidc_secret_client_id" { - type = string - sensitive = true +locals { + common_name = trimprefix(trimprefix(var.coder_access_url, "https://"), "http://") + wildcard_name = trimprefix(trimprefix(var.coder_wildcard_access_url, "https://"), "http://") + ssl_vol_friendly_name = replace(local.common_name, ".", "-") } -variable "coder_oidc_secret_issuer_url" { - type = string - sensitive = true -} +resource "kubernetes_manifest" "certificate" { -variable "coder_db_secret_url" { - type = string - sensitive = true -} + field_manager { + force_conflicts = true + } -variable "coder_token" { - type = string - sensitive = true -} + wait { + condition { + type = "Ready" + status = "True" + } + } -variable "image_repo" { - type = string - sensitive = true -} + timeouts { + create = "10m" + update = "10m" + delete = "30s" + } -variable "image_tag" { - type = string - default = "latest" + manifest = { + apiVersion = "cert-manager.io/v1" + kind = "Certificate" + metadata = { + name = local.ssl_vol_friendly_name + namespace = module.coder-server.namespace + } + spec = { + commonName = local.common_name + dnsNames = [ + local.common_name, + local.wildcard_name + ] + duration = "2160h" # 90 days + renewBefore = "360h" # 15 days + issuerRef = { + kind = "ClusterIssuer" + name = "issuer" + } + secretName = local.ssl_vol_friendly_name + privateKey = { + rotationPolicy = "Never" + algorithm = "RSA" + encoding = "PKCS1" + size = "2048" + } + } + } } -variable "kubernetes_ssl_secret_name" { - type = string +locals { + azs = var.azs + pub_subs = [for az in local.azs : "${var.vpc_name}-public-${data.aws_region.this.region}${az}"] + release_name = "coder" + chart_name = "coder" + namespace = "coder" } -variable "kubernetes_create_ssl_secret" { - type = bool - default = true +resource "aws_eip" "coder" { + count = length(local.pub_subs) + domain = "vpc" + public_ipv4_pool = "amazon" + tags = { + Name = "coder-eip-${count.index}" + } } -variable "cloudflare_api_token" { - type = string - sensitive = true +# resource "kubernetes_pod_disruption_budget_v1" "coder" { +# metadata { +# name = local.release_name +# namespace = module.coder-server.namespace +# } +# spec { +# # Avoid disrupting ongoing connections. +# max_unavailable = 1 +# selector { +# match_labels = { +# "app.kubernetes.io/instance" = local.release_name +# "app.kubernetes.io/name" = local.chart_name +# "app.kubernetes.io/part-of" = local.chart_name +# } +# } +# } +# } + +# --- + +resource "aws_iam_user" "bedrock" { + name = "${local.release_name}-bedrock" + path = "/${var.cluster_name}/${var.region}/" + force_destroy = true + + tags = { + Purpose = "coder-aibridge" + ManagedBy = "terraform" + } } -variable "oidc_sign_in_text" { - type = string -} +data "aws_iam_policy_document" "bedrock" { + statement { + sid = "InvokeBedrockModels" + effect = "Allow" -variable "oidc_icon_url" { - type = string -} + actions = [ + "bedrock:InvokeModel", + "bedrock:InvokeModelWithResponseStream" + ] -variable "oidc_scopes" { - type = list(string) + resources = [ + "arn:aws:bedrock:*::foundation-model/*", + "arn:aws:bedrock:*:*:inference-profile/*" + ] + } } -variable "oidc_email_domain" { - type = string +resource "aws_iam_policy" "bedrock" { + name = "${local.release_name}-bedrock" + description = "Allow Coder AI Bridge to invoke Amazon Bedrock models." + policy = data.aws_iam_policy_document.bedrock.json } -variable "anthropic_llm_endpoint" { - type = string - sensitive = true +resource "aws_iam_user_policy_attachment" "bedrock" { + user = aws_iam_user.bedrock.name + policy_arn = aws_iam_policy.bedrock.arn } -variable "anthropic_llm_key" { - type = string - sensitive = true +resource "aws_iam_access_key" "bedrock" { + user = aws_iam_user.bedrock.name } -variable "openai_llm_endpoint" { - type = string - sensitive = true -} +# --- -variable "openai_llm_key" { - type = string - sensitive = true -} +module "coder-server" { -provider "aws" { - region = var.cluster_region - profile = var.cluster_profile -} + source = "../../../../../modules/k8s/bootstrap/coder-server" -data "aws_eks_cluster" "this" { - name = var.cluster_name -} + release_name = local.release_name + chart_version = var.addon_version + chart_name = local.chart_name + cluster_name = data.aws_eks_cluster.this.id + cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.this.arn + + coder = { + access_url = var.coder_access_url + wildcard_url = var.coder_wildcard_access_url + mount_ssl = true + mount_ssl_name = kubernetes_manifest.certificate.manifest.spec.secretName + + image_repo = var.image_repo + image_tag = var.image_tag + + rep_cnt = length(local.pub_subs) + # External Provisioners will be used + prov_rep_cnt = var.coder_builtin_provisioner_count + env_vars = { + CODER_EXPERIMENTS = join(",", var.coder_experiments) + } + } -data "aws_eks_cluster_auth" "this" { - name = var.cluster_name -} + db = { + url = data.aws_db_instance.coder.endpoint + username = var.coder_db_username + password = var.coder_db_password + db = var.coder_db_name + pg_auth = "awsiamrds" + } -provider "helm" { - kubernetes = { - host = data.aws_eks_cluster.this.endpoint - cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) - token = data.aws_eks_cluster_auth.this.token + prometheus = { + enable = true } -} -provider "kubernetes" { - host = data.aws_eks_cluster.this.endpoint - cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) - token = data.aws_eks_cluster_auth.this.token -} + oidc = { + enable = true + sign_in_text = var.oidc_sign_in_text + icon_url = var.oidc_icon_url + scopes = var.oidc_scopes + email_domain = var.oidc_email_domain + issuer_url = var.coder_oidc_secret_issuer_url + client_id = var.coder_oidc_secret_client_id + client_secret = var.coder_oidc_secret_client_secret + } -provider "coderd" { - url = var.coder_access_url - token = var.coder_token -} + oauth2 = { + enable = true + default_provider_enable = false + allow_signups = true + device_flow = false + allowed_orgs = var.coder_github_allowed_orgs + client_id = var.coder_oauth_secret_client_id + client_secret = var.coder_oauth_secret_client_secret + use_extern_auth = false + } -provider "acme" { - server_url = var.acme_server_url -} + extern_auth = [{ + id = "primary-github" + type = "github" + client_id = var.coder_github_external_auth_secret_client_id + client_secret = var.coder_github_external_auth_secret_client_secret + }] -module "coder-server" { - source = "../../../../../modules/k8s/bootstrap/coder-server" + aibridge = { + enabled = true + } - cluster_name = var.cluster_name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - - namespace = "coder" - acme_registration_email = var.acme_registration_email - acme_days_until_renewal = 90 - replica_count = 2 - helm_version = var.addon_version - image_repo = var.image_repo - image_tag = var.image_tag - primary_access_url = var.coder_access_url - wildcard_access_url = var.coder_wildcard_access_url - cloudflare_api_token = var.cloudflare_api_token - coder_experiments = var.coder_experiments - coder_builtin_provisioner_count = var.coder_builtin_provisioner_count - coder_github_allowed_orgs = var.coder_github_allowed_orgs - anthropic_llm_endpoint = var.anthropic_llm_endpoint - anthropic_llm_key = var.anthropic_llm_key - openai_llm_endpoint = var.openai_llm_endpoint - openai_llm_key = var.openai_llm_key - ssl_cert_config = { - name = var.kubernetes_ssl_secret_name - create_secret = var.kubernetes_create_ssl_secret + namespace = local.namespace + resource_limit = { + cpu = "2" + memory = "2Gi" } - oidc_config = { - sign_in_text = var.oidc_sign_in_text - icon_url = var.oidc_icon_url - scopes = var.oidc_scopes - email_domain = var.oidc_email_domain + resource_request = { + cpu = "500m" + memory = "2Gi" } - db_secret_url = var.coder_db_secret_url - oidc_secret_issuer_url = var.coder_oidc_secret_issuer_url - oidc_secret_client_id = var.coder_oidc_secret_client_id - oidc_secret_client_secret = var.coder_oidc_secret_client_secret - oauth_secret_client_id = var.coder_oauth_secret_client_id - oauth_secret_client_secret = var.coder_oauth_secret_client_secret - github_external_auth_secret_client_id = var.coder_github_external_auth_secret_client_id - github_external_auth_secret_client_secret = var.coder_github_external_auth_secret_client_secret - tags = {} - service_annotations = { - "service.beta.kubernetes.io/aws-load-balancer-nlb-target-type" = "instance" + lb_class = "service.k8s.aws/nlb" + svc_annot = { + "service.beta.kubernetes.io/aws-load-balancer-nlb-target-type" = "ip" "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing" - "service.beta.kubernetes.io/aws-load-balancer-attributes" = "deletion_protection.enabled=true" - } - node_selector = { - "node.coder.io/managed-by" = "karpenter" - "node.coder.io/used-for" = "coder-server" + "service.beta.kubernetes.io/aws-load-balancer-attributes" = "deletion_protection.enabled=false,load_balancing.cross_zone.enabled=true" + "service.beta.kubernetes.io/aws-load-balancer-eip-allocations" = join(",", aws_eip.coder.*.allocation_id) + "service.beta.kubernetes.io/aws-load-balancer-subnets" = join(",", local.pub_subs) } tolerations = [{ - key = "dedicated" - operator = "Equal" - value = "coder-server" - effect = "NoSchedule" + key = "platform" + value = "coder-server" + effect = "NoSchedule" }] - topology_spread_constraints = [{ - max_skew = 1 - topology_key = "kubernetes.io/hostname" - when_unsatisfiable = "ScheduleAnyway" - label_selector = { - match_labels = { - "app.kubernetes.io/name" = "coder" - "app.kubernetes.io/part-of" = "coder" + topology_spread = [] + # topology_spread = [{ + # max_skew = 1 + # topology_key = "topology.kubernetes.io/zone" + # when_unsatisfiable = "ScheduleAnyway" + # label_selector = { + # match_labels = { + # "app.kubernetes.io/name" = local.chart_name + # "app.kubernetes.io/part-of" = local.chart_name + # } + # } + # match_label_keys = [ + # "app.kubernetes.io/instance" + # ] + # }] + affinity = { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [ + { + key = "topology.kubernetes.io/zone" + operator = "In" + values = [for az in local.azs : "${data.aws_region.this.region}${az}"] + }, + { + key = "node.coder.io/used-for", + operator = "In", + values = ["coder-server"] + } + ] + }] } } - match_label_keys = [ - "app.kubernetes.io/instance" - ] - }] - pod_anti_affinity_preferred_during_scheduling_ignored_during_execution = [{ - weight = 100 - pod_affinity_term = { - label_selector = { - match_labels = { - "app.kubernetes.io/instance" = "coder-v2" - "app.kubernetes.io/name" = "coder" - "app.kubernetes.io/part-of" = "coder" - } - } - topology_key = "kubernetes.io/hostname" + podAntiAffinity = { + preferredDuringSchedulingIgnoredDuringExecution = [] + requiredDuringSchedulingIgnoredDuringExecution = [] + # requiredDuringSchedulingIgnoredDuringExecution = [{ + # labelSelector = { + # matchExpressions = [{ + # key = "app.kubernetes.io/instance" + # operator = "In" + # values = [local.chart_name] + # }] + # } + # topologyKey = "kubernetes.io/hostname" + # }] } - }] + } } \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/coder-server/terragrunt.hcl b/infra/aws/us-east-2/k8s/coder-server/terragrunt.hcl new file mode 100644 index 0000000..6790e89 --- /dev/null +++ b/infra/aws/us-east-2/k8s/coder-server/terragrunt.hcl @@ -0,0 +1,58 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks", + "../../rds", + "../litellm", + "../karpenter", + "../lb-controller", + "../cert-manager", + "../other" # Deploy's auxillary manifests + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region=include.root.locals.CODER_AWS_REGION + + azs=jsondecode(include.root.locals.CODER_AWS_AZS) + vpc_name=include.root.locals.CODER_VPC_NAME + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + coder_access_url=include.root.locals.CODER_DOMAIN_NAME + coder_wildcard_access_url=include.root.locals.CODER_WILDCARD_URL + coder_experiments=jsondecode(include.root.locals.CODER_EXPERIMENTS) + coder_builtin_provisioner_count=include.root.locals.CODER_BUILT_IN_PROVISIONER_COUNT + + coder_db_rds_name=include.root.locals.CODER_DB_RDS_ID + coder_db_username=include.root.locals.CODER_DB_USERNAME + coder_db_password=include.root.locals.CODER_DB_PASSWORD + coder_db_name=include.root.locals.CODER_DB_NAME + + oidc_sign_in_text=include.root.locals.CODER_OIDC_SIGN_IN_TEXT + oidc_icon_url=include.root.locals.CODER_OIDC_ICON_URL + oidc_scopes=include.root.locals.CODER_OIDC_SCOPES + oidc_email_domain=include.root.locals.CODER_OIDC_EMAIL_DOMAIN + coder_oidc_secret_issuer_url=include.root.locals.CODER_OIDC_ISSUER_URL + coder_oidc_secret_client_id=include.root.locals.CODER_OIDC_CLIENT_ID + coder_oidc_secret_client_secret=include.root.locals.CODER_OIDC_CLIENT_SECRET + + coder_oauth_secret_client_id=include.root.locals.CODER_GITHUB_OAUTH_CLIENT_ID + coder_oauth_secret_client_secret=include.root.locals.CODER_GITHUB_OAUTH_CLIENT_SECRET + coder_github_external_auth_secret_client_id=include.root.locals.CODER_GITHUB_EXTERN_AUTH_CLIENT_ID + coder_github_external_auth_secret_client_secret=include.root.locals.CODER_GITHUB_EXTERN_AUTH_CLIENT_SECRET + + image_repo=include.root.locals.CODER_IMAGE_REPO + image_tag=include.root.locals.CODER_IMAGE_TAG + addon_version=include.root.locals.CODER_ADDON_VERSION + + anthropic_llm_endpoint=include.root.locals.CODER_ANTHROPIC_LLM_ENDPOINT + anthropic_llm_key=include.root.locals.CODER_ANTHROPIC_LLM_KEY + + openai_llm_endpoint=include.root.locals.CODER_OPENAI_LLM_ENDPOINT + openai_llm_key=include.root.locals.CODER_OPENAI_LLM_KEY +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/coder-server/variables.tf b/infra/aws/us-east-2/k8s/coder-server/variables.tf new file mode 100644 index 0000000..41ca65d --- /dev/null +++ b/infra/aws/us-east-2/k8s/coder-server/variables.tf @@ -0,0 +1,147 @@ +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "azs" { + type = list(string) + default = ["a", "c"] +} + +variable "vpc_name" { + type = string +} + +variable "cluster_name" { + type = string +} + +variable "addon_version" { + type = string + default = "2.25.1" +} + +variable "coder_access_url" { + type = string +} + +variable "coder_wildcard_access_url" { + type = string +} + +variable "coder_experiments" { + type = list(string) + default = [] +} + +variable "coder_github_allowed_orgs" { + type = list(string) + default = [] +} + +variable "coder_builtin_provisioner_count" { + type = number + default = 0 +} + +variable "coder_github_external_auth_secret_client_secret" { + type = string + sensitive = true +} + +variable "coder_github_external_auth_secret_client_id" { + type = string + sensitive = true +} + +variable "coder_oauth_secret_client_secret" { + type = string + sensitive = true +} + +variable "coder_oauth_secret_client_id" { + type = string + sensitive = true +} + +variable "coder_oidc_secret_client_secret" { + type = string + sensitive = true +} + +variable "coder_oidc_secret_client_id" { + type = string + sensitive = true +} + +variable "coder_oidc_secret_issuer_url" { + type = string + sensitive = true +} + +variable "coder_db_rds_name" { + type = string +} + +variable "coder_db_username" { + type = string +} + +variable "coder_db_password" { + type = string + sensitive = true +} + +variable "coder_db_name" { + type = string +} + +variable "image_repo" { + type = string + sensitive = true +} + +variable "image_tag" { + type = string + default = "latest" +} + +variable "oidc_sign_in_text" { + type = string +} + +variable "oidc_icon_url" { + type = string +} + +variable "oidc_scopes" { + type = list(string) +} + +variable "oidc_email_domain" { + type = string +} + +variable "anthropic_llm_endpoint" { + type = string + sensitive = true +} + +variable "anthropic_llm_key" { + type = string + sensitive = true +} + +variable "openai_llm_endpoint" { + type = string + sensitive = true +} + +variable "openai_llm_key" { + type = string + sensitive = true +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/coder-ws/main.tf b/infra/aws/us-east-2/k8s/coder-ws/main.tf index 0dc793f..b77b504 100644 --- a/infra/aws/us-east-2/k8s/coder-ws/main.tf +++ b/infra/aws/us-east-2/k8s/coder-ws/main.tf @@ -1,94 +1,10 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - kubernetes = { - source = "hashicorp/kubernetes" - } - coderd = { - source = "coder/coderd" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "cluster_oidc_provider_arn" { - type = string -} - -variable "provisioner_addon_version" { - type = string - default = "2.23.0" -} - -variable "logstream_addon_version" { - type = string - default = "0.0.11" -} - -variable "coder_access_url" { - type = string - sensitive = true -} - -variable "coder_token" { - type = string - sensitive = true -} - -variable "image_repo" { - type = string - sensitive = true -} - -variable "image_tag" { - type = string - default = "latest" -} - -variable "rotate_key_image_repo" { - type = string - sensitive = true -} - -variable "rotate_key_image_tag" { - type = string - sensitive = true -} - -variable "aws_secret_id" { - type = string - sensitive = true -} - -variable "aws_secret_region" { - type = string - sensitive = true -} - provider "aws" { - region = var.cluster_region - profile = var.cluster_profile + region = var.region + profile = var.profile } +data "aws_region" "this" {} + data "aws_eks_cluster" "this" { name = var.cluster_name } @@ -97,6 +13,27 @@ data "aws_eks_cluster_auth" "this" { name = var.cluster_name } +data "aws_iam_openid_connect_provider" "this" { + url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer +} + +data "http" "login" { + url = "${var.coder_access_url}/api/v2/users/login" + method = "POST" + request_headers = { + Accept = "application/json" + } + request_body = jsonencode({ + email = var.coder_admin_email + password = var.coder_admin_password + }) + + retry { + attempts = 5 + min_delay_ms = (5 * 1000) # 5 seconds + } +} + provider "helm" { kubernetes = { host = data.aws_eks_cluster.this.endpoint @@ -113,97 +50,374 @@ provider "kubernetes" { provider "coderd" { url = var.coder_access_url - token = var.coder_token + token = jsondecode(data.http.login.response_body).session_token } locals { - service_account_labels = { - "app.kubernetes.io/instance" = "coder-provisioner" - "app.kubernetes.io/name" = "coder-provisioner" - "app.kubernetes.io/part-of" = "coder-provisioner" - } - node_selector = { - "node.coder.io/managed-by" = "karpenter" - "node.coder.io/used-for" = "coder-provisioner" - } + release_name = "coder" + chart_name = "coder-provisioner" + namespace = "coder" + + node_selector = {} + topology_spread = [] + # topology_spread = [{ + # max_skew = 2 + # topology_key = "kubernetes.io/hostname" + # when_unsatisfiable = "ScheduleAnyway" + # label_selector = { + # match_labels = { + # "app.kubernetes.io/name" = local.chart_name + # "app.kubernetes.io/part-of" = local.chart_name + # } + # } + # match_label_keys = [ + # "app.kubernetes.io/instance" + # ] + # }] tolerations = [{ - key = "dedicated" - operator = "Equal" - value = "coder-provisioner" - effect = "NoSchedule" + key = "coder" + operator = "Exists" + values = ["provisioner"] }] + affinity = { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [ + { + key = "node.coder.io/used-for", + operator = "In", + values = ["coder-provisioner"] + } + ] + }] + } + } + podAntiAffinity = {} + # podAntiAffinity = { + # preferredDuringSchedulingIgnoredDuringExecution = [{ + # weight = 100 + # podAffinityTerm = { + # labelSelector = { + # match_labels = { + # "app.kubernetes.io/name" = local.chart_name + # "app.kubernetes.io/part-of" = local.chart_name + # } + # } + # topologyKey = "kubernetes.io/hostname" + # } + # }] + # } + } } module "default-ws" { - source = "../../../../../modules/k8s/bootstrap/coder-provisioner" - cluster_name = var.cluster_name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - - coder_organization_name = "coder" - - namespace = "coder-ws" - image_repo = var.image_repo - image_tag = var.image_tag - ws_service_account_name = "coder-ws" - ws_service_account_labels = local.service_account_labels - provisioner_service_account_name = "coder" - replica_count = 6 - primary_access_url = var.coder_access_url - env_vars = { - CODER_PROMETHEUS_ENABLE = "true" - CODER_PROMETHEUS_COLLECT_AGENT_STATS = "true" - CODER_PROMETHEUS_COLLECT_DB_METRICS = "true" + + source = "../../../../../modules/k8s/bootstrap/coder-provisioner" + + release_name = local.release_name + chart_version = var.addon_version + chart_name = local.chart_name + cluster_name = data.aws_eks_cluster.this.id + cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.this.arn + + namespace = "coder-ws" + + coder = { + access_url = var.coder_access_url + org_name = "coder" + image_repo = var.image_repo + image_tag = var.image_tag + rep_cnt = 1 + ws_extra_rules = [{ + apiGroups = [""] + resources = ["configmaps"] + verbs = [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }] + env_vars = { + CODER_PROMETHEUS_ENABLE = "true" + CODER_PROMETHEUS_COLLECT_AGENT_STATS = "true" + CODER_PROMETHEUS_COLLECT_DB_METRICS = "true" + # TF_VAR_namespace = "coder-ws" + } + } + + svc_acc = { + create = true + name = "coder" } - node_selector = local.node_selector - tolerations = local.tolerations + + node_selector = local.node_selector + tolerations = local.tolerations + topology_spread = local.topology_spread + affinity = local.affinity } module "experiment-ws" { + source = "../../../../../modules/k8s/bootstrap/coder-provisioner" + release_name = local.release_name + chart_version = var.addon_version + chart_name = local.chart_name cluster_name = var.cluster_name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - - coder_organization_name = "experiment" - - namespace = "coder-ws-experiment" - image_repo = var.image_repo - image_tag = var.image_tag - ws_service_account_name = "coder-ws-experiment" - ws_service_account_labels = local.service_account_labels - provisioner_service_account_name = "coder" - replica_count = 2 - primary_access_url = var.coder_access_url - env_vars = { - CODER_PROMETHEUS_ENABLE = "false" - CODER_PROMETHEUS_COLLECT_AGENT_STATS = "false" - CODER_PROMETHEUS_COLLECT_DB_METRICS = "false" + cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.this.arn + + namespace = "coder-ws-experiment" + + coder = { + access_url = var.coder_access_url + org_name = "experiment" + image_repo = var.image_repo + image_tag = var.image_tag + rep_cnt = 1 + ws_extra_rules = [{ + apiGroups = [""] + resources = ["configmaps"] + verbs = [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + },{ + apiGroups = [""] + resources = ["serviceaccounts"] + verbs = [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + },{ + apiGroups = ["rbac.authorization.k8s.io"] + resources = ["clusterrolebindings"] + verbs = [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }] + env_vars = { + CODER_PROMETHEUS_ENABLE = "true" + CODER_PROMETHEUS_COLLECT_AGENT_STATS = "true" + CODER_PROMETHEUS_COLLECT_DB_METRICS = "true" + # TF_VAR_namespace = "coder-ws-experiment" + } + } + + svc_acc = { + create = true + name = "coder" } - node_selector = local.node_selector - tolerations = local.tolerations + + node_selector = local.node_selector + tolerations = local.tolerations + topology_spread = local.topology_spread + affinity = local.affinity +} + +data "aws_iam_policy_document" "eks" { + statement { + effect = "Allow" + actions = [ + "eks:*", + "iam:*" + ] + resources = ["*"] + } +} + +module "eks-admin-policy" { + source = "../../../../../modules/security/policy" + name = "demo-ws-eks-policy" + path = "/" + description = "Coder External Demo-Provisioner Policy (EKS)" + policy_json = data.aws_iam_policy_document.eks.json } module "demo-ws" { + source = "../../../../../modules/k8s/bootstrap/coder-provisioner" + release_name = local.release_name + chart_version = var.addon_version + chart_name = local.chart_name cluster_name = var.cluster_name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - - coder_organization_name = "demo" - - namespace = "coder-ws-demo" - image_repo = var.image_repo - image_tag = var.image_tag - ws_service_account_name = "coder-ws-demo" - ws_service_account_labels = local.service_account_labels - provisioner_service_account_name = "coder" - replica_count = 2 - primary_access_url = var.coder_access_url - env_vars = { - CODER_PROMETHEUS_ENABLE = "true" - CODER_PROMETHEUS_COLLECT_AGENT_STATS = "true" - CODER_PROMETHEUS_COLLECT_DB_METRICS = "true" + cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.this.arn + + namespace = "coder-ws-demo" + + coder = { + access_url = var.coder_access_url + org_name = "demo" + image_repo = var.image_repo + image_tag = var.image_tag + rep_cnt = 1 + ws_extra_rules = [{ + apiGroups = [""] + resources = ["configmaps"] + verbs = [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + },{ + apiGroups = [""] + resources = ["serviceaccounts"] + verbs = [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + },{ + apiGroups = ["rbac.authorization.k8s.io"] + resources = ["clusterrolebindings"] + verbs = [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }] + env_vars = { + CODER_PROMETHEUS_ENABLE = "true" + CODER_PROMETHEUS_COLLECT_AGENT_STATS = "true" + CODER_PROMETHEUS_COLLECT_DB_METRICS = "true" + # TF_VAR_namespace = "coder-ws-experiment" + } } - node_selector = local.node_selector - tolerations = local.tolerations + + svc_acc = { + create = true + name = "coder" + iam_policy_arns = { + "EKSAdminPolicy" = module.eks-admin-policy.policy_arn + } + } + + node_selector = local.node_selector + tolerations = local.tolerations + topology_spread = local.topology_spread + affinity = local.affinity +} + +# resource "kubernetes_cluster_role_v1" "coder-ws-demo" { +# metadata { +# name = "coder-ws-demo" +# } + +# rule { +# api_groups = ["rbac.authorization.k8s.io"] +# resources = ["clusterrolebindings"] +# verbs = [ +# "create", +# "delete", +# "deletecollection", +# "get", +# "list", +# "patch", +# "update", +# "watch" +# ] +# } +# } + +# resource "kubernetes_cluster_role_binding_v1" "coder-cluster-workspace-perms" { + +# depends_on = [module.demo-ws] + +# metadata { +# name = "coder-cluster-workspace-perms" +# } + +# subject { +# kind = "ServiceAccount" +# name = "coder" +# namespace = "coder-ws-demo" +# } + +# role_ref { +# api_group = "rbac.authorization.k8s.io" +# kind = "ClusterRole" +# name = kubernetes_cluster_role_v1.coder-ws-demo.metadata[0].name +# } +# } + +module "coder-logstream-kube" { + + source = "../../../../../modules/k8s/bootstrap/coder-logstream" + + release_name = "coder-logstream-kube" + chart_version = "0.0.15" + chart_name = "coder-logstream-kube" + + namespace = "coder-logstream-kube" + image_tag = "v0.0.15" + + coder = { + access_url = var.coder_access_url + ws_ns = ["coder-ws", "coder-ws-demo", "coder-ws-experiment"] + } + + tolerations = [{ + key = "CriticalAddonsOnly" + operator = "Exists" + }, { + key = "dedicated" + value = "general" + effect = "NoSchedule" + }] + affinity = { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [ + { + key = "eks.amazonaws.com/compute-type", + operator = "In", + values = ["auto"] + } + ] + }] + } + } + } + } \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/coder-ws/terragrunt.hcl b/infra/aws/us-east-2/k8s/coder-ws/terragrunt.hcl new file mode 100644 index 0000000..bc4bd7c --- /dev/null +++ b/infra/aws/us-east-2/k8s/coder-ws/terragrunt.hcl @@ -0,0 +1,30 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks", + "../litellm", + "../coder-server", + "../kyverno", + "../other" # Deploy's auxillary manifests + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region=include.root.locals.CODER_AWS_REGION + + coder_access_url=include.root.locals.CODER_DOMAIN_NAME + coder_admin_email=include.root.locals.CODER_EMAIL + coder_admin_password=include.root.locals.CODER_PASSWORD + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + image_repo=include.root.locals.CODER_IMAGE_REPO + image_tag=include.root.locals.CODER_IMAGE_TAG + addon_version=include.root.locals.CODER_ADDON_VERSION + logstream_addon_version=include.root.locals.CODER_LOGSTREAM_ADDON_VERSION +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/coder-ws/variables.tf b/infra/aws/us-east-2/k8s/coder-ws/variables.tf new file mode 100644 index 0000000..edc2d5d --- /dev/null +++ b/infra/aws/us-east-2/k8s/coder-ws/variables.tf @@ -0,0 +1,45 @@ +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "cluster_name" { + type = string +} + +variable "addon_version" { + type = string + default = "2.23.0" +} + +variable "logstream_addon_version" { + type = string + default = "0.0.11" +} + +variable "coder_access_url" { + type = string +} + +variable "image_repo" { + type = string + sensitive = true +} + +variable "image_tag" { + type = string + default = "latest" +} + +variable "coder_admin_email" { + type = string +} + +variable "coder_admin_password" { + type = string + sensitive = true +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/ebs-controller/main.tf b/infra/aws/us-east-2/k8s/ebs-controller/main.tf deleted file mode 100644 index df1252c..0000000 --- a/infra/aws/us-east-2/k8s/ebs-controller/main.tf +++ /dev/null @@ -1,78 +0,0 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - kubernetes = { - source = "hashicorp/kubernetes" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "cluster_oidc_provider_arn" { - type = string -} - -variable "addon_version" { - type = string - default = "2.22.1" -} - -variable "addon_namespace" { - type = string - default = "default" -} - -variable "addon_replace" { - type = bool - default = false -} - -provider "aws" { - region = var.cluster_region - profile = var.cluster_profile -} - -data "aws_eks_cluster" "this" { - name = var.cluster_name -} - -data "aws_eks_cluster_auth" "this" { - name = var.cluster_name -} - -provider "helm" { - kubernetes = { - host = data.aws_eks_cluster.this.endpoint - cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) - token = data.aws_eks_cluster_auth.this.token - } -} - -module "ebs-controller" { - source = "../../../../../modules/k8s/bootstrap/ebs-controller" - cluster_name = data.aws_eks_cluster.this.name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - - namespace = var.addon_namespace - chart_version = var.addon_version - replace = var.addon_replace -} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/ebs-csi/main.tf b/infra/aws/us-east-2/k8s/ebs-csi/main.tf new file mode 100644 index 0000000..8cc6fbb --- /dev/null +++ b/infra/aws/us-east-2/k8s/ebs-csi/main.tf @@ -0,0 +1,85 @@ +provider "aws" { + region = var.region + profile = var.profile +} + +data "aws_eks_cluster" "this" { + name = var.cluster_name +} + +data "aws_eks_cluster_auth" "this" { + name = var.cluster_name +} + +data "aws_iam_openid_connect_provider" "this" { + url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer +} + +provider "helm" { + kubernetes = { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token + } +} + +module "ebs-controller" { + source = "../../../../../modules/k8s/bootstrap/ebs-csi" + cluster_name = data.aws_eks_cluster.this.name + cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.this.arn + + namespace = var.addon_namespace + chart_version = var.addon_version + replace = var.addon_replace + + tolerations = [{ + key = "CriticalAddonsOnly" + operator = "Exists" + }, { + key = "dedicated" + value = "general" + effect = "NoSchedule" + }] + topology_spread = [{ + topologyKey = "topology.kubernetes.io/zone" + maxSkew = 1 + whenUnsatisfiable = "ScheduleAnyway" + }] + affinity = { + nodeAffinity = { + preferredDuringSchedulingIgnoredDuringExecution = [] + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [ + { + key = "eks.amazonaws.com/compute-type", + operator = "In", + values = ["auto"] + } + ] + }] + } + } + podAntiAffinity = { + preferredDuringSchedulingIgnoredDuringExecution = [{ + podAffinityTerm = { + topologyKey = "topology.kubernetes.io/zone" + labelSelector = { + matchLabels = { + "app" = "ebs-csi-controller" + } + } + } + weight = 100 + }] + requiredDuringSchedulingIgnoredDuringExecution = [{ + topologyKey = "kubernetes.io/hostname" + labelSelector = { + matchLabels = { + "app" = "ebs-csi-controller" + } + } + }] + } + } +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/ebs-csi/terragrunt.hcl b/infra/aws/us-east-2/k8s/ebs-csi/terragrunt.hcl new file mode 100644 index 0000000..2a314ed --- /dev/null +++ b/infra/aws/us-east-2/k8s/ebs-csi/terragrunt.hcl @@ -0,0 +1,21 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks" + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region=include.root.locals.CODER_AWS_REGION + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + addon_namespace=include.root.locals.EBS_ADDON_NAMESPACE + addon_version=include.root.locals.EBS_ADDON_VERSION + addon_replace=include.root.locals.EBS_ADDON_REPLACE +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/ebs-csi/variables.tf b/infra/aws/us-east-2/k8s/ebs-csi/variables.tf new file mode 100644 index 0000000..dd4d169 --- /dev/null +++ b/infra/aws/us-east-2/k8s/ebs-csi/variables.tf @@ -0,0 +1,27 @@ +variable "cluster_name" { + type = string +} + +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "addon_version" { + type = string + default = "2.22.1" +} + +variable "addon_namespace" { + type = string + default = "default" +} + +variable "addon_replace" { + type = bool + default = false +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/efs-controller/main.tf b/infra/aws/us-east-2/k8s/efs-controller/main.tf deleted file mode 100644 index a5e2eb2..0000000 --- a/infra/aws/us-east-2/k8s/efs-controller/main.tf +++ /dev/null @@ -1,78 +0,0 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - kubernetes = { - source = "hashicorp/kubernetes" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "cluster_oidc_provider_arn" { - type = string -} - -variable "addon_version" { - type = string - default = "3.2.4" -} - -variable "addon_namespace" { - type = string - default = "default" -} - -variable "addon_replace" { - type = bool - default = false -} - -provider "aws" { - region = var.cluster_region - profile = var.cluster_profile -} - -data "aws_eks_cluster" "this" { - name = var.cluster_name -} - -data "aws_eks_cluster_auth" "this" { - name = var.cluster_name -} - -provider "helm" { - kubernetes = { - host = data.aws_eks_cluster.this.endpoint - cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) - token = data.aws_eks_cluster_auth.this.token - } -} - -module "efs-controller" { - source = "../../../../../modules/k8s/bootstrap/efs-controller" - cluster_name = data.aws_eks_cluster.this.name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - - namespace = var.addon_namespace - chart_version = var.addon_version - replace = var.addon_replace -} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/efs-controller/pv.yaml b/infra/aws/us-east-2/k8s/efs-controller/pv.yaml deleted file mode 100644 index 9b0a19f..0000000 --- a/infra/aws/us-east-2/k8s/efs-controller/pv.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: PersistentVolume -metadata: - name: efs-pv -spec: - capacity: - storage: 10Gi - volumeMode: Filesystem - accessModes: - - ReadWriteMany - persistentVolumeReclaimPolicy: Retain - storageClassName: efs-sc - csi: - driver: efs.csi.aws.com - volumeHandle: "fs-0243b71fd52de9c0d" \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/efs-controller/storageclass.yaml b/infra/aws/us-east-2/k8s/efs-controller/storageclass.yaml deleted file mode 100644 index 5878fda..0000000 --- a/infra/aws/us-east-2/k8s/efs-controller/storageclass.yaml +++ /dev/null @@ -1,14 +0,0 @@ -kind: StorageClass -apiVersion: storage.k8s.io/v1 -metadata: - name: efs-sc -provisioner: efs.csi.aws.com -parameters: - provisioningMode: efs-ap - fileSystemId: fs-0243b71fd52de9c0d - directoryPerms: "777" - # gidRangeStart: "1000" # optional - # gidRangeEnd: "2000" # optional - uid: "0" - gid: "1000" - basePath: "/dynamic_provisioning" \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/fetch-and-store/main.tf b/infra/aws/us-east-2/k8s/fetch-and-store/main.tf deleted file mode 100644 index 49194c0..0000000 --- a/infra/aws/us-east-2/k8s/fetch-and-store/main.tf +++ /dev/null @@ -1,78 +0,0 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - kubernetes = { - source = "hashicorp/kubernetes" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_oidc_provider_arn" { - type = string - sensitive = true -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "addon_namespace" { - type = string - default = "kube-system" -} - -variable "addon_version" { - type = string - default = "3.13.0" -} - -variable "image_repo" { - type = string - sensitive = true -} - -variable "image_tag" { - type = string - sensitive = true -} - -provider "aws" { - region = var.cluster_region - profile = var.cluster_profile -} - -data "aws_eks_cluster" "this" { - name = var.cluster_name -} - -data "aws_eks_cluster_auth" "this" { - name = var.cluster_name -} - -provider "kubernetes" { - host = data.aws_eks_cluster.this.endpoint - cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) - token = data.aws_eks_cluster_auth.this.token -} - -module "fetch-and-store" { - source = "../../../../../modules/k8s/bootstrap/fetch-and-store" - - cluster_name = var.cluster_name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - namespace = var.addon_namespace - image_repo = var.image_repo - image_tag = var.image_tag -} diff --git a/infra/aws/us-east-2/k8s/karpenter/main.tf b/infra/aws/us-east-2/k8s/karpenter/main.tf index 5e1a8a7..cbbd788 100644 --- a/infra/aws/us-east-2/k8s/karpenter/main.tf +++ b/infra/aws/us-east-2/k8s/karpenter/main.tf @@ -1,48 +1,6 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - kubernetes = { - source = "hashicorp/kubernetes" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_oidc_provider_arn" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "addon_version" { - type = string -} - -variable "addon_namespace" { - type = string - default = "default" -} - provider "aws" { - region = var.cluster_region - profile = var.cluster_profile + region = var.region + profile = var.profile } data "aws_eks_cluster" "this" { @@ -53,6 +11,12 @@ data "aws_eks_cluster_auth" "this" { name = var.cluster_name } +data "aws_iam_openid_connect_provider" "this" { + url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer +} + +data "aws_caller_identity" "me" {} + provider "helm" { kubernetes = { host = data.aws_eks_cluster.this.endpoint @@ -101,126 +65,92 @@ locals { } } -locals { - nodepool_configs = [{ - name = "coder-server" - node_labels = merge(local.global_node_labels, { - "node.coder.io/name" = "coder" - "node.coder.io/part-of" = "coder" - "node.coder.io/used-for" = "coder-server" - }) - node_taints = [{ - key = "dedicated" - value = "coder-server" - effect = "NoSchedule" - }] - node_requirements = concat(local.global_node_reqs, [{ - key = "node.kubernetes.io/instance-type" - operator = "In" - values = ["m5a.xlarge", "m6a.xlarge"] - }]) - node_class_ref_name = "coder-server-class" - }, { - name = "coder-provisioner" - node_labels = merge(local.global_node_labels, { - "node.coder.io/name" = "coder" - "node.coder.io/part-of" = "coder" - "node.coder.io/used-for" = "coder-provisioner" - }) - node_taints = [{ - key = "dedicated" - value = "coder-provisioner" - effect = "NoSchedule" - }] - node_requirements = concat(local.global_node_reqs, [{ - key = "node.kubernetes.io/instance-type" - operator = "In" - values = ["m5a.4xlarge", "m6a.4xlarge"] - }]) - node_class_ref_name = "coder-provisioner-class" - }, { - name = "coder-ws-all" - node_labels = merge(local.global_node_labels, { - "node.coder.io/name" = "coder" - "node.coder.io/part-of" = "coder" - "node.coder.io/used-for" = "coder-ws-all" - }) - node_taints = [{ - key = "dedicated" - value = "coder-ws" - effect = "NoSchedule" - }] - node_requirements = concat(local.global_node_reqs, [{ - key = "node.kubernetes.io/instance-type" - operator = "In" - values = ["c6a.32xlarge", "c5a.32xlarge"] - }]) - node_class_ref_name = "coder-ws-class" - disruption_consolidate_after = "30m" - }] +data "aws_iam_policy_document" "ecr-mirror" { + + statement { + effect = "Allow" + actions = ["ecr:CreateRepository"] + resources = ["*"] + } + + statement { + effect = "Allow" + actions = ["ecr:GetAuthorizationToken"] + resources = ["*"] + } + + statement { + effect = "Allow" + actions = [ + "ecr:BatchImportUpstreamImage", + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer" + ] + resources = [ "arn:aws:ecr:${var.region}:${data.aws_caller_identity.me.account_id}:repository/cache/*" ] + } +} + +resource "aws_iam_policy" "ecr-mirror" { + name_prefix = "ecr-mirror" + description = "Allows ECR pull-through cache automation including repository creation" + path = "/${var.region}/kptr/" + policy = data.aws_iam_policy_document.ecr-mirror.json } module "karpenter-addon" { + source = "../../../../../modules/k8s/bootstrap/karpenter" cluster_name = var.cluster_name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn + cluster_oidc_provider = trimprefix(data.aws_iam_openid_connect_provider.this.url, "https://") + cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.this.arn namespace = var.addon_namespace chart_version = var.addon_version - node_selector = { - "node.amazonaws.io/managed-by" : "asg" + + node_selector = {} + tolerations = [{ + key = "CriticalAddonsOnly" + operator = "Exists" + }] + topology_spread = [] + affinity = { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [ + { + key = "eks.amazonaws.com/compute-type", + operator = "In", + values = ["auto"] + } + ] + }] + } + } + podAntiAffinity = { + preferredDuringSchedulingIgnoredDuringExecution = [{ + weight = 100 + podAffinityTerm = { + labelSelector = { + matchExpressions = [{ + key = "app.kubernetes.io/name" + operator = "In" + values = ["karpenter"] + }] + } + topologyKey = "kubernetes.io/hostname" + } + }] + } } + + iam_role_use_name_prefix = true + node_iam_role_use_name_prefix = true + replicas = 2 karpenter_controller_role_policies = { "AmazonEFSCSIDriverPolicy" = "arn:aws:iam::aws:policy/service-role/AmazonEFSCSIDriverPolicy" } - ec2nodeclass_configs = [{ - name = "coder-server-class" - subnet_selector_tags = local.provisioner_subnet_tags - sg_selector_tags = local.provisioner_sg_tags - }, { - name = "coder-ws-class" - subnet_selector_tags = local.ws_all_subnet_tags - sg_selector_tags = local.ws_all_sg_tags - ami_alias = "al2023@latest" # Use /dev/xvda - user_data = <<-EOF - apiVersion: node.eks.aws/v1alpha1 - kind: NodeConfig - spec: - kubelet: - config: - registryPullQPS: 30 - EOF - block_device_mappings = [{ - device_name = "/dev/xvda" - ebs = { - volume_size = "500Gi" - volume_type = "gp3" - } - }] - # # Bottlerocket configs. - # ami_alias = "bottlerocket@latest" # Use /dev/xvda + /dev/xvdb - # user_data = <<-EOF - # [settings.kubernetes] - # 'registry-qps' = 30 - # [settings.kernel.sysctl] - # 'user.max_user_namespaces' = "16384" - # EOF - # block_device_mappings = [{ - # device_name = "/dev/xvda" - # ebs = { - # volume_size = "500Gi" - # volume_type = "gp3" - # } - # }, { - # device_name = "/dev/xvdb" - # ebs = { - # volume_size = "500Gi" - # volume_type = "gp3" - # } - # }] - }, { - name = "coder-provisioner-class" - subnet_selector_tags = local.provisioner_subnet_tags - sg_selector_tags = local.provisioner_sg_tags - }] + karpenter_node_role_policies = { + "ECRMirrorPolicy" = aws_iam_policy.ecr-mirror.arn + } } \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/karpenter/terragrunt.hcl b/infra/aws/us-east-2/k8s/karpenter/terragrunt.hcl new file mode 100644 index 0000000..2a69c09 --- /dev/null +++ b/infra/aws/us-east-2/k8s/karpenter/terragrunt.hcl @@ -0,0 +1,20 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks" + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region=include.root.locals.CODER_AWS_REGION + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + addon_namespace=include.root.locals.KPTR_ADDON_NAMESPACE + addon_version=include.root.locals.KPTR_ADDON_VERSION +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/karpenter/variables.tf b/infra/aws/us-east-2/k8s/karpenter/variables.tf new file mode 100644 index 0000000..442aaa5 --- /dev/null +++ b/infra/aws/us-east-2/k8s/karpenter/variables.tf @@ -0,0 +1,21 @@ +variable "cluster_name" { + type = string +} + +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "addon_version" { + type = string +} + +variable "addon_namespace" { + type = string + default = "default" +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/kyverno/main.tf b/infra/aws/us-east-2/k8s/kyverno/main.tf new file mode 100644 index 0000000..b686e6f --- /dev/null +++ b/infra/aws/us-east-2/k8s/kyverno/main.tf @@ -0,0 +1,116 @@ +provider "aws" { + region = var.region + profile = var.profile +} + +data "aws_eks_cluster" "this" { + name = var.cluster_name +} + +data "aws_eks_cluster_auth" "this" { + name = var.cluster_name +} + +data "aws_iam_openid_connect_provider" "this" { + url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer +} + +provider "helm" { + kubernetes = { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token + } +} + +provider "kubernetes" { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token +} + +resource "kubernetes_namespace_v1" "this" { + + count = var.create_namespace ? 1 : 0 + + metadata { + name = var.namespace + } +} + +locals { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [{ + key = "karpenter.sh/nodepool" + operator = "In" + values = ["system"] + }] + }] + } + } +} + +resource "helm_release" "kyverno" { + name = var.release_name + namespace = try(kubernetes_namespace_v1.this[0].metadata[0].name, var.namespace) + chart = var.chart_name + repository = "https://kyverno.github.io/kyverno/" + create_namespace = false + upgrade_install = true + skip_crds = false + wait = true + wait_for_jobs = true + version = var.chart_version + timeout = 600 + + values = [yamlencode({ + + global = { + tolerations = [{ + effect = "NoSchedule" + key = "CriticalAddonsOnly" + operator = "Exists" + }] + } + + config = { + defaultRegistry = "docker.io" + enableDefaultRegistryMutation = true + webhooks = { + namespacesSelector = { + matchExpressions = [{ + key = "kubernetes.io/metadata.name" + operator = "NotIn" + values = [ + "kube-system", + ] + }] + } + } + } + + crds = { + migration = { + nodeAffinity = local.nodeAffinity + } + } + admissionController = { + replicas = 3 + nodeAffinity = local.nodeAffinity + } + backgroundController = { + replicas = 2 + nodeAffinity = local.nodeAffinity + } + cleanupController = { + replicas = 2 + nodeAffinity = local.nodeAffinity + } + reportsController = { + replicas = 2 + nodeAffinity = local.nodeAffinity + } + })] +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/kyverno/terragrunt.hcl b/infra/aws/us-east-2/k8s/kyverno/terragrunt.hcl new file mode 100644 index 0000000..1d7058d --- /dev/null +++ b/infra/aws/us-east-2/k8s/kyverno/terragrunt.hcl @@ -0,0 +1,20 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks" + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region=include.root.locals.CODER_AWS_REGION + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + # addon_namespace=include.root.locals.KYVERNO_ADDON_NAMESPACE + # addon_version=include.root.locals.KYVERNO_ADDON_VERSION +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/kyverno/variables.tf b/infra/aws/us-east-2/k8s/kyverno/variables.tf new file mode 100644 index 0000000..4148cbc --- /dev/null +++ b/infra/aws/us-east-2/k8s/kyverno/variables.tf @@ -0,0 +1,37 @@ +variable "cluster_name" { + type = string +} + +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "release_name" { + type = string + default = "kyverno" +} + +variable "chart_name" { + type = string + default = "kyverno" +} + +variable "chart_version" { + type = string + default = "3.7.1" +} + +variable "create_namespace" { + type = bool + default = true +} + +variable "namespace" { + type = string + default = "kyverno" +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/lb-controller/main.tf b/infra/aws/us-east-2/k8s/lb-controller/main.tf index b0b7e2d..a51a8b1 100644 --- a/infra/aws/us-east-2/k8s/lb-controller/main.tf +++ b/infra/aws/us-east-2/k8s/lb-controller/main.tf @@ -1,54 +1,6 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - kubernetes = { - source = "hashicorp/kubernetes" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "cluster_oidc_provider_arn" { - type = string -} - -variable "addon_namespace" { - type = string - default = "default" -} - -variable "addon_version" { - type = string - default = "1.13.2" -} - -variable "use_cert_manager" { - type = bool - default = false -} - provider "aws" { - region = var.cluster_region - profile = var.cluster_profile + region = var.region + profile = var.profile } data "aws_eks_cluster" "this" { @@ -59,6 +11,16 @@ data "aws_eks_cluster_auth" "this" { name = var.cluster_name } +data "aws_iam_openid_connect_provider" "this" { + url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer +} + +data "aws_vpc" "this" { + tags = { + Name = var.vpc_name + } +} + provider "helm" { kubernetes = { host = data.aws_eks_cluster.this.endpoint @@ -73,18 +35,67 @@ provider "kubernetes" { token = data.aws_eks_cluster_auth.this.token } +locals { + release_name = "aws-load-balancer-controller" +} + module "lb-controller" { source = "../../../../../modules/k8s/bootstrap/lb-controller" cluster_name = data.aws_eks_cluster.this.name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn + cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.this.arn + release_name = local.release_name namespace = var.addon_namespace chart_version = var.addon_version enable_cert_manager = var.use_cert_manager + vpc_id = data.aws_vpc.this.id + + tolerations = [{ + key = "CriticalAddonsOnly" + operator = "Exists" + }] + topology_spread = [{ + topologyKey = "topology.kubernetes.io/zone" + maxSkew = 1 + whenUnsatisfiable = "ScheduleAnyway" + }] + affinity = { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [ + { + key = "eks.amazonaws.com/compute-type", + operator = "In", + values = ["auto"] + } + ] + }] + } + } + podAntiAffinity = { + preferredDuringSchedulingIgnoredDuringExecution = [{ + weight = 100 + podAffinityTerm = { + topologyKey = "topology.kubernetes.io/zone" + labelSelector = { + matchLabels = { + "app.kubernetes.io/name" = local.release_name + } + } + } + }] + requiredDuringSchedulingIgnoredDuringExecution = [{ + topologyKey = "kubernetes.io/hostname" + labelSelector = { + matchLabels = { + "app.kubernetes.io/name" = local.release_name + } + } + }] + } + } service_target_eni_sg_tags = { Name = "aidemo-eks-node" } - cluster_asg_node_labels = { # var.karpenter_node_labels - "node.amazonaws.io/managed-by" = "asg" - } } \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/lb-controller/terragrunt.hcl b/infra/aws/us-east-2/k8s/lb-controller/terragrunt.hcl new file mode 100644 index 0000000..ee0e6fb --- /dev/null +++ b/infra/aws/us-east-2/k8s/lb-controller/terragrunt.hcl @@ -0,0 +1,22 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks" + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region=include.root.locals.CODER_AWS_REGION + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + addon_namespace=include.root.locals.LB_ADDON_NAMESPACE + addon_version=include.root.locals.LB_ADDON_VERSION + vpc_name=include.root.locals.CODER_VPC_NAME + use_cert_manager=include.root.locals.LB_ADDON_USE_CERTMGR +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/lb-controller/variables.tf b/infra/aws/us-east-2/k8s/lb-controller/variables.tf new file mode 100644 index 0000000..1b505c0 --- /dev/null +++ b/infra/aws/us-east-2/k8s/lb-controller/variables.tf @@ -0,0 +1,31 @@ +variable "cluster_name" { + type = string +} + +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "addon_namespace" { + type = string + default = "default" +} + +variable "addon_version" { + type = string + default = "1.13.2" +} + +variable "vpc_name" { + type = string +} + +variable "use_cert_manager" { + type = bool + default = false +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/litellm/main.tf b/infra/aws/us-east-2/k8s/litellm/main.tf index a759c7f..43ac7f6 100644 --- a/infra/aws/us-east-2/k8s/litellm/main.tf +++ b/infra/aws/us-east-2/k8s/litellm/main.tf @@ -1,151 +1,442 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 3.1.1" - } - } - backend "s3" {} +provider "aws" { + region = var.region + profile = var.profile } -variable "cluster_name" { - type = string +data "aws_eks_cluster" "this" { + name = var.cluster_name } -variable "cluster_region" { - type = string +data "aws_eks_cluster_auth" "this" { + name = var.cluster_name } -variable "cluster_oidc_provider_arn" { - type = string +data "aws_iam_openid_connect_provider" "this" { + url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer } -variable "cluster_profile" { - type = string - default = "default" +data "aws_db_instance" "litellm" { + db_instance_identifier = var.db_rds_id } -variable "addon_namespace" { - type = string - default = "kube-system" -} +data "aws_region" "this" {} -variable "aws_ingress_certificate_arn" { - type = string - sensitive = true +provider "kubernetes" { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token } -variable "db_url" { - type = string - sensitive = true +provider "helm" { + kubernetes = { + host = data.aws_eks_cluster.this.endpoint + cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) + token = data.aws_eks_cluster_auth.this.token + } } -variable "litellm_salt_key" { - type = string - sensitive = true +locals { + common_name = replace(replace(var.host_name, "https://", ""), "http://", "") + ssl_vol_friendly_name = replace(local.common_name, ".", "-") } -variable "litellm_master_key" { - type = string - sensitive = true -} +# resource "kubernetes_manifest" "cert" { -variable "redis_host" { - type = string - sensitive = true -} +# field_manager { +# force_conflicts = true +# } +# manifest = { +# apiVersion = "cert-manager.io/v1" +# kind = "Certificate" +# metadata = { +# labels = {} # var.cert_labels +# name = local.ssl_vol_friendly_name +# namespace = module.litellm.namespace +# } +# spec = { +# secretName = local.ssl_vol_friendly_name +# commonName = local.common_name +# dnsNames = [local.common_name] +# duration = "${90 * 24}h" +# renewBefore = "8h" +# additionalOutputFormats = [{ +# type = "CombinedPEM" +# }, { +# type = "DER" +# }] +# issuerRef = { +# kind = "ClusterIssuer" +# name = "issuer" +# } +# } +# } +# } -variable "redis_password" { - type = string - sensitive = true -} +# resource "kubernetes_secret_v1" "gcloud" { +# metadata { +# name = "gcloud-auth" +# namespace = module.litellm.namespace +# labels = {} +# } +# data = { +# "service_account.json" = var.gcloud_auth +# } +# } -variable "gcloud_auth" { - type = string - sensitive = true +locals { + azs = var.azs + pub_subs = [for az in local.azs : "${var.vpc_name}-public-${data.aws_region.this.region}${az}"] + # App port is actually being ignored on the LiteLLM app-level. Statically set to 4000 + app_port = 4000 + release_name = "litellm" + chart_name = "litellm-helm" + namespace = "litellm" } -variable "host_name" { - type = string - sensitive = true -} +# resource "kubernetes_pod_disruption_budget_v1" "litellm" { +# metadata { +# name = local.release_name +# namespace = module.litellm.namespace +# } +# spec { +# # Avoid disrupting ongoing connections. +# max_unavailable = 1 +# selector { +# match_labels = { +# "app.kubernetes.io/instance" = local.release_name +# "app.kubernetes.io/name" = local.release_name +# } +# } +# } +# } -variable "rotate_key_image_repo" { - type = string - sensitive = true -} +# resource "aws_eip" "litellm" { +# count = length(local.pub_subs) +# domain = "vpc" +# public_ipv4_pool = "amazon" +# tags = { +# Name = "litellm-eip-${count.index}" +# } +# } -variable "rotate_key_image_tag" { - type = string - sensitive = true -} +# module "litellm" { +# source = "../../../../../modules/k8s/bootstrap/litellm" -variable "aws_secret_id" { - type = string - sensitive = true -} +# release_name = local.release_name +# chart_name = local.chart_name + +# image_config = { +# repository = "ghcr.io/berriai/litellm-database" +# # tag = "v1.82.0.patch5" +# tag = "v1.83.7-stable.patch.1" +# } -variable "aws_secret_region" { - type = string - sensitive = true -} +# cluster_name = var.cluster_name +# cluster_oidc_provider_arn = data.aws_iam_openid_connect_provider.this.arn +# namespace = var.addon_namespace -provider "aws" { - region = var.cluster_region - profile = var.cluster_profile -} +# replicas = length(aws_eip.litellm.*.allocation_id) +# autoscaling_min_replicas = length(aws_eip.litellm.*.allocation_id) +# autoscaling_max_replicas = 12 -data "aws_eks_cluster" "this" { - name = var.cluster_name -} +# resource_limit = { +# cpu = "2" +# memory = "2Gi" +# } +# resource_request = { +# cpu = "500m" +# memory = "2Gi" +# } -data "aws_eks_cluster_auth" "this" { - name = var.cluster_name -} +# litellm_master_key = var.litellm_master_key -provider "kubernetes" { - host = data.aws_eks_cluster.this.endpoint - cluster_ca_certificate = base64decode(data.aws_eks_cluster.this.certificate_authority[0].data) - token = data.aws_eks_cluster_auth.this.token -} +# service_lb_class = "service.k8s.aws/nlb" +# svc_port = local.app_port +# svc_annots = { +# "service.beta.kubernetes.io/aws-load-balancer-nlb-target-type" = "ip" +# "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing" +# "service.beta.kubernetes.io/aws-load-balancer-attributes" = "deletion_protection.enabled=false,load_balancing.cross_zone.enabled=true" +# "service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol" = "https" +# "service.beta.kubernetes.io/aws-load-balancer-healthcheck-port" = "${local.app_port}" +# "service.beta.kubernetes.io/aws-load-balancer-eip-allocations" = join(",", aws_eip.litellm.*.allocation_id) +# "service.beta.kubernetes.io/aws-load-balancer-subnets" = join(",", local.pub_subs) +# } +# node_selector = {} +# tolerations = [{ +# key = "platform" +# value = "litellm" +# effect = "NoSchedule" +# }] +# topology_spread = [] +# # topology_spread = [{ +# # maxSkew = 1 +# # topologyKey = "topology.kubernetes.io/zone" +# # whenUnsatisfiable = "DoNotSchedule" +# # labelSelector = { +# # matchLabels = { +# # "app.kubernetes.io/name" = local.release_name +# # } +# # } +# # matchLabelKeys = [ +# # "app.kubernetes.io/instance" +# # ] +# # }] +# affinity = { +# nodeAffinity = { +# requiredDuringSchedulingIgnoredDuringExecution = { +# nodeSelectorTerms = [{ +# matchExpressions = [ +# { +# key = "topology.kubernetes.io/zone" +# operator = "In" +# values = [for az in local.azs : "${data.aws_region.this.region}${az}"] +# }, +# { +# key = "node.coder.io/used-for", +# operator = "In", +# values = ["litellm"] +# } +# ] +# }] +# } +# } +# podAntiAffinity = { +# preferredDuringSchedulingIgnoredDuringExecution = [] +# requiredDuringSchedulingIgnoredDuringExecution = [] +# # requiredDuringSchedulingIgnoredDuringExecution = [{ +# # labelSelector = { +# # matchExpressions = [{ +# # key = "app.kubernetes.io/instance" +# # operator = "In" +# # values = [local.release_name] +# # }] +# # } +# # topologyKey = "kubernetes.io/hostname" +# # }] +# } +# } -module "litellm" { - source = "../../../../../modules/k8s/bootstrap/litellm" - - cluster_name = var.cluster_name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - namespace = var.addon_namespace - name = "litellm" - host_name = var.host_name - resource_limits = { - cpu = "2" - memory = "4Gi" - } - replicas = 8 - aws_ingress_certificate_arn = var.aws_ingress_certificate_arn - db_url = var.db_url - litellm_salt_key = var.litellm_salt_key - litellm_master_key = var.litellm_master_key - redis_host = var.redis_host - redis_password = var.redis_password - gcloud_auth = var.gcloud_auth -} +# db = { +# use_existing = true +# db_name = "litellm" +# endpoint = split(":", data.aws_db_instance.litellm.endpoint)[0] +# username = "litellm" +# admin_password = var.db_admin_password +# user_password = var.db_user_password +# auth_type = "awsiamrds" +# } + +# proxy_config = { +# general_settings = { +# master_key = "os.environ/PROXY_MASTER_KEY" + +# store_model_in_db = true +# store_prompts_in_spend_logs = true +# proxy_batch_write_at = 60 +# database_connection_pool_limit = 10 + +# disable_error_logs = true +# allow_requests_on_db_unavailable = true +# } + +# litellm_settings = { +# allowed_fails = 3 +# cooldown_time = 30 +# num_retries = 1 +# request_timeout = 45 +# set_verbose = true +# json_logs = false +# cache = false +# } + +# model_list = [ +# { +# model_name = "anthropic.claude-opus-4-5-20251101-v1:0" +# litellm_params = { +# max_parallel_requests = 50 +# model = "vertex_ai/claude-opus-4-5@20251101" +# rpm = 450 +# tpm = 6000000 +# vertex_location = "us-east5" +# vertex_project = "coder-vertex-demos" +# vertex_credentials = "/tmp/gcloud/service_account.json" +# } +# }, +# { +# model_name = "anthropic.claude-opus-4-5-20251101-v1:0" +# litellm_params = { +# max_parallel_requests = 50 +# model = "vertex_ai/claude-opus-4-5@20251101" +# rpm = 450 +# tpm = 6000000 +# vertex_location = "europe-west1" +# vertex_project = "coder-vertex-demos" +# vertex_credentials = "/tmp/gcloud/service_account.json" +# } +# }, +# { +# model_name = "anthropic.claude-haiku-4-5-20251001-v1:0" +# litellm_params = { +# max_parallel_requests = 50 +# model = "vertex_ai/claude-haiku-4-5@20251001" +# rpm = 3000 +# tpm = 3000000 +# vertex_location = "us-east5" +# vertex_project = "coder-vertex-demos" +# vertex_credentials = "/tmp/gcloud/service_account.json" +# } +# }, +# { +# model_name = "anthropic.claude-haiku-4-5-20251001-v1:0" +# litellm_params = { +# max_parallel_requests = 50 +# model = "vertex_ai/claude-haiku-4-5@20251001" +# rpm = 3600 +# tpm = 3600000 +# vertex_location = "europe-west1" +# vertex_project = "coder-vertex-demos" +# vertex_credentials = "/tmp/gcloud/service_account.json" +# } +# }, +# { +# model_name = "claude-opus-4-6" +# litellm_params = { +# max_parallel_requests = 50 +# model = "vertex_ai/claude-opus-4-6@default" +# rpm = 450 +# tpm = 6000000 +# vertex_location = "us-east5" +# vertex_project = "coder-vertex-demos" +# vertex_credentials = "/tmp/gcloud/service_account.json" +# } +# }, +# { +# model_name = "claude-opus-4-6" +# litellm_params = { +# max_parallel_requests = 50 +# model = "vertex_ai/claude-opus-4-6@default" +# rpm = 450 +# tpm = 6000000 +# vertex_location = "europe-west1" +# vertex_project = "coder-vertex-demos" +# vertex_credentials = "/tmp/gcloud/service_account.json" +# } +# }, +# { +# model_name = "claude-opus-4-5" +# litellm_params = { +# max_parallel_requests = 50 +# model = "vertex_ai/claude-opus-4-5@20251101" +# rpm = 450 +# tpm = 6000000 +# vertex_location = "us-east5" +# vertex_project = "coder-vertex-demos" +# vertex_credentials = "/tmp/gcloud/service_account.json" +# } +# }, +# { +# model_name = "claude-opus-4-5" +# litellm_params = { +# max_parallel_requests = 50 +# model = "vertex_ai/claude-opus-4-5@20251101" +# rpm = 450 +# tpm = 6000000 +# vertex_location = "europe-west1" +# vertex_project = "coder-vertex-demos" +# vertex_credentials = "/tmp/gcloud/service_account.json" +# } +# }, +# { +# model_name = "claude-sonnet-4-6" +# litellm_params = { +# max_parallel_requests = 50 +# model = "vertex_ai/claude-sonnet-4-6@default" +# rpm = 450 +# tpm = 6000000 +# vertex_location = "us-east5" +# vertex_project = "coder-vertex-demos" +# vertex_credentials = "/tmp/gcloud/service_account.json" +# } +# }, +# { +# model_name = "claude-sonnet-4-6" +# litellm_params = { +# max_parallel_requests = 50 +# model = "vertex_ai/claude-sonnet-4-6@default" +# rpm = 450 +# tpm = 6000000 +# vertex_location = "europe-west1" +# vertex_project = "coder-vertex-demos" +# vertex_credentials = "/tmp/gcloud/service_account.json" +# } +# }, +# { +# model_name = "claude-sonnet-4-5" +# litellm_params = { +# max_parallel_requests = 50 +# model = "vertex_ai/claude-sonnet-4-5@20250929" +# rpm = 3000 +# tpm = 3000000 +# vertex_location = "us-east5" +# vertex_project = "coder-vertex-demos" +# vertex_credentials = "/tmp/gcloud/service_account.json" +# } +# }, +# { +# model_name = "claude-sonnet-4-5" +# litellm_params = { +# max_parallel_requests = 50 +# model = "vertex_ai/claude-sonnet-4-5@20250929" +# rpm = 3600 +# tpm = 3600000 +# vertex_location = "europe-west1" +# vertex_project = "coder-vertex-demos" +# vertex_credentials = "/tmp/gcloud/service_account.json" +# } +# }, +# { +# model_name = "claude-haiku-4-5" +# litellm_params = { +# max_parallel_requests = 50 +# model = "vertex_ai/claude-haiku-4-5@20251001" +# rpm = 3000 +# tpm = 3000000 +# vertex_location = "us-east5" +# vertex_project = "coder-vertex-demos" +# vertex_credentials = "/tmp/gcloud/service_account.json" +# } +# }, +# { +# model_name = "claude-haiku-4-5" +# litellm_params = { +# max_parallel_requests = 50 +# model = "vertex_ai/claude-haiku-4-5@20251001" +# rpm = 3600 +# tpm = 3600000 +# vertex_location = "europe-west1" +# vertex_project = "coder-vertex-demos" +# vertex_credentials = "/tmp/gcloud/service_account.json" +# } +# } +# ] + +# router_settings = { +# # enable_pre_call_checks = true +# # fallbacks = [{"claude-opus-4-6": ["claude-opus-4-5"]}] +# num_retries = 2 +# routing_strategy = "usage-based-routing-v2" + +# } +# } + +# mount_ssl = { +# enable = true +# secret_name = kubernetes_manifest.cert.manifest.metadata.name +# path = "/tmp/ssl" +# } -module "litellm-gen-key" { - depends_on = [module.litellm] - source = "../../../../../modules/k8s/bootstrap/litellm-generate-key" - - cluster_name = var.cluster_name - cluster_oidc_provider_arn = var.cluster_oidc_provider_arn - namespace = var.addon_namespace - name = "rotate-key" - image_repo = var.rotate_key_image_repo - image_tag = var.rotate_key_image_tag - litellm_create_secret = false - litellm_url = "https://${var.host_name}" - secret_id = var.aws_secret_id - secret_region = var.aws_secret_region -} \ No newline at end of file +# mounts = [{ +# secret_name = kubernetes_secret_v1.gcloud.metadata[0].name +# path = "/tmp/gcloud/" +# }] +# } \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/litellm/terragrunt.hcl b/infra/aws/us-east-2/k8s/litellm/terragrunt.hcl new file mode 100644 index 0000000..38f7c15 --- /dev/null +++ b/infra/aws/us-east-2/k8s/litellm/terragrunt.hcl @@ -0,0 +1,35 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks", + "../../rds", + "../lb-controller", + "../cert-manager", + "../other" # Deploy's auxillary manifests + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region=include.root.locals.CODER_AWS_REGION + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + addon_namespace=include.root.locals.LITELLM_ADDON_NAMESPACE + addon_version=include.root.locals.LITELLM_ADDON_VERSION + + host_name=include.root.locals.LITELLM_DOMAIN_NAME + vpc_name=include.root.locals.CODER_VPC_NAME + azs=include.root.locals.CODER_VPC_AZS + + db_rds_id=include.root.locals.LITELLM_DB_RDS_ID + db_admin_password=include.root.locals.LITELLM_DB_ADMIN_PASSWORD + db_user_password=include.root.locals.LITELLM_DB_USER_PASSWORD + + gcloud_auth=include.root.locals.LITELLM_GCLOUD_AUTH + litellm_master_key=include.root.locals.LITELLM_MASTER_KEY +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/litellm/variables.tf b/infra/aws/us-east-2/k8s/litellm/variables.tf new file mode 100644 index 0000000..192cd55 --- /dev/null +++ b/infra/aws/us-east-2/k8s/litellm/variables.tf @@ -0,0 +1,56 @@ +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "cluster_name" { + type = string +} + +variable "addon_namespace" { + type = string + default = "kube-system" +} + +variable "db_rds_id" { + type = string + sensitive = true +} + +variable "db_admin_password" { + type = string + sensitive = true +} + +variable "db_user_password" { + type = string + sensitive = true +} + +variable "litellm_master_key" { + type = string + sensitive = true +} + +variable "gcloud_auth" { + type = string + sensitive = true +} + +variable "host_name" { + type = string + sensitive = true +} + +variable "vpc_name" { + type = string +} + +variable "azs" { + type = list(string) + default = ["a", "b", "c"] +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/metrics-server/main.tf b/infra/aws/us-east-2/k8s/metrics-server/main.tf index fe2967e..6d5cd99 100644 --- a/infra/aws/us-east-2/k8s/metrics-server/main.tf +++ b/infra/aws/us-east-2/k8s/metrics-server/main.tf @@ -1,42 +1,6 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - helm = { - source = "hashicorp/helm" - version = ">= 2.17.0" - } - } - backend "s3" {} -} - -variable "cluster_name" { - type = string -} - -variable "cluster_region" { - type = string -} - -variable "cluster_profile" { - type = string - default = "default" -} - -variable "addon_namespace" { - type = string - default = "kube-system" -} - -variable "addon_version" { - type = string - default = "3.13.0" -} - provider "aws" { - region = var.cluster_region - profile = var.cluster_profile + region = var.region + profile = var.profile } data "aws_eks_cluster" "this" { @@ -60,7 +24,21 @@ module "metrics-server" { namespace = var.addon_namespace chart_version = var.addon_version - node_selector = { - "node.amazonaws.io/managed-by" = "asg" + tolerations = [{ + key = "CriticalAddonsOnly" + operator = "Exists" + }] + affinity = { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [{ + key = "karpenter.sh/nodepool" + operator = "In" + values = ["system"] + }] + }] + } + } } } \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/metrics-server/terragrunt.hcl b/infra/aws/us-east-2/k8s/metrics-server/terragrunt.hcl new file mode 100644 index 0000000..d7c1154 --- /dev/null +++ b/infra/aws/us-east-2/k8s/metrics-server/terragrunt.hcl @@ -0,0 +1,20 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +dependencies { + paths = [ + "../../eks" + ] +} + +inputs = { + profile=include.root.locals.CODER_AWS_PROFILE + region=include.root.locals.CODER_AWS_REGION + + cluster_name=include.root.locals.CODER_CLUSTER_NAME + + addon_namespace=include.root.locals.METRICS_SRV_ADDON_NAMESPACE + addon_version=include.root.locals.METRICS_SRV_ADDON_VERSION +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/metrics-server/variables.tf b/infra/aws/us-east-2/k8s/metrics-server/variables.tf new file mode 100644 index 0000000..6e7dfc0 --- /dev/null +++ b/infra/aws/us-east-2/k8s/metrics-server/variables.tf @@ -0,0 +1,22 @@ +variable "cluster_name" { + type = string +} + +variable "region" { + type = string +} + +variable "profile" { + type = string + default = "default" +} + +variable "addon_namespace" { + type = string + default = "kube-system" +} + +variable "addon_version" { + type = string + default = "3.13.0" +} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/monitoring/dashboards/aibridge-demoable.json b/infra/aws/us-east-2/k8s/monitoring/dashboards/aibridge-demoable.json deleted file mode 100644 index 70d570f..0000000 --- a/infra/aws/us-east-2/k8s/monitoring/dashboards/aibridge-demoable.json +++ /dev/null @@ -1,1382 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": 138, - "links": [], - "panels": [ - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 11, - "panels": [], - "title": "Usage leaderboards", - "type": "row" - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "text", - "value": 0 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 12, - "w": 4, - "x": 0, - "y": 1 - }, - "id": 3, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", - "targets": [ - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "editorMode": "code", - "format": "table", - "rawQuery": true, - "rawSql": "select sum(t.input_tokens) as input,\nsum(t.output_tokens) as output,\nsum(\n COALESCE(\n t.metadata->>'cache_read_input', -- Anthropic\n t.metadata->>'prompt_cached' -- OpenAI\n )::int\n) AS cache_read_input,\nsum((t.metadata->>'cache_creation_input')::int) AS cache_creation_input -- Anthropic\nfrom aibridge_token_usages t\njoin aibridge_interceptions i on t.interception_id = i.id\njoin users u on i.initiator_id = u.id\nwhere $__timeFilter(i.started_at)\n AND u.username ~ '${username:regex}'\n AND i.provider ~ '${provider:regex}'\n AND i.model ~ '${model:regex}'\norder by input desc", - "refId": "A", - "sql": { - "columns": [ - { - "parameters": [], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - } - } - ], - "title": "Total usage for $username", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": {}, - "includeByName": {}, - "indexByName": { - "cache_creation_input": 3, - "cache_read_input": 2, - "input": 0, - "output": 1 - }, - "renameByName": { - "cache_creation_input": "Cache Write", - "cache_read_input": "Cache Read", - "input": "Input", - "output": "Ouput" - } - } - } - ], - "type": "stat" - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "fillOpacity": 80, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineWidth": 1, - "scaleDistribution": { - "type": "linear" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "output" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "yellow", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Cache Read" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "green", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Input" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "orange", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Cache Write" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "light-red", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 12, - "w": 20, - "x": 4, - "y": 1 - }, - "id": 1, - "options": { - "barRadius": 0, - "barWidth": 0.97, - "fullHighlight": false, - "groupWidth": 0.7, - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "orientation": "auto", - "showValue": "auto", - "stacking": "none", - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - }, - "xTickLabelRotation": 0, - "xTickLabelSpacing": 0 - }, - "pluginVersion": "12.1.0", - "targets": [ - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "editorMode": "code", - "format": "table", - "rawQuery": true, - "rawSql": "select u.username, sum(t.input_tokens) as input,\nsum(t.output_tokens) as output,\nsum(\n COALESCE(\n t.metadata->>'cache_read_input', -- Anthropic\n t.metadata->>'prompt_cached' -- OpenAI\n )::int\n) AS cache_read_input,\nsum((t.metadata->>'cache_creation_input')::int) AS cache_creation_input -- Anthropic\nfrom aibridge_token_usages t\njoin aibridge_interceptions i on t.interception_id = i.id\njoin users u on i.initiator_id = u.id\nwhere $__timeFilter(i.started_at)\n AND u.username ~ '${username:regex}'\n AND i.provider ~ '${provider:regex}'\n AND i.model ~ '${model:regex}'\ngroup by u.username\norder by input desc", - "refId": "A", - "sql": { - "columns": [ - { - "parameters": [], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - } - } - ], - "title": "Leaderboard per user", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": {}, - "includeByName": {}, - "indexByName": {}, - "renameByName": { - "cache_creation_input": "Cache Write", - "cache_read_input": "Cache Read", - "input": "Input", - "output": "Output", - "username": "" - } - } - } - ], - "type": "barchart" - }, - { - "datasource": { - "type": "datasource", - "uid": "-- Mixed --" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byRegexp", - "options": "/.*Cost.*/" - }, - "properties": [ - { - "id": "unit", - "value": "currencyUSD" - }, - { - "id": "decimals", - "value": 2 - } - ] - } - ] - }, - "gridPos": { - "h": 9, - "w": 24, - "x": 0, - "y": 13 - }, - "id": 12, - "options": { - "cellHeight": "sm", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": true - }, - "frameIndex": 0, - "showHeader": true, - "sortBy": [ - { - "desc": true, - "displayName": "Total Cost" - } - ] - }, - "pluginVersion": "12.1.0", - "targets": [ - { - "columns": [], - "computed_columns": [], - "datasource": { - "type": "yesoreyeram-infinity-datasource", - "uid": "dezthk9i9m48we" - }, - "filterExpression": "", - "filters": [], - "format": "table", - "global_query_id": "", - "hide": false, - "pagination_mode": "none", - "parser": "backend", - "refId": "A", - "root_selector": "$ ~> $each(function($v, $k) {\n {\n \"model\": $k,\n \"input_cost_per_token\": $v.input_cost_per_token ? $v.input_cost_per_token : 0,\n \"output_cost_per_token\": $v.output_cost_per_token ? $v.output_cost_per_token : 0,\n \"cache_creation_input_token_cost\": $v.cache_creation_input_token_cost ? $v.cache_creation_input_token_cost : 0,\n \"cache_read_input_token_cost\": $v.cache_read_input_token_cost ? $v.cache_read_input_token_cost : 0\n }\n})", - "source": "url", - "type": "json", - "url": "", - "url_options": { - "data": "", - "method": "GET" - } - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "editorMode": "code", - "format": "table", - "hide": false, - "rawQuery": true, - "rawSql": "select i.provider, i.model,\nsum(t.input_tokens) as input,\nsum(t.output_tokens) as output,\nsum(\n COALESCE(\n t.metadata->>'cache_read_input', -- Anthropic\n t.metadata->>'prompt_cached' -- OpenAI\n )::int\n) AS cache_read_input,\nsum((t.metadata->>'cache_creation_input')::int) AS cache_creation_input -- Anthropic\nfrom aibridge_token_usages t\njoin aibridge_interceptions i on t.interception_id = i.id\njoin users u on i.initiator_id = u.id\nwhere $__timeFilter(i.started_at)\n AND u.username ~ '${username:regex}'\n AND i.provider ~ '${provider:regex}'\n AND i.model ~ '${model:regex}'\ngroup by i.provider, i.model\norder by input desc", - "refId": "B", - "sql": { - "columns": [ - { - "parameters": [], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - } - } - ], - "title": "Approximate Cost", - "transformations": [ - { - "id": "joinByField", - "options": { - "byField": "model", - "mode": "inner" - } - }, - { - "id": "calculateField", - "options": { - "alias": "Input Cost", - "binary": { - "left": { - "matcher": { - "id": "byName", - "options": "input_cost_per_token A" - } - }, - "operator": "*", - "right": { - "matcher": { - "id": "byName", - "options": "input" - } - } - }, - "mode": "binary", - "reduce": { - "include": [ - "input_cost_per_token A", - "input" - ], - "reducer": "sum" - } - } - }, - { - "id": "calculateField", - "options": { - "alias": "Output Cost", - "binary": { - "left": { - "matcher": { - "id": "byName", - "options": "output_cost_per_token A" - } - }, - "operator": "*", - "right": { - "matcher": { - "id": "byName", - "options": "output" - } - } - }, - "mode": "binary", - "reduce": { - "reducer": "sum" - } - } - }, - { - "id": "calculateField", - "options": { - "alias": "Cache Read Cost", - "binary": { - "left": { - "matcher": { - "id": "byName", - "options": "cache_read_input_token_cost A" - } - }, - "operator": "*", - "right": { - "matcher": { - "id": "byName", - "options": "cache_read_input" - } - } - }, - "mode": "binary", - "reduce": { - "reducer": "sum" - } - } - }, - { - "id": "calculateField", - "options": { - "alias": "Cache Write Cost", - "binary": { - "left": { - "matcher": { - "id": "byName", - "options": "cache_creation_input_token_cost A" - } - }, - "operator": "*", - "right": { - "matcher": { - "id": "byName", - "options": "cache_creation_input" - } - } - }, - "mode": "binary", - "reduce": { - "reducer": "sum" - } - } - }, - { - "id": "calculateField", - "options": { - "alias": "Total Cost", - "binary": { - "left": { - "matcher": { - "id": "byType", - "options": "number" - } - }, - "right": { - "fixed": "" - } - }, - "cumulative": { - "field": "Input Cost", - "reducer": "sum" - }, - "mode": "reduceRow", - "reduce": { - "include": [ - "Input Cost", - "Output Cost", - "Cache Read Cost", - "Cache Write Cost" - ], - "reducer": "sum" - } - } - }, - { - "id": "organize", - "options": { - "excludeByName": { - "cache_creation_input": false, - "cache_creation_input_token_cost A": true, - "cache_read_input": false, - "cache_read_input_token_cost A": true, - "input": false, - "input_cost_per_token A": true, - "output": false, - "output_cost_per_token A": true - }, - "includeByName": {}, - "indexByName": { - "Cache Read Cost": 13, - "Cache Write Cost": 14, - "Input Cost": 11, - "Output Cost": 12, - "Total Cost": 2, - "cache_creation_input": 10, - "cache_creation_input_token_cost A": 3, - "cache_read_input": 9, - "cache_read_input_token_cost A": 4, - "input": 7, - "input_cost_per_token A": 5, - "model": 1, - "output": 8, - "output_cost_per_token A": 6, - "provider": 0 - }, - "renameByName": { - "cache_creation_input": "Cache Write", - "cache_read_input": "Cache Read", - "input": "Input", - "model": "Model", - "output": "Output", - "provider": "Provider" - } - } - } - ], - "type": "table" - }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 22 - }, - "id": 10, - "panels": [], - "title": "Interceptions", - "type": "row" - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "output" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "orange", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 12, - "w": 4, - "x": 0, - "y": 23 - }, - "id": 5, - "interval": "1m", - "maxDataPoints": 500, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", - "targets": [ - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "editorMode": "code", - "format": "table", - "rawQuery": true, - "rawSql": "select count(*) from aibridge_interceptions\nWHERE started_at > $__timeFrom() AND started_at <= $__timeTo()\nAND provider ~ '${provider:regex}'\nAND model ~ '${model:regex}'", - "refId": "A", - "sql": { - "columns": [ - { - "name": "COUNT", - "parameters": [ - { - "name": "id", - "type": "functionParameter" - } - ], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "name": "started_at", - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - }, - "table": "aibridge_interceptions" - } - ], - "title": "Total interceptions", - "type": "stat" - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "fillOpacity": 80, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineWidth": 1, - "scaleDistribution": { - "type": "linear" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "fieldMinMax": false, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "output" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "orange", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 12, - "w": 20, - "x": 4, - "y": 23 - }, - "id": 4, - "maxDataPoints": 30, - "options": { - "barRadius": 0, - "barWidth": 0.97, - "fullHighlight": false, - "groupWidth": 0.7, - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "orientation": "auto", - "showValue": "auto", - "stacking": "normal", - "text": { - "valueSize": 10 - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - }, - "xTickLabelRotation": -45, - "xTickLabelSpacing": 0 - }, - "pluginVersion": "12.1.0", - "targets": [ - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "editorMode": "code", - "format": "time_series", - "rawQuery": true, - "rawSql": "SELECT\n$__timeGroupAlias(i.started_at, $__interval, NULL),\ncount(i.id) AS value,\nu.username AS metric\nFROM aibridge_interceptions i\njoin users u ON i.initiator_id = u.id\nWHERE\n$__timeFilter(i.started_at)\nAND u.username ~ '${username:regex}'\nAND i.provider ~ '${provider:regex}'\nAND i.model ~ '${model:regex}'\nGROUP BY u.username, $__timeGroup(i.started_at, $__interval)\nORDER BY $__timeGroup(i.started_at, $__interval)", - "refId": "A", - "sql": { - "columns": [ - { - "name": "COUNT", - "parameters": [ - { - "name": "id", - "type": "functionParameter" - } - ], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "name": "started_at", - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - }, - "table": "aibridge_interceptions" - } - ], - "title": "Interceptions over time by user", - "type": "barchart" - }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 35 - }, - "id": 9, - "panels": [], - "title": "Prompts & tool calls details", - "type": "row" - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto", - "wrapText": false - }, - "inspect": true - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Interception ID" - }, - "properties": [ - { - "id": "custom.width", - "value": 357 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Model" - }, - "properties": [ - { - "id": "custom.width", - "value": 240 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Provider" - }, - "properties": [ - { - "id": "custom.width", - "value": 157 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Username" - }, - "properties": [ - { - "id": "custom.width", - "value": 188 - } - ] - } - ] - }, - "gridPos": { - "h": 14, - "w": 24, - "x": 0, - "y": 36 - }, - "id": 7, - "options": { - "cellHeight": "sm", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true, - "sortBy": [ - { - "desc": true, - "displayName": "Created At" - } - ] - }, - "pluginVersion": "12.1.0", - "targets": [ - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "editorMode": "code", - "format": "table", - "rawQuery": true, - "rawSql": "SELECT i.id,\n u.username,\n i.provider,\n i.model,\n 'REDACTED' as prompt,\n p.created_at\nFROM aibridge_user_prompts p\nJOIN aibridge_interceptions i ON p.interception_id = i.id\nJOIN users u ON i.initiator_id = u.id\nWHERE $__timeFilter(i.started_at)\n AND u.username ~ '${username:regex}'\n AND i.provider ~ '${provider:regex}'\n AND i.model ~ '${model:regex}'\nORDER BY p.created_at DESC;", - "refId": "A", - "sql": { - "columns": [ - { - "parameters": [], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - } - } - ], - "title": "User Prompts", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": {}, - "includeByName": {}, - "indexByName": {}, - "renameByName": { - "created_at": "Created At", - "id": "Interception ID", - "input": "Tool Input", - "invocation_error": "Tool Error", - "model": "Model", - "prompt": "Prompt", - "provider": "Provider", - "server_url": "MCP Server", - "tool": "Tool Name", - "username": "Username" - } - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto", - "wrapText": false - }, - "inspect": true - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Tool Name" - }, - "properties": [ - { - "id": "custom.width", - "value": 342 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "invocation_error" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "applyToRow": true, - "type": "color-background", - "wrapText": false - } - }, - { - "id": "noValue" - }, - { - "id": "mappings", - "value": [ - { - "options": { - "match": "null", - "result": { - "color": "green", - "index": 0 - } - }, - "type": "special" - }, - { - "options": { - "pattern": ".+", - "result": { - "color": "red", - "index": 1 - } - }, - "type": "regex" - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Tool Input" - }, - "properties": [ - { - "id": "custom.width", - "value": 309 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Interception ID" - }, - "properties": [ - { - "id": "custom.width", - "value": 357 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Model" - }, - "properties": [ - { - "id": "custom.width", - "value": 240 - } - ] - } - ] - }, - "gridPos": { - "h": 14, - "w": 24, - "x": 0, - "y": 50 - }, - "id": 6, - "options": { - "cellHeight": "sm", - "footer": { - "countRows": false, - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "showHeader": true, - "sortBy": [ - { - "desc": true, - "displayName": "Created At" - } - ] - }, - "pluginVersion": "12.1.0", - "targets": [ - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "editorMode": "code", - "format": "table", - "rawQuery": true, - "rawSql": "select i.id, u.username, i.provider, i.model, t.server_url, t.tool, 'REDACTED' as input, CASE WHEN t.invocation_error IS NULL THEN '' ELSE 'REDACTED' END as invocation_error, t.created_at FROM aibridge_tool_usages t\njoin aibridge_interceptions i ON t.interception_id = i.id\njoin users u on i.initiator_id = u.id\nwhere $__timeFilter(i.started_at)\nAND u.username ~ '${username:regex}'\nAND i.provider ~ '${provider:regex}'\nAND i.model ~ '${model:regex}'\norder by t.created_at desc", - "refId": "A", - "sql": { - "columns": [ - { - "parameters": [], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - } - } - ], - "title": "Tool Calls", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": {}, - "includeByName": {}, - "indexByName": {}, - "renameByName": { - "created_at": "Created At", - "id": "Interception ID", - "input": "Tool Input", - "invocation_error": "Tool Error", - "model": "Model", - "provider": "Provider", - "server_url": "MCP Server", - "tool": "Tool Name", - "username": "Username" - } - } - } - ], - "type": "table" - } - ], - "preload": false, - "refresh": "1m", - "schemaVersion": 41, - "tags": [], - "templating": { - "list": [ - { - "allValue": ".+", - "current": { - "text": "All", - "value": [ - "$__all" - ] - }, - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "definition": "select username from users where deleted=false;", - "description": "", - "includeAll": true, - "multi": true, - "name": "username", - "options": [], - "query": "select username from users where deleted=false;", - "refresh": 1, - "regex": "", - "sort": 1, - "type": "query" - }, - { - "allValue": ".+", - "current": { - "text": "All", - "value": [ - "$__all" - ] - }, - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "definition": "SELECT DISTINCT provider FROM aibridge_interceptions WHERE provider IS NOT NULL ORDER BY 1;", - "description": "", - "includeAll": true, - "multi": true, - "name": "provider", - "options": [], - "query": "SELECT DISTINCT provider FROM aibridge_interceptions WHERE provider IS NOT NULL ORDER BY 1;", - "refresh": 1, - "regex": "", - "sort": 1, - "type": "query" - }, - { - "allValue": ".+", - "current": { - "text": "All", - "value": [ - "$__all" - ] - }, - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "aezivxsm5pnggc" - }, - "definition": "SELECT DISTINCT model FROM aibridge_interceptions WHERE model IS NOT NULL AND provider ~ '${provider:regex}' ORDER BY 1;", - "description": "", - "includeAll": true, - "multi": true, - "name": "model", - "options": [], - "query": "SELECT DISTINCT model FROM aibridge_interceptions WHERE model IS NOT NULL AND provider ~ '${provider:regex}' ORDER BY 1;", - "refresh": 1, - "regex": "", - "sort": 1, - "type": "query" - } - ] - }, - "time": { - "from": "now-7d", - "to": "now" - }, - "timepicker": {}, - "timezone": "utc", - "title": "aibridge [DEMOABLE]", - "uid": "0c61d33f-c809-4184-9e88-cb27e2d9d223", - "version": 2 -} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/monitoring/dashboards/aibridge-example.json b/infra/aws/us-east-2/k8s/monitoring/dashboards/aibridge-example.json deleted file mode 100644 index 304d49a..0000000 --- a/infra/aws/us-east-2/k8s/monitoring/dashboards/aibridge-example.json +++ /dev/null @@ -1,1411 +0,0 @@ -{ - "__inputs": [ - { - "name": "DS_CODER-OBSERVABILITY-RO", - "label": "coder-observability-ro", - "description": "", - "type": "datasource", - "pluginId": "grafana-postgresql-datasource", - "pluginName": "PostgreSQL" - }, - { - "name": "DS_LITELLM-PRICING-DATA", - "label": "litellm-pricing-data", - "description": "", - "type": "datasource", - "pluginId": "yesoreyeram-infinity-datasource", - "pluginName": "Infinity" - } - ], - "__elements": {}, - "__requires": [ - { - "type": "panel", - "id": "barchart", - "name": "Bar chart", - "version": "" - }, - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "12.1.0" - }, - { - "type": "datasource", - "id": "grafana-postgresql-datasource", - "name": "PostgreSQL", - "version": "12.1.0" - }, - { - "type": "panel", - "id": "stat", - "name": "Stat", - "version": "" - }, - { - "type": "panel", - "id": "table", - "name": "Table", - "version": "" - }, - { - "type": "datasource", - "id": "yesoreyeram-infinity-datasource", - "name": "Infinity", - "version": "3.6.0" - } - ], - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": null, - "links": [], - "panels": [ - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 11, - "panels": [], - "title": "Usage leaderboards", - "type": "row" - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "fillOpacity": 80, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineWidth": 1, - "scaleDistribution": { - "type": "linear" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "output" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "yellow", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Cache Read" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "green", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Input" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "orange", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Cache Write" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "light-red", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 12, - "w": 20, - "x": 0, - "y": 1 - }, - "id": 1, - "options": { - "barRadius": 0, - "barWidth": 0.97, - "fullHighlight": false, - "groupWidth": 0.7, - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "orientation": "auto", - "showValue": "auto", - "stacking": "none", - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - }, - "xTickLabelRotation": 0, - "xTickLabelSpacing": 0 - }, - "pluginVersion": "12.1.0", - "targets": [ - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "editorMode": "code", - "format": "table", - "rawQuery": true, - "rawSql": "select u.username, sum(t.input_tokens) as input,\nsum(t.output_tokens) as output,\nsum(\n COALESCE(\n t.metadata->>'cache_read_input', -- Anthropic\n t.metadata->>'prompt_cached' -- OpenAI\n )::int\n) AS cache_read_input,\nsum((t.metadata->>'cache_creation_input')::int) AS cache_creation_input -- Anthropic\nfrom aibridge_token_usages t\njoin aibridge_interceptions i on t.interception_id = i.id\njoin users u on i.initiator_id = u.id\nwhere $__timeFilter(i.started_at)\n AND u.username ~ '${username:regex}'\n AND i.provider ~ '${provider:regex}'\n AND i.model ~ '${model:regex}'\ngroup by u.username\norder by input desc", - "refId": "A", - "sql": { - "columns": [ - { - "parameters": [], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - } - } - ], - "title": "Leaderboard per user", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": {}, - "includeByName": {}, - "indexByName": {}, - "renameByName": { - "cache_creation_input": "Cache Write", - "cache_read_input": "Cache Read", - "input": "Input", - "output": "Output", - "username": "" - } - } - } - ], - "type": "barchart" - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "text", - "value": 0 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 12, - "w": 4, - "x": 20, - "y": 1 - }, - "id": 3, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", - "targets": [ - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "editorMode": "code", - "format": "table", - "rawQuery": true, - "rawSql": "select sum(t.input_tokens) as input,\nsum(t.output_tokens) as output,\nsum(\n COALESCE(\n t.metadata->>'cache_read_input', -- Anthropic\n t.metadata->>'prompt_cached' -- OpenAI\n )::int\n) AS cache_read_input,\nsum((t.metadata->>'cache_creation_input')::int) AS cache_creation_input -- Anthropic\nfrom aibridge_token_usages t\njoin aibridge_interceptions i on t.interception_id = i.id\njoin users u on i.initiator_id = u.id\nwhere $__timeFilter(i.started_at)\n AND u.username ~ '${username:regex}'\n AND i.provider ~ '${provider:regex}'\n AND i.model ~ '${model:regex}'\norder by input desc", - "refId": "A", - "sql": { - "columns": [ - { - "parameters": [], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - } - } - ], - "title": "Total usage for $username", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": {}, - "includeByName": {}, - "indexByName": { - "cache_creation_input": 3, - "cache_read_input": 2, - "input": 0, - "output": 1 - }, - "renameByName": { - "cache_creation_input": "Cache Write", - "cache_read_input": "Cache Read", - "input": "Input", - "output": "Output" - } - } - } - ], - "type": "stat" - }, - { - "datasource": { - "type": "datasource", - "uid": "-- Mixed --" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byRegexp", - "options": "/.*Cost.*/" - }, - "properties": [ - { - "id": "unit", - "value": "currencyUSD" - }, - { - "id": "decimals", - "value": 2 - } - ] - } - ] - }, - "gridPos": { - "h": 9, - "w": 24, - "x": 0, - "y": 13 - }, - "id": 12, - "options": { - "cellHeight": "sm", - "footer": { - "countRows": false, - "fields": "", - "reducer": ["sum"], - "show": true - }, - "frameIndex": 0, - "showHeader": true, - "sortBy": [ - { - "desc": true, - "displayName": "Total Cost" - } - ] - }, - "pluginVersion": "12.1.0", - "targets": [ - { - "columns": [], - "computed_columns": [], - "datasource": { - "type": "yesoreyeram-infinity-datasource", - "uid": "${DS_LITELLM-PRICING-DATA}" - }, - "filterExpression": "", - "filters": [], - "format": "table", - "global_query_id": "", - "hide": false, - "pagination_mode": "none", - "parser": "backend", - "refId": "A", - "root_selector": "$ ~> $each(function($v, $k) {\n {\n \"model\": $k,\n \"input_cost_per_token\": $v.input_cost_per_token ? $v.input_cost_per_token : 0,\n \"output_cost_per_token\": $v.output_cost_per_token ? $v.output_cost_per_token : 0,\n \"cache_creation_input_token_cost\": $v.cache_creation_input_token_cost ? $v.cache_creation_input_token_cost : 0,\n \"cache_read_input_token_cost\": $v.cache_read_input_token_cost ? $v.cache_read_input_token_cost : 0\n }\n})", - "source": "url", - "type": "json", - "url": "", - "url_options": { - "data": "", - "method": "GET" - } - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "editorMode": "code", - "format": "table", - "hide": false, - "rawQuery": true, - "rawSql": "select i.provider, i.model,\nsum(t.input_tokens) as input,\nsum(t.output_tokens) as output,\nsum(\n COALESCE(\n t.metadata->>'cache_read_input', -- Anthropic\n t.metadata->>'prompt_cached' -- OpenAI\n )::int\n) AS cache_read_input,\nsum((t.metadata->>'cache_creation_input')::int) AS cache_creation_input -- Anthropic\nfrom aibridge_token_usages t\njoin aibridge_interceptions i on t.interception_id = i.id\njoin users u on i.initiator_id = u.id\nwhere $__timeFilter(i.started_at)\n AND u.username ~ '${username:regex}'\n AND i.provider ~ '${provider:regex}'\n AND i.model ~ '${model:regex}'\ngroup by i.provider, i.model\norder by input desc", - "refId": "B", - "sql": { - "columns": [ - { - "parameters": [], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - } - } - ], - "title": "Approximate Cost", - "transformations": [ - { - "id": "joinByField", - "options": { - "byField": "model", - "mode": "inner" - } - }, - { - "id": "calculateField", - "options": { - "alias": "Input Cost", - "binary": { - "left": { - "matcher": { - "id": "byName", - "options": "input_cost_per_token A" - } - }, - "operator": "*", - "right": { - "matcher": { - "id": "byName", - "options": "input" - } - } - }, - "mode": "binary", - "reduce": { - "include": ["input_cost_per_token A", "input"], - "reducer": "sum" - } - } - }, - { - "id": "calculateField", - "options": { - "alias": "Output Cost", - "binary": { - "left": { - "matcher": { - "id": "byName", - "options": "output_cost_per_token A" - } - }, - "operator": "*", - "right": { - "matcher": { - "id": "byName", - "options": "output" - } - } - }, - "mode": "binary", - "reduce": { - "reducer": "sum" - } - } - }, - { - "id": "calculateField", - "options": { - "alias": "Cache Read Cost", - "binary": { - "left": { - "matcher": { - "id": "byName", - "options": "cache_read_input_token_cost A" - } - }, - "operator": "*", - "right": { - "matcher": { - "id": "byName", - "options": "cache_read_input" - } - } - }, - "mode": "binary", - "reduce": { - "reducer": "sum" - } - } - }, - { - "id": "calculateField", - "options": { - "alias": "Cache Write Cost", - "binary": { - "left": { - "matcher": { - "id": "byName", - "options": "cache_creation_input_token_cost A" - } - }, - "operator": "*", - "right": { - "matcher": { - "id": "byName", - "options": "cache_creation_input" - } - } - }, - "mode": "binary", - "reduce": { - "reducer": "sum" - } - } - }, - { - "id": "calculateField", - "options": { - "alias": "Total Cost", - "binary": { - "left": { - "matcher": { - "id": "byType", - "options": "number" - } - }, - "right": { - "fixed": "" - } - }, - "cumulative": { - "field": "Input Cost", - "reducer": "sum" - }, - "mode": "reduceRow", - "reduce": { - "include": [ - "Input Cost", - "Output Cost", - "Cache Read Cost", - "Cache Write Cost" - ], - "reducer": "sum" - } - } - }, - { - "id": "organize", - "options": { - "excludeByName": { - "cache_creation_input": false, - "cache_creation_input_token_cost A": true, - "cache_read_input": false, - "cache_read_input_token_cost A": true, - "input": false, - "input_cost_per_token A": true, - "output": false, - "output_cost_per_token A": true - }, - "includeByName": {}, - "indexByName": { - "Cache Read Cost": 12, - "Cache Write Cost": 13, - "Input Cost": 10, - "Output Cost": 11, - "Total Cost": 14, - "cache_creation_input": 9, - "cache_creation_input_token_cost A": 2, - "cache_read_input": 8, - "cache_read_input_token_cost A": 3, - "input": 6, - "input_cost_per_token A": 4, - "model": 1, - "output": 7, - "output_cost_per_token A": 5, - "provider": 0 - }, - "renameByName": { - "cache_creation_input": "Cache Write", - "cache_read_input": "Cache Read", - "input": "Input", - "model": "Model", - "output": "Output", - "provider": "Provider" - } - } - } - ], - "type": "table" - }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 22 - }, - "id": 10, - "panels": [], - "title": "Interceptions", - "type": "row" - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "fillOpacity": 80, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineWidth": 1, - "scaleDistribution": { - "type": "linear" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "fieldMinMax": false, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "output" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "orange", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 12, - "w": 20, - "x": 0, - "y": 23 - }, - "id": 4, - "maxDataPoints": 30, - "options": { - "barRadius": 0, - "barWidth": 0.97, - "fullHighlight": false, - "groupWidth": 0.7, - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "orientation": "auto", - "showValue": "auto", - "stacking": "normal", - "text": { - "valueSize": 10 - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - }, - "xTickLabelRotation": -45, - "xTickLabelSpacing": 0 - }, - "pluginVersion": "12.1.0", - "targets": [ - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "editorMode": "code", - "format": "time_series", - "rawQuery": true, - "rawSql": "SELECT\n$__timeGroupAlias(i.started_at, $__interval, NULL),\ncount(i.id) AS value,\nu.username AS metric\nFROM aibridge_interceptions i\njoin users u ON i.initiator_id = u.id\nWHERE\n$__timeFilter(i.started_at)\nAND u.username ~ '${username:regex}'\nAND i.provider ~ '${provider:regex}'\nAND i.model ~ '${model:regex}'\nGROUP BY u.username, $__timeGroup(i.started_at, $__interval)\nORDER BY $__timeGroup(i.started_at, $__interval)", - "refId": "A", - "sql": { - "columns": [ - { - "name": "COUNT", - "parameters": [ - { - "name": "id", - "type": "functionParameter" - } - ], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "name": "started_at", - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - }, - "table": "aibridge_interceptions" - } - ], - "title": "Interceptions over time by user", - "type": "barchart" - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - } - ] - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "output" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "orange", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 12, - "w": 4, - "x": 20, - "y": 23 - }, - "id": 5, - "interval": "1m", - "maxDataPoints": 500, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.1.0", - "targets": [ - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "editorMode": "code", - "format": "table", - "rawQuery": true, - "rawSql": "select count(*) from aibridge_interceptions\nWHERE started_at > $__timeFrom() AND started_at <= $__timeTo()\nAND provider ~ '${provider:regex}'\nAND model ~ '${model:regex}'", - "refId": "A", - "sql": { - "columns": [ - { - "name": "COUNT", - "parameters": [ - { - "name": "id", - "type": "functionParameter" - } - ], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "name": "started_at", - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - }, - "table": "aibridge_interceptions" - } - ], - "title": "Total interceptions", - "type": "stat" - }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 35 - }, - "id": 9, - "panels": [], - "title": "Prompts & tool calls details", - "type": "row" - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto", - "wrapText": false - }, - "inspect": true - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Interception ID" - }, - "properties": [ - { - "id": "custom.width", - "value": 357 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Model" - }, - "properties": [ - { - "id": "custom.width", - "value": 240 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Provider" - }, - "properties": [ - { - "id": "custom.width", - "value": 157 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Username" - }, - "properties": [ - { - "id": "custom.width", - "value": 188 - } - ] - } - ] - }, - "gridPos": { - "h": 14, - "w": 24, - "x": 0, - "y": 36 - }, - "id": 7, - "options": { - "cellHeight": "sm", - "footer": { - "countRows": false, - "fields": "", - "reducer": ["sum"], - "show": false - }, - "showHeader": true, - "sortBy": [ - { - "desc": true, - "displayName": "Created At" - } - ] - }, - "pluginVersion": "12.1.0", - "targets": [ - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "editorMode": "code", - "format": "table", - "rawQuery": true, - "rawSql": "SELECT i.id,\n u.username,\n i.provider,\n i.model,\n p.prompt,\n p.created_at\nFROM aibridge_user_prompts p\nJOIN aibridge_interceptions i ON p.interception_id = i.id\nJOIN users u ON i.initiator_id = u.id\nWHERE $__timeFilter(i.started_at)\n AND u.username ~ '${username:regex}'\n AND i.provider ~ '${provider:regex}'\n AND i.model ~ '${model:regex}'\nORDER BY p.created_at DESC;", - "refId": "A", - "sql": { - "columns": [ - { - "parameters": [], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - } - } - ], - "title": "User Prompts", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": {}, - "includeByName": {}, - "indexByName": {}, - "renameByName": { - "created_at": "Created At", - "id": "Interception ID", - "input": "Tool Input", - "invocation_error": "Tool Error", - "model": "Model", - "prompt": "Prompt", - "provider": "Provider", - "server_url": "MCP Server", - "tool": "Tool Name", - "username": "Username" - } - } - } - ], - "type": "table" - }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto", - "wrapText": false - }, - "inspect": true - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Tool Name" - }, - "properties": [ - { - "id": "custom.width", - "value": 342 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "invocation_error" - }, - "properties": [ - { - "id": "custom.cellOptions", - "value": { - "applyToRow": true, - "type": "color-background", - "wrapText": false - } - }, - { - "id": "noValue" - }, - { - "id": "mappings", - "value": [ - { - "options": { - "match": "null", - "result": { - "color": "green", - "index": 0 - } - }, - "type": "special" - }, - { - "options": { - "pattern": ".+", - "result": { - "color": "red", - "index": 1 - } - }, - "type": "regex" - } - ] - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Tool Input" - }, - "properties": [ - { - "id": "custom.width", - "value": 309 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Interception ID" - }, - "properties": [ - { - "id": "custom.width", - "value": 357 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Model" - }, - "properties": [ - { - "id": "custom.width", - "value": 240 - } - ] - } - ] - }, - "gridPos": { - "h": 14, - "w": 24, - "x": 0, - "y": 50 - }, - "id": 6, - "options": { - "cellHeight": "sm", - "footer": { - "countRows": false, - "fields": "", - "reducer": ["sum"], - "show": false - }, - "showHeader": true, - "sortBy": [ - { - "desc": true, - "displayName": "Created At" - } - ] - }, - "pluginVersion": "12.1.0", - "targets": [ - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "editorMode": "code", - "format": "table", - "rawQuery": true, - "rawSql": "select i.id, u.username, i.provider, i.model, t.server_url, t.tool, t.input, t.invocation_error, t.created_at FROM aibridge_tool_usages t\njoin aibridge_interceptions i ON t.interception_id = i.id\njoin users u on i.initiator_id = u.id\nwhere $__timeFilter(i.started_at)\nAND u.username ~ '${username:regex}'\nAND i.provider ~ '${provider:regex}'\nAND i.model ~ '${model:regex}'\norder by t.created_at desc", - "refId": "A", - "sql": { - "columns": [ - { - "parameters": [], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - } - } - ], - "title": "Tool Calls", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": {}, - "includeByName": {}, - "indexByName": {}, - "renameByName": { - "created_at": "Created At", - "id": "Interception ID", - "input": "Tool Input", - "invocation_error": "Tool Error", - "model": "Model", - "provider": "Provider", - "server_url": "MCP Server", - "tool": "Tool Name", - "username": "Username" - } - } - } - ], - "type": "table" - } - ], - "refresh": "1m", - "schemaVersion": 41, - "tags": [], - "templating": { - "list": [ - { - "allValue": ".+", - "current": {}, - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "definition": "select username from users where deleted=false;", - "description": "", - "includeAll": true, - "multi": true, - "name": "username", - "options": [], - "query": "select username from users where deleted=false;", - "refresh": 1, - "regex": "", - "sort": 1, - "type": "query" - }, - { - "allValue": ".+", - "current": {}, - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "definition": "SELECT DISTINCT provider FROM aibridge_interceptions WHERE provider IS NOT NULL ORDER BY 1;", - "description": "", - "includeAll": true, - "multi": true, - "name": "provider", - "options": [], - "query": "SELECT DISTINCT provider FROM aibridge_interceptions WHERE provider IS NOT NULL ORDER BY 1;", - "refresh": 1, - "regex": "", - "sort": 1, - "type": "query" - }, - { - "allValue": ".+", - "current": {}, - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "postgres" - }, - "definition": "SELECT DISTINCT model FROM aibridge_interceptions WHERE model IS NOT NULL AND provider ~ '${provider:regex}' ORDER BY 1;", - "description": "", - "includeAll": true, - "multi": true, - "name": "model", - "options": [], - "query": "SELECT DISTINCT model FROM aibridge_interceptions WHERE model IS NOT NULL AND provider ~ '${provider:regex}' ORDER BY 1;", - "refresh": 1, - "regex": "", - "sort": 1, - "type": "query" - } - ] - }, - "time": { - "from": "now-7d", - "to": "now" - }, - "timepicker": {}, - "timezone": "utc", - "title": "aibridge", - "uid": "0c61d33f-c809-4184-9e88-cb27e2d9d224", - "version": 43, - "weekStart": "" -} \ No newline at end of file diff --git a/infra/aws/us-east-2/k8s/monitoring/dashboards/aibridge.json b/infra/aws/us-east-2/k8s/monitoring/dashboards/aibridge.json index 0d1fb72..f7a5286 100644 --- a/infra/aws/us-east-2/k8s/monitoring/dashboards/aibridge.json +++ b/infra/aws/us-east-2/k8s/monitoring/dashboards/aibridge.json @@ -5,7 +5,7 @@ "label": "coder-observability-ro", "description": "", "type": "datasource", - "pluginId": "grafana-postgresql-datasource", + "pluginId": "postgres", "pluginName": "PostgreSQL" } ], @@ -25,7 +25,7 @@ }, { "type": "datasource", - "id": "grafana-postgresql-datasource", + "id": "postgres", "name": "PostgreSQL", "version": "12.1.0" }, @@ -79,8 +79,8 @@ }, { "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "fieldConfig": { "defaults": { @@ -216,8 +216,8 @@ "targets": [ { "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "editorMode": "code", "format": "table", @@ -265,8 +265,8 @@ }, { "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "fieldConfig": { "defaults": { @@ -313,8 +313,8 @@ "targets": [ { "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "editorMode": "code", "format": "table", @@ -379,8 +379,8 @@ }, { "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "fieldConfig": { "defaults": { @@ -476,8 +476,8 @@ "targets": [ { "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "editorMode": "code", "format": "time_series", @@ -516,8 +516,8 @@ }, { "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "fieldConfig": { "defaults": { @@ -582,8 +582,8 @@ "targets": [ { "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "editorMode": "code", "format": "table", @@ -635,8 +635,8 @@ }, { "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "fieldConfig": { "defaults": { @@ -744,8 +744,8 @@ "targets": [ { "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "editorMode": "code", "format": "table", @@ -798,8 +798,8 @@ }, { "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "fieldConfig": { "defaults": { @@ -951,8 +951,8 @@ "targets": [ { "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "editorMode": "code", "format": "table", @@ -1012,8 +1012,8 @@ "allValue": ".+", "current": {}, "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "definition": "select username from users where deleted=false;", "description": "", @@ -1031,8 +1031,8 @@ "allValue": ".+", "current": {}, "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "definition": "SELECT DISTINCT provider FROM aibridge_interceptions WHERE provider IS NOT NULL ORDER BY 1;", "description": "", @@ -1050,8 +1050,8 @@ "allValue": ".+", "current": {}, "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "grafana-postgresql-datasource" + "type": "postgres", + "uid": "postgres" }, "definition": "SELECT DISTINCT model FROM aibridge_interceptions WHERE model IS NOT NULL AND provider ~ '$${provider:regex}' ORDER BY 1;", "description": "", diff --git a/infra/aws/us-east-2/k8s/monitoring/dashboards/boundary.json b/infra/aws/us-east-2/k8s/monitoring/dashboards/boundary.json new file mode 100644 index 0000000..5990323 --- /dev/null +++ b/infra/aws/us-east-2/k8s/monitoring/dashboards/boundary.json @@ -0,0 +1,875 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "This dashboard shows HTTP requests audited by agent boundaries within Coder workspaces to provide visibility into workspace network activity.\n\nWhat it shows:\n - Total count of allowed and denied outbound HTTP requests\n - Top 20 most frequently accessed allowed domains\n - Top 20 most frequently blocked domains\n - Recent allowed requests with details (time, domain, method, path, workspace owner, workspace name, template ID)\n - Recent denied requests with the same details\n\nWho it's for:\n - Platform administrators and template administrators who need to audit workspace network activity and define agent boundary policies\n - Security team members who want to know what HTTP requests AI agents made in Coder workspaces for security incident review\n - Agent Boundaries policy owners who want to refine network access controls/security posture\n\nFilters available:\n - Template ID\n - HTTP request domain\n - Workspace owner", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "Total number of requests allowed/denied", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "{decision=\"allow\"}" + }, + "properties": [ + { + "id": "displayName", + "value": "Allowed" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "{decision=\"deny\"}" + }, + "properties": [ + { + "id": "displayName", + "value": "Denied" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "sum by (decision) (count_over_time({ ${NON_WORKSPACE_SELECTOR}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=~`deny|allow` | owner=~`$owner` | domain=~`$domain` | template_id=~`$template_id` | template_version_id=~`$template_version_id` [$__range]))", + "queryType": "range", + "refId": "A" + } + ], + "title": "Request Totals", + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "Top 20 allowed domains for HTTP requests", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Domain" + }, + "properties": [ + { + "id": "custom.width", + "value": 340 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 7 + }, + "id": 1, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 1, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Count" + } + ] + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "topk(20, sum by (domain) (count_over_time({ ${NON_WORKSPACE_SELECTOR}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | template_id=~`$template_id` | template_version_id=~`$template_version_id` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", + "legendFormat": "", + "queryType": "instant", + "refId": "A" + } + ], + "title": "Top Allowed Domains", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Time": "", + "Value #A": "Count", + "domain": "Domain" + } + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "desc": true, + "field": "Count" + } + ] + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "Top 20 denied domains for HTTP requests", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Domain" + }, + "properties": [ + { + "id": "custom.width", + "value": 382 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 7 + }, + "id": 2, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 1, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Count" + } + ] + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "topk(20, sum by (domain) (count_over_time({ ${NON_WORKSPACE_SELECTOR}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | template_id=~`$template_id` | template_version_id=~`$template_version_id` | regexp `http_url=(?Phttps?)://(?P[^/:]+)` | domain=~`$domain` [$__auto])))", + "legendFormat": "", + "queryType": "instant", + "refId": "A" + } + ], + "title": "Top Denied Domains", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Time": "", + "Value #A": "Count", + "domain": "Domain" + } + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "desc": true, + "field": "Count" + } + ] + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.width", + "value": 282 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Domain" + }, + "properties": [ + { + "id": "custom.width", + "value": 185 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "method" + }, + "properties": [ + { + "id": "custom.width", + "value": 73 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "path" + }, + "properties": [ + { + "id": "custom.width", + "value": 397 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Workspace Owner" + }, + "properties": [ + { + "id": "custom.width", + "value": 144 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Template ID" + }, + "properties": [ + { + "id": "custom.width", + "value": 195 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Workspace Name" + }, + "properties": [ + { + "id": "custom.width", + "value": 204 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 19 + }, + "id": 3, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "{ ${NON_WORKSPACE_SELECTOR}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`allow` | owner=~`$owner` | template_id=~`$template_id` | template_version_id=~`$template_version_id` | regexp `http_url=https?://(?P[^/?# ]+)(?P/[^?# ]*)?` | domain=~`$domain` | line_format `time=\"{{.event_time}}\" method=\"{{.http_method}}\" domain=\"{{.domain}}\" path=\"{{.path}}\" owner=\"{{.owner}}\" workspace_name=\"{{.workspace_name}}\" template_id=\"{{.template_id}}\" template_version_id=\"{{.template_version_id}}\"`", + "queryType": "range", + "refId": "A" + } + ], + "title": "Most recent allowed requests", + "transformations": [ + { + "id": "extractFields", + "options": { + "delimiter": "|", + "format": "kvp", + "keepTime": false, + "replace": true, + "source": "Line" + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": { + "domain": true, + "method": true, + "owner": true, + "path": true, + "template_id": true, + "template_version_id": true, + "time": true, + "workspace_name": true + }, + "indexByName": { + "domain": 2, + "method": 1, + "owner": 4, + "path": 3, + "template_id": 6, + "time": 0, + "workspace_name": 5 + }, + "renameByName": { + "domain": "Domain", + "method": "Method", + "owner": "Workspace Owner", + "path": "Path", + "template_id": "Template ID", + "template_version_id": "Template Version ID", + "time": "Time", + "workspace_name": "Workspace Name" + } + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "desc": true, + "field": "Time" + } + ] + } + }, + { + "id": "limit", + "options": { + "limitField": "10" + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.width", + "value": 282 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Domain" + }, + "properties": [ + { + "id": "custom.width", + "value": 185 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "method" + }, + "properties": [ + { + "id": "custom.width", + "value": 73 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "path" + }, + "properties": [ + { + "id": "custom.width", + "value": 397 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Workspace Owner" + }, + "properties": [ + { + "id": "custom.width", + "value": 152 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 31 + }, + "id": 6, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "direction": "backward", + "editorMode": "code", + "expr": "{ ${NON_WORKSPACE_SELECTOR}, logger=`coderd.agentrpc`} |= `boundary_request` | logfmt | decision=`deny` | owner=~`$owner` | template_id=~`$template_id` | template_version_id=~`$template_version_id` | regexp `http_url=https?://(?P[^/?# ]+)(?P/[^?# ]*)?` | domain=~`$domain` | line_format `time=\"{{.event_time}}\" method=\"{{.http_method}}\" domain=\"{{.domain}}\" path=\"{{.path}}\" owner=\"{{.owner}}\" workspace_name=\"{{.workspace_name}}\" template_id=\"{{.template_id}}\" template_version_id=\"{{.template_version_id}}\"`", + "queryType": "range", + "refId": "A" + } + ], + "title": "Most recent denied requests", + "transformations": [ + { + "id": "extractFields", + "options": { + "delimiter": "|", + "format": "kvp", + "keepTime": false, + "replace": true, + "source": "Line" + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": { + "domain": true, + "method": true, + "owner": true, + "path": true, + "template_id": true, + "template_version_id": true, + "time": true, + "workspace_name": true + }, + "indexByName": { + "domain": 2, + "method": 1, + "owner": 4, + "path": 3, + "template_id": 6, + "time": 0, + "workspace_name": 5 + }, + "renameByName": { + "domain": "Domain", + "method": "Method", + "owner": "Workspace Owner", + "path": "Path", + "template_id": "Template ID", + "template_version_id": "Template Version ID", + "time": "Time", + "workspace_name": "Workspace Name" + } + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "desc": true, + "field": "Time" + } + ] + } + }, + { + "id": "limit", + "options": { + "limitField": "10" + } + } + ], + "type": "table" + } + ], + "preload": false, + "refresh": "${DASHBOARD_REFRESH}", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": "", + "value": "" + }, + "description": "Filter by request domain", + "label": "Domain", + "name": "domain", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "type": "textbox" + }, + { + "current": { + "text": "", + "value": "" + }, + "description": "Filter requests by workspace owner", + "label": "Workspace Owner", + "name": "owner", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "type": "textbox" + }, + { + "current": { + "text": "", + "value": "" + }, + "description": "Filter requests by template ID (UUID). Template IDs can be found via the CLI with \"coder templates list\".", + "label": "Template ID", + "name": "template_id", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "type": "textbox" + }, + { + "current": { + "text": "", + "value": "" + }, + "description": "Filter by template version ID (UUID). A templates version IDs can be found via the CLI with \"coder templates versions list