diff --git a/.icons/oci.svg b/.icons/oci.svg new file mode 100644 index 00000000..3244c974 --- /dev/null +++ b/.icons/oci.svg @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/registry/coder/templates/oci-linux/README.md b/registry/coder/templates/oci-linux/README.md new file mode 100644 index 00000000..4ee7723f --- /dev/null +++ b/registry/coder/templates/oci-linux/README.md @@ -0,0 +1,155 @@ +--- +display_name: Oracle Cloud Infrastructure (Linux) +description: Provision Oracle Cloud Infrastructure VMs as Coder workspaces +icon: ../../../../.icons/oci.svg +maintainer_github: coder +verified: false +tags: [vm, linux, oci, oracle] +--- + +# Remote Development on Oracle Cloud Infrastructure (Linux) + +Provision Oracle Cloud Infrastructure (OCI) VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template. + +## Prerequisites + +### Authentication + +This template assumes that coderd is run in an environment that is authenticated with Oracle Cloud Infrastructure. The recommended authentication methods are: + +1. **Instance Principal** (Recommended for production): Run Coder on an OCI instance with proper IAM policies +2. **API Key**: Set environment variables `OCI_TENANCY_OCID`, `OCI_USER_OCID`, `OCI_FINGERPRINT`, and `OCI_PRIVATE_KEY_PATH` +3. **Configuration File**: Use `~/.oci/config` file + +For detailed authentication setup, see the [OCI Terraform provider documentation](https://registry.terraform.io/providers/oracle/oci/latest/docs#authentication). + +### Required IAM Policies + +The following IAM policies are required for the template to work: + +```json +{ + "statements": [ + { + "effect": "Allow", + "action": [ + "core:instance:create", + "core:instance:delete", + "core:instance:get", + "core:instance:update", + "core:volume:create", + "core:volume:delete", + "core:volume:get", + "core:volume:update", + "core:volumeAttachment:create", + "core:volumeAttachment:delete", + "core:volumeAttachment:get", + "core:vcn:create", + "core:vcn:delete", + "core:vcn:get", + "core:vcn:update", + "core:subnet:create", + "core:subnet:delete", + "core:subnet:get", + "core:subnet:update", + "core:internetGateway:create", + "core:internetGateway:delete", + "core:internetGateway:get", + "core:internetGateway:update", + "core:routeTable:create", + "core:routeTable:delete", + "core:routeTable:get", + "core:routeTable:update", + "core:securityList:create", + "core:securityList:delete", + "core:securityList:get", + "core:securityList:update", + "core:image:get", + "identity:compartment:get" + ], + "resource": "*" + } + ] +} +``` + +## Architecture + +This template provisions the following resources: + +- **OCI VM** (ephemeral, deleted on stop) +- **OCI Block Volume** (persistent, mounted to `/home/coder`) +- **VCN with Internet Gateway** (for network connectivity) +- **Security List** (with SSH, HTTP, and HTTPS access) + +The template uses Ubuntu 22.04 LTS as the base image and includes: +- Code Server for web-based development +- JetBrains Gateway for IDE access +- Persistent home directory storage +- Automatic Coder agent installation + +## Usage + +1. **Set up authentication** using one of the methods above +2. **Create a compartment** in your OCI tenancy +3. **Deploy the template** with your compartment OCID +4. **Optionally provide an SSH public key** for direct SSH access + +### Template Variables + +- `compartment_ocid`: The OCID of your OCI compartment +- `ssh_public_key`: (Optional) SSH public key for direct access + +### Instance Shapes + +The template supports various OCI instance shapes: +- **VM.Standard.A1.Flex**: ARM-based flexible shapes (1-4 OCPUs, 6-24 GB RAM) +- **VM.Standard.E2.1.Micro**: Cost-effective micro instances +- **VM.Standard.E2.1.Small**: Small instances for development +- **VM.Standard.E2.1.Medium**: Medium instances for larger workloads +- **VM.Standard.E3.Flex**: AMD-based flexible shapes + +### Regions + +The template supports all major OCI regions: +- **Americas**: US East (Ashburn), US West (Phoenix), Canada Southeast (Montreal) +- **Europe**: UK South (London), Germany Central (Frankfurt), Netherlands Northwest (Amsterdam), Switzerland North (Zurich) +- **Asia Pacific**: Japan East (Tokyo), Japan Central (Osaka), South Korea Central (Seoul), Australia Southeast (Sydney), India West (Mumbai), India South (Hyderabad) +- **Middle East**: Saudi Arabia West (Jeddah), UAE East (Dubai) +- **South America**: Brazil East (São Paulo), Chile (Santiago) + +## Cost Optimization + +- Use **VM.Standard.A1.Flex** shapes for cost-effective ARM-based instances +- Choose **VM.Standard.E2.1.Micro** for minimal development workloads +- Consider **VM.Standard.E3.Flex** for AMD-based workloads requiring more memory +- Use smaller home disk sizes (50 GB) for basic development +- Stop workspaces when not in use to avoid charges + +## Security + +- Instances are created with public IP addresses for Coder access +- SSH access is restricted to the provided public key +- Security lists allow only necessary ports (22, 80, 443) +- All resources are tagged with `Coder_Provisioned = true` + +## Troubleshooting + +### Common Issues + +1. **Authentication Errors**: Ensure proper OCI authentication is configured +2. **Permission Errors**: Verify IAM policies are correctly set +3. **Network Issues**: Check VCN and security list configuration +4. **Volume Attachment**: Ensure the home volume is properly attached + +### Debugging + +- Check OCI console for instance status and logs +- Verify network connectivity and security list rules +- Review Terraform logs for detailed error messages + +## Contributing + +This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +For issues and contributions, please visit the [Coder Registry repository](https://github.com/coder/registry). \ No newline at end of file diff --git a/registry/coder/templates/oci-linux/cloud-init/cloud-config.yaml.tftpl b/registry/coder/templates/oci-linux/cloud-init/cloud-config.yaml.tftpl new file mode 100644 index 00000000..5951050b --- /dev/null +++ b/registry/coder/templates/oci-linux/cloud-init/cloud-config.yaml.tftpl @@ -0,0 +1,39 @@ +#cloud-config +hostname: ${hostname} +users: + - name: ${linux_user} + uid: 1000 + gid: 1000 + groups: sudo + packages: + - curl + shell: /bin/bash + sudo: ['ALL=(ALL) NOPASSWD:ALL'] + ssh_authorized_keys: + - ${ssh_public_key} + +# Update package list and install basic packages +package_update: true +package_upgrade: true +packages: + - curl + - wget + - git + - unzip + - software-properties-common + - apt-transport-https + - ca-certificates + - gnupg + - lsb-release + +# Write the Coder agent token to a file +write_files: + - path: /opt/coder/init.env + content: | + CODER_AGENT_TOKEN=${coder_agent_token} + owner: ${linux_user}:${linux_user} + permissions: '0600' + +# Run commands after package installation +runcmd: + - systemctl enable --now coder-agent \ No newline at end of file diff --git a/registry/coder/templates/oci-linux/cloud-init/userdata.sh.tftpl b/registry/coder/templates/oci-linux/cloud-init/userdata.sh.tftpl new file mode 100644 index 00000000..832ffcdc --- /dev/null +++ b/registry/coder/templates/oci-linux/cloud-init/userdata.sh.tftpl @@ -0,0 +1,48 @@ +#!/bin/bash +set -e + +# Set hostname +hostnamectl set-hostname ${hostname} + +# Create coder user if it doesn't exist +if ! id "${linux_user}" &>/dev/null; then + useradd -m -s /bin/bash -G sudo ${linux_user} + echo "${linux_user} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers +fi + +# Create necessary directories +mkdir -p /opt/coder +mkdir -p /home/${linux_user} + +# Set up SSH key if provided +if [ -n "${ssh_public_key}" ]; then + mkdir -p /home/${linux_user}/.ssh + echo "${ssh_public_key}" >> /home/${linux_user}/.ssh/authorized_keys + chown -R ${linux_user}:${linux_user} /home/${linux_user}/.ssh + chmod 700 /home/${linux_user}/.ssh + chmod 600 /home/${linux_user}/.ssh/authorized_keys +fi + +# Mount home volume if it exists +if [ -b /dev/sdb ]; then + # Check if the disk is already formatted + if ! blkid /dev/sdb; then + mkfs.ext4 /dev/sdb + fi + + # Create mount point and mount + mkdir -p /home/${linux_user} + mount /dev/sdb /home/${linux_user} + + # Add to fstab for persistence + echo "/dev/sdb /home/${linux_user} ext4 defaults 0 2" >> /etc/fstab + + # Set ownership + chown -R ${linux_user}:${linux_user} /home/${linux_user} +fi + +# Download and install Coder agent +curl -fsSL https://coder.com/install.sh | sh + +# Start the Coder agent +systemctl enable --now coder-agent \ No newline at end of file diff --git a/registry/coder/templates/oci-linux/main.tf b/registry/coder/templates/oci-linux/main.tf new file mode 100644 index 00000000..3617469b --- /dev/null +++ b/registry/coder/templates/oci-linux/main.tf @@ -0,0 +1,491 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + oci = { + source = "oracle/oci" + } + cloudinit = { + source = "hashicorp/cloudinit" + } + } +} + +# Variables +variable "compartment_ocid" { + description = "The OCID of the compartment to create resources in" + type = string +} + +variable "ssh_public_key" { + description = "SSH public key for the instance" + type = string + default = "" +} + +# OCI Region parameter +data "coder_parameter" "region" { + name = "region" + display_name = "Region" + description = "The region to deploy the workspace in." + default = "us-ashburn-1" + mutable = false + option { + name = "US East (Ashburn)" + value = "us-ashburn-1" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "US West (Phoenix)" + value = "us-phoenix-1" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "Canada Southeast (Montreal)" + value = "ca-montreal-1" + icon = "/emojis/1f1e8-1f1e6.png" + } + option { + name = "UK South (London)" + value = "uk-london-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "Germany Central (Frankfurt)" + value = "eu-frankfurt-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "Netherlands Northwest (Amsterdam)" + value = "eu-amsterdam-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "Switzerland North (Zurich)" + value = "eu-zurich-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "Japan East (Tokyo)" + value = "ap-tokyo-1" + icon = "/emojis/1f1ef-1f1f5.png" + } + option { + name = "Japan Central (Osaka)" + value = "ap-osaka-1" + icon = "/emojis/1f1ef-1f1f5.png" + } + option { + name = "South Korea Central (Seoul)" + value = "ap-seoul-1" + icon = "/emojis/1f1f0-1f1f7.png" + } + option { + name = "Australia Southeast (Sydney)" + value = "ap-sydney-1" + icon = "/emojis/1f1e6-1f1fa.png" + } + option { + name = "India West (Mumbai)" + value = "ap-mumbai-1" + icon = "/emojis/1f1ee-1f1f3.png" + } + option { + name = "India South (Hyderabad)" + value = "ap-hyderabad-1" + icon = "/emojis/1f1ee-1f1f3.png" + } + option { + name = "Saudi Arabia West (Jeddah)" + value = "me-jeddah-1" + icon = "/emojis/1f1f8-1f1e6.png" + } + option { + name = "UAE East (Dubai)" + value = "me-dubai-1" + icon = "/emojis/1f1e6-1f1ea.png" + } + option { + name = "Brazil East (São Paulo)" + value = "sa-saopaulo-1" + icon = "/emojis/1f1e7-1f1f7.png" + } + option { + name = "Chile (Santiago)" + value = "sa-santiago-1" + icon = "/emojis/1f1e8-1f1f1.png" + } +} + +# Instance shape parameter +data "coder_parameter" "instance_shape" { + name = "instance_shape" + display_name = "Instance Shape" + description = "What instance shape should your workspace use?" + default = "VM.Standard.A1.Flex" + mutable = false + option { + name = "VM.Standard.A1.Flex (1 OCPU, 6 GB RAM)" + value = "VM.Standard.A1.Flex" + } + option { + name = "VM.Standard.A1.Flex (2 OCPU, 12 GB RAM)" + value = "VM.Standard.A1.Flex" + } + option { + name = "VM.Standard.A1.Flex (4 OCPU, 24 GB RAM)" + value = "VM.Standard.A1.Flex" + } + option { + name = "VM.Standard.E2.1.Micro (1 OCPU, 1 GB RAM)" + value = "VM.Standard.E2.1.Micro" + } + option { + name = "VM.Standard.E2.1.Small (1 OCPU, 2 GB RAM)" + value = "VM.Standard.E2.1.Small" + } + option { + name = "VM.Standard.E2.1.Medium (1 OCPU, 4 GB RAM)" + value = "VM.Standard.E2.1.Medium" + } + option { + name = "VM.Standard.E2.2.Medium (2 OCPU, 8 GB RAM)" + value = "VM.Standard.E2.2.Medium" + } + option { + name = "VM.Standard.E2.4.Medium (4 OCPU, 16 GB RAM)" + value = "VM.Standard.E2.4.Medium" + } + option { + name = "VM.Standard.E3.Flex (1 OCPU, 8 GB RAM)" + value = "VM.Standard.E3.Flex" + } + option { + name = "VM.Standard.E3.Flex (2 OCPU, 16 GB RAM)" + value = "VM.Standard.E3.Flex" + } + option { + name = "VM.Standard.E3.Flex (4 OCPU, 32 GB RAM)" + value = "VM.Standard.E3.Flex" + } +} + +# Home disk size parameter +data "coder_parameter" "home_size" { + name = "home_size" + display_name = "Home Disk Size" + description = "How large should the home disk be?" + default = "50" + mutable = false + option { + name = "50 GB" + value = "50" + } + option { + name = "100 GB" + value = "100" + } + option { + name = "200 GB" + value = "200" + } + option { + name = "500 GB" + value = "500" + } + option { + name = "1 TB" + value = "1024" + } +} + +# OCI Provider configuration +provider "oci" { + region = data.coder_parameter.region.value +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +# Get the compartment OCID from environment variable +data "oci_identity_compartments" "compartments" { + compartment_id = var.compartment_ocid + access_level = "ACCESSIBLE" + state = "ACTIVE" +} + +# Get the latest Ubuntu image +data "oci_core_images" "ubuntu" { + compartment_id = var.compartment_ocid + operating_system = "Canonical Ubuntu" + operating_system_version = "22.04" + state = "AVAILABLE" + sort_by = "TIMECREATED" + sort_order = "DESC" +} + +locals { + hostname = lower(data.coder_workspace.me.name) + linux_user = "coder" +} + +# Coder Agent +resource "coder_agent" "dev" { + count = data.coder_workspace.me.start_count + arch = "amd64" + auth = "token" + 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/jetbrains-gateway +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/jetbrains-gateway/coder" + + # JetBrains IDEs to make available for the user to select + jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"] + default = "IU" + + # Default folder to open when starting a JetBrains IDE + folder = "/home/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 + agent_name = "dev" + order = 2 +} + +# Cloud-init configuration +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_public_key = var.ssh_public_key + coder_agent_token = coder_agent.dev[0].token + }) + } + + 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 + ssh_public_key = var.ssh_public_key + coder_agent_token = coder_agent.dev[0].token + }) + } +} + +# VCN +resource "oci_core_vcn" "vcn" { + compartment_id = var.compartment_ocid + cidr_blocks = ["10.0.0.0/16"] + display_name = "coder-vcn-${data.coder_workspace.me.id}" + dns_label = "coder${data.coder_workspace.me.id}" +} + +# Internet Gateway +resource "oci_core_internet_gateway" "internet_gateway" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.vcn.id + display_name = "coder-internet-gateway-${data.coder_workspace.me.id}" +} + +# Route Table +resource "oci_core_route_table" "route_table" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.vcn.id + display_name = "coder-route-table-${data.coder_workspace.me.id}" + + route_rules { + destination = "0.0.0.0/0" + destination_type = "CIDR_BLOCK" + network_entity_id = oci_core_internet_gateway.internet_gateway.id + } +} + +# Security List +resource "oci_core_security_list" "security_list" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.vcn.id + display_name = "coder-security-list-${data.coder_workspace.me.id}" + + egress_security_rules { + destination = "0.0.0.0/0" + protocol = "all" + } + + ingress_security_rules { + protocol = "6" + source = "0.0.0.0/0" + + tcp_options { + min = 22 + max = 22 + } + } + + ingress_security_rules { + protocol = "6" + source = "0.0.0.0/0" + + tcp_options { + min = 80 + max = 80 + } + } + + ingress_security_rules { + protocol = "6" + source = "0.0.0.0/0" + + tcp_options { + min = 443 + max = 443 + } + } +} + +# Subnet +resource "oci_core_subnet" "subnet" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.vcn.id + cidr_block = "10.0.1.0/24" + display_name = "coder-subnet-${data.coder_workspace.me.id}" + dns_label = "coder${data.coder_workspace.me.id}" + + security_list_ids = [oci_core_security_list.security_list.id] + route_table_id = oci_core_route_table.route_table.id +} + +# Home disk +resource "oci_core_volume" "home_volume" { + compartment_id = var.compartment_ocid + display_name = "coder-${data.coder_workspace.me.id}-home" + size_in_gbs = data.coder_parameter.home_size.value + availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name +} + +# Get availability domains +data "oci_identity_availability_domains" "ads" { + compartment_id = var.compartment_ocid +} + +# OCI Instance +resource "oci_core_instance" "dev" { + count = data.coder_workspace.me.start_count + availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name + compartment_id = var.compartment_ocid + display_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}" + shape = data.coder_parameter.instance_shape.value + + shape_config { + ocpus = 1 + memory_in_gbs = 6 + } + + create_vnic_details { + subnet_id = oci_core_subnet.subnet.id + assign_public_ip = true + } + + source_details { + source_type = "image" + source_id = data.oci_core_images.ubuntu.images[0].id + } + + metadata = { + ssh_authorized_keys = var.ssh_public_key + user_data = base64encode(data.cloudinit_config.user_data.rendered) + } + + freeform_tags = { + "Coder_Provisioned" = "true" + } +} + +# Attach home volume +resource "oci_core_volume_attachment" "home_attachment" { + count = data.coder_workspace.me.start_count + attachment_type = "paravirtualized" + compartment_id = var.compartment_ocid + instance_id = oci_core_instance.dev[0].id + volume_id = oci_core_volume.home_volume.id +} + +# Workspace metadata +resource "coder_metadata" "workspace_info" { + count = data.coder_workspace.me.start_count + resource_id = oci_core_instance.dev[0].id + + item { + key = "type" + value = oci_core_instance.dev[0].shape + } + item { + key = "region" + value = data.coder_parameter.region.value + } +} + +resource "coder_metadata" "home_info" { + resource_id = oci_core_volume.home_volume.id + + item { + key = "size" + value = "${data.coder_parameter.home_size.value} GiB" + } +} \ No newline at end of file