Skip to content

Dagster with AWS compute

This page adds Polars Nanook authentication and per-asset compute sizing to a Dagster pipeline. It uses typed ConfigurableResource subclasses.

Credentials

Credentials are referenced through EnvVar so the raw values are resolved at run launch and never displayed in the Dagster UI. Populate NANOOK_CLIENT_ID and NANOOK_CLIENT_SECRET in the Dagster deployment environment, either directly or by exporting from a secret manager (e.g. AWS Secrets Manager).

Compute context lifecycle

PolarsNanookCompute declares PolarsNanookAuth as a nested resource so Dagster runs PolarsNanookAuth.setup_for_execution, which calls authenticate(...), before any compute resource is used. The compute resource owns the ComputeContext lifecycle through yield_for_execution: the compute cluster is started before each asset that uses the resource runs and stopped on exit, including on failure. Asset bodies contain only Polars code.

Per-asset machine size

Per-asset compute sizing follows from the same pattern: one PolarsNanookCompute instance per machine size (cpus/memory) under distinct resource keys, and each asset takes the instance sized for it. Dagster manages the lifecycle of each instance independently, so the asset that uses the larger cluster does not pay the cost of spinning it up while the smaller-cluster assets are running.

Complete example

The complete module below defines two resources (2 vCPU/4 GB and 4 vCPU/16 GB), three assets, and the Definitions object. Replace the ... placeholders with your source paths and S3 destinations. Each resource keeps the context it started, and each asset passes its resource's context to .remote(). A process-wide default set with pn.set_compute_context would send every asset to the cluster started last when Dagster runs assets in one process.

from contextlib import contextmanager

import polars as pl
import polars_nanook as pn
from dagster import asset, ConfigurableResource, Definitions, EnvVar
from pydantic import PrivateAttr


class PolarsNanookAuth(ConfigurableResource):
    """Service account authentication for Polars Nanook."""

    client_id: str
    client_secret: str

    def setup_for_execution(self, context) -> None:
        pn.authenticate(client_id=self.client_id, client_secret=self.client_secret)


class PolarsNanookCompute(ConfigurableResource):
    """Polars Nanook compute context with auto-managed cluster lifecycle.

    Declares `PolarsNanookAuth` as a nested resource so Dagster runs `authenticate(...)`
    before this resource is used. `yield_for_execution(...)` starts a `ComputeContext`
    of the configured shape and the `finally` clause guarantees the cluster is stopped even
    when an asset raises.
    """

    auth: PolarsNanookAuth
    cpus: int
    memory: int
    _ctx: pn.ComputeContext = PrivateAttr()

    @property
    def ctx(self) -> pn.ComputeContext:
        return self._ctx

    @contextmanager
    def yield_for_execution(self, context):
        ctx = pn.ComputeContext(
            workspace="My workspace", cpus=self.cpus, memory=self.memory
        )
        ctx.start()
        try:
            self._ctx = ctx
            yield self
        finally:
            ctx.stop()


@asset
def dataset_1(small_vm: PolarsNanookCompute):
    pl.scan_csv(...).remote(small_vm.ctx).sink_parquet(...).await_result()


@asset
def dataset_2(small_vm: PolarsNanookCompute):
    pl.scan_ndjson(...).remote(small_vm.ctx).sink_parquet(...).await_result()


# Use a bigger machine for the join.
@asset(deps=[dataset_1, dataset_2])
def joined(large_vm: PolarsNanookCompute):
    pl.scan_parquet(...).remote(large_vm.ctx).sink_parquet(...).await_result()


# Share a single `PolarsNanookAuth` instance between the compute resources.
auth = PolarsNanookAuth(
    client_id=EnvVar("NANOOK_CLIENT_ID"),
    client_secret=EnvVar("NANOOK_CLIENT_SECRET"),
)

defs = Definitions(
    assets=[dataset_1, dataset_2, joined],
    resources={
        "small_vm": PolarsNanookCompute(auth=auth, cpus=2, memory=4),
        "large_vm": PolarsNanookCompute(auth=auth, cpus=4, memory=16),
    },
)

Materializing joined starts the 4 vCPU cluster, runs the join remotely, and stops the cluster in the finally clause even if the asset raises. A service account starts without a default workspace, so the ComputeContext names the workspace with workspace=.

dataset_1 and dataset_2 do not depend on each other, so Dagster runs them in parallel, each on its own 2 vCPU cluster.

Resource key and parameter name

The Dagster resource key (the dictionary key in Definitions.resources) must match the parameter name in each asset signature. The type annotation marks the parameter as a resource, but Dagster injects by key, not by type.