Com vários projetos usando tecnologias diferentes, surgiu a necessidade de uma maneira mais ágil de organizar o deploy em produção, desses e de outros novos projetos que surgem. Uma dessas maneiras, que escolhi explicar nesse artigo, é rodar cada aplicação dentro de um contêiner. Quando fazemos isso, basta fazer uma única configuração, uma única vez, para garantir o mesmo funcionamento nas máquinas de desenvolvimento, homologação e produção. Isso permite que cada time de desenvolvimento seja responsável por decidir e gerenciar quais versões de bibliotecas e dependências seus projetos vão utilizar.
Na arquitetura abaixo, cada aplicação web roda dentro de um contêiner, expondo uma porta específica. Cada contêiner rodando uma aplicação web notifica ao serviço do etcd (que também roda dentro de um contêiner) seu próprio IP e a porta exposta. Certo? Agora vamos ao passo a passo.
Configurando o Docker Host
Instalação
Vamos utilizar um servidor com o Debian Jessie 64 bit instalado. Pode ser qualquer distro Linux com Kernel maior ou igual a 3.1.0. Se você possui interesse em montar um cluster Docker, sugiro distros específicas como CoreOS ou RancherOS.
Todos os comandos serão executados como root nesse artigo apenas para facilitar o entendimento. No ambiente de produção, eu aconselho a criar outros usuários e definir suas políticas de segurança.
As instruções para instalação de acordo com a documentação são:
$ sudo apt-get purge lxc-docker* $ sudo apt-get purge docker.io* $ sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D $ sudo echo "deb https://apt.dockerproject.org/repo debian-jessie main" > /etc/apt/sources.list.d/docker.list $ sudo apt-get update $ sudo apt-cache policy docker-engine $ sudo apt-get install -y docker-engine
Estrutura de diretórios
Usaremos a seguinte estrutura:
- /src – códigos de cada aplicação web.
- /static – arquivos estáticos como páginas html, imagens, css e javascript.
- /data – arquivos de dados do MySQL ou Postgres, configuração do servidor web e logs.
Com isso, crie os diretórios no servidor:
$ sudo mkdir /src /static /data
Criando os contêineres
Para seguir as boas práticas, é importante ter apenas um serviço em execução por contêiner criado. Por isso, vamos criar os seguintes contêineres:
- etcd: para armazenar nome, ip e porta dos contêineres rodando aplicações web;
- dnsdock: para permitir comunicação entre contêineres por meio do hostname;
- postgres: banco de dados;
- goapp: aplicação web escrita em Go;
- caddy: servidor web em Go, rápido e performático, para servir estáticos e fazer proxy reverso com as aplicações web.
Agora, vamos falar detalhadamente sobre cada um desses contêineres e criá-los efetivamente.
etcd
O etcd é um sistema distribuído para armazenamento de chave/valor. Sua utilidade nessa arquitetura é armazenar como chave o nome do contêiner e como valor o ip e a porta. Cada contêiner rodando uma aplicação web vai ser responsável por atualizar esses valores no etcd e o servidor web vai ler esses valores para criar e configurar um proxy reverso, como mostraremos depois.
$ ETCD_DATA_DIR=/data/etcd $ DOCKER_IP=172.17.0.1 $ ETCD_PORT=2379 $ sudo mkdir $ETCD_DATA_DIR $ sudo docker run -d --name etcd \ -p $ETCD_PORT:$ETCD_PORT \ -v $ETCD_DATA_DIR:$ETCD_DATA_DIR \ -e ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:$ETCD_PORT \ -e ETCD_DATA_DIR=$ETCD_DATA_DIR \ -e ETCD_ADVERTISE_CLIENT_URLS=http://$DOCKER_IP:$ETCD_PORT \ quay.io/coreos/etcd:v2.2.3
Com esse comando, criamos um contêiner usando a imagem quay.io/coreos/etcd:v2.2.2 escutando na porta 2379. Você pode testar se o etcd está funcionando corretamente:
$ curl http://172.17.0.1:2379/v2/keys
E o resultado será:
{"action":"get","node":{"dir":true}}
dnsdock
O dnsdock é um serviço de descoberta para contêineres similar a um dns. Quando criamos um contêiner, não conseguimos garantir qual IP que o Docker vai fornecer a ele. Isso se torna um problema quando um contêiner precisa se comunicar com o outro, por exemplo, uma aplicação web e um banco de dados. O dnsdock é muito útil nesse ponto.
$ sudo docker run -d --name dnsdock \ -v /var/run/docker.sock:/var/run/docker.sock \ -p 172.17.0.1:53:53/udp \ tonistiigi/dnsdock -nameserver="8.8.8.8:53"
Esse comando executa o dnsdock dentro de um contêiner escutando na porta 53 UDP (porta padrão DNS) e usando o DNS do Google como secundário, caso ele não consiga resolver um nome. Você pode testar executando o dig para obter o ip do contêiner do etcd que foi criado anteriormente:
$ nslookup etcd.docker 172.17.0.1
E o resultado será:
Server: 172.17.0.1 Address 1: 172.17.0.1 Name: etcd.docker Address 1: 172.17.0.2 etcd.docker
postgres
O Postgres é um banco de dados robusto e popular devido às suas funcionalidades. A aplicação web vai utilizar esse banco para armazenar os dados.
$ cat > /tmp/initial.sql <<EOF DROP DATABASE IF EXISTS mydb; CREATE DATABASE mydb WITH OWNER postgres TEMPLATE template0 ENCODING 'UTF8' TABLESPACE pg_default LC_COLLATE 'en_US.utf8' LC_CTYPE 'en_US.utf8' CONNECTION LIMIT -1; \c mydb; CREATE TABLE hello ( message varchar(50) not null, created_at date default current_date ); INSERT INTO hello (message) VALUES ('Hello World'); EOF $ sudo mkdir /data/postgres $ sudo docker run -d --name postgres \ -v /data/postgres:/var/lib/postgresql/data \ -v /tmp/initial.sql:/tmp/initial.sql \ -h postgres \ -e POSTGRES_PASSWORD=postgres \ -p 5432:5432 \ postgres:9.5 $ sudo docker exec postgres bash -c "PGPASSWORD=postgres psql -U postgres -f /tmp/initial.sql"
goapp
Exemplo de uma aplicação web com acesso ao banco de dados Postgres. Como já implementamos o dnsdock, é possível acessar o contêiner usando o host postgres.docker. A aplicação web deve retornar a mensagem que foi armazenada no banco de dados.
$ sudo mkdir /src/goapp && cd /src/goapp $ sudo cat > app.sh <<EOF IP=`ip add | grep global | awk -F ' ' '{print \$2}' | awk -F '/' '{print \$1}'` echo "Registering \$SERVICE_NAME:\$SERVICE_PORT in \$ETCD_ADDR" curl -XPUT \$ETCD_ADDR/v2/keys/containers/\$SERVICE_NAME -d value="\$IP:\$SERVICE_PORT" go get github.com/lib/pq && go install && /usr/lib/go/bin/goapp EOF $ sudo chmod +x app.sh $ sudo cat > Dockerfile <<EOF FROM alpine RUN apk add --update curl bash gcc go musl-dev git openssh-client ca-certificates && rm -rf /var/cache/apk/* ENV GOPATH /.go ENV PATH \$PATH:\$GOPATH/bin RUN go get github.com/tools/godep WORKDIR /usr/lib/go/src/goapp EOF $ sudo cat > main.go <<EOF package main import ( "net/http" "fmt" "database/sql" _ "github.com/lib/pq" ) func hello(w http.ResponseWriter, r *http.Request) { db, _ := sql.Open("postgres", "user=postgres password=postgres dbname=mydb host=postgres.docker sslmode=disable") if db.Ping() != nil { fmt.Println("Could not connect to database.") } rows, err := db.Query("select message from hello limit 1") defer rows.Close() if err != nil { fmt.Println("Error running the query.", err) } rows.Next() var message string err = rows.Scan(&message) if err == nil { w.Header().Set("Content-type", "text/plain") w.Write([]byte(message)) } else { fmt.Println("Error trying to scan.", err) } } func main() { http.HandleFunc("/", hello) fmt.Println("Server has been started.") http.ListenAndServe(":8001", nil) } EOF
O código acima cria os arquivos app.sh, Dockerfile e main.go dentro do /src/goapp. Agora é necessário criar uma imagem a partir do Dockerfile e depois criar um contêiner para compilar e executar o main.go.
$ cd /src/goapp $ sudo docker build -t=goapp . $ sudo docker run -d --name goapp \ -e ETCD_ADDR=http://etcd.docker:2379 \ -e SERVICE_NAME=goapp \ -e SERVICE_PORT=8001 \ -p 8001:8001 \ -v /src/goapp:/usr/lib/go/src/goapp --dns 172.17.0.1 \ goapp bash -c "/usr/lib/go/src/goapp/app.sh"
As variáveis ETCD_ADDR, SERVICE_NAME e SERVICE_PORT são utilizadas pelo script app.sh para armazenar esses valores no etcd. O parâmetro –dns 172.17.0.1 é para que o contêiner utilize o dnsdock e consiga se comunicar com o host postgres.docker. No final, a aplicação web é executada na porta 8001.
caddy
O caddy é um servidor web que será utilizado para proxy reverso com a aplicação web. Ele é usado em conjunto com o confd para obter todas as aplicações web registradas no etcd e, a partir de um template, criar o arquivo de configuração Caddyfile, no qual cada aplicação web vai ser configurada como sub-domínio. Por exemplo, goapp.meudominio.com vai fazer um proxy para o contêiner goapp.
Antes de tudo, precisamos criar os arquivos que o confd utilizará para criar o Caddyfile:
$ sudo mkdir -p /data/caddy/logs $ sudo mkdir -p /data/confd/conf.d $ sudo mkdir -p /data/confd/templates $ sudo cat > /data/confd/conf.d/caddy.toml <<EOF [template] src = "caddy.tmpl" dest = "/etc/caddy/Caddyfile" keys = [ "/containers", ] owner = "root" mode = "0644" EOF $ sudo cat > /data/confd/templates/caddy.tmpl <<EOF # Generated by confd {{datetime}} meudominio.com { root /static browse tls off errors /etc/caddy/logs/static.log cors } {{range gets "/containers/*"}} {{\$name := base .Key}} {{\$name}}.meudominio.com { proxy / {{.Value}} { proxy_header Host {host} } errors /etc/caddy/logs/{{base .Key}}.log tls off } {{end}} EOF
Quando o confd for executado, ele vai ler o arquivo de configuração caddy.toml e informar que o template utilizado será o caddy.tmpl, além de que o resultado renderizado será salvo em /etc/caddy/Caddyfile. Nesse caso, os diretórios /etc/caddy e /etc/confd serão volumes montados a partir do host.
O próximo passo é criar o script que vai usar o confd para consultar o etcd, gerar o Caddyfile e executar o caddy:
$ mkdir /tmp/caddy-confd-docker && cd /tmp/caddy-confd-docker $ cat > start.sh <<EOF CONFD_TOML="/etc/confd/conf.d/caddy.toml" CADDYFILE="/etc/caddy/Caddyfile" confd -onetime -node \$ETCD_ADDR -config-file \$CONFD_TOML && caddy --conf \$CADDYFILE EOF $ chmod +x start.sh
Depois, vamos criar o Dockerfile para gerar uma imagem:
$ cd /tmp/caddy-confd-docker $ cat > Dockerfile <<EOF FROM alpine RUN apk add --update openssh-client git tar \ && mkdir /caddysrc \ && curl -sL -o /caddysrc/caddy_linux_amd64.tar.gz "https://caddyserver.com/download/build?os=linux&arch=amd64&features=cors&git&ipfilter&jsonp&search" \ && tar -xf /caddysrc/caddy_linux_amd64.tar.gz -C /caddysrc \ && mv /caddysrc/caddy /usr/bin/caddy \ && chmod 755 /usr/bin/caddy \ && rm -rf /caddysrc \ && printf "0.0.0.0\nbrowse" > /etc/Caddyfile \ && curl -sL -o /usr/bin/confd "https://github.com/kelseyhightower/confd/releases/download/v0.11.0/confd-0.11.0-linux-amd64" \ && chmod 755 /usr/bin/confd EXPOSE 2015 EXPOSE 443 VOLUME /etc/caddy VOLUME /etc/confd ADD start.sh /bin/start.sh CMD /bin/start.sh EOF
Agora, vamos criar a imagem e executar o contêiner:
$ cd /tmp/caddy-confd-docker $ sudo docker build -t=caddy-confd . $ sudo docker run -d --name caddy \ -v $HOME/.caddy:/root/.caddy \ -v /data/caddy:/etc/caddy \ -v /data/confd:/etc/confd \ -v /static:/static \ -e ETCD_ADDR=http://etcd.docker:2379 \ -p 80:2015 -p 443:443 \ --dns 172.17.0.1 \ caddy-confd
Se tudo funcionou corretamente, a aplicação vai estar acessível via http://goapp.meudominio.com. Lembrando que você deve substituir meudominio.com pelo seu domínio verdadeiro, certo?!
Bônus
Execução dos contêineres durante o boot
É possível configurar os contêineres para serem iniciados durante o boot do servidor. Na documentação existem exemplos utilizando Systemd, Upstart e Supervisor. Para configurar no Debian (systemd):
$ sudo cat > /etc/systemd/system/goapp.service <<EOF [Unit] Description=My API written in Go Requires=docker.service After=docker.service [Service] Restart=always ExecStart=/usr/bin/docker start -a goapp ExecStop=/usr/bin/docker stop -t 2 goapp [Install] WantedBy=multi-user.target EOF $ sudo systemctl enable /etc/systemd/system/goapp.service
Monitoramento
A ferramenta New Relic possui um plano free e é uma ótima opção para monitorar o host. Para monitorar os contêineres existe o cAdivisor. Execute o comando abaixo e acesse http://meudominio.com:8080:
$ sudo docker run -d --name cadvisor \ -v /:/rootfs:ro \ -v /var/run:/var/run:rw \ -v /sys:/sys:ro \ -v /var/lib/docker/:/var/lib/docker:ro \ -p 8080:8080 \ google/cadvisor:latest
Conclusão
Deploy utilizando Docker é uma das práticas de DevOps que trazem mais segurança e agilidade aos times. Outras ferramentas como Docker Compose e Docker Machine agilizam ainda mais a vida do desenvolvedor.
Na arquitetura que usamos nesse artigo foram utilizados alguns patterns para desenvolvimento de microservices. Percebam que para registrar o serviço foi utilizado um script que envia ao etcd o IP e a porta do contêiner por meio do curl. Para novos projetos web teremos que adicionar esse script também. Há um detalhe: se o contêiner for finalizado,é preciso criar algum mecanismo para remover as informações desse contêiner do etcd.
Existe um projeto chamado Registrator que automaticamente registra e desregistra todos os contêineres que estão rodando. Até a versão atual não é possível filtrar quais contêineres devem ser registrados.
A cada novo contêiner web criado é necessário destruir e executar o contêiner caddy para atualizar as definições de proxy reverso, pois o caddy não suporta reload. Se preferir, pode utilizar o nginx que possui essa funcionalidade.
Consul é uma alternativa ao etcd e possui uma interface web. O confd usado junto com o caddy como exemplo também possui uma alternativa chamada consul-template.
Por último, se você pensa em escalabilidade, balanceamento de carga, cache ou criar um cluster, por exemplo, existem outras distros que podem ajudar, como CoreOS e RancherOS. Vale a pena estudá-las. Lembrem-se sempre de bloquear no firewall as portas que não devem ser expostas publicamente.
E é isso! Ficou alguma dúvida ou tem alguma observação? Fique à vontade para usar os campos abaixo. Até a próxima!