Gabriel • 6 minutes

PET-SI-Infraestrutura


PET-SI - Infraestrutura

Na EACH-USP, durante o ano de calouro, há uma competição denominada de BXComp, consiste em um campeonato separado em alguns dias em que consiste em resolver desafios feitos pelos veteranos em estilo maratona em equipes. Para essa competição acontecer é como utilizar uma plataforma de auto-correção de código e formação de equipes, porém desde a vez que eu participei, fiquei intrigado com o acesso somente em IPv4...

Nesse ano, conversando com o pessoal do PET, descobri que na sala reservada há uns computadores sem utilização que pelo que entendi já foram usados no passado para uso dos serviços do PET. Desde então propus e nesse artigo descrevo o servidor do PET!

Hardware

Do que vi há uns 2-3 computadores com especificações parecidas, mas por enquanto foi somente usado um com o seguinte conjunto:

OS e ferramentas

Para o sistema operacional foi escolhido o Debian GNU/Linux pela sua alta estabilidade e familiaridade com o Universo mais próximo do Ubuntu GNU/Linux instalado pela universidade nos computadores dos laboratórios.

Como Stack principal de infraestrutura foi escolhido utilizar Containers em Docker. Fiz essa escolha porque containers permitem isolar os serviços evitando possíveis problemas e pelo aprendizado de quem quiser gerenciar, já que acho esse tipo de tecnologia fundamental de entender que existe, e o Docker por ser a ferramenta mais utilizada e documentada para essa tecnologia.

A internet fornecida para o computador chega direto da universidade cabeado, porém somente em IPv4 e com NAT, então foi necessário utilizar ferramentas e meios extras para o acesso. Dessa forma há o local pelo WI-FI da sala do PET, pelo Cloudflare Tunnels para os serviços públicos e formas de VPN que por enquanto a escolha foi o Tailscale.

Software

Pacotes instalador no Host (Debian)

Para conseguir todos os pacotes explicitamente instalados pelo usuário é possível filtar pelo historico de uso do APT:

zgrep -E "^Commandline:.*install" /var/log/apt/history.log* | grep -v '\-q' | cut -d':' -f3

Assim os pacotes e ferramentas CLI instalados incluem as seguintes:

sudo apt update
sudo apt upgrade -y
sudo apt --assume-no install -yq htop ncdu tar git unzip zip curl wget fastfetch nano vim lm-sensors gnupg figlet btop tmux bind9-dnsutils ca-certificates ethtool iperf3 rsync lsof bash-completion rsync sudo quota lshw fzf dos2unix avahi-daemon distrobox

Para instalar o Tailscale:

curl -fsSL https://tailscale.com/install.sh | sh

Para instalar o Docker:

# Add Docker's official GPG key:
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Container

O principal container é o Portainer, um painel de controle em web para gerenciar o Docker, para instalar rode os seguintes comandos:

mkdir -p ~/compose/portainer
cat << 'EOF' > ~/compose/portainer/docker-compose.yml
services:
  portainer:
    container_name: portainer
    image: portainer/portainer-ce:lts
    ports:
      - 9443:9443
    volumes:
      - data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped
volumes:
  data:
EOF

sudo docker compose -f ~/compose/portainer/docker-compose.yml up -d

E para a configuração inicial acesse https://<IP do servidor>:9443/. Crie o usuário administrador e adicione o "environment" em "Socket".

Portainer

Após logar e acessar o environment nomeado por mim de local, há um menu na esquerda com as principais opções:

Infraestrutura de serviços

Para expor múltiplos serviços utilizando as portas 80 (http) e 443 (https) é necessário o uso de um proxy reverso, há várias opções interessantes incluindo NGINX, NGINX proxy manager, Caddy, HAProxy, Envoy, Traefik. Esse último foi escolhido pela possibilidade de a configuração estar centralizada no próprio compose, com intrgração automática ao Docker e renovação automática dos certificador HTTPS em DNS na Cloudflare.

Como diagrama pode-se dizer que os acessos vão funcionar assim:

graph TD

UserExt["Usuário Externo"]

subgraph CF["Cloudflare"]
    DNS["Cloudflare DNS"]
    Tunnel["Cloudflare Tunnel"]
    DNS --> Tunnel
end

