Эфемерные окружения
Updated Jul 2026
Что такое эфемерные окружения
Эфемерные окружения — это одноразовые копии всего вашего инфраструктурного стека, создаваемые для каждого пулл-реквеста и уничтожаемые после мержа. Вместо того чтобы все разработчики разделяли один staging (с его неизбежными моментами «кто сломал staging?»), каждый PR получает собственный изолированный мир.
Разработчик отправляет PR. CI создаёт полное окружение — VPC, базу данных, очереди, контейнеры приложения, API Gateway. Тесты запускаются на этой реальной инфраструктуре по уникальному URL для предпросмотра. Комментарий к PR показывает результаты тестов и ссылку на предпросмотр. Когда PR мержится или закрывается, всё уничтожается.
Архитектура
PR #142 opened
|
+-- Terraform workspace: pr-142
| +-- VPC (изолированный)
| +-- RDS (маленький инстанс, заполненный тестовыми данными)
| +-- ECS service (контейнеры приложения)
| +-- S3 buckets (с префиксом pr-142-)
| +-- API Gateway (pr-142.preview.example.com)
|
+-- Запуск полного набора тестов на pr-142.preview.example.com
| +-- Модульные тесты (уже пройдены в CI)
| +-- Интеграционные тесты (реальная БД, реальные очереди)
| +-- E2E-тесты в браузере (Playwright на URL предпросмотра)
| +-- Сканирование безопасности (DAST на живых эндпоинтах)
|
+-- Комментарий к PR с результатами тестов + URL предпросмотра
|
PR #142 merged
|
+-- terraform workspace select pr-142 && terraform destroy -auto-approve
Terraform Workspaces для эфемерных окружений
Terraform workspaces обеспечивают лёгкую изоляцию. Каждый workspace поддерживает собственный файл состояния, поэтому ресурсы, созданные в одном workspace, не влияют на другой.
Конфигурация с учётом workspace
# main.tf -- workspace-aware naming
locals {
env_name = terraform.workspace == "default" ? "production" : terraform.workspace
prefix = "myapp-${local.env_name}"
# Use smaller instances for PR environments
is_ephemeral = terraform.workspace != "default"
}
resource "aws_s3_bucket" "data" {
bucket = "${local.prefix}-data"
# Each PR gets its own bucket: myapp-pr-142-data
}
resource "aws_db_instance" "main" {
identifier = "${local.prefix}-db"
instance_class = local.is_ephemeral ? "db.t4g.micro" : "db.r6g.xlarge"
allocated_storage = local.is_ephemeral ? 20 : 500
# PR environments use minimal instance sizes to control cost
# Skip final snapshot for ephemeral environments
skip_final_snapshot = local.is_ephemeral
# Use a smaller backup retention for ephemeral
backup_retention_period = local.is_ephemeral ? 0 : 7
}
resource "aws_ecs_service" "app" {
name = "${local.prefix}-app"
desired_count = local.is_ephemeral ? 1 : 3
# PR environments need only one replica
}
# DNS entry for the preview URL
resource "aws_route53_record" "preview" {
count = local.is_ephemeral ? 1 : 0
zone_id = data.aws_route53_zone.preview.zone_id
name = "${local.env_name}.preview.example.com"
type = "CNAME"
records = [aws_lb.app.dns_name]
ttl = 60
}
output "preview_url" {
value = local.is_ephemeral ? "https://${local.env_name}.preview.example.com" : ""
}
CI-пайплайн для эфемерных окружений
#!/bin/bash
# scripts/ephemeral-env.sh
set -euo pipefail
ACTION="$1" # "create" or "destroy"
PR_NUM=$(echo "$GITHUB_REF" | grep -oP '\d+')
WORKSPACE="pr-${PR_NUM}"
case "$ACTION" in
create)
cd terraform/
terraform workspace new "$WORKSPACE" 2>/dev/null || \
terraform workspace select "$WORKSPACE"
terraform apply -auto-approve \
-var="pr_number=${PR_NUM}" \
-var="git_sha=${GITHUB_SHA:0:8}"
PREVIEW_URL=$(terraform output -raw preview_url)
echo "Preview environment ready: $PREVIEW_URL"
# Wait for health check
for i in $(seq 1 30); do
if curl -sf "$PREVIEW_URL/healthz" > /dev/null; then
echo "Environment is healthy"
break
fi
echo "Waiting for environment to be ready... ($i/30)"
sleep 10
done
# Run tests
cd ..
npx playwright test --base-url "$PREVIEW_URL"
TEST_RESULT=$?
# Post results to PR
if [ $TEST_RESULT -eq 0 ]; then
gh pr comment "$PR_NUM" --body \
"Preview: $PREVIEW_URL | Tests: PASSED"
else
gh pr comment "$PR_NUM" --body \
"Preview: $PREVIEW_URL | Tests: FAILED (see CI logs)"
fi
;;
destroy)
cd terraform/
terraform workspace select "$WORKSPACE"
terraform destroy -auto-approve
terraform workspace select default
terraform workspace delete "$WORKSPACE"
echo "Environment pr-${PR_NUM} destroyed"
;;
esac
Pulumi Stacks для эфемерных окружений
Pulumi stacks — это аналог Terraform workspaces, но с программным управлением:
// index.ts -- Pulumi stack per PR
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const stack = pulumi.getStack(); // e.g., "pr-142"
const isEphemeral = stack.startsWith("pr-");
const db = new aws.rds.Instance("main", {
instanceClass: isEphemeral ? "db.t4g.micro" : "db.r6g.xlarge",
allocatedStorage: isEphemeral ? 20 : 500,
skipFinalSnapshot: isEphemeral, // No snapshot needed for PR envs
identifier: `myapp-${stack}-db`,
engine: "postgres",
engineVersion: "16",
});
const service = new aws.ecs.Service("app", {
desiredCount: isEphemeral ? 1 : 3,
// ... other configuration
});
export const dbEndpoint = db.endpoint;
export const previewUrl = isEphemeral
? pulumi.interpolate`https://${stack}.preview.example.com`
: undefined;
Стратегии контроля затрат
Эфемерные окружения могут быть дорогими без надлежащего управления. Одно PR-окружение с RDS-инстансом, ECS-сервисом и балансировщиком нагрузки обходится примерно в $5-15 в день. При 20 активных PR это $100-300 в день.
| Стратегия | Реализация | Экономия |
|---|---|---|
| Автоматическое уничтожение через N часов | Cron-задача GitHub Action + terraform destroy |
60-80% |
| Минимальные размеры инстансов | Условный размер на основе имени workspace | 70-90% |
| Общие ресурсы только для чтения | Ссылка на продакшен VPC, DNS-зону через data sources | 20-30% |
| Spot-инстансы для вычислений | capacity_type = "SPOT" для узлов EKS |
60-70% |
| Масштабирование до нуля по расписанию | Lambda, масштабирующая PR-окружения вне рабочих часов | 50-60% |
| TTL-теги на всех ресурсах | Автоматическая очистка ресурсов старше 48 часов | Предотвращает зомби-ресурсы |
Cron-задача автоматического уничтожения
# .github/workflows/cleanup-ephemeral.yml
name: Cleanup Stale Ephemeral Environments
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM UTC
workflow_dispatch:
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Find stale environments
run: |
cd terraform/
terraform workspace list | grep "pr-" | while read ws; do
WS_NAME=$(echo "$ws" | tr -d ' *')
PR_NUM=$(echo "$WS_NAME" | grep -oP '\d+')
# Check if the PR is still open
PR_STATE=$(gh pr view "$PR_NUM" --json state -q '.state' 2>/dev/null || echo "UNKNOWN")
if [ "$PR_STATE" != "OPEN" ]; then
echo "Destroying stale environment: $WS_NAME (PR state: $PR_STATE)"
terraform workspace select "$WS_NAME"
terraform destroy -auto-approve
terraform workspace select default
terraform workspace delete "$WS_NAME"
fi
done
TTL-теги для ресурсов
# Add TTL tags to all ephemeral resources
locals {
common_tags = merge(
{
Team = "platform"
Environment = local.env_name
ManagedBy = "terraform"
},
local.is_ephemeral ? {
EphemeralTTL = timeadd(timestamp(), "48h")
PRNumber = var.pr_number
} : {}
)
}
Заполнение базы данных для эфемерных окружений
Эфемерные окружения нуждаются в данных, чтобы быть полезными. Варианты от самого быстрого до самого реалистичного:
| Подход | Скорость | Реалистичность | Лучше всего для |
|---|---|---|---|
| Только пустая схема | Секунды | Низкая | API-тестирование с созданием тестовых данных |
| Фикстурные данные (SQL-скрипты) | Секунды | Средняя | Предсказуемые тестовые сценарии |
| Анонимизированный снапшот продакшена | Минуты | Высокая | Реалистичное тестирование, демо-окружения |
| Реплика продакшена для чтения | Быстро (без копирования) | Наивысшая | Тестирование только для чтения на реальных данных |
# Seed ephemeral database from fixture files
psql "$DATABASE_URL" < fixtures/schema.sql
psql "$DATABASE_URL" < fixtures/test-data.sql
Эфемерные окружения трансформируют уверенность в тестировании, поскольку устраняют проблему «работает на staging». Когда каждый PR получает собственную инфраструктуру, вы точно знаете, что изменилось и работает ли это.