<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Adam Ross</title>
    <description>The latest articles on DEV Community by Adam Ross (@grayside).</description>
    <link>https://dev.to/grayside</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F279678%2Ff5ceef7d-df32-4869-8540-05984f6b72aa.jpeg</url>
      <title>DEV Community: Adam Ross</title>
      <link>https://dev.to/grayside</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/grayside"/>
    <language>en</language>
    <item>
      <title>How My Team Aligns on Prompting for Production</title>
      <dc:creator>Adam Ross</dc:creator>
      <pubDate>Tue, 17 Feb 2026 16:53:46 +0000</pubDate>
      <link>https://dev.to/googlecloud/how-my-team-aligns-on-prompting-for-production-1lpf</link>
      <guid>https://dev.to/googlecloud/how-my-team-aligns-on-prompting-for-production-1lpf</guid>
      <description>&lt;p&gt;My team at Google is &lt;a href="https://cloud.google.com/blog/topics/developers-practitioners/7-technical-takeaways-from-using-gemini-to-generate-code-samples-at-scale" rel="noopener noreferrer"&gt;automating sample code generation and maintenance&lt;/a&gt;. Part of that is using Generative AI to produce and assess instructional code. This introduces a challenge: How do we trust the system to meet our specific standards, when core components are non-deterministic?&lt;/p&gt;

&lt;p&gt;Establishing trust requires isolating and understanding each &lt;a href="https://cloud.google.com/ai/llms" rel="noopener noreferrer"&gt;large language model (LLM)&lt;/a&gt; request. We need to know exactly what goes into the model, and a guarantee of what comes out.&lt;/p&gt;

&lt;p&gt;This challenge isn't different from other feature development. To succeed, we realized we had to stop treating prompting like chatting or guessing and start treating it like coding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Natural Language is a Fuzzy Programming Language
&lt;/h2&gt;

&lt;p&gt;Prompting an LLM is effectively "&lt;a href="https://ai.google.dev/gemini-api/docs/prompting-strategies" rel="noopener noreferrer"&gt;natural language programming&lt;/a&gt;": we are programming in English. The problem is that English is not the greatest language for programming. It is ambiguous. It is subjective. It is open to interpretation.&lt;/p&gt;

&lt;p&gt;In C++, a missing semicolon breaks the build. In English, a missing comma changes the objective entirely: &lt;a href="https://en.wikipedia.org/wiki/Vocative_case" rel="noopener noreferrer"&gt;&lt;em&gt;"I don't know, John"&lt;/em&gt; becomes &lt;em&gt;"I don't know John"&lt;/em&gt;&lt;/a&gt;. In a programming language, syntax is binary; it works or it doesn't. In English, the difference between "Ensure variables are immutable" and "Make sure variables never change" might yield different results based on the model’s training data.&lt;/p&gt;

&lt;p&gt;When you combine the fuzziness of human language with the "black box" probabilistic processing of an LLM, you face a difficult question: &lt;em&gt;What is the weather going to be today in the land of AI?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To answer that, you have to make the intentions behind your prompts explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be Efficient with Brains (Pair &amp;amp; Review)
&lt;/h2&gt;

&lt;p&gt;Writing a prompt is an exploratory process of finding words that trigger the best response. However, a single writer is limited by their own understanding. This is risky with LLMs, which are suited for ambiguous problems but require strict guardrails.&lt;/p&gt;

&lt;p&gt;Relying on one person to do this creates blind spots. We found that prompt quality benefits from &lt;a href="https://martinfowler.com/articles/on-pair-programming.html" rel="noopener noreferrer"&gt;pairing&lt;/a&gt;. A diversity of thought helps create a more complete definition of any problem. What one engineer considers a clear instruction, another might see as a loophole. Pairing covers the gaps that a single brain might miss.&lt;/p&gt;

&lt;p&gt;Furthermore, you should also review every prompt. This isn't just checking for typos; it’s a logic check. Does this prompt align with business requirements? &lt;strong&gt;We’ve found that prompt reviews often uncover disagreements about the requirements themselves, forcing us to align as a team before we ship.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Document "The Why" and manage change
&lt;/h2&gt;

&lt;p&gt;Because English is fuzzy, the intent behind a specific word choice isn't always obvious. Why did we use the passive voice here? Why did we specify "immutable"?&lt;/p&gt;

