|
| 1 | +# GCP Cloud Controller Manager (CCM) Manual Setup Guide |
| 2 | + |
| 3 | +This guide provides instructions for building and deploying the GCP Cloud Controller Manager (CCM) to a self-managed Kubernetes cluster. |
| 4 | + |
| 5 | +## Prerequisites |
| 6 | + |
| 7 | +1. **Kubernetes Cluster**: A Kubernetes cluster running on Google Cloud Platform. |
| 8 | + * The cluster's components (`kube-apiserver`, `kube-controller-manager`, and `kubelet`) must have the `--cloud-provider=external` flag. |
| 9 | +2. **GCP Service Account**: The nodes (or the CCM pod itself) must have access to a GCP IAM Service Account with sufficient permissions to manage compute resources (e.g. instances, load balancers, and routes). |
| 10 | +3. **Docker & gcloud CLI**: Authorized and configured for pushing images to GCP Artifact Registry. |
| 11 | + |
| 12 | +## Step 1: Build and Push the CCM Image (Manual Clusters) |
| 13 | + |
| 14 | +If you are using a manually provisioned cluster (e.g. `kubeadm`), build the `cloud-controller-manager` Docker image and push it to your registry: |
| 15 | + |
| 16 | +```sh |
| 17 | +# Google Cloud Project ID, registry location, and repository name. |
| 18 | +GCP_PROJECT=$(gcloud config get-value project) |
| 19 | +GCP_LOCATION=us-central1 |
| 20 | +REPO=my-repo |
| 21 | + |
| 22 | +# Create an Artifact Registry repository (if it doesn't already exist) |
| 23 | +gcloud artifacts repositories create ${REPO} \ |
| 24 | + --project=${GCP_PROJECT} \ |
| 25 | + --repository-format=docker \ |
| 26 | + --location=${GCP_LOCATION} \ |
| 27 | + --description="Docker repository for CCM" |
| 28 | + |
| 29 | +# Grant the cluster nodes permission to read from the newly created Artifact Registry. |
| 30 | +# This automatically extracts your GCE node's service account using kubectl and gcloud. |
| 31 | +NODE_NAME=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') |
| 32 | +NODE_ZONE=$(kubectl get node $NODE_NAME -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}') |
| 33 | +NODE_SA=$(gcloud compute instances describe $NODE_NAME \ |
| 34 | + --zone=$NODE_ZONE --project=${GCP_PROJECT} \ |
| 35 | + --format="value(serviceAccounts[0].email)") |
| 36 | + |
| 37 | +gcloud artifacts repositories add-iam-policy-binding ${REPO} \ |
| 38 | + --project=${GCP_PROJECT} \ |
| 39 | + --location=${GCP_LOCATION} \ |
| 40 | + --member="serviceAccount:${NODE_SA}" \ |
| 41 | + --role="roles/artifactregistry.reader" |
| 42 | +# Configure docker to authenticate with Artifact Registry |
| 43 | +gcloud auth configure-docker ${GCP_LOCATION}-docker.pkg.dev |
| 44 | + |
| 45 | +# Build and Push |
| 46 | +IMAGE_REPO=${GCP_LOCATION}-docker.pkg.dev/${GCP_PROJECT}/${REPO} IMAGE_TAG=v0 make publish |
| 47 | +``` |
| 48 | + |
| 49 | +*Note: If `IMAGE_TAG` is omitted, the Makefile will use a combination of the current Git commit SHA and the build date.* |
| 50 | + |
| 51 | +## Step 2: Deploy the CCM to your Cluster (Manual Clusters) |
| 52 | + |
| 53 | +Once the image is pushed, you must deploy the necessary RBAC permissions and the CCM pod itself to the Kubernetes cluster. |
| 54 | + |
| 55 | +For native Kubernetes clusters, avoid the legacy `deploy/cloud-controller-manager.manifest` (which is a SaltStack template used by legacy `kube-up`). Instead, use the kustomize-ready DaemonSet which correctly includes the RBAC roles and deployment: |
| 56 | + |
| 57 | +1. Update the image to your newly pushed tag: |
| 58 | +```sh |
| 59 | +(cd deploy/packages/default && kustomize edit set image k8scloudprovidergcp/cloud-controller-manager=$IMAGE_REPO:$IMAGE_TAG) |
| 60 | +``` |
| 61 | +2. The `manifest.yaml` DaemonSet is left intentionally blank of execution flags (`args: []`). You **must** provide the necessary command-line arguments to the `cloud-controller-manager` container. For a typical Kops or GCE cluster, you can supply these arguments by creating a Kustomize patch. |
| 62 | + |
| 63 | +> [!NOTE] |
| 64 | +> If you skipped building your own image in Step 1 and chose to deploy the public upstream image (`k8scloudprovidergcp/cloud-controller-manager:latest`), you **must** also include `command: ["/cloud-controller-manager"]` in your patch's `containers` block. Locally built Dockerfile images automatically set the correct `ENTRYPOINT`, so they do not require this override! |
| 65 | +
|
| 66 | +> [!IMPORTANT] |
| 67 | +> Be sure to update the `--cluster-cidr` and `--cluster-name` arguments below to match your specific cluster's configuration. Note that GCP resource names cannot contain dots (`.`), so if your cluster name is `my.cluster.net`, you **must** use a sanitized format like `my-cluster-net` here! |
| 68 | +
|
| 69 | +```sh |
| 70 | +cat << EOF > deploy/packages/default/args-patch.yaml |
| 71 | +apiVersion: apps/v1 |
| 72 | +kind: DaemonSet |
| 73 | +metadata: |
| 74 | + name: cloud-controller-manager |
| 75 | + namespace: kube-system |
| 76 | +spec: |
| 77 | + template: |
| 78 | + spec: |
| 79 | + containers: |
| 80 | + - name: cloud-controller-manager |
| 81 | + args: |
| 82 | + - --cloud-provider=gce |
| 83 | + - --allocate-node-cidrs=true |
| 84 | + - --cluster-cidr=10.4.0.0/14 |
| 85 | + - --cluster-name=kops-k8s-local |
| 86 | + - --configure-cloud-routes=true |
| 87 | + - --leader-elect=true |
| 88 | + - --use-service-account-credentials=true |
| 89 | + - --v=2 |
| 90 | +EOF |
| 91 | +(cd deploy/packages/default && kustomize edit add patch --path args-patch.yaml) |
| 92 | + |
| 93 | +# Deploy the configured package (this applies the DaemonSet and its required roles): |
| 94 | +kubectl apply -k deploy/packages/default |
| 95 | +``` |
| 96 | + |
| 97 | +```sh |
| 98 | +# Clean up the local patch file and reset all changes to kustomization.yaml |
| 99 | +rm deploy/packages/default/args-patch.yaml |
| 100 | +git checkout deploy/packages/default/kustomization.yaml |
| 101 | +``` |
| 102 | + |
| 103 | +### Alternative: Apply Standalone RBAC Roles |
| 104 | + |
| 105 | +If you prefer to deploy the RBAC rules independently from the base daemonset package, you can apply them directly: |
| 106 | + |
| 107 | +```sh |
| 108 | +kubectl apply -f deploy/cloud-node-controller-role.yaml |
| 109 | +kubectl apply -f deploy/cloud-node-controller-binding.yaml |
| 110 | +kubectl apply -f deploy/pvl-controller-role.yaml |
| 111 | +``` |
| 112 | + |
| 113 | +## Step 3: Verification |
| 114 | + |
| 115 | +To verify that the Cloud Controller Manager is running successfully: |
| 116 | + |
| 117 | +1. **Check the Pod Status**: Verify the pod is `Running` in the `kube-system` namespace. |
| 118 | +```sh |
| 119 | +kubectl get pods -n kube-system -l component=cloud-controller-manager |
| 120 | +``` |
| 121 | + |
| 122 | +2. **Check Pod Logs**: Look for any errors or access and authentication issues with the GCP API. |
| 123 | +```sh |
| 124 | +kubectl logs -n kube-system -l component=cloud-controller-manager |
| 125 | +``` |
| 126 | + |
| 127 | +3. **Check Node Initialization**: The `kubelet` initially applies a `node.cloudprovider.kubernetes.io/uninitialized` taint when bound to an external cloud provider. The CCM should remove this taint once it successfully fetches the node's properties from the GCP API. |
| 128 | +```sh |
| 129 | +# Ensure no nodes have the uninitialized taint, output should be empty. |
| 130 | +kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints | grep uninitialized |
| 131 | +``` |
| 132 | + |
| 133 | +4. **Verify External IPs and ProviderID**: Check if your nodes are correctly populated with GCP-specific data (e.g., `ProviderID` in the format `gce://...`). |
| 134 | +```sh |
| 135 | +kubectl describe nodes | grep "ProviderID:" |
| 136 | +``` |
| 137 | + |
| 138 | +If you used `make kops-up` to provision your cluster, you can use `make kops-down` to tear down the cluster. |
0 commit comments