반응형

이전 글 배포자동화(CI/CD) - Github Action/Nuxtjs/Docker/EC2에서는 Github Action을 활용한 배포자동화를 중점적으로 다루었습니다.

 

아래는 이 글의 동영상 강의입니다.

https://youtu.be/eyFO57ZkJaI

 

이걸 조금 더 발전시켜 컨테이너를 2중화 시키고 순차적으로 배포를 해서, 시스템의 중단 없이 배포하는 방법에 대해 알아봅니다.

구성도는 다음과 같습니다.

환경은 이전과 동일합니다.

다만 분산처리를 위해 nginx proxy만 추가 되었습니다.

그리고 docker instance를 2개를 생성합니다.

환경

  • 프론트 : Vuejs (Nuxt)
  • 형상관리 : Git (Github)
  • 빌드 : Docker (Github Action)
  • 서버 : Linux (AWS EC2 micro)
  • 프록시 : nginx

이전 글에서 이미 Docker Image등의 구성이 완료되었으므로, 프로젝트 폴더의 .github/workflow/main.yml 파일을 수정하여 2중화를 구성할 수 있습니다.

 

.github/workflows/main.yml

name: CI/CD Docker

# 트리거를 수행할 브랜치를 지정합니다.
on:
  push:
    branches: [ main ]

# 환경설정
env:
  DOCKER_IMAGE: ghcr.io/${{ github.actor }}/nuxt-auto-deploy
  VERSION: ${{ github.sha }}
  CONTANIER_NAME1: nuxt-auto-deploy1
  CONTANIER_NAME2: nuxt-auto-deploy2
  DOMAIN_NAME: domain or ip
  NAME: prod

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      # github repository에서 checkout
      - uses: actions/checkout@v2
      # docker build 수행
      - name: Set up docker buildx
        id: buildx
        uses: docker/setup-buildx-action@v1
      - name: Cache docker layers
        uses: actions/cache@v2
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ env.VERSION }}
          restore-keys: |
            ${{ runner.os }}-buildx-
      # GitHub 컨테이너 레지스트리에 로그인 후 빌드 & 푸시
      - name: Login to ghcr
        uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v2
        with:
          builder: ${{ steps.buildx.outputs.name }}
          push: true
          tags: ${{ env.DOCKER_IMAGE }}:${{ env.VERSION }}
  # 배포 Job
  deploy:
    needs: build  # build 후에 실행되도록 정의
    name: Deploy
    runs-on: [ self-hosted, label-go ] # AWS ./configure에서 사용할 label명
    steps:
      - name: Login to ghcr
        uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Docker run
        run: |
          docker ps -q --filter "name=${{ env.CONTANIER_NAME1 }}" | grep -q . && docker stop ${{ env.CONTANIER_NAME1 }} && docker rm -fv ${{ env.CONTANIER_NAME1 }}
          docker run -d \
                -e VIRTUAL_HOST=${{ env.DOMAIN_NAME }} \
                -e CONTANIER_NAME=${{ env.CONTANIER_NAME1 }} \
                --name ${{ env.CONTANIER_NAME1 }} \
                --restart always ${{ env.DOCKER_IMAGE }}:${{ env.VERSION }}
          docker ps -q --filter "name=${{ env.CONTANIER_NAME2 }}" | grep -q . && docker stop ${{ env.CONTANIER_NAME2 }} && docker rm -fv ${{ env.CONTANIER_NAME2 }}
          docker run -d \
                -e VIRTUAL_HOST=${{ env.DOMAIN_NAME }} \
                -e CONTANIER_NAME=${{ env.CONTANIER_NAME2 }} \
                --name ${{ env.CONTANIER_NAME2 }} \
                --restart always ${{ env.DOCKER_IMAGE }}:${{ env.VERSION }}
          docker image prune -af

