GitLab Shell Runner. Esecuzione competitiva dei servizi testati tramite Docker Compose

GitLab Shell Runner. Esecuzione competitiva dei servizi testati tramite Docker Compose

Questo articolo sarà di interesse sia per i tester che per gli sviluppatori, ma è maggiormente rivolto agli automatizzatori che si sono trovati ad affrontare il problema della configurazione di GitLab CI/CD per eseguire test di integrazione in condizioni di insufficienti risorse infrastrutturali e/o assenza di una piattaforma di orchestrazione dei container. Spiegherò come configurare il deployment degli ambienti di test utilizzando docker compose su un unico runner GitLab shell, in modo tale che durante il deployment di più ambienti i servizi avviati non si disturbino a vicenda.


Contenuto

Premesse

  1. Nella mia esperienza, mi è capitato spesso di "curare" il testing di integrazione nei progetti. E spesso il primo e più significativo problema è il CI pipeline, in cui il testing di integrazione del servizio (o servizi) sviluppati viene eseguito in un ambiente dev/stage. Questo ha causato non pochi problemi: A causa di difetti in un certo servizio, durante il testing di integrazione il circuito di test può essere compromesso da dati danneggiati. Ci sono stati casi in cui l'invio di una richiesta con un formato JSON danneggiato ha bloccato il servizio, portando l'ambiente in uno stato completamente non funzionante.

    • Rallentamenti nel funzionamento del circuito di test con l'aumentare dei dati di test. Penso che sia inutile descrivere un esempio di pulizia/rollback del DB. Nella mia esperienza, non ho mai incontrato un progetto in cui questa procedura sia andata liscia.
    • Rischio di compromettere il funzionamento del circuito di test durante la verifica delle impostazioni comuni del sistema. Ad esempio, user/group/password/application policy.
    • I dati di test degli autotest ostacolano il lavoro dei tester manuali.
    • Qualcuno dirà che buoni autotest dovrebbero pulire i dati dopo di loro. Ho delle argomentazioni contro:

    Gli ambienti dinamici sono molto comodi da usare.

    • Non ogni oggetto può essere eliminato dal sistema tramite API. Ad esempio, la chiamata all'eliminazione di un oggetto non è implementata, in quanto contraddice la logica aziendale.
    • La creazione di un oggetto tramite API può generare una quantità enorme di metadati, che sono problematici da eliminare.
    • Se i test dipendono l'uno dall'altro, il processo di pulizia dei dati dopo l'esecuzione dei test diventa un vero e proprio grattacapo.
    • Chiamate aggiuntive (e, a mio avviso, ingiustificate) all'API.
    • E il principale argomento: quando i dati di test iniziano a essere puliti direttamente dal DB. Questo si trasforma in un vero circo PK/FK! Si sente dire dagli sviluppatori: "Ho solo aggiunto/rimosso/rinominato una tabella, perché 100500 test di integrazione sono falliti?"
    • A mio parere, la soluzione più ottimale è un ambiente dinamico.

    Molti usano docker-compose per avviare l'ambiente di test, ma pochi utilizzano docker-compose durante i test di integrazione in CI/CD. E qui non considero kubernetes, swarm e altre piattaforme di orchestrazione dei container. Non tutte le aziende le hanno. Sarebbe interessante se docker-compose.yml fosse universale.

  2. Se anche abbiamo il nostro runner QA, come possiamo fare in modo che i servizi avviati tramite docker-compose non si disturbino a vicenda?
  3. Come raccogliere i log dei servizi testati?
  4. Come pulire il runner?
  5. Ho il mio runner GitLab per i miei progetti e mi sono trovato di fronte a queste domande durante lo sviluppo

del client Java TestRail per . Più precisamente, durante l'esecuzione dei test di integrazione. Ora andremo a risolvere queste questioni con esempi da questo progetto.Per il runner, consiglio una VM Linux con 4 vCPU, 4 GB di RAM, 50 GB di HDD.

Al contenuto

GitLab Shell Runner

