Cássio Gabriel

Provisionando um simples repositório Git com Terraform

Utilizei da ferramenta Terraform para provisionar em uma EC2 t3.small um repositório git para enviar meus projetos.

Publicado em:   ·   Última atualização:   ·  8 min de leitura

O porquê deste projeto #

Estabelecer de forma prática e versionável um respositório remoto próprio para enviar meus projetos e coisas que tais. Utilizei das ferramentas Git como versionamento, Ngix como servidor e Terraform como infraestrutura-como-código, configurando assim o ambiente em uma instância AWS EC2 t3.small de 2GB, sem necessidade de armazenamento extra.


Objetivos do projeto #

  • Provisionar infraestrutura na AWS de forma declarativa com Terraform;
  • Criar uma instância EC2 Ubuntu para hospedar um servidor Git;
  • Permitir operações Git apenas via SSH (clone, push, pull);
  • Disponibilizar uma interface web de visualização dos repositórios com GitWeb;
  • Expor o GitWeb exclusivamente via CloudFront (HTTPS);
  • Restringir o acesso HTTP da instância apenas ao CloudFront;
  • Manter o ambiente simples, sem ALB, WAF ou serviços adicionais.

Visão geral da arquitetura #

A arquitetura final é composta por poucos elementos, mas bem definidos:

  • EC2 (Ubuntu 22.04)
    Responsável por:

    • Git (repositórios bare)
    • GitWeb
    • Nginx
    • fcgiwrap
  • CloudFront Atua como camada de exposição segura:

    • HTTPS para o público
    • Origem restrita à instância EC2
    • Nenhum acesso HTTP direto à instância
  • Rede (VPC padrão)

    • Sub-rede pública
    • Internet Gateway
    • Tabela de rotas explícita

Repositório Git em produção #

Link do repositório em produção –> aqui

Provisionamento com Terraform #

Pensando em deixar o mais simples possível, decidi por escolher uma configuração simples para provisionar o ambiente na AWS, sem muitas atribuições ou configurações que não sejam úteis para um simples repositório Git.

Foram provisionados:

  • VPC padrão da AWS;
  • Sub-rede pública em uma AZ;
  • Internet Gateway e associação explícita na tabela de rotas;
  • Security Group com regras mínimas:
    • SSH (22): permitido apenas a partir do meu IP público;
    • HTTP (80): permitido somente para o prefix list gerenciado da AWS referente ao CloudFront origin-facing;
  • Instância EC2 t3.small, com Ubuntu 22.04;
    • Script user_data_config.sh para deploy automático das configurações do servidor para o repositório git;
  • Elastic IP para garantir estabilidade da origem;
  • Distribuição CloudFront apontando para o IP público da instância.

variables.tf:

variable "region" {
  type    = string
  default = "us-east-1"
}

variable "my_ip_cidr" {
  type        = string
  description = "Your public IP in CIDR format, e.g. 203.0.113.10/32"
}

variable "key_name" {
  type        = string
  description = "Existing EC2 Key Pair name in the target region"
}

variable "instance_type" {
  type    = string
  default = "t3.small"
}

variable "project_name" {
  type    = string
  default = "host-gitweb"
}

main.tf:

provider "aws" {
  region = var.region
}

# Default VPC
data "aws_vpc" "default" {
  default = true
}

# Default subnet in us-east-1a
resource "aws_default_subnet" "a" {
  availability_zone = "us-east-1a"
}

# Internet Gateway for default VPC
resource "aws_internet_gateway" "igw" {
  vpc_id = data.aws_vpc.default.id
}

# Public route table (0.0.0.0/0 -> IGW)
resource "aws_route_table" "public" {
  vpc_id = data.aws_vpc.default.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "${var.project_name}-rt-public"
  }
}

# Associate route table to subnet
resource "aws_route_table_association" "a" {
  subnet_id      = aws_default_subnet.a.id
  route_table_id = aws_route_table.public.id
}

# Ubuntu AMI
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

# CloudFront origin-facing managed prefix list
data "aws_ec2_managed_prefix_list" "cloudfront_origin" {
  name = "com.amazonaws.global.cloudfront.origin-facing"
}

