모든 것이 HTTP

HTTP는 Hyper Text Transfer Protocol의 약자로 Hyper Text인 html을 전송하는 프로토콜로 시작했습니다. 하지만 지금은 아래와 같은 모든 것을 HTTP에 담아서 전송합니다. 

 

HTML, TEXT 뿐만이 아닌 

  • Image, 음성, 영상, 파일
  • JSON, XML (API)
  • 거의 모든 형태의 데이터 전송 가능
  • 서버간에 데이터를 주고 받을 때도 대부분 HTTP 사용

 

HTTP 역사

  • HTTP/0.9 1991년 : GET 메서드만 지원, HTTP 헤더 X
  • HTTP/1.0 1996년 : 메서드, 헤더 추가
  • HTTP/1.1 1997년 : 가장 많이 사용, 우리에게 가장 중요한 버전
    • RFC2068 (1997) -> RFC2616 (1999) -> RFC7230~7235 (2014)
  • HTTP/2 2015년 : 성능 개선
  • HTTP/3 진행중 : TCP 대신 UDP 사용, 성능 개선

 

기반 프로토콜

  • TCP : HTTP/1.1, HTTP/2
  • UDP : HTTP/3 - TCP는 3 way hanshake, 기본적인 데이터가 많은 등의 문제로 속도가 빠른 매커니즘이 아닙니다. 그래서 HTTP/3는 UDP 프로토콜위에 애플리케이션 레벨에서 성능을 최적화하도록 새로 설계해서 나온 것입니다.
  • 현재 HTTP/1.1 주로 사용
    • HTTP/2, HTTP/3 점점 증가

실제 어떤 것들이 HTTP 버전으로 통신이 되고있는지 확인해보겠습니다. 크롬 브라우저에서 F12 (윈도우), alt+command+i (mac)을 누르면 개발자 도구가 나옵니다. Network 탭을 누른 뒤 아래 Name 탭에서 오른쪽 마우스를 눌러 Protocol을 체크해줍니다. 그 후 구글에 들어가 hello라고 검색해보면, 프로토콜에서 h2 혹은 h3라고 나와있을텐데 h2는 HTTP/2, h3는 HTTP/3입니다. 현재 제가 해본 결과 h3를 구글에서는 주로 사용하네요. 네이버도 확인해보니 네이버는 h2를 대부분 사용하고 있었습니다.

 

구글과 네이버 HTTP 버전 확인

 

HTTP 특징

  • 클라이언트 서버 구조
  • 무상태 프로토콜(stateless), 비연결성
  • HTTP 메시지
  • 단순함, 확장 가능

 

 

 

클라이언트 서버 구조

HTTP의 특징 중 하나 입니다. HTTP는 클라이언트가 HTTP 메시지를 통해 서버 요청을 보내고, 클라이언트는 서버에서 응답이 올때까지 기다립니다. 서버가 클라이언트의 요청에 대한 결과를 만들어서 응답을 하게되면 응답 결과를 클라이언트가 받고 열어서 동작하게 됩니다.

 

비즈니스 로직과 데이터 같은 것들은 서버에 밀어 넣고, 클라이언트는 UI/UX에 집중합니다. 이렇게 하게되면 좋은 점은 클라이언트와 서버가 각각 독립적으로 진화를 할 수 있습니다. 예를들어, 클라이언트는 복잡한 비즈니스 로직과 데이터 같은 것들이 없기 때문에 UI/UX에만 집중할 수 있습니다. 반대로 서버는 트래픽 폭주 시에 클라이언트를 손대지 않고 서버를 고도화하고 진화하는 것에만 집중할 수 있습니다.

 

즉, 이렇게 분리를 하는 이유는 클라이언트와 서버 각각 독립적으로 진화를 할 수 있습니다.

 

 

 

Stateful, Stateless

Stateless

  • 서버가 클라이언트의 상태를 보존하지 않는다.

    잘 이해가 가지않을 텐데요. 아래의 예시로 이해해봅시다. 예시는 고객과 점원 사이의 노트북 구매 플로우로 들도록 하겠습니다.

    예를들어, Stateful인 경우는 아래와 같습니다.
    Stateful에서 점원이 중간에 바뀌는 경우는 아래와 같습니다.
    Stateful을 정리하자면 아래와 같습니다.

    Stateless의 경우를 봐봅시다. 이번것만 보면 Stateful과 별로 다를게 없습니다.
    그렇다면, 점원이 바뀌는 경우를 Stateless로 봐봅시다. Stateful과 다르게 점원이 바뀌어도 새로운 점원과 의사소통의 문제가 일어나지 않습니다.
  • 장점 : 서버 확장성 높음 (스케일 아웃 - 서버를 늘립니다. 수평 확장)
  • 단점 : 클라이언트가 추가 데이터 전송

 

Stateful, Stateless 차이

  • 상태 유지 : 중간에 다른 점원으로 바뀌면 안됩니다.
    (중간에 다른 점원으로 바뀔 때 상태 정보를 다른 점원에게 미리 알려줘야 합니다.)
  • 무상태 : 중간에 다른 점원으로 바뀌어도 됩니다.
    • 갑자기 고객이 증가해도 점원을 대거 투입할 수 있습니다.
    • 갑자기 클라이언트 요청이 증가해도 서버를 대거 투입할 수 있습니다. (때문에 확장성이 높습니다.)
  • 무상태는 응답 서버를 쉽게 바꿀 수 있습니다. -> 무한한 서버 증설 가능

 

상태 유지의 경우 위처럼 서버 1이 클라이언트A의 요청 정보를 유지하고 있습니다. 하지만 이렇게 정보를 유지하다가 서버 1이 장애가 발생하면 어떻게 될까요?? 이럴경우 클라이언트A는 기존에 했던 작업들의 정보가 사라지기 때문에 다시 처음부터해야합니다. 

 

하지만 Stateless의 경우는 클라이언트가 요청할 때부터 데이터를 다 담아서 보냅니다. 서버는 응답만하고 상태를 보관하지 않기때문에 서버 1이 장애가 발생해도 중계서버에서 요청을 서버2로 보내서 처리하기만하면 되기때문에 클라이언트A에게 다시 정보를 요청하지 않아도 됩니다. 상태를 유지하지 않기 때문에 서버 확장에 유리합니다.

여기까지만 보면 Stateful은 쓸 이유가 없고 Stateless만 사용하면 되는 것 아닌가라는 생각이 듭니다. 하지만, Stateless에도 한계가 있습니다.

 

Stateless 실무 한계

  • 모든 것을 무상태로 설계할 수 있는 경우도 있고 없는 경우도 있다.
  • 무상태
    • 예) 로그인이 필요 없는 단순한 서비스 소개 화면
  • 상태 유지
    • 예) 로그인
  • 로그인한 사용자의 경우 로그인을 했다는 상태를 서버에 유지
  • 일반적으로 브라우저 쿠키와 서버 세션등을 사용해서 상태 유지
  • 상태 유지는 최소한만 사용
  • 데이터를 너무 많이 보내는 경우가 생길 수 있다.

 

 

 

비연결성(connectionless)

TCP/IP의 경우는 연결을 유지한 상태로 클라이언트와 서버가 데이터를 주고 받습니다. 한대면 상관이 없지만, 여러대가 서버에 연결을 계속 유지하는 경우 클라이언트가 놀고있어도 서버는 연결을 유지해야하기 때문에 자원을 소모하게 되는 단점이 있습니다.

그렇다면 위의 방법대로 요청할때만 연결하여 데이터를 주고받은 뒤, 사용하지 않고있을 때는 연결을 끊는 방법이 있습니다. 이렇게하면 위의 방식의 단점을 보완할 수 있습니다.

 

비 연결성

  • HTTP는 기본이 연결을 유지하지 않는 모델
  • 일반적으로 초 단위 이하의 빠른 속도로 응답
  • 1시간 동안 수천명이 서비스를 사용해도 실제 서버에서 동시에 처리하는 요청은 수십개 이하로 매우 작음
    • 예) 웹 브라우저에서 계속 연속해서 검색 버튼을 누르지는 않는다.
  • 서버 자원을 매우 효율적으로 사용할 수 있음

하지만, 비 연결성에도 단점이 있습니다.

 

비 연결성 한계와 극복

  • TCP/IP 연결을 새로 맺어야 함. 3 way handshake 작업을 연결할 때마다 해야함.
  • 웹 브라우저로 사이트를 요청하면 HTML 뿐만 아니라 자바스크립트, css, 추가 이미지 등등 수 많은 자원이 함께 다운로드
  • 지금은 위의 문제를 HTTP 지속 연결(Persistent Connections)로 문제 해결
  • HTTP/2, HTTP/3(UDP를 사용해서 연결 속도에서부터 시간을 많이 단축시킴)에서 더 많은 최적화

HTTP 초기 -> HTTP 지속 연결

 

Stateless를 기억하자

  • 정말 같은 시간에 딱 맞추어 발생하는 대용량 트래픽
  • 예) 선착순 이벤트, 명절 KTX 예약, 학과 수업 등록
  • 예) 저녁 6:00 선착순 1000명 치킨 할인 이벤트 -> 수만명 동시 요청

위의 문제를 해결하기 위해서는 최대한 Stateless하게 설계하는 것이 중요합니다. Stateless하게 설계되었다면 위의 이벤트가 발생해도 잠깐동안 서버를 늘려서 대응할 수 있기 때문입니다.

 

 

 

HTTP 메시지

HTTP는 요청 메시지와 응답 메시지의 구조가 아래와 같이 다릅니다.

HTTP 메시지의 구조는 아래와 같습니다.

 

시작 라인 - 요청 메시지

  • start-line = request-line / status-line
  • request-line = method SP(공백) request-target(path) SP HTTP-version CRLF(엔터)
  • HTTP method
    • GET : resource를 보내줘
    • POST : resource를 처리해줘
    • DELETE : resource를 삭제해줘
  • 요청 대상 (/search?q=hello&hl=ko)
    • absolute-path[?query] (절대경로[?쿼리])
    • 절대경로 = "/"로 시작하는 경로
  • HTTP Version

시작 라인 - 응답 메시지

  • start-line = request-line / status-line
  • status-line = HTTP-version SP status-code SP reason-phrase CRLF(엔터)
  • HTTP version
  • HTTP 상태 코드 
    • 200 : 성공
    • 400 : 클라이언트 요청 오류
    • 500 : 서버 내부 오류
  • 이유 문구 : 사람이 이해할 수 있는 짧은 상태 코드 설명 글

 

HTTP 헤더

  • header-field = field-name ":" OWS field-value OWS (OWS:띄어쓰기 허용)
  • field-name은 대소문자 구문 없음

HTTP 헤더 용도

  • HTTP 전송에 필요한 모든 부가정보가 다 들어있다. (message body빼고 필요한 메타정보가 다 있습니다.)
  • 예) 메시지 바디의 내용, 메시지 바디의 크기, 압축, 인증, 요청 클라이언트(브라우저) 정보, 서버 애플리케이션 정보, 캐시 관리 정보...
  • 표준 헤더가 너무 많다.
  • 필요시 임의의 헤더 추가 가능
    • 임의의 헤더를 추가하는 경우엔 약속한 클라이언트와 서버만 이해가 가능

 

HTTP 메시지 바디 용도

  • 실제 전송할 데이터
  • HTML 문서, 이미지, 영상, JSON 등등 byte로 표현할 수 있는 모든 데이터 전송 가능

 

 

 

 

출처

 

모든 개발자를 위한 HTTP 웹 기본 지식 - 인프런 | 강의

실무에 꼭 필요한 HTTP 핵심 기능과 올바른 HTTP API 설계 방법을 학습합니다., - 강의 소개 | 인프런...

www.inflearn.com

 

 

 

URI (Uniform Resource Identifier)

URI? URL? URN?

URI는 로케이터(locator), 이름(name) 또는 둘다 추가로 분류될 수 있습니다.

URI라는 리소스를 식별한다는 가장 큰 개념이 있습니다. 사람들을 식별할 때 주민번호로 식별하듯이 자원 자체를 식별하는 방법입니다. 여기에는 URL과 URN으로 크게 두가지가 있습니다. URL은 리소스의 위치, URN은 리소스의 이름입니다. 

 

URL과 URN은 아래와 같이 생겼습니다. 

URL은 흔히 웹 브라우저에 적는 http://www.naver.com과 같은 문자열을 말합니다. URN은 위에서 보듯 진짜 이름을 부여하는 것 입니다. 문제는 이름을 부여하면 거의 찾을 수 없기때문에 URL만 사용합니다.

 

 

URI

  • Uniform : 통일된 방식
  • Resource : 자원, URI로 식별할 수 있는 모든 것(제한 없음)
  • Identifier : 다른 항목과 구분하는데 필요한 정보
  • URL : Uniform Resource Locator
    • 리소스가 있는 위치를 지정
    • 변할 수 있다.
  • URN : Uniform Resource Name
    • 리소스에 이름을 부여
    • 변하지 않는다.
  • URN은 거의 사용하지 않기 때문에, 앞으로 URI를 URL과 같은 의미로 이야기하겠습니다.

 

https://www.google.com/search?q=hello&hl=ko

라는 URL을 웹 브라우저에서 검색해봅시다. 그리고 문법을 분석해보자면 아래와 같습니다.

  • scheme://[userinfo@]host[:port][/path][?query][#fragment]
  • https://www.google.com/search?q=hello&hl=ko
  • 프로토콜(https)
  • 호스트명(www.google.com)
  • 포트 번호(443 - https 포트번호)
  • 패스(/search)
  • 쿼리 파라미터(q=hello&hl=ko)

 

URL - scheme

  • 주로 프로토콜 사용
  • 프로토콜 : 어떤 방식으로 자원에 접근할 것인가 하는 약속 규칙
  • http는 80, https는 443 포트를 주로 사용, 포트는 생략 가능
  • https는 http에 보안 추가 (HTTP Secure)

 

URL - userinfo

  • URL에 사용자 정보를 포함해서 인증
  • 거의 사용하지 않음

 

URL - host

  • 호스트명
  • 도메인명 또는 IP 주소를 직접 사용 가능

 

URL - PORT

  • 포트
  • 접속 포트
  • 일반적으로 생략, 생략시 http는 80, https는 443

 

URL - path

  • 리소스 경로(path), 계층적 구조
  • 예) 
    • /home/file1.jpg
    • /members
    • /members/100, /items/iphone12

 

URL - query

  • key=value 형태
  • ?로 시작, &로 추가 가능 ?keyA=valueA&keyB=valueB
  • query parameter, query string 등으로 불림, 웹 서버에 제공하는 파라미터, 문자 형태

 

URL - fragment

  • scheme://[userinfo@]host[:port][/path][?query][#fragment]
  • https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started.html#getting-started-introducing-spring-boot
  • html 내부 북마크 등에 사용
  • 서버에 전송하는 정보 아님

 

 

 

웹 브라우저 요청 흐름

아래와 같이 웹 브라우저에 아래와 같이 요청한다고 가정합시다.

