<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Engineering at Premise - Medium]]></title>
        <description><![CDATA[Premise engineers close the information gap in a complex, evolving world. We empower millions around the globe to collect and share information with decision makers, providing them with Data for Every Decision™ - Medium]]></description>
        <link>https://engineering.premise.com?source=rss----c5fada0a103d---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Engineering at Premise - Medium</title>
            <link>https://engineering.premise.com?source=rss----c5fada0a103d---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 25 Jun 2026 15:02:53 GMT</lastBuildDate>
        <atom:link href="https://engineering.premise.com/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Configuring a Multi-Instance Looker Deployment]]></title>
            <link>https://engineering.premise.com/configuring-a-multi-instance-looker-deployment-0f1eec1b8e7a?source=rss----c5fada0a103d---4</link>
            <guid isPermaLink="false">https://medium.com/p/0f1eec1b8e7a</guid>
            <category><![CDATA[gcp]]></category>
            <category><![CDATA[business-intelligence]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[looker]]></category>
            <category><![CDATA[github]]></category>
            <dc:creator><![CDATA[Dennis Mutia ]]></dc:creator>
            <pubDate>Mon, 13 Nov 2023 15:00:28 GMT</pubDate>
            <atom:updated>2023-11-13T15:00:28.189Z</atom:updated>
            <content:encoded><![CDATA[<p><strong><em>By Dennis Mutia, Software Engineer</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GPW2eryxF-hl0dqrrIy6tQ.jpeg" /><figcaption>Image by <a href="https://www.istockphoto.com/portfolio/ismagilov?mediatype=photography">ismagilov</a> on Unsplash</figcaption></figure><p>This article provides a high level overview of how we have configured multiple <a href="https://cloud.google.com/looker-bi#:~:text=Resources%20to%20learn%20more%20about%20Looker%20for%20BI">Looker</a> deployments to separate development and production environments for both <a href="https://cloud.google.com/looker/docs/what-is-lookml">LookML</a> and dashboards. Having a Looker deployment on a single instance has many challenges which include:</p><ul><li>a dashboard can get edited when someone else is presenting it to a client or a stakeholder</li><li>someone testing a large dashboard can slow down Looker which will affect other dashboards</li><li>Looker users cannot test the effects of model changes to their dashboards without having to request for developer access which has cost implications</li></ul><h3>Multiple Looker instances</h3><p>We have set up 3 Looker instances: a development instance where LookML updates are made and dashboards created and reviewed before being released to production, and two production instances. One of the production instances is for internal looks and dashboards and the other for creating dashboards that can be shared with external clients.</p><p>All three Looker instances have a standardized data connection to read from the same data warehouse. This is to ensure that dashboards and looks work the same access all environments. To ensure all development actually occurs on the development instance, both production instances enforce view only access for all users.</p><p>This set up allows us to test both data models and dashboards without affecting production content. It also leads to standardized data models across all production instances.</p><h3>Linking development to production</h3><p>To take advantage of the benefits of multiple Looker instances, while limiting the operational maintance, we have linked all three looker instances using GitHub, and use GitHub Releases to update the production instances whenever changes have been tested and approved in development.</p><p>To release dashboards from development to production we use <a href="https://backstage.io/">Backstage</a>, which is also our developer portal. We have created a template which dashboard builders use to release ready-to-use content to production instances, but any type of trigger would work. We have also integrated with slack for notifications.</p><p>This setup can be achieved using the steps outlined below.</p><p><strong>Step 1</strong>: Create a new GitHub repository to host your LookML code.</p><p><strong>Step 2: </strong>Enable <a href="https://cloud.google.com/looker/docs/setting-up-git-connection">GitHub integration</a> in all Looker instances, and enable either <em>Pull Requests Recommended</em> or <em>Pull Requests Required</em> options. Steps to enable GitHub integration can be found in this <a href="https://cloud.google.com/looker/docs/setting-up-git-connection">link</a>. All instances should point to the repository created in step 1.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MzBsBt4fXuCn6EJkoxZQ5A.png" /></figure><p><strong>Step 3: </strong>Configure deploy webhooks in GitHub, one for each instance. These are used to deploy the head commit from the production branch after a push event or a release. The URLs take the format &lt;looker_instance_url&gt;webhooks/projects/&lt;looker_project_name&gt;/deploy. For example https://company.looker.com/webhooks/projects/test/deploy. Steps to add the URLs to the GitHub repo can be found <a href="https://cloud.google.com/looker/docs/git-options#:~:text=for%20more%20information.-,Setting%20up%20your%20project%20with%20integrated%20pull%20requests,-To%20set%20up">here</a>.</p><p>Our current set up is to trigger the development instance deploy URL using a <em>push event</em> and the production instances deploy URLs using <em>releases</em>. When triggered, these webhooks deploy to the production mode in the respective instances. To change the triger event go to the LookML repo &gt; Settings &gt; Webhooks and click on the webhook you want to modify.</p><h3>LookML and content development flow</h3><p>The setup above allows us to achieve the development flow below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*m3KnVdK0aLqYONUr4By_aw.png" /><figcaption>Image adapted from <a href="https://help.looker.com/hc/article_attachments/360101051313/dev-prod-workflow-202011.png">Google Cloud Community</a></figcaption></figure><p><strong>LookML development flow</strong></p><ol><li>A developer modifies LookML code and creates a PR</li><li>PR is reviewed and merged. This auto-deploys the changes to the production mode in the development instance using the deploy webhook for the developer instance. Looker users and dashboard builders can test the updated models in the development instance without requiring developer permissions.</li><li>Finally, a release is created in GitHub which auto-deploys to the production mode in the production instances.</li></ol><p><strong>Content development flow</strong></p><ol><li>A Looker user creates a new dashboard in the development instance and has it reviewed.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1IYLkNwZv_R-mei2pnqfdA.png" /></figure><p>2. The user makes a request in Backstage to release the dashboard to production. This request is forwarded to GitHub Actions which runs a Python script which uses <a href="https://github.com/looker-open-source/gzr">Gazer</a> to do the actual dashboard migration. Any trigger can be used in this step as long as the request is ultimately forwarded to Github Actions.</p><p>3. An alert is send to Slack on the status of the release, successful or failed. The message also includes a link to the released dashboard in the destination instance.</p><p>Below is a summary of the content development and release workflow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8medT_vO5j3Nsrwpl7Xnlg.png" /></figure><h3>Conclusion</h3><p>Looker is extremely powerful and extensive as a Business Intelligence platform. We hope this article is useful to anyone trying to connect multiple Looker deployments, or anyone trying to separate development and production environments.</p><p>We are interested in hearing about your current Looker multi-instance setup so feel free to let us know in the comments.</p><p><strong><em>Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings, data and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our </em></strong><a href="https://bit.ly/3wnTrLn"><strong><em>Careers</em></strong></a><strong><em> page.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0f1eec1b8e7a" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/configuring-a-multi-instance-looker-deployment-0f1eec1b8e7a">Configuring a Multi-Instance Looker Deployment</a> was originally published in <a href="https://engineering.premise.com">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Publishing Kotlin Multiplatform Swift Packages Using Google Cloud Storage and Cloud Run]]></title>
            <link>https://engineering.premise.com/publishing-kotlin-multiplatform-swift-packages-to-google-cloud-storage-be5c6987e5d?source=rss----c5fada0a103d---4</link>
            <guid isPermaLink="false">https://medium.com/p/be5c6987e5d</guid>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[google-cloud-run]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[kotlin]]></category>
            <dc:creator><![CDATA[Nate Ebel]]></dc:creator>
            <pubDate>Wed, 18 Oct 2023 05:28:28 GMT</pubDate>
            <atom:updated>2023-10-18T05:47:23.219Z</atom:updated>
            <content:encoded><![CDATA[<p><strong><em>By Nate Ebel, Android developer</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CDESlxjxJDwaBwAjIqIVdg.png" /></figure><p>In this post, we’ll detail our solution for publishing, and consuming, Kotlin Multiplatform Swift Packages. Our solution leverages a custom Gradle plugin publishing XCFrameworks to <a href="https://cloud.google.com/storage">Google Cloud Storage</a> and a <a href="https://cloud.google.com/run">Google Cloud Run</a> service to download <a href="https://developer.apple.com/documentation/xcode/swift-packages">Swift Package</a> binaries requested by XCode. With this solution in place, we’ve been able to more efficiently serve multiple Kotlin Multiplatform libraries to our iOS application.</p><p>This is a part of an ongoing series on our usage of Kotlin Multiplatform at Premise:</p><ul><li><strong>Part 1: </strong><a href="https://engineering.premise.com/kotlin-multiplatform-at-premise-b28d85825c9f"><strong>Kotlin Multiplatform at Premise</strong></a></li><li><a href="https://engineering.premise.com/kotlin-multiplatform-project-structure-brownfield-applications-58038800a6ea"><strong>Part 2: Kotlin Multiplatform Project Structure for Integrating with Brownfield Applications</strong></a></li><li><a href="https://engineering.premise.com/kotlin-multiplatforrm-github-actions-workflow-3e5e0fcb7081"><strong>Part 3: Building a CI Pipeline for Kotlin Multiplatform Mobile Using GitHub Actions</strong></a></li><li><strong>Part 4: Publishing Kotlin Multiplatform Swift Packages Using Google Cloud Storage and Cloud Run </strong>— This Post</li><li><strong>Part 5: Generating BuildConfig Files for a Kotlin Multiplatform Library</strong> — Coming Soon</li><li><strong>Part 6: Optimizing Local Build Times for Kotlin Multiplatform Mobile Projects</strong> — Coming Soon</li></ul><h3>Premise and Kotlin Multiplatform Swift Packages</h3><p>We’ve been <a href="https://engineering.premise.com/kotlin-multiplatform-at-premise-b28d85825c9f">using Kotlin Multiplatform in production</a> since early 2021 in the form of our mobile-shared project.</p><p>During that time, we’ve consumed our shared code as a Swift Package within our iOS application. The integration of that Swift Package has gone through several iterations.</p><ul><li>v1: Use the <a href="https://github.com/ge-org/multiplatform-swiftpackage"><em>multiplatform-swiftpackage</em> plugin</a> to build the Swift Package and store the XCFramework binary in GitHub</li><li>v2: Use our own custom Gradle plugin to build the Swift Package and store the XCFramework binary in GitHub</li></ul><p>These two solutions were very similar. Build the XCFramework. Generate the Package.swiftfile. Check both into git with the desired version tag.</p><p>These approaches worked fine for a while, but eventually we started to pay the price for our simple initial solution.</p><p>An XCFramework binary can be pretty large. Ours were in the ballpark of 100MB. So checking 2–3 of these into each commit (1 for each iOS architecture) started to really increase the overall download size of our git repo.</p><p>Why is this a problem?</p><p>Well, Swift Package Manager does a full clone of the git repository when it has to sync package dependencies or reset package caches. So, when our developers had to switch branches with difference library versions, or had to reset their Xcode caches, they had to download GBs worth of XCFramework binaries; thereby dramatically slowing down their Xcode sync.</p><p>Once we determined what was causing this slow down, we knew we needed a new approach. We decided to move to a <a href="https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html#target">remote url distribution model</a> for our Kotlin Multiplatform Swift Packages. Rather than check the binary into source control, we would upload the binary to some remote location, acquire the url to that resource, and add that url, along with the checksum, to our Package.swift file.</p><p>This is conceptually pretty simple. However, there wasn’t an ideal out-of-the-box solution at the time. So, we built a solution ourselves which has been serving us well in managing the publication and integration of Swift Packages for multiple Kotlin Multiplatform projects.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*U4C7axtkrAA_nhE56KKgGA.png" /></figure><p>The solution is broken down into three parts:</p><ul><li>Uploading XCFramework binaries to Google Cloud Storage</li><li>Publishing the Package.swift file to GitHub</li><li>Consuming the Swift Package from Xcode using Google Cloud Run</li></ul><h3>Uploading XCFramework Binaries to Google Cloud Storage</h3><p>As part of our Kotlin Multiplatform project, we have a custom Gradle plugin named SwiftPackagePlugin that adds several key tasks to our build.</p><pre>class SwiftPackagePlugin : Plugin&lt;Project&gt; {<br>    override fun apply(target: Project) {<br>        target.registerMoveXCFrameworkTask()<br>        target.registerZipFileTask()<br>        target.registerSwiftPackageTask()<br>        target.registerLocalSwiftPackageTask()<br>    }<br>}</pre><p>registerMoveXCFrameworkTask moves a build XCFramework from the build directory to our desired output directory location</p><pre>/**<br> * Creates a moveXCFramework Gradle task to move files from the<br> * /build/swiftpackage directory to a /swiftpackage output directory<br> */<br>private fun Project.registerMoveXCFrameworkTask() {<br>    tasks.register(&quot;moveXCFramework&quot;, Copy::class.java) {<br>        group = TASK_GROUP_NAME<br>        description = &quot;Moves XCFramework into swiftpackage output directory&quot;<br><br>        dependsOn(&quot;assemble${rootProject.name.capitalized()}XCFramework&quot;)<br><br>        from(&quot;${this@registerMoveXCFrameworkTask.buildDir}/$OUTPUT_FILE_DIR_NAME&quot;)<br>        into(File(OUTPUT_FILE_DIR_NAME))<br>    }<br>}</pre><p>registerZipFileTask creates a zip containing the build XCFrameworks</p><pre>/**<br> * Creates a zipXCFramework Gradle task<br> */<br>private fun Project.registerZipFileTask() {<br>    tasks.register(&quot;zipXCFramework&quot;, Zip::class.java) {<br>        group = TASK_GROUP_NAME<br>        description = &quot;Creates a ZIP file for the XCFramework&quot;<br><br>        // dependsOn assembleMobilesharedXCFramework task<br>        dependsOn(&quot;assemble${rootProject.name.capitalized()}XCFramework&quot;)<br><br>        archiveFileName.set(project.getZipFileName())<br>        destinationDirectory.set(File(&quot;${this@registerZipFileTask.buildDir}/$OUTPUT_FILE_DIR_NAME&quot;))<br>        from(&quot;${this@registerZipFileTask.buildDir}/XCFrameworks/release&quot;)<br>    }<br>}</pre><p>registerLocalSwiftPackageTask constructs a Package.swift file useful for locally testing changes in our iOS project</p><pre>/**<br> * Creates a createLocalSwiftPackage Gradle task that generates a<br> * Package.swift file needed for the generated XCFramework so it<br> * can be consumed as a local Swift Package<br> */<br>private fun Project.registerLocalSwiftPackageTask() {<br>    tasks.register(&quot;createLocalSwiftPackage&quot;) {<br>        group = TASK_GROUP_NAME<br>        description = &quot;Creates a local Swift package to distribute the XCFramework&quot;<br><br>        dependsOn(&quot;zipXCFramework&quot;)<br>        finalizedBy(&quot;moveXCFramework&quot;)<br><br>        doLast {<br>            val packageDotSwiftFile = createPackageDotSwiftFile(<br>                outputDirName = OUTPUT_FILE_DIR_NAME,<br>                filename = OUTPUT_PACKAGE_CONFIG_FILE_NAME<br>            )<br><br>            val packageConfiguration = SwiftPackageConfiguration.local(<br>                packageName = rootProject.name,<br>                zipFileName = project.getZipFileName()<br>            )<br><br>            packageDotSwiftFile.writePackageDotSwiftFile(packageConfiguration)<br>        }<br>    }<br>}</pre><p>registerSwiftPackageTask uploads a zipped set of XCFrameworks to Google Cloud Storage, builds a SwiftPackageConfigurationand then constructs a Package.swift file pointing to this remote file.</p><pre>/**<br> * Generates the Package.swift file needed for the generated<br> * XCFramework so it can be consumed as a Swift Package<br> */<br>private fun Project.registerSwiftPackageTask() {<br>    tasks.register(&quot;createSwiftPackage&quot;) {<br>        group = TASK_GROUP_NAME<br>        description = &quot;Creates the Swift package to distribute the XCFramework&quot;<br><br>        dependsOn(&quot;zipXCFramework&quot;)<br><br>        doLast {<br>            val packageDotSwiftFile = createPackageDotSwiftFile(<br>                outputDirName = OUTPUT_FILE_DIR_NAME,<br>                filename = OUTPUT_PACKAGE_CONFIG_FILE_NAME<br>            )<br><br>            uploadeToCloudStorage(<br>                projectId = project.swiftPackageGCPProjectId,<br>                bucketName = project.swiftPackageBucketName,<br>                packageName = project.getZipFileName(),<br>                filePath = &quot;${project.buildDir}/$OUTPUT_FILE_DIR_NAME/${project.getZipFileName()}&quot;<br>            )<br><br>            val packageConfiguration = SwiftPackageConfiguration.remote(<br>                packageName = rootProject.name,<br>                zipFileName = project.getZipFileName(),<br>                zipChecksum = project.zipFileChecksum().trim(),<br>                distributionUrl = project.swiftPackageDistributionURL<br>            )<br><br>            packageDotSwiftFile.writePackageDotSwiftFile(packageConfiguration)<br>        }<br>    }<br>}</pre><h4>Uploading to Google Cloud Storage</h4><p>The implementation of uploadToCloudStorage looks like the following. This relies on the com.google.cloud:google-cloud-storage:&lt;version&gt; dependency for interacting with Cloud Storage. By extension this also means the task must be run from an environment authenticated with Google Cloud. Locally, this generally means authentication via application-default credentials. In CI, we use the google-github-actions/auth GitHub Action to authenticate via a service account that has permissions to upload to our desired Cloud Storage bucket.</p><pre>/**<br> * Uploads a specified XCFramework zip file to a specified Cloud Storage bucket<br> *<br> * @param projectId The ID of your GCP project<br> * @param bucketName The Cloud Storage bucket, within the specific project, to upload zip files to<br> * @param packageName The name of the XCFramework zip file<br> * @param filePath Path to the file to upload<br> */<br>private fun uploadeToCloudStorage(projectId: String, bucketName: String, packageName: String, filePath: String) {<br>    val storage = StorageOptions.newBuilder().setProjectId(projectId).build().service<br>    val blobId: BlobId = BlobId.of(bucketName, packageName)<br>    val blobInfo: BlobInfo = BlobInfo.newBuilder(blobId).build()<br><br>    // Optional: set a generation-match precondition to avoid potential race<br>    // conditions and data corruptions. The request returns a 412 error if the<br>    // preconditions are not met.<br>    val precondition = if (storage[bucketName, packageName] == null) {<br>        // For a target object that does not yet exist, set the DoesNotExist precondition.<br>        // This will cause the request to fail if the object is created before the request runs.<br>        Storage.BlobWriteOption.doesNotExist()<br>    } else {<br>        // If the destination already exists in your bucket, instead set a generation-match<br>        // precondition. This will cause the request to fail if the existing object&#39;s generation<br>        // changes before the request runs.<br>        Storage.BlobWriteOption.generationMatch(<br>            storage[bucketName, packageName].generation<br>        )<br>    }<br><br>    storage.createFrom(blobInfo, Paths.get(filePath), precondition)<br>}</pre><h4>Building a Package.swift file</h4><p>To build the desired Package.swift file we construct an instance of a SwiftPackageConfiguration which will hold the values we want to write out to disk</p><pre>private data class SwiftPackageConfiguration private constructor(<br>    private val packageName: String,<br>    private val swiftToolsVersion: String,<br>    private val platforms: String,<br>    private val zipFileName: String,<br>    private val zipChecksum: String,<br>    private val isLocal: Boolean,<br>    private val distributionUrl: String,<br>) {<br><br>    val templateProperties = mapOf(<br>        &quot;toolsVersion&quot; to swiftToolsVersion,<br>        &quot;name&quot; to packageName,<br>        &quot;zipName&quot; to zipFileName,<br>        &quot;platforms&quot; to platforms,<br>        &quot;isLocal&quot; to isLocal,<br>        &quot;checksum&quot; to zipChecksum,<br>        &quot;url&quot; to &quot;$distributionUrl/$zipFileName&quot;<br>    )<br><br>    companion object {<br>        internal val templateFile =<br>            SwiftPackagePlugin::class.java.getResource(&quot;/templates/$OUTPUT_PACKAGE_CONFIG_FILE_NAME.template&quot;)<br><br>        fun local(<br>            packageName: String,<br>            swiftToolsVersion: String = DEFAULT_SWIFT_TOOLS_VERSION,<br>            platforms: String = DEFAULT_IOS_PLATFORM_VERSION,<br>            zipFileName: String<br>        ) = SwiftPackageConfiguration(packageName, swiftToolsVersion, platforms, zipFileName, &quot;&quot;, true, &quot;&quot;)<br><br>        fun remote(<br>            packageName: String,<br>            swiftToolsVersion: String = DEFAULT_SWIFT_TOOLS_VERSION,<br>            platforms: String = DEFAULT_IOS_PLATFORM_VERSION,<br>            zipFileName: String,<br>            zipChecksum: String,<br>            distributionUrl: String,<br>        ) = SwiftPackageConfiguration(packageName, swiftToolsVersion, platforms, zipFileName, zipChecksum, false, distributionUrl)<br>    }<br>}</pre><p>With this class, we then configure our instance as follows</p><pre>val packageConfiguration = SwiftPackageConfiguration.remote(<br>                packageName = rootProject.name,<br>                zipFileName = project.getZipFileName(),<br>                zipChecksum = project.zipFileChecksum().trim(),<br>                distributionUrl = project.swiftPackageDistributionURL<br>            )</pre><p>With an instance of this SwiftPackageConfiguration we then build our Package.swift file by using a template and subsituting in our desired configuration values.</p><pre>// Writes the Package.swift file by<br>// first loading from a template file and<br>// second subtituting in the config values<br>private fun File.writePackageDotSwiftFile(packageConfiguration: SwiftPackageConfiguration) {<br>    SimpleTemplateEngine()<br>        .createTemplate(SwiftPackageConfiguration.templateFile)<br>        .make(packageConfiguration.templateProperties)<br>        .writeTo(writer())<br>}</pre><p>The base template file lives in /src/main/resources/templates/Package.swift.template</p><pre>// swift-tools-version:$toolsVersion<br>import PackageDescription<br><br>let package = Package(<br>    name: &quot;$name&quot;,<br>    platforms: [<br>        $platforms<br>    ],<br>    products: [<br>        .library(<br>            name: &quot;$name&quot;,<br>            targets: [&quot;$name&quot;]<br>        ),<br>    ],<br>    targets: [<br>&lt;% if (isLocal) print &quot;&quot;&quot;        .binaryTarget(<br>            name: &quot;$name&quot;,<br>            path: &quot;./${zipName}&quot;<br>        ),&quot;&quot;&quot; else print &quot;&quot;&quot;        .binaryTarget(<br>            name: &quot;$name&quot;,<br>            url: &quot;$url&quot;,<br>            checksum: &quot;$checksum&quot;<br>        ),&quot;&quot;&quot; %&gt;<br>    ]<br>)</pre><p>When the configuration is applied to the template, it resulst in an output Package.swift file that looks like the following:</p><pre>// swift-tools-version:5.8.0<br>import PackageDescription<br><br>let package = Package(<br>    name: &quot;mobileshared&quot;,<br>    platforms: [<br>        .iOS(.v14)<br>    ],<br>    products: [<br>        .library(<br>            name: &quot;mobileshared&quot;,<br>            targets: [&quot;mobileshared&quot;]<br>        ),<br>    ],<br>    targets: [<br>        .binaryTarget(<br>            name: &quot;mobileshared&quot;,<br>            url: &quot;https://&lt;cloud run url&gt;/swiftpackage/mobileshared-&lt;version&gt;.zip&quot;,<br>            checksum: &quot;&lt;XCFramework zip checksum&gt;&quot;<br>        ),<br>    ]<br>)</pre><p>Note the url value. This is a url pointing to a Google Cloud Run service which will be described below. The purpose of this service is to give us a known location to build up our artifact urls so we can add them to this Package.swift file.</p><h4>Additional plugin code</h4><p>To round out the plugin implementation, we have a handful of of other configuration values and helper functions/classes.</p><pre>private const val DEFAULT_SWIFT_TOOLS_VERSION = &quot;5.8.0&quot;<br>private const val DEFAULT_IOS_PLATFORM_VERSION = &quot;.iOS(.v14)&quot;<br><br>private val Project.swiftPackageGCPProjectId<br>    get() = System.getenv(&quot;SWIFT_PACKAGE_BUCKET_GCP_PROJECT_ID&quot;) ?: &quot;&quot;<br><br>private val Project.swiftPackageBucketName<br>    get() = System.getenv(&quot;SWIFT_PACKAGE_BUCKET_NAME&quot;) ?: &quot;&quot;<br><br>private val Project.swiftPackageDistributionURL<br>    get() = System.getenv(&quot;SWIFT_PACKAGE_DISTRIBUTION_URL&quot;) ?: &quot;&quot;<br><br>private fun Project.createPackageDotSwiftFile(outputDirName: String, filename: String): File {<br>    return File(&quot;$buildDir/$outputDirName&quot;, filename).apply {<br>        parentFile.mkdirs()<br>        createNewFile()<br>    }<br>}<br><br><br>private const val TASK_GROUP_NAME = &quot;Premise Swift Package&quot;<br>private const val OUTPUT_PACKAGE_CONFIG_FILE_NAME =  &quot;Package.swift&quot;<br>private const val OUTPUT_FILE_DIR_NAME = &quot;swiftpackage&quot;<br>private fun Project.getZipFileName() = &quot;${rootProject.name}-${rootProject.version}.zip&quot;<br><br>internal fun Project.zipFileChecksum(): String {<br>    val outputPath = &quot;${buildDir}/$OUTPUT_FILE_DIR_NAME&quot;<br>    logger.info(&quot;checksum for path: $outputPath&quot;)<br>    logger.info(&quot;checksum for file: ${getZipFileName()}&quot;)<br>    return File(outputPath, getZipFileName())<br>        .takeIf { it.exists() }<br>        ?.let { zipFile -&gt;<br>            ByteArrayOutputStream().use { os -&gt;<br>                project.exec {<br>                    workingDir = File(outputPath)<br>                    executable = &quot;swift&quot;<br>                    args = listOf(&quot;package&quot;, &quot;compute-checksum&quot;, zipFile.name)<br>                    standardOutput = os<br>                }<br>                os.toString()<br>            }<br>        } ?: &quot;&quot;<br>}<br><br></pre><h3>Publishing the Package.swift File to GitHub</h3><p>With our SwiftPackagePlugin applied to our project, we can run the createSwiftPacakge task from CI to build the binaries, upload them to Google Cloud Storage, and generate a Package.swift referencing that version.</p><p>However, we will need to publish that Package.swift file to GitHub so Xcode can reference it, and use it to eventually download the binaries.</p><p>To do this, we are using GitHub actions. The following, is a snippet from our release build that includes the neccessary job steps to build and publish the Swift Package. (this does not include the neccessary gcloud env setup)</p><pre># runs our Gradle tasks for building and uploading<br>- name: Create Swift Package<br>  uses: gradle/gradle-build-action@v2.4.2<br>  with:<br>    arguments: createSwiftPackage<br><br># copies Package.swift to desired location in repo so Xcode sees it<br>- name: Prepare iOS Artifacts<br>  run: |<br>    cp -r shared/build/swiftpackage/Package.swift .<br><br># commits the updated Package.swift and tags it so Xcode can find it<br>- name: Tag Release<br>  run: |<br>    git add --all<br>    git commit -m &quot;[skip ci] Update Package.swift to version ${{ env.ARTIFACT_VERSION }}&quot;<br>    git tag -a ${{ env.ARTIFACT_VERSION }} -m &quot;mobile-shared v${{ env.ARTIFACT_VERSION }}&quot;<br>    git push<br>    git push origin ${{ env.ARTIFACT_VERSION }}</pre><h3>Consuming the Swift Package from Xcode Using Google Cloud Run</h3><p>From Xcode, we can now point our project at GitHub and ask it for a specific tagged version. Xcode and Swift Package Manager will then</p><ul><li>download the repo</li><li>examine the Package.swift file for the tagged commit</li><li>extract the binaryTarget url</li><li>attempt to download the binary</li></ul><p>When building the Package.swift file, we construct the url using the name of the shared artifact, the version of that artifact, and add those values to a known service url.</p><p>https://&lt;cloud run service url&gt;/swiftpackage/&lt;artifact name&gt;-&lt;version&gt;.zip</p><p>That service is our swift-package-distribution-service .</p><p>It’s a Google Cloud Run service that is permissioned to access our Cloud Storage bucket and stream the requested files back for download.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xJa_-CVQaRkgzJWOy_1A-A.png" /><figcaption>Xcode uses the url from Package.swift to make a request to swift-package-distribution-service to download the binaries from Google Cloud Storage</figcaption></figure><h4>Implementing swift-package-distribution-service</h4><p>How you would go about implementing your own version of this type of service is for you to decide. We built our service with Python using <a href="https://goblet.github.io/goblet/build/html/index.html">Goblet</a>. Goblet is a framework designed to streamline the creation og Google Cloud Services using Python.</p><p>With Goblet, our entire service looks like the following:</p><pre>import io<br>import os<br>from goblet import Goblet, goblet_entrypoint, Response<br>from flask import stream_with_context<br>from google.cloud import storage<br>import base64<br><br>CHUNK_SIZE = 8192<br>BUCKET_NAME = os.environ.get(&quot;CLOUD_STORAGE_BUCKET&quot;)<br>SWIFT_DISTRIBUTION_USERNAME = os.environ.get(&quot;SWIFT_DISTRIBUTION_USERNAME&quot;)<br>SWIFT_DISTRIBUTION_TOKEN = os.environ.get(&quot;SWIFT_DISTRIBUTION_TOKEN&quot;)<br><br>app = Goblet(function_name=&quot;swift-package-distribution-service&quot;, backend=&quot;cloudrun&quot;, routes_type=&quot;cloudrun&quot;)<br>goblet_entrypoint(app)<br><br><br>@app.route(&#39;/swiftpackage/{name}&#39;)<br>def home(name: str):<br>    auth_header = get_auth_token()<br><br>    if not is_token_valid(auth_header):<br>        return Response(&quot;Invalid token&quot;, None, 401)<br><br>    swiftpackage_filename = f&#39;{name}&#39;<br>    storage_client = storage.Client()<br><br>    bucket = storage_client.get_bucket(BUCKET_NAME)<br>    if bucket is None or not bucket.exists():<br>        missing_bucket_msg = f&#39;Bucket {bucket} does not exist&#39;<br>        app.log.info(missing_bucket_msg)<br>        return Response(missing_bucket_msg, None, 404)<br>    else:<br>        app.log.info(f&#39;Located bucket {bucket}&#39;)<br><br>    swiftpackage_blob = bucket.get_blob(swiftpackage_filename)<br>    if swiftpackage_blob is None or not swiftpackage_blob.exists():<br>        missing_blob_msg = f&#39;Requested Swift Package {swiftpackage_filename} does not exist&#39;<br>        app.log.info(missing_blob_msg)<br>        return Response(missing_blob_msg, None, 404)<br>    else:<br>        app.log.info(f&#39;Located file {swiftpackage_filename} and started download&#39;)<br><br>    swiftpackage_content = swiftpackage_blob.download_as_bytes()<br><br>    app.log.info(&quot;Streaming response&quot;)<br>    return stream_response(swiftpackage_content, swiftpackage_filename)<br><br><br>def stream_response(content: bytes, filename: str):<br>    return stream_with_context(<br>        read_bytes_chunks(content)), \<br>        200, \<br>        {<br>            &quot;Content-Type&quot;: &quot;application/octet-stream&quot;,<br>            &quot;Content-Disposition&quot;: f&#39;attachment; filename={filename}&#39;<br>        }<br><br><br>def read_bytes_chunks(content: bytes):<br>    app.log.info(&quot;Reading chunks&quot;)<br>    content_stream = io.BytesIO(content)<br>    with content_stream:<br>        while 1:<br>            buffer = content_stream.read(CHUNK_SIZE)<br>            if buffer:<br>                app.log.info(&quot;chunk&quot;)<br>                yield buffer<br>            else:<br>                app.log.info(&quot;no chunks left&quot;)<br>                break<br><br><br>def get_auth_token() -&gt; str:<br>    # We&#39;ll skip this specific implementation<br>    # We include a token taken from .netrc on developers&#39; and CI machines<br>    # If the token is not present, or incorrect, we will reject the request<br>    # This is used due to Swift Package Manager not allowing us to auth via gcloud<br><br><br>def is_token_valid(auth_header: str) -&gt; bool:<br>    # We&#39;ll skip this specific implementation<br><br><br>def decode_auth_header(auth_header: str) -&gt; str:<br>    # We&#39;ll skip this specific implementation</pre><p>With this service deployed, we can use its url when building our Package.swift file.</p><p>https://&lt;cloud run service url&gt;/swiftpackage/&lt;artifact name&gt;-&lt;version&gt;.zip</p><p>And now, when Xcode tries to download the file referenced in Package.swift it makes a request against this service, validates the auth token, and downloads the desired XCFramework binaries.</p><p>From there, our iOS build is off and running.</p><h3>How Has This Solution Worked?</h3><p>We’ve been using this solution for 6+ months now without issue.</p><p>By enabling us to move away from checking binaries into GitHub, it dramatically improved package sync time for our iOS devs that were regularly switching between branches requiring different versions of our mobile-shared project.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*E71Pl0gYAcpi_GkBg25yfg.png" /><figcaption>Today, we have multiple Kotlin Multiplatform projects leveraging this same solution for integration Swift Packages</figcaption></figure><p>Because the plugin, and service, are general purpose, we were able to apply the same plugin and deployment model for our second Kotlin Multiplatform project in the org.</p><p>Because the plugin and service were both already in place, applying this approach to the second project was trivial; not requiring any real new development work beyond configuring the plugin in the new project.</p><h3>What Are The Drawbacks To This Solution?</h3><p>Perhaps the biggest drawback to this solution is that it requires cloud resources for storage and request handling. In an organization already using cloud resources this is likely not an issue. However, for open source projects or hobbyists, this could be a significant barrier to entry; both in terms of cost required to run these resources and the administration/security of cloud resources/organizations/etc.</p><p>On a similar note, another drawback is that it’s not an off-the-shelf solution. It required dev work leveraging knowledge of Swift Package Manager, Gradle plugins, and Google Cloud. This might not be something every time has access to and could be an additional barrier to entry. For a more, off-the-shelf option, I recommend looking into <a href="https://github.com/touchlab/KMMBridge">KMMBridge from TouchLab</a>.</p><p>Finally, another, albiet smaller, limitation of the current implementation is the ordering of binary upload and Package.swift file creation. If uploading the XCFramework binary to Cloud Storage succeeds, but creation of the Package.swift file fails, it would result in a stranded artifact in our Cloud Storage bucket. In practice, this wouldn’t result in any significant cost unless it was happening repeatedly; and in 6+ months of using the current implementation I don’t believe we’ve ever encountered this issue.</p><p><strong><em>Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings, data and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our </em></strong><a href="https://bit.ly/3wnTrLn"><strong><em>Careers</em></strong></a><strong><em> page.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=be5c6987e5d" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/publishing-kotlin-multiplatform-swift-packages-to-google-cloud-storage-be5c6987e5d">Publishing Kotlin Multiplatform Swift Packages Using Google Cloud Storage and Cloud Run</a> was originally published in <a href="https://engineering.premise.com">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Serverless Data Pipelines in GCP using Dataform and BigQuery Remote Functions]]></title>
            <link>https://engineering.premise.com/serverless-data-pipelines-in-gcp-using-dataform-and-bigquery-remote-functions-9ee235d0cb18?source=rss----c5fada0a103d---4</link>
            <guid isPermaLink="false">https://medium.com/p/9ee235d0cb18</guid>
            <category><![CDATA[dataform]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[gcp]]></category>
            <category><![CDATA[goblet]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <dc:creator><![CDATA[Austen Novis]]></dc:creator>
            <pubDate>Wed, 23 Aug 2023 13:35:22 GMT</pubDate>
            <atom:updated>2023-08-23T13:35:22.715Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*cSfINV5gIm4zLBx9" /><figcaption>Photo by <a href="https://unsplash.com/@growtika?utm_source=medium&amp;utm_medium=referral">Growtika</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p><a href="https://cloud.google.com/dataform">Dataform</a> is an amazing new offering from <a href="https://cloud.google.com/gcp?utm_source=google&amp;utm_medium=cpc&amp;utm_campaign=na-US-all-en-dr-bkws-all-all-trial-e-dr-1605212&amp;utm_content=text-ad-none-any-DEV_c-CRE_665735450624-ADGP_Hybrid+%7C+BKWS+-+EXA+%7C+Txt_GCP-KWID_43700077223807298-kwd-87853815&amp;utm_term=KW_gcp-ST_gcp&amp;gclid=Cj0KCQjwuNemBhCBARIsADp74QQNLPlImqheJCB4pu0-ZL0f9HuT7W1GmNIhkT0qfNjRLxpO1dZf-VwaAvqdEALw_wcB&amp;gclsrc=aw.ds&amp;hl=en">Google Cloud Platform</a> (GCP) that allows for serverless orchestration for data pipelines. Previously, data engineering pipelines were either supported by separate data engineering teams, which many smaller organizations don’t have the resources to support, or were stitched together using a series of interrelated BigQuery scheduled queries, which could be time-consuming to build and error-prone to run and maintain.</p><p>Dataform solves these problems, especially for data scientists and data analysts, by including features such as</p><ul><li>Ability to build data pipelines using SQL</li><li>Integrates with GitHub and GitLab for code versioning and deployment</li><li>Monitor pipelines for failures and trigger alerts</li></ul><p>The main limitation of Dataform is that it only supports SQL, with limited ability to add javascript, but this limitation can be overcome with <a href="https://cloud.google.com/bigquery/docs/remote-functions">BigQuery Remote Functions</a>. BigQuery Remote Functions allow you to call external services running in <a href="https://cloud.google.com/run">Cloudrun</a> or <a href="https://cloud.google.com/functions">Cloudfunctions</a> from BigQuery, making it possible to have a serverless data pipeline that can run custom logic in any programming language that you may need.</p><p>The rest of this blog post will go through a complete example on how to setup both Dataform and BigQuery Remote Functions in just a few steps.</p><h3>Setup Dataform</h3><p>We will begin by setting up a simple Dataform pipeline in the GCP console. Navigate to Dataform and create a repository.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/578/1*jj9V9s9zc_cTbRNGAemoxw.png" /><figcaption>Create Repository</figcaption></figure><p>Make sure to grant the default Dataform service account service-PROJECT_ID@gcp-sa-dataform.iam.gserviceaccount.com BigQuery read and write permissions. This can be accomplished using the roles/bigquery.user role.</p><p>Next create a new development workspace and allow GCP to initialize it, which will add the default files needed to run the pipeline.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/303/1*txPxvOuBEIl5jRzjbXZ1ng.png" /><figcaption>Dataform files</figcaption></figure><p>Next create a new BigQuery dataset called tutorial , and inside a table called test with schema id: int, val: int, type: str .</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/418/1*TXUqBxmildsCK_kJK72Jtw.png" /><figcaption>BigQuery Schema</figcaption></figure><p>We can insert some test data by running the following query</p><pre>INSERT `PROJECT.tutorial.test` (id, val, type) VALUES (1,1,&quot;double&quot;),(2,2,&quot;square&quot;),(3,3,&quot;zero&quot;) </pre><p>Now lets return back to Dataform and in the file dataform.json update the defaultSchema to be the dataset tutorial. This ensures that Dataform creates any new tables in this dataset, and will allow us to deploy our goblet application correctly.</p><p>Next we will update the definitions/first_view.sqlx file to create a new table from the table we selected in the previous step.</p><pre>config { type: &quot;table&quot; }<br><br>SELECT * FROM PROJECT.DATASET.TABLE</pre><p>We will now setup the BigQuery Remote Function before coming back and updating the second_view.sqlx.</p><h3>Setup BigQuery Remote Function</h3><p>Setting up the BigQuery Remote Function requires a few steps, including creating a Cloudrun or Cloudfunction endpoint, creating a BigQuery routine, creating a BigQuery connection, and setting up the IAM permissions.</p><p>Luckily we can leverage <a href="https://github.com/goblet/goblet">Goblet</a>, an open source framework for writing, deploying, and managing serverless REST APIs and related GCP resources. With Goblet, we will be able to simply write our API code, while Goblet will handle all of the deployment steps. The source code for the following steps can be found in <a href="https://github.com/premisedata/gcp-tutorials/tree/main/018-dataform-bq-remote-functions">Github</a>.</p><p>To setup our Goblet application we need a requirements.txt that includes goblet-gcp .</p><p>We also need to pip install goblet locally in order to deploy our application, which can be done by running the commandpip install goblet-gcp.</p><p>Next we will setup our main.py , which will contain our function logic. In our example we will create a remote function that takes in a val and a type and returns an int. Note that we can put any logic we would like in this function including calls to a machine learning model.</p><pre>from goblet import Goblet, goblet_entrypoint<br><br>app = Goblet(function_name=&quot;goblet&quot;)<br>goblet_entrypoint(app)<br><br><br>@app.bqremotefunction(dataset_id=&quot;tutorial&quot;, location=&quot;US&quot;)<br>def tutorial(value: int, type: str) -&gt; int:<br>    if type == &quot;double&quot;:<br>        return value * 2<br>    if type == &quot;square&quot;:<br>        return value**2<br>    if type == &quot;zero&quot;:<br>        return 0<br>    else:<br>        return value</pre><p>To deploy the cloudfunction, the BigQuery connection, the BigQuery routine, and the correct invoker permissions, we simple need to run goblet deploy -p PROJECT -l LOCATION .</p><p>(To read a full writeup on Goblet and BigQuery Remote Functions, check out <a href="https://engineering.premise.com/tutorial-deploying-bigquery-remote-functions-9040316d9d3e">Tutorial: Deploying BigQuery Remote Function</a>s)</p><h3>Setup Dataform with BigQuery Remote Function</h3><p>Now that we have deployed our BigQuery Remote Function we are able to reference it in Dataform in defintions/second_view.sqlx , by using the following code</p><pre>config { type: &quot;table&quot;,<br>columns:{<br>             id: &quot;id&quot;,<br>             val:&quot;val&quot;,<br>             type:&quot;type&quot;<br>      } }<br><br>SELECT id, `PROJECT.tutorial`.goblet_tutorial(val, type) as val, type FROM ${ref(&quot;first_view&quot;)}</pre><p>Note how we call the BigQuery remote function we created in the previous section by using PROJECT.tutorial`.goblet_tutorial(val, type) , where val and type fields are passed to our Goblet application, and an int is returned, which we save as val in our new table.</p><p>We can view the full pipeline by looking at the compiled graph in Dataform.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ghLzR-S-xElIJPHteF-k8g.png" /><figcaption>Compiled Graph</figcaption></figure><h3>Executing the Dataform Pipeline</h3><p>Now that we have created a BigQuery test dataset, a Dataform pipeline and deployed a BigQuery Remote Function, all that is left is to execute our pipeline!</p><p>This can be triggered in the console, by clicking the start execution dropdown and selecting all actions .</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/358/1*O_r3aTnmc-hiNKDtmA19NQ.png" /><figcaption>Start Execution</figcaption></figure><p>The results of the pipeline can been seen in our test dataset in the table second_view.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/501/1*2yWyY2nBvuHRHU40jIcJhw.png" /><figcaption>Results of Dataform Pipeline</figcaption></figure><h3>Conclusion</h3><p>Dataform is a powerful new offering from GCP that allows for serverless orchestration for data pipelines. The main limitation of using Dataform compared to other workflow tools, is that Dataform is only able to run SQL queries. However, we have shown that we can actually add any custom logic into our pipelines using BigQuery Remote Functions in just a few simple steps. This makes Dataform a powerful tool, that can handle a wide variety of use cases, especially for data scientists and data analysts.</p><p>Feel free to leave a comment letting us know your interesting use cases for Dataform and BigQuery Remote Functions and don’t forget to star our open source project <a href="https://github.com/goblet/goblet/issues">Goblet</a>.</p><p><strong><em>Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our </em></strong><a href="https://bit.ly/3wnTrLn"><strong><em>Careers</em></strong></a><strong><em> page.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9ee235d0cb18" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/serverless-data-pipelines-in-gcp-using-dataform-and-bigquery-remote-functions-9ee235d0cb18">Serverless Data Pipelines in GCP using Dataform and BigQuery Remote Functions</a> was originally published in <a href="https://engineering.premise.com">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Concurrent Programming in Kotlin: Ensuring Thread Safety with Mutex]]></title>
            <link>https://engineering.premise.com/concurrent-programming-in-kotlin-ensuring-thread-safety-with-mutex-5d9c6b80644b?source=rss----c5fada0a103d---4</link>
            <guid isPermaLink="false">https://medium.com/p/5d9c6b80644b</guid>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[concurrency]]></category>
            <category><![CDATA[kotlin]]></category>
            <dc:creator><![CDATA[Kwabena Bio Berko]]></dc:creator>
            <pubDate>Tue, 18 Jul 2023 03:33:24 GMT</pubDate>
            <atom:updated>2023-07-18T03:33:24.756Z</atom:updated>
            <content:encoded><![CDATA[<p><strong><em>By Kwabena Bio Berko, Android Engineer</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ur7wqMdDlv5qPB9WVh-70A.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@cdr6934?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Chris Ried</a> on <a href="https://unsplash.com/photos/bN5XdU-bap4?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></figcaption></figure><p>I have recently been experimenting with <a href="https://en.wikipedia.org/wiki/Mutual_exclusion">mutual exclusion(Mutex)</a>, a concept in software engineering to solve race conditions when accessing shared resources.</p><p>Let’s assume we have a component with a <strong>refresh</strong> function that can be accessed from multiple threads or coroutines:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e19aa2a9d57bc105b841d28061a199b8/href">https://medium.com/media/e19aa2a9d57bc105b841d28061a199b8/href</a></iframe><p>In the implementation of this component, the function retrieves data from a remote resource and inserts it into the local database. However, these operations need to be atomic. Now, imagine if 10 threads call this function simultaneously, or even a few milliseconds apart. What happens? If you guessed that we make 10 network calls and 10 database IO operations to save the items, then you would be right, as confirmed by the below test:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9b279658dbf87021068ee21a913dc80e/href">https://medium.com/media/9b279658dbf87021068ee21a913dc80e/href</a></iframe><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/81cdc164ad807fd45df77389c00bd4cc/href">https://medium.com/media/81cdc164ad807fd45df77389c00bd4cc/href</a></iframe><p>But that’s not what we want. It’s definitely not resource-friendly. To address this issue, we can use a <a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/"><strong>Mutex</strong></a>.</p><p>As the name suggests, a <strong>Mutex</strong> provides mutual exclusion for a specific portion of your code, imposing restrictions on the access to that portion in situations where multiple threads or coroutines may attempt to access it concurrently. In essence, Mutex allows only one thread or coroutine to work within the confines of that portion of the code at any given time.</p><p>By introducing a Mutex in our <strong>refresh</strong> function, we can ensure that only one thread is executing the critical sections of the code, preventing race conditions and ensuring atomicity of the operations.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9d88a802b4c73f14ceb4d5527158af5d/href">https://medium.com/media/9d88a802b4c73f14ceb4d5527158af5d/href</a></iframe><p>In the example above, whenever the <strong>refresh</strong> function is called, we check to see if the Mutex is already locked or been used. If not, we lock it and then perform our network call and save the results in our local database. This means that whenever another thread or coroutine calls this refresh function as it’s being used, the refresh is skipped, as confirmed by the test below:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/34c17f87cbf5427811c4bb5be7a6f5b5/href">https://medium.com/media/34c17f87cbf5427811c4bb5be7a6f5b5/href</a></iframe><p>There are some other cases where we do not want to skip but wait for the current lock to be released. Achieving this is very straightforward in Kotlin as the <a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/with-lock.html"><strong>withLock</strong></a> lambda is a suspend function, meaning if the refresh function is called and the Mutex is already locked, the call suspends or waits for the Mutex to be unlocked:</p><pre>class MutexEnabledComponent: Component {<br>    private val mutex = Mutex()<br><br>    override suspend fun refresh() {<br>        mutex.withLock {<br>            delay(1L) // Delay to simulate a long running operation<br>        }<br>    }<br>}</pre><p>Using a Mutex has many advantages besides making your application run faster and use resources better. It helps keep your data safe and consistent by allowing only one thread or coroutine to access important sections at a time. This prevents any issues that can arise when multiple threads try to change the same data simultaneously.</p><p>However, it’s crucial to avoid overusing Mutex. When too much code is wrapped with Mutex locks, excessive serialization can occur, which can hinder parallelism and slow down your application. So, as engineers, it is important to identify the critical sections in our application that genuinely require exclusive access and limit the usage of Mutex locks to those specific sections.</p><p><strong><em>Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings, data and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our </em></strong><a href="https://bit.ly/3wnTrLn"><strong><em>Careers</em></strong></a><strong><em> page.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5d9c6b80644b" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/concurrent-programming-in-kotlin-ensuring-thread-safety-with-mutex-5d9c6b80644b">Concurrent Programming in Kotlin: Ensuring Thread Safety with Mutex</a> was originally published in <a href="https://engineering.premise.com">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Easily Manage IAM Policies for Serverless REST Applications in GCP with Goblet]]></title>
            <link>https://engineering.premise.com/easily-manage-iam-policies-for-serverless-rest-applications-in-gcp-with-goblet-f1580a97b74?source=rss----c5fada0a103d---4</link>
            <guid isPermaLink="false">https://medium.com/p/f1580a97b74</guid>
            <category><![CDATA[gcp]]></category>
            <category><![CDATA[identity-and-access]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[goblet]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <dc:creator><![CDATA[Austen Novis]]></dc:creator>
            <pubDate>Mon, 10 Jul 2023 20:09:55 GMT</pubDate>
            <atom:updated>2023-07-10T20:09:55.640Z</atom:updated>
            <content:encoded><![CDATA[<p><strong><em>By Austen Novis, Staff Software Engineer</em></strong></p><figure><img alt="Golden keys of various sizes arrayed on a wooden table" src="https://cdn-images-1.medium.com/max/1024/0*ouxzdExy8DC8eSF7" /><figcaption>Photo by <a href="https://unsplash.com/@fotonium?utm_source=medium&amp;utm_medium=referral">Akhilesh Sharma</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Writing and deploying serverless applications has never been easier, especially on <a href="https://cloud.google.com/gcp?utm_source=google&amp;utm_medium=cpc&amp;utm_campaign=na-US-all-en-dr-bkws-all-all-trial-e-dr-1605212&amp;utm_content=text-ad-none-any-DEV_c-CRE_491349594127-ADGP_Desk+%7C+BKWS+-+EXA+%7C+Txt+_+Google+Cloud+Platform+Core-KWID_43700060017921809-kwd-87853815&amp;utm_term=KW_gcp-ST_gcp&amp;gclid=CjwKCAjw-vmkBhBMEiwAlrMeF5CEwg3bINXm4o52K9T3ej459FXqqwDLFQXpviQjK0ZQbvUmnzkoUxoCVGAQAvD_BwE&amp;gclsrc=aw.ds&amp;hl=en">Google Cloud Platform</a> (GCP). With a few clicks developers are able to deploy their application to the cloud and then trigger calls in a variety of ways from <a href="https://cloud.google.com/scheduler">Cloud Schedulers</a> and <a href="https://cloud.google.com/pubsub/docs/overview#:~:text=Pub%2FSub%20is%20an%20asynchronous,the%20order%20of%20100%20milliseconds.">Pubsub Subscriptions</a> to <a href="https://cloud.google.com/api-gateway">API Gateways</a>.</p><p>Deploying your resources to GCP seems straightforward until you get a 403 error, <em>permission denied</em>. This error causes frustration as the root cause can be a number of issues, and often the solution requires knowing GCP roles and permissions in an in depth manner. The issue can be caused by the user not having the correct permissions to deploy their services or the services themselves not having the correct permissions to connect to each other.</p><p>Once you have identified the problem you will need to understand what roles have the required permissions, how to apply those roles, and where to apply them. Instead of application developers wasting their time researching GCP documentation, searching stack overflow, or reaching out to devops teams, IAM access should be as automated as much as possible.</p><p>Using the <a href="https://github.com/goblet/goblet">Goblet</a> framework, we are now able to view exactly what permissions are needed to deploy our serverless applications, enable required GCP service API’s, create a new custom role with these permissions, create a service account with this role, and add the correct invoker bindings so that all serverless services can connect to each other.</p><p>Goblet is a python framework for writing serverless applications in GCP with the goal of making it as simple as possible to write and deploy REST applications. Goblet uses simple decorators, similar to flask, to create the necessary configurations and automatically deploy the required services and infrastructure.</p><p>For example, with code below we can deploy a simple cloudfunction that is triggered by a pubsub subscription and a cloud scheduler. Goblet will also take care of adding the invoker bindings so both the subscription and cloud scheduler will have the permissions needed to actually make the call to the cloudfunction.</p><pre>import logging<br>from goblet import Goblet, goblet_entrypoint<br><br># goblet setup<br>app = Goblet(function_name=&quot;goblet-iam-tutorial&quot;)<br>goblet_entrypoint(app)<br><br># set debug level<br>app.log.setLevel(logging.DEBUG)<br><br># Define resources <br><br># Pubsub Topic<br>app.pubsub_topic(&quot;test&quot;)<br><br># Pubsub Subscription<br>@app.pubsub_subscription(topic=&quot;test&quot;, use_subscription=True)<br>def subscription(data):<br>    return &quot;success&quot;<br><br># Cloud Scheduler<br>@app.schedule(&quot;1 * * * *&quot;)<br>def schedule():<br>    return &quot;success&quot;</pre><p>In the rest of the tutorial we will go over how to write a simple Goblet application, how to enable the required GCP services, how to view the permissions needed to deploy the application, how to create a new custom role with these permissions, and finally how to create a deployment service account with this role. The code used for this tutorial can be found in <a href="https://github.com/premisedata/gcp-tutorials/tree/main/017-goblet-iam">github</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5VH8QV2ynaYIarHU7glKTw.png" /><figcaption>Visualization of what Resources Goblet Manages</figcaption></figure><h3>Prerequisites</h3><ul><li>GCP Account</li><li>Python environment (&gt;3.8)</li><li><a href="https://cloud.google.com/sdk/docs/install">Gcloud cli</a></li></ul><h3>Getting Started:</h3><p>Before you can deploy an application, be sure you have GCP credentials configured. You should run gcloud auth login and sign in to the desired GCP project.</p><p>Next you will need to install goblet, which can be done by running the commandpip install goblet-gcp .</p><h3>Writing your Application:</h3><p>We will write a simple application that can be triggered by both a Pubsub topic and cron job. Our code will also create the Pubsub topic if it doesn’t already exist. See below for the complete application, which we will write to main.py .</p><pre>import logging<br>from goblet import Goblet, goblet_entrypoint<br><br># goblet setup<br>app = Goblet(function_name=&quot;goblet-iam-tutorial&quot;)<br>goblet_entrypoint(app)<br><br># set debug level<br>app.log.setLevel(logging.DEBUG)<br><br># Define resources <br><br># Pubsub Topic<br>app.pubsub_topic(&quot;test&quot;)<br><br># Pubsub Subscription<br>@app.pubsub_subscription(topic=&quot;test&quot;, use_subscription=True)<br>def subscription(data):<br>    return &quot;success&quot;<br><br># Cloud Scheduler<br>@app.schedule(&quot;1 * * * *&quot;)<br>def schedule():<br>    return &quot;success&quot;</pre><p>Our requirements.txt will contain goblet-gcp .</p><pre>goblet-gcp</pre><p>And finally we will need a .goblet/config.json where we can specify our service accounts for our Pubsub topic and Cloud Scheduler.</p><pre>{<br>    &quot;pubsub&quot;: {<br>        &quot;serviceAccountEmail&quot;: &quot;sa_pubsub@goblet.iam.gserviceaccount.com&quot;<br>    },<br>    &quot;scheduler&quot;: {<br>        &quot;serviceAccount&quot;: &quot;sa_scheduler@goblet.iam.gserviceaccount.com&quot;<br>    }<br>}</pre><p>That is all that is needed for actually writing our application.</p><h3>Enabling Services</h3><p>Before we can deploy our application and the dependent resources, we need to ensure that the required GCP apis are enabled. This can be done by running goblet services check . This will check to see what service API’s are needed for the application code you have written.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/968/1*52ZcHVZG3k_nV60LbVZC4g.png" /><figcaption>GCP Service Api’s Required</figcaption></figure><p>If there are some services not enabled you can run goblet services enable and goblet will handle enabling the APIs. Note that it will take a few minutes for the API’s to be enabled.</p><h3>Generating an IAM Policy, an IAM Role, and a Service Account</h3><p>We can view the required permissions needed to deploy our application by using Goblet to generate a deployment IAM policy. This is done by running goblet services autogen_iam . This will create a autogen_iam_role.jsonfile in the .goblet directory.</p><pre>{<br>    &quot;roleId&quot;: &quot;Goblet_Deployment_Role_goblet_iam_tutorial&quot;,<br>    &quot;role&quot;: {<br>        &quot;title&quot;: &quot;Deployment role for goblet-iam-tutorial&quot;,<br>        &quot;description&quot;: &quot;Goblet generated role&quot;,<br>        &quot;includedPermissions&quot;: [<br>            &quot;cloudfunctions.functions.create&quot;,<br>            &quot;cloudfunctions.functions.delete&quot;,<br>            &quot;cloudfunctions.functions.get&quot;,<br>            &quot;cloudfunctions.functions.getIamPolicy&quot;,<br>            &quot;cloudfunctions.functions.list&quot;,<br>            &quot;cloudfunctions.functions.setIamPolicy&quot;,<br>            &quot;cloudfunctions.functions.sourceCodeSet&quot;,<br>            &quot;cloudfunctions.functions.update&quot;,<br>            &quot;cloudfunctions.operations.get&quot;,<br>            &quot;cloudscheduler.jobs.create&quot;,<br>            &quot;cloudscheduler.jobs.delete&quot;,<br>            &quot;cloudscheduler.jobs.get&quot;,<br>            &quot;cloudscheduler.jobs.list&quot;,<br>            &quot;cloudscheduler.jobs.update&quot;,<br>            &quot;pubsub.subscriptions.create&quot;,<br>            &quot;pubsub.subscriptions.delete&quot;,<br>            &quot;pubsub.subscriptions.get&quot;,<br>            &quot;pubsub.subscriptions.list&quot;,<br>            &quot;pubsub.subscriptions.update&quot;,<br>            &quot;pubsub.topics.create&quot;,<br>            &quot;pubsub.topics.delete&quot;,<br>            &quot;pubsub.topics.get&quot;,<br>            &quot;pubsub.topics.list&quot;,<br>            &quot;pubsub.topics.update&quot;<br>        ],<br>        &quot;stage&quot;: &quot;GA&quot;<br>    }<br>}</pre><p>This role contains all the required permissions needed to deploy our application. This is useful if you are using an external service to handle deployments, such as Github Actions, and want to create a service account that has the minimal permissions needed for deployment.</p><p>Goblet can also handle the creation of a custom role based on the IAM policy that was autogenerated, the creation of a deployment service account, and attaching the role to the service account. This is done by running goblet services create_service_account -p PROJECT .</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jWf4Pvj7aA3ui4VZW4ouVw.png" /><figcaption>Output of running goblet services create_service_account -p PROJECT</figcaption></figure><p>You can now view the service account in the IAM console and verify that the custom deployment role is attached.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fGIePWWjgWJ_oZphsiz8Mg.png" /><figcaption>Screenshot of the service account created by Goblet</figcaption></figure><h3>Deploying the Application</h3><p>Now that we have enabled the required GCP service API’s and created a custom deployment role and custom deployment service account, we are ready to deploy our application. Now using our custom deployment service account, we can simply run goblet deploy -p PROJECT -l LOCATION .</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5U938nVBwIA2YEyxBa36sw.png" /><figcaption>Output of running goblet deploy</figcaption></figure><h3>Triggering the Application</h3><p>You can verify everything is working correctly by submitting a test message in the Pubsub topic, which can be done via the CLI or the cloud console. You can also trigger the Cloud Scheduler manually if you do not want to wait for the cron schedule to trigger a scheduled event.</p><h3>Cleaning up</h3><p>You can easily delete all resources created by running goblet destroy -p PROJECT -l LOCATION . The only resources you will need to delete manually are the service account and custom role.</p><h3>Conclusion</h3><p>Using the <a href="https://github.com/goblet/goblet">Goblet</a> framework, we are now able to view exactly what permissions are needed to deploy our serverless applications, enable required GCP service API’s, create a new custom role with these permissions, create a service account with this role, and add the correct invoker bindings so that all serverless services can connect to each other.</p><p>Check out the full<a href="https://goblet.github.io/goblet/"> Goblet documentation</a> for more information on supported backends, infrastircture, and handlers. If you enjoyed this blog, then check out our other <a href="https://github.com/premisedata/gcp-tutorials">tutorials</a> or explore additional content on our newly launched <a href="https://engineering.premise.com/">engineer blog</a>!</p><p><strong><em>Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our </em></strong><a href="https://bit.ly/3wnTrLn"><strong><em>Careers</em></strong></a><strong><em> page.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f1580a97b74" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/easily-manage-iam-policies-for-serverless-rest-applications-in-gcp-with-goblet-f1580a97b74">Easily Manage IAM Policies for Serverless REST Applications in GCP with Goblet</a> was originally published in <a href="https://engineering.premise.com">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Tutorial: Low Usage Alerting On Slack for Google Cloud Platform (GCP)]]></title>
            <link>https://engineering.premise.com/tutorial-low-usage-alerting-on-slack-for-google-cloud-platform-gcp-cc68ac8ca4d?source=rss----c5fada0a103d---4</link>
            <guid isPermaLink="false">https://medium.com/p/cc68ac8ca4d</guid>
            <category><![CDATA[cost-savings]]></category>
            <category><![CDATA[gcp]]></category>
            <category><![CDATA[cloudrun]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[metrics]]></category>
            <dc:creator><![CDATA[Mauricio Martinez]]></dc:creator>
            <pubDate>Mon, 24 Apr 2023 15:57:09 GMT</pubDate>
            <atom:updated>2023-04-24T17:45:46.225Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/549/0*HmqTlZXdvZfEJiB3.png" /></figure><p><a href="https://cloud.google.com/">Google Cloud Platform</a> (GCP) is a powerful cloud computing platform that allows businesses to run their applications and workloads with ease. However, as the number of services and applications increases, it becomes challenging to keep track of the usage of each service and ensure they are cost optimized. To address this issue, we can deploy a Python-based Cloud Function that will monitor the low usage of GCP services and notify service owners weekly via Slack that their service can be safely downscaled to minimize costs.</p><p>This tutorial will walk you through setting up this entire flow in a few quick and easy steps, while allowing you to easily customize the service thresholds, Slack message ui, and the cron schedule. The code for this tutorial can be found on our <a href="https://github.com/premisedata/gcp-tutorials/tree/main/016-gcp-metrics-slack-alerts">gcp-tutorials</a> GitHub repository.</p><h3>Architecture</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QlhpbQBFssScEQJjPcMYbQ.png" /></figure><h3>Get CPU/Memory Usage Metrics</h3><p>We can use the <a href="https://cloud.google.com/monitoring/charts/metrics-explorer">Metrics Explorer</a> UI to build a MQL query to retrieve the desired data from the monitoring metrics API. In this tutorial we will monitor Cloud Run services, but you can monitor other resources by using a different MQL query.</p><p>In our example we selected the CPU and Memory Cloud Run metrics:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mnbAehi9fsVzTvFFrhBuRQ.png" /></figure><p>We group by service name and location and select the alignment for the 99th percentage to get the max usage over the 1 week duration.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*drjyLpoYd33mGphtbRNo7Q.png" /></figure><p>Then click on CODE EDITOR to generate a sample MQL query:</p><pre>fetch cloud_run_revision<br>| metric &#39;run.googleapis.com/container/cpu/utilizations&#39;<br>| group_by 1w,<br>    [value_utilizations_percentile: percentile(value.utilizations, 99)]<br>| every 1w<br>| group_by [resource.service_name, resource.location],<br>    [value_utilizations_percentile_max: max(value_utilizations_percentile)]</pre><pre>fetch cloud_run_revision<br>| metric &#39;run.googleapis.com/container/memory/utilizations&#39;<br>| group_by 1w,<br>    [value_utilizations_percentile: percentile(value.utilizations, 99)]<br>| every 1w<br>| group_by [resource.service_name, resource.location],<br>    [value_utilizations_percentile_max: max(value_utilizations_percentile)]</pre><h3>Code</h3><p>Once we have our MQL queries we will use the <a href="https://cloud.google.com/monitoring/api/ref_v3/rest/v3/projects.timeSeries/query">Metrics API</a> to run the query within our Cloud Function. We leverage <a href="https://github.com/goblet/goblet">goblet</a> to deploy the scheduled Cloud Function that will be running each week to do the check. Our code also uses the <a href="https://github.com/goblet/goblet_gcp_client">goblet-gcp-client</a> library to easily create our metrics client. This client is used to fetch the requested CPU/Memory data points.</p><p>We do additional checks after requesting the data to see if the service is already at the minimum thresholds, since downscaling is not possible in that case. If the service can be downscaled our Cloud Function will send a Slack message notifying the owner of the service that the service can be downscaled to save costs.</p><h4>Deploy it!</h4><p>The sample code can be cloned from <a href="https://github.com/premisedata/gcp-tutorials/tree/main/016-gcp-metrics-slack-alerts">gcp-tutorials</a>.</p><p>Rename .goblet/config.json.sample to .goblet/config.json and replace all values that have {} with your environment details. This includes a <strong><em>SLACK_CHANNEL_ID</em></strong>, <strong><em>SLACK_TOKEN</em></strong>, and <strong><em>SERVICE_ACCOUNT_EMAIL</em></strong>.</p><pre>{<br>  &quot;function_name&quot;: &quot;{FUNCTION_NAME}&quot;,<br>  &quot;cloudfunction&quot;: {<br>      &quot;environmentVariables&quot;: {<br>          &quot;CLOUDRUN_CPU_MIN_VALUE&quot;: &quot;1000&quot;,<br>          &quot;CLOUDRUN_MEMORY_MIN_VALUE&quot;: &quot;512&quot;,<br>          &quot;CLOUDRUN_CPU_MIN_THRESHOLD&quot;: &quot;10%&quot;,<br>          &quot;CLOUDRUN_MEMORY_MIN_THRESHOLD&quot;: &quot;10%&quot;,<br>          &quot;SLACK_CHANNEL_ID&quot;: &quot;{SLACK_CHANNEL_ID}&quot;,<br>          &quot;CRON_EXPRESSION&quot;: &quot;0 10 * 1 *&quot;,<br>          &quot;DEBUG&quot;: &quot;true&quot;<br>      },<br>      &quot;secretEnvironmentVariables&quot;:[<br>          {<br>              &quot;key&quot;: &quot;SLACK_BOT_TOKEN&quot;,<br>              &quot;secret&quot;: &quot;{SLACK_TOKEN}&quot;,<br>              &quot;version&quot;: &quot;latest&quot;<br>          }<br>      ],<br>      &quot;serviceAccountEmail&quot;: &quot;{SERVICE_ACCOUNT_EMAIL}&quot;,<br>      &quot;timeout&quot;: &quot;540s&quot;<br>  }<br>}</pre><p>Next you can run the following two commands to install the required dependencies and deploy the application:</p><pre>pip install -r requirements.txt<br>goblet deploy --project=&lt;PROJECT&gt; --location=&lt;LOCATION&gt;</pre><p>When the command finishes successfully you will have a Cloud Function in the chosen project and a Cloud Scheduler that will execute the function every week, which results in slack messages that post which Cloud Run services that can be downscaled because they were below the configured thresholds for the entire week.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*R_1FJ08VUPWlP7j06rnUZQ.png" /><figcaption>Sample Slack Message</figcaption></figure><p>The schedule can be adjusting by changing the <strong><em>CRON_EXPRESSION </em></strong>variable in <em>config.json</em>.</p><h3>Conclusion</h3><p>We have shown how to build and deploy a simple solution that can have a significant effect on your cloud cost savings. This is especially important as you scale up your cloud environment, as costs can quickly spiral if you are not careful.</p><p>If you found this tutorial helpful you should check out another tutorial on <a href="https://medium.com/engineering-at-premise/tutorial-cost-spike-alerting-for-google-cloud-platform-gcp-46fd26ae3f6a">cloud cost spike alerting</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cc68ac8ca4d" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/tutorial-low-usage-alerting-on-slack-for-google-cloud-platform-gcp-cc68ac8ca4d">Tutorial: Low Usage Alerting On Slack for Google Cloud Platform (GCP)</a> was originally published in <a href="https://engineering.premise.com">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Deploy and Handle GCP CloudTasks with Goblet in minutes.]]></title>
            <link>https://engineering.premise.com/deploy-and-handle-gcp-cloudtasks-with-goblet-in-minutes-ee138e9dd2c5?source=rss----c5fada0a103d---4</link>
            <guid isPermaLink="false">https://medium.com/p/ee138e9dd2c5</guid>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[gcp]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[rate-limiting]]></category>
            <category><![CDATA[cloud-tasks]]></category>
            <dc:creator><![CDATA[Mauricio Wittenberg]]></dc:creator>
            <pubDate>Tue, 18 Apr 2023 13:55:58 GMT</pubDate>
            <atom:updated>2023-04-18T13:55:58.770Z</atom:updated>
            <content:encoded><![CDATA[<p>by Mauricio Wittenberg.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Jvg4xgpEwxfSbH9TbLolDA.png" /><figcaption>Goblet Deploy</figcaption></figure><h3>Introduction</h3><p><a href="https://cloud.google.com/tasks">CloudTasks</a> is a <a href="https://cloud.google.com/">Google Cloud Platform</a> (GCP) service that allows users to enqueue tasks in a queue. It is somewhat similar to <a href="https://cloud.google.com/pubsub/docs/choosing-pubsub-or-cloud-tasks"><em>PubSub</em></a><em>,</em> but with <em>CloudTasks</em>, the queue has features such as <a href="https://cloud.google.com/tasks/docs/reference/rest/v2/projects.locations.queues#ratelimits"><em>rateLimits</em></a><em> </em>and <a href="https://cloud.google.com/tasks/docs/reference/rest/v2/projects.locations.queues#retryconfig"><em>retryConfig</em></a><em>. </em>This can be very useful when having to enforce a rate limit over an external API or retrying tasks that have a known probability of failure.</p><p>If you want to use <em>CloudTasks </em>you will have to manually deploy cloud infrastructure, understand the <em>CloudTask</em> object, deal with client libraries and figure out multiple IAM role bindings. Or you can use <a href="https://github.com/goblet/goblet">Goblet</a> for all the heavy lifting and in just a few minutes be able push a task and later process the task in a Python function of our choosing.</p><p>Goblet is a Python based cloud framework for building serverless microservices on Google Cloud Platform. It enables developers to quickly and reliably deploy cloud resources together with the code that will make use of those resources.</p><p>In this blog post we will review how Goblet deploys <strong><em>CloudTaskQueues</em></strong> and provides the user with an simple code interface to enqueue and handle <strong><em>CloudTasks⁶</em></strong>.</p><p>Using the short snipped of code below we can define a queue, create a client, push a task to the queue, and handle tasks from the queue!</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/d81fb2d8bd47a3f69479708d61dd5230/href">https://medium.com/media/d81fb2d8bd47a3f69479708d61dd5230/href</a></iframe><p>See the rest of the tutorial for setting up your Goblet environment, setting permissions, and deploying you infrastructure and application in a few quick steps.</p><h3>Infrastructure</h3><p>For this tutorial we will use an example GCP project called goblet-cloudtask and deploy region specific resources to us-central1. If you would like to follow along the complete code can be found at <a href="https://github.com/premisedata/gcp-tutorials">gcp-tutorials</a>. The account used to run this example must have enough IAM role binding to run all the gcloud commands described in the <strong>Set-Up </strong>section<strong> </strong>below.</p><p>Now, let’s take a look at the infrastructure Goblet will create. We won’t have to take care of any of these steps ourselves. All we need to do is write our code in main.pyand Goblet will take care of the rest when we run <em>goblet deploy</em>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Jvg4xgpEwxfSbH9TbLolDA.png" /><figcaption>Goblet Deploy</figcaption></figure><ol><li><strong>Deploy Queue:</strong> Goblet will register line 5³ in main.py as an instruction to create or update a <em>CloudTaskQueue</em> with id my-queue.</li><li><strong>Create CloudBuild:</strong> Upload all files needed to build a Docker image that will be able to run main.py in a <em>CloudRun Revision</em>.</li><li><strong>Store Image:</strong> the Docker image created in the previous step is stored in <em>Container Registry</em>.</li><li><strong>Deploy CloudRun:</strong> Create a new <em>CloudRun Revision</em> pulling our previously built docker image from <em>Container Registry </em><strong>(5)</strong>.</li></ol><p>The last diagram, Runtime Flow shows how the example will run after the infrastructure and code are deployed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SG6Cu2fC4NVEjbQbWE2RoA.png" /><figcaption>Runtime Flow</figcaption></figure><ol><li>We make an HTTP request to the /enqueue path of the <em>CloudRun Revision</em> running our main.py code.</li><li>Because we decorated function enqueue of our main.py with @app.route(&quot;/enqueue&quot;, methods=[&quot;GET]) , Goblet will route the request to the enqueue function. In line 10 we use client.enqueue⁴ to push a new <em>CloudTask</em> (with a <em>payload </em>and a <em>target</em>) to the queue.<em> </em>As soon as the task in enqueued, client.enqueue returns.</li><li>The <em>CloudTask </em>now lives in the queue. Based on the queue’s configuration, it will independently decide when to make an HTTP request to the <em>target </em>with the <em>payload</em>.</li><li>The <em>CloudTaskQueue </em>decides to process the <em>CloudTask. </em>Notice how in line 10 and line 13 of our example the new task has target=&quot;my_target&quot; and the app.cloudtasktarget decorator has name=&quot;my_target&quot;. This links the <em>CloudTask </em>with the function that will handle it when called from the <em>CloudTaskQueue.⁵</em></li></ol><p>When we test the deployment, we will be able to see these requests in the service’s log.</p><h3><strong>Set-Up</strong></h3><p>For Goblet to be able to apply all the actions described in the previous section, we will need to enable APIs, create service accounts with the required IAM policy bindings and create a container repository for our Docker image.</p><p><strong><em>APIs</em></strong></p><pre>gcloud services enable run.googleapis.com<br>gcloud services enable cloudfunctions.googleapis.com<br>gcloud services enable cloudbuild.googleapis.com<br>gcloud services enable iam.googleapis.com<br>gcloud services enable artifactregistry.googleapis.com<br>gcloud services enable cloudresourcemanager.googleapis.com<br>gcloud services enable apigateway.googleapis.com<br>gcloud services enable cloudtasks.googleapis.com</pre><p><strong><em>IAM Policy Bindings</em></strong></p><p>The Goblet client <strong><em>goblet </em></strong>uses GCP’s REST API to achieve the infrastructure state described in the Goblet Deploy diagram. This is the list of roles at the project level needed by the user running the <em>goblet</em> command-line client.</p><pre>roles/apigateway.admin<br>roles/cloudbuild.serviceAgent<br>roles/cloudfunctions.developer<br>roles/run.developer<br>roles/run.serviceAgent<br>roles/cloudtasks.queueAdmin</pre><p>In this example, we will use a service account instead of a user account to run the <em>goblet </em>command. By the time we are ready to deploy — when we need to run <em>goblet deploy</em>— we will impersonate the service account so API calls are authenticated as coming from the service account.²</p><p><strong><em>Service Accounts</em></strong></p><p>The first service account we need is the deployer. This is the service account described in the previous paragraph. It is the account that will run steps 1 through 5 in the Goblet Deploy diagram.</p><pre>gcloud iam service-accounts create deployer --display-name=&quot;deployer&quot;<br>     <br>gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:deployer@goblet-cloudtask.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/apigateway.admin&quot;<br><br>gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:deployer@goblet-cloudtask.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/cloudbuild.serviceAgent&quot;<br><br>gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:deployer@goblet-cloudtask.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/run.serviceAgent&quot;<br><br>gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:deployer@goblet-cloudtask.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/cloudfunctions.developer&quot;<br><br>gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:deployer@goblet-cloudtask.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/run.developer&quot;<br><br>gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:deployer@goblet-cloudtask.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/cloudtasks.queueAdmin&quot;</pre><p>Next we need the cloudtask service account. When the <em>CloudTask</em> runs, it will use this service account to authenticate itself against <em>CloudRun</em>¹. Goblet implements an easy way to create <em>CloudTask</em>s that will be handled by a function in the same <em>CloudRun</em> service. The cloudtask service account needs roles/run.invoker to make requests to <em>CloudRun </em>endpoints.</p><pre>gcloud iam service-accounts create cloudtask --display-name=&quot;cloudtask&quot;<br><br>gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:cloudtask@goblet-cloudtask.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/run.invoker&quot;</pre><p>The last account we need is the cloudrun service account. This is the account that runs our <em>CloudRun</em> service and calls the <em>CloudTaskQueue API </em>to enqueue tasks. Therefore it needs the roles/cloudtasks.enqueuer and roles/run.viewer roles. It also needs roles/iam.serviceAccountUser bound to service account cloudtask to be able to create <em>CloudTasks</em> that will authenticate with the cloudtask service account.</p><pre>gcloud iam service-accounts create cloudrun --display-name=&quot;cloudrun&quot;<br><br>gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:cloudrun@goblet-cloudtask.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/cloudtasks.enqueuer&quot;<br><br>gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:cloudrun@goblet-cloudtask.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/run.viewer&quot;<br><br>gcloud iam service-accounts add-iam-policy-binding \<br>  cloudtask@goblet-cloudtask.iam.gserviceaccount.com \<br>  --member=&quot;serviceAccount:cloudrun@goblet-cloudtask.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/iam.serviceAccountUser&quot;</pre><p><strong><em>Container Registry</em></strong></p><pre>gcloud artifacts repositories create cloud-run-source-deploy \<br>   --location=us-central1 --repository-format=docker</pre><h3>Deploy</h3><p>First we will <em>pull</em> our example <a href="https://github.com/premisedata/gcp-tutorials/tree/goblet-cloudtask/015-goblet-cloudtask">code</a>.</p><pre>$ git clone https://github.com/premisedata/gcp-tutorial<br>$ cd gcp-tutoriales/015-goblet-cloudtask<br>$ tree<br>.<br>├── .goblet<br>│   └── config.json.sample<br>├── Dockerfile<br>├── README.md<br>├── main.py<br>└── requirements.txt</pre><p>To deploy our Goblet app, we need to create a config.json file that links our service accounts with the services that they will run. Our code example already has a template configuration file in .goblet/config.json.sample. We just need to replace the {PROJECT} boilerplate variable.</p><pre>$ pwd<br>./gcp-tutorials/015-goblet-cloudtask<br><br>$ export GOBLET_PROJECT=&quot;goblet-cloudtask&quot;<br>$ export GOBLET_LOCATION=&quot;us-central1&quot;<br>$ sed &quot;s/{PROJECT}/$GOBLET_PROJECT/g&quot; .goblet/config.json.sample &gt; .goblet/config.json<br><br>$ cat .goblet/config.json<br>{<br>  &quot;cloudrun_revision&quot;: {<br>    &quot;serviceAccount&quot;: &quot;cloudrun@goblet-cloudtask.iam.gserviceaccount.com&quot;<br>  },<br>  &quot;cloudtask&quot;: {<br>    &quot;serviceAccount&quot;: &quot;cloudtask@goblet-cloudtask.iam.gserviceaccount.com&quot;<br>  },<br>  &quot;cloudtaskqueue&quot;: {<br>    &quot;my-queue&quot;: {<br>      &quot;rateLimits&quot;: {<br>        &quot;maxDispatchesPerSecond&quot;: 500,<br>        &quot;maxBurstSize&quot;: 100,<br>        &quot;maxConcurrentDispatches&quot;: 1000<br>      },<br>      &quot;retryConfig&quot;: {<br>        &quot;maxAttempts&quot;: 10,<br>        &quot;minBackoff&quot;: &quot;0.100s&quot;,<br>        &quot;maxBackoff&quot;: &quot;3600s&quot;,<br>        &quot;maxDoublings&quot;: 16<br>      }<br>    }<br>  }<br>}</pre><p>Almost all of Goblet’s features can be configured in the config.json file. Here we see how the cloudrun service account is set to run the <em>CloudRun Revision. </em>We have also set the cloudtask service account as the service account the <em>CloudTask</em> will use to authenticate itself.</p><p>We are almost ready to deploy. As mentioned at the beginning of the post, we will deploy impersonating the deployer service account. We will need an additional IAM policy binding to be able to do that before authenticating.</p><pre>export MY_GCP_ACCOUNT=`gcloud auth list --filter=status:ACTIVE --format=&quot;value(account)&quot;`<br><br># IAM binding to impersonate deployer service account<br>gcloud iam service-accounts add-iam-policy-binding \<br>  deployer@goblet-cloudtask.iam.gserviceaccount.com \<br>  --member=&quot;user:$MY_GCP_ACCOUNT&quot; \<br>  --role=&quot;roles/iam.serviceAccountTokenCreator&quot;<br><br># authenticate impersonating deployer service account<br>gcloud auth application-default login \<br>  --impersonate-service-account=deployer@goblet-cloudtask.iam.gserviceaccount.com</pre><p>We can now install Goblet and deploy.</p><pre>$ pwd<br>./gcp-tutorials/015-goblet-cloudtask<br><br>$ export GOBLET_PROJECT=&quot;goblet-cloudtask&quot;<br>$ export GOBLET_LOCATION=&quot;us-central1&quot;<br><br>$ python3 -m venv venv<br>$ . venv/bin/activate<br>(venv) $ pip install -r requirements.txt<br>(venv) $ goblet deploy -l $GOBLET_LOCATION -p $GOBLET_PROJECT<br><br>INFO:goblet.app:deploying infrastructure<br>INFO:goblet.decorators:deploying cloudtaskqueue<br>INFO:goblet.deployer:CloudTask Queue [my-queue] deployed<br>INFO:goblet.decorators:deploying apigateway<br>INFO:goblet.app:preparing to deploy with backend cloudrun<br>INFO:goblet.backend:zipping source code<br>INFO:goblet.backend:uploading source zip to gs......<br>INFO:goblet.backend:source code uploaded<br>INFO:goblet.deployer:creating cloudbuild<br>INFO:goblet.deployer:creating cloudrun<br>INFO:goblet.decorators:deploying cloudtasktarget<br>INFO:goblet.decorators:deploying route<br>INFO:goblet.deployer:deploying api......<br>INFO:goblet.deployer:api successfully deployed...<br>INFO:goblet.deployer:api endpoint is cloudtask-example-xxxxxxxx.uc.gateway.dev</pre><h3>Test</h3><p>To test that everything is working, we will make a request to the /enqueue endpoint in our deployed URL as described in the Runtime Flow diagram. Since we are impersonating the deployer service account and the request has to be authenticated, we will bind roles/run.invoker to deployer so we can use it to make requests to the <em>CloudRun</em> endpoint.</p><pre>gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:deployer@goblet-cloudtask.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/run.invoker&quot;<br><br>CLOUDRUN_URL=`gcloud run services list --filter=SERVICE:cloudtask-example --format=&quot;value(URL)&quot;`<br>curl $CLOUDRUN_URL/enqueue \<br>  -H &quot;Authorization: Bearer $(gcloud auth print-identity-token)&quot;  </pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*U1Ot0pKgB3Z5SpfoOPuJ5w.png" /><figcaption>CloudRun Logs</figcaption></figure><p>We can see from the logs that the CloudRun gets deployed by the deployer service account. Then we see the <em>curl</em> GET request to /enqueue and at last the POST request from the <em>CloudTaskQueue </em>and the log {&quot;title&quot;: &quot;enqueue&quot;} from the response when then <em>CloudTask </em>is processed.</p><h3>Conclusion</h3><p>Even though GCP has a huge number of useful services and features, orchestrating them together can be a time consuming and error prone task. Goblet solves this problem by reliably orchestrating and deploying the infrastructure and code needed for you application to work.</p><h3>TL;DR</h3><pre>$ export GOBLET_PROJECT=&quot;goblet-cloudtask&quot;<br>$ export GOBLET_LOCATION=&quot;us-central1&quot;<br>$ export MY_GCP_ACCOUNT=`gcloud auth list --filter=status:ACTIVE --format=&quot;value(account)&quot;`<br>##### set the project<br>$ gcloud config set project $GOBLET_PROJECT<br>##### enable APIs<br>$ gcloud services enable run.googleapis.com<br>$ gcloud services enable cloudfunctions.googleapis.com<br>$ gcloud services enable cloudbuild.googleapis.com<br>$ gcloud services enable iam.googleapis.com <br>$ gcloud services enable artifactregistry.googleapis.com<br>$ gcloud services enable cloudresourcemanager.googleapis.com<br>$ gcloud services enable apigateway.googleapis.com<br>$ gcloud services enable cloudtasks.googleapis.com<br>##### service account to run goblet deploy<br>$ gcloud iam service-accounts create deployer --display-name=&quot;deployer&quot;     <br>$ gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:deployer@${GOBLET_PROJECT}.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/apigateway.admin&quot;<br>$ gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:deployer@${GOBLET_PROJECT}.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/cloudbuild.serviceAgent&quot;<br>$ gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:deployer@${GOBLET_PROJECT}.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/run.serviceAgent&quot;<br>$ gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:deployer@${GOBLET_PROJECT}.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/cloudfunctions.developer&quot;<br>$ gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:deployer@${GOBLET_PROJECT}.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/run.developer&quot;<br>$ gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:deployer@${GOBLET_PROJECT}.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/cloudtasks.queueAdmin&quot;<br>##### service account to authenticate the CloudTask against CloudRun<br>$ gcloud iam service-accounts create cloudtask --display-name=&quot;cloudtask&quot;<br>$ gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:cloudtask@${GOBLET_PROJECT}.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/run.invoker&quot;<br>##### service account to run the CloudRun Revision<br>gcloud iam service-accounts create cloudrun --display-name=&quot;cloudrun&quot;<br>gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:cloudrun@${GOBLET_PROJECT}.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/cloudtasks.enqueuer&quot;<br>gcloud projects add-iam-policy-binding goblet-cloudtask \<br>  --member=&quot;serviceAccount:cloudrun@${GOBLET_PROJECT}.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/run.viewer&quot;<br>gcloud iam service-accounts add-iam-policy-binding \<br>  cloudtask@${GOBLET_PROJECT}.iam.gserviceaccount.com \<br>  --member=&quot;serviceAccount:cloudrun@${GOBLET_PROJECT}.iam.gserviceaccount.com&quot; \<br>  --role=&quot;roles/iam.serviceAccountUser&quot;<br>##### impersonate deployer service account<br>$ gcloud iam service-accounts add-iam-policy-binding \<br>  deployer@${GOBLET_PROJECT}.iam.gserviceaccount.com \<br>  --member=&quot;user:${MY_GCP_ACCOUNT}&quot; \<br>  --role=&quot;roles/iam.serviceAccountTokenCreator&quot;<br>$ gcloud auth application-default login \<br>  --impersonate-service-account=deployer@${GOBLET_PROJECT}.iam.gserviceaccount.com<br>##### fetch example and deploy<br>$ git clone https://github.com/premisedata/gcp-tutorial<br>$ cd gcp-tutoriales/015-goblet-cloudtask<br>$ python3 -m venv venv<br>$ . venv/bin/activate<br>$ sed &quot;s/{PROJECT}/$GOBLET_PROJECT/g&quot; .goblet/config.json.sample &gt; .goblet/config.json<br>(venv) $ pip install -r requirements.txt<br>(venv) $ goblet deploy -l $GOBLET_LOCATION -p $GOBLET_PROJECT<br># test<br>$ SERVICE_URL=`gcloud run services list \<br>  --filter=SERVICE:cloudtask-example --format=&quot;value(URL)&quot;<br>$ curl $SERVICE_URL/enqueue \<br>  -H &quot;Authorization: Bearer $(gcloud auth print-identity-token)&quot; </pre><p>[1] The httpRequest of the <em>CloudTask </em>expects a service account for authentication. <a href="https://github.com/goblet/goblet/blob/608512c7fccec6cb29ce1b6d9353c2b6e4977d3b/goblet/infrastructures/cloudtask.py#L30">https://github.com/goblet/goblet/blob/608512c7fccec6cb29ce1b6d9353c2b6e4977d3b/goblet/infrastructures/cloudtask.py#L30</a></p><p>[2] This makes it much easier later when integrating with CI/CD.</p><p>[3] During <em>goblet deploy</em> line 5 creates or updates a queue. During <em>runtime </em>Goblet returns an object that is wrapper for <em>CloudTasks REST API</em>.</p><p>[4] Since the client is a Goblet wrapper for the <em>CloudTask REST API</em>, the <em>CloudTask</em> target is set to the URL of the <em>CloudRun Revision</em>. The client also added headers that will let Goblet route the request to the the function decorated with a matching cloudtasktarget.</p><p>[5] The client adds an HTTP Header to the <em>CloudTask </em>with the name of the target that handles the task. In this example it would be X-Goblet-CloudTask-Target: my_target. When the <em>CloudTaskQueue </em>makes a request to the target it does so with User-Agent: Google-Cloud-Tasks. The combination of these two headers allows Goblet to route the <em>CloudTask </em>to the defined target/function.</p><p>[6] <a href="https://cloud.google.com/tasks/docs">CloudTasks Documentation</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ee138e9dd2c5" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/deploy-and-handle-gcp-cloudtasks-with-goblet-in-minutes-ee138e9dd2c5">Deploy and Handle GCP CloudTasks with Goblet in minutes.</a> was originally published in <a href="https://engineering.premise.com">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Dynamic Data On Backstage Templates]]></title>
            <link>https://engineering.premise.com/dynamic-data-on-backstage-templates-ad8a0ae316e1?source=rss----c5fada0a103d---4</link>
            <guid isPermaLink="false">https://medium.com/p/ad8a0ae316e1</guid>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[backstage]]></category>
            <category><![CDATA[plugins]]></category>
            <category><![CDATA[dynamic-data]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <dc:creator><![CDATA[Mauricio Martinez]]></dc:creator>
            <pubDate>Mon, 13 Mar 2023 17:47:07 GMT</pubDate>
            <atom:updated>2023-03-13T17:47:07.054Z</atom:updated>
            <content:encoded><![CDATA[<h3>Dynamic Data in Backstage Templates</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yC4_b1pgbFBabkkXJez7VA.png" /></figure><h3>Table Of Contents</h3><blockquote><a href="#3099">Introduction</a></blockquote><blockquote><a href="#9fb9">Frontend Plugin (dynamic-pick-extension</a>)</blockquote><blockquote><a href="#cae0">Backend Plugin (form-data-backend)</a></blockquote><h3>Introduction</h3><p>If you’ve ever built a <a href="https://backstage.io/docs/features/software-templates/writing-templates">Backstage template</a>, you know that creating user-friendly forms is a must. One of the most common form elements is the enumcomponent. It allows users to choose from a list of options, which is especially useful when you have a large number of choices. But what happens when you have to dynamically populate that list with data from an external API? That&#39;s where the dynamic-data-form-extension plugin collection comes in.</p><p>The dynamic-data-form-extension plugin collection allows you to populate a &lt;Select&gt; component with data fetched from an HTTP endpoint. It&#39;s a simple, yet powerful plugin that can save you a lot of time and effort.</p><h3>Frontend Plugin (dynamic-pick-extension)</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/0*AxeyOM_mBIcLV3We.gif" /><figcaption><a href="https://www.loom.com/share/9de1f5c431864f19982fcdfab4cd029a">https://www.loom.com/share/9de1f5c431864f19982fcdfab4cd029a</a></figcaption></figure><p>This plugin is a <a href="https://backstage.io/docs/features/software-templates/writing-custom-field-extensions">Custom Field Extension</a> that allow you to create &lt;Select&gt; components that fetches data dynamically from an endpoint. This can be used together with the form-data-backend plugin to write custom logic to fill the field.</p><h4>Installation</h4><pre>yarn add --cwd packages/app @premise/plugin-dynamic-pick-extension</pre><h4>Configuration</h4><p>Add the import to your App.tsx on the frontend package of your backstage instance:</p><pre>import { DynamicPickFieldExtension } from &#39;@premise/plugin-dynamic-pick-extension&#39;;</pre><p>Then add the imported field extension as a child of ScaffolderFieldExtensions.</p><pre>&lt;ScaffolderFieldExtensions&gt;<br>  &lt;DynamicPickFieldExtension /&gt;<br>&lt;/ScaffolderFieldExtensions&gt;</pre><h4>Usage</h4><p>To use the extension on a <a href="https://backstage.io/docs/features/software-templates/writing-templates">Backstage Template Action</a> just add the ui-field and ui-options fields to the parameter.</p><h4>Basic Usage:</h4><pre>parameters:<br>  - category:<br>      title: Category<br>      type: string<br>      ui:field: DynamicPickExtension<br>      ui:options:<br>        # IMPORTANT: The endpoint needs to return a JSON array of strings.<br>        external_data: https://dummyjson.com/products/categories</pre><h4>Using the form-data-backend plugin:</h4><pre>parameters:<br>  - team:<br>      title: Github Team to add as admin of the repository<br>      type: string<br>      ui:field: DynamicPickExtension<br>      ui:options:<br>        # This is a provider added on the form-data-backend plugin<br>        form_data: github/teams</pre><h3>Backend Plugin (form-data-backend)</h3><p>This plugin is a <a href="https://backstage.io/docs/plugins/backend-plugin">Backstage Backend Plugin</a> that allows you to write custom providers to add custom logic and build the data to be shown on the dynamic-field-extension for example for APIs that require authentication or to parse data that doesn’t come on the required JSON Array String format.</p><h4>Installation</h4><pre>yarn add --cwd packages/backend @premise/plugin-form-data-backend</pre><h4>Configuration</h4><p>Create a form-data.ts file under packages/backend/src/plugins with the following content:</p><pre>import { createRouter } from &#39;@premise/plugin-form-data-backend&#39;;<br>import { Router } from &#39;express&#39;;<br>import { PluginEnvironment } from &#39;../types&#39;;<br>import { exampleRouter } from &#39;../providers&#39;;<br>export default async function createPlugin(<br>  env: PluginEnvironment,<br>): Promise&lt;Router&gt; {<br>  return await createRouter(<br>    {<br>      logger: env.logger,<br>    },<br>    [<br>      {<br>        path: &#39;/example&#39;,<br>        router: exampleRouter,<br>      }<br>    ],<br>  );<br>}</pre><p>Create a providers folder under packages/backend/src containing all of your custom providers for example:</p><p>example.ts</p><pre>import express from &#39;express&#39;;<br>import Router from &#39;express-promise-router&#39;;<br>import { RouterOptions } from &#39;@premise/plugin-form-data-backend&#39;;<br><br>export async function exampleRouter(<br>  options: RouterOptions,<br>): Promise&lt;express.Router&gt; {<br>  const { logger } = options;<br>  const router = Router();<br><br>  // You can add in here all of the endpoints you want<br>  router.get(&#39;/ping&#39;, async (_, response) =&gt; {<br>    logger.info(&#39;Pong&#39;);<br>    response.json([&#39;pong&#39;]);<br>  });<br><br>  return router;<br>}</pre><p>Then under packages/backend/src/index.ts import the plugin:</p><pre>import formData from &#39;./plugins/form-data&#39;;<br>// …<br>const formDataEnv = useHotMemoize(module, () =&gt; createEnv(&#39;form-data&#39;));<br>// …<br>apiRouter.use(&#39;/form-data&#39;, await formData(formDataEnv));</pre><h4>Usage with dynamic-pick-extension</h4><pre>parameters:<br>  - example:<br>      title: Example<br>      type: string<br>      ui:field: DynamicPickExtension<br>      ui:options:<br>        # This is a provider added on the form-data-backend plugin<br>        form_data: example/ping</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ad8a0ae316e1" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/dynamic-data-on-backstage-templates-ad8a0ae316e1">Dynamic Data On Backstage Templates</a> was originally published in <a href="https://engineering.premise.com">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Hosting a fully Serverless Web-Based Postgres Admin Client on GCP using Pgweb, Cloud Run, & IAP]]></title>
            <link>https://engineering.premise.com/hosting-a-fully-serverless-web-based-postgres-admin-client-on-gcp-using-pgweb-cloud-run-iap-cff0ce8f471b?source=rss----c5fada0a103d---4</link>
            <guid isPermaLink="false">https://medium.com/p/cff0ce8f471b</guid>
            <category><![CDATA[cloud-sql]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[pgweb]]></category>
            <category><![CDATA[cloudrun]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <dc:creator><![CDATA[Austen Novis]]></dc:creator>
            <pubDate>Mon, 06 Mar 2023 13:17:20 GMT</pubDate>
            <atom:updated>2023-03-06T13:17:20.614Z</atom:updated>
            <content:encoded><![CDATA[<p><strong><em>By Austen Novis, Staff Software Engineer</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/993/1*58nld7no2yd0cYo27UMySA.png" /><figcaption>Pgweb example screenshot</figcaption></figure><p>There are a number of quality Postgres open source administration tools such as <a href="https://www.pgadmin.org/">pgAdmin</a> or <a href="https://dbeaver.io/">DBeaver</a>, but these tools require a persistent server to run. This means that each user needs to install the tool locally and setup their connections, or you need to host the tool in a cloud server, which can get expensive.</p><p>A much cheaper alternative is to use <a href="https://sosedoff.github.io/pgweb/">Pgweb</a>, a lightweight web-based database explorer for PostgreSQL written in Go, and deploy to <a href="https://cloud.google.com/run">Cloud Run</a>. This allows you to utilize a web-based tool, for minimal cost, that can scale for any number of users.</p><p>We will leverage several <a href="https://cloud.google.com/">Google Cloud Platform</a> services for the complete setup starting with a http load balancer to allow for a custom DNS name as well as enable <a href="https://cloud.google.com/iap">Identity Aware Proxy</a> (IAP), which is what we will be using for authentication. We will host the service in Cloud Run and connect directly to CloudSQL using a <a href="https://cloud.google.com/sql/docs/postgres/connect-connectors">CloudSQL connector</a>. You can bypass the load balancer and IAP if you would like to use username and passwords for authentication.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/626/1*5hDcCfOoiyrSn0LrE64X8w.png" /><figcaption>Cloud components</figcaption></figure><h3>Cloud Setup</h3><p>The first step is setting up your CloudSQL instance. This can be done via terraform or in the GCP console. The main requirement is that you enable a public ip address, which will allow us to use the CloudSQL Auth proxy to connect our Cloud Run instance to our CloudSQL instance. If this is not possible you can still connect to your CloudSQL instance to Cloud Run through a VPC Connector instead.</p><p>Next we will need to save our database connection credentials in GCP’s <a href="https://cloud.google.com/secret-manager">Secret Manger</a> by creating a new secret called PGWEB_DATABASE_URL in the format of postgres:///DB_NAME?host=/cloudsql/PROJECT:REGION:INSTANCE_NAME&amp;user=DB_USER&amp;password=DB_PASSWORD .</p><p>Now that we have our CloudSQL instance and connection secret we will deploy our Cloud Run instance using the <a href="https://cloud.google.com/sdk/gcloud">gcloud cli</a>. First we will need to push the desired Pgweb container to GCP’s Artifact Registry, which we can do using the following commands.</p><pre>gcloud auth configure-docker us-central1-docker.pkg.dev<br><br>export PROJECT=PROJECT<br><br>docker pull sosedoff/pgweb:0.14.0<br>docker tag sosedoff/pgweb:0.14.0  us-central1-docker.pkg.dev/$PROJECT/cloud-run-source-deploy/pgweb:0.14.0<br>docker push us-central1-docker.pkg.dev/$PROJECT/cloud-run-source-deploy/pgweb:0.14.0</pre><p>Next you can run the following gcloud command to deploy your Cloud Run instance. You will need to replace the following variables (PROJECT, SERVICE_ACCOUNT, INSTANCE_NAME).</p><pre>export PROJECT=PROJECT<br>export SERVICE_ACCOUNT=SERVICE_ACCOUNT<br>export INSTANCE_NAME=INSTANCE_NAME<br><br>gcloud alpha run deploy pgweb --set-secrets PGWEB_DATABASE_URL=PGWEB_DATABASE_URL:latest --no-allow-unauthenticated --project=$PROJECT --region=us-central1 --platform managed --service-account=$SEVICE_ACCOUNT --min-instances=1 --max-instances=1 --port=8081 --image=us-central1-docker.pkg.dev/$PROJECT/cloud-run-source-deploy/pgweb:0.14.0 --set-cloudsql-instances=$PROJECT:us-central1:$INSTANCE_NAME</pre><p>Note that your service account will need CloudSQL user permissions for your instance. The min instance is set to 1 to make sure that an instance is always running.</p><p>The next step is deploying the HTTP Load Balancer. This can be deploying in the console, by selecting a global load balancer and creating a route to your Cloud Run instance.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9m8iArjHLSw8wpFpu3uUHQ.png" /><figcaption>Load Balancer Sceenshot</figcaption></figure><p>Once this is created you will need to go to the IAP page in the GCP console and enable IAP on your http load balancer. If this is your first time using IAP you will need to follow the IAP setup steps, which requires adding a firewall rule to your project.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*b1zRjutMjs4IcjY2DQTH6A.png" /><figcaption>IAP Screenshot</figcaption></figure><p>In order to grant users permission to Pgweb, you will need to grant specific users <strong>IAP-Secured Web App User </strong>permission in IAP.</p><p>An additional step to make it easier on users to access is to setup a DNS record to point to your load balancer. For example you can create a pgweb.DOMAIN.com DNS record to point to the ip address of your load balancer.</p><h3>Conclusion</h3><p>We now have a way to deploy a lightweight web-based database explorer for PostgreSQL hosted on Cloud Run. This is great for rapid development and keeping costs low.</p><p>If you enjoyed this blog, then check out our other <a href="https://github.com/premisedata/gcp-tutorials">tutorials</a> or explore additional content on our newly launched <a href="https://engineering.premise.com/">engineer blog</a>!</p><p><strong><em>Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our </em></strong><a href="https://bit.ly/3wnTrLn"><strong><em>Careers</em></strong></a><strong><em> page.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cff0ce8f471b" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/hosting-a-fully-serverless-web-based-postgres-admin-client-on-gcp-using-pgweb-cloud-run-iap-cff0ce8f471b">Hosting a fully Serverless Web-Based Postgres Admin Client on GCP using Pgweb, Cloud Run, &amp; IAP</a> was originally published in <a href="https://engineering.premise.com">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Tutorial: Connecting Cloudrun and Cloudfunctions to Redis and other Private Services using Goblet]]></title>
            <link>https://engineering.premise.com/tutorial-connecting-cloudrun-and-cloudfunctions-to-redis-and-other-private-services-using-goblet-5782f80da6a0?source=rss----c5fada0a103d---4</link>
            <guid isPermaLink="false">https://medium.com/p/5782f80da6a0</guid>
            <category><![CDATA[python]]></category>
            <category><![CDATA[gcp]]></category>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[google-cloud-run]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <dc:creator><![CDATA[Qua Jones]]></dc:creator>
            <pubDate>Mon, 13 Feb 2023 16:38:05 GMT</pubDate>
            <atom:updated>2023-02-13T16:38:05.880Z</atom:updated>
            <content:encoded><![CDATA[<p>By Qua Jones, Senior Software Engineer</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KeRzOCkgAgKofagb" /><figcaption>Photo by <a href="https://unsplash.com/@othentikisra?utm_source=medium&amp;utm_medium=referral">israel palacio</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Google Cloud Platform’s (GCP) <a href="https://cloud.google.com/vpc/docs/private-services-access">Private Services Access</a> allows you to connect to GCP services without an external IP being assigned. <a href="https://cloud.google.com/memorystore/docs/redis/redis-overview">Redis</a> is an in-memory DB that is used for caching and is a service that supports private access. In order to take advantage of this feature, a private connection is necessary. For serverless environments, this can be done using a <a href="https://cloud.google.com/vpc/docs/serverless-vpc-access">VPC Connector</a>.</p><p>VPC Connectors allow serverless environments to connect to <a href="https://cloud.google.com/vpc/docs/vpc">VPC Networks</a> by handling traffic between the network and environment, restricting requests to use only internal IP addresses. This can be beneficial for applications that do not require exposure to the public internet or if latency for requests across services needs to be improved.</p><p>Currently you can provision a connector using the console, cli, or terraform but you will still need to configure your serverless environment to utilize this connector. Instead of manually going through the segmented process of creating a connector and updating your environment, you can leverage Goblet to provision your connector and inject needed configuration values into your deployment.</p><p>This tutorial will walk you through the steps of deploying a Cloud Run service that privately connects to a Redis Instance using a VPC Connector.</p><h4><strong>Prerequisites:</strong></h4><ul><li>GCP Account</li><li>Python Environment (≥ 3.7)</li><li>Gcloud CLI</li></ul><p>You will also need to have goblet and redis installed which can be done by running pip install goblet-gcp &amp;&amp; pip install redis</p><h4><strong>Getting Started:</strong></h4><p>Ensure you have credentials configured by running gcloud auth login and sign in to the desired project. Make sure to have the correct services enabled in your GCP project which include:</p><ul><li>Cloud Run</li><li>Cloud Build</li><li>Artifact Registry</li><li>Serverless VPC Access</li><li>Memorystore for Redis</li></ul><p>To get started, clone the code from the <a href="https://github.com/premisedata/gcp-tutorials">gcp-tutorials</a> repository and navigate to the directory. This can be done with the Github CLI by running:</p><pre>gh repo clone premisedata/gcp-tutorials/014-goblet-private-services</pre><h4>Configuring the VPC Connector and Redis Instance:</h4><p>Adding a VPC Connector and Redis Instance to your Goblet deploy is as simple as:</p><pre>app.vpcconnector(&quot;goblet-vpcconnector&quot;)<br>app.redis(&quot;goblet-redis&quot;)</pre><p>For Redis configuration, update the PROJECT in the authorizedNetwork field within the redis key of theconfig.json file in the .goblet folder.</p><p>The default range for the VPC Connector is 10.32.1.0/28 if this range is already taken by another VPC connector, update the ipCidrRange field within the vpcconnector key of the config.json file in the .goblet folder</p><h4>Deploying the VPC Connector and Redis Instance:</h4><p>Deploying your infrastructure and backend can be done with:</p><pre>goblet deploy -p PROJECT -l LOCATION</pre><blockquote>To deploy just the VPC connector and Redis Instance add the <em>--skip-backend</em> flag</blockquote><p>After a few minutes you will have the following:</p><ul><li>Redis Instance with private service access connection mode enabled</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*szRId-YSKkcbLgZEftCH7g.png" /><figcaption>Memorystore for Redis UI</figcaption></figure><ul><li>VPC Connector for your default network</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2RFdiL4m38UvKSCo7214xw.png" /><figcaption>Serverless VPC Access UI</figcaption></figure><ul><li>Cloud Run instance that is connected to your <strong><em>default </em></strong>VPC Network through the connector with environment variables set for your Redis HOST, PORT, and INSTANCE_NAME.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ASGmaCmPJv-vyRQHQPFcwQ.png" /><figcaption>Cloud Run UI</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4CqHrGseCe3nRct1P720lA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/514/1*AUMMMZdggbO_SxhH_o0J2w.png" /></figure><p>If you want to update your local config file with your recent infrastructure updates you can easily write out the config using --write-config flag.</p><h4>Testing:</h4><p>Make sure you have Cloud Run Invoker permissions for the Cloud Run service. We will call the /redis endpoint, this will connect to our Redis instance and increment a counter every time the endpoint is hit and return the value.</p><p>The URL for your service can be found on the Cloud Run details page.</p><pre>curl $URL/redis  -H &quot;Authorization: Bearer $(gcloud auth print-identity-token)&quot;</pre><p>On first call you will see the count is set to 1, on every subsequent call this should increase for the lifespan of the Redis Instance:</p><pre>&gt; curl $URL/redis  -H &quot;Authorization: Bearer $(gcloud auth print-identity-token)&quot;<br>{&quot;Visitor number&quot;:1}<br>&gt; curl $URL/redis  -H &quot;Authorization: Bearer $(gcloud auth print-identity-token)&quot;<br>{&quot;Visitor number&quot;:2}</pre><h4>Cleaning up the VPC Connector:</h4><p>To cleanup all deployed resources, including the VPC Connector and Redis instance you can run the goblet destroy command:</p><pre>goblet destroy -p PROJECT -l LOCATION</pre><h3>Conclusion:</h3><p>We now have an extremely simple way to deploy infrastructure for your serverless environment. By leveraging VPC Connectors you can easily connect to a Redis instance using private access, improving the security of your application. Check out the full <a href="https://goblet.github.io/goblet/">Goblet documentation</a> for more information on supported backends, infrastructures, and resources. If you enjoyed this blog, then check out our other <a href="https://github.com/premisedata/gcp-tutorials">tutorials</a> or explore additional content on our newly launched <a href="https://engineering.premise.com/">engineer blog</a>!</p><p><strong><em>Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our </em></strong><a href="https://bit.ly/3wnTrLn"><strong><em>Careers</em></strong></a><strong><em> page.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5782f80da6a0" width="1" height="1" alt=""><hr><p><a href="https://engineering.premise.com/tutorial-connecting-cloudrun-and-cloudfunctions-to-redis-and-other-private-services-using-goblet-5782f80da6a0">Tutorial: Connecting Cloudrun and Cloudfunctions to Redis and other Private Services using Goblet</a> was originally published in <a href="https://engineering.premise.com">Engineering at Premise</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>