<?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>DEV Community: Daniel Kraszewski</title>
    <description>The latest articles on DEV Community by Daniel Kraszewski (@kraszdan).</description>
    <link>https://dev.to/kraszdan</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%2Fuser%2Fprofile_image%2F3330969%2Feacbbc8e-df0b-4291-848b-153e2e2eb668.jpg</url>
      <title>DEV Community: Daniel Kraszewski</title>
      <link>https://dev.to/kraszdan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kraszdan"/>
    <language>en</language>
    <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://dev.to/u11d/architecting-and-operating-geospatial-workflows-with-dagster-5a52</link>
      <guid>https://dev.to/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>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://dev.to/u11d/forwarding-cookies-using-cloudfront-a-workaround-for-aws-cache-policy-limitations-1hjc</link>
      <guid>https://dev.to/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>
    <item>
      <title>The default user in the Docker image</title>
      <dc:creator>Daniel Kraszewski</dc:creator>
      <pubDate>Mon, 10 Nov 2025 07:00:00 +0000</pubDate>
      <link>https://dev.to/u11d/the-default-user-in-the-docker-image-17gm</link>
      <guid>https://dev.to/u11d/the-default-user-in-the-docker-image-17gm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This fifth article concludes the series of Docker best practices that deserve more love. We will look closely at the default user in the image and pave the way for uninterrupted usage of Docker platform.&lt;br&gt;
Make sure you reviewed the previous articles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Proper use of cache to speed up and optimize builds&lt;/li&gt;
&lt;li&gt;Selecting the appropriate base image&lt;/li&gt;
&lt;li&gt;Understanding Docker multi-stage builds&lt;/li&gt;
&lt;li&gt;Understanding the context of the build&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Using administrator privileges&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The default user in the Docker image
&lt;/h2&gt;

&lt;p&gt;Regardless of the operating system, it is always good practice to reasonably use administrator privileges. Whether it's the &lt;code&gt;root&lt;/code&gt; user on Unix-like systems or the disabled &lt;code&gt;UAC&lt;/code&gt; module on Windows the effect may be the same and lead to increased chances of malicious code breaking through security. The principles are the same when it comes to Docker images, despite the fact that it introduces additional isolation between the host system and containers.&lt;/p&gt;

&lt;p&gt;So what is the default user? The answer is: the one that was set in the base image or &lt;code&gt;root&lt;/code&gt; if the user remained unchanged. Using &lt;code&gt;root&lt;/code&gt; gives us some advantages, such as the possibility to install additional dependencies. Due to the location of some libraries, these operations cannot be performed with limited privileges.&lt;/p&gt;

&lt;p&gt;Ultimately, we aim to change the user that will be least privileged, which in Dockerfile is quite simple. This is done with the &lt;code&gt;USER&lt;/code&gt; instruction which comes in two variants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;USER &amp;lt;user&amp;gt;[:&amp;lt;group&amp;gt;]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;USER &amp;lt;UID&amp;gt;[:&amp;lt;GID&amp;gt;]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first variant requires the textual names of the user and group we want to switch to, while the latter allows us to use their numeric identifiers directly. Let’s find out how the build process works when &lt;code&gt;USER&lt;/code&gt; instruction in both variants is used.&lt;/p&gt;

&lt;p&gt;Textual user name as &lt;code&gt;nginx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"
&amp;gt; FROM alpine:3.16
&amp;gt; USER nginx
&amp;gt; "&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Dockerfile

tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker build &lt;span class="nb"&gt;.&lt;/span&gt;
Sending build context to Docker daemon  10.75kB
Step 1/2 : FROM alpine:3.16
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 9b18e9b68314
Step 2/2 : USER nginx
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Running &lt;span class="k"&gt;in &lt;/span&gt;b67f70601e4c
Removing intermediate container b67f70601e4c
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 8410027170c6
Successfully built 8410027170c6

tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; 8410027170c6
docker: Error response from daemon: unable to find user nginx: no matching entries &lt;span class="k"&gt;in &lt;/span&gt;passwd file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Numeric user identifier as &lt;code&gt;1000&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"
&amp;gt; FROM alpine:3.16
&amp;gt; USER 1000
&amp;gt; "&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Dockerfile

tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker build &lt;span class="nb"&gt;.&lt;/span&gt;
Sending build context to Docker daemon  9.728kB
Step 1/2 : FROM alpine:3.16
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 9b18e9b68314
Step 2/2 : USER 1000
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Running &lt;span class="k"&gt;in &lt;/span&gt;869f2e785e83
Removing intermediate container 869f2e785e83
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 7d0dcceafd9f
Successfully built 7d0dcceafd9f

tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; 7d0dcceafd9f
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;id
&lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000 &lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="o"&gt;(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we can see from the examples above, both versions were built without a problem. Unfortunately, only the version with a numeric user ID was successfully executed. This is because the &lt;code&gt;USER&lt;/code&gt; instruction does not create a user in the image, but only switches to it. Docker, while running the container, could not replace the user's name with his UID (numeric ID), which caused the error.&lt;/p&gt;

&lt;p&gt;The solution is to manually add the user before using it. In images based on the &lt;a href="https://alpinelinux.org/" rel="noopener noreferrer"&gt;Alpine&lt;/a&gt; distribution, we can do this with the &lt;code&gt;adduser -D -u 1000 username&lt;/code&gt; command, where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-D&lt;/code&gt; parameter disables the password configuration,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-u 1000&lt;/code&gt; sets the numeric user and group ID to the indicated value,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;username&lt;/code&gt; is the name of the new user.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"
&amp;gt; FROM alpine:3.16
&amp;gt; RUN adduser -D -u 1000 nginx
&amp;gt; USER nginx
&amp;gt; "&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Dockerfile

tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker build &lt;span class="nb"&gt;.&lt;/span&gt;
Sending build context to Docker daemon  9.728kB
Step 1/3 : FROM alpine:3.16
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 9b18e9b68314
Step 2/3 : RUN adduser &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 1000 nginx
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Running &lt;span class="k"&gt;in &lt;/span&gt;a788a92773f9
Removing intermediate container a788a92773f9
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 84aecbe49298
Step 3/3 : USER nginx
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Running &lt;span class="k"&gt;in &lt;/span&gt;4db5f569c0dc
Removing intermediate container 4db5f569c0dc
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 63a098041085
Successfully built 63a098041085

tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; 63a098041085
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;id
&lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000&lt;span class="o"&gt;(&lt;/span&gt;nginx&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000&lt;span class="o"&gt;(&lt;/span&gt;nginx&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a result the user is successfully created and changed in the image. The next step is to adjust file and directory permissions. When copying data to the image with &lt;code&gt;COPY&lt;/code&gt; or &lt;code&gt;ADD&lt;/code&gt; instructions the ownership still remains &lt;code&gt;root&lt;/code&gt;, which is a default behavior. Docker's authors anticipated this situation and added the ability to change ownership on the fly. This saves the extra &lt;code&gt;RUN&lt;/code&gt; instruction, which would have to do it separately.&lt;/p&gt;

&lt;p&gt;To change the ownership of copied files and directories use the &lt;code&gt;--chown=&amp;lt;user&amp;gt;:&amp;lt;group&amp;gt;&lt;/code&gt; parameter of the &lt;code&gt;COPY&lt;/code&gt; or &lt;code&gt;ADD&lt;/code&gt; instruction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; alpine:3.16&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;adduser &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 1000 nginx
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=nginx . .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;If you have trouble remembering the &lt;code&gt;chown&lt;/code&gt; shortcut you can always think of its full version - "change owner"&lt;/em&gt;.&lt;/p&gt;

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

&lt;p&gt;Summarizing, the security principle of least privilege (POLP) can be applied to the Docker platform and containerization concept.  It is always a good practice to prevent uncontrolled files or directories ownership and give only the minimum level of access required. On the other hand it is a modus operandi that ownership as well as the process is executed by the same system user. This is definitely a good design as well as a way to meet the security rules and policies and improve the overall security of the system.&lt;/p&gt;

&lt;p&gt;Still feeling unsure or need more information about the Docker platform? You can always go through our Docker series to catch up with things.&lt;/p&gt;

&lt;p&gt;If this is not enough and you need professional support, do not hesitate to &lt;a href="https://u11d.com/contact" rel="noopener noreferrer"&gt;contact us&lt;/a&gt;. We are always happy to help.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>Understanding the Docker build context</title>
      <dc:creator>Daniel Kraszewski</dc:creator>
      <pubDate>Mon, 03 Nov 2025 13:56:03 +0000</pubDate>
      <link>https://dev.to/u11d/understanding-the-docker-build-context-4fmc</link>
      <guid>https://dev.to/u11d/understanding-the-docker-build-context-4fmc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;In the fourth installment of our Docker best practices series, we take a deep dive into the context of the build to understand further details of image building.&lt;br&gt;
Be sure you checked the previous articles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Proper use of cache to speed up and optimize builds.&lt;/li&gt;
&lt;li&gt;Selecting the appropriate base image.&lt;/li&gt;
&lt;li&gt;Understanding Docker multi-stage builds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Understanding the context of the build.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Using administrator privileges.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The context of the build - or where the megabytes in the console are coming from.
&lt;/h2&gt;

&lt;p&gt;When the view of a Dockerfile no longer makes you shudder and you have several working images in your account, you start paying attention to the small details. Along the way some questions start to arise. Why despite maintaining all of the best practices does the build cache not always work? Why does Docker send hundreds of megabytes before it finally starts building my image? The answers to both these questions comes down to understanding what the build context is and how it works.&lt;/p&gt;

&lt;p&gt;The Docker commands you execute in the console are not run directly by the &lt;code&gt;docker&lt;/code&gt; executable file. It only serves the purpose of a user interface that communicates with the Docker daemon running in the background of your computer or server. Communication is usually done using a Unix socket available as a &lt;code&gt;/var/run/docker.sock&lt;/code&gt; file or a TCP protocol on port &lt;code&gt;2375&lt;/code&gt; (&lt;code&gt;2376&lt;/code&gt; with encryption). In the following data coming from the console, you can see how the &lt;code&gt;docker version&lt;/code&gt; command will behave when the Docker daemon is unavailable on the system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker version
Client:
 Version:           20.10.17
 API version:       1.41
 Go version:        go1.17.11
 Git commit:        100c701
 Built:             Mon Jun  6 22:59:14 2022
 OS/Arch:           linux/arm64
 Context:           default
 Experimental:      &lt;span class="nb"&gt;true
&lt;/span&gt;Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image build command is no exception here. By running &lt;code&gt;docker build .&lt;/code&gt; we inform Docker to use the current directory (the dot at the end) as the data source. As a result, what's in a folder will be packaged and uploaded to the Docker daemon, where the instructions from the Dockerfile will be executed. Sounds reasonable right? The data sent to the server is called the &lt;em&gt;build context&lt;/em&gt;, and we can see its size in the first lines of the logs. In the following example, it is 104.9MB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;big_file.bin &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
100+0 records &lt;span class="k"&gt;in
&lt;/span&gt;100+0 records out
104857600 bytes &lt;span class="o"&gt;(&lt;/span&gt;105 MB, 100 MiB&lt;span class="o"&gt;)&lt;/span&gt; copied, 0.0607578 s, 1.7 GB/s

tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"
FROM alpine:3.16
COPY . .
"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Dockerfile

tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker build &lt;span class="nb"&gt;.&lt;/span&gt;
Sending build context to Docker daemon  104.9MB
Step 1/2 : FROM alpine:3.16
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 9b18e9b68314
Step 2/2 : COPY &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 23acc061163e
Successfully built 23acc061163e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker copies all the existing files and directories into the context, including the hidden ones like &lt;code&gt;.git&lt;/code&gt;. If we use, for example, &lt;code&gt;COPY . .&lt;/code&gt; to conveniently copy the entire context into the image, they will also be included. The hidden files are one of the most common reasons why the build cache works selectively, and we are unable to locate the cause.&lt;/p&gt;

&lt;p&gt;Another potential problem is copying compilation results, or installed packages remaining on the host system after testing. This usually leads to transferring large amounts of data to the Docker server (e.g. a huge &lt;code&gt;node_modules&lt;/code&gt; directory), and in the worst case, event developer versions of files.&lt;/p&gt;

&lt;p&gt;How to deal with this problem? Certainly, you should carefully copy data to the image, and in addition to the Dockerfile, prepare a &lt;code&gt;.dockerignore&lt;/code&gt; file. This file should be placed directly in the root directory of the build context and contain rules for ignoring what should not be included in the build context. Its syntax resembles the well-known equivalent of &lt;code&gt;.gitignore&lt;/code&gt; from the Git versioning system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Dockerfile
.dockerignore
.git
.idea
.vscode

/build
/dist
/gradle
/node_modules
/reports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Should &lt;code&gt;.dockerignore&lt;/code&gt; also exclude itself and the Dockerfile? If you don't need these files in the image then by all means yes. Docker leaves that decision up to you.&lt;/p&gt;

&lt;p&gt;Finally, let's see what the logs from the earlier example look like when we ignore all files with the &lt;code&gt;.bin&lt;/code&gt; extension.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;big_file.bin &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
100+0 records &lt;span class="k"&gt;in
&lt;/span&gt;100+0 records out
104857600 bytes &lt;span class="o"&gt;(&lt;/span&gt;105 MB, 100 MiB&lt;span class="o"&gt;)&lt;/span&gt; copied, 0.0817181 s, 1.3 GB/s

tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"
FROM alpine:3.16
COPY . .
"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Dockerfile

tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"
&amp;gt; *.bin
&amp;gt; "&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .dockerignore

tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker build &lt;span class="nb"&gt;.&lt;/span&gt;
Sending build context to Docker daemon  10.75kB
Step 1/2 : FROM alpine:3.16
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 9b18e9b68314
Step 2/2 : COPY &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
 &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 0ef632c6dacf
Successfully built 0ef632c6dacf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you probably already found out, the build context shrank to 10.75kB. This way we have been able to achieve better performance, efficiency and much lower data transfer rates between host and the Docker server.&lt;/p&gt;

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

&lt;p&gt;As we found out, the context of the build process refers to the set of files and directories that are used as input to the build process. This can include source code files, configuration files, and any other files that are needed to create the final Docker image. The build context is the set of input files that are provided to the build system, and it is used to determine which files should be included in the final build output.&lt;br&gt;
As a next step, I suggest revisiting the images you have previously created and looking at the image building process from a build context perspective. There is a chance that you may be able to introduce some valuable optimization.&lt;/p&gt;

&lt;p&gt;Check out the next article that refers to cybersecurity and applies the principle of least privilege to control the artifact’s ownership.&lt;br&gt;
&lt;strong&gt;Using administrator privileges&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>performance</category>
    </item>
    <item>
      <title>Understanding Docker multi-stage builds</title>
      <dc:creator>Daniel Kraszewski</dc:creator>
      <pubDate>Mon, 27 Oct 2025 09:29:26 +0000</pubDate>
      <link>https://dev.to/u11d/understanding-docker-multi-stage-builds-572n</link>
      <guid>https://dev.to/u11d/understanding-docker-multi-stage-builds-572n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Third article in the series of Docker best practices will help you to understand how to make use of multi-stage Docker builds feature. If this is the first article in this series for you, be sure to check out the others.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Multi-stage builds
&lt;/h2&gt;

&lt;p&gt;Many best practices on how to properly prepare Docker images focus their attention on size of images and number of layers. Certainly, the consequences of both of these parameters may result in the rapid exhausting of disk space or long downloading times of base images. This way it is very easy to "pollute" the resulting Docker images.&lt;br&gt;
Not so long ago, since Docker version 17.05, a new mechanism was introduced to keep images "clean" in a quite convenient way. This mechanism is called &lt;code&gt;multi-stage builds&lt;/code&gt;. Please see the exercise below to get the idea of multi-staging and the benefits it brings.&lt;/p&gt;

&lt;p&gt;Let's start by preparing a sample application that we want to place in a Docker image. This will be a web application created using the &lt;a href="https://reactjs.org/" rel="noopener noreferrer"&gt;React&lt;/a&gt; framework and its &lt;a href="https://github.com/facebook/create-react-app" rel="noopener noreferrer"&gt;create-react-app&lt;/a&gt; tool. It will generate a code template and configuration, allowing us to focus on the image creation aspects.&lt;/p&gt;

&lt;p&gt;Instead of installing Node.js locally, let's take advantage of the benefits of Docker and spawn a temporary container to generate the skeleton of React the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;PWD&lt;span class="si"&gt;)&lt;/span&gt;:/opt &lt;span class="nt"&gt;-w&lt;/span&gt; /opt &lt;span class="nt"&gt;--entrypoint&lt;/span&gt; sh node:18.7.0-alpine &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"npm install create-react-app &amp;amp;&amp;amp; npx create-react-app example"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Tip: If you are using Windows, we recommend using &lt;a href="https://docs.microsoft.com/en-us/windows/wsl/wsl2-index" rel="noopener noreferrer"&gt;WSL2&lt;/a&gt;.&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The above line will start a container with Node.js 18 based on the lightweight distribution &lt;a href="https://alpinelinux.org/" rel="noopener noreferrer"&gt;Alpine&lt;/a&gt;. The current directory will be mounted under the &lt;code&gt;/opt&lt;/code&gt; path. The default action after container launch will be to install the &lt;code&gt;create-react-app&lt;/code&gt; package and run it using the &lt;code&gt;npx&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;Ok, we have the application. Now let’s proceed with the "classic" Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nginx:1.23.1-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /opt/example&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;--virtual&lt;/span&gt; .build-deps &lt;span class="se"&gt;\
&lt;/span&gt;  nodejs &lt;span class="se"&gt;\
&lt;/span&gt;  npm
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; build/&lt;span class="k"&gt;*&lt;/span&gt; /usr/share/nginx/html &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apk del .build-deps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use the NGINX base image, which is one of the most popular web servers. We change the current directory to &lt;code&gt;/opt/example&lt;/code&gt;, and then install Node.js and NPM, which are necessary to build our application. We copy the package information and install it with the &lt;code&gt;npm ci&lt;/code&gt; command. The next step is to copy all the code of the application and build it with &lt;code&gt;npm run build&lt;/code&gt;. Then eventually copy the results to the directory where the web server expects them and uninstall Node.js along with NPM.&lt;/p&gt;

&lt;p&gt;Has anything caught your attention? You are right, layers! By design Docker images are made up of layers. As a result if Node.js and NPM were installed with a separate &lt;code&gt;RUN&lt;/code&gt; instruction then unfortunately deleting them at the end will not make the image smaller.&lt;/p&gt;

&lt;p&gt;Before multi-stage builds were introduced, one solution for this problem was to split the Dockerfile into two parts or copy files previously prepared on the host system into the image. However both solutions are against the idea of moving the application preparation process to an independent environment.&lt;/p&gt;

&lt;p&gt;Let's take a look at a Dockerfile which uses a multi-stage builds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:18.7.0-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /opt/example&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nginx:1.23.1-alpine&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /opt/example/build/* /usr/share/nginx/html/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isn't this form more readable? What you should pay attention to is the use of more than one &lt;code&gt;FROM&lt;/code&gt; instruction. Each one starts the image creation process from scratch, but we can still use the results of the previous ones thanks to &lt;code&gt;COPY --from=...&lt;/code&gt; instruction. The &lt;code&gt;from&lt;/code&gt; parameter can take the index of the step, its name is given with &lt;code&gt;AS&lt;/code&gt; (in our case it is &lt;code&gt;builder&lt;/code&gt;), or the name of a completely separate image.&lt;/p&gt;

&lt;p&gt;Analyzing the Dockerfile step by step you can notice that the image building process starts with selecting the Node.js 18.7.0 image based on the &lt;a href="https://alpinelinux.org/" rel="noopener noreferrer"&gt;Alpine&lt;/a&gt; distribution. We labeled this stage as &lt;code&gt;builder&lt;/code&gt;. In the next step we set the current directory and copied the information about the required packages into it. We installed the packages using &lt;code&gt;npm ci&lt;/code&gt; and copied the application code and ran &lt;code&gt;npm run build&lt;/code&gt;. The next instruction is to start a new stage based on the NGINX server image, this time without any additional name. We copied the files of the prepared application from the previous step to the place required by the server configuration.&lt;/p&gt;

&lt;p&gt;Thanks to the multi-step build, the image we built contains exactly what we need: the web server and the generated application files. This approach also brings some form of simplification because we are able to perform the whole process by running a single &lt;code&gt;docker build&lt;/code&gt; command.&lt;/p&gt;

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

&lt;p&gt;Docker multi-stage builds are a feature in Docker that allows creating Docker images using multiple build stages. This splits the build process into multiple steps, each of which can use a different base image and produce a different intermediate image. This is useful because it allows one to take advantage of the benefits of different base images, while also keeping the final image as small and efficient as possible.&lt;br&gt;
Overall, using multi-stage builds can help improve the performance and efficiency of Docker images, while also making the build process more flexible and customizable.&lt;/p&gt;

&lt;p&gt;If you are interesting why there may be a lot of data transferred during build process, please check the next article in the series:&lt;br&gt;
&lt;strong&gt;Understanding the context of the build&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Speed up Docker image builds with cache management</title>
      <dc:creator>Daniel Kraszewski</dc:creator>
      <pubDate>Wed, 15 Oct 2025 19:13:06 +0000</pubDate>
      <link>https://dev.to/u11d/speed-up-docker-image-builds-with-cache-management-1di</link>
      <guid>https://dev.to/u11d/speed-up-docker-image-builds-with-cache-management-1di</guid>
      <description>&lt;p&gt;Over the past few years I have been working in multiple IT projects where the Docker platform was used to develop, ship and run applications targeting various industries. In addition, I have conducted many interviews and over the time I have noticed that many DevOps engineers do not pay enough attention to the details and essential elements of the platform.&lt;/p&gt;

&lt;p&gt;Therefore, I decided to collect and summarize information relevant to create optimal Docker images.&lt;/p&gt;

&lt;p&gt;I see five critical areas that are often overlooked or misused by developers. This is the first article related to this topic. Please see the full list of topics that will be covered below.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Proper use of cache to speed up and optimize builds.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/u11d/selecting-the-appropriate-docker-base-image-2126"&gt;Selecting the appropriate base image.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Understanding Docker multi-stage builds.&lt;/li&gt;
&lt;li&gt;Understanding the context of the build.&lt;/li&gt;
&lt;li&gt;Using administrator privileges.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I encourage you to start writing an optimal Dockerfiles journey now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed up image builds with cache management
&lt;/h2&gt;

&lt;p&gt;What interests us the most in creating Docker images is the ability to add custom files and execute commands, such as installing dependencies or compiling code. We can achieve these goals very quickly by creating a Dockerfile that resembles the steps we would perform in a console.&lt;/p&gt;

&lt;p&gt;An example for Node.js, where npm is the dependency management tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM node:18.7.0-alpine
COPY . .
RUN npm ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the above example, we are using the Node.js 18 image in a lightweight version based on a Linux distribution called Alpine. We copy all the files from the current context and start installing dependencies using the npm ci command.&lt;/p&gt;

&lt;p&gt;Simple right? However, this is not the optimal approach from Docker's point of view. This is because it uses an internal mechanism that allows you to reuse the layers of an image you built earlier. This mechanism will not be used if we leave the Dockerfile as presented.&lt;/p&gt;

&lt;p&gt;The image build cache is not very complicated to use. When copying files to an image using the ADD or COPY statement, Docker compares the contents of the files and their metadata with those it already has (using checksums). If nothing has changed, it will use the previously prepared layers, including for subsequent RUN instructions. Encountering another ADD or COPY it will check the files again, and so on until the end of the build process.&lt;/p&gt;

&lt;p&gt;Our Dockerfile should therefore look in the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM node:18.7.0-alpine
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As before, we use the same Node.js base image. This time, however, instead of immediately copying all the files into the image, we first copy only the files responsible for the information about the required dependencies. We run dependencies installation and finally copy the application code.&lt;/p&gt;

&lt;p&gt;Let's now follow what the process of building this image will look like, assuming that only the application code has changed and the dependencies remain the same. Docker will encounter the first use of the COPY instruction, checking its resources it will find that nothing has changed, so it will use the previously prepared layer. In the next step, it will compare the contents of the RUN instruction. It remains the same, so you can also use the existing layer here. In the last step, it will copy the new application files into the image, since it's the only place where the changes have been made.&lt;/p&gt;

&lt;p&gt;How much time and IOPS we have saved, will be understood by anyone who has at least once seen the size and number of files in the node_modules directory, where Node.js stores packages. After all, this is not an exception, similar dependency management can now be found in many languages/environments.&lt;/p&gt;

&lt;p&gt;It is worth noting that installing changed dependencies is just one of many tasks that are performed less frequently than actual application code changes. Sometimes there is a need to prepare the appropriate directory structure, permissions or users accounts. All of these operations should be declared in the Dockerfile as early as possible, and using this rule is the easiest way to properly use the caching mechanisms in Docker.&lt;/p&gt;

&lt;p&gt;If you want to know more about Docker best practices check out the next article in this series: &lt;a href="https://dev.to/u11d/selecting-the-appropriate-docker-base-image-2126"&gt;Selecting the appropriate Docker base image &lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>security</category>
      <category>u11d</category>
    </item>
    <item>
      <title>Selecting the appropriate Docker base image</title>
      <dc:creator>Daniel Kraszewski</dc:creator>
      <pubDate>Tue, 14 Oct 2025 15:15:25 +0000</pubDate>
      <link>https://dev.to/u11d/selecting-the-appropriate-docker-base-image-2126</link>
      <guid>https://dev.to/u11d/selecting-the-appropriate-docker-base-image-2126</guid>
      <description>&lt;p&gt;This is the second article related to Docker best practices. In this  article I will explain how exactly you can choose the best base image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose base images wisely
&lt;/h2&gt;

&lt;p&gt;When learning Docker, we very quickly come across descriptions of how images are built. A set of layers that, stacked one on top of the other, form the final file system of a running container. Seemingly clear, but what do they give us in practice?&lt;br&gt;
First of all, the fact that we can (although we don't have to) use another image as the base of our image, e.g. one available on public registries such as &lt;a href="https://hub.docker.com" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt;. It can be Ubuntu, CentOS, a Python interpreter or Bash. What matters is what libraries and tools we need to port our project to the Docker environment.&lt;/p&gt;

&lt;p&gt;We configure the base image (that's how we call the image that our implementation will be based on) using one of the most commonly used instructions in the Dockerfile - the &lt;code&gt;FROM&lt;/code&gt; instruction. Assuming that this time we are Dockerizing an application written in Python, an example of its use is shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.10.6&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; helloworld.py .&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; [ "python", "./helloworld.py" ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line instructs Docker that we want to use an existing Python image with the &lt;code&gt;3.10.6&lt;/code&gt; tag as the base image. The &lt;code&gt;3.10.6&lt;/code&gt; tag as described in &lt;a href="https://hub.docker.com/_/python" rel="noopener noreferrer"&gt;repository&lt;/a&gt; means using Python version 3.10.6. In the second line of the Dockerfile we copy our sample application, and in the third line we decide that it will be executed when the Docker container starts.&lt;/p&gt;

&lt;p&gt;At this point, we already know that we don't need to install Python manually. Someone has prepared the Python image for us. Same as the community prepares all kinds of packages used in software development. &lt;em&gt;The question arises, however, what to look for when selecting a base image?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is the author?
&lt;/h2&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%2Fjhekqrmx0050crbx5whz.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%2Fjhekqrmx0050crbx5whz.png" alt="docker-official-images.png" width="800" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Anyone can publish their images on &lt;a href="https://hub.docker.com" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt;, which comes with risk of including malicious code in their application. To minimize this risk, it is worth checking who the author is in the &lt;code&gt;By&lt;/code&gt; section. As an added convenience, you can find trusted images labeled with &lt;code&gt;Docker Official Image&lt;/code&gt; and &lt;code&gt;Verified Publisher&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&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%2F2paaw0ydbawub2nn2vwp.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%2F2paaw0ydbawub2nn2vwp.png" alt="architecture.png" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Images are built to run in specific environments. Code compiled for the processor of a typical desktop computer will not run directly on a MacBook M1/2 or RaspberryPi. Although it is more advanced knowledge, it is worth checking that the appropriate architecture is available in the list of tags.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version
&lt;/h2&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%2Fqibmpnjeynqkt1jcq92a.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%2Fqibmpnjeynqkt1jcq92a.png" alt="version.png" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When deciding on a version, it's a good idea to choose a tag that narrows down the possible image content as much as possible. Even upgrading with a patch version, may inconsistently cause backward compatibility problems that we do not expect. Therefore, given a choice of &lt;code&gt;python:3&lt;/code&gt;, &lt;code&gt;python:3.10&lt;/code&gt; or &lt;code&gt;python:3.10.6&lt;/code&gt;, a reasonable choice would be the latter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Size
&lt;/h2&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%2Ffmhylqgtnazaol6y7imd.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%2Ffmhylqgtnazaol6y7imd.png" alt="size.png" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The size of the image translates directly into the amount of data needed to be sent over the network, as well as disk space. If we need to run a script in Bash, it is not worth using all of Ubuntu for this. A good practice is to select images tailored for specific needs, e.g. using smaller and leaner Linux distributions like &lt;a href="https://alpinelinux.org/" rel="noopener noreferrer"&gt;Alpine&lt;/a&gt;. Such images often have &lt;code&gt;alpine&lt;/code&gt; in the tags.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security
&lt;/h2&gt;

&lt;p&gt;Typical Linux distributions contain many libraries and tools that could potentially bring vulnerabilities. An immediate way to minimize the risk is to use smaller distributions, like the aforementioned &lt;a href="https://alpinelinux.org/" rel="noopener noreferrer"&gt;Alpine&lt;/a&gt;. Fewer dependencies mean simpler monitoring and upgradeability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standard library C
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tips@u11d:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; alpine:3.16
&lt;span class="nv"&gt;$ &lt;/span&gt;wget &lt;span class="nt"&gt;-O&lt;/span&gt; docker-compose https://github.com/docker/compose/releases/download/v2.7.0/docker-compose-linux-x86_64
Connecting to github.com &lt;span class="o"&gt;(&lt;/span&gt;140.82.121.3:443&lt;span class="o"&gt;)&lt;/span&gt;
Connecting to objects.githubusercontent.com &lt;span class="o"&gt;(&lt;/span&gt;185.199.111.133:443&lt;span class="o"&gt;)&lt;/span&gt;
saving to &lt;span class="s1"&gt;'docker-compose'&lt;/span&gt;
docker-compose       100% |&lt;span class="k"&gt;*****************************************&lt;/span&gt;| 11.6M  0:00:00 ETA
&lt;span class="s1"&gt;'docker-compose'&lt;/span&gt; saved

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x docker-compose

&lt;span class="nv"&gt;$ &lt;/span&gt;./docker-compose
/bin/sh: ./docker-compose: not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Unix-like systems, the standard library is treated as part of the operating system. This means that we will not be able to run an application built with &lt;code&gt;glibc&lt;/code&gt; in an image based on, for example, the &lt;code&gt;musl&lt;/code&gt; library. An example of this would be trying to run the &lt;code&gt;docker-compose&lt;/code&gt; utility downloaded directly from GitHub in an image based on &lt;a href="https://alpinelinux.org/" rel="noopener noreferrer"&gt;Alpine&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Is there anything else you can do better? Yes, such as building your image completely from scratch using &lt;code&gt;FROM scratch&lt;/code&gt; and putting only the application and its direct dependencies in it. It is also worth looking at the &lt;a href="https://github.com/GoogleContainerTools/distroless" rel="noopener noreferrer"&gt;Distroless&lt;/a&gt; initiative striving to achieve the same goal.&lt;/p&gt;

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

&lt;p&gt;Selecting a good Docker base image is important for several reasons. First, the base image forms the foundation of Docker image, and it provides the underlying OS and runtime environment that the application will run in. Therefore, choosing a base image that is well-suited to your needs can help ensure that the application runs efficiently.&lt;/p&gt;

&lt;p&gt;Second, the base image can impact the size of the final Docker image. For example, choosing a base image that includes a lot of unnecessary libraries or applications may end up with bloated Docker images. This can affect the performance of applications and make it more difficult to distribute and deploy.&lt;/p&gt;

&lt;p&gt;Finally, the base image can also impact the security of a final Docker image. Using a base image that is known to have vulnerabilities results in a fact that Docker image may be more susceptible to attack. Therefore, it's important to choose a base image that is well-maintained and regularly updated with security patches.&lt;/p&gt;

&lt;p&gt;If you want to build performant Docker images check out the next article in this series:&lt;br&gt;
Understanding Docker multi-stage builds&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>security</category>
    </item>
    <item>
      <title>Exposing Private Load Balancers with CloudFront VPC Origins</title>
      <dc:creator>Daniel Kraszewski</dc:creator>
      <pubDate>Mon, 06 Oct 2025 08:41:06 +0000</pubDate>
      <link>https://dev.to/u11d/exposing-private-load-balancers-with-cloudfront-vpc-origins-4ffo</link>
      <guid>https://dev.to/u11d/exposing-private-load-balancers-with-cloudfront-vpc-origins-4ffo</guid>
      <description>&lt;p&gt;Let's explore &lt;a href="https://aws.amazon.com/blogs/networking-and-content-delivery/introducing-cloudfront-virtual-private-cloud-vpc-origins-shield-your-web-applications-from-public-internet/" rel="noopener noreferrer"&gt;CloudFront VPC Origins&lt;/a&gt;, an AWS feature that allows you to connect CloudFront directly to private resources in your VPC without exposing them to the Internet. In this article, we'll see why this matters and how you can implement it using &lt;a href="https://developer.hashicorp.com/terraform" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Introduction: Why Keep Your Load Balancers Private?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When building web applications on AWS, we've traditionally needed public load balancers so CloudFront could reach our origin servers. This presented several challenges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your load balancer is exposed to the world&lt;/strong&gt; - Even with tight security groups, a public load balancer increases your application's attack surface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintaining IP allow lists is a pain&lt;/strong&gt; - The traditional approach requires whitelisting CloudFront's IP ranges in your security groups, which change periodically and require updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret headers aren't secure enough&lt;/strong&gt; - Some try using secret headers as an alternative to IP whitelisting, but this "security by obscurity" approach can be discovered and exploited&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct access bypassing&lt;/strong&gt; - Attackers might discover your load balancer's public endpoint and bypass CloudFront entirely, circumventing any protections you've set up there&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;CloudFront VPC Origins provides a better solution. It creates a direct, secure connection between CloudFront and resources in your private subnets, keeping your load balancers completely hidden from the public internet while still being accessible through CloudFront.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Architecture&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before diving into implementation details, let's visualize how CloudFront VPC Origins creates a secure architecture. The diagram below illustrates the end-to-end flow from internet users to your private resources, showing how CloudFront VPC Origins bridges the gap between the public internet and your private VPC without exposing your infrastructure:&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%2F8b228g1hdsaf4gi1bo9o.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%2F8b228g1hdsaf4gi1bo9o.png" alt=" " width="471" height="791"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Practical Implementation with Terraform&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Let's implement this using Terraform, based on our &lt;a href="https://registry.terraform.io/modules/u11d-com/medusajs/aws" rel="noopener noreferrer"&gt;Medusa.js AWS module&lt;/a&gt;. We'll go through this in a logical order based on dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Setting Up the Private Load Balancer&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;First, we create a load balancer in private subnets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_lb" "main" {
  load_balancer_type = "application"  # Default value
  subnets            = var.vpc.private_subnet_ids  # The key part - using private subnets!
  security_groups    = [aws_security_group.lb.id]
  name               = "${local.prefix}-lb"
  tags               = local.tags
}

resource "aws_lb_target_group" "main" {
  port        = 9000  # Default container port for Medusa backend
  protocol    = "HTTP"
  vpc_id      = var.vpc.id
  target_type = "ip"
  name        = "${local.prefix}-tg"
  health_check {
    protocol            = "HTTP"
    port                = 9000
    interval            = 30
    matcher             = "200"
    timeout             = 3
    path                = "/health"
    healthy_threshold   = 3
    unhealthy_threshold = 3
  }

  tags = local.tags
}

resource "aws_lb_listener" "main" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
        type = "forward"
        forward {
            target_group {
                arn = aws_lb_target_group.main.arn
            }
        }
    }

  lifecycle {
    replace_triggered_by = [aws_lb_target_group.main]
  }

  tags = local.tags
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;2. Creating the Security Groups&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Next, we set up security groups that only allow traffic from CloudFront VPC Origins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Using AWS-managed prefix list for CloudFront VPC Origins
data "aws_ec2_managed_prefix_list" "vpc_origin" {
  name = "com.amazonaws.global.cloudfront.origin-facing"
}

resource "aws_security_group" "lb" {
  name_prefix = "${local.prefix}-lb-"
  description = "Allow inbound traffic from CloudFront VPC Origins"
  vpc_id      = var.vpc.id
  tags        = local.tags

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_vpc_security_group_ingress_rule" "vpc_origin" {
  security_group_id = aws_security_group.lb.id
  prefix_list_id    = data.aws_ec2_managed_prefix_list.vpc_origin.id
  from_port         = 80
  to_port           = 80
  ip_protocol       = "tcp"
  tags              = local.tags
}

resource "aws_vpc_security_group_egress_rule" "lb" {
  security_group_id = aws_security_group.lb.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
  tags              = local.tags
}

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

&lt;/div&gt;



&lt;p&gt;This is where the magic happens - instead of maintaining our own list of CloudFront IPs, we use AWS's managed prefix list &lt;code&gt;com.amazonaws.global.cloudfront.origin-facing&lt;/code&gt;. AWS keeps this updated automatically, so you don't have to worry about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Setting Up the VPC Origin Connection&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Now, we create the CloudFront VPC Origin that connects to our private load balancer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;locals {
  origin_id = "${local.prefix}-lb"
}

resource "aws_cloudfront_vpc_origin" "main" {
  vpc_origin_endpoint_config {
    name                   = local.origin_id
    arn                    = aws_lb.main.arn
    http_port              = 80
    https_port             = 443
    origin_protocol_policy = "http-only"

    origin_ssl_protocols {
      quantity = 1
      items    = ["TLSv1.2"]
    }
  }

  timeouts {
    create = "30m"  # Important: VPC Origins take time to provision
  }

  depends_on = [aws_lb_target_group.main, aws_security_group.lb]

  tags = local.tags
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: CloudFront VPC Origins can take a while to provision, and without extended timeout, Terraform might give up too early.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Configuring the CloudFront Distribution&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Finally, we set up the CloudFront distribution to use our VPC Origin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_cloudfront_distribution" "main" {
  enabled = true
  comment = "My-Project-Dev-Backend"  # Example of what title(local.prefix) might render

  origin {
    domain_name = aws_lb.main.dns_name
    origin_id   = local.origin_id

    vpc_origin_config {
      vpc_origin_id = aws_cloudfront_vpc_origin.main.id
    }
  }

  # Default cache behavior
  default_cache_behavior {
    target_origin_id       = local.origin_id
    viewer_protocol_policy = "redirect-to-https"

    # Cache settings for APIs - disabled to allow dynamic content
    min_ttl     = 0
    default_ttl = 0
    max_ttl     = 0

    forwarded_values {
      query_string = true
      headers      = ["*"]

      cookies {
        forward = "all"
      }
    }

    allowed_methods = ["GET", "HEAD", "POST", "PUT", "PATCH", "OPTIONS", "DELETE"]
    cached_methods  = ["GET", "HEAD", "OPTIONS"]
  }

  # Certificate configuration
  viewer_certificate {
    cloudfront_default_certificate = true  # Use CloudFront's default certificate
  }

  # Other settings
  price_class = "PriceClass_100"  # Use only North America and Europe edge locations

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  tags = {
    Project     = "my-project"
    Environment = "dev"
    Owner       = "DevOps Team"
    ManagedBy   = "terraform"
    Component   = "Backend"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;vpc_origin_config block&lt;/code&gt; that references our VPC Origin is the key difference from a traditional CloudFront setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion: Solving Real Security Challenges&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;CloudFront VPC Origins represents a significant advancement for securing web applications on AWS, directly addressing the security challenges we outlined at the beginning:&lt;/p&gt;

&lt;p&gt;Remember the problem of exposed load balancers? With VPC Origins, your load balancers now remain completely isolated in private subnets, dramatically reducing your attack surface. The headache of maintaining IP allowlists is eliminated through AWS's managed prefix list &lt;code&gt;com.amazonaws.global.cloudfront.origin-facing&lt;/code&gt;, which is automatically maintained for you.&lt;/p&gt;

&lt;p&gt;Insecure secret header approaches are no longer needed, as the direct connection between CloudFront and your VPC resources provides a much stronger security model. And attackers trying to bypass CloudFront? With your load balancer in a private subnet, there's simply no way for external traffic to reach it except through CloudFront.&lt;/p&gt;

&lt;p&gt;There are a few practical considerations: CloudFront VPC Origins take time to provision, each region needs its own VPC Origin, there's a cost for the connectivity, and setup is slightly more complex. However, for organizations handling sensitive data or with compliance requirements, these minor considerations are easily outweighed by the significant security benefits.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>devops</category>
      <category>medusajs</category>
    </item>
    <item>
      <title>Terraform as a One-Shot Init Container in Docker Compose and CI: Ending "It Worked On My Machine"</title>
      <dc:creator>Daniel Kraszewski</dc:creator>
      <pubDate>Wed, 24 Sep 2025 09:15:04 +0000</pubDate>
      <link>https://dev.to/u11d/terraform-as-a-one-shot-init-container-in-docker-compose-and-ci-ending-it-worked-on-my-machine-2c49</link>
      <guid>https://dev.to/u11d/terraform-as-a-one-shot-init-container-in-docker-compose-and-ci-ending-it-worked-on-my-machine-2c49</guid>
      <description>&lt;p&gt;&lt;em&gt;Picture this: It's Friday afternoon. Your pull request looks perfect locally - tests green, endpoints responsive, everything just works. You push to GitHub, confident it'll sail through CI. Twenty minutes later: red build. An Elasticsearch error pops up: "no such index [blog_posts]". This all-too-common 'it worked on my machine' problem highlights the dangers of environment drift - exactly what Terraform as a one-shot init container in Docker Compose and CI is designed to solve.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You scramble to check. Locally? Index exists. CI logs? Nothing obvious. You spend an hour discovering that your local Docker setup manually created that index months ago, while CI starts fresh every time. The infrastructure your app depends on exists in your head, not in code.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If this sounds familiar, you're not alone. Most of us have lived through the pain of &lt;strong&gt;environment drift&lt;/strong&gt; - where your local development setup slowly becomes a unique snowflake that no one else can reproduce. Your &lt;code&gt;docker compose up&lt;/code&gt; works, but only because of that one-off &lt;code&gt;curl&lt;/code&gt; command you ran three sprints ago to "fix" an index mapping.&lt;/p&gt;

&lt;p&gt;This post shows you a pattern that eliminates this drift entirely: &lt;strong&gt;treat infrastructure setup as code that runs automatically in every environment&lt;/strong&gt;. We'll use Terraform as a one-shot initialization container that provisions Elasticsearch indices before your app even starts. The same container that sets up your local dev environment also runs in CI and can deploy to production. No more manual steps, no more "works on my machine," no more Friday afternoon mysteries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Infrastructure Assumptions Hidden in Plain Sight
&lt;/h2&gt;

&lt;p&gt;Before diving into the solution, let's get concrete about what goes wrong. Consider a typical FastAPI application that needs Elasticsearch. Most tutorials show you this:&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;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/blogs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_blog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BlogCreate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blog_posts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks simple, right? But this code makes a &lt;strong&gt;critical assumption&lt;/strong&gt;: the &lt;code&gt;blog_posts&lt;/code&gt; index exists and has the right mapping. In development, you probably created it once with a curl command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="s2"&gt;"localhost:9200/blog_posts"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;'
{
  "mappings": {
    "properties": {
      "title": {"type": "text"},
      "content": {"type": "text"},
      "author": {"type": "keyword"}
    }
  }
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your app works perfectly... until a new team member clones the repo, runs &lt;code&gt;docker compose up&lt;/code&gt;, and gets index errors. Or until CI runs in a clean environment. Or until you deploy to production and forget to create the index there too.&lt;/p&gt;

&lt;p&gt;The real problem isn't that you forgot to document the setup step (though that happens). It's that &lt;strong&gt;the setup step exists outside your application's deployment process&lt;/strong&gt;. Your app code and your infrastructure setup live in separate worlds, creating countless opportunities for them to drift apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Infrastructure as Code, Everywhere
&lt;/h2&gt;

&lt;p&gt;Here's the key insight: &lt;strong&gt;if your app needs infrastructure to exist, that infrastructure should be created by code, not by hand&lt;/strong&gt;. And that code should run automatically in every environment where your app runs.&lt;/p&gt;

&lt;p&gt;We'll build a FastAPI blog application that needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Elasticsearch indices with specific mappings&lt;/li&gt;
&lt;li&gt;An API key with limited permissions&lt;/li&gt;
&lt;li&gt;Audit logging that writes to a separate index&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of hoping these exist, we'll use Terraform to create them. But here's the twist: &lt;strong&gt;Terraform runs as a container in our Docker Compose stack&lt;/strong&gt;, not as a separate manual step.&lt;/p&gt;

&lt;p&gt;Let's look at how this works in practice. Our &lt;code&gt;compose.yaml&lt;/code&gt; defines a &lt;code&gt;terraform&lt;/code&gt; service that runs once and exits:&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;terraform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/terraform:1.13.1&lt;/span&gt;
  &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;no&lt;/span&gt;
  &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/workspace/terraform&lt;/span&gt;
  &lt;span class="na"&gt;entrypoint&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;/bin/sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-ec"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;terraform init -backend-config=local.s3.tfbackend&lt;/span&gt;
    &lt;span class="s"&gt;terraform apply -var-file=local.tfvars -auto-approve&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/workspace&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;terraform_data:/workspace/terraform/.terraform&lt;/span&gt;
  &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;minio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;elasticsearch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This container waits for MinIO and Elasticsearch to be healthy, then runs &lt;code&gt;terraform apply&lt;/code&gt; to create our indices and API key. It uses MinIO instance as an S3 backend for state storage, making the whole setup self-contained.&lt;/p&gt;

&lt;p&gt;The magic is in the ordering guarantees. With &lt;code&gt;depends_on&lt;/code&gt; and healthchecks, we get a deterministic startup sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;MinIO starts and becomes healthy (S3 backend ready)&lt;/li&gt;
&lt;li&gt;Elasticsearch starts and becomes healthy (database ready)&lt;/li&gt;
&lt;li&gt;Terraform runs and provisions indices/API key (infrastructure ready)&lt;/li&gt;
&lt;li&gt;Only then can your application start or tests run&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Terraform Actually Creates
&lt;/h2&gt;

&lt;p&gt;Let's look at the concrete infrastructure our application needs. Here's the Terraform configuration that runs in that container:&lt;br&gt;
&lt;/p&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;"elasticstack_elasticsearch_index"&lt;/span&gt; &lt;span class="s2"&gt;"blog_posts"&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;"blog_posts"&lt;/span&gt;

  &lt;span class="nx"&gt;mappings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;title&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"text"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;content&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"text"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;author&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"keyword"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;created_at&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"date"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;updated_at&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"date"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"integer"&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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"elasticstack_elasticsearch_index"&lt;/span&gt; &lt;span class="s2"&gt;"blog_logs"&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;"blog_posts_log"&lt;/span&gt;

  &lt;span class="nx"&gt;mappings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;action&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"keyword"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;blog_id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"keyword"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"date"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"integer"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;data&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"object"&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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"elasticstack_elasticsearch_security_api_key"&lt;/span&gt; &lt;span class="s2"&gt;"backend"&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;"backend"&lt;/span&gt;

  &lt;span class="nx"&gt;role_descriptors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;blog_backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;indices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="nx"&gt;names&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"blog_posts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"blog_posts_log"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;privileges&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"maintenance"&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="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 is infrastructure as code at its best. Every field mapping, every privilege, every index name is explicitly defined. No assumptions, no manual steps, no tribal knowledge. When you change a mapping, you update this file and redeploy. When you add a new index, it's defined here first.&lt;/p&gt;

&lt;p&gt;The beautiful part? This exact same Terraform code can run against a local Elasticsearch instance, a staging cluster, or production. The only thing that changes is the connection configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local ↔ CI Parity: The Same Flow Everywhere
&lt;/h2&gt;

&lt;p&gt;Now here's where this approach really shines: &lt;strong&gt;the exact same workflow runs locally and in CI&lt;/strong&gt;. No special CI scripts, no different Docker configurations, no "it works locally but not in CI" mysteries.&lt;/p&gt;

&lt;p&gt;Locally, you run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="c"&gt;# Wait for terraform container to exit successfully&lt;/span&gt;
pytest &lt;span class="nt"&gt;-v&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;In GitHub Actions, the workflow does 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="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;Start compose stack&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker compose -p blog up -d&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;Wait for terraform to finish&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;while [ "$(docker inspect -f '{{.State.Status}}' blog-terraform-1)" != "exited" ]; do&lt;/span&gt;
      &lt;span class="s"&gt;sleep 1&lt;/span&gt;
    &lt;span class="s"&gt;done&lt;/span&gt;
    &lt;span class="s"&gt;terraform_exit_code=$(docker inspect -f '{{.State.ExitCode}}' blog-terraform-1)&lt;/span&gt;
    &lt;span class="s"&gt;if [ "$terraform_exit_code" != "0" ]; then&lt;/span&gt;
      &lt;span class="s"&gt;echo "Terraform failed with exit code $terraform_exit_code"&lt;/span&gt;
      &lt;span class="s"&gt;docker logs blog-terraform-1&lt;/span&gt;
      &lt;span class="s"&gt;exit 1&lt;/span&gt;
    &lt;span class="s"&gt;fi&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;Run tests&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest -v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CI workflow is just the automated version of what you do locally. Same containers, same Terraform, same tests. If it works locally, it works in CI. If it fails in CI, you can reproduce the failure locally by running the exact same commands.&lt;/p&gt;

&lt;p&gt;This eliminates the most frustrating class of CI failures: the ones that only happen "in the cloud" because the environment is subtly different from your local setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Against Real Infrastructure (Not Mocks)
&lt;/h2&gt;

&lt;p&gt;Here's where this approach gets really powerful for testing. Instead of mocking Elasticsearch calls, our tests run against the real thing:&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;test_create_blog_and_log&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TestClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;elasticsearch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_elasticsearch_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# This hits real Elasticsearch indices created by Terraform
&lt;/span&gt;    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/blogs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&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;title&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;First post&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;content&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;Hello world&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;author&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;tester&lt;/span&gt;&lt;span class="sh"&gt;"&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;

&lt;span class="c1"&gt;# Verify the audit log was created
&lt;/span&gt;    &lt;span class="n"&gt;elasticsearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blog_posts_log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;log_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;elasticsearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blog_posts_log&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;count&lt;/span&gt;&lt;span class="sh"&gt;"&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;log_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These aren't unit tests, they're &lt;strong&gt;integration tests that validate your entire stack&lt;/strong&gt;. They catch problems that mocks can't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrong field mappings (Elasticsearch rejects documents)&lt;/li&gt;
&lt;li&gt;Missing indices (immediate failure, not silent bugs)&lt;/li&gt;
&lt;li&gt;Permission issues (API key lacks required privileges)&lt;/li&gt;
&lt;li&gt;Data type mismatches (string where integer expected)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When your tests pass, you know your application actually works with your infrastructure, not just with your assumptions about it.&lt;/p&gt;

&lt;p&gt;The confidence boost is enormous. Instead of wondering "will this work in production?", you know it will because it's already working against the same infrastructure patterns that production uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Local to Production: The Path Forward
&lt;/h2&gt;

&lt;p&gt;The beauty of this approach becomes clear when you need to deploy to production. You're not rewriting infrastructure setup - you're just pointing the same Terraform code at different targets.&lt;/p&gt;

&lt;p&gt;For local development, our &lt;code&gt;local.tfvars&lt;/code&gt; file might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;elasticsearch_url&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://host.docker.internal:9200"&lt;/span&gt;
&lt;span class="nx"&gt;elasticsearch_indices&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;blog_posts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"blog_posts"&lt;/span&gt;
  &lt;span class="nx"&gt;blog_logs&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"blog_posts_log"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production, you'd have a &lt;code&gt;prod.tfvars&lt;/code&gt; that points to your actual Elasticsearch cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;elasticsearch_url&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://your-elasticsearch.cloud.es.io:443"&lt;/span&gt;
&lt;span class="nx"&gt;elasticsearch_indices&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;blog_posts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod_blog_posts"&lt;/span&gt;
  &lt;span class="nx"&gt;blog_logs&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod_blog_posts_log"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same Terraform code, same index definitions, same API key privileges. The only difference is where it runs and what it connects to.&lt;/p&gt;

&lt;p&gt;You can also run this pattern for &lt;strong&gt;ephemeral preview environments&lt;/strong&gt;. Each pull request gets its own namespace in Kubernetes, with its own Elasticsearch indices created by the same Terraform container. Perfect isolation, perfect consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters Beyond Just "Working"
&lt;/h2&gt;

&lt;p&gt;This isn't just about avoiding Friday afternoon debugging sessions (though that's nice). This pattern gives you something more valuable: &lt;strong&gt;confidence in your entire development workflow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you can run &lt;code&gt;docker compose up&lt;/code&gt; and get a perfect replica of your production infrastructure, you catch problems early. When your CI tests run against real services, you catch integration issues before they reach users. When your deployment process is identical across environments, you eliminate a huge class of production surprises.&lt;/p&gt;

&lt;p&gt;Most importantly, when a new team member joins, they don't need to run seven manual setup commands from a README that might be outdated. They run &lt;code&gt;docker compose up&lt;/code&gt;, wait for the terraform container to exit, and they have a working development environment that matches everyone else's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started: Your Next Steps
&lt;/h2&gt;

&lt;p&gt;Ready to try this pattern? Start simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identify your manual setup steps&lt;/strong&gt; - What curl commands, database migrations, or configuration tweaks does your local environment need?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convert one step to Terraform&lt;/strong&gt; - Pick the simplest infrastructure dependency (maybe an index or a queue) and define it in Terraform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add it to your Docker Compose&lt;/strong&gt; - Create a terraform service that runs your configuration and exits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the flow&lt;/strong&gt; - Run &lt;code&gt;docker compose up&lt;/code&gt; from a clean checkout. Does everything work without manual intervention?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expand gradually&lt;/strong&gt; - Add more infrastructure components to Terraform as you build confidence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You don't need to solve everything at once. Even converting one manual setup step eliminates a whole class of "works on my machine" problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;This pattern is about more than just Terraform and Elasticsearch. It's about treating infrastructure as an integral part of your application, not as an afterthought. It's about making your development environment as reproducible as your CI pipeline. It's about catching problems early, when they're cheap to fix.&lt;/p&gt;

&lt;p&gt;In a world where microservices depend on dozens of backing services, and where a single misconfigured index can bring down a feature, this kind of deterministic infrastructure setup isn't just nice to have - it's essential.&lt;/p&gt;

&lt;p&gt;The next time you hear "it worked on my machine," you'll know exactly how to fix it. Not with documentation or Slack messages, but with code that runs the same way everywhere.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Want to see this in action? Check out the &lt;a href="https://github.com/u11d-com/blog_test-stack-in-gh-actions" rel="noopener noreferrer"&gt;complete example repository&lt;/a&gt; with a working FastAPI + Elasticsearch + Terraform setup you can run locally.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>docker</category>
      <category>terraform</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>Creating Docker images that can run on different platforms, including Raspberry Pi</title>
      <dc:creator>Daniel Kraszewski</dc:creator>
      <pubDate>Mon, 01 Sep 2025 07:00:00 +0000</pubDate>
      <link>https://dev.to/u11d/creating-docker-images-that-can-run-on-different-platforms-including-raspberry-pi-2bci</link>
      <guid>https://dev.to/u11d/creating-docker-images-that-can-run-on-different-platforms-including-raspberry-pi-2bci</guid>
      <description>&lt;h2&gt;
  
  
  Building a multi-architecture Docker images
&lt;/h2&gt;

&lt;p&gt;As each generation of the Raspberry Pi single-board computer emerges, its processing power continues to advance. The current fourth generation boasts a processor with four Cortex-A72 cores and up to 8GB of RAM. These impressive capabilities,  enclosed within a printed circuit board measuring 85✕55, make the Raspberry Pi an excellent choice for use as a home server, offering a wide range of possibilities from media serving to automation of the software development process (CI/CD) or home automation. Given our commitment to containerization through Docker, it makes sense to consider deploying Docker images on the Raspberry Pi.&lt;/p&gt;

&lt;p&gt;It's worth noting that the Raspberry Pi's processor architecture and instruction set differ from those of typical desktop computers. As a result, the Docker image must be built separately, targeting specific architecture. Fortunately, the increasing popularity of other processor families has led to many Docker Hub images already being prepared for use on the Raspberry Pi. For example, let's examine the Debian &lt;code&gt;stable&lt;/code&gt; version:&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%2Fdblu8tafc7lg6pnerpkd.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%2Fdblu8tafc7lg6pnerpkd.png" alt="debian.png" width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are most interested in the &lt;code&gt;linux/arm/v7&lt;/code&gt; and &lt;code&gt;linux/arm64&lt;/code&gt; platforms, because these instruction sets have &lt;a href="https://en.wikipedia.org/wiki/Raspberry_Pi#Specifications" rel="noopener noreferrer"&gt;the last 3 generations&lt;/a&gt; of RPi. These platforms are targeting various operating systems. In this case the &lt;code&gt;linux/arm/v7&lt;/code&gt; is aimed for 32-bit and &lt;code&gt;linux/arm64/v8&lt;/code&gt; for 64-bit operating systems.&lt;/p&gt;

&lt;p&gt;The question arises as to what to do when a specific Raspberry Pi version requires an image that has not been provided. While it is possible to build the image directly on a single-board computer (&lt;a href="https://en.wikipedia.org/wiki/Single-board_computer" rel="noopener noreferrer"&gt;SBC&lt;/a&gt;), this process can be complicated. In this case, we recommend using the Docker extension Buildx to build the image on a local machine. This approach offers greater flexibility and can help to streamline the development process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Buildx
&lt;/h2&gt;

&lt;p&gt;Buildx is a plugin for Docker's CLI that significantly expands Docker's image building and management capabilities. Introduced in version 19.03, Buildx is currently enabled by default. For more detailed information on how to use Buildx, please consult the official &lt;a href="https://docs.docker.com/build/buildx/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ok, Docker build capabilities are enhanced, so what’s next?&lt;/p&gt;

&lt;p&gt;With Buildx, it is possible to prepare images for one or multiple platforms depending on the configuration. The building process can be performed using various drivers, each with different feature sets. These include the Docker server, an additional container capable of emulating other platforms, and the use of a Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;While Buildx offers a great deal of flexibility, its implementation has certain limitations. For instance, using the Docker server driver only allows for the creation of an image for a &lt;a href="https://docs.docker.com/build/buildx/drivers/" rel="noopener noreferrer"&gt;single platform&lt;/a&gt;. In such cases, the goal is often to build an image that can be executed on different platforms using only the local machine.&lt;/p&gt;

&lt;p&gt;Once the appropriate driver has been selected, the next step is to identify the available instances of builders (build environments) that Buildx can locate on a given machine. This can be accomplished by issuing the &lt;code&gt;docker buildx ls&lt;/code&gt; command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  ~ docker buildx &lt;span class="nb"&gt;ls
&lt;/span&gt;NAME/NODE       DRIVER/ENDPOINT STATUS  PLATFORMS
default &lt;span class="k"&gt;*&lt;/span&gt;       docker
  default       default         running linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;default&lt;/code&gt; builder is included by default in Buildx, and uses the local Docker server as its backend. While it supports cross-platform image building, it is important to note that it does not enable the creation of multi-platform images, as stated in the documentation. To build multi-platform images, a different builder configuration must be used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;error: multiple platforms feature is currently not supported &lt;span class="k"&gt;for &lt;/span&gt;docker driver. Please switch to a different driver &lt;span class="o"&gt;(&lt;/span&gt;eg. &lt;span class="s2"&gt;"docker buildx create --use"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s start with creating a new builder called &lt;code&gt;multi&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  ~ docker buildx create &lt;span class="nt"&gt;--driver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker-container &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;multi &lt;span class="nt"&gt;--use&lt;/span&gt;
multi
➜  ~ docker buildx &lt;span class="nb"&gt;ls
&lt;/span&gt;NAME/NODE       DRIVER/ENDPOINT             STATUS   PLATFORMS
multi &lt;span class="k"&gt;*&lt;/span&gt;         docker-container
  multi0        unix:///var/run/docker.sock inactive
default         docker
  default       default                     running  linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: in conjunction with the &lt;code&gt;docker buildx create&lt;/code&gt; command, we utilized the following options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;--driver - selects the driver that will be used,&lt;/li&gt;
&lt;li&gt;--name - gives a name to our builder instance,&lt;/li&gt;
&lt;li&gt;--use - instructs to use the newly created instance for subsequent operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Upon analyzing the output, it is evident that the builder is currently inactive. However, this will change once the first build is initiated.&lt;/p&gt;

&lt;p&gt;In order to test our configuration, we can create a simple &lt;code&gt;Dockerfile&lt;/code&gt; that displays information about the platform upon which it is executed at build time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;FROM debian:stable-slim
RUN &lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it's build time! Buildx command syntax is modeled on a regular &lt;code&gt;docker build&lt;/code&gt; command with additional parameters.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; &lt;code&gt;--progress&lt;/code&gt; - changes the way logs are displayed,&lt;/li&gt;
&lt;li&gt; &lt;code&gt;--platform&lt;/code&gt; - allows providing a comma separated list of platforms for which the image will be created.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this exercise, we will choose a PC as well as the two most popular platforms for Raspberry Pi.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  ~ docker buildx build &lt;span class="nt"&gt;-t&lt;/span&gt; dockerpro/multi-arch:latest &lt;span class="nt"&gt;--progress&lt;/span&gt; plain &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64,linux/arm/v7,linux/arm64 &lt;span class="nb"&gt;.&lt;/span&gt;

WARNING: No output specified &lt;span class="k"&gt;for &lt;/span&gt;docker-container driver. Build result will only remain &lt;span class="k"&gt;in &lt;/span&gt;the build cache. To push result image into registry use &lt;span class="nt"&gt;--push&lt;/span&gt; or to load image into docker use &lt;span class="nt"&gt;--load&lt;/span&gt;
&lt;span class="c"&gt;#1 [internal] booting buildkit&lt;/span&gt;
&lt;span class="c"&gt;#1 pulling image moby/buildkit:buildx-stable-1&lt;/span&gt;
&lt;span class="c"&gt;#1 pulling image moby/buildkit:buildx-stable-1 5.9s done&lt;/span&gt;
&lt;span class="c"&gt;#1 creating container buildx_buildkit_multi0&lt;/span&gt;
&lt;span class="c"&gt;#1 creating container buildx_buildkit_multi0 0.6s done&lt;/span&gt;
&lt;span class="c"&gt;#1 DONE 6.5s&lt;/span&gt;

&lt;span class="c"&gt;#2 [internal] load .dockerignore&lt;/span&gt;
&lt;span class="c"&gt;#2 transferring context: 2B done&lt;/span&gt;
&lt;span class="c"&gt;#2 DONE 0.0s&lt;/span&gt;

&lt;span class="c"&gt;#3 [internal] load build definition from Dockerfile&lt;/span&gt;
&lt;span class="c"&gt;#3 transferring dockerfile: 74B done&lt;/span&gt;
&lt;span class="c"&gt;#3 DONE 0.0s&lt;/span&gt;

&lt;span class="k"&gt;***&lt;/span&gt; image downloading &lt;span class="nb"&gt;cut &lt;/span&gt;out &lt;span class="k"&gt;for &lt;/span&gt;readability &lt;span class="k"&gt;***&lt;/span&gt;

&lt;span class="c"&gt;#9 [linux/amd64 2/2] RUN uname -m&lt;/span&gt;
&lt;span class="c"&gt;#0 0.069 x86_64&lt;/span&gt;
&lt;span class="c"&gt;#9 DONE 0.2s&lt;/span&gt;

&lt;span class="c"&gt;#11 [linux/arm64 2/2] RUN uname -m&lt;/span&gt;
&lt;span class="c"&gt;#0 0.050 aarch64&lt;/span&gt;
&lt;span class="c"&gt;#11 DONE 0.1s&lt;/span&gt;

&lt;span class="c"&gt;#12 [linux/arm/v7 2/2] RUN uname -m&lt;/span&gt;
&lt;span class="c"&gt;#0 0.079 armv7l&lt;/span&gt;
&lt;span class="c"&gt;#12 DONE 0.2s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As observed in the output, a significant amount of logs are being generated. This can be attributed to BuildKit, an advanced Docker mechanism that enables parallel execution of tasks and offers improved cache management, among other capabilities. It is worth delving further into BuildKit, even if building Raspberry Pi images is not a priority. Additional information can be found &lt;a href="https://docs.docker.com/develop/develop-images/build_enhancements/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Going back to the log analysis.&lt;/p&gt;

&lt;p&gt;Returning to the logs we can observe that in the first step the BuildKit image container is started, then the &lt;code&gt;.dockerignore&lt;/code&gt; file is loaded and the build context is transferred. The next step is to load the &lt;code&gt;Dockerfile&lt;/code&gt; and execute its instructions. For our image we selected 3 platforms, so the instructions are executed for each platform separately. At first, the Debian image is downloaded then the &lt;code&gt;uname -m&lt;/code&gt; program is executed to print system information. Specifically on which "machine" it is currently running. You can see this in more details in steps &lt;code&gt;#9&lt;/code&gt;, &lt;code&gt;#11&lt;/code&gt; and &lt;code&gt;#12&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As mentioned earlier, regular computers and Raspberry Pi processors are not compatible. However, Buildx and BuildKit use &lt;a href="https://www.qemu.org/" rel="noopener noreferrer"&gt;QEMU&lt;/a&gt; to emulate other hardware, allowing the &lt;code&gt;uname -m&lt;/code&gt; program to report other platforms during the build process. It should be noted that emulation using QEMU is slower than direct code execution, but it provides the ability to build images in one place. Alternatively, we can use other Raspberry Pi boards and add them to Buildx as external builders, which could be a more efficient solution depending on the requirements. Of course, this approach would require additional hardware.&lt;/p&gt;

&lt;p&gt;The image building process was completed successfully. However, it is worth noting that a warning was displayed at the beginning of the build logs.&lt;/p&gt;

&lt;p&gt;This one exactly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;WARNING: No output specified &lt;span class="k"&gt;for &lt;/span&gt;docker-container driver. Build result will only remain &lt;span class="k"&gt;in &lt;/span&gt;the build cache. To push result image into registry use &lt;span class="nt"&gt;--push&lt;/span&gt; or to load image into docker use &lt;span class="nt"&gt;--load&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we use a regular &lt;code&gt;docker build&lt;/code&gt; command, the image defaults to a local collection and we can run a container from it or send it to an external registry.&lt;/p&gt;

&lt;p&gt;In the Buildx context, a driver using an additional container to build images requires configuring what should happen with the image after it is built. The suggested &lt;code&gt;--load&lt;/code&gt; option (which is a shorthand for &lt;code&gt;--output=type=docker&lt;/code&gt;) will unfortunately not work in our case. &lt;br&gt;
Using &lt;code&gt;--load&lt;/code&gt; option will cause a following error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;error: docker exporter does not currently support exporting manifest lists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is because the "docker exporter" used by the --load option cannot save the image manifest, which contains metadata for multiple platforms, at once. Therefore, we need to use a different output option to save the image in a way that supports multi-platform images.&lt;/p&gt;

&lt;p&gt;[CTA]&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom image registry
&lt;/h2&gt;

&lt;p&gt;Even though the image is built on the local machine, it ultimately needs to be transferred to the Raspberry Pi. In the Buildx context, the only available option to save the image requires a registry where images can be transferred and stored. Thus, a registry is a necessary solution to our problem.&lt;/p&gt;

&lt;p&gt;To transfer the built multi-platform image to the Raspberry Pi, we need to save it in a registry. The Docker registry can run on any machine with sufficient disk space. The &lt;a href="https://docs.docker.com/registry/deploying/" rel="noopener noreferrer"&gt;official implementation&lt;/a&gt;, written in Golang, requires minimal CPU and memory resources. Setting up the registry is straightforward and can be accomplished by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  ~ docker volume create registry-data
registry-data
➜  ~ docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 5000:5000 &lt;span class="nt"&gt;-v&lt;/span&gt; registry-data:/var/lib/registry &lt;span class="nt"&gt;--restart&lt;/span&gt; always &lt;span class="nt"&gt;--name&lt;/span&gt; registry registry:2docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 5000:5000 &lt;span class="nt"&gt;-v&lt;/span&gt; registry-data:/var/lib/registry &lt;span class="nt"&gt;--restart&lt;/span&gt; always &lt;span class="nt"&gt;--name&lt;/span&gt; registry registry:2
216f1553784bd45d2fbaf43906ac5f55182c1475ad1d0e94d2e41175293a48c7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will start the registry on port &lt;code&gt;5000&lt;/code&gt;. Additionally, the registry will automatically start after a system reboot and will save data in a volume named &lt;code&gt;registry-data&lt;/code&gt;. Note that if you plan to use the registry for anything beyond testing purposes, it is recommended to use &lt;code&gt;docker-compose&lt;/code&gt; to keep the entire configuration as code.&lt;/p&gt;

&lt;p&gt;To fully utilize the benefits of Buildx, we need to recreate the builder with a configuration file named &lt;code&gt;builder.toml&lt;/code&gt;, which will serve as a blueprint for our builder. It is important to note that the IP address used in the file (&lt;code&gt;192.168.1.2&lt;/code&gt;) must be replaced with the IP address of your computer. To find your IP address, you can use a command such as &lt;code&gt;hostname -I&lt;/code&gt;:&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="o"&gt;[&lt;/span&gt;registry.&lt;span class="s2"&gt;"192.168.1.2:5000"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
  http &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true
  &lt;/span&gt;insecure &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file is required to connect to the registry without encryption. While we have omitted encryption for the purpose of this blog entry, it's important to note that encryption is a must(!) in a production environment to ensure the security of the data. When deploying a registry, it's recommended to use SSL certificates to encrypt the communication between the registry and the clients. This can be achieved by either obtaining a trusted SSL certificate or generating a self-signed one. It's important to note that using a self-signed certificate may result in warnings from the clients about the untrusted certificate.&lt;/p&gt;

&lt;p&gt;Now, we can use the &lt;code&gt;builder.toml&lt;/code&gt; configuration file to create a builder in Buildx. To use the configuration file, we need to add the &lt;code&gt;--config&lt;/code&gt; option to the &lt;code&gt;docker buildx create&lt;/code&gt; command. After that, we can build and upload the image to the registry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  ~ docker buildx &lt;span class="nb"&gt;rm &lt;/span&gt;multi
➜  ~ docker buildx create &lt;span class="nt"&gt;--driver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker-container &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;multi &lt;span class="nt"&gt;--use&lt;/span&gt; &lt;span class="nt"&gt;--config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;builder.toml
multi
➜  ~ docker buildx build &lt;span class="nt"&gt;-t&lt;/span&gt; 192.168.1.2:5000/multi-arch:latest &lt;span class="nt"&gt;--progress&lt;/span&gt; plain &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64,linux/arm/v7,linux/arm64 &lt;span class="nt"&gt;--push&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="k"&gt;***&lt;/span&gt; &lt;span class="nb"&gt;cut &lt;/span&gt;off &lt;span class="k"&gt;***&lt;/span&gt;

&lt;span class="c"&gt;#13 exporting to image&lt;/span&gt;
&lt;span class="c"&gt;#13 exporting layers 0.1s done&lt;/span&gt;
&lt;span class="c"&gt;#13 exporting manifest sha256:c6623eda280c8ee76b2bd63f9a73f429204ad4a260d60edd44de92a9401d276e&lt;/span&gt;
&lt;span class="c"&gt;#13 exporting manifest sha256:c6623eda280c8ee76b2bd63f9a73f429204ad4a260d60edd44de92a9401d276e done&lt;/span&gt;
&lt;span class="c"&gt;#13 exporting config sha256:279c30a6d250e68984e909a414149aa21a62138523b0f59c41d0beaa5e65f27d done&lt;/span&gt;
&lt;span class="c"&gt;#13 exporting manifest sha256:ba2339518538ebd6e178db4b3f604fbe82d0167f25b453bd75dbd6bde5203732 done&lt;/span&gt;
&lt;span class="c"&gt;#13 exporting config sha256:a25a708b066296c7c952fa48bd59dda84ec3a0a7206e086302fd402f0a6d4d4c done&lt;/span&gt;
&lt;span class="c"&gt;#13 exporting manifest sha256:7625f06a22638df4e5749709851816a83222ca8e8ab35c1979eb4d206d22c72b done&lt;/span&gt;
&lt;span class="c"&gt;#13 exporting config sha256:1b27246fc54aaa67e4d09b8161e1068dc8527982c4c0173a31f025bde9ebbff2 done&lt;/span&gt;
&lt;span class="c"&gt;#13 exporting manifest list sha256:95a7f6cd1a6e7d5b2f4a5dd0d936d56f767166dedc13669cbd6c627b25a43cf4 done&lt;/span&gt;
&lt;span class="c"&gt;#13 pushing layers&lt;/span&gt;
&lt;span class="c"&gt;#13 pushing layers 2.5s done&lt;/span&gt;
&lt;span class="c"&gt;#13 pushing manifest for 192.168.1.2:5000/multi-arch:latest@sha256:95a7f6cd1a6e7d5b2f4a5dd0d936d56f767166dedc13669cbd6c627b25a43cf4 0.0s done&lt;/span&gt;
&lt;span class="c"&gt;#13 DONE 2.6s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The private Docker Hub has received and stored its first image. I would like to write that the next steps are super easy and it is only the download to Raspberry Pi operation that is left. Unfortunately, it is still not the case. As previously in the case of Buildx, we must first enable Docker to use an unencrypted connection to the registry. &lt;br&gt;
To enable Docker to use an unencrypted connection to the registry, we need to create a file called &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; with the following content, replacing the IP address used here (192.168.1.2) with the IP address of your computer:&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="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"insecure-registries"&lt;/span&gt; : &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"192.168.1.2:5000"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's restart Docker and see if we can successfully run the container with the image previously prepared:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pi@raspberry:~ &lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart docker
pi@raspberry:~ &lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; 192.168.1.2:5000/multi-arch:latest &lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt;
Unable to find image &lt;span class="s1"&gt;'192.168.1.2:5000/multi-arch:latest'&lt;/span&gt; locally
latest: Pulling from multi-arch
f83522bf96d7: Pull &lt;span class="nb"&gt;complete
&lt;/span&gt;329a37630ca9: Pull &lt;span class="nb"&gt;complete
&lt;/span&gt;Digest: sha256:f994065ab94a50f09de56873529bcdb4660f50fa3c1b1fbb9797da96725c5766
Status: Downloaded newer image &lt;span class="k"&gt;for &lt;/span&gt;192.168.1.2:5000/multi-arch:latest
armv7l
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations, you have successfully completed the process of building and deploying a Docker image to a private registry for your Raspberry Pi. While the process may have required some effort, the end result is a highly efficient and customizable system that you can easily manage from your home network.&lt;/p&gt;

&lt;p&gt;It is highly recommended to configure encryption and authentication to ensure the security of your registry. Additionally, you can enhance your registry with a user-friendly web interface, such as the one provided by &lt;a href="https://github.com/Joxit/docker-registry-ui" rel="noopener noreferrer"&gt;docker-registry-ui&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;The blog post describes the process of building and deploying a Docker image on a Raspberry Pi using a private Docker registry. It covers the use of Buildx, a Docker CLI plugin, for building multi-arch images, and creating a private Docker registry to store and transfer the images to the Raspberry Pi.&lt;/p&gt;

&lt;p&gt;I hope that this post has been informative and useful in simplifying your multi-arch builds. Now you should know the process of building and deploying a Docker image on a Raspberry Pi using a private Docker registry. It covers the use of Buildx, a Docker CLI plugin, for building multi-arch images, and creating a private Docker registry to store and transfer the images to the Raspberry Pi.&lt;/p&gt;

&lt;p&gt;If you require additional assistance beyond what has been provided, please feel free to &lt;a href="https://uninterrupted.tech/contact/" rel="noopener noreferrer"&gt;reach out to us&lt;/a&gt; for professional support. We are always available and eager to assist you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Useful links
&lt;/h3&gt;

&lt;p&gt;Buildx:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/buildx/working-with-buildx/" rel="noopener noreferrer"&gt;https://docs.docker.com/buildx/working-with-buildx/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/docker/buildx" rel="noopener noreferrer"&gt;https://github.com/docker/buildx&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BuildKit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/develop/develop-images/build_enhancements/" rel="noopener noreferrer"&gt;https://docs.docker.com/develop/develop-images/build_enhancements/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/moby/buildkit" rel="noopener noreferrer"&gt;https://github.com/moby/buildkit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Registry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/registry/deploying/" rel="noopener noreferrer"&gt;https://docs.docker.com/registry/deploying/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/registry/insecure/" rel="noopener noreferrer"&gt;https://docs.docker.com/registry/insecure/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Joxit/docker-registry-ui" rel="noopener noreferrer"&gt;https://github.com/Joxit/docker-registry-ui&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Raspberry Pi:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Raspberry_Pi#Specifications" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/Raspberry_Pi#Specifications&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>raspberrypi</category>
    </item>
    <item>
      <title>Short-Circuit Evaluation in Terraform: A Deep Dive</title>
      <dc:creator>Daniel Kraszewski</dc:creator>
      <pubDate>Wed, 20 Aug 2025 15:55:06 +0000</pubDate>
      <link>https://dev.to/u11d/short-circuit-evaluation-in-terraform-a-deep-dive-5bin</link>
      <guid>https://dev.to/u11d/short-circuit-evaluation-in-terraform-a-deep-dive-5bin</guid>
      <description>&lt;p&gt;Short-circuit evaluation is a fundamental concept in programming that can significantly impact how your Terraform configurations behave. Unlike traditional programming languages, Terraform's HashiCorp Configuration Language (HCL) has some unique characteristics when it comes to short-circuit evaluation that every DevOps engineer should understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What is Short-Circuit Evaluation?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Short-circuit evaluation is an optimization technique where the second operand of a logical operation is only evaluated if the first operand doesn't determine the result. In most programming languages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For &lt;code&gt;AND&lt;/code&gt; operations: if the first condition is &lt;code&gt;false&lt;/code&gt;, the second isn't evaluated&lt;/li&gt;
&lt;li&gt;For &lt;code&gt;OR&lt;/code&gt; operations: if the first condition is &lt;code&gt;true&lt;/code&gt;, the second isn't evaluated&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How Traditional Languages Handle Short-Circuit Evaluation&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In languages like JavaScript, Python, or Go, short-circuit evaluation prevents unnecessary computation and potential errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// JavaScript example&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// user.password is only checked if user exists&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User has password set&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python example
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# user.password only evaluated if user is not None
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User has password set&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;h2&gt;
  
  
  &lt;strong&gt;The Problem: Terraform's Short-Circuit Evaluation Doesn't Always Work&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here's a real-world example that demonstrates the issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"user_config"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;email&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# ❌ This WILL FAIL with "Attempt to get attribute from null value"&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;needs_random_password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_config&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&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;Why does this fail?&lt;/strong&gt; Even though we check &lt;code&gt;var.user_config != null&lt;/code&gt; first, Terraform still evaluates the entire expression during the parsing phase and discovers that &lt;code&gt;var.user_config.password&lt;/code&gt; is trying to access an attribute on a null value.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Terraform's Unique Approach&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Terraform's HCL has a more complex relationship with short-circuit evaluation due to its declarative nature and the way it processes configurations during different phases (plan, apply, etc.).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The Reality: Limited Short-Circuit Evaluation&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Unlike traditional programming languages, Terraform's short-circuit evaluation is limited because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parsing Phase&lt;/strong&gt;: Terraform parses the entire expression before evaluation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type Checking&lt;/strong&gt;: All attribute accesses are validated regardless of conditions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static Analysis&lt;/strong&gt;: Terraform needs to understand all possible code paths&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Solutions: Safe Ways to Handle Null Values&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Solution 1: Explicit Boolean Variables&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The most straightforward and readable approach is to use explicit boolean variables with validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"create_user"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Whether to create the user"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bool&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"user_config"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"User configuration (required when create_user is true)"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;email&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

  &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create_user&lt;/span&gt; &lt;span class="err"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_config&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"user_config must be provided when create_user is true."&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"random_password"&lt;/span&gt; &lt;span class="s2"&gt;"user_password"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create_user&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="nx"&gt;length&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;
  &lt;span class="nx"&gt;special&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="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_user"&lt;/span&gt; &lt;span class="s2"&gt;"example"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create_user&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&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;Email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_user_login_profile"&lt;/span&gt; &lt;span class="s2"&gt;"example"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create_user&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="nx"&gt;user&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&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="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;random_password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_password&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="nx"&gt;result&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;Why this works:&lt;/strong&gt; By separating the boolean flag from the configuration object, we avoid null attribute access entirely. The validation block ensures data integrity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Variable validation is a powerful tool that should be leveraged whenever possible. It provides early feedback to users, prevents configuration errors, and makes your modules more robust and user-friendly. The validation block runs during &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt;, catching issues before resources are created.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Solution 2: Use &lt;code&gt;try()&lt;/code&gt; Function&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;For cases where explicit boolean variables aren't preferred:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;needs_random_password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;try&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"random_password"&lt;/span&gt; &lt;span class="s2"&gt;"user_password"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;needs_random_password&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="nx"&gt;length&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;
  &lt;span class="nx"&gt;special&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt; The &lt;code&gt;try()&lt;/code&gt; function catches any errors during expression evaluation and returns the fallback value (&lt;code&gt;false&lt;/code&gt;) if the expression fails. This prevents the "Attempt to get attribute from null value" error by gracefully handling the case where &lt;code&gt;var.user_config&lt;/code&gt; is null.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Solution 3: Use &lt;code&gt;can()&lt;/code&gt; Function&lt;/strong&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;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;needs_random_password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"random_password"&lt;/span&gt; &lt;span class="s2"&gt;"user_password"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;needs_random_password&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="nx"&gt;length&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;
  &lt;span class="nx"&gt;special&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt; The &lt;code&gt;can()&lt;/code&gt; function tests whether an expression can be evaluated without errors. It returns &lt;code&gt;true&lt;/code&gt; if the expression is valid and &lt;code&gt;false&lt;/code&gt; if it would cause an error. This allows us to safely check if &lt;code&gt;var.user_config.password&lt;/code&gt; is accessible before evaluating it, effectively implementing our own short-circuit logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Solution 4: Nested Conditional Expression&lt;/strong&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;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;needs_random_password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_config&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"random_password"&lt;/span&gt; &lt;span class="s2"&gt;"user_password"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;needs_random_password&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="nx"&gt;length&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;
  &lt;span class="nx"&gt;special&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt; The ternary conditional operator (&lt;code&gt;condition ? true_value : false_value&lt;/code&gt;) provides proper short-circuit behavior in Terraform. The second part of the expression (&lt;code&gt;var.user_config.password == null&lt;/code&gt;) is only evaluated if the first condition (&lt;code&gt;var.user_config != null&lt;/code&gt;) is true, preventing null attribute access errors.&lt;/p&gt;

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

&lt;p&gt;Short-circuit evaluation in Terraform is &lt;strong&gt;not&lt;/strong&gt; the same as in traditional programming languages. The most effective approaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use explicit boolean variables&lt;/strong&gt; with validation for feature flags and optional resources - this is the most robust approach that leverages Terraform's validation capabilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;try()&lt;/code&gt; function&lt;/strong&gt; for optional nested attributes and fallback values&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;can()&lt;/code&gt; function&lt;/strong&gt; to test expression validity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use variable validation&lt;/strong&gt; wherever possible to catch errors early and provide better user experience&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combine approaches&lt;/strong&gt; based on your team's preferences and the complexity of your configuration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The explicit boolean variable approach with validation tends to be more readable, maintainable, and provides the best user experience, while function-based approaches can be more concise for simple cases. Choose what works best for your team and use case, but always consider adding validation to improve reliability.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Additional Resources&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.terraform.io/docs/language/functions/index.html" rel="noopener noreferrer"&gt;Terraform Functions Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.terraform.io/docs/language/expressions/conditionals.html" rel="noopener noreferrer"&gt;HCL Conditional Expressions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.terraform.io/docs/language/values/variables.html#custom-validation-rules" rel="noopener noreferrer"&gt;Variable Validation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>terraform</category>
      <category>devops</category>
      <category>infrastructureascode</category>
    </item>
  </channel>
</rss>