DNS 서버에서 host 명을 조회한 뒤, IP를 받아옵니다. 그 후 IP와 PORT번호를 기반으로 HTTP 요청 메시지를 생성합니다. 

HTTP 요청 메시지는 아래와 같이 생성이 됩니다.

생성된 HTTP 메시지는 아래와 같이 계층을 통해 서버로 전송합니다.

TCP/IP 계층을 거쳐 완성된 패킷은 아래와 같을 것 입니다.

해당 패킷을 받은 구글 서버는 TCP/IP를 깐 뒤 HTTP 메시지를 확인합니다. 

search로 왔고, qeury string을 읽어 웹 브라우저에서 원하는 응답 메시지를 아래와 같이 만들어 요청한 웹 브라우저에게 전송합니다. 

만든 응답 메시지는 아래와 같이 웹 브라우저로 전송됩니다.

 

 

 

출처

 

모든 개발자를 위한 HTTP 웹 기본 지식 - 인프런 | 강의

실무에 꼭 필요한 HTTP 핵심 기능과 올바른 HTTP API 설계 방법을 학습합니다., - 강의 소개 | 인프런...

www.inflearn.com

 

 

인터넷 통신

클라이언트와 서버는 어떻게 통신할까요? 만약 클라이언트와 서버가 바로 옆에 있다면, 아래와 같은 방식으로 선을 연결하여 통신하면 됩니다.

하지만 위와 같은 방식대로 한다면 하나의 서버에는 클라이언트마다 선으로 하나씩 연결이 되야하고, 멀 경우에는 선을 연결하는 방법은 비효율적 입니다. 이러한 방법으로는 통신을 할 수 없기때문에, 인터넷  망을 사용해서 통신을 합니다. 그런데 인터넷은 단순하지 않습니다. 중간에 해저 광케이블이 있을 수도 있고, 인공위성을 통해서 갈 수도있고, 알 수 없는 수 많은 중간 노드(서버)들을 통해 원하는 목적지까지 보내야합니다. 그렇다면 어떻게 내가 보낸 요청을 정확하게 목적지까지 노드들을 통해 전달할 수 있을까요?? 이것을 이해하기 위해선 IP에 대해서 학습해야 합니다.

 

 

 

IP(인터넷 프로토콜)

한국에서 미국에 있는 친구에게 "Hello, world!"라는 메시지를 정확하게 보내기 위해서는 규칙이 있어야 할 것 입니다. IP 주소는 이러한 정확한 전달의 책임을 갖고있습니다.

일단 정확한 통신을 하기 위해서는 클라이언트, 서버 모두 각각의 IP를 갖고있어야 합니다.

 

IP의 역할은 지정한 IP 주소에 데이터 전달, 패킷(Packet)이라는 통신 단위로 데이터를 전달입니다. 메시지는 그냥 보내는 것이 아닌 IP 패킷이라는 규칙이 있습니다. 

메시지를 보낼 때 전송 데이터에 "Hello, world!"를 추가하기 전에, 자신의 IP와 목적지 IP를 적어야합니다. 만든 패킷에 전송데이터를 추가한 뒤, 인터넷 망에 전송합니다. 인터넷 망에 있는 노드(서버)들은 공통된 규약이 있기때문에 패킷을 확인할 수 있고 목적에 전달이 되도록 다른 노드로 전달하게 됩니다.

목적지에 패킷이 전달이되면 목적지에선 출발지로 전달이 완료되었다는 패킷을 보냅니다. 참고로 인터넷 망은 복잡하기 때문에 보냈을 때 지나갔던 노드들과 받을 때 지나온 노드들은 다를 수 있습니다.

 

하지만, IP 프로토콜에는 아래와 같은 한계가 있습니다.

 

IP 프로토콜의 한계

  • 비연결성
    • 패킷을 받을 대상이 없거나 서비스 불능 상태여도 패킷 전송

  • 비신뢰성
    • 중간에 패킷이 사라지면?
    • 패킷이 순서대로 안오면?

  • 프로그램 구분
    • 같은 IP를 사용하는 서버에서 통신하는 어플리케이션이 둘 이상이면?

IP의 문제를 해결해주는 것이 바로 TCP 프로토콜 입니다.

 

 

 

TCP, UDP

인터넷 프로토콜 스택 4계층

TCP는 IP위에 살짝 올려서 IP의 부족한 부분을 보완해준다고 보시면 됩니다. 만약 채팅 프로그램에서 미국에 있는 친구한테 Hello, world!라는 메시지를 보내게되면 아래와 같이 작동하게 됩니다. 

위의 그림을 보시면 이전에 배웠던 Packet을 씌우기 전 전송계층에서 초록색 무언가를 씌우는 것을 볼 수 있습니다. 그렇다면 TCP 프로토콜에서는 무엇을 씌웠는지 확인해봅시다. 참고로 IP는 패킷이라 부르고, TCP는 세그먼트라고 부릅니다. 

TCP 세그먼트에는 출발지 PORT, 목적지 PORT, 전송 제어, 순서, 검증 정보 등이 들어갑니다. IP에서 해결이 안됬던 부분들을 전송 제어, 순서, 검증 정보 등을 통해 해결한다고 보면 됩니다. 

 

TCP 특징 - 전송 제어 프로토콜 (Transmission Control Protocol)

  • 연결 지향 - TCP 3 way handshake (가상 연결)
  • 데이터 전달 보증 (메시지 전달이 누락되면 알 수 있습니다.)
  • 순서 보장

TCP는 신뢰할 수 있는 프로토콜이며, 현재 대부분 애플리케이션에서 TCP를 사용합니다.

 

연결 지향

3 way handshake

3 way handshake란 TCP/IP 프로토콜로 연결을 하게되면, 클라이언트에서 서버로 SYN 메시지를 보내고 서버에서 받게되면 SYN+ACK 메시지를 보내고 클라이언트에서 서버로 ACK 메시지를 보내며 클라이언트와 서버가 연결을 하게 됩니다. 이를 통해 신뢰성을 보장받을 수 있습니다. 참고로 요즘은 최적화가 되어서 3번째 ACK를 보낼 때 데이터도 같이 보낸다고 합니다.

 

참고로 진짜 연결된 것이 아닌 개념적으로만 연결이 된 것 입니다. 즉, 논리적으로만 연결이 된 것이지 나를 위한 전용 랜선이 만들어 진 것은 아닙니다.

 

 

데이터 전달 보증

데이터를 전송하면 서버에서 데이터를 잘 받았다고 보내주어서 데이터가 잘 전달이 되었다는 것을 알 수 있습니다.

 

 

순서 보장

예를들어, 아래와 같이 1,2,3 패킷으로 나누어서 메시지를 전송했다고 가정합시다. 서버에는 2,3,1 순서로 도착이 되어지게 되면 잘못된 패킷부터 클라이언트에게 다시 전송하라고 요청합니다. 

물론, 서버 측에서 잘못된 데이터를 최적화할 수 있지만, 기본적인 로직은 위의 로직과 같습니다.  이를 통해 순서가 보장됩니다.

 

 

UDP 특징

  • TCP와 같은 계층에 있는 프로토콜입니다.
  • 하얀 도화지에 비유 (기능이 거의 없음)
  • 연결 지향 - TCP 3 way handshake X
  • 데이터 전달 보증 X
  • 순서 보장 X
  • 단순하고 빠름
  • 정리
    • IP와 거의 같습니다. PORT, CHECKSUM 정도만 추가되었습니다.
    • 애플리케이션에서 추가 작업 필요

PORT는 하나의 클라이언트에서는 다양한 작업을 합니다. 웹 서핑, 음악, 카톡 등 이러한 작업들이 받는 패킷들이 정확하게 받을 수 있도록 해줍니다.

CHECKSUM은 메시지에 대해서 제대로 받는지 검증해주는 데이터만 간단하게 추가가 되어 있습니다.

 

UDP는 최근에 각광받고 있습니다. 이유는 웹 브라우저에서 http통신을 할 때, 최근 Http 3가 나왔는데 3-way-handshake를 줄이며 최적화를 해보자라며 UDP를 사용하게되면서 뜨고있다고 합니다.

 

 

 

PORT

만약 지금 우리의 컴퓨터가 아래와 같은 여러 작업을 한다고 가정합시다.

하나의 PC가 여러개의 서버와 통신해야 합니다. 각각의 서버에서 전달한 패킷들이 클라이언트로 올텐데 게임, 화상통화, 웹 요청 중 어떠한 패킷인지 알 수가 없습니다. 이전의 TCP 세그먼트에 출발지 PORT, 목적지 PORT가 있었습니다. TCP/IP를 사용하는 경우 출발지 IP, 도착지 IP가 패킷에 저장되어 있고 출발지 PORT, 목적지 PORT가 TCP 세그먼트에 담겨져 있습니다. IP는 목적지의 서버를 찾는 것이고, 서버안에서 돌아가는 애플리케이션을 구분하는 것은 PORT라고 보면됩니다.

그렇게 되어 TCP/IP 패킷은 아래와 같다고 보시면됩니다. 

 

아래와 같이 PORT를 통해 클라이언트의 각각 애플리케이션이 정상적으로 통신을 할 수 있게되는 것 입니다.

 

PORT 번호

  • 0~65535 할당 가능
  • 0~1023 : 잘 알려진 포트, 사용하지 않는 것이 좋음
    • FTP - 20, 21
    • TELNET - 23
    • HTTP - 80
    • HTTPS - 443

 

 

 

DNS

IP는 기억하기 어렵습니다. 

또한 IP는 변경될 수 있습니다.

그래서 DNS(Domain Name System)이 있습니다. 전화번호부라고 보면 됩니다. DNS는 아래와 같이 도메인 명을 IP주소로 변환해주는 작업을 합니다.

DNS를 통해 기억하기 어려운 IP를 기억할 수 있고, 바뀐 IP도 도메인 명만 알고 있으면 접근할 수 있도록 해줍니다.

 

 

출처

 

모든 개발자를 위한 HTTP 웹 기본 지식 - 인프런 | 강의

실무에 꼭 필요한 HTTP 핵심 기능과 올바른 HTTP API 설계 방법을 학습합니다., - 강의 소개 | 인프런...

www.inflearn.com

 

배포하는 동안 애플리케이션이 종료된다는 문제가 있었습니다. 긴 기간은 아니지만, 새로운 Jar가 실행되기 전까진 기존 Jar를 종료시켜 놓기때문에 서비스가 중단됩니다.

반면 24시간 서비스하는 네이버나 카카오톡 같은 경우 배포하는 동안 서비스가 정지되지는 않습니다. 그럼, 어떻게 서비스 중단 없이 배포를 계속할 수 있는지 배워봅시다.

 

 

 

무중단 배포 소개

예전에는 배포는 팀의 아주 큰 이벤트이기 때문에 다 같이 코드를 합치는 날과 배포를 하는 날을 정하고 진행했습니다. 특히 배포일에는 사용자가 적은 새벽 시간에 개발자들이 모두 남아 배포 준비를 해야만 했고 배포가 잦아질 때는 새벽마다 남아야만 했습니다.

더군다나 배포를 하고 나서 치명적인 문제가 발견되면 어떻게 해야 할까요? 새벽 시간에 부랴부랴 문제를 해결하다가, 사용자 유입이 많아지는 아침이 되면 긴급점검 공지를 올리고 수정해야만 했습니다. 실제로 게임을 할 때 이런공지를 많이 보곤했었습니다.

이런 수고스러움을 덜기 위해 기존 서비스를 정지하지 않고, 배포할 수 있는 방법들을 찾기 시작 했고 이를 무중단 배포라고 합니다.

 

무중단 배포 방식에는 몇 가지가 있습니다.

  • AWS에서 블루 그린(Blue-Green) 무중단 배포
  • 도커를 이용한 웹서비스 무중단 배포

이외에도 L4 스위치를 이용한 무중단 배포 방법도 있지만, L4가 워낙 고가의 장비이다 보니 대형 인터넷 기업 외에는 쓸 일이 거의 없다고합니다.

 

이 책에서는 NGINX를 이용한 무중단 배포를 진행합니다. NGINX는 웹 서버, 리버스 프록시, 캐싱, 로드 밸런싱, 미디어 스트리밍 등을 위한 오픈소스 소프트웨어입니다. 이전에 아파치가 대세였던 자리를 완전히 빼앗은 가장 유명한 웹 서버이자 오픈소스입니다. 고성능 웹 서버이기때문에 대부분 서비스들이 현재는 NGINX를 사용하고 있습니다.

 

NGINX가 가지고 있는 여러 기능 중 리버스 프록시가 있습니다. 리버스 프록시란 NGINX가 외부의 요청을 받아 백엔드 서버로 요청을 전달하는 행위를 이야기합니다. 리버스 프록시 서버(NGINX)는 요청을 전달하고, 실제 요청에 대한 처리는 뒷단의 웹 애플리케이션 서버들이 처리합니다. 

 

우리는 이 리버스 프록시를 통해 무중단 배포 환경을 구축할 예정입니다. NGINX를 이용한 무중단 배포를 하는 이유는 가장 저렴하고 쉽기 때문입니다. 기존에 쓰던 EC2에 그대로 적용하면 되므로 배포를 위해 AWS EC2 인스턴스가 하나 더 필요하지 않습니다. 추가로 이 방식은 꼭 AWS와 같은 클라우드 인프라가 구축되어 있지 않아도 사용할 수 있는 범용ㅇ적인 방법입니다. 즉, 개인 서버 혹은 사내 서버에서도 동일한 방식으로 구축할 수 있으므로 사용처가 많습니다.

 

구조는 간단합니다. 하나의 EC2 혹은 리눅스 서버에 NGINX 1대와 스프링 부트 Jar를 2대 사용하는 것입니다.

  • NGINX는 80(http), 443(https) 포트를 할당합니다.
  • 스프링 부트1은 8081포트로 실행합니다.
  • 스프링 부트2는 8082포트로 실행합니다.

NGINX 무중단 배포 1은 아래와 같은 구조가 됩니다.

NGINX 무중단 배포 1

운영 과정을 아래와 같습니다.

  1. 사용자는 서비스 주소로 접속합니다. (80 or 443 port)
  2. NGINX는 사용자의 요청을 받아 현재 연결된 스프링 부트로 요청을 전달합니다. 
  3. 스프링 부트2는 NGINX와 연결된 상태가 아니니 요청받지 못합니다.

1.1 버전으로 신규 배포가 필요하면, NGINX와 연결되지 않은 스프링 부트2(8082 port)로 배포합니다.

 

NGINX 무중단 배포 2

  1. 배포하는 동안에도 서비스는 중단되지 않습니다. (NGINX는 스프링 부트1을 바라보기 때문)
  2. 배포가 끝나고 정상적으로 스프링 부트2가 구동 중인지 확인합니다.
  3. 스프링 부트2가 정상 구동 중이면 nginx reload 명령어를 통해 8081대신에 8082를 바라보도록합니다.
  4. nginx reload는 0.1초 이내에 완료됩니다.

이후 1.2 버전 배포가 필요하면 이번에는 스프링 부트1로 배포합니다.

 

이렇게 구성하게 되면 전체 시스템 구조는 아래와 같습니다.

 

무중단 배포 전체 구조

기존 구조에서 EC2 내부의 구조만 변경된 것입니다.

 

 

 

NGINX 설치와 스프링 부트 연동하기

먼저 EC2에 NGINX를 설치합시다.

 

  • NGINX 설치

EC2에 접속해서 아래 명령어로 NGINX를 설치합니다.

sudo amazon-linux-extras install nginx1

설치가 완료되었으면 아래 명령어로 nginx를 실행합니다.

sudo service nginx start

아래 로그가 나오면 정상 실행하는 것 입니다.

Redirecting to /bin/systemctl start nginx.service

 

  • 보안 그룹 추가

먼저 nginx의 포트번호를 보안 그룹에 추가하겠습니다. nginx의 포트번호는 기본적으로 80입니다. 해당 포트 번호가 보안 그룹에 없으니 ec2->보안 그룹-> ec2 보안 그룹 선택 -> 인바운트 편집으로 차례로 이동해서 변경해줍니다.

 

  • 리다이렉션 주소 추가

8080이 아닌 80포트로 주소가 변경되니 구글과 네이버 로그인에도 변경된 주소를 등록해야만 합니다. 기존에 등록된 리디렉션 주소에서 8080 부분을 제거하여 추가로 등록합니다.

 

 

추가한 후에는 EC2의 도메인으로 접근하되, 8080 포트를 제거하고 접근해 봅니다. 그럼 NGINX 웹 페이지를 볼 수 있습니다. 이제 스프링 부트와 연동해봅시다.

 

 

  • NGINX와 스프링 부트 연동

NGINX가 현재 실행 중인 스프링 부트 프로젝트를 바라볼 수 있도록 프록시 설정을 하겠습니다. NGINX 설정파일을 아래 코드를 통해 열어봅시다.

sudo vim /etc/nginx/nginx.conf

설정 내용 중 server 아래의 location / 부분을 찾아서 아래와 같이 추가합니다.

   location / {
        proxy_pass http://localhost:8080;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
    }
  • proxy_pass
    • nginx로 요청이오면 http://locahost:8080으로 전달합니다.
  • proxy_set_header XXX
    • 실제 요청 데이터를 header의 각 항목에 할당합니다.
    • 예) proxy_set_header X-Real-IP $remote_addr: Request Header의 X-Real-IP에 요청자의 IP를 저장합니다.

수정이 끝났으면 :wq 명령어로 저장하고 종료한 뒤, 아래 코드로 nginx를 재시작 하겠습니다.

sudo service nginx restart

다시 브라우저로 포트번호 없이 접속하면 스프링 부트 프로젝트가 나오는 것을 볼 수 있습니다.

 

 

 

무중단 배포 스크립트 만들기

무중단 배포 스크립트 작업 전에 API를 하나 추가하겠습니다. 이 API는 이후 배포 시에 8081을 쓸지, 8082를 쓸지 판단하는 기준이 됩니다.

 

profile API 추가

profileController를 만들어 아래와 같이 간단한 API 코드를 추가합니다. web 디렉토리에 생성합니다.

ProfileController 클래스 생성

@RequiredArgsConstructor
@RestController
public class ProfileController {
    private final Environment env;

    @GetMapping("/profile")
    public String profile() {
        List<String> profiles = Arrays.asList(env.getActiveProfiles());
        List<String> realProfiles = Arrays.asList("real", "real1", "real2");
        String defaultProfile = profiles.isEmpty()? "default" : profiles.get(0);

        return profiles.stream()
                .filter(realProfiles::contains)
                .findAny()
                .orElse(defaultProfile);
    }
}
  • evn.getActiveProfiles()
    • 현재 실행 중인 ActiveProfile을 모두 가져옵니다.
    • 즉, real, oauth, real-db 등이 활성화되어 있다면 3개가 모두 담겨있습니다.
    • 여기서 real, real1, real2는 모두 배포에 사용될 profile이라 이 중 하나라도 있으면 그 값을 반환하도록합니다.
    • 실제 이번 무중단 배포에서는 real1과 real2만 사용되지만, step2를 다시 사용해볼 수도있으니 real도 남겨둡니다.

해당 코드가 잘 작동하는지 테스트 코드를 작성해보겠습니다. 해당 컨트롤러는 특별히 스프링 환경이 필요하지는 않습니다. 그래서 @SpringBootTest없이 테스트 코드를 작성합니다. test에 web 디렉토리에 테스트 클래스를 생성해줍시다.

ProfileControllerUnitTest 클래스 생성

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.security.core.parameters.P;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class ProfileControllerUnitTest {

    @Test
    public void real_profile_조회() {
        //given
        String expectedProfile = "real";
        MockEnvironment env = new MockEnvironment();
        env.addActiveProfile(expectedProfile);
        env.addActiveProfile("oauth");
        env.addActiveProfile("real-db");

        ProfileController controller = new ProfileController(env);

        //when
        String profile = controller.profile();

        //then
        assertThat(profile).isEqualTo(expectedProfile);
    }

    @Test
    public void real_profile_없으면_첫번째_조회() {
        //given
        String expectedProfile = "oauth";
        MockEnvironment env = new MockEnvironment();

        env.addActiveProfile(expectedProfile);
        env.addActiveProfile("real-db");

        ProfileController controller = new ProfileController(env);

        //when
        String profile = controller.profile();

        //then
        assertThat(profile).isEqualTo(expectedProfile);
    }

    @Test
    public void active_profile_없으면_default_조회() {
        //given
        String expectedProfile = "default";
        MockEnvironment env = new MockEnvironment();
        ProfileController controller = new ProfileController(env);

        //when
        String profile = controller.profile();

        //then
        assertThat(profile).isEqualTo(expectedProfile);
    }
}

 

Security.config에 인증없이 호출될 수 있게 andMatcher에 /profile 추가

@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomOAuth2UserService customOauth2UserService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().headers().frameOptions().disable()
                .and()
                    .authorizeRequests()
                    .antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**", "/profile").permitAll()
                    .antMatchers("/api/v1/**").hasRole(Role.USER.name())
                    .anyRequest().authenticated()
                .and()
                    .logout()
                    .logoutSuccessUrl("/")
                .and()
                    .oauth2Login()
                    .userInfoEndpoint()
                    .userService(customOauth2UserService);
    }
}

그리고 SecurityConfig 설정이 잘 되었는지도 테스트 코드로 검증합니다. 이 검증은 스프링 시큐리티 설정을 불러와야 하니 @SpringBootTest를 사용하는 테스트 클래스를 하나 더 추가합니다.

ProfileControllerTest 클래스 생성

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ProfileControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void profile은_인증없이_호출된다() throws Exception {
        String expected = "default";

        ResponseEntity<String> response = restTemplate.getForEntity("/profile", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).isEqualTo(expected);
    }
}

여기까지 모든 테스트가 성공했다면 깃허브로 푸시하여 배포합니다. url/profile로 접속해서 profile이 잘 나오는지 확인합니다.

 

 

real1, real2 profile 생성

현재 EC2 환경에서 실행되는 profile은 real밖에 없습니다. 해당 profile은 Travis CI 배포 자동화를 위한 profile이니 무중단 배포를 위한 profile 2개를 src/main/resources 아래에 추가합시다.

application-real1.properties 생성

server.port=8081
spring.profiles.include=oauth,real-db
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.session.store-type=jdbc

application-real2.properties 생성

server.port=8082
spring.profiles.include=oauth,real-db
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.session.store-type=jdbc

2개의 profile은 real profile과 크게 다른 점은 없지만 한 가지가 다릅니다. server.port가 8080가 아닌 8081/8082로 되어있습니다. 이제 깃에 푸시해줍시다.

 

 

NGINX 설정 수정

무중단 배포의 핵심은 NGINX 설정입니다. 배포 때마다 nginx의 프록시 설정(스프링 부트로 요청을 흘려보내는)이 순식간에 교체됩니다. 여기서 프록시 설정이 교체될 수 있도록 설정을 추가하겠습니다.

 

nginx 설정이 모여있는 /etc/nginx/conf.d/에 service-url.inc라는 파일을 아래 명령어로 하나생성합니다.

sudo vim /etc/nginx/conf.d/service-url.inc

그리고 다음 코드를 입력합니다.

set $service_url http://127.0.0.1:8080;

저장하고 종료한 뒤, 해당 파일은 nginx가 사용할 수 있게 설정합니다. 아래와 같이 nginx.conf 파일을 열어줍니다.

sudo vim /etc/nginx/nginx.conf

location / 부분을 찾아 아래와 같이 변경해줍니다.

저장하고 종료한 뒤 재시작합니다.

sudo service nginx restart

다시 브라우저에서 정상적으로 호출되는지 확인합니다. 확인되었다면 nginx 설정까지 잘 된 것 입니다.

 

 

배포 스크립트들 작성

먼저 step2와 중복되지 않기 위해 EC2에 step3 디렉토리를 생성합니다.

mkdir ~/app/step3 && mkdir ~/app/step3/zip

무중단 배포는 앞으로 step3를 사용하겠습니다. 그래서 appspec.yml 역시 step3로 배포되도록 수정합니다.

version: 0.0 # CodeDeploy 버전을 명시한다.
os: linux
files:
  - source:  / # 전체 파일을 나타낸다.
    destination: /home/ec2-user/app/step3/zip/ # source로 지정된 전체 파일이 받을 위치이다.
    overwrite: yes # 기존 파일들이 있으면 덮어 쓸지를 결정한다.

무중단 배포를 진행할 스크립트들은 총 5개 입니다.

  • stop.sh : 기존 nginx에 연결되어 있진 않지만, 실행 중이던 스프링 부트 종료
  • start.sh : 배포할 신규 버전 스프링 부트 프로젝트를 stop.sh로 종료한 'profile'로 실행
  • health.sh : 'start.sh'로 실행시킨 프로젝트가 정상적으로 실행됐는지 체크
  • switch.sh : nginx가 바라보는 스프링 부트를 최신 버전으로 변경
  • profile.sh : 앞선 4개 스크립트 파일에서 공용으로 사용할 'profile'와 포트 체크 로직

appspec.yml에 앞선 스크립트를 사용하도록 설정합니다.

hooks:
  AfterInstall:
    - location: stop.sh # 엔진엑스와 연결되어 있지 않은 스프링 부트를 종료합니다.
      timeout: 60
      runas: ec2-user
  ApplicationStart:
    - location: start.sh # 엔진엑스와 연결되어 있지 않은 Port로 새 버전의 스프링 부트를 시작합니다.
      timeout: 60
      runas: ec2-user
  ValidateService:
    - location: health.sh # 새 스프링 부트가 정상적으로 실행됐는지 확인 합니다.
      timeout: 60
      runas: ec2-user

Jar 파일이 복사된 이후부터 차례로 앞선 스크립트들이 실행된다고 보면 됩니다. 다음은 각 스크립트 입니다. 이 스크립트들 역시 scripts 디렉토리에 추가합니다.

health.sh 생성

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh
source ${ABSDIR}/switch.sh

IDLE_PORT=$(find_idle_port)

echo "> Health Check Start!"
echo "> IDLE_PORT: $IDLE_PORT"
echo "> curl -s http://localhost:$IDLE_PORT/profile "
sleep 10

