LoginSignup
14
10

More than 3 years have passed since last update.

GitOps導入指南書

Last updated at Posted at 2020-10-07

背景

先日、私が携わっているプロダクトでGitOpsを導入しました。
一言でGitOpsといっても、ただGitのプッシュをトリガーにデプロイするだけではなく、マニフェストの自動更新フローの構築Secretの扱いの変更などの作業が発生し、一つ一つ調べながらやったので、これらの手順をまとめることにしました。
ちょっとした工夫点もあります。

また、CD(Continuous Delivery)ツールには ArgoCD を、Secretの暗号化には SealedSecrets を採用しました。
この記事では、これらを用いて、以下の画像のようなデプロイ戦略を実現するためのGitOpsの導入手順をまとめていますが、基本的な流れは他のツールを導入する場合でも有用だと思います。GitOpsを導入する上で必要な手順を全て書いているのでまぁまぁ長いですが、ご容赦ください。

Screen Shot 2020-10-07 at 13.24.06.png

※記事中に出てくる環境は、個人のものであり、弊社のプロダクトとは関係ありません。

環境

使用している各ツール等のバージョンは以下の通りです。

  • GKE: 1.16.13-gke.401
  • Argo CD: v1.7.7
  • argocd: v1.7.4
  • Sealed Secrets: v0.12.5
  • kubeseal: v0.12.5

また、サンプルは以下のリポジトリにコミットしてあります。

GitOpsでは、アプリケーションとマニフェストのリポジトリを分離することが望ましいです。

デプロイ戦略

大まかに順序をまとめると、以下のようになります。

  1. Appリポジトリにプッシュ
  2. CloudBuildのビルドが実行され、Manifestリポジトリにプッシュされる
  3. ArgoCDによって最新のマニフェストが適用される = デプロイ

それぞれのリポジトリとブランチの対応表です。

トリガーとなる
Appリポジトリのブランチ
CloudBuildによりでプッシュされる
Manifestリポジトリのブランチ
デプロイ環境
main dev Dev
staging staging Staging
production production Production

環境構築

必要な作業を順に解説していきます。
リンクを載せておくので、一部だけ必要な方はここから飛んでください。

  1. GCPでの作業
  2. SealedSecretの導入
  3. Manifestリポジトリでの作業
  4. Appリポジトリでの作業
  5. ArgoCDのセットアップ

GCPでの作業

CloudBuildに、GKEへのアクセス権を付与

  1. Google Cloud Consoleで、Cloud Buildの設定ページを開きます。
  2. Kubernetes Engine Developerのステータスを有効にします。

Screen Shot 2020-10-07 at 16.22.png

GitHubのSSH認証鍵をSecretManagerに保存する

  1. workingdirディレクトリを作成し、そこに新しいGitHub SSH認証鍵を作成します。 (ディレクトリ名は任意)
  2. キーの保存先ファイル名の入力を求められたら、id_githubと入力します。
    ※ パスフレーズは空のままにしてください。
  3. Google Cloud Consoleで、セキュリティ > シークレット マネージャーを開きます。
  4. +CREATE SECRETをクリック
    ・名前はid_github
    ・先ほど作成したSSH認証鍵を選択
    シークレットを作成をクリックします。
$ mkdir -p ~/workingdir
$ cd ~/workingdir
$ ssh-keygen -t rsa -b 4096 -C ${GITHUB_EMAIL}

Screen Shot 2020-10-07 at 3.15.42.png

限定公開 GitHub リポジトリへのアクセスもご参考ください。

GitHubに公開SSH認証鍵を追加する

  1. ManifestリポジトリのSettings > Deploy KeysAdd deploy keyをクリックする。
  2. タイトルを入力し、先ほど作成した公開SSH認証鍵を貼り付ける。
  3. Allow write accesにチェックをいれて、キーの追加をクリックする。
    ※ この認証鍵を使用して、Manifestリポジトリにプッシュするので、必ずチェックを入れる
  4. ローカルのSSH認証鍵は不要なので、削除する。

Screen Shot 2020-10-07 at 16.49.27.png

CloudBuildのサービスアカウントに、SecretManagerへのアクセス権を付与する

  1. Google Cloud ConsoleでIAMと管理 > IAMを開く。
  2. 表から、末尾が@cloudbuild.gserviceaccount.comのメンバーを見つけて、編集アイコンをクリックする。
  3. Secret Managerのシークレット アクセサーのロールを追加し、保存をクリックする。

Screen Shot 2020-10-07 at 16.53.12.png

SealedSecretの導入

GitOpsではマニフェストをGit管理するので、SecretもGitに上げる必要があります。
しかしSecretはbase64エンコードされているだけなので、そのままコミットしてしまっては、そのリポジトリにアクセスできるユーザーであれば、誰でもデコードして見ることができてしまいます。

そこでSealedSecretというツールを用いて暗号化することにしました。
鍵はクラスターで管理されているので、暗号化された状態でコミットすることができます。

kubesealのインストール

まず最初に、kubesealをインストールする必要があります。
手順は以下のリンクにも載っているので、よかったらご覧ください。
sealed-secrets - Releases

ここでは、Macでの手順をご紹介します。

$ # kubesealのインストール
$ brew install kubeseal

$ # バージョンの確認
$ kubeseal --version
kubeseal version: v0.12.5

SealedSecret関連のオブジェクトを生成

SealedSecretのCustomResourceDefinitions(CRD)をインストールします。

$ # sealed-secrets-controllerに関連するオブジェクトを生成
$ kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.12.5/controller.yaml

kubesealのヘルプを見ると、namespaceはkube-systemのようです。

$ kubeseal -h
...
--controller-name string           Name of sealed-secrets controller. (default "sealed-secrets-controller")
--controller-namespace string      Namespace of sealed-secrets controller. (default "kube-system")
...

必要なオブジェクトが生成されているか確認します。

$ # Podの確認
$ kubectl get pods -n kube-system
NAME                                         READY   STATUS    RESTARTS   AGE
...
sealed-secrets-controller-867447b788-7sdhq   1/1     Running   0          8m58s
...

$ # Secretの確認
$ kubectl get secrets -n kube-system
NAME                                             TYPE                                  DATA   AGE
...
sealed-secrets-controller-token-xplmz            kubernetes.io/service-account-token   3      23h
sealed-secrets-key4wlwz                          kubernetes.io/tls                     2      9m56s
...