subgraph PET["Rede do PET"]
    UserInt["Usuário Interno"]
    subgraph DockerNet["Rede Docker (traefik_proxy)"]
	    CFD["cloudflared"]
        Traefik["Traefik"]
        Apps["HTTP/HTTPS Apps"]
        Traefik --> Apps
    end

    CFD --> Traefik
    UserInt --> Traefik
end

UserExt --> DNS
Tunnel ==> CFD

Para utilizar os serviços da Cloudflare é necessário ter um domínio registrado. Ter um SLD (second-level domain) tipo gabrielsouza.top é pago e para fins de infra foi utilizado alternativas Third-Level Domain que apesar de geralmente a Cloudflare não aceitar, vários listados na lista do Public Suffix são aceitos. Na data de hoje, foi registrado no provedor DigitalPlat um domínio .dpdns.org, lembrando de sempre preferir TLDs (top-level domain) de 3 letras ou mais, pois os de 2 letras são considerados ccTLDs (Country code TLD) o que significa que a existência do domínio fica sob jurisdição do país pertencente.

Após a criação do domínio, foi alterado o servidor NS (nameserver record) para os indicados durante a configuração inicial de adição do domínio na Cloudflare. No painel da Cloudflare vá no menu esquerdo em "zero Trust"->"Networks"->"Tunnels & Mesh" e crie um Tunnel no tipo "Cloudflared", coloque um nome e copie o Token Em outra aba, abra o painel de "API Tokens", crie com o template "Edit zone DNS" no domínio específico e copie o Token.

Agora abra o Portainer e em "Stacks" crie o "traefik" com o seguinte conteúdo base (leia e modifique conforme necessário) e variáveis de ambiente:

services:
  traefik:
    image: traefik:v3.7
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    ports:
      - 80:80
      - 443:443
      - 8080:8080 # (optional) expose the dashboard !don't use in production!
    volumes:
      - config:/etc/traefik
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      proxy:
    environment:
      CERTIFICATEMAIL:
      FQDNMAIN:
      FQDNSANS:
      CF_DNS_API_TOKEN:
    healthcheck: # enable healthchecks
      test: ["CMD", "traefik", "healthcheck", "--ping"]
      start_period: 5s
      interval: 5s
      timeout: 3s
      retries: 5
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.rule=Host(`traefik.petsi.dpdns.org`)"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.services.traefik.loadbalancer.server.port=8080"
    command:
      # Global
      - "--global.sendanonymoususage=false"
      # EntryPoints
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--entrypoints.web.http.redirections.entrypoint.permanent=true"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.websecure.http.tls=true"
      - "--entrypoints.websecure.http.tls.certresolver=letsencrypt"
      - "--entrypoints.websecure.http.tls.domains[0].main=$FQDNMAIN"
      - "--entrypoints.websecure.http.tls.domains[0].sans=$FQDNSANS"
      - "--entryPoints.websecure.forwardedHeaders.trustedIPs=127.0.0.1/32,172.16.0.0/12"

      # Certificate
      - "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"
      - "--certificatesresolvers.letsencrypt.acme.dnschallenge.resolvers=1.1.1.1:53"
      - "--certificatesresolvers.letsencrypt.acme.email=$CERTIFICATEMAIL"
      - "--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme.json"

      # Providers
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=traefik_proxy"

      # API & Dashboard
      - "--api.dashboard=true"
      - "--api.insecure=true"

      # Observability
      - "--log=true"
      - "--log.level=DEBUG"
      - "--accesslog=true"
      - "--metrics.prometheus=true"
      - "--ping=true"

      # EITS
      - "--entrypoints.websecure.http.tls.domains[1].main=$FQDNMAINEITS"
      - "--entrypoints.websecure.http.tls.domains[1].sans=$FQDNSANSEITS"
  cloudflared:
    image: cloudflare/cloudflared
    container_name: cloudflared
    environment:
      - TZ=America/Sao_Paulo
      - TUNNEL_TOKEN=$CLOUDFLARE_TOKEN
      - TUNNEL_METRICS=127.0.0.1:20241
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    healthcheck: # enable healthchecks
      test: ["CMD", "cloudflared", "tunnel", "ready"]
      start_period: 5s
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      proxy:
    depends_on:
      traefik:
        condition: service_healthy
  whoami:
    image: "traefik/whoami"
    container_name: "whoami"
    networks:
      proxy:
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`petsi.dpdns.org`) || Host(`eits.dpdns.org`)"
      - "traefik.http.routers.whoami.entrypoints=websecure"
      - "traefik.http.services.whoami.loadbalancer.server.port=80"
    depends_on:
      - traefik
