メインコンテンツまでスキップ

設定

この章では、Go-Sailを利用した設定ファイルのパース方法やホットアップデートについて紹介します。

はじめに

設定(コンフィギュレーション)はあらゆるソフトウェアサービスの「魂」であり、プログラムがどのように動作し、振る舞うかを決定します。Go-Sailの文脈においては、非常に多岐に渡る設定オプションが用意されています。前述の例のように全てハードコーディングに頼るのは、標準的でも実用的でもなく、セキュリティ面でも大きなリスクがあります。一般的には設定(設定ファイル)はソースコードと分離されて管理し、アプリケーションの適応性やセキュリティ性を高めることが推奨されます。

分離

堅牢なソフトウェア設計では、サービスを提供するために設定ファイルから値を読み取る機能が必要不可欠です。よって、ソースコードと設定ファイルをしっかり分離する必要があります。

Go-Sailは以下の3つの代表的な設定ファイル形式をサポートしています:

  • YAML
  • TOML
  • JSON

これらに優劣はなく、開発者やチームの好み・スタイルに応じて利用できます。

ゼロから始める

最初は設定ファイルが存在しない場合や、空のままの場合もあるでしょう。その都度一つひとつ設定項目を手動で宣言していくのは現実的ではありません。そこでGo-Sailが提供するスキャフォールディング機能を用いて、設定ファイルのテンプレートを自動生成しましょう。

configuration_test.go
import (
"testing"
"github.com/keepchen/go-sail/v3/sail/config"
)

func TestGivemeConfigurationTemplate(t *testing.T) {
config.PrintTemplateConfig("yaml", "./go-sail.config.local.yaml")
}

ここではmain.goではなく、configuration_test.goという新しいファイルを作成しています。これにより、GoLandやVisual Studio CodeといったIDE上で直接テストとして実行できるようになります。

上記を実行すると、現在のディレクトリに go-sail.config.local.yaml という設定ファイルが生成されます。このファイルにはGo-Sailのコアとなる設定項目が含まれますが、全て埋める必要はなく、利用したい項目だけ値を入力し、他は削除しても問題ありません。

生成される内容はおおよそ以下の通りです:

config.example.yaml
http_conf:
debug: false
addr: ""
swagger_conf:
enable: false
redoc_ui_path: ""
json_path: ""
favicon_path: ""
prometheus_conf:
enable: false
addr: ""
access_path: ""
disable_system_sample: false
disk_path: ""
sample_interval: ""
websocket_route_path: ""
trusted_proxies: []
logger_conf:
console_output: false
env: ""
level: ""
modules: []
filename: ""
max_size: 0
max_backups: 0
compress: false
exporter:
provider: ""
redis:
list_key: ""
conn_conf:
endpoint:
host: ""
port: 0
username: ""
password: ""
enable: false
database: 0
ssl_enable: false
cluster_conn_conf:
enable: false
ssl_enable: false
endpoints: []
nats:
subject: ""
conn_conf:
enable: false
endpoints: []
username: ""
password: ""
kafka:
topic: ""
conn_conf:
enable: false
endpoints: []
SASLAuthType: ""
username: ""
password: ""
timeout: 0
db_conf:
enable: false
driver_name: ""
auto_migrate: false
disable_foreign_key_constraint_when_migrating: false
disable_nested_transaction: false
allow_global_update: false
skip_default_transaction: false
logger:
level: ""
slow_threshold: 0
skip_caller_lookup: false
ignore_record_not_found_error: false
colorful: false
connection_pool:
max_open_conn_count: 0
max_idle_conn_count: 0
conn_max_life_time_minutes: 0
conn_max_idle_time_minutes: 0
mysql:
read:
host: ""
port: 0
username: ""
password: ""
database: ""
charset: ""
parseTime: false
loc: ""
write:
host: ""
port: 0
username: ""
password: ""
database: ""
charset: ""
parseTime: false
loc: ""
postgres:
read:
host: ""
port: 0
username: ""
password: ""
database: ""
ssl_mode: ""
timezone: ""
write:
host: ""
port: 0
username: ""
password: ""
database: ""
ssl_mode: ""
timezone: ""
sqlserver:
read:
host: ""
port: 0
username: ""
password: ""
database: ""
write:
host: ""
port: 0
username: ""
password: ""
database: ""
sqlite:
read:
file: ""
write:
file: ""
clickhouse:
read:
host: ""
port: 0
username: ""
password: ""
database: ""
read_timeout: 0
write_timeout: 0
write:
host: ""
port: 0
username: ""
password: ""
database: ""
read_timeout: 0
write_timeout: 0
redis_conf:
endpoint:
host: ""
port: 0
username: ""
password: ""
enable: false
database: 0
ssl_enable: false
redis_cluster_conf:
enable: false
ssl_enable: false
endpoints: []
nats_conf:
enable: false
endpoints: []
username: ""
password: ""
jwt_conf: null
email_conf:
workers: 0
worker_throttle_seconds: 0
host: ""
port: 0
username: ""
password: ""
from: ""
subject: ""
params:
variables: []
kafka_conf:
conf:
enable: false
endpoints: []
SASLAuthType: ""
username: ""
password: ""
timeout: 0
topic: ""
groupID: ""
etcd_conf:
enable: false
endpoints: []
username: ""
password: ""
timeout: 0
valkey_conf:
enable: false
username: ""
password: ""
endpoints: []
ヒント

実際の内容はバージョンによって異なる場合があります。

設定値の読み込みとホットアップデート

設定ファイルの読み込み

必要な設定項目を記述したら、それを読み込むためソースコードも修正する必要があります。

まず設定用の構造体を定義し、Go-Sailのビルトイン関数で読み込ませます。

main.go
package main

import (
"fmt"
"time"

"go.uber.org/zap"
"gopkg.in/yaml.v2"

"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
"github.com/keepchen/go-sail/v3/lib/logger"
"github.com/keepchen/go-sail/v3/http/api"
sailConstants "github.com/keepchen/go-sail/v3/constants"
sailMiddleware "github.com/keepchen/go-sail/v3/http/middleware"
)

type ErrorCode int

func (v ErrorCode) Int() int {
return int(v)
}

const (
ErrUserNotExist ErrorCode = 1000
ErrUserAlreadyExist ErrorCode = 1001
ErrUsernameAndPasswordNotMatch ErrorCode = 1002
)

var codeMsgMap = sailConstants.MMBox{
//en
sailConstants.LanguageEnglish: {
ErrUserNotExist: "User not exist",
ErrUserAlreadyExist: "User already exist",
ErrUsernameAndPasswordNotMatch: "Username and password not match",
},
//zh-CN
sailConstants.LanguageChinesePRC: {
ErrUserNotExist: "ユーザーが存在しません",
ErrUserAlreadyExist: "ユーザーは既に存在します",
ErrUsernameAndPasswordNotMatch: "ユーザー名またはパスワードが正しくありません",
},
//ja
sailConstants.LanguageJapanese: {
ErrUserNotExist: "ユーザーが存在しません",
ErrUserAlreadyExist: "ユーザーは既に存在します",
ErrUsernameAndPasswordNotMatch: "ユーザー名またはパスワードが正しくありません",
},
//その他の言語
}


var once sync.Once

func init() {
once.Do(func() {
time.AfterFunc(time.Second*2, func() {
for language, msgMap := range codeMsgMap {
for code, msg := range msgMap {
sail.Code().Register(language.String(), code.Int(), msg)
}
}
})
})
}

type LoginRequest struct {
Username string `json:"username" form:"username" query:"username"`
Password string `json:"password" form:"password" query:"password"`
}

func (v LoginRequest) Validator() (sailConstants.ICodeType, error) {
if len(v.Username) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("username can not be empty")
}

if len(v.Password) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("password can not be empty")
}

return sailConstants.ErrorNone, nil
}

var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUvUDx+LPQ0S+L
+5UmtD2EJw1L953mVCMWBJktBbqPTIhDmrd33+3cNq0t7rXuALhoqZS/53nDchU1
wsCveieNDR7SsdO4HMS4bnxgyuYCkC1ugAdyvJ2FCv7xUppc7PvyIQ1gQS/nOP0w
...
vplU0p7ayaXuNF2t73k/L5f92+8VBuYECEUOXw2xST5gvkPdKGK1xM1cLT6y8TrF
RIXvUK2duHjDxiaPKtANi2P4
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1L1A8fiz0NEvi/uVJrQ9
...
KTJQ+GGzUqOGzruYQ5sM3TnU8Avb4OF36uyADBwA4bP944tKSNSET7BC3N0UerRo
QwIDAQAB
-----END PUBLIC KEY-----`)

conf config.Config


registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.Use(sailMiddleware.DetectUserAgentLanguage())

ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
if err := c.ShouldBind(&loginRequest); err != nil {
sail.LogTrace(c).Warn("リクエストパラメータのバインドに失敗", zap.Error(err))
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}

if code, err := loginRequest.Validator(); err != nil {
sail.Response(c).Wrap(code, nil, err.Error()).Send()
return
}

var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// ユーザーが存在しない
if len(loginRequest.Username) == 0 {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
// パスワード不一致
if loginRequest.Password != user.Password {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}

headers := map[string]string{
"X-Request-Id": sail.LogTrace(c).RequestID(),
}
sail.Utils().HttpClient().SendRequest("POST", "https://....", nil, headers)

token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
ginEngine.POST("/third-party/notify", func(c *gin.Context){
c.JSON(200, ...)
})
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User)
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}

sail.GetLogger("schedule").Info("logging something...")
}
)

func main() {
parseFn := func(content []byte, viaWatch bool) {
fmt.Println("config content: ", string(content))
yaml.Unmarshal(content, &conf)
}
sail.Config(nil).ViaFile("./go-sail.config.local.yaml").Parse(parseFn)

options := &api.Option{
ForceHttpCode200: true,
DetectAcceptLanguage: true,
}
sail.WakeupHttp("go-sail", conf).
SetupApiOption(options).
Hook(registerRoutes, nil, afterFunc).Launch()
}

ホットアップデート

通常、設定変更を即時反映させたい場合はサービスの再起動をしたくありません。Go-Sailが提供する設定監視ソリューションを使えば、設定変更のホットリロードにもスムーズに対応できます。

main.go
package main

import (
"fmt"
"time"

"go.uber.org/zap"
"gopkg.in/yaml.v2"

"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
"github.com/keepchen/go-sail/v3/lib/logger"
"github.com/keepchen/go-sail/v3/http/api"
sailConstants "github.com/keepchen/go-sail/v3/constants"
sailMiddleware "github.com/keepchen/go-sail/v3/http/middleware"
)

type ErrorCode int

func (v ErrorCode) Int() int {
return int(v)
}

const (
ErrUserNotExist ErrorCode = 1000
ErrUserAlreadyExist ErrorCode = 1001
ErrUsernameAndPasswordNotMatch ErrorCode = 1002
)

var codeMsgMap = sailConstants.MMBox{
//en
sailConstants.LanguageEnglish: {
ErrUserNotExist: "User not exist",
ErrUserAlreadyExist: "User already exist",
ErrUsernameAndPasswordNotMatch: "Username and password not match",
},
//zh-CN
sailConstants.LanguageChinesePRC: {
ErrUserNotExist: "ユーザーが存在しません",
ErrUserAlreadyExist: "ユーザーは既に存在します",
ErrUsernameAndPasswordNotMatch: "ユーザー名またはパスワードが正しくありません",
},
//ja
sailConstants.LanguageJapanese: {
ErrUserNotExist: "ユーザーが存在しません",
ErrUserAlreadyExist: "ユーザーは既に存在します",
ErrUsernameAndPasswordNotMatch: "ユーザー名またはパスワードが正しくありません",
},
//その他の言語
}


var once sync.Once

func init() {
once.Do(func() {
time.AfterFunc(time.Second*2, func() {
for language, msgMap := range codeMsgMap {
for code, msg := range msgMap {
sail.Code().Register(language.String(), code.Int(), msg)
}
}
})
})
}

type LoginRequest struct {
Username string `json:"username" form:"username" query:"username"`
Password string `json:"password" form:"password" query:"password"`
}

func (v LoginRequest) Validator() (sailConstants.ICodeType, error) {
if len(v.Username) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("username can not be empty")
}

if len(v.Password) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("password can not be empty")
}

return sailConstants.ErrorNone, nil
}

var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUvUDx+LPQ0S+L
+5UmtD2EJw1L953mVCMWBJktBbqPTIhDmrd33+3cNq0t7rXuALhoqZS/53nDchU1
wsCveieNDR7SsdO4HMS4bnxgyuYCkC1ugAdyvJ2FCv7xUppc7PvyIQ1gQS/nOP0w
...
vplU0p7ayaXuNF2t73k/L5f92+8VBuYECEUOXw2xST5gvkPdKGK1xM1cLT6y8TrF
RIXvUK2duHjDxiaPKtANi2P4
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1L1A8fiz0NEvi/uVJrQ9
...
KTJQ+GGzUqOGzruYQ5sM3TnU8Avb4OF36uyADBwA4bP944tKSNSET7BC3N0UerRo
QwIDAQAB
-----END PUBLIC KEY-----`)

conf config.Config

registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.Use(sailMiddleware.DetectUserAgentLanguage())

ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
if err := c.ShouldBind(&loginRequest); err != nil {
sail.LogTrace(c).Warn("リクエストパラメータのバインドに失敗", zap.Error(err))
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}

if code, err := loginRequest.Validator(); err != nil {
sail.Response(c).Wrap(code, nil, err.Error()).Send()
return
}

var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// ユーザーが存在しない
if len(loginRequest.Username) == 0 {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
// パスワード不一致
if loginRequest.Password != user.Password {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}

headers := map[string]string{
"X-Request-Id": sail.LogTrace(c).RequestID(),
}
sail.Utils().HttpClient().SendRequest("POST", "https://....", nil, headers)

token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
ginEngine.POST("/third-party/notify", func(c *gin.Context){
c.JSON(200, ...)
})
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User)
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}

sail.GetLogger("schedule").Info("logging something...")
}
)

func main() {
parseFn := func(content []byte, viaWatch bool) {
if viaWatch {
// 設定リロード時の処理をここに記述
}
fmt.Println("config content: ", string(content))
yaml.Unmarshal(content, &conf)
}
sail.
Config(parseFn).
ViaFile("./go-sail.config.local.yaml").Parse(parseFn)

options := &api.Option{
ForceHttpCode200: true,
DetectAcceptLanguage: true,
}
sail.WakeupHttp("go-sail", conf).
SetupApiOption(options).
Hook(registerRoutes, nil, afterFunc).Launch()
}

プロバイダー

Go-Sailは現在、以下3種の設定プロバイダーに対応しています。

  • ファイル
  • Nacos
  • Etcd

ファイル

parseFn := func(content []byte, viaWatch bool){
fmt.Println("config content: ", string(content))
if viaWatch {
// 設定リロード時の処理
}
}
filename := "go-sail.config.local.yaml"

sail.Config(parseFn).ViaFile(filename).Parse(parseFn)
ヒント

ファイルモード監視はファイルの更新時刻ベースで実施されます。

Nacos

parseFn := func(content []byte, viaWatch bool){
fmt.Println("config content: ", string(content))
if viaWatch {
// 設定リロード時の処理
}
}

endpoints := "endpoint1,endpoint2"
namspaceID := ""
groupName := ""
dataID := "go-sail.config.local.yaml"

sail.Config(true, parseFn).ViaNacos(endpoints, namespaceID, groupName, dataID).Parse(parseFn)

Etcd

parseFn := func(content []byte, viaWatch bool){
fmt.Println("config content: ", string(content))
if viaWatch {
// 設定リロード時の処理
}
}
etcdConf := etcd.Conf{
Endpoints: []string{""},
Username: "",
Password: "",
}
key := "go-sail.config.local.yaml"

sail.Config(parseFn).ViaEtcd(etcdConf, key).Parse(parseFn)

コンポーネントとして起動

NacosやEtcdで設定を取得する場合、コンポーネントとして有効化するかどうかをブーリアンで指定できます。有効化もとても簡単で、第2引数に true を渡すだけです。

parseFn := func(content []byte, viaWatch bool){
fmt.Println("config content: ", string(content))
if viaWatch {
// 設定リロード時の処理
}
}
etcdConf := etcd.Conf{
Endpoints: []string{""},
Username: "",
Password: "",
}
key := "go-sail.config.local.yaml"

sail.
Config(parseFn, true).
ViaEtcd(etcdConf, key).Parse(parseFn)

その後、コントロールプレーン経由で下記のようにアクセスできます。

sail.GetEtcdInstance()