Conteúdo


Ative a descoberta de serviço plug and play com o Consul e o Docker

Injete o reconhecimento dinamicamente nos apps distribuídos

A maioria das grandes implementações na nuvem atualmente, baseiam-se em serviços externos: bancos de dados, armazenamento em cache e APIs de terceiros. Atingir esses recursos críticos pode ser um desafio em ambientes em que as máquinas virtuais sejam componentes efêmeros. Neste tutorial, você aprenderá sobre uma solução de drop-in que expõe o conhecimento de infraestrutura a um tempo de execução de aplicativo. Além disso, você verá como projetar uma estrutura versátil para monitorar uma topologia de servidor complexa a fim de obter uma melhor orquestração de microserviços.

Minha solução não invasiva para descoberta de serviço fornece uma ferramenta essencial para DevOps. Ela é imediatamente acionável e não nos bloqueia em determinadas estruturas.

Informações básicas

A implementação de app moderno geralmente envolve microserviços em escala. Por exemplo, em vez de criar um único aplicativo monolítico, é possível dividir um aplicativo em unidades com finalidade única que colaboram entre si. Dessa forma, se obtém um desenvolvimento modular com uma separação de interesses e ajuste de escala horizontal, gratuitamente.

É realmente grátis? Não totalmente. Nós ainda temos a dificuldade de orquestrar todas essas partes em movimento da infraestrutura. E a ampla adoção do Docker, o mecanismo de contêiner, contribui com o desafio. Embora o Docker libere um fluxo de trabalho para desenvolver, enviar e executar programas, muitos desenvolvedores encontram uma barreira ao considerar a implementação de vários hosts ou problemas antigos, como gerenciamento de log.

O desafio

Atualmente, um aplicativo da web típico envolve um front-end de complexidade variada, um backend, um banco de dados e, frequentemente, serviços de terceiros. Todas essas tecnologias comunicam-se na rede, e é possível tirar vantagem desse fato: o backend é implementado onde os recursos estão disponíveis e um shard do banco de dados ativa nós para considerações de desempenho. Enquanto isso, toda a configuração desenvolve-se dinamicamente no cluster para manipular a carga.

Mas como o backend pode localizar a URL do banco de dados nessa topologia de nuvem em constante mudança? É necessário projetar um processo que forneça aos aplicativos um conhecimento atualizado da infraestrutura.

Apresentando o Consul

O Consul é descrito no GitHub como "uma ferramenta para descoberta, monitoramento e configuração de serviço". O Consul é um dos projetos de software livre desenvolvidos pela HashiCorp, a criadora do Vagrant. Ele oferece um sistema distribuído, altamente disponível para registrar serviços, armazenar configuração compartilhada e manter uma visualização precisa de vários datacenters. Além disso, ele é distribuído como um binário Go simples, o que facilita sua implementação.

Para que as etapas sejam fáceis de seguir (e consistentes com nosso assunto), vamos usar o Docker.

Depois que o Docker estiver instalado e, graças ao progrium (também conhecido como Jeff Lindsay), a linha única em Lista 1 é suficiente para autoinicializar um servidor Consul.

Lista 1. Autoinicialização de um servidor Consul
docker run --detach --name consul --hostname consul-server-1 progrium/consul 
-server -bootstrap -ui-dir /ui

# Get  container ip for further interactions
CONSUL_IP=$(docker inspect -f '{{ .NetworkSettings.IPAddress }}' consul)

# The container also runs a web UI at $CONSUL_IP:8500/ui

Observação: Embora a documentação oficial recomende ativar pelo menos três servidores para manipular os casos de falha, essas considerações estão além da proposta deste tutorial.

Agora, é possível consultar a infraestrutura e descobrir um serviço: o próprio Consul (consulte Lista 2).

Lista 2. Descubra o serviço Consul
curl $CONSUL_IP:8500/v1/catalog/services
{"consul": []}

# we can fetch more details about a specific service
curl $CONSUL_IP:8500/v1/catalog/service/consul
[{"Node":"consul-server-1","Address":"172.17.0.1","ServiceID":"consul",
"ServiceName":"consul","ServiceTags":[],"ServiceAddress":"",
"ServicePort":8300}]

Como você pode ver, o Consul armazena fatos importantes sobre os serviços. Ele cobre informações e tags, os dados fundamentais para acessar programaticamente os serviços remotos.

Serviços declarativos

Agora vamos observar as funções de registro, de serviços externos e do Docker na solução. Para ilustrar, vamos imaginar um aplicativo moderno que armazene dados no MongoDB e envie emails por meio do Mailgun. O Mailgun é um serviço externo, enquanto executaremos o antigo por conta própria. Leia para saber como é possível manipular ambos os casos.

