Worker queue sering diputar ulang oleh autoscaler, deployment, atau pemulihan insiden. Jika setiap worker dibangun dari dependency yang berubah diam-diam, dua worker dengan source code yang sama dapat menjalankan binary berbeda. Nix untuk worker queue menyelesaikan bagian build ini dengan mengunci input build dan menghasilkan artifact yang dapat dibangun ulang serta dirujuk kembali saat rollback.

Pendekatannya sederhana: source Go, compiler Go, dan package set Nix menjadi input derivation; hasilnya adalah binary di Nix store. Sebaliknya, URL Redis, nama queue, concurrency, dan secret tetap menjadi konfigurasi runtime. Dengan pemisahan ini, artifact yang sama dapat dipromosikan dari staging ke production tanpa memasukkan credential atau endpoint lingkungan ke dalam hasil build.

Reproducible build berarti input build yang sama diharapkan menghasilkan output yang sama secara fungsional dan dapat ditelusuri. Ini bukan jaminan bahwa worker lama tetap kompatibel dengan payload job, schema database, protokol Redis, atau secret yang sudah berubah.

Mengapa worker queue memerlukan artifact yang reproducible

Queue worker biasanya berjalan dalam banyak replika dan dapat hidup lebih lama daripada satu deployment API. Ketika satu replika restart setelah dependency upstream berubah, perbedaan artifact dapat menimbulkan perilaku yang sulit dilacak, misalnya:

  • Worker lama dan baru melakukan serialisasi payload dengan perilaku berbeda.
  • Patch dependency diterapkan pada sebagian pod, tetapi tidak pada pod yang belum direstart.
  • Rollback source code tidak benar-benar mengembalikan toolchain atau dependency build sebelumnya.
  • Binary yang dipakai saat insiden tidak dapat dibuat ulang karena channel package manager telah bergerak.

Nix membuat hubungan input dan output lebih eksplisit. Derivation mendeskripsikan apa yang dibutuhkan untuk membangun artifact. File flake.lock kemudian mengunci revisi input, termasuk nixpkgs. Jika commit source dan lockfile sama, tim memiliki basis yang jauh lebih baik untuk membangun atau mengambil artifact yang sama.

Anatomi derivation Nix secara ringkas

Derivation adalah resep build yang dideklarasikan. Dalam contoh Go di bawah, input utamanya adalah source repository, package go dari nixpkgs, dan instruksi buildGoModule. Output-nya adalah direktori immutable di Nix store yang berisi binary worker.

  • Input: source code, revisi nixpkgs, compiler, dan dependency build.
  • Builder: fungsi seperti buildGoModule yang mengatur fase build Go.
  • Output: path store, misalnya /nix/store/...-queue-worker-....
  • Lockfile: flake.lock, yang mencatat revisi konkret input flake.

Untuk contoh yang tetap kecil, worker berikut memakai library standar Go dan protokol Redis RESP dasar untuk menjalankan BLPOP. Ini cukup untuk menunjukkan batas build dan runtime, tetapi bukan klien Redis lengkap: ia tidak menangani TLS, Redis Cluster, reconnect tingkat lanjut, atau semua bentuk respons Redis.

Struktur proyek

.
├── flake.nix
├── flake.lock
├── go.mod
└── main.go
module example.com/queue-worker

go 1.22

Worker Go minimal

package main

import (
    "bufio"
    "fmt"
    "log"
    "net"
    "net/url"
    "os"
    "strconv"
    "strings"
)

func writeCommand(w *bufio.Writer, args ...string) error {
    if _, err := fmt.Fprintf(w, "*%d\r\n", len(args)); err != nil { return err }
    for _, arg := range args {
        if _, err := fmt.Fprintf(w, "$%d\r\n%s\r\n", len(arg), arg); err != nil { return err }
    }
    return w.Flush()
}

func readBulk(r *bufio.Reader) (string, error) {
    line, err := r.ReadString('\n')
    if err != nil { return "", err }
    if strings.HasPrefix(line, "$-1") { return "", nil }
    if !strings.HasPrefix(line, "$") { return "", fmt.Errorf("unexpected Redis response: %q", line) }
    n, err := strconv.Atoi(strings.TrimSpace(line[1:]))
    if err != nil { return "", err }
    buf := make([]byte, n+2)
    if _, err := r.Read(buf); err != nil { return "", err }
    return string(buf[:n]), nil
}

