<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-05-15T08:05:25+00:00</updated><id>/feed.xml</id><title type="html">Jan Domanski</title><subtitle>&quot;a biochemist with programming skills&quot; CTO and co-founder of a science tech startup @Labstep 🧫 Helps co-organize @HackAndTellLdn a 1000+ tech meetup in London</subtitle><entry><title type="html">State of co-folding 2025</title><link href="/cofolding/alphafold/2025/11/02/state-of-cofolding-post.html" rel="alternate" type="text/html" title="State of co-folding 2025" /><published>2025-11-02T23:00:00+00:00</published><updated>2025-11-02T23:00:00+00:00</updated><id>/cofolding/alphafold/2025/11/02/state-of-cofolding/post</id><content type="html" xml:base="/cofolding/alphafold/2025/11/02/state-of-cofolding-post.html"><![CDATA[<p>Last week, the co-folding world exploded: Pearl, OpenFold3, and BoltzGen all dropped within 48 hours. With so many AF3 “clones” now in play, I wanted to map out who’s who and what challenges these models face in 2026.</p>

<h3 id="why-does-structure-prediction-matter">Why does structure prediction matter?</h3>

<p>Structure prediction is a 2nd order thing to drug-discovery programs, where people really care about:</p>

<p>1.⁠ ⁠Finding therapeutic targets to go after,
2.⁠ ⁠⁠finding hit molecules for those targets, and
3.⁠ ⁠⁠optimizing those hit molecules.</p>

<p>In the discovery context, structure prediction is a foundation, a “means to an end”, not the goal itself – that’s markedly different from a tech crowd, where perhaps “making the number go higher” (benchmarks) is seen as sufficient.</p>

<p>How does structure prediction impact lead optimization? Well, for one thing, better co-folding models equals better starting structures for FEP simulations. As these models improve geometry and chemistry awareness, they’ll reduce the setup time and expert touch currently required for FEP workflows.</p>

<h3 id="comparisons-are-tricky">Comparisons are tricky</h3>

<p>The public structure prediction benchmarks often focus on aspects of the predicted pose, like ligand RMSD to crystal structure. 
Geometric problems are often incompletely captured but that’s changing: people are looking at non-physical protein/ligand/water contacts and geometries.</p>

<figure class="image">
    <img src="/docs/images/posts/2025-10-30-state-of-cofolding/boltz-slack.png" alt="Boltz community is starting to flag the geometry issues" />
    <figcaption>Boltz community is starting to flag the geometry issues</figcaption>
</figure>

<figure class="image">
    <img src="/docs/images/posts/2025-10-30-state-of-cofolding/pearl-evaluation-pose-quality.png" alt="Pearl appears to have a more comprehensive understanding of what's needed for successful drug-discovery usage" />
    <figcaption>Pearl appears to have a more comprehensive understanding of what's needed for successful drug-discovery usage</figcaption>
</figure>

<p>While many SAAS products (Nvidia NIM, Tamarind.bio, Deepmirror, Rowan) will allow you to run these models, almost none of them really allow you to benchmark (Apheris is a notable exception). 
Simple inference is the level of “kicking the tires”, barely above Hugging Face examples, and hard to imagine how that fits into a discovery workflow.</p>

<h2 id="whos-who-2025">Who’s who 2025</h2>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Parent company / org</th>
      <th>Open source?</th>
      <th>What’s special</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>AlphaFold 3</strong></td>
      <td>Google DeepMind &amp; Isomorphic Labs</td>
      <td>❌ No (free server access for non-commercial use)</td>
      <td>“The OG”: universal co-folding, which predicts structures/interactions for proteins, DNA/RNA, small molecules, antibodies, and ions.</td>
    </tr>
    <tr>
      <td><strong>RoseTTAFold All-Atom</strong></td>
      <td>Baker Lab / RoseTTAFold team</td>
      <td>✅ Yes (open GitHub release)</td>
      <td>Extends RoseTTAFold to full biomolecular assemblies — proteins plus DNA/RNA, ligands, metals, and covalent modifications</td>
    </tr>
    <tr>
      <td><strong>Boltz-1</strong></td>
      <td>Boltz (MIT Jameel Clinic collaborators)</td>
      <td>✅ Yes (MIT License; code &amp; weights)</td>
      <td>Open all-atom co-folding.</td>
    </tr>
    <tr>
      <td><strong>Boltz-1x</strong></td>
      <td>Boltz</td>
      <td>✅ Yes (MIT)</td>
      <td>Boltz-1 variant.</td>
    </tr>
    <tr>
      <td><strong>Boltz-2</strong></td>
      <td>Boltz</td>
      <td>❔ Weights-only (MIT), no training code, no data</td>
      <td>Affinity prediction.</td>
    </tr>
    <tr>
      <td><strong>BoltzGen</strong></td>
      <td>Boltz / MIT &amp; collaborators</td>
      <td>❔ Yes (MIT), no data</td>
      <td>Generative peptide/protein model on top of Boltz-2.</td>
    </tr>
    <tr>
      <td><strong>Chai-1</strong></td>
      <td>Chai Discovery</td>
      <td>✅ Yes (Apache-2.0)</td>
      <td>Biologics-only?</td>
    </tr>
    <tr>
      <td><strong>Protenix (“Protein-X”)</strong></td>
      <td>ByteDance</td>
      <td>✅ Yes (Apache-2.0)</td>
      <td>-</td>
    </tr>
    <tr>
      <td><strong>HelixFold3</strong></td>
      <td>Baidu</td>
      <td>❔ Yes, non-commercial only</td>
      <td>-</td>
    </tr>
    <tr>
      <td><strong>NeuralPLexer3</strong></td>
      <td>Iambic Tx</td>
      <td>❌ No (proprietary)</td>
      <td>-</td>
    </tr>
    <tr>
      <td><strong>DragonFold</strong></td>
      <td>CHARM Therapeutics</td>
      <td>❌ No (proprietary)</td>
      <td>Matches AF3 on small-molecule prediction, used in discovery.</td>
    </tr>
    <tr>
      <td><strong>Pearl</strong></td>
      <td>Genesis Molecular AI</td>
      <td>❌ No (proprietary)</td>
      <td>Exceeds AF3 on small-molecule prediction, used in discovery.</td>
    </tr>
    <tr>
      <td><strong>OpenFold 3</strong></td>
      <td>OpenFold Consortium</td>
      <td>✅ Yes (preview open-source release)</td>
      <td>First open-source re-implementation to match AF3 performance across modalities (RNA, small molecule, biologics).</td>
    </tr>
  </tbody>
</table>

<h2 id="favorites-for-2026">Favorites for 2026</h2>

<h3 id="closed-source-pearl">Closed-source: Pearl</h3>

<p>Probably the best closed-source model for small molecules at the moment, released earlier this week. Another giggle: the company renamed from “Genesis Therapeutics” to “Genesis Molecular AI”… I see what you did here!
With the (dire) state of the biotech investment, everyone seems to be cuddling closer and closer to “AI”, where investment dollar flows happily.</p>

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Excited to share Pearl from Genesis Molecular AI (yes, we&#39;ve updated our name!): the first co-folding model to clearly surpass AlphaFold 3 on protein-ligand structure prediction.<br /><br />Unlike LLMs that train on vast public data, drug discovery AI faces fundamental data scarcity. Our… <a href="https://t.co/Jmc2FQ65mA">pic.twitter.com/Jmc2FQ65mA</a></p>&mdash; Genesis Molecular AI (@genesistxai) <a href="https://twitter.com/genesistxai/status/1983275689643229286?ref_src=twsrc%5Etfw">October 28, 2025</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<p>As an ex-competitor of theirs, this is certainly interesting to me: Charm Therapeutics also focused on small molecules. CharmTx DragonFold model was <a href="https://charmtx.com/dragon-a-top-performing-structure-prediction-model-for-small-molecule-discovery/">able to match AF3 but not meaningfully exceeded it</a>. The Genesis crew significantly outperforms AF3 on small-molecule structure prediction.</p>

<p>The code and model remains closed-source, even to academics/non-commercial users. The <a href="https://genesis.ml/wp-content/uploads/2025/10/pearl_technical_report.pdf">technical report</a> which accompanied the release is sparse on the details but if you’ve worked in this field, the subtle nudges within are pretty clear: some mixture of architecture changes, and synthetic data generation.</p>

<p>The team is suitably cross discipline and appears well funded. Such teams will continue to outperform pure tech plays – in my view – because pure tech will often tap-out in the data curation angle, or success metric definition: the ability to curate data with help of crystallographers, affinity data with biology and chemistry teams, ability to generate synthetic data with computational chemists. “Make number go higher” only works if you have a clue how to define the number, and your science colleagues do.</p>

<h3 id="open-source-of3">Open-source: OF3</h3>

<p>I personally share the enthusiasm of the post below :D</p>

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">🔥 It&#39;s here: OpenFold3 is now live.<br /><br />THE open-source foundation model for predicting 3D structures of proteins, nucleic acids &amp; small molecules. This is where the future of drug discovery and biomolecular AI lives.<br /><br />Built by <a href="https://twitter.com/open_fold?ref_src=twsrc%5Etfw">@open_fold</a>. Hosted on <a href="https://twitter.com/huggingface?ref_src=twsrc%5Etfw">@huggingface</a>.<br />👇 more <a href="https://t.co/IukdhBL8rD">pic.twitter.com/IukdhBL8rD</a></p>&mdash; Georgia Channing (@cgeorgiaw) <a href="https://twitter.com/cgeorgiaw/status/1983241877479379187?ref_src=twsrc%5Etfw">October 28, 2025</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<p>OF3 certainly looks to be more “opener” than “open-weights” competitors. To be fully and meaningfully open, the models should disclose their training code and data. Weights and inference code just allows the model to be run “as is”.</p>

<p>What’s important for OF3, and more broadly the open-model community, is to move beyond AF3 and into the future. The OF3 crew has the (massive) benefit of multiple talents in the house (tech + science, academia + industry) – bringing those together will be the key to success.</p>

<h2 id="challenges-and-ideas">Challenges and ideas</h2>

<h3 id="generalization">Generalization</h3>

<p><strong>The problem</strong> Performance drops sharply on targets not seen in training. Models interpolate well but struggle to extrapolate.</p>

<p><strong>Why it matters</strong> Biologics – where the hypervariable Ab regions by definition don’t have many “similar” sequences in the training dataset. Small molecules – targets with limited structural data.</p>

<p><strong>What could work</strong></p>
<ul>
  <li>better architectures to increase what the model can squeeze out of the same data,</li>
  <li>federated learning to provide proprietary data,</li>
  <li>better measuring sticks, Runs’N’Poses (RnP) is a great start.</li>
</ul>

<h3 id="validation-beyond-benchmarks">Validation beyond benchmarks</h3>

<p><strong>The problem</strong> The public structure prediction benchmarks are a “minimum bar” to pass. Most of the AF3 clones have not been tested in discovery.</p>

<p><strong>Why it matters</strong> The benchmarks don’t fully capture the expectations of a medicinal chemist, trust is lost when used prospectively and artifacts are observed.</p>

<p><strong>What could work</strong></p>
<ul>
  <li>Better benchmarks, with more medicinal chemistry focus</li>
  <li>Back-testing on industry data (preferably data not seen in training).</li>
  <li>Blind prospective validation – but that takes time and money.</li>
</ul>

<h3 id="protein-dynamics-including-disorder">Protein dynamics (including disorder)</h3>

<p>Protein dynamics and protein disorder comes in many different flavors: from fully disordered proteins, to those that fold upon contact with others “partially disordered”. On the more humble end, you could think as allostery as the minimal case of protein disorder albeit that’s a bit unorthodox.</p>

<p><strong>The problem</strong> Current generation of models are great “structure predictors” and don’t appear to have a great understanding of protein physics/dynamics, folding mechanism or kinetics/thermodynamics is out of reach.</p>

<p><strong>Why it matters</strong>
Many drug targets are partially disordered or undergo conformational changes upon binding. Single static structures miss the ensemble.</p>

<p><strong>What could work</strong></p>
<ul>
  <li>Training on experimental stability/flexibility data (HDX-MS, radical reactivity)</li>
  <li>Synthetic MD trajectories, e.g., <a href="https://www.biorxiv.org/content/10.1101/2025.10.19.683306v1">AF CALVADOS</a></li>
</ul>

<h3 id="data-scarcity-and-quality">Data scarcity and quality</h3>

<p><strong>The problem</strong> Unlike LLMs trained on the entire internet, structure models are data-starved. Self-distillation risks “exhaust gas recirculation”—training on your own mediocre predictions degrades quality.</p>

<p><strong>Why it matters</strong>: More data = better performance, but only if the data is high-quality. Garbage in, garbage out.</p>

<p><strong>What could work</strong></p>
<ul>
  <li>Synthetic data generation, with careful curation
    <ul>
      <li>Molecular dynamics (Boltz-2)</li>
      <li>From affinity datasets with closely-linked structural data (Genesis?)</li>
    </ul>
  </li>
  <li>Experimental data generation for structure and affinity (OpenBind consortium)</li>
  <li>Fine-tuning on proprietary data (works better than RAG for these models)</li>
</ul>

<h3 id="scaling-laws">Scaling laws</h3>

<p><strong>The problem</strong> We haven’t seen clear scaling laws emerge yet, where same architecture can be scaled up to a larger model and unlock new capabilities.</p>

<p><strong>Why it matters</strong> Scaling laws drive investment, without them only data/architecture breakthrough are needed.</p>

<p><strong>What we’re learning</strong> Genesis hints at scaling with synthetic data. More investigation needed.</p>

<h3 id="chemistry-awareness">Chemistry awareness</h3>

<p><strong>The problem</strong> Models still generate non-physical geometries – protein and ligand impacted – bad bond angles, steric clashes, impossible ring conformations.</p>

<p><strong>Why it matters</strong> Medicinal chemists won’t trust poses that violate basic chemistry. “This nitrogen can’t be sp3 in this fragment” = loss of trust in the tech.</p>

<p><strong>What could work</strong></p>
<ul>
  <li>Improve the handling of chemistry features: bond orders, protonation, chirality, ring puckering, non-physical intra-molecular contacts,</li>
  <li>Physics-based geometry constraints during training/inference,</li>
  <li><em>Water co-folding</em> Many discovery programs hinge on explicit water interactions—currently missing from all models.</li>
</ul>

<h3 id="affinity-prediction">Affinity prediction</h3>

<p><strong>The problem</strong> Current affinity models appear to overfit training distributions. Performance drops on novel scaffolds or targets not in training data.</p>

<p><strong>Why it matters</strong> In lead optimization, accurate ΔΔG (relative binding affinity) prediction can save money spent on synthesis and experimental validation.</p>

<p><strong>What could work</strong>:</p>
<ul>
  <li>Split the affinity problem into two: (1) binder/non-binder separation between different series, and (2) accurate affinity prediction within a series,</li>
  <li>Synthetic data: pair known affinity datasets with predicted/AlphaFilled structures</li>
</ul>

<h3 id="training-complexity">Training complexity</h3>

<p><strong>The problem</strong> Multi-stage training regimes make reproduction difficult. These models aren’t as big as LLMs (64 A100s suffices) but the “fiddly” training process creates replication barriers.</p>

<p><strong>Why it matters</strong> Complex training = fewer groups can validate/extend the work, because they have to “guess” the training stage parameters.</p>

<h3 id="architecture-frontiers">Architecture frontiers</h3>

<p><strong>The problem</strong> Hard to predict what architecture changes will matter. Attention mechanisms have memory limitations for all-atom polymer representations.</p>

<p><strong>Why it matters</strong> Current generation of models could be permanently limited because of the loss of all-atom representation, needed to save memory.</p>

<p><strong>What could work</strong></p>

<ul>
  <li>Pearl hint at the return of SO(3)-equivariant architecture (present in AF2 but removed in AF3)</li>
  <li>Improved coarse-graining: 2-3 tokens per residue instead of 1 (captures more geometry without full all-atom overhead)</li>
  <li>Hybrid approaches: coarse for bulk structure, all-atom for binding site (already the case for PTM residues, which are all-atom)</li>
</ul>

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Why do inverse folding methods hate bulky aromatics so much? From the BoltzGen paper <a href="https://t.co/BhDcOHuk42">pic.twitter.com/BhDcOHuk42</a></p>&mdash; Diego del Alamo (@DdelAlamo) <a href="https://twitter.com/DdelAlamo/status/1982833984859164778?ref_src=twsrc%5Etfw">October 27, 2025</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<h2 id="conclusion-and-predictions-for-2026">Conclusion and predictions for 2026</h2>

<p>This field is currently pretty buzzing or at the very least showing “healthy signs of activity”. These models are here to stay, and are quickly becoming the bread-and-butter of discovery. We understand much better where they work and why. However, there are quite of few of them and the differences are to express (commodification at least at the surface level).</p>

<p>Maybe we’ll cure all diseases in 10 years – that’d be really awesome – but we probably won’t. We need more AF3-like moments for different parts of the drug discovery process.</p>

<p>Prediction is hard, I was certainly wrong about two major things last year</p>

<ul>
  <li>I was skeptical of federated learning, yet that strategy seems to be doing pretty well (eg Apheris, OpenFold)</li>
  <li>I was skeptical of synthetic data via but that too seems to have done well (Genesis and Boltz have reported on this, albeit have taken different strategies)</li>
</ul>

<p>For 2026, I think we’ll see multiple models go beyond AF3, both in terms of capabilities and raw structure prediction performance. People are definitely trying to address the data limitations, both via synthetic data generation and experimentally via initiatives like OpenBind. Targets with little structural data will remain challenging, unless new break-through architectures emerge.</p>

<p>Scientists, talk to your tech buddies – tech people talk to your science mates. None of you can do it alone, only together can you complete the puzzle.</p>

<h2 id="acknowledgements">Acknowledgements</h2>

<p>Many thanks to Peter Mernyei &amp; Jenke Scheen for their feedback on this post.</p>]]></content><author><name></name></author><category term="cofolding" /><category term="alphafold" /><summary type="html"><![CDATA[Last week, the co-folding world exploded: Pearl, OpenFold3, and BoltzGen all dropped within 48 hours. With so many AF3 “clones” now in play, I wanted to map out who’s who and what challenges these models face in 2026.]]></summary></entry><entry><title type="html">Which companies it makes sense to work for</title><link href="/companies/startups/2023/04/30/join-or-not-to-join-post.html" rel="alternate" type="text/html" title="Which companies it makes sense to work for" /><published>2023-04-30T23:00:00+00:00</published><updated>2023-04-30T23:00:00+00:00</updated><id>/companies/startups/2023/04/30/join-or-not-to-join/post</id><content type="html" xml:base="/companies/startups/2023/04/30/join-or-not-to-join-post.html"><![CDATA[<blockquote>
  <p><strong>Update (2025-10):</strong><br />
This post didn’t age that well!</p>
</blockquote>

<p>Recently I was approached by an old co-worker and good colleague to consider re-joining a place we used to work together. I pretty much declined and probably offended him a bit (sorry!) but it really got me thinking about which opportunities I’d like to take vs pass on.</p>

<p>Maybe this view comes from a personal bias. Surviving cancer definitely sharpens the focus. There is more time for family and friends, and more time for meaningful work, but less time for money for the sake of money. Can’t take status to the grave with you!</p>

<p>Here are a few dimensions to consider, when joining, or re-joining a company:</p>

<ul>
  <li>
    <p><strong>What’s the purpose of the company?</strong> is it about solving a burning problem, or an insurmountable challenge (good), or is it about the greatness/legacy of the owner, or a advancing a particular favored method/technique (bad), is there any clarity at all? is any of this backed by measurable actions (very important)</p>
  </li>
  <li>
    <p><strong>What happens to the people who leave the place?</strong> Do they become more or less successful? Do super-stars ever return? Actions speak louder than words, people may simply be polite and avoiding speaking their mind… choosing to instead speak with their exit. Can you reach leavers an LinkedIn and get them on the phone to get the “real” exit interview?</p>
  </li>
  <li>
    <p><strong>What’s the money like?</strong> This one is kind of U-shaped: on one end you have grift-startups with charismatic founders, on the other places with no soul that pay top-$$$. For the grifting startups, they’re unable to really raise enough money, so they descend into sweat-shop mode – paying the staff the bare minimum and going the cult path. On the other hand, you have ultra-high paying places that solve problems by just paying you more to shut up. Some people need the money, because circumstances and reasons. In the middle, you should get places that are the best: they pay a lot but not insane. Discount startup stock completely: you’re already 100% exposed and it’ll be years before you see stock option profit, if at all, and you can always get more of it later down the road.</p>
  </li>
  <li>
    <p><strong>What’s the competition like?</strong> This one is U-shaped again. Too much competition is clearly bad, but no competition at all may mean that it’s too soon, or simply a hermit-like organization.</p>
  </li>
</ul>

<p>And the most important one of all</p>

<ul>
  <li><strong>What’s the progress for the money and time spent?</strong> What’s the bang for your buck? Given an estimate spend of an organization, and they years they’ve been around, what have they actually accomplished? Was it something truly relevant to their claimed mission, or an improvement (however ingenious) on the periphery? Under-performing on this dimension is an indicator of real problems, especially when backed by enormous resources: things are probably too screwed up internally to make any real progress. Could be leadership, could be team, could be everything. Typically this is not a failure mode available to startups, unless they go zombie mode.</li>
</ul>

<p>This is probably not exhaustive but it feels good enough!</p>]]></content><author><name></name></author><category term="companies" /><category term="startups" /><summary type="html"><![CDATA[Update (2025-10): This post didn’t age that well!]]></summary></entry><entry><title type="html">Christmas Special</title><link href="/puzzles/python/2022/12/25/christmas-special-post.html" rel="alternate" type="text/html" title="Christmas Special" /><published>2022-12-25T23:00:00+00:00</published><updated>2022-12-25T23:00:00+00:00</updated><id>/puzzles/python/2022/12/25/christmas-special/post</id><content type="html" xml:base="/puzzles/python/2022/12/25/christmas-special-post.html"><![CDATA[<p>Among the various group chats, the family group chat on whatsapp is king. This Christmas, a little puzzle has surfaced on the chat, courtesy of Flavio. What this says about our family chats, is another matter..</p>

<figure class="image">
    <img src="/docs/images/posts/2022-12-26-christmas-special/post.jpeg" alt="Here is the puzzle" />
    <figcaption>Here is the puzzle</figcaption>
</figure>

<p>It’s as simple linear set of equations, and you’ve just been nerd-sniped so good luck solving it!</p>

<p>While simple, the problem inspired me to write a tiny Python solver – it’s brute force and assumes each of the variables is an int bound between 0..10</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">solver</span><span class="p">():</span>
    <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">11</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">11</span><span class="p">):</span>
            <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">11</span><span class="p">):</span>
                <span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">11</span><span class="p">):</span>
                    <span class="n">check1</span> <span class="o">=</span> <span class="n">c</span> <span class="o">+</span> <span class="n">r</span> <span class="o">+</span> <span class="n">t</span> <span class="o">==</span> <span class="mi">4</span><span class="o">*</span><span class="n">w</span>
                    <span class="n">check2</span> <span class="o">=</span> <span class="n">c</span> <span class="o">+</span> <span class="n">r</span> <span class="o">==</span> <span class="n">t</span>
                    <span class="n">check3</span> <span class="o">=</span> <span class="n">c</span> <span class="o">+</span> <span class="n">t</span> <span class="o">==</span> <span class="mi">8</span> <span class="o">+</span> <span class="n">r</span>
                    <span class="n">check4</span> <span class="o">=</span> <span class="n">t</span> <span class="o">==</span> <span class="mi">3</span><span class="o">*</span><span class="n">r</span>

                    <span class="k">if</span> <span class="n">check1</span> <span class="ow">and</span> <span class="n">check2</span> <span class="ow">and</span> <span class="n">check3</span> <span class="ow">and</span> <span class="n">check4</span><span class="p">:</span>
                        <span class="k">print</span><span class="p">(</span><span class="nb">dict</span><span class="p">(</span><span class="n">c</span><span class="o">=</span><span class="n">c</span><span class="p">,</span> <span class="n">r</span><span class="o">=</span><span class="n">r</span><span class="p">,</span> <span class="n">t</span><span class="o">=</span><span class="n">t</span><span class="p">,</span> <span class="n">w</span><span class="o">=</span><span class="n">w</span><span class="p">))</span>
                        <span class="k">print</span><span class="p">(</span><span class="n">c</span> <span class="o">+</span> <span class="p">(</span><span class="n">r</span> <span class="o">*</span> <span class="n">t</span><span class="p">)</span> <span class="o">-</span> <span class="n">w</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">solver</span><span class="p">()</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="puzzles" /><category term="python" /><summary type="html"><![CDATA[Among the various group chats, the family group chat on whatsapp is king. This Christmas, a little puzzle has surfaced on the chat, courtesy of Flavio. What this says about our family chats, is another matter..]]></summary></entry><entry><title type="html">European Winter Schedule</title><link href="/work/routine/life-hack/2022/11/04/european-winter-schedule-post.html" rel="alternate" type="text/html" title="European Winter Schedule" /><published>2022-11-04T23:00:00+00:00</published><updated>2022-11-04T23:00:00+00:00</updated><id>/work/routine/life-hack/2022/11/04/european-winter-schedule/post</id><content type="html" xml:base="/work/routine/life-hack/2022/11/04/european-winter-schedule-post.html"><![CDATA[<p>It’s winter in Europe again. The place is more North than people think – to give you an intuition, Madrid is the same latitude as NYC. Most major European cities are significantly more North than Madrid, giving you as little as 4-5 hours of sunlight in the winter months.</p>

<p>Every year we repeat the idiotic ritual of switching our clocks back by one hour. While sleeping one hour longer is a genuinely pleasant prospect, the change is disruptive to your circadian clock. Yeah, the mornings are brighter but you’ll end up leaving your office at 5pm in complete darkness. Not ideal. The switch to winter time is generally unpopular with the European crowd, a fact that continues to be ignored by policymakers.</p>

<p>Here is a hack that I’ve tried for 2-3 years now, a system called “European Winter Schedule”. It’s designed to essentially ignore the time change and maximize your daylight leisure time. It’s a tweak around the 9-to-5 working time.</p>

<p>Start the day EARLY, with your first slot of work between 8am–noon. You’ll find less disruption for the first hour, while people rock up. You’ll get ahead with work, especially tasks that require focus.</p>

<p>Then comes then comes the best part: a JUMBO lunch break 12pm–2pm, maybe longer. Get lunch, get out, meet friends, get some sun, go for a run, do groceries. Do whatever, as long as you’ll get some of that daylight it’ll be worth it.</p>

<figure class="image">
    <img src="/docs/images/posts/2022-11-05-european-winter-schedule/post.png" alt="Here is my schedule" />
    <figcaption>Here is my schedule</figcaption>
</figure>

<p>After lunch, back in the trenches for another 4 hours: 2pm–6pm. It’s like a reset after taking a large break and will go so fast. Chances are you’ll be seen as more hard-working: people pay attention to silly things like when people leave, so despite chilling more, you’ll get your boss to love you more.</p>

<p>European Winter Schedule is my go-to move for the winter months. Admittedly it works best without kids, and with a job that allows you to go remote. It also works better outside the US where you’re expected to grid your life away.</p>]]></content><author><name></name></author><category term="work" /><category term="routine" /><category term="life-hack" /><summary type="html"><![CDATA[It’s winter in Europe again. The place is more North than people think – to give you an intuition, Madrid is the same latitude as NYC. Most major European cities are significantly more North than Madrid, giving you as little as 4-5 hours of sunlight in the winter months.]]></summary></entry><entry><title type="html">In Support of Tests Coverage</title><link href="/testing/unit/test/coverage/2021/07/03/test-coverage-evangelist-post.html" rel="alternate" type="text/html" title="In Support of Tests Coverage" /><published>2021-07-03T23:00:00+00:00</published><updated>2021-07-03T23:00:00+00:00</updated><id>/testing/unit/test/coverage/2021/07/03/test-coverage-evangelist/post</id><content type="html" xml:base="/testing/unit/test/coverage/2021/07/03/test-coverage-evangelist-post.html"><![CDATA[<blockquote>
  <p><strong>Update (2024-06):</strong><br />
Internet has this funny property of making your beliefs/opinions expressed at one time immortal and seem like held forever. And this rather short post continues to haunt me: two separate candidates have mentioned it (concerned). Presumably candidates think that I will put the gun to their head and ask for 100% test coverage. I won’t, I promise… Not over coverage anyway… 
100% coverage was definitely the answer at one point in Labstep’s history but the team that inspired that idea departed from it after I moved on. 100% coverage is definitely not appropriate for fast-exploration code-bases (ML Research, SAAS looking for product-market fit). However, I’d still argue that it’s a good idea for any maturing code – I’ve seen too many code-bases become unmaintainable because of “pragmatic” decisions to maybe not test some code because it’s “hard”. What that general vibe is driven by is another question… 
More introspectively this post remains a clear example of confirmation bias: I held a belief that high test coverage is a good thing, and found some random interview that supported the notion.</p>
</blockquote>

<p><strong>Original post</strong></p>

<p>This weekend I stumbled upon a remarkable conversation:</p>

<blockquote>
  <p>Richard: […] It’s pretty easy to get up to 90 or 95% test coverage. Getting that last 5% is really, really hard and it took about a year for me to get there, but once we got to that point, we stopped 
getting bug reports from Android.
Adam: Oh, wow.
Richard: Yeah. IT just worked from there on out. It made a huge, huge difference. We just didn’t really have any bugs for the next eight or nine years.</p>
</blockquote>

<p>This is from a <a href="https://corecursive.com/066-sqlite-with-richard-hipp/">conversation with Richard Hipp</a> – the creator of SQLite.</p>

<p>You know what else happened this week? An insane heat wave in the North East of the US (typically an area with “nice” weather). “Jan, you lunatic, what does testing have in common with climate change?”. Bare with me!</p>

<table>
  <thead>
    <tr>
      <th>Cause</th>
      <th>Effect</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Global warming</td>
      <td>Portland heat wave</td>
    </tr>
    <tr>
      <td>Insufficient testing</td>
      <td>Bugs never stop</td>
    </tr>
  </tbody>
</table>

<p>Figuring out causality is hard. Really really hard. Even when we really know the mechanism, it’s just baffling. What’s the cause of this extreme heat wave? It’s caused by global warming. What’s the reason for endless bug reports on my project? It’s insufficient testing.</p>

<p>Once you see it, it won’t stop amusing you. People claiming that anything below 100% test coverage is “practical”, “good”, “pragmatic”. That unit testing to the extreme is dogmatic. Or just complete nonsense like <a href="https://blog.bitgloss.ro/2020/10/stop-mocking-your-system/">this</a>. Meanwhile the bug reports keep coming, and seemingly never stop. Why would that be?</p>

<p>If you understand the link between climate change and extreme weather events, please consider extending your thinking to other parts of life. Bugs don’t come from nowhere, tests prevent them from happening.</p>]]></content><author><name></name></author><category term="testing" /><category term="unit" /><category term="test" /><category term="coverage" /><summary type="html"><![CDATA[Update (2024-06): Internet has this funny property of making your beliefs/opinions expressed at one time immortal and seem like held forever. And this rather short post continues to haunt me: two separate candidates have mentioned it (concerned). Presumably candidates think that I will put the gun to their head and ask for 100% test coverage. I won’t, I promise… Not over coverage anyway… 100% coverage was definitely the answer at one point in Labstep’s history but the team that inspired that idea departed from it after I moved on. 100% coverage is definitely not appropriate for fast-exploration code-bases (ML Research, SAAS looking for product-market fit). However, I’d still argue that it’s a good idea for any maturing code – I’ve seen too many code-bases become unmaintainable because of “pragmatic” decisions to maybe not test some code because it’s “hard”. What that general vibe is driven by is another question… More introspectively this post remains a clear example of confirmation bias: I held a belief that high test coverage is a good thing, and found some random interview that supported the notion.]]></summary></entry><entry><title type="html">Lessons learned refactoring Pulumi programs</title><link href="/pulumi/aws/2020/11/07/deploy-create-react-app-aws-pulumi-post4.html" rel="alternate" type="text/html" title="Lessons learned refactoring Pulumi programs" /><published>2020-11-07T23:00:00+00:00</published><updated>2020-11-07T23:00:00+00:00</updated><id>/pulumi/aws/2020/11/07/deploy-create-react-app-aws-pulumi/post4</id><content type="html" xml:base="/pulumi/aws/2020/11/07/deploy-create-react-app-aws-pulumi-post4.html"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>So you built your first Pulumi program? Maybe even got some workloads running in production?
The team loves it, the thing just works. 
People are adopting it, adding components, importing more of the existing resources. 
The stacks are getting bigger… and bigger.</p>

<p>Overnight, what was a simple proof of concept before turns into a 200+ lines of infrastructure code. 
You look to the community for some best practices: how to structure this thing, how to break it up?
How to add some unit tests maybe?</p>

<p>In this article, let’s refactor a Pulumi program and break it down into more re-usable components. 
The components will be more maintainable and <em>bingo</em> testable. 
Because there is a bunch of changes to make, we’ll make so incrementally.</p>

<p>This is a problem that many people bump into and most people just want/need guidance and simple rules of thumb to structure their code. 
What should be a really interesting and valuable proposition (“unit testing you infrastructure code”) becomes hard to achieve.</p>

<h4 id="other-posts-in-this-series">Other posts in this series</h4>

<p><a href="/pulumi/aws/react/2020/07/29/deploy-create-react-app-aws-pulumi-post1.html">Part 1</a> – <a href="/pulumi/aws/react/2020/08/29/deploy-create-react-app-aws-pulumi-post2.html">Part 2</a> – <a href="/pulumi/aws/react/2020/10/29/deploy-create-react-app-aws-pulumi-post3.html">Part 3</a></p>

<h1 id="getting-a-lay-of-the-land">Getting a “lay of the land”</h1>

<p>Over the last three posts in this series, we worked on a simple static web app (React). 
We went from a simple S3 bucket architecture, to a solution with a CDN, a domain record and a proper SSL certificate. 
But as time went on the <code class="language-plaintext highlighter-rouge">index.ts</code> became bigger and bigger. 
This is what it looks like now.</p>

<p><img src="/docs/images/posts/2020-07-30-deploy-create-react-app-aws-pulumi/carbon-index-post3.png" alt="Before" /></p>

<p>Well this is an unholy messy! Let’s see if we can get from the mess above to neat and tidy the <code class="language-plaintext highlighter-rouge">index.ts</code> below.</p>

<p><img src="/docs/images/posts/2020-07-30-deploy-create-react-app-aws-pulumi/carbon-index-post4.png" alt="And after!" /></p>

<p>This truly sparks Marie Kondo-levels of joy!
But how do we make it happen?</p>

<h1 id="refactoring-with-janski">Refactoring with Janski</h1>

<h2 id="figuring-out-how-to-split-things-out">Figuring out how to split things out</h2>

<p>Let’s have a look at the monster <code class="language-plaintext highlighter-rouge">index.ts</code> and see which things we might split off. Color-coding to help the eye.</p>

<p><img src="/docs/images/posts/2020-07-30-deploy-create-react-app-aws-pulumi/carbon-index-post3-annotated.png" alt="And after!" /></p>

<p>We can already see three potential improvements:</p>

<ul>
  <li>stuff related to the Content Delivery Network (S3 bucket, the CDN itself, and the domain record)</li>
  <li>stuff related to the SSL certificate (could come useful, just as a generic “hey get me an SSL for blah”)</li>
  <li>a utility function <code class="language-plaintext highlighter-rouge">getDomainAndSubdomain</code> and a constant <code class="language-plaintext highlighter-rouge">tenMinutes</code></li>
</ul>

<p>Beyond this, the domain name is repeated over and over – just a copy pasted string. 
That string constant can be easily factored out as a Pulumi config. 
So let’s get started in reverse order!.</p>

<h2 id="avoiding-domain-name-repetition">Avoiding domain name repetition</h2>

<p>You probably noticed the domain name <code class="language-plaintext highlighter-rouge">my-app.somedomain.com</code> being endlessly copy&amp;pasted in the code. 
This is obviously not good practice but it was simple enough to get started. 
It can be simply refactored by using the Pulumi config component.</p>

<p>In your terminal simply define a new config value</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pulumi config <span class="nb">set </span>targetDomain my-app.somedomain.com
</code></pre></div></div>

<p>This should now update the <code class="language-plaintext highlighter-rouge">Pulumi.dev.yaml</code> to look something like this</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">encryptionsalt</span><span class="pi">:</span> <span class="s">&lt;SOME RANDOM SALT&gt;</span>
<span class="na">config</span><span class="pi">:</span>
  <span class="s">aws:region: eu-west-1</span>
  <span class="s">my-app:targetDomain: my-app.somedomain.com</span>
</code></pre></div></div>

<p>Now the only thing that remains is to make our Pulumi program <code class="language-plaintext highlighter-rouge">index.ts</code> aware of this code.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">stackConfig</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">pulumi</span><span class="p">.</span><span class="nx">Config</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-app</span><span class="dl">"</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">config</span> <span class="o">=</span> <span class="p">{</span>
    <span class="c1">// targetDomain is the domain/host to serve content at.</span>
    <span class="na">targetDomain</span><span class="p">:</span> <span class="nx">stackConfig</span><span class="p">.</span><span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">targetDomain</span><span class="dl">"</span><span class="p">),</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now the <code class="language-plaintext highlighter-rouge">config.targetDomain</code> can be used to configure the various resources in your program.</p>

<h2 id="pulling-out-a-simple-utils-function">Pulling out a simple utils function</h2>

<p>The <code class="language-plaintext highlighter-rouge">index.ts</code> starts with a simple utility function, this is the simplest refactor of all – the function should just go into a separate <code class="language-plaintext highlighter-rouge">util.ts</code> file. 
While we’re at it, let’s also put the <code class="language-plaintext highlighter-rouge">tenMinutes</code> constant in there.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/utils.ts</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">aws</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/aws</span><span class="dl">"</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">tenMinutes</span> <span class="o">=</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">10</span><span class="p">;</span>

<span class="c1">// Split a domain name into its subdomain and parent domain names.</span>
<span class="c1">// e.g. "www.example.com" =&gt; "www", "example.com".</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">getDomainAndSubdomain</span><span class="p">(</span>
  <span class="nx">domain</span><span class="p">:</span> <span class="kr">string</span>
<span class="p">):</span> <span class="p">{</span> <span class="nl">subdomain</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">parentDomain</span><span class="p">:</span> <span class="kr">string</span> <span class="p">}</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">parts</span> <span class="o">=</span> <span class="nx">domain</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">"</span><span class="s2">.</span><span class="dl">"</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">parts</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`No TLD found on </span><span class="p">${</span><span class="nx">domain</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="c1">// No subdomain, e.g. awesome-website.com.</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">parts</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span> <span class="na">subdomain</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span> <span class="na">parentDomain</span><span class="p">:</span> <span class="nx">domain</span> <span class="p">};</span>
  <span class="p">}</span>

  <span class="kd">const</span> <span class="nx">subdomain</span> <span class="o">=</span> <span class="nx">parts</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
  <span class="nx">parts</span><span class="p">.</span><span class="nx">shift</span><span class="p">();</span> <span class="c1">// Drop first element.</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="nx">subdomain</span><span class="p">,</span>
    <span class="c1">// Trailing "." to canonicalize domain.</span>
    <span class="na">parentDomain</span><span class="p">:</span> <span class="nx">parts</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2">.</span><span class="dl">"</span><span class="p">)</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">.</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="refactoring-the-ssl-certificate-code">Refactoring the SSL certificate code</h2>

<p>The interesting stuff starts now. In the <code class="language-plaintext highlighter-rouge">index.ts</code>, the code looks like this.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c1">// Per AWS, ACM certificate must be in the us-east-1 region.</span>
  <span class="kd">const</span> <span class="nx">eastRegion</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">Provider</span><span class="p">(</span><span class="dl">"</span><span class="s2">east</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">profile</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">profile</span><span class="p">,</span>
    <span class="na">region</span><span class="p">:</span> <span class="dl">"</span><span class="s2">us-east-1</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">});</span>

  <span class="kd">const</span> <span class="nx">certificate</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">acm</span><span class="p">.</span><span class="nx">Certificate</span><span class="p">(</span>
    <span class="dl">"</span><span class="s2">certificate</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">{</span>
      <span class="na">domainName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">my-app.somedomain.com</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">validationMethod</span><span class="p">:</span> <span class="dl">"</span><span class="s2">DNS</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
    <span class="p">{</span> <span class="na">provider</span><span class="p">:</span> <span class="nx">eastRegion</span> <span class="p">}</span>
  <span class="p">);</span>

  <span class="cm">/**
   *  Create a DNS record to prove that we _own_ the domain we're requesting a certificate for.
   *  See https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html for more info.
   */</span>
  <span class="kd">const</span> <span class="nx">certificateValidationDomain</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">route53</span><span class="p">.</span><span class="nb">Record</span><span class="p">(</span>
    <span class="dl">"</span><span class="s2">my-app.somedomain.com-validation</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="nx">certificate</span><span class="p">.</span><span class="nx">domainValidationOptions</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">resourceRecordName</span><span class="p">,</span>
      <span class="na">zoneId</span><span class="p">:</span> <span class="nx">hostedZoneId</span><span class="p">,</span>
      <span class="na">type</span><span class="p">:</span> <span class="nx">certificate</span><span class="p">.</span><span class="nx">domainValidationOptions</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">resourceRecordType</span><span class="p">,</span>
      <span class="na">records</span><span class="p">:</span> <span class="p">[</span><span class="nx">certificate</span><span class="p">.</span><span class="nx">domainValidationOptions</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">resourceRecordValue</span><span class="p">],</span>
      <span class="na">ttl</span><span class="p">:</span> <span class="nx">tenMinutes</span><span class="p">,</span>
    <span class="p">}</span>
  <span class="p">);</span>

  <span class="cm">/**
   * This is a _special_ resource that waits for ACM to complete validation via the DNS record
   * checking for a status of "ISSUED" on the certificate itself. No actual resources are
   * created (or updated or deleted).
   *
   * See https://www.terraform.io/docs/providers/aws/r/acm_certificate_validation.html for slightly more detail
   * and https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/resource_aws_acm_certificate_validation.go
   * for the actual implementation.
   */</span>
  <span class="kd">const</span> <span class="nx">certificateValidation</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">acm</span><span class="p">.</span><span class="nx">CertificateValidation</span><span class="p">(</span>
    <span class="dl">"</span><span class="s2">certificateValidation</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">{</span>
      <span class="na">certificateArn</span><span class="p">:</span> <span class="nx">certificate</span><span class="p">.</span><span class="nx">arn</span><span class="p">,</span>
      <span class="na">validationRecordFqdns</span><span class="p">:</span> <span class="p">[</span><span class="nx">certificateValidationDomain</span><span class="p">.</span><span class="nx">fqdn</span><span class="p">],</span>
    <span class="p">},</span>
    <span class="p">{</span> <span class="na">provider</span><span class="p">:</span> <span class="nx">eastRegion</span> <span class="p">}</span>
  <span class="p">);</span>
</code></pre></div></div>

<p>What’s a good way to refactor this? Let’s refactor this into a “component resource”. 
Looking top-down, how could we use this component from <code class="language-plaintext highlighter-rouge">index.ts</code>?</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">MyCertificate</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./src/my-certificate</span><span class="dl">"</span><span class="p">;</span>

<span class="p">...</span>

<span class="kd">function</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">stackConfig</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">pulumi</span><span class="p">.</span><span class="nx">Config</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-app</span><span class="dl">"</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">config</span> <span class="o">=</span> <span class="p">{</span>
    <span class="c1">// targetDomain is the domain/host to serve content at.</span>
    <span class="na">targetDomain</span><span class="p">:</span> <span class="nx">stackConfig</span><span class="p">.</span><span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">targetDomain</span><span class="dl">"</span><span class="p">),</span>
  <span class="p">};</span>

  <span class="kd">const</span> <span class="nx">certificate</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MyCertificate</span><span class="p">(</span><span class="dl">'</span><span class="s1">my-certificate</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">targetDomain</span><span class="p">:</span> <span class="nx">config</span><span class="p">.</span><span class="nx">targetDomain</span><span class="p">,</span>
  <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Wouldn’t that be neat, huh? 
A reusable way to generate an SSL certificate for any domain. 
Well, here is how you do that. 
All the same components appear again. 
The special sauce here is <code class="language-plaintext highlighter-rouge">{ parent: this }</code> which attaches all the resources to the component resource.</p>

<blockquote>
  <p>Curiously, it can be used to nest multiple components inside another, more than one level deep, when that’s needed.</p>
</blockquote>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Created a new file in src/my-certificate/index.ts</span>

<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">pulumi</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/pulumi</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">aws</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/aws</span><span class="dl">"</span><span class="p">;</span>

<span class="k">import</span> <span class="p">{</span> <span class="nx">getDomainAndSubdomain</span><span class="p">,</span> <span class="nx">tenMinutes</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">../utils</span><span class="dl">"</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nx">MyCertificate</span> <span class="kd">extends</span> <span class="nx">pulumi</span><span class="p">.</span><span class="nx">ComponentResource</span> <span class="p">{</span>
  <span class="nl">certificate</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">acm</span><span class="p">.</span><span class="nx">Certificate</span><span class="p">;</span>
  <span class="nl">certificateValidation</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">acm</span><span class="p">.</span><span class="nx">CertificateValidation</span><span class="p">;</span>

  <span class="kd">constructor</span><span class="p">(</span>
    <span class="nx">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span>
    <span class="nx">args</span><span class="p">:</span> <span class="p">{</span>
      <span class="nl">targetDomain</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
    <span class="p">},</span>
    <span class="nx">opts</span><span class="p">:</span> <span class="kr">any</span> <span class="o">=</span> <span class="p">{}</span>
  <span class="p">)</span> <span class="p">{</span>
    <span class="k">super</span><span class="p">(</span><span class="dl">"</span><span class="s2">pkg:index:Certificate</span><span class="dl">"</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="p">{},</span> <span class="nx">opts</span><span class="p">);</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">targetDomain</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">args</span><span class="p">;</span>

    <span class="c1">// Per AWS, ACM certificate must be in the us-east-1 region.</span>
    <span class="kd">const</span> <span class="nx">eastRegion</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">Provider</span><span class="p">(</span>
      <span class="dl">"</span><span class="s2">east</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">{</span>
        <span class="na">profile</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">profile</span><span class="p">,</span>
        <span class="na">region</span><span class="p">:</span> <span class="dl">"</span><span class="s2">us-east-1</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">},</span>
      <span class="p">{</span> <span class="na">parent</span><span class="p">:</span> <span class="k">this</span> <span class="p">}</span>
    <span class="p">);</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">certificate</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">acm</span><span class="p">.</span><span class="nx">Certificate</span><span class="p">(</span>
      <span class="dl">"</span><span class="s2">certificate</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">{</span>
        <span class="na">domainName</span><span class="p">:</span> <span class="nx">targetDomain</span><span class="p">,</span>
        <span class="na">validationMethod</span><span class="p">:</span> <span class="dl">"</span><span class="s2">DNS</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">},</span>
      <span class="p">{</span> <span class="na">provider</span><span class="p">:</span> <span class="nx">eastRegion</span><span class="p">,</span> <span class="na">parent</span><span class="p">:</span> <span class="k">this</span> <span class="p">}</span>
    <span class="p">);</span>

    <span class="kd">const</span> <span class="nx">domainParts</span> <span class="o">=</span> <span class="nx">getDomainAndSubdomain</span><span class="p">(</span><span class="nx">targetDomain</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">hostedZoneId</span> <span class="o">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">route53</span>
      <span class="p">.</span><span class="nx">getZone</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="nx">domainParts</span><span class="p">.</span><span class="nx">parentDomain</span> <span class="p">},</span> <span class="p">{</span> <span class="na">async</span><span class="p">:</span> <span class="kc">true</span> <span class="p">})</span>
      <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">zone</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">zone</span><span class="p">.</span><span class="nx">zoneId</span><span class="p">);</span>

    <span class="cm">/**
     *  Create a DNS record to prove that we _own_ the domain we're requesting a certificate for.
     *  See https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html for more info.
     */</span>
    <span class="kd">const</span> <span class="nx">certificateValidationDomain</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">route53</span><span class="p">.</span><span class="nb">Record</span><span class="p">(</span>
      <span class="s2">`</span><span class="p">${</span><span class="nx">targetDomain</span><span class="p">}</span><span class="s2">-validation`</span><span class="p">,</span>
      <span class="p">{</span>
        <span class="na">name</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">certificate</span><span class="p">.</span><span class="nx">domainValidationOptions</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">resourceRecordName</span><span class="p">,</span>
        <span class="na">zoneId</span><span class="p">:</span> <span class="nx">hostedZoneId</span><span class="p">,</span>
        <span class="na">type</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">certificate</span><span class="p">.</span><span class="nx">domainValidationOptions</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">resourceRecordType</span><span class="p">,</span>
        <span class="na">records</span><span class="p">:</span> <span class="p">[</span><span class="k">this</span><span class="p">.</span><span class="nx">certificate</span><span class="p">.</span><span class="nx">domainValidationOptions</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">resourceRecordValue</span><span class="p">],</span>
        <span class="na">ttl</span><span class="p">:</span> <span class="nx">tenMinutes</span><span class="p">,</span>
      <span class="p">},</span>
      <span class="p">{</span>
        <span class="na">parent</span><span class="p">:</span> <span class="k">this</span><span class="p">,</span>
      <span class="p">}</span>
    <span class="p">);</span>

    <span class="cm">/**
     * This is a _special_ resource that waits for ACM to complete validation via the DNS record
     * checking for a status of "ISSUED" on the certificate itself. No actual resources are
     * created (or updated or deleted).
     *
     * See https://www.terraform.io/docs/providers/aws/r/acm_certificate_validation.html for slightly more detail
     * and https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/resource_aws_acm_certificate_validation.go
     * for the actual implementation.
     */</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">certificateValidation</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">acm</span><span class="p">.</span><span class="nx">CertificateValidation</span><span class="p">(</span>
      <span class="dl">"</span><span class="s2">certificateValidation</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">{</span>
        <span class="na">certificateArn</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">certificate</span><span class="p">.</span><span class="nx">arn</span><span class="p">,</span>
        <span class="na">validationRecordFqdns</span><span class="p">:</span> <span class="p">[</span><span class="nx">certificateValidationDomain</span><span class="p">.</span><span class="nx">fqdn</span><span class="p">],</span>
      <span class="p">},</span>
      <span class="p">{</span> <span class="na">provider</span><span class="p">:</span> <span class="nx">eastRegion</span><span class="p">,</span> <span class="na">parent</span><span class="p">:</span> <span class="k">this</span> <span class="p">}</span>
    <span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="refactoring-the-cdn-component">Refactoring the CDN component</h2>

<p>Much like the certificate, the CDN definition is rather verbose. 
Especially the <code class="language-plaintext highlighter-rouge">DistributionArgs</code> takes like 70 lines of code :yikes:!</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">const</span> <span class="nx">distributionArgs</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">cloudfront</span><span class="p">.</span><span class="nx">DistributionArgs</span> <span class="o">=</span> <span class="p">{</span>
    <span class="p">...</span> <span class="c1">// a massive JS object</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>Let’s start by putting defining a function that creates <code class="language-plaintext highlighter-rouge">DistributionArgs</code> inside a new file.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Created a new file in src/my-app/index.ts</span>

<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">pulumi</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/pulumi</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">aws</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/aws</span><span class="dl">"</span><span class="p">;</span>

<span class="k">import</span> <span class="p">{</span> <span class="nx">tenMinutes</span><span class="p">,</span> <span class="nx">getDomainAndSubdomain</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">../utils</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">createDistributionArgs</span><span class="p">(</span>
  <span class="nx">targetDomain</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span>
  <span class="nx">bucket</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">s3</span><span class="p">.</span><span class="nx">Bucket</span><span class="p">,</span>
  <span class="nx">certificateValidation</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">acm</span><span class="p">.</span><span class="nx">CertificateValidation</span><span class="p">,</span>
  <span class="nx">tenMinutes</span><span class="p">:</span> <span class="kr">number</span>
<span class="p">):</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">cloudfront</span><span class="p">.</span><span class="nx">DistributionArgs</span> <span class="p">{</span>
  <span class="c1">// distributionArgs configures the CloudFront distribution. Relevant documentation:</span>
  <span class="c1">// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html</span>
  <span class="c1">// https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="na">enabled</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="c1">// Alternate aliases the CloudFront distribution can be reached at, in addition to https://xxxx.cloudfront.net.</span>
    <span class="c1">// Required if you want to access the distribution via config.targetDomain as well.</span>
    <span class="na">aliases</span><span class="p">:</span> <span class="p">[</span><span class="nx">targetDomain</span><span class="p">],</span>

    <span class="c1">// We only specify one origin for this distribution, the S3 content bucket.</span>
    <span class="na">origins</span><span class="p">:</span> <span class="p">[</span>
      <span class="p">{</span>
        <span class="na">originId</span><span class="p">:</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">arn</span><span class="p">,</span>
        <span class="na">domainName</span><span class="p">:</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">websiteEndpoint</span><span class="p">,</span>
        <span class="na">customOriginConfig</span><span class="p">:</span> <span class="p">{</span>
          <span class="c1">// Amazon S3 doesn't support HTTPS connections when using an S3 bucket configured as a website endpoint.</span>
          <span class="c1">// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesOriginProtocolPolicy</span>
          <span class="na">originProtocolPolicy</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http-only</span><span class="dl">"</span><span class="p">,</span>
          <span class="na">httpPort</span><span class="p">:</span> <span class="mi">80</span><span class="p">,</span>
          <span class="na">httpsPort</span><span class="p">:</span> <span class="mi">443</span><span class="p">,</span>
          <span class="na">originSslProtocols</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">TLSv1.2</span><span class="dl">"</span><span class="p">],</span>
        <span class="p">},</span>
      <span class="p">},</span>
    <span class="p">],</span>

    <span class="na">defaultRootObject</span><span class="p">:</span> <span class="dl">"</span><span class="s2">index.html</span><span class="dl">"</span><span class="p">,</span>

    <span class="c1">// A CloudFront distribution can configure different cache behaviors based on the request path.</span>
    <span class="c1">// Here we just specify a single, default cache behavior which is just read-only requests to S3.</span>
    <span class="na">defaultCacheBehavior</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">targetOriginId</span><span class="p">:</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">arn</span><span class="p">,</span>

      <span class="na">viewerProtocolPolicy</span><span class="p">:</span> <span class="dl">"</span><span class="s2">redirect-to-https</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">allowedMethods</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">GET</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">HEAD</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">OPTIONS</span><span class="dl">"</span><span class="p">],</span>
      <span class="na">cachedMethods</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">GET</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">HEAD</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">OPTIONS</span><span class="dl">"</span><span class="p">],</span>

      <span class="na">forwardedValues</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">cookies</span><span class="p">:</span> <span class="p">{</span> <span class="na">forward</span><span class="p">:</span> <span class="dl">"</span><span class="s2">none</span><span class="dl">"</span> <span class="p">},</span>
        <span class="na">queryString</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
      <span class="p">},</span>

      <span class="na">minTtl</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
      <span class="na">defaultTtl</span><span class="p">:</span> <span class="nx">tenMinutes</span><span class="p">,</span>
      <span class="na">maxTtl</span><span class="p">:</span> <span class="nx">tenMinutes</span><span class="p">,</span>
    <span class="p">},</span>

    <span class="c1">// "All" is the most broad distribution, and also the most expensive.</span>
    <span class="c1">// "100" is the least broad, and also the least expensive.</span>
    <span class="na">priceClass</span><span class="p">:</span> <span class="dl">"</span><span class="s2">PriceClass_100</span><span class="dl">"</span><span class="p">,</span>

    <span class="c1">// You can customize error responses. When CloudFront receives an error from the origin (e.g. S3 or some other</span>
    <span class="c1">// web service) it can return a different error code, and return the response for a different resource.</span>
    <span class="na">customErrorResponses</span><span class="p">:</span> <span class="p">[</span>
      <span class="p">{</span> <span class="na">errorCode</span><span class="p">:</span> <span class="mi">404</span><span class="p">,</span> <span class="na">responseCode</span><span class="p">:</span> <span class="mi">404</span><span class="p">,</span> <span class="na">responsePagePath</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/404.html</span><span class="dl">"</span> <span class="p">},</span>
    <span class="p">],</span>

    <span class="na">restrictions</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">geoRestriction</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">restrictionType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">none</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">},</span>
    <span class="p">},</span>

    <span class="na">viewerCertificate</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">acmCertificateArn</span><span class="p">:</span> <span class="nx">certificateValidation</span><span class="p">.</span><span class="nx">certificateArn</span><span class="p">,</span> <span class="c1">// Per AWS, ACM certificate must be in the us-east-1 region.</span>
      <span class="na">sslSupportMethod</span><span class="p">:</span> <span class="dl">"</span><span class="s2">sni-only</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Pulling out this verbose chunk, we can define a new component resource in the same file. 
Let’s go ahead and update <code class="language-plaintext highlighter-rouge">src/my-app/index.ts</code>.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Editing in src/my-app/index.ts</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">MyApp</span> <span class="kd">extends</span> <span class="nx">pulumi</span><span class="p">.</span><span class="nx">ComponentResource</span> <span class="p">{</span>
  <span class="nl">bucket</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">s3</span><span class="p">.</span><span class="nx">Bucket</span><span class="p">;</span>
  <span class="nl">cdn</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">cloudfront</span><span class="p">.</span><span class="nx">Distribution</span><span class="p">;</span>
  <span class="nl">record</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">route53</span><span class="p">.</span><span class="nb">Record</span><span class="p">;</span>

  <span class="kd">constructor</span><span class="p">(</span>
    <span class="nx">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span>
    <span class="nx">args</span><span class="p">:</span> <span class="p">{</span>
      <span class="nl">targetDomain</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
      <span class="nl">certificateValidation</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">acm</span><span class="p">.</span><span class="nx">CertificateValidation</span><span class="p">;</span>
    <span class="p">},</span>
    <span class="nx">opts</span><span class="p">:</span> <span class="kr">any</span> <span class="o">=</span> <span class="p">{}</span>
  <span class="p">)</span> <span class="p">{</span>
    <span class="k">super</span><span class="p">(</span><span class="dl">"</span><span class="s2">pkg:index:MyApp</span><span class="dl">"</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="p">{},</span> <span class="nx">opts</span><span class="p">);</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">targetDomain</span><span class="p">,</span> <span class="nx">certificateValidation</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">args</span><span class="p">;</span>
    <span class="c1">// Create an AWS resource (S3 Bucket)</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">bucket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">s3</span><span class="p">.</span><span class="nx">Bucket</span><span class="p">(</span>
      <span class="nx">targetDomain</span><span class="p">,</span>
      <span class="p">{</span>
        <span class="na">bucket</span><span class="p">:</span> <span class="nx">targetDomain</span><span class="p">,</span>
        <span class="na">acl</span><span class="p">:</span> <span class="dl">"</span><span class="s2">public-read</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">website</span><span class="p">:</span> <span class="p">{</span>
          <span class="na">indexDocument</span><span class="p">:</span> <span class="dl">"</span><span class="s2">index.html</span><span class="dl">"</span><span class="p">,</span>
        <span class="p">},</span>
      <span class="p">},</span>
      <span class="p">{</span> <span class="na">parent</span><span class="p">:</span> <span class="k">this</span> <span class="p">}</span>
    <span class="p">);</span>

    <span class="kd">const</span> <span class="nx">distributionArgs</span> <span class="o">=</span> <span class="nx">createDistributionArgs</span><span class="p">(</span>
      <span class="nx">targetDomain</span><span class="p">,</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">bucket</span><span class="p">,</span>
      <span class="nx">certificateValidation</span><span class="p">,</span>
      <span class="nx">tenMinutes</span>
    <span class="p">);</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">cdn</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">cloudfront</span><span class="p">.</span><span class="nx">Distribution</span><span class="p">(</span><span class="dl">"</span><span class="s2">cdn</span><span class="dl">"</span><span class="p">,</span> <span class="nx">distributionArgs</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">parent</span><span class="p">:</span> <span class="k">this</span><span class="p">,</span>
    <span class="p">});</span>

    <span class="kd">const</span> <span class="nx">domainParts</span> <span class="o">=</span> <span class="nx">getDomainAndSubdomain</span><span class="p">(</span><span class="nx">targetDomain</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">hostedZoneId</span> <span class="o">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">route53</span>
      <span class="p">.</span><span class="nx">getZone</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="nx">domainParts</span><span class="p">.</span><span class="nx">parentDomain</span> <span class="p">},</span> <span class="p">{</span> <span class="na">async</span><span class="p">:</span> <span class="kc">true</span> <span class="p">})</span>
      <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">zone</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">zone</span><span class="p">.</span><span class="nx">zoneId</span><span class="p">);</span>

    <span class="c1">// Create a Route53 A-record</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">record</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">route53</span><span class="p">.</span><span class="nb">Record</span><span class="p">(</span>
      <span class="dl">"</span><span class="s2">targetDomain</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">{</span>
        <span class="na">name</span><span class="p">:</span> <span class="nx">targetDomain</span><span class="p">,</span>
        <span class="na">zoneId</span><span class="p">:</span> <span class="nx">hostedZoneId</span><span class="p">,</span>
        <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">A</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">aliases</span><span class="p">:</span> <span class="p">[</span>
          <span class="p">{</span>
            <span class="na">name</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">cdn</span><span class="p">.</span><span class="nx">domainName</span><span class="p">,</span>
            <span class="na">zoneId</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">cdn</span><span class="p">.</span><span class="nx">hostedZoneId</span><span class="p">,</span>
            <span class="na">evaluateTargetHealth</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
          <span class="p">},</span>
        <span class="p">],</span>
      <span class="p">},</span>
      <span class="p">{</span> <span class="na">parent</span><span class="p">:</span> <span class="k">this</span> <span class="p">}</span>
    <span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And voila, another nice component resource to include in <code class="language-plaintext highlighter-rouge">index.ts</code>. Looking top-down here is how it can used?</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">MyApp</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./src/my-app</span><span class="dl">"</span><span class="p">;</span>

<span class="p">...</span>

<span class="kd">function</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">stackConfig</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">pulumi</span><span class="p">.</span><span class="nx">Config</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-app</span><span class="dl">"</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">config</span> <span class="o">=</span> <span class="p">{</span>
    <span class="c1">// targetDomain is the domain/host to serve content at.</span>
    <span class="na">targetDomain</span><span class="p">:</span> <span class="nx">stackConfig</span><span class="p">.</span><span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">targetDomain</span><span class="dl">"</span><span class="p">),</span>
  <span class="p">};</span>

  <span class="kd">const</span> <span class="nx">certificate</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MyCertificate</span><span class="p">(</span><span class="dl">'</span><span class="s1">my-certificate</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">targetDomain</span><span class="p">:</span> <span class="nx">config</span><span class="p">.</span><span class="nx">targetDomain</span><span class="p">,</span>
  <span class="p">});</span>

  <span class="kd">const</span> <span class="nx">myApp</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MyApp</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-app</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">targetDomain</span><span class="p">:</span> <span class="nx">config</span><span class="p">.</span><span class="nx">targetDomain</span><span class="p">,</span>
    <span class="na">certificateValidation</span><span class="p">:</span> <span class="nx">certificate</span><span class="p">.</span><span class="nx">certificateValidation</span><span class="p">,</span>
  <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Wrapping up here are some follow-up resources. 
I found nothing about best way to structure pulumi projects, so I just improvised here – this is what worked for me.</p>
<ul>
  <li>The topic of component resources is covered briefly in the <a href="https://www.pulumi.com/docs/intro/concepts/programming-model/#components">Programming Model</a> of Pulumi docs,</li>
  <li>There also appears to be a nice <a href="https://www.pulumi.com/docs/tutorials/aws/s3-folder-component/">official tutorial</a> with an S3 bucket example.</li>
</ul>

<h2 id="visual-improvement-in-project-structure">Visual improvement in project structure</h2>

<p>And you know what’s great? The structure of the stack resources is visibly improved. 
Initially the output of <code class="language-plaintext highlighter-rouge">pulumi refresh</code> looked something like this</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pulumi refresh <span class="nt">-y</span>
Previewing refresh <span class="o">(</span>dev<span class="o">)</span>:
     Type                              Name                               Plan     
     pulumi:pulumi:Stack               my-app-dev                                  
     ├─ pulumi:providers:aws           east                                        
     ├─ aws:acm:CertificateValidation  certificateValidation                       
     ├─ aws:acm:Certificate            certificate                                 
     ├─ aws:route53:Record             my-app.somedomain.com-validation           
     ├─ aws:s3:Bucket                  my-app.somedomain.com                      
     ├─ aws:route53:Record             targetDomain                                
     └─ aws:cloudfront:Distribution    cdn                                         
 
Resources:
    8 unchanged
</code></pre></div></div>

<p>All the resources are a flat list, without any visual linkage or grouping between them. 
However, after our little refactoring this now looks much cleaner, with resources being grouped logically.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pulumi refresh 
Previewing refresh <span class="o">(</span>dev<span class="o">)</span>:
     Type                                 Name                               Plan           
     pulumi:pulumi:Stack                  my-app-dev                         running...     
     ├─ pkg:index:Certificate             my-certificate                                    
     │  ├─ pulumi:providers:aws           east                                              
     │  ├─ aws:acm:CertificateValidation  certificateValidation                             
     │  ├─ aws:route53:Record             my-app.somedomain.com-validation                 
     │  └─ aws:acm:Certificate            certificate                                       
     └─ pkg:index:MyApp                   my-app                                            
        ├─ aws:route53:Record             targetDomain                                      
        ├─ aws:s3:Bucket                  my-app.somedomain.com             refreshing...  
        └─ aws:cloudfront:Distribution    cdn                                               
</code></pre></div></div>

<h1 id="unit-testing-resource-components">Unit testing resource components</h1>

<p>Some of you might be thinking “wait, what? unit testing infrastructure? I thought you could only big integration tests where infrastructure is spun up?”. 
Well, prepare your mind to be blown. 
Pulumi mocks cloud provider responses allowing you to mock that a fake piece of infrastructure is created. 
Relative to tests with <code class="language-plaintext highlighter-rouge">jest</code> that you might be familiar with, things are a bit more awkward here but still okay.</p>

<p>Let’s start by building a unit test for the MyCertificate component. 
What does this test need to include?
Well, my certificate internall creates at least two resources:</p>
<ul>
  <li>Certificate – <code class="language-plaintext highlighter-rouge">aws:acm/certificate:Certificate</code></li>
  <li>CertificateValidation – <code class="language-plaintext highlighter-rouge">aws:acm/certificateValidation:CertificateValidation</code></li>
</ul>

<p>So for both of these a mock is needed. 
Pulumi exposes an API for unit testing via <code class="language-plaintext highlighter-rouge">pulumi.runtime.setMocks</code> that allows for API responses to be mocked. 
Here is how you can start</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Create a new file in src/my-certificate/index.test.ts</span>

<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">pulumi</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/pulumi</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">assert</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">assert</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="dl">"</span><span class="s2">mocha</span><span class="dl">"</span><span class="p">;</span>

<span class="nx">pulumi</span><span class="p">.</span><span class="nx">runtime</span><span class="p">.</span><span class="nx">setMocks</span><span class="p">({</span>
  <span class="na">newResource</span><span class="p">:</span> <span class="kd">function</span> <span class="p">(</span>
    <span class="na">type</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span>
    <span class="na">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span>
    <span class="na">inputs</span><span class="p">:</span> <span class="kr">any</span>
  <span class="p">):</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">state</span><span class="p">:</span> <span class="kr">any</span> <span class="p">}</span> <span class="p">{</span>
    <span class="k">switch</span> <span class="p">(</span><span class="kd">type</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">case</span> <span class="dl">"</span><span class="s2">aws:acm/certificate:Certificate</span><span class="dl">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="p">{</span>
          <span class="na">id</span><span class="p">:</span> <span class="nx">inputs</span><span class="p">.</span><span class="nx">name</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">_id</span><span class="dl">"</span><span class="p">,</span>
          <span class="na">state</span><span class="p">:</span> <span class="p">{</span>
            <span class="p">...</span><span class="nx">inputs</span><span class="p">,</span>
            <span class="na">arn</span><span class="p">:</span> <span class="dl">"</span><span class="s2">arn:aws:some-cert-arn</span><span class="dl">"</span><span class="p">,</span>
          <span class="p">},</span>
        <span class="p">};</span>
      <span class="k">case</span> <span class="dl">"</span><span class="s2">aws:acm/certificateValidation:CertificateValidation</span><span class="dl">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="p">{</span>
          <span class="na">id</span><span class="p">:</span> <span class="nx">inputs</span><span class="p">.</span><span class="nx">name</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">_id</span><span class="dl">"</span><span class="p">,</span>
          <span class="na">state</span><span class="p">:</span> <span class="p">{</span>
            <span class="p">...</span><span class="nx">inputs</span><span class="p">,</span>
          <span class="p">},</span>
        <span class="p">};</span>
      <span class="nl">default</span><span class="p">:</span>
        <span class="k">return</span> <span class="p">{</span>
          <span class="na">id</span><span class="p">:</span> <span class="nx">inputs</span><span class="p">.</span><span class="nx">name</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">_id</span><span class="dl">"</span><span class="p">,</span>
          <span class="na">state</span><span class="p">:</span> <span class="p">{</span>
            <span class="p">...</span><span class="nx">inputs</span><span class="p">,</span>
          <span class="p">},</span>
        <span class="p">};</span>
    <span class="p">}</span>
  <span class="p">},</span>
  <span class="na">call</span><span class="p">:</span> <span class="kd">function</span> <span class="p">(</span><span class="na">token</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="na">args</span><span class="p">:</span> <span class="kr">any</span><span class="p">,</span> <span class="nx">provider</span><span class="p">?:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">args</span><span class="p">;</span>
  <span class="p">},</span>
<span class="p">});</span>
</code></pre></div></div>

<p>That’s a little verbose but gives us control on the <code class="language-plaintext highlighter-rouge">state</code> of the returned object. 
For example, see you the ARN is set on the Certificate via <code class="language-plaintext highlighter-rouge">arn: "arn:aws:some-cert-arn"</code>.</p>

<p>Next, we import the module under test, and write down the tests as usual using <code class="language-plaintext highlighter-rouge">describe()</code> definitions.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Continuing in src/my-certificate/index.test.ts</span>

<span class="c1">// It's important to import the program _after_ the mocks are defined.</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">infra</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./index</span><span class="dl">"</span><span class="p">;</span>

<span class="nx">describe</span><span class="p">(</span><span class="dl">"</span><span class="s2">MyCertificate</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">targetDomain</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">some.domain.com</span><span class="dl">"</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">resource</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">infra</span><span class="p">.</span><span class="nx">MyCertificate</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-certificate</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="nx">targetDomain</span><span class="p">,</span>
  <span class="p">});</span>
  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">must have a targetDomain</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">pulumi</span><span class="p">.</span><span class="nx">all</span><span class="p">([</span><span class="nx">resource</span><span class="p">.</span><span class="nx">certificate</span><span class="p">.</span><span class="nx">domainName</span><span class="p">]).</span><span class="nx">apply</span><span class="p">(([</span><span class="nx">domainName</span><span class="p">])</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="nx">domainName</span><span class="p">,</span> <span class="nx">targetDomain</span><span class="p">);</span>
      <span class="nx">done</span><span class="p">();</span>
    <span class="p">});</span>
  <span class="p">});</span>
  <span class="nx">it</span><span class="p">(</span><span class="dl">"</span><span class="s2">must have a certificateValidation</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">pulumi</span>
      <span class="p">.</span><span class="nx">all</span><span class="p">([</span><span class="nx">resource</span><span class="p">.</span><span class="nx">certificateValidation</span><span class="p">.</span><span class="nx">certificateArn</span><span class="p">])</span>
      <span class="p">.</span><span class="nx">apply</span><span class="p">(([</span><span class="nx">certificateArn</span><span class="p">])</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="nx">certificateArn</span><span class="p">,</span> <span class="dl">"</span><span class="s2">arn:aws:some-cert-arn</span><span class="dl">"</span><span class="p">);</span>
        <span class="nx">done</span><span class="p">();</span>
      <span class="p">});</span>
  <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Two asserts are declared here:</p>

<ul>
  <li>the certificate takes on the <code class="language-plaintext highlighter-rouge">targetDomain</code> passed from the config,</li>
  <li>the certificateValidation gets the <code class="language-plaintext highlighter-rouge">arn</code> from the certificate.</li>
</ul>

<p>Simple enough and effective for this very simple component.</p>

<p>How do you actually run the test itself? Again it’s a bit more verbose than ideal…</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>node_modules/.bin/mocha <span class="nt">-r</span> ts-node/register src/my-certificate/index.test.ts 

  MyCertificate
    ✓ must have a targetDomain
    ✓ must have a certificateValidation


  2 passing <span class="o">(</span>7ms<span class="o">)</span>

</code></pre></div></div>

<p>Further reading could include the official documentation for <a href="https://www.pulumi.com/docs/guides/testing/unit/">unit testing</a>.</p>

<h1 id="conclusions">Conclusions</h1>

<h2 id="refactoring">Refactoring</h2>

<p>Take action before your Pulumi project becomes a long <code class="language-plaintext highlighter-rouge">index.ts</code> file with hundreds of resources. 
You can take advantage of compound resources to break code up into smaller, re-usable chunks. 
Beyond just making the code maintainable, it’ll allow you to unit-test the components in isolation.</p>

<h2 id="testing">Testing</h2>
<p>I can’t say that I loved writing Pulumi unit tests in Typescript.
It feels awkward and clunky, relative to my experience with other tools (Jest?). 
Despite all my love for Pulumi, you won’t hear me sing accolades here. 
Still, it’s infinitely better than having no unit tests at all. 
Together with a robust integration tests, it should be entirely possible to bring the best of testing into your infrastructure code.</p>]]></content><author><name></name></author><category term="pulumi" /><category term="aws" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Deploying a Create React App to AWS with Pulumi (part III)</title><link href="/pulumi/aws/react/2020/10/29/deploy-create-react-app-aws-pulumi-post3.html" rel="alternate" type="text/html" title="Deploying a Create React App to AWS with Pulumi (part III)" /><published>2020-10-29T23:00:00+00:00</published><updated>2020-10-29T23:00:00+00:00</updated><id>/pulumi/aws/react/2020/10/29/deploy-create-react-app-aws-pulumi/post3</id><content type="html" xml:base="/pulumi/aws/react/2020/10/29/deploy-create-react-app-aws-pulumi-post3.html"><![CDATA[<p><img src="/docs/images/posts/2020-07-30-deploy-create-react-app-aws-pulumi/logos.svg" alt="Introduction" /></p>

<h1 id="introduction">Introduction</h1>

<p>Welcome back to the series! We’re nearly done here with your SPA application happily sitting behind a domain an an S3 bucket. 
Why push forward, you might ask? After all things are looking pretty dandy. 
Well, it turns out that serving files from your S3 bucket may not be the ideal option. 
Depending on how often your website changes on S3, and its probably not that often, you’re essentially serving the same files over and over again. 
Would that make for an excellent case for caching? It would.</p>

<p>Putting a content distribution network in front of the S3 bucket would allow you to cache the responses to incoming requests. 
For example, once a CSS file is shipped once, the CDN will keep track of it.</p>

<p>Well, this seems complicated, is it really worth it? 
This setup is as close to “industry best practice as it gets”. 
As a matter of fact, I’ll be ripping off from <a href="https://www.pulumi.com/docs/tutorials/aws/aws-ts-static-website/">Pulumi’s own tutorial</a> pretty heavily and shamelessly. 
But to be completely honest, for a small  page like it probably doesn’t make sense. 
Using GitHub Pages would be just as suitable of an alternative.</p>

<h4 id="other-posts-in-this-series">Other posts in this series</h4>

<p><a href="/pulumi/aws/react/2020/07/29/deploy-create-react-app-aws-pulumi-post1.html">Part 1</a> – <a href="/pulumi/aws/react/2020/08/29/deploy-create-react-app-aws-pulumi-post2.html">Part 2</a> – <a href="/pulumi/aws/2020/11/07/deploy-create-react-app-aws-pulumi-post4.html">Part 4</a></p>

<h2 id="the-plan">The plan</h2>

<p>This is it people – it’s the 3rd part and so we’re entering “level HARD”. 
No more joking around – and if you completed parts I and II, you’re practically a cloud engineer now ;-)</p>

<p>The principal aim will be to create an AWS CloudFront resource and place it between the S3 bucket and the Route53 domain. 
Additionally, we will create a new bucket for storing AWS CloudFront logs – but that’s just a nice to have.</p>

<p>As we’re about to jump in, the time is ripe for a friendly warning:</p>

<blockquote>
  <p>In this post we’ll be working with the AWS CloudFront service, which is notoriously slow to work with – in the sense of configuration changes taking 15-20 minutes to perform.</p>
</blockquote>

<p>So don’t get your coffee just yet – there will be plenty of opportunity to do that later.</p>

<h1 id="results">Results</h1>

<h2 id="logging-back-into-pulumi">Logging back into Pulumi</h2>

<p>Before jumping in, and especially if you’re returning to this series after a break, make sure that your Pulumi environment is configured correctly.</p>

<p>Here is a handy checklist</p>
<ul>
  <li>Did you login to the right Pulumi project with <code class="language-plaintext highlighter-rouge">pulumi login</code>?</li>
  <li>Did you configure <code class="language-plaintext highlighter-rouge">PULUMI_CONFIG_PASSPHRASE</code> as your environment variable?</li>
</ul>

<p>If yes, you can check if it’s all working with <code class="language-plaintext highlighter-rouge">pulumi stack</code> to display the resources in the stack.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pulumi stack
Current stack is dev:
    Managed by jans-mbp.mynet
    Last updated: 1 minute ago <span class="o">(</span>2020-10-31 07:10:29.907622 +0000 UTC<span class="o">)</span>
    Pulumi version: v2.12.1
Current stack resources <span class="o">(</span>4<span class="o">)</span>:
    TYPE                              NAME
    pulumi:pulumi:Stack               my-app-dev
    ├─ aws:s3/bucket:Bucket           my-app.jandomanski.com
    ├─ aws:route53/record:Record      targetDomain
    └─ pulumi:providers:aws           default_3_11_0

Current stack outputs <span class="o">(</span>2<span class="o">)</span>:
    OUTPUT      VALUE
    bucketName  my-app.jandomanski.com
    recordName  my-app.jandomanski.com
</code></pre></div></div>

<h2 id="defining-a-helper-function">Defining a helper function</h2>

<p>Let’s kick off with a very simple helper function.
The helper function takes a domain (“www.example.com”) and returns an object with 2 properties (subdomain and parentDomain).
What this is needed for will become apparent soon!</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Split a domain name into its subdomain and parent domain names.</span>
<span class="c1">// e.g. "www.example.com" =&gt; "www", "example.com".</span>
<span class="kd">function</span> <span class="nx">getDomainAndSubdomain</span><span class="p">(</span>
  <span class="nx">domain</span><span class="p">:</span> <span class="kr">string</span>
<span class="p">):</span> <span class="p">{</span> <span class="nl">subdomain</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">parentDomain</span><span class="p">:</span> <span class="kr">string</span> <span class="p">}</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">parts</span> <span class="o">=</span> <span class="nx">domain</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">"</span><span class="s2">.</span><span class="dl">"</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">parts</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`No TLD found on </span><span class="p">${</span><span class="nx">domain</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="c1">// No subdomain, e.g. awesome-website.com.</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">parts</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span> <span class="na">subdomain</span><span class="p">:</span> <span class="dl">""</span><span class="p">,</span> <span class="na">parentDomain</span><span class="p">:</span> <span class="nx">domain</span> <span class="p">};</span>
  <span class="p">}</span>

  <span class="kd">const</span> <span class="nx">subdomain</span> <span class="o">=</span> <span class="nx">parts</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
  <span class="nx">parts</span><span class="p">.</span><span class="nx">shift</span><span class="p">();</span> <span class="c1">// Drop first element.</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="nx">subdomain</span><span class="p">,</span>
    <span class="c1">// Trailing "." to canonicalize domain.</span>
    <span class="na">parentDomain</span><span class="p">:</span> <span class="nx">parts</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2">.</span><span class="dl">"</span><span class="p">)</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">.</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="building-the-foundations">Building the foundations</h2>

<p>What do we have now? We have an S3 bucket and a Route53 record. 
We agreed to slot in a CDN in between those two. 
How exactly do we do that?</p>

<p>The CDN can serve responses via HTTPS so let’s provide it with a SSL certificate, as a nice bonus quest. 
So let’s start by setting this up: insert the following snippet after the bucket definition.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">const</span> <span class="nx">domainParts</span> <span class="o">=</span> <span class="nx">getDomainAndSubdomain</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-app.jandomanski.com</span><span class="dl">"</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">hostedZoneId</span> <span class="o">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">route53</span>
    <span class="p">.</span><span class="nx">getZone</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="nx">domainParts</span><span class="p">.</span><span class="nx">parentDomain</span> <span class="p">},</span> <span class="p">{</span> <span class="na">async</span><span class="p">:</span> <span class="kc">true</span> <span class="p">})</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">zone</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">zone</span><span class="p">.</span><span class="nx">zoneId</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">tenMinutes</span> <span class="o">=</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">10</span><span class="p">;</span>

  <span class="c1">// Per AWS, ACM certificate must be in the us-east-1 region.</span>
  <span class="kd">const</span> <span class="nx">eastRegion</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">Provider</span><span class="p">(</span><span class="dl">"</span><span class="s2">east</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">profile</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">profile</span><span class="p">,</span>
    <span class="na">region</span><span class="p">:</span> <span class="dl">"</span><span class="s2">us-east-1</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">});</span>
