ArgoCD内でのRepositoryの取り扱いについて

こんにちは!

最近開発チームにジョインした新卒のsZma5aと申します。

今回は、ArgoCDのApplication Setを使用する際に遭遇したRepository周辺のエラーを踏まえ、どのようにApplicationで指定したRepositoryが使用されているかをまとめたいと思います。
先にエラーの原因から申し上げると、今回の問題はkubectlを使用しApplication SetとApplicationを適用したことにより、一部バリデーションが通らなかったことが原因でした。

経緯

弊社では、Kubernetesを活用しインフラ設計を行っていますが、マニフェストとクラスタの実態との間に乖離が生じるという問題がありました。これ解決するため、各環境(dev, stage, prod)で適用されているリソースの差分検出と運用を簡便化する目的でArgoCDを導入しました。
私がこのタスクを前任者から引き継いだ時点では、すでにクラスタの構築が完了しいくつかのApplicationが登録されていました。この状態から、新たにApplicationを増やし、ArgoCDの管理下にリソースを順次追加するという方法も考えられました。しかし、リソースが増えて管理が難しくなること、そして各リソースに一貫性を持たせる必要性を考慮し、ApplicationSetを用いることにしました。

発生したエラー内容

今回遭遇したエラーについては、以下のようなリポジトリに関する許可がないというものでした。この原因自体は単純なものであり、GitリポジトリについてApplication Set内とRepositoryではHTTPで指定しているのに対し、ProjectではSSHで登録していたことに起因していました。

ArgoCDリソースの値

  • Project(spec.sourceRepos): git://github.com/{org}/{repo}.git
  • Repository(data.url): https://github.com/{org}/{repo}.git

登録しようとしたApplicationの値

  • Application(spec.source.repoURL): https://github.com/{org}/{repo}.git