Su Internet ci sono molte informazioni sulla configurazione di gitlab-runner, quindi in breve:
Accediamo alla macchina via SSH

  • Installiamo gitlab-runner, docker, docker-composeAggiungiamo un utente
  • al gruppo gitlab-runner sudo groupadd docker sudo usermod -aG docker gitlab-runner docker
    gitlab-runner.
  • Registriamo Apriamo per modifica
  • e aggiungiamo /etc/gitlab-runner/config.toml concurrent=20 [[runners]] request_concurrency = 10

    Questo permetterà di eseguire task paralleli su un unico runner. Leggere più dettagliatamente

    Se la vostra macchina è più potente, per esempio 8 vCPU, 16 GB di RAM, questi numeri possono essere aumentati di almeno il doppio. Ma tutto dipende da cosa esattamente verrà eseguito su questo runner e in quale quantità. qui.
    Se disponete di una macchina più potente, ad esempio 8 vCPU e 16 GB di RAM, è possibile raddoppiare almeno questi valori. Tuttavia, tutto dipende da cosa verrà eseguito su questo runner e in quale quantità.

È sufficiente.

Al contenuto

Preparazione docker-compose.yml

L'obiettivo principale è un docker-compose.yml versatile che possa essere utilizzato dai programmatori/tester sia localmente che nel pipeline CI.

In primo luogo, creiamo nomi di servizi unici per CI. Una delle variabili uniche in GitLab CI è la variabile CI_JOB_ID. Se specifichiamo container_name con il valore "service-${CI_JOB_ID:-local}", in caso di:

  • se CI_JOB_ID non definita nelle variabili di ambiente,
    il nome del servizio sarà service-local
  • se CI_JOB_ID definita nelle variabili di ambiente (ad esempio 123),
    il nome del servizio sarà service-123

In secondo luogo, creiamo una rete comune per i servizi in esecuzione. Questo ci fornisce isolamento a livello di rete quando avviamo più ambienti di test.

networks:
  default:
    external:
      name: service-network-${CI_JOB_ID:-local}

Questo è, in effetti, il primo passo verso il successo =)

Esempio del mio docker-compose.yml con commenti

version: "3"

# Per il corretto funzionamento di web (php) e fmt, è necessario che i container condividano il contenuto eseguibile.
# Nel nostro caso, si tratta della directory /var/www/testrail
volumes:
  static-content:

# Isoliamo l'ambiente a livello di rete
networks:
  default:
    external:
      name: testrail-network-${CI_JOB_ID:-local}

services:
  db:
    image: mysql:5.7.22
    # Ogni container_name contiene ${CI_JOB_ID:-local}
    container_name: "testrail-mysql-${CI_JOB_ID:-local}"
    environment:
      MYSQL_HOST: db
      MYSQL_DATABASE: mydb
      MYSQL_ROOT_PASSWORD: 1234
      SKIP_GRANT_TABLES: 1
      SKIP_NETWORKING: 1
      SERVICE_TAGS: dev
      SERVICE_NAME: mysql
    networks:
    - default

  migration:
    image: registry.gitlab.com/touchbit/image/testrail/migration:latest
    container_name: "testrail-migration-${CI_JOB_ID:-local}"
    links:
    - db
    depends_on:
    - db
    networks:
    - default

  fpm:
    image: registry.gitlab.com/touchbit/image/testrail/fpm:latest
    container_name: "testrail-fpm-${CI_JOB_ID:-local}"
    volumes:
    - static-content:/var/www/testrail
    links:
    - db
    networks:
    - default

  web:
    image: registry.gitlab.com/touchbit/image/testrail/web:latest
    container_name: "testrail-web-${CI_JOB_ID:-local}"
    # Se le variabili TR_HTTP_PORT o TR_HTTPS_PORT non sono definite,
    # il servizio partirà sulle porte 80 e 443 rispettivamente.
    ports:
      - ${TR_HTTP_PORT:-80}:80
      - ${TR_HTTPS_PORT:-443}:443
    volumes:
      - static-content:/var/www/testrail
    links:
      - db
      - fpm
    networks:
      - default

Esempio di avvio locale

docker-compose -f docker-compose.yml up -d
Starting   testrail-mysql-local     ... done
Starting   testrail-migration-local ... done
Starting   testrail-fpm-local       ... done
Recreating testrail-web-local       ... done

