Create a Cloud Development Environment with Coder | Part 1/3

Hey! I’m coding in my backyard on my iPad today, and it’s all thanks to a Cloud Dev Environment provided by Coder. In this post, we’ll be looking at a product I’ve been testing for a week: Coder. With former colleagues from Docker and HashiCorp working at Coder, I’m happy to recommend this.

Me coding in my backyard

Video

Video Chapters

  • 00:00 Introduction to Cloud Development Environments
  • 00:27 Agenda
  • 01:08
  • Why Cloud Development Environments?
  • 03:28 Why Choose Coder?
  • 04:30 Coder’s Architecture
  • 05:47 Palantir’s Onboarding Success Story
  • 08:36 Setting Up Coder: Initial Steps
  • 08:57 Configuring the Database and Secrets
  • 09:42 Deploying and Accessing Coder
  • 10:51 Creating and Managing Workspaces
  • 12:55 Exploring VS Code Integration
  • 14:55 Advanced Features and Performance
  • 17:54 Onboarding New Developers
  • 20:29 Template Walk-Through

Code

Join the Newsletter and get FREE access to the Source Code

Agenda

This is the first of a three-part series on Coder. We’ll be looking at how to get developers up and running and productive as fast as possible. Here’s what we’ll cover:

  1. What are Cloud Development Environments?

  2. Why Coder?

  3. Coder’s Architecture

  4. Palantir’s Onboarding Success Story

  5. Demo: Coder in Action

What are Cloud Development Environments (CDEs)?

Cloud dev environments are virtual workspaces hosted in the cloud, designed to streamline and enhance the software development process. Unlike traditional local development environments, where developers rely on physical machines, cloud environments leverage the power of cloud infrastructure to provide scalable, flexible, and accessible development platforms.

This shift to cloud-based development environments addresses several pain points of local setups, such as configuration drift, security risks, and limited resource availability. By hosting the development environment in the cloud, organizations can offer developers consistent, pre-configured workspaces that can be accessed from anywhere, ensuring a uniform development experience and improved collaboration across distributed teams. A cloud environment also integrates seamlessly with CI/CD pipelines, allowing for faster iterations and more efficient deployments, ultimately leading to enhanced productivity and reduced operational overhead.

Current Developer Work Environment

How developers code today

New developers typically receive a laptop when they join an organization. This traditional local development environment setup has several issues:

  • Code Escape Risk: Code on developer laptops poses an Intellectual Property (IP) risk.

  • Configuration Complexity: IT teams have to configure each laptop, a time-consuming task.

  • Poor Observability: Limited visibility into what’s happening on individual laptops.

  • Virtual Desktop Infrastructure (VDI): Often plagued by latency, poor developer experience, and limited flexibility.

  • Shadow Virtual Machines (VMs): Running a virtual machine on platforms like AWS involves governance issues, high costs, and inefficiencies.

Outcome of Local Development Environments

Cloud Solutions

Since CI/CD pipelines and infrastructure deployments are already in the cloud, moving development workspaces to the cloud makes sense and provides a better development workflow. The setup for local environments leads to:

  • Frustrating Developer Experience: Slow and inefficient processes.

  • Operational Inefficiency: High costs and wasted resources.

  • Governance Risks: Lack of control over code and activities.

Coder recommends moving the IDE or workspace to the cloud and providing developers with lightweight laptops. By leveraging cloud instances, resources can be scaled up or down based on demand, ensuring optimal performance and cost-efficiency. Developers can then access these cloud workspaces via a browser or desktop IDE on their laptops.

How developers code with Coder

Why Coder?

Benefits

  1. Open Source Distribution: Core features are free. I’m using it for my projects at TeKanAid Academy. Coder streamlines developer environments, making it easier for teams to collaborate and be productive.

  2. Self-Hosted Platform: You can host Coder in your cloud tenant or an air-gapped on-premise environment, giving you control over your development environment.

  3. Performance and Scalability: Networking is enabled by Terraform-built templates, which we’ll see in the demo.

  4. Cloud and IDE Agnostic: Works with multiple tools and is highly customizable for developers. You can use any cloud provider for your workspaces.

