feat: DevPod remote development environment
Adds one-click remote dev on Kapsule with Claude Code, persistent storage, and autoscale-to-zero. GP1-L (16 vCPU, 64GB) dev pool, 50Gi PVC for home directory, devcontainer image on SCW registry. - .devcontainer/ (Dockerfile, devcontainer.json, pod-template, init-home) - Terraform gpu-dev pool (GP1-L, 0→1 autoscaling) - CI job to build devcontainer image (Kaniko → SCW registry) - scripts/devpod-setup.sh for one-time provider config - "Open in DevPod" link: https://devpod.sh/open#ssh://git@100.90.76.85:2222/root/foxhunt.git Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
105
.devcontainer/Dockerfile
Normal file
105
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,105 @@
|
||||
# Foxhunt devcontainer — extends CI builder with interactive dev tooling
|
||||
# Built by CI, pushed to GitLab registry as devcontainer:latest
|
||||
# See: docs/plans/2026-02-25-devcontainer-design.md
|
||||
|
||||
ARG CI_BUILDER_IMAGE=rg.fr-par.scw.cloud/foxhunt-ci/ci-builder:latest
|
||||
FROM ${CI_BUILDER_IMAGE}
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Dev tooling: Node.js 22 (Claude Code), Python 3, shell tools
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
zsh \
|
||||
sudo \
|
||||
openssh-server \
|
||||
git-lfs \
|
||||
jq \
|
||||
ripgrep \
|
||||
fd-find \
|
||||
htop \
|
||||
less \
|
||||
vim \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Node.js 22 LTS (Claude Code runtime)
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get install -y nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Claude Code CLI
|
||||
RUN npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# kubectl
|
||||
RUN curl -fsSL "https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
|
||||
-o /usr/local/bin/kubectl \
|
||||
&& chmod +x /usr/local/bin/kubectl
|
||||
|
||||
# helm
|
||||
RUN curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||||
|
||||
# scw CLI (Scaleway)
|
||||
RUN curl -fsSL https://github.com/scaleway/scaleway-cli/releases/latest/download/scaleway-cli_linux_amd64 \
|
||||
-o /usr/local/bin/scw \
|
||||
&& chmod +x /usr/local/bin/scw
|
||||
|
||||
# glab (GitLab CLI)
|
||||
RUN curl -fsSL "https://gitlab.com/gitlab-org/cli/-/releases/permalink/latest/downloads/glab_linux_amd64.deb" \
|
||||
-o /tmp/glab.deb \
|
||||
&& dpkg -i /tmp/glab.deb \
|
||||
&& rm /tmp/glab.deb
|
||||
|
||||
# terraform
|
||||
RUN curl -fsSL https://releases.hashicorp.com/terraform/1.11.2/terraform_1.11.2_linux_amd64.zip \
|
||||
-o /tmp/terraform.zip \
|
||||
&& unzip -o /tmp/terraform.zip -d /usr/local/bin \
|
||||
&& rm /tmp/terraform.zip
|
||||
|
||||
# terragrunt
|
||||
RUN curl -fsSL https://github.com/gruntwork-io/terragrunt/releases/download/v0.72.6/terragrunt_linux_amd64 \
|
||||
-o /usr/local/bin/terragrunt \
|
||||
&& chmod +x /usr/local/bin/terragrunt
|
||||
|
||||
# yq (YAML processor)
|
||||
RUN curl -fsSL https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \
|
||||
-o /usr/local/bin/yq \
|
||||
&& chmod +x /usr/local/bin/yq
|
||||
|
||||
# Non-root dev user with sudo
|
||||
RUN groupadd -g 1000 dev \
|
||||
&& useradd -m -u 1000 -g dev -s /bin/zsh dev \
|
||||
&& echo "dev ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/dev
|
||||
|
||||
# Install oh-my-zsh for dev user
|
||||
RUN su - dev -c 'sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended'
|
||||
|
||||
# Move Rust toolchain to shared location so dev user can access it
|
||||
# (CI builder installs to /root/.cargo and /root/.rustup)
|
||||
RUN cp -a /root/.cargo /opt/cargo \
|
||||
&& cp -a /root/.rustup /opt/rustup \
|
||||
&& chown -R dev:dev /opt/cargo /opt/rustup
|
||||
|
||||
# Environment for dev user
|
||||
ENV CARGO_HOME=/home/dev/.cargo
|
||||
ENV RUSTUP_HOME=/opt/rustup
|
||||
ENV PATH="/home/dev/.cargo/bin:/opt/rustup/bin:/opt/cargo/bin:${PATH}"
|
||||
ENV SQLX_OFFLINE=true
|
||||
ENV CUDA_COMPUTE_CAP=90
|
||||
|
||||
# sccache local disk cache (PVC-backed home dir)
|
||||
ENV SCCACHE_DIR=/home/dev/.cache/sccache
|
||||
ENV RUSTC_WRAPPER=/usr/local/bin/sccache
|
||||
|
||||
# First-run init script: copies cargo bins to home if not already present
|
||||
COPY .devcontainer/init-home.sh /usr/local/bin/init-home.sh
|
||||
RUN chmod +x /usr/local/bin/init-home.sh
|
||||
|
||||
USER dev
|
||||
WORKDIR /workspace
|
||||
|
||||
# Verify key tools
|
||||
RUN rustc --version && cargo --version && node --version && claude --version \
|
||||
&& kubectl version --client 2>/dev/null && helm version --short \
|
||||
&& terraform --version && terragrunt --version && scw version && glab --version
|
||||
23
.devcontainer/devcontainer.json
Normal file
23
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "foxhunt",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"context": "..",
|
||||
"args": {
|
||||
"CI_BUILDER_IMAGE": "rg.fr-par.scw.cloud/foxhunt-ci/ci-builder:latest"
|
||||
}
|
||||
},
|
||||
"remoteUser": "dev",
|
||||
"containerEnv": {
|
||||
"SQLX_OFFLINE": "true",
|
||||
"CUDA_COMPUTE_CAP": "90",
|
||||
"EDITOR": "vim"
|
||||
},
|
||||
"postCreateCommand": "/usr/local/bin/init-home.sh",
|
||||
"postStartCommand": "claude --version && cargo --version && kubectl cluster-info 2>/dev/null || true",
|
||||
"customizations": {
|
||||
"devpod": {
|
||||
"provider": "kubernetes"
|
||||
}
|
||||
}
|
||||
}
|
||||
25
.devcontainer/init-home.sh
Executable file
25
.devcontainer/init-home.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Bootstrap cargo into PVC-backed home if not already present
|
||||
if [ ! -d "$HOME/.cargo/bin" ]; then
|
||||
echo "[init-home] First run: linking cargo toolchain..."
|
||||
mkdir -p "$HOME/.cargo/bin"
|
||||
# Copy cargo config and link binaries from shared rustup
|
||||
for bin in /opt/cargo/bin/*; do
|
||||
ln -sf "$bin" "$HOME/.cargo/bin/$(basename "$bin")"
|
||||
done
|
||||
echo "[init-home] Cargo toolchain linked."
|
||||
fi
|
||||
|
||||
# Ensure sccache cache dir exists
|
||||
mkdir -p "${SCCACHE_DIR:-$HOME/.cache/sccache}"
|
||||
|
||||
# Default .zshrc if none exists (oh-my-zsh is in the image, config in PVC)
|
||||
if [ ! -f "$HOME/.zshrc" ]; then
|
||||
cp /home/dev/.oh-my-zsh/templates/zshrc.zsh-template "$HOME/.zshrc" 2>/dev/null || true
|
||||
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> "$HOME/.zshrc"
|
||||
echo 'export SQLX_OFFLINE=true' >> "$HOME/.zshrc"
|
||||
fi
|
||||
|
||||
echo "[init-home] Ready."
|
||||
41
.devcontainer/pod-template.yaml
Normal file
41
.devcontainer/pod-template.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
# DevPod pod template — merged with DevPod's generated pod spec
|
||||
# Ref: https://github.com/loft-sh/devpod-provider-kubernetes
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: devpod
|
||||
app.kubernetes.io/part-of: foxhunt
|
||||
app.kubernetes.io/component: dev-environment
|
||||
spec:
|
||||
nodeSelector:
|
||||
k8s.scaleway.com/pool-name: gpu-dev
|
||||
containers:
|
||||
- name: devpod
|
||||
resources:
|
||||
requests:
|
||||
cpu: "4000m"
|
||||
memory: "16Gi"
|
||||
limits:
|
||||
cpu: "14000m"
|
||||
memory: "56Gi"
|
||||
volumeMounts:
|
||||
- name: dev-home
|
||||
mountPath: /home/dev
|
||||
- name: training-data
|
||||
mountPath: /data/training
|
||||
readOnly: true
|
||||
initContainers:
|
||||
- name: init-home
|
||||
image: busybox:latest
|
||||
command: ["sh", "-c", "chown -R 1000:1000 /home/dev"]
|
||||
volumeMounts:
|
||||
- name: dev-home
|
||||
mountPath: /home/dev
|
||||
volumes:
|
||||
- name: dev-home
|
||||
persistentVolumeClaim:
|
||||
claimName: dev-home-pvc
|
||||
- name: training-data
|
||||
persistentVolumeClaim:
|
||||
claimName: training-data-pvc
|
||||
@@ -57,6 +57,35 @@ build-ci-builder:
|
||||
--dockerfile "${CI_PROJECT_DIR}/infra/docker/Dockerfile.ci-builder"
|
||||
--destination "${CI_BUILDER_IMAGE}"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Stage 0b: Build devcontainer image → push to internal GitLab registry
|
||||
# --------------------------------------------------------------------------
|
||||
build-devcontainer:
|
||||
stage: prepare
|
||||
image:
|
||||
name: gcr.io/kaniko-project/executor:debug
|
||||
entrypoint: [""]
|
||||
tags:
|
||||
- kapsule
|
||||
- docker
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "push"
|
||||
changes:
|
||||
- .devcontainer/**
|
||||
when: on_success
|
||||
- when: manual
|
||||
allow_failure: true
|
||||
before_script:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- |
|
||||
echo "{\"auths\":{\"rg.fr-par.scw.cloud\":{\"username\":\"nologin\",\"password\":\"${SCW_SECRET_KEY}\"}}}" > /kaniko/.docker/config.json
|
||||
script:
|
||||
- /kaniko/executor
|
||||
--context "${CI_PROJECT_DIR}"
|
||||
--dockerfile "${CI_PROJECT_DIR}/.devcontainer/Dockerfile"
|
||||
--destination "rg.fr-par.scw.cloud/foxhunt-ci/devcontainer:${CI_COMMIT_SHA}"
|
||||
--destination "rg.fr-par.scw.cloud/foxhunt-ci/devcontainer:latest"
|
||||
|
||||
# Base template for Rust jobs — pre-baked CI builder from Scaleway CR
|
||||
# Image pre-exists in SCR; rebuild via build-ci-builder when Dockerfile changes
|
||||
.rust-base:
|
||||
|
||||
16
infra/k8s/storage/dev-home-pvc.yaml
Normal file
16
infra/k8s/storage/dev-home-pvc.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: dev-home-pvc
|
||||
namespace: foxhunt
|
||||
labels:
|
||||
app.kubernetes.io/name: dev-home
|
||||
app.kubernetes.io/part-of: foxhunt
|
||||
app.kubernetes.io/component: devpod
|
||||
spec:
|
||||
storageClassName: scw-bssd
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 50Gi
|
||||
@@ -26,4 +26,9 @@ inputs = {
|
||||
gitlab_type = "GP1-XS"
|
||||
|
||||
# CI builds run on gpu-training pool (H100: 24 vCPU, 240GB, fast builds + GPU tests)
|
||||
|
||||
# Dev pool (GP1-L for DevPod remote development, autoscales to zero)
|
||||
enable_dev_pool = true
|
||||
dev_pool_type = "GP1-L"
|
||||
dev_pool_max_size = 1
|
||||
}
|
||||
|
||||
@@ -89,3 +89,21 @@ resource "scaleway_k8s_pool" "gitlab" {
|
||||
region = var.region
|
||||
}
|
||||
|
||||
# CPU pool for remote development (DevPod — autoscales to zero)
|
||||
resource "scaleway_k8s_pool" "dev" {
|
||||
count = var.enable_dev_pool ? 1 : 0
|
||||
cluster_id = scaleway_k8s_cluster.foxhunt.id
|
||||
name = "gpu-dev"
|
||||
node_type = var.dev_pool_type
|
||||
size = 1
|
||||
min_size = 0
|
||||
max_size = var.dev_pool_max_size
|
||||
autoscaling = true
|
||||
autohealing = true
|
||||
region = var.region
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [size]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,3 +69,21 @@ variable "gitlab_type" {
|
||||
default = "DEV1-L"
|
||||
}
|
||||
|
||||
variable "enable_dev_pool" {
|
||||
description = "Create CPU node pool for remote development (DevPod)"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "dev_pool_type" {
|
||||
description = "Instance type for the dev node pool"
|
||||
type = string
|
||||
default = "GP1-L"
|
||||
}
|
||||
|
||||
variable "dev_pool_max_size" {
|
||||
description = "Maximum number of nodes in the dev pool"
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
|
||||
70
scripts/devpod-setup.sh
Executable file
70
scripts/devpod-setup.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# DevPod one-time setup for Foxhunt development
|
||||
# Prerequisites: devpod CLI installed, kubectl configured for foxhunt cluster
|
||||
#
|
||||
# Install DevPod:
|
||||
# curl -fsSL https://devpod.sh/install.sh | bash
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/devpod-setup.sh
|
||||
# devpod up . # from the foxhunt repo root
|
||||
# devpod up git@100.90.76.85:2222/root/foxhunt.git # from anywhere
|
||||
|
||||
echo "=== Foxhunt DevPod Setup ==="
|
||||
|
||||
# Check prerequisites
|
||||
command -v devpod >/dev/null 2>&1 || { echo "Error: devpod not installed. Run: curl -fsSL https://devpod.sh/install.sh | bash"; exit 1; }
|
||||
command -v kubectl >/dev/null 2>&1 || { echo "Error: kubectl not found"; exit 1; }
|
||||
|
||||
# Verify cluster access
|
||||
if ! kubectl -n foxhunt get ns foxhunt >/dev/null 2>&1; then
|
||||
echo "Error: Cannot reach foxhunt namespace. Check kubectl config."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure PVC exists
|
||||
if ! kubectl -n foxhunt get pvc dev-home-pvc >/dev/null 2>&1; then
|
||||
echo "Creating dev-home-pvc (50Gi block storage)..."
|
||||
kubectl apply -f infra/k8s/storage/dev-home-pvc.yaml
|
||||
fi
|
||||
|
||||
# Add kubernetes provider (idempotent)
|
||||
devpod provider add kubernetes 2>/dev/null || true
|
||||
|
||||
# Configure provider
|
||||
devpod provider set kubernetes \
|
||||
KUBERNETES_NAMESPACE=foxhunt \
|
||||
NODE_SELECTOR="k8s.scaleway.com/pool-name=gpu-dev" \
|
||||
DISK_SIZE=10Gi \
|
||||
STORAGE_CLASS=scw-bssd \
|
||||
INACTIVITY_TIMEOUT=30m \
|
||||
IMAGE_PULL_SECRETS=gitlab-registry \
|
||||
POD_MANIFEST_TEMPLATE=.devcontainer/pod-template.yaml
|
||||
|
||||
# Configure image pull secrets if not already present
|
||||
if ! kubectl -n foxhunt get secret gitlab-registry >/dev/null 2>&1; then
|
||||
echo ""
|
||||
echo "NOTE: You need to create the gitlab-registry pull secret:"
|
||||
echo " kubectl -n foxhunt create secret docker-registry gitlab-registry \\"
|
||||
echo " --docker-server=gitlab-registry.foxhunt.svc.cluster.local:5000 \\"
|
||||
echo " --docker-username=<user> --docker-password=<token>"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Setup complete ==="
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " devpod up . # from repo root"
|
||||
echo " devpod up git@100.90.76.85:2222/root/foxhunt.git # from anywhere"
|
||||
echo ""
|
||||
echo "Open in DevPod link (bookmark this):"
|
||||
echo " https://devpod.sh/open#ssh://git@100.90.76.85:2222/root/foxhunt.git"
|
||||
echo ""
|
||||
echo "Session lifecycle:"
|
||||
echo " devpod up foxhunt # start/reconnect"
|
||||
echo " devpod stop foxhunt # stop (PVC preserved, node scales down)"
|
||||
echo " devpod delete foxhunt # delete workspace (PVC preserved)"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user