TECHSTEP

ITインフラ関連の記事を公開してます。

ArgoCD ApplicationSetを動かしてみる

今回はArgoCDをより拡張する機能を持つ ApplicationSetを動かしてみます。

ApplicationSetとは

ApplicationSetは、Argo Projectに含まれるプロダクトの一つです。ArgoCDを利用する複数のユースケースにおいて、Applicationリソースの管理を自動化しつつ、柔軟に管理する方法を提供するControllerになります。

ArgoCDはApplicationというリソースの中で、利用するGitリポジトリマニフェストファイルの場所デプロイ先のクラスターなどを指定します。ArgoCDを利用する環境の規模が大きくなったりすると、管理をする上で面倒になるケースが出てきます。

例えば、1つのアプリケーションを提供する環境が複数のKubernetesクラスターで構成されており、それぞれのクラスターに同じアプリケーションをデプロイしようとすると、クラスターごとにApplicationリソースを作成しなければなりません。 また、monorepoのような、1つのリポジトリ中で複数のアプリケーションを管理する場合も、それぞれのアプリケーションごとに Application リソースを用意しなくてはなりません。

ApplicationSetは、上記のような悩みを解消するため、複数のクラスターやGitリポジトリを効率的に扱うための機能を提供します。

Generator

ApplicationSetは Generator というパラメータを含みます。 Generatorクラスターの名称やURL、Gitリポジトリなどのパラメータを設定することで、それぞれに対応した Application マニフェストを生成し、複数のクラスターやGitリポジトリを使用したArgoCDによるアプリケーション管理を実現します。

Generator にはいくつか種類があり、以下の通りになります。

  • List generator : クラスターの名称とアクセス先URLなどのリストを生成する
  • Cluster generator : ArgoCDに登録されているクラスター情報を生成する。ArgoCDに登録されたクラスターは自動的に検知する。
  • Git generator : Gitリポジトリリポジトリ中のフォルダ構成などの情報を生成する。
  • Matrix generator : 2種類のgeneratorを組み合わせて使う。
  • SCM Provider generator : GitHub/GitlabのようなSCMの提供するAPIを使用し、Organization中のリポジトリを探索してパラメータを生成する
  • Cluster Decision Resource generator : ArgoCDに登録されたクラスターのリストを生成する。Custom Resourceのインターフェイスとして利用し、事前にConfigMapに定義したCustom Resourceを作成する。

※ 参考:ApplicationSet Controller - Generators

ユースケース

ApplicationSetでは、いくつかのユースケースを想定しています。

Cluster add-on

Cluster add-onとは、Kubernetesクラスターに機能を追加・拡張するようなプロダクトのことを総称しており、例えば Prometheus OperatorArgo workflow などを指しています。これらは各クラスターに共通して導入することが多いため、ApplicationSetを生かすことのできるユースケースの一つとなります。

Cluster add-onはクラスターレベルの権限を必要とするため、その管理はインフラ管理者が担うことが多くなります。Add-onを導入するクラスター数の増減が多くなるほど管理コストは高くなります。またクラスターごとに利用用途が異なる(テスト・本番環境など)と、add-onの設定を変更する場合も出てくるため、より複雑になります。

ApplicationSetでは複数のGeneratorを提供しており、それぞれ用途に応じて使い分けることができます。

  • List generator : デプロイするadd-onごとにApplicationSetを用意し、デプロイ先のクラスターリストを定義することで管理できます。List generatorの場合クラスターの増減に応じてApplicationSetの手動更新が必要となります。

  • Cluster generator : デプロイするadd-onごとにApplicationSetを用意します。Cluster generatorはArgoCDに登録されたクラスターを自動で検知するため、クラスターの変更に合わせた自動更新が可能となります。

  • Git generator : Git generatorの files というfieldを使い、クラスターのリストをJSONファイルで定義しておきます。このJSONファイルを更新することでクラスター情報を更新し、add-onをデプロイすることができます。また directories というfieldを使うと、例えばクラスターごとに対応するディレクトリを用意しておき、ディレクトリ名とクラスター名を一致させておくと、ディレクトリの更新に合わせてクラスター側を更新することができます。

Monorepo

あるプロジェクトでMonorepoを採用している場合、一つのGitリポジトリにすべてのコードが格納されます。Monorepoの場合は複数チームが共通の基準に従う必要があり、それらを促進・強制するためにも、自動化が重要になってきます(参考リンク)。

Monorepoを採用する場合、Kubernetesクラスターの管理者は、1つのリポジトリからクラスター全体の状態を管理することになります。そのため、リポジトリ中のマニフェストファイルが更新されれば、その変更は即座にクラスターへと反映されるべきです。

Git generator を利用することで、monorepoを採用するプロジェクトの管理者をサポートすることが可能となります。directories fieldでアプリケーション個別に分かれたサブディレクトリを指定することが可能です。また files fieldを使うと、JSONメタデータを含むファイルを参照し、そこに定義されたアプリケーションをデプロイすることも可能です。

Self-service of ArgoCD Applications on multi-tenant cluster

開発者が自らArgoCDの Applicationを開発・利用する場合、app-of-appsというパターンと組み合わせ、クラスター管理者がPRを通じてマニフェストファイルをチェックし、デプロイの許可を行う、というケースがあります。Applicationには project cluster など重要なパラメータがあり、この設定を誤ってしまうと別のArgoCD Projectやクラスターが更新されるため、レビュアーの重要度が上がってしまいます。そのため管理者としては、開発者の変更できるパラメータを絞り、重要なものは変更できないようにしておきたくなります。

ApplicationSetでは、Git generatorと組み合わせた代替手段を提供しています。ApplicationSetの template filedに、 project destination などの情報をべた書きして固定し、 source 部分のみ Git generatorで生成するようにしておきます。開発者にはGit generator部分のみ更新してもらうことで、アプリケーションの更新先をある程度コントロールすることが可能になります。

※参考:ApplicationSet Controller - Use Cases

ApplicationSetを動かしてみる

ここから実際にApplicationSetを動かしてみます。今回はひとまず動かしてみるという目的で、いくつかのパターンで動かしました。

基本的にはドキュメント通りに動かすだけですが、1点だけ気を付けるとすると、ApplicationSet Controllerと同じNamespaceに ApplicationSetリソースを作成する必要があります。

※参考:ApplicationSet Controller - How ApplicationSet controller interacts with Argo CD

今回の検証環境は以下の通りです。

インストール

ApplicationSetはArgoCDと一緒に利用する必要があります。今回はArgoCDとApplicationSetを別々に導入しました。ArgoCDの導入はこちらのページに記載されているので割愛します。ArgoCD導入後は、ArgoCDと同じNamespace(今回は argocd )にApplicationSet Controllerなどのリソースをデプロイすれば、準備は完了です。

kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/applicationset/v0.2.0/manifests/install.yaml

※参考:ApplicationSet Controller - Getting Started

Local cluster

まずはArgoCDの導入されたクラスター上に、ApplicationSetを使ってリソースを作成します。

List generator

使ったマニフェストファイルはこちらです。 spec.generator.list 部分にクラスターの名称とURLを指定します。そうすると spec.template 配下にある {{cluster}} {{url}} 部分にパラメータが設定され、それぞれのパラメータに応じた Applicationリソースが作成されます。

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: guestbook
  namespace: argocd
spec:
  generators:
  - list:
      elements:
      - cluster: in-cluster
        url: https://kubernetes.default.svc
  template:
    metadata:
      name: '{{cluster}}-guestbook'
    spec:
      project: default
      source:
        repoURL: https://github.com/argoproj/argocd-example-apps.git
        targetRevision: HEAD
        path: guestbook
      destination:
        server: '{{url}}'
        namespace: default

※ 参考:ApplicationSet Controller - List Generator

上記ファイルをデプロイすると、以下のようにリソースが作成されます。

$ kubectl apply -f list-applicationset.yaml 
applicationset.argoproj.io/guestbook created

$ kubectl get applicationset -n argocd
NAME        AGE
guestbook   16s


$ kubectl get app -n argocd
NAME                   SYNC STATUS   HEALTH STATUS
in-cluster-guestbook   OutOfSync     Missing

$ argocd app list
NAME                  CLUSTER                         NAMESPACE  PROJECT  STATUS     HEALTH   SYNCPOLICY  CONDITIONS  REPO                                                 PATH       TARGET
in-cluster-guestbook  https://kubernetes.default.svc  argocd     default  OutOfSync  Missing  <none>      <none>      https://github.com/argoproj/argocd-example-apps.git  guestbook  HEAD  

あとは argocd app sync コマンドなどでSyncすれば、テスト用のアプリケーションがデプロイされます。もちろん templatespec.syncPolicy: automated を追加すればして自動Syncを有効にすれば、ApplicationSetデプロイ後にテスト用アプリのリソースは作られます。

なお、ApplicationSet controllerのログは以下の通り。

ApplicationSet作成時にControllerに出力されるログ

$ kubectl logs -n argocd argocd-applicationset-controller-5558c458d5-n52d9 -f
2021-11-23T01:58:32.411Z        INFO    setup   ApplicationSet controller v0.2.0 using namespace 'argocd'       {"namespace": "argocd", "COMMIT_ID": "2fb043581b8692c84205075494acab6f4b5aef2d"}
2021-11-23T01:58:33.118Z        INFO    controller-runtime.metrics      metrics server is starting to listen    {"addr": ":8080"}
2021-11-23T01:58:33.119Z        INFO    setup   Starting manager
2021-11-23T01:58:33.219Z        INFO    controller-runtime.manager.controller.applicationset    Starting EventSource   {"reconciler group": "argoproj.io", "reconciler kind": "ApplicationSet", "source": "kind source: /, Kind="}
2021-11-23T01:58:33.220Z        INFO    controller-runtime.manager.controller.applicationset    Starting EventSource   {"reconciler group": "argoproj.io", "reconciler kind": "ApplicationSet", "source": "kind source: /, Kind="}
2021-11-23T01:58:33.220Z        INFO    controller-runtime.manager.controller.applicationset    Starting EventSource   {"reconciler group": "argoproj.io", "reconciler kind": "ApplicationSet", "source": "kind source: /, Kind="}
2021-11-23T01:58:33.220Z        INFO    controller-runtime.manager.controller.applicationset    Starting Controller    {"reconciler group": "argoproj.io", "reconciler kind": "ApplicationSet"}
2021-11-23T01:58:33.219Z        INFO    controller-runtime.manager      starting metrics server {"path": "/metrics"}
2021-11-23T01:58:33.421Z        INFO    controller-runtime.manager.controller.applicationset    Starting workers       {"reconciler group": "argoproj.io", "reconciler kind": "ApplicationSet", "worker count": 1}




# ApplicationSet作成時
time="2021-11-23T02:08:33Z" level=info msg="Alloc=6126 TotalAlloc=18354 Sys=73297 NumGC=9 Goroutines=49"
time="2021-11-23T02:11:09Z" level=info msg="generated 1 applications" generator="{0xc0002b3080 <nil> <nil> <nil> <nil> <nil>}"
time="2021-11-23T02:11:09Z" level=info msg="Starting configmap/secret informers"
time="2021-11-23T02:11:09Z" level=info msg="Configmap/secret informer synced"
time="2021-11-23T02:11:09Z" level=info msg="created Application" app=in-cluster-guestbook appSet=guestbook
2021-11-23T02:11:09.581Z        DEBUG   controller-runtime.manager.events       Normal  {"object": {"kind":"ApplicationSet","namespace":"argocd","name":"guestbook","uid":"35d9488c-837a-46c0-a029-014890b804f9","apiVersion":"argoproj.io/v1alpha1","resourceVersion":"1618215"}, "reason": "created", "message": "created Application \"in-cluster-guestbook\""}
time="2021-11-23T02:11:09Z" level=info msg="end reconcile" requeueAfter=0s
time="2021-11-23T02:11:09Z" level=info msg="generated 1 applications" generator="{0xc00092cdc0 <nil> <nil> <nil> <nil> <nil>}"
time="2021-11-23T02:11:09Z" level=info msg="unchanged Application" app=in-cluster-guestbook appSet=guestbook
2021-11-23T02:11:09.607Z        DEBUG   controller-runtime.manager.events       Normal  {"object": {"kind":"ApplicationSet","namespace":"argocd","name":"guestbook","uid":"35d9488c-837a-46c0-a029-014890b804f9","apiVersion":"argoproj.io/v1alpha1","resourceVersion":"1618215"}, "reason": "unchanged", "message": "unchanged Application \"in-cluster-guestbook\""}
time="2021-11-23T02:11:09Z" level=info msg="end reconcile" requeueAfter=0s
time="2021-11-23T02:11:11Z" level=info msg="generated 1 applications" generator="{0xc0007f22c0 <nil> <nil> <nil> <nil> <nil>}"
time="2021-11-23T02:11:11Z" level=info msg="unchanged Application" app=in-cluster-guestbook appSet=guestbook
2021-11-23T02:11:11.713Z        DEBUG   controller-runtime.manager.events       Normal  {"object": {"kind":"ApplicationSet","namespace":"argocd","name":"guestbook","uid":"35d9488c-837a-46c0-a029-014890b804f9","apiVersion":"argoproj.io/v1alpha1","resourceVersion":"1618215"}, "reason": "unchanged", "message": "unchanged Application \"in-cluster-guestbook\""}
time="2021-11-23T02:11:11Z" level=info msg="end reconcile" requeueAfter=0s

Cluster generator

マニフェストファイルは以下の通りです。

