GitOps本番2年で痛感した、教科書と現実のギャップ|ArgoCD・Flux運用の罠と対策
GitOps導入で本番環境が大混乱。2年の運用で学んだ同期失敗・シークレット管理・マルチクラスタの地雷と、実装した実践的な対策をコード付きで公開します。
先日のプロジェクトで学んだ、GitOpsの「教科書通り」な落とし穴
うちのチームでGitOpsを本格導入したのが今から2年前。当初は「Infrastructure as Code(IaC)を徹底して、Git履歴が全てになる世界」みたいなイメージで、かなり理想化されていたんですよね。でも実際に本番環境に適用してみたら、想像とぜんぜん違う現実が待ってました。
正直、最初の6ヶ月は地獄でした。デプロイパイプラインが詰まるわ、シークレット管理で悶え苦しむわ、マルチクラスタ環境では同期がずれるわ。でも2年運用してようやく「GitOpsって何なのか」が腑に落ちたので、その過程で学んだことを共有したいんです。
GitOps導入で最初にぶつかった「同期失敗の地雷」
最初はArgoCDを入れて「よし、これでGit pushするだけでデプロイされる天国だ」と思ってたんですよ。でも1週間目からもう問題が出ました。
問題1: Git上のリビジョンと実際の環境がずれ続ける
ArgoCDのデフォルト設定だと、同期間隔が3分間隔なんですよね。でも本番環境では、どうしても手動で一時的にリソースを調整することがあるんです。そうするとGitの定義と実際のクラスタが乖離して、次の同期タイミングで勝手に戻されるという地獄の自動ロールバック。これ、本当に頭を抱えました。
対策として実装したのがこれです:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops-repo
targetRevision: HEAD
path: apps/my-app
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
key pointは selfHeal: true ですね。これで自動的にGitの定義に戻してくれます。でも正直なところ、この設定だけだと不十分だったんです。チーム側のルール作りが本当に大事だったんですよ。
「本番環境に手動変更を加えるのはいいけど、必ず1時間以内にGitに反映させる」みたいなポリシーを定めることで、同期ズレを最小限に抑えられるようになりました。文書化して全員で共有するのが大切です。
問題2: 大量リソース同期時のクラスタへの負荷
ArgoCDは優秀ですが、1回の同期で1000個のリソースを処理するとなると、ちょっと怖いんですよ。Webhook連携で「Gitにpushされたから即座に同期」となると、高いアクセス負荷がクラスタにかかります。実際、本番環境で大量同期がトリガーされたときは、APIサーバーの応答が落ちかけました。
そこで実装したのが段階的な同期戦略です。ディレクトリ構造をこう分けました:
GitOps/
├── apps/
│ ├── tier-1-critical/ # 最優先
│ │ ├── kube-system/
│ │ └── cert-manager/
│ ├── tier-2-core/ # 次点
│ │ └── my-service/
│ └── tier-3-optional/ # 最後
│ └── monitoring/
ArgoCDのApplicationも3つに分けて、Webhookのトリガーを段階化しました:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app-tier1
spec:
syncPolicy:
automated:
prune: false # 慎重に制御
selfHeal: true
syncOptions:
- RespectIgnoreDifferences=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
こうすることで、1つのマニフェスト変更が全体に波及するのを防げるようになりました。地味に重要な工夫です。
シークレット管理:GitOpsの最大の落とし穴
正直、ここが一番難しかった。「Gitに全部あるべき」というGitOpsの理想と「パスワードやAPIキーはGitに絶対置くな」というセキュリティ常識が真っ向から対立するんですよね。
うちが最終的に落ち着いたのは Sealed Secrets + External Secrets Operator の組み合わせです。
# インストール
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml
Secretを暗号化してGitに保存できるようになります:
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: my-secret
namespace: default
spec:
encryptedData:
password: AgBx8k3f2+DvL...[暗号化されたデータ]
template:
metadata:
name: my-secret
namespace: default
type: Opaque
sealed-secrets-key というマスターキーは絶対にGitに入れません。これをクラスタのシークレットストアに保管しておくことで、「Gitには暗号化されたデータ、クラスタには復号化キー」という分離ができるんです。
でも実際には、より柔軟な External Secrets Operator に移行しました。こっちの方が運用が楽だったんですよ:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets
spec:
provider:
aws:
service: SecretsManager
region: ap-northeast-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: my-app-secret
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets
kind: SecretStore
target:
name: my-app-secret
creationPolicy: Owner
data:
- secretKey: db-password
remoteRef:
key: /prod/my-app/db-password
これなら、AWSやAzure、GCPのSecrets Managerと連携して、Gitには「何のシークレットを参照するか」の宣言だけ置いておけるんです。実際の値はクラウドサービスが管理する。これで「Gitに全て」という理想と「シークレットセキュアに」という現実の折り合いがつきました。地味に便利な組み合わせです。
マルチクラスタ運用:同期タイミングのズレで学んだこと
うちのシステムは本番・ステージング・開発で3つのクラスタを持ってるんですけど、ここでまた新しい問題が出ました。
環境ごとのマニフェスト管理の課題
最初は環境ごとにディレクトリを分けてました:
GitOps/
├── prod/
│ └── app.yaml
├── staging/
│ └── app.yaml
└── dev/
└── app.yaml
でもこれ、マニフェストの重複が酷い。replicas: 5 が本番で replicas: 1 が開発、みたいな細かい差分が山ほどあるんです。保守性が最悪で、本当に困りました。
そこで Kustomize を導入しました:
GitOps/
├── base/
│ ├── kustomization.yaml
│ ├── app.yaml
│ └── service.yaml
├── overlays/
│ ├── prod/
│ │ ├── kustomization.yaml
│ │ └── replicas.yaml
│ ├── staging/
│ │ ├── kustomization.yaml
│ │ └── replicas.yaml
│ └── dev/
│ ├── kustomization.yaml
│ └── replicas.yaml
base/kustomization.yaml に共通部分を一度だけ書きます:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- app.yaml
- service.yaml
commonLabels:
app: my-app
そして overlays/prod/kustomization.yaml で本番特有の設定をパッチします:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
patchesJson6902:
- target:
group: apps
version: v1
kind: Deployment
name: my-app
patch: |-
- op: replace
path: /spec/replicas
value: 5
- op: replace
path: /spec/template/spec/resources/limits/memory
value: "2Gi"
こうすることで、共通部分は base/ に一度だけ書いて、環境ごとの差分だけ overlays/ で定義できるようになりました。Gitのdiff も見やすくなるし、保守性が格段に上がりました。本当に効果的でしたね。
マルチクラスタの同期タイミング制御
そしてもう1つ、マルチクラスタ特有の悩みが「デプロイの順序」なんです。本番環境に先に変更を入れるべき?それともステージングで検証してから?
うちのチームでは、ArgoCDのWebhook設定を工夫することで這制御してます。staging 用のApplicationはproduction の3時間後に同期が始まるように設定してるんです。こうすることで、本番で問題が起きたらすぐに Git revert で対応できるし、その間ステージングでは検証が進むという寸法ですね。
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app-prod
spec:
source:
repoURL: https://github.com/myorg/gitops-repo
path: overlays/prod
destination:
server: https://prod-cluster-api.example.com
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
このApplicationが同期した3時間後に、staging用が動くという仕組みです。
実装してわかった、GitOpsの「向き・不向き」
2年運用してきて分かったのは、GitOpsが万能じゃないということなんです。地味に重要な気づきですね。
GitOpsが向いてるケース:
- アプリケーションマニフェストの管理(Deployment、Service等)
- 環境差分が明確に定義できる場合
- チーム全体でGit操作に習熟してる環境
GitOpsが向かないケース:
- クラスタそのものの構築・更新(ノードのOSアップデート、ネットワーク設定等)
- インフラストラクチャ層の管理(VPC、セキュリティグループ等)
- エマージェンシーな設定変更が頻繁に発生する運用
うちの場合、クラスタの初期構築やノード管理は Terraform で、アプリケーション層を GitOps で分けました。これが想像以上に効果的でしたね。
graph TB
A["インフラストラクチャ層<br/>Terraform"] -->|マネージド| B["Kubernetes クラスタ"]
B --> C["GitOps層<br/>ArgoCD"]
C --> D["アプリケーション<br/>Service, Deployment"]
C --> E["設定<br/>ConfigMap"]
C --> F["シークレット<br/>External Secrets"]
style A fill:#e1f5ff
style C fill:#f3e5f5
style D fill:#fff3e0
インフラ層と GitOps 層を分けることで、それぞれの強みを活かせるようになりました。
本当に効果があったメトリクス
導入前後で何が変わったかを正直に話すと:
| 指標 | 導入前 | 導入後(2年目) |
|---|---|---|
| デプロイ所要時間 | 平均45分 | 平均8分 |
| ロールバック成功率 | 65% | 98% |
| 環境差異によるバグ | 月3〜4件 | 月0〜1件 |
| Git操作による人的ミス | 月2件程度 | ほぼ0 |
| 夜間対応の呼び出し | 月平均2回 | 月平均0.3回 |
ただし、導入コストは無視できません。最初の3ヶ月は本当に辛かったし、チーム全体のKubernetes理解度も上がる必要があります。
xychart-beta
title GitOps導入による生産性推移(最初の1年)
x-axis [1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月]
y-axis "デプロイ所要時間(分)" 0 --> 50
line [45, 48, 50, 45, 40, 35, 28, 20, 15, 12, 9, 8]
最初の3ヶ月は逆に遅くなってるんですよ。GitOpsを正しく設計するために試行錯誤してました。でも6ヶ月目以降、加速度的に効率が上がっていきました。グラフで見るとはっきりしますね。
GitOps導入で本当に大事だったこと
技術的な部分より、むしろ文化的な側面の方が重要だと気づきました。正直、ここが一番大変だったんです:
1. レビュープロセスの厳密化
PR レビューを本当に厳しくする必要があります。GitOpsは「Gitが真実」なので、Git上のレビューが本番デプロイの最後の砦なんです。誰か1人が「これ大丈夫そう」と判断してマージしたら、それがそのまま本番に反映されます。だからレビューの品質が全てです。
2. デプロイとロールバックの明確化
「このコミットはいつデプロイされるのか」「ロールバックはこのコマンド」みたいなドキュメントが不可欠なんですよ。チーム全員が同じ理解を持ってないと、本番環境で混乱が生まれます。個人的には、運用マニュアルを丁寧に整備することが後々すごく効いてきたと感じてます。
3. 監視とアラート
ArgoCDの同期失敗を検知する監視が必須です。同期がずっと失敗してても気づかないなんてことが起きます。実際、うちも1回やらかしてて、本番環境が古いマニフェストのままになってました。監視なしで GitOps は絶対に運用できません。
まとめ
正直なところ、2年前の自分たちに戻って「GitOps導入するぞ」と言ったら、今なら「やめとけ」と言うかもしれません。でも、そのあと「いや、やっぱりやった方がいい」と追加しますね。
要点としては:
-
GitOpsは確かに強力だけど、銀の弾ではない — 向き・不向きを冷静に判断することが大事。うちはインフラ層と分離して成功しました。技術を無理やり全体に当てはめるのは本当に危ないんです。
-
シークレット管理を最初に設計すること — External Secrets Operator や Sealed Secrets などの選択肢がありますが、セキュリティとGitOpsの理想のバランスを考えてから実装を始めるべき。中途半端だと後で地獄を見ます。
-
マルチクラスタは Kustomize を前提に — 環境差分を上手に吸収できる構成設計が、2年目の保守性を大きく左右します。最初が肝心ですね。
-
チーム全体の Git・Kubernetes 理解度向上が前提 — 技術を導入するより前に、人の準備が9割。これ、本当に重要です。勉強会を開いたり、ペアプログラミングしたりと、地道な取り組みが結果を左右します。
-
段階的な導入をお勧めします — アプリケーション層から始めて、メリットを感じてからインフラ層への拡大を検討する。全部一度にやろうとすると、絶対に地獄を見ます。
うちのチームは今、ArgoCD + Kustomize + External Secrets の組み合わせでかなり安定した本番運用ができています。夜中の緊急対応も劇的に減りました。導入当初の地獄を思えば、今の状態は本当に価値があると感じてます。
もし GitOps の導入を検討してるなら、まずは限定的なアプリケーションで試してみることを強くお勧めします。全体導入前に「自分たちのチームに本当に向いてるのか」を冷静に判断できますから。