<?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: mrduguo</title>
    <description>The latest articles on DEV Community by mrduguo (@mrduguo).</description>
    <link>https://dev.to/mrduguo</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F312970%2F1b27413f-cae3-4188-9706-3ffc8eeedae8.png</url>
      <title>DEV Community: mrduguo</title>
      <link>https://dev.to/mrduguo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrduguo"/>
    <language>en</language>
    <item>
      <title>Slide Builder: RevealJS in YAML, with Prezi-style zoom</title>
      <dc:creator>mrduguo</dc:creator>
      <pubDate>Sun, 05 Jul 2026 15:51:56 +0000</pubDate>
      <link>https://dev.to/mrduguo/slide-builder-revealjs-in-yaml-with-prezi-style-zoom-560p</link>
      <guid>https://dev.to/mrduguo/slide-builder-revealjs-in-yaml-with-prezi-style-zoom-560p</guid>
      <description>&lt;p&gt;Slides have an odd status in engineering: we make them constantly, but we treat them as disposable. They rarely live in a repo. They rarely go through review. The diagram you spent twenty minutes aligning in Keynote is gone the moment the talk ends.&lt;/p&gt;

&lt;p&gt;Dinghy's &lt;strong&gt;Slide Builder&lt;/strong&gt; treats a slide like any other artifact: a folder of source files that compiles to a single deliverable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;A presentation builder layered on top of &lt;a href="https://revealjs.com/" rel="noopener noreferrer"&gt;RevealJS&lt;/a&gt;, wired into the same Dinghy CLI you already have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;YAML DSL:&lt;/strong&gt; recognized keys map to semantic blocks, and any other key becomes the matching HTML element, so you describe slides in a structured, indented form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Markdown and HTML, auto-loaded:&lt;/strong&gt; drop a &lt;code&gt;.md&lt;/code&gt; or &lt;code&gt;.html&lt;/code&gt; file in the slide folder and it becomes a section.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-contained HTML output:&lt;/strong&gt; assets inlined, a single file you can share or host anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live reload dev server:&lt;/strong&gt; run &lt;code&gt;dinghy slide start&lt;/code&gt; and edit-to-reload feedback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prezi-style zoom and pan:&lt;/strong&gt; exclusive to Dinghy, covered below.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A YAML slide
&lt;/h2&gt;

&lt;p&gt;The core idea is that a slide is a tree of HTML elements, and YAML is a tidy way to describe nested structure:&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;sections&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;badge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Show Case&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Slide Builder&lt;/span&gt;
    &lt;span class="na"&gt;subtitle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;author RevealJS presentations in YAML&lt;/span&gt;
    &lt;span class="na"&gt;ul&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;li&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;YAML DSL maps keys to HTML elements&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Self-contained HTML output&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Live reload development server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some keys are recognized aliases: &lt;code&gt;badge&lt;/code&gt; becomes &lt;code&gt;&amp;lt;div class="badge"&amp;gt;&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt; becomes &lt;code&gt;&amp;lt;h2 class="title"&amp;gt;&lt;/code&gt;, &lt;code&gt;subtitle&lt;/code&gt; becomes &lt;code&gt;&amp;lt;p class="subtitle"&amp;gt;&lt;/code&gt;. Any other key, such as &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;h3&lt;/code&gt;, &lt;code&gt;ul&lt;/code&gt;, or &lt;code&gt;li&lt;/code&gt;, becomes that HTML element directly. Multi-file slides are just multiple YAML files in the same folder, picked up in filename order.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Markdown slide
&lt;/h2&gt;

&lt;p&gt;If you want to add a section quickly, plain Markdown works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Slide Builder&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; YAML DSL maps keys to HTML elements
&lt;span class="p"&gt;-&lt;/span&gt; Self-contained HTML output
&lt;span class="p"&gt;-&lt;/span&gt; Live reload dev server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Markdown files become slides automatically. You can mix YAML, Markdown, and raw HTML in the same slide and use whichever fits each section.&lt;/p&gt;

&lt;h2&gt;
  
  
  The single-file output
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;dinghy slide build&lt;/code&gt; and you get &lt;strong&gt;one HTML file&lt;/strong&gt;, with every image, font, and dependency inlined. There is nothing to host. You can drop it into a chat, open it offline, or email it. RevealJS rendering is intact, navigation works, and the speaker view works.&lt;/p&gt;

&lt;p&gt;This matters for talks at venues with unreliable WiFi: your slide is a self-contained file that depends on nothing but a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Prezi-style feature
&lt;/h2&gt;