Coder’s Architecture

Components

  1. Control Plane: Runs the Coder daemon, exposes REST APIs, and manages dashboards. It interacts with the data layer (Postgres database) and external resources like container registries and machine images.

  2. Workspaces: Can be hosted anywhere (Kubernetes, Docker, VMs) and run an agent. They require a persistent disk to save data across sessions.

Developers access these workspaces via a web IDE or desktop IDE, both communicating with the control plane.

Coder's Architecture

Coder’s Architecture Breakdown

  1. Control Plane: The control plane is the central component of Coder’s architecture. It runs the Coder daemon and exposes the REST API and dashboard for users to interact with. It also provisions workspaces using Terraform, which provides flexibility and scalability to the system. The control plane interacts with the data layer, which includes a Postgres database for storing metadata and configuration settings.

  2. Data Layer: This layer is crucial for core data management. It involves the Postgres database for storing configurations, user data, and other important information.

  3. Workspace Infrastructure: Workspaces can be hosted on Kubernetes clusters, Docker containers, or virtual machines. Each workspace runs an agent that manages the environment and ensures it operates smoothly. Persistent disks maintain data integrity across sessions, allowing developers to stop and restart workspaces without losing their data.

  4. Access Mechanisms: Developers can access workspaces via web IDEs or desktop IDEs. The web IDE connects directly to the control plane, while the desktop IDE communicates directly with the workspace. This dual access mechanism ensures developers can work with their preferred tools and environments, enhancing their productivity and comfort.

Palantir’s Onboarding Success Story

Palantir saw huge improvements in their development process with Coder:

  • 78% Faster Front-End Build Times

  • 71% Faster Git Clone Times

  • Cost Efficiency: Reduced hardware costs by switching from $2,900 32GB M1 MacBooks to $2,000 standard M1 MacBooks. Factoring in AWS EC2 instance costs, the total cost increased by 41% but was offset by higher productivity and better operations. Coder provides a centralized platform for software development teams to collaborate and be productive.

Cost Analysis and Productivity Gains

Palantir initially provided their developers with high-end 32GB M1 MacBooks, each costing $2,900. Although this was a significant investment, it was justified by the need for high-performance development machines. However, with Coder, Palantir switched to standard M1 MacBooks, which cost $2,000 each. This switch alone reduced the annual cost per developer from approximately $1,450 to $1,000.

To offset the need for high-performance local machines, Palantir leveraged AWS EC2 instances for their development workspaces. Specifically, they used R5.4xlarge instances, which are optimized for memory-intensive applications and cost about $1 per hour. By optimizing their instance usage (e.g., sharing instances among three developers, limiting instance uptime to five days a week, and 12 hours a day), Palantir managed to keep the additional cloud costs to about $8,130 per year.

Despite a 41% increase in overall costs, the productivity gains were substantial. With faster build and clone times, developers spent less time waiting and more time coding, significantly enhancing overall productivity and efficiency.

You can find out more from their blog post.

Demo: Coder in Action

You can follow the steps below from the demo video above or in the GitHub repo.

Initial Setup

  1. Kubernetes Cluster: Create a Kubernetes cluster and a namespace called ‘coder’.

  2. Database Configuration: Use Helm to create a Postgres database.

  3. Secret Creation: Generate a secret for database access.

Deploying Coder

  1. Install Coder: Using Helm and merging values and secrets files.

  2. Access the UI: Access the Coder UI through an ingress. Set up an admin account.

  3. Create Workspaces: Utilize starter templates (e.g., Kubernetes template) to create workspaces with specified resources (CPU, memory).

Exploring VS Code Integration

  • Terminal Access: Access a Linux terminal within the workspace.

  • Code Server: Use VS Code in a browser with customizable themes.

  • VS Code Desktop: Open workspaces in native VS Code on a local machine.

