external-dnsとは
[kubernetes-incubator/external-dns]
(https://github.com/kubernetes-incubator/external-dns)Configure external DNS servers (AWS Route53, Google CloudDNS and others) for Kubernetes Ingresses and Services
externa-dnsはKubernetes Incubatorプロジェクトの一つで、KubernetesユーザがクラウドプロバイダのWebコンソールやCLIを使わずとも簡単にDNSレコードを作成・更新してサービスを公開するために利用します。
AWSの場合、AWSコンソールやawscliを使わずともkubectlだけで簡単にRoute53でサービスを公開することができます。
external-dnsの仕組み
Kubernetesユーザが何かサービスをデプロイするとき、Deployment、Service、Ingressなどを作成します。そのとき、IngressやServiceに特定のAnnotationをつけると、その内容に応じたRoute53 RecordSetを作成してくれます。
external-dnsの使い方
この記事の後半に書きました。既にexternal-dnsがセットアップされていて使い方を知りたいだけの場合はそちらを参照してください!
external-dnsのセットアップ手順
IAMポリシーの割当
external-dnsが稼働するKubernetesノード、またはPodに必要なIAMポリシーを割り当てます。
具体的には、
- kube2iam/kiamを使わない場合は、Kubernetesノード用EC2インスタンスに割り当てられるIAMロール
- kube2iamやkiamを使う場合は、annotationで指定するIAMロール
に以下のようなIAMポリシーを設定します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets"
],
"Resource": [
"arn:aws:route53:::hostedzone/*"
]
},
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:ListResourceRecordSets"
],
"Resource": [
"*"
]
}
]
}
kube-awsの場合、cluster.yamlに以下のように記述します。
controller:
iam:
policy:
statements:
- effect: Allow
actions:
- "route53:ChangeResourceRecordSets"
resources:
- "arn:aws:route53:::hostedzone/*"
- effect: Allow
actions:
- "route53:ListHostedZones"
- "route53:ListResourceRecordSets"
resources:
- "*"
Ref: external-dnsのドキュメント: IAM Permissions
external-dnsのデプロイ
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: "node-role.kubernetes.io/master"
operator: "Exists"
tolerations:
- key: "node.alpha.kubernetes.io/role"
operator: "Equal"
value: "master"
effect: "NoSchedule"
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:v0.4.8
args:
- --source=service
- --source=ingress
- --domain-filter=example.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
- --provider=aws
- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
- --registry=txt
- --txt-owner-id=my-identifier
ポイント:
-
--domain-filter=example.com
はHostedZoneに対応するドメイン名に置き換えます。 - Service LoadBalancerにだけRecordSetを作成するようにしたい場合は、
--source=service
、Ingress LoadBalancerにだけRecordSetを作成するようにしたい場合は、--souce=ingress
、または両方を指定します。 - tolerationとaffinityは今回IAMポリシーを割り当てたmasterノードにexternal-dnsのpodをスケジュールするために指定します。
- workerノードのIAMポリシーも変更した場合は、tolerationとaffinityどちらも不要です。
external-dnsのログ確認
まだserviceにもingressにもannotationをつけていないとして、以下のように"All records are already up to date"と表示されればひとまずListHostedZones, ListResourceRecordSetsは呼び出せた、ということになります。IAMの設定はだいたいあっていそう、ということですね。
$ stern external-dns
+ external-dns-f76db487d-m6btx › external-dns
external-dns-f76db487d-m6btx external-dns time="2017-12-05T14:52:26Z" level=info msg="config: &{Master: KubeConfig: Sources:[service ingress] Namespace: AnnotationFilter: FQDNTemplate: Compatibility: PublishInternal:false Provider:aws GoogleProject: DomainFilter:[example.com] AWSZoneType: AzureConfigFile:/etc/kubernetes/azure.json AzureResourceGroup: CloudflareProxied:false InfobloxGridHost: InfobloxWapiPort:443 InfobloxWapiUsername:admin InfobloxWapiPassword: InfobloxWapiVersion:2.3.1 InfobloxSSLVerify:true InMemoryZones:[] Policy:upsert-only Registry:txt TXTOwnerID:my-identifier TXTPrefix: Interval:1m0s Once:false DryRun:false LogFormat:text MetricsAddress::7979 LogLevel:info}"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T14:52:26Z" level=info msg="Connected to cluster at https://10.3.0.1:443"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T14:52:27Z" level=info msg="All records are already up to date"
external-dnsの利用手順
Service LoadBalancerのRecordSetをつくってみる
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: mynginx.example.com
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 80
selector:
app: nginx
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
$ pbpaste | k create -f -
service "nginx" created
deployment "nginx" created
Service LoadBalancer(ELB)が作成されて、その後にexternal-dnsがELBへのA(lias)レコードを作成します。
external-dnsのログを眺めてみると、今回は、最後の更新チェックである"All records are already up to date"から約1分ほどでELBの作成からRecordSetの作成までが完了したことがわかります。
external-dns-f76db487d-m6btx external-dns time="2017-12-05T14:56:31Z" level=info msg="All records are already up to date"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T14:57:32Z" level=info msg="Desired change: CREATE mynginx.example.com A"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T14:57:32Z" level=info msg="Desired change: CREATE mynginx.example.com TXT"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T14:57:33Z" level=info msg="Record in zone example.com. were successfully updated"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T14:58:34Z" level=info msg="All records are already up to date"
awscliで該当ドメインのHostedZoneの内容を見てみると、たしかにRecordSetが作成されていることがわかります。
$ aws route53 list-resource-record-sets --hosted-zone-id "/hostedzone/MYHOSTEDZONEID"
{
"ResourceRecordSets": [
{
"AliasTarget": {
"HostedZoneId": "MYHOSTEDZONEID",
"EvaluateTargetHealth": true,
"DNSName": "__HASH__.ap-northeast-1.elb.amazonaws.com."
},
"Type": "A",
"Name": "mynginx.example.com.net."
},
{
"ResourceRecords": [
{
"Value": "\"heritage=external-dns,external-dns/owner=my-identifier\""
}
],
"Type": "TXT",
"Name": "mynginx.example.com.",
"TTL": 300
}
]
}
TXTレコードはホスト名に関連付けるテキストデータを保持するレコードですが、external-dnsはそれをレコードの所有権を表すことに使っています。external-dns/owner=my-identifier
とある部分が、external-dnsのコマンドライン・オプションで指定した--txt-owner-id=my-identifier
に対応します。
これのおかげで、external-dnsは既存の、external-dnsの管理外のレコードセットを誤って削除せずにすみます。
また、同じHostedZoneを複数のクラスタのexternal-dnsで共有する場合でも、txt-owner-idをクラスタ間で異なる値にしておけば、他のクラスタで作られたRecordSetを誤って削除するということもありません。
Service LoadBalancerのRecordSetを削除してみる
試しにexternal-dns.alpha.kubernetes.io/hostname: mynginx.example.com
というannotationを消してみると・・・
$ k edit svc nginx
いつまでたっても消えませんね。
external-dns-f76db487d-m6btx external-dns time="2017-12-05T14:58:34Z" level=info msg="All records are already up to date"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T14:59:35Z" level=info msg="All records are already up to date"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T15:00:36Z" level=info msg="All records are already up to date"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T15:01:37Z" level=info msg="All records are already up to date"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T15:02:38Z" level=info msg="All records are already up to date"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T15:03:39Z" level=info msg="All records are already up to date"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T15:04:40Z" level=info msg="All records are already up to date"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T15:05:41Z" level=info msg="All records are already up to date"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T15:06:42Z" level=info msg="All records are already up to date"
これは、external-dnsのコマンドライン・オプションで--policy=upsert-only
を指定しているためです。upsert-onlyの場合、annotationの内容に応じてRecordSetを新規作成か更新するだけで、削除はしません。うっかりRecordSetを削除してしまって、サービスをダウンさせてしまわないようにするための機能ですね。
試しに、external-dnsのpolicyを他のものに変更してみます。
external-dnsのソースをみるとupsert-onlyかsyncしかないので、syncにしてみます。
$ k edit deploy external-dns
--policy=sync
deployment "external-dns" edited
ログを見てみると、10秒ほどでexternal-dnsが再起動して、どこからも使われていないRecordSetを削除します。なお、今回利用したHostedZoneには他にもRecordSetがあったのですが、ちゃんと先程作成してからannotationを削除したものだけが削除されています。TXTレコードのおかげですね。
external-dns-f76db487d-m6btx external-dns time="2017-12-05T15:12:48Z" level=info msg="All records are already up to date"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T15:13:35Z" level=info msg="Received SIGTERM. Terminating..."
external-dns-f76db487d-m6btx external-dns time="2017-12-05T15:13:35Z" level=info msg="Terminating main controller loop"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T15:13:35Z" level=info msg="Pod waiting to be deleted"
external-dns-f76db487d-m6btx external-dns time="2017-12-05T15:14:05Z" level=info msg="Pod waiting to be deleted"
- external-dns-f76db487d-m6btx
+ external-dns-7fd4749755-gsjbl › external-dns
external-dns-7fd4749755-gsjbl external-dns time="2017-12-05T15:14:08Z" level=info msg="config: &{Master: KubeConfig: Sources:[service ingress] Namespace: AnnotationFilter: FQDNTemplate: Compatibility: PublishInternal:false Provider:aws GoogleProject: DomainFilter:[example.com] AWSZoneType: AzureConfigFile:/etc/kubernetes/azure.json AzureResourceGroup: CloudflareProxied:false InfobloxGridHost: InfobloxWapiPort:443 InfobloxWapiUsername:admin InfobloxWapiPassword: InfobloxWapiVersion:2.3.1 InfobloxSSLVerify:true InMemoryZones:[] Policy:sync Registry:txt TXTOwnerID:my-identifier TXTPrefix: Interval:1m0s Once:false DryRun:false LogFormat:text MetricsAddress::7979 LogLevel:info}"
external-dns-7fd4749755-gsjbl external-dns time="2017-12-05T15:14:08Z" level=info msg="Connected to cluster at https://10.3.0.1:443"
external-dns-7fd4749755-gsjbl external-dns time="2017-12-05T15:14:10Z" level=info msg="Desired change: DELETE mynginx.example.com A"
external-dns-7fd4749755-gsjbl external-dns time="2017-12-05T15:14:10Z" level=info msg="Desired change: DELETE mynginx.example.com TXT"
external-dns-7fd4749755-gsjbl external-dns time="2017-12-05T15:14:10Z" level=info msg="Record in zone example.com. were successfully updated"
IAMパーミッションを絞る
今回はIAMポリシーで全HostedZoneの操作をexternal-dnsに許可しました。セキュリティを考慮すると、Kubernetesから変更できてよいHostedZoneを絞り込むべきです。
幸い、Route53のChangeResourceRecordSets APIはResource Level Permissionに対応しており、以下のように特定のHostedZoneだけを許可することができます。
"Resource": "arn:aws:route53:::hostedzone/MYHOSTEDZONEID"
kube-awsの場合は、cluster.yamlを以下のように変更します。
controller:
iam:
policy:
statements:
- effect: Allow
actions:
- "route53:ChangeResourceRecordSets"
resources:
# BEFORE:
# - "arn:aws:route53:::hostedzone/*"
# AFTER:
- "arn:aws:route53:::hostedzone/MYHOSTEDZONEID"
- effect: Allow
actions:
- "route53:ListHostedZones"
- "route53:ListResourceRecordSets"
resources:
- "*"
参考: Amazon Route 53 API Permissions: Actions, Resources, and Conditions Reference - Amazon Route 53
この状態でもう一度annotationをつけてみます。
$ k edit svc nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: mynginx.example.com
stern external-dns
ポリシーを間違えていると以下のようにエラーになりますが、
external-dns-7fd4749755-gsjbl external-dns time="2017-12-05T15:43:39Z" level=info msg="Desired change: CREATE mynginx.example.com A"
external-dns-7fd4749755-gsjbl external-dns time="2017-12-05T15:43:39Z" level=info msg="Desired change: CREATE mynginx.example.com TXT"
external-dns-7fd4749755-gsjbl external-dns time="2017-12-05T15:43:40Z" level=error msg="AccessDenied: User: arn:aws:sts::myaccountid:assumed-role/k8s3-Controlplane-7VONPPP3RI90-IAMRoleController-1VP8UCUTQBGBC/bc931bf0-k8s3-Controlplane-7VONPPP3RI90-IAMRoleController-1VP8UC is not authorized to perform: route53:ChangeResourceRecordSets on resource: arn:aws:route53:::hostedzone/MYHOSTEDZONEID
external-dns-7fd4749755-gsjbl external-dns status code: 403, request id: 10b69f55-d9d3-11e7-947b-0dd69a6ee2ee"
あっている場合は以下のようにRecordSetがつくられます。
external-dns-7fd4749755-gsjbl external-dns time="2017-12-05T15:44:41Z" level=info msg="Desired change: CREATE mynginx.example.com A"
external-dns-7fd4749755-gsjbl external-dns time="2017-12-05T15:44:41Z" level=info msg="Desired change: CREATE mynginx.example.com TXT"
external-dns-7fd4749755-gsjbl external-dns time="2017-12-05T15:44:41Z" level=info msg="Record in zone example.com. were successfully updated"
external-dns-7fd4749755-gsjbl external-dns time="2017-12-05T15:45:42Z" level=info msg="All records are already up to date"
TTLを設定する
ttlというannotationでRecordSetのTTLを設定できます。例えば以下のようにするとTTLが60秒のレコードセットが作成されます。
external-dns.alpha.kubernetes.io/ttl: 60
Private Hosted Zoneにレコードをつくる
Private Hosted ZoneのHostedZoneIdに対するChangeResourceRecordSetsをIAMで許可して、external-dnsのコマンドラインオプションでPrivate Hosted Zoneに対応するドメイン名を指定すればOKです。
kube-awsのcluster.yaml
controller:
iam:
policy:
statements:
- effect: Allow
actions:
- "route53:ChangeResourceRecordSets"
resources:
- "arn:aws:route53:::hostedzone/MYPUBLICHOSTEDZONE"
- "arn:aws:route53:::hostedzone/MYPRIVATEHOSTEDZONE"
- effect: Allow
actions:
- "route53:ListHostedZones"
- "route53:ListResourceRecordSets"
resources:
- "*"
external-dnsのコマンドラインオプション
--domain-filter=private.example.com
external-dnsのログ(通常のHostedZoneの場合と全く一緒です)
external-dns-85576898b4-c2qdj external-dns time="2017-12-05T15:56:32Z" level=info msg="Desired change: CREATE mynginx.private.example.com A"
external-dns-85576898b4-c2qdj external-dns time="2017-12-05T15:56:32Z" level=info msg="Desired change: CREATE mynginx.private.example.com TXT"
external-dns-85576898b4-c2qdj external-dns time="2017-12-05T15:56:32Z" level=info msg="Record in zone private.example.com. were successfully updated"
レコードの削除
Serviceを削除すると、10秒ほどで自動的に削除されます。
kubectl delete svc nginx
external-dns-85576898b4-c2qdj external-dns time="2017-12-05T16:15:51Z" level=info msg="Desired change: DELETE mynginx.private.example.com A"
external-dns-85576898b4-c2qdj external-dns time="2017-12-05T16:15:51Z" level=info msg="Desired change: DELETE mynginx.private.example.com TXT"
external-dns-85576898b4-c2qdj external-dns time="2017-12-05T16:15:51Z" level=info msg="Record in zone private.example.com. were successfully updated"
まとめ
external-dnsを使うと、annotationをつけるだけで簡単にService/Ingress Load BalancerをDNSで公開することができます。
IAMで権限を絞れば、自由に使っていいドメインについてはAWSがあまりわからないエンジニアでも自分でDNSレコードをつくれるようになり、権限委譲が捗りそうですね!