KR_CI_CD - somaz94/python-study GitHub Wiki

Python CI/CD ๊ฐœ๋… ์ •๋ฆฌ


1๏ธโƒฃ GitHub Actions

GitHub Actions๋ฅผ ์‚ฌ์šฉํ•œ CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์„ฑ์ด๋‹ค.

# .github/workflows/python-app.yml
name: Python application

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.9'
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install flake8 pytest
        if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
    
    - name: Lint with flake8
      run: |
        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
    
    - name: Test with pytest
      run: |
        pytest

โœ… ํŠน์ง•:

  • ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ
  • ์ฝ”๋“œ ํ’ˆ์งˆ ๊ฒ€์‚ฌ
  • ์˜์กด์„ฑ ๊ด€๋ฆฌ
  • ๊ฐ„ํŽธํ•œ ์›Œํฌํ”Œ๋กœ์šฐ ์„ค์ •
  • ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ ์ง€์›


2๏ธโƒฃ Jenkins ํŒŒ์ดํ”„๋ผ์ธ

Jenkins๋ฅผ ์‚ฌ์šฉํ•œ ํŒŒ์ดํ”„๋ผ์ธ ์ž๋™ํ™” ๊ตฌ์„ฑ์ด๋‹ค.

// Jenkinsfile
pipeline {
    agent any
    
    environment {
        PYTHON_VERSION = '3.9'
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Setup Python') {
            steps {
                sh """
                    pyenv install ${PYTHON_VERSION}
                    pyenv global ${PYTHON_VERSION}
                    python -m pip install --upgrade pip
                """
            }
        }
        
        stage('Install Dependencies') {
            steps {
                sh 'pip install -r requirements.txt'
            }
        }
        
        stage('Run Tests') {
            steps {
                sh 'pytest --junitxml=test-results.xml'
            }
            post {
                always {
                    junit 'test-results.xml'
                }
            }
        }
        
        stage('Build and Deploy') {
            when {
                branch 'main'
            }
            steps {
                sh '''
                    docker build -t myapp:${BUILD_NUMBER} .
                    docker push myapp:${BUILD_NUMBER}
                '''
            }
        }
    }
}

โœ… ํŠน์ง•:

  • ๋‹จ๊ณ„๋ณ„ ํŒŒ์ดํ”„๋ผ์ธ
  • ํ™˜๊ฒฝ ์„ค์ • ๊ด€๋ฆฌ
  • ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ๋ณด๊ณ 
  • ์กฐ๊ฑด๋ถ€ ๋ฐฐํฌ
  • ํ”Œ๋Ÿฌ๊ทธ์ธ ํ™•์žฅ์„ฑ


3๏ธโƒฃ GitLab CI/CD

GitLab์—์„œ ์ œ๊ณตํ•˜๋Š” CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์„ฑ์ด๋‹ค.

# .gitlab-ci.yml
image: python:3.9

stages:
  - test
  - build
  - deploy

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"

cache:
  paths:
    - .pip-cache/

before_script:
  - python -V
  - pip install -r requirements.txt

test:
  stage: test
  script:
    - pytest --cov=.
  coverage: '/TOTAL.+ ([0-9]{1,3}%)/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
        
build:
  stage: build
  script:
    - python setup.py bdist_wheel
  artifacts:
    paths:
      - dist/
    expire_in: 1 week
    