証明書の取得

Secretを暗号化するために必要な証明書を取得します。
この証明書は、後にManifestリポジトリでの作業で使用するので、予め移動しておいてください。
また、証明書はコミットする必要がないので、.gitignoreに追加するなどしてコミットできないようにすることをお勧めします。

$ kubeseal --fetch-cert > cert.pem

Manifestリポジトリでの作業

自動更新するyamlのテンプレートを作成

GitOpsでは、Appリポジトリへのプッシュをトリガーに、yamlを自動編集します。
そこで、対象となるyamlのテンプレートを用意します。

ここでは1ファイルだけ紹介します。
deployment.yaml.tpl

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gitops-sample-server
spec:
  replicas: 2
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: gitops-sample-server
  template:
    metadata:
      labels:
        app: gitops-sample-server
    spec:
      containers:
        - name: gitops-sample-server
          image: gcr.io/PROJECT_ID/app:COMMIT_SHA
          command: ["/gitops-sample-server"]
          imagePullPolicy: Always
          resources:
            limits:
              cpu: 500m
              memory: 1000Mi
            requests:
              cpu: 200m
              memory: 500Mi
          ports:
            - containerPort: 8080
          envFrom:
            - secretRef:
                name: env-secret
          readinessProbe:
            exec:
              command: ["/bin/grpc_health_probe", "-addr=:8080"]
            initialDelaySeconds: 1
          livenessProbe:
            exec:
              command: ["/bin/grpc_health_probe", "-addr=:8080"]
            initialDelaySeconds: 1
        - name: envoy-proxy
          image: envoyproxy/envoy-alpine:v1.14.4
          command: ["/bin/sh", "-c", "/usr/local/bin/envoy -c /etc/envoy/envoy.yaml"]
          resources:
            limits:
              cpu: 500m
              memory: 500Mi
            requests:
              cpu: 100m
              memory: 100Mi
          ports:
            - name: app
              containerPort: 10000
            - name: envoy-admin
              containerPort: 8001
          volumeMounts:
            - name: envoy-volume
              mountPath: /etc/envoy
          readinessProbe:
            httpGet:
              scheme: HTTP
              path: /healthz
              httpHeaders:
                - name: x-envoy-livenessprobe
                  value: healthz
              port: 10000
            initialDelaySeconds: 3
          livenessProbe:
            httpGet:
              scheme: HTTP
              path: /healthz
              httpHeaders:
                - name: x-envoy-livenessprobe
                  value: healthz
              port: 10000
            initialDelaySeconds: 10
      volumes:
        - name: envoy-volume
          configMap:
            name: envoy-gitops-sample-server-config

Appリポジトリと連携しているCloudBuildで、このテンプレートからdeployment.yamlが生成されるのですが、その時に、イメージのパスを自動で更新します。
gcr.io/PROJECT_ID/app:COMMIT_SHAPROJECT_ID$PROJECT_IDで、COMMIT_SHA$COMMIT_SHAで更新します。

  • $PROJECT_ID: GCPのプロジェクトID
  • $COMMIT_SHA: トリガーとなったコミットのコミットID

SealedSecretのyamlを生成する

例として、以下のSecretのyamlを使用します。

secret.yaml
apiVersion: v1
data:
  MYSQL_HOST: bG9jYWxob3N0
  MYSQL_PASSWORD: cGFzc3dvcmQ=
  MYSQL_USER: cm9vdA==
kind: Secret
metadata:
  creationTimestamp: null
  name: sample-secret
  namespace: default

このSecretからSealedSecretのyamlを生成します。
cert.pemは先ほど生成したものを使用します。

$ kubeseal --format=yaml --cert=cert.pem \
< secret.yaml \
> sealed-secret.yaml

生成されたSealedSecretのyamlはこちら。

sealed-secret.yaml
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: sample-secret
  namespace: default