&lt;p&gt;The one part that is exclusive to Dinghy is &lt;strong&gt;zoom-and-pan slides&lt;/strong&gt;. You give an image and a set of regions by pixel coordinate, and the slide zooms into each region in turn instead of cutting to a new one:&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;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dinghy-proxy-project&lt;/span&gt;
  &lt;span class="na"&gt;img&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;imgs/dinghy-proxy-project.png&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1562&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1398&lt;/span&gt;
  &lt;span class="na"&gt;sections&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dinghy-proxy-title&lt;/span&gt;
      &lt;span class="na"&gt;x1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;71&lt;/span&gt;
      &lt;span class="na"&gt;y1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;842&lt;/span&gt;
      &lt;span class="na"&gt;x2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;954&lt;/span&gt;
      &lt;span class="na"&gt;y2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1132&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dinghy-proxy-ruby&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;❤️&lt;/span&gt;
      &lt;span class="na"&gt;x1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1052&lt;/span&gt;
      &lt;span class="na"&gt;y1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;119&lt;/span&gt;
      &lt;span class="na"&gt;x2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1550&lt;/span&gt;
      &lt;span class="na"&gt;y2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;293&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The slide flies into each rectangle in turn. This is useful for architecture diagrams you want to walk through region by region without redrawing them five times. Under the hood it builds on RevealJS's &lt;code&gt;data-auto-animate&lt;/code&gt; to morph between views, but the coordinate-based syntax is unique to Dinghy: in plain RevealJS you would have to calculate the CSS transforms by hand.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu1ep10o5u5vdi2nmrzc0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu1ep10o5u5vdi2nmrzc0.gif" alt="Prezi-style zoom and pan walking through a sequence diagram" width="599" height="399"&gt;&lt;/a&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2k98178ybc2v60hbaehe.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2k98178ybc2v60hbaehe.gif" alt="Prezi-style zoom and pan walking through a sequence diagram" width="599" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;The same reasons authoring in source pays off everywhere else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The slide lives in the repo, gets reviewed, and gets diffed.&lt;/li&gt;
&lt;li&gt;Change a shared section once, and every slide that includes it updates.&lt;/li&gt;
&lt;li&gt;The output is one file, not a Keynote bundle, not a Google Slides URL, not a link that only works on the company VPN.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you give more than a couple of talks a year, the cost of authoring in YAML is repaid by the sixth or seventh slide.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it in action
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://dinghy.dev/slides/introduction-to-dinghy" rel="noopener noreferrer"&gt;Introduction to Dinghy&lt;/a&gt; slide is built with the Slide Builder. It is a live example of the YAML DSL, the auto-loaded multi-file layout, and the Prezi-style zoom-and-pan described above. The &lt;a href="https://github.com/dinghydev/dinghy/tree/main/sites/www/slides/introduction-to-dinghy" rel="noopener noreferrer"&gt;source is on GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>slide</category>
      <category>prezi</category>
      <category>revealjs</category>
      <category>presentation</category>
    </item>
    <item>
      <title>Site Builder: from one Markdown file to a deployed site in three commands</title>
      <dc:creator>mrduguo</dc:creator>
      <pubDate>Sun, 05 Jul 2026 14:42:47 +0000</pubDate>
      <link>https://dev.to/mrduguo/site-builder-from-one-markdown-file-to-a-deployed-site-in-three-commands-3682</link>
      <guid>https://dev.to/mrduguo/site-builder-from-one-markdown-file-to-a-deployed-site-in-three-commands-3682</guid>
      <description>&lt;p&gt;If you have ever tried to spin up a Docusaurus site, you know the routine: scaffold the project, install dependencies, write a config, pick a theme, get the navbar right, and only then can you start writing the actual docs.&lt;/p&gt;

&lt;p&gt;Dinghy collapses all of that setup into a single command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; src/pages
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"# Welcome"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; src/pages/INDEX.mdx
dinghy site start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is all the setup there is. One folder, one file, one command, and you have live preview running at &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt; with hot reload.&lt;/p&gt;

&lt;p&gt;There is no &lt;code&gt;package.json&lt;/code&gt;, no &lt;code&gt;node_modules&lt;/code&gt;, and no Docusaurus config to write. The Dinghy engine ships a fully configured Docusaurus inside its Docker image and points it at your &lt;code&gt;src/&lt;/code&gt; folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;p&gt;The defaults are designed to work without any further setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;src/pages/&lt;/code&gt;&lt;/strong&gt;: standalone MDX pages (the homepage lives at &lt;code&gt;INDEX.mdx&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;src/docs/&lt;/code&gt;&lt;/strong&gt;: docs section with sidebar navigation generated from the directory tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;src/blog/&lt;/code&gt; or &lt;code&gt;blog/&lt;/code&gt;&lt;/strong&gt;: blog section with date-prefixed posts. Both the root-level &lt;code&gt;blog/&lt;/code&gt; and &lt;code&gt;src/blog/&lt;/code&gt; are picked up; this site uses the root-level &lt;code&gt;blog/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;src/css/custom.css&lt;/code&gt;&lt;/strong&gt;: custom theme overrides, loaded automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;static/&lt;/code&gt;&lt;/strong&gt;: assets, served as-is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a folder is present, it is used. If it is not, Dinghy leaves it alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customizing
&lt;/h2&gt;

&lt;p&gt;When the defaults are not enough, drop a &lt;code&gt;docusaurus.config.yml&lt;/code&gt; in the site root. Dinghy intercepts a set of top-level keys such as &lt;code&gt;docs&lt;/code&gt;, &lt;code&gt;blog&lt;/code&gt;, &lt;code&gt;theme&lt;/code&gt;, &lt;code&gt;themeConfig&lt;/code&gt;, &lt;code&gt;navbar&lt;/code&gt;, &lt;code&gt;footer&lt;/code&gt;, and &lt;code&gt;sidebars&lt;/code&gt;, and merges everything else straight into the underlying Docusaurus config:&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;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;My Project&lt;/span&gt;
&lt;span class="na"&gt;navbar&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;My Project&lt;/span&gt;
  &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Guides&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/guides/getting-started&lt;/span&gt;
      &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;left&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;logo: false&lt;/code&gt; to drop the default logo. Set &lt;code&gt;footer: false&lt;/code&gt; to drop the footer entirely. Anything Docusaurus supports, you can reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying to S3
&lt;/h2&gt;

&lt;p&gt;S3 is a built-in deploy target. Add it to your config:&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;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;s3Url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3://my-bucket/path&lt;/span&gt;
  &lt;span class="na"&gt;s3Region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dinghy site deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deploy command is not a thin wrapper. It does the things you would otherwise do by hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Removes &lt;code&gt;.html&lt;/code&gt; extensions&lt;/strong&gt; so URLs look clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gzip-compresses&lt;/strong&gt; text files (HTML, JS, CSS, SVG, XML, TXT, JSON) before upload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sets cache-control headers&lt;/strong&gt; that match the file type: &lt;code&gt;public,max-age=2592000&lt;/code&gt; (about 30 days) for content-hashed assets, and &lt;code&gt;public,max-age=3600,must-revalidate&lt;/code&gt; (one hour) for mutable files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have ever shipped a static site and found out months later that your HTML was being served with a one-year cache header, this is the kind of detail the deploy step handles for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Dinghy's builders work the same way every time: sensible defaults that cover the common case, overrides for the rest, and a single command (&lt;code&gt;start&lt;/code&gt;, &lt;code&gt;build&lt;/code&gt;, &lt;code&gt;deploy&lt;/code&gt;) for each step of the lifecycle.&lt;/p&gt;

</description>
      <category>docusaurus</category>
      <category>dac</category>
      <category>mdx</category>
    </item>
    <item>
      <title>Infrastructure as Code with OpenTofu/Terraform</title>
      <dc:creator>mrduguo</dc:creator>
      <pubDate>Tue, 23 Jun 2026 20:40:42 +0000</pubDate>
      <link>https://dev.to/mrduguo/infrastructure-as-code-with-opentofuterraform-1i58</link>
      <guid>https://dev.to/mrduguo/infrastructure-as-code-with-opentofuterraform-1i58</guid>
      <description>&lt;p&gt;The &lt;a href="https://dev.to/mrduguo/where-react-for-iac-came-from-10e0"&gt;origin story&lt;/a&gt; explained &lt;em&gt;why&lt;/em&gt; Dinghy renders TSX to Terraform. This post is the &lt;em&gt;how&lt;/em&gt;, at two levels. First we create an EC2 instance by hand from the lowest-level pieces Dinghy gives you, one component per Terraform resource. Then we bundle those pieces into composite components and watch eight lines of TSX turn into a complete server stack: VPC, subnet, security group, IAM, and more.&lt;/p&gt;

&lt;h1&gt;
  
  
  Part 1: Basic service components
&lt;/h1&gt;

&lt;h2&gt;
  
  
  One component per Terraform resource
&lt;/h2&gt;

&lt;p&gt;The foundation of Dinghy's AWS support is a set of &lt;strong&gt;basic service components&lt;/strong&gt;. There is one component for every Terraform resource, generated directly from the provider schema:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Terraform resource&lt;/th&gt;
&lt;th&gt;Dinghy component&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws_instance&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AwsInstance&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws_vpc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AwsVpc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws_subnet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AwsSubnet&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aws_security_group&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AwsSecurityGroup&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mapping is mechanical: the snake_case resource name becomes a &lt;code&gt;Aws&lt;/code&gt;-prefixed PascalCase component, every argument becomes a prop, and the whole thing is type checked against the provider schema. There are around 245 AWS services covered this way, so if Terraform can describe it, there is a component for it.&lt;/p&gt;

&lt;p&gt;These are deliberately thin. A basic component does not add opinions or defaults. It is a 1:1 typed wrapper around a single resource. That makes them the building blocks everything else is assembled from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a server by hand
&lt;/h2&gt;

&lt;p&gt;Here is a complete program that creates an EC2 instance, using nothing but a basic component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Shape&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@dinghy/base-components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AwsProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@dinghy/tf-aws&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LocalBackend&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@dinghy/tf-common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AwsInstance&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@dinghy/tf-aws/serviceEc2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt; &lt;span class="na"&gt;_title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'Server With Basic Building Blocks'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AwsProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Server&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LocalBackend&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AwsProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AwsInstance&lt;/span&gt;
    &lt;span class="na"&gt;ami&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'ami-005e54dee72cc1d00'&lt;/span&gt;
    &lt;span class="na"&gt;instance_type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'t3.nano'&lt;/span&gt;
    &lt;span class="na"&gt;_title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'my-demo-server'&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three pieces do the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AwsProvider&lt;/code&gt;&lt;/strong&gt; configures the AWS provider (region and so on).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LocalBackend&lt;/code&gt;&lt;/strong&gt; stores Terraform state on disk. In production you would swap this for an S3 backend, but local is perfect for a first run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AwsInstance&lt;/code&gt;&lt;/strong&gt; is the resource we actually want.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the whole point of a basic component. &lt;code&gt;AwsInstance&lt;/code&gt; is a typed, 1:1 stand-in for the &lt;code&gt;aws_instance&lt;/code&gt; resource: its props (&lt;code&gt;ami&lt;/code&gt;, &lt;code&gt;instance_type&lt;/code&gt;, and the rest) map straight onto the resource's arguments, with nothing added.&lt;/p&gt;

&lt;h2&gt;
  
  
  What renders out
&lt;/h2&gt;

&lt;p&gt;Running this through Dinghy produces plain Terraform JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"aws_instance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"awsinstance_mydemoserver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ami"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ami-005e54dee72cc1d00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"instance_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"t3.nano"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-demo-server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"iac:id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"awsinstance_mydemoserver"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only thing in there you did not type is the tag block. &lt;code&gt;iac:id&lt;/code&gt; is how Dinghy keeps a stable identity for each resource across renders, and &lt;code&gt;Name&lt;/code&gt; comes from the &lt;code&gt;_title&lt;/code&gt;. Everything else is a plain translation of the props you wrote. This is regular Terraform: you can read it, diff it, and &lt;code&gt;apply&lt;/code&gt; it with the standard tooling.&lt;/p&gt;

&lt;p&gt;Basic components give you complete control and a 1:1 map to Terraform, at the cost of writing every resource yourself. For a single instance that is fine. But a real server also needs a VPC, a subnet, an internet gateway, a security group, and an IAM role. Writing all of those by hand is a lot of repetitive work. That is where the second layer comes in.&lt;/p&gt;

&lt;h1&gt;
  
  
  Part 2: Composite components
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Infrastructure in one tag
&lt;/h2&gt;

&lt;p&gt;A single instance is small. Composite components pay off when the thing you want is made of a dozen resources that all have to reference each other correctly. A working EC2 server needs a VPC, a subnet, an internet gateway, a route, a security group, IAM, an AMI lookup, and the instance itself. Here is all of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AwsStack&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@dinghy/tf-aws&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Ec2Servers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@dinghy/tf-aws/ec2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AwsStack&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Ec2Servers&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AwsStack&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two composites do all the work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;AwsStack&lt;/code&gt; replaced the scaffolding.&lt;/strong&gt; In Part 1 we wrote &lt;code&gt;Shape&lt;/code&gt;, &lt;code&gt;AwsProvider&lt;/code&gt;, and &lt;code&gt;LocalBackend&lt;/code&gt; ourselves. &lt;code&gt;AwsStack&lt;/code&gt; folds the provider, the state backend, and an optional regional log bucket into one wrapper with sensible defaults. You only reach for the individual pieces when you want to override one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Ec2Servers&lt;/code&gt; built the server.&lt;/strong&gt; Eight lines of TSX render to &lt;strong&gt;248 lines&lt;/strong&gt; of Terraform JSON. It looks at what you asked for and creates whatever is missing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Networking:&lt;/strong&gt; &lt;code&gt;aws_vpc&lt;/code&gt;, &lt;code&gt;aws_subnet&lt;/code&gt;, &lt;code&gt;aws_internet_gateway&lt;/code&gt;, a route, and the security group with ingress and egress rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity:&lt;/strong&gt; an &lt;code&gt;aws_iam_role&lt;/code&gt;, a policy attachment, and an &lt;code&gt;aws_iam_instance_profile&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image:&lt;/strong&gt; a &lt;code&gt;data.aws_ami&lt;/code&gt; lookup for the latest Amazon Linux 2023 (or Ubuntu) image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The instance:&lt;/strong&gt; &lt;code&gt;aws_instance&lt;/code&gt;, plus an output with its id and public IP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also wires up &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html" rel="noopener noreferrer"&gt;AWS Session Manager&lt;/a&gt; by default, so you can connect to the instance without opening SSH or managing keys.&lt;/p&gt;

&lt;p&gt;When the defaults are not what you want, you override through configuration rather than by dropping down to raw resources:&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="c1"&gt;# dinghy.config.yml&lt;/span&gt;
&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;instance_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;t4g.nano&lt;/span&gt; &lt;span class="c1"&gt;# arm64 instead of the t3.nano default&lt;/span&gt;
    &lt;span class="na"&gt;linuxFlavor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu&lt;/span&gt;
  &lt;span class="na"&gt;web2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;instance_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;t3.small&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you two servers, on different architectures and distributions, still in the same eight lines of TSX.&lt;/p&gt;

&lt;p&gt;The full example, with the source, the generated diagram, the complete 248-line Terraform, and the commands to deploy and connect, is at &lt;a href="https://dinghy.dev/show-cases/ec2-servers" rel="noopener noreferrer"&gt;dinghy.dev/show-cases/ec2-servers&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A composite is just a component
&lt;/h2&gt;

&lt;p&gt;The 248 lines might look surprising, so it is worth seeing that &lt;code&gt;Ec2Servers&lt;/code&gt; is not a special language feature. It is an ordinary TSX component built from the basic building blocks of Part 1. The part that creates each instance, with the detail removed, looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Ec2Server&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;_server&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;awsAmi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAwsAmi&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;awsSubnet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAwsSubnet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;instanceProfile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAwsIamInstanceProfile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AwsInstance&lt;/span&gt;
      &lt;span class="na"&gt;ami&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;awsAmi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;subnet_id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;awsSubnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;iam_instance_profile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;instanceProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;_server&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the &lt;em&gt;same&lt;/em&gt; &lt;code&gt;AwsInstance&lt;/code&gt; we wrote by hand in Part 1, now fed by the context lookup the &lt;a href="https://dev.to/mrduguo/where-react-for-iac-came-from-10e0"&gt;origin story&lt;/a&gt; described. The composite renders the VPC, subnet, AMI lookup, and IAM role alongside the instance, and each &lt;code&gt;useAws...()&lt;/code&gt; call reaches up the tree to find them, so you never pass references down through props by hand. The opinions and the boilerplate live in one place instead of being copied into every server you ever create.&lt;/p&gt;

&lt;p&gt;It also creates only what you did not supply. Before rendering the instances, the composite inspects each server's props and decides which supporting resources to build on demand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createVpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;servers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subnet_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;referenceAmi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;servers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ami&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createInstanceProfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;servers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iam_instance_profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass your own &lt;code&gt;iam_instance_profile&lt;/code&gt; and the composite uses it. Leave it out and it builds an IAM role and an &lt;code&gt;aws_iam_instance_profile&lt;/code&gt; for you, with the SSM policy attached so &lt;code&gt;dinghy aws connect&lt;/code&gt; can reach the instance. The VPC and the AMI lookup work the same way: supplied means reused, missing means created on demand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two levels, one model
&lt;/h2&gt;

&lt;p&gt;That is the whole picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Basic components&lt;/strong&gt; are the 1:1 building blocks. Complete control, more typing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composite components&lt;/strong&gt; are ordinary components assembled from those blocks, with opinions and defaults built in. Less typing, fewer mistakes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither replaces the other. A composite is built from basics, so you can always reach past it: nest a raw &lt;code&gt;AwsVpcSecurityGroupIngressRule&lt;/code&gt; as a child of &lt;code&gt;&amp;lt;Ec2Servers&amp;gt;&lt;/code&gt;, or swap an internal piece through the &lt;code&gt;_components&lt;/code&gt; prop, when you need something the defaults do not cover. You start at the high level and drop down only where it matters.&lt;/p&gt;

</description>
      <category>iac</category>
      <category>terraform</category>
      <category>opentofu</category>
    </item>
    <item>
      <title>Diagram as Code with draw.io</title>
      <dc:creator>mrduguo</dc:creator>
      <pubDate>Mon, 25 May 2026 21:35:50 +0000</pubDate>
      <link>https://dev.to/mrduguo/diagram-as-code-with-drawio-50ea</link>
      <guid>https://dev.to/mrduguo/diagram-as-code-with-drawio-50ea</guid>
      <description>&lt;p&gt;Architecture diagrams have a habit of going stale the moment you redraw them. The boxes stop matching the systems, the arrows stop matching the dependencies, and after a quarter or two the diagram has more in common with the &lt;em&gt;vibe&lt;/em&gt; of the system than with the actual one.&lt;/p&gt;

&lt;p&gt;Dinghy fixes that the same way Terraform fixes ad-hoc infrastructure changes: by treating the diagram as &lt;strong&gt;code&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic shape
&lt;/h2&gt;

&lt;p&gt;Every Dinghy diagram is a tree of &lt;code&gt;Shape&lt;/code&gt; components. Dependencies are part of the model, declared with &lt;code&gt;_dependsOn&lt;/code&gt; / &lt;code&gt;_dependsBy&lt;/code&gt;. Layout direction comes from &lt;code&gt;_direction&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Shape&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@dinghy/base-components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Web App
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt; &lt;span class="na"&gt;_dependsOn&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'Load Balancer'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Client&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Cloud
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt; &lt;span class="na"&gt;_direction&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'vertical'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Public Subnet
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt; &lt;span class="na"&gt;_dependsOn&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'Application'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Load Balancer&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt; &lt;span class="na"&gt;_dependsBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'Load Balancer'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Firewall&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt; &lt;span class="na"&gt;_direction&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'vertical'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Private Subnet
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt; &lt;span class="na"&gt;_dependsOn&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'Postgres'&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Application&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Postgres&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Render it and you get a draw.io page with all the right boxes, all the right arrows, and a layout that looks like the source code looks: a Web App on the outside, a Cloud nested inside, two subnets stacked vertically, all the dependency edges drawn for you:&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%2Fmvsyqajvsrfst7ozdrow.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%2Fmvsyqajvsrfst7ozdrow.png" alt="diagram with shape" width="799" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Named shapes for readability
&lt;/h2&gt;

&lt;p&gt;The plain &lt;code&gt;Shape&lt;/code&gt; version does describe the diagram — every box is labelled, every dependency is there — but it is not much fun to read. Everything is a &lt;code&gt;Shape&lt;/code&gt; with a &lt;code&gt;name&lt;/code&gt;, and you have to read the strings to work out what is what. This is exactly the kind of thing React's named components were made for. Lift each role into its own component and the diagram code becomes easy to read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Shape&lt;/span&gt; &lt;span class="na"&gt;_dependsOn&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'Load Balancer'&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WebApp&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Cloud&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PublicSubnet&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LoadBalancer&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Firewall&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;PublicSubnet&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PrivateSubnet&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Application&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Postgres&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;PrivateSubnet&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Cloud&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;WebApp&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same diagram, but now &lt;code&gt;&amp;lt;Client /&amp;gt;&lt;/code&gt; carries its semantics in its name. The layout primitives are still there — they have just moved one layer down, where they belong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Styled shapes with icons
&lt;/h2&gt;

&lt;p&gt;The final pass is to drop in real AWS icons instead of generic boxes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;awsGeneralResources&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@dinghy/diagrams/entitiesAwsGeneralResources&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;awsGeneralResources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;
    &lt;span class="na"&gt;_dependsOn&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'Load Balancer'&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@dinghy/diagrams/entitiesAwsGeneralResources&lt;/code&gt; ships the official AWS resource icons, ready to be dropped into a tree. The TSX still reads exactly the same — only the rendered output changes from generic shapes to AWS-flavored ones.&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%2Frh5z8swz7qkcyvs2kzar.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%2Frh5z8swz7qkcyvs2kzar.png" alt="diagram with icon" width="800" height="617"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A new way to build draw.io diagrams
&lt;/h2&gt;

&lt;p&gt;Dinghy is not a new diagram software. The output of every example above is still a plain &lt;code&gt;.drawio&lt;/code&gt; file: you can open it in the draw.io editor, tweak it by hand, share it with someone who has never heard of Dinghy, and it will behave exactly like any other draw.io diagram.&lt;/p&gt;

&lt;p&gt;What Dinghy changes is the way you &lt;em&gt;build&lt;/em&gt; the diagram. Instead of dragging shapes around in an editor and nudging them by a few pixels until the layout looks roughly right — only to have it drift again the next time someone edits it — you describe the shapes and their relationships in TSX, and Dinghy lays them out pixel-perfect every time. Same input, same output, no manual cleanup. Everything draw.io already gives you — the shape library, the rendering, the editing experience, the ecosystem of tools that read &lt;code&gt;.drawio&lt;/code&gt; files — stays exactly where it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learn more
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Follow the &lt;a href="https://dinghy.dev/guides/get-started/diagram-as-code" rel="noopener noreferrer"&gt;Diagram as Code get-started guide&lt;/a&gt; to scaffold your first project and render your first &lt;code&gt;.drawio&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Browse the &lt;a href="https://dinghy.dev/examples/diagrams" rel="noopener noreferrer"&gt;example diagrams&lt;/a&gt; to see what real Dinghy diagrams look like — from a handful of shapes up to full AWS architectures.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dinghy</category>
      <category>react</category>
      <category>drawio</category>
    </item>
    <item>
      <title>Where React for IaC came from</title>
      <dc:creator>mrduguo</dc:creator>
      <pubDate>Sun, 17 May 2026 09:47:27 +0000</pubDate>
      <link>https://dev.to/mrduguo/where-react-for-iac-came-from-10e0</link>
      <guid>https://dev.to/mrduguo/where-react-for-iac-came-from-10e0</guid>
      <description>&lt;p&gt;This is the first technical post in the Dinghy series, and it is the one I had to write before any of the others made sense. It is the &lt;strong&gt;origin story&lt;/strong&gt; of the project — how a Terraform codebase that had grown beyond what copy-paste could maintain ended up being rendered out of TSX — and the &lt;strong&gt;core concept&lt;/strong&gt; the rest of Dinghy is built on.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I ended up here
&lt;/h2&gt;

&lt;p&gt;Back in the day, I was maintaining a Terraform codebase across several environments. Most of the configuration was duplicated between them — the same resources, the same modules, repeated for every environment with only the inputs changing. Every change had to land in every environment. Every difference between them was either a deliberate choice or a bug waiting to happen.&lt;/p&gt;

&lt;p&gt;Modules helped. For a while. Then the modules themselves became the maintenance burden — a layer of indirection that still couldn't express what I actually wanted to say.&lt;/p&gt;

&lt;p&gt;HCL is HashiCorp &lt;strong&gt;Configuration&lt;/strong&gt; Language. The keyword is &lt;em&gt;configuration&lt;/em&gt;. It describes resources. It does not let you reason about them.&lt;/p&gt;

&lt;p&gt;I wanted a real language.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first move: CDKTF
&lt;/h2&gt;

&lt;p&gt;CDKTF — Cloud Development Kit for Terraform — was the obvious next stop. Write infrastructure in TypeScript, render to Terraform JSON, deploy with the regular Terraform tooling. I tried it. It did what it said on the tin: real loops, real functions, real imports, real IDE support.&lt;/p&gt;

&lt;p&gt;But two things kept nagging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Construct boilerplate.&lt;/strong&gt; CDKTF inherits AWS CDK's construct pattern: &lt;code&gt;new Vpc(this, 'vpc', {...})&lt;/code&gt;, then &lt;code&gt;new Subnet(this, 'subnet', { vpc, ... })&lt;/code&gt;. Every resource needs a parent scope and a unique id, hand-passed. It is faithful to AWS CDK, but it never stopped feeling like extra steps around the thing I actually wanted to write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-resource wiring.&lt;/strong&gt; When two resources needed to talk to each other, I was still threading handles by hand. Pass the VPC into the subnet. Pass the subnet into the EC2. Pass the EC2 into the security group rule. The hierarchy in my head — &lt;em&gt;VPC contains subnets contains instances&lt;/em&gt; — was nowhere visible in the source.&lt;/p&gt;

&lt;p&gt;CDKTF had solved the programmable-language problem. It had not, for me, solved the &lt;em&gt;expressing nesting&lt;/em&gt; problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The click: TSX nests the way infrastructure nests
&lt;/h2&gt;

&lt;p&gt;When you draw a cloud architecture diagram, you naturally nest things inside containers — the VPC box wraps the subnet boxes; the subnet boxes wrap the instances. That nesting is how the infrastructure composes.&lt;/p&gt;

&lt;p&gt;The moment that turned this into a project rather than a complaint was noticing that &lt;strong&gt;TSX is already nested in the same way&lt;/strong&gt;. When you write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Vpc&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Subnet&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Ec2&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Subnet&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Vpc&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source code shape matches the runtime shape. The thing you write looks like the thing you mean. No &lt;code&gt;new X(parent, id, {...})&lt;/code&gt;. No explicit handle threading. The parent is right there — it is the JSX element containing you.&lt;/p&gt;

&lt;p&gt;The next step was obvious. React looks like a tool for building web pages, but underneath, it is a general-purpose engine for building trees. The part that turns your TSX into HTML doesn't actually need to produce HTML — it can produce anything, as long as you tell it how. Tell it to produce Terraform JSON, and that is what you get.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't worry about React's complexity
&lt;/h2&gt;

&lt;p&gt;If React's hooks, lifecycle, and state-management debates have put you off, you can relax. Dinghy uses almost none of that.&lt;/p&gt;

&lt;p&gt;Dinghy renders &lt;strong&gt;once&lt;/strong&gt;. You write the TSX, Dinghy walks the tree from top to bottom one time, writes the Terraform JSON, and stops. No reactivity. No re-renders. No state to manage. No lifecycle.&lt;/p&gt;

&lt;p&gt;So none of the modern React you might have struggled with applies here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;del&gt;&lt;code&gt;useState&lt;/code&gt;&lt;/del&gt; — no local state&lt;/li&gt;
&lt;li&gt;
&lt;del&gt;&lt;code&gt;useEffect&lt;/code&gt;&lt;/del&gt; — no side effects or lifecycle&lt;/li&gt;
&lt;li&gt;
&lt;del&gt;&lt;code&gt;useMemo&lt;/code&gt;&lt;/del&gt; — nothing to memoise&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dinghy only keeps the bits that make the nesting work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TSX&lt;/strong&gt; — the authoring syntax&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;tree-walker&lt;/strong&gt; that turns TSX into output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;useContext&lt;/code&gt;&lt;/strong&gt; — for letting a child component reach a surrounding parent (the bucket trick you will see in the next section)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of what React has accumulated for browser UI doesn't apply here, and you don't need to know it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context fixes the wiring problem
&lt;/h2&gt;

&lt;p&gt;Here is the bit that finally felt right. With CDKTF I was threading a bucket handle down through props. With Dinghy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BucketVersioning&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;s3Bucket&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAwsS3Bucket&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AwsS3BucketVersioning&lt;/span&gt;
      &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;s3Bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;versioning_configuration&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Enabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AwsS3Bucket&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BucketVersioning&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AwsS3Bucket&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BucketVersioning&lt;/code&gt; does not need to be told &lt;em&gt;which&lt;/em&gt; bucket it is enabling versioning on. It asks the surrounding &lt;code&gt;AwsS3Bucket&lt;/code&gt; via context. Drop it anywhere under a bucket and it Just Works.&lt;/p&gt;

&lt;p&gt;That is the kind of composition HCL fundamentally cannot give you. CDKTF can do it, but only by passing the parent reference by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond Web
&lt;/h2&gt;

&lt;p&gt;The other thing that fell out of choosing React: &lt;strong&gt;the output can be anything&lt;/strong&gt;. React doesn't care whether the result is HTML, native mobile widgets, terminal UI, or Terraform JSON. Point it at a different kind of output, and it produces that.&lt;/p&gt;

&lt;p&gt;React started as a web library, but it has long since moved beyond the web:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web&lt;/strong&gt; — the original&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native&lt;/strong&gt; — iOS and Android apps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI&lt;/strong&gt; — Ink, Claude Code, and similar tools render terminal interfaces with React-like trees&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure&lt;/strong&gt; — Dinghy, rendering diagrams and Terraform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same model, different output. That is why a single engine can sensibly cover diagrams and infrastructure under one roof — what the tree renders to is just a detail.&lt;/p&gt;

</description>
      <category>react</category>
      <category>terraform</category>
      <category>cdktf</category>
      <category>drawio</category>
    </item>
    <item>
      <title>Introduction to Dinghy: a Swiss-army knife for everyday engineering</title>
      <dc:creator>mrduguo</dc:creator>
      <pubDate>Tue, 12 May 2026 20:39:11 +0000</pubDate>
      <link>https://dev.to/mrduguo/introduction-to-dinghy-a-swiss-army-knife-for-everyday-engineering-20l2</link>
      <guid>https://dev.to/mrduguo/introduction-to-dinghy-a-swiss-army-knife-for-everyday-engineering-20l2</guid>
      <description>&lt;p&gt;If you ship anything for a living — diagrams, infrastructure, docs, slide decks — you have probably noticed that each of those things lives in its own world, with its own toolchain, its own opinionated editor, and its own subtle ways to drift out of sync.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dinghy.dev" rel="noopener noreferrer"&gt;&lt;strong&gt;Dinghy&lt;/strong&gt;&lt;/a&gt; is an open-source toolchain that puts all of them under one roof. One CLI. One install. One consistent way to &lt;em&gt;describe&lt;/em&gt; what you want &lt;strong&gt;as code&lt;/strong&gt; and let the tools deliver the final artwork.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;A Swiss-army knife for engineers who like writing code more than clicking through dialogs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Diagram as Code&lt;/strong&gt; — render architecture diagrams to draw.io.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure as Code&lt;/strong&gt; — render OpenTofu / Terraform from React TSX.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site Builder&lt;/strong&gt; — author docs sites with Docusaurus, live-preview them, deploy to S3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slide Builder&lt;/strong&gt; — author RevealJS presentations in YAML / Markdown / HTML, with Prezi-style zoom-and-pan as a Dinghy-exclusive bonus.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is powered by &lt;strong&gt;Deno&lt;/strong&gt; and &lt;strong&gt;Docker&lt;/strong&gt;, so you do not need to babysit Node versions, Python virtualenvs, Terraform providers, or any of the rest of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  From a DevOps engineer's bench
&lt;/h2&gt;

&lt;p&gt;Dinghy was built by a DevOps engineer, so it is shaped to walk with your code across the whole software development lifecycle — from local development through to CI/CD — instead of stopping at the edge of one stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it is shaped
&lt;/h2&gt;

&lt;p&gt;Dinghy splits cleanly into a thin &lt;strong&gt;CLI&lt;/strong&gt; and a fat &lt;strong&gt;engine&lt;/strong&gt; that runs in Docker. The CLI is what you install on your machine; the engine is a versioned image that ships every dependency Dinghy needs.&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;-fsSL&lt;/span&gt; https://get.dinghy.dev/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single command gets you the CLI. From then on, everything Dinghy does happens inside the engine image — which means &lt;strong&gt;every machine on your team gets the exact same versions&lt;/strong&gt; of Deno, Node, OpenTofu / Terraform, and every TF provider, pinned through a single &lt;code&gt;.dinghyrc&lt;/code&gt; file.&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="nv"&gt;DINGHY_ENGINE_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.1...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One lock. Predictable versions. No more "it works on my laptop" — and no more two-year-old projects you can't rebuild because the dependencies have rotted away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it's for
&lt;/h2&gt;

&lt;p&gt;Anyone who needs to write code for any of this work — a diagram, a piece of infrastructure, a docs site or web app, a slide deck. You do not need to use them all, and you do not need to use them all the time. Reach for one when that is all you need; pull in a few when the work spans more than one.&lt;/p&gt;

&lt;p&gt;If any of that sounds useful for what you are building, the rest of this series digs into each capability one post at a time &lt;em&gt;(coming soon — links will go live as each post is published)&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Where React for IaC came from&lt;/strong&gt; — the origin of the Dinghy project and core concept.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagram as Code with draw.io&lt;/strong&gt; — author architecture diagrams as TSX components.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure as Code&lt;/strong&gt; — 248 lines of Terraform from 8 lines of source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site Builder&lt;/strong&gt; — from one Markdown file to a deployed site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slide Builder&lt;/strong&gt; — RevealJS in YAML, with Prezi-style zoom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dinghy in the AI age&lt;/strong&gt; — still relevant? How it works alongside AI.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Ready to set sail?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.dinghy.dev/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guides and examples are at &lt;a href="https://dinghy.dev" rel="noopener noreferrer"&gt;https://dinghy.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>revealjs</category>
      <category>terraform</category>
      <category>opentofu</category>
    </item>
  </channel>
</rss>
