<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: u11d</title>
    <description>The latest articles on Forem by u11d (@u11d).</description>
    <link>https://forem.com/u11d</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F11016%2F3aaba768-bbba-47eb-999a-9c314cf428f1.png</url>
      <title>Forem: u11d</title>
      <link>https://forem.com/u11d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/u11d"/>
    <language>en</language>
    <item>
      <title>Asset-Based Data Orchestration: Lessons from Building a Multi-State Social Data Platform</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Thu, 26 Mar 2026 07:21:25 +0000</pubDate>
      <link>https://forem.com/u11d/asset-based-data-orchestration-lessons-from-building-a-multi-state-social-data-platform-9l</link>
      <guid>https://forem.com/u11d/asset-based-data-orchestration-lessons-from-building-a-multi-state-social-data-platform-9l</guid>
      <description>&lt;p&gt;Building reliable data platforms rarely fails because of scale alone.&lt;br&gt;
More often, reliability collapses under &lt;strong&gt;heterogeneity&lt;/strong&gt;: multiple data providers, inconsistent schemas, partial updates, and unclear ownership.&lt;/p&gt;

&lt;p&gt;While building a multi-state social data platform ingesting resource data from dozens of organizations, we discovered that reliability is not a property of pipelines. It is a property of &lt;strong&gt;data artifacts and their relationships&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Why Reliability Becomes a Systems Problem at Scale
&lt;/h2&gt;

&lt;p&gt;The accompanying essay frames trust as something earned through consistent system behavior under real-world pressure. What that framing leaves implicit, but what became unavoidable in practice, is that reliability stops being a property of individual components very early on. Once multiple organizations, jurisdictions, and publishing surfaces are involved, reliability becomes an emergent property of the &lt;em&gt;entire system&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;For us, this meant that no single pipeline, connector, or database could be made "reliable enough" in isolation. Failures were rarely total. They were partial, localized, and often silent. The engineering challenge was not preventing all failure, but designing the system so that failures were isolated, detectable, and explainable.&lt;/p&gt;

&lt;p&gt;That requirement drove nearly every architectural decision that followed.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. System Overview
&lt;/h2&gt;

&lt;p&gt;Technically, the platform ingests social service resource data from independent organizations operating across multiple U.S. states. Each organization exposes data via a different source system (for example iCarol, WellSky, VisionLink, RTM), with varying schemas, update cadences, and quality guarantees.&lt;/p&gt;

&lt;p&gt;At a high level, the system consists of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Writers&lt;/strong&gt; - per-tenant ingestion and transformation projects that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch raw data from source systems via connector adapters&lt;/li&gt;
&lt;li&gt;Persist raw data into Snowflake source schemas&lt;/li&gt;
&lt;li&gt;Normalize and standardize data via DBT&lt;/li&gt;
&lt;li&gt;Apply enhancements such as geocoding or translations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Readers&lt;/strong&gt; - publisher processes that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React to completed writer runs, either bulk or incremental&lt;/li&gt;
&lt;li&gt;Publish curated artifacts to OpenSearch, MongoDB, and optionally Postgres&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Snowflake acts as the system of record for intermediate and normalized datasets. Dagster coordinates the execution and materialization of data assets. DBT is used explicitly for set-based transformations, not orchestration.&lt;/p&gt;

&lt;p&gt;Scale is not extreme in raw volume, but complexity is high: dozens of tenants, hundreds of tables per tenant, and frequent partial updates.&lt;/p&gt;
&lt;h3&gt;
  
  
  High-Level Architecture
&lt;/h3&gt;

&lt;p&gt;The following diagram illustrates the writer → Snowflake → DBT → asset orchestration → readers pattern.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgmvl4kvl30b7i0uowsfj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgmvl4kvl30b7i0uowsfj.webp" alt="Asset based data orchestration high level architecture" width="800" height="216"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The platform is designed around &lt;strong&gt;asset lineage and normalization contracts&lt;/strong&gt;, not pipelines.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Source Heterogeneity as the Dominant Constraint
&lt;/h2&gt;

&lt;p&gt;The hardest constraint was not throughput or storage. It was heterogeneity.&lt;/p&gt;

&lt;p&gt;Each data provider differed along several axes simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema shape&lt;/strong&gt;: even when nominally "the same" entities existed, fields varied&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantics&lt;/strong&gt;: identical fields often meant subtly different things&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update cadence&lt;/strong&gt;: some sources updated continuously, others weekly or ad hoc&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality guarantees&lt;/strong&gt;: missing fields, stale records, or partial exports were common&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Early on, we underestimated how strongly these differences would dominate system design. Attempting to treat ingestion as a uniform, pipeline-shaped process led to brittle assumptions and cross-tenant coupling.&lt;/p&gt;

&lt;p&gt;The system only became manageable once heterogeneity was treated as &lt;em&gt;fundamental&lt;/em&gt;, not incidental.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Normalization (HSDS, SDOH or Equivalent) as an Architectural Contract
&lt;/h2&gt;

&lt;p&gt;Normalization into an HSDS-like model was not implemented as a downstream convenience. It became an architectural contract.&lt;/p&gt;

&lt;p&gt;All downstream consumers, internal and external, implicitly rely on the guarantees of the normalized model: stable fields, predictable relationships, and documented semantics. That meant normalization could not be "best effort" or delayed until the end of a pipeline.&lt;/p&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Raw source data is written verbatim into Snowflake source schemas&lt;/li&gt;
&lt;li&gt;DBT ELT projects transform this raw data into standardized intermediate models&lt;/li&gt;
&lt;li&gt;DBT STAGE projects apply tenant-specific adaptations while preserving the contract&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation made it explicit where interpretation happens. If a field is wrong in the normalized model, the question becomes &lt;em&gt;which contract was violated&lt;/em&gt;, not "what broke in the pipeline".&lt;/p&gt;
&lt;h2&gt;
  
  
  5. What We Got Wrong Initially
&lt;/h2&gt;

&lt;p&gt;Several early assumptions did not survive contact with reality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pipeline-first thinking&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
We initially modeled work as long-running jobs. This obscured which intermediate datasets were durable, reusable, or safe to depend on. Debugging often meant rerunning more than necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manual validation&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Data quality checks lived outside the orchestration layer. Engineers and analysts manually inspected outputs, which worked at small scale but failed under concurrency and time pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared failure domains&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Multiple tenants often shared execution paths. A failure in one tenant’s ingestion could block or delay others, even when their data was unrelated.&lt;/p&gt;

&lt;p&gt;None of these issues were catastrophic individually. Together, they made reliability depend on human attention.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Transitioning to Asset-Based Data Orchestration with Dagster
&lt;/h2&gt;

&lt;p&gt;The shift to asset-based orchestration was driven less by tooling preference and more by a change in mental model.&lt;/p&gt;

&lt;p&gt;Instead of asking "what jobs should run?", we started asking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What data artifacts must exist?&lt;/li&gt;
&lt;li&gt;What do they depend on?&lt;/li&gt;
&lt;li&gt;How fresh do they need to be?&lt;/li&gt;
&lt;li&gt;What constitutes success or failure for &lt;em&gt;this&lt;/em&gt; artifact?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dagster assets provided a way to encode those questions directly.&lt;/p&gt;

&lt;p&gt;A simplified example from a writer project shows how DBT models are treated as assets rather than opaque steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# writer-xyz/assets.py (excerpt)
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dagster_dbt&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_assets_from_dbt_project&lt;/span&gt;

&lt;span class="n"&gt;dbt_elt_assets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_assets_from_dbt_project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;project_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_elt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;profiles_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbt_elt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not explain how DBT runs. It declares that the resulting tables are first-class assets with lineage and state.&lt;/p&gt;

&lt;p&gt;Once assets replaced jobs as the primary abstraction, freshness, lineage, and partial recomputation became explicit rather than implicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Partitioning for Failure Isolation
&lt;/h2&gt;

&lt;p&gt;Partitioning was critical for isolating failures.&lt;/p&gt;

&lt;p&gt;We partitioned primarily along tenant and state boundaries, not time. This reflected operational reality: data issues almost always affected a single organization or region.&lt;/p&gt;

&lt;p&gt;In Dagster terms, this meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Separate writer projects per tenant&lt;/li&gt;
&lt;li&gt;Independent schedules and sensors&lt;/li&gt;
&lt;li&gt;Asset materializations scoped to a tenant’s data domain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A failure in one writer no longer blocked publishing for others. More importantly, remediation could be targeted and auditable.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Data Quality Embedded in the Asset Graph
&lt;/h2&gt;

&lt;p&gt;Data validation moved into the asset graph itself.&lt;/p&gt;

&lt;p&gt;Instead of post-hoc checks, validations became explicit dependencies. If a validation asset failed, downstream assets simply did not materialize.&lt;/p&gt;

&lt;p&gt;An example pattern used across writers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@asset&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_staging_tables&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;staging_tables&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;staging_tables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count_missing_ids&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is intentionally simple. The key point is not the check itself, but that failure is structural. The system records that an expected artifact does not exist, rather than silently publishing bad data.&lt;/p&gt;

&lt;p&gt;This shifted failure detection earlier and reduced the blast radius of errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Operational Outcomes
&lt;/h2&gt;

&lt;p&gt;Day-to-day operations changed in several concrete ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On-call work shifted from rerunning pipelines to inspecting asset lineage&lt;/li&gt;
&lt;li&gt;Partial backfills became routine rather than exceptional&lt;/li&gt;
&lt;li&gt;Publishing delays were easier to attribute to specific upstream causes&lt;/li&gt;
&lt;li&gt;New tenants could be added without increasing shared operational risk&lt;/li&gt;
&lt;li&gt;None of this eliminated operational effort. It made that effort more focused and less reactive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10. Open Trade-offs and Unresolved Questions
&lt;/h2&gt;

&lt;p&gt;Some challenges remain unresolved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cross-tenant schema evolution still requires coordination and discipline&lt;/li&gt;
&lt;li&gt;Observability across Snowflake, DBT, Dagster, and downstream stores is fragmented&lt;/li&gt;
&lt;li&gt;Cost attribution at the asset level is still coarse-grained&lt;/li&gt;
&lt;li&gt;Human review remains necessary for certain semantic validations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With more time, we would invest earlier in unified observability and more formal schema versioning.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Why These Lessons Matter Beyond This Platform
&lt;/h2&gt;

&lt;p&gt;These lessons are not unique to this system.&lt;/p&gt;

&lt;p&gt;Any platform operating in civic tech, govtech, or environmental data shares similar constraints: multiple data producers, uneven quality, and real-world consequences for failure.&lt;/p&gt;

&lt;p&gt;The core takeaway is not "use asset-based orchestration", but treat data artifacts as obligations. Once that shift happens, many architectural decisions become clearer.&lt;/p&gt;

&lt;p&gt;Reliability stops being something you hope for and becomes something you can reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from this platform was not about any particular tool. It was about how we model the system itself.&lt;/p&gt;

&lt;p&gt;Once data artifacts became the core abstraction, many reliability problems became easier to reason about. Failures became visible, dependencies became explicit, and operational work shifted from firefighting pipelines to managing data contracts.&lt;/p&gt;

&lt;p&gt;For data platforms operating across heterogeneous sources, this shift can be the difference between a system that merely runs - and one that can be trusted.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is asset-based data orchestration?
&lt;/h3&gt;

&lt;p&gt;Asset-based data orchestration treats data artifacts (tables, datasets, models) as the primary units of orchestration instead of pipelines or jobs. Systems like Dagster allow teams to define dependencies between assets, enabling better lineage tracking, partial recomputation, and failure isolation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is asset-based orchestration useful for complex data platforms?
&lt;/h3&gt;

&lt;p&gt;In large systems with many data producers and consumers, failures are rarely binary. Asset-based orchestration makes dependencies explicit, allowing teams to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;isolate failures&lt;/li&gt;
&lt;li&gt;recompute only affected datasets&lt;/li&gt;
&lt;li&gt;track lineage across transformations&lt;/li&gt;
&lt;li&gt;enforce data quality checks before publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How does Dagster differ from traditional pipeline orchestrators?
&lt;/h3&gt;

&lt;p&gt;Traditional orchestrators schedule jobs or workflows. Dagster emphasizes data assets and lineage. This enables better visibility into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what datasets exist&lt;/li&gt;
&lt;li&gt;what produced them&lt;/li&gt;
&lt;li&gt;what depends on them&lt;/li&gt;
&lt;li&gt;whether they meet freshness or quality requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When should teams adopt asset-based orchestration?
&lt;/h3&gt;

&lt;p&gt;Asset-based orchestration becomes particularly valuable when systems have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;many data sources&lt;/li&gt;
&lt;li&gt;heterogeneous schemas&lt;/li&gt;
&lt;li&gt;multiple downstream consumers&lt;/li&gt;
&lt;li&gt;partial or incremental updates&lt;/li&gt;
&lt;li&gt;strict reliability requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These conditions are common in &lt;strong&gt;multi-tenant data platforms and civic tech systems&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does asset-based orchestration replace tools like DBT?
&lt;/h3&gt;

&lt;p&gt;No. Asset-based orchestration &lt;strong&gt;complements transformation tools&lt;/strong&gt; like DBT. In many architectures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DBT performs &lt;strong&gt;set-based transformations&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Dagster manages &lt;strong&gt;asset lineage, dependencies, scheduling, and validation&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation keeps orchestration and transformation responsibilities clear.&lt;/p&gt;

</description>
      <category>dagster</category>
      <category>dataorchestration</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Orchestrating Trust: Building Reliable Data Systems for Social Impact</title>
      <dc:creator>Paweł Sławacki</dc:creator>
      <pubDate>Tue, 24 Mar 2026 08:54:40 +0000</pubDate>
      <link>https://forem.com/u11d/orchestrating-trust-building-reliable-data-systems-for-social-impact-2b49</link>
      <guid>https://forem.com/u11d/orchestrating-trust-building-reliable-data-systems-for-social-impact-2b49</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;How production-grade orchestration enables impact at scale&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Systems built for social impact are often judged by their intent. The assumption is that good outcomes naturally follow from good goals, and that technical sophistication is secondary to mission. In practice, the opposite is often true. When information systems fail, the cost is not measured in lost revenue or delayed insights, but in missed opportunities for help, support, or timely intervention.&lt;/p&gt;

&lt;p&gt;At scale, social impact is not a matter of aspiration. It is a matter of reliability.&lt;/p&gt;

&lt;p&gt;Platforms that serve vulnerable populations operate under constraints that are both technical and ethical. Information must be accurate, current, and accessible. It must adapt as the world changes. It must withstand uneven demand and tolerate partial failure without collapsing. Most importantly, it must continue operating without constant human supervision, because manual intervention does not scale to moments of urgency.&lt;/p&gt;

&lt;p&gt;These requirements are familiar to anyone who has built systems for finance, logistics, or large-scale commerce. What differs is the margin for error. In social contexts, latency is not an inconvenience. Inconsistency is not an annoyance. Failure is not an abstract metric. The system either delivers trustworthy information when it is needed, or it does not.&lt;/p&gt;

&lt;p&gt;This is where data architecture quietly becomes social infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden fragility of well-intentioned systems
&lt;/h2&gt;

&lt;p&gt;Many social platforms begin with pragmatic solutions. Data is collected from disparate sources, normalized through custom pipelines, and exposed through simple interfaces. Early success reinforces the approach: the system works, organizations adopt it, and impact grows.&lt;/p&gt;

&lt;p&gt;Over time, however, complexity accumulates. Data sources evolve independently. Update cycles diverge. Quality varies across contributors. What once felt manageable starts to strain under its own assumptions.&lt;/p&gt;

&lt;p&gt;In building social data platforms, we learned that fragility rarely appears all at once. It emerges gradually. Pipelines grow longer. Reprocessing becomes broader than necessary. Validation shifts from design to manual oversight. Eventually, the system still functions but confidence in its outputs begins to erode.&lt;/p&gt;

&lt;p&gt;When correctness depends on human vigilance, availability depends on institutional memory. When updates become opaque, trust shifts away from architecture toward individual heroics. For systems intended to support people under real-world pressure, this is an unsustainable state.&lt;/p&gt;

&lt;p&gt;The problem is not a lack of data or compute. It is a lack of structural guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  From pipelines to obligations
&lt;/h2&gt;

&lt;p&gt;Traditional data pipelines are designed around execution. They define a sequence of tasks that transform inputs into outputs. This model assumes that intermediate states are transient and that value resides primarily at the end of the flow.&lt;/p&gt;

&lt;p&gt;In social data systems, this assumption does not hold.&lt;/p&gt;

&lt;p&gt;Normalized datasets, enriched resources, derived aggregates-these are not disposable by-products. They are durable artefacts with meaning beyond a single run. They are reused, audited, compared over time, and relied upon by downstream organizations making decisions under uncertainty.&lt;/p&gt;

