5

Distribuindo uma aplicação Go sem o Docker

 9 months ago
source link: https://dev.to/faabiosr/distribuindo-uma-aplicacao-go-sem-o-docker-p73
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Cover image for Distribuindo uma aplicação Go sem o Docker
Fábio Ribeiro

Posted on Oct 27

• Originally published at fabioribeiro.dev

10 2 2 2 2

Distribuindo uma aplicação Go sem o Docker

Quando pensamos em escalabilidade de software, invariavelmente, em alguma eventualidade nos deparamos com a necessidade de distribuir nosso software.

A grande maioria dos artigos e tutoriais que encontramos e que falam sobre distribuição de software, acabam descrevendo sobre como distribuir usando Kubernetes, Docker Swarm e às vezes preparando o bom e velho docker-compose.yaml.

Dependendo do tamanho da sua aplicação, às vezes não é necessário ter um cluster de Kubernetes, também em alguns casos queremos obter todo o potencial do servidor, como memória, disco e rede, evitando ter uma camada extra entre a aplicação e a máquina.

Nesse artigo, apresento um compilado de como distribuir a sua aplicação para ser executada diretamente no servidor, por exemplo na sua instância AWS EC2, Google Compute Engine, e quem sabe talvez no seu droplet na Digital Ocean (no final do artigo compartilho um cupom de crédito para você brincar).

Tudo será apresentado em passos que serão incrementais, o empacotamento de uma aplicação em Golang, a preparação das dependências requeridas, a execução, e finalizamos com a atualização e tempo de inatividade.

De forma simplificada, uma aplicação será desenvolvida, e para cada mudança, novas tags serão criadas, e iremos focar o empacotamento e distribuição usando Ubuntu Linux (server). Ao final deixo o link onde você encontrará o projeto completo.

A aplicação

Antes de tudo, precisaremos de uma aplicação,e ela guardará os nomes dos filmes e seu dia de lançamento. Na parte que cabe ao banco de dados, eles serão armazenados em um banco de dados em arquivo, e é requerido que seja definida uma variável de ambiente chamada MOVIES_DB_PATH, onde contém a localização deste arquivo.

Vamos para a aplicação em si (de momento não se preocupe com as libs usadas, tudo estará no repositório no final do artigo):

package main

import (
    "context"
    "os"
    "os/signal"
    "path/filepath"
    "time"

    "github.com/labstack/echo/v4"
    bolt "go.etcd.io/bbolt"

    "github.com/faabiosr/go-movies-demo/internal/movies"
)

const (
    appAddr   = "0.0.0.0:8000"
    appName   = "moviez"
    dbName    = "catalog.db"
    dbPathEnv = "MOVIES_DB_PATH"
)

const timeout = 10 * time.Second

func main() {
    e := echo.New()

    if os.Getenv(dbPathEnv) == "" {
        e.Logger.Fatalf("env '%s' was not defined", dbPathEnv)
    }

    dbPath := filepath.Join(os.Getenv(dbPathEnv), dbName)

    // Database connect
    db, err := bolt.Open(dbPath, 0o600, nil)
    if err != nil {
        e.Logger.Fatal(err)
    }

    ds := movies.NewDatasource(db)

    // API Resources
    movies.Routes(e.Group("/movies"), ds)

    // Start server
    e.Logger.Infof("%s service", appName)

    go func() {
        if err := e.Start(appAddr); err != nil {
            e.Logger.Info("shutting down the service")
        }
    }()

    // Graceful shutdown
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt)

    <-quit

    ctx, cancel := context.WithTimeout(context.TODO(), timeout)
    defer cancel()

    if err := e.Shutdown(ctx); err != nil {
        e.Logger.Fatal(err)
    }
}

Note o ponto que informamos anteriormente, a aplicação necessita da variável de ambiente MOVIES_DB_PATH:

package main

const (
    dbName    = "catalog.db"
    dbPathEnv = "MOVIES_DB_PATH"
)

