Skaffold’s custom deploy plugins let you escape the confines of kubectl and Helm, giving you granular control over your application deployments.
Let’s see this in action. Imagine you have a simple Go web server and you want to deploy it to Kubernetes using a custom manifest generation tool.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from my custom deployed app!")
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
We’ll also create a simple Go program that acts as our custom deployer. This program will take a Go struct representing our deployment configuration and generate a Kubernetes Deployment and Service YAML.
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"text/template"
)
type DeployConfig struct {
ImageTag string `json:"imageTag"`
Replicas int32 `json:"replicas"`
Port int32 `json:"port"`
}
const deploymentTemplate = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-custom-app
spec:
replicas: {{.Replicas}}
selector:
matchLabels:
app: my-custom-app
template:
metadata:
labels:
app: my-custom-app
spec:
containers:
- name: my-custom-app
image: my-docker-registry/my-custom-app:{{.ImageTag}}
ports:
- containerPort: {{.Port}}
`
const serviceTemplate = `
apiVersion: v1
kind: Service
metadata:
name: my-custom-app-service
spec:
selector:
app: my-custom-app
ports:
- protocol: TCP
port: 80
targetPort: {{.Port}}
type: LoadBalancer
`
func main() {
if len(os.Args) < 2 {
log.Fatalf("Usage: %s <deploy-config.json>", os.Args[0])
}
configFilePath := os.Args[1]
configData, err := ioutil.ReadFile(configFilePath)
if err != nil {
log.Fatalf("Failed to read config file %s: %v", configFilePath, err)
}
var config DeployConfig
err = json.Unmarshal(configData, &config)
if err != nil {
log.Fatalf("Failed to unmarshal deploy config: %v", err)
}
// Generate Deployment YAML
deployTmpl, err := template.New("deployment").Parse(deploymentTemplate)
if err != nil {
log.Fatalf("Failed to parse deployment template: %v", err)
}
err = deployTmpl.Execute(os.Stdout, config)
if err != nil {
log.Fatalf("Failed to execute deployment template: %v", err)
}
// Generate Service YAML
serviceTmpl, err := template.New("service").Parse(serviceTemplate)
if err != nil {
log.Fatalf("Failed to parse service template: %v", err)
}
err = serviceTmpl.Execute(os.Stdout, config)
if err != nil {
log.Fatalf("Failed to execute service template: %v", err)
}
}
First, you’ll need to build your application and push it to a container registry. For this example, let’s assume you’ve built an image named my-docker-registry/my-custom-app and tagged it latest.
Next, you’ll create a skaffold.yaml file to orchestrate this. The key here is the deploy section, specifically using custom and pointing to your deployer executable.
apiVersion: skaffold/v2beta28
kind: Config
metadata:
name: skaffold-custom-deploy-example
build:
artifacts:
my-app:
docker:
dockerfile: Dockerfile
local: {}
deploy:
custom:
- deploy:
command: "go run ./deployer/main.go" # Path to your deployer executable
flags:
- "--config=deployer/config.json" # Path to your deploy configuration file
And here’s our deployer/config.json:
{
"imageTag": "latest",
"replicas": 2,
"port": 8080
}
Now, when you run skaffold dev, Skaffold will:
- Build your application image.
- Pass the image tag (obtained from the build step) to your custom deployer.
- Execute your deployer command, which in turn reads the
config.json, generates the Kubernetes manifests, and prints them to standard output. - Skaffold then pipes these generated manifests to
kubectl apply.
The problem this solves is that kubectl and Helm, while powerful, can be rigid. kubectl requires you to manage raw YAML, and Helm has its own templating language and structure. Custom deploy plugins allow you to use any templating engine or logic you prefer – be it Go’s text/template, Jinja2 in Python, or even a custom DSL – and integrate it seamlessly into Skaffold’s build-deploy loop. This gives you maximum flexibility for complex deployment strategies, dynamic configuration based on build artifacts, or integration with proprietary deployment systems.
The mental model to build is that Skaffold’s deploy.custom section essentially acts as a black box. You provide an executable and arguments, and Skaffold expects that executable to print valid Kubernetes manifests to stdout. Skaffold doesn’t care how those manifests are generated, only that they are valid and arrive on time. This decoupling is the core of its power. You can swap out your entire deployment logic without changing Skaffold’s core configuration, as long as your new logic adheres to the "print manifests to stdout" contract.
When Skaffold executes a custom deploy, it captures the standard output of your deploy command. This output is then treated as a set of Kubernetes manifests that Skaffold will apply to your cluster using kubectl apply. If your deploy command outputs multiple YAML documents (separated by ---), Skaffold will apply them all as distinct resources. This is how our simple Go deployer can generate both a Deployment and a Service in a single execution.
The next concept you’ll run into is managing the output of your custom deployer. While Skaffold applies everything to stdout, you might want to save these generated manifests for auditing or debugging. You could redirect the output of skaffold dev to a file, or, more elegantly, modify your custom deployer to write to a specific file alongside printing to stdout, which Skaffold would then pick up.