Ma non è così semplice avviare in CI.

Al contenuto

Preparazione Makefile

Utilizzo un Makefile, poiché è molto comodo sia per la gestione locale dell'ambiente che in CI. Di seguito commenti inline

# У меня в проектах все вспомогательные вещи лежат в директории `.indirect`,
# в том числе и `docker-compose.yml`

# Использовать bash с опцией pipefail 
# pipefail - фейлит выполнение пайпа, если команда выполнилась с ошибкой
SHELL=/bin/bash -o pipefail

# Останавливаем контейнеры и удаляем сеть
docker-kill:
    docker-compose -f $${CI_JOB_ID:-.indirect}/docker-compose.yml kill
    docker network rm network-$${CI_JOB_ID:-testrail} || true

# Предварительно выполняем docker-kill 
docker-up: docker-kill
    # Создаем сеть для окружения 
    docker network create network-$${CI_JOB_ID:-testrail}
    # Забираем последние образы из docker-registry
    docker-compose -f $${CI_JOB_ID:-.indirect}/docker-compose.yml pull
    # Запускаем окружение
    # force-recreate - принудительное пересоздание контейнеров
    # renew-anon-volumes - не использовать volumes предыдущих контейнеров
    docker-compose -f $${CI_JOB_ID:-.indirect}/docker-compose.yml up --force-recreate --renew-anon-volumes -d
    # Ну и, на всякий случай, вывести что там у нас в принципе запущено на машинке
    docker ps

# Коллектим логи сервисов
docker-logs:
    mkdir ./logs || true
    docker logs testrail-web-$${CI_JOB_ID:-local}       >& logs/testrail-web.log
    docker logs testrail-fpm-$${CI_JOB_ID:-local}       >& logs/testrail-fpm.log
    docker logs testrail-migration-$${CI_JOB_ID:-local} >& logs/testrail-migration.log
    docker logs testrail-mysql-$${CI_JOB_ID:-local}     >& logs/testrail-mysql.log

# Очистка раннера
docker-clean:
    @echo Останавливаем все testrail-контейнеры
    docker kill $$(docker ps --filter=name=testrail -q) || true
    @echo Очистка докер контейнеров
    docker rm -f $$(docker ps -a -f --filter=name=testrail status=exited -q) || true
    @echo Очистка dangling образов
    docker rmi -f $$(docker images -f "dangling=true" -q) || true
    @echo Очистка testrail образов
    docker rmi -f $$(docker images --filter=reference='registry.gitlab.com/touchbit/image/testrail/*' -q) || true
    @echo Очистка всех неиспользуемых volume
    docker volume rm -f $$(docker volume ls -q) || true
    @echo Очистка всех testrail сетей
    docker network rm $(docker network ls --filter=name=testrail -q) || true
    docker ps

Controlliamo

make docker-up

$ make docker-up 
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml kill
Killing testrail-web-local   ... done
Killing testrail-fpm-local   ... done
Killing testrail-mysql-local ... done
docker network rm network-${CI_JOB_ID:-testrail} || true
network-testrail
docker network create network-${CI_JOB_ID:-testrail}
d2ec063324081c8bbc1b08fd92242c2ea59d70cf4025fab8efcbc5c6360f083f
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml pull
Pulling db        ... done
Pulling migration ... done
Pulling fpm       ... done
Pulling web       ... done
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml up --force-recreate --renew-anon-volumes -d
Recreating testrail-mysql-local ... done
Recreating testrail-fpm-local       ... done
Recreating testrail-migration-local ... done
Recreating testrail-web-local       ... done
docker ps
CONTAINER ID  PORTS                                     NAMES
a845d3cb0e5a  0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp  testrail-web-local
19d8ef001398  9000/tcp                                  testrail-fpm-local
e28840a2369c  3306/tcp, 33060/tcp                       testrail-migration-local
0e7900c23f37  3306/tcp                                  testrail-mysql-local

make docker-logs

$ make docker-logs
mkdir ./logs || true
mkdir: impossibile creare la directory ‘./logs’: File esistente
docker logs testrail-web-${CI_JOB_ID:-local}       >& logs/testrail-web.log
docker logs testrail-fpm-${CI_JOB_ID:-local}       >& logs/testrail-fpm.log
docker logs testrail-migration-${CI_JOB_ID:-local} >& logs/testrail-migration.log
docker logs testrail-mysql-${CI_JOB_ID:-local}     >& logs/testrail-mysql.log

GitLab Shell Runner. Esecuzione competitiva dei servizi testati tramite Docker Compose

Al contenuto

Preparazione .gitlab-ci.yml

Esecuzione dei test di integrazione

Integrazione:
  stage: test
  tags:
    - my-shell-runner
  before_script:
    # Autenticati nel registry
    - docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
    # Genera TR_HTTP_PORT e TR_HTTPS_PORT pseudo-unici
    - export TR_HTTP_PORT=$(shuf -i10000-60000 -n1)
    - export TR_HTTPS_PORT=$(shuf -i10000-60000 -n1)
    # crea una directory con l'identificatore del job
    - mkdir ${CI_JOB_ID}
    # copia il nostro docker-compose.yml nella directory creata
    # in modo che il contesto sia diverso per ogni job
    - cp .indirect/docker-compose.yml ${CI_JOB_ID}/docker-compose.yml
  script:
    # alziamo il nostro ambiente
    - make docker-up
    # eseguiamo i test con jar eseguibile (così faccio io)
    - java -jar itest.jar --http-port ${TR_HTTP_PORT} --https-port ${TR_HTTPS_PORT}
    # oppure in container
    - docker run --network=testrail-network-${CI_JOB_ID:-local} --rm itest
  after_script:
    # raccogliamo i log
    - make docker-logs
    # fermiamo l'ambiente
    - make docker-kill
  artifacts:
    # salviamo i log
    when: always
    paths:
      - logs
    expire_in: 30 days

Dopo l'esecuzione di un tale job, nella directory degli artefatti i log conterranno i log dei servizi e dei test. Questo è molto comodo in caso di errori. Ogni test in parallelo scrive il proprio log, ma ne parlerò separatamente.

GitLab Shell Runner. Esecuzione competitiva dei servizi testati tramite Docker Compose

Al contenuto

Pulizia del runner

Il job verrà eseguito solo su programmazione.

stages:
- clean
- build
- test

Clean runner:
  stage: clean
  only:
    - schedules
  tags:
    - my-shell-runner
  script:
    - make docker-clean

Poi andiamo nel nostro progetto GitLab -> CI/CD -> Schedules -> New Schedule e aggiungiamo un nuovo programma

GitLab Shell Runner. Esecuzione competitiva dei servizi testati tramite Docker Compose

Al contenuto

Risultato

Avviamo 4 job in GitLab CI
GitLab Shell Runner. Esecuzione competitiva dei servizi testati tramite Docker Compose

Nei log dell'ultimo job con i test di integrazione vediamo i container di diversi job

ID CONTENITORE NOME
c6b76f9135ed  testrail-web-204645172
01d303262d8e  testrail-fpm-204645172
2cdab1edbf6a  testrail-migration-204645172
826aaf7c0a29  testrail-mysql-204645172
6dbb3fae0322  testrail-web-204645084
3540f8d448ce  testrail-fpm-204645084
70fea72aa10d  testrail-mysql-204645084
d8aa24b2892d  testrail-web-204644881
6d4ccd910fad  testrail-fpm-204644881
685d8023a3ec  testrail-mysql-204644881
1cdfc692003a  testrail-web-204644793
6f26dfb2683e  testrail-fpm-204644793
029e16b26201  testrail-mysql-204644793
c10443222ac6  testrail-web-204567103
04339229397e  testrail-fpm-204567103
6ae0accab28d  testrail-mysql-204567103
b66b60d79e43  testrail-web-204553690
033b1f46afa9  testrail-fpm-204553690
a8879c5ef941  testrail-mysql-204553690
069954ba6010  testrail-web-204553539
ed6b17d911a5  testrail-fpm-204553539
1a1eed057ea0  testrail-mysql-204553539

Log più dettagliato

$ docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
ATTENZIONE! Utilizzare --password tramite CLI è insicuro. Usare --password-stdin.
ATTENZIONE! La tua password sarà memorizzata non criptata in /home/gitlab-runner/.docker/config.json.
Configura un gestore delle credenziali per rimuovere questo avviso. Vedi
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Accesso riuscito
$ export TR_HTTP_PORT=$(shuf -i10000-60000 -n1)
$ export TR_HTTPS_PORT=$(shuf -i10000-60000 -n1)
$ mkdir ${CI_JOB_ID}
$ cp .indirect/docker-compose.yml ${CI_JOB_ID}/docker-compose.yml
$ make docker-up
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml kill
docker network rm testrail-network-${CI_JOB_ID:-local} || true
Errore: Rete non trovata: testrail-network-204645172
docker network create testrail-network-${CI_JOB_ID:-local}
0a59552b4464b8ab484de6ae5054f3d5752902910bacb0a7b5eca698766d0331
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml pull
Pulling web       ... done
Pulling fpm       ... done
Pulling migration ... done
Pulling db        ... done
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml up --force-recreate --renew-anon-volumes -d
Creazione volume "204645172_static-content" con driver predefinito
Creazione testrail-mysql-204645172 ... 
Creazione testrail-mysql-204645172 ... completato
Creazione testrail-migration-204645172 ... completato
Creazione testrail-fpm-204645172       ... completato
Creazione testrail-web-204645172       ... completato
docker ps
ID CONTENITORE        IMMAGINE                                                          COMANDO                  CREATO              STATO              PORTI                                           NOME
c6b76f9135ed        registry.gitlab.com/touchbit/image/testrail/web:latest         "nginx -g 'daemon of…"   13 secondi fa       In esecuzione 1 secondo         0.0.0.0:51148->80/tcp, 0.0.0.0:25426->443/tcp   testrail-web-204645172
01d303262d8e        registry.gitlab.com/touchbit/image/testrail/fpm:latest         "docker-php-entrypoi…"   16 secondi fa       In esecuzione 13 secondi       9000/tcp                                        testrail-fpm-204645172
2cdab1edbf6a        registry.gitlab.com/touchbit/image/testrail/migration:latest   "docker-entrypoint.s…"   16 secondi fa       In esecuzione 13 secondi       3306/tcp, 33060/tcp                             testrail-migration-204645172
826aaf7c0a29        mysql:5.7.22                                                   "docker-entrypoint.s…"   18 secondi fa       In esecuzione 16 secondi       3306/tcp                                        testrail-mysql-204645172
6dbb3fae0322        registry.gitlab.com/touchbit/image/testrail/web:latest         "nginx -g 'daemon of…"   36 secondi fa       In esecuzione 22 secondi       0.0.0.0:44202->80/tcp, 0.0.0.0:20151->443/tcp   testrail-web-204645084
3540f8d448ce        registry.gitlab.com/touchbit/image/testrail/fpm:latest         "docker-php-entrypoi…"   38 secondi fa       In esecuzione 35 secondi       9000/tcp                                        testrail-fpm-204645084
70fea72aa10d        mysql:5.7.22                                                   "docker-entrypoint.s…"   40 secondi fa       In esecuzione 37 secondi       3306/tcp                                        testrail-mysql-204645084
d8aa24b2892d        registry.gitlab.com/touchbit/image/testrail/web:latest         "nginx -g 'daemon of…"   Circa un minuto fa   In esecuzione 53 secondi       0.0.0.0:31103->80/tcp, 0.0.0.0:43872->443/tcp   testrail-web-204644881
6d4ccd910fad        registry.gitlab.com/touchbit/image/testrail/fpm:latest         "docker-php-entrypoi…"   Circa un minuto fa   In esecuzione Circa un minuto   9000/tcp                                        testrail-fpm-204644881
685d8023a3ec        mysql:5.7.22                                                   "docker-entrypoint.s…"   Circa un minuto fa   In esecuzione Circa un minuto   3306/tcp                                        testrail-mysql-204644881
1cdfc692003a        registry.gitlab.com/touchbit/image/testrail/web:latest         "nginx -g 'daemon of…"   Circa un minuto fa   In esecuzione Circa un minuto   0.0.0.0:44752->80/tcp, 0.0.0.0:23540->443/tcp   testrail-web-204644793
6f26dfb2683e        registry.gitlab.com/touchbit/image/testrail/fpm:latest         "docker-php-entrypoi…"   Circa un minuto fa   In esecuzione Circa un minuto   9000/tcp                                        testrail-fpm-204644793
029e16b26201        mysql:5.7.22                                                   "docker-entrypoint.s…"   Circa un minuto fa   In esecuzione Circa un minuto   3306/tcp                                        testrail-mysql-204644793
c10443222ac6        registry.gitlab.com/touchbit/image/testrail/web:latest         "nginx -g 'daemon of…"   5 ore fa          In esecuzione 5 ore          0.0.0.0:57123->80/tcp, 0.0.0.0:31657->443/tcp   testrail-web-204567103
04339229397e        registry.gitlab.com/touchbit/image/testrail/fpm:latest         "docker-php-entrypoi…"   5 ore fa          In esecuzione 5 ore          9000/tcp                                        testrail-fpm-204567103
6ae0accab28d        mysql:5.7.22                                                   "docker-entrypoint.s…"   5 ore fa          In esecuzione 5 ore          3306/tcp                                        testrail-mysql-204567103
b66b60d79e43        registry.gitlab.com/touchbit/image/testrail/web:latest         "nginx -g 'daemon of…"   5 ore fa          In esecuzione 5 ore          0.0.0.0:56321->80/tcp, 0.0.0.0:58749->443/tcp   testrail-web-204553690
033b1f46afa9        registry.gitlab.com/touchbit/image/testrail/fpm:latest         "docker-php-entrypoi…"   5 ore fa          In esecuzione 5 ore          9000/tcp                                        testrail-fpm-204553690
a8879c5ef941        mysql:5.7.22                                                   "docker-entrypoint.s…"   5 ore fa          In esecuzione 5 ore          3306/tcp                                        testrail-mysql-204553690
069954ba6010        registry.gitlab.com/touchbit/image/testrail/web:latest         "nginx -g 'daemon of…"   5 ore fa          In esecuzione 5 ore          0.0.0.0:32869->80/tcp, 0.0.0.0:16066->443/tcp   testrail-web-204553539
ed6b17d911a5        registry.gitlab.com/touchbit/image/testrail/fpm:latest         "docker-php-entrypoi…"   5 ore fa          In esecuzione 5 ore          9000/tcp                                        testrail-fpm-204553539
1a1eed057ea0        mysql:5.7.22                                                   "docker-entrypoint.s…"   5 ore fa          In esecuzione 5 ore          3306/tcp                                        testrail-mysql-204553539