volumes:
  config:
  certs:
networks:
  proxy:

Em "Environment variables" adicione as seguintes variáveis:

CLOUDFLARE_TOKEN=<token cloudflared>
CF_DNS_API_TOKEN=<token zona DNS>
CERTIFICATEMAIL=<email para solicitar certificados let's encrypt>
FQDNMAIN=<fqdn>
FQDNSANS=*.<fqdn>
FQDNMAINEITS=<fqdn secundário>
FQDNSANSEITS=*.<fqdn secundário>

Volte no painel de Tunnels, acesse o recém criado, vá na aba "Published application routes" e para cada entrada edite adicionando em "TLS"->"Origin Server Name" o hostname completo como descrito acima da página em "Hostname", ative também o "HTTP2 connection" e salve.

Se tudo está certo ao acessar a página do domínio, vai responder com o teste do serviço "whoami" já com HTTPS totalmente funcional.

JudgeHost

Para hospedar o serviço JudgeHost para montar uma maratona de programação, é necessário rodar alguns comandos antes de criar a stack.

# 1. Back up your existing GRUB config
sudo cp /etc/default/grub /etc/default/grub.bak

# 2. Append missing flags to GRUB_CMDLINE_LINUX_DEFAULT
for flag in cgroup_enable=memory swapaccount=1 systemd.unified_cgroup_hierarchy=0; do
  sudo grep -q "^GRUB_CMDLINE_LINUX_DEFAULT=.*$flag" /etc/default/grub || \
  sudo sed -i -E "s/^(GRUB_CMDLINE_LINUX_DEFAULT=\"[^\"]*)/\1 $flag/" /etc/default/grub
done

sudo grub-mkconfig -o /boot/grub/grub.cfg

use o seguinte compose, criado a partir da documentação oficial e ajuda do tutorial de como rodar na AWS feito pela Giane Mayumi com as variáveis:

x-common-configuration: &common-configuration
  image: domjudge/judgehost:latest
  privileged: true
  restart: unless-stopped
  volumes:
    - /sys/fs/cgroup:/sys/fs/cgroup
  depends_on:
    domserver:
      condition: service_healthy

services:
  mariadb:
    container_name: mariadb
    image: mariadb:latest
    env_file:
      - stack.env
    volumes:
      - mysql:/var/lib/mysql
    command: --max-connections=1000 --innodb-log-file-size=2G --max-allowed-packet=512M
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      start_period: 15s
      interval: 10s
      timeout: 5s
      retries: 3

  domserver:
    container_name: domserver
    image: domjudge/domserver:latest
    restart: unless-stopped
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
      - public_images:/opt/domjudge/domserver/webapp/public/images
      - public_css:/opt/domjudge/domserver/webapp/public/css/custom
      - public_js:/opt/domjudge/domserver/webapp/public/js/custom
      - public_docs:/opt/domjudge/domserver/webapp/public/docs
    ports:
      - 12345:80
    networks:
      - default
      - traefik_proxy
    depends_on:
      mariadb:
        condition: service_healthy
    environment:
      - CONTAINER_TIMEZONE=America/Sao_Paulo
      - MYSQL_HOST=mariadb
      - MYSQL_DATABASE=$MYSQL_DATABASE
      - MYSQL_USER=$MYSQL_USER
      - MYSQL_PASSWORD=$MYSQL_PASSWORD
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/api/v4/"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 15s
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.domjudge.rule=Host(`bxcomp.petsi.dpdns.org`)"
      - "traefik.http.routers.domjudge.entrypoints=websecure"
      - "traefik.http.services.domjudge.loadbalancer.server.port=80"

  judgehost-0:
    <<: *common-configuration
    container_name: judgehost-0
    hostname: judgedaemon-0
    environment:
      - DAEMON_ID=0
      - CONTAINER_TIMEZONE=America/Sao_Paulo
      - JUDGEDAEMON_PASSWORD=$JUDGEDAEMON_PASSWORD

  judgehost-1:
    <<: *common-configuration
    container_name: judgehost-1
    hostname: judgedaemon-1
    environment:
      - DAEMON_ID=1
      - CONTAINER_TIMEZONE=America/Sao_Paulo
      - JUDGEDAEMON_PASSWORD=$JUDGEDAEMON_PASSWORD

  judgehost-2:
    <<: *common-configuration
    container_name: judgehost-2
    hostname: judgedaemon-2
    environment:
      - DAEMON_ID=2
      - CONTAINER_TIMEZONE=America/Sao_Paulo
      - JUDGEDAEMON_PASSWORD=$JUDGEDAEMON_PASSWORD

  judgehost-3:
    <<: *common-configuration
    container_name: judgehost-3
    hostname: judgedaemon-3
    environment:
      - DAEMON_ID=3
      - CONTAINER_TIMEZONE=America/Sao_Paulo
      - JUDGEDAEMON_PASSWORD=$JUDGEDAEMON_PASSWORD
