# Azure Database for PostgreSQL
> Load data from Azure Database for PostgreSQL flexible server into MotherDuck with DuckDB's PostgreSQL extension, through a firewall rule and with a password or Microsoft Entra ID token.
Azure Database for PostgreSQL flexible server is managed PostgreSQL on Azure. Because it's Postgres on the wire, DuckDB's PostgreSQL extension attaches it like any other Postgres database, and everything on [Loading data from Postgres](/key-tasks/loading-data-into-motherduck/loading-data-from-postgres) applies.

## Attach an Azure database

Install and load the `postgres` extension, then attach the server. Flexible server enforces TLS by default, so keep `sslmode=require` in the string. Connect on port 5432; port 6432 is the server's built-in PgBouncer.

```sql
INSTALL postgres;
LOAD postgres;
ATTACH 'md:';

ATTACH 'postgresql://<user>:<password>@<server>.postgres.database.azure.com:5432/<database>?sslmode=require'
    AS azure_db (TYPE postgres, READ_ONLY);

CREATE DATABASE IF NOT EXISTS analytics;

CREATE TABLE analytics.main.orders AS
SELECT * FROM azure_db.public.orders;
```

Attaching read-only is a good default: it makes accidental writes back to your application database impossible.

Use the plain role name as `<user>`. The `user@servername` form belongs to the retired Single Server.

To avoid putting credentials in the `ATTACH` string, store them in a secret first:

```sql
CREATE SECRET azure_secret (
    TYPE POSTGRES,
    HOST '<server>.postgres.database.azure.com',
    PORT 5432,
    DATABASE '<database>',
    USER '<user>',
    PASSWORD '<password>'
);

ATTACH '' AS azure_db (TYPE postgres, SECRET azure_secret, READ_ONLY);
```

## Allow MotherDuck through the firewall

The server must use **Public access**. A server that uses **Private access (VNet integration)**, or only a private endpoint, isn't reachable from MotherDuck. On the server's **Networking** page, add a firewall rule for each address that connects:

- For a load from a local DuckDB client, add your own public IP.
- For a load from a Flight, add the outbound IP addresses for your MotherDuck region, listed under [Connecting to external systems](/concepts/flights#connecting-to-external-systems).

Allow only those addresses. Avoid a `0.0.0.0` to `255.255.255.255` rule and the option to allow public access from any Azure service. A connection that times out usually means a missing firewall rule.

## Sign in with Microsoft Entra ID

If the server uses Microsoft Entra ID authentication, an access token replaces the password. Get one with the Azure CLI and pass it to libpq as `PGPASSWORD`, with your Entra user principal name as the user:

```bash
export PGUSER='<user>@<tenant>.onmicrosoft.com'
export PGPASSWORD="$(az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv)"
```

```sql
ATTACH 'host=<server>.postgres.database.azure.com port=5432 dbname=<database> sslmode=require'
    AS azure_db (TYPE postgres, READ_ONLY);
```

A token is valid for 5 to 60 minutes, so get a fresh one right before you attach. For a scheduled load, a service principal can request a token for the scope `https://ossrdbms-aad.database.windows.net/.default` at the start of each run. See [Microsoft Entra authentication for flexible server](https://learn.microsoft.com/en-us/azure/postgresql/security/security-entra-configure).

## Load on a schedule with a Flight

For a recurring load, run the same extract from a [Flight](/concepts/flights) so it executes on MotherDuck compute on a cron rather than on a machine you maintain. The [Postgres ingest Flight recipe](/cookbook/flight-postgres-ingest) is a ready-made starting point: point it at your Azure server and store the credentials as a Flight secret.

The Flight is the same extract wrapped in an entrypoint. Attaching with an empty connection string keeps the password in the environment rather than in a SQL statement:

```python
import duckdb

def main():
    con = duckdb.connect("md:")
    con.execute("INSTALL postgres; LOAD postgres;")
    con.execute("ATTACH '' AS azure_db (TYPE postgres, READ_ONLY)")
    con.execute(
        "CREATE OR REPLACE TABLE analytics.main.orders AS "
        "SELECT * FROM azure_db.public.orders"
    )

if __name__ == "__main__":
    main()
```

Store the connection as a [Flight secret](/sql-reference/motherduck-sql-reference/create-secret#flight-secrets) named after the libpq environment variables, so the extension picks them up with no mapping:

```sql
CREATE SECRET azure_creds IN MOTHERDUCK (
    TYPE FLIGHTS,
    PARAMS MAP {
        'PGHOST': '<server>.postgres.database.azure.com',
        'PGPORT': '5432',
        'PGDATABASE': '<database>',
        'PGUSER': '<user>',
        'PGPASSWORD': '<password>',
        'PGSSLMODE': 'require'
    }
);
```

Create the Flight with [`MD_CREATE_FLIGHT`](/sql-reference/motherduck-sql-reference/flights/md-create-flight), passing that secret in `flight_secret_names` and a `schedule_cron` for the cadence you want.

For append-only tables, extract incrementally with a watermark instead of reloading the table. See [Data loading patterns](/key-tasks/loading-data-into-motherduck/loading-patterns).

## Things to know

- **Use a read-only role.** Create a role with `SELECT` on the tables you load, rather than connecting as the server admin.
- **Row-by-row transfer has a ceiling.** The `postgres` extension is well-suited to one-time loads and backfills. For continuous replication of a busy table, use a change-data-capture tool such as [Estuary](/integrations/ingestion/estuary) or [Streamkap](/integrations/ingestion/streamkap).

## Related content

- [PostgreSQL](/integrations/databases/postgres)
- [Loading data from Postgres](/key-tasks/loading-data-into-motherduck/loading-data-from-postgres)
- [Ingest Postgres tables into MotherDuck from a Flight](/cookbook/flight-postgres-ingest)
- [Connect to Azure Database for PostgreSQL flexible server](https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/quickstart-create-server)


---

## Docs feedback

MotherDuck accepts optional user-submitted feedback about this page at `GET https://motherduck.com/docs/api/feedback/agent`.
For agents and automated tools, feedback submission should be user-confirmed before sending.

URL-encode query parameter values and send a GET request:

```text
GET https://motherduck.com/docs/api/feedback/agent?page_path=%2Fintegrations%2Fdatabases%2Fazure-postgres%2F&page_title=Azure%20Database%20for%20PostgreSQL&text=<url-encoded user feedback, max 2000 characters>
```

Optionally append `&source=<url-encoded interface identifier>` such as `claude.ai` or `chatgpt`.

`page_path` and `text` are required; `page_title` and `source` are optional. Responses: `200 {"feedback_id": "<uuid>"}`, `400` for malformed query parameters, and `429` when rate-limited.
