GoFrame framework (RK boot): differentiated configuration file (Config) based on cloud native environment

Posted by westair on Thu, 03 Feb 2022 13:08:00 +0100

introduce

Through a complete example, in gogf/gf In the framework, the configuration files are distinguished according to the environment. That is, how to read different configuration files in [test], [Online] and other environments.

We will use rk-boot To start gogf/gf Microservices.

Please visit the following address for a complete tutorial:

install

go get github.com/rookie-ninja/rk-boot/gf

Quick start

We will create config / Beijing yaml, config/shanghai. yaml, config/default. Yaml three configuration files, and then read different files according to different environment variables.

Rk boot uses REALM, REGION, AZ and DOMAIN environment variables to distinguish different environments. This is also the cloud native environment resolution method we recommend.

For example, REALM = "your business", REGION = "Beijing", AZ = "Beijing first district", DOMAIN = "test environment".

Rk boot integrates viper To process the configuration file.

1. Create a profile

  • config/beijing.yaml
---
my-region: beijing
  • config/shanghai.yaml
---
my-region: shanghai
  • config/default.yaml
---
my-region: default

2. Create boot yaml

boot.yaml file tells rk boot how to start gogf/gf Service.

We use config as boot The entry of configuration file in yaml, which can provide multiple config file paths.

Locale represents the environment of Config. We use locale to distinguish different configs.

Why config Name uses the same name?

We want to use the same set of code, but read different files, and we want the file names to be different. Therefore, different files are distinguished by locale. We will introduce the logic of locale in detail later.

config:
  # default
  - name: my-config
    locale: "*::*::*::*"
    path: config/default.yaml
  # If the environment variable REGION=beijing, read this file
  - name: my-config
    locale: "*::beijing::*::*"
    path: config/beijing.yaml
  # If the environment variable REGION=shanghai, read this file
  - name: my-config
    locale: "*::shanghai::*::*"
    path: config/shanghai.yaml
gf:
  - name: greeter
    port: 8080
    enabled: true

3. Create main go

Set the environment variable: REGION="beijing", and then read the configuration file, config / Beijing Yaml will be read.

// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package main

import (
	"context"
	"fmt"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gf"
	"os"
)

// Application entrance.
func main() {
	// Set REGION=beijing
	os.Setenv("REGION", "beijing")

	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Load config which is config/beijing.yaml
	fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("my-region"))

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

4. Folder structure

$ tree
.
├── boot.yaml
├── config
│   ├── beijing.yaml
│   ├── default.yaml
│   └── shanghai.yaml
├── go.mod
├── go.sum
└── main.go

5. Verification

$ go run main.go

We will get the following output:

beijing

6. No matching environment variables found

If Region = "not matched", i.e. no matching environment variable is found, the default configuration file (config/default.yaml) is read. Because config / default The locale attribute of yaml is *:: *:: *::*

// Application entrance.
func main() {
    // Set REGION=not-matched
    os.Setenv("REGION", "not-matched")
    
    ...
    // Load config which is config/default.yaml
    fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("my-region"))
    ...
}

We will get the following output:

$ go run main.go
default

7. The environment variable is not configured

If we do not configure the REGION environment variable, we will read config / default Yaml file.

// Application entrance.
func main() {
    ...
    // Load config which is config/beijing.yaml
    fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("my-region"))
    ...
}

We will get the following output:

$ go run main.go
default

concept

Rk boot uses four environment variables: real, REGION, AZ and DOMAIN to distinguish configuration files.

These four environment variables can be arbitrary values.

Best practices

For example, we have a cloud album business. If the IP address of MySQL used by this service in different environments is different, it can be configured in this way.

framework

It is assumed that our business has servers in [Beijing] and [Shanghai]. At the same time, in order to improve service availability, we have opened two districts in [Beijing] and [Shanghai].

At this time, we can configure the following environment variables on the machine, which can be set in batch through Ansible and other tools.

environment

Corresponding environment variable

Beijing, zone 1, test

REALM="cloud-album",REGION="bj",AZ="bj-1",DOMAIN="test"

Beijing, District 1, online

REALM="cloud-album",REGION="bj",AZ="bj-1",DOMAIN="prod"

Beijing, zone 2, test

REALM="cloud-album",REGION="bj",AZ="bj-2",DOMAIN="test"

Beijing, District 2, online

REALM="cloud-album",REGION="bj",AZ="bj-2",DOMAIN="prod"

Shanghai, zone 1, test

REALM="cloud-album",REGION="sh",AZ="sh-1",DOMAIN="test"

Shanghai, District 1, online

REALM="cloud-album",REGION="sh",AZ="sh-1",DOMAIN="prod"

Shanghai, zone 2, test

REALM="cloud-album",REGION="sh",AZ="sh-2",DOMAIN="test"

District 2, Shanghai Online

REALM="cloud-album",REGION="sh",AZ="sh-2",DOMAIN="prod"

At the same time, if we do not use services such as ETCD and consult to pull configuration files remotely, we can directly add the following files to the machine. Each file has a different MySQL IP address.

Folder structure

.
├── boot.yaml
├── config
│   ├── bj-1-test.yaml
│   ├── bj-1-prod.yaml
│   ├── bj-2-test.yaml
│   ├── bj-2-prod.yaml
│   ├── sh-1-test.yaml
│   ├── sh-1-prod.yaml
│   ├── sh-2-test.yaml
│   ├── sh-2-prod.yaml
│   └── default.yaml
├── go.mod
├── go.sum
└── main.go

boot.yaml

Next, we are in boot Add the following config entry to yaml.

config:
  # Default entry
  - name: my-config
    locale: "*::*::*::*"
    path: config/default.yaml
  # Beijing, zone 1, test environment
  - name: my-config
    locale: "cloud-album::bj::bj-1::test"
    path: config/bj-1-test.yaml
  # Beijing, first district, online environment
  - name: my-config
    locale: "cloud-album::bj::bj-1::prod"
    path: config/bj-1-prod.yaml
  # Beijing, zone 2, test environment
  - name: my-config
    locale: "cloud-album::bj::bj-2::test"
    path: config/bj-2-test.yaml
  # Beijing, zone 2, online environment
  - name: my-config
    locale: "cloud-album::bj::bj-2::prod"
    path: config/bj-2-prod.yaml
  # Shanghai, zone 1, test environment
  - name: my-config
    locale: "cloud-album::sh::sh-1::test"
    path: config/sh-1-test.yaml
  # Shanghai, first district, online environment
  - name: my-config
    locale: "cloud-album::sh::sh-1::prod"
    path: config/sh-1-prod.yaml
  # Shanghai, zone 2, test environment
  - name: my-config
    locale: "cloud-album::sh::sh-2::test"
    path: config/sh-2-test.yaml
  # Shanghai, zone 2, online environment
  - name: my-config
    locale: "cloud-album::sh::sh-2::prod"
    path: config/sh-2-prod.yaml
gf:
  - name: greeter
    port: 8080
    enabled: true

main.go to read the configuration file.

Because all configs are named my Config in main When reading in go, we can use my Config to get configeentry.

package main

import (
	"context"
	"fmt"
	"github.com/rookie-ninja/rk-boot"
	"os"
)

// Application entrance.
func main() {
	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Get viper instance based on environment variable
	boot.GetConfigEntry("my-config").GetViper()

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

Override with environment variables

Rk boot integrates viper To process the configuration file, so it is naturally integrated viper All the functions that come with it.

This includes overwriting the existing configuration [value] through [environment variable]. Let's take an example.

1.config/default.yaml

In config / default Yaml file, add a K/V.

---
endpoint: 8.8.8.8

2.main.go

Under normal circumstances, the following code will get [8.8.8.8], but we override the value of [endpoint] through the environment variable. Note that the Key of the environment variable needs to be in uppercase English when overridden by the environment variable.

// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package main

import (
	"context"
	"fmt"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gf"
	"os"
)

// Application entrance.
func main() {
	// Set ENDPOINT=localhost
	os.Setenv("ENDPOINT", "localhost")

	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Load config which is config/default.yaml
	fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("endpoint"))

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}

3. Verification

$ go run main.go
localhost

4. Use environment variable prefix

In the actual environment, there may be the problem of environment variable conflict. At this time, we can configure an [environment variable prefix] in Viper to mark our Config.

For example, suppose that the system has initialized HOSTNAME into each machine as an environment variable. If we forcibly modify this value, we will encounter unpredictable errors. At this time, we can add a prefix.

example:

  • config/default.yaml
---
hostname: my-hostname
  • boot.yaml In the config option, add our ENV prefix.
config:
  - name: my-config
    locale: "*::*::*::*"
    path: config/default.yaml
    envPrefix: rk
gf:
  - name: greeter
    port: 8080
    enabled: true
  • main.go At this time, when overriding the HOSTNAME through the environment variable, we add RK_ As a prefix.

Refer to viper official documents

// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package main

import (
	"context"
	"fmt"
	"github.com/rookie-ninja/rk-boot"
	_ "github.com/rookie-ninja/rk-boot/gf"
	"os"
)

// Application entrance.
func main() {
	// Set RK_HOSTNAME=override-hostname
	os.Setenv("RK_HOSTNAME", "override-hostname")

	// Create a new boot instance.
	boot := rkboot.NewBoot()

	// Load config which is config/default.yaml
	fmt.Println(boot.GetConfigEntry("my-config").GetViper().GetString("hostname"))

	// Bootstrap
	boot.Bootstrap(context.Background())

	// Wait for shutdown sig
	boot.WaitForShutdownSig(context.Background())
}
  • verification
$ go run main.go
override-hostname

Topics: Go Microservices goframe