# Infrastructure
Manage clusters, roles, secrets, connections, sources, and tables with mz-deploy apply.
`mz-deploy apply` converges your infrastructure objects declaratively — it creates what's missing and alters what has drifted. This page covers all object types managed by `apply`.

## Overview

`mz-deploy apply` applies all types in dependency order: clusters → roles → network policies → secrets → connections → sources → tables. Each step is idempotent — running `apply` multiple times converges to the same state.

Preview changes before applying:

```bash
mz-deploy apply --dry-run
```

Skip secrets (useful in CI where secret values aren't available):

```bash
mz-deploy apply --skip-secrets
```

You can also target individual object types with subcommands for granular control:

```bash
mz-deploy apply clusters
mz-deploy apply secrets
```

## Clusters

```sql
-- clusters/orders.sql
CREATE CLUSTER orders (SIZE = '25cc');
```

`apply` creates missing clusters and alters drifted configuration. Grants and comments are applied idempotently.

## Roles

```sql
-- roles/order_reader.sql
CREATE ROLE order_reader;
```

## Secrets

Secret values use client-side provider functions that are resolved at `apply` time. This means `compile` works without access to actual secret values.

Use `env_var()` to read values from environment variables:

```sql
-- models/materialize/public/pg_user.sql
CREATE SECRET pg_user AS env_var('PG_USER');
```

```sql
-- models/materialize/public/pg_password.sql
CREATE SECRET pg_password AS env_var('PG_PASSWORD');
```

Alternatively, use `aws_secret()` to pull values from AWS Secrets Manager:

```sql
-- models/materialize/public/pg_password.sql
CREATE SECRET pg_password AS aws_secret('prod/pg-password');
```

`aws_secret()` requires an `aws_profile` in your `project.toml`:

```toml
[profiles.default.security]
aws_profile = "my-aws-profile"
```

`apply secrets` is idempotent — it runs `CREATE SECRET IF NOT EXISTS` then `ALTER SECRET` to update the value.

## Connections

Postgres connection using secrets:

```sql
-- models/materialize/public/pg_conn.sql
CREATE CONNECTION pg_conn TO POSTGRES (
    HOST 'my-postgres.example.com',
    DATABASE 'app',
    USER SECRET pg_user,
    PASSWORD SECRET pg_password,
    SSL MODE 'require'
);
```

## Sources

Postgres source:

```sql
-- models/materialize/public/pg_source.sql
CREATE SOURCE pg_source
IN CLUSTER orders
FROM POSTGRES CONNECTION pg_conn
(PUBLICATION 'mz_source');
```

## Tables

```sql
-- models/materialize/public/orders.sql
CREATE TABLE orders FROM SOURCE pg_source
(REFERENCE public.orders);
```

Before creating anything, `apply tables` checks each `REFERENCE` against what its source can currently read from the upstream system. A reference the source cannot read fails the command, and the error suggests any reference the source does expose that is spelled closely enough to be the one you meant. Tables reading from a source the same command is creating are not checked ahead of time, because the source does not exist yet; those references are resolved when the table is created.

The check runs in a dry run as well as a real apply, and it is not read-only. It refreshes each source's references first, which contacts the upstream system and updates what the source records. Refreshing requires owning the source, and one you do not own is checked against the references already recorded for it.

After `apply tables`, the table's column schema is automatically captured in `types.lock`. This is how `compile` knows what columns `orders` has when type-checking views that reference it. See [Local development — Lock types](/developer-tools/mz-deploy/local-development/#lock-types) for details.
