
Volcengine Deploy
- 31 installs
- 16 repo stars
- Updated August 3, 2026
- volcengine/volcengine-skills
Helps with devops & ci/cd tasks.
About
volcengine-deploy is a Claude Code skill for devops & ci/cd. It helps solo builders move faster with AI-assisted development.
- volcengine-deploy
- DevOps & CI/CD
- AI-coding skill
Volcengine Deploy by the numbers
- 31 all-time installs (skills.sh)
- +2 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #849 of 1,437 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/volcengine/volcengine-skills --skill volcengine-deployAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 31 |
|---|---|
| repo stars | ★ 16 |
| Last updated | August 3, 2026 |
| Repository | volcengine/volcengine-skills ↗ |
What it does
Helps with devops & ci/cd tasks.
Files
Volcengine Deploy Skill
Deploy a local project directory or remote Git URL to Volcengine after the user chooses ECS / VKE / veFaaS and resource management (cli or iac). Keep deployment execution pragmatic: use volcengine-iac only when the user chooses Terraform/IaC or already has an IaC workflow; otherwise use ve CLI plus .volcengine/created-resources.json.
---
0. Prerequisites
Volcengine authentication is checked by the execution skill you call (volcengine-cli, volcengine-iac, or volcengine-vefaas). Accept either the required AK/SK env vars for that skill or an already configured CLI profile when that skill supports it; do not duplicate their hard env requirements here.
Check tools after the user chooses a path:
| Mode | Required tools |
|---|---|
| ECS | ve, git, jq, curl; ssh only if the user opens port 22; docker/docker compose only for Docker or compose packaging |
| VKE | ve, docker, kubectl, git, jq, curl |
| veFaaS | switch to/call the volcengine-vefaas skill, which checks vefaas, Node.js, auth, framework detection, and deploy commands |
tosutil is optional for ECS artifact transfer and TOS buckets. Do not add it as a hard prerequisite for volcengine-deploy; if it is absent, use SSH/scp when allowed or ask the user for an existing artifact URL.
If the user has not chosen a mode, run volcengine-prepare inline or ask for these decisions:
1. Deployment mode: ECS / VKE / veFaaS (recorded as `ecs` / `vke` / `vefaas`)
2. Resource strategy: new isolated project deploy-<repo>, or reuse existing resources
3. Resource management: CLI resource ledger / Terraform IaC (recorded as `cli` / `iac`)Persistent local state lives under .volcengine/ in the repo root:
.volcengine/
deploy-choice.json
created-resources.json # CLI fast path only
iac-outputs.json
terraform/ # IaC-managed resources---
1. Stage 0 — Resolve repo and choice
input="${1:-.}"
if [[ "$input" =~ ^(https?|git@) ]]; then
repo_name=$(basename "$input" .git)
work_dir="/tmp/volcengine-deploy/$repo_name"
mkdir -p "$work_dir"
if [ -d "$work_dir/src/.git" ]; then
git -C "$work_dir/src" pull --ff-only
else
git clone --depth 1 "$input" "$work_dir/src"
fi
repo_dir="$work_dir/src"
else
repo_dir=$(cd "${input:-.}" && pwd)
repo_name=$(basename "$repo_dir")
work_dir="$repo_dir/.volcengine"
mkdir -p "$work_dir"
fi
git_sha=$(cd "$repo_dir" && git rev-parse --short HEAD 2>/dev/null || echo "$(date +%s)")Local directories are deployed in place and are not cloned. For Git URLs, use shallow clone first; if clone repeatedly fails, try an archive/subdirectory path or stop with a clear "not suitable for quick remote build" message. Do not claim a README/static mirror is the deployed application.
Load .volcengine/deploy-choice.json if present. If absent, ask the fixed decisions above or run volcengine-prepare.
Choice file shape:
{
"schema_version": "1",
"repo_dir": "/absolute/path",
"repo_name": "my-app",
"git_sha": "abc1234",
"region": "cn-beijing",
"mode": "ecs",
"port": 8080,
"dependencies": ["postgresql", "redis"],
"database_product": "aidap",
"database_engine": "supabase",
"resource_strategy": "create-isolated-project",
"project": "deploy-my-app",
"infra_management": "cli"
}Confirm before creating resources:
Deploying <repo_name> via <mode> in <region>.
Resources: <new isolated project deploy-... | reuse existing resources>
Proceed? [y/N]---
2. Resource ledger
Use the resource ledger only for CLI-created resources. IaC-created resources are tracked by Terraform state and exported through .volcengine/iac-outputs.json.
Every resource created by volcengine-deploy must be appended to .volcengine/created-resources.json immediately after creation. This is mandatory for cleanup and failure recovery.
Ledger entry:
{
"type": "eip",
"id": "eip-xxxx",
"name": "deploy-myapp-eip",
"region": "cn-beijing",
"project": "deploy-myapp",
"reused": false,
"created_at": "2026-05-29T00:00:00Z",
"delete_command": "ve vpc ReleaseEipAddress --AllocationId eip-xxxx"
}Rules:
- New resources:
reused=false, include exact delete command. - Reused resources:
reused=true, do not include them in destructive cleanup. - If an EIP is created inline with an ECS instance and released with that instance, mark it as
dependent=true/cleanup_optional=trueor omit it as an independent ledger item. Do not make cleanup fail just because the instance already released the EIP. - On failure, print cleanup commands in reverse ledger order. There is currently no one-command cleanup runner; the user must review and run ledger
delete_commandvalues manually. Do not silently delete unless the user confirms. - Prefer creating or using an isolated Volcengine project named
deploy-<repo>for new resources, but confirm the project exists or can be created before passing that project name to resource creation. If project creation is unavailable, usedefaultand isolate resources with names and tags.
---
3. Resource management dispatch
Before provisioning, confirm one resource management path with the user:
| Condition | Path |
|---|---|
| VKE, managed DB/cache/storage/LB/domain/certificate, team-owned infra, or plan/diff/destroy matters | volcengine-iac |
| Pure ECS single-VM service with no managed dependencies and no explicit plan/diff/destroy requirement | CLI fast path |
| User says temporary/demo/quick validation/just run it | CLI fast path |
| Terraform/provider registry is unavailable, especially in China networks, and the target is not VKE/managed dependencies/team-owned infra | CLI fallback |
| User explicitly says no Terraform/IaC | CLI fast path |
These are recommendations, not defaults. If .volcengine/deploy-choice.json lacks infra_management, ask before creating resources:
Resource management recommendation: <cli|iac>, reason: <short reason>. Confirm the CLI resource ledger or Terraform/IaC? (`cli` / `iac`)When using IaC:
1. Call or switch to volcengine-iac with .volcengine/deploy-choice.json. 2. Run Terraform generation, validate, plan, and explicit apply confirmation under that skill. 3. Consume .volcengine/iac-outputs.json for VPC/subnet/security group/cluster/CR/database/cache outputs. 4. Continue deployment packaging and runtime steps here: build/pull image, run Cloud Assistant, apply Kubernetes manifests, run migrations, and verify health.
When using CLI:
1. Create resources directly with ve. 2. Append every created resource to .volcengine/created-resources.json immediately. 3. Print reverse-order cleanup commands on failure.
---
4. Environment and Dependency Wiring
Before starting ECS services or applying Kubernetes manifests, resolve runtime configuration:
1. Read .env.example, .env.sample, framework config, and dependency outputs from IaC/CLI provisioning. 2. Split non-sensitive values into config and sensitive values into secrets. Treat connection strings, passwords, tokens, AK/SK, and session tokens as secrets. 3. Ask the user for missing required values. Do not print secret values back to the user, do not write them to logs, and write generated local files with mode 0600. 4. For ECS systemd, write /opt/<repo>/.env before starting the service; the unit template reads it through EnvironmentFile=-/opt/<repo>/.env. 5. For VKE, generate ConfigMap and Secret manifests from the resolved values. Never leave <connection-string> placeholders in an applied Secret.
Managed dependency wiring must be completed before health checks:
- RDS database (
database_product=rds, enginemysql/postgresql/sqlserver): create or reuse the instance, database, and app account; use the private endpoint; buildDATABASE_URL; add the ECS/VKE subnet CIDR or security group source to the database allowlist; run migrations explicitly whenmigration_pathsis non-empty. - AIDAP database (
database_product=aidap, enginesupabase/postgresql): callvolcengine-db-supabaseto create or reuse the workspace, branch, app DB account/database, and return database/AIDAP env values before app health checks. - Redis: create or reuse the instance and app account/password; use the private endpoint; build
REDIS_URL; add the ECS/VKE subnet CIDR or security group source to the Redis allowlist. - If the user declines managed services for a detected dependency, state the persistence/scaling tradeoff and wire the chosen alternative into the same env/Secret path.
---
5. Branch dispatch
case "$deploy_mode" in
ecs) proceed_ecs ;;
vke) proceed_vke ;;
vefaas) run_vefaas_skill ;;
*) echo "Unknown deploy mode: $deploy_mode"; exit 2 ;;
esac---
6. ECS branch
ECS is the default lightweight VM path. Public services must get an EIP so the user can access the service after deployment.
Select packaging from the repo shape: compose file -> compose on ECS; Dockerfile -> Docker on ECS; clear binary or single process -> binary + systemd; otherwise ask one focused start-command question.
Keep these hard boundaries in the main context:
- Use IaC outputs when
infra_management=iac; otherwise use the CLI ledger path and record every CLI-created resource immediately. - Do not hardcode instance type or OS image. Query availability and avoid fuzzy image matches that return GPU, WebUI, marketplace, or unrelated images.
- If SSH 22 is not explicitly approved, keep it closed and use Cloud Assistant. If SSH is approved, restrict it to the current outbound IP when possible.
- Volcengine RunCommand is asynchronous. Poll invocation results before treating the command as successful.
- Generated one-time ECS passwords must not be printed or written to ledger/state.
- Validate listening port, local health/root path, public endpoint, logs, and one core app behavior where possible.
Read `references/ecs-deploy-steps.md` for the detailed ECS packaging, upload, Cloud Assistant, Docker mirror, architecture, health-gate, and cleanup workflow.
---
7. veFaaS branch
Do not duplicate veFaaS deployment details here. If the user chooses veFaaS, switch to/call the volcengine-vefaas skill with:
- repo path
- app name
- region
- detected framework/port if known
- environment variable notes
- any warning from prepare
Tell the user the volcengine-vefaas skill will run vefaas inspect, verify login, create/link the app, configure env vars if needed, deploy, and print domains.
If the volcengine-vefaas skill fails, return to this main deployment flow. Summarize the failure, then offer the user a choice:
- fix the veFaaS issue and retry,
- switch to ECS,
- switch to VKE.
---
8. VKE branch
Recommend volcengine-iac for VKE resource provisioning because cluster, node pool, CR, LB, and managed dependencies benefit from plan/diff/destroy safety. Use ve CLI plus the resource ledger when the user chooses CLI after seeing the tradeoff, for temporary validation, explicit user preference, or IaC fallback.
After choosing VKE, check docker, kubectl, ve, and terraform/jq if using IaC. Build for the node architecture, defaulting to linux/amd64 unless cluster data proves otherwise; inspect the pushed image platform before rollout.
Keep this ordered execution skeleton — these actions must be chained in sequence, and a later step run before an earlier one converges is the most common VKE failure:
1. Provision or reuse VKE + CR (IaC outputs or CLI fast path). 2. Wait for the cluster to be Running, then fetch the kubeconfig (from IaC outputs or CreateKubeconfig). 3. Verify addons: core-dns present; prefer cr-credential-controller for private CR pulls. 4. Build the image for the node architecture. 5. Authenticate to CR, push the image, and inspect the pushed platform. 6. Resolve env/Secret values and dependency outputs. 7. Generate manifests from resolved values. 8. Run migrations as a Kubernetes Job when migration paths exist. 9. Apply, then wait for rollout and the LoadBalancer/EIP. 10. Verify the public endpoint and one core app behavior.
Keep these hard boundaries in the main context:
- Use IaC outputs for VPC/subnets/security group/VKE/CR when available; otherwise create or reuse through the CLI fast path and record resources.
CreateKubeconfigbefore the cluster isRunningreturnsOperationDenied— poll toRunningfirst.- Confirm
core-dnsbefore relying on in-cluster DNS. - Prefer
cr-credential-controllerfor private Volcengine CR image pulls instead of storing registry passwords in app manifests. - Re-read
Result.UsernamefromGetAuthorizationTokenfordocker login; never invent a fallback username. - Resolve ConfigMap/Secret values before applying workloads; never leave placeholders in applied Secret manifests.
- Run migrations as a Kubernetes Job when migration paths exist.
- Wait for rollout and LoadBalancer/EIP, then verify the public endpoint.
For managed dependencies, prefer managed Volcengine services when practical; otherwise state clearly when the plan is deploying stateful containers inside VKE.
Read `references/vke-deploy-steps.md` for the full VKE pipeline (cluster wait, kubeconfig, addon checks, CR auth/push, rollout, endpoint verify), with `references/k8s-manifests.md` for manifest templates and `references/dockerfile-templates.md` for image build templates.
---
9. Deployment summary
Print one access card:
volcengine-deploy — <repo_name> (<git_sha>)
Mode: <ecs|vke|vefaas>
Region: <region>
Project: <deploy-project or reused resources>
URL: <public endpoint>
Health: <checked URL/status>
Acceptance: <core app behavior checked, or reason only transport health was possible>
Resources: .volcengine/created-resources.json
IaC: <.volcengine/terraform + .volcengine/iac-outputs.json | n/a>
Logs: <journalctl / docker logs / kubectl logs / vefaas logs command>
Cleanup: <reverse-order cleanup commands or ledger path>
Notes: <credentials/env/migration warnings>Do not add custom domain, HTTPS, dashboards, or cost cards unless the user asks; those are day-2 tasks.
---
10. Reference details
Use these references only when executing the corresponding path:
- ECS build/systemd/upload details: `references/ecs-deploy-steps.md`
- VKE deploy pipeline (cluster/kubeconfig/addons/CR/rollout): `references/vke-deploy-steps.md`
- veFaaS deploy handoff details: `references/faas-deploy-steps.md`
- Dockerfile templates: `references/dockerfile-templates.md`
- Kubernetes manifests: `references/k8s-manifests.md`
- Runtime dependencies: `references/supported-dependencies.md`
---
11. Common failure modes
Common gotchas are intentionally kept as references so the main skill stays adaptive:
- ECS instance/image/Cloud Assistant/SSH/Docker mirror issues: `references/ecs-deploy-steps.md`
- Container architecture and Dockerfile pitfalls: `references/dockerfile-templates.md`
- VKE pipeline sequencing (kubeconfig/addons/CR auth/rollout): `references/vke-deploy-steps.md`
- Kubernetes readiness, LoadBalancer, probes, and manifest issues: `references/k8s-manifests.md`
- Managed dependency wiring and migrations: `references/supported-dependencies.md`
- veFaaS CLI/auth/framework setup: `references/faas-deploy-steps.md` and
volcengine-vefaas
Dockerfile templates
When the repo has no Dockerfile, generate one based on the project type. All templates follow best practices:
- Multi-stage build
- Non-root user
- Minimal base image
- HEALTHCHECK instruction
- Pinned version tags
---
Node.js (Express / Fastify / NestJS / Koa)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && cp -R node_modules /prod_modules
RUN npm ci
COPY . .
RUN npm run build 2>/dev/null || true
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /prod_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
COPY --from=builder /app/src ./src
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]Adaptation notes:
- Adjust the port fromPORTinpackage.jsonor from code detection
- If there is no build step, dropnpm run buildand thedistdirectory andCOPY srcdirectly
- If usingyarn/pnpm, replace with the matching package-manager commands
- NestJS start command is usually node dist/main.js---
Python (FastAPI / Flask / Django)
FastAPI / Flask
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Django
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
RUN python manage.py collectstatic --noinput 2>/dev/null || true
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')" || exit 1
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "config.wsgi:application"]Adaptation notes:
- Adjust Django's WSGI module path to the project structure
- If apyproject.tomlexists, usepip install .instead ofrequirements.txt
- Poetry projects: run poetry export -f requirements.txt first, then install---
Go
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache git ca-certificates
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
USER nonroot:nonroot
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD ["/server", "--health-check"]
ENTRYPOINT ["/server"]Adaptation notes:
- Adjust the ./cmd/server path to the actual main package location- Ifmain.gois in the repo root, use.instead
- distroless images have no shell; use alpine instead if you need to debug- distroless does not support the CMD shell form of HEALTHCHECK; use a K8s probe instead
---
Java (Spring Boot / Quarkus)
Spring Boot (Maven)
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY pom.xml mvnw ./
COPY .mvn .mvn
RUN chmod +x mvnw && ./mvnw dependency:go-offline -B
COPY src src
RUN ./mvnw package -DskipTests -B && \
java -Djarmode=layertools -jar target/*.jar extract --destination /extracted
FROM eclipse-temurin:21-jre-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /extracted/dependencies/ ./
COPY --from=builder /extracted/spring-boot-loader/ ./
COPY --from=builder /extracted/snapshot-dependencies/ ./
COPY --from=builder /extracted/application/ ./
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]Spring Boot (Gradle)
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY build.gradle* settings.gradle* gradlew ./
COPY gradle gradle
RUN chmod +x gradlew && ./gradlew dependencies --no-daemon
COPY src src
RUN ./gradlew bootJar --no-daemon -x test
FROM eclipse-temurin:21-jre-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]---
Rust
FROM rust:1.77-alpine AS builder
RUN apk add --no-cache musl-dev
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main(){}" > src/main.rs && cargo build --release && rm -rf src
COPY . .
RUN touch src/main.rs && cargo build --release
FROM alpine:3.19
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /app/target/release/<binary-name> /usr/local/bin/app
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:8080/health || exit 1
CMD ["app"]Adaptation notes: replace<binary-name>with the[[bin]]name or package name inCargo.toml
---
Ruby (Rails)
FROM ruby:3.3-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y build-essential libpq-dev && rm -rf /var/lib/apt/lists/*
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local without 'development test' && bundle install
COPY . .
RUN SECRET_KEY_BASE=placeholder bundle exec rails assets:precompile 2>/dev/null || true
FROM ruby:3.3-slim
RUN apt-get update && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
WORKDIR /app
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY --from=builder /app .
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD ruby -e "require 'net/http'; Net::HTTP.get(URI('http://localhost:3000/health'))" || exit 1
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]---
.dockerignore (common)
Regardless of language, always generate a .dockerignore:
.git
.gitignore
.env
.env.*
node_modules
__pycache__
*.pyc
.pytest_cache
.mypy_cache
target/debug
target/release/deps
target/release/build
*.log
.DS_Store
.vscode
.idea
*.md
!README.md
docker-compose*.yml
Dockerfile
.dockerignore
tests/
test/
spec/
coverage/
.github/---
Gotchas (image build / architecture)
| Symptom | Cause | Fix |
|---|---|---|
Container exits immediately with exec format error on VKE/ECS | image architecture does not match the target node architecture (common when building directly on Apple Silicon/arm64) | Build for the target architecture: docker buildx build --platform linux/amd64 ...; do not trust the local Docker default platform; inspect the pushed image platform before rollout |
| Runs locally but crashes once pushed | a local arm64 image was pushed to amd64 nodes | Rebuild/push with explicit --platform linux/amd64; VKE defaults to linux/amd64 unless node-pool data proves another architecture |
ECS Deployment Details
Detailed reference for the ECS branch of volcengine-deploy. The main SKILL.md owns the flow; this document holds build commands, command-channel patterns, and the systemd unit template.
Pitfall sources — for ECS provisioning quirks (instance type availability, image search, Cloud Assistant agent boot delay, RunInstances password/EIP fields, RunCommand invocation name/result polling/timeout), consult skills/volcengine-cli/references/ecs.md.---
1. Per-language build commands
Run inside the repo root. Output paths are relative.
Go
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w" -o dist/server \
$([ -d cmd ] && echo "./cmd/server" || echo ".")Artifact: dist/server (statically linked, ~10–30 MB typical).
Node.js
npm ci --only=production
[ -f tsconfig.json ] && npx tsc 2>/dev/null || trueArtifact: source dir + node_modules/. Bundle as a tarball before upload:
tar -czf dist/app.tar.gz --exclude='.git' --exclude='node_modules/.cache' .Python
python3 -m venv .venv
.venv/bin/pip install --no-cache-dir -r requirements.txtArtifact: source dir + .venv/. Bundle:
tar -czf dist/app.tar.gz --exclude='.git' --exclude='.venv/lib/python*/site-packages/__pycache__' .Java (Maven)
./mvnw package -DskipTests -BArtifact: target/*-SNAPSHOT.jar (or release JAR per project naming).
Java (Gradle)
./gradlew bootJar --no-daemon -x testArtifact: build/libs/*.jar.
Rust
cargo build --releaseArtifact: target/release/<binary-name> (binary name = package name in Cargo.toml).
Ruby
bundle install --deployment --without development testArtifact: source dir + vendor/bundle/. Bundle as tarball.
---
2. Command channel
New public ECS deployments must allocate an EIP. Ask the user whether to open SSH 22:
- If the user allows SSH and port 22 is reachable, SSH/scp can be used for upload and debugging.
- If the user declines SSH, SSH is blocked, or the connection is slow/unreliable, use Cloud Assistant.
- When creating an instance, install/enable Cloud Assistant so fallback is always available.
Only detect the outbound IP when the user chooses to open SSH 22. Application ports normally stay public, so they do not need a source-IP probe.
Before writing the SSH rule, detect the current outbound IP from the same machine that will run SSH and restrict port 22 to that CIDR when possible. Re-check before deployment if there was a delay; if the outbound IP changed, update the rule instead of troubleshooting a stale whitelist.
Keep SSH retries short. If nc -zw5 "$eip" 22 fails or SSH does not connect promptly, switch to Cloud Assistant instead of waiting.
Use local wrappers for every Cloud Assistant command so examples cannot drift from the ECS CLI rules. InvocationName is required; keep it short, stable, and compliant with the naming rules in skills/volcengine-cli/references/ecs.md. CommandContent must be base64-encoded; RunCommand returns only scheduling metadata, so run_cmd submits the command and then polls the actual result.
submit_cmd() {
local invocation_name="$1"
local timeout_seconds="$2"
local command_content="$3"
local command_b64
command_b64=$(printf '%s' "$command_content" | base64 | tr -d '\n')
ve ecs RunCommand \
--Type "Shell" \
--InstanceIds.1 "$instance_id" \
--InvocationName "$invocation_name" \
--Timeout "$timeout_seconds" \
--CommandContent "$command_b64"
}
wait_cmd_result() {
local invocation_id="$1"
local max_attempts="${2:-60}"
local sleep_seconds="${3:-5}"
local result result_status exit_code
for _ in $(seq 1 "$max_attempts"); do
result=$(ve ecs DescribeInvocationResults \
--InvocationId "$invocation_id" \
--InstanceId "$instance_id")
result_status=$(echo "$result" | jq -r '.Result.InvocationResults[0].InvocationResultStatus // empty')
exit_code=$(echo "$result" | jq -r '.Result.InvocationResults[0].ExitCode // empty')
case "$result_status" in
Success|Failed|Timeout)
echo "$result"
[ "$result_status" = "Success" ] && [ "$exit_code" = "0" ]
return
;;
esac
sleep "$sleep_seconds"
done
echo "Timed out waiting for invocation $invocation_id" >&2
return 124
}
run_cmd() {
local response invocation_id
response=$(submit_cmd "$@")
invocation_id=$(echo "$response" | jq -r '.Result.InvocationId // empty')
[ -n "$invocation_id" ] || { echo "$response"; return 1; }
wait_cmd_result "$invocation_id"
}
cmd_output() {
jq -r '.Result.InvocationResults[0].Output // ""' | base64 -d 2>/dev/null || true
}Cloud Assistant Pattern A — small files (< 1 MB)
Embed base64-encoded content in the command body. Suitable for binaries and config files.
encoded=$(base64 < dist/server | tr -d '\n')
run_cmd "upload-small" 300 "mkdir -p /opt/$repo_name && \
echo '$encoded' | base64 -d > /opt/$repo_name/server && \
chmod +x /opt/$repo_name/server"Note: Volcengine RunCommand's CommandContent has a size limit. For files larger than ~1 MB, use Pattern B, SSH/scp when SSH is allowed, or a user-provided artifact URL.
Cloud Assistant Pattern B — large files via TOS pre-signed URL
ve tos is not available in all Volcengine CLI builds. If tosutil is installed and configured, upload the artifact with tosutil, generate a short-lived pre-signed download URL, then have the instance pull it with bounded HTTP retries. If tosutil is unavailable, use SSH/scp when SSH is open, or ask the user for an existing HTTPS artifact URL.
# 1. Upload to TOS (bucket created or provided before this step)
tosutil cp dist/app.tar.gz "tos://$deploy_bucket/artifacts/$repo_name-$git_sha.tar.gz"
# 2. Pre-signed url (15 minute validity)
url=$(tosutil presign "tos://$deploy_bucket/artifacts/$repo_name-$git_sha.tar.gz" -vp=15min | grep -E '^https://' | head -1)
[ -n "$url" ] || { echo "tosutil presign did not return an https URL"; exit 1; }
# Optional validation from the agent machine. HEAD can return 403 for a valid
# presigned object URL; use GET or Range GET instead.
curl --noproxy '*' -fsS -H 'Range: bytes=0-0' "$url" -o /dev/null
# 3. Pull on the instance
run_cmd "upload-tos" 600 "mkdir -p /opt/$repo_name && \
curl --http1.1 --retry 5 --retry-all-errors --connect-timeout 10 --max-time 180 -L '$url' -o /tmp/app.tar.gz && \
tar -xzf /tmp/app.tar.gz -C /opt/$repo_name && \
rm /tmp/app.tar.gz"Only pass the pre-signed URL into the remote command. Do not print it, put it in the resource ledger, include it in the final summary, or write it to README/log files. Reports and ledgers must record the durable tos://bucket/key object path instead. For cleanup, delete only the deployment prefix, for example tosutil rm tos://bucket/prefix/ -r -f; do not use -y.
Pre-flight: agent readiness
Before any RunCommand, confirm the Cloud Assistant agent is Running:
for _ in $(seq 1 24); do
ca_status=$(ve ecs DescribeCloudAssistantStatus --InstanceIds.1 "$instance_id" \
| jq -r '.Result.Instances[0].Status // empty')
[ "$ca_status" = "Running" ] && break
sleep 5
done
[ "$ca_status" = "Running" ] || { echo "Cloud Assistant not ready; reboot or wait"; exit 1; }If the agent isn't running and the instance was just created without --InstallRunCommandAgent true, a reboot is required. Always pass --InstallRunCommandAgent true at RunInstances time to avoid this.
Instance type retry
Use DescribeAvailableResource to build a short candidate list. If RunInstances reports the chosen type unavailable, sold out, or not found in the zone, try the next candidate automatically. Only stop after the candidate list is exhausted or the error is unrelated to capacity/type availability.
RunInstances credentials and EIP fields
RunInstances requires either --Password or --KeyPairName even when SSH is not opened and Cloud Assistant will be used. For no-SSH deployments, generate a one-time strong password only for instance creation and do not print or persist it.
For inline EIP creation, --EipAddress.ChargeType accepts PayByBandwidth, PayByTraffic, or PrePaid. Do not pass values from other EIP APIs such as PostPaidByBandwidth.
CLI ECS creation skeleton with inline EIP
This skeleton shows the command shape for creating an ECS instance with inline EIP and Cloud Assistant. Query current zone inventory, image availability, quotas, and user requirements before choosing the image, zone, and instance type; do not treat previously validated region/spec/image values as defaults.
name="deploy-$repo_name-$git_sha"
password="<generated-one-time-strong-password>"
# If this run just created VPC/SG resources, wait before creating child resources
# or writing rules. Otherwise short consistency windows can return
# InvalidVpc.InvalidStatus or InvalidSecurityGroup.InvalidStatus.
for _ in $(seq 1 30); do
vpc_status=$(ve vpc DescribeVpcs --VpcIds.1 "$vpc_id" \
| jq -r '.Result.Vpcs[0].Status // empty')
[ "$vpc_status" = "Available" ] && break
sleep 5
done
for _ in $(seq 1 30); do
sg_seen=$(ve vpc DescribeSecurityGroups --SecurityGroupIds.1 "$sg_id" \
| jq -r '.Result.SecurityGroups[0].SecurityGroupId // empty')
[ "$sg_seen" = "$sg_id" ] && break
sleep 5
done
# 1. DryRun validates parameters and creates nothing. A successful DryRun exits non-zero
# and prints DryRunOperation.
ve ecs RunInstances \
--ZoneId "$zone_id" \
--InstanceTypeId "$instance_type_id" \
--ImageId "$image_id" \
--NetworkInterfaces.1.SubnetId "$subnet_id" \
--NetworkInterfaces.1.SecurityGroupIds.1 "$sg_id" \
--SystemVolume.Size 40 \
--SystemVolume.VolumeType ESSD_PL0 \
--InstanceName "$name" \
--HostName "$name" \
--Password "$password" \
--InstallRunCommandAgent true \
--EipAddress.ChargeType PayByBandwidth \
--EipAddress.BandwidthMbps 1 \
--EipAddress.ReleaseWithInstance true \
--Count 1 \
--Tags.1.Key "publish-by" \
--Tags.1.Value "deploy-skill" \
--DryRun true
# 2. Create after DryRun passes.
create_json=$(ve ecs RunInstances \
--ZoneId "$zone_id" \
--InstanceTypeId "$instance_type_id" \
--ImageId "$image_id" \
--NetworkInterfaces.1.SubnetId "$subnet_id" \
--NetworkInterfaces.1.SecurityGroupIds.1 "$sg_id" \
--SystemVolume.Size 40 \
--SystemVolume.VolumeType ESSD_PL0 \
--InstanceName "$name" \
--HostName "$name" \
--Password "$password" \
--InstallRunCommandAgent true \
--EipAddress.ChargeType PayByBandwidth \
--EipAddress.BandwidthMbps 1 \
--EipAddress.ReleaseWithInstance true \
--Count 1 \
--Tags.1.Key "publish-by" \
--Tags.1.Value "deploy-skill")
instance_id=$(printf '%s' "$create_json" | jq -r '.Result.InstanceIds[0]')
# 3. Poll until RUNNING and capture the EIP.
for _ in $(seq 1 60); do
instance_json=$(ve ecs DescribeInstances --InstanceIds.1 "$instance_id")
inst_status=$(printf '%s' "$instance_json" | jq -r '.Result.Instances[0].Status // empty')
eip=$(printf '%s' "$instance_json" | jq -r '.Result.Instances[0].EipAddress.IpAddress // empty')
[ "$inst_status" = "RUNNING" ] && [ -n "$eip" ] && break
sleep 10
done
# 4. Confirm Cloud Assistant is available before using RunCommand.
for _ in $(seq 1 24); do
ca_status=$(ve ecs DescribeCloudAssistantStatus --InstanceIds.1 "$instance_id" \
| jq -r '.Result.Instances[0].Status // empty')
[ "$ca_status" = "Running" ] && break
sleep 5
done
[ "$ca_status" = "Running" ] || { echo "Cloud Assistant not ready"; exit 1; }When ReleaseWithInstance=true, do not treat the inline EIP as an independent mandatory cleanup item. Delete the ECS instance first, then confirm the EIP is gone before deleting the security group, subnet, and VPC.
Public endpoint verification and EIP troubleshooting
Local proxy settings can produce false public-endpoint results. Verify direct public access with:
curl --noproxy '*' -v --connect-timeout 5 --max-time 15 "http://$eip:$port/"When the public URL fails but local health looks good, inspect the path by symptom instead of following a fixed checklist. Useful evidence includes EIP attachment and ENI details, security group ingress for the public port, process listeners (ss -lntp), local and private-IP health checks, a direct public check with curl --noproxy '*', active TCP samples during a public request, and service/gateway/reverse-proxy logs.
If TCP reaches the process but app logs show no HTTP request and clients receive zero bytes until timeout, report it as an unresolved public ingress/EIP path issue rather than an application health failure.
Docker on veLinux and China networks
veLinux 2 is Debian-like, but VERSION_CODENAME may be lyra; do not use that value as the Docker official Debian repository codename. Prefer the system package:
apt-get update
apt-get install -y docker.io
systemctl enable --now dockerDocker Hub and GHCR may be slow or unreachable from China regions. Prefer Volcengine CR or a user-provided registry for deployment images. If a temporary public-registry mirror or domestic sync service is considered, verify it at execution time with the exact image before relying on it; do not keep stale mirror candidates as defaults. If image pulls remain blocked, fall back to a release binary, local artifact, or TOS artifact before abandoning the ECS path.
GitHub and artifact transfer fallback
If remote GitHub clone, archive download, Docker Hub, GHCR, or external package download is flaky from the ECS instance, do not keep retrying indefinitely. Prefer bounded retries first; if the remote path remains unreliable, build or package locally and transfer a release artifact. TOS plus a short-lived tosutil presign URL is one supported artifact-transfer option; SSH/scp or a user-provided artifact URL can be better when they are already available. Prefer artifact transfer over adding broad package-manager dependencies on the target host.
veLinux package and Python caveats
Do not assume python3 -m venv works on veLinux images. ensurepip may be unavailable, and installing python3-venv, media packages such as ffmpeg, or extra apt repositories can hit package conflicts. For validation workloads, prefer release binaries, reachable container images, local build artifacts uploaded through TOS, or a prebuilt standalone runtime. Install Python packages into the system interpreter only when the user accepts the risk.
When editing app config, prefer structured parsers over string replacement. Parse TOML/YAML/JSON and set the exact key instead of replacing a guessed literal.
Long command phases
Split long remote work into separate invocations for clone, install, image pull, build, run, and health check. Write each phase to /var/log/volcengine-deploy/<phase>.log; when cancelling a clone/build/pull phase, stop the process tree so child processes such as git-remote-https or stale docker pull jobs do not continue after the Cloud Assistant command exits. Use bounded timeouts for raw GitHub scripts and public image pulls.
RunCommand result polling
ve ecs RunCommand returns scheduling metadata. Treat it as "command submitted", not "command succeeded". Extract the invocation ID, then poll DescribeInvocationResults and use .Result.InvocationResults[0].InvocationResultStatus plus .ExitCode as documented in skills/volcengine-cli/references/ecs.md. Decode Output from base64 when inspecting command output. Use submit_cmd only when you intentionally want the scheduling response; otherwise use run_cmd, which returns the polled result JSON.
---
3. systemd unit template
Write the unit file through the chosen channel. For Cloud Assistant, encode it and write via RunCommand; for SSH, copy it and run the same systemctl commands.
[Unit]
Description=__REPO_NAME__ service
After=network.target
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/__REPO_NAME__
EnvironmentFile=-/opt/__REPO_NAME__/.env
ExecStart=__EXEC_START__
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
LimitNOFILE=65536
[Install]
WantedBy=multi-user.targetReplace __REPO_NAME__ with the project slug and __EXEC_START__ with the language-appropriate command:
| Language | ExecStart value |
|---|---|
| Go / Rust | /opt/<repo>/server |
| Node.js | /usr/bin/node /opt/<repo>/dist/index.js (or npm start --prefix /opt/<repo>) |
| Python | /opt/<repo>/.venv/bin/python -m <module> (or gunicorn / uvicorn invocation) |
| Java | /usr/bin/java -jar /opt/<repo>/app.jar |
| Ruby | /opt/<repo>/bin/<bootstrap> (Rails: bundle exec rails s -e production) |
Install via Cloud Assistant:
unit_content=$(envsubst < unit-template.service) # after substituting placeholders
encoded_unit=$(echo "$unit_content" | base64 | tr -d '\n')
run_cmd "install-unit" 60 "echo '$encoded_unit' | base64 -d > /etc/systemd/system/$repo_name.service && \
systemctl daemon-reload && \
systemctl enable --now $repo_name.service"User creation (run once per instance, before first deploy):
run_cmd "create-user" 60 "id appuser >/dev/null 2>&1 || useradd --system --no-create-home appuser; \
chown -R appuser:appuser /opt/$repo_name"Environment file injection (run after collecting non-secret and secret values, and before starting the service):
database_url="<resolved-database-url>"
redis_url="<resolved-redis-url>"
env_content=$(printf '%s\n' \
"NODE_ENV=production" \
"PORT=$port" \
"DATABASE_URL=$database_url" \
"REDIS_URL=$redis_url")
encoded_env=$(printf '%s' "$env_content" | base64 | tr -d '\n')
run_cmd "write-env" 60 "install -d -m 0750 /opt/$repo_name && \
echo '$encoded_env' | base64 -d > /opt/$repo_name/.env && \
chmod 0600 /opt/$repo_name/.env && \
chown appuser:appuser /opt/$repo_name/.env"---
4. Health check polling
After systemctl start, first confirm the process is listening on the expected port, then poll the local health endpoint through SSH or Cloud Assistant. Then verify the public EIP endpoint from the agent machine. Prefer a detected health path such as /health or /actuator/health; if none is known, verify TCP/listening state and the root path.
if ! listen_result=$(run_cmd "check-listen" 60 "ss -ltnp | grep -E '(:$port\\b).*LISTEN' && \
(ss -ltnp | grep -E '0\\.0\\.0\\.0:$port\\b|\\*:$port\\b|\\[::\\]:$port\\b' || true)"); then
echo "$listen_result" | cmd_output
echo "Port $port is not listening on $instance_id"
exit 1
fi
if [ -n "${health_path:-}" ]; then
for i in $(seq 1 12); do
result=$(run_cmd "health-check" 60 "curl -sf http://localhost:$port$health_path -o /dev/null && echo OK || echo FAIL")
output=$(echo "$result" | cmd_output)
if printf '%s\n' "$output" | grep -qx "OK"; then
echo "Service healthy on $instance_id"
exit 0
fi
sleep 10
done
echo "Health check timeout on $instance_id"
exit 1
fi
echo "No health_path detected; port listening check passed on $instance_id"Note: RunCommand itself has a 60-second minimum timeout per the volcengine-cli ECS notes. The --Timeout value above must be ≥ 60 even for fast curls.
If the app does not have /health, fall back to checking process state:
systemctl is-active --quiet $repo_name.service && echo OK
ss -ltnp | grep -E ":$port\\b"Public endpoint check:
if [ -n "${health_path:-}" ]; then
curl -fsS "http://$eip:$port$health_path"
else
curl -fsS "http://$eip:$port/"
fiFor final acceptance, do not stop at RunCommand Success, a listening port, or an HTTP 200 home page. Verify one core application behavior when the app exposes one: login, create/read a record, run a database-backed request, or another user-visible operation tied to the deployed service.
Avoid curl ... | head inside set -o pipefail checks; head can close the pipe early and make a healthy response look failed through curl: (23) Failure writing output to destination.
---
5. Multi-instance rolling restart
For N instances tagged project=$repo_name (lookup via ve ecs DescribeInstances --TagKey.1 project --TagValue.1 $repo_name), iterate one at a time. Never stop more than one instance simultaneously unless the user explicitly opts out of rolling.
# Pseudocode flow per instance — execute via your shell loop, not as a single block
for instance_id in $instance_ids; do
echo "==> Updating $instance_id"
# 1. Drain (if behind a load balancer, deregister first; skip if not)
# 2. systemctl stop $repo_name.service
# 3. Upload new artifact (Pattern A or B)
# 4. systemctl start $repo_name.service
# 5. Health check (section 4)
# 6. Re-register with load balancer (if applicable)
doneDrain via CLB: ve clb DeregisterServers to remove the instance from the listener; after health check passes, ve clb RegisterServers to add back.
---
6. Security group inbound rules
The application port must be opened for public access:
ve vpc AuthorizeSecurityGroupIngress \
--SecurityGroupId "$sg_id" \
--PortStart "$port" --PortEnd "$port" \
--Protocol tcp \
--CidrIp "0.0.0.0/0" \
--Policy acceptFor multi-tier deployments, restrict source CIDR to the LB or front-tier security group ID instead of 0.0.0.0/0.
SSH 22 is optional. Ask before opening it. If opened, prefer a trusted CIDR rather than 0.0.0.0/0. If not opened, use Cloud Assistant.
If a source CIDR was derived from outbound IP detection, write the rule immediately after detection and re-check before first SSH use if enough time has passed for NAT egress to drift.
---
7. Resource ledger and cleanup
Every new ECS-side resource must be recorded in .volcengine/created-resources.json immediately after creation: instance, EIP, security group, security group rule, TOS artifact bucket if created, and any managed dependency. Mark reused resources as reused=true and never include them in destructive cleanup.
There is currently no one-command cleanup runner. On failure, print reverse-order cleanup commands from the ledger. The user must review and run the delete_command values manually; do not delete automatically without user confirmation.
For a typical CLI-created single-ECS stack, the dependency direction is usually ECS/ENI -> remaining EIP -> custom security group -> subnet -> VPC. TOS artifacts are independent of that VPC chain and are often cleaned last so failure evidence remains available. Treat this as a dependency guide, not a fixed cleanup script: derive actual commands from the ledger and current resource state.
Deletion APIs can return AsyncTaskId. Poll until IDs disappear or TotalCount=0 before moving to the next dependency; otherwise later deletes can fail with InvalidVpc.InvalidStatus or InvalidOperation.Conflict.
---
8. Failure paths
If health check times out, the deploy skill prints (and does not auto-execute):
# Stop the failing service
run_cmd "stop-service" 60 "systemctl stop $repo_name.service"
# Roll back to previous binary (kept at /opt/<repo>/server.bak by the deploy script before overwrite)
run_cmd "rollback-service" 60 "[ -f /opt/$repo_name/server.bak ] && \
mv /opt/$repo_name/server.bak /opt/$repo_name/server && \
systemctl start $repo_name.service"The user runs these manually after reading the failure summary. Auto-rollback is intentionally avoided — the deploy skill surfaces the issue with full context instead of silently masking it.
---
9. Gotchas (failure modes, symptom-indexed)
Look up by symptom; act on the mapped cause directly rather than diagnosing unrelated layers first.
| Symptom | Likely cause | Action |
|---|---|---|
RunInstances reports instance type unavailable/sold out | type not available in target zone | Query DescribeAvailableResource, pick another available type, retry automatically until the candidate list is exhausted |
RunInstances returns MissingParameter.PasswordAndKeyPair | ECS requires Password or KeyPairName even with SSH closed | Generate a one-time strong --Password (or use an existing --KeyPairName); never print or persist the generated password |
InvalidEipAddressChargeType.Malformed | EIP billing value copied from another EIP API | For RunInstances --EipAddress.ChargeType use only PayByBandwidth, PayByTraffic, or PrePaid (not PostPaidByBandwidth) |
| SSH connect hangs or is blocked | port 22 closed by design or network policy | Use Cloud Assistant; do not wait on long SSH retries |
Cloud Assistant status jq returns empty | wrong response path | Read .Result.Instances[0].Status and wait for Running before RunCommand |
RunCommand looks scheduled but app unchanged | only the scheduling response was checked | Extract invocation ID, poll DescribeInvocationResults, check InvocationResultStatus + ExitCode before continuing |
RunCommand returns Success but app not usable | script exited before real runtime verification | Check unit/container status, listening port (ss -ltnp), logs, and one core app behavior; HTTP 200 alone is not acceptance |
RunCommand returns InvalidParameter.Timeout | timeout too low for the API/CLI | Pass --Timeout 60 minimum (see volcengine-cli/references/ecs.md) |
| Docker Hub/GHCR pull hangs or times out | China-region network / public registry throttling | Prefer CR or user registry; if using a temporary mirror, verify the exact image at execution time; otherwise fall back to binary, local artifact, or TOS artifact |
Domestic mirror hostname returns no basic auth credentials | the site may be a search/sync frontend, not a drop-in registry path | Inspect the service's current instructions and use the exact docker pull command it provides instead of guessing a prefixed image path |
docker login to CR returns 401 | wrong CR username | Re-read Result.Username from GetAuthorizationToken; if absent, inspect the CR API response instead of inventing a username |
| App starts but config-dependent requests fail | .env was not generated from required values | Resolve .env.example/dependency outputs, inject the .env, restart the service |
| App cannot connect to RDS/Redis | private endpoint or allowlist not wired | Use the private endpoint, build DATABASE_URL/REDIS_URL, add the ECS subnet CIDR or security group source to the service allowlist |
PostgreSQL migrations fail on public schema | database owner and schema owner differ | Set the database owner to the app account and use rdspostgresql ModifySchemaOwner for public before migrations |
Shell health check fails with curl: (23) | `curl | head under set -o pipefail` |
| veFaaS setup fails | vefaas CLI/auth/framework issue | Return to the main deploy flow, summarize the failure, let the user retry veFaaS or switch to ECS/VKE |
veFaaS Skill Execution
volcengine-deploy does not duplicate veFaaS deployment details. When the user chooses veFaaS, switch to/call the volcengine-vefaas skill. If that path fails, return to the main deployment flow so the user can fix the issue, retry veFaaS, or choose ECS/VKE.
What to pass
Provide the volcengine-vefaas skill with:
- repo path or Git URL
- app name
- target region
- detected framework and port, if known
- whether env vars or
.env.exampleexist - warnings from prepare, such as migrations, long-running workers, WebSocket usage, or external dependencies
Expected volcengine-vefaas workflow
The volcengine-vefaas skill owns:
vefaas --version
vefaas login --check
vefaas inspect
vefaas deploy --newApp <app-name> --gatewayName $(vefaas run listgateways --first) --yes
vefaas domainsFor apps with environment variables, tell volcengine-vefaas to link/create the app, configure env vars, then deploy.
Failure return path
If volcengine-vefaas fails:
1. summarize the failure in user terms, such as auth failure, no gateway, framework detection failure, build failure, or deploy timeout; 2. show the relevant debug/log command from vefaas if available; 3. return to the main deployment choice and offer:
- retry veFaaS after fixing the issue,
- switch to ECS,
- switch to VKE.
Recommendation constraints
veFaaS remains a visible option even when it is not first in the recommendation order. Explain the reason plainly:
- supported framework and stateless shape: strong veFaaS candidate
- migrations: needs a separate migration step before/around deploy
- long-running workers or WebSocket: ECS/VKE is usually safer
- unknown start command or unsupported framework:
vefaas inspectmust confirm before deployment
Do not fall back to the legacy ve vefaas ZIP/API flow unless the user explicitly asks for low-level API work.
Kubernetes manifest templates
Generated K8s manifests must include the following best practices. By default, place YAML files under .volcengine/k8s/; a temporary clone of a remote Git URL may use /tmp first, but final state and reusable files should land under the repo's .volcengine/.
---
1. Namespace
# k8s/00-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: <repo-name>
labels:
project: <repo-name>
managed-by: volcengine-deploy---
2. ConfigMap & Secret
Generate ConfigMap and Secret only after resolving runtime configuration. Values can come from .env.example, user input, IaC outputs, or CLI-created dependency outputs. Do not apply manifests that still contain placeholder connection strings.
# k8s/01-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: <repo-name>-config
namespace: <repo-name>
data:
# Non-sensitive config
NODE_ENV: "production"
PORT: "<port>"
LOG_LEVEL: "info"
---
apiVersion: v1
kind: Secret
metadata:
name: <repo-name>-secret
namespace: <repo-name>
type: Opaque
stringData:
# Sensitive config (DB connections, etc.); replace with real values at generation time, never leave placeholders
DATABASE_URL: "<resolved-database-url>"
REDIS_URL: "<resolved-redis-url>"Before kubectl apply, grep for unresolved placeholders:
! rg -n "<connection-string>|<redis-connection-string>|<resolved-" .volcengine/k8s---
3. Deployment
# k8s/02-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: <repo-name>
namespace: <repo-name>
labels:
app: <repo-name>
project: <repo-name>
managed-by: volcengine-deploy
spec:
replicas: 2
revisionHistoryLimit: 5
selector:
matchLabels:
app: <repo-name>
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: <repo-name>
spec:
# Anti-affinity: spread across different nodes
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- <repo-name>
topologyKey: kubernetes.io/hostname
# Graceful termination
terminationGracePeriodSeconds: 30
# VKE nodes are usually linux/amd64; the built/pushed image must match the node architecture
nodeSelector:
kubernetes.io/arch: amd64
# Password-free CR pulls rely on the VKE addon cr-credential-controller; do not put CR passwords in app Secrets
imagePullSecrets:
- name: volcengine-cr-credential
containers:
- name: <repo-name>
image: <cr-endpoint>/<namespace>/<repo-name>:<tag>
ports:
- containerPort: <port>
name: http
protocol: TCP
# Environment variables
envFrom:
- configMapRef:
name: <repo-name>-config
- secretRef:
name: <repo-name>-secret
# Resource limits
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: "1"
memory: 512Mi
# Liveness probe: is the container still alive; only use httpGet once the app is confirmed to expose a health path
livenessProbe:
httpGet:
path: <health-path>
port: http
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 3
failureThreshold: 3
# Readiness probe: can it receive traffic; if there is no HTTP health path, use the tcpSocket template below
readinessProbe:
httpGet:
path: <health-path>
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
# Startup probe: slow-starting apps (Java, etc.)
startupProbe:
httpGet:
path: <health-path>
port: http
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 30
# Security context
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: false
allowPrivilegeEscalation: falseAdaptation notes:
- Do not assume the app has a/healthendpoint by default; determinehealth_pathfrom code, the Dockerfile HEALTHCHECK, framework defaults, or user input first
- If the app has no HTTP health path, use atcpSocketprobe instead ofhttpGet
- Java/Spring Boot apps may need a larger startupProbe.failureThreshold (slow startup)- Adjust resource limits to the app type (Java usually needs more memory)
TCP probe fallback:
livenessProbe:
tcpSocket:
port: http
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
tcpSocket:
port: http
initialDelaySeconds: 5
periodSeconds: 10
startupProbe:
tcpSocket:
port: http
periodSeconds: 5
failureThreshold: 30---
4. Service
# k8s/03-service.yaml
apiVersion: v1
kind: Service
metadata:
name: <repo-name>
namespace: <repo-name>
labels:
app: <repo-name>
annotations:
# Volcengine CLB annotations
service.beta.kubernetes.io/volcengine-loadbalancer-subnet-id: "<subnet-id>"
service.beta.kubernetes.io/volcengine-loadbalancer-address-type: "PUBLIC"
spec:
type: LoadBalancer
selector:
app: <repo-name>
ports:
- name: http
port: 80
targetPort: http
protocol: TCPNotes:
- The LoadBalancer type automatically creates a Volcengine CLB- The subnet-id annotation is required for the CLB to be created correctly
- If public access is not needed, switch to the ClusterIP type---
5. HPA (Horizontal Pod Autoscaler)
# k8s/04-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: <repo-name>
namespace: <repo-name>
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: <repo-name>
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Pods
value: 2
periodSeconds: 60---
6. PDB (Pod Disruption Budget)
# k8s/05-pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: <repo-name>
namespace: <repo-name>
spec:
minAvailable: 1
selector:
matchLabels:
app: <repo-name>---
7. NetworkPolicy (optional, recommended)
# k8s/06-network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: <repo-name>-policy
namespace: <repo-name>
spec:
podSelector:
matchLabels:
app: <repo-name>
policyTypes:
- Ingress
- Egress
ingress:
# Allow inbound traffic only from the CLB
- ports:
- port: <port>
protocol: TCP
egress:
# Allow DNS resolution
- to: []
ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
# Allow access to internal services such as databases
- to:
- ipBlock:
cidr: 172.16.0.0/16---
File organization
Generated K8s manifests are numbered to ensure they apply in order:
k8s/
├── 00-namespace.yaml
├── 01-config.yaml
├── 02-deployment.yaml
├── 03-service.yaml
├── 04-hpa.yaml
├── 05-pdb.yaml
└── 06-network-policy.yaml # optionalOne-shot deploy:
kubectl apply -f .volcengine/k8s/---
Gotchas (common VKE deployment failure modes)
Look up by symptom; act on the mapped cause directly rather than suspecting unrelated layers first.
| Symptom | Cause | Fix |
|---|---|---|
CreateKubeconfig returns OperationDenied | cluster not yet Running | Poll ListClusters until Status.Phase=Running before fetching the kubeconfig |
Service LoadBalancer stuck at <pending> | missing CLB subnet annotation | Add service.beta.kubernetes.io/volcengine-loadbalancer-subnet-id to the Service |
BLB no available backend / no available backend nodes | Pod readinessProbe failing, often because the probed /health path does not exist | Use the actual detected health path, or switch the probe to tcpSocket; diagnose with kubectl describe pod + kubectl logs |
Container exits immediately with exec format error | image architecture does not match the VKE node architecture | Rebuild/pull/push with the node architecture (usually linux/amd64) and inspect the image platform before rollout (see `dockerfile-templates.md`) |
| App is up but all config-dependent requests fail | ConfigMap/Secret was not generated from real values and left placeholders | Resolve .env.example/dependency outputs, regenerate the Secret, then rollout again; never apply placeholders into a Secret |
Private CR image pull fails with no basic auth / ImagePullBackOff | cr-credential-controller not installed, or wrong registry credentials hardcoded in the manifest | Prefer cr-credential-controller for private CR pulls instead of putting registry passwords in the app manifest |
Supported runtime dependencies
Wiring loop
A managed dependency is not done when it reports "created". Before deploying the app, complete this loop:
1. Get the private endpoint and ensure the dependency and ECS/VKE are in the same VPC when the selected product uses private VPC connectivity. 2. Create the app database/account or app Redis account, and generate or collect the password. 3. Add the ECS/VKE subnet CIDR, security group source, or node source to the dependency allowlist. 4. Assemble runtime variables such as DATABASE_URL, engine-specific database connection strings, and REDIS_URL, then hand them to the env/Secret injection stage of volcengine-deploy. 5. If migration_paths is non-empty, run migrations first, then do the final health check.
Do not use a public endpoint as the default wiring method; only do so when the user explicitly asks for public exposure and accepts the security group / allowlist risk.
Database Product Choice
Represent database selection with:
{
"database_product": "rds",
"database_engine": "mysql"
}Valid combinations:
| Product | Engines | Execution path |
|---|---|---|
rds | mysql, postgresql, sqlserver | Use the matching RDS CLI service: rdsmysql, rdspostgresql, or rdsmssql |
aidap | supabase, postgresql | Call volcengine-db-supabase for AIDAP workspace provisioning |
AIDAP here refers to Volcengine's AI 原生 BaaS 平台 Supabase 版 product; for deployment, treat its database workspace surface as the managed database provider. Keep deploy selection at the stable engine level (supabase or postgresql) and let volcengine-db-supabase resolve current CreateWorkspace EngineType / EngineVersion enums. Do not model AIDAP Supabase as an RDS PostgreSQL provider variant.
MySQL
Detection keywords (config files / code / dependencies):
mysql,mysql2,mysqlclient,pymysql,MYSQL_HOST,3306,mysql://,jdbc:mysqlimage: mysqlindocker-compose.yml
Volcengine product/engine: database_product=rds, database_engine=mysql
Volcengine service: RDS MySQL (ve rdsmysql)
Creation parameters:
# RDS swagger discovery can return 404; use CLI help for the body schema.
ve rdsmysql CreateDBInstance --helpRecommended spec (entry level):
- Instance type:
HA(high availability) - Spec:
rds.mysql.2c4g(2 vCPU / 4 GB) - Storage: 20GB ESSD PL0
- Version: MySQL 8.0
Notes:
- Must be in the same VPC as VKE
- After creation, create the database and account
- The allowlist must include the VKE subnet CIDR
- Assemble the private endpoint, database name, account, and password into
DATABASE_URL; do not write credentials to logs
---
PostgreSQL
Detection keywords:
pg,postgres,postgresql,psycopg2,pg-promise,POSTGRES_HOST,5432,postgres://,jdbc:postgresqlimage: postgresindocker-compose.yml
Volcengine product/engine options:
database_product=rds,database_engine=postgresql: RDS PostgreSQL (ve rdspostgresql) for managed RDS PostgreSQL.database_product=aidap,database_engine=postgresql: AIDAP PostgreSQL engine workspace viavolcengine-db-supabase.database_product=aidap,database_engine=supabase: AIDAP Supabase engine workspace viavolcengine-db-supabase.
When the user explicitly chooses an AIDAP engine from the console choices, preserve that choice. If they only say "PostgreSQL", ask whether they want RDS PostgreSQL, AIDAP PostgreSQL, or AIDAP Supabase unless the surrounding deployment context already makes one product clear or the user has delegated the choice. If the user says "you decide" and there is no explicit RDS/AIDAP PostgreSQL/Supabase signal, use AIDAP Supabase.
Recommended spec:
- Instance type:
HA - Spec:
rds.postgres.2c4g - Storage: 20GB ESSD PL0
- Version: PostgreSQL 15
Wiring:
- For HA instances,
NodeInfoat creation must include bothPrimaryandSecondary - Use
Inherit,Loginfor the app account privileges, notReadWrite - When creating the database, prefer setting
Ownerto the app account; if migrations still lackpublicschema privileges, runModifySchemaOwneronpublic - Omit
CharacterSetNameonCreateDatabaseat first; do not pass an unverified uppercaseUTF8 - After the instance reaches
Running, account/database/schema operations may still hit a brief exclusive status; wait and retry - After creating the database and app account, assemble
DATABASE_URLfrom the private endpoint - Add the ECS/VKE subnet CIDR or security group source to the allowlist
- When a migration directory exists, run migrations first, then ramp the health check
AIDAP database workspace
Use volcengine-db-supabase when database_product=aidap. Do not duplicate AIDAP workspace provisioning in volcengine-deploy; read ../../volcengine-db-supabase/references/deploy-provider.md, then return with DATABASE_URL and any engine-specific AIDAP/Supabase values such as SUPABASE_URL, SUPABASE_ANON_KEY, and server-only SUPABASE_SERVICE_ROLE_KEY wired into the deploy env/Secret path.
SQL Server
Detection keywords:
mssql,sqlserver,tedious,pyodbc,SQLSERVER_HOST,MSSQL_HOST,1433,sqlserver://,jdbc:sqlserverimage: mcr.microsoft.com/mssql/serverorimage: *sqlserver*indocker-compose.yml
Volcengine product/engine: database_product=rds, database_engine=sqlserver
Volcengine service: RDS SQL Server (ve rdsmssql)
Recommended spec:
- Instance type: Basic or HA according to workload and current regional availability.
- Version/spec must be selected from current
ve rdsmssqlhelp or describe APIs; do not invent defaults.
Wiring:
- Use the private endpoint and app login credentials to assemble the SQL Server connection string.
- Add the ECS/VKE subnet CIDR or security group source to the SQL Server allowlist.
- Treat creation and deletion as slower than stateless resources; poll instance state before database/account follow-up operations.
---
Redis
Detection keywords:
redis,ioredis,redis-py,REDIS_HOST,REDIS_URL,6379,redis://image: redisindocker-compose.yml
Volcengine service: Redis (ve redis)
Recommended spec:
- Type: primary-replica
- Spec: 1GB memory
- Version: Redis 6.0
Creation example:
ve redis CreateDBInstance --body '{
"InstanceName": "deploy-<repo>-redis",
"RegionId": "<region>",
"ConfigureNodes": [{"AZ": "<zone-id>"}],
"ShardedCluster": 0,
"NodeNumber": 2,
"ShardCapacity": 1024,
"ShardNumber": 1,
"EngineVersion": "6.0",
"SubnetId": "<subnet-id>",
"VpcId": "<vpc-id>",
"Password": "<auto-generated>",
"Tags": [
{"Key": "project", "Value": "<repo>"},
{"Key": "publish-by", "Value": "deploy-skill"}
]
}'Wiring:
- Assemble
REDIS_URLfrom the private endpoint and app account/password - Add the ECS/VKE subnet CIDR or security group source to the allowlist
- Run a connectivity check from the app runtime environment first, then a public health check
---
MongoDB
Detection keywords:
mongodb,mongoose,pymongo,mongoclient,MONGO_URI,MONGODB_URL,27017,mongodb://image: mongoindocker-compose.yml
Volcengine service: MongoDB (ve mongodb)
Recommended spec:
- Type: replica set
- Spec:
mongo.2c4g(2 vCPU / 4 GB) - Storage: 20GB
- Version: MongoDB 5.0
---
Kafka
Detection keywords:
kafka,kafkajs,kafka-python,confluent-kafka,KAFKA_BROKERS,KAFKA_BOOTSTRAP_SERVERS,9092image: *kafka*indocker-compose.yml
Volcengine service: Kafka (ve kafka)
Recommended spec:
- Version: 2.8
- Spec:
kafka.20xrate.hw(entry level) - Storage: 100GB
- Partitions: per topic configuration
---
RabbitMQ
Detection keywords:
rabbitmq,amqplib,amqp,pika,RABBITMQ_HOST,AMQP_URL,5672,amqp://image: rabbitmqindocker-compose.yml
Volcengine service: no managed service
Deployment: deploy the official Docker image in the VKE cluster
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: rabbitmq
spec:
serviceName: rabbitmq
replicas: 1
selector:
matchLabels:
app: rabbitmq
template:
metadata:
labels:
app: rabbitmq
spec:
containers:
- name: rabbitmq
image: rabbitmq:3.12-management-alpine
ports:
- containerPort: 5672
name: amqp
- containerPort: 15672
name: management
env:
- name: RABBITMQ_DEFAULT_USER
valueFrom:
secretKeyRef:
name: rabbitmq-secret
key: username
- name: RABBITMQ_DEFAULT_PASS
valueFrom:
secretKeyRef:
name: rabbitmq-secret
key: password
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
volumeMounts:
- name: data
mountPath: /var/lib/rabbitmq
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
name: rabbitmq
spec:
selector:
app: rabbitmq
ports:
- port: 5672
name: amqp
- port: 15672
name: management---
TOS (object storage)
Detection keywords:
@volcengine/tos,tos-sdk,TOS_ENDPOINT,TOS_BUCKET,tos.volces.com- code references an S3-compatible API with the endpoint pointing at Volcengine
Volcengine service: TOS (tosutil / Terraform IaC)
The current ve CLI build may not have a tos service; do not generate ve tos commands. When object transfer is needed, prefer the standalone volcengine-tosutil skill or create the bucket with Terraform/IaC. In volcengine-deploy, tosutil can only be an optional capability, and you must keep SSH/scp or a user-provided artifact URL as a fallback.
tosutil mb tos://deploy-<repo>-assets -acl=private -sc=STANDARD
tosutil cp ./dist/app.tar.gz tos://deploy-<repo>-assets/artifacts/app.tar.gz
tosutil presign tos://deploy-<repo>-assets/artifacts/app.tar.gz -vp=15min---
Generic dependency handling (not in the list above)
For other service dependencies (e.g. Elasticsearch, MinIO, Memcached), use this strategy:
1. Deploy the official Docker image as a StatefulSet in the VKE cluster 2. Persist data with a PVC 3. Expose it to the app via a ClusterIP Service
# Generic template
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: <dependency-name>
spec:
serviceName: <dependency-name>
replicas: 1
selector:
matchLabels:
app: <dependency-name>
template:
metadata:
labels:
app: <dependency-name>
spec:
containers:
- name: <dependency-name>
image: <official-docker-image>
ports:
- containerPort: <port>
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
volumeMounts:
- name: data
mountPath: <data-path>
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10GiVKE Deployment Details
Execution runbook for the VKE branch. The main SKILL.md keeps only the control-flow skeleton and hard boundaries; this file carries the command-level pipeline. Template details live in `k8s-manifests.md` and `dockerfile-templates.md`; deeper CLI detail lives in volcengine-cli/references/vke.md and volcengine-cli/references/cr.md; a real end-to-end run is in volcengine-iac/references/volcengine-vke-cr-nginx.md.
Do not skip steps or reorder them. The most common failures come from running a later step before an earlier one converges (e.g. kubectl apply before the kubeconfig exists, or generating a Deployment before the image is pushed).
Execution pipeline
1. Provision or reuse VKE + CR
infra_management=iac: usevolcengine-iacoutputs in.volcengine/iac-outputs.jsonfor VPC/subnets/security group,cluster_id, CR registry/namespace/repository, and (when present)kubeconfig_private.- CLI fast path: create or reuse VPC/subnet/security group, the VKE cluster + node pool, and a CR registry/namespace/repository, recording every CLI-created resource in
.volcengine/created-resources.jsonimmediately. Reuse a user-specified cluster/registry when given.
Do not hardcode node instance types. Build the image for the node architecture (default linux/amd64 unless node-pool data proves otherwise).
2. Wait for the cluster, then fetch kubeconfig
Poll the cluster until it is running before any kubeconfig or workload call:
for _ in $(seq 1 60); do
phase=$(ve vke ListClusters --body '{"Filter":{"Ids":["'"$cluster_id"'"]}}' \
| jq -r '.Result.Items[0].Status.Phase // empty')
[ "$phase" = "Running" ] && break
sleep 15
done
[ "$phase" = "Running" ] || { echo "cluster not Running: $phase" >&2; exit 1; }Then read kubeconfig from .volcengine/iac-outputs.json when present, otherwise create one:
ve vke CreateKubeconfig --ClusterId "$cluster_id" --Type Public
# Decode the returned Kubeconfig (base64) to a file and export KUBECONFIG.CreateKubeconfig before the cluster is Running returns OperationDenied — that is why the poll above must succeed first. Use --Type Private only when the agent runs inside the VPC.
3. Verify addons
ve vke ListAddons --body '{"Filter":{"ClusterIds":["'"$cluster_id"'"]}}' \
| jq -r '.Result.Items[].Name'core-dnsmust be present, or in-cluster service-name resolution fails. Install/repair it before relying on cluster DNS.- Prefer
cr-credential-controllerfor private CR image pulls. If it is absent, either install it or create animagePullSecretfrom the CR token instead of hardcoding registry passwords into app manifests.
4. Build the image for the node architecture
Build with an explicit platform matching the nodes; do not trust the local Docker default on arm64 machines. See `dockerfile-templates.md` for templates and the exec format error gotcha.
docker buildx build --platform linux/amd64 -t "$image_ref" --load .5. Authenticate to CR, push, and inspect the platform
token_json=$(ve cr GetAuthorizationToken --Registry "$registry_name")
cr_username=$(printf '%s' "$token_json" | jq -r '.Result.Username // empty')
cr_password=$(printf '%s' "$token_json" | jq -r '.Result.Token // empty')
[ -n "$cr_username" ] || { echo "CR token response missing Result.Username" >&2; exit 1; }
printf '%s' "$cr_password" | docker login "$registry_endpoint" --username "$cr_username" --password-stdin
docker push "$image_ref"
docker manifest inspect "$image_ref" | jq -r '.. | .architecture? // empty' | sort -uIf docker login returns 401, re-read Result.Username; never invent a fallback username. The token is temporary — re-run GetAuthorizationToken if push/pull starts failing after a long session. See volcengine-cli/references/cr.md.
6. Resolve env / Secret and dependency outputs
Resolve real values from .env.example, IaC outputs, or CLI-created dependency outputs before generating manifests. Never apply a Secret manifest that still contains placeholder connection strings. For managed dependency wiring (private endpoints, allowlists, DATABASE_URL/REDIS_URL), see `supported-dependencies.md`.
7. Generate manifests
Generate the Namespace/ConfigMap/Secret/Deployment/Service (and optional HPA/PDB/NetworkPolicy) from resolved values, with probes matched to the app and the CLB subnet annotation filled from outputs (not a placeholder). See `k8s-manifests.md`.
8. Run migrations as a Job (when needed)
When migration_paths is non-empty, run migrations as a Kubernetes Job and wait for completion before or alongside rollout, per the app's migration semantics. Do not bake migrations into the app container start in a way that races multiple replicas.
9. Apply and wait for rollout + LoadBalancer
kubectl apply -f .volcengine/k8s/
kubectl -n "$ns" rollout status deploy/"$app" --timeout=300s
# Wait for the CLB/EIP to be assigned:
for _ in $(seq 1 60); do
lb=$(kubectl -n "$ns" get svc "$app" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null)
[ -n "$lb" ] && break
sleep 10
doneIf the Service stays <pending>, the CLB subnet annotation is missing. If rollout stalls with BLB no available backend, the readinessProbe is failing — see the gotchas in `k8s-manifests.md`.
10. Verify the public endpoint and one core behavior
Verify http://<lb>:<port><path> from outside the cluster. HTTP 200 alone is not acceptance — check one core app behavior and kubectl logs where possible before reporting success.
#!/usr/bin/env bash
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: MIT
# gen-docker-compose-test.sh — Read a lightweight analysis/context JSON and
# emit a docker-compose.test.yml that includes the application image plus
# managed dependency containers (mysql / postgresql / redis / mongodb /
# kafka / rabbitmq / elasticsearch / memcached / clickhouse).
#
# The generated compose file lets the operator smoke-test the freshly built
# image locally before pushing to CR.
#
# Usage:
# gen-docker-compose-test.sh <analysis-or-context.json> <image-name:tag> [app-port]
#
# Output: full YAML to stdout.
set -uo pipefail
report="${1:-}"
image="${2:-}"
app_port="${3:-}"
if [ -z "$report" ] || [ -z "$image" ]; then
echo "Usage: $0 <analysis-or-context.json> <image-name:tag> [app-port]" >&2
exit 2
fi
if [ ! -f "$report" ]; then
echo "Error: report file not found: $report" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq is required" >&2
exit 1
fi
# Derive port from context if not passed explicitly. Support the current
# lightweight deploy-choice shape and the older nested analysis shape.
[ -z "$app_port" ] && app_port=$(jq -r '.port // .repo_analysis.port // "8080"' "$report")
deps=$(jq -r '(.dependencies // .repo_analysis.dependencies // [])[]?' "$report")
# Collect env-var references the app should receive for each dep
app_envs=()
dep_blocks=""
emit_dep() {
local name="$1"
shift
dep_blocks="${dep_blocks}$(printf '\n %s:\n%s' "$name" "$*")"
}
for dep in $deps; do
case "$dep" in
mysql)
emit_dep "mysql" " image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: testpw
MYSQL_DATABASE: appdb
ports: [\"3306:3306\"]
healthcheck:
test: [\"CMD\", \"mysqladmin\", \"ping\", \"-h\", \"localhost\"]
interval: 5s
retries: 10"
app_envs+=("DB_HOST=mysql" "DB_PORT=3306" "DB_USER=root" "DB_PASSWORD=testpw" "DB_NAME=appdb")
;;
postgresql)
emit_dep "postgres" " image: postgres:16
environment:
POSTGRES_PASSWORD: testpw
POSTGRES_DB: appdb
ports: [\"5432:5432\"]
healthcheck:
test: [\"CMD\", \"pg_isready\", \"-U\", \"postgres\"]
interval: 5s
retries: 10"
app_envs+=("DB_HOST=postgres" "DB_PORT=5432" "DB_USER=postgres" "DB_PASSWORD=testpw" "DB_NAME=appdb")
;;
redis)
emit_dep "redis" " image: redis:7-alpine
ports: [\"6379:6379\"]
healthcheck:
test: [\"CMD\", \"redis-cli\", \"ping\"]
interval: 5s
retries: 10"
app_envs+=("REDIS_HOST=redis" "REDIS_PORT=6379")
;;
mongodb)
emit_dep "mongo" " image: mongo:7
ports: [\"27017:27017\"]
healthcheck:
test: [\"CMD\", \"mongosh\", \"--eval\", \"db.adminCommand('ping')\"]
interval: 5s
retries: 10"
app_envs+=("MONGO_URL=mongodb://mongo:27017/appdb")
;;
kafka)
emit_dep "kafka" " image: bitnami/kafka:3.7
environment:
KAFKA_CFG_NODE_ID: 1
KAFKA_CFG_PROCESS_ROLES: controller,broker
KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
ports: [\"9092:9092\"]"
app_envs+=("KAFKA_BOOTSTRAP=kafka:9092")
;;
rabbitmq)
emit_dep "rabbitmq" " image: rabbitmq:3-management
ports: [\"5672:5672\", \"15672:15672\"]
healthcheck:
test: [\"CMD\", \"rabbitmq-diagnostics\", \"ping\"]
interval: 10s
retries: 10"
app_envs+=("RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672/")
;;
elasticsearch)
emit_dep "elasticsearch" " image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
environment:
discovery.type: single-node
xpack.security.enabled: \"false\"
ES_JAVA_OPTS: -Xms512m -Xmx512m
ports: [\"9200:9200\"]"
app_envs+=("ES_HOSTS=http://elasticsearch:9200")
;;
memcached)
emit_dep "memcached" " image: memcached:1.6-alpine
ports: [\"11211:11211\"]"
app_envs+=("MEMCACHED_SERVERS=memcached:11211")
;;
clickhouse)
emit_dep "clickhouse" " image: clickhouse/clickhouse-server:latest
ports: [\"8123:8123\", \"9000:9000\"]
ulimits:
nofile: 262144"
app_envs+=("CLICKHOUSE_HOST=clickhouse" "CLICKHOUSE_PORT=8123")
;;
tos|s3-compatible)
# No local equivalent shipped — TOS is remote; surface a note instead
emit_dep "minio" " image: minio/minio:latest
command: server /data
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: minio12345
ports: [\"9000:9000\", \"9001:9001\"]"
app_envs+=("S3_ENDPOINT=http://minio:9000" "S3_ACCESS_KEY=minio" "S3_SECRET_KEY=minio12345")
;;
*)
;;
esac
done
# Build the app's environment block as YAML list entries (- KEY=VALUE)
app_env_lines=""
for kv in ${app_envs[@]+"${app_envs[@]}"}; do
app_env_lines="${app_env_lines} - ${kv}"$'\n'
done
cat <<EOF
# Generated by gen-docker-compose-test.sh — local smoke test for $image
services:
app:
image: $image
ports: ["${app_port}:${app_port}"]
environment:
- PORT=${app_port}
EOF
# Inline app env vars
if [ -n "$app_env_lines" ]; then
printf '%s' "$app_env_lines"
fi
# Depends-on block (only if there are deps)
dep_names=$(printf '%s\n' $deps | sort -u | sed 's/tos\|s3-compatible/minio/' | sed 's/postgresql/postgres/' | sed 's/mongodb/mongo/' | tr '\n' ' ')
if [ -n "$dep_names" ] && [ "$dep_names" != " " ]; then
echo " depends_on:"
for d in $dep_names; do
echo " $d:"
echo " condition: service_started"
done
fi
# Append dependency service blocks
[ -n "$dep_blocks" ] && printf '%s\n' "$dep_blocks"
#!/usr/bin/env bash
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: MIT
# gen-systemd-unit.sh — Generate a systemd unit file for a deployed binary.
# Output goes to stdout; redirect to /etc/systemd/system/<name>.service.
#
# Usage:
# gen-systemd-unit.sh --name <service-name> --exec <ExecStart-cmd> \
# [--user appuser] [--workdir /opt/<name>] [--env-file PATH]
# [--description "..."]
set -uo pipefail
name=""
exec=""
user_name="appuser"
work_dir=""
env_file=""
desc=""
while [ $# -gt 0 ]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--exec) exec="$2"; shift 2 ;;
--user) user_name="$2"; shift 2 ;;
--workdir) work_dir="$2"; shift 2 ;;
--env-file) env_file="$2"; shift 2 ;;
--description) desc="$2"; shift 2 ;;
*)
echo "Unknown arg: $1" >&2
exit 2
;;
esac
done
if [ -z "$name" ] || [ -z "$exec" ]; then
echo "Error: --name and --exec are required" >&2
exit 2
fi
[ -z "$work_dir" ] && work_dir="/opt/$name"
[ -z "$desc" ] && desc="$name service (managed by volcengine-deploy)"
cat <<EOF
[Unit]
Description=$desc
After=network.target
[Service]
Type=simple
User=$user_name
Group=$user_name
WorkingDirectory=$work_dir
EOF
if [ -n "$env_file" ]; then
echo "EnvironmentFile=-$env_file"
fi
cat <<EOF
ExecStart=$exec
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
#!/usr/bin/env bash
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: MIT
# poll-status.sh — Run a probe command on an interval until its stdout
# matches a success pattern, or until max attempts is reached.
#
# Usage:
# poll-status.sh --cmd "<command>" --pattern "<regex>" \
# [--interval 10] [--max-attempts 30] [--quiet]
#
# Exit:
# 0 — pattern matched within budget
# 1 — exhausted attempts without a match
# 2 — invalid arguments
set -uo pipefail
cmd=""
pattern=""
interval=10
max_attempts=30
quiet=false
while [ $# -gt 0 ]; do
case "$1" in
--cmd) cmd="$2"; shift 2 ;;
--pattern) pattern="$2"; shift 2 ;;
--interval) interval="$2"; shift 2 ;;
--max-attempts) max_attempts="$2"; shift 2 ;;
--quiet) quiet=true; shift ;;
*)
echo "Unknown arg: $1" >&2
echo "Usage: $0 --cmd <command> --pattern <regex> [--interval N] [--max-attempts N] [--quiet]" >&2
exit 2
;;
esac
done
if [ -z "$cmd" ] || [ -z "$pattern" ]; then
echo "Error: --cmd and --pattern are required" >&2
exit 2
fi
attempt=0
while [ "$attempt" -lt "$max_attempts" ]; do
attempt=$((attempt + 1))
out=$(eval "$cmd" 2>&1 || true)
if echo "$out" | grep -qE "$pattern"; then
[ "$quiet" = false ] && echo "[poll-status] match on attempt $attempt"
exit 0
fi
[ "$quiet" = false ] && echo "[poll-status] attempt $attempt/$max_attempts — no match, sleeping ${interval}s"
sleep "$interval"
done
[ "$quiet" = false ] && echo "[poll-status] exhausted $max_attempts attempts" >&2
exit 1