Bu fikir GitOps paradigmasından esinlenmektedir. Size bir uygulamanın ve bir mühendisin uygulama konfigürasyonlarıyla nasıl etkileşime gireceğini göstereceğim. Bu yaklaşımın birkaç yerde kullanıldığını gördüm ve aşağıda listelenen nedenlerden dolayı mantıklı olduğunu düşünüyorum.


Hepimizin bildiği gibi, uygulama yapılandırmaları genellikle uygulamaların deposunda bulunur. Bir .env dosyası veya başka bir biçimde olma eğilimindedir. Ayrıca her ortam için birden fazla kopya olabilir, bu nedenle etrafa dağılmış birçok dosyanız olur. Şimdilik, bu uygulamadan uzaklaşmanın neden iyi olduğunu düşündüğümü görmek için kısa keselim. Bu örnekte bir Golang uygulaması kullanacağım.


Faydalar



Nasıl çalışır


İki depo var.



Çalışma şekli "local" (yerel) ve "other" (diğer) ortamlar için farklılık gösterir. "local", mühendislerin bilgisayarlarında uygulamaları çalıştırdığı, "other" ise Kubernetes'in uygulamalarınızı çalıştırdığı yerdir (qa, staging, sandbox, prod ...). Her ikisi de aşağıda açıklandığı gibi "yeşil" ve "kırmızı" bölümlere sahiptir.


Local




Other



Yeşil ve kırmızıyı birleştiren tek bir çalışma şekli vardır ve çok basittir. Uygulama ilk kez çalıştırıldığında, yapılandırmayı bir yapıya eşlemek üzere çekmek için bir GitHub istemcisi kullanarak uzak yapılandırma deposuyla konuşur.


Uygulama


Yapılandırma deposu


Aşağıda gösterildiği gibi tüm hizmet ve altyapı yapılandırmalarını içinde tutarak GitOps paradigmasını takip etmek için bu depoyu kullanabilirsiniz.


├── argocd
│   └── ...
├── helm
│   └── ...
├── service
│   ├── local
│   │   └── api.json
│   ├── prod
│   │   └── api.json
│   └── test
│   └── api.json
├── terraform
│   └── ...
└── ...

local/api.json

{
"auth_url": "http://localhost:8000/api/v1/auth"
}

prod/api.json

{
"auth_url": "http://auth:8000/",
"auth_keys": [
"prod_1"
]
}

test/api.json

{
"auth_url": "http://auth:8000/",
"auth_keys": [
"test_1"
]
}

Servis deposu


Bu tamamen ayrılmış bir örnektir. Ancak, kurulumu minimumda tutmak istiyorsanız, kodunuzu birleştirebilirsiniz, ancak buna tavsiye etmiyorum.


├── config
│   └── store.go
├── github
│   └── client.go
├── go.mod
├── go.sum
└── main.go

main.go

package main

import (
"context"
"fmt"
"log"
"os"

"api/config"
"api/github"
)

type Config struct {
AuthURL string `json:"auth_url"`
AuthKeys []string `json:"auth_keys"`
}

func main() {
// Initialise remote config storage client.
cfgClient := github.Client{
Token: os.Getenv("GITHUB_TOKEN"),
Owner: os.Getenv("GITHUB_OWNER"),
Repo: os.Getenv("GITHUB_CONFIG_REPO"),
}

// Initialise config storage using remote client and local storage path.
cfgStore := config.Store{
Client: cfgClient,
ServiceEnv: os.Getenv("SERVICE_ENV"),
ServiceName: "api",
RepoBase: "../config",
FileBase: "/service",
}

// Initialise service config without any value.
var svcConfig Config

// Load config into service config.
if err := cfgStore.Load(context.Background(), &svcConfig); err != nil {
log.Fatalln(err)
}

fmt.Printf("%+v\n", svcConfig)
}

Kodunuzu birleştirirseniz, main.go dosyanız bu şekilde görünür.


package main

import (
"context"
"fmt"
"log"

"api/config"
)

type Config struct {
AuthURL string `json:"auth_url"`
AuthKeys []string `json:"auth_keys"`
}

func main() {
// Initialise service config without any value.
var svcConfig Config

// Load config into service config.
if err := config.Load(context.Background(), &svcConfig, "api"); err != nil {
log.Fatalln(err)
}

fmt.Printf("%+v\n", svcConfig)
}

config/store.go

package config

import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
)

// Client interfaces remote config storage layer and all remote storages must satisfy this type.
type Client interface {
LoadConfig(ctx context.Context, cfg interface{}, path string) error
}

type Store struct {
// Client represents remote config storage.
Client Client
// ServiceEnv represents the environment which service is running on.
ServiceEnv string
// ServiceName represents the service name which satisfies remote and local storage.
ServiceName string
// RepoRoot represents the absolute/relative base path to local clone of remote config storage.
RepoBase string
// FileRoot represents the base path to config file in local clone of remote config storage.
FileBase string
}

// Load reads service config values from either remote storage or local clone of remote storage
// and maps to `cfg` argument which is a struct as in pointer.
func (s Store) Load(ctx context.Context, cfg interface{}) error {
if s.ServiceEnv == "" {
return fmt.Errorf("service environment is required")
}

if s.ServiceEnv == "local" {
return s.fromLocal(cfg)
}

path := fmt.Sprintf("%s/%s/%s.json", s.FileBase, s.ServiceEnv, s.ServiceName)

return s.Client.LoadConfig(ctx, cfg, path)
}

// fromLocal reads service configuration file from the local file system.
func (s Store) fromLocal(cfg interface{}) error {
path := fmt.Sprintf("%s/%s/local/%s.json", s.RepoBase, s.FileBase, s.ServiceName)

file, err := ioutil.ReadFile(path)
if err != nil {
return fmt.Errorf("read file from local environment: %w", err)
}

if err := json.Unmarshal(file, cfg); err != nil {
return fmt.Errorf("unmarshal file from local environment: %w", err)
}

return nil
}

github/client.go

GitHub API hız sınırlamasına dikkat edin.


package github

import (
"context"
"encoding/json"
"fmt"
"io/ioutil"

"github.com/google/go-github/v42/github"
"golang.org/x/oauth2"
)

type Client struct {
Token string
Owner string
Repo string
}

// LoadConfig reads config file from the remote repository and maps it to its struct which is a pointer.
func (c Client) LoadConfig(ctx context.Context, cfg interface{}, path string) error {
if c.Token == "" {
return fmt.Errorf("github token is required")
}

clt := github.NewClient(
oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.Token})),
)

con, res, err := clt.Repositories.DownloadContents(ctx, c.Owner, c.Repo, path, nil)
if err != nil {
return fmt.Errorf("download content: %w", err)
}
if res.StatusCode != 200 {
return fmt.Errorf("unexpected response code: %d", res.StatusCode)
}

file, err := ioutil.ReadAll(con)
if err != nil {
return fmt.Errorf("read file from github: %w", err)
}

if err := json.Unmarshal(file, cfg); err != nil {
return fmt.Errorf("unmarshal file from github: %w", err)
}

return nil
}

Bu ortam değişkenlerine ihtiyacınız olacak veya tercih ettiğiniz adları ve değerleri kullanacaksınız.


SERVICE_ENV: A single environment. local/test/sbox/prod/....
GITHUB_TOKEN: User personal access token
GITHUB_OWNER: GitHub account owner
GITHUB_CONFIG_REPO: GitHub config repository

Test


$ env | grep GITHUB
GITHUB_CONFIG_REPO=config
GITHUB_TOKEN=ghp_cctTG5cyYy6cyxbcFJG7u56fghyfhjFgd432f
GITHUB_OWNER=you

$ export SERVICE_ENV=local
$ go run main.go
{AuthURL:http://localhost:8000/api/v1/auth AuthKeys:[]}

$ export SERVICE_ENV=test
$ go run main.go
{AuthURL:http://auth:8000/ AuthKeys:[test_1]}