Deploy cloud infrastructure from a YAML file. archer is an open-source CLI for provisioning AWS, Azure, and GCP resources without writing Terraform HCL, Pulumi programs, or CloudFormation templates. Define your entire stack declaratively in one YAML file and let archer handle the rest.
| Cloud | Status | Resources |
|---|---|---|
| AWS | ✅ Full support | VPC, Subnets, IGW, NAT GW, TGW, VPC Endpoints, Security Groups, EC2, ASG, EKS, ECS Fargate, RDS, ElastiCache (Redis/Memcached), S3, EFS, ALB, NLB, Route53, ACM, IAM, KMS, Secrets Manager, CloudWatch |
| Azure | 🚧 Basic stub | Resource Group, VNet, Subnets, Linux VMs |
| GCP | 🚧 Basic stub | VPC Network, Subnetworks, Compute Instances |
Azure and GCP will be expanded in future releases.
- Why archer?
- How it works
- Prerequisites
- Installation
- Quick start
- CLI reference
- YAML schema
- Examples
- Project structure
- Architecture decisions
- Extending archer
- Development
- Contributing
Most infrastructure-as-code tools make you write code. Terraform requires HCL, AWS CDK requires Python or TypeScript, CloudFormation requires verbose JSON/YAML with provider-specific syntax, and raw Pulumi requires a full program. archer flips this: you write a single, plain YAML file and archer generates and runs the Pulumi resources automatically.
| Tool | What you write | Language |
|---|---|---|
| archer | One plain YAML file | None |
| Terraform | .tf files in HCL |
HCL |
| AWS CDK | Full program | Python / TS / Java |
| Pulumi | Full program | Python / TS / Go / .NET |
| CloudFormation | Verbose provider YAML | CloudFormation DSL |
| Ansible | Playbooks + roles | YAML + Jinja2 |
archer is the right tool when you want:
- No IaC language to learn — just YAML that reads like a config file
- Multi-cloud from one file — switch
provider: awstoprovider: gcpand redeploy - Fast iteration —
archer previewshows a diff in seconds, no plan files, no state locks - Secrets-safe by design — passwords are referenced by env-var name, never stored in YAML
archer wraps the Pulumi Automation API.
You describe your infrastructure in YAML. archer translates it into Pulumi resources and runs
up, preview, destroy, or refresh — no Pulumi programs to write, no Pulumi.yaml to manage.
infrastructure.yaml → archer preview → Pulumi Automation API → AWS / Azure / GCP
| Property | Detail |
|---|---|
| State backend | Local filesystem (default) or Pulumi Cloud |
| Credential source | Standard SDK env-var / credential-file chain |
| Config validation | Pydantic v2 with cross-field CIDR checks |
| Logging | loguru, verbosity controlled by --verbose |
| Presentation | rich (tables, spinners, panels) — CLI layer only |
# providers/__init__.py
PROVIDER_REGISTRY: dict[str, type[BaseProvider]] = {
"aws": AWSProvider,
"azure": AzureProvider,
"gcp": GCPProvider,
}Adding a new provider is a single line here. The engine does
PROVIDER_REGISTRY[config.provider] and calls build_resources() +
get_outputs() — that's the entire interface contract.
Every builder checks if not resources.<field>: return empty_result() as its
first line. Resources you don't declare in YAML are never created. A minimal config
with only a VPC and subnets will not touch NAT Gateways, EC2, RDS, or anything else.
Cross-field checks (e.g. "does subnet CIDR fit inside the VPC CIDR?") use
@model_validator(mode="after") which runs after all fields are parsed:
@model_validator(mode="after")
def validate_subnet_cidrs_fit_vpc(self) -> "InfrastructureConfig":
vpc_network = ipaddress.ip_network(self.resources.vpc.cidr_block)
for subnet in self.resources.subnets:
subnet_network = ipaddress.ip_network(subnet.cidr_block)
if not subnet_network.subnet_of(vpc_network):
raise ValueError(f"Subnet {subnet.name} CIDR {subnet.cidr_block} is outside VPC {vpc_network}")
return selfYAML files are typically committed to version control. Passwords and tokens are referenced by env-var name only:
rds:
- password_env_var: DB_PASSWORD # ← env var name, not the valuearcher reads os.environ["DB_PASSWORD"] at deploy time.
Each builder receives only the outputs it depends on:
KMS → IAM → VPC → Subnets → SecurityGroups
→ NatGateway → TransitGateway → VpcEndpoints
→ S3 → EFS → ALB/NLB → Route53 → ACM
→ EC2 → ASG → EKS → ECS → RDS
| Tool | Version | Purpose |
|---|---|---|
| Python | ≥ 3.11 | Runtime |
| uv | latest | Dependency management + virtual env |
| Pulumi CLI | ≥ 3.110 | Required by the Automation API |
| AWS credentials | — | AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (or ~/.aws/credentials) — required only when deploying to AWS |
Install Pulumi:
# macOS / Linux
curl -fsSL https://get.pulumi.com | sh
# Windows (winget)
winget install pulumi
# Homebrew
brew install pulumi/tap/pulumi# 1. Clone the repository
git clone https://github.com/your-org/archer.git
cd archer
# 2. Create a virtual environment and install dependencies with uv
uv venv
uv pip install -e ".[dev]"
# 3. Verify the CLI is available
archer --version# AWS
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# RDS password (never put this in the YAML)
export DB_PASSWORD="MyStr0ng!Password"archer preview --config infrastructure.yamlarcher up --config infrastructure.yamlarcher prints all Pulumi stack outputs (VPC ID, EC2 IPs, RDS endpoint, etc.)
in a table after every successful up.
archer destroy --config infrastructure.yaml
# add --yes to skip the confirmation prompt
archer destroy --config infrastructure.yaml --yesUsage: archer [OPTIONS] COMMAND [ARGS]...
archer — Infrastructure-as-Code wrapper around Pulumi.
Options:
--version Show version and exit.
-v, --verbose Enable DEBUG-level log output.
--help Show this message and exit.
Commands:
up Deploy or update infrastructure (pulumi up).
preview Preview infrastructure changes without deploying (pulumi preview).
destroy Destroy all infrastructure resources (pulumi destroy).
refresh Reconcile local stack state with the real cloud state (pulumi refresh).
validate Validate the configuration file without connecting to any cloud API.
output Print the current stack outputs without running any operation.
Each command accepts --config / -c PATH (default: infrastructure.yaml) and --stack / -s NAME (overrides the stack name):
archer up --config infra/production.yaml
archer preview -c infra/staging.yaml -v
archer destroy --config infra/dev.yaml --yes
archer refresh -c infra/dev.yaml
archer validate -c infra/prod.yaml
archer output -c infra/prod.yaml --stack prod
# same YAML, three stacks:
archer up -c infra/app.yaml --stack dev
archer up -c infra/app.yaml --stack staging
archer up -c infra/app.yaml --stack prod# ── Top-level ──────────────────────────────────────────────────────────────
project: <string> # Pulumi project name
stack: <string> # Stack name (default: dev) ← override with --stack
provider: aws | azure | gcp
region: <string> # Provider-specific region/location
# Global tags — applied to every resource; resource-level tags override these
tags:
team: platform
env: prod
cost-center: eng-42
# ── Backend (optional) ─────────────────────────────────────────────────────
backend:
type: local | cloud # default: local
path: .archer-state # only for type: local
url: <string> # only for type: cloud (omit to use Pulumi Cloud)
# ── AWS resources ──────────────────────────────────────────────────────────
resources:
vpc:
name: <string>
cidr_block: <CIDR> # e.g. 10.0.0.0/16
enable_dns_hostnames: true
enable_dns_support: true
subnets:
- name: <string>
cidr_block: <CIDR> # must be contained within vpc.cidr_block
availability_zone: <string> # e.g. us-east-1a
type: public | private
# Named security groups — reference from ec2/rds/alb/elasticache via security_group_refs
security_groups:
- name: app-sg
description: "App tier"
ingress_rules:
- protocol: tcp
from_port: 8080
to_port: 8080
cidr_blocks: ["10.0.0.0/8"]
# egress_rules: [] ← defaults to allow-all when omitted
ec2:
- name: <string>
instance_type: <string> # validated against allowed set
ami: <string> # AMI ID for the target region
subnet_ref: <subnet.name> # must reference a declared subnet
assign_public_ip: true | false
key_name: <string> # optional EC2 key pair name
tags: {} # optional extra tags
rds:
- name: <string>
engine: postgres | mysql | mariadb | …
engine_version: <string>
instance_class: db.t3.micro | … # validated against allowed set
allocated_storage: <int> # minimum 20
db_name: <string>
username: <string>
password_env_var: DB_PASSWORD # env var name (not the value!)
subnet_refs: [<subnet.name>, …] # must reference declared subnets
multi_az: false
publicly_accessible: false
tags: {}
elasticache:
- name: app-redis
engine: redis # redis | memcached
node_type: cache.t3.micro
num_cache_nodes: 1
subnet_refs: [app-1, app-2]
security_group_refs: [app-sg]
transit_encryption: true
at_rest_encryption: true
secrets:
- name: db-password
env_var: DB_PASSWORD # value read from os.environ[DB_PASSWORD] at deploy time
- name: api-key
env_var: THIRD_PARTY_API_KEY
log_groups:
- name: /app/production
retention_days: 30
cloudwatch_alarms:
- name: high-cpu
metric_name: CPUUtilization
namespace: AWS/EC2
threshold: 80
comparison_operator: GreaterThanOrEqualToThreshold
alarm_actions: [arn:aws:sns:us-east-1:123:alerts]| Rule | How |
|---|---|
Provider must be aws, azure, or gcp |
field_validator |
| Every subnet CIDR must fit inside the VPC CIDR | @model_validator + ipaddress.subnet_of() |
| Subnet names must be unique | @model_validator |
ec2[].subnet_ref must reference a declared subnet |
@model_validator |
rds[].subnet_refs must all reference declared subnets |
@model_validator |
allocated_storage ≥ 20 GiB |
field_validator |
| Region, instance type, engine | Validated by the cloud provider at preview/deploy time |
- Model — add a Pydantic model in
src/archer/models/aws/<service>.py(named after the AWS service namespace, e.g.waf.py) - Register — add it as a field on
AwsResourcesinsrc/archer/models/aws/__init__.py - Builder — create
src/archer/modules/aws/<service>.pywith an early-return guard - Wire — call the builder from
AWSProvider.build_resources()in the correct dependency position - Test — add a unit test in
tests/unit/test_builders.py
See CONTRIBUTING.md for a complete worked example.
- Create
src/archer/providers/<name>.py— implementBaseProvider - Add models to
src/archer/models/<name>.py - Register in
providers/__init__.py:from archer.providers.digitalocean import DigitalOceanProvider PROVIDER_REGISTRY: dict[str, type[BaseProvider]] = { "aws": AWSProvider, "azure": AzureProvider, "gcp": GCPProvider, "digitalocean": DigitalOceanProvider, # ← one line }
- No changes needed in
engine.pyorcli.py.
| Path | Purpose |
|---|---|
src/archer/cli.py |
Click commands + rich presentation (no Pulumi imports) |
src/archer/engine.py |
Pulumi Automation API wrapper |
src/archer/models/base.py |
Shared models: BackendConfig, OperationResult, ResourceChange |
src/archer/models/<cloud>/ |
One file per service namespace (e.g. models/aws/ec2.py, models/aws/rds.py). Adding a new service = add one file here. |
src/archer/modules/<cloud>/ |
One builder file per service — mirrors models/<cloud>/. Each builder has an early-return guard so unused services add zero cost. |
src/archer/providers/ |
Thin orchestration layer. aws.py calls builders in dependency order; azure.py / gcp.py delegate to their respective module packages. |
examples/ |
Ready-to-run YAML configurations |
tests/unit/ |
Pydantic validator tests + builder early-return tests |
Ready-to-use YAML files are in the examples/ directory:
| File | What it deploys |
|---|---|
aws-minimal.yaml |
VPC + 2 subnets + 1 EC2 bastion |
aws-3tier.yaml |
ALB + ASG (web/app tiers) + RDS PostgreSQL |
aws-eks.yaml |
EKS cluster with managed node group |
| Item | Status |
|---|---|
| AWS — core service coverage | ✅ 0.1.0 |
| Standalone security groups + global tags | ✅ 0.2.0 |
| ElastiCache, Secrets Manager, CloudWatch | ✅ 0.2.0 |
archer validate + --stack flag + archer output |
✅ 0.2.0 |
| Lambda + IAM execution role | 🔜 planned |
| API Gateway (HTTP API + REST API) | 🔜 planned |
| SNS topics + SQS queues | 🔜 planned |
| DynamoDB tables | 🔜 planned |
| CloudFront distributions | 🔜 planned |
| Aurora / Aurora Serverless v2 | 🔜 planned |
Variable substitution in YAML (${env}, ${region}) |
🔜 planned |
| Azure — full parity with AWS | 🔜 planned |
| GCP — full parity with AWS | 🔜 planned |
archer import — bring existing resources under management |
💡 idea |
| Stack references — pass outputs from one config as inputs to another | 💡 idea |
Drift detection (archer diff) |
💡 idea |
# Install with dev extras
uv pip install -e ".[dev]"
# Lint
ruff check src/ tests/
# Format
ruff format src/ tests/
# Type check
pyright src/
# Run tests
pytest
# Tests with coverage
pytest --cov=src/archer --cov-report=term-missingAll linting rules are in pyproject.toml under [tool.ruff]. Line length is 180 characters, target Python 3.11+.
See CONTRIBUTING.md.