From 0e88019bc5947dd11a418e56b8a5bbb92d062f7c Mon Sep 17 00:00:00 2001
From: Timo Furrer <tuxtimo@gmail.com>
Date: Wed, 26 Feb 2025 14:51:08 +0100
Subject: [PATCH] Support input for auto definting the HTTP backend

Closes https://gitlab.com/components/opentofu/-/issues/100

Changelog: added
---
 .gitlab/README.md.template                    | 10 ++++---
 README.md                                     | 13 +++++----
 src/gitlab-tofu.sh                            | 27 ++++++++++++++++++-
 templates/apply.yml                           |  5 ++++
 templates/destroy.yml                         |  5 ++++
 templates/full-pipeline.yml                   | 10 +++++++
 templates/graph.yml                           |  5 ++++
 templates/job-templates.yml                   | 10 +++++++
 templates/plan.yml                            |  5 ++++
 templates/test.yml                            |  5 ++++
 templates/validate-plan-apply.yml             |  8 ++++++
 templates/validate-plan-destroy.yml           |  8 ++++++
 templates/validate-plan.yml                   |  7 +++++
 templates/validate.yml                        |  5 ++++
 tests/iac-auto-define-backend/main.tf         | 19 +++++++++++++
 .../tests/main.tftest.hcl                     |  6 +++++
 .../AutoDefineBackend.gitlab-ci.yml           | 20 ++++++++++++++
 tests/integration.gitlab-ci.yml               | 17 ++++++++++++
 18 files changed, 175 insertions(+), 10 deletions(-)
 create mode 100644 tests/iac-auto-define-backend/main.tf
 create mode 100644 tests/iac-auto-define-backend/tests/main.tftest.hcl
 create mode 100644 tests/integration-tests/AutoDefineBackend.gitlab-ci.yml

diff --git a/.gitlab/README.md.template b/.gitlab/README.md.template
index 53feabf..523109b 100644
--- a/.gitlab/README.md.template
+++ b/.gitlab/README.md.template
@@ -125,9 +125,10 @@ The base image OS can be specified with the `base_os` input.
 ### GitLab-managed Terraform state backend
 
 This component - by leveraging the [`gitlab-tofu`](src/gitlab-tofu.sh) CLI internally -
-automatically configures the
+can automatically define and configure the
 [GitLab-managed Terraform state backend](https://docs.gitlab.com/ee/user/infrastructure/iac/terraform_state.html).
-The only thing required is that the Terraform configuration must specify an empty `http` backend block, like this:
+
+By default the HTTP backend must be defined manually using the following HCL:
 
 ```hcl
 terraform {
@@ -135,8 +136,9 @@ terraform {
 }
 ```
 
-We recommend having a dedicated `backend.tf` file inside your `root_dir`
-with the aforementioned block.
+However, you may simply enable the `auto_define_backend` so that the component takes care of this step.
+
+**Note**: in future versions of this component we may enable `auto_define_backend` by default.
 
 ### State and Plan Encryption
 
diff --git a/README.md b/README.md
index 88d55c0..82e05fa 100644
--- a/README.md
+++ b/README.md
@@ -127,9 +127,10 @@ The base image OS can be specified with the `base_os` input.
 ### GitLab-managed Terraform state backend
 
 This component - by leveraging the [`gitlab-tofu`](src/gitlab-tofu.sh) CLI internally -
-automatically configures the
+can automatically define and configure the
 [GitLab-managed Terraform state backend](https://docs.gitlab.com/ee/user/infrastructure/iac/terraform_state.html).
-The only thing required is that the Terraform configuration must specify an empty `http` backend block, like this:
+
+By default the HTTP backend must be defined manually using the following HCL:
 
 ```hcl
 terraform {
@@ -137,8 +138,9 @@ terraform {
 }
 ```
 
-We recommend having a dedicated `backend.tf` file inside your `root_dir`
-with the aforementioned block.
+However, you may simply enable the `auto_define_backend` so that the component takes care of this step.
+
+**Note**: in future versions of this component we may enable `auto_define_backend` by default.
 
 ### State and Plan Encryption
 
@@ -315,7 +317,8 @@ The following environment variables are respected by the `gitlab-tofu` script:
 - `GITLAB_TOFU_AUTO_ENCRYPTION`: if set to true, enables auto state and plan encryption. Defaults to `false`.
 - `GITLAB_TOFU_AUTO_ENCRYPTION_PASSPHRASE`: the passphrase to use for state and plan encryption. Required if `GITLAB_TOFU_AUTO_ENCRYPTION` is true.
 - `GITLAB_TOFU_AUTO_ENCRYPTION_ENABLE_MIGRATION_FROM_UNENCRYPTED_ENABLED`: if set to true, enables a fallback for state and plan encryption to migrate unencrypted plans and states to encrypted ones. Defaults to `false`.
-- `GITLAB_TOFU_ALLOW_DEVELOPER_ROLE: Users with the Developer role are not able to lock the state. Thus a regular `tofu plan` fails. When set to `true` a `-lock=false` is passed to plan.
+- `GITLAB_TOFU_ALLOW_DEVELOPER_ROLE`: Users with the Developer role are not able to lock the state. Thus a regular `tofu plan` fails. When set to `true` a `-lock=false` is passed to plan.
+- `GITLAB_TOFU_AUTO_DEFINE_BACKEND`: if set to true, automatically creates a file with a HTTP backend configuration block.
 
 #### Respected OpenTofu Environment Variables
 
diff --git a/src/gitlab-tofu.sh b/src/gitlab-tofu.sh
index e5ecfc1..8a339e0 100644
--- a/src/gitlab-tofu.sh
+++ b/src/gitlab-tofu.sh
@@ -30,7 +30,8 @@
 # - `GITLAB_TOFU_AUTO_ENCRYPTION`: if set to true, enables auto state and plan encryption. Defaults to `false`.
 # - `GITLAB_TOFU_AUTO_ENCRYPTION_PASSPHRASE`: the passphrase to use for state and plan encryption. Required if `GITLAB_TOFU_AUTO_ENCRYPTION` is true.
 # - `GITLAB_TOFU_AUTO_ENCRYPTION_ENABLE_MIGRATION_FROM_UNENCRYPTED_ENABLED`: if set to true, enables a fallback for state and plan encryption to migrate unencrypted plans and states to encrypted ones. Defaults to `false`.
-# - `GITLAB_TOFU_ALLOW_DEVELOPER_ROLE: Users with the Developer role are not able to lock the state. Thus a regular `tofu plan` fails. When set to `true` a `-lock=false` is passed to plan.
+# - `GITLAB_TOFU_ALLOW_DEVELOPER_ROLE`: Users with the Developer role are not able to lock the state. Thus a regular `tofu plan` fails. When set to `true` a `-lock=false` is passed to plan.
+# - `GITLAB_TOFU_AUTO_DEFINE_BACKEND`: if set to true, automatically creates a file with a HTTP backend configuration block.
 #
 # #### Respected OpenTofu Environment Variables
 #
@@ -163,6 +164,7 @@ fi
 # ============================
 
 # Backend related variables
+auto_define_backend=${GITLAB_TOFU_AUTO_DEFINE_BACKEND:-false}
 backend_username="gitlab-ci-token"
 backend_password="${CI_JOB_TOKEN}"
 backend_state_name="$(jq -rn --arg x "${GITLAB_TOFU_STATE_NAME:-default}" '$x|@uri')"
@@ -177,6 +179,8 @@ if [ -n "${GITLAB_TOFU_ROOT_DIR}" ]; then
 
   default_tf_plan_cache="${abs_tf_root}/${base_plan_name}.cache"
   default_tf_plan_json="${abs_tf_root}/${base_plan_name}.json"
+else
+  abs_tf_root=$(realpath "${CI_PROJECT_DIR}")
 fi
 
 # Init related variables
@@ -227,6 +231,26 @@ fi
 # Helper functions
 # ================
 
+# define_http_backend defines the HTTP backend in a file called __gitlab-opentofu-backend.tf if no backend can be found.
+# The backend configuration is attempted to be found with a simple grep.
+define_http_backend() {
+  if ! $auto_define_backend; then
+    return
+  fi
+
+  if ! grep -q '^[[:space:]]*backend[[:space:]]\+"http"[[:space:]]\+{.*$' "${abs_tf_root}" -r 2>/dev/null; then
+    echo "gitlab-tofu: automatically defining the HTTP backend in __gitlab-opentofu-backend.tf. If that is a mistake, please disable it with the auto_define_backend: false input."
+
+    cat <<EOF > __gitlab-opentofu-backend.tf
+terraform {
+  backend "http" {}
+}
+EOF
+  else
+    echo "gitlab-tofu: auto_define_backend is enabled, but found manually configured HTTP backend, doing nothing."
+  fi
+}
+
 # configure_variables_for_tofu sets and exports all relevant variables for subsequent `tofu` command invocations.
 configure_variables_for_tofu() {
   # Use terraform automation mode (will remove some verbose unneeded messages)
@@ -352,6 +376,7 @@ EOF
 }
 
 # We always want to configure the tofu variables, even in source-mode.
+define_http_backend
 configure_variables_for_tofu
 configure_encryption_for_tofu
 
diff --git a/templates/apply.yml b/templates/apply.yml
index 9ee6039..3b98e55 100644
--- a/templates/apply.yml
+++ b/templates/apply.yml
@@ -98,6 +98,10 @@ spec:
       default: false
       type: boolean
       description: 'Whether to setup automatic state and plan encryption for currently unencrypted state. This is only temporarily useful when migrating from an unencrypted state.'
+    auto_define_backend:
+      default: false
+      type: boolean
+      description: 'Whether to automatically define the HTTP backend configuration block.'
 
 ---
 
@@ -124,6 +128,7 @@ spec:
     GITLAB_TOFU_AUTO_ENCRYPTION: '$[[ inputs.auto_encryption ]]'
     GITLAB_TOFU_AUTO_ENCRYPTION_PASSPHRASE: '$[[ inputs.auto_encryption_passphrase ]]'
     GITLAB_TOFU_AUTO_ENCRYPTION_ENABLE_MIGRATION_FROM_UNENCRYPTED: '$[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]'
+    GITLAB_TOFU_AUTO_DEFINE_BACKEND: '$[[ inputs.auto_define_backend ]]'
   image:
     name: '$[[ inputs.image_registry_base ]]/$[[ inputs.image_name ]]:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]-$[[ inputs.base_os ]]$[[ inputs.image_digest ]]'
   script:
diff --git a/templates/destroy.yml b/templates/destroy.yml
index 90b1e55..fa350f8 100644
--- a/templates/destroy.yml
+++ b/templates/destroy.yml
@@ -102,6 +102,10 @@ spec:
       default: false
       type: boolean
       description: 'Whether to automatically delete the Terraform state. This only makes sense when using the GitLab-managed state backend. It is equivalent to running the delete-state job.'
+    auto_define_backend:
+      default: false
+      type: boolean
+      description: 'Whether to automatically define the HTTP backend configuration block.'
 
 ---
 
@@ -128,6 +132,7 @@ spec:
     GITLAB_TOFU_AUTO_ENCRYPTION: '$[[ inputs.auto_encryption ]]'
     GITLAB_TOFU_AUTO_ENCRYPTION_PASSPHRASE: '$[[ inputs.auto_encryption_passphrase ]]'
     GITLAB_TOFU_AUTO_ENCRYPTION_ENABLE_MIGRATION_FROM_UNENCRYPTED: '$[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]'
+    GITLAB_TOFU_AUTO_DEFINE_BACKEND: '$[[ inputs.auto_define_backend ]]'
   image:
     name: '$[[ inputs.image_registry_base ]]/$[[ inputs.image_name ]]:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]-$[[ inputs.base_os ]]$[[ inputs.image_digest ]]'
   script:
diff --git a/templates/full-pipeline.yml b/templates/full-pipeline.yml
index f4f22f1..290e81b 100644
--- a/templates/full-pipeline.yml
+++ b/templates/full-pipeline.yml
@@ -198,6 +198,10 @@ spec:
       default: false
       type: boolean
       description: 'Users with the Developer role are not able to lock the state. Thus a regular `tofu plan` fails. When set to `true` a `-lock=false` is passed to plan.'
+    auto_define_backend:
+      default: false
+      type: boolean
+      description: 'Whether to automatically define the HTTP backend configuration block.'
 
 ---
 
@@ -236,6 +240,7 @@ include:
       auto_encryption: $[[ inputs.auto_encryption ]]
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/test.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "true"'
@@ -259,6 +264,7 @@ include:
       auto_encryption: $[[ inputs.auto_encryption ]]
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/plan.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -282,6 +288,7 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       allow_developer_role: $[[ inputs.allow_developer_role_to_plan ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/apply.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -302,6 +309,7 @@ include:
       auto_encryption: $[[ inputs.auto_encryption ]]
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/destroy.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -321,6 +329,7 @@ include:
       auto_encryption: $[[ inputs.auto_encryption ]]
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/delete-state.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -408,6 +417,7 @@ stages:
           auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
           auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
           allow_developer_role_to_plan: $[[ inputs.allow_developer_role_to_plan ]]
+          auto_define_backend: $[[ inputs.auto_define_backend ]]
           trigger_in_child_pipeline: false
     forward:
       yaml_variables: true
diff --git a/templates/graph.yml b/templates/graph.yml
index 02da517..c78cde0 100644
--- a/templates/graph.yml
+++ b/templates/graph.yml
@@ -96,6 +96,10 @@ spec:
       default: false
       type: boolean
       description: 'Whether to setup automatic state and plan encryption for currently unencrypted state. This is only temporarily useful when migrating from an unencrypted state.'
+    auto_define_backend:
+      default: false
+      type: boolean
+      description: 'Whether to automatically define the HTTP backend configuration block.'
 
 ---
 
@@ -116,6 +120,7 @@ spec:
     GITLAB_TOFU_AUTO_ENCRYPTION: '$[[ inputs.auto_encryption ]]'
     GITLAB_TOFU_AUTO_ENCRYPTION_PASSPHRASE: '$[[ inputs.auto_encryption_passphrase ]]'
     GITLAB_TOFU_AUTO_ENCRYPTION_ENABLE_MIGRATION_FROM_UNENCRYPTED: '$[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]'
+    GITLAB_TOFU_AUTO_DEFINE_BACKEND: '$[[ inputs.auto_define_backend ]]'
   image:
     name: '$[[ inputs.image_registry_base ]]/$[[ inputs.image_name ]]:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]-$[[ inputs.base_os ]]$[[ inputs.image_digest ]]'
   script:
diff --git a/templates/job-templates.yml b/templates/job-templates.yml
index 387f02f..eaa2d30 100644
--- a/templates/job-templates.yml
+++ b/templates/job-templates.yml
@@ -103,6 +103,10 @@ spec:
       default: false
       type: boolean
       description: 'Users with the Developer role are not able to lock the state. Thus a regular `tofu plan` fails. When set to `true` a `-lock=false` is passed to plan.'
+    auto_define_backend:
+      default: false
+      type: boolean
+      description: 'Whether to automatically define the HTTP backend configuration block.'
 
 ---
 
@@ -134,6 +138,7 @@ include:
       auto_encryption: $[[ inputs.auto_encryption ]]
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/graph.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]graph'
@@ -148,6 +153,7 @@ include:
       auto_encryption: $[[ inputs.auto_encryption ]]
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/test.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]test'
@@ -164,6 +170,7 @@ include:
       auto_encryption: $[[ inputs.auto_encryption ]]
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/plan.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]plan'
@@ -183,6 +190,7 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       allow_developer_role: $[[ inputs.allow_developer_role_to_plan ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/apply.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]apply'
@@ -200,6 +208,7 @@ include:
       auto_encryption: $[[ inputs.auto_encryption ]]
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/destroy.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]destroy'
@@ -217,6 +226,7 @@ include:
       auto_encryption: $[[ inputs.auto_encryption ]]
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/delete-state.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]delete-state'
diff --git a/templates/plan.yml b/templates/plan.yml
index 752c37c..73b4a51 100644
--- a/templates/plan.yml
+++ b/templates/plan.yml
@@ -109,6 +109,10 @@ spec:
       default: false
       type: boolean
       description: 'Users with the Developer role are not able to lock the state. Thus a regular `tofu plan` fails. When set to `true` a `-lock=false` is passed to plan.'
+    auto_define_backend:
+      default: false
+      type: boolean
+      description: 'Whether to automatically define the HTTP backend configuration block.'
 
 ---
 
@@ -175,6 +179,7 @@ spec:
     GITLAB_TOFU_AUTO_ENCRYPTION_PASSPHRASE: '$[[ inputs.auto_encryption_passphrase ]]'
     GITLAB_TOFU_AUTO_ENCRYPTION_ENABLE_MIGRATION_FROM_UNENCRYPTED: '$[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]'
     GITLAB_TOFU_ALLOW_DEVELOPER_ROLE: '$[[ inputs.allow_developer_role ]]'
+    GITLAB_TOFU_AUTO_DEFINE_BACKEND: '$[[ inputs.auto_define_backend ]]'
   image:
     name: '$[[ inputs.image_registry_base ]]/$[[ inputs.image_name ]]:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]-$[[ inputs.base_os ]]$[[ inputs.image_digest ]]'
   script:
diff --git a/templates/test.yml b/templates/test.yml
index 79b2b0b..a76e329 100644
--- a/templates/test.yml
+++ b/templates/test.yml
@@ -98,6 +98,10 @@ spec:
       default: false
       type: boolean
       description: 'Whether to setup automatic state and plan encryption for currently unencrypted state. This is only temporarily useful when migrating from an unencrypted state.'
+    auto_define_backend:
+      default: false
+      type: boolean
+      description: 'Whether to automatically define the HTTP backend configuration block.'
 
 ---
 
@@ -119,6 +123,7 @@ spec:
     GITLAB_TOFU_AUTO_ENCRYPTION: '$[[ inputs.auto_encryption ]]'
     GITLAB_TOFU_AUTO_ENCRYPTION_PASSPHRASE: '$[[ inputs.auto_encryption_passphrase ]]'
     GITLAB_TOFU_AUTO_ENCRYPTION_ENABLE_MIGRATION_FROM_UNENCRYPTED: '$[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]'
+    GITLAB_TOFU_AUTO_DEFINE_BACKEND: '$[[ inputs.auto_define_backend ]]'
   image:
     name: '$[[ inputs.image_registry_base ]]/$[[ inputs.image_name ]]:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]-$[[ inputs.base_os ]]$[[ inputs.image_digest ]]'
   script:
diff --git a/templates/validate-plan-apply.yml b/templates/validate-plan-apply.yml
index 6924516..f1c5005 100644
--- a/templates/validate-plan-apply.yml
+++ b/templates/validate-plan-apply.yml
@@ -162,6 +162,10 @@ spec:
       default: false
       type: boolean
       description: 'Users with the Developer role are not able to lock the state. Thus a regular `tofu plan` fails. When set to `true` a `-lock=false` is passed to plan.'
+    auto_define_backend:
+      default: false
+      type: boolean
+      description: 'Whether to automatically define the HTTP backend configuration block.'
 
 ---
 
@@ -201,6 +205,7 @@ include:
       auto_encryption: $[[ inputs.auto_encryption ]]
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/plan.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -225,6 +230,7 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       allow_developer_role: $[[ inputs.allow_developer_role_to_plan ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/apply.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -246,6 +252,7 @@ include:
       auto_encryption: $[[ inputs.auto_encryption ]]
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
 
 
 # NOTE: the following configuration is only used if `trigger_in_child_pipeline` is enabled.
@@ -303,6 +310,7 @@ stages:
           auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
           auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
           allow_developer_role_to_plan: $[[ inputs.allow_developer_role_to_plan ]]
+          auto_define_backend: $[[ inputs.auto_define_backend ]]
           trigger_in_child_pipeline: false
     forward:
       yaml_variables: true
diff --git a/templates/validate-plan-destroy.yml b/templates/validate-plan-destroy.yml
index c6a54b7..1d303fd 100644
--- a/templates/validate-plan-destroy.yml
+++ b/templates/validate-plan-destroy.yml
@@ -168,6 +168,10 @@ spec:
       default: false
       type: boolean
       description: 'Users with the Developer role are not able to lock the state. Thus a regular `tofu plan` fails. When set to `true` a `-lock=false` is passed to plan.'
+    auto_define_backend:
+      default: false
+      type: boolean
+      description: 'Whether to automatically define the HTTP backend configuration block.'
 
 ---
 
@@ -207,6 +211,7 @@ include:
       auto_encryption: $[[ inputs.auto_encryption ]]
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/plan.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -232,6 +237,7 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       allow_developer_role: $[[ inputs.allow_developer_role_to_plan ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/destroy.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -254,6 +260,7 @@ include:
       auto_encryption: $[[ inputs.auto_encryption ]]
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/delete-state.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -336,6 +343,7 @@ stages:
           auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
           auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
           allow_developer_role_to_plan: $[[ inputs.allow_developer_role_to_plan ]]
+          auto_define_backend: $[[ inputs.auto_define_backend ]]
           trigger_in_child_pipeline: false
     forward:
       yaml_variables: true
diff --git a/templates/validate-plan.yml b/templates/validate-plan.yml
index 96d199e..ed976b6 100644
--- a/templates/validate-plan.yml
+++ b/templates/validate-plan.yml
@@ -146,6 +146,10 @@ spec:
       default: false
       type: boolean
       description: 'Users with the Developer role are not able to lock the state. Thus a regular `tofu plan` fails. When set to `true` a `-lock=false` is passed to plan.'
+    auto_define_backend:
+      default: false
+      type: boolean
+      description: 'Whether to automatically define the HTTP backend configuration block.'
 
 ---
 
@@ -185,6 +189,7 @@ include:
       auto_encryption: $[[ inputs.auto_encryption ]]
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
   - local: '/templates/plan.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -209,6 +214,7 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       allow_developer_role: $[[ inputs.allow_developer_role_to_plan ]]
+      auto_define_backend: $[[ inputs.auto_define_backend ]]
 
 
 # NOTE: the following configuration is only used if `trigger_in_child_pipeline` is enabled.
@@ -263,6 +269,7 @@ stages:
           auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
           auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
           allow_developer_role_to_plan: $[[ inputs.allow_developer_role_to_plan ]]
+          auto_define_backend: $[[ inputs.auto_define_backend ]]
           trigger_in_child_pipeline: false
     forward:
       yaml_variables: true
diff --git a/templates/validate.yml b/templates/validate.yml
index e4dcb8e..14154a1 100644
--- a/templates/validate.yml
+++ b/templates/validate.yml
@@ -95,6 +95,10 @@ spec:
       default: false
       type: boolean
       description: 'Whether to setup automatic state and plan encryption for currently unencrypted state. This is only temporarily useful when migrating from an unencrypted state.'
+    auto_define_backend:
+      default: false
+      type: boolean
+      description: 'Whether to automatically define the HTTP backend configuration block.'
 
 ---
 
@@ -116,6 +120,7 @@ spec:
     GITLAB_TOFU_AUTO_ENCRYPTION: '$[[ inputs.auto_encryption ]]'
     GITLAB_TOFU_AUTO_ENCRYPTION_PASSPHRASE: '$[[ inputs.auto_encryption_passphrase ]]'
     GITLAB_TOFU_AUTO_ENCRYPTION_ENABLE_MIGRATION_FROM_UNENCRYPTED: '$[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]'
+    GITLAB_TOFU_AUTO_DEFINE_BACKEND: '$[[ inputs.auto_define_backend ]]'
   image:
     name: '$[[ inputs.image_registry_base ]]/$[[ inputs.image_name ]]:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]-$[[ inputs.base_os ]]$[[ inputs.image_digest ]]'
   script:
diff --git a/tests/iac-auto-define-backend/main.tf b/tests/iac-auto-define-backend/main.tf
new file mode 100644
index 0000000..6f610dc
--- /dev/null
+++ b/tests/iac-auto-define-backend/main.tf
@@ -0,0 +1,19 @@
+resource "local_file" "foo" {
+  content  = "foo!"
+  filename = "${path.module}/foo.bar"
+}
+
+locals {
+  ts = plantimestamp()
+}
+
+// NOTE: always force a change.
+resource "null_resource" "this" {
+  triggers = {
+    timestamp = local.ts
+  }
+}
+
+output "this_always_changes" {
+  value = local.ts
+}
diff --git a/tests/iac-auto-define-backend/tests/main.tftest.hcl b/tests/iac-auto-define-backend/tests/main.tftest.hcl
new file mode 100644
index 0000000..fedf96e
--- /dev/null
+++ b/tests/iac-auto-define-backend/tests/main.tftest.hcl
@@ -0,0 +1,6 @@
+run "test" {
+  assert {
+    condition     = file(local_file.foo.filename) == "foo!"
+    error_message = "Incorrect content in ${local_file.foo.filename}"
+  }
+}
diff --git a/tests/integration-tests/AutoDefineBackend.gitlab-ci.yml b/tests/integration-tests/AutoDefineBackend.gitlab-ci.yml
new file mode 100644
index 0000000..4a64e52
--- /dev/null
+++ b/tests/integration-tests/AutoDefineBackend.gitlab-ci.yml
@@ -0,0 +1,20 @@
+include:
+  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/full-pipeline@$CI_COMMIT_SHA
+    inputs:
+      image_registry_base: $GITLAB_OPENTOFU_IMAGE_BASE
+      version: $CI_COMMIT_SHA
+      base_os: $GITLAB_OPENTOFU_BASE_IMAGE_OS
+      opentofu_version: $OPENTOFU_VERSION
+      root_dir: $TEST_GITLAB_TOFU_ROOT_DIR
+      state_name: $TEST_GITLAB_TOFU_STATE_NAME
+      auto_define_backend: true
+      # Required to run everything immediately, instead of manually.
+      fmt_rules: [{when: always}]
+      validate_rules: [{when: always}]
+      test_rules: [{when: always}]
+      plan_rules: [{when: always}]
+      apply_rules: [{when: always}]
+      destroy_rules: [{when: always}]
+      delete_state_rules: [{when: always}]
+
+stages: [validate, test, build, deploy, cleanup]
diff --git a/tests/integration.gitlab-ci.yml b/tests/integration.gitlab-ci.yml
index c9298a5..f5ed0b8 100644
--- a/tests/integration.gitlab-ci.yml
+++ b/tests/integration.gitlab-ci.yml
@@ -17,6 +17,23 @@ full-pipeline:
           - alpine
           - debian
 
+auto-define-backend:
+  stage: test-integration
+  variables:
+    OPENTOFU_VERSION: $LATEST_OPENTOFU_VERSION
+    TEST_GITLAB_TOFU_STATE_NAME: ci-integration-$CI_JOB_NAME_SLUG-$CI_PIPELINE_IID-$CI_NODE_INDEX
+    TEST_GITLAB_TOFU_ROOT_DIR: tests/iac-auto-define-backend
+  trigger:
+    include: tests/integration-tests/$PIPELINE_NAME.gitlab-ci.yml
+    strategy: depend
+  parallel:
+    matrix:
+      - PIPELINE_NAME:
+          - AutoDefineBackend
+        GITLAB_OPENTOFU_BASE_IMAGE_OS:
+          - alpine
+          - debian
+
 validate-plan-apply:
   stage: test-integration
   variables:
-- 
GitLab