Há algumas semanas, escrevi sobre a implantação contínua de um aplicativo escrito com a linguagem de programação Go usando um serviço popular chamado Travis CI. Esse exemplo demonstrou a criação de um aplicativo que usava um Couchbase banco de dados NoSQL, criando testes de unidade, executando esses testes no pipeline de integração contínua do Golang e, por fim, implantando o aplicativo em algum servidor remoto quando tudo for bem-sucedido.
O Travis CI não é o único serviço que oferece esses recursos. Na verdade, você pode hospedar seu próprio serviço de CI/CD usando o Jenkins.
Veremos como usar o Jenkins para um pipeline de um aplicativo Golang, permitindo a integração e a implantação contínuas.
Se você ainda não leu meu tutorial anterior Golang com Travis CIRecomendo que você o faça, pois ele fornece muitas explicações úteis. Muito do mesmo material aparecerá aqui, mas será explicado de forma diferente, portanto, duas explicações podem ser úteis.
Se quiser experimentar de verdade este tutorial Jenkins with Golang, você precisará de Servidor Couchbase instalado em algum lugar. O objetivo é fazer com que o aplicativo seja executado e use essa instância do banco de dados após a implantação.
Desenvolvimento de um aplicativo Go com Couchbase
Para ter sucesso com este tutorial, precisaremos de um aplicativo Go para testar e implantar. Se você quiser se adiantar, fiz o upload de um projeto funcional no GitHub. Na verdade, é o mesmo projeto do exemplo do Travis CI.
Se você preferir conhecer o projeto, vamos dedicar algum tempo para isso.
Em algum lugar em seu $GOPATH criar um arquivo chamado main.go e inclua o seguinte código Go. Vamos detalhá-lo depois.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
package main import ( "fmt" "os" gocb "gopkg.in/couchbase/gocb.v1" ) type BucketInterface interface { Get(key string, value interface{}) (gocb.Cas, error) Insert(key string, value interface{}, expiry uint32) (gocb.Cas, error) } type Database struct { bucket BucketInterface } type Person struct { Type string `json:"type"` Firstname string `json:"firstname"` Lastname string `json:"lastname"` } func (d Database) GetPersonDocument(key string) (interface{}, error) { var data interface{} _, err := d.bucket.Get(key, &data) if err != nil { return nil, err } return data, nil } func (d Database) CreatePersonDocument(key string, data interface{}) (interface{}, error) { _, err := d.bucket.Insert(key, data, 0) if err != nil { return nil, err } return data, nil } func main() { fmt.Println("Starting the application...") var database Database cluster, _ := gocb.Connect("couchbase://" + os.Getenv("DB_HOST")) cluster.Authenticate(gocb.PasswordAuthenticator{Username: os.Getenv("DB_USER"), Password: os.Getenv("DB_PASS")}) database.bucket, _ = cluster.OpenBucket(os.Getenv("DB_BUCKET"), "") fmt.Println(database.GetPersonDocument("8eaf1065-5bc7-49b5-8f04-c6a33472d9d5")) database.CreatePersonDocument("blawson", Person{Type: "person", Firstname: "Brett", Lastname: "Lawson"}) } |
O aplicativo não faz muita coisa, mas há muita coisa acontecendo.
Nas importações, você notará que usamos o SDK do Couchbase para Go. Para poder compilar esse projeto, você precisará fazer o download do SDK. Isso pode ser feito com o seguinte comando:
|
1 |
go get gopkg.in/couchbase/gocb.v1 |
Antes de começarmos a analisar o código, precisamos dar um passo atrás e descobrir como esse aplicativo deve funcionar.
O objetivo aqui é conectar-se ao banco de dados NoSQL, Couchbase, recuperar alguns dados e criar alguns dados. Naturalmente, isso seria muito fácil por meio do SDK, mas queremos criar testes de unidade para o nosso aplicativo. É uma prática recomendada nunca testar um banco de dados em um teste de unidade. Guarde isso para seus testes de integração. Se não estivermos testando o banco de dados, precisaremos criar cenários de simulação.
Em vez de criar um monte de maluquices, a melhor maneira de dividir entre cenários reais e simulados é criar uma interface para ambos com Go. O aplicativo principal usará as classes reais como parte da interface, enquanto os testes usarão a simulação.
Por esse motivo, precisamos criar uma interface para o Couchbase Go SDK Balde componente.
|
1 2 3 4 |
type BucketInterface interface { Get(key string, value interface{}) (gocb.Cas, error) Insert(key string, value interface{}, expiry uint32) (gocb.Cas, error) } |
Um Couchbase Bucket tem muito mais funções do que Obter e Inserirmas essas serão as únicas funções que usaremos neste exemplo. Para simplificar, mais adiante no aplicativo, criaremos uma função estrutura com a nova interface.
|
1 2 3 |
type Database struct { bucket BucketInterface } |
Haverá apenas um modelo de dados para este exemplo. Usaremos um modelo de dados baseado no modelo Pessoa estrutura de dados. Ela pode ser alterada livremente sem afetar nosso aplicativo.
Dê uma olhada em uma de nossas funções para a qual eventualmente teremos testes unitários:
|
1 2 3 4 5 6 7 8 |
func (d Database) GetPersonDocument(key string) (interface{}, error) { var data interface{} _, err := d.bucket.Get(key, &data) if err != nil { return nil, err } return data, nil } |
No GetPersonDocument estamos usando uma função BucketInterface e obter um documento específico pela chave do documento.
Da mesma forma, se quisermos criar dados, teremos o seguinte:
|
1 2 3 4 5 6 7 |
func (d Database) CreatePersonDocument(key string, data interface{}) (interface{}, error) { _, err := d.bucket.Insert(key, data, 0) if err != nil { return nil, err } return data, nil } |
Sinto que preciso reiterar isso, mas essas funções foram projetadas para serem mais complexas do que o necessário. Estamos fazendo isso porque queremos demonstrar alguns testes. Se você se sentir melhor, adicione um pouco mais de complexidade a elas em vez de apenas Obter e Inserir funcionalidade.
Por fim, temos o seguinte, que é executado em tempo de execução:
|
1 2 3 4 5 6 7 8 9 |
func main() { fmt.Println("Starting the application...") var database Database cluster, _ := gocb.Connect("couchbase://" + os.Getenv("DB_HOST")) cluster.Authenticate(gocb.PasswordAuthenticator{Username: os.Getenv("DB_USER"), Password: os.Getenv("DB_PASS")}) database.bucket, _ = cluster.OpenBucket(os.Getenv("DB_BUCKET"), "") fmt.Println(database.GetPersonDocument("8eaf1065-5bc7-49b5-8f04-c6a33472d9d5")) database.CreatePersonDocument("blawson", Person{Type: "person", Firstname: "Brett", Lastname: "Lawson"}) } |
Quando o aplicativo é executado, estabelecemos uma conexão com o Couchbase usando variáveis de ambiente. O Bucket aberto é definido como nosso BucketInterfacee, em seguida, as duas funções são executadas.
Então, como podemos testar isso?
Crie um arquivo em seu projeto chamado main_test.go com o seguinte código:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
package main import ( "encoding/json" "os" "testing" "github.com/mitchellh/mapstructure" gocb "gopkg.in/couchbase/gocb.v1" ) type MockBucket struct{} var testdatabase Database func convert(start interface{}, end interface{}) error { bytes, err := json.Marshal(start) if err != nil { return err } err = json.Unmarshal(bytes, end) if err != nil { return err } return nil } func (b MockBucket) Get(key string, value interface{}) (gocb.Cas, error) { switch key { case "nraboy": err := convert(Person{Type: "person", Firstname: "Nic", Lastname: "Raboy"}, value) if err != nil { return 0, err } default: return 0, gocb.ErrKeyNotFound } return 1, nil } func (b MockBucket) Insert(key string, value interface{}, expiry uint32) (gocb.Cas, error) { switch key { case "nraboy": return 0, gocb.ErrKeyExists } return 1, nil } func TestMain(m *testing.M) { testdatabase.bucket = &MockBucket{} os.Exit(m.Run()) } func TestGetPersonDocument(t *testing.T) { data, err := testdatabase.GetPersonDocument("nraboy") if err != nil { t.Fatalf("Expected `err` to be `%s`, but got `%s`", "nil", err) } var person Person mapstructure.Decode(data, &person) if person.Type != "person" { t.Fatalf("Expected `type` to be %s, but got %s", "person", person.Type) } } func TestCreatePersonDocument(t *testing.T) { _, err := testdatabase.CreatePersonDocument("blawson", Person{Type: "person", Firstname: "Brett", Lastname: "Lawson"}) if err != nil { t.Fatalf("Expected `err` to be `%s`, but got `%s`", "nil", err) } } |
Você perceberá que esse arquivo é bastante longo e que também estamos incluindo outro pacote personalizado. Antes de analisarmos o código, vamos fazer o download desse pacote. Na linha de comando, execute o seguinte:
|
1 |
go get github.com/mitchellh/mapstructure |
O pacote mapstructure nos permitirá pegar mapas e convertê-los em estruturas de dados reais, como o Pessoa estrutura de dados que havíamos criado anteriormente. Basicamente, isso nos dá um pouco de flexibilidade no que podemos fazer.
Se você quiser saber mais sobre o pacote mapstructure, confira um artigo anterior que escrevi intitulado, Decodificar valores de mapas em estruturas nativas da Golang.
Com as dependências instaladas, agora podemos dar uma olhada no código. Lembra-se de como usamos o Bucket do Go SDK em nosso código principal? No código de teste, não faremos isso.
|
1 2 3 |
type MockBucket struct{} var testdatabase Database |
Em nosso código de teste, estamos criando um arquivo estruturamas estamos definindo-o como BucketInterface no Banco de dados que foi criada em nosso código principal.
A configuração real da estrutura de dados ocorre no TestMain que é executada antes de todos os outros testes:
|
1 2 3 4 |
func TestMain(m *testing.M) { testdatabase.bucket = &MockBucket{} os.Exit(m.Run()) } |
Agora, como estamos usando um MockBucketEle não tem todas as funções que o gocb.Bucket poderia ter tido. Em vez disso, precisamos confiar na BucketInterface definição.
Precisamos criar um Obter e um Inserir conforme definido na interface.
Começando com o Obter temos o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
func convert(start interface{}, end interface{}) error { bytes, err := json.Marshal(start) if err != nil { return err } err = json.Unmarshal(bytes, end) if err != nil { return err } return nil } func (b MockBucket) Get(key string, value interface{}) (gocb.Cas, error) { switch key { case "nraboy": err := convert(Person{Type: "person", Firstname: "Nic", Lastname: "Raboy"}, value) if err != nil { return 0, err } default: return 0, gocb.ErrKeyNotFound } return 1, nil } |
Se estivermos usando um MockBucket e tentamos ObterEsperamos que apenas uma chave seja válida. Lembre-se de que este é um teste, portanto, somos nós que estabelecemos as regras. Se garoto é usado como uma chave, retornamos alguns dados simulados; caso contrário, retornamos um chave não encontrada erro. Como estamos trabalhando com vários tipos de dados em potencial, precisamos converter nossos dados usando a função converter função. Essencialmente, estamos transformando uma interface em JSON e, em seguida, transformando-a de volta.
Agora vamos dar uma olhada nessa simulação Inserir função.
|
1 2 3 4 5 6 7 |
func (b MockBucket) Insert(key string, value interface{}, expiry uint32) (gocb.Cas, error) { switch key { case "nraboy": return 0, gocb.ErrKeyExists } return 1, nil } |
Se tentarmos inserir dados usando nosso Bucket de simulação, esperamos que a chave não seja igual a garotocaso contrário, gera um erro.
Com as funções de interface criadas, podemos nos concentrar nos testes reais que testam as funções no código principal em Go.
|
1 2 3 4 5 6 7 8 9 10 11 |
func TestGetPersonDocument(t *testing.T) { data, err := testdatabase.GetPersonDocument("nraboy") if err != nil { t.Fatalf("Expected `err` to be `%s`, but got `%s`", "nil", err) } var person Person mapstructure.Decode(data, &person) if person.Type != "person" { t.Fatalf("Expected `type` to be %s, but got %s", "person", person.Type) } } |
O TestGetPersonDocument usará nosso Bucket de simulação no GetPersonDocument function. Lembre-se de que estamos usando interfaces, portanto, o Go descobrirá qual função de interface usar, seja a função real do Couchbase Go SDK ou a função de simulação que usamos. Dependendo dos resultados, é isso que acontece no teste.
|
1 2 3 4 5 6 |
func TestCreatePersonDocument(t *testing.T) { _, err := testdatabase.CreatePersonDocument("blawson", Person{Type: "person", Firstname: "Brett", Lastname: "Lawson"}) if err != nil { t.Fatalf("Expected `err` to be `%s`, but got `%s`", "nil", err) } } |
O TestCreatePersonDocument não é diferente do anterior. Estamos chamando o CreatePersonDocumentmas estamos usando nosso Bucket de simulação com a função de simulação Inserir função.
Neste momento, temos um aplicativo Go funcional com testes e estamos prontos para a integração contínua e a implantação contínua.
Instalação e configuração do Jenkins para implementações de SSH e Golang
A próxima etapa pressupõe que você tenha um servidor remoto pronto para receber implementações. Como eu não tinha, criei um contêiner do Docker com o Ubuntu. Na verdade, tanto a minha instalação do Jenkins quanto o servidor remoto estão usando o Docker.
Se você quiser seguir o que eu fiz, dê uma olhada nisto. Na linha de comando, execute o seguinte para iniciar um contêiner do Ubuntu:
|
1 |
docker run -it --name ubuntu ubuntu /bin/bash |
O comando acima implantará um contêiner do Ubuntu e o nomeará ubuntu. Depois de implantado, você estará conectado por meio do terminal interativo. Não abri nenhuma porta porque a comunicação entre contêineres não precisará de uma porta mapeada.
O contêiner do Ubuntu não terá um servidor SSH disponível, portanto, precisamos instalá-lo. No shell do Ubuntu, execute o seguinte:
|
1 2 3 |
apt-get update apt-get install openssh-server service ssh restart |
Os comandos acima instalarão servidor openssh e iniciá-lo. Enquanto isso, provavelmente deveríamos criar uma combinação de chave pública e privada para o Jenkins usar.
No shell do Ubuntu, execute o seguinte:
|
1 |
ssh-keygen -t rsa |
Quando terminar, copie o ~/.ssh/id_rsa.pub conteúdo em ~/.ssh/authorized_keys pois usaremos a chave privada no servidor Jenkins.
Lembre-se de que também estou usando o Jenkins como um contêiner do Docker. Você não precisa usar nenhum contêiner se não quiser. Tudo deve se traduzir bem.
Se estiver usando o Docker, crie um contêiner do Jenkins executando o seguinte:
|
1 |
docker run -d -p 8080:8080 -p 50000:50000 --name jenkins jenkins |
O comando acima implantará o Jenkins no modo desanexado e mapeará algumas portas para nós.
Quando você visita https://localhost:8080 em seu navegador da Web, siga as etapas do assistente e certifique-se de optar por instalar os plug-ins recomendados.
Quando você chegar ao painel principal do Jenkins, selecione Gerenciar Jenkins -> Gerenciar plug-ins pois precisamos fazer o download de algumas coisas.

Precisaremos de uma maneira de compilar nosso código Go, portanto, precisaremos do Ir plugin. Vamos precisar executar nossos próprios scripts personalizados para a construção, portanto, precisamos do PostBuildScript plugin. Por fim, queremos poder publicar em um servidor remoto e executar comandos, portanto, precisaremos do plug-in Publicar por SSH que vem com outros plug-ins incluídos.
Depois que o download dos plug-ins for concluído, precisaremos configurá-los globalmente.
No painel principal do Jenkins, selecione Gerenciar Jenkins -> Configuração global de ferramentas e procure a seção Go.

Você deverá definir quais versões do Go estão disponíveis. Para este projeto, precisamos apenas da versão 1.8, mas o restante fica a seu critério.
A próxima etapa é configurar nossas chaves SSH para implantação. Lembre-se de que ainda não estamos criando nosso fluxo de trabalho, apenas configurando o Jenkins como um todo.
No painel principal do Jenkins, selecione Gerenciar Jenkins -> Configurar sistema e localize a seção SSH.
Você vai querer fornecer sua chave privada e as informações de conexão do servidor. Se tanto o Jenkins quanto o servidor remoto forem contêineres do Docker na mesma rede que a minha, não se esqueça de usar os endereços IP ou nomes de host do contêiner, e não o host local.
Com tudo configurado, selecione Novo item no painel principal do Jenkins.

Você vai querer dar um nome a ele e selecionar Projeto Freestyle para que possamos adicionar nosso próprio fluxo de trabalho. Observe o nome, pois ele será o binário do projeto que será criado.
Agora podemos definir nosso fluxo de trabalho.
Começaremos com o Gerenciamento de código-fonte seção. Lembre-se, tenho esse projeto em GitHubportanto, você definitivamente deve tirar proveito disso.

Como o Jenkins, neste exemplo, está sendo executado no localhost e não em um domínio, não podemos realmente fazer nada com os acionadores de compilação. Para este exemplo, vamos acionar as coisas manualmente.
Antes de tentarmos executar qualquer script, precisamos definir o ambiente de compilação para a versão do Go especificada anteriormente.

Quando o fluxo de trabalho for iniciado, ele baixará e instalará essa versão do Go antes de executar testes ou compilações.
Para a fase de construção, vamos realizar três etapas diferentes, separadas para manter o fluxo das coisas bem limpo.

A primeira etapa de compilação é fazer o download dos pacotes Go. Depois que tivermos nossos pacotes, poderemos executar nossos testes. Depois de executar os testes, podemos fazer um ir construir para criar nosso binário. Se alguma dessas etapas falhar, toda a compilação falhará, e é assim que deve ser.
A etapa final é a implantação. Na seção Ações pós-construção, queremos enviar nosso binário por SSH e executá-lo.

Na verdade, haverá dois conjuntos de transferências envolvidos nesse processo.
A primeira fase é pegar nosso arquivo de origem, que é o binário, e enviá-lo usando o perfil SSH que criamos anteriormente. Depois que o arquivo for transferido, alteraremos as permissões para que ele possa ser executado.
Depois que o arquivo for carregado, queremos executá-lo de fato usando outro Conjunto de transferência. Em vez de ter um arquivo de código-fonte no segundo conjunto, teremos apenas um comando:
|
1 |
DB_HOST=ec2-34-226-41-140.compute-1.amazonaws.com DB_USER=demo DB_PASS=123456 DB_BUCKET=example ./Golang_CI |
Observe que estou passando variáveis para serem usadas como variáveis de ambiente no aplicativo. Troque-as pelo que você estiver usando ou pense em outra abordagem, como definir essas variáveis no servidor para fins de segurança.
Em teoria, você estaria implantando um aplicativo da Web e esse comando final é usado para iniciar o servidor com informações de conexão.
Conclusão
Você acabou de ver como configurar Jenkins e Golang em um pipeline para implantação contínua. Para completar, usamos de fato o Couchbase e Docker neste exemplo para lidar com o nosso servidor remoto, bem como com o nosso servidor Jenkins. Sua configuração pode ser diferente, mas as etapas são mais ou menos as mesmas.
Se você quiser saber mais sobre como usar o Jenkins e o Go com o Couchbase, confira o artigo Portal do desenvolvedor do Couchbase.