env: 아래에 컨테이너 명을 다음과 같이 추가합니다.

  CONTANIER_NAME1: nuxt-auto-deploy1
  CONTANIER_NAME2: nuxt-auto-deploy2
  DOMAIN_NAME: domain or ip

deploy -> steps -> -name: Docker run

아래에 컨테이너 실행 명령을 다음과 같이 입력합니다.

- name: Docker run
run: |
  docker ps -q --filter "name=${{ env.CONTANIER_NAME1 }}" | grep -q . && docker stop ${{ env.CONTANIER_NAME1 }} && docker rm -fv ${{ env.CONTANIER_NAME1 }}
  docker run -d \
        -e VIRTUAL_HOST=${{ env.DOMAIN_NAME }} \
        -e CONTANIER_NAME=${{ env.CONTANIER_NAME1 }} \
        --name ${{ env.CONTANIER_NAME1 }} \
        --restart always ${{ env.DOCKER_IMAGE }}:${{ env.VERSION }}
  docker ps -q --filter "name=${{ env.CONTANIER_NAME2 }}" | grep -q . && docker stop ${{ env.CONTANIER_NAME2 }} && docker rm -fv ${{ env.CONTANIER_NAME2 }}
  docker run -d \
        -e VIRTUAL_HOST=${{ env.DOMAIN_NAME }} \
        -e CONTANIER_NAME=${{ env.CONTANIER_NAME2 }} \
        --name ${{ env.CONTANIER_NAME2 }} \
        --restart always ${{ env.DOCKER_IMAGE }}:${{ env.VERSION }}
  docker image prune -af

이렇게 하면 컨테이너가 순차적으로 2개 구동이 됩니다.

 

배포시에 1개 서버 배포가 완료되어야 다음 단계를 진행하게 되므로, 사용자는 중단 없이 서비스를 이용할 수 있습니다.

 

이제 서버를 분배할 nginx proxy를 설정합니다. 

우리는 jwilder/nginx-proxy 도커 이미지를 사용합니다.

https://hub.docker.com/r/jwilder/nginx-proxy

 

Docker Hub

 

hub.docker.com

EC2에 로그인한 후 vhost.d 폴더를 생성합니다.

/home/ec2-user/vhost.d

 

vhost.d 폴더로 이동한 후,

ec2의 domain명으로 파일을 생성하고 아래 내용을 작성합니다.

- 이 옵션은 nginx의 보안을 위해 버전을 표시하지 않게 합니다.

server_tokens off;

 

docker를 실행합니다.

docker run -d -p 80:80 -p 443:443 
 -v /home/ec2-user/vhost.d:/etc/nginx/vhost.d:ro 
 -v /var/run/docker.sock:/tmp/docker.sock:ro 
 jwilder/nginx-proxy:alpine

 

반응형
반응형

여러 테이블의 데이터를 한꺼번에 처리할 경우가 있습니다.

예를 들면 회원 탈퇴의 경우,

고객의 계정 정보와 다른 여러 테이블에 들어있는 정보들을 한꺼번에 삭제해줘야만 하죠.

이때 특정 데이터가 삭제되지 않고 남게 되면 문제가 발생할 수 있습니다.

이렇게 모든 데이터를 한꺼번에 지우고, 만약 하나라도 오류가 발생할 경우 모든 것을 원복하는 것을 트랜젝션이라 합니다.

 

Nestjs에서 typeorm을 사용할때 트랜젝션 처리 하는 샘플코드를 만들어봅니다.

 

총 3개의 테이블에 데이터가 들어 있습니다.

- user : 사용자

- user_authority : 사용자 권한

- survey : 설문 정보

 

각각의 테이블 정보를 삭제하는 코드는 다음과 같습니다.

- user(사용자) 삭제

async deleteUser(user: User): Promise<any>{
    return await this.userRepository.createQueryBuilder()
        .delete()
        .from(User, 'user')
        .where('id = :id', { id: user.id })
        .execute();
}

- user_authority(사용자 권한) 삭제

async deleteUserAuthorities(user: User): Promise<any>{
    return await this.userAuthorityRepository.createQueryBuilder()
        .delete()
        .from(UserAuthority, 'userAuthority')
        .where('userId = :userId', { userId: user.id })
        .execute();
}

- survey(설문 정보) 삭제

async deleteAllUserSurvey(user: User): Promise<any>{
    return await this.surveyRepository.createQueryBuilder()
        .delete()
        .from(Survey, 'survey')
        .where('userId = :userId', { userId: user.id})
        .execute();
}

마지막에는 commit을 해줍니다.

await queryRunner.commitTransaction()

이 3가지를 transaction으로 처리하는 코드는 다음과 같습니다.

import {DataSource, FindOneOptions, Repository} from 'typeorm';
...

// 회원 탈퇴
async cancelUser(user: User): Promise<any>{
    // transaction 처리
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    try{
        // survey 삭제
        await this.deleteAllUserSurvey(user);
        // 사용자 삭제
        await this.deleteUser(user);
        // 사용자 권한 삭제
        await this.deleteUserAuthorities(user);
        // commit
        await queryRunner.commitTransaction()
    } catch (err) {
    	// 실패시 rollback
        await queryRunner.rollbackTransaction();
    } finally {
    	// release
        await queryRunner.release();
    }
}

이렇게 작성하시면 3개가 모두 성공해야만 회원 탈퇴가 성공하게 되고, 하나라도 실패할 경우 Rollback이 됩니다.

이상으로 Nestjs에서 typeorm의 tranaction 처리에 대해 알아보았습니다.

반응형
반응형

이 글은 아래 동영상 강의로 제공됩니다.

 

이번엔 Nestjs로 백엔드 개발을 진행합니다.

 

전체적인 진행 순서는 다음과 같습니다.

  1. kakao developer에서 애플리케이션 생성
  2. 전체 Workflow 이해하기
  3. Nuxtjs(Frontend) 개발
  4. Nestjs(Backend) 개발

 

아래 명령으로 프로젝트를 생성합니다.

nest new kakao-login-back

- package manager : yarn

 

axios package를 추가합니다.

yarn add axios

server port를 3001로 바꿔줍니다.

src/main.ts

await app.listen(3001);

 

src/app.controller에 login api를 만듭니다.

@Post('/login')
  async login(@Body() body: any, @Response() res): Promise<any> {
    try {
      // 카카오 토큰 조회 후 계정 정보 가져오기
      const { code, domain } = body;
      if (!code || !domain) {
        throw new BadRequestException('카카오 정보가 없습니다.');
      }
      const kakao = await this.appService.kakaoLogin({ code, domain });

      console.log(`kakaoUserInfo : ${JSON.stringify(kakao)}`);
      if (!kakao.id) {
        throw new BadRequestException('카카오 정보가 없습니다.');
      }

      res.send({
        user: kakao,
        message: 'success',
      });
    } catch (e) {
      console.log(e);
      throw new UnauthorizedException();
    }
  }

src/app.service.ts에 다음 내용을 추가합니다.