Registro

Para expor essas propriedades de valor, primeiro é necessário registrar o serviço. Você executará um agente Consul, responsável por unir um servidor Consul, expondo o serviço do nó e executando uma verificação de funcionamento (consulte Lista 3).

Lista 3. Registre o serviço
# download and install the latest version
wget https://dl.bintray.com/mitchellh/consul/0.5.2_linux_amd64.zip -O 
/tmp/consul.zip
cd /usr/local/bin && unzip /tmp/consul.zip

# create state and configuration directories
mkdir -p {/srv/consul,/etc/consul.d}

# check that everything worked
consul --help

Com mais de 10 milhões de downloads, o MongoDB é uma opção popular como banco de dados de documento. Vamos usá-lo e salvar o arquivo a seguir em /etc/consul.d/mongo.json (consulte Lista 4).

Lista 4. Use o MongoDB como um banco de dados
{
    "service": {
        "name": "mongo",
        "tags": [
            "database",
            "nosql"
        ],
         "port": 27017,
         "check": {
             "name": "status",
             "script": "mongo --eval 'printjson(rs.status())'",
             "interval": "30s"
         }
     }
}

A sintaxe oferece uma maneira concisa, legível e declarativa de definir as propriedades de serviço e a verificação de funcionamento. É possível selecionar esses arquivos em um sistema de controle de versão e identificar imediatamente os componentes de um aplicativo. O arquivo acima declara um serviço chamado "mongo" na porta 27017. A seção de verificação fornece ao agente Consul um script que testa se o nó está em funcionamento ou não. Na verdade, ao solicitar ao servidor os requisitos do serviço, é necessário assegurar-se de que ele retorne terminais confiáveis.

Apenas resta iniciar o servidor Mongo real e o agente Consul local (consulte Lista 5).

Lista 5. Inicie o servidor Mongo e o agente Consul local
# launch mongodb server on default port 27017
mongod

# launch local agent
consul agent \
    -join $CONSUL_HOST \  # explicitly provide how to reach the server
    -data-dir /data/consul \  # internal state storage
    -config-dir /etc/consul.d  # configuration directory where services and checks 
    are expected to be defined

Funcionou? Vamos consultar a API HTTP do Consul (consulte Lista 6).

Lista 6. Consulte a API HTTP do Consul
# fetch infrastructure overview
curl $CONSUL_IP:8500/v1/catalog/nodes
[{"Node":"consul-server-1","Address":"172.17.0.1"},{"Node":"mongo-1","Address"
:"172.17.0.2"}]

# consul correctly registered mongo service
curl $CONSUL_IP:8500/v1/catalog/service/mongo
[{
    "Node": "mongo-1",
    "Address": "172.17.0.2",
    "ServiceID": "mongo",
    "ServiceName": "mongo",
    "ServiceTags": ["database", "no-sql"],
    "ServiceAddress": "",
    "ServicePort": 27017
}]

# it also exposes health state
curl $CONSUL_IP:8500/v1/health/service/mongo
[{
    "Node": {
        "Node":"mongo-1",
    },
    "Service": {
        "ID": "mongo",
        "Service": "mongo",
        "Tags": ["database","no-sql"],
        "Address": "",
    },
    "Checks":[{
        "Node": "mongo-1",
        "CheckID": "service:mongo",
        "Name": "Service 'mongo' check",
        "Status": "passing",
        "Notes": "",
        "Output": "MongoDB shell version: 3.0.3\nconnecting to: test\n{ \"ok\" : 0, 
    \"errmsg\" : \"not running with --replSet\", \"code\" : 76 }\n",
        "ServiceID": "mongo",
        "ServiceName": "mongo"
    },{
        "Node": "mongo-1",
        "CheckID": "serfHealth",
        "Status": "passing",
        "Notes": "",
        "Output": "Agent alive and reachable",
        "ServiceID": "",
        "ServiceName": ""
    }]
}]

Dado um agente Consul ou um endereço do servidor, qualquer parte do código no cluster com capacidade para solicitações de HTTP, agora poderá consumir essas informações. Em breve vou explicar como processar, mas antes disso, vamos ver como registrar serviços que estejam fora do nosso controle e, como bônus, como automatizar as etapas acima com o Docker.

Serviços externos

Para não precisar "reinventar a roda", uma boa ideia é integrar serviços de terceiros no aplicativo. Mas, nesse caso, não é possível iniciar um agente Consul no nó apropriado. Mais uma vez, o Consul cuida disso para você (consulte Lista 7).

