Docker: consigli non dannosi

Nei commenti al mio articolo Docker: consigli dannosi ci sono state molte richieste di spiegare perché il Dockerfile descritto sia così terribile.

Riepilogo della precedente puntata: due sviluppatori stanno creando un Dockerfile con una scadenza serrata. Durante il lavoro, entra Ops Igor Ivanovich. Il Dockerfile finale è così scarso che l'IA è sul punto di avere un infarto.

Docker: consigli non dannosi

Adesso analizziamo cosa non va in questo Dockerfile.

Dunque, è passata una settimana.

Dev Petya incontra Ops Igor Ivanovich in mensa per un caffè.

P: Igor Ivanovich, sei molto occupato? Vorrei capire dove abbiamo sbagliato.

I: È strano, non capita spesso di incontrare sviluppatori interessati all'operatività.
Iniziamo con alcune convenzioni:

  1. Ideologia di Docker: un contenitore — un processo.
  2. Più il contenitore è piccolo, meglio è.
  3. Più viene preso dalla cache, meglio è.

P: Perché in un contenitore deve esserci un solo processo?

L'IA: Docker monitora lo stato del processo con pid 1 al momento dell'avvio del contenitore. Se il processo termina, Docker cerca di riavviare il contenitore. Supponiamo che nel tuo contenitore siano in esecuzione più applicazioni o che l'applicazione principale non sia in esecuzione con pid 1. Se il processo si arresta, Docker non lo saprà.

Se non ci sono altre domande, mostra il tuo Dockerfile.

E Petya ha mostrato:

FROM ubuntu:latest

# Copiamo il codice sorgente
COPY ./ /app
WORKDIR /app

# Aggiorniamo l'elenco dei pacchetti
RUN apt-get update 

# Aggiorniamo i pacchetti
RUN apt-get upgrade

# Installa i pacchetti necessari
RUN apt-get -y install libpq-dev imagemagick gsfonts ruby-full ssh supervisor

# Installa bundler
RUN gem install bundler

# Installa nodejs per la generazione della statica
RUN curl -sL https://deb.nodesource.com/setup_9.x | sudo bash -
RUN apt-get install -y nodejs

# Installa le dipendenze
RUN bundle install --without development test --path vendor/bundle