</code></pre></div></div>

<ul>
  <li>The <code class="language-plaintext highlighter-rouge">tenMinutes</code> is just a simple constant that we’ll use later,</li>
  <li>The <code class="language-plaintext highlighter-rouge">eastRegion</code> is more interesting.</li>
</ul>

<p>The <code class="language-plaintext highlighter-rouge">eastRegion</code> is just a resource, like everything else in Pulumi, but it’s important for the SSL. 
The SSL certificates can only be created in the “us-east-1” region. 
My default region is “eu-west-1” and thus I need an additional provider to create other resources in that region. 
This may not be necessary for you if you’re already using the “us-east-1” region.</p>

<p>This looks like a simple enough change, so let’s be “greedy” and get this update done. 
Quick win to start the post. Here is what you should seen when you run <code class="language-plaintext highlighter-rouge">pulumi up</code>.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pulumi up
Previewing update <span class="o">(</span>dev<span class="o">)</span>:
     Type                     Name        Plan       
     pulumi:pulumi:Stack      my-app-dev             
 +   └─ pulumi:providers:aws  east        create     
 
Resources:
    + 1 to create
    3 unchanged

Do you want to perform this update? <span class="nb">yes
</span>Updating <span class="o">(</span>dev<span class="o">)</span>:
     Type                     Name        Status      
     pulumi:pulumi:Stack      my-app-dev              
 +   └─ pulumi:providers:aws  east        created     
 
Outputs:
    bucketName: <span class="s2">"my-app.jandomanski.com"</span>
    recordName: <span class="s2">"my-app.jandomanski.com"</span>

Resources:
    + 1 created
    3 unchanged

Duration: 16s

</code></pre></div></div>

<h2 id="creating-and-validating-an-ssl-certificate">Creating and validating an SSL certificate</h2>

<p>With the <code class="language-plaintext highlighter-rouge">eastProvider</code> we can now create an SSL certificate on AWS. 
If you’ve ever done it manually, this will be <strong>pure magic</strong>.
Using a special resource, you can <em>*automatically</em> validate the SSL certificate.
No more email validation, or manual DNS validation!
In this case, a Route53 record is created automatically to validate the certificate.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">const</span> <span class="nx">certificate</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">acm</span><span class="p">.</span><span class="nx">Certificate</span><span class="p">(</span>
    <span class="dl">"</span><span class="s2">certificate</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">{</span>
      <span class="na">domainName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">my-app.jandomanski.com</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">validationMethod</span><span class="p">:</span> <span class="dl">"</span><span class="s2">DNS</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
    <span class="p">{</span> <span class="na">provider</span><span class="p">:</span> <span class="nx">eastRegion</span> <span class="p">}</span>
  <span class="p">);</span>

  <span class="kd">const</span> <span class="nx">certificateValidationDomain</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">route53</span><span class="p">.</span><span class="nb">Record</span><span class="p">(</span>
    <span class="dl">"</span><span class="s2">my-app.jandomanski.com-validation</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="nx">certificate</span><span class="p">.</span><span class="nx">domainValidationOptions</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">resourceRecordName</span><span class="p">,</span>
      <span class="na">zoneId</span><span class="p">:</span> <span class="nx">hostedZoneId</span><span class="p">,</span>
      <span class="na">type</span><span class="p">:</span> <span class="nx">certificate</span><span class="p">.</span><span class="nx">domainValidationOptions</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">resourceRecordType</span><span class="p">,</span>
      <span class="na">records</span><span class="p">:</span> <span class="p">[</span><span class="nx">certificate</span><span class="p">.</span><span class="nx">domainValidationOptions</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">resourceRecordValue</span><span class="p">],</span>
      <span class="na">ttl</span><span class="p">:</span> <span class="nx">tenMinutes</span><span class="p">,</span>
    <span class="p">}</span>
  <span class="p">);</span>

  <span class="kd">const</span> <span class="nx">certificateValidation</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">acm</span><span class="p">.</span><span class="nx">CertificateValidation</span><span class="p">(</span>
    <span class="dl">"</span><span class="s2">certificateValidation</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">{</span>
      <span class="na">certificateArn</span><span class="p">:</span> <span class="nx">certificate</span><span class="p">.</span><span class="nx">arn</span><span class="p">,</span>
      <span class="na">validationRecordFqdns</span><span class="p">:</span> <span class="p">[</span><span class="nx">certificateValidationDomain</span><span class="p">.</span><span class="nx">fqdn</span><span class="p">],</span>
    <span class="p">},</span>
    <span class="p">{</span> <span class="na">provider</span><span class="p">:</span> <span class="nx">eastRegion</span> <span class="p">}</span>
  <span class="p">);</span>
