diff --git a/.icons/oracle.svg b/.icons/oracle.svg new file mode 100644 index 000000000..0981dfcff --- /dev/null +++ b/.icons/oracle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/registry/anis/templates/oci-vm/README.md b/registry/anis/templates/oci-vm/README.md new file mode 100644 index 000000000..90ddf97b8 --- /dev/null +++ b/registry/anis/templates/oci-vm/README.md @@ -0,0 +1,78 @@ +--- +display_name: Oracle Cloud VM (Linux) +description: Provision Oracle Cloud Infrastructure (OCI) instances as Coder workspaces +icon: ../../../../.icons/oracle.svg +verified: false +tags: [vm, linux, oracle, oci] +--- + +# Remote Development on Oracle Cloud Infrastructure (OCI) + +Provision OCI virtual machines as [Coder workspaces](https://coder.com/docs/workspaces) using this Terraform template. + +## Prerequisites + +To deploy Coder workspaces on Oracle Cloud, you’ll need the following: + +### OCI Resources + +Before deploying, ensure your Oracle Cloud tenancy has: + +- A **VCN (Virtual Cloud Network)** already created +- At least one **subnet** within that VCN (can be public or private) +- An **Internet Gateway** attached to the VCN +- A **Route Table** that routes `0.0.0.0/0` traffic to the Internet Gateway + +> [!NOTE] +> This template **does not create networking resources** (VCN, subnet, gateway, etc.). +> You must reference an existing subnet using its **OCID** via the `subnet_id` variable. + +> The available regions and instance shapes listed in this template are examples only, not all shapes are available in every region, and availability depends on your OCI tenancy and subscription tier. Check the [OCI documentation](https://docs.oracle.com/en-us/iaas/Content/Compute/References/computeshapes.htm) to confirm which shapes are available in your target region before deploying. + +> [!IMPORTANT] +> Your tenancy is only auto-subscribed to its **home region** — everything else requires an explicit region subscription (**Governance & Administration > Region Management** in the OCI Console). `us-ashburn-1` (the default here) is one of OCI's two original commercial regions and a common home region, but it isn't universal. If your tenancy's home region is different (for example `us-phoenix-1`), change the `region` parameter's default to match — otherwise both `coder templates push` and workspace builds fail with what looks like a credentials/authentication error, even though the credentials themselves are fine. + +### OCI Authentication + +You’ll also need the following credentials: + +- **Tenancy OCID** +- **User OCID** +- **Fingerprint** +- **Private Key** +- **Compartment OCID**(Optional) default to Tenancy OCID if not defined +- **Subnet OCID** + +[OCI Documentation](https://docs.oracle.com/en-us/iaas/Content/dev/terraform/configuring.htm#api-key-auth) + +--- + +## Workspace Lifecycle + +| Event | OCI Resources | +| --------------------- | --------------------------------------------------------------------------------------- | +| **Workspace started** | A new compute instance (`oci_core_instance`) is created and the home volume is attached | +| **Workspace stopped** | The compute instance is destroyed, but the home volume (`oci_core_volume`) persists | +| **Workspace deleted** | All resources are destroyed, including the home volume | + +> Only `/home/` (mounted from the persistent block volume) survives a stop. Everything else, including the root disk and anything installed outside `/home`, is recreated from the base Ubuntu image on every start. + +### Debugging + +If you set the `ssh_public_key` template variable, its matching private key can be used to `ssh @` directly, independent of the Coder agent — useful if the agent itself fails to come up. + +--- + +## Example `.tfvars` File + +```hcl +tenancy_ocid = "ocid1.tenancy.oc1..xxxx" +user_ocid = "ocid1.user.oc1..xxxx" +fingerprint = "aa:bb:cc:dd:ee:ff" +subnet_id = "ocid1.subnet.oc1.iad.aaaaaaaaxxx" +private_key = <&2 + exit 0 + fi + + # Only format on first boot. On every later stop/start cycle the + # volume already has our filesystem + label, and re-running mkfs + # would destroy whatever the user saved in their home directory. + if ! blkid "$device" >/dev/null 2>&1; then + mkfs.ext4 -L "$label" "$device" + fi + + if ! grep -q "LABEL=$label" /etc/fstab; then + echo "LABEL=$label $mount_point ext4 defaults,nofail 0 2" >> /etc/fstab + fi + + mount "$mount_point" + chown "${username}:${username}" "$mount_point" + + # cloud-init's own ssh_authorized_keys handling runs before this + # script and writes to the root disk, but mounting the home volume + # here immediately shadows that. Provision the debug key directly on + # the mounted volume instead, so it actually survives. + ssh_public_key="${ssh_public_key}" + if [ -n "$ssh_public_key" ]; then + mkdir -p "$mount_point/.ssh" + chmod 700 "$mount_point/.ssh" + touch "$mount_point/.ssh/authorized_keys" + grep -qxF "$ssh_public_key" "$mount_point/.ssh/authorized_keys" || echo "$ssh_public_key" >> "$mount_point/.ssh/authorized_keys" + chmod 600 "$mount_point/.ssh/authorized_keys" + chown -R "${username}:${username}" "$mount_point/.ssh" + fi + - path: /etc/systemd/system/coder-agent.service + permissions: "0644" + content: | + [Unit] + Description=Coder Agent + After=network-online.target + Wants=network-online.target + + [Service] + User=${username} + ExecStart=/opt/coder/init + Environment=CODER_AGENT_TOKEN=${coder_agent_token} + Restart=always + RestartSec=10 + TimeoutStopSec=90 + KillMode=process + + OOMScoreAdjust=-1000 + SyslogIdentifier=coder-agent + + [Install] + WantedBy=multi-user.target +runcmd: + - /opt/coder/mount-home-volume.sh + - systemctl enable coder-agent + - systemctl start coder-agent \ No newline at end of file diff --git a/registry/anis/templates/oci-vm/main.tf b/registry/anis/templates/oci-vm/main.tf new file mode 100644 index 000000000..cd18897b9 --- /dev/null +++ b/registry/anis/templates/oci-vm/main.tf @@ -0,0 +1,343 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + oci = { + source = "oracle/oci" + } + } +} + +variable "tenancy_ocid" { + description = "Tenancy OCID" + type = string + default = "" +} + +variable "compartment_ocid" { + description = "Compartment OCID" + type = string + default = "" +} + +variable "user_ocid" { + description = "User OCID" + type = string + default = "" +} + +variable "fingerprint" { + description = "fingerprint" + type = string + default = "" +} + +variable "private_key" { + description = "Private Key File" + type = string + default = < Region Management in the OCI Console). + default = "us-ashburn-1" + mutable = false + option { + name = "France South (Marseille)" + value = "eu-marseille-1" + icon = "/emojis/1f1eb-1f1f7.png" + } + option { + name = "France Central (Paris)" + value = "eu-paris-1" + icon = "/emojis/1f1eb-1f1f7.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 = "US West (Phoenix)" + value = "us-phoenix-1" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "US East (Ashburn)" + value = "us-ashburn-1" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "Australia Southeast (Sydney)" + value = "ap-sydney-1" + icon = "/emojis/1f1e6-1f1fa.png" + } +} +data "coder_parameter" "instance_type" { + name = "instance_type" + display_name = "Instance type" + description = "What instance type should your workspace use?" + default = "VM.Standard.E3.Flex" + mutable = false + option { + name = "VM.Standard.E3.Flex" + value = "VM.Standard.E3.Flex" + } +} + +data "coder_parameter" "instance_ocpus" { + name = "instance_ocpus" + display_name = "OCPUs" + description = "Number of OCPUs (only for Flex shapes)" + type = "number" + default = 1 + mutable = true + option { + name = "1 OCPU" + value = 1 + } + option { + name = "2 OCPUs" + value = 2 + } + option { + name = "4 OCPUs" + value = 4 + } + option { + name = "8 OCPUs" + value = 8 + } +} + +data "coder_parameter" "instance_memory" { + name = "instance_memory" + display_name = "Memory (GB)" + description = "Amount of RAM (only for Flex shapes). Must be >= OCPUs (min 1:1 ratio) and <= 64x OCPUs (max 1:64 ratio)" + type = "number" + default = 2 + mutable = true + option { + name = "1 GB" + value = 1 + } + option { + name = "2 GB" + value = 2 + } + option { + name = "4 GB" + value = 4 + } + option { + name = "8 GB" + value = 8 + } + option { + name = "16 GB" + value = 16 + } + option { + name = "32 GB" + value = 32 + } + + validation { + min = data.coder_parameter.instance_ocpus.value + error = "Memory must be at least equal to OCPUs (minimum 1:1 ratio)." + } + +} + +provider "oci" { + tenancy_ocid = var.tenancy_ocid + user_ocid = var.user_ocid + fingerprint = var.fingerprint + private_key = var.private_key + region = data.coder_parameter.region.value +} + + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_agent" "main" { + os = "linux" + arch = "amd64" + + 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 = "home" + display_name = "Home Usage" + interval = 600 # every 10 minutes + timeout = 30 # df can take a while on large filesystems + script = "coder stat disk --path /home/${lower(data.coder_workspace_owner.me.name)}" + } +} + +locals { + compartment_id = var.compartment_ocid != "" ? var.compartment_ocid : var.tenancy_ocid + vm_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}" + root_disk_label = substr("${local.vm_name}-root", 0, 32) + home_volume_label = substr("${local.vm_name}-home", 0, 32) + # ext4 filesystem labels are capped at 16 bytes and mkfs silently + # truncates anything longer. home_volume_label (above) is fine for the + # OCI volume's display name (32-char limit) but can't double as the + # filesystem label without the two going out of sync, so it gets its + # own, guaranteed-short value. + home_fs_label = substr("${local.vm_name}-home", 0, 16) +} + +data "oci_core_images" "ubuntu_image" { + compartment_id = local.compartment_id + operating_system = "Canonical Ubuntu" + operating_system_version = "22.04" + shape = data.coder_parameter.instance_type.value +} + +data "oci_identity_availability_domains" "ads" { + compartment_id = local.compartment_id +} + +data "coder_parameter" "home_volume_size" { + name = "home_volume_size" + display_name = "Home Volume Size (GB)" + description = "How large would you like your home volume to be (in GB)?" + type = "number" + default = 50 + mutable = true + order = 3 + option { + name = "50GB" + value = 50 + } + option { + name = "60GB" + value = 60 + } + option { + name = "70GB" + value = 700 + } + + validation { + monotonic = "increasing" + } +} + +resource "oci_core_instance" "workspace" { + count = data.coder_workspace.me.start_count + availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name + compartment_id = local.compartment_id + shape = data.coder_parameter.instance_type.value + dynamic "shape_config" { + for_each = can(regex("Flex", data.coder_parameter.instance_type.value)) ? [1] : [] + content { + ocpus = data.coder_parameter.instance_ocpus.value + memory_in_gbs = data.coder_parameter.instance_memory.value + } + } + source_details { + source_type = "image" + source_id = data.oci_core_images.ubuntu_image.images[0].id + } + display_name = local.vm_name + create_vnic_details { + assign_public_ip = true + subnet_id = var.subnet_id + } + metadata = { + user_data = base64encode(templatefile("cloud-init/cloud-config.yaml.tftpl", { + hostname = local.vm_name + username = lower(data.coder_workspace_owner.me.name) + home_fs_label = local.home_fs_label + init_script = base64encode(coder_agent.main.init_script) + coder_agent_token = coder_agent.main.token + ssh_public_key = var.ssh_public_key + })) + } +} + +resource "oci_core_volume" "home_volume" { + availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name + compartment_id = local.compartment_id + display_name = local.home_volume_label + size_in_gbs = data.coder_parameter.home_volume_size.value +} + +resource "oci_core_volume_attachment" "attach_home" { + count = data.coder_workspace.me.start_count + attachment_type = "paravirtualized" + instance_id = oci_core_instance.workspace[0].id + #device = "/dev/sdb" + volume_id = oci_core_volume.home_volume.id +} + +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + version = "~> 1.0" + + agent_id = coder_agent.main.id + order = 1 +} + +resource "coder_metadata" "workspace-info" { + count = data.coder_workspace.me.start_count + resource_id = oci_core_instance.workspace[0].id + + item { + key = "region" + value = data.coder_parameter.region.value + } + item { + key = "type" + value = data.coder_parameter.instance_type.value + } +} \ No newline at end of file