status:
  conditions:
  - lastTransitionTime: "2023-06-30T08:21:42Z"
    message: 'application spec is invalid: InvalidSpecError: application repo https://github.com/{org}/{repo}.git
      is not permitted in project ''{project name}'''
    reason: ApplicationValidationError
    status: "True"
    type: ErrorOccurred

コードリーディング

上述の通り、新たに設定したApplication Setについては正常にバリデーションが機能していました。しかし、不明な点は、なぜ既存のApplicationがこのような不完全な状態でも動作していたのか、ということでした。そこで、バリデーションとApplication登録周辺に関するコードを調べた内容を下記に示します。

ApplicationSetのバリデーションについて

始めにバリデーションの挙動について確認します。

先程のエラーが発生した場所を調べたところ、最終的にはcontroller内で投げられていますが、起点は該当リポジトリがProjectで使用可能かを判定するValidationPermissionsであることがわかります。具体的な処理を調べるため、使用されている関数(IsSourcePermitted)の実装を見ると、下記の通りApplicationで使用するリポジトリがProjectで登録されているかについて確認しています。先述の通り、エラー発生時Projectには誤った値が設定されており、Applicationで指定したものと一致していなかったためここでエラーが発生したことがわかります。

func (proj AppProject) IsSourcePermitted(src ApplicationSource) bool {
    srcNormalized := git.NormalizeGitURL(src.RepoURL)

    var normalized string
    anySourceMatched := false

    // ProjectのSourceReposとApplicationで指定されたSourceを比較している
    for _, repoURL := range proj.Spec.SourceRepos {
        if isDenyPattern(repoURL) {
            normalized = "!" + git.NormalizeGitURL(strings.TrimPrefix(repoURL, "!"))
        } else {
            normalized = git.NormalizeGitURL(repoURL)
        }

        matched := globMatch(normalized, srcNormalized, true, '/')
        if matched {
            anySourceMatched = true
        } else if !matched && isDenyPattern(normalized) {
            return false
        }
    }

    return anySourceMatched
}

ApplicationのRepositoryに関する処理について

ArgoCDの登録されている値チェック

なぜApplicationが動作していたのかを調べるため、まずCLIを用いて登録状態を確認します。以下の結果から、RepositoriesではProjectで指定した値が参照されているのに対し、Scoped RepositoriesではRepositoryで指定したものが使用されている様子が伺えます。

これらはどちらもAPIを通し値を取得していますが、前者はProjectで指定したものをそのまま反映させているのに対し、後者はIndexerを通し、Secretリソースとして登録したRepositoryを取得し表示しています。これらのことから、ProjectでリポジトリのURLを指定せずとも、Repository側のみでProjectと紐づけることができることがわかりました。

argocd proj get {project} --grpc-web
Name:                        {project}
Description:                 
Destinations:                https://kubernetes.default.svc
Repositories:                git@github.com:{org}/{repo}.git
Scoped Repositories:         https://github.com/{org}/{repo}.git
Allowed Cluster Resources:   <none>
Scoped Clusters:             <none>
Denied Namespaced Resources: <none>
Signature keys:              <none>
Orphaned Resources:          disabled

上記の結果から、Applicationは動作している以上ProjectのsourceReposは参照されず、直接Repositoryで登録した内容を呼び出していることが考えられます。

Application実行時の挙動確認

先程の内容から、Applicationが直接Repositoryの情報を使用していると考えられるため、マニフェストとApplicationを比較するために用いられるAppStateManagerを中心に実装を確認します。

ArgoCD Controller内での比較処理については、SyncAppStateから呼び出されているCompareAppState内でリポジトリの参照が行われています。具体的には、Sync対象のマニフェストが存在しない場合において、getRepoObjsを通しGetRefSourcesでIndexer内のリポジトリ情報を取得しています。この時、Applicationで指定されているspec.source.repoURLをもとにを呼び出していることがわかります。

func GetRefSources(ctx context.Context, spec argoappv1.ApplicationSpec, db db.ArgoDB) (argoappv1.RefTargetRevisionMapping, error) {
    refSources := make(argoappv1.RefTargetRevisionMapping)
    if spec.HasMultipleSources() {
        // Validate first to avoid unnecessary DB calls.
        refKeys := make(map[string]bool)
        for _, source := range spec.Sources {
            if source.Ref != "" {
                isValidRefKey := regexp.MustCompile(^[a-zA-Z0-9_-]+$).MatchString
                if !isValidRefKey(source.Ref) {
                    return nil, fmt.Errorf("sources.ref %s cannot contain any special characters except '_' and '-'", source.Ref)
                }
                refKey := "$" + source.Ref
                if _, ok := refKeys[refKey]; ok {
                    return nil, fmt.Errorf("invalid sources: multiple sources had the same ref key")
                }
                refKeys[refKey] = true
            }
        }
        // Get Repositories for all sources before generating Manifests
        for _, source := range spec.Sources {
            if source.Ref != "" {
                repo, err := db.GetRepository(ctx, source.RepoURL)
                if err != nil {
                    return nil, fmt.Errorf("failed to get repository %s: %v", source.RepoURL, err)
                }
                refKey := "$" + source.Ref
                refSources[refKey] = &argoappv1.RefTarget{
                    Repo:           *repo,
                    TargetRevision: source.TargetRevision,
                    Chart:          source.Chart,
                }
            }
        }
    }
    return refSources, nil
}

まとめ

以上のことから、ApplicationについてはProjectを経由せずにrepoURLの値から直接Repositoryの値を参照しているため、単体では正常に動作していたことがわかりました。

また、Applicationは実行時にSharedIndexInformerを通し取得されているため、今回ようなトラブルを防ぐためには適用時にバリデーションを行う必要があります。ArgoCDから提供されているCLIを用いて作成した場合、きちんとバリデーションを通すことができるので今後はこちらの使用しArgoCDを運用していこうと思います。

最後に

AI Shiftでは、toB向けのSaaS事業を展開しており、絶賛はエンジニアを募集しています。もし少しでも興味をお持ちでいただけましたら、是非下記フォームより応募いただけると幸いです。

(オンライン・19時以降の面談も可能です)

【面談フォームはこちら】
https://hrmos.co/pages/cyberagent-group/jobs/1826557091831955459

最後までお読みいただきありがとうございました!

PICK UP

TAG