Lista 7. Consultando a API HTTP do Consul
# manually register mailgun service through the HTTP API
curl -X PUT -d \
    '{"Datacenter": "dc1", "Node": "mailgun", "Address": "http://www.mailgun.com",
 "Service": {"Service": "email", "Port": 80}, "Check": {"Name": "mailgun api", 
 "http": "www.status.mailgun.com", "interval": "360s", "timeout": "1s"}}' \
    http://$CONSUL_IP:8500/v1/catalog/register

# looks like we're all good !
curl $CONSUL_IP:8500/v1/catalog/services
{"consul":[],"email":[],"mongo":["database","nosql"]}

Como o Mailgun é um serviço da web, use o campo HTTP para verificar a disponibilidade da API. Para aprofundar-se nos superpoderes do Consul, consulte sua documentação abrangente.

Integração do Docker

Até agora, um binário Go, um único arquivo JSON e algumas solicitações de HTTP ativaram o fluxo de trabalho de descoberta do serviço. Você não está amarrado a uma tecnologia específica, mas como mencionado anteriormente, essa configuração ágil é especialmente adequada para microserviços.

Nesse contexto, o Docker permite empacotar serviços em um contêiner de autorregistro reproduzível. Dado o mongo.json existente, somente são necessários o Dockerfile e o Procfile em Lista 8.

Lista 8. Empacote serviços em um contêiner de autorregistro reproduzível
# Dockerfile
# start from official mongo image
FROM mongo:3.0

RUN apt-get update && apt-get install -y unzip

# install consul agent
ADD https://dl.bintray.com/mitchellh/consul/0.5.2_linux_amd64.zip /tmp/consul.zip
RUN cd /bin && \
    unzip /tmp/consul.zip&& \
    chmod +x /bin/consul && \
    mkdir -p {/data/consul,/etc/consul.d} && \
    rm /tmp/consul.zip

# copy service and check definition, as we wrote them earlier
ADD mongo.json /etc/consul.d/mongo.json

# Install goreman - foreman clone written in Go language
ADD https://github.com/mattn/goreman/releases/download/v0.0.6
/goreman_linux_amd64.tar.gz /tmp/goreman.tar.gz
RUN tar -xvzf /tmp/goreman.tar.gz -C /usr/local/bin --strip-components 1 && \
    rm -r  /tmp/goreman*

# copy startup script
ADD Procfile /root/Procfile

# launch both mongo server and consul agent
ENTRYPOINT ["goreman"]
CMD ["-f", "/root/Procfile", "start"]

Os Dockerfiles permitem definir um único comando a ser executado ao inicializar contêineres. No entanto, agora é necessário executar o MongoDB e o Consul. O Goreman permite fazer isso. Ele lê um arquivo de configuração chamado Procfile, definindo vários processos para gerenciar (ciclo de vida, ambiente, logs, etc.). Uma abordagem dessa no campo dos contêineres é um debate em si, e existem outras soluções, mas por agora, ele executa a tarefa necessária de uma maneira simples.

Lista 9. Procfile
# Procfile
database: mongod
consul: consul agent -join $CONSUL_HOST -data-dir /data/consul -config-dir
/etc/consul.d
Lista 10. Comandos shell para criar o contêiner
ls
Dockerfile  mongo.json  Procfile

docker build -t article/mongo .
# ...

docker run --detach --name docker-mongo \
    --hostname docker-mongo-2 \  # if not explicitly configured, consul agent 
set its name to the node hostname
    --env CONSUL_HOST=$CONSUL_IP article/mongo

curl $CONSUL_IP:8500/v1/catalog/nodes
[
    {
        "Node": "consul-server-1",
        "Address": "172.17.0.1"
    }, {
        "Node": "docker-mongo-2",
        "Address": "172.17.0.3"
    }, {
        "Node": "mailgun",
        "Address": "http://www.mailgun.com"
    }, {
        "Node": "mongo-1",
        "Address": "172.17.0.2"
    }
]

Perfeito! O Docker e a descoberta de serviço trabalhando juntos funcionam muito bem!

É possível buscar mais detalhes consultando $CONSUL_IP:8500/v1/catalog/service/mongo como em Lista 6, e localizar a porta de serviço. O Consul expõe o IP do contêiner como o endereço de serviço. Essa abordagem funcionará desde que o contêiner exponha a porta, mesmo se o Docker a tiver mapeado para um valor aleatório no host. No entanto, em uma topologia de vários hosts, será necessário mapear explicitamente a porta do contêiner para o mesmo host. Para evitar essa limitação, considere o Weave.

