<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nirmal Chhodvadiya</title>
    <description>The latest articles on DEV Community by Nirmal Chhodvadiya (@nirmalpatel008).</description>
    <link>https://dev.to/nirmalpatel008</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F258365%2F1d41c732-bf02-4080-a5b7-f6c8c5b13211.jpg</url>
      <title>DEV Community: Nirmal Chhodvadiya</title>
      <link>https://dev.to/nirmalpatel008</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nirmalpatel008"/>
    <language>en</language>
    <item>
      <title>One manifest, two clouds: multi-cloud infrastructure with stackql-deploy</title>
      <dc:creator>Nirmal Chhodvadiya</dc:creator>
      <pubDate>Mon, 28 Sep 2026 15:44:16 +0000</pubDate>
      <link>https://dev.to/nirmalpatel008/one-manifest-two-clouds-multi-cloud-infrastructure-with-stackql-deploy-358j</link>
      <guid>https://dev.to/nirmalpatel008/one-manifest-two-clouds-multi-cloud-infrastructure-with-stackql-deploy-358j</guid>
      <description>&lt;p&gt;Tutorial 3 introduced stackql-deploy against a single Google Cloud project. One manifest, one provider, one VPC. The framework proved its shape: declarative resources described as SQL-anchored files, an exists-create-statecheck-exports loop that runs idempotently against live cloud APIs, no state file to maintain.&lt;/p&gt;

&lt;p&gt;This tutorial takes the same framework and puts it to work across two clouds at once. One manifest declares both a GCP VPC and an AWS VPC. One build command provisions both in the same run. The manifest becomes a single source of truth for infrastructure that spans providers, without introducing a second tool or a second DSL.&lt;/p&gt;

&lt;p&gt;Along the way, the AWS side of the walkthrough shows features that the minimal Google example does not need: per-environment values, tag-based resource identification, resource composability through exports, and provider-specific query helpers. The interesting part is not just deploying to two clouds, but managing both through the same manifest and lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What multi-cloud in one manifest looks like
&lt;/h2&gt;

&lt;p&gt;A multi-cloud stackql-deploy project is structurally the same as a single-cloud one, with two additions: multiple providers in the providers block, and one &lt;code&gt;.iql&lt;/code&gt; file per resource under either cloud. Scaffold two starter projects first, then combine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stackql-deploy init multi-cloud-stack &lt;span class="nt"&gt;--provider&lt;/span&gt; google
stackql-deploy init aws-stack &lt;span class="nt"&gt;--provider&lt;/span&gt; aws
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merge the resulting manifests and copy both resource files into a single project. The combined manifest structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;multi-cloud-stack"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GCP&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VPC&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AWS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VPC&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;one&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;deployment"&lt;/span&gt;
&lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;google&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;awscc&lt;/span&gt;
&lt;span class="na"&gt;globals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;project&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;MY_PROJECT_NAME&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region_aws&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AWS_REGION&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;global_tags&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stackql:stack-name'&lt;/span&gt;
        &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stack_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stackql:stack-env'&lt;/span&gt;
        &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stack_env&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcp_vpc&lt;/span&gt;
    &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vpc_name&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stack_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stack_env&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-gcp-vpc"&lt;/span&gt;
    &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;vpc_name&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;vpc_link&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws_vpc&lt;/span&gt;
    &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vpc_cidr_block&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;prd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.0.0/16"&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;sit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.1.0.0/16"&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.2.0.0/16"&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vpc_tags&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Name&lt;/span&gt;
            &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stack_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stack_env&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-aws-vpc"&lt;/span&gt;
        &lt;span class="na"&gt;merge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;global_tags'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;vpc_id&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;vpc_cidr_block&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two details to notice in the manifest. First, the provider list carries two entries, and the framework loads both before processing the resources. Second, the AWS resource declares per-environment CIDRs through a values block, while the GCP resource uses a simpler single-value pattern. Using an AWS-specific variable name (&lt;code&gt;region_aws&lt;/code&gt;) also avoids collisions when provider-specific settings share generic names such as region.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GCP side
&lt;/h2&gt;

&lt;p&gt;The GCP resource file (&lt;code&gt;resources/gcp_vpc.iql&lt;/code&gt;) follows the same shape covered in Tutorial 3. The exists check queries &lt;code&gt;google.compute.networks&lt;/code&gt; by name. The create anchor INSERTs into the same view. For this provider method, the request-body fields use the &lt;code&gt;data__&lt;/code&gt; prefix while &lt;code&gt;project&lt;/code&gt; stays plain because it maps to the URL path. Statecheck verifies &lt;code&gt;autoCreateSubnetworks&lt;/code&gt;, and delete removes the network.&lt;/p&gt;

&lt;p&gt;For readers who want the full anchor breakdown, Tutorial 3 walks through each block. Here the point is that adding a GCP resource to a multi-cloud stack takes exactly one &lt;code&gt;.iql&lt;/code&gt; file. No coordination code, no shared state, no cross-provider glue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AWS side
&lt;/h2&gt;

&lt;p&gt;The AWS resource file (&lt;code&gt;resources/aws_vpc.iql&lt;/code&gt;) uses the awscc provider, which wraps AWS Cloud Control API. The anchor structure is the same as GCP, but the queries have real differences worth walking through.&lt;/p&gt;

&lt;p&gt;The exists check for AWS uses tags as the resource identifier, not names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="cm"&gt;/*+ exists */&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;tagged_resources&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;split_part&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ResourceARN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;vpc_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;awscc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tagging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tagged_resources&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{{ region_aws }}'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;TagFilters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{{ global_tags | to_aws_tag_filters }}'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;ResourceTypeFilters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'["ec2:vpc"]'&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;vpcs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;vpc_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;awscc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vpcs_list_only&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{{ region_aws }}'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vpc_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vpcs&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;tagged_resources&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vpc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vpc_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a JOIN between two AWS API surfaces. The &lt;code&gt;tagged_resources&lt;/code&gt; view returns anything with matching stackql tags. The &lt;code&gt;vpcs_list_only&lt;/code&gt; view returns VPCs in the region. The inner join returns only VPCs that match both, which is how the framework asks "has our stack already put a VPC here?" without needing to remember an ID from a previous run. The stack tags identify the VPC, so the framework does not need to persist the VPC ID from a previous run.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;to_aws_tag_filters&lt;/code&gt; filter is a stackql helper that turns the manifest's tag list into the JSON shape the AWS tagging API expects. Provider-specific helpers like this keep the manifest declarative without leaking wire-format details into the resource files.&lt;/p&gt;