async kakaoLogin(options: { code: string; domain: string }): Promise<any> {
    const { code, domain } = options;
    const kakaoKey = '87073966cb41...';
    const kakaoTokenUrl = 'https://kauth.kakao.com/oauth/token';
    const kakaoUserInfoUrl = 'https://kapi.kakao.com/v2/user/me';
    const body = {
      grant_type: 'authorization_code',
      client_id: kakaoKey,
      redirect_uri: `${domain}/kakao-callback`,
      code,
    };
    const headers = {
      'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
    };
    try {
      const response = await axios({
        method: 'POST',
        url: kakaoTokenUrl,
        timeout: 30000,
        headers,
        data: qs.stringify(body),
      });
      if (response.status === 200) {
        console.log(`kakaoToken : ${JSON.stringify(response.data)}`);
        // Token 을 가져왔을 경우 사용자 정보 조회
        const headerUserInfo = {
          'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
          Authorization: 'Bearer ' + response.data.access_token,
        };
        console.log(`url : ${kakaoTokenUrl}`);
        console.log(`headers : ${JSON.stringify(headerUserInfo)}`);
        const responseUserInfo = await axios({
          method: 'GET',
          url: kakaoUserInfoUrl,
          timeout: 30000,
          headers: headerUserInfo,
        });
        console.log(`responseUserInfo.status : ${responseUserInfo.status}`);
        if (responseUserInfo.status === 200) {
          console.log(
            `kakaoUserInfo : ${JSON.stringify(responseUserInfo.data)}`,
          );
          return responseUserInfo.data;
        } else {
          throw new UnauthorizedException();
        }
      } else {
        throw new UnauthorizedException();
      }
    } catch (error) {
      console.log(error);
      throw new UnauthorizedException();
    }
  }

서버를 실행합니다.

yarn start:dev

브라우저의 콘솔 로그에 사용자 정보가 리턴된것을 확인할 수 있습니다.

이상으로 카카오로그인을 RestAPI를 이용하여 처리하는 방법에 대해 알아보았습니다.

반응형
반응형

이 글은 아래 동영상 강의로 제공됩니다.

 

Frontend 개발은 Nuxtjs를 이용하여 진행하도록 합니다.

 

전체적인 진행 순서는 다음과 같습니다.

  1. kakao developer에서 애플리케이션 생성
  2. 전체 Workflow 이해하기
  3. Nuxtjs(Frontend) 개발
  4. Nestjs(Backend) 개발

 

우선 아래 명령으로 Nuxt 프로젝트를 생성합니다.

npx create-nuxt-app kakao-login-front

- 프로젝트 명 : kakao-login-front

- programing language : Javascript

- Package manager : Yarn

- UI framework : None

- Nuxt.js modules : Axios

- Linting tools : ESLint

- Testing framework : None

- Rendering mode : Single Page App

- Server : Node.js

- Development tools : Enter

- Continuous integration : None

- Version control system : None

 

프로젝트 생성이 완료되면 아래와 같은 메시지가 나옵니다.

에디터에서 프로젝트를 열고, yarn dev를 사용해서 프로젝트를 실행합니다.

브라우저에서 http://localhost:3000번을 입력합니다.

이때 아래와 같은 페이지가 나오면 프로젝트가 정상적으로 만들어진 것입니다.

 

카카오 로그인 기능을 만들기 위해 아래와 같은 버튼이미지가 필요합니다.

아래 사이트에서 다운로드 하실 수 있습니다.

https://developers.kakao.com/tool/resource/login

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

이미지를 static 폴더 아래에 copy합니다.

 

카카오로그인을 사용하기 위해서 kakao.js 파일이 필요합니다.
nuxt.config.js의 'head' 아래에 다음 코드를 추가합니다.

script: [
  {
    src: 'https://developers.kakao.com/sdk/js/kakao.js'
  }
]

 

pages/index.vue 파일에 카카오 로그인 버튼을 추가하고 로그인 method를 작성 합니다.

<template>
  <a @click="loginWithKakao">
    <img src="/kakao_login_large_narrow.png"/>
  </a>
</template>

<script>
export default {
  name: 'IndexPage',
  methods: {
    kakaoInit () {
      Kakao.init('329a6a74...')// KaKao client key
      Kakao.isInitialized()
    },
    async loginWithKakao () {
      await Kakao.Auth.authorize({
        redirectUri: `${window.location.origin}/kakao-callback`
      })
    }
  },
  mounted () {
    this.kakaoInit()
  }
}
</script>

다음과 같이 컴파일 오류가 발생할 수 있습니다.

이 경우 .eslintrc.js 파일을 열고 아래 내용을 추가하면 오류를 해결 할 수 있습니다.

'Kakao' 변수가 선언되지 않았다는 오류이고, 선언되지 않은 변수를 eslint에서 인식할 수 있도록 해 준것입니다.

  globals: {
    Kakao: true
  }

카카오 인증 후 callback 페이지를 아래와 같이 만듭니다.

axios를 이용해 서버API를 호출할 것이기 때문에 nuxt.config.js 다음을 추가합니다.

- baseURL은 서버의 주소를 지정합니다.

  modules: [
    '@nuxtjs/axios'
  ],

  axios: {
    baseURL: 'http://localhost:3001/'
  },

 

pages/kakao-callback.vue

<template>
  <div>
    로그인 중 입니다...
  </div>
</template>

<script>
export default {
  async mounted () {
    try {
      // code 유무 체크
      if (!this.$route.query.code) {
        return this.$router.push('/')
      }
      console.log(`code : ${this.$route.query.code}`)

      // 카카오 로그인 요청
      const body = {
        code: this.$route.query.code,
        domain: window.location.origin
      }
      const response = await this.$axios.$post(
        '/login', body, {}
      )
      console.log(response)
    } catch (error) {
      console.log(error)
    }
  }
}
</script>

브라우저에서 확인을 해봅니다.

확인하고 계속하기를 클릭하면 아래와 같은 화면이 보입니다.

브라우저의 콘솔을 확인해보면 다음과 같이 code 값이 넘어온것을 확인 할 수 있습니다.

이렇게 해서 frontend 작업이 완료되었습니다.

다음시간은 backend 작업을 진행해보겠습니다.

반응형
반응형

이 글은 아래 동영상 강의로 제공됩니다.

 

카카오 로그인(REST API)을 활용하여 웹애플리케이션에서 로그인 기능을 구현하는 것을 배워보도록 하겠습니다.

 

전체적인 진행 순서는 다음과 같습니다.

  1. kakao developer에서 애플리케이션 생성
  2. 전체 Workflow 이해하기
  3. Nuxtjs(Frontend) 개발
  4. Nestjs(Backend) 개발

이번시간은 REST API를 이용한 카카오로그인 전체 Workflow에 대해 설명드립니다. 

1. 서비스 화면에서 카카오로그인 버튼을 클릭합니다. 이때 카카오에서 제공하는 동의 화면이 아래와 같이 나타납니다.

2. 동의를 클릭하면 카카오 인증서버에서 인가코드를 발급해서 우리가 등록한 redirect URI로 전달을 합니다.

 

3. frontend에서 backend로 로그인을 요청합니다.
    URI는 "/auth/login"이고 post로 요청합니다.
    이때 "인가코드"를 파라메터로 전달합니다.

 

4. backend에서 카카오인증 서버로 토큰 발급을 요청합니다.

https://kauth.kakao.com/oauth/token


    이때 인가코드, redirect uri, 애플리케이션Key 등을 파라메터로 전달합니다.

 

5. 정보가 일치할 경우 카카오에서 토큰을 발급하여 backend로 return합니다.

 

6. backend에서 카카오API 서버로 "사용자정보"를 요청합니다.

https://kapi.kakao.com/v2/user/me

7. 사용자 DB에서 해당 사용자를 확인하고 인증 토큰을 발급합니다.

   이때 신규 사용자일 경우 회원가입 처리를 진행한 후 인증 토큰을 발급합니다.

 

8. frontend에서 전달받은 토큰으로 backend로 사용자 정보를 요청합니다.

9. backend에서 토큰을 받아 사용자 정보를 return합니다.

10. frontend에서 사용자 정보 중 이름이 없을 경우 이름을 입력받습니다.

11. frontend에서 입력받은 이름으로 사용자 정보를 update 요청합니다.

12. backend에서 사용자 정보를 update 한 후 변경된 정보를 return 합니다.

 

이상이 전체 로그인을 처리하는 flow입니다.

반응형

+ Recent posts