CI - 100-hours-a-week/21-iceT-wiki GitHub Wiki
1. κ°μ
Kocoλ κ°λ° λ° λ°°ν¬ κ³Όμ μμ λ°μν μ μλ ν΄λ¨Ό μλ¬, μλ μ ν, νμ λΉν¨μ¨μ± λ¬Έμ λ₯Ό ν΄κ²°νκ³ μ CI(μ§μμ ν΅ν©) νμ΄νλΌμΈμ ꡬμΆνμ΅λλ€.
μ΄ λ¬Έμλ CIλ₯Ό λμ ν λ°°κ²½, λꡬ μ ν μ΄μ , κ΅¬μ± λ°©μ, μ€μ¬μ© λ°©λ²κΉμ§ μ 리ν©λλ€.
νμ΄νλΌμΈ νλ¦λ
2. λμ λ°°κ²½
(1) ν΄λ¨Ό μλ¬ κ°μ
- μμμ μ€μλ‘ μΈν μλΉμ€ μ₯μ μν μ‘΄μ¬
- μλνλ ν μ€νΈ λ° λΉλλ₯Ό μ¬λ μλ¦Όμ ν΅ν΄ ν΄λ¨Ό μλ¬λ₯Ό μ§μ νμΈ ν μ μλλ‘ ν¨
(2) μλ κ°μ
- κΈ°μ‘΄: (νλ‘ νΈ, λ°±μλ, ai) λ‘컬 λΉλ β ν μ€νΈ β λ°°ν¬ μ€λΉκΉμ§ μ½ 10λΆ μ΄μ μμ
- CI λμ μ΄ν: ν μ€νΈ λ° λΉλ 5~10λΆ μ΄λ΄ μλ£
- νκ· μμ μκ° 50% κ°μ, κ°λ° μμ°μ± ν₯μ
(3) λ°λ³΅ μμ μλν
- κΈ°μ‘΄: Git Clone μ΄ν μλ λΉλ λ° ν μ€νΈ λ°λ³΅
- CI μ μ© μ΄ν: μ½λ Pushλ§μΌλ‘ μλ λΉλ λ° ν μ€νΈ μν
(4) μμ μ± λ° ν¨μ¨μ± ν보
- κΈ°μ‘΄: Git Clone κΈ°λ° μμμ λ°°ν¬λ‘ 보μ λ° μ΄μ μν μ‘΄μ¬
- CI λμ μ΄ν: μλ² μ κ·Ό μμ΄ μ½λ κΈ°λ° κ²μ¦, 보μμ± λ° ν¨μ¨μ± ν₯μ
(5) νμ ν₯μ
- PR μμ± μ μΌκ΄λ ν μ€νΈ λ° νμ§ κ²μ¦ μλν
λ°λΌμ, μ΄λ¬ν λ¬Έμ λ€μ ν΄κ²°νκ³ μ μ ν¬λ CI(μ§μμ ν΅ν©)λ₯Ό λμ νμμ΅λλ€.
μλνλ‘ CIκ° νκ²λλ μΌ
λ¨κ³ | μ€λͺ |
---|---|
μ½λ ν΅ν© | μ¬λ¬ κ°λ°μκ° μμ±ν μ½λλ₯Ό νλμ λΈλμΉμ μμ£Ό λ³ν© |
μλ λΉλ | μ½λκ° λ³ν©λλ©΄ μλμΌλ‘ λΉλ(μ»΄νμΌ λ±)λ₯Ό μν |
μλ ν μ€νΈ | λΉλκ° μλ£λλ©΄ ν μ€νΈ μ½λ(Unit Test, Lint λ±)λ₯Ό μλμΌλ‘ μν |
νΌλλ°± μ 곡 | λΉλ/ν μ€νΈ κ²°κ³Όλ₯Ό κ°λ°μμκ² μ¦μ μλ¦Ό(GitHub Checks, Slack λ±) |
3. CI λꡬ μ ν
(1) GitHub Actionsλ₯Ό μ νν μ΄μ
- GitHub μ μ₯μ μ°λ μ΅μ ν:
.github/workflows/*.yml
μ€μ λ§μΌλ‘ λ°λ‘ μ¬μ© κ°λ₯ - κ°λ³κ³ λΉ λ₯Έ μ΄κΈ° μ€μ : Jenkinsμ²λΌ λ³λ μλ² νμ μμ
- λΉμ© ν¨μ¨: Public μ μ₯μ 무λ£, Private μ μ₯μλ μ 2,000λΆ λ¬΄λ£
- 500mb / 2000λΆ(μ) λ¬΄λ£ μ¬μ©λ κΈ°μ€μΌλ‘ 5λΆμ ci μλλ₯Ό κ°μ . ν루μ 10νμ© μ»€λ° ν μ€νΈλ₯Ό μ€νν΄λ μ¬μ λ‘μ°λ―λ‘, μ΄κΈ° νλ‘μ νΈ μ§ν μ€ ν¬λ λ§μ΄ λͺ¨μλΌμ§ μμ κ²μΌλ‘ μΆμ°.
- νλΆν μνκ³μ λ¬Έμ: λ€μν μμ μ ν νλ¦Ώ μ 곡
(2) λ€λ₯Έ CI λꡬλ₯Ό μ¬μ©νμ§ μμ μ΄μ
λꡬ | μ₯μ | λ¨μ | λΆμ ν© μ΄μ |
---|---|---|---|
Jenkins | μ€νμμ€, μ μ°μ± | μ€μΉΒ·μ΄μ λΆλ΄ νΌ | λ¨μΌ EC2μ Jenkinsλ κ³Όν¨ |
Travis CI | GitHub μ°λ μ¬μ | λλ¦° μλ, μκΈμ μ μ½ | λ¬΄λ£ νλ μ΄μ λΆκ° |
CircleCI | λΉ λ₯Έ λΉλ | 볡μ‘ν YAML ꡬ쑰 | κ³Όλ³΅μ‘ |
GitLab CI | GitLab ν΅ν© | GitHub μ°λ λΆνΈ | λΆμ ν© |
Bitbucket Pipelines | Bitbucket μ΅μ ν | κΈ°λ₯ μ ν | GitHub μ¬μ© μ€ |
CI/CD λꡬ μμ: Jenkinsμ GitHub Actions λΉκ΅
4. νμ΄νλΌμΈ ꡬμ±
(1) λΈλμΉ μ΄μ νλ¦
λΈλμΉ | λͺ©μ | νΉμ§ |
---|---|---|
feature/* |
κΈ°λ₯ κ°λ° | Lint/Unit Test μλ μν, λΉ λ₯Έ κΈ°λ₯ μμ± |
develop |
ν΅ν© ν μ€νΈ | μ¬λ¬ feature λ³ν© ν ν΅ν© ν μ€νΈ μν |
main |
μ΄μ λ°°ν¬ | μ΅μ’ λΉλ μν, νμ§ κ²μ¦ μλ£ μ½λλ§ λ³ν© |
hotfix/* |
κΈ΄κΈ μμ | μ΄μ μ€ λ²κ·Έ μμ λ° λ³ν© ν μ¬λ°μ |
(2) CI νμ΄νλΌμΈ νλ¦
λ¨κ³ | λͺ©μ | μ€λͺ | λꡬ λ° μΈμ΄ |
---|---|---|---|
μ½λ ν΅ν© (Checkout) | μ΅μ μ½λ μλ μμ | GitHubμ Push νΉμ PRμ΄ λ°μνλ©΄ CIκ° ν΄λΉ λΈλμΉμ μ½λλ₯Ό μλμΌλ‘ κ°μ Έμ΄ | actions/checkout |
μμ‘΄μ± μ€μΉ / μλ λΉλ | μ€ν νκ²½ μ€λΉ + μ½λ λΉλ | package.json , pom.xml , requirements.txt μμ μμ‘΄μ± μ€μΉ ν λΉλ μν νλ‘ νΈ: λ²λ€λ§ λ°±μλ: μ»΄νμΌ AI: λͺ¨λΈ λ‘λ© λ° ν
μ€νΈ νκ²½ κ΅¬μ± |
Node: npm ci , Java: ./gradlew build , Python: pip install -r |
μ μ λΆμ (Lint) | μ½λ μ€νμΌ μΌκ΄μ± μ μ§ | λ¬Έλ² μ€λ₯, μ€νμΌ μ€λ₯ λ± μλ λΆμ. μ¬μ λ£°μ ν΅κ³Όνμ§ λͺ»νλ©΄ PR κ±°λΆ | FE: Prettier BE: Checkstyle AI: flake8 |
λ¨μ ν μ€νΈ (Unit Test) | κΈ°λ₯ λ¨μ μ νμ± κ²μ¦ | ν¨μ, ν΄λμ€ λ± μ΅μ λ¨μκ° λ 립μ μΌλ‘ μ¬λ°λ₯΄κ² μλνλμ§ μλ κ²μ¦ | FE: Jest BE: JUnit5 AI: pytest |
ν΅ν© ν μ€νΈ (Integration Test) | μλΉμ€ μ°λ νμΈ | μ¬λ¬ μ»΄ν¬λνΈκ° μ€μ μ²λΌ ν΅ν©λμ΄ λμνλμ§ ν μ€νΈ | FE: Cypress BE: Spring νκ²½ AI: FastAPI |
ν μ€νΈ 컀λ²λ¦¬μ§ μΈ‘μ | ν μ€νΈ λ²μ νμΈ | μ 체 μ½λ μ€ ν μ€νΈκ° μ μ©λ λΉμ¨ μΈ‘μ . 80% μ΄μ κΆμ₯ | coverage , pytest-cov , jacoco |
κ²°κ³Ό νμΈ λ° μλ¦Ό | μ€μκ° νΌλλ°± μ 곡 | SlackμΌλ‘ μλ¦Ό μ μ‘. μ€ν¨ μ λΉ λ₯Έ λμ κ°λ₯ | Slack Webhook |
(3) νλ‘ νΈμλ .github/workflows/frontend-deploy-s3.yml
μμμ ci μ£Όμ ꡬ쑰
name: Deploy Frontend React s3 static hosting
on:
workflow_dispatch:
jobs:
build-deploy-frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '20.19.1'
- name: Install dependencies
run: npm ci
- name: Generate .env file for Vite (based on branch)
run: |
echo "[FRONTEND] Generating .env file for branch: ${{ github.ref_name }}"
if [ "${{ github.ref_name }}" = "main" ]; then
echo "VITE_API_BASE_URL=https://ktbkoco.com/" >> .env
echo "VITE_REDIRECT_URL=https://ktbkoco.com/oauth/kakao/callback" >> .env
echo "VITE_KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}" >> .env
else
echo "VITE_API_BASE_URL=https://koco.click/" >> .env
echo "VITE_REDIRECT_URL=https://koco.click/oauth/kakao/callback" >> .env
echo "VITE_KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}" >> .env
fi
- name: Build React App
run: npm run build
(4) λ°±μλ .github/workflows/deploy.yml
μμμ ci μ£Όμ ꡬ쑰
name: Deploy Spring Boot App
on:
workflow_dispatch:
inputs:
image_tag:
description: 'ECR μ΄λ―Έμ§ νκ·Έ (μ: v1.0.0)'
required: true
env:
AWS_REGION: ap-northeast-2
ECR_HOST: ${{ secrets.ECR_HOST }}
IMAGE_TAG: ${{ github.event.inputs.image_tag }}
DEPLOY_ZIP_KEY: spring-app-deploy.zip
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 1) Set dynamic S3 bucket and ECR repo name based on branch
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- name: Configure environment for branch
run: |
BRANCH=${GITHUB_REF#refs/heads/}
if [ "$BRANCH" = "dev" ]; then
echo "S3_BUCKET=dev-koco-codedeploy-artifacts" >> $GITHUB_ENV
echo "ECR_REPO_NAME=dev-app-repo" >> $GITHUB_ENV
echo "SSM_PATH=/spring/dev/" >> $GITHUB_ENV
echo "CD_APP_NAME=dev-was-deploy-app" >> $GITHUB_ENV
echo "CD_DG_NAME=dev-was-deploy-group" >> $GITHUB_ENV
else
echo "S3_BUCKET=prod-koco-codedeploy-artifacts" >> $GITHUB_ENV
echo "ECR_REPO_NAME=prod-app-repo" >> $GITHUB_ENV
echo "SSM_PATH=/spring/prod/" >> $GITHUB_ENV
echo "CD_APP_NAME=prod-was-deploy-app" >> $GITHUB_ENV
echo "CD_DG_NAME=prod-was-deploy-group" >> $GITHUB_ENV
fi
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 2) If manual dispatch, check for existing tag
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- name: Check if tag already exists in ECR
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
FULL_REPO="$ECR_HOST/$ECR_REPO_NAME"
echo "π Checking if tag '$IMAGE_TAG' exists at $FULL_REPO..."
RESULT=$(aws ecr describe-images \
--registry-id ${ECR_HOST%%.*} \
--repository-name $ECR_REPO_NAME \
--image-ids imageTag=$IMAGE_TAG \
--region $AWS_REGION 2>/dev/null || true)
if echo "$RESULT" | grep -q imageDigest; then
echo "β ERROR: Tag '$IMAGE_TAG' already exists in $FULL_REPO."
exit 1
fi
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 3) Build & Push Docker image for both dev and main
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- name: Build and Push Docker image to ECR
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
FULL_REPO="$ECR_HOST/$ECR_REPO_NAME"
echo "π Logging in to Amazon ECR at $FULL_REPO..."
aws ecr get-login-password --region $AWS_REGION \
| docker login --username AWS --password-stdin $ECR_HOST
echo "π§ Building Docker image..."
docker build -t my-spring-app .
echo "π· Tagging image with version: $IMAGE_TAG"
docker tag my-spring-app:latest $FULL_REPO:$IMAGE_TAG
echo "π· Tagging image as latest"
docker tag my-spring-app:latest $FULL_REPO:latest
echo "π€ Pushing image $FULL_REPO:$IMAGE_TAG"
docker push $FULL_REPO:$IMAGE_TAG
echo "π€ Pushing image $FULL_REPO:latest"
docker push $FULL_REPO:latest
5. ν μ€νΈ 컀λ²λ¦¬μ§ λͺ©ν
- 컀λ²λ¦¬μ§ κΈ°μ€:
80% μ΄μ
κΆμ₯ - μ¬μ© λꡬ μμ:
- Frontend:
Jest + coverage
- Backend:
Jacoco
- AI:
pytest-cov
- Frontend:
- Slack λ©μμ§λ PR μ½λ©νΈμ 컀λ²λ¦¬μ§ μμ½ ν¬ν¨
6. λΉλ μ±κ³΅ 쑰건
νλͺ© | 쑰건 |
---|---|
λ¦°νΈ | λͺ¨λ μλΉμ€μμ ν΅κ³Ό |
ν μ€νΈ | λͺ¨λ μλΉμ€μμ μ€ν¨ μμ΄ ν΅κ³Ό |
컀λ²λ¦¬μ§ | μ νμ κΈ°μ€ λ§μ‘± (κΆμ₯ 80% μ΄μ) |
μ¬λ μλ¦Ό | μ±κ³΅/μ€ν¨ μ¬λΆ 무κ΄νκ² νμ μ μ‘ |
μ’ λ£ | μ€ν¨ μ λ¨κ³ μ€λ¨ λ° μνμ½λ λ°ν |