</code></pre></div></div>

<p>Let’s run the update and get our SSL certificate. Here is what the pulumi output should look like:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pulumi up 
Previewing update <span class="o">(</span>dev<span class="o">)</span>:
     Type                              Name                               Plan       
     pulumi:pulumi:Stack               my-app-dev                                    
 +   ├─ aws:acm:Certificate            certificate                        create     
 +   ├─ aws:route53:Record             my-app.jandomanski.com-validation  create     
 +   └─ aws:acm:CertificateValidation  certificateValidation              create     
 
Resources:
    + 3 to create
    4 unchanged

Do you want to perform this update? <span class="nb">yes
</span>Updating <span class="o">(</span>dev<span class="o">)</span>:
     Type                              Name                               Status      
     pulumi:pulumi:Stack               my-app-dev                                     
 +   ├─ aws:acm:Certificate            certificate                        created     
 +   ├─ aws:route53:Record             my-app.jandomanski.com-validation  created     
 +   └─ aws:acm:CertificateValidation  certificateValidation              created     
 
Outputs:
    bucketName: <span class="s2">"my-app.jandomanski.com"</span>
    recordName: <span class="s2">"my-app.jandomanski.com"</span>

Resources:
    + 3 created
    4 unchanged

Duration: 57s
</code></pre></div></div>

<h2 id="configuring-and-creating-the-cdn">Configuring and creating the CDN</h2>

<p>AWS CloudFront is a complicated service with a lot of belts and whistles. 
Examples include</p>
<ul>
  <li>HTTPS -&gt; HTTPS redirects,</li>
  <li>ability to use AWS Lambdas to process requests and responses,</li>
  <li>injecting custom headers.</li>
</ul>

<p>Many of these features can be defined by <code class="language-plaintext highlighter-rouge">DistributionArgs</code>. 
It’s rather verbose and almost completely copy&amp;pasted from the <a href="https://www.pulumi.com/docs/tutorials/aws/aws-ts-static-website/">Pulumi Tutorial</a> .
If you want a better sense of what’s available in CDN configuration, have a look below:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c1">// distributionArgs configures the CloudFront distribution. Relevant documentation:</span>
  <span class="c1">// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html</span>
  <span class="c1">// https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html</span>
  <span class="kd">const</span> <span class="nx">distributionArgs</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">cloudfront</span><span class="p">.</span><span class="nx">DistributionArgs</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">enabled</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="c1">// Alternate aliases the CloudFront distribution can be reached at, in addition to https://xxxx.cloudfront.net.</span>
    <span class="c1">// Required if you want to access the distribution via config.targetDomain as well.</span>
    <span class="na">aliases</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">my-app.jandomanski.com</span><span class="dl">"</span><span class="p">],</span>

    <span class="c1">// We only specify one origin for this distribution, the S3 content bucket.</span>
    <span class="na">origins</span><span class="p">:</span> <span class="p">[</span>
      <span class="p">{</span>
        <span class="na">originId</span><span class="p">:</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">arn</span><span class="p">,</span>
        <span class="na">domainName</span><span class="p">:</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">websiteEndpoint</span><span class="p">,</span>
        <span class="na">customOriginConfig</span><span class="p">:</span> <span class="p">{</span>
          <span class="c1">// Amazon S3 doesn't support HTTPS connections when using an S3 bucket configured as a website endpoint.</span>
          <span class="c1">// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesOriginProtocolPolicy</span>
          <span class="na">originProtocolPolicy</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http-only</span><span class="dl">"</span><span class="p">,</span>
          <span class="na">httpPort</span><span class="p">:</span> <span class="mi">80</span><span class="p">,</span>
          <span class="na">httpsPort</span><span class="p">:</span> <span class="mi">443</span><span class="p">,</span>
          <span class="na">originSslProtocols</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">TLSv1.2</span><span class="dl">"</span><span class="p">],</span>
        <span class="p">},</span>
      <span class="p">},</span>
    <span class="p">],</span>

    <span class="na">defaultRootObject</span><span class="p">:</span> <span class="dl">"</span><span class="s2">index.html</span><span class="dl">"</span><span class="p">,</span>

    <span class="c1">// A CloudFront distribution can configure different cache behaviors based on the request path.</span>
    <span class="c1">// Here we just specify a single, default cache behavior which is just read-only requests to S3.</span>
    <span class="na">defaultCacheBehavior</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">targetOriginId</span><span class="p">:</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">arn</span><span class="p">,</span>

      <span class="na">viewerProtocolPolicy</span><span class="p">:</span> <span class="dl">"</span><span class="s2">redirect-to-https</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">allowedMethods</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">GET</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">HEAD</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">OPTIONS</span><span class="dl">"</span><span class="p">],</span>
      <span class="na">cachedMethods</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">GET</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">HEAD</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">OPTIONS</span><span class="dl">"</span><span class="p">],</span>

      <span class="na">forwardedValues</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">cookies</span><span class="p">:</span> <span class="p">{</span> <span class="na">forward</span><span class="p">:</span> <span class="dl">"</span><span class="s2">none</span><span class="dl">"</span> <span class="p">},</span>
        <span class="na">queryString</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
      <span class="p">},</span>

      <span class="na">minTtl</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
      <span class="na">defaultTtl</span><span class="p">:</span> <span class="nx">tenMinutes</span><span class="p">,</span>
      <span class="na">maxTtl</span><span class="p">:</span> <span class="nx">tenMinutes</span><span class="p">,</span>
    <span class="p">},</span>

    <span class="c1">// "All" is the most broad distribution, and also the most expensive.</span>
    <span class="c1">// "100" is the least broad, and also the least expensive.</span>
    <span class="na">priceClass</span><span class="p">:</span> <span class="dl">"</span><span class="s2">PriceClass_100</span><span class="dl">"</span><span class="p">,</span>

    <span class="c1">// You can customize error responses. When CloudFront receives an error from the origin (e.g. S3 or some other</span>
    <span class="c1">// web service) it can return a different error code, and return the response for a different resource.</span>
    <span class="na">customErrorResponses</span><span class="p">:</span> <span class="p">[</span>
      <span class="p">{</span> <span class="na">errorCode</span><span class="p">:</span> <span class="mi">404</span><span class="p">,</span> <span class="na">responseCode</span><span class="p">:</span> <span class="mi">404</span><span class="p">,</span> <span class="na">responsePagePath</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/404.html</span><span class="dl">"</span> <span class="p">},</span>
    <span class="p">],</span>

    <span class="na">restrictions</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">geoRestriction</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">restrictionType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">none</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">},</span>
    <span class="p">},</span>

    <span class="na">viewerCertificate</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">acmCertificateArn</span><span class="p">:</span> <span class="nx">certificateValidation</span><span class="p">.</span><span class="nx">certificateArn</span><span class="p">,</span> <span class="c1">// Per AWS, ACM certificate must be in the us-east-1 region.</span>
      <span class="na">sslSupportMethod</span><span class="p">:</span> <span class="dl">"</span><span class="s2">sni-only</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">};</span>
</code></pre></div></div>

<p>Finally, let’s add a line to create the CDN and run <code class="language-plaintext highlighter-rouge">pulumi up</code>.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">const</span> <span class="nx">cdn</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">cloudfront</span><span class="p">.</span><span class="nx">Distribution</span><span class="p">(</span><span class="dl">"</span><span class="s2">cdn</span><span class="dl">"</span><span class="p">,</span> <span class="nx">distributionArgs</span><span class="p">);</span>
</code></pre></div></div>

<p>Before proceeding, you should know one thing.</p>

<blockquote>
  <p>WARNING AWS CloudFormation is a very big service and it can take a WHILE (5-25 minutes) to create or update resources</p>
</blockquote>

<p>Here is what your pulumi up should look like</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pulumi up 
Previewing update <span class="o">(</span>dev<span class="o">)</span>:
     Type                            Name        Plan       
     pulumi:pulumi:Stack             my-app-dev             
 +   └─ aws:cloudfront:Distribution  cdn         create     
 
Resources:
    + 1 to create
    7 unchanged

Do you want to perform this update? <span class="nb">yes
</span>Updating <span class="o">(</span>dev<span class="o">)</span>:
     Type                            Name        Status      
     pulumi:pulumi:Stack             my-app-dev              
 +   └─ aws:cloudfront:Distribution  cdn         created     
 
Outputs:
    bucketName: <span class="s2">"my-app.jandomanski.com"</span>
    recordName: <span class="s2">"my-app.jandomanski.com"</span>

Resources:
    + 1 created
    7 unchanged

Duration: 2m53s
</code></pre></div></div>

<h2 id="updating-the-route53">Updating the Route53</h2>

<p>This is the final step and should take no time at all!
At the moment, we have a Route53 record that points to the S3 bucket holding the React app. 
The Route53 record is defined like this:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c1">// Create a Route53 A-record</span>
  <span class="kd">const</span> <span class="nx">record</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">route53</span><span class="p">.</span><span class="nb">Record</span><span class="p">(</span><span class="dl">"</span><span class="s2">targetDomain</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">my-app.jandomanski.com</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">zoneId</span><span class="p">:</span> <span class="nx">hostedZoneId</span><span class="p">,</span>
    <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">A</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">aliases</span><span class="p">:</span> <span class="p">[</span>
      <span class="p">{</span>
        <span class="na">zoneId</span><span class="p">:</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">hostedZoneId</span><span class="p">,</span>
        <span class="na">name</span><span class="p">:</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">websiteDomain</span><span class="p">,</span>
        <span class="na">evaluateTargetHealth</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="p">},</span>
    <span class="p">],</span>
  <span class="p">});</span>
