使用viper实现yaml配置文件的合并

本文永久链接 – https://tonybai.com/2022/09/20/use-viper-to-do-merge-of-yml-configuration-files

作为小厂,我们的基础设施还不够完备,项目经理中秋节通知我们的系统近期要上second-to-last stage环境和生产环境,于是从运维人员部署效率方面考量,我们紧急开发了一个一键安装脚本生成工具,这样运维人员便可以利用该工具结合实际目标环境生成一键安装脚本。这个工具的原理十分简单,如下示意图所示:

从上图可以知道,我们的工具是基于模板定制最终的配置与安装脚本的,其中:

  • templates/conf下面是服务配置;
  • templates/manifests下面是服务的k8s yaml脚本;
  • custom/configure文件存储的是针对templates/conf下面服务配置的定制化配置数据;
  • custom/manifests文件存储的是针对templates/manifests下面k8s yaml的定制化配置数据;
  • templates/install.sh则是安装脚本。

custom目录下的两个存储定制化配置的文件是与目标环境紧密相关的

提到template,Gopher们首先想到的是Go text/template技术,利用模板语法编写上面templates目录下的模板配置文件。不过基于text/template就需要我们事先将所有需要定制化的变量都一一识别出来,这个量有些大,且不够灵活。

那我们还可以采用什么技术方案呢?我最终选择了yaml文件合并(包括覆盖与追加)的方案,该方案示意图如下:

这个示例包含了覆盖和(追加)合并两种情况,我们首先看一下覆盖。

  • custom/manifests.yml中配置覆盖templates/manifests/*.yaml的配置

以templates/manifests/a.yml为例,该模板中metadata.name的默认值为default,但运维人员根据目标环境定制了(customizing)custom/manifests.yml文件。在该文件中,a.yml文件名作为key值,然后将要覆盖的配置项的全路径配置到该文件中(这里的全路径为metadata.name):

a.yml:
  metadata:
    name: foo

custom/manifests.yml文件中对namespace name的修改值foo将会覆盖原模板中的default,这在最终的xx_install/manifests/a.yml中会体现出来:

// a.yml
apiVersion: v1
kind: Namespace
metadata:
  name: foo
  • custom/manifests.yml中配置追加到templates/manifests/*.yaml配置中

对于原模板文件中没有而custom中新增的配置,会追加到最终生成的配置文件中,以b.yml为例。原模板目录下的b.yml内容如下:

// templates/manifests/b.yml
log:
  type: file
  level: 0
  compress: true

这里log下仅有三个子配置项:type、level和compress。

而运维在custom/manifests.yml为log增加了其他若干种配置,比如access_log、error_log等:

// custom/manifests.yml
b.yml:
  log:
    level: 1
    compress: false
    access_log: "access.log"
    error_log: "error.log"
    max_age: 3
    maxbackups: 7
    maxsize: 100

这样,除了level、compress会覆盖原模板中的值之外,其余新增的配置都会追加到生成的xx_install/manifests/b.yml中会体现出来:

// b.yml
log:
  type: file
  level: 1
  compress: false
  access_log: "access.log"
  error_log: "error.log"
  max_age: 3
  maxbackups: 7
  maxsize: 100

好了!方案确定了,那如何实现yaml文件的合并呢?Go社区的yaml包要数https://github.com/go-yaml/yaml(Canonical import paths为gopkg.in/yaml.v2或gopkg.in/yaml.v3)最为知名,这个包实现了YAML 1.2规范,可以方便实现Yaml与go struct之间的marshal与unmarshal。不过,yaml包提供的接口都比较初级,要想实现yaml文件的合并,还需要自己做较多额外工作,时间上可能不允许了。那有没有现成可用的工具呢?答案是有的,它就是在Go社区大名鼎鼎的viper

viper是由gohugo作者、前Go语言项目组产品经理Steve Francia开发的开源Go应用配置框架。viper不仅支持命令行参数传入配置,还支持从各种类型配置文件、环境变量、远程配置系统(etcd等)等获取配置。除此之外,viper还支持配置文件的merge和对配置文件的写入操作。

我们是否可以直接使用viper的Merge系列操作呢?答案是不能!为什么呢?因为这与我们上面的设计有关。我们将与环境有关的配置都放入了custom/manifests.yml这一个文件中了,这与一merge就会导致custom/manifests.yml中的配置数据出现在每一个最终生成的templates/xx.yml配置文件中。

那我们就自行来实现一套merge(覆盖和追加)操作!

我们先来看驱动merge的main函数:

// github.com/bigwhite/experiments/tree/master/yml-merge-using-viper/main.go

var (
    sourceDir string
    dstDir    string
)

func init() {
    flag.StringVar(&sourceDir, "s", "./", "template directory path")
    flag.StringVar(&dstDir, "d", "./k8s-install", "the target directory path in which the generated files are put")
}

func main() {
    var err error
    flag.Parse()

    // create target directory if not exist
    err = os.MkdirAll(dstDir+"/conf", 0775)
    if err != nil {
        fmt.Printf("create %s error: %s\n", dstDir+"/conf", err)
        return
    }

    err = os.MkdirAll(dstDir+"/manifests", 0775)
    if err != nil {
        fmt.Printf("create %s error: %s\n", dstDir+"/manifests", err)
        return
    }

    // override manifests files with same config item in custom/manifests.yml,
    // store the final result to the target directory
    err = mergeManifestsFiles()
    if err != nil {
        fmt.Printf("override and generate manifests files error: %s\n", err)
        return
    }
    fmt.Printf("override and generate manifests files ok\n")
}

我们看到main包利用标准库flag包创建了两个命令行参数-s和-d,分别代表存放templates/custom的源路径和存储生成文件的目标路径。进入main函数后,我们首先在目标路径下建立manifests和conf目录用于分别存储相关配置文件(本例中,conf目录下不生成任何文件),然后main函数调用mergeManifestsFiles对源路径下的templates/manifests中的yml文件与custom/manifests.yml进行合并:

// github.com/bigwhite/experiments/tree/master/yml-merge-using-viper/main.go

var (
    manifestFiles = []string{
        "a.yml",
        "b.yml",
    }
)

func mergeManifestsFiles() error {
    for _, file := range manifestFiles {
        // check whether the file exist
        srcFile := sourceDir + "/templates/manifests/" + file
        _, err := os.Stat(srcFile)
        if os.IsNotExist(err) {
            fmt.Printf("%s not exist, ignore it\n", srcFile)
            continue
        }

        err = mergeConfig("yml", sourceDir+"/templates/manifests", strings.TrimSuffix(file, ".yml"),
            sourceDir+"/custom", "manifests", dstDir+"/manifests/"+file)
        if err != nil {
            fmt.Println("mergeConfig error: ", err)
            return err
        }
        fmt.Printf("mergeConfig %s ok\n", file)

    }
    return nil
}

我们看到mergeManifestsFiles遍历模板文件,并针对每个文件调用一次真正进行yml文件merge的函数mergeConfig:

// github.com/bigwhite/experiments/tree/master/yml-merge-using-viper/main.go

func mergeConfig(configType, srcPath, srcFile, overridePath, overrideFile, target string) error {
    v1 := viper.New()
    v1.SetConfigType(configType) // e.g. "yml"
    v1.AddConfigPath(srcPath)    // file directory
    v1.SetConfigName(srcFile)    // filename(without postfix)
    err := v1.ReadInConfig()
    if err != nil {
        return err
    }

    v2 := viper.New()
    v2.SetConfigType(configType)
    v2.AddConfigPath(overridePath)
    v2.SetConfigName(overrideFile)
    err = v2.ReadInConfig()
    if err != nil {
        return err
    }

    overrideKeys := v2.AllKeys()

    // override special keys
    prefixKey := srcFile + "." + configType + "." // e.g "a.yml."
    for _, key := range overrideKeys {
        if !strings.HasPrefix(key, prefixKey) {
            continue
        }

        stripKey := strings.TrimPrefix(key, prefixKey)
        val := v2.Get(key)
        v1.Set(stripKey, val)
    }

    // write the final result after overriding
    return v1.WriteConfigAs(target)
}

我们看到:mergeConfig函数针对templates/manifests下的文件和custom下的manifests.yml文件创建了两个viper实例(viper.New())并分别加载各自的配置数据。然后遍历custom下manifests.yml中的key,将符合要求的配置项的值set到代表对templates/manifests下文件的viper实例中,最后我们将merge后的viper实例数据写到目标文件中。

编译运行该生成工具:

$make
go build
$./generator
mergeConfig a.yml ok
mergeConfig b.yml ok
override and generate manifests files ok

在默认命令行参数的情况下,文件被生成在k8s-install路径下,我们查看一下生成的文件:

$cat k8s-install/manifests/a.yml
apiversion: v1
kind: Namespace
metadata:
    name: foo

$cat k8s-install/manifests/b.yml
log:
    access_log: access.log
    compress: false
    error_log: error.log
    level: 1
    max_age: 3
    maxbackups: 7
    maxsize: 100
    type: file

我们看到merge的结果与我们预期的一致(字段顺序不一致没关系,这与viper内部存储key-value时使用go map有关,go map的遍历顺序是随机的)。

不过细心的朋友可能会发现一处问题:那就是a.yml中原先的apiVersion在结果文件中变成了小写的apiversion,这会a.yml在提交给k8s时校验失败!

为什么会这样呢?viper官方给出的说明如下(机翻):

Viper合并了来自不同来源的配置,其中许多配置是不区分大小写的,或者使用与其他来源不同的大小写(例如,env vars)。为了在使用多个资源时提供最佳体验,我们决定让所有按键不区分大小写。

已经有一些人试图实现大小写敏感,但不幸的是,这不是那么简单的事情。我们可能会在Viper v2中试着实现它。。。。

好吧,既然官方说在v2可能支持,但v2又遥遥无期,我们就用viper的fork版本来解决这个问题吧!开发者lnashier曾因这个大小写问题fork过一份viper代码并fix了这个问题,虽然比较old(且可能改的不全面),但能满足我们的要求就行!我们来试试将spf13/viper换为lnashier/viper,并重新构建和执行generator:

$go mod tidy
go: finding module for package github.com/lnashier/viper
go: found github.com/lnashier/viper in github.com/lnashier/viper v0.0.0-20180730210402-cc7336125d12

$make clean
rm -fr generator k8s-install

$make
go build 

$./generator
mergeConfig a.yml ok
mergeConfig b.yml ok
override and generate manifests files ok

$cat k8s-install/manifests/a.yml
apiVersion: v1
kind: Namespace
metadata:
  name: foo

$cat k8s-install/manifests/b.yml
log:
  access_log: access.log
  compress: false
  error_log: error.log
  level: 1
  max_age: 3
  maxbackups: 7
  maxsize: 100
  type: file

我们看到更换为lnashier/viper后,a.yml中的apiVersion这个key没有再被改为小写。

这个工具基本可以使用了。但是这个工具是否没有问题了呢?很遗憾不是的!当generator面对下面的两种形式的配置文件时就会生成错误的文件:

//c.yml

apiVersion: v1
data:
  .dockerconfigjson: xxxxyyyyyzzz==
kind: Secret
type: kubernetes.io/dockerconfigjson
metadata:
  name: mysecret
  namespace: foo

//d.yml

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
  namespace: foo
data:
  my-nginx.conf: |
    server {
          listen 80;
          client_body_timeout 60000;
          client_max_body_size 1024m;
          send_timeout 60000;
          proxy_headers_hash_bucket_size 1024;
          proxy_headers_hash_max_size 4096;
          proxy_read_timeout 60000;
          location /dashboard {
             proxy_pass http://localhost:8081;
          }
    }

这两个问题就比较棘手了,lnashier/viper也无法解决。我也只能fork lnashier/viper到bigwhite/viper自己解决这个问题,并且像d.yml这样的配置形式十分特化,不具有通用性,因此bigwhite/viper并不具有通用性,这里就不细说了,有兴趣的朋友可以自行阅读代码(commit diff)来查看解决上述问题的方法。

本文涉及的代码可以从这里下载。


后记:

kustomize是k8s官方工具,它可以让你基于k8s resource模板YAML文件(类似本文的templates/manifests目录下的文件)并结合kustomization.yaml(类似custom/manifests.yaml)为多种目的定制YAML文件,原始的YAML不会进行任何改动。

不过它的目标仅仅是k8s相关的yaml文件,对于我们的业务服务配置可能无能为力。

CUE是这两年流行起来的一种强大的声明性配置语言,它由前Go核心团队成员Marcel van Lohuizen创建,他曾与人合作创建了Borg配置语言(BCL)–在谷歌用于部署所有应用程序的语言。CUE是谷歌多年编写配置语言经验的结晶,旨在改善开发者的体验,同时避免一些陷阱。它是JSON的超集且还具有额外的功能特性。Docker之父Solomon Hykes的新创业项目dagger大量使用CUE,阿里力推的企业云原生应用管理平台kubevela也是CUE的重度用户。

关于如何使用CUE来替代我上述的方案,还待后续深入研究。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如何像gitlab-runner那样将Go应用安装为系统服务

本文永久链接 – https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner

《让reviewdog支持gitlab-push-commit,守住代码质量下限》一文中,gitlab-runner(一个Go语言开发的应用)通过自身提供的install命令将自己安装为了一个系统服务(如下面步骤):

# Create a GitLab CI user
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

# Install and run as service
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

在主流新版linux上(其他os或linux上的旧版守护服务管理器如sysvinit、upstart等,我们暂不care),系统服务就是由systemd管理的daemon process(守护进程)。

systemd是什么?linux主机上电后,os内核被加载并启动,os内核完成初始化以后,由内核第一个启动的程序是init程序,其PID(进程ID)为1,它为系统里所有进程的“祖先”,systemd便是主流新版linux中的那个init程序,它负责在主机启动后拉起所有安装为系统服务的程序

这些被systemd拉起的服务程序以守护进程(daemon process)的形式运行,那什么又是守护进程呢?《UNIX环境高级编程3rd(Advanced Programming in the UNIX Environment)》一书中是这样定义的:

Daemons are processes that live for a long time. They are often started when the system is bootstrapped and terminate only when the system is shut down. Because they don’t have a controlling terminal, we say that they run in the background. UNIX systems have numerous daemons that perform day-to-day activities.

守护进程是长期存在的进程。它们通常在系统启动时被启动,并在系统关闭时才终止。因为它们没有控制终端,我们说它们是在后台运行的。UNIX系统有许多执行日常活动的守护进程。

该书还提供了一个用户层应用程序将自己变为守护进程的标准步骤(编码规则(coding rules)),并给出了一个C语言示例:

#include "apue.h"
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>

void
daemonize(const char *cmd)
{
    int i, fd0, fd1, fd2;
    pid_t pid;
    struct rlimit rl;
    struct sigaction sa;

    /*
     * Clear file creation mask.
     */
    umask(0);
    /*
     * Get maximum number of file descriptors.
     */
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
        err_quit("%s: can’t get file limit", cmd);
    /*
     * Become a session leader to lose controlling TTY.
     */
    if ((pid = fork()) < 0)
        err_quit("%s: can’t fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);
    setsid();

    /*
     * Ensure future opens won’t allocate controlling TTYs.
     */
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);

    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0)
        err_quit("%s: can’t ignore SIGHUP", cmd);
    if ((pid = fork()) < 0)
        err_quit("%s: can’t fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);
    /*
     * Change the current working directory to the root so
     * we won’t prevent file systems from being unmounted.
     */
    if (chdir("/") < 0)
        err_quit("%s: can’t change directory to /", cmd);
    /*
     * Close all open file descriptors.
     */
    if (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    for (i = 0; i < rl.rlim_max; i++)
        close(i);
    /*
     * Attach file descriptors 0, 1, and 2 to /dev/null.
     */
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);
    /*
     * Initialize the log file.
     */
    openlog(cmd, LOG_CONS, LOG_DAEMON);
    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        syslog(LOG_ERR, "unexpected file descriptors %d %d %d",
          fd0, fd1, fd2);
        exit(1);
    }
}

