Skip to main content

[Ep.20] Managing ACM Policies with OpenShift GitOps

In [Ep.18] I recommended ACM policies for the audited baseline of a cluster and Argo CD for everything that needs rendering. [Ep.19] showed how ACM feeds Argo CD with clusters and the different models that can be used in Argo CD. This part turns the integration around: Argo CD deploys ACM policies, and a policy bootstraps Argo CD on every new cluster.

The result is a setup where the policies live in Git, are reviewed in pull requests like any other change, and ACM does what it is good at: distribution, enforcement and compliance reporting.

The topic turned out to be too big for a single article, so I have split it into multiple parts and maybe I should have split into even more. This is the third:

  1. [Ep.18] OpenShift GitOps or Advanced Cluster Management? Choosing the right tool What each tool does, where they overlap and a decision guide.

  2. [Ep.19] ACM and OpenShift GitOps: push, pull and the Argo CD Agent: how ACM registers clusters in Argo CD, and how to set up each of the three delivery models.

  3. [Ep.20] Managing ACM Policies with OpenShift GitOps (this article): policies as code, the PolicyGenerator, hub templates and bootstrapping Argo CD with a policy.

Everything in this series was written against OpenShift GitOps 1.22 (Argo CD 3.5) and Advanced Cluster Management 2.17 (Open Cluster Management). Technology Preview and GA states move quickly in both products, so check the release notes of your versions before you commit to a design.
While I am using the term "Advanced Cluster Management" in this article, I am referring to the product formerly known as Open Cluster Management. Same counts for Argo CD and OpenShift GitOps.

Three integration patterns

There are three ways to combine policies and Argo CD. They are not exclusive, most mature setups combine the first two.

PatternHow it worksStrengthWeakness

A: Argo CD deploys policies

Policies live in Git, generated by the PolicyGenerator. The hub Argo CD syncs them to the hub, ACM propagates and evaluates them.

Policy as code with review, compliance dashboard in ACM

Needs the PolicyGenerator plugin in Argo CD

B: A policy bootstraps Argo CD

One enforced policy installs OpenShift GitOps and a root Application on every placed cluster. From there, Argo CD takes over.

New clusters configure themselves on import

The bootstrap needs proper planning and automation.

C: Policies configure directly

ConfigurationPolicies apply the cluster configuration themselves. No Argo CD on the spokes.

One tool, compliance built in

Weaker diff and ordering, large YAML in policies

Pattern C is fine for small, compliance-driven setups. For everything else I recommend A for governance and B for the fleet bootstrap. The rest of this article shows both.

Whatever option you choose, one rule from Ep.18 still applies: one owner per object. If Argo CD manages an object, a policy may only look at it in inform mode.

Preparing the hub Argo CD

You need an Argo CD instance on the hub that can sync to the hub itself. That instance needs three things before it can deploy policies:

  1. permissions for the policy API,

  2. the PolicyGenerator plugin,

  3. optionally, health checks for policies.

A second Argo CD instance for the hub

In Ep.19, the default instance openshift-gitops became the principal of the Argo CD Agent. Its application-controller is disabled, so it cannot sync anything to the hub itself, and the manifests of agent Applications are rendered on the managed clusters anyway. The policies, however, must be created on the hub.

This article therefore uses a second, small Argo CD instance on the hub that only takes care of the hub configuration: policy-gitops in the namespace of the same name. If your default instance still has its application-controller (push or pull model), you can use it instead. Replace policy-gitops with openshift-gitops in the examples below.

Second Argo CD instance for the hub configuration
apiVersion: v1
kind: Namespace
metadata:
  name: policy-gitops
---
apiVersion: argoproj.io/v1beta1
kind: ArgoCD
metadata:
  name: policy-gitops (1)
  namespace: policy-gitops
spec: {} (2)
---
apiVersion: v1
kind: Namespace
metadata:
  name: acm-policies
  labels:
    argocd.argoproj.io/managed-by: policy-gitops (3)
1The service account of the application-controller is named after the instance: policy-gitops-argocd-application-controller.
2The PolicyGenerator plugin is added in the next sections.
3policy-gitops is a namespace-scoped instance. The label allows it to manage the namespace acm-policies, where the policies live. The namespace must exist before the first sync, because the instance cannot create namespaces itself.

Permissions

The application-controller of the instance cannot create policies and Placements. Create a ClusterRole and bind it to the controller’s service account:

Not required if Argo CD runs as cluster-admin.
ClusterRole and ClusterRoleBinding for the hub Argo CD
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: openshift-gitops-policy-admin
rules:
  - apiGroups:
      - policy.open-cluster-management.io
    resources:
      - policies
      - configurationpolicies
      - certificatepolicies
      - operatorpolicies
      - policysets
      - placementbindings
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups:
      - cluster.open-cluster-management.io
    resources:
      - placements
      - placements/status
      - placementdecisions
      - placementdecisions/status
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: policy-gitops-policy-admin
subjects:
  - kind: ServiceAccount
    name: policy-gitops-argocd-application-controller
    namespace: policy-gitops
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: openshift-gitops-policy-admin

The ClusterRole in the ACM documentation also contains placementrules. I left them out on purpose: PlacementRules are deprecated.

The PolicyGenerator plugin

Creating the policy YAML by hand is tough and error-prone. Every manifest has to be wrapped in a ConfigurationPolicy, which is wrapped in a Policy, which needs a Placement and a PlacementBinding. The PolicyGenerator solves this. It is a Kustomize generator plugin that takes plain manifests and wraps them for you.

The plugin is not part of the OpenShift GitOps images. An init container copies it from an ACM image into the repo-server, and Kustomize must be allowed to run alpha plugins.

The setup is described in the ACM documentation: ACM 2.17: Accessing the Policy Generator from OpenShift GitOps.

ArgoCD resource on the hub with the PolicyGenerator plugin (excerpt)
apiVersion: argoproj.io/v1beta1
kind: ArgoCD
metadata:
  name: policy-gitops
  namespace: policy-gitops
spec:
  kustomizeBuildOptions: --enable-alpha-plugins (1)
  repo:
    env:
      - name: KUSTOMIZE_PLUGIN_HOME
        value: /etc/kustomize/plugin
    initContainers:
      - name: policy-generator-install
        image: registry.redhat.io/rhacm2/multicluster-operators-subscription-rhel9:v2.17 (2)
        command:
          - /bin/bash
        args:
          - -c
          - cp /policy-generator/PolicyGenerator-not-fips-compliant /policy-generator-tmp/PolicyGenerator (3)
        volumeMounts:
          - mountPath: /policy-generator-tmp
            name: policy-generator
    volumeMounts:
      - mountPath: /etc/kustomize/plugin/policy.open-cluster-management.io/v1/policygenerator
        name: policy-generator
    volumes:
      - name: policy-generator
        emptyDir: {}
1Allows Kustomize to execute the generator plugin.
2The tag must match the minor version of your ACM hub (oc get multiclusterhub -n open-cluster-management -o jsonpath='{.items[0].status.currentVersion}'). Update it after every hub upgrade, or let a policy do it (see Keeping the image tag in sync).
3Copies the plugin from the image to the plugin directory.

If your policy folders contain Helm charts that should be rendered before the policies are generated, add the environment variable POLICY_GEN_ENABLE_HELM with the value "true" to spec.repo.env.

Keeping the image tag in sync

A fixed tag has one weakness: after a hub upgrade, the PolicyGenerator in the repo-server is still the old one until somebody updates the tag. The ACM documentation provides an example of a policy that keeps the image tag in sync. After an upgrade of the hub, the policy changes the image of the init container automatically.

The policy runs on the hub only, so it needs a Placement for local-cluster. The hub carries the label local-cluster=true, and it is part of the cluster set global, which must be bound to the policy namespace:

Policy that keeps the PolicyGenerator image in sync with the hub
apiVersion: cluster.open-cluster-management.io/v1beta2
kind: ManagedClusterSetBinding
metadata:
  name: global
  namespace: acm-policies
spec:
  clusterSet: global (1)
---
apiVersion: policy.open-cluster-management.io/v1
kind: Policy
metadata:
  name: argocd-policy-generator
  namespace: acm-policies
  annotations:
    policy.open-cluster-management.io/standards: NIST SP 800-53
    policy.open-cluster-management.io/categories: CM Configuration Management
    policy.open-cluster-management.io/controls: CM-2 Baseline Configuration
spec:
  remediationAction: enforce (2)
  disabled: false
  policy-templates:
    - objectDefinition:
        apiVersion: policy.open-cluster-management.io/v1
        kind: ConfigurationPolicy
        metadata:
          name: argocd-policy-generator
        spec:
          severity: medium
          object-templates:
            - complianceType: musthave
              objectDefinition:
                apiVersion: argoproj.io/v1beta1
                kind: ArgoCD
                metadata:
                  name: policy-gitops
                  namespace: policy-gitops
                spec:
                  kustomizeBuildOptions: --enable-alpha-plugins
                  repo:
                    env:
                      - name: KUSTOMIZE_PLUGIN_HOME
                        value: /etc/kustomize/plugin
                    initContainers:
                      - name: policy-generator-install
                        image: '{{ (index (lookup "apps/v1" "Deployment" "open-cluster-management" "multicluster-operators-hub-subscription").spec.template.spec.containers 0).image }}' (3)
                        command:
                          - /bin/bash
                        args:
                          - -c
                          - cp /policy-generator/PolicyGenerator-not-fips-compliant /policy-generator-tmp/PolicyGenerator
                        volumeMounts:
                          - mountPath: /policy-generator-tmp
                            name: policy-generator
                    volumeMounts:
                      - mountPath: /etc/kustomize/plugin/policy.open-cluster-management.io/v1/policygenerator
                        name: policy-generator
                    volumes:
                      - name: policy-generator
                        emptyDir: {}
---
apiVersion: cluster.open-cluster-management.io/v1beta1
kind: Placement
metadata:
  name: hub-only
  namespace: acm-policies
spec:
  clusterSets:
    - global
  predicates:
    - requiredClusterSelector:
        labelSelector:
          matchLabels:
            local-cluster: "true" (4)
---
apiVersion: policy.open-cluster-management.io/v1
kind: PlacementBinding
metadata:
  name: argocd-policy-generator
  namespace: acm-policies
placementRef:
  apiGroup: cluster.open-cluster-management.io
  kind: Placement
  name: hub-only
subjects:
  - apiGroup: policy.open-cluster-management.io
    kind: Policy
    name: argocd-policy-generator
1The cluster set global contains every managed cluster, including the hub. The Placement below narrows it down to the hub. Creating a ManagedClusterSetBinding requires the permissions managedclustersetbindings and managedclustersets/bind, which are not part of the ClusterRole above. Create it once as cluster administrator, or extend the ClusterRole if Argo CD shall sync it.
2We enforce this policy.
3lookup reads the deployment of the ACM subscription controller, and the template takes the image of its first container. That is exactly the image the init container needs, in the version of the running hub.
4ACM sets this label on the hub’s own ManagedCluster.

Two things to keep in mind:

  • Write this policy as plain YAML, not as PolicyGenerator input. It installs the generator, so it cannot depend on it (hen-egg problem). Argo CD can sync it from Git without the plugin.

  • One owner per object. The policy now manages the plugin part of the ArgoCD resource policy-gitops. If that resource is also deployed by Argo CD (for example, from a Helm chart for OpenShift GitOps), remove the plugin settings from there, or the two will overwrite each other.

Policy health in Argo CD

By default, Argo CD considers a policy healthy as soon as it exists. That is not very useful: you want to see in Argo CD whether the policy is compliant. The policy collection of the Open Cluster Management project contains a policy that adds the matching health checks (resourceHealthChecks) to the ArgoCD resource: argocd-policy-healthchecks.yaml. The policy adds the health checks to every ArgoCD instance it finds on the cluster, so policy-gitops is covered as well. It is shipped with remediationAction: inform: set it to enforce, otherwise it only reports the missing health checks. After that, Argo CD shows a non-compliant policy as Degraded.


Pattern A: policies as code

Pattern A is the governance part of the integration: the policies live in Git, Argo CD on the hub brings them into the cluster, and ACM does the rest. You never create a policy in the ACM console or with oc apply again. Every change is a pull request, and the history of your compliance is stored in Git.

The example in this section builds two policies that show both sides of a policy:

  • service-mesh installs Red Hat OpenShift Service Mesh 3 on the selected clusters: the Operator and a control plane. It is enforced and the manifests are applied in a fixed order, because the control plane resources can only be created after the Operator has installed its CRDs.

  • no-kubeadmin checks that the kubeadmin secret does not exist anymore. It only informs: ACM reports every cluster where the secret is still present, but nothing is deleted automatically.

Both start as plain Kubernetes manifests in Git. The PolicyGenerator wraps them into Policy, Placement and PlacementBinding, so you do not have to write the policy structure yourself.

This requires the hub Argo CD instance policy-gitops with the PolicyGenerator plugin, as described in the previous section.

The following diagram shows the complete flow, from Git to the compliance status in ACM and Argo CD:

Policies as code: the hub Argo CD renders Policy, Placement and PlacementBinding from Git into the namespace acm-policies. ACM propagates a replicated policy per selected cluster, the policy framework on the managed cluster evaluates and enforces it, and the compliance status flows back to the ACM governance view and, with health checks, to Argo CD.GITHUBMANAGED CLUSTERS · bm, snoopenshift-gitopsacm-policiescluster namespaces · bm, snoGitinput/: plainmanifests, plus thegenerator configEvery policy changeis a pull request.hub Argo CDrenders the policies(PolicyGenerator pluginor Helm chart) and syncs1PoliciesPolicy, Placement,PlacementBindingrendered from Git2Replicated policiesacm-policies.<policy> in bm and snocreated by ACM for every selected cluster,IgnoreExtraneous set automatically3Argo CD healthHealthy if compliant,Degraded if not (with thepolicy health checks)6ACM governancecompliance per policy andcluster, with history5Policy frameworkpulls the replicated policy,evaluates it and enforces it(inform or enforce)4Objects on the spokecreated or checkedby the policysyncpulled by the spokestatusstatus.compliantcreated by yougenerated by ACMgenerated by Argo CDplatform componentDashed arrows: compliance status back to the hub
Policies as code: from plain manifests in Git to compliance on every cluster

What happens under the hood:

  1. The hub Argo CD renders the policies from Git and syncs them to the hub.

  2. The result is a set of Policy, Placement and PlacementBinding objects in the namespace acm-policies.

  3. ACM creates a replicated policy in the namespace of every selected cluster on the hub.

  4. The policy framework on each managed cluster pulls its replicated policy, evaluates it and, in enforce mode, fixes what is different.

  5. The compliance status flows back to the hub and shows up in the ACM governance view, per policy and cluster.

  6. With the policy health checks, Argo CD shows a non-compliant policy as Degraded.


Repository layout

acm-policies/
├── kustomization.yaml
├── policy-generator.yaml
└── input
    ├── service-mesh
    │   ├── 01-subscription.yaml       # OpenShift Service Mesh 3 Operator
    │   ├── 02-namespace-istio-system.yaml
    │   ├── 03-namespace-istio-cni.yaml
    │   ├── 04-istiocni.yaml           # CNI plugin, required on OpenShift
    │   └── 05-istio.yaml              # the control plane
    └── no-kubeadmin
        └── kubeadmin-secret.yaml

The files in input/ are plain Kubernetes manifests, exactly as you would apply them with oc apply. The Operator is installed in the namespace openshift-operators, which already has an OperatorGroup for all namespaces, so neither a namespace nor an OperatorGroup is needed for it:

input/service-mesh/01-subscription.yaml
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
  name: servicemeshoperator3
  namespace: openshift-operators
spec:
  channel: stable-3.4 (1)
  installPlanApproval: Automatic
  name: servicemeshoperator3
  source: redhat-operators
  sourceNamespace: openshift-marketplace
1Pins the minor version. Use stable to always get the latest version.

The control plane consists of two cluster-scoped resources, IstioCNI and Istio, each pointing to its own namespace (istio-cni and istio-system):

input/service-mesh/04-istiocni.yaml
apiVersion: sailoperator.io/v1
kind: IstioCNI
metadata:
  name: default
spec:
  namespace: istio-cni
input/service-mesh/05-istio.yaml
apiVersion: sailoperator.io/v1
kind: Istio
metadata:
  name: default
spec:
  namespace: istio-system

And the kubeadmin secret that must not exist:

if your original kubeadmin secret is still present, you can ignore this yaml. Honestly, I am not even sure if this would work, once you have removed the secret.
input/no-kubeadmin/kubeadmin-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: kubeadmin
  namespace: kube-system

The kustomization.yaml only references the generator:

kustomization.yaml
generators:
  - policy-generator.yaml

Render the folder locally before you push. If it does not render on your machine, it will not render in Argo CD either. With the PolicyGenerator binary in your Kustomize plugin path:

kustomize build --enable-alpha-plugins acm-policies/

The output contains a Policy, a Placement and a PlacementBinding per policy.


The PolicyGenerator configuration

The file policy-generator.yaml is the heart of the setup. It does not contain any policy itself, it only describes which policies the generator shall create from the manifests in input/. It has two parts:

  • policyDefaults apply to every policy: the namespace on the hub where the policies are created, the default remediation action and severity, and the Placement that decides which clusters receive them.

  • policies lists the policies, each with its name, the manifests it contains and the settings that differ from the defaults.

For every entry in policies, the generator creates a Policy, a Placement and a PlacementBinding. The manifests are wrapped into a ConfigurationPolicy inside the Policy: by default all manifests of a policy into one, with consolidateManifests: false one per manifest. Our two policies use this as follows: service-mesh overrides the defaults to enforce and to apply its manifests in order, while no-kubeadmin keeps the defaults and only changes the compliance type.

policy-generator.yaml
apiVersion: policy.open-cluster-management.io/v1
kind: PolicyGenerator
metadata:
  name: cluster-baseline
policyDefaults:
  namespace: acm-policies
  remediationAction: inform (1)
  severity: medium
  placement:
    labelSelector:
      matchExpressions:
        - key: name (5)
          operator: In
          values:
            - sno
            - bm
policies:
  - name: service-mesh
    remediationAction: enforce (2)
    orderManifests: true (3)
    consolidateManifests: false
    manifests:
      - path: input/service-mesh/01-subscription.yaml
      - path: input/service-mesh/02-namespace-istio-system.yaml
      - path: input/service-mesh/03-namespace-istio-cni.yaml
      - path: input/service-mesh/04-istiocni.yaml
      - path: input/service-mesh/05-istio.yaml
  - name: no-kubeadmin
    manifests:
      - path: input/no-kubeadmin/
        complianceType: mustnothave (4)
1Default for all policies: report only.
2This policy is enforced.
3Creates one ConfigurationPolicy per manifest and makes each one depend on the previous one: first the Subscription, then the namespaces, then IstioCNI and Istio. Note that the Subscription policy is compliant as soon as the Subscription exists, not when the Operator is installed. Until the CRDs are there, the IstioCNI policy reports an error and is evaluated again. If you want to wait for the installed Operator, use an OperatorPolicy instead of the Subscription, as shown in Pattern B.
4The kubeadmin secret must not exist. In inform mode, ACM reports every cluster where it still exists. Remove it with care, only after an identity provider is configured.
5Selects the clusters sno and bm. ACM sets the label name on every ManagedCluster. For a growing fleet, a label such as environment=production or the cluster set label cluster.open-cluster-management.io/clusterset scales better than a list of names.

Bind the cluster sets to the policy namespace

A Placement only sees the clusters of cluster sets that are bound to its own namespace. The bindings from Ep.19 live in openshift-gitops, so the Placements in acm-policies would find no cluster at all. ACM then shows the warning "ClusterSets failed to load. Verify that there is at least one ClusterSet bound to your selected namespace."

Bind the cluster set production to acm-policies, and global as well if you use the policy that keeps the PolicyGenerator image in sync (its Placement selects the hub from global):

ManagedClusterSetBindings in the policy namespace
apiVersion: cluster.open-cluster-management.io/v1beta2
kind: ManagedClusterSetBinding
metadata:
  name: production
  namespace: acm-policies
spec:
  clusterSet: production
---
apiVersion: cluster.open-cluster-management.io/v1beta2
kind: ManagedClusterSetBinding
metadata:
  name: global
  namespace: acm-policies
spec:
  clusterSet: global

The generated Placements select sno and bm by name, from the cluster sets bound to acm-policies. Check the result:

oc get placementdecisions -n acm-policies \
  -o custom-columns=NAME:.metadata.name,CLUSTERS:.status.decisions[*].clusterName

The Argo CD Application

On the hub, one Application syncs the folder:

Application for the policies
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: acm-policies
  namespace: policy-gitops (1)
spec:
  project: default
  source:
    repoURL: https://github.com/tjungbauer/acm-policies.git
    targetRevision: main
    path: .
  destination:
    server: https://kubernetes.default.svc
    namespace: acm-policies
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
1The Application belongs to the instance policy-gitops. No CreateNamespace=true: the namespace acm-policies already exists with the managed-by label.

Replicated policies

When ACM propagates a policy, it creates a copy in the namespace of every managed cluster on the hub, the replicated policy. Argo CD could treat these copies as extraneous resources of the Application and show it as OutOfSync or even try to delete them. ACM prevents that: it automatically sets the annotation argocd.argoproj.io/compare-options: IgnoreExtraneous on every replicated policy.


Pattern B: a policy bootstraps Argo CD

The bootstrap question from Ep.18: who installs Argo CD on a new cluster? With ACM, the answer is a policy. As soon as a cluster is imported and matches the Placement of the policy, the policy installs OpenShift GitOps and creates a root Application that points to the cluster’s folder in Git. From there, Argo CD configures the rest.

Bootstrap policy: the Placement selects the ManagedClusters sno and bm by their name label. The enforced policy gitops-bootstrap applies five templates in order: namespace, OperatorPolicy for OpenShift GitOps, ClusterRoleBinding, repository secret and root Application. The local Argo CD then syncs everything else from Git.HUBMANAGED CLUSTER · snoManagedCluster snolabel name=snothe only manual stepPlacementlabelSelectorname in [sno, bm]acm-policiesPolicy gitops-bootstrapenforce · templates applied in order, each one waitsuntil the previous one is compliant1Namespaceopenshift-gitops-operatorNamespaceopenshift-gitops-operator2OperatorPolicyOpenShift GitOps, channel gitops-1.22OpenShift GitOpsoperator and default Argo CD instance3ClusterRoleBindingrights for the local application-controllerClusterRoleBindinglocal controller may deploy4Repository secretcopySecretData from the hubRepository secretGit credentials for the local Argo CD5Root Applicationpath clusters/sno/bootstrapRoot Applicationapp-of-apps for sno, prune: falseapplied in order by the policy framework on snoLocal Argo CDinstalled by the operator,reads the repository secretEverything elseoperators, OAuth, RBAC, apps ...synced by the local Argo CDfrom clusters/sno/The policy owns the five objects on the left.Argo CD owns everything the root Application brings in.Never manage the same object from both sides.created by yougenerated by ACMgenerated by Argo CDplatform componentBuild-up order = order of the policy templates
Bootstrap policy: the templates are applied in order, then the local Argo CD takes over

We extend our repository with:

acm-policies/
└── input
    └── gitops-bootstrap
        ├── 01-namespace.yaml          # openshift-gitops-operator
        ├── 02-operatorpolicy.yaml     # OpenShift GitOps Operator
        ├── 03-controller-rbac.yaml    # rights for the local controller
        ├── 04-repo-credentials.yaml   # Git credentials, copied from the hub
        └── 05-root-application.yaml   # app-of-apps for this cluster

The first file creates the namespace for the Operator. The OperatorPolicy in the next step expects it to exist:

input/gitops-bootstrap/01-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: openshift-gitops-operator
input/gitops-bootstrap/02-operatorpolicy.yaml
apiVersion: policy.open-cluster-management.io/v1beta1
kind: OperatorPolicy (1)
metadata:
  name: openshift-gitops-operator
spec:
  remediationAction: enforce
  severity: high
  complianceType: musthave
  subscription:
    name: openshift-gitops-operator
    namespace: openshift-gitops-operator
    channel: gitops-1.22 (2)
    source: redhat-operators
    sourceNamespace: openshift-marketplace
  upgradeApproval: Automatic
1The PolicyGenerator adds OperatorPolicy manifests unchanged as a policy template, no extra wrapping.
2Pin the channel. Moving to the next version is a one-line pull request.

The root Application is the app-of-apps that contains the Application for every cluster.

input/gitops-bootstrap/05-root-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: openshift-gitops
spec:
  project: default
  source:
    repoURL: https://github.com/tjungbauer/openshift-clusterconfig-gitops.git
    targetRevision: main
    path: '{{hub printf "clusters/%s/bootstrap" .ManagedClusterName hub}}' (1)
  destination:
    server: https://kubernetes.default.svc
  syncPolicy:
    automated:
      prune: false (2)
      selfHeal: true
1A hub template, resolved on the hub for every cluster: sno gets clusters/sno/bootstrap.
2Never prune on the root Application. A broken path would otherwise remove everything below it.

The local Argo CD must be allowed to create whatever the root Application brings in. 03-controller-rbac.yaml grants its application-controller cluster-admin rights, the same as in the pull model of Ep.19:

input/gitops-bootstrap/03-controller-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: argo-admin
subjects:
  - kind: ServiceAccount
    name: openshift-gitops-argocd-application-controller (1)
    namespace: openshift-gitops
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin (2)
1The service account of the default instance openshift-gitops, which the Operator creates after the installation.
2Simple and usually fine for platform configuration. For application teams, prefer namespaces labelled with argocd.argoproj.io/managed-by.

And the generator configuration ties it together:

policy-generator.yaml (excerpt)
policies:
  - name: gitops-bootstrap
    remediationAction: enforce
    orderManifests: true (1)
    consolidateManifests: false
    placement:
      labelSelector:
        matchExpressions:
          - key: name (2)
            operator: In
            values:
              - sno
              - bm
    manifests:
      - path: input/gitops-bootstrap/01-namespace.yaml
      - path: input/gitops-bootstrap/02-operatorpolicy.yaml
      - path: input/gitops-bootstrap/03-controller-rbac.yaml
      - path: input/gitops-bootstrap/04-repo-credentials.yaml
      - path: input/gitops-bootstrap/05-root-application.yaml
1The order matters: the Application CRD only exists after the operator is installed. With orderManifests, the root Application is only created when the operator policy is compliant.
2The same selection as for the other policies: the clusters sno and bm, selected by the name label that ACM sets on every ManagedCluster. In a larger fleet, a dedicated label such as gitops=enabled makes the bootstrap an opt-in per cluster.
Pattern B is meant for clusters that get their Argo CD from this policy. If you followed the pull or agent part of Ep.19, the GitOps add-on of ACM has already installed OpenShift GitOps on sno and bm, and the policy and the add-on would then manage the same operator. Decide which of the two owns the operator before you enforce this policy, or test Pattern B on a cluster without the add-on.

Who owns what afterwards? The policy owns exactly five objects: the namespace, the operator, the ClusterRoleBinding, the repository secret and the root Application. Everything the root Application brings in belongs to Argo CD. Keep it that way: if you also manage the OpenShift GitOps Subscription with Argo CD on the spoke, the two will fight.


The result in ACM & Argo CD

Once Argo CD has synced the repository, the policies show up in the ACM console under Governance.

The list of Policies shows the three policies of this article in the namespace acm-policies: service-mesh, no-kubeadmin and gitops-bootstrap. For each policy you see the remediation action (inform or enforce), the cluster violations and the policy set, if there is one.

ACM Governance policies
Figure 1. Policies in the namespace acm-policies

A click on a policy opens the details. The Details tab shows the policy templates, here the ConfigurationPolicies and the OperatorPolicy that the PolicyGenerator created from the plain manifests, and the Placement with the selected clusters sno and bm.

The no-kubeadmin policy is a good example for inform: ACM only reports that the secret kubeadmin still exists, it does not delete it.

Results of the policy no-kubeadmin
Figure 2. Results of the policy no-kubeadmin

In the Argo CD UI we can also verify the status (including the resource tree).

The Application acm-policies shows the status Degraded. This is because the policy no-kubeadmin is non-compliant …​ the secrets are still present.

Application acm-policies in Argo CD
Figure 3. Application acm-policies in Argo CD

The resource tree proves this:

Resource tree of the application acm-policies
Figure 4. Resource tree of the application acm-policies

Summary

That concludes the mini-series. ACM decides where and checks compliance, Argo CD decides what and keeps it in sync, and Git is the only place where changes are made.

And as always: If it is not in Git, it does not exist. That includes your ACM policies.

The diagrams in this article have been manually drawn on a whiteboard. AI then generated the SVGs from photos and made them look equal in all articles.

Discussion

← Previous
Use arrow keys to navigate
Next →
←
→