ハウテレビジョンブログ

『外資就活ドットコム』『Liiga』『Mond』を開発している株式会社ハウテレビジョンのブログです。

AWS Load Balancer ControllerとExternalDNSを利用しています

こんにちは、SREチームの小川です。
「夏への扉」を読みました。過去・現在・未来の時間軸が最後にかち合った時には感動を覚えました。月並みですが、さすがSFの金字塔ですね。
今回は「外資就活への扉」である、ALB・Route53をSREがどのように管理しているか紹介したいと思います!

はじめに

以前よりSREチームが「外資就活ドットコム」のインフラをEC2中心のものから、コンテナ中心のEKSへと移行したお話をしてきました。
今回はKubernetesを導入した際に、トラフィックの流入元であるALBをAWS Load Balancer ControllerExternalDNSを利用して管理を行うようにしたお話しをします。

EKSのサービスをALBを利用して公開する際に、アプリケーションコンテナをNodePortで公開し、ALBがNodeGroupの特定のNodePortにリバースプロキシするなどがあります。
一方AWS Load Balancer Controllerを利用すると、Ingressリソースを元に作成されたALBは、アプリケーションコンテナのClusterIPを解決してリバースプロキシすることができるようになります。
このように不用意にノードのポートをクラスタ外に公開する必要がなくなるため、AWS Load Balancer Controllerを採用しました。

またExternalDNSを併せて利用することで、特定のIngressリソースから作成されたALBのルールを元に、自動でRoute53にレコードを登録することができます。
これにより、Ingressのrulesパラメータを設定するだけで、公開したサービスを名前解決することができるようになります。

AWS Load Balancer Controllerのデプロイ

AWS Load Balancer Controllerを利用してALBを作成するためには

  1. SecurityGroup
  2. IAMロール
  3. aws-load-balancer-controller
  4. ingress

の定義をする必要があります。

このうち、1・2はTerraformで構成管理し、3・4はK8sのマニフェストで管理しています。
3・4を作成する際には1・2のARNなどを埋め込んで読み込む形になっています。

SG, IAMロール

SGは適宜インターネットなどに公開したいポートをオープンにしたものを定義します。公式ドキュメントに酷似した定義を利用しているため割愛します。

IAMロールは以下の通り定義します。
Controllerの利用するServiceAccountとIAMロールをIRSAで関連づけることを目的としたリソースとなっています。
AssumeRoleのポリシーをFederatedにすることで、EKSのOIDC ID プロバイダとIAMロールを関連づけています。

resource "aws_iam_openid_connect_provider" "eks_oidc" {
  url = var.eks_oidc

  client_id_list = [
    "sts.amazonaws.com"
  ]
  thumbprint_list = [data.tls_certificate.eks_oidc.certificates[0].sha1_fingerprint]
}

data "http" "alb_management" {
  url = format(
    "https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/%s/docs/install/iam_policy.json",
    var.alb_version
  )
}

data "aws_iam_policy_document" "alb_management" {
  statement {
    effect = "Allow"
    principals {
      identifiers = [aws_iam_openid_connect_provider.eks_oidc.arn]
      type        = "Federated"
    }
    actions = ["sts:AssumeRoleWithWebIdentity"]
    condition {
      test     = "StringEquals"
      variable = format("%s:sub", replace(aws_iam_openid_connect_provider.eks_oidc.url, "https://", ""))
      values = [
        "system:serviceaccount:kube-system:aws-load-balancer-controller"
      ]
    }
  }
}

resource "aws_iam_role" "alb_management" {
  name               = format("%s-%s-alb-management", var.env, var.project)
  assume_role_policy = data.aws_iam_policy_document.alb_management.json
}

resource "aws_iam_role_policy_attachment" "alb_management" {
  policy_arn = aws_iam_policy.alb_management.arn
  role       = aws_iam_role.alb_management.name
}

resource "aws_iam_policy" "alb_management" {
  name   = format("%s-%s-alb-management", var.env, var.project)
  policy = data.http.alb_management.body
}

aws_iam_openid_connect_providerのurlに渡す値は下記のように取得可能です。

output "eks_oidc" {
  value = aws_eks_cluster.cluster.identity[0].oidc[0].issuer
}

aws-load-balancer-controller

外資就活ドットコムではK8sリソースのデプロイ管理にArgo CDを利用しています。
aws-load-balancer-controllerについてもArgoCDを利用してデプロイしています。

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: aws-load-balancer-controller
spec:
  project: gaishishukatsu
  source:
    repoURL: マニフェストのGitHubリポジトリURL
    targetRevision: main
    path: alb/overlays/x
  destination:
    server: https://kubernetes.default.svc
    namespace: kube-system
  syncPolicy:
    automated:
      prune: true
      allowEmpty: true
    retry:
      limit: 0
      backoff:
        duration: 10s
        factor: 1
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cert-manager
spec:
  project: gaishishukatsu
  source:
    repoURL: マニフェストのGitHubリポジトリURL
    targetRevision: main
    path: cert_manager/overlays/x
  destination:
    server: https://kubernetes.default.svc
    namespace: cert-manager
  syncPolicy:
    automated:
      prune: true
      allowEmpty: true
    retry:
      limit: 0
      backoff:
        duration: 10s
        factor: 1

ArgoCDがrepoURLからポーリングするaws-load-balancer-controllerのマニフェストは、Kustomizeを利用して下記のように定義しています。
Terraformで作成したIAMロールのARNと、EKSの所属するVPCのIDをそれぞれ埋め込みます。
ここまでで作成したリソースがデプロイされると、EKSのIngressリソースを作成するだけでALB経由でサービスを公開する準備が整います。

# base/kustomization.yaml
bases:
  - https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/${controllerのversion}/docs/install/${controllerのversion}full.yaml

# overlays/x/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: aws-load-balancer-controller
  namespace: kube-system
spec:
  template:
    spec:
      containers:
        - name: controller
          args:
            - --cluster-name=cluster
            - --aws-region=ap-northeast-1
            - --aws-vpc-id=EKSの所属するVPCのID
          env:
            - name: AWS_DEFAULT_REGION
              value: ap-northeast-1
            - name: AWS_ROLE_ARN
              value: Terraformで作成したIAMロールのARN
            - name: AWS_WEB_IDENTITY_TOKEN_FILE
              value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token

# overlays/x/service-account-patch.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: aws-load-balancer-controller
  namespace: kube-system
  annotations:
    eks.amazonaws.com/role-arn: Terraformで作成したIAMロールのARN

# overlays/x/kustomization.yaml
resources:
  - ../../base
patchesStrategicMerge:
  - service-account-patch.yaml
  - deployment-patch.yaml

ingress

下記のようなIngressを作成することで、ALBが80,443番で公開され、K8sのapp-svcというClusterIPへとリバースプロキシすることができます。
他にもalb.ingress.kubernetes.io/load-balancer-attributesというアノテーションを設定することで、別途Terraformで作成したS3へALBのログを保存することや、alb.ingress.kubernetes.io/wafv2-acl-arnでWAFを設定することもできます。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  namespace: app
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/group.name: "public"
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
    alb.ingress.kubernetes.io/security-groups: Terraformで作成したSGのID
    alb.ingress.kubernetes.io/certificate-arn: HTTPSの場合にはTerraformで作成したACMのARN
    alb.ingress.kubernetes.io/actions.app: "{\"type\":\"forward\",\"forwardConfig\":{\"targetGroups\":[{\"serviceName\":\"app-svc\",\"servicePort\":80}]}}"
spec:
  rules:
    - host: app.gaishishukatsu.com
      http:
        paths:
          - backend:
              service:
                name: app
                port:
                  name: use-annotation
            pathType: ImplementationSpecific
---
apiVersion: v1
kind: Service
metadata:
  name: app-svc
  annotations:
    alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
    alb.ingress.kubernetes.io/healthcheck-port: traffic-port
    alb.ingress.kubernetes.io/healthcheck-path: /healthz
    alb.ingress.kubernetes.io/healthy-threshold-count: '5'
    alb.ingress.kubernetes.io/unhealthy-threshold-count: '2'
    alb.ingress.kubernetes.io/healthcheck-interval-seconds: '300'
    alb.ingress.kubernetes.io/healthcheck-timeout-seconds: '5'
    alb.ingress.kubernetes.io/success-codes: '200'
spec:
  type: ClusterIP
  ports:
    - name: "http"
      protocol: "TCP"
      port: 80
      targetPort: 80
  selector:
    app: app

ExternalDNS

ここまででALBを作成することができました。
ホストベース・パスベースのルールを増やしたい場合には、Ingressを修正することでALBのルーティングを操作することができます。

ここからはExternalDNSを利用して、作成したIngressからRoute53のレコードを登録するためのマニフェストの管理を紹介していきます。

IAMロール

本来はIRSAを利用したいですが、EKSノードにRoute53を操作可能なIAMロールをアタッチしています。

data "aws_iam_policy_document" "eks_node_route53_policy" {
  statement {
    effect = "Allow"
    actions = [
      "route53:ChangeResourceRecordSets",
      "route53:ListHostedZones",
      "route53:ListResourceRecordSets"
    ]
    resources = ["*"]
  }
}

external-dns

ArgoCDとKustomizeを利用してデプロイしています。
--annotation-filterで先ほど作成したIngressのアノテーションalb.ingress.kubernetes.io/group.nameを指定することで、特定のALBのルールを管理することができます。
policyは3段階あり、upsert-onlyではレコードの新規作成と上書きができ、レコードの削除は行わないようになっています。開発環境などではsyncにすることで削除も行うことが可能となるため、検証がスムーズに進みます。Kustomizeを利用することでこの辺の環境差分を管理することが可能です。

このマニフェストのデプロイに成功することで、gaishishukatsu.comというホストゾーンに

レコード Type
app.gaishishukatsu.com A (エイリアス) ALBのDNS名

というレコードを作成することができます。これで本エントリーの目的である、ALBの公開とDNSでの名前解決が達成されました。

# base/deployment.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:tag
# overlays/x/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
spec:
  template:
    spec:
      containers:
        - name: external-dns
          image: k8s.gcr.io/external-dns/external-dns:tag
          args:
            - --domain-filter=gaishishukatsu.com
            - --annotation-filter=alb.ingress.kubernetes.io/group.name in (public)
            - --provider=aws
            - --policy=upsert-only
            - --source=service
            - --source=ingress

また、外資就活ドットコムはインターネットに公開していますが、バックオフィス向けのサービスなどについてはVPNからのアクセスしか許可しない体制をとっています。
aws-load-balancer-controllerexternal-dnsを利用している場合には、Ingressを追加しalb.ingress.kubernetes.io/group.name: "vpn"など、別のグループを指定することで複数のALBを管理することができます。
そしてexternal-dns側ではannotation-filterを別途作成したIngressのアノテーションにし、argsに--aws-zone-type=privateを追加することで、プライベートなDNSレコードを利用した名前解決により、閉ざされたネットワークでサービスを公開することができます。

さいごに

ここまでが外資就活ドットコムのトラフィックの入り口を、SREチームでどのように管理しているか紹介でした。
この構成を採用したことで、マニフェストの管理のみでL7のルーティングを管理することが実現できたため、運用の負荷を非常に低く抑えることができています。
また紹介した通り、ALBを複数管理したり、ルーティングのルールを細かく設定することもできるため、新規にサービスを公開するための実装コストも低いです。

他にもexternal-secrets + Secrets Mangerであったり、ArgoCD + ArgoCD Notificationsなど、K8sのエコシステムを活用した取り組みを行なっています。
こういった挑戦できる機会が外資就活にはあります。「これを使ってもっと良くしたい」とか「あそこはこうしなきゃ...」など改善していきたいと考えています。一緒に取り組んでくれるエンジニアの方、募集しております!