How to install Go app as system service like gitlab-runner

Permalink to this article – https://ift.tt/RyIoGJS

In the article “Let reviewdog support gitlab-push-commit and keep the lower limit of code quality” , gitlab-runner (an application developed in Go language) installs itself as a system service through the install command provided by itself (as follows):

 # 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

On mainstream new versions of linux (other os or old versions of daemon service managers on linux such as sysvinit, upstart, etc., we will not care for the time being), system services are the daemon process (daemon process) managed by systemd .

what is systemd? After the linux host is powered on, the os kernel is loaded and started. After the os kernel is initialized, the first program started by the kernel is the init program, and its PID (process ID) is 1, which is the “ancestor” of all processes in the system. , systemd is the init program in the mainstream new version of linux, which is responsible for pulling up all the programs installed as system services after the host is started .

These service programs pulled up by systemd run in the form of daemon processes, so what are daemon processes? The book “Advanced Programming in the UNIX Environment 3rd (Advanced Programming in the UNIX Environment)” defines it as follows:

 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系统有许多执行日常活动的守护进程。

The book also provides standard steps (coding rules) for a userland application to daemonize itself, and gives an example in 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); } }

So, can a Go application convert itself into a daemon by referring to the conversion steps above? Unfortunately! The Go team says it’s hard to do . There are many third-party solutions in the Go community, such as third-party implementations like go-daemon , but I have not verified these solutions, and I cannot guarantee that they are completely ok.

The Go team recommends daemonizing Go programs through an init system like systemd . gitlab-runner installs itself as a system service and manages it by systemd.

Digression: In fact, since the advent of container technology (eg: docker), the need for daemon services seems to have decreased. Because the -d option is used to run the container, the application itself runs in the background. With the –restart=always/on-failure option, the container engine (such as docker engine) will help us manage the service and restart the service after the service goes down.

So how do we install ourselves as a systemd service like gitlab-runner does? We continue to look down.

Note: This is just to install the Go application as a systemd service, not to convert itself into a daemon process. It is feasible and safe to install it as a systemd service itself.

Looking at the source code of gitlab-runner, you will find that gitlab-runner installs itself as a system service by relying on the Go package github.com/kardianos/service, which is an open source system by Daniel Theophanes, one of the maintainers of the Go standard library database package. The service operation package, which shields the differences of the os layer, provides developers with a relatively simple service operation interface, including the following control actions:

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

Well, let’s use an example myapp to introduce how to use the kardianos/service package to give your Go application the ability to install itself as a system service .

myapp is an http server that serves on a certain port and returns a “Welcome” response when a request is received:

 // 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) }

Now we want to add some capabilities to myapp to support installing itself as a systemd service, and to start, stop, and uninstall the systemd service via subcommands.

We first increase the parsing capability of subcommand and its parameters for the program through the os package and the flag package. We don’t use third-party command-line argument parsing packages, just the standard library’s flag package. Since myapp supports subcommands, we need to apply for a separate FlagSet instance for each subcommand with command line parameters, such as installCommand and runCommand in the following code. The command line parameters of each subcommand should also be bound to the FlagSet instance corresponding to the respective subcommand, such as the content in the init function body of the following code.

In addition, due to the use of subcommand, the default flag.Usage can no longer meet our requirements. We need to implement a usage function and assign it to 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 } ... ... }

After all these are completed, we obtain the meta configuration information of the service to be installed as a systemd service in the getServiceConfig function:

 // 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 }

What should be noted here are Option and Arguments in service.Config. The former is used to place any key-value pair in the systemd service unit configuration file (such as Restart=always here), while Arguments will be composed as the value of the ExecStart key , the value will be passed in and used when starting the service.

Next, we use the service package to create an instance (srv) of the operation service based on the loaded Config, and then pass it and subCommand to runServiceControl to control the systemd service (as shown in the following code).

 // 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) } }

Well, the code is done! Now let’s verify the capabilities of myapp.

Let’s first complete the compilation and installation of the binary program:

 $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

Next, let’s install myapp as a systemd service:

 $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)

We see that after installation, myApp has become myApp.service and is in an inactive state. The content of the systemd unit file /etc/systemd/system/myApp.service is as follows:

 $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

Next, let’s start the service:

 $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

We see that the myApp service starts successfully and is listening on port 65432!

We use curl to send a request to this port:

 $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

We see that the myApp service is running fine and returning the expected response.

Now we stop the service with the 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.

Modify the configuration /etc/myapp/config.ini (change the listening port from 65432 to 65431), and then restart the service:

 $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

From the status log of systemd, we see that the myApp service was successfully started and changed to listen on port 65431. Let’s visit this port:

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

As can be seen from the above results, our configuration update and restart were successful!

We can also uninstall the service from systemd using the uninstall function of myapp:

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

Well, here we see: the goal proposed at the beginning of the article to add the ability to install itself as a systemd service for Go applications has been successfully achieved.

Finally, a summary: The service package gives our program the ability to install itself as a system service. It also allows you to develop the ability to install other programs as a system service, but that’s a job left to you :). If you have any questions, please leave a message in the comment area.

The code involved in this article can be downloaded here .


“Gopher Tribe” Knowledge Planet aims to create a high-quality Go learning and advanced community! High-quality first published Go technical articles, “three-day” first published reading rights, analysis of the current situation of Go language development twice a year, read the fresh Gopher daily 1 hour in advance every day, online courses, technical columns, book content preview, must answer within six hours Guaranteed to meet all your needs about the Go language ecosystem! In 2022, the Gopher tribe will be fully revised, and will continue to share knowledge, skills and practices in the Go language and Go application fields, and add many forms of interaction. Everyone is welcome to join!

img{512x368}

img{512x368}

img{512x368}

img{512x368}

I love texting : Enterprise-level SMS platform customization development expert https://51smspush.com/. smspush : A customized SMS platform that can be deployed within the enterprise, with three-network coverage, not afraid of large concurrent access, and can be customized and expanded; the content of the SMS is determined by you, no longer bound, with rich interfaces, long SMS support, and optional signature. On April 8, 2020, China’s three major telecom operators jointly released the “5G Message White Paper”, and the 51 SMS platform will also be newly upgraded to the “51 Commercial Message Platform” to fully support 5G RCS messages.

The famous cloud hosting service provider DigitalOcean released the latest hosting plan. The entry-level Droplet configuration is upgraded to: 1 core CPU, 1G memory, 25G high-speed SSD, and the price is 5$/month. Friends who need to use DigitalOcean can open this link : https://ift.tt/95z4AEK to open your DO host road.

Gopher Daily Archive Repository – https://ift.tt/Hpsh3PE

my contact information:

  • Weibo: https://ift.tt/7KUR3IY
  • Blog: tonybai.com
  • github: https://ift.tt/4uDdCI1

Business cooperation methods: writing, publishing books, training, online courses, partnership entrepreneurship, consulting, advertising cooperation.

© 2022, bigwhite . All rights reserved.

This article is reprinted from https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner/
This site is for inclusion only, and the copyright belongs to the original author.