# Security Group
resource "aws_security_group" "gitweb" {
  name        = "${var.project_name}-sg"
  description = "SSH from my IP; HTTP only from CloudFront origin-facing"
  vpc_id      = data.aws_vpc.default.id

  ingress {
    description = "SSH only from my IP"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.my_ip_cidr]
  }

  ingress {
    description     = "HTTP only from CloudFront origin-facing"
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    prefix_list_ids = [data.aws_ec2_managed_prefix_list.cloudfront_origin.id]
  }

  egress {
    description = "Allow outbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# EC2
resource "aws_instance" "gitweb" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = var.instance_type
  subnet_id              = aws_default_subnet.a.id
  vpc_security_group_ids = [aws_security_group.gitweb.id]
  key_name               = var.key_name

  # Launch the script into the instance
  user_data = file("${path.module}/user_data_config.sh")

  tags = {
    Name = "${var.project_name}-ec2"
  }
}

# Elastic IP
resource "aws_eip" "gitweb" {
  domain   = "vpc"
  instance = aws_instance.gitweb.id

  tags = {
    Name = "${var.project_name}-eip"
  }
}

# CloudFront distribution
resource "aws_cloudfront_distribution" "gitweb" {
  enabled         = true
  is_ipv6_enabled = true
  comment         = "GitWeb behind CloudFront (origin restricted)"

  origin {
    domain_name = aws_eip.gitweb.public_dns
    origin_id   = "${var.project_name}-origin"

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "http-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  default_cache_behavior {
    target_origin_id        = "${var.project_name}-origin"
    viewer_protocol_policy = "redirect-to-https"

    allowed_methods = ["GET", "HEAD"]
    cached_methods  = ["GET", "HEAD"]

    forwarded_values {
      query_string = true

      cookies {
        forward = "none"
      }
    }

    min_ttl     = 0
    default_ttl = 0
    max_ttl     = 60
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

O restante das outras configurações podem ser encontradas aqui.

Seguindo com a aplicação da infraestrutura:

$ terraform apply

Tive que destruir algumas vezes a infraestrutura para realizar alguns testes, então alguns prints estarão com um IP elástico fixo distinto.

Com a infraestrutura já estabelecida, partamos para a função do script user_data_config.sh.


Configuração do servidor via user_data_config.sh #

O script em questão:

#!/bin/bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive

apt-get update
apt-get install -y git gitweb fcgiwrap nginx 

# --- a dedicated 'git' user, git-shell only ---
if ! id git >/dev/null 2>&1; then
  useradd -m -d /home/git -s /usr/bin/git-shell git
fi

# Repo root /var/lib/git
mkdir -p /var/lib/git
chown -R git:git /var/lib/git
chmod 2750 /var/lib/git

# --- GitWeb config ---
cat >/etc/gitweb.conf <<'EOF'
$projectroot = "/var/lib/git";
$projects_list = $projectroot;
$site_name = "My Git Server (GitWeb)";
EOF

systemctl enable --now fcgiwrap

# --- Nginx serving GitWeb via fcgiwrap ---
cat >/etc/nginx/sites-available/gitweb <<'EOF'
server {
  listen 80;
  server_name _;

  add_header X-Content-Type-Options nosniff always;
  add_header X-Frame-Options SAMEORIGIN always;
  add_header Referrer-Policy no-referrer always;

  location = / { return 302 /cgi-bin/gitweb.cgi; }

  location /gitweb/static/ {
    alias /usr/share/gitweb/static/;
  }

  location /cgi-bin/gitweb.cgi {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /usr/lib/cgi-bin/gitweb.cgi;
    fastcgi_param GITWEB_CONFIG /etc/gitweb.conf;
    fastcgi_pass unix:/run/fcgiwrap.socket;
  }
}
EOF

rm -f /etc/nginx/sites-enabled/default
ln -sf /etc/nginx/sites-available/gitweb /etc/nginx/sites-enabled/gitweb
nginx -t
systemctl enable --now nginx

# --- SSH hardening ---
sed -i 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config

# Your requirement: allow root login (key-only)
sed -i 's/^#\?PermitRootLogin .*/PermitRootLogin yes/' /etc/ssh/sshd_config
systemctl restart ssh

# Convenience: allow same key used for ubuntu user to be used for git user
if [ -f /home/ubuntu/.ssh/authorized_keys ]; then
  install -d -m 700 -o git -g git /home/git/.ssh
  cat /home/ubuntu/.ssh/authorized_keys > /home/git/.ssh/authorized_keys
  chown git:git /home/git/.ssh/authorized_keys
  chmod 600 /home/git/.ssh/authorized_keys
fi

Com a infraestrutura provisionada, o próximo passo foi garantir que a instância EC2 subisse já configurada para atuar como servidor Git. Para isso, utilizei um script user_data_config.sh, executado automaticamente no primeiro boot da instância.

A ideia foi simples: subir o servidor já pronto, sem necessita de configuraçõs manuais de boot posteriores.


Instalação dos pacotes essenciais #

Logo no início do script, atualizei os repositórios e instalei apenas os pacotes necessários:

  • git: para gerenciamento dos repositórios;
  • gitweb: interface web de visualização;
  • nginx: servidor HTTP;
  • fcgiwrap: para permitir que o Nginx execute o CGI do GitWeb.

Nada além do necessário, mantendo o ambiente leve e fácil de entender.


Criação do usuário git #

Seguindo a prática comum para servidores Git, criei um usuário dedicado chamado git.

Esse usuário:

  • não possui shell interativo;
  • utiliza exclusivamente o git-shell;
  • é responsável por hospedar e gerenciar os repositórios.

Isso garante uma separação clara entre:

  • administração do sistema (ubuntu/root);
  • operações Git (git).

Estrutura dos repositórios #

Defini o diretório /var/lib/git como raiz dos repositórios.

Esse caminho:

  • é simples;
  • é facilmente referenciável pelo GitWeb;
  • facilita permissões e organização.

Todos os repositórios criados são do tipo bare, conforme esperado para servidores Git.

Configurando os respositórios remoto e local #

Para iniciar o repositório git “bare” (vazio), tive que executar os seguintes comandos no servidor, para configurar o ambiente remoto:

$ sudo -u git git init --bare /var/lib/git/<projeto>.git


Configuração do GitWeb #

Em seguida, configurei o arquivo /etc/gitweb.conf apontando o GitWeb para o diretório correto dos repositórios.

A ideia foi permitir que o GitWeb:

  • detecte automaticamente os repositórios;
  • liste todos os projetos disponíveis;
  • não dependa de arquivos extras ou indexações manuais.

Essa configuração foi propositalmente simples para evitar erros comuns e facilitar manutenção.


Integração GitWeb + Nginx #

O GitWeb funciona como um CGI. Para integrá-lo ao Nginx, utilizei o fcgiwrap.

Configurei o Nginx para:

  • responder na porta 80;
  • redirecionar o arquivo / para o GitWeb;
  • servir corretamente os arquivos estáticos do GitWeb;
  • executar o gitweb.cgi via FastCGI.

Essa abordagem elimina a necessidade de Apache e mantém o stack mais simples.


Hardening básico de SSH #

Como o acesso ao servidor é feito via SSH, desativei autenticação por senha, mantendo apenas key-based authentication.

Também habilitei login como root via chave, por necessidade de testes e administração direta, ciente de que isso não é o padrão mais restritivo, mas aceitável dentro do contexto do projeto.


Preparação de acesso Git via SSH #

Por fim, garanti que a chave SSH associada à instância fosse automaticamente instalada para o usuário git.

Isso resolve um ponto importante:

  • o acesso SSH administrativo funciona com ubuntu;
  • o git push funciona com o usuário git;
  • ambos utilizam a mesma chave, sem configuração manual adicional.

Com isso, qualquer git push via SSH passa a funcionar imediatamente após o provisionamento.

Configuração específica no ~/.ssh/config #

Para facilitar a utilização do git junto ao servidor, como também para separar logicamente minhha conexão a este ou outros repositórios git remotos, configurei no arquivo ~/.ssh/config as especificações de conexão ao repositório remoto aqui discutido:

Então, fiz as seguintes modificações no repositório local:

Adicionei o origin apontando para o repositório remoto, que recebe/envia pull/push via ssh, que são enviados pela chave do usuário git no servidor.

Todo o controle de escrita acontece exclusivamente via SSH, utilizando o usuário git.


Resultado final #

Ao final do boot da instância:

  • o servidor já aceita conexões SSH;
  • repositórios Git podem ser criados imediatamente;
  • pushes via git push funcionam sem ajustes adicionais;
  • o GitWeb já está disponível e funcional;
  • a interface web é exposta de forma segura via CloudFront.
  • É possível adicionar outras funcionalidades na infraestrutura posteriormente, como ALB e Backup automático;
  • Também é possível configurar uma outra interface Git posterior, caso surja necessidade.

Todo o ambiente nasce pronto para uso, reproduzível e controlado via Terraform.


Este repositório está disponibilizado aqui, igualmente.