spec:
  encryptedData:
    MYSQL_HOST: AgBPhntyTvUOYPtjJrdZk0fbVyljZFQExAVKAHYjE0rtTNsqaNXygninGs5CPPALfIf/BgmONK35xepHnH7kqc+WNm2nt4MueAMHAtrHQ/8SCkzXbQGLuiyUUds2Q2xVu/rBAVwGfWaKkwi9Db8Y7u6UOLE2wjgk2hpfqnf0KCgR4+u4q936tszBlqQrzk6+ZCqn7oPkOHI3iHJIi4Myo+Xh51Cx0ItEcq18huAK04tTgPoNDJgjLZZnWt83eOJcdqs6mNuI2khsi+/sXizA/naRRDJjiZat5//v/e73ejPRYLJkLCxmbiqBkZUmzBcTgj9PNvOPd+Be/VlKV0I4oO23e4f4CTp+xsq/hxje1qo38zL5UEalbYvtWVCAQCHyA5zcacpD7Sbl0hDSn4JCmXgktrtmgp4iADw2dGDqj7yGyvgQt18VUyWwqIzdhdih9a9LLvqK+gEf9pHs6p6j+rSlvIhKWM9k+vp44h9GLv5ViXB0jFD6Ig5kuyrne6OPceJgj1kuoGQbGdwCvd6thjNytWwBuPujKo5AK4VepLUUh7q7zZwqBv0yXMwSkmFDpJMTmMuy6W32HZvmJw1m/ajaAqlltDKl2pbyDClRLJ/K1n9QDiOEgIdLX3t9C5tJL1G9b1bMTpTfqqYcQbTQnDBEJJ7du4eYQRoT9EP4vI0eyab8beuvBvwIdPvsPWwY+uf7TzuyX0ve6/8=
    MYSQL_PASSWORD: AgAV4O7rOmyqL2JtPZ/XtUIMNW5GPgSMhc8zU0zU7LyaNfu5PPFfZrqmyvV4u0XjiNL0nH0rxCEnbm8ZlYtDQCDTMAZMgQkDSWVq7RGnL0txkE3mtsgzctNvIk54lvoVi7fzsBUHXLWsXu58NgQsGhc/VG25zMIq2ycEKd0nEFoa3EK1EoNsb3gKNS4qGM7hqkS5y7ufOcUR5mVuvXlihrfn9nGcucFsRsMbxsTYWUauM8x0wqxrnw7o74EkA0Gyhu2FI43n4mleubC8pzOEYvurfyfd1UmS7H6j/Cm4VuPtiX/uynvKyb2iVJX3vkeyKnmvhP6AcegfI1UYSD1nLg6taDOUFDdScCRsdI97lF0ehYauinLLp94rRQIXOFWYaSqq8VNbQzApXTDAgSzjazdx17p1IVrspEc7xCzD1Dcn3RiiiSpGnYEbG4rF4pFu8AcKH5j/MSydKN4iJgQ2utOgJ7pUXNeShgRrNf/BI67uus44fgeIXnkG8+LG7/WGhsjnYTRsECZD4g+q2vB3tYeBSFQ7ojUFVN5ChsZgTS+uziIp3jzxu3sWfSt4puGx+BlFp+dxv0Zp1YrRKuje+dBWlCkrf9Sa16SR7DCqKdX+1G0W6rwYYEyXwRW9Y0s6lUZwfpd0kOVcYZABohfH3RkWmxgKjF76xFlyFfHILVtdGJbe1nz1144wAMQeE3jvk/ypeAGy+I8lbg==
    MYSQL_USER: AgDjq7qBJR1DDOWapYF4AhtQi1sQgeglSRhu6tZ+JHPTPPdNFBV+zQk3xzKKRldZeeIlXQ5ZquEdc3lMWYKqg5xvL2e21YhgPv4oWADDvsWxWdklZVLDl0SGPQT64v6K+lPVR5Gia6zIpocvv4v3GbMcCXFkFK/b5HeSU6FcxrAhrQ+vdxSWKfkezO8vrS1wJ3MYLgLSwtiRDsibjpYwVTg0sLDcpTlrDk95IEnmcY8bE6qGJ3BO8mUBPhsBnk5yLJO2MZeOofA9IbpUDIpUJSFcaaXc+PK3C+vKxxJ2Ia94Y07UshUOrl72dTrp9aJv1mdkVfzZ7EHQpHh+2k7b2U1nMFVFwK2QV193vJVSQDM21IzIttE4QOxllv0ei48FVMVw3pJ0ABJMKiYta6KAZvKQ5EJkEIVVHtVqRH6plpBgU72dAPVuJAmdlAQ3I844lj1AKwfydylFRJP/olkHegB5Lu3ScwblicPZt4Q+f+IV79YefKR+Bf8zwLB5bd5uFOVx5Jj3dkiSfTN9P60FE6uA1axxyECafqwcgEmxafODtRpP+94rbAUboWCCXTAqjL+KyclCkXg0lKKLDHBmIzhsV3+eDwlApHWfvs1/x2dyW17Rjs7Nqb6vi/xX+YXT0d67TzNEUFfYFABRN9ABN/YSCWsVxIJXvMulg4RpsGD4g5E4GG5gwj3Jjjfr0yEtsu9fS5Zn
  template:
    metadata:
      creationTimestamp: null
      name: sample-secret
      namespace: default

ご覧の通り、暗号化されるのは、ファイル全体ではなくSecretの各キーに対する値です。
Gitにはこのファイルをコミットします。
Secretのyamlはコミットしないでください。

Appリポジトリでの作業

cloudbuild.yamlの定義

Appリポジトリの特定のブランチのプッシュをトリガーに、CloudBuildを実行します。
各ステップは以下のように定義します。

cloudbuild.yaml
timeout: 900s
steps:
  - id: build and push app docker image
    name: gcr.io/kaniko-project/executor:v1.0.0
    args:
      - --destination=gcr.io/$PROJECT_ID/app:$COMMIT_SHA
      - --dockerfile=Dockerfile
      - --cache=true
      - --cache-ttl=6h
    waitFor: ['-']
  - id: build and push migration docker image
    name: gcr.io/kaniko-project/executor:v1.0.0
    args:
      - --destination=gcr.io/$PROJECT_ID/migration:$COMMIT_SHA
      - --dockerfile=Dockerfile.migration
      - --cache=true
      - --cache-ttl=6h
    waitFor: ['-']
  - id: access the id_github file from secret manager
    name: gcr.io/cloud-builders/gcloud
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        gcloud secrets versions access latest --secret=id_github > /root/.ssh/id_github
    volumes:
      - name: 'ssh'
        path: /root/.ssh
  - id: set up git with key
    name: 'gcr.io/cloud-builders/git'
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        chmod 600 /root/.ssh/id_github
        cat <<EOF >/root/.ssh/config
        Hostname github.com
        IdentityFile /root/.ssh/id_github
        EOF
        ssh-keyscan -t rsa github.com > /root/.ssh/known_hosts
    volumes:
      - name: 'ssh'
        path: /root/.ssh
  - id: connect to the repository
    name: 'gcr.io/cloud-builders/git'
    args:
      - clone
      - --recurse-submodules
      - git@github.com:${_GITHUB_ACCOUNT}/${_MANIFEST_REPOSITORY}.git
    volumes:
      - name: 'ssh'
        path: /root/.ssh
  - id: switch branch
    name: 'gcr.io/cloud-builders/gcloud'
    dir: ${_MANIFEST_REPOSITORY}
    entrypoint: /bin/sh
    args:
      - '-c'
      - |
        set -x && \
        git config --global user.email $(git log --format='%an <%ae>' -n 1 HEAD | sed 's/.*\<\([^>]*\)\>.*/\1/g') && \
        git fetch origin ${_TARGET_BRANCH} && git switch ${_TARGET_BRANCH} && \
        git fetch origin ${_DEFAULT_BRANCH} && git merge --ff origin/${_DEFAULT_BRANCH}
    volumes:
      - name: 'ssh'
        path: /root/.ssh
  - id: generate manifest for grpc-gateway
    name: 'gcr.io/cloud-builders/gcloud'
    dir: ${_MANIFEST_REPOSITORY}/${_GRPC_GATEWAY_MANIFEST_DIR}
    entrypoint: /bin/sh
    args:
      - '-c'
      - |
        set -x && \
        sed "s/PROJECT_ID/$PROJECT_ID/g" ${_GRPC_GATEWAY_TEMPLATE_FILE} | \
        sed "s/COMMIT_SHA/$COMMIT_SHA/g" >| ${_GRPC_GATEWAY_GENERATE_FILE}
    waitFor: ['switch branch']
  - id: generate manifest for grpc-server
    name: 'gcr.io/cloud-builders/gcloud'
    dir: ${_MANIFEST_REPOSITORY}/${_GRPC_SERVER_MANIFEST_DIR}
    entrypoint: /bin/sh
    args:
      - '-c'
      - |
        set -x && \
        sed "s/PROJECT_ID/$PROJECT_ID/g" ${_GRPC_SERVER_TEMPLATE_FILE} | \
        sed "s/COMMIT_SHA/$COMMIT_SHA/g" >| ${_GRPC_SERVER_GENERATE_FILE}
    waitFor: ['switch branch']
  - id: generate manifest for job
    name: 'gcr.io/cloud-builders/gcloud'
    dir: ${_MANIFEST_REPOSITORY}/${_JOB_MANIFEST_DIR}
    entrypoint: /bin/sh
    args:
      - '-c'
      - |
        set -x && \
        sed "s/PROJECT_ID/$PROJECT_ID/g" ${_JOB_TEMPLATE_FILE} | \
        sed "s/COMMIT_SHA/$COMMIT_SHA/g" >| ${_JOB_GENERATE_FILE}
    waitFor: ['switch branch']
  - id: generate manifest for migration
    name: 'gcr.io/cloud-builders/gcloud'
    dir: ${_MANIFEST_REPOSITORY}/${_MIGRATION_MANIFEST_DIR}
    entrypoint: /bin/sh
    args:
      - '-c'
      - |
        set -x && \
        sed "s/PROJECT_ID/$PROJECT_ID/g" ${_MIGRATION_TEMPLATE_FILE} | \
        sed "s/COMMIT_SHA/$COMMIT_SHA/g" >| ${_MIGRATION_GENERATE_FILE}
    waitFor: [ 'switch branch' ]
  - id: push generated manifests to ${_TARGET_BRANCH} branch
    name: 'gcr.io/cloud-builders/gcloud'
    dir: ${_MANIFEST_REPOSITORY}
    entrypoint: /bin/sh
    args:
      - '-c'
      - |
        set -x && \
        git add \
          ${_GRPC_GATEWAY_MANIFEST_DIR}/${_GRPC_GATEWAY_GENERATE_FILE} \
          ${_GRPC_SERVER_MANIFEST_DIR}/${_GRPC_SERVER_GENERATE_FILE} \
          ${_JOB_MANIFEST_DIR}/${_JOB_GENERATE_FILE} \
          ${_MIGRATION_MANIFEST_DIR}/${_MIGRATION_GENERATE_FILE} && \
        git commit \
        --author="Cloud Build Service Account <***@cloudbuild.gserviceaccount.com>" \
        -m "Deploying images
        - gcr.io/$PROJECT_ID/app:$COMMIT_SHA
        Built from commit $COMMIT_SHA of ${_APP_REPOSITORY} repository" && \
        git push origin ${_TARGET_BRANCH}
    volumes:
      - name: 'ssh'
        path: /root/.ssh
substitutions:
  _GITHUB_ACCOUNT: istsh
  _APP_REPOSITORY: gitops-sample-app
  _MANIFEST_REPOSITORY: gitops-sample-manifests
  _GRPC_GATEWAY_MANIFEST_DIR: k8s/ops/base/proxy
  _GRPC_GATEWAY_TEMPLATE_FILE: deployment.yaml.tpl
  _GRPC_GATEWAY_GENERATE_FILE: deployment.yaml
  _GRPC_SERVER_MANIFEST_DIR: k8s/ops/base/server
  _GRPC_SERVER_TEMPLATE_FILE: deployment.yaml.tpl
  _GRPC_SERVER_GENERATE_FILE: deployment.yaml
  _JOB_MANIFEST_DIR: k8s/ops/base/job
  _JOB_TEMPLATE_FILE: job.yaml.tpl
  _JOB_GENERATE_FILE: job.yaml
  _MIGRATION_MANIFEST_DIR: k8s/ops/base/migration
  _MIGRATION_TEMPLATE_FILE: job.yaml.tpl
  _MIGRATION_GENERATE_FILE: job.yaml
#  _DEFAULT_BRANCH: <Defined on the edit page for the Cloud Build trigger>
#  _TARGET_BRANCH: <Defined on the edit page for the Cloud Build trigger>

ステップは以下の順序・内容で実行されます

  1. Dockerfileをビルドし、イメージをContainerRegistryにプッシュ。タグは$COMMIT_SHA
  2. SecretManagerからGitHub SSH認証鍵を取得し、/root/.ssh/に配置。
  3. GitHubに接続する為の設定。
  4. Manifestリポジトリをクローン。
  5. ${_TARGET_BRANCH}ブランチにスイッチし、${_DEFAULT_BRANCH}ブランチをマージ。
    マニフェストファイルの変更をmainブランチにマージしておけば、このステップで各ブランチに取り込まれます。
  6. deployment.yaml.tplの内容を一部変更し、deployment.yamlを作成または変更。
    ・PROJECT_ID -> $PROJECT_ID
    ・COMMIT_SHA -> $COMMIT_SHA
  7. 作成または変更したファイルをコミットし、Manifestリポジトリにプッシュ

CloudBuildのトリガーを追加する

トリガーとなるブランチや、上記のyamlのパスを設定します。
また、例として、環境変数には以下の値を設定します。

Screen Shot 2020-10-07 at 23.16.37.png

デプロイ戦略の表に対応するブランチ名を設定してください。

設定が完了したら、CloudBuildを実行してみましょう!
成功すると、Manifestリポジトリのブランチに、以下のようなコミットがされているはずです。

Author: Cloud Build Service Account <***@cloudbuild.gserviceaccount.com>
Date:   Wed Oct 7 06:56:18 2020 +0000

    Deploying images
    - gcr.io/gitops-sample-dev/app:0b63ec0d43355956a9972ee7e031b9d180824ffb

    Built from commit 0b63ec0d43355956a9972ee7e031b9d180824ffb of gitops-sample-app repository

ArgoCDのセットアップ

インストール

$ # argocdコマンドのインストール
$ brew install argocd

$ kubectl create namespace argocd
$ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v1.7.7/manifests/install.yaml

Service(argocd-server)のタイプをLoadBalancerに変更

$ kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'

$ kubectl get svc argocd-server -n argocd
NAME            TYPE           CLUSTER-IP   EXTERNAL-IP      PORT(S)                      AGE
argocd-server   LoadBalancer   10.4.5.30    104.198.42.124   80:31795/TCP,443:30291/TCP   104m

argocdコマンドでログイン

初期パスワードはargocd-serverのPod名になっています。

$ kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o name | cut -d'/' -f 2
argocd-server-69b7fcb95d-g8tpt

$ argocd login 104.198.42.124
WARNING: server is not configured with TLS. Proceed (y/n)? y
Username: admin
Password:
'admin' logged in successfully
Context '104.198.42.124' updated

adminユーザーのパスワードを変更

初期パスワードのままだと、Pod名を調べられる人は誰でも管理者権限でログインできてしまうので、最初に変更しておきましょう。

$ argocd account update-password
*** Enter current password:
*** Enter new password:
*** Confirm new password:
Password updated
Context '104.198.42.124' updated

ArgoCDの管理画面にアクセス

$ kubectl port-forward svc/argocd-server -n argocd 8080:443

http://localhost:8080 でアクセスできます。
screencapture-localhost-8080-login-2020-10-07-05_03_03.png

アプリケーションを登録する

CREATE APPLICATIONをクリックして、アプリケーションを登録します。

screencapture-localhost-8080-applications-2020-10-07-05_10_28.png
Screen Shot 2020-10-07 at 5.14.28.png
Screen Shot 2020-10-07 at 5.13.36.png
Screen Shot 2020-10-07 at 5.13.47.png

全てのPodが起動し、ヘルスチェックが通ると、以下のような画面になります。

screencapture-localhost-8080-applications-gitops-sample-app-2020-10-07-15_57_22.png

Sync Statusを見ると、どの順序でyamlが適用されていったかわかるようになっています。
Screen Shot 2020-10-07 at 5.33.56.png

まとめ

長めの記事になってしまいましたが、GitOpsを導入する上で必要な手順と工夫点をまとめました。
CDツールはいろいろあるので、それぞれのプロダクトに合ったものを採用していただきたいですが、基本的にやることや気を付けることは変わらないと思います。
今後GitOpsを導入するプロダクトのお役に立てれば幸いです。

付録

SealedSecret

鍵の扱い

バックアップ

クラスターにある秘密鍵と証明書は、Secretになっています。
これを削除してしまったり、クラスターそのものを削除してしまった場合に、SealedSecretを復号できなくなってしまいます。
それを防ぐ為に、このSecretは必ずバックアップを取りましょう。

$ # ラベルを指定して、Secretを取得
$ kubectl get secret -n kube-system \
-l sealedsecrets.bitnami.com/sealed-secrets-key=active \
-o yaml > sealed-secrets-key.yaml 

生成されたSecretのyamlはこちら。

sealed-secret-key.yaml
apiVersion: v1
items:
- apiVersion: v1
  data:
    tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVyakNDQXBhZ0F3SUJBZ0lSQUpsYnRkeUI5bmRqZ1ljZFI1Qkh0dk13RFFZSktvWklodmNOQVFFTEJRQXcKQURBZUZ3MHlNREE1TWpReE5ESXhNRGxhRncwek1EQTVNakl4TkRJeE1EbGFNQUF3Z2dJaU1BMEdDU3FHU0liMwpEUUVCQVFVQUE0SUNEd0F3Z2dJS0FvSUNBUUR5cVJQT3pCSzk4UFZvVlJBYVJJV1gyS3dSdmJIZVBMTnhxY2Q4CmQ3UlM2a3pDZUhINzFFa0E2NzlyQWoxZVUzQ3g0dG8zTlNPWkhqNEFIVk1OS256WTVWYXRFY1F4WEZOWjdRci8Kb2hKTFNuS1VHVG55UCszZU55S29wY3hYeUlibkdlUmh0elVQOTRPdzAyWld5MHEreDdyS29uZk11ZUNMTlNsRwpqeXVKelN3UW9HZUFCbXZYL2NUc29pTHNRdHJoK1Jvc29ZL3VIYU1KNDlYaytiSzRrWFJBeEZPenh3dUZGd0JZCk9NVEZnS3d3WWlaM1l2RytOYjBwWGx6MyttZ1NTVkxRem9CallPT0tMRloxWnBBMXRKYWJIcVhwdUF6b2FJOFUKL0svQjV1cWxLNngrMnhtYUFvcjUxTHZubEdxWER4K2hOZXRDd0NUMVB4Uk5PS0ErNTRiTmVaUjh5alVGMDM5MQpUOXM4a1ZKT2RjN3VTTVdkbytCSUVGM2tJcjdiNThrcThiRTdVcERUOUVybG8va2xhNmZOV2hSaldGR3VHdjgrClJCaUZYZFVxZHZTeVNOTC91VndUVHN2SWpZQ2RqajJsMDZJM2lvQWxsUjQvejNDa21ORW5hQ1oxVmEyclRMc0IKRW5yRHhmRGJsUnhMVHpENWRZVHJ1MTd0MTdadHc3emp1Q3RRWlViM2NzWlJER1EwTzlYcWxZbjZJQzVaRjRxeQpXOFF1b3N6Wlo1cVAwSVFXOUZsK3Nvb3lCbjJhdmNtNnZoMFBvOVZYNGl1U2NLcnBKUkFaNHlIYnRIQlJxTXZOCklFYXBua2dZVk1XMGF0UkN1c2gzTktIaFNCakYyM1ZXMWV4Y01lbFdFWVc4TWhub0d2S1RnZjI5ZW9oMjllVXYKdlZNeVhRSURBUUFCb3lNd0lUQU9CZ05WSFE4QkFmOEVCQU1DQUFFd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTgpCZ2txaGtpRzl3MEJBUXNGQUFPQ0FnRUFaWkRrblBvWVRrTXltTU01cHdlZ2JLSmRwQlRDRDgvbUR3dUZBS1ZRCkhia0lCMVR1NmVwOVgzbzYxQVF4ZWF3Ukd5NVRvS29pVHNqUTRhcCsrNUJqYmpWZHdBREpZWVd6d2twR0taenAKTDFXV3IyTlQzOXYvR1dPOHl3ZnlUb3BQcjZPN3h5dVRPYk4vb0JjdC9lVVZVV3gzemZyM1hxb3FLZ3IzTDlTLwpuYU5abEc1ZFcvNTNEcEFTSHNEMVgrRWZOQXQ5KzZtMXZpZm5FRmxFQmVWeU02Mm9NQ3NPbFdNVGNEcnBIN0JrCkthSTM4Zno2aTQzd3pJK1ZncDAzVUx0aVVsMnZ4ZVdJbHdFcXVGeUZoTDFkeFBqR0JFNHAraGhCNGR3a3hGVEgKelk1bVFweUhJNWdvK1FvQk1pclZzcFZXeW9HNHo3WUJNdGZSamYweWdWUXlXd2RPbUpZMDF3NVM4ektMMG9tYgpmZU5wbWdFY1NkNWo3SEIydklnVWJGVWxlaERIZkszcXFGR2FsOXYwMXZWckJ5aklnRW9TUGhUOVFlVkh6TXk1CnI4Ump1TkZiUWZidXVWazRCd1hHeW9tYUJUdkdqVXhqMWRHRlRPNjdTMHMvMFBzOVpGM29ONkdZbFRQZDQ2WjYKeHJ5b2FqMXA2V2s3VVhBaGlWd3E3VFhMZEhPcVIxZnl5RFF3eEJLYTArTkI0NHlqcXp6Tmk5TzRuYnovM2hLagpHK1JpcWdFaFFkTUg5MVFkcGVvUUZKeUFmQzVQcnBxUkVoQUY3dG5mUExqOTZRTlJGS0pXeW5MRXFma3cxN0tGCnlvbWFUMmVVZ05Ock11emczdXB4UXNsakp6d2RQLzZxYklmb1Uwa0V1TXo3V3gvQ09WeGtqWDdlckZNT0N2cUIKcWQ0PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
    tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS0FJQkFBS0NBZ0VBOHFrVHpzd1N2ZkQxYUZVUUdrU0ZsOWlzRWIyeDNqeXpjYW5IZkhlMFV1cE13bmh4Cis5UkpBT3UvYXdJOVhsTndzZUxhTnpVam1SNCtBQjFURFNwODJPVldyUkhFTVZ4VFdlMEsvNklTUzBweWxCazUKOGovdDNqY2lxS1hNVjhpRzV4bmtZYmMxRC9lRHNOTm1Wc3RLdnNlNnlxSjN6TG5naXpVcFJvOHJpYzBzRUtCbgpnQVpyMS8zRTdLSWk3RUxhNGZrYUxLR1A3aDJqQ2VQVjVQbXl1SkYwUU1SVHM4Y0xoUmNBV0RqRXhZQ3NNR0ltCmQyTHh2alc5S1Y1YzkvcG9Fa2xTME02QVkyRGppaXhXZFdhUU5iU1dteDZsNmJnTTZHaVBGUHl2d2VicXBTdXMKZnRzWm1nS0srZFM3NTVScWx3OGZvVFhyUXNBazlUOFVUVGlnUHVlR3pYbVVmTW8xQmROL2RVL2JQSkZTVG5YTwo3a2pGbmFQZ1NCQmQ1Q0srMitmSkt2R3hPMUtRMC9SSzVhUDVKV3VuelZvVVkxaFJyaHIvUGtRWWhWM1ZLbmIwCnNralMvN2xjRTA3THlJMkFuWTQ5cGRPaU40cUFKWlVlUDg5d3BKalJKMmdtZFZXdHEweTdBUko2dzhYdzI1VWMKUzA4dytYV0U2N3RlN2RlMmJjTzg0N2dyVUdWRzkzTEdVUXhrTkR2VjZwV0oraUF1V1JlS3NsdkVMcUxNMldlYQpqOUNFRnZSWmZyS0tNZ1o5bXIzSnVyNGRENlBWVitJcmtuQ3E2U1VRR2VNaDI3UndVYWpMelNCR3FaNUlHRlRGCnRHclVRcnJJZHpTaDRVZ1l4ZHQxVnRYc1hESHBWaEdGdkRJWjZCcnlrNEg5dlhxSWR2WGxMNzFUTWwwQ0F3RUEKQVFLQ0FnQnNOcTVZcUhVck0wdWRiV0d5OVIvR2FaL0NnWi9TaGF0WVl1aE5QMnl4RlQrSjhnQ1MxMFovSEtMTwphNzlHVTF1TVdLZ0x1cXpYV2I4NGVkdFJvY0x2VHNicWcyUEV4M0Y4UnRPQzBKbnI3WlZQS2pqSEtXOUFpOEh2CkI1RXJESWZzZzRWdmRpNDVvcDJkdTRpRjZENjYrWUw1WHA2aU03cEpHam4vOTFUcExSQWJrZ3pWOFFjaTJVNTYKWUl2R1pNSUx4L1MrTm9aakgrQlhScjFhVVdnOEd0R0hHSVpqUTc2RmFZNkR5VDBtL296TFB0bjhuNmxDcytCWgpsSFZOT09RMFUzS1ZINkh0cjRXSlZ1QnZsbjkxRThXZUEzcmwwV2dnTkpDcVFVMTM0U3grNEEwYXZVYWJnY3JNClF1eFJCOFRJL0x1VVB1RmRjU2FLSDhsRDdwNjIwR1dzcDBXditMMWJlY3N6YkphcHRhMzhsUnNrLy8rUmpCMW8KUWszRXRkNnZWblZxVTF1Q0FDdlYzcWplY25Kd244QkJ1OElyOXdXMHFSR1RQUUhKak5pMzdYVC95RVJGNjJtVApHMEVodmVjQnZqdE5QcVE5OUdGc2l0YUtFeGhSK3FWTWZDZktVQ2dMZmRGYTFvU0x3NFVJY2NrZzZ1Ymxoc2YyCmxIaHBDWlpzL3piQzFxbDJob3NHRHR2YnNzOEIwbTJWdW9HalFuMHF2SEFZeEMrZG9QK3FZTlB6VjRxS1VweFUKV0RXZEZ5QXRvZ0M4bVgyMXZvbHNvU3grRWdrVW5ZV0Z6WmViQXpleTFuZGxXNnVQcmJBQUM0aVQ0VUFRZko1aAo3Q0hIUlo4M2V4ZGo4L1Fnd0FBZHByZXplV3VTNFdyUVJYOVl1WGJudXE3OEFLWEFtUUtDQVFFQSs3a3l1Z3lVCmxRYXlsNFV4Qmp5ajdSTCtTWGErbXYwWkhUQVd5dXhra2FsSHNCYms1U0hNT2RBbWFrR1hJdEhTckxZMEd3Yi8KRys2WC9YQWYybFZtZUlBRUM3WlJiRk9kRHE1TXBZZkdSWmpBZWNRamRJQjQvMWNMNnExT2dRMjZtaFNaalJKegpFQnVocVQwNHJIcmovQ2lyT2lER3dMUDJlejg2aXViOHB6YWtkeEMwMERsLzliTDNPWWJmWG1wL29BcDZYem9pCnExcUpzdzIzTFJSNlNkd29xQmlZdHczZ1hJK3dPV3ozci9uNUFJOHNuSUVwdGNrTjVLYWQ0NnlYOEpzSFYvVjcKNnBqeWU3cWpkRVp3TktGRGlDeWhVVFA0bit1WW5TblhjeTdteTUwNkh0V2M4SHNNekRyclFFeFZsSkY3UitPMQo3ZmhnbG55V0tBcGVDd0tDQVFFQTlzaDJXb2hyOWpVUlk1M0s2NVFpa0JxQkUySCs2aGhSc0pXaGhBbUlwUUlmCmFTVVorNThIL0JmMGx5czdCNTUxU24xVTlWNmxQTFNOQm5lb25OWnIzZ0hwaGNhN2o5Rm45Y2tpdHVTZzhDN3MKSGg1RzEvOVptSDlvRTdPRjRwSzdCT1JVUGVHRUdsbE1QdWpNYVV4a3BpamJhdmFXd2htU2NNN1E4bVFxKzhYUwpIVkpoMW5uUmpwY1B0ZzBwSVFnZUtkTFViMytRZ1RXV3B6RWg1V2NKNjY2OCtNWDBJRENoWUdOOVFINklWNDNmCm9ZZE1reE1WajVoQXQ3dVZ4UzdrQmY4a2o0RndmbmhIc3hta3NpMUp0aTBMUUZJWUhLaEh3RjhRTjYzM1RWNFUKbENNcm9Sb3NzWVpwbGRUR0c3TE5IYm05NFd6MStJUVlIRkxNL0crNk53S0NBUUJIY1FCY25VVnVKa0I1a0d1aApnWVJrdkljL0FseUdVRjdZVWRXbU1nRTQ5ZnBLbDdUTzh4Q3JOOUF2Ui81RSs1ZjNQSjc5TExjcWprVEV3UlhKCk9ZT2puM0dHZ1hBS2RwQ1VvaE5PeDRJV0xvd3lBdUN0SitrdlR0MHE5WlRhTzdOQ0Y2YWN5eWVzNHFxM0JaRVkKSXFpaDRFajRibVQ2UEJrd1VYbWtBbnFpV25mQzh4TGVKZk9USC8vYWE5VHBUd0I1dzMrSGwxQlBvWnFESHRsbQpDZkhMRkpqVlhHVzdUa3ptK0VEamkzR2dtQ2w3WnljYUkrNWFrWDFINzZIUUJDUDdQWVNRQ2pQcEdROTQzamVWCndJZ0g1OXpxd1AvbnRBQlVJdUZsZVlLVVJqTnFobTBBWTAvdlVIMEpXWTk2NkM5Qnd5aGg5dGFqZTJLdVV4MnEKcjRhUkFvSUJBRTQvaWFkekVpaGk4enlPejhTYWw4cnhYSFAwNG9yL1l3ZUdxa3ZmYWdCSUNBV2l5ZlpLbXBHSQpWdm1IcjVQZTNubmIrNUJCamtzTlJKb0VYdVk3NXIvaUExVnppZzB6N0s2Mk05ZWg3cFc1aXd5UnRRelAzbXpJCkdRd0dKREdQTE5XRVFHSE9tOEJ2Q0FuNmJyWUVqdlZRaHlJSFJnNE5aYTEzSmpGMUtWdmpnWmZpZ3pzcUxSUDEKT2ZvVGVqTUxDK1ZmQUJUS2NkdEdUcHA4cmE2N1dSM3RyZVdEZnNDbUtzVVJScW1vZFRxdHRYYldHNldXcTRONApWeXpxd2JaZ0E5VVM3VmpEUmhRVHQwaEduVDRSdGdtWmhyUENVL3JpTUw0Q1puWUJKeVRVNjlsOHZWeTdtK2crCklrUnJ6dFVCZ0tBb1FOaTFYOWlJcWN4eFJLWFdGMGtDZ2dFQkFNQXVjWEs4eGo5VEJuWkdGMW5mR2pOWlFQWkcKb2UvMEdOaWpHVUtUZkN1dXZNcU16Tm1JeVY2VlVFNXhiaWVFNElIbE5EQXROSG5uYTBnNWUxb0cycDdISWo2cApWNG10QVlMYUU0dTg4c3hFMFdKNHVEUVN3aWg3b0Q5UWkrTUN1R1ZZRTY3LzNDbElZdFhaK2dpSWRRY3ZEcDNwCk9WVzlXYjhENk1mSTQ0Ukp4RlFQb2hheU9LazAvOXVuck1vQlAzZXdpcVRLY2EwNFZ2V2dONHVaU3pMQ0wwaDQKeUJiL3AwdUNlcW1XOUhLSWlsdXc0NmN3OVdOZnZPVDZYZHB4b25LNysya2RieTQ4VFk0QVpNa3JEU21JYzkwTApXeFFlb2NlYWkxUlRIRG9Ock44dzB5Qk1WOGNKYjdsRW9yY21xV0tteEZ6cjNoQjRyRktrc3hlZE93ST0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K
  kind: Secret
  metadata:
    creationTimestamp: "2020-09-24T14:21:09Z"
    generateName: sealed-secrets-key
    labels:
      sealedsecrets.bitnami.com/sealed-secrets-key: active
    name: sealed-secrets-key4wlwz
    namespace: kube-system
    resourceVersion: "3522389"
    selfLink: /api/v1/namespaces/kube-system/secrets/sealed-secrets-key4wlwz
    uid: 1fb9a39d-1b5f-4a4b-a47f-ebd56d02303d
  type: kubernetes.io/tls
kind: List
metadata:
  resourceVersion: ""
  selfLink: ""

local環境用の鍵はそのままコミットしても構いませんが、ステージングや本番などの鍵はそうはいきません。
何もせずにコミットしてしまうと、SealedSecretからSecretを生成できてしますので、SealedSecretを使う意味がなくなってしまいます。
KMSなどを使用して暗号化してからコミットしましょう。

バックアップから証明書を取得

上記のsealed-secrets-key.yamlから証明書を取得します。
ここでは、yqコマンドを使用します。

$ yq r sealed-secrets-key.yaml 'items.*.data."tls.crt"' | base64 --decode > cert.pem

鍵のリストア

バックアップした鍵をクラスターにリストアします。
ここでは、鍵のSecretがない場合の手順を紹介しますが、残っている場合は事前に削除してください。

$ # 鍵のSecretを生成
$ kubectl apply -f sealed-secrets-key.yaml

$ # Secretを生成しただけでは反映されないので、ControllerのPodを再生成する
$ # このPodはDeploymentによって生成されているので、Podを削除するとすぐに生成される
$ kubectl delete pods -n kube-system -l name=sealed-secrets-controller

$ # ControllerのPodが作り直されたことを確認
$ kubectl get pods -n kube-system -l name=sealed-secrets-controller
NAME                                         READY   STATUS    RESTARTS   AGE
sealed-secrets-controller-867447b788-zhrg4   1/1     Running   0          18s

Secretの内容を更新する

運用していると、Secretを更新したい場面があると思います。
しかし、SecretはSealedSecretとして暗号化されているので、簡単には更新できません。
そこで、SealedSecretを復号してから更新する手順を紹介します。
もちろんクラスターからSecretを取得しても構いません。

$ # バックアップした鍵のyamlを渡して、SealedSecretを復号する
$ kubeseal \
--controller-namespace=kube-system \
--controller-name=sealed-secrets-controller \
< sealed-secret.yaml \
--recovery-unseal \
--recovery-private-key sealed-secrets-key.yaml -o yaml > secret.yaml

Secretの値を更新し、再度SealedSecretのyamlを生成すれば完了です。

ArgoCD

Hookについて

公式ドキュメント

Hookの定義方法

Hookはアノテーションとして定義します。
ここではJobのyamlを紹介します。

apiVersion: batch/v1
kind: Job
metadata:
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
    argocd.argoproj.io/sync-wave: "3"
  name: migration
  labels:
    job-name: migration-job

Hookの種類

ArgoCDのHookは5種類あります。

Hook 説明
PreSync マニフェストの適用前に実行される。
Sync すべてのPreSync Hookが成功として完了した後で、マニフェストの適用と同時に実行される。
Skip マニフェストの適用をスキップする。
PostSync すべてのSync Hookが成功として完了し、すべてのリソースが正常な状態になった後に実行される。
SyncFail PreSyncやSync, PostSyncが失敗したときに実行される。(ver1.2以降)

Hookの削除

Hookを何らかの方法で削除するには、hook-delete-policyをアノテーションに定義します。
Policyは3種類あります。

Policy 説明
HookSucceeded Hookが成功した後に削除される。
HookFailed Hook失敗した後に削除される。
BeforeHookCreation 次の新しいHookが作成される直前に削除される。(ver1.3以降)

hook-delete-policyを定義しないと、次回の実行時にHookが残ったままなので、PreSyncそのものが失敗してしまいます。必ず定義しましょう。
HookSucceededは失敗した場合に削除されずに残ってしまいます。HookFailedも同様なので、BeforeHookCreationが有用なのではないでしょうか。

同一Hookでの実行順序の指定

sync-waveをアノテーションに定義します。
これを定義すると、その値の昇順にHookが実行されます。
ただし、注意点が2つあります。
1つ目は、値は数値ではなく文字列で定義すること。
2つ目は、公式ドキュメントにもあるように、sync-waveの初期値は0です。同一Hookでsync-waveを設定する場合に、どれかに設定漏れがあると、それが意図せず先に実行されることがあるので注意してください。

GitHubのSSOで入れるようにする

ArgoCDは初期ユーザーとしては、管理者権限を持つadminしかいないようです。
しかし関係者全員にadminのパスワードを共有するのは望ましくないと思います。
そこで、GitHubのSSOで入れるように設定します。Organizationもチェックするので、組織に属するGitHubアカウントなら誰でも入れます。
また、GitHubアカウントでログインする場合は、ReadOnlyにすると良いでしょう。

GitHubの設定

GitHubの Organization > Settings > OAuth Appsで以下のように登録します。
callback URLは /api/dex/callbackで終わるように入力してください。
Screen Shot 2020-10-07 at 17.53.11.png

ConfigMapを編集する

argocd-cmという名前のConfigMapを編集します。
dataを以下のように入力して、ClientIDとClientSecretを登録してください。

$ kubectl edit configmap argocd-cm -n argocd

...
data:
  dex.config: |
    connectors:
      - type: github
        id: github
        name: GitHub
        config:
          clientID: f227da4f3f260e45410d
          clientSecret: c67c6c389f393f11319fc12d49e2059c36e18bea
  url: http://104.198.42.124
...

続いて、argocd-rbac-cmという名前のConfigMapを編集します。
admin以外のユーザーを全てReadOnlyにします。

$ kubectl edit configmap argocd-rbac-cm -n argocd

...
data:
  policy.default: role:readonly
...

最後にargocd-serverのPodを再起動します。
argocd-serverはDeploymentになっているので、Podを削除するとすぐに新しいPodが作られます。

$ kubectl delete pods -n argocd -l app.kubernetes.io/name=argocd-server

少し待ってから、ArgoCDの管理画面にアクセスすると、以下のように変わっていると思います。
screencapture-104-198-42-124-login-2020-10-07-05_49_28.png

14
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
10