diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 2ffdcd4e7da32948942bcb14881e5b748d30dbf8..7c253de17af960046a7304fd576780cf318e1359 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -9853,6 +9853,75 @@ The edge type for [`CiProjectVariable`](#ciprojectvariable).
 | <a id="ciprojectvariableedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
 | <a id="ciprojectvariableedgenode"></a>`node` | [`CiProjectVariable`](#ciprojectvariable) | The item at the end of the edge. |
 
+#### `CiRunnerCloudProvisioningMachineTypeConnection`
+
+The connection type for [`CiRunnerCloudProvisioningMachineType`](#cirunnercloudprovisioningmachinetype).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningmachinetypeconnectionedges"></a>`edges` | [`[CiRunnerCloudProvisioningMachineTypeEdge]`](#cirunnercloudprovisioningmachinetypeedge) | A list of edges. |
+| <a id="cirunnercloudprovisioningmachinetypeconnectionnodes"></a>`nodes` | [`[CiRunnerCloudProvisioningMachineType]`](#cirunnercloudprovisioningmachinetype) | A list of nodes. |
+| <a id="cirunnercloudprovisioningmachinetypeconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `CiRunnerCloudProvisioningMachineTypeEdge`
+
+The edge type for [`CiRunnerCloudProvisioningMachineType`](#cirunnercloudprovisioningmachinetype).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningmachinetypeedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="cirunnercloudprovisioningmachinetypeedgenode"></a>`node` | [`CiRunnerCloudProvisioningMachineType`](#cirunnercloudprovisioningmachinetype) | The item at the end of the edge. |
+
+#### `CiRunnerCloudProvisioningRegionConnection`
+
+The connection type for [`CiRunnerCloudProvisioningRegion`](#cirunnercloudprovisioningregion).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningregionconnectionedges"></a>`edges` | [`[CiRunnerCloudProvisioningRegionEdge]`](#cirunnercloudprovisioningregionedge) | A list of edges. |
+| <a id="cirunnercloudprovisioningregionconnectionnodes"></a>`nodes` | [`[CiRunnerCloudProvisioningRegion]`](#cirunnercloudprovisioningregion) | A list of nodes. |
+| <a id="cirunnercloudprovisioningregionconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `CiRunnerCloudProvisioningRegionEdge`
+
+The edge type for [`CiRunnerCloudProvisioningRegion`](#cirunnercloudprovisioningregion).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningregionedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="cirunnercloudprovisioningregionedgenode"></a>`node` | [`CiRunnerCloudProvisioningRegion`](#cirunnercloudprovisioningregion) | The item at the end of the edge. |
+
+#### `CiRunnerCloudProvisioningZoneConnection`
+
+The connection type for [`CiRunnerCloudProvisioningZone`](#cirunnercloudprovisioningzone).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningzoneconnectionedges"></a>`edges` | [`[CiRunnerCloudProvisioningZoneEdge]`](#cirunnercloudprovisioningzoneedge) | A list of edges. |
+| <a id="cirunnercloudprovisioningzoneconnectionnodes"></a>`nodes` | [`[CiRunnerCloudProvisioningZone]`](#cirunnercloudprovisioningzone) | A list of nodes. |
+| <a id="cirunnercloudprovisioningzoneconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `CiRunnerCloudProvisioningZoneEdge`
+
+The edge type for [`CiRunnerCloudProvisioningZone`](#cirunnercloudprovisioningzone).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningzoneedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="cirunnercloudprovisioningzoneedgenode"></a>`node` | [`CiRunnerCloudProvisioningZone`](#cirunnercloudprovisioningzone) | The item at the end of the edge. |
+
 #### `CiRunnerConnection`
 
 The connection type for [`CiRunner`](#cirunner).
@@ -16309,6 +16378,84 @@ Returns [`CiRunnerStatus!`](#cirunnerstatus).
 | ---- | ---- | ----------- |
 | <a id="cirunnerstatuslegacymode"></a>`legacyMode` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.0. Will be removed in 17.0. |
 
+### `CiRunnerCloudProvisioningMachineType`
+
+Machine type used for runner cloud provisioning.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningmachinetypedescription"></a>`description` | [`String`](#string) | Description of the machine type. |
+| <a id="cirunnercloudprovisioningmachinetypename"></a>`name` | [`String`](#string) | Name of the machine type. |
+| <a id="cirunnercloudprovisioningmachinetypezone"></a>`zone` | [`String`](#string) | Zone of the machine type. |
+
+### `CiRunnerCloudProvisioningOptions`
+
+Options for runner cloud provisioning.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningoptionsregions"></a>`regions` | [`CiRunnerCloudProvisioningRegionConnection`](#cirunnercloudprovisioningregionconnection) | Regions available for provisioning a runner. (see [Connections](#connections)) |
+
+#### Fields with arguments
+
+##### `CiRunnerCloudProvisioningOptions.machineTypes`
+
+Machine types available for provisioning a runner.
+
+Returns [`CiRunnerCloudProvisioningMachineTypeConnection`](#cirunnercloudprovisioningmachinetypeconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, and `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningoptionsmachinetypeszone"></a>`zone` | [`String!`](#string) | Zone for which to retrieve machine types. |
+
+##### `CiRunnerCloudProvisioningOptions.zones`
+
+Zones available for provisioning a runner.
+
+Returns [`CiRunnerCloudProvisioningZoneConnection`](#cirunnercloudprovisioningzoneconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, and `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningoptionszonesregion"></a>`region` | [`String`](#string) | Region for which to retrieve zones. Returns all zones if not specified. |
+
+### `CiRunnerCloudProvisioningRegion`
+
+Region used for runner cloud provisioning.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningregiondescription"></a>`description` | [`String`](#string) | Description of the region. |
+| <a id="cirunnercloudprovisioningregionname"></a>`name` | [`String`](#string) | Name of the region. |
+
+### `CiRunnerCloudProvisioningZone`
+
+Zone used for runner cloud provisioning.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cirunnercloudprovisioningzonedescription"></a>`description` | [`String`](#string) | Description of the zone. |
+| <a id="cirunnercloudprovisioningzonename"></a>`name` | [`String`](#string) | Name of the zone. |
+
 ### `CiRunnerManager`
 
 #### Fields
@@ -25921,6 +26068,22 @@ four standard [pagination arguments](#connection-pagination-arguments):
 | <a id="projectrequirementsworkitemiid"></a>`workItemIid` | [`ID`](#id) | IID of the requirement work item, for example, "1". |
 | <a id="projectrequirementsworkitemiids"></a>`workItemIids` | [`[ID!]`](#id) | List of IIDs of requirement work items, for example, `[1, 2]`. |
 
+##### `Project.runnerCloudProvisioningOptions`
+
+Options for runner cloud provisioning by a specified cloud provider. Returns `null` if `:gcp_runner` feature flag is disabled, or the GitLab instance is not a SaaS instance.
+
+NOTE:
+**Introduced** in 16.9.
+**Status**: Experiment.
+
+Returns [`CiRunnerCloudProvisioningOptions`](#cirunnercloudprovisioningoptions).
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="projectrunnercloudprovisioningoptionsprovider"></a>`provider` | [`CiRunnerCloudProvider!`](#cirunnercloudprovider) | Identifier of the cloud provider. |
+
 ##### `Project.runners`
 
 Find runners visible to the current user.
@@ -30222,6 +30385,14 @@ Direction of access.
 | <a id="cirunneraccesslevelnot_protected"></a>`NOT_PROTECTED` | A runner that is not protected. |
 | <a id="cirunneraccesslevelref_protected"></a>`REF_PROTECTED` | A runner that is ref protected. |
 
+### `CiRunnerCloudProvider`
+
+Runner cloud provider.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="cirunnercloudprovidergoogle_cloud"></a>`GOOGLE_CLOUD` | Google Cloud. |
+
 ### `CiRunnerJobExecutionStatus`
 
 | Value | Description |
diff --git a/ee/app/graphql/ee/types/project_type.rb b/ee/app/graphql/ee/types/project_type.rb
index 3ae375fe2fb4db10448a8ac05a79d533ccce0f43..3e561be321fcbdf8e9096083767c8e3f63eba7a7 100644
--- a/ee/app/graphql/ee/types/project_type.rb
+++ b/ee/app/graphql/ee/types/project_type.rb
@@ -338,6 +338,17 @@ module ProjectType
           method: :downstream_project_subscriptions,
           description: 'Pipeline subscriptions for projects subscribed to the project.'
 
+        field :runner_cloud_provisioning_options,
+          ::Types::Ci::RunnerCloudProvisioningOptionsType,
+          null: true,
+          alpha: { milestone: '16.9' },
+          description: 'Options for runner cloud provisioning by a specified cloud provider. ' \
+                       'Returns `null` if `:gcp_runner` feature flag is disabled, or the GitLab instance ' \
+                       'is not a SaaS instance.' do
+                         argument :provider, ::Types::Ci::RunnerCloudProviderEnum, required: true,
+                           description: 'Identifier of the cloud provider.'
+                       end
+
         field :ai_agents, ::Types::Ai::Agents::AgentType.connection_type,
           null: true,
           alpha: { milestone: '16.9' },
@@ -387,6 +398,16 @@ def compliance_frameworks
           end
         end
       end
+
+      # TODO To be removed along with :gcp_runner feature flag.
+      # Use `method: :itself` on the related field (see https://graphql-ruby.org/fields/introduction.html#field-resolution).
+      # TODO Before unmarking the field as alpha, figure out solution for polymorphism based on provider argument,
+      #      so that child objects call the correct cloud services
+      def runner_cloud_provisioning_options(provider:) # rubocop:disable Lint/UnusedMethodArgument -- Only one provider type is possible, and is already enforced by GraphQL
+        return if ::Feature.disabled?(:gcp_runner, project, type: :wip)
+
+        project
+      end
     end
   end
 end
diff --git a/ee/app/graphql/resolvers/ci/runner_cloud_provisioning_base_resolver.rb b/ee/app/graphql/resolvers/ci/runner_cloud_provisioning_base_resolver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..660234a337bb9a5e86aa4e5bb02c7d67bbafd058
--- /dev/null
+++ b/ee/app/graphql/resolvers/ci/runner_cloud_provisioning_base_resolver.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Resolvers
+  module Ci
+    # rubocop: disable Graphql/ResolverType -- the type is decided on the derived resolver class
+    class RunnerCloudProvisioningBaseResolver < BaseResolver
+      include Gitlab::Graphql::Authorize::AuthorizeResource
+
+      authorize :read_runner_cloud_provisioning_options
+
+      private
+
+      alias_method :project, :object
+
+      def default_params(after, first)
+        { max_results: first, page_token: after }.compact
+      end
+
+      def externally_paginated_array(response, after)
+        raise_resource_not_available_error!(response.message) if response.error?
+
+        Gitlab::Graphql::ExternallyPaginatedArray.new(
+          after,
+          response.payload[:next_page_token],
+          *response.payload[:items]
+        )
+      end
+    end
+    # rubocop: enable Graphql/ResolverType
+  end
+end
diff --git a/ee/app/graphql/resolvers/ci/runner_cloud_provisioning_machine_types_resolver.rb b/ee/app/graphql/resolvers/ci/runner_cloud_provisioning_machine_types_resolver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ac64b780534fef2dab5314783bacf379a784277b
--- /dev/null
+++ b/ee/app/graphql/resolvers/ci/runner_cloud_provisioning_machine_types_resolver.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Resolvers
+  module Ci
+    class RunnerCloudProvisioningMachineTypesResolver < Resolvers::Ci::RunnerCloudProvisioningBaseResolver
+      type Types::Ci::RunnerCloudProvisioningMachineTypeType.connection_type, null: true
+
+      description 'Machine types available for provisioning a runner.'
+
+      argument :zone, GraphQL::Types::String,
+        required: true,
+        description: 'Zone for which to retrieve machine types.'
+
+      max_page_size GoogleCloudPlatform::Compute::ListMachineTypesService::MAX_RESULTS_LIMIT
+      default_page_size GoogleCloudPlatform::Compute::ListMachineTypesService::MAX_RESULTS_LIMIT
+
+      def resolve(zone:, after: nil, first: nil)
+        response = GoogleCloudPlatform::Compute::ListMachineTypesService
+          .new(project: project, current_user: current_user, zone: zone, params: default_params(after, first))
+          .execute
+
+        externally_paginated_array(response, after)
+      end
+    end
+  end
+end
diff --git a/ee/app/graphql/resolvers/ci/runner_cloud_provisioning_regions_resolver.rb b/ee/app/graphql/resolvers/ci/runner_cloud_provisioning_regions_resolver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bc641b3dcdd58cf7d094c9f4cf1d49df2991bdb8
--- /dev/null
+++ b/ee/app/graphql/resolvers/ci/runner_cloud_provisioning_regions_resolver.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Resolvers
+  module Ci
+    class RunnerCloudProvisioningRegionsResolver < Resolvers::Ci::RunnerCloudProvisioningBaseResolver
+      type Types::Ci::RunnerCloudProvisioningRegionType.connection_type, null: true
+
+      description 'Regions available for provisioning a runner.'
+
+      max_page_size GoogleCloudPlatform::Compute::ListRegionsService::MAX_RESULTS_LIMIT
+      default_page_size GoogleCloudPlatform::Compute::ListRegionsService::MAX_RESULTS_LIMIT
+
+      def resolve(after: nil, first: nil)
+        response = GoogleCloudPlatform::Compute::ListRegionsService
+          .new(project: project, current_user: current_user, params: default_params(after, first))
+          .execute
+
+        externally_paginated_array(response, after)
+      end
+    end
+  end
+end
diff --git a/ee/app/graphql/resolvers/ci/runner_cloud_provisioning_zones_resolver.rb b/ee/app/graphql/resolvers/ci/runner_cloud_provisioning_zones_resolver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..72cd216eb9fe82692eaa4bece3d08a9b130cc22c
--- /dev/null
+++ b/ee/app/graphql/resolvers/ci/runner_cloud_provisioning_zones_resolver.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Resolvers
+  module Ci
+    class RunnerCloudProvisioningZonesResolver < Resolvers::Ci::RunnerCloudProvisioningBaseResolver
+      type Types::Ci::RunnerCloudProvisioningZoneType.connection_type, null: true
+
+      description 'Zones available for provisioning a runner.'
+
+      argument :region, GraphQL::Types::String,
+        required: false,
+        description: 'Region for which to retrieve zones. Returns all zones if not specified.'
+
+      max_page_size GoogleCloudPlatform::Compute::ListZonesService::MAX_RESULTS_LIMIT
+      default_page_size GoogleCloudPlatform::Compute::ListZonesService::MAX_RESULTS_LIMIT
+
+      def resolve(region: nil, after: nil, first: nil)
+        params = default_params(after, first)
+        params[:filter] = "name=#{region}-*" if region
+
+        response = GoogleCloudPlatform::Compute::ListZonesService
+          .new(project: project, current_user: current_user, params: params)
+          .execute
+
+        externally_paginated_array(response, after)
+      end
+    end
+  end
+end
diff --git a/ee/app/graphql/types/ci/runner_cloud_provider_enum.rb b/ee/app/graphql/types/ci/runner_cloud_provider_enum.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fa4c8d841ab2f5382b5fdd859600a33183482d12
--- /dev/null
+++ b/ee/app/graphql/types/ci/runner_cloud_provider_enum.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Types
+  module Ci
+    class RunnerCloudProviderEnum < BaseEnum
+      graphql_name 'CiRunnerCloudProvider'
+      description 'Runner cloud provider.'
+
+      value 'GOOGLE_CLOUD', value: :google_cloud, description: 'Google Cloud.'
+    end
+  end
+end
diff --git a/ee/app/graphql/types/ci/runner_cloud_provisioning_machine_type_type.rb b/ee/app/graphql/types/ci/runner_cloud_provisioning_machine_type_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..990e574db8db58f65398ca723affd6757f11ed39
--- /dev/null
+++ b/ee/app/graphql/types/ci/runner_cloud_provisioning_machine_type_type.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Types
+  module Ci
+    # rubocop:disable Graphql/AuthorizeTypes -- This object is only available through one field, which is authorized. This type is mapped onto a simple hash, and therefore will not have a policy class for it.
+    class RunnerCloudProvisioningMachineTypeType < BaseObject
+      graphql_name 'CiRunnerCloudProvisioningMachineType'
+      description 'Machine type used for runner cloud provisioning.'
+
+      field :zone, GraphQL::Types::String,
+        null: true, description: 'Zone of the machine type.'
+
+      field :name, GraphQL::Types::String,
+        null: true, description: 'Name of the machine type.'
+
+      field :description, GraphQL::Types::String,
+        null: true, description: 'Description of the machine type.'
+    end
+    # rubocop:enable Graphql/AuthorizeTypes
+  end
+end
diff --git a/ee/app/graphql/types/ci/runner_cloud_provisioning_options_type.rb b/ee/app/graphql/types/ci/runner_cloud_provisioning_options_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dc7f52e3d7b94afa0d575fd330e54fae1ef77d2b
--- /dev/null
+++ b/ee/app/graphql/types/ci/runner_cloud_provisioning_options_type.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Types
+  module Ci
+    class RunnerCloudProvisioningOptionsType < BaseObject
+      graphql_name 'CiRunnerCloudProvisioningOptions'
+      description 'Options for runner cloud provisioning.'
+
+      include Gitlab::Graphql::Authorize::AuthorizeResource
+
+      authorize :read_runner_cloud_provisioning_options
+
+      field :regions, Types::Ci::RunnerCloudProvisioningRegionType.connection_type,
+        null: true,
+        resolver: ::Resolvers::Ci::RunnerCloudProvisioningRegionsResolver,
+        connection_extension: Gitlab::Graphql::Extensions::ForwardOnlyExternallyPaginatedArrayExtension
+
+      field :zones, Types::Ci::RunnerCloudProvisioningZoneType.connection_type,
+        null: true,
+        resolver: ::Resolvers::Ci::RunnerCloudProvisioningZonesResolver,
+        connection_extension: Gitlab::Graphql::Extensions::ForwardOnlyExternallyPaginatedArrayExtension
+
+      field :machine_types,
+        Types::Ci::RunnerCloudProvisioningMachineTypeType.connection_type,
+        null: true,
+        resolver: ::Resolvers::Ci::RunnerCloudProvisioningMachineTypesResolver,
+        connection_extension: Gitlab::Graphql::Extensions::ForwardOnlyExternallyPaginatedArrayExtension
+    end
+  end
+end
diff --git a/ee/app/graphql/types/ci/runner_cloud_provisioning_region_type.rb b/ee/app/graphql/types/ci/runner_cloud_provisioning_region_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..468eb08f9075dd3ce22004de1e583eed8269fab7
--- /dev/null
+++ b/ee/app/graphql/types/ci/runner_cloud_provisioning_region_type.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Types
+  module Ci
+    # rubocop:disable Graphql/AuthorizeTypes -- This object is only available through one field, which is authorized. This type is mapped onto a simple hash, and therefore will not have a policy class for it.
+    class RunnerCloudProvisioningRegionType < BaseObject
+      graphql_name 'CiRunnerCloudProvisioningRegion'
+      description 'Region used for runner cloud provisioning.'
+
+      field :name, GraphQL::Types::String,
+        null: true, description: 'Name of the region.'
+
+      field :description, GraphQL::Types::String,
+        null: true, description: 'Description of the region.'
+    end
+    # rubocop:enable Graphql/AuthorizeTypes
+  end
+end
diff --git a/ee/app/graphql/types/ci/runner_cloud_provisioning_zone_type.rb b/ee/app/graphql/types/ci/runner_cloud_provisioning_zone_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cde0927cb9b20be5c3bff9801597423d513d21d3
--- /dev/null
+++ b/ee/app/graphql/types/ci/runner_cloud_provisioning_zone_type.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Types
+  module Ci
+    # rubocop:disable Graphql/AuthorizeTypes -- This object is only available through one field, which is authorized. This type is mapped onto a simple hash, and therefore will not have a policy class for it.
+    class RunnerCloudProvisioningZoneType < BaseObject
+      graphql_name 'CiRunnerCloudProvisioningZone'
+      description 'Zone used for runner cloud provisioning.'
+
+      field :name, GraphQL::Types::String,
+        null: true, description: 'Name of the zone.'
+
+      field :description, GraphQL::Types::String,
+        null: true, description: 'Description of the zone.'
+    end
+    # rubocop:enable Graphql/AuthorizeTypes
+  end
+end
diff --git a/ee/spec/graphql/types/ci/runner_cloud_provisioning_machine_type_type_spec.rb b/ee/spec/graphql/types/ci/runner_cloud_provisioning_machine_type_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3de774cf83f46bb38fb882975566431c31e8341a
--- /dev/null
+++ b/ee/spec/graphql/types/ci/runner_cloud_provisioning_machine_type_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiRunnerCloudProvisioningMachineType'], feature_category: :fleet_visibility do
+  specify do
+    expect(described_class.description).to eq('Machine type used for runner cloud provisioning.')
+  end
+
+  it 'includes all expected fields' do
+    expected_fields = %w[zone name description]
+
+    expect(described_class).to include_graphql_fields(*expected_fields)
+  end
+end
diff --git a/ee/spec/graphql/types/ci/runner_cloud_provisioning_region_type_spec.rb b/ee/spec/graphql/types/ci/runner_cloud_provisioning_region_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..85d1bccffeb2f2729a1cf020178e55cdfc3ad02b
--- /dev/null
+++ b/ee/spec/graphql/types/ci/runner_cloud_provisioning_region_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiRunnerCloudProvisioningRegion'], feature_category: :fleet_visibility do
+  specify do
+    expect(described_class.description).to eq('Region used for runner cloud provisioning.')
+  end
+
+  it 'includes all expected fields' do
+    expected_fields = %w[name description]
+
+    expect(described_class).to include_graphql_fields(*expected_fields)
+  end
+end
diff --git a/ee/spec/graphql/types/ci/runner_cloud_provisioning_zone_type_spec.rb b/ee/spec/graphql/types/ci/runner_cloud_provisioning_zone_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c915f58aaa3452dd2a8eb4f8ae7575e48cf6cee3
--- /dev/null
+++ b/ee/spec/graphql/types/ci/runner_cloud_provisioning_zone_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiRunnerCloudProvisioningZone'], feature_category: :fleet_visibility do
+  specify do
+    expect(described_class.description).to eq('Zone used for runner cloud provisioning.')
+  end
+
+  it 'includes all expected fields' do
+    expected_fields = %w[name description]
+
+    expect(described_class).to include_graphql_fields(*expected_fields)
+  end
+end
diff --git a/ee/spec/graphql/types/project_type_spec.rb b/ee/spec/graphql/types/project_type_spec.rb
index 8b62b1dc3a8aca4ce5bc8e5a6ce2054433d5e58e..222933d5e19787e5e25fd87625cdb24c6feb93ab 100644
--- a/ee/spec/graphql/types/project_type_spec.rb
+++ b/ee/spec/graphql/types/project_type_spec.rb
@@ -29,7 +29,7 @@
       security_policy_project security_training_urls vulnerability_images only_allow_merge_if_all_status_checks_passed
       security_policy_project_linked_projects security_policy_project_linked_namespaces
       dependencies merge_requests_disable_committers_approval has_jira_vulnerability_issue_creation_enabled
-      ci_subscriptions_projects ci_subscribed_projects ai_agents duo_features_enabled
+      ci_subscriptions_projects ci_subscribed_projects ai_agents duo_features_enabled runner_cloud_provisioning_options
     ]
 
     expect(described_class).to include_graphql_fields(*expected_fields)
@@ -491,6 +491,12 @@
     it { is_expected.to have_graphql_resolver(Resolvers::Ai::Agents::FindAgentResolver) }
   end
 
+  describe 'runnerCloudProvisioningOptions', feature_category: :fleet_visibility do
+    subject { described_class.fields['runnerCloudProvisioningOptions'] }
+
+    it { is_expected.to have_graphql_type(::Types::Ci::RunnerCloudProvisioningOptionsType) }
+  end
+
   private
 
   def query_for_project(project)
diff --git a/ee/spec/requests/api/graphql/project/runner_cloud_provisioning_options_spec.rb b/ee/spec/requests/api/graphql/project/runner_cloud_provisioning_options_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1973920a007139f80f317556def2dc349671e80d
--- /dev/null
+++ b/ee/spec/requests/api/graphql/project/runner_cloud_provisioning_options_spec.rb
@@ -0,0 +1,254 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'runnerCloudProvisioningOptions', feature_category: :fleet_visibility do
+  include GraphqlHelpers
+
+  let_it_be_with_refind(:project) { create(:project) }
+  let_it_be_with_refind(:integration) { create(:google_cloud_platform_artifact_registry_integration, project: project) }
+  let_it_be(:maintainer) { create(:user).tap { |user| project.add_maintainer(user) } }
+
+  let(:client_klass) { GoogleCloudPlatform::Compute::Client }
+  let(:current_user) { maintainer }
+  let(:expected_compute_client_args) do
+    {
+      project: project,
+      user: current_user,
+      gcp_project_id: integration.artifact_registry_project_id,
+      gcp_wlif: integration.wlif
+    }
+  end
+
+  let(:current_page_token) { nil }
+  let(:expected_next_page_token) { nil }
+  let(:node_name) { :regions }
+  let(:item_type) { 'CiRunnerCloudProvisioningRegion' }
+  let(:base_item_query_args) { {} }
+  let(:item_query_args) { {} }
+  let(:query) do
+    graphql_query_for(
+      :project, { fullPath: project.full_path },
+      query_graphql_field(
+        :runner_cloud_provisioning_options, { provider: :GOOGLE_CLOUD },
+        query_nodes(node_name, args: base_item_query_args.merge(item_query_args), of: item_type,
+          include_pagination_info: true)
+      )
+    )
+  end
+
+  let(:options_response) do
+    request
+    graphql_data_at('project', 'runnerCloudProvisioningOptions')
+  end
+
+  subject(:request) do
+    post_graphql(query, current_user: current_user)
+  end
+
+  before do
+    stub_saas_features(google_artifact_registry: true)
+  end
+
+  shared_examples 'a query handling client errors' do
+    shared_examples 'returns error when client raises' do |error_klass, message|
+      it "returns error when client raises #{error_klass}" do
+        expect_next_instance_of(GoogleCloudPlatform::Compute::Client, expected_compute_client_args) do |client|
+          expect(client).to receive(client_method).and_raise(error_klass, message)
+        end
+
+        post_graphql(query, current_user: current_user)
+        expect_graphql_errors_to_include(message)
+      end
+    end
+
+    it_behaves_like 'returns error when client raises', GoogleCloudPlatform::ApiError, 'api error'
+    it_behaves_like 'returns error when client raises',
+      GoogleCloudPlatform::AuthenticationError, 'Unable to authenticate against Google Cloud'
+  end
+
+  shared_examples 'a query calling compute client' do
+    let(:page_size) { GoogleCloudPlatform::Compute::BaseService::MAX_RESULTS_LIMIT }
+    let(:expected_client_args) { {} }
+    let(:expected_pagination_client_args) { { max_results: page_size, page_token: current_page_token, order_by: nil } }
+    let(:actual_returned_nodes) { returned_nodes }
+
+    before do
+      allow_next_instance_of(client_klass, expected_compute_client_args) do |client|
+        allow(client).to receive(client_method)
+          .with(a_hash_including(**expected_pagination_client_args.merge(expected_client_args))) do
+            compute_type = client_method.to_s.camelize.singularize
+            google_cloud_object_list(compute_type, actual_returned_nodes, next_page_token: expected_next_page_token)
+          end
+      end
+
+      request
+    end
+
+    shared_examples 'a client returning paginated response' do
+      it 'returns paginated response with items from client' do
+        graphql_field_name = GraphqlHelpers.fieldnamerize(client_method)
+
+        expect(options_response[graphql_field_name]).to match({
+          'nodes' => expected_nodes.map { |node_props| a_graphql_entity_for(nil, **node_props) },
+          'pageInfo' => a_hash_including(
+            'hasPreviousPage' => !!current_page_token,
+            'hasNextPage' => !!expected_next_page_token,
+            'endCursor' => expected_next_page_token
+          )
+        })
+      end
+    end
+
+    it_behaves_like 'a working graphql query'
+    it_behaves_like 'a client returning paginated response'
+
+    context 'with arguments' do
+      let(:current_page_token) { 'prev_page_token' }
+      let(:page_size) { 10 }
+      let(:base_item_query_args) do
+        { after: current_page_token, first: page_size }
+      end
+
+      it_behaves_like 'a client returning paginated response'
+
+      context 'with pagination arguments requesting next page' do
+        let(:current_page_token) { 'next_page_token' }
+        let(:expected_next_page_token) { 'next_page_token2' }
+        let(:page_size) { 1 }
+        let(:expected_nodes) { returned_nodes[1..] }
+        let(:actual_returned_nodes) { returned_nodes[1..] }
+        let(:base_item_query_args) { { after: current_page_token, first: page_size } }
+
+        it_behaves_like 'a client returning paginated response'
+      end
+    end
+  end
+
+  describe 'regions' do
+    let(:item_type) { 'CiRunnerCloudProvisioningRegion' }
+    let(:client_method) { :regions }
+    let(:node_name) { :regions }
+    let(:regions) do
+      [
+        { name: 'us-east1', description: 'us-east1' },
+        { name: 'us-west1', description: 'us-west1' }
+      ]
+    end
+
+    let(:returned_nodes) { regions }
+    let(:expected_nodes) { returned_nodes }
+    let(:expected_client_args) { { filter: nil } }
+
+    it_behaves_like 'a query handling client errors'
+    it_behaves_like 'a query calling compute client'
+  end
+
+  describe 'zones' do
+    let(:item_type) { 'CiRunnerCloudProvisioningZone' }
+    let(:client_method) { :zones }
+    let(:node_name) { :zones }
+    let(:zones) do
+      [
+        { name: 'us-east1-a', description: 'us-east1-a' },
+        { name: 'us-west1-a', description: 'us-west1-a' }
+      ]
+    end
+
+    let(:returned_nodes) { zones }
+    let(:expected_nodes) { returned_nodes }
+    let(:expected_client_args) { { filter: nil } }
+
+    it_behaves_like 'a query handling client errors'
+    it_behaves_like 'a query calling compute client'
+
+    context 'with specified region' do
+      let(:region) { 'us-east1' }
+      let(:item_query_args) { { region: region } }
+      let(:returned_nodes) { zones.select { |z| z[:name].starts_with?(region) } }
+      let(:expected_next_page_token) { 'next_page_token' }
+
+      it_behaves_like 'a query calling compute client' do
+        let(:expected_client_args) { { filter: "name=#{region}-*" } }
+      end
+    end
+  end
+
+  describe 'machineTypes' do
+    let(:item_type) { 'CiRunnerCloudProvisioningMachineType' }
+    let(:client_method) { :machine_types }
+    let(:node_name) { :machine_types }
+    let(:machine_types) do
+      [
+        { zone: zone, name: 'e2-highcpu-8', description: 'Efficient Instance, 8 vCPUs, 8 GB RAM' },
+        { zone: zone, name: 'e2-highcpu-16', description: 'Efficient Instance, 16 vCPUs, 16 GB RAM' }
+      ]
+    end
+
+    let(:zone) { 'us-east1-a' }
+    let(:item_query_args) { { zone: zone } }
+    let(:returned_nodes) { machine_types }
+    let(:expected_nodes) { returned_nodes }
+    let(:expected_client_args) { { filter: "name=#{zone}-*" } }
+
+    it_behaves_like 'a query handling client errors'
+    it_behaves_like 'a query calling compute client'
+  end
+
+  context 'when user does not have required permissions' do
+    let(:current_user) { create(:user).tap { |user| project.add_developer(user) } }
+
+    it { is_expected.to be nil }
+  end
+
+  context 'when SaaS feature is not enabled' do
+    before do
+      stub_saas_features(google_artifact_registry: false)
+    end
+
+    it { is_expected.to be nil }
+  end
+
+  context 'when gcp_runner FF is disabled' do
+    before do
+      stub_feature_flags(gcp_runner: false)
+    end
+
+    it { is_expected.to be nil }
+  end
+
+  context 'when integration is not present' do
+    before do
+      integration.destroy!
+    end
+
+    it 'returns error' do
+      post_graphql(query, current_user: current_user)
+      expect_graphql_errors_to_include(/integration not set/)
+    end
+  end
+
+  context 'when integration is inactive' do
+    before do
+      integration.update_column(:active, false)
+    end
+
+    it 'returns error' do
+      post_graphql(query, current_user: current_user)
+      expect_graphql_errors_to_include(/integration not active/)
+    end
+  end
+
+  private
+
+  def google_cloud_object_list(compute_type, returned_nodes, next_page_token:)
+    item_type = "Google::Cloud::Compute::V1::#{compute_type}"
+
+    # rubocop:disable RSpec/VerifiedDoubles -- these generated objects don't actually expose the methods
+    double("#{item_type}List",
+      items: returned_nodes.map { |props| double(item_type, **props) },
+      next_page_token: next_page_token
+    )
+    # rubocop:enable RSpec/VerifiedDoubles
+  end
+end