<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts | Chris K Wensel</title><link>https://chris.wensel.net/post/</link><atom:link href="https://chris.wensel.net/post/index.xml" rel="self" type="application/rss+xml"/><description>Posts</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><image><url>https://chris.wensel.net/og-image.png</url><title>Posts</title><link>https://chris.wensel.net/post/</link></image><item><title>Determinism Injection</title><link>https://chris.wensel.net/post/determinism-injection/</link><pubDate>Sat, 29 Aug 2026 00:00:00 +0000</pubDate><guid>https://chris.wensel.net/post/determinism-injection/</guid><description>&lt;p&gt;I&amp;rsquo;ve been refining a specification-driven development model for LLM software engineering for over a year now. The &lt;a href="https://chris.wensel.net/post/recommendation-decisioning-records/"&gt;RDR model&lt;/a&gt; started with a simple template, but has &lt;a href="https://chris.wensel.net/post/rdr-process/"&gt;evolved&lt;/a&gt; into a full human-driven engineering workflow. There are a number of stages in the flow, all derived from process, management, and engineering literature, as well as 30 years of experience writing software and managing software engineering teams.&lt;/p&gt;
&lt;p&gt;Every step has an artifact a human is either expected or can review and refine to ensure they are building the correct thing, while also ensuring the whole project maintains its conceptual integrity as it evolves.&lt;/p&gt;
&lt;p&gt;The challenge is that even though the flow is well defined and orchestrated by a human, the flow elements are just large LLM prompts that internally make localized decisions and look up facts across related documents.&lt;/p&gt;
&lt;p&gt;This is decidedly non-deterministic. Guo et al., in &lt;a href="https://arxiv.org/abs/2606.00376" target="_blank" rel="noopener"&gt;&lt;em&gt;The Deterministic Horizon&lt;/em&gt;&lt;/a&gt;, identify a &lt;em&gt;deterministic horizon&lt;/em&gt; as the step depth past which unaided model accuracy collapses. And name the mechanism &lt;em&gt;state-space decoherence&lt;/em&gt;, divergence that accumulates from extended reasoning rather than being resolved by it. This decoherence is aggravated by a prompt performing tasks that can be externalized.&lt;/p&gt;
&lt;p&gt;So, instead of writing an application that executes prompts in a deterministic way, I need to inject determinism into the prompts I execute in order to improve their ability to actually solve the problem they are tasked with and not lose accuracy or focus because they had to grep 100 files for a single fact.&lt;/p&gt;
&lt;p&gt;I think we should call this what it is: Determinism Injection.&lt;/p&gt;
&lt;p&gt;Determinism Injection is the inverse of code orchestrating small prompts, the approach behind frameworks like Alibaba&amp;rsquo;s &lt;a href="https://arxiv.org/abs/2508.02721" target="_blank" rel="noopener"&gt;Blueprint First, Model Second&lt;/a&gt;. The prompt is the orchestrator, and generic or purpose-built tools perform the lookups, state transitions, and writes.&lt;/p&gt;
&lt;p&gt;This is a clear play on &lt;a href="https://en.wikipedia.org/wiki/Dependency_injection" target="_blank" rel="noopener"&gt;Dependency Injection&lt;/a&gt;, where capabilities are polymorphically supplied to adapt an application&amp;rsquo;s behavior to a given context. Determinism Injection is a precursor step that isolates a deterministic behavior and externalizes it, which allows contextual binding of tools that will satisfy the expectations.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/cwensel/intrastate" target="_blank" rel="noopener"&gt;Intrastate&lt;/a&gt; is a tool for resolving state transitions. I call it a stateless state-machine. It has no backing datastore, it is handed context for a given flow it&amp;rsquo;s intended to execute, it may look up additional facts, then determines the next state. Since an LLM is driving it, if there is any ambiguity or failure, it generally has enough context to recover. But the net effect is that we lower prompt size, token spend, and wall-clock duration. And gain both (more) deterministic results and higher reasoning quality.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://chris.wensel.net/post/introducing-retrofit/"&gt;Retrofit&lt;/a&gt; is another tool for Determinism Injection. Instead of relying on a prompt to generate SQL schemas and migration scripts, the LLM stops at intent. It hands its intent (RenameColumn, DecomposeTable, etc) to retrofit, and when asked, &lt;a href="https://retrofit.sh" target="_blank" rel="noopener"&gt;retrofit&lt;/a&gt; can render a SQL dump (with or without data), or migration scripts (or their reverse for rollback).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://chris.wensel.net/project/arcaneum/"&gt;Arcaneum&lt;/a&gt; was my first such tool, started over a year and a half ago, to compensate for hallucinated assertions. It is a RAG search tool for both full-text and semantic indexes. Instead of filling my context by grepping all my documents, I use it daily for searching and retrieving literature, documentation, source-code, and LLM harness transcript histories. Every design decision I make is grounded in research. And every assertion has a valid citation.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://www.katatracker.com" target="_blank" rel="noopener"&gt;kata&lt;/a&gt; and &lt;a href="https://www.roborev.io" target="_blank" rel="noopener"&gt;roborev&lt;/a&gt; projects are also examples of this model. The first is an inline issue tracker for LLM agents, and the second is an asynchronous code reviewer. Work that is actually quite hard for an LLM to manage (remember &lt;a href="https://github.com/steveyegge/beads" target="_blank" rel="noopener"&gt;beads&lt;/a&gt;?).&lt;/p&gt;</description></item><item><title>Introducing Retrofit</title><link>https://chris.wensel.net/post/introducing-retrofit/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://chris.wensel.net/post/introducing-retrofit/</guid><description>&lt;p&gt;In my last couple roles I worked with teams spending an outsized amount of their time either avoiding making changes to their SQL database schemas or cleaning up debt in their schemas and production databases.&lt;/p&gt;
&lt;p&gt;The non-avoiders were performing simple tasks like fixing the types on primary keys (yes, they were running out of numbers), renaming columns, linking tables with foreign keys, or deduplicating columns across tables and the data within those tables (how many &lt;code&gt;email&lt;/code&gt; columns do you have, company wide?).&lt;/p&gt;
&lt;p&gt;Each change required days or weeks of planning, testing against snapshots, and finally scripted zero-downtime changes against production, only to be babysat by one or more senior engineers over a weekend.&lt;/p&gt;
&lt;p&gt;The avoiders eventually just hired consultants, like myself, to assist with the fixes (yes, they were running out of numbers).&lt;/p&gt;
&lt;p&gt;It felt like there was a better way.&lt;/p&gt;
&lt;p&gt;As a developer I&amp;rsquo;ve been refactoring code for decades. But I&amp;rsquo;ve also run away from many projects that required a SQL backend, in part for the compounding complexity and a lack of simple refactoring flow.&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;And as a developer and engineer, I usually attempt to find and build better ways to solve repeating challenges.&lt;/p&gt;
&lt;p&gt;In the early 90&amp;rsquo;s I was building interactive web sites for clients in C++ (and frequently SQL). I felt there must be a better way than C++ (or perl), so I created &lt;a href="https://web.archive.org/web/20011216231334/http://www.rtis.com/" target="_blank" rel="noopener"&gt;iPerform&lt;/a&gt;, a database-backed web scripting language for Netscape and MS IIS web servers.&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;A decade later I was consulting on a large geo-spatial project in Europe. The team was force fitting a relational database model into what amounted to a document model. My bias against working with relational databases may have colored my opinions, but this felt overly complex and wrong. It was definitely very difficult to develop against.&lt;/p&gt;
&lt;p&gt;At about the same time &lt;a href="https://hadoop.apache.org" target="_blank" rel="noopener"&gt;Apache Hadoop&lt;/a&gt; broke out of &lt;a href="https://nutch.apache.org" target="_blank" rel="noopener"&gt;Apache Nutch&lt;/a&gt; and &lt;a href="https://www.oreilly.com/library/view/hadoop-the-definitive/9781491901687/ch24.html" target="_blank" rel="noopener"&gt;I was one of the first to kick the tires&lt;/a&gt;.&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;With some of my former client&amp;rsquo;s challenges in mind (not exactly the document model mismatch) and the intent to bypass SQL entirely, I realized Hadoop needed an alternate API, so I created &lt;a href="https://chris.wensel.net/project/cascading/"&gt;Cascading&lt;/a&gt; (a suspiciously relational model as an API) to solve those problems in a better way than Map-Reduce could.&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Having started my career build SQL driven applications and having evolved through and contributed to the NoSQL movement, now I find myself back to facing SQL backed applications, or interfacing with them more often.&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;ve spent the last few months reflecting, researching, and designing what I hope is a better way to design, build, and importantly, maintain database-backed applications. Especially applications that have accumulated debt and need to be modified to unlock more value from them. And doubly so for applications being operated by second or third generation engineers who fear making structural changes.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;We&amp;rsquo;ve spent all our time helping developers write code faster, but the actual bottleneck is the database.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;The trend I&amp;rsquo;m hearing more and more often is that this bottleneck is happening at the database.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&amp;ndash; Nicole Forsgren, PhD, DORA co-creator, ComputerWeekly (2018)&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Developers need to refactor, collaborate, and release database schemas with the same level of trust as they do with source code. This requires schema changes to be safe by construction.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Schema changes (i) require costly data migration, (ii) burden the DBA with tedious and error prone maintenance for queries, and (iii) cause unacceptable system downtimes.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&amp;ndash; Carlo A. Curino, Hyun J. Moon, and Carlo Zaniolo, &lt;em&gt;Graceful Database Schema Evolution: the PRISM Workbench&lt;/em&gt; (2008)&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With the emergence of LLM coding agents, the problem becomes more acute. LLM agents are great at making code changes, but I do not want one writing scripts to modify my database.&lt;/p&gt;
&lt;p&gt;So if an LLM is given a tool that intermediates changes to the database, and provides a query interface to the underlying semantic and structural model, I believe the LLM will be much more effective at refactoring an application and it&amp;rsquo;s underlying data model, as will developers.&lt;/p&gt;
&lt;p&gt;With more trust in the schema and data changes, the smaller the database release bottleneck becomes, the more value developers can ship to users in a given amount of time.&lt;/p&gt;
&lt;p&gt;This is why I created &lt;code&gt;retrofit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Migration tools typically manage &lt;em&gt;what your schema is&lt;/em&gt;. &lt;code&gt;retrofit&lt;/code&gt; captures &lt;em&gt;how it changes&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;retrofit&lt;/code&gt; is a CLI (command line interface) that maintains a text log of logical schema change operators, not hand-written DDL. The operators are logical, thus a column rename is one operation with one logical outcome, not as raw DDL without any semantic meaning or history. There is value in quickly seeing and understanding how a table or column has changed over time and the dependencies between, for example, tables and views.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AddColumn&lt;/code&gt;, &lt;code&gt;RenameColumn&lt;/code&gt;, &lt;code&gt;CastColumn&lt;/code&gt;, &lt;code&gt;CopyTable&lt;/code&gt;, &lt;code&gt;DecomposeTable&lt;/code&gt;, and &lt;code&gt;MoveColumn&lt;/code&gt; are just a few operations out of about 70 &lt;code&gt;retrofit&lt;/code&gt; supports. Each of these are a refactoring operation, many of which are reversible (data neutral), and the details on how they are implemented (executed) is deferred until the snapshot or migration scripts are rendered, on demand from the CLI.&lt;/p&gt;
&lt;p&gt;With a log of change operations (an &lt;code&gt;oplog&lt;/code&gt;), &lt;strong&gt;you can render a DDL snapshot of your database (with or without seed data in DML) at any point in the change history&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Or render migration scripts between any two points in the change history&lt;/strong&gt;. And if the operations are reversible, then you can render a script that downgrades a database.&lt;/p&gt;
&lt;p&gt;And if you (or someone else) changed your database in place, &lt;strong&gt;you can detect the drift and import the delta into the &lt;code&gt;oplog&lt;/code&gt; to bring it up-to-date&lt;/strong&gt;. It can even detect common refactorings, like a column rename, or table split or join, and mint them as a logical operator.&lt;/p&gt;
&lt;p&gt;Sealing a set of operations allows you to mark a stable version in the &lt;code&gt;oplog&lt;/code&gt;. The seal itself allows for quick detection of any edits in the log that may have been merged in by accident that could corrupt your production database if they were applied. If the seal is broken, you are prevented from rendering a schema snapshot or migration script until recovered (from git or user intervention).&lt;/p&gt;
&lt;p&gt;Actually, since the &lt;code&gt;oplog&lt;/code&gt; is safe by construction, you cannot add an operation that makes no sense, like renaming a non-existent (dropped) column or table.&lt;/p&gt;
&lt;p&gt;You can see retrofit in action on &lt;a href="https://retrofit.sh" target="_blank" rel="noopener"&gt;retrofit.sh&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There is still a lot more work to do before I release it publicly. Plus I have a fair bit of off-the-grid vacationing between now and then, and a bit more coding work to do.&lt;/p&gt;
&lt;p&gt;On the site you can sign up to be notified when it&amp;rsquo;s released. If interested in early access, you know how to reach me.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;This being a primary contributing factor to the NoSQL movement.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;This was before there was &lt;a href="https://en.wikipedia.org/wiki/Adobe_ColdFusion" target="_blank" rel="noopener"&gt;Cold Fusion&lt;/a&gt;. Actually, when I started iPerform, browsers had no cookies and there wasn&amp;rsquo;t even a &lt;a href="https://datatracker.ietf.org/doc/html/rfc2616#section-5.3" target="_blank" rel="noopener"&gt;HOST header&lt;/a&gt; specified in the HTTP protocol.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;Hadoop was the poster child of the NoSQL movement.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;Funny thing, later I was one of the first to put SQL (on Cascading) on Hadoop, in party by sponsoring the development of &lt;a href="https://calcite.apache.org" target="_blank" rel="noopener"&gt;Apache Calcite&amp;rsquo;s&lt;/a&gt; pre-apache predecessor.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;ComputerWeekly, &lt;a href="https://www.computerweekly.com/blog/Ahead-in-the-Clouds/Unblocking-the-database-bottleneck-in-enterprise-DevOps-deployments" target="_blank" rel="noopener"&gt;Unblocking the Database Bottleneck&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;Carlo A. Curino, Hyun J. Moon, and Carlo Zaniolo, &lt;a href="https://doi.org/10.14778/1453856.1453939" target="_blank" rel="noopener"&gt;&lt;em&gt;Graceful Database Schema Evolution: the PRISM Workbench&lt;/em&gt;&lt;/a&gt;, Proceedings of the VLDB Endowment, 2008.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>LLM Toolchain - June 2026</title><link>https://chris.wensel.net/post/llm-toolchain-june/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://chris.wensel.net/post/llm-toolchain-june/</guid><description>&lt;p&gt;My toolchain is constantly evolving, but as of June 2026 I&amp;rsquo;ve landed on the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;claude harness - latest model&lt;/li&gt;
&lt;li&gt;codex harness - latest model&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ollama.com/library/glm-5.2" target="_blank" rel="noopener"&gt;glm-5.2 via olama&lt;/a&gt; via claude harness&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/cwensel/arcaneum" target="_blank" rel="noopener"&gt;Arcaneum&lt;/a&gt; - local full text and semantic search&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/cwensel/rdr" target="_blank" rel="noopener"&gt;RDR skills&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/cwensel/kata-flight" target="_blank" rel="noopener"&gt;kata-flight skills&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://katatracker.com/" target="_blank" rel="noopener"&gt;kata&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://roborev.io/" target="_blank" rel="noopener"&gt;roborev&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Coming soon:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/cwensel/intrastate" target="_blank" rel="noopener"&gt;intrastate&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Arcaneum is how I start every project, via deep research against academic literature, adjacent open source projects, reference materials, and any online documents. For academic research I have a full corpus named PapersFast (with ~5k papers) that is fast to index into and search across. Often I create focused corpora with a better embedding model for papers on a specific topic to help catch nuances across papers.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://chris.wensel.net/post/rdr-process/"&gt;RDR process&lt;/a&gt; has been evolving for a year. It&amp;rsquo;s a spec-first process that now has skills wrapping each stage of the process. It is not fully automated by design as I still do all the user-facing design work and drop into the specs to revise them.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kata-flight&lt;/code&gt; is new. Over the past month or so I&amp;rsquo;ve effectively created an ad-hoc state machine around issue tracking with kata. &lt;code&gt;kata-flight&lt;/code&gt; formalizes that in a set of skills.&lt;/p&gt;
&lt;div class="mermaid"&gt;flowchart TD
init["/kata-flight-init&lt;br/&gt;(bind repo)"] -.setup.-&gt; flight
doctor["/kata-flight-doctor&lt;br/&gt;(health check)"]
ops["/kata-flow-ops&lt;br/&gt;(dashboard / reaper)"]
kinbox["/kata-inbox&lt;br/&gt;(human inbox drain)"]
flight["/kata-flight&lt;br/&gt;batch orchestrator"]
review["/kata-scope-review&lt;br/&gt;review gate"]
kship["/kata-ship&lt;br/&gt;single-kata ship"]
resolve["/kata-resolve&lt;br/&gt;fix in worktree"]
roborev(["roborev refine / respond"])
flight --&gt;|"per wave"| review
flight --&gt;|"per kata"| kship
kship --&gt;|"resolve"| resolve
kship --&gt;|"refine"| roborev
review --&gt;|"design fork"| seed[/"kind:rdr-seed&lt;br/&gt;(kata label)"/]
review --&gt;|"held / needs human"| inbox[/"inbox:*&lt;br/&gt;(kata label)"/]
inbox --&gt; kinbox
kinbox --&gt;|"READY"| flight
kinbox --&gt;|"TO-SEED"| seed
seed --&gt; seedtri["/rdr-seed-triage"]
seedtri --&gt;|"/rdr-seed (Stage 1)"| rdrflow{{"RDR design flow&lt;br/&gt;Stages 1-7 (external engine)"}}
rdrflow --&gt; impltri["/rdr-implement-triage&lt;br/&gt;(Stage 8: build)"]
impltri --&gt; roretri["/roborev-triage"]
impltri --&gt; implland["/rdr-implement-land&lt;br/&gt;(land)"]
implland --&gt;|"flight bug children"| flight
pship["/prompt-ship"] --&gt;|"refine"| roborev
wsp["worktree-ship-pipeline"]:::lib -. read by .-&gt; kship
wsp -. read by .-&gt; pship
lland["lib-land-rdr"]:::lib -. read by .-&gt; impltri
lland -. read by .-&gt; implland
classDef skill fill:#EDE9FE,stroke:#7C3AED,color:#1E1B4B;
classDef lib fill:#F5F5F4,stroke:#A8A29E,color:#1C1917,stroke-dasharray:4 3;
classDef label fill:#FEF3C7,stroke:#D97706,color:#713F12;
classDef ext fill:#DBEAFE,stroke:#2563EB,color:#1E3A8A;
class init,doctor,ops,kinbox,flight,review,kship,resolve,seedtri,impltri,roretri,implland,pship skill;
class seed,inbox label;
class roborev,rdrflow ext;
&lt;/div&gt;
&lt;p&gt;It allows me to ship well-researched bug fixes and small features. But if a feature or bug hits a specific threshold, it funnels the issue into the RDR process for more intentional design. There is also a wrapper skill that will drive an RDR implementation (after a design is marked final) through any roborev fixes that spin off during the implementation.&lt;/p&gt;
&lt;p&gt;Some projects use codex as the roborev reviewer, others use claude+glm-2.5 as the reviewer.&lt;/p&gt;
&lt;p&gt;Note that I&amp;rsquo;m publishing these skills as a matter of transparency with no expectation of any adoption. It&amp;rsquo;s great to see what others are doing, so I&amp;rsquo;m sharing. I know everyone has their own bespoke workflows and skills, so hopefully others will publish their own flows.&lt;/p&gt;
&lt;p&gt;I also use Arcaneum to index/search harness transcripts to find ways to improve the skills and prompts. As well as &lt;a href="https://github.com/cwensel/rdr/blob/main/RESEARCH.md" target="_blank" rel="noopener"&gt;leverage current research&lt;/a&gt; on prompt engineering. This is an ongoing process.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not a &amp;ldquo;&lt;a href="https://en.wikipedia.org/wiki/Token_maxxing" target="_blank" rel="noopener"&gt;token maxer&lt;/a&gt;&amp;rdquo;, but I am tracking my usage publicly on &lt;a href="https://tkmx.odio.dev/u/cwensel" target="_blank" rel="noopener"&gt;https://tkmx.odio.dev/u/cwensel&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It would be great if there was a productivity metric that showed that I was getting more capability for the same token usage. It would mean my RDR/Flight skills/prompt changes were improving my token usage.&lt;/p&gt;
&lt;p&gt;One major issue I&amp;rsquo;ve found is that LLMs are bad at state transitions, they are always slow, but frequently make poor choices as to the next state regardless of how simple the decision criteria is.&lt;/p&gt;
&lt;p&gt;Because of that I&amp;rsquo;ve started a new project named &lt;a href="https://github.com/cwensel/intrastate" target="_blank" rel="noopener"&gt;intrastate&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As of this moment it is nothing but a collection of &lt;a href="https://github.com/cwensel/intrastate/tree/main/docs/rdr" target="_blank" rel="noopener"&gt;final RDR instances&lt;/a&gt;. Tokens permitting I&amp;rsquo;ll kick off the implementation before I drop out for summer vacation.&lt;/p&gt;</description></item><item><title>Evolving the RDR Process</title><link>https://chris.wensel.net/post/rdr-process/</link><pubDate>Fri, 29 May 2026 00:00:00 +0000</pubDate><guid>https://chris.wensel.net/post/rdr-process/</guid><description>&lt;p&gt;For the last year I have been experimenting with a specification-first LLM (agentic) development model I&amp;rsquo;ve been calling &lt;a href="https://github.com/cwensel/rdr" target="_blank" rel="noopener"&gt;Recommendation Decisioning Records&lt;/a&gt; or RDRs. You can read about the &lt;a href="https://chris.wensel.net/post/recommendation-decisioning-records/"&gt;genesis to get more context&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/cwensel/arcaneum" target="_blank" rel="noopener"&gt;Arcaneum&lt;/a&gt; was written using early &amp;ldquo;versions&amp;rdquo; of the RDR template, as well were a few internal projects.&lt;/p&gt;
&lt;p&gt;This year I&amp;rsquo;ve started a new project that is a lot more ambitious and realized I was using/creating a lot of ceremony (copy/paste) as I revised any given RDR, iterating it through to the final stage (the stage right before implementation). This became quite tedious. So a couple of things happened.&lt;/p&gt;
&lt;p&gt;First, using Arcaneum, I indexed a large amount of research on specification-driven development, &lt;a href="https://github.com/cwensel/rdr/blob/main/RESEARCH.md#critique--premortem-lens" target="_blank" rel="noopener"&gt;both old and new&lt;/a&gt;, and devised a few prompts that would better formalize how I was, by hand, hardening the specifications. I tend to write a few at a time before implementation since they usually represent architectural layers, so hardening them in isolation and against each other was important. This worked quite well and led to fewer iterations or inconsistencies found within a given RDR during the implementation phase.&lt;/p&gt;
&lt;p&gt;Of note, one primary benefit of the RDR process is moving from a NxM model to a Nx1xM model. Instead of lots of iterations against a large number of files, I iterate more against a single file, which is used to generate a large number of files or edits. And the seams between the RDRs are also validated as much as they can so they can be built in tandem.&lt;/p&gt;
&lt;p&gt;I also formalized a &amp;ldquo;launch&amp;rdquo; implementation prompt (&lt;a href="https://github.com/cwensel/rdr/blob/main/RESEARCH.md#implementation-launch-stage-9" target="_blank" rel="noopener"&gt;research based&lt;/a&gt;) to drive implementation with further hardening and TDD best practices.&lt;/p&gt;
&lt;p&gt;After quite a few more rounds, I decided to index my Claude Code sessions in Arcaneum and mine them for additional ad-hoc patterns I was using to see if I could formalize even more of the process.&lt;/p&gt;
&lt;p&gt;This landed me with additional steps to the process, which all can be found on the Github repo.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/cwensel/rdr/blob/main/README.md" target="_blank" rel="noopener"&gt;README&lt;/a&gt; is a great place to start, but make note of the &lt;a href="https://github.com/cwensel/rdr/blob/main/RESEARCH.md" target="_blank" rel="noopener"&gt;RESEARCH&lt;/a&gt; behind every element of the flow.&lt;/p&gt;
&lt;p&gt;The process is still evolving, but the flow captures more data that can be used to continue to refine the process.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also experimented with the &lt;a href="https://github.com/obra/superpowers" target="_blank" rel="noopener"&gt;Superpowers&lt;/a&gt; skills on smaller projects. I like the &lt;a href="https://github.com/obra/superpowers/blob/main/skills/using-git-worktrees/SKILL.md" target="_blank" rel="noopener"&gt;worktree&lt;/a&gt; tool a lot and have directed a few skills to only work in worktrees now that it&amp;rsquo;s more stable and streamlined. But the specification-first model provided by Superpowers isn&amp;rsquo;t as robust for complex systems with lots of interacting layers. I&amp;rsquo;ll continue to use Superpowers for smaller or more throwaway projects, but not for projects that require a lot of upfront research and nuanced behaviors.&lt;/p&gt;
&lt;p&gt;Arcaneum is part of my everyday workflow. From competitive analysis and focused research to iterating on research-driven RDR specifications. I&amp;rsquo;m using the &lt;code&gt;stella&lt;/code&gt; embedding model for pdfs and md usually, but it depends on the size of the corpus. My general research corpus (PapersFast) uses &lt;code&gt;minilm&lt;/code&gt; for embedding and has 3586 documents. But I usually re-index a subset (after searching PapersFast) into a new corpus using &lt;code&gt;stella&lt;/code&gt;. For source code I use &lt;code&gt;jina-code&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Keep an eye on the RDR repo, I do want to convert things to skills etc, but there are so many touch points that require human judgment (which is the point of it all) that I&amp;rsquo;m unsure it would be any easier to deploy and iterate with.&lt;/p&gt;</description></item><item><title>Arcaneum RDR Postmortem</title><link>https://chris.wensel.net/post/arcaneum-rdr-postmortem/</link><pubDate>Wed, 25 Feb 2026 00:00:00 +0000</pubDate><guid>https://chris.wensel.net/post/arcaneum-rdr-postmortem/</guid><description>&lt;p&gt;One of my early goals for the &lt;a href="https://chris.wensel.net/post/recommendation-decisioning-records/"&gt;RDR process&lt;/a&gt; was to enable postmortems so I can measure the drift between the original prompt and the final stable code. This way I could attempt to improve the process by identifying issues that repeat across RDRs.&lt;/p&gt;
&lt;p&gt;You can see the postmortem docs in the &lt;a href="https://github.com/cwensel/arcaneum/tree/main/docs/rdr" target="_blank" rel="noopener"&gt;Arcaneum repo&lt;/a&gt;. But I asked Claude to summarize the results.&lt;/p&gt;
&lt;p&gt;From Claude:&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2 id="key-findings"&gt;
Key Findings
&lt;a class="heading-anchor" href="#key-findings" aria-label="Anchor link to Key Findings"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;17 post-mortems. 208 drift instances. One meta-lesson.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The three biggest sources of plan-vs-reality drift were authoring-process problems, not domain-knowledge gaps — meaning they&amp;rsquo;re fixable by changing how plans are written.&lt;/p&gt;
&lt;h3 id="key-findings-1"&gt;
Key Findings
&lt;a class="heading-anchor" href="#key-findings-1" aria-label="Anchor link to Key Findings"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Code samples in plans are waste.&lt;/strong&gt; Over-specified code was the #1 drift category (35 instances, 17%). Every single RDR included full class implementations that were completely rewritten. YAML schemas and config formats were never implemented. Code for deferred features was never used. The fix: specify interfaces and behaviors (signatures, contracts, data flow) rather than implementation code.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;30-minute spikes would have prevented the biggest surprises.&lt;/strong&gt; Unvalidated assumptions and framework API errors accounted for 45 drift instances across 13 RDRs. Documentation-only research was consistently wrong in detail — method signatures, filter syntax, model coverage all differed from docs. Searching dependency source code (not just reading docs) would have caught the majority.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GPU/memory management was the largest unplanned effort in the project.&lt;/strong&gt; 8 of 17 RDRs required GPU acceleration or OOM recovery that wasn&amp;rsquo;t anticipated. One RDR described GPU integration as &amp;ldquo;low complexity: just add &lt;code&gt;device='mps'&lt;/code&gt;&amp;rdquo; — the implementation required OOM recovery, GPU poisoning detection, NaN/Inf validation, Metal timeout handling, and systematic garbage collection.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;Day 2 operations&amp;rdquo; are not optional and not future work.&lt;/strong&gt; List, delete, verify, backup, and info commands for every resource created were consistently deferred to &amp;ldquo;Future Enhancements&amp;rdquo; — then all were needed immediately. Day 2 ops represent 30-50% of the actual implementation effort for any feature that creates persistent resources.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scope was systematically underestimated by 3-5x.&lt;/strong&gt; CLI commands grew from 9 planned to 30+ built. A 50-line embedding wrapper became a 1,600-line GPU-aware system. A simple sync module became 140KB. The recurring cause: focusing on the happy path without counting error handling, Day 2 operations, and cross-cutting integrations.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Speculative performance targets were universally ignored.&lt;/strong&gt; 13 of 17 RDRs included specific numeric targets (e.g., &amp;ldquo;100 PDFs/min&amp;rdquo;, &amp;ldquo;query latency &amp;lt; 50ms&amp;rdquo;). No benchmark infrastructure was created. No target was validated. They consumed authoring effort and created false precision.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CLI namespaces designed in isolation conflict with each other.&lt;/strong&gt; 8 RDRs made independent naming decisions that required renames (&lt;code&gt;fulltext&lt;/code&gt; → &lt;code&gt;indexes&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt; → &lt;code&gt;search semantic&lt;/code&gt;, &lt;code&gt;inject&lt;/code&gt; → &lt;code&gt;store&lt;/code&gt;), breaking backwards compatibility.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Existing infrastructure was routinely duplicated.&lt;/strong&gt; Multiple RDRs designed new components when equivalent modules already existed in the codebase. A 5-minute audit of existing code would have prevented redundant designs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="the-meta-lesson"&gt;
The Meta-Lesson
&lt;a class="heading-anchor" href="#the-meta-lesson" aria-label="Anchor link to The Meta-Lesson"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Specify interfaces and behaviors, not implementations. Verify APIs against source code, not documentation. Plan for the full scope — Day 2 ops, error handling, and cross-cutting concerns — not just the happy path.&lt;/p&gt;
&lt;p&gt;— &lt;em&gt;Summarized from the Arcaneum RDR post-mortem synthesis, Claude Code conversation&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With this feedback, and postmortems from other projects, I updated my templates in the &lt;a href="https://github.com/cwensel/rdr" target="_blank" rel="noopener"&gt;RDR repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Possibly over time I can come up with some sort of index that represents the drift and see if it decreases.&lt;/p&gt;
&lt;p&gt;I also would be interested in determining if there are multiple types of RDRs that might require different templates. For example RDRs that specify subsystems that support actual application features (like build tools), where application features would be defined in a different template. Currently I&amp;rsquo;m using my one-size-fits-all template.&lt;/p&gt;</description></item><item><title>Arcaneum via RDRs</title><link>https://chris.wensel.net/post/arcaneum-via-rdrs/</link><pubDate>Wed, 31 Dec 2025 00:00:00 +0000</pubDate><guid>https://chris.wensel.net/post/arcaneum-via-rdrs/</guid><description>&lt;p&gt;&lt;a href="https://chris.wensel.net/project/arcaneum/"&gt;Arcaneum&lt;/a&gt; is a set of tools to index pdf documents, source code repositories, and markdown files. It can be installed as a &lt;a href="https://github.com/cwensel/arcaneum?tab=readme-ov-file#quick-start" target="_blank" rel="noopener"&gt;Claude Code Marketplace plugin&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It is intended to create both semantic search and full text search indexes for use by an LLM &amp;ndash; when rendering code or creating a feature specification. But currently I&amp;rsquo;ve had my hands full just getting the semantic search working reliably. This is a me problem, not a technology problem.&lt;/p&gt;
&lt;p&gt;Prior to building Arcaneum I started using what I call a specification first LLM driven development model via &lt;a href="https://chris.wensel.net/post/recommendation-decisioning-records/"&gt;RDRs&lt;/a&gt;. In my day job, I write a single RDR per feature. But for Arcaneum, I decided to write as many RDRs as possible before starting to render a single line of code. Check out &lt;a href="https://github.com/cwensel/arcaneum/tree/main/docs/rdr#readme" target="_blank" rel="noopener"&gt;the RDR index&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t write Python, but Arcaneum is in Python, knowing there were a rich set of supporting libraries. Also, I wanted to see how far I could get not reviewing code, but just by using the app and adjusting the app at an LLM level of abstraction. If I did read the code, I wouldn&amp;rsquo;t have any opinions on it, so why bother.&lt;/p&gt;
&lt;p&gt;The RDR model worked great getting my thoughts in order and during subsequent development. My primary challenge has been performance and memory management of the chunking and embedding pipeline. This is somewhat aggravated by my lack of reasonable tests. As a Java person, I complete every RDR with a suite of tests. But this wasn&amp;rsquo;t something I pushed hard on with Arcaneum.&lt;/p&gt;
&lt;p&gt;I use Arcaneum every day I write code and on the days I&amp;rsquo;m just doing research.&lt;/p&gt;
&lt;p&gt;One of my primary flows is to clone a git repo, index it, and create a document analyzing the code base over a different lenses. For example, how complete is the JWT support in a given framework. Or compare two frameworks footprint&amp;rsquo;s in relation to size of transitive dependencies and number of documented CVEs over time. I even &amp;lsquo;diff&amp;rsquo; two versions of a standards document (this is not a replacement of your compliance team&amp;hellip;.).&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m about to pick up my Clusterless project again and refresh parts of it. In preparation, I indexed the full &lt;a href="https://github.com/aws/aws-cdk" target="_blank" rel="noopener"&gt;aws-cdk&lt;/a&gt; repo last week, it took almost 30 hours using the &lt;a href="https://jina.ai/models/jina-embeddings-v2-base-code/" target="_blank" rel="noopener"&gt;jina-code&lt;/a&gt; embedding model &amp;ndash; most repos take a new minutes to an hour depending on the embedding model used. Every line of &lt;a href="https://chris.wensel.net/project/clusterless/"&gt;Clusterless&lt;/a&gt; and &lt;a href="https://chris.wensel.net/project/tessellate/"&gt;Tessellate&lt;/a&gt; to-date are human crafted. I&amp;rsquo;m cautiously looking at using an LLM moving forward (using the RDR in strict mode).&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s important to note that semantic search isn&amp;rsquo;t complete without a complimentary full text search interface. So far I&amp;rsquo;ve been mirroring my pdfs documents (mostly standards documents) with markdown copies in a parallel directory (not true of my full academic paper corpus of +3k files), so my LLM can grep for lines to use as citations etc.&lt;/p&gt;
&lt;p&gt;Also, &lt;a href="https://github.com/metaspartan/mactop" target="_blank" rel="noopener"&gt;mactop&lt;/a&gt; is great for watching GPU utilization.&lt;/p&gt;</description></item><item><title>Recommendation Decisioning Records</title><link>https://chris.wensel.net/post/recommendation-decisioning-records/</link><pubDate>Tue, 30 Dec 2025 00:00:00 +0000</pubDate><guid>https://chris.wensel.net/post/recommendation-decisioning-records/</guid><description>&lt;p&gt;Like many developers, I&amp;rsquo;ve been experimenting with LLM code development. Over the past 6 months or so I&amp;rsquo;ve landed on a model that I call &lt;a href="https://github.com/cwensel/rdr" target="_blank" rel="noopener"&gt;Recommendation Decisioning Records&lt;/a&gt; or RDRs.&lt;/p&gt;
&lt;p&gt;When prompting code directly via an LLM like Claude Code, I found myself in a corner more than once. This was especially true for complex features. Being stubborn, I felt I could prompt myself out of it without having to discard the code I had spent the last few hours generating. I learned a lot over the past few hours and all of it was in my head or locked in the codebase. The idea of throwing the code away felt wasteful.&lt;/p&gt;
&lt;p&gt;I almost never prompted myself out of the corners.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://adr.github.io" target="_blank" rel="noopener"&gt;Architectural Decision Records&lt;/a&gt; are a great way to document a decision. They are created, discussed, revised, and then agreed on. When I write an ADR I&amp;rsquo;m also writing the code to better understand the problem. My ADRs are documentation, but also the journey. They capture why I landed on the decision.&lt;/p&gt;
&lt;p&gt;An RDR is a prompt. It is the journey to a decision. It is a specification. It has the background as to why I want things the way I want them. It captures the hard won discoveries about the problem I couldn&amp;rsquo;t originally articulate or technical details that I had to search through research and source code to find.&lt;/p&gt;
&lt;p&gt;An RDR is also not code and is subsequently more concise.&lt;/p&gt;
&lt;p&gt;This model is context engineering. The problem I want to design and solve is bigger than the LLM context I can work with to find a reasonable implementation. My context and subsequent purpose start to drift as I start coding. Because the moment I start coding, I discover new problems or options, and every side-quest throws me off track.&lt;/p&gt;
&lt;p&gt;So the solution was to not start coding, but to start specifying and defining and researching.&lt;/p&gt;
&lt;p&gt;An RDR is the specification for a single cohesive concept.&lt;/p&gt;
&lt;p&gt;When faced with a related set of new features or designs, I create multiple RDRs, before coding, in order to maintain conceptual integrity across the design and reduce coupling between the components. You can see an example here in my &lt;a href="https://github.com/cwensel/arcaneum/tree/main/docs/rdr" target="_blank" rel="noopener"&gt;Arcaneum project RDRs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;How to get started?&lt;/p&gt;
&lt;p&gt;Copy the relevant bits from the &lt;a href="https://github.com/cwensel/rdr" target="_blank" rel="noopener"&gt;RDR repository&lt;/a&gt;. Then ask Claude to create a new RDR using the &lt;a href="https://github.com/cwensel/rdr/blob/main/README.md" target="_blank" rel="noopener"&gt;README.md&lt;/a&gt; and &lt;a href="https://github.com/cwensel/rdr/blob/main/TEMPLATE.md" target="_blank" rel="noopener"&gt;TEMPLATE.md&lt;/a&gt; for guidance for a given problem statement. For example, &amp;ldquo;I need a logging utility that will mask PII and emit structured logs&amp;rdquo;. Once the file is created, I&amp;rsquo;ll ask to research the source code ecosystem for any existing libraries or utilities. In this particular case, I might realize I&amp;rsquo;m conflating two issues. PII masking and structured logging causing me to create two RDRs instead of one where I crisply define PII masking in the first and then how to leverage that solution best in the second RDR.&lt;/p&gt;
&lt;p&gt;Some notes.&lt;/p&gt;
&lt;p&gt;Ensure you use the RDR to identify multiple options/solutions to the problem statement. I find as I get to know the problem better, an originally less attractive option may float up as the best solution.&lt;/p&gt;
&lt;p&gt;I lock down an RDR before implementation. If I got the design wrong on the RDR, I throw the code out and revise the RDR itself. I&amp;rsquo;m not terribly strict about this, but if you find yourself meandering away from the original RDR, reconsider.&lt;/p&gt;
&lt;p&gt;I never delete an RDR, but if I find it no longer relevant, I mark it unimplemented or abandoned. I&amp;rsquo;ve also reverted an RDR after implementation.&lt;/p&gt;
&lt;p&gt;Mentioned above, stack RDRs that are related and cross reference them for conceptual integrity.&lt;/p&gt;
&lt;p&gt;I use &lt;a href="https://github.com/cwensel/arcaneum" target="_blank" rel="noopener"&gt;Arcaneum&lt;/a&gt; to index source code dependencies that I may be leveraging or deciding to use in a design option. This prevents API hallucinations, which I find common. Use the RDR to capture any hidden flags or APIs that are relevant.&lt;/p&gt;
&lt;p&gt;RDRs do end up with a fair amount of code in them to capture API usage etc. This is ok, but I&amp;rsquo;ve been trying to bias toward pseudo-code vs actual code where I can to reduce the token count.&lt;/p&gt;
&lt;p&gt;The final solution, the actual implementation, will inevitably drift from the RDR. The goal of the RDR is to prevent the drift happening too early in the cycle, because once it begins it&amp;rsquo;s too hard to get back into focus. At some point I&amp;rsquo;ll begin capturing the drift in a post-mortem document to accompany the RDR so that I can better craft future RDRs via an improved template. This is one reason I recommend to lock the RDR before implementation (in a single git commit) so the gaps and methods to prevent the gaps can be reasoned about.&lt;/p&gt;</description></item><item><title>AWS API Gateway with AppRunner</title><link>https://chris.wensel.net/post/api-gateway-with-app-runner/</link><pubDate>Fri, 23 Feb 2024 00:00:00 +0000</pubDate><guid>https://chris.wensel.net/post/api-gateway-with-app-runner/</guid><description>&lt;p&gt;AWS AppRunner is a reasonably new service that aims to simplify deploying and managing web based applications and REST endpoints.&lt;/p&gt;
&lt;p&gt;New, in the sense that the AWS CDK does not offer stable L2 constructs yet for the service.&lt;/p&gt;
&lt;p&gt;New also, in the sense that some capabilities available on the service are under-documented and difficult, if not impossible, to enable via the CDK.&lt;/p&gt;
&lt;p&gt;Custom &lt;a href="https://github.com/aws/apprunner-roadmap/issues/129" target="_blank" rel="noopener"&gt;domains over AppRunner&lt;/a&gt; are one such capability.&lt;/p&gt;
&lt;p&gt;Another such capability is having the &lt;a href="https://github.com/aws/apprunner-roadmap/issues/193" target="_blank" rel="noopener"&gt;AWS API Gateway front a given private AppRunner instance&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Without a long winded overview and explanation, if you find yourself with an AppRunner instance on a private VPC that is intentionally not publicly accessible, it is possible to place an API Gateway in front of one or more AppRunner instances.&lt;/p&gt;
&lt;p&gt;First, making an AppRunner instance not publicly accessible requires dropping down to the L1 constructs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// in order to make an app runner service private, we need use L1 constructs to set the network configuration&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;CfnService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;NetworkConfigurationProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;networkConfiguration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CfnService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;NetworkConfigurationProperty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ingressConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CfnService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IngressConfigurationProperty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isPubliclyAccessible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publiclyAccessible&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// this api is not on the L2 construct&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;egressConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CfnService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;EgressConfigurationProperty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;egressType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;VPC&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vpcConnectorArn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vpcConnector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getVpcConnectorArn&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;CfnService&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cfnService&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CfnService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNode&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getDefaultChild&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requireNonNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfnService&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;setNetworkConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;networkConfiguration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When this happens, the AppRunner no longer allocates a domain name for access within the VPC. This requires an &lt;code&gt;InterfaceVpcEndpoint&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Vpc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;vpc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;InterfaceVpcEndpoint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;InterfaceVpcEndpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;InterfaceEndpoint&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subnets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subnetSelection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;securityGroups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;securityGroups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;InterfaceVpcEndpointService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;com.amazonaws.%s.apprunner.requests&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;formatted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;privateDnsEnabled&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="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;open&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="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;CfnVpcIngressConnection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ingressConnection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CfnVpcIngressConnection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;EndpointConnection&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vpcIngressConnectionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;IngressConnection&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;lowerHyphen&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ingressVpcConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CfnVpcIngressConnection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IngressVpcConfigurationProperty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vpcId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getVpcId&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vpcEndpointId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getVpcEndpointId&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;serviceArn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getServiceArn&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can get the domain name from &lt;code&gt;ingressConnection.getAttrDomainName()&lt;/code&gt;. This will be required below.&lt;/p&gt;
&lt;p&gt;Next up, we have to put a Network Load Balancer listener in front of the &lt;code&gt;InterfaceVpcEndpoint&lt;/code&gt;, to do that we are required to make a target group of the endpoint IPs. Sadly, we can only ask the endpoint for &lt;code&gt;endpoint.getVpcEndpointNetworkInterfaceIds()&lt;/code&gt;, ENI ids, and even those have to be lazily resolved.&lt;/p&gt;
&lt;p&gt;This is where, the new to me construct, &lt;code&gt;AwsCustomResource&lt;/code&gt; comes in.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Vpc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;vpc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;NetworkLoadBalancer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;balancer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NetworkLoadBalancer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;NetworkLoadBalancer&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadBalancerName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;nlb&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;internetFacing&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="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;crossZoneEnabled&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="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vpcSubnets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subnetSelection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;securityGroups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;securityGroups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ipAddressType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IpAddressType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IPV4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// look up ips for the enis&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// we cannot ask the endpoint for the number of enis via getVpcEndpointNetworkInterfaceIds() as it returns a single array token&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// not representative of the actual number of enis&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// we assume one ip per subnet given to the endpoint&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;outputPaths&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IntStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;subnetSelection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSubnets&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mapToObj&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;NetworkInterfaces.%s.PrivateIpAddress&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;formatted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// we need to use a custom resource to get the private ip addresses of the network interfaces&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;AwsCustomResource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;customResource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AwsCustomResource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;EniLookup&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;physicalName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;/// do not do this. will cause the stack to hang on deployment, and prevents an easy deletion&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// .vpc(vpc).vpcSubnets(subnetSelection)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// actually installs during the event, let&amp;#39;s rely on the installed version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;installLatestAwsSdk&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="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// by default onCreate calls onUpdate, so we only need to define onUpdate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AwsSdkCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;physicalResourceId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PhysicalResourceId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;physicalName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;EC2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;describeNetworkInterfaces&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderedSafeMaps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;NetworkInterfaceIds&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getVpcEndpointNetworkInterfaceIds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;outputPaths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputPaths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AwsCustomResourcePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromStatements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;ec2:DescribeNetworkInterfaces&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;*&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IpTarget&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ipTargets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;outputPaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customResource&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;getResponseField&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IpTarget&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;NetworkTargetGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;targetGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NetworkTargetGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;TargetGroup&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;targetGroupName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;app-runner&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;443&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// the port apprunner presents&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TCP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;targetType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TargetType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ipTargets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;NetworkListener&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;networkListener&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;balancer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Listener&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;camelCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NetworkListenerProps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TCP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// arbitrary, but don&amp;#39;t duplicate across multiple listeners on this balancer&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultTargetGroups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetGroup&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadBalancer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;balancer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You will need the listener ARN, &lt;code&gt;networkListener.getListenerArn()&lt;/code&gt;, in a later step.&lt;/p&gt;
&lt;p&gt;Now we create the API Gateway V2.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;HttpApi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;httpApi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HttpApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;HttpApi&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apiName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;api&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createDefaultStage&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="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;disableExecuteApiEndpoint&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="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// turns off the default endpoint, optional&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You may find it useful to customize the logging.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;LogGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;logGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LogGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;LogGroup&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;camelCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;logGroupName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/aws/apigateway/&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lowerHyphen&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;removalPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DESTROY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RetentionDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FIVE_DAYS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;CfnStage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;httpApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDefaultStage&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getNode&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getDefaultChild&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;setAccessLogSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CfnStage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AccessLogSettingsProperty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;destinationArn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLogGroupArn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;$context.identity.sourceIp - - [$context.requestTime] \&amp;#34;$context.httpMethod $context.routeKey $context.protocol\&amp;#34; $context.status $context.responseLength $context.requestId&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this last step, we integrate the API Gateway to the balancer listener via a VpcLink over a RESTful route. Obvious, right?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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="c1"&gt;// the domain name from the ingressConnection&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;listenerArn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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="c1"&gt;// from the listener&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// if VpcLink.Builder.create is used instead, api gateway returns 503&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;VpcLink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;httpApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addVpcLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VpcLinkProps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vpcLinkName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;vpc-link&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// where the apprunner service resides&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subnets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subnets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;securityGroups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;securityGroups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;NetworkListener&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NetworkListener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromNetworkListenerArn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;serviceName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;camelCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;listenerArn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;HttpNlbIntegration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;integration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HttpNlbIntegration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;app-runner&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parameterMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ParameterMapping&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;overwritePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MappingValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/${request.path.proxy}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// see path below&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;overwriteHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Host&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MappingValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// since this is over tls, need to set the host header&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vpcLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vpcLink&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;secureServerName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// since this is over tls, we need to set the server name&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;%s{proxy+}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;formatted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/api/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// translate /api/.... to /....&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;AddRoutesOptions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;apiRoute&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AddRoutesOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;integration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;integration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Without setting &lt;code&gt;secureServerName()&lt;/code&gt;, a &lt;code&gt;503&lt;/code&gt; error will be returned.&lt;/p&gt;
&lt;p&gt;Without setting &lt;code&gt;.overwriteHeader(&amp;quot;Host&amp;quot;, MappingValue.custom(host))&lt;/code&gt; a &lt;code&gt;404&lt;/code&gt; error will be returned. This particular issue is quite maddening since the back-end hosted service isn&amp;rsquo;t returning the &lt;code&gt;404&lt;/code&gt;, it&amp;rsquo;s returned from AppRunner (envoy) because it doesn&amp;rsquo;t recognize the &lt;code&gt;host&lt;/code&gt; header passed by the client to the API Gateway.&lt;/p&gt;
&lt;p&gt;Of note, by testing API endpoints with &lt;code&gt;curl -sD -&lt;/code&gt; you can see the returned headers. If the &lt;code&gt;server&lt;/code&gt; header has the value &lt;code&gt;envoy&lt;/code&gt;, I&amp;rsquo;m going to say you can assume AppRunner is in the request path. &lt;code&gt;envoy&lt;/code&gt; also is returned for &lt;code&gt;200&lt;/code&gt; responses. It would be wise to replace this with the public facing domain name.&lt;/p&gt;
&lt;p&gt;Network security issues are harder to sort out. For those you&amp;rsquo;ll see a &lt;code&gt;503&lt;/code&gt; response, and if you check the logs in CloudWatch, look for the &lt;code&gt;$context.error.responseType&lt;/code&gt; value of &lt;code&gt;INTEGRATION_NETWORK_FAILURE&lt;/code&gt;. You&amp;rsquo;ll need to add this variable above to the logging format &amp;ndash; see this list of other useful &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging-variables.html" target="_blank" rel="noopener"&gt;logging variables&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For simplicity I left out a number of constructs and configuration, but it&amp;rsquo;s reasonable to drop everything into the same VPC, subnets, and security group, in order to get things working.&lt;/p&gt;
&lt;p&gt;Also note I also have some &lt;a href="https://chris.wensel.net/post/naming-things/"&gt;clusterless-commons&lt;/a&gt; API calls littered about in the above code.&lt;/p&gt;
&lt;p&gt;Github issues of note:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/109" target="_blank" rel="noopener"&gt;https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/109&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/1254" target="_blank" rel="noopener"&gt;https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/1254&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>CloudWatch Log Export</title><link>https://chris.wensel.net/post/cloudwatch-log-export/</link><pubDate>Wed, 20 Dec 2023 00:00:00 +0000</pubDate><guid>https://chris.wensel.net/post/cloudwatch-log-export/</guid><description>&lt;p&gt;AWS services send their logs to CloudWatch whether they are configured to do so or not. AWS StepFunctions pushes all transition events to CloudWatch under the log group &lt;code&gt;/aws/vendedlogs/states/*&lt;/code&gt;. Even if your code is wrapped in their service, your logs will show up there as well. AWS Batch will send &lt;code&gt;stdout&lt;/code&gt; logs to log group &lt;code&gt;/aws/batch/*&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Note, if you are using the AWS CDK or CloudFormation, you will need to declare these log groups directly in order to configure the retention period and removal policy as they will be created for you automatically. See &lt;a href="https://github.com/ClusterlessHQ/clusterless-commons/blob/4c761b61c4d6ae1fc0e20dce06ab9dd14bca55de/clusterless-commons-aws/src/main/java/clusterless/commons/substrate/aws/cdk/construct/LambdaLogGroupConstruct.java" target="_blank" rel="noopener"&gt;LambdaLogGroupConstruct&lt;/a&gt; for an example.&lt;/p&gt;
&lt;p&gt;You can write your code to push your logs elsewhere, but for provider services, CloudWatch will be the interface unless you export your log group into AWS S3 via the &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_CreateExportTask.html" target="_blank" rel="noopener"&gt;CreateExportTask&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Clusterless now provides an Activity (think cron job) specifically for exporting a declared CloudWatch log to an S3 bucket, &lt;a href="https://docs.clusterless.io/reference/1.0-wip/components/aws-core-cloud-watch-export" target="_blank" rel="noopener"&gt;aws:core:cloudWatchExport&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Unfortunately, there is a &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/cloudwatch_limits_cwl.html" target="_blank" rel="noopener"&gt;quota&lt;/a&gt; on this task: &lt;code&gt;One active (running or pending) export task at a time, per account. This quota can't be changed.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This doesn&amp;rsquo;t mean the task is unusable, but it now becomes tricky. If there is already a task in play, a &lt;code&gt;LimitExceededException&lt;/code&gt; will be thrown. It is unfortunate that this quota limit isn&amp;rsquo;t promoted up into the docs.&lt;/p&gt;
&lt;p&gt;Now the question becomes, how many CloudWatch log groups can be exported via cron like processes (via a Clusterless Activity) within an account?&lt;/p&gt;
&lt;p&gt;An Activity requires as a parameter the frequency in which to create the export task, say every 5 or 10 minutes. The Activity will set the &lt;code&gt;from&lt;/code&gt; and &lt;code&gt;to&lt;/code&gt; timestamps for the previous interval of arrived logs. If the frequency is 10 minutes, then the prior 10 minute interval is exported.&lt;/p&gt;
&lt;p&gt;Assuming the task creator/submitter (an AWS Lambda in the Clusterless case) will retry failed task attempts, how long should the retry wait &amp;ndash; until trying again so that it&amp;rsquo;s more likely to succeed and not cause any API rate limiting. This is in part a function of how long a task takes to complete, and how many competing Activities there are.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;describe-export-tasks&lt;/code&gt; &lt;code&gt;aws&lt;/code&gt; cli command will export all export task metadata. This example here uses data from only one Activity (exporting one log group), where the log data is quite small.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aws logs describe-export-tasks &lt;span class="p"&gt;|&lt;/span&gt; jq &lt;span class="s1"&gt;&amp;#39;.exportTasks[].executionInfo | .completionTime - .creationTime&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; ministat -n
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x &amp;lt;stdin&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; N Min Max Median Avg Stddev
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x &lt;span class="m"&gt;2663&lt;/span&gt; &lt;span class="m"&gt;1549&lt;/span&gt; &lt;span class="m"&gt;608539&lt;/span&gt; &lt;span class="m"&gt;2326&lt;/span&gt; 7376.5332 37206.281
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As can be seen above, the median (p50) is 2.3 seconds. But a 10 minute duration was also encountered.&lt;/p&gt;
&lt;p&gt;Unfortunately, any given Lambda attempting to submit a successful create export task command only has (up to) 15 minutes in which to have a success. If another Activity was exporting a different log group, and it was submitted milliseconds before the current Activity, the current Activity may need to wait (continue retrying) for 10 minutes before it can succeed and exit. This would be an expensive blocking operation.&lt;/p&gt;
&lt;p&gt;Currently Clusterless does nothing clever to maximize the number of possible log groups that can be exported in an account. Clusterless only uses an exponential backoff retry policy starting at 3 seconds up to a maximum wait of 5 minutes. This all bounded into the Lambda configured timeout. That is, retries will continue until the Lambda function was configured to stop (timeout) so that log data can be emitted and the Lambda invocation marked as a failure.&lt;/p&gt;
&lt;p&gt;A more clever arrangement can be devised in order to reliably support multiple log group exports at without costly blocking operations. Reach out if this would be of value.&lt;/p&gt;</description></item><item><title>Naming Things in AWS</title><link>https://chris.wensel.net/post/naming-things/</link><pubDate>Wed, 13 Dec 2023 00:00:00 +0000</pubDate><guid>https://chris.wensel.net/post/naming-things/</guid><description>&lt;p&gt;Things in AWS frequently have names. Things like AWS S3 buckets or AWS Lambda Functions. And when using the AWS CDK, &lt;code&gt;Constructs&lt;/code&gt; have names so that they can be referenced elsewhere in code or error messages.&lt;/p&gt;
&lt;p&gt;I rely on four scopes to name things, what scope applies to what resource is only discovered in docs or trial-and-error.&lt;/p&gt;
&lt;p&gt;For example, S3 bucket names are global. If two users each in unique accounts in unique regions attempt to create a bucket with the same name, the first to try wins. Sometimes these names are called &amp;ldquo;physical ids&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;On the otherside of the spectrum, if you are creating a new AWS CloudFormation stack, it must have a unique name in the account and region (&lt;code&gt;us-west-1&lt;/code&gt;). The name could be used in another region concurrently.&lt;/p&gt;
&lt;p&gt;The naming scopes I work with are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;global: e.g. S3 buckets&lt;/li&gt;
&lt;li&gt;account: e.g. Elastic Container Registry (ECR)&lt;/li&gt;
&lt;li&gt;region: e.g. Virtual Private Cloud (VPC)&lt;/li&gt;
&lt;li&gt;stack: e.g. any &lt;code&gt;Construct&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I sometimes throw in an orthogonal name for the current lifecycle stage of the deployment. For example, &amp;ldquo;dev&amp;rdquo;, &amp;ldquo;uat&amp;rdquo;, or &amp;ldquo;prod&amp;rdquo;. This further prevents crossover or issues within a shared account (for those at companies that limit the number of AWS accounts granted to developers).&lt;/p&gt;
&lt;p&gt;To further confuse matters, names have a kind of de facto format. When naming a &lt;code&gt;Construct&lt;/code&gt;, CamelCase is frequently used. And when naming a bucket, lower-hyphen format is used.&lt;/p&gt;
&lt;p&gt;If you consider many names are contextual, making them hierarchical, it makes sense to have a simple API that lets you build a name, then apply it where it&amp;rsquo;s needed.&lt;/p&gt;
&lt;p&gt;The project &lt;a href="https://github.com/ClusterlessHQ/clusterless-commons" target="_blank" rel="noopener"&gt;&lt;code&gt;clusterless-commons&lt;/code&gt;&lt;/a&gt; provides a &lt;a href="https://javadoc.io/doc/io.clusterless/clusterless-commons-core/latest/clusterless/commons/naming/Label.html" target="_blank" rel="noopener"&gt;&lt;code&gt;Label&lt;/code&gt;&lt;/a&gt; interface for building labels.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;lower&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;case&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Assertions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;LowerCase&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;camelCase&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Assertions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;lowerCase&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lowerCamelCase&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Assertions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;lower-case&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lowerHyphen&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Assertions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;lower/case&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lowerHyphenPath&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Sometimes you may not want any formatting applied, the &lt;code&gt;Fixed&lt;/code&gt; class locks an element of the final label.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;DataRepo&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Fixed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;us-west-2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Assertions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;data-repo-us-west-2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lowerHyphen&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Assertions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;data-repo/us-west-2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lowerHyphenPath&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When using AWS regions in a label, it may be easier to simply use &lt;code&gt;Region.of(&amp;quot;us-west-2&amp;quot;)&lt;/code&gt;, this way a custom &lt;code&gt;Construct&lt;/code&gt; and require region information on the constructor by declaring a parameter type as &lt;code&gt;Region&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There are a few other useful &lt;code&gt;Label&lt;/code&gt; derivatives in the &lt;a href="https://javadoc.io/doc/io.clusterless/clusterless-commons-core/latest/clusterless/commons/naming/package-summary.html" target="_blank" rel="noopener"&gt;javadoc&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you are willing to adopt the base classes &lt;code&gt;ScopedApp&lt;/code&gt;, &lt;code&gt;ScopedStack&lt;/code&gt;, and &lt;code&gt;ScopedConstruct&lt;/code&gt;, naming things becomes a little simpler.&lt;/p&gt;
&lt;p&gt;The class &lt;a href="https://javadoc.io/doc/io.clusterless/clusterless-commons-aws/latest/clusterless/commons/substrate/aws/cdk/naming/ResourceNames.html" target="_blank" rel="noopener"&gt;&lt;code&gt;ResourceNames&lt;/code&gt;&lt;/a&gt; has a few helper methods that return a &lt;code&gt;Label&lt;/code&gt; with the requested scope.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ResourceNames#globalUniqueLabel()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ResoruceNames#accountUniqueLabel()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ResourceNames#regionUniqueLabel()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each of these take a &lt;code&gt;Construct&lt;/code&gt; and attempt to find the parent &lt;code&gt;ScopedApp&lt;/code&gt; for the relevant metadata.&lt;/p&gt;
&lt;p&gt;Respectively, you can end up with names like:&lt;/p&gt;
&lt;p&gt;Globally unique:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getRegion&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Fixed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getAccount&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ScopedApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;scopedOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;upperOnly&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Account unique:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getRegion&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ScopedApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;scopedOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;upperOnly&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Region unique:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Stage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ScopedApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;scopedOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;upperOnly&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note, most names may have a size limit. Lambda function names are limited to 64 characters. &lt;code&gt;Label&lt;/code&gt; currently does nothing to enforce this, but it could be added. But &lt;code&gt;Label&lt;/code&gt; does support abbreviations via &lt;code&gt;Label#of(&amp;quot;LongValue&amp;quot;, &amp;quot;abb&amp;quot;);&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://javadoc.io/doc/io.clusterless" target="_blank" rel="noopener"&gt;https://javadoc.io/doc/io.clusterless&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ClusterlessHQ/clusterless-commons" target="_blank" rel="noopener"&gt;https://github.com/ClusterlessHQ/clusterless-commons&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Speedup AWS CDK Updates</title><link>https://chris.wensel.net/post/deterministic-maps/</link><pubDate>Mon, 11 Dec 2023 00:00:00 +0000</pubDate><guid>https://chris.wensel.net/post/deterministic-maps/</guid><description>&lt;p&gt;A great feature of the AWS CDK (inherited from AWS CloudFormation) is the ability to update a stack in place. This is accomplished by only sending changes across to CloudFormation (the changes can be viewed by calling &lt;code&gt;diff&lt;/code&gt; instead of &lt;code&gt;deploy&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Unfortunately, a number of artifacts may register as having changed and be re-deployed, even though the only difference between the artifact might be a timestamp on a file. Or the order of the metadata may have changed.&lt;/p&gt;
&lt;p&gt;If you are using the Java CDK bindings, many &lt;code&gt;Construct&lt;/code&gt; builders take &lt;code&gt;java.util.Map&lt;/code&gt; instances that contain configuration information. Unfortunately the &lt;code&gt;java.util.HashMap&lt;/code&gt; does not retain the order of items added, nor does it guarantee an stability of that order across executions (or JDK versions). &lt;code&gt;java.util.LinkedHashMap&lt;/code&gt; does retain the order, so when creating &lt;code&gt;Constructs&lt;/code&gt; that expect a Map of configuration values, remember to use &lt;code&gt;LinkedHashMap&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That said, &lt;code&gt;java.util.Map#of(...)&lt;/code&gt; is a very convenient convenience function for creating inline Map instances without all the boilerplate of &lt;code&gt;Map&amp;lt;String,Object&amp;gt; config = new LinkedHashMap(); //etc&lt;/code&gt;, but &lt;code&gt;Map#of&lt;/code&gt; returns a &lt;code&gt;HashMap&lt;/code&gt; instance.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;key1,&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;value1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;key2,&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;value2&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The project &lt;a href="https://github.com/ClusterlessHQ/clusterless-commons" target="_blank" rel="noopener"&gt;&lt;code&gt;clusterless-commons&lt;/code&gt;&lt;/a&gt; provides a &lt;code&gt;OrderedMaps#of(...)&lt;/code&gt; method that duplicates the &lt;code&gt;Map#of&lt;/code&gt; method, but returns a &lt;code&gt;LinkedHashMap&lt;/code&gt; instead.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OrderedMaps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;key1,&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;value1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;key2,&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;value2&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Additionally, the sister class &lt;code&gt;OrderedSafeMaps&lt;/code&gt; provides an &lt;code&gt;#of(...)&lt;/code&gt; method that will drop a key if the associated value is &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OrderedSafeMaps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;key1,&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ignored&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;key2,&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OrderedSafeMaps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ifPresent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// mutate if not null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;key3,&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OrderedSafeMaps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ifPresent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can read more about these classes &lt;a href="https://javadoc.io/doc/io.clusterless/clusterless-commons-core/latest/clusterless/commons/collection/package-summary.html" target="_blank" rel="noopener"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ClusterlessHQ/clusterless-commons" target="_blank" rel="noopener"&gt;https://github.com/ClusterlessHQ/clusterless-commons&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://javadoc.io/doc/io.clusterless/" target="_blank" rel="noopener"&gt;https://javadoc.io/doc/io.clusterless/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cdk/" target="_blank" rel="noopener"&gt;https://docs.aws.amazon.com/cdk/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Cascading 3.0 Query Planner</title><link>https://chris.wensel.net/post/cascading-3-0-query-planner/</link><pubDate>Thu, 18 Sep 2014 19:51:10 +0000</pubDate><guid>https://chris.wensel.net/post/cascading-3-0-query-planner/</guid><description>&lt;p&gt;Original post: &lt;a href="https://www.cascading.org/2014/09/18/the-cascading-3-0-query-planner/" target="_blank" rel="noopener"&gt;https://www.cascading.org/2014/09/18/the-cascading-3-0-query-planner/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Cascading 1.0 when released, represented a huge milestone. An enterprise friendly Java API, not a syntax, and fail fast planner allowed developers to build robust, maintainable, data-oriented applications that could execute reliably on Apache Hadoop for hours or days.&lt;/p&gt;
&lt;p&gt;Cascading 2.0 made a nod towards cluster platform independence with the addition of an in memory ‘local’ mode that proved applications could be built without direct API access to the underlying platform. This provided much needed isolation across the evolving Hadoop platform and improved developer experience. Write once, run on any vendor provided platform.&lt;/p&gt;
&lt;p&gt;Cascading 3.0 proves we can abstract away the underlying platform by providing additional platforms users can execute against, not just MapReduce. &lt;a href="https://tez.apache.org" target="_blank" rel="noopener"&gt;Apache Tez&lt;/a&gt; is the first such addition. Tez is the latest parallel computation fabric from the Apache group. It builds on Hadoop, keeping the Hadoop File System for instance, but uses a model that removes inefficiencies that are baked into the MapReduce model.&lt;/p&gt;
&lt;p&gt;But we should point out the Cascading 3.0 abstraction is not just over the platform APIs, but also the semantics of the given platform like MapReduce or a DAG like model (like the one provided by Apache Tez). This is achieved by the Cascading “query planner”, it maps from one model to another.&lt;/p&gt;
&lt;p&gt;Another way to restate this is that the Cascading query planner is not ‘mapping’ MapReduce onto other models, but mapping the Cascading model directly onto the underlying platform model, be it MapReduce or a DAG representation.&lt;/p&gt;
&lt;p&gt;This makes the Cascading object model and their associated semantics an Intermediate Representation (or &lt;a href="http://en.wikipedia.org/wiki/Intermediate_language" target="_blank" rel="noopener"&gt;Intermediate Language&lt;/a&gt;) for data parallel cluster computing. Where new models can be created on top, over Cascading, and new ones can be mapped below, underneath Cascading. All the while retaining the qualities of both. LLVM provides this functionality to the compiler world. For example, Clang’s C, C++, and Objective-C; the Julia language (a technical computing language); and the Swift language from Apple all rely on the LLVM provided Intermediate Representation (IR).&lt;/p&gt;
&lt;p&gt;To keep things less ivory tower, we usually call Cascading a framework.&lt;/p&gt;
&lt;p&gt;But in practice, Cascading as an IR is quite common. Cascading is the basis for &lt;a href="https://www.cascading.org/projects/lingual/" target="_blank" rel="noopener"&gt;Lingual&lt;/a&gt;, &lt;a href="https://www.cascading.org/projects/pattern/" target="_blank" rel="noopener"&gt;Pattern&lt;/a&gt;, &lt;a href="https://twitter.github.io/scalding/" target="_blank" rel="noopener"&gt;Scalding&lt;/a&gt;, and &lt;a href="http://nathanmarz.com/blog/introducing-cascalog-a-clojure-based-query-language-for-hado.html" target="_blank" rel="noopener"&gt;Cascalog&lt;/a&gt;, each of which are higher order syntaxes or languages that are feature rich, reliable, and very useful unto themselves. Cascading is also being embedded in a number of commercial products that expose a different (more vertical) model to the end user.&lt;/p&gt;
&lt;p&gt;How is this done? In large part, it’s due to the brand new query planner we have spent the balance of this year developing and testing. First with our ‘local’ mode and the MapReduce model, which were made available earlier this year in a Cascading 3.0 WIP release. And during the last couple of months, adding more degrees of freedom to support a model with, well, more degrees.&lt;/p&gt;
&lt;p&gt;We could have just used the MapReduce planner directly on Tez, but the point of the DAG model in Tez is to remove the inefficiencies inherent to MapReduce. For example, no more ‘identity mapper’; added support for multiple outputs; no prefixing data with join ordinality; suppressing sorting when not needed; removing HDFS as an intermediate store between jobs; etc.&lt;/p&gt;
&lt;p&gt;Over the years we realized we need to rethink how the prior planners mapped Cascading into new platform models, but more importantly, how we can move control of adding new Cascading primitives, optimizations, or whole new platforms back to the Cascading user. The answer was somewhat obvious when looking at similar systems — create a rule language users can use to create rules and to create new rule sets.&lt;/p&gt;
&lt;p&gt;But there are some reasonably complicated difficulties we had to address. Recognizing that what a Cascading user creates is itself a DAG (Directed Acyclic Graph) of elements that the user would like to execute remotely is key.&lt;/p&gt;
&lt;p&gt;From there, the key difficulties we had to address boiled down to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Allowing rules to be abstract enough so that they can target patterns of nodes and edges in the user DAG without having to account for all possible permutations&lt;/li&gt;
&lt;li&gt;Isolating the structural and compatibility testing of the target DAG from the structural transformation of the DAG&lt;/li&gt;
&lt;li&gt;Allow for high levels of code reusability&lt;/li&gt;
&lt;li&gt;Allow rules to offer syntactic context on errors&lt;/li&gt;
&lt;li&gt;Allow rules to leverage all available meta-data, including the structural qualities of the DAG itself&lt;/li&gt;
&lt;li&gt;Present a simple ‘language’ that allows users to develop custom rules&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of the above translates to having “rules” that can assert a DAG is a valid structure, transform a DAG into a new DAG, or partition a DAG into sub-DAGs.&lt;/p&gt;
&lt;p&gt;In terms of MapReduce, we want to define rules that verify aggregations always follow a grouping; identify and group “map” side and “reduce” side elements; insert temporary files between the “reduce” side elements and the “map” side elements; break the graph up into physical jobs.&lt;/p&gt;
&lt;p&gt;The root of the solution is to rely on a way to match sub-DAGs (or sub-graphs) within a larger graph. That is, we need a regular expression like “language”, not for strings and text, but for graphs of nodes and edges. In the literature this is called Sub-Graph Isomorphism Matching.&lt;/p&gt;
&lt;p&gt;So we created what we call the Expression Graph API, a basis for building higher order complex logic allowing us to reason about or manipulate large complex graphs.&lt;/p&gt;
&lt;p&gt;Fundamentally what we have is an API that allows a ‘match graph’ (think regular expression string) to be constructed that then can be applied to a larger graph to see if there is a match or, more generally useful, similarity between both structures. And if so, we can then perform an Assertion, Transformation, or Partition against the matched elements.&lt;/p&gt;
&lt;p&gt;Consider the expression graph below, a simple DAG of two elements with some meta-data:&lt;/p&gt;
&lt;figure &gt;
&lt;div class="d-flex justify-content-center"&gt;
&lt;div class="w-100" &gt;&lt;img alt="" srcset="
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_1_hu_5bd167e2fe6ac334.webp 400w,
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_1_hu_9220f1e35320aebb.webp 760w,
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_1_hu_5f811c5cfff50ae6.webp 1200w"
src="https://chris.wensel.net/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_1_hu_5bd167e2fe6ac334.webp"
width="248"
height="136"
loading="lazy" data-zoomable /&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/figure&gt;
&lt;p&gt;It will match the following elements in gray and not those with a dotted outline.&lt;/p&gt;
&lt;figure &gt;
&lt;div class="d-flex justify-content-center"&gt;
&lt;div class="w-100" &gt;&lt;img alt="" srcset="
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_2_hu_b8bdd9a8f76899d7.webp 400w,
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_2_hu_1fc485a045317f8.webp 760w,
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_2_hu_d3fd3d62f89a3ebd.webp 1200w"
src="https://chris.wensel.net/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_2_hu_b8bdd9a8f76899d7.webp"
width="535"
height="240"
loading="lazy" data-zoomable /&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/figure&gt;
&lt;p&gt;The above expression graph knows it is looking for a circle that is also a ‘join’ between two incoming elements. If there is no ‘join’, then don’t match.&lt;/p&gt;
&lt;p&gt;But the world isn’t that simple. We had to go further to allow for matching of sub-graphs of distinguished elements.&lt;/p&gt;
&lt;p&gt;That is, some elements in a graph can be ignored in a given case, but not in others. Thus the elements that matter, the distinguished elements, must be the only ones that match the graph expression. But the transformation must be applied to the full sub-graph bound by any matched distinguished elements.&lt;/p&gt;
&lt;p&gt;Consider the following expression graph, a circle or a triangle will match:&lt;/p&gt;
&lt;figure &gt;
&lt;div class="d-flex justify-content-center"&gt;
&lt;div class="w-100" &gt;&lt;img alt="" srcset="
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_3_hu_19fc8b2b88a81532.webp 400w,
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_3_hu_b3ce2a2d1c8a14b4.webp 760w,
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_3_hu_809238d1b4d44462.webp 1200w"
src="https://chris.wensel.net/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_3_hu_19fc8b2b88a81532.webp"
width="198"
height="125"
loading="lazy" data-zoomable /&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/figure&gt;
&lt;p&gt;It will match the gray elements in the following graph:&lt;/p&gt;
&lt;figure &gt;
&lt;div class="d-flex justify-content-center"&gt;
&lt;div class="w-100" &gt;&lt;img alt="" srcset="
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_4_hu_13bd034877a5dcc0.webp 400w,
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_4_hu_4a49f8852b522f13.webp 760w,
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_4_hu_75aa7f54450255ff.webp 1200w"
src="https://chris.wensel.net/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_4_hu_13bd034877a5dcc0.webp"
width="482"
height="219"
loading="lazy" data-zoomable /&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/figure&gt;
&lt;p&gt;These would be the distinguished elements. Matching them allows us to remove all the other elements, or actually, mask them from subsequent matches, resulting in a contracted graph of the form:&lt;/p&gt;
&lt;figure &gt;
&lt;div class="d-flex justify-content-center"&gt;
&lt;div class="w-100" &gt;&lt;img alt="" srcset="
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_5_hu_f70771e2a3c0066d.webp 400w,
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_5_hu_1f7eb5306cb7584e.webp 760w,
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_5_hu_2c5a3ff5ad1493cf.webp 1200w"
src="https://chris.wensel.net/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_5_hu_f70771e2a3c0066d.webp"
width="195"
height="199"
loading="lazy" data-zoomable /&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/figure&gt;
&lt;p&gt;Now we can apply a new expression graph (not shown) to find our target sub-graph of distinguished elements, the result is as follows:&lt;/p&gt;
&lt;figure &gt;
&lt;div class="d-flex justify-content-center"&gt;
&lt;div class="w-100" &gt;&lt;img alt="" srcset="
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_6_hu_8f56424e9b531dbf.webp 400w,
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_6_hu_a12f9452da0d3530.webp 760w,
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_6_hu_88dd8e91eca8b1f3.webp 1200w"
src="https://chris.wensel.net/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_6_hu_8f56424e9b531dbf.webp"
width="222"
height="212"
loading="lazy" data-zoomable /&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/figure&gt;
&lt;p&gt;If we isolate the new sub-graph, above in gray, we can expand it to include it original intermediate nodes and edges, that is, unmask previously masked elements.&lt;/p&gt;
&lt;figure &gt;
&lt;div class="d-flex justify-content-center"&gt;
&lt;div class="w-100" &gt;&lt;img alt="" srcset="
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_7_hu_9936f9b9ce9670b1.webp 400w,
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_7_hu_9dd65a9ee9d0dbe6.webp 760w,
/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_7_hu_635065d078ff692c.webp 1200w"
src="https://chris.wensel.net/post/cascading-3-0-query-planner/Cascading-3.0-Query-Planner-IMG_7_hu_9936f9b9ce9670b1.webp"
width="347"
height="189"
loading="lazy" data-zoomable /&gt;&lt;/div&gt;
&lt;/div&gt;&lt;/figure&gt;
&lt;p&gt;Finally we can provide yet another expression graph to assert or transform this final graph within the original full graph. In the graph above, we could provide a new expression graph looking for a square with horizontal lines, and if found we an insert a new element after it, along the dotted edge.&lt;/p&gt;
&lt;p&gt;Alternately, we can extract the new sub-graph from the full original graph effectively letting us partition the larger graph into smaller ones.&lt;/p&gt;
&lt;p&gt;We have only touched on the basics, but the language is still evolving while already quite powerful.&lt;/p&gt;
&lt;p&gt;One last value of having assertions, transformations, and partitioning implemented as basic building blocks for re-usability; users can create new “rule sets” to optimize specific applications, or create a set of “rule sets” with a voting strategy to choose the best resulting execution plan based on cost or some other metric.&lt;/p&gt;</description></item></channel></rss>