deploy_staging:
  stage: deploy
  script:
    - pip install twine
    - TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/*
  only:
    - develop
    
deploy_production:
  stage: deploy
  script:
    - pip install twine
    - twine upload dist/*
  only:
    - main
  when: manual

โœ… ํŠน์ง•:

  • ๋ฉ€ํ‹ฐ ์Šคํ…Œ์ด์ง€ ํŒŒ์ดํ”„๋ผ์ธ
  • ์บ์‹œ ๊ด€๋ฆฌ
  • ์ปค๋ฒ„๋ฆฌ์ง€ ๋ฆฌํฌํŠธ
  • ๋นŒ๋“œ ์•„ํ‹ฐํŒฉํŠธ ๋ณด์กด
  • ํ™˜๊ฒฝ๋ณ„ ๋ฐฐํฌ ์ „๋žต


4๏ธโƒฃ Docker ๊ธฐ๋ฐ˜ ๋ฐฐํฌ

Docker๋ฅผ ํ™œ์šฉํ•œ ์ผ๊ด€๋œ ํ™˜๊ฒฝ ๊ตฌ์„ฑ๊ณผ ๋ฐฐํฌ ์ž๋™ํ™”์ด๋‹ค.

# docker-compose.ci.yml
version: '3.8'

services:
  test:
    build:
      context: .
      dockerfile: Dockerfile.test
    volumes:
      - .:/app
    command: pytest

  build:
    build:
      context: .
      dockerfile: Dockerfile
    image: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}
    
  deploy:
    image: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}
    restart: unless-stopped
    environment:
      - ENVIRONMENT=production
      - DATABASE_URL=${DATABASE_URL}
      - SECRET_KEY=${SECRET_KEY}
    ports:
      - "8000:8000"
    depends_on:
      - db
      
  db:
    image: postgres:13-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_DB=${DB_NAME}
    restart: unless-stopped
    
volumes:
  postgres_data:

Dockerfile ์˜ˆ์‹œ:

# Dockerfile
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN python -m pytest

EXPOSE 8000

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

โœ… ํŠน์ง•:

  • ์ปจํ…Œ์ด๋„ˆํ™”๋œ ํ…Œ์ŠคํŠธ
  • ์ผ๊ด€๋œ ํ™˜๊ฒฝ
  • ๋ฒ„์ „ ๊ด€๋ฆฌ
  • ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ตฌ์กฐ ์ง€์›
  • ์Šค์ผ€์ผ๋ง ์šฉ์ด


5๏ธโƒฃ ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ์™€ ๋ฐฐํฌ

๋‹ค์–‘ํ•œ ํ…Œ์ŠคํŠธ ์ „๋žต๊ณผ ๋ฐฐํฌ ์ž๋™ํ™” ๊ตฌ์„ฑ์ด๋‹ค.

# .github/workflows/automated-testing.yml
name: Automated Testing

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.8, 3.9]

    steps:
    - uses: actions/checkout@v2
    
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v2
      with:
        python-version: ${{ matrix.python-version }}
    
    - name: Run tests
      run: |
        pytest --cov=./ --cov-report=xml
    
    - name: Upload coverage
      uses: codecov/codecov-action@v2
      with:
        file: ./coverage.xml
        fail_ci_if_error: true
        
  integration-test:
    runs-on: ubuntu-latest
    needs: test
    services:
      postgres:
        image: postgres:13
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_USER: postgres
          POSTGRES_DB: test_db
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
          
    steps:
    - uses: actions/checkout@v2
    
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.9'
        
    - name: Install dependencies
      run: |
        pip install -r requirements.txt
        pip install -r requirements-dev.txt
        
    - name: Run integration tests
      run: |
        pytest tests/integration --cov=./ --cov-report=xml
      env:
        DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
        
  deploy:
    runs-on: ubuntu-latest
    needs: [test, integration-test]
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Deploy to production
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.HOST }}
        username: ${{ secrets.USERNAME }}
        key: ${{ secrets.SSH_KEY }}
        script: |
          cd /opt/app
          git pull
          docker-compose down
          docker-compose up -d --build

โœ… ํŠน์ง•:

  • ๋งคํŠธ๋ฆญ์Šค ํ…Œ์ŠคํŠธ
  • ์ปค๋ฒ„๋ฆฌ์ง€ ๋ถ„์„
  • ์ž๋™ ๋ฐฐํฌ
  • ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ
  • ์‹คํŒจ ์‹œ ๋กค๋ฐฑ ์ „๋žต


6๏ธโƒฃ ๊ณ ๊ธ‰ CI/CD ์ „๋žต

๋ณต์žกํ•œ ํ”„๋กœ์ ํŠธ๋ฅผ ์œ„ํ•œ ๊ณ ๊ธ‰ CI/CD ์ „๋žต์ด๋‹ค.

๋ธ”๋ฃจ/๊ทธ๋ฆฐ ๋ฐฐํฌ:

# .github/workflows/blue-green-deploy.yml
name: Blue-Green Deployment

on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    
    - name: Build Docker image
      run: |
        docker build -t myapp:${{ github.sha }} .
        docker tag myapp:${{ github.sha }} myregistry.com/myapp:${{ github.sha }}
        docker push myregistry.com/myapp:${{ github.sha }}
        
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
    - name: Deploy new environment
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.PROD_HOST }}
        username: ${{ secrets.USERNAME }}
        key: ${{ secrets.SSH_KEY }}
        script: |
          # ์ƒˆ ํ™˜๊ฒฝ ๋ฐฐํฌ
          NEW_ENV="green"
          CURRENT_ENV=$(cat /opt/env_status)
          
          if [ "$CURRENT_ENV" = "green" ]; then
            NEW_ENV="blue"
          fi
          
          cd /opt
          docker-compose -f docker-compose.$NEW_ENV.yml down
          
          # ์ƒˆ ์ด๋ฏธ์ง€๋กœ ์—…๋ฐ์ดํŠธ
          sed -i "s|image:.*|image: myregistry.com/myapp:${{ github.sha }}|g" docker-compose.$NEW_ENV.yml
          
          # ์ƒˆ ํ™˜๊ฒฝ ์‹œ์ž‘
          docker-compose -f docker-compose.$NEW_ENV.yml up -d
          
          # ํ—ฌ์Šค์ฒดํฌ
          sleep 30
          if curl -s http://localhost:8080/health | grep -q "ok"; then
            # ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ ์—…๋ฐ์ดํŠธ
            sed -i "s/proxy_pass http:\/\/[a-z]*_backend/proxy_pass http:\/\/${NEW_ENV}_backend/g" /etc/nginx/conf.d/app.conf
            nginx -s reload
            
            # ํ˜„์žฌ ํ™˜๊ฒฝ ์—…๋ฐ์ดํŠธ
            echo $NEW_ENV > /opt/env_status
            
            # ์ด์ „ ํ™˜๊ฒฝ ์ข…๋ฃŒ (์ง€์—ฐ ์‹œ๊ฐ„ ํ›„)
            sleep 60
            docker-compose -f docker-compose.$CURRENT_ENV.yml down
          else
            # ๋ฐฐํฌ ์‹คํŒจ ์‹œ ๋กค๋ฐฑ
            docker-compose -f docker-compose.$NEW_ENV.yml down
            echo "Deployment failed health check" >&2
            exit 1
          fi

์นด๋‚˜๋ฆฌ ๋ฐฐํฌ:

# canary_deploy.py
import os
import time
import requests
import subprocess

def deploy_canary():
    """์นด๋‚˜๋ฆฌ ๋ฐฐํฌ ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ"""
    # ์นด๋‚˜๋ฆฌ ๋ฒ„์ „ ๋ฐฐํฌ (ํŠธ๋ž˜ํ”ฝ์˜ 10%)
    subprocess.run([
        "kubectl", "set", "image", "deployment/app", 
        f"app-container=myregistry.com/myapp:{os.environ['NEW_VERSION']}", 
        "--record"
    ])
    
    # ์นด๋‚˜๋ฆฌ ๋น„์ค‘ ์„ค์ •
    subprocess.run([
        "kubectl", "scale", "deployment/app", "--replicas=10"
    ])
    subprocess.run([
        "kubectl", "scale", "deployment/app-canary", "--replicas=1"
    ])
    
    # ๋ชจ๋‹ˆํ„ฐ๋ง ๊ธฐ๊ฐ„
    print("Monitoring canary deployment for 15 minutes...")
    for i in range(15):
        # ์˜ค๋ฅ˜์œจ ํ™•์ธ
        error_rate = check_error_rate()
        if error_rate > 0.01:  # ์˜ค๋ฅ˜์œจ 1% ์ดˆ๊ณผ ์‹œ ๋กค๋ฐฑ
            print(f"Error rate too high: {error_rate}, rolling back")
            rollback_canary()
            return False
        
        # ์„ฑ๋Šฅ ์ง€ํ‘œ ํ™•์ธ
        latency = check_latency()
        if latency > 500:  # 500ms ์ด์ƒ ์ง€์—ฐ์‹œ ๋กค๋ฐฑ
            print(f"Latency too high: {latency}ms, rolling back")
            rollback_canary()
            return False
        
        print(f"Minute {i+1}/15: Metrics within acceptable range")
        time.sleep(60)
    
    # ๋ชจ๋“  ํŠธ๋ž˜ํ”ฝ์„ ์ƒˆ ๋ฒ„์ „์œผ๋กœ ์ „ํ™˜
    print("Canary deployment successful, switching all traffic to new version")
    subprocess.run([
        "kubectl", "set", "image", "deployment/app", 
        f"app-container=myregistry.com/myapp:{os.environ['NEW_VERSION']}", 
        "--record"
    ])
    
    # ์นด๋‚˜๋ฆฌ ์ œ๊ฑฐ
    subprocess.run([
        "kubectl", "scale", "deployment/app-canary", "--replicas=0"
    ])
    
    return True

def check_error_rate():
    """ํ”„๋กœ๋ฉ”ํ…Œ์šฐ์Šค์—์„œ ์˜ค๋ฅ˜์œจ ํ™•์ธ"""
    response = requests.get(
        "http://prometheus:9090/api/v1/query",
        params={
            "query": 'sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))'
        }
    )
    result = response.json()
    return float(result['data']['result'][0]['value'][1])

def check_latency():
    """ํ”„๋กœ๋ฉ”ํ…Œ์šฐ์Šค์—์„œ ์‘๋‹ต ์‹œ๊ฐ„ ํ™•์ธ"""
    response = requests.get(
        "http://prometheus:9090/api/v1/query",
        params={
            "query": 'histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))'
        }
    )
    result = response.json()
    return float(result['data']['result'][0]['value'][1]) * 1000  # ms๋กœ ๋ณ€ํ™˜

def rollback_canary():
    """์นด๋‚˜๋ฆฌ ๋กค๋ฐฑ"""
    subprocess.run([
        "kubectl", "scale", "deployment/app-canary", "--replicas=0"
    ])
    print("Canary deployment rolled back")

if __name__ == "__main__":
    deploy_canary()

โœ… ํŠน์ง•:

  • ๋ธ”๋ฃจ/๊ทธ๋ฆฐ ๋ฐฐํฌ
  • ์นด๋‚˜๋ฆฌ ๋ฐฐํฌ
  • ์ž๋™ํ™”๋œ ๋กค๋ฐฑ
  • ์ ์ง„์ ์ธ ํŠธ๋ž˜ํ”ฝ ์ด์ „
  • ์ง€ํ‘œ ๊ธฐ๋ฐ˜ ๋ฐฐํฌ ๊ฒฐ์ •


7๏ธโƒฃ CI/CD ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋ณด์•ˆ

CI/CD ํŒŒ์ดํ”„๋ผ์ธ์˜ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋ณด์•ˆ ๊ด€๋ฆฌ ๋ฐฉ๋ฒ•์ด๋‹ค.

๋ณด์•ˆ ์Šค์บ” ํ†ตํ•ฉ:

# .github/workflows/security-scan.yml
name: Security Scanning

on:
  push:
    branches: [ main, develop ]
  schedule:
    - cron: '0 0 * * *'  # ๋งค์ผ ์ž์ •์— ์‹คํ–‰

jobs:
  dependency-scan:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.9'
    
    - name: Install dependencies
      run: |
        pip install safety bandit
    
    - name: Check for vulnerable dependencies
      run: |
        safety check -r requirements.txt
        
    - name: Static code security analysis
      run: |
        bandit -r . -x tests/

  container-scan:
    runs-on: ubuntu-latest
    needs: dependency-scan
    steps:
    - uses: actions/checkout@v2
    
    - name: Build image
      run: |
        docker build -t myapp:${{ github.sha }} .
    
    - name: Scan container for vulnerabilities
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: 'myapp:${{ github.sha }}'
        format: 'table'
        exit-code: '1'
        severity: 'CRITICAL,HIGH'

ํŒŒ์ดํ”„๋ผ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง:

# pipeline_monitor.py
import requests
import datetime
import json
import os
from prometheus_client import Counter, Gauge, start_http_server

# Prometheus ๋ฉ”ํŠธ๋ฆญ ์ •์˜
builds_total = Counter('ci_builds_total', 'Total number of CI builds', ['status', 'branch'])
build_duration = Gauge('ci_build_duration_seconds', 'Duration of CI builds', ['branch'])
test_failures = Counter('ci_test_failures_total', 'Total number of test failures')
deployment_success = Counter('ci_deployments_total', 'Total number of deployments', ['environment', 'status'])

def get_github_builds(owner, repo, token):
    """GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ ์‹คํ–‰ ์ •๋ณด ์ˆ˜์ง‘"""
    headers = {'Authorization': f'token {token}'}
    url = f'https://api.github.com/repos/{owner}/{repo}/actions/runs'
    
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        runs = response.json()['workflow_runs']
        
        for run in runs:
            status = run['conclusion'] or 'pending'
            branch = run['head_branch']
            
            # ๋นŒ๋“œ ์นด์šดํ„ฐ ์ฆ๊ฐ€
            builds_total.labels(status=status, branch=branch).inc()
            
            # ๋นŒ๋“œ ์‹œ๊ฐ„ ์ธก์ •
            if run['updated_at'] and run['created_at'] and status != 'pending':
                start_time = datetime.datetime.fromisoformat(run['created_at'].replace('Z', '+00:00'))
                end_time = datetime.datetime.fromisoformat(run['updated_at'].replace('Z', '+00:00'))
                duration = (end_time - start_time).total_seconds()
                build_duration.labels(branch=branch).set(duration)
            
            # ํ…Œ์ŠคํŠธ ์‹คํŒจ ์ˆ˜์ง‘
            if status == 'failure':
                # ํ…Œ์ŠคํŠธ ์‹คํŒจ ์ƒ์„ธ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
                jobs_url = f'https://api.github.com/repos/{owner}/{repo}/actions/runs/{run["id"]}/jobs'
                jobs_response = requests.get(jobs_url, headers=headers)
                
                if jobs_response.status_code == 200:
                    jobs = jobs_response.json()['jobs']
                    for job in jobs:
                        if job['conclusion'] == 'failure' and 'test' in job['name'].lower():
                            test_failures.inc()

def get_deployment_status(owner, repo, token, environment):
    """๋ฐฐํฌ ์ƒํƒœ ์ •๋ณด ์ˆ˜์ง‘"""
    headers = {'Authorization': f'token {token}'}
    url = f'https://api.github.com/repos/{owner}/{repo}/deployments'
    
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        deployments = response.json()
        
        for deployment in deployments:
            if deployment['environment'] == environment:
                # ๋ฐฐํฌ ์ƒํƒœ ๊ฐ€์ ธ์˜ค๊ธฐ
                status_url = deployment['statuses_url']
                status_response = requests.get(status_url, headers=headers)
                
                if status_response.status_code == 200:
                    statuses = status_response.json()
                    if statuses:
                        latest_status = statuses[0]['state']
                        deployment_success.labels(
                            environment=environment, 
                            status=latest_status
                        ).inc()

def main():
    # Prometheus ๋ฉ”ํŠธ๋ฆญ ์„œ๋ฒ„ ์‹œ์ž‘
    start_http_server(8000)
    
    # ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ
    owner = os.environ.get('GITHUB_OWNER')
    repo = os.environ.get('GITHUB_REPO')
    token = os.environ.get('GITHUB_TOKEN')
    environments = os.environ.get('DEPLOY_ENVIRONMENTS', 'production,staging').split(',')
    
    while True:
        try:
            # GitHub ๋นŒ๋“œ ์ •๋ณด ์ˆ˜์ง‘
            get_github_builds(owner, repo, token)
            
            # ๋ฐฐํฌ ์ƒํƒœ ์ •๋ณด ์ˆ˜์ง‘
            for env in environments:
                get_deployment_status(owner, repo, token, env)
                
        except Exception as e:
            print(f"Error collecting metrics: {e}")
        
        # 5๋ถ„๋งˆ๋‹ค ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
        time.sleep(300)

if __name__ == "__main__":
    main()

โœ… ํŠน์ง•:

  • ์ทจ์•ฝ์  ์Šค์บ” ํ†ตํ•ฉ
  • ์˜์กด์„ฑ ๋ณด์•ˆ ๊ฒ€์‚ฌ
  • ์ปจํ…Œ์ด๋„ˆ ์ด๋ฏธ์ง€ ์Šค์บ”
  • ํŒŒ์ดํ”„๋ผ์ธ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ๋ฐฐํฌ ์ƒํƒœ ์ถ”์ 


์ฃผ์š” ํŒ

โœ… ๋ชจ๋ฒ” ์‚ฌ๋ก€:

  • ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ: ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋‚˜ ์‹œํฌ๋ฆฟ์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌ
  • ๋ณด์•ˆ ์„ค์ • ์ฃผ์˜: ๋นŒ๋“œ ๋ฐ ๋ฐฐํฌ ๊ณผ์ •์—์„œ ๋ณด์•ˆ ๊ฒ€์‚ฌ ๋‹จ๊ณ„ ํฌํ•จ
  • ํ…Œ์ŠคํŠธ ์ž๋™ํ™”: ๋‹จ์œ„, ํ†ตํ•ฉ, E2E ํ…Œ์ŠคํŠธ๋ฅผ ์ž๋™ํ™”ํ•˜์—ฌ ํ’ˆ์งˆ ๋ณด์žฅ
  • ๋ฐฐํฌ ์ „๋žต ์ˆ˜๋ฆฝ: ๋ธ”๋ฃจ/๊ทธ๋ฆฐ, ์นด๋‚˜๋ฆฌ, ๋กค๋ง ๋ฐฐํฌ ์ค‘ ์ ํ•ฉํ•œ ์ „๋žต ์„ ํƒ
  • ๋ชจ๋‹ˆํ„ฐ๋ง ๊ตฌ์ถ•: ํŒŒ์ดํ”„๋ผ์ธ ์„ฑ๋Šฅ๊ณผ ๋ฐฐํฌ ๊ฒฐ๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ๋กค๋ฐฑ ๊ณ„ํš ์ˆ˜๋ฆฝ: ๋ฐฐํฌ ์‹คํŒจ ์‹œ ์ž๋™ ๋˜๋Š” ์ˆ˜๋™ ๋กค๋ฐฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๊ตฌํ˜„
  • ๋ฌธ์„œํ™” ์œ ์ง€: ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์„ฑ๊ณผ ํ™˜๊ฒฝ ์„ค์ • ๋ฌธ์„œํ™”
  • ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋นŒ๋“œ ์บ์‹ฑ, ๋ณ‘๋ ฌ ์ž‘์—… ํ™œ์šฉ์œผ๋กœ ํŒŒ์ดํ”„๋ผ์ธ ์†๋„ ๊ฐœ์„ 
  • ์ฝ”๋“œํ˜• ์ธํ”„๋ผ: ์ธํ”„๋ผ ๊ตฌ์„ฑ์„ ์ฝ”๋“œ๋กœ ๊ด€๋ฆฌํ•˜์—ฌ ์žฌํ˜„์„ฑ ํ™•๋ณด
  • ๋ธŒ๋žœ์น˜ ์ „๋žต: ์ ์ ˆํ•œ Git ๋ธŒ๋žœ์น˜ ์ „๋žต๊ณผ PR ์ •์ฑ… ์ˆ˜๋ฆฝ
  • ์ง„๋ณด์  ํ…Œ์ŠคํŠธ: ์ ์ง„์ ์œผ๋กœ ๋ณต์žกํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์ ์šฉํ•˜๋Š” ์ „๋žต
  • ์•„ํ‹ฐํŒฉํŠธ ๊ด€๋ฆฌ: ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ์˜ ๋ฒ„์ „ ๊ด€๋ฆฌ์™€ ๋ณด๊ด€ ์ •์ฑ… ์ˆ˜๋ฆฝ
  • ํ†ตํ•ฉ ์•Œ๋ฆผ: ํŒŒ์ดํ”„๋ผ์ธ ๊ฒฐ๊ณผ๋ฅผ ํŒ€ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜ ๋„๊ตฌ์— ์•Œ๋ฆผ
  • ๋ฆฌ์†Œ์Šค ์ œํ•œ: CI/CD ํ™˜๊ฒฝ์˜ ์ž์› ์‚ฌ์šฉ๋Ÿ‰ ์ œํ•œ ์„ค์ •
  • ์ง€์†์  ๊ฐœ์„ : ํŒŒ์ดํ”„๋ผ์ธ ์„ฑ๋Šฅ๊ณผ ํšจ์œจ์„ฑ ์ •๊ธฐ์  ๊ฒ€ํ†  ๋ฐ ๊ฐœ์„ 


โš ๏ธ **GitHub.com Fallback** โš ๏ธ