Cluster generatorはArgoCDに登録されているクラスターを検知してパラメータを生成するため、デプロイ先のクラスターを指定しない場合は特に設定は必要ありません。 spec.template{{name}} {{server}} には、ArgoCDで登録されているクラスター情報がそのまま使われます(ここではそれぞれ in-cluster https://kubernetes.default.svc)。

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: guestbook
  namespace: argocd
spec:
  generators:
  - clusters: {} 
  template:
    metadata:
      name: '{{name}}-guestbook'
    spec:
      project: "default"
      source:
        repoURL: https://github.com/argoproj/argocd-example-apps/
        targetRevision: HEAD
        path: guestbook
      destination:
        server: '{{server}}'
        namespace: default

※ 参考:ApplicationSet Controller - Cluster Generator

こちらもデプロイすれば、先ほどと同様リソースが作成されます。

$ kubectl apply -f cluster-applicationset.yaml 
applicationset.argoproj.io/guestbook created

$ kubectl get appset -n argocd
NAME        AGE
guestbook   8s

$ kubectl get apps -n argocd
NAME                   SYNC STATUS   HEALTH STATUS
in-cluster-guestbook   OutOfSync     Missing

あとはSyncすればPodも作成されます。

$ argocd app sync in-cluster-guestbook
$ kubectl get pods 
NAME                            READY   STATUS    RESTARTS   AGE
guestbook-ui-85985d774c-t7mxr   1/1     Running   0          13s

なお、ApplicationSetを削除すると、関連するApplication / Podなどのリソースがすべて削除されます。

$ kubectl delete -f cluster-applicationset.yaml 
applicationset.argoproj.io "guestbook" deleted

$ kubectl get appset -n argocd
No resources found in argocd namespace.

$ kubectl get app -n argocd
No resources found in argocd namespace.

$ kubectl get pods
No resources found in default namespace.

Git generator

マニフェストファイルはこちらです。

ここでは directories fieldで使用するマニフェストファイルの場所を指定します。また template にある path.basename には、path に指定したディレクトリの配下にあるサブディレクトリの名称が入ります(ここでは argo-workflows prometheus-operator)。

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: cluster-addons
  namespace: argocd
spec:
  generators:
  - git:
      repoURL: https://github.com/argoproj-labs/applicationset.git
      revision: HEAD
      directories:
      - path: examples/git-generator-directory/cluster-addons/*
  template:
    metadata:
      name: '{{path.basename}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/argoproj-labs/applicationset.git
        targetRevision: HEAD
        path: '{{path}}'
      destination:
        server: https://kubernetes.default.svc
        namespace: '{{path.basename}}'

※ 参考:ApplicationSet Controller - Git Generator

こちらも同様にデプロイすれば、以下のようにリソースは作成されます。ここではadd-onをデプロイするためにリポジトリを変更しており、2つのApplicationリソースが作成されます。

$ kubectl apply -f git-applicationset.yaml 
applicationset.argoproj.io/cluster-addons created

$ kubectl get appset -n argocd
NAME             AGE
cluster-addons   25s

$ kubectl get app -n argocd
NAME                  SYNC STATUS   HEALTH STATUS
argo-workflows        OutOfSync     Missing
prometheus-operator   OutOfSync     Missing

こちらもSyncすればリソースは作成されますが、今回はNamespaceがサブディレクトリ名と一致する形式のため、事前にNamespaceを作成してからSyncします。

$ kubectl create ns argo-workflows

$ argocd app sync argo-workflows

$ kubectl get pods -n argo-workflows
NAME                                   READY   STATUS    RESTARTS   AGE
argo-server-5ddd56c954-mkgkg           1/1     Running   0          31s
workflow-controller-574596cc9f-2wzmk   1/1     Running   0          31s

External cluster

次は外部クラスターに向けてのデプロイです。今回は eksctl コマンドで別のEKSクラスターを作成し、その後ArgoCDにクラスターを追加登録します。

# external clusterの作成
$ eksctl create cluster -f eks-clusterconfig-2.yaml

$ eksctl get clusters
2021-11-23 15:22:29 []  eksctl version 0.69.0
2021-11-23 15:22:29 []  using region ap-northeast-1
NAME                    REGION          EKSCTL CREATED
eks-cluster-argodst     ap-northeast-1  True #追加したクラスター
eks-cluster-argosrc     ap-northeast-1  True

# 登録するクラスターの確認
$ kubectl config get-contexts

# クラスターの登録
$ argocd cluster add <external cluster name>

※ 参考:ArgoCD Docs - Argocd cluster add

クラスターを登録すると以下のようにSecretリソースが作成されます。なおSecretリソース名にあるURL先のクラスターは、既に削除されています。

$ kubectl get secret -n argocd
NAME                                                                                       TYPE                                  DATA   AGE
argocd-application-controller-token-22xzv                                                  kubernetes.io/service-account-token   3      4h39m
argocd-applicationset-controller-token-v84pl                                               kubernetes.io/service-account-token   3      4h29m
argocd-dex-server-token-g76wx                                                              kubernetes.io/service-account-token   3      4h39m
argocd-initial-admin-secret                                                                Opaque                                1      4h38m
argocd-redis-token-m94v5                                                                   kubernetes.io/service-account-token   3      4h39m
argocd-secret                                                                              Opaque                                5      4h39m
argocd-server-token-rqn9r                                                                  kubernetes.io/service-account-token   3      4h39m
# "cluster-"で始まるSecretが作成される
cluster-c68e805bffb0dd155295b27b15cbf8ac.gr7.ap-northeast-1.eks.amazonaws.com-3093402059   Opaque                                3      90s
default-token-qc648                                                                        kubernetes.io/service-account-token   3      4h39m

List generator

ここでは以下のマニフェストファイルを使用します。これは先ほどのマニフェストファイルに外部クラスター情報を追加したものです。

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: guestbook
  namespace: argocd
spec:
  generators:
  - list:
      elements:
      - cluster: in-cluster
        url: https://kubernetes.default.svc
      # 追加
      - cluster: external-cluster
        url: https://C68E805BFFB0DD155295B27B15CBF8AC.gr7.ap-northeast-1.eks.amazonaws.com
  template:
    metadata:
      name: '{{cluster}}-guestbook'
    spec:
      project: default
      source:
        repoURL: https://github.com/argoproj/argocd-example-apps.git
        targetRevision: HEAD
        path: guestbook
      destination:
        server: '{{url}}'
        namespace: default

こちらをデプロイするとApplicationが作成されます。今回は2つのクラスターに対してApplication リソースが作られ、その名称はクラスターと対応しています。

$ kubectl apply -f list-applicationset.yaml 
applicationset.argoproj.io/guestbook created

$ kubectl get appset -n argocd
NAME        AGE
guestbook   14s

$ kubectl get app -n argocd
NAME                         SYNC STATUS   HEALTH STATUS
external-cluster-guestbook   OutOfSync     Missing
in-cluster-guestbook         OutOfSync     Missing

あとはSyncすればデプロイは完了です。

$ argocd app sync external-cluster-guestbook

$ argocd app list
NAME                        CLUSTER                                                                        NAMESPACE  PROJECT  STATUS     HEALTH   SYNCPOLICY  CONDITIONS  REPO
                                    PATH       TARGET
external-cluster-guestbook  https://C68E805BFFB0DD155295B27B15CBF8AC.gr7.ap-northeast-1.eks.amazonaws.com  default    default  Synced     Healthy  <none>      <none>      https://github.com/argoproj/argocd-example-apps.git  guestbook  HEAD
in-cluster-guestbook        https://kubernetes.default.svc                                                 default    default  OutOfSync  Missing  <none>      <none>      https://github.com/argoproj/argocd-example-apps.git  guestbook  HEAD

# 操作対象のクラスターを切り替える
$ kubectl config use-context <external cluster name>

$ kubectl get pods
NAME                            READY   STATUS    RESTARTS   AGE
guestbook-ui-85985d774c-v7rv6   1/1     Running   0          4m34s

Cluster generator

ここでは外部クラスターのみにデプロイするよう、条件を追加しています。ArgoCDにクラスターを登録した際に作られるSecretには argocd.argoproj.io/secret-type=cluster というタグが付与されており、これを含むクラスターだけを対象とします。

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: guestbook
  namespace: argocd
spec:
  generators:
  - clusters:
    # 追加
      selector:
        matchLabels:
          argocd.argoproj.io/secret-type: cluster
  template:
    metadata:
      name: 'external-guestbook'
    spec:
      project: "default"
      source:
        repoURL: https://github.com/argoproj/argocd-example-apps/
        targetRevision: HEAD
        path: guestbook
      destination:
        server: '{{server}}'
        namespace: default

なお spec.template.metadata.name の名称を {{name}}-guestbook から external-guestbook に変更していますが、クラスターの名称が長すぎるとSync時に以下のようなエラーが発生するため変更しています。

# エラーの例
$ argocd app sync 

(中略)

Deployment.apps "guestbook-ui" is invalid: metadata.labels: Invalid value: "<cluster name>": must be no more than 63 characters

デプロイ後は以下の通りです。

$ kubectl apply -f cluster-applicationset.yaml

$ argocd app sync external-guestbook

$ kubectl config use-context <external cluster>

$ kubectl get pods
NAME                            READY   STATUS    RESTARTS   AGE
guestbook-ui-85985d774c-v4xqc   1/1     Running   0          36s

Matrix generator

Matrix generatorは2種類のgeneratorを組み合わせるgeneratorです。今回は Cluster generator と Git generator を組み合わせたパターンにしています。

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: cluster-git
  namespace: argocd
spec:
  generators:
    - matrix:
        generators:
          - git:
              repoURL: https://github.com/argoproj-labs/applicationset.git
              revision: HEAD
              directories:
              - path: examples/matrix/cluster-addons/*
          - clusters:
              selector:
                matchLabels:
                  argocd.argoproj.io/secret-type: cluster
  template:
    metadata:
      name: '{{path.basename}}-external'
    spec:
      project: default
      source:
        repoURL: https://github.com/argoproj-labs/applicationset.git
        targetRevision: HEAD
        path: '{{path}}'
      destination:
        server: '{{server}}'
        namespace: default

※ 参考:ApplicationSet Controller - Matrix Generator

デプロイ後は以下の通りです。

$ kubectl apply -f matrix-applicationset.yaml
applicationset.argoproj.io/cluster-git created

$ kubectl get appset -n argocd
NAME          AGE
cluster-git   11s

$ kubectl get app -n argocd
NAME                           SYNC STATUS   HEALTH STATUS
argo-workflows-external        OutOfSync     Missing
prometheus-operator-external   OutOfSync     Missing

Flaggerに入門する

今回はProgressive Deliveryを実現するツールの一つであるFlaggerを試してみます。なお、Progressive Deliveryについては、以下の参考リンクなどを参照ください。

※参考リンク:

Flaggerとは

Flaggerは Progressive Dlivery Operator for Kubernetes と打ち出されている通り、KubernetesをベースにProgressive Deliveryの実現をサポートするプロダクトです。

Flaggerを利用する環境は、以下のような構成となります。

※画像:Flagger Docより

flagger

Flaggerの実現するのは大きく3つ紹介されており、 Progressive Deliveryによる「より安全なリリース」 サービスメッシュやIngress Controllerによる「柔軟なTraffic Routing」 Progressive Deliveryに用いるメトリックをカスタムすることも可能な「拡張性のあるValidation」 となります。

具体的な機能としては、以下のようなものが挙げられます。

サービスメッシュ・Ingress Controllerと組み合わせたトラフィックコントロール

FlaggerはKubernetes CNIと組み合わせることで、単体でもトラフィックの切り替えを行うことができますが、Istio/Linkerdといったサービスメッシュ、Nginx/ContourなどのIngress Controllerとして機能するプロダクトと組み合わせることで、CanaryリリースやA/Bテストなど、より発展的なデリバリーを実現します。

GitOpsツールとの組み合わせ

FlaggerはGitOpsプロダクトと組み合わせることで、GitOps + Progressive Deliveryというデプロイ・リリースパイプラインを実現します。以前動かしてみたFluxなどと組み合わせることが可能です

4種類のデプロイ・デリバリー戦略に対応

Flaggerは Canary というCustom Resourceの設定を変更することで、4種類のデプロイ・デリバリー戦略に対応しています。

  • Canary Release: HTTPリクエスト成功率などの重要な指標(KPI、Key Performace Indicator)を計測しつつ、異なるバージョンのアプリケーションへのトラフィック量を少しずつ変更します。
  • A/B Testing: Session Affinity (Sticky Session)を要求するようなフロントエンドのアプリケーションに対応するため、Canary ReleaseにHTTPヘッダーやCookieの条件一致を加えることで、同じユーザーが同一のバージョンにアクセスし続けるようトラフィックを制御し、A/Bテストを実現します。
  • Blue/Green: KPIなどをベースに新しいバージョンのアプリケーションに問題がないと判断できたら、一気にトラフィックを新しいバージョンのほうへ切り替えます。
  • Blue/Green with Traffic Mirroring: Canary ReleaseやBlue/Greenの前段階として利用します。インバウンドな通信をコピーして、一方は現在稼働中のServiceに、もう一方をCanary Serviceへ転送します。Canary Serviceからのレスポンスは破棄しますが、メトリックは収集し、Canaryのほうが問題ない状態かを確認することができます。

※参考リンク:Flagger Doc - Deployment Strategies

モニタリングツールへのクエリ

Flaggerはメトリックをベースにして、可用性やエラー率などのSLOを評価します。Flaggerは HTTPリクエスト成功率Duration を組み込みのメトリックとして持ちますが、カスタムすることも可能です。メトリックのターゲットにはPrometheusやDataDogを利用できます。

※参考リンク:Flagger Doc - Metrics Analysis

WebhookによるAnalysisの拡張

Flaggerでは、アプリケーション切替の前後に行うテスト(Analysis)を、Webhookによって拡張することができます。Flaggerでは8種類のHookが用意されており、そのレスポンスコードによって切り替えが発生します。またFlaggerではWebhhokの設定でコマンドを指定することで、切り替え前後に負荷テストなどを実施できます。

※参考リンク:Flagger Doc - Webhooks

通知サービスへのアラート

FlaggerはSlackやMicrosoft Teamsなどの各種チャットツールへアラートを送信することができます。送信先のプロバイダーは AlertProvider というCustom Resourceで定義します。

※参考リンク:Flagger Doc - Alerting

Flaggerを動かしてみる

ここから実際にFlaggerを動かします。Flaggerのドキュメントには、各デプロイ戦略とツールとの組み合わせごとにチュートリアルが用意されています。今回はIstioをベースに2種類のデプロイ戦略をなぞってみます。Istioについては、以前の投稿などをご参照ください。環境の用意はこちらのドキュメントをベースにしています。

※参考リンク:

前提条件

Flaggerを利用する前提条件は以下の通りです。

  • Kubernetes version: 1.16 以上
  • istio version: 1.5 以上

今回の検証環境は以下の通りです。

# eksctl version
$ eksctl version
0.57.0

# Kubernetes / kubectl version
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.0", GitCommit:"af46c47ce925f4c4ad5cc8d1fca46c7b77d13b38", GitTreeState:"clean", BuildDate:"2020-12-08T17:59:43Z", GoVersion:"go1.15.5", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"20+", GitVersion:"v1.20.7-eks-8be107", GitCommit:"8be107e517a77bde856aced857f3428e4f344969", GitTreeState:"clean", BuildDate:"2021-06-12T03:14:13Z", GoVersion:"go1.15.12", Compiler:"gc", Platform:"linux/amd64"}

# Istio version
$ istioctl version
no running Istio pods in "istio-system"
1.10.3

Istio / Prometheus / Flaggerのデプロイ

まずはIstioのデプロイを行います。Istioは istioctl コマンドからデプロイを行います。今回使用しているProfileはこちらから確認できます。なお、Istioインストールの際、istiodに要求されるメモリ量が多いため、nodeのスペックは十分大きいものを指定したほうが良いでしょう。今回はEC2インスタンスのsペックは t3.large を指定しています。

$ istioctl manifest install --set profile=default
This will install the Istio 1.10.3 default profile with ["Istio core" "Istiod" "Ingress gateways"] components into the cluster. Proceed? (y/N) y
✔ Istio core installed                                                                                                 
✔ Istiod installed                                                                                                     
✔ Ingress gateways installed                                                                                           
✔ Installation complete     


# デプロイ後の確認
$ kubectl get pods -n istio-system
NAME                                   READY   STATUS    RESTARTS   AGE
istio-ingressgateway-c4d8648bc-pkm72   1/1     Running   0          59s
istiod-7bd65bd549-76xgz                1/1     Running   0          71s


$ kubectl get svc -n istio-system
NAME                   TYPE           CLUSTER-IP       EXTERNAL-IP                                                                    PORT(S)                                      AGE
istio-ingressgateway   LoadBalancer   10.100.230.248   a50db11ad30db428b926a77884d22fa1-1849188677.ap-northeast-1.elb.amazonaws.com   15021:30212/TCP,80:31556/TCP,443:32089/TCP   96s
istiod                 ClusterIP      10.100.89.234    <none>                                                                         15010/TCP,15012/TCP,443/TCP,15014/TCP        108s

次にPrometheusをデプロイします。

$ kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.10/samples/addons/prometheus.yaml
serviceaccount/prometheus created
configmap/prometheus created
clusterrole.rbac.authorization.k8s.io/prometheus created
clusterrolebinding.rbac.authorization.k8s.io/prometheus created
service/prometheus created
deployment.apps/prometheus created


# デプロイ後の確認
$ kubectl get pods -n istio-system
NAME                                   READY   STATUS    RESTARTS   AGE
istio-ingressgateway-c4d8648bc-pkm72   1/1     Running   0          3m19s
istiod-7bd65bd549-76xgz                1/1     Running   0          3m31s
prometheus-69f7f4d689-bz2z5            2/2     Running   0          15s

$ kubectl get svc -n istio-system
NAME                   TYPE           CLUSTER-IP       EXTERNAL-IP                                                                    PORT(S)                                      AGE
istio-ingressgateway   LoadBalancer   10.100.230.248   a50db11ad30db428b926a77884d22fa1-1849188677.ap-northeast-1.elb.amazonaws.com   15021:30212/TCP,80:31556/TCP,443:32089/TCP   3m32s
istiod                 ClusterIP      10.100.89.234    <none>                                                                         15010/TCP,15012/TCP,443/TCP,15014/TCP        3m44s
prometheus             ClusterIP      10.100.238.59    <none>                                                                         9090/TCP                                     28s

最後にFlaggerをデプロイします。

$ kubectl apply -k github.com/fluxcd/flagger//kustomize/istio
customresourcedefinition.apiextensions.k8s.io/alertproviders.flagger.app created
customresourcedefinition.apiextensions.k8s.io/canaries.flagger.app created
customresourcedefinition.apiextensions.k8s.io/metrictemplates.flagger.app created
serviceaccount/flagger created
clusterrole.rbac.authorization.k8s.io/flagger created
clusterrolebinding.rbac.authorization.k8s.io/flagger created
deployment.apps/flagger created


# デプロイ後の確認
$ kubectl get pods -n istio-system
NAME                                   READY   STATUS    RESTARTS   AGE
flagger-5cf787b9c8-jgdtv               1/1     Running   0          44s
istio-ingressgateway-c4d8648bc-pkm72   1/1     Running   0          6m15s
istiod-7bd65bd549-76xgz                1/1     Running   0          6m27s
prometheus-69f7f4d689-bz2z5            2/2     Running   0          3m11s

Canary Deployment

まずはCanary Deploymentを試します。手順はこちらのドキュメントに記載されています。

前準備

まず使用するデモ用アプリが、サービスメッシュの外からアクセスできるよう Gateway リソースをデプロイします。


ingress-gateway.yaml

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: public-gateway
  namespace: istio-system
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
        - "*"


$ kubectl apply -f ingress-gateway.yaml
gateway.networking.istio.io/public-gateway created


# デプロイ後の確認
$ kubectl get gateway -n istio-system
NAME             AGE
public-gateway   9s

次にテスト用アプリを起動するNamespaceの作成と、IstioによるSidecar Proxyの注入を行うためのラベリングを行います。

$ kubectl create ns test
namespace/test created

$ kubectl label namespace test istio-injection=enabled
namespace/test labeled

# デプロイ後の確認
$ kubectl get ns --show-labels
NAME              STATUS   AGE   LABELS
default           Active   28m   <none>
istio-system      Active   13m   <none>
kube-node-lease   Active   28m   <none>
kube-public       Active   28m   <none>
kube-system       Active   28m   <none>
test              Active   82s   istio-injection=enabled

次にテスト用のアプリとHPAをデプロイします。なお、今回の手順ではHPAは特に利用しないため、 metrics-server のインストールは実施しません。そのためHPAのターゲットも Unknown 状態となります。

$ kubectl apply -k https://github.com/fluxcd/flagger//kustomize/podinfo?ref=main
deployment.apps/podinfo created
horizontalpodautoscaler.autoscaling/podinfo created


# デプロイ後の確認
$ kubectl get pods -n test
NAME                      READY   STATUS    RESTARTS   AGE
podinfo-99dc84b6f-bmtvv   2/2     Running   0          56s
podinfo-99dc84b6f-l5pk8   2/2     Running   0          71s

$ kubectl get hpa -n test
NAME      REFERENCE            TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
podinfo   Deployment/podinfo   <unknown>/99%   2         4         2          98s

また、Progressive DeliveryでのAnalysisで利用する loadtester Podも合わせてデプロイします。この loadtesterrakyll/hey / bojand/ghz という2つの負荷テストツールをベースとしています。

$ kubectl apply -k https://github.com/fluxcd/flagger//kustomize/tester?ref=main
service/flagger-loadtester created
deployment.apps/flagger-loadtester created


# デプロイ後の確認
$ kubectl get pods -n test
NAME                                  READY   STATUS    RESTARTS   AGE
flagger-loadtester-64695f854f-6mkgj   2/2     Running   0          29s
podinfo-99dc84b6f-bmtvv               2/2     Running   0          4m25s
podinfo-99dc84b6f-l5pk8               2/2     Running   0          4m40s

$ kubectl get svc -n test
NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
flagger-loadtester   ClusterIP   10.100.21.132   <none>        80/TCP    25s

Canary リソースのデプロイ

Flaggerは Canary というCustom Resourceを利用し、既存のDeploymentに対して、指定したデプロイ戦略を実行します。どのデプロイ戦略を利用するかは、 Canary 中に指定したパラメータによって変更します。

今回利用する Canaryマニフェストファイルは以下の通りです。少しだけパラメータの説明を付け加えます。

  • spec.targetRef: ターゲットとするリソース種別を指定。 Deployment DaemonSet のいずれかを指定可能
  • spec.progressDeadlineSeconds: Canary Deployment実行時、ロールバック前に進行する際の最大秒数。 kubectl get canary 実行時にCanary Deploymentの進行を表示する間隔。
  • spec.analysis: Progressive DeliveryのAnalysisを設定する部分。
    • interval: analysisの実行間隔
    • threshold: ロールバックを実行する基準となる、analysisの失敗回数
    • maxWeight: Canaryへ転送する最大のトラフィック転送割合
    • stepWeight: Canaryへの転送量を1回あたりに増やす割合。
    • metrics: メトリックの設定箇所
      • thresholdRange: request-success-rate の場合はリクエスト成功率の最低値、 request-duration の場合はリクエストのP99 duration最大ミリセカンド値を指定
    • webhooks: Webhookの設定

podinfo-canary.yaml

apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: podinfo
  namespace: test
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: podinfo
  progressDeadlineSeconds: 60
  autoscalerRef:
    apiVersion: autoscaling/v2beta2
    kind: HorizontalPodAutoscaler
    name: podinfo
  service:
    port: 9898
    targetPort: 9898
    gateways:
    - public-gateway.istio-system.svc.cluster.local
    hosts:
    - app.example.com
    trafficPolicy:
      tls:
        mode: DISABLE
    retries:
      attempts: 3
      perTryTimeout: 1s
      retryOn: "gateway-error,connect-failure,refused-stream"
  analysis:
    interval: 1m
    threshold: 5
    maxWeight: 50
    stepWeight: 10
    metrics:
    - name: request-success-rate
      thresholdRange:
        min: 99
      interval: 1m
    - name: request-duration
      thresholdRange:
        max: 500
      interval: 30s
    webhooks:
      - name: acceptance-test
        type: pre-rollout
        url: http://flagger-loadtester.test/
        timeout: 30s
        metadata:
          type: bash
          cmd: "curl -sd 'test' http://podinfo-canary:9898/token | grep token"
      - name: load-test
        url: http://flagger-loadtester.test/
        timeout: 5s
        metadata:
          cmd: "hey -z 1m -q 10 -c 2 http://podinfo-canary.test:9898/"


上記マニフェストファイルをデプロイすると、先ほど作成した podinfo Podを置き換える形で podinfo-primary というPodが作成されます。またPod以外にも以下のリソースが作成されます。

  • Deployment / Pod: podinfo-primary
  • HPA: podinfo-primary
  • Service: podinfo podinfo-canary podinfo-primary
  • Destination Rule: podinfo-canary podinfo-primary
  • Virtual Service: podinfo
$ kubectl apply -f podinfo-canary.yaml
canary.flagger.app/podinfo created


# デプロイ後の確認
$ kubectl get canary -n test
NAME      STATUS        WEIGHT   LASTTRANSITIONTIME
podinfo   Initialized   0        2021-07-21T10:02:46Z

$ kubectl get pods -n test
NAME                                  READY   STATUS    RESTARTS   AGE
flagger-loadtester-64695f854f-6mkgj   2/2     Running   0          30m
podinfo-primary-657f8c97dd-28lbv      2/2     Running   0          2m31s
podinfo-primary-657f8c97dd-z7sx6      2/2     Running   0          2m31s

$ kubectl get hpa -n test
NAME              REFERENCE                    TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
podinfo           Deployment/podinfo           <unknown>/99%   2         4         0          34m
podinfo-primary   Deployment/podinfo-primary   <unknown>/99%   2         4         2          70s

$ kubectl get service -n test
NAME                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
flagger-loadtester   ClusterIP   10.100.21.132    <none>        80/TCP     29m
podinfo              ClusterIP   10.100.172.155   <none>        9898/TCP   9s
podinfo-canary       ClusterIP   10.100.249.86    <none>        9898/TCP   69s
podinfo-primary      ClusterIP   10.100.129.195   <none>        9898/TCP   69s

$ kubectl get destinationrule -n test
NAME              HOST              AGE
podinfo-canary    podinfo-canary    57s
podinfo-primary   podinfo-primary   57s

$ kubectl get virtualservice -n test
NAME      GATEWAYS                                            HOSTS                           AGE
podinfo   ["public-gateway.istio-system.svc.cluster.local"]   ["app.example.com","podinfo"]   43s

またこの時 Canary リソースのログを見ると、Initializationのプロセスが確認できます。

$ kubectl describe canary podinfo -n test

(中略)

Events:
  Type     Reason  Age                    From     Message
  ----     ------  ----                   ----     -------
  Warning  Synced  4m41s                  flagger  podinfo-primary.test not ready: waiting for rollout to finish: observed deployment generation less then desired generation
  Normal   Synced  3m41s (x2 over 4m41s)  flagger  all the metrics providers are available!
  Normal   Synced  3m41s                  flagger  Initialization done! podinfo.test

これでProgressive Deliveryを実行する準備ができました。

アプリケーションのアップデート

ここからCanary Deploymentを実行します。デプロイした podinfoのイメージを更新することでアップデートを開始すると、Analysisを実行した後にPodのアップデートを行います。

Canary Deploymentの場合は以下のような構成となります。

※画像: Flagger Docより

canary-deployment

Flaggerでは Primary Canary という2種類のPodを利用します。Canary PodはAnalysisを実行する対象のPodで、アップデート中に作成し、完了後に削除されます。 Primary Podは実際にトラフィックを流す対象のPodで、Analysisをクリアすると Primary Podをバージョンアップすることで、アップデートが完了します。

Canary Deployment実行時のFlaggerの処理は、以下のように進行します。基本的な流れはCanary DeploymentもBlue/Green Deploymentも共通です。

  • Progressing フェーズ: Analysisの実行
    • 新しいイメージバージョンのCanary Podを作成
    • Canary Podへトラフィックを転送する。この時メトリックも収集し、評価する
    • Canary Podへの転送量を maxWeight まで増加する
  • Promoting フェーズ: Analysisの完了後、Primaryのスペックを更新
    • 新しいイメージタグのPrimary Podを作成する(Secret等のリソースがある場合はそれも複製する)
    • 古いPrimary Podを削除する
  • Finalising フェーズ: トラフィックをPrimary側へ切り替える
  • Succeeded フェーズ: Analysisとアップデートの完了
    • Canary Podを削除する

※参考リンク:GitHub - flux/flagger: status.go

アップデート前のイメージタグは 3.1.0 です。

$ kubectl describe pod podinfo-primary-657f8c97dd-28lbv -n test | grep 'Image:'
    Image:         docker.io/istio/proxyv2:1.10.3
    Image:         stefanprodan/podinfo:3.1.0
    Image:         docker.io/istio/proxyv2:1.10.3

アップデート実行前に、PodとCanaryのウォッチを開始しておきます。

# watch
$ kubectl get canary -n test -w
$ kubectl get pods -n test -w

# イメージアップデート
kubectl -n test set image deployment/podinfo podinfod=stefanprodan/podinfo:3.1.1

ここからフェーズごとの推移を載せておきます。

Progressing

# Progressing
## Weightが徐々に増加する
$ kubectl get canary -n test -w
NAME      STATUS        WEIGHT   LASTTRANSITIONTIME
podinfo   Initialized   0        2021-07-21T10:02:46Z
podinfo   Progressing   0        2021-07-21T10:13:46Z
podinfo   Progressing   10       2021-07-21T10:14:46Z
podinfo   Progressing   20       2021-07-21T10:15:46Z
podinfo   Progressing   30       2021-07-21T10:16:46Z
podinfo   Progressing   40       2021-07-21T10:17:46Z
podinfo   Progressing   50       2021-07-21T10:18:46Z


## Canary Podが作成される
$ kubectl get pods -n test -w
NAME                                  READY   STATUS    RESTARTS   AGE
flagger-loadtester-64695f854f-6mkgj   2/2     Running   0          39m
podinfo-primary-657f8c97dd-28lbv      2/2     Running   0          11m
podinfo-primary-657f8c97dd-z7sx6      2/2     Running   0          11m


podinfo-5f8fd4f546-vtjvl              0/2     Pending   0          0s
podinfo-5f8fd4f546-vtjvl              0/2     Pending   0          0s
podinfo-5f8fd4f546-vtjvl              0/2     Init:0/1   0          0s
podinfo-5f8fd4f546-vtjvl              0/2     PodInitializing   0          1s
podinfo-5f8fd4f546-6mqz8              0/2     Pending           0          0s
podinfo-5f8fd4f546-6mqz8              0/2     Pending           0          0s
podinfo-5f8fd4f546-6mqz8              0/2     Init:0/1          0          0s
podinfo-5f8fd4f546-6mqz8              0/2     Init:0/1          0          1s
podinfo-5f8fd4f546-6mqz8              0/2     PodInitializing   0          2s
podinfo-5f8fd4f546-vtjvl              0/2     Running           0          8s
podinfo-5f8fd4f546-vtjvl              1/2     Running           0          9s
podinfo-5f8fd4f546-6mqz8              0/2     Running           0          7s
podinfo-5f8fd4f546-6mqz8              1/2     Running           0          9s
podinfo-5f8fd4f546-vtjvl              2/2     Running           0          16s
podinfo-5f8fd4f546-6mqz8              2/2     Running           0          14s

Promoting

# Promoting
NAME      STATUS        WEIGHT   LASTTRANSITIONTIME
podinfo   Promoting     0        2021-07-21T10:19:46Z


## Primary Podを更新
NAME                                  READY   STATUS    RESTARTS   AGE
podinfo-primary-657f8c97dd-z7sx6      2/2     Terminating       0          18m
podinfo-primary-76655b74b9-cvs9z      0/2     Pending           0          0s
podinfo-primary-76655b74b9-cvs9z      0/2     Pending           0          0s
podinfo-primary-76655b74b9-cvs9z      0/2     Init:0/1          0          0s
podinfo-primary-76655b74b9-b99lq      0/2     Pending           0          0s
podinfo-primary-76655b74b9-b99lq      0/2     Pending           0          0s
podinfo-primary-76655b74b9-b99lq      0/2     Init:0/1          0          0s
podinfo-primary-76655b74b9-cvs9z      0/2     PodInitializing   0          2s
podinfo-primary-76655b74b9-b99lq      0/2     PodInitializing   0          3s
podinfo-primary-76655b74b9-cvs9z      0/2     Running           0          3s
podinfo-primary-76655b74b9-b99lq      0/2     Running           0          4s
podinfo-primary-76655b74b9-cvs9z      1/2     Running           0          4s
podinfo-primary-76655b74b9-b99lq      1/2     Running           0          5s
podinfo-primary-657f8c97dd-z7sx6      0/2     Terminating       0          18m
podinfo-primary-657f8c97dd-z7sx6      0/2     Terminating       0          18m
podinfo-primary-657f8c97dd-z7sx6      0/2     Terminating       0          18m
podinfo-primary-76655b74b9-cvs9z      2/2     Running           0          8s
podinfo-primary-76655b74b9-b99lq      2/2     Running           0          13s
podinfo-primary-657f8c97dd-28lbv      2/2     Terminating       0          18m
podinfo-primary-657f8c97dd-28lbv      0/2     Terminating       0          18m
podinfo-primary-657f8c97dd-28lbv      0/2     Terminating       0          18m
podinfo-primary-657f8c97dd-28lbv      0/2     Terminating       0          18m

Finalising Succeeded

# Finalising
NAME      STATUS        WEIGHT   LASTTRANSITIONTIME
podinfo   Finalising    0        2021-07-21T10:20:46Z

# Succeeded
NAME      STATUS        WEIGHT   LASTTRANSITIONTIME
podinfo   Succeeded     0        2021-07-21T10:21:46Z


## Canary Podの削除
NAME                                  READY   STATUS    RESTARTS   AGE
podinfo-5f8fd4f546-6mqz8              2/2     Terminating       0          7m54s
podinfo-5f8fd4f546-vtjvl              2/2     Terminating       0          8m
podinfo-5f8fd4f546-6mqz8              0/2     Terminating       0          8m
podinfo-5f8fd4f546-vtjvl              0/2     Terminating       0          8m6s
podinfo-5f8fd4f546-vtjvl              0/2     Terminating       0          8m7s
podinfo-5f8fd4f546-vtjvl              0/2     Terminating       0          8m7s
podinfo-5f8fd4f546-6mqz8              0/2     Terminating       0          8m7s
podinfo-5f8fd4f546-6mqz8              0/2     Terminating       0          8m7s

最終的には以下のような状態になります。

$ kubectl get canary -n test
NAME      STATUS      WEIGHT   LASTTRANSITIONTIME
podinfo   Succeeded   0        2021-07-21T10:21:46Z

$ kubectl get pods -n test
NAME                                  READY   STATUS    RESTARTS   AGE
flagger-loadtester-64695f854f-6mkgj   2/2     Running   0          52m
podinfo-primary-76655b74b9-b99lq      2/2     Running   0          6m32s
podinfo-primary-76655b74b9-cvs9z      2/2     Running   0          6m32s

podinfo Canaryのログは以下の通りです。

$ kubectl describe canary -n test podinfo

(中略)

Events:
  Type     Reason  Age                From     Message
  ----     ------  ----               ----     -------
  Warning  Synced  47m                flagger  podinfo-primary.test not ready: waiting for rollout to finish: observed deployment generation less then desired generation
  Normal   Synced  46m (x2 over 47m)  flagger  all the metrics providers are available!
  Normal   Synced  46m                flagger  Initialization done! podinfo.test
  Normal   Synced  35m                flagger  New revision detected! Scaling up podinfo.test
  Normal   Synced  34m                flagger  Starting canary analysis for podinfo.test
  Normal   Synced  34m                flagger  Pre-rollout check acceptance-test passed
  Normal   Synced  34m                flagger  Advance podinfo.test canary weight 10
  Normal   Synced  33m                flagger  Advance podinfo.test canary weight 20
  Normal   Synced  32m                flagger  Advance podinfo.test canary weight 30
  Normal   Synced  31m                flagger  Advance podinfo.test canary weight 40
  Normal   Synced  30m                flagger  Advance podinfo.test canary weight 50
  Normal   Synced  29m                flagger  Copying podinfo.test template spec to podinfo-primary.test
  Normal   Synced  27m (x2 over 28m)  flagger  (combined from similar events): Promotion completed! Scaling down podinfo.test

ロールバック

次にロールバックの場合を見てみます。先ほどと同様コンテナイメージを更新後、 flagger-loadtester から特定のエンドポイント (http://podinfo-canary:9898/status/500) を叩き、エラーを発生させます。その結果、Canary Deploymentは失敗・中断され、Canary Podが削除されます。

# watch
$ kubectl get canary -n test -w
$ kubectl get pods -n test -w


# コンテナイメージの更新
$ kubectl -n test set image deployment/podinfo podinfod=stefanprodan/podinfo:3.1.2

flagger-loadtester からcurlを実行します。

$ kubectl get pods -n test
NAME                                  READY   STATUS    RESTARTS   AGE
flagger-loadtester-64695f854f-6mkgj   2/2     Running   0          85m
podinfo-75459749c9-8w9tl              2/2     Running   0          3m47s
podinfo-75459749c9-mqd7m              2/2     Running   0          3m51s
podinfo-primary-76655b74b9-b99lq      2/2     Running   0          39m
podinfo-primary-76655b74b9-cvs9z      2/2     Running   0          39m


# エラーを発生させる
$ kubectl exec -it flagger-loadtester-64695f854f-6mkgj -n test sh
/home/app $ watch curl http://podinfo-canary:9898/status/500

Deployment Failed

# Canary
NAME      STATUS      WEIGHT   LASTTRANSITIONTIME
podinfo   Succeeded   0        2021-07-21T10:21:46Z

podinfo   Progressing   0        2021-07-21T10:55:46Z

## Analysisが成功しないためにWEIGHTの値が変化しない
podinfo   Progressing   10       2021-07-21T10:56:46Z

podinfo   Progressing   10       2021-07-21T10:57:46Z
podinfo   Progressing   10       2021-07-21T10:58:46Z
podinfo   Progressing   10       2021-07-21T10:59:46Z
podinfo   Progressing   10       2021-07-21T11:00:46Z

podinfo   Progressing   10       2021-07-21T11:01:46Z

## 最終的にはFailedとなる
podinfo   Failed        0        2021-07-21T11:02:46Z


# Pod
NAME                                  READY   STATUS    RESTARTS   AGE
flagger-loadtester-64695f854f-6mkgj   2/2     Running   0          81m
podinfo-primary-76655b74b9-b99lq      2/2     Running   0          35m
podinfo-primary-76655b74b9-cvs9z      2/2     Running   0          35m

## Canary Podの作成
podinfo-75459749c9-mqd7m              0/2     Pending   0          0s
podinfo-75459749c9-mqd7m              0/2     Pending   0          0s
podinfo-75459749c9-mqd7m              0/2     Init:0/1   0          0s
podinfo-75459749c9-mqd7m              0/2     PodInitializing   0          2s
podinfo-75459749c9-8w9tl              0/2     Pending           0          0s
podinfo-75459749c9-8w9tl              0/2     Pending           0          0s
podinfo-75459749c9-8w9tl              0/2     Init:0/1          0          0s
podinfo-75459749c9-8w9tl              0/2     PodInitializing   0          2s
podinfo-75459749c9-8w9tl              0/2     Running           0          8s
podinfo-75459749c9-8w9tl              1/2     Running           0          10s
podinfo-75459749c9-mqd7m              0/2     Running           0          15s
podinfo-75459749c9-mqd7m              1/2     Running           0          16s
podinfo-75459749c9-8w9tl              2/2     Running           0          19s
podinfo-75459749c9-mqd7m              2/2     Running           0          24s



## Canary Podの削除
podinfo-75459749c9-mqd7m              2/2     Terminating       0          7m
podinfo-75459749c9-8w9tl              2/2     Terminating       0          6m56s
podinfo-75459749c9-mqd7m              0/2     Terminating       0          7m6s
podinfo-75459749c9-8w9tl              0/2     Terminating       0          7m2s
podinfo-75459749c9-mqd7m              0/2     Terminating       0          7m7s
podinfo-75459749c9-mqd7m              0/2     Terminating       0          7m7s
podinfo-75459749c9-8w9tl              0/2     Terminating       0          7m11s
podinfo-75459749c9-8w9tl              0/2     Terminating       0          7m11s

最終的には以下のような状態となります。

$ kubectl get canary -n test
NAME      STATUS   WEIGHT   LASTTRANSITIONTIME
podinfo   Failed   0        2021-07-21T11:02:46Z

$ kubectl get pods -n test
NAME                                  READY   STATUS    RESTARTS   AGE
flagger-loadtester-64695f854f-6mkgj   2/2     Running   0          91m
podinfo-primary-76655b74b9-b99lq      2/2     Running   0          45m
podinfo-primary-76655b74b9-cvs9z      2/2     Running   0          45m

podinfo Canaryのログは以下の通りです。

$ kubectl describe canary -n test podinfo

(中略)

Events:
  Type     Reason  Age                  From     Message
  ----     ------  ----                 ----     -------
  Normal   Synced  53m                  flagger  Advance podinfo.test canary weight 20
  Normal   Synced  52m                  flagger  Advance podinfo.test canary weight 30
  Normal   Synced  51m                  flagger  Advance podinfo.test canary weight 40
  Normal   Synced  50m                  flagger  Advance podinfo.test canary weight 50
  Normal   Synced  49m                  flagger  Copying podinfo.test template spec to podinfo-primary.test
  Normal   Synced  47m (x2 over 48m)    flagger  (combined from similar events): Promotion completed! Scaling down podinfo.test
  Normal   Synced  13m (x2 over 55m)    flagger  New revision detected! Scaling up podinfo.test
  Normal   Synced  12m (x2 over 54m)    flagger  Advance podinfo.test canary weight 10
  Normal   Synced  12m (x2 over 54m)    flagger  Pre-rollout check acceptance-test passed
  Normal   Synced  12m (x2 over 54m)    flagger  Starting canary analysis for podinfo.test
  Warning  Synced  10m                  flagger  Halt podinfo.test advancement success rate 97.50% < 99%
  Warning  Synced  8m25s                flagger  Halt podinfo.test advancement success rate 97.61% < 99%
  Warning  Synced  7m25s (x3 over 11m)  flagger  Halt podinfo.test advancement success rate 0.00% < 99%
  Warning  Synced  6m25s                flagger  Rolling back podinfo.test failed checks threshold reached 5
  Warning  Synced  6m25s                flagger  Canary failed! Scaling down podinfo.test

Blue/Green Deployment

続いてBlue/Green Deploymentを試します。Canary Deploymentから連続して実施する場合は、まず podinfo Canaryリソースを一度削除します。

$ kubectl delete -f podinfo-canary.yaml
canary.flagger.app "podinfo" deleted

podinfo Canaryを削除すると、作成時に一緒に作られたPodなどのリソースも合わせて削除されます。また、Canary作成時に置き換えられた podinfo Podは、Canaryを削除しても復旧されません。

# podinfo Podは再作成されない
$ kubectl get pods -n test
NAME                                  READY   STATUS    RESTARTS   AGE
flagger-loadtester-64695f854f-6mkgj   2/2     Running   0          126m

$ kubectl get deploy -n test
NAME                 READY   UP-TO-DATE   AVAILABLE   AGE
flagger-loadtester   1/1     1            1           126m
podinfo              0/0     0            0           130m

ここでは podinfo Deploymentを編集して podinfo Podを再作成します。編集したのはレプリカ数とコンテナイメージタグの2か所です。

$ kubectl edit deploy podinfo -n test
deployment.apps/podinfo edited


# Deployment編集後
$ kubectl get pods -n test
NAME                                  READY   STATUS    RESTARTS   AGE
flagger-loadtester-64695f854f-h9jdv   2/2     Running   0          33m
podinfo-99dc84b6f-svnvg               2/2     Running   0          25s
podinfo-99dc84b6f-vmv7n               2/2     Running   0          25s

Canary リソースのデプロイ

次に新しいCanaryリソースをデプロイします。Canary Deploymentの際に利用したものと比べ、Analysis部分に maxWeight stepWeight がありません。Weightの設定がないため、Canary Podへトラフィックを流すことなく、spec.analysis.iterations の回数だけAnalysisを実行、チェックを通過したら新しいバージョンのPrimary Podを作成してトラフィックを切り替えます。


podinfo-bg.yaml

apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: podinfo
  namespace: test
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: podinfo
  progressDeadlineSeconds: 60
  autoscalerRef:
    apiVersion: autoscaling/v2beta2
    kind: HorizontalPodAutoscaler
    name: podinfo
  service:
    port: 9898
    targetPort: 9898
    gateways:
    - public-gateway.istio-system.svc.cluster.local
    hosts:
    - app.example.com
    trafficPolicy:
      tls:
        mode: DISABLE
    retries:
      attempts: 3
      perTryTimeout: 1s
      retryOn: "gateway-error,connect-failure,refused-stream"
  analysis:
    interval: 1m
    threshold: 5
    iterations: 10
    metrics:
    - name: request-success-rate
      thresholdRange:
        min: 99
      interval: 1m
    - name: request-duration
      thresholdRange:
        max: 500
      interval: 30s
    webhooks:
      - name: acceptance-test
        type: pre-rollout
        url: http://flagger-loadtester.test/
        timeout: 30s
        metadata:
          type: bash
          cmd: "curl -sd 'test' http://podinfo-canary:9898/token | grep token"
      - name: load-test
        url: http://flagger-loadtester.test/
        timeout: 5s
        metadata:
          cmd: "hey -z 1m -q 10 -c 2 http://podinfo-canary.test:9898/"


まずは上記マニフェストファイルをデプロイします。

$ kubectl apply -f podinfo-bg.yaml
canary.flagger.app/podinfo created


# デプロイ後の確認
$ kubectl get pods -n test
NAME                                  READY   STATUS    RESTARTS   AGE
flagger-loadtester-64695f854f-h9jdv   2/2     Running   0          38m
podinfo-primary-7b67c7fbc4-4fhm6      2/2     Running   0          82s
podinfo-primary-7b67c7fbc4-w5jpt      2/2     Running   0          82s

$ kubectl get canary -n test
NAME      STATUS        WEIGHT   LASTTRANSITIONTIME
podinfo   Initialized   0        2021-07-22T00:46:56Z

アプリケーションのアップデート

次にアプリケーションを更新します。

Canary Deploymentと同様、イメージタグの更新を行います。

$ kubectl -n test set image deployment/podinfo podinfod=stefanprodan/podinfo:3.1.1
deployment.apps/podinfo image updated

PodとCanaryの経過を見てみます。Canary Deploymentと異なり、こちらのログからは経過が確認できませんが、 kubectl describe canary でのEventsにはAnalysisが進行している様子が確認できます。

Progressing

# Progressing
$ kubectl get canary -n test -w
NAME      STATUS        WEIGHT   LASTTRANSITIONTIME
podinfo   Initialized   0        2021-07-22T00:46:56Z

podinfo   Progressing   0        2021-07-22T00:47:56Z


$ kubectl get pods -n test  -w
NAME                                  READY   STATUS    RESTARTS   AGE
flagger-loadtester-64695f854f-h9jdv   2/2     Running   0          38m
podinfo-primary-7b67c7fbc4-4fhm6      2/2     Running   0          93s
podinfo-primary-7b67c7fbc4-w5jpt      2/2     Running   0          93s


## Canary Podの作成
podinfo-5f8fd4f546-5tbmv              0/2     Pending   0          0s
podinfo-5f8fd4f546-5tbmv              0/2     Pending   0          0s
podinfo-5f8fd4f546-5tbmv              0/2     Init:0/1   0          0s
podinfo-5f8fd4f546-5tbmv              0/2     PodInitializing   0          2s
podinfo-5f8fd4f546-5tbmv              0/2     Running           0          3s
podinfo-5f8fd4f546-5tbmv              1/2     Running           0          3s
podinfo-5f8fd4f546-bkxbz              0/2     Pending           0          0s
podinfo-5f8fd4f546-bkxbz              0/2     Pending           0          1s
podinfo-5f8fd4f546-bkxbz              0/2     Init:0/1          0          1s
podinfo-5f8fd4f546-bkxbz              0/2     PodInitializing   0          3s
podinfo-5f8fd4f546-bkxbz              0/2     Running           0          4s
podinfo-5f8fd4f546-bkxbz              1/2     Running           0          5s
podinfo-5f8fd4f546-bkxbz              2/2     Running           0          9s
podinfo-5f8fd4f546-5tbmv              2/2     Running           0          16s


## Iterationが進行している
$ kubectl describe canary podinfo -n test | tail -10
Events:
  Type     Reason  Age                    From     Message
  ----     ------  ----                   ----     -------
  Warning  Synced  3m24s                  flagger  podinfo-primary.test not ready: waiting for rollout to finish: observed deployment generation less then desired generation
  Normal   Synced  2m24s (x2 over 3m24s)  flagger  all the metrics providers are available!
  Normal   Synced  2m24s                  flagger  Initialization done! podinfo.test
  Normal   Synced  84s                    flagger  New revision detected! Scaling up podinfo.test
  Normal   Synced  24s                    flagger  Starting canary analysis for podinfo.test
  Normal   Synced  24s                    flagger  Pre-rollout check acceptance-test passed
  Normal   Synced  24s                    flagger  Advance podinfo.test canary iteration 1/10

$ kubectl describe canary podinfo -n test | tail -10
  Normal   Synced  11m (x2 over 12m)    flagger  all the metrics providers are available!
  Normal   Synced  11m                  flagger  Initialization done! podinfo.test
  Normal   Synced  10m                  flagger  New revision detected! Scaling up podinfo.test
  Normal   Synced  9m26s                flagger  Starting canary analysis for podinfo.test
  Normal   Synced  9m26s                flagger  Pre-rollout check acceptance-test passed
  Normal   Synced  9m26s                flagger  Advance podinfo.test canary iteration 1/10
  Normal   Synced  8m26s                flagger  Advance podinfo.test canary iteration 2/10
  Normal   Synced  7m26s                flagger  Advance podinfo.test canary iteration 3/10
  Normal   Synced  6m26s                flagger  Advance podinfo.test canary iteration 4/10
  Normal   Synced  26s (x6 over 5m26s)  flagger  (combined from similar events): Advance podinfo.test canary iteration 10/10

Promoting

# Promoting
NAME      STATUS        WEIGHT   LASTTRANSITIONTIME
podinfo   Promoting     0        2021-07-22T00:59:56Z


## Primary Podの更新
NAME                                  READY   STATUS    RESTARTS   AGE
podinfo-primary-7b67c7fbc4-w5jpt      2/2     Terminating       0          14m
podinfo-primary-666bd89c86-s8pkg      0/2     Pending           0          0s
podinfo-primary-666bd89c86-s8pkg      0/2     Pending           0          0s
podinfo-primary-666bd89c86-s8pkg      0/2     Init:0/1          0          0s
podinfo-primary-666bd89c86-vqhm5      0/2     Pending           0          0s
podinfo-primary-666bd89c86-vqhm5      0/2     Pending           0          0s
podinfo-primary-666bd89c86-vqhm5      0/2     Init:0/1          0          0s
podinfo-primary-666bd89c86-s8pkg      0/2     PodInitializing   0          2s
podinfo-primary-666bd89c86-vqhm5      0/2     PodInitializing   0          2s
podinfo-primary-666bd89c86-s8pkg      0/2     Running           0          3s
podinfo-primary-666bd89c86-vqhm5      0/2     Running           0          3s
podinfo-primary-666bd89c86-s8pkg      1/2     Running           0          4s
podinfo-primary-666bd89c86-vqhm5      1/2     Running           0          4s
podinfo-primary-7b67c7fbc4-w5jpt      0/2     Terminating       0          14m
podinfo-primary-7b67c7fbc4-w5jpt      0/2     Terminating       0          14m
podinfo-primary-7b67c7fbc4-w5jpt      0/2     Terminating       0          14m
podinfo-primary-666bd89c86-vqhm5      2/2     Running           0          16s
podinfo-primary-666bd89c86-s8pkg      2/2     Running           0          16s
podinfo-primary-7b67c7fbc4-4fhm6      2/2     Terminating       0          14m
podinfo-primary-7b67c7fbc4-4fhm6      0/2     Terminating       0          14m
podinfo-primary-7b67c7fbc4-4fhm6      0/2     Terminating       0          14m
podinfo-primary-7b67c7fbc4-4fhm6      0/2     Terminating       0          14m



$ kubectl describe canary podinfo -n test | tail -10
  Normal   Synced  13m (x2 over 14m)  flagger  all the metrics providers are available!
  Normal   Synced  13m                flagger  Initialization done! podinfo.test
  Normal   Synced  12m                flagger  New revision detected! Scaling up podinfo.test
  Normal   Synced  11m                flagger  Starting canary analysis for podinfo.test
  Normal   Synced  11m                flagger  Pre-rollout check acceptance-test passed
  Normal   Synced  11m                flagger  Advance podinfo.test canary iteration 1/10
  Normal   Synced  10m                flagger  Advance podinfo.test canary iteration 2/10
  Normal   Synced  9m5s               flagger  Advance podinfo.test canary iteration 3/10
  Normal   Synced  8m5s               flagger  Advance podinfo.test canary iteration 4/10
  Normal   Synced  5s (x8 over 7m5s)  flagger  (combined from similar events): Copying podinfo.test template spec to podinfo-primary.test

Finalising

# Finalising
NAME      STATUS        WEIGHT   LASTTRANSITIONTIME
podinfo   Finalising    0        2021-07-22T01:00:56Z


## Primary Podへトラフィックを流す
$ kubectl describe canary podinfo -n test | tail -10
  Normal   Synced  14m (x2 over 15m)    flagger  all the metrics providers are available!
  Normal   Synced  14m                  flagger  Initialization done! podinfo.test
  Normal   Synced  13m                  flagger  New revision detected! Scaling up podinfo.test
  Normal   Synced  12m                  flagger  Starting canary analysis for podinfo.test
  Normal   Synced  12m                  flagger  Pre-rollout check acceptance-test passed
  Normal   Synced  12m                  flagger  Advance podinfo.test canary iteration 1/10
  Normal   Synced  11m                  flagger  Advance podinfo.test canary iteration 2/10
  Normal   Synced  10m                  flagger  Advance podinfo.test canary iteration 3/10
  Normal   Synced  9m15s                flagger  Advance podinfo.test canary iteration 4/10
  Normal   Synced  15s (x9 over 8m15s)  flagger  (combined from similar events): Routing all traffic to primary

Succeeded

# Succeeded
NAME      STATUS        WEIGHT   LASTTRANSITIONTIME
podinfo   Succeeded     0        2021-07-22T01:01:56Z


## Canary Podの削除
NAME                                  READY   STATUS    RESTARTS   AGE
podinfo-5f8fd4f546-5tbmv              2/2     Terminating       0          14m
podinfo-5f8fd4f546-bkxbz              2/2     Terminating       0          13m
podinfo-5f8fd4f546-5tbmv              0/2     Terminating       0          14m
podinfo-5f8fd4f546-bkxbz              0/2     Terminating       0          14m
podinfo-5f8fd4f546-5tbmv              0/2     Terminating       0          14m
podinfo-5f8fd4f546-5tbmv              0/2     Terminating       0          14m
podinfo-5f8fd4f546-bkxbz              0/2     Terminating       0          14m
podinfo-5f8fd4f546-bkxbz              0/2     Terminating       0          14m


$ kubectl describe canary podinfo -n test | tail -10
  Normal   Synced  15m (x2 over 16m)  flagger  all the metrics providers are available!
  Normal   Synced  15m                flagger  Initialization done! podinfo.test
  Normal   Synced  14m                flagger  New revision detected! Scaling up podinfo.test
  Normal   Synced  13m                flagger  Starting canary analysis for podinfo.test
  Normal   Synced  13m                flagger  Pre-rollout check acceptance-test passed
  Normal   Synced  13m                flagger  Advance podinfo.test canary iteration 1/10
  Normal   Synced  12m                flagger  Advance podinfo.test canary iteration 2/10
  Normal   Synced  11m                flagger  Advance podinfo.test canary iteration 3/10
  Normal   Synced  10m                flagger  Advance podinfo.test canary iteration 4/10
  Normal   Synced  0s (x10 over 9m)   flagger  (combined from similar events): Promotion completed! Scaling down podinfo.test

ロールバック

ロールバックも試してみます。やることはCanary Deploymentとまったく同じです。

# コンテナイメージの更新
$ kubectl -n test set image deployment/podinfo podinfod=stefanprodan/podinfo:3.1.2
deployment.apps/podinfo image updated


# Canary/Podのウォッチを開始
$ kubectl get canary -n test -w
NAME      STATUS      WEIGHT   LASTTRANSITIONTIME
podinfo   Succeeded   0        2021-07-22T01:01:56Z


$ kubectl get pods -n test  -w
NAME                                  READY   STATUS    RESTARTS   AGE
flagger-loadtester-64695f854f-h9jdv   2/2     Running   0          66m
podinfo-primary-666bd89c86-s8pkg      2/2     Running   0          15m
podinfo-primary-666bd89c86-vqhm5      2/2     Running   0          15m
# flagger-loadtesterから500ステータスコードの実行
$ kubectl get pods -n test
NAME                                  READY   STATUS    RESTARTS   AGE
flagger-loadtester-64695f854f-6mkgj   2/2     Running   0          85m
podinfo-75459749c9-8w9tl              2/2     Running   0          3m47s
podinfo-75459749c9-mqd7m              2/2     Running   0          3m51s
podinfo-primary-76655b74b9-b99lq      2/2     Running   0          39m
podinfo-primary-76655b74b9-cvs9z      2/2     Running   0          39m

$ kubectl exec -it -n test flagger-loadtester-64695f854f-6mkgj sh
/home/app $ watch curl http://podinfo-canary.test:9898/status/500

Deployment Proceeding (Failed)

$ kubectl describe canary podinfo -n test | tail -10
  Normal   Synced  31m                 flagger  Advance podinfo.test canary iteration 2/10
  Normal   Synced  30m                 flagger  Advance podinfo.test canary iteration 3/10
  Normal   Synced  29m                 flagger  Advance podinfo.test canary iteration 4/10
  Normal   Synced  19m (x10 over 28m)  flagger  (combined from similar events): Promotion completed! Scaling down podinfo.test
  Normal   Synced  5m4s (x2 over 33m)  flagger  New revision detected! Scaling up podinfo.test
  Normal   Synced  4m4s (x2 over 32m)  flagger  Advance podinfo.test canary iteration 1/10
  Normal   Synced  4m4s (x2 over 32m)  flagger  Pre-rollout check acceptance-test passed
  Normal   Synced  4m4s (x2 over 32m)  flagger  Starting canary analysis for podinfo.test
  Warning  Synced  64s (x2 over 3m4s)  flagger  Halt podinfo.test advancement success rate 0.00% < 99%
  Warning  Synced  4s (x2 over 2m4s)   flagger  Halt podinfo.test advancement success rate 97.51% < 99%

Deployment Failed

NAME      STATUS      WEIGHT   LASTTRANSITIONTIME
podinfo   Failed        0        2021-07-22T01:22:56Z


NAME                                  READY   STATUS    RESTARTS   AGE
podinfo-75459749c9-w8vx8              2/2     Terminating       0          7m
podinfo-75459749c9-7qbvd              2/2     Terminating       0          7m
podinfo-75459749c9-w8vx8              0/2     Terminating       0          7m6s
podinfo-75459749c9-7qbvd              0/2     Terminating       0          7m6s
podinfo-75459749c9-7qbvd              0/2     Terminating       0          7m13s
podinfo-75459749c9-7qbvd              0/2     Terminating       0          7m13s
podinfo-75459749c9-w8vx8              0/2     Terminating       0          7m15s
podinfo-75459749c9-w8vx8              0/2     Terminating       0          7m15s


$ kubectl describe canary podinfo -n test | tail -10
  Normal   Synced  32m                    flagger  Advance podinfo.test canary iteration 4/10
  Normal   Synced  22m (x10 over 31m)     flagger  (combined from similar events): Promotion completed! Scaling down podinfo.test
  Normal   Synced  8m32s (x2 over 36m)    flagger  New revision detected! Scaling up podinfo.test
  Normal   Synced  7m32s (x2 over 35m)    flagger  Pre-rollout check acceptance-test passed
  Normal   Synced  7m32s (x2 over 35m)    flagger  Advance podinfo.test canary iteration 1/10
  Normal   Synced  7m32s (x2 over 35m)    flagger  Starting canary analysis for podinfo.test
  Warning  Synced  3m32s (x2 over 5m32s)  flagger  Halt podinfo.test advancement success rate 97.51% < 99%
  Warning  Synced  2m32s (x3 over 6m32s)  flagger  Halt podinfo.test advancement success rate 0.00% < 99%
  Warning  Synced  92s                    flagger  Rolling back podinfo.test failed checks threshold reached 5
  Warning  Synced  92s                    flagger  Canary failed! Scaling down podinfo.test

Slackへのメッセージ送信

次にSlackへメッセージを送信する場合を試してみます。Flaggerでは AlertProvider というリソースで、メッセージの送信先となるプロバイダーやWebhooknURLを設定します。

※参考リンク:Flagger Doc - Alerting

今回は以下のような AlertProvider マニフェストを利用します。前提として、SlackのIncoming Webhookを取得しておきます。

※参考リンク:SlackでのIncoming Webhookの利用


podinfo-alert.yaml

apiVersion: flagger.app/v1beta1
kind: AlertProvider
metadata:
  name: on-call
  namespace: test
spec:
  type: slack
  channel: on-call-alerts
  username: flagger
  secretRef:
    name: on-call-url
---
apiVersion: v1
kind: Secret
metadata:
  name: on-call-url
  namespace: test
data:
  address: <encoded webhook URL>


上記マニフェストを使ってリソースをデプロイします。

$ kubectl apply -f podinfo-alert.yaml
alertprovider.flagger.app/on-call created
secret/on-call-url created


# デプロイ後の確認
$ kubectl get alertprovider -n test
NAME      TYPE
on-call   slack

AlertProvider を利用するには Canary リソースのほうも修正が必要です。以下のマニフェストのように、 spec.analysis.alerts にて、利用する AlertProvider とログレベルを指定します。


podinfo-canary-alert.yaml

apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: podinfo
  namespace: test
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: podinfo
  progressDeadlineSeconds: 60
  autoscalerRef:
    apiVersion: autoscaling/v2beta2
    kind: HorizontalPodAutoscaler
    name: podinfo
  service:
    port: 9898
    targetPort: 9898
    gateways:
    - public-gateway.istio-system.svc.cluster.local
    hosts:
    - app.example.com
    trafficPolicy:
      tls:
        mode: DISABLE
    retries:
      attempts: 3
      perTryTimeout: 1s
      retryOn: "gateway-error,connect-failure,refused-stream"
  analysis:
    alerts:
      - name: "on-call Slack"
        severity: info
        providerRef:
          name: on-call
          namespace: test
    interval: 1m
    threshold: 5
    maxWeight: 50
    stepWeight: 10
    metrics:
    - name: request-success-rate
      thresholdRange:
        min: 99
      interval: 1m
    - name: request-duration
      thresholdRange:
        max: 500
      interval: 30s
    webhooks:
      - name: acceptance-test
        type: pre-rollout
        url: http://flagger-loadtester.test/
        timeout: 30s
        metadata:
          type: bash
          cmd: "curl -sd 'test' http://podinfo-canary:9898/token | grep token"
      - name: load-test
        url: http://flagger-loadtester.test/
        timeout: 5s
        metadata:
          cmd: "hey -z 1m -q 10 -c 2 http://podinfo-canary.test:9898/"


上記マニフェストから Canary リソースをデプロイします。

$ kubectl apply -f podinfo-canary-alert.yaml
canary.flagger.app/podinfo created

$ kubectl get pods -n test
NAME                                  READY   STATUS    RESTARTS   AGE
flagger-loadtester-64695f854f-h9jdv   2/2     Running   0          104m
podinfo-primary-df5797b7c-bj86k       2/2     Running   0          90s
podinfo-primary-df5797b7c-xgf4v       2/2     Running   0          90s

$ kubectl get canary -n test
NAME      STATUS        WEIGHT   LASTTRANSITIONTIME
podinfo   Initialized   0        2021-07-22T01:53:06Z

Slackとの連携が成功していれば、以下の画像のようなメッセージが送られます。

Canary リソース作成時

f:id:FY0323:20210722181703j:plain

※ コンテナイメージアップデート成功時

f:id:FY0323:20210722181743j:plain

※ アップデート失敗時

f:id:FY0323:20210722181807j:plain

参考ドキュメント

Flux v2に入門する

今回はGitOpsツールの一つであるFlux v2を試してみます。GitOpsについては以下の参考リンクなどをご参照ください。

※参考リンク:

Flux v2とは

Flux v2は、複数のCustom Controllerを組み合わせて、GitOpsの各機能を提供します。

Fluxの構成は以下の通りです。

Flux-component

※画像:Fluxドキュメントより

Fluxが実現する機能として、以下のような機能が挙げられています。

GitなどのリソースとKubernetesクラスター間の"同期"

FluxはGitHub/GitLabなどのGitリポジトリ、Helmリポジトリ、そしてminioのようなS3-compatibleなオブジェクトバケットなどを、リソースとして管理し、Kubernetesクラスターとの同期を実行します。

source-controllers ※画像:Fluxドキュメントより

コンテナイメージの自動アップデート

Fluxはコンテナレジストリもリソースとして管理し、コンテナイメージのアップデートを検知します。またイメージ更新を検知すると、対象のKubernetesマニフェストファイルのイメージタグを自動的に更新し、最終的にはクラスター上のPodのイメージをアップデートします。

image-update-controllers ※画像:Fluxドキュメントより

リソースの状態変化の通知

Fluxは、管理するリソースの状態変化に応じて、SlackやDiscordなどの外部システムに通知を送る機能が備わっています。

notification-controllers ※画像:Fluxドキュメントより

その他

  • リソース間依存関係の管理: Fluxの Kustomization HelmRelease リソースには spec.dependsOn というフィールドがあり、依存関係のリソースを指定することで、対象のリソースが作成・起動するまでは、リソースの作成を行わないよう制御できます。

※参考リンク:Flux v2のdependsOnを簡単に検証してみた

  • 外部イベントの監視と応答:Fluxの Receiver というリソースではWebhook receiverを定義し、Reconciliationのトリガーとして利用できます。利用できるリソースには、GitHubやDockerHubなどがあります。

  • 他ソフトウェアとの相互運用性GitHub Actions / Argo / Tektonなどのワークフロープロバイダ、クラスター管理を行うCluster APIなどのソフトウェアとの相互運用性を提供します。

Flux v1 / v2の違い

Flux v2は、以前はv1として開発が進んでいましたが、長年要望されていた機能をより簡単に実装できるよう、v2として大きな変更を加えました。v1ではモノリスなオペレーターで全ての機能を実現していたのに対し、v2では各機能を専用のCustom Controllerに実装することで分離しています。

v1とv2の機能差分はこちらのページに記載されていますが、特に大きな違いは以下のあたりかと思います。

  • v1

    • 一つのGitリポジトリの同期をサポート
    • flux deploy時のみ、Gitのコンフィグや秘匿情報を指定可能
    • HEADの追跡のみサポート
  • v2

    • 複数のGitリポジトリの同期をサポート
    • GitRepositoryリソースでGitリポジトリの設定などを定義
    • 指定したブランチやタグの追跡をサポート
    • Prometheusなど、エコシステムのコアのコンポーネントと統合できる
    • マルチテナンシーをサポート(参考リンク

なお、現在v1はメンテナンスモードで、新機能の追加は行われず、バグ修正とCVEのパッチのみ対応しております。

Fluxではv2の利用が推奨されています。まだGAには至っていませんが、こちらのロードマップに沿って開発が進められています。

Flux v2を試す

ここから実際にFluxを動かしてみます。

前提条件の確認と動作環境

ここからはGet Startedの手順をベースに動かしてみます。操作を行う上での前提条件は以下の通りです。

  • k8s version: 1.16 以上
  • kubectl: 1.18 以上
  • GitHubリポジトリの用意
  • GitHub Personal Access Tokenの用意:scopeでは repo 配下の権限をすべて許可しておきます。

また今回は Amazon EKS上で動作検証を行いました。動作確認を行った環境の情報は以下の通りです。

$ eksctl version
0.57.0

$ eksctl create cluster -f eks-clusterconfig.yml

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.0", GitCommit:"af46c47ce925f4c4ad5cc8d1fca46c7b77d13b38", GitTreeState:"clean", BuildDate:"2020-12-08T17:59:43Z", GoVersion:"go1.15.5", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"20+", GitVersion:"v1.20.4-eks-6b7464", GitCommit:"6b746440c04cb81db4426842b4ae65c3f7035e53", GitTreeState:"clean", BuildDate:"2021-03-19T19:33:03Z", GoVersion:"go1.15.8", Compiler:"gc", Platform:"linux/amd64"}

$ kubectl get nodes
NAME                                               STATUS   ROLES    AGE   VERSION
ip-192-168-0-173.ap-northeast-1.compute.internal   Ready    <none>   25m   v1.20.4-eks-6b7464
ip-192-168-1-253.ap-northeast-1.compute.internal   Ready    <none>   25m   v1.20.4-eks-6b7464
ip-192-168-2-59.ap-northeast-1.compute.internal    Ready    <none>   25m   v1.20.4-eks-6b7464

また、後ほどFluxからGitHubへのアクセスに利用する環境変数を設定しておきます。

export GITHUB_TOKEN=<GitHub Personal Access Token>
export GITHUB_USER=fy0323

Flux CLIのインストール

GitHubリポジトリとEKSクラスターが用意できたので、次に flux CLIのインストールを行います。Fluxは flux CLIを利用して、Flux自体のデプロイやリソースの作成・管理を行うことができます。今回の手順でも、リソースの作成や制御はほぼ flux CLIを利用して行います。

$ curl -s https://fluxcd.io/install.sh | sudo bash
[INFO]  Downloading metadata https://api.github.com/repos/fluxcd/flux2/releases/latest
[INFO]  Using 0.16.1 as release
[INFO]  Downloading hash https://github.com/fluxcd/flux2/releases/download/v0.16.1/flux_0.16.1_checksums.txt
[INFO]  Downloading binary https://github.com/fluxcd/flux2/releases/download/v0.16.1/flux_0.16.1_linux_amd64.tar.gz
[INFO]  Verifying binary download
[INFO]  Installing flux to /usr/local/bin/flux

$ flux --version
flux version 0.16.1

Fluxデプロイ

Fluxのデプロイを行うコマンドには flux install flux bootstrap の2種類があります。 flux install はFluxをクラスター上にインストールするだけですが、 flux bootstrap は、サブコマンドに指定したGitプロバイダに対して、Fluxインストール用のマニフェストファイルをコミットします。その後、コミットしたリポジトリをFluxの同期対象とし、クラスターと同期をすることでFluxのデプロイを実現します。

ここでは flux bootstrap コマンドを利用します。まずは flux check コマンドを実行し、Fluxのデプロイに必要な前提条件を満たすことを確認します。

$ flux check --pre
► checking prerequisites
✔ kubectl 1.20.0 >=1.18.0-0
✔ Kubernetes 1.20.4-eks-6b7464 >=1.16.0-0
✔ prerequisites checks passed

チェックで問題がないため、Fluxのデプロイを実行します。

$ flux bootstrap github \
>   --owner=$GITHUB_USER \
>   --repository=fluxv2-test \
>   --branch=main \
>   --path=./clusters/my-cluster \
>   --personal

(中略)

✔ source-controller: deployment ready
✔ kustomize-controller: deployment ready
✔ helm-controller: deployment ready
✔ notification-controller: deployment ready
✔ all components are healthy

flux bootstrap コマンド後、クラスター上にリソースが作成されたことが確認できます。

$ kubectl get pods -n flux-system
NAME                                       READY   STATUS    RESTARTS   AGE
helm-controller-5b96d94c7f-525lt           1/1     Running   0          13m
kustomize-controller-5b95b78ddc-wqkmg      1/1     Running   0          13m
notification-controller-55f94bc746-zx8m9   1/1     Running   0          13m
source-controller-78bfb8576-ftnxl          1/1     Running   0          13m

$ kubectl get gitrepository -n flux-system
NAME          URL                                       READY   STATUS                                                            AGE
flux-system   ssh://git@github.com/fy0323/fluxv2-test   True    Fetched revision: main/e1d0fb9ed858d425e0bce380ab096a34c4e7035c   15m

GitHubリポジトリとの同期によるアプリケーションのデプロイ

続いてアプリケーションのマニフェストファイルを含むGitHubリポジトリを登録し、クラスター間の同期によってデプロイを行います。

※参考リンク:

まずは作業用のリポジトリをクローンします。次に GitRepository というリソースを用意しますが、ここでは flux create コマンドを利用してマニフェストファイルを生成、リポジトリに保存することでクラスターと同期し、自動的にデプロイされる、という流れで作業します。

$ git clone https://github.com/fy0323/fluxv2-test
$ cd fluxv2-test/

# GitRepositoryリソースのマニフェストファイル生成
$ flux create source git podinfo \
>   --url=https://github.com/stefanprodan/podinfo \
>   --branch=master \
>   --interval=30s \
>   --export > ./clusters/my-cluster/podinfo-source.yaml

# コミット
$ git add .
$ git commit -m "add podinfo gitrepository"
$ git push

なお、上記コマンドで生成されたマニフェストファイルは以下のようになります。

podinfo-source.yaml

apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
  name: podinfo
  namespace: flux-system
spec:
  interval: 30s
  ref:
    branch: master
  url: https://github.com/stefanprodan/podinfo

次に Kustomization というリソースを作成します。このリソースはその名の通り kustomizeが利用する kustomization.yaml のパスを指定し、Kubernetesマニフェストファイルの管理元を定義するリソースとなります。

# Kustomaizationリソースのマニフェストファイル生成
$ flux create kustomization podinfo \
>   --source=podinfo \
>   --path="./kustomize" \
>   --prune=true \
>   --validation=client \
>   --interval=5m \
>   --export > ./clusters/my-cluster/podinfo-kustomization.yaml

# コミット
$ git add .
$ git commit -m "add podinfo kustomization"
$ git push

podinfo-kustomization.yaml

---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
  name: podinfo
  namespace: flux-system
spec:
  interval: 5m0s
  path: ./kustomize
  prune: true
  sourceRef:
    kind: GitRepository
    name: podinfo
  validation: client

しばらくするとリポジトリクラスター間での同期が発生し、以下の通り podinfo というアプリケーションがクラスターにデプロイされます。

# kustomizationリソースの状態確認
$ flux get kustomization
NAME            READY   MESSAGE                                                                 REVISION                                        SUSPENDED
flux-system     True    Applied revision: main/7e17dea66b87e57121472eb95abe876184c9ee8b         main/7e17dea66b87e57121472eb95abe876184c9ee8b   False
podinfo         True    Applied revision: master/627d5c4bb67b77185f37e31d734b085019ff2951       master/627d5c4bb67b77185f37e31d734b085019ff2951 False


# podinfoがデプロイされている
$ kubectl get all
NAME                           READY   STATUS    RESTARTS   AGE
pod/podinfo-576d5bf6bd-qssrz   1/1     Running   0          4m58s
pod/podinfo-576d5bf6bd-rqx8d   1/1     Running   0          5m14s

NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
service/kubernetes   ClusterIP   10.100.0.1      <none>        443/TCP             71m
service/podinfo      ClusterIP   10.100.113.99   <none>        9898/TCP,9999/TCP   5m14s

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/podinfo   2/2     2            2           5m14s

NAME                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/podinfo-576d5bf6bd   2         2         2       5m14s

NAME                                          REFERENCE            TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
horizontalpodautoscaler.autoscaling/podinfo   Deployment/podinfo   <unknown>/99%   2         4         2          5m14s

イメージの同期

次に、コンテナイメージの自動同期を試してみます。

※参考リンク:

まずは前項で利用した podinfo の定義ファイルを一度削除します。クラスターからリソースを削除しても、リポジトリと同期することで再作成されるため、リポジトリから削除します。

$ rm clusters/my-cluster/podinfo-kustomization.yaml
$ rm clusters/my-cluster/podinfo-source.yaml
$ git add .
$ git commit -m "remove podinfo-source & podinfo-kustomization"
$ git push

# 削除されたことの確認
$ kubectl get pods
No resources found in default namespace.
$ kubectl get gitrepository -n flux-system
NAME          URL                                       READY   STATUS                                                            AGE
flux-system   ssh://git@github.com/fy0323/fluxv2-test   True    Fetched revision: main/1c5df5beab6e782467663bddab02e33a525ad5a2   51m

次にFluxの再インストールを行います。Fluxのコンテナイメージの管理を行うのは image-reflector image-automation というControllerなのですが、これらはデフォルトではインストールされず、リソースを追加する必要があります。 flux bootstrap によるFluxのインストールは冪等であり、実行時に新しいバージョンがリリースされている場合は、新しいバージョンがインストールされます。

また、コンテナイメージの更新後、上記Controllerにより、マニフェストファイルのイメージタグを書き換えるため、デプロイ用のキーに書き込み権限を付与する必要があります。前項でFluxのインストールを実行した際は、読み取り権限のみを付与したキーが発行されており、これを変更する必要があります。

ここではFluxのキー情報が格納されている flux-system というSecretリソースを削除し、その後Fluxの再インストールを行います。

# Secretの削除
$ kubectl get secret -n flux-system
NAME                                      TYPE                                  DATA   AGE
default-token-649ts                       kubernetes.io/service-account-token   3      90m
flux-system                               Opaque                                3      90m
helm-controller-token-cdrqn               kubernetes.io/service-account-token   3      90m
image-automation-controller-token-8nrpz   kubernetes.io/service-account-token   3      49m
image-reflector-controller-token-p96zm    kubernetes.io/service-account-token   3      49m
kustomize-controller-token-k2zft          kubernetes.io/service-account-token   3      90m
notification-controller-token-z4npl       kubernetes.io/service-account-token   3      90m
source-controller-token-ssh2v             kubernetes.io/service-account-token   3      90m

$ kubectl delete secret -n flux-system flux-system
secret "flux-system" deleted


# Fluxの再インストール
$ flux bootstrap github \
> --components-extra=image-reflector-controller,image-automation-controller \
> --owner=$GITHUB_USER \
> --repository=fluxv2-test \
> --branch=main \
> --path=./clusters/my-cluster \
> --read-write-key \
> --personal

(中略)

✔ image-automation-controller: deployment ready
✔ source-controller: deployment ready
✔ kustomize-controller: deployment ready
✔ helm-controller: deployment ready
✔ notification-controller: deployment ready
✔ image-reflector-controller: deployment ready
✔ all components are healthy


# リソースの確認
$ kubectl get pods -n flux-system
NAME                                           READY   STATUS    RESTARTS   AGE
helm-controller-5b96d94c7f-525lt               1/1     Running   0          41m
image-automation-controller-5cf75fd555-px4r7   1/1     Running   0          45s
image-reflector-controller-6787985855-bssjr    1/1     Running   0          45s
kustomize-controller-5b95b78ddc-wqkmg          1/1     Running   0          41m
notification-controller-55f94bc746-zx8m9       1/1     Running   0          41m
source-controller-78bfb8576-ftnxl              1/1     Running   0          41m

※なお、ここでSecretリソースを削除せずにキー情報が更新されないと、後ほどの操作時に以下のようにエラーが発生します。

コンテナイメージアップデート時のエラー

# コンテナイメージのアップデート時にエラーが発生
$ flux get image update
NAME            READY   MESSAGE                                                                                 LAST RUNSUSPENDED
flux-system     False   remote: ERROR: The key you are authenticating with has been marked as read only.                False

Fluxの再デプロイが完了したので、テスト用のアプリケーションのデプロイを行います。ここではコンテナイメージが 5.0.0 のものを指定します。

# podinfoマニフェストファイルの取得
$ curl -sL https://raw.githubusercontent.com/stefanprodan/podinfo/5.0.0/kustomize/deployment.yaml > ./clusters/my-cluster/podinfo-deployment.yaml

# コミット
$ git add .
$ git commit -m "add podinfo deployment"
$ git push

podinfo-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: podinfo
spec:
  minReadySeconds: 3
  revisionHistoryLimit: 5
  progressDeadlineSeconds: 60
  strategy:
    rollingUpdate:
      maxUnavailable: 0
    type: RollingUpdate
  selector:
    matchLabels:
      app: podinfo
  template:
    metadata:
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9797"
      labels:
        app: podinfo
    spec:
      containers:
      - name: podinfod
        image: ghcr.io/stefanprodan/podinfo:5.0.0
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 9898
          protocol: TCP
        - name: http-metrics
          containerPort: 9797
          protocol: TCP
        - name: grpc
          containerPort: 9999
          protocol: TCP
        command:
        - ./podinfo
        - --port=9898
        - --port-metrics=9797
        - --grpc-port=9999
        - --grpc-service-name=podinfo
        - --level=info
        - --random-delay=false
        - --random-error=false
        env:
        - name: PODINFO_UI_COLOR
          value: "#34577c"
        livenessProbe:
          exec:
            command:
            - podcli
            - check
            - http
            - localhost:9898/healthz
          initialDelaySeconds: 5
          timeoutSeconds: 5
        readinessProbe:
          exec:
            command:
            - podcli
            - check
            - http
            - localhost:9898/readyz
          initialDelaySeconds: 5
          timeoutSeconds: 5
        resources:
          limits:
            cpu: 2000m
            memory: 512Mi
          requests:
            cpu: 100m
            memory: 64Mi

続いてここでは flux reconcile コマンドを利用し、リポジトリクラスター間の同期を実行します。コマンドを実行しなくとも、一定間隔でリポジトリクラスター間の差分はチェックされており(ここでは10分間隔)、差分を検知すれば同期は行われます。

# リポジトリ・クラスター間の同期
$ flux reconcile kustomization flux-system --with-source
(中略)
✔ Kustomization reconciliation completed
✔ applied revision main/4301e2dda8423ab580df8ef5d025f9a278cb7664


# podinfoアプリがデプロイされる
$ kubectl get pods
NAME                      READY   STATUS    RESTARTS   AGE
podinfo-c598c9677-v9rvq   1/1     Running   0          20s


# この時点ではコンテナイメージタグは5.0.0
$ kubectl get deployment podinfo -oyaml | grep 'image:'
                f:image: {}
        image: ghcr.io/stefanprodan/podinfo:5.0.0

次にコンテナレジストリを定義する ImageRepository 、同期するコンテナイメージの指定を行う ImagePolicy というリソースを作成します。今回指定する ImageRepository には、複数のバージョンのイメージが保存されています。ここで指定する 5.0.x の範囲では、 5.0.3 のタグが付与されたイメージが最新のバージョンとなります。

# ImageRegistryリソースのマニフェスト生成
$ flux create image repository podinfo \
> --image=ghcr.io/stefanprodan/podinfo \
> --interval=1m \
> --export > ./clusters/my-cluster/podinfo-registry.yaml


# ImagePolicyリソースのマニフェスト生成
$ flux create image policy podinfo \
> --image-ref=podinfo \
> --select-semver=5.0.x \
> --export > ./clusters/my-cluster/podinfo-policy.yaml


# コミット
$ git add . && git commit -m "add podinfo image scan"
$ git push

podinfo-registry.yaml

---
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageRepository
metadata:
  name: podinfo
  namespace: flux-system
spec:
  image: ghcr.io/stefanprodan/podinfo
  interval: 1m0s

podinfo-policy.yaml

---
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImagePolicy
metadata:
  name: podinfo
  namespace: flux-system
spec:
  imageRepositoryRef:
    name: podinfo
  policy:
    semver:
      range: 5.0.x

ここでも flux reconcile コマンドを実行し、リポジトリクラスター間の同期を手動で実行します。

# リポジトリ・クラスター間の同期
$ flux reconcile kustomization flux-system --with-source
(中略)
✔ Kustomization reconciliation completed
✔ applied revision main/242b5514ff60e0c02a29b95564fc48a6e75c0ac0


# リソースの確認
$ kubectl get imagerepository -n flux-system
NAME      LAST SCAN              TAGS
podinfo   2021-07-18T02:27:06Z   19

$ kubectl get imagepolicy -n flux-system
NAME      LATESTIMAGE
podinfo   ghcr.io/stefanprodan/podinfo:5.0.3

なお、リソースの状態は flux get コマンドなどでも確認できます。

$ flux get image repository
NAME    READY   MESSAGE                         LAST SCAN                       SUSPENDED
podinfo True    successful scan, found 19 tags  2021-07-18T11:28:06+09:00       False

$ flux get image policy
NAME    READY   MESSAGE                                                                 LATEST IMAGE                    
podinfo True    Latest image tag for 'ghcr.io/stefanprodan/podinfo' resolved to: 5.0.3  ghcr.io/stefanprodan/podinfo:5.0.3

次に podinfo-deployment.yaml を一部修正します。コンテナイメージの自動更新を行う ImageUpdateAutomation というリソースは、マニフェスト中の特定のマーカーを目印にして、イメージタグの書き換えを行います。

※参考リンク:

$ vi clusters/my-cluster/podinfo-deployment.yaml

# 以下のように修正
    spec:
      containers:
      - name: podinfod
        image: ghcr.io/stefanprodan/podinfo:5.0.0 # {"$imagepolicy": "flux-system:podinfo"}
        imagePullPolicy: IfNotPresent

次に ImageUpdateAutomation リソースを作成し、コンテナイメージの自動更新を実行可能にします。

# ImageUpdateAutomationリソースのマニフェスト生成
$ flux create image update flux-system \
> --git-repo-ref=flux-system \
> --git-repo-path="./clusters/my-cluster" \
> --checkout-branch=main \
> --push-branch=main \
> --author-name=fluxcdbot \
> --author-email=fluxcdbot@users.noreply.github.com \
> --commit-template="{{range .Updated.Images}}{{println .}}{{end}}" \
> --export > ./clusters/my-cluster/flux-system-automation.yaml


# コミット
$ git add . && git commit -m "add image updates automation" && git push

flux-system-automation.yaml

---
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
  name: flux-system
  namespace: flux-system
spec:
  git:
    checkout:
      ref:
        branch: main
    commit:
      author:
        email: fluxcdbot@users.noreply.github.com
        name: fluxcdbot
      messageTemplate: '{{range .Updated.Images}}{{println .}}{{end}}'
    push:
      branch: main
  interval: 1m0s
  sourceRef:
    kind: GitRepository
    name: flux-system
  update:
    path: ./clusters/my-cluster
    strategy: Setters

しばらくすると、以下のように ImageUpdateAutomation の作成が確認できます。

$ kubectl get imageupdateautomation -n flux-system
NAME          LAST RUN
flux-system   2021-07-18T02:46:40Z

$ flux get image update
NAME            READY   MESSAGE                                                                 LAST RUN                        SUSPENDED
flux-system     True    committed and pushed 614e62d52ec55844f61fcd7f5d67999757229729 to main   2021-07-18T11:45:33+09:00       False

マニフェストファイルを確認すると、イメージタグが 5.0.3 に更新されており、Podのイメージも更新されていることがわかります。

$ git pull
$ cat clusters/my-cluster/podinfo-deployment.yaml | grep "image:"
          image: ghcr.io/stefanprodan/podinfo:5.0.3 # {"$imagepolicy": "flux-system:podinfo"}


$ kubectl get deploy podinfo -oyaml | grep 'image:'
                f:image: {}
        image: ghcr.io/stefanprodan/podinfo:5.0.3


# イメージ関連のリソースの状態を一括で確認できる
$ flux get images all
NAME                    READY   MESSAGE                         LAST SCAN                       SUSPENDED
imagerepository/podinfo True    successful scan, found 19 tags  2021-07-18T11:51:18+09:00       False

NAME                    READY   MESSAGE                                                                 LATEST IMAGE    
imagepolicy/podinfo     True    Latest image tag for 'ghcr.io/stefanprodan/podinfo' resolved to: 5.0.3  ghcr.io/stefanprodan/podinfo:5.0.3

NAME                                    READY   MESSAGE                                                         LAST RUN                        SUSPENDED
imageupdateautomation/flux-system       True    no updates made; last commit 614e62d at 2021-07-18T02:45:33Z    2021-07-18T11:50:54+09:00       False

イメージ更新の停止

flux suspend コマンドはコンテナイメージの同期を一時的に停止するコマンドです。例えばあるイメージのバージョンに問題があり、一時的に同期を停止したい場合などに利用できます。停止後は flux resume コマンドにより、同期を再開することができます。

$ flux suspend image update flux-system
► suspending image update automation flux-system in flux-system namespace
✔ image update automation suspended


$ flux get image update
NAME            READY   MESSAGE                                                         LAST RUN                       SUSPENDED
flux-system     True    no updates made; last commit 614e62d at 2021-07-18T02:45:33Z    2021-07-18T11:53:01+09:00      True


$ flux resume image update flux-system
► resuming image update automation flux-system in flux-system namespace
✔ image update automation resumed
◎ waiting for ImageUpdateAutomation reconciliation
✔ ImageUpdateAutomation reconciliation completed
✔ no updates made; last commit 614e62d at 2021-07-18T02:45:33Z

コンテナイメージのロールバック

コンテナイメージを最新のバージョンから変更したい場合、 ImagePolicy で指定するイメージタグを修正することで、イメージのロールバックを実行することができます。

# 変更前のバージョン
$ kubectl describe imagepolicy podinfo -n flux-system

(中略)
Spec:
  Image Repository Ref:
    Name:  podinfo
  Policy:
    Semver:
      Range:  5.0.x


# イメージタグの変更
$ flux create image policy podinfo \
> --image-ref=podinfo \
> --select-semver=5.0.0
✚ generating ImagePolicy
► applying ImagePolicy
✔ ImageRepository updated
◎ waiting for ImagePolicy reconciliation
✔ ImagePolicy reconciliation completed


# 更新後のイメージタグ
$ kubectl describe imagepolicy podinfo -n flux-system | grep Semver -3
  Image Repository Ref:
    Name:  podinfo
  Policy:
    Semver:
      Range:  5.0.0
Status:
  Conditions:


# Podのイメージも変更されている
$ kubectl get deploy podinfo -oyaml | grep "image:"
                f:image: {}
        image: ghcr.io/stefanprodan/podinfo:5.0.0

なお、コンテナイメージの同期に利用できるレジストリは、DockerHubやGitHub Container Registryのほか、Amazon Elastic Container Registry (ECR) やAzure Container Registry (ACR)など、パブリッククラウドのサービスも利用可能です。ただし、これらはレジストリのアクセストークンの期限が一定時間で期限切れとなるため、それを更新するためのCronJobの利用方法が紹介されています

通知

次にFluxからの通知を試してみます。今回はSlackに通知を飛ばすよう設定します。

※参考リンク:

作業を行う前提として、SlackのIncoming Webhookを有効にし、Webhook URLを取得しておきます。Webhook URLの情報はSecretリソースとして登録しておきます。

# Slack Webhook URLの登録
$ kubectl -n flux-system create secret generic slack-url \
> --from-literal=address=<Slack Incoming Webhook URL>


$ kubectl get secret -n flux-system
NAME                                      TYPE                                  DATA   AGE
default-token-649ts                       kubernetes.io/service-account-token   3      117m
flux-system                               Opaque                                3      25m
helm-controller-token-cdrqn               kubernetes.io/service-account-token   3      117m
image-automation-controller-token-8nrpz   kubernetes.io/service-account-token   3      76m
image-reflector-controller-token-p96zm    kubernetes.io/service-account-token   3      76m
kustomize-controller-token-k2zft          kubernetes.io/service-account-token   3      117m
notification-controller-token-z4npl       kubernetes.io/service-account-token   3      117m
slack-url                                 Opaque                                1      15s
source-controller-token-ssh2v             kubernetes.io/service-account-token   3      117m

次にFluxで通知機能を利用するための Provider Alert というリソースを作成します。 Provider は通知先となるプロバイダーを、 Alertは通知の対象となるリソースや通知レベルの設定などを定義します。

# Providerリソースのマニフェスト生成
$ flux create alert-provider slack \
> --type slack \
> --channel fluxcd-test \
> --secret-ref slack-url \
> --export > ./clusters/my-cluster/slack-alert-provider.yaml


# Alertリソースのマニフェスト生成
$ flux create alert flux-system \
> --event-severity info \
> --event-source Kustomization/*,GitRepository/*,ImagePolicy/* \
> --provider-ref slack \
> --export > ./clusters/my-cluster/slack-alert.yaml


# コミット
$ git add . && git commit -m "add alert provider & alert" && git push

slack-alert-provider.yaml

---
apiVersion: notification.toolkit.fluxcd.io/v1beta1
kind: Provider
metadata:
  name: slack
  namespace: flux-system
spec:
  channel: fluxcd-test
  secretRef:
    name: slack-url
  type: slack

slack-alert.yaml

---
apiVersion: notification.toolkit.fluxcd.io/v1beta1
kind: Alert
metadata:
  name: flux-system
  namespace: flux-system
spec:
  eventSeverity: info
  eventSources:
  - kind: Kustomization
    name: '*'
  - kind: GitRepository
    name: '*'
  - kind: ImagePolicy
    name: '*'
  providerRef:
    name: slack

flux reconcile コマンドにより、リポジトリクラスター間を同期します。

$ flux reconcile kustomization flux-system --with-source


# リソースの状態確認
$ kubectl get provider -n flux-system
NAME    READY   STATUS        AGE
slack   True    Initialized   100s

$ kubectl get alert -n flux-system
NAME          READY   STATUS        AGE
flux-system   True    Initialized   6m55s

$ flux get alert-providers
NAME    READY   MESSAGE
slack   True    Initialized

$ flux get alerts
NAME            READY   MESSAGE         SUSPENDED
flux-system     True    Initialized     False

通知設定が完了したので、リソースの変更を行います。ここでは、先ほど変更した ImagePolicy のイメージタグを変更してみます。

$ flux create image policy podinfo \
> --image-ref=podinfo \
> --select-semver=5.0.x

上記設定からしばらくすると、Slackのほうに通知が飛ぶ様子が確認できます。

f:id:FY0323:20210719131304j:plain

Fluxのアンインストール

検証が完了したので、Fluxをアンインストールします。

$ flux uninstall --namespace=flux-system
? Are you sure you want to delete Flux and its custom resource definitions? [y/N] y
(中略)
✔ uninstall finished


# 削除の確認
$ kubectl get ns
NAME              STATUS   AGE
default           Active   12h
kube-node-lease   Active   12h
kube-public       Active   12h
kube-system       Active   12h

Veleroに入門する

今回はKubernetesリソースのバックアップ・リストアを実現するVeleroを使ってみます。

Cloud Nativeな世界のバックアップ・リストア

Veleroを動かす前に、バックアップ・リストアについていくつか書いておきます。

そもそもバックアップ・リストアを実現する理由としては、大きく以下の3つの理由があるかと思います。

  • DR: システムの稼働する主要サイトで災害等が発生し、システムとしての機能を失ったときに、別のサイトにてシステムを再稼働させる。
  • オペレーションのリカバリ: 業務の中でデータの紛失や論理的破壊が発生したときに、バックアップデータから対象のデータを復旧する。
  • アーカイブ: 法規制などに対応するため長期的なデータ保存が必要となるときに、バックアップデータを使用する。

また、これらの目的を達成することに加え、「Cloud Nativeな世界でのバックアップ・リストア」という観点で調べてみると、以下のような観点を合わせて検討することが重要となるようです。

サーバーでなくワークロード単位でバックアップ

Cloud Nativeな世界では、アーキテクチャ疎結合化により、それにかかわる組織やチームも疎結合となり、各チームが自律的に動けることを目指します。自律的なチームが複数に分かれると、各チームはそれぞれ独自のデータやワークロードをもつことになり、これらが分散して存在することになります。そのため、これまでの、データが一極集中管理されたシステムではなく、各チームごとにデータを所持するシステムでは、サーバー単位でなくワークロード単位でのバックアップが求められます。

例えば1つのKubernetesクラスター上で複数チームがワークロードを載せている場合、クラスターやNode単位でバックアップを取ると、バックアップデータには自分たちのワークロード以外のものも多く含まれ、そこから必要なものを探す手間が発生します。またリストアの際にクラスター・Node単位で実行するとなると、他のチームへの影響も発生し、組織全体のパフォーマンスが低下する要因となりえます。ワークロード単位でバックアップを取ることで、必要なものを見つけやすく、また自チーム内にのみ影響範囲を抑えることが可能となります。

データに着目してバックアップする

現在はCloud Nativeに関わらずインフラリソースのIaC化が進んでおり、必要なリソースはIaCファイルから作成することができます。バックアップを取得する際も、インフラリソースは定義ファイルから再現できるため、重要度は高くありません。一方で、そこに含まれる動的なデータは復元が難しいため、バックアップデータとして重要度が高くなります。

例としては以下のようなものが挙げられます。

  • ソースコードやビルドによる成果物など
  • アプリケーションの設定情報
  • インフラリソースの「状態」:インスタンス数やポート番号など
  • アプリ起動中に保存されたデータを含むデータベース
  • オブジェクトストレージに保存されたファイル

その他

その他、Cloud Nativeな環境で利用するバックアップ・リストアツールには、以下のような要素が含まれているとなお良いでしょう。

  • 複数環境・プロバイダーへの汎用性: オンプレやクラウドなど複数の環境で動作するシステムに対し、汎用的なツールを利用することで、不要な学習コストの削減やリストアのスピードアップにつながるでしょう。一方で、一つのクラウドプロバイダーのみを利用する予定の場合、各プロバイダーの提供するバックアップソリューションを利用することが、最も手軽かつ安全な場合もあります。

  • スケーラビリティ: システムの構成や規模が動的に変化するCloud Nativeなアーキテクチャでは、システムの構成変更に追従する、あるいは新規追加が容易なツールであるほど利用価値が高いといえるでしょう。


※参考ドキュメント:


バックアップ・リストアで考えること

バックアップ・リストアを設計・実装するうえで考えなければならないことは色々とあります。以下にその一例を記載します。

  • 利用目的
  • 取得頻度
  • バックアップ手法
  • 保存場所
  • 保存期間
  • 対象リソース

これらの観点に対して、Veleroはどのように対応しているかを見ていきます。

利用目的

バックアップ・リストアは、その利用目的を明確にすることが重要です。バックアップ・リストアを用いる理由は既に記載しましたが、Veleroでは以下のような用途を想定しています(Veleroドキュメントより)。

取得頻度

バックアップデータをどのような頻度で取得するかは、バックアップの利用目的によって変わります。作業前にバックアップデータを取得する、スケジュール実行で1日1回取得する、など、複数のタイミングでデータ取得を実行できる必要があります。

Veleroではコマンドからバックアップ・リストアを実行することができます。また velero schedule コマンドにより、Cron形式で指定したスケジュール実行も可能です。

バックアップ手法

バックアップの取得方式としては、以下のようなものがあります。

  • 完全バックアップ:バックアップ対象のすべてのデータをバックアップする
  • 増分バックアップ:前回のバックアップ以降に更新されたデータのみをバックアップする
  • 差分バックアップ:前回の完全バックアップ以降に更新されたデータのみをバックアップする

Veleroでは明確にどの手法か書かれてはいないように見えますが、実際に試したところでは完全バックアップしか提供していないように見えます。

保存場所

Veleroではオブジェクトストレージバケットをバックアップ・リストアデータの保存先として利用できます。オンプレ・クラウドどちらも利用可能です。

保存期間

Veleroではバックアップデータの保存期間は --ttl で設定することができます(Veleroドキュメントより)。デフォルトの保存期間は30日間です。またオブジェクトストレージに保存されたデータが削除されると、Veleroがそれを検知・同期して、クラスター上のバックアップデータを削除する動きになります。

対象リソース

VeleroではKubernetesクラスターリソース及びPersistent Volumeが保存対象です。Podなどのワークロードはjson形式で保存します。

PersistentVolumeでクラウドプロバイダーの提供するブロックストレージを利用している場合、PersistentVolumeに格納されたデータをスナップショットとして保存します。

Velero docs - Output file format

Veleroの紹介

構成

veleroの構成は以下の図のようになります。Veleroでは BackupController というControllerが中心となってバックアップ・リストア機能を提供します。Backup というCustom Resourceが作成されると、 BackupControllerKubernetes APIにクエリを投げてバックアップデータを収集し、オブジェクトストレージへバックアップデータのアップロードを行います。

velero

Veleroドキュメントより

Veleroを使ってみる

ここからVeleroを実際に動かしてみます。利用した環境は以下の通りです。

Amazon EKS上でVeleroを動かす際は、EKS Workshopのページに手順がコンパクトにまとまっているので楽です。

www.eksworkshop.com

Veleroを利用するには、 velero CLIのインストールと、クラスター上へのController等リソースの配置が必要になります。CLIのインストールはこちらのページから行い、Controllerなどのデプロイは、マニフェストファイルを用意するほか、velero CLIからも実行することが可能です。今回はCLIを利用する方法で準備を行いました。

$ velero install \
>     --provider aws \
>     --plugins velero/velero-plugin-for-aws:v1.1.0 \
>     --bucket velero-backup-20210523 \
>     --backup-location-config region=ap-northeast-1 \
>     --snapshot-location-config region=ap-northeast-1 \
>     --secret-file ./credentials-velero

$ kubectl get all -n velero
NAME                          READY   STATUS    RESTARTS   AGE
pod/velero-679457dc45-hfn9f   1/1     Running   0          101s

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/velero   1/1     1            1           101s

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/velero-679457dc45   1         1         1       101s

今回はAWSを利用するため --plugins velero/velero-plugin-for-aws:v1.1.0というオプションを指定しています。指定するバージョンはこちらのページを見て変更してください。また --secret-file で指定したファイルには、AWSにアクセスするのに必要な秘匿情報を記載しています。

※その他の利用可能なプロバイダーはこちらのページに記載されています。

Veleroのインストールが完了したので、バックアップの取得とリストアを試します。今回はEKSクラスターをもう一つ作成し、異なるクラスター上に同じワークロードを作成できるか試しました。

バックアップの取得

# バックアップ対象
$ kubectl get pods
NAME             READY   STATUS              RESTARTS   AGE
velero-testpod   1/1     Running             0          29s

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                STORAGECLASS   REASON   AGE
pvc-4b04c94b-c9bd-4206-8806-0e783b5ccce4   8Gi        RWO            Delete           Bound    default/velero-pvc   gp2                     2m45s

$ kubectl get pvc
NAME         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
velero-pvc   Bound    pvc-4b04c94b-c9bd-4206-8806-0e783b5ccce4   8Gi        RWO            gp2            81s



# バックアップの作成
$ velero backup create velero-backup-`date +%Y%m%d%H%M`
Backup request "velero-backup-202105230844" submitted successfully.
Run `velero backup describe velero-backup-202105230844` or `velero backup logs velero-backup-202105230844` for more details.



# バックアップ作成結果の確認
$ velero backup describe velero-backup-202105230844
Name:         velero-backup-202105230844
Namespace:    velero
Labels:       velero.io/storage-location=default
Annotations:  velero.io/source-cluster-k8s-gitversion=v1.19.8-eks-96780e
              velero.io/source-cluster-k8s-major-version=1
              velero.io/source-cluster-k8s-minor-version=19+

Phase:  Completed

Errors:    0
Warnings:  0

Namespaces:
  Included:  *
  Excluded:  <none>

Resources:
  Included:        *
  Excluded:        <none>
  Cluster-scoped:  auto

Label selector:  <none>

Storage Location:  default

Velero-Native Snapshot PVs:  auto

TTL:  720h0m0s

Hooks:  <none>

Backup Format Version:  1.1.0

Started:    2021-05-23 08:44:43 +0900 JST
Completed:  2021-05-23 08:44:50 +0900 JST

Expiration:  2021-06-22 08:44:43 +0900 JST

Total items to be backed up:  353
Items backed up:              353

Velero-Native Snapshots:  1 of 1 snapshots completed successfully (specify --details for more information)

クラスターの用意

# 別クラスターの用意
$ eksctl create cluster -f eks-clusterconfig-2.yml

$ eksctl get cluster
2021-05-23 10:18:20 [ℹ]  eksctl version 0.40.0
2021-05-23 10:18:20 [ℹ]  using region ap-northeast-1
NAME            REGION          EKSCTL CREATED
eks-cluster     ap-northeast-1  True
eks-cluster-2   ap-northeast-1  True★

# 別クラスターでのVeleroインストール
# バックアップデータの保存されているオブジェクトストレージを指定
$ velero install \
>     --provider aws \
>     --plugins velero/velero-plugin-for-aws:v1.1.0 \
>     --bucket velero-backup-20210523 \
>     --backup-location-config region=ap-northeast-1 \
>     --snapshot-location-config region=ap-northeast-1 \
>     --secret-file ./credentials-velero

# 作成したバックアップが確認できる
$ velero backup get
NAME                         STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
velero-backup-202105230844   Completed   0        0          2021-05-23 08:44:43 +0900 JST   29d       default            <none>

リストアによるリソース作成

# リストアの実行
$ velero restore create --from-backup velero-backup-202105230844
Restore request "velero-backup-202105230844-20210523095302" submitted successfully.
Run `velero restore describe velero-backup-202105230844-20210523095302` or `velero restore logs velero-backup-202105230844-20210523095302` for more details.


# リストア結果の確認
$ velero restore describe velero-backup-202105230844-20210523095302
Name:         velero-backup-202105230844-20210523095302
Namespace:    velero
Labels:       <none>
Annotations:  <none>

Phase:  Completed

Started:    2021-05-23 09:53:13 +0900 JST
Completed:  2021-05-23 09:53:44 +0900 JST

Warnings:
  Velero:     <none>
  Cluster:  could not restore, customresourcedefinitions.apiextensions.k8s.io "backups.velero.io" already exists. Warning: the in-cluster version is different than the backed-up version.
            could not restore, customresourcedefinitions.apiextensions.k8s.io "backupstoragelocations.velero.io" already exists. Warning: the in-cluster version is different than the backed-up version.
            could not restore, customresourcedefinitions.apiextensions.k8s.io "deletebackuprequests.velero.io" already exists. Warning: the in-cluster version is different than the backed-up version.
            could not restore, customresourcedefinitions.apiextensions.k8s.io "downloadrequests.velero.io" already exists. Warning: the in-cluster version is different than the backed-up version.
            could not restore, customresourcedefinitions.apiextensions.k8s.io "podvolumebackups.velero.io" already exists. Warning: the in-cluster version is different than the backed-up version.
            could not restore, customresourcedefinitions.apiextensions.k8s.io "podvolumerestores.velero.io" already exists. Warning: the in-cluster version is different than the backed-up version.
            could not restore, customresourcedefinitions.apiextensions.k8s.io "resticrepositories.velero.io" already exists. Warning: the in-cluster version is different than the backed-up version.
            could not restore, customresourcedefinitions.apiextensions.k8s.io "restores.velero.io" already exists. Warning: the in-cluster version is different than the backed-up version.
            could not restore, customresourcedefinitions.apiextensions.k8s.io "schedules.velero.io" already exists. Warning: the in-cluster version is different than the backed-up version.
            could not restore, customresourcedefinitions.apiextensions.k8s.io "serverstatusrequests.velero.io" already exists. Warning: the in-cluster version is different than the backed-up version.
            could not restore, customresourcedefinitions.apiextensions.k8s.io "volumesnapshotlocations.velero.io" already exists. Warning: the in-cluster version is different than the backed-up version.
            could not restore, mutatingwebhookconfigurations.admissionregistration.k8s.io "pod-identity-webhook" already exists. Warning: the in-cluster version is different than the backed-up version.
            could not restore, mutatingwebhookconfigurations.admissionregistration.k8s.io "vpc-resource-mutating-webhook" already exists. Warning: the in-cluster version is different than the backed-up version.
            could not restore, validatingwebhookconfigurations.admissionregistration.k8s.io "vpc-resource-validating-webhook" already exists. Warning: the in-cluster version is different than the backed-up version.
  Namespaces:
    default:      could not restore, endpoints "kubernetes" already exists. Warning: the in-cluster version is different than the backed-up version.
                  could not restore, endpointslices.discovery.k8s.io "kubernetes" already exists. Warning: the in-cluster version is different than the backed-up version.
                  could not restore, services "kubernetes" already exists. Warning: the in-cluster version is different than the backed-up version.
    kube-system:  could not restore, configmaps "aws-auth" already exists. Warning: the in-cluster version is different than the backed-up version.
                  could not restore, configmaps "cp-vpc-resource-controller" already exists. Warning: the in-cluster version is different than the backed-up version.
                  could not restore, configmaps "eks-certificates-controller" already exists. Warning: the in-cluster version is different than the backed-up version.
                  could not restore, configmaps "extension-apiserver-authentication" already exists. Warning: the in-cluster version is different than the backed-up version.
                  could not restore, configmaps "kube-proxy" already exists. Warning: the in-cluster version is different than the backed-up version.
                  could not restore, endpoints "kube-controller-manager" already exists. Warning: the in-cluster version is different than the backed-up version.
                  could not restore, endpoints "kube-dns" already exists. Warning: the in-cluster version is different than the backed-up version.
                  could not restore, endpoints "kube-scheduler" already exists. Warning: the in-cluster version is different than the backed-up version.
                  could not restore, leases.coordination.k8s.io "kube-controller-manager" already exists. Warning: the in-cluster version is different than the backed-up version.
                  could not restore, leases.coordination.k8s.io "kube-scheduler" already exists. Warning: the in-cluster version is different than the backed-up version.
                  could not restore, services "kube-dns" already exists. Warning: the in-cluster version is different than the backed-up version.

Backup:  velero-backup-202105230844

Namespaces:
  Included:  all namespaces found in the backup
  Excluded:  <none>

Resources:
  Included:        *
  Excluded:        nodes, events, events.events.k8s.io, backups.velero.io, restores.velero.io, resticrepositories.velero.io
  Cluster-scoped:  auto

Namespace mappings:  <none>

Label selector:  <none>

Restore PVs:  auto


$ kubectl get pods
NAME             READY   STATUS    RESTARTS   AGE
velero-testpod   1/1     Running   0          22m
$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                STORAGECLASS   REASON   AGE
pvc-4b04c94b-c9bd-4206-8806-0e783b5ccce4   8Gi        RWO            Delete           Bound    default/velero-pvc   gp2                     23m
$ kubectl get pvc
NAME         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
velero-pvc   Bound    pvc-4b04c94b-c9bd-4206-8806-0e783b5ccce4   8Gi        RWO            gp2            23m

取得頻度

velero schedule コマンドを実行すると、Cron形式で指定したサイクルでバックアップを取得することができます。

$ velero schedule create velero-backup-shcedule-`date +%Y%m%d` --schedule="*/10 * * * *"
Schedule "velero-backup-shcedule-20210523" created successfully.

# 初回バックアップの取得
$ velero backup get
NAME                                             STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
velero-backup-shcedule-20210523-20210523013142   Completed   0        0          2021-05-23 10:31:42 +0900 JST   30d       default            <none>

# 10分後
$ velero backup get
NAME                                             STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
velero-backup-shcedule-20210523-20210523014027   Completed   0        0          2021-05-23 10:40:27 +0900 JST   29d       default            <none>
velero-backup-shcedule-20210523-20210523013142   Completed   0        0          2021-05-23 10:31:42 +0900 JST   29d       default            <none>

対象リソース

Veleroのバックアップには複数のオプションがあります。以下のコマンドでは、指定のNamespace上にあるリソースのうち、Secret リソースをバックアップ対象から除外しています。

$ velero backup create velero-backup-exclude-`date +%Y%m%d%H%M%S` --include-namespaces default --exclude-resources secret

$ velero backup describe velero-backup-exclude-20210523112153

<中略>

Namespaces:
  Included:  default★
  Excluded:  <none>

Resources:
  Included:        *
  Excluded:        secret★
  Cluster-scoped:  auto

Secret リソースには秘匿情報が記載されており、このデータをストレージ上に保管することがセキュリティリスクとなる可能性があります。そのため、上記コマンドのような形でリソースを除外し、セキュリティリスクを軽減することが可能です。

保存期間

バックアップ取得時は --ttl オプションで保存期間を指定することができます。指定した保存期間を経過すると、そのバックアップデータは削除対象となり、しばらくすると削除されます。

$ velero backup create velero-backup-20210529-ttl-minutes --ttl 0h1m0s

$ velero backup get
NAME                                 STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
velero-backup-20210529               Completed   0        0          2021-05-29 09:21:55 +0900 JST   29d       default            <none>
velero-backup-20210529-ttl-minutes   Completed   0        0          2021-05-29 09:28:42 +0900 JST   43m ago   default            <none>

$ velero backup get
NAME                         STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
velero-backup-20210529       Completed   0        0          2021-05-29 09:21:55 +0900 JST   29d       default            <none>

$

またバックアップデータを保存しているオブジェクトストレージ側のデータを削除すると、クラスター上の backup リソースも削除されます。

$ velero backup create velero-backup-20210529-object-storage-test

$ velero backup get
NAME                                         STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
velero-backup-20210529                       Completed   0        0          2021-05-29 09:21:55 +0900 JST   29d       default            <none>
velero-backup-20210529-object-storage-test   Completed   0        0          2021-05-29 10:10:09 +0900 JST   30d       default            <none>


# S3バケットの削除
$ aws s3 rm s3://velero-backup-20210523/backups/velero-backup-20210529-object-storage-test --recursive
$ aws s3 rm s3://velero-backup-20210523/backups/velero-backup-20210529-object-storage-test


# Backupが削除される
$ velero backup get
NAME                                 STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
velero-backup-20210529               Completed   0        0          2021-05-29 09:21:55 +0900 JST   29d       default            <none>

※参考ドキュメント:


その他

その他、Veleroを利用するうえで気を付けたほうがよさそうな点についても書き残しておきます。

  • Veleroのバックアップは厳密にはatomicでないため、バックアップ中に作成されたリソースがデータに含まれない場合もあります(Veleroドキュメントより)

  • バックアップ前にPodでコマンドを実行したい場合もあると思います(例:DBでインメモリデータをディスクに吐き出すなど)が、その場合はbackup hooksを利用して、バックアップデータ取得前に特定のコマンドを実行することができます。

  • Veleroでは現在、複数ロケーションに同時にバックアップ・スナップショットを保存することはできません。一方でスケジュール実行で場所だけを変更して、複数ロケーションへの保存を実現することはできるようです(Veleroドキュメントより)。

  • バックアップデータの容量という観点から、重複排除の機能があるか、という点も重要となるかと思います。Veleroのドキュメントからは、重複排除の有無を見つけることができませんでしたが、resticでは重複排除に言及した部分があるため、内部的には実装しているのかもしれません。

参考ドキュメント

【メモ】CircleCIとAmazon ECR / EKSを利用したCI/CDパイプラインの例

はじめに

今回はCircleCIとAmazon ECRを利用したCI/CDパイプラインを用意し、コンテナイメージのビルドからKubernetesクラスタへのデプロイまで実行するサンプルを作成しました。

構成

今回は以下のようなCI/CDパイプラインを作成しました。

構成図は以下のようになります。CI/CDパイプラインはCircleCI、コンテナレジストリAmazon ECR、Kubernets環境はAmazon EKSを選択しています。

f:id:FY0323:20210515172615j:plain

CircleCI

今回利用したリポジトリディレクトリ構造はこちら。.circleci/config.yml 以外は前回のGitHub Actionsの例と同じファイルを使用しています。

.
├── .circleci
│   └── config.yml
├── README.md
├── app
│   ├── dockerfile
│   ├── main.go
│   └── main_test.go
└── manifest
    ├── deployment.yaml
    └── test.sh

CI/CDパイプラインは以下のようなファイルで構成しています。CircleCIを利用するには .circleci/config.yml ファイル上にワークフローを定義する必要があります。

今回はAmazon ECRへコンテナイメージをPushするために circleci/aws-ecrというOrbを利用しています。このOrbは、サンプルなどを見る限りWorkflow上で定義することが多いのですが、今回処理結果をSlackに通知したかったためecr-pushというJob上で定義しています。本当はJobの実行基盤をDockerにしたかったのですが上手くいかなかったため、ecr-push JobはVM上で実行しています。

./circleci/config.yml

version: 2.1
 
orbs:
  slack: circleci/slack@4.4.2
  aws-ecr: circleci/aws-ecr@7.0.0
  aws-eks: circleci/aws-eks@1.1.0
  kubernetes: circleci/kubernetes@0.12.0
 
commands:
  slack_fail:
    steps:
      - slack/notify:
          event: fail
          template: basic_fail_1
  slack_success:
    steps:
      - slack/notify:
          event: pass
          template: basic_success_1
 
jobs:
  go-test:
    docker:
      - image: circleci/golang:1.15.6
    working_directory: ~/repo
    steps:
      - checkout
      - run:
          name: Run tests
          command: go test -v ./app
      - slack_fail
  ecr-push:
    machine:
      image: ubuntu-2004:202010-01
    steps:
      - aws-ecr/build-and-push-image:
          account-url: AWS_ECR_ACCOUNT_URL
          aws-access-key-id: AWS_ACCESS_KEY_ID
          aws-secret-access-key: AWS_SECRET_ACCESS_KEY
          dockerfile: dockerfile
          path: ./app
          region: AWS_REGION
          repo: <repository-name>
          tag: latest
      - slack_fail
      - slack_success
 
  k8s-validate:
    docker:
      - image: circleci/golang:1.15.6
    working_directory: ~/repo
    steps:
      - checkout
      - run:
          name: install
          command: |
            wget https://github.com/instrumenta/kubeval/releases/latest/download/kubeval-linux-amd64.tar.gz
            tar xf kubeval-linux-amd64.tar.gz
            sudo cp kubeval /usr/local/bin
      - run:
          name: validation
          command: kubeval ./manifest/*.yaml
      - slack_fail
  k8s-deployment:
    executor: aws-eks/python3
    parameters:
      cluster-name:
        default: <cluster-name>
        type: string
      aws-region:
        default: <aws-region>
        type: string
    steps:
      - checkout
      - aws-eks/update-kubeconfig-with-authenticator:
          cluster-name: << parameters.cluster-name >>
          aws-region: << parameters.aws-region >>
          install-kubectl: true
      - kubernetes/create-or-update-resource:
          resource-file-path: manifest/deployment.yaml
      - run:
          name: check resource
          command: kubectl get pods
      - run:
          name: test apps
          command: |
            chmod +x ./manifest/test.sh
            sh ./manifest/test.sh
      - slack_fail
      - slack_success
 
 
workflows:
  docker-build-and-deploy:
    jobs:
      - go-test
      - k8s-validate
      - ecr-push:
          requires:
            - go-test
          filters:
            branches:
              only:
                - main
      - k8s-deployment:
          requires:
            - k8s-validate
            - ecr-push
          filters:
            branches:
              only:
                - main


利用したOrbsはこちら。

参考ドキュメントはこちら。


利用時の前提

パイプラインを利用するうえで必要な準備なども記載します。

CircleCIの登録

CircleCIに登録していない場合は登録と利用するリポジトリの選択を済ませておきます。

※参考ドキュメント:

Slackへの通知用設定

Jobの実行結果をSlackに通知するため、Slackの設定を行います。Slack AppにてTokenの取得と投稿用チャンネルの作成をしておきます。Slackでの設定はこちらのドキュメントに書かれた通り実行すれば完了するかと思います。

CircleCIでの変数の設定

CircleCIの Environment Variables には以下の情報を登録します。

  • AWS_ACCESS_KEY_ID: AWSへのアクセスに利用するアカウントID
  • AWS_SECRET_ACCESS_KEY: AWSへのアクセスに利用するシークレットアクセスキー
  • AWS_ECR_ACCOUNT_URL: Amazon ECRへのアクセスに利用するURL
  • AWS_REGION: AWSで利用するリージョン
  • SLACK_ACCESS_TOKEN: Slack Appで取得したアクセスToken
  • SLACK_DEFAULT_CHANNEL: Slack通知に利用するチャンネル名

その他

CircleCIでは、リポジトリ上のディレクトリやファイル毎にJobを起動することができない、と認識しています。そのため、以前GitHub Actionsでつくったパイプラインと異なり、今回はひとつのワークフローの中に、イメージのビルドとクラスターへのデプロイのJobを入れています。

CIとCDのプロセスを分けようと思った場合、いくつか方法はあると考えられます。

アプリ・インフラそれぞれ専用のリポジトリを用意する

リポジトリごとに.circleci/config.ymlを作成し、ワークフローを設定すれば処理を分けることが可能になり、CIとCDを分離することができます。アプリとインフラのコードを別のリポジトリに管理する場合、こちらの方法を採用することができるでしょう。

シェルスクリプト上で処理する

こちらの記事などで紹介されていますが、シェルスクリプト上でCircleCIを実行するタイミングをコントロールする、という方法もあるようです。プロジェクトで利用するコードをmonorepoで管理する場合、こちらの方法を採用することになるでしょう。

【メモ】GitHub ActionsとGitHub Container Registryを利用したCI/CDパイプラインの例

はじめに

今回はタイトルの通り、GItHub ActionsとGitHub Container Registryを利用したCI/CDを用意し、コンテナイメージのビルドからKubernetesクラスターへのデプロイまでを実行するサンプルを作成しました。

構成

CI/CDのパイプラインのパターンはいくつか考えられますが、今回メインで用意したのは、以下のような特徴を備えたものです。

今回の構成は以下のようになります。CI/CDパイプラインはGitHub Actionsで、コンテナイメージのレジストリGitHub Contianer Registryを利用しています。KubernetesクラスターはAmazon EKSを利用していますが、kubectl でアクセスできる環境であれば何でも良いと思います。

f:id:FY0323:20210504141133j:plain

f:id:FY0323:20210504141150j:plain

GitHub Actions

今回のサンプルはこちらのリポジトリに配置しています。

ディレクトリ構造は以下の通り。

.
├── .github
│   └── workflows
│       ├── docker-build-and-push.yaml
│       ├── k8s-deploy.yaml
│       ├── k8s-validate.yaml
│       └── test-and-build-code.yaml
├── README.md
├── app
│   ├── dockerfile
│   ├── main.go
│   └── main_test.go
└── manifest
    ├── deployment.yaml
    └── test.sh

CIは以下のようなアクションを用意しました。コンテナイメージビルド時に ghcr.io を指定して、GitHub Container Registryを利用しています。また secrets.USERNAME という値を指定していますが、この理由については後述します。

test-and-build-code.yaml

name: CI test Go code
on: 
  pull_request:
    branches:
    - main
    paths:
    - 'app/**'

jobs:
  test-and-build:
    runs-on: ubuntu-latest
    steps:
    - name: setup
      uses: actions/setup-go@v1
      with:
        go-version: 1.15
    - name: checkout
      uses: actions/checkout@v2
    - name: go test
      run: |
        go test -v ./app
    - name: notification
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: always() 

docker-build-and-push.yaml

name: CI docker build and push
on: 
  pull_request:
    branches:
    - main
    paths: 
    - 'app/**'
    types: [closed]

jobs:
  docker-build-and-push:
    runs-on: ubuntu-latest
    steps:
    - name: checkout
      uses: actions/checkout@v2
    - name: Set up Docker Builder
      uses: docker/setup-buildx-action@v1
    - name: Log into GitHub Container Registry
      uses: docker/login-action@v1
      with:
        registry: ghcr.io
        username: ${{ github.repository_owner }}
        password: ${{ secrets.CR_PAT }}
    - name: build container image
      uses: docker/build-push-action@v2
      with:
        context: ./app
        file: dockerfile
        push: true
        tags: ghcr.io/${{ secrets.USERNAME }}/sample-app:latest
    - name: notification
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: always() 


使用したActionsはこちら。

参考にしたドキュメントはこちら。


CDは以下のようなアクションを用意しました。

k8s-validate.yaml

name: CD validate Kubernetes manifests
on: 
  pull_request:
    branches:
    - main
    paths:
    - 'manifest/**'
    - 'app/**'

jobs:
  validation:
    name: validate k8s manifest
    runs-on: ubuntu-latest
    steps:
    - name: checkout
      uses: actions/checkout@v2
    - name: validation
      uses: instrumenta/kubeval-action@master
      with:
        files: ./manifest/
        version: 1.18
    - name: notification
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: always() 

k8s-deploy.yaml

name: CD deploy container to Kubernetes
on: 
  pull_request:
    branches:
    - main
    paths:
    - 'manifest/**'
    - 'app/**'
    types: [closed]

jobs:
  deploy-to-EKS:
    name: deploy manifests to k8s
    runs-on: ubuntu-latest
    steps:
    - name: checkout
      uses: actions/checkout@v2
    - name: login to AWS
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1
    - name: set kubectl
      uses: azure/setup-kubectl@v1
    - name: deploy manifest
      env:
        KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
      run: |
        echo "$KUBE_CONFIG" > /tmp/kubeconfig
        export KUBECONFIG=/tmp/kubeconfig
        kubectl apply -f manifest/
        chmod +x manifest/test.sh
        sh ./manifest/test.sh
    - name: notification
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: always() 

Kubernetes Podデプロイ後に実行しているテストスクリプト中で kubectl exec ~ を実行しています。GitHub ActionsではTTYを使用することができないため、 kubectl オプションには -i のみを指定しています。


使用したActionsはこちら。

参考にしたドキュメントはこちら。


利用時の前提条件

上記アクションを利用するうえで必要な設定も書いておきます。

GitHub Container Registryを利用可能にする

GitHub Container Registryは現在パブリックベータで提供されるため、GitHubでFeature Previewで有効にする必要があります。

f:id:FY0323:20210504130938j:plain

またGitHub Container Registryにアクセスするために、GitHubのPersonal Access Tokenを用意する必要があります。

f:id:FY0323:20210504131014j:plain

GitHub Secretsに必要な値を設定する

GitHub Secretsには以下の値を設定しています。

  • AWS_ACCESS_KEY_ID : AWSへのアクセスに利用するアカウントID
  • AWS_SECRET_ACCESS_KEY : AWSへのアクセスに利用するシークレットアクセスキー
  • CR_PAT : GitHub Container Registry用のPersonal Access Token。
  • KUBE_CONFIG : Kubernetesクラスターへアクセスするのに利用する kubeconfig
  • SLACK_WEBHOOK_URL : SlackチャンネルにJobの実行結果を送信するためのIncoming Webhook URL
  • USERNAME : コンテナレジストリへのイメージ格納時に使用

AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY は、利用するクラウドプロバイダーに合わせて変更します。KUBE_CONFIGクラスター作成後の ~/.kube/config ファイルの内容を設定します。

USERNAME については少し補足すると、通常は repository_owner を指定すればGitHubアカウント名を指定することができるのですが、私の作成したアカウント名に大文字が含まれており、このまま利用すると repository name must be lowercase. というエラーが発生します。そのためここでは小文字に直したアカウント名を指定しています。

KubernetesからイメージをPullできるようSecretを設定する

GitHub Container Registryはデフォルトでプライベートレジストリになります。KubernetesクラスターがプライベートレジストリからイメージをPullするには、コンテナレジストリにアクセスするためのSecretリソースを作成し、イメージを利用するPodマニフェスト中でImagePullSecret を設定する必要があります。

私はWindows端末でWSL2を利用しているため、以下のようなコマンドを実行してSecretリソースを作成しました。

# secretリソースの作成
$ kubectl create secret docker-registry regcred \
--docker-server=https://ghcr.io \
--docker-username=fy0323 \
--docker-password=<GitHub_Personal_Access_Token>

secret/regcred created


# 作成後の確認
$ kubectl get secret regcred -oyaml
apiVersion: v1
data:
  .dockerconfigjson: <base64_decoded_data>
kind: Secret
metadata:
  creationTimestamp: "2021-05-02T08:11:01Z"
  managedFields:
  - apiVersion: v1
    fieldsType: FieldsV1
    fieldsV1:
      f:data:
        .: {}
        f:.dockerconfigjson: {}
      f:type: {}
    manager: kubectl-create
    operation: Update
    time: "2021-05-02T08:11:01Z"
  name: regcred
  namespace: default
  resourceVersion: "75368"
  selfLink: /api/v1/namespaces/default/secrets/regcred
  uid: ee113b17-4d53-4e19-b475-887f3acc32a2
type: kubernetes.io/dockerconfigjson

Podマニフェストでは、以下のように spec.imagePullSecrets を指定します。

(中略)
    spec:
      containers:
      - name: app
        image: ghcr.io/fy0323/sample-app:latest
        ports:
        - containerPort: 8080
      imagePullSecrets:
      - name: regcred

その他

今回はCIとCDを別のタイミングで実行するよう設定していますが、CIでコンテナイメージをビルドしたタイミングで、テスト環境などのクラスターに対してデプロイを実行したい場合もあります。その場合は、今回のアクションをベースに、アクションの実行パスやタイミングを変更することで実現できます。

また、それとは別に、本番環境へのデプロイは、指定のレビューアの許可を得ることで実行したい、という場合もあります。GitHub Actionsの Environment の機能を利用することで、これを実現することが可能です。ただしこの機能はパブリックリポジトリでのみ利用可能なので、実際のプロジェクトで利用するには制限が発生しそうです。

f:id:FY0323:20210504133843j:plain

NATSに入門する

今回はメッセージングシステムの一つであるNATSを試してみました。

NATSとは

分散システムにおけるアプリケーションとサービス間のコミュニケーションは複雑で理解が難しいものとなります。現代のメッセージングシステムは複数のコミュニケーションパターンをサポートする必要があり、それに加えてセキュリティ・マルチテナンシー・拡張性など多様な要素も求められています。

NATSはこれらの要求を満たすために開発されたソフトウェアで、以下のような特徴を備えています。なおNATSはNeural Automatic Transport Systemの略称です。

複数のメッセージングパターンのサポート

NATSはSubjectをベースとしたメッセージングシステムで、PublisherがSubjectに対してメッセージを送信し、Subjectと紐づくSubscriberへメッセージを転送します。Subjectは、PublisherとSubscriberが互いを見つけるために利用する名前を形成する「文字列」です。Subjectは階層構造やワイルドカードなどを利用して表現でき、複数のSubscriberへの送信や、特定のパターンに合致したもののみにメッセージを送信するといったコントロールも可能になります。

ワイルドカードを利用したSubject(画像は公式ドキュメントより)

※特定のSubscriberにのみ送信されるパターン(画像は公式ドキュメントより)

NATSはPublish-Subscribe / Request-Reply / Queueという3つのメッセージングパターンに対応しています。

Publish-Subscribeの場合、NATSは「1対多」のコミュニケーションとして扱い、Publisherから送られてきたメッセージはSubjectが合致したアクティブなSubscriberへと送られます。

※画像:公式ドキュメントより

Request-Replyの場合、クライアントがReply Subject付きのRequestを送信すると、Responderがそれを捉えてReply Subject宛にResponseを返します。また複数のResponderを設定することも可能で、それらを使って動的なQueue Groupを形成し、スケールアップすることもできます。

※画像:公式ドキュメントより

Queueの場合、NATSは分散Queueと呼ばれる負荷分散機能を提供します。同一のQueue Nameを与えられたSubscriberはQueue Groupのメンバーとなり、Publisherから送られるメッセージがグループ内のメンバーに対して均一に送信されます。この設定はPublisher/Subscriber側で行い、Serverでは行いません。

※画像:公式ドキュメントより

At Most Once / At Least Onceのサポート

NATSはCore NATS (NATS Server、NATSとも呼ばれる)とNATS Streamingとで構成されます。Core NATSはクライアント間のメッセージをルーティングします。クライアントとの接続は、クライアントライブラリによって確立されるTCP接続を介して行われます。

※画像:GitHubより

nats-server

NATS StreamingはCore NATSのクライアントとして位置しています。NATS Streamingを利用するにはCore NATSが必要で、クライアントからの通信はCore NATSを介してNATS Streamingへ送られます。

※画像:GitHubより

nats-streaming

Core NATSはAT Most Onceと呼ばれるデリバリー戦略を採用しており、これは最高でも1回しか配信を行わず、再送は行わないことを意味します。これはメッセージの欠損する可能性を含んでおり、アプリケーションによってはこの戦略は適さない場合があります。

その一方でNATS StreamingではAt Least Once、つまり最低でも1回は配信されるよう再送を行います。こちらを採用することでSubscriberへの配信が保証できる一方、メッセージの重複が発生する可能性もあります。

NATS StreamingはメッセージをディスクやSQLデータベースに書き込むことでAt Least Onceを実現しており、Kubernetesへデプロイする際はPersistentVolume PersistentVolumeClaimを利用しています。


※参考ドキュメント:


高可用性・拡張性

NATSは可用性と拡張性を実現するため、Server Clusteringをサポートしています。NATS ServerはClusterを形成するすべてのNATS Serverと接続し、フルメッシュを形成します。NATSが利用するクラスタリングプロトコルでは、NATS Serverにクラスターメンバーを伝え、全てのNATS Serverはクラスター内の他のメンバーをDiscoverすることができます。またクライアントがNATS Serverに接続すると、クラスターに関する情報が通知されます。これにより、クラスターの構成が動的に変更したり、自己修復することを可能にします。

※画像:GitHubより

cluster

またクライアントライブラリでは、接続・Subscriptionを削除する際にDrainを行う機能が用意されています。これにより、実行中、あるいはキャッシュされたメッセージを処理してから、接続・Subscriptionの停止をすることが可能です。この機能を利用することで、メッセージが失われることなくScale Dwonを実行することも可能になります。


※参考ドキュメント:


その他


※参考ドキュメント:


NATSを使ってみる

ここから実際にNATSを動かしてみます。今回はKubernetesへNATS Serverをデプロイした後、Tutorialとして紹介されているこちらのページの内容をなぞっていきます。

検証環境

NATSのインストール

まずはNATSをインストールします。NATSはKubernetesへのインストールで利用するためのリポジトリを用意しており、こちらに置いてあるマニフェストファイルを利用します。

NATS、NATS Streaming用のマニフェストファイルは、それぞれ以下の通りです。今回はNATS、NATS Streamingを一つずつ作成する最小構成のパターンで行いました。

single-server-nats.yaml

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nats-config
data:
  nats.conf: |
    pid_file: "/var/run/nats/nats.pid"
    http: 8222
---
apiVersion: v1
kind: Service
metadata:
  name: nats
  labels:
    app: nats
spec:
  selector:
    app: nats
  clusterIP: None
  ports:
  - name: client
    port: 4222
  - name: cluster
    port: 6222
  - name: monitor
    port: 8222
  - name: metrics
    port: 7777
  - name: leafnodes
    port: 7422
  - name: gateways
    port: 7522
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nats
  labels:
    app: nats
spec:
  selector:
    matchLabels:
      app: nats
  replicas: 1
  serviceName: "nats"
  template:
    metadata:
      labels:
        app: nats
    spec:
      # Common volumes for the containers
      volumes:
      - name: config-volume
        configMap:
          name: nats-config
      - name: pid
        emptyDir: {}

      # Required to be able to HUP signal and apply config reload
      # to the server without restarting the pod.
      shareProcessNamespace: true

      #################
      #               #
      #  NATS Server  #
      #               #
      #################
      terminationGracePeriodSeconds: 60
      containers:
      - name: nats
        image: nats:2.1.7-alpine3.11
        ports:
        - containerPort: 4222
          name: client
          hostPort: 4222
        - containerPort: 7422
          name: leafnodes
          hostPort: 7422
        - containerPort: 6222
          name: cluster
        - containerPort: 8222
          name: monitor
        - containerPort: 7777
          name: metrics
        command:
         - "nats-server"
         - "--config"
         - "/etc/nats-config/nats.conf"

        # Required to be able to define an environment variable
        # that refers to other environment variables.  This env var
        # is later used as part of the configuration file.
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: CLUSTER_ADVERTISE
          value: $(POD_NAME).nats.$(POD_NAMESPACE).svc
        volumeMounts:
          - name: config-volume
            mountPath: /etc/nats-config
          - name: pid
            mountPath: /var/run/nats

        # Liveness/Readiness probes against the monitoring
        #
        livenessProbe:
          httpGet:
            path: /
            port: 8222
          initialDelaySeconds: 10
          timeoutSeconds: 5
        readinessProbe:
          httpGet:
            path: /
            port: 8222
          initialDelaySeconds: 10
          timeoutSeconds: 5

        # Gracefully stop NATS Server on pod deletion or image upgrade.
        #
        lifecycle:
          preStop:
            exec:
              # Using the alpine based NATS image, we add an extra sleep that is
              # the same amount as the terminationGracePeriodSeconds to allow
              # the NATS Server to gracefully terminate the client connections.
              #
              command: ["/bin/sh", "-c", "/nats-server -sl=ldm=/var/run/nats/nats.pid && /bin/sleep 60"]

single-server-stan.yaml

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: stan-config
data:
  stan.conf: |
    port: 4222
    http: 8222
    streaming {
     ns: "nats://nats:4222"
     id: stan
     store: file
     dir: /data/stan/store
    }
---
apiVersion: v1
kind: Service
metadata:
  name: stan
  labels:
    app: stan
spec:
  selector:
    app: stan
  clusterIP: None
  ports:
  - name: metrics
    port: 7777
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: stan
  labels:
    app: stan
spec:
  selector:
    matchLabels:
      app: stan
  serviceName: stan
  replicas: 1
  volumeClaimTemplates:
  - metadata:
      name: stan-sts-vol
    spec:
      accessModes:
      - ReadWriteOnce
      volumeMode: "Filesystem"
      resources:
        requests:
          storage: 1Gi
  template:
    metadata:
      labels:
        app: stan
    spec:
      # Prevent NATS Streaming pods running in same host.
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - topologyKey: "kubernetes.io/hostname"
            labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - stan
      # STAN Server
      containers:
      - name: stan
        image: nats-streaming:0.16.2
        ports:
        - containerPort: 8222
          name: monitor
        - containerPort: 7777
          name: metrics
        args:
         - "-sc"
         - "/etc/stan-config/stan.conf"

        # Required to be able to define an environment variable
        # that refers to other environment variables.  This env var
        # is later used as part of the configuration file.
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        volumeMounts:
          - name: config-volume
            mountPath: /etc/stan-config
          - name: stan-sts-vol
            mountPath: /data/stan

        # Disable CPU limits.
        resources:
          requests:
            cpu: 0

        livenessProbe:
          httpGet:
            path: /
            port: 8222
          initialDelaySeconds: 10
          timeoutSeconds: 5
      volumes:
      - name: config-volume
        configMap:
          name: stan-config

上記マニフェストファイルを利用してインストールを行います。

# GitHubリポジトリのクローン
$ git clone https://github.com/nats-io/k8s
$ cd k8s/


# NATSのインストール
$ kubectl apply -f nats-server/single-server-nats.yml 
configmap/nats-config created
service/nats created
statefulset.apps/nats created

# NATS Streamingのインストール
$ kubectl apply -f nats-streaming-server/single-server-stan.yml 
configmap/stan-config created
service/stan created
statefulset.apps/stan created


# インストール後の確認
$ kubectl get cm
NAME          DATA   AGE
nats-config   1      57s
stan-config   1      26s


$ kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                                                 AGE
kubernetes   ClusterIP   10.100.0.1   <none>        443/TCP                                                 2d
nats         ClusterIP   None         <none>        4222/TCP,6222/TCP,8222/TCP,7777/TCP,7422/TCP,7522/TCP   74s
stan         ClusterIP   None         <none>        7777/TCP                                                43s


# NATS Streamingが利用するPV/PVC
$ kubectl get pvc
NAME                  STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
stan-sts-vol-stan-0   Bound    pvc-37cf09c9-b32b-4573-bb3b-9a32d6bfff7d   1Gi        RWO            gp2            20h


$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                         STORAGECLASS   REASON   AGE
pvc-37cf09c9-b32b-4573-bb3b-9a32d6bfff7d   1Gi        RWO            Delete           Bound    default/stan-sts-vol-stan-0   gp2                     20h


$ kubectl get sc
NAME            PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
gp2 (default)   kubernetes.io/aws-ebs   Delete          WaitForFirstConsumer   false                  2d22h


$ kubectl get statefulsets
NAME   READY   AGE
nats   1/1     94s
stan   1/1     64s


$ kubectl get pods
NAME     READY   STATUS    RESTARTS   AGE
nats-0   1/1     Running   0          101s
stan-0   1/1     Running   0          71s


# NATSのログは以下の通り
$ kubectl logs nats-0
[7] 2020/12/28 04:50:09.349020 [INF] Starting nats-server version 2.1.7
[7] 2020/12/28 04:50:09.349061 [INF] Git commit [bf0930e]
[7] 2020/12/28 04:50:09.349569 [INF] Starting http monitor on 0.0.0.0:8222
[7] 2020/12/28 04:50:09.349616 [INF] Listening for client connections on 0.0.0.0:4222
[7] 2020/12/28 04:50:09.349625 [INF] Server id is NBBQ76INWDZXMFBGF5V4MBXCV3PBEQ5EWNFPPKS3FSTNPF3VFCR3RY2K
[7] 2020/12/28 04:50:09.349629 [INF] Server is ready


# NATS Streamingのログは以下の通り
$ kubectl logs stan-0
[1] 2020/12/28 04:51:02.605219 [INF] STREAM: Starting nats-streaming-server[stan] version 0.16.2
[1] 2020/12/28 04:51:02.605250 [INF] STREAM: ServerID: TgyiHElDc171yiUZ30g5pL
[1] 2020/12/28 04:51:02.605254 [INF] STREAM: Go version: go1.11.13
[1] 2020/12/28 04:51:02.605258 [INF] STREAM: Git commit: [910d6e1]
[1] 2020/12/28 04:51:02.631773 [INF] STREAM: Recovering the state...
[1] 2020/12/28 04:51:02.631932 [INF] STREAM: No recovered state
[1] 2020/12/28 04:51:02.883723 [INF] STREAM: Message store is FILE
[1] 2020/12/28 04:51:02.883738 [INF] STREAM: Store location: /data/stan/store
[1] 2020/12/28 04:51:02.883770 [INF] STREAM: ---------- Store Limits ----------
[1] 2020/12/28 04:51:02.883774 [INF] STREAM: Channels:                  100 *
[1] 2020/12/28 04:51:02.883778 [INF] STREAM: --------- Channels Limits --------
[1] 2020/12/28 04:51:02.883781 [INF] STREAM:   Subscriptions:          1000 *
[1] 2020/12/28 04:51:02.883785 [INF] STREAM:   Messages     :       1000000 *
[1] 2020/12/28 04:51:02.883788 [INF] STREAM:   Bytes        :     976.56 MB *
[1] 2020/12/28 04:51:02.883791 [INF] STREAM:   Age          :     unlimited *
[1] 2020/12/28 04:51:02.883795 [INF] STREAM:   Inactivity   :     unlimited *
[1] 2020/12/28 04:51:02.883798 [INF] STREAM: ----------------------------------
[1] 2020/12/28 04:51:02.883802 [INF] STREAM: Streaming Server is ready

インストールが完了したので、簡単な動作確認を行います。

NATSはnats-boxというコンテナイメージを用意しており、これを利用することでNATSの各コマンドを実行することができます。

# nats-boxコンテナの起動
$ kubectl run -i --rm --tty nats-box --image=synadia/nats-box --restart=Never
If you dont see a command prompt, try pressing enter.
nats-box:~#


# バックグラウンドでSubscriberを起動
nats-box:~# nats-sub -s nats hello &
nats-box:~# Listening on [hello]


# Publisherからメッセージを送信
nats-box:~# nats-pub -s nats hello world
[#1] Received on [hello]: 'world'


# PublisherからNATS Streamingへメッセージ送信
nats-box:~# stan-pub -s nats -c stan hello world
Published [hello] : 'world'


# NATS Streamingからも送られたメッセージが確認できる
nats-box:~# stan-sub -s nats -c stan hello
Connected to nats clusterID: [stan] clientID: [stan-sub]
Listening on [hello], clientID=[stan-sub], qgroup=[] durable=[]
[#1] Received: sequence:1 subject:"hello" data:"world" timestamp:1609131378713322197


# NATS Streaming Podでは以下のようなログが確認できる
$ kubectl logs stan-0 -f

[1] 2020/12/28 04:56:18.713310 [INF] STREAM: Channel "hello" has been created

NATSはその他にも複数のツールを提供しており、例えばnats-topコマンドを実行するとNATS Serverのモニタリングを行うことができます。

nats-box:~# nats-top -s nats

NATS server version 2.1.7 (uptime: 8m24s)
Server:
  Load: CPU:  0.0%  Memory: 9.9M  Slow Consumers: 0
  In:   Msgs: 26  Bytes: 1022  Msgs/Sec: 0.0  Bytes/Sec: 0
  Out:  Msgs: 25  Bytes: 985  Msgs/Sec: 0.0  Bytes/Sec: 0

Connections Polled: 3
  HOST                  CID    NAME                SUBS    PENDING     MSGS_TO     MSGS_FROM   BYTES_TO    BYTES_FROM  LANG     VERSION  UPTIME   LAST ACTIVITY
  192.168.1.45:56406    1      _NSS-stan-send      0       0           0           8           0           159         go       1.8.1    7m57s    2020-12-28 08:45:11.674703256 +0000 U  192.168.1.45:56408    2      _NSS-stan-general   8       0           8           5           361         461         go       1.8.1    7m57s    2020-12-28 08:45:11.677642417 +0000 U  192.168.1.45:56410    3      _NSS-stan-acks      0       0           4           0           36          0           go       1.8.1    7m57s    2020-12-28 08:45:11.673086331 +0000 U

またNATSはモニタリング機能を提供しており、以下のようにPort: 8222からアクセスすることで、各種設定やメッセージ受信数などを確認することができます。

$ kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                                                 AGE
kubernetes   ClusterIP   10.100.0.1   <none>        443/TCP                                                 2d20h
nats         ClusterIP   None         <none>        4222/TCP,6222/TCP,8222/TCP,7777/TCP,7422/TCP,7522/TCP   44s
stan         ClusterIP   None         <none>        7777/TCP                                                27s

# ポートフォワーディング
$ kubectl port-forward svc/nats 8222:8222 &
[1] 10071

Forwarding from [::1]:8222 -> 8222

f:id:FY0323:20201229135232j:plain

例えばlocalhost:8222/subszにアクセスすると、Subscriptionの数などが表示されます。

f:id:FY0323:20201229135246j:plain

NATSを利用する

NATS/NATS Streamingの起動確認ができたので、クライアントライブラリからNATS Serverを利用してみます。今回はnats.goというGoのライブラリに含まれているサンプルプログラムを実行していきます。

まずはnat.goパッケージを入手します。Goのバージョンが古いと取得できないことがあるので、必要があればバージョンアップを実施します。

$ go version
go version go1.15.6 linux/amd64

$ go get github.com/nats-io/nats.go/

また以降の操作ではlocalhostからNATS Serverにアクセスするため、kubectl port-forwardコマンドによりNATSへアクセスできるようにします。

$ kubectl port-forward svc/nats 4222:4222 &
[2] 10088
$ Forwarding from 127.0.0.1:4222 -> 4222
Forwarding from [::1]:4222 -> 4222
NATS Pub/Sub

まずはPub/Subパターンを実行します。ここではPublisher1つ、Subscriber 3つを起動し、それぞれにメッセージが配信される様子を確認します。

まずは1つ目のSubscriberを起動します。ここではmsg.testというSubjectを指定します。

# Subscriber-1
$ cd $GOPATH/src/src/github.com/nats-io/nats.go/examples
$ go run nats-sub/main.go msg.test
Listening on [msg.test]

次に別のターミナルを用意し、Publisherを起動、メッセージを送信します。

# Publisher
# “hello”メッセージを送信
$ cd $GOPATH/src/src/github.com/nats-io/nats.go/examples
$ go run nats-pub/main.go msg.test hello

# “NATS MESSAGE”メッセージを送信
$ go run nats-pub/main.go msg.test "NATS MESSAGE"

Publisherからメッセージを送信すると、Publisher側では送信したことを示すメッセージが表示され、Subscriber側ではメッセージの受け取りが確認できます。

# Publisher
Published [msg.test] : 'hello'
Published [msg.test] : 'NATS MESSAGE'


# Subscriber-1
[#1] Received on [msg.test]: 'hello'
[#2] Received on [msg.test]: 'NATS MESSAGE'

次に新しいターミナルを開き、2つ目のSubscrierを起動します。ここでもmsg.testSubjectを指定します。

# Subscriber-2
$ cd $GOPATH/src/src/github.com/nats-io/nats.go/examples
$ go run nats-sub/main.go msg.test
Listening on [msg.test]

この状態でPublisherからメッセージを送信すると、2つのSubscriberへメッセージが送信されることが確認できます。

# Publisher
$ go run nats-pub/main.go msg.test "NATS MESSAGE 2"
Published [msg.test] : 'NATS MESSAGE 2'

# Subscriber-1
[#3] Received on [msg.test]: 'NATS MESSAGE 2'

# Subscriber-2
[#1] Received on [msg.test]: 'NATS MESSAGE 2'

ここでmsg.test.newという別のSubjectを指定したSubscriberを起動します。

# Subscriber-3
$ cd $GOPATH/src/src/github.com/nats-io/nats.go/examples
$ go run nats-sub/main.go msg.test.new
Listening on [msg.test.new]

ここで再びPublisherからメッセージを送信すると、今回はSubscriber-3はメッセージを受け取らない様子が確認できます。これはSubscriberで指定したmsg.test.newというSubjectが、Publisherがメッセージ送信時に指定したmsg.testというSubjectと一致しなかったためです。

# Publisher
$ go run nats-pub/main.go msg.test "NATS MESSAGE 3"
Published [msg.test] : 'NATS MESSAGE 3'

# Subscriber-1
[#4] Received on [msg.test]: 'NATS MESSAGE 3'

# Subscriber-2
[#2] Received on [msg.test]: 'NATS MESSAGE 3'

# Subscriber-3

(no message)

ここでSubscriber-3を一度停止し、起動時に指定するSubjectをmsg.*に変更します。今度はSubjectにワイルドカードを含み、先ほどPublisherが指定したmsg.testが条件に一致するため、メッセージを受け取る様子が確認できます。

# Subscriber-3を停止、再起動
$ go run nats-sub/main.go msg.test.new
Listening on [msg.test.new]

^Csignal: interrupt

$ go run nats-sub/main.go msg.*
Listening on [msg.*]


# Publisher
$ go run nats-pub/main.go msg.test "NATS MESSAGE 4"
Published [msg.test] : 'NATS MESSAGE 4'

# Subscriber-1
[#5] Received on [msg.test]: 'NATS MESSAGE 4'

# Subscriber-2
[#3] Received on [msg.test]: 'NATS MESSAGE 4'

# Subscriber-3
[#1] Received on [msg.test]: 'NATS MESSAGE 4'

なお、NATSを起動する際に特定のオプションを指定することで、NATS Server側でログを出力することができます。例えば-Vオプションを追加すると、Subscriber・Publisher間でやり取りが発生した際、以下のようにログが出力されます。

single-server-nats.yaml

        command:
         - "nats-server"
         - "-V"  # 追加
         - "--config"
         - "/etc/nats-config/nats.conf"
$ kubectl logs nats-0 -f
# NATS Server起動時のログ
[7] 2020/12/29 04:42:11.784867 [INF] Starting nats-server version 2.1.7
[7] 2020/12/29 04:42:11.784895 [INF] Git commit [bf0930e]
[7] 2020/12/29 04:42:11.785860 [INF] Starting http monitor on 0.0.0.0:8222
[7] 2020/12/29 04:42:11.785910 [INF] Listening for client connections on 0.0.0.0:4222
[7] 2020/12/29 04:42:11.785919 [INF] Server id is NDK7LIGLOBR5MC5DOJKNVHHTUOBZWIMJRVXBRNNDFNHM5UKUZKJPZ6ST
[7] 2020/12/29 04:42:11.785923 [INF] Server is ready



# Subscriber-1起動時
[7] 2020/12/29 04:43:35.461255 [TRC] 127.0.0.1:38544 - cid:1 - <<- [CONNECT {"verbose":false,"pedantic":false,"tls_required":false,"name":"NATS Sample Subscriber","lang":"go","version":"1.11.0","protocol":1,"echo":true,"headers":false,"no_responders":false}]
[7] 2020/12/29 04:43:35.461364 [TRC] 127.0.0.1:38544 - cid:1 - <<- [PING]
[7] 2020/12/29 04:43:35.461372 [TRC] 127.0.0.1:38544 - cid:1 - ->> [PONG]
[7] 2020/12/29 04:43:35.471896 [TRC] 127.0.0.1:38544 - cid:1 - <<- [SUB msg.test  1]
[7] 2020/12/29 04:43:35.471916 [TRC] 127.0.0.1:38544 - cid:1 - <<- [PING]
[7] 2020/12/29 04:43:35.471921 [TRC] 127.0.0.1:38544 - cid:1 - ->> [PONG]
[7] 2020/12/29 04:43:37.787668 [TRC] 127.0.0.1:38544 - cid:1 - ->> [PING]
[7] 2020/12/29 04:43:37.798709 [TRC] 127.0.0.1:38544 - cid:1 - <<- [PONG]



# Publisher起動・メッセージ送信時
[7] 2020/12/29 04:44:02.334750 [TRC] 127.0.0.1:38702 - cid:2 - <<- [CONNECT {"verbose":false,"pedantic":false,"tls_required":false,"name":"NATS Sample Subscriber","lang":"go","version":"1.11.0","protocol":1,"echo":true,"headers":false,"no_responders":false}]
[7] 2020/12/29 04:44:02.334822 [TRC] 127.0.0.1:38702 - cid:2 - <<- [PING]
[7] 2020/12/29 04:44:02.334829 [TRC] 127.0.0.1:38702 - cid:2 - ->> [PONG]
[7] 2020/12/29 04:44:02.345670 [TRC] 127.0.0.1:38702 - cid:2 - <<- [SUB msg.test  1]
[7] 2020/12/29 04:44:02.345694 [TRC] 127.0.0.1:38702 - cid:2 - <<- [PING]
[7] 2020/12/29 04:44:02.345700 [TRC] 127.0.0.1:38702 - cid:2 - ->> [PONG]
[7] 2020/12/29 04:44:04.424048 [TRC] 127.0.0.1:38702 - cid:2 - ->> [PING]
[7] 2020/12/29 04:44:04.437254 [TRC] 127.0.0.1:38702 - cid:2 - <<- [PONG]
NATS Request/Reply

次にRequest/Replyパターンを実行します。ここではPublisher/Subscriberを一つずつ起動し、Publisherからメッセージを送信するとResponseが返ってくる様子を確認します。

まずはSubscriberを起動します。ここではhelp.pleaseというSubjectを設定し、またResponseメッセージも設定します。

# Subscriber
$ go run nats-rply/main.go help.please "OK, I CAN HELP!"
Listening on [help.please]

次にPublisherを起動し、同一のSubjectとRequestメッセージを指定します。

# Publisher
$ go run nats-req/main.go help.please "I need help!"

Publisherがメッセージを送信すると、Subscriberがメッセージを受け取る様子に加え、Publisher側でResponseメッセージが表示されることが確認できます。

# Subscriber
[#1] Received on [help.please]: 'I need help!'


# Publisher
Published [help.please] : 'I need help!'
Received  [_INBOX.g672l3mO17Q5MuaBrdZalB.roViAMQe] : 'OK, I CAN HELP!'  # Response Message
NATS Queueing

最後にQueueingパターンを実行します。ここでは複数のSubscriberを起動してQueue Groupを構成し、Publisherからのメッセージが各Subscriberに分散されて送られる様子を確認します。

まずは2つのSubscriberをQueue Groupを与えて起動します。

# Subscriber-1 (Queue Group)
$ go run nats-qsub/main.go foo my-queue
Listening on [foo], queue group [my-queue]

# Subscriber-2 (Queue Group)
$ go run nats-qsub/main.go foo my-queue
Listening on [foo], queue group [my-queue]

次にQueue Groupを指定せず、先ほどと同じSubjectを指定したSubscriberを起動します。

# Subscriber-3 (No Queue Group)
go run nats-sub/main.go foo
Listening on [foo]

この状態でPublisherからメッセージが送られると、Queue Groupに属するSubscriberはメッセージが分散されて送られますが、Queue Groupに属さないSubscriberは全てのメッセージが配信されます。

# Publisher
$ go run nats-pub/main.go foo "Hello NATS-1"

# 同様のメッセージを計5回送信する
$ go run nats-pub/main.go foo "Hello NATS-5"


# Subscriber-1 (Queue Group)
[#1] Received on [foo] Queue[my-queue] Pid[11326]: 'Hello NATS-3'
[#2] Received on [foo] Queue[my-queue] Pid[11326]: 'Hello NATS-4'


# Subscriber-2 (Queue Group)
[#1] Received on [foo] Queue[my-queue] Pid[11392]: 'Hello NATS-1'
[#2] Received on [foo] Queue[my-queue] Pid[11392]: 'Hello NATS-2'
[#3] Received on [foo] Queue[my-queue] Pid[11392]: 'Hello NATS-5'


# Subscriber-3 (No Queue Group)
[#1] Received on [foo]: 'Hello NATS-1'
[#2] Received on [foo]: 'Hello NATS-2'
[#3] Received on [foo]: 'Hello NATS-3'
[#4] Received on [foo]: 'Hello NATS-4'
[#5] Received on [foo]: 'Hello NATS-5'

参考ドキュメント