Chapter 13. Tutorial: Deploying the External DNS Operator on ROSA
The External DNS Operator deploys and manages ExternalDNS
to provide the name resolution for services and routes from the external DNS provider, like Amazon Route 53, to Red Hat OpenShift Service on AWS (ROSA) clusters. In this tutorial, we will deploy and configure the External DNS Operator with a secondary ingress controller to manage DNS records in Amazon Route 53.
The External DNS
Operator does not support STS using IAM Roles for Service Accounts (IRSA) and uses long-lived Identity Access Management (IAM) credentials instead. This tutorial will be updated when the Operator supports STS.
13.1. Prerequisites
A ROSA Classic cluster
NoteROSA with HCP is not supported at this time.
-
A user account with
cluster-admin
privileges -
The OpenShift CLI (
oc
) -
The Amazon Web Services (AWS) CLI (
aws
) -
A unique domain, such as
apps.example.com
- An Amazon Route 53 public hosted zone for the above domain
13.2. Setting up your environment
Configure the following environment variables:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow export DOMAIN=<apps.example.com> export AWS_PAGER="" export CLUSTER=$(oc get infrastructure cluster -o=jsonpath="{.status.infrastructureName}" | sed 's/-[a-z0-9]\{5\}$//') export REGION=$(oc get infrastructure cluster -o=jsonpath="{.status.platformStatus.aws.region}") export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) export SCRATCH="/tmp/${CLUSTER}/external-dns" mkdir -p ${SCRATCH}
$ export DOMAIN=<apps.example.com>
1 $ export AWS_PAGER="" $ export CLUSTER=$(oc get infrastructure cluster -o=jsonpath="{.status.infrastructureName}" | sed 's/-[a-z0-9]\{5\}$//') $ export REGION=$(oc get infrastructure cluster -o=jsonpath="{.status.platformStatus.aws.region}") $ export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) $ export SCRATCH="/tmp/${CLUSTER}/external-dns" $ mkdir -p ${SCRATCH}
- 1
- Replace with the custom domain you want to use for the
IngressController
.
Ensure all fields output correctly before moving to the next section:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow echo "Cluster: ${CLUSTER}, Region: ${REGION}, AWS Account ID: ${AWS_ACCOUNT_ID}"
$ echo "Cluster: ${CLUSTER}, Region: ${REGION}, AWS Account ID: ${AWS_ACCOUNT_ID}"
NoteThe "Cluster" output from the previous command may be the name of your cluster, the internal ID of your cluster, or the cluster’s domain prefix. If you prefer to use another identifier, you can manually set this value by running the following command:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow export CLUSTER=my-custom-value
$ export CLUSTER=my-custom-value
13.3. Secondary ingress controller setup
Use the following procedure to deploy a secondary ingress controller using a custom domain.
Prerequisites
-
A unique domain, such as
apps.example.com
-
A wildcard or SAN TLS certificate configured with the custom domain selected above (
CN=*.apps.example.com
)
Procedure
Create a new TLS secret from a private key and a public certificate, where
fullchain.pem
is your full wildcard certificate chain (including any intermediaries) andprivkey.pem
is your wildcard certificate’s private key:Copy to Clipboard Copied! Toggle word wrap Toggle overflow oc -n openshift-ingress create secret tls external-dns-tls --cert=fullchain.pem --key=privkey.pem
$ oc -n openshift-ingress create secret tls external-dns-tls --cert=fullchain.pem --key=privkey.pem
Create a new
IngressController
resource:Copy to Clipboard Copied! Toggle word wrap Toggle overflow cat << EOF | oc apply -f - apiVersion: operator.openshift.io/v1 kind: IngressController metadata: name: external-dns-ingress namespace: openshift-ingress-operator spec: domain: ${DOMAIN} defaultCertificate: name: external-dns-tls endpointPublishingStrategy: loadBalancer: dnsManagementPolicy: Unmanaged providerParameters: aws: type: NLB type: AWS scope: External type: LoadBalancerService EOF
$ cat << EOF | oc apply -f - apiVersion: operator.openshift.io/v1 kind: IngressController metadata: name: external-dns-ingress namespace: openshift-ingress-operator spec: domain: ${DOMAIN} defaultCertificate: name: external-dns-tls endpointPublishingStrategy: loadBalancer: dnsManagementPolicy: Unmanaged providerParameters: aws: type: NLB type: AWS scope: External type: LoadBalancerService EOF
WarningThis
IngressController
example will create an internet accessible Network Load Balancer (NLB) in your AWS account. To provision an internal NLB instead, set the.spec.endpointPublishingStrategy.loadBalancer.scope
parameter toInternal
before creating theIngressController
resource.Verify that your custom domain IngressController has successfully created an external load balancer:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow oc -n openshift-ingress get service/router-external-dns-ingress
$ oc -n openshift-ingress get service/router-external-dns-ingress
Example output
Copy to Clipboard Copied! Toggle word wrap Toggle overflow NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE router-external-dns-ingress LoadBalancer 172.30.71.250 a4838bb991c6748439134ab89f132a43-aeae124077b50c01.elb.us-east-1.amazonaws.com 80:32227/TCP,443:30310/TCP 43s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE router-external-dns-ingress LoadBalancer 172.30.71.250 a4838bb991c6748439134ab89f132a43-aeae124077b50c01.elb.us-east-1.amazonaws.com 80:32227/TCP,443:30310/TCP 43s
13.4. Preparing your AWS account
Retrieve the Amazon Route 53 public hosted zone ID:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow export ZONE_ID=$(aws route53 list-hosted-zones-by-name --output json \ --dns-name "${DOMAIN}." --query 'HostedZones[0]'.Id --out text | sed 's/\/hostedzone\///')
$ export ZONE_ID=$(aws route53 list-hosted-zones-by-name --output json \ --dns-name "${DOMAIN}." --query 'HostedZones[0]'.Id --out text | sed 's/\/hostedzone\///')
Prepare a document with the necessary DNS changes to enable DNS resolution for the canonical domain of the Ingress Controller:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow NLB_HOST=$(oc -n openshift-ingress get service/router-external-dns-ingress -ojsonpath="{.status.loadBalancer.ingress[0].hostname}") cat << EOF > "${SCRATCH}/create-cname.json"
$ NLB_HOST=$(oc -n openshift-ingress get service/router-external-dns-ingress -ojsonpath="{.status.loadBalancer.ingress[0].hostname}") $ cat << EOF > "${SCRATCH}/create-cname.json" { "Comment":"Add CNAME to ingress controller canonical domain", "Changes":[{ "Action":"CREATE", "ResourceRecordSet":{ "Name": "router-external-dns-ingress.${DOMAIN}", "Type":"CNAME", "TTL":30, "ResourceRecords":[{ "Value": "${NLB_HOST}" }] } }] } EOF
The External DNS Operator uses this canonical domain as the target for CNAME records.
Submit your changes to Amazon Route 53 for propagation:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow aws route53 change-resource-record-sets \ --hosted-zone-id ${ZONE_ID} \ --change-batch file://${SCRATCH}/create-cname.json
aws route53 change-resource-record-sets \ --hosted-zone-id ${ZONE_ID} \ --change-batch file://${SCRATCH}/create-cname.json
Create an AWS IAM Policy document that allows the
External DNS
Operator to update only the custom domain public hosted zone:Copy to Clipboard Copied! Toggle word wrap Toggle overflow cat << EOF > "${SCRATCH}/external-dns-policy.json" { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "route53:ChangeResourceRecordSets" ], "Resource": [ "arn:aws:route53:::hostedzone/${ZONE_ID}" ] }, { "Effect": "Allow", "Action": [ "route53:ListHostedZones", "route53:ListResourceRecordSets" ], "Resource": [ "*" ] } ] } EOF
$ cat << EOF > "${SCRATCH}/external-dns-policy.json" { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "route53:ChangeResourceRecordSets" ], "Resource": [ "arn:aws:route53:::hostedzone/${ZONE_ID}" ] }, { "Effect": "Allow", "Action": [ "route53:ListHostedZones", "route53:ListResourceRecordSets" ], "Resource": [ "*" ] } ] } EOF
Create an AWS IAM user:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow aws iam create-user --user-name "${CLUSTER}-external-dns-operator"
$ aws iam create-user --user-name "${CLUSTER}-external-dns-operator"
Attach the policy:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow aws iam attach-user-policy --user-name "${CLUSTER}-external-dns-operator" --policy-arn $POLICY_ARN
$ aws iam attach-user-policy --user-name "${CLUSTER}-external-dns-operator" --policy-arn $POLICY_ARN
NoteThis will be changed to STS using IRSA in the future.
Create AWS keys for the IAM user:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow SECRET_ACCESS_KEY=$(aws iam create-access-key --user-name "${CLUSTER}-external-dns-operator")
$ SECRET_ACCESS_KEY=$(aws iam create-access-key --user-name "${CLUSTER}-external-dns-operator")
Create static credentials:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow cat << EOF > "${SCRATCH}/credentials" [default] aws_access_key_id = $(echo $SECRET_ACCESS_KEY | jq -r '.AccessKey.AccessKeyId') aws_secret_access_key = $(echo $SECRET_ACCESS_KEY | jq -r '.AccessKey.SecretAccessKey') EOF
$ cat << EOF > "${SCRATCH}/credentials" [default] aws_access_key_id = $(echo $SECRET_ACCESS_KEY | jq -r '.AccessKey.AccessKeyId') aws_secret_access_key = $(echo $SECRET_ACCESS_KEY | jq -r '.AccessKey.SecretAccessKey') EOF
13.5. Installing the External DNS Operator
Create a new project:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow oc new-project external-dns-operator
$ oc new-project external-dns-operator
Install the
External DNS
Operator from OperatorHub:Copy to Clipboard Copied! Toggle word wrap Toggle overflow cat << EOF | oc apply -f - apiVersion: operators.coreos.com/v1 kind: OperatorGroup metadata: name: external-dns-group namespace: external-dns-operator spec: targetNamespaces: - external-dns-operator --- apiVersion: operators.coreos.com/v1alpha1 kind: Subscription metadata: name: external-dns-operator namespace: external-dns-operator spec: channel: stable-v1.1 installPlanApproval: Automatic name: external-dns-operator source: redhat-operators sourceNamespace: openshift-marketplace EOF
$ cat << EOF | oc apply -f - apiVersion: operators.coreos.com/v1 kind: OperatorGroup metadata: name: external-dns-group namespace: external-dns-operator spec: targetNamespaces: - external-dns-operator --- apiVersion: operators.coreos.com/v1alpha1 kind: Subscription metadata: name: external-dns-operator namespace: external-dns-operator spec: channel: stable-v1.1 installPlanApproval: Automatic name: external-dns-operator source: redhat-operators sourceNamespace: openshift-marketplace EOF
Wait until the
External DNS
Operator is running:Copy to Clipboard Copied! Toggle word wrap Toggle overflow oc rollout status deploy external-dns-operator --timeout=300s
$ oc rollout status deploy external-dns-operator --timeout=300s
Create a secret from the AWS IAM user credentials:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow oc -n external-dns-operator create secret generic external-dns \ --from-file "${SCRATCH}/credentials"
$ oc -n external-dns-operator create secret generic external-dns \ --from-file "${SCRATCH}/credentials"
Deploy the
ExternalDNS
controller:Copy to Clipboard Copied! Toggle word wrap Toggle overflow cat << EOF | oc apply -f - apiVersion: externaldns.olm.openshift.io/v1beta1 kind: ExternalDNS metadata: name: ${DOMAIN} spec: domains: - filterType: Include matchType: Exact name: ${DOMAIN} provider: aws: credentials: name: external-dns type: AWS source: openshiftRouteOptions: routerName: external-dns-ingress type: OpenShiftRoute zones: - ${ZONE_ID} EOF
$ cat << EOF | oc apply -f - apiVersion: externaldns.olm.openshift.io/v1beta1 kind: ExternalDNS metadata: name: ${DOMAIN} spec: domains: - filterType: Include matchType: Exact name: ${DOMAIN} provider: aws: credentials: name: external-dns type: AWS source: openshiftRouteOptions: routerName: external-dns-ingress type: OpenShiftRoute zones: - ${ZONE_ID} EOF
Wait until the controller is running:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow oc rollout status deploy external-dns-${DOMAIN} --timeout=300s
$ oc rollout status deploy external-dns-${DOMAIN} --timeout=300s
13.6. Deploying a sample application
Now that the ExternalDNS
controller is running, you can deploy a sample application to confirm that the custom domain is configured and trusted when you expose a new route.
Create a new project for your sample application:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow oc new-project hello-world
$ oc new-project hello-world
Deploy a hello world application:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow oc new-app -n hello-world --image=docker.io/openshift/hello-openshift
$ oc new-app -n hello-world --image=docker.io/openshift/hello-openshift
Create a route for the application specifying your custom domain name:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow oc -n hello-world create route edge --service=hello-openshift hello-openshift-tls \ --hostname hello-openshift.${DOMAIN}
$ oc -n hello-world create route edge --service=hello-openshift hello-openshift-tls \ --hostname hello-openshift.${DOMAIN}
Check if the DNS record was created automatically by ExternalDNS:
NoteIt can take a few minutes for the record to appear in Amazon Route 53.
Copy to Clipboard Copied! Toggle word wrap Toggle overflow aws route53 list-resource-record-sets --hosted-zone-id ${ZONE_ID} \ --query "ResourceRecordSets[?Type == 'CNAME']" | grep hello-openshift
$ aws route53 list-resource-record-sets --hosted-zone-id ${ZONE_ID} \ --query "ResourceRecordSets[?Type == 'CNAME']" | grep hello-openshift
Optional: You can also view the TXT records that indicate they were created by ExternalDNS:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow aws route53 list-resource-record-sets --hosted-zone-id ${ZONE_ID} \ --query "ResourceRecordSets[?Type == 'TXT']" | grep ${DOMAIN}
$ aws route53 list-resource-record-sets --hosted-zone-id ${ZONE_ID} \ --query "ResourceRecordSets[?Type == 'TXT']" | grep ${DOMAIN}
Curl the newly created DNS record to your sample application to verify the hello world application is accessible:
Copy to Clipboard Copied! Toggle word wrap Toggle overflow curl https://hello-openshift.${DOMAIN}
$ curl https://hello-openshift.${DOMAIN}
Example output
Copy to Clipboard Copied! Toggle word wrap Toggle overflow Hello OpenShift!
Hello OpenShift!