1. Pengenalan Viper

golang viper

Memahami kebutuhan akan solusi konfigurasi dalam aplikasi Go

Untuk membangun perangkat lunak yang dapat diandalkan dan mudah dipelihara, para pengembang perlu memisahkan konfigurasi dari logika aplikasi. Hal ini memungkinkan Anda untuk menyesuaikan perilaku aplikasi tanpa mengubah basis kode. Sebuah solusi konfigurasi memungkinkan pemisahan ini dengan memfasilitasi eksternalisasi data konfigurasi.

Aplikasi Go dapat sangat diuntungkan dari sistem tersebut, terutama saat aplikasi tersebut semakin kompleks dan menghadapi berbagai lingkungan implementasi, seperti pengembangan, perancangan, dan produksi. Setiap lingkungan ini mungkin memerlukan pengaturan yang berbeda untuk koneksi basis data, kunci API, nomor port, dan lainnya. Mengkodekan nilai-nilai ini dapat menyebabkan masalah dan rentan terhadap kesalahan, karena hal ini mengarah pada banyak jalur kode untuk memelihara konfigurasi yang berbeda dan meningkatkan risiko paparan data sensitif.

Solusi konfigurasi seperti Viper mengatasi kekhawatiran ini dengan menyediakan pendekatan yang terpadu yang mendukung beragam kebutuhan konfigurasi dan format.

Gambaran umum tentang Viper dan perannya dalam mengelola konfigurasi

Viper adalah pustaka konfigurasi komprehensif untuk aplikasi Go, yang bertujuan menjadi solusi standar untuk semua kebutuhan konfigurasi. Ini sesuai dengan praktik yang ditetapkan dalam metodologi Dua Belas Faktor Aplikasi, yang mendorong penyimpanan konfigurasi di lingkungan untuk mencapai portabilitas antara lingkungan eksekusi.

Viper memainkan peranan penting dalam mengelola konfigurasi dengan:

  • Membaca dan mengurai file konfigurasi dalam berbagai format seperti JSON, TOML, YAML, HCL, dan lainnya.
  • Mengesampingkan nilai konfigurasi dengan variabel lingkungan, sehingga mematuhi prinsip konfigurasi eksternal.
  • Membinding dan membaca dari bendera baris perintah untuk memungkinkan pengaturan dinamis dari opsi konfigurasi saat runtime.
  • Memungkinkan nilai default diatur di dalam aplikasi untuk opsi konfigurasi yang tidak disediakan secara eksternal.
  • Mengawasi perubahan dalam file konfigurasi dan pembaruan langsung, memberikan fleksibilitas dan mengurangi waktu henti untuk perubahan konfigurasi.

2. Instalasi dan Penyiapan

Memasang Viper menggunakan Go modules

Untuk menambahkan Viper ke proyek Go Anda, pastikan proyek Anda sudah menggunakan Go modules untuk manajemen dependensi. Jika Anda sudah memiliki proyek Go, kemungkinan besar Anda memiliki file go.mod di akar proyek Anda. Jika belum, Anda dapat menginisialisasi Go modules dengan menjalankan perintah berikut:

go mod init <module-name>

Ganti <module-name> dengan nama atau jalur proyek Anda. Setelah Anda menginisialisasi Go Modules di proyek Anda, Anda dapat menambahkan Viper sebagai dependensi:

go get github.com/spf13/viper

Perintah ini akan mengambil paket Viper dan mencatat versinya di file go.mod Anda.

Menginisialisasi Viper dalam proyek Go

Untuk mulai menggunakan Viper dalam proyek Go Anda, Anda perlu mengimpor paket dan kemudian membuat contoh baru dari Viper atau menggunakan singleton yang telah ditentukan sebelumnya. Berikut adalah contoh bagaimana melakukannya:

package main

import (
	"fmt"
	"github.com/spf13/viper"
)

func main() {
	// Menggunakan singleton Viper, yang sudah dikonfigurasi dan siap digunakan
	viper.SetDefault("serviceName", "Layanan Hebat Saya")

	// Atau, membuat contoh Viper baru
	myViper := viper.New()
	myViper.SetDefault("serviceName", "Layanan Baru Saya")

	// Mengakses nilai konfigurasi menggunakan singleton
	namaLayanan := viper.GetString("serviceName")
	fmt.Println("Nama Layanan adalah:", namaLayanan)

	// Mengakses nilai konfigurasi menggunakan contoh baru
	namaLayananBaru := myViper.GetString("serviceName")
	fmt.Println("Nama Layanan Baru adalah:", namaLayananBaru)
}