# Puliamo le cache
RUN rm -rf /usr/local/bundle/cache/*.gem 
RUN apt-get clean 
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 
RUN rake assets:precompile
# Eseguiamo lo script, all'avvio del contenitore, che avvierà tutto il resto.
CMD ["/app/init.sh"]

L'IA: Oh, iniziamo a esaminare i dettagli. Cominciamo dalla prima riga:

FROM ubuntu:latest

Prendi il tag latest. Utilizzo del tag latest porta a conseguenze imprevedibili. Immagina che il maintainer dell'immagine crei una nuova versione con un diverso elenco di software e questa immagine riceva il tag latest. E il tuo container, nella migliore delle ipotesi, smette di costruirsi, nella peggiore inizi a riportare bug che prima non esistevano.

Prendi un'immagine con un sistema operativo completo e un sacco di software non necessario, il che gonfia le dimensioni del container. E più software ci sono, più falle e vulnerabilità ci sono.

Inoltre, più grande è l'immagine, più spazio occupa sull'host e nel registry (dove pensi di memorizzare le immagini)?

D: Sì, certo, abbiamo il registry, lo hai configurato tu stesso.

IA: Così, di cosa stavo parlando?... Ah già, i volumi... Aumenta anche il carico sulla rete. Per un'immagine singola è impercettibile, ma quando si fa un'assemblaggio continuo, test e deploy, diventa evidente. E se non hai il God’s mode su AWS, ti arriverà anche una bolletta stratosferica.

Perciò, è necessario scegliere l'immagine più adatta, con la versione esatta e il minimo di software. Ad esempio, prendi: FROM ruby:2.5.5-stretch

D: Oh, capisco. Come posso vedere le immagini disponibili? Come faccio a capire quale mi serve?

IA: Di solito, le immagini si prendono da Docker Hub, non confonderlo con PornHub :). Di solito esistono diverse build per un'immagine:
Alpine: le immagini sono basate su un'immagine Linux minimalista, solo 5 MB. Il suo svantaggio: è costruita con una propria implementazione di libc, i pacchetti standard non funzionano. Ci vorrà un po' di tempo per cercare e installare il pacchetto necessario.
Scratch: un'immagine di base, non utilizzata per costruire altre immagini. È destinata esclusivamente all'esecuzione di dati binari preparati. È perfetta per l'esecuzione di applicazioni binarie che includono tutto il necessario, come le applicazioni Go.
Basata su un sistema operativo come Ubuntu o Debian. Qui non credo ci sia bisogno di spiegazioni.

AI: Ora dobbiamo installare tutti i pacchetti aggiuntivi e pulire le cache. E possiamo subito eliminare apt-get upgrade. Altrimenti, ad ogni build, nonostante il tag fisso dell'immagine di base, si otterranno immagini diverse. L'aggiornamento dei pacchetti nell'immagine è compito del maintainer e comporta la modifica del tag.

P: Sì, ho provato a farlo, e mi è riuscito così:

WORKDIR /app
COPY ./ /app

RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - 
    && apt-get -y install libpq-dev imagemagick gsfonts ruby-full ssh supervisor nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle

RUN rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

IA: Non male, ma c'è ancora del lavoro da fare. Guarda, questo comando qui:

RUN rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*  

… non elimina i dati dall'immagine finale, ma crea solo un ulteriore strato senza quei dati. È corretto così:

RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - 
    && apt-get -y install libpq-dev imagemagick gsfonts nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle   
    && rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

Ma non è tutto. Che ne dite di Ruby? Allora non è necessario copiare l'intero progetto all'inizio. È sufficiente copiare Gemfile e Gemfile.lock.

Con questo approccio, bundle install non verrà eseguito con ogni modifica del codice sorgente, ma solo se Gemfile o Gemfile.lock sono cambiati.

Gli stessi metodi funzionano anche per altri linguaggi con gestori di dipendenze, come npm, pip, composer e altri basati su un file con l'elenco delle dipendenze.

E infine, ti ricordi che all'inizio ho parlato dell'ideologia di Docker "un contenitore - un processo"? Questo significa che il supervisor non è necessario. Non dovresti nemmeno installare systemd, per le stesse ragioni. In effetti, Docker stesso funge da supervisor. Quando cerchi di avviare più processi al suo interno, è come cercare di eseguire più applicazioni in un singolo processo supervisor.
Durante la creazione genererai un'immagine unica e poi avvierai il numero necessario di contenitori, in modo che ognuno esegua un processo.

Ma ne parleremo più tardi.

D: Capisco, vediamo che ne viene fuori:

FROM ruby:2.5.5-stretch

WORKDIR /app
COPY Gemfile* /app

RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - 
    && apt-get -y install libpq-dev imagemagick gsfonts nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle   
    && rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

COPY . /app
RUN rake assets:precompile

CMD ["bundle", "exec", "passenger", "start"]

E sulla gestione dei demoni, lo faremo all'avvio del contenitore?

AI: Sì, esatto. A proposito, puoi usare sia CMD che ENTRYPOINT. Scoprire la differenza sarà il tuo compito a casa. Su questo argomento ci sono buone informazioni su Habr. articolo.

D'accordo, procediamo. Stai scaricando un file per installare node, ma non c'è alcuna garanzia che contenga ciò di cui hai bisogno. Devi aggiungere una validazione. Ad esempio, così:

RUN curl -sL https://deb.nodesource.com/setup_9.x > setup_9.x 
    && echo "958c9a95c4974c918dca773edf6d18b1d1a41434  setup_9.x" | sha1sum -c - 
    && bash setup_9.x 
    && rm -rf setup_9.x 
    && apt-get -y install libpq-dev imagemagick gsfonts nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle   
    && rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

Con la somma di controllo puoi verificare di aver scaricato il file giusto.

P: Ma se il file si modifica, la build non andrà a buon fine.

AI: Già, e sorprendentemente, questo è un vantaggio. Scoprirai che il file è cambiato e potrai vedere quali modifiche sono state apportate. Può darsi che abbiano aggiunto, diciamo, uno script che elimina tutto ciò che tocca, o che crea un backdoor.

P: Grazie. Quindi, il Dockerfile finale sarà così:

FROM ruby:2.5.5-stretch

WORKDIR /app
COPY Gemfile* /app

RUN curl -sL https://deb.nodesource.com/setup_9.x > setup_9.x 
    && echo "958c9a95c4974c918dca773edf6d18b1d1a41434  setup_9.x" | sha1sum -c - 
    && bash  setup_9.x 
    && rm -rf setup_9.x 
    && apt-get -y install libpq-dev imagemagick gsfonts nodejs 
    && gem install bundler 
    && bundle install --without development test --path vendor/bundle   
    && rm -rf /usr/local/bundle/cache/*.gem 
    && apt-get clean  
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 

COPY . /app
RUN rake assets:precompile

CMD ["bundle", "exec", "passenger", "start"]

P: Igor Ivanovich, grazie per l'aiuto. Devo andare, devo fare altri 10 commit oggi.

Igor Ivanovich, fermando con lo sguardo un collega di fretta, sorseggia un caffè forte. Dopo aver riflettuto per qualche secondo su SLA 99,9% e codice senza bug, pone una domanda.

AI: E dove conservate i log?

P: Certo, in production.log. A proposito, come possiamo accedervi senza ssh?

AI: Se li lasciate in file, una soluzione è già stata pensata per voi. Il comando docker exec permette di eseguire qualsiasi comando all'interno del contenitore. Ad esempio, potete usare cat per i log. E usando la chiave -it e lanciando bash (se è installato nel contenitore), otterrete accesso interattivo al contenitore.

Ma non è consigliabile conservare i log nei file. Questo porta almeno a una crescita incontrollata del contenitore, e i log non vengono ruotati. Tutti i log devono essere inviati a stdout. Lì possono essere visualizzati tramite il comando docker logs.

P: Igor Ivanovich, e se portassimo i log in una directory montata, su un nodo fisico, come i dati degli utenti?

AI: È positivo che tu non abbia dimenticato di portare i dati memorizzati su disco nel nodo. Anche con i log si può fare, solo non dimenticare di configurare la rotazione.
Tutto, puoi andare.

P: Igor Ivanovich, cosa consiglieresti di leggere?

AI: In primo luogo, leggi le raccomandazioni degli sviluppatori di Docker, difficilmente qualcuno conosce Docker meglio di loro.

E se vuoi fare pratica, partecipa a un intensivo. La teoria senza pratica è morta.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster