14. Implantação e DevOps
Este documento detalha estratégias e práticas para implantação, integração contínua e operações (DevOps) de soluções web desenvolvidas com Astro 5.
Estratégias de Implantação
Implantação Estática
Astro é otimizado para gerar sites estáticos, que podem ser implantados em qualquer serviço de hospedagem estática.
Configuração para Build Estático
import { defineConfig } from 'astro/config';
export default defineConfig({ output: 'static', // Modo padrão site: 'https://meusite.com.br', build: { // Opções de build format: 'directory', // Gera URLs mais limpas (sem .html) assets: 'assets', // Pasta para assets com hash },});Serviços de Hospedagem Recomendados
- Netlify
[build] command = "npm run build" publish = "dist"
[[redirects]] from = "/*" to = "/index.html" status = 200- Vercel
{ "buildCommand": "npm run build", "outputDirectory": "dist", "framework": "astro", "routes": [ { "handle": "filesystem" }, { "src": "/(.*)", "dest": "/index.html" } ]}- Cloudflare Pages
[build] command = "npm run build" publish = "dist"Implantação SSR (Server-Side Rendering)
Para sites que necessitam de renderização no servidor:
Configuração para SSR
import { defineConfig } from 'astro/config';import node from '@astrojs/node';
export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone', // ou 'middleware' }),});Serviços de Hospedagem para SSR
- Render
services: - type: web name: meu-site-astro env: node buildCommand: npm install && npm run build startCommand: node dist/server/entry.mjs envVars: - key: NODE_VERSION value: 18 - key: HOST value: 0.0.0.0 - key: PORT fromService: type: web name: meu-site-astro envVarKey: PORT- Railway
{ "build": { "builder": "NIXPACKS", "buildCommand": "npm run build" }, "deploy": { "startCommand": "node dist/server/entry.mjs", "healthcheckPath": "/", "healthcheckTimeout": 60, "restartPolicyType": "ON_FAILURE" }}- Fly.io
app = "meu-site-astro"primary_region = "gru"
[build] dockerfile = "Dockerfile"
[http_service] internal_port = 3000 force_https = true auto_stop_machines = true auto_start_machines = true min_machines_running = 0Dockerfile para Fly.io:
# DockerfileFROM node:18-alpine as build
WORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
FROM node:18-alpine as runtime
WORKDIR /appCOPY --from=build /app/dist /app/distCOPY --from=build /app/node_modules /app/node_modulesCOPY package.json .
ENV HOST=0.0.0.0ENV PORT=3000EXPOSE 3000
CMD ["node", "dist/server/entry.mjs"]Implantação Híbrida (Ilhas)
Astro permite uma abordagem híbrida, onde a maior parte do site é estática, mas componentes específicos são hidratados no cliente.
import { defineConfig } from 'astro/config';import react from '@astrojs/react';
export default defineConfig({ output: 'static', integrations: [ react(), // Permite usar componentes React ],});Exemplo de componente com hidratação:
---import { Counter } from '../components/react/Counter';---
<!-- O componente será hidratado apenas quando visível --><Counter client:visible initialCount={0} />Integração Contínua e Entrega Contínua (CI/CD)
GitHub Actions
name: Deploy
on: push: branches: [main] pull_request: branches: [main]
jobs: build: runs-on: ubuntu-latest
steps: - uses: actions/checkout@v3
- name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'npm'
- name: Install dependencies run: npm ci
- name: Lint run: npm run lint
- name: Type check run: npm run typecheck
- name: Test run: npm run test
- name: Build run: npm run build
- name: Upload build artifacts uses: actions/upload-artifact@v3 with: name: build-output path: dist/
deploy: needs: build if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest
steps: - name: Download build artifacts uses: actions/download-artifact@v3 with: name: build-output path: dist/
- name: Deploy to Netlify uses: nwtgck/actions-netlify@v2 with: publish-dir: './dist' production-branch: main github-token: ${{ secrets.GITHUB_TOKEN }} deploy-message: "Deploy from GitHub Actions" env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}GitLab CI/CD
stages: - test - build - deploy
variables: NODE_VERSION: "18"
cache: key: ${CI_COMMIT_REF_SLUG} paths: - node_modules/
test: stage: test image: node:${NODE_VERSION} script: - npm ci - npm run lint - npm run typecheck - npm run test
build: stage: build image: node:${NODE_VERSION} script: - npm ci - npm run build artifacts: paths: - dist/
deploy:production: stage: deploy image: node:${NODE_VERSION} script: - npm install -g netlify-cli - netlify deploy --site $NETLIFY_SITE_ID --auth $NETLIFY_AUTH_TOKEN --prod --dir=dist only: - main environment: name: production url: https://meusite.com.brConfiguração de Ambientes
Variáveis de Ambiente
Astro suporta variáveis de ambiente através de arquivos .env:
# .env (não deve ser versionado)DATABASE_URL=postgres://user:password@localhost:5432/mydbAPI_KEY=sua_chave_secreta
# .env.example (versionado como referência)DATABASE_URL=postgres://user:password@localhost:5432/mydbAPI_KEY=your_api_keyAcesso às variáveis de ambiente:
---// Variáveis públicas (prefixo PUBLIC_)const apiUrl = import.meta.env.PUBLIC_API_URL;
// Variáveis privadas (apenas no servidor)const apiKey = import.meta.env.API_KEY;---
<div> <!-- Apenas variáveis públicas podem ser usadas no cliente --> <p>API URL: {import.meta.env.PUBLIC_API_URL}</p></div>Configuração por Ambiente
import { defineConfig } from 'astro/config';
const isProd = process.env.NODE_ENV === 'production';
export default defineConfig({ site: isProd ? 'https://meusite.com.br' : 'http://localhost:3000', output: 'static', build: { // Opções específicas para produção minify: isProd, sourcemap: !isProd, },});Monitoramento e Logging
Monitoramento de Erros
Integração com Sentry:
import * as Sentry from '@sentry/browser';
export function initSentry() { if (import.meta.env.PROD && import.meta.env.PUBLIC_SENTRY_DSN) { Sentry.init({ dsn: import.meta.env.PUBLIC_SENTRY_DSN, environment: import.meta.env.PUBLIC_ENVIRONMENT || 'production', release: import.meta.env.PUBLIC_VERSION || '1.0.0', tracesSampleRate: 0.2, }); }}
export function captureException(error, context = {}) { if (import.meta.env.PROD) { Sentry.captureException(error, { extra: context }); } else { console.error('Error:', error, 'Context:', context); }}Uso no layout principal:
<html lang="pt-BR"><head> <!-- ... outros metadados ... -->
<!-- Inicialização do Sentry --> <script> import { initSentry } from '../utils/sentry'; initSentry(); </script></head><body> <slot /></body></html>Logging
Implementação de um sistema de logging:
const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3,};
// Determina o nível mínimo de log com base no ambienteconst MIN_LOG_LEVEL = import.meta.env.PROD ? LOG_LEVELS.INFO : LOG_LEVELS.DEBUG;
// Função para enviar logs para um serviço externofunction sendToLogService(level, message, data) { if (import.meta.env.PROD && import.meta.env.LOG_SERVICE_URL) { try { fetch(import.meta.env.LOG_SERVICE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ level, message, data, timestamp: new Date().toISOString(), app: import.meta.env.PUBLIC_APP_NAME || 'astro-app', environment: import.meta.env.PUBLIC_ENVIRONMENT || 'production', }), }); } catch (error) { console.error('Failed to send log to service:', error); } }}
export const logger = { debug(message, data = {}) { if (MIN_LOG_LEVEL <= LOG_LEVELS.DEBUG) { console.debug(`[DEBUG] ${message}`, data); } },
info(message, data = {}) { if (MIN_LOG_LEVEL <= LOG_LEVELS.INFO) { console.info(`[INFO] ${message}`, data); sendToLogService('info', message, data); } },
warn(message, data = {}) { if (MIN_LOG_LEVEL <= LOG_LEVELS.WARN) { console.warn(`[WARN] ${message}`, data); sendToLogService('warn', message, data); } },
error(message, error = null, data = {}) { if (MIN_LOG_LEVEL <= LOG_LEVELS.ERROR) { console.error(`[ERROR] ${message}`, error, data);
// Capturar erro no Sentry if (error instanceof Error) { import('./sentry.js').then(({ captureException }) => { captureException(error, { message, ...data }); }); }
sendToLogService('error', message, { ...data, errorMessage: error?.message, errorStack: error?.stack, }); } },};Uso do logger:
import { logger } from '../utils/logger';
export async function fetchData(endpoint) { try { logger.info(`Fetching data from ${endpoint}`);
const response = await fetch(endpoint);
if (!response.ok) { throw new Error(`HTTP error ${response.status}`); }
const data = await response.json();
logger.debug('Data fetched successfully', { endpoint, itemCount: data.length });
return data; } catch (error) { logger.error(`Failed to fetch data from ${endpoint}`, error); throw error; }}Segurança
Headers de Segurança
Configuração de headers de segurança no Netlify:
[[headers]] for = "/*" [headers.values] X-Content-Type-Options = "nosniff" X-Frame-Options = "DENY" X-XSS-Protection = "1; mode=block" Referrer-Policy = "strict-origin-when-cross-origin" Permissions-Policy = "camera=(), microphone=(), geolocation=()" Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' https://analytics.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://images.example.com; font-src 'self'; connect-src 'self' https://api.example.com;"Middleware de Segurança (SSR)
export function onRequest({ request, redirect, locals }, next) { // Verificar origem para proteção CSRF const origin = request.headers.get('Origin'); const allowedOrigins = [ 'https://meusite.com.br', 'https://www.meusite.com.br', ];
if (request.method !== 'GET' && origin && !allowedOrigins.includes(origin)) { return new Response('Forbidden', { status: 403 }); }
// Verificar autenticação para rotas protegidas if (request.url.includes('/admin') && !locals.user) { return redirect('/login'); }
// Sanitizar parâmetros de entrada const url = new URL(request.url); for (const [key, value] of url.searchParams.entries()) { // Exemplo simples de sanitização if (value.includes('<script>')) { return new Response('Bad Request', { status: 400 }); } }
// Continuar para o próximo middleware ou rota return next();}Escalabilidade
CDN e Edge Functions
Configuração para Cloudflare Pages com Functions:
import { defineConfig } from 'astro/config';import cloudflare from '@astrojs/cloudflare';
export default defineConfig({ output: 'server', adapter: cloudflare({ mode: 'directory', functionPerRoute: true, // Cria uma função por rota }),});Exemplo de Edge Function:
export async function GET({ request, cf }) { // cf contém informações do Cloudflare const country = cf?.country || 'Unknown'; const city = cf?.city || 'Unknown';
return new Response(JSON.stringify({ country, city, timestamp: new Date().toISOString(), }), { status: 200, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=60', }, });}Cache Distribuído
Estratégia de cache com Cloudflare Workers:
export async function onRequest({ request, env, params, waitUntil }) { const url = new URL(request.url); const cacheKey = new Request(url.toString(), request); const cache = caches.default;
// Verificar se a resposta está em cache let response = await cache.match(cacheKey);
if (!response) { // Se não estiver em cache, buscar da origem const apiUrl = `${env.API_BASE_URL}/${params.path}`;
response = await fetch(apiUrl, { headers: request.headers, method: request.method, body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.clone().arrayBuffer() : undefined, });
// Clonar a resposta antes de modificá-la response = new Response(response.body, response);
// Adicionar headers de cache response.headers.set('Cache-Control', 'public, max-age=300');
// Armazenar em cache (apenas para GET) if (request.method === 'GET') { waitUntil(cache.put(cacheKey, response.clone())); } }
return response;}Automação de Tarefas
Scripts NPM
Configuração de scripts úteis no package.json:
{ "scripts": { "dev": "astro dev", "build": "astro build", "preview": "astro preview", "lint": "eslint . --ext .js,.astro,.ts,.tsx", "lint:fix": "eslint . --ext .js,.astro,.ts,.tsx --fix", "format": "prettier --write .", "typecheck": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", "test:e2e": "playwright test", "prepare": "husky install", "analyze": "astro build --analyze", "clean": "rimraf dist .astro node_modules", "update-deps": "npm update", "check-deps": "npm outdated", "postinstall": "node scripts/postinstall.js" }}Husky e Lint-Staged
Configuração para verificações automáticas antes de commits:
{ "devDependencies": { "husky": "^8.0.3", "lint-staged": "^13.2.3" }, "lint-staged": { "*.{js,ts,jsx,tsx,astro}": [ "eslint --fix", "prettier --write" ], "*.{json,md,css,scss}": [ "prettier --write" ] }}Configuração do Husky:
#!/bin/sh. "$(dirname "$0")/_/husky.sh"
npx lint-staged
# .husky/pre-push#!/bin/sh. "$(dirname "$0")/_/husky.sh"
npm run typechecknpm run testEstratégias de Backup e Recuperação
Backup de Conteúdo
Script para backup de conteúdo em CMS headless:
import fs from 'fs/promises';import path from 'path';import { fetchAllContent } from '../src/api/cms';
async function backupContent() { try { // Criar diretório de backup const backupDir = path.join(process.cwd(), 'backups'); await fs.mkdir(backupDir, { recursive: true });
// Timestamp para o nome do arquivo const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupFile = path.join(backupDir, `content-backup-${timestamp}.json`);
// Buscar todo o conteúdo console.log('Fetching content from CMS...'); const content = await fetchAllContent();
// Salvar em arquivo await fs.writeFile(backupFile, JSON.stringify(content, null, 2));
console.log(`Backup saved to ${backupFile}`); return backupFile; } catch (error) { console.error('Backup failed:', error); process.exit(1); }}
backupContent();Agendamento com GitHub Actions:
name: Content Backup
on: schedule: - cron: '0 0 * * *' # Diariamente à meia-noite workflow_dispatch: # Permite execução manual
jobs: backup: runs-on: ubuntu-latest
steps: - uses: actions/checkout@v3
- name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'npm'
- name: Install dependencies run: npm ci
- name: Run backup run: node scripts/backup-content.js env: CMS_API_KEY: ${{ secrets.CMS_API_KEY }}
- name: Upload backup artifact uses: actions/upload-artifact@v3 with: name: content-backup path: backups/ retention-days: 30Conclusão
A implementação de uma estratégia abrangente de implantação e DevOps é essencial para o sucesso de soluções web desenvolvidas com Astro 5. Ao seguir as práticas recomendadas neste documento, você estará criando um fluxo de trabalho eficiente e confiável para desenvolver, testar, implantar e monitorar suas aplicações.
Lembre-se de adaptar estas práticas às necessidades específicas do seu projeto, considerando fatores como tamanho da equipe, complexidade da aplicação e requisitos de negócio.
Os próximos documentos detalharão outros aspectos do desenvolvimento:
- Internacionalização: Suporte a múltiplos idiomas e regiões
- Análise de Dados: Estratégias para coletar e analisar dados de usuários
Última atualização: 21 de março de 2025