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.

💻 Run locally

Copy the code above and run it on your machine

© 2026 ByteLearn.dev. Free courses for developers. · Privacy