در مدل کلاسیک CI/CD، آخرین Job در Pipeline یک kubectl apply یا helm upgrade اجرا میکند. این روش کار میکند، اما سه مشکل جدی دارد:
- Runner باید دسترسی Admin به کلاستر داشته باشد؛ یعنی اگر Runner نفوذ شود، کلاستر هم از دست رفته است.
- اگر کسی دستی چیزی را در کلاستر تغییر دهد (
kubectl edit)، هیچکس متوجه نمیشود. به این وضعیت Configuration Drift میگویند. - سؤال «الان دقیقاً چه چیزی روی Production است؟» جواب دقیقی ندارد.
GitOps این مشکلات را با یک ایدهی ساده حل میکند: وضعیت مطلوب کلاستر در Git نوشته میشود و یک Agent داخل کلاستر مدام آن را با وضعیت واقعی مقایسه و هماهنگ میکند. محبوبترین این Agentها ArgoCD است.
Push در برابر Pull
| ویژگی | Push (CI مستقیم Deploy میکند) | Pull (GitOps با ArgoCD) |
|---|---|---|
| دسترسی به کلاستر | Runner دسترسی کامل دارد | فقط ArgoCD داخل کلاستر |
| تشخیص Drift | ندارد | خودکار، همراه با Self-Heal |
| Rollback | اجرای دوبارهی Pipeline قدیمی | git revert |
| Audit | پراکنده در لاگ Jobها | تاریخچهی Git |
| چند کلاستر | پیچیده | یک ArgoCD، چند کلاستر |
نصب ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d; echo
ساختار مخزن: کد و Config را جدا کنید
بهترین الگو این است که دو مخزن داشته باشید:
- مخزن کد (
api): سورس برنامه، Dockerfile و.gitlab-ci.yml - مخزن Config (
deploy-config): Helm Chart یا Manifestها و Valueهای هر محیط
deploy-config/
├── charts/
│ └── api/ # Helm chart
└── envs/
├── dev/values.yaml
├── staging/values.yaml
└── production/values.yaml
با این جداسازی، هر تغییر در Production یک Commit در deploy-config است که میشود برایش Merge Request و Approval گذاشت.
تعریف Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: api-dev
namespace: argocd
spec:
project: default
source:
repoURL: https://gitlab.example.ir/devops/deploy-config.git
targetRevision: main
path: charts/api
helm:
valueFiles: [../../envs/dev/values.yaml]
destination:
server: https://kubernetes.default.svc
namespace: dev
syncPolicy:
automated:
prune: true # delete resources removed from Git
selfHeal: true # revert manual changes in the cluster
syncOptions: [CreateNamespace=true]
selfHeal: true یعنی اگر کسی دستی Replica را عوض کند، ArgoCD چند ثانیه بعد آن را به مقدار Git برمیگرداند. اول شاید عجیب باشد، ولی همین ویژگی است که Production را قابل پیشبینی میکند.
سه محیط با یک فایل: ApplicationSet
بهجای سه Application تقریباً یکسان، از ApplicationSet استفاده کنید:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: api
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- list:
elements:
- env: dev
autoSync: "true"
- env: staging
autoSync: "true"
- env: production
autoSync: "false" # production syncs only after manual approval
template:
metadata:
name: "api-{{.env}}"
spec:
project: default
source:
repoURL: https://gitlab.example.ir/devops/deploy-config.git
targetRevision: main
path: charts/api
helm:
valueFiles: ["../../envs/{{.env}}/values.yaml"]
destination:
server: https://kubernetes.default.svc
namespace: "{{.env}}"
templatePatch: |
{{- if eq .autoSync "true" }}
spec:
syncPolicy:
automated: { prune: true, selfHeal: true }
{{- end }}
اتصال CI به GitOps
حالا Pipeline دیگر Deploy نمیکند. فقط Image را میسازد و Tag جدید را در مخزن Config مینویسد:
update-dev:
stage: deploy
image: alpine:3.20
needs: [docker-image]
before_script:
- apk add --no-cache git yq
script:
- git clone "https://ci-bot:${CONFIG_REPO_TOKEN}@gitlab.example.ir/devops/deploy-config.git"
- cd deploy-config
- yq -i ".image.tag = \"$CI_COMMIT_SHORT_SHA\"" envs/dev/values.yaml
- git -c user.name=ci-bot -c user.email=ci@example.ir commit -am "api: dev -> $CI_COMMIT_SHORT_SHA"
- git push origin main
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
ArgoCD تغییر را میبیند و Develop را بهروز میکند. برای Staging و Production همین کار با یک Merge Request انجام میشود که تیم QA یا مدیر فنی Approve میکند.
Rollback: فقط یک Commit
git revert HEAD # undo the last deployment
git push
ArgoCD نسخهی قبلی را برمیگرداند و در تاریخچهی Git دقیقاً ثبت میشود چه کسی، چه زمانی و چرا Rollback کرده است. در پروژههای بانکی همین تاریخچه برای Audit کافی است.
چند نکته از تجربهی Production
- Secretها را در Git ساده نگذارید. از Sealed Secrets، SOPS یا External Secrets همراه HashiCorp Vault استفاده کنید.
- Sync Wave برای ترتیب استقرار: مثلاً Migration دیتابیس با
argocd.argoproj.io/sync-wave: "-1"قبل از خود سرویس اجرا شود. - ArgoCD Notifications را به Mattermost یا تلگرام وصل کنید تا تیم از Sync ناموفق یا وضعیت Degraded باخبر شود.
- AppProject برای محدود کردن دسترسی تیمها به Namespaceها و مخزنهای مشخص.
جمعبندی
GitOps فقط یک ابزار جدید نیست؛ تغییری در طرز فکر است: هر چیزی که در کلاستر هست باید در Git باشد. نتیجهاش امنیت بیشتر (CI دیگر کلید کلاستر را ندارد)، Rollback چندثانیهای و پاسخ دقیق به سؤال «روی Production چه داریم؟» است.