<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>CI/CD automation on Grafana Labs</title><link>https://grafana.com/docs/learning-hub/dashboards-as-code/04-cicd-automation/</link><description>Recent content in CI/CD automation on Grafana Labs</description><generator>Hugo -- gohugo.io</generator><language>en</language><atom:link href="/docs/learning-hub/dashboards-as-code/04-cicd-automation/index.xml" rel="self" type="application/rss+xml"/><item><title>CI/CD pipeline architecture</title><link>https://grafana.com/docs/learning-hub/dashboards-as-code/04-cicd-automation/01-pipeline-architecture/</link><pubDate>Wed, 24 Jun 2026 15:16:44 +0200</pubDate><guid>https://grafana.com/docs/learning-hub/dashboards-as-code/04-cicd-automation/01-pipeline-architecture/</guid><content><![CDATA[&lt;h2 id=&#34;cicd-pipeline-flow&#34;&gt;CI/CD pipeline flow&lt;/h2&gt;
&lt;p&gt;This diagram shows the end-to-end pipeline from code commit to deployed dashboard.&lt;/p&gt;
&lt;p&gt;&lt;img
  class=&#34;lazyload d-inline-block&#34;
  data-src=&#34;pipeline-flow.svg&#34;
  alt=&#34;Diagram showing CI/CD pipeline: on pull request, code flows through generate, plan, and PR comment stages; on merge to main, it continues through Terraform apply to a live dashboard in Grafana&#34;/&gt;&lt;/p&gt;
&lt;h2 id=&#34;what-each-stage-does&#34;&gt;What each stage does&lt;/h2&gt;
&lt;p&gt;The pipeline has three stages, each triggered at a different point in the workflow.&lt;/p&gt;
&lt;section class=&#34;expand-table-wrapper&#34;&gt;&lt;div class=&#34;responsive-table-wrapper&#34;&gt;
    &lt;table&gt;
      &lt;thead&gt;
          &lt;tr&gt;
              &lt;th&gt;Stage&lt;/th&gt;
              &lt;th&gt;Trigger&lt;/th&gt;
              &lt;th&gt;Action&lt;/th&gt;
          &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;strong&gt;Generate&lt;/strong&gt;&lt;/td&gt;
              &lt;td&gt;Every push&lt;/td&gt;
              &lt;td&gt;Run SDK code, produce dashboard JSON&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;strong&gt;Plan&lt;/strong&gt;&lt;/td&gt;
              &lt;td&gt;Every push&lt;/td&gt;
              &lt;td&gt;Show what Terraform will change&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;strong&gt;Apply&lt;/strong&gt;&lt;/td&gt;
              &lt;td&gt;Merge to main only&lt;/td&gt;
              &lt;td&gt;Deploy changes to Grafana&lt;/td&gt;
          &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/div&gt;
&lt;/section&gt;&lt;h2 id=&#34;when-each-path-runs&#34;&gt;When each path runs&lt;/h2&gt;
&lt;p&gt;The same stages run along two paths, depending on where the code is in the workflow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;On a pull request&lt;/strong&gt;: the pipeline runs generate and plan, then posts the plan as a PR comment for review. Nothing is deployed yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On merge to main&lt;/strong&gt;: the pipeline runs all three stages. Generate produces the latest JSON and Terraform applies it, so the dashboard in Grafana always reflects the latest code on main.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;the-review-gate&#34;&gt;The review gate&lt;/h2&gt;
&lt;p&gt;The Terraform plan output is posted as a PR comment so you can see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which dashboards will be created, updated, or destroyed&lt;/li&gt;
&lt;li&gt;Exactly what configuration will change&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gives you the full benefits of dashboards as code: every change is reviewed, every deployment is automated, and you can trace any dashboard back to the commit that produced it.&lt;/p&gt;
]]></content><description>&lt;h2 id="cicd-pipeline-flow">CI/CD pipeline flow&lt;/h2>
&lt;p>This diagram shows the end-to-end pipeline from code commit to deployed dashboard.&lt;/p>
&lt;p>&lt;img
class="lazyload d-inline-block"
data-src="pipeline-flow.svg"
alt="Diagram showing CI/CD pipeline: on pull request, code flows through generate, plan, and PR comment stages; on merge to main, it continues through Terraform apply to a live dashboard in Grafana"/>&lt;/p></description></item><item><title>GitHub Actions workflow</title><link>https://grafana.com/docs/learning-hub/dashboards-as-code/04-cicd-automation/02-github-actions-workflow/</link><pubDate>Wed, 24 Jun 2026 15:16:44 +0200</pubDate><guid>https://grafana.com/docs/learning-hub/dashboards-as-code/04-cicd-automation/02-github-actions-workflow/</guid><content><![CDATA[&lt;h2 id=&#34;complete-workflow&#34;&gt;Complete workflow&lt;/h2&gt;
&lt;p&gt;You can deploy dashboards with a single &lt;code&gt;terraform apply&lt;/code&gt;. But if someone still has to remember to run that command after every change, you haven&amp;rsquo;t removed the human bottleneck. Now you&amp;rsquo;ll automate the full dashboard lifecycle with GitHub Actions.&lt;/p&gt;
&lt;p&gt;This workflow generates and deploys your dashboards. It runs in two cases: when you open or update a pull request, and when commits are pushed to main. On a pull request, it runs generate and plan to preview the changes. On a push to main, it also runs apply to deploy them.&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;YAML&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;name: Deploy Grafana Dashboards

on:
  push:
    branches: [main]
  pull_request:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: &amp;#39;1.24&amp;#39;

      - name: Generate dashboard JSON
        run: go run main.go

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v1

      - name: Terraform Init
        run: terraform init

      - name: Terraform Plan
        run: terraform plan -input=false -no-color

      - name: Terraform Apply
        if: github.ref == &amp;#39;refs/heads/main&amp;#39;
        run: terraform apply -auto-approve
        env:
          GRAFANA_AUTH: ${{ secrets.GRAFANA_TOKEN }}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&#34;step-breakdown&#34;&gt;Step breakdown&lt;/h2&gt;
&lt;p&gt;Each step in the workflow handles one stage of the generate-plan-apply pipeline.&lt;/p&gt;
&lt;section class=&#34;expand-table-wrapper&#34;&gt;&lt;div class=&#34;responsive-table-wrapper&#34;&gt;
    &lt;table&gt;
      &lt;thead&gt;
          &lt;tr&gt;
              &lt;th&gt;Step&lt;/th&gt;
              &lt;th&gt;Purpose&lt;/th&gt;
          &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;strong&gt;Checkout&lt;/strong&gt;&lt;/td&gt;
              &lt;td&gt;Access your repository code&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;strong&gt;Set up Go&lt;/strong&gt;&lt;/td&gt;
              &lt;td&gt;Install the language runtime for SDK code&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;strong&gt;Generate JSON&lt;/strong&gt;&lt;/td&gt;
              &lt;td&gt;Run your SDK code to produce dashboard files&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;strong&gt;Terraform Init&lt;/strong&gt;&lt;/td&gt;
              &lt;td&gt;Download the Grafana provider&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;strong&gt;Plan/Apply&lt;/strong&gt;&lt;/td&gt;
              &lt;td&gt;Preview or deploy dashboard changes&lt;/td&gt;
          &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/div&gt;
