diff --git a/app/controllers/groups/clusters_controller.rb b/app/controllers/groups/clusters_controller.rb
index 50c44b7a58b46a58ae587d193d4a8f7cbe366fff..b846fb21266e0e42a19d79ebf7733ce53500c466 100644
--- a/app/controllers/groups/clusters_controller.rb
+++ b/app/controllers/groups/clusters_controller.rb
@@ -3,8 +3,8 @@
 class Groups::ClustersController < Clusters::ClustersController
   include ControllerWithCrossProjectAccessCheck
 
-  prepend_before_action :check_group_clusters_feature_flag!
   prepend_before_action :group
+  prepend_before_action :check_group_clusters_feature_flag!
   requires_cross_project_access
 
   layout 'group'
@@ -20,6 +20,10 @@ class Groups::ClustersController < Clusters::ClustersController
   end
 
   def check_group_clusters_feature_flag!
-    render_404 unless Feature.enabled?(:group_clusters)
+    render_404 unless group_clusters_enabled?
+  end
+
+  def group_clusters_enabled?
+    group.group_clusters_enabled?
   end
 end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index e9b9b9b7721e05cc4fe40f668c8afac2ac397f8c..866fc555856c1574b5bf4982c27ea14f36fc79c3 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -140,7 +140,7 @@ module GroupsHelper
       can?(current_user, "read_group_#{resource}".to_sym, @group)
     end
 
-    if can?(current_user, :read_cluster, @group) && Feature.enabled?(:group_clusters)
+    if can?(current_user, :read_cluster, @group) && @group.group_clusters_enabled?
       links << :kubernetes
     end
 
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 13906c903b9f03c57a7536f2410ef5d712eab58b..c9bd1728dbdf7be1ccc32b4e59f70ed4913d6a88 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -4,6 +4,7 @@ module Clusters
   class Cluster < ActiveRecord::Base
     include Presentable
     include Gitlab::Utils::StrongMemoize
+    include FromUnion
 
     self.table_name = 'clusters'
 
@@ -86,6 +87,19 @@ module Clusters
 
     scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
 
+    scope :missing_kubernetes_namespace, -> (kubernetes_namespaces) do
+      subquery = kubernetes_namespaces.select('1').where('clusters_kubernetes_namespaces.cluster_id = clusters.id')
+
+      where('NOT EXISTS (?)', subquery)
+    end
+
+    def self.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: :asc)
+      hierarchy_groups = clusterable.ancestors_upto(hierarchy_order: hierarchy_order).eager_load(:clusters)
+      hierarchy_groups = hierarchy_groups.merge(current_scope) if current_scope
+
+      hierarchy_groups.flat_map(&:clusters)
+    end
+
     def status_name
       if provider
         provider.status_name
@@ -122,6 +136,16 @@ module Clusters
       !user?
     end
 
+    def all_projects
+      if project_type?
+        projects
+      elsif group_type?
+        first_group.all_projects
+      else
+        Project.none
+      end
+    end
+
     def first_project
       strong_memoize(:first_project) do
         projects.first
@@ -140,11 +164,17 @@ module Clusters
       platform_kubernetes.kubeclient if kubernetes?
     end
 
-    def find_or_initialize_kubernetes_namespace(cluster_project)
-      kubernetes_namespaces.find_or_initialize_by(
-        project: cluster_project.project,
-        cluster_project: cluster_project
-      )
+    def find_or_initialize_kubernetes_namespace_for_project(project)
+      if project_type?
+        kubernetes_namespaces.find_or_initialize_by(
+          project: project,
+          cluster_project: cluster_project
+        )
+      else
+        kubernetes_namespaces.find_or_initialize_by(
+          project: project
+        )
+      end
     end
 
     def allow_user_defined_namespace?
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index e57a3383544832dbb319c5408c2daac52a2f9bd4..0107af5f8ec61d5ef5ed591668e1fb2b61c9406a 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -13,6 +13,7 @@ module DeploymentPlatform
 
   def find_deployment_platform(environment)
     find_cluster_platform_kubernetes(environment: environment) ||
+      find_group_cluster_platform_kubernetes_with_feature_guard(environment: environment) ||
       find_kubernetes_service_integration ||
       build_cluster_and_deployment_platform
   end
@@ -23,6 +24,18 @@ module DeploymentPlatform
       .last&.platform_kubernetes
   end
 
+  def find_group_cluster_platform_kubernetes_with_feature_guard(environment: nil)
+    return unless group_clusters_enabled?
+
+    find_group_cluster_platform_kubernetes(environment: environment)
+  end
+
+  # EE would override this and utilize environment argument
+  def find_group_cluster_platform_kubernetes(environment: nil)
+    Clusters::Cluster.enabled.default_environment.ancestor_clusters_for_clusterable(self)
+      .first&.platform_kubernetes
+  end
+
   def find_kubernetes_service_integration
     services.deployment.reorder(nil).find_by(active: true)
   end
diff --git a/app/models/group.rb b/app/models/group.rb
index 02ddc8762afa9d346cfb0e5d971f0dca2c77d5bf..233747cc2c2ad5056298a3a1cd79665eebccfc0a 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -400,6 +400,10 @@ class Group < Namespace
     ensure_runners_token!
   end
 
+  def group_clusters_enabled?
+    Feature.enabled?(:group_clusters, root_ancestor, default_enabled: true)
+  end
+
   private
 
   def update_two_factor_requirement
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 11b03846f0bc90c0fb22a2414ce7aed7a3f2e659..8865c164b112b906115d7ad2a96c5fe2c1d97121 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -192,9 +192,9 @@ class Namespace < ActiveRecord::Base
 
   # returns all ancestors upto but excluding the given namespace
   # when no namespace is given, all ancestors upto the top are returned
-  def ancestors_upto(top = nil)
+  def ancestors_upto(top = nil, hierarchy_order: nil)
     Gitlab::GroupHierarchy.new(self.class.where(id: id))
-      .ancestors(upto: top)
+      .ancestors(upto: top, hierarchy_order: hierarchy_order)
   end
 
   def self_and_ancestors
@@ -243,7 +243,7 @@ class Namespace < ActiveRecord::Base
   end
 
   def root_ancestor
-    ancestors.reorder(nil).find_by(parent_id: nil)
+    self_and_ancestors.reorder(nil).find_by(parent_id: nil)
   end
 
   def subgroup?
diff --git a/app/models/project.rb b/app/models/project.rb
index 0ab3ea53675af0aa9d2398131a0cde4505444e94..587bada469e87f7934cfb0e8c38ebe3ea0a55153 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -238,6 +238,7 @@ class Project < ActiveRecord::Base
   has_one :cluster_project, class_name: 'Clusters::Project'
   has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
   has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress'
+  has_many :kubernetes_namespaces, class_name: 'Clusters::KubernetesNamespace'
 
   has_many :prometheus_metrics
 
@@ -300,6 +301,8 @@ class Project < ActiveRecord::Base
   delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team
   delegate :add_master, to: :team # @deprecated
   delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
+  delegate :group_clusters_enabled?, to: :group, allow_nil: true
+  delegate :root_ancestor, to: :namespace, allow_nil: true
 
   # Validations
   validates :creator, presence: true, on: :create
@@ -392,6 +395,12 @@ class Project < ActiveRecord::Base
     .where(project_ci_cd_settings: { group_runners_enabled: true })
   end
 
+  scope :missing_kubernetes_namespace, -> (kubernetes_namespaces) do
+    subquery = kubernetes_namespaces.select('1').where('clusters_kubernetes_namespaces.project_id = projects.id')
+
+    where('NOT EXISTS (?)', subquery)
+  end
+
   enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
 
   chronic_duration_attr :build_timeout_human_readable, :build_timeout,
@@ -556,9 +565,9 @@ class Project < ActiveRecord::Base
 
   # returns all ancestor-groups upto but excluding the given namespace
   # when no namespace is given, all ancestors upto the top are returned
-  def ancestors_upto(top = nil)
+  def ancestors_upto(top = nil, hierarchy_order: nil)
     Gitlab::GroupHierarchy.new(Group.where(id: namespace_id))
-      .base_and_ancestors(upto: top)
+      .base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
   end
 
   def lfs_enabled?
@@ -1071,6 +1080,12 @@ class Project < ActiveRecord::Base
     path
   end
 
+  def all_clusters
+    group_clusters = Clusters::Cluster.joins(:groups).where(cluster_groups: { group_id: ancestors_upto } )
+
+    Clusters::Cluster.from_union([clusters, group_clusters])
+  end
+
   def items_for(entity)
     case entity
     when 'issue' then
diff --git a/app/services/clusters/refresh_service.rb b/app/services/clusters/refresh_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7c82b98a33f1415741bee2395766cf45cbab760d
--- /dev/null
+++ b/app/services/clusters/refresh_service.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Clusters
+  class RefreshService
+    def self.create_or_update_namespaces_for_cluster(cluster)
+      projects_with_missing_kubernetes_namespaces_for_cluster(cluster).each do |project|
+        create_or_update_namespace(cluster, project)
+      end
+    end
+
+    def self.create_or_update_namespaces_for_project(project)
+      clusters_with_missing_kubernetes_namespaces_for_project(project).each do |cluster|
+        create_or_update_namespace(cluster, project)
+      end
+    end
+
+    def self.projects_with_missing_kubernetes_namespaces_for_cluster(cluster)
+      cluster.all_projects.missing_kubernetes_namespace(cluster.kubernetes_namespaces)
+    end
+
+    private_class_method :projects_with_missing_kubernetes_namespaces_for_cluster
+
+    def self.clusters_with_missing_kubernetes_namespaces_for_project(project)
+      project.all_clusters.missing_kubernetes_namespace(project.kubernetes_namespaces)
+    end
+
+    private_class_method :clusters_with_missing_kubernetes_namespaces_for_project
+
+    def self.create_or_update_namespace(cluster, project)
+      kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace_for_project(project)
+
+      ::Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
+        cluster: cluster,
+        kubernetes_namespace: kubernetes_namespace
+      ).execute
+    end
+
+    private_class_method :create_or_update_namespace
+  end
+end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 9e77a3237e34219c027d5a486229796f397cc4dc..d03137b63b28b4276353de761b19087425699e70 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -96,6 +96,8 @@ module Projects
       current_user.invalidate_personal_projects_count
 
       create_readme if @initialize_with_readme
+
+      configure_group_clusters_for_project
     end
 
     # Refresh the current user's authorizations inline (so they can access the
@@ -121,6 +123,10 @@ module Projects
       Files::CreateService.new(@project, current_user, commit_attrs).execute
     end
 
+    def configure_group_clusters_for_project
+      ClusterProjectConfigureWorker.perform_async(@project.id)
+    end
+
     def skip_wiki?
       !@project.feature_available?(:wiki, current_user) || @skip_wiki
     end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 9d40ab166ff5844755097694f34fe73187153549..9db3fd9cf174135813aa4e4d682b9cf77f8e1d03 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -54,6 +54,7 @@ module Projects
       end
 
       attempt_transfer_transaction
+      configure_group_clusters_for_project
     end
     # rubocop: enable CodeReuse/ActiveRecord
 
@@ -162,5 +163,9 @@ module Projects
         @new_namespace.full_path
       )
     end
+
+    def configure_group_clusters_for_project
+      ClusterProjectConfigureWorker.perform_async(project.id)
+    end
   end
 end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index c0b410472eba5505d4067162dc7f9cd88e1f8baf..e51da79c6b50de29d5db3428533b27da62565d9f 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -29,6 +29,7 @@
 - gcp_cluster:wait_for_cluster_creation
 - gcp_cluster:cluster_wait_for_ingress_ip_address
 - gcp_cluster:cluster_platform_configure
+- gcp_cluster:cluster_project_configure
 
 - github_import_advance_stage
 - github_importer:github_import_import_diff_note
diff --git a/app/workers/cluster_platform_configure_worker.rb b/app/workers/cluster_platform_configure_worker.rb
index 8f3689f01666e6edd7d4f0545d1c4cd508019ffe..aa7570caa7902f01ed7908fb204fee0d100dad99 100644
--- a/app/workers/cluster_platform_configure_worker.rb
+++ b/app/workers/cluster_platform_configure_worker.rb
@@ -6,17 +6,7 @@ class ClusterPlatformConfigureWorker
 
   def perform(cluster_id)
     Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
-      next unless cluster.cluster_project
-
-      kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project)
-
-      Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
-        cluster: cluster,
-        kubernetes_namespace: kubernetes_namespace
-      ).execute
+      Clusters::RefreshService.create_or_update_namespaces_for_cluster(cluster)
     end
-
-  rescue ::Kubeclient::HttpError => err
-    Rails.logger.error "Failed to create/update Kubernetes namespace for cluster_id: #{cluster_id} with error: #{err.message}"
   end
 end
diff --git a/app/workers/cluster_project_configure_worker.rb b/app/workers/cluster_project_configure_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..497e57c0d0b646b9c92b8ec45ae0c48fdd8029de
--- /dev/null
+++ b/app/workers/cluster_project_configure_worker.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class ClusterProjectConfigureWorker
+  include ApplicationWorker
+  include ClusterQueue
+
+  def perform(project_id)
+    project = Project.find(project_id)
+
+    ::Clusters::RefreshService.create_or_update_namespaces_for_project(project)
+  end
+end
diff --git a/changelogs/unreleased/34758-deployment-cluster.yml b/changelogs/unreleased/34758-deployment-cluster.yml
new file mode 100644
index 0000000000000000000000000000000000000000..063740983439d942cb82d3d247bf2d3ec85a55a1
--- /dev/null
+++ b/changelogs/unreleased/34758-deployment-cluster.yml
@@ -0,0 +1,5 @@
+---
+title: Use group clusters when deploying (DeploymentPlatform)
+merge_request: 22308
+author:
+type: changed
diff --git a/lib/gitlab/group_hierarchy.rb b/lib/gitlab/group_hierarchy.rb
index c940ea7305e1f5ec0098e7d77625dd37b7a950c0..97cbdc6cb39f968e1731b6b5796caf68a56cec04 100644
--- a/lib/gitlab/group_hierarchy.rb
+++ b/lib/gitlab/group_hierarchy.rb
@@ -34,8 +34,8 @@ module Gitlab
     # reached. So all ancestors *lower* than the specified ancestor will be
     # included.
     # rubocop: disable CodeReuse/ActiveRecord
-    def ancestors(upto: nil)
-      base_and_ancestors(upto: upto).where.not(id: ancestors_base.select(:id))
+    def ancestors(upto: nil, hierarchy_order: nil)
+      base_and_ancestors(upto: upto, hierarchy_order: hierarchy_order).where.not(id: ancestors_base.select(:id))
     end
     # rubocop: enable CodeReuse/ActiveRecord
 
@@ -45,11 +45,22 @@ module Gitlab
     # Passing an `upto` will stop the recursion once the specified parent_id is
     # reached. So all ancestors *lower* than the specified acestor will be
     # included.
-    def base_and_ancestors(upto: nil)
+    #
+    # Passing a `hierarchy_order` with either `:asc` or `:desc` will cause the
+    # recursive query order from most nested group to root or from the root
+    # ancestor to most nested group respectively. This uses a `depth` column
+    # where `1` is defined as the depth for the base and increment as we go up
+    # each parent.
+    # rubocop: disable CodeReuse/ActiveRecord
+    def base_and_ancestors(upto: nil, hierarchy_order: nil)
       return ancestors_base unless Group.supports_nested_groups?
 
-      read_only(base_and_ancestors_cte(upto).apply_to(model.all))
+      recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(model.all)
+      recursive_query = recursive_query.order(depth: hierarchy_order) if hierarchy_order
+
+      read_only(recursive_query)
     end
+    # rubocop: enable CodeReuse/ActiveRecord
 
     # Returns a relation that includes the descendants_base set of groups
     # and all their descendants (recursively).
@@ -107,16 +118,22 @@ module Gitlab
     private
 
     # rubocop: disable CodeReuse/ActiveRecord
-    def base_and_ancestors_cte(stop_id = nil)
+    def base_and_ancestors_cte(stop_id = nil, hierarchy_order = nil)
       cte = SQL::RecursiveCTE.new(:base_and_ancestors)
+      depth_column = :depth
+
+      base_query = ancestors_base.except(:order)
+      base_query = base_query.select("1 as #{depth_column}", groups_table[Arel.star]) if hierarchy_order
 
-      cte << ancestors_base.except(:order)
+      cte << base_query
 
       # Recursively get all the ancestors of the base set.
       parent_query = model
         .from([groups_table, cte.table])
         .where(groups_table[:id].eq(cte.table[:parent_id]))
         .except(:order)
+
+      parent_query = parent_query.select(cte.table[depth_column] + 1, groups_table[Arel.star]) if hierarchy_order
       parent_query = parent_query.where(cte.table[:parent_id].not_eq(stop_id)) if stop_id
 
       cte << parent_query
diff --git a/spec/features/ide_spec.rb b/spec/features/ide_spec.rb
index 65989c36c1e43107588f8e96e171f3f351ff2c5f..6eb59ef72c255f52d16dc599212b2ac0ec0481b1 100644
--- a/spec/features/ide_spec.rb
+++ b/spec/features/ide_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
 describe 'IDE', :js do
-  describe 'sub-groups' do
+  describe 'sub-groups', :nested_groups do
     let(:user) { create(:user) }
     let(:group) { create(:group) }
     let(:subgroup) { create(:group, parent: group) }
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 0add129dde23e2c2267b75ca07ff54509b55ce4b..b56bb272b46cb01e8f370453d85d9ec28562e3ec 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -277,7 +277,7 @@ describe 'Project' do
       end
     end
 
-    context 'for subgroups', :js do
+    context 'for subgroups', :js, :nested_groups do
       let(:group) { create(:group) }
       let(:subgroup) { create(:group, parent: group) }
       let(:project) { create(:project, :repository, group: subgroup) }
diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb
index 30686634af4cb4a1d541f78199d4e24b728203ea..f3de7adcec77d9907c2572006b3b4bcaa6f3c835 100644
--- a/spec/lib/gitlab/group_hierarchy_spec.rb
+++ b/spec/lib/gitlab/group_hierarchy_spec.rb
@@ -34,6 +34,28 @@ describe Gitlab::GroupHierarchy, :postgresql do
       expect { relation.update_all(share_with_group_lock: false) }
         .to raise_error(ActiveRecord::ReadOnlyRecord)
     end
+
+    describe 'hierarchy_order option' do
+      let(:relation) do
+        described_class.new(Group.where(id: child2.id)).base_and_ancestors(hierarchy_order: hierarchy_order)
+      end
+
+      context ':asc' do
+        let(:hierarchy_order) { :asc }
+
+        it 'orders by child to parent' do
+          expect(relation).to eq([child2, child1, parent])
+        end
+      end
+
+      context ':desc' do
+        let(:hierarchy_order) { :desc }
+
+        it 'orders by parent to child' do
+          expect(relation).to eq([parent, child1, child2])
+        end
+      end
+    end
   end
 
   describe '#base_and_descendants' do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index d9a4f1960ad1515f0999a4f4446742a17ae1cab6..7df129da95adaa3982236a8e9f9be9b3322789e3 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -307,6 +307,7 @@ project:
 - import_export_upload
 - repository_languages
 - pool_repository
+- kubernetes_namespaces
 award_emoji:
 - awardable
 - user
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 7dcf97276b20e45e9fbc7d099e3f4426e8854bbc..840f74c98907f3b89bca238227af17ad2964a582 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -92,6 +92,26 @@ describe Clusters::Cluster do
     it { is_expected.to contain_exactly(cluster) }
   end
 
+  describe '.missing_kubernetes_namespace' do
+    let!(:cluster) { create(:cluster, :provided_by_gcp, :project) }
+    let(:project) { cluster.project }
+    let(:kubernetes_namespaces) { project.kubernetes_namespaces }
+
+    subject do
+      described_class.joins(:projects).where(projects: { id: project.id }).missing_kubernetes_namespace(kubernetes_namespaces)
+    end
+
+    it { is_expected.to contain_exactly(cluster) }
+
+    context 'kubernetes namespace exists' do
+      before do
+        create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+      end
+
+      it { is_expected.to be_empty }
+    end
+  end
+
   describe 'validation' do
     subject { cluster.valid? }
 
@@ -233,6 +253,81 @@ describe Clusters::Cluster do
     end
   end
 
+  describe '.ancestor_clusters_for_clusterable' do
+    let(:group_cluster) { create(:cluster, :provided_by_gcp, :group) }
+    let(:group) { group_cluster.group }
+    let(:hierarchy_order) { :desc }
+    let(:clusterable) { project }
+
+    subject do
+      described_class.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: hierarchy_order)
+    end
+
+    context 'when project does not belong to this group' do
+      let(:project) { create(:project, group: create(:group)) }
+
+      it 'returns nothing' do
+        is_expected.to be_empty
+      end
+    end
+
+    context 'when group has a configured kubernetes cluster' do
+      let(:project) { create(:project, group: group) }
+
+      it 'returns the group cluster' do
+        is_expected.to eq([group_cluster])
+      end
+    end
+
+    context 'when sub-group has configured kubernetes cluster', :nested_groups do
+      let(:sub_group_cluster) { create(:cluster, :provided_by_gcp, :group) }
+      let(:sub_group) { sub_group_cluster.group }
+      let(:project) { create(:project, group: sub_group) }
+
+      before do
+        sub_group.update!(parent: group)
+      end
+
+      it 'returns clusters in order, descending the hierachy' do
+        is_expected.to eq([group_cluster, sub_group_cluster])
+      end
+
+      it 'avoids N+1 queries' do
+        another_project = create(:project)
+        control_count = ActiveRecord::QueryRecorder.new do
+          described_class.ancestor_clusters_for_clusterable(another_project, hierarchy_order: hierarchy_order)
+        end.count
+
+        cluster2 = create(:cluster, :provided_by_gcp, :group)
+        child2 = cluster2.group
+        child2.update!(parent: sub_group)
+        project = create(:project, group: child2)
+
+        expect do
+          described_class.ancestor_clusters_for_clusterable(project, hierarchy_order: hierarchy_order)
+        end.not_to exceed_query_limit(control_count)
+      end
+
+      context 'for a group' do
+        let(:clusterable) { sub_group }
+
+        it 'returns clusters in order for a group' do
+          is_expected.to eq([group_cluster])
+        end
+      end
+    end
+
+    context 'scope chaining' do
+      let(:project) { create(:project, group: group) }
+
+      subject { described_class.none.ancestor_clusters_for_clusterable(project) }
+
+      it 'returns nothing' do
+        is_expected.to be_empty
+      end
+    end
+  end
+
   describe '#provider' do
     subject { cluster.provider }
 
@@ -265,6 +360,31 @@ describe Clusters::Cluster do
     end
   end
 
+  describe '#all_projects' do
+    let(:project) { create(:project) }
+    let(:cluster) { create(:cluster, projects: [project]) }
+
+    subject { cluster.all_projects }
+
+    context 'project cluster' do
+      it 'returns project' do
+        is_expected.to eq([project])
+      end
+    end
+
+    context 'group cluster' do
+      let(:cluster) { create(:cluster, :group) }
+      let(:group) { cluster.group }
+      let(:project) { create(:project, group: group) }
+      let(:subgroup) { create(:group, parent: group) }
+      let(:subproject) { create(:project, group: subgroup) }
+
+      it 'returns all projects for group' do
+        is_expected.to contain_exactly(project, subproject)
+      end
+    end
+  end
+
   describe '#first_project' do
     subject { cluster.first_project }
 
diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb
index 7bb89fe41dc1be5bd25b9fddcb804fad2b1434fb..19ab4382b53288037238fcc08301c91cf31d92da 100644
--- a/spec/models/concerns/deployment_platform_spec.rb
+++ b/spec/models/concerns/deployment_platform_spec.rb
@@ -43,13 +43,86 @@ describe DeploymentPlatform do
       it { is_expected.to be_nil }
     end
 
-    context 'when user configured kubernetes from CI/CD > Clusters' do
+    context 'when project has configured kubernetes from CI/CD > Clusters' do
       let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
       let(:platform_kubernetes) { cluster.platform_kubernetes }
 
       it 'returns the Kubernetes platform' do
         expect(subject).to eq(platform_kubernetes)
       end
+
+      context 'with a group level kubernetes cluster' do
+        let(:group_cluster) { create(:cluster, :provided_by_gcp, :group) }
+
+        before do
+          project.update!(group: group_cluster.group)
+        end
+
+        it 'returns the Kubernetes platform from the project cluster' do
+          expect(subject).to eq(platform_kubernetes)
+        end
+      end
+    end
+
+    context 'when group has configured kubernetes cluster' do
+      let!(:group_cluster) { create(:cluster, :provided_by_gcp, :group) }
+      let(:group) { group_cluster.group }
+
+      before do
+        project.update!(group: group)
+      end
+
+      it 'returns the Kubernetes platform' do
+        is_expected.to eq(group_cluster.platform_kubernetes)
+      end
+
+      context 'when child group has configured kubernetes cluster', :nested_groups do
+        let!(:child_group1_cluster) { create(:cluster, :provided_by_gcp, :group) }
+        let(:child_group1) { child_group1_cluster.group }
+
+        before do
+          project.update!(group: child_group1)
+          child_group1.update!(parent: group)
+        end
+
+        it 'returns the Kubernetes platform for the child group' do
+          is_expected.to eq(child_group1_cluster.platform_kubernetes)
+        end
+
+        context 'deeply nested group' do
+          let!(:child_group2_cluster) { create(:cluster, :provided_by_gcp, :group) }
+          let(:child_group2) { child_group2_cluster.group }
+
+          before do
+            child_group2.update!(parent: child_group1)
+            project.update!(group: child_group2)
+          end
+
+          it 'returns most nested group cluster Kubernetes platform' do
+            is_expected.to eq(child_group2_cluster.platform_kubernetes)
+          end
+
+          context 'cluster in the middle of hierarchy is disabled' do
+            before do
+              child_group2_cluster.update!(enabled: false)
+            end
+
+            it 'returns closest enabled Kubenetes platform' do
+              is_expected.to eq(child_group1_cluster.platform_kubernetes)
+            end
+          end
+        end
+      end
+
+      context 'feature flag disabled' do
+        before do
+          stub_feature_flags(group_clusters: false)
+        end
+
+        it 'returns nil' do
+          is_expected.to be_nil
+        end
+      end
     end
 
     context 'when user configured kubernetes integration from project services' do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index ada00f039286334ce572268fddd8781657616249..0c3a49cd0f2d6cf31405d3afca55ab40e62ec591 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -745,4 +745,33 @@ describe Group do
       let(:uploader_class) { AttachmentUploader }
     end
   end
+
+  describe '#group_clusters_enabled?' do
+    before do
+      # Override global stub in spec/spec_helper.rb
+      expect(Feature).to receive(:enabled?).and_call_original
+    end
+
+    subject { group.group_clusters_enabled? }
+
+    it { is_expected.to be_truthy }
+
+    context 'explicitly disabled for root ancestor' do
+      before do
+        feature = Feature.get(:group_clusters)
+        feature.disable(group.root_ancestor)
+      end
+
+      it { is_expected.to be_falsey }
+    end
+
+    context 'explicitly disabled for root ancestor' do
+      before do
+        feature = Feature.get(:group_clusters)
+        feature.enable(group.root_ancestor)
+      end
+
+      it { is_expected.to be_truthy }
+    end
+  end
 end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 2db42fe802a6673881b1c2aa587ebe114415dd28..6ee19c0ddf42ce0eb84a5b2809d17f6b461e20b5 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -560,6 +560,7 @@ describe Namespace do
     let!(:project2) { create(:project_empty_repo, namespace: child) }
 
     it { expect(group.all_projects.to_a).to match_array([project2, project1]) }
+    it { expect(child.all_projects.to_a).to match_array([project2]) }
   end
 
   describe '#all_pipelines' do
@@ -720,6 +721,7 @@ describe Namespace do
       deep_nested_group = create(:group, parent: nested_group)
       very_deep_nested_group = create(:group, parent: deep_nested_group)
 
+      expect(root_group.root_ancestor).to eq(root_group)
       expect(nested_group.root_ancestor).to eq(root_group)
       expect(deep_nested_group.root_ancestor).to eq(root_group)
       expect(very_deep_nested_group.root_ancestor).to eq(root_group)
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index c0bc9eb2ef07d70dac3b967447ffbda7f25de3d7..50920d9d1fc8cb46158ada971173b596405ae5b7 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -87,6 +87,7 @@ describe Project do
     it { is_expected.to have_many(:pipeline_schedules) }
     it { is_expected.to have_many(:members_and_requesters) }
     it { is_expected.to have_many(:clusters) }
+    it { is_expected.to have_many(:kubernetes_namespaces) }
     it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
     it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') }
     it { is_expected.to have_many(:lfs_file_locks) }
@@ -177,6 +178,24 @@ describe Project do
     it { is_expected.to include_module(Sortable) }
   end
 
+  describe '.missing_kubernetes_namespace' do
+    let!(:project) { create(:project) }
+    let!(:cluster) { create(:cluster, :provided_by_user, :group) }
+    let(:kubernetes_namespaces) { project.kubernetes_namespaces }
+
+    subject { described_class.missing_kubernetes_namespace(kubernetes_namespaces) }
+
+    it { is_expected.to contain_exactly(project) }
+
+    context 'kubernetes namespace exists' do
+      before do
+        create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+      end
+
+      it { is_expected.to be_empty }
+    end
+  end
+
   describe 'validation' do
     let!(:project) { create(:project) }
 
@@ -416,6 +435,8 @@ describe Project do
 
     it { is_expected.to delegate_method(:members).to(:team).with_prefix(true) }
     it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).with_arguments(allow_nil: true) }
+    it { is_expected.to delegate_method(:group_clusters_enabled?).to(:group).with_arguments(allow_nil: true) }
+    it { is_expected.to delegate_method(:root_ancestor).to(:namespace).with_arguments(allow_nil: true) }
   end
 
   describe '#to_reference_with_postfix' do
@@ -2121,6 +2142,39 @@ describe Project do
     it 'includes ancestors upto but excluding the given ancestor' do
       expect(project.ancestors_upto(parent)).to contain_exactly(child2, child)
     end
+
+    describe 'with hierarchy_order' do
+      it 'returns ancestors ordered by descending hierarchy' do
+        expect(project.ancestors_upto(hierarchy_order: :desc)).to eq([parent, child, child2])
+      end
+
+      it 'can be used with upto option' do
+        expect(project.ancestors_upto(parent, hierarchy_order: :desc)).to eq([child, child2])
+      end
+    end
+  end
+
+  describe '#root_ancestor' do
+    let(:project) { create(:project) }
+
+    subject { project.root_ancestor }
+
+    it { is_expected.to eq(project.namespace) }
+
+    context 'in a group' do
+      let(:group) { create(:group) }
+      let(:project) { create(:project, group: group) }
+
+      it { is_expected.to eq(group) }
+    end
+
+    context 'in a nested group', :nested_groups do
+      let(:root) { create(:group) }
+      let(:child) { create(:group, parent: root) }
+      let(:project) { create(:project, group: child) }
+
+      it { is_expected.to eq(root) }
+    end
   end
 
   describe '#lfs_enabled?' do
@@ -4017,6 +4071,27 @@ describe Project do
     end
   end
 
+  describe '#all_clusters' do
+    let(:project) { create(:project) }
+    let(:cluster) { create(:cluster, cluster_type: :project_type, projects: [project]) }
+
+    subject { project.all_clusters }
+
+    it 'returns project level cluster' do
+      expect(subject).to eq([cluster])
+    end
+
+    context 'project belongs to a group' do
+      let(:group_cluster) { create(:cluster, :group) }
+      let(:group) { group_cluster.group }
+      let(:project) { create(:project, group: group) }
+
+      it 'returns clusters for groups of this project' do
+        expect(subject).to contain_exactly(cluster, group_cluster)
+      end
+    end
+  end
+
   def rugged_config
     rugged_repo(project.repository).config
   end
diff --git a/spec/services/clusters/refresh_service_spec.rb b/spec/services/clusters/refresh_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..58ab3c3cf73f6a45652098907830987409e75358
--- /dev/null
+++ b/spec/services/clusters/refresh_service_spec.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::RefreshService do
+  shared_examples 'creates a kubernetes namespace' do
+    let(:token) { 'aaaaaa' }
+    let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) }
+    let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) }
+
+    it 'creates a kubernetes namespace' do
+      expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator)
+      expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher)
+
+      expect { subject }.to change(project.kubernetes_namespaces, :count)
+
+      kubernetes_namespace = cluster.kubernetes_namespaces.first
+      expect(kubernetes_namespace).to be_present
+      expect(kubernetes_namespace.project).to eq(project)
+    end
+  end
+
+  shared_examples 'does not create a kubernetes namespace' do
+    it 'does not create a new kubernetes namespace' do
+      expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).not_to receive(:namespace_creator)
+      expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).not_to receive(:new)
+
+      expect { subject }.not_to change(Clusters::KubernetesNamespace, :count)
+    end
+  end
+
+  describe '.create_or_update_namespaces_for_cluster' do
+    let(:cluster) { create(:cluster, :provided_by_user, :project) }
+    let(:project) { cluster.project }
+
+    subject { described_class.create_or_update_namespaces_for_cluster(cluster) }
+
+    context 'cluster is project level' do
+      include_examples 'creates a kubernetes namespace'
+
+      context 'when project already has kubernetes namespace' do
+        before do
+          create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+        end
+
+        include_examples 'does not create a kubernetes namespace'
+      end
+    end
+
+    context 'cluster is group level' do
+      let(:cluster) { create(:cluster, :provided_by_user, :group) }
+      let(:group) { cluster.group }
+      let(:project) { create(:project, group: group) }
+
+      include_examples 'creates a kubernetes namespace'
+
+      context 'when project already has kubernetes namespace' do
+        before do
+          create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+        end
+
+        include_examples 'does not create a kubernetes namespace'
+      end
+    end
+  end
+
+  describe '.create_or_update_namespaces_for_project' do
+    let(:project) { create(:project) }
+
+    subject { described_class.create_or_update_namespaces_for_project(project) }
+
+    it 'creates no kubernetes namespaces' do
+      expect { subject }.not_to change(project.kubernetes_namespaces, :count)
+    end
+
+    context 'project has a project cluster' do
+      let!(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :project_type, projects: [project]) }
+
+      include_examples 'creates a kubernetes namespace'
+
+      context 'when project already has kubernetes namespace' do
+        before do
+          create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+        end
+
+        include_examples 'does not create a kubernetes namespace'
+      end
+    end
+
+    context 'project belongs to a group cluster' do
+      let!(:cluster) { create(:cluster, :provided_by_gcp, :group) }
+
+      let(:group) { cluster.group }
+      let(:project) { create(:project, group: group) }
+
+      include_examples 'creates a kubernetes namespace'
+
+      context 'when project already has kubernetes namespace' do
+        before do
+          create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+        end
+
+        include_examples 'does not create a kubernetes namespace'
+      end
+    end
+  end
+end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 08de27ca44a3f1ff20dbb73d6e7401da524af227..f71e2b4bc24dd474ae209c368680b244c145265e 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -261,6 +261,32 @@ describe Projects::CreateService, '#execute' do
     end
   end
 
+  context 'when group has kubernetes cluster' do
+    let(:group_cluster) { create(:cluster, :group, :provided_by_gcp) }
+    let(:group) { group_cluster.group }
+
+    let(:token) { 'aaaa' }
+    let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) }
+    let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) }
+
+    before do
+      group.add_owner(user)
+
+      expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator)
+      expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher)
+    end
+
+    it 'creates kubernetes namespace for the project' do
+      project = create_project(user, opts.merge!(namespace_id: group.id))
+
+      expect(project).to be_valid
+
+      kubernetes_namespace = group_cluster.kubernetes_namespaces.first
+      expect(kubernetes_namespace).to be_present
+      expect(kubernetes_namespace.project).to eq(project)
+    end
+  end
+
   context 'when there is an active service template' do
     before do
       create(:service, project: nil, template: true, active: true)
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 2e07d4f8013f8a94af0a902d1c80e3266d1233fc..132ad9a2646cdac77778461ffa5dc1f240bd2ec0 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -62,6 +62,32 @@ describe Projects::TransferService do
 
       expect(rugged_config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}"
     end
+
+    context 'new group has a kubernetes cluster' do
+      let(:group_cluster) { create(:cluster, :group, :provided_by_gcp) }
+      let(:group) { group_cluster.group }
+
+      let(:token) { 'aaaa' }
+      let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) }
+      let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) }
+
+      subject { transfer_project(project, user, group) }
+
+      before do
+        expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator)
+        expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher)
+      end
+
+      it 'creates kubernetes namespace for the project' do
+        subject
+
+        expect(project.kubernetes_namespaces.count).to eq(1)
+
+        kubernetes_namespace = group_cluster.kubernetes_namespaces.first
+        expect(kubernetes_namespace).to be_present
+        expect(kubernetes_namespace.project).to eq(project)
+      end
+    end
   end
 
   context 'when transfer fails' do
diff --git a/spec/workers/cluster_platform_configure_worker_spec.rb b/spec/workers/cluster_platform_configure_worker_spec.rb
index b51f6e07c6abf033759e0cb28ef235573a74e760..0eead0ab13dd8c8a783d3990d47cf5b6ace9b0d5 100644
--- a/spec/workers/cluster_platform_configure_worker_spec.rb
+++ b/spec/workers/cluster_platform_configure_worker_spec.rb
@@ -2,7 +2,43 @@
 
 require 'spec_helper'
 
-describe ClusterPlatformConfigureWorker, '#execute' do
+describe ClusterPlatformConfigureWorker, '#perform' do
+  let(:worker) { described_class.new }
+
+  context 'when group cluster' do
+    let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
+    let(:group) { cluster.group }
+
+    context 'when group has no projects' do
+      it 'does not create a namespace' do
+        expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:execute)
+
+        worker.perform(cluster.id)
+      end
+    end
+
+    context 'when group has a project' do
+      let!(:project) { create(:project, group: group) }
+
+      it 'creates a namespace for the project' do
+        expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).once
+
+        worker.perform(cluster.id)
+      end
+    end
+
+    context 'when group has project in a sub-group' do
+      let!(:subgroup) { create(:group, parent: group) }
+      let!(:project) { create(:project, group: subgroup) }
+
+      it 'creates a namespace for the project' do
+        expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).once
+
+        worker.perform(cluster.id)
+      end
+    end
+  end
+
   context 'when provider type is gcp' do
     let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
 
@@ -30,18 +66,4 @@ describe ClusterPlatformConfigureWorker, '#execute' do
       described_class.new.perform(123)
     end
   end
-
-  context 'when kubeclient raises error' do
-    let(:cluster) { create(:cluster, :project) }
-
-    it 'rescues and logs the error' do
-      allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).and_raise(::Kubeclient::HttpError.new(500, 'something baaaad happened', ''))
-
-      expect(Rails.logger)
-        .to receive(:error)
-        .with("Failed to create/update Kubernetes namespace for cluster_id: #{cluster.id} with error: something baaaad happened")
-
-      described_class.new.perform(cluster.id)
-    end
-  end
 end