<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Darell Estren</title>
    <description>The latest articles on DEV Community by Darell Estren (@darell).</description>
    <link>https://dev.to/darell</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1175409%2F4b266de8-c003-424b-9ae3-97df69510cfd.jpeg</url>
      <title>DEV Community: Darell Estren</title>
      <link>https://dev.to/darell</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/darell"/>
    <language>en</language>
    <item>
      <title>Multi-tenant Angular deployments: CloudFront vs Cloudflare Pages</title>
      <dc:creator>Darell Estren</dc:creator>
      <pubDate>Tue, 25 Aug 2026 20:19:44 +0000</pubDate>
      <link>https://dev.to/darell/multi-tenant-angular-deployments-cloudfront-vs-cloudflare-pages-2pil</link>
      <guid>https://dev.to/darell/multi-tenant-angular-deployments-cloudfront-vs-cloudflare-pages-2pil</guid>
      <description>&lt;h2&gt;
  
  
  The decision is not CloudFront versus Cloudflare Pages
&lt;/h2&gt;

&lt;p&gt;The real decision is &lt;strong&gt;what must be isolated per tenant&lt;/strong&gt;. One Angular build can serve several hostnames when the edge identifies the host and the application receives tenant context. You can also publish one build per tenant, with its own project, domain, and version. Both can be correct; they solve different operational boundaries.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shared build:&lt;/strong&gt; one release, one artifact, and hostname routing at the edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent deployment:&lt;/strong&gt; one artifact and one release per tenant.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shared build:
  school.example.test ─┐
                        ├─&amp;gt; edge: identifies host ─&amp;gt; shared build
  clinic.example.test ─┘

Independent deployments:
  school.example.test ─&amp;gt; Angular app (deployment per tenant)
  clinic.example.test ─&amp;gt; Angular app (deployment per tenant)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 The original post has an interactive diagram of both models — &lt;a href="https://blog.darell.co/en/articles/angular-multi-tenant-deployment-2026-en/" rel="noopener noreferrer"&gt;see it here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Compare the boundary that matters
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Shared build with hostname routing&lt;/th&gt;
&lt;th&gt;Independent deployment per tenant&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tenant isolation&lt;/td&gt;
&lt;td&gt;Must live in authentication, authorization, data, and backend configuration; the bundle is not a security boundary.&lt;/td&gt;
&lt;td&gt;Adds artifact and release separation, but does not replace authorization or data controls.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routing&lt;/td&gt;
&lt;td&gt;The CDN or an edge function resolves &lt;code&gt;school.example.test&lt;/code&gt; and serves the same frontend with tenant context.&lt;/td&gt;
&lt;td&gt;Each hostname maps to its own publication; the frontend can include tenant-specific configuration.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release independence&lt;/td&gt;
&lt;td&gt;Low: one shared version reaches everyone.&lt;/td&gt;
&lt;td&gt;High: one tenant can be published, paused, or rolled back without changing others.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caching and domains&lt;/td&gt;
&lt;td&gt;A common policy simplifies the platform; it requires discipline to avoid mixing content or responses by host.&lt;/td&gt;
&lt;td&gt;Policies and domains are managed per tenant; there are more surfaces to review.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operations&lt;/td&gt;
&lt;td&gt;Fewer pipelines, artifacts, and deployment points.&lt;/td&gt;
&lt;td&gt;More projects or distributions, configuration, verification, and automation.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not turn this table into a promise of automatic isolation. For example, an independent build does not stop an API from returning another tenant's data if the server accepts an identifier without validating identity. The security boundary belongs where the request is authorized and its data is filtered.&lt;/p&gt;




&lt;h2&gt;
  
  
  Model 1: one Angular build, multiple hostnames
&lt;/h2&gt;

&lt;p&gt;This model fits when tenants share a product, code, and release calendar. The edge receives a hostname such as &lt;code&gt;school.example.test&lt;/code&gt; and routes the request to the same static artifact. Angular can read configuration context at startup; the backend remains responsible for validating the tenant and user on every call.&lt;/p&gt;

&lt;p&gt;The design is small, but it needs clear rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Do not trust only a browser-supplied hostname.&lt;/strong&gt; The backend should derive or validate the tenant against identity, an allowed domain, and permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate tenant-dependent content.&lt;/strong&gt; If an API response, generated HTML, or redirect varies by host, the cache key and headers must respect that variation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the Angular shell updateable.&lt;/strong&gt; &lt;code&gt;index.html&lt;/code&gt; and service-worker manifests often need a different policy from hashed assets. That lets the application discover a new version without leaving the entry page stale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test deep links.&lt;/strong&gt; The CDN needs to serve the SPA entry point for Angular Router routes; otherwise a refresh on an internal URL can return a 404.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;CloudFront commonly models this pattern with origins, cache behaviors, edge functions, and domains associated with a distribution. You do not need an edge function when the origin and content are identical for every hostname, but you still need to decide where tenant context is obtained and validated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Model 2: one deployment per tenant
&lt;/h2&gt;

&lt;p&gt;One Cloudflare Pages project—or a separate distribution and origin—per tenant fits when the artifact must change independently: a build configuration, integration, maintenance window, or isolated rollback.&lt;/p&gt;

&lt;p&gt;The main advantage is delivery, not security. You can keep &lt;code&gt;school.example.test&lt;/code&gt; on one publication while &lt;code&gt;clinic.example.test&lt;/code&gt; receives another. The operational cost is that every tenant needs reproducible build configuration, domain setup, caching, SPA fallback, and post-deployment verification.&lt;/p&gt;

&lt;p&gt;In Cloudflare Pages, SPA fallback and cache headers can be declared as static files in the build output. Avoid overlapping header rules when the provider combines values rather than replacing them: a precise rule per path is easier to verify. Custom domains also need to be checked against the correct project; a page may return successfully while still serving another tenant's build when the domain points to the wrong publication.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to choose without over-designing
&lt;/h2&gt;

&lt;p&gt;Choose a &lt;strong&gt;shared build&lt;/strong&gt; when every tenant can accept the same version at the same time and the real differences are data, permissions, and server-validated configuration. It has fewer artifacts and fewer repeated steps.&lt;/p&gt;

&lt;p&gt;Choose &lt;strong&gt;independent deployments&lt;/strong&gt; when you need any of these operational guarantees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;release, roll back, or freeze one tenant without affecting others;&lt;/li&gt;
&lt;li&gt;compile configurations that should not coexist in the same artifact;&lt;/li&gt;
&lt;li&gt;validate a tenant's domain, integration, or cache behavior before publishing;&lt;/li&gt;
&lt;li&gt;delegate operation of one publication without handing over control of the whole platform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can start with a shared build and split only tenants that demonstrate a need for independence. Reversing that creates a matrix of pipelines and domains before anyone needs it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Release checklist
&lt;/h2&gt;

&lt;p&gt;Before publishing either model, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] The backend authorizes both user and tenant; hostname is not the only proof.&lt;/li&gt;
&lt;li&gt;[ ] Angular internal routes survive a browser refresh.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;index.html&lt;/code&gt; and update files are not trapped in a long cache.&lt;/li&gt;
&lt;li&gt;[ ] Hashed assets can reuse cache without hiding a new shell version.&lt;/li&gt;
&lt;li&gt;[ ] Every custom domain serves the expected artifact.&lt;/li&gt;
&lt;li&gt;[ ] Rollback has an identifiable artifact or publication and a post-rollback check.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;CloudFront and Cloudflare Pages are delivery mechanisms; the multi-tenant model defines the boundary you operate. Share a build when you share releases, and separate deployments when you need true publishing independence. In both cases, keep data isolation and authorization in the backend, where they can be verified on every request.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://blog.darell.co/en/articles/angular-multi-tenant-deployment-2026-en/" rel="noopener noreferrer"&gt;DevEdge Blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>cloudflare</category>
      <category>aws</category>
      <category>deployment</category>
    </item>
    <item>
      <title>Angular cache bugs after switching user context</title>
      <dc:creator>Darell Estren</dc:creator>
      <pubDate>Tue, 25 Aug 2026 20:19:10 +0000</pubDate>
      <link>https://dev.to/darell/angular-cache-bugs-after-switching-user-context-idl</link>
      <guid>https://dev.to/darell/angular-cache-bugs-after-switching-user-context-idl</guid>
      <description>&lt;h2&gt;
  
  
  The bug: the API was correct, but the user saw data from another context
&lt;/h2&gt;

&lt;p&gt;A cache often breaks after switching user, tenant, active account, or permission set. The screen requests the same endpoint and the cache responds quickly—but that response belongs to the previous context.&lt;/p&gt;

&lt;p&gt;The cause is not Angular or &lt;code&gt;HttpClient&lt;/code&gt;. It is a key that does not represent the result. If a response depends on &lt;code&gt;contextId&lt;/code&gt;, a URL-only key incorrectly claims that both requests are equivalent.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Incomplete key (&lt;code&gt;GET /api/summary&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Composite key (&lt;code&gt;summary:{contextId}&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context A&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GET /api/summary&lt;/code&gt; → response A&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;summary:context-a&lt;/code&gt; → response A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context B&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GET /api/summary&lt;/code&gt; → &lt;strong&gt;HIT: reuses A's response — stale data&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;summary:context-b&lt;/code&gt; → response B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 The original post has an interactive demo of this comparison — &lt;a href="https://blog.darell.co/en/articles/angular-context-aware-cache-2026-en/" rel="noopener noreferrer"&gt;try it here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rule is simple: &lt;strong&gt;a cache key must include every input that can change the result&lt;/strong&gt;. That commonly means route, normalized parameters, current user or tenant, locale, relevant permissions, and a data version when applicable. Do not add decorative values; include only real result dependencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  A small service with a composite key
&lt;/h2&gt;

&lt;p&gt;This example keeps an in-memory cache. The service that knows about a context transition explicitly invalidates it; stale entries do not survive by accident.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Injectable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HttpClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/common/http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shareReplay&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Summary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;providedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SummaryCache&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;contextId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Summary&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contextId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contextId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contextId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contextId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contextId&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="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;getSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Summary&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalizedFilter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`summary:&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="nx"&gt;contextId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;normalizedFilter&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Summary&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;normalizedFilter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;shareReplay&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;bufferSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;refCount&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the &lt;code&gt;Map&lt;/code&gt;; it is identity. Different contexts or filters produce different keys. &lt;code&gt;setContext&lt;/code&gt; clears entries whenever the identity source changes, which also prevents sensitive data from remaining available longer than needed.&lt;/p&gt;

&lt;p&gt;A full application restart on sign-out may incidentally clear an in-memory cache. Do not rely on that: the bug returns with a tenant picker, administrative impersonation, a permission update, or persistent storage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Diagnose it safely
&lt;/h2&gt;

&lt;p&gt;Start without exposing real data.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reproduce with two test contexts and distinct synthetic responses, such as &lt;code&gt;response A&lt;/code&gt; and &lt;code&gt;response B&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Log or inspect only the key shape: route, normalized filters, and a synthetic context identifier.&lt;/li&gt;
&lt;li&gt;Switch context without reloading the application, then verify whether the second request gets a &lt;code&gt;HIT&lt;/code&gt; for a key that omits context.&lt;/li&gt;
&lt;li&gt;Review every cache reader and invalidator. Fixing one component leaves sibling paths exposed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not log response bodies, tokens, or real identifiers. The diagnosis only needs to prove that two different results use the same key.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tests that catch the regression
&lt;/h2&gt;

&lt;p&gt;The smallest useful tests verify identity and invalidation—not the &lt;code&gt;Map&lt;/code&gt; implementation. Each test creates its own cache and fake HTTP client, so it can run in isolation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;does not reuse a summary after the context changes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fakeHttp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSummaryCacheWithFakeHttp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;context-a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;context-b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fakeHttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reuses the same request inside one context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fakeHttp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSummaryCacheWithFakeHttp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;context-a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; OPEN &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fakeHttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add one case for every dimension that changes a result: tenant, locale, filter, role, or version. If a dimension is absent from the key, the test must prove it cannot change the response; otherwise, include it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pitfall&lt;/th&gt;
&lt;th&gt;Why it fails&lt;/th&gt;
&lt;th&gt;Smallest correction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;URL-only key&lt;/td&gt;
&lt;td&gt;Two contexts share one entry&lt;/td&gt;
&lt;td&gt;Add context identity to the key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clear only on sign-out&lt;/td&gt;
&lt;td&gt;Tenant or permission changes retain old data&lt;/td&gt;
&lt;td&gt;Invalidate on every context transition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use unnormalized objects as keys&lt;/td&gt;
&lt;td&gt;Ordering and whitespace create duplicate entries&lt;/td&gt;
&lt;td&gt;Normalize values before composing the key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One global, unscoped cache&lt;/td&gt;
&lt;td&gt;One screen shares data with another&lt;/td&gt;
&lt;td&gt;Keep cache ownership close to its data domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rely on TTL alone&lt;/td&gt;
&lt;td&gt;Incorrect data remains possible during the TTL&lt;/td&gt;
&lt;td&gt;Explicitly invalidate on context change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A TTL controls age; it does not correct identity. The key prevents incorrect data from entering, and invalidation removes data that is no longer valid.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] The key includes every input that changes the result.&lt;/li&gt;
&lt;li&gt;[ ] Values are normalized before the key is created.&lt;/li&gt;
&lt;li&gt;[ ] Every user, tenant, permission, or account transition invalidates the affected domain.&lt;/li&gt;
&lt;li&gt;[ ] Tests cover a &lt;code&gt;HIT&lt;/code&gt; within one context and a &lt;code&gt;MISS&lt;/code&gt; across contexts.&lt;/li&gt;
&lt;li&gt;[ ] Diagnostic logs use synthetic values and contain neither responses nor credentials.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://blog.darell.co/en/articles/angular-context-aware-cache-2026-en/" rel="noopener noreferrer"&gt;DevEdge Blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>cache</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Terraform Says the Resource Already Exists: Recover State Ownership</title>
      <dc:creator>Darell Estren</dc:creator>
      <pubDate>Tue, 25 Aug 2026 20:19:09 +0000</pubDate>
      <link>https://dev.to/darell/terraform-says-the-resource-already-exists-recover-state-ownership-1gpd</link>
      <guid>https://dev.to/darell/terraform-says-the-resource-already-exists-recover-state-ownership-1gpd</guid>
      <description>&lt;h2&gt;
  
  
  The problem is not that Terraform cannot create the resource
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;AlreadyExists&lt;/code&gt;, &lt;code&gt;AlreadyAssociated&lt;/code&gt;, or creation conflict often triggers the wrong response: delete something and run &lt;code&gt;apply&lt;/code&gt; again. Do not do that. Terraform is not necessarily saying the infrastructure is wrong; it is saying it is trying to create something the provider already knows about while its state does not consider it owned.&lt;/p&gt;

&lt;p&gt;The right question is not "how do I delete it?" It is: &lt;strong&gt;which real object exists, who should manage it, and what does the state represent today?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Terraform works with two sources of truth: configuration defines desired state, and state records which remote objects each resource address manages. When an object exists but that relationship is missing from state, Terraform plans a &lt;code&gt;create&lt;/code&gt;. The provider rejects it because the object—or an exclusive relationship such as an association—already exists.&lt;/p&gt;

&lt;p&gt;In short, the recovery cycle has four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The remote resource exists&lt;/strong&gt; — the provider already knows the object, even though Terraform does not yet manage it from this address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform plans to create it&lt;/strong&gt; — without state ownership, configuration proposes a create that the provider rejects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import establishes ownership&lt;/strong&gt; — import links the existing object to the correct state address without modifying the remote resource.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reviewed plan reconciles&lt;/strong&gt; — a reviewed plan confirms Terraform stops recreating it and shows only understood changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 The original post has an interactive diagram of this flow — &lt;a href="https://blog.darell.co/en/articles/terraform-resource-already-exists-2026-en/" rel="noopener noreferrer"&gt;see it here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Drift and an actual conflict are not the same
&lt;/h2&gt;

&lt;p&gt;These errors share symptoms, not treatment.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;th&gt;Safe response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Existing resource outside state&lt;/td&gt;
&lt;td&gt;The object was created outside Terraform, migrated, or lost from the backend&lt;/td&gt;
&lt;td&gt;Confirm the configuration should own it, then import it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drift&lt;/td&gt;
&lt;td&gt;A managed resource changed outside configuration&lt;/td&gt;
&lt;td&gt;Compare state, configuration, and remote; reconcile through the plan or update code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actual conflict&lt;/td&gt;
&lt;td&gt;Configuration tries to manage an object owned by another module, state, or team&lt;/td&gt;
&lt;td&gt;Establish one owner; do not import blindly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong address&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;for_each&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;, module, or name changed&lt;/td&gt;
&lt;td&gt;Move or import to the correct address after reviewing the plan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important detail: &lt;strong&gt;drift does not automatically mean the object is absent from state&lt;/strong&gt;. It may be a changed property on a resource Terraform already knows. A create failure, on the other hand, often signals an ownership gap: the provider has the object and the Terraform address does not.&lt;/p&gt;

&lt;p&gt;There may also be more than one state managing the same thing. Importing the object into both does not fix that; it duplicates the dispute. First decide which configuration owns it and which one only reads it through data sources or remote outputs.&lt;/p&gt;




&lt;h2&gt;
  
  
  A safe diagnostic flow
&lt;/h2&gt;

&lt;p&gt;Do not run &lt;code&gt;import&lt;/code&gt; on instinct. Follow a short, reversible sequence.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Freeze concurrent changes
&lt;/h3&gt;

&lt;p&gt;While investigating, avoid parallel &lt;code&gt;apply&lt;/code&gt; operations over the same scope. You do not need to delete, recreate, or edit the remote object to diagnose this. The priority is preventing state and provider changes from happening at the same time.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Read the error as a clue, not an instruction
&lt;/h3&gt;

&lt;p&gt;Identify the address Terraform tried to create and the object type. Then check whether it is already recorded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform state list | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'aws_example_binding.app'&lt;/span&gt;
terraform state show &lt;span class="s1"&gt;'aws_example_binding.app["blue"]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it is absent from &lt;code&gt;state list&lt;/code&gt;, that only confirms a local absence: you still need to establish whether the remote resource belongs at that address. If it is present, compare its attributes with configuration and with the provider API or console in read-only mode.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Confirm configuration intent
&lt;/h3&gt;

&lt;p&gt;Review the Terraform block that declares the resource. Ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should this resource exist once, or once for every &lt;code&gt;for_each&lt;/code&gt; key?&lt;/li&gt;
&lt;li&gt;Did the module, key, or index change recently?&lt;/li&gt;
&lt;li&gt;Does another state already have an address describing the same object?&lt;/li&gt;
&lt;li&gt;Does current configuration represent how the existing object should end up?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not import an object just to make a pipeline green if configuration would replace it, associate it with the wrong target, or duplicate it in the next plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Inspect the remote using synthetic identifiers
&lt;/h3&gt;

&lt;p&gt;Use a read-only provider query to confirm identity and critical properties. For example, using a synthetic resource ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cloudctl example describe &lt;span class="nt"&gt;--id&lt;/span&gt; example-1234567890
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact command depends on the provider. What matters is a verifiable match between the remote object, the Terraform address, and the identifier accepted by &lt;code&gt;terraform import&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Import declares ownership; it does not repair infrastructure
&lt;/h2&gt;

&lt;p&gt;When configuration should manage the existing object and no other state owns it, import is the smallest change. Import changes state; it does not create, delete, or update the remote resource.&lt;/p&gt;

&lt;p&gt;Example with a deliberately synthetic address and identifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform import &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'aws_example_binding.app["blue"]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'example-1234567890'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Many compound resources use an import ID made from several fields. Check the resource-type documentation and use only values from the environment where Terraform runs. Never copy identifiers across accounts, environments, or backends.&lt;/p&gt;

&lt;p&gt;After import, the work has only started. Create and read a saved plan in full:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan &lt;span class="nt"&gt;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;recovery.tfplan
terraform show recovery.tfplan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected result is not always "no changes." An in-place update may appear because the remote resource reflects an earlier manual decision while configuration defines the desired state. That can be correct, but only when every change is understood and acceptable.&lt;/p&gt;

&lt;p&gt;Stop if the plan proposes to destroy, replace, or modify objects outside the recovery scope. A successful import proves only that Terraform can read the object; it does not prove the code is safe to apply.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cases that need a different tool
&lt;/h2&gt;

&lt;p&gt;Not everything is an import.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Address changes with no remote change:&lt;/strong&gt; if the resource is already in the same state but its module or key changed, a &lt;code&gt;moved&lt;/code&gt; block is more precise than importing it again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An object that should no longer be managed:&lt;/strong&gt; &lt;code&gt;terraform state rm&lt;/code&gt; removes state ownership, not the remote object. Use it only after explicitly deciding who will manage the object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A shared resource:&lt;/strong&gt; model it as a data source or expose values from the owning state. Two configurations should not compete for the same lifecycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A resource incompatible with configuration:&lt;/strong&gt; fix configuration before importing. State cannot make an incorrect intent safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production, deleting and recreating to resolve a conflict is particularly risky: it can change addresses, dependencies, data, permissions, or connectivity. State recovery exists to avoid that risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rollback and verification
&lt;/h2&gt;

&lt;p&gt;Before changing state, make sure a recoverable copy exists through the backend and your team's approved operating process. If the import targeted the wrong address, the usual rollback is restoring state from that copy or removing &lt;strong&gt;only&lt;/strong&gt; that ownership entry after reviewing the correct address. Do not treat rollback as permission to alter the remote resource.&lt;/p&gt;

&lt;p&gt;The minimum verification looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;terraform state show&lt;/code&gt; displays the object at the expected address.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform plan&lt;/code&gt; no longer attempts to create it.&lt;/li&gt;
&lt;li&gt;Every remaining planned change has a specific explanation.&lt;/li&gt;
&lt;li&gt;No other state is trying to manage the same object.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apply&lt;/code&gt; runs from the reviewed plan with state locking enabled.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once the plan is predictable again, this stops being a creation error and becomes what Terraform does best: reconciling declared intent with existing infrastructure.&lt;/p&gt;




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

&lt;p&gt;&lt;code&gt;Resource Already Exists&lt;/code&gt; is not an invitation to destroy infrastructure. It is an ownership alarm. Separate drift from a real conflict, verify who should own the object, import only when configuration is the correct owner, and review the plan before applying it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://blog.darell.co/en/articles/terraform-resource-already-exists-2026-en/" rel="noopener noreferrer"&gt;DevEdge Blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>devops</category>
      <category>aws</category>
      <category>iac</category>
    </item>
    <item>
      <title>Despliegues Angular multi-tenant: CloudFront vs Cloudflare Pages</title>
      <dc:creator>Darell Estren</dc:creator>
      <pubDate>Tue, 25 Aug 2026 20:16:34 +0000</pubDate>
      <link>https://dev.to/darell/despliegues-angular-multi-tenant-cloudfront-vs-cloudflare-pages-1jai</link>
      <guid>https://dev.to/darell/despliegues-angular-multi-tenant-cloudfront-vs-cloudflare-pages-1jai</guid>
      <description>&lt;h2&gt;
  
  
  La decisión no es CloudFront contra Cloudflare Pages
&lt;/h2&gt;

&lt;p&gt;La decisión real es &lt;strong&gt;qué debe aislarse por tenant&lt;/strong&gt;. Un mismo build de Angular puede atender varios hostnames si el edge identifica el host y la aplicación recibe un contexto de tenant. También puedes publicar un build por tenant, con su propio proyecto, dominio y versión. Ambas opciones pueden ser correctas; resuelven límites operativos distintos.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build compartido:&lt;/strong&gt; una liberación, un artefacto y routing por hostname en el edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Despliegue independiente:&lt;/strong&gt; un artefacto y una liberación por tenant.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build compartido:
  escuela.example.test ─┐
                         ├─&amp;gt; edge: identifica el host ─&amp;gt; build compartido
  clinica.example.test ─┘

Despliegues independientes:
  escuela.example.test ─&amp;gt; app Angular (despliegue por tenant)
  clinica.example.test ─&amp;gt; app Angular (despliegue por tenant)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 El post original tiene un diagrama interactivo de ambos modelos — &lt;a href="https://blog.darell.co/articles/angular-multi-tenant-deployment-2026/" rel="noopener noreferrer"&gt;míralo aquí&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Compara el límite que importa
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterio&lt;/th&gt;
&lt;th&gt;Build compartido con routing por host&lt;/th&gt;
&lt;th&gt;Despliegue independiente por tenant&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Aislamiento de tenants&lt;/td&gt;
&lt;td&gt;Debe estar en autenticación, autorización, datos y configuración de backend; el bundle no es una frontera de seguridad.&lt;/td&gt;
&lt;td&gt;Añade separación de artefactos y releases, pero no reemplaza autorización ni controles de datos.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routing&lt;/td&gt;
&lt;td&gt;El CDN o una función edge resuelve &lt;code&gt;escuela.example.test&lt;/code&gt; y entrega el mismo frontend con contexto de tenant.&lt;/td&gt;
&lt;td&gt;Cada hostname se asocia a su propia publicación; el frontend puede incorporar configuración específica.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Independencia de releases&lt;/td&gt;
&lt;td&gt;Baja: una versión compartida alcanza a todos.&lt;/td&gt;
&lt;td&gt;Alta: puedes publicar, pausar o revertir un tenant sin cambiar los demás.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caché y dominios&lt;/td&gt;
&lt;td&gt;Una política común simplifica la plataforma; requiere disciplina para no mezclar contenido o respuestas por host.&lt;/td&gt;
&lt;td&gt;Las políticas y los dominios se administran por tenant; hay más superficies que revisar.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operación&lt;/td&gt;
&lt;td&gt;Menos pipelines, artefactos y puntos de despliegue.&lt;/td&gt;
&lt;td&gt;Más proyectos o distribuciones, configuraciones, verificaciones y automatización.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No conviertas esta tabla en una promesa de aislamiento automático. Por ejemplo, un build independiente no impide que una API devuelva datos de otro tenant si el servidor acepta un identificador sin validar la identidad. El límite de seguridad debe estar donde se autoriza la solicitud y se filtran los datos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Modelo 1: un build Angular, varios hostnames
&lt;/h2&gt;

&lt;p&gt;Este modelo encaja cuando los tenants comparten producto, código y calendario de cambios. El edge recibe un hostname, por ejemplo &lt;code&gt;escuela.example.test&lt;/code&gt;, y enruta la solicitud hacia el mismo artefacto estático. Angular puede leer un contexto de configuración al iniciar; el backend sigue siendo responsable de validar el tenant y el usuario en cada llamada.&lt;/p&gt;

&lt;p&gt;El diseño es pequeño, pero necesita reglas claras:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No confíes solo en el hostname enviado por el navegador.&lt;/strong&gt; El backend debe derivar o validar el tenant contra identidad, dominio permitido y permisos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separa contenido que depende del tenant.&lt;/strong&gt; Si una respuesta de API, HTML generado o redirección varía por host, la clave de caché y los encabezados deben respetar esa variación.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mantén el shell de Angular actualizable.&lt;/strong&gt; &lt;code&gt;index.html&lt;/code&gt; y los manifiestos del service worker suelen requerir una política distinta de los recursos con hash. Así la aplicación puede descubrir una nueva versión sin dejar obsoleta la página inicial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prueba rutas profundas.&lt;/strong&gt; El CDN necesita servir el punto de entrada de la SPA para rutas de Angular Router; de otro modo, una recarga en una URL interna puede devolver un 404.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;CloudFront suele modelar este patrón con orígenes, comportamientos de caché, funciones edge y dominios asociados a una distribución. No necesitas una función edge si el origen y el contenido son idénticos para todos los hostnames, pero sí necesitas decidir dónde se obtiene y valida el contexto de tenant.&lt;/p&gt;




&lt;h2&gt;
  
  
  Modelo 2: un despliegue por tenant
&lt;/h2&gt;

&lt;p&gt;Un proyecto de Cloudflare Pages —o una distribución y un origen separados— por tenant sirve cuando el artefacto debe cambiar de forma independiente: una configuración de compilación, una integración, una ventana de mantenimiento o un rollback aislado.&lt;/p&gt;

&lt;p&gt;La ventaja principal es de entrega, no de seguridad. Puedes mantener &lt;code&gt;escuela.example.test&lt;/code&gt; en una publicación mientras &lt;code&gt;clinica.example.test&lt;/code&gt; recibe otra. El costo es operativo: cada tenant necesita configuración reproducible de build, dominio, caché, fallback de SPA y verificación posterior al despliegue.&lt;/p&gt;

&lt;p&gt;En Cloudflare Pages, el fallback de SPA y los encabezados de caché se pueden declarar como archivos estáticos del build. Evita reglas de encabezados superpuestas cuando el proveedor combina valores en lugar de reemplazarlos: una regla precisa por ruta es más fácil de verificar. Los dominios personalizados también deben comprobarse contra el proyecto correcto; una página que responde correctamente puede seguir sirviendo el build de otro tenant si el dominio apunta a la publicación equivocada.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cómo elegir sin diseñar de más
&lt;/h2&gt;

&lt;p&gt;Elige &lt;strong&gt;build compartido&lt;/strong&gt; si todos los tenants pueden aceptar la misma versión al mismo tiempo y la diferencia real está en datos, permisos y configuración validada por el servidor. Es el modelo con menos artefactos y menos pasos repetidos.&lt;/p&gt;

&lt;p&gt;Elige &lt;strong&gt;despliegues independientes&lt;/strong&gt; si necesitas cualquiera de estas garantías operativas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;liberar, revertir o congelar un tenant sin afectar a los demás;&lt;/li&gt;
&lt;li&gt;compilar configuraciones que no deben convivir en el mismo artefacto;&lt;/li&gt;
&lt;li&gt;validar dominios, integración o comportamiento de caché por tenant antes de publicar;&lt;/li&gt;
&lt;li&gt;delegar la operación de una publicación concreta sin entregar el control de toda la plataforma.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Puedes empezar con el build compartido y separar solo los tenants que demuestren una necesidad de independencia. Hacer lo contrario crea una matriz de pipelines y dominios antes de saber si alguien necesita usarla.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checklist de release
&lt;/h2&gt;

&lt;p&gt;Antes de publicar cualquiera de los dos modelos, verifica:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] El backend autoriza al usuario y el tenant; el hostname no es la única prueba.&lt;/li&gt;
&lt;li&gt;[ ] Las rutas internas de Angular sobreviven una recarga.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;index.html&lt;/code&gt; y los archivos de actualización no quedan atrapados en una caché larga.&lt;/li&gt;
&lt;li&gt;[ ] Los recursos con hash pueden reutilizar caché sin ocultar una versión nueva del shell.&lt;/li&gt;
&lt;li&gt;[ ] Cada dominio personalizado entrega el artefacto esperado.&lt;/li&gt;
&lt;li&gt;[ ] El rollback tiene un artefacto o publicación identificable y una verificación posterior.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;CloudFront y Cloudflare Pages son mecanismos de entrega; el modelo multi-tenant define el límite que operas. Comparte el build cuando compartes releases y separa despliegues cuando necesitas independencia real de publicación. En ambos casos, deja el aislamiento de datos y autorización en el backend, donde puede verificarse en cada solicitud.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Publicado originalmente en &lt;a href="https://blog.darell.co/articles/angular-multi-tenant-deployment-2026/" rel="noopener noreferrer"&gt;DevEdge Blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>cloudflare</category>
      <category>aws</category>
      <category>deployment</category>
    </item>
    <item>
      <title>Bugs de caché en Angular al cambiar de contexto de usuario</title>
      <dc:creator>Darell Estren</dc:creator>
      <pubDate>Tue, 25 Aug 2026 20:16:33 +0000</pubDate>
      <link>https://dev.to/darell/bugs-de-cache-en-angular-al-cambiar-de-contexto-de-usuario-l7k</link>
      <guid>https://dev.to/darell/bugs-de-cache-en-angular-al-cambiar-de-contexto-de-usuario-l7k</guid>
      <description>&lt;h2&gt;
  
  
  El bug: la API respondió bien, pero el usuario vio datos ajenos a su contexto
&lt;/h2&gt;

&lt;p&gt;Una caché suele fallar después de cambiar de usuario, tenant, cuenta activa o permiso. La pantalla solicita el mismo endpoint y la caché responde rápido; el problema es que esa respuesta pertenece al contexto anterior.&lt;/p&gt;

&lt;p&gt;La causa no es Angular ni &lt;code&gt;HttpClient&lt;/code&gt;. Es una clave que no representa el resultado. Si la respuesta depende de &lt;code&gt;contextId&lt;/code&gt;, una clave basada solo en la URL afirma erróneamente que ambas solicitudes son equivalentes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Clave incompleta (&lt;code&gt;GET /api/summary&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Clave compuesta (&lt;code&gt;summary:{contextId}&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Contexto A&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GET /api/summary&lt;/code&gt; → response A&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;summary:context-a&lt;/code&gt; → response A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contexto B&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GET /api/summary&lt;/code&gt; → &lt;strong&gt;HIT: reutiliza la respuesta de A — dato obsoleto&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;summary:context-b&lt;/code&gt; → response B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 El post original tiene una demo interactiva de esta comparación — &lt;a href="https://blog.darell.co/articles/angular-context-aware-cache-2026/" rel="noopener noreferrer"&gt;pruébala aquí&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;La regla es sencilla: &lt;strong&gt;la clave debe incluir cada input que pueda cambiar la respuesta&lt;/strong&gt;. Normalmente incluye ruta, parámetros normalizados, usuario o tenant actual, idioma, permisos relevantes y versión de datos cuando aplique. No agregues valores decorativos: solo dependencias reales del resultado.&lt;/p&gt;




&lt;h2&gt;
  
  
  Un servicio pequeño con clave compuesta
&lt;/h2&gt;

&lt;p&gt;Este ejemplo mantiene la caché en memoria. El servicio que conoce el cambio de contexto la invalida explícitamente; no deja que una entrada vieja sobreviva por casualidad.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Injectable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HttpClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/common/http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shareReplay&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Summary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;providedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SummaryCache&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;contextId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Summary&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contextId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contextId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contextId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contextId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contextId&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="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;getSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Summary&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalizedFilter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`summary:&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="nx"&gt;contextId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;normalizedFilter&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Summary&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;normalizedFilter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;shareReplay&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;bufferSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;refCount&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El detalle importante no es el &lt;code&gt;Map&lt;/code&gt;; es la identidad. Dos contextos o filtros distintos producen claves distintas. &lt;code&gt;setContext&lt;/code&gt; borra las entradas cuando la fuente de identidad cambia, lo que también evita conservar datos sensibles más tiempo del necesario.&lt;/p&gt;

&lt;p&gt;Si el cambio de usuario crea una aplicación completamente nueva, una caché en memoria puede desaparecer sola. No dependas de ese efecto: el problema reaparece con un selector de tenant, una impersonación administrativa, un cambio de permisos o una caché persistente.&lt;/p&gt;




&lt;h2&gt;
  
  
  Diagnóstico seguro
&lt;/h2&gt;

&lt;p&gt;Empieza sin exponer datos reales.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reproduce con dos contextos de prueba y respuestas sintéticas distinguibles, como &lt;code&gt;response A&lt;/code&gt; y &lt;code&gt;response B&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Registra o inspecciona solo la forma de la clave: ruta, filtros normalizados y un identificador sintético de contexto.&lt;/li&gt;
&lt;li&gt;Cambia de contexto sin recargar la aplicación y confirma si la segunda solicitud obtiene un &lt;code&gt;HIT&lt;/code&gt; para una clave que no contiene el contexto.&lt;/li&gt;
&lt;li&gt;Revisa todos los lugares que leen y limpian la caché. Corregir solo un componente deja rutas hermanas vulnerables.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Evita registrar cuerpos de respuestas, tokens o identificadores reales. Para este diagnóstico basta demostrar que dos resultados distintos están usando la misma clave.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pruebas que detectan la regresión
&lt;/h2&gt;

&lt;p&gt;La prueba mínima verifica identidad e invalidación, no la implementación interna de &lt;code&gt;Map&lt;/code&gt;. Cada prueba crea su propia caché y cliente HTTP falso, así que puede ejecutarse de forma aislada.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;does not reuse a summary after the context changes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fakeHttp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSummaryCacheWithFakeHttp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;context-a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;context-b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fakeHttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reuses the same request inside one context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fakeHttp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSummaryCacheWithFakeHttp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;context-a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; OPEN &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fakeHttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Añade un caso por cada dimensión que altere la respuesta: tenant, idioma, filtro, rol o versión. Si una dimensión no aparece en la clave, la prueba debe demostrar que no cambia el resultado; de lo contrario, inclúyela.&lt;/p&gt;




&lt;h2&gt;
  
  
  Errores frecuentes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;Por qué falla&lt;/th&gt;
&lt;th&gt;Corrección mínima&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Usar solo la URL&lt;/td&gt;
&lt;td&gt;Dos contextos comparten una entrada&lt;/td&gt;
&lt;td&gt;Añadir identidad de contexto a la clave&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Limpiar solo al cerrar sesión&lt;/td&gt;
&lt;td&gt;Un cambio de tenant o permisos mantiene datos anteriores&lt;/td&gt;
&lt;td&gt;Invalidar en toda transición de contexto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usar objetos sin normalizar como clave&lt;/td&gt;
&lt;td&gt;El orden o el espaciado crean entradas duplicadas&lt;/td&gt;
&lt;td&gt;Normalizar valores antes de formar la clave&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guardar una caché global sin alcance&lt;/td&gt;
&lt;td&gt;Una pantalla comparte datos con otra&lt;/td&gt;
&lt;td&gt;Mantener la caché cerca de su dominio de datos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confiar solo en TTL&lt;/td&gt;
&lt;td&gt;Durante el TTL el dato equivocado sigue siendo posible&lt;/td&gt;
&lt;td&gt;Invalidar de forma explícita al cambiar contexto&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Un TTL controla antigüedad; no corrige identidad. La clave evita que un dato incorrecto entre; la invalidación elimina datos que dejaron de ser válidos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lista de verificación
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] La clave contiene todos los inputs que cambian el resultado.&lt;/li&gt;
&lt;li&gt;[ ] Los valores se normalizan antes de crear la clave.&lt;/li&gt;
&lt;li&gt;[ ] Cada transición de usuario, tenant, permiso o cuenta invalida el dominio afectado.&lt;/li&gt;
&lt;li&gt;[ ] Las pruebas cubren un &lt;code&gt;HIT&lt;/code&gt; dentro del mismo contexto y un &lt;code&gt;MISS&lt;/code&gt; entre contextos.&lt;/li&gt;
&lt;li&gt;[ ] Los logs de diagnóstico usan valores sintéticos y no incluyen respuestas ni credenciales.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Publicado originalmente en &lt;a href="https://blog.darell.co/articles/angular-context-aware-cache-2026/" rel="noopener noreferrer"&gt;DevEdge Blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>cache</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Terraform dice que el recurso ya existe: recupera la propiedad del state</title>
      <dc:creator>Darell Estren</dc:creator>
      <pubDate>Tue, 25 Aug 2026 20:13:00 +0000</pubDate>
      <link>https://dev.to/darell/terraform-dice-que-el-recurso-ya-existe-recupera-la-propiedad-del-state-1e68</link>
      <guid>https://dev.to/darell/terraform-dice-que-el-recurso-ya-existe-recupera-la-propiedad-del-state-1e68</guid>
      <description>&lt;h2&gt;
  
  
  El problema no es que Terraform no pueda crear el recurso
&lt;/h2&gt;

&lt;p&gt;Un &lt;code&gt;AlreadyExists&lt;/code&gt;, &lt;code&gt;AlreadyAssociated&lt;/code&gt; o conflicto de creación suele disparar la reacción equivocada: borrar algo y volver a ejecutar &lt;code&gt;apply&lt;/code&gt;. No lo hagas. Terraform no está diciendo que la infraestructura esté necesariamente mal; está diciendo que intenta crear algo que el proveedor ya conoce, mientras su state no lo considera bajo su propiedad.&lt;/p&gt;

&lt;p&gt;La pregunta correcta no es “¿cómo lo elimino?”, sino: &lt;strong&gt;¿qué objeto real existe, quién debería administrarlo y qué representa hoy el state?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Terraform trabaja con dos fuentes de verdad: la configuración define el estado deseado y el state registra qué objetos remotos administra cada dirección de recurso. Si un objeto existe pero falta esa relación en el state, Terraform planea un &lt;code&gt;create&lt;/code&gt;. El proveedor rechaza la creación porque el objeto —o una relación exclusiva, como una asociación— ya existe.&lt;/p&gt;

&lt;p&gt;En resumen, el ciclo de recuperación tiene cuatro pasos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;El recurso remoto existe&lt;/strong&gt; — el proveedor ya conoce el objeto, aunque Terraform todavía no lo administra desde esta dirección.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform planea crearlo&lt;/strong&gt; — al no encontrar ownership en el state, la configuración propone un &lt;code&gt;create&lt;/code&gt; que el proveedor rechaza.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El import establece ownership&lt;/strong&gt; — importar vincula el objeto existente con la dirección correcta en el state sin modificar el recurso remoto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El plan revisado reconcilia&lt;/strong&gt; — un plan revisado confirma que Terraform deja de recrearlo y muestra solo los cambios entendidos.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 El post original tiene un diagrama interactivo de este flujo — &lt;a href="https://blog.darell.co/articles/terraform-resource-already-exists-2026/" rel="noopener noreferrer"&gt;mirálo acá&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Drift y conflicto real no son lo mismo
&lt;/h2&gt;

&lt;p&gt;Estos errores comparten síntomas, pero no tratamiento.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situación&lt;/th&gt;
&lt;th&gt;Qué ocurrió&lt;/th&gt;
&lt;th&gt;Respuesta segura&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recurso existente fuera del state&lt;/td&gt;
&lt;td&gt;El objeto fue creado fuera de Terraform, migrado o se perdió del backend&lt;/td&gt;
&lt;td&gt;Confirmar que la configuración debe poseerlo e importarlo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drift&lt;/td&gt;
&lt;td&gt;Un recurso ya administrado cambió fuera de la configuración&lt;/td&gt;
&lt;td&gt;Comparar state, configuración y remoto; dejar que el plan reconcilie o ajustar código&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflicto real&lt;/td&gt;
&lt;td&gt;La configuración intenta administrar un objeto que pertenece a otro módulo, state o equipo&lt;/td&gt;
&lt;td&gt;Definir un único dueño; no importar a ciegas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dirección equivocada&lt;/td&gt;
&lt;td&gt;Un &lt;code&gt;for_each&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;, módulo o nombre cambió&lt;/td&gt;
&lt;td&gt;Mover o importar hacia la dirección correcta, después de revisar el plan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;El detalle importante: &lt;strong&gt;drift no significa automáticamente que el objeto falte del state&lt;/strong&gt;. Puede ser una propiedad diferente en un recurso que Terraform sí conoce. En cambio, un error al crear normalmente señala una brecha de ownership: el proveedor tiene el objeto y la dirección de Terraform no.&lt;/p&gt;

&lt;p&gt;También puede haber más de un state gestionando la misma cosa. Importar el mismo objeto en ambos no arregla nada; duplica la disputa. Primero decide cuál configuración será la propietaria y cuál solo la consulta mediante &lt;code&gt;data&lt;/code&gt; sources o salidas remotas.&lt;/p&gt;




&lt;h2&gt;
  
  
  Flujo de diagnóstico seguro
&lt;/h2&gt;

&lt;p&gt;No ejecutes &lt;code&gt;import&lt;/code&gt; por intuición. Seguí una secuencia corta y reversible.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Congelá cambios concurrentes
&lt;/h3&gt;

&lt;p&gt;Mientras investigas, evita &lt;code&gt;apply&lt;/code&gt; paralelos sobre el mismo alcance. No necesitas borrar, recrear ni editar el objeto remoto para diagnosticar. La prioridad es que nadie cambie state y proveedor al mismo tiempo.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Leé el error como una pista, no como una instrucción
&lt;/h3&gt;

&lt;p&gt;Identificá la dirección que Terraform intentó crear y el tipo de objeto. Luego buscá si ya está registrado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform state list | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'aws_example_binding.app'&lt;/span&gt;
terraform state show &lt;span class="s1"&gt;'aws_example_binding.app["blue"]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si no aparece en &lt;code&gt;state list&lt;/code&gt;, eso confirma solo una ausencia local: todavía falta saber si el recurso remoto corresponde a esa dirección. Si aparece, compara sus atributos con la configuración y con la API o consola del proveedor en modo lectura.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Confirmá la intención de la configuración
&lt;/h3&gt;

&lt;p&gt;Revisá el bloque Terraform que declara el recurso. Preguntá:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Ese recurso debe existir una sola vez o por cada clave de &lt;code&gt;for_each&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;¿El módulo, la clave o el índice cambiaron recientemente?&lt;/li&gt;
&lt;li&gt;¿Otro state ya tiene una dirección que describe el mismo objeto?&lt;/li&gt;
&lt;li&gt;¿La configuración actual representa el objeto existente tal como debería quedar?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No importes un objeto para “hacer verde el pipeline” si la configuración lo reemplazaría, lo asociaría a un destino incorrecto o lo duplicaría en el siguiente plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Inspeccioná el remoto con identificadores sintéticos
&lt;/h3&gt;

&lt;p&gt;Usá una consulta de sólo lectura del proveedor para confirmar identidad y propiedades críticas. Por ejemplo, para un recurso con ID sintético:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cloudctl example describe &lt;span class="nt"&gt;--id&lt;/span&gt; example-1234567890
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El comando exacto depende del proveedor. Lo importante es registrar una correspondencia verificable entre el objeto remoto, la dirección Terraform y el identificador que acepta &lt;code&gt;terraform import&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Importar es declarar ownership, no reparar infraestructura
&lt;/h2&gt;

&lt;p&gt;Cuando la configuración debe administrar el objeto existente y ningún otro state lo posee, importarlo es el cambio mínimo. El import modifica el state; no crea, borra ni actualiza el recurso remoto.&lt;/p&gt;

&lt;p&gt;Ejemplo con una dirección y un identificador deliberadamente sintéticos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform import &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'aws_example_binding.app["blue"]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'example-1234567890'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para muchos recursos compuestos, el import ID combina campos. Consulta la documentación del tipo de recurso y usa solo los valores del entorno donde ejecutas Terraform. No copies identificadores entre cuentas, entornos o backends.&lt;/p&gt;

&lt;p&gt;Después del import, el trabajo recién empieza. Ejecutá un plan guardado y leelo completo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan &lt;span class="nt"&gt;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;recovery.tfplan
terraform show recovery.tfplan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El resultado esperado no siempre es “sin cambios”. Puede haber una actualización in-place porque el recurso remoto refleja una decisión manual anterior y la configuración define el estado deseado. Eso puede ser correcto, pero sólo si cada cambio es entendido y aceptable.&lt;/p&gt;

&lt;p&gt;Detenete si el plan propone destruir, reemplazar o modificar objetos fuera del alcance de la recuperación. Un import exitoso sólo prueba que Terraform puede leer el objeto; no prueba que el código sea seguro para aplicarlo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Casos que requieren otra herramienta
&lt;/h2&gt;

&lt;p&gt;No todo se resuelve con import.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cambio de dirección sin cambio remoto:&lt;/strong&gt; si el recurso ya está en el mismo state pero cambió de módulo o clave, un bloque &lt;code&gt;moved&lt;/code&gt; es más preciso que importar otra vez.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Objeto que debe dejar de ser administrado:&lt;/strong&gt; &lt;code&gt;terraform state rm&lt;/code&gt; elimina ownership del state, no el objeto remoto. Es útil sólo después de decidir explícitamente quién lo administrará.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recurso compartido:&lt;/strong&gt; modélalo como &lt;code&gt;data&lt;/code&gt; source o expón sus valores desde el state dueño. Dos configuraciones no deberían competir por el mismo ciclo de vida.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recurso no compatible con la configuración:&lt;/strong&gt; corrige la configuración antes de importar. El state no convierte una intención incorrecta en una segura.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En producción, borrar y recrear para resolver un conflicto es especialmente peligroso: puede cambiar direcciones, dependencias, datos, permisos o conectividad. La recuperación de state busca precisamente evitar ese riesgo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rollback y verificación
&lt;/h2&gt;

&lt;p&gt;Antes de cambiar state, asegura una copia recuperable mediante el backend y el mecanismo operativo aprobado por tu equipo. Si el import fue hacia una dirección equivocada, el rollback habitual es restaurar el state desde esa copia o eliminar &lt;strong&gt;solo&lt;/strong&gt; esa entrada de ownership después de revisar la dirección correcta. No uses el rollback como permiso para tocar el recurso remoto.&lt;/p&gt;

&lt;p&gt;La verificación mínima queda así:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;terraform state show&lt;/code&gt; muestra el objeto en la dirección esperada.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform plan&lt;/code&gt; no intenta crearlo de nuevo.&lt;/li&gt;
&lt;li&gt;Cada cambio restante del plan tiene una explicación concreta.&lt;/li&gt;
&lt;li&gt;No existe otro state intentando administrar el mismo objeto.&lt;/li&gt;
&lt;li&gt;El &lt;code&gt;apply&lt;/code&gt; se ejecuta con el plan revisado y el bloqueo de state activo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cuando el plan vuelve a ser predecible, el incidente deja de ser un error de creación y vuelve a ser lo que Terraform hace mejor: reconciliar una intención declarada con infraestructura existente.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Resource Already Exists&lt;/code&gt; no es una invitación a destruir infraestructura. Es una alarma sobre ownership. Separa drift de conflicto real, verifica quién debe poseer el objeto, importa solo cuando la configuración sea la dueña correcta y revisa el plan antes de aplicar.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Publicado originalmente en &lt;a href="https://blog.darell.co/articles/terraform-resource-already-exists-2026/" rel="noopener noreferrer"&gt;DevEdge Blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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