</code></pre></div></div>

<p>Now that we have a CDN, we can update the Route53 record to point to a new endpoint. 
Same domain name, same zoneId, just pointing to the new CDN – which acts as a proxy for the S3 bucket.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c1">// Create a Route53 A-record</span>
  <span class="kd">const</span> <span class="nx">record</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">route53</span><span class="p">.</span><span class="nb">Record</span><span class="p">(</span><span class="dl">"</span><span class="s2">targetDomain</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">my-app.jandomanski.com</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">zoneId</span><span class="p">:</span> <span class="nx">hostedZoneId</span><span class="p">,</span>
    <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">A</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">aliases</span><span class="p">:</span> <span class="p">[</span>
      <span class="p">{</span>
        <span class="na">name</span><span class="p">:</span> <span class="nx">cdn</span><span class="p">.</span><span class="nx">domainName</span><span class="p">,</span>
        <span class="na">zoneId</span><span class="p">:</span> <span class="nx">cdn</span><span class="p">.</span><span class="nx">hostedZoneId</span><span class="p">,</span>
        <span class="na">evaluateTargetHealth</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="p">},</span>
    <span class="p">],</span>
  <span class="p">});</span>
</code></pre></div></div>

<p>Here is how the pulumi log output looks like</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pulumi up 
Previewing update <span class="o">(</span>dev<span class="o">)</span>:
     Type                   Name          Plan       Info
     pulumi:pulumi:Stack    my-app-dev               
 ~   └─ aws:route53:Record  targetDomain  update     <span class="o">[</span>diff: ~aliases]
 
Resources:
    ~ 1 to update
    7 unchanged

Do you want to perform this update? <span class="nb">yes
</span>Updating <span class="o">(</span>dev<span class="o">)</span>:
     Type                   Name          Status      Info
     pulumi:pulumi:Stack    my-app-dev                
 ~   └─ aws:route53:Record  targetDomain  updated     <span class="o">[</span>diff: ~aliases]
 
Outputs:
    bucketName: <span class="s2">"my-app.jandomanski.com"</span>
    recordName: <span class="s2">"my-app.jandomanski.com"</span>

Resources:
    ~ 1 updated
    7 unchanged

Duration: 51s
</code></pre></div></div>

<h2 id="testing-if-it-all-works">Testing if it all works</h2>

<p>There are some quick ways to diagnose if we got what we want the two simple tests should be</p>

<ul>
  <li>open http://my-app.jandomanski.com -&gt; should redirect to https://</li>
  <li>open https://my-app.jandomanski.com -&gt; should open index.html</li>
  <li>open https://my-app.jandomanski.com/index.html -&gt; same as above</li>
</ul>

<p>Beyond, there are some further advanced experiments we can do using curl.</p>

<p>First of all, remember <code class="language-plaintext highlighter-rouge">index.html</code> and how we set the Cache-Control headers? 
Well, those are respected by the CDN. 
You can clearly see this by hitting the endpoint with cURL.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl https://my-app.jandomanski.com/index.html <span class="nt">-I</span>
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 2219
Connection: keep-alive
Date: Sun, 01 Nov 2020 19:20:52 GMT
Cache-Control: no-cache,no-store
Last-Modified: Sun, 01 Nov 2020 19:20:43 GMT
ETag: <span class="s2">"67e4d5da5073a0ba60ce72a01c3feee4"</span>
Server: AmazonS3
X-Cache: Miss from cloudfront
Via: 1.1 a5c420a169b19bd150b00f34513e997d.cloudfront.net <span class="o">(</span>CloudFront<span class="o">)</span>
X-Amz-Cf-Pop: LHR62-C3
X-Amz-Cf-Id: <span class="nv">P3048HiqZZ66rJ2PujNg44G51v2wzkwumXp1aO2LOumDmbsT03nVFg</span><span class="o">==</span>

</code></pre></div></div>

<p>You can hit this over and over again, in all cases you’ll get <code class="language-plaintext highlighter-rouge">X-Cache: Miss from cloudfront</code> and <code class="language-plaintext highlighter-rouge">Cache-Control: no-cache,no-store</code>. 
Because of the Cache-Control header, the content is always served from S3 directly. 
What about other files? Does caching work for them as expected?</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl https://my-app.jandomanski.com/favicon.ico <span class="nt">-I</span>
HTTP/1.1 200 OK
Content-Type: image/x-icon
Content-Length: 3150
Connection: keep-alive
Date: Sun, 01 Nov 2020 19:24:45 GMT
Cache-Control: max-age<span class="o">=</span>31536000
Last-Modified: Sat, 31 Oct 2020 08:31:51 GMT
ETag: <span class="s2">"6e1267d9d946b0236cdf6ffd02890894"</span>
Server: AmazonS3
X-Cache: Hit from cloudfront
Via: 1.1 6fc6ff9b881f0fff41ff95cfddcc92eb.cloudfront.net <span class="o">(</span>CloudFront<span class="o">)</span>
X-Amz-Cf-Pop: LHR52-C1
X-Amz-Cf-Id: <span class="nv">6MIpI2IkMlT1UiGnmP4hsO8qM_TIbxEG5ZbhRh3fhoGREYz_O08Snw</span><span class="o">==</span>
Age: 3
</code></pre></div></div>

<p>Can you see the <code class="language-plaintext highlighter-rouge">Cache-Control</code> and <code class="language-plaintext highlighter-rouge">X-Cache</code> headers above? Both indicate a few things about the CDN:</p>
<ul>
  <li>it respected the Cache-Control property we set on the S3 objects,</li>
  <li>it cached the appropriate files, and is serving them from cache – no trips to the S3 bucket are done.</li>
</ul>

<h1 id="conclusions">Conclusions</h1>

<p>So that’s it, right? 
Over the course of the series, we travelled from a very simple S3 hosting of a simple site. 
Things were simple then but not very cost efficient. 
Then we added a Route53 domain for some nice access. 
Finally, we put a CDN in front of the S3 bucket as a caching layer.</p>

<p>But did we sacrifice something? The initial <code class="language-plaintext highlighter-rouge">index.ts</code> seemed very simple but the one we have now is quite large. 
Is there a way to refactor? How to make this more modular?
What about testing? Is there an easy way to test this code, without creating the infrastructure?
Well, those are very wise questions indeed – and we’ll answer them in the BONUS fourth part to this series.</p>]]></content><author><name></name></author><category term="pulumi" /><category term="aws" /><category term="react" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Deploying a Create React App to AWS with Pulumi (part II)</title><link href="/pulumi/aws/react/2020/08/29/deploy-create-react-app-aws-pulumi-post2.html" rel="alternate" type="text/html" title="Deploying a Create React App to AWS with Pulumi (part II)" /><published>2020-08-29T23:00:00+00:00</published><updated>2020-08-29T23:00:00+00:00</updated><id>/pulumi/aws/react/2020/08/29/deploy-create-react-app-aws-pulumi/post2</id><content type="html" xml:base="/pulumi/aws/react/2020/08/29/deploy-create-react-app-aws-pulumi-post2.html"><![CDATA[<p><img src="/docs/images/posts/2020-07-30-deploy-create-react-app-aws-pulumi/logos.svg" alt="Introduction" /></p>

<h1 id="introduction">Introduction</h1>

<p>Welcome back to part II in the series! If you remember, we’re going to figure out how to deploy a small Create React App to AWS infrastructure.
How are we going to do that exactly? Using a neat tool called Pulumi. 
While using Create React App as an example, we’ll cover reusable architectures for creating any web apps on AWS infrastructure.</p>

<p>The series is mainly targeted at frontend developers who want to get their hands dirty with AWS infrastructure. 
The series can also be useful if you’re looking for a gateway drug into the majestic world of Pulumi.</p>

<p>By way of a re-cap, what did we do last time?</p>
<ul>
  <li>Outlined 3 possible architectures (easy, medium and hard),</li>
  <li>Transpiled a ‘stock’ Create React App and got a build,</li>
  <li>Setup boilerplate for managing stacks of infrastructure resources is Pulumi,</li>
  <li>Deployed the “easy” architecture (a single S3 bucket).</li>
</ul>

<p>With all this out of the way, we have some new interesting waters to sail.</p>

<h4 id="other-posts-in-this-series">Other posts in this series</h4>

<p><a href="/pulumi/aws/react/2020/07/29/deploy-create-react-app-aws-pulumi-post1.html">Part 1</a> - <a href="/pulumi/aws/react/2020/10/29/deploy-create-react-app-aws-pulumi-post3.html">Part 3</a> – <a href="/pulumi/aws/2020/11/07/deploy-create-react-app-aws-pulumi-post4.html">Part 4</a></p>

<h2 id="the-plan">The plan</h2>

<p>So what’s new this time around? We’ll be putting together the “medium architecture”, it’s time to make things interesting. 
What exactly are looking for? Well, instead of accessing the app via the S3 bucket URL, it’d be nice to park it behind a proper domain (www.your-create-react-app.com). 
AWS manages DNS records via a service called Route53, so we’re going to use that.</p>

<p>What domain do we want use? It doesn’t really matter, maybe use some domain that you already bought? 
In this guide, I’ll use a subdomain at <code class="language-plaintext highlighter-rouge">myapp.jandomanski.com</code>.</p>

<h1 id="results">Results</h1>

<p>Before jumping in, and especially if you’re returning to this series after a break, make sure that your Pulumi environment is configured correctly.</p>

<p>Here is a handy checklist</p>
<ul>
  <li>Did you login to the right Pulumi project with <code class="language-plaintext highlighter-rouge">pulumi login</code>?</li>
  <li>Did you configure <code class="language-plaintext highlighter-rouge">PULUMI_CONFIG_PASSPHRASE</code> as your environment variable?</li>
</ul>

<p>If yes, you can check if it’s all working with <code class="language-plaintext highlighter-rouge">pulumi stack</code> to display the resources in the stack.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pulumi stack
Current stack is dev:
    Managed by jans-mbp.mynet
    Last updated: 2 weeks ago <span class="o">(</span>2020-08-31 21:21:49.628757 +0100 BST<span class="o">)</span>
    Pulumi version: v2.9.1
Current stack resources <span class="o">(</span>4<span class="o">)</span>:
    TYPE                              NAME
    pulumi:pulumi:Stack               my-app-dev
    ├─ aws:s3/bucket:Bucket           my-app.jandomanski.com
    ├─ aws:route53/record:Record      targetDomain
    └─ pulumi:providers:aws           default_2_13_0

Current stack outputs <span class="o">(</span>2<span class="o">)</span>:
    OUTPUT      VALUE
    bucketName  my-app.jandomanski.com
    recordName  my-app.jandomanski.com
</code></pre></div></div>

<h2 id="updating-the-definition-of-an-s3-bucket">Updating the definition of an S3 bucket</h2>

<p>At the end of the last article, we were left with the following Pulumi program. 
The program create an S3 bucket that we could access at a public URL <code class="language-plaintext highlighter-rouge">my-bucket-f01e841.s3-eu-west-1.amazonaws.com</code>.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">pulumi</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/pulumi</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">aws</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/aws</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">awsx</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/awsx</span><span class="dl">"</span><span class="p">;</span>

<span class="c1">// Create an AWS resource (S3 Bucket)</span>
<span class="kd">const</span> <span class="nx">bucket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">s3</span><span class="p">.</span><span class="nx">Bucket</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-bucket</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">acl</span><span class="p">:</span> <span class="dl">"</span><span class="s2">public-read</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">website</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">indexDocument</span><span class="p">:</span> <span class="dl">"</span><span class="s2">index.html</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
<span class="p">});</span>

<span class="c1">// Export the name of the bucket</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">bucketName</span> <span class="o">=</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">id</span><span class="p">;</span>
</code></pre></div></div>

<p>That’s a good start but hardly adequate. 
How can we publish your app to <code class="language-plaintext highlighter-rouge">my-bucket-f01e841.s3-eu-west-1.amazonaws.com</code>? 
Clearly nobody will care, it looks weird. 
What’s needed here is a nice domain such as <code class="language-plaintext highlighter-rouge">my-app.jandomanski.com</code>.</p>

<p>To get there, we first need rewrite that program slightly: to serve contents as a website, the S3 service needs bucket names to contain the domain name.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">pulumi</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/pulumi</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">aws</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/aws</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">awsx</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/awsx</span><span class="dl">"</span><span class="p">;</span>

<span class="c1">// Create an AWS resource (S3 Bucket)</span>
<span class="kd">const</span> <span class="nx">bucket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">s3</span><span class="p">.</span><span class="nx">Bucket</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-app.jandomanski.com</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">bucket</span><span class="p">:</span> <span class="dl">"</span><span class="s2">my-app.jandomanski.com</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">acl</span><span class="p">:</span> <span class="dl">"</span><span class="s2">public-read</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">website</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">indexDocument</span><span class="p">:</span> <span class="dl">"</span><span class="s2">index.html</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
<span class="p">});</span>

<span class="c1">// Export the name of the bucket</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">bucketName</span> <span class="o">=</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">id</span><span class="p">;</span>
</code></pre></div></div>

<p>Then we run the familiar <code class="language-plaintext highlighter-rouge">pulumi up</code> and here is a recording of how things should play out: <code class="language-plaintext highlighter-rouge">my-bucket</code> gets deleted and <code class="language-plaintext highlighter-rouge">my-app.jandomanski.com</code> gets created.</p>

<p><a href="https://asciinema.org/a/fhdrDlVWeBM5AOUfQHUPmr8CB"><img src="https://asciinema.org/a/fhdrDlVWeBM5AOUfQHUPmr8CB.svg" alt="asciicast" /></a></p>

<h2 id="refactoring-to-wrap-in-a-main-function">Refactoring to wrap in a main function</h2>

<p>Things are looking great – we’re iteratively moving towards the designed solution. 
It’s time for a little twist: a refactor to wrap our program into a main function. 
For now it’s just eye-candy and doesn’t really change too much.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">pulumi</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/pulumi</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">aws</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/aws</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">awsx</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/awsx</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">// Create an AWS resource (S3 Bucket)</span>
  <span class="kd">const</span> <span class="nx">bucket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">s3</span><span class="p">.</span><span class="nx">Bucket</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-app.jandomanski.com</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">bucket</span><span class="p">:</span> <span class="dl">"</span><span class="s2">my-app.jandomanski.com</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">acl</span><span class="p">:</span> <span class="dl">"</span><span class="s2">public-read</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">website</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">indexDocument</span><span class="p">:</span> <span class="dl">"</span><span class="s2">index.html</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">});</span>

  <span class="k">return</span> <span class="p">{</span>
    <span class="c1">// Export the name of the bucket</span>
    <span class="na">bucketName</span><span class="p">:</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
  <span class="p">};</span>
<span class="p">}</span>

<span class="kr">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">main</span><span class="p">();</span>
</code></pre></div></div>

<p>We can now run <code class="language-plaintext highlighter-rouge">pulumi up</code> again but since no resources are changed, it doesn’t really matter.</p>

<h2 id="adding-a-route-53-a-record">Adding a Route 53 A-record</h2>

<p>What’s our aim here? We need to put our S3 bucket behind a domain. 
To do that, we need to create a Route53 record in the DNS. 
How do we create Route53 record in the DNS? Well, we take an existing hosted zone (I’m assuming that you have that setup already) and use that to create the Record.</p>

<p>Getting the hosted zone uses this magical invocation</p>
<pre><code class="language-typescipt">aws.route53.getZone({ name: "jandomanski.com" }, { async: true }).then(...)
</code></pre>

<p>Here is the entire pulumi program. It retrieves the hosted zone information (a promise, under the hood) and creates a DNS record linking the domain to the S3 bucket.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">// Create an AWS resource (S3 Bucket)</span>
  <span class="kd">const</span> <span class="nx">bucket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">s3</span><span class="p">.</span><span class="nx">Bucket</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-app.jandomanski.com</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">bucket</span><span class="p">:</span> <span class="dl">"</span><span class="s2">my-app.jandomanski.com</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">acl</span><span class="p">:</span> <span class="dl">"</span><span class="s2">public-read</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">website</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">indexDocument</span><span class="p">:</span> <span class="dl">"</span><span class="s2">index.html</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">});</span>

  <span class="c1">// Get the hosted zone by domain name</span>
  <span class="kd">const</span> <span class="nx">hostedZoneId</span> <span class="o">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">route53</span>
    <span class="p">.</span><span class="nx">getZone</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">jandomanski.com</span><span class="dl">"</span> <span class="p">},</span> <span class="p">{</span> <span class="na">async</span><span class="p">:</span> <span class="kc">true</span> <span class="p">})</span>
    <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">zone</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">zone</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>

  <span class="c1">// Create a Route53 A-record</span>
  <span class="kd">const</span> <span class="nx">record</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">route53</span><span class="p">.</span><span class="nb">Record</span><span class="p">(</span><span class="dl">"</span><span class="s2">targetDomain</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">my-app.jandomanski.com</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">zoneId</span><span class="p">:</span> <span class="nx">hostedZone</span><span class="p">.</span><span class="nx">zoneId</span><span class="p">,</span>
    <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">A</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">aliases</span><span class="p">:</span> <span class="p">[{</span>
        <span class="na">zoneId</span><span class="p">:</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">hostedZoneId</span><span class="p">,</span>
        <span class="na">name</span><span class="p">:</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">websiteDomain</span><span class="p">,</span>
        <span class="na">evaluateTargetHealth</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="p">}],</span>
  <span class="p">});</span>

  <span class="k">return</span> <span class="p">{</span>
    <span class="c1">// Export the name of the bucket</span>
    <span class="na">bucketName</span><span class="p">:</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
    <span class="c1">// Export the name of the record</span>
    <span class="na">recordName</span><span class="p">:</span> <span class="nx">record</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With our new program, we need to run <code class="language-plaintext highlighter-rouge">pulumi up</code> again to create new resources with our cloud provider. 
Here is a recording that shows more-or-less what should happen. 
<a href="https://asciinema.org/a/j69yMkvwJfvqdJOjNjS3n5ZBR?t=34"><img src="https://asciinema.org/a/j69yMkvwJfvqdJOjNjS3n5ZBR.svg" alt="asciicast" /></a></p>

<h2 id="using-dig-to-confirm-domain-configuration-change">Using dig to confirm domain configuration change</h2>

<p>Joy fills the air - you did it! Using a small Pulumi program, you ran to AWS and made it update the DNS for your website.
But you know what’s the funny thing about DNS changes? Changes to the DNS can tak a while to propagate.</p>

<p>Hang on… but what if something is not working? 
Let’s say you go to <code class="language-plaintext highlighter-rouge">my-app.jandomanski.com</code> in your browser and the page doesn’t open. What then?
How do you know if you still need for the DNS to propagate VS if there was a bug in your config that got propatade?
What tools are available to diagnose DNS issues? Well, look no further keen reader and open your mind to the power of <code class="language-plaintext highlighter-rouge">dig</code>.</p>

<blockquote>
  <p>dig is the master tool that you need to know, just like cURL.</p>
</blockquote>

<p>Importantly, while cURL is the tool to master with HTTP requests, dig is the universal device for dealing with DNS issues.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ dig my-app.jandomanski.com A 
[...]

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;my-app.jandomanski.com.		IN	A

;; AUTHORITY SECTION:
jandomanski.com.	113	IN	SOA	ns-906.awsdns-49.net. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400

</code></pre></div></div>

<p>After the change</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ dig my-app.jandomanski.com A
[...]

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;my-app.jandomanski.com.		IN	A

;; ANSWER SECTION:
my-app.jandomanski.com.	5	IN	A	52.218.97.212

</code></pre></div></div>

<p>The relevant change to look for is the change from “AUTHORITY SECTION” to “ANSWER SECTION” that contains a valid IP4 address.</p>

<blockquote>
  <p>Another tool that you can use to diagnose DNS issue is <code class="language-plaintext highlighter-rouge">nslookup</code>, try <code class="language-plaintext highlighter-rouge">nslookup my-app.jandomanski.com</code>.</p>
</blockquote>

<h2 id="using-curl-to-confirm-page-contents-can-be-loaded">Using cURL to confirm page contents can be loaded</h2>

<p>We’re almost there! We’ve created a DNS record and confirmed that it has been propagated correctly using <code class="language-plaintext highlighter-rouge">dig</code>. 
What’s left to do is to confirm that your app can still be accessed. 
Accessing it via the bucket URL should continue to work, let’s confirm that quickly.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl http://my-app.jandomanski.com.s3-website-eu-west-1.amazonaws.com <span class="nt">-I</span>
<span class="go">HTTP/1.1 200 OK
x-amz-id-2: GmBsU0g/xUwEbglwJtNF9kccPdPooUengo+M4JJUF74sS9qVK81mByp7mAL4LMyTcq8vOBSEYWw=
x-amz-request-id: 54B4D8A94D9BF9F8
Date: Mon, 31 Aug 2020 20:17:44 GMT
Cache-Control: no-cache,no-store
Last-Modified: Mon, 31 Aug 2020 20:15:25 GMT
ETag: "67e4d5da5073a0ba60ce72a01c3feee4"
Content-Type: text/html
Content-Length: 2219
Server: AmazonS3
</span></code></pre></div></div>

<p>Okay, all good here. What about accessing it via the domain? Let’s try a simple test via <code class="language-plaintext highlighter-rouge">curl</code> just like above.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl http://my-app.jandomanski.com <span class="nt">-I</span>
<span class="go">HTTP/1.1 200 OK
x-amz-id-2: Gf/CYg8wVqE9DH1qj6/YCkCJU7NfgukwsENIEKGRuXRs0B33557+euz5mKtiTvskWSyYaHvwFrE=
x-amz-request-id: 4F134C079414F428
Date: Mon, 31 Aug 2020 20:25:52 GMT
Cache-Control: no-cache,no-store
Last-Modified: Mon, 31 Aug 2020 20:15:25 GMT
ETag: "67e4d5da5073a0ba60ce72a01c3feee4"
Content-Type: text/html
Content-Length: 2219
Server: AmazonS3
</span></code></pre></div></div>

<p>Whoa, that’s really cool! It worked!</p>

<h1 id="conclusions">Conclusions</h1>

<p>Well done for bearing with this one! We took a pretty windy road through the space of infrastructure and Pulumi. 
There was a lot of group covered in this post:</p>
<ul>
  <li>New Pulumi concepts in managing DNS records via the AWS Route53 service,</li>
  <li>Diagnosing and debugging DNS setting using <code class="language-plaintext highlighter-rouge">dig</code>.</li>
</ul>

<p>This was much harder than the previous “easy” architecture, and a lot more complex!
Now you have a domain with an S3 bucket but is that all we need?</p>
<ul>
  <li>What about access via HTTPS?</li>
  <li>What happens if many people access the domain?</li>
  <li>How does AWS price access to S3 bucket contents? Can we cache them somehow?</li>
</ul>

<p>All this and more in the 3rd and final episode.</p>

<h1 id="credits">Credits</h1>

<p>Still waiting for some eager reviewers</p>]]></content><author><name></name></author><category term="pulumi" /><category term="aws" /><category term="react" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Deploying a Create React App to AWS with Pulumi</title><link href="/pulumi/aws/react/2020/07/29/deploy-create-react-app-aws-pulumi-post1.html" rel="alternate" type="text/html" title="Deploying a Create React App to AWS with Pulumi" /><published>2020-07-29T23:00:00+00:00</published><updated>2020-07-29T23:00:00+00:00</updated><id>/pulumi/aws/react/2020/07/29/deploy-create-react-app-aws-pulumi/post1</id><content type="html" xml:base="/pulumi/aws/react/2020/07/29/deploy-create-react-app-aws-pulumi-post1.html"><![CDATA[<p><img src="/docs/images/posts/2020-07-30-deploy-create-react-app-aws-pulumi/logos.svg" alt="Introduction" /></p>

<h1 id="introduction">Introduction</h1>

<p>So you have just created your first app with <a href="https://reactjs.org/docs/create-a-new-react-app.html">Create React App</a>.
You built it, changed some source files. It works! Well, it works on your machine. 
What’s next? How do you actually get it out there, running?</p>

<blockquote>
  <p>You can build it but you want to ship it.</p>
</blockquote>

<p>The aim of this series of posts is to provide a step-by-step, incremental improvement journey.
The posts will show how to build your infrastructure, starting from the simplest configuration. 
What’s the point in showing you how to build the castle, if it’s not clear why one needs anything more than a shack?</p>

<p>This series may be particularly interesting to frontend developers who want to increase their understanding of infrastructure.</p>

<p>Going beyond just a cookbook recipe will be a must (troubleshooting should be an essential part of any series). 
When possible, debug and diagnostic commands are run to make sure things are in the state that they need to be.</p>

<p>This will be a big journey but if we can break it up into smaller iterative sub-steps, we’ll go from a shack to a castle.</p>

<h4 id="other-posts-in-this-series">Other posts in this series</h4>

<p><a href="/pulumi/aws/react/2020/08/29/deploy-create-react-app-aws-pulumi-post2.html">Part 2</a> – <a href="/pulumi/aws/react/2020/10/29/deploy-create-react-app-aws-pulumi-post3.html">Part 3</a> – <a href="/pulumi/aws/2020/11/07/deploy-create-react-app-aws-pulumi-post4.html">Part 4</a></p>

<h2 id="the-plan">The plan</h2>

<p>What will we need from AWS? It is going to be a million services or just a few? For now just one: the app will need an S3 bucket to place the transpiled JS files for Create React App.</p>

<p><img src="/docs/images/posts/2020-07-30-deploy-create-react-app-aws-pulumi/diagram.svg" alt="The plan" /></p>

<p>There are further wrinkles:</p>
<ul>
  <li>How to ensure an encrypted SSL connection?</li>
  <li>Which caching settings to use for the resources?</li>
  <li>How to scale the service in the future and how to monitor it?</li>
</ul>

<p>All these we’ll be covered in the series – we’ll be building up from the simplest shack to a more robust configuration.</p>

<h2 id="to-click-or-not-to-click">To click or not to click?</h2>

<p>What’s the gateway drug of AWS? It’s obviously the AWS console. 
Things are very easy to setup but once the complexity becomes larger, things get trickier. 
How trickier? Suppose you want to do a production environments but then also a staging and a testing environments. 
How would you apply a configuration change across all three environments? 
Well, unfortunately, it’s point and click with the AWS console.
We don’t want to do that, so we’ll use something else, a tool called Pulumi. 
<em>True infrastructure as code</em>.</p>

<h2 id="why-pulumi">Why Pulumi?</h2>

<p>Let’s start by explaining the choice of Pulumi since it’s a relatively new tool. 
A catchy (and provocative) summary would be:</p>

<blockquote>
  <p>Pulumi is React for infrastructure</p>
</blockquote>

<p>Instead of managing the infrastructure via the AWS console (easy to start, hard to manage), we will codify the infrastructure. 
The standard solution to this is using products such as CloudFormation or Terraform. 
These products are based on custom markup languages, you may have heard them referred to as “infrastructure as code”.
However, that’s not accurate. There is no real “code”, instead there is “markup” either as JSON or YAML. 
What does that mean? It means that it’s very difficult to use programming concepts you’re familiar with. 
For example, refactoring a Terraform files becomes a copy’n’paste bonanza.</p>

<p>So how does Pulumi help? The promise is that you can write a Pulumi program in a familiar language (JS, TypeScript, Python).
The Pulumi program can then be broken up, refactored, and unit tested – much like any other coding tool you’re familiar with. 
Declaring the infrastructure state you desire, much like you would declare React component structure you want rendered.</p>

<p>But how does a pulumi program look <strong>exactly</strong>? 
Foreshadowing is all the jazz, so why not try some of that here.
Here is pulumi snippet that creates an S3 bucket called <code class="language-plaintext highlighter-rouge">my-bucket</code> on AWS.</p>

<p><img src="/docs/images/posts/2020-07-30-deploy-create-react-app-aws-pulumi/carbon-s3-bucket.svg" alt="Pulumi example" /></p>

<h1 id="lets-get-our-hands-dirty">Let’s get our hands dirty</h1>

<p>Without further delay, let’s hit the road to a tech nirvana and get the answers you’ve all been looking for!</p>

<h2 id="starting-with-a-stock-create-react-app">Starting with a ‘stock’ Create React App</h2>

<p>So what’s the starting point of this journey? 
If you follow the create react app docs, you’ll see something like this</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">npx create-react-app my-app
cd my-app
npm build
</span></code></pre></div></div>

<p>This transpiles the sources and gives you a build/ directory – let’s have a look inside</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="nb">ls </span>build/
<span class="go">asset-manifest.json					logo512.png						service-worker.js
favicon.ico						manifest.json						static
index.html						precache-manifest.a6c522ff242ab9465073ffb9aae702c8.js
logo192.png						robots.txt
</span></code></pre></div></div>

<p>But what do you do with that? These are some interesting questions:</p>
<ul>
  <li>How do you get your precious creation into the cloud?</li>
  <li>How to ensure that as you push updates to your app clients get the latest version?</li>
</ul>

<p>There are some big questions there, in particular which <em>cache directives</em> to set for the files in build/. 
These settings will be super important for the browser. 
We’ll cover all of that later once we have the nuts and bolts ready.</p>

<p>Can these topics feel confusing and annoying? Hell yeah. 
Have you ever seen them covered in create react app documentation? Hell no.</p>

<h2 id="getting-setup-with-pulumi">Getting setup with Pulumi</h2>

<p>To avoid setting up resources in AWS console by hand, we’ll use Pulumi to write little programs that setup resources on the AWS cloud. 
These programs are declarative and can be written in any language of your choice, so it’s very easy to do.</p>

<blockquote>
  <p>What’s a Pulumi program? Program: a collection of files written in your chosen programming language</p>
</blockquote>

<p>Here we’ll be sticking with Typescript (consistent with create react app).</p>

<p>https://www.pulumi.com/docs/get-started/aws/begin/</p>

<p>What’s the first decision we need to make? 
It’s deciding where to put the Pulumi code managing our infrastructure. 
Let’s create a new directory ‘pulumi’, alongside the ‘build’ directory.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="nb">mkdir </span>pulumi
<span class="gp">$</span><span class="w"> </span><span class="nb">ls</span>
<span class="go">README.md	build		node_modules	package.json	public		pulumi		src		yarn.lock
</span><span class="gp">$</span><span class="w"> </span><span class="nb">cd </span>pulumi
</code></pre></div></div>

<p>Then let’s login into Pulumi, here we’ll us a local file to store the state of the project. 
This is okay for individual work and tinkering but gets insufficient once multiple people contribute to the project.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>pulumi login file://...
</code></pre></div></div>

<p>So why not get started and create a new Pulumi <strong>project</strong>? Here is the command you need to run and the expected output.</p>

<blockquote>
  <p>What’s a Pulumi project? Project is a directory containing a program, with metadata, so Pulumi knows how to run it</p>
</blockquote>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>pulumi new aws-typescript
<span class="go">This command will walk you through creating a new Pulumi project.

</span><span class="gp">Enter a value or leave blank to accept the (default), and press &lt;ENTER&gt;</span><span class="nb">.</span>
<span class="go">Press ^C at any time to quit.

project name: (pulumi) my-app
project description: (A minimal AWS TypeScript Pulumi program) 
Created project 'my-app'

stack name: (dev) 
Enter your passphrase to protect config/secrets: 
Re-enter your passphrase to confirm: 
Created stack 'dev'
</span></code></pre></div></div>

<p>Here is a little asciicast to show you how this step will look like.</p>

<p><a href="https://asciinema.org/a/356815"><img src="https://asciinema.org/a/356815.png" alt="asciicast" /></a></p>

<p>Now we need to configure our region of choice, here we’ll opt for <code class="language-plaintext highlighter-rouge">eu-west-1</code> but it doesn’t matter what you choose here (<code class="language-plaintext highlighter-rouge">us-west-1</code> or any other will do just fine).</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">pulumi config set aws:region eu-west-1
</span></code></pre></div></div>

<p>This command also creates our first <strong>stack</strong> – called “dev” – it will hold the state of the infrastructure we maintain.</p>

<blockquote>
  <p>What’s a Pulumi stack? Stack is an instance of your project, each often corresponding to a different cloud environment</p>
</blockquote>

<p>Well done, that’s how we setup a Pulumi and project boiler plate. 
Let’s create our first stack – it will hold the state of the infrastructure we maintain.</p>

<h2 id="defining-an-s3-bucket">Defining an S3 bucket</h2>

<p>Before we jump in, let’s have a look around what we got at this step. 
There should now be a first Pulumi program in index.ts, let’s have a look inside:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">pulumi</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/pulumi</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">aws</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/aws</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">awsx</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/awsx</span><span class="dl">"</span><span class="p">;</span>

<span class="c1">// Create an AWS resource (S3 Bucket)</span>
<span class="kd">const</span> <span class="nx">bucket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">s3</span><span class="p">.</span><span class="nx">Bucket</span><span class="p">(</span><span class="dl">"</span><span class="s2">my-bucket</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">acl</span><span class="p">:</span> <span class="dl">"</span><span class="s2">public-read</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">website</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">indexDocument</span><span class="p">:</span> <span class="dl">"</span><span class="s2">index.html</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
<span class="p">});</span>

<span class="c1">// Export the name of the bucket</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">bucketName</span> <span class="o">=</span> <span class="nx">bucket</span><span class="p">.</span><span class="nx">id</span><span class="p">;</span>
</code></pre></div></div>

<p>This is too simple for what we want to do <strong>eventually</strong> but good enough for now.</p>

<p>Let’s get this miniature to work – if there are any problems in your Pulumi config you’ll catch them now. 
What’s better than incremental progression, when you’re breaking new ground and trying not to get lost?</p>

<p>To get Pulumi to show you the available stacks (dev, production, testing), just run</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>pulumi stack <span class="nb">ls</span>
<span class="go">NAME  LAST UPDATE  RESOURCE COUNT
dev*  n/a          n/a
</span></code></pre></div></div>

<p>If you want to jump more into the Pulumi nomenclature, here is a great <a href="https://www.pulumi.com/docs/intro/concepts/programming-model/#program-structure">introduction</a></p>

<h2 id="creating-an-s3-bucket">Creating an S3 bucket</h2>

<p>Now we should be ready to get our first piece of infrastructure setup in Pulumi! 
To get Pulumi to create the stack on AWS, just run <code class="language-plaintext highlighter-rouge">pulumi up</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>pulumi up
<span class="go">Previewing update (dev):
     Type                 Name        Plan       
 +   pulumi:pulumi:Stack  my-app-dev  create     
 +   └─ aws:s3:Bucket     my-bucket   create     
 
Resources:
    + 2 to create

Do you want to perform this update? yes
Updating (dev):
     Type                 Name        Status      
 +   pulumi:pulumi:Stack  my-app-dev  created     
 +   └─ aws:s3:Bucket     my-bucket   created     
 
Outputs:
    bucketName: "my-bucket-6daefdf"

Resources:
    + 2 created

Duration: 11s

Permalink: file:///Users/jandom/.pulumi/stacks/dev.json
</span></code></pre></div></div>

<p>Pulumi says it’s all done – but can you trust it?
Heading over to AWS console, you should see the bucket created and confirm it has been created.</p>

<p><img src="/docs/images/posts/2020-07-30-deploy-create-react-app-aws-pulumi/bucket-created.png" alt="Bucket Created" /></p>

<h2 id="publishing-contents-to-the-s3-bucket">Publishing contents to the S3 bucket</h2>

<p>We have an S3 bucket but now let’s get the Create React App into it. 
How can we accomplish that? Well, we’ve got the build/ directory and we’ve got a bucket on S3. 
Let’s sync the contents of build with the S3 bucket using the <code class="language-plaintext highlighter-rouge">aws s3 cp</code> command</p>

<p>What’s in the newly bucket? Well, unsurprisingly, nothing.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>aws s3 <span class="nb">ls </span>s3://my-bucket-f01e841
</code></pre></div></div>

<p>Yup, nada. So let’s get some stuff in there! What could be easier?</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>aws s3 <span class="nb">cp </span>build/ s3://my-bucket-f01e841 <span class="nt">--recursive</span> <span class="nt">--exclude</span> <span class="k">*</span>.map
<span class="go">upload: build/favicon.ico to s3://my-bucket-f01e841/favicon.ico
upload: build/index.html to s3://my-bucket-f01e841/index.html  
upload: build/service-worker.js to s3://my-bucket-f01e841/service-worker.js
upload: build/robots.txt to s3://my-bucket-f01e841/robots.txt  
upload: build/manifest.json to s3://my-bucket-f01e841/manifest.json
upload: build/precache-manifest.a6c522ff242ab9465073ffb9aae702c8.js to s3://my-bucket-f01e841/precache-manifest.a6c522ff242ab9465073ffb9aae702c8.js
upload: build/asset-manifest.json to s3://my-bucket-f01e841/asset-manifest.json
upload: build/static/css/main.5f361e03.chunk.css to s3://my-bucket-f01e841/static/css/main.5f361e03.chunk.css
upload: build/logo192.png to s3://my-bucket-f01e841/logo192.png 
upload: build/logo512.png to s3://my-bucket-f01e841/logo512.png
upload: build/static/js/2.a430f49c.chunk.js.LICENSE.txt to s3://my-bucket-f01e841/static/js/2.a430f49c.chunk.js.LICENSE.txt
upload: build/static/js/main.4f4a69a4.chunk.js to s3://my-bucket-f01e841/static/js/main.4f4a69a4.chunk.js
upload: build/static/js/runtime-main.f8c5b4be.js to s3://my-bucket-f01e841/static/js/runtime-main.f8c5b4be.js
upload: build/static/media/logo.5d5d9eef.svg to s3://my-bucket-f01e841/static/media/logo.5d5d9eef.svg
upload: build/static/js/2.a430f49c.chunk.js to s3://my-bucket-f01e841/static/js/2.a430f49c.chunk.js
</span></code></pre></div></div>

<p>Now that was easy… but is that what we want? 
Well… Everything in <code class="language-plaintext highlighter-rouge">build</code> got published so that’s good news. 
We excluded <code class="language-plaintext highlighter-rouge">*.map</code> files which you may want to keep private.
Also what about the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control">caching settings</a> used by browsers? 
There is only one way to find out: with the swiss-army knife of all things web, cURL</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl https://my-bucket-f01e841.s3-eu-west-1.amazonaws.com/index.html
<span class="gp">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span><span class="w">
</span><span class="gp">&lt;Error&gt;</span>&lt;Code&gt;AccessDenied&lt;/Code&gt;&lt;Message&gt;Access Denied&lt;/Message&gt;&lt;RequestId&gt;079C0D51FEE2F8E1&lt;/RequestId&gt;&lt;HostId&gt;FPIAUu0YGJ1XEyCTuJPdSWiAQGBLkC7ftzbraMq4FchBUo7kEv8MTjoemmXumaiyIffhTv/ikMk<span class="o">=</span>&lt;/HostId&gt;&lt;/Error&gt;
</code></pre></div></div>

<p>Now that’s a funny thing: the S3 bucket contents are not available to us by default. They are private.
We can certainly change that!</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">aws s3 rm s3://my-bucket-f01e841/ --recursive

aws s3 cp build/ s3://my-bucket-f01e841 \
  --recursive \
  --exclude *.map \
  --exclude index.html \
  --cache-control max-age=31536000 \
  --acl public-read 

aws s3 cp build/index.html s3://my-bucket-f01e841/index.html \
  --metadata-directive REPLACE \
  --cache-control no-cache,no-store \
  --content-type text/html \
  --acl public-read
</span></code></pre></div></div>

<p>And with any luck, all of these files should upload to S3 with new cache-control headers. 
Let’s verify how things are working by requesting index.html</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl http://my-bucket-f01e841.s3-eu-west-1.amazonaws.com/index.html <span class="nt">-v</span>
<span class="c">...
</span><span class="go">&lt; Cache-Control: max-age=0,no-cache,no-store,must-revalidate
</span><span class="c">...
</span><span class="gp">&lt;!doctype html&gt;</span>&lt;html <span class="nv">lang</span><span class="o">=</span><span class="s2">"en"</span><span class="o">&gt;</span>&lt;<span class="nb">head</span><span class="o">&gt;</span>&lt;meta <span class="nv">charset</span><span class="o">=</span><span class="s2">"utf-8"</span>/&gt;&lt;<span class="nb">link </span><span class="nv">rel</span><span class="o">=</span><span class="s2">"icon"</span> <span class="nv">href</span><span class="o">=</span><span class="s2">"/favicon.ico"</span>/&gt;...
</code></pre></div></div>

<p>Importantly, the root page is also working (redirecting to <code class="language-plaintext highlighter-rouge">index.html</code>). 
We can verify that with cURL again</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>curl <span class="nt">-v</span> http://my-bucket-f01e841.s3-website-eu-west-1.amazonaws.com
<span class="go">* Rebuilt URL to: http://my-bucket-f01e841.s3-website-eu-west-1.amazonaws.com/
*   Trying 52.218.60.172...
* TCP_NODELAY set
</span><span class="gp">* Connected to my-bucket-f01e841.s3-website-eu-west-1.amazonaws.com (52.218.60.172) port 80 (#</span>0<span class="o">)</span>
<span class="gp">&gt;</span><span class="w"> </span>GET / HTTP/1.1
<span class="gp">&gt;</span><span class="w"> </span>Host: my-bucket-f01e841.s3-website-eu-west-1.amazonaws.com
<span class="gp">&gt;</span><span class="w"> </span>User-Agent: curl/7.54.0
<span class="gp">&gt;</span><span class="w"> </span>Accept: <span class="k">*</span>/<span class="k">*</span>
<span class="gp">&gt;</span><span class="w"> 
</span><span class="go">&lt; HTTP/1.1 200 OK
&lt; x-amz-id-2: VKg+mTQ8wGUge3GzLx9J1BN+cBUNmbQuixY95AZ4wgV4U7H9fKF3v8is1ms6tUTalGIEUMwEW+Y=
&lt; x-amz-request-id: 1ZAT1PAWEP0MFXAR
&lt; Date: Wed, 29 Jul 2020 19:11:31 GMT
&lt; Cache-Control: max-age=0,no-cache,no-store,must-revalidate
&lt; Last-Modified: Tue, 28 Jul 2020 19:57:17 GMT
&lt; ETag: "67e4d5da5073a0ba60ce72a01c3feee4"
&lt; Content-Type: text/html
&lt; Content-Length: 2219
&lt; Server: AmazonS3
&lt; 
</span><span class="gp">* Connection #</span>0 to host my-bucket-f01e841.s3-website-eu-west-1.amazonaws.com left intact
<span class="gp">&lt;!doctype html&gt;</span>&lt;html <span class="nv">lang</span><span class="o">=</span><span class="s2">"en"</span><span class="o">&gt;</span>&lt;<span class="nb">head</span><span class="o">&gt;</span>&lt;meta <span class="nv">charset</span><span class="o">=</span><span class="s2">"utf-8"</span>/&gt;
</code></pre></div></div>

<h2 id="caching-settings">Caching settings</h2>

<p>Following this StackOverflow thread, we’ll follow similar defaults https://stackoverflow.com/questions/49604821/cache-busting-with-cra-react</p>

<blockquote>
  <p>Using Cache-Control: max-age=31536000 for your build/static assets, and Cache-Control: no-cache for everything else is a safe and effective starting point that ensures your user’s browser will always check for an updated index.html file, and will cache all of the build/static files for one year. Note that you can use the one year expiration on build/static safely because the file contents hash is embedded into the filename.</p>
</blockquote>

<p>This is a much wider topic and we’ll only stick to the simplest solution. 
Searching around for best practices might give you some ideas for what to do depending on your situation.</p>

<h1 id="conclusions">Conclusions</h1>

<p>That brings us to a conclusion, we have a rudimentary setup for hosting a Create React App. 
With a single bucket we can serve contents using HTTP requests. 
This is a far cry from what we want, it’s hard to expect users to access your website by the bucket URL!
What’s next in this series? Well, we need to connect the S3 bucket to a Route53 record. 
Then, some considerations about load and caching will follow, showing how CloudFront can be used to cache the contents of your S3 bucket. 
But that’ll all come later!</p>

<h2 id="whats-next">What’s next?</h2>

<p>This was a simple intro to publishing a Create React App to AWS S3 via Pulumi. 
It’s rudimentary and not suitable for a production-level workload. 
Which components to add next? Checkout the second blog post in the series on how to connect Route 53 with our Create React App.</p>

<h2 id="credits">Credits</h2>

<p>Big big thanks to my colleagues</p>

<ul>
  <li><a href="https://www.linkedin.com/in/charlie-shepherd-82946656/">Charlie “Cloud Wizard” Shepherd</a></li>
  <li><a href="https://www.linkedin.com/in/giulio-cirnigliaro-038602161">Giulio Cirnigliaro</a></li>
</ul>

<p>For helping me review and improve this post.</p>]]></content><author><name></name></author><category term="pulumi" /><category term="aws" /><category term="react" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Rescuing a decade old scientfic database</title><link href="/vagrant/pulumi/science/2020/03/13/rescuing-a-decade-old-scientific-database-post.html" rel="alternate" type="text/html" title="Rescuing a decade old scientfic database" /><published>2020-03-13T23:00:00+00:00</published><updated>2020-03-13T23:00:00+00:00</updated><id>/vagrant/pulumi/science/2020/03/13/rescuing-a-decade-old-scientific-database/post</id><content type="html" xml:base="/vagrant/pulumi/science/2020/03/13/rescuing-a-decade-old-scientific-database-post.html"><![CDATA[<h1 id="intro">Intro</h1>

<p>How do you know you’re old in tech? The tell-tale sign is that your new projects and up being about saving your old projects.
How old? Like DECADES old. But I’m getting ahead of myself here… let’s introduce our characters.</p>

<h1 id="how-did-we-get-here">How did we get here?</h1>

<p>When I was a wee little lad, my first encounter with programming was as a biochemistry student, staying over the summer at Oxford.
Funded by Wellcome Trust, that summer thrust me into the tech trajectory that I stayed on for over 10 years now.
What was the mission?
Researchers typically stored the parameter files on random FTP servers or shared them via email.
Our task was to build a database of lipid parameters for molecular dynamics simulation.</p>

<p>Having no prior coding experience, what could you expect from a student?
The product was a database called Lipidbook, a simple CMS-like app for researchers to upload these parameters and link them to their papers (for proper citation by their peer) .
The summer ended, a paper got written up and that’s it. End of story, right?
That’s the reality it in terms of science sustainability for software development: there often is funding to develop new tools but once it’s out there… the funding situations becomes more “dire”.</p>

<h1 id="the-troubles-begin">The troubles begin</h1>

<p>With the Lipidbook published the researcher started to publish stuff to it.
Not like millions of files but enough to make the tool valuable to the community.
People not only started depositing but also citing the paper.
So far it’s my 2nd most cited paper of all time: a software tool paper that overshadowed other “actual” science papers.</p>

<p><img src="/docs/images/posts/2020-03-14-rescuing-a-decade-old-scientific-database/google-scholar.png" alt="Lipidbook paper on my google scholar profile" /></p>

<p>Remarkably, the system held with little human intervention.
For nearly a decade the website run of a mac mini in a cupboard in Oxford.
Yeah, sometimes it needed a reboot but beyond that, smooth sailing!
Everything was kind of “MVP” (minimal viable product) with bash scripts and cron jobs running support functions.
The real crisis began when the people running the hardware moved out of the lab.
Without anybody to occasionally press the reset button, or without any SSH access, we were stranded.
The website was down for days, then weeks then months.
People kept messaging us but we simply didn’t have the time.</p>

<h1 id="how-can-we-fix-this">How can we fix this?</h1>

<p>The impetus came from Phil Stansfeld and Oliver Beckstein, both mentors of mine, who equally facilitated “good decisions” and suppressed “questionable choices”.
What should we do to get this service up and running without a tremendous amount of work? Where should we host it?
Typically, academia prefers to host things in-house, rather than on AWS.
Why is that strange choice, you might ask?
Comes down to the funding model: once you get your grant money in science, you can buy a lot of hardware, but running that hardware will largely be done by the university.
Even with your grant money running low, you don’t have to cut down on usage.
It’s really unfortunate that funding bodies haven’t changed their model to include year-long support costs.
That wouldn’t be the case with AWS, billed per hour, as you can imagine.
Despite that, Oliver wanted to push ahead with AWS. This project is an interesting example of using AWS in a cost-efficient way for science infrastructure.
But how do we yank Lipidbook from a single MacMini to the cloud? Enter Pulumi and AWS.</p>

<p>AWS needs no introduction, it’s the mother-of-clouds, the <a href="https://aws.amazon.com">Amazon Web Services</a>.
<a href="https://www.pulumi.com">Pulumi</a> is tool that allows you to manipulate and manage AWS resources via declarative programs that specify the desired end-state of your infrastructure.</p>

<p><img src="/docs/images/posts/2020-03-14-rescuing-a-decade-old-scientific-database/mac-mini.jpg" alt="From a Mac mini into the cloud" /></p>

<h1 id="assembling-a-web-app">Assembling a web app</h1>

<p>Will this be a primer on Pulumi or AWS? Probably not but what I’ll attempt to do here is to outline how these pieces fit together.
What’s the first thing that comes to mind? We’ll probably need an EC2 instance on AWS to host the webserver and all.
But let’s take a step back and start by putting together our networking layer, and the boilerplate on which the app will stand.</p>

<p><strong>BEWARE</strong></p>

<p>All examples given here will be in Typescript, the programming language that is the love child of the web in 2020. Pulumi comes with a variety of other SDKs, including Python, so you’re free to choose between them.</p>

<h2 id="networking-layer">Networking layer</h2>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">awsx</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/awsx</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">vpc</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">awsx</span><span class="p">.</span><span class="nx">ec2</span><span class="p">.</span><span class="nx">Vpc</span><span class="p">(</span><span class="dl">"</span><span class="s2">custom</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">numberOfAvailabilityZones</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div></div>

<p>This will create our VPC with public and private subnets. VPC will ensure the isolation of all the services inside. Single availabilility zone to keep the costs super-duper low.</p>

<h2 id="backup-bucket">Backup bucket</h2>

<p>What’s the next useful thing to add? The Lipidbook database needs to be backed up regularly, so let’s create an S3 bucket to hold the backups</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">aws</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pulumi/aws</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">backupBucket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">s3</span><span class="p">.</span><span class="nx">Bucket</span><span class="p">(</span><span class="dl">"</span><span class="s2">backup-bucket</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">bucket</span><span class="p">:</span> <span class="nx">config</span><span class="p">.</span><span class="nx">backupBucket</span><span class="p">,</span>
    <span class="na">serverSideEncryptionConfiguration</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">rule</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">applyServerSideEncryptionByDefault</span><span class="p">:</span> <span class="p">{</span>
                <span class="na">sseAlgorithm</span><span class="p">:</span> <span class="dl">'</span><span class="s1">AES256</span><span class="dl">'</span><span class="p">,</span>
            <span class="p">},</span>
        <span class="p">},</span>
    <span class="p">},</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Notice that the bucket contents get encrypted, because why not!</p>

<h2 id="securitygroups">SecurityGroups</h2>

<p>The EC2 instance needs to know on which ports to accept the connections, we probably want to keep 443 and 80 ports open. To define that behavior, we create a SecurityGroup</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">securityGroup</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">ec2</span><span class="p">.</span><span class="nx">SecurityGroup</span><span class="p">(</span>
    <span class="dl">"</span><span class="s2">security-group</span><span class="dl">"</span><span class="p">,</span>
    <span class="nx">createSecurityGroupArgs</span><span class="p">(</span><span class="nx">config</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">createSecurityGroupArgs</code> is a helper function to create the SecurityGroup configuration that you need. The details are left to the eager reader!</p>

<h2 id="keypair-access">KeyPair access</h2>

<p>Next, we’ll need a KeyPair to access our EC2 instance via SSH and do servicing/maintenance on it.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ssh-keygen <span class="nt">-t</span> rsa <span class="nt">-f</span> rsa
</code></pre></div></div>

<p>We then import the publicKey part</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">keyPair</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">ec2</span><span class="p">.</span><span class="nx">KeyPair</span><span class="p">(</span>
    <span class="dl">"</span><span class="s2">key-pair</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">keyName</span><span class="p">:</span> <span class="s2">`lipidbook-</span><span class="p">${</span><span class="nx">config</span><span class="p">.</span><span class="nx">environmentName</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
        <span class="na">publicKey</span><span class="p">:</span> <span class="nx">config</span><span class="p">.</span><span class="nx">publicKey</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="amazon-machine-image">Amazon Machine Image</h2>

<p>Lastly, we need to get an amazon machine image for our Ubuntu version up and running</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">size</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">t1.micro</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">ami</span> <span class="o">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">getAmi</span><span class="p">({</span>
    <span class="na">filters</span><span class="p">:</span> <span class="p">[{</span>
        <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">values</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">ubuntu/images/hvm-ssd/ubuntu-trusty-****</span><span class="dl">"</span><span class="p">],</span>
    <span class="p">}],</span>
    <span class="na">owners</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">099720109477</span><span class="dl">"</span><span class="p">],</span> <span class="err">#</span> <span class="nx">canonical</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="getting-the-webserver">Getting the webserver</h2>

<p>All the pieces are assembled now: we have the VPC for networking, we have a security group and the machine image. Let’s get the final piece assembled!</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">ec2</span><span class="p">.</span><span class="nx">Instance</span><span class="p">(</span><span class="dl">"</span><span class="s2">webserver-www</span><span class="dl">"</span><span class="p">,</span>
    <span class="nx">createEC2Args</span><span class="p">(</span><span class="nx">config</span><span class="p">,</span> <span class="nx">vpc</span><span class="p">,</span> <span class="nx">size</span><span class="p">,</span> <span class="nx">securityGroup</span><span class="p">,</span> <span class="nx">ami</span><span class="p">),</span>
<span class="p">);</span>
</code></pre></div></div>

<p>Now obviously this is just a blank EC2 box. It still needs to be provisioned to run our app. This is beyond the scope of this post but we’ve used Hashicorps Vagrant for that.</p>

<h2 id="elastic-ip">Elastic IP</h2>

<p>The EC2 instance we’ve created does have a public IP but it keeps changing between each machine spin-up. To get our DNS setting working we’ll need an elastic IP.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">elasticIp</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">ec2</span><span class="p">.</span><span class="nx">Eip</span><span class="p">(</span>
    <span class="dl">"</span><span class="s2">elastic-ip</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">{</span>
        <span class="na">instance</span><span class="p">:</span> <span class="nx">server</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
    <span class="p">}</span>
<span class="p">);</span>
</code></pre></div></div>

<p>No matter what the instance public IP is, this ElasticIP will not change and can be used to configure the DNS.</p>

<h2 id="dns-configuration">DNS Configuration</h2>

<p>This is the last piece. Again, I’m omitting the details of <code class="language-plaintext highlighter-rouge">createARecordArgs</code> and <code class="language-plaintext highlighter-rouge">createCNAMERecordArgs</code>.
There are obviously important details… But let’s allow the big picture to take priority.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">record</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">route53</span><span class="p">.</span><span class="nb">Record</span><span class="p">(</span>
    <span class="dl">"</span><span class="s2">record</span><span class="dl">"</span><span class="p">,</span>
    <span class="nx">createARecordArgs</span><span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">targetDomain</span><span class="p">,</span> <span class="nx">elasticIp</span><span class="p">),</span>
<span class="p">);</span>
</code></pre></div></div>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">aliasRecord</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">route53</span><span class="p">.</span><span class="nb">Record</span><span class="p">(</span>
    <span class="dl">"</span><span class="s2">alias-record</span><span class="dl">"</span><span class="p">,</span>
    <span class="nx">createCNAMERecordArgs</span><span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">targetDomain</span><span class="p">,</span> <span class="nx">config</span><span class="p">.</span><span class="nx">aliasDomain</span><span class="p">),</span>
<span class="p">);</span>
</code></pre></div></div>

<h1 id="future-work">Future work</h1>

<p>There is a number of things here that I’m not happy about and could be improved:</p>

<ol>
  <li>
    <p>With the app we’re running, given it’s age, I see no incremental migration path. It’s a re-write and these don’t get funded.</p>
  </li>
  <li>
    <p>The EC2 instance should ideally be in a private subnet, all HTTP requests should go via a LoadBalancer, and direct access should be possible only via SSH hopping over a bastion box.</p>
  </li>
  <li>
    <p>The database is hosted directly on the EC2 instance, ideally, it should run on its own RDS instance. So that when the EC2 goes down the database is not lost.</p>
  </li>
</ol>

<h1 id="closing">Closing</h1>

<p>Amazon Web Services model has not been super popular in academia. The reasons are probably related to funding structure and grants.
However, for running scientific infrastructure that often needs to be maintained for years, it is a suitable choice.
The entire cost for us to run this operation is 35$ / month, aka peanuts.
Using tools such a pulumi, the infrastructure can be quickly spun-up and modified.
The pulumi program presented here was written in typescript but SDKs for most popular languages exist (including Python).
The scientific community could run many more pieces of infrastructure for less, in a more maintainable fashion using cloud providers and declarative infrastructure.</p>]]></content><author><name></name></author><category term="vagrant" /><category term="pulumi" /><category term="science" /><summary type="html"><![CDATA[Intro]]></summary></entry></feed>