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ì orribile.

Riepilogo della puntata precedente: due sviluppatori alle prese con una scadenza stretta compongono un Dockerfile. Durante il processo, entra Ops Igor Ivanovich. Il Dockerfile finale è così mal fatto da mettere l'IA in una situazione critica.

Docker: consigli non dannosi

Adesso vediamo cosa non va in questo Dockerfile.

Quindi, è passata una settimana.

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

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

IA: È bello, non capita spesso di incontrare sviluppatori interessati all'operatività.
Per cominciare, mettiamo d'accordo alcune cose:

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

P: E perché in un contenitore deve esserci un solo processo?

IA: Docker, all'avvio di un contenitore, monitora lo stato del processo con pid 1. Se il processo muore, Docker cerca di riavviare il contenitore. Supponiamo che tu abbia più applicazioni in un contenitore o che l'applicazione principale non sia avviata con pid 1. Se il processo muore, Docker non lo saprà.

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

E Petya mostrò:

FROM ubuntu:latest

# Copia il codice sorgente
COPY .\/ \/app
WORKDIR \/app

# Aggiorna la lista dei pacchetti
RUN apt-get update 

# Aggiorna 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 utilizzato per la costruzione 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

# Pulisci 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
# Esegui lo script all'avvio del contenitore, che avvierà tutto il resto.
CMD ["\/app\/init.sh"]

IA: Oh, cominciamo con ordine. Iniziamo dalla prima riga:

FROM ubuntu:latest

Stai usando il tag latest. Utilizzare un tag latest può portare a conseguenze imprevedibili. Immagina che il maintainer dell'immagine compili una nuova versione dell'immagine con un elenco di software diverso, quest'immagine riceve il tag latest. E il tuo contenitore, nel migliore dei casi, non si costruisce più, nel peggiore dei casi, ottieni bug che prima non c'erano.

Stai prendendo un'immagine con un sistema operativo completo con un sacco di software non necessario, che gonfia la dimensione del container. E più software ci sono, più porte e vulnerabilità si creano.

Inoltre, più è grande l'immagine, più spazio occupa sull'host e nel registry (da qualche parte dovrai pur conservare le immagini)?

P: Sì, certo, abbiamo un registry, lo hai configurato tu stesso.

I: Allora, di cosa stavo parlando?.. Ah già, le dimensioni… Cresce anche il carico sulla rete. Per un'immagine singola è impercettibile, ma quando ci sono build continue, test e deployment, diventa evidente. E se non hai il God’s mode su AWS, riceverai anche una bolletta astronomica.

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

P: Oh, capito. E come e dove posso vedere le immagini disponibili? Come capire quale mi serve?

I: Di solito le immagini vengono prelevate da dockerhub, non confonderlo con pornhub :). Per ogni immagine ci sono di solito diverse build:
Alpine: immagini costruite su una base Linux minimale, solo 5 MB. Il suo svantaggio: è costruito 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 già preparati. È perfetta per eseguire applicazioni binarie che contengono tutto il necessario, ad esempio le applicazioni go.
Basato su un qualche sistema operativo, ad esempio Ubuntu o Debian. Qui non credo ci sia bisogno di spiegazioni.

I: Adesso dobbiamo installare tutti i pacchetti aggiuntivi e pulire le cache. E si può subito eliminare apt-get upgrade. Altrimenti, ad ogni build, nonostante il tag fisso dell'immagine di base, si otterranno immagini differenti. L'aggiornamento dei pacchetti nell'immagine è responsabilità del maintainer, e comporta modifiche al tag.

P: Sì, ho provato a farlo, ed è venuto fuori 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/*

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

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 uno strato aggiuntivo senza questi 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. Cosa hai lì, Ruby? Allora non è necessario copiare l'intero progetto all'inizio. È sufficiente copiare Gemfile e Gemfile.lock.

Con questo approccio bundle install non verrà eseguito ad ogni modifica dei sorgenti, ma solo se Gemfile o Gemfile.lock sono stati modificati.

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

E infine, ricordi, all'inizio ti ho parlato dell'ideologia di Docker «un contenitore - un processo»? Questo significa che non è necessario un supervisore. Inoltre, non è consigliabile installare systemd, per gli stessi motivi. In sostanza, Docker è già un supervisore. E quando cerchi di eseguire più processi all'interno, è come eseguire più applicazioni in un singolo processo di supervisore.
Durante la creazione, creerai un'unica immagine e poi avvierai il numero necessario di contenitori, affinché ognuno esegua un singolo processo.

Ma di questo parleremo più avanti.

P: Sembra che abbia capito. Guarda cosa 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 riduciamo l'avvio dei demoni durante l'avvio del contenitore?

AI: Sì, esatto. Tra l'altro, puoi usare sia CMD che ENTRYPOINT. Scoprire la differenza è il tuo compito a casa. A riguardo, su Habr c'è un buon articolo su Habr..

Allora, continuiamo. Stai scaricando il file per installare node, ma non c'è alcuna garanzia che contenga ciò di cui hai bisogno. Dobbiamo aggiungere una validazione. Ad esempio, così:

Esegui 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 l'hash puoi verificare che il file scaricato sia corretto.

P: Ma se il file cambia, la compilazione non andrà a buon fine.

AI: Sì, e strano a dirsi, è anche un vantaggio. Saprai che il file è cambiato e potrai vedere cosa è stato modificato. Potrebbe essere stato aggiunto uno script che elimina tutto a cui può accedere, o crea un backdoor.

P: Grazie. Quindi, il Dockerfile finale apparirà 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. Adesso devo correre, devo fare altri 10 commit oggi.

Igor Ivanovich, fermando il collega di fretta con lo sguardo, sorseggia un caffè forte. Dopo aver riflettuto per qualche secondo sul SLA 99.9% e sul 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, per voi è già stata trovata una soluzione. Il comando docker exec consente di eseguire qualsiasi comando all'interno del container. Ad esempio, puoi usare cat per i log. E usando la chiave -it e avviando bash (se è installato nel container), otterrai accesso interattivo al container.

Ma non è consigliabile mantenere i log in file. Al minimo, questo porta a una crescita incontrollata del container, dato che nessuno effettua la rotazione dei log. Tutti i log devono essere inviati a stdout. Lì puoi già visualizzarli usando il comando docker logs.

P: Igor Ivanovich, potrebbe essere utile portare i log in una directory montata, su un nodo fisico, come i dati degli utenti?

AI: È un bene che non ti sia dimenticato di esternalizzare i dati caricati sul disco del nodo. Anche i log si possono gestire così, solo non dimenticare di configurare la rotazione.
Bene, puoi correre.

P: Igor Ivanovič, cosa mi consiglia di leggere?

II: Per cominciare, leggi le raccomandazioni degli sviluppatori di Docker, difficilmente qualcuno conosce Docker meglio di loro.

E se vuoi fare pratica, vai a un intensivo. Infatti, 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