func main() {
    redisURL := os.Getenv("REDIS_URL")
    queue := os.Getenv("QUEUE_NAME")
    if redisURL == "" || queue == "" {
        log.Fatal("REDIS_URL and QUEUE_NAME are required")
    }

    u, err := url.Parse(redisURL)
    if err != nil || u.Host == "" || (u.Scheme != "redis" && u.Scheme != "") {
        log.Fatal("REDIS_URL must be a redis:// URL")
    }

    conn, err := net.Dial("tcp", u.Host)
    if err != nil { log.Fatal(err) }
    defer conn.Close()

    r, w := bufio.NewReader(conn), bufio.NewWriter(conn)
    if u.User != nil {
        password, _ := u.User.Password()
        if password == "" { log.Fatal("REDIS_URL password is empty") }
        if err := writeCommand(w, "AUTH", password); err != nil { log.Fatal(err) }
        if _, err := r.ReadString('\n'); err != nil { log.Fatal(err) }
    }

    for {
        if err := writeCommand(w, "BLPOP", queue, "0"); err != nil { log.Fatal(err) }
        header, err := r.ReadString('\n')
        if err != nil { log.Fatal(err) }
        if !strings.HasPrefix(header, "*2") { log.Fatalf("unexpected Redis response: %q", header) }
        if _, err := readBulk(r); err != nil { log.Fatal(err) } // nama queue
        payload, err := readBulk(r)
        if err != nil { log.Fatal(err) }

        // Ganti dengan handler idempoten yang memvalidasi payload dan mencatat hasilnya.
        log.Printf("processing job: %s", payload)
    }
}

Contoh ini sengaja mengambil REDIS_URL dan QUEUE_NAME dari environment saat proses dimulai. Nilai tersebut tidak menjadi bagian dari derivation, sehingga perubahan endpoint Redis tidak memaksa rebuild binary.

Konfigurasi Nix minimal untuk worker queue

Buat flake.nix berikut. Flake hanya mendeklarasikan channel input; revisi konkret channel akan direkam oleh flake.lock setelah perintah lock dijalankan.

{
  description = "Reproducible Go queue worker";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
    in {
      packages.${system}.worker = pkgs.buildGoModule {
        pname = "queue-worker";
        version = if self ? rev then self.rev else "dirty";
        src = ./.;

        # Proyek contoh hanya memakai standard library Go.
        # Jika menambah module eksternal, gunakan hash vendor yang benar.
        vendorHash = null;

        env.CGO_ENABLED = 0;
        ldflags = [ "-s" "-w" ];
      };

      packages.${system}.default = self.packages.${system}.worker;

      devShells.${system}.default = pkgs.mkShell {
        packages = [ pkgs.go ];
      };
    };
}

buildGoModule cocok untuk proyek Go karena Nix mengelola fase download, vendor, dan build secara terkontrol. Pada contoh ini, vendorHash = null valid karena tidak ada module Go eksternal. Jika worker memakai klien Redis seperti go-redis, jangan mempertahankan nilai tersebut: Nix harus memverifikasi dependency vendor melalui hash yang dihasilkan dari dependency proyek.

Menambah dependency Go eksternal dengan benar

Setelah menambahkan module eksternal dan menjalankan go mod tidy, ubah derivation agar memakai hash vendor. Saat hash belum diketahui, gunakan lib.fakeHash sekali untuk memicu kegagalan build; Nix akan menampilkan hash yang diharapkan. Salin hash tersebut ke repository dan commit bersama go.mod, go.sum, serta flake.lock.

vendorHash = pkgs.lib.fakeHash;

Jangan menyimpan fakeHash sebagai konfigurasi final. Artifact tidak akan dapat dibangun sampai hash aktual dipasang.

Pin input dengan flake.lock, build, dan verifikasi

Inisialisasi lockfile dan bangun package dari root repository:

nix flake lock
nix build .#worker --out-link result
./result/bin/queue-worker

Perintah terakhir akan gagal tanpa environment runtime, dan itu diharapkan. Jalankan dengan konfigurasi eksplisit:

REDIS_URL='redis://:[email protected]:6379' \
QUEUE_NAME='email-jobs' \
./result/bin/queue-worker

Jangan memasukkan URL dengan password ke shell history, Dockerfile, atau repository. Di production, injeksikan REDIS_URL dari secret manager, Kubernetes Secret, systemd credential, atau mekanisme rahasia platform yang sesuai.

Verifikasi minimum sebelum promosi:

  1. Pastikan repository bersih atau catat commit source yang dibangun.
  2. Jalankan nix flake check bila proyek mendefinisikan checks.
  3. Jalankan tes Go melalui development shell: nix develop -c go test ./....
  4. Bangun artifact: nix build .#worker --out-link result.
  5. Catat path hasil dengan readlink -f result dan, bila dikirim ke registry, catat digest image container.
  6. Uji worker terhadap Redis non-produksi dengan payload yang representatif.

Commit flake.lock. Tanpa lockfile yang dikomit, evaluator lain dapat mengambil revisi nixpkgs berbeda meskipun flake.nix tampak sama.

Memasukkan artifact ke container

Jika platform menjalankan container, ada dua pilihan praktis. Untuk binary Go statis seperti contoh dengan CGO_ENABLED=0, hasil result/bin/queue-worker dapat disalin ke image runtime minimal. Untuk binary yang memakai library native atau membutuhkan file lain dari Nix store, gunakan image builder berbasis Nix atau salin closure runtime secara lengkap; menyalin satu binary saja dapat membuat dependency dinamis hilang.

Contoh pola multi-stage berikut membangun artifact lebih dahulu dan menyalin binary statis ke image akhir. Pastikan build context mencakup flake.nix, flake.lock, dan source code.

FROM nixos/nix AS build
WORKDIR /src
COPY . .
RUN nix build .#worker --out-link /out/result

FROM scratch
COPY --from=build /out/result/bin/queue-worker /queue-worker
ENTRYPOINT ["/queue-worker"]

Image tetap tidak berisi endpoint Redis maupun secret. Masukkan environment tersebut saat deployment, misalnya melalui manifest orchestrator. Setelah image dipush, gunakan digest image, bukan hanya tag mutable seperti latest, sebagai identitas artifact yang dipromosikan dan di-rollback.

Prosedur rollback lockfile dan artifact

Rollback paling aman memilih artifact yang sebelumnya sudah lulus pengujian dan pernah dipakai. Jika registry menyimpan image berdasarkan digest, deploy kembali digest tersebut. Cara ini tidak memerlukan rebuild saat insiden.

Rollback artifact yang sudah tersimpan

  1. Ambil digest image atau path artifact yang tercatat pada rilis sehat sebelumnya.
  2. Ubah manifest deployment agar merujuk digest tersebut.
  3. Restart atau lakukan rollout worker secara bertahap.
  4. Amati backlog queue, tingkat kegagalan job, latency handler, dan jumlah retry sebelum melanjutkan rollout.

Rebuild dari lockfile lama

Jika artifact lama tidak tersedia, kembalikan source dan lockfile ke commit rilis yang diketahui sehat, lalu build ulang:

git checkout <commit-rilis-sehat> -- flake.nix flake.lock go.mod go.sum main.go
nix build .#worker --out-link result
readlink -f result

Dalam praktik, lebih aman melakukan checkout commit rilis penuh pada worktree atau pipeline terpisah daripada memilih file satu per satu. Source, go.mod, go.sum, dan flake.lock harus kembali sebagai satu set yang konsisten. Build baru dapat diberi image tag rilis, tetapi deployment sebaiknya tetap menggunakan digest hasil push.

Batasan dan kesalahan yang sering terjadi

  • Reproducible build bukan rollback data. Worker versi lama mungkin tidak memahami payload baru, migration database, atau perubahan kontrak API. Terapkan versioning payload, handler idempoten, dan strategi migrasi kompatibel mundur.
  • Konfigurasi runtime tetap dapat mengubah perilaku. Binary identik yang diarahkan ke queue berbeda, Redis berbeda, atau feature flag berbeda tetap dapat berperilaku berbeda.
  • Jangan menaruh secret dalam derivation. Secret yang masuk ke source, argumen build, atau store path berisiko tersimpan dalam cache build dan dapat dibaca pihak yang memiliki akses store.
  • Jangan memperbarui lockfile tanpa review. Perintah update flake dapat mengubah banyak package build sekaligus. Review diff flake.lock, lakukan tes, lalu promosikan artifact baru secara eksplisit.
  • Perhatikan target platform. Contoh menetapkan x86_64-linux. Untuk ARM atau macOS, definisikan system yang sesuai dan build artifact pada platform target atau gunakan builder yang mendukungnya.
  • Binary statis bukan selalu pilihan. Menonaktifkan CGO memudahkan image minimal, tetapi aplikasi yang membutuhkan library native, resolver tertentu, atau dependency sistem mungkin memerlukan runtime image/closure yang lebih lengkap.

Ringkasan implementasi

Gunakan Nix untuk mengunci toolchain dan dependency build worker queue melalui flake.lock; gunakan environment atau secret manager untuk Redis URL, nama queue, dan credential. Bangun artifact dengan nix build, catat path store atau digest container, lalu jadikan identitas tersebut sebagai unit promosi dan rollback. Hasilnya bukan pengganti desain kompatibilitas job, tetapi menghilangkan satu sumber variasi penting ketika banyak worker harus direstart atau dikembalikan ke rilis sebelumnya.