Frontend

Cypress를 GitHub Actions에 연동해보자!

mechaniccoder 2022. 10. 7. 01:31

이번 포스팅에서는 실무에서 cypress를 github actions CI에 연동하며 겪었던 이슈들을 공유해보려 합니다. 아래와 같은 순서로 글을 이어가겠습니다.

 

  • cypress를 github actions에 연동을 하려 했던 배경
  • github actions에 대한 설명 그리고 발생했던 이슈
  • trouble shooting

Cypress를 github actions에 연동하려한 배경


최근에 장기간 진행했던 프로젝트의 알파버전 출시를 위해 QA를 돌리는 과정에서 문제가 됐습니다. 이미 해결한 QA항목들에 대해 새로운 PR이 머지되고 다시 QA를 돌리게 되면 해결됐던 버그들이 다시 발생했습니다. 아무래도 사람이 직접 테스트를 하다보니까 regression test가 제대로 되지 않았나 봅니다... 이런 문제들을 해결하기 위해 cypress를 도입하게 됐습니다. cypress를 도입한 후, production에 나가기 전이나, 이틀에 한 번씩 테스트를 돌리는 시점을 자동화하기 위해 github actions을 이용해 cypress test를 붙이기로 했습니다.

github actions CI Flow


이번 프로젝트에서는 쿠키 인증방식을 사용했습니다. 그에 따라 github actions을 구성하는데도 몇가지 설정이 추가가 됐습니다.

호스트명 변경

로컬에서 개발할때, localhost가 아닌 실제 도메인을 사용합니다. 이렇게 호스트명을 변경해서 사용하는 이유는 쿠키를 사용하기 때문입니다. api서버가 쿠키 인증 방식을 사용하기 때문에 쿠키 도메인 옵션을 맞춰줬습니다.

루프백 호스트명을 변경하기 위해 /etc/hosts 파일을 수정합니다.

# /etc/hosts

127.0.0.1 example-local.com

HTTPS

마찬가지로, 쿠키 인증방식을 사용하는게 큽니다. secure 옵션이 켜져있기 때문에 HTTPS 환경이어야만 쿠키로 인증을 할 수 있기 때문입니다.

로컬에서 HTTPS환경을 구축하려면 웹서버와 인증서가 있어야하는데요. 각각 Nginx, mkcert를 활용해서 이를 설정했습니다.

GitHub Actions


위의 사항들을 고려해서 github actions workflow를 생성했습니다.

루프백 호스트명 추가

먼저 github host에 루프백 ip에 대한 호스트명을 추가해줍니다. 이렇게 설정하면 나중에 서버 요청시, example-local.com 도메인으로 접근할 수 있습니다.

- name: Add host "example-local.com"
  run: sudo echo "127.0.0.1  example-local.com" | sudo tee -a /etc/hosts

인증서 발급

mkcert를 설치한 후, 인증서와 키를 발급아야합니다. 인증서와 키에 대한 경로는 이후에 Nginx가 설정파일이 사용하는 경로랑 맞춰줘야 합니다. 여기서는 nginx/로 설정했습니다.

- name: Install mkcert
  run: sudo apt install libnss3-tools -y && curl -JLO "<https://dl.filippo.io/mkcert/latest?for=linux/amd64>" && chmod +x mkcert-v*-linux-amd64 && sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert

- name: Create CA and private key
  run: mkcert -install && mkcert example.local.com && mv ./example-local.com.pem ./example-local.com-key.pem ./nginx/

Nginx 실행

루프백 호스트명을 추가하고 인증서를 발급했으니 이를 사용하는 웹서버인 Nginx를 실행해줘야 합니다. Nginx는 https 요청을 받아서 api서버로 요청을 전달하는 역할을 합니다. github host에서는 기본적으로 docker와 docker compose를 지원해주기 때문에 이를 활용해 nginx 서버를 실행합니다.

- name: Run nginx
  run: docker-compose -f ./docker-compose.e2e.yml up --build -d
  working-directory: ./nginx

cypress 실행

