Go en contenedores

Como desplegar tu aplicación Go en un contenedor Docker

Uno de los casos de uso en los que mejor se desenvuelve Go es en una arquitectura de microservicios.

Para explotar lo mejor posible la arquitectura de microservicios lo mejor es utilizar contenedores (docker, k8s, etc…). Si hablamos de producción esto es prácticamente necesario si queremos escalar cada microservicio de forma asimétrica, etc.

Pero a la hora de desarrollar es también muy cómodo. Podemos por ejemplo orquestar estos contenedores con un sencillo archivo de docker-compose.yml y así gestionar los microservicios, las bases de datos que necesitan, etc.

Pre-requisitos

Antes de empezar necesitarás tener instalado en tu sistema:

  • Docker
  • Docker Compose
  • Go

Si usas Windows o Mac lo más sencillo es que utilices Docker Desktop. Si usas linux, puedes revisar este tutorial sobre como instalar docker (community edition) y docker compose en linux

Si no tienes Go, puedes descubrir como instalarlo en este tutorial

Creando el proyecto en Go

Para demostrar como desplegar en Docker crearemos una aplicación sencilla que simplemente ejecute un servidor http en el puerto 8080.

mkdir go-docker
cd go-docker
go mod init yeraycat/go-docker
touch main.go

Dentro del archivo crearemos una función main simple que ponga el servidor HTTP a la escucha y devuelva un mensaje cuando se le envíe una petición.

package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Holi")
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Ejecutando nuestro contenedor con un comando

Llegados a este punto podríamos simplemente ejecutar nuestra aplicación en un contenedor con docker run y la imagen oficial de golang.

docker run -v "$PWD":/app -w /app -p 8080:8080 golang go run .

Esto es una opción aceptable para ejecutarlo en un entorno local de desarrollo, al menos si nuestra aplicación es lo único que tenemos que ejecutar. Pero si dependemos de una base de datos o nuestra aplicación está compuesta de varios microservicios que se ejecutan en distintos contenedores, se vuelve más complicado gestionarlo con comandos de docker.

Docker compose para entornos de desarrollo multi-contenedor

Docker Compose permite gestionar de forma agrupada varios contenedores definidos por un archivo .yml.

Primero crearemos un archivo docker-compose.yml en el directorio del proyecto. Vamos a aprovechar también para añadir un contenedor para la base de datos que ejecute MySQL.

version: '3.1'
services:
  db:
    image: mysql
    command: --default-authentication-plugin=mysql_native_password
    ports:
      - 3306:3306
      
    environment:
      MYSQL_ROOT_PASSWORD: rootexample
      MYSQL_DATABASE: godocker
      MYSQL_USER: godocker
      MYSQL_PASSWORD: example

  app:
    image: golang
    ports:
      - 8080:8080
    volumes:
      - .:/app
    depends_on:
      - db
    command: > 
      bash -c "sleep 2 && cd /app && go run ."

Voy a crear un archivo create-table.sql para importar en la base de datos, para así crear la tabla y los datos iniciales:

DROP TABLE IF EXISTS message;
CREATE TABLE message
(
    id INT AUTO_INCREMENT NOT NULL,
    content VARCHAR(128) NOT NULL,
    PRIMARY KEY (`id`)
);
INSERT INTO message (content) VALUES ('Holi');

Para importarlo, voy a levantar los contenedores primero, con el comando docker-compose up y luego copio el archivo al contenedor de la base de datos. Después ejecuto bash en el contenedor de la base de datos para importar el archivo.

docker cp ./create-table.sql go-docker_db_1:/create-table.sql
docker-compose exec db bash

# En el contenedor
mysql -u root -p godocker < /create-table.sql

Ahora vamos a cambiar el código para que se conecte a la base de datos. Entender este código no es parte del tutorial realmente, pero lo publico para que podáis seguir el ejemplo en vuestra máquina.

package main

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"

	"github.com/go-sql-driver/mysql"
)

var db *sql.DB

type Message struct {
	ID      int64
	Content string
}

func handler(w http.ResponseWriter, r *http.Request) {
	msg, err := messageByID(1)
	if err != nil {
		log.Fatal(err)
		fmt.Fprint(w, "Message not found")
	} else {
		fmt.Fprintf(w, "%s", msg.Content)
	}

}

func messageByID(id int64) (Message, error) {
	var msg Message

	row := db.QueryRow("SELECT * FROM message WHERE id = ?", id)
	if err := row.Scan(&msg.ID, &msg.Content); err != nil {
		if err == sql.ErrNoRows {
			return msg, fmt.Errorf("messageById %d: no such message", id)
		}
		return msg, fmt.Errorf("messageById %d: %v", id, err)
	}
	return msg, nil
}


func main() {
	config := mysql.Config{
		User:                 "godocker",
		Passwd:               "example",
		Net:                  "tcp",
		Addr:                 "db:3306",
		DBName:               "godocker",
		AllowNativePasswords: true,
	}

	var err error

	db, err = sql.Open("mysql", config.FormatDSN())
	if err != nil {
		log.Fatal(err)
	}

	pingErr := db.Ping()
	if pingErr != nil {
		log.Fatal(pingErr)
	}

	fmt.Println("Connected to database!")

	http.HandleFunc("/", handler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Para terminar, añadiremos las dependencias que estamos usando en el nuevo código:

go mod tidy

Tras todo esto, ya podemos levantar nuestros contenedores y empezarán a funcionar.

docker-compose up

Si vamos a nuestro navegador, a http://localhost:8080 podremos ver el mensaje recuperado de base de datos.

Esto es solo el principio y nos servirá para desarrollar usando contenedores, pero estamos utilizando go run para ejecutar nuestra aplicación.

En un próximo tutorial veremos como preparar una imagen lista para desplegar nuestra aplicación en producción, que primero compile el código y luego lo ejecute.