那么,Go应用程序是否可以参考上面的转换步骤将自己转换为一个守护进程呢?很遗憾!Go团队说很难做到。Go社区倒是有很多第三方的方案,比如像go-daemon这样的第三方实现,不过我并没有验证过这些方案,不保证完全ok。

Go团队推荐通过像systemd这样的init system来实现Go程序的守护进程转换。gitlab-runner就是将自己安装为system服务,并由systemd对其进行管理的。

题外话:其实,自从有了容器技术(比如:docker)后,daemon service(守护进程服务)的需求似乎减少了。因为使用-d选项运行容器,应用本身就运行于后台,使用–restart=always/on-failure选项,容器引擎(比如docker engine)会帮我们管理service,并在service宕掉后重启service。

那么,我们如何像gitlab-runner那样将自己安装为一个systemd service呢?我们继续向下看。

注意:这里只是将Go应用安装成一个systemd service,并不是自己将自己转换为守护进程,安装为systemd service本身是可行的,也是安全的。

翻看gitlab-runner源码,你会发现gitlab-runner将自己安装为系统服务全依仗于github.com/kardianos/service这个Go包,这个包是Go标准库database包维护者之一Daniel Theophanes开源的系统服务操作包,该包屏蔽了os层的差异,为开发人员提供了相对简单的Service操作接口,包括下面这些控制动作:

// github.com/kardianos/service/blob/master/service.go
var ControlAction = [5]string{"start", "stop", "restart", "install", "uninstall"}

好了,下面我们就用一个例子myapp来介绍一下如何利用kardianos/service包让你的Go应用具备将自己安装为system service的能力

myapp是一个http server,它在某个端口上提供服务,当收到请求时,返回”Welcome”字样的应答:

// https://github.com/bigwhite/experiments/blob/master/system-service/main.go

func run(config string) error {
    ... ...

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Printf("[%s]: receive a request from: %s\n", c.Server.Addr, r.RemoteAddr)
        w.Write([]byte("Welcome"))
    })
    fmt.Printf("listen on %s\n", c.Server.Addr)
    return http.ListenAndServe(c.Server.Addr, nil)
}

现在我们要为myapp增加一些能力,让它支持将自己安装为systemd service,并可以通过subcommand启动(start)、停止(stop)和卸载(uninstall)systemd service。

我们首先通过os包和flag包为该程序增加subcommand和其参数的解析能力。我们不使用第三方命令行参数解析包,只是用标准库的flag包。由于myapp支持subcommand,我们需要为每个带命令行参数的subcommand单独申请一个FlagSet实例,如下面代码中的installCommand和runCommand。每个subcommand的命令行参数也要绑定到各自subcommand对应的FlagSet实例上,比如下面代码init函数体中的内容。

