Back-End

26 fev, 2016

Servidor echo simples escrito em Go, dockerizado!

Publicidade

Neste artigo, vamos ver como escrever um aplicativo de servidor Go (muito) simples e como colocá-lo em um container Docker e executá-lo.

go-1

Aviso: Eu sou um novato tanto com Go quanto com Docker, e este é apenas um experimento que eu fiz para começar a explorar essas duas tecnologias interessantes. Se você encontrar algo estranho ou errado, te encorajo a gritar comigo nos comentários, eu serei extremamente grato.

Introdução

Iremos escrever um servidor echo simples em Go, em seguida, vamos empacotá-lo em um container Docker para futura execução/distribuição.

“Luciano, você é um desenvolvedor PHP, por que se preocupar com Go?”.

Boa pergunta! Bom, existem pelo menos duas razões para essa escolha:

  • Experimentando! Sim Go parece ser a próxima coisa legal por ai, e eu queria ter uma desculpa para experimentar.
  • Simplicidade! Escrever um aplicativo de servidor em Go é apenas uma questão de escrever algumas linhas de código em um arquivo, então é um cenário muito simples para testar a integração com Docker.

Para aqueles que não estão familiarizados com essas duas tecnologias, sugiro dar uma olhada nos sites oficiais (Docker.com e Golang.org), pois eu não irei fornecer muitos detalhes específicos sobre eles.

Vou supor que você tem ambas as tecnologias instaladas em sua máquina. Se você está usando o Windows ou um Mac, dê uma olhada no Boot2Docker, ele irá definitivamente tornar a sua vida mais fácil.

Saiba que Go em sua máquina local é necessário apenas se você quiser testar o aplicativo antes de “dockerizá-lo”. Vamos colocar o Go runtime no container Docker na segunda parte do artigo e, dessa forma, seremos capazes de executá-lo sem ter Go instalado em nossa máquina local (o que é de fato a vantagem real do uso do Docker).

O servidor echo Go

Ok, antes de mais nada, vamos escrever e testar nosso aplicativo de servidor. O aplicativo deve abrir um socket e escutar as solicitações TCP em uma determinada porta. Então, quando ele receber algum dado, deve responder dizendo algo como “Oi, eu recebi sua mensagem! Foi X bytes de comprimento e ele disse: XXX! Honestamente, eu não tenho ideia do que fazer com suas mensagens, então Bye Bye!”.

Então vamos escrever um pouco de código. O script a seguir foi amplamente inspirado por este aqui.

//server.go

package main

import (  
    "fmt"
    "net"
    "os"
    "strconv"
    "bytes"
)

const (  
    CONN_HOST = ""
    CONN_PORT = "3333"
    CONN_TYPE = "tcp"
)

func main() {  
    // Listen for incoming connections.
    l, err := net.Listen(CONN_TYPE, ":"+CONN_PORT)
    if err != nil {
        fmt.Println("Error listening:", err.Error())
        os.Exit(1)
    }
    // Close the listener when the application closes.
    defer l.Close()
    fmt.Println("Listening on " + CONN_HOST + ":" + CONN_PORT)
    for {
        // Listen for an incoming connection.
        conn, err := l.Accept()
        if err != nil {
            fmt.Println("Error accepting: ", err.Error())
            os.Exit(1)
        }

        //logs an incoming message
        fmt.Printf("Received message %s -> %s \n", conn.RemoteAddr(), conn.LocalAddr())

        // Handle connections in a new goroutine.
        go handleRequest(conn)
    }
}

// Handles incoming requests.
func handleRequest(conn net.Conn) {  
  // Make a buffer to hold incoming data.
  buf := make([]byte, 1024)
  // Read the incoming connection into the buffer.
  reqLen, err := conn.Read(buf)
  if err != nil {
    fmt.Println("Error reading:", err.Error())
  }
  // Builds the message.
  message := "Hi, I received your message! It was "
  message += strconv.Itoa(reqLen) 
  message += " bytes long and that's what it said: \"" 
  n := bytes.Index(buf, []byte{0})
  message += string(buf[:n-1])
  message += "\" ! Honestly I have no clue about what to do with your messages, so Bye Bye!\n"

  // Write the message in the connection channel.
  conn.Write([]byte(message));
  // Close the connection when you're done with it.
  conn.Close()
}

Ok, o código é comentado, o que deve ser compreensível o suficiente. Vamos tentar ver se funciona.

Basta iniciar o servidor com o comando:

go run server.go

Teremos o nosso servidor funcionando, e ele irá imprimir algo como:

Listening on :3333

Vamos abrir outra janela de terminal para tentar falar com ele. Nós vamos usar o netcat:

echo "Hello server" | nc localhost 3333

Isyo é o que devemos ver nas duas janelas de terminal:

go-2

A janela de terminal no topo está executando o nosso servidor e registrando todas as solicitações recebidas (imprimindo o endereço TCP de entrada e o local), enquanto o segundo terminal é nosso cliente que enviou uma mensagem simples “Olá servidor” e recebeu uma resposta muito útil a partir do servidor.

Isso é tudo do lado do Go. Na próxima parte do artigo, vamos ver como “dockerizar” esse app Go simples. Por enquanto, você pode, obviamente, sair do servidor com CTRL+C.

Dockerizando a aplicação

Ok, agora nós queremos “dockerizar” esse aplicativo simples. Espera, o que significa “dockerizar”? Bem, ao “dockerizar” uma aplicação, somos capazes de colocar o aplicativo em si e todas as suas dependências dentro de um container Docker que pode ser facilmente transportado e executado em outros lugares.

Imagens, containers e dockerfile(s)

Um trecho da documentação do Docker deve tornar as coisas um pouco mais claras:

Docker permite que aplicativos sejam rapidamente montados a partir de componentes e elimina o atrito entre desenvolvimento, controle de qualidade e ambientes de produção. Como resultado, a TI pode enviar mais rápido e executar o mesmo aplicativo, sem alterações, em laptops, data centers VMs, e em qualquer nuvem.

Existem várias maneiras de dockerizar um aplicativo e criar um container Docker dedicado. O que eu prefiro no momento é por meio da adoção de um “Dockerfile”.

“Dockerfile” é um arquivo chamado exatamente de Dockerfile que contém várias instruções reproduzíveis para criar uma imagem Docker a partir do zero.

Que diabos é uma imagem e como ela é diferente de um container?

Se você é um noob com Docker (como eu), provavelmente está se fazendo essa pergunta. Quero citar uma brilhante resposta do stackoverflow que deve tornar as coisas um pouco mais claras:

Uma imagem é o conjunto de camadas que são construídas e podem ser movidas. As imagens são apenas para leitura.

http://docs.docker.io/en/latest/terms/image/ http://docs.docker.io/en/latest/terms/layer/ Um container é uma instanciação dinâmica ativa (ou inativa ) de uma imagem.

http://docs.docker.io/en/latest/terms/container/

É interessante visitar esses links se as definições ainda não estão claras.

Agora vamos voltar para o nosso Dockerfile. Cada Dockerfile contém alguns metadados (imagem base para começar, o nome do mantenedor etc.) e um conjunto de instruções usado para construir a imagem. Essas instruções são normalmente utilizadas para “instalar” todas as dependências necessárias e descrevem como executar o aplicativo.

Vamos escrever o Dockerfile

Chega de conversa, vamos pular para o código:

FROM ubuntu:12.04

MAINTAINER Luciano Mammino

RUN apt-get install -y python-software-properties

RUN add-apt-repository ppa:duh/golang  
RUN apt-get update  
RUN apt-get install -y golang

ADD server.go /var/server/server.go

EXPOSE 3333

CMD ["go", "run", "/var/server/server.go"]

Nota: Um leitor mencionou em um comentário que esse dockerfile não está mais funcionando (provavelmente o PPA foi removido ou estava temporariamente indisponível). Se esse é o seu caso, você pode usar um dockerfile mínimo otimizado para Golang fornecido pelo no próprio blog do Golang.

Vamos analisar todas as instruções uma a uma:

  • A partir do Ubuntu: 12.04 defina a imagem de base a partir da qual começar. Neste caso, vamos utilizar a imagem ubuntu: 12.04, que é muito leve e baseada no Ubuntu 12.04.
  • MAINTAINER Luciano Mammino: autoexplicativo o suficiente? ;).
  • RUN apt-get install -y python-software-properties instala o pacote python-software-properties para ser capaz de executar add-apt-repository em seguida.
  • RUN add-apt-repository ppa:duh/golang, RUN apt-get update, RUN apt-get install -y golang e RUN apt-get install -y golang: adicionam um repositório personalizado para o Go e o instala.
  • ADD server.go /var/server/server.go: copia o arquivo server.go na imagem (armazenando-a em var/servidor/folder).
  • EXPOSE 3333: expõe a porta 3333 para permitir que os containers lançados a partir dessa imagem possam ouvir nessa porta. Precisamos exatamente dessa porta, porque é a usada pela nossa aplicação Go.
  • CMD [“go”, “run”, “/var/server/server.go”]: descreve o comando para executar nossa aplicação Go quando o container é lançado (sim, ele irá executar go run /var/server/server.go).

Vamos construir!

Antes de construir a imagem do nosso Dockerfile, se você estiver usando boot2docker, você precisa executá-lo com:

boot2docker start

Caso ele imprima algo como isto:

2014/06/29 16:09:32 Started.  
2014/06/29 16:09:32 To connect the Docker client to the Docker daemon, please set:  
2014/06/29 16:09:32     export DOCKER_HOST=tcp://192.168.59.103:2375

Execute o comando export sugerido. Também tome nota do endereço IP dado, ele será necessário mais tarde para conectar ao nosso servidor.

Agora é só ir para a pasta que contém o Dockerfile e o arquivo server.go, e executar:

docker build -t goecho .

Observe que . se refere à pasta atual (sim, você entendeu, ele diz ao Docker para procurar um Dockerfile ali mesmo).

A opção -t goecho não é obrigatória e é utilizada para “dar um nome conveniente” para a imagem gerada.

Você vai ver uma série de comandos sendo executados (sim, aqueles que escrevemos em nosso Dockerfile) e, se tudo correr bem, você deve ver algo como isto aqui no final:

Successfully built 713c09526bc1

Você também pode executar:

docker images

Para listar todas as imagens disponíveis, você deve ver nosso goecho no topo da lista.

Nossa imagem está pronta para ser executada e transformada em um container vivo. Vamos ver como fazer isso na próxima parte do artigo.

Executando o container

Executar a imagem e ter um container vivo é apenas uma questão de executar:

docker run -i -t -p 3333:3333 goecho

As opções -i e -t nos permitem executar o container no modo interativo, o que permite desligá-lo com CTRL+C quando necessário.

A opção -p 3333: 3333 mapeia a porta do container 3333 para a mesma porta na nossa máquina. Ela vai efetivamente nos permitir falar com o servidor echo usando a porta 3333.

Agora o nosso container está vivo e funcionando, e nosso aplicativo de servidor está ativo.

Vamos abrir uma nova janela do terminal e tentar novamente:

echo "Hello server" | nc <ip> 3333

<ip> é localhost se você não estiver usando boot2docker. Se você estiver usando-o, você precisa fornecer o IP da máquina virtual intermediária fornecida por boot2docker. É aquele que registramos anteriormente, mas, se você não conseguir encontrar, pode simplesmente executar boot2docker ip para descobrir (normalmente é 192.168.59.103).

Conclusões

Isso é apenas o começo com Docker, há várias outras características interessantes e abordagens. Eu provavelmente irei escrever outro artigo enquanto eu continuar aprendendo e usando Docker, portanto, fique atento e escreva todas as suas sugestões nos comentários.

***

Luciano Mammino faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://loige.co/simple-echo-server-written-in-go-dockerized/