Open18

AKS で git-sync を使って Azure Files に複製するときの試行錯誤

yukiko_bassyukiko_bass

環境

AKS

やりたいこと

  • アプリケーションが読み取るファイル(フォルダ階層あり)をボリュームマウントして、永続ストレージとする。その永続ストレージはAzureFilesとファイル共有をする
  • アプリケーションが読み取るファイル(フォルダ階層あり)はgitリポジトリにある
  • gitリポジトリはそれなりに更新されるので、cloneだけではなくpullもされる必要がある(同期が必要)
  • ファイル量/階層はそれなりに多い
  • gitのsyncがされたら、アプリケーションpodに通知したい
yukiko_bassyukiko_bass

最初はgitOpsでできるんじゃないかと思ってたけど、なんか思ってたのと違った。
gitOps は ArgoCDを入れている。

Copilotにきいたらこんな感じでできるよ!とは言われたけどできなかった。
Gitリポジトリは 以下。

https://username@dev.azure.com/organizationName/ProjectName/_git/dev-files-sync
  • manifest // ArgoCD のアプリケーションマニフェスト

  • config-files // 実アプリケーションが読み取りたいフォルダ
    という構成。
    別リポジトリだとややこしいかなと思って、同じリポジトリにしてみたけど、よりややこしくなった感がある。

  • ArgoCDに登録するアプリケーションのマニフェスト

files-sync-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: files-sync # アプリケーションの名前
spec:
  destination:
    namespace: default # デプロイ先のネームスペース
    server: https://kubernetes.default.svc # デプロイ先のクラスター
  project: default # Argo CD のプロジェクト名
  source:
    path: manifests # Git リポジトリ内のマニフェストファイルがあるパス
    repoURL: https://username@dev.azure.com/organizationName/ProjectName/_git/dev-files-sync
    targetRevision: HEAD # Git リポジトリのリビジョン
  syncPolicy:
    automated: # 自動同期を有効にする
      prune: false # クラスター内の不要なリソースを削除する
      selfHeal: true # クラスター内のリソースの状態を修復する
files-sync.yaml
apiVersion: v1
kind: Pod
metadata:
  name: git-repo-sync # ポッドの名前
spec:
  containers:
  - name: git-repo-sync # コンテナの名前
    image: argoproj/gitops-agent # GitOps エンジンを提供するイメージ
    args:
    - /bin/sh
    - -c
    - |
      git clone https://<PAT>@dev.azure.com/organizationName/ProjectName/_git/dev-files-sync /mnt/sync-files # Git リポジトリを Azure Files にクローンする
      cd /mnt/sync-files # クローンしたディレクトリに移動する
      gitops --repo-url https://<PAT>@dev.azure.com/organizationName/ProjectName/_git/dev-files-sync --branch main --path . --dest-path /mnt/sync-files # GitOps エンジンを起動する
    volumeMounts:
    - name: azurefile # ボリュームの名前
      mountPath: /mnt/sync-files # マウント先のパス
  volumes:
  - name: azurefile # ボリュームの名前
    persistentVolumeClaim:
      claimName: azurefile # 永続ボリュームクレームの名前
yukiko_bassyukiko_bass

AzureFiles を永続ストレージとして使うよ宣言

ストレージアカウントはAKSを作ると自動でできるAKSのネットワークとかもろもろのリソースがあるリソースグループに作成。MC_ から始まるやつ。ここではMC_rg_2 としときます。
https://learn.microsoft.com/ja-jp/azure/aks/faq#why-are-two-resource-groups-created-with-aks

azurefiles-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  annotations:
    pv.kubernetes.io/provisioned-by: file.csi.azure.com
  name: azurefile
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: azurefile-csi
  csi:
    driver: file.csi.azure.com
    readOnly: false
    volumeHandle: unique-volumeid  # make sure this volumeid is unique for every identical share in the cluster
    volumeAttributes:
      resourceGroup: MC_rg_2
      shareName: files-sync // ファイル共有名
    nodeStageSecretRef:
      name: azure-secret
      namespace: default
  mountOptions:
    - dir_mode=0777
    - file_mode=0777
    - uid=0
    - gid=0
    - noperm
azurefiles-mount-options-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: azurefile
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: azurefile-csi
  volumeName: azurefile
  resources:
    requests:
      storage: 5Gi

azure-secret は以下のコマンドで登録しておく

Azure Cloud Shell
kubectl create secret generic azure-secret --from-literal=azurestorageaccountname=<ストレージ名> --from-literal=azurestorageaccountkey=<ストレージキー>
yukiko_bassyukiko_bass

ちなみに、azure-secret をyamlで登録するとなんでか認証が通らなくなる。

azurefiles-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: azure-secret
type: Opaque
data:
  azurestorageaccountname: ストレージアカウントをbase64エンコードした値
  azurestorageaccountkey: ストレージキー

指定の仕方が悪いんかなぁ


追記

ストレージキーもbase64エンコードが必要だった。
ストレージキー自体がbase64なのになんでやと思ってたけど、コマンドでやるときは値をbase64でエンコードして保存しているけど、ファイルの場合はプレーンで保存されるから、そこで値が一致しなくなるとかなんだろうな。

ということで以下が正解。

azurefiles-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: azure-secret
type: Opaque
data:
  azurestorageaccountname: ストレージアカウントをbase64エンコードした値
  azurestorageaccountkey: ストレージキーをbase64エンコードした値

ストレージキーのbase64エンコードは以下コマンドで。
base64 -w 0 にしているのは、値を途中で改行されたので。

echo "ストレージキー" | base64 -w 0

さらに追記

yamlファイルにはわざわざエンコードしなくてもいい。関数あるらしい

azurefiles-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
  name: azure-secret
type: Opaque
data:
  azurestorageaccountname: {{ .Values.secret.azurestorageaccountname | b64enc }}
  azurestorageaccountkey: {{ .Values.secret.azurestorageaccountkey | b64enc }}
yukiko_bassyukiko_bass

azurefiles-pv.yaml
azurefiles-mount-options-pvc.yaml
はHelmチャートに入れ込みたい。
本アプリのpodができる前に作られていてほしい

yukiko_bassyukiko_bass

helmプロジェクトのtemplateフォルダの下に置いておけば良いらしい。
PersistentVolumeやPersistentVolumeClaimはdeploymentより前に動く仕様らしいので、あんまり順序とか気にしなくても最初の方にやってくれる
あとはvalues.yaml に変数切り出したりするだけ。

今現状動いているファイル/フォルダ構成は以下

git-sync-app
├─templates
│  ├─azurefiles-mount-options-pvc.yaml
│  ├─azurefiles-pv.yaml
│  ├─azurefiles-secret.yaml
│  ├─deployment.yaml
│  ├─ingress.yaml
│  └─service.yaml
├─Chart.yaml
└─values.yaml
yukiko_bassyukiko_bass

実アプリケーションのHelmチャート
AzureFilesをマウントしたところまで

template/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-deployment
  namespace: {{ default "staging" .Release.Namespace }}
  labels:
    app: {{ .Values.label.app }}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: {{ .Values.label.app }}
  template:
    metadata:
      labels:
        app: {{ .Values.label.app }}
    spec:
      containers:
        - name: {{ .Release.Name }}
          image: {{ .Values.image.registry }}.azurecr.io/{{ .Values.image.name }}:{{ default "latest" .Values.image.tag }}
          resources:
            requests:
              cpu: 300m
              memory: 512Mi
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: azurefile
              mountPath: /mnt/sync-files
      volumes:
        - name: azurefile
          persistentVolumeClaim:
            claimName: azurefile
template/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}
  namespace: {{ default "staging" .Release.Namespace }}
spec:
  selector:
    app: {{ .Values.label.app }}
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 8080
  type: ClusterIP
template/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ .Release.Name }}
  namespace: {{ default "staging" .Release.Namespace }}
  annotations:
    kubernetes.io/ingress.class: addon-http-application-routing
spec:
  rules:
    - host: <AKSのサブドメイン>.{{ .Values.dns.name }}
      http:
        paths:
          - backend:
              service:
                name: {{ .Release.Name }}
                port:
                  name: http
            path: /
            pathType: Prefix
values.yaml
label:
  app: main-app
image:
  registry: ACRのリポジトリ名
  name: imageの名前
  tag: latest
dns:
  name: *****.japaneast.aksapp.io
yukiko_bassyukiko_bass

ストレージ周りの値をまだvaluesに持っていけてないから最終的には持っていく

yukiko_bassyukiko_bass

アプリケーションのimageのハッシュが更新されたのに、全然podのイメージが変わらなかった問題
(イメージのタグが変わってないのにハッシュが変わったのは開発であれこれやってるせい)
(運用環境だとタグ変わるから大丈夫かな)

imagePullPolicy を Always で常にレジストリを見に行くようにする

deployment.yaml
  template:
    metadata:
      labels:
        app: {{ .Values.label.app }}
    spec:
      containers:
        - name: {{ .Release.Name }}
          image: {{ .Values.image.registry }}.azurecr.io/{{ .Values.image.name }}:{{ default "latest" .Values.image.tag }}
          imagePullPolicy: Always

デフォルトはIfNotPresent。
コンテナランタイムにいない場合に取得しに行く、とあるのでローカルキャッシュ的なのを参照するってことかな。

https://blog.andyserver.com/2021/09/adding-image-digest-references-to-your-helm-charts/

yukiko_bassyukiko_bass

サイドカーコンテナでgit-syncを使うやり方があるっぽいのでやってみる。
試行錯誤した結果できたっぽい。

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-deployment
  namespace: {{ default "staging" .Release.Namespace }}
  labels:
    app: {{ .Values.label.app }}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: {{ .Values.label.app }}
  template:
    metadata:
      labels:
        app: {{ .Values.label.app }}
    spec:
      containers:
        - name: {{ .Release.Name }}
          image: {{ .Values.image.registry }}.azurecr.io/{{ .Values.image.name }}:{{ default "latest" .Values.image.tag }}
          resources:
            requests:
              cpu: 300m
              memory: 512Mi
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: azurefile
              mountPath: /mnt/sync-files
        - name: git-sync # サイドカーコンテナの名前
          image: registry.k8s.io/git-sync/git-sync:v4.1.0 # git-sync
          securityContext: # セキュリティコンテキストを追加する
            runAsUser: 0 # ユーザーIDを0にする
            runAsGroup: 0 # グループIDを0にする
          volumeMounts:
            - name: azurefile # メインコンテナと同じボリュームをマウントする
              mountPath: /mnt/sync-files # マウント先のディレクトリ
          args:
            - --repo=https://<PAT>@dev.azure.com/organizationName/ProjectName/_git/dev-files-sync
            - --depth=1
            - --period=30s
            - --ref=main
            - --link=current
            - --root=/mnt/sync-files
      volumes:
        - name: azurefile
          persistentVolumeClaim:
            claimName: azurefile # AzureFilesのCSIドライバーを使ってプロビジョニングされたファイル共有を参照するPVCの名前

ずっとpermissionエラーが出て、なかなか解決しなかったけど、AzureFiles にファイルを置くときのユーザが問題だったよう

このsecurityContextがポイント。
ボリュームをマウントするのはrootユーザ(0)
git-syncでcloneとかするときにchmodしたりするらしんだけど、その時のユーザと一致してなくてpermissionで怒られたらしい。
明示的にsecurityContextでユーザを指定してあげるとよいとか。
この辺のことが書いてたページがどっかにいってしまった。。

          securityContext: # セキュリティコンテキストを追加する
            runAsUser: 0 # ユーザーIDを0にする
            runAsGroup: 0 # グループIDを0にする

runAsGroup じゃなくてfsGroupでもよいのかも?

参考

https://github.com/kubernetes/git-sync/blob/master/docs/kubernetes.md

yukiko_bassyukiko_bass

AzureFilesに共有ができたときの中身

current はシンボリックリンク。
--link=current で指定したときの名前で作成される。
指定しない場合は、gitのリポジトリ名で作られた。

で実体はどこにあるかというと、.worktrees の中にブランチ(タグもかな?)ごとにディレクトリが作成される。

このフォルダにリンクが貼られている形なので、使う方(アプリ)側からは、/mnt/sync-files/current を起点にすればよい。

yukiko_bassyukiko_bass

git-sync で使えるオプションは以下

Usage of /git-sync:
      --add-user                          add a record to /etc/passwd for the current UID/GID (needed to use SSH with an arbitrary UID)
      --askpass-url string                a URL to query for git credentials (username=<value> and password=<value>)
      --cookie-file                       use a git cookiefile (/etc/git-secret/cookie_file) for authentication
      --credential credentialSlice        one or more credentials (see --man for details) available for authentication (default [])
      --depth int                         create a shallow clone with history truncated to the specified number of commits (default 1)
      --error-file string                 the path (absolute or relative to --root) to an optional file into which errors will be written (defaults to disabled)
      --exechook-backoff duration         the time to wait before retrying a failed exechook (default 3s)
      --exechook-command string           an optional command to be run when syncs complete (must be idempotent)
      --exechook-timeout duration         the timeout for the exechook (default 30s)
      --git string                        the git command to run (subject to PATH search, mostly for testing) (default "git")
      --git-config string                 additional git config options in 'section.var1:val1,"section.sub.var2":"val2"' format
      --git-gc string                     git garbage collection behavior: one of 'auto', 'always', 'aggressive', or 'off' (default "always")
      --group-write                       ensure that all data (repo, worktrees, etc.) is group writable
  -h, --help                              print help text and exit
      --http-bind string                  the bind address (including port) for git-sync's HTTP endpoint
      --http-metrics                      enable metrics on git-sync's HTTP endpoint
      --http-pprof                        enable the pprof debug endpoints on git-sync's HTTP endpoint
      --link string                       the path (absolute or relative to --root) at which to create a symlink to the directory holding the checked-out files (defaults to the leaf dir of --repo)
      --man                               print the full manual and exit
      --max-failures int                  the number of consecutive failures allowed before aborting (-1 will retry forever
      --one-time                          exit after the first sync
      --password string                   the password or personal access token to use for git auth (prefer --password-file or this env var)
      --password-file string              the file from which the password or personal access token for git auth will be sourced
      --period duration                   how long to wait between syncs, must be >= 10ms; --wait overrides this (default 10s)
      --ref string                        the git revision (branch, tag, or hash) to sync (default "HEAD")
      --repo string                       the git repository to sync (required) (default "https://5h4quzpeia6wn5v273bxxp5xjzqfraltxi6iofeimsjvcy6g7kna@dev.azure.com/mtisoldev/MobileConvert/_git/dev-mcp-site-config")
      --root string                       the root directory for git-sync operations (required) (default "/git")
      --sparse-checkout-file string       the path to a sparse-checkout file
      --ssh-key-file stringArray          the SSH key(s) to use (default [/etc/git-secret/ssh])
      --ssh-known-hosts                   enable SSH known_hosts verification (default true)
      --ssh-known-hosts-file string       the known_hosts file to use (default "/etc/git-secret/known_hosts")
      --stale-worktree-timeout duration   how long to retain non-current worktrees
      --submodules string                 git submodule behavior: one of 'recursive', 'shallow', or 'off' (default "recursive")
      --sync-on-signal string             sync on receipt of the specified signal (e.g. SIGHUP)
      --sync-timeout duration             the total time allowed for one complete sync, must be >= 10ms; --timeout overrides this (default 2m0s)
      --touch-file string                 the path (absolute or relative to --root) to an optional file which will be touched whenever a sync completes (defaults to disabled)
      --username string                   the username to use for git auth
  -v, --verbose int                       logs at this V level and lower will be printed
      --version                           print the version and exit
      --webhook-backoff duration          the time to wait before retrying a failed webhook (default 3s)
      --webhook-method string             the HTTP method for the webhook (default "POST")
      --webhook-success-status int        the HTTP status code indicating a successful webhook (0 disables success checks (default 200)
      --webhook-timeout duration          the timeout for the webhook (default 1s)
      --webhook-url string                a URL for optional webhook notifications when syncs complete (must be idempotent)
yukiko_bassyukiko_bass

本家のマニュアルを見るに、git-syncのv3までは
環境変数は GIT_SYNC_XXXX というプリフィクスだったけど、v4からは
GITSYNC_XXXX という形式になってるっぽい。

yukiko_bassyukiko_bass

よく使うコマンド

  • Helm
helm list
helm install リリース名 .
helm upgrade リリース名 .
helm uninstall リリース名
helm history リリース名
  • kubectl
# pod のリスト
kubectl get pods

# deploy したpod の詳細を確認
kubectl describe pod リリース名

# アプリケーションがマウントしているボリュームの中の確認
kubectl exec リリース名-deployment-********** -- ls -l /mnt/files-sync

# サイドカー(git-sync)のログ取得
kubectl logs pod名 -c git-sync

# 変更を適用
kubectl apply -f azurefiles-pv.yaml

# シークレットの削除
kubectl delete secret azure-secret
yukiko_bassyukiko_bass
yukiko_bassyukiko_bass

以下のyamlを用意して、適用する。

サービスアカウントの作成

sample-serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sample-serviceaccount
  namespace: app-namespace
Azure Cloud Shell
$ kubectl apply -f sample-serviceaccount.yaml
$ kubectl get serviceaccount sample-serviceaccount -o yaml -n app-namespace

kubectl を実行できるpodの作成

sample-kubectl.yaml
apiVersion: v1
kind: Pod
metadata:
  name: sample-kubectl
  namespace: app-namespace
spec:
  serviceAccountName: sample-serviceaccount
  containers:
    - name: kubectl-container
      image: lachlanevenson/k8s-kubectl:v1.10.4
      command: ["sleep", "86400"]
Azure Cloud Shell
$ kubectl apply -f sample-kubectl.yaml
$ kubectl get pod sample-kubectl -n app-namespace

ClusterRoleBinding の作成

podのステータスを見たいだけなので、プリセットのviewロールを付与するようにする

sample-clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: sample-clusterrolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: view
subjects:
- kind: ServiceAccount
  name: sample-serviceaccount
  namespace: app-namespace
$ kubectl apply -f sample-clusterrolebinding.yaml
clusterrolebinding.rbac.authorization.k8s.io/sample-clusterrolebinding created
$ kubectl describe clusterrolebinding sample-clusterrolebinding
Name:         sample-clusterrolebinding
Labels:       <none>
Annotations:  <none>
Role:
  Kind:  ClusterRole
  Name:  view
Subjects:
  Kind            Name                   Namespace
  ----            ----                   ---------
  ServiceAccount  sample-serviceaccount  app-namespace

確認

Azure Cloud Shell
$ kubectl exec -it sample-kubectl -n app-namespace-- kubectl get pods
NAME                                                READY     STATUS                  RESTARTS   AGE
git-sync                                            1/1       Running                 0          3h
sample-kubectl                                      1/1       Running                 0          2h

本当にやりたかったコマンド

Azure Cloud Shell
$ kubectl exec -it sample-kubectl -n app-namespace -- /bin/sh -c "until [ \"$(kubectl get pods -l app=git-sync -n app-namespace -o jsonpath='{.items[0].status.phase}')\" == 'Running' ]; do sleep 1; done"