for RETRY_COUNT in {1..10}
do
  RESPONSE=$(curl -s http://localhost:${IDLE_PORT}/profile)
  UP_COUNT=$(echo ${RESPONSE} | grep 'real' | wc -l)

  if [ ${UP_COUNT} -ge 1 ]
  then # $up_count >= 1 ("real" 문자열이 있는지 검증)
      echo "> Health check 성공"
      switch_proxy
      break
  else
      echo "> Health check의 응답을 알 수 없거나 혹은 실행 상태가 아닙니다."
      echo "> Health check: ${RESPONSE}"
  fi

  if [ ${RETRY_COUNT} -eq 10 ]
  then
    echo "> Health check 실패. "
    echo "> 엔진엑스에 연결하지 않고 배포를 종료합니다."
    exit 1
  fi

  echo "> Health check 연결 실패. 재시도..."
  sleep 10
done
  1. 엔진엑스와 연결되지 않은 포트로 스프링 부트가 잘 수행되었는지 체크
  2. 정상 확인 후 엔진엑스 프록시 설정 변경(switch_proxy)
  3. 엔진엑스 프록시 설정 변경은 switch.sh에서 수행

 

profile.sh 생성

#!/usr/bin/env bash

# bash는 return value가 안되니 *제일 마지막줄에 echo로 해서 결과 출력*후, 클라이언트에서 값을 사용한다

# 쉬고 있는 profile 찾기: real1이 사용중이면 real2가 쉬고 있고, 반대면 real1이 쉬고 있음
function find_idle_profile()
{
    RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/profile)

    if [ ${RESPONSE_CODE} -ge 400 ] # 400 보다 크면 (즉, 40x/50x 에러 모두 포함)
    then
        CURRENT_PROFILE=real2
    else
        CURRENT_PROFILE=$(curl -s http://localhost/profile)
    fi

    if [ ${CURRENT_PROFILE} == real1 ]
    then
      IDLE_PROFILE=real2
    else
      IDLE_PROFILE=real1
    fi

    echo "${IDLE_PROFILE}"
}

# 쉬고 있는 profile의 port 찾기
function find_idle_port()
{
    IDLE_PROFILE=$(find_idle_profile)

    if [ ${IDLE_PROFILE} == real1 ]
    then
      echo "8081"
    else
      echo "8082"
    fi
}
  • $(curl -s -o /dev/null -w "%{http_code}"http://localhost/profile)
    • 현재 엔진엑스가 바라보고 있는 스프링 부트가 정상적으로 수행 중인지 확인
    • 응답값을 HttpStatus로 받습니다.
    • 정상이면 200, 오류가 발생한다면 400~503 사이로 발생하니 400 이상은 모두 예외로 보고 real2를 현재 profile로 사용
  • IDLE_PROFILE
    • 엔진엑스와 연결되지 않은 profile
    • 스프링 부트 프로젝트를 이 profile로 연결하기 위해 반환
  • echo "${IDLE_PROFILE}"
    • bash라는 스크립트는 값을 반환하는 기능이 없습니다.
    • 그래서 제일 마지막 줄에 echo로 결과를 출력 후, 클라이언트에서 그값을 잡아서 $(find_idle_profile) 사용합니다.
    • 중간에 echo를 사용해선 안됩니다.

 

start.sh 생성

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh

REPOSITORY=/home/ec2-user/app/step3
PROJECT_NAME=SpringBootWebService

echo "> Build 파일 복사"
echo "> cp $REPOSITORY/zip/*.jar $REPOSITORY/"

cp $REPOSITORY/zip/*.jar $REPOSITORY/

echo "> 새 어플리케이션 배포"
JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)

echo "> JAR Name: $JAR_NAME"

echo "> $JAR_NAME 에 실행권한 추가"

chmod +x $JAR_NAME

echo "> $JAR_NAME 실행"

IDLE_PROFILE=$(find_idle_profile)

echo "> $JAR_NAME 를 profile=$IDLE_PROFILE 로 실행합니다."
nohup java -jar \
    -Dspring.config.location=classpath:/application.properties,classpath:/application-$IDLE_PROFILE.properties,/home/ec2-user/app/application-oauth.properties,/home/ec2-user/app/application-real-db.properties \
    -Dspring.profiles.active=$IDLE_PROFILE \
    $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &

prfile.sh를 통해 IDLE_PROFILE를 가져온다.

 

stop.sh 생성

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh

IDLE_PORT=$(find_idle_port)

echo "> $IDLE_PORT 에서 구동중인 애플리케이션 pid 확인"
IDLE_PID=$(lsof -ti tcp:${IDLE_PORT})

if [ -z ${IDLE_PID} ]
then
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
  echo "> kill -15 $IDLE_PID"
  kill -15 ${IDLE_PID}
  sleep 5
fi
  • ABSDIR=$(dirname $ABSPATH)
    • 현재 stop.sh가 속해 있는 경로 찾기
  • source ${ABSDIR}/profile.sh
    • 자바로 보면 일종의 import 구문
    • 해당 코드로 인해 stop.sh에서도 profile.sh의 여러 function을 사용할 수 있게 됩니다.

 

switch.sh 생성

#!/usr/bin/env bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh

function switch_proxy() {
    IDLE_PORT=$(find_idle_port)

    echo "> 전환할 Port: $IDLE_PORT"
    echo "> Port 전환"
    echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc

    echo "> 엔진엑스 Reload"
    sudo service nginx reload
}
  • echo "set $service_url http://127.0.0.1:${IDLE_PORT};"
    • 하나의 문장을 만들어 파이프라인(|)으로 넘겨주기 위해 echo를 사용
    • 엔진엑스가 변경할 프록시 주소를 생성
  • sudo tee /etc/nginx/conf.d/service-url.inc
    • 앞에서 넘겨준 문장을 service-url.inc에 덮어 씁니다.
  • sudo service nginx reload
    • 엔진엑스 설정을 다시 불러온다.
    • restart는 잠시 끊기는 현상이 있지만, reload는 끊김 없이 다시 불러온다.

이제 실제로 무중단 배포를 진행해 봅시다.

 

 

 

무중단 배포 테스트

build.gradle

version '1.0.1-SNAPSHOT-'+new Date().format("yyyyMMddHHmmss")
  1. build.gradle은 Groovy 기반의 빌드 툴입니다.
  2. 당연히 Groovy 언어의 여러 문법을 사용할 수 있는데, 여기서는 new Date()로 빌드할 때마다 그 시간이 버전에 추가되도록 구성하였습니다.

여기까지 구성한 뒤 최종 코드를 깃허브로 푸시합니다. 배포가 자동으로 진행되면 CodeDeploy 로그로 잘 진행되는지 확인해 봅시다.

tail -f /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log

위와 같은 메시지가 차례대로 출력되면 성공입니다.

한 번 더 배포하면 그때는 real2로 배포됩니다. 이 과정에서 브라우저 새로고침을 해보면 전혀 중단 없는 것을 확인할 수 있습니다. 2번 배포를 진행한 뒤에 아래와 같이 자바 애플리케이션 실행 여부를 확인해봅시다.

ps -ef | grep java

아래와 같이 2개의 애플리케이션이 실행되고 있음을 알 수 있습니다.

이제 해당 시스템은 마스터 브랜치에 푸시가 발생하면 자동으로 서버 배포가 진행되고 서버 중단 역시 전혀 없는 시스템이 되었습니다. 

 

 

 

 

 

 

출처

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - YES24

가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현한다. JPA와 JUnit 테스트, 그레이들, 머스테치, 스프링

www.yes24.com

 

 

24시간 365일 운영되는 서비스에 배포 환경 구축은 필수 과제 중 하나입니다. 여러 개발자의 코드가 실시간으로 병합되고, 테스트가 수행되는 환경, master 브랜치가 푸시되면 배포가 자동으로 이루어지는 환경을 구축하지 않으면 실수할 여지가 너무나 많습니다. 이번엔 이런 배포 환경을 구성해 보겠습니다!

 

 

CI & CD 소개

저는 개인적으로 CI(Continuous Integration 지속적인 통합)와 CD(Continuous Deployment(지속적인 배포)가 되게 생소합니다. 그래서 간략하게 정리해보았습니다. CI란 코드 버전 관리를 하는 VCS 시스템(Git 등)에 PUSH가 되면 자동으로 테스트와 빌드가 수행되어 안정적인 배포 파일을 만드는 과정을 말합니다. CD란 CI의 빌드 결과를 자동으로 운영 서버에 무중단 배포까지 진행되는 과정을 말합니다. 

 

현대의 웹 서비스 개발에서는 하나의 프로젝트를 여러 개발자가 함께 개발을 진행합니다. 그러다 보니 각자가 개발한 코드가 합쳐야 할 때마다 큰 일이었습니다. 그래서 매주 병합일(코드 Merge만 하는 날)을 정하여 이날은 각자가 개발한 코드를 합치는 일만 진행했다고 합니다.

 

이런 수작업 때문에 생산성이 좋을 수가 없었으며 개발자들은 지속해서 코드가 통합되는 환경인 CI를 구축하게 되었다고 합니다. 개발자 각자가 원격 저장소(Git)로 푸시가 될 때마다 코드를 병합하고, 테스트 코드와 빌드를 수행하면서 자동으로 코드가 통합되어 더는 수동으로 코드를 통합할 필요가 없어지면서 자연스럽게 개발자들 역시 개발에만 집중할 수 있게 되었습니다.

 

CD 역시 마찬가지입니다. 한 두 대의 서버에 개발자가 수동으로 배포를 할 수 있지만, 수십 대 수백 대의 서버에 배포를 해야 하거나 긴박하게 당장 배포를 해야 하는 상황이 오면 더는 수동으로 배포할 수가

없습니다. 그래서 이 역시 자동화하게 되었고, 개발자들이 개발에만 집중할 수 있게 되었습니다.

 

CI와 CI 모두 등장 배경은 개발자들이 개발에만 집중할 수 있도록 하기위해 등장했다고 보면 좋을 것 같습니다.

 

여기서 주의할 점은 단순히 CI 도구를 도입했다고 해서 CI를 하고 있는 것은 아닙니다. 마틴 파울러의 블로그(http://bit.ly/2Yv0vFp)를 참고해보면 CI에 대해 아래와 같은 4가지 규칙을 이야기합니다.

  • 모든 소스 코드가 살아있고(현재 실행되고) 누구든 현재의 소스에 접근할 수 있는 단일 지점을 유지할 것
  • 빌드 프로세스를 자동화해서 누구든 소스로부터 시스템을 빌드하는 단일 명령어를 사용할 수 있게 할 것
  • 테스팅을 자동화해서 단일 명령어로 언제든지 시스템에 대한 건전한 테스트 수트를 실행할 수 있게 할 것
  • 누구나 현재 실행 파일을 얻으면 지금까지 가장 완전한 실행 파일을 얻었다는 확신을 하게 할 것

 특히 중요한 것은 테스팅 자동화입니다. 지속적으로 통합하기 위해서는 무엇보다 프로젝트가 완전한 상태임을 보장하기 위해 테스트 코드가 구현되어 있어야만 합니다.

 

 

 

Travis CI 연동하기

Travis CI는 Github에서 제공하는 무료 CI 서비스입니다. 젠킨스와 같은 CI 도구도 있지만, 젠킨스는 설치형이기 때문에 이를 위한 EC2 인스턴스가 하나 더 필요합니다. 이제 시작하는 서비스에서 배포를 위한 EC2 인스턴스는 부담스럽기 때문에 오픈소스 웹 서비스인 Travis CI를 사용하겠습니다.

 

 

Travis CI 웹 서비스 설정

https://travis-ci.com/에서 깃허브 계정으로 로그인을 한 뒤, 오른쪽 위에 계정명->setting을 클릭합니다. 설정 페이지 아래쪽에 깃허브 저장소 검색창에 저장소 이름을 입력해서 찾은 다음 클릭해줍시다. 그 후 build history 페이지를 눌러줍시다. Travis CI 웹사이트에서 설정은 이것이 끝입니다. 상세한 설정을 프로젝트의 yml 파일로 해야합니다.

 

 

프로젝트 설정

Travis CI의 상세한 설정은 프로젝트에 존재하는 .travis.yml 파일로 할 수 있습니다. yml 확장자는 YAML으로 JSON에서 괄호를 제거한 것 이라고 보면 편합니다. Travis CI 설정을 YAML을 통해서 하고 있습니다. 

 

프로젝트의 build.gradle과 같은 위치에서 .travis.yml을 생성한 후 아래의 코드를 추가해줍니다.

language: java
jdk:
  - openjdk8

branches:
  only:
    - main

# Travis CI 서버의 Home
cache:
  directories:
    - '$HOME/.m2/repository'
    - '$HOME/.gradle'

before_install:
  - chmod +x gradlew

script: "./gradlew clean build"

# CI 실행 완료 시 메일로 알람
notifications:
  email:
    recipients:
      - 본인 메일 주소
  • branches
    • Travis CI를 어느 브랜치가 푸시될 때 수행할지 지정합니다.
    • 현재 옵션은 오직 master 브랜치에 push될 때만 수행합니다.
  • cache
    • 그레이들을 통해 의존성을 받게되면 이를 해당 디렉토리에 캐시하여, 같은 의존성은 다음 배포 때부터 다시 받지 않도록 설정합니다.
  • script
    • master 브랜치에 푸시되었을 때 수행하는 명령어입니다.
    • 여기서는 프로젝트 내부에 둔 gradlew을 통해 clean & build를 수행합니다.
  • notifications
    • Travis CI 실행 완료 시 자동으로 알림이 가도록 설정합니다.

여기까지 마친 뒤, master 브랜치에 커밋과 푸시를 하고, Travis CI 저장소 페이지를 확인해봅시다. 여기서 바로 확인이 되지않는데, 시간을 두고 다시 보시면 바뀐다고 합니다. 하지만 저는 아래와 같은 에러가 발생했습니다.

찾아보니 비자카드를 등록하고 무료 플랜을 가입해야했습니다. setting에 Plan에서 Trial Plan이라는 무료 플랜을 등록했습니다. 이후에도 잘 되지않아서 찾아보니 branch를 master -> main으로 바꿔주어야 했습니다. (git branch 기본이 master -> main 으로 바뀜)

정상 작동

적어놓은 메일로도 정상작동 된다는 메일이 온 것을 볼 수 있을 것 입니다.

 

 

 

Travis CI와 AWS S3 연동하기

S3란 AWS에서 제공하는 일종의 파일 서버입니다. 이미지 파일을 비롯한 정적 파일들을 관리하거나 지금 진행하는 것처럼 배포 파일들을 관리하는 등의 기능을 지원합니다. 보통 이미지 업로드를 구현한다면 S3를 이용하여 구현하는 경우가 많습니다. S3를 비롯한 AWS 서비스와 Travis CI를 연동하게 되면 전체 구조는 아래와 같습니다.

 

Travis CI 연동시 구조

Travis CI와 S3를 연동해봅시다. 실제 배포는 AWS CodeDeploy라는 서비스를 이용합니다. S3 연동이 먼저 필요한 이유는 Jar 파일을 전달하기 위해서 입니다.

CodeDeploy는 저장 기능이 없습니다. 그래서 Travis CI가 빌드한 결과물을 받아서 CodeDeploy가 가져갈 수 있도록 보관할 수 있는 공간이 필요합니다. 보통 이럴 때 S3를 사용합니다.

 

 

AWS Key 발급

일반적으로 AWS 서비스에 외부 서비스가 접근할 수 없습니다. 그러므로 접근 가능한 권한을 가진 Key를 생성해서 사용해야 합니다. AWS에서는 이러한 인증과 관련된 기능을 제공하는 서비스로 IAM(Identity And AccessManagement)이 있습니다.

 

IAM은 AWS에서 제공하는 서비스의 접근 방식과 권한을 관리합니다. IAM을 통해 Travis CI가 AWS S3와 CodeDeploy에 접근할 수 있도록 하겠습니다. AWS 웹 콘솔에서 IAM을 검색하여 이동합니다.

  1. IAM 페이지 왼쪽 사이드바에서 사용자->사용자 추가 버튼을 차례로 클릭해줍시다.
  2. 사용자 이름과 엑세스 유형은 프로그래밍 방식을 선택한 뒤 다음버튼을 눌러줍시다. 기존 정책 직접 연결을 선택합니다. 아래 정책 검색 창에서 s3full과 codedeployfull을 검색한 뒤 둘다 체크해줍니다.
  3. 태그는 Name 값을 지정하는데, 본인이 인지 가능한 정도의 이름으로 만들어줍시다.
  4. 마지막으로 본인이 생성한 권한 설정 항목을 확인합니다.

최종 생성 완료되면 엑세스 키와 비밀 엑세스 키가 생성 됩니다. 이 두 값이 Travis CI에서 사용될 키 입니다. 이제 이 키를 Travis CI에 등록해줍시다.

 

 

Travis CI에 키 등록

Travis CI 설정 화면으로 이동합니다. 프로젝트 좌측 More Option에서 Setting을 눌러주면됩니다. 아래 Environment Variables 항목이 있습니다. 아래처럼 변수명으로 하여 엑세스 키값과 시크릿 키 값을 추가해줍니다.

이렇게 등록된 값은 .travis.yml에서 $AWS_ACCESS_KEY, $AWS_SECRET_KEY란 이름으로 이용할 수 있습니다. 이제 해당 키를 사용해서 Jar를 관리할 S3 버킷을 생성해줍시다.

 

 

S3 버킷 생성

파일 서버의 역할을 하기 때문에, Travis CI에서 생성된 Build 파일을 저장하도록 구성하겠습니다. S3에 저장된 Build 파일은 이후 AWS의 CodeDeploy에서 배포할 파일로 가져가도록 구성할 예정입니다. AWS 웹에서 S3를 검색하여 이동해줍니다.

  1. 버킷 만들기를 눌러줍니다. 
  2. 원하는 버킷명을 작성합니다. 이 버킷에 배포할 Zip 파일이 모여있는 장소임을 의미하도록 짓는 것을 추천합니다.
  3. 다음으로 버전관리를 설정하는데, 별다른 설정을 할 것이 없어서 바로 넘어갑니다.

S3가 생성되었으니 이제 해당 S3로 배포 파일을 전달해봅시다.

 

 

.travis.yml 추가

travis CI에 빌드하여 만든 Jar 파일을 S3에 올릴 수 있도록 .travis.yml에 아래 코드를 추가해줍시다.

...
before_deploy:
  - zip -r SpringBootWebService ./*
  - mkdir -p deploy
  - mv SpringBootWebService.zip deploy/SpringBootWebService.zip

deploy:
  - provider: s3
    access_key_id: $AWS_ACCESS_KEY
    secret_access_key: $AWS_SECRET_KEY

    bucket: qazyj-springboot-build #S3 버킷 이름
    region: ap-northeast-2
    skip_cleanup: true
    acl: private #zip 파일 접근 private으로 
    local_dir: deploy #before_deploy에서 생성한 디렉토리
    wait_until_deployed : true    
...

전체 코드는 아래와 같습니다.

language: java
jdk:
  - openjdk8
branches:
  only:
    - main

# Travis CI Server's Home
cache:
  - directories:
      - '$HOME/.m2/repository'
      - '$HOME/.gradle'

script: "./gradlew clean build"

before_deploy:
  - zip -r SpringBootWebService ./*
  - mkdir -p deploy
  - mv SpringBootWebService.zip deploy/SpringBootWebService.zip

deploy:
  - provider: s3
    access_key_id: $AWS_ACCESS_KEY
    secret_access_key: $AWS_SECRET_KEY

    bucket: qazyj-springboot-build #S3 버킷 이름
    region: ap-northeast-2
    skip_cleanup: true
    acl: private #zip 파일 접근 private으로 
    local_dir: deploy #before_deploy에서 생성한 디렉토리
    wait_until_deployed : true

# CI 실행 완료 시 메일로 알람
notifications:
  email:
    recipients:
      - qazyj@naver.com
  • before_deploy
    • deploy 명령어가 실행되기 전에 수행됩니다.
    • CodeDeploy는 Jar 파일을 인식하지 못하므로 Jar+기타 설정 파일들을 모아 압축(zip)합니다.
  • zip -r SpringBootWebService
    • 현재 위치의 모든 파일을 springboot2-webservice 이름으로 압축합니다.
    • 명령어의 마지막 위치는 본인의 프로젝트 이름이어야합니다.
  • mkdir -p deploy
    • deploy라는 디렉토리를 Travis CI가 실행중인 위치에서 생성합니다.
  • mv SpringBootWebService.zip deploy/SpringBootWebService.zip
    • SpringBootWebService.zip 파일을 deploy/SpringBootWebService.zip 으로 이동시킵니다.
  • deploy
    • S3로 파일 업로드 혹은 CodeDeploy로 배포 등 외부 서비스와 연동될 행위들을 선언합니다.
  • local_dir: deploy
    • 앞에 생성한 deploy 디렉토리를 지정합니다.
    • 해당 위치의 파일들만 S3로 전송합니다.

설정이 다 되었으면 푸쉬합니다. 그리고 S3 버킷을 가보면 업로드가 성공한 것을 확인할 수 있습니다.

이제 CodeDeploy로 배포까지 완료해봅시다.

 

 

 

Travis CI와 AWS S3, CodeDeploy 연동하기

AWS의 배포 시스템인 CodeDeploy를 이용하기 전에 배포 대상인 EC2가 CodeDeploy를 연동 받을 수 있게 IAM 역할을 하나 생성해주겠습니다.

 

 

EC2에 IAM 역할 추가하기

S3와 마찬가지로 IAM을 검색해줍시다.

  1. 역할 탭을 클릭해서 이동합니다.
  2. 역할 만들기 버튼을 클릭합니다.
  3. EC2에서 사용할 것이기 때문에 AWS 서비스를 누른뒤, EC2를 눌러줍니다. 다음 버튼을 누릅시다.
  4. 정책에선 EC2RoleForA를 검색해서 AmazonEC2RoleforAWSCodeDeploy를 선택합니다. 다음 버튼을 누릅시다.
  5. 태그틑 본인이 원하는 이름으로 짓습니다. 다음 버튼을 누릅시다.
  6. 최종적으로 확인한 뒤, 만들어줍니다.

이렇게 만든 역할을 EC2 서비스에 등록하겠습니다. EC2 인스턴스 목록으로 이동한 뒤, 본인의 인스턴스를 마우스 오른쪽 버튼으로 눌러 보안 -> IAM 역할 수정을 선택해줍니다. IAM 역할에 방금 만든 역할을 추가해줍니다. 그 후 EC2 인스턴스를 재부팅 합니다. 재부팅을 해야만 역할이 정상적으로 적용되니 재부팅 해줍시다!

 

재부팅이 완료되었으면 CodeDeploy의 요청을 받을 수 있도록 에이전트를 하나 설치해줍시다.

 

 

CodeDeploy 에이전트 설치

ssh로 EC2에 접속해서 아래의 명령어를 입력해줍시다.

aws s3 cp s3://aws-codedeploy-ap-northeast-2/latest/install . --region ap-northeast-2

내려받기가 성공했다면 아래와 같은 메시지가 콘솔에 출력됩니다.

download: s3://aws-codedeploy-ap-northeast-2/latest/install to ./install

install 파일에 실행 권한이 없으니 실행 권한을 추가합니다.

chmod +x ./install

install 파일로 설치를 진행합니다.

sudo ./install auto

위의 코드가 안되면 아래의 코드처럼 루비를 설치한 뒤 위의 코드를 적어줍시다.

sudo yum install ruby

설치가 끝났으면 Agent가 정상적으로 실행되고 있는지 상태 검사를 해줍시다.

sudo service codedeploy-agent status

아래와 같이 running 메시지가 출력된다면 정상적인 상태입니다.

The AWS CodeDeploy agent is running as PID 4573

 

 

CodeDeploy를 위한 권한 생성

CodeDeploy에서 EC2에 접근하려면 마찬가지로 권한이 필요합니다. AWS의 서비스이니 IAM 역할을 생성합니다. EC2에 IAM 역할을 만들어 준 것처럼 CodeDeploy를 선택해줍니다. CodeDeploy는 역할이 하나 뿐이라서 선택 없이 바로 다음으로 넘어가면 됩니다. 

 

 

CodeDeploy 생성

CodeDeploy는 AWS의 배포 삼형제 중 하나입니다. 배포 삼형제에 대해 간단하게 소개하자면 아래와 같습니다.

  • Code Commit
    • 깃허브와 같은 코드 저장소의 역할을 합니다.
    • 프라이빗 기능을 지원한다는 강점이 있지만, 현재 깃허브에서 무료로 프라이빗 지원을 하고 있어서 거의 사용되지 않습니다.
  • Code Build
    • Travis CI와 마찬가지로 빌드용 서비스입니다.
    • 멀티 모듈을 배포해야 하는 경우 사용해 불편하지만, 규모가 있는 서비스에서는 대부분 젠킨스/팀시티 등을 이용하니 이것 역시 사용할 일이 거의 없습니다.
  • CodeDeploy
    • AWS의 배포 서비스입니다.
    • 앞에서 언급한 다른 서비들은 대체재가 있고, 딱히 대체재보다 나은 점이 없지만, CodeDeploy는 대체재가 없습니다.
    • 오토 스케일링 그룹 배포, 블루 그린 배포, 롤링 배포, EC2 단독 배포 등 많은 기능을 지원합니다.

이 중 현재 진행 중인 프로젝트에서는 Code Commit의 역할은 깃 허브가, Code Build의 역할은 Travis CI가 하고 있습니다. CodeDeploy 서비스로 이동해서 화면 중앙에 있는 애플리케이션 생성 버튼을 클릭해줍시다. 이름은 원하시는 이름으로, 컴퓨팅은 아래와 같이 EC2/온프레미스를 선택해줍시다.

생성이 완료되면 배포 그룹을 생성하라는 메시지를 볼 수 있습니다. 화면 중앙의 배포 그룹 생성 버튼을 클릭합니다. 이름은 원하시는 이름으로 해주시고, 서비스 역할 선택에서 이전에 만들어 주었던 codedeploy IAM 역할을 선택해 줍시다. 배포 유형은 현재 위치를 선택합니다. 환경 구성에서는 Amazon EC2 인스턴스를 체크합니다. 태그 그룹은 아래와 같이 해줍니다.

마지막으로 아래와 같이 배포 구성을 선택하고 로드밸런싱은 체크해제합니다.

이제 배포 그룹까지 생성되었다면 CodeDeploy 설정은 끝입니다. 이제 Travis CI와 CodeDeploy를 연동해 보겠습니다.

 

 

Travis CI, S3, CodeDeploy 연동

먼저 S3에 넘겨줄 zip 파일을 저장할 디렉토리를 하나 생성해줍시다. EC2 서버에 접속해서 아래 명령어를 입력해 디렉토리를 생성해줍시다.

mkdir ~/app/step2 && mkdir ~/app/step2/zip

Travis CI의 build가 끝나면 S3에 zip 파일이 전송되고, 이 zip 파일은 /home/ec2-user/app/step2/zip 경로에 복사되어 압축을 풀 예정입니다.

Travis CI 설정은 .travis.yml에서 진행했습니다.

AWS CodeDeploy의 설정은 appspec.yml으로 진행합니다. 코드는 아래와 같습니다.

version: 0.0 # CodeDeploy 버전을 명시한다.
os: linux 
files:
  - source:  / # 전체 파일을 나타낸다.
    destination: /home/ec2-user/app/step2/zip/ # source로 지정된 전체 파일이 받을 위치이다.
    overwrite: yes # 기존 파일들이 있으면 덮어 쓸지를 결정한다.
  • version: 0.0
    • CodeDeploy 버전을 이야기합니다.
    • 프로젝트 버전이 아니므로 0.0 외에 다른 버전을 사용하면 오류가 발생합니다.
  • source
    • CodeDeploy에서 전달해 준 파일 중 destination으로 이동시킬 대상을 지정합니다.
    • 루트 경로(/)를 지정하면 전체 파일을 이야기합니다.
  • destination
    • source에서 지정된 파일을 받을 위치입니다.
    • 이후 Jar를 실행하는 등은 destination에서 옮긴 파일들로 진행됩니다.
  • overwrite
    • 기존에 파일들이 있으면 덮어쓸지를 결정합니다.
    • yes라고 했으니 파일들을 덮어쓰게 됩니다.

.travis.yml에도 CodeDeploy 내용을 추가합니다. deploy 항목에 다음 코드를 추가합니다.

language: java
jdk:
  - openjdk8
branches:
  only:
    - main

# Travis CI Server's Home
cache:
  - directories:
      - '$HOME/.m2/repository'
      - '$HOME/.gradle'

script: "./gradlew clean build"

before_deploy:
  - zip -r SpringBootWebService *
  - mkdir -p deploy
  - mv SpringBootWebService.zip deploy/SpringBootWebService.zip

deploy:
  - provider: s3
    access_key_id: $AWS_ACCESS_KEY
    secret_access_key: $AWS_SECRET_KEY
    bucket: qazyj-springboot-build #S3 버킷 이름
    region: ap-northeast-2
    skip_cleanup: true
    acl: private #zip 파일 접근 private으로
    local_dir: deploy #before_deploy에서 생성한 디렉토리
    wait_until_deployed : true
    on:
      branch: main

## 새롭게 추가된 부분
  - provider: codedeploy
    access_key_id: $AWS_ACCESS_KEY # Travis repo settings에 설정된 값
    secret_access_key: $AWS_SECRET_KEY # Travis repo settings에 설정된 값
    bucket: qazyj-springboot-build # S3 버킷
    key: SpringBootWebService.zip # 빌드 파일을 압축해서 전달
    bundle_type: zip # 압축 확장자
    application: springboot2-webservice # 웹 콘솔에서 등록한 CodeDeploy 어플리케이션
    deployment_group: springboot2-webservice-group # 웹 콘솔에서 등록한 CodeDeploy 배포 그룹
    region: ap-northeast-2
    wait-until-deployed: true
    on:
      branch: main
## 새롭게 추가된 부분

# CI 실행 완료 시 메일로 알람
notifications:
  email:
    recipients:
      - qazyj@naver.com

모든 내용을 작성했다면 프로젝트를 커밋하고 푸쉬합니다. Travis CI가 끝나면 CodeDeploy 화면 아래에서 배포가 수행되는 것을 확인할 수 있습니다. on: branch:main을 추가안해주면 

위의 에러가 뜨며 정상적으로 작업이 진행되지 않습니다.

여러번의 실패 끝에 on: branch:main을 추가해서 성공적으로 배포가 되었습니다.

 

이제 Travis CI, S3, CodeDeploy가 연동되었습니다.

 

 

 

배포 자동화 구성

이제 실제로 Jar를 배포하여 실행까지 해봅시다.

 

deploy.sh 파일 추가

먼저 step 환경에 실행될 deploy.sh를 생성하겠습니다. scripts 디렉토리를 생성해서 여기에 deploy.sh 스크립트를 생성합니다. 코드는 아래와 같습니다.

#!/bin/bash

REPOSITORY=/home/ec2-user/app/step2
PROJECT_NAME=SpringBootWebService

echo "> Build 파일 복사"

cp $REPOSITORY/zip/*.jar $REPOSITORY/

echo "> 현재 구동중인 애플리케이션 pid 확인"

CURRENT_PID=$(pgrep -fl SpringBootWebService | grep java | awk '{print $1}')

echo "현재 구동중인 어플리케이션 pid: $CURRENT_PID"

if [ -z "$CURRENT_PID" ]; then
    echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
    echo "> kill -15 $CURRENT_PID"
    kill -15 $CURRENT_PID
    sleep 5
fi

echo "> 새 어플리케이션 배포"

JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)

echo "> JAR Name: $JAR_NAME"

echo "> $JAR_NAME 에 실행권한 추가"

chmod +x $JAR_NAME

echo "> $JAR_NAME 실행"

nohup java -jar \
    -Dspring.config.location=classpath:/application.properties,classpath:/application-real.properties,/home/ec2-user/app/application-oauth.properties,/home/ec2-user/app/application-real-db.properties \
    -Dspring.profiles.active=real \
    $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &

다음은 .travis.yml 파일을 수정해줍시다.

 

 

.travis.yml 파일 수정

현재는 프로젝트의 모든 파일을 zip 파일로 만드는데, 실제로 필요한 파일은 Jar, appspec.yml, 배포를 위한 스크립트들 입니다. 이 외 나머지는 배포에 필요하지 않으니 포함하지 않도록 before_deploy를 아래와 같이 수정합니다. 

language: java
jdk:
  - openjdk8
branches:
  only:
    - main

# Travis CI Server's Home
cache:
  - directories:
      - '$HOME/.m2/repository'
      - '$HOME/.gradle'

script: "./gradlew clean build"

before_deploy:
  - mkdir -p before-deploy # zip에 포함시킬 파일들을 담을 디렉토리 생성
  - cp scripts/*.sh before-deploy/
  - cp appspec.yml before-deploy/
  - cp build/libs/*.jar before-deploy/
  - cd before-deploy && zip -r before-deploy * # before-deploy로 이동후 전체 압축
  - cd ../ && mkdir -p deploy # 상위 디렉토리로 이동후 deploy 디렉토리 생성
  - mv before-deploy/before-deploy.zip deploy/SpringBootWebService.zip # deploy로 zip파일 이동

deploy:
  - provider: s3
    access_key_id: $AWS_ACCESS_KEY
    secret_access_key: $AWS_SECRET_KEY
    bucket: qazyj-springboot-build #S3 버킷 이름
    region: ap-northeast-2
    skip_cleanup: true
    acl: private #zip 파일 접근 private으로
    local_dir: deploy #before_deploy에서 생성한 디렉토리
    wait_until_deployed : true
    on:
      branch: main

## 새롭게 추가된 부분
  - provider: codedeploy
    access_key_id: $AWS_ACCESS_KEY # Travis repo settings에 설정된 값
    secret_access_key: $AWS_SECRET_KEY # Travis repo settings에 설정된 값
    bucket: qazyj-springboot-build # S3 버킷
    key: SpringBootWebService.zip # 빌드 파일을 압축해서 전달
    bundle_type: zip # 압축 확장자
    application: springboot2-webservice # 웹 콘솔에서 등록한 CodeDeploy 어플리케이션
    deployment_group: springboot2-webservice-group # 웹 콘솔에서 등록한 CodeDeploy 배포 그룹
    region: ap-northeast-2
    wait-until-deployed: true
    on:
      branch: main
## 새롭게 추가된 부분

# CI 실행 완료 시 메일로 알람
notifications:
  email:
    recipients:
      - qazyj@naver.com
  • Travis CI는 S3로 특정 파일만 업로드가 안됩니다.
    • 디렉토리 단위로만 업로드할 수 있기 때문에 before-deploy 디렉토리는 항상 생성합ㄴ디ㅏ.
  • before-deploy에는 zip 파일에 포함시킬 파일들을 저장합니다.
  • zip- r 명령어를 통해 before-deploy 디렉토리 전체 파일을 압축합니다.

마지막으로 CodeDeploy의 명령을 담당할 appspec.yml 파일을 수정합니다.

 

 

appspec.yml 파일 수정

appspec.yml 파일에 아래 코드를 추가합니다. location, timeout, runas의 들여쓰기를 주의해야 합니다. 잘못될 경우 배포가 실패합니다.

version: 0.0 # CodeDeploy 버전을 명시한다.
os: linux
files:
  - source:  / # 전체 파일을 나타낸다.
    destination: /home/ec2-user/app/step2/zip/ # source로 지정된 전체 파일이 받을 위치이다.
    overwrite: yes # 기존 파일들이 있으면 덮어 쓸지를 결정한다.
    
permissions:
  - object: /
    pattern: "**"
    owner: ec2-user
    group: ec2-user

hooks:
  ApplicationStart:
    - location: deploy.sh # 엔진엑스와 연결되어 있지 않은 Port로 새 버전의 스프링 부트를 시작합니다.
      timeout: 60
      runas: ec2-user
  • permissions
    • CodeDeploy에서 EC2 서버로 넘겨준 파일들을 모두 ec2-user 권한을 갖도록합니다.
  • hooks
    • CodeDeploy 배포 단계에서 실행할 명령어를 지정합니다.
    • ApplicationStart라는 단계에서 deploy.sh를 ec2-user권한으로 실행하게 합니다.
    • timeout: 60으로 스크립트 실행 60초 이상 수행되면 실패가 됩니다.(무한정 기다릴 수 없으니 시간 제한을 둬야만 합니다.)

모든 설정이 완료되었으니 커밋과 푸시를 합니다. 

 

CodeDeploy에 배포가 성공하면 웹브라우저에서 EC2 도메인을 이용해서 확인해봅시다. 마지막으로 실제 배포하듯이 진행해 보겠습니다.

 

 

실제 배포 과정 체험

build.gradle에서 프로젝트 버전을 아래와 같이 변경합니다.

version '1.0.1-SNAPSHOT'

간단하게나마 변경된 내용을 알 수 있게 src/main/resources/templates/index.mustache 내용에 아래와 같이 Ver.3 텍스트를 추가합니다.

<h1>스프링부트로 시작하는 웹 서비스 Ver.3</h1>

그리고 깃허브로 커밋과 푸시를 합니다. 변경된 코드가 배포된 것을 확인할 수 있습니다.

 

 

 

CodeDeploy 로그 확인

CodeDeploy와 같이 AWS가 지원하는 서비스에서는 오류가 발생했을 때 로그 찾는 방법을 모르면 오류를 해결하기가 어렵습니다. 그래서 배포가 실패할 경우 어느 로그를 봐야할지 간단하게 배워봅시다.

CodeDeploy에 관한 대부분 내용은 /opt/codedeploy-agent/deployment-root에 있습니다. 해당 디렉터리로 이동(cd /opt/codedeploy-agent/deployment-root) 한 뒤 ll 명령어를 입력하면 아래와 같은 내용을 확인할 수 있습니다.

drwxr-xr-x 7 root root 101  1월  7 00:41 18e713ab-302e-4750-ac92-3fa4f156e15b
drwxr-xr-x 2 root root 247  1월  7 00:41 deployment-instructions
drwxr-xr-x 2 root root  46  1월  7 00:36 deployment-logs
drwxr-xr-x 2 root root   6  1월  7 00:41 ongoing-deployment
  1. 최상단의 영문과 대시(-)가 있는 디렉토리명은 CodeDeploy ID입니다.
    • 사용자마다 고유한 ID가 생성되어 각자 다른 ID가 발급되니 본인의 서버에는 다른 코드로 되어있습니다.
    • 해당 디렉토리로 들어가보면 배포한 단위별로 배포 파일들이 있습니다.
    • 본인의 배포 파일이 정상적으로 왔는지 확인해 볼 수 있습니다.
  2. /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log
    • CodeDeploy 로그 파일입니다.
    • CodeDeploy로 이루어지는 배포 내용 중 표준 입/출력 내용은 모두 여기에 담겨 있습니다.
    • 작성한 echo 내용도 모두 표기됩니다.

테스트, 빌드, 배포까지 전부 자동화되었습니다. 이제는 작업이 끝난 내용을 Master 브랜치에 푸시만하면 자동으로 EC2에 배포가 됩니다. 

하지만, 문제가 한 가지 남았습니다. 배포하는 동안 스프링 부트 프로젝트는 종료 상태가 되어 서비스를 이용할 수 없다는 것 입니다. 어떻게 하면 배포하는 동안에도 서비스는 계속 유지될 수 있을까요??

 

다음 장에서는 서비스 중단 없는 배포 방법에 대해서 배워봅시다.

 

 

 

 

 

출처

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - YES24

가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현한다. JPA와 JUnit 테스트, 그레이들, 머스테치, 스프링

www.yes24.com

 

 

이제 완성된 프로젝트와 배포 환경을 구축했습니다. 이들을 조합해 실제로 서비스를 한번 배포해봅시다.

 

 

 

EC2에 프로젝트 Clone 받기

EC2에 깃 설치

sudo yum install git

깃 설치 상태 확인

git --version

git clone으로 프로젝트를 저장할 디렉토리를 생성해줍시다.

mkdir ~/app && mkdir ~/app/step1

생성한 디렉토리로 이동합시다.

cd ~/app/step1

본인의 https 주소를 복사합니다. git clone을 해줍시다.

git clone (복사한 주소)

파일이 잘 복사 되었는지 확인합시다.

cd (프로젝트명)
ls

코드들이 잘 수행되는지 테스트로 검증해봅시다.

./gradlew test

5장까지 잘 적용했다면 정상적으로 테스트를 통과합니다.

만약 테스트가 실패해서 수정하고 깃허브에 푸시를 했다면 프로젝트 폴더 안에서 다음 명령어를 사용하면 됩니다.

git pull

만약 아래와 같이 gradlew 실행 권한이 없다는 메시지가 뜬다면

-bash: ./gradlew: Permission denied

아래 명령어로 실행 권한을 추가한 뒤 다시 테스트를 수행해주면 됩니다.

chmod +x ./gradlew

 

 

 

배포 스크립트 만들기

작성한 코드를 실제 서버에 반영하는 것을 배포라고 합니다. 이 책에서 배포라 하면 아래의 과정을 모두 포괄하는 의미라고 보면 됩니다.

  • git clone 혹은 git pull을 통해 새 버전의 프로젝트 받음
  • Gradle이나 Maven을 통해 프로젝트 테스트와 빌드
  • EC2 서버에서 해당 프로젝트 실행 및 재실행

앞선 과정을 배포할 때마다 개발자가 하나하나 명령어를 실행하는 것은 불편함이 많습니다. 그래서 이를 쉘 스크립트로 작성해 스크립트만 실행하면 앞의 과정이 차례로 진행되도록 하겠습니다.

~/app/step1에 deploy.sh 파일을 하나 생성합니다.

vim ~/app/step1/deploy.sh

빔은 기타 에디터와 다르게 빔만의 사용법이 있다고 합니다. 아래의 코드를 추가해줍시다. (PHP코드와 비슷합니다.)

#!/bin/bash 

REPOSITORY=/home/ec2-user/app/step1 
PROJECT_NAME=spring_web_service

cd $REPOSITORY/$PROJECT_NAME 

echo "> Git pull" 

git pull 

echo "> 프로젝트 Build 시작" 

./gradlew build 

echo "> step1 디렉토리로 이동" 

cd $REPOSITORY 

echo "> Build 파일복사" 

cp $REPOSITORY/$PROJECT_NAME/build/libs/*.jar $REPOSITORY/ 

echo "> 현재 구동중인 애플리케이션 pid 확인" 

CURRENT_PID=$(pgrep -f ${PROJECT_NAME}.*.jar) 

echo " 현재 구동중인 애플리케이션pid: $CURRENT_PID" 

if [ -z "$CURRENT_PID" ]; then 
	echo "> 현재구동중인 애플리케이션이 없으므로 종료하지 않습니다." 
else 
	echo "> kill -15 $CURRENT_PID" 
    kill -15 $CURRENT_PID 
    sleep 5
fi 

echo "> 새 애플리케이션 배포" 

JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1) 

echo "> JAR Name: $JAR_NAME" 

nohup java -jar $REPOSITORY/$JAR_NAME 2>&1 &
  • REPOSITORY=/home/ec2-user/app/step1
    • 프로젝트 디렉토리 주소는 스크립트 내에서 자주 사용하는 값이기 때문에 이를 변수로 저장
    • PROJECT_NAME도 마찬가지입니다.
    • $변수명으로 사용가능합니다.
  • cd $REPOSITORY/$PROJECT_NAME/
    • 제일 처음 git clone 받았던 디렉토리로 이동합니다.
  • git pull
    • 디렉토리 이동 후, Master 브랜치의 최신 내용을 받습니다.
  • ./gradlew build
    • 프로젝트 내부의 gradlew로 build를 수행합니다.
  • cp ./build/libs/*.jar %REPOSITORY/
    • build의 결과물인 jar 파일을 복사해 jar 파일을 모아둔 위치로 복사합니다.
  • CURRENT_PID=$(pgrep -f springboot-webservice)
    • 기존에 수행 중이던 스프링 부트 애플리케이션을 종료합니다.
    • pgrep은 process id만 추출하는 명령어입니다.
    • -f 옵션은 프로세스 이름으로 찾습니다.
  • if ~ else ~ fi
    • 현재 구동 중인 프로세스가 있는지 없는지를 판단해서 기능을 수행합니다.
  • JAR_NAME=$(ls -tr $REPOSITORY/|grep jar |tail -n 1)
    • 새로 실행할 jar 파일명을 찾습니다
    • 여러 jar 파일이 생기기 때문에 tail -n으로 가장 나중에 jar 파일을 변수에 저장합니다.
  • nohup java -jar $REPOSITORY/$JAR_NAME 2>&1 &
    • 찾은 jar 파일명으로 해당 jar 파일을 nohup으로 실행합니다.
    • 스프링 부트의 장점으로 특별히 외장 톰캣을 설치할 필요가 없습니다.

이렇게 생성한 스크립트에 실행 권한을 추가합니다.

chmod +x ./deploy.sh

아래 명령어로 확인해보면 x 권한이 추가된 것을 확인할 수 있습니다.

ll

이제 이 스크립트를 다음 명령어로 실행합니다.

./deploy.sh

잘 실행되었으면 nohup.out 파일으르 열어 로그를 보겠습니다.

vim nohup.out

로그를 보니, ClientRegistrationRepository를 찾을 수 없다는 에러가 발생하면서 애플리케이션 실행에 실패하는 것을 볼 수 있습니다.

***************************

APPLICATION FAILED TO START

***************************



Description:



Method springSecurityFilterChain in org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration required a bean of type 'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository' that could not be found.



The following candidates were found but could not be injected:

        - Bean method 'clientRegistrationRepository' in 'OAuth2ClientRegistrationRepositoryConfiguration' not loaded because OAuth2 Clients Configured Condition registered clients is not available





Action:



Consider revisiting the entries above or defining a bean of type 'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository' in your configuration.

이유는 ClientRegistrationRepository를 생성하려면 clientId와 clientSecret가 필수입니다. 로컬 PC에서는 application-oauth.properties가 있어 문제가 되지 않았지만, 해당 파일은 .gitignore로 제외 대상이기 때문에 깃허브에 올라가지 않습니다.

 

 

 

외부 Security 파일 등록하기

애플리케이션을 실행하기 위해 공개된 저장소에 clientId와 clientSecret을 올릴 수는 없습니다. 서버에서 직접 이 설정들을 갖고 있게 해줍시다.

 

먼저 step1이 아닌 app 디렉토리에 properties파일을 생성합니다.

vim /home/ec2-user/app/application-oauth.properties

로컬에 있는 application-oauth.properties 파일 내용을 그대로 붙여넣기 합시다. 붙여넣기 한 뒤, :wq를 누르면 저장됩니다.

방금 생성한 application-oauth.properties를 쓰도록 deploy.sh 파일을 수정합니다.

vim deploy.sh
nohup java -jar \
   -Dspring.config.location=classpath:/application.properties,/home/ec2-user/app/application-oauth.properties \
   $REPOSITORY/$JAR_NAME 2>&1 &
  • -Dspring.config.location
    • 스프링 설정 파일 위치를 지정합니다.
    • 기본 옵션들을 담고 있는 application.properties과 OAuth 설정들을 담고 있는 application-oauth.properties의 위치를 지정합니다.
    • classpath가 붙으면 jar 안에 있는 resources 디렉토리를 기준으로 경로가 생성됩니다.
    • application-oauth.properties는 절대경로를 사용합니다. 외부 파일이 있기 때문입니다.

이제 다시 deploy.sh를 실행해봅시다.

./deploy.sh

 

 

 

스프링 부트 프로젝트로 RDS 접근하기

MariaDB에서 스프링부트 프로젝트를 실행하기 위해선 몇 가지 작업이 필요합니다.

  • 테이블 생성 : H2에서 자동 생성해주던 테이블들을 MariaDB에선 직접 쿼리를 이용해 생성합니다.
  • 프로젝트 설정 : 자바 프로젝트가 MariaDB에 접근하려면 데이터베이스 드라이버가 필요합니다.
  • EC2 설정 : 데이터베이스의 접속 정보는 중요하게 보호해야 할 정보입니다. 공개되면 외부에서 데이터를 모두 가져갈 수 있기 때문입니다. 프로젝트 안에 접속 정보를 갖고 있다면 깃허브와 같이 오픈된 공간에선 누구나 해킹할 위험이 있습니다.

 

 

RDS 테이블 생성

먼저 RDS 테이블을 생성해줍시다. 여기선 JPA가 사용될 엔티티 테이블과 스프링 세션이 사용될 테이블 2가지 종류를 생성합니다. JPA가 사용할 테이블은 테스트 코드 수행 시 로그로 생성되는 쿼리를 사용하면 됩니다.

 

복사하여 RDS에 반영해줍시다.

create table posts (id bigint not null auto_increment, created_date datetime, modified_date datetime, author varchar(255), content TEXT not null, title varchar(500) not null, primary key (id)) engine=InnoDB;
create table user (id bigint not null auto_increment, created_date datetime, modified_date datetime, email varchar(255) not null, name varchar(255) not null, picture varchar(255), role varchar(255) not null, primary key (id)) engine=InnoDB;

CREATE TABLE SPRING_SESSION (
	PRIMARY_ID CHAR(36) NOT NULL,
	SESSION_ID CHAR(36) NOT NULL,
	CREATION_TIME BIGINT NOT NULL,
	LAST_ACCESS_TIME BIGINT NOT NULL,
	MAX_INACTIVE_INTERVAL INT NOT NULL,
	EXPIRY_TIME BIGINT NOT NULL,
	PRINCIPAL_NAME VARCHAR(100),
	CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;

CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);

CREATE TABLE SPRING_SESSION_ATTRIBUTES (
	SESSION_PRIMARY_ID CHAR(36) NOT NULL,
	ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
	ATTRIBUTE_BYTES BLOB NOT NULL,
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;

 

 

프로젝트 설정

MariaDB 드라이버를 build.gradle에 등록합시다.

implementation("org.mariadb.jdbc:mariadb-java-client")

그리고 서버에 구동될 환경을 하나 구성합니다.

src/main/resources/에 application-real.properties파일을 추가합니다.

spring.profiles.include=oauth,real-db
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.session.store-type=jdbc

이렇게 하면 profile=real인 환경이 구성된다고 보면됩니다. 실제 운영 될 환경이기 때문에 보안/로그상 이슈가 될만한 설정들을 모두 제거하며 RDS 환경 profile 설정이 추가됩니다.

github에 푸시합니다.

 

 

EC2 설정

OAuth와 마찬가지로 RDS 접속 정보도 보호해야할 정보이니 EC2 서버에 직접 설정 파일으 둡니다. app디렉토리에 application-real-db.properties 파일을 생성합니다. 아래의 코드를 추가한 뒤 :wq로 저장해줍니다.

spring.jpa.hibernate.ddl-auto=none
spring.datasource.url=jdbc:mariadb://(rds주소):(포트명)/(database명)
spring.datasource.username=db계정
spring.datasource.password=db계정 비밀번호
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
  • spring.jpa.hibernate.ddl-auto=none
    • JPA로 테이블이 자동 생성되는 옵션을 None(생성하지 않음)으로 지정합니다.
    • RDS에는 실제 운영으로 사용될 테이블이니 절대 스프링 부트에서 새로 만들지 않도록 해야 합니다.
    • 이 옵션을 하지 않으면 자칫 테이블이 모두 새로 생성될 수 있습니다.

마지막으로 deploy.sh가 real profile을 쓸 수 있도록 수정해줍니다.

nohup java -jar \
   -Dspring.config.location=classpath:/application.properties,classpath:/application-real.properties,/home/ec2-user/app/application-oauth.properties,/home/ec2-user/app/application-real-db.properties \
   -Dspring.profiles.active=real \
   $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &

구동해봅니다.

./deploy.sh

아래의 명령어로 html 코드가 정상적으로 보인다면 성공입니다.

curl localhost:8080

저는 잘 되지 않았습니다. 

해결 방법은 일단 책 저자분의 블로그를 참고했습니다.

 

(2020.12.16) 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 최신 코드로 변경하기

작년 11월 말에 스프링 부트와 AWS로 혼자 구현하는 웹 서비스를 출판 하였습니다. Spring Boot가 2.1 -> 2.4로, IntelliJ IDEA가 2019 -> 2020으로 오면서 너무 많은 변화가 있다보니, 집필할 때와 비교해 실습

jojoldu.tistory.com

여기서 읽어 보시면 맨아래

이렇게 바꾸라고 되어있는데 이렇게 바꾸면

***************************

APPLICATION FAILED TO START

***************************



Description:



Failed to bind properties under 'spring.datasource.hikari' to com.zaxxer.hikari.HikariDataSource:



    Property: spring.datasource.hikari.driver-class-name

    Value: org.mariadb.jdbc.Driver

    Origin: URL [file:/home/ec2-user/app/application-real-db.properties]:7:44

    Reason: Failed to load driver class org.mariadb.jdbc.Driver in either of HikariConfig class loader or Thread context classloader



Action:



Update your application's configurat

위의 에러가 나오게 됩니다. application-real-db.properties 파일을  spring.datasource.hikari를 com.zaxxer.hikari.HikariDataSource로 전부 고쳐주니 정상적으로 돌아갔습니다.

spring.jpa.hibernate.ddl-auto=none
spring.jpa.show_sql=false

com.zaxxer.hikari.HikariDataSource.jdbc-url=jdbc:mariadb://(rds주소):(포트명)/(database명)
com.zaxxer.hikari.HikariDataSource.username=db 계정
com.zaxxer.hikari.HikariDataSource.password=db 비밀번호
com.zaxxer.hikari.HikariDataSource.driver-class-name=org.mariadb.jdbc.Driver

돌려 보니 SQL 테이블이 제대로 생성되지 않는 에러가 계속 발생해서 찾아보니 springboot 2.1.9가 되면서 기존에 사용중이던 MySQL5InnoDBDialect 이 Deprecated가 된것으로 보입니다. application.properties의 코드인 아래 코드를

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

2.1.10부터는 아래의 코드로 추가해줘야합니다.

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect spring.jpa.properties.hibernate.dialect.storage_engine=innodb spring.datasource.hikari.jdbc-url=jdbc:h2:mem://localhost/~/testdb;MODE=MYSQL
  • MySQL5Dialect 로 하게되면 auto innodb가 적용이 안된채로 작동되는것을 확인하였습니다.
    • 스프링부트 2.2.2가 되어도 MySQL57Dialect 를 사용해야될것 같습니다.
  • 2.1.10 이후에는 직접 jdbc-url을 선언하여 ;MODE=MYSQL가 h2주소 뒤에 붙어야만 mysql 테이블 쿼리가 정상 작동됩니다.

제가 잘 되지 않았던 부분은 application.properties에서 application-real.properties에 접근을 잘 못했던 것 같습니다. 아래의 코드로 테이블이 생성되면 안되는데

spring.jpa.hibernate.ddl-auto=none

계속 테이블이 생성되며 에러가 발생했던 것이였고, application.properties에서 application-real-db.properties로 직접 접근을 시켜주니 해결이 됐습니다.. (이것 때문에 EC2, RDS 두번 설치했습니다..ㅠㅠ)

application.properties를 아래와 같이 변경해주시면 됩니다!!

spring.profiles.include=oauth,real-db

만약 아래와 같이 입력했을 때,

curl localhost:8080

아래와 같이 나온다면

curl: (7) Failed to connect to localhost port 8080: Connection refused

아래 명령어를 입력해줍시다!

netstat -ln | grep 8080

그 후 다시 curl localhost:8080을 해보시면  아래와 같은 html 코드를 볼 수 있습니다!!

추가적으로 저는 두번째에 할때는 DB Navigator가 잘 동작하지 않아 IntelliJ 우측 탭에있는 Database를 사용해서 쿼리문을 작성하였습니다.

 

 

 

 

EC2에 소셜 로그인하기

잘 배포된 것은 확인했습니다. 이제 브라우저를 확인해 볼텐데, 그 전에 몇 가지 작업을 해보겠습니다.

  • AWS 보안 그룹 변경
    • 먼저 EC2에 스프링 부트 프로젝트가 8080 포트로 배포되었으니 8080 포트가 보안 그룹에 열려 있는지 확인합니다.

저는 열려 있었습니다.

 

  • AWS EC2 도메인으로 접속

왼쪽 사이드 바의 [인스턴스] 메뉴를 클릭합니다. 본인이 생성한 EC2 인스턴스를 선택하면 상세 정보에서 퍼블릭 DNS를 확인할 수 있습니다. 해당 주소가 EC2에 자동으로 할당된 도메인 입니다. 

그럼 도메인 주소에 8080 포트를 붙여 브라우저에 입력합니다. 아래와 같이 localhost:8080을 입력할 때와 같은 화면을 볼 수 있습니다.

이제 도메인을 가진 서비스가 되었습니다. 

 

아직 현재 상태에서는 해당 서비스에 EC2의 도메인을 등록하지 않았기 때문에 구글과 네이버 로그인이 작동하지 않습니다. 그렇기 때문에 구글먼저 등록하도록 하겠습니다.

 

 

  • 구글에 EC2 주소 등록

구글 console에 들어가서 우측에 있는 API 및 서비스 -> 사용자 인증 정보를 눌러줍니다. OAuth 2.0 클라이언트 ID에 만들어져있는 ID를 눌러줍니다. 승인된 리디렉션 URI에 (퍼블릭DNS주소:8080/login/oauth2/code/google)를 추가해준 뒤 저장해줍니다.

 

  • 네이버에 EC2 주소 등록

네이버 개발자 센터로 접속해서 본인의 프로젝트로 이동해줍니다. API 설정에서 PC웹 항목에 있는 서비스 URL과 Callback URL 2개를 수정합니다.  서비스 URL은 포트 번호를 제외한 실제 도메인 주소, Callback URL에는 구글에서 등록한 주소와 비슷한 전체 주소인 (퍼블릭DNS주소:8080/login/oauth2/code/naver)를 입력한 뒤 수정버튼을 눌러줍니다.

 

구글, 네이버 둘 다 도메인 주소로 로그인을 테스트 해보면 정상적으로 로그인이 될 것 입니다.

 

 

 

 

 

 

출처

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - YES24

가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현한다. JPA와 JUnit 테스트, 그레이들, 머스테치, 스프링

www.yes24.com

 

 

 

AWS에서는 모니터링, 알람, 백업, HA 구성 등을 모두 지원하는 관리형 서비스인 RDS를 제공합니다. RDS는 AWS에서 지원하는 클라우드 기반 관계형 데이터베이스입니다. 하드웨어 프로비저닝, 데이터베이스 설정, 패치 및 백업과 같이 잦은 운영 작업을 자동화하여 개발자가 개발에 집중할 수 있게 지원하는 서비스입니다. 

 

 

RDS 인스턴스 생성하기

먼저 RDS 인스턴스를 생성합시다. EC2 인스턴스를 만들 때와 똑같이 검색창에 RDS를 검색한 뒤 선택하고, RDS 대시보드에서 데이터베이스 생성 버튼을 클립합시다.

 

RDS 생성 과정이 진행됩니다. DB 엔진 선택화면에서 MariaDB를 선택해줍시다. MariaDB를 선택하는 이유는 아래와 같습니다.

  • 비용
  • Amazon Aurora(오로라) 교체 용이성

RDS의 가격은 라이센스 비용 영향을 받습니다. 상용 데이터베이스인 오라클,MSSQL이 오픈소스인 MySQL, MariaDB, PostgreSQL보다는 동일한 사양 대비 더 가격이 높습니다. 

두 번째로 Amazon Aurora 교체 용이성 입니다. Amazon Aurora는 AWS에서 MySQL과 PostgreSQL을 클라우드 기반에 맞게 재구성한 데이터베이스입니다. 공식 자료에 의하면 RDS MySQL 대비 5배, RDS PostgreSQL 보다 3배의 성능을 제공합니다. 더군다나 AWS에서 직접 엔지니어링하고 있기 때문에 계속해서 발전하고 있습니다. 그렇기 때문에 MariaDB를 사용하도록 하겠습니다.

 

MariaDB는 MySQL을 기반으로 만들어졌습니다. 쿼리를 비롯한 전반적인 사용법은 MySQL과 유사하니 사용 방법에 대해서는 크게 걱정하지 않아도 됩니다. MariaDB는 MySQL 대비 아래의 장점이 있습니다.

  • 동일 하드웨어 사양으로 MySQL보다 향상된 성능
  • 좀 더 활성화된 커뮤니티
  • 다양한 기능
  • 다양한 스토리지 엔진

MySQL을 써왔다면 이번 기회에 MariaDB를 선택해서 사용해보길 추천한다고 합니다.

 

 

 

 

 

RDS 운영 환경에 맞는 파라미터 설정하기

RDS를 처음 생성하면 몇가지 설정을 필수로 해야 합니다. 우선 아래 3개의 설정을 차례로 진행해 보겠습니다.

  • 타임존
  • Character Set
  • Max Connection

왼쪽 파라미터 그룹 탭을 클릭해서 이동합니다. 파라미터 그룹 생성 버튼을 눌러줍니다. 세부 정보 위쪽에 DB 엔진을 선택하는 항복이 있습니다. 여기서 방금 생성한 MariaDB와 같은 버전을 맞춰야 합니다. 그룹을 생성했으면 해당 파라미터 그룹을 클릭합니다. 파라미터 편집 버튼을 클릭해 편집 모드로 전환합니다.

  1. time_zone의 값을 Asia/Seoul로 바꿔줍니다. 
  2. character 검색해서 나온 값은 utf8mb4(이모지 저장가능), collation 검색해서 나온 값은 utf8mb4_general_ci로 바꿔줍니다.
  3. max_connections 150으로 바꿔줍니다.
  4. 변경사항 저장 버튼 클릭

이제 파라미터 그룹을 데이터베이스에 연결합시다. 왼쪽 메뉴탭에 데이터베이스를 클릭한 뒤 데이터베이스 체크 후 수정버튼을 눌러줍니다.

 

 

위와 같이 수정해줍시다.

 

 

 

내 PC에서 RDS에 접속해보기

내 PC에서 RDS로 접근하기 위해서 RDS의 보안 그룹에 본인 PC의 IP를 추가하겠습니다. 

 

RDS의 보안 그룹 정보를 그대로 두고, EC2에 사용된 보안 그룹의 그룹 ID를 복사한 뒤 복사된 그룹 ID와 본인의 IP를 RDS 보안 그룹의 인바운드로 추가합니다.

 

 

Database 플러그인 설치

로컬에서 원격 데이터베이스로 붙을 때 GUT 클라이언트를 많이 사용합니다. MySQL의 대표적인 클라이언트로 Weorkbench 등이 있습니다. 각각의 도구마다 큰 차이가 없으니 본인이 가장 좋아하는 틀을 사용하면 됩니다. 이 책에서는 인텔리제이에 Database 플러그인을 설치해서 진행했습니다.

 

RDS 정보 페이지에서 엔드 포인트를 확인합니다. 이 엔드 포인트가 접근 가능한 URL이므로 메모장 같은 곳에 복사해둡니다.

 

이제 Action검색(커맨드+시프트+a)으로 database browser를 실행합니다. 왼쪽 + 버튼을 누른 뒤, MySQL을 눌러줍니다. MariaDB는 MySQL 기반이므로 MySQL을 사용하면 됩니다. 본인이 생성한 RDS의 정보를 차례로 등록합니다.

 

 

 

 

 

출처

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - YES24

가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현한다. JPA와 JUnit 테스트, 그레이들, 머스테치, 스프링

www.yes24.com

 

 

 

이번엔 AWS라는 클라우드 서비스를 이용해 본격적으로 서버 배포를 진행해 보겠습니다. 

 

클라우드 서비스를 이용하는 이유 중 하나는 만든 서비스를 24시간 아무때나 외부에서 접근하려면 24시간동안 서버가 켜져있어야합니다. 

 

24시간 작동하는 서버에는 3가지의 선택지가 있습니다.

  • 집에 PC를 24시간 구동시킨다. 
  • 호스팅 서비스(Cafe 24, 코리아호스팅 등)을 이용한다.
  • 클라우드 서비스(AWS, AZURE, GCP. NAVER CLOUD PLATFORM 등)을 이용한다.

일반적으로 비용은 호스팅 서비스나 집 PC를 이용하는 것이 저렴합니다. 만약 특정 시간에만 트래픽이 몰린다면 유동적으로 사양을 늘릴 수 있는 클라우드가 유리합니다. 

 

클라우드에 대해서 잠깐 이야기하자면, 클라우드 서비스는 쉽게 말하면 인터넷(클라우드)를 통해 서버, 스토리지(파일 저장소), 데이터베이스, 네트워크, 소프트웨어, 모니터링 등의 컴퓨팅 서비스를 제공하는 것 입니다. 

 

AWS의 EC2는 서버 장비를 대여하는 것이지만, 실제로는 그 안의 로그 관리, 모니터링, 하드웨어 교체, 네트워크 관리 등을 기본적으로 지원하고 있습니다. 개발자가 직접 해야 할 일을 AWS가 전부 지원하는 것 입니다.

 

이런 클라우드에는 몇 가지의 형태가 있습니다.

  1. Infrastructure as a Service (IaaS, 아이아스, 이에스)
    • 기존 물리 장비를 미들웨어와 함께 묶어둔 추상화 서비스입니다.
    • 가상머신, 스토리지, 네트워크, 운영체제 등의 IT 인프라를 대여해 주는 서비스라고 보면 됩니다.
    • AWS의 EC2, S3 등
  2. Platform as a Service (PaaS, 파스)
    • 앞에서 언급한 IaaS에서 한 번 더 추상화한 서비스입니다.
    • 한 번 더 추상화 했기 때문에 많은 기능이 자동화되어 있습니다.
    • AWS의 Beanstalk(빈스톡), Heroku(헤로쿠) 등
  3. Software as a Service (SaaS, 사스)
    • 소프트웨어 서비스를 이야기합니다.
    • 구글 드라이브, 드랍박스, 네이버 클라우드 등

여러 클라우드 서비스 중 AWS를 선택합니다. 이유는 아래와 같습니다.

  • 첫 가입 시 1년간 대부분 서비스가 무료입니다. 단, 서비스마다 제한이 있는데 이는 각 서비스를 설정할 때 설명하겠습니다.
  • 클라우드에서는 기본적으로 지원하는 기능(모니터링, 로그관리, 백업, 복구, 클러스터링 등등)이 많아 개인이나 소규모일 때 개발에 좀 더 집중할 수 있습니다.
  • 많은 기업이 AWS로 이전 중이기 때문에 이직할 때 AWS 사용 경험이 도움이 됩니다. 국내에서는 쿠팡, 우아한형제들, 리멤버 등 클라우드를 사용할 수 있는 회사에서는 대부분 AWS를 사용한다고 합니다.
  • 사용자가 많아 국내 자료와 커뮤니티가 활성화되어 있습니다.

이 책에서 진행하는 모든 AWS 서비스는 IaaS를 사용합니다. AWS의 PaaS 서비스인 빈스톡을 사용하면 대부분 작업이 간소화되지만, 프리티어로 무중단 배포가 불가능합니다.

배포할 때마다 서버가 다운되면 제대로된 서비스를 만들 수 없으니 무중단 배포는 필수이고 빈스톡은 사용할 수 없습니다. 그리고 AWS 초보자인 저로서는 직접 하나씩 다 다뤄보는 것이 공부하는 데 도움이 될거라고 합니다.

 

 

 

AWS 회원가입

AWS 고식 사이트(https://aws.amazon.com/ko/)로 이동한 뒤 무료 계정을 만들어 줍시다. 무료로 개정을 만들고, 영문 주소와 신용 카드 정보를 입력한 뒤 가입하면 됩니다. 선택사항은 개인, 무료를 골라주시면 됩니다. 

 

 

 

EC2 인스턴스 생성하기 

EC2는 AWS에서 생성하는 성능, 용량 등을 유동적으로 사용할 수 있는 서버입니다. 

 

AWS에서 무료로 제공하는 프리티어 플랜에서는 EC2 사용에 다음과 같은 제한이 있습니다.

  • 사양이 t2.micro만 가능합니다.
    • vCPU(가상 CPU) 1Core, 메로리 1GB 사양입니다.
    • 보통 vCPU는 물리 CPU 사양의 절반 정도의 성능을 가집니다.
  • 월 750 시간의 제한이 있습니다. 초과하면 비용이 부과됩니다.
    • 24시간 * 31일 = 744시간 입니다.
    • 즉, 1대의 t2.micro만 사용한다면 24시간 사용할 수 있습니다.

앞의 제한 사항을 주의하면서 AWS를 사용하면 1년간 재미나게 써볼 수 있습니다. 자 그럼 EC2를 만들기 전에, 본인의 리전을 확인해 봅시다. 리전은 우측상단에 있습니다. 서울로 되어있지 않다면 서울로 바꿔줍시다.

 

그 후 검색창에 EC2를 검색해줍시다. AWS 서비스에 있는 EC2를 선택해 줍시다. EC2 대스보드가 나오는데, 중앙에 있는 인스턴스 시작 버튼을 클릭합니다. 인스턴스란 EC2 서비스에 생성된 가상 머신을 이야기합니다. 

 

인스턴스를 생성하는 첫 단계는 AMI(Amazon Machine Image)를 선택하는 것 입니다. AMI는 EC2 인스턴스를 시작하는데 필요한 정보를 이미지로 만들어 둔 것을 이야기합니다. 인스턴스라는 가상 머신에 운영체제 등을 설치할 수 있게 구워 넣은 이미지로 생각하면 됩니다.

예를들어 아마존 리눅스 2 AMI를 사용한다면 Amazon Linux 2 OS가 인스턴스에 설치되어 개발자가 사용할 수 있음을 의미합니다. 여기서는 Amazon Linux AMI를 선택합니다. 책에서는 Amazon Linux AMI를 선택했지만 저는 없어서 Amazon Linux 2 AMI를 선택했습니다.

 

Amazon Linux 2는 센토스(Centos) 7버전 자료들을 그대로 사용할 수 있다고 합니다. 그럼 굳이 센토스 AMI를 사용하지 않고 아마존 리눅스 AMI를 사용한 이유가 무엇일까요?? 이유는 아래와 같습니다.

  • 아마존이 개발하고 있기 때문에 지원받기가 쉽다.
  • 레드햇 베이스이므로 레드햇 계열의 배포판을 많이 다뤄본 사람일수록 문제없이 사용할 수 있다.
  • AWS의 각종 서비스와의 상성이 좋다
  • Amazon 독자적인 개발 리포지터리를 사용하고 있어 yum이 매우 빠르다.

Amazon Linux 2 AMI를 선택 한 뒤 프리티어로 표기된 t2.micro를 선택하고 다음버튼을 누릅니다. 다음단계는 세부정보 구성입니다. 혼자서 1대의 서버만 사용하니 별다른 설정없이 다음을 눌러 넘어가줍시다. 

 

다음 단계는 스토리지 선택입니다. 스토리지는 흔히 하드디스크라고 불리는 서버의 디스크를 이야기하며 서버의 용량을 얼마나 정할지 선택하는 단계입니다. 기본값은 8GB이지만, 30GB까지 프리티어로 가능하니 30GB으로 수정한 후, 다음버튼을 눌러줍시다.

 

다음 단계는 태그입니다. 웹 콘솔에서 표기될 태그인 Name 태그를 등록합시다. 여러 인스턴스가 있을 경우 이를 태그별로 구분하면 검색이나 그룹 짓기 편하므로 여기서 본인의 서비스의 인스턴스를 나타낼 수 있는 값으로 등록해준 뒤, 다음버튼을 눌러줍시다.

 

다음은 보안 그룹입니다. 보안 그룹은 방화벽을 이야기합니다. '서버로 80 포트 외에는 허용하지 않는다'는 역할을 하는 방화벽이 AWS에서는 보안 그룹으로 사용됩니다. 기존에 생성된 보안 그룹이 없으므로 보안 그룹 이름엔 유의미한 이름으로 변경합니다. 아래와 같이 변경해주시면 됩니다.

이 보안 그룹이 굉장히 중요한 부분입니다. 유형 항목에서 SSH이면서 포트 항목에서 22인 경우는 AWS EC2에 터미널로 접속할 때를 이야기합니다. pem 키가 없으면 접속이 안 되니 전체 오픈(0.0.0.0/0,::/0)하는 경우를 종종 발견합니다. 이렇게 되면 이후 파일 공유 디렉토리나 깃허브 등에 실수로 pem키가 노출되는 순간 서버에서 가상화폐가 채굴되는 것을 볼 수 있습니다.

보안은 언제나 높을수록 좋으니 pem 키 관리와 지정된 IP에서만 ssh 접속이 가능하도록 구성하는 것이 안전합니다. 그래서 본인의 집 IP를 기본적으로 추가하고 카페와 같이 집 외에 다른 장소에서 접속할 때는 해당 장소의 IP를 다시 SSH 규칙에 추가하는 것이 안전합니다.  그 후 검토 및 시작 버튼을 눌러줍시다. 그렇게 되면 검토 화면에서 보안 그룹을 경고하는데, 이는 8080포트가 전체 오픈이 되어서 발생합니다. 8080을 열어 놓는 것은 위험한 일이 아니니 바로 시작해줍시다.

 

인스턴스로 접근하기 위해서는 pem 키(비밀키)가 필요합니다. 그래서 인스턴스 마지막 단계는 할당할 pem키를 선택하는 것 입니다. 인스턴스는 지정된 pem 키(비밀키)와 매칭되는 공개키를 가지고 있어, 해당 pem 키 외에는 접근을 허용하지 않습니다. 일종의 마스터키이기 때문에 절대 유출되면 안 됩니다. pem 키는 이후 EC2 서버로 접속할 때 필수 파일이니 잘 관리할 수 있는 디렉토리로 저장합니다. 그 후 인스턴스를 시작해줍시다.

 

생성이 다 되었다면 IP와 도메인이 할당된 것을 확인할 수 있습니다. 인스턴스도 결국 하나의 서버이기 때문에 IP가 존재합니다. 인스턴스 생성 시에 항상 새 IP를 할당하는데, 같은 인스턴스를 중지하고 다시 시작할 때도 새 IP가 할당됩니다. 

즉, 요금을 아끼기 위해 잠깐 인스턴스를 중지하고 다시 시작하면 IP가 변경되는 것 입니다. 이렇게되면 매번 접속해야 하는 IP가 변경되서 PC에서 접근할 때마다 IP주소를 확인해야 합니다. 굉장히 번거로우므로 인스턴스의 IP가 매번 변경되지 않고 고정 IP를 가지게 해야합니다.

 


EIP 할당

AWS의 고정 IP를 Elastic IP(EIP, 탄력적 IP)라고 합니다. EC2 인스턴스 페이지 왼쪽 카테고리에서 탄력적 IP를 눌러 선택하고 주소가 없으므로 탄력적 IP주소 할당을 클릭해서할당 해준 뒤, 인스턴스와 연결해줍니다.

 

여기까지 진행했으면 EC2 인스턴스 생성 과정은 끝났지만 주의할 점이 있습니다. 방금 생성한 탄력적 IP는 생성하고 EC2 서버에 연결하지 않으면 비용이 발생합니다. 즉, 생성한 탈력적 IP는 무조건 EC2에 바론 연결해야 하며 만약 더는 사용할 인스턴스가 없을 때도 탄력적 IP를 삭제해야 합니다. 

 

 

 

EC2 서버에 접속하기

여기서는 Mac과 Window가 다른데, 저는 Mac을 사용하므로 Mac으로의 방법만 설명하겠습니다. Mac & Linux는 터미널에서 작업합니다.

 

AWS와 같은 외부 서버로 SSH 접속을 하려면 터미널에 매번 아래와 같이 긴 명령어를 입력해야 합니다.

ssh -i (pem 키 위치) (EC2의 탄력적 IP 주소)

이렇게 하라고 하는데 전 잘 되지않아서 검색해보니

ssh -i (pem 키 위치 & 확장자를 포함한 파일명) ec-user@(EC2의 탄력적 IP 주소)

로 하니까 연결이 되었습니다.

 

연결할 때마다 위의 코드를 적기는 힘듭니다. 아래와 같은 작업을 해줍시다.

cp (pem 키 위치 & 확장자를 포함한 파일명) ~/.ssh/
cd ~/.ssh/
ls
chmod 600 ~/.ssh/(pem 키 이름 & 확장자를 포함한 파일명)
vim ~/.ssh/config

이렇게 입력하면 입력할 수 있는 공간이 생깁니다. i 버튼을 눌러 수정할 수 있게 해준 후 아래의 코드를 입력합니다.

# 주석
Host (본인이 원하는 서비스명)
    HostName (ec2의 탄력적 IP 주소)
    User ec2-user
    IdentityFile ~/.ssh/(pem 키 이름 확장자 명 포함)

를 입력한 뒤 esc 버튼을 누르고 :wq를 입력하면 저장됩니다. 후 실행 권한이 필요하므로 아래의 권할 설정을 해줍시다.

chmod 700 ~/.ssh/config

이렇게 하면, 아래의 코드를 입력하면 ec2 서버와 연결이 가능해집니다.

ssh (config에 등록한 서비스명)

 

 

 

아마존 리눅스 서버 생성 시 꼭 해야할 설정들

java 8 설치

sudo yum install -y java-1.8.0-openjdk-devel.x86_64

타임존 변경

sudo rm /etc/localtime
sudo ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime
date

Hostname 변경

sudo hostnamectl set-hostname freelec-springboot2-webservice
sudo reboot
sudo vim /etc/hosts

에러 뜨는지 test(에러 뜨면 성공)

curl freelec-springboot2-webservice

 

 

 

 

 

 

출처

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - YES24

가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현한다. JPA와 JUnit 테스트, 그레이들, 머스테치, 스프링

www.yes24.com

 

 

+ Recent posts