&lt;p&gt;The create INSERT uses direct column names (no &lt;code&gt;data__&lt;/code&gt; prefix) for this awscc method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="cm"&gt;/*+ create */&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;awscc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vpcs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;CidrBlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;EnableDnsSupport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;EnableDnsHostnames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;region&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="s1"&gt;'{{ vpc_cidr_block }}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'{{ vpc_tags }}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'{{ region_aws }}'&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AWS resource also demonstrates two useful features. The &lt;code&gt;vpc_cidr_block&lt;/code&gt; value comes from the per-environment values block in the manifest, so this same create runs different CIDRs in dev, sit, and prd. The &lt;code&gt;RETURNING *&lt;/code&gt; clause captures the created resource's fields into the framework's context, which is how downstream anchors can reference &lt;code&gt;{{ this.vpc_id }}&lt;/code&gt; without another query.&lt;/p&gt;

&lt;p&gt;Statecheck uses a custom awscc function to compare tag policies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="cm"&gt;/*+ statecheck, retries=5, retry_delay=5 */&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;AWS_POLICY_EQUAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{{ vpc_tags }}'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;test_tags&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;awscc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vpcs&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Identifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{{ this.vpc_id }}'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{{ region_aws }}'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;cidr_block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{{ vpc_cidr_block }}'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;test_tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AWS_POLICY_EQUAL&lt;/code&gt; is a provider-specific helper used here to compare the returned tags with the desired tag policy. It keeps the statecheck compact instead of expanding the tag comparison into a much larger SQL expression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preview with a dry run
&lt;/h2&gt;

&lt;p&gt;With both AWS and GCP credentials available in your environment, render the full multi-cloud plan first. The dry run resolves variables and prints the provider-specific SQL without creating resources.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stackql-deploy build multi-cloud-stack dev &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MY_PROJECT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-gcp-project &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ap-south-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the validated run, stackql-deploy loaded both &lt;code&gt;google&lt;/code&gt; and &lt;code&gt;awscc&lt;/code&gt;, rendered the GCP VPC queries with the method-specific &lt;code&gt;data__&lt;/code&gt; fields, rendered the AWS tag-based lookup and VPC INSERT for &lt;code&gt;ap-south-1&lt;/code&gt;, and finished with &lt;code&gt;dry-run build complete&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2For2t3390q23cwo1b5qdm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2For2t3390q23cwo1b5qdm.png" alt="Dry run part 1 output: stackql-deploy loads both google and awscc providers, then renders the GCP VPC exists and create queries with data__ prefixed body fields" width="800" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Dry run, part 1: both providers load and the GCP resource renders without creating anything.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyd9lmcn5rrhn3u127rul.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyd9lmcn5rrhn3u127rul.png" alt="Dry run part 2 output: AWS aws_vpc renders with tag-based exists JOIN and INSERT into awscc.ec2.vpcs for ap-south-1, ending with dry-run build complete" width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Dry run, part 2: AWS queries render with ap-south-1, then the run ends with dry-run build complete.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the real multi-cloud build
&lt;/h2&gt;

&lt;p&gt;After the dry run looks right, run the same stack without &lt;code&gt;--dry-run&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stackql-deploy build multi-cloud-stack dev &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MY_PROJECT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-gcp-project &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ap-south-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first validated build created both VPCs in one deployment. GCP completed the exists → create → statecheck → exports sequence, followed by AWS. The AWS exists query retried while Cloud Control finished provisioning, then captured the VPC ID. The full multi-cloud build completed successfully in 13.39 seconds in the captured run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyb6wpof2zqkfgrx5eep6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyb6wpof2zqkfgrx5eep6.png" alt="First real multi-cloud build output: gcp_vpc completes exists check, create, statecheck, and exports; then aws_vpc completes the same flow with tag-based exists retry catching the async Cloud Control creation; deployment completed in 13.39 seconds with build complete" width="799" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;First real build: both gcp_vpc and aws_vpc are created, verified, exported, and the build completes.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it again: verify idempotence
&lt;/h2&gt;

&lt;p&gt;Run the same build command again without changing the manifest. On the second validated run, both VPCs already existed, both statechecks passed, and there was no create step for either resource. The same AWS VPC ID was reused and the build completed in 4.69 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9e3s6rcpd4myrwdu6qkq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9e3s6rcpd4myrwdu6qkq.png" alt="Second stackql-deploy build output demonstrating idempotence: both gcp_vpc and aws_vpc already exist, statechecks pass on the first attempt, no create operations run, deployment completed in 4.69 seconds" width="799" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Second build: both resources already exist, statechecks pass, and nothing is recreated.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tear down both clouds
&lt;/h2&gt;

&lt;p&gt;The same project can clean up both resources with one teardown command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stackql-deploy teardown multi-cloud-stack dev &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MY_PROJECT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-gcp-project &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ap-south-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The validated teardown collected the resource exports, deleted the AWS VPC first, then deleted the GCP VPC, and used post-delete checks to confirm that both resources were gone. The run finished with &lt;code&gt;teardown complete&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc4v8qzoifoxojxvvbey0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc4v8qzoifoxojxvvbey0.png" alt="Teardown output showing exports collected for both resources, aws_vpc de-provisioned and confirmed deleted, then gcp_vpc de-provisioned with post-delete checks retrying until confirmed deleted, ending with teardown complete in 35.56 seconds" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Teardown: both cloud resources are deleted and confirmed absent.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When Cloud Control operations run async
&lt;/h2&gt;

&lt;p&gt;AWS Cloud Control operations can complete asynchronously. In this example, the create request returns before the VPC is immediately discoverable through the tag-based exists query, so the resource definition retries the check while the API finishes provisioning.&lt;/p&gt;

&lt;p&gt;The stackql-deploy framework handles that timing through retry settings on the exists and state checks. In this AWS resource, &lt;code&gt;retries=5&lt;/code&gt; with &lt;code&gt;retry_delay=5&lt;/code&gt; causes repeated checks at five-second intervals. Slower resources may need larger retry windows based on their normal provisioning time.&lt;/p&gt;

&lt;p&gt;If the exists check exhausts its retries after a create, the framework reports the resource as failed. When that happens, the create might still be running in the background. To check the actual status of a stuck Cloud Control operation, query it directly through AWS CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudcontrol list-resource-requests &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-request-status-filter&lt;/span&gt; &lt;span class="s2"&gt;"OperationStatuses=FAILED,Operations=CREATE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows recent failed CREATE requests with their &lt;code&gt;StatusMessage&lt;/code&gt; and &lt;code&gt;ErrorCode&lt;/code&gt;. That is useful when the build does not complete because the underlying Cloud Control operation was rejected or is still progressing. Common causes include quota limits, IAM permission gaps, and parameter validation errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this enables
&lt;/h2&gt;

&lt;p&gt;This run demonstrates that AWS and GCP resources can use the same deployment lifecycle while keeping their provider-specific SQL separate.&lt;/p&gt;

&lt;p&gt;Environment values, tags, exports, state checks, and teardown remain in one project, while each &lt;code&gt;.iql&lt;/code&gt; file reflects the API semantics of its provider. The second build demonstrated the idempotent path: both VPCs already existed, their statechecks passed, and neither resource was recreated.&lt;/p&gt;

&lt;p&gt;Instead of maintaining separate provisioning scripts for each cloud, the project keeps the resource definitions, environment-specific values, tags, and exports in one manifest-driven workflow.&lt;/p&gt;

&lt;p&gt;The same pattern can be extended to additional stackql providers, subject to the capabilities and method contracts of each provider. Provider-specific SQL stays in each resource file while the lifecycle remains consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits in the series
&lt;/h2&gt;

&lt;p&gt;Tutorial 1 introduced stackql for querying live cloud state across multiple clouds with SQL. Tutorial 2 showed how the same query engine lets agents read live state before mutating anything, one query at a time. The Actions guide put the audit pattern in CI. Tutorial 3 introduced stackql-deploy as the framework that operationalizes the query-before-mutation pattern for a full cloud stack. This tutorial takes that framework across two clouds at once, showing what a real multi-cloud manifest looks like and how the framework handles the different provider semantics under the same lifecycle.&lt;/p&gt;

&lt;p&gt;If the pattern makes sense to you, star the &lt;a href="https://github.com/stackql/stackql" rel="noopener noreferrer"&gt;stackql repository&lt;/a&gt; so more developers find it. Full documentation, more manifest examples, and provider-specific guides are at stackql-deploy.io.&lt;/p&gt;

&lt;p&gt;This closes the series for now. Cloud infrastructure exposes structured APIs that can be queried and changed declaratively. Across the four tutorials, the same query layer was used to audit live cloud state, gate mutations with SQL checks, run continuous audit checks in CI, and declare a tested AWS + GCP stack through manifest-plus-anchored queries.&lt;/p&gt;

</description>
      <category>stackql</category>
      <category>terraform</category>
      <category>aws</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Declarative cloud stacks in SQL: an intro to stackql-deploy</title>
      <dc:creator>Nirmal Chhodvadiya</dc:creator>
      <pubDate>Thu, 17 Sep 2026 04:03:49 +0000</pubDate>
      <link>https://dev.to/nirmalpatel008/declarative-cloud-stacks-in-sql-an-intro-to-stackql-deploy-41i</link>
      <guid>https://dev.to/nirmalpatel008/declarative-cloud-stacks-in-sql-an-intro-to-stackql-deploy-41i</guid>
      <description>&lt;p&gt;Tutorial 2 in this series showed the query-before-mutation pattern by hand. Query live state, apply a policy gate, mutate what's out of policy, verify convergence. Every step was a SQL statement typed into a shell. The pattern held, but the shape was low-level. One resource at a time. One session at a time. No structure for repeating it across environments or holding it right over time.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stackql-deploy&lt;/code&gt; takes that pattern and turns it into a framework. Define your resources once as SQL-anchored files, declare them in a manifest, and let the framework run the exists check, the create-or-update, the state check, and the exports as a single build. Same underlying pattern, now something a team can operate at scale.&lt;/p&gt;

&lt;p&gt;This tutorial walks through the tool end to end. What it is, how a project is shaped, how a single resource is defined, and how the whole thing runs against a real Google Cloud project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stackql-deploy is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;stackql-deploy&lt;/code&gt; is a model-driven deployment framework built on top of stackql. You describe cloud resources as SQL query files (each with a few named blocks called anchors), collect them in a manifest, and run a single command to converge your cloud state to what the manifest declares. It's inspired by dbt in shape: a project directory, models as files, environments as configurations, and a small CLI that operates on the whole thing.&lt;/p&gt;

&lt;p&gt;Three properties are worth naming up front.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Declarative.&lt;/strong&gt; You describe what you want to exist. The tool figures out whether to create, update, or leave alone based on live queries against the cloud API, not a cached state file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotent by design.&lt;/strong&gt; Every resource has an exists check and an optional state check. The tool skips resources that already match, updates what has drifted, and creates what is missing. Running build twice in a row produces the same converged state, but only the first run actually mutates anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provider-agnostic through the stackql query layer.&lt;/strong&gt; Because everything under the hood is stackql SQL against real cloud APIs, the same tool can deploy AWS, GCP, Azure, Snowflake, Databricks, or any other provider stackql supports. One framework, one query grammar, many clouds.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a project is shaped
&lt;/h2&gt;

&lt;p&gt;A stackql-deploy project is a directory with a manifest at the root and one &lt;code&gt;.iql&lt;/code&gt; file per resource. Scaffolding a new project uses the &lt;code&gt;init&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stackql-deploy init example-stack &lt;span class="nt"&gt;--provider&lt;/span&gt; google
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a directory shaped like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example-stack/
├── stackql_manifest.yml
└── resources/
    └── example_vpc.iql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The manifest declares the providers, the global variables, and the resources the stack contains. Here is a minimal one for a single VPC network in GCP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;example-stack"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;description for "example-stack"&lt;/span&gt;
&lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;google&lt;/span&gt;
&lt;span class="na"&gt;globals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;project&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;MY_PROJECT_NAME&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;australia-southeast1&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example_vpc&lt;/span&gt;
    &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vpc_name&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stack_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stack_env&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-vpc"&lt;/span&gt;
    &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;vpc_name&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;vpc_link&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The globals block holds values that apply across every resource. The template syntax reads environment variables or values passed at build time. The resources block lists each thing to deploy, with per-resource properties that get substituted into the SQL below.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a resource is defined
&lt;/h2&gt;

&lt;p&gt;Each &lt;code&gt;.iql&lt;/code&gt; file holds one resource. Inside the file, named blocks (anchors) declare the different query shapes the framework runs at different points in the lifecycle:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fep7udkbv21x52tfp9b2t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fep7udkbv21x52tfp9b2t.png" alt="The six anchor blocks in a stackql-deploy resource file: exists, create, statecheck, exports, delete" width="799" height="631"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read the anchors in order. &lt;code&gt;exists&lt;/code&gt; is a live query that answers whether the resource is already there. &lt;code&gt;create&lt;/code&gt; is the mutation to run if it isn't. &lt;code&gt;statecheck&lt;/code&gt; runs after create to confirm the resource actually reached the desired configuration, with retries and delays for slow-to-propagate cloud APIs. &lt;code&gt;exports&lt;/code&gt; captures values to reuse in other resources within the same stack. &lt;code&gt;delete&lt;/code&gt; is the reverse mutation for teardown.&lt;/p&gt;

&lt;p&gt;A note on the INSERT syntax. The columns split between path parameters (&lt;code&gt;project&lt;/code&gt; stays plain since it maps to the URL path) and body fields (&lt;code&gt;data__name&lt;/code&gt;, &lt;code&gt;data__autoCreateSubnetworks&lt;/code&gt;, &lt;code&gt;data__routingConfig&lt;/code&gt; get the &lt;code&gt;data__&lt;/code&gt; prefix). This is method-specific rather than universal across providers, and you can confirm the exact contract for any resource with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DESCRIBE&lt;/span&gt; &lt;span class="k"&gt;METHOD&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same query-before-mutation loop Tutorial 2 walked through by hand, formalized into a resource-file structure. The framework runs it for every resource in the manifest, in dependency order, with proper handling for the states each resource can be in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running a dry-run build
&lt;/h2&gt;

&lt;p&gt;Install &lt;code&gt;stackql-deploy&lt;/code&gt; via cargo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;stackql-deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, from the parent directory of your stack, run build in dry-run mode with a stack name, an environment name, and your project ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stackql-deploy build example-stack dev &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MY_PROJECT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-project-id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The framework loads the manifest, resolves the globals with your environment variables, then walks each resource in order. In dry-run mode, it renders every query it would execute against the cloud API and reports what it would do, without actually mutating anything. All the template variables are resolved: &lt;code&gt;{{ project }}&lt;/code&gt; becomes your project ID, &lt;code&gt;{{ vpc_name }}&lt;/code&gt; becomes &lt;code&gt;example-stack-dev-vpc&lt;/code&gt;, and the routingConfig JSON is inlined into the INSERT.&lt;/p&gt;

&lt;p&gt;Dry-run is the first thing to reach for. It gives you the full execution plan without touching your cloud, which makes it the natural pattern for pull-request checks and pre-deploy previews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running for real
&lt;/h2&gt;

&lt;p&gt;Once the dry-run output looks right, drop the flag to execute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stackql-deploy build example-stack dev &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MY_PROJECT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-project-id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The framework runs the same sequence, but this time the exists check hits the cloud API, and if the resource is missing, the create INSERT is sent for real. &lt;code&gt;statecheck&lt;/code&gt; retries until GCP reports the resource has converged to the desired configuration, then exports capture the live values:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft5d5vwte3b7pn7e2wqcs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft5d5vwte3b7pn7e2wqcs.png" alt="stackql-deploy build output showing exists check, create, state check, and exports completing successfully against Google Cloud in 3.61 seconds" width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read that output top to bottom. &lt;code&gt;exists&lt;/code&gt; returned no match, so &lt;code&gt;create&lt;/code&gt; ran. Right after create, the framework re-ran the exists check and confirmed the VPC was there. &lt;code&gt;statecheck&lt;/code&gt; ran once and passed. &lt;code&gt;exports&lt;/code&gt; captured &lt;code&gt;vpc_name&lt;/code&gt; and &lt;code&gt;vpc_link&lt;/code&gt;. Total time: under 4 seconds for a single-resource stack. The same pattern scales to a stack with dozens of resources across multiple providers.&lt;/p&gt;

&lt;p&gt;For test-only runs (checks without mutations, useful in CI):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stackql-deploy &lt;span class="nb"&gt;test &lt;/span&gt;example-stack dev &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MY_PROJECT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-project-id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to reverse the whole stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stackql-deploy teardown example-stack dev &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MY_PROJECT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-project-id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this enables
&lt;/h2&gt;

&lt;p&gt;The obvious use is provisioning: describe a stack once, deploy it across dev, sit, and prod environments by changing one flag. But the deeper value is what the anchor structure enables.&lt;/p&gt;

&lt;p&gt;Because every resource has an exists check and a state check, running build against an already-deployed stack is a policy convergence run. If someone changed a firewall rule out of band, the next build detects the drift via &lt;code&gt;statecheck&lt;/code&gt; and re-applies the correct configuration. Nothing else on the stack gets touched. That's continuous convergence without needing a separate drift-detection tool.&lt;/p&gt;

&lt;p&gt;Because everything is SQL and the query grammar is the same across every provider, extending a stack from GCP-only to multi-cloud is a matter of adding new &lt;code&gt;.iql&lt;/code&gt; files and provider entries in the manifest. No new tool to learn, no separate DSL, no provider-specific templating conventions.&lt;/p&gt;

&lt;p&gt;And because the framework is CI-friendly (&lt;code&gt;test&lt;/code&gt; for validation without mutation, dry-run for previews, exit codes for pass/fail), it slots into whatever pipeline you already use to ship infrastructure changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits in the series
&lt;/h2&gt;

&lt;p&gt;Tutorial 1 introduced stackql for querying live cloud state across multiple clouds with SQL. Tutorial 2 showed how the same query engine lets agents read live state before mutating anything, one query at a time. The Actions guide put the audit pattern in CI. This tutorial shows the framework that operationalizes the whole pattern: not one query at a time, but a manifest-driven declaration of an entire cloud stack, with idempotence, environment scoping, and continuous convergence built in.&lt;/p&gt;

&lt;p&gt;If the pattern makes sense to you, star the &lt;a href="https://github.com/stackql/stackql" rel="noopener noreferrer"&gt;stackql repository&lt;/a&gt; so more developers find it. Full documentation, more manifest examples, and provider-specific guides are at stackql-deploy.io.&lt;/p&gt;

&lt;p&gt;Next in this series: authoring your own multi-cloud stack from scratch, using stackql-deploy against AWS and GCP together.&lt;/p&gt;

</description>
      <category>stackql</category>
      <category>terraform</category>
      <category>gcp</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Add continuous cloud audit to your CI/CD in ten minutes</title>
      <dc:creator>Nirmal Chhodvadiya</dc:creator>
      <pubDate>Tue, 08 Sep 2026 14:48:48 +0000</pubDate>
      <link>https://dev.to/nirmalpatel008/add-continuous-cloud-audit-to-your-cicd-in-ten-minutes-4nk2</link>
      <guid>https://dev.to/nirmalpatel008/add-continuous-cloud-audit-to-your-cicd-in-ten-minutes-4nk2</guid>
      <description>&lt;p&gt;Cloud security reviews are usually quarterly. Configuration drift happens daily. Between review cycles, buckets get created, firewall rules get loosened, service accounts get reused, and a bucket that was locked down last quarter might not be locked down today. Nothing about the review cadence catches any of it.&lt;/p&gt;

&lt;p&gt;Tutorial 1 in this series walked through auditing three clouds with &lt;code&gt;stackql&lt;/code&gt; from your terminal. Tutorial 2 showed how the same query engine lets an agent read live state before mutating anything. This guide takes the same audit pattern and puts it in your CI, so the check runs every day, or on every pull request, or every hour, without anyone remembering to run it.&lt;/p&gt;

&lt;p&gt;The tool is &lt;a href="https://github.com/stackql/stackql-audit-action" rel="noopener noreferrer"&gt;stackql-audit-action&lt;/a&gt;, a GitHub Action that runs an opinionated set of security checks against your cloud accounts and renders findings inline on the workflow run page. Setup is copy-paste. First results in under two minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it checks
&lt;/h2&gt;

&lt;p&gt;The action ships with seven built-in checks for GCP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH (22/tcp) open to the internet — HIGH&lt;/li&gt;
&lt;li&gt;RDP (3389/tcp) open to the internet — HIGH&lt;/li&gt;
&lt;li&gt;Cloud SQL instances reachable on a public IP — HIGH&lt;/li&gt;
&lt;li&gt;Compute instances with public IPs — MEDIUM&lt;/li&gt;
&lt;li&gt;Compute instances using the default service account — MEDIUM&lt;/li&gt;
&lt;li&gt;Storage buckets without uniform bucket-level access — MEDIUM&lt;/li&gt;
&lt;li&gt;Default VPC network exists — MEDIUM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Similar built-in check sets exist for AWS and Azure. A provider only runs if its credentials are supplied, so you can start with GCP and add the others by dropping in their secrets. This walkthrough covers GCP end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;You need three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A GitHub repository&lt;/li&gt;
&lt;li&gt;A GCP project you're authorized to audit&lt;/li&gt;
&lt;li&gt;A Google Cloud service account JSON key with read access to Compute, Cloud SQL, Storage, and IAM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store the service account key as a GitHub secret. In your repository, go to &lt;strong&gt;Settings → Secrets and variables → Actions → New repository secret&lt;/strong&gt;. Name the secret &lt;code&gt;GCP_SA_JSON&lt;/code&gt; and paste the full JSON key as the value.&lt;/p&gt;

&lt;p&gt;Never commit credentials to the repo. Never share a key JSON on Slack or email. Rotate the key if you ever paste one somewhere by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the workflow
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/cloud-audit.yml&lt;/code&gt; with this content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GCP Audit&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;project-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GCP project ID to audit&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;fail-on-severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Severity threshold to fail on (CRITICAL/HIGH/MEDIUM/LOW/NONE)&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HIGH&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;audit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stackql/stackql-audit-action@v0.1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;project-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.project-id }}&lt;/span&gt;
          &lt;span class="na"&gt;gcp-credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GCP_SA_JSON }}&lt;/span&gt;
          &lt;span class="na"&gt;fail-on-severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.fail-on-severity }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth noting. The action is pinned to &lt;code&gt;@v0.1&lt;/code&gt; for reproducibility. If you use &lt;code&gt;@main&lt;/code&gt;, tomorrow's audit runs against tomorrow's version, which can change without warning. Pin to a real tag or a commit SHA. The &lt;code&gt;fail-on-severity&lt;/code&gt; input controls when the workflow fails. For a first run, set it to &lt;code&gt;NONE&lt;/code&gt; so the workflow completes even if findings surface. Once you've reviewed the output and confirmed the checks match your expectations, raise it to &lt;code&gt;HIGH&lt;/code&gt; or &lt;code&gt;MEDIUM&lt;/code&gt; as an enforcement gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the audit
&lt;/h2&gt;

&lt;p&gt;Commit and push. Then in your repository, go to &lt;strong&gt;Actions → GCP Audit → Run workflow&lt;/strong&gt;. Fill in the project ID and start the run. First run takes about 20-30 seconds.&lt;/p&gt;

&lt;p&gt;When the run completes, click into it and scroll down. The audit summary renders inline as a markdown table on the workflow run page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdknblp0qw41lxo671o7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdknblp0qw41lxo671o7n.png" alt="StackQL GCP audit summary rendered inline on the GitHub Actions workflow run page" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every check reports its status. Green checkmarks mean the check ran and found nothing. Yellow (MEDIUM) or red (HIGH) markers mean findings surfaced, with the affected resource names listed in a table and a short remediation note underneath.&lt;/p&gt;

&lt;p&gt;In this test run, the audit surfaced nine medium-severity findings across three categories: one Compute instance using the default service account, one Compute instance with a public IP, and seven buckets without uniform bucket-level access. The other four checks ran and returned no findings. That distinction matters. A check running cleanly is signal too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it continuous
&lt;/h2&gt;

&lt;p&gt;Manual triggers are for testing. For continuous audit, change the trigger to run on a schedule or on pull requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;9&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;   &lt;span class="c1"&gt;# every day at 09:00 UTC&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# keep manual trigger as a fallback&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;project-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GCP project ID to audit&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scheduled run catches drift that happens outside your PR flow. The pull-request run catches policy violations before they merge. Together they cover both the intentional and the accidental changes that can widen your attack surface between security reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending it with your own checks
&lt;/h2&gt;

&lt;p&gt;The seven built-in checks are a starting point, not the full set your organization cares about. The action lets you point at your own directory of check definitions, one YAML file per check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-org-firewall-check&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow only known source ranges&lt;/span&gt;
&lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HIGH&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Catch firewall rules with sourceRanges outside our corp CIDRs.&lt;/span&gt;
&lt;span class="na"&gt;remediation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restrict to known CIDRs or migrate to IAP.&lt;/span&gt;
&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;SELECT name, network, sourceRanges&lt;/span&gt;
  &lt;span class="s"&gt;FROM google.compute.firewalls&lt;/span&gt;
  &lt;span class="s"&gt;WHERE project = '${PROJECT_ID}'&lt;/span&gt;
    &lt;span class="s"&gt;AND direction = 'INGRESS'&lt;/span&gt;
&lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;network&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;sourceRanges&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A query returning zero rows means no findings. Any rows returned become finding rows. Every check is a small YAML file, so extending the audit to cover your organization's specific policies is one file per rule. See the &lt;code&gt;queries/&lt;/code&gt; directory in the action repository for the built-in checks as reference implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;Tutorial 1 in this series introduced the audit pattern manually: SELECT across three clouds, spot the risky resources, know what to fix. Tutorial 2 introduced query-before-mutation: read live state before an agent acts, apply a policy gate, converge safely. This guide connects the two. The audit runs continuously in CI. When something drifts, the query-before-mutation pattern is how an agent or an operator brings it back into policy.&lt;/p&gt;

&lt;p&gt;If the pattern makes sense to you, star the &lt;a href="https://github.com/stackql/stackql-audit-action" rel="noopener noreferrer"&gt;stackql-audit-action repository&lt;/a&gt; so more developers can find it. And if you have suggestions for checks that belong in the built-in set, the &lt;code&gt;queries/&lt;/code&gt; directory takes pull requests.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>devops</category>
      <category>security</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Query before you mutate: how agents should touch your infrastructure</title>
      <dc:creator>Nirmal Chhodvadiya</dc:creator>
      <pubDate>Tue, 01 Sep 2026 09:53:58 +0000</pubDate>
      <link>https://dev.to/nirmalpatel008/query-before-you-mutate-how-agents-should-touch-your-infrastructure-1h25</link>
      <guid>https://dev.to/nirmalpatel008/query-before-you-mutate-how-agents-should-touch-your-infrastructure-1h25</guid>
      <description>&lt;h2&gt;
  
  
  The problem with plan, review, apply
&lt;/h2&gt;

&lt;p&gt;Infrastructure-as-code was designed around human-speed changes: write a plan, review it, apply it, and use state snapshots to know what was managed before. That works best when writers are few and changes are infrequent.&lt;/p&gt;

&lt;p&gt;Agents change those assumptions. They run continuously and multiple actors can touch the same resources at the same time. Drift is no longer exceptional. It is unavoidable and relentless.&lt;/p&gt;

&lt;p&gt;Canonical IaC tools like Terraform still matter because they represent intent. What's weakened is their ability to reflect current reality. Agents rarely own the whole picture. They usually operate in narrow scopes, working on a slice of infrastructure that other agents, other tools, or humans are also touching. In that world, an agent will regularly encounter infrastructure that was mutated out of band, outside whatever IaC system nominally manages it.&lt;/p&gt;

&lt;p&gt;Query-before-mutation handles this case. Read live cloud state, compare it with policy, apply a bounded policy gate, mutate only what is out of policy, then verify. Each run starts from reality rather than a cached view.&lt;/p&gt;

&lt;p&gt;The demo takes about ten minutes and uses Google Cloud Storage bucket encryption, but the pattern is provider-agnostic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query before mutation
&lt;/h2&gt;

&lt;p&gt;Correctness comes from querying live state before acting. Safety comes from something separate: policy gates that bound what a single run is allowed to do. Location locks, resource caps, budget ceilings, allowlists. Two patterns, two jobs. Agentic query-before-mutation complements centralised IaC approaches leveraging snapshots of state. Policy gates replace the review meeting.&lt;/p&gt;

&lt;p&gt;The combined loop is correct by default because it reads reality, and safe by default because it cannot do more than the gate allows. Agents can run it continuously. Humans can run it manually. The output converges either way.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stackql&lt;/code&gt; implements this pattern. Any resource on any supported cloud can be read with SQL and mutated with SQL where the provider exposes mutation methods. Kubernetes solved reconciliation inside the cluster, but nothing reconciles across your cloud. That gap is where this pattern belongs. The demo uses Google Cloud Storage because the mutation surface is clean, but the shape is the same for AWS S3, Vault secrets, GitHub org settings, or anything else an agent needs to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;You need three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker (to run &lt;code&gt;stackql&lt;/code&gt; without installing anything else)&lt;/li&gt;
&lt;li&gt;A Google Cloud project with credentials that can read buckets and update encryption config, plus an existing Cloud KMS key you want to enforce&lt;/li&gt;
&lt;li&gt;About ten minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clone the tutorial folder from the &lt;code&gt;stackql&lt;/code&gt; repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/stackql/stackql.git
&lt;span class="nb"&gt;cd &lt;/span&gt;stackql/docs/tutorials/query-before-mutation-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the sample environment file and add your service account credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.sample .env
&lt;span class="c"&gt;# Then edit .env to set:&lt;/span&gt;
&lt;span class="c"&gt;# GOOGLE_CREDENTIALS=&amp;lt;service-account-json-on-one-line&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bring up an interactive &lt;code&gt;stackql&lt;/code&gt; shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--entrypoint&lt;/span&gt; bash stackql
&lt;span class="c"&gt;# then inside the container:&lt;/span&gt;
stackql shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pull the Google provider from the registry. This only needs to happen once per shell session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;REGISTRY&lt;/span&gt; &lt;span class="n"&gt;PULL&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt; &lt;span class="n"&gt;v26&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;07&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00432&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query the live state
&lt;/h2&gt;

&lt;p&gt;Ask the GCS API which buckets exist and what encryption is currently applied to each.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encryption&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buckets&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your-project-id'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output on a project with a mix of encryption configurations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| name                       | location | encryption                                    |
| demo-app-bucket1           | US       | null                                          |
| demo-app-bucket2           | US       | null                                          |
| stackql-demo-src-bucket    | US       | null                                          |
| stackql-encrypted-bucket-1 | US       | {"defaultKmsKeyName":"projects/.../keys/..."} |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three buckets show encryption as null, meaning Google-managed encryption keys (GMEK), the default. One already uses a customer-managed key. Under a policy that requires customer-managed keys, three buckets are out of policy.&lt;/p&gt;

&lt;p&gt;A state snapshot can tell you what Terraform previously configured. This query tells you what GCS reports now, regardless of who created or changed the bucket. If ten agents and two humans are all touching this project at various points during the day, the query still returns the current truth every time you run it. That's the property the pattern is built on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply a policy gate
&lt;/h2&gt;

&lt;p&gt;Three buckets don't match policy. Before mutating, the agent needs to prove it's allowed to. That's what the policy gate does.&lt;/p&gt;

&lt;p&gt;A gate is anything that bounds what a single run of the agent can do. Scope by project or location, cap the number of resources changed, or enforce an explicit allowlist. The shape depends on how much freedom the agent has and how much you're willing to lose in one bad run.&lt;/p&gt;

&lt;p&gt;For this demo, two gates cover most of the risk. The first is a location lock. The agent should only ever act on buckets in the location it was configured for. This is a WHERE clause on every query and mutation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'US'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trivial when the project's buckets are in one location. In production, agents move between locations based on task, and touching a bucket in the wrong region can mean acting on resources in an environment the agent has no business in. One clause, but it's the difference between a bounded action and an unbounded one.&lt;/p&gt;

&lt;p&gt;The second gate is a count cap. Before running a mutation across multiple buckets, ask the API how many the mutation would touch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buckets&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your-project-id'&lt;/span&gt; 
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'US'&lt;/span&gt; 
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;encryption&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the query returns 20 buckets and the agent's policy expects to be operating in a project with 3 to 5 non-compliant buckets, something has changed. Maybe an account merger, maybe a script created buckets out of band, maybe the credentials point to the wrong project entirely. The agent shouldn't proceed. It should log the discrepancy and wait for a human to look.&lt;/p&gt;

&lt;p&gt;Gates like these aren't clever. They're routine, boring, and easy to skip. They're also the reason a query-before-mutation loop can run without a human review meeting attached to every apply. The gate carries the weight the meeting used to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mutate to converge
&lt;/h2&gt;

&lt;p&gt;The gates passed. For each bucket without a customer-managed key, apply the policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buckets&lt;/span&gt; 
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;data__encryption&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{"defaultKmsKeyName":"projects/your-project-id/locations/us/keyRings/your-ring/cryptoKeys/your-key"}'&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'demo-app-bucket1'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth noting. The &lt;code&gt;data__encryption&lt;/code&gt; prefix is how &lt;code&gt;stackql&lt;/code&gt; passes fields into the request body, distinguishing them from URL parameters. The &lt;code&gt;WHERE&lt;/code&gt; clause uses &lt;code&gt;bucket&lt;/code&gt; rather than &lt;code&gt;name&lt;/code&gt;, because &lt;code&gt;bucket&lt;/code&gt; is the identifier the underlying patch method requires.&lt;/p&gt;

&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The operation was despatched successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In an agent loop, this runs as a bounded iteration over the non-compliant buckets returned by the earlier query, applying the same UPDATE to each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify convergence
&lt;/h2&gt;

&lt;p&gt;A successful mutation is not enough. Query the bucket again to verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encryption&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buckets&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'demo-app-bucket1'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| name             | encryption                                                                    |
| demo-app-bucket1 | {"defaultKmsKeyName":"projects/your-project-id/locations/us/keyRings/..."}    |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bucket converged from GMEK to the required customer-managed key.&lt;/p&gt;

&lt;p&gt;Run the loop again and something interesting happens: the mutation is a no-op. The SELECT finds no drift, the count check passes trivially, no UPDATE fires. That's idempotence by design. The agent can run every minute, every hour, every day, and it does the same right thing whether the target is already compliant or has just drifted. No state snapshot needed to remember what it did last time. Every run starts from live reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same pattern from an agent
&lt;/h2&gt;

&lt;p&gt;Everything above is you typing SQL into a shell. The same pattern maps directly to an agent through &lt;code&gt;stackql&lt;/code&gt;'s MCP server, listed in the Claude directory at &lt;a href="https://claude.ai/directory/ant.dir.gh.stackql.stackql" rel="noopener noreferrer"&gt;claude.ai/directory/ant.dir.gh.stackql.stackql&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Install it, add your GCP credentials, and ask Claude in plain English:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;List all GCS buckets in the project stackql-demo, and for each 
show whether encryption is a customer-managed key or the 
Google-managed default.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrdt153155w6m794anoz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrdt153155w6m794anoz.png" alt="Claude Desktop response showing stackql MCP query result with bucket encryption table" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude uses the &lt;code&gt;stackql&lt;/code&gt; MCP server to run the same underlying SELECT you saw earlier, then formats the result into a readable table. The query, gate, mutate, and verify loop runs the same way an agent runs it, just as reasoning steps in a conversation instead of shell commands.&lt;/p&gt;

&lt;p&gt;Notice what the agent adds that the raw shell output doesn't: it separates CMEK buckets from Google-managed default ones, calls out which KMS key each CMEK bucket uses, and flags a caveat you didn't ask about (the difference between bucket-default encryption and per-object encryption). That's the value of running the pattern through an agent rather than a script. Same query, same result, plus the reasoning layer on top.&lt;/p&gt;

&lt;p&gt;The shell walkthrough above was for you to see the pattern happen line by line. The MCP integration is how the pattern runs end to end in an actual agent's workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;Query before mutation is one instance of a broader pattern. Six patterns, really, that hold together as a working model for how agents should operate on infrastructure. Query before mutation is one. Idempotent assertions, bi-temporal views, agents as event sources, universal interfaces, and policy gates are the other five. Together they replace the assumptions the plan-review-apply model was built on.&lt;/p&gt;

&lt;p&gt;The full argument is in Jeff Aven's talk from AI Engineer Melbourne, "Treating Infrastructure as Data: Building an AI-Native Control Plane." If the pattern in this tutorial made sense to you, the talk is the map for what else fits with it.&lt;/p&gt;

&lt;p&gt;If this tutorial was useful, star the &lt;a href="https://github.com/stackql/stackql" rel="noopener noreferrer"&gt;stackql repo&lt;/a&gt;. Every star helps other developers and agents find the pattern, and helps the project climb the MCP registry where more agents can discover it.&lt;/p&gt;

&lt;p&gt;Next in this series: idempotent assertions with &lt;code&gt;stackql-deploy&lt;/code&gt;. Same shape, different pattern, applied to declarative infrastructure that runs continuously without breaking on repeat.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>mcp</category>
      <category>gcp</category>
    </item>
    <item>
      <title>Auditing three clouds without writing three scripts</title>
      <dc:creator>Nirmal Chhodvadiya</dc:creator>
      <pubDate>Tue, 18 Aug 2026 12:49:44 +0000</pubDate>
      <link>https://dev.to/nirmalpatel008/auditing-three-clouds-without-writing-three-scripts-5fhc</link>
      <guid>https://dev.to/nirmalpatel008/auditing-three-clouds-without-writing-three-scripts-5fhc</guid>
      <description>&lt;p&gt;If you've audited buckets in more than one cloud, you already know what this is like. Three consoles. Three CLIs. Three auth patterns. And a bit of glue code to pull the outputs together every time someone asks a question that spans all three.&lt;/p&gt;

&lt;p&gt;The question is usually simple. "Which buckets are public across all our clouds?" You end up running three separate scripts, then reconciling three different output shapes just to give one answer.&lt;/p&gt;

&lt;p&gt;This tutorial doesn't make that faster. It replaces it. One query, one view, all three clouds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What StackQL is, in three sentences
&lt;/h2&gt;

&lt;p&gt;StackQL is SQL for cloud APIs. You write a normal SQL query, and it makes the API calls to AWS, GCP, Azure, or dozens of other providers to get the answer back as rows and columns.&lt;/p&gt;

&lt;p&gt;The important part for this tutorial: it queries the live provider APIs on every run. There's no local database, no cache, no sync job to schedule. What you see is the current state of your cloud, right now.&lt;/p&gt;

&lt;p&gt;It exists because someone got tired of writing the same audit logic three times.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll need
&lt;/h2&gt;

&lt;p&gt;You'll need Docker installed and read-only credentials for whichever clouds you want to audit. All three are optional. If you leave a cloud's credentials blank, it gets skipped.&lt;/p&gt;

&lt;p&gt;For AWS, GCP, and Azure, the audit needs a service account or role that can list storage buckets and read their configuration. The audit-action repo has the exact permissions per provider &lt;a href="https://github.com/stackql/stackql-audit-action/blob/main/docs/required-auth.md" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the audit
&lt;/h2&gt;

&lt;p&gt;The tutorial folder has three files: a docker-compose file, an &lt;code&gt;.env.audit.example&lt;/code&gt; template, and a README. Grab the folder from &lt;a href="https://github.com/stackql/stackql/tree/main/docs/tutorials/preview-bucket-01" rel="noopener noreferrer"&gt;&lt;code&gt;docs/tutorials/preview-bucket-01/&lt;/code&gt;&lt;/a&gt; in the &lt;code&gt;stackql/stackql&lt;/code&gt; repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Copy the env template&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; ./audit/.env.audit.example ./audit/.env.audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;.env.audit&lt;/code&gt; and fill in credentials for whichever clouds you want. The variables are named for what they are: &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;, &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;, &lt;code&gt;AWS_REGION&lt;/code&gt;, &lt;code&gt;AZURE_TENANT_ID&lt;/code&gt;, &lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt;, &lt;code&gt;AZURE_CLIENT_SECRET&lt;/code&gt;, &lt;code&gt;GOOGLE_CREDENTIALS&lt;/code&gt; (the service account JSON, on one line), and &lt;code&gt;GOOGLE_ORG_ID&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you're only auditing one or two clouds, leave the others blank.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Pull the StackQL Docker image&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.bucket.audit.yaml pull
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tutorial pins to &lt;code&gt;stackql/stackql:v0.10.601&lt;/code&gt;. The audit uses views under &lt;code&gt;stackql_preview.*&lt;/code&gt; which are still evolving, so pinning matters for reproducibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Run the audit&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.bucket.audit.yaml run &lt;span class="nt"&gt;--rm&lt;/span&gt; stackql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a modest account this takes about 30 seconds. Output goes straight to your terminal as a table.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the output tells you
&lt;/h2&gt;

&lt;p&gt;When the audit finishes, you get a single table printed to your terminal. Something like this (bucket names are placeholders):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpchvx5vwuvhftu3n9bib.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpchvx5vwuvhftu3n9bib.png" alt="Terminal screenshot showing StackQL audit output as a table with columns for provider, bucket name, encryption class, public flag, and HTTPS enforcement. Rows include AWS, GCP, and Azure buckets with a mix of provider-managed and customer-managed encryption, and one public bucket flagged per AWS and GCP." width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Example output from the audit. Bucket configuration across three cloud providers in one normalised table.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The point isn't the specific findings. It's the shape.&lt;/p&gt;

&lt;p&gt;Three different resource types (S3 buckets, GCS buckets, and Azure Storage Accounts) normalised into one table with the same columns. Encryption class, public flag, HTTPS enforcement. Side by side, for every cloud you provided credentials for.&lt;/p&gt;

&lt;p&gt;A few patterns to notice in the table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Public buckets across providers.&lt;/strong&gt; Filter by &lt;code&gt;public = true&lt;/code&gt; and you have the answer to your security team's question, in every cloud, in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption side by side.&lt;/strong&gt; The &lt;code&gt;encryption_class&lt;/code&gt; column shows which buckets use provider-managed keys versus customer-managed. Different providers have different naming conventions internally; StackQL normalises them to the same two categories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS enforcement in one column.&lt;/strong&gt; Whether it's S3 bucket policies, GCS uniform access, or Azure secure transfer, same column, same values, no translation needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what SQL for cloud APIs actually delivers. Not just SQL as a query language, but a uniform data model across providers, so you can ask one question and get one answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance and scope
&lt;/h2&gt;

&lt;p&gt;StackQL runs each provider's query in parallel using the credentials you provided. There's no local database being populated and no background sync; every run hits the live APIs and returns what's there right now.&lt;/p&gt;

&lt;p&gt;On the test account this took about 30 seconds. The query is scoped to one AWS region and one GCP organization, which keeps the API footprint bounded and reflects the current preview default. Streaming output and broader audits (across regions, deeper across resource types) are in flight from the StackQL team.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this covers today
&lt;/h2&gt;

&lt;p&gt;Today, this audit covers storage buckets across AWS, GCP, and Azure. One region at a time for AWS, one organization at a time for GCP. Entitlements, IAM, and other resource types are on the roadmap as separate audits under &lt;code&gt;stackql_preview.*&lt;/code&gt; and will get their own tutorials as they land.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/stackql/stackql" rel="noopener noreferrer"&gt;&lt;code&gt;github.com/stackql/stackql&lt;/code&gt;&lt;/a&gt; is the flagship repo. If this tutorial was worth reading, give it a star. It's genuinely the thing that tells the team to build more tutorials like this one, and it helps other engineers with the same three-console problem find the project.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudsecurity</category>
      <category>stackql</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