기본적인 설정은 끝났으니, cypress를 돌립니다. cypress가 제공해주는 actions를 활용하면 cypress를 실행하기전에 dependency 설치, build 그리고 서버를 시작할 수 있습니다.

- name: Cypress run
  uses: cypress-io/github-action@v4
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    # 환경변수
  with:
    build: yarn build
    start: yarn start -p 3001
    record: true

Issues

Docker와 호스트 네트워크

Nginx는 docker compose로 실행하기 때문에 docker가 제공해주는 네트워크 환경에 있습니다. 따라서 Voicelab이 실행되고 있는 GitHub host환경과는 다른 네트워크이죠. docker 네트워크가 host 환경에 대한 이름을 알 수 있어야 하는데요. voicelab의 nginx 설정파일을 확인해보시면 host.docker.internal 이라는 호스트명이 호스트 네트워크를 가리킨다는 것을 알 수 있습니다.

다만, 이 호스트명은 mac, window 환경에서 유효하기 때문에 linux 환경인 github host에서는 유효하지 않습니다. 이를 해결하기 위해 여러 솔루션들을 찾아봤는데 그 중에 대표적인 해결방법은 아래와 같습니다.

# docker-compose file
version: '3.4'
services:
  nginx:
    build:
      dockerfile: ./Dockerfile.e2e
      context: .
		# 아래와 같이 host를 설정해 해결하는 대표적인 방법입니다.
		extra_hosts:
      - "host.docker.internal:host-gateway"

이를 적용해봤을때 해결이 되지 않아서 docker와 host 네트워크를 분리하지 않는 방법을 선택했습니다.

version: '3.4'
services:
  nginx:
    build:
      dockerfile: ./Dockerfile.e2e
      context: .
		# container와 host가 동일한 네트워크에 있습니다.
    network_mode: host

network_mode를 host로 설정해주면 container와 host가 같은 네트워크 안에 있기 때문에 port만 맞춰주면 정상적으로 요청이 가는 것을 확인할 수 있었습니다.

환경변수 관련 이슈

cypress를 실행할때 환경변수가 제대로 주입되지 않는 이슈가 있었습니다. github secrets에 설정하면 cypress actions에서 next.js 서버를 실행할때 환경변수를 넣어준다고 생각했는데 그게 아니었습니다. 아래와 같이 cypress actions의 env에 명시적으로 적어줘야 next.js 서버를 실행할때 환경변수를 주입해주더라고요.

따라서 해당 step에 env 설정을 해서 이를 해결했습니다.

- name: Cypress run
  uses: cypress-io/github-action@v4
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    CYPRESS_apiUrl: ${{ secrets.NEXT_PUBLIC_API_SERVER_URL }}
    CYPRESS_TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
    CYPRESS_TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
    CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
    CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
    NEXT_PUBLIC_API_SERVER_URL: ${{ secrets.NEXT_PUBLIC_API_SERVER_URL }}
    NEXT_PUBLIC_FACEBOOK_APP_ID: ${{ secrets.NEXT_PUBLIC_FACEBOOK_APP_ID }}
    NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_GOOGLE_CLIENT_ID }}
  with:
    build: yarn build
    start: yarn start -p 3001
    record: true

Background


voicelab이 개발되고 알파버전 출시를 위해 QA를 돌리는 과정에서 문제가 됐습니다. 이미 해결한 QA항목들에 대해 새로운 PR이 머지되고 다시 QA를 돌리게 되면 해결됐던 버그들이 다시 발생했습니다. 아무래도 사람이 직접 테스트를 하다보니까 regression test도 제대로 되지 않았기 때문입니다. 이런 문제들을 해결하기 위해 cypress를 도입하게 됐습니다.

cypress를 도입한 후, production에 나가기 전이나, 이틀에 한 번씩 테스트를 돌리는 시점을 자동화하기 위해 github actions을 이용해 cypress test를 붙이기로 했습니다.

CI Flow


voicelab product에 대한 e2e test를 돌리기 위해서는 몇가지 추가적인 설정이 필요했습니다.

호스트명 변경

로컬에서 개발할때, localhost가 아닌 voice-lab.lovo.ai를 사용합니다. 이렇게 호스트명을 변경해서 사용하는 이유는 쿠키를 사용하기 때문입니다. api서버가 쿠키 인증 방식을 사용하기 때문에 쿠키 도메인 옵션을 맞춰주기 위함입니다.

루프백 호스트명을 변경하기 위해 /etc/hosts 파일을 수정합니다.

# /etc/hosts

127.0.0.1 voicelab-local.lovo.ai

HTTPS

마찬가지로, 쿠키 인증방식을 사용하는게 큽니다. secure 옵션이 켜져있기 때문에 HTTPS 환경이어야만 쿠키로 인증을 할 수 있기 때문입니다.

로컬에서 HTTPS환경을 구축하려면 웹서버와 인증서가 있어야하는데요. 각각 Nginx, mkcert를 활용해서 이를 설정했습니다.

Check out how to configure Nginx and mkcert

GitHub Actions


위의 사항들을 고려해서 github actions workflow를 생성해야 했습니다. workflow에서 고려해야할 점들은 아래와 같습니다.

<aside> 💡 여기서는 github actions 문법을 다루지는 않습니다. workflow syntax를 확인해보세요.

</aside>

루프백 호스트명 추가

먼저 github host에 루프백 ip에 대한 호스트명을 추가해줍니다. 이렇게 설정하면 나중에 서버 요청시, voicelab-local.lovo.ai 도메인으로 접근할 수 있습니다.

- name: Add host "voicelab-local.lovo.ai"
  run: sudo echo "127.0.0.1  voicelab-local.lovo.ai" | sudo tee -a /etc/hosts

인증서 발급

mkcert를 설치한 후, 인증서와 키를 발급아야합니다. 인증서와 키에 대한 경로는 이후에 Nginx가 설정파일이 사용하는 경로랑 맞춰줘야 합니다. 여기서는 nginx/로 설정했습니다.

<aside> 💡 mkcert를 설치 시, linux 환경에 대한 설치 가이드를 따라야 합니다. github host가 linux 환경이기 때문입니다.

</aside>

- name: Install mkcert
  run: sudo apt install libnss3-tools -y && curl -JLO "<https://dl.filippo.io/mkcert/latest?for=linux/amd64>" && chmod +x mkcert-v*-linux-amd64 && sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert

- name: Create CA and private key
  run: mkcert -install && mkcert voicelab-local.lovo.ai && mv ./voicelab-local.lovo.ai.pem ./voicelab-local.lovo.ai-key.pem ./nginx/

Nginx 실행

루프백 호스트명을 추가하고 인증서를 발급했으니 이를 사용하는 웹서버인 Nginx를 실행해줘야 합니다. Nginx는 https 요청을 받아서 Voicelab 서버로 요청을 전달하는 역할을 합니다. github host에서는 기본적으로 docker와 docker compose를 지원해주기 때문에 이를 활용해 nginx 서버를 실행합니다.

- name: Run nginx
  run: docker-compose -f ./docker-compose.e2e.yml up --build -d
  working-directory: ./nginx

cypress 실행

기본적인 설정은 끝났으니, cypress를 돌립니다. cypress가 제공해주는 actions를 활용하면 cypress를 실행하기전에 dependency 설치, build 그리고 서버를 시작할 수 있습니다.

- name: Cypress run
  uses: cypress-io/github-action@v4
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    CYPRESS_apiUrl: ${{ secrets.NEXT_PUBLIC_API_SERVER_URL }}
    CYPRESS_TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
    CYPRESS_TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
    CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
    CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
    NEXT_PUBLIC_API_SERVER_URL: ${{ secrets.NEXT_PUBLIC_API_SERVER_URL }}
    NEXT_PUBLIC_FACEBOOK_APP_ID: ${{ secrets.NEXT_PUBLIC_FACEBOOK_APP_ID }}
    NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_GOOGLE_CLIENT_ID }}
  with:
    build: yarn build
    start: yarn start -p 3001
    record: true

 

Trouble shooting


 

Docker와 host network

Nginx는 docker compose로 실행하기 때문에 docker가 제공해주는 네트워크 환경에 있습니다. 따라서 Voicelab이 실행되고 있는 GitHub host환경과는 다른 네트워크이죠. docker 네트워크가 host 환경에 대한 이름을 알 수 있어야 하는데요. api 서버의 nginx 설정파일을 확인해보시면 host.docker.internal 이라는 호스트명이 호스트 네트워크를 가리킨다는 것을 알 수 있습니다.

다만, 이 호스트명은 mac, window 환경에서 유효하기 때문에 linux 환경인 github host에서는 유효하지 않습니다. 이를 해결하기 위해 여러 솔루션들을 찾아봤는데 그 중에 대표적인 해결방법은 아래와 같습니다.

# docker-compose.yml
version: '3.4'
services:
  nginx:
    build:
      dockerfile: ./Dockerfile.e2e
      context: .
    	# 아래와 같이 host를 설정해 해결하는 대표적인 방법입니다.
    	extra_hosts:
      - "host.docker.internal:host-gateway"

이를 적용해봤을때 해결이 되지 않았습니다. docker와 host 네트워크를 분리하지 않는 방법을 선택했습니다.

version: '3.4'
services:
  nginx:
    build:
      dockerfile: ./Dockerfile.e2e
      context: .
	# container와 host가 동일한 네트워크에 있습니다.
    network_mode: host

network_mode를 host로 설정해주면 container와 host가 같은 네트워크 안에 있기 때문에 port만 맞춰주면 정상적으로 요청이 가는 것을 확인할 수 있었습니다.

환경변수 관련 이슈

cypress를 실행할때 환경변수가 제대로 주입되지 않는 이슈가 있었습니다. github secrets에 설정하면 cypress actions에서 next.js 서버를 실행할때 환경변수를 넣어준다고 생각했는데 그게 아니었습니다. 아래와 같이 cypress actions의 env에 명시적으로 적어줘야 next.js 서버를 실행할때 환경변수를 주입해주더라고요.

따라서 해당 step에 env 설정을 해서 이를 해결했습니다.

- name: Cypress run
  uses: cypress-io/github-action@v4
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    CYPRESS_apiUrl: ${{ secrets.NEXT_PUBLIC_API_SERVER_URL }}
    CYPRESS_TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
    CYPRESS_TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
    CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
    CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
    NEXT_PUBLIC_API_SERVER_URL: ${{ secrets.NEXT_PUBLIC_API_SERVER_URL }}
    NEXT_PUBLIC_FACEBOOK_APP_ID: ${{ secrets.NEXT_PUBLIC_FACEBOOK_APP_ID }}
    NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_GOOGLE_CLIENT_ID }}
  with:
    build: yarn build
    start: yarn start -p 3001
    record: true

 

마치면서

이렇게 구성한 cypress는 현재 신뢰할 수 없는 테스트가 됐습니다. 로컬에서 돌리때는 테스트가 잘 통과가 되지만 github actions을 통해 돌린 cypress test 결과가 대부분이 fail이 났기 때문입니다. 아마 로컬은 macos이고 github host는 linux이기 때문에 os가 다름에서 발생하는 이슈라 가정을 하고 있고 당장 처리해야될 이슈가 아니기 때문에 백로그에 쌓아둔 상태입니다.

cypress를 사용하면서 느꼈던 점은 아무래도 e2e test framework을 처음 도입한 것이고 경험이 많지 않았기 때문에 모든 것을 e2e test로 처리할 수 있다는 생각을 했던 것 같습니다. unit test로 처리할 수 있음에도 불구하고 간단한 기능을 e2e test로 처리하려고 하니 테스트가 매우 느릴 수 밖에 없고 flaky한 testcase들이 발생할 수 밖에 없는 구조라는 생각을 했습니다. 따라서 앞으로 개선할 점은 아래와 같습니다.

  • 간단한 기능에 대한 테스트는 unit test로 처리할 것 (testing-library와 jest를 이용하자.)
  • unit test로 처리할 수 있는지를 먼저 생각해보자. (unit test는 빠르다. e2e test는 느리다!)
  • github actions로 cypress를 돌릴때 로컬에서 돌린 환경을 최대한 맞춰주자. (이 다음에는 github host를 macOS로 사용해보자.)