Para resumir, aqui está como é possível expor informações de serviços em diversos datacenters:

  1. Ative pelo menos um servidor Consul e armazene seu endereço.
  2. Em cada nó:
    1. Faça o download do binário do Consul.
    2. Grave o serviço e verifique as definições no diretório de configuração do Consul.
    3. Ative o aplicativo.
    4. Ative o agente Consul com o endereço de outro agente do servidor.

Crie aplicativos com reconhecimento de infraestrutura

Você acabou de construir um fluxo de trabalho fácil e não invasivo para implementar e registrar novos serviços. A próxima etapa lógica é exportar esse conhecimento para os aplicativos dependentes.

The Twelve-Factor App, uma metodologia para desenvolver apps de software como serviço, é uma boa razão para o armazenamento de configuração no ambiente:

  • Manter uma separação rigorosa entre a configuração e o código em constante mudança.
  • Evitar que sejam inseridas informações confidenciais nos repositórios.
  • Manter a linguagem e a agnóstica do sistema operacional.

Este é o momento de gravar um wrapper com capacidade de consultar um terminal Consul quanto a serviços disponíveis, exportar suas propriedades de conexão para o ambiente e executar o comando fornecido. A escolha da linguagem Go fornece um potencial binário entre plataformas (como as outras ferramentas, até agora) e acesso à API do cliente oficial (consulte Lista 11).

Lista 11. Empacote serviços em um contêiner de autorregistro reproduzível
package main

import (
    "strconv"
    "strings"
    "flag"
    "log"
    "os"
    "os/exec"
    "fmt"

    "github.com/hashicorp/consul/api"
)

// critical quits on errors with a debug message
func critical(err error) {
    if err != nil {
        log.Printf("error: %v", err)
        os.Exit(1)
    }
}

// inject exports properties into runtime environment
func inject(properties map[string]string) []string {
    // read current process environment
    processEnv := os.Environ()
    // allocate and copy it
    env := make([]string, len(processEnv), len(properties) + len(processEnv))
    copy(env, processEnv)

    for k, v := range properties {
        // format key/value mapping as exec.Command and system style (i.e. KEY=VALUE)
        env = append(env, fmt.Sprintf("%s=%s", k, v))
    }
    return env
}

// discoverServices queries Consul for services data
func discoverServices(addr string, healthyOnly bool) map[string]string {
    servicesEnv := make(map[string]string)
    // initialize consul api client
    consulConf := api.DefaultConfig()
    consulConf.Address = addr
    client, err := api.NewClient(consulConf)
    critical(err)

    // retrieve full list of services throughout our infrastructure
    services, _, err := client.Catalog().Services(&api.QueryOptions{})
    critical(err)
    for name, _ := range services {
        // query healthy services information
        servicesData, _, err := client.Health().Service(name, "", healthyOnly, 
&api.QueryOptions{})
        critical(err)
        // loop over this category of service
        for _, entry := range servicesData {
            // store connection information like environment variables : {"MONGO_HOST":
"172.17.0.5"}
            id := strings.ToUpper(entry.Service.ID)
            servicesEnv[id + "_HOST"] = entry.Node.Address
            servicesEnv[id + "_PORT"] = strconv.Itoa(entry.Service.Port)
        }
    }
    return servicesEnv
}

func main() {
  flag.Parse()
  // keep it consistent and read consul service address from environment
  consulAddress = os.Getenv("CONSUL")
  command = flag.Args()

  log.Printf("inspecting infrastructure")
  services := discoverServices(consulAddress, true)
  env := inject(services)

  log.Printf("running `%s`", strings.Join(command, " "))
  cmd := exec.Command(command[0], command[1:]...)
  cmd.Stdout = os.Stdout
  cmd.Stderr = os.Stderr
  cmd.Env = env

  critical(cmd.Start())
  critical(cmd.Wait())
}

O próximo comando, em Lista 12, compila este protótipo e valida seu comportamento.

Lista 12. Compile e valide o protótipo
# install the single dependency
go get github.com/hashicorp/consul
# compile to `wrapper` (depends on your directory name)
go build ./...

export CONSUL=$CONSUL_IP:8500
./wrapper env

O último comando deve imprimir algo como MONGO_PORT=27017, entre outras variáveis. Agora, qualquer comando deve ter a capacidade de ler dados de serviços a partir de seu ambiente.

Reconfigure a infraestrutura dinamicamente

