> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reasonos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Defining infrastructure

> Write infrastructure in the build language: cloud-agnostic resources, environments, provider-specific types, and wiring services to what they use.

Infrastructure definitions live in `infra.rbs` or `BUILD.rbs` files anywhere in your workspace. A bare `rbs infra plan` discovers all of them — colocating each service's infrastructure with its code across many packages is a fully supported layout, with no aggregator file required. A targeted invocation (`rbs infra plan //backend:*`) scopes to that package.

## Configure a provider

Register the provider you target and give it its settings:

```python theme={null}
infra.register_provider(
    name = "aws",
    source = "hashicorp/aws",
    version = "5.31.0",
)

infra.provider_config(
    provider = "aws",
    config = {"region": "us-west-2"},
)
```

For the built-in clouds this is mostly about configuration (region, project, subscription): using any resource type from the embedded catalogs automatically registers the provider at its pinned version. For any other Terraform-registry provider, `source = "namespace/name"` is how rbs knows what to download.

## Declare cloud-agnostic resources

Load the abstract rules from the prelude and declare what you need. The same declaration targets any supported cloud through the `providers` list:

```python theme={null}
load("@rbs//infra/prelude.rbs", "networking", "security_group", "database", "storage")

PROVIDERS = ["aws"]  # or ["google"], ["azurerm"] — same declarations

vpc = networking(
    name = "app-vpc",
    cidr = "10.0.0.0/16",
    providers = PROVIDERS,
    subnets = [
        {"cidr": "10.0.1.0/24", "public": True, "az": "a"},
        {"cidr": "10.0.10.0/24", "public": False, "az": "a"},
    ],
    enable_dns = True,
    enable_nat = True,
)

db_sg = security_group(
    name = "db-sg",
    network = vpc,
    providers = PROVIDERS,
    ingress = [
        {"from_port": 5432, "to_port": 5432, "protocol": "tcp", "cidr_blocks": ["10.0.0.0/16"]},
    ],
)

db = database(
    name = "maindb",
    engine = "postgres",
    version = "15",
    instance_type = "medium",   # abstract size: micro, small, medium, large, xlarge
    storage_gb = 100,
    network = vpc,
    security_groups = [db_sg],
    providers = PROVIDERS,
    backup_retention_days = 30,
)

assets = storage(
    name = "app-assets",
    providers = PROVIDERS,
    versioning = True,
    encryption = True,
    public = False,
)
```

Attributes use a shared vocabulary — `instance_type = "medium"` maps to the right machine size on each cloud, and cloud-native values (`"t3.micro"`) pass through untouched when you need a specific SKU. Attribute values are validated at declaration time, and referencing one resource from another (`network = vpc`) both wires the dependency and resolves the real identifiers at apply time.

## Environments and variables

Declare environments with their variables, read them with `infra.var`, and select one with `-e`:

```python theme={null}
infra.environment(name = "dev", variables = {"cloud": "aws", "db_size": "small"})
infra.environment(name = "prod", variables = {"cloud": "aws", "db_size": "large"})

CLOUD = infra.var("cloud", default = "aws")

database(
    name = "maindb",
    engine = "postgres",
    instance_type = infra.var("db_size", default = "small"),
    providers = [CLOUD],
)
```

```bash theme={null}
rbs infra apply -e prod
```

Each environment has its own state, so `dev` and `prod` are fully independent stacks of the same declarations.

## Scale by loop, not by copy

Definitions are ordinary `.rbs` code, so a monorepo provisions per-service infrastructure with a list comprehension:

```python theme={null}
load("@rbs//infra/prelude.rbs", "container_registry", "database")

SERVICES = ["api", "worker", "billing"]

[container_registry(
    name = svc + "-registry",
    providers = ["aws"],
) for svc in SERVICES]

database(name = "maindb", engine = "postgres", providers = ["aws"])
```

Adding a service means adding one name to the list: the next `rbs infra plan` shows exactly one create, and everything else plans as unchanged.

## Provider-specific resources

When an abstraction doesn't cover what you need, declare the concrete resource type directly with `infra.resource`. Every resource type of the built-in providers is available, typed and validated against the provider's real schema:

```python theme={null}
infra.resource(
    type = "aws_sqs_queue",
    name = "jobs",
    config = {
        "name": "jobs-queue",
        "visibility_timeout_seconds": 60,
    },
)
```

Unknown attributes are flagged at plan time, and the full provider-schema validation runs before anything is applied.

**References resolve at apply time.** Refer to another resource as `:name`, or to one of its attributes as `:name.attribute` — the value is filled in from real state during apply, in dependency order:

```python theme={null}
infra.resource(
    type = "aws_s3_bucket",
    name = "logs",
    config = {"bucket": "acme-logs"},
)

infra.resource(
    type = "aws_s3_bucket_versioning",
    name = "logs-versioning",
    config = {
        "bucket": ":logs.id",
        "versioning_configuration": {"status": "Enabled"},
    },
)
```

