Distribuindo uma aplicação Go sem o Docker
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.
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:
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:
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK