CI/CD는 자동으로 통합(테스트 및 빌드)하고 자동으로 배포를 한다는 의미이다.
자동은 아니고 Continuous로 쭈욱 지속적으로 한다는 말이지만, 자동으로 빠르게 라는 뉘앙스가 크다.
백엔드에서는 CI 과정에서 코드 포매팅 및 테스트를 진행하고 이를 통과 하면 빌드를 하는 과정을 거친다.
우리는 이전에 React - 코드 규칙 설정에서 다루었다.
이러한 CI/CD를 도와주는 툴로는 Github Actions이 대표적으로 쓰인다.
Travis 혹은 Jenkins도 쓰이지만 대부분의 문서들도 Github Actions으로 시작하므로 해당을 사용해보자.
Github Actions는 깃허브자체에서 제공되는 CI/CD 워크플로우 툴이다.
main 브랜치에 푸시가 되거나, 태그로 v1.0.0 같은 태그의 커밋이 푸시가 되거나,
PR이 되어 머지가 되거나 이런 이벤트를 감지하면 우리가 정의한 명령을 수행한다.
이러한 이벤트나 행동을 .github/workflows/이름.yaml
파일을 통해 정의할 수 있다.
.github/workflows/react-cicd.yaml
파일을 다음과 같이 생성한다.
name: React CI/CD
on:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Install Dependencies
run: npm ci
- name: ESLint and Prettier Check
run: npx eslint .
npm ci 는 무엇이죠?
여기서 Install Dependencies 스텝에서 npm ci 라는 명령어가 보일 것이다.
개발 단계에서는 npm install을 사용해서 종속성을 설치하지만, CI/CD 단계에서는 단순히 배포를 위한 과정을 거치는 것이기 중요하다.
package-lock.json
을 통해 정확한 버전의 종속성을 설치하고package.json
으로 버전을 검증한다고 한다.
그리고 yarn 환경이라면 마지막 환경에서 npx eslint .
대신 yarn eslint .
를 사용하면 된다.
프론트엔드에서는 테스트를 한다는 경우가 있다. 테스트코드를 만약 작성했다면, 다음의 작업도 진행하자.
test:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Install Dependencies
run: npm ci
- name: Run Tests
run: npm test
이후 린트와 테스트를 통과했다면 빌드를 진행한다.
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Install Dependencies
run: npm ci
- name: Build React App
run: npm run build
다음의 코드는 배포를 하는 과정이다.
마지막 Deploy to Hosting Service에서는 aws를 사용할지 각종 호스팅 서버를 사용할지
아니면 on premise로 본인의 컴퓨터에서 배포할지를 정할 수 있다.
우리는 대표적인 aws의 s3를 활용한 cloudfront로 배포할 것이다.
deploy:
runs-on: ubuntu-latest
needs: [lint, test, build]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Install Dependencies
run: npm ci
- name: Deploy to Hosting Service
run: |
# Add your deployment script or commands here
echo "Deploying to production..."
리전과 이름을 지정한다.
객체 소유권은 권장으로 지정한다.
버킷 내부의 객체(파일 및 폴더)에 IAM으로 정책을 통해 권한을 부여한 사람만 접근 가능하도록 설정하는 것인데,
어지간하면 개인 or 포트폴리오용 팀플이기 때문에 필요가 없다.
모든 퍼블릭 접근은 차단한다.
나중에 CloudFront에서 접근을 풀기 때문이다.
버킷 버전 관리는 혹시나 해서 활성화 한다.
이외 다른 옵션은 기본값으로 진행하였다.
CI/CD를 누구가 하나. 바로 Github actions에서 Ubuntu:latest 친구가 해줄 것이다.
이 친구는 우리 s3에 프로젝트를 빌드해서 업로드 해주어야할 것이다.
이를 위해 Ubuntu:latest 친구에게 s3에 대한 권한을 줘야한다.
그래서 IAM 사용자를 생성하고 s3 권한을 부여한다.
IAM - 사용자 - 사용자 생성에서 다음처럼 이름을 지정해 생성한다.
사용자 지정 암호는 골뱅이를 넣고 느낌표를 (@!Aa82*) 열심히 넣고 잘복사해서 간직하고 있는다.
권한은 직접 정책 연결로 하여 s3 풀액세스를 때리자.
그리고 사용자 생성을 한다.
생성한 사용자에 들어가 보안 자격 증명으로 들어간다.
우리는 Github Actions에서 IAM에 접근하므로 AWS 외부에서 실행되는 애플리케이션
을 선택한다.
액세스 키와 비밀 액세스 키는 잘 복사해서 메모장에 잠시 둔다.
절대절대 IAM 키를 노출시키지 마라
이제 이렇게 만든 사용자를 github-actions 의 빌드 머신에게 권한으로 넘겨줄 건데, 이를 보통 secret key로 관리한다.
이게 노출되면 해커들은 github repo를 보고 열심히 당신의 iam s3 full access 사용자를 마음껏 사용할 것이다.
깃 레포를 퍼블릭으로 해놓으면 메일에 해커들이 님 노출됨 ㅋㅋ 돈내셈 이렇게 올수도있는데 그냥 무시해라.
이 글을 잘 따라하면 저 메일들이 날아와도 무시할 만한 보안성이 만족된다.
이제 사용자 이름과 비번을 Git repo에 등록한다.
우리 레포지토리는 퍼블릭이라 키가 공개되는거 아닌가요??
그렇다. 우리는 컴공인으로써, 모든 레포지토리를 오픈소스에 기여하기 위해
혹은 포트폴리오를 위해 기록으로 남겨두기 위해 코드를 퍼블릭으로 공개중에 있다.
그렇다면 Deploy 일련의 단계에서 코드를 하드코딩해버릴까?
아니다. Github Secret을 사용하면 된다.
위에서 Setting에 들어간다.
그리고 Secrets and variables에서 Actions을 눌러 Repository secrets를 추가한다.
이름은 자유이고 이렇게 3개를 추가한다.
이제 위 3개는 환경변수처럼 .github/workflows/react-cicd.yaml
에서 사용할 수 있다.
# GitHub Actions 에서 S3 풀액세스 권한 사용자에 로그인
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.IAM_S3_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.IAM_S3_ACCESS_PWD }}
aws-region: ap-northeast-2
# react 빌드한 파일 /build를 s3로 업로드
- name: Upload /build to S3
env:
BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME}}
run: |
aws s3 cp --recursive --region ap-northeast-2 build s3://$BUCKET_NAME
최종적으로 생성된 yaml 파일이다.
name: ESLint and Prettier Check
on:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Install Dependencies
run: npm ci
- name: ESLint and Prettier Check
run: npx eslint .
test:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Install Dependencies
run: npm ci
- name: Run Tests
run: npm test
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Install Dependencies
run: npm ci
- name: Build React App
run: npm run build
deploy:
runs-on: ubuntu-latest
needs: [lint, test, build]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Install Dependencies
run: npm ci
# GitHub Actions 에서 S3 풀액세스 권한 사용자에 로그인
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.IAM_S3_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.IAM_S3_ACCESS_PWD }}
aws-region: ap-northeast-2
# react 빌드한 파일 /build를 s3로 업로드
- name: Upload /build to S3
env:
BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME}}
run: |
aws s3 cp --recursive --region ap-northeast-2 build s3://$BUCKET_NAME
하지만 여기서는 각 과정마다 우분투 머신을 부팅하고 node 를 설치하고 npm ci 를 하는 과정이 포함되어 있다.
이를 빠르게 하려면 패키지들을 캐싱할 수 있다.
name: ESLint and Prettier Check
on:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Cache Node Modules
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Dependencies
run: npm ci
- name: ESLint and Prettier Check
run: npx eslint .
test:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Cache Node Modules
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Dependencies
run: npm ci
- name: Run Tests
run: npm test
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Cache Node Modules
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Dependencies
run: npm ci
- name: Build React App
run: npm run build
deploy:
runs-on: ubuntu-latest
needs: [lint, test, build]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Cache Node Modules
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Dependencies
run: npm ci
# GitHub Actions에서 S3 풀액세스 권한 사용자에 로그인
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.IAM_S3_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.IAM_S3_ACCESS_PWD }}
aws-region: ap-northeast-2
# React 빌드한 파일 /build를 S3로 업로드
- name: Upload /build to S3
env:
BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME}}
run: |
aws s3 cp --recursive --region ap-northeast-2 build s3://$BUCKET_NAME
설명을 위해 각 스텝 lint -> test -> build -> deploy를 따로 머신을 생성해 진행하였다.
build에서 빌드를 하고 deploy에서 다른 우분투머신으로 생성하면 이미 이전의 빌드한 내용은 사라져있다.
그래서 이 스텝을 하나로 묶는다.
name: React CI/CD
on:
push:
branches:
- main
jobs:
react-cicd:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Cache Node Modules
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Dependencies
run: npm ci
- name: Run Lint
run: npm run lint
npm test
- name: Run Test
run: npm test
- name: Build React App
run: npm run build
- name: Deploy to S3
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.IAM_S3_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.IAM_S3_ACCESS_PWD }}
aws-region: ap-northeast-2
- name: Upload /build to S3
env:
BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME}}
run: |
aws s3 cp --recursive --region ap-northeast-2 build s3://$BUCKET_NAME
테스트를 위해 메인 브랜치에 변경을 하고 푸시를한다.
일련의 과정들이 모두 성공하면 S3로 파일이 빌드되어 업로드 된다.
필수 참고
글쓴이는 여기서 1시간동안 트러블 슈팅을 했다.
react 18.0.2버전인데 node 버전은 14였다. 14를 20으로 변경하니 해결되었다.
반드시 본인의 노드버전과 리액트 버전이 동일한지 확인하고 Github Actions의 우분투 머신도 버전을 동일하게 한다.
또한, TEST 패키지도 잘 포함되어있는지 확인한다.
"@testing-library/jest-dom": "^6.2.0",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.2",
나는 이렇게 쓰고 있다.
CloudFront에 들어가 배포를 생성한다.
원본 도메인에서 본인의 s3를 선택한다.
AWS WAF 도 할건지 물어보는데 돈드니까 일단 비허용을 한다.
여기서 제어 설정 생성을 한다.
기본값으로 두고 생성한다.
참고로 제어 설정의 이름은 원본 도메인과 일치해야한다.
다르다면 Access Deny가 뜬다.
이후 생성한 제어 설정을 등록해준다.
기본값의 링크도 index.html 문서로 설정해준다. (/index.html 금지)
이외 HTTPS 를 사용한다면 SSL 인증서를 발급받고 지정하여 배포를 시작한다.
클라우드프론트 링크로 안들어가져요!
클라우드 프론트의 배포를 눌러 원본-편집을 들어간다.
정책을 복사한다.
S3의 정책을 추가하기 위해 버킷-권한에 들어간다.
버킷 정책에 들어가 복사한 정책을 추가한다.
이렇게 하면 드디어 동작한다!