&lt;p&gt;Once data outputs are treated as obligations rather than by-products, the role of orchestration changes fundamentally. The system’s responsibility is no longer to &lt;em&gt;run jobs&lt;/em&gt;, but to &lt;em&gt;ensure that specific states of data exist, remain current, and remain explainable&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This distinction matters because obligations persist. They require guarantees: freshness, lineage, and reproducibility. They require the system to know what it has produced, what it depends on, and what must change when assumptions shift.&lt;/p&gt;

&lt;p&gt;In this framing, orchestration stops being an operational convenience and becomes a form of governance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reliability under real-world constraints
&lt;/h2&gt;

&lt;p&gt;These principles became tangible while building Connect211, a modern search platform designed to support 211 organizations operating across multiple U.S. states. The platform aggregates resource data from independent organizations, each maintaining its own systems, taxonomies, and update rhythms.&lt;/p&gt;

&lt;p&gt;What we learned early is that reliability in such an environment cannot be retrofitted. Data sources change independently. Failures are localized. Demand is uneven and often event-driven. Manual coordination quickly becomes the bottleneck.&lt;/p&gt;

&lt;p&gt;Meeting these constraints required treating data artefacts as first-class citizens. Each normalized dataset, each enrichment step, each derived index represents a commitment: this information must exist, must be correct, and must be traceable back to its origins.&lt;/p&gt;

&lt;p&gt;Asset-oriented orchestration provided a natural way to express these commitments. Instead of reasoning about execution order, the system reasons about data state. Instead of pushing data through pipelines, it ensures that required artefacts are materialized and kept current as upstream conditions change.&lt;/p&gt;

&lt;p&gt;Dagster’s asset-based model aligned closely with this way of thinking. It allowed us to encode not only how data is processed, but what must be true for the system to be considered healthy. Orchestration became a mechanism for maintaining trust rather than merely coordinating tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automation without opacity
&lt;/h2&gt;

&lt;p&gt;Automation is often presented as a universal solution to scale. In social systems, automation without structure can be as dangerous as manual fragility. When updates propagate automatically but their causes remain hidden, errors scale just as efficiently as value.&lt;/p&gt;

&lt;p&gt;What distinguishes resilient systems is not the absence of automation, but the presence of clarity. Asset-based orchestration preserves the narrative of the data. Every artefact carries its provenance. Every update has a reason. When stakeholders ask why information changed, the answer is embedded in the structure of the system itself.&lt;/p&gt;

&lt;p&gt;In environments where information influences real-world outcomes, this explainability underpins legitimacy. Trust is not established through assurances, but through the ability to demonstrate correctness when it matters.&lt;/p&gt;

&lt;p&gt;Automation, in this sense, is not about removing humans from the loop. It is about ensuring that when humans intervene, they do so with understanding rather than guesswork.&lt;/p&gt;

&lt;h2&gt;
  
  
  Social impact as an emergent property
&lt;/h2&gt;

&lt;p&gt;It is tempting to frame social impact in terms of outcomes alone. Did the platform help more people? Did it improve access? Did it reduce friction?&lt;/p&gt;

&lt;p&gt;These questions are essential, but they are downstream. They describe effects, not causes.&lt;/p&gt;

&lt;p&gt;At scale, social impact emerges from systems that behave predictably under stress. From platforms that continue operating as inputs change unexpectedly. From architectures that degrade gracefully rather than fail catastrophically. From data systems that are transparent by design rather than opaque by accident.&lt;/p&gt;

&lt;p&gt;The same principles that govern production-grade platforms in commercial domains apply here, but with heightened stakes. Reliability is not an optimization. It is the foundation upon which impact rests.&lt;/p&gt;

&lt;p&gt;In this light, orchestration is not a technical detail. It is part of the social contract embedded in the system-defining how obligations are met, how failures are contained, and how trust is maintained over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond mission-driven engineering
&lt;/h2&gt;

&lt;p&gt;There is a persistent tendency to treat social platforms as exceptional-worthy of different standards because their goals are noble. In practice, this often leads to underinvestment in architecture, justified by urgency or limited resources.&lt;/p&gt;

&lt;p&gt;Our experience suggests the opposite conclusion. When tolerance for error is low and consequences are real, architectural rigor becomes more important, not less. Production-grade data systems are not at odds with social missions. They are prerequisites for sustaining them.&lt;/p&gt;

&lt;p&gt;Asset-based orchestration, exemplified by tools like Dagster, provides a framework for expressing this rigor. It shifts focus from execution to responsibility, from pipelines to promises. It allows systems to scale not only in size, but in trustworthiness.&lt;/p&gt;

&lt;p&gt;Social impact does not arise from technology alone. But without reliable systems, even the strongest intentions struggle to translate into lasting effect. When data platforms are designed as social infrastructure, reliability ceases to be a purely technical concern and becomes a public good.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ: Production-Grade Orchestration for Social Impact Systems
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is asset-based orchestration in data engineering?
&lt;/h3&gt;

&lt;p&gt;Asset-based orchestration is an architectural approach where &lt;strong&gt;data artefacts (datasets, models, indexes, aggregates)&lt;/strong&gt; are treated as first-class citizens rather than by-products of pipeline runs.&lt;/p&gt;

&lt;p&gt;Instead of defining execution steps, you define &lt;strong&gt;data states that must exist&lt;/strong&gt; and their dependencies. The orchestration system ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Correct dependency resolution&lt;/li&gt;
&lt;li&gt;Incremental recomputation&lt;/li&gt;
&lt;li&gt;Freshness guarantees&lt;/li&gt;
&lt;li&gt;Lineage tracking&lt;/li&gt;
&lt;li&gt;Failure isolation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This shifts orchestration from task coordination to &lt;strong&gt;state governance&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is asset-based orchestration different from traditional pipelines?
&lt;/h3&gt;

&lt;p&gt;Traditional pipelines are &lt;strong&gt;execution-oriented&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step A → Step B → Step C&lt;/li&gt;
&lt;li&gt;Outputs are transient&lt;/li&gt;
&lt;li&gt;Reprocessing is often coarse-grained&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Asset-based systems are &lt;strong&gt;state-oriented&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explicit dependency graphs&lt;/li&gt;
&lt;li&gt;Selective re-materialization&lt;/li&gt;
&lt;li&gt;Persistent artefacts with lineage&lt;/li&gt;
&lt;li&gt;Declarative data contracts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key difference is that pipelines answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What runs next?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Asset-based orchestration answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What must be true about the data?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For systems operating under strict reliability constraints, that distinction is critical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does data lineage matter in social impact systems?
&lt;/h3&gt;

&lt;p&gt;In social systems, incorrect data can influence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access to services&lt;/li&gt;
&lt;li&gt;Emergency response decisions&lt;/li&gt;
&lt;li&gt;Resource allocation&lt;/li&gt;
&lt;li&gt;Regulatory reporting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lineage provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auditability&lt;/li&gt;
&lt;li&gt;Explainability&lt;/li&gt;
&lt;li&gt;Reproducibility&lt;/li&gt;
&lt;li&gt;Impact traceability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When stakeholders ask, &lt;em&gt;“Why did this information change?”&lt;/em&gt;, lineage allows engineering teams to answer with certainty, not speculation.&lt;/p&gt;

&lt;h3&gt;
  
  
  What architectural risks do social data platforms typically face?
&lt;/h3&gt;

&lt;p&gt;Common failure modes include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Silent schema drift from independent data providers&lt;/li&gt;
&lt;li&gt;Broad, expensive reprocessing triggered by minor upstream changes&lt;/li&gt;
&lt;li&gt;Manual validation becoming a hidden operational dependency&lt;/li&gt;
&lt;li&gt;Lack of observability into partial failures&lt;/li&gt;
&lt;li&gt;Tight coupling between ingestion and serving layers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without structural guarantees, reliability degrades gradually — often without obvious alerts.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does orchestration contribute to data governance?
&lt;/h3&gt;

&lt;p&gt;Orchestration becomes governance when it encodes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explicit data ownership boundaries&lt;/li&gt;
&lt;li&gt;Dependency contracts&lt;/li&gt;
&lt;li&gt;Freshness expectations&lt;/li&gt;
&lt;li&gt;Failure domains&lt;/li&gt;
&lt;li&gt;Version-aware updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rather than governance being a policy document, it becomes embedded in the system’s execution model.&lt;/p&gt;

&lt;p&gt;This reduces reliance on institutional memory and tribal knowledge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is asset-based orchestration only relevant at large scale?
&lt;/h3&gt;

&lt;p&gt;No. It becomes more visible at scale, but its benefits appear earlier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Faster iteration cycles&lt;/li&gt;
&lt;li&gt;Safer refactoring&lt;/li&gt;
&lt;li&gt;More predictable deployments&lt;/li&gt;
&lt;li&gt;Lower operational overhead&lt;/li&gt;
&lt;li&gt;Clearer system reasoning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For mission-critical domains, reliability requirements often emerge before traffic scale does.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does this relate to data observability and reliability engineering?
&lt;/h3&gt;

&lt;p&gt;Asset-based orchestration complements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data observability (freshness, volume, schema monitoring)&lt;/li&gt;
&lt;li&gt;Data reliability engineering practices&lt;/li&gt;
&lt;li&gt;SLA/SLO enforcement&lt;/li&gt;
&lt;li&gt;Incident response workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because dependencies are explicit, blast radius and impact analysis become tractable. Observability signals can be tied directly to defined data obligations.&lt;/p&gt;

&lt;h3&gt;
  
  
  What role does automation play in maintaining trust?
&lt;/h3&gt;

&lt;p&gt;Automation enables scale, but trust requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transparency&lt;/li&gt;
&lt;li&gt;Traceability&lt;/li&gt;
&lt;li&gt;Deterministic recomputation&lt;/li&gt;
&lt;li&gt;Controlled failure propagation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Well-structured orchestration ensures that automation is explainable, not opaque. Errors do not silently cascade across the system.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should a team consider moving from pipelines to asset-oriented architecture?
&lt;/h3&gt;

&lt;p&gt;Signals include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Increasing reprocessing cost&lt;/li&gt;
&lt;li&gt;Growing dependency complexity&lt;/li&gt;
&lt;li&gt;Difficulty explaining data changes&lt;/li&gt;
&lt;li&gt;Manual intervention becoming routine&lt;/li&gt;
&lt;li&gt;Rising stakeholder sensitivity to correctness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If correctness is non-negotiable, state-aware orchestration becomes a strategic investment rather than an optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;About the authors / context&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;This article is based on our direct experience building production-grade data infrastructure for &lt;a href="https://connect211.com" rel="noopener noreferrer"&gt;https://connect211.com&lt;/a&gt;, a modern search platform supporting 211 organizations across multiple U.S. states. The insights presented reflect real architectural decisions made while scaling a social impact system operating under strict reliability and data-quality constraints.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dagster</category>
      <category>datascience</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Next.js 16 Partial Prerendering (PPR): The Best of Static and Dynamic Rendering</title>
      <dc:creator>uninterrupted</dc:creator>
      <pubDate>Thu, 05 Mar 2026 08:21:46 +0000</pubDate>
      <link>https://forem.com/u11d/nextjs-16-partial-prerendering-ppr-the-best-of-static-and-dynamic-rendering-2fgg</link>
      <guid>https://forem.com/u11d/nextjs-16-partial-prerendering-ppr-the-best-of-static-and-dynamic-rendering-2fgg</guid>
      <description>&lt;p&gt;Next.js has long been a leader in giving developers flexible, high-performance rendering strategies - &lt;a href="https://u11d.com/blog/ssg-isr-ssr-csr-which-strategy-should-i-use-in-my-next-js-e-commerce-platform/" rel="noopener noreferrer"&gt;Static Site Generation (SSG), Server-Side Rendering (SSR), Incremental Static Regeneration (ISR), and Client-Side Rendering (CSR)&lt;/a&gt; all play roles depending on your use case. With Next.js 14, Partial Prerendering (PPR) was introduced as an experimental feature, starting from Next.js 16, PPR has become a stable and becomes a foundational part of this landscape by letting you &lt;em&gt;blend static pre-rendering and dynamic behavior in the same route&lt;/em&gt;, improving perceived performance and SEO without sacrificing real-time data needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What Is Partial Prerendering (PPR)?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Partial Prerendering&lt;/strong&gt; is a rendering strategy that lets Next.js deliver a &lt;strong&gt;static HTML “shell” immediately&lt;/strong&gt;, then &lt;em&gt;stream in dynamic content&lt;/em&gt; as it becomes available - &lt;em&gt;all in the same server response&lt;/em&gt;. Instead of choosing static &lt;em&gt;or&lt;/em&gt; dynamic at the page level, PPR combines both on a per-component basis.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;static shell&lt;/strong&gt; (layout, logo, product titles, navigation) is pre-generated at build time or cached ahead of requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic parts&lt;/strong&gt; (cart state, recommendations, session data) load later and are &lt;em&gt;streamed&lt;/em&gt; to the client using React’s &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundaries.&lt;/li&gt;
&lt;li&gt;Users see meaningful UI immediately, with dynamic content filling in seamlessly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This leads to &lt;em&gt;faster Time to First Byte (TTFB)&lt;/em&gt; and a smoother perceived experience, while still allowing data personalization and real-time updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How Partial Prerendering Works in Next.js 16&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Cache Components: The Heart of PPR&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;In Next.js 16, PPR isn’t just an experimental flag - it’s integrated into the &lt;a href="https://nextjs.org/docs/app/getting-started/cache-components" rel="noopener noreferrer"&gt;&lt;strong&gt;Cache Components&lt;/strong&gt;&lt;/a&gt; system. Cache Components lets you &lt;em&gt;opt-in to cache certain components or data instead of rendering them on every request&lt;/em&gt;, making pre-rendering more predictable and performant.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cacheComponents: true&lt;/code&gt;&lt;/strong&gt; in &lt;code&gt;next.config.ts&lt;/code&gt; enables PPR and component-level caching.&lt;/li&gt;
&lt;li&gt;The new &lt;a href="https://nextjs.org/docs/app/getting-started/cache-components#during-prerendering" rel="noopener noreferrer"&gt;&lt;code&gt;"use cache"&lt;/code&gt;&lt;/a&gt; directive lets you mark data-fetching functions or components as cacheable.&lt;/li&gt;
&lt;li&gt;Anything not cacheable or depending on request-specific data can be wrapped in a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach gives you fine-grained control: static UI is reused across requests, while dynamic parts rehydrate or stream in only when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Streaming and Suspense: The Engine Under the Hood&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;React’s &lt;strong&gt;&lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;&lt;/strong&gt; plays a key role in PPR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrap dynamic components in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; with a fallback UI (like a skeleton loader).&lt;/li&gt;
&lt;li&gt;During rendering, Suspense tells Next.js where to &lt;em&gt;halt pre-rendering&lt;/em&gt; and stream dynamic content later.&lt;/li&gt;
&lt;li&gt;The server sends the pre-rendered shell immediately and then streams dynamic sections in parallel as they’re ready.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This strategy avoids blocks in page delivery and reduces the “white screen” effect often seen in traditional SSR or CSR approaches.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why PPR Matters for E-commerce&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For e-commerce platforms, performance directly impacts sales and SEO - factors also emphasized in the U11D articles about rendering strategies. A slow page can hurt conversions and search rankings. Partial Prerendering improves this by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Faster initial loads:&lt;/strong&gt; Users see the page skeleton instantly, improving engagement and performance metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO advantages:&lt;/strong&gt; Static content is available for crawler indexing immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic personalization:&lt;/strong&gt; Cart contents, recommendations, user-specific prices or availability can appear without blocking the initial render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More flexible than SSG/SSR alone:&lt;/strong&gt; You’re not limited to fully static or completely dynamic pages - the best of both lives together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In e-commerce apps where product detail pages might be static for most users but show personalized recommendations or inventory status, PPR is a compelling hybrid.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How to Enable and Use PPR in Next.js 16&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Enable Cache Components:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In your &lt;code&gt;next.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cacheComponents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// enables Partial Prerendering&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Wrap dynamic UI:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use React’s &lt;code&gt;Suspense&lt;/code&gt; for dynamic content:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading product...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProductDetails&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;use cache&lt;/code&gt; for predictable dynamic data:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Inside dynamic data functions, prefix with &lt;code&gt;"use cache"&lt;/code&gt; to mark them cacheable:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This tells Next.js that caching is safe for that data.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How PPR Fits with Other Rendering Strategies&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Partial Prerendering doesn’t replace SSR, SSG, ISR, or CSR - but &lt;em&gt;coordinates with them&lt;/em&gt;. While U11D’s articles give a great overview of traditional strategies like SSG, SSR, and ISR, PPR adds a hybrid strategy that sits between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SSG/ISR:&lt;/strong&gt; Good for full static pages or pages that regenerate occasionally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSR:&lt;/strong&gt; Ideal for real-time personalized content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSR:&lt;/strong&gt; Great for highly interactive client-heavy UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PPR (Next.js 16):&lt;/strong&gt; Combines static cached shells with streaming dynamic content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of PPR as a &lt;em&gt;component-level SSG + dynamic hybrid -&lt;/em&gt; fast to load like SSG, flexible like SSR.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Wrapping Up&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Next.js 16 elevates Partial Prerendering from an experimental concept to a practical, integrated performance strategy through &lt;strong&gt;Cache Components&lt;/strong&gt; and React Suspense. It’s especially powerful for complex, dynamic sites like e-commerce stores, where you want the benefits of static pre-rendering &lt;em&gt;and&lt;/em&gt; dynamic personalization without sacrificing UX or SEO.&lt;/p&gt;

