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  ```