References double as dependency edges; add `depends_on = [":other"]` for ordering that no attribute expresses.

**Keep provider choice a variable** even at this level with the cross-cloud equivalence table — `infra.resolve` maps a kind to the concrete type per provider:

```python theme={null}
P = infra.var("cloud", default = "aws")

infra.resource(
    type = infra.resolve("object_storage", provider = P),  # aws_s3_bucket / google_storage_bucket / azurerm_storage_account
    name = "assets",
    config = {"bucket": "acme-assets"},
)
```

## Wire services to infrastructure

The `service` rule binds a binary to the resources it uses. You declare *what* the app needs, never *how* to reach it:

```python theme={null}
load("@rbs//go/rules.rbs", "go_binary")
load("@rbs//infra/service.rbs", "service")

go_binary(name = "api", srcs = ["main.go"])

service(
    name = "api-svc",
    binary = ":api",
    uses = ["maindb"],                            # resources declared in infra.rbs
    needs = {"DB_PASSWORD": "maindb.password"},   # explicit env <- resource.attribute
    env = {"LOG_LEVEL": "info"},                  # static config, highest precedence
)
```

`rbs run //backend:api-svc` builds the binary, resolves the environment from applied state, and launches the process with it injected. Each resource type declares conventional exports, so `uses = ["maindb"]` is usually all the wiring a service needs:

| Resource type        | Conventional exports                             |
| -------------------- | ------------------------------------------------ |
| `database`           | `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_URL` |
| `cache`              | `CACHE_HOST`, `CACHE_PORT`                       |
| `storage`            | `STORAGE_BUCKET`, `STORAGE_DOMAIN`               |
| `container_registry` | `REGISTRY_URL`                                   |

Other types export similarly (`KMS_KEY_ID`, `TOPIC_ID`, `LOG_GROUP`, ...). Anything the conventions don't cover — or an attribute a provider doesn't populate — is wired explicitly with `needs`.

Resolution happens at run time, not build time: infra changes take effect on the next run without a rebuild, and secrets never land in launcher scripts on disk. Outside `rbs run`, the same resolution is available on the command line:

```bash theme={null}
rbs infra env --uses maindb --need DB_PASSWORD=maindb.password
eval "$(rbs infra env --uses maindb --export)"
```

The `oci_push` rule accepts the same `uses` — `uses = ["api-registry"]` resolves the push repository from the registry rbs provisioned, never a hardcoded URL.

## Enforce policies

Policies run over declared resources on every plan; `severity = "error"` blocks apply. A baseline compliance pack ships in the binary — load it to enforce encryption at rest, no public data stores, and no open admin ingress across all providers at once:

```python theme={null}
load("@rbs//infra/policy/baseline.rbs", "BASELINE_POLICIES")
```

Write your own with `infra.define_policy` the same way — one control covers every cloud because policies see the abstract declarations.

## Group with modules

`infra.module` namespaces a group of resources so a stack can be instantiated more than once without name collisions:

```python theme={null}
def _web_stack(ctx):
    vpc = networking(name = "vpc", cidr = ctx.inputs["cidr"], providers = ctx.inputs["providers"])
    db = database(name = "db", engine = "postgres", network = vpc, providers = ctx.inputs["providers"])
    return {"vpc": vpc, "db": db}

team_a = infra.module(
    name = "team-a",
    definition = _web_stack,
    inputs = {"cidr": "10.1.0.0/16", "providers": ["aws"]},
)
```

Resources inside are named `team-a.vpc`, `team-a.db` in plans and state; modules nest, and the definition's return value becomes the module's outputs.

## Grow the catalog

When you need resource types beyond what ships in the binary — a newer provider version, or a provider rbs doesn't embed — generate typed definitions into your workspace:

```bash theme={null}
rbs infra generate aws aws_mq_broker          # one type
rbs infra generate cloudflare --all           # a whole provider
```

Generated files land under `rules/infra/embedded/<provider>/` by default, where they auto-load exactly like the built-in catalogs: `infra.resource(type = "cloudflare_...")` just works, version-pinned. The files are yours — edit freely or regenerate.

To build a new *cloud-agnostic* abstraction of your own, scaffold it across providers and refine:

```bash theme={null}
rbs infra scaffold message_broker \
    --map aws=aws_mq_broker \
    --map google=google_pubsub_topic \
    --map azurerm=azurerm_servicebus_namespace
```

This fetches each provider's schema, aligns matching attributes into a shared surface, and writes a draft abstraction plus one adapter per provider into `infra/` — the mechanical 80%. The judgement pass is yours: rename attributes to a shared vocabulary, add companion resources, wire outputs, then validate with `rbs infra verify`.
