ArgoCDをAWS EKS上で構築する

GitOpsがやりたくてArgoCDを導入しようと思ったんだけど,そもそもArgoCD自体はみんなどこで動かしてるんです?

もちろんin clusterでも動くので,デプロイ対象のアプリケーションが乗っているKubernetesクラスタに構築しようと思いました.

が,ArgoCDのチュートリアルを試すという意味では,みんなport-forwardを使う例が多くて(確かに圧倒的に楽なんだけど),本番で使うことを前提に構築している例があまり見当たらない. ので,AWS EKS上に,外部(といっても社内とか)からアクセスできるArgoCDを,helmを使って構築する例を書き残しておく.

とりあえずインストールしてみる

helm chartはここにある.

github.com

  - name: argocd
    namespace: argo
    chart: argo/argo-cd
    version: 2.0.3

みたいなhelmfileをかけばインストールできた.ここまではすごく簡単.

公式のガイドに従ってCLIを入れておく.

argoproj.github.io

そして

$ argocd login my-argocd.h3poteto.dev

としたいのだが,もちろんインストールしたArgoCDは,まだどこにもingressを作っていないし,Route53の設定等もしていないので, my-argocd.h3poteto.dev の名前解決ができるわけがない.

証明書が必要になる

ArgoCDはhttpsとgRPCの通信を行う. このとき,当然ながらTLSで通信するように作られている.となると,TLSの終端を誰に任せるかという問題が出てくる.また,証明書を誰が発行し,どのように管理するかという問題も発生する.

ArgoCDは起動時のパラメータに --insecure を付けない限り,ArgoCDのサーバ自身がTLSの終端になってくれる.ただし,ここで用意されている証明書には,当然のことながら my-argocd.h3poteto.dev みたいなドメインは含まれていない.そのため単にArgoCDのサーバを起動し,そこに外からアクセスできるELBやALBを作成しても,証明書のドメイン不一致となり警告が表示される.

というわけで, my-argocd.h3poteto.devドメインで証明書を発行しつつ,それをArgoCDに使ってもらう必要がある.

目指す構成

というわけで以下のような構成を目指した.

NetworkLoadBalancerとNginxIngress,CertManagerを使ったArgoCDの構成図

Nginx Ingress Controller

github.com

helm-chartでは,ingressのリソースが用意されている.

github.com

これを利用するためには,server.ingress.enabled をtrueにし,その他必要な情報を入れる必要がある. ただ,annotationsはvaluesで上書きできるようになっているので,Nginx Ingress Controllerを使うには十分なtemplateになっている.

これと,後述するCertManagerを使うことで,任意のドメインの証明書をLet's Encryptから取得し,NginxにTLSの終端を任せることができる.

CertManager

github.com

Nginx Ingress Controllerで利用する証明書を管理してくれる.これのおかげで,俺が手動でLet's Encryptから証明書を取得して設定したり,更新したりする必要がなくなる.

Network Load Balancer

Nginx Ingress Controllerを作るだけでは,クラスタの外部からアクセスすることができない.

そのため,Nginx Ingress Controllerに外からアクセスできる口を作るために,Network Load Balancerを用意した. これについては,公式でもガイドが用意されているので,詳しくはこちらを参照してほしい.

aws.amazon.com

構築

Nginx Ingress Controller

まず,NLBで外部から疎通できるようにした,Nginx Ingress Controllerを作成する.

以下のガイドに従う.

aws.amazon.com

このガイドでは,Ingress Controllerを

$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/mandatory.yaml

として入れている.

今回俺はhelmで構築しているので,ここを

  - name: nginx-ingress
    namespace: ingress-nginx
    chart: stable/nginx-ingress
    version: 1.35.0

このようなhelmfileで代替する.

このときに一点注意することがある.ガイドでは,この後,

$ kubectl apply -f https://raw.githubusercontent.com/cornellanthony/nlb-nginxIngress-eks/master/nlb-service.yaml

として,

kind: Service
apiVersion: v1
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
  annotations:
    # by default the type is elb (classic load balancer).
    service.beta.kubernetes.io/aws-load-balancer-type: nlb
spec:
  # this setting is to make sure the source IP address is preserved.
  externalTrafficPolicy: Local
  type: LoadBalancer
  selector:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
  ports:
    - name: http
      port: 80
      targetPort: http
    - name: https
      port: 443
      targetPort: https

このようなyamlをapplyしている.ここでNLBが作成されるわけだが,実はこのService,helm chart内にすでに定義されている.

github.com

そのため,helm installでnginx ingress controllerを入れた後に,このyamlをapplyすると,Serviceが二重に定義されることになる. というわけで, nlb-service.yamlのapplyは不要である. 代わりに,

  - name: nginx-ingress
    namespace: ingress-nginx
    chart: stable/nginx-ingress
    version: 1.35.0
    values:
      - rbac:
          create: true
      - controller:
          publishService:
            enabled: true
          service:
            enabled: true
            type: LoadBalancer
            externalTrafficPolicy: Local
            targetPorts:
              http: http
              https: https
            annotations:
              service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "tcp"
              service.beta.kubernetes.io/aws-load-balancer-type: nlb

このようにして,helm chart内のServiceをNLBにしてしまう.

$ kubectl get pods -n ingress-nginx
NAME                                            READY   STATUS    RESTARTS   AGE
nginx-ingress-controller-cbf4bc7c9-7r9vm        1/1     Running   0          27h
nginx-ingress-default-backend-7db6cc5bf-hw8rr   1/1     Running   0          27h
$ kubectl get svc -n ingress-nginx
NAME                            TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
nginx-ingress-controller        LoadBalancer   172.20.65.116    <pending>     80:31385/TCP,443:30137/TCP   27h
nginx-ingress-default-backend   ClusterIP      172.20.252.236   <none>        80/TCP                       27h

これでNginx Ingress Controllerの準備はできた.

Cert Manager

インストール

もちろんhelmで入れる.

  - name: cert-manager
    namespace: kube-system
    chart: jetstack/cert-manager
    version: v0.14.1

これだけ.

Issuer

CertManagerで証明書を得るためには,手動でIssuerを立ててやる必要がある.

cert-manager.io

issuer.yaml を作る.

apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    # The ACME server URL
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    # ↑はstagingのURL.本番で使う場合は↓に書き換えること
    # server: https://acme-v02.api.letsencrypt.org/directory
    # Email address used for ACME registration
    email: hogehoge@example.com
    # Name of a secret used to store the ACME account private key
    privateKeySecretRef:
      name: my-argocd-tls
    solvers:
      - selector:
          dnsZones:
            - "my-argocd.h3poteto.dev"
        dns01:
          route53:
            region: ap-northeast-1
            hostedZoneID: ASDF0234512312
            # IRSAで認証させるので認証情報は書かない
$ kubectl apply -f issuer.yaml

solversにはdns01を使っている.ドメインの確認を,http-01でやるかdns-01でやるかは自由なのだが,dns-01の方が楽なので(更新とか)dns01にしている. DNS認証させるということは,当然Route53へのアクセス権が必要である. その場合,本来であればここにAWSのアクセスキー等を書くか,Roleの情報を書くのだが,

cert-manager.io

ここをIRSAで解決したいので,何も書かない. IRSAでの認証については次項で.

CertManagerからRoute53への認証

ここの認証はIAM Role for Service Accountに対応している.

github.com

そのため,Issuerの定義では認証情報を書かなかった.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "route53:GetChange",
            "Resource": "arn:aws:route53:::change/*"
        },
        {
            "Effect": "Allow",
            "Action": [
              "route53:ChangeResourceRecordSets",
              "route53:ListResourceRecordSets"
            ],
            "Resource": "arn:aws:route53:::hostedzone/*"
        },
        {
            "Effect": "Allow",
            "Action": "route53:ListHostedZonesByName",
            "Resource": "*"
        }
    ]
}
resource "aws_iam_policy" "cert_manager_policy" {
  name        = "cert-manager-policy"
  path        = "/"
  description = ""
  policy      = file("aws_iam_policies/cert_manager_policy.json")
}

こんなPolicyを作って,IRSAできるIAM Roleに付与する.

resource "aws_iam_role" "cert_manager_role" {
  name               = "cert-manager-role"
  path               = "/"
  assume_role_policy = data.template_file.irsa_assume_role_policy.rendered
}
resource "aws_iam_policy_attachment" "cert_manager_policy" {
  name = "cert-manager"

  roles = [
    aws_iam_role.cert_manager_role.name,
  ]

  policy_arn = aws_iam_policy.cert_manager_policy.arn
}

IRSAの詳しい設定については,こちらで書いているので参考にしてほしい.

h3poteto.hatenablog.com

そしたら,このIAM RoleをCertManagerのServiceAccountのAnnotationsに付与する.

  - name: cert-manager
    namespace: kube-system
    chart: jetstack/cert-manager
    version: v0.14.1
    values:
      - serviceAccount:
          create: true
          annotations:
            eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/cert-manager-role

これで,Issuerに定義したHostedZoneに,IRSAで付与されたIAM Roleの権限でアクセスできるようになる.

ArgoCD

Certificate

ArgoCDのhelm chartには,CertManager用のtemplateが用意されているので,こいつを有効化して,証明書を取得できるようにしてやる.

  - name: argocd
    namespace: argo
    chart: argo/argo-cd
    version: 2.0.3
    values:
      - server:
          certificate:
            domain: "my-argocd.h3poteto.dev"
            enabled: true
            issuer:
              kind: ClusterIssuer
              name: letsencrypt-staging

これでCertificateリソースができる.

$ kubectl get cert -n argo
NAME            READY   SECRET          AGE
argocd-server   True    argocd-secret   5h22m
$ kubectl describe cert argocd-server -n argo
Name:         argocd-server
Namespace:    argo
API Version:  cert-manager.io/v1alpha2
Kind:         Certificate
...
Events:
  Type    Reason          Age                  From          Message
  ----    ------          ----                 ----          -------
  Normal  GeneratedKey    110s (x2 over 112s)  cert-manager  Generated a new private key
  Normal  Requested       110s (x2 over 112s)  cert-manager  Created new CertificateRequest resource "argocd-server-1787752625"
  Normal  PrivateKeyLost  110s                 cert-manager  Lost private key for CertificateRequest "argocd-server-1787752625", deleting old resource
  Normal  Issued          4s                   cert-manager  Certificate issued successfully

Certificate issued successfully になってれば大丈夫.

Ingress

上記で作成されたCertificateを使ってNginx Ingressを作る.

  - name: argocd
    namespace: argo
    chart: argo/argo-cd
    version: 2.0.3
    values:
      - server:
          certificate:
            domain: "my-argocd.h3poteto.dev"
            enabled: true
            issuer:
              kind: ClusterIssuer
              name: letsencrypt-staging
          extraArgs:
            # 証明書はCertManagerに取得させnginx-ingress-controllerで終端させるので,insecureで良い
            - --insecure
          ingress:
            enabled: true
            hosts:
              - my-argocd.h3poteto.dev
            annotations:
              cert-manager.io/cluster-issuer: letsencrypt-staging
              cert-manager.io/issuer-kind: ClusterIssuer
              kubernetes.io/ingress.class: nginx
              kubernetes.io/tls-acme: "true"
              nginx.ingress.kubernetes.io/ssl-passthrough: "true"
            tls:
              - hosts:
                - my-argocd.h3poteto.dev
                # secretNameはCertificate側で決め打ちされているので変更しないこと
                # https://github.com/argoproj/argo-helm/blob/master/charts/argo-cd/templates/argocd-server/certificate.yaml#L27
                secretName: argocd-secret

これでingressが作成される.

$ kubectl describe ingress argocd-server -n arg
Name:             argocd-server
Namespace:        argo
Address:
Default backend:  default-http-backend:80 (<none>)
TLS:
  argocd-secret terminates my-argocd.h3poteto.dev
Rules:
  Host              Path  Backends
  ----              ----  --------
  my-argocd.h3poteto.dev
                    /   argocd-server:80 (10.5.29.125:8080)
Annotations:
  cert-manager.io/cluster-issuer:               letsencrypt-staging
  cert-manager.io/issuer-kind:                  ClusterIssuer
  kubernetes.io/ingress.class:                  nginx
  kubernetes.io/tls-acme:                       true
  nginx.ingress.kubernetes.io/ssl-passthrough:  true
Events:                                         <none>

ちゃんと,my-argocd.h3poteto.dev の証明書になっている.

これで構築は終わり.

ログインしてみる

$ argocd login my-argocd.h3poteto.dev --grpc-we
Username: admin
Password:
'admin' logged in successfully
Context 'my-argocd.h3poteto.dev updated

いけたよ!!!

もちろん,WebUIも https://my-argocd.h3poteto.dev から確認できました.

ただしNLBを使っているのでSecurityGroupに注意する必要がある. NLB自体にSGの設定はできず,アクセス元のIPを維持したままリクエストを流すので,EKSのノード側のSGでアクセス元のIPを許可してやる必要がある.

社内からアクセスするのであれば,オフィスのIP等からのアクセスを許可する必要がある. そいういう制約上,PrivateSubnetに置かれたNodeでこれを運用することはできないんじゃないかなぁ.

もちろん,Nginx Ingressまで疎通すればいいので,IngressだけPublicSubnetに置いてあれば良さそうではあるが.ここは試してないのでわからない.

試行錯誤の履歴

ここからは,この構成に至るまでに試して,撃沈したことについて書いていく.

ServiceはClusterIP or NodePortにしてALB Ingress Controllerを使ってみる

ちなみにここまで読んできて,「ALB使ってACM設定すれば良くない?」と思った方.

ALBs and Classic ELBs don't fully support HTTP2/gRPC, which is used by the argocd CLI. Thus, when using an AWS load balancer, either Classic ELB in passthrough mode is needed, or NLBs.

argoproj.github.io

なんですよ. ちなみに,実際にServiceをNodePortにして,ALB Ingress Controllerを使ってALBでIngressを構築してみたのだが,エラーが出て通信できなかったのであった. むしろこれをALBで疎通する方法があったら教えてほしい.

というわけで早々にALBについては諦めました.

ServiceをNLBにしてみる

最初,ArgoCDが用意しているServiceを,そのまま type: LoadBalancerにして,NLBにしちゃえばいいじゃん!と思ったのだが,もちろんTLSの終端はArgoサーバになるわけで, my-argocd.h3poteto.dev なんて許可されてないわけで. 当然証明書エラーが表示された.

また,type: LoadBalancer でNLBを使う場合に,service.beta.kubernetes.io/aws-load-balancer-ssl-cert が使えるものかと思っていたんだけど,今の手元のクラスタ(v1.14.9-eks)ではこれを指定してもNLBに証明書をが設定されることはなかった. まぁここを手動でACM指定しても,やっぱり証明書のエラーが出ることには変わりなかったのだが.

まとめ

ひとまずArgoCDをEKS上で使える状態にすることはできた.

しかし本当にみんなArgoCDはどこに置いてデプロイしているんだろうか……. CDをやるコンポーネントである以上,あんまりデプロイ対象のクラスタの上に乗せたくはない気がするのだが…….