Ainda é provável que ocorra alguma situação que desafie a implementação atual. Um app da web poderia ser iniciado como esse acima e conectado ao MongoDB, e ainda poderiam ocorrer problemas durante as falhas ou migrações do banco de dados. O que queremos é atualizar dinamicamente o conhecimento do aplicativo quando a infraestrutura estiver enfrentando mudanças normais ou inesperadas.

Embora o design de uma solução robusta para esse problema possa exigir um tutorial próprio, o Consul Template apresenta uma abordagem interessante.

O Consul Template consulta uma instância do Consul e atualiza qualquer número de modelos especificados no sistema de arquivos. Como um bônus agregado, o Consul Template pode executar comandos arbitrários quando uma atualização de modelo é concluída. Portanto, o Consul Template pode ser usado para monitorar serviços (endereços e funcionamento) e reiniciar o aplicativo automaticamente sempre que uma mudança é detectada. Como o wrapper buscará dados de serviços, o ambiente de tempo de execução espelhará o estado correto da infraestrutura (consulte Lista 13).

Lista 13. Use o Consul Template para monitorar serviços e reiniciar o aplicativo
consul-template \
    -consul $CONSUL \
    -wait 1s  \  # Avoid re-running multiple times on changes
    -template "app.ctmpl:/tmp/app.conf:./wrapper env"

Agora, você desfruta de todos os benefícios de um arquivo de configuração de modelo. A Listagem 14 é um exemplo adaptado do hackathon-starter no GitHub.

Lista 14. Exemplo de um arquivo de configuração de modelo
// app.ctmpl

// store third-party service information in the environment
db: 'mongodb://' + process.env.MONGO_HOST + ':' + process.env.MONGO_PORT + '/test',

// or you can leverage consul-template built-in service discovery
{{ range service "mongo" }}
      db2: 'mongodb://{{ .Address }}:{{ .Port }}/test',
{{ end}}

// Use consul-template to fetch information from consul kv store
// curl -X PUT "http://$CONSUL/v1/kv/hackathon/mailgun_user" -d "xavier"
mailgun: {
    user: '{{ key "hackathon/mailgun_user" }}',
    password: '{{ key "hackathon/mailgun_password" }}'
}

Esta experiência requer mais raciocínio. Pode ser complicado, por exemplo, reiniciar o aplicativo para atualizar seu conhecimento de serviços. Nesse caso, seria possível enviá-lo um sinal especial para que ele pudesse manipular as mudanças corretamente. No entanto, para isso seria necessário alterar o código base do aplicativo e, até agora, ele não precisava reconhecer nada. Além disso, o surgimento de microserviços em provedores em nuvem passíveis de falhas deve encorajar a execução de apps stateless, resistentes a falhas.

Contudo, a combinação de ferramentas eficientes com contratos claros permite integrar aplicativos distribuídos em infraestruturas complexas, sem limitação a um provedor ou a uma pilha de aplicativos específica.

Conclusão

A descoberta de serviço e, mais amplamente, a orquestração de serviços, é um dos desafios mais estimulantes do desenvolvimento moderno. Importantes participantes do setor, juntamente com a comunidade de desenvolvedores, estão trabalhando ativamente e dando grande impulso nas tecnologias e ideais.

O IBM Cloud™, por exemplo, trata desse desafio com planejadores de carga de trabalho, bancos de dados inteligentes, monitoramento, gerenciamento de custos, sincronização de dados, API REST e muito mais. Somente um amplo conjunto de ferramentas pode permitir que os desenvolvedores se concentrem unicamente nos módulos fracamente acoplados de seus aplicativos.

Graças ao Consul e à Go, é possível seguir nessa direção e desenvolver um conjunto de serviços, como:

  • Autorregistro
  • Autoatualização
  • Agnosticismo de pilha
  • Implementação de drop-in
  • Facilidade de uso de contêiner

Este tutorial apresentou o básico de uma implementação de produção e mostrou como uma abordagem plug and play da descoberta de serviço libera-o para concentrar-se nas outras partes do pipeline de uma implementação moderna, sem todas as restrições comuns. Etapas adicionais poderiam incluir ampliar o wrapper com criptografia e oferecer uma integração consistente para expor as credenciais com segurança como tokens de serviços. Espero que este tutorial tenha oferecido ideias para enfrentar os desafios das implementações ultra-ágeis na nuvem.


Recursos para download


Temas relacionados

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Cloud computing
ArticleID=1022432
ArticleTitle=Ative a descoberta de serviço plug and play com o Consul e o Docker
publish-date=11262015