GitLab CD beállítása avagy autodeploy ingyér' előnézeti képe

GitLab CD beállítása avagy autodeploy ingyér'

| Olvasási idő: 7 perc

Az előző két részben beállítottunk egy VPS staging szerver-t, valamint kialakítottunk egy aldomaineket automatikusan konfiguráló, jelszóval ellátó reverse proxy-t a Traefik személyében. Most pedig ássuk bele magunkat kicsit a GitLab CD-be. Finom lesz, jujuj! :)

De miééért?

Ábel kérdésére válaszolva: mert lusták vagyunk (igen, a jó fejlesztő lusta, de attól tartok, ez fordítva nem igaz). Ha valami frissült, és szeretnénk, ha látná az ügyfél, akkor több lehetőségünk is van: felmásolhatjuk FTP-re (ne), Github/GitLab helyett pusholhatjuk a szerveren egy bare repóba alternatív remote-ként (ez nem rossz, egy ideig használtam is), de:

  • FTP: még mindig ne,
  • Bare repo: ha már egyébként is commitolunk és pusholunk szépen, miért pusholjunk duplán?

GitLab CI/CD

Azon kívül, hogy a GitLab ingyenes csomagjában korlátlan mennyiségű privát repositorynk lehet, elérhetőek a GitLab CI/CD eszközei is, ezek közül pedig most a GitLab Runnereket - és ehhez kapcsolódóan a Variables menüpontot - fogjuk jobban szemügyre venni.

Az alábbiakban egy Craft CMS-sel készülő projekt példáján keresztül fogjuk átnézni a folyamatot. Kezdjünk is neki!

.gitlab-ci.yml

Amennyiben a projekt gyökerében létezik .gitlab-ci.yml, úgy a GitLab a push után rögtön elindítja egy osztott Runneren az abban foglaltak végrehajtását, aminek a folyamatát mi a CI/CD > Jobs menüpont alatt tudunk nyomonkövetni.

yaml
## Used variables in this file
# Please, set these variables in the GitLab repository of the project
#
# $BUILD_PATH = the path generated by GitLab:
# gitlab.com/organization/repo --> /builds/organization/repo
#
# $REPOSITORY_NAME
# name of the repository
#
# $SERVER_SSH_KEY
# SSH key for the staging server. GitLab Runner will log in using this key
#
# $SERVER_PORT
# SSH port number on server
#
# $SERVER_USERNAME
# Username used to login to the server
#
# $SERVER_IP
# IP address of the server
#
# $SERVER_PROJECTS_ROOT
# Absolute path on your server where you store your projects (think about it like webroot)

image: composer

before_script:
  # This part will prepare the GitLab Runner to be able to assemble our payload
  - apk update
  - apk add zip

  # Install project dependencies
  - composer install --no-interaction --prefer-dist --optimize-autoloader

  # Create .env file
  - sh .docker/generate_env.sh $REPOSITORY_NAME $ENVIRONMENT

  # This part makes SSH connection possible from GitLab to our server
  - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
  - mkdir -p ~/.ssh
  - eval $(ssh-agent -s)
  - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'

deployment:
  script:
    # Add SSH key to GitLab Runner from Variables
    - ssh-add <(echo "$SERVER_SSH_KEY")

    - cd $BUILD_PATH/$REPOSITORY_NAME && rm -Rf .git && zip -r0 -q ../$REPOSITORY_NAME-bundle.zip ./

    # Copy the build files to the server
    - scp -P$SERVER_PORT $BUILD_PATH/$REPOSITORY_NAME-bundle.zip [email protected]$SERVER_IP:$SERVER_PROJECTS_ROOT
    - ssh -p$SERVER_PORT [email protected]$SERVER_IP "cd $SERVER_PROJECTS_ROOT && rm -Rf $REPOSITORY_NAME && unzip -q -o $REPOSITORY_NAME-bundle.zip -d $REPOSITORY_NAME && rm $REPOSITORY_NAME-bundle.zip"

    # Stop the Docker containers for the project
    - ssh -p$SERVER_PORT [email protected]$SERVER_IP "cd $SERVER_PROJECTS_ROOT/$REPOSITORY_NAME && docker-compose down --volumes"

    # Build the Docker containers for the project
    - ssh -p$SERVER_PORT [email protected]$SERVER_IP "cd $SERVER_PROJECTS_ROOT/$REPOSITORY_NAME && docker-compose -f docker-compose-ci.yml up -d --build --remove-orphans"

    # Restart Traefik
    - ssh -p$SERVER_PORT [email protected]$SERVER_IP "docker restart traefik"

A kódot igyekeztem agyonkommentelni, a változók mennyisége már kicsit az olvashatóság rovására ment, de cserébe könnyen hordozható lett. Mentsük le a projekt gyökerébe, .gitlab-ci.yml néven.

Látszik, hogy lefuttat bizonyos fájlokat, ezért azokat létre kell hoznunk.

.docker/generate_env.sh

bash
#!/bin/sh
  cmdPath=$(pwd)

  COMPOSE_PROJECT_NAME=$1
    COMPOSE_PROJECT_NAME=$(echo "$COMPOSE_PROJECT_NAME" | tr '[:upper:]' '[:lower:]')
    DB_DATABASE=$COMPOSE_PROJECT_NAME"_database"
    DB_USER=$COMPOSE_PROJECT_NAME"_user"
    ENVIRONMENT=$2
    SECURITY_KEY=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 30)
    DB_DRIVER="mysql"
    DB_SERVER="database"
    DB_PASSWORD=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 12)
    DB_ROOT_PASSWORD=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 20)
    DB_PORT="3306"

    ####
    #
    # Write it to .env for Craft
    #
    ####

    rm "$cmdPath/.env"
    touch "$cmdPath/.env"

    #
    # Set project name. This defines what prefix should
    # Compose use.
    #
    echo '# Project name to use as prefix' >> "$cmdPath/.env"
    echo 'COMPOSE_PROJECT_NAME='$COMPOSE_PROJECT_NAME >> "$cmdPath/.env"
    echo '' >> "$cmdPath/.env"

    echo '# Root password for MySQL container' >> "$cmdPath/.env"
    echo 'DB_ROOT_PASSWORD='$DB_ROOT_PASSWORD >> "$cmdPath/.env"
    echo '' >> "$cmdPath/.env"

    #
    # Common variables used by both Docker and Craft CMS
    #
    echo '# The environment Craft is currently running in ('dev', 'staging', 'production', etc.)' >> "$cmdPath/.env"
    echo 'ENVIRONMENT='$ENVIRONMENT >> "$cmdPath/.env"
    echo '' >> "$cmdPath/.env"

    echo "# The secure key Craft will use for hashing and encrypting data" >> "$cmdPath/.env"
    echo 'SECURITY_KEY='$SECURITY_KEY >> "$cmdPath/.env"
    echo '' >> "$cmdPath/.env"

    echo "# The database driver that will used ('mysql' or 'pgsql')" >> "$cmdPath/.env"
    echo 'DB_DRIVER='$DB_DRIVER >> "$cmdPath/.env"
    echo '' >> "$cmdPath/.env"

    echo "# The database server name or IP address (usually this is 'localhost' or '127.0.0.1')" >> "$cmdPath/.env"
    echo 'DB_SERVER='$DB_SERVER >> "$cmdPath/.env"
    echo '' >> "$cmdPath/.env"

    echo "# The database username to connect with" >> "$cmdPath/.env"
    echo 'DB_USER='$DB_USER >> "$cmdPath/.env"
    echo '' >> "$cmdPath/.env"

    echo "# The database password to connect with" >> "$cmdPath/.env"
    echo 'DB_PASSWORD='$DB_PASSWORD >> "$cmdPath/.env"
    echo '' >> "$cmdPath/.env"

    echo "# The database password to connect with" >> "$cmdPath/.env"
    echo 'DB_DATABASE='$DB_DATABASE >> "$cmdPath/.env"
    echo '' >> "$cmdPath/.env"

    echo "# The prefix that should be added to generated table names (only necessary if multiple things are sharing the same database)" >> "$cmdPath/.env"
    echo 'DB_TABLE_PREFIX=' >> "$cmdPath/.env"
    echo '' >> "$cmdPath/.env"

    echo "# The port to connect to the database with. Will default to 5432 for PostgreSQL and 3306 for MySQL." >> "$cmdPath/.env"
    echo 'DB_PORT='$DB_PORT >> "$cmdPath/.env"
    echo '' >> "$cmdPath/.env"

