この1年でインフラ担当としてやったこと その2

2021/12/11に公開

この記事は「イエソド アウトプット筋 トレーニング Advent Calendar 2021」11日目の記事です。

この記事は前回の続きとなります。

GitOpsの導入

k8sリソースをkustomizeで管理をするようになり、環境差分を柔軟に管理できるようになりました。そのためk8sリソースを別リポジトリに分けてGitOpsによってCDのフローを構築することにしました。GitOpsを導入したかった理由は、本番反映に対する承認フローをGitのPRに乗っかりたかったのと、k8sリソースの状況を誰でもGUI上から確認できるようにしておきたくてargocdを導入したかった二点です。アプリケーションコードレポジトリの特定ブランチの更新やタグの付与によってCloudBuildのCIが走ってビルドに成功したらDockerfileにビルドしてGCR上にアップロード、それが成功したらk8sリソースを管理しているレポジトリをcloneしてCloudBuild上でファイルの更新をかけ、pushするというフローを取りました。また、本番環境のビルドである場合は、それに加えてk8sリソースのdevelopブランチからmasterブランチに対してのPRを作成するようなフローにしました。それによって本番環境反映の際にはgithub上にできたPRをマージするだけで好きなタイミングで本番反映を行えるようにしました。

Argo Rolloutsの導入によるBlueGreenDeploymentの実現

CI/CDはある程度整備をすることはできたのですが、課題は残っていました。本番環境ではもちろんアプリケーションPodは冗長化されているのですが、冗長化されていることにより、k8sの標準のDeployment Strategyであるローリングアップデートでは、本番適用が完了するまでの間古いアプリケーションPodと新しいアプリケーションPodが混在するタイミングがあり、それによって予期せぬ不具合が発生する恐れがあるという課題が残っていました。それを解消するためにDeploymentリソースを全てArgo RolloutsのカスタムリソースであるRolloutに置き換えることにしました。RolloutsはDeploymentとほぼ同様のリソース定義に加え、より詳細なDeployment Strategyを構築することが可能です。これにより、CanaryリリースやBlue Green Deploymentを簡単に実現することが可能で、私たちのユースケースとしてはBlueGreenDeploymentを導入しました。その結果、アプリケーションコードの新旧混在という事象は起きなくなり、本番反映の考慮点が一つ減ったことは大きな変化でした。

Helmの導入とkustomizeとの住み分け

StackDriver監視だけでなく、ある程度自分たちでカスタマイズしたメトリクスを監視したいし、Prometheusを導入したりLokiスタックを導入したりなどOSSのツールをいくつか導入していきたいと思っていました。それらのツールをインストールするにあたってやはりHelmで導入するのが一番楽だなとなって、Helmを使用することにしました。管理方法についてどうしようかなと考えたところHelmのテンプレートを落としてレポジトリで管理してArgoCDに管理させるようにすればいいなと思い、そのように管理することにしました。ディレクトリ構成を以下のようにしました。

.
├── helm
│   └── charts
│       ├── argo-cd
│       ├── grafana
...
│       ├── loki-stack
│       └── prometheus
├── kustomize
│   ├── base
│   │   ├── argocd
│   │   │   ├── loki.yaml
...
│   │   │   ├── kustomization.yaml
│   │   │   └── prometheus_application.yaml
│   └── overlays
└── pre_install # 初回クラスター構築時の初期設定自動スクリプト置き場

また、各application.yamlの中身のサンプルは以下のような形で設定をします

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: prometheus
spec:
  destination:
    name: ''
    namespace: XXXX
    server: XXXX
  source:
    path: helm/charts/prometheus
    repoURL: XXXX
    targetRevision: XXXX # ブランチ名
    helm:
      valueFiles:
        - values.yaml ### ここをoverlaysで各環境ごとに置き換えてあげて、環境差分を作る
  project: XXXX
  syncPolicy:
    automated:
      prune: true

このようなディレクトリ構成にすることによって、kustomizeディレクトリをkustomizeで適用してあげるだけで必要なツールのインストールまで完了するような仕組みにしています。

ArgoCD ResourceHooksを使用して、マイグレーションの自動実行

今までは本番反映時にDBのマイグレーションを実行する際には、手動でマイグレーション作業を行なっていましたが、環境が増えてきたことに伴って、アプリケーションコードとあるべきDB構造のズレが発生したり、純粋にマイグレーション実行のし忘れなどが多発してきました。そのためArgoCD ResourceHooksを使用して、アプリケーションコードの反映時にマイグレーションを実行するようにしました。マイグレーションについては、アプリケーションコードのディレクトリ内に置き、マイグレーションの実行にはgolang-migrateを使用することにしました。
CloudBUild上でCIを実行するタイミングでビルドが成功すると、CloudBuildから各環境のクラスターに対して自身のCOMMIT_SHAを書き込んだConfigMapを適用します。全てのビルドが成功するとk8sリソースのレポジトリに対してコミットが走るため、それを検知してArgoCDによるデプロイ作業が開始します。ArgoCD ResourceHooksによってPreSyncJobとしてマイグレーションのJobが実行され、マイグレーションが実行され、完了後アプリケーションコードが反映されるという流れになっています。この際にハマったこととして、弊社はDBとしてCloudSQLを使用していますが、マイグレーションのJobコンテナはCloudSQLと通信するためにアンバサダーパターンを使用してCLoudSQL Proxyを介して通信するようにしています。Job完了時にCloudSQL Proxyのコンテナが終了してくれず、永久にJobが動き続けてしまうという問題が発生しました。それを回避するためにこちらのissueを参考にtrapコマンドを使用してJobのEXITシグナルを受け取った際にCloudSQLコンテナにマウントしているディレクトリ内に一時ファイルを生成して、CloudSQLのプロセスはそれが生成されるまで実行をするという風に書いてあげることで回避をすることにしました。

apiVersion: batch/v1
kind: Job
metadata:
  name: XXXXX
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
  backoffLimit: X
  parallelism: X
  completions: X
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: migration-container
        image: XXXXX
        imagePullPolicy: Always
        command:
          - "/bin/bash"
          - -c
        args:
          - |
            trap "touch /tmp/pod/terminated" EXIT
            /usr/local/bin/XXXX.sh # マイグレーション実行するためのスクリプト
        envFrom:
          - configMapRef:
              name: XXXXX # CloudBuildからapplyしたCOMMIT_SHAを含む設定ファイル
          - secretRef:
              name: XXXXX # DBのパスワードなどの情報
        readinessProbe:
          exec:
            command:
              - psql
          initialDelaySeconds: 10
        volumeMounts:
          - mountPath: /tmp/pod
            name: tmp-pod
      - name: cloudsql-proxy
        image: gcr.io/cloudsql-docker/gce-proxy:1.23.0-alpine
        command:
          - "/bin/ash"
          - -c
        args:
          - |
            /cloud_sql_proxy -instances=${PROJECT}:${DEST_DB_REGION}:${DEST_DB_INSTANCE}=tcp:5432 -verbose=false & CHILD_PID=$!
            (while true; do if [[ -f "/tmp/pod/terminated" ]]; then kill $CHILD_PID; fi; sleep 1; done) &
            wait $CHILD_PID
            if [[ -f "/tmp/pod/terminated" ]]; then exit 0; fi
        envFrom:
          - configMapRef:
              name: XXXX

マイグレーション自動実行の導入により、アプリケーションコードの反映の際に、実行し忘れによるエラーが発生することは無くなりました。

最後に

Istio導入まで書きたかったのですが、少し長くなってきたので次回に回します。

Discussion