CI/CD چیست؟
از تغییر کد تا تست، ساخت و استقرار خودکار؛ ساده، مرحلهبهمرحله و کاربردی
در این آموزش چه میسازیم؟
یک پروژه .NET را طوری تنظیم میکنیم که بعد از هر Pull Request بهصورت خودکار restore، build و test شود. بعد از merge در شاخه main نیز Docker image ساخته، در GitHub Container Registry منتشر و روی یک سرور لینوکسی deploy میشود.
قبل از کدنویسی، تمام مفاهیم را با مثالهای ساده توضیح میدهیم؛ بنابراین برای شروع به تجربه قبلی DevOps نیاز ندارید.
۱. CI/CD چیست؟
CI/CD مجموعهای از روشها و ابزارهاست که کارهای تکراری مسیر انتشار نرمافزار را خودکار میکند. یعنی بهجای اینکه هر بار دستی پروژه را build، تست و روی سرور کپی کنیم، یک Pipeline این کارها را با ترتیب مشخص انجام میدهد.
CI یعنی Continuous Integration
ترجمه ساده آن «یکپارچهسازی مداوم» است. توسعهدهندگان تغییرات کوچک خود را مرتب وارد مخزن میکنند و سیستم بعد از هر تغییر بهصورت خودکار کد را build و test میکند.
CD دو معنی نزدیک دارد
| اصطلاح | معنی ساده | مرحله آخر |
|---|---|---|
| Continuous Delivery | نسخه همیشه آماده انتشار است. | انتشار Production با تأیید دستی انجام میشود. |
| Continuous Deployment | هر تغییر تأییدشده خودکار منتشر میشود. | انتشار Production بدون دخالت دستی انجام میشود. |
هر دو رویکرد معتبرند. برای سیستمهای حساس مثل بانک، تأیید دستی قبل از Production معمولاً منطقی است. برای محصولی با انتشارهای کوچک و تستهای قوی، Continuous Deployment میتواند مناسب باشد.
۲. چرا به CI/CD نیاز داریم؟
فرض کنید انتشار دستی این مراحل را دارد:
- توسعهدهنده پروژه را روی لپتاپ خود build میکند.
- فایلها را ZIP میکند و برای مسئول سرور میفرستد.
- مسئول سرور نسخه قبلی را پاک و نسخه جدید را کپی میکند.
- کسی یادش میرود تستها یا migration را اجرا کند.
- سرویس خراب میشود و دقیقاً معلوم نیست چه نسخهای نصب شده است.
CI/CD این مشکلات را کمتر میکند:
- تکرارپذیری: انتشار هر بار با دستورهای یکسان انجام میشود.
- سرعت: کارهای تکراری بهجای انسان توسط ماشین اجرا میشوند.
- کیفیت: کد بدون عبور از build و test منتشر نمیشود.
- ردیابی: میدانیم چه commit و چه image روی سرور است.
- کاهش ریسک: تغییرات کوچکتر و دفعات انتشار بیشتر، پیدا کردن مشکل و rollback را آسانتر میکند.
۳. اصطلاحات مهم
| اصطلاح | توضیح ساده |
|---|---|
| Pipeline | کل مسیر خودکار از دریافت کد تا تست و انتشار. |
| Workflow | فایل تعریف Pipeline؛ در GitHub Actions معمولاً یک فایل YAML است. |
| Trigger | رویدادی که Workflow را شروع میکند؛ مثل push یا Pull Request. |
| Job | یک گروه از کارها که روی یک Runner اجرا میشود؛ مثل job تست. |
| Step | یک مرحله داخل Job؛ مثل اجرای dotnet test. |
| Runner | ماشین مجازی یا سروری که دستورهای Pipeline را اجرا میکند. |
| Artifact | خروجی قابل نگهداری یک اجرا؛ مثل فایل publish یا گزارش تست. |
| Registry | محل نگهداری Docker image؛ مثل GHCR یا Docker Hub. |
| Environment | محیط مقصد مثل Development، Staging و Production. |
| Secret | اطلاعات محرمانه مثل SSH key که رمزگذاریشده نگهداری میشود. |
۴. جریان استاندارد Pipeline
۵. آمادهسازی پروژه نمونه
ساختار فرضی مخزن ما:
SampleApi/
├── src/
│ └── SampleApi/
│ ├── SampleApi.csproj
│ ├── Program.cs
│ └── Dockerfile
├── tests/
│ └── SampleApi.Tests/
│ └── SampleApi.Tests.csproj
├── .github/
│ └── workflows/
│ └── ci-cd.yml
└── SampleApi.sln
پیش از ساخت Pipeline، این دستورها باید روی سیستم خودتان موفق باشند:
dotnet restore SampleApi.sln
dotnet build SampleApi.sln --configuration Release --no-restore
dotnet test SampleApi.sln --configuration Release --no-build
یک health check ساده
برای اینکه بعد از deploy بتوانیم سلامت برنامه را بررسی کنیم، در Program.cs یک endpoint اضافه کنید:
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/health");
app.MapControllers();
app.Run();
۶. ساخت CI با GitHub Actions
داخل مخزن فایل .github/workflows/ci-cd.yml را بسازید. YAML به فاصلهها حساس است؛ از Tab استفاده نکنید.
name: CI/CD
on:
pull_request:
branches: [main]
push:
branches: [main]
permissions:
contents: read
jobs:
test:
name: Build and test
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Restore dependencies
run: dotnet restore SampleApi.sln
- name: Build
run: dotnet build SampleApi.sln --configuration Release --no-restore
- name: Test
run: >
dotnet test SampleApi.sln
--configuration Release
--no-build
--logger "trx"
--results-directory TestResults
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: TestResults
if-no-files-found: ignore
خطبهخط چه اتفاقی میافتد؟
onمشخص میکند Workflow روی Pull Request و push بهmainاجرا شود.permissionsدسترسی پیشفرض Workflow را محدود میکند.runs-onیک Runner لینوکسی موقت از GitHub میگیرد.checkoutکد مخزن را داخل Runner قرار میدهد.setup-dotnetنسخه موردنیاز SDK را نصب میکند.--no-restoreو--no-buildاز تکرار کار مرحله قبل جلوگیری میکنند.if: always()گزارش تست را حتی هنگام شکست تستها نگه میدارد.
Branch protection
فقط داشتن CI کافی نیست. در تنظیمات GitHub برای شاخه main یک Branch Protection Rule بسازید و این موارد را فعال کنید:
- تغییرات فقط از طریق Pull Request وارد شوند.
- حداقل یک review لازم باشد.
- Job مربوط به build و test قبل از merge موفق باشد.
- push مستقیم یا force push به
mainغیرفعال باشد.
یک Pipeline در GitHub، GitLab و Microsoft Azure DevOps
مفهوم CI/CD در هر سه پلتفرم یکی است؛ فقط نامها و شکل فایل YAML فرق میکند. تبها را عوض کنید تا نمونه معادل build و test پروژه .NET و امکانات هر سرویس را ببینید.
GitHub Actions
فایل Pipeline در مسیر .github/workflows/ci.yml قرار میگیرد. GitHub-hosted Runner، توکن داخلی GITHUB_TOKEN و اتصال مستقیم به GHCR از مزیتهای اصلی آن هستند.
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- run: dotnet restore SampleApi.sln
- run: dotnet build SampleApi.sln -c Release --no-restore
- run: dotnet test SampleApi.sln -c Release --no-build
- Secrets: در Settings → Secrets and variables → Actions
- Runner: GitHub-hosted یا self-hosted
- Registry: معمولاً GHCR، ولی Docker Hub و ACR نیز قابل استفادهاند.
- کنترل انتشار: Environment، Required Reviewers و Branch Protection
GitLab CI/CD
Pipeline در فایل .gitlab-ci.yml ریشه مخزن تعریف میشود. هر Job داخل یک stage قرار میگیرد و GitLab Runner آن را اجرا میکند.
stages:
- test
- publish
variables:
CONFIGURATION: "Release"
dotnet-test:
stage: test
image: mcr.microsoft.com/dotnet/sdk:8.0
script:
- dotnet restore SampleApi.sln
- dotnet build SampleApi.sln -c $CONFIGURATION --no-restore
- dotnet test SampleApi.sln -c $CONFIGURATION --no-build
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
publish-image:
stage: publish
image: docker:27-cli
services:
- docker:27-dind
script:
- echo "$CI_JOB_TOKEN" | docker login "$CI_REGISTRY"
-u gitlab-ci-token --password-stdin
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- Secrets: در Settings → CI/CD → Variables با گزینههای Masked و Protected
- Runner: Shared Runner یا GitLab Runner شخصی روی Docker، Shell یا Kubernetes
- Registry: GitLab Container Registry با متغیرهای آماده
CI_REGISTRYوCI_REGISTRY_IMAGE - کنترل انتشار: Protected Branch، Protected Environment و Manual Job با
when: manual
Microsoft Azure DevOps Pipelines
فایل معمولاً azure-pipelines.yml است. Azure DevOps علاوه بر Pipeline، امکانات Repos، Boards، Artifacts، Environments و اتصال مستقیم به Azure را یکجا ارائه میدهد.
trigger:
branches:
include:
- main
pr:
branches:
include:
- main
pool:
vmImage: ubuntu-latest
steps:
- task: UseDotNet@2
inputs:
packageType: sdk
version: "8.0.x"
- task: DotNetCoreCLI@2
displayName: Restore
inputs:
command: restore
projects: SampleApi.sln
- task: DotNetCoreCLI@2
displayName: Build
inputs:
command: build
projects: SampleApi.sln
arguments: "-c Release --no-restore"
- task: DotNetCoreCLI@2
displayName: Test
inputs:
command: test
projects: SampleApi.sln
arguments: "-c Release --no-build"
- task: Docker@2
displayName: Build and push
condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')
inputs:
command: buildAndPush
containerRegistry: acr-service-connection
repository: sample-api
Dockerfile: src/SampleApi/Dockerfile
tags: |
$(Build.SourceVersion)
- Secrets: Secret Variable، Variable Group یا اتصال به Azure Key Vault
- Agent: Microsoft-hosted Agent یا self-hosted Agent
- Registry: معمولاً Azure Container Registry با Service Connection
- Deploy: Azure Web App، Container Apps، AKS، Virtual Machine یا سرور SSH
- کنترل انتشار: Environment، Approvals and Checks و Branch Policies در Azure Repos
acr-service-connection نام فرضی Service Connection است. آن را در Project Settings بسازید و فقط دسترسی لازم به Registry بدهید.
| پلتفرم | فایل اصلی | انتخاب مناسب وقتی… |
|---|---|---|
| GitHub Actions | .github/workflows/*.yml | کد روی GitHub است و راهاندازی سریع و Marketplace بزرگ میخواهید. |
| GitLab CI/CD | .gitlab-ci.yml | یک پلتفرم یکپارچه یا نصب داخل سازمان و Runnerهای قابلکنترل میخواهید. |
| Azure DevOps | azure-pipelines.yml | سازمان شما از اکوسیستم Microsoft، Azure Repos، Boards و Service Connection استفاده میکند. |
۷. ساخت و انتشار Docker image
داخل مسیر پروژه API یک Dockerfile چندمرحلهای قرار دهید:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["src/SampleApi/SampleApi.csproj", "src/SampleApi/"]
RUN dotnet restore "src/SampleApi/SampleApi.csproj"
COPY . .
RUN dotnet publish "src/SampleApi/SampleApi.csproj" \
--configuration Release \
--output /app/publish \
--no-restore
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
USER $APP_UID
ENTRYPOINT ["dotnet", "SampleApi.dll"]
و یک .dockerignore ساده:
**/bin/
**/obj/
.git/
.github/
.vs/
.vscode/
TestResults/
README.md
GHCR چیست؟
GHCR مخفف GitHub Container Registry است؛ سرویسی از GitHub برای نگهداری و توزیع container imageها. آدرس آن ghcr.io است.
Dockerfile مثل «دستور پخت» برنامه است، Docker image بسته نهایی و آماده اجراست، و Registry انباری است که این بستهها را نگه میدارد. GHCR همان انبار متصل به حساب و مخزن GitHub شماست.
ghcr.io push میشود.نام یک image در GHCR
ghcr.io/OWNER/IMAGE_NAME:TAG
# مثال
ghcr.io/mortenaho/sample-api:latest
ghcr.io/mortenaho/sample-api:sha-a1b2c3d
ghcr.io: آدرس Registrymortenaho: نام کاربر یا سازمان GitHubsample-api: نام image یا packagelatestیاsha-a1b2c3d: نسخه یا tag image
ورود، Push و Pull بهصورت دستی
برای کار محلی، یک Personal Access Token با حداقل دسترسی لازم بسازید. رمز حساب GitHub برای docker login استفاده نمیشود.
# ورود؛ توکن را مستقیم داخل تاریخچه shell ننویسید
echo "$CR_PAT" | docker login ghcr.io \
-u YOUR_GITHUB_USERNAME --password-stdin
# ساخت image
docker build -t ghcr.io/mortenaho/sample-api:v1.0.0 .
# ارسال به GHCR
docker push ghcr.io/mortenaho/sample-api:v1.0.0
# دریافت روی سرور
docker pull ghcr.io/mortenaho/sample-api:v1.0.0
# اجرای container
docker run -d --name sample-api -p 8080:8080 \
ghcr.io/mortenaho/sample-api:v1.0.0
| نوع عملیات | دسترسی لازم |
|---|---|
| دریافت package خصوصی | read:packages |
| ارسال یا بهروزرسانی package | write:packages |
| حذف نسخههای package | delete:packages (فقط در صورت نیاز واقعی) |
Public یا Private؟
- Public: همه میتوانند image را pull کنند؛ برای پروژه متنباز مناسب است.
- Private: فقط کاربران و سرورهای مجاز میتوانند image را دریافت کنند و قبل از pull باید login انجام شود.
package جدید ممکن است بهصورت Private ساخته شود. از صفحه Package در GitHub میتوانید visibility، دسترسی repositoryها و نسخههای موجود را مدیریت کنید.
Tag و Digest چه فرقی دارند؟
Tag یک نام قابلفهم مثل v1.0.0 یا latest است و میتواند بعداً به image دیگری اشاره کند. Digest شناسه محتوایی تغییرناپذیر image است:
ghcr.io/mortenaho/sample-api@sha256:abc123...
برای Production از tag مبتنی بر commit یا digest استفاده کنید. latest برای مشاهده سریع خوب است، اما بهتنهایی مشخص نمیکند دقیقاً کدام نسخه deploy شده است.
GHCR چه تفاوتی با Docker Hub دارد؟
| GHCR | Docker Hub |
|---|---|
| مستقیم با repository، GitHub Actions و دسترسیهای GitHub یکپارچه است. | Registry عمومی و مستقل با اکوسیستم گسترده Docker است. |
در Workflow میتواند از GITHUB_TOKEN داخلی استفاده کند. | معمولاً username و access token جدا لازم دارد. |
| برای پروژهای که کد آن روی GitHub است انتخاب طبیعی و سادهای است. | برای imageهای عمومی و استفاده مستقل از میزبان کد بسیار رایج است. |
حالا Job زیر را بعد از Job تست به همان فایل Workflow اضافه کنید. این Job فقط هنگام push به main اجرا میشود؛ یعنی Pull Requestها اجازه انتشار image ندارند.
publish-image:
name: Build and publish image
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create image tags
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest
type=sha
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: src/SampleApi/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
GITHUB_TOKEN کوتاهعمر میسازد؛ کافی است Job مجوز packages: write داشته باشد. PAT بیشتر برای سیستم شخصی، سرور یا دسترسی بین repositoryها لازم میشود.
برای هر commit دو tag ساخته میشود:
latest: آخرین نسخه؛ برای انسان ساده است ولی دقیق و تغییرناپذیر نیست.sha-abc1234: به یک commit مشخص اشاره میکند و برای deploy و rollback بهتر است.
latest تکیه نکنید. با tag یا digest تغییرناپذیر دقیقاً میدانید چه کدی اجرا شده است.
۸. آمادهسازی سرور لینوکسی
روی سرور Docker و Docker Compose Plugin را نصب کنید. سپس یک کاربر مخصوص deploy بسازید؛ از کاربر root برای Pipeline استفاده نکنید.
sudo useradd --create-home --shell /bin/bash deploy
sudo usermod -aG docker deploy
sudo mkdir -p /opt/sample-api
sudo chown deploy:deploy /opt/sample-api
کلید SSH مخصوص CI بسازید:
ssh-keygen -t ed25519 -C "github-actions-deploy" \
-f github-actions-deploy
- محتوای کلید عمومی
github-actions-deploy.pubرا در فایل~deploy/.ssh/authorized_keysسرور قرار دهید. - محتوای کلید خصوصی
github-actions-deployرا بعداً در GitHub Secret بگذارید. - این کلید فقط باید دسترسی لازم برای deploy داشته باشد.
فایل docker-compose.yml روی سرور
services:
api:
image: ghcr.io/mortenaho/sampleapi:${IMAGE_TAG:-latest}
container_name: sample-api
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
ASPNETCORE_ENVIRONMENT: Production
ConnectionStrings__Default: ${DB_CONNECTION}
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
نام واقعی image را مطابق نام مخزن خود تغییر دهید. فایل .env سرور نیز باید شامل اطلاعات محیط باشد و هرگز وارد Git نشود:
DB_CONNECTION=Server=db;Database=Sample;User Id=app;Password=CHANGE_ME
curl نداشته باشد. یا curl را در image نصب کنید، یا health check را بیرون container با ابزار موجود انجام دهید. وجود endpoint بهتنهایی کافی نیست؛ دستور health check باید واقعاً در image اجرا شود.
دسترسی سرور به GHCR خصوصی
اگر package خصوصی است، یک GitHub Personal Access Token با حداقل دسترسی read:packages بسازید و یک بار روی سرور login کنید:
echo "YOUR_READ_ONLY_TOKEN" | docker login ghcr.io \
-u YOUR_GITHUB_USERNAME --password-stdin
اگر package عمومی باشد، معمولاً میتوان بدون login آن را pull کرد.
۹. استقرار خودکار روی سرور
در GitHub به مسیر Settings → Secrets and variables → Actions بروید و این Secretها را بسازید:
| نام | مقدار |
|---|---|
VPS_HOST | IP یا دامنه سرور |
VPS_USER | مثلاً deploy |
VPS_SSH_KEY | محتوای کامل کلید خصوصی |
VPS_KNOWN_HOSTS | اثر انگشت SSH سرور |
مقدار known hosts را از یک سیستم مورداعتماد بگیرید و fingerprint را بررسی کنید:
ssh-keyscan -H your-server.example.com
Job استقرار را به انتهای Workflow اضافه کنید:
deploy:
name: Deploy to production
runs-on: ubuntu-latest
needs: publish-image
environment: production
steps:
- name: Configure SSH
env:
SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
KNOWN_HOSTS: ${{ secrets.VPS_KNOWN_HOSTS }}
run: |
install -m 700 -d ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts
- name: Pull and start the new version
env:
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
run: |
IMAGE_TAG="sha-${GITHUB_SHA::7}"
ssh -i ~/.ssh/deploy_key "$VPS_USER@$VPS_HOST" \
"cd /opt/sample-api && \
IMAGE_TAG=$IMAGE_TAG docker compose pull && \
IMAGE_TAG=$IMAGE_TAG docker compose up -d --remove-orphans"
- name: Verify deployment
env:
VPS_HOST: ${{ secrets.VPS_HOST }}
run: |
for attempt in {1..10}; do
if curl --fail --silent "https://$VPS_HOST/health"; then
exit 0
fi
sleep 6
done
echo "Health check failed"
exit 1
Environment و تأیید دستی
در GitHub یک Environment به نام production بسازید. میتوانید Required Reviewer تعیین کنید. در این حالت Pipeline بعد از ساخت image متوقف میشود و فقط پس از تأیید فرد مسئول، deploy را انجام میدهد؛ این همان Continuous Delivery است.
staging داشته باشید، نسخه را آنجا deploy و smoke test کنید، سپس همان image بدون build مجدد وارد Production شود.
۱۰. Secrets و امنیت Pipeline
- رمز، token، connection string و private key را داخل YAML یا مخزن commit نکنید.
- اصل Least Privilege را رعایت کنید؛ هر Job فقط دسترسی لازم را داشته باشد.
- Secret را با
echoچاپ نکنید. مخفیسازی خودکار GitHub مفید است، اما کامل و همیشه قابلاعتماد نیست. - Workflow مربوط به کد Pull Requestهای ناشناس نباید به Secretهای Production دسترسی داشته باشد.
- اکشنهای شخص ثالث را بررسی و ترجیحاً به commit SHA معتبر pin کنید تا تغییر ناگهانی آنها به Pipeline آسیب نزند.
- SSH key مخصوص Pipeline باشد تا بتوانید بدون تأثیر بر کاربران دیگر آن را تعویض یا لغو کنید.
- Secretها را دورهای rotate کنید و دسترسی اعضای تیم به Environment را بازبینی کنید.
- Dependency و container image را از نظر آسیبپذیری اسکن کنید.
۱۱. Rollback و مهاجرت دیتابیس
Rollback برنامه
چون برای هر commit یک image جدا با tag مبتنی بر SHA داریم، بازگشت به نسخه قبلی سادهتر است:
cd /opt/sample-api
IMAGE_TAG=sha-PREVIOUS docker compose pull
IMAGE_TAG=sha-PREVIOUS docker compose up -d
شماره image موفق قبلی را در لاگ Pipeline یا GHCR پیدا کنید. بهتر است Pipeline نسخه deployشده، زمان و commit را در یک deployment log ثبت کند.
Migration دیتابیس
دیتابیس سختترین بخش rollback است. اگر نسخه جدید ستونی را حذف کند، برگشت کد قبلی ممکن است دیگر امکانپذیر نباشد. از روش Expand and Contract استفاده کنید:
- Expand: ستون یا جدول جدید را اضافه کنید، بدون حذف ساختار قدیمی.
- نسخهای منتشر کنید که با ساختار قدیم و جدید سازگار باشد.
- دادهها را منتقل و صحت آنها را بررسی کنید.
- Contract: در انتشار بعدی و پس از اطمینان، ساختار قدیمی را حذف کنید.
۱۲. بهترین روشها برای Pipeline واقعی
- Build once, deploy many: همان image تستشده در Staging را به Production ببرید؛ دوباره build نکنید.
- Fast feedback: تستهای سریع را اول اجرا کنید و مراحل سنگین را بعد از آن قرار دهید.
- Immutable artifacts: image منتشرشده را تغییر ندهید؛ نسخه جدید، tag یا digest جدید داشته باشد.
- Quality gates: شکست تست، lint، اسکن امنیتی یا حداقل coverage باید جلوی merge را بگیرد.
- Cache: کش NuGet و لایههای Docker سرعت را زیاد میکند، اما نباید جایگزین restore واقعی شود یا صحت build را زیر سؤال ببرد.
- Concurrency: اجازه ندهید دو deploy همزمان Production را تغییر دهند.
- Timeout: برای Jobها timeout بگذارید تا Runner گیرکرده بینهایت منتظر نماند.
- Observability: بعد از deploy فقط به سبز شدن Pipeline اکتفا نکنید؛ log، metric، error rate و latency را مانیتور کنید.
- Notification: شکست Production باید از طریق Slack، Email یا ابزار مانیتورینگ اطلاع داده شود.
- Runbook: مراحل rollback و مدیریت incident را مستند و دورهای تمرین کنید.
جلوگیری از deploy همزمان
این تنظیم را میتوان در سطح Workflow اضافه کرد:
concurrency:
group: production
cancel-in-progress: false
با این کار deploy بعدی منتظر قبلی میماند. برای CI مربوط به Pull Request میتوان گروه جدا تعریف و اجرای قدیمی را لغو کرد.
روشهای انتشار بدون قطعی
| روش | ایده | کاربرد |
|---|---|---|
| Rolling | instanceها بهتدریج تعویض میشوند. | روش رایج در Kubernetes و چند replica. |
| Blue/Green | نسخه قدیم و جدید همزمان هستند؛ ترافیک یکباره جابهجا میشود. | rollback سریع، با هزینه منابع بیشتر. |
| Canary | ابتدا درصد کمی از کاربران نسخه جدید را میبینند. | کاهش ریسک تغییرات بزرگ. |
مثال ساده Docker Compose این آموزش ممکن است هنگام تعویض container چند ثانیه قطعی داشته باشد. برای zero-downtime واقعی به چند instance، load balancer و health-aware rollout نیاز دارید.
۱۳. خطاهای رایج
| مشکل | علت احتمالی و راهحل |
|---|---|
| روی سیستم من کار میکند، در CI نه | نسخه SDK، سیستمعامل یا فایلهای commitنشده متفاوت است. نسخهها را ثابت و محیط را تکرارپذیر کنید. |
| Workflow اجرا نمیشود | مسیر فایل باید دقیقاً داخل .github/workflows باشد و trigger با نام branch هماهنگ باشد. |
| push به GHCR با 403 شکست میخورد | packages: write یا تنظیم Workflow permissions وجود ندارد. |
| سرور image را pull نمیکند | نام image یا tag اشتباه است، package خصوصی است یا login سرور منقضی شده است. |
| SSH با Permission denied شکست میخورد | کلید عمومی در authorized_keys نیست، user اشتباه است یا permission فایلها مناسب نیست. |
| Deploy سبز است ولی برنامه خراب است | فقط دستور start اجرا شده و health/smoke test وجود ندارد. |
| Secret خالی است | نام Secret اشتباه است یا Workflow مربوط به fork اجازه دریافت Secret ندارد. |
| نسخه اشتباه deploy میشود | استفاده از latest و cache ممکن است ابهام ایجاد کند؛ tag مبتنی بر SHA یا digest استفاده کنید. |
۱۴. جمعبندی
- CI کد را مرتب build و test میکند تا خطا زود پیدا شود.
- Continuous Delivery نسخه را آماده نگه میدارد ولی Production تأیید دستی میخواهد.
- Continuous Deployment نسخه موفق را خودکار وارد Production میکند.
- Pipeline نمونه ما پروژه .NET را تست، Docker image را در GHCR منتشر و با SSH روی سرور deploy میکند.
- Secrets، دسترسی حداقلی، image تغییرناپذیر، health check و rollback بخش اصلی راهحل هستند؛ نه امکانات جانبی.
- CI/CD خوب فقط انتشار را سریع نمیکند؛ انتشار را قابلتکرار، قابلردیابی و کمریسکتر میکند.
با یک Pipeline کوچک شروع کنید: build و test. سپس بهتدریج انتشار image، Staging، تأیید Production و مانیتورینگ را اضافه کنید.