@leettools/leet-deploy
v0.1.1
Published
Deploy applications to Kubernetes clusters or cloud VM instances
Maintainers
Readme
leet-deploy
A full-stack tool for deploying applications to Kubernetes clusters and cloud VM instances — a Python backend (CLI + FastAPI service) paired with a Vue web UI.
What's in this Repository
| Path | What it is |
| --------------------------------------------- | ------------------------------------------------------------------------------------------- |
| leet_deploy/ | Python package — CLI, FastAPI service, and Python API. See its README for full details. |
| leet_deploy_web/ | Vue 3 web UI that talks to the FastAPI service. |
| examples/ | Sample deploy.yaml files and VM configs. |
| skills/ | LLM-harness skills (spec-finder, purchase-spec, deploy-app, deploy-dockerfile-edge) that drive the CLI. |
| docs/ | Marketing and tutorial material for explaining the product workflow. |
| development/ | Developer notes and scratch material. |
What It Does
- Create Kubernetes clusters on AWS (EKS) or GCP (GKE) with a single command
- Deploy applications to Kubernetes from a
deploy.yamlfile (local, GitHub, or HTTP URL) - Launch cloud VMs with Docker containers on AWS EC2, GCP Compute Engine, or Aliyun ECS
- Publish VM apps through HTTPS edge stacks on GCP or Aliyun
- Ship local apps (compose file + locally-built images) to a VM over SSH — no registry required
- Manage deployments with status, rollback, and delete commands
- Drive everything from the browser via the Vue web UI, or from the CLI / HTTP API
Architecture
+-----------------+ HTTP +------------------------+ Helm / Cloud APIs +--------------------+
| Vue Web UI | <---------------> | FastAPI service | ----------------------------> | Kubernetes / VMs |
| leet_deploy_web | | leet_deploy (Python) | | (EKS / GKE / EC2 / ECS) |
+-----------------+ +-----------+------------+ +--------------------+
^
|
CLI (leet-deploy)
Python APIEverything the UI does ultimately goes through the Python package's FastAPI service, which in turn uses Helm and cloud provider APIs.
Prerequisites
- Python >= 3.11 (a conda environment named
deployis recommended) - Node.js v20.19.0 (via nvm) for the web UI
- Helm 3.x installed and in PATH
- kubectl configured with access to target cluster(s)
- Cloud credentials (AWS, GCP, and/or Aliyun) if you plan to use those providers
See leet_deploy/README.md for the exact environment variables.
Required Cloud Privileges
Grant the identity running leet-deploy at least the privileges below. Each
capability is separable — e.g., you can skip the pricing rows if you only
need catalog-refresh --skip-prices, or the EKS/GKE rows if you only launch
standalone VMs.
GCP
Credentials:
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
export GOOGLE_CLOUD_PROJECT=<project-id>Enable these APIs on the project (gcloud services enable <name>):
compute.googleapis.com— Compute Engine (VMs, firewalls, zones)container.googleapis.com— Kubernetes Engine (GKE clusters)cloudbilling.googleapis.com— Cloud Billing Catalog (instance prices)
Grant these roles to the service account:
| Capability | Role(s) |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| VM create/terminate + firewall rules (instance-deploy) | roles/compute.admin (or roles/compute.instanceAdmin.v1 + roles/compute.securityAdmin) |
| VM + global HTTPS load balancer + self-managed SSL (deploy-dockerfile-edge) | roles/compute.admin, or an equivalent custom role covering instances, firewalls, global addresses, backend services, health checks, URL maps, target HTTPS proxies, forwarding rules, zonal NEGs, and SSL certificates |
| VM + catalog read (cloud-discover, instance-status, catalog-refresh --skip-prices) | roles/compute.viewer |
| GKE cluster create/delete (cluster-create / cluster-delete) | roles/container.admin, plus roles/iam.serviceAccountUser on the node service account |
| Instance prices (catalog-refresh without --skip-prices) | roles/billing.viewer |
The service-account key must live in the same project as
GOOGLE_CLOUD_PROJECT — a key owned by a different project will work for
auth but the Cloud Billing Catalog then 403s against the key's home project
rather than the workload project.
The deploy-dockerfile-edge flow also requires GoDaddy API credentials
(GODADDY_API_KEY, GODADDY_API_SECRET) and an ACME account email in
LEET_DEPLOY_EDGE_TLS_EMAIL.
AWS
Credentials:
export AWS_ACCESS_KEY_ID=<your-key>
export AWS_SECRET_ACCESS_KEY=<your-secret>
export AWS_DEFAULT_REGION=us-west-2Grant these IAM actions to the principal:
| Capability | IAM actions |
| ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| VM instances (instance-deploy, instance-status, instance-cleanup, cloud-discover) | ec2:DescribeRegions, ec2:DescribeInstances, ec2:RunInstances, ec2:TerminateInstances, ec2:CreateTags, ec2:DescribeKeyPairs, ec2:ImportKeyPair, ec2:DescribeSecurityGroups, ec2:CreateSecurityGroup, ec2:AuthorizeSecurityGroupIngress |
| EKS clusters (cluster-create / cluster-delete) | eks:CreateCluster, eks:DescribeCluster, eks:ListClusters, eks:DeleteCluster, eks:CreateNodegroup, eks:DeleteNodegroup; plus iam:CreateRole, iam:GetRole, iam:AttachRolePolicy, iam:ListAttachedRolePolicies, iam:PassRole for the EKS service & node roles |
| --auto-vpc flag on cluster-create | ec2:CreateVpc, ec2:DescribeVpcs, ec2:ModifyVpcAttribute, ec2:DeleteVpc, ec2:CreateSubnet, ec2:DescribeSubnets, ec2:ModifySubnetAttribute, ec2:DeleteSubnet, ec2:CreateInternetGateway, ec2:DescribeInternetGateways, ec2:AttachInternetGateway, ec2:DetachInternetGateway, ec2:DeleteInternetGateway, ec2:CreateRouteTable, ec2:DescribeRouteTables, ec2:AssociateRouteTable, ec2:DisassociateRouteTable, ec2:CreateRoute, ec2:DeleteRouteTable, ec2:DescribeAvailabilityZones |
| Catalog refresh (catalog-refresh --provider aws [--skip-prices]) | ec2:DescribeRegions, ec2:DescribeInstanceTypes, ec2:DescribeInstanceTypeOfferings |
| On-demand prices (catalog-refresh without --skip-prices) | pricing:GetProducts, pricing:DescribeServices (the Pricing API is only served from us-east-1, ap-south-1, eu-central-1 — leet-deploy pins to us-east-1) |
| Spot prices (catalog-refresh --pricing-model spot) | ec2:DescribeSpotPriceHistory |
If you prefer AWS-managed policies over a custom one, the superset
AmazonEC2FullAccess + AWSPriceListServiceFullAccess covers the VM +
catalog + pricing rows. EKS also needs the cluster to assume
AmazonEKSClusterPolicy and worker nodes AmazonEKSWorkerNodePolicy +
AmazonEC2ContainerRegistryReadOnly + AmazonEKS_CNI_Policy — leet-deploy
creates and attaches these automatically when given the iam:* actions
above.
Aliyun
Credentials:
export ALIBABA_CLOUD_ACCESS_KEY_ID=<your-access-key-id>
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=<your-access-key-secret>
export ALIBABA_CLOUD_REGION_ID=cn-hangzhouThe Alibaba Cloud SDK credential chain can also read ~/.aliyun/config.json.
The ALIBABA_CLOUD_REGION_ID value is optional for commands that pass
--region, but it is useful for certificate DNS-01 operations.
Grant these RAM actions to the principal:
| Capability | RAM actions |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| VM discovery/status (cloud-discover, instance-status) | ecs:DescribeRegions, ecs:DescribeInstances |
| VM create/delete (instance-deploy, instance-cleanup) | ecs:RunInstances, ecs:DeleteInstance, ecs:DescribeImages, ecs:DescribeInstances, ecs:DescribeKeyPairs, ecs:ImportKeyPair, ecs:DescribeSecurityGroups, ecs:CreateSecurityGroup, ecs:AuthorizeSecurityGroup |
| VPC/VSwitch creation for ECS | vpc:DescribeVpcs, vpc:CreateVpc, vpc:DescribeVSwitches, vpc:CreateVSwitch |
| Catalog and pricing (catalog-refresh --provider aliyun) | ecs:DescribeRegions, ecs:DescribeInstanceTypes, ecs:DescribePrice |
| Aliyun HTTPS edge stack (ECS VM + CLB/SLB + AliDNS) | The VM and VPC actions above, plus slb:CreateLoadBalancer, slb:DescribeLoadBalancers, slb:DescribeLoadBalancerAttribute, slb:AddBackendServers, slb:RemoveBackendServers, slb:CreateLoadBalancerHTTPSListener, slb:StartLoadBalancerListener, slb:UploadServerCertificate, slb:DescribeServerCertificates, alidns:DescribeDomains, alidns:DescribeDomainRecords, alidns:AddDomainRecord, alidns:UpdateDomainRecord, alidns:DeleteDomainRecord |
If you prefer broad managed policies while testing, AliyunECSFullAccess,
AliyunVPCFullAccess, AliyunSLBFullAccess, and AliyunDNSFullAccess cover
the VM plus Aliyun edge path. cluster-create and cluster-delete currently
reject --provider aliyun; ACK orchestration is not implemented yet.
See docs/aliyun-edge-tutorial.md for an end-to-end ECS + CLB + AliDNS HTTPS walkthrough.
Running the Full Stack
1. Backend (FastAPI service)
From the repo root:
# Install with all cloud providers (pick [aws], [gcp], [aliyun], or omit for core-only)
conda run -n deploy pip install -e ".[cloud]"
# Start the API service
conda run -n deploy leet-deploy-service --host 0.0.0.0 --port 8000The API is available at http://localhost:8000 with interactive docs at http://localhost:8000/docs.
For CLI usage, default-cluster bootstrap, full HTTP API reference, deploy.yaml formats, and the Python API, see leet_deploy/README.md.
2. Frontend (Vue web UI)
From the repo root:
cd leet_deploy_web
# Copy the env template and configure the API URL
cp .env.example .env
# Defaults: VITE_API_BASE=http://localhost:8000, VITE_FRONTEND_PORT=5173
# Install dependencies and start the dev server (hot reload)
npm install
npm run devThe UI is available at http://localhost:5173. The API base URL can also be changed at runtime from the Settings page.
3. Production build of the UI
cd leet_deploy_web
npm run build # outputs to leet_deploy_web/dist
npm run preview # optional: serve the built assets locallyQuick Start (CLI only)
If you only want the CLI — no UI, no service — install the Python package and go:
pip install -e ".[cloud]"
# Create a cluster and deploy
leet-deploy cluster-create --provider aws --name my-cluster --region us-west-2 --nodes 2 --auto-vpc
leet-deploy deploy ./deploy.yaml --target aws:eks:my-clusterFull CLI reference: leet_deploy/README.md#cli-reference.
Using with an LLM Harness (Skills)
The skills/ directory ships four Anthropic-style agent skills that let a harness (Claude Code, the Claude API with skill loading, Claude Desktop with MCP, etc.) drive leet-deploy from natural-language prompts. The skills are plain Markdown — no in-repo LLM client is required; the harness does the model calls, the skills tell it which CLI invocations to produce.
| Skill | Purpose | | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | | skills/spec-finder/ | Turn "cheapest GCP VM for a daemon" into a concrete instance type by filtering the local catalog + prices. | | skills/purchase-spec/ | Provision a VM at a chosen spec with no app running (idle box for later SSH / custom setup). | | skills/deploy-app/ | Provision a VM at a chosen spec and run a Docker image on it, in one CLI call. | | skills/deploy-dockerfile-edge/ | Build a local Dockerfile, ship it to a fresh GCP VM, then wire GCP HTTPS load balancing, TLS, and GoDaddy DNS cutover. |
Typical end-to-end flow:
user: "cheapest GCP VM in us-central1 to run my image acme/hello:1.0"
├── spec-finder → calls `leet-deploy catalog-query` → picks e2-small
└── deploy-app → calls `leet-deploy instance-deploy
--provider gcp --region us-central1
--cpu 2 --memory 2
--image acme/hello:1.0 --wait`
← returns public IP + cleanup commandspec-finder only picks; purchase-spec and deploy-app are the two ways to act on that pick. purchase-spec never deploys an app, and deploy-app does both provisioning and deployment in the same step — you do not need to run purchase-spec first.
deploy-dockerfile-edge is the path for "I have a local Dockerfile and a GoDaddy domain" workflows. It runs a staged preflight first, then builds the Dockerfile locally, deploys a GCP VM, ships the image over SSH, creates the HTTPS load balancer, issues the certificate, and updates GoDaddy DNS.
For an Aliyun-only ECS + CLB + AliDNS edge deployment, use
leet-deploy instance-deploy --config with an edge VM config. See
docs/aliyun-edge-tutorial.md.
Loading the skills
- Claude Code: copy (or symlink)
skills/<name>/into~/.claude/skills/or your project's.claude/skills/. The harness will auto-match SKILL.mddescriptionagainst user prompts. - Claude API (skill loading): point your skill-loader at the
skills/directory; each subfolder is one skill. - Other harnesses: any tool that understands the
SKILL.md+ frontmatter convention can consume these directly.
Before the skills can recommend an instance, the catalog must be populated once per provider/region:
leet-deploy catalog-refresh --provider gcp --region us-central1
leet-deploy catalog-refresh --provider aws --region us-west-2
leet-deploy catalog-refresh --provider aliyun --region cn-hangzhouThe skills expect cloud credentials to be configured the same way the CLI does — see Required Cloud Privileges. They run leet-deploy connection-check as a preflight, so broken credentials surface before any VM is launched.
For deploy-dockerfile-edge, also set:
export GODADDY_API_KEY=<godaddy-key>
export GODADDY_API_SECRET=<godaddy-secret>
export LEET_DEPLOY_EDGE_TLS_EMAIL=<[email protected]>Optional overrides for that skill:
export LEET_DEPLOY_EDGE_ZONE=example.com # needed when deploying a subdomain
export LEET_DEPLOY_EDGE_REGION=us-central1
export LEET_DEPLOY_EDGE_CPU=2
export LEET_DEPLOY_EDGE_MEMORY_GIB=4Run the helper in preflight mode first:
conda run -n deploy python -m leet_deploy.edge.dockerfile_workflow \
--dockerfile examples/web_server/Dockerfile \
--domain xxx.com \
--preflight-onlyThen run the real deploy:
conda run -n deploy python -m leet_deploy.edge.dockerfile_workflow \
--dockerfile examples/web_server/Dockerfile \
--domain xxx.comPreparing an app for deploy-app
leet-deploy instance-deploy --image <ref> (which deploy-app invokes) boots an Ubuntu 22.04 VM, installs Docker via cloud-init, then runs:
docker pull <image>
docker run -d --name app -p 8080:8080 <image>Port 8080 is the only port opened on the VM's firewall / security group. So, for an image to work with deploy-app, it must:
- Be hosted on a publicly pullable registry (Docker Hub public, GHCR public, a public ECR/Artifact Registry repo). The VM has no registry credentials by default — private images will fail at
docker pull. If you need a private registry, use the config-file form ofinstance-deploywith a customcloudInit:that runsdocker loginbefore the pull. - Listen on
0.0.0.0:8080inside the container. Apps hard-coded to127.0.0.1or any other port won't be reachable onhttp://<public_ip>:8080. Map the port inside your Dockerfile (EXPOSE 8080) and bind your server accordingly. - Be
linux/amd64unless you picked an ARM spec. The AWS Graviton and GCP Tau/Axion families arearm64; everything else in the catalog isx86_64. Build multi-arch images (docker buildx build --platform linux/amd64,linux/arm64 …) or match the image arch to the spec you chose. - Start a long-running process in the foreground. The container is launched
-dwith no restart policy — if the process exits, the container stops and the app goes dark. Use an entrypoint that stays up (uvicorn,node,nginx -g 'daemon off;', etc.).
A minimal Dockerfile that satisfies all four:
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 8080
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]Push it to a public registry, then hand the image reference to the deploy-app skill (or to leet-deploy instance-deploy --image … directly). For anything more complex — multiple ports, env vars, volume mounts, private registries, custom OS image — use the config-file form with a full vm-config-*.yaml (see examples/vm-config-aws.yaml, examples/vm-config-gcp.yaml).
With the default --wait, the CLI does not return as soon as the VM gets
an IP. It now waits for SSH, the Docker daemon, the app container, and an
HTTP response from http://<public_ip>:8080.
For apps that need local secret files at deploy time, instance-deploy also supports repeatable --secret <local_path>=<container_path> mounts and a numeric --container-user override. When secrets are configured, leet-deploy waits for the VM to boot, uploads the local secret tree to /var/lib/leet-deploy/secrets/<instance-id>, tightens it to owner-only permissions, and starts the container with read-only bind mounts. The config-file form accepts the same settings under:
container:
user: "1000:1000"
secrets:
- source: "./secrets/prod.env"
target: "/run/secrets/app.env"Shipping a Local App (no registry)
When the app isn't published to a registry — a local build, a private
image, a quick iteration — use leet-deploy app-ship to push a
docker-compose.yml plus any locally-built images to an existing
VM and start them with docker compose up -d.
# 1. Build + tag locally (no push required):
docker build -t my-app:local ./app
# 2. Provision a VM once (idle):
leet-deploy instance-deploy --provider gcp --region us-central1 \
--name my-vm --cpu 2 --memory 4 --wait
# 3. Ship the compose app to it:
leet-deploy app-ship \
--compose examples/docker-compose-local.yml \
--instance-id <id> --provider gcp --region us-central1 \
--ssh-key ~/.ssh/id_rsaWhat happens:
- Parses the compose file and lists its images.
- For each image that exists locally, streams it to the VM over
SSH into
docker loadon the remote. Images not present locally (e.g.redis:7-alpine) are left for the remotedocker composeto pull. - Uploads a
build:-stripped copy ofdocker-compose.ymlto the VM (default/opt/app/docker-compose.yml). - Runs
docker compose up -don the VM and returnsdocker compose ps.
Key properties:
- No docker CLI on the local host. Images are read via the Docker
Engine API (
docker-py), so leet-deploy can run inside a container with the host socket mounted (-v /var/run/docker.sock:/var/run/docker.sock). - No registry push. Nothing leaves your machine except over the SSH connection to the target VM.
- Services without an
image:tag fail fast. Compose services that only have abuild:block are rejected —app-shipnever builds; tag the image locally (docker build -t name:tag) and addimage: name:tagin the compose file. - Targeting. Use
--instance-id/--provider/--regionto resolve the VM's public IP via the cloud provider, or pass--host <ip>directly.
Install the extra deps:
pip install -e ".[app-ship]" # adds docker-py and paramikoSee examples/docker-compose-local.yml for a minimal starting point.
Development
Backend:
conda run -n deploy pip install -e ".[dev]"
conda run -n deploy pytest
conda run -n deploy mypy leet_deploy
conda run -n deploy ruff check leet_deployFrontend:
cd leet_deploy_web
npm install
npm run devLicense
MIT