&lt;/section&gt;&lt;p&gt;The &lt;code&gt;if: github.ref == &#39;refs/heads/main&#39;&lt;/code&gt; condition on the apply step gates deployment: plan runs on every push, but apply runs only when changes land on &lt;code&gt;main&lt;/code&gt;. Pull requests get a plan preview without deploying anything.&lt;/p&gt;
&lt;h2 id=&#34;alternative-ci-systems&#34;&gt;Alternative CI systems&lt;/h2&gt;
&lt;p&gt;You can adapt this to other CI systems (GitLab CI, CircleCI, Jenkins) by translating the same five steps. Your Foundation SDK code and Terraform configuration stay the same regardless of which CI tool runs them.&lt;/p&gt;
]]></content><description>&lt;h2 id="complete-workflow">Complete workflow&lt;/h2>
&lt;p>You can deploy dashboards with a single &lt;code>terraform apply&lt;/code>. But if someone still has to remember to run that command after every change, you haven&amp;rsquo;t removed the human bottleneck. Now you&amp;rsquo;ll automate the full dashboard lifecycle with GitHub Actions.&lt;/p></description></item><item><title>Secrets and variables</title><link>https://grafana.com/docs/learning-hub/dashboards-as-code/04-cicd-automation/03-secrets-and-variables/</link><pubDate>Wed, 24 Jun 2026 15:16:44 +0200</pubDate><guid>https://grafana.com/docs/learning-hub/dashboards-as-code/04-cicd-automation/03-secrets-and-variables/</guid><content><![CDATA[&lt;h2 id=&#34;what-you-need&#34;&gt;What you need&lt;/h2&gt;
&lt;p&gt;Your pipeline must authenticate to Grafana without exposing credentials in code. GitHub Actions provides two mechanisms: secrets for sensitive values and variables for non-sensitive configuration. You need three values: one secret and two variables.&lt;/p&gt;
&lt;section class=&#34;expand-table-wrapper&#34;&gt;&lt;div class=&#34;responsive-table-wrapper&#34;&gt;
    &lt;table&gt;
      &lt;thead&gt;
          &lt;tr&gt;
              &lt;th&gt;Value&lt;/th&gt;
              &lt;th&gt;What it is&lt;/th&gt;
              &lt;th&gt;Type&lt;/th&gt;
              &lt;th&gt;Where to store&lt;/th&gt;
          &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;strong&gt;GRAFANA_TOKEN&lt;/strong&gt;&lt;/td&gt;
              &lt;td&gt;Service account token that authenticates the pipeline to Grafana&lt;/td&gt;
              &lt;td&gt;Secret&lt;/td&gt;
              &lt;td&gt;&lt;strong&gt;Repository secrets&lt;/strong&gt;&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;strong&gt;GRAFANA_SERVER&lt;/strong&gt;&lt;/td&gt;
              &lt;td&gt;Your Grafana instance URL, for example &lt;code&gt;https://your-stack.grafana.net&lt;/code&gt;&lt;/td&gt;
              &lt;td&gt;Variable&lt;/td&gt;
              &lt;td&gt;&lt;strong&gt;Repository variables&lt;/strong&gt;&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;&lt;strong&gt;GRAFANA_STACK_ID&lt;/strong&gt;&lt;/td&gt;
              &lt;td&gt;Your Grafana stack identifier&lt;/td&gt;
              &lt;td&gt;Variable&lt;/td&gt;
              &lt;td&gt;&lt;strong&gt;Repository variables&lt;/strong&gt;&lt;/td&gt;
          &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/div&gt;
&lt;/section&gt;&lt;h2 id=&#34;add-and-reference-your-credentials&#34;&gt;Add and reference your credentials&lt;/h2&gt;
&lt;p&gt;Store the three values in your repository, then reference them from your workflow.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In your repository, go to &lt;strong&gt;Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;Secrets and variables&lt;/strong&gt; &amp;gt; &lt;strong&gt;Actions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;On &lt;strong&gt;Repository secrets&lt;/strong&gt;, add &lt;code&gt;GRAFANA_TOKEN&lt;/code&gt; with your service account token. Never commit this token to your repository.&lt;/li&gt;
&lt;li&gt;On &lt;strong&gt;Repository variables&lt;/strong&gt;, add &lt;code&gt;GRAFANA_SERVER&lt;/code&gt; and &lt;code&gt;GRAFANA_STACK_ID&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reference the secret with the &lt;code&gt;secrets&lt;/code&gt; context and the variables with the &lt;code&gt;vars&lt;/code&gt; context.&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;YAML&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;env:
  GRAFANA_SERVER: ${{ vars.GRAFANA_SERVER }}
  GRAFANA_STACK_ID: ${{ vars.GRAFANA_STACK_ID }}
  GRAFANA_TOKEN: ${{ secrets.GRAFANA_TOKEN }}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&#34;authenticate-terraform&#34;&gt;Authenticate Terraform&lt;/h2&gt;
&lt;p&gt;Pass authentication to the Terraform provider through environment variables or directly in the provider block. Set &lt;code&gt;TF_VAR_grafana_url&lt;/code&gt; and &lt;code&gt;TF_VAR_grafana_token&lt;/code&gt; as environment variables, and Terraform picks them up automatically. The environment variable approach keeps your Terraform code portable: the same configuration works locally and in CI.&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;hcl&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-hcl&#34;&gt;provider &amp;#34;grafana&amp;#34; {
  url  = var.grafana_url
  auth = var.grafana_token
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
]]></content><description>&lt;h2 id="what-you-need">What you need&lt;/h2>
&lt;p>Your pipeline must authenticate to Grafana without exposing credentials in code. GitHub Actions provides two mechanisms: secrets for sensitive values and variables for non-sensitive configuration. You need three values: one secret and two variables.&lt;/p></description></item><item><title>Reliable, repeatable deployments</title><link>https://grafana.com/docs/learning-hub/dashboards-as-code/04-cicd-automation/04-create-or-update/</link><pubDate>Wed, 24 Jun 2026 15:16:44 +0200</pubDate><guid>https://grafana.com/docs/learning-hub/dashboards-as-code/04-cicd-automation/04-create-or-update/</guid><content><![CDATA[&lt;h2 id=&#34;how-terraform-handles-existing-dashboards&#34;&gt;How Terraform handles existing dashboards&lt;/h2&gt;
&lt;p&gt;A reliable pipeline produces the same result no matter how many times you run it, with no duplicate dashboards and no errors. Terraform ensures this with a state file: a record of the resources it has created. On each run, Terraform decides what to change by comparing three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your configuration&lt;/li&gt;
&lt;li&gt;The state file&lt;/li&gt;
&lt;li&gt;What actually exists in Grafana&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When the live dashboard no longer matches your code (for example, someone edited it in the UI), that difference is called drift. Because of this, you can safely retry failed runs and merge to main repeatedly without creating duplicates.&lt;/p&gt;
&lt;section class=&#34;expand-table-wrapper&#34;&gt;&lt;div class=&#34;responsive-table-wrapper&#34;&gt;
    &lt;table&gt;
      &lt;thead&gt;
          &lt;tr&gt;
              &lt;th&gt;Scenario&lt;/th&gt;
              &lt;th&gt;What Terraform does&lt;/th&gt;
          &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
          &lt;tr&gt;
              &lt;td&gt;Dashboard doesn&amp;rsquo;t exist&lt;/td&gt;
              &lt;td&gt;Creates it and records in state&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;Dashboard exists and matches&lt;/td&gt;
              &lt;td&gt;No changes needed&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;Dashboard was edited manually&lt;/td&gt;
              &lt;td&gt;Detects drift, restores code-defined version&lt;/td&gt;
          &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/div&gt;
&lt;/section&gt;&lt;h2 id=&#34;why-stable-uids-matter&#34;&gt;Why stable UIDs matter&lt;/h2&gt;
&lt;p&gt;A stable UID in your &lt;code&gt;DashboardBuilder&lt;/code&gt; gives Terraform a consistent identifier to track across runs. With it, Terraform updates the same dashboard each time. Without it, Terraform has nothing to match against, so each deployment creates a new dashboard and you get duplicates on every run.&lt;/p&gt;

&lt;div class=&#34;code-snippet &#34;&gt;&lt;div class=&#34;lang-toolbar&#34;&gt;
    &lt;span class=&#34;lang-toolbar__item lang-toolbar__item-active&#34;&gt;Go&lt;/span&gt;
    &lt;span class=&#34;code-clipboard&#34;&gt;
      &lt;button x-data=&#34;app_code_snippet()&#34; x-init=&#34;init()&#34; @click=&#34;copy()&#34;&gt;
        &lt;img class=&#34;code-clipboard__icon&#34; src=&#34;/media/images/icons/icon-copy-small-2.svg&#34; alt=&#34;Copy code to clipboard&#34; width=&#34;14&#34; height=&#34;13&#34;&gt;
        &lt;span&gt;Copy&lt;/span&gt;
      &lt;/button&gt;
    &lt;/span&gt;
    &lt;div class=&#34;lang-toolbar__border&#34;&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-snippet &#34;&gt;
    &lt;pre data-expanded=&#34;false&#34;&gt;&lt;code class=&#34;language-go&#34;&gt;builder := dashboard.NewDashboardBuilder(&amp;#34;My Dashboard&amp;#34;).
  Uid(&amp;#34;my-dashboard&amp;#34;)  // Same UID = update, not duplicate&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;
]]></content><description>&lt;h2 id="how-terraform-handles-existing-dashboards">How Terraform handles existing dashboards&lt;/h2>
&lt;p>A reliable pipeline produces the same result no matter how many times you run it, with no duplicate dashboards and no errors. Terraform ensures this with a state file: a record of the resources it has created. On each run, Terraform decides what to change by comparing three things:&lt;/p></description></item></channel></rss>