This is an automated email from the ASF dual-hosted git repository.

shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-ci-infra.git


The following commit(s) were added to refs/heads/main by this push:
     new a28d1e1  Run the RunsOn runners in all three us-east-2 zones from 
RunsOn's Terraform module (#82)
a28d1e1 is described below

commit a28d1e14ad37c1f5b5ff15c4d10b62435a98c8e0
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sat Oct 31 12:20:41 2026 +0100

    Run the RunsOn runners in all three us-east-2 zones from RunsOn's Terraform 
module (#82)
    
    * Run the RunsOn runners in all three us-east-2 zones from RunsOn's 
Terraform module
    
    The CloudFormation template only uses the first two zones, use2-az1 and 
use2-az2, whose Spot placement score for the runners' families is 1 against 9 
in use2-az3; 5.6% of the Spot runners were reclaimed from 2026-10-25 to 
2026-10-30. The template has no parameter for the zones, so a second stack from 
the Flex module runs beside it until the jobs move with an env=flex label.
    
    Generated-by: Claude Opus 5
    
    * Lock the providers of the RunsOn Flex stack
    
    Generated-by: Claude Opus 5
    
    * Keep the runs-on-flex roles in us-east-2 and open the setup routes for 
the first apply
    
    The permissions boundary is the ceiling of every role the apply role 
creates, and those roles can
    be passed to Lambda, ECS or EC2, so the boundary now carries the region 
deny the apply role has.
    A job that uses the runner instance profile for AWS calls outside us-east-2 
is now denied.
    
    The first apply opens the /setup routes so ASF Infra can register the 
stack's GitHub App; a
    follow-up change closes them again.
    
    Generated-by: Claude Opus 5.5
    
    * Send the runs-on-flex alerts to a personal address for now
    
    AWS confirms an SNS email subscription only after someone clicks the link 
in the confirmation
    email, and the moderated [email protected] holds that email back. 
The alerts go to
    [email protected] until there is a way to confirm the subscription for a 
mailing list.
    
    Generated-by: Claude Opus 5.5
    
    ---------
    
    Co-authored-by: Shahar Epstein <[email protected]>
---
 .github/workflows/terraform-runs-on-flex.yml | 116 ++++++++++++
 terraform/runs-on-access/runs-on-flex.tf     | 253 +++++++++++++++++++++++++++
 terraform/runs-on-flex/.terraform.lock.hcl   |  74 ++++++++
 terraform/runs-on-flex/README.rst            | 129 ++++++++++++++
 terraform/runs-on-flex/main.tf               |  59 +++++++
 terraform/runs-on-flex/network.tf            |  89 ++++++++++
 terraform/runs-on-flex/outputs.tf            |  36 ++++
 terraform/runs-on-flex/provider.tf           |  32 ++++
 terraform/runs-on-flex/state.tf              |  27 +++
 terraform/runs-on-flex/variables.tf          |  68 +++++++
 terraform/runs-on-flex/version.tf            |  28 +++
 11 files changed, 911 insertions(+)

diff --git a/.github/workflows/terraform-runs-on-flex.yml 
b/.github/workflows/terraform-runs-on-flex.yml
new file mode 100644
index 0000000..9981f49
--- /dev/null
+++ b/.github/workflows/terraform-runs-on-flex.yml
@@ -0,0 +1,116 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+---
+# Plans terraform/runs-on-flex on pull requests and on main; applies the plan 
made on main once a
+# required reviewer approves the runs-on-prod environment. Same flow as 
terraform-runs-on.yml; the
+# AWS roles live in terraform/runs-on-access and are applied by hand.
+name: Terraform (RunsOn Flex)
+
+on:  # yamllint disable-line rule:truthy
+  pull_request:
+    paths:
+      - "terraform/runs-on-flex/*.tf"
+      - "terraform/runs-on-flex/.terraform.lock.hcl"
+      - ".github/workflows/terraform-runs-on-flex.yml"
+      # Pull requests only: a plan proves the plan role still admits pull 
requests after an account
+      # admin applied the changed trust policies by hand. On main the change 
needs no apply.
+      - "terraform/runs-on-access/*.tf"
+  push:
+    branches: [main]
+    paths:
+      - "terraform/runs-on-flex/*.tf"
+      - "terraform/runs-on-flex/.terraform.lock.hcl"
+      - ".github/workflows/terraform-runs-on-flex.yml"
+
+permissions: {}
+
+concurrency:
+  group: terraform-runs-on-flex-${{ github.ref }}
+  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+env:
+  AWS_REGION: us-east-2
+  TF_IN_AUTOMATION: "true"
+  TF_INPUT: "false"
+
+jobs:
+  plan:
+    # Fork pull requests get no OIDC token, so they cannot plan.
+    if: >-
+      github.event_name == 'push' ||
+      github.event.pull_request.head.repo.full_name == github.repository
+    runs-on: ubuntu-slim
+    permissions:
+      contents: read
+      id-token: write
+    defaults:
+      run:
+        working-directory: terraform/runs-on-flex
+    steps:
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # 
v7.0.1
+        with:
+          persist-credentials: false
+      - uses: 
aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd  
# v6.3.0
+        with:
+          role-to-assume: 
arn:aws:iam::827901512104:role/runs-on-terraform-flex-plan
+          aws-region: ${{ env.AWS_REGION }}
+      - uses: 
hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e  # v4.0.1
+        with:
+          terraform_version: 1.16.2
+          terraform_wrapper: false
+      - run: terraform init -lockfile=readonly
+      # The plan role cannot write the lock file; apply takes the lock and 
refuses a stale plan.
+      - run: terraform plan -lock=false -out=runs-on-flex.tfplan
+      - if: github.event_name == 'push'
+        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 
 # v7.0.2
+        with:
+          name: runs-on-flex-tfplan
+          path: terraform/runs-on-flex/runs-on-flex.tfplan
+          retention-days: 7
+
+  apply:
+    needs: plan
+    if: github.event_name == 'push'
+    # Not ubuntu-slim: its 15-minute job limit is shorter than creating the 
stack may take, and a
+    # killed apply leaves the state lock held.
+    runs-on: ubuntu-24.04
+    # Required reviewers and the main-only branch policy come from .asf.yaml.
+    environment: runs-on-prod
+    permissions:
+      contents: read
+      id-token: write
+    defaults:
+      run:
+        working-directory: terraform/runs-on-flex
+    steps:
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # 
v7.0.1
+        with:
+          persist-credentials: false
+      - uses: 
aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd  
# v6.3.0
+        with:
+          role-to-assume: 
arn:aws:iam::827901512104:role/runs-on-terraform-flex-apply
+          aws-region: ${{ env.AWS_REGION }}
+      - uses: 
hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e  # v4.0.1
+        with:
+          terraform_version: 1.16.2
+          terraform_wrapper: false
+      - run: terraform init -lockfile=readonly
+      - uses: 
actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333  # v8.0.2
+        with:
+          name: runs-on-flex-tfplan
+          path: terraform/runs-on-flex
+      - run: terraform apply runs-on-flex.tfplan
diff --git a/terraform/runs-on-access/runs-on-flex.tf 
b/terraform/runs-on-access/runs-on-flex.tf
new file mode 100644
index 0000000..06a98b6
--- /dev/null
+++ b/terraform/runs-on-access/runs-on-flex.tf
@@ -0,0 +1,253 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Roles of the Terraform (RunsOn Flex) workflow, which applies 
terraform/runs-on-flex: RunsOn's
+# Terraform module instead of its CloudFormation template. No CloudFormation 
service role sits in
+# between, so the apply role itself creates the infrastructure. It gets what 
the CloudFormation
+# service role in main.tf has (AdministratorAccess with denies) and three 
limits more: it acts
+# only in us-east-2 (IAM and Budgets are global), it reaches no S3 bucket but 
the stack's cache
+# buckets and its own state, and every role it creates carries the 
runs-on-flex-boundary
+# permissions boundary, so no role of the stack can be given more than the 
boundary allows.
+locals {
+  flex_stack = "runs-on-flex"
+  flex_state = "arn:aws:s3:::airflow-ci-tfstate/tf-state/runs-on-flex.tfstate"
+
+  # Every IAM role and instance profile of the module (v3.5.1) is named 
<stack>-<purpose>. AWS
+  # creates the service-linked roles on first use of ECS and Spot.
+  flex_iam_resources = [
+    "arn:aws:iam::${local.account_id}:role/${local.flex_stack}-*",
+    "arn:aws:iam::${local.account_id}:instance-profile/${local.flex_stack}-*",
+    "arn:aws:iam::${local.account_id}:role/aws-service-role/*",
+  ]
+  # The only AWS-managed policies the module attaches (runner SSM access, ECS 
task execution).
+  flex_managed_policies = [
+    "arn:aws:iam::aws:policy/AmazonSSMManagedEC2InstanceDefaultPolicy",
+    "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
+  ]
+  iam_write_actions = [
+    "iam:Add*", "iam:Attach*", "iam:Change*", "iam:Create*", 
"iam:Deactivate*", "iam:Delete*",
+    "iam:Detach*", "iam:Enable*", "iam:PassRole", "iam:Put*", "iam:Remove*", 
"iam:Reset*",
+    "iam:Set*", "iam:Tag*", "iam:Untag*", "iam:Update*", "iam:Upload*",
+  ]
+  tfstate_bucket = ["arn:aws:s3:::airflow-ci-tfstate", 
"arn:aws:s3:::airflow-ci-tfstate/*"]
+}
+
+# Named outside the stack's runs-on-flex-* prefix: the apply role may write 
IAM only under that
+# prefix, and must not be able to remove its own guardrails.
+resource "aws_iam_role" "flex_terraform" {
+  for_each = local.github_subjects
+
+  name               = "runs-on-terraform-flex-${each.key}"
+  description        = "terraform ${each.key} of terraform/runs-on-flex from 
apache/airflow-ci-infra GitHub Actions"
+  assume_role_policy = data.aws_iam_policy_document.github_trust[each.key].json
+}
+
+# Refreshing the module's ~150 resources reads across a dozen services, so 
plans get
+# ReadOnlyAccess. Plans also run on pull requests, so the denies keep secrets 
out of reach:
+# ReadOnlyAccess can read every S3 object (the other Terraform states 
included) and SSM
+# parameters, which the aws/ssm key would decrypt.
+resource "aws_iam_role_policy_attachment" "flex_plan_read_only" {
+  role       = aws_iam_role.flex_terraform["plan"].name
+  policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
+}
+
+data "aws_iam_policy_document" "flex_plan_guardrails" {
+  statement {
+    sid           = "OnlyThisStateObject"
+    effect        = "Deny"
+    actions       = ["s3:GetObject*"]
+    not_resources = [local.flex_state]
+  }
+  statement {
+    sid       = "NoSsmDecrypt"
+    effect    = "Deny"
+    actions   = ["kms:Decrypt"]
+    resources = ["*"]
+    condition {
+      test     = "StringLike"
+      variable = "kms:ViaService"
+      values   = ["ssm.*.amazonaws.com"]
+    }
+  }
+  statement {
+    sid       = "NoSecretValues"
+    effect    = "Deny"
+    actions   = ["secretsmanager:GetSecretValue", 
"secretsmanager:BatchGetSecretValue"]
+    resources = ["*"]
+  }
+}
+
+resource "aws_iam_role_policy" "flex_plan_guardrails" {
+  name   = "guardrails"
+  role   = aws_iam_role.flex_terraform["plan"].name
+  policy = data.aws_iam_policy_document.flex_plan_guardrails.json
+}
+
+# RunsOn documents only "admin rights" for deploying the stack, and the 
module's resource set
+# changes between releases, so AdministratorAccess as for the CloudFormation 
service role.
+resource "aws_iam_role_policy_attachment" "flex_apply" {
+  role       = aws_iam_role.flex_terraform["apply"].name
+  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
+}
+
+data "aws_iam_policy_document" "flex_apply_guardrails" {
+  statement {
+    sid           = "OnlyTheStackIam"
+    effect        = "Deny"
+    actions       = local.iam_write_actions
+    not_resources = local.flex_iam_resources
+  }
+  # A stack role without the boundary could be given any permission and then 
be passed to a Lambda
+  # function or an ECS task, which would lift every deny of this policy.
+  statement {
+    sid       = "StackRolesKeepTheBoundary"
+    effect    = "Deny"
+    actions   = ["iam:CreateRole", "iam:PutRolePermissionsBoundary", 
"iam:PutRolePolicy", "iam:AttachRolePolicy"]
+    resources = ["arn:aws:iam::${local.account_id}:role/${local.flex_stack}-*"]
+    condition {
+      test     = "StringNotEquals"
+      variable = "iam:PermissionsBoundary"
+      values   = [aws_iam_policy.flex_boundary.arn]
+    }
+  }
+  statement {
+    sid       = "NoBoundaryRemoval"
+    effect    = "Deny"
+    actions   = ["iam:DeleteRolePermissionsBoundary"]
+    resources = ["*"]
+  }
+  statement {
+    sid       = "OnlyTheModuleManagedPolicies"
+    effect    = "Deny"
+    actions   = ["iam:AttachRolePolicy"]
+    resources = ["*"]
+    condition {
+      test     = "ArnNotEquals"
+      variable = "iam:PolicyARN"
+      values   = local.flex_managed_policies
+    }
+  }
+  statement {
+    sid       = "NoRoleHopping"
+    effect    = "Deny"
+    actions   = ["sts:AssumeRole", "sts:AssumeRoleWithWebIdentity", 
"sts:AssumeRoleWithSAML"]
+    resources = ["*"]
+  }
+  # Only the stack's cache bucket (bucket_prefix "<stack>-cache-") and this 
state and its lock file.
+  statement {
+    sid     = "OnlyTheStackBuckets"
+    effect  = "Deny"
+    actions = ["s3:*"]
+    not_resources = [
+      "arn:aws:s3:::${local.flex_stack}-cache-*",
+      local.flex_state,
+      "${local.flex_state}.tflock",
+      "arn:aws:s3:::airflow-ci-tfstate",
+    ]
+  }
+  statement {
+    sid       = "NoStateBucketChanges"
+    effect    = "Deny"
+    actions   = ["s3:Put*", "s3:Delete*"]
+    resources = ["arn:aws:s3:::airflow-ci-tfstate"]
+  }
+  statement {
+    sid         = "OnlyTheRunnersRegion"
+    effect      = "Deny"
+    not_actions = ["iam:*", "budgets:*", "sts:*"]
+    resources   = ["*"]
+    condition {
+      test     = "StringNotEquals"
+      variable = "aws:RequestedRegion"
+      values   = ["us-east-2"]
+    }
+  }
+}
+
+resource "aws_iam_role_policy" "flex_apply_guardrails" {
+  name   = "guardrails"
+  role   = aws_iam_role.flex_terraform["apply"].name
+  policy = data.aws_iam_policy_document.flex_apply_guardrails.json
+}
+
+# The most any role of the stack may do, whatever its own policies say. The 
control plane passes
+# the runner role to the instances it launches and creates the Spot 
service-linked role; nothing
+# of the stack writes other IAM, assumes roles outside the stack or touches 
the Terraform states.
+data "aws_iam_policy_document" "flex_boundary" {
+  statement {
+    sid       = "Everything"
+    actions   = ["*"]
+    resources = ["*"]
+  }
+  statement {
+    sid    = "NoIamWrites"
+    effect = "Deny"
+    # PassRole and CreateServiceLinkedRole are limited below.
+    actions = [
+      "iam:Add*", "iam:Attach*", "iam:Change*", "iam:CreateA*", 
"iam:CreateG*", "iam:CreateI*",
+      "iam:CreateL*", "iam:CreateO*", "iam:CreateP*", "iam:CreateR*", 
"iam:CreateSA*",
+      "iam:CreateServiceS*", "iam:CreateU*", "iam:CreateV*", 
"iam:Deactivate*", "iam:Delete*",
+      "iam:Detach*", "iam:Enable*", "iam:Put*", "iam:Remove*", "iam:Reset*", 
"iam:Set*", "iam:Tag*",
+      "iam:Untag*", "iam:Update*", "iam:Upload*",
+    ]
+    resources = ["*"]
+  }
+  statement {
+    sid           = "PassOnlyStackRoles"
+    effect        = "Deny"
+    actions       = ["iam:PassRole"]
+    not_resources = 
["arn:aws:iam::${local.account_id}:role/${local.flex_stack}-*"]
+  }
+  statement {
+    sid           = "OnlyServiceLinkedRoles"
+    effect        = "Deny"
+    actions       = ["iam:CreateServiceLinkedRole"]
+    not_resources = 
["arn:aws:iam::${local.account_id}:role/aws-service-role/*"]
+  }
+  statement {
+    sid           = "AssumeOnlyStackRoles"
+    effect        = "Deny"
+    actions       = ["sts:AssumeRole"]
+    not_resources = 
["arn:aws:iam::${local.account_id}:role/${local.flex_stack}-*"]
+  }
+  # The apply role's own OnlyTheRunnersRegion deny does not reach the roles it 
passes to Lambda,
+  # ECS or EC2, so the boundary repeats it. Exempted: IAM, STS and Budgets as 
on the apply role,
+  # and the global endpoints the module's roles call (Cost Explorer, Pricing, 
public ECR).
+  statement {
+    sid         = "OnlyTheRunnersRegion"
+    effect      = "Deny"
+    not_actions = ["iam:*", "sts:*", "budgets:*", "ce:*", "pricing:*", 
"ecr-public:*"]
+    resources   = ["*"]
+    condition {
+      test     = "StringNotEquals"
+      variable = "aws:RequestedRegion"
+      values   = ["us-east-2"]
+    }
+  }
+  statement {
+    sid       = "NoTerraformState"
+    effect    = "Deny"
+    actions   = ["s3:*"]
+    resources = local.tfstate_bucket
+  }
+}
+
+resource "aws_iam_policy" "flex_boundary" {
+  name        = "${local.flex_stack}-boundary"
+  description = "Permissions boundary of every IAM role the runs-on-flex stack 
creates"
+  policy      = data.aws_iam_policy_document.flex_boundary.json
+}
diff --git a/terraform/runs-on-flex/.terraform.lock.hcl 
b/terraform/runs-on-flex/.terraform.lock.hcl
new file mode 100644
index 0000000..6990ac1
--- /dev/null
+++ b/terraform/runs-on-flex/.terraform.lock.hcl
@@ -0,0 +1,74 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/aws" {
+  version     = "6.71.0"
+  constraints = ">= 6.0.0, >= 6.33.0, >= 6.39.0, >= 6.45.0, ~> 6.45"
+  hashes = [
+    "h1:aLOQts7nCLMR0kb/jCdP56wlQQoGqhKLctKgtQjDopc=",
+    "h1:sZCupscga0Grk22+04FNY1RYDeOcVbCTd6ff8+hsF94=",
+    "h1:tmOIsPrIHwR6en567EnkR0nqIr9yD0XQcadgSsPpG2g=",
+    "zh:09aa14408c5ab26915fb2a469b5eb67c06393a2054d78592ba4c32f7a882e918",
+    "zh:1d1138128dc4866f6c16b1ef3b871b3fd9a3dc648e9056e06e210d1817aa052b",
+    "zh:2c17a79821a0e06fbf1834a12e92227db29c2001fef7ef0b347af2e6dad83f14",
+    "zh:55616730a485c9c7283546a84f1ac5f8341152c5572338a54fc7af5f69ebfb92",
+    "zh:7e6461ec2976b5961a1d4f3c08c68fae0645d7f5e0a404f7647baa3b88d337d8",
+    "zh:7ec935166b2251d075fea980012f5267078785399585e8b443dca484fc57df0f",
+    "zh:885b3290cd48738cbf9ee8a6d47a4452889843f8f53b8cf67fc8c9b16e553721",
+    "zh:8930fb33c6c965f7cc82230cb7fbb0cfff9bb30becdfce5e0c2242512d849894",
+    "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
+    "zh:a702f9a4dc7340182a8daf5b6ee562f14657fe14752f7d54dc799cf2fd91a868",
+    "zh:b624cb320d787fbfa6dc94be5a1fd52683d1ebd11b42646262b221a694e6ffb8",
+    "zh:bcbaded68b553afeea93604ec22c254b98f3951862bdeb68ae89c179c7dda117",
+    "zh:cb57bbcaf0fa3a70ea29b60d183d3a0dd020f041b18da5d1725d2acd17fa7d59",
+    "zh:d4ca164b1b6039557d8f5f5f911c4aaad1a187e9b3758f6885c95f8954f7de58",
+    "zh:eb0c705337cef7b96086e8b84702ba965eeeace7a4aa3235b2c8aad5f50f2fca",
+    "zh:f2787f40ce873d8c1308c92adec9b6adbd223506fe9d79bba447729e422f9146",
+  ]
+}
+
+provider "registry.terraform.io/hashicorp/random" {
+  version     = "3.9.1"
+  constraints = ">= 3.7.0"
+  hashes = [
+    "h1:PYbnOqRuGn4c0/Ae1f7yOS/0zvmXNHFJRZjuF4KECnM=",
+    "h1:PlW+UZ4EElQF3NQwf41KQwavFujab3Czc51zu9dyVM8=",
+    "h1:g40qr7yDmIpaur4SsK5BcOda3HSo1RJ6zHVMqN4EJ+0=",
+    "zh:05f4734c1f0be840b711b3eff259ebc5fca436784c728955b1678078466f48d7",
+    "zh:0b91bf19371d012434eba1deeb6aab77158def9b39601dcbd94450b3974a2a26",
+    "zh:0ee6eacd47ec00183d55d726a4b6c4ce951a199f944bf22f1aa58392ebdfa7a2",
+    "zh:19388a4074b76a89a43a6c8328d7ae8ee2e7de3d346af51e80d3e6d3d12925f1",
+    "zh:23e74d48c5e2ac2e823fd527f49fee9db37d32a1990c9e3bf126ead697b843eb",
+    "zh:3cabf7fbd096c520064aae3aba61aba670af83ab91291a71fa1b1332929c2b7f",
+    "zh:5c0a3b8af0be60be4eca12ddee385cfa8babc1ec8e98cdf9de2f2274c73eabfa",
+    "zh:60b4f8a8ef18f52bf8e19215229dae408bee732825964092db7c989fd2de4097",
+    "zh:7359015acfedcbd6366f2329c854cf8d3c8ca5cd0faa89d2d37db358d6eba6c5",
+    "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+    "zh:7b38758402f0e13a1071162da28994023cd2ac676e54af350c9ffd8dfa73fa7b",
+    "zh:7c7fbb8895eb75bb4de1f933e98553bd99c8d048c89a925ddba490aa5a67f7dc",
+    "zh:8c2b8c6a7ccdec16b73e2fb9f3700ea097f58c592571e4c5de60c93d2301732c",
+  ]
+}
+
+provider "registry.terraform.io/hashicorp/time" {
+  version     = "0.14.2"
+  constraints = ">= 0.9.0"
+  hashes = [
+    "h1:eQRXh8mZFlUJfzYXKdaYRHRMhiS2cFyCfgP1mjkrtuI=",
+    "h1:gnP2hptiFIHSHUFBvAFKhE/Yh5u5yVEx+P7XSB58A/E=",
+    "h1:mwxQFMxGnM+Xm3F76yeHSAfPr3RZq/+XGT1vjgwiRWQ=",
+    "zh:0aa1028d91041f4dceba193e3707dac57358d0063d97e20700e554758b67baca",
+    "zh:32bee9f2b2678e2a0789ad86e716d09ca1d5450180b3cd8033ee7a251bfd352e",
+    "zh:3aded9ef4dc6f4aec202a50c68a08b40013d325f9947f10168ebc8bee54109fc",
+    "zh:4d924637f3115ffa4ffc7f16d3f366bc472594f7447d400adb9def7ac92e3fc8",
+    "zh:5c35008e1363deafaa440ab43519409866dd7ec72aabeb2317bc16cd82756784",
+    "zh:6b30d97c9827501d7010fe49c8889d7a6cc8b45b77cdb1668c65af7f28a27d73",
+    "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+    "zh:851e0f0e9c4de395e42220de51e7fb20d0e845e629f3cf37056da19e597304e7",
+    "zh:85622b4779b3ba7424780f7f8efcfa585227e245201cdb68303f2118b388e971",
+    "zh:9039153e3d45147183804188a1ce36c3811db9a4dca80f36fa382408d5f50b72",
+    "zh:a3d385413dc258a53fe8f4ded5d1fcd1eba715c65a015d74e70911bab2919207",
+    "zh:b9310a2327f7c8ad2aa3266f89c4d026b4bf18b09f7c4257c0ecd71a32f32db3",
+    "zh:cee7f2143da0c494115da94984bf3630e3e5855e27dae720a7bc29bb6acd9be2",
+  ]
+}
diff --git a/terraform/runs-on-flex/README.rst 
b/terraform/runs-on-flex/README.rst
new file mode 100644
index 0000000..16ce262
--- /dev/null
+++ b/terraform/runs-on-flex/README.rst
@@ -0,0 +1,129 @@
+.. Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+
+..   http://www.apache.org/licenses/LICENSE-2.0
+
+.. Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.
+
+RunsOn Flex — the runners from RunsOn's Terraform module
+========================================================
+
+This module deploys the same RunsOn runners as `terraform/runs-on 
<../runs-on/README.rst>`_, from
+RunsOn's `Terraform module 
<https://github.com/runs-on/terraform-aws-runs-on>`_ (Flex, v3.5.1)
+instead of its CloudFormation template, into a VPC this module creates with a 
public subnet in
+each of the three us-east-2 zones. Both stacks run side by side until the jobs 
of
+``apache/airflow`` have moved here; the CloudFormation stack is then removed 
in a separate change.
+
+Why this module
+---------------
+
+The CloudFormation template places runners in the first two zones of the 
region, use2-az1 and
+use2-az2 for this account, and has no parameter to change that. On 2026-10-30 
those two zones
+had the worst Spot capacity in the region for the runners' instance families:
+
+=================================  ==========  ==========  ==========
+Spot placement score (1-10)        use2-az1    use2-az2    use2-az3
+=================================  ==========  ==========  ==========
+x64, 8-16 vCPU (m5-m8 a/i)         1           1           9
+x64, 4-8 vCPU                      2           1           8
+arm64, 8-64 vCPU (m6g-m8g)         1           1           3
+=================================  ==========  ==========  ==========
+
+From 2026-10-25 to 2026-10-30 AWS reclaimed 911 of 16,145 Spot runners (5.6%), 
at the median
+7 minutes after launch, and 30% of all runners ran on-demand. A third zone 
gives the Spot
+allocator the pools where capacity is.
+
+Files
+-----
+
+``network.tf``
+    VPC (``vpc_cidr``, default ``10.3.0.0/16``), internet gateway, one public 
subnet per zone ID
+    and an S3 gateway endpoint. No NAT: runners get public IPs, as on the 
CloudFormation stack.
+``main.tf``
+    The ``runs-on/runs-on/aws//modules/flex`` module with the CloudFormation 
stack's settings, the
+    license-key lookup and the permissions boundary of the stack's roles.
+``variables.tf``
+    All variables have defaults: ``stack_name`` (``runs-on-flex``), 
``runs_on_environment``
+    (``flex``), ``github_organization``, ``alert_email``, 
``enable_admin_routes``
+    (``true`` until the GitHub App is registered) and ``vpc_cidr``.
+``state.tf``
+    S3 backend ``airflow-ci-tfstate`` / ``tf-state/runs-on-flex.tfstate``.
+
+How it differs from the CloudFormation stack
+--------------------------------------------
+
+- **Environment.** The stack takes only jobs whose label carries ``env=flex``; 
the CloudFormation
+  stack keeps the default ``production`` and ignores them. That is what lets 
both run at once.
+- **Who creates the resources.** The CloudFormation stack is applied by a role 
that can only
+  resubmit RunsOn's published template; CloudFormation's service role does the 
work. Here the
+  ``runs-on-terraform-flex-apply`` role creates everything itself, with 
AdministratorAccess and
+  the denies in `terraform/runs-on-access/runs-on-flex.tf 
<../runs-on-access/runs-on-flex.tf>`_:
+  IAM writes only on ``runs-on-flex-*`` roles and only with the 
``runs-on-flex-boundary``
+  permissions boundary, only the two AWS-managed policies the module attaches, 
no S3 but the
+  stack's cache bucket and this state, no role assumption, and us-east-2 only. 
Anything merged to
+  ``main`` here and approved in the ``runs-on-prod`` environment can therefore 
change any non-IAM
+  resource in us-east-2, not just this stack: review pull requests to this 
module as such.
+- **Upgrades** bump the module ``version`` in ``main.tf`` (with the lock 
file), not a template
+  URL. Before a bump, check the new release's IAM role names and 
managed-policy attachments
+  against ``flex_iam_resources`` and ``flex_managed_policies`` in 
``runs-on-access``.
+
+Setup
+-----
+
+1. **Access (account admin, once).** Apply `terraform/runs-on-access 
<../runs-on-access/README.rst>`_:
+   it adds the ``runs-on-terraform-flex-plan`` and ``-apply`` roles and the
+   ``runs-on-flex-boundary`` policy.
+2. **License key (manual, once).** The same RunsOn license as the 
CloudFormation stack, under
+   this stack's name. The module receives only the parameter's ARN, so the key 
never reaches
+   Terraform state:
+
+   .. code-block:: bash
+
+       aws ssm put-parameter --region us-east-2 \
+           --name /runs-on-flex/license-key \
+           --type SecureString \
+           --description "RunsOn license key for the runs-on-flex stack 
(GitHub organization apache)" \
+           --value 'RUNS-ON-LICENSE-KEY'
+
+3. **Create the stack.** Merge the pull request that adds this module; the
+   ``Terraform (RunsOn Flex)`` workflow plans on ``main`` and applies once a 
reviewer approves
+   ``runs-on-prod``. Set ``enable_admin_routes = true`` in that first change.
+4. **GitHub App (with ASF Infra).** Open the ``entry_point`` output → 
**Register app**. This
+   creates a second private GitHub App for this stack; ASF Infra installs it on
+   ``apache/airflow`` (and ``.github-private``, for the presets). Then set
+   ``enable_admin_routes`` back to ``false`` in a follow-up change.
+5. **Move the jobs.** In ``apache/airflow``, add ``/env=flex`` to the RunsOn 
label templates in
+   ``dev/breeze/src/airflow_breeze/global_constants.py``. All jobs of a run 
must move together:
+   the stash actions keep image tars in the cache bucket of the stack that ran 
the job, so a
+   job on the other stack would not find them. Rolling back is removing 
``/env=flex``.
+6. **Retire the CloudFormation stack** once the runs have been green on this 
stack for a while:
+   a separate change in ``terraform/runs-on`` (remove ``prevent_destroy``, 
destroy), the old
+   GitHub App uninstalled by ASF Infra, and the ``runs-on-terraform-plan``, 
``-apply`` and
+   ``runs-on-cloudformation`` roles removed from ``runs-on-access``.
+
+Locally, an account admin runs the same steps as the workflow::
+
+    cd terraform/runs-on-flex
+    terraform init
+    terraform plan -out=runs-on-flex.tfplan
+    terraform apply runs-on-flex.tfplan
+    terraform output -raw getting_started
+
+Not carried over yet
+--------------------
+
+- ``RUNS_ON_SPOT_FORCE_ZONE_SPREAD`` (RunsOn v3.5.0, beta) would spread Spot 
runners across the
+  zones instead of letting the allocator pick; it can be set through 
``extra_env_vars`` once the
+  three zones have run for a while.
+- ``prevent_destroy`` cannot be set on a module; a ``terraform destroy`` of 
this module is
+  stopped only by review.
diff --git a/terraform/runs-on-flex/main.tf b/terraform/runs-on-flex/main.tf
new file mode 100644
index 0000000..7ed8ce4
--- /dev/null
+++ b/terraform/runs-on-flex/main.tf
@@ -0,0 +1,59 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# The license key is never in Terraform state: the Flex module accepts 
license_key as the ARN of
+# an SSM SecureString parameter named /<stack name>/license-key, created by 
hand (README.rst). This
+# lookup without decryption only proves it exists before the stack is planned.
+data "aws_ssm_parameter" "license_key" {
+  name            = "/${var.stack_name}/license-key"
+  with_decryption = false
+}
+
+data "aws_caller_identity" "current" {}
+
+module "runs_on" {
+  source  = "runs-on/runs-on/aws//modules/flex"
+  version = "v3.5.1"
+
+  stack_name          = var.stack_name
+  environment         = var.runs_on_environment
+  github_organization = var.github_organization
+  license_key         = data.aws_ssm_parameter.license_key.arn
+  email               = var.alert_email
+
+  vpc_id            = aws_vpc.runners.id
+  public_subnet_ids = [for zone_id in local.zone_ids : 
aws_subnet.public[zone_id].id]
+  private_mode      = "false"
+
+  # Created in terraform/runs-on-access; the apply role cannot create a stack 
role without it.
+  permission_boundary_arn = 
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${var.stack_name}-boundary"
+
+  # The values of the CloudFormation stack (terraform/runs-on/main.tf); 
everything else keeps the
+  # module default.
+  ssh_allowed           = false
+  ssm_allowed           = true
+  enable_admin_routes   = var.enable_admin_routes
+  enable_ecr            = false
+  runner_max_runtime    = 720
+  cache_expiration_days = 10
+  app_size              = "small"
+  # With hundreds of runners two interruptions in 15 minutes are routine, so 
the default 2/15/30
+  # keeps Spot off (terraform/runs-on/main.tf).
+  spot_circuit_breaker = "30/15/10"
+  enable_cost_reports  = "daily"
+  app_budget_daily_usd = 10 # alert only, not a spending cap
+}
diff --git a/terraform/runs-on-flex/network.tf 
b/terraform/runs-on-flex/network.tf
new file mode 100644
index 0000000..0ddc73f
--- /dev/null
+++ b/terraform/runs-on-flex/network.tf
@@ -0,0 +1,89 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Runners get public IPs in public subnets (no NAT cost, Docker Hub pulls per 
runner IP), as on the
+# CloudFormation stack, but in all three zones of the region: that stack is 
fixed to the first two,
+# and on 2026-10-30 their Spot placement score for the runners' families was 1 
against 9 in
+# use2-az3 (README, "Why this module"). Zones are pinned by ID because zone 
names map to different
+# zones in every account, and Spot placement scores are reported by ID.
+locals {
+  zone_ids = ["use2-az1", "use2-az2", "use2-az3"]
+}
+
+resource "aws_vpc" "runners" {
+  cidr_block           = var.vpc_cidr
+  enable_dns_support   = true
+  enable_dns_hostnames = true
+
+  tags = {
+    Name = "${var.stack_name}-vpc"
+  }
+}
+
+resource "aws_internet_gateway" "runners" {
+  vpc_id = aws_vpc.runners.id
+
+  tags = {
+    Name = "${var.stack_name}-igw"
+  }
+}
+
+resource "aws_subnet" "public" {
+  for_each = toset(local.zone_ids)
+
+  vpc_id                  = aws_vpc.runners.id
+  availability_zone_id    = each.key
+  cidr_block              = cidrsubnet(var.vpc_cidr, 4, index(local.zone_ids, 
each.key))
+  map_public_ip_on_launch = true
+
+  tags = {
+    Name = "${var.stack_name}-public-${each.key}"
+  }
+}
+
+resource "aws_route_table" "public" {
+  vpc_id = aws_vpc.runners.id
+
+  route {
+    cidr_block = "0.0.0.0/0"
+    gateway_id = aws_internet_gateway.runners.id
+  }
+
+  tags = {
+    Name = "${var.stack_name}-public"
+  }
+}
+
+resource "aws_route_table_association" "public" {
+  for_each = aws_subnet.public
+
+  subnet_id      = each.value.id
+  route_table_id = aws_route_table.public.id
+}
+
+# Free, and keeps the stash payloads in the cache bucket (multi-gigabyte image 
tars) off the
+# internet gateway.
+resource "aws_vpc_endpoint" "s3" {
+  vpc_id            = aws_vpc.runners.id
+  service_name      = "com.amazonaws.us-east-2.s3"
+  vpc_endpoint_type = "Gateway"
+  route_table_ids   = [aws_route_table.public.id]
+
+  tags = {
+    Name = "${var.stack_name}-s3"
+  }
+}
diff --git a/terraform/runs-on-flex/outputs.tf 
b/terraform/runs-on-flex/outputs.tf
new file mode 100644
index 0000000..60dcd5d
--- /dev/null
+++ b/terraform/runs-on-flex/outputs.tf
@@ -0,0 +1,36 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+output "entry_point" {
+  description = "RunsOn web entry point: open it once after creation to 
register the GitHub App."
+  value       = module.runs_on.ingress.url
+}
+
+output "cache_bucket" {
+  description = "S3 bucket the runners can use under the cache/ prefix (stash 
payloads)."
+  value       = module.runs_on.platform.cache.bucket_name
+}
+
+output "subnets" {
+  description = "Public subnet of the runners in each zone, by zone ID."
+  value       = { for zone_id, subnet in aws_subnet.public : zone_id => 
subnet.id }
+}
+
+output "getting_started" {
+  description = "RunsOn's post-apply instructions, including the env= label 
jobs need for this stack."
+  value       = module.runs_on.stack.getting_started
+}
diff --git a/terraform/runs-on-flex/provider.tf 
b/terraform/runs-on-flex/provider.tf
new file mode 100644
index 0000000..0902b26
--- /dev/null
+++ b/terraform/runs-on-flex/provider.tf
@@ -0,0 +1,32 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Credentials come from the environment (AWS_PROFILE locally, the OIDC role in 
the
+# Terraform (RunsOn Flex) workflow) — do not hardcode a profile here.
+provider "aws" {
+  # Also hard-coded in the backend (state.tf), the zone IDs (network.tf) and 
the README commands;
+  # change all together.
+  region = "us-east-2"
+
+  default_tags {
+    tags = {
+      Project   = "airflow-ci"
+      ManagedBy = "terraform"
+      Repo      = "apache/airflow-ci-infra"
+    }
+  }
+}
diff --git a/terraform/runs-on-flex/state.tf b/terraform/runs-on-flex/state.tf
new file mode 100644
index 0000000..ab9c02d
--- /dev/null
+++ b/terraform/runs-on-flex/state.tf
@@ -0,0 +1,27 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+terraform {
+  backend "s3" {
+    region = "us-east-2"
+    bucket = "airflow-ci-tfstate"
+    # Own key, next to the CloudFormation stack's tf-state/runs-on.tfstate and 
the EKS states.
+    key          = "tf-state/runs-on-flex.tfstate"
+    encrypt      = true
+    use_lockfile = true
+  }
+}
diff --git a/terraform/runs-on-flex/variables.tf 
b/terraform/runs-on-flex/variables.tf
new file mode 100644
index 0000000..5e1ea65
--- /dev/null
+++ b/terraform/runs-on-flex/variables.tf
@@ -0,0 +1,68 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+variable "stack_name" {
+  description = <<-EOT
+    RunsOn stack name. Every resource the module creates is named after it, 
the license-key SSM
+    parameter is /<stack_name>/license-key, and terraform/runs-on-access 
scopes the apply role's
+    IAM rights to it, so changing it re-creates the stack and needs an access 
change.
+  EOT
+  type        = string
+  default     = "runs-on-flex"
+}
+
+variable "runs_on_environment" {
+  description = <<-EOT
+    RunsOn environment: the stack only takes jobs whose label carries 
env=<this value>. It differs
+    from the CloudFormation stack's "production" so both stacks can serve 
apache/airflow at once and
+    jobs move by label.
+  EOT
+  type        = string
+  default     = "flex"
+}
+
+variable "github_organization" {
+  description = "GitHub organization the runners serve (case-sensitive)."
+  type        = string
+  default     = "apache"
+}
+
+variable "alert_email" {
+  description = "Recipient of RunsOn cost reports and alerts; AWS sends an SNS 
confirmation email on creation."
+  type        = string
+  # A personal address until we find a way to confirm the SNS subscription for 
a mailing list:
+  # the confirmation email needs a click, and the moderated 
[email protected] holds it.
+  default = "[email protected]"
+}
+
+variable "enable_admin_routes" {
+  description = <<-EOT
+    Expose the /setup admin routes on the public entry point. Set to true only 
while registering
+    (or re-registering) the GitHub App, then back to false.
+  EOT
+  type        = bool
+  default     = true
+}
+
+variable "vpc_cidr" {
+  description = <<-EOT
+    CIDR of the runners' VPC; must not overlap existing VPCs in the account 
(10.0.0.0/16,
+    10.1.0.0/16 and the CloudFormation stack's 10.2.0.0/16 are taken).
+  EOT
+  type        = string
+  default     = "10.3.0.0/16"
+}
diff --git a/terraform/runs-on-flex/version.tf 
b/terraform/runs-on-flex/version.tf
new file mode 100644
index 0000000..a2b7aec
--- /dev/null
+++ b/terraform/runs-on-flex/version.tf
@@ -0,0 +1,28 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+terraform {
+  required_providers {
+    aws = {
+      source = "hashicorp/aws"
+      # The RunsOn module needs >= 6.45.
+      version = "~> 6.45"
+    }
+  }
+  # S3-native state locking (use_lockfile) is GA from 1.11
+  required_version = ">= 1.11.0"
+}

Reply via email to