volumes:
  mysql:
  public_images:
  public_css:
  public_js:
  public_docs:
networks:
  traefik_proxy:
    external: true

Adicione as seguintes ENVs, trocando as senhas por saídas aleatórias geradas por esse comando:

tr -dc 'A-Za-z0-9_!@#$%&*' < /dev/random | head -c 64;echo
MYSQL_DATABASE=bxcomp
MYSQL_USER=bxcomp
MYSQL_PASSWORD=<generated pwd>
MYSQL_ROOT_PASSWORD=<generated pwd>
JUDGEDAEMON_PASSWORD=

Na primeira vez que os containers rodarem, os judgehost-* irão morrer, pois deixamos a senha "JUDGEDAEMON_PASSWORD" vazia. Para conseguir a senha certa copie do comando docker logs domserver e adicione editando a *stack* e então execute o "Update the stack". Pronto! Se tudo estiver certo é para ter um servidor JudgeHost rodando. Acredite essa é a forma mais fácil de obter tudo rodando.

Manutenção

Para os navegantes que apesar de achar legal ver funcionando ainda não conhece todos os mecanismos descritos está alguns comandos importantes para diagnosticar alguns problemas que podem acontecer nesse sistema:

Linux

Ver todas as portas expostas ouvindo no sistema:

ss -tulpn | grep LISTEN

Ver uso total dos discos:

df -h

Ver resumo do sistema (gerenciador de tarefas):

btop
htop

Ver temperaturas atuais do sistema:

sensors

Busca por arquivo, pasta, ou interativo (TUI) dentro da pasta atual:

find . -type f -name "*<substring no nome>*"
find . -type d -name "*<substring no nome>*"
fzf

Manual, tldr (too long don't read) para cada comando:

man <comando>
curl cheat.sh/<comando>

Importante: Ver se o sistema está gargalando ou está aguentando a carga:

uptime

Exemplo de saída:

14:39:04 up 6 days, 1:38, 4 users, load average: 0.14, 0.18, 0.17

Esse comando mostra a quanto tempo o sistema está ligado, quantas sessões de usuários logados e o mais importante a média de load dos últimos 1 min, 5 min, 15 min. Para interpretar é necessário saber a quantidade de threads do processador, nesse caso há 8 threads, assim valores abaixo de 8 significam que o sistema está conseguindo escalonar todos os processos normalmente, caso estiver acima, analisar o que está acontecendo, pois o sistema não está conseguindo processar tudo.

Docker

Ver todos os containers rodando:

docker ps

Ver todos os containers criados, rodando ou não:

docker ps -a

Ver logs de determinado container:

docker logs <nome ou ID container>

Ver uso de recursos do sistema para cada container

docker stats

Entrar no shell/executar comando de um determinado container:

docker exec -it <nome ou ID container> sh
docker exec -it <nome ou ID container> bash

docker exec -it <nome ou ID container> ls /

Iniciar, parar, reiniciar container específico:

docker start/stop/restart <nome ou ID container>

Ver todos os detalhes de um container específico:

docker inspect <nome ou ID container>

Limpar containers parados, redes não usadas e imagens sem tag (dangling):

docker system prune