Skip to content

Prefect with AWS compute

This page adds Polars Nanook authentication and per-task compute sizing to a Prefect flow. The credentials are kept in Prefect Secret blocks.

Credentials

The service-account client ID and secret are stored as Prefect Secret blocks so raw values are never hard-coded in flow code. Create them once from Python:

from prefect.blocks.system import Secret

Secret(value="<CLIENT_ID>").save("polars-nanook-client-id")
Secret(value="<CLIENT_SECRET>").save("polars-nanook-client-secret")

Cluster lifecycle

Authentication is performed once at the top of the flow so every task in the run shares the same session. The flow creates the contexts after that, because creating a ComputeContext contacts the control plane. Each task receives its context as an argument and passes it to .remote(ctx), and the flow owns the compute cluster lifecycle: start() is called before any task runs and stop() is guaranteed by the finally clause. The tasks set cache_policy=NO_CACHE because Prefect's default cache policy cannot hash a ComputeContext.

dataset_1 and dataset_2 share a single compute context (small_ctx) which handles both tasks sequentially (concurrently for Prefect, but sequentially in Polars Nanook). joined runs on a larger cluster (4 vCPU / 16 GB), which is started and stopped independently.

Complete example

The complete flow below starts both clusters, runs dataset_1 and dataset_2 on the 2 vCPU/4 GB cluster, then joined on the 4 vCPU/16 GB cluster. Replace ... with your paths.

import polars as pl
import polars_nanook as pn
from prefect import flow, task
from prefect.blocks.system import Secret
from prefect.cache_policies import NO_CACHE


@task(cache_policy=NO_CACHE)
def dataset_1(ctx):
    pl.scan_csv(...).remote(ctx).sink_parquet(...).await_result()


@task(cache_policy=NO_CACHE)
def dataset_2(ctx):
    pl.scan_ndjson(...).remote(ctx).sink_parquet(...).await_result()


# Use a bigger machine for the join.
@task(cache_policy=NO_CACHE)
def joined(ctx):
    pl.scan_parquet(...).remote(ctx).sink_parquet(...).await_result()


@flow(name="Report")
def report():
    pn.authenticate(
        client_id=Secret.load("polars-nanook-client-id").get(),
        client_secret=Secret.load("polars-nanook-client-secret").get(),
    )

    small_ctx = pn.ComputeContext(workspace="My workspace", cpus=2, memory=4)
    large_ctx = pn.ComputeContext(workspace="My workspace", cpus=4, memory=16)
    small_ctx.start()
    large_ctx.start()
    try:
        f1 = dataset_1.submit(small_ctx)
        f2 = dataset_2.submit(small_ctx)
        joined.submit(large_ctx, wait_for=[f1, f2]).wait()
    finally:
        small_ctx.stop()
        large_ctx.stop()


report()

Both clusters are stopped in the finally clause even when a task fails. The contexts pass workspace= because a service account starts without a default workspace.