另外由于使用了subcommand,默认的flag.Usage不再能满足我们的要求了,我们需要自己实现一个usage函数并赋值给flag.Usage:

// https://github.com/bigwhite/experiments/blob/master/system-service/main.go

var (
    installCommand = flag.NewFlagSet("install", flag.ExitOnError)
    runCommand     = flag.NewFlagSet("run", flag.ExitOnError)
    user           string
    workingdir     string
    config         string
)

const (
    defaultConfig = "/etc/myapp/config.ini"
)

func usage() {
    s := `
USAGE:
   myapp command [command options] 

COMMANDS:
     install               install service
     uninstall             uninstall service
     start                 start service
     stop                  stop service
     run                   run service

OPTIONS:
     -config string
        config file of the service (default "/etc/myapp/config.ini")
     -user string
        user account to run the service
     -workingdir string
        working directory of the service`

    fmt.Println(s)
}

func init() {
    installCommand.StringVar(&user, "user", "", "user account to run the service")
    installCommand.StringVar(&workingdir, "workingdir", "", "working directory of the service")
    installCommand.StringVar(&config, "config", "/etc/myapp/config.ini", "config file of the service")
    runCommand.StringVar(&config, "config", defaultConfig, "config file of the service")
    flag.Usage = usage
}

func main() {
    var err error
    n := len(os.Args)
    if n <= 1 {
        fmt.Printf("invalid args\n")
        flag.Usage()
        return
    }

    subCmd := os.Args[1] // the second arg

    // get Config
    c, err := getServiceConfig(subCmd)
    if err != nil {
        fmt.Printf("get service config error: %s\n", err)
        return
    }
... ...
}

这些都完成后,我们在getServiceConfig函数中获取即将安装为systemd service的本服务的元配置信息:

// https://github.com/bigwhite/experiments/blob/master/system-service/config.go

func getServiceConfig(subCmd string) (*service.Config, error) {
    c := service.Config{
        Name:             "myApp",
        DisplayName:      "Go Daemon Service Demo",
        Description:      "This is a Go daemon service demo",
        Executable:       "/usr/local/bin/myapp",
        Dependencies:     []string{"After=network.target syslog.target"},
        WorkingDirectory: "",
        Option: service.KeyValue{
            "Restart": "always", // Restart=always
        },
    }   

    switch subCmd {
    case "install":
        installCommand.Parse(os.Args[2:])
        if user == "" {
            fmt.Printf("error: user should be provided when install service\n")
            return nil, errors.New("invalid user")
        }
        if workingdir == "" {
            fmt.Printf("error: workingdir should be provided when install service\n")
            return nil, errors.New("invalid workingdir")
        }
        c.UserName = user
        c.WorkingDirectory = workingdir

        // arguments
        // ExecStart=/usr/local/bin/myapp "run" "-config" "/etc/myapp/config.ini"
        c.Arguments = append(c.Arguments, "run", "-config", config)
    case "run":
        runCommand.Parse(os.Args[2:]) // parse config
    }   

    return &c, nil
}

这里要注意的是service.Config中的Option和Arguments,前者用于在systemd service unit配置文件中放置任意的键值对(比如这里的Restart=always),而Arguments则会被组成为ExecStart键的值,该值会在start service时传入使用。

接下来,我们便利用service包基于加载的Config创建操作服务的实例(srv),然后将它和subCommand一并传入runServiceControl实现对systemd service的控制(如下面代码)。

// https://github.com/bigwhite/experiments/blob/master/system-service/main.go
func main() {

    // ... ...
    c, err := getServiceConfig(subCmd)
    if err != nil {
        fmt.Printf("get service config error: %s\n", err)
        return
    }

    prg := &NullService{}
    srv, err := service.New(prg, c)
    if err != nil {
        fmt.Printf("new service error: %s\n", err)
        return
    }

    err = runServiceControl(srv, subCmd)
    if err != nil {
        fmt.Printf("%s operation error: %s\n", subCmd, err)
        return
    }

    fmt.Printf("%s operation ok\n", subCmd)
    return
}

func runServiceControl(srv service.Service, subCmd string) error {
    switch subCmd {
    case "run":
        return run(config)
    default:
        return service.Control(srv, subCmd)
    }
}

好了,代码已经完成!现在让我们来验证一下myapp的能力。

我们先来完成编译和二进制程序的安装:

$make
go build -o myapp main.go config.go

$sudo make install
cp ./myapp /usr/local/bin
$sudo make install-cfg
mkdir -p /etc/myapp
cp ./config.ini /etc/myapp

接下来,我们就来将myapp安装为systemd的服务:

$sudo ./myapp install -user tonybai -workingdir /home/tonybai
install operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: inactive (dead)

我们看到安装后,myApp已经成为了myApp.service,并处于inactive状态,其systemd unit文件/etc/systemd/system/myApp.service内容如下:

$sudo cat /etc/systemd/system/myApp.service
[Unit]
Description=This is a Go daemon service demo
ConditionFileIsExecutable=/usr/local/bin/myapp

After=network.target syslog.target 

[Service]
StartLimitInterval=5
StartLimitBurst=10
ExecStart=/usr/local/bin/myapp "run" "-config" "/etc/myapp/config.ini"

WorkingDirectory=/home/tonybai
User=tonybai

Restart=always

RestartSec=120
EnvironmentFile=-/etc/sysconfig/myApp

[Install]
WantedBy=multi-user.target

接下来,我们来启动一下该服务:

$sudo ./myapp start
start operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:30:01 CST; 5s ago
   Main PID: 623859 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.3M
     CGroup: /system.slice/myApp.service
             └─623859 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432

我们看到myApp服务成功启动,并在65432这个端口上监听!

我们利用curl向这个端口发送一个请求:

$curl localhost:65432
Welcome                                                                         

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:30:01 CST; 1min 27s ago
   Main PID: 623859 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.4M
     CGroup: /system.slice/myApp.service
             └─623859 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432
Sep 09 23:31:24 tonybai myapp[623859]: [:65432]: receive a request from: 127.0.0.1:10348

我们看到myApp服务运行正常并返回预期应答结果。

现在我们利用stop subcommand停掉该服务:

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: inactive (dead) since Fri 2022-09-09 23:33:03 CST; 3s ago
    Process: 623859 ExecStart=/usr/local/bin/myapp run -config /etc/myapp/config.ini (code=killed, signal=TERM)
   Main PID: 623859 (code=killed, signal=TERM)

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432
Sep 09 23:31:24 tonybai myapp[623859]: [:65432]: receive a request from: 127.0.0.1:10348
Sep 09 23:33:03 tonybai systemd[1]: Stopping This is a Go daemon service demo...
Sep 09 23:33:03 tonybai systemd[1]: myApp.service: Succeeded.
Sep 09 23:33:03 tonybai systemd[1]: Stopped This is a Go daemon service demo.

修改配置/etc/myapp/config.ini(将监听端口从65432改为65431),然后再重启该服务:

$sudo cat /etc/myapp/config.ini
[server]
addr=":65431"

$sudo ./myapp start
start operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:34:38 CST; 3s ago
   Main PID: 624046 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.4M
     CGroup: /system.slice/myApp.service
             └─624046 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:34:38 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:34:38 tonybai myapp[624046]: listen on :65431

从systemd的状态日志中我们看到myApp服务启动成功,并改为监听65431端口,我们访问一下该端口:

$curl localhost:65431
Welcome                                                                                                                      

$curl localhost:65432
curl: (7) Failed to connect to localhost port 65432: Connection refused

从上述结果可以看出,我们的配置更新和重启都是成功的!

我们亦可以使用myapp的uninstall功能从systemd中卸载该服务:

$sudo ./myapp uninstall
uninstall operation ok
$sudo systemctl status myApp
Unit myApp.service could not be found.

好了,到这里我们看到:在文章开始处提出的给Go应用增加将自己安装为systemd service的能力的目标已经顺利实现了。

最后小结一下:service包让我们的程序有了将自己安装为system service的能力。它也可以让你开发出将其他程序安装为一个system service的能力,不过这个作业就留给大家了:)。大家如有问题,欢迎在评论区留言。

本文涉及的代码可以在这里下载。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats