Pular para o conteúdo

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

astro.config.mjs
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

  1. Netlify
netlify.toml
[build]
command = "npm run build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
  1. Vercel
vercel.json
{
"buildCommand": "npm run build",
"outputDirectory": "dist",
"framework": "astro",
"routes": [
{ "handle": "filesystem" },
{ "src": "/(.*)", "dest": "/index.html" }
]
}
  1. Cloudflare Pages
.cfpages.toml
[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

astro.config.mjs
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

  1. Render
render.yaml
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
  1. Railway
railway.json
{
"build": {
"builder": "NIXPACKS",
"buildCommand": "npm run build"
},
"deploy": {
"startCommand": "node dist/server/entry.mjs",
"healthcheckPath": "/",
"healthcheckTimeout": 60,
"restartPolicyType": "ON_FAILURE"
}
}
  1. Fly.io
fly.toml
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 = 0

Dockerfile para Fly.io:

# Dockerfile
FROM node:18-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine as runtime
WORKDIR /app
COPY --from=build /app/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules
COPY package.json .
ENV HOST=0.0.0.0
ENV PORT=3000
EXPOSE 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.

astro.config.mjs
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:

src/components/Counter.astro
---
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

.github/workflows/deploy.yml
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

.gitlab-ci.yml
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.br

Configuraçã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/mydb
API_KEY=sua_chave_secreta
# .env.example (versionado como referência)
DATABASE_URL=postgres://user:password@localhost:5432/mydb
API_KEY=your_api_key

Acesso à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

astro.config.mjs
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:

src/utils/sentry.js
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:

src/layouts/BaseLayout.astro
<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:

src/utils/logger.js
const LOG_LEVELS = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
};
// Determina o nível mínimo de log com base no ambiente
const MIN_LOG_LEVEL = import.meta.env.PROD
? LOG_LEVELS.INFO
: LOG_LEVELS.DEBUG;
// Função para enviar logs para um serviço externo
function 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:

src/api/data.js
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:

netlify.toml
[[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)

src/middleware.js
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:

astro.config.mjs
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:

src/pages/api/geo.js
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:

functions/api/[...path].js
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:

package.json
{
"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:

.husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
# .husky/pre-push
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run typecheck
npm run test

Estratégias de Backup e Recuperação

Backup de Conteúdo

Script para backup de conteúdo em CMS headless:

scripts/backup-content.js
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:

.github/workflows/backup.yml
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: 30

Conclusã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:


Última atualização: 21 de março de 2025