Rolling Deploy Controller
A deployment controller that performs a rolling update across a fleet of instances with automatic rollback on failure. It deploys one instance at a time, verifies health after each update, and if consecutive failures exceed a threshold, rolls back only the instances that were modified. Exits with a non-zero code for CI integration.
// Run: go run . v1.3.0
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"time"
)
// Instance represents a deployable service instance
type Instance struct {
ID string
Address string
Version string
}
// DeployResult holds the outcome of a rolling deploy
type DeployResult struct {
Status string
UpdatedInstances []string
}
// HealthChecker verifies instance health via HTTP
type HealthChecker struct {
Client *http.Client
Endpoint string
Retries int
Interval time.Duration
}
func (h *HealthChecker) Check(ctx context.Context, address string) error {
url := "http://" + address + h.Endpoint
for attempt := 0; attempt <= h.Retries; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := h.Client.Do(req)
if err == nil && resp.StatusCode == http.StatusOK {
resp.Body.Close()
return nil
}
if resp != nil {
resp.Body.Close()
}
if attempt < h.Retries {
time.Sleep(h.Interval)
}
}
return fmt.Errorf("health check failed for %s after %d retries", address, h.Retries)
}
// RollingDeployer performs a rolling deploy with failure threshold
type RollingDeployer struct {
Health *HealthChecker
MaxFailed int
}
func (rd *RollingDeployer) Deploy(
ctx context.Context,
instances []Instance,
newVersion string,
deployFn func(ctx context.Context, inst Instance, version string) error,
) (*DeployResult, error) {
result := &DeployResult{Status: "success"}
consecutiveFails := 0
for _, inst := range instances {
log.Printf(" Deploying %s: %s → %s", inst.ID, inst.Version, newVersion)
if err := deployFn(ctx, inst, newVersion); err != nil {
consecutiveFails++
log.Printf(" Deploy failed for %s: %v", inst.ID, err)
} else {
result.UpdatedInstances = append(result.UpdatedInstances, inst.ID)
// Verify health after deploy
if err := rd.Health.Check(ctx, inst.Address); err != nil {
consecutiveFails++
log.Printf(" Health check failed for %s: %v", inst.ID, err)
} else {
consecutiveFails = 0
log.Printf(" %s healthy", inst.ID)
}
}
if consecutiveFails >= rd.MaxFailed {
result.Status = "failed"
return result, fmt.Errorf("max failures (%d) reached", rd.MaxFailed)
}
}
return result, nil
}
// Rollback reverts updated instances to a previous version
type Rollback struct {
PreviousVersion string
DeployFn func(ctx context.Context, inst Instance, version string) error
Instances []Instance
Health *HealthChecker
}
func (rb *Rollback) Execute(ctx context.Context) error {
for _, inst := range rb.Instances {
log.Printf(" Rolling back %s to %s", inst.ID, rb.PreviousVersion)
if err := rb.DeployFn(ctx, inst, rb.PreviousVersion); err != nil {
return fmt.Errorf("rollback failed for %s: %w", inst.ID, err)
}
if err := rb.Health.Check(ctx, inst.Address); err != nil {
return fmt.Errorf("rollback health check failed for %s: %w", inst.ID, err)
}
}
return nil
}
func main() {
instances := []Instance{
{ID: "app-1", Address: "10.0.1.1:8080", Version: "v1.2.0"},
{ID: "app-2", Address: "10.0.1.2:8080", Version: "v1.2.0"},
{ID: "app-3", Address: "10.0.1.3:8080", Version: "v1.2.0"},
{ID: "app-4", Address: "10.0.1.4:8080", Version: "v1.2.0"},
}
newVersion := "v1.3.0"
if len(os.Args) > 1 {
newVersion = os.Args[1]
}
health := &HealthChecker{
Client: &http.Client{Timeout: 5 * time.Second},
Endpoint: "/healthz",
Retries: 3,
Interval: 2 * time.Second,
}
deployer := &RollingDeployer{
Health: health,
MaxFailed: 2,
}
// The deploy function. In production this calls Docker/K8s API
deployFn := func(ctx context.Context, inst Instance, version string) error {
log.Printf(" Updating %s to %s (simulated)", inst.ID, version)
// Real implementation:
// return k8sClient.UpdatePod(ctx, inst.ID, version)
return nil
}
ctx := context.Background()
previousVersion := instances[0].Version
result, err := deployer.Deploy(ctx, instances, newVersion, deployFn)
if err != nil {
log.Printf("Deploy failed: %v", err)
log.Printf("Initiating rollback...")
// Rollback only the instances that were updated
var rollbackTargets []Instance
for _, inst := range instances {
for _, updated := range result.UpdatedInstances {
if inst.ID == updated {
rollbackTargets = append(rollbackTargets, inst)
}
}
}
rb := &Rollback{
PreviousVersion: previousVersion,
DeployFn: deployFn,
Instances: rollbackTargets,
Health: health,
}
if rbErr := rb.Execute(ctx); rbErr != nil {
log.Fatalf("CRITICAL: Rollback failed: %v", rbErr)
}
result.Status = "rolled_back"
fmt.Printf("Deploy rolled back. Updated then reverted: %v\n", result.UpdatedInstances)
os.Exit(1)
}
fmt.Printf("Deploy complete. Updated: %v\n", result.UpdatedInstances)
}This controller handles the full lifecycle: attempt rolling deploy, detect failure threshold, automatically roll back affected instances, and exit with appropriate status code for CI integration.