<?xml version="1.0" encoding="utf-8"?><rss version="2.0"
    xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:wfw="http://wellformedweb.org/CommentAPI/"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:atom="http://www.w3.org/2005/Atom"
    xmlns:media="http://search.yahoo.com/mrss/"
    xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
    xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
>
<channel>
  <title>Srijan Choudhary, all posts tagged: cloud</title>
  <link>https://srijan.ch/feed/all/tag:cloud</link>
  <lastBuildDate>Sun, 27 Nov 2022 19:30:00 +0000</lastBuildDate>
  <image>
    <url>https://srijan.ch/assets/favicon/favicon-32x32.png</url>
    <title>Srijan Choudhary, all posts tagged: cloud</title>
    <link>https://srijan.ch/feed/all/tag:cloud</link>
  </image>
  <sy:updatePeriod>daily</sy:updatePeriod>
  <sy:updateFrequency>1</sy:updateFrequency>
  <generator>Kirby</generator>
  <atom:link href="https://srijan.ch/feed/all.xml/tag:cloud" rel="self" type="application/rss+xml" />
  <description>Srijan Choudhary&#039;s Articles and Notes Feed for tag: cloud</description>
  <item>
    <title>Download a file securely from GCS on an untrusted system</title>
    <description><![CDATA[Download files from google cloud storage using temporary credentials or time-limited access URLs]]></description>
    <link>https://srijan.ch/secure-gcs-download</link>
    <guid isPermaLink="false">632920ea8948d20001269e4e</guid>
    <category><![CDATA[cloud]]></category>
    <category><![CDATA[security]]></category>
    <category><![CDATA[devops]]></category>
    <dc:creator>Srijan Choudhary</dc:creator>
    <pubDate>Sun, 27 Nov 2022 19:30:00 +0000</pubDate>
    <content:encoded><![CDATA[<h2>The Problem</h2>
<p>We publish some of our build artifacts to <a href="https://cloud.google.com/storage" rel="noreferrer">Google Cloud Storage</a>,
 and users need to download these to the target installation system. 
But, this target system is not always trusted and can have shared local 
users, so we don't want to store long-lived credentials.</p> <p>As a 
user, I can download the artifact on my (secure) laptop and transfer it 
to the target system. But, the artifact can be large (several GBs). So, 
downloading and uploading again makes it cumbersome and slow.</p><h2>Option 1: use <a href="https://cloud.google.com/sdk/docs/install" rel="noreferrer">gcloud CLI</a> on the target system</h2>
<p>Log in to the target system, install gcloud CLI, authenticate, and then download the file:</p><figure>
  <pre><code class="language-shellsession">$ gcloud storage cp gs://$BUCKET/$FILE ./</code></pre>
  </figure>
<p>This has two problems:</p><ol><li>The user must install (and maybe update) gcloud CLI on the target system.</li><li>The
 user needs to store their credentials on the target system. These 
credentials have full access to whatever resources the user has. So, 
it's a huge security risk, especially if we don't trust the target 
system.</li></ol><p>To mitigate (2), the user can log out of gcloud CLI after downloading. But, this is a manual step they might miss.</p><h2>Option 2: use gcloud CLI with a service account</h2>
<p>This
 is a variation of the above solution - we log in using a service 
account instead of the user account. This service account can have 
restricted access to only the resources needed.</p><figure>
  <pre><code class="language-shellsession">$ gcloud iam service-accounts create $SA_NAME \
    --description=&quot;Service Account for downloading artifacts&quot;
$ gsutil iam ch \
    serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com:roles/storage.objectViewer \
    gs://$BUCKET</code></pre>
  </figure>
<p>This partially mitigates problem (2) above. If 
the user forgets to log out of gcloud CLI, the damage will be restricted
 to the resources accessible by the service account.</p><h2>Option 3: Short-lived access token</h2>
<p>Gcloud CLI supports creating short-lived credentials for the end-user account or <a href="https://cloud.google.com/iam/docs/create-short-lived-credentials-direct" rel="noreferrer">any service account</a>.</p> <p>This credential can be used to download the artifact using wget with an authorization header - no need to install gcloud CLI.</p> <p>Here's
 a small script that asks for the auth token as input, parses various 
GCS bucket URL formats, and downloads the requested artifact directly 
using wget:</p><figure>
  <pre><code class="language-bash">#!/bin/bash
# Download artifact from GCS bucket

set -e

echo -e &quot;====&gt; Run \`gcloud auth print-access-token\` on a system where you&#039;ve setup gcloud to get access token\n&quot;
read -r -p &quot;Enter access token: &quot; StorageAccessToken
read -r -p &quot;Enter GCS artifact URL: &quot; ArtifactURL

if [[ &quot;${ArtifactURL:0:33}&quot; == &quot;https://console.cloud.google.com/&quot; ]]; then
    BucketAndFile=&quot;${ArtifactURL#*https://console.cloud.google.com/storage/browser/_details/}&quot;
elif [[ &quot;${ArtifactURL:0:33}&quot; == &quot;https://storage.cloud.google.com/&quot; ]]; then
    BucketAndFile=&quot;${ArtifactURL#*https://storage.cloud.google.com/}&quot;
elif [[ &quot;${ArtifactURL:0:5}&quot; == &quot;gs://&quot; ]]; then
    BucketAndFile=&quot;${ArtifactURL#*gs://}&quot;
else
    echo &quot;Invalid GCS artifact URL&quot;
    exit 1
fi

StorageBucket=&quot;${BucketAndFile%%/*}&quot;
StorageFile=&quot;${BucketAndFile#*/}&quot;
StorageFileEscaped=$(echo &quot;${StorageFile}&quot; | sed &#039;s/\//%2F/g&#039;)
OutputFileName=&quot;${StorageFile##*/}&quot;

echo -e &quot;\n====&gt; Downloading gs://${StorageBucket}/${StorageFile} to ${OutputFileName}\n&quot;

wget -O &quot;${OutputFileName}&quot; --header=&quot;Authorization: Bearer ${StorageAccessToken}&quot; \
    &quot;https://storage.googleapis.com/storage/v1/b/${StorageBucket}/o/${StorageFileEscaped}?alt=media&quot;</code></pre>
  </figure>
<h2>Option 4: Signed URLs</h2>
<p>Google Cloud Storage also supports <a href="https://cloud.google.com/storage/docs/access-control/signed-urls" rel="noreferrer">signed URLs</a>
 - which give time-limited access to a specific Cloud Storage resource. 
Anyone possessing the signed URL can use it while it's active without 
any further credentials. This fits our use case brilliantly.</p> <p>To do this, first we need to give ourselves the <code>iam.serviceAccountTokenCreator</code> role so that we can impersonate a service account.</p><figure>
  <pre><code class="language-shellsession">$ gcloud iam service-accounts add-iam-policy-binding \
	$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com \
    --member=$MY_EMAIL \
    --role=roles/iam.serviceAccountTokenCreator</code></pre>
  </figure>
<p>Then, we can generate a signed URL:</p><figure>
  <pre><code class="language-shellsession">$ gcloud config set auth/impersonate_service_account \
    $SA_NAME@$PROJECT_ID.iam.gserviceaccount.com

$ gsutil signurl -u -r $REGION -d 10m gs://$BUCKET/$FILE

$ gcloud config unset auth/impersonate_service_account</code></pre>
  </figure>
<p>And we can use wget to download the artifact from this URL without any further authentication.</p>]]></content:encoded>
    <comments>https://srijan.ch/secure-gcs-download#comments</comments>
    <slash:comments>0</slash:comments>
  </item><item>
    <title>Slackbot using google cloud serverless functions</title>
    <description><![CDATA[Slack bot using Google Cloud Functions to post a roundup of recently created channels]]></description>
    <link>https://srijan.ch/slackbot-google-cloud-part-1</link>
    <guid isPermaLink="false">634164f0219ca50001581813</guid>
    <category><![CDATA[development]]></category>
    <category><![CDATA[cloud]]></category>
    <category><![CDATA[slack]]></category>
    <dc:creator>Srijan Choudhary</dc:creator>
    <pubDate>Fri, 04 Nov 2022 19:15:00 +0000</pubDate>
    <media:content url="https://srijan.ch/media/pages/blog/slackbot-google-cloud-part-1/a7a4e31f92-1699621096/screenshot_20221009_161507.png" medium="image" />
    <content:encoded><![CDATA[<p>At my org, we wanted a simple Slack bot that posts a roundup 
of new channels created recently in the workspace to a channel. While 
writing this is easy enough, I wanted to do it using <a href="https://cloud.google.com/functions" rel="noreferrer">Google Cloud Functions</a> with Python, trying to follow best practices as much as possible.</p> <p>Here's how the overall flow will look like:</p><figure data-ratio="auto">
    <img src="https://srijan.ch/media/pages/blog/slackbot-google-cloud-part-1/1fc9e55238-1699621096/slackbot01.5.excalidraw.png" alt="">
  
    <figcaption class="text-center">
    Google Cloud Functions Slack Bot  </figcaption>
  </figure>
<p>We want this roundup post triggered on some schedule (maybe daily), so the <a href="https://cloud.google.com/scheduler" rel="noreferrer">Cloud Scheduler</a> is required to send an event to a <a href="https://cloud.google.com/pubsub" rel="noreferrer">Google Pub/Sub</a>
 topic that triggers our cloud function, which queries the slack API to 
get channels details, filter recently created, and post it back to a 
slack channel. <a href="https://cloud.google.com/secret-manager" rel="noreferrer">Secret Manager</a> is used to securely store slack's bot token and signing secret.</p> <p>Note that the credentials shown in any screenshots below are not valid.</p><h2>Create the slack app</h2>
<p>The
 first step will be to create the slack app. Go to https://api.slack.com
 and click on "Create an app". Choose "From scratch" in the first 
dialog; enter an app name and choose a workspace for your app in the 
second dialog. In the next screen, copy the "<strong>Signing Secret</strong>" from the "App Credentials" section and save it for later use.</p><figure data-ratio="auto">
  <ul>
        <li>
      <img alt="" src="https://srijan.ch/media/pages/blog/slackbot-google-cloud-part-1/a7a4e31f92-1699621096/screenshot_20221009_161507.png">    </li>
        <li>
      <img alt="" src="https://srijan.ch/media/pages/blog/slackbot-google-cloud-part-1/d075d74c25-1699621096/screenshot_20221009_161622-1.png">    </li>
        <li>
      <img alt="" src="https://srijan.ch/media/pages/blog/slackbot-google-cloud-part-1/e0500cfbdf-1699621096/screenshot_20221009_162905.png">    </li>
      </ul>
  </figure>
<p>Next,
 go to the "OAuth and Permissions" tab from the left sidebar, and scroll
 down to "Scopes" -&gt; "Bot Token Scopes". Here, add the scopes:</p><ul><li><a href="https://api.slack.com/scopes/channels:read" rel="noopener noreferrer"><code>channels:read</code></a>: required to query public channels and find their creation times</li><li><a href="https://api.slack.com/scopes/chat:write" rel="noopener noreferrer"><code>chat:write</code></a>: required to write to a channel (where the bot is invited)</li></ul><figure data-ratio="auto">
    <img src="https://srijan.ch/media/pages/blog/slackbot-google-cloud-part-1/0cf73d1181-1699621096/screenshot_20221009_162243.png" alt="">
  
  </figure>
<p>Next,
 scroll up on the same screen and click "Install to Workspace" to 
install to your workspace. Click "Allow" in the next screen to allow the
 installation. Next, copy the "<strong>Bot User OAuth Token</strong>" from the "OAuth Tokens for Your Workspace" section on the same page and save it for later use.</p><figure data-ratio="auto">
  <ul>
        <li>
      <img alt="" src="https://srijan.ch/media/pages/blog/slackbot-google-cloud-part-1/a79785c4f6-1699621096/screenshot_20221009_162420.png">    </li>
        <li>
      <img alt="" src="https://srijan.ch/media/pages/blog/slackbot-google-cloud-part-1/d075d74c25-1699621096/screenshot_20221009_161622-1.png">    </li>
        <li>
      <img alt="" src="https://srijan.ch/media/pages/blog/slackbot-google-cloud-part-1/f0e359ac2e-1699621096/screenshot_20221009_163221.png">    </li>
      </ul>
  </figure>
<p>💡Keep track of the <strong>Bot User OAuth Token</strong> and <strong>Signing Secret</strong> you copied above.</p><h2>Post to a Slack channel from a Google Cloud Function</h2>
<p>Next, we will try to use the credentials copied above to enable a Google Cloud Function to send a message to a Slack channel.</p><h3>Google Cloud Basic Setup</h3>
<p>We will use gcloud cli for the following sections, so <a href="https://cloud.google.com/sdk/docs/install" rel="noreferrer">install</a> and <a href="https://cloud.google.com/sdk/docs/initializing" rel="noreferrer">initialize</a> the Google Cloud CLI if not done yet. If you already have gcloud cli, run <code>gcloud components update</code> to update it to the latest version.</p> <p>Create
 a new project for this if required, or choose an existing project, set 
it as default, and export the project id as a shell environment for 
using later. Also export the region you want to use.</p><figure>
  <pre><code class="language-shell">export PROJECT_ID=slackbot-project
export REGION=us-central1

gcloud config set project ${PROJECT_ID}</code></pre>
  </figure>
<p>You will have to enable billing for this project to be able to use some of the functionality we require.</p> <p>You
 may also have to enable the Secret Manager, Cloud Functions, Cloud 
Build, Artifact Registry, and Logging APIs if this is the first time 
you're using Functions in this project. Note that some services like 
Secret Manager need billing to be setup before they can be enabled.</p><figure>
  <pre><code class="language-shell">gcloud services enable --project slackbot-project \
        secretmanager.googleapis.com \
        cloudfunctions.googleapis.com \
        cloudbuild.googleapis.com \
        artifactregistry.googleapis.com \
        logging.googleapis.com</code></pre>
  </figure>
<h3>Create a service account</h3>
<p>By default, Cloud Functions uses a <a href="https://cloud.google.com/functions/docs/securing/function-identity#runtime_service_account" rel="noreferrer">default service account</a> as its identity for function execution. These default service accounts have the <strong>Editor</strong>
 role, which allows them broad access to many Google Cloud services. Of 
course, this is not recommended for production, so we will create a new 
service account for this and <a href="https://cloud.google.com/iam/docs/understanding-service-accounts#granting_minimum" rel="noreferrer">grant it the minimum permissions</a> that it requires.</p><figure>
  <pre><code class="language-shell">SA_NAME=channelbot-sa
SA_EMAIL=${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com

gcloud iam service-accounts create ${SA_NAME} \
    --description=&quot;Service Account for ChannelBot slackbot&quot; \
    --display-name=&quot;ChannelBot SlackBot SA&quot;</code></pre>
  </figure>
<h3>Store secrets and give permissions to service account</h3>
<p>First, we need to store the secrets in Secret Manager.</p><figure>
  <pre><code class="language-shell">printf $SLACK_BOT_TOKEN | gcloud secrets create \
    channelbot-slack-bot-token --data-file=- \
    --project=${PROJECT_ID} \
    --replication-policy=user-managed \
    --locations=${REGION}

printf $SLACK_SIGNING_SECRET | gcloud secrets create \
    channelbot-slack-signing-secret --data-file=- \
    --project=${PROJECT_ID} \
    --replication-policy=user-managed \
    --locations=${REGION}</code></pre>
  </figure>
<p>And give our service account the <code><a href="https://cloud.google.com/secret-manager/docs/access-control#secretmanager.secretAccessor" rel="noreferrer">roles/secretmanager.secretAccessor</a></code> role on these secrets.</p><figure>
  <pre><code class="language-shell">gcloud secrets add-iam-policy-binding \
    projects/${PROJECT_ID}/secrets/channelbot-slack-bot-token \
    --member serviceAccount:${SA_EMAIL} \
    --role roles/secretmanager.secretAccessor

gcloud secrets add-iam-policy-binding \
    projects/${PROJECT_ID}/secrets/channelbot-slack-signing-secret \
    --member serviceAccount:${SA_EMAIL} \
    --role roles/secretmanager.secretAccessor</code></pre>
  </figure>
<h3>Create and deploy the function</h3>
<p>Here's a simple HTTP function that sends a message to slack on any HTTP call:</p><figure>
  <pre><code class="language-python">import functions_framework
from slack_bolt import App

# process_before_response must be True when running on FaaS
app = App(process_before_response=True)

print(&#039;Function has started&#039;)

@functions_framework.http
def send_to_slack(request):
    print(&#039;send_to_slack triggered&#039;)
    channel = &#039;#general&#039;
    text = &#039;Hello from Google Cloud Functions!&#039;
    app.client.chat_postMessage(channel=channel, text=text)
    return &#039;Sent to slack!&#039;</code></pre>
    <figcaption class="text-center">src-v1/main.py</figcaption>
  </figure>
<figure>
  <pre><code class="language-text">functions-framework
slack_bolt</code></pre>
    <figcaption class="text-center">src-v1/requirements.txt</figcaption>
  </figure>
<p>Assuming <code>main.py</code> and <code>requirements.txt</code> are present in <code>src-v1</code> folder, deploy using:</p><figure>
  <pre><code class="language-shell">gcloud beta functions deploy channelbot-send-to-slack \
    --gen2 \
    --runtime python310 \
    --project=${PROJECT_ID} \
    --service-account=${SA_EMAIL} \
    --source ./src-v1 \
    --entry-point send_to_slack \
    --trigger-http \
    --allow-unauthenticated \
    --region ${REGION} \
    --memory=128MiB \
    --min-instances=0 \
    --max-instances=1 \
    --set-secrets &#039;SLACK_BOT_TOKEN=channelbot-slack-bot-token:latest,SLACK_SIGNING_SECRET=channelbot-slack-signing-secret:latest&#039; \
    --timeout 60s</code></pre>
  </figure>
<p>💡We're using <code>--allow-unauthenticated</code> here just to test it out. It will be removed in later sections.</p><h3>Test it out</h3>
<p>Once the deployment is complete, we can view the function logs using:</p><figure>
  <pre><code class="language-shell">gcloud beta functions logs read channelbot-send-to-slack \
	--project ${PROJECT_ID} --gen2</code></pre>
  </figure>
<p>If everything was successful above, once of the recent log statements should say: <code>Function has started</code>.</p> <p>Next, add the bot to the <code>#general</code> channel using <code>/invite @ChannelBot</code> in the general channel on your slack workspace.</p> <p>Next, find the service endpoint using:</p><figure>
  <pre><code class="language-shell">gcloud functions describe channelbot-send-to-slack \
    --project ${PROJECT_ID} \
    --gen2 \
    --region ${REGION} \
    --format &quot;value(serviceConfig.uri)&quot;</code></pre>
  </figure>
<p>This will give a URL like <code>https://channelbot-send-to-slack-ga6Ofi9to0-uc.a.run.app</code>.</p> <p>To trigger the channel post, just do <code>curl ${SERVICE_URL}</code>. This should result in a test message from ChannelBot to the <code>#general</code> channel.</p><figure data-ratio="auto">
    <img src="https://srijan.ch/media/pages/blog/slackbot-google-cloud-part-1/27e2d0931e-1699621096/screenshot_20221018_235104.png" alt="">
  
    <figcaption class="text-center">
    ChannelBot message from Google Cloud Functions  </figcaption>
  </figure>
<h2>Trigger via Google Pub/Sub</h2>
<p>Now,
 instead of an unauthenticated HTTP trigger, we would like to trigger 
this via Google Pub/Sub. We would also like to pass the channel name and
 the message to post in the event.</p><h3>Google Pub/Sub basics</h3>
<p>Pub/Sub enables you to create systems of event producers and consumers, called <strong><strong>publishers</strong></strong> and <strong><strong>subscribers</strong></strong>. Publishers communicate with subscribers asynchronously by broadcasting events. Some core concepts:</p><ul><li><strong><strong>Topic.</strong></strong> A named resource to which messages are sent by publishers.</li><li><strong><strong>Subscription.</strong></strong>
 A named resource representing the stream of messages from a single, 
specific topic, to be delivered to the subscribing application.</li><li><strong><strong>Message.</strong></strong> The combination of data and (optional) attributes that a publisher sends to a topic and is eventually delivered to subscribers.</li><li><strong><strong>Publisher.</strong></strong> An application that creates and sends messages to a single or multiple topics.</li></ul><p>In
 this section, we will create a topic, create a subscription for our 
cloud function to listen to messages to that topic, and produce messages
 manually to that topic using <code>gcloud</code> cli. The message will 
contain the channel name and message to post, and the cloud function 
will post that message to the specified slack channel.</p><h3>Create pub/sub topic</h3>
<p>First, we need to create a topic.</p><figure>
  <pre><code class="language-shell">export PUBSUB_TOPIC=channelbot-pubsub
gcloud pubsub topics create ${PUBSUB_TOPIC} \
    --project ${PROJECT_ID}</code></pre>
  </figure>
<h3>Grant permissions to the service account</h3>
<p>Next, we need to give the <code>roles/pubsub.editor</code> role to the service account we're using for the function execution so that it can create a subscription to this pub/sub topic.</p><figure>
  <pre><code class="language-shell">gcloud pubsub topics add-iam-policy-binding ${PUBSUB_TOPIC} \
    --project ${PROJECT_ID} \
    --member serviceAccount:${SA_EMAIL} \
    --role roles/pubsub.editor</code></pre>
  </figure>
<h3>Update the function code</h3>
<p>Here's the <code>main.py</code> we'll need to listen to pub/sub events, extract <code>channel</code> and <code>text</code>, and sent it to slack:</p><figure>
  <pre><code class="language-python">import base64
import json
import functions_framework
from slack_bolt import App

# process_before_response must be True when running on FaaS
app = App(process_before_response=True)

print(&#039;Function has started&#039;)

# Triggered from a message on a Cloud Pub/Sub topic.
@functions_framework.cloud_event
def pubsub_handler(cloud_event):
    try:
        data = base64.b64decode(
            cloud_event.data[&quot;message&quot;][&quot;data&quot;]).decode()
        print(&quot;Received from pub/sub: %s&quot; % data)
        event_data = json.loads(data)
        channel = event_data[&quot;channel&quot;]
        text = event_data[&quot;text&quot;]
        app.client.chat_postMessage(channel=channel, text=text)
    except Exception as E:
        print(&quot;Error decoding message: %s&quot; % E)</code></pre>
    <figcaption class="text-center">src-v2/main.py</figcaption>
  </figure>
<p>Before deploying, we also need to enable the Eventarc API in this project.</p><figure>
  <pre><code class="language-shell">gcloud services enable --project ${PROJECT_ID} \
    eventarc.googleapis.com</code></pre>
  </figure>
<h3>Deploy and Test</h3>
<p>Now, there's a slightly modified version of the deploy command to deploy this:</p><figure>
  <pre><code class="language-shell">gcloud beta functions deploy channelbot-send-to-slack \
    --gen2 \
    --runtime python310 \
    --project ${PROJECT_ID} \
    --service-account ${SA_EMAIL} \
    --source ./src-v2 \
    --entry-point pubsub_handler \
    --trigger-topic ${PUBSUB_TOPIC} \
    --region ${REGION} \
    --memory 128MiB \
    --min-instances 0 \
    --max-instances 1 \
    --set-secrets &#039;SLACK_BOT_TOKEN=channelbot-slack-bot-token:latest,SLACK_SIGNING_SECRET=channelbot-slack-signing-secret:latest&#039; \
    --timeout 60s</code></pre>
  </figure>
<p>The main changes are:</p><ul><li>Changed entry-point to the new function <code>pubsub_handler</code></li><li>Replaced <code>--trigger-http</code> with <code>--trigger-topic</code></li><li>Removed <code>--allow-unauthenticated</code></li></ul><p>Before sending a pub/sub message, we will also need to give the <code>roles/run.invoker</code> role to our service account to be able to trigger our newly deployed function.</p><figure>
  <pre><code class="language-shell">gcloud run services add-iam-policy-binding channelbot-send-to-slack \
    --project ${PROJECT_ID} \
    --region ${REGION} \
    --member=serviceAccount:${SA_EMAIL} \
    --role=roles/run.invoker</code></pre>
  </figure>
<p>To test this out, we can send a pub/sub message using gcloud cli:</p><figure>
  <pre><code class="language-shell">gcloud pubsub topics publish ${PUBSUB_TOPIC} \
    --project ${PROJECT_ID} \
    --message &#039;{&quot;channel&quot;: &quot;#general&quot;, &quot;text&quot;: &quot;Hello from Cloud Pub/Sub!&quot;}&#039;</code></pre>
  </figure>
<figure data-ratio="auto">
    <img src="https://srijan.ch/media/pages/blog/slackbot-google-cloud-part-1/f184b7795b-1699621096/screenshot_20221104_232055.png" alt="">
  
    <figcaption class="text-center">
    ChannelBot message via pub/sub  </figcaption>
  </figure>
<h2>Post new channels roundup using cloud scheduler</h2>
<h3>Manually post recently created channels</h3>
<p>Now
 that we have gained the capability to trigger a message from pub/sub to
 slack, we can add some logic to fetch the recently created channels 
from slack and post it as a message on this trigger.</p> <p>Here's the modified <code>main.py</code> to do this:</p><figure>
  <pre><code class="language-python">import base64
import json
import time
import functions_framework
from slack_bolt import App

# process_before_response must be True when running on FaaS
app = App(process_before_response=True)

print(&#039;Function has started&#039;)

# Triggered from a message on a Cloud Pub/Sub topic.
@functions_framework.cloud_event
def pubsub_handler(cloud_event):
    try:
        data = base64.b64decode(
            cloud_event.data[&quot;message&quot;][&quot;data&quot;]).decode()
        print(&quot;Received from pub/sub: %s&quot; % data)
        event_data = json.loads(data)
        max_days = event_data[&quot;max_days&quot;] # Max age of channels
        channel = event_data[&quot;channel&quot;]
        recent_channels = get_recent_channels(app, max_days)
        if len(recent_channels) &gt; 0:
            blocks, text = format_channels(recent_channels, max_days)
            app.client.chat_postMessage(channel=channel, text=text,
                                        blocks=blocks)
        else:
            print(&quot;No recent channels&quot;)
    except Exception as E:
        print(&quot;Error decoding message: %s&quot; % E)


def get_recent_channels(app, max_days):
    max_age_s = max_days * 24 * 60 * 60
    result = app.client.conversations_list()
    all = result[&quot;channels&quot;]
    now = time.time()
    return [ c for c in all if (now - c[&quot;created&quot;] &lt;= max_age_s) ]

def format_channels(channels, max_days):
    text = (&quot;%s channels created in the last %s day(s):&quot; %
            (len(channels), max_days))
    blocks = [{
        &quot;type&quot;: &quot;header&quot;,
        &quot;text&quot;: {
            &quot;type&quot;: &quot;plain_text&quot;,
            &quot;text&quot;: text
        }
    }]
    summary = &quot;&quot;
    for c in channels:
        summary += &quot;\n*&lt;#%s&gt;*: %s&quot; % (c[&quot;id&quot;], c[&quot;purpose&quot;][&quot;value&quot;])
    blocks.append({
        &quot;type&quot;: &quot;section&quot;,
        &quot;text&quot;: {
            &quot;type&quot;: &quot;mrkdwn&quot;,
            &quot;text&quot;: summary
        }
    })
    return blocks, text</code></pre>
    <figcaption class="text-center">src-v3/main.py</figcaption>
  </figure>
<p>After deploying this with the same command above (just change <code>--source ./src-v2</code> to <code>--source ./src-v3</code>), we can send a pub/sub event to trigger it:</p><figure>
  <pre><code class="language-shell">gcloud pubsub topics publish ${PUBSUB_TOPIC} \
    --project ${PROJECT_ID} \
    --message &#039;{&quot;channel&quot;: &quot;#general&quot;, &quot;max_days&quot;: 7}&#039;</code></pre>
  </figure>
<p>And it posts a message like this:</p><figure data-ratio="auto">
    <img src="https://srijan.ch/media/pages/blog/slackbot-google-cloud-part-1/7e9ecc0ef4-1699621096/screenshot_20221104_235500.png" alt="">
  
    <figcaption class="text-center">
    Recently created channels posted by ChannelBot  </figcaption>
  </figure>
<h3>Create schedule</h3>
<p>Next,
 we want to periodically schedule this message. For this, we will 
configure a cron job in Google Cloud Scheduler to send a Pub/Sub event 
with the required parameters periodically.</p> <p>Before we create a schedule, we will have to enable the Cloud Scheduler API</p><figure>
  <pre><code class="language-shell">gcloud services enable --project ${PROJECT_ID} \
    cloudscheduler.googleapis.com</code></pre>
  </figure>
<p>To schedule the Pub/Sub trigger at 1600 hours UTC time every day:</p><figure>
  <pre><code class="language-shell">gcloud scheduler jobs create pubsub channelbot-job \
    --project ${PROJECT_ID} \
    --location ${REGION} \
    --schedule &quot;0 16 * * *&quot; \
    --time-zone &quot;UTC&quot; \
    --topic ${PUBSUB_TOPIC} \
    --message-body &#039;{&quot;channel&quot;: &quot;#general&quot;, &quot;max_days&quot;: 1}&#039;</code></pre>
  </figure>
<p>After this, a Pub/Sub event should be fired to the <code>channelbot-pubsub</code> topic every day, which should result in a slack message to <code>#general</code> with a list of channels created in the last day.</p><h2>Closing Thoughts</h2>
<p>Full code samples for this can be found in <a href="https://github.com/srijan/gcloud_slackbot" rel="noreferrer">this github repo</a>. I've also included a <code>Makefile</code> with targets split into sections associated with the different steps in this post.</p> <p>I
 also plan to follow this up with a part 2 where we will use slack's 
slash commands to allow the end-user of this bot to setup the channel 
and frequency of posting of the recent channels list, and even configure
 multiple schedules. Please comment below if this is something you will 
be interested in.</p>]]></content:encoded>
    <comments>https://srijan.ch/slackbot-google-cloud-part-1#comments</comments>
    <slash:comments>0</slash:comments>
  </item></channel>
</rss>