Advanced Features and Performance

  • Stopping and Restarting Workspaces: Demonstrated persistent data across sessions.

  • User Management: Onboard new developers using GitHub authentication. Limit workspace usage to optimize costs.

  • Performance Metrics: Low latency and high performance ensure a smooth developer experience.

Detailed Demo Walkthrough

  1. Setting Up the Environment: We start by setting up the environment, which involves creating a Kubernetes cluster and a namespace called ‘coder’. The next step is configuring the database using Helm to create a Postgres database, followed by generating a secret for database access.

  2. Installing Coder: We proceed by installing Coder using Helm, merging values.yaml and secrets.yaml files. Once the installation is complete, we access the Coder UI through an ingress and set up an admin account. The UI provides a user-friendly interface for managing workspaces and templates.

  3. Creating Workspaces: Using starter templates, we create workspaces with specified resources like CPU and memory. For example, we use the Kubernetes template to create a workspace with four CPU cores and 10GB of memory. Terraform handles the workspace provisioning, ensuring a seamless setup process.

  4. Accessing Workspaces: Developers can access their workspaces via terminal access, Code Server (VS Code in a browser), or VS Code Desktop. These access methods provide flexibility and cater to different developer preferences.

  5. Advanced Features: The ability to stop and restart workspaces ensures data persistence across sessions. User management is streamlined through GitHub authentication, making onboarding new developers and managing access easy. Performance metrics, including low latency and high responsiveness, contribute to a positive developer experience.

Onboarding New Developers

  1. GitHub Integration: By integrating GitHub authentication, developers can log in using their GitHub credentials. This simplifies the login process and enhances security by leveraging GitHub’s robust authentication mechanisms.

  2. Creating Workspaces: Once logged in, developers can create workspaces based on the templates provided by the admin. These templates can be customized to meet the specific needs of the development team, ensuring that each developer has the tools and resources they need to be productive.

  3. Managing Resource Allocation: Admins can limit workspace usage by limiting the number of hours a workspace can be active per day. This helps manage costs by ensuring that resources are not left running unnecessarily. Additionally, admins can monitor workspace usage and performance metrics to optimize resource allocation further.

Template Walk-Through

Terraform Code for Templates

Coder uses Terraform for building templates, offering flexibility and familiarity to DevOps professionals:

  • Providers: Define Coder and Kubernetes providers.

  • Parameters: Expose CPU, memory, and disk size options to users.

  • Persistent Volume Claims: Ensure data persistence across workspace sessions.

  • Kubernetes Deployment: Configure deployments with specific container images and resources.

Detailed Template Breakdown

  1. Defining Providers: The template starts by defining the necessary providers, including Coder and Kubernetes. These providers facilitate the interaction with the Coder platform and the Kubernetes cluster.

  2. Setting Parameters: Parameters such as CPU, memory, and disk size are exposed to users, allowing them to customize their workspaces according to their needs. This flexibility ensures that developers can configure their environments to match their specific requirements.

  3. Persistent Volume Claims: The template includes definitions for persistent volume claims, which ensure that data is retained across workspace sessions. This persistence is crucial for maintaining continuity and preventing data loss.

  4. Kubernetes Deployment: The template configures the Kubernetes deployment, specifying the container image and resources needed for the workspace. This includes setting limits and requests for CPU and memory, as well as defining volume mounts for data persistence.