&lt;p&gt;Even well-structured prompts can eventually obscure the original business requirements. As optimizations blur into the general text, we must take every opportunity to document "the why":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; Avoid relying on prompts as canonical business requirements. Our LLM requests are a combination of system instructions, user input, context, and deterministic post-processing; the prompt is not complete for onboarding a developer.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Comments:&lt;/strong&gt; Comment on complex prompts just as you would complex code. Spotlight specific constraints or even punctuation to explain the problem they solve. The model is a moving target, so any unintentional changes can make troubleshooting hard.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Commit Messages:&lt;/strong&gt; Use commit messages as an opportunity to explain what was wrong with the prompt. (for example, &lt;code&gt;fixed: Missing comma lost John&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Separation of Concerns (Use Dedicated Files)
&lt;/h2&gt;

&lt;p&gt;Writing code and writing prompts require distinct mindsets. One focuses on syntax and execution flow; the other on semantics and intent. &lt;a href="https://medium.com/@rexnino/what-is-separation-of-concerns-in-coding-and-why-is-it-important-731aa8cfa898" rel="noopener noreferrer"&gt;Embedding long English instructions inside code creates a distraction&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We keep prompts in dedicated files to disentangle application logic from the LLM interaction configuration, which requires frequent tuning.&lt;/p&gt;

&lt;p&gt;By treating the prompt as a standalone component, we can prototype and iterate on the LLM behavior independent of the application's control flow. &lt;a href="https://google.github.io/dotprompt/" rel="noopener noreferrer"&gt;Tools like dotprompt&lt;/a&gt; allow us to treat these files as first-class artifacts containing text, model parameters, and schema definitions. This highlights that invoking a model isn't just a function call; it’s an integration with a distinct system that requires its own configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Structured Output
&lt;/h2&gt;

&lt;p&gt;To build a reliable tool, you need a bridge between unpredictable LLM output and deterministic computers.&lt;/p&gt;

&lt;p&gt;We rely on &lt;a href="https://genkit.dev/docs/models/#structured-output" rel="noopener noreferrer"&gt;structured output&lt;/a&gt;&lt;sup id="fnref1"&gt;1&lt;/sup&gt; to guide the model to emit JSON according to a schema. Even if we only need a single field, defining a schema provides a guardrail that helps the model output conform to a shape we can validate programmatically. This is critical for code generation, where models often add unwanted preambles, conversational filler, or inconsistent markdown fences.&lt;/p&gt;

&lt;p&gt;If the output doesn't match the schema, we fail fast or retry. This allows us to integrate the LLM output into our process with the same confidence we have in detecting a bad API response.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Magic to Engineering
&lt;/h2&gt;

&lt;p&gt;Moving from one successful prompting to a reliable system requires acknowledging that prompts are code. You need to manage, review, and test them with the same rigor applied to the rest of your stack. While we are still working on better ways to &lt;a href="https://www.statsig.com/perspectives/what-are-non-deterministic-ai-outputs-" rel="noopener noreferrer"&gt;benchmark quality&lt;/a&gt;, treating our prompts as first-class codebase assets is our first step towards building confidence in our AI assisted automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Join the conversation
&lt;/h2&gt;

&lt;p&gt;I'm curious how you are handling the fuzzy nature of LLMs in production.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How does your team review and test prompts?
&lt;/li&gt;
&lt;li&gt;Do you treat prompts as configuration, code, or something else entirely?
&lt;/li&gt;
&lt;li&gt;Share your workflow in the comments.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Read More
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/googlecloud/the-lumberjack-paradox-from-theory-to-practice-2lb5"&gt;The lumberjack paradox: From theory to practice&lt;/a&gt; by Jennifer Davis&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://medium.com/google-cloud/google-sandwich-manager-and-the-hallucinated-sdk-6bed653e6318" rel="noopener noreferrer"&gt;Google Sandwich Manager, and the hallucinated SDK&lt;/a&gt; by Katie McLaughlin and Brian Dorsey&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Thanks to &lt;a href="https://dev.to/sigje"&gt;Jennifer Davis&lt;/a&gt; &amp;amp; &lt;a href="https://shawnmjones.org/" rel="noopener noreferrer"&gt;Shawn Jones&lt;/a&gt; for review and contributions.&lt;br&gt;
Cover Photo by &lt;a href="https://unsplash.com/@jeton7?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Jeton Bajrami&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/a-group-of-people-rowing-a-boat-in-the-water-d4e2mitxgsE?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;This links to how structured output works in the Genkit framework, which my team is using. A succinct example. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>productivity</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Adam Ross</dc:creator>
      <pubDate>Wed, 19 Nov 2025 00:43:10 +0000</pubDate>
      <link>https://dev.to/grayside/-9kb</link>
      <guid>https://dev.to/grayside/-9kb</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/googlecloud/the-lumberjack-paradox-from-theory-to-practice-2lb5" class="crayons-story__hidden-navigation-link"&gt;The lumberjack paradox: From theory to practice&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/googlecloud"&gt;
            &lt;img alt="Google Cloud logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F809%2Fc7814399-cf4a-4dc9-9f12-d0a97ed21bf6.png" class="crayons-logo__image"&gt;
          &lt;/a&gt;

          &lt;a href="/sigje" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F150821%2F75e6e1cd-7d12-4a70-9ece-de03f308349b.JPG" alt="sigje profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/sigje" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Jennifer Davis
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Jennifer Davis
                
              
              &lt;div id="story-author-preview-content-3032696" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/sigje" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F150821%2F75e6e1cd-7d12-4a70-9ece-de03f308349b.JPG" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Jennifer Davis&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/googlecloud" class="crayons-story__secondary fw-medium"&gt;Google Cloud&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/googlecloud/the-lumberjack-paradox-from-theory-to-practice-2lb5" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 19 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/googlecloud/the-lumberjack-paradox-from-theory-to-practice-2lb5" id="article-link-3032696"&gt;
          The lumberjack paradox: From theory to practice
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cloud"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cloud&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/googlecloud/the-lumberjack-paradox-from-theory-to-practice-2lb5" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;12&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/googlecloud/the-lumberjack-paradox-from-theory-to-practice-2lb5#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              1&lt;span class="hidden s:inline"&gt; comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




</description>
      <category>cloud</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Write to Google Sheets from a local script via gcloud CLI authentication</title>
      <dc:creator>Adam Ross</dc:creator>
      <pubDate>Wed, 25 Sep 2024 22:16:33 +0000</pubDate>
      <link>https://dev.to/googlecloud/write-to-google-sheets-from-a-local-script-via-gcloud-cli-authentication-3p1f</link>
      <guid>https://dev.to/googlecloud/write-to-google-sheets-from-a-local-script-via-gcloud-cli-authentication-3p1f</guid>
      <description>&lt;p&gt;Recently I needed to pull data from the GitHub API and publish to a Google Sheet so I could share some charts about code review workload. This post is about how I got authentication working.&lt;/p&gt;

&lt;p&gt;I wrote a Node.js script because my use case was too complex for BASH and seemed too temporary for Go. Initially, the script generated ad hoc CSV data that I could manually copy into Google Sheets using the Paste-as-CSV feature. After a few rounds of manually copying, I wanted to use my time wisely: I estimated that I could get a Sheets integration working within a couple hours and if so that would&lt;br&gt;
&lt;a href="https://dev.to/grayside/why-automate-1fac"&gt;probably pay off within a few months&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Aside:&lt;/strong&gt; Sheets isn't my database. It's my data reporting UI. &lt;em&gt;Don't use Sheets as your database.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It took me closer to 4 hours to get this working, because authentication is hard. This post shows the more direct path to a working solution.&lt;/p&gt;

&lt;p&gt;It's easy to be distracted with the complexity of &lt;a href="https://developers.google.com/workspace/guides/configure-oauth-consent" rel="noopener noreferrer"&gt;creating an oAuth application&lt;/a&gt;, and while I especially like &lt;a href="https://developers.google.com/workspace/guides/create-credentials#service-account" rel="noopener noreferrer"&gt;using a service account&lt;/a&gt; from my Cloud services, that's&lt;br&gt;
&lt;a href="https://cloud.google.com/sdk/docs/authorizing#service-account" rel="noopener noreferrer"&gt;less convenient when running locally&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The trick I learned is that the gcloud CLI's setup of &lt;a href="https://cloud.google.com/docs/authentication/application-default-credentials" rel="noopener noreferrer"&gt;application default credentials&lt;/a&gt; can operate as a sort of OAuth proxy for Google Workspace code, by expanding how it authenticates your account with Google to include some more permissions (OAuth Scopes).&lt;/p&gt;
&lt;h3&gt;
  
  
  🔐 Local authentication with gcloud CLI
&lt;/h3&gt;

&lt;p&gt;To make API requests to Google Sheets, enable the Sheets API in your Cloud project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&amp;gt;&lt;/span&gt; gcloud services &lt;span class="nb"&gt;enable &lt;/span&gt;sheets.googleapis.com

Operation &lt;span class="s2"&gt;"operations/acat.p2-480745230567-02564c8d-c6ba-4f60-90bd-13f33e41f0fe"&lt;/span&gt; finished successfully.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set your &lt;strong&gt;application default credentials&lt;/strong&gt;, also claiming some non-default OAuth scopes so the credential can be used with sheets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&amp;gt;&lt;/span&gt; gcloud auth application-default login &lt;span class="nt"&gt;--scopes&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="s1"&gt;'https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/spreadsheets'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will trigger an OAuth flow that involves visiting a webpage in your browser. Depending on the terminal configuration this may display a URL or even open the page.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When gcloud is granted this scope, code that can access your local credentials will have read/write access to all your Google Sheets data. You can re-run this command without the customized scopes to toggle that access on and off as needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  🗝️ Initialize a Node client for Google Sheets
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;googleapis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sheetsClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GoogleAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt;     &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt;     &lt;span class="c1"&gt;// Only 'spreadsheets' scope is needed in the code.&lt;/span&gt;
&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;// gcloud CLI also needs 'cloud-platform' and 'drive'.&lt;/span&gt;
&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.googleapis.com/auth/spreadsheets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;authConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sheets&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🖇 Append data to the sheet
&lt;/h3&gt;

&lt;p&gt;The sample from the docs on how to &lt;a href="https://developers.google.com/sheets/api/guides/values#append_values" rel="noopener noreferrer"&gt;append data to the sheet&lt;/a&gt;&lt;br&gt;
(click on Node.js tab) worked well. However, I couldn't understand how to get authentication working from there. The sample worked once I understood the trick above to add the missing OAuth scopes to my dev environment credentials.&lt;/p&gt;

&lt;p&gt;I made a few changes to enable easier reuse of the client, parameterize the request differently, and emphasize how I wanted the data handled by Sheets.&lt;/p&gt;

&lt;p&gt;My code to append data to the sheet, leveraging the client initialization code above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;appendDataToSheet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;spreadsheetId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sheetsClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spreadsheets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="nx"&gt;spreadsheetId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!A2:AG`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="c1"&gt;// Use my data as provided.&lt;/span&gt;
            &lt;span class="na"&gt;valueInputOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RAW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="c1"&gt;// Inserts rows as part of appending to reduce overwrites.&lt;/span&gt;
            &lt;span class="na"&gt;insertDataOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT_ROWS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;requestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedCells&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; cells appended.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Show the error, do not stop. Cross-reference the error with terminal output&lt;/span&gt;
        &lt;span class="c1"&gt;// and decide case-by-case to re-run the script or manually copy data.&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm using "append" because my script collects monthly metrics, and append allows me to add new rows without removing earlier rows.&lt;/p&gt;

&lt;p&gt;Here's an example of how to call the &lt;code&gt;appendDataToSheet()&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="c1"&gt;// Each nested array is a spreadsheet row.&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;luggage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;N/A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sticks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nf"&gt;appendDataToSheet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HPDkfqdu6rfIq5-4uTGDqz2tvmPxDZMul27JFexample&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Exported Data Tab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;values&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are some good tips about working with the &lt;a href="https://developers.google.com/sheets/api/troubleshoot-api-errors" rel="noopener noreferrer"&gt;Sheets API in the docs&lt;/a&gt; such as the suggestion not to send more than one API request per second per sheet. I found out the hard way: overwriting data from a first request with data from a second.&lt;/p&gt;

&lt;h3&gt;
  
  
  🚀 Ship it!
&lt;/h3&gt;

&lt;p&gt;If I move forward to productionize this, I might switch to using&lt;br&gt;
&lt;a href="https://cloud.google.com/scheduler/docs" rel="noopener noreferrer"&gt;Cloud Scheduler&lt;/a&gt; and &lt;a href="https://cloud.google.com/run/docs/quickstarts/jobs/build-create-nodejs" rel="noopener noreferrer"&gt;Cloud Run Jobs&lt;/a&gt;. Let me know if you'd like to read about that.&lt;/p&gt;

&lt;p&gt;Cover Photo by &lt;a href="https://unsplash.com/@albovsky?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Glib Albovsky&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/an-aerial-view-of-a-green-field-ClSDm_cFdOo?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>automation</category>
      <category>javascript</category>
      <category>googlecloud</category>
    </item>
    <item>
      <title>Where to Host My Static Tech Blog</title>
      <dc:creator>Adam Ross</dc:creator>
      <pubDate>Tue, 13 Aug 2024 22:10:02 +0000</pubDate>
      <link>https://dev.to/googlecloud/where-to-host-my-static-tech-blog-2d8m</link>
      <guid>https://dev.to/googlecloud/where-to-host-my-static-tech-blog-2d8m</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Context: I'm a decision record enthusiast and will absolutely write more about this in the future. This post format is inspired by &lt;a href="https://adr.github.io/" rel="noopener noreferrer"&gt;Architectural Decision Records&lt;/a&gt; and is meant to show how I got to my hosting solution, not how to use it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Status:&lt;/strong&gt; [&lt;del&gt;thinking&lt;/del&gt; | &lt;del&gt;nope&lt;/del&gt; | &lt;strong&gt;current&lt;/strong&gt; | &lt;del&gt;superceded by &lt;/del&gt;]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Last Updated:&lt;/strong&gt; Mar 9, 2024&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Objective:&lt;/strong&gt; Choose a web host for my static site&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Context &amp;amp; Problem Statement
&lt;/h3&gt;

&lt;p&gt;To have a website, there must be a web host. A website is the visual artifact that everyone sees, the pages, images and videos. The web host is a service provides the compute, storage, networking, and all the bits that make it usable. For my personal website, I need to choose a web host that adheres to the following priorities and constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Priorities &amp;amp; Constraints
&lt;/h3&gt;

&lt;p&gt;Every project has requirements, here are a few of mine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Be low cost:&lt;/strong&gt; My personal website is a personal expense I'd rather keep minimal. My goal is to fit within a free tier, but not to exceed a "dollars per month" cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be focused on publishing over development:&lt;/strong&gt; My time is immensely valuable. I want to publish content with some flexibility to tailor for my way of working. However, I do not want to generate (too many) new side projects in web development instead of writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be low attention:&lt;/strong&gt; I want to ignore this site for months at a time without incurring security risks or a backlog of infrastructure projects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal Service Providers:&lt;/strong&gt; I prefer to use the fewest number of service providers for my personal IT. I have GitHub, DNS, and Google Cloud already in the mix. Each additional company is another credit card profile to update and set of corporate customer policies to track.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learn from Everything:&lt;/strong&gt; Balance existing skills with learning opportunities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sourcing from GitHub:&lt;/strong&gt; I've decided to keep my site source and content in a public GitHub repository. I want to see merges to my &lt;code&gt;main&lt;/code&gt; branch deploy to a live site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraint:&lt;/strong&gt; I've previously chosen Hugo and Go with code hosted on GitHub as tools for building the site. Hugo is a static site generator flexible enough to be used across many (all?) hosting services.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Considered Options
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Option 1:&lt;/strong&gt; Use &lt;a href="https://firebase.google.com/docs/hosting" rel="noopener noreferrer"&gt;Firebase Hosting&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option 2:&lt;/strong&gt; Use &lt;a href="https://pages.github.com/" rel="noopener noreferrer"&gt;GitHub Pages&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option 3:&lt;/strong&gt; Use &lt;a href="https://cloud.google.com/run/docs/" rel="noopener noreferrer"&gt;Google Cloud Run&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option 4:&lt;/strong&gt; Use &lt;a href="https://www.netlify.com/" rel="noopener noreferrer"&gt;Netlify&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision
&lt;/h2&gt;

&lt;p&gt;Chosen option &lt;strong&gt;Option 1: Firebase Hosting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Firebase Hosting is a specialized tool for static site hosting, providing ease of deployment, a global CDN, and a good free tier. I have a lot of familiarity with Google Cloud, but less familiarity with the Firebase products.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Working for Google means my day job pays me to be familiar with Google Cloud, which may lead me to use Google Cloud for more personal projects. This gives Firebase Hosting some advantage in the decision.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm choosing Firebase Hosting because it meets all my requirements and broadens my knowledge of Google Cloud.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alternatives Considered
&lt;/h3&gt;

&lt;p&gt;I have experience hosting sites on &lt;strong&gt;Option 2: GitHub Pages&lt;/strong&gt;. However, I have been frustrated by the lack of performant redirects (redirects via &lt;code&gt;HTML meta refresh&lt;/code&gt; lack wildcard configuration options and can't be cached compared to server-side options). Other solutions have server-side redirect features (&lt;a href="https://firebase.google.com/docs/hosting/full-config#redirects" rel="noopener noreferrer"&gt;firebase hosting&lt;/a&gt;, &lt;a href="https://docs.netlify.com/routing/redirects/" rel="noopener noreferrer"&gt;netlify&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;I've found GitHub Pages to be a bit complex for PR Previews, with various advice around multiple repositories, random &amp;amp; niche GitHub Actions, or building your own &lt;a href="https://github.com/grayside/gh-page-previews" rel="noopener noreferrer"&gt;custom preview solution&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'm not choosing GitHub Pages because it's missing features I want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 3: Use Google Cloud Run.&lt;/strong&gt; With 6 years of experience working on the developer experience of Google Cloud Run, using it would be straightforward. I don't need (or want) the control over the serving details like which web server is running. The other options are better aligned with my preference for minimal maintenance.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 My teammate Brian Dorsey has a great post on &lt;a href="https://til.cafe/blog/2024/hosting-static-web-site-using-google-cloud-run/" rel="noopener noreferrer"&gt;hosting a static website using Google Cloud Run&lt;/a&gt; if you'd like a recipe.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm not choosing Cloud Run because I want more abstraction away from the technical details. I don't want to pick up a new infrastructure hobby.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 4: Use Netlify.&lt;/strong&gt; Several folks recommended Netlify as their preferred static site host, with strong GitHub integration. Since I am already using Google Cloud for other projects, Netlify has a disadvantage as a new service provider.&lt;/p&gt;

&lt;p&gt;If Firebase Hosting doesn't work out, Netlify would be my next choice. It was a close decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expected Consequences
&lt;/h3&gt;

&lt;p&gt;Any of these options will meet my goal of serving a few megabytes of data. I expect problems to come from pet peeves or curiousity about the alternatives. Either way, this is a &lt;strong&gt;&lt;em&gt;low stakes decision not worth a deeper analysis&lt;/em&gt;&lt;/strong&gt;. If I had to change my decision, I expect the switching cost to move to another static site host to be an evening of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revisiting this Decision
&lt;/h2&gt;

&lt;p&gt;I've scheduled a reminder for March 2025 to reflect on this decision, and decide if I'd like to change hosting. With the low expected switching cost, it might be worth trying another host just for the learning experience.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover Photo by &lt;a href="https://unsplash.com/@mikeenerio?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Mike Enerio&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/aerial-view-of-grass-H58bnmnedTc?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>webdev</category>
      <category>github</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Dependency Updates: More to Do Than Review</title>
      <dc:creator>Adam Ross</dc:creator>
      <pubDate>Sat, 29 Jun 2024 17:05:36 +0000</pubDate>
      <link>https://dev.to/grayside/dependency-updates-more-to-do-than-review-545b</link>
      <guid>https://dev.to/grayside/dependency-updates-more-to-do-than-review-545b</guid>
      <description>&lt;p&gt;I enjoy tools that automate dependency updates in my projects (looking at you, &lt;a href="https://docs.renovatebot.com/" rel="noopener noreferrer"&gt;Renovate&lt;/a&gt; and &lt;a href="https://docs.github.com/en/code-security/getting-started/dependabot-quickstart-guide" rel="noopener noreferrer"&gt;Dependabot&lt;/a&gt;). They save my team a lot of time: with a quick glance and the push of a couple buttons, I can update dozens of dependencies in a repository. Even better, the work came to me, I didn't need to go looking for release schedules or subscribe to a newsletter. This is a big change to how I see teams handle 3rd party updates.&lt;/p&gt;

&lt;p&gt;Merging an update just because the tests pass creates a mess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context: What do these dependency automation tools do?
&lt;/h2&gt;

&lt;p&gt;They help in the following ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notify project maintainers that a dependency has an update&lt;/li&gt;
&lt;li&gt;Modify the project's package manifest to reference the updated code&lt;/li&gt;
&lt;li&gt;Open a Pull Request and highlight some context on the change&lt;/li&gt;
&lt;li&gt;and because you have CI set up, they trigger your automated tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I start my day and look at a Pull Request created by a tool like this, I may see a lot of encouraging green checkmarks✅ declaring merge-worthiness.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools did all the work, I can review &amp;amp; merge, right?
&lt;/h2&gt;

&lt;p&gt;Seeing green checkmarks is necessary but not sufficient to merge. I trust automated checks to tell me where to prioritize my review time. As the human in the loop, I should verify:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Did the automated tests pass?&lt;/li&gt;
&lt;li&gt;Does the library have a history of causing trouble? Maybe I should manually test the change.&lt;/li&gt;
&lt;li&gt;Does the package follow &lt;a href="https://semver.org/" rel="noopener noreferrer"&gt;semantic versioning&lt;/a&gt;? Is their a compatibility break from a major update? What if the break doesn't change the API specification, but critical details in the payload data?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With answers "Yes", "No Problem", and "No", I could just merge...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🛑 WAIT. I've left something out: &lt;em&gt;I know that I don't have 100% test coverage&lt;/em&gt;. Are the tests passing because the change is good, or because there is a test coverage gap and I have no idea what will happen next?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The question &lt;em&gt;"Did the automated tests pass?"&lt;/em&gt; becomes &lt;em&gt;"Is there automated test coverage in my project and does it cover the use of this dependency? Did those tests pass?"&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's see an example of untested dependencies slipping through
&lt;/h2&gt;

&lt;p&gt;In this example, suppose a dependency called &lt;em&gt;ThirdPartyAuth&lt;/em&gt; is an important part of the business logic. Below I'll show some of the potential misunderstandings from different testing strategies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Important business logic that takes a callback/function parameter.&lt;/span&gt;
&lt;span class="nf"&gt;doGoodThings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maybeAMockedRequestFunction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;maybeAMockedRequestFunction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;RealRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;http_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;ThirdPartyAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;coolLibraryAuthV2&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;doGoodThings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;RealRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test Example #1: No Test
&lt;/h3&gt;

&lt;p&gt;Test success reported with no tests run. Do I remember during code review?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A commented out test stub.&lt;/span&gt;
&lt;span class="c1"&gt;// Test_doGoodThings()&lt;/span&gt;

&lt;span class="err"&gt;✔️&lt;/span&gt; &lt;span class="nx"&gt;Success&lt;/span&gt; &lt;span class="c1"&gt;// No tests were run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test Example #2: Mocked Test
&lt;/h3&gt;

&lt;p&gt;End-to-end testing can be expensive to write and much more expensive to maintain. Instead, we could mock the responses from external APIs and scope the tests to local business logic.&lt;/p&gt;

&lt;p&gt;This test does not cover the use of the &lt;em&gt;ThirdPartyAuth&lt;/em&gt; library.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nc"&gt;MockRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;Test_doGoodThings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MockRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="err"&gt;✔️&lt;/span&gt; &lt;span class="nx"&gt;Success&lt;/span&gt; &lt;span class="c1"&gt;// What does that mean?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test Example #3: End-to-End Test
&lt;/h3&gt;

&lt;p&gt;The integration of &lt;code&gt;ThirdPartyAuth.coolLibraryAuthV2()&lt;/code&gt; with the codebase is verified.&lt;/p&gt;

&lt;p&gt;If the test fails it may be a breaking change in the library or perhaps &lt;code&gt;https://example.com&lt;/code&gt; is down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nc"&gt;Test_doGoodThings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;RealRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="err"&gt;✅&lt;/span&gt; &lt;span class="nx"&gt;Success&lt;/span&gt; &lt;span class="c1"&gt;// Probability of false negative &amp;gt; false positive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value of the ✅ passing test is limited by the depth of test coverage and my knowledge of the limitation. No only me: since my team handles maintenance on a rotating basis, the value of the tests depends on how well each teammate understands those limitations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Functionality not broken, but update not complete
&lt;/h2&gt;

&lt;p&gt;Just because a library works doesn't mean it is used effectively.&lt;/p&gt;

&lt;p&gt;When a dependency has significant changes over time, it likely gains new methods, recommended usage patterns, and planned deprecations on old ways of working. Upgrading the dependency without understanding the new, happy paths sets up a future with a broken update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Today's outdated practice is tomorrow's deprecated design.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Call to Action
&lt;/h2&gt;

&lt;p&gt;Passing tests can be deceptive, and package manifests aren't the only spot in your codebase to need changes when a big update happens.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 The robots cannot do all the necessary work to update a dependency safely.&lt;/strong&gt; You &lt;em&gt;do not need&lt;/em&gt; deeper test coverage. You and your collaborators &lt;em&gt;do need&lt;/em&gt; a common understanding of the limits of what a passing test means, and what needs to happen to keep shipping quality software after your automated testing has done what it could.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In terms of Renovate configuration, maybe add some &lt;a href="https://docs.renovatebot.com/configuration-options/#prbodynotes" rel="noopener noreferrer"&gt;extra guidance to those Pull Requests&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prBodyNotes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Do tests cover this dependency? The PR needs you."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>productivity</category>
      <category>opensource</category>
      <category>github</category>
      <category>testing</category>
    </item>
    <item>
      <title>Why Automate?</title>
      <dc:creator>Adam Ross</dc:creator>
      <pubDate>Tue, 30 Apr 2024 19:28:36 +0000</pubDate>
      <link>https://dev.to/grayside/why-automate-1fac</link>
      <guid>https://dev.to/grayside/why-automate-1fac</guid>
      <description>&lt;p&gt;Automation saves time and toil when performing a task, and we all appreciate that. This is only one frame for justifying automation but it limits view of other opportunities.&lt;/p&gt;

&lt;p&gt;I love how automation transforms the tedium of work into puzzle-solving. Early in my career, I learned that management would value puzzle time (automation work) by pointing out a greater benefit for the effort than the time spent. (Aim for minimal time investment without sacrificing opportunities or incurring hidden costs like a high maintenance burden.)&lt;/p&gt;

&lt;p&gt;Folks enjoy citing &lt;a href="https://xkcd.com/1205/" rel="noopener noreferrer"&gt;XKCD 1205&lt;/a&gt; in automation discussions because it does a great job illustrating the opportunity time cost of automation. The time spent to code a new solution for an already solved problem doesn't help solve business goals until after the investment pays off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs0lkfya38rrykgj3qmos.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs0lkfya38rrykgj3qmos.png" title="XKCD #1205: Is it worth the time?" alt="A grid with the header 'How long can you work on making a routine task more efficient before you're spending more time than you save' the x-axis is the frequency of the task, the y-axis is the time it takes using prospective automation. It implies a narrow path where automating has value." width="571" height="464"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://xkcd.com/1205/" rel="noopener noreferrer"&gt;XKCD #1205: Is it worth the time?&lt;/a&gt;&lt;/p&gt;



&lt;blockquote&gt;
&lt;p&gt;What's automation anyway? I'd call it "&lt;em&gt;software that reduces the amount of effort humans need to put in to get a job done&lt;/em&gt;". It replaces people work with CPU work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As I continued my journey as a developer, I found that the cost/benefit time ratio around automation didn't line up with my thinking. Time trade-offs are essential, but there are more variables in the trade-off equation than time--other ways of &lt;strong&gt;&lt;em&gt;recouping effort&lt;/em&gt;&lt;/strong&gt;. A particular automation may have other goals than time-savings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Planning for a big push
&lt;/h2&gt;

&lt;p&gt;Perhaps the usual routine doesn't deserve automation. What if your team plans a sprint next month, focusing everyone's effort on a big change? The trade-offs are different: Increasing the frequency of a task and the number of people to onboard to a task both change the math, raising the value of automation. A bit of scripting here also improves the chances the team meets a time-bound goal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimgs.xkcd.com%2Fcomics%2F2045.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimgs.xkcd.com%2Fcomics%2F2045.png" title="XKCD #2685: 2045" alt="(Cueball, a friend also drawn as Cueball, Danish, and Black Hat are standing together. Danish is looking at her phone.)&amp;lt;br&amp;gt;
Cueball: ...And then after the one in 2024, there's another on August 12, 2045.&amp;lt;br&amp;gt;
Friend: We're in! We can invite our kids, assuming we have any.&amp;lt;br&amp;gt;
Danish: I'll create an event. Do you think we'll still be using Google Calendar in 2045?&amp;lt;br&amp;gt;
Black Hat: Sorry, I'd love to make it, but I have a thing that day.&amp;lt;br&amp;gt;
(Caption below the panel:)&amp;lt;br&amp;gt;
It's weird making plans for eclipses." width="350" height="457"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://xkcd.com/2685/" rel="noopener noreferrer"&gt;XKCD #2685: 2045&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;This kind of automation makes the most sense when swarming on a specific task, such as updating code to use a new logging library or migrating data between systems. This may represent a one-time need, so build with minimal flexibility and worry more about good error messages than over-all maintainability. (&lt;em&gt;This is a good use for a shell script shared, maybe not even committed to source control.&lt;/em&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Hiding complexity
&lt;/h2&gt;

&lt;p&gt;Folks need to do perform complex tasks but workflow automation can simplify the details. You don't need to ask everyone to carry the full cognitive load when tools can do more of the lifting. It also provides paths to onboard folks to the team in stages. How fast can folks be productive in your environment? For "new person job satisfaction", it is best to measure this in hours or days instead of weeks.&lt;/p&gt;

&lt;p&gt;More persistent automation like this is an abstraction layer to hide a mess.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimgs.xkcd.com%2Fcomics%2Ftech_loops.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimgs.xkcd.com%2Fcomics%2Ftech_loops.png" title="XKCD #1579: Tech Loops" alt="A flow chart showing the tooling dependencies you need to be productive, with loops back showing the chaos and the way tooling becomes it's own goal" width="468" height="415"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://xkcd.com/1579/" rel="noopener noreferrer"&gt;XKCD #1579: Tech Loops&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Reducing cognitive load helps makes a complex system more approachable. More than a simplification in performing the task, it provides the vocabulary for non-experts to discuss the process. When folks have the ability to communicate about a task, new opportunities for collaboration open.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimgs.xkcd.com%2Fcomics%2Faverage_familiarity.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimgs.xkcd.com%2Fcomics%2Faverage_familiarity.png" title="XKCD #2501: Average Familiarity" alt="Two people talking. Person 1 says " width="295" height="480"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://xkcd.com/2501/" rel="noopener noreferrer"&gt;XKCD #2501: Average Familiarity&lt;/a&gt;&lt;/p&gt;



&lt;h2&gt;
  
  
  What do you do every day?
&lt;/h2&gt;

&lt;p&gt;In the last month, I've given thought to the developer superpower of "defining the job you want". Automation is often thought of as a way to create more space to do the job you already have, but it is also a way to &lt;strong&gt;redefine the job&lt;/strong&gt;. You can reinvest the time gained from automation on improving the automation, perhaps even expanding the scope of what the task achieves for your team. As a software developer, maintaining evolving software is more satisfying than turning a crank every 5 minutes!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn2ru1gmcuqcpwtnr67r4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn2ru1gmcuqcpwtnr67r4.png" title="XKCD #1319: Automation" alt="Top caption says " width="404" height="408"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://xkcd.com/1319/" rel="noopener noreferrer"&gt;XKCD #1319: Automation&lt;/a&gt;&lt;/p&gt;



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

&lt;p&gt;When I think of automation now, I consider these factors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Will it save time? When will the up-front investment pay off?&lt;/li&gt;
&lt;li&gt;Will it help others contribute in a complex process?&lt;/li&gt;
&lt;li&gt;Will it make discussing a big, complex system easier, leading to better high-level decisions?&lt;/li&gt;
&lt;li&gt;Will it create space for more interesting activities?&lt;/li&gt;
&lt;li&gt;Will it replace existing activities with a style of work I prefer?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't limit your thinking about the time payoff of automating something, but do consider all the relevant trade-offs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Do you have a favorite XKCD comic that helps you think about automation? &lt;a href="https://hachyderm.io/@grayside" rel="noopener noreferrer"&gt;Let's talk about it!&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Author's Context&lt;/strong&gt;: I’ve worked on products that can be used to help automation projects, such as Google Cloud Run, Scheduler, and Workflows. Within my organization, I’m currently arguing for the need to use more automation to scale our code review and policy enforcement practices.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Additional Credits
&lt;/h2&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@britishlibrary?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;British Library&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/gigantic-machine-sketch-Y1S0ApwC054?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://www.explainxkcd.com/" rel="noopener noreferrer"&gt;explain xkcd&lt;/a&gt; for XKCD alt text (not to be confused with XKCD hover text, which you get by visiting xkcd.com).&lt;/p&gt;

</description>
      <category>software</category>
      <category>automation</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Modernizing cloudbuild.yaml for Container Builds</title>
      <dc:creator>Adam Ross</dc:creator>
      <pubDate>Thu, 21 Mar 2024 23:18:39 +0000</pubDate>
      <link>https://dev.to/googlecloud/modernizing-cloudbuildyaml-for-container-builds-1je0</link>
      <guid>https://dev.to/googlecloud/modernizing-cloudbuildyaml-for-container-builds-1je0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;tl;dr: &lt;a href="https://cloud.google.com/build/docs/configuring-builds/run-bash-scripts" rel="noopener noreferrer"&gt;Running bash scripts&lt;/a&gt; in the Cloud Build documentation tells you use the &lt;code&gt;script&lt;/code&gt; property with the &lt;code&gt;automapSubstitutions&lt;/code&gt; option and I give this recommendation a 💯.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One of my favorite things to do in writing YAML is minimizing the square brackets and hyphens. The frequency of these things really impacts readability for me, especially when I'm trying to glance at a whole block of markup and assess what it's trying to do.&lt;/p&gt;

&lt;p&gt;Over the last couple years, Cloud Build has made improvements in reducing square brackets and enabling more concise configuration for running shell scripts.&lt;/p&gt;

&lt;p&gt;I'm going to step through a couple "generations" of simplification I walked through today in overhauling a container image build pipeline.&lt;/p&gt;

&lt;p&gt;This is what a lot of minimal Cloud Build configurations look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Image'&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/cloud-builders/docker:latest'&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;build'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--tag'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/${PROJECT_ID}/${_IMAGE}:latest'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Push&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Image&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Registry'&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/cloud-builders/docker:latest'&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;push'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/${PROJECT_ID}/${_IMAGE}:latest'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;substitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;_IMAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Starting character count:&lt;/strong&gt; 335&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step #1: Remove square-bracketed [distractions]
&lt;/h2&gt;

&lt;p&gt;Use the &lt;code&gt;script&lt;/code&gt; property instead of &lt;code&gt;args&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Image'&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/cloud-builders/docker:latest'&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build --tag gcr.io/${PROJECT_ID}/${_IMAGE}:latest .&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Push&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Image&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Registry'&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/cloud-builders/docker:latest'&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker push gcr.io/${PROJECT_ID}/${_IMAGE}:latest&lt;/span&gt;

&lt;span class="na"&gt;substitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;_IMAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🛑 If you try to use this it won't work.&lt;/p&gt;

&lt;p&gt;It turns out switching from &lt;code&gt;args&lt;/code&gt; to &lt;code&gt;script&lt;/code&gt; means the code &lt;strong&gt;no longer has substitutions, only environment variables&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A year ago, the next step would be to manually map the substitutions to environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Image'&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/cloud-builders/docker:latest'&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build --tag gcr.io/${PROJECT_ID}/${_IMAGE}:latest .&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Push&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Image&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Registry'&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/cloud-builders/docker:latest'&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker push gcr.io/${PROJECT_ID}/${_IMAGE}:latest&lt;/span&gt;

&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PROJECT_ID=$PROJECT&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;_IMAGE=$_IMAGE&lt;/span&gt;

&lt;span class="na"&gt;substitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;_IMAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Updated character count&lt;/strong&gt;: 390 (sometimes shorter isn't clearer)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step #2: Remove excess hyphenation
&lt;/h2&gt;

&lt;p&gt;Use the &lt;code&gt;automapSubstitutions&lt;/code&gt; global option and the substitutions are injected as environment variables to all step scripts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Image'&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/cloud-builders/docker:latest'&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build . --tag gcr.io/${PROJECT_ID}/${_IMAGE}:latest&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Push&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Image&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Registry'&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/cloud-builders/docker:latest'&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker push gcr.io/${PROJECT_ID}/${_IMAGE}:latest&lt;/span&gt;

&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;automapSubstitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;substitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;_IMAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Updated character count&lt;/strong&gt;: 373 (progress, but I'd be more satisfied if we got back to 335 somehow)&lt;/p&gt;

&lt;h2&gt;
  
  
  Simplify the Image Push
&lt;/h2&gt;

&lt;p&gt;Since this Cloud Build configuration doesn't have any follow-up steps that need to use the image outside the build, I can simplify further, by replacing the step &lt;em&gt;Push Container Image to Container Registry&lt;/em&gt; with the &lt;code&gt;images&lt;/code&gt; property. (Read more about that in &lt;a href="https://dev.to/googlecloud/remember-images-in-your-cloudbuild-yaml-o3l"&gt;Remember images in your cloudbuild.yaml!&lt;/a&gt; by &lt;a class="mentioned-user" href="https://dev.to/glasnt"&gt;@glasnt&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Image'&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/cloud-builders/docker:latest'&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build . --tag gcr.io/${PROJECT_ID}/${_IMAGE}:latest&lt;/span&gt;

&lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gcr.io/${PROJECT_ID}/${_IMAGE}:latest&lt;/span&gt;

&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;automapSubstitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;substitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;_IMAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't reduce hyphens any further, but it does reduce two lines of configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Updated character count&lt;/strong&gt;: 263 (Success!)&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus Round: Migrating to Artifact Registry
&lt;/h2&gt;

&lt;p&gt;You may have noticed this configuration ships the container image to a Google Container Registry URL! There's a good chance if you are modernizing your Cloud Build YAML this way you've got some GCR in place, if so, you need to migrate soon. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Check out &lt;a href="https://cloud.google.com/artifact-registry/docs/transition/transition-from-gcr" rel="noopener noreferrer"&gt;Transition from Container Registry&lt;/a&gt;. There are options to migrate while keeping the gcr.io URL.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Say I've already created a new Artifact Registry instance for my container, what does that YAML look like?&lt;/p&gt;

&lt;p&gt;(Let's say it's a multi-regional repository called &lt;code&gt;container&lt;/code&gt; in the &lt;code&gt;us&lt;/code&gt; location.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Image'&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/cloud-builders/docker:latest'&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build . --tag "us-docker.pkg.dev/${PROJECT_ID}/container/${_IMAGE}:latest"&lt;/span&gt;

&lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-docker.pkg.dev/${PROJECT_ID}/container/${_IMAGE}:latest"&lt;/span&gt;

&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;automapSubstitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;substitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;_IMAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since an Artifact Repository is a Cloud resource, a project might have more than one. This encourages me to parameterize the location and repository name, so that development &amp;amp; testing are easier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Image'&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/cloud-builders/docker:latest'&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build . --tag "${_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/${_IMAGE}:latest"&lt;/span&gt;

&lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/${_IMAGE}:latest"&lt;/span&gt;

&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;automapSubstitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;substitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;_IMAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service&lt;/span&gt;
  &lt;span class="na"&gt;_LOCATION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us&lt;/span&gt;
  &lt;span class="na"&gt;_REPO&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;container&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Updated character count&lt;/strong&gt;: 358 (A little longer than 335, but Artifact Registry is more verbose than Container Registry, not much we can do about that.)&lt;/p&gt;

&lt;p&gt;In a more complicated YAML block, &lt;code&gt;automapSubstitutions&lt;/code&gt; would make a much bigger difference. I go out of my way to avoid using &lt;code&gt;args&lt;/code&gt;, and this option helps me do that without needing to add a lot of boilerplate config.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Aside Info: Reader, maybe you'll suggest I delete all those pesky curly braces around my variables, getting me a win at 342 characters. This &lt;a href="https://stackoverflow.com/a/8748880" rel="noopener noreferrer"&gt;stackoverflow answer&lt;/a&gt; explains the value of the curly braces, and I'm one of the people that makes curly braced variables part of my "shell variables in strings" practice.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>googlecloud</category>
      <category>devops</category>
      <category>docker</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Migrating to Cloud Run's Secret Manager Integration from Libraries</title>
      <dc:creator>Adam Ross</dc:creator>
      <pubDate>Thu, 27 May 2021 16:36:32 +0000</pubDate>
      <link>https://dev.to/googlecloud/migrating-to-cloud-run-s-secret-manager-integration-from-libraries-34o</link>
      <guid>https://dev.to/googlecloud/migrating-to-cloud-run-s-secret-manager-integration-from-libraries-34o</guid>
      <description>&lt;p&gt;Secret Manager is easier to use with Cloud Run since I last wrote about "&lt;a href="https://dev.to/googlecloud/serverless-mysteries-with-secret-manager-libraries-on-google-cloud-3a1p"&gt;serverless mysteries&lt;/a&gt;". At the time of that writing, the best way to use a Secret in Cloud Run was to add code to your service to integrate with the Secret Manager API. Now Cloud Run does that for you!&lt;/p&gt;

&lt;p&gt;In this post we'll migrate the "Loyalty Checker" service from using the &lt;a href="https://cloud.google.com/secret-manager/docs" rel="noopener noreferrer"&gt;Secret Manager&lt;/a&gt; client library to use Cloud Run's direct Secret Manager integration. (As of this writing, the integration is still in preview.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Prepare to follow along
&lt;/h2&gt;

&lt;p&gt;If you're still set up from last time, just open &lt;a href="https://console.cloud.google.com/home/dashboard?cloudshell=true" rel="noopener noreferrer"&gt;Cloud Shell&lt;/a&gt; and continue where we left off.&lt;/p&gt;

&lt;p&gt;Otherwise,  please walk through the &lt;a href="https://dev.to/googlecloud/serverless-mysteries-with-secret-manager-libraries-on-google-cloud-3a1p"&gt;last post&lt;/a&gt; then continue here to enjoy the feeling of deleting code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modify the Loyalty Checker code
&lt;/h2&gt;

&lt;p&gt;Direct integration with Secret Manager makes the code simpler:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Remove code to initialize the client library&lt;/li&gt;
&lt;li&gt;Remove code to retrieve the secret on the first request&lt;/li&gt;
&lt;li&gt;Rename the &lt;code&gt;SECRET_NAME&lt;/code&gt; variable to &lt;code&gt;SECRET_VALUE&lt;/code&gt;, the service will have direct access to the secret from the environment variable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After these changes the service has shrunk by 46 lines of code, and no longer has any dependencies on Secret Manager:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;h2&gt;
  
  
  Re-deploy the Service
&lt;/h2&gt;

&lt;p&gt;We'll combine the Secret integration with the new all-in-one deploy command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud beta run deploy loyalty &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--remove-env-vars&lt;/span&gt; SECRET_NAME &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--update-secrets&lt;/span&gt; &lt;span class="nv"&gt;SECRET_VALUE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;you-know-who:1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;--source&lt;/code&gt; flag tells gcloud to check for a Dockerfile in the current directory and use Cloud Build under the hood to build a container image without a separate command. (Read more about &lt;a href="https://cloud.google.com/run/docs/deploying-source-code" rel="noopener noreferrer"&gt;deploying from source code&lt;/a&gt; if you're curious about the details.)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;--remove-env-vars&lt;/code&gt; flag removes the no-longer-needed reference to a secret for our code to load.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;--update-secrets&lt;/code&gt; flag integrates a new secret with the service without disrupting any existing secrets. (Use &lt;code&gt;--set-secrets&lt;/code&gt; to simultaneously remove any the other secrets.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the nice things about Cloud Run's secrets flag is how you can use just the "resource ID" part of the overall resource name. Secrets are normally referenced as "&lt;code&gt;projects/PROJECT_ID/secrets/SECRET_ID/versions/VERSION&lt;/code&gt;" but all you need to reference a secret in the same project is &lt;code&gt;SECRET_ID:VERSION&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  No IAM Update?!
&lt;/h2&gt;

&lt;p&gt;There's no need to change IAM settings. Last time we set up a dedicated service account and gave it access to the secret. The same configuration works for client libraries or for this direct integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;If you want to be able to "live refresh" the secret in your service, check out the &lt;a href="https://cloud.google.com/run/docs/configuring/secrets" rel="noopener noreferrer"&gt;documentation to learn how to mount your secrets as a volume&lt;/a&gt;. The environment variable method in this post is configured when an instance is started and is never updated.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All code © Google w/ Apache 2 license&lt;/em&gt;&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>serverless</category>
      <category>googlecloudrun</category>
      <category>secretmanager</category>
    </item>
    <item>
      <title>Serverless Mysteries with Secret Manager Libraries on Google Cloud</title>
      <dc:creator>Adam Ross</dc:creator>
      <pubDate>Fri, 06 Mar 2020 18:10:03 +0000</pubDate>
      <link>https://dev.to/googlecloud/serverless-mysteries-with-secret-manager-libraries-on-google-cloud-3a1p</link>
      <guid>https://dev.to/googlecloud/serverless-mysteries-with-secret-manager-libraries-on-google-cloud-3a1p</guid>
      <description>&lt;p&gt;Secrets are private information used by an application. They're often obfuscated passphrases used to authenticate with APIs or connect to databases: not something that should ever be in your code! Until Google Cloud's &lt;a href="https://cloud.google.com/blog/products/identity-security/introducing-google-clouds-secret-manager" rel="noopener noreferrer"&gt;Secret Manager&lt;/a&gt;, many tutorials suggested using secrets on platforms such as App Engine or Cloud Run with the magic of plaintext (in-the-clear) environment variables.&lt;/p&gt;

&lt;p&gt;Unencrypted environment variables aren't very secret, but at least they don't need to be committed to your codebase. Using one to carry a secret is simple but it's not very secure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YOU_KNOW_WHO="Lord Voldemort" npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Environment variables are a gossipy approach to injecting data, some examples of weak security include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;developers type them into terminal history&lt;/li&gt;
&lt;li&gt;configuration manifests reveal them&lt;/li&gt;
&lt;li&gt;exfiltrating them from production is as easy as a stray bit of code like &lt;code&gt;console.log(process.env)&lt;/code&gt; and access to view the logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Secret Manager provides a better approach for managing your secrets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a named secret&lt;/li&gt;
&lt;li&gt;Grant your code access to it&lt;/li&gt;
&lt;li&gt;Watch the records of everything that tries to access it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Secret manager works smoothly. My first experience was a bit magical (work with me, we're on a theme). And what secrets are there in magic? In myth and fiction, names are significant mysteries doubling as a magic spell or a homing beacon for evil ears. How many times in the &lt;strong&gt;Harry Potter by J.K. Rowling&lt;/strong&gt; series did we hear the phrase &lt;em&gt;"You-Know-Who"&lt;/em&gt; or &lt;em&gt;"He-Who-Must-Not-Be-Named"&lt;/em&gt; as an alias for the big bad? &lt;/p&gt;

&lt;p&gt;In this post we'll walk through creating a "Ministry of Magic Loyalty Checker" service on Cloud Run, using Secret Manager to supply the dark wizard's true name.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prepare to follow along
&lt;/h2&gt;

&lt;p&gt;If you'd like to follow along, you'll need a Google Cloud project, a working installation of the gcloud CLI, and enabled APIs for Cloud Run and Secret Manager.&lt;/p&gt;

&lt;p&gt;Assuming you already have a project, the fastest way to get started is a quick 2-step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open the &lt;a href="https://console.cloud.google.com/home/dashboard?cloudshell=true" rel="noopener noreferrer"&gt;Cloud Shell&lt;/a&gt; to get a VM workspace provisioned with all the tools you'll need.&lt;/li&gt;
&lt;li&gt;Enable the APIs with a CLI command:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud services &lt;span class="nb"&gt;enable &lt;/span&gt;run.googleapis.com secretmanager.googleapis.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 1: Keeping Secrets Close
&lt;/h2&gt;

&lt;p&gt;Secret Manager is a Key-Value Store, one with encryption, versioning, access control, and audit logging around individual key-values. Each one is initialized before it can be assigned a value.&lt;/p&gt;
&lt;h3&gt;
  
  
  Create a Secret
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;gcloud&lt;/code&gt; to create a new secret. This is like a variable declaration: it's a placeholder for something to come.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud secrets create you-know-who &lt;span class="nt"&gt;--replication-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"automatic"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The replication policy flag instructs Google Cloud to manage storage locations of the key.&lt;/p&gt;
&lt;h3&gt;
  
  
  Create a Version
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;version&lt;/code&gt; is the secret data itself: every secret value is going to change eventually. Versioning as a first-class concept makes rotation much easier.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open &lt;code&gt;secret.txt&lt;/code&gt; in your favorite editor&lt;/li&gt;
&lt;li&gt;Type "Lord Voldemort"&lt;/li&gt;
&lt;li&gt;Save the file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps the secret out of your terminal history.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud secrets versions add &lt;span class="s2"&gt;"you-know-who"&lt;/span&gt; &lt;span class="nt"&gt;--data-file&lt;/span&gt; secret.txt
Created version &lt;span class="o"&gt;[&lt;/span&gt;1] of the secret &lt;span class="o"&gt;[&lt;/span&gt;you-know-who].
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 2: Configure the Cloud Run service identity
&lt;/h2&gt;

&lt;p&gt;Cloud Run allows you to specify the &lt;a href="https://cloud.google.com/run/docs/securing/service-identity" rel="noopener noreferrer"&gt;"service identity"&lt;/a&gt; of each service. This allows you to attach IAM permissions to your service to limit it's access to other Cloud resources. I prefer to create one service identity for each Cloud Run service so my services run with minimal privileges.&lt;/p&gt;

&lt;p&gt;This feature ensures only the &lt;code&gt;loyalty-check&lt;/code&gt; service is able to access the secret because only it (and Project Owners) have read permission.&lt;/p&gt;

&lt;p&gt;Create a service account for the loyalty-checker service:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam service-accounts create loyalty-identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Grant the service account access to the &lt;code&gt;you-know-who&lt;/code&gt; secret:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud secrets add-iam-policy-binding you-know-who &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;--member&lt;/span&gt; serviceAccount:loyalty-identity@example-project-id.iam.gserviceaccount.com &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;--role&lt;/span&gt; roles/secretmanager.secretAccessor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 3: Implement the "Loyalty Check" service
&lt;/h2&gt;

&lt;p&gt;The code of this service is mostly interesting as an example of how to use the Secret Manager client libraries. We'll walk through it step by step.&lt;/p&gt;
&lt;h3&gt;
  
  
  Create a new &lt;code&gt;npm&lt;/code&gt; package
&lt;/h3&gt;

&lt;p&gt;The only dependencies are Express v4 and the Secret Manager v3 client library.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm init loyalty
npm i express@4 @google-cloud/secret-manager@3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Create a Dockerfile
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;Dockerfile&lt;/code&gt; is used to define how to create a container image for deployment to Cloud Run.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;p&gt;This Dockerfile uses a &lt;a href="https://hub.docker.com/_/node" rel="noopener noreferrer"&gt;Node.js 14 base image&lt;/a&gt;, copies in the package.json manifest to install npm dependencies, and adds the custom source code. When the container is started &lt;code&gt;npm start&lt;/code&gt; is executed to run the service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the application code
&lt;/h3&gt;

&lt;p&gt;Both code blocks are placed in a single &lt;code&gt;index.js&lt;/code&gt; file for use.&lt;/p&gt;

&lt;p&gt;The code uses the Secret Manager client library to dig up the secret: &lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;The code above does the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensures the &lt;code&gt;SECRET&lt;/code&gt; environment variable is present to name the secret.&lt;/li&gt;
&lt;li&gt;Defines a &lt;code&gt;getSecret()&lt;/code&gt; function that uses global state to keep the Secret Manager client library in memory for reuse.&lt;/li&gt;
&lt;li&gt;Provides an escape valve if the code is running locally: just call the service with &lt;code&gt;SECRET=dev npm start&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Logs an error if the API interaction fails but allows the caller to decide what to do.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next define a web server that takes requests to perform a loyalty check. The secret is only retrieved when first requested. No need to have it decrypted in active memory until it's needed!&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;By storing the secret value in global state, changes to the secret value or access revocation will not affect this instance of the service. On Cloud Run, this means this value will be used by a given container instance until it scales down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who checks the loyalty checker?
&lt;/h3&gt;

&lt;p&gt;Everyone is loyal to the Ministry of Magic unless the Loyalty Checker knows the name of You-Know-Who. Seems like this secret has turned the service into a traitor!&lt;/p&gt;

&lt;p&gt;Maybe this could be made a little smarter: according to the &lt;a href="https://cloud.google.com/secret-manager/quotas" rel="noopener noreferrer"&gt;quota reference&lt;/a&gt; Secret Manager supports payloads up to 64 KiB per key, so we could encode JSON as a string and stash a lookup table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Harry Potter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ministry of Magic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Voldemort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lord Voldemort"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Severus Snape"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Lucius Malfoy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lord Vodemort"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works for several characters, but quickly reaches a point where using a database is more sensible. The secret's role would change from directly holding the mystery to holding the database credentials to look up a trove of PII.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Ship the Cloud Run service
&lt;/h2&gt;

&lt;p&gt;Now that we're code complete, let's get the Loyalty Checker into production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build the container
&lt;/h3&gt;

&lt;p&gt;This could be done with docker, but today we'll use Cloud Build. This step builds the service into a container image and pushes it to Google Container Registry. From there we can deploy to Cloud Run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud builds submit &lt;span class="nt"&gt;--tag&lt;/span&gt; gcr.io/&lt;span class="nv"&gt;$GOOGLE_CLOUD_PROJECT&lt;/span&gt;/loyalty
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploy the service
&lt;/h3&gt;

&lt;p&gt;This step is a bit more complicated than the typical Cloud Run deployment, because we need to specify the service account and configure the full name of the secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run deploy loyalty &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; gcr.io/&lt;span class="nv"&gt;$GOOGLE_CLOUD_PROJECT&lt;/span&gt;/loyalty &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--update-env-vars&lt;/span&gt; &lt;span class="nv"&gt;SECRET_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;projects/&lt;span class="nv"&gt;$GOOGLE_CLOUD_PROJECT&lt;/span&gt;/secrets/you-know-who/versions/1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-account&lt;/span&gt; loyalty-identity@adamross-svls-kibble.iam.gserviceaccount.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-unauthenticated&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--platform&lt;/span&gt; managed &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-central1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;1&lt;/code&gt; at the end of the &lt;code&gt;SECRET_NAME&lt;/code&gt; value specifies the version of the secret to use. When new versions are created, the number increments. You can double-check what the most recent enabled version is by running &lt;code&gt;gcloud secrets versions list you-know-who&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Try out the "Loyalty Checker"
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;curl&lt;/code&gt; to send an HTTP request to the URL of your Cloud Run service. You'll see that URL on screen after you deploy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://loyalty-[HASH]-uc.a.run.app/loyalty
You serve the Lord Voldemort!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We've created a new Cloud Run service which pulls essential configuration from Secret Manager. Access has been carefully managed to limit it to a single service account, which is only used by a single Cloud Run service. That's the traditional way to keep a secret: by not sharing it with anyone. Unfortunately once *&lt;em&gt;this service&lt;/em&gt;* unlocks the true name it tells anyone who asks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Down with Environment Variables!?
&lt;/h2&gt;

&lt;p&gt;What about the "Boy Who Lived," is the name "Harry Potter" a secret too? No, it was a newspaper headline. If folks didn't know the name, he wouldn't have been their hero. &lt;/p&gt;

&lt;p&gt;Don't overuse secrets: they're not only more expensive than environment variables, but the more you hide production configuration the more mysteries there are when it's time to troubleshoot production failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;I'm looking forward to how Secret Manager helps improve security practices on Google Cloud. What will your first secret be?&lt;/p&gt;

&lt;p&gt;Learn how to leave the libraries behind with &lt;a href="https://cloud.google.com/run/docs/configuring/secrets" rel="noopener noreferrer"&gt;Cloud Run's built-in Secret Manager integration&lt;/a&gt;, perhaps by &lt;a href="https://dev.to/googlecloud/migrating-to-cloud-run-s-secret-manager-integration-from-libraries-34o"&gt;reading the follow-up blog post&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;Generalize your product knowledge by reading the &lt;a href="https://cloud.google.com/secret-manager/docs" rel="noopener noreferrer"&gt;Secret Manager documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All code © Google w/ Apache 2 license&lt;/em&gt;&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>node</category>
      <category>googlecloudrun</category>
      <category>secretmanager</category>
    </item>
    <item>
      <title>Portable code migrating across Google Cloud's serverless platforms</title>
      <dc:creator>Adam Ross</dc:creator>
      <pubDate>Thu, 05 Dec 2019 17:37:36 +0000</pubDate>
      <link>https://dev.to/googlecloud/portable-code-migrating-across-google-cloud-s-serverless-platforms-2ifk</link>
      <guid>https://dev.to/googlecloud/portable-code-migrating-across-google-cloud-s-serverless-platforms-2ifk</guid>
      <description>&lt;p&gt;Google Cloud has a &lt;a href="https://cloud.google.com/hosting-options/" rel="noopener noreferrer"&gt;number of options&lt;/a&gt; to run your code. We'll focus on three serverless platforms: Cloud Functions, App Engine, and Cloud Run, all of which automatically scale by traffic and bill by usage. The biggest difference between these platforms is the level of abstraction. We can deploy a function to Cloud Functions, an app to App Engine, or an app with a custom runtime (a Docker container) to Cloud Run.&lt;/p&gt;

&lt;p&gt;The most abstract serverless hosting option is a function. It has no code except the core business logic of a specialized service. Two challenges in working with serverless functions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developing locally without a platform to convert requests to function calls&lt;/li&gt;
&lt;li&gt;Finding hosting alternatives when the infrastructure, runtime, or cost constraints of your platform are reached&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using a "Hello World" function with an international twist: "Hello Translate", we'll explore a new way to mitigate these challenges on Google Cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standalone Functions with the Functions Framework
&lt;/h2&gt;

&lt;p&gt;Like all Google Cloud Functions runtimes, the Node.js v10 runtime supports direct HTTP triggers while removing the overhead of HTTP routing from developers. Under the hood this magic is done by the open source &lt;a href="https://github.com/GoogleCloudPlatform/functions-framework-nodejs" rel="noopener noreferrer"&gt;Node.js functions framework&lt;/a&gt;. The framework installs as a normal dependency for your function and encapsulates &lt;a href="https://expressjs.com/" rel="noopener noreferrer"&gt;express.js&lt;/a&gt; to provide clean and standardized HTTP request handling.&lt;/p&gt;

&lt;p&gt;Because it provides an HTTP server you can run your function as a localhost web service, or deploy it to any web hosting platform.&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Write business logic&lt;/li&gt;
&lt;li&gt;Add a &lt;strong&gt;Function Framework&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Deploy to a web host&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's get to the code for "Hello Translate".&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing a Function
&lt;/h2&gt;

&lt;p&gt;Create a new &lt;code&gt;npm&lt;/code&gt; package and install the functions framework and the translate library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm init
npm &lt;span class="nb"&gt;install&lt;/span&gt; @google-cloud/functions-framework@1 @google-cloud/translate@5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;(The &lt;code&gt;@1&lt;/code&gt; and &lt;code&gt;@5&lt;/code&gt; bits instruct &lt;code&gt;npm&lt;/code&gt; to retrieve specific major versions of the libraries. It makes the code in this post less timeless, but more likely to work as long as these versions are compatible with Google Cloud.)&lt;/p&gt;

&lt;p&gt;Cook up the code for "Hello Translate!":&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;p&gt;Before we're ready to deploy HelloTranslate, let's modify the &lt;code&gt;package.json&lt;/code&gt; to define a start script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; "scripts": {
    "start": "functions-framework --target=helloTranslate"
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;When &lt;code&gt;npm start&lt;/code&gt; is run, the Node process will start a web server in the function framework package which is configured to use the target function above. This enables local development with no other special dependencies. The same mechanism will be used in Cloud Functions, App Engine, and Cloud Run.&lt;/p&gt;
&lt;h2&gt;
  
  
  Getting setup to work with Google Cloud
&lt;/h2&gt;

&lt;p&gt;Set up a development environment and configure your project to support the "Hello Translate" function:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create or select a Google Cloud Project. Add it to your shell environment with &lt;code&gt;export PROJECT=[Your Project ID]&lt;/code&gt; to streamline the commands below.&lt;/li&gt;
&lt;li&gt;Install and authenticate with the &lt;a href="https://cloud.google.com/sdk/docs/quickstarts" rel="noopener noreferrer"&gt;Cloud SDK&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Enable the the Translate API: &lt;code&gt;gcloud services enable translate.googleapis.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Testing on Cloud Functions
&lt;/h2&gt;

&lt;p&gt;Start by confirming this is works as a Cloud Function, let's deploy it!&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud functions deploy helloTranslate &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--runtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nodejs10 &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1 &lt;span class="nt"&gt;--trigger-http&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Wait a couple minutes and we're ready to make a request. Use curl to ask for "Hello World!" in the default Spanish language:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://us-central1-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.cloudfunctions.net/helloTranslate
Hola Mundo!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;To learn more about how to use Cloud Functions, check out the &lt;a href="https://cloud.google.com/functions/docs/writing/http#writing_http_helloworld-nodejs" rel="noopener noreferrer"&gt;HTTP Functions how-to&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Deploying a Function to App Engine
&lt;/h2&gt;

&lt;p&gt;Now that we've confirmed the code works with Cloud Functions, let's see it work as an app on App Engine. App Engine allows more control over the instance scaling and behavior, and the requirement to deploy an HTTP server is met by the function framework. This means we can deploy code to App Engine without stopping for code changes.&lt;/p&gt;

&lt;p&gt;Adapt &lt;strong&gt;Hello Translate&lt;/strong&gt; to App Engine by following these steps:&lt;/p&gt;

&lt;p&gt;1) Add an &lt;code&gt;app.yaml&lt;/code&gt; configuration file to declare the runtime:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nodejs10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;2) Deploy the code&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud app deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;3) Ask our production service for the latest translation of "Hello World!" in Yoruba:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.appspot.com?lang&lt;span class="o"&gt;=&lt;/span&gt;yo
Mo ki O Ile Aiye!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;To learn more about how to use App Engine Standard, check out the &lt;a href="https://cloud.google.com/appengine/docs/standard/nodejs/building-app/" rel="noopener noreferrer"&gt;Building an App Quickstart&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Deploying a Function to Cloud Run
&lt;/h2&gt;

&lt;p&gt;Cloud Run is similar to App Engine in expecting a full HTTP app to be deployed. Unlike App Engine, Cloud Run is driven by containers. Cloud Run does not know if you are using javascript, java, or a &lt;a href="https://github.com/steren/awesome-cloudrun/blob/master/Dockerfile" rel="noopener noreferrer"&gt;web server written in BASH&lt;/a&gt;. Containers allow the developer to customize the runtime environment.&lt;/p&gt;

&lt;p&gt;Adapt &lt;strong&gt;Hello Translate&lt;/strong&gt; to Cloud Run by following these steps:&lt;/p&gt;

&lt;p&gt;1) Add a Dockerfile. Copy the Node.js Dockerfile from the &lt;a href="https://cloud.google.com/run/docs/quickstarts/build-and-deploy" rel="noopener noreferrer"&gt;Cloud Run quickstart&lt;/a&gt;. Here's a streamlined copy for quick reading:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;p&gt;2) Build the Container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud builds submit &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gcr.io/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/hello-translate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3) Deploy the service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run deploy hello-translate &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gcr.io/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/hello-translate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4) Ask our production service for the latest translation of "Hello World!" in Japanese:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://hello-translate-j6jxwetqdq-uc.a.run.app?lang&lt;span class="o"&gt;=&lt;/span&gt;ja
&lt;span class="s2"&gt;"こんにちは世界"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To learn more about how to use Cloud Run, check out the &lt;a href="https://cloud.google.com/run/docs/quickstarts/build-and-deploy" rel="noopener noreferrer"&gt;Build and Deploy Quickstart&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Helpful Abstraction!
&lt;/h2&gt;

&lt;p&gt;The function framework is a good case of an abstraction. Code that makes sense to deploy as a function doesn't need much customization in HTTP handling, so why include the boilerplate anywhere that code is hosted? With an open source package, your functions-that-are-services code is more portable across compute platforms.&lt;/p&gt;

&lt;p&gt;If your function is chafing at the runtime or concurrency limitations of Cloud Functions, consider using a &lt;a href="https://github.com/GoogleCloudPlatform/functions-framework" rel="noopener noreferrer"&gt;Functions Framework&lt;/a&gt; to migrate your service to another hosting option such as &lt;a href="https://cloud.google.com/run/docs" rel="noopener noreferrer"&gt;Cloud Run&lt;/a&gt;. With a little more work, you can even bring over event-driven functions by configuring &lt;a href="https://cloud.google.com/run/docs/tutorials/pubsub" rel="noopener noreferrer"&gt;Pub/Sub push subscriptions&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read Onward
&lt;/h2&gt;

&lt;p&gt;For an example application using a Functions Framework to be simultaneously deployable to Cloud Functions and Cloud Run, check out Grant Timmerman's &lt;a href="https://github.com/GoogleCloudPlatform/cloud-tasks-pizza-map" rel="noopener noreferrer"&gt;Pizza Map sample&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>googlecloudrun</category>
      <category>cloudfunctions</category>
      <category>functionsframework</category>
    </item>
  </channel>
</rss>