&lt;p&gt;By serving instantly usable HTML and streaming dynamic parts in parallel, PPR bridges the traditional divide between static and dynamic rendering - helping your app &lt;em&gt;feel&lt;/em&gt; faster while staying robust and scalable.&lt;/p&gt;

&lt;p&gt;If you’re building with Next.js 16, definitely explore PPR alongside other strategies like SSR, ISR, and CSR to find the most performance-optimized combination for your routes.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>ppr</category>
    </item>
    <item>
      <title>Secure RDS Access Without Bastion Hosts: Using ECS Containers and SSM</title>
      <dc:creator>Bartek Gałęzowski</dc:creator>
      <pubDate>Wed, 25 Feb 2026 08:00:00 +0000</pubDate>
      <link>https://forem.com/u11d/secure-rds-access-without-bastion-hosts-using-ecs-containers-and-ssm-43pe</link>
      <guid>https://forem.com/u11d/secure-rds-access-without-bastion-hosts-using-ecs-containers-and-ssm-43pe</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Accessing RDS databases in production environments presents a common security challenge. Direct internet access to databases is a significant security risk, so most organizations place their RDS instances in private subnets without public endpoints. But how do you access these databases for debugging, data analysis, or administrative tasks?&lt;/p&gt;

&lt;p&gt;Traditional approaches include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bastion hosts&lt;/strong&gt;: Requires maintaining and securing additional EC2 instances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPN connections&lt;/strong&gt;: Complex setup and ongoing maintenance overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH tunneling&lt;/strong&gt;: Still requires a jump server with SSH access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a more elegant solution: leveraging your existing ECS containers as secure tunnels to your RDS databases using AWS Systems Manager (SSM) Session Manager.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;This script creates a secure tunnel from your local machine to an RDS database through a running ECS container, using SSM Session Manager for the connection. No SSH keys, no bastion hosts, no exposed ports—just IAM-based authentication and encrypted connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before using this script, ensure you have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AWS CLI&lt;/strong&gt; installed and configured with appropriate credentials&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session Manager Plugin&lt;/strong&gt; for AWS CLI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ECS Task Role&lt;/strong&gt; with permissions to use SSM:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
           &lt;/span&gt;&lt;span class="s2"&gt;"ssmmessages:CreateControlChannel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
           &lt;/span&gt;&lt;span class="s2"&gt;"ssmmessages:CreateDataChannel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
           &lt;/span&gt;&lt;span class="s2"&gt;"ssmmessages:OpenControlChannel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
           &lt;/span&gt;&lt;span class="s2"&gt;"ssmmessages:OpenDataChannel"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ECS Exec enabled&lt;/strong&gt; on your service (can be enabled with &lt;code&gt;aws ecs update-service --cluster &amp;lt;cluster&amp;gt; --service &amp;lt;service&amp;gt; --enable-execute-command --force-new-deployment&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The script performs the following operations:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Task Discovery
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TASK_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ecs list-tasks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--desired-status&lt;/span&gt; RUNNING &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'taskArns[0]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finds the first running task in the specified ECS service.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Container Runtime ID Retrieval
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CONTAINER_RUNTIME_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ecs describe-tasks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tasks&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TASK_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'tasks[0].containers[?name==`'&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s1"&gt;'`].runtimeId | [0]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Port Forwarding Session
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ssm start-session &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; &lt;span class="s2"&gt;"ecs:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CLUSTER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TASK_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONTAINER_RUNTIME_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--document-name&lt;/span&gt; AWS-StartPortForwardingSessionToRemoteHost &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt; &lt;span class="s1"&gt;'{"host":["$DB_HOST"],"portNumber":["5432"], "localPortNumber":["$LOCAL_PORT"]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Establishes a secure tunnel through the ECS container to the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Usage&lt;/strong&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eu&lt;/span&gt;

usage&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Usage: &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;CLUSTER&amp;gt; &amp;lt;SERVICE_NAME&amp;gt; &amp;lt;DB_HOST&amp;gt; &amp;lt;LOCAL_PORT&amp;gt; &amp;lt;REGION&amp;gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  cluster: ECS cluster name"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  service: ECS service name"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  db_host: Database host"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  local_port: Local port to forward"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  region:  AWS region (e.g., us-east-2)"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$# &lt;/span&gt;&lt;span class="nt"&gt;-ne&lt;/span&gt; 5 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Error: Expected 5 arguments, got $#"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  usage
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;CLUSTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;DB_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;LOCAL_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$4&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$5&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

get_first_task_id&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;FIRST_TASK_ID
  &lt;span class="nv"&gt;FIRST_TASK_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ecs list-tasks &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cluster&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--service-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--desired-status&lt;/span&gt; RUNNING &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; text &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'taskArns[0]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/.*\///'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FIRST_TASK_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"None"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FIRST_TASK_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Error: No running tasks found for service '&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_NAME&lt;/span&gt;&lt;span class="s2"&gt;' in cluster '&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="k"&gt;fi

  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FIRST_TASK_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;TASK_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;get_first_task_id&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;CONTAINER_RUNTIME_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ecs describe-tasks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tasks&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TASK_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'tasks[0].containers[?name==`'&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s1"&gt;'`].runtimeId | [0]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ecs:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CLUSTER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TASK_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CONTAINER_RUNTIME_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

aws ssm start-session &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--document-name&lt;/span&gt; AWS-StartPortForwardingSessionToRemoteHost &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;host&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$DB_HOST&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;],&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;portNumber&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;5432&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;], &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;localPortNumber&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$LOCAL_PORT&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the script as &lt;code&gt;ecs-db-tunnel.sh&lt;/code&gt; and make it executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ecs-db-tunnel.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the script with the required parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./ecs-db-tunnel.sh &amp;lt;CLUSTER&amp;gt; &amp;lt;SERVICE_NAME&amp;gt; &amp;lt;DB_HOST&amp;gt; &amp;lt;LOCAL_PORT&amp;gt; &amp;lt;REGION&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./ecs-db-tunnel.sh &lt;span class="se"&gt;\&lt;/span&gt;
  production-cluster &lt;span class="se"&gt;\&lt;/span&gt;
  api-service &lt;span class="se"&gt;\&lt;/span&gt;
  mydb.c9akciq32.us-east-2.rds.amazonaws.com &lt;span class="se"&gt;\&lt;/span&gt;
  5432 &lt;span class="se"&gt;\&lt;/span&gt;
  us-east-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once connected, you can access the database from your local machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;-h&lt;/span&gt; localhost &lt;span class="nt"&gt;-p&lt;/span&gt; 5432 &lt;span class="nt"&gt;-U&lt;/span&gt; dbuser &lt;span class="nt"&gt;-d&lt;/span&gt; mydb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or using other tools such as &lt;a href="https://dbeaver.io/" rel="noopener noreferrer"&gt;DBeaver&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Key Benefits&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. No Infrastructure Overhead&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;No need to maintain bastion hosts or VPN servers. You leverage existing ECS containers.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Enhanced Security&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;No SSH keys to manage or rotate&lt;/li&gt;
&lt;li&gt;No exposed ports or public endpoints&lt;/li&gt;
&lt;li&gt;IAM-based authentication and authorization&lt;/li&gt;
&lt;li&gt;All traffic encrypted through SSM&lt;/li&gt;
&lt;li&gt;Audit trail through CloudTrail&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Zero Configuration&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If your ECS service is already running with ECS Exec enabled, you're ready to go.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Cost Effective&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;No additional EC2 instances to run. Session Manager has no additional cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. Temporary Access&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Connection exists only while the script is running—perfect for adhoc administrative tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Security Considerations&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Network Security&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This approach works because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ECS container is in the same VPC as the RDS instance&lt;/li&gt;
&lt;li&gt;The container's security group allows outbound connections to the RDS security group&lt;/li&gt;
&lt;li&gt;The RDS security group permits inbound connections from the ECS container's security group&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Access Control&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Control who can establish tunnels using IAM policies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ecs:ListTasks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ecs:DescribeTasks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ssm:StartSession"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:RequestedRegion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us-east-2"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Troubleshooting&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;No running tasks found&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Ensure the ECS service has at least one running task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecs list-tasks &lt;span class="nt"&gt;--cluster&lt;/span&gt; &amp;lt;cluster&amp;gt; &lt;span class="nt"&gt;--service-name&lt;/span&gt; &amp;lt;service&amp;gt; &lt;span class="nt"&gt;--desired-status&lt;/span&gt; RUNNING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Session Manager plugin not found&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Install the Session Manager plugin following &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html" rel="noopener noreferrer"&gt;AWS documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Connection refused&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RDS security group allows inbound from ECS container security group&lt;/li&gt;
&lt;li&gt;Database endpoint is correct&lt;/li&gt;
&lt;li&gt;ECS task has network connectivity to RDS&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;ECS Exec not enabled&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Enable it on your service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecs update-service &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; &amp;lt;cluster&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service&lt;/span&gt; &amp;lt;service&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-execute-command&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--force-new-deployment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Using ECS containers as secure tunnels to RDS databases is an elegant solution that leverages existing infrastructure while maintaining strong security posture. This approach eliminates the need for bastion hosts, reduces attack surface, and provides auditable, temporary access to private databases.&lt;/p&gt;

&lt;p&gt;The script demonstrates that sometimes the best security solutions are those that work with your existing architecture rather than adding more complexity on top of it.&lt;/p&gt;

</description>
      <category>rds</category>
      <category>aws</category>
      <category>ssm</category>
      <category>ecs</category>
    </item>
    <item>
      <title>How to Deploy Next.js 16 SSG to AWS Amplify: Complete Guide</title>
      <dc:creator>Michał Miler</dc:creator>
      <pubDate>Tue, 24 Feb 2026 07:02:03 +0000</pubDate>
      <link>https://forem.com/u11d/how-to-deploy-nextjs-16-ssg-to-aws-amplify-complete-guide-5g9j</link>
      <guid>https://forem.com/u11d/how-to-deploy-nextjs-16-ssg-to-aws-amplify-complete-guide-5g9j</guid>
      <description>&lt;p&gt;Deploying Next.js Static Site Generation (SSG) applications to AWS Amplify can be challenging without the right configuration. While Vercel offers a seamless experience, AWS Amplify may provide a cost-effective alternative with up to 96% savings for smaller sites and 20-30% for medium-traffic applications. In this comprehensive guide, we'll walk through the exact steps to successfully deploy your Next.js 16 SSG site to AWS Amplify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AWS Amplify for SSG?
&lt;/h2&gt;

&lt;p&gt;AWS Amplify is particularly attractive for SSG deployments because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lower costs&lt;/strong&gt; than Vercel, especially for commercial use (see our &lt;a href="https://u11d.com/blog/vercel-vs-aws-amplify-pricing-nextjs/" rel="noopener noreferrer"&gt;detailed cost comparison&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No per-seat pricing&lt;/strong&gt; for team collaboration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generous free tier&lt;/strong&gt; for the first 12 months&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard AWS infrastructure&lt;/strong&gt; (S3 + CloudFront) with no vendor lock-in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note on static export limitations&lt;/strong&gt;: Static Site Generation pre-generates all pages at build time. This means no server-side rendering (SSR), no Incremental Static Regeneration (ISR), and no API routes. If you need these features, you'll need a different deployment strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we begin, ensure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 22.x or later installed&lt;/li&gt;
&lt;li&gt;Next.js 16 project using App Router&lt;/li&gt;
&lt;li&gt;An AWS account (free tier eligible for 12 months)&lt;/li&gt;
&lt;li&gt;Git repository (GitHub, GitLab, Bitbucket, or AWS CodeCommit)&lt;/li&gt;
&lt;li&gt;Basic familiarity with Next.js App Router&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Configure Next.js 16 for SSG
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Update &lt;code&gt;next.config.ts&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Create or modify your &lt;code&gt;next.config.ts&lt;/code&gt; file to enable static export:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;export&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;unoptimized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Points:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;output: 'export'&lt;/code&gt; disables the Next.js runtime and enforces a full static export. This ensures your site can be deployed as plain static HTML/CSS/JS.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;images.unoptimized: true&lt;/code&gt; disables Next.js Image Optimization API (required for static export)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Handle Image Optimization
&lt;/h3&gt;

&lt;p&gt;Since Next.js Image Optimization doesn't work with static exports, choose one of these options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Unoptimized images&lt;/strong&gt; (simplest for testing)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Image&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/images/photo.jpg"&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Description"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option 2: Standard HTML img tags&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/images/photo.jpg"&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Description"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option 3: Third-party CDN&lt;/strong&gt; (recommended for production)&lt;/p&gt;

&lt;p&gt;Use services like Cloudflare Images, Cloudinary, or imgix. See our guide on &lt;a href="https://u11d.com/blog/speed-up-your-next-js-app-optimizing-s3-images-with-cloudflare-images/" rel="noopener noreferrer"&gt;optimizing S3 images with Cloudflare Images&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageLoader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`https://your-cdn.com/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?w=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;imageLoader&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"photo.jpg"&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Description"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Verify SSG Compatibility
&lt;/h3&gt;

&lt;p&gt;Ensure your project doesn't use these incompatible features:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cannot use:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Route Handlers (API Routes) - require server runtime&lt;/li&gt;
&lt;li&gt;Server-Side Rendering (SSR) - e.g., &lt;code&gt;cache: 'no-store'&lt;/code&gt; in fetches&lt;/li&gt;
&lt;li&gt;Incremental Static Regeneration (ISR) - &lt;code&gt;revalidate&lt;/code&gt; property&lt;/li&gt;
&lt;li&gt;Dynamic data without &lt;code&gt;generateStaticParams&lt;/code&gt; — including any data fetched from “dynamic APIs” that depend on runtime values (see Next.js guide on &lt;a href="https://nextjs.org/docs/app/guides/caching#dynamic-rendering" rel="noopener noreferrer"&gt;dynamic rendering&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Must implement:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All dynamic routes need &lt;code&gt;generateStaticParams&lt;/code&gt; to pre-generate pages&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;cache: 'force-cache'&lt;/code&gt; for data fetching at build time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example for dynamic routes:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/blog/[slug]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStaticParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;first-post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;second-post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BlogPost&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Blog Post: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test Locally
&lt;/h3&gt;

&lt;p&gt;Verify your static export works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
npx serve out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;http://localhost:3000&lt;/code&gt; to ensure everything renders correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Deploy to AWS Amplify
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Push to Git
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Configure Next.js for SSG deployment"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Access AWS Amplify Console
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Log into &lt;a href="https://console.aws.amazon.com/" rel="noopener noreferrer"&gt;AWS Console&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Search for "Amplify" in the services search bar&lt;/li&gt;
&lt;li&gt;Click "Get Started" under "Amplify Hosting"&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Connect Your Repository
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Select your Git provider (GitHub, GitLab, Bitbucket, or AWS CodeCommit)&lt;/li&gt;
&lt;li&gt;Authorize AWS Amplify to access your repositories&lt;/li&gt;
&lt;li&gt;Choose the repository and branch to deploy&lt;/li&gt;
&lt;li&gt;Click "Next"&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Configure Build Settings
&lt;/h3&gt;

&lt;p&gt;Amplify should auto-detect Next.js, but verify your &lt;code&gt;amplify.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;phases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;preBuild&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;baseDirectory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;out&lt;/span&gt;
    &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*"&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules/**/*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Critical settings:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;baseDirectory: out&lt;/code&gt; - Next.js static export directory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm ci&lt;/code&gt; - Consistent, reproducible builds&lt;/li&gt;
&lt;li&gt;Cache &lt;code&gt;node_modules&lt;/code&gt; for faster builds&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Add Environment Variables (Optional)
&lt;/h3&gt;

&lt;p&gt;If your app needs environment variables, add them in "Advanced settings":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_ANALYTICS_ID=G-XXXXXXXXXX
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Remember&lt;/strong&gt;: Only &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; variables are accessible in the browser. Server-side variables are available during build time only.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Review settings&lt;/li&gt;
&lt;li&gt;Click "Save and deploy"&lt;/li&gt;
&lt;li&gt;Monitor the build progress (typically 3-5 minutes)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once complete, you'll get a URL like: &lt;code&gt;https://[branch].[app-id].amplifyapp.com&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Configure Redirects &amp;amp; Routing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fix 404 Pages
&lt;/h3&gt;

&lt;p&gt;Configure redirects in Amplify console under "Rewrites and redirects":&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 1: Custom 404 page&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source: &amp;lt;/^[^.]+$|\\.(?!(css|gif|ico|jpg|js|png|txt|svg|woff|woff2|ttf|map|json|webp)$)([^.]+$)/&amp;gt;
Target: /404.html
Status: 404

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule 2: Client-side routing fallback&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source: /&amp;lt;*&amp;gt;
Target: /index.html
Status: 200 (Rewrite)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Place Rule 2 last - Amplify processes rules in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Handle Common Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Environment Variables
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Client Components&lt;/strong&gt; (with &lt;code&gt;"use client"&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can only access &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; variables&lt;/li&gt;
&lt;li&gt;Variables are embedded in the build output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Server Components&lt;/strong&gt; (default):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can access all environment variables at build time&lt;/li&gt;
&lt;li&gt;Never expose secrets in &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; variables&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Forms and Interactive Features
&lt;/h3&gt;

&lt;p&gt;SSG sites need external services for dynamic functionality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Forms&lt;/strong&gt;: Formspree, Web3Forms, AWS Lambda&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search&lt;/strong&gt;: FlexSearch, Fuse.js, Algolia, Meilisearch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comments&lt;/strong&gt;: Giscus, Utterances, Disqus&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt;: Clerk, Auth0, AWS Cognito&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Next.js 16 App Router Features
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Works perfectly with SSG:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Metadata API&lt;/li&gt;
&lt;li&gt;Route Groups&lt;/li&gt;
&lt;li&gt;Loading and Error UI&lt;/li&gt;
&lt;li&gt;Server Components (data fetched at build time)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Requires pre-generation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic routes (use &lt;code&gt;generateStaticParams&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Parallel and Intercepting Routes (all combinations must be pre-generated)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting Common Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Build Fails: "Output directory not found"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Verify &lt;code&gt;amplify.yml&lt;/code&gt; has &lt;code&gt;baseDirectory: out&lt;/code&gt; and &lt;code&gt;next.config.ts&lt;/code&gt; has &lt;code&gt;output: 'export'&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Images Not Loading
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Ensure &lt;code&gt;images.unoptimized: true&lt;/code&gt; in config. Images should be in &lt;code&gt;public/&lt;/code&gt; directory and referenced without the &lt;code&gt;public&lt;/code&gt; prefix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blank Page After Deployment
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Check browser console for errors. Usually means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix on environment variables&lt;/li&gt;
&lt;li&gt;Client-side JavaScript error&lt;/li&gt;
&lt;li&gt;Incorrect build output&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dynamic Routes Return 404
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Implement &lt;code&gt;generateStaticParams&lt;/code&gt; for all dynamic routes&lt;/li&gt;
&lt;li&gt;Configure redirect rules in Amplify console&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Environment Variables Not Working
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client-side variables must use &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix&lt;/li&gt;
&lt;li&gt;Set in Amplify Console under "Environment variables"&lt;/li&gt;
&lt;li&gt;Rebuild after adding new variables&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When NOT to Use This Approach
&lt;/h2&gt;

&lt;p&gt;Consider alternatives if you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incremental Static Regeneration (ISR)&lt;/strong&gt;: Requires Node.js server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-Side Rendering (SSR)&lt;/strong&gt;: Real-time, per-request rendering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Routes&lt;/strong&gt;: Need serverless functions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-demand Revalidation&lt;/strong&gt;: Dynamic content updates without rebuilds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge Middleware&lt;/strong&gt;: Complex routing logic at the edge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For these use cases, consider Vercel, AWS Amplify with SSR configuration, or AWS App Runner/ECS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;After deployment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add custom domain&lt;/strong&gt;: In Amplify console under "Domain management"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up monitoring&lt;/strong&gt;: Use CloudWatch for build and deployment alerts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure preview deployments&lt;/strong&gt;: For pull requests and feature branches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize costs&lt;/strong&gt;: Review bandwidth usage and optimize images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test thoroughly&lt;/strong&gt;: Verify all pages and features work as expected&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AWS Amplify provides a cost-effective platform for deploying Next.js 16 SSG applications with proper configuration. The key is understanding SSG limitations, configuring Next.js correctly, and setting up Amplify's build and routing properly.&lt;/p&gt;

&lt;p&gt;While the setup requires more configuration than Vercel, the cost savings (up to 96% for small sites) and lack of per-seat pricing make it an excellent choice for businesses looking to optimize their hosting costs without sacrificing performance or reliability.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>aws</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Partial Catchment Delineation for Inundation Modeling</title>
      <dc:creator>Aleksy Bohdziul</dc:creator>
      <pubDate>Thu, 19 Feb 2026 07:34:53 +0000</pubDate>
      <link>https://forem.com/u11d/partial-catchment-delineation-for-inundation-modeling-275</link>
      <guid>https://forem.com/u11d/partial-catchment-delineation-for-inundation-modeling-275</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;A System-Oriented Approach to Scalable Hydrological Feature Engineering&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Traditional watershed delineation wasn't designed for machine learning at scale. The standard approach treats each watershed as a complete, self-contained unit, which makes sense when you're studying individual rivers. But it creates real problems when you need to train models across hundreds of locations.&lt;/p&gt;

&lt;p&gt;We ran into this on a recent flood prediction project. The study area had almost no hydrological data, no stream gauges, no riverbed surveys, nothing you'd typically use for hydraulic modeling. So we trained an LSTM model to predict discharge instead. It worked, but there was a catch: the model could only predict flow at watershed outlets. One outlet per watershed meant we didn't have enough training points.&lt;/p&gt;

&lt;p&gt;The obvious solution was to create more outlets by subdividing watersheds along the river. But traditional catchment boundaries overlap heavily when you do this, which breaks parallel processing and makes selective updates nearly impossible. We needed many independent spatial units for the ML model, but we also needed to preserve the downstream flow aggregation that hydrological modeling depends on.&lt;/p&gt;

&lt;p&gt;Our solution was to delineate watersheds at the reach level instead. Each reach gets its own partial catchment, smaller units that remain hydrologically valid but can be computed and updated independently. It's a compromise between what the data pipeline needs and what the hydrology requires.&lt;/p&gt;

&lt;p&gt;Getting the geometry right was only part of the problem. We tested multiple DEM sources, modified the river network repeatedly, and needed to recompute catchments constantly during development. Treating delineation as a one-time preprocessing step wasn't viable. We needed to version every intermediate result, recompute selectively when inputs changed, and compare outputs across iterations.&lt;/p&gt;

&lt;p&gt;This pushed us toward Dagster's asset model. Instead of treating catchments as temporary pipeline outputs, we manage them as persistent spatial assets with explicit dependencies and lineage tracking.&lt;/p&gt;

&lt;p&gt;The following sections cover the hydrological rationale, the technical implementation, and how asset orchestration made this approach practical for production use.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Catchment Delineation in the Context of Inundation Modeling&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Inundation modeling relies on an accurate representation of how water accumulates and propagates through a river network. Traditionally, this begins with watershed delineation derived from a digital elevation model, followed by hydraulic simulation over the resulting domain. When applied to large regions, however, this workflow introduces practical limitations. Entire catchments must be processed as single units, even when only a small portion of the river network is relevant for a given prediction or model update.&lt;/p&gt;

&lt;p&gt;From a data engineering perspective, this creates an undesirable coupling between upstream and downstream regions. A change in DEM preprocessing, stream burning strategy, or river vector alignment forces recomputation of large spatial extents, even when the change is localized. This coupling becomes a bottleneck when experimenting with multiple configurations or when operating a system that must adapt continuously to new data.&lt;/p&gt;

&lt;p&gt;The core insight behind partial catchment delineation is that hydraulic dependency flows downstream, but computational dependency does not need to. By separating catchments into smaller, reach-aligned units, it becomes possible to preserve hydrological correctness while dramatically improving computational flexibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Core Idea: Reach-Level and Progressive Catchments&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We segmented each river into short reaches (100 m) and delineated a catchment for each reach. From those building blocks we constructed larger, progressively downstream catchments.&lt;/p&gt;

&lt;p&gt;The method introduced here distinguishes between two complementary spatial constructs: reach-level catchments and progressive catchments.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Reach Catchments (non-overlapping)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Reach-level catchments are defined for individual river segments, bounded upstream by the nearest confluence and downstream by the segment's outlet. These units do not overlap and collectively partition the drainage area of the river network. Their non-overlapping nature makes them well suited for parallel processing, independent feature extraction, and localized recomputation.&lt;/p&gt;

&lt;p&gt;Visualize the landscape divided into narrow, adjacent drainage areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each reach catchment drains only to its own 100 m river segment&lt;/li&gt;
&lt;li&gt;None of them include upstream contributions&lt;/li&gt;
&lt;li&gt;Their boundaries tile the basin without overlaps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxmhs84we4q135s0n9nr4.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxmhs84we4q135s0n9nr4.gif" alt="Reach catchments" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Progressive Catchments (overlapping by design)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Progressive catchments, by contrast, represent the cumulative upstream area contributing to a given river reach. Each progressive catchment is constructed by aggregating all upstream reach-level catchments along the river network. This structure mirrors traditional hydrological reasoning and provides a direct bridge to downstream hydraulic modeling.&lt;/p&gt;

&lt;p&gt;Now start combining those catchments as you move downstream:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Progressive Catchment 1 = Reach 1&lt;/li&gt;
&lt;li&gt;Progressive Catchment 2 = Reach 1 + Reach 2&lt;/li&gt;
&lt;li&gt;Progressive Catchment 3 = Reach 1 + Reach 2 + Reach 3&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Visually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the first progressive catchment is small and upstream&lt;/li&gt;
&lt;li&gt;each subsequent one contains the previous&lt;/li&gt;
&lt;li&gt;downstream catchments fully envelop upstream ones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why we call them progressive: each one represents the basin area contributing flow up to that point along the river.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnjs1waprdtg2luvsg64z.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnjs1waprdtg2luvsg64z.gif" alt="Progressive catchments&amp;lt;br&amp;gt;
" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By maintaining both representations explicitly, the system can operate at two levels simultaneously. Reach-level catchments support scalable computation and machine learning workflows, while progressive catchments preserve the physical context required for inundation modeling.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Why not delineate progressive catchments directly?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;We could have. It would actually be simpler than whatever we're actually doing. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We also needed the reach catchments for inundation simulation later&lt;/li&gt;
&lt;li&gt;Running delineation logic twice felt like a smell&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So we delineate once, and compose later.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Tributaries&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Where a tributary joins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;its reach catchments are merged into the progressive catchment only after the confluence&lt;/li&gt;
&lt;li&gt;upstream progressive catchments on the main stem remain unaffected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reach catchments are spatial building blocks&lt;/li&gt;
&lt;li&gt;progressive catchments are cumulative assemblies of those blocks&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;What the Two Catchment Types Are Used For&lt;/strong&gt;
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Progressive catchments → model training&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Our LSTM predicts discharge at outlet points. For each progressive catchment, we derive features such as precipitation, temperature, humidity, pressure, the aridity index, and others.&lt;/p&gt;

&lt;p&gt;All inputs are provided as raster datasets. Catchment geometries are used as spatial masks to extract and aggregate pixel values.&lt;/p&gt;

&lt;p&gt;This workflow requires repeated spatial joins, raster masking, and temporal aggregation over large geospatial datasets. We implement and orchestrate these pipelines using Dagster, which allows us to manage dependencies, partition computations, and scale processing across large spatial-temporal datasets.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Reach catchments → inundation mapping&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Each reach gets its own discharge estimate (derived from differences between progressive catchments), which later feeds the inundation simulation.&lt;/p&gt;

&lt;p&gt;Once reach-level and progressive catchments are established, they become the foundation for feature extraction. Terrain attributes, land cover statistics, soil properties, and hydrological indices can be computed independently for each reach-level catchment. These features serve as inputs to machine learning models predicting discharge or inundation extent.&lt;/p&gt;

&lt;p&gt;Progressive catchments then provide a natural mechanism for aggregating upstream contributions. Features derived at the reach level can be accumulated downstream in a controlled, traceable manner. This separation simplifies both training and inference: models operate on consistent, non-overlapping units, while hydraulic context is reintroduced through aggregation.&lt;/p&gt;

&lt;p&gt;At this stage, the delineation method transitions from a GIS exercise into a data orchestration problem. Each derived feature depends on specific preprocessing choices, spatial units, and upstream dependencies. Managing these relationships manually quickly becomes infeasible.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;DEM (Digital Elevation Model) Preprocessing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Implementing partial catchment delineation at high spatial resolution exposes a range of practical challenges. Reliable catchment delineation depends far more on DEM preprocessing than on the delineation algorithm itself.&lt;/p&gt;

&lt;p&gt;High-resolution DEMs (1 m × 1 m in our case) amplify artifacts that are negligible at coarser scales, including spurious sinks, artificial barriers, and noise-induced flow paths. Stream burning and sink filling become necessary, but their parameters introduce additional degrees of freedom that affect downstream results.&lt;/p&gt;

&lt;p&gt;Below we summarize the preprocessing steps that proved essential for stable and repeatable results.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Depression filling&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Raw DEMs frequently contain spurious sinks caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;measurement noise&lt;/li&gt;
&lt;li&gt;vegetation and built structures&lt;/li&gt;
&lt;li&gt;interpolation artifacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Left untreated, these sinks interrupt downstream connectivity and lead to fragmented or incomplete catchments. We therefore applied depression filling prior to any flow calculations.&lt;/p&gt;

&lt;p&gt;Our goal was not to aggressively flatten terrain, but to ensure continuous drainage paths with minimal elevation modification. Priority-flood-style algorithms worked well in practice and preserved overall terrain structure.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Stream burning&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Even after sink removal, we observed inconsistencies between modeled flow paths and known river locations. To address this, we burned the vector river network into the DEM by lowering elevations along river centerlines.&lt;/p&gt;

&lt;p&gt;Aligning raster-based flow accumulation with vector river networks proved particularly sensitive. Small positional discrepancies between datasets can lead to misaligned pour points, fragmented catchments, or unrealistic drainage patterns. These issues are not purely geometric; they directly influence the stability and reproducibility of downstream features.&lt;/p&gt;

&lt;p&gt;This step serves two purposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it enforces hydrologically plausible drainage paths&lt;/li&gt;
&lt;li&gt;it reduces sensitivity to small elevation errors in flat or low-gradient terrain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stream burning significantly improved watershed stability, especially near confluences and in wide floodplains where DEM gradients are weak.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Flow accumulation and its limitations&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;We initially experimented with flow accumulation to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;identify channelized flow paths&lt;/li&gt;
&lt;li&gt;snap pour points automatically to areas of high contributing area&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, the high spatial resolution of the DEM (1 m × 1 m) introduced significant noise into flow accumulation outputs. Minor elevation perturbations resulted in fragmented or unrealistic accumulation patterns, making automated snapping unreliable.&lt;/p&gt;

&lt;p&gt;As a result, we limited the use of flow accumulation and instead relied more heavily on burned-in river vectors and explicit reach endpoints for pour point placement.&lt;/p&gt;

&lt;p&gt;During later experiments we found that using D-infinity for calculating flow paths rather than D8 significantly improved flow accumulation calculation, but due to it being discovered too late, we weren't able to implement it before the end of first phase of the project.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Spatial alignment issues&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;During development we discovered small but still significant horizontal offsets between the DEM and the river vector dataset, on the order of a few meters. At some point we discovered that our river geometries were offset by a couple of meters relative to the DEM.&lt;/p&gt;

&lt;p&gt;These discrepancies led to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pour points falling outside effective drainage paths&lt;/li&gt;
&lt;li&gt;unstable catchment boundaries&lt;/li&gt;
&lt;li&gt;inconsistent results across neighboring reaches&lt;/li&gt;
&lt;li&gt;weird catchment boundaries&lt;/li&gt;
&lt;li&gt;several hours of existential doubt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While stream burning mitigated some of these effects, resolving DEM-vector alignment remains an important area for future improvement. This is still on our list of things to fix properly. For now, burning rivers into DEM somewhat alleviated the issue.&lt;/p&gt;

&lt;p&gt;Rather than attempting to eliminate these uncertainties entirely, we treated them as explicit dimensions of experimentation. Different preprocessing strategies were preserved as separate artifacts, allowing their effects to be compared systematically. This approach only becomes feasible when intermediate results are treated as first-class entities rather than overwritten pipeline outputs.&lt;/p&gt;

&lt;p&gt;Overall, careful DEM preprocessing proved essential not only for hydrologic correctness, but also for producing geometries stable enough to support downstream machine-learning workflows.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Implementation Outline&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Below is a cleaned-up, simplified sketch of the workflow. The real code is longer, louder, and contains more comments written at 2 a.m.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 1. Load DEM and preprocess
filled_dem = fill_depressions(dem)
burned_dem = burn_streams(filled_dem, river_lines)
flow_dir   = d8_flow_direction(burned_dem)

# 2. Split rivers into fixed-length reaches
reaches = split_lines(river_lines, segment_length=100)

# 3. Create pour points at reach outlets
pour_points = reaches.geometry.apply(get_downstream_endpoint)

# 4. Delineate reach catchments
reach_catchments = delineate_watersheds(
    flow_dir=flow_dir,
    pour_points=pour_points
)

# 5. Build progressive catchments
progressive = []
current = None
for reach in ordered_downstream(reach_catchments):
    current = reach if current is None else union(current, reach)
    progressive.append(current)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The devil, as always, lives in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tributary joins&lt;/li&gt;
&lt;li&gt;reach ordering&lt;/li&gt;
&lt;li&gt;and spatial indexing performance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Joining tributaries means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;identifying parent-child relationships between reaches&lt;/li&gt;
&lt;li&gt;merging reach catchments in the correct downstream order&lt;/li&gt;
&lt;li&gt;avoiding double-counting areas&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Asset-Based Orchestration of Spatial Dependencies&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To make this workflow operational, we modeled reach-level catchments, progressive catchments, and derived features as explicit assets within Dagster. Each asset represents a durable spatial artifact with well-defined dependencies on upstream inputs. Changes in DEM preprocessing, river network alignment, or feature definitions propagate through the asset graph in a controlled way.&lt;/p&gt;

&lt;p&gt;This asset-oriented approach allows recomputation to be both selective and explainable. When a preprocessing parameter changes, only the affected reach-level catchments and their downstream aggregates are recomputed. Historical artifacts remain available for comparison, enabling systematic evaluation of alternative configurations.&lt;/p&gt;

&lt;p&gt;Dagster's lineage tracking plays a critical role here. Each feature can be traced back through the chain of spatial transformations that produced it, providing transparency during debugging and model validation. Rather than reasoning about pipeline execution order, the system reasons about data state and dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Operational Implications&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Treating partial catchment delineation as an orchestrated asset graph changes the operational profile of inundation modeling workflows. Iteration becomes cheaper because recomputation is localized. Failures become easier to diagnose because dependencies are explicit. Experimentation becomes safer because previous states are preserved rather than overwritten.&lt;/p&gt;

&lt;p&gt;Perhaps most importantly, this approach aligns hydrological reasoning with modern data platform design. Physical dependencies are respected, but they no longer dictate computational coupling. The system can evolve incrementally, accommodating new data sources, preprocessing strategies, and modeling approaches without requiring full recomputation of the spatial domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Lessons Learned&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;DEM preprocessing is very important&lt;/li&gt;
&lt;li&gt;1 m DEMs are great until you compute derivatives&lt;/li&gt;
&lt;li&gt;River vectors and DEMs rarely agree — believe neither blindly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Segmenting rivers into reach-level catchments gave us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more training points&lt;/li&gt;
&lt;li&gt;spatially consistent features&lt;/li&gt;
&lt;li&gt;and a clean bridge between ML discharge prediction and inundation modeling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Partial catchment delineation proved valuable not because it produced a single optimal representation of a watershed, but because it enabled a shift in how spatial dependencies are managed at scale. By decomposing watersheds into reach-level units and reconstructing downstream context through progressive aggregation, we gained a representation that supports both hydrological correctness and computational scalability.&lt;/p&gt;

&lt;p&gt;The effectiveness of this approach ultimately depended on its orchestration. Without an asset-oriented framework, the complexity introduced by multiple delineation strategies and iterative experimentation would quickly become unmanageable. By modeling spatial artifacts explicitly and preserving their lineage, we were able to integrate hydrology, machine learning, and geospatial preprocessing into a coherent, production-ready system.&lt;/p&gt;

&lt;p&gt;If nothing else, this workflow taught us humility, patience, and how many ways water can refuse to flow downhill.&lt;/p&gt;

&lt;p&gt;While this article focused on inundation modeling, the underlying pattern extends to any domain where high-resolution geospatial data meets iterative, data-driven workflows. Partial decomposition of space, combined with asset-based orchestration, offers a practical path toward scalable and trustworthy spatial modeling systems.&lt;/p&gt;

</description>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Architecting and Operating Geospatial Workflows with Dagster</title>
      <dc:creator>Daniel Kraszewski</dc:creator>
      <pubDate>Tue, 17 Feb 2026 12:00:00 +0000</pubDate>
      <link>https://forem.com/u11d/architecting-and-operating-geospatial-workflows-with-dagster-5a52</link>
      <guid>https://forem.com/u11d/architecting-and-operating-geospatial-workflows-with-dagster-5a52</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft66s3rvtueth0eqvbwrs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft66s3rvtueth0eqvbwrs.png" alt="A Technical Deep Dive into Asset-Based Orchestration for Production Geospatial Data Platforms" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is a technical companion to our earlier essay on geospatial data orchestration (link: &lt;a href="https://u11d.com/blog/geospatial-data-orchestration/" rel="noopener noreferrer"&gt;https://u11d.com/blog/geospatial-data-orchestration/&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Geospatial data pipelines present a distinct set of challenges that traditional ETL frameworks struggle to address. Raster datasets measured in gigabytes, coordinate reference system transformations, temporal partitioning across decades of observations, and the need for reproducible lineage across every derived artifact-these requirements demand an orchestration model that treats data as the primary actor rather than a byproduct of task execution. This article describes the architecture we developed for water management analytics: a system that ingests elevation models, meteorological observations, satellite imagery, and forecast data to support flood prediction and hydrological modeling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The System Boundary
&lt;/h2&gt;

&lt;p&gt;The platform serves as a data preparation layer, not a model serving infrastructure. Its responsibility begins at external data sources-WFS endpoints, FTP servers, governmental APIs-and ends at materialized artifacts in object storage ready for consumption by downstream ML training pipelines and QGIS-based analysts. We deliberately exclude real-time inference, user-facing APIs, and visualization from this system's scope.&lt;/p&gt;

&lt;p&gt;Two Dagster projects comprise the workspace. The &lt;code&gt;landing-zone&lt;/code&gt; project handles all data ingestion and transformation: elevation tiles, meteorological station observations, satellite imagery, climatic indices, and forecast GRIB files. The &lt;code&gt;discharge-model&lt;/code&gt; project consumes these prepared datasets to train neural network models for water discharge prediction using NeuralHydrology. Both projects share a common library under &lt;code&gt;shared/&lt;/code&gt; containing IO managers, resource definitions, helper utilities, and cross-project asset references.&lt;/p&gt;

&lt;p&gt;The platform does not manage model deployment, handle user authentication, or serve predictions. Those responsibilities belong to separate systems that consume our outputs via S3-compatible object storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Types and Access Patterns
&lt;/h2&gt;

&lt;p&gt;Three fundamental data types flow through the system, each with distinct storage and access characteristics.&lt;/p&gt;

&lt;p&gt;Raster data-digital elevation models, satellite imagery, land cover maps-dominates storage volume. We store all rasters as Cloud Optimized GeoTIFFs (COGs) in S3, enabling range-request access for partial reads. The elevation pipeline alone processes tiles from multiple national geodetic services, converting formats like ARC/INFO ASCII Grid and XYZ point clouds into standardized COGs with consistent coordinate reference systems. A single consolidated elevation model can exceed several gigabytes.&lt;/p&gt;

&lt;p&gt;Tabular data encompasses meteorological observations, hydrological measurements, station metadata, and computed indices. We standardize on Parquet with zstd compression, leveraging Polars for in-process transformations and DuckDB for SQL-based quality checks. A custom IO manager handles serialization of both raw DataFrames and Pydantic model collections, automatically recording row counts and column schemas as Dagster metadata.&lt;/p&gt;

&lt;p&gt;Vector data-catchment boundaries, station locations, regional polygons-exists primarily as intermediate artifacts used for spatial joins and raster clipping operations. Shapely geometries serialize alongside tabular records in Parquet files, with coordinate transformations handled through pyproj.&lt;/p&gt;

&lt;p&gt;All data resides in S3-compatible object storage. The storage layout follows a predictable convention: &lt;code&gt;s3://{bucket}/{asset-path}/{partition-keys}/filename.{ext}&lt;/code&gt;, where asset paths derive directly from Dagster asset keys. This enables both programmatic access through IO managers and ad-hoc exploration via S3 browsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asset Graph Design
&lt;/h2&gt;

&lt;p&gt;Every durable artifact in the system maps to a Dagster asset. This is not a philosophical preference but a practical requirement: when a hydrologist questions why a model prediction differs from last month's, we need to trace backward through the exact elevation tiles, meteorological observations, and climatic indices that produced those training features.&lt;/p&gt;

&lt;p&gt;Asset naming follows a hierarchical convention reflecting data lineage. Elevation data progresses through &lt;code&gt;elevation/dtm/wfs_index&lt;/code&gt; → &lt;code&gt;elevation/dtm/raw_tiles&lt;/code&gt; → &lt;code&gt;elevation/dtm/converted_tiles&lt;/code&gt; → &lt;code&gt;elevation/dtm/consolidated&lt;/code&gt;. Each stage represents a distinct, independently materializable artifact with its own partitioning strategy. The &lt;code&gt;shared/assets/landing_zone.py&lt;/code&gt; module maintains a centralized registry of asset keys, enabling type-safe cross-project references:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ELEVATION_DSM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AssetKey&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;elevation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dsm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;ELEVATION_DTM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AssetKey&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;elevation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dtm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;FORECAST_GRIB_RAW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AssetKey&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;forecast&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grib&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;CLIMATIC_INDICES_CATCHMENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AssetKey&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;climatic_indices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;catchment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dependencies between assets express both data flow and materialization order. The converted elevation tiles asset explicitly declares its dependency on raw tiles through the ins parameter, ensuring Dagster's asset graph correctly represents the relationship and prevents stale data from propagating downstream.&lt;/p&gt;

&lt;p&gt;We employ Dagster's component pattern for assets that share structural similarities but operate on different data. The elevation pipeline defines Converted as a component that can be instantiated for both DTM and DSM processing, sharing conversion logic while maintaining separate asset keys and partition spaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Partitioning Strategy
&lt;/h2&gt;

&lt;p&gt;Partitioning serves two purposes: it bounds the scope of individual materializations to manageable sizes, and it enables incremental updates without full recomputation. We use different partitioning strategies depending on the data's natural structure.&lt;/p&gt;

&lt;p&gt;Elevation data partitions spatially by tile grid and region. A &lt;code&gt;MultiPartitionsDefinition&lt;/code&gt; combines a tile index dimension with a regional dimension, allowing selective materialization of specific geographic areas. Dynamic partition definitions enable the tile catalog to grow without code changes-sensors read from index parquet files and issue &lt;code&gt;AddDynamicPartitionsRequest&lt;/code&gt; calls to register new partitions.&lt;/p&gt;

&lt;p&gt;Satellite imagery partitions temporally using year and month dimensions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_multipartitions_def&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MultiPartitionsDefinition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MultiPartitionsDefinition&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indices_partition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;regions_partition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New temporal partitions register automatically through a sensor that monitors the catalog file for previously unseen year/month combinations.&lt;/p&gt;

&lt;p&gt;Meteorological observations partition by data source and processing stage rather than by time. Then pipeline uses a sync-plan/execute-plan pattern where a planning asset determines which source files need synchronization, and an execution asset processes only the delta. This approach handles the irregular update patterns of governmental data sources more gracefully than fixed temporal partitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Raster Processing Pipeline
&lt;/h2&gt;

&lt;p&gt;The raster processing subsystem converts heterogeneous input formats into standardized COGs suitable for ML feature extraction. A dedicated module provides the core transformation utilities, built on GDAL and Rasterio.&lt;/p&gt;

&lt;p&gt;The main COG writing function orchestrates the complete transformation: coordinate reprojection, resolution resampling, nodata gap filling, geometry clipping, and overview generation. Memory management is critical—we process large rasters block-wise to avoid loading entire datasets into RAM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_fill_nodata_gaps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DatasetWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_search_distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;smoothing_iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fill nodata gaps block-wise to avoid loading the whole raster into memory.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;block_windows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;masked&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_masked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uint8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;filled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fillnodata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fill_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodata&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_search_distance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_search_distance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;smoothing_iterations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;smoothing_iterations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Format-specific converters handle the idiosyncrasies of source data. The XYZ converter detects axis ordering issues in point cloud data, the ASCII grid converter parses ESRI's legacy format, and the VRT builder creates virtual rasters for multi-file operations. Each converter produces consistent metadata that downstream assets can rely on.&lt;/p&gt;

&lt;p&gt;For smaller rasters below a configurable threshold, we process entirely in memory using Rasterio's &lt;code&gt;MemoryFile&lt;/code&gt;. Larger rasters write to temporary files before final COG copy to S3 via GDAL's &lt;code&gt;/vsis3/&lt;/code&gt; virtual filesystem driver. This dual-path approach optimizes for both small-tile throughput and large-raster memory safety.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Quality as Dependencies
&lt;/h2&gt;

&lt;p&gt;Data quality checks execute as first-class Dagster asset checks, not as afterthoughts in logging statements. The climatic indices pipeline defines sixteen distinct checks covering structural integrity, range validation, and business logic constraints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dg.multi_asset_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;specs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AssetCheckSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;non_empty_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CLIMATIC_INDICES_CATCHMENT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AssetCheckSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;primary_key_uniqueness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CLIMATIC_INDICES_CATCHMENT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AssetCheckSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pet_avg_valid_range&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CLIMATIC_INDICES_CATCHMENT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AssetCheckSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aridity_index_avg_valid_range&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CLIMATIC_INDICES_CATCHMENT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;# ... additional checks
&lt;/span&gt;    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;catchment_climatic_indices_checks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_catchment_climatic_indices_checks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AssetCheckExecutionContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DuckDBResourceExtended&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AssetCheckResult&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# Checks execute SQL against DuckDB, returning structured results
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checks validate domain-specific constraints: potential evapotranspiration must fall within 0-15 mm/day, aridity indices between 0-5, precipitation seasonality between -1 and +1. Each check returns structured metadata-not just pass/fail, but the actual values found, enabling rapid diagnosis when checks fail.&lt;/p&gt;

&lt;p&gt;A helper function loads the asset's parquet file into a DuckDB temporary table, enabling SQL-based validation without loading the entire dataset into Python memory. This pattern scales to multi-million row datasets while keeping check execution times reasonable.&lt;/p&gt;

&lt;p&gt;Geometry validation occurs inline during raster conversion. A dedicated validation function compares source and destination bounds, skipping tiles where reprojection would introduce unacceptable distortion. Rather than failing the entire partition, we record skipped tiles with explicit reasons, allowing manual review of edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Execution and Elasticity
&lt;/h2&gt;

&lt;p&gt;The platform runs locally for development using &lt;code&gt;uv run dg dev&lt;/code&gt; and deploys to Kubernetes for production workloads. Resource requirements vary dramatically across asset types-a metadata sync might need 256MB of memory, while elevation tile conversion demands 16GB.&lt;/p&gt;

&lt;p&gt;We express resource requirements through operation tags that map to Kubernetes node pools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;K8sOpTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nd"&gt;@staticmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;xlarge&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;K8sOpTags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;M6I_2XLARGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;divider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@staticmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;K8sOpTags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;G4DN_2XLARGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;divider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;divider&lt;/code&gt; parameter enables fractional node allocation-running two medium workloads on a single large instance, or dedicating an entire GPU node to model training. Assets declare their requirements via &lt;code&gt;op_tags=K8sOpTags.xlarge()&lt;/code&gt;, and the Kubernetes executor schedules pods accordingly.&lt;/p&gt;

&lt;p&gt;Concurrent processing within assets uses a custom worker pool implementation that handles the messy realities of I/O-bound geospatial work: network timeouts, partial failures, and graceful cancellation. The worker pool provides retry policies, progress logging, and fail-fast behavior while aggregating per-item metrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;worker_pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cancellation_event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cancellation_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metrics_extractor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;size_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_if_failures&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker pool tracks success, failure, skip, and cancellation states for each item, building aggregate statistics that appear in Dagster's materialization metadata. When processing fails partway through, we know exactly which items succeeded and which need retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability and Lineage
&lt;/h2&gt;

&lt;p&gt;Every asset materialization records structured metadata: row counts for tabular data, dimensions and CRS for rasters, processing duration, and custom metrics like total bytes written. The custom IO manager automatically captures table schemas and row counts; raster assets explicitly record resolution, bounds, and file sizes.&lt;/p&gt;

&lt;p&gt;Sensors provide the observability layer for external data sources. The satellite data partition sensor polls catalog files every 30 seconds, logging new partition discoveries and registration requests. Forecast schedules run four times daily at fixed UTC times, with run keys that encode the scheduled timestamp for easy identification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dg.schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;cron_schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30 8,13,16,20 * * *&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AssetSelection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORECAST_GRIB_RAW&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forecast_grib_raw_schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ScheduleEvaluationContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;scheduled_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheduled_execution_time&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RunRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;run_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;forecast_grib_raw_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scheduled_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y%m%d_%H%M&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schedule&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;forecast_grib_raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The declarative automation condition enables declarative freshness policies-assets can specify how stale they're allowed to become, and Dagster automatically triggers materializations to maintain freshness. We use this for assets that need to stay current with upstream changes without manual intervention.&lt;/p&gt;

&lt;p&gt;Asset lineage flows automatically from dependency declarations. When investigating a model prediction, we can trace from the model asset back through training data, through climatic indices, through individual station observations, to the original source files. This lineage persists across runs, enabling historical comparisons when methodology changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons from Production: Architectural Revisions
&lt;/h2&gt;

&lt;p&gt;Three architectural decisions required significant revision after initial deployment.&lt;/p&gt;

&lt;p&gt;First, we underestimated the memory requirements for raster operations. Early implementations loaded entire tiles into memory, which worked fine for small test datasets but caused OOM kills on production-scale elevation models. The fix required systematic refactoring to block-wise processing throughout the raster pipeline-reading, transforming, and writing in chunks that fit within pod memory limits. This added complexity but eliminated an entire class of production incidents.&lt;/p&gt;

&lt;p&gt;Second, we initially implemented dynamic partitions without proper cleanup logic. Sensors would happily add new partitions as data arrived, but nothing removed partitions for data that had been superseded or corrected. Over time, the partition space accumulated stale entries that confused operators and wasted storage. We added explicit partition lifecycle management: sensors now compare desired partitions against current state and issue both &lt;code&gt;AddDynamicPartitionsRequest&lt;/code&gt; and &lt;code&gt;DeleteDynamicPartitionsRequest&lt;/code&gt; as needed.&lt;/p&gt;

&lt;p&gt;Third, we placed too much validation logic inside asset compute functions rather than as separate asset checks. This made debugging failures difficult-a validation error would fail the entire materialization, losing the partial work already completed. Extracting validation into dedicated asset checks with structured metadata output dramatically improved debuggability and allowed us to materialize "known-bad" data when necessary for investigation, running checks separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Questions and Next Steps
&lt;/h2&gt;

&lt;p&gt;Several architectural questions remain unresolved in the current implementation.&lt;/p&gt;

&lt;p&gt;The platform lacks a unified approach to schema evolution. When upstream data sources change their formats-which governmental APIs do without warning-we currently handle it through ad-hoc converter updates. A more systematic approach might involve schema registries or versioned asset definitions, but the right pattern for geospatial data with complex nested structures remains unclear.&lt;/p&gt;

&lt;p&gt;Cross-project asset dependencies work through shared asset key definitions, but the materialization coordination relies on manual scheduling or external triggers. A more elegant solution might use Dagster's cross-code-location asset dependencies, but this would require restructuring how projects deploy and discover each other's assets.&lt;/p&gt;

</description>
      <category>dagster</category>
      <category>dataengineering</category>
      <category>python</category>
      <category>geospatial</category>
    </item>
    <item>
      <title>The Memory of Water: Why LSTMs Demand Polished Data</title>
      <dc:creator>Paweł Sławacki</dc:creator>
      <pubDate>Wed, 04 Feb 2026 13:02:30 +0000</pubDate>
      <link>https://forem.com/u11d/the-memory-of-water-why-lstms-demand-polished-data-113h</link>
      <guid>https://forem.com/u11d/the-memory-of-water-why-lstms-demand-polished-data-113h</guid>
      <description>&lt;p&gt;In the era of "Big Data," there is a pervasive myth in environmental science that quantity is a proxy for quality. We assume that if we have terabytes of telemetry logs from thousands of sensors, the sheer volume of information will overpower the noise. We assume that modern Deep Learning architectures—specifically Long Short-Term Memory (LSTM) networks—are smart enough to figure it out.&lt;/p&gt;

&lt;p&gt;They are not.&lt;/p&gt;

&lt;p&gt;In hydrology, raw data is not fuel; it is crude oil. It is full of impurities, gaps, and artifacts that, if fed directly into a neural network, will clog the engine. When building systems to predict flash floods or manage reservoir levels, the sophistication of your model architecture matters far less than the continuity and physical integrity of your input data.&lt;/p&gt;

&lt;p&gt;We don't just need to "clean" data. We need to &lt;strong&gt;polish&lt;/strong&gt; it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Illusion of Abundance
&lt;/h2&gt;

&lt;p&gt;A modern hydrological sensor network is a chaotic environment. Pressure transducers drift as sediment builds up. Telemetry radios fail during the very storms we need to measure. Batteries die in the cold.&lt;/p&gt;

&lt;p&gt;When you look at a raw dataset, you see a time series. But an LSTM sees a narrative. If that narrative is riddled with holes, spikes, and flatlines, the model cannot learn the underlying physics of the catchment.&lt;/p&gt;

&lt;p&gt;We often see feeding raw sensor logs into training pipelines, hoping the neural network will learn to ignore the errors. This is a fundamental misunderstanding of how LSTMs work. A standard regression model might average out the noise. An LSTM, however, tries to learn the &lt;em&gt;sequence&lt;/em&gt; of events. If we feed it noise, it doesn't just make a bad prediction for that timestep; it learns a false causal relationship that corrupts its understanding of future events.&lt;/p&gt;

&lt;h2&gt;
  
  
  The High Cost of Discontinuity
&lt;/h2&gt;

&lt;p&gt;To understand why data polishing is critical, you have to understand the "Memory" in Long Short-Term Memory.&lt;/p&gt;

&lt;p&gt;Unlike a standard feed-forward network that looks at a snapshot of data, an LSTM maintains an internal "cell state"—a vector that carries context forward through time. In hydrology, this cell state represents the physical state of the catchment: How saturated is the soil? How high is the groundwater? Is the river already swollen from yesterday's rain?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data continuity is the lifeline of this cell state.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a sensor goes offline for three hours, we don't just lose three hours of data. We sever the model's connection to the past. If we simply drop those rows and stitch the time series back together, we are teleporting the catchment three hours into the future instantly. The LSTM sees a sudden, inexplicable jump in state that violates the laws of physics.&lt;/p&gt;

&lt;p&gt;It tries to learn a pattern to explain this jump. But there is no pattern—only a broken sensor. The result is a model that "hallucinates," predicting sudden floods or droughts based on data artifacts rather than meteorological forcing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Pipeline with Dagster
&lt;/h2&gt;

&lt;p&gt;To solve this, we cannot rely on ad-hoc cleaning scripts scattered across Jupyter notebooks. We need a rigorous, reproducible engineering standard. This is where we leverage &lt;strong&gt;Dagster&lt;/strong&gt; to orchestrate the transformation from chaos to clarity.&lt;/p&gt;

&lt;p&gt;In our architecture, we treat data stages as distinct software-defined assets.&lt;/p&gt;

&lt;p&gt;First, we define a &lt;code&gt;raw_sensor_ingestion&lt;/code&gt; asset. Dagster pulls this directly from our telemetry APIs or S3 buckets. This asset is immutable; it represents the "ground truth" of what the sensors actually reported, warts and all. We never modify this layer, ensuring we always have a pristine audit trail.&lt;/p&gt;

&lt;p&gt;Next, we define a downstream &lt;code&gt;polished_timeseries&lt;/code&gt; asset. This is where the engineering happens. Dagster manages the dependency, ensuring that the polishing logic only runs when new raw data is available. Inside this asset, we execute our cleaning algorithms—removing outliers, handling gaps, and normalizing timestamps.&lt;/p&gt;

&lt;p&gt;By using Dagster, we gain full lineage. If a model starts behaving strangely, we don't have to guess which cleaning script was run. We can look at the asset graph and see exactly which version of the code produced the training data, ensuring that our "polish" is as version-controlled as our model architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforcing the Laws of Physics on Data
&lt;/h2&gt;

&lt;p&gt;The logic inside that &lt;code&gt;polished_timeseries&lt;/code&gt; asset is designed to enforce the laws of physics. A neural network starts as a blank slate; it doesn't know that water cannot flow uphill or that a river cannot dry up in seconds.&lt;/p&gt;

&lt;p&gt;We must teach it these boundaries through rigorous checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Physical Bounds:&lt;/strong&gt; A river stage cannot be negative. Soil moisture cannot exceed porosity. Precipitation cannot physically reach 500mm in 10 minutes. These aren't just outliers; they are impossibilities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporal Consistency:&lt;/strong&gt; Water has mass and momentum; it accelerates and decelerates according to gravity and friction. A reading that jumps from 1m to 5m and back to 1m in a single 15-minute interval is almost certainly a sensor glitch, not a flash flood.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If we leave these "ghost signals" in the training set, the LSTM wastes its capacity trying to model impossible physics. By removing them, we allow the model to focus its gradient descent on learning the actual behavior of water.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filling the Void Without Lying to the Model
&lt;/h2&gt;

&lt;p&gt;Once we identify the gaps and the ghosts, we face the hardest choice in data engineering: &lt;strong&gt;Imputation.&lt;/strong&gt; How do we fill the silence without lying to the model?&lt;/p&gt;

&lt;p&gt;This is where domain expertise becomes code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linear Interpolation&lt;/strong&gt; might work for temperature, which changes gradually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forward Filling&lt;/strong&gt; might work for a reservoir level that changes slowly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Masking&lt;/strong&gt; is often the most honest approach for precipitation. If we don't know if it rained, we shouldn't guess. We should explicitly tell the model, "I don't know," often by using a separate boolean channel in the input tensor indicating data validity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The danger of aggressive polishing is creating a "perfect" dataset that doesn't exist in reality. If we smooth out every peak and fill every gap with a perfect average, we train a model that is terrified of extremes. It will under-predict floods because it has never seen the raw, jagged reality of a storm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Respecting the Journey of the Data
&lt;/h2&gt;

&lt;p&gt;In the rush to adopt the latest Transformer architectures or state-of-the-art LSTMs, it is easy to view data processing as a janitorial task—something to be automated away so we can get to the "real work" of modeling.&lt;/p&gt;

&lt;p&gt;But in environmental science, the data &lt;em&gt;is&lt;/em&gt; the real work.&lt;/p&gt;

&lt;p&gt;The performance ceiling of any hydrological forecast is not determined by the number of layers in your neural network, but by the fidelity of the story your data tells. A simple model trained on polished, physically consistent data will outperform a complex model trained on raw noise every time.&lt;/p&gt;

&lt;p&gt;We are not just training models to predict numbers. We are training them to understand the memory of water. And that memory must be clear.&lt;/p&gt;

</description>
      <category>dagster</category>
      <category>datascience</category>
      <category>machinelearning</category>
      <category>hydrology</category>
    </item>
    <item>
      <title>Decoupling Ingress with TargetGroupBinding in EKS</title>
      <dc:creator>Paweł Swiridow</dc:creator>
      <pubDate>Wed, 28 Jan 2026 11:00:00 +0000</pubDate>
      <link>https://forem.com/u11d/decoupling-ingress-with-targetgroupbinding-in-eks-2409</link>
      <guid>https://forem.com/u11d/decoupling-ingress-with-targetgroupbinding-in-eks-2409</guid>
      <description>&lt;p&gt;As we scale our EKS clusters, relying solely on Kubernetes Ingress objects to provision AWS Application Load Balancers (ALBs) can become restrictive. Sometimes we need to attach an EKS Service to a pre-existing ALB managed by Terraform, or we need complex routing rules that are easier to manage in HCL (Terraform) than in K8s annotations.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;AWS Load Balancer Controller&lt;/strong&gt; supports a custom resource called &lt;code&gt;TargetGroupBinding&lt;/code&gt; (TGB). This allows us to provision the Load Balancer and Target Group in Terraform (Infrastructure layer) and simply "bind" our Kubernetes Service to it at runtime (Application layer).&lt;/p&gt;

&lt;p&gt;This guide walks through how to set up an AWS Target Group in Terraform and register a &lt;strong&gt;Prometheus&lt;/strong&gt; instance to it using Helm values.&lt;/p&gt;




&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Before proceeding, ensure your environment meets the following requirements. This architecture relies on specific AWS components to route traffic directly to Pods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon EKS Cluster:&lt;/strong&gt; A running EKS cluster is required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS VPC CNI Plugin:&lt;/strong&gt; We will be using &lt;code&gt;target_type = "ip"&lt;/code&gt;. This mode requires the &lt;strong&gt;AWS VPC CNI&lt;/strong&gt; (the default networking plugin for EKS), which assigns native AWS VPC IP addresses to Pods. This allows the ALB to route traffic directly to the Pod IP, bypassing the worker node's kube-proxy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Load Balancer Controller:&lt;/strong&gt; You must have the &lt;strong&gt;AWS Load Balancer Controller&lt;/strong&gt; (v2.0+) installed and running in your cluster. This controller is responsible for installing the &lt;code&gt;TargetGroupBinding&lt;/code&gt; CRD and actively managing the registration of targets.

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Note:&lt;/em&gt; Ensure the controller has the necessary IAM permissions to &lt;code&gt;elasticloadbalancing:RegisterTargets&lt;/code&gt; and &lt;code&gt;elasticloadbalancing:DeregisterTargets&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Part 1: Infrastructure (Terraform)
&lt;/h2&gt;

&lt;p&gt;First, we need to create the Target Group. The critical setting here is &lt;code&gt;target_type = "ip"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When using the AWS Load Balancer Controller with the AWS VPC CNI, we want the ALB to send traffic directly to the Pod IP addresses, bypassing NodePorts.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;main.tf&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb_target_group"&lt;/span&gt; &lt;span class="s2"&gt;"prometheus_tg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eks-prometheus-tg"&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9090&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_id&lt;/span&gt;

  &lt;span class="c1"&gt;# CRITICAL: Must be 'ip' for direct Pod routing via AWS LB Controller&lt;/span&gt;
  &lt;span class="nx"&gt;target_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ip"&lt;/span&gt;

  &lt;span class="nx"&gt;health_check&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/-/healthy"&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP"&lt;/span&gt;
    &lt;span class="nx"&gt;matcher&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt;
    &lt;span class="nx"&gt;interval&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
    &lt;span class="nx"&gt;timeout&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="nx"&gt;healthy_threshold&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="nx"&gt;unhealthy_threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"production"&lt;/span&gt;
    &lt;span class="nx"&gt;ManagedBy&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# We need to export this ARN to pass it to our Helm chart later&lt;/span&gt;
&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"prometheus_tg_arn"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prometheus_tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Do not use &lt;code&gt;aws_lb_target_group_attachment&lt;/code&gt; in Terraform. The AWS Load Balancer Controller running inside the cluster will manage the targets dynamically as Pods come and go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: Security (IAM Least Privilege)
&lt;/h2&gt;

&lt;p&gt;By default, the generic AWS Load Balancer Controller policy is permissive (often using &lt;code&gt;Resource: *&lt;/code&gt;). In a production environment - especially one with multiple teams sharing an AWS account - we should adhere to &lt;strong&gt;Least Privilege&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We must restrict the Controller's ability so it can &lt;strong&gt;only&lt;/strong&gt; register/deregister targets for this specific Target Group, preventing it from accidentally modifying other Load Balancers.&lt;/p&gt;

&lt;p&gt;Add this policy to the IAM Role used by your Load Balancer Controller ServiceAccount:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;iam.tf&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;data "aws_iam_policy_document" "lb_controller_tgb_policy" {&lt;/span&gt;
  &lt;span class="s"&gt;statement {&lt;/span&gt;
    &lt;span class="s"&gt;sid       = "AllowRegisterTargets"&lt;/span&gt;
    &lt;span class="s"&gt;effect    = "Allow"&lt;/span&gt;
    &lt;span class="s"&gt;actions   = [&lt;/span&gt;
      &lt;span class="s"&gt;"elasticloadbalancing:RegisterTargets",&lt;/span&gt;
      &lt;span class="s"&gt;"elasticloadbalancing:DeregisterTargets"&lt;/span&gt;
    &lt;span class="s"&gt;]&lt;/span&gt;
    &lt;span class="s"&gt;# Scope permissions strictly to the specific Target Group ARN created above&lt;/span&gt;
    &lt;span class="s"&gt;resources = [aws_lb_target_group.prometheus_tg.arn]&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;

  &lt;span class="s"&gt;statement {&lt;/span&gt;
    &lt;span class="s"&gt;sid       = "AllowDescribeHealth"&lt;/span&gt;
    &lt;span class="s"&gt;effect    = "Allow"&lt;/span&gt;
    &lt;span class="s"&gt;actions   = [&lt;/span&gt;
      &lt;span class="s"&gt;"elasticloadbalancing:DescribeTargetHealth"&lt;/span&gt;
    &lt;span class="s"&gt;]&lt;/span&gt;
    &lt;span class="s"&gt;resources = [aws_lb_target_group.prometheus_tg.arn]&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;resource "aws_iam_policy" "tgb_strict_policy" {&lt;/span&gt;
  &lt;span class="s"&gt;name        = "eks-alb-controller-tgb-restricted"&lt;/span&gt;
  &lt;span class="s"&gt;description = "Restricted access for TargetGroupBinding to specific TGs only"&lt;/span&gt;
  &lt;span class="s"&gt;policy      = data.aws_iam_policy_document.lb_controller_tgb_policy.json&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Part 3: The Glue (TargetGroupBinding CRD)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;TargetGroupBinding&lt;/code&gt; CRD tells the controller: &lt;em&gt;"Watch this Kubernetes Service, and whenever its endpoints change, update this specific AWS Target Group."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A raw TGB manifest looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;elbv2.k8s.aws/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TargetGroupBinding&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prometheus-tgb&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monitoring&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;serviceRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prometheus-k8s&lt;/span&gt; &lt;span class="c1"&gt;# The name of your K8s Service&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9090&lt;/span&gt;           &lt;span class="c1"&gt;# The port defined in the Service&lt;/span&gt;
  &lt;span class="na"&gt;targetGroupARN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;YOUR_TF_OUTPUT_ARN&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Part 4: Application Deployment (Prometheus Helm Values)
&lt;/h2&gt;

&lt;p&gt;We don't want to apply that YAML manually. We want it version-controlled with our Prometheus deployment.&lt;/p&gt;

&lt;p&gt;Most Prometheus Helm charts (like &lt;code&gt;kube-prometheus-stack&lt;/code&gt;) support an &lt;code&gt;extraManifests&lt;/code&gt; or &lt;code&gt;additionalManifests&lt;/code&gt; property in their &lt;code&gt;values.yaml&lt;/code&gt;. This allows us to inject arbitrary K8s objects-like our TGB-directly during the Helm install.&lt;/p&gt;

&lt;p&gt;Here is how you configure your &lt;code&gt;values.yaml&lt;/code&gt; to register Prometheus to the Terraform-managed Target Group.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;values.yaml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Configuration for kube-prometheus-stack&lt;/span&gt;
&lt;span class="na"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9090&lt;/span&gt;
    &lt;span class="c1"&gt;# Ensure the service selector matches what the TGB expects&lt;/span&gt;
    &lt;span class="c1"&gt;# usually standard, but good to verify.&lt;/span&gt;

&lt;span class="c1"&gt;# Injecting the Custom Resource Definition&lt;/span&gt;
&lt;span class="na"&gt;extraManifests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;elbv2.k8s.aws/v1beta1&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TargetGroupBinding&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prometheus-binding&lt;/span&gt;
      &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monitoring&lt;/span&gt; &lt;span class="c1"&gt;# Must match the release namespace&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;serviceRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prometheus-kube-prometheus-prometheus&lt;/span&gt; &lt;span class="c1"&gt;# Default name in the stack&lt;/span&gt;
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9090&lt;/span&gt;
      &lt;span class="na"&gt;targetGroupARN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:elasticloadbalancing:us-east-1:1234567890:targetgroup/eks-prometheus-tg/6d0ecf831eec9f09"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Summary of Flow&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Terraform&lt;/strong&gt; creates the empty Target Group (Mode: IP).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Helm&lt;/strong&gt; deploys Prometheus + the &lt;code&gt;TargetGroupBinding&lt;/code&gt; CR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Load Balancer Controller&lt;/strong&gt; sees the new Binding CR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controller&lt;/strong&gt; looks up the Pod IPs backing the Prometheus Service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controller&lt;/strong&gt; registers those Pod IPs into the AWS Target Group automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach gives us the stability of Terraform-managed Infrastructure (the ALB and Listeners) with the flexibility of Kubernetes-managed endpoints.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>AWS CloudFront Explained: How Cache, Origin, and Response Policies Supercharge Your CDN</title>
      <dc:creator>Maciej Łopalewski</dc:creator>
      <pubDate>Wed, 21 Jan 2026 09:00:00 +0000</pubDate>
      <link>https://forem.com/u11d/aws-cloudfront-explained-how-cache-origin-and-response-policies-supercharge-your-cdn-3l11</link>
      <guid>https://forem.com/u11d/aws-cloudfront-explained-how-cache-origin-and-response-policies-supercharge-your-cdn-3l11</guid>
      <description>&lt;p&gt;If you have configured Amazon CloudFront in the past, you might remember wrestling with "Cache Behaviors" - a monolithic setting where caching logic, origin forwarding, and header manipulation were all jumbled together.&lt;/p&gt;

&lt;p&gt;Those days are over.&lt;/p&gt;

&lt;p&gt;Modern CloudFront architecture uses a modular &lt;strong&gt;Policy System&lt;/strong&gt;. This approach decouples &lt;strong&gt;caching&lt;/strong&gt; (what is stored) from &lt;strong&gt;origin requests&lt;/strong&gt; (what is sent to the backend) and &lt;strong&gt;response headers&lt;/strong&gt; (security/CORS).&lt;/p&gt;

&lt;p&gt;For DevOps engineers and cloud architects, understanding these three policy types is the key to building performant, secure, and scalable content delivery networks. This guide breaks down the ecosystem of CloudFront Managed Policies and helps you choose the right tools for the job.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is CloudFront?
&lt;/h2&gt;

&lt;p&gt;Before diving into policies, let’s ground ourselves in the basics. &lt;strong&gt;Amazon CloudFront&lt;/strong&gt; is a global Content Delivery Network (CDN). Its primary job is to sit between your users and your infrastructure (the "Origin" - like an S3 bucket or an EC2 load balancer).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency:&lt;/strong&gt; It serves content from "Edge Locations" physically closer to the user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; It terminates TLS connections at the edge and blocks DDoS attacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale:&lt;/strong&gt; It absorbs traffic spikes so your backend doesn't crash.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Policy Trio: How They Work
&lt;/h2&gt;

&lt;p&gt;In the modern CloudFront request flow, three distinct policies interact to process a user's request. Understanding the distinction between them is critical for avoiding common pitfalls like "cache misses" or CORS errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Cache Policy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Where it sits:&lt;/strong&gt; At the very front of the flow.&lt;br&gt;
&lt;strong&gt;What it does:&lt;/strong&gt; It determines the &lt;strong&gt;Cache Key&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When a user requests content, CloudFront uses this policy to decide if it already has a copy. It defines which headers, cookies, or query strings make a request "unique."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Strict policies = Higher cache hit ratio.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Loose policies = Lower cache hit ratio (more load on origin).&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Origin Request Policy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Where it sits:&lt;/strong&gt; Between CloudFront and your Backend (Origin).&lt;br&gt;
&lt;strong&gt;What it does:&lt;/strong&gt; It determines what data is forwarded to the backend &lt;strong&gt;during a cache miss&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the most misunderstood policy. It allows you to send data (like user-specific cookies) to your backend &lt;em&gt;without&lt;/em&gt; including that data in the Cache Key. This keeps your cache efficiency high while still giving your application the data it needs to process logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Response Headers Policy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Where it sits:&lt;/strong&gt; On the way back to the user.&lt;br&gt;
&lt;strong&gt;What it does:&lt;/strong&gt; It injects specific HTTP headers into the response.&lt;/p&gt;

&lt;p&gt;Regardless of what your backend sends, this policy ensures the browser receives the correct Security (HSTS, XSS protection) and CORS headers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Top Managed Policies: A Cheat Sheet
&lt;/h2&gt;

&lt;p&gt;AWS maintains a library of "Managed Policies" that cover about 90% of use cases. Using these is a best practice - they are rigorously tested, updated by AWS, and require zero maintenance.&lt;/p&gt;

&lt;p&gt;Here are the most essential managed policies for each category.&lt;/p&gt;

&lt;h3&gt;
  
  
  A. Managed Cache Policies
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Control the Cache Key and TTL (Time To Live).&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;CachingOptimized&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Best For: S3 Buckets, Static Websites, Images/Assets.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How it works:&lt;/strong&gt; It ignores almost all headers and cookies. It aggressively caches content based solely on the URL path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why choose it:&lt;/strong&gt; This provides the highest possible cache hit ratio. If your content doesn't change based on who is viewing it, use this.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;CachingDisabled&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Best For: Dynamic APIs, WebSockets, Real-time data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How it works:&lt;/strong&gt; It sets the Time-To-Live (TTL) to 0. Every request bypasses the cache and goes straight to the origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why choose it:&lt;/strong&gt; Essential for endpoints where data changes every second, or for write operations (POST/PUT) where caching would break functionality.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;UseOriginCacheControlHeaders&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Best For: CMS (WordPress/Drupal), Hybrid Apps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How it works:&lt;/strong&gt; It defers the decision to your server. It looks for &lt;code&gt;Cache-Control&lt;/code&gt; headers sent by your backend to decide how long to store the file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why choose it:&lt;/strong&gt; Perfect if you have a mix of static and dynamic content and want your application code, rather than CloudFront configuration, to control cache duration.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  B. Managed Origin Request Policies
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Control what the backend sees (without breaking the cache).&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;AllViewer&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Best For: Legacy Applications, Complex Dynamic Apps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How it works:&lt;/strong&gt; Forwards &lt;strong&gt;everything&lt;/strong&gt; - every header, every cookie, every query string - to the origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why choose it:&lt;/strong&gt; If your application relies on specific, obscure headers or client-side cookies to function, this ensures nothing is stripped out. &lt;em&gt;Warning: This may expose internal origin details.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;CORS-S3Origin&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Best For: S3 Buckets serving assets to other domains.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How it works:&lt;/strong&gt; Specifically whitelists the headers S3 requires to process CORS checks (&lt;code&gt;Origin&lt;/code&gt;, &lt;code&gt;Access-Control-Request-Method&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why choose it:&lt;/strong&gt; S3 handles CORS differently than a standard web server. Standard forwarding often fails with S3; this policy fixes those specific issues instantly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;UserAgentRefererHeaders&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Best For: Analytics, Hotlink Protection.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How it works:&lt;/strong&gt; It specifically forwards the &lt;code&gt;User-Agent&lt;/code&gt; and &lt;code&gt;Referer&lt;/code&gt; headers while stripping others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why choose it:&lt;/strong&gt; If your backend needs to block requests from specific sites (hotlinking) or serve different content to mobile vs. desktop devices, but doesn't need full cookie visibility.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  C. Managed Response Headers Policies
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Control browser security and access.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;SecurityHeadersPolicy&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Best For: Everything. (Seriously, use this everywhere).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How it works:&lt;/strong&gt; Automatically injects industry-standard security headers like:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Strict-Transport-Security&lt;/code&gt; (HSTS)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-Frame-Options: DENY&lt;/code&gt; (prevents clickjacking)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Why choose it:&lt;/strong&gt; It instantly hardens your application against common web attacks without requiring code changes on your server.&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;CORS-with-preflight-and-SecurityHeadersPolicy&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Best For: Single Page Apps (React, Vue, Angular) calling APIs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How it works:&lt;/strong&gt; Combines the security headers above with a permissive CORS configuration. It handles the &lt;code&gt;OPTIONS&lt;/code&gt; pre-flight requests that modern browsers send before making API calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why choose it:&lt;/strong&gt; It solves the dreaded "CORS Error" in browser consoles for modern web applications.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;SimpleCORS&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Best For: Public, read-only data feeds.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How it works:&lt;/strong&gt; Adds &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; to the response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why choose it:&lt;/strong&gt; If you are hosting public data (like a weather feed or public JSON file) that you want &lt;em&gt;anyone&lt;/em&gt; to be able to use on their website, this is the quickest way to enable it.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common CloudFront Misconfigurations and How Managed Policies Fix Them
&lt;/h2&gt;

&lt;p&gt;Even experienced DevOps teams run into the same CloudFront issues over and over. Almost all of them trace back to legacy cache behaviors or overly customized settings. Here’s how CloudFront Managed Policies solve the most common problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. “My cache hit ratio is terrible.”
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; Your Cache Key is too loose - it includes unnecessary headers, cookies, or query strings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; Every request is seen as "unique," forcing a constant stream of cache misses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fix:&lt;/strong&gt; Use the &lt;strong&gt;CachingOptimized&lt;/strong&gt; managed policy. It strips almost everything from the Cache Key, restoring high hit ratios - perfect for static assets, SPAs, and S3 origins.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. “CloudFront keeps forwarding too many headers to my origin.”
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; Legacy behaviors often forward all headers by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Impact:&lt;/strong&gt; Increased origin load, slower responses, and potential "Request Header Too Large" errors on the backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fix:&lt;/strong&gt; Switch to an Origin Request Policy like &lt;strong&gt;UserAgentRefererHeaders&lt;/strong&gt; or &lt;strong&gt;CORS-S3Origin&lt;/strong&gt;. This ensures you forward &lt;em&gt;only&lt;/em&gt; what your backend actually needs to function.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. “I’m still getting CORS errors in the browser.”
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; Missing or inconsistent &lt;code&gt;Access-Control-*&lt;/code&gt; headers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fix:&lt;/strong&gt; Apply the &lt;strong&gt;CORS-with-preflight-and-SecurityHeadersPolicy&lt;/strong&gt; response policy. It handles &lt;code&gt;OPTIONS&lt;/code&gt; preflight requests and injects all required CORS headers at the edge - even if your backend forgets them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. “S3 CORS works on localhost, but not in CloudFront.”
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; S3 requires specific headers to process CORS. If CloudFront strips them, S3 treats the request as standard and omits the CORS response headers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fix:&lt;/strong&gt; Use the &lt;strong&gt;CORS-S3Origin&lt;/strong&gt; Origin Request Policy. This explicitly forwards &lt;code&gt;Origin&lt;/code&gt;, &lt;code&gt;Access-Control-Request-Method&lt;/code&gt;, and &lt;code&gt;Access-Control-Request-Headers&lt;/code&gt; so S3 knows to respond correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. “My dynamic API is being cached when it shouldn’t be.”
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; Your API path (&lt;code&gt;/api/*&lt;/code&gt;) is falling through to a default behavior that has caching enabled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fix:&lt;/strong&gt; Create a specific behavior for your API path and attach &lt;strong&gt;CachingDisabled&lt;/strong&gt;. This guarantees every request bypasses the edge and reaches your application.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Moving to Managed Policies allows you to operate with "Intent-Based Configuration." Instead of tweaking individual settings, you select a policy that matches your architectural intent.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Intent&lt;/th&gt;
&lt;th&gt;Recommended Policy Combo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Static Website&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cache: &lt;code&gt;CachingOptimized&lt;/code&gt; Origin Request: &lt;code&gt;None&lt;/code&gt; (or &lt;code&gt;CORS-S3Origin&lt;/code&gt;) Response: &lt;code&gt;SecurityHeadersPolicy&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dynamic API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cache: &lt;code&gt;CachingDisabled&lt;/code&gt; Origin Request: &lt;code&gt;AllViewer&lt;/code&gt; Response: &lt;code&gt;SimpleCORS&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Modern Web App (SPA)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cache: &lt;code&gt;CachingOptimized&lt;/code&gt; (for assets) Origin Request: &lt;code&gt;None&lt;/code&gt; Response: &lt;code&gt;CORS-with-preflight&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>aws</category>
      <category>cdn</category>
      <category>webdev</category>
      <category>cloudfront</category>
    </item>
    <item>
      <title>Geospatial Data Orchestration: Why Modern GIS Pipelines Require an Asset-Based Approach</title>
      <dc:creator>Paweł Sławacki</dc:creator>
      <pubDate>Thu, 15 Jan 2026 07:41:53 +0000</pubDate>
      <link>https://forem.com/u11d/geospatial-data-orchestration-why-modern-gis-pipelines-require-an-asset-based-approach-4mdo</link>
      <guid>https://forem.com/u11d/geospatial-data-orchestration-why-modern-gis-pipelines-require-an-asset-based-approach-4mdo</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F187sa64lhcbj7vzdjvqb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F187sa64lhcbj7vzdjvqb.webp" alt="Main image" width="800" height="436"&gt;&lt;/a&gt;In the world of data, true turning points are rare—moments when a technology originally designed for one category of problems turns out to be the missing piece in a completely different domain. This is precisely what is happening now in geospatial data. Workflows traditionally rooted in the GIS niche have become one of the most demanding components of contemporary AI systems and environmental analytics.&lt;/p&gt;

&lt;p&gt;What once relied on manual work inside desktop tools must now meet requirements of scalability, reproducibility, and full automation. Models need to be retrained continuously, data arrives in real time, and every forecast must be explainable and fully reproducible.&lt;/p&gt;

&lt;p&gt;These were exactly the challenges we faced while building a cloud-native hydrological and environmental data processing system — one that merges dynamic measurements, large raster datasets, machine learning, and GIS-based interpretation. That experience made one thing very clear: geospatial does not simply need “better workflows.” It needs an &lt;strong&gt;orchestration layer that treats data as the primary actor — not a byproduct&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Dagster became the natural choice for such an architecture. Dagster is increasingly used for geospatial data orchestration because its asset-based model aligns naturally with GIS datasets, raster processing pipelines, and reproducible environmental analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is geospatial data orchestration?
&lt;/h2&gt;

&lt;p&gt;Geospatial data orchestration is the practice of managing, automating, and governing complex GIS and spatial data pipelines — including raster processing, feature engineering, machine learning training, and data publication — in a way that is scalable, reproducible, and fully traceable.&lt;/p&gt;

&lt;p&gt;Unlike traditional GIS workflows that rely on manual execution inside desktop tools, geospatial orchestration treats datasets and derived artefacts as first-class assets with explicit dependencies, versioning, and lineage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why asset-based orchestration is the language geospatial systems speak intuitively
&lt;/h2&gt;

&lt;p&gt;In geospatial projects, every element of the workflow — an elevation raster, a land-cover classification, a soil map, a catchment-level aggregation, a training tensor — exists as a meaningful artefact with its own purpose and lineage. These artefacts form the narrative spine of the entire system.&lt;/p&gt;

&lt;p&gt;While building the hydrological platform, we quickly discovered that geospatial processing fundamentally conflicts with the task-oriented paradigm used by most workflow tools. In hydrology, meteorology, or environmental modelling, “a task” is merely a transient carrier of work. What matters is the end product: the raster, the derived feature set, the trained model, the forecast.&lt;/p&gt;

&lt;p&gt;This is precisely why Dagster’s model — where the core unit is the &lt;em&gt;asset&lt;/em&gt;, not the task — feels almost native to geospatial data.&lt;/p&gt;

&lt;p&gt;When we convert a DEM to a tile-optimized raster format, we create an asset.&lt;/p&gt;

&lt;p&gt;When we generate soil attributes or retention-capacity parameters for a catchment, we create assets.&lt;/p&gt;

&lt;p&gt;When we produce features for training a model or build the final forecasts — those are assets as well.&lt;/p&gt;

&lt;p&gt;Each of these objects has a life of its own, a history, and a network of dependencies. Dagster makes this structure visible, not as an incidental side effect of code, but as the logical architecture of the entire system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why traditional workflow orchestrators struggle with geospatial pipelines
&lt;/h2&gt;

&lt;p&gt;Most workflow orchestrators were designed for task-centric ETL pipelines. In geospatial systems, this approach breaks down because tasks are transient, while spatial datasets — rasters, tiles, features, and models — are long-lived analytical artefacts.&lt;/p&gt;

&lt;p&gt;As a result, task-based orchestration makes lineage harder to understand, reproducibility fragile, and debugging costly in GIS-heavy and environmental data pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  In geospatial, transparency is not a nice-to-have — it is a necessity
&lt;/h2&gt;

&lt;p&gt;One of the core lessons from developing environmental systems is simple: &lt;strong&gt;results must be explainable&lt;/strong&gt;. A hydrologist, GIS analyst, or decision-maker responsible for assessing risk must understand where every value in the model comes from and what transformations shaped it.&lt;/p&gt;

&lt;p&gt;The orchestration we implemented enforces this clarity. Every stage — from data ingestion, through raster processing, to modelling and publication — leaves behind a durable artefact. There are no hidden transformations, no opaque steps, no “magic.” If a forecast changes from one iteration to another, we can point to the reason. If an experiment needs to be repeated, we do it deterministically.&lt;/p&gt;

&lt;p&gt;Dagster amplifies this transparency, because it expresses the system as a web of dependencies between artefacts. In the geospatial architecture we built, full lineage is visible: from raw rasters to intermediate steps to the final products consumed in QGIS. This is not optional — it is a foundational requirement for analytical responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cloud removes infrastructure friction; Dagster gives the system its rhythm
&lt;/h2&gt;

&lt;p&gt;Geospatial data is large, and its processing can be computationally intensive. That is why one of the priorities of our solution was to clearly separate data processing from the infrastructure that executes it.&lt;/p&gt;

&lt;p&gt;A central object store in S3, container-based processing, demand-driven autoscaling, version control of experiments in MLflow, and a permanent division between ETL and model-training environments allowed us to simplify the entire ecosystem. The team could focus on data, not on the platform itself.&lt;/p&gt;

&lt;p&gt;Dagster acted as the coordinator in this architecture. It defined the relationships between artefacts, governed how data was refreshed, and set the cadence for model training. It provided structure without imposing unnecessary constraints, enabling architectural decisions to be made at the level of data — not infrastructure.&lt;/p&gt;

&lt;p&gt;This is one of the benefits that only becomes visible in large geospatial systems: orchestration should not be a heavyweight layer “on top” of the system but a lightweight skeleton on which the system naturally rests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineered for elasticity: The physical architecture
&lt;/h2&gt;

&lt;p&gt;Our implementation relies on AWS EKS to absorb the extreme variance in geospatial compute. We treat infrastructure as elastic capacity, not a fixed cluster: it expands and contracts in response to the asset graph.&lt;/p&gt;

&lt;p&gt;The cluster is divided into specialized node pools. Core services run on steady instances, while processing tasks route to autoscaling groups sized for their load — from lightweight CPU jobs to memory‑heavy raster operations. For machine learning, GPU nodes are provisioned on demand; a Dagster asset declares its needs via tags, and the cluster autoscaler supplies them. We pay for high‑performance compute only during the minutes that model training runs.&lt;/p&gt;

&lt;p&gt;Operational rigor comes from isolation and clear identity boundaries. We split the Dagster deployment into two code locations — Data Preparation and Machine Learning — because geospatial stacks like GDAL and Rasterio conflict with the numerical stacks behind PyTorch or TensorFlow. Collapsing them into one environment creates brittle builds and version lock. By keeping them separate, each location owns its dependencies, and Dagster orchestrates across the seam cleanly. Security uses AWS Pod Identity to avoid long‑lived credentials, and GitHub Actions maintains a clean lineage from commit to ECR images. CloudWatch then provides a unified view of infrastructure health and pipeline performance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstrapi-aws-s3-img-media-bucket.s3.eu-west-1.amazonaws.com%2Fdagster_eks_orchestration_da81eb19a3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstrapi-aws-s3-img-media-bucket.s3.eu-west-1.amazonaws.com%2Fdagster_eks_orchestration_da81eb19a3.png" alt="Dagster eks orchestration" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This architecture delivers elasticity as a daily operational fact. After a training run, GPU nodes drain and terminate within minutes, returning spend to baseline. When a large raster arrives, the relevant pool expands to meet it, then contracts once the asset materializes. The system breathes with the workload — responsive at peaks, economical in troughs — without manual intervention or capacity planning. Most importantly, infrastructure fades from view: an asset definition states what it needs, and the platform ensures those resources appear exactly when required.&lt;/p&gt;

&lt;h2&gt;
  
  
  GIS remains a first-class partner, not collateral damage of modernization
&lt;/h2&gt;

&lt;p&gt;Many contemporary data platforms try to replace GIS with proprietary viewers or dashboards. Yet in practice — and especially in environmental and hydrological projects — GIS tools remain irreplaceable. In our approach, GIS is not a competitor to modern data architecture but its natural consumer.&lt;/p&gt;

&lt;p&gt;Final datasets are exposed in formats analysts know: GeoTIFF or Cloud Optimized GeoTIFF. As a result, GIS becomes a direct extension of the orchestration layer. Dagster produces the data; GIS interprets it. This separation of roles not only simplifies the system but also increases acceptance among experts who rely on these outputs daily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Business value emerges not from automation, but from reduced analytical risk
&lt;/h2&gt;

&lt;p&gt;From a technological standpoint, Dagster streamlines the workflow.&lt;/p&gt;

&lt;p&gt;From a business standpoint, it does something more important: &lt;strong&gt;it reduces operational and analytical risk&lt;/strong&gt;, which in geospatial projects is distributed across data quality, model correctness, and the reliability of forecasts used in decision-making.&lt;/p&gt;

&lt;p&gt;In the architecture we built, every decision about data is reflected in the structure of artefacts. Every change is visible. Every experiment is reproducible. This means more control, less uncertainty, and a system that is significantly more resilient to errors and shifts in external conditions.&lt;/p&gt;

&lt;p&gt;That is why well-designed orchestration becomes a strategic component — not an accessory — in geospatial data platforms. In domains where forecasts influence infrastructure planning, risk mitigation, or public safety, this level of analytical control is not a technical luxury — it is an operational requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Dagster suitable for geospatial and GIS workloads?
&lt;/h3&gt;

&lt;p&gt;Yes. Dagster’s asset-based orchestration model works particularly well with geospatial pipelines where rasters, features, and models must be versioned, traced, and recomputed deterministically.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does geospatial orchestration differ from traditional ETL?
&lt;/h3&gt;

&lt;p&gt;Geospatial orchestration focuses on managing spatial data artefacts and their lineage rather than executing isolated tasks. The goal is analytical transparency and reproducibility, not just automation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can cloud-native orchestration coexist with GIS tools like QGIS?
&lt;/h3&gt;

&lt;p&gt;Yes. Orchestrated pipelines can publish standard formats such as GeoTIFF or Cloud Optimized GeoTIFF, allowing GIS tools to remain first-class consumers of cloud-native data platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Geospatial is entering the era of orchestration — and Dagster is its natural foundation
&lt;/h2&gt;

&lt;p&gt;Geospatial data has a unique character: it blends the physical world with the mathematical complexity of models and the interpretability of spatial visualization. When these three worlds meet in a single project, traditional approaches to data processing quickly show their limits.&lt;/p&gt;

&lt;p&gt;Dagster, applied to geospatial systems, breaks through these limits. It enables an architecture in which large environmental datasets, machine learning models, and GIS-based analytics do not fight for dominance but coexist within a coherent ecosystem.&lt;/p&gt;

&lt;p&gt;It is not a tool that promises magic. It offers something more valuable: clarity, reproducibility, and accountability.&lt;/p&gt;

&lt;p&gt;This is why geospatial is increasingly gravitating toward orchestration.&lt;/p&gt;

&lt;p&gt;And why Dagster, with its asset-oriented philosophy, is emerging as the most natural language for that transformation.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>dagster</category>
      <category>dataengineering</category>
      <category>geospatial</category>
    </item>
    <item>
      <title>Forwarding Cookies Using CloudFront: A Workaround for AWS Cache Policy Limitations</title>
      <dc:creator>Daniel Kraszewski</dc:creator>
      <pubDate>Wed, 07 Jan 2026 12:00:00 +0000</pubDate>
      <link>https://forem.com/u11d/forwarding-cookies-using-cloudfront-a-workaround-for-aws-cache-policy-limitations-1hjc</link>
      <guid>https://forem.com/u11d/forwarding-cookies-using-cloudfront-a-workaround-for-aws-cache-policy-limitations-1hjc</guid>
      <description>&lt;p&gt;When building our Terraform module for deploying Medusa on AWS, we ran into an unexpected challenge with Amazon CloudFront. We wanted to use CloudFront as a simple way to provide HTTPS and a public URL without requiring users to bring their own domain or SSL certificate. However, we discovered that CloudFront's managed cache policies don't forward cookies, headers, and query parameters when caching is disabled - exactly what we needed for our backend API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Managed Cache Policies and CachingDisabled
&lt;/h2&gt;

&lt;p&gt;AWS CloudFront offers &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html#managed-cache-policy-caching-disabled" rel="noopener noreferrer"&gt;managed cache policies&lt;/a&gt; that handle common caching scenarios. The "CachingDisabled" policy seems perfect for dynamic content that shouldn't be cached. However, &lt;strong&gt;this policy doesn't forward cookies, headers, or query parameters to your origin by default&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For e-commerce platforms like Medusa, this is a dealbreaker. The backend needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cookies&lt;/strong&gt; for session management and authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headers&lt;/strong&gt; for content negotiation and API functionality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query&lt;/strong&gt; parameters for filtering and pagination&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We initially tried to create a custom cache policy with &lt;code&gt;MinTTL=0&lt;/code&gt; (no caching) while specifying header and cookie forwarding behaviors. AWS rejected this with an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operation error CloudFront: CreateCachePolicy, https response error StatusCode: 400,
InvalidArgument: The parameter HeaderBehavior is invalid for policy with caching disabled.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS's validation logic considers forwarding settings incompatible with disabled caching when using formal cache policies. The problem is clear: cache policies won't let you forward data without caching, but dynamic applications need that data forwarded to work properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Use CloudFront
&lt;/h2&gt;

&lt;p&gt;Before diving into the solution, let's clarify why we chose CloudFront in the first place:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Free HTTPS with Default Certificate&lt;/strong&gt; - CloudFront provides a free SSL/TLS certificate via &lt;code&gt;cloudfront_default_certificate = true&lt;/code&gt;, giving you a URL like &lt;code&gt;https://d123456abcdef.cloudfront.net&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Domain Required&lt;/strong&gt; - Users don't need to purchase a domain, manage DNS records, or provision ACM certificates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPC Security&lt;/strong&gt; - Our Application Load Balancer (ALB) stays in private subnets, accessible only through CloudFront's VPC Origin feature&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple Setup&lt;/strong&gt; - One Terraform resource provides HTTPS, DNS, and secure origin access without additional configuration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a deployment-focused module, this convenience is valuable. Users get a working HTTPS endpoint immediately after &lt;code&gt;terraform apply&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Legacy &lt;code&gt;forwarded_values&lt;/code&gt; Configuration
&lt;/h2&gt;

&lt;p&gt;The workaround is to use CloudFront's &lt;strong&gt;legacy &lt;code&gt;forwarded_values&lt;/code&gt; block&lt;/strong&gt; instead of modern cache policies. While AWS recommends cache policies for new distributions, the &lt;code&gt;forwarded_values&lt;/code&gt; configuration still works and allows zero-TTL caching with full data forwarding.&lt;/p&gt;

&lt;p&gt;Here's the configuration we use in our backend module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;default_cache_behavior {
  target_origin_id       = local.origin_id
  viewer_protocol_policy = "redirect-to-https"

  # Disable caching by setting all TTLs to zero
  min_ttl     = 0
  default_ttl = 0
  max_ttl     = 0

  forwarded_values {
    query_string = true    # Forward all query parameters
    headers      = ["*"]   # Forward all headers to origin

    cookies {
      forward = "all"      # Forward all cookies to origin
    }
  }

  allowed_methods = ["GET", "HEAD", "POST", "PUT", "PATCH", "OPTIONS", "DELETE"]
  cached_methods  = ["GET", "HEAD", "OPTIONS"]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Configuration Elements
&lt;/h2&gt;

&lt;p&gt;The heart of this solution is the TTL configuration. By setting &lt;code&gt;min_ttl&lt;/code&gt;, &lt;code&gt;default_ttl&lt;/code&gt;, and &lt;code&gt;max_ttl&lt;/code&gt; all to &lt;code&gt;0&lt;/code&gt;, we're telling CloudFront "don't cache anything, ever." Every request goes straight through to the origin, which is essential for dynamic content like user sessions and real-time inventory updates.&lt;/p&gt;

&lt;p&gt;Inside the &lt;code&gt;forwarded_values&lt;/code&gt; block, we're basically saying "pass everything through." Setting &lt;code&gt;query_string = true&lt;/code&gt; ensures that API parameters like &lt;code&gt;?page=2&amp;amp;limit=20&lt;/code&gt; reach your backend. The &lt;code&gt;headers = ["*"]&lt;/code&gt; configuration is particularly important-it forwards every header, including &lt;code&gt;Authorization&lt;/code&gt;, &lt;code&gt;Content-Type&lt;/code&gt;, and custom headers your application might use. And crucially, &lt;code&gt;forward = "all"&lt;/code&gt; in the cookies block ensures that session cookies make the round trip from browser to CloudFront to your backend and back again.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;allowed_methods&lt;/code&gt; array supports the full spectrum of HTTP verbs (GET, POST, PUT, PATCH, DELETE) because Medusa's admin API needs them all. This configuration effectively turns CloudFront into a passthrough proxy with HTTPS termination-not a traditional CDN, but a secure front door for your API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs and Considerations
&lt;/h2&gt;

&lt;p&gt;This approach shines when you're working with dynamic applications that maintain session state-think authentication systems, shopping carts, or any API where each request is unique. It's particularly valuable in rapid deployment scenarios where getting HTTPS working quickly matters more than squeezing out every bit of performance optimization. We've also found it perfect for development and staging environments where managing domains and certificates feels like overkill.&lt;/p&gt;

&lt;p&gt;That said, this isn't a one-size-fits-all solution. If you're serving static content like CSS, JavaScript bundles, or images, you're missing out on CloudFront's real strength: global edge caching. Similarly, if you're running a high-traffic production service where caching could significantly reduce origin load and costs, the no-cache approach leaves performance on the table. For applications serving a global audience where edge caching could shave hundreds of milliseconds off response times, you'd want to reconsider this pattern.&lt;/p&gt;

&lt;p&gt;For our Medusa module specifically, the no-cache approach makes sense because backend APIs are inherently dynamic-every request involves database queries, authentication checks, and business logic that can't be cached safely. Caching would actually break core functionality like session management and real-time inventory updates. The convenience of instant HTTPS deployment is worth the trade-off, and users always have the option to add a proper CDN layer in front for their static storefront assets if needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AWS CloudFront's managed cache policies work well for typical CDN use cases, but they have limitations when you need no caching with full data forwarding. The legacy &lt;code&gt;forwarded_values&lt;/code&gt; configuration provides a reliable workaround that's been working in production for our Medusa. deployments.&lt;/p&gt;

&lt;p&gt;While AWS's documentation encourages using modern cache policies, the &lt;code&gt;forwarded_values&lt;/code&gt; approach remains supported and is sometimes the pragmatic choice for dynamic applications. As always in infrastructure engineering, the "right" solution depends on your specific requirements-in our case, deployment convenience and session state management won the day.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is based on our experience building the &lt;a href="https://github.com/u11d-com/terraform-aws-medusajs" rel="noopener noreferrer"&gt;terraform-aws-medusajs&lt;/a&gt; module for deploying Medusa. e-commerce backends on AWS.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>cloudfront</category>
      <category>terraform</category>
    </item>
  </channel>
</rss>
