Visão Geral
Este tutorial documenta uma solução completa e robusta para a transferência automática de backups do Amazon S3 (arquivos dump do PostgreSQL, entre 5 GB e 10 GB) para servidores SFTP externos utilizando uma infraestrutura serverless da AWS em conjunto com uma instância EC2 como ponte de processamento.
Arquitetura da Solução
O sistema implementa um fluxo de trabalho event-driven que:
- Detecta automaticamente novos backups depositados no S3
- Valida e filtra arquivos baseado em critérios específicos (prefixo, sufixo, nome)
- Aciona uma função Lambda que dispara comandos remotos via AWS Systems Manager
- Processa o download seguro do S3 para uma instância EC2 (EC2 evita o timeout de execução da Lambda)
- Transfere os arquivos para o servidor SFTP com verificação de integridade (EC2 para evitar o timeout)
- Garante confiabilidade através de retry automático, validação MD5 e logs detalhados
Características Principais
- ✅ Upload resiliente: Retry automático com backoff configurável
- ✅ Integridade garantida: Verificação MD5 e validação de tamanho
- ✅ Upload multipart: Transferência otimizada para arquivos grandes (chunks de 64KB)
- ✅ Temporary naming: Upload com extensão
.partantes da finalização - ✅ Logs completos: Rastreamento detalhado em
/var/log/sftp_uploader/ - ✅ Configuração flexível: Variáveis de ambiente via
.env - ✅ Autenticação múltipla: Suporte para senha ou chave SSH (RSA/OpenSSH)
Fluxo de Operação
S3 Event (PUT) → Lambda (filtro) → SSM Run Command → EC2 (download S3)
→ Validação Local → Upload SFTP (com retry) → Verificação MD5 → Conclusão
1. Criação da Lambda
- No console AWS Lambda: Create function → Author from scratch
- Name:
BackupTransferLambda - Runtime: Python 3.13
- Role: Crie uma role com permissões SSM SendCommand e S3 read.
- Name:
- Configure environment variables se necessário.
- Faça upload do código da Lambda (arquivo
lambda_function.pyfornecido).
2. Configuração da Trigger do S3
- No bucket S3
company-backups-bucket→ Properties → Event notifications → Create Event Notification - Configurações:
- Name:
TriggerBackupUpload - Event type: PUT
- Prefix:
db/daily/ - Suffix:
.dump - Destination: Lambda function →
BackupTransferLambda
- Name:
- Salve.
3. Permissões Necessárias
EC2 Role (Alterar o nome do bucket)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject","s3:ListBucket"],
"Resource": [
"arn:aws:s3:::company-backups-bucket",
"arn:aws:s3:::company-backups-bucket/*"
]
},
{
"Effect": "Allow",
"Action": ["ssm:SendCommand","ssm:GetCommandInvocation"],
"Resource": "*"
}
]
}
Lambda Role (Alterar id da conta AWS, região e instancia)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:SendCommand",
"ssm:GetCommandInvocation"
],
"Resource": [
"arn:aws:ec2:us-east-1:123456789012:instance/i-XXXXXXXXXX",
"arn:aws:ssm:us-east-1::document/AWS-RunShellScript"
]
}
]
}
4. Preparação da EC2 Ubuntu 24.04 e Testes
sudo apt update && sudo apt upgrade -y
sudo apt install -y python3 python3-pip python3-venv unzip curl
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
aws --version
cd /opt
sudo mkdir sftp_uploader_app
sudo chown ubuntu:ubuntu sftp_uploader_app
cd sftp_uploader_app
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install paramiko python-dotenv boto3
python3 -c "import paramiko; import boto3; import dotenv; print('Módulos OK')"
5. Estrutura de Pastas
/opt/sftp_uploader_app/
├── sftp_uploader.py
├── sftp_uploader_main.py
├── logrotate_sftp_uploader
├── sftp-uploader.service
├── sftp-uploader.timer
├── id_rsa
├── .env
└── venv/
-
/var/tmp/sftp_uploader→ temporário -
/var/log/sftp_uploader→ logs
sudo mkdir -p /var/tmp/sftp_uploader
sudo mkdir -p /var/log/sftp_uploader
sudo chown -R ubuntu:ubuntu /var/tmp/sftp_uploader /var/log/sftp_uploader
6. Configuração do Arquivo .env
Crie o arquivo /opt/sftp_uploader_app/.env com o seguinte conteúdo:
# Configuração SFTP
SFTP_HOST=sftp.external-server.com
SFTP_PORT=22
SFTP_USER=transfer_user
SFTP_PASSWORD=your_secure_password_here
SFTP_KEY_PATH=/opt/sftp_uploader_app/id_rsa
SFTP_KEY_PASSPHRASE=your_key_passphrase
# Pasta local (temporária para download S3)
LOCAL_DIR=/var/tmp/sftp_uploader
# Pasta remota SFTP
REMOTE_DIR=/
# Apagar arquivos locais após envio
DELETE_AFTER_UPLOAD=True
# Número de tentativas em caso de falha
RETRY_ATTEMPTS=3
RETRY_DELAY_SECONDS=10
# Logs
LOG_DIR=/var/log/sftp_uploader
# MD5 para verificação de integridade
VERIFY_MD5=True
# Bucket S3
S3_BUCKET_NAME=company-backups-bucket
7. Como descompactar o zip atualizado
cd /opt
sudo unzip sftp_uploader_app.zip -d /opt/sftp_uploader_app
sudo chown -R ubuntu:ubuntu /opt/sftp_uploader_app
Conteúdo do ZIP:
-
env(renomear para.env) -
id_rsa(chave SSH privada) lambda_handler.pylogrotate_sftp_uploadersftp_uploader_main.pysftp_uploader.pysftp-uploader.servicesftp-uploader.timer
8. Teste manual e logs
source /opt/sftp_uploader_app/venv/bin/activate
python3 /opt/sftp_uploader_app/sftp_uploader_main.py
- Logs em
/var/log/sftp_uploader/sftp_uploader.log - Run Command history na Lambda console
Exemplo de log:
2026-01-07 09:00:01 [INFO] Conectado ao SFTP sftp.external-server.com:22
2026-01-07 09:00:02 [INFO] Processando arquivo: /var/tmp/sftp_uploader/backup.dump -> /
2026-01-07 09:00:05 [INFO] Upload concluído (temp): backup.dump -> /backup.dump.part
2026-01-07 09:00:06 [INFO] Arquivo movido para destino: /backup.dump
2026-01-07 09:00:06 [INFO] Processamento concluído
Log Completo:
2026-01-07 10:54:12,561 [INFO] Baixando s3://company-backups-bucket/db/daily/database_client_backup.dump para /var/tmp/sftp_uploader/database_client_backup.dump
2026-01-07 10:54:12,581 [INFO] Found credentials from IAM Role: ssm
2026-01-07 10:54:36,407 [INFO] Connected (version 2.0, client AWS_SFTP_1.2)
2026-01-07 10:54:36,519 [INFO] Auth banner: b'Welcome to External SFTP Server!\n'
2026-01-07 10:54:36,520 [INFO] Authentication (publickey) successful!
2026-01-07 10:54:36,690 [INFO] [chan 0] Opened sftp connection (server version 3)
2026-01-07 10:54:36,690 [INFO] Conectado ao SFTP sftp.external-server.com:22 (attempt 1)
2026-01-07 10:54:36,691 [INFO] Processando arquivo: /var/tmp/sftp_uploader/database_client_backup.dump -> /
2026-01-07 10:54:42,737 [INFO] database_client_backup.dump - 50.0 MB enviados (1.69%)
2026-01-07 10:54:47,352 [INFO] database_client_backup.dump - 100.0 MB enviados (3.39%)
2026-01-07 10:54:52,081 [INFO] database_client_backup.dump - 150.0 MB enviados (5.08%)
[...]
2026-01-07 10:59:36,365 [INFO] Upload concluído (temp): /var/tmp/sftp_uploader/database_client_backup.dump -> /database_client_backup.dump.part
2026-01-07 11:26:07,525 [INFO] Arquivo movido para destino: /database_client_backup.dump
2026-01-07 11:26:07,676 [INFO] Arquivo local removido após upload: /var/tmp/sftp_uploader/database_client_backup.dump
2026-01-07 11:26:07,676 [INFO] Processamento concluído
2026-01-07 11:26:07,676 [INFO] [chan 0] sftp session closed.
9. Troubleshooting
-
Chave PPK não funciona: Paramiko precisa de chave OpenSSH (
.pemou sem conversão). Converta:puttygen chave.ppk -O private-openssh -o id_rsa -
Permissões:
chmod 600 id_rsa - Upload falha: Verifique prefix/suffix S3 e paths locais/remotos
- SSM não executa: Confirme role da Lambda e permissões da EC2
-
Timeout: Ajuste
TimeoutSecondsna Lambda oubanner_timeoutno Paramiko - MD5 não bate: Verifique se o arquivo foi completamente transferido ou se houve corrupção
Detalhes Técnicos da Implementação
Processo de Upload Seguro
-
Download do S3: Arquivo baixado para
/var/tmp/sftp_uploader/ - Filtro: Apenas arquivos contendo palavra-chave no nome são processados (configurável)
-
Upload temporário: Arquivo enviado com extensão
.part -
Validações:
- Comparação de tamanho (bytes locais vs remotos)
- Checksum MD5 (local vs remoto)
-
Rename atômico:
.part→ nome final -
Cleanup opcional: Remoção do arquivo local se
DELETE_AFTER_UPLOAD=True
Mecanismo de Retry
- Tentativas configuráveis via
RETRY_ATTEMPTS(padrão: 3) - Delay entre tentativas:
RETRY_DELAY_SECONDS(padrão: 10s) - Retry aplicado em:
- Conexão SFTP
- Upload de arquivo
- Validação MD5
Logs e Monitoramento
- Logs da Lambda: CloudWatch Logs
- Logs SSM: Run Command history no console AWS Systems Manager
-
Logs EC2:
/var/log/sftp_uploader/sftp_uploader.log -
Formato de log:
timestamp [LEVEL] mensagem
Opção 2: Lambda com Conexão Direta SSH à EC2
Esta opção remove o SSM Run Command e estabelece uma conexão SSH direta da Lambda para a EC2 através de uma interface de rede na mesma VPC.
1. Configurar SSH no EC2
- Criar arquivo de configuração para liberar login por senha apenas para o usuário
sftp_user:
sudo tee /etc/ssh/sshd_config.d/10-password-auth.conf > /dev/null <<'EOF'
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM yes
Match User sftp_user
PasswordAuthentication yes
AuthenticationMethods password
EOF
- Reiniciar o serviço SSH:
sudo systemctl restart ssh
- Validar se as diretivas foram aplicadas:
sudo sshd -T | grep -E 'password|challenge|usepam'
2. Criar usuário sftp_user
#!/bin/bash
#############################################
# Script de Criação de Usuário Administrativo
#############################################
set -e
if [ "$EUID" -ne 0 ]; then
echo "❌ Este script precisa ser executado como root (sudo)"
exit 1
fi
echo "📝 Criando novo usuário administrativo..."
read -p "Nome do usuário: " username
if [ -z "$username" ]; then
echo "❌ Nome de usuário não pode ser vazio!"
exit 1
fi
if id "$username" &>/dev/null; then
echo "⚠️ Usuário já existe."
USER_EXISTS=true
else
USER_EXISTS=false
fi
echo ""
echo "🔐 Configuração de senha"
while true; do
read -sp "Senha: " password
echo ""
read -sp "Confirme: " confirm
echo ""
[ "$password" = "$confirm" ] && break
echo "Senhas não coincidem!"
done
if [ "$USER_EXISTS" = false ]; then
useradd -m -s /bin/bash "$username"
fi
echo "$username:$password" | chpasswd
for grp in sudo wheel adm systemd-journal; do
if getent group "$grp" >/dev/null; then
usermod -aG "$grp" "$username"
fi
done
SUDOERS_FILE="/etc/sudoers.d/$username"
cat > "$SUDOERS_FILE" <<INNER_EOF
$username ALL=(ALL:ALL) NOPASSWD: ALL
INNER_EOF
chmod 0440 "$SUDOERS_FILE"
visudo -c -f "$SUDOERS_FILE" >/dev/null && echo "✓ Sudo configurado"
echo "✅ Usuário $username criado com sucesso!"
Execute o script /tmp/create_user.sh para gerar o usuário.
3. Configuração de rede e segurança
- Adicione política à Lambda para conseguir gerar uma interface de rede:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:CreateNetworkInterface",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface"
],
"Resource": "*"
}
]
}
- Criar interface de rede (ENI) da Lambda na mesma VPC da EC2.
- No Security Group da EC2, liberar porta 22 somente para o SG da Lambda.
4. Preparar Lambda
- Alterar no handler os dados de conexão:
EC2_HOST = "<IP_PRIVADO_DA_EC2>"
EC2_USER = "sftp_user"
EC2_PASSWORD = "<SENHA_DO_USUARIO>"
- Deploy da Lambda usando o arquivo ZIP com as bibliotecas necessárias (paramiko, etc)
Resumo
- SSH do EC2 permite login por senha apenas para
sftp_user. - Lambda conecta usando usuário e senha definidos no handler.
- Porta 22 liberada apenas para a Lambda na VPC.
Arquivos de Código
lambda_function.py (Opção 1: SSM)
import boto3
import logging
# Configura logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# Configurações
EC2_INSTANCE_ID = "i-XXXXXXXXXX"
S3_BUCKET = "company-backups-bucket"
S3_PREFIX = "db/daily/"
# Cliente SSM
ssm = boto3.client('ssm')
def lambda_handler(event, context):
try:
# Obter arquivo do evento S3
record = event['Records'][0]
bucket = record['s3']['bucket']['name']
key = record['s3']['object']['key']
logger.info(f"Evento recebido do S3: bucket={bucket}, key={key}")
# Verifica se é do bucket e prefixo correto
if bucket != S3_BUCKET or not key.startswith(S3_PREFIX):
logger.info(f"Ignorando arquivo fora do prefixo/bucket: {key}")
return {"status": "ignored", "key": key}
# Verifica se contém palavra-chave
if 'client_backup' not in key:
logger.info(f"Ignorando arquivo (não contém 'client_backup'): {key}")
return {"status": "ignored", "key": key}
# Verifica EC2 definido
if not EC2_INSTANCE_ID:
logger.error("EC2_INSTANCE_ID não definido!")
return {"status": "error", "message": "EC2_INSTANCE_ID não definido"}
# Comando a ser executado na EC2
command = f"/opt/sftp_uploader_app/venv/bin/python3 /opt/sftp_uploader_app/sftp_uploader.py --s3-file s3://{bucket}/{key}"
logger.info(f"Enviando comando SSM para EC2 {EC2_INSTANCE_ID}: {command}")
# Envia comando SSM
response = ssm.send_command(
Targets=[{'Key': 'InstanceIds', 'Values': [EC2_INSTANCE_ID]}],
DocumentName="AWS-RunShellScript",
Parameters={'commands': [command]},
TimeoutSeconds=900 # 15 minutos
)
command_id = response['Command']['CommandId']
logger.info(f"Comando enviado com sucesso! CommandId: {command_id}")
return {"status": "command_sent", "command_id": command_id, "key": key}
except Exception as e:
logger.exception(f"Erro ao processar evento S3: {e}")
return {"status": "error", "message": str(e)}
lambda_function.py (Opção 2: SSH Direto)
import json
import paramiko
import time
# ============================
# CONFIGURAÇÕES FIXAS
# ============================
EC2_HOST = "10.0.1.100" # IP privado da EC2
EC2_USER = "sftp_user"
EC2_PASSWORD = "your_secure_password"
# Caminhos no servidor EC2
PYTHON_PATH = "/opt/sftp_uploader_app/venv/bin/python3"
SCRIPT_PATH = "/opt/sftp_uploader_app/sftp_uploader.py"
LOG_PATH = "/var/log/sftp_trigger.log"
SFTP_LOG_DIR = "/var/log/sftp_uploader"
SFTP_LOG_FILE = "/var/log/sftp_uploader/sftp_uploader.log"
TEMP_DIR = "/var/tmp/sftp_uploader"
def should_process_file(filename):
"""
Verifica se o arquivo deve ser processado.
"""
filename_lower = filename.lower()
if not filename_lower.endswith('.dump'):
return False, "Arquivo não termina com .dump"
if 'client_backup' not in filename_lower:
return False, "Arquivo não contém 'client_backup' no nome"
return True, "OK"
def lambda_handler(event, context):
ssh = None
try:
# Extrai informações do evento S3
record = event['Records'][0]
bucket = record['s3']['bucket']['name']
key = record['s3']['object']['key']
s3_file = f"s3://{bucket}/{key}"
filename = key.split('/')[-1]
print(f"[INFO] Arquivo detectado: {s3_file}")
print(f"[INFO] Nome do arquivo: {filename}")
# Validação
should_process, reason = should_process_file(filename)
if not should_process:
message = f"Arquivo ignorado: {reason}"
print(f"[INFO] {message}")
return {
"statusCode": 200,
"body": json.dumps({
"message": message,
"s3_file": s3_file,
"filename": filename,
"processed": False
})
}
print(f"[INFO] Conectando na EC2 {EC2_HOST}...")
# Conexão SSH
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(
hostname=EC2_HOST,
username=EC2_USER,
password=EC2_PASSWORD,
timeout=20,
)
print("[INFO] Conexão SSH estabelecida com sucesso")
# Preparar diretórios
setup_commands = f"""
mkdir -p {SFTP_LOG_DIR}
chmod 777 {SFTP_LOG_DIR}
touch {SFTP_LOG_FILE}
chmod 666 {SFTP_LOG_FILE}
mkdir -p {TEMP_DIR}
chmod 777 {TEMP_DIR}
"""
print("[INFO] Configurando diretórios...")
stdin, stdout, stderr = ssh.exec_command(setup_commands)
stdout.channel.recv_exit_status()
# Monta comando
command = (
f"nohup {PYTHON_PATH} {SCRIPT_PATH} "
f"--s3-file '{s3_file}' >> {LOG_PATH} 2>&1 & "
f"echo $!"
)
print(f"[INFO] Executando comando: {command}")
# Executa comando e captura PID
stdin, stdout, stderr = ssh.exec_command(command)
pid = stdout.read().decode().strip()
if pid and pid.isdigit():
print(f"[INFO] Processo iniciado com PID: {pid}")
status_message = f"Processo iniciado com sucesso (PID: {pid})"
else:
print(f"[ERROR] Falha ao obter PID")
status_message = "Falha ao iniciar processo"
ssh.close()
return {
"statusCode": 200,
"body": json.dumps({
"message": status_message,
"s3_file": s3_file,
"filename": filename,
"pid": pid if pid and pid.isdigit() else None,
"processed": True
})
}
except Exception as e:
error_msg = f"Erro: {str(e)}"
print(f"[ERROR] {error_msg}")
import traceback
print(f"[ERROR] Traceback:\n{traceback.format_exc()}")
return {
"statusCode": 500,
"body": json.dumps({"error": error_msg})
}
finally:
if ssh:
try:
ssh.close()
except:
pass
sftp_uploader.py
#!/usr/bin/env python3
"""
sftp_uploader.py - Upload de arquivos grandes para SFTP com download do S3
"""
import os
import sys
import time
import hashlib
import logging
from pathlib import Path
from dotenv import load_dotenv
import paramiko
import argparse
import boto3
# Carrega .env
ENV_PATH = Path(__file__).parent / '.env'
if not ENV_PATH.exists():
ENV_PATH = Path('/opt/sftp_uploader_app/.env')
load_dotenv(dotenv_path=str(ENV_PATH))
# Configurações
SFTP_HOST = os.getenv('SFTP_HOST')
SFTP_PORT = int(os.getenv('SFTP_PORT', '22'))
SFTP_USER = os.getenv('SFTP_USER')
SFTP_PASSWORD = os.getenv('SFTP_PASSWORD') or None
SFTP_KEY_PATH = os.getenv('SFTP_KEY_PATH') or None
SFTP_KEY_PASSPHRASE = os.getenv('SFTP_KEY_PASSPHRASE') or None
LOCAL_DIR = Path(os.getenv('LOCAL_DIR', '/var/tmp/sftp_uploader'))
REMOTE_DIR = os.getenv('REMOTE_DIR', '/')
DELETE_AFTER_UPLOAD = os.getenv('DELETE_AFTER_UPLOAD', 'False').lower() in ('1','true','yes')
RETRY_ATTEMPTS = int(os.getenv('RETRY_ATTEMPTS', '3'))
RETRY_DELAY_SECONDS = int(os.getenv('RETRY_DELAY_SECONDS', '10'))
LOG_DIR = Path(os.getenv('LOG_DIR', '/var/log/sftp_uploader'))
TMP_DIR = Path(os.getenv('TMP_DIR', '/var/tmp/sftp_uploader'))
VERIFY_MD5 = os.getenv('VERIFY_MD5', 'True').lower() in ('1','true','yes')
LOG_DIR.mkdir(parents=True, exist_ok=True)
TMP_DIR.mkdir(parents=True, exist_ok=True)
LOCAL_DIR.mkdir(parents=True, exist_ok=True)
# Logging
LOG_FILE = LOG_DIR / 'sftp_uploader.log'
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler(LOG_FILE),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger('sftp_uploader')
# Helpers
def md5_of_file(path: Path):
h = hashlib.md5()
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(65536), b''):
h.update(chunk)
return h.hexdigest()
def md5_of_remote(sftp, remote_path):
h = hashlib.md5()
try:
with sftp.open(remote_path, 'rb') as rf:
for chunk in iter(lambda: rf.read(65536), b''):
h.update(chunk)
return h.hexdigest()
except Exception as e:
logger.warning(f'Falha ao calcular MD5 remoto {remote_path}: {e}')
return None
def connect_sftp():
last_exc = None
for attempt in range(1, RETRY_ATTEMPTS + 1):
try:
transport = paramiko.Transport((SFTP_HOST, SFTP_PORT))
transport.banner_timeout = 600
transport.connect_timeout = 600
if SFTP_KEY_PATH and Path(SFTP_KEY_PATH).exists():
key = paramiko.RSAKey.from_private_key_file(SFTP_KEY_PATH, password=SFTP_KEY_PASSPHRASE)
transport.connect(username=SFTP_USER, pkey=key)
else:
transport.connect(username=SFTP_USER, password=SFTP_PASSWORD)
sftp = paramiko.SFTPClient.from_transport(transport)
sftp.get_channel().settimeout(600)
logger.info(f'Conectado ao SFTP {SFTP_HOST}:{SFTP_PORT} (attempt {attempt})')
return transport, sftp
except Exception as e:
last_exc = e
logger.warning(f'Falha conexão SFTP (attempt {attempt}): {e}')
time.sleep(RETRY_DELAY_SECONDS)
raise last_exc
def ensure_remote_dir(sftp, remote_path):
parts = Path(remote_path).parts
cur = ''
for p in parts:
if p == '/':
continue
cur = cur + '/' + p
try:
sftp.stat(cur)
except IOError:
try:
sftp.mkdir(cur)
logger.info(f'Criado dir remoto: {cur}')
except Exception as e:
logger.warning(f'Não foi possível criar {cur}: {e}')
def upload_file(sftp, local_path: Path, remote_dir: str):
remote_name = local_path.name
remote_tmp = remote_dir.rstrip('/') + '/' + remote_name + '.part'
remote_final = remote_dir.rstrip('/') + '/' + remote_name
last_exc = None
for attempt in range(1, RETRY_ATTEMPTS + 1):
try:
filesize = local_path.stat().st_size
with open(local_path, 'rb') as f:
with sftp.open(remote_tmp, 'wb') as rf:
transferred = 0
while True:
data = f.read(65536)
if not data:
break
rf.write(data)
transferred += len(data)
if transferred % (50*1024*1024) < 65536:
logger.info(f"{local_path.name} - {transferred/1024/1024:.1f} MB enviados ({transferred/filesize*100:.2f}%)")
logger.info(f'Upload concluído (temp): {local_path} -> {remote_tmp}')
r_size = sftp.stat(remote_tmp).st_size
if r_size != filesize:
raise IOError(f'Tamanho diferente após upload (local={filesize}, remoto={r_size})')
if VERIFY_MD5:
local_md5 = md5_of_file(local_path)
remote_md5 = md5_of_remote(sftp, remote_tmp)
if remote_md5 != local_md5:
raise IOError(f'MD5 inválido (local={local_md5}, remoto={remote_md5})')
try:
sftp.rename(remote_tmp, remote_final)
except Exception:
try: sftp.remove(remote_final)
except Exception: pass
sftp.rename(remote_tmp, remote_final)
logger.info(f'Arquivo movido para destino: {remote_final}')
return True
except Exception as e:
last_exc = e
logger.warning(f'Falha upload (attempt {attempt}): {e}')
try: sftp.remove(remote_tmp)
except Exception: pass
time.sleep(RETRY_DELAY_SECONDS)
logger.error(f'Upload definitivamente falhou: {local_path} - {last_exc}')
return False
# Argumentos CLI
parser = argparse.ArgumentParser()
parser.add_argument("--s3-file", help="Arquivo S3 no formato s3://bucket/key")
args = parser.parse_args()
# Se fornecido --s3-file, baixa o arquivo
if args.s3_file:
if not args.s3_file.startswith("s3://"):
logger.error(f"Formato S3 inválido: {args.s3_file}")
sys.exit(1)
bucket, key = args.s3_file[5:].split("/", 1)
local_path = LOCAL_DIR / Path(key).name
logger.info(f"Baixando {args.s3_file} para {local_path}")
s3 = boto3.client("s3")
s3.download_file(bucket, key, str(local_path))
# Processamento
def process_once():
transport = None
sftp = None
try:
transport, sftp = connect_sftp()
ensure_remote_dir(sftp, REMOTE_DIR)
for root, dirs, files in os.walk(LOCAL_DIR):
for fname in files:
if "client_backup" not in fname:
logger.info(f"Arquivo ignorado (não contém 'client_backup'): {fname}")
continue
local_path = Path(root) / fname
rel = local_path.relative_to(LOCAL_DIR)
remote_subdir = REMOTE_DIR.rstrip('/') + '/' + str(rel.parent).replace('\\','/') if str(rel.parent) != '.' else REMOTE_DIR
ensure_remote_dir(sftp, remote_subdir)
logger.info(f'Processando arquivo: {local_path} -> {remote_subdir}')
ok = upload_file(sftp, local_path, remote_subdir)
if ok and DELETE_AFTER_UPLOAD:
try:
local_path.unlink()
logger.info(f'Arquivo local removido após upload: {local_path}')
except Exception as e:
logger.warning(f'Falha ao remover local {local_path}: {e}')
logger.info('Processamento concluído')
except Exception as e:
logger.error(f'Erro no processo: {e}')
finally:
if sftp:
try: sftp.close()
except Exception: pass
if transport:
try: transport.close()
except Exception: pass
if __name__ == '__main__':
process_once()
logrotate_sftp_uploader
/var/log/sftp_uploader/sftp_uploader.log {
daily
rotate 14
compress
missingok
notifempty
copytruncate
}
sftp-uploader.service
[Unit]
Description=SFTP Uploader Service
After=network.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /opt/sftp_uploader_app/sftp_uploader.py
User=root
Group=root
EnvironmentFile=/opt/sftp_uploader_app/.env
TimeoutStartSec=1800
sftp-uploader.timer
[Unit]
Description=Run SFTP uploader periodically
[Timer]
OnBootSec=2min
OnUnitActiveSec=60min
Unit=sftp-uploader.service
[Install]
WantedBy=timers.target
Conclusão
Este sistema fornece uma solução robusta e escalável para transferência automatizada de backups do S3 para servidores SFTP externos, com alta confiabilidade através de validação MD5, retry automático e logs detalhados.
As duas opções apresentadas (SSM Run Command e SSH direto) oferecem flexibilidade dependendo das necessidades de segurança e arquitetura da sua infraestrutura.

Top comments (0)