sigs.k8s.io/external-dns@v0.14.1/docs/tutorials/gke.md (about) 1 # Setting up ExternalDNS on Google Kubernetes Engine 2 3 This tutorial describes how to setup ExternalDNS for usage within a [GKE](https://cloud.google.com/kubernetes-engine) ([Google Kuberentes Engine](https://cloud.google.com/kubernetes-engine)) cluster. Make sure to use **>=0.11.0** version of ExternalDNS for this tutorial 4 5 ## Single project test scenario using access scopes 6 7 *If you prefer to try-out ExternalDNS in one of the existing environments you can skip this step* 8 9 The following instructions use [access scopes](https://cloud.google.com/compute/docs/access/service-accounts#accesscopesiam) to provide ExternalDNS with the permissions it needs to manage DNS records within a single [project](https://cloud.google.com/docs/overview#projects), the organizing entity to allocate resources. 10 11 Note that since these permissions are associated with the instance, all pods in the cluster will also have these permissions. As such, this approach is not suitable for anything but testing environments. 12 13 This solution will only work when both CloudDNS and GKE are provisioned in the same project. If the CloudDNS zone is in a different project, this solution will not work. 14 15 ### Configure Project Environment 16 17 Setup your environment to work with Google Cloud Platform. Fill in your variables as needed, e.g. target project. 18 19 ```bash 20 # set variables to the appropriate desired values 21 PROJECT_ID="my-external-dns-test" 22 REGION="europe-west1" 23 ZONE="europe-west1-d" 24 ClOUD_BILLING_ACCOUNT="<my-cloud-billing-account>" 25 # set default settings for project 26 gcloud config set project $PROJECT_ID 27 gcloud config set compute/region $REGION 28 gcloud config set compute/zone $ZONE 29 # enable billing and APIs if not done already 30 gcloud beta billing projects link $PROJECT_ID \ 31 --billing-account $BILLING_ACCOUNT 32 gcloud services enable "dns.googleapis.com" 33 gcloud services enable "container.googleapis.com" 34 ``` 35 36 ### Create GKE Cluster 37 38 ```bash 39 gcloud container clusters create $GKE_CLUSTER_NAME \ 40 --num-nodes 1 \ 41 --scopes "https://www.googleapis.com/auth/ndev.clouddns.readwrite" 42 ``` 43 44 **WARNING**: Note that this cluster will use the default [compute engine GSA](https://cloud.google.com/compute/docs/access/service-accounts#default_service_account) that contians the overly permissive project editor (`roles/editor`) role. So essentially, anything on the cluster could potentially grant escalated privileges. Also, as mentioned earlier, the access scope `ndev.clouddns.readwrite` will allow anything running on the cluster to have read/write permissions on all Cloud DNS zones within the same project. 45 46 ### Cloud DNS Zone 47 48 Create a DNS zone which will contain the managed DNS records. If using your own domain that was registered with a third-party domain registrar, you should point your domain's name servers to the values under the `nameServers` key. Please consult your registrar's documentation on how to do that. This tutorial will use example domain of `example.com`. 49 50 ```bash 51 gcloud dns managed-zones create "example-com" --dns-name "example.com." \ 52 --description "Automatically managed zone by kubernetes.io/external-dns" 53 ``` 54 55 Make a note of the nameservers that were assigned to your new zone. 56 57 ```bash 58 gcloud dns record-sets list \ 59 --zone "example-com" --name "example.com." --type NS 60 ``` 61 62 Outputs: 63 64 ``` 65 NAME TYPE TTL DATA 66 example.com. NS 21600 ns-cloud-e1.googledomains.com.,ns-cloud-e2.googledomains.com.,ns-cloud-e3.googledomains.com.,ns-cloud-e4.googledomains.com. 67 ``` 68 69 In this case it's `ns-cloud-{e1-e4}.googledomains.com.` but your's could slightly differ, e.g. `{a1-a4}`, `{b1-b4}` etc. 70 71 ## Cross project access scenario using Google Service Account 72 73 More often, following best practices in regards to security and operations, Cloud DNS zones will be managed in a separate project from the Kubernetes cluster. This section shows how setup ExternalDNS to access Cloud DNS from a different project. These steps will also work for single project scenarios as well. 74 75 ExternalDNS will need permissions to make changes to the Cloud DNS zone. There are three ways to configure the access needed: 76 77 * [Worker Node Service Account](#worker-node-service-account) 78 * [Static Credentials](#static-credentials) 79 * [Workload Identity](#workload-identity) 80 81 ### Setup Cloud DNS and GKE 82 83 Below are examples on how you can configure Cloud DNS and GKE in separate projects, and then use one of the three methods to grant access to ExternalDNS. Replace the environment variables to values that make sense in your environment. 84 85 #### Configure Projects 86 87 For this process, create projects with the appropriate APIs enabled. 88 89 ```bash 90 # set variables to appropriate desired values 91 GKE_PROJECT_ID="my-workload-project" 92 DNS_PROJECT_ID="my-cloud-dns-project" 93 ClOUD_BILLING_ACCOUNT="<my-cloud-billing-account>" 94 # enable billing and APIs for DNS project if not done already 95 gcloud config set project $DNS_PROJECT_ID 96 gcloud beta billing projects link $CLOUD_DNS_PROJECT \ 97 --billing-account $ClOUD_BILLING_ACCOUNT 98 gcloud services enable "dns.googleapis.com" 99 # enable billing and APIs for GKE project if not done already 100 gcloud config set project $GKE_PROJECT_ID 101 gcloud beta billing projects link $CLOUD_DNS_PROJECT \ 102 --billing-account $ClOUD_BILLING_ACCOUNT 103 gcloud services enable "container.googleapis.com" 104 ``` 105 106 #### Provisioning Cloud DNS 107 108 Create a Cloud DNS zone in the designated DNS project. 109 110 ```bash 111 gcloud dns managed-zones create "example-com" --project $DNS_PROJECT_ID \ 112 --description "example.com" --dns-name="example.com." --visibility=public 113 ``` 114 115 If using your own domain that was registered with a third-party domain registrar, you should point your domain's name servers to the values under the `nameServers` key. Please consult your registrar's documentation on how to do that. The example domain of `example.com` will be used for this tutorial. 116 117 #### Provisioning a GKE cluster for cross project access 118 119 Create a GSA (Google Service Account) and grant it the [minimal set of privileges required](https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster#use_least_privilege_sa) for GKE nodes: 120 121 ```bash 122 GKE_CLUSTER_NAME="my-external-dns-cluster" 123 GKE_REGION="us-central1" 124 GKE_SA_NAME="worker-nodes-sa" 125 GKE_SA_EMAIL="$GKE_SA_NAME@${GKE_PROJECT_ID}.iam.gserviceaccount.com" 126 127 ROLES=( 128 roles/logging.logWriter 129 roles/monitoring.metricWriter 130 roles/monitoring.viewer 131 roles/stackdriver.resourceMetadata.writer 132 ) 133 134 gcloud iam service-accounts create $GKE_SA_NAME \ 135 --display-name $GKE_SA_NAME --project $GKE_PROJECT_ID 136 137 # assign google service account to roles in GKE project 138 for ROLE in ${ROLES[*]}; do 139 gcloud projects add-iam-policy-binding $GKE_PROJECT_ID \ 140 --member "serviceAccount:$GKE_SA_EMAIL" \ 141 --role $ROLE 142 done 143 ``` 144 145 Create a cluster using this service account and enable [workload identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity): 146 147 ```bash 148 gcloud container clusters create $GKE_CLUSTER_NAME \ 149 --project $GKE_PROJECT_ID --region $GKE_REGION --num-nodes 1 \ 150 --service-account "$GKE_SA_EMAIL" \ 151 --workload-pool "$GKE_PROJECT_ID.svc.id.goog" 152 ``` 153 154 ### Worker Node Service Account method 155 156 In this method, the GSA (Google Service Account) that is associated with GKE worker nodes will be configured to have access to Cloud DNS. 157 158 **WARNING**: This will grant access to modify the Cloud DNS zone records for all containers running on cluster, not just ExternalDNS, so use this option with caution. This is not recommended for production environments. 159 160 ```bash 161 GKE_SA_EMAIL="$GKE_SA_NAME@${GKE_PROJECT_ID}.iam.gserviceaccount.com" 162 163 # assign google service account to dns.admin role in the cloud dns project 164 gcloud projects add-iam-policy-binding $DNS_PROJECT_ID \ 165 --member serviceAccount:$GKE_SA_EMAIL \ 166 --role roles/dns.admin 167 ``` 168 169 After this, follow the steps in [Deploy ExternalDNS](#deploy-externaldns). Make sure to set the `--google-project` flag to match the Cloud DNS project name. 170 171 ### Static Credentials 172 173 In this scenario, a new GSA (Google Service Account) is created that has access to the CloudDNS zone. The credentials for this GSA are saved and installed as a Kubernetes secret that will be used by ExternalDNS. 174 175 This allows only containers that have access to the secret, such as ExternalDNS to update records on the Cloud DNS Zone. 176 177 #### Create GSA for use with static credentials 178 179 ```bash 180 DNS_SA_NAME="external-dns-sa" 181 DNS_SA_EMAIL="$DNS_SA_NAME@${GKE_PROJECT_ID}.iam.gserviceaccount.com" 182 183 # create GSA used to access the Cloud DNS zone 184 gcloud iam service-accounts create $DNS_SA_NAME --display-name $DNS_SA_NAME 185 186 # assign google service account to dns.admin role in cloud-dns project 187 gcloud projects add-iam-policy-binding $DNS_PROJECT_ID \ 188 --member serviceAccount:$DNS_SA_EMAIL --role "roles/dns.admin" 189 ``` 190 191 #### Create Kubernetes secret using static credentials 192 193 Generate static credentials from the ExternalDNS GSA. 194 195 ```bash 196 # download static credentials 197 gcloud iam service-accounts keys create /local/path/to/credentials.json \ 198 --iam-account $DNS_SA_EMAIL 199 ``` 200 201 Create a Kubernetes secret with the credentials in the same namespace of ExternalDNS. 202 203 ```bash 204 kubectl create secret generic "external-dns" --namespace ${EXTERNALDNS_NS:-"default"} \ 205 --from-file /local/path/to/credentials.json 206 ``` 207 208 After this, follow the steps in [Deploy ExternalDNS](#deploy-externaldns). Make sure to set the `--google-project` flag to match Cloud DNS project name. Make sure to uncomment out the section that mounts the secret to the ExternalDNS pods. 209 ### Workload Identity 210 211 [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) allows workloads in your GKE cluster to impersonate GSA (Google Service Accounts) using KSA (Kubernetes Service Accounts) configured during deployemnt. These are the steps to use this feature with ExternalDNS. 212 213 #### Create GSA for use with Workload Identity 214 215 ```bash 216 DNS_SA_NAME="external-dns-sa" 217 DNS_SA_EMAIL="$DNS_SA_NAME@${GKE_PROJECT_ID}.iam.gserviceaccount.com" 218 219 gcloud iam service-accounts create $DNS_SA_NAME --display-name $DNS_SA_NAME 220 gcloud projects add-iam-policy-binding $DNS_PROJECT_ID \ 221 --member serviceAccount:$DNS_SA_EMAIL --role "roles/dns.admin" 222 ``` 223 224 #### Link KSA to GSA 225 226 Add an IAM policy binding bewtween the workload identity GSA and ExternalDNS GSA. This will link the ExternalDNS KSA to ExternalDNS GSA. 227 228 ```bash 229 gcloud iam service-accounts add-iam-policy-binding $DNS_SA_EMAIL \ 230 --role "roles/iam.workloadIdentityUser" \ 231 --member "serviceAccount:$GKE_PROJECT_ID.svc.id.goog[${EXTERNALDNS_NS:-"default"}/external-dns]" 232 ``` 233 234 #### Deploy External DNS 235 236 Deploy ExternalDNS with the following steps below, documented under [Deploy ExternalDNS](#deploy-externaldns). Set the `--google-project` flag to the Cloud DNS project name. 237 238 #### Link KSA to GSA in Kubernetes 239 240 Add the proper workload identity annotation to the ExternalDNS KSA. 241 242 ```bash 243 kubectl annotate serviceaccount "external-dns" \ 244 --namespace ${EXTERNALDNS_NS:-"default"} \ 245 "iam.gke.io/gcp-service-account=$DNS_SA_EMAIL" 246 ``` 247 248 #### Update ExternalDNS pods 249 250 Update the Pod spec to schedule the workloads on nodes that use Workload Identity and to use the annotated Kubernetes service account. 251 252 ```bash 253 kubectl patch deployment "external-dns" \ 254 --namespace ${EXTERNALDNS_NS:-"default"} \ 255 --patch \ 256 '{"spec": {"template": {"spec": {"nodeSelector": {"iam.gke.io/gke-metadata-server-enabled": "true"}}}}}' 257 ``` 258 259 After all of these steps you may see several messages with `googleapi: Error 403: Forbidden, forbidden`. After several minutes when the token is refreshed, these error messages will go away, and you should see info messages, such as: `All records are already up to date`. 260 261 ## Deploy ExternalDNS 262 263 Then apply the following manifests file to deploy ExternalDNS. 264 265 ```yaml 266 apiVersion: v1 267 kind: ServiceAccount 268 metadata: 269 name: external-dns 270 labels: 271 app.kubernetes.io/name: external-dns 272 --- 273 apiVersion: rbac.authorization.k8s.io/v1 274 kind: ClusterRole 275 metadata: 276 name: external-dns 277 labels: 278 app.kubernetes.io/name: external-dns 279 rules: 280 - apiGroups: [""] 281 resources: ["services","endpoints","pods","nodes"] 282 verbs: ["get","watch","list"] 283 - apiGroups: ["extensions","networking.k8s.io"] 284 resources: ["ingresses"] 285 verbs: ["get","watch","list"] 286 --- 287 apiVersion: rbac.authorization.k8s.io/v1 288 kind: ClusterRoleBinding 289 metadata: 290 name: external-dns-viewer 291 labels: 292 app.kubernetes.io/name: external-dns 293 roleRef: 294 apiGroup: rbac.authorization.k8s.io 295 kind: ClusterRole 296 name: external-dns 297 subjects: 298 - kind: ServiceAccount 299 name: external-dns 300 namespace: default # change if namespace is not 'default' 301 --- 302 apiVersion: apps/v1 303 kind: Deployment 304 metadata: 305 name: external-dns 306 labels: 307 app.kubernetes.io/name: external-dns 308 spec: 309 strategy: 310 type: Recreate 311 selector: 312 matchLabels: 313 app.kubernetes.io/name: external-dns 314 template: 315 metadata: 316 labels: 317 app.kubernetes.io/name: external-dns 318 spec: 319 serviceAccountName: external-dns 320 containers: 321 - name: external-dns 322 image: registry.k8s.io/external-dns/external-dns:v0.14.0 323 args: 324 - --source=service 325 - --source=ingress 326 - --domain-filter=example.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones 327 - --provider=google 328 - --log-format=json # google cloud logs parses severity of the "text" log format incorrectly 329 # - --google-project=my-cloud-dns-project # Use this to specify a project different from the one external-dns is running inside 330 - --google-zone-visibility=public # Use this to filter to only zones with this visibility. Set to either 'public' or 'private'. Omitting will match public and private zones 331 - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization 332 - --registry=txt 333 - --txt-owner-id=my-identifier 334 # # uncomment below if static credentials are used 335 # env: 336 # - name: GOOGLE_APPLICATION_CREDENTIALS 337 # value: /etc/secrets/service-account/credentials.json 338 # volumeMounts: 339 # - name: google-service-account 340 # mountPath: /etc/secrets/service-account/ 341 # volumes: 342 # - name: google-service-account 343 # secret: 344 # secretName: external-dns 345 ``` 346 347 Create the deployment for ExternalDNS: 348 349 ```bash 350 kubectl create --namespace "default" --filename externaldns.yaml 351 ``` 352 353 ## Verify ExternalDNS works 354 355 The following will deploy a small nginx server that will be used to demonstrate that ExternalDNS is working. 356 357 ### Verify using an external load balancer 358 359 Create the following sample application to test that ExternalDNS works. This example will provision a L4 load balancer. 360 361 ```yaml 362 apiVersion: v1 363 kind: Service 364 metadata: 365 name: nginx 366 annotations: 367 # change nginx.example.com to match an appropriate value 368 external-dns.alpha.kubernetes.io/hostname: nginx.example.com 369 spec: 370 type: LoadBalancer 371 ports: 372 - port: 80 373 targetPort: 80 374 selector: 375 app: nginx 376 --- 377 apiVersion: apps/v1 378 kind: Deployment 379 metadata: 380 name: nginx 381 spec: 382 selector: 383 matchLabels: 384 app: nginx 385 template: 386 metadata: 387 labels: 388 app: nginx 389 spec: 390 containers: 391 - image: nginx 392 name: nginx 393 ports: 394 - containerPort: 80 395 ``` 396 397 Create the deployment and service objects: 398 399 ```bash 400 kubectl create --namespace "default" --filename nginx.yaml 401 ``` 402 403 After roughly two minutes check that a corresponding DNS record for your service was created. 404 405 ```bash 406 gcloud dns record-sets list --zone "example-com" --name "nginx.example.com." 407 ``` 408 409 Example output: 410 411 ``` 412 NAME TYPE TTL DATA 413 nginx.example.com. A 300 104.155.60.49 414 nginx.example.com. TXT 300 "heritage=external-dns,external-dns/owner=my-identifier" 415 ``` 416 417 Note created `TXT` record alongside `A` record. `TXT` record signifies that the corresponding `A` record is managed by ExternalDNS. This makes ExternalDNS safe for running in environments where there are other records managed via other means. 418 419 Let's check that we can resolve this DNS name. We'll ask the nameservers assigned to your zone first. 420 421 ```bash 422 dig +short @ns-cloud-e1.googledomains.com. nginx.example.com. 423 104.155.60.49 424 ``` 425 426 Given you hooked up your DNS zone with its parent zone you can use `curl` to access your site. 427 428 ```bash 429 curl nginx.example.com 430 ``` 431 432 ### Verify using an ingress 433 434 Let's check that Ingress works as well. Create the following Ingress. 435 436 ```yaml 437 apiVersion: networking.k8s.io/v1 438 kind: Ingress 439 metadata: 440 name: nginx 441 spec: 442 rules: 443 - host: server.example.com 444 http: 445 paths: 446 - path: / 447 pathType: Prefix 448 backend: 449 service: 450 name: nginx 451 port: 452 number: 80 453 ``` 454 455 Create the ingress objects with: 456 457 ```bash 458 kubectl create --namespace "default" --filename ingress.yaml 459 ``` 460 461 Note that this will ingress object will use the default ingress controller that comes with GKE to create a L7 load balancer in addition to the L4 load balancer previously with the service object. To use only the L7 load balancer, update the service manafest to change the Service type to `NodePort` and remove the ExternalDNS annotation. 462 463 After roughly two minutes check that a corresponding DNS record for your Ingress was created. 464 465 ```bash 466 gcloud dns record-sets list \ 467 --zone "example-com" \ 468 --name "server.example.com." \ 469 ``` 470 Output: 471 472 ``` 473 NAME TYPE TTL DATA 474 server.example.com. A 300 130.211.46.224 475 server.example.com. TXT 300 "heritage=external-dns,external-dns/owner=my-identifier" 476 ``` 477 478 Let's check that we can resolve this DNS name as well. 479 480 ```bash 481 dig +short @ns-cloud-e1.googledomains.com. server.example.com. 482 130.211.46.224 483 ``` 484 485 Try with `curl` as well. 486 487 ```bash 488 curl server.example.com 489 ``` 490 491 ### Clean up 492 493 Make sure to delete all Service and Ingress objects before terminating the cluster so all load balancers get cleaned up correctly. 494 495 ```bash 496 kubectl delete service nginx 497 kubectl delete ingress nginx 498 ``` 499 500 Give ExternalDNS some time to clean up the DNS records for you. Then delete the managed zone and cluster. 501 502 ```bash 503 gcloud dns managed-zones delete "example-com" 504 gcloud container clusters delete "external-dns" 505 ```