Ennek a fájlnak a feladata a .env fájl generálása. Mivel a Docker és a Craft CMS egyaránt támogatja a .env fájl használatát, ezért adja magát, hogy ezt kihasználjuk: a docker-compose.yml fájlba nem drótozzuk be a projektspecifikus értékeket, helyette olyanra alakítottam át, ami minden projektben felhasználható.

docker-compose.yml

yaml
# Docker-based Craft CMS 3 setup
# Author: Otto Radics
#
# Below you can find the definitions of two containers:
# the first is based on php:7.2-apache image, the other one is
# based on mariadb:latest.
#
# You can find more details about the build process in the  Dockerfile_webserver.
#
# Do you have any questions?
# Feel free to contact me: [email protected]

volumes:
  database_volume: {}

version: '3.6'
services:

  web:
    build:
      context: ./.docker
      dockerfile: Dockerfile_webserver
    working_dir: /var/www/
    volumes:
      - ./:/var/www/
      - ./.docker/logs/apache2/:/var/log/apache2/
    depends_on:
      - database
    ports:
      - 3000:80

  database:
    image: mariadb:latest
    environment:
     - MYSQL_USER=${DB_USER}
     - MYSQL_PASSWORD=${DB_PASSWORD}
     - MYSQL_DATABASE=${DB_DATABASE}
     - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
    volumes:
     - ./.docker/dump:/docker-entrypoint-initdb.d
     - database_volume:/var/lib/mysql
    ports:
     - 3306:3306

Ez az alapértelmezett docker-compose.yml fájlunk helyi fejlesztéshez - ezt használja a docker-compose up, viszont ugyanazon a hoston nem lehet ugyanarra a portra kötni két külön container-t, amivel viszont ütközne az eredeti célunk. Ezért van egy külön docker-compose-ci.yml fájlunk is, amit csak a GitLab Runner használ a staging szerverünkre deployolásnál.

docker-compose-ci.yml

yaml
# Docker-based Craft CMS 3 setup
# Author: Otto Radics
#
# Below you can find the definitions of two containers:
# the first is based on php:7.2-apache image, the other one is
# based on mariadb:latest.
#
# You can find more details about the build process in the  Dockerfile_webserver.
#
# Please run ./setup.sh before `docker-compose up`: the .env file is needed for
# Docker Compose to start this project.
#
# Do you have any questions?
# Feel free to contact me: [email protected]

volumes:
  database_volume: {}

networks:
  infrastructure_traefik_web:
    external: true
  default:
    external: false

version: '3.6'
services:

  web:
    build:
      context: ./.docker
      dockerfile: Dockerfile_webserver
    working_dir: /var/www/
    volumes:
      - ./:/var/www/
    depends_on:
      - database
    expose:
      - 80
    labels:
      - traefik.backend=${COMPOSE_PROJECT_NAME}.yoda.webmenedzser.hu
      - traefik.port=80
      - traefik.enable=true
    networks:
      - default
      - infrastructure_traefik_web
    environment:
      - APACHE_RUN_USER=#1000
    logging:
      driver: json-file
      options:
        max-size: '1m'
        max-file: '3'

  database:
    image: mariadb:latest
    environment:
     - MYSQL_USER=${DB_USER}
     - MYSQL_PASSWORD=${DB_PASSWORD}
     - MYSQL_DATABASE=${DB_DATABASE}
     - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
    volumes:
     - ./.docker/dump:/docker-entrypoint-initdb.d
     - database_volume:/var/lib/mysql
    expose:
     - 3306
    networks:
     - default
    logging:
      driver: json-file
      options:
        max-size: '1m'
        max-file: '3'

Nem különbözik sokban az alap fájltól: ports helyett expose van a 80-as portra, ami már nem ütközik, viszont arra pont elég, hogy a Traefik ráirányítsa a forgalmat. Megemlítendő érdekesség a volumes-on belüli ./.docker/dump:/docker-entrypoint-initdb.d: ezzel a docker-compose.yml-lal azonos mappából kiindulva a .docker/dump/ mappában lévő összes .sql fájlt átadjuk a docker-entrypoint-initdb.d scriptnek, ami azokat a container database_volume-jának létrehozásakor automatikusan importálja. Vagyis ha ide bedobunk mondjuk egy db.sql fájlt, és azt mindig, amikor DB-t szeretnénk frissíteni, egy újabb dump-pal felülírjuk, akkor a GitLab Runner-en keresztüli CD-vel egyben DB-t is tudunk deployolni.

Mindkettő .yml fájlhoz szükségünk van a .docker/Dockerfile_webserver fájlra, ennek a tartalma:

dockerfile
# Use this image as base
FROM php:7.2-apache

# Install Craft CMS 3 requirements' requirements ¯\_(ツ)_/¯
RUN apt-get update && apt-get install -y \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libjpeg-dev \
        libmcrypt-dev \
        libcurl4-openssl-dev \
        libxml2-dev \
        libmagickwand-dev imagemagick \
        mariadb-client \
        libicu-dev \
        jpegoptim optipng gifsicle webp \
    && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ --with-png-dir=/usr/include --with-jpeg-dir=/usr/include

# Install Craft CMS 3 requirements
RUN docker-php-ext-install -j$(nproc) curl
RUN docker-php-ext-install -j$(nproc) dom
RUN docker-php-ext-install -j$(nproc) gd
RUN docker-php-ext-install -j$(nproc) intl
RUN docker-php-ext-install -j$(nproc) iconv
RUN docker-php-ext-install -j$(nproc) json
RUN docker-php-ext-install -j$(nproc) mbstring
RUN docker-php-ext-install -j$(nproc) mysqli
RUN docker-php-ext-install -j$(nproc) opcache
RUN docker-php-ext-install -j$(nproc) pdo
RUN docker-php-ext-install -j$(nproc) pdo_mysql
RUN docker-php-ext-install -j$(nproc) simplexml
RUN docker-php-ext-install -j$(nproc) zip
RUN printf "\n" | pecl install imagick
RUN docker-php-ext-enable imagick.so

## Configure Apache server
RUN a2enmod rewrite expires headers ssl
ADD sites.conf /etc/apache2/sites-enabled/000-default.conf

RUN rm -Rf /var/www/html
RUN useradd -u 1000 -G www-data -M wbmngr

CMD ["apache2-foreground"]

Semmi extra: 7.2-apache image-t vesszük alapul, telepítjük a Craft CMS működéséhez szükséges cuccokat, illetve létrehozunk egy user-t 1000-es ID-val a containeren belül. Ennek köszönhetően nem lesznek jogosultsági problémáink a containeren kívül akkor, ha törölni szeretnénk a létrehozott fájlokat (a GitLab Runner nem interaktív shell-t futtat, ezért sudo-zni nem tudunk, a root login meg ugye le van tiltva). Az ADD-hoz kapcsolódó fájl tartalma:

apache
ServerName dev.example
ServerAdmin [email protected]

ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined

<VirtualHost *:80>
	DocumentRoot /var/www/web

	ServerAdmin [email protected]
</VirtualHost>

Kódok szintjén ezekre van szükségünk, innentől már csak konfigurálunk. :)

Változók beállítása

Ahhoz, hogy a Runner rendben tudjon deployolni, a változóknak értéket kell adnunk. Navigáljunk el a projekt Settings > CI / CD menüpontjához, és nyissuk ki a Variables szekciót:

Gitlab Cd Autodeploy 2

Folytatva a korábban megkezdett gondolatsort a változók az alábbiak szerint alakulnak:

  • BUILD_PATH: /builds/{{ organization }}, https://gitlab.com/gitlab-org/... esetén gitlab-org lenne
  • ENVIRONMENT: staging
  • REPOSITORY_NAME: a repository neve, https://gitlab.com/gitlab-org/... esetén gitlab-ce lenne
  • SERVER_IP: 188.166.22.129
  • SERVER_PORT: 62222
  • SERVER_PROJECTS_ROOT: /home/wbmngr/sites/
  • SERVER_SSH_KEY: ide egy SSH kulcs private része kell. Érdemes lehet létrehozni egyet külön a GitLab-nak, és felpusholni a staging szerverre a wbmngr user nevében, ahogyan itt írtam, és a Variables részre ehhez a változóhoz a private részt copypasta megoldással feldobni (cat ~/.ssh/gitlab_ssh_key kiírja a kulcs privát felének tartalmát)
  • SERVER_USERNAME: wbmngr (létrehozhatnánk külön user-t a GitLab-nak, de az megbonyolítaná a jogosultságok kérdését)

Ezzel a résszel végeztünk is.

Szerver felkészítése

Lépj be a staging szerverre SSH-n keresztül, hogy elő tudjuk készíteni a terepet az érkező csomagoknak.

Először is hozd létre azt az útvonalat, amit megadtál a SERVER_PROJECTS_ROOT-nál, illetve állíts be rá megfelelő jogosultságokat.

bash
mkdir -p /home/wbmngr/sites
chown wbmngr:www-data /home/wbmngr/sites
chmod 2770 /home/wbmngr/sites

Mivel minden deploynál törölni szeretnénk a korábbi fájlokat, ezért fontos, hogy a tulajdonosok mi legyünk, és csak a fájlok/mappák csoportja egyezzen meg a webszerver csoportjával.

A .gitlab-ci.yml-ben van egy unzip lépés a szerveren - viszont az unzip program alapból nincs telepítve a VPS-en, így arra még szükségünk lesz:

bash
sudo apt install unzip

Elméletileg el is készültünk - nincs más dolgod, mint commitolni egyet, majd pusholni a GitLab-ra. 

Összefoglalás

Most, hogy megy a build a GitLabon, miközben izgulunk, hogy sikeres lesz-e, nézzük át nagy vonalakban a folyamatot: offline fejlesztesz, elkészülsz valamilyen funkcióval, ezután commitolsz és pusholsz. A push végeztével a GitLab elindítja a Runner-t, az összecsomagolja a kész csomagot egy .zip-be, majd felmásolja a szerverre a $SERVER_PROJECTS_ROOT-ba. Ezután újra belép a szerveren a $SERVER_PROJECTS_ROOT mappába, törli a $REPOSITORY_NAME mappát, kicsomagolja a bundle-t, majd törli, és lelövi az esetleg még futó Docker containereket, törölve a hozzájuk tartozó volume-okat is. Ezután elindítja a docker-compose-ci.yml fájl alapján a containereket, majd újraindítja a Traefik-et - semmi extra. :)

Köszi a figyelmet, készen is vagyunk - remélem, hogy Neked simábban ment a folyamat, mint nekem! :)

Ha valami nem egyértelmű, kérdésed van, vagy hibát találtál, dobj egy üzenetet!