func main() {

    dbPath := filepath.Join(os.Getenv(dbPathEnv), dbName)

    // Database connect
    db, err := bolt.Open(dbPath, 0o600, nil)

Agora que temos a aplicação de exemplo, é hora de compilar, e é bem simples, o resultado do comando abaixo será um binário com o nome de movies:

go build -o movies ./cmd/movies

Com o binário em mãos, já podemos copiar e executar no servidor.

MOVIES_DB_PATH=/tmp ./movies

Pronto é isso! Guia finalizado!

WTF?

OK, calma, calma!

Mesmo com o binário criado, a distribuição não é fácil, copiar para o servidor todas as vezes que houver uma nova atualização e executar manualmente pode tornar-se complexo e chato.

Vale ressaltar que não há uma forma consistente para a instalação, e não há como evitar que seja usada outra pasta no servidor, tornando o processo difícil de controlar e passível a erros.

2. Empacotando o binário

Sabendo que a aplicação será executada em um Ubuntu Linux, temos a possibilidade de distribuir como um pacote Debian (deb), ou até mesmo como snap, mas neste exemplo iremos focar no Debian.

Empacotar como .deb nos dá algumas vantagens, como:

  • Poder controlar a versão do pacote.
  • Executar scripts antes e depois da instalação/remoção do pacote.
  • Adicionar arquivos extras.

Preparar um pacote Debian "na mão", não é uma tarefa muito simples, e para facilitar a nossa vida vamos usar o GoReleaser, essa ferramenta maravilhosa, que internamente faz uso da é nFPM, responsável por criar pacotes Linux. Também é importante dizer que o GoReleaser não só nos ajuda a criar os pacotes Debian, mas também pacotes para Windows, MacOS, RPM, APK e muito mais.

Em nosso projeto, vamos definir o arquivo .goreleaser.yaml, que contém as informações do pacote a ser gerado, atente-se para seção compartilhada abaixo e note que em formats foi definido o .deb:

nfpms:
  - id: movies
    package_name: movies
    file_name_template: "{{ .ConventionalFileName }}"
    description: Manages movie collection through API
    license: MIT
    formats:
      - deb

Adicionalmente, foi configurado o Github Workflows onde contém os passos para compilar e distribuir em .deb, e o resultado é esse:

contém um printscreen da lista de arquivos gerados pelo Goreleaser

Todas as versões criadas estarão em releases.

Pronto! Agora sim temos um maior controle sobre o versionamento e distribuição da aplicação. De maneira simples e consistente podemos instalar em qualquer distro baseada em Debian.

3. Lidando com dependências (banco de dados em arquivo)

Anteriormente foi mencionado que a aplicação necessita de uma pasta onde o banco de dados será criado, a pasta elegida será em /var/lib/movies-demo, e para a criá-la vamos usar alguns dos hooks que pacote Debian nos fornece:

postinst (post-install.sh): é executado após instalar ou atualizar um pacote, esse hook ficará responsável por criar a pasta definida acima e também o usuário/grupo dessa pasta (é recomendável que sempre tenha um usuário/grupo, ficando isolado dos demais).

#!/bin/sh

set -e

MOVIES_DB_PATH=/var/lib/movies-demo
MOVIES_USER=movies-demo

if [ "$1" = "configure" ]; then
    # creating user and group
    adduser --quiet \
            --system \
            --home /nonexistent \
            --no-create-home \
            --disabled-password \
            --group "$MOVIES_USER"

    # creating database folder
    if [ ! -d $MOVIES_DB_PATH ]; then
        mkdir -p $MOVIES_DB_PATH
        chown $MOVIES_USER:$MOVIES_USER $MOVIES_DB_PATH
    fi

    exit 0
fi

Da mesma maneira que temos o postinst, também temos um hook para quando removemos o pacote.

postrm (post-remove-sh): é executado quando removemos um pacote, e removerá a pasta apenas quando o arquivo de catalog.db não existir.

#!/bin/sh

set -e

MOVIES_DB_PATH=/var/lib/movies-demo

if [ "$1" = "remove" ]; then
    if [ -f "$MOVIES_DB_PATH/catalog.db" ]; then
        echo "Database file found and won't be removed." >&2
    else
        echo "Removing database folder." >&2
        rm -fr $MOVIES_DB_PATH
    fi

    exit 0
fi

Scripts prontos, agora é só fazer a inclusão deles no .goreleaser.yaml e quando empacotar e instalar a aplicação novamente, a pasta será criada:

diff --git a/.goreleaser.yaml b/.goreleaser.yaml
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -46,6 +46,9 @@ nfpms:
     license: MIT
     formats:
       - deb
+    scripts:
+      postinstall: "env/debian/post-install.sh"
+      postremove: "env/debian/post-remove.sh"

4. Rodando a aplicação em background como um serviço (systemd)

A pasta já está pronta, mas a execução pela linha de comando continua sendo manual, e para resolver essa questão, vamos executar a aplicação em background como um serviço. Para isso tiraremos proveito do systemd, que já vem instalado no Ubuntu Linux.

Em poucas palavras, o systemd é um conjunto de blocos de construção para uma sistema Linux, ele fornece um gerenciador de sistema e serviço, e é justamente do segundo ponto que necessitamos.

Para um serviço é requerido que seja criado um arquivo onde contém as referências para um recurso que o sistema saberá como operar e gerenciar, chamado de unit.

O unit que usaremos é o service, que descreve como gerenciar um serviço ou aplicação no servidor:

movies.service:

[Unit]
Description=Manages movie collection through API
Documentation="https://github.com/faabiosr/go-movies-demo"

[Service]
EnvironmentFile=/etc/default/movies
ExecStart=/usr/bin/movies
Restart=on-failure
User=movies-demo
Group=movies-demo
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target

Na seção Service:

  • EnvironmentFile: arquivo que contém as variáveis de ambiente.
  • ExecStart: caminho do binário.
  • User/Group: usaremos o mesmo criado anteriormente.

Precisaremos agora criar um arquivo que contém a variável de ambiente usada pela aplicação:

movies.conf:

MOVIES_DB_PATH="/var/lib/movies-demo"

Uma vez finalizada a criação dos arquivos necessários para o systemd, é imprescindível atualizar os hooks do debian para que o mesmo seja ativado e incializado após a instalação:

postint (post-install.sh): ativa o serviço se não foi ativo, e inicia ou reinicia caso já esteja rodando.

diff --git a/env/debian/post-install.sh b/env/debian/post-install.sh
--- a/env/debian/post-install.sh
+++ b/env/debian/post-install.sh
@@ -4,6 +4,7 @@ set -e

 MOVIES_DB_PATH=/var/lib/movies-demo
 MOVIES_USER=movies-demo
+MOVIES_SERVICE=movies.service

 if [ "$1" = "configure" ]; then
     # creating user and group
@@ -20,5 +21,25 @@ if [ "$1" = "configure" ]; then
         chown $MOVIES_USER:$MOVIES_USER $MOVIES_DB_PATH
     fi

-    exit 0
+    # enable systemd service
+    deb-systemd-helper unmask $MOVIES_SERVICE >/dev/null || true
+
+    if deb-systemd-helper --quiet was-enabled $MOVIES_SERVICE; then
+        deb-systemd-helper enable $MOVIES_SERVICE >/dev/null || true
+    else
+        deb-systemd-helper update-state $MOVIES_SERVICE >/dev/null || true
+    fi
+
+    # starting service
+    if [ -d /run/systemd/system ]; then
+        systemctl --system daemon-reload >/dev/null || true
+
+        if [ -n "$2" ]; then
+            _dh_action=restart
+        else
+            _dh_action=start
+        fi
+
+        deb-systemd-invoke $_dh_action $MOVIES_SERVICE >/dev/null || true
+    fi
 fi

postrm (post-remove.sh): a nova adição irá reiniciar o serviço do próprio systemd, e o serviço só será removido caso o usuário opte por uma remoção completa.

diff --git a/env/debian/post-remove.sh b/env/debian/post-remove.sh
--- a/env/debian/post-remove.sh
+++ b/env/debian/post-remove.sh
@@ -3,6 +3,7 @@
 set -e

 MOVIES_DB_PATH=/var/lib/movies-demo
+MOVIES_SERVICE=movies.service

 if [ "$1" = "remove" ]; then
     if [ -f "$MOVIES_DB_PATH/catalog.db" ]; then
@@ -12,5 +13,16 @@ if [ "$1" = "remove" ]; then
         rm -fr $MOVIES_DB_PATH
     fi

-    exit 0
+    # disabling service
+    if [ -d /run/systemd/system ]; then
+        systemctl --system daemon-reload >/dev/null || true
+    fi
+
+    deb-systemd-helper mask $MOVIES_SERVICE >/dev/null || true
+fi
+
+if [ "$1" = "purge" ]; then
+    # disabling service
+    deb-systemd-helper purge $MOVIES_SERVICE >/dev/null || true
+    deb-systemd-helper unmask $MOVIES_SERVICE >/dev/null || true
 fi

Adicionalmente criamos o prerm hook, que é executado antes de remover o pacote, e será ele responsável por finalizar a execução da aplicação, assim removemos o pacote de forma segura:

prerm (pre-remove.sh):

#!/bin/sh

set -e

MOVIES_SERVICE=movies.service

# stopping service
if [ -d /run/systemd/system ]; then
    deb-systemd-invoke stop $MOVIES_SERVICE >/dev/null || true
fi

Agora é só incluir todos os arquivos no pacote Debian atualizado o .goreleaser.yaml

diff --git a/.goreleaser.yaml b/.goreleaser.yaml
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -46,7 +46,14 @@ nfpms:
     license: MIT
     formats:
       - deb
+    contents:
+      - src: "env/debian/movies.service"
+        dst: "/lib/systemd/system/movies.service"
+      - src: "env/debian/movies.conf"
+        dst: "/etc/default/movies"
+        type: config
     scripts:
+      preremove: "env/debian/pre-remove.sh"
       postinstall: "env/debian/post-install.sh"
       postremove: "env/debian/post-remove.sh"

Quando o pacote for instalado no sistema operacional, automaticamente será copiado os arquivos da seção contents para os respectivos destinos, fará o registro no systemd e iniciará automaticamente.

Poderia dizer que o empacotamento e a distribuição do aplicativo está finalizado, mas ainda temos um último problema para ser resolvido, vejamos na última parte.

5. Atualização e tempo de inatividade

Instalar ou atualizar ficou extremamente simples e com um controle mais rígido, todavia, a aplicação pode ficar inativa durante a atualização. Isso ocorre pelo simples fato de finalizar o serviço e instalar uma nova versão, e tendo uma degradação da disponibilidade.

Felizmente, isso pode ser contornado ainda usando o systemd, através de um outro unit, o socket. Esse arquivo de unit codifica a informação sobre um soquete de rede ou arquivo, controlado e supervisionado, para uma ativação baseada em sockets.

Vale lembrar que os unit sockets não iniciam os serviços por conta própria, em vez disso, eles apenas esperam e escutam um endereço IP:PORT, ou um Unix socket, e quando algo se conecta a ele, o serviço ao qual o socket se destina será iniciado e a conexão é entregue a ele. Já que nossa aplicação lida com requisições HTTP, podemos usá-lo.

Alguns passos adicionais precisarão ser concluídos, como a criação e modificação dos units, alteração dos hooks, e uma mudança na aplicação, pois ela precisa suportar essa funcionalidade.

movies.socket:

[Unit]
Description=Manages movie collection through API
Documentation="https://github.com/faabiosr/go-movies-demo"

[Socket]
ListenStream=8000
SocketUser=movies-demo
SocketGroup=movies-demo

[Install]
WantedBy=sockets.target

Aliás, é necessário atualizar o movies.service e informar que o unit socket é requerido:

diff --git a/env/debian/movies.service b/env/debian/movies.service
--- a/env/debian/movies.service
+++ b/env/debian/movies.service
@@ -1,6 +1,8 @@
 [Unit]
 Description=Manages movie collection through API
 Documentation="https://github.com/faabiosr/go-movies-demo"
+After=network.target
+Requires=movies.socket

 [Service]
 EnvironmentFile=/etc/default/movies

Alteramos também os hooks do Debian:

postinst (post-install.sh): também fará o registro do unit socket e só reiniciará o serviço caso houver uma atualização no pacote.

diff --git a/env/debian/post-install.sh b/env/debian/post-install.sh
--- a/env/debian/post-install.sh
+++ b/env/debian/post-install.sh
@@ -5,6 +5,7 @@ set -e
 MOVIES_DB_PATH=/var/lib/movies-demo
 MOVIES_USER=movies-demo
 MOVIES_SERVICE=movies.service
+MOVIES_SOCKET=movies.socket

 if [ "$1" = "configure" ]; then
     # creating user and group
@@ -30,16 +31,24 @@ if [ "$1" = "configure" ]; then
         deb-systemd-helper update-state $MOVIES_SERVICE >/dev/null || true
     fi

+    # enable systemd socket
+    deb-systemd-helper unmask $MOVIES_SOCKET >/dev/null || true
+
+    if deb-systemd-helper --quiet was-enabled $MOVIES_SOCKET; then
+        deb-systemd-helper enable $MOVIES_SOCKET >/dev/null || true
+    else
+        deb-systemd-helper update-state $MOVIES_SOCKET >/dev/null || true
+    fi
+
     # starting service
     if [ -d /run/systemd/system ]; then
         systemctl --system daemon-reload >/dev/null || true

         if [ -n "$2" ]; then
-            _dh_action=restart
+            deb-systemd-invoke restart $MOVIES_SERVICE >/dev/null || true
         else
-            _dh_action=start
+            deb-systemd-invoke start $MOVIES_SOCKET >/dev/null || true
         fi

-        deb-systemd-invoke $_dh_action $MOVIES_SERVICE >/dev/null || true
     fi
 fi

prerm (pre-remove.sh): quando seja feita uma atualização, apenas o serviço será desligado, finalizará o socket, e o serviço apenas na remoção.

diff --git a/env/debian/pre-remove.sh b/env/debian/pre-remove.sh
--- a/env/debian/pre-remove.sh
+++ b/env/debian/pre-remove.sh
@@ -3,8 +3,18 @@
 set -e

 MOVIES_SERVICE=movies.service
+MOVIES_SOCKET=movies.socket

-# stopping service
-if [ -d /run/systemd/system ]; then
-    deb-systemd-invoke stop $MOVIES_SERVICE >/dev/null || true
+if [ "$1" = "remove" ]; then
+    # stopping service and socket
+    if [ -d /run/systemd/system ]; then
+        deb-systemd-invoke stop $MOVIES_SERVICE $MOVIES_SOCKET >/dev/null || true
+    fi
+fi
+
+if [ "$1" = "upgrade" ]; then
+    # stopping service
+    if [ -d /run/systemd/system ]; then
+        deb-systemd-invoke stop $MOVIES_SERVICE >/dev/null || true
+    fi
 fi

No arquivo .goreleaser.yaml, foi incluído o arquivo .socket, na seção contents:

diff --git a/.goreleaser.yaml b/.goreleaser.yaml
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -49,6 +49,8 @@ nfpms:
     contents:
       - src: "env/debian/movies.service"
         dst: "/lib/systemd/system/movies.service"
+      - src: "env/debian/movies.socket"
+        dst: "/lib/systemd/system/movies.socket"
       - src: "env/debian/movies.conf"
         dst: "/etc/default/movies"
         type: config

Independente de configurar o systemd, a aplicação ainda não está preparada para fazer uso de sockets, para isso vamos adicionar o suporte de ativação de sockets.

A equipe do CoreOS desenvolveu o pacote go-systemd, nele contém várias ferramentas para integrar com o systemd, entre eles o activation.

main.go: inclusão do activation e integração com o servidor http.

diff --git a/cmd/movies/main.go b/cmd/movies/main.go
--- a/cmd/movies/main.go
+++ b/cmd/movies/main.go
@@ -7,6 +7,7 @@ import (
    "path/filepath"
    "time"

+   "github.com/coreos/go-systemd/activation"
    "github.com/labstack/echo/v4"
    bolt "go.etcd.io/bbolt"

@@ -46,7 +47,7 @@ func main() {
    e.Logger.Infof("%s service", appName)

    go func() {
-       if err := e.Start(appAddr); err != nil {
+       if err := start(e, appAddr); err != nil {
            e.Logger.Info("shutting down the service")
        }
    }()
@@ -64,3 +65,16 @@ func main() {
        e.Logger.Fatal(err)
    }
 }
+
+func start(e *echo.Echo, host string) error {
+   listeners, err := activation.Listeners()
+   if err != nil {
+       return nil
+   }
+
+   if len(listeners) > 0 {
+       e.Listener = listeners[0]
+   }
+
+   return e.Start(host)
+}

Com essa parte final, a aplicação terá garantias de disponibilidade durante uma reinicialização ou atualização, e estará completamente funcional para distribuir.

Conclusão

Agora nós sabemos como empacotar e distribuir o aplicativo, seguindo um modelo onde podemos versionar, preparar as dependências, e garantir disponibilidade.

Acredito que os pontos compartilhados, não tem um curva de dificuldade alta, mas sim, pontos estratégicos para futura manutenção do aplicativo, tal qual, reduzir a complexidade na hora de distribuir a aplicação, e o ponto central é tirar proveito das ferramentas que estão disponíveis no sistema operacional que será rodado.

A aplicação completa usada no artigo está em https://github.com/faabiosr/go-movies-demo, você encontrará tudo lá.

Como prometido, aqui vai um link de créditos para brincar na Digital Ocean e criar os seus droplets.

Recomendo a leitura das referências abaixo para entender um pouco mais sobre o systemd, e os hooks do Debian:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK