What's This
external-dnsを使用すればマニフェストに登録したいレコードを書くだけで、
自動でレコードとIngressのURLを紐づけてくれ、Route53にもレコードを登録してくれます。
今回はterraformを使用してAWS側で設定する部分を実現しました。
先人が残してくれた記事を大いに参考にしており、n番煎じかという内容かもしれませんが、先人たちと異なる実装部分もあるかと思い、まとめました。
構成
Serviceにexternal-dnsを使用している例も多いかと思いますが、今回の例ではIngressに対して使用しております。
Terraform側で用意するもの
kubectl_manifest リソースを使用して、TF内でk8sのリソースを定義しました。
WAFやSecurity Group、ACMなどを使うのであれば、それを変数として渡す方が組み立てが簡単になるかと思います。
k8sとTFの作業の境界がここだけ曖昧になるのが難点ですが…
eks.tf
resource "kubectl_manifest" "ingress_yaml" {
yaml_body = <<-EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: default
name: ${var.env}-ingress
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: instance
alb.ingress.kubernetes.io/wafv2-acl-arn: ${data.terraform_remote_state.waf_acmのoutputs} // WAF-ARN
alb.ingress.kubernetes.io/security-groups: ${aws_security_group.ingressのsg.id} // SG-ARN
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
alb.ingress.kubernetes.io/certificate-arn: ACMのARN
alb.ingress.kubernetes.io/ssl-redirect: "443"
external-dns.alpha.kubernetes.io/hostname: "${var.env}-domain name" // 使用したいレコードの名前
spec:
rules:
- host: ${var.env}-domain name // 使用したいレコードの名前
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service
port:
number: 80
EOF
}
// remote_stateを使っている場合、以下の記載も必要
data "terraform_remote_state" "waf_acm" {
backend = "s3"
config = {
bucket = "バケット名"
key = "tfstateの名前"
region = "リージョン"
}
}
.tfvars
なお、別途tfvarsで以下のように変数を定義しています。
これで環境ごとに名前付けをしてくれるので、上記で${var.env}で分けています。
env = "環境名"
external_dns.tf
resource "kubernetes_service_account" "external_dns" {
metadata {
name = "external-dns"
namespace = "default"
annotations = {
"eks.amazonaws.com/role-arn" : "${aws_iam_role.external_dns_role.arn}"
}
}
}
resource "kubernetes_cluster_role" "external_dns" {
metadata {
name = "${var.env}-external-dns"
}
rule {
api_groups = [""]
resources = ["services", "endpoints", "pods"]
verbs = ["get", "watch", "list"]
}
rule {
api_groups = ["extensions", "networking.k8s.io"]
resources = ["ingresses"]
verbs = ["get", "watch", "list"]
}
rule {
api_groups = [""]
resources = ["nodes"]
verbs = ["list", "watch"]
}
}
resource "kubernetes_cluster_role_binding" "external_dns_viewer" {
metadata {
name = "${var.env}-external-dns-viewer"
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "ClusterRole"
name = kubernetes_cluster_role.external_dns.metadata[0].name
}
subject {
kind = "ServiceAccount"
name = kubernetes_service_account.external_dns.metadata[0].name
namespace = "default"
}
}
data "aws_iam_policy_document" "external_dns_assume_role_policy" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
effect = "Allow"
condition {
test = "StringEquals"
variable = "${replace(module.cluster.oidc_provider, "https://", "")}:sub"
values = ["system:serviceaccount:default:external-dns"]
}
principals {
identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${module.cluster.oidc_provider}"]
type = "Federated"
}
}
}
resource "aws_iam_role" "external_dns_role" {
name = "${var.env}-external-dns-role"
assume_role_policy = data.aws_iam_policy_document.external_dns_assume_role_policy.json
}
resource "aws_iam_role_policy_attachment" "external_dns_policy_attachment" {
policy_arn = aws_iam_policy.external_dns_policy.arn
role = aws_iam_role.external_dns_role.name
}
resource "aws_iam_policy" "external_dns_policy" {
name = "${var.env}-external-dns-policy"
policy = file("external_dns.jsonのPATH")
}
IAM Policy 作成して、Roleにアタッチしています。PolicyのJsonは別ファイルで分けています。
Roleに環境名をつけて、それをサービスアカウントに紐づけています。
external_dns.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets"
],
"Resource": [
"arn:aws:route53:::hostedzone/*"
]
},
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:ListResourceRecordSets"
],
"Resource": [
"*"
]
}
]
}
Kubernetes側で用意するもの
external_dns.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.10.2
args:
- --source=ingress
- --domain-filter=ホストゾーンの名前 # 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
- --aws-zone-type=public
- --registry=txt
- --txt-owner-id=ホストゾーンのID
securityContext:
fsGroup: 65534 # For ExternalDNS to be able to read Kubernetes and AWS token files
serviceAccountName:で、TFで作成したサービスアカウントの名前を記載しています。
適用の順序
今回の設計構成上、ちょっと面倒な感じですが一応書きます。
1.terraform applyする
2.external-dns.yamlをデプロイする
3.Ingressを削除し、もう一度デプロイする
4.external-dnsがこんなログを出していて、Route53上でレコードが登録されていたら成功
kubectl logs "external-dns Podの名前" -f
~中略~
time="hogehoge" level=info msg="Applying provider record filter for domains: [”ホストゾーンの名前"]"
time="hogehoge" level=info msg="Desired change: CREATE "使用したいレコードの名前" A [Id: /hostedzone/"ホストゾーンのID"]"
time="hogehoge" level=info msg="Desired change: CREATE "使用したいレコードの名前" TXT [Id: /hostedzone/"ホストゾーンのID"]"
time="hogehoge" level=info msg="2 record(s) in zone ”ホストゾーンの名前". [Id: /hostedzone/"ホストゾーンのID"] were successfully updated"
curl https://"使用したいレコードの名前
で期待通りの反応が返ってくるか確認
備考
遭遇したエラーと解決についても記載しておきます。
external-dns Podが正常に立ち上がらず、Restartを繰り返していました。
kubectl get pod | grep dns
external-dns-hogehoge 0/1 CrashLoopBackOff 38 3h32m
kubectl logs external-dns-hogehoge -f
~中略~
time="hogehoge" level=fatal msg="failed to sync *v1.Ingress: context deadline exceeded"
以下参考にして、サービスアカウントとClusterRoleBindingが問題ありそうだなと考えました。
https://github.com/kubernetes-sigs/external-dns/issues/2407
https://stackoverflow.com/questions/71407994/kubernetes-error-context-deadline-exceeded
なので、両方が同じ名前空間にあることを修正、確認しました。
resource "kubernetes_service_account" "external_dns" {
metadata {
name = "external-dns"
namespace = "default" // ココ
annotations = {
"eks.amazonaws.com/role-arn" : "${aws_iam_role.external_dns_role.arn}"
}
}
}
resource "kubernetes_cluster_role_binding" "external_dns_viewer" {
metadata {
name = "${var.env}-external-dns-viewer"
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "ClusterRole"
name = kubernetes_cluster_role.external_dns.metadata[0].name
}
subject {
kind = "ServiceAccount"
name = kubernetes_service_account.external_dns.metadata[0].name
namespace = "default" // ココ
}
}
後は、--domain-filterを、ホストゾーンの名前ではなく、使用したいレコードの名前で記載していたので、修正しました。
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.10.2
args:
- --source=ingress
- --domain-filter=ホストゾーンの名前 // ココ
- --provider=aws
- --policy=upsert-only
- --aws-zone-type=public
- --registry=txt
- --txt-owner-id=ホストゾーンのID
securityContext:
fsGroup: 65534
すると解決しました。
Reference
・https://aws.amazon.com/jp/premiumsupport/knowledge-center/eks-set-up-externaldns/
・https://github.com/kubernetes-sigs/external-dns
・https://qiita.com/guile/items/d41399f579d1a62634c0
・https://developer.mamezou-tech.com/containers/k8s/tutorial/ingress/external-dns/
・https://tech.polyconseil.fr/external-dns-helm-terraform.html
・https://febc-yamamoto.hatenablog.jp/entry/2018/12/28/172830
・社内有識者
参考にさせていただきました。ありがとうございます。