Tutti i task sono stati completati con successo

Gli artefatti del task contengono i log dei servizi e dei test
GitLab Shell Runner. Esecuzione competitiva dei servizi testati tramite Docker Compose

GitLab Shell Runner. Esecuzione competitiva dei servizi testati tramite Docker Compose

Tutto sembra a posto, ma c'è un piccolo dettaglio. Il pipeline può essere interrotto forzatamente durante l'esecuzione dei test di integrazione e in questo caso i container avviati non verranno fermati. Di tanto in tanto è necessario ripulire il runner. Purtroppo, la richiesta di miglioramento in GitLab CE è ancora in attesa. Aperto

Ma abbiamo aggiunto l'avvio di attività programmate e nessuno ci impedisce di avviarle manualmente.
Passiamo al nostro progetto -> CI/CD -> Schedules e avviamo l'attività. Clean runner

GitLab Shell Runner. Esecuzione competitiva dei servizi testati tramite Docker Compose

In totale:

  • Abbiamo un solo shell runner.
  • Non ci sono conflitti tra le attività e l'ambiente.
  • Abbiamo l'esecuzione parallela delle attività con i test di integrazione.
  • È possibile eseguire i test di integrazione sia localmente che nel container.
  • I log dei servizi e dei test vengono raccolti e allegati al task del pipeline.
  • C'è la possibilità di ripulire il runner da vecchi docker images.

Il tempo di configurazione è di circa 2 ore.
Ecco, sostanzialmente, è tutto. Sarò felice di ricevere feedback.

Al contenuto

Fonte: habr.com

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