Pada kode di atas, SetDefault digunakan untuk mendefinisikan nilai default untuk kunci konfigurasi. Metode GetString mengambil nilai. Ketika Anda menjalankan kode ini, itu akan mencetak kedua nama layanan yang kami konfigurasikan menggunakan kedua instance singleton serta instance baru.

3. Membaca dan Menulis File Konfigurasi

Bekerja dengan file konfigurasi adalah fitur inti dari Viper. Ini memungkinkan aplikasi Anda untuk mengeksternalisasikan konfigurasi sehingga dapat diperbarui tanpa perlu mengompilasi ulang kode. Di bawah ini, kami akan menjelajahi menyiapkan berbagai format konfigurasi dan menunjukkan bagaimana membaca dari dan menulis ke file-file tersebut.

Menyiapkan format konfigurasi (JSON, TOML, YAML, HCL, dll.)

Viper mendukung beberapa format konfigurasi seperti JSON, TOML, YAML, HCL, dll. Untuk memulai, Anda harus menetapkan nama dan jenis file konfigurasi yang ingin dicari oleh Viper:

v := viper.New()

v.SetConfigName("app")  // Nama file konfigurasi tanpa ekstensi
v.SetConfigType("yaml") // atau "json", "toml", "yml", "hcl", dll.

// Path pencarian file konfigurasi. Tambahkan path berbeda jika lokasi file konfigurasi Anda bervariasi.
v.AddConfigPath("$HOME/.appconfig") // Lokasi konfigurasi pengguna UNIX standar
v.AddConfigPath("/etc/appconfig/")  // Path konfigurasi sistem UNIX
v.AddConfigPath(".")                // Direktori kerja

Membaca dan menulis ke file konfigurasi

Setelah instance Viper tahu di mana mencari file konfigurasi dan apa yang harus dicari, Anda dapat memintanya untuk membaca konfigurasi:

if err := v.ReadInConfig(); err != nil {
    if _, ok := err.(viper.ConfigFileNotFoundError); ok {
        // File konfigurasi tidak ditemukan; abaikan jika diinginkan atau tangani secara berbeda
        log.Printf("Tidak ada file konfigurasi ditemukan. Menggunakan nilai default, dan/atau variabel lingkungan.")
    } else {
        // File konfigurasi ditemukan tetapi terjadi kesalahan lain
        log.Fatalf("Error membaca file konfigurasi, %s", err)
    }
}

Untuk menulis modifikasi kembali ke file konfigurasi, atau membuat yang baru, Viper menawarkan beberapa metode. Berikut cara menulis konfigurasi saat ini ke file:

err := v.WriteConfig() // Menulis konfigurasi saat ini ke path yang ditentukan oleh `v.SetConfigName` dan `v.AddConfigPath`
if err != nil {
    log.Fatalf("Error menulis file konfigurasi, %s", err)
}

Menetapkan nilai konfigurasi default

Nilai default berfungsi sebagai cadangan jika kunci tidak diatur dalam file konfigurasi atau oleh variabel lingkungan:

v.SetDefault("ContentDir", "content")
v.SetDefault("LogLevel", "debug")
v.SetDefault("Database.Port", 5432)

// Struktur data yang lebih kompleks untuk nilai default
viper.SetDefault("Taxonomies", map[string]string{
    "tag":       "tags",
    "category":  "categories",
})

4. Mengelola Variabel Lingkungan dan Flag

Viper tidak hanya terbatas pada file konfigurasi—ia juga dapat mengelola variabel lingkungan dan flag baris perintah, yang sangat berguna saat menangani pengaturan yang spesifik untuk lingkungan.

Mengikat variabel lingkungan dan flag ke Viper

Mengikat variabel lingkungan:

v.AutomaticEnv() // Otomatis mencari kunci variabel lingkungan yang cocok dengan kunci Viper

v.SetEnvPrefix("APP") // Awalan untuk variabel lingkungan untuk membedakannya dari yang lain
v.BindEnv("port")     // Mengikat variabel lingkungan PORT (mis., APP_PORT)

// Anda juga dapat mencocokkan variabel lingkungan dengan nama yang berbeda ke kunci dalam aplikasi Anda
v.BindEnv("database_url", "DB_URL") // Ini memberitahu Viper untuk menggunakan nilai variabel lingkungan DB_URL untuk kunci konfigurasi "database_url"

Mengikat flag menggunakan pflag, sebuah paket Go untuk parsing flag baris perintah:

var port int

// Mendefinisikan sebuah flag menggunakan pflag
pflag.IntVarP(&port, "port", "p", 808, "Port untuk aplikasi")

// Mengikat flag ke kunci Viper
pflag.Parse()
if err := v.BindPFlag("port", pflag.Lookup("port")); err != nil {
    log.Fatalf("Error mengikat flag ke kunci, %s", err)
}

Menangani konfigurasi yang spesifik untuk lingkungan

Sebuah aplikasi sering perlu beroperasi secara berbeda di berbagai lingkungan (pengembangan, staging, produksi, dll.). Viper dapat menggunakan konfigurasi dari variabel lingkungan yang dapat mengganti pengaturan dalam file konfigurasi, memungkinkan konfigurasi yang spesifik untuk lingkungan:

v.SetConfigName("config") // Nama file konfigurasi default

// Konfigurasi dapat diganti oleh variabel lingkungan
// dengan awalan APP dan sisa kunci dalam huruf besar
v.SetEnvPrefix("APP")
v.AutomaticEnv()

// Di lingkungan produksi, Anda dapat menggunakan variabel lingkungan APP_PORT untuk mengganti port default
fmt.Println(v.GetString("port")) // Output akan menjadi nilai dari APP_PORT jika diatur, jika tidak nilai dari file konfigurasi atau default

Ingatlah untuk menangani perbedaan antara lingkungan dalam kode aplikasi Anda, jika diperlukan, berdasarkan konfigurasi yang dimuat oleh Viper.

5. Dukungan Penyimpanan Kunci/Nilai Jarak Jauh

Viper menyediakan dukungan yang kuat untuk mengelola konfigurasi aplikasi menggunakan penyimpanan kunci/nilai jarak jauh seperti etcd, Consul, atau Firestore. Hal ini memungkinkan konfigurasi untuk dijadikan terpusat dan diperbarui secara dinamis di seluruh sistem terdistribusi. Selain itu, Viper memungkinkan penanganan yang aman terhadap konfigurasi sensitif melalui enkripsi.

Mengintegrasikan Viper dengan Penyimpanan Kunci/Nilai Jarak Jauh (etcd, Consul, Firestore, dll.)

Untuk mulai menggunakan Viper dengan penyimpanan kunci/nilai jarak jauh, Anda perlu melakukan impor kosong dari paket viper/remote dalam aplikasi Go Anda:

import _ "github.com/spf13/viper/remote"

Mari kita lihat contoh integrasinya dengan etcd:

import (
    "log"

    "github.com/spf13/viper"
    _ "github.com/spf13/viper/remote"
)

func initRemoteConfig() {
    viper.SetConfigType("json") // Tetapkan tipe file konfigurasi jarak jauh
    viper.AddRemoteProvider("etcd", "http://127...1:4001", "/config/myapp.json")
  
    err := viper.ReadRemoteConfig() // Upaya membaca konfigurasi jarak jauh
    if err != nil {
        log.Fatalf("Gagal membaca konfigurasi jarak jauh: %v", err)
    }
  
    log.Println("Berhasil membaca konfigurasi jarak jauh")
}

func main() {
    initRemoteConfig()
    // Logika aplikasi Anda di sini
}

Pada contoh ini, Viper terhubung ke server etcd yang berjalan pada http://127...1:4001 dan membaca konfigurasi yang terletak di /config/myapp.json. Ketika bekerja dengan penyimpanan lain seperti Consul, gantikan "etcd" dengan "consul" dan sesuaikan parameter spesifik penyedia yang sesuai.

Mengelola Konfigurasi yang Terenkripsi

Konfigurasi sensitif, seperti kunci API atau kredensial basis data, sebaiknya tidak disimpan dalam teks biasa. Viper memungkinkan konfigurasi terenkripsi untuk disimpan dalam penyimpanan kunci/nilai dan dienkripsi dalam aplikasi Anda.

Untuk menggunakan fitur ini, pastikan pengaturan terenkripsi disimpan dalam penyimpanan kunci/nilai Anda. Kemudian manfaatkan AddSecureRemoteProvider dari Viper. Berikut adalah contoh pemanfaatannya dengan etcd:

import (
    "log"

    "github.com/spf13/viper"
    _ "github.com/spf13/viper/remote"
)

func initSecureRemoteConfig() {
    const secretKeyring = "/path/to/secret/keyring.gpg" // Path ke file keyring Anda
  
    viper.SetConfigType("json")
    viper.AddSecureRemoteProvider("etcd", "http://127...1:4001", "/config/myapp.json", secretKeyring)
  
    err := viper.ReadRemoteConfig()
    if err != nil {
        log.Fatalf("Tidak dapat membaca konfigurasi jarak jauh: %v", err)
    }

    log.Println("Berhasil membaca dan mendekripsi konfigurasi jarak jauh")
}

func main() {
    initSecureRemoteConfig()
    // Logika aplikasi Anda di sini
}

Pada contoh di atas, AddSecureRemoteProvider digunakan, dengan spesifikasi jalur ke sebuah keyring GPG yang berisi kunci-kunci yang diperlukan untuk dekripsi.

6. Memantau dan Menangani Perubahan Konfigurasi

Salah satu fitur kuat Viper adalah kemampuannya untuk memantau dan merespons perubahan konfigurasi secara langsung, tanpa harus me-restart aplikasi.

Memantau Perubahan Konfigurasi dan Membaca Ulang Konfigurasi

Viper menggunakan paket fsnotify untuk memantau perubahan pada file konfigurasi Anda. Anda dapat menyiapkan pengawas untuk memicu peristiwa setiap kali file konfigurasi berubah:

import (
    "log"

    "github.com/fsnotify/fsnotify"
    "github.com/spf13/viper"
)

func watchConfig() {
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        log.Printf("File konfigurasi berubah: %s", e.Name)
        // Di sini Anda dapat membaca konfigurasi yang telah diperbarui jika diperlukan
        // Lakukan tindakan seperti meinisialisasi ulang layanan atau memperbarui variabel
    })
}

func main() {
    viper.SetConfigName("myapp")
    viper.AddConfigPath(".")
    err := viper.ReadInConfig()
    if err != nil {
        log.Fatalf("Kesalahan membaca file konfigurasi, %s", err)
    }

    watchConfig()
    // Logika aplikasi Anda di sini
}

Pemicu untuk Memperbarui Konfigurasi dalam Aplikasi yang Sedang Berjalan

Dalam aplikasi yang sedang berjalan, Anda mungkin ingin memperbarui konfigurasi sebagai tanggapan terhadap berbagai pemicu, seperti sinyal, pekerjaan berbasis waktu, atau permintaan API. Anda dapat struktur aplikasi Anda untuk menyegarkan keadaan internalnya berdasarkan konfigurasi yang dibaca ulang dari Viper:

import (
    "os"
    "os/signal"
    "syscall"
    "time"
    "log"

    "github.com/spf13/viper"
)

func setupSignalHandler() {
    signalChannel := make(chan os.Signal, 1)
    signal.Notify(signalChannel, syscall.SIGHUP) // Mendengarkan sinyal SIGHUP

    go func() {
        for {
            sig := <-signalChannel
            if sig == syscall.SIGHUP {
                log.Println("Menerima sinyal SIGHUP. Memuat ulang konfigurasi...")
                err := viper.ReadInConfig() // Membaca ulang konfigurasi
                if err != nil {
                    log.Printf("Error saat membaca ulang konfigurasi: %s", err)
                } else {
                    log.Println("Konfigurasi berhasil dimuat ulang.")
                    // Muat ulang aplikasi Anda berdasarkan konfigurasi baru di sini
                }
            }
        }
    }()
}

func main() {
    viper.SetConfigName("myapp")
    viper.AddConfigPath(".")
    err := viper.ReadInConfig()
    if err != nil {
        log.Fatalf("Error saat membaca file konfigurasi, %s", err)
    }

    setupSignalHandler()
    for {
        // Logika utama aplikasi
        time.Sleep(10 * time.Second) // Simulasikan beberapa pekerjaan
    }
}

Pada contoh ini, kita menyiapkan pengendali untuk mendengarkan sinyal SIGHUP. Saat menerima sinyal tersebut, Viper memuat ulang file konfigurasi dan aplikasi kemudian seharusnya memperbarui konfigurasinya atau keadaannya sesuai kebutuhan.

Selalu ingat untuk menguji konfigurasi ini untuk memastikan aplikasi Anda dapat menangani pembaruan dinamis dengan baik.