ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • React CI/CD 파이프라인 분리
    클라우드 2025. 5. 30. 23:36

    이전 Infisical을 통한 React + Vite CI/CD의 포스팅의 느낀점으로 썼던 PR돌 때 CI/CD 파이프라인이 전부 도는 것이 이상하다고 생각하고, PR 올라갈때 CI 파이프라인이 돌고, Merge 되었을 때 CD 파이프라인이 도는게 맞다고 생각하여 파이프라인을 분리했습니다.

    기존 파이프라인은 다음과 같았습니다.

    기존 통합 파이프라인

    name: Frontend CI/CD
    
    # 풀리퀘 할때 트리거
    on:
      pull_request:
        branches: [ dev ]
    
    jobs:
    # 빌드 과정
      build:
        runs-on: ubuntu-latest
        steps:
    # 레포에서 Github Runner 서버로 코드 전송
          - uses: actions/checkout@v3
    			
    # Infisical CLI 설치
          - name: Install Infisical CLI
            run: |
              curl -1sLf '<https://artifacts-cli.infisical.com/setup.deb.sh>' | sudo -E bash
              sudo apt-get update && sudo apt-get install -y infisical
              
    # Infisical에서 .env 가져오기
          - name: Fetch .env from Infisical
            run: |
              infisical export \\\\
                --env=${{ secrets.INFISICAL_ENV_DEV }} \\\\
                --projectId=${{ secrets.INFISICAL_PROJECT_ID }} \\\\
                --token=${{ secrets.INFISICAL_DEV_TOKEN }} \\\\
                --format=dotenv \\\\
                --domain=${{ secrets.INFISICAL_API_URL }} \\\\
                > $GITHUB_WORKSPACE/.env
                
    # 프로젝트에서 사용하는 노드 버전 설치
          - name: Setup Node.js
            uses: actions/setup-node@v3
            with:
              node-version: '23.11.0'
              cache: 'npm'
    				
    # 의존성 설치
          - run: npm ci
          
    # 빌드
          - run: npm run build
    
    # 빌드 결과를 다른 job(deploy)에서 사용하기 위해 아티팩트(임시 스토리지)에 업로드
          - name: Upload dist
            uses: actions/upload-artifact@v4
            with:
              name: dist
              path: dist/
    
      # test:
      #   runs-on: ubuntu-latest
      #   steps:
      #     - uses: actions/checkout@v3
      #     - uses: actions/setup-node@v3
      #       with:
      #         node-version: '18'
      #         cache: 'npm'
      #     - run: npm ci
      #     - run: npm test
    
      deploy:
        needs: [build] # test
        runs-on: ubuntu-latest
        steps:
    # build에서 업로드 했던 빌드 결과 아티팩트로부터 다운로드
          - name: Download dist
            uses: actions/download-artifact@v4
            with:
              name: dist
              path: dist/
              
    # SSH 접속 설정
          - name: Setup SSH
            run: |
              mkdir -p ~/.ssh
              echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
              chmod 600 ~/.ssh/id_rsa
              ssh-keyscan -H ${{ secrets.GCP_VM_IP }} >> ~/.ssh/known_hosts
    
    # build 결과 SCP로 VM에 업로드
          - name: Deploy build files
            run: |
              ssh ${{ secrets.GCP_VM_USER }}@${{ secrets.GCP_VM_IP }} "rm -rf /home/${{ secrets.GCP_VM_USER }}/fe && mkdir -p /home/${{ secrets.GCP_VM_USER }}/fe"
              scp -r dist/* ${{ secrets.GCP_VM_USER }}@${{ secrets.GCP_VM_IP }}:/home/${{ secrets.GCP_VM_USER }}/fe
    
    # Nginx 설치 후 필요한 작업 수행
    # 1. 기본 설정파일 삭제
    # 2. www-data가 접근할 수 있게 o+x 명령 수행
    # 3. sites-available/myapp.conf에 설정파일 생성(빌드 결과 위치 참조, SSL 인증서 설정)
          - name: Setup Nginx and Configure Site
            run: |
              ssh ${{ secrets.GCP_VM_USER }}@${{ secrets.GCP_VM_IP }} 'sudo apt update && sudo apt install -y nginx && \\\\
              sudo rm -f /etc/nginx/sites-available/default && \\\\
              sudo rm -f /etc/nginx/sites-enabled/default && \\\\
              sudo chmod o+x /home/${{ secrets.GCP_VM_USER }} && \\\\
              sudo chmod o+x /home/${{ secrets.GCP_VM_USER }}/fe && \\\\
              echo "server {" | sudo tee /etc/nginx/sites-available/myapp.conf && \\\\
              echo "  listen 443 ssl;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "  server_name {server_name};" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "  ssl_certificate /etc/letsencrypt/live/{server_name}/fullchain.pem;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "  ssl_certificate_key /etc/letsencrypt/live/{server_name}/privkey.pem;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "  location / {" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "    root /home/${{ secrets.GCP_VM_USER }}/fe;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "    index index.html index.htm;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "    try_files \\\\$uri \\\\$uri/ /index.html;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "  }" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "}" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "server {" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "  listen 80;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "  server_name {server_name};" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "  return 301 https://\\\\$host\\\\$request_uri;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              echo "}" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\\\
              sudo ln -sf /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/myapp.conf && \\\\
              sudo nginx -t && sudo systemctl restart nginx'
    

    빌드하고 나온 그 dist 결과를 아티팩트에 올리고 같은 job 이기 때문에 바로 다운로드 받아서 VM에 올리고 nginx 리버스 프록시 설정을 해주면 배포가 완료되는 구조였습니다.

    근데 CI 파이프라인과 CD 파이프라인을 분리하게 되면 같은 job에 있지 않기 때문에 빌드한 결과물을 받아 올 수 없는 문제가 있었습니다.

    그래서 쓴 방법이 CI 파이프라인에서 빌드한 결과물을 S3에 업로드하고, CD 파이프라인에서 빌드한 결과물을 다운로드하여 VM에 배포하고 nginx 리버스하는 방식이였습니다.

    쪼개진 파이프라인은 다음과 같습니다.

    분리된 CI 파이프라인

    name: Frontend Dev CI Pipeline
    on:
      pull_request:
        branches: [ dev ]
        types: [ opened, synchronize, reopened ]
    
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v3
    
          - name: Install Infisical CLI
            run: |
              curl -1sLf '<https://artifacts-cli.infisical.com/setup.deb.sh>' | sudo -E bash
              sudo apt-get update && sudo apt-get install -y infisical
          
          - name: Fetch .env from Infisical
            run: |
              infisical export \\
                --env=${{ secrets.INFISICAL_ENV_DEV }} \\
                --projectId=${{ secrets.INFISICAL_PROJECT_ID }} \\
                --token=${{ secrets.INFISICAL_TOKEN_DEV }} \\
                --format=dotenv \\
                --domain=${{ secrets.INFISICAL_API_URL }} \\
                > $GITHUB_WORKSPACE/.env
    
          - name: Setup Node.js
            uses: actions/setup-node@v3
            with:
              node-version: '23.11.0'
              cache: 'npm'
    
          - run: npm ci
    
          - run: npm run build
    
          - name: Upload dist to S3
            env:
              AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
              AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
              AWS_REGION: ap-northeast-2
            run: |
              zip -r dist.zip dist
              aws s3 cp dist.zip s3://${{ secrets.AWS_S3_BUCKET_NAME }}/${{ secrets.AWS_S3_UPLOAD_PREFIX_DEV }}dist.zip
    

    여기서 수정된것은 on.pull_request.types 부분이 추가되었는데 type 부분이 없다면 PR 살짝만 수정해도 파이프라인이 돌아갑니다.

    예를 들면 PR에 label 추가할때 파이프라인이 돌아가는 것입니다. 이건 매우 비효율적이기 때문에 추가해주었습니다.

    그리고 마지막 단계 보면 S3에 업로드하는 부분이 생깁니다. S3 GET,PUT권한이 있는 IAM 사용자를 만들어 크리덴셜 입력해주고, 업로드할 버킷을 지정해주어 React 빌드 결과물을 올리게 수정하였습니다.

    분리된 CD 파이프라인

    name: Frontend Dev CD Pipeline
    on:
      push:
        branches: [ dev ]
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        env:  # 여기에서 설정 시 전체 step에 자동 적용됨
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: ap-northeast-2
        steps:
          - name: Download dist.zip from S3
            run: |
              aws s3 cp s3://${{ secrets.AWS_S3_BUCKET_NAME }}/${{ secrets.AWS_S3_UPLOAD_PREFIX_DEV }}dist.zip dist.zip
              unzip dist.zip -d dist
    
          - name: Setup SSH
            run: |
              mkdir -p ~/.ssh
              echo "${{ secrets.SSH_PRIVATE_KEY_DEV }}" > ~/.ssh/id_rsa
              chmod 600 ~/.ssh/id_rsa
              ssh-keyscan -H ${{ secrets.GCP_VM_DOMAIN_DEV }} >> ~/.ssh/known_hosts
    
          - name: Deploy build files
            run: |
              ssh ${{ secrets.GCP_VM_USER_DEV }}@${{ secrets.GCP_VM_DOMAIN_DEV }} "rm -rf /home/${{ secrets.GCP_VM_USER_DEV }}/fe && mkdir -p /home/${{ secrets.GCP_VM_USER_DEV }}/fe"
              scp -r dist/*/* ${{ secrets.GCP_VM_USER_DEV }}@${{ secrets.GCP_VM_DOMAIN_DEV }}:/home/${{ secrets.GCP_VM_USER_DEV }}/fe
    
          - name: Setup Nginx and Configure Site
            run: |
              ssh ${{ secrets.GCP_VM_USER_DEV }}@${{ secrets.GCP_VM_DOMAIN_DEV }} 'sudo apt update && sudo apt install -y nginx && \\
              sudo rm -f /etc/nginx/sites-available/default && \\
              sudo rm -f /etc/nginx/sites-enabled/default && \\
              sudo chmod o+x /home/${{ secrets.GCP_VM_USER_DEV }} && \\
              sudo chmod o+x /home/${{ secrets.GCP_VM_USER_DEV }}/fe && \\
              echo "server {" | sudo tee /etc/nginx/sites-available/myapp.conf && \\
              echo "  listen 443 ssl;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "  server_name ${{secrets.GCP_VM_DOMAIN_DEV}};" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "  ssl_certificate /etc/letsencrypt/live/${{secrets.GCP_VM_DOMAIN_DEV}}/fullchain.pem;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "  ssl_certificate_key /etc/letsencrypt/live/${{secrets.GCP_VM_DOMAIN_DEV}}/privkey.pem;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "  location /api/ {" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    proxy_pass <http://localhost:8080/api/;"> | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    proxy_http_version 1.1;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    proxy_set_header Host \\$host;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    proxy_set_header X-Real-IP \\$remote_addr;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    proxy_set_header X-Forwarded-For \\$proxy_add_x_forwarded_for;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    proxy_set_header X-Forwarded-Proto \\$scheme;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "  }" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "  location /api-docs {" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    proxy_pass <http://localhost:8080/api-docs;"> | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    proxy_set_header Host \\$host;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "  }" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "  location /swagger-ui/ {" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    proxy_pass <http://localhost:8080/swagger-ui/;"> | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    proxy_http_version 1.1;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    proxy_set_header Host \\$host;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    proxy_set_header X-Real-IP \\$remote_addr;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    proxy_set_header X-Forwarded-For \\$proxy_add_x_forwarded_for;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    proxy_set_header X-Forwarded-Proto \\$scheme;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "  }" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "  location / {" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    root /home/${{ secrets.GCP_VM_USER_DEV }}/fe;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    index index.html index.htm;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "    try_files \\$uri \\$uri/ /index.html;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "  }" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "}" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "server {" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "  listen 80;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "  server_name ${{secrets.GCP_VM_DOMAIN_DEV}};" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "  return 301 https://\\$host\\$request_uri;" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              echo "}" | sudo tee -a /etc/nginx/sites-available/myapp.conf && \\
              sudo ln -sf /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/myapp.conf && \\
              sudo nginx -t && sudo systemctl restart nginx'
    
    

    마찬가지로 S3 권한 가진 IAM 사용자의 크리덴셜을 입력해주고, S3에 있는 리액트 빌드 파일 다운로드 받아 서버에 배포합니다. nginx는 swagger가 사용하는 URL경로(/api-docs, /swagger-ui)를 추가해었습니다.

    지금 echo로 nginx 설정을 쓰고 있는데 yml 문법이 어려워서 그냥 이렇게 했는데 한번 yml 문법을 알고 조금 더 효율적으로 바꾸거나 설정파일을 S3에 미리 만들어두고 가져가서 입력되게 하는 방법 등 여러가지 방법을 생각해봐야할 것 같습니다.

    볼때마다 비효율적인것처럼 보여 신경이 쓰이긴 합니다.

Designed by Tistory.