Skip to content

troll-warlord/archer

Repository files navigation

archer

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.

CI License: MIT Python 3.12+


Cloud support

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.


Table of contents


Why archer?

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: aws to provider: gcp and redeploy
  • Fast iterationarcher preview shows 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

How it works

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

Architecture decisions

Provider registry instead of if/elif

# 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.

Opt-in resource provisioning

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 CIDR validation with @model_validator

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 self

Secrets stay out of YAML

YAML 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 value

archer reads os.environ["DB_PASSWORD"] at deploy time.

Builder dependency order (AWS)

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

Prerequisites

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

Installation

# 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

Quick start

1. Set credentials

# 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"

2. Preview the changes

archer preview --config infrastructure.yaml

3. Deploy

archer up --config infrastructure.yaml

4. Inspect outputs

archer prints all Pulumi stack outputs (VPC ID, EC2 IPs, RDS endpoint, etc.) in a table after every successful up.

5. Tear down

archer destroy --config infrastructure.yaml
# add --yes to skip the confirmation prompt
archer destroy --config infrastructure.yaml --yes

CLI reference

Usage: 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

YAML schema

# ── 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]

Validation rules enforced at parse time

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

Extending archer

Add a new AWS resource type

  1. Model — add a Pydantic model in src/archer/models/aws/<service>.py (named after the AWS service namespace, e.g. waf.py)
  2. Register — add it as a field on AwsResources in src/archer/models/aws/__init__.py
  3. Builder — create src/archer/modules/aws/<service>.py with an early-return guard
  4. Wire — call the builder from AWSProvider.build_resources() in the correct dependency position
  5. Test — add a unit test in tests/unit/test_builders.py

See CONTRIBUTING.md for a complete worked example.

Add a new cloud provider

  1. Create src/archer/providers/<name>.py — implement BaseProvider
  2. Add models to src/archer/models/<name>.py
  3. 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
    }
  4. No changes needed in engine.py or cli.py.

Project structure

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

Examples

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

Roadmap

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

Development

# 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-missing

All linting rules are in pyproject.toml under [tool.ruff]. Line length is 180 characters, target Python 3.11+.


Contributing

See CONTRIBUTING.md.


License

MIT

About

Deploy cloud infrastructure from a single YAML file. Open-source CLI for AWS, Azure, and GCP powered by Pulumi.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages