Immagini e linguaggio Go

Immagine con Compilatore Go

Usiamo il metodo manuale:

  • partire da un'immagine di base
  • aggiungere il software necessario
  • clonare il contenitore in una nuova immagine

Partiamo da Alpine:

docker run -ti --name alpine alpine sh

Aggiorniamo l’ambiente e aggiungiamo pacchetti:

apk update
apk add vim
apk add git

Installiamo il supporto go per vim da Github:

cd
git clone https://github.com/fatih/vim-go.git \
~/.vim/pack/plugins/start/vim-go

Installiamo go:

apk add go
apk add musl-dev

Alpine usa la libreria runtime MUSL non GLIBC. Occorre installarne il pacchetto di sviluppo.

Prepariamo lo scaffolding di go:

su -
mkdir -p ~/go/src ~/go/bin ~/go/pkg

Aggiunta dei plugin go a vim. Lanciare vim:

vim

Dare il comando vim (preso dal supporto installato)

:GoInstallBinaries

La possibilità di dare comandi interattivi uno dei motivi per cui alle volte si preferisce una costruzione manuale di immagine a quella tramite Dockerfile. Quest'ultimo permette solo l'esecuzione di comandi batch.

Attendere il completamento e uscire da vim con :q Enter

Prepariamo l’ambiente editando /root/.profile:

cat <<EOF >> /root/.profile
export GOROOT=/usr/lib/go
export GOPATH=/root/go
export GOBIN=\$GOPATH/bin
export GO111MODULE=off
export PATH=~/bin:\$GOBIN:\$GOROOT/bin:\$PATH
EOF

Uscire dal contenitore con Ctrl-P Ctrl-Q, NON col comando exit.

Sulla macchina host generiamo la nuova immagine:

docker commit alpine vimgo

Attenzione: è grosso, ci vuole tempo.

Uso di vimgo

Creare una directory locale da condividere:

mkdir -p ~/bin

Lanciare il contenitore:

docker run --rm -ti --name vimgo \
  -v ~/bin:/root/go/bin vimgo su -

Il comando eseguito, su -, lancia una shell di login di root, che dal .profile setta tutte le variabili d'ambiente.

E' necessario eseguire su - perchè se si esegue semplicemente sh parte una shell senza ambiente.

Scrittura di un semplice programma in Go:

cd go/src
mkdir hello
cd hello
vim hello.go
package main

import "fmt"

func main() {
  fmt.Println("vim-go")
}

Compilazione statica:

go build --buildmode=exe --ldflags '-linkmode external \
  -extldflags "-static" -s' hello.go

Copia nella cartella condivisa:

cp hello ~/go/bin

Uscire dal contenitore con exit. Il contenitore è automaticamente rimosso poichè era stato lanciato con l'opzione --rm.

Provare in locale:

~/bin/hello

Immagine e Go

Produrremo un altro eseguibile Go linkato staticamente e lo inseriremo in un'immagine.

Preparazione del progetto:

mkdir -p ~/docker/ex/go
cd ~/docker/ex/go

Partenza di Vimgo:

```bash
docker run --rm -ti --name vimgo \
  -v $PWD:/root/go/bin vimgo su -

Questa volta mappiamo la directory corrente a quella degli eseguibili Go sul contenitore.

Prepariamo lo scaffolding:

cd go/src
mkdir helloworld
cd helloworld
vim helloworld.go
package main

import (
  "fmt"
  "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, "Hello World from Go in Docker")
}

func main() {
  http.HandleFunc("/", helloHandler)

  fmt.Println("Started, serving at 8080")
  err := http.ListenAndServe(":8080", nil)
  if err != nil {
    panic("ListenAndServe: " + err.Error())
  }
}

E’ un web server sulla porta 8080 che da un semplice messaggio.

Compilazione statica:

go build --buildmode=exe --ldflags '-linkmode external \
  -extldflags "-static" -s' helloworld.go

Copia nella cartella condivisa:

cp helloworld ~/go/bin

Uscire dal contenitore con exit. Il contenitore è automaticamente rimosso.

Generazione dell'Immagine

Dovrebbe esserci nella directory corrente il file eseguibile helloworld, prodotto col contenitore.

Scrivere il Dockerfile:

vim Dockerfile
FROM scratch
ADD ./helloworld /helloworld
EXPOSE 8080
CMD ["/helloworld"]

scratch è il nome di un'immagine vuota.

Un programma Go è già il 99% di un’immagine.

Solitamente si parte da un'immagine di base perche fornisce uno scheletro di ambiente operativo di cui il nostro applicativo ha bisogno: librerie, ma anche aggancio a funzionalità del kernel, come gestione memoria e schedulazione.

Un eseguibile Go linkato staticamente non ha bisogno di librerie. Inoltre è fornito di serie di un monitor operativo, nello spazio user, che fornisce:

  • schedulazione concorrente
  • allocazione di memoria
  • garbage collection

Non ha quindi bisogno neppure di uno stub di sistema operativo sottostante.

Compiere il build:

docker build -t greet .

Lanciare un container per testare:

docker run -d --rm --name saluti greet

Controllare alla URL: localhost:8080.

Al termine fermare il container che verrà automaticamente rimosso:

docker stop saluti

Chain Build

Possiamo compiere due migliorie:

  • usare un'immagine per il compilatore Go già disponibile su Docker Hub, anzichè la nostra vimgo
  • compiere la compilazione e subito la preparazione di un'immagine finale con un unico Dockerfile, usando la tecnica del chain build

Preparare un programma demo:

mkdir -p ~/docker/ex/demo
cd ~/docker/ex/demo
vim main.go
package main

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

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, "Hello, 世界")
}
func main() {
  http.HandleFunc("/", handler)
  fmt.Println("Running demo app. Press Ctrl+C to exit...")
  log.Fatal(http.ListenAndServe(":8888", nil))
}

Preparare il Dockerfile.

vim Dockerfile
FROM golang:1.17-alpine AS build

WORKDIR /src/
COPY main.go go.* /src/
ENV GO111MODULE="off"
RUN CGO_ENABLED=0 go build -o /bin/demo

FROM scratch
COPY --from=build /bin/demo /bin/demo
EXPOSE 8888
ENTRYPOINT ["/bin/demo"]

Build dell’immagine myhello:

docker build -t myhello .

Test dell'immagine:

docker run -d --rm --name try -p 8888:8888 myhello

Connessione al contenitore:

curl localhost:8888

Al termine del test, stop del contenitore, che lo rimuove.