https://daniel-azuma.com/Daniel Azuma2022-06-23T17:14:43+00:00This is the website and blog of Daniel Azuma, a software engineer in the Seattle area.Daniel Azumadazuma@gmail.comhttp://daniel-azuma.com/Jekyllhttps://daniel-azuma.com/blog/2021/05/04/discord-command-in-ruby-on-google-cloud-functions-part-4Building a Discord Command in Ruby on Google Cloud Functions: Part 42021-05-04T00:00:00+00:00Daniel Azumadazuma@gmail.comhttp://daniel-azuma.com/<p>This is the last of a four-part series on writing a <a href="https://discord.com/developers/docs/interactions/slash-commands" target="_blank">Discord “slash” command</a> in <a href="https://www.ruby-lang.org" target="_blank">Ruby</a> using <a href="https://cloud.google.com/functions" target="_blank">Google Cloud Functions</a>. In previous parts, we created a Discord command that displays a short Scripture passage, and implemented it in a webhook. In this part, we show how to split a longer passage across multiple messages, by triggering a second Function that makes Discord API calls. It will illustrate how to use Google Cloud Pub/Sub to run background jobs in Cloud Functions, combining an event architecture with the Discord API to implement a more complex application.</p>
<p>Previous articles in this series:</p>
<ul>
<li><a href="/blog/2021/04/30/discord-command-in-ruby-on-google-cloud-functions-intro">Introduction</a></li>
<li><a href="/blog/2021/05/01/discord-command-in-ruby-on-google-cloud-functions-part-1">Part 1</a></li>
<li><a href="/blog/2021/05/02/discord-command-in-ruby-on-google-cloud-functions-part-2">Part 2</a></li>
<li><a href="/blog/2021/05/03/discord-command-in-ruby-on-google-cloud-functions-part-3">Part 3</a></li>
</ul>
<h2 id="the-challenge-size-and-timing">The challenge: size and timing</h2>
<p>So far in this series, we’ve accomplished quite a bit. We’ve written a Discord app in Ruby. We’ve integrated with the Discord API and the Bible API. We’ve added a Discord command to a server, and implemented it in our app. And we’ve followed best practices for handling secrets such as API keys.</p>
<p>However, <a href="/blog/2021/05/03/discord-command-in-ruby-on-google-cloud-functions-part-3">Part 3</a> left off with two significant issues that are impairing the usability of our Discord command.</p>
<p>First, Discord imposes a <a href="https://discord.com/developers/docs/resources/webhook#execute-webhook" target="_blank">2000-character limit</a> on message size. This means our command response cannot display a Scripture passage longer than 2000 characters. It can display John 1:1-5, for example, but not the first chapter of John in its entirety.</p>
<p>Second, if the webhook has been idle for a while, the first call to it sometimes fails because it takes too long. This happens because Cloud Functions may need to spin up a new instance of our webhook to handle the request, and that involves not only starting up the Ruby process, but also making calls to Secret Manager to obtain API credentials, in addition to the latency of the Bible API itself. Together, this “cold start” latency sometimes takes longer than the <a href="https://discord.com/developers/docs/interactions/slash-commands#responding-to-an-interaction" target="_blank">3 seconds</a> allowed by Discord.</p>
<p>One way to address these problems is to <em>defer</em> processing of the command. <a href="https://discord.com/developers/docs/interactions/slash-commands#interaction-response" target="_blank">Discord’s documentation</a> describes this technique, which involves sending back an initial response quickly, but then continuing processing in the background and posting further updates to the response later. For our command, we could use a background process to retrieve a long passage, break it up into sections that are under the 2000-character limit, and issue Discord API calls to post each section in a separate message to the channel.</p>
<p>To implement this kind of “deferred” processing in a serverless environment, we’ll have our app post an event to itself, triggering the background task in a separate Function. We could do so using an event broker such as Apache Kafka, or we can remain within Google’s ecosystem by using a service like <a href="https://cloud.google.com/pubsub" target="_blank">Cloud Pub/Sub</a>.</p>
<p>The following diagrams illustrate how this works. Previously we had a single responder that makes lengthy calls and can return only a single message response.</p>
<p><img src="/img/discord-bot/diagram-part3.png" alt="Flow diagram for the current webhook" /></p>
<p>Now we’ll modify the responder to instead just post an event to Cloud Pub/Sub, and return a message saying “I’m working on it…” Then, the Pub/Sub event will trigger another Cloud Function that performs the actual work. This Pub/Sub initiated function is not subject to the 3 second time limit, and can post an arbitrary number of messages to the channel by calling the Discord API.</p>
<p><img src="/img/discord-bot/diagram-part4.png" alt="Flow diagram for the revised webhook" /></p>
<p>That’s our goal for part 4! Let’s get started.</p>
<h2 id="deferred-processing-using-pubsub">Deferred processing using pub/sub</h2>
<p>These days, the excitement around serverless often has to do with DevOps—or more specifically, the potential to reduce or simplify DevOps. It’s true that DevOps is way too hard, and we’re making progress on improving it, but I think the long-term impact of functions-as-a-service will actually be on <em>software architecture</em>. FaaS has a close affinity with evented systems. As applications get more distributed, and we see more interest in evented control flow to manage that complexity, Functions has the potential to make that really easy.</p>
<p>Acknowledging that trend, Google Cloud Functions already has tight integration with Google’s Pub/Sub service. You can deploy functions that are <a href="https://cloud.google.com/functions/docs/calling/pubsub" target="_blank">triggered on Pub/Sub messages</a>, and we’ll use that strategy to implement deferred processing.</p>
<h3 id="deploying-a-pubsub-subscriber">Deploying a pub/sub subscriber</h3>
<p>Before we can handle events coming from Pub/Sub, we need to create a topic to publish to.</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>gcloud pubsub topics create discord-bot-topic <span class="nt">--project</span><span class="o">=</span><span class="nv">$MY_PROJECT</span></code></pre></figure>
<p>Now we’ll write and deploy a <em>second</em> function for the deferred task.</p>
<p>First we’ll add the new function to <code class="language-plaintext highlighter-rouge">app.rb</code>. Because it handles events coming from Pub/Sub, it takes a <a href="https://github.com/cloudevents/sdk-ruby" target="_blank">CloudEvent</a> (instead of a Rack HTTP request) as the argument. For now we’ll log the event so that we can see when events are triggered.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># app.rb</span>
<span class="c1"># ...</span>
<span class="c1"># Existing webhook function (http)</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">http</span> <span class="s2">"discord_webhook"</span> <span class="k">do</span> <span class="o">|</span><span class="n">request</span><span class="o">|</span>
<span class="n">global</span><span class="p">(</span><span class="ss">:responder</span><span class="p">).</span><span class="nf">respond</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># New subscriber function (cloud event)</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">cloud_event</span> <span class="s2">"discord_subscriber"</span> <span class="k">do</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span>
<span class="n">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="s2">"Received event: </span><span class="si">#{</span><span class="n">event</span><span class="p">.</span><span class="nf">data</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="k">end</span></code></pre></figure>
<p>Note that our app now defines multiple functions. When you deploy to Cloud Functions, you deploy one function at a time, and you must specify which one to deploy by name. So to deploy this new Function, we identify it by its name <code class="language-plaintext highlighter-rouge">"discord_subscriber"</code> and indicate that it should be triggered from the Pub/Sub topic we created.</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>gcloud functions deploy discord_subscriber <span class="se">\</span>
<span class="nt">--project</span><span class="o">=</span><span class="nv">$MY_PROJECT</span> <span class="nt">--region</span><span class="o">=</span>us-central1 <span class="se">\</span>
<span class="nt">--trigger-topic</span><span class="o">=</span>discord-bot-topic <span class="nt">--entry-point</span><span class="o">=</span>discord_subscriber <span class="se">\</span>
<span class="nt">--runtime</span><span class="o">=</span>ruby27</code></pre></figure>
<p>This time, instead of triggering on http requests, we configure this function to trigger when a message is published to our pubsub topic. We can test this by publishing to the topic manually, and then looking at the Cloud Functions logs to see the log entry written by our function.</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>gcloud pubsub topics publish discord-bot-topic <span class="se">\</span>
<span class="nt">--project</span><span class="o">=</span><span class="nv">$MY_PROJECT</span> <span class="nt">--message</span><span class="o">=</span>hello</code></pre></figure>
<h3 id="publishing-an-event">Publishing an event</h3>
<p>But what we actually want is for our <em>webhook</em> to publish the event. So we’ll start by adding the client library for the Pub/Sub API to our Gemfile.</p>
<p>Now, normally, I’d recommend using the main <a href="https://rubygems.org/gems/google-cloud-pubsub" target="_blank">google-cloud-pubsub</a> gem for interacting with Pub/Sub, but for this app, we’re actually going to use the lower-level <a href="https://rubygems.org/gems/google-cloud-pubsub-v1" target="_blank">google-cloud-pubsub-v1</a> library. This is to improve cold start time. The higher-level library takes measurably longer to load in the Cloud Functions environment, and with a three-second budget, any milliseconds we can shave off will be useful. Addintionally, for our purposes, we only need to publish a single message, and don’t need the advanced subscription management code in the higher-level library.</p>
<p>So let’s add the library to our Gemfile:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># Gemfile</span>
<span class="n">source</span> <span class="s2">"https://rubygems.org"</span>
<span class="n">gem</span> <span class="s2">"ed25519"</span><span class="p">,</span> <span class="s2">"~> 1.2"</span>
<span class="n">gem</span> <span class="s2">"faraday"</span><span class="p">,</span> <span class="s2">"~> 1.4"</span>
<span class="n">gem</span> <span class="s2">"functions_framework"</span><span class="p">,</span> <span class="s2">"~> 0.9"</span>
<span class="n">gem</span> <span class="s2">"google-cloud-pubsub-v1"</span><span class="p">,</span> <span class="s2">"~> 0.4"</span>
<span class="n">gem</span> <span class="s2">"google-cloud-secret_manager"</span><span class="p">,</span> <span class="s2">"~> 1.1"</span></code></pre></figure>
<p>After bundle installing, we’ll update our Responder class to publish a message to trigger our job. First, we construct a pubsub client in the initialize method. Again, we’re using the low-level publisher client.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># responder.rb</span>
<span class="c1"># ...</span>
<span class="nb">require</span> <span class="s2">"google/cloud/pubsub/v1/publisher"</span>
<span class="k">class</span> <span class="nc">Responder</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">api_key</span><span class="p">:)</span>
<span class="c1"># Create a verification key (from part 1)</span>
<span class="n">public_key</span> <span class="o">=</span> <span class="no">DISCORD_PUBLIC_KEY</span>
<span class="n">public_key_binary</span> <span class="o">=</span> <span class="p">[</span><span class="n">public_key</span><span class="p">].</span><span class="nf">pack</span><span class="p">(</span><span class="s2">"H*"</span><span class="p">)</span>
<span class="vi">@verification_key</span> <span class="o">=</span> <span class="no">Ed25519</span><span class="o">::</span><span class="no">VerifyKey</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">public_key_binary</span><span class="p">)</span>
<span class="c1"># Create a Bible API client (from part 3)</span>
<span class="vi">@bible_api</span> <span class="o">=</span> <span class="no">BibleApi</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">api_key: </span><span class="n">api_key</span><span class="p">)</span>
<span class="c1"># Create a Pub/Sub client</span>
<span class="vi">@pubsub</span> <span class="o">=</span> <span class="no">Google</span><span class="o">::</span><span class="no">Cloud</span><span class="o">::</span><span class="no">PubSub</span><span class="o">::</span><span class="no">V1</span><span class="o">::</span><span class="no">Publisher</span><span class="o">::</span><span class="no">Client</span><span class="p">.</span><span class="nf">new</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span></code></pre></figure>
<p>Now we can enhance the <code class="language-plaintext highlighter-rouge">handle_command</code> method to publish an event. For now we’ll keep the existing functionality (i.e. calling the Bible API and returning its content), and we’ll just provide additional code to publish to Pub/Sub.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># responder.rb</span>
<span class="c1"># ...</span>
<span class="k">class</span> <span class="nc">Responder</span>
<span class="no">PROJECT_ID</span> <span class="o">=</span> <span class="s2">"my-project-id"</span>
<span class="no">TOPIC_NAME</span> <span class="o">=</span> <span class="s2">"discord-bot-topic"</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">handle_command</span><span class="p">(</span><span class="n">interaction</span><span class="p">)</span>
<span class="n">reference</span> <span class="o">=</span> <span class="n">reference_from_interaction</span><span class="p">(</span><span class="n">interaction</span><span class="p">)</span>
<span class="c1"># Publish a simple pubsub event</span>
<span class="n">topic</span> <span class="o">=</span> <span class="s2">"projects/</span><span class="si">#{</span><span class="no">PROJECT_ID</span><span class="si">}</span><span class="s2">/topics/</span><span class="si">#{</span><span class="no">TOPIC_NAME</span><span class="si">}</span><span class="s2">"</span>
<span class="n">attributes</span> <span class="o">=</span> <span class="p">{</span><span class="ss">message: </span><span class="s2">"Looked up </span><span class="si">#{</span><span class="n">reference</span><span class="si">}</span><span class="s2">"</span><span class="p">}</span>
<span class="vi">@pubsub</span><span class="p">.</span><span class="nf">publish</span><span class="p">(</span><span class="ss">topic: </span><span class="n">topic</span><span class="p">,</span> <span class="ss">messages: </span><span class="p">[{</span><span class="ss">attributes: </span><span class="n">attributes</span><span class="p">}])</span>
<span class="c1"># We'll leave this here for now.</span>
<span class="n">content</span> <span class="o">=</span> <span class="vi">@bible_api</span><span class="p">.</span><span class="nf">lookup_passage</span><span class="p">(</span><span class="n">reference</span><span class="p">)</span>
<span class="p">{</span>
<span class="ss">type: </span><span class="mi">4</span><span class="p">,</span>
<span class="ss">data: </span><span class="p">{</span>
<span class="ss">content: </span><span class="s2">"</span><span class="si">#{</span><span class="n">reference</span><span class="si">}</span><span class="se">\n</span><span class="si">#{</span><span class="n">content</span><span class="si">}</span><span class="s2">"</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span></code></pre></figure>
<p>Now let’s redeploy the webhook function to put this change into production. Recall the command to deploy the webhook function:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>gcloud functions deploy discord_webhook <span class="se">\</span>
<span class="nt">--project</span><span class="o">=</span><span class="nv">$MY_PROJECT</span> <span class="nt">--region</span><span class="o">=</span>us-central1 <span class="se">\</span>
<span class="nt">--trigger-http</span> <span class="nt">--entry-point</span><span class="o">=</span>discord_webhook <span class="se">\</span>
<span class="nt">--runtime</span><span class="o">=</span>ruby27 <span class="nt">--allow-unauthenticated</span></code></pre></figure>
<p>After redploying, we can now invoke the comamnd in Discord, and now in addition to seeing the response in Discord, we can look at the Cloud Function logs and see it trigger the subscriber function.</p>
<p>So far so good—we now know how to trigger background tasks in Cloud Functions using Pub/Sub. Next, we’ll use this mechanism to support splitting a long passage across multiple chat postings.</p>
<h2 id="creating-a-multi-part-response">Creating a multi-part response</h2>
<p>For commands that require a longer time to process, or that need to post mulitple responses to the channel, Discord <a href="https://discord.com/developers/docs/interactions/slash-commands#interaction-response" target="_blank">recommends</a> returning an initial “deferred” response (which displays a “thinking” message in the channel) and following it up with additional responses sent via the Discord API. We’ll now change our response to a deferred response, and move the Scripture lookup logic into the Pub/Sub subscriber.</p>
<h3 id="sending-a-deferred-response">Sending a deferred response</h3>
<p>A deferred response is simply a JSON message with the type set to 5. We’ll delete our code that calls the Bible API, and just return a static response:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># responder.rb</span>
<span class="c1"># ...</span>
<span class="k">class</span> <span class="nc">Responder</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">handle_command</span><span class="p">(</span><span class="n">interaction</span><span class="p">)</span>
<span class="n">reference</span> <span class="o">=</span> <span class="n">reference_from_interaction</span><span class="p">(</span><span class="n">interaction</span><span class="p">)</span>
<span class="c1"># Publish a simple pubsub event</span>
<span class="n">topic</span> <span class="o">=</span> <span class="s2">"projects/</span><span class="si">#{</span><span class="no">PROJECT_ID</span><span class="si">}</span><span class="s2">/topics/</span><span class="si">#{</span><span class="no">TOPIC_NAME</span><span class="si">}</span><span class="s2">"</span>
<span class="n">attributes</span> <span class="o">=</span> <span class="p">{</span><span class="ss">message: </span><span class="s2">"Looked up </span><span class="si">#{</span><span class="n">reference</span><span class="si">}</span><span class="s2">"</span><span class="p">}</span>
<span class="vi">@pubsub</span><span class="p">.</span><span class="nf">publish</span><span class="p">(</span><span class="ss">topic: </span><span class="n">topic</span><span class="p">,</span> <span class="ss">messages: </span><span class="p">[{</span><span class="ss">attributes: </span><span class="n">attributes</span><span class="p">}])</span>
<span class="c1"># Return a Type 5 response</span>
<span class="p">{</span> <span class="ss">type: </span><span class="mi">5</span> <span class="p">}</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span></code></pre></figure>
<p>Next, in order to perform the logic, the deferred task needs two pieces of information. First is, of course, the scripture reference to look up. And second is the <em>interaction token</em> which lets us associate additional responses we send, to the original request. So we’ll update the Pub/Sub message to include these two pieces of information:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># responder.rb</span>
<span class="c1"># ...</span>
<span class="k">class</span> <span class="nc">Responder</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">handle_command</span><span class="p">(</span><span class="n">interaction</span><span class="p">)</span>
<span class="n">reference</span> <span class="o">=</span> <span class="n">reference_from_interaction</span><span class="p">(</span><span class="n">interaction</span><span class="p">)</span>
<span class="c1"># Publish a pubsub event including the reference and interaction token</span>
<span class="n">topic</span> <span class="o">=</span> <span class="s2">"projects/</span><span class="si">#{</span><span class="no">PROJECT_ID</span><span class="si">}</span><span class="s2">/topics/</span><span class="si">#{</span><span class="no">TOPIC_NAME</span><span class="si">}</span><span class="s2">"</span>
<span class="n">attributes</span> <span class="o">=</span> <span class="p">{</span><span class="ss">reference: </span><span class="n">reference</span><span class="p">,</span> <span class="ss">token: </span><span class="n">interaction</span><span class="p">[</span><span class="s2">"token"</span><span class="p">]}</span>
<span class="vi">@pubsub</span><span class="p">.</span><span class="nf">publish</span><span class="p">(</span><span class="ss">topic: </span><span class="n">topic</span><span class="p">,</span> <span class="ss">messages: </span><span class="p">[{</span><span class="ss">attributes: </span><span class="n">attributes</span><span class="p">}])</span>
<span class="c1"># Return a Type 5 response</span>
<span class="p">{</span> <span class="ss">type: </span><span class="mi">5</span> <span class="p">}</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span></code></pre></figure>
<p>Finally, since we no longer use the BibleApi class here in the Responder, we no longer need to pass in the <code class="language-plaintext highlighter-rouge">api_key</code>. So we can remove that code from the Responder’s initialize method. And we can remove the <code class="language-plaintext highlighter-rouge">on_startup</code> code that loads the API key from secrets.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># responder.rb</span>
<span class="c1"># ...</span>
<span class="nb">require</span> <span class="s2">"google/cloud/pubsub/v1/publisher"</span>
<span class="k">class</span> <span class="nc">Responder</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">initialize</span>
<span class="c1"># Create a verification key (from part 1)</span>
<span class="n">public_key</span> <span class="o">=</span> <span class="no">DISCORD_PUBLIC_KEY</span>
<span class="n">public_key_binary</span> <span class="o">=</span> <span class="p">[</span><span class="n">public_key</span><span class="p">].</span><span class="nf">pack</span><span class="p">(</span><span class="s2">"H*"</span><span class="p">)</span>
<span class="vi">@verification_key</span> <span class="o">=</span> <span class="no">Ed25519</span><span class="o">::</span><span class="no">VerifyKey</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">public_key_binary</span><span class="p">)</span>
<span class="c1"># We can now delete this code</span>
<span class="c1"># @bible_api = BibleApi.new(api_key: api_key)</span>
<span class="c1"># Create a Pub/Sub client</span>
<span class="vi">@pubsub</span> <span class="o">=</span> <span class="no">Google</span><span class="o">::</span><span class="no">Cloud</span><span class="o">::</span><span class="no">PubSub</span><span class="o">::</span><span class="no">V1</span><span class="o">::</span><span class="no">Publisher</span><span class="o">::</span><span class="no">Client</span><span class="p">.</span><span class="nf">new</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span></code></pre></figure>
<p>Deploying this webhook update, we can see the effect on the command. It now displays a “thinking” message in response to the “type 5” response:</p>
<p><img src="/img/discord-bot/thinking.png" alt="Displaying a thinking message" /></p>
<h3 id="creating-a-deferred-task-handler">Creating a deferred task handler</h3>
<p>Now that we’re going to write some non-trivial code for the Pub/Sub-triggered Function, let’s create a class for it, like we did previously for the Responder class.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># deferred_task.rb</span>
<span class="k">class</span> <span class="nc">DeferredTask</span>
<span class="k">def</span> <span class="nf">initialize</span>
<span class="c1"># TODO</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">handle_event</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
<span class="c1"># TODO</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Now to create a DeferredTask object on cold start, we can add code to the <code class="language-plaintext highlighter-rouge">on_startup</code> block, just as we did for the Responder class. But hang on… we now have two separate functions—the webhook uses only the Responder class but not DeferredTask, and the subscriber uses only the DeferredTask class but not Responder. It would be nice to for each function to construct only the objects that it needs. This will be especially important in the next section because the DeferredTask will require a round-trip to the Secret Manager, and we’d like to avoid needlessly incurring that latency in the webhook Function.</p>
<p>To ensure each Function constructs only the objects that it needs, we can use <a href="https://cloud.google.com/functions/docs/bestpractices/tips#do_lazy_initialization_of_global_variables" target="_blank">lazy initialization of globals</a>. The Ruby Functions Framework supports this feature by passing a block to <code class="language-plaintext highlighter-rouge">set_global</code>.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># app.rb</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">on_startup</span> <span class="k">do</span>
<span class="c1"># Define how to construct a Responder</span>
<span class="n">set_global</span><span class="p">(</span><span class="ss">:responder</span><span class="p">)</span> <span class="k">do</span>
<span class="c1"># This does not actually run until the discord_webhook</span>
<span class="c1"># function actually accesses it.</span>
<span class="nb">require_relative</span> <span class="s2">"responder"</span>
<span class="no">Responder</span><span class="p">.</span><span class="nf">new</span>
<span class="k">end</span>
<span class="c1"># Define how to construct a DeferredTask</span>
<span class="n">set_global</span><span class="p">(</span><span class="ss">:deferred_task</span><span class="p">)</span> <span class="k">do</span>
<span class="c1"># This does not actually run until the discord_subscriber</span>
<span class="c1"># function actually accesses it.</span>
<span class="nb">require_relative</span> <span class="s2">"deferred_task"</span>
<span class="no">DeferredTask</span><span class="p">.</span><span class="nf">new</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># Call the Responder from the webhook function</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">http</span> <span class="s2">"discord_webhook"</span> <span class="k">do</span> <span class="o">|</span><span class="n">request</span><span class="o">|</span>
<span class="n">global</span><span class="p">(</span><span class="ss">:responder</span><span class="p">).</span><span class="nf">respond</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># Call the DeferredTask from the pubsub subscriber function</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">cloud_event</span> <span class="s2">"discord_subscriber"</span> <span class="k">do</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span>
<span class="n">global</span><span class="p">(</span><span class="ss">:deferred_task</span><span class="p">).</span><span class="nf">handle_event</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
<span class="k">end</span></code></pre></figure>
<p>Notice how even the <code class="language-plaintext highlighter-rouge">require</code> instructions are executed lazily, ensuring the libraries are loaded only if they’re actually going to be used. This can often make a significant difference in a serverless or container-based environment where file system access may be relatively slow.</p>
<h3 id="passing-secrets-to-the-deferred-task">Passing secrets to the deferred task</h3>
<p>Now for the functionality of DeferredTask. We’ll need to call both the Bible API and the Discord API, so we’ll need both the Bible API key and the Discord Bot Token. In Part 3, we set up the Bible API key in a local file for local testing, and uploaded it to the Google Secret Manager for production. Now let’s enhance that Secrets class to add support for the Discord Bot Token:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># secrets.rb</span>
<span class="c1"># ...</span>
<span class="k">class</span> <span class="nc">Secrets</span>
<span class="c1"># ...</span>
<span class="nb">attr_reader</span> <span class="ss">:bible_api_key</span>
<span class="nb">attr_reader</span> <span class="ss">:discord_bot_token</span>
<span class="c1"># ..</span>
<span class="k">def</span> <span class="nf">load_from_hash</span><span class="p">(</span><span class="nb">hash</span><span class="p">)</span>
<span class="vi">@bible_api_key</span> <span class="o">=</span> <span class="nb">hash</span><span class="p">[</span><span class="s2">"bible_api_key"</span><span class="p">]</span>
<span class="vi">@discord_bot_token</span> <span class="o">=</span> <span class="nb">hash</span><span class="p">[</span><span class="s2">"discord_bot_token"</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Now add <code class="language-plaintext highlighter-rouge">discord_bot_token</code> to the secrets.yaml file. (Once again, you can find the token in your bot’s page in the Discord developer console.)</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="c1"># secrets.yaml</span>
<span class="na">bible_api_key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mybibleapikey12345"</span>
<span class="na">discord_bot_token</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mydiscordbottoken12345"</span></code></pre></figure>
<p>And upload the updated file to Secret Manager:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>gcloud secrets versions add discord-bot-secrets <span class="se">\</span>
<span class="nt">--project</span><span class="o">=</span><span class="nv">$MY_PROJECT</span> <span class="nt">--data-file</span><span class="o">=</span>secrets.yaml</code></pre></figure>
<p>Now let’s update our startup code to access these secrets and pass them into the DeferredTask constructor:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># app.rb</span>
<span class="c1"># ...</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">on_startup</span> <span class="k">do</span>
<span class="c1"># ...</span>
<span class="c1"># Define how to construct a DeferredTask</span>
<span class="n">set_global</span><span class="p">(</span><span class="ss">:deferred_task</span><span class="p">)</span> <span class="k">do</span>
<span class="c1"># This does not actually run until the discord_subscriber</span>
<span class="c1"># function actually accesses it.</span>
<span class="nb">require_relative</span> <span class="s2">"secrets"</span>
<span class="nb">require_relative</span> <span class="s2">"deferred_task"</span>
<span class="n">secrets</span> <span class="o">=</span> <span class="no">Secrets</span><span class="p">.</span><span class="nf">new</span>
<span class="no">DeferredTask</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">api_key: </span><span class="n">secrets</span><span class="p">.</span><span class="nf">bible_api_key</span><span class="p">,</span>
<span class="ss">bot_token: </span><span class="n">secrets</span><span class="p">.</span><span class="nf">discord_bot_token</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># ...</span></code></pre></figure>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># deferred_task.rb</span>
<span class="nb">require_relative</span> <span class="s2">"bible_api"</span>
<span class="nb">require_relative</span> <span class="s2">"discord_api"</span>
<span class="k">class</span> <span class="nc">DeferredTask</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">api_key</span><span class="p">:,</span> <span class="n">bot_token</span><span class="p">:)</span>
<span class="vi">@bible_api_client</span> <span class="o">=</span> <span class="no">BibleApi</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">api_key: </span><span class="n">api_key</span><span class="p">)</span>
<span class="vi">@discord_api_client</span> <span class="o">=</span> <span class="no">DiscordApi</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">bot_token: </span><span class="n">bot_token</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span></code></pre></figure>
<p>Now that we’ve accessed the secrets and constructed the needed clients, we’re ready to implement the task.</p>
<h3 id="sending-multiple-ressages">Sending multiple ressages</h3>
<p>Our DeferredTask will make two types of calls to the Discord API. First, we’ll call <a href="https://discord.com/developers/docs/interactions/slash-commands#edit-original-interaction-response" target="_blank">Edit Original Interaction Response</a> to update the original response (which was a Type 5 “thinking” message) to indicate that the bot is done “thinking” and is ready to display content. Then we’ll call <a href="https://discord.com/developers/docs/interactions/slash-commands#create-followup-message" target="_blank">Create Followup Message</a> potentially multiple times, to post messages with Scripture content.</p>
<p>First, let’s implement those calls in our Discord API client:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># discord_api.rb</span>
<span class="c1"># ,,,</span>
<span class="k">class</span> <span class="nc">DiscordApi</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">edit_interaction_response</span><span class="p">(</span><span class="n">interaction_token</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>
<span class="n">data_json</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">({</span><span class="ss">content: </span><span class="n">content</span><span class="p">})</span>
<span class="n">headers</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"Content-Type"</span> <span class="o">=></span> <span class="s2">"application/json"</span><span class="p">}</span>
<span class="n">call_api</span><span class="p">(</span><span class="s2">"/webhooks/</span><span class="si">#{</span><span class="vi">@client_id</span><span class="si">}</span><span class="s2">/</span><span class="si">#{</span><span class="n">interaction_token</span><span class="si">}</span><span class="s2">/messages/@original"</span><span class="p">,</span>
<span class="ss">method: :patch</span><span class="p">,</span> <span class="ss">body: </span><span class="n">data_json</span><span class="p">,</span> <span class="ss">headers: </span><span class="n">headers</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">create_followup</span><span class="p">(</span><span class="n">interaction_token</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>
<span class="n">data_json</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">({</span><span class="ss">content: </span><span class="n">content</span><span class="p">})</span>
<span class="n">headers</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"Content-Type"</span> <span class="o">=></span> <span class="s2">"application/json"</span><span class="p">}</span>
<span class="n">call_api</span><span class="p">(</span><span class="s2">"/webhooks/</span><span class="si">#{</span><span class="vi">@client_id</span><span class="si">}</span><span class="s2">/</span><span class="si">#{</span><span class="n">interaction_token</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span>
<span class="ss">method: :post</span><span class="p">,</span> <span class="ss">body: </span><span class="n">data_json</span><span class="p">,</span> <span class="ss">headers: </span><span class="n">headers</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span></code></pre></figure>
<p>And finally, implement the logic in DeferredTask. For simplicity, this code will naively split the content into 2000-character chunks. For a better experience, we’d probably want to split on word boundaries, or even better, verse boundaries.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># deferred_task.rb</span>
<span class="k">class</span> <span class="nc">DeferredTask</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">handle_event</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
<span class="c1"># Get data from the Pub/Sub message</span>
<span class="n">attributes</span> <span class="o">=</span> <span class="n">event</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s2">"message"</span><span class="p">][</span><span class="s2">"attributes"</span><span class="p">]</span>
<span class="n">reference</span> <span class="o">=</span> <span class="n">attributes</span><span class="p">[</span><span class="s2">"reference"</span><span class="p">]</span>
<span class="n">interaction_token</span> <span class="o">=</span> <span class="n">attributes</span><span class="p">[</span><span class="s2">"token"</span><span class="p">]</span>
<span class="c1"># Get the content from the Bible API</span>
<span class="n">full_content</span> <span class="o">=</span> <span class="vi">@bible_api_client</span><span class="p">.</span><span class="nf">lookup_passage</span><span class="p">(</span><span class="n">reference</span><span class="p">)</span>
<span class="c1"># Edit the original interaction response to signal we're done "thinking"</span>
<span class="vi">@discord_api_client</span><span class="p">.</span><span class="nf">edit_interaction_response</span><span class="p">(</span>
<span class="n">interaction_token</span><span class="p">,</span> <span class="s2">"Looked up </span><span class="si">#{</span><span class="n">reference</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="c1"># Split the content into 2000-character sections and send Discord meessages</span>
<span class="n">full_content</span><span class="p">.</span><span class="nf">scan</span><span class="p">(</span><span class="sr">/.{1,2000}/m</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">section</span><span class="o">|</span>
<span class="c1"># Space API calls out a bit, to avoid Discord's rate limiting.</span>
<span class="nb">sleep</span> <span class="mf">0.5</span>
<span class="vi">@discord_api_client</span><span class="p">.</span><span class="nf">create_followup</span><span class="p">(</span><span class="n">interaction_token</span><span class="p">,</span> <span class="n">section</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Redeploy both the webhook and the subscriber, and our final app is ready!</p>
<p><img src="/img/discord-bot/final-output.png" alt="The final output" /></p>
<h2 id="now-what">Now what?</h2>
<p>We now have a working, nontrivial Discord command, running in Google Cloud Functions! We’ve also covered some of the key techniques in a robust serverless app, including handling secrets in production, and using a pub/sub system to schedule tasks.</p>
<p>We do still have a few TODOs, which I will leave as “exercises for the reader”. For example:</p>
<ul>
<li>
<p>It would make for a better experience to parse “normal” Scripture reference syntax such as “Matthew 1:2-3” rather than forcing users to use the Bible API’s reference format. When I wrote this app for my church, I also used a string distance metric to detect book name abbreviations such as “Matt” or “Mt”.</p>
</li>
<li>
<p>Currently, if the Bible API returns an error (e.g. because the Scripture reference was invalid), our BibleApi client raises an exception. This causes the deferred task function to terminate, but the user doesn’t see any indication of this in the discord channel. It would be a good idea to catch any exceptions and post an error message to the channel.</p>
</li>
<li>
<p>It would be nice to split content into sections on verse boundaries rather than just taking 2000-character chunks. When I wrote this for my church, I actually passed an option to the Bible API call that causes it to return the passage in semantic structured JSON rather than a simple string. I then had to parse that data structure to construct a displayable string, but because it was structured data, I could extract the exact verse boundaries, and customize the output format.</p>
</li>
</ul>
<p>Additionally, there are alternative approaches that could be explored. One might consider, for example, using a background task manager such as <a href="https://cloud.google.com/tasks" target="_blank">Google Cloud Tasks</a> instead of Pub/Sub to schedule the deferred task, and there are pros and cons to that approach. Discord also supports receiving events <a href="https://discord.com/developers/docs/topics/gateway" target="_blank">via WebSocket</a> rather than a webhook. This option could provide much better performance, but would require reworking much of the design and deployment of the app, and may increase the cost of running it.</p>
<p>But overall, the techniques that we’ve covered should provide a good foundation for writing a variety of serverless applications using Google Cloud Platform. It should also provide a good set of getting-started information on writing Discord apps. I hope it was helpful!</p>
<h2 id="notes">Notes</h2>
<p>I work at Google in my day job, so all code in this article is:</p>
<figure class="highlight"><pre><code class="language-text" data-lang="text">Copyright 2021 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.</code></pre></figure>
2021-05-04T00:00:00+00:00https://daniel-azuma.com/blog/2021/05/03/discord-command-in-ruby-on-google-cloud-functions-part-3Building a Discord Command in Ruby on Google Cloud Functions: Part 32021-05-03T00:00:00+00:00Daniel Azumadazuma@gmail.comhttp://daniel-azuma.com/<p>This is the third of a four-part series on writing a <a href="https://discord.com/developers/docs/interactions/slash-commands" target="_blank">Discord “slash” command</a> in <a href="https://www.ruby-lang.org" target="_blank">Ruby</a> using <a href="https://cloud.google.com/functions" target="_blank">Google Cloud Functions</a>. In previous parts, we deployed a Discord app to Google Cloud Functions, and added a command to a server using the Discord API. In this part, we actually implement the command, calling an external API and displaying results in the channel. It will illustrate how to respond to commands, and how to handle secrets such as API keys in production.</p>
<p>Previous articles in this series:</p>
<ul>
<li><a href="/blog/2021/04/30/discord-command-in-ruby-on-google-cloud-functions-intro">Introduction</a></li>
<li><a href="/blog/2021/05/01/discord-command-in-ruby-on-google-cloud-functions-part-1">Part 1</a></li>
<li><a href="/blog/2021/05/02/discord-command-in-ruby-on-google-cloud-functions-part-2">Part 2</a></li>
</ul>
<h2 id="responding-to-commands">Responding to commands</h2>
<p>When we left off in <a href="/blog/2021/05/02/discord-command-in-ruby-on-google-cloud-functions-part-2">part 2</a>, we had created a command in a Discord server, but hadn’t yet implemented the back end. We’ll do so now.</p>
<p>When someone invokes a command, an <a href="https://discord.com/developers/docs/interactions/slash-commands#interaction" target="_blank">interaction request</a> of type 2 gets sent to our webhook. The JSON data sent with this request will include the command that was sent, and any options that were provided. For our command, that includes a Scripture reference.</p>
<p>To start off, let’s update the <code class="language-plaintext highlighter-rouge">Responder#respond</code> method to recognize type 2, and call a new method <code class="language-plaintext highlighter-rouge">handle_command</code> that we will implement:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># responder.rb</span>
<span class="c1"># ...</span>
<span class="k">class</span> <span class="nc">Responder</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">respond</span><span class="p">(</span><span class="n">rack_request</span><span class="p">)</span>
<span class="n">raw_body</span> <span class="o">=</span> <span class="n">rack_request</span><span class="p">.</span><span class="nf">body</span><span class="p">.</span><span class="nf">read</span>
<span class="k">unless</span> <span class="n">verify_request</span><span class="p">(</span><span class="n">raw_body</span><span class="p">,</span> <span class="n">rack_request</span><span class="p">.</span><span class="nf">env</span><span class="p">)</span>
<span class="c1"># Discord expects a 401 response if the verification failed</span>
<span class="k">return</span> <span class="p">[</span><span class="mi">401</span><span class="p">,</span>
<span class="p">{</span><span class="s2">"Content-Type"</span> <span class="o">=></span> <span class="s2">"text/plain"</span><span class="p">},</span>
<span class="p">[</span><span class="s2">"invalid request signature"</span><span class="p">]]</span>
<span class="k">end</span>
<span class="n">interaction</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">raw_body</span><span class="p">)</span>
<span class="k">case</span> <span class="n">interaction</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span>
<span class="k">when</span> <span class="mi">1</span>
<span class="c1"># Ping</span>
<span class="p">{</span><span class="ss">type: </span><span class="mi">1</span><span class="p">}</span>
<span class="k">when</span> <span class="mi">2</span>
<span class="c1"># Command</span>
<span class="n">handle_command</span><span class="p">(</span><span class="n">interaction</span><span class="p">)</span>
<span class="k">else</span>
<span class="p">[</span><span class="mi">400</span><span class="p">,</span>
<span class="p">{</span><span class="s2">"Content-Type"</span> <span class="o">=></span> <span class="s2">"text/plain"</span><span class="p">},</span>
<span class="p">[</span><span class="s2">"Unrecognized interaction type"</span><span class="p">]]</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span></code></pre></figure>
<p>Now for the <code class="language-plaintext highlighter-rouge">handle_command</code> method. Discord expects an <a href="https://discord.com/developers/docs/interactions/slash-commands#interaction-response" target="_blank">interaction response</a> JSON message in the HTTP response. Typically, the response includes a message that the command will then display in the Discord channel. We signal such a response by setting the “type” field to 4, and returning the text to display in a “content” field.</p>
<p>To start off, let’s construct a response by echoing back the Scripture reference that we were asked to look up. First we’ll write code to parse the interaction and get the option with name “reference”. For simplicity, we’ll just raise an exception (which Cloud Functions will translate to a 500 response) if the command data doesn’t have the expected format.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># responder.rb</span>
<span class="c1"># ...</span>
<span class="k">class</span> <span class="nc">Responder</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">reference_from_interaction</span><span class="p">(</span><span class="n">interaction</span><span class="p">)</span>
<span class="n">command_data</span> <span class="o">=</span> <span class="n">interaction</span><span class="p">[</span><span class="s2">"data"</span><span class="p">]</span>
<span class="k">unless</span> <span class="n">command_data</span><span class="p">[</span><span class="s2">"name"</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"bible"</span>
<span class="k">raise</span> <span class="s2">"Unexpected command: </span><span class="si">#{</span><span class="n">command_data</span><span class="p">[</span><span class="s1">'name'</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
<span class="n">command_data</span><span class="p">[</span><span class="s2">"options"</span><span class="p">].</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">option</span><span class="o">|</span>
<span class="k">if</span> <span class="n">option</span><span class="p">[</span><span class="s2">"name"</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"reference"</span>
<span class="k">return</span> <span class="n">option</span><span class="p">[</span><span class="s2">"value"</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">raise</span> <span class="s2">"No reference option found"</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span></code></pre></figure>
<p>Now we’ll use that to implement <code class="language-plaintext highlighter-rouge">handle_command</code> to return a message to display. Remember that if we return a hash from a Cloud Function, it is automatically rendered as JSON in the HTTP response.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># responder.rb</span>
<span class="c1"># ...</span>
<span class="k">class</span> <span class="nc">Responder</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">handle_command</span><span class="p">(</span><span class="n">interaction</span><span class="p">)</span>
<span class="n">reference</span> <span class="o">=</span> <span class="n">reference_from_interaction</span><span class="p">(</span><span class="n">interaction</span><span class="p">)</span>
<span class="p">{</span>
<span class="ss">type: </span><span class="mi">4</span><span class="p">,</span>
<span class="ss">data: </span><span class="p">{</span>
<span class="ss">content: </span><span class="s2">"You looked up </span><span class="si">#{</span><span class="n">reference</span><span class="si">}</span><span class="s2">"</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span></code></pre></figure>
<p>Now let’s redeploy the function:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>bundle <span class="nb">install</span>
<span class="nv">$ </span>gcloud functions deploy discord_webhook <span class="se">\</span>
<span class="nt">--project</span><span class="o">=</span><span class="nv">$MY_PROJECT</span> <span class="nt">--region</span><span class="o">=</span>us-central1 <span class="se">\</span>
<span class="nt">--trigger-http</span> <span class="nt">--entry-point</span><span class="o">=</span>discord_webhook <span class="se">\</span>
<span class="nt">--runtime</span><span class="o">=</span>ruby27 <span class="nt">--allow-unauthenticated</span></code></pre></figure>
<p>And when we go back to our Discord channel and invoke the command… success!</p>
<p><img src="/img/discord-bot/test-input.png" alt="Invoking a command" /></p>
<p><img src="/img/discord-bot/temp-output.png" alt="Simple output" /></p>
<h2 id="calling-an-external-api">Calling an external API</h2>
<p>Now it’s time to implement the main functionality of our command, that of actually returning Scripture content from an API. We’ll access a suitable API, show how to manage its API key in production using <a href="https://cloud.google.com/secret-manager" target="_blank">Google Secret Manager</a>, and write code to make API calls and include the results in the command response.</p>
<h3 id="signing-up-with-apibible">Signing up with API.Bible</h3>
<p><a href="https://scripture.api.bible" target="_blank">API.Bible</a> is a basic Scripture lookup API that boasts thousands of versions and translations, and allows reference lookup and keyword search. Their free tier includes access to public domain translations like the World English Bible, and a modest number of calls per day that will be more than adequate for my church’s needs.</p>
<p>I created an account and registered an application. The site claims that new accounts are subject to approval, but it looks like, since mine was for non-commercial use by a small church, it pretty much went through immediately. As part of my account, they gave me an API key. This is effectively a password to my account, and I need to handle it like any secret, keeping it out of code, source control, and any unencrypted communication.</p>
<p>The API website also includes “live” <a href="https://scripture.api.bible/livedocs" target="_blank">reference documentation</a> that includes the ability to make API calls directly from the web page. From here, I identified the “passages” API call as the likely call I’ll need for my Discord command. The resource path for the request includes the ID of the Bible version, and the reference in a specific format. Then the response is a JSON payload including the passage content. Looks good, let’s experiment with it!</p>
<h3 id="calling-the-api">Calling the API</h3>
<p>As we did in part 2 with the Discord API, we’ll create a simple client class for the Bible API. We’ll start with a helper method that makes HTTP calls and handles results. This helper will also set the needed API key header. As we did with the Discord client, we’ll pass in the API key as a constructor argument.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># bible_api.rb</span>
<span class="nb">require</span> <span class="s2">"faraday"</span>
<span class="nb">require</span> <span class="s2">"json"</span>
<span class="k">class</span> <span class="nc">BibleApi</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">api_key</span><span class="p">:)</span>
<span class="vi">@api_key</span> <span class="o">=</span> <span class="n">api_key</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">call_api</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="ss">params: </span><span class="kp">nil</span><span class="p">)</span>
<span class="n">faraday</span> <span class="o">=</span> <span class="no">Faraday</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
<span class="ss">url: </span><span class="s2">"https://api.scripture.api.bible"</span><span class="p">,</span>
<span class="ss">headers: </span><span class="p">{</span>
<span class="s2">"api-key"</span> <span class="o">=></span> <span class="vi">@api_key</span>
<span class="p">}</span>
<span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">faraday</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">req</span><span class="o">|</span>
<span class="n">req</span><span class="p">.</span><span class="nf">params</span> <span class="o">=</span> <span class="n">params</span> <span class="k">if</span> <span class="n">params</span>
<span class="k">end</span>
<span class="n">body</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="nf">body</span><span class="p">)</span>
<span class="k">unless</span> <span class="n">response</span><span class="p">.</span><span class="nf">status</span> <span class="o">==</span> <span class="mi">200</span> <span class="o">&&</span> <span class="n">body</span><span class="p">[</span><span class="s2">"data"</span><span class="p">]</span>
<span class="k">raise</span> <span class="n">body</span><span class="p">[</span><span class="s2">"message"</span><span class="p">]</span> <span class="o">||</span> <span class="s2">"Unknown error"</span>
<span class="k">end</span>
<span class="n">body</span><span class="p">[</span><span class="s2">"data"</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Now we’ll implement a method for the “passages” call. It will take the reference as an argument. For simplicity, we’ll just hard-code the Bible ID for a reasonable translation, the World English Bible, Ecumenical version.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># bible_api.rb</span>
<span class="c1"># ...</span>
<span class="k">class</span> <span class="nc">BibleApi</span>
<span class="no">WEB_BIBLE_ID</span> <span class="o">=</span> <span class="s2">"9879dbb7cfe39e4d-01"</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">lookup_passage</span><span class="p">(</span><span class="n">reference</span><span class="p">)</span>
<span class="n">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">"content-type"</span> <span class="o">=></span> <span class="s2">"text"</span><span class="p">,</span>
<span class="s2">"include-titles"</span> <span class="o">=></span> <span class="s2">"false"</span>
<span class="p">}</span>
<span class="n">resource</span> <span class="o">=</span> <span class="s2">"/v1/bibles/</span><span class="si">#{</span><span class="no">WEB_BIBLE_ID</span><span class="si">}</span><span class="s2">/passages/</span><span class="si">#{</span><span class="n">reference</span><span class="si">}</span><span class="s2">"</span>
<span class="n">response_object</span> <span class="o">=</span> <span class="n">call_api</span><span class="p">(</span><span class="n">resource</span><span class="p">,</span> <span class="ss">params: </span><span class="n">params</span><span class="p">)</span>
<span class="n">response_object</span><span class="p">[</span><span class="s2">"content"</span><span class="p">]</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span></code></pre></figure>
<p>We can now test this by creating a simple command-line script using Toys. As before, we’ll pass the API key in on the command line, to keep it out of our code. We’ll also pass the Scripture reference on the command line.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># .toys.rb</span>
<span class="n">tool</span> <span class="s2">"lookup"</span> <span class="k">do</span>
<span class="n">flag</span> <span class="ss">:api_key</span><span class="p">,</span> <span class="s2">"--api-key TOKEN"</span>
<span class="n">required_arg</span> <span class="ss">:reference</span>
<span class="k">def</span> <span class="nf">run</span>
<span class="nb">require_relative</span> <span class="s2">"bible_api"</span>
<span class="n">client</span> <span class="o">=</span> <span class="no">BibleApi</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">api_key: </span><span class="n">api_key</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="nf">lookup_passage</span><span class="p">(</span><span class="n">reference</span><span class="p">)</span>
<span class="nb">puts</span> <span class="n">result</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># ... and other scripts from before</span></code></pre></figure>
<p>Note that the Bible API uses a particular format for Scripture references. We need to use that format when we test our API client:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>toys lookup <span class="nt">--api-key</span><span class="o">=</span><span class="nv">$MY_API_KEY</span> JHN.1.1
<span class="o">[</span>1] In the beginning was the Word, and the Word was with God, and the Word was God.</code></pre></figure>
<h3 id="calling-the-api-from-the-webhook">Calling the API from the webhook</h3>
<p>Now that we have the Bible API working, we can write the rest of the code to integrate it into our webhook. We’ll construct a Bible API client in our Responder class, and rewrite our <code class="language-plaintext highlighter-rouge">Responder#handle_command</code> method to call it to get the passage content.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># responder.rb</span>
<span class="c1"># ...</span>
<span class="nb">require_relative</span> <span class="s2">"bible_api"</span>
<span class="k">class</span> <span class="nc">Responder</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">api_key</span><span class="p">:)</span>
<span class="c1"># Create a verification key (from part 1)</span>
<span class="n">public_key</span> <span class="o">=</span> <span class="no">DISCORD_PUBLIC_KEY</span>
<span class="n">public_key_binary</span> <span class="o">=</span> <span class="p">[</span><span class="n">public_key</span><span class="p">].</span><span class="nf">pack</span><span class="p">(</span><span class="s2">"H*"</span><span class="p">)</span>
<span class="vi">@verification_key</span> <span class="o">=</span> <span class="no">Ed25519</span><span class="o">::</span><span class="no">VerifyKey</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">public_key_binary</span><span class="p">)</span>
<span class="c1"># Create a Bible API client</span>
<span class="vi">@bible_api</span> <span class="o">=</span> <span class="no">BibleApi</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">api_key: </span><span class="n">api_key</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">handle_command</span><span class="p">(</span><span class="n">interaction</span><span class="p">)</span>
<span class="c1"># Call the Bible API to get the content to return</span>
<span class="n">reference</span> <span class="o">=</span> <span class="n">reference_from_interaction</span><span class="p">(</span><span class="n">interaction</span><span class="p">)</span>
<span class="n">content</span> <span class="o">=</span> <span class="vi">@bible_api</span><span class="p">.</span><span class="nf">lookup_passage</span><span class="p">(</span><span class="n">reference</span><span class="p">)</span>
<span class="p">{</span>
<span class="ss">type: </span><span class="mi">4</span><span class="p">,</span>
<span class="ss">data: </span><span class="p">{</span>
<span class="ss">content: </span><span class="s2">"</span><span class="si">#{</span><span class="n">reference</span><span class="si">}</span><span class="se">\n</span><span class="si">#{</span><span class="n">content</span><span class="si">}</span><span class="s2">"</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span></code></pre></figure>
<p>Okay, we’re almost there. We have a working Bible API client, and our webhook command handler calls it to get content, and returns it to Discord for display. There’s just one problem. The Bible API client needs an API key. We’ve added an <code class="language-plaintext highlighter-rouge">api_key</code> argument to the Responder constructor so we can pass it down, but where does the Responder get the API key from? Remember from part 1 that a Responder is created when the function starts up:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># app.rb</span>
<span class="nb">require</span> <span class="s2">"functions_framework"</span>
<span class="nb">require_relative</span> <span class="s2">"responder"</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">on_startup</span> <span class="k">do</span>
<span class="n">set_global</span><span class="p">(</span><span class="ss">:responder</span><span class="p">,</span> <span class="no">Responder</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">api_key: </span><span class="s2">"???"</span><span class="p">))</span>
<span class="k">end</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">http</span> <span class="s2">"discord_webhook"</span> <span class="k">do</span> <span class="o">|</span><span class="n">request</span><span class="o">|</span>
<span class="n">global</span><span class="p">(</span><span class="ss">:responder</span><span class="p">).</span><span class="nf">respond</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="k">end</span></code></pre></figure>
<p>Somehow we need to pass a secret into this app, and do so safely and securely. Let’s talk about how to do that.</p>
<h3 id="handling-secrets">Handling secrets</h3>
<p>There are many ways to handle secrets without exposing them in our code. In the past, it has been common to set secrets in environment variables, or store them in special source files that we exclude from source control. These techniques work, but each has its flaws. Environment variables can be logged by accident or read by malicious code. Files can be accessed by anyone with access to the deployment image.</p>
<p>The technique we’ll implement here uses a local file for local testing, but uses a secret handling service, <a href="https://cloud.google.com/secret-manager" target="_blank">Google Cloud Secret Manager</a>, for production. When we test locally, the secrets will be present in a local file that is excluded from source control, letting us test without having to make calls to a cloud service. When we deploy, however, we <em>exclude</em> this file, avoiding the security risk of having the file present in our deployment image. Instead, in production, we load the secret directly into memory from the Secret Manager service.</p>
<p>So the basic logic is: first check for a file, and if that’s not present, invoke Secret Manager. We’ll implement this logic in a new class, Secrets.</p>
<p>First, we’ll add the client library for Secret Manager to our Gemfile:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># Gemfile</span>
<span class="n">source</span> <span class="s2">"https://rubygems.org"</span>
<span class="n">gem</span> <span class="s2">"ed25519"</span><span class="p">,</span> <span class="s2">"~> 1.2"</span>
<span class="n">gem</span> <span class="s2">"faraday"</span><span class="p">,</span> <span class="s2">"~> 1.4"</span>
<span class="n">gem</span> <span class="s2">"functions_framework"</span><span class="p">,</span> <span class="s2">"~> 0.9"</span>
<span class="n">gem</span> <span class="s2">"google-cloud-secret_manager"</span><span class="p">,</span> <span class="s2">"~> 1.1"</span></code></pre></figure>
<p>Don’t forget to <code class="language-plaintext highlighter-rouge">bundle install</code> to ensure your <code class="language-plaintext highlighter-rouge">Gemfile.lock</code> gets updated.</p>
<p>Now let’s implement the Secrets class. In the code below, make sure you substitute the name of your Google Cloud project as the value of the<code class="language-plaintext highlighter-rouge">PROJECT_ID</code> constant. I’ve also chosen a name for the secret in the <code class="language-plaintext highlighter-rouge">SECRET_NAME</code> constant, but you can use a different name if you want.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># secrets.rb</span>
<span class="nb">require</span> <span class="s2">"psych"</span>
<span class="nb">require</span> <span class="s2">"google/cloud/secret_manager"</span>
<span class="k">class</span> <span class="nc">Secrets</span>
<span class="no">SECRETS_FILE</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">__dir__</span><span class="p">,</span> <span class="s2">"secrets.yaml"</span><span class="p">)</span>
<span class="no">SECRET_NAME</span> <span class="o">=</span> <span class="s2">"discord-bot-secrets"</span>
<span class="no">PROJECT_ID</span> <span class="o">=</span> <span class="s2">"my-project-id"</span>
<span class="k">def</span> <span class="nf">initialize</span>
<span class="k">if</span> <span class="no">File</span><span class="p">.</span><span class="nf">file?</span><span class="p">(</span><span class="no">SECRETS_FILE</span><span class="p">)</span>
<span class="n">load_from_file</span>
<span class="k">else</span>
<span class="n">load_from_secret_manager</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="nb">attr_reader</span> <span class="ss">:bible_api_key</span>
<span class="k">def</span> <span class="nf">load_from_file</span>
<span class="n">secret_data</span> <span class="o">=</span> <span class="no">Psych</span><span class="p">.</span><span class="nf">load_file</span><span class="p">(</span><span class="no">SECRETS_FILE</span><span class="p">)</span>
<span class="n">load_from_hash</span><span class="p">(</span><span class="n">secret_data</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">load_from_secret_manager</span>
<span class="n">secret_manager</span> <span class="o">=</span> <span class="no">Google</span><span class="o">::</span><span class="no">Cloud</span><span class="o">::</span><span class="no">SecretManager</span><span class="p">.</span><span class="nf">secret_manager_service</span>
<span class="n">version_name</span> <span class="o">=</span> <span class="n">secret_manager</span><span class="p">.</span><span class="nf">secret_version_path</span><span class="p">(</span>
<span class="ss">project: </span><span class="no">PROJECT_ID</span><span class="p">,</span> <span class="ss">secret: </span><span class="no">SECRET_NAME</span><span class="p">,</span> <span class="ss">secret_version: </span><span class="s2">"latest"</span>
<span class="p">)</span>
<span class="n">version_data</span> <span class="o">=</span> <span class="n">secret_manager</span><span class="p">.</span><span class="nf">access_secret_version</span><span class="p">(</span><span class="ss">name: </span><span class="n">version_name</span><span class="p">)</span>
<span class="n">secret_data</span> <span class="o">=</span> <span class="no">Psych</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="n">version_data</span><span class="p">.</span><span class="nf">payload</span><span class="p">.</span><span class="nf">data</span><span class="p">)</span>
<span class="n">load_from_hash</span><span class="p">(</span><span class="n">secret_data</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">load_from_hash</span><span class="p">(</span><span class="nb">hash</span><span class="p">)</span>
<span class="vi">@bible_api_key</span> <span class="o">=</span> <span class="nb">hash</span><span class="p">[</span><span class="s2">"bible_api_key"</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Now we can invoke this class from our function and use it to retrieve the API key:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># app.rb</span>
<span class="nb">require</span> <span class="s2">"functions_framework"</span>
<span class="nb">require_relative</span> <span class="s2">"responder"</span>
<span class="nb">require_relative</span> <span class="s2">"secrets"</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">on_startup</span> <span class="k">do</span>
<span class="n">secrets</span> <span class="o">=</span> <span class="no">Secrets</span><span class="p">.</span><span class="nf">new</span>
<span class="n">set_global</span><span class="p">(</span><span class="ss">:responder</span><span class="p">,</span> <span class="no">Responder</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">api_key: </span><span class="n">secrets</span><span class="p">.</span><span class="nf">bible_api_key</span><span class="p">))</span>
<span class="k">end</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">http</span> <span class="s2">"discord_webhook"</span> <span class="k">do</span> <span class="o">|</span><span class="n">request</span><span class="o">|</span>
<span class="n">global</span><span class="p">(</span><span class="ss">:responder</span><span class="p">).</span><span class="nf">respond</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="k">end</span></code></pre></figure>
<p>To get this to work for local testing, we just need to create the “secrets.yaml” file, substituting your actual API key:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="c1"># secrets.yaml</span>
<span class="na">bible_api_key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mybibleapikey12345"</span></code></pre></figure>
<p>Make sure it stays out of source control. I use git, so I’ll add a <code class="language-plaintext highlighter-rouge">.gitignore</code> file:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c"># .gitignore</span>
secrets.yaml</code></pre></figure>
<p>We also want to avoid <em>deploying</em> this file. By default, Cloud Functions honors <code class="language-plaintext highlighter-rouge">.gitignore</code> when deploying, so it will do this by default. But if you don’t use git, or you have more complex requirements, you can configure which files are omitted from deployment by providing a <a href="https://cloud.google.com/sdk/gcloud/reference/topic/gcloudignore" target="_blank">gcloudignore file</a>.</p>
<p>In production, we want to read this data from Secret Manager, so we’ll need to upload it to Secret Manager first, using the Google Cloud command line:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>gcloud secrets create discord-bot-secrets <span class="se">\</span>
<span class="nt">--project</span><span class="o">=</span><span class="nv">$MY_PROJECT</span> <span class="nt">--data-file</span><span class="o">=</span>secrets.yaml</code></pre></figure>
<p>There’s one more thing we need to do. We’ve written code that loads the secrets from Secret Manager in production, but we need to grant that code <em>access</em> to Secret Manager. To do so, we note that when an app runs in Cloud Functions, by default it uses a particular service account called <code class="language-plaintext highlighter-rouge">$MY_PROJECT@appspot.gserviceaccount.com</code> to talk to Google Cloud APIs. So we need to grant that service account access to the secret:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>gcloud secrets add-iam-policy-binding discord-bot-secrets <span class="se">\</span>
<span class="nt">--project</span><span class="o">=</span><span class="nv">$MY_PROJECT</span> <span class="nt">--role</span><span class="o">=</span>roles/secretmanager.secretAccessor <span class="se">\</span>
<span class="nt">--member</span><span class="o">=</span>serviceAccount:<span class="nv">$MY_PROJECT</span>@appspot.gserviceaccount.com</code></pre></figure>
<p>For both the above commands, make sure the project name and the secret name <code class="language-plaintext highlighter-rouge">discord-bot-secrets</code> matches the corresponding names you used in the constants in the Secrets class.</p>
<p>Now we can redeploy our Function, and try it out.</p>
<p><img src="/img/discord-bot/test-input.png" alt="Invoking a command" /></p>
<p><img src="/img/discord-bot/short-passage.png" alt="Displaying a short passage" /></p>
<p>Woot!</p>
<h2 id="now-what">Now what?</h2>
<p>When I got to this point, things were mostly working, but I encountered a few issues.</p>
<p>The bot wasn’t very stable. Sometimes, the first time I tried running a command, I’d get a failure message, but then the next time it would work. Oddly, when I looked at the logs in Cloud Functions, everything seemed to be fine, even for the supposedly failed request.</p>
<p>The cause, it turns out, was that Discord will wait for a <a href="https://discord.com/developers/docs/interactions/slash-commands#responding-to-an-interaction" target="_blank">maximum of 3 seconds</a> for a response, and if it doesn’t receive one in a timely manner, it will give up and display an error. This can sometimes be a challenge for a webhook that makes API requests to external services, if those calls may incur significant latency. It’s also a particular issue for serverless hosting. An environment like Cloud Functions might shut down your app if it’s not in use, in order to conserve resources. Then, the next time it receives a request, it might take some time to boot back up. This is called a “cold start” in serverless lingo, and it can be a challenge for app developers to keep it low. In addition to calling the Bible API, our app makes a call out to the Secret Manager during cold start, and sometimes the latency of that additional call is enough to push the entire cold start over the 3 second threshold.</p>
<p>Additionally, the syntax that the Bible API requires to specify the scripture reference, wasn’t very nice. Culturally, we’re used to a syntax like “John 1:1-5” rather than “JHN.1.1-JHN.1.5”. So I had to write a parser for “human” references, and convert them to the format needed by the Bible API.</p>
<p>Finally, displaying longer passages would also fail. There was no way, for example, to display the entire first chapter of John. Even though the Bible API would return the passage content, it turns out that Discord imposes a <a href="https://discord.com/developers/docs/resources/webhook#execute-webhook" target="_blank">2000-character limit</a> on message length, and it refused to post a message that is any longer.</p>
<p>So I had a working command, but I wasn’t really satisfied with the results. Parsing reference syntax is just programming, and I’ll leave improving it as an exercise for the reader. But the latency and the longer passages were a real usability hindrance. The good news is, there are solutions, and we’ll explore them in <a href="/blog/2021/05/04/discord-command-in-ruby-on-google-cloud-functions-part-4">part 4</a>.</p>
<h2 id="notes">Notes</h2>
<p>I work at Google in my day job, so all code in this article is:</p>
<figure class="highlight"><pre><code class="language-text" data-lang="text">Copyright 2021 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.</code></pre></figure>
2021-05-03T00:00:00+00:00https://daniel-azuma.com/blog/2021/05/02/discord-command-in-ruby-on-google-cloud-functions-part-2Building a Discord Command in Ruby on Google Cloud Functions: Part 22021-05-02T00:00:00+00:00Daniel Azumadazuma@gmail.comhttp://daniel-azuma.com/<p>This is the second of a four-part series on writing a <a href="https://discord.com/developers/docs/interactions/slash-commands" target="_blank">Discord “slash” command</a> in <a href="https://www.ruby-lang.org" target="_blank">Ruby</a> using <a href="https://cloud.google.com/functions" target="_blank">Google Cloud Functions</a>. In the previous part, we set up a Discord application and deployed its webhook to Google Cloud Functions. In this part, we investigate how to add a command to a Discord channel. It will illustrate how to authenticate and interact with the Discord API, how to use it to add a command to a Discord server, and demonstrate some nice tools for invoking these tasks from the command line.</p>
<p>Previous articles in this series:</p>
<ul>
<li><a href="/blog/2021/04/30/discord-command-in-ruby-on-google-cloud-functions-intro">Introduction</a></li>
<li><a href="/blog/2021/05/01/discord-command-in-ruby-on-google-cloud-functions-part-1">Part 1</a></li>
</ul>
<h2 id="installing-on-a-discord-server">Installing on a Discord server</h2>
<p>In <a href="/blog/2021/05/01/discord-command-in-ruby-on-google-cloud-functions-part-1">part 1</a>, we set up a Discord application. But to make commands available in a Discord server, we’ll need to add our application to the server.</p>
<p>First, we’ll create a <em>Bot user</em> for the application. According to the <a href="https://discord.com/developers/docs/interactions/slash-commands#slash-commands-interactions-and-bot-users" target="_blank">Discord documentation</a>, this is not always necessary for slash commands, but in our case we’ll need it later to make additional API calls. Back in the application page in the Discord Developer console, under the Bot tab, click “Add Bot.”</p>
<p><img src="/img/discord-bot/add-bot.png" alt="Adding a new bot" /></p>
<p>Next, we need to add the bot to a Discord server (also known as a “guild” in the Discord API). More precisely, we’re granting it permissions in the guild to behave as a bot user and to respond to commands.</p>
<p><a href="https://discord.com/developers/docs/topics/oauth2#bots" target="_blank">Discord’s documentation</a> shows how you can authorize a bot via an OAuth2 flow. If you have “manage server” permissions on the guild, you can go to a particular page and grant guild permissions to the bot. The URL for that page looks something like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://discord.com/api/oauth2/authorize?client_id=MY_APPLICATION_ID&scope=bot%20applications.commands
</code></pre></div></div>
<p>Replace <code class="language-plaintext highlighter-rouge">MY_APPLICATION_ID</code> with the application ID from the Discord application’s page in the Discord developer console.</p>
<p><img src="/img/discord-bot/authorize-bot.png" alt="Authorizing a bot with a server" /></p>
<p>On the authorization page, choose the server from the dropdown, and authorize the application. Once this is done, the bot will show up as a user in the server. Behind the scenes, it will have the <code class="language-plaintext highlighter-rouge">bot</code> and <code class="language-plaintext highlighter-rouge">applications.commands</code> permissions that it will need to implement commands.</p>
<h2 id="creating-a-command">Creating a command</h2>
<p>Next we’ll use the bot to create a command on the Discord server. This requires making calls to the Discord API. In this section, I’ll demonstrate how to call the API, provide credentials with those calls, and perform them from the command line.</p>
<h3 id="toys-scripts">Toys Scripts</h3>
<p>I kind of wish Discord had a UI in its console for registering and managing commands, or at least an official CLI that will do so. As it is, creating a command is pretty involved and you pretty much have to write code to make an API call. To make it a bit easier to invoke this code, we’ll use the <a href="https://rubygems.org/gems/toys" target="_blank">Toys</a> gem to write quick command line scripts. Toys works similar to the familiar tool Rake, but is designed specifically for writing command line tools rather than make-style dependencies.</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>gem <span class="nb">install </span>toys</code></pre></figure>
<p>Writing a hello-world script is simple. Create a file called <code class="language-plaintext highlighter-rouge">.toys.rb</code> (the leading period is important.)</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># .toys.rb</span>
<span class="n">tool</span> <span class="s2">"hello"</span> <span class="k">do</span>
<span class="k">def</span> <span class="nf">run</span>
<span class="nb">puts</span> <span class="s2">"Hello, world!"</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>The script can be run using:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>toys hello
Hello, world!
<span class="err">$</span></code></pre></figure>
<p>Below, we’ll use scripts like this to run code that talks to the API.</p>
<h3 id="writing-an-api-client">Writing an API client</h3>
<p>There are a few gems out there for Discord—I briefly looked into <a href="https://github.com/shardlab/discordrb" target="_blank">discordrb</a>—but the needs of this bot are simple, and it’s instructive to roll your own client. Start by adding the HTTP client library <a href="https://lostisland.github.io/faraday/" target="_blank">Faraday</a> to your Gemfile:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># Gemfile</span>
<span class="n">source</span> <span class="s2">"https://rubygems.org"</span>
<span class="n">gem</span> <span class="s2">"ed25519"</span><span class="p">,</span> <span class="s2">"~> 1.2"</span>
<span class="n">gem</span> <span class="s2">"faraday"</span><span class="p">,</span> <span class="s2">"~> 1.4"</span>
<span class="n">gem</span> <span class="s2">"functions_framework"</span><span class="p">,</span> <span class="s2">"~> 0.9"</span></code></pre></figure>
<p>Then start a basic API client class. First, a helper method that makes different kinds of API calls and handles results.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># discord_api.rb</span>
<span class="nb">require</span> <span class="s2">"faraday"</span>
<span class="nb">require</span> <span class="s2">"json"</span>
<span class="k">class</span> <span class="nc">DiscordApi</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">call_api</span><span class="p">(</span><span class="n">path</span><span class="p">,</span>
<span class="ss">method: :get</span><span class="p">,</span>
<span class="ss">body: </span><span class="kp">nil</span><span class="p">,</span>
<span class="ss">params: </span><span class="kp">nil</span><span class="p">,</span>
<span class="ss">headers: </span><span class="kp">nil</span><span class="p">)</span>
<span class="n">faraday</span> <span class="o">=</span> <span class="no">Faraday</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">url: </span><span class="s2">"https://discord.com"</span><span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">faraday</span><span class="p">.</span><span class="nf">run_request</span><span class="p">(</span><span class="nb">method</span><span class="p">,</span> <span class="s2">"/api/v9</span><span class="si">#{</span><span class="n">path</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="n">body</span><span class="p">,</span> <span class="n">headers</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">req</span><span class="o">|</span>
<span class="n">req</span><span class="p">.</span><span class="nf">params</span> <span class="o">=</span> <span class="n">params</span> <span class="k">if</span> <span class="n">params</span>
<span class="k">end</span>
<span class="k">unless</span> <span class="p">(</span><span class="mi">200</span><span class="o">...</span><span class="mi">300</span><span class="p">).</span><span class="nf">include?</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="nf">status</span><span class="p">)</span>
<span class="k">raise</span> <span class="s2">"Discord API failure: </span><span class="si">#{</span><span class="n">response</span><span class="p">.</span><span class="nf">status</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="n">response</span><span class="p">.</span><span class="nf">body</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
<span class="k">return</span> <span class="kp">nil</span> <span class="k">if</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span><span class="p">.</span><span class="nf">nil?</span> <span class="o">||</span> <span class="n">response</span><span class="p">.</span><span class="nf">body</span><span class="p">.</span><span class="nf">empty?</span>
<span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="nf">body</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Next, let’s write a method that <a href="https://discord.com/developers/docs/interactions/slash-commands#get-guild-application-commands" target="_blank">lists the commands</a> defined in a server by a Discord application.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># discord_api.rb</span>
<span class="nb">require</span> <span class="s2">"faraday"</span>
<span class="nb">require</span> <span class="s2">"json"</span>
<span class="k">class</span> <span class="nc">DiscordApi</span>
<span class="no">DISCORD_APPLICATION_ID</span> <span class="o">=</span> <span class="s2">"838132693479850004"</span>
<span class="no">DISCORD_GUILD_ID</span> <span class="o">=</span> <span class="s2">"828125771288805436"</span>
<span class="k">def</span> <span class="nf">initialize</span>
<span class="vi">@client_id</span> <span class="o">=</span> <span class="no">DISCORD_APPLICATION_ID</span>
<span class="vi">@guild_id</span> <span class="o">=</span> <span class="no">DISCORD_GUILD_ID</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">list_commands</span>
<span class="n">call_api</span><span class="p">(</span><span class="s2">"/applications/</span><span class="si">#{</span><span class="vi">@client_id</span><span class="si">}</span><span class="s2">/guilds/</span><span class="si">#{</span><span class="vi">@guild_id</span><span class="si">}</span><span class="s2">/commands"</span><span class="p">)</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="c1"># def call_api...</span>
<span class="k">end</span></code></pre></figure>
<p>The <code class="language-plaintext highlighter-rouge">DISCORD_APPLICATION_ID</code> is the Application ID of the Discord app (from the application’s general information page in the Discord developer console). The <code class="language-plaintext highlighter-rouge">DISCORD_GUILD_ID</code> is the ID of the Discord server, which is part of the server URL. Replace these with the values for your app. These are not secret values, and are safe to have in code, although normally you might want to read them from environment variables or a config file.</p>
<p>Now it should be easy to write a quick Toys script to call this method.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># .toys.rb</span>
<span class="n">tool</span> <span class="s2">"list-commands"</span> <span class="k">do</span>
<span class="k">def</span> <span class="nf">run</span>
<span class="nb">require_relative</span> <span class="s2">"discord_api"</span>
<span class="n">result</span> <span class="o">=</span> <span class="no">DiscordApi</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">list_commands</span>
<span class="nb">puts</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">pretty_generate</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>And try running it:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>toys list-commands
RuntimeError: Discord API failure: 401 <span class="s2">"{</span><span class="se">\"</span><span class="s2">message</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">401: Unauthorized</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">code</span><span class="se">\"</span><span class="s2">: 0}"</span>
<span class="err">$</span></code></pre></figure>
<p>So that didn’t quite work. We got a 401 Unauthorized result from the API call. And of course that makes sense: we need to provide credentials, otherwise anyone can access our commands.</p>
<h3 id="authorizing-discord-api-calls">Authorizing Discord API calls</h3>
<p>Discord uses a variety of <a href="https://discord.com/developers/docs/topics/oauth2" target="_blank">OAuth2 flows</a> for authorization, and these can of course be complicated, especially for applications that need to act on behalf of other users. For our case, however, a simple flow will work: <a href="https://discord.com/developers/docs/topics/oauth2#bots" target="_blank">authorizing as the bot user</a>.</p>
<p>Each bot is assigned a <em>bot token</em> that it can use to authenticate to the Discord API. The bot token is a <em>secret value</em>; it’s essentialy the bot’s password. If you own a bot, you can find it on the bot’s page in the Discord developer console. Find your Discord application, navigate to the “Bot” tab, and click the “Copy” button for the Token, to copy the token to your clipboard. It will be a series of around 60 characters.</p>
<p>Because a token is sensitive information, it should not live in your code or source control. For now, we’ll alter our command line tool to read the token from a command line argument. In later articles, we’ll discuss strategies for accessing such secrets in production using Google Cloud Secret Manager.</p>
<p>First, modify the DiscordApi constructor to take the bot token as an argument.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># discord_api.rb</span>
<span class="c1"># ...</span>
<span class="k">class</span> <span class="nc">DiscordApi</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">bot_token</span><span class="p">:)</span>
<span class="vi">@client_id</span> <span class="o">=</span> <span class="no">DISCORD_APPLICATION_ID</span>
<span class="vi">@guild_id</span> <span class="o">=</span> <span class="no">DISCORD_GUILD_ID</span>
<span class="vi">@bot_token</span> <span class="o">=</span> <span class="n">bot_token</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span></code></pre></figure>
<p>The token needs to be set in an authorization header in API requests. So modify the <code class="language-plaintext highlighter-rouge">call_api</code> helper method to set this header in the Faraday object’s constructor:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># discord_api.rb</span>
<span class="c1"># ...</span>
<span class="k">class</span> <span class="nc">DiscordApi</span>
<span class="c1"># ...</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">call_api</span><span class="p">(</span><span class="n">path</span><span class="p">,</span>
<span class="ss">method: :get</span><span class="p">,</span>
<span class="ss">body: </span><span class="kp">nil</span><span class="p">,</span>
<span class="ss">params: </span><span class="kp">nil</span><span class="p">,</span>
<span class="ss">headers: </span><span class="kp">nil</span><span class="p">)</span>
<span class="n">faraday</span> <span class="o">=</span> <span class="no">Faraday</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">url: </span><span class="s2">"https://discord.com"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">conn</span><span class="o">|</span>
<span class="c1"># Set the authorization header to include a token of type "Bot"</span>
<span class="n">conn</span><span class="p">.</span><span class="nf">authorization</span><span class="p">(</span><span class="ss">:Bot</span><span class="p">,</span> <span class="vi">@bot_token</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># make the request ...</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Finally, add a command line flag to set the token in the Toys script, and pass it into the DiscordApi constructor:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># .toys.rb</span>
<span class="n">tool</span> <span class="s2">"list-commands"</span> <span class="k">do</span>
<span class="n">flag</span> <span class="ss">:token</span><span class="p">,</span> <span class="s2">"--token TOKEN"</span>
<span class="k">def</span> <span class="nf">run</span>
<span class="nb">require_relative</span> <span class="s2">"discord_api"</span>
<span class="n">client</span> <span class="o">=</span> <span class="no">DiscordApi</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">bot_token: </span><span class="n">token</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="nf">list_commands</span>
<span class="nb">puts</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">pretty_generate</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Now we can run the command line tool again, substituting in the actual token:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>toys list-commands <span class="nt">--token</span><span class="o">=</span><span class="nv">$MY_BOT_TOKEN</span>
<span class="o">[</span>
<span class="o">]</span>
<span class="err">$</span></code></pre></figure>
<p>If all went well, this should now display an empty array, indicating that the application has not yet installed any commands in this server.</p>
<h3 id="creating-the-command">Creating the Command</h3>
<p>Now we finally have all the parts in place to make an API call to create a command in a Discord server. First, we’ll add a method to the client class that calls the <a href="https://discord.com/developers/docs/interactions/slash-commands#create-guild-application-command" target="_blank">Create Guild Application Command API</a>. This API adds a command to a specific guild. (You can also create “global commands” that apply to multiple guilds, but they’re a bit more complicated to manage, so we’ll use guild-specific commands for now.)</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># discord_api.rb</span>
<span class="c1"># ,,,</span>
<span class="k">class</span> <span class="nc">DiscordApi</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">create_command</span><span class="p">(</span><span class="n">command_definition</span><span class="p">)</span>
<span class="n">definition_json</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">command_definition</span><span class="p">)</span>
<span class="n">headers</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"Content-Type"</span> <span class="o">=></span> <span class="s2">"application/json"</span><span class="p">}</span>
<span class="n">call_api</span><span class="p">(</span><span class="s2">"/applications/</span><span class="si">#{</span><span class="vi">@client_id</span><span class="si">}</span><span class="s2">/guilds/</span><span class="si">#{</span><span class="vi">@guild_id</span><span class="si">}</span><span class="s2">/commands"</span><span class="p">,</span>
<span class="ss">method: :post</span><span class="p">,</span>
<span class="ss">body: </span><span class="n">definition_json</span><span class="p">,</span>
<span class="ss">headers: </span><span class="n">headers</span><span class="p">)</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="c1"># def call_api...</span>
<span class="k">end</span></code></pre></figure>
<p>That method takes a hash object that describes the command to create. For this project, this is a Scripture lookup command called <code class="language-plaintext highlighter-rouge">/bible</code>, which takes one required option, the Scripture reference to look up. Here’s the definition of this command:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="p">{</span>
<span class="ss">name: </span><span class="s2">"bible"</span><span class="p">,</span>
<span class="ss">description: </span><span class="s2">"Simple Scripture lookup"</span><span class="p">,</span>
<span class="ss">options: </span><span class="p">[</span>
<span class="p">{</span>
<span class="ss">type: </span><span class="mi">3</span><span class="p">,</span>
<span class="ss">name: </span><span class="s2">"reference"</span><span class="p">,</span>
<span class="ss">description: </span><span class="s2">"Scripture reference (e.g. `JHN.1.1-JHN.1.5`)"</span><span class="p">,</span>
<span class="ss">required: </span><span class="kp">true</span>
<span class="p">}</span>
<span class="p">]</span>
<span class="p">}</span></code></pre></figure>
<p>The full format is specified <a href="https://discord.com/developers/docs/interactions/slash-commands#applicationcommand" target="_blank">in the Discord documentation</a>.</p>
<p>So I wrote a quick Toys script that takes the above description and feeds it into the API:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># .toys.rb</span>
<span class="n">tool</span> <span class="s2">"create-command"</span> <span class="k">do</span>
<span class="n">flag</span> <span class="ss">:token</span><span class="p">,</span> <span class="s2">"--token TOKEN"</span>
<span class="k">def</span> <span class="nf">run</span>
<span class="nb">require_relative</span> <span class="s2">"discord_api"</span>
<span class="n">client</span> <span class="o">=</span> <span class="no">DiscordApi</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">bot_token: </span><span class="n">token</span><span class="p">)</span>
<span class="n">definition</span> <span class="o">=</span> <span class="p">{</span>
<span class="ss">name: </span><span class="s2">"bible"</span><span class="p">,</span>
<span class="ss">description: </span><span class="s2">"Simple Scripture lookup"</span><span class="p">,</span>
<span class="ss">options: </span><span class="p">[</span>
<span class="p">{</span>
<span class="ss">type: </span><span class="mi">3</span><span class="p">,</span>
<span class="ss">name: </span><span class="s2">"reference"</span><span class="p">,</span>
<span class="ss">description: </span><span class="s2">"Scripture reference (e.g. `JHN.1.1-JHN.1.5`)"</span><span class="p">,</span>
<span class="ss">required: </span><span class="kp">true</span>
<span class="p">}</span>
<span class="p">]</span>
<span class="p">}</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="nf">create_command</span><span class="p">(</span><span class="n">definition</span><span class="p">)</span>
<span class="nb">puts</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">pretty_generate</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># The "list" command from before is still here ...</span>
<span class="n">tool</span> <span class="s2">"list-commands"</span> <span class="k">do</span>
<span class="n">flag</span> <span class="ss">:token</span><span class="p">,</span> <span class="s2">"--token TOKEN"</span>
<span class="k">def</span> <span class="nf">run</span>
<span class="nb">require_relative</span> <span class="s2">"discord_api"</span>
<span class="n">client</span> <span class="o">=</span> <span class="no">DiscordApi</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">bot_token: </span><span class="n">token</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="nf">list_commands</span>
<span class="nb">puts</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">pretty_generate</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Now I was able to call the new script, again substituting in the bot token. The script creates the command and displays the result:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>toys create-command <span class="nt">--token</span><span class="o">=</span><span class="nv">$MY_BOT_TOKEN</span>
<span class="o">{</span>
<span class="s2">"id"</span>: <span class="s2">"838195819437228113"</span>,
<span class="s2">"application_id"</span>: <span class="s2">"838132693479850004"</span>,
<span class="s2">"name"</span>: <span class="s2">"bible"</span>,
<span class="s2">"description"</span>: <span class="s2">"Simple Scripture lookup"</span>,
<span class="s2">"version"</span>: <span class="s2">"838195819437228114"</span>,
<span class="s2">"default_permission"</span>: <span class="nb">true</span>,
<span class="s2">"guild_id"</span>: <span class="s2">"828125771288805436"</span>,
<span class="s2">"options"</span>: <span class="o">[</span>
<span class="o">{</span>
<span class="s2">"type"</span>: 3,
<span class="s2">"name"</span>: <span class="s2">"reference"</span>,
<span class="s2">"description"</span>: <span class="s2">"Scripture reference (e.g. </span><span class="sb">`</span>JHN.1.1-JHN.1.5<span class="sb">`</span><span class="s2">)"</span>,
<span class="s2">"required"</span>: <span class="nb">true</span>
<span class="o">}</span>
<span class="o">]</span>
<span class="o">}</span>
<span class="err">$</span></code></pre></figure>
<p>And now calling list again, shows the new command:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>toys list-commands <span class="nt">--token</span><span class="o">=</span><span class="nv">$MY_BOT_TOKEN</span>
<span class="o">[</span>
<span class="o">{</span>
<span class="s2">"id"</span>: <span class="s2">"838195819437228113"</span>,
<span class="s2">"application_id"</span>: <span class="s2">"838132693479850004"</span>,
<span class="s2">"name"</span>: <span class="s2">"bible"</span>,
<span class="s2">"description"</span>: <span class="s2">"Simple Scripture lookup"</span>,
<span class="s2">"version"</span>: <span class="s2">"838195819437228114"</span>,
<span class="s2">"default_permission"</span>: <span class="nb">true</span>,
<span class="s2">"guild_id"</span>: <span class="s2">"828125771288805436"</span>,
<span class="s2">"options"</span>: <span class="o">[</span>
<span class="o">{</span>
<span class="s2">"type"</span>: 3,
<span class="s2">"name"</span>: <span class="s2">"reference"</span>,
<span class="s2">"description"</span>: <span class="s2">"Scripture reference (e.g. </span><span class="sb">`</span>JHN.1.1-JHN.1.5<span class="sb">`</span><span class="s2">)"</span>,
<span class="s2">"required"</span>: <span class="nb">true</span>
<span class="o">}</span>
<span class="o">]</span>
<span class="o">}</span>
<span class="o">]</span>
<span class="err">$</span></code></pre></figure>
<h2 id="now-what">Now what?</h2>
<p>Great, we’e created a command in our Discord server! The Discord UI updates immediately, so now it should be possible to go to a chat room in the Discord server, and type <code class="language-plaintext highlighter-rouge">/bible</code>, and see the autocomplete kick in.</p>
<p>But of course, if you’re following along, and try to invoke an entire command:</p>
<p><img src="/img/discord-bot/test-input.png" alt="Invoking a command" /></p>
<p>…you’ll get this:</p>
<p><img src="/img/discord-bot/interaction-failed.png" alt="Interaction failed" /></p>
<p>And that’s because we haven’t actually implemented the command in our webhook yet. That will be the topic of <a href="/blog/2021/05/03/discord-command-in-ruby-on-google-cloud-functions-part-3">part 3</a>.</p>
<h2 id="notes">Notes</h2>
<p>I work at Google in my day job, so all code in this article is:</p>
<figure class="highlight"><pre><code class="language-text" data-lang="text">Copyright 2021 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.</code></pre></figure>
2021-05-02T00:00:00+00:00https://daniel-azuma.com/blog/2021/05/01/discord-command-in-ruby-on-google-cloud-functions-part-1Building a Discord Command in Ruby on Google Cloud Functions: Part 12021-05-01T00:00:00+00:00Daniel Azumadazuma@gmail.comhttp://daniel-azuma.com/<p>This is the first of a four-part series on writing a <a href="https://discord.com/developers/docs/interactions/slash-commands" target="_blank">Discord “slash” command</a> in <a href="https://www.ruby-lang.org" target="_blank">Ruby</a> using <a href="https://cloud.google.com/functions" target="_blank">Google Cloud Functions</a>. In this part, we cover setting up a Discord bot, deploying a webhook to Cloud Functions, and validating webhook requests from Discord using the <a href="https://github.com/RubyCrypto/ed25519" target="_blank">Ruby ed25519</a> library. At the end of this part, we’ll have a working webhook that Discord recognizes, but that doesn’t yet actually implement a command.</p>
<p>Previous articles in this series:</p>
<ul>
<li><a href="/blog/2021/04/30/discord-command-in-ruby-on-google-cloud-functions-intro">Introduction</a></li>
</ul>
<h2 id="creating-a-discord-application">Creating a Discord application</h2>
<p>Discord is a big system. Often used for gaming and streaming, but also increasingly for online community interaction, it includes a wide variety of features involving chat, voice, video, and content. Bots are an integral part of the ecosystem, and all bots start in the same place: with a Discord application.</p>
<p>Discord’s developer portal can be accessed at <a href="https://discord.com/developers" target="_blank">https://discord.com/developers</a>. Once you’ve registered and logged in, it shows you a list of your applications. I created a new application here called <code class="language-plaintext highlighter-rouge">scripture-bot</code>.</p>
<p><img src="/img/discord-bot/general-info.png" alt="Screenshot of a new Discord app" /></p>
<p>Each application comes with a number of properties. These appear in the “general information” tab of the application, and include the following:</p>
<ul>
<li>The <em>application ID</em> is a unique number identifying your application. It’s kind of like your application’s “username”, and will be important later when we call the Discord API to register with servers and create commands.</li>
<li>The <em>public key</em> will be used by your bot to authenticate requests—that is, to verify that HTTP requests you receive actually came from Discord and not from someone else trying to spoof Discord.</li>
<li>The <em>interactions endpoint URL</em> is the URL of the webhook that will be called when someone invokes your command. It starts off empty because <em>you</em> need to fill it in. And that’s what we’ll be doing next.</li>
</ul>
<p>There’s also a tab labeled “Bot” that includes information about the “bot user”. It also starts off empty, but we will create a bot user later when we install our command into a Discord server.</p>
<h2 id="writing-a-webhook">Writing a webhook</h2>
<p>A Discord application can respond to commands in two ways: via the Gateway or by implementing a webhook. The Gateway communicates over a websocket, which is flexible and low-latency, but complicated to implement, and requires running a permanent process. For this project, we’ll opt for the simpler approach of providing a webhook that Discord will call whenever a command is invoked. The webhook option has limitations, but a key advantage: it can be deployed as a serverless web app, and thus likely to be inexpensive to run if it’s not heavily used.</p>
<p>Writing and deploying webhooks is quite easy with functions-as-a-service, or “FaaS”, a serverless architecture that models your app as a simple function that handles events. Many major cloud providers offer a FaaS environment, for example <a href="https://aws.amazon.com/lambda/" target="_blank">Lambda</a> from AWS, or <a href="https://cloud.google.com/functions/" target="_blank">Cloud Functions</a> from Google. For this article, we’ll use Cloud Functions.</p>
<h3 id="hello-functions">Hello Functions</h3>
<p>Deploying a hello-world app to Cloud Functions is quite simple even if you haven’t done it before. Create a project in the <a href="https://console.cloud.google.com" target="_blank">Google Cloud Console</a>, and install the <a href="https://cloud.google.com/sdk" target="_blank">Google Cloud SDK</a>, Google Cloud’s command-line tool. Then you can write a quick function called “<code class="language-plaintext highlighter-rouge">discord_webook</code>” using the <a href="https://github.com/GoogleCloudPlatform/functions-framework-ruby" target="_blank">Functions Framework</a>:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># Gemfile</span>
<span class="n">source</span> <span class="s2">"https://rubygems.org"</span>
<span class="n">gem</span> <span class="s2">"functions_framework"</span><span class="p">,</span> <span class="s2">"~> 0.9"</span></code></pre></figure>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># app.rb</span>
<span class="nb">require</span> <span class="s2">"functions_framework"</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">http</span> <span class="s2">"discord_webhook"</span> <span class="k">do</span> <span class="o">|</span><span class="n">request</span><span class="o">|</span>
<span class="s2">"Hello, world!</span><span class="se">\n</span><span class="s2">"</span>
<span class="k">end</span></code></pre></figure>
<p>You can then deploy the function from the command line. Cloud Functions requires that an up-to-date Gemfile.lock file is present in order to deploy, so that means installing the bundle, then running the gcloud command to deploy a function:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>bundle <span class="nb">install</span>
<span class="nv">$ </span>gcloud functions deploy discord_webhook <span class="se">\</span>
<span class="nt">--project</span><span class="o">=</span><span class="nv">$MY_PROJECT</span> <span class="nt">--region</span><span class="o">=</span>us-central1 <span class="se">\</span>
<span class="nt">--trigger-http</span> <span class="nt">--entry-point</span><span class="o">=</span>discord_webhook <span class="se">\</span>
<span class="nt">--runtime</span><span class="o">=</span>ruby27 <span class="nt">--allow-unauthenticated</span></code></pre></figure>
<p>Substitute your own project ID (or use <code class="language-plaintext highlighter-rouge">gcloud config set project</code> to set it globally.) The command above deploys to the <code class="language-plaintext highlighter-rouge">us-central1</code> availability region, and specifies a function that responds to HTTP requests using a Ruby 2.7 runtime. Note that it also disables Google’s default authentication. Instead, we will implement Discord’s authentication mechansim below.</p>
<p>If successful, the output of the gcloud deployment command will display the URL for the function. At this point you can use <code class="language-plaintext highlighter-rouge">curl</code> to send http requests to the function and see the response.</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>curl https://us-central1-<span class="nv">$MY_PROJECT</span>.cloudfunctions.net/discord_webhook
Hello, world!
<span class="err">$</span></code></pre></figure>
<p>Even though it’s very simple to get started, Google Cloud Functions has a long and growing list of features to make it easy to write and test your functions. You can run your function locally with a single command, and there’s a useful set of tools for running functions in isolation so you can write unit tests in Minitest or Rspec. I won’t cover the details here, but a lot of imformation is available in the <a href="https://googlecloudplatform.github.io/functions-framework-ruby/latest" target="_blank">Functions Framework documentation</a>.</p>
<h3 id="responding-to-pings">Responding to pings</h3>
<p>Now that we have a working function, it’s time to configure it as the webhook endpoint for our Discord application. This is set in the “General Information” tab on your application’s page in the Discord console. However, if you just attempt to set the field now, Discord gives an error:</p>
<p><img src="/img/discord-bot/endpoint-verify-failed.png" alt="Endpoint verification failure message" /></p>
<p>This is because <em>verification</em> failed. When you set up a webhook for an application, Discord will verify it is running correctly by sending it a <a href="https://discord.com/developers/docs/interactions/slash-commands#receiving-an-interaction" target="_blank">ping message</a> and expecting the proper reply. So we first need to update our function to handle pings.</p>
<p>Since we’re about to implement some real logic, let’s break it out into a separate class. The Functions Framework lets you define a function as a block, and you can put all the logic there. But for maintainability sake, it’s often a good idea to write separate Ruby classes encapsulating your application logic. So we’ll start by creating a Responder class to respond to HTTP requests sent by Discord, and refactoring our function to call it:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># responder.rb</span>
<span class="k">class</span> <span class="nc">Responder</span>
<span class="k">def</span> <span class="nf">respond</span><span class="p">(</span><span class="n">rack_request</span><span class="p">)</span>
<span class="s2">"Hello, world!</span><span class="se">\n</span><span class="s2">"</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># app.rb</span>
<span class="nb">require</span> <span class="s2">"functions_framework"</span>
<span class="nb">require_relative</span> <span class="s2">"responder"</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">on_startup</span> <span class="k">do</span>
<span class="n">set_global</span><span class="p">(</span><span class="ss">:responder</span><span class="p">,</span> <span class="no">Responder</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span>
<span class="k">end</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">http</span> <span class="s2">"discord_webhook"</span> <span class="k">do</span> <span class="o">|</span><span class="n">request</span><span class="o">|</span>
<span class="n">global</span><span class="p">(</span><span class="ss">:responder</span><span class="p">).</span><span class="nf">respond</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="k">end</span></code></pre></figure>
<p><strong>Note:</strong> The above code uses a <em>startup block</em> to instantiate our Responder and set it in a “global” that can be accessed by our function. The startup block and global storage are features of the <a href="https://github.com/GoogleCloudPlatform/functions-framework-ruby" target="_blank">Ruby Functions Framework</a>. You could also use a Ruby global variable, or even a local variable scoped to the file, but the globals mechanism provided by the Functions Framework makes it easier to isolate runs when you write unit tests.</p>
<p>At this point, you can redeploy the function and verify that it still works. It should still just respond with the “Hello, world!” message. But we’ll change that now.</p>
<p>Discord’s messages, known in the Discord API as <a href="https://discord.com/developers/docs/interactions/slash-commands#interaction" target="_blank">“interactions”</a>, are sent as JSON and have a “type” field indicating the interaction type. Ping interactions have a type of 1, and when Discord sends you a ping, it expects you to respond with a similar JSON object, also with the “type” field set to 1. Let’s implement this in our Responder.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># responder.rb</span>
<span class="nb">require</span> <span class="s2">"json"</span>
<span class="k">class</span> <span class="nc">Responder</span>
<span class="k">def</span> <span class="nf">respond</span><span class="p">(</span><span class="n">rack_request</span><span class="p">)</span>
<span class="n">raw_body</span> <span class="o">=</span> <span class="n">rack_request</span><span class="p">.</span><span class="nf">body</span><span class="p">.</span><span class="nf">read</span>
<span class="n">interaction</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">raw_body</span><span class="p">)</span>
<span class="k">if</span> <span class="n">interaction</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</span>
<span class="p">{</span><span class="ss">type: </span><span class="mi">1</span><span class="p">}</span>
<span class="k">else</span>
<span class="p">[</span><span class="mi">400</span><span class="p">,</span>
<span class="p">{</span><span class="s2">"Content-Type"</span> <span class="o">=></span> <span class="s2">"text/plain"</span><span class="p">},</span>
<span class="p">[</span><span class="s2">"Unrecognized interaction type"</span><span class="p">]]</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Notice the return types: we return a hash if we receive a ping, or a standard <a href="https://github.com/rack/rack/blob/master/SPEC.rdoc" target="_blank">Rack response array</a> to report a 400 Bad Request if we receive anything else. The Functions Framework recognizes a variety of return types: a string will be encoded as plain text, a hash will be encoded as JSON, and Rack response types are also recognized.</p>
<p>Redeploy to Cloud Functions, and you can test it there by posting a JSON request using curl and seeing the expected response:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>curl https://us-central1-<span class="nv">$MY_PROJECT</span>.cloudfunctions.net/discord_webhook <span class="se">\</span>
<span class="nt">--data</span> <span class="s1">'{"type":1}'</span>
<span class="o">{</span><span class="s2">"type"</span>:1<span class="o">}</span>
<span class="err">$</span></code></pre></figure>
<p>Now you can go back to the Discord developer site, and fill in the <em>interactions endpoint url</em> field with the URL of your function. And…</p>
<p><img src="/img/discord-bot/endpoint-verify-failed.png" alt="Endpoint verification failure message" /></p>
<p>We’re still getting a verification failure. It turns out, even though we’re returning the correct response to a ping, Discord <em>also</em> requires that we <em>verify request signatures</em> correctly before it will let us set the endpoint. So we’ll turn our attention there next.</p>
<h3 id="validating-discord-requests">Validating Discord requests</h3>
<p>When you write a web service, it’s always good practice to validate that any requests you receive are actually from whom you think they’re from. Before it lets you set your endpoint URL, Discord will enforce this practice by checking that you’ve implemented validation correctly. It does this by sending send test requests to your endpoint with both correct and incorrect credentials, and making sure you respond appropriately</p>
<p>So let’s implement this verification, following the <a href="https://discord.com/developers/docs/interactions/slash-commands#security-and-authorization" target="_blank">instructions from Discord</a>.</p>
<p>First, we’ll need a library that can validate ED25519 signatures. There are several to choose from, but we’ll use the <a href="https://github.com/RubyCrypto/ed25519" target="_blank">ed25519</a> gem because it doesn’t depend on outside C libraries, making it easier to deploy it to serverless runtimes.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># Gemfile</span>
<span class="n">source</span> <span class="s2">"https://rubygems.org"</span>
<span class="n">gem</span> <span class="s2">"ed25519"</span><span class="p">,</span> <span class="s2">"~> 1.2"</span>
<span class="n">gem</span> <span class="s2">"functions_framework"</span><span class="p">,</span> <span class="s2">"~> 0.9"</span></code></pre></figure>
<p>Run <code class="language-plaintext highlighter-rouge">bundle install</code> to install the gem and ensure that your <code class="language-plaintext highlighter-rouge">Gemfile.lock</code> is updated. If you forget to do this when you update your bundle, Cloud Functions will fail to deploy your app, and will report an error that your lockfile is out of date.</p>
<p>Then it’s time to write the signature verification code. First, create a verification key from the app’s <em>public key</em> (which is available from the General Information tab on Discord.) Set this in the constructor for the Responder class because it’s the same for all requests.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># responder.rb</span>
<span class="nb">require</span> <span class="s2">"json"</span>
<span class="nb">require</span> <span class="s2">"ed25519"</span>
<span class="k">class</span> <span class="nc">Responder</span>
<span class="c1"># Substitute your Discord app's public key here</span>
<span class="no">DISCORD_PUBLIC_KEY</span> <span class="o">=</span> <span class="s2">"1904a4821ccb7f5212ad0ce8cfd32a385dee845d9f7dc5113b35066e3b05db78"</span>
<span class="k">def</span> <span class="nf">initialize</span>
<span class="n">public_key</span> <span class="o">=</span> <span class="no">DISCORD_PUBLIC_KEY</span>
<span class="n">public_key_binary</span> <span class="o">=</span> <span class="p">[</span><span class="n">public_key</span><span class="p">].</span><span class="nf">pack</span><span class="p">(</span><span class="s2">"H*"</span><span class="p">)</span>
<span class="vi">@verification_key</span> <span class="o">=</span> <span class="no">Ed25519</span><span class="o">::</span><span class="no">VerifyKey</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">public_key_binary</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span></code></pre></figure>
<p>In the above example code, substitute your app’s public key for mine.</p>
<p><strong>Note:</strong> We’ve hard-coded the public key for now. This is not great practice, but it’s generally safe because a public key is not secret. In a real application you’ll likely want to load it from an environment variable or configuration file instead.</p>
<p>Once you have a verification key, you can verify a request by checking the contents of the request against the signature sent by Discord, using your key. The signature will match only if it was created using the corresponding private key, which only Discord should have. Additionally, the request content will include a timestamp, and you should check that it is close to the current time, in order to prevent replay attacks. Here’s the final code:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># responder.rb</span>
<span class="nb">require</span> <span class="s2">"json"</span>
<span class="nb">require</span> <span class="s2">"ed25519"</span>
<span class="k">class</span> <span class="nc">Responder</span>
<span class="c1"># Substitute your Discord app's public key here</span>
<span class="no">DISCORD_PUBLIC_KEY</span> <span class="o">=</span> <span class="s2">"1904a4821ccb7f5212ad0ce8cfd32a385dee845d9f7dc5113b35066e3b05db78"</span>
<span class="c1"># Allowed difference in seconds betwen the current time and</span>
<span class="c1"># the timestamp sent by Discord</span>
<span class="no">ALLOWED_CLOCK_SKEW</span> <span class="o">=</span> <span class="mi">10</span>
<span class="k">def</span> <span class="nf">initialize</span>
<span class="n">public_key</span> <span class="o">=</span> <span class="no">DISCORD_PUBLIC_KEY</span>
<span class="n">public_key_binary</span> <span class="o">=</span> <span class="p">[</span><span class="n">public_key</span><span class="p">].</span><span class="nf">pack</span><span class="p">(</span><span class="s2">"H*"</span><span class="p">)</span>
<span class="vi">@verification_key</span> <span class="o">=</span> <span class="no">Ed25519</span><span class="o">::</span><span class="no">VerifyKey</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">public_key_binary</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">respond</span><span class="p">(</span><span class="n">rack_request</span><span class="p">)</span>
<span class="n">raw_body</span> <span class="o">=</span> <span class="n">rack_request</span><span class="p">.</span><span class="nf">body</span><span class="p">.</span><span class="nf">read</span>
<span class="k">unless</span> <span class="n">verify_request</span><span class="p">(</span><span class="n">raw_body</span><span class="p">,</span> <span class="n">rack_request</span><span class="p">.</span><span class="nf">env</span><span class="p">)</span>
<span class="c1"># Discord expects a 401 response if the verification failed</span>
<span class="k">return</span> <span class="p">[</span><span class="mi">401</span><span class="p">,</span>
<span class="p">{</span><span class="s2">"Content-Type"</span> <span class="o">=></span> <span class="s2">"text/plain"</span><span class="p">},</span>
<span class="p">[</span><span class="s2">"invalid request signature"</span><span class="p">]]</span>
<span class="k">end</span>
<span class="n">interaction</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">raw_body</span><span class="p">)</span>
<span class="k">if</span> <span class="n">interaction</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</span>
<span class="p">{</span><span class="ss">type: </span><span class="mi">1</span><span class="p">}</span>
<span class="k">else</span>
<span class="p">[</span><span class="mi">400</span><span class="p">,</span>
<span class="p">{</span><span class="s2">"Content-Type"</span> <span class="o">=></span> <span class="s2">"text/plain"</span><span class="p">},</span>
<span class="p">[</span><span class="s2">"Unrecognized interaction type"</span><span class="p">]]</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="c1"># Verify a request by checking the timestamp and signature</span>
<span class="k">def</span> <span class="nf">verify_request</span><span class="p">(</span><span class="n">raw_body</span><span class="p">,</span> <span class="n">rack_env</span><span class="p">)</span>
<span class="c1"># Get the timestamp and check for replay attacks</span>
<span class="n">timestamp</span> <span class="o">=</span> <span class="n">rack_env</span><span class="p">[</span><span class="s2">"HTTP_X_SIGNATURE_TIMESTAMP"</span><span class="p">].</span><span class="nf">to_s</span>
<span class="n">current_time</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_REALTIME</span><span class="p">)</span>
<span class="n">clock_skew</span> <span class="o">=</span> <span class="p">(</span><span class="n">current_time</span> <span class="o">-</span> <span class="n">timestamp</span><span class="p">.</span><span class="nf">to_i</span><span class="p">).</span><span class="nf">abs</span>
<span class="k">return</span> <span class="kp">false</span> <span class="k">if</span> <span class="n">clock_skew</span> <span class="o">></span> <span class="no">ALLOWED_CLOCK_SKEW</span>
<span class="c1"># Get the signature and verify it against the content and timestamp</span>
<span class="n">signature_hex</span> <span class="o">=</span> <span class="n">rack_env</span><span class="p">[</span><span class="s2">"HTTP_X_SIGNATURE_ED25519"</span><span class="p">].</span><span class="nf">to_s</span>
<span class="n">signature</span> <span class="o">=</span> <span class="p">[</span><span class="n">signature_hex</span><span class="p">].</span><span class="nf">pack</span><span class="p">(</span><span class="s2">"H*"</span><span class="p">)</span>
<span class="k">begin</span>
<span class="vi">@verification_key</span><span class="p">.</span><span class="nf">verify</span><span class="p">(</span><span class="n">signature</span><span class="p">,</span> <span class="n">timestamp</span> <span class="o">+</span> <span class="n">raw_body</span><span class="p">)</span>
<span class="kp">true</span>
<span class="k">rescue</span> <span class="no">Ed25519</span><span class="o">::</span><span class="no">VerifyError</span>
<span class="kp">false</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>A quick redeploy, and now at last we can set our Discord application’s endpoint URL. If you go look at the Cloud Functions logs in the Google Cloud Console, you’ll be able to see the test requests that Discord sends you. Typically it will send two requests when you attempt to set the webhook URL: one with a correct signature and one with an incorrect signature, just to make sure you have pings and verification implemented.</p>
<p><strong>Note:</strong> It can take a few seconds, even after Cloud Functions finishes deploying your function, for the backend to “switch over” to the new deployment. So if you’re following along, and you believe you’ve implemented the verification, but Discord is still reporting a verification error, wait about a minute and then try setting the endpoint field in Discord again.</p>
<h2 id="now-what">Now what?</h2>
<p>So far so good. We have a working Discord application, deployed to Google Cloud Functions, and responding correctly to requests sent by Discord. Next we actually have to create a command. We’ll cover that in <a href="/blog/2021/05/02/discord-command-in-ruby-on-google-cloud-functions-part-2">part 2</a>.</p>
<h2 id="notes">Notes</h2>
<p>I work at Google in my day job, so all code in this article is:</p>
<figure class="highlight"><pre><code class="language-text" data-lang="text">Copyright 2021 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.</code></pre></figure>
2021-05-01T00:00:00+00:00https://daniel-azuma.com/blog/2021/04/30/discord-command-in-ruby-on-google-cloud-functions-introBuilding a Discord Command in Ruby on Google Cloud Functions2021-04-30T00:00:00+00:00Daniel Azumadazuma@gmail.comhttp://daniel-azuma.com/<p>I recently spent a weekend experimenting with <a href="https://discord.com/developers" target="_blank">Discord integration</a>. My church had decided to move our online services from Zoom to Discord, and, being a software geek, I thought it would be fun to try to build some things for the community.</p>
<p>So far I’ve built a simple command that does Scripture lookups. You can type a command in one of our Discord channels:</p>
<p><img src="/img/discord-bot/demo-input.png" alt="Invoking the command: /bible John 1:1-5" /></p>
<p>…and the bot will look up the passage using an API, and display it in the channel:</p>
<p><img src="/img/discord-bot/demo-output.png" alt="The command output, displaying a passage" /></p>
<p><a href="https://discord.com/developers/docs/interactions/slash-commands" target="_blank">“Slash” commands</a> such as this, are a relatively recent adition to the Discord API. They’re easy for users to interact with, and convenient to deploy as a webhook.</p>
<p>Following will be a short series of articles describing how to write a Discord command such as this “scripture-bot” in Ruby, along with how to deploy it to the cloud. Along the way, we’ll cover a lot of the issues you may encounter with a real-world application, including:</p>
<ul>
<li><strong>Deploying to a “serverless” environment.</strong> Specifically, we’ll use <a href="https://cloud.google.com/functions" target="_blank">Cloud Functions</a>, a functions-as-a-service offering from Google.</li>
<li><strong>Verifying request signatures</strong> using Ruby libraries, in this case using the <a href="https://github.com/RubyCrypto/ed25519" target="_blank">ed25519</a> gem.</li>
<li><strong>Good practices for handling secrets</strong>, such as API keys, in production. For this project, we’ll demonstrate the use of Google <a href="https://cloud.google.com/secret-manager" target="_blank">Secret Manager</a>.</li>
<li><strong>Techniques for reducing cold start time</strong>, including lazy initialization.</li>
<li><strong>Using an event-driven architecture</strong> to schedule and run background tasks. In this project we’ll use <a href="https://cloud.google.com/pubsub" target="_blank">Google Pub/Sub</a>.</li>
</ul>
<h2 id="contents">Contents</h2>
<p>Because there’s a lot to cover, I’ll be splitting this article into four parts:</p>
<p>In <a href="/blog/2021/05/01/discord-command-in-ruby-on-google-cloud-functions-part-1">part 1</a>, we’ll cover setting up a Discord bot, deploying a webhook to Google Cloud Functions, and validating webhook requests from Discord using the Ruby ed25519 library. At the end of this part, we’ll have a working webhook that Discord recognizes, but that doesn’t yet actually implement a command.</p>
<p>In <a href="/blog/2021/05/02/discord-command-in-ruby-on-google-cloud-functions-part-2">part 2</a>, we’ll add our bot to a Discord server, and use the Discord API to register a command with the server. At the end of this part, we’ll know how to authenticate with the Discord API, and we’ll have a command set up, but it won’t yet be implemented.</p>
<p>In <a href="/blog/2021/05/03/discord-command-in-ruby-on-google-cloud-functions-part-3">part 3</a>, we’ll implement the command, calling an external API to get the actual Scripture text to display on the Discord channel. We’ll also learn how to use Google’s Secret Manager to access the API key securely in production. At the end of this part, our command will be working, with a few caveats.</p>
<p>In <a href="/blog/2021/05/04/discord-command-in-ruby-on-google-cloud-functions-part-4">part 4</a>, we’ll deal with one of the main issues with our implementation so far, which is that Discord doesn’t let you display more than 2000 characters in a chat message. To display a longer Scripture passage, we’ll need to send follow-up messages using the Discord API, and we’ll do that by scheduling a follow-up task using Google Pub/Sub.</p>
<p>All the code will be included, so at the end, if you follow along, you’ll have your own fully-functional Discord bot. (Note: I do work at Google in my day job, so the sample code is Copyright 2021 Google LLC, and licensed under the <a href="http://www.apache.org/licenses/LICENSE-2.0" target="_blank">Apache 2.0 License</a>.)</p>
<p>Ready? On to <a href="/blog/2021/05/01/discord-command-in-ruby-on-google-cloud-functions-part-1">part 1</a>…</p>
2021-04-30T00:00:00+00:00https://daniel-azuma.com/blog/2021/02/02/should-i-use-instance-eval-or-class-evalShould I use instance_eval or class_eval?2021-02-02T00:00:00+00:00Daniel Azumadazuma@gmail.comhttp://daniel-azuma.com/<p>Ruby objects have methods called <code class="language-plaintext highlighter-rouge">instance_eval</code> and <code class="language-plaintext highlighter-rouge">class_eval</code>. They both execute a block with <code class="language-plaintext highlighter-rouge">self</code> referencing the object.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">C</span><span class="p">;</span> <span class="k">end</span>
<span class="no">C</span><span class="p">.</span><span class="nf">instance_eval</span> <span class="p">{</span> <span class="nb">puts</span> <span class="s2">"instance_eval: self = </span><span class="si">#{</span><span class="nb">self</span><span class="si">}</span><span class="s2">"</span> <span class="p">}</span>
<span class="c1"># prints "instance_eval: self = C"</span>
<span class="no">C</span><span class="p">.</span><span class="nf">class_eval</span> <span class="p">{</span> <span class="nb">puts</span> <span class="s2">"class_eval: self = </span><span class="si">#{</span><span class="nb">self</span><span class="si">}</span><span class="s2">"</span> <span class="p">}</span>
<span class="c1"># prints "class_eval: self = C"</span></code></pre></figure>
<p>For a long time I thought these methods were basically synonymous, that for some reason I was simply supposed to use <code class="language-plaintext highlighter-rouge">class_eval</code> for classes (and <code class="language-plaintext highlighter-rouge">module_eval</code> for modules) and <code class="language-plaintext highlighter-rouge">instance_eval</code> for everything else. Just accept it and don’t ask questions, I told myself.</p>
<p>But there <em>is</em> a difference, one that points to an important but seldom understood aspect of Ruby. In this article, we’ll explore the distinction and what it means for our Ruby code.</p>
<h2 id="the-current-object">The “current object”</h2>
<p>Let’s start with what many Ruby programmers already know.</p>
<p>Like many object-oriented languages, Ruby has the notion of a “current object”. This is the object that receives method calls by default, and it is the object that owns any instance variables you reference. Ruby sets the current object inside every method call, so the method can access the object’s instance variables and easily call other methods of the object.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">Greeter</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="nb">name</span><span class="p">)</span>
<span class="vi">@name</span> <span class="o">=</span> <span class="nb">name</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">greeting</span>
<span class="c1"># Reference "@name" from the current object</span>
<span class="s2">"Hello, </span><span class="si">#{</span><span class="vi">@name</span><span class="si">}</span><span class="s2">!"</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">print_greeting</span>
<span class="c1"># Call the "greeting" method in the current object</span>
<span class="nb">puts</span> <span class="n">greeting</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>You can get a reference to the current object using the <code class="language-plaintext highlighter-rouge">self</code> keyword, but for the most part, it’s used implicitly when resolving methods or instance variables.</p>
<p>There is always a current object, even when you’re not in a method. Within a class definition, the current object is the class. And Ruby even provides a “main” object that is the current object at the top level of a Ruby script.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">Greeter</span>
<span class="nb">puts</span> <span class="nb">self</span>
<span class="c1"># Prints "Greeter"</span>
<span class="k">end</span>
<span class="nb">puts</span> <span class="nb">self</span>
<span class="c1"># Prints "main"</span></code></pre></figure>
<h2 id="changing-the-current-object-with-instance_eval">Changing the current object with instance_eval</h2>
<p>You can also <em>change</em> the current object using the <code class="language-plaintext highlighter-rouge">instance_eval</code> method (or the closely related <code class="language-plaintext highlighter-rouge">instance_exec</code>).</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">greet</span> <span class="o">=</span> <span class="no">Greeter</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"world"</span><span class="p">)</span>
<span class="c1"># Change the object context within the given block.</span>
<span class="n">greet</span><span class="p">.</span><span class="nf">instance_eval</span> <span class="k">do</span>
<span class="c1"># Self now references the greeter object</span>
<span class="n">assert</span> <span class="nb">self</span> <span class="o">==</span> <span class="n">greet</span>
<span class="c1"># You can call its methods without a receiver</span>
<span class="n">print_greeting</span>
<span class="c1"># You can even access its instance variables</span>
<span class="nb">puts</span> <span class="vi">@name</span>
<span class="k">end</span></code></pre></figure>
<p>The <code class="language-plaintext highlighter-rouge">instance_eval</code> method is commonly used for building domain-specific-languages (DSLs) because it lets us control how methods are looked up. When you write <a href="https://guides.rubyonrails.org/routing.html">Rails routes</a>, for example, Rails is using <code class="language-plaintext highlighter-rouge">instance_eval</code> to give you a simple syntax for declaring paths and resources.</p>
<p>The current object has a strong effect on looking up method names, but what about <em>defining</em> a method? This is where the story gets a bit more complicated.</p>
<h2 id="defining-methods">Defining methods</h2>
<p>The <code class="language-plaintext highlighter-rouge">def</code> keyword is typically used to define a new method. Normally, <code class="language-plaintext highlighter-rouge">def</code> appears within a class or module definition, so it’s clear where the method is defined. But Ruby is very flexible. You can put a <code class="language-plaintext highlighter-rouge">def</code> almost anywhere: outside any class, in a block, even within another method.</p>
<p>What do you think happens when a method is defined inside another method?</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">Greeter</span>
<span class="k">def</span> <span class="nf">greeting</span>
<span class="k">def</span> <span class="nf">dismissal</span>
<span class="s2">"Bye, world!"</span>
<span class="k">end</span>
<span class="s2">"Hello, world!"</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>No, Ruby doesn’t have any weird notion of “nested” methods. All that’s happening here is that defining the <code class="language-plaintext highlighter-rouge">dismissal</code> method is part of the <em>functionality</em> of the <code class="language-plaintext highlighter-rouge">greeting</code> method. <code class="language-plaintext highlighter-rouge">dismissal</code> is defined when you <em>call</em> <code class="language-plaintext highlighter-rouge">greeting</code>.</p>
<p>So what class is <code class="language-plaintext highlighter-rouge">dismissal</code> defined on? One might guess that it’s also defined on the <code class="language-plaintext highlighter-rouge">Greeter</code> class, and in this case you’d be right. But <em>why</em> is that the case? It may seem “obvious” or “intuitive,” but it’s very important to understand what’s actually going on, because things won’t always be obvious. Take this example:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">Greeter</span>
<span class="k">def</span> <span class="nf">greeting</span>
<span class="s2">"Hello, world!"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">Greeter</span><span class="p">.</span><span class="nf">instance_eval</span> <span class="k">do</span>
<span class="k">def</span> <span class="nf">dismissal</span>
<span class="s2">"Bye, world!"</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>We’re using <code class="language-plaintext highlighter-rouge">instance_eval</code> to set the object context to the <code class="language-plaintext highlighter-rouge">Greeter</code> class when we define the method. So where is <code class="language-plaintext highlighter-rouge">dismissal</code> defined? You might guess, on the <code class="language-plaintext highlighter-rouge">Greeter</code> class. But you’d be wrong. It gets defined on the <code class="language-plaintext highlighter-rouge">Object</code> class.</p>
<p>So what’s really going on? What actually governs on which class a method is defined?</p>
<h2 id="the-current-class">The “current class”</h2>
<p>The answer is that “self” isn’t the only piece of context that Ruby maintains. The “current object” governs lookup of method names (and instance variables), but method <em>definitions</em> are governed by a separate piece of context that I’ll call the “current class”.</p>
<blockquote>
<p>Note: other terms have been used elsewhere for the “current class”. For example, Yugui used the term “default definee” when she wrote about the Ruby contexts in an <a href="https://blog.yugui.jp/entry/846">earlier article</a>.</p>
</blockquote>
<p>When you define a class using the <code class="language-plaintext highlighter-rouge">class</code> keyword, it creates a class and sets the current class context so that methods are attached to it. Similarly, when a method is called, the current class context is set to <code class="language-plaintext highlighter-rouge">self</code>’s class.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">Greeter</span>
<span class="c1"># Current class is set to Greeter.</span>
<span class="c1"># This ensures the greeting method is defined on Greeter.</span>
<span class="k">def</span> <span class="nf">greeting</span>
<span class="c1"># Current class is set to self's class, which is Greeter.</span>
<span class="c1"># This ensures the dismissal method is also defind on Greeter.</span>
<span class="k">def</span> <span class="nf">dismissal</span>
<span class="s2">"Bye, world!"</span>
<span class="k">end</span>
<span class="s2">"Hello, world!"</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>However, importantly, <code class="language-plaintext highlighter-rouge">instance_eval</code> sets the current object but <em>not</em> the current class.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># Current class is Object at the top level of a Ruby file</span>
<span class="no">Greeter</span><span class="p">.</span><span class="nf">instance_eval</span> <span class="k">do</span>
<span class="c1"># The instance_eval method sets self but not the current class,</span>
<span class="c1"># so the current class is still Object here.</span>
<span class="k">def</span> <span class="nf">dismissal</span>
<span class="s2">"Bye, world!"</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>And this is where <code class="language-plaintext highlighter-rouge">class_eval</code> is different. Whereas <code class="language-plaintext highlighter-rouge">instance_eval</code> sets <em>only</em> the current object, <code class="language-plaintext highlighter-rouge">class_eval</code> sets <em>both</em> the current object and current class.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="no">Greeter</span><span class="p">.</span><span class="nf">class_eval</span> <span class="k">do</span>
<span class="c1"># The current class is now Greeter.</span>
<span class="k">def</span> <span class="nf">dismissal</span>
<span class="s2">"Bye, world!"</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>So we’ve seen that Ruby maintains <em>separate, independent</em> class and object contexts. And that brings up an interesting question: Why?</p>
<h2 id="why-are-current-class-and-current-object-distinct">Why are “current class” and “current object” distinct?</h2>
<p>Why maintain two separate pieces of state? Isn’t that needlessly complicated?</p>
<p>It turns out there’s a good reason for it, and it has to do with Ruby’s goal of making programming intuitive. Let’s look again at the two places we’ve seen method definitions.</p>
<p>When you use a <code class="language-plaintext highlighter-rouge">class</code> declaration, Ruby sets both the current object and the current class to the same thing, the class. This lets you both define methods and call class methods such as <code class="language-plaintext highlighter-rouge">attr_reader</code>, <code class="language-plaintext highlighter-rouge">include</code>, and even <code class="language-plaintext highlighter-rouge">private</code>.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">Greeter</span>
<span class="c1"># current class == self</span>
<span class="c1"># This means you can define methods on the class</span>
<span class="k">def</span> <span class="nf">greeting</span>
<span class="s2">"Hello, world!"</span>
<span class="k">end</span>
<span class="c1"># And you can also call class methods</span>
<span class="nb">attr_reader</span> <span class="ss">:language</span>
<span class="k">end</span></code></pre></figure>
<p>But when you define a method in another method, the current object and the current class are <em>not</em> the same. An arbitrary object doesn’t have methods; only classes do. So the current class is set to the class of the object.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">Greeter</span>
<span class="k">def</span> <span class="nf">greeting</span>
<span class="c1"># current class != self</span>
<span class="c1"># current class == self.class</span>
<span class="k">def</span> <span class="nf">dismissal</span>
<span class="s2">"Bye, world!"</span>
<span class="k">end</span>
<span class="s2">"Hello, world!"</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>In order to support reasonable behavior in both of these two cases, Ruby needs the flexibility to be able to set up the two values, current object and current class, differently.</p>
<h2 id="why-does-this-matter">Why does this matter?</h2>
<p>So Ruby has more context than just <code class="language-plaintext highlighter-rouge">self</code>. Why does it matter?</p>
<p>Well, it might help you avoid some bugs, and it’s always useful to understand the details of the language you are using. But it’s particularly important when you are designing interfaces for other developers to use, especially if you’re designing a domain-specific language.</p>
<p>Ruby is a flexible language, and Ruby programmers expect to be able to use that flexibility, calling code, writing helper methods, and generally doing things you might not expect. If the Rails router had set the current class to some strange value and made it difficult for users to write helper methods, Rails would have been much more brittle and difficult to use, and ultimately less successful.</p>
<p>So it’s important for Ruby library writers to make sure you choose the correct method when using <code class="language-plaintext highlighter-rouge">class_eval</code> or <code class="language-plaintext highlighter-rouge">instance_eval</code>. And in general, library designers need to pay attention to the current class in order to avoid unexpected behavior.</p>
<p>If you’re interested in learning some tips for designing interfaces that pay attention to these issues, see my talk <a href="https://www.youtube.com/watch?v=Ov-tMtOkKS4">“Ruby Ate My DSL!”</a> from RubyConf 2019.</p>
<h2 id="investigating-further">Investigating further</h2>
<p>It turns out, the rabbit hole goes even deeper. A third independent piece of Ruby context governs constants, where they are defined and how they are looked up. This piece of context, known internally as the <code class="language-plaintext highlighter-rouge">cref</code>, represents the lexical nesting of classes and modules, basically what you get from calling <code class="language-plaintext highlighter-rouge">Module.nesting</code>. However, unlike the current object and current class, the cref can’t be changed programmatically, at least not without dropping into C.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">Greeter</span>
<span class="c1"># The cref points to Greeter, so GREETING is defined there</span>
<span class="no">GREETING</span> <span class="o">=</span> <span class="s2">"hello"</span>
<span class="k">end</span>
<span class="nb">puts</span> <span class="no">Greeter</span><span class="o">::</span><span class="no">GREETING</span>
<span class="c1"># prints "hello"</span>
<span class="no">Greeter</span><span class="p">.</span><span class="nf">class_eval</span> <span class="k">do</span>
<span class="c1"># class_eval doesn't affect cref, so SALUTATION is defined on Object</span>
<span class="no">SALUTATION</span> <span class="o">=</span> <span class="s2">"hi"</span>
<span class="k">end</span>
<span class="nb">puts</span> <span class="no">Object</span><span class="o">::</span><span class="no">SALUTATION</span>
<span class="c1"># prints "hi"</span>
<span class="nb">puts</span> <span class="no">Greeter</span><span class="o">::</span><span class="no">SALUTATION</span>
<span class="c1"># Raises NameError</span></code></pre></figure>
<p>Because cref can’t be modified from Ruby, it’s difficult to control how constants are defined and managed in DSLs. This is generally why many DSLs eschew constants or provide alternatives.</p>
<p>And indeed, there are several other elements to the Ruby “context”, controlling such functionality as what <code class="language-plaintext highlighter-rouge">super</code> calls, how iteration keywords like <code class="language-plaintext highlighter-rouge">next</code> and <code class="language-plaintext highlighter-rouge">break</code> behave, and so forth. If you’re interested in exploring this further, the old Ruby hacking guide has <a href="https://ruby-hacking-guide.github.io/module.html">a chapter</a> dedicated to the context, but it’s from the Ruby 1.7 era and might be out of date. Pat Shaughnessy’s excellent book <a href="http://patshaughnessy.net/ruby-under-a-microscope"><em>Ruby Under a Microsocope</em></a> is somewhat newer and also covers some of these topics. (If anyone knows of other resources, leave a note in the comments!)</p>
2021-02-02T00:00:00+00:00https://daniel-azuma.com/blog/2021/01/20/designing-a-ruby-serverless-runtimeDesigning a Ruby Serverless Runtime2021-01-20T00:00:00+00:00Daniel Azumadazuma@gmail.comhttp://daniel-azuma.com/<p>Last week, Google <a href="https://cloud.google.com/blog/products/application-development/ruby-comes-to-cloud-functions">announced</a> the public beta of the Ruby runtime for <a href="https://cloud.google.com/functions">Cloud Functions</a>, Google’s functions-as-a-service (FaaS) hosting platform. Ruby support has lagged a bit behind other languages over the past year or so, but now that we’ve caught up, I thought I’d share some of the design process behind the product.</p>
<p>This article is not a traditional design document. I won’t go through the design itself step-by-step. Instead, I want to discuss some of the design issues we faced, the decisions we made, and why we made them, because it was an interesting exercise in figuring out how to fuse Ruby conventions with those of the public cloud. Some of the trade-offs we made are, I think, emblematic of the challenges the Ruby community as a whole is facing as the industry evolves.</p>
<h2 id="a-ruby-way-of-doing-serverless">A Ruby way of doing serverless</h2>
<p>Bringing Ruby support to a serverless product is a lot more involved than you might expect. At the most basic level, a language runtime is just a Ruby installation, and sure, it’s not hard to configure a Ruby image and install it on a VM. But things become more complex when you bring “serverless” into the mix. Severless is much more than just automatic maintenance and scaling. It’s an entirely different way of thinking about compute resources, one that goes contrary to much of we’ve been taught about deploying Ruby apps for the past fifteen years. When the Ruby team at Google Cloud took on the task of designing the Ruby runtime for Cloud Functions, we were also taking on the daunting task of proposing a <em>Ruby way of doing serverless</em>. While remaining true to the Ruby idioms, practices, and tools familiar to our community, we also had to rethink how we approach web application development at almost every level, from code, to dependencies, persistence, testing, everything.</p>
<p>This article will examine our approach to five different aspects of the design: function syntax, concurrency and lifecycle, testing, dependencies, and standards. In each case, we’ll see a balance between the importance of remaining true to our Ruby roots, and the desire to embrace the new serverless paradigms. We tried very hard to maintain continuity with the traditional Ruby way of doing things, and we also took cues from other Google Cloud Functions language runtimes, as well as precedents set by serverless products from other cloud providers. However, in a few cases, we chose to blaze a different trail. We did so when we felt that current approaches either abused a language feature, or were misleading and encouraged the wrong ideas about serverless app development.</p>
<p>It’s possible, even likely, that some of these decisions will eventually prove to have been wrong. That’s why I’m offering this article now, to discuss what we’ve done and to start the conversation about how we as the Ruby community practice serverless app development. The good news is that Ruby is a very flexible language, and we will have plenty of opportunity to adapt as we learn and as our needs evolve.</p>
<p>So let’s take a look at some of the initial design decisions and trade-offs we made and why we made them.</p>
<h2 id="functional-ruby">Functional Ruby</h2>
<p>“Functions-as-a-Service” (FaaS) is currently one of the more popular serverless paradigms. Google’s Cloud Functions is just one implementation. Many other major cloud providers have their own FaaS product, and there are <a href="https://www.openfaas.com/">open source implementations</a> as well.</p>
<p>The idea, of course, is to use a programming model centered not around web servers, but around <em>functions</em>: stateless pieces of code that take input arguments and return results. It seems like a simple, almost obvious, change in terminology, but it actually has profound implications.</p>
<p><img src="/img/posts/cloud-functions-diagram.png" alt="Diagram of a cloud function" /></p>
<p>The first challenge for Ruby is that, unlike many other programming languages, Ruby actually <em>doesn’t have</em> first-class functions. Ruby is first and foremost an object-oriented language. When we write code and wrap it in a <code class="language-plaintext highlighter-rouge">def</code>, we are writing a <em>method</em>, code that runs in response to a <em>message</em> sent to an <em>object</em>. This is an important distinction, because the objects and classes that form the context of a method call are not part of the serverless abstraction. So their presence can complicate a serverless application, and even mislead us when we’re writing it.</p>
<p>For example, some FaaS frameworks let you write a function with a <code class="language-plaintext highlighter-rouge">def</code> at the top level of a Ruby file:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">handler</span><span class="p">(</span><span class="n">event</span><span class="p">:,</span> <span class="n">context</span><span class="p">:)</span>
<span class="s2">"Hello, world!"</span>
<span class="k">end</span></code></pre></figure>
<p>While this code appears straightforward, it’s important to remember what it actually does. It adds this “function” as a private method on the <code class="language-plaintext highlighter-rouge">Object</code> class, the base class of the Ruby class hierarchy. In other words, the “function” has been added to <em>nearly every object in the Ruby virtual machine</em>. (Unless, of course, the application changes the main object and class context when loading the file, a technique that carries other risks.) At best, this breaks encapsulation and single responsibility. At worst, it risks interfering with the functionality of your application, its dependencies, or even the Ruby standard library. This is why such “top level” methods, while common in simple single-file Ruby scripts and Rakefiles, are not recommended in larger Ruby applications.</p>
<p>The Google Ruby team decided this issue was serious enough that we chose a different syntax, writing functions as blocks:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s2">"functions_framework"</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">http</span><span class="p">(</span><span class="s2">"handler"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">request</span><span class="o">|</span>
<span class="s2">"Hello, world!"</span>
<span class="k">end</span></code></pre></figure>
<p>This provides a Ruby-like way to define functions without modifying the <code class="language-plaintext highlighter-rouge">Object</code> base class. It also has a few side benefits:</p>
<ul>
<li>The name (“handler” in this case) is just a string argument. It doesn’t need to be a legal Ruby method name, nor is there any concern of it colliding with a Ruby keyword.</li>
<li>Blocks exhibit more traditional lexical scoping than do methods, so this will behave more similarly to functions in other languages.</li>
<li>The block syntax makes it easier to manage function definitions. For example, it’s possible to “undefine” functions cleanly, which is important for testing.</li>
</ul>
<p>Of course, there are trade-offs. Among them:</p>
<ul>
<li>The syntax is slightly more verbose.</li>
<li>It requires a library to provide the interface for defining functions as blocks. (Here, Ruby follows other language runtimes for Cloud Functions by utilizing a <a href="https://github.com/GoogleCloudPlatform/functions-framework-ruby">Functions Framework</a> library.)</li>
</ul>
<p>We decided it was worth these trade-offs for the goal of properly distinguishing functions.</p>
<h2 id="to-share-or-not-to-share">To share or not to share</h2>
<p>Concurrency is hard. This is one of the key observations underlying the design of serverless in general, and functions-as-a-service in particular: that we live in a concurrent world and we need ways to cope. The functional paradigm addresses concurrency by insisting that functions not share state (except through an external persistence system such as a queue or database).</p>
<p>This is in fact another reason we chose to use block syntax rather than method syntax. Methods imply objects, which carry state in the form of instance variables, state that might not work as expected in a stateless FaaS environment. Eschewing methods is a subtle but effective syntactic way to discourage practices we know to be problematic.</p>
<p>That said, what if you need to share <em>resources</em>, such as database connection pools? When would you initialize such resources, and how would you access them?</p>
<p>For this purpose, the Ruby runtime supports <em>startup functions</em> that can initialize resources and passes them into function calls. Importantly, while the startup function can create resources, normal functions can only read them.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s2">"functions_framework"</span>
<span class="c1"># Use an on_startup block to initialize a shared client and store it in</span>
<span class="c1"># the global shared data.</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">on_startup</span> <span class="k">do</span>
<span class="nb">require</span> <span class="s2">"google/cloud/storage"</span>
<span class="n">set_global</span> <span class="ss">:storage_client</span><span class="p">,</span> <span class="no">Google</span><span class="o">::</span><span class="no">Cloud</span><span class="o">::</span><span class="no">Storage</span><span class="p">.</span><span class="nf">new</span>
<span class="k">end</span>
<span class="c1"># The shared storage_client can be accessed by all function invocations</span>
<span class="c1"># via the global shared data.</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">http</span> <span class="s2">"storage_example"</span> <span class="k">do</span> <span class="o">|</span><span class="n">request</span><span class="o">|</span>
<span class="n">bucket</span> <span class="o">=</span> <span class="n">global</span><span class="p">(</span><span class="ss">:storage_client</span><span class="p">).</span><span class="nf">bucket</span> <span class="s2">"my-bucket"</span>
<span class="n">file</span> <span class="o">=</span> <span class="n">bucket</span><span class="p">.</span><span class="nf">file</span> <span class="s2">"path/to/my-file.txt"</span>
<span class="n">file</span><span class="p">.</span><span class="nf">download</span><span class="p">.</span><span class="nf">to_s</span>
<span class="k">end</span></code></pre></figure>
<p>Notice that we chose to define special methods <code class="language-plaintext highlighter-rouge">global</code> and <code class="language-plaintext highlighter-rouge">set_global</code> to interact with global resources. (By the way, these are not methods on Object, but methods on a specific class we use as the function context.) Again, we could have used more traditional idioms such as Ruby global variables, or even a constructor and instance variables, to pass information from startup code to function calls. However, those idioms would have communicated the wrong things. We’re not writing ordinary Ruby classes and methods where sharing data is normal, but <em>serverless functions</em> where sharing data is hazardous if even possible, and we felt it was important for the <em>syntax</em> to emphasize the distinction. The special methods were a deliberate design decision, to discourage practices could be dangerous in the presence of concurrency.</p>
<h2 id="test-first">Test first</h2>
<p>A strong testing culture is central to the Ruby community. Popular frameworks, such as Rails, acknowledge this and encourage active testing by providing testing tools and scaffolding as part of the framework, and the Ruby runtime for Google Cloud Functions follows suit by providing testing tools for serverless functions.</p>
<p>The FaaS paradigm actually fits very well with tests. Functions are by nature easily testable; simply pass in arguments and assert against results. In particular, you don’t need to spin up a web server to run tests, because web servers are not part of the abstraction. The Ruby runtime provides a module of helper methods for creating HTTP request and Cloud Event objects to use as inputs, and otherwise most tests are very straightforward to write.</p>
<p>One of the main testing challenges we encountered, though, had to do with testing <em>initialization code</em>. Indeed, this is an issue that some of Google’s Ruby team members have had with other frameworks, including Rails: it is difficult to test an app’s initialization process, because framework initialization typically happens outside the tests, before they run. We therefore designed a way for tests to isolate the entire lifecycle of a function, including initialization. This allows us to run initialization within a test, and even repeat it multiple times allowing tests of different aspects:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s2">"minitest/autorun"</span>
<span class="nb">require</span> <span class="s2">"functions_framework/testing"</span>
<span class="k">class</span> <span class="nc">MyTest</span> <span class="o"><</span> <span class="no">Minitest</span><span class="o">::</span><span class="no">Test</span>
<span class="c1"># Include testing helper methods</span>
<span class="kp">include</span> <span class="no">FunctionsFramework</span><span class="o">::</span><span class="no">Testing</span>
<span class="k">def</span> <span class="nf">test_startup_tasks</span>
<span class="c1"># Run the lifecycle, and test the startup tasks in isolation.</span>
<span class="n">load_temporary</span> <span class="s2">"app.rb"</span> <span class="k">do</span>
<span class="n">globals</span> <span class="o">=</span> <span class="n">run_startup_tasks</span> <span class="s2">"storage_example"</span>
<span class="n">assert_kind_of</span> <span class="no">Google</span><span class="o">::</span><span class="no">Cloud</span><span class="o">::</span><span class="no">Storage</span><span class="p">,</span> <span class="n">globals</span><span class="p">[</span><span class="ss">:storage_client</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">test_storage_request</span>
<span class="c1"># Rerun the entire lifecycle, including the startup tasks, and</span>
<span class="c1"># test a function call.</span>
<span class="n">load_temporary</span> <span class="s2">"app.rb"</span> <span class="k">do</span>
<span class="n">request</span> <span class="o">=</span> <span class="n">make_get_request</span> <span class="s2">"https://example.com/foo"</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">call_http</span> <span class="s2">"storage_example"</span><span class="p">,</span> <span class="n">request</span>
<span class="n">assert_equal</span> <span class="mi">200</span><span class="p">,</span> <span class="n">response</span><span class="p">.</span><span class="nf">status</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>The <code class="language-plaintext highlighter-rouge">load_temporary</code> method loads function definitions in a sandbox, isolating them and their initialization from other test runs. That and other helper methods are defined in the <code class="language-plaintext highlighter-rouge">FunctionsFramework::Testing</code> module, which can be included in minitest or rspec tests.</p>
<p>So far we’ve really provided only basic testing tools for the Ruby runtime, and I expect we’ll add significantly to the toolset as our users develop more apps and we identify more of the common testing patterns. But I strongly believe testing tools are an important part of any library, especially one that purports to be a framework or runtime, and so it was a core part of our design from the start.</p>
<h2 id="the-dependable-runtime">The dependable runtime</h2>
<p>Most nontrivial Ruby apps require third-party gems. For Ruby apps that use Google Cloud Functions, we require at least one gem, the <code class="language-plaintext highlighter-rouge">functions_framework</code> that provides the Ruby interfaces for writing functions. You may also need other gems for handling data, authenticating and integrating with other services, and so forth. Dependency management is a crucial part of any runtime framework.</p>
<p>We made several design decisions around dependency management. And the first and most important was to embrace <a href="https://bundler.io">Bundler</a>.</p>
<p>I know that sounds a bit frivolous. Most Ruby apps these days use Bundler anyway, and there are very few alternatives, hardly any in widespread use. But we actually took it a step further and built Bundler deep into our infrastructure, <em>requiring</em> that apps use it in order to work with Cloud Functions. We did this because, knowing exactly how an app will manage its dependencies would allow us to implement some important optimizations.</p>
<p><img src="/img/posts/cloud-functions-bundle.png" alt="Deployed cloud function" /></p>
<p>Vital to a good FaaS system is the speed of deployments and cold starts. In a serverless world, your code might be updated, deployed, and torn down many times in rapid succession, so it’s crucial to eliminate bottlenecks such as resolving and installing dependencies. Because we standardize on one system for dependency management, we are able to <em>cache</em> dependencies aggressively. We judged that the performance gains of implementing such caching, as well as the reduced load on the Rubygems.org infrastructure, far outweighed the reduced flexibility of not being able to use an alternative to Bundler.</p>
<p>Another feature, or maybe quirk, of the Ruby runtime for Google Cloud Functions, is that it will fail deployments if the gem lockfile is missing or inconsistent. We require that <code class="language-plaintext highlighter-rouge">Gemfile.lock</code> is present when you deploy. This was another decision made to enforce a best practice. If the lockfile gets reresolved during deployment, your builds may not be repeatable, and you may not be running against the same dependencies you tested with. We avoid this by requiring an up-to-date <code class="language-plaintext highlighter-rouge">Gemfile.lock</code> file, and again, we are able to enforce this because we require the use of Bundler.</p>
<h2 id="standards-old-and-new">Standards old and new</h2>
<p>Finally, good designs lean on standards and prior art. We had to innovate a bit to define robust functions in Ruby, but when it comes to representing the function arguments, there were already existing libraries or emerging standards to follow.</p>
<p>For example, in the near term, many functions will respond to <em>web hooks</em>, and will need information about the incoming HTTP request. It would not be difficult to design a class that represents an HTTP request, but the Ruby community already has a standard API for this sort of thing: <a href="https://github.com/rack/rack">Rack</a>. We adopted the Rack request class for our event parameters, and we support standard Rack responses for return values.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s2">"functions_framework"</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">http</span> <span class="s2">"http_example"</span> <span class="k">do</span> <span class="o">|</span><span class="n">request</span><span class="o">|</span>
<span class="c1"># request is a Rack::Request object.</span>
<span class="n">logger</span><span class="p">.</span><span class="nf">info</span> <span class="s2">"I received </span><span class="si">#{</span><span class="n">request</span><span class="p">.</span><span class="nf">request_method</span><span class="si">}</span><span class="s2"> from </span><span class="si">#{</span><span class="n">request</span><span class="p">.</span><span class="nf">url</span><span class="si">}</span><span class="s2">!"</span>
<span class="c1"># You can return a standard Rack response array, or use one of</span>
<span class="c1"># several convenience formats.</span>
<span class="p">[</span><span class="mi">200</span><span class="p">,</span> <span class="p">{},</span> <span class="s2">"ok"</span><span class="p">]</span>
<span class="k">end</span></code></pre></figure>
<p>Not only does this provide a familiar API, but it also makes it easy to integrate with other Rack-based libraries. For example, it is easy to layer a <a href="http://sinatrarb.com">Sinatra</a> app atop Cloud Functions because they both speak Rack.</p>
<p>In the longer term, we increasingly expect functions-as-a-service to fit as a component in <em>evented</em> systems. Event-based architectures are rapidly growing in popularity, often surrounding event queues such as <a href="https://kafka.apache.org/">Apache Kafka</a>. And a crucial element of an event architecture is a standard way to describe the events themselves, a standard understood by event senders, brokers, transport, and consumers.</p>
<p>Google Cloud Functions has thrown its support behind <a href="https://cloudevents.io">CNCF CloudEvents</a>, an emerging standard for describing and delivering events. In addition to HTTP requests, Cloud Functions can also receive data in the form of a CloudEvent, and the runtime will even convert some legacy event types to CloudEvents when calling your function.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s2">"functions_framework"</span>
<span class="no">FunctionsFramework</span><span class="p">.</span><span class="nf">cloud_event</span> <span class="s2">"my_handler"</span> <span class="k">do</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span>
<span class="c1"># event is a CloudEvent object defined by the cloud_events gem</span>
<span class="n">logger</span><span class="p">.</span><span class="nf">info</span> <span class="s2">"I received a CloudEvent of type </span><span class="si">#{</span><span class="n">event</span><span class="p">.</span><span class="nf">type</span><span class="si">}</span><span class="s2">!"</span>
<span class="k">end</span></code></pre></figure>
<p>To support CloudEvents in Ruby, the Google Ruby team worked closely with the CNCF Serverless Working Group, and even volunteered to take over development of the <a href="https://github.com/cloudevents/sdk-ruby">Ruby SDK</a> for CloudEvents. This turned out to be a lot of work, but we considered it crucial to be able to use the official, standard Ruby interfaces, even if we had to implement it ourselves.</p>
<h2 id="the-serverless-future">The serverless future</h2>
<p>“Serverless” and “functions-as-a-service” hosting has garnered a lot of interest over the past few years. I think the jury is still out on how useful it will be for most workloads, but the possibilities are intriguing. “Zero”-devops, automatic maintenance and scaling, no servers to maintain, and pay only for the compute resources you actually use. I recently <a href="https://daniel-azuma.com/blog/2019/07/01/deploying-my-blog-to-google-cloud-run">moved this very blog</a> from a personal <a href="https://kubernetes.io">Kubernetes</a> cluster to Google’s managed <a href="https://cloud.google.com/run">Cloud Run</a> service, and slashed my monthly bill from dozens of dollars down to pennies.</p>
<p>That said, serverless is a fundamentally different way of thinking about compute resources, and as an industry we are still very early in our understanding of the implications. As my team designed the Ruby runtime for Google Cloud Functions, we were mindful about the ways the serverless paradigm interacts with our normal Ruby practices. In some cases, as with testing, it encourages us to double down on the good parts of Ruby culture. In others, as with how to express and notate a function in a language that strictly speaking doesn’t have them, it challenges our ideas of how to present code and communicate its intent.</p>
<p>But in all cases, the experience of designing the runtime reminded me that we’re in a industry of constant change. Serverless is just the latest in a string of disruptions that have included the public cloud in general, and even Rails, and Ruby itself. It’s not yet clear how much serverless will stick, but it is here today, and it’s up to us to respond with curiosity, creativity, and a willingness not to take what we know for granted.</p>
2021-01-20T00:00:00+00:00https://daniel-azuma.com/blog/2019/11/06/is-it-time-to-replace-rakeIs it time to replace Rake?2019-11-06T00:00:00+00:00Daniel Azumadazuma@gmail.comhttp://daniel-azuma.com/<p>Do you have really heavy Rakefiles?</p>
<p>I do. My day job includes working in a Ruby-based <a href="https://github.com/googleapis/google-cloud-ruby">repository</a> with a lot of test, build, and release tooling, living in an <em>enormous</em> Rakefile. Actually, an enormous main Rakefile, plus lots of additional Rakefiles in dozens of subdirectories. Combined, it’s well over seven thousand lines of Rakefile—and that’s actually quite a bit smaller than it has been in the past.</p>
<p>Rakefiles are the ugly junk closets of our Ruby projects. Our app might have nicely factored classes, short methods, and excellent test coverage for our application code, but that seldom extends to our Rakefiles. Is your Rakefile covered by your code health tools? Do you have tests for your Rake tasks? Didn’t think so.</p>
<p>And running Rake tasks is another adventure. Simple cases such as <code class="language-plaintext highlighter-rouge">rake test</code> work great. But what if you need to pass in flags or arguments to a task? Yes, you can do it with Rake. There are several ways in fact. But it’s not pretty.</p>
<p>Rake is an incredibly useful tool that has served the community really well for many years. But I’ve always felt like it was not quite right, that its design didn’t quite match up. And that’s not surprising, because… it doesn’t.</p>
<h2 id="rake-and-make">Rake and Make</h2>
<p>Rake was written as a replacement for the unix <em>make</em> tool. As a <em>make</em> tool, it excels, covering all the essential aspects of file-based dependencies and builds. And the Rakefile format is, to a Rubyist, <em>much</em> nicer to work with than Makefile.</p>
<p>But it is still a <em>make</em> replacement, designed for building projects in languages like C with file-based compilation dependencies. Where you have to recompile object files because source and header files changed.</p>
<p>We generally don’t do that sort of thing in Ruby.</p>
<p>Indeed I think the only time Ruby devs compile C regularly is when we install a gem with C extensions. And for that, most gems use <a href="https://ruby-doc.org/stdlib/libdoc/mkmf/rdoc/MakeMakefile.html">mkmf</a> to create, yes, a Makefile. That’s right, we generally don’t even use Rake to build our C extensions. Maybe we should. But I digress.</p>
<p>When was the last time you wrote a Rakefile with a FileTask? Maybe you have, but most of us seldom if ever use Rake’s primary features. <em>We don’t use Rake for what it’s designed for.</em></p>
<p>Instead, we mostly use Rake to write <em>scripts</em>. Scripts to run our tests, build assets, initiate deployments, and so forth.</p>
<h2 id="managing-project-scripts">Managing project scripts</h2>
<p>Now when it comes to writing and using scripts, there are a few capabilities I find important.</p>
<p>I might need to pass <strong>arguments or flags</strong> to the script. For example, a test script might use arguments to configure the test environment or select which tests to run. Rake does have a syntax for arguments, but I’d like to use normal unix arguments and flags. It would be much nicer to say</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>run-test --integration --env=production
</code></pre></div></div>
<p>rather than</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rake 'run-test[integration,production]'
</code></pre></div></div>
<p><strong>Online help</strong> for my scripts is also very important. I’m not going to remember the usage for all my scripts, and I don’t want to have to open the source every time. Rake provides a way to set a short description for each task, but it’s not well suited for long or complex content, or especially for documenting arguments.</p>
<p>I want my implementation to be <strong>organized, testable, and maintainable</strong>, just like application code. Large scripts should be broken into smaller methods. There should be principled ways to share code and data, and to break big projects into multiple files. Yes, all this is technically possible with Rake. It’s just Ruby after all. But, let’s face it: the Rakefile format doeesn’t exactly encourage good software practice.</p>
<p>Support for typical <strong>script-y features</strong> would be nice. Terminal features such as input, progress meters, and styled text. Shell completion. Rich file system manipulation and process control features. Not a lot of this is provided by Rake, and while you can bring in some additional gems, it’s not common practice.</p>
<p>But there’s one thing I like about Rake that I want to keep: the <strong>Rakefile</strong>. Or something like it. The ability to define scripts in a file in the current directory, with a simple DSL. That’s why I’ve been using Rake all this time, despite its limitations. I suspect that’s why we all still use it.</p>
<h2 id="alternatives-to-rake">Alternatives to Rake</h2>
<p>So is there an alternative?</p>
<p>Actually, there are a few. <a href="https://github.com/erikhuda/thor">Thor</a> is probably best known as a framework for building command line applications in your own gems. But it also can read “Thorfiles” in the current directory, and run them as tasks using the <code class="language-plaintext highlighter-rouge">thor</code> executable. Your tasks then have access to all the features of Thor, including normal unix-style command line options and flags, and automatic help.</p>
<p>I’ve also been experimenting with a similar tool myself, called <a href="https://github.com/dazuma/toys">Toys</a>. Like Thor, Toys reads its scripts from files in the current directory, and it supports unix-style command line options and flags, and automatically generates help screens. But it goes further, providing features that I’ve found helpful to manage the complex projects I work on. It provides better code organization, integrates with tab completion and the <code class="language-plaintext highlighter-rouge">did_you_mean</code> gem, includes templates for generating common tasks, provides helpers for building terminal apps and controlling subprocesses, has built-in logging, and a whole lot more. Toys can also read an existing Rakefile, making it easy to migrate.</p>
<p>I’ve been using Toys to manage workflow scripts at my day job for several years already. And recently I’ve started replacing the Rakefiles for my open-source Ruby projects. Both usability and maintainability have improved significantly.</p>
<p>The Rails team also decided to move on from Rake for Rails apps. Several versions ago, the Rake tasks in new Rails apps were deprecated in favor of <code class="language-plaintext highlighter-rouge">bin/rails</code>, a custom command line tool.</p>
<p>So we already have some good alternatives. I think it’s just a matter of the Ruby community at large trying them out.</p>
<h2 id="is-it-time-to-replace-rake">Is it time to replace Rake?</h2>
<p>Rake has served the community well for many years, and for some uses it’s still very appropriate. Do you need to orchestrate building files in your project—whether that’s C source, assets, codegen, or other tasks with file-based dependencies? Then Rake will continue to be your friend.</p>
<p>But for many, maybe most, of your project tasks—running tests, deploying code, performing admin tasks, etc.—it may make more sense to use a tool that is optimized for <em>writing scripts</em>. For these cases, maybe it is indeed time to replace Rake.</p>
<p>If you agree, consider checking out <a href="https://github.com/dazuma/toys">Toys</a>. I’d love to hear whether it works for you, and how it could be better. And if you think Rake should continue to be the tool of choice, I’d also love to hear your thoughts.</p>
2019-11-06T00:00:00+00:00https://daniel-azuma.com/blog/2019/07/01/deploying-my-blog-to-google-cloud-runDeploying My Blog to Google Cloud Run2019-07-01T00:00:00+00:00Daniel Azumadazuma@gmail.comhttp://daniel-azuma.com/<p>For the past few years, this blog has been hosted on a personal <a href="https://kubernetes.io">Kubernetes</a> cluster. In addition to my blog, I used the cluster to host a few other websites, as well as run occasional compute jobs. Kubernetes is a great, very flexible technology, and since (full disclosure) I work at Google, throwing container images at a server infrastructure feels very natural to me. But maintaining a Kubernetes cluster means paying for the VMs, and lately I’ve been wondering if I can avoid those costs.</p>
<p>So I started to move my sites and my jobs off Kubernetes, and finally, a month ago, I was able to turn down the cluster completely. I migrated this blog to <a href="https://cloud.google.com/run">Cloud Run</a>, a container-based serverless environment that Google introduced in April. It’s been working perfectly so far, and since it fits easily into the free tier, my costs have gone down to effectively zero. And I know that if I ever get slashdotted… this is Google: they’ll scale me up as needed.</p>
<p>In this article, I’ll discuss whether a serverless platform might be a good fit for a static site. Then I’ll provide a tutorial for deploying a <a href="https://jekyllrb.com">Jekyll</a>-based static site to Cloud Run. Along the way you’ll also learn some good practices for containerizing static sites. It shouldn’t be too difficult to adapt these instructions for other static site generators such as <a href="https://gohugo.io/">Hugo</a>.</p>
<h2 id="why-serverless-for-static-sites">Why serverless for static sites?</h2>
<p>These days we have a number of choices for hosting static sites. One of the easiest is to use a cloud storage service such as <a href="https://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteHosting.html">Amazon S3</a> or <a href="https://cloud.google.com/storage/docs/hosting-static-website">Google Cloud Storage</a>. Just upload your static content to a storage bucket, configure a few knobs, and you have a website. Source control services such as <a href="https://pages.github.com/">GitHub</a> and <a href="https://confluence.atlassian.com/bitbucket/publishing-a-website-on-bitbucket-cloud-221449776.html">Bitbucket</a> also often provide web hosting for content pushed to their repositories. For simple static sites, options such as these are generally straightforward and inexpensive.</p>
<p>With the advent of the serverless cloud, we now have a third inexpensive option. With many serverless platforms, you don’t pay for containers or VMs or storage—just the compute resources that you actually use. And for a static site, you don’t use very much.</p>
<p>But still, why choose a general-purpose serverless platform over storage-based or source control hosting?</p>
<p>Different deployment options always have pros and cons. One of the chief benefits of general-purpose serverless, however, is <em>flexibility</em>. Static sites are almost never “purely” static. Maybe you need custom error handling and error documents. Maybe you need more sophisticated redirects. Maybe you need to hide some content behind authentication. Maybe the majority of your site is static, but you still need server-side scripts for a few cases. Simple content-based hosting generally provides some configuration knobs to help with these extras, but they can’t always handle every case.</p>
<p>In my blog, I have a large number of redirects, and a few special cases handled by specially crafted Nginx configuration. So for me, one of the big draws of Google Cloud Run was the ability to provide my own container with my own Nginx config files.</p>
<p>There’s of course no one size that fits all. But if you’re having trouble getting S3 to handle your site the way you need, or if you’re currently running on VMs or on your own servers and want to cut costs, a serverless platform might be your sweet spot as well.</p>
<h2 id="jekyll-on-cloud-run">Jekyll on Cloud Run</h2>
<p>The rest of this article is a tutorial for deploying a <a href="https://jekyllrb.com">Jekyll</a>-based site to <a href="https://cloud.google.com/run">Google Cloud Run</a>. Cloud Run is particularly effective for static sites because it uses containers, letting you configure your own web server for static content. (In contrast, “function” or “lambda” based platforms are typically tailored for specific dynamic web languages or frameworks, and might not support static sites at all.)</p>
<p>Note that there are two “flavors” of Cloud Run: a fully-managed flavor that runs directly in Google’s infrastructure, and a flavor that runs on your own <a href="https://cloud.google.com/kubernetes-engine/">Kubernetes Engine</a> cluster. For static sites, you will probably prefer the former, because it’s the one that gives you the very cheap pay-per-use model. (And in my case, part of the whole purpose was to get rid of my Kubernetes cluster.) However, it’s still easy to adapt these instructions to deploy to Cloud Run on GKE if you want more control over the infrastructure. <a href="https://www.youtube.com/watch?v=RVdhyprptTQ">This video</a> has additional info on the differences between the two Cloud Run flavors.</p>
<p>Jekyll is written in Ruby. I’ll assume you already have Ruby and Jekyll installed, but if you need help, the <a href="https://jekyllrb.com/docs/installation/">Jekyll documentation</a> has detailed instructions.</p>
<h3 id="creating-and-testing-a-jekyll-project">Creating and testing a Jekyll project</h3>
<p>We’ll start by creating a new Jekyll project. (If you have an existing project, feel free to use it instead.)</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>jekyll new mysite
<span class="nv">$ </span><span class="nb">cd </span>mysite</code></pre></figure>
<p>Now to run your site locally:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>bundle <span class="nb">exec </span>jekyll serve</code></pre></figure>
<p>Jekyll will build your site locally and serve it on port 4000. Point your web browser to localhost:4000 to see.</p>
<p>You can edit your site by editing posts in the <code class="language-plaintext highlighter-rouge">_posts</code> directory, or by making changes to the configuration and layouts. As you make changes, <code class="language-plaintext highlighter-rouge">jekyll serve</code> will notice the file edits and rebuild your site automatically. We won’t go through all the details of how to use Jekyll here, but there’s a good <a href="https://jekyllrb.com/docs/step-by-step/01-setup/">tutorial</a> in the Jekyll documentation.</p>
<p>Type <code class="language-plaintext highlighter-rouge">CTRL</code>-<code class="language-plaintext highlighter-rouge">C</code> to stop the server.</p>
<p>Using <code class="language-plaintext highlighter-rouge">jekyll serve</code> is a convenient way to view your site during development, but it’s not how you should run it in production. In fact, as we shall see, you don’t need Jekyll, or even Ruby, installed at all in your final production container. So let’s explore how to create an efficient production image of your site.</p>
<h3 id="creating-a-production-docker-image">Creating a production Docker image</h3>
<p><a href="https://www.docker.com/">Docker</a> has become the <em>de facto</em> standard way to package applications for deployment, and Cloud Run conveniently uses Docker images as its input format. Here we’ll create a Docker image to deploy your site.</p>
<p>Our desired image will serve our static site directly from <a href="https://www.nginx.com/">Nginx</a>, a high-performance web server. It’s a static site, so we don’t need Jekyll or even Ruby at runtime. However, we do need Jekyll to <em>build</em> the static site. That is, <em>building</em> and <em>running</em> have different requirements, so we’re going to separate them into different phases, as diagrammed below.</p>
<p><img src="/img/posts/jekyll-cloud-run-diagram.png" alt="Diagram of the build process" /></p>
<p>We will write the items in the blue boxes:</p>
<ul>
<li>Site content and Jekyll configs as inputs to Jekyll</li>
<li>Nginx config files and startup scripts that launch Nginx with the correct configuration.</li>
<li>A Dockerfile that describes the build process.</li>
</ul>
<p>Then when we perform a <code class="language-plaintext highlighter-rouge">docker build</code>, it will proceed in two phases. First, the build phase runs, taking our blog posts and Jekyll configs, and running Jekyll to produce HTML output. Second, the built HTML, along with the Nginx configs and startup scripts, are installed into the runtime image.</p>
<h4 id="configuring-nginx">Configuring Nginx</h4>
<p>Inside our <code class="language-plaintext highlighter-rouge">mysite</code> directory, create a subdirectory called <code class="language-plaintext highlighter-rouge">_app</code>. This directory will contain our Nginx configuration files and startup script. Because it begins with an underscore, Jekyll won’t try to build it as part of your site html. It will just pass directly through into your final runtime image.</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span><span class="nb">mkdir </span>_app
<span class="nv">$ </span><span class="nb">cd </span>_app</code></pre></figure>
<p>Let’s write the Nginx configuration. Create a file called <code class="language-plaintext highlighter-rouge">_app/nginx.conf.in</code> with the following content:</p>
<figure class="highlight"><pre><code class="language-nginx" data-lang="nginx"><span class="k">worker_processes</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">working_directory</span> <span class="n">/app</span><span class="p">;</span>
<span class="k">daemon</span> <span class="no">off</span><span class="p">;</span>
<span class="k">events</span> <span class="p">{</span>
<span class="kn">worker_connections</span> <span class="mi">80</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">http</span> <span class="p">{</span>
<span class="kn">include</span> <span class="n">/etc/nginx/mime.types</span><span class="p">;</span>
<span class="kn">server</span> <span class="p">{</span>
<span class="kn">listen</span> <span class="nv">$PORT</span><span class="p">;</span>
<span class="kn">root</span> <span class="n">/app/site</span><span class="p">;</span>
<span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
<span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri</span><span class="s">.html</span> <span class="nv">$uri</span><span class="n">/</span> <span class="n">/404.html</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>This isn’t an Nginx tutorial, so I won’t go over everything here in detail. However, I’ll point out a couple of things.</p>
<p>First, we intentionally set <code class="language-plaintext highlighter-rouge">worker_connections</code> to 80. Cloud Run currently has a maximum concurrency of 80 (meaning it will allow up to 80 simultaneous connections to each instance). Nginx can often handle more, but because Cloud Run currently has this limit, we’ll pass that info on to Nginx so it can optimize itself.</p>
<p>Second, notice that we’re listening to port <code class="language-plaintext highlighter-rouge">$PORT</code>. This is actually not “valid” Nginx config syntax by itself. Instead, we’re going to write a startup script that treats this Nginx config as a <em>template</em>, and substitutes the actual port number here at runtime. This is because Cloud Run’s <a href="https://cloud.google.com/run/docs/reference/container-contract">runtime specification</a> states that the port isn’t actually known until runtime: it will tell you what port to listen to via the <code class="language-plaintext highlighter-rouge">PORT</code> environment variable.</p>
<p>So our next task is to write a script that reads the environment variable and substitutes the correct value into the config template. Create a file called <code class="language-plaintext highlighter-rouge">_app/start.sh</code> with the following content:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c">#!/bin/bash</span>
<span class="o">[[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$PORT</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&&</span> <span class="nb">export </span><span class="nv">PORT</span><span class="o">=</span>8080
envsubst <span class="s1">'$PORT'</span> < /app/nginx.conf.in <span class="o">></span> /app/nginx.conf
<span class="nb">exec </span>nginx <span class="nt">-c</span> /app/nginx.conf</code></pre></figure>
<p>Let’s pick apart what this is doing. First, it checks that the <code class="language-plaintext highlighter-rouge">PORT</code> variable is set, and if not, it sets it to a default value of <code class="language-plaintext highlighter-rouge">8080</code>. Cloud Run always sets the variable, but we’re also going to test this image locally outside Cloud Run, so we’ll make sure it has a value in that case.</p>
<p>Next, we substitute the value of the <code class="language-plaintext highlighter-rouge">PORT</code> environment variable in the <code class="language-plaintext highlighter-rouge">nginx.conf.in</code> file, and write the result to the final <code class="language-plaintext highlighter-rouge">nginx.conf</code> that we’ll use. Finally, we start Nginx.</p>
<p>It’s important to tell <code class="language-plaintext highlighter-rouge">envsubst</code> to substitute only the <code class="language-plaintext highlighter-rouge">PORT</code> environment variable (because our Nginx configuration file also includes syntax like <code class="language-plaintext highlighter-rouge">$uri</code> that we want <code class="language-plaintext highlighter-rouge">envsubst</code> to leave alone.)</p>
<p>It’s also important to prefix our Nginx command with <code class="language-plaintext highlighter-rouge">exec</code>. This causes Nginx to <em>replace</em> the script process so that it receives signals. This is important for Cloud Run to be able to control the container effectively.</p>
<p>Save the two files, and set the execute bit on your start script:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span><span class="nb">chmod </span>a+x start.sh</code></pre></figure>
<p>You should now have the two files <code class="language-plaintext highlighter-rouge">nginx.conf.in</code> and <code class="language-plaintext highlighter-rouge">start.sh</code> inside the <code class="language-plaintext highlighter-rouge">_app</code> directory in your <code class="language-plaintext highlighter-rouge">mysite</code> project.</p>
<h4 id="writing-the-dockerfile">Writing the Dockerfile</h4>
<p>Next we’re going to write a Dockerfile to build a Docker image of your site.</p>
<p>Go back to your <code class="language-plaintext highlighter-rouge">mysite</code> project directory, and create another subdirectory called <code class="language-plaintext highlighter-rouge">_build</code>. Again, because this name begins with an underscore, Jekyll won’t try to treat any of its files as site content.</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span><span class="nb">cd</span> /path/to/mysite
<span class="nv">$ </span><span class="nb">mkdir </span>_build
<span class="nv">$ </span><span class="nb">cd </span>_build</code></pre></figure>
<p>Create a file called <code class="language-plaintext highlighter-rouge">_build/Dockerfile</code> with the following content:</p>
<figure class="highlight"><pre><code class="language-docker" data-lang="docker"><span class="k">FROM</span><span class="w"> </span><span class="s">ruby:2</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build</span>
<span class="k">RUN </span>gem <span class="nb">install </span>bundler
<span class="k">WORKDIR</span><span class="s"> /workspace</span>
<span class="k">COPY</span><span class="s"> Gemfile* /workspace/</span>
<span class="k">RUN </span>bundle <span class="nb">install</span>
<span class="k">COPY</span><span class="s"> . /workspace</span>
<span class="k">ENV</span><span class="s"> JEKYLL_ENV=production</span>
<span class="k">RUN </span>bundle <span class="nb">exec </span>jekyll build
<span class="k">FROM</span><span class="s"> nginx:1</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> _app /app</span>
<span class="k">COPY</span><span class="s"> --from=build /workspace/_site /app/site</span>
<span class="k">CMD</span><span class="s"> ["/app/start.sh"]</span></code></pre></figure>
<p>This may look a bit more complex than other “getting started” Dockerfiles you may have seen, so let’s take a closer look at what it’s doing.</p>
<p>Remember that our build process was going to proceed in two stages. You can see that now in this multi-stage Dockerfile. The first stage stage starts with the standard <a href="https://hub.docker.com/_/ruby">Ruby-Debian base image</a>, installs your bundle (which should include Jekyll), and performs a production Jekyll build. The results are left in the <code class="language-plaintext highlighter-rouge">/workspace/_site</code> directory.</p>
<p>Then the second stage creates the final image that we will deploy to Cloud Run. It starts with the standard <a href="https://hub.docker.com/_/nginx">Nginx base image</a>, copies in our Nginx config file and startup script, and also copies in the built html files from the first stage. Note that the final image includes only what is needed to serve your site: Nginx, but not Ruby or Jekyll. This <a href="https://docs.docker.com/develop/develop-images/multistage-build/">two-stage</a> strategy is a common best practice when building Docker images.</p>
<h4 id="testing-your-image-locally">Testing your image locally</h4>
<p>Now that we have a Dockerfile, we can build the image locally. Return to your <code class="language-plaintext highlighter-rouge">mysite</code> directory, and perform a docker build, pointing at our Dockerfile.</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span><span class="nb">cd</span> /path/to/mysite
<span class="nv">$ </span>docker build <span class="nt">-t</span> mysite <span class="nt">-f</span> _build/Dockerfile .</code></pre></figure>
<p>This will build your site as a Docker image and tag it with the name <code class="language-plaintext highlighter-rouge">mysite</code>. We can now try running it.</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>docker run <span class="nt">--rm</span> <span class="nt">-it</span> <span class="nt">-p</span> 8080:8080 mysite</code></pre></figure>
<p>This will run your image with your static site, and expose port 8080. Now you can test it by pointing your web browser to localhost:8080.</p>
<p>Hit <code class="language-plaintext highlighter-rouge">CTRL</code>-<code class="language-plaintext highlighter-rouge">C</code> to stop your Docker image.</p>
<h3 id="deploying-to-cloud-run">Deploying to Cloud Run</h3>
<p>Now that we have a working Docker image, it’s time to deploy to Cloud Run.</p>
<h4 id="setting-up-google-cloud">Setting up Google Cloud</h4>
<p>If you do not yet have a Google Cloud project, go to the <a href="https://console.cloud.google.com/">console</a> and create one. You’ll need to enable billing in order to use Cloud Run. But don’t worry—unless your site has a truly massive amount of traffic, you’ll easily fit into the free tier for this tutorial.</p>
<p>Enable Cloud Build and Cloud Run in your project, if you haven’t already:</p>
<ul>
<li>Navigate to <a href="https://console.cloud.google.com/cloud-build">Cloud Build</a> in the console and click “Enable Cloud Build API”.</li>
<li>Navigate to <a href="https://console.cloud.google.com/run">Cloud Run</a> in the console and click “Start Using Cloud Run”.</li>
</ul>
<p>You’ll also need the <a href="https://cloud.google.com/sdk/">Google Cloud SDK</a> installed. Set the default project as follows (substituting your project ID for <code class="language-plaintext highlighter-rouge">$MY_PROJECT_ID</code>)</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>gcloud config <span class="nb">set </span>project <span class="nv">$MY_PROJECT_ID</span></code></pre></figure>
<h4 id="building-in-the-cloud">Building in the cloud</h4>
<p>You could push a local image to the cloud in preparation to deploy to Cloud Run, but it is easier and safer to build in the cloud. We’ll create a simple configuration to build your site image in <a href="https://cloud.google.com/cloud-build/">Google Cloud Build</a>.</p>
<p>Create a file <code class="language-plaintext highlighter-rouge">_build/cloudbuild.yaml</code>. Copy the following into it:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">gcr.io/cloud-builders/docker'</span>
<span class="na">args</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">build'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">--no-cache'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">--pull'</span><span class="pi">,</span>
<span class="s1">'</span><span class="s">--file'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">_build/Dockerfile'</span><span class="pi">,</span>
<span class="s1">'</span><span class="s">--tag'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">$_IMAGE'</span><span class="pi">,</span>
<span class="s1">'</span><span class="s">.'</span><span class="pi">]</span>
<span class="na">images</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">$_IMAGE'</span></code></pre></figure>
<p>This configuration performs a Docker build using the <code class="language-plaintext highlighter-rouge">_build/Dockerfile</code> you created, and tags it with an image name that you need to specify when you invoke the build.</p>
<p>Move back into the root directory for your Jekyll site, and build using this command (substituting your project ID for <code class="language-plaintext highlighter-rouge">$MY_PROJECT_ID</code>).</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span><span class="nb">cd</span> /path/to/mysite
<span class="nv">$ </span>gcloud builds submit <span class="nt">--config</span> _build/cloudbuild.yaml <span class="se">\</span>
<span class="nt">--substitutions</span> <span class="nv">_IMAGE</span><span class="o">=</span>gcr.io/<span class="nv">$MY_PROJECT_ID</span>/mysite:v1</code></pre></figure>
<p>This will build your site’s Docker image, and upload it to <a href="https://cloud.google.com/container-registry/">Google Container Registry</a>. If you like, you can view your images by visiting your <a href="https://console.cloud.google.com/gcr">container registry</a> in the cloud console. You can also view your <a href="https://console.cloud.google.com/cloud-build/builds">build results and logs</a> in the console.</p>
<p>The image name you provide as the value of <code class="language-plaintext highlighter-rouge">_IMAGE</code> should be of the form <code class="language-plaintext highlighter-rouge">gcr.io/$MY_PROJECT_ID/$SITE_NAME:$TAG</code> in order to upload to Google Container Registry. In that name, <code class="language-plaintext highlighter-rouge">$MY_PROJECT_ID</code> must be your project ID. <code class="language-plaintext highlighter-rouge">$SITE_NAME</code> should be some identifying name for this image. (We used <code class="language-plaintext highlighter-rouge">mysite</code> in this case.) <code class="language-plaintext highlighter-rouge">$TAG</code> should be a name for this build. (We used <code class="language-plaintext highlighter-rouge">v1</code> here, but you might consider using a timestamp, git hash, or other system of generating build IDs.)</p>
<h4 id="deploying-to-cloud-run-1">Deploying to Cloud Run</h4>
<p>Now, to deploy to Cloud run, type this command (substituting your project ID for <code class="language-plaintext highlighter-rouge">$MY_PROJECT_ID</code>).</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>gcloud beta run deploy mysite <span class="se">\</span>
<span class="nt">--platform</span> managed <span class="nt">--region</span> us-central1 <span class="se">\</span>
<span class="nt">--image</span> gcr.io/<span class="nv">$MY_PROJECT_ID</span>/mysite:v1 <span class="se">\</span>
<span class="nt">--allow-unauthenticated</span> <span class="nt">--concurrency</span> 80</code></pre></figure>
<p>Unless you have a very large site, this should take only a few seconds. When deployment is done, the command will output your site’s URL. It will look something like <code class="language-plaintext highlighter-rouge">https://mysite-somecode.a.run.app</code>. Open that URL in your web browser to view your site.</p>
<p>If this is a real site, you’ll probably want to point your own domain at it. You can do this in the console. Go to the <a href="https://console.cloud.google.com/run">Cloud Run section</a> and click “Manage custom domains”.</p>
<h4 id="deploying-updates">Deploying updates</h4>
<p>When you have updates, you should repeat the build and run steps above. I recommend using a different <code class="language-plaintext highlighter-rouge">$TAG</code> for each update. This will let you identify each build uniquely, making it easy to roll your site forward and back as needed.</p>
<p>For example, our initial deployment used a tag of <code class="language-plaintext highlighter-rouge">v1</code>. Make some edits to your site (maybe add or edit a post), and test it out locally using <code class="language-plaintext highlighter-rouge">jekyll serve</code>. Then, let’s build <code class="language-plaintext highlighter-rouge">v2</code>:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>gcloud builds submit <span class="nt">--config</span> _build/cloudbuild.yaml <span class="se">\</span>
<span class="nt">--substitutions</span> <span class="nv">_IMAGE</span><span class="o">=</span>gcr.io/<span class="nv">$MY_PROJECT_ID</span>/mysite:v2
<span class="nv">$ </span>gcloud beta run deploy mysite <span class="se">\</span>
<span class="nt">--platform</span> managed <span class="nt">--region</span> us-central1 <span class="se">\</span>
<span class="nt">--image</span> gcr.io/<span class="nv">$MY_PROJECT_ID</span>/mysite:v2 <span class="se">\</span>
<span class="nt">--allow-unauthenticated</span> <span class="nt">--concurrency</span> 80</code></pre></figure>
<p>As you work with your site, you’ll probably want to create a rake task or similar script to generate new tags and automate those commands. I use <a href="https://github.com/dazuma/toys">Toys</a> for this purpose, and my <code class="language-plaintext highlighter-rouge">.toys.rb</code> file looks something like this:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="no">LOCAL_IMAGE</span> <span class="o">=</span> <span class="s2">"mysite"</span>
<span class="no">PROJECT</span> <span class="o">=</span> <span class="s2">"my-project-id"</span>
<span class="no">SERVICE</span> <span class="o">=</span> <span class="s2">"mysite"</span>
<span class="n">tool</span> <span class="s2">"run-local"</span> <span class="k">do</span>
<span class="n">flag</span> <span class="ss">:no_cache</span>
<span class="kp">include</span> <span class="ss">:exec</span><span class="p">,</span> <span class="ss">exit_on_nonzero_status: </span><span class="kp">true</span>
<span class="k">def</span> <span class="nf">run</span>
<span class="n">cache_args</span> <span class="o">=</span> <span class="n">no_cache</span> <span class="p">?</span> <span class="p">[</span><span class="s2">"--pull"</span><span class="p">,</span> <span class="s2">"--no-cache"</span><span class="p">]</span> <span class="p">:</span> <span class="p">[]</span>
<span class="nb">exec</span> <span class="p">[</span><span class="s2">"docker"</span><span class="p">,</span> <span class="s2">"build"</span><span class="p">]</span> <span class="o">+</span> <span class="n">cache_args</span> <span class="o">+</span>
<span class="p">[</span><span class="s2">"-t"</span><span class="p">,</span> <span class="no">LOCAL_IMAGE</span><span class="p">,</span> <span class="s2">"-f"</span><span class="p">,</span> <span class="s2">"_build/Dockerfile"</span><span class="p">,</span> <span class="s2">"."</span><span class="p">]</span>
<span class="nb">puts</span> <span class="s2">"Running on http://localhost:8080"</span>
<span class="nb">exec</span> <span class="p">[</span><span class="s2">"docker"</span><span class="p">,</span> <span class="s2">"run"</span><span class="p">,</span> <span class="s2">"--rm"</span><span class="p">,</span> <span class="s2">"-it"</span><span class="p">,</span> <span class="s2">"-p"</span><span class="p">,</span> <span class="s2">"8080:8080"</span><span class="p">,</span> <span class="no">LOCAL_IMAGE</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">tool</span> <span class="s2">"deploy"</span> <span class="k">do</span>
<span class="n">flag</span> <span class="ss">:tag</span><span class="p">,</span> <span class="ss">default: </span><span class="no">Time</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="s2">"%Y-%m-%d-%H%M%S"</span><span class="p">)</span>
<span class="kp">include</span> <span class="ss">:exec</span><span class="p">,</span> <span class="ss">exit_on_nonzero_status: </span><span class="kp">true</span>
<span class="k">def</span> <span class="nf">run</span>
<span class="n">image</span> <span class="o">=</span> <span class="s2">"gcr.io/</span><span class="si">#{</span><span class="no">PROJECT</span><span class="si">}</span><span class="s2">/</span><span class="si">#{</span><span class="no">SERVICE</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">tag</span><span class="si">}</span><span class="s2">"</span>
<span class="nb">exec</span> <span class="p">[</span><span class="s2">"gcloud"</span><span class="p">,</span> <span class="s2">"builds"</span><span class="p">,</span> <span class="s2">"submit"</span><span class="p">,</span> <span class="s2">"--project"</span><span class="p">,</span> <span class="no">PROJECT</span><span class="p">,</span>
<span class="s2">"--config"</span><span class="p">,</span> <span class="s2">"_build/cloudbuild.yaml"</span><span class="p">,</span>
<span class="s2">"--substitutions"</span><span class="p">,</span> <span class="s2">"_IMAGE=</span><span class="si">#{</span><span class="n">image</span><span class="si">}</span><span class="s2">"</span><span class="p">]</span>
<span class="nb">exec</span> <span class="p">[</span><span class="s2">"gcloud"</span><span class="p">,</span> <span class="s2">"beta"</span><span class="p">,</span> <span class="s2">"run"</span><span class="p">,</span> <span class="s2">"deploy"</span><span class="p">,</span> <span class="no">SERVICE</span><span class="p">,</span>
<span class="s2">"--project"</span><span class="p">,</span> <span class="no">PROJECT</span><span class="p">,</span> <span class="s2">"--platform"</span><span class="p">,</span> <span class="s2">"managed"</span><span class="p">,</span>
<span class="s2">"--region"</span><span class="p">,</span> <span class="s2">"us-central1"</span><span class="p">,</span> <span class="s2">"--allow-unauthenticated"</span><span class="p">,</span>
<span class="s2">"--image"</span><span class="p">,</span> <span class="n">image</span><span class="p">,</span> <span class="s2">"--concurrency"</span><span class="p">,</span> <span class="s2">"80"</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<h3 id="cleaning-up">Cleaning up</h3>
<p>When you’re done with this tutorial, if you do not want to continue serving your site, you can delete the project so you do not incur any expenses related to it.</p>
<p>Note that deleting the project will delete all resources related to it, including any VMs, databases, storage, and networking resources. If you are still using some of the project’s resources and do not want to delete the entire project, you can clean up after this tutorial by doing this:</p>
<ul>
<li>Go to the <a href="https://console.cloud.google.com/run">Cloud Run console</a> and delete the service you deployed for your site. (It might be called “mysite”.)</li>
<li>Go to your <a href="https://console.cloud.google.com/gcr">container registry</a> in the console and delete the images for your site.</li>
</ul>
<h2 id="where-to-go-from-here">Where to go from here</h2>
<p>The Cloud Run documentation includes a lot of <a href="https://cloud.google.com/run/docs/how-to">additional information</a> about deploying and running your site. Of particular interest are:</p>
<ul>
<li><a href="https://cloud.google.com/run/docs/mapping-custom-domains">How to map a custom domain</a></li>
<li><a href="https://cloud.google.com/run/docs/continuous-deployment">Setting up continuous deployment from Cloud Build</a> and <a href="https://cloud.google.com/cloud-build/docs/running-builds/automate-builds">triggering on pushes to GitHub</a></li>
<li><a href="https://cloud.google.com/run/docs/monitoring">Monitoring your site</a></li>
<li><a href="https://cloud.google.com/run/pricing">Pricing</a> and <a href="https://cloud.google.com/run/quotas">quotas</a>.</li>
</ul>
<p>For more information about the runtime environment, or if you want to customize your container further, you should refer to the <a href="https://cloud.google.com/run/docs/reference/container-contract">container runtime contract</a>.</p>
<p>There’s a lot of hype around serverless these days, and not all of it is justified. For me, Cloud Run is the first product I’ve actually been somewhat excited about, because of how it successfully fuses serverless with containers, and because of how well it works for common use cases such as static sites.</p>
2019-07-01T00:00:00+00:00https://daniel-azuma.com/articles/talks/rubyconf-2018Yes, you should provide a client library for your API! (RubyConf 2018)2018-11-13T00:00:00+00:00Daniel Azumadazuma@gmail.comhttp://daniel-azuma.com/<p>On Nov 13, 2018, I spoke on "Yes, you should provide a client library for your API!" at RubyConf in Los Angeles, CA. The talk discussed the benefits of providing client libraries for HTTP-based APIs, and some techniques for writing them.</p>
<p>Here’s a small list of resources that I either mentioned in the talk or are closely related. Feel free to share more in the comments!</p>
<h2 id="talk-resources">Talk resources</h2>
<ul>
<li><a href="https://confreaks.tv/videos/rubyconf2018-yes-you-should-provide-a-client-library-for-your-api">Video on Confreaks</a></li>
<li><a href="https://speakerdeck.com/dazuma/yes-you-should-provide-a-client-library-for-your-api">Slides on SpeakerDeck</a></li>
</ul>
<h2 id="code-generation-frameworks">Code generation frameworks</h2>
<ul>
<li><a href="https://openapis.org">OpenAPI</a> is an open standard that works well for REST APIs.</li>
<li><a href="https://grpc.io">gRPC</a> is a high-performance RPC framework originally developed by Google, that uses HTTP/2 and protocol buffers.</li>
<li><a href="https://thrift.apache.org/">Apache Thrift</a> is an RPC framework originally developed by Facebook.</li>
</ul>
<h3 id="resources-related-to-openapi">Resources related to OpenAPI</h3>
<ul>
<li><a href="https://yos.io/2018/02/11/schema-first-api-design/">Schema-first API Design</a> is a great article for getting oriented with OpenAPI.</li>
<li><a href="https://www.blazemeter.com/blog/how-to-generate-openapi-definitions-from-code">Generating OpenAPI From Code</a></li>
<li><a href="https://swagger.io/tools/open-source/open-source-integrations/">Swagger integration libraries</a></li>
<li><a href="https://swagger.io/tools/swagger-editor/">Swagger editor</a> for editing OpenAPI spec.</li>
</ul>
<h3 id="resources-related-to-grpc">Resources related to gRPC</h3>
<ul>
<li><a href="https://developers.google.com/protocol-buffers/">Protocol buffers</a> is the structured data serialization mechanism used by gRPC.</li>
<li><a href="https://www.cncf.io/blog/2018/07/03/http-2-smarter-at-scale/">HTTP/2: Smarter at Scale</a> is a useful article describing HTTP/2 which underlies gRPC.</li>
<li><a href="https://www.cncf.io/blog/2018/08/31/grpc-on-http-2-engineering-a-robust-high-performance-protocol/">gRPC on HTTP/2: Engineering a Robust High Performance Protocol</a> is another article useful for understanding gRPC.</li>
</ul>
<h3 id="related-resources">Related resources</h3>
<ul>
<li><a href="https://schd.ws/hosted_files/apistrat18/b2/APIStrat-presentation-joe-levy-david-justice.pdf">Slide deck discussing how Oracle and Azure manage APIs</a></li>
<li><a href="https://graphql.org/learn/">GraphQL</a> is a data schema system, similar in some respects but more limited in scope. You might find it useful in conjunction with a full API specification framework.</li>
<li><a href="https://opencensus.io/">OpenCensus</a> is an open framework for collecting and reporting instrumentation information.</li>
</ul>
<h2 id="other-api-design-resources">Other API design resources</h2>
<ul>
<li><a href="https://cloud.google.com/apis/design/">Google’s API design guide</a> is public, and describes Google’s API design principles, some of which are specific to Google, but most of which should apply broadly to APIs in general.</li>
</ul>
<h2 id="thanks">Thanks</h2>
<p>Thanks to Jeff who did a bunch of preliminary research and presented an earlier version of this talk at CodeBEAM, and to Graham for reviewing and providing suggestions. Thanks to Google for sponsoring my appearance at RubyConf this year.</p>
2018-11-13T00:00:00+00:00