main.tf Template File

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."
}
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 and start code-server
    curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.11.0
    /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   = <<EOT
      echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
    EOT
    interval = 60
    timeout  = 1
  }
}
# code-server
resource "coder_app" "code-server" {
  agent_id     = coder_agent.main.id
  slug         = "code-server"
  display_name = "code-server"
  icon         = "/icon/code.svg"
  url          = "http://localhost:13337?folder=/home/coder"
  subdomain    = false
  share        = "owner"
  healthcheck {
    url       = "http://localhost:13337/healthz"
    interval  = 3
    threshold = 10
  }
}
resource "kubernetes_persistent_volume_claim" "home" {
  metadata {
    name      = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-home"
    namespace = var.namespace
    labels = {
      "app.kubernetes.io/name"     = "coder-pvc"
      "app.kubernetes.io/instance" = "coder-pvc-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
      "app.kubernetes.io/part-of"  = "coder"
      //Coder-specific labels.
      "com.coder.resource"       = "true"
      "com.coder.workspace.id"   = data.coder_workspace.me.id
      "com.coder.workspace.name" = data.coder_workspace.me.name
      "com.coder.user.id"        = data.coder_workspace_owner.me.id
      "com.coder.user.username"  = data.coder_workspace_owner.me.name
    }
    annotations = {
      "com.coder.user.email" = data.coder_workspace_owner.me.email
    }
  }
  wait_until_bound = false
  spec {
    access_modes = ["ReadWriteOnce"]
    resources {
      requests = {
        storage = "${data.coder_parameter.home_disk_size.value}Gi"
      }
    }
  }
}
resource "kubernetes_deployment" "main" {
  count = data.coder_workspace.me.start_count
  depends_on = [
    kubernetes_persistent_volume_claim.home
  ]
  wait_for_rollout = false
  metadata {
    name      = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
    namespace = var.namespace
    labels = {
      "app.kubernetes.io/name"     = "coder-workspace"
      "app.kubernetes.io/instance" = "coder-workspace-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
      "app.kubernetes.io/part-of"  = "coder"
      "com.coder.resource"         = "true"
      "com.coder.workspace.id"     = data.coder_workspace.me.id
      "com.coder.workspace.name"   = data.coder_workspace.me.name
      "com.coder.user.id"          = data.coder_workspace_owner.me.id
      "com.coder.user.username"    = data.coder_workspace_owner.me.name
    }
    annotations = {
      "com.coder.user.email" = data.coder_workspace_owner.me.email
    }
  }
  spec {
    replicas = 1
    selector {
      match_labels = {
        "app.kubernetes.io/name" = "coder-workspace"
      }
    }
    strategy {
      type = "Recreate"
    }
    template {
      metadata {
        labels = {
          "app.kubernetes.io/name" = "coder-workspace"
        }
      }
      spec {
        security_context {
          run_as_user = 1000
          fs_group    = 1000
        }
        container {
          name              = "dev"
          image             = "codercom/enterprise-base:ubuntu"
          image_pull_policy = "Always"
          command           = ["sh", "-c", coder_agent.main.init_script]
          security_context {
            run_as_user = "1000"
          }
          env {
            name  = "CODER_AGENT_TOKEN"
            value = coder_agent.main.token
          }
          resources {
            requests = {
              "cpu"    = "250m"
              "memory" = "512Mi"
            }
            limits = {
              "cpu"    = "${data.coder_parameter.cpu.value}"
              "memory" = "${data.coder_parameter.memory.value}Gi"
            }
          }
          volume_mount {
            mount_path = "/home/coder"
            name       = "home"
            read_only  = false
          }
        }
        volume {
          name = "home"
          persistent_volume_claim {
            claim_name = kubernetes_persistent_volume_claim.home.metadata.0.name
            read_only  = false
          }
        }
        affinity {
          // This affinity attempts to spread out all workspace pods evenly across
          // nodes.
          pod_anti_affinity {
            preferred_during_scheduling_ignored_during_execution {
              weight = 1
              pod_affinity_term {
                topology_key = "kubernetes.io/hostname"
                label_selector {
                  match_expressions {
                    key      = "app.kubernetes.io/name"
                    operator = "In"
                    values   = ["coder-workspace"]
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Conclusion

In this post, we’ve explored how setting up and using Coder can enhance developer onboarding and productivity by leveraging cloud development environments. We discussed the benefits of Coder over traditional development methods and demonstrated a step-by-step guide to setting up a Coder instance on Kubernetes, creating and managing workspaces, and integrating with VS Code. Stay tuned for part 2 of this series, where we’ll explore creating more powerful and customized templates and optimizing resource utilization with Coder. Thanks for reading!

Suggested Reading

Scroll to Top