<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>support-kim.log</title>
        <link>https://velog.io/</link>
        <description>덕업일치</description>
        <lastBuildDate>Sun, 31 Mar 2024 15:40:10 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>support-kim.log</title>
            <url>https://velog.velcdn.com/images/support-kim/profile/e7120301-7f3d-407a-a9e2-4e476c1b6ed4/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. support-kim.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/support-kim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[도커와 CI 환경 - 7]]></title>
            <link>https://velog.io/@support-kim/%EB%8F%84%EC%BB%A4%EC%99%80-CI-%ED%99%98%EA%B2%BD-7</link>
            <guid>https://velog.io/@support-kim/%EB%8F%84%EC%BB%A4%EC%99%80-CI-%ED%99%98%EA%B2%BD-7</guid>
            <pubDate>Sun, 31 Mar 2024 15:40:10 GMT</pubDate>
            <description><![CDATA[<h2 id="설명">설명</h2>
<ul>
<li>이전에 했었던 부분은 애플리케이션을 위한 전체 소스코드를 작성하고 각각에 맞는 Dockerfile 을 작성했으며 그 각각의 컨테이너를 연결시켜주고 간단하게 한 번에 실행시킬 수 있는 Docker compose 를 작성했다.</li>
<li>이번에는 소스코드가 에러가 없는지 테스트를 한 후 테스트에서 성공을 하면 AWS 를 통해서 배포를 하는 것을 해볼 예정이다.</li>
</ul>
<p>데이터베이스를 운영환경에서는 AWS RDS 를 사용해야하기 때문에 이전에 작성했던 mysql 부분을 수정을 먼저 해보자.</p>
<h2 id="도커-환경의-mysql-부분-정리">도커 환경의 MySQL 부분 정리</h2>
<ul>
<li>mysql 이 도커 안에서 돌아가는게 아닌 AWS 에서 돌아가고 있는 것을 우리의 어플리케이션에 연결만 해줄것이니 mysql 을 어플리케이션에 연결해주는 부분 빼고 다 지워준다.</li>
<li>그래서 docker-compose.yml 에서 mysql 부분을 모두 주석 처리만 해주면 된다.</li>
</ul>
<h2 id="github-에-소스-코드-올리기">Github 에 소스 코드 올리기</h2>
<ul>
<li>github 에서 repository 생성</li>
<li>git init -&gt; git add -&gt; git commit -&gt; git remote add origin -&gt; git push origin main</li>
</ul>
<h2 id="travis-ci">Travis CI</h2>
<ul>
<li>Github main 브랜치에 새로 업데이트된 소스가 올라왔다면 그 소스를 Travis CI 에서 가져와야 한다.</li>
</ul>
<h3 id="travis-ci-에서-하는-일들">Travis CI 에서 하는 일들</h3>
<ol>
<li>Github 에 코드 Push</li>
<li>Travis CI 가 자동으로 코드를 가져온다.</li>
<li>가져온 코드로 &quot;테스트&quot; 코드를 실행</li>
<li>성공하면 운영 환경의 이미지를 빌드</li>
<li>빌드된 이미지를 Docker Hub 로 전송</li>
</ol>
<ul>
<li>Docker Hub 에서는 유명한 이미지를 다운로드 받을 수 있고, 자신이 만든 이미지도 업로드 할 수 있다.</li>
<li>Docker Hub 에 빌드된 이미지를 보내고 AWS 에서 그 이미지를 가져간다.</li>
<li>따라서 ElasticBeanStalk 에서 다시 이미지를 빌드하지 않아도 된다.</li>
</ul>
<ol start="6">
<li>AWS EB 에 DockerHub 에 이미지를 보냈다고 알린다.</li>
<li>AWS EB 에서 DockerHub 에 있는 이미지를 가져온 후 배포</li>
</ol>
<h3 id="순서">순서</h3>
<ul>
<li>Travis CI 사이트로 가서 Github 아이디로 로그인</li>
<li>Setting 페이지에서 위에서 올렸던 Repository 를 찾고 Setting 버튼 누른다.</li>
<li>그렇게 되면 Travis CI 에게 Github 저장소의 소스가 변경될 떄 마다 소스를 가져와서 테스트하고 배포하라고 알려준다.</li>
</ul>
<h2 id="travisyml-파일-작성">.travis.yml 파일 작성</h2>
<ol>
<li>파일생성 (.travis.yml)</li>
<li>테스트를 수행하기 위한 준비</li>
</ol>
<ul>
<li>앱을 도커 환경에서 실행하고 있기 때문에 Travis CI 에게 도커 환경으로 만들라고 선언</li>
<li>구성된 도커 환경에서 Dockerfile.dev 를 이용해 도커 이미지 생성</li>
</ul>
<ol start="3">
<li>테스트 수행</li>
</ol>
<ul>
<li>생성된 테스트 이미지를 이용해 테스트 수행</li>
</ul>
<ol start="4">
<li>모든 프로젝트의 운영 버전 이미지 빌드</li>
</ol>
<ul>
<li>테스트에 성공하면 각 프로젝트의 운영 버전 이미지를 빌드하도록 설정</li>
</ul>
<ol start="5">
<li>빌드된 이미지를 도커 허브로 푸시</li>
</ol>
<ul>
<li>빌드된 파일을 도커 허브에 보내기 위해 도커 허브 로그인</li>
<li>빌드된 이미지 도커 허브로 푸시</li>
</ul>
<ol start="6">
<li>배포</li>
</ol>
<ul>
<li>AWS ElasticBeanStalk 이 업데이트된 빌드 이미지를 가져와서 배포할 수 있게 설정<pre><code class="language-yml"># .travis.yml
</code></pre>
</li>
</ul>
<h1 id="언어-플랫폼-선택">언어 (플랫폼) 선택</h1>
<p>language: generic</p>
<h1 id="관리자-권한-갖기">관리자 권한 갖기</h1>
<p>sudo: required</p>
<h1 id="도커-환경-구성">도커 환경 구성</h1>
<p>services:</p>
<ul>
<li>docker</li>
</ul>
<h1 id="스크립트를-실행할-수-있는-환경-구성">스크립트를 실행할 수 있는 환경 구성</h1>
<p>before_install:</p>
<h1 id="dockerfiledev-명시-하기-위해--f-옵션-사용">Dockerfile.dev 명시 하기 위해 -f 옵션 사용</h1>
<ul>
<li>docker build -t supportkim/react-test-app -f ./frontend/Dockerfile.dev ./frontend</li>
</ul>
<h1 id="실행할-스크립트-테스트-실행">실행할 스크립트 (테스트 실행)</h1>
<p>script:</p>
<ul>
<li>docker run -e CI=true supportkim/react-test-app npm run test</li>
</ul>
<h1 id="테스트-성공-후-할일-명시">테스트 성공 후 할일 명시</h1>
<p>after_success:</p>
<h1 id="각각의-이미지-빌드">각각의 이미지 빌드</h1>
<ul>
<li><p>docker build -t supportkim/docker-frontend ./frontend</p>
</li>
<li><p>docker build -t supportkim/docker-backend ./backend</p>
</li>
<li><p>docker build -t supportkim/docker-nginx ./nginx</p>
<h1 id="도커-허브-로그인">도커 허브 로그인</h1>
</li>
<li><p>echo &quot;$DOCKER_HUB_PASSWORD&quot; | docker login -u &quot;$DOCKER_HUB_ID&quot; --password-stdin</p>
<h1 id="빌드-된-이미지들을-도커-허브에-push">빌드 된 이미지들을 도커 허브에 PUSH</h1>
</li>
<li><p>docker push supportkim/docker-frontend</p>
</li>
<li><p>docker push supportkim/docker-backend</p>
</li>
<li><p>docker push supportkim/docker-nginx</p>
<pre><code>- travis.yml파일에 아이디와 비밀번호를 넣으면 노출 위험이 있기 때문에 도커 허브 아이디와 비밀번호를 Travis CI 홈페이지에 미리 넣어줘야 한다.
- Travis CI 에서 Repository 를 찾고 More options -&gt; Settings 에서 환경 변수를 만들어주면 된다. (DOCKER_HUB_ID , DOCKER_HUB_PASSWORD)
- 이렇게 만들면 Travis CI 가 Script 에서 이 변수를 읽을 떄 자동으로 해당하는 값을 가져가서 로그인 할 수 있다.
</code></pre></li>
</ul>
<h3 id="정리">정리</h3>
<ul>
<li>.travis.yml 에서는 도커 환경으로 선언하고 스크립트를 실행하기 이전에 Dockerfile.dev 로 도커 이미지를 만든다.</li>
<li>그 도커 이미지로 테스트를 실행하고 이것이 성공되면 Dockerfile 로 각각의 이미지를 빌드하고 그 이미지들을 도커 허브에 넣는 로직을 수행한다.</li>
</ul>
<h2 id="dockerrunawsjson-이란">Dockerrun.aws.json 이란?</h2>
<ul>
<li>이전에는 Dockerfile 이 하나였기 때문에 Dockerrun.aws.json 을 사용하지 않아도 됐다.</li>
<li>하지만 이번에는 도커 파일이 여러 개를 가지기 떄문에 Dockerrun.aws.json 이 필요하다.</li>
<li>여러가지 도커파일이 있을땐 ElasticBeanStalk 이 어떤 파일을 먼저 실행하고 어떻게 행동을 취해야 하는지 자동으로 프로세스를 해나갈 수 없기 떄문에 우리가 임의로 설정을 해줘야 한다.</li>
<li>그걸 설정해주는 파일이 바로 Dockerrun.aws.json 이다.</li>
<li>즉 여러 개의 도커 파일이 있을 때 어떻게 수행해야할지 알려주는 문서라고 생각하면 된다.</li>
</ul>
<p>EalsticBeanStalk 은 사실 어떻게 컨테이너를 실행해야하는지 모른다.</p>
<ul>
<li>그래서 EB 안에 있는 ECS (Elastic Container Service) 안에 Task 에다가 어떻게 컨테이너를 실행할지 정의 해야하는데 이를 작업 정의 (Trask Definition) 이라고 한다.</li>
<li>또한 작업 정의를 등록할 때는 컨테이너 정의(Container Definition) 을 명시해야 한다.</li>
<li>컨테이너 정의는 Dockerrun.aws.json 에 명시한다.</li>
</ul>
<p>정리하면 작업 정의를 등록하려면 Container Definitoin 을 명시해줘야 하고 Container Definition 은 dockerrun.aws.json 에 명시해주며 도커 Daemon 으로 전해진다.</p>
<h2 id="dockerrunawsjson-파일-작성">Dockerrun.aws.json 파일 작성</h2>
<ol>
<li>Dockerrun.aws.json 파일 생성</li>
<li>Container Definitions 작성<pre><code class="language-json">// Dockerrun.aws.json
{
// Dockerrun 버전 지정
&quot;AWSEBDockerrunVersion&quot; : 2,
// 이 안에서 컨테이너들을 정의
&quot;containerDefinitions&quot; : [
 {
   // 컨테이너의 이름
   &quot;name&quot; : &quot;frontend&quot;,
   // Docker 컨테이너를 구축할 Docker 이미지 이름
   &quot;image&quot; : &quot;supportkim/docker-frontend&quot;,
   // 호스트 이름으로, 이 이름을 이용해서 Docker Compose 를 통해 생성된 다른 컨테이너에서 접근이 가능
   &quot;hostname&quot; : &quot;frontend&quot;,
   // 컨테이너가 실패할 경우 작업을 중지해야 하면 true , 그렇지 않으면 false
   &quot;essential&quot; : false,
   // 컨테이너용으로 예약할 컨테이너 인스턴스에 있는 메모리 양
   &quot;memory&quot; : 128
 } ,
 {
   &quot;name&quot; : &quot;backend&quot;,
   &quot;image&quot; : &quot;supportkim/docker-backend&quot;,
   &quot;hostname&quot; : &quot;backend&quot;,
   &quot;essential&quot; : false,
   &quot;memory&quot; : 128
 } ,
 {
   &quot;name&quot; : &quot;nginx&quot; ,
   &quot;image&quot; : &quot;supportkim/docker-nginx&quot;,
   &quot;hostname&quot; : &quot;nginx&quot;,
   &quot;essential&quot; : true,
   // 컨테이너에 있는 네트워크 지점을 호스트에 있는 지점에 매핑
   &quot;portMappings&quot; : [
     {
       &quot;hostPort&quot; : 80,
       &quot;containerPort&quot; : 80
     }
   ] ,
   // 연결할 컨테이너의 목록으로, 연결된 컨테이너는 서로를 검색하고 안전하게 통신할 수 있다.
   &quot;links&quot; : [&quot;frontend&quot; , &quot;backend&quot;],
   &quot;memory&quot; : 128
 }
]
}</code></pre>
</li>
</ol>
<ul>
<li>각 컨테이너들의 작업 정의를 해주면 된다.</li>
<li>nginx 는 매우 중요하기 때문에 essential 을 true 를 줬고, nginx 를 통해 각 서버로 가기 때문에 links 도 설정해준다.</li>
</ul>
<h2 id="다중-컨테이너-앱을-위한-elastic-beanstalk-환경-생성">다중 컨테이너 앱을 위한 Elastic BeanStalk 환경 생성</h2>
<ul>
<li>AWS ElasticBeanStalk 에 들어가서 Create Application 을 한다.</li>
<li>적절한 애플리케이션 이름을 넣어주고 플랫폼은 Docker 를 지정하고 플랫폼 브랜치는 Multi-container 로 해주면 된다.</li>
</ul>
<h2 id="vpcvirtual-private-cloud-와-security-group-설정">VPC(Virtual Private Cloud) 와 Security Group 설정</h2>
<ul>
<li>AWS 의 RDS 를 이용하여 MySQL 을 애플리케이션과 연결시켜야 하는데 그것을 하기 위해서 VPC 와 Security Group 을 설정해줘야 한다.</li>
</ul>
<p>그렇다면 왜 VPC 와 Security Group 을 설정해줘야 할까?</p>
<ul>
<li>ElasticBeanStalk -------(X)-------&gt; RDS(MySQL)</li>
<li>AWS 에서 기본적으로 연결돼 있지 않아서 서비스간에 통신할 수 없기 떄문에 따로 설정해서 연결해야 한다.</li>
</ul>
<h3 id="vpc-란">VPC 란?</h3>
<ul>
<li>VPC 를 사용하면 AWS 클라우드에서 논리적으로 격리된 공간을 제공하여 고객이 정의하는 가상 네트워크에서 AWS 리소스를 시작할 수 있다.</li>
<li>즉 내가 만든 EC2 인스턴스나 RDS , EB 에 나의 아이디에서만 접근이 가능하게 논리적으로 격리된 네트워크에서 생성이 되게 해주는 것이다.</li>
<li>그렇기 때문에 다른 아이디로 접근하는 것은 물론 보는것도 불가능하게 된다.</li>
<li>ElasticBeanStalk 인스턴스나 RDS 를 생성하면 자동적으로 기본 VPC 가 할당이 되고 할당이 될때는 지역별로 다르게 할당이 된다.</li>
</ul>
<h3 id="security-group-보안-그룹-이란">Security Group (보안 그룹) 이란?</h3>
<ul>
<li>보안 그룹에는 인바운드와 아웃 바운드라는게 존재한다.</li>
<li>인바운드는 외부에서 EC2 인스턴스나 EB 인스턴스로 요청을 보내는 트래픽으로 HTTP , HTTPS , SSH 등이 있다.</li>
<li>아웃 바운드는 EC2 인스턴스나 EB 인스턴스등에서 외부로 나가는 트래픽으로, 파일을 다운로드 하거나 인 바운드로 들어온 트래픽을 처리하여 응답하는 경우도 포함된다.</li>
</ul>
<p>즉 Security Group 이 인바운드와 아웃바운드를 통제해서 트래픽을 열어줄 수 있고 닫아 줄 수도 있다.</p>
<p>그렇다면 어떻게 VPC 와 Security Group 을 이용해서 EB 인스턴스와 RDS 가 서로 통할 수 있게 만들 수 있을까?</p>
<ul>
<li>같은 VPC 안에 AWS 서비스 간에는 트래픽을 모두 허용할 수 있는 Security Group 을 만들어주면 된다.</li>
<li>즉 EB 인스턴스와 RDS 가 존재하는 VPC 에 오는 트래픽은 모두 허용하는 보안 그룹을 만들면 된다.</li>
</ul>
<h2 id="mysql-을-위한-aws-rds-생성">MySQL 을 위한 AWS RDS 생성</h2>
<ul>
<li>AWS RDS 에서 데이터 베이스를 생성하고 엔진 옵션을 선택하고 USERNAME , PASSWORD 을 설정해준다.</li>
<li>데이터베이스 옵션에서 초기 데이터베이스 이름도 설정할 수 있고, 자동 백업 활성화도 체크해준다.</li>
</ul>
<h2 id="security-group-생성-및-적용">Security Group 생성 및 적용</h2>
<ul>
<li>보안 그룹 생성할 때 인바운드 규칙은 MySQL(3306) 만 추가해주면 된다.</li>
<li>MySQL 과 소통을 해야하기 때문에 MySQL 이 실행되고 있는 Port 인 3306 을 적어주면 된다.</li>
</ul>
<h3 id="rds-에-생성한-보안-그룹-적용">RDS 에 생성한 보안 그룹 적용</h3>
<ul>
<li>AWS RDS 에 들어가서 생성한 RDS 에서 위에서 만든 보안 그룹을 추가해주면 된다.</li>
</ul>
<h3 id="eb-에-생성한-보안-그룹-적용">EB 에 생성한 보안 그룹 적용</h3>
<ul>
<li>AWS EB 에 들어가고 만들었던 환경 이름을 클릭한 후 구성에 들어간다.</li>
<li>인스턴스 편집을 들어가고 위에서 생성한 보안 그룹을 추가하고 적용해주면 된다.</li>
</ul>
<h2 id="eb-와-rds-소통을-위한-환경-변수-설정">EB 와 RDS 소통을 위한 환경 변수 설정</h2>
<ul>
<li>보안 그룹까지 다 설정했지만, 아직은 ElasticBeanStalk 안에 있는 컨테이너들이 MySQL 인스턴스와 소통할 때 환경변수 부분을 인식하지 못한다.</li>
<li>그래서 AWS ElasticBeanStalk 에 환경변수를 설정하므로써 그 부분의 문제를 해결해보자.</li>
<li>AWS ElasticBeanStalk 에 들어가고 구성에서 소프트웨어 편집을 눌러준다.</li>
<li>환경 속성에 MYSQL_HOST (RDS EndPoint) , MYSQL_USER , MYSQL_PASSWORD , MYSQL_DATABASE , MYSQL_PORT 와 같은 정보들을 넣어준다.</li>
<li>이렇게 설정하면 EB 와 RDS 가 소통할 때 해당 환경변수를 참고하면서 소통을 할 수 있다.</li>
</ul>
<h2 id="travisyml-파일-작성-배포">travis.yml 파일 작성 (배포)</h2>
<ul>
<li>현재 각각의 필요한 이미지들을 빌드한 후 도커 허브까지 넣어줬다.</li>
<li>이제는 AWS 에서 배포를 위해 필요한 설정들을 travis.yml 파일에 작성해주면 된다.<pre><code class="language-yml"># .travis.yml 에 추가
deploy:
# 외부 서비스 표시 (s3 , firebase , eb 등)
provider: elasticbeanstalk
# 현재 사용하고 있는 AWS의 서비스가 위치하고 있는 물리적인 위치
region: &quot;ap-northeast-2&quot;
# 생성된 어플리케이션의 이름
app: &quot;docker-fullstack-app&quot;
# env 이름
env: &quot;DockerFullstackApp-env&quot;
# 해당 EB 를 위한 S3 버켓 이름
bucket_name: &quot;버킷 이름&quot;
# 어플리케이션의 이름과 동일
bucket_path: &quot;app 이름과 동일&quot;
on:
# 어떤 브랜치에 Push 를 할 떄 AWS 에 배포를 할 것인지 설정
  branch: main</code></pre>
</li>
<li>대부분의 설정이 끝났지만 이렇게 아무런 인증 없이는 Travis CI 에서 마음대로 AWS 과 소통할 수 없다.</li>
<li>이제는 Travis CI 가 AWS 에 접근할 수 있게 해주는 방법을 알아보자.</li>
</ul>
<h2 id="travis-ci-의-aws-접근을-위한-api-key-생성">Travis CI 의 AWS 접근을 위한 API key 생성</h2>
<ul>
<li>현재까지 Travis CI 에서 AWS 에 어떤 파일을 전해줄 것이며, AWS 에서 어떤 서비스를 이용할 것이며, 부수적인 설정들을 넣어줬다.</li>
<li>하지만 Travis CI 와 AWS 가 실질적으로 소통을 할 수 있게 인증하는 부분을 설정해주진 않았다.</li>
</ul>
<p>인증을 위해서는 API Key 가 필요하다.</p>
<ul>
<li>API Key 를 받기 위해서는 IAM USER 를 생성해야 한다.</li>
<li>AWS IAM 에서 사용자 추가를 한다.</li>
<li>사용자 세부 설정 정보에서 적절한 이름을 넣어주고 프로그래밍 방식 엑세스를 선택한다.</li>
<li>현재 EB 를 사용하기 때문에 기존 정책 직접 연결에서 AWSElasticBeanstalkFullAccess 를 선택한다.</li>
<li>그렇게 만든 USER 에서 API Key 를 만들면 된다.</li>
<li>해당 API Key 는 .travis.yml 에 적어 주면 노출이 되기 떄문에 다른 곳에 적고 그것을 가져와야 한다.</li>
<li>그래서 Travis 에서 More options -&gt; Settings 에 AWS_ACCESS_KEY , AWS_SECRET_ACCESS_KEY 추가 하고 .travis.yml 파일에 아래 내용을 추가해주면 된다.<pre><code class="language-yml">access_key_id: $AWS_ACCESS_KEY
secret_access_key: $AWS_SECRET_ACCESS_KEY</code></pre>
main 에 push 하면 travis CI 에서 테스트를 진행하고 AWS EB 로 전달되서 업데이트를 하고 나면 정상적으로 실행이 된다.</li>
</ul>
<h2 id="travis-ci-에서-github-action-으로-교체">Travis CI 에서 Github Action 으로 교체</h2>
<ol>
<li>.travis.yml 을 삭제 후 github action 의 deploy.yaml 로 교체</li>
</ol>
<ul>
<li>.github/workflows/deploy.yaml <pre><code class="language-yml"># deploy.yaml
name: Deploy FullStackApp
</code></pre>
</li>
</ul>
<p>on:
  push:
    branches:
      - main</p>
<p>jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      # Travis CI 와 같이 도커에 로그인
      - run: echo &quot;${{ secrets.DOCKER_PASSWORD }}&quot; | docker login -u &quot;${{ secrets.DOCKER_USERNAME }}&quot; --password-stdin
      # before_install
      - run: docker build -t supportkim/react-test-app -f ./frontend/Dockerfile.dev ./frontend
      # 테스트 하는 코드 (script)
      - run: docker run -e CI=true supportkim/react-test-app npm test</p>
<pre><code>  # after_success
  - run: docker build -t supportkim/docker-frontend ./frontend
  - run: docker build -t supportkim/docker-nginx ./nginx
  - run: docker build -t supportkim/docker-backend ./backend

  - run: docker push supportkim/docker-frontend
  - run: docker push supportkim/docker-nginx
  - run: docker push supportkim/docker-backend

  # ElasticBeanStalk 에 전달
  - name: Generate deployment package
    run: zip -r deploy.zip . -x &#39;*.git*&#39;

  - name: Deploy to EB
    uses: einaregilsson/beanstalk-deploy@v18
    with:
      aws_access_key: ${{ secrets.AWS_ACCESS_KEY }}
      aws_secret_key: ${{ secrets.AWS_SECRET_KEY }}
      # 바꿔줘야 하는 부분 1
      application_name: fullstack-docker-app
      # 바꿔줘야 하는 부분 2
      environment_name: Fullstack-docker-app-env
      # 바꿔줘야 하는 부분 3
      existing_bucket_name: elasticbeanstalk-ap-northeast-2-637423586273
      region: ap-northeast-2
      version_label: ${{ github.sha }}
      deployment_package: deploy.zip</code></pre><pre><code>```yml
# docker-compose.yml
version: &quot;3&quot;
services:
  frontend:
    image: supportkim/docker-frontend
    volumes:
      - /app/node_modules
      - ./frontend:/app
    stdin_open: true
    mem_limit: 128m

  nginx: 
    restart: always
    image: supportkim/docker-nginx
    ports: 
      - &quot;80:80&quot;

  backend:
    image: supportkim/docker-backend
    container_name: app_backend
    volumes:
      - /app/node_modules
      - ./backend:/app
    mem_limit: 128m
    environment: 
      MYSQL_HOST: $MYSQL_HOST 
      MYSQL_USER: $MYSQL_USER 
      MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD
      MYSQL_DATABASE: $MYSQL_DATABASE
      MYSQL_PORT: $MYSQL_PORT   </code></pre><ol start="2">
<li>역할 생성</li>
</ol>
<ul>
<li>역할 생성에 들어가서 AWSElasticBeanstalkMulticontainerDocker , AWSElasticBeanstalkWebTier , AWSElasticBeanstalkWorkerTier 3개 추가</li>
</ul>
<ol start="3">
<li>ElasticBeanStalk 만들기</li>
</ol>
<ul>
<li>ElasticBeanStalk 에서 애플리케이션 생성</li>
<li>적절한 이름 생성 후 서비스 액세스에서 새 서비스 역할 생성 및 사용을 누르고 EC2 인스턴스 프로파일에서 2번에서 생성한 역할을 지정해준다.</li>
</ul>
<ol start="4">
<li>IAM 유저 생성</li>
</ol>
<ul>
<li>AdministratorAccess-AWSElasticBeanstalk 정책을 가지는 유저 생성</li>
<li>생성한 후 보안 증명에 들어가서 Key 를 발급 후 보관</li>
</ul>
<ol start="5">
<li>GitHub Repository 생성</li>
</ol>
<ul>
<li>Github 에 저장소를 만들고 해당 코드들을 다 PUSH 해준다.</li>
<li>Repository 에서 Settings -&gt; Secrets and variables -&gt; Actions 로 가서 값을 하나씩 넣어주면 된다.</li>
<li>AWS_ACCESS_KEY , AWS_SECRET_KEY , DOCKER_USERNAME , DOCKER_PASSWORD</li>
</ul>
<ol start="6">
<li><p>RDS 생성</p>
</li>
<li><p>보안 그룹 생성</p>
</li>
</ol>
<ul>
<li>인바운드 규칙에 3306 만 추가</li>
<li>EB , RDS 에 해당 보안 그룹을 적용시키면 된다.</li>
<li>EB 는 구성에서 인스턴스 트래픽 및 크기 조정에서 보안그룹을 정해주면 된다.</li>
</ul>
<ol start="8">
<li>환경변수 설정</li>
</ol>
<ul>
<li>docker-compose.yml 을 이용해서 EB 에서 도커 컨테이너를 실행한다.</li>
<li>이때 docker-compose.yml 안에 있는 DB 관련 환경변수들을 EB 에 넣어줘야 한다.</li>
<li>구성 -&gt; 업데이트 , 모니터링 및 로깅 편집 -&gt; 환경속성에서 환경변수 추가</li>
<li>MYSQL_HOST , MYSQL_USER , MYSQL_ROOT_PASSWORD , MYSQL_DATABASE , MYSQL_PORT 값들을 추가 해줬다.</li>
</ul>
<p>이렇게 GitAcation 으로 CI/CD 파이프라인을 구축할 수 있다.</p>
<p><a href="https://www.inflearn.com/course/%EB%94%B0%EB%9D%BC%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8F%84%EC%BB%A4-ci/dashboard">참고자료</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커와 CI 환경 - 6]]></title>
            <link>https://velog.io/@support-kim/%EB%8F%84%EC%BB%A4%EC%99%80-CI-%ED%99%98%EA%B2%BD-6</link>
            <guid>https://velog.io/@support-kim/%EB%8F%84%EC%BB%A4%EC%99%80-CI-%ED%99%98%EA%B2%BD-6</guid>
            <pubDate>Sun, 31 Mar 2024 11:07:38 GMT</pubDate>
            <description><![CDATA[<p>이번에는 프론트 부분만을 사용하는게 아니라 백엔드 서버 + DB 까지 포함시켜서 만들어본다.</p>
<h2 id="설명">설명</h2>
<ul>
<li>아무 글이나 입력하면 리액트를 통해서 노드로 전달된 이후에 MySQL 데이터베이스에 저장한 후 그 저장된 것을 화면에 보여주는 앱을 만들어본다.</li>
<li>컨테이너를 재시작해도 DB 에 저장된 데이터는 남아있게 된다.</li>
</ul>
<h3 id="multi-container-앱을-위한-설계-방식">Multi Container 앱을 위한 설계 방식</h3>
<ol>
<li>Nginx 로 클라이언트에서 오는 요청을 백엔드 서버와 프론트 서버로 나눠주는 구조</li>
<li>Nginx 는 프론트 서버로만 사용하여 클라이언트에서 정적 파일을 요구할 때 제공해주는 구조</li>
</ol>
<p>1번 방식</p>
<ul>
<li>클라이언트(브라우저) 의 요청이 Nginx(프록시) 로 가고 여기에서 라우팅 역할을 하게 된다.</li>
<li>프론트 엔드 서버와 백엔드 서버를 가기 위해서는 Nginx 를 거친다.</li>
<li>장점은 Request 를 보낼 떄 URL 부분에 HOST 이름이나 포트가 바뀌어도 변경을 하지 않아도 된다.</li>
<li>즉 axios.get(&quot;/api/values&quot;) 처럼 api 경로만 넣어준다.</li>
<li>단점으로는 Nginx 설정이나 전체 설계가 복잡하다.</li>
</ul>
<p>2번 방식</p>
<ul>
<li>클라이언트(브라우저) 의 요청이 오면 요청된 PORT 에 따라서 프론트 서버나 백엔드 서버로 가게 된다.</li>
<li>nginx 는 정적 파일을 보여줄 때만 사용된다.</li>
<li>장점으로는 설계가 다소 간단하고 구현하는데 쉽다.</li>
<li>단점으로는 HOST 이름이나 포트 변경이 있을 때 Request URL 도 변경되어야 한다.</li>
<li>axios.get(&quot;<a href="http://localhost:5000/api/values&quot;">http://localhost:5000/api/values&quot;</a>) 로 하다가 만약 포트 번호가 바뀌게 된다면 수정을 해줘야한다.</li>
</ul>
<h3 id="전체적인-구현-순서">전체적인 구현 순서</h3>
<ol>
<li>애플리케이션 소스 코드 작성</li>
<li>도커 파일 작성 (개발 환경 + 운영 환경 총 2개)</li>
<li>도커 컴포즈 파일 작성 (frontend , backend , nginx , mysql)</li>
<li>github 에 push (메인 브랜치로 병합)</li>
<li>Travis CI (테스트 소스 실행 후 성공하면 각 도커 파일을 이용해 이미지를 생성하고 도커 허브로 전달)</li>
</ol>
<ul>
<li>전에는 도커 허브로 전달하지 않았지만 이번엔 도커 허브로 전달해본다.</li>
</ul>
<ol start="6">
<li>도커 허브</li>
</ol>
<ul>
<li>Travis CI 에서 빌드된 이미지를 보관하고 AWS ElasticBeanStalk 에서 가져가려고 할 때 전달해준다.</li>
</ul>
<ol start="7">
<li>AWS ElasticBeanStalk 에 배포</li>
</ol>
<h2 id="node-js-구성">Node JS 구성</h2>
<ul>
<li>폴더안에 backend , frontend 폴더 생성</li>
<li>backend 에서 npm init</li>
</ul>
<h3 id="구현-순서">구현 순서</h3>
<ol>
<li>package.json 파일안에 스크립트와 사용할 모듈들 명시</li>
</ol>
<ul>
<li>express , mysql , nodemon , body-parser(요청 본문 해석)</li>
</ul>
<ol start="2">
<li>server.js 구현</li>
<li>mysql 연결을 위한 ds.js 구현</li>
<li>host , usrename , password 명시하고 pool 생성</li>
</ol>
<ul>
<li>이렇게 만든 pool 을 다른 부분에서도 사용할 수 있게 export</li>
</ul>
<ol start="5">
<li>export 된 pool 을 server.js 에서 불러오기</li>
<li>2가지 API 설계</li>
</ol>
<ul>
<li>데이터 노출 (GET)</li>
<li>데이터 입력 (POST)</li>
</ul>
<h2 id="react-js-구성">React JS 구성</h2>
<ul>
<li>npx create-react-app frontend 로 frontend 파일에 리액트 설치</li>
</ul>
<h3 id="구현-순서-1">구현 순서</h3>
<ol>
<li>UI 를 위한 코드 작성 (데이터 입력할 수 있는 Form)</li>
<li>데이터의 흐름을 위한 State 생성</li>
</ol>
<ul>
<li>usetState 을 사용하기 위해 react 라이브러리에서 가져오기</li>
<li>lists : 데이터베이스에 저장된 값을 가져와서 화면에 보여주기 전 이 State 에 넣어둔다.</li>
<li>value : Input 박스에 입력한 값이 이 state 에 들어간다.</li>
</ul>
<ol start="3">
<li>데이터 베이스에서 데이터를 가져오기 위해 필요한 userEffect 사용</li>
</ol>
<ul>
<li>useEffect 을 사용하기 위해 react 라이브러리에서 가져오기</li>
<li>useEffect 는 데이터베이스에 있는 값을 가져오기위해 사용된다.</li>
</ul>
<ol start="4">
<li>이벤트 핸들러 처리</li>
</ol>
<ul>
<li>changeHandler : input 박스에 입력할 때 onChange Event 가 발생하는데 value State 을 변화 시켜준다.</li>
<li>submitHandler : 값을 input 박스에 입력하고 확인 버튼을 누르면 입력한 값이 데이터 베이스에 저장되고 그 후에 화면에 표출도 시켜준다.</li>
</ul>
<h2 id="리액트-앱을-위한-도커-파일-만들기">리액트 앱을 위한 도커 파일 만들기</h2>
<ol>
<li>frontend 파일안에 Dockerfile , Dockerfile.dev 생성<pre><code># Dockerfile.dev (개발 환경)
FROM node:alpine
</code></pre></li>
</ol>
<p>WORKDIR /app</p>
<p>RUN npm install</p>
<p>COPY ./ ./</p>
<p>CMD [ &quot;npm&quot; , &quot;run&quot; , &quot;start&quot; ]</p>
<hr>
<h1 id="dockerfile-운영-환경">Dockerfile (운영 환경)</h1>
<p>FROM node:alpine as builder</p>
<p>WORKIDR /app</p>
<p>COPY ./package.json ./</p>
<p>RUN npm install</p>
<p>COPY . .</p>
<p>RUN npm run build</p>
<p>FROM nginx</p>
<p>EXPOSE 3000</p>
<p>COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf</p>
<p>COPY --from=builder /app/build /usr/share/nginx/html</p>
<pre><code>- FROM nginx 전까지는 Nginx 가 제공을 해줄 build 파일들을 생성하는 단계
- 프론트 부분에서 정적 파일들을 가져다주는 nginx 에 대한 부분
- COPY 부분은 밑에 나올 default.conf 에서 해준 설정을 nginx 컨테이너 안에 있는 설정이 되게 복사를 해준다.

정적 파일을 제공해주기 위한 Nginx 를 위해서 frontend 안에 nginx 파일을 생성하고 default.conf 파일을 생성한다.</code></pre><h1 id="defaultconf">default.conf</h1>
<p>server {
    listen 3000;
    location / {
        root /usr/share/nginx/html; # HTML 파일이 위치할 루트 설정
        index index.html index.htm; # 사이트의 index 페이지로 할 파일명 설정
        try_files $uri $uri/ /index.html #React Router 를 사용해서 페이지간 이동할 떄 사용
   }
}</p>
<pre><code>
개발환경을 위한 Dockerfile.dev 는 이해하기 어렵지 않다.

운영환경을 위한 Dockerfile 을 생각해보면 Nginx 가 2가지의 역할을 한다.
- 첫 번째는 라우팅 하는 역할 , 두 번째는 정적 파일을 제공하는 역할
- 이러한 역할들을 하기 위해서는 nginx 에게 Build 파일을 제공해줘야 한다.
- 그게 바로 as builder 부분이다. (COPY --from=builder...)
- 뿐만 아니라 default.conf 에서 설정 한것도 nginx 가 알고 있어야 하기 때문에 그 부분도 COPY 를 해줘야한다.
- default.conf 에는 정적 파일들이 어디에 위치해있고, 라우팅을 할 때 필요한 부분들이 정의되어 있다.

그래서 nginx 의 역할을 하기 위해 필요한 정보들을 COPY 할 수 있도록 Dockerfile 을 만든다고 생각하면 된다.

## 노드 앱을 위한 도커 파일 만들기
1. 개발환경을 위한 Dockerfile.dev 작성</code></pre><p>FROM node:alpine</p>
<p>WORKDIR /app</p>
<p>COPy ./package.json ./</p>
<p>RUN npm install</p>
<p>COPY . .</p>
<p>CMD [&quot;npm&quot; , &quot;run&quot; , &quot;dev&quot;]</p>
<pre><code>- CMD 에서 start 가 아닌 dev 인 이유는 코드가 변경될 때 바로 반영을 시켜주게 해 주는 nodeman 이라는 모듈을 사용하고 싶기 떄문에 dev 로 한다.
2. 운영환경을 위한 Dockerfile 작성</code></pre><p>FROM node:alpine</p>
<p>WORKDIR /app</p>
<p>COPY ./package.json ./</p>
<p>RUN npm install</p>
<p>COPY . .</p>
<p>CMD [&quot;npm&quot; , &quot;run&quot; , &quot;start&quot;]</p>
<pre><code>
리액트는 nginx 부분까지 생각해야 했기 때문에 조금 길었지만 노드 앱을 위한 도커 파일은 어렵지 않게 작성할 수 있다.

## DB
- Mysql 데이터베이스를 위해 도커 파일을 만들어야 한다.
- 개발 환경과 운영 환경 각각에서 DB 구성을 어떻게 해야할지 알아보자.

개발 환경 ----------&gt; 도커 환경
운영 환경 ----------&gt; AWS RDS 서비스 이용

### 굳이 이렇게 나누는 이유?
- DB 작업은 중요한 데이터들을 보관하고 이용하는 부분이기 때문에 조금의 실수로도 안 좋은 결과를 얻을 수 있다.
- 실제 중요한 데이터들을 다루는 운영환경에서는 더욱 안정적인 AWS RDS 를 이용하여 DB 를 구성해 보는 것이 실제로 실무에서 더 보편적으로 쓰이는 방법이기에 이렇게 개발 환경과 운영환경을 나눈다.

개발환경에서는 ElasticBeanStalk 안에 MySQL 까지 모두 들어간다.
운영환경에서는 ElasticBeanStalk 안에 MySQL 이 들어가지 않고 그 밖에서 AWS RDS 로 연결된다.

## MySQL 을 위한 도커 파일 만들기
- 원래 데이터베이스를 사용하려면 먼저 데이터베이스 설치 파일을 다운로드하고 그걸 이용해서 데이터베이스를 설치하고 Node 앱에 연결을 해줘야하지만, 지금은 설치를 하지 않았다.
- 그래서 도커 이미지를 이용해서 설치를 해야한다.

1. mysql 폴더 만들고 Dockerfile 작성</code></pre><p>FROM mysql:5.7 # 베이스 이미지를 도커 허브에서 가져온다.
ADD ./my.cnf /etc/mysql/conf.d/my.cnf # 이 부분은 2,3번 가보면 알 수 있다.</p>
<pre><code>2. mysql 을 시작할 때 Database 와 Table 이 필요한데 만들 장소 생성
- mysql 에서 sqls 폴더 만들고 initalize.sql 파일 생성
- 여기에 CREATE DATABSE , USE , CREATE TABLE 와 같은 쿼리문 작성
3. 한글도 저장할 수 있도록 설정 추가
- 현재는 어떠한 글을 데이터베이스에 넣어줄 때 한글이 깨지기 때문에 오류가 발생한다.
- 그래서 mysql 폴더안에 my.conf 라는 파일을 생성하고 아래와 같이 작성한다.</code></pre><h1 id="myconf">my.conf</h1>
<p>[mysqld]
character-set-server=utf8</p>
<p>[mysql]
default-character-set=utf8</p>
<p>[client]
default-character-set=utf8</p>
<pre><code>- 한글이 깨지는 현상을 막기 위해 utf8 로 인코딩할 수 있게 설정
- 실제 mysql 설정을 해주는 my.conf 파일로 덮어 씌우기 위해서 Dockerfile 에 ADD 가 들어가는 것이다.
- 즉 이렇게 생성한 my.conf 를 실제 mysql 설정에도 적용시키는 것

실제로 mysql status 로 확인을 해보면 server , db , client characterset 이 원래는 latine1 이지만 utf8 로 바뀐다.

또한 Dockerfile 과 Dockerfile.dev 가 특별하게 다르게 해 줄 이유가 없다면 같은 내용이 들어간다.

## NGINX 를 위한 도커 파일 만들기
- 현재 Nginx 가 쓰이는 곳은 2가지이며 서로 다른 이유로 쓰인다.
- 첫 번째는 Proxy , 두 번째는 Static 파일 제공
- 클라이언트에 요청을 보낼 때 정적 파일을 원할 때는 Nginx 의 설정에 따라 자동적으로 React JS 로 보내주고 API 요청일 경우에는 Node JS 로 보내준다.
- 요청을 나눠서 보내는 기준은 location 이 / 로 시작하는지 , /api 로 시작하는지로 판단한다.
- / 로 시작하면 프론트서버에게 , /api 로 시작하면 백엔드 서버에게 보낸다.

이러한 Proxy 기능을 위해 Nginx 설정을 해야한다.
1. nginx 폴더 만들고 default.conf , Dockerfile 생성
- 위에서 만들었던 frontend 폴더 안에 nginx 와는 다른 역할을 하는 nginx 이다.
- frontend 안에 있는 nginx 는 정적 파일 제공
- 이번에 만드는 nginx 는 Proxy 


2. default.conf 파일에 프록시 기능 작성</code></pre><h1 id="3000번-포트에서-frontend-가-돌아가고-있다는-것을-명시">3000번 포트에서 frontend 가 돌아가고 있다는 것을 명시</h1>
<p>upstream frontend {
    server frontend:3000;
}</p>
<h1 id="5000번-포트에서-backend-가-돌아가고-있다는-것을-명시">5000번 포트에서 backend 가 돌아가고 있다는 것을 명시</h1>
<p>upstream backend {
    server backend:5000;
}</p>
<p>server {
    # nginx 서버 포트 80 번으로 열기
    listen 80;</p>
<pre><code># location 에는 우선순위가 있는데, 그냥 / 이렇게만 되는 것은 우선순위가 가장 낮다.
# /api 로 시작하는 것을 먼저 찾고 그게 없다면 / 이렇게 시작되는 것 이기 떄문에
# 그 요청을 http://frontend 에게 보낸다.
location / {
    proxy_pass http://frontend
}

location /api {
    proxy_pass http://backend;
}

# 이 부분이 없다면 에러가 발생
location /sockjs-node {
    proxy_pass http://frontend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection &quot;Upgrade&quot;;
}</code></pre><p>}</p>
<pre><code>- 바로 뒤에 docker-compose 파일을 작성할 때 services: 부분에 frontend: , backend: 로 만들 예정이기 때문에 http://frontend , http://backend 로 설정해도 된다.
- 도커 파일을 이용하기 떄문에 이처럼 가능한 것이지 원래는 도메인 주소나 IP 주소를 넣어줘야한다.
3. Nginx 를 위한 Dockerfile 작성</code></pre><p>FROM nginx
COPY ./default.conf /etc/nginx/conf.d/default.conf</p>
<pre><code>- 위에서 작성한 conf 파일을 컨테이너에서 실행될 Nginx 에도 적용될 수 있게 COPY 한다.

## Docker Compose 파일 작성
- 각각의 컨테이너를 위한 도커파일을 작성했지만 컨테이너들을 따로 작동시킨다면 서로 통신을 할 수 없다.
- 그래서 컨테이너들을 서로 연결시켜주기 위해서 Docker Compose 를 작성해보자.
- backend , frontend , mysql , nginx 컨테이너 연결
```yml
version: &#39;3&#39;
services:
  frontend:
  # 개발환경을 위한 Dockerfile 이 어디있는지 알려준다.
    build:
      dockerfile: Dockerfile.dev
      context: ./frontend
  # 코드를 수정 후 다시 이미지를 build 하지 않고도 코드가 반영될 수 있게 volume 사용
    volumes:
      - /app/node_modules
      - ./frontend:/app
  # 리액트 앱을 종료할 떄 나오는 버그를 잡을 수 있도록 하는 코드
    stdin_open: true

  nginx:
  # 재시작 정책
    restart: always
    build:
      dockerfile: Dokerfile.dev
      context: ./nginx
    ports:
      - &quot;3000:80&quot;

  backend:
    build:
      dockerfile: Dockerfile.dev
      context: ./backend
    container_name: app_backend
    volumes:
      - /app/node_moudles
      - ./backend:/app

  mysql:
    build: ./mysql
    restart: unless-stopped
    container_name: app_mysql
    ports:
      - &quot;3306:3306&quot;
    # 이부분은 뒤에서 설명
    volumes:
      - ./mysql/mysql_data:/var/lib/mysql
      - ./mysql/sqls/:/docker-entrypoint-initdb.d/
    environment:
      MYSQL_ROOT_PASSWORD: jiwon
      MYSQL_DATABASE: myqpp
</code></pre><h3 id="재시작-정책">재시작 정책</h3>
<ul>
<li>no : 어떠한 상황에서도 재시작 하지 않는다.</li>
<li>always : 항상 재시작 한다.</li>
<li>on-failure : 에러 코드와 함께 컨테이너가 멈췄을 때만 재시작</li>
<li>unless-stopped : 개발자가 임의로 멈추려고 할 때 뺴고는 항상 재시작</li>
<li>docker-compose up 실행</li>
</ul>
<h2 id="volume-을-이용한-데이터베이스-데이터-유지">Volume 을 이용한 데이터베이스 데이터 유지</h2>
<pre><code class="language-yml"># mysql 설정 부분 코드
    volumes:
      - ./mysql/mysql_data:/var/lib/mysql
      - ./mysql/sqls/:/docker-entrypoint-initdb.d/</code></pre>
<ul>
<li>해당 volumes 는 무엇 때문에 사용하는 걸까?</li>
<li>현재까지는 volume 을 사용하는 용도는 리액트나 노드쪽에서 코드를 업데이트할 때 바로 그 고크다 애플리케이션에 적용이 될 수 있게 해주기 위해서 사용했다.</li>
<li>이번에는 데이터베이스의 저장된 자료를 컨테이너를 지우더라도 자료가 지워지지 않을 수 있게 해주기 위한 volume 이라고 생각하면 된다.</li>
</ul>
<p>원래는 컨테이너를 지우면 컨테이너에 저장된 데이터들이 지워진다.</p>
<ol>
<li>도커 이미지로 컨테이너를 생성한다.</li>
<li>이때 도커 이미지는 읽기 전용이 된다.</li>
<li>데이터가 추가될 때는 컨테이너 안에 데이터가 저장된다.</li>
<li>컨테이너 삭제할 때 컨테이너 안에 저장된 데이터도 함께 삭제된다.</li>
</ol>
<p>그렇기 때문에 컨테이너를 삭제하면 컨테이너 안에 저장된 데이터까지 삭제가 된다면 영속성이 필요한 데이터들은 어떻게 처리해야할까?</p>
<p>이를 위해서 volumes: 를 설정해줘야 한다.</p>
<ul>
<li>볼륨이란 도커 컨테이너에 의해 생성되고 사용되는 지속적인 데이터를 위한 선호 메커니즘</li>
<li>볼륨을 이용하면 데이터 영속성을 보장할 수 있다.</li>
<li>도커 컨테이너 안에 데이터는 호스트 파일 시스템(DockerArea) 와 연결이 되어 있다.</li>
<li><blockquote>
<p>DockerArea 의 위치는 /var/lib/docker/volumes/</p>
</blockquote>
</li>
<li>컨테이너에서 변화가 일어난 데이터가 컨테이너 안에 저장되는것이 아니라 호스트 파일 시스템에 저장되고 그중에서도 도커에 의해서만 통제가 되는 DockerArea 에 저장이 되므로 컨테이너를 삭제해도 변환된 데이터는 사라지지 않는다.</li>
</ul>
<p><a href="https://www.inflearn.com/course/%EB%94%B0%EB%9D%BC%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8F%84%EC%BB%A4-ci/dashboard">참고자료</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS 배포 하기 - 4 (S3 + CloudFront)]]></title>
            <link>https://velog.io/@support-kim/AWS-%EB%B0%B0%ED%8F%AC-%ED%95%98%EA%B8%B0-4-S3-CloudFront</link>
            <guid>https://velog.io/@support-kim/AWS-%EB%B0%B0%ED%8F%AC-%ED%95%98%EA%B8%B0-4-S3-CloudFront</guid>
            <pubDate>Fri, 29 Mar 2024 01:13:04 GMT</pubDate>
            <description><![CDATA[<h2 id="웹-페이지-배포하기-s3--cloudfront">웹 페이지 배포하기 (S3 , CloudFront)</h2>
<h3 id="웹-서비스를-배포할-때-사용하는-s3--cloudfront">웹 서비스를 배포할 때 사용하는 S3 , CloudFront</h3>
<ul>
<li>S3 는 파일 저장 서비스의 역할 뿐만 아니라 부가적인 기능을 더 가지고 있다.</li>
<li>그 기능은 바로 &quot;정적 웹 사이트 호스팅&quot; 이다.</li>
<li>웹 서비스를 다른 사용자들도 쓸 수 있게 인터넷에 배포하는 걸 뜻한다.</li>
<li>CloudFront 는 컨텐츠(파일 , 동영상 등)를 빠르게 전송하게 해주는 서비스이다.</li>
</ul>
<h4 id="aws-cloudfront-작동-과정">AWS CloudFront 작동 과정</h4>
<ul>
<li>컨텐츠(파일 , 이미지, 동영상등)는 S3 라는 곳에 저장될 것인데, S3 저장소가 만약 한국에 있었다면 한국 사용자는 거리가 가깝기 때문에 데이터를 빠르게 전송 받을 수 있다.</li>
<li>하지만 미국에 있는 사용자가 S3 로부터 데이터를 전송 받으려면 거리가 멀어 시간이 오래 걸리게 한다.</li>
<li>이러한 문제를 해결하기 위해 전세계 곳곳에 컨텐츠의 복사본을 저장해놓을 수 있는 임시 저장소를 구축한다.</li>
<li>미국에 있는 사용자가 컨텐츠를 전송바독 싶을 떄, 가장 가까운 임시 저장소에서 컨텐츠를 가져오면 훨씬 속도가 빨라진다.</li>
</ul>
<p>이러한 서비스를 CDN(Content Delivery Network) 이라고 하며, CloudFront 를 CDN 서비스라고도 표현한다.</p>
<h4 id="cloudfront-사용-이유">CloudFront 사용 이유?</h4>
<ul>
<li>S3 만 사용해도 웹 서비스를 배포할 수 있긴 한데 왜 굳이 CloudFront 를 사용할까?</li>
<li>CloudFront 는 컨텐츠를 전송 받는 성능을 향상시키기 위해 사용하기도 하고 HTTPS 를 적용하려면 CloudFront 를 사용해야한다.</li>
<li>S3 에는 HTTPS 를 적용시키는 기능을 제공하고 있지 않기 떄문에 보안을 한층 강화할 수 있다.</li>
<li>그래서 S3 와 CloudFront 를 같이 써서 웹 서비스를 배포한다.</li>
</ul>
<h4 id="현업에서는-s3--cloudfront-를-많이-사용할까">현업에서는 S3 + CloudFront 를 많이 사용할까?</h4>
<ul>
<li>현업에서 웹 서비스를 배포할 때 실제로 S3 와 CloudFront 를 많이 활용하고 있다.</li>
<li>물론 Netlify , Vercel , Cloudflare 와 같은 서비스들도 사용하기도 한다.</li>
</ul>
<h3 id="아키텍처-구성">아키텍처 구성</h3>
<ul>
<li>사용자는 바로 S3 에 접근하는게 아니라 CloudFront 에게 요청을 하면 CloudFront 가 S3 로 요청을 보낸다.</li>
<li>User -&gt; Amazon CLoudFront -&gt; Amazon S3</li>
</ul>
<p>우선 실습을 위해 전에 배웠던 방법과 유사하게 S3 Bucket 을 만들면 된다.</p>
<ul>
<li>만들면서 정책도 똑같이 작성하면 된다.</li>
</ul>
<h3 id="s3-에-업로드-하기--웹-호스팅-설정하기">S3 에 업로드 하기 / 웹 호스팅 설정하기</h3>
<ul>
<li>만든 S3 버킷에 간단한 index.html 을 만들고 업로드 해준다.</li>
<li>그런후 S3 버킷에 들어가서 속성에 가보면 정적 웹 사이트 호스팅이 있는데 편집을 누른다.</li>
<li>정적 웹 사이트 호스팅을 활성화 하고 인덱스 문서에 우리가 업로드한 index.html 을 지정해주면 된다.</li>
<li>이렇게 만들고 나면 웹 사이트 엔드포인트가 나오는데 이 URL 로 접근해보면 우리가 만든 index.html 이 잘 나오는 것을 확인할 수 있다.</li>
</ul>
<p>현재는 S3 에 업로드를 한 것이고 우리는 CloudFront 를 통해서 접근할 수 있도록 바꿔야 한다.</p>
<h3 id="cloudfront-생성하기">CloudFront 생성하기</h3>
<ul>
<li>CloudFront 페이지에 들어가서 배포 생성을 누른다.</li>
<li>원본 도메인에 위에서 우리가 만든 S3 버킷을 선택하고 S3 정적 호스팅 웹 사이트의 엔드포인트를 사용</li>
<li>기본 캐시 동작에서 뷰어 프로토콜 정책을 Redirect HTTP to HTTPS 를 선택</li>
<li>웹 애플리케이션 방화벽은 보안 보호 비활성화를 선택</li>
<li><blockquote>
<p>비활성화를 한다고 해서 보안이 취약해지는것이 아니기 때문에 선택해도 무방하다.</p>
</blockquote>
</li>
<li>가격 분류에서는 북미 , 유럽 , 아시아 , 중동 및 아프리카에서 사용 선택</li>
<li>기본값 루트 객체는 우리가 만든 index.html 설정</li>
<li>생성하고 시간이 좀 지나면 배포 도메인 이름으로 접근해보면 index.html 이 잘 실행된다.</li>
</ul>
<p>이제는 이 CloudFront 에 도메인을 연결하고 HTTPS 까지 적용해보자.</p>
<h3 id="도메인-연결--https-적용">도메인 연결 + HTTPS 적용</h3>
<ul>
<li>전에 배웠듯이 HTTPS 적용을 위해서는 인증서를 발급 받아야 한다.</li>
<li>Certificate Manager 에서 만들면 되는데 여기서 중요한 점은 CloudFront 에 HTTPS 를 적용하기 위해 인증서를 발급할 때 리전을 서울이 아니라 버지니아 북부로 설정을 해야한다.</li>
<li>요청을 누르고 도메인 이름 넣어주고 요청을 누르면 인증서가 만들어진다.</li>
<li>만든 후 인증서에 들어가보면 Route53 에서 레코드 생성을 누르고 검증을 해주면 된다.</li>
<li>다시 만든 CloudFront 에 들어가고 일반에서 편집을 누르고 대체 도메인 이름에 우리가 인증서 만들 때 설정한 도메인을 넣어주고 사용자 정의 SSL 인증서에 인증서를 선택해준다.</li>
<li>그런 후 변경 사항 저장해주면 된다.</li>
</ul>
<p>인증서 적용을 해주고 해야할 것은 Route53 에서 CloudFront 에 도메인을 연결해줘야 한다.</p>
<ul>
<li>Route53 에 들어가서 우리가 가지고 있던 호스팅 영역(도메인)에 들어가서 레코드 생성을 눌러주고 레코드 이름 설정하고 레코드 유형은 A , 별칭은 CloudFront 배포에 대한 별칭을 선택하고 생성해주면 된다.</li>
</ul>
<p>해당 도메인으로 접속해보면 정상적으로 index.html 의 화면이 나온다.</p>
<ul>
<li>도메인도 적용이 됐고 HTTPS 도 잘 적용 된 것을 확인할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS 배포 하기 - 3 (RDS + S3)]]></title>
            <link>https://velog.io/@support-kim/AWS-%EB%B0%B0%ED%8F%AC-%ED%95%98%EA%B8%B0-3-RDS-S3</link>
            <guid>https://velog.io/@support-kim/AWS-%EB%B0%B0%ED%8F%AC-%ED%95%98%EA%B8%B0-3-RDS-S3</guid>
            <pubDate>Thu, 28 Mar 2024 14:33:50 GMT</pubDate>
            <description><![CDATA[<h2 id="데이터베이스-연결-rds">데이터베이스 연결 (RDS)</h2>
<h3 id="rds-란">RDS 란?</h3>
<ul>
<li>RDS (Relational Database Service) 는 관계형 데이터베이스 서비스로 MysQL , MariaDB 등 여러 관계형 데이터베이스 서비스를 AWS 로부터 빌려서 사용한다고 생각하면 된다.</li>
</ul>
<h4 id="rds-사용-이유">RDS 사용 이유</h4>
<ul>
<li>로컬 환경에서 개발할 때는 로컬 환경에 설치된 MySQL 와 같은 DB 를 연결해서 사용하지만, 서버를 배포하게 되면 서버가 내 컴퓨터에 설치된 MySQL 과 연결을 할 수 없다.</li>
<li>즉 DB 도 외부 인터넷에서 접근할 수 있도록 같이 배포를 해줘야한다.</li>
<li>이러한 이유 떄문에 AWS RDS 라는 데이터베이스를 빌려서 사용하는 것이며, 이외에도 자동 백업 , 모니터링 , 다중AZ 와 같은 편리한 부가기능도 제공한다.</li>
</ul>
<h4 id="ec2-에-mysql-직접-설치-후-운영한다면">EC2 에 MySQL 직접 설치 후 운영한다면?</h4>
<ul>
<li>EC2 에 MySQL 을 설치하면 별도의 RDS 의 비용이 나오지 않는다.</li>
<li>하지만 실제 현업에서는 이런 방식을 사용하지 않는데, 그 이유는 백엔드 서버에 장애로 인해 EC2 컴퓨터가 죽을 경우, 애꿏은 MySQL 도 같이 죽기 때문이다.</li>
<li>추가로 RDS 가 제공하는 부가적인 편리한 기능이 많기 떄문에 MySQL 을 직접 설치 하지 않고 RDS 를 쓴다.</li>
</ul>
<h4 id="정리">정리</h4>
<p>현업에서는 EC2 와 RDS 를 분리해서 사용하는 것이 일반적이지만, 비용이 부담될때는 EC2에 백엔드 서버와 MySQL 을 모두 설치해서 사용해도 무방하다.</p>
<h3 id="ecs---rds-아키텍처">ECS -&gt; RDS 아키텍처</h3>
<ul>
<li>해당 예제에서는 사용자는 EC2 에 요청을 보내고 EC2 는 RDS 에서 데이터를 가져온다.</li>
</ul>
<h3 id="rds-생성">RDS 생성</h3>
<ul>
<li>AWS 에서 RDS 들어가기</li>
<li>표준 생성 , MySQL , 프리티어 , DB 인스턴스 식별자 설정 , 마스터 이름 + 마스터 비밀번호 설정 , 스토리지 설정</li>
<li>EC2 컴퓨팅 리소스에 연결 안 함 , 퍼블릭 액세스는 &quot;예&quot; 하고 데이터 생성하기</li>
<li>퍼블릭 액세스를 한다고 해서 보안적으로 취약해지는 것은 아니기 때문에 그냥 사용해도 무방하다.</li>
</ul>
<h3 id="보안-그룹-설정">보안 그룹 설정</h3>
<ul>
<li>RDS 라는 집에 접근하기 전에 울타리와 대문와 같은 곳에서 어떤 IP 들을 허용할 것인지 정하는 것이 보안 그룹이라고 생각하면 된다.</li>
<li>보안 그룹은 AWS EC2 에 들어가서 보안 그룹에서 생성하면 된다.</li>
<li>적절한 이름을 넣어주고 인바운드 규칙에 MySQL/Aurora , 소스 유형은 IPv4 를 설정해준다.</li>
<li>RDS 는 MySQL 의 유형 요청만 받는다는 것이다.</li>
<li>생성한 RDS 에 들어가 수정을 누른 후 연결에서 위에서 만든 보안 그룹을 선택하고 즉시수정을 하면 된다.</li>
</ul>
<h3 id="파라미터-그룹-추가하기">파라미터 그룹 추가하기</h3>
<ul>
<li>파라미터 그룹은 MySQL 에도 여러가지 설정 값들을 가진다.</li>
<li>RDS 를 사용하면 MySQL 설정을 어렵게 설정하는게 아니라 파라미터 그룹에서 쉽게 설정할 수 있도록 해준다.</li>
<li>파라미터 그룹 생성을 누르고 유형은 mysql8.0 , 그룹 이름을 넣어주고 생성하면 된다.</li>
<li>들어가보면 여러가지 설정 값들이 많은데 이 값들이 MySQL 에서 적용시킬 옵션 값이다.</li>
<li>아래에 있는 값들을 모두 수정해줘야 한다.</li>
</ul>
<ol>
<li>속성들을 utf8mb4 로 설정</li>
</ol>
<ul>
<li>character_set_client</li>
<li>character_set_connection</li>
<li>character_set_database</li>
<li>character_set_filesystem</li>
<li>character_set_results</li>
<li>character_set_server</li>
<li>인코딩 방식을 utf8mb4 로 바꿔준다. (한글 지원뿐만 아니라 이모티콘도 지원)</li>
</ul>
<ol start="2">
<li>속성들을 utf8mb4_unicode_ci 로 설정</li>
</ol>
<ul>
<li>collation_connection</li>
<li>collation_server</li>
<li>utf8mb4_unicode_ci 는 정렬 , 비교 방식을 나타낸다.</li>
</ul>
<ol start="3">
<li>time_zone 을 Aisa/Seoul 로 설정</li>
</ol>
<ul>
<li>시간을 우리나라 시간으로 맞추기 위한 설정</li>
</ul>
<p>다시 만든 RDS 로 가서 수정에서 추가구성-&gt;DB 파라미터 그룹에 우리가 만든 파라미터 그룹을 넣어주면 된다.</p>
<ul>
<li>DB 파라미터 그룹을 변경한 뒤에는 RDS 의 DB 를 재부팅해야만 정상적으로 적용된다.</li>
</ul>
<h3 id="rds에-접속하기">RDS에 접속하기</h3>
<ul>
<li>RDS 에 대한 기본적인 설정은 끝났고</li>
<li>데이터베이스를 만들면 데이터베이스 관리 툴과 연결을 하고 데이터베이스가 잘 동작하고 연결이 됐는지 체크를 먼저 해야한다.</li>
<li>가장 많이 사용하는 것은 Datagrip , Workbench , DBeaver 가 있다.</li>
<li>RDS 에 들어가보면 엔드포인트가 있는데 그 값을 복사한 후, 워크벤치에서 새로운 커넥션을 만들 때 HostName 에 엔드포인트를 넣어주고 비밀번호에는 RDS 를 만들 때 설정했던 비밀번호를 넣어주면 된다.</li>
<li>그렇게 접속하면 sys 라는 데이터베이스가 이미 만들어져있는데 거기에는 설정에 관련된 값들이 있기 때문에 삭제해서는 안 된다.</li>
</ul>
<h4 id="엔드포인트endpoint란">엔드포인트(EndPoint)란?</h4>
<ul>
<li>특정 리소스(Server , DB등)에 접근할 수 있도록 해주는 URL</li>
<li>RDS 뿐만 아니라 다양한 곳에서 사용하는 단어이다.</li>
<li>우리는 EC2 나 RDS 와 같은 리로스에 접근 할 때 특정 주소를 통해 접근하는데, 그 주소가 해당 리소스의 대문 같은 역할을 한다.</li>
<li>그 대문을 넘어서야 내부에 있는 코드나 데이터를 가져올 수 있다.</li>
</ul>
<h3 id="spring-서버에-rds-연결">Spring 서버에 RDS 연결</h3>
<ul>
<li>yml 파일에 위에서 만든 USERNAME , PASSWORD , HOST(엔드포인트) 들을 넣으면 된다.</li>
<li>EC2 배포는 이 자체를 배포하게 되면 EC2 에 있는 컴퓨터가 RDS 와 연결이 되기 떄문에 이 자체를 JAR 로 만들어서 배포하면 된다.</li>
</ul>
<h2 id="파일-및-이미지-업로드-s3">파일 및 이미지 업로드 (S3)</h2>
<h3 id="s3-란">S3 란?</h3>
<ul>
<li>S3는 파일 저장 서비스로 사진이나 동영상과 같은 파일들을 구글 드라이브나 iCloud 에 저장하는데, S3 는 이런 종류의 서비스라고 생각하면 된다.</li>
</ul>
<h4 id="s3-사용-이유">S3 사용 이유?</h4>
<ul>
<li>백엔드 서버를 구현하면 이미지 업로드 기능을 구현할 때가 많은데, 이 이미지 파일을 어디에 저장을 해야할까?</li>
<li>물론 EC2 내부에 넣을 수 있지만 서비스를 조금만 운영하다보면 EC2 에 쌓이는 파일들이 너무 많아지고 지저분해지기 때문에 좋지 않다.</li>
<li>핸드폰에 저장공간이 있어도 구글 드라이브나 iCloud 같은 곳에 사진을 옮기는 것과 같은 맥락으로, S3 는 파일 저장에 특화된 서비스이다.</li>
<li>파일 저장 뿐만 아니라 파일을 다운 받는 것에 대해서도 최적화가 되어 있는 서비스이기 때문에 S3 를 사용한다.</li>
</ul>
<p>실제로 현업에서도 파일 업로드 기능을 구현해야할 때 AWS S3 를 많이 활용한다.</p>
<h3 id="s3-을-활용한-아키텍처-구성">S3 을 활용한 아키텍처 구성</h3>
<h4 id="이미지-파일-업로드-과정">이미지 파일 업로드 과정</h4>
<ol>
<li>사용자는 EC2 에게 이미지 업로드 API 로 요청을 보낸다.</li>
<li>EC2 는 S3 에 이미지를 업로드 한다.</li>
<li>S3 는 이미지가 저장된 URL 을 EC2에게 리턴한다.</li>
<li>EC2 는 DB(RDS) 에 이미지가 저장된 URL 을 저장한다.</li>
</ol>
<ul>
<li>이미지 자체를 저장한다는 개념이 아니라 이미지가 저장된 URL 을 저장한다.</li>
</ul>
<h4 id="이미지-파일-다운로드-과정">이미지 파일 다운로드 과정</h4>
<ol>
<li>사용자는 EC2 에게 이미지 조회 API 로 요청을 보낸다.</li>
<li>EC2 는 DB(RDS) 에 조회 SQL 문을 날린다.</li>
<li>DB(RDS) 에 저장되어 있던 이미지 URL 을 EC2 에게 전달한다.</li>
<li>EC2 는 사용자한테 이미지 URL 을 응답을 보내준다.</li>
<li>사용자가 이미지 URL 을 사용할 경우, S3 로부터 이미지를 다운로드 받는다.</li>
</ol>
<ul>
<li>5번에 경우는 우리가 네이버를 들어가보면 여러가지 이미지들이 보인다.</li>
<li>이때 우리는 어디선가 다운로드 받은 이미지들을 가지고 오는 것과 똑같다고 생각하면 된다.</li>
</ul>
<p>해당 예시들은 이미지 파일이지만, 이미지 이외에도 다양한 종류의 파일을 저장하는 용도로 사용된다.</p>
<h3 id="s3-버킷-생성">S3 버킷 생성</h3>
<ul>
<li>S3 에서는 버킷과 객체라는 용어를 사용하기 때문에 이 용어들에 대해서 알아보자.</li>
<li>버킷 : 깃허브에서도 여러 개의 Repository 를 만들 수 있는 것 처럼 S3 에서도 여러 개의 저장소를 만들 수 있고, 여기서 하나의 저장소를 버킷이라고 한다.</li>
<li>객체 : S3 업로드한 파일을 보고 S3 에서는 파일이라고 하지 않고 객체라고 부른다.</li>
<li><blockquote>
<p>S3 버킷에 업로드된 파일</p>
</blockquote>
</li>
</ul>
<h4 id="aws-bucket-생성">AWS Bucket 생성</h4>
<ol>
<li>버킷 만들기에서 버킷 이름을 두고 퍼블릭 액세스 차단 설정을 모두 풀어주고 다른 것들은 기본값을 사용하고 만들어준다.</li>
<li>정책 추가</li>
</ol>
<ul>
<li>정책은 권한(Permission) 을 정의하는 JSON 문서를 의미하는데, AWS 는 기본적으로 대부분의 권한이 주어져있지 않기 때문에 AWS의 특정 소스에 접근하려면 권한을 허용해줘야 한다.</li>
<li>그 허용을 하기 위해서는 정책을 작성해야 한다.</li>
<li>1번에서 만든 버킷에 들어가서 권한에 들어가고 버킷 생성에서 편집을 누르고 새문 추가에서 서비스 중에서 S3 를 선택한다.</li>
<li>그 중에서 GetObject 를 선택하고 리소스에는 S3 , object , bucket 이름에는 생성한 버킷 이름과 object 는 * 을 넣어준다.</li>
<li>이렇게 하면 S3 중에서 해당 Bucket 이름에 해당하는 버킷에 모든파일(*) 을 사용할 수 있다는 것이다.</li>
<li>그런 후 Principal 은 &quot;*&quot; 로 바꿔줘서 모두가 접근할 수 있도록 해준다.</li>
</ul>
<h3 id="s3에-파일을-업로드할-수-있도록-iam-에서-액세스-키-발급">S3에 파일을 업로드할 수 있도록 IAM 에서 액세스 키 발급</h3>
<ul>
<li>기본적으로 AWS 의 리소스에 아무나 접근을 못하도록 막았기 때문에 S3 에 접근해서 파일을 업로드할 수 없다.</li>
<li>백엔드 서버가 S3 접근해서 파일을 업로드 할 수 있어야 하는데, 그렇게 하기 위한 권한을 IAM 을 통해서 받을 수 있다.</li>
<li>IAM 에 들어가서 사용자 추가를 누른 후 사용자 이름 설정하고 , 직접 정책 연결에서 AmazonS3FullAccess 를 선택하고 만들어주면 된다.</li>
<li>만든 후 보안 자격 증명-&gt;액세스 키 에서 액세스 키 생성을 할 떄 AWS 외부에서 실행되는 애플리케이션을 선택하고 만들어주며 된다.</li>
<li>그렇게 발급된 액세스 키와 비밀 액세스 키를 어딘가에 잠깐 복사해놓자.</li>
</ul>
<h3 id="s3-를-활용해-spring-서버에-이미지-업로드-기능-구현">S3 를 활용해 Spring 서버에 이미지 업로드 기능 구현</h3>
<ul>
<li>S3 접속할 수 있는 라이브러리를 다운로드 받고 yml 파일에 ACESS_KEY , SECRET_KEY , BUCKET_NAME 들을 넣어준다.</li>
<li>S3Client 객체를 생성하고 key 값들을 넣어주면 된다.</li>
<li>POSTMAN 으로 이미지를 업로드 할 때 Body 에서 form-data 로 간 후 key 를 넣고 Value 에는 업로드할 파일을 넣어주면 된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS 배포 하기 - 2 (Route53 + ELB)]]></title>
            <link>https://velog.io/@support-kim/AWS-%EB%B0%B0%ED%8F%AC-%ED%95%98%EA%B8%B0-2-Route53-ELB</link>
            <guid>https://velog.io/@support-kim/AWS-%EB%B0%B0%ED%8F%AC-%ED%95%98%EA%B8%B0-2-Route53-ELB</guid>
            <pubDate>Thu, 28 Mar 2024 04:06:50 GMT</pubDate>
            <description><![CDATA[<h2 id="도메인-연결-route53">도메인 연결 (Route53)</h2>
<h3 id="route53-이란">Route53 이란?</h3>
<ul>
<li>Route53 은 도메인을 발급하고 관리해주는 서비스</li>
<li>도메인은 <a href="http://www.naver.com">www.naver.com</a> , youtube.com 와 같은 문자로 표현된 인터넷 주소</li>
<li>더 전문적으로 표현하면 Route53 은 DNS(Domain Name System)</li>
</ul>
<p>원래는 IP 주소로 접근을 했지만 IP 주소를 일일이 외우기 너무 어렵기 떄문에 사람들이 기억하기 쉬운 문자로 만들고 싶어했다.</p>
<ul>
<li>그래서 문자를 IP 주소로 변환해주는 하나의 시스템(서버)을 만들게 됐고 이게 바로 DNS 이다.</li>
<li>즉 우리는 문자로 된 주소를 DNS 서버에게 던지면 DNS 서버에서 문자로 넘어온 주소에 대응하는 IP 주소를 반환해준다.</li>
</ul>
<h4 id="현업에서의-route53-활용-여부">현업에서의 Route53 활용 여부</h4>
<ul>
<li>프론트 웹 페이지든 백엔드 서버든 일반적으로 IP를 기반으로 통신하지 않고 도메인을 기반으로 통신한다.</li>
<li>여러가지 이유가 존재하지만 그 이유 중 하나는 HTTPS 적용 때문인데, IP 주소에는 HTTPS 적용을 할 수 없다.</li>
<li>도메인 주소가 있어야만 HTTPS 적용을 할 수 있고, 이 떄문에 특정 서비스를 운영할 떄 도메인은 필수적으로 사용하게 된다.</li>
</ul>
<p>AWS Route53 말고도 가비아 , 후이즈와 같은 다양한 DNS 서버도 있다.</p>
<p>현업에서는 무조건적으로 AWS Route53 을 고집하진 않는데, 그 이유는 각 서비스마다 구매할 수 있는 도메인의 종류가 다르기 때문이다.</p>
<ul>
<li>Route53 에는 없는 것이 가비아에는 있을 수 있기 떄문에 내가 원하는 도메인이 존재하는 곳의 DNS 서버를 활용하면 된다.</li>
</ul>
<h3 id="route53-에서-도메인-구매">Route53 에서 도메인 구매</h3>
<ul>
<li>Route53 에 연결할 EC2 를 전에 배웠던 방식으로 생성</li>
<li>생성한 후 AWS Route53 에 들어가서 사용하고 싶은 도메인 검색</li>
<li>검색하고 나서 나오는 금액에 대한 결제를 하면 된다.</li>
<li>구매하게 되면 왼쪽 메뉴에서 호스팅 영역으로 가보면 구매한 도메인의 창이 나온다.</li>
</ul>
<h3 id="route53의-도메인을-ec2에-연결하기">Route53의 도메인을 EC2에 연결하기</h3>
<ul>
<li>구매한 도메인을 jiwon-test.net 이라고 가정해보자.</li>
<li>레코드 유형에서 A 와 CNAME 만 알면 된다.</li>
<li>IP 주소를 입력해서 도메인 주소로 바꿀것 이기 떄문에 A 를 선택</li>
<li>값에는 인스턴스의 IP 주소를 넣어주면 된다.</li>
<li>이렇게 하면 구매한 도메인으로 접근하면 그 안에서 값에 입력한 IP 주소로 보내준다는 것이다.</li>
<li>레코드 이름을 api 로 지정하면 api.jiwon-test.net 으로 접근하는 것을 의미한다.</li>
<li>즉 www 처럼 도메인 앞에 붙은 것이라고 생각하면 된다.</li>
</ul>
<h3 id="무료로-도메인-구매하는-방법">무료로 도메인 구매하는 방법</h3>
<ul>
<li>내도메인한국 검색</li>
<li>회원가입 후 로그인</li>
<li>원하는 도메인을 검색후 가능한 도메인을 생성하면 된다.</li>
<li>생성한 후 도메인 관리에서 수정 들아가서 IP연결(A) 에 EC2 IP 를 넣어주면 된다.</li>
<li>그런후 생성한 도메인으로 정상적으로 접속할 수 있다.</li>
</ul>
<h2 id="https-연결하기-elb">HTTPS 연결하기 (ELB)</h2>
<h3 id="elb란">ELB란?</h3>
<ul>
<li>ELB(Elastic Load Balancer) 는 트래픽을 적절하게 분배해주는 장치</li>
<li>서버를 2대 이상 가용할 떄 ELB를 필수적으로 도입하게 된다.</li>
<li>ELB 의 부가 기능인 SSL/TLS(HTTPS) 를 적용시키는 방법에 대해 배울 예정</li>
</ul>
<h4 id="ssl--tls-란">SSL / TLS 란?</h4>
<ul>
<li>SSL/TLS 는 HTTP 를 HTTPS 로 바꿔주는 인증서</li>
<li>ELB 는 SSL/TLS 의 기능을 제공하고, SSL/TLS 인증서를 활용하면 HTTP 가 아닌 HTTPS 로 통신할 수 있게 만들어준다.</li>
</ul>
<h4 id="https-를-사용해야-하는-이유">HTTPS 를 사용해야 하는 이유?</h4>
<ol>
<li>보안적인 이유</li>
</ol>
<ul>
<li>데이터를 서버와 주고 받을 때 암호화를 시켜서 통신</li>
<li>만약 암호화하지 않는다면 누군가 중간에서 데이터를 가로채 해킹할 수 있다.</li>
</ul>
<ol start="2">
<li>사용자 이탈</li>
</ol>
<ul>
<li>HTTPS 를 사용하지 않고 웹 사이트에 들어가면 보안에 취약하다는 경고창이 같이 나오기 떄문에 사용자가 이탈한다.</li>
</ul>
<h4 id="현업에서는">현업에서는?</h4>
<ul>
<li>대부분의 웹 사이트에서 HTTPS 를 적용시킨다.</li>
<li>HTTPS 인증을 받은 웹 사이트가 백엔드 서버와 통신하려면 백엔드 서버의 주소도 HTTPS 인증을 받아야 한다.</li>
<li>따라서 백엔드 서버와 통신할 때도 IP 주소로 통신하는게 아니라, HTTPS 인증을 받은 도메인 주소로 통신을 한다.</li>
</ul>
<p>주소 도메인을 구성할 때 사용하는 방법</p>
<ul>
<li>웹 사이트 주소 : <a href="https://jiwon-test.co.kr">https://jiwon-test.co.kr</a></li>
<li>백엔드 주소 : <a href="https://api.jiwon-test.co.kr">https://api.jiwon-test.co.kr</a></li>
</ul>
<h3 id="elb-를-활용한-아키텍처">ELB 를 활용한 아키텍처</h3>
<ul>
<li>ELB 를 사용하기 전에는 사용자가 직접 EC2 의 IP 주소 또는 도메인 주소에 직접 요청을 보내는 구조</li>
<li>ELB 를 추가적으로 도입하게 되면, 사용자들이 EC2 에 직접적으로 요청 보내는게 아니라 ELB 를 향해 요청을 보내도록 구성한다.</li>
<li>EC2 달았던 도메인도 ELB 에 달고, HTTPS 도 ELB 의 도메인에 적용시킬 예정이다.</li>
</ul>
<h3 id="elb-setting-기본-구성">ELB Setting 기본 구성</h3>
<ul>
<li>AWS EC2 왼쪽 메뉴에서 로드밸런서 서비스로 간다.</li>
<li>생성을 누르고 Application Load Balancer 를 선택</li>
<li>인터넷 경계와 내부라는 옵션이 있는데 내부 옵션은 Private IP 를 활용할 때 사용하는데, 이러한 개념을 활용하지 않기 때문에 인터넷 경계를 선택하면 된다.</li>
<li>우리가 만든 EC2 인스턴스는 IPv4 로 이루어져있기 때문에 IPv4 를 선택하면 된다.</li>
<li>네트워크 매핑을 설정해줘야하는데, 로드 밸런서가 어떤 가용 영역으로만 트래픽을 보낼 건지 제한하는 기능인데, 일단은 가용 영역에 제한을 두지 않고 모든 영역에 트래픽을 보내게 설정하면 된다. (모든 가용 영역에 다 체크)</li>
</ul>
<h3 id="elb-setting-보안그룹">ELB Setting 보안그룹</h3>
<ul>
<li>EC2 의 보안그룹과 ELB 의 보안그룹은 별개이다.</li>
<li>그래서 ELB 를 위한 보안 그룹을 만들어보자.</li>
<li>인바운드 규칙에 HTTP , HTTPS 를 가지는 보안 그룹을 생성하고 ELB Setting 할 때 지금 만든 보안그룹을 넣어주면 된다.</li>
</ul>
<h3 id="elb-setting-리스너-및-라우팅--헬스-체크">ELB Setting 리스너 및 라우팅 / 헬스 체크</h3>
<ul>
<li>리스너 및 라우팅 설정은 ELB 로 들어온 요청을 어떤 EC2 인스턴스에 전달할 건지를 설정하는 부분이다.</li>
<li>ELB 가 사용자로부터 트래픽을 받아서 대상 그룹에게 어떤 방식으로 전달을 할지를 설정해야 한다.</li>
<li><blockquote>
<p>ELB 로 들어온 요청을 &quot;어떤곳&quot; 에 전달을 해야하는데 그 &quot;어떤곳&quot; 을 대상 그룹이라고 표현하고, ELB 로 들어온 요청을 어디로 보낼지 대상 그룹을 만들어야 한다.</p>
</blockquote>
</li>
<li>대상 그룹 생성에서 인스턴스 , HTTP , IPv4 , HTTP1 을 골라주면 된다.</li>
<li><blockquote>
<p>HTTP , 80번 포트 , IPv4 주소로 통신을 한다는 것을 뜻하고 현업에서도 많이 쓰이는 셋팅 방법이다.</p>
</blockquote>
</li>
<li>상태 검사 설정하기는 중요한 기능이기 때문에 정확하게 짚고 넘어가보자.</li>
</ul>
<h4 id="상태검사">상태검사</h4>
<ul>
<li>실제로 ELB 로 들어온 요청을 대상 그룹에 있는 여러 EC2 인스턴스로 전달하는 역할을 가지는데, 만약 특정 EC2 인스턴스 내에 있는 서버가 예상치 못한 에러로 고장났다고 가정해보자.</li>
<li>그러면 ELB 입장에서는 고장난 서버에 요청을 전달하는게 비효율적이기 떄문에 ELB 는 주기적으로 대상 그룹에 속해있는 각각의 EC2 인스턴스에 요청을 보내고, 그 요청에 대한 응답이 잘 날라온다면 서버가 정상적으로 잘 작동되고 있다는 것을 판단한다.</li>
<li>만약 요청을 보냈는데 응답이 오지 않는다면 서버가 고장났다고 판단해서 ELB 가 고장났다고 판단하는 EC2 로는 트래픽을 보내지 않는다.</li>
<li>GET /health 으로 요청을 보내게끔 설정하고, 정상적인 헬스 체크 기능을 위해 EC2 인스턴스에서 작동하고 있는 서버에 Health Check 용 API 를 만들어야 한다. (만들 예정)</li>
</ul>
<p>다시 돌아와서 이렇게 생성한 대상 그룹을 지정해주고 로드 밸런서를 생성하면 된다.</p>
<ul>
<li>정리하면 ELB 에 HTTP 를 활용해 80번 포트로 들어온 요청을 설정한 대상 그룹으로 전달하겠다는 것</li>
</ul>
<p>상태 검사를 위한 GET /health API 를 생성하고 다시 서버를 업데이트 시켜준다.</p>
<p>그런후에 로드 밸런서 주소를 통해 서버로 접속해보자.</p>
<ul>
<li>생성된 로드 밸런스에 가보면 DNS 이름이 있는데 이 주소로 접속하면 된다.</li>
<li>그 주소에 /health 로도 요청해보면 정상적으로 실행되는 것을 확인할 수 있다.</li>
<li>ELB 는 IP 주소가 아닌 도메인 주소를 가지고 접속을 할 수 있다.</li>
</ul>
<p>정리하면 사용자의 요청을 ELB 가 받고 ELB 에서 EC2 인스턴스에 접속할 수 있도록 한 것이다.</p>
<ul>
<li>현재는 EC2 인스턴스 주소의 도메인과 연결을 했는데, 다음에는 ELB 에 도메인을 연결해보자.</li>
</ul>
<h3 id="elb-에-도메인-연결하기">ELB 에 도메인 연결하기</h3>
<ul>
<li>바로 전에 말헀듯이 현재는 EC2 인스턴스 주소의 도메인과 연결을 했는데, 사용자는 EC2 에 직접적으로 보내는게 아니라 ELB 를 거쳐서 보낸다.</li>
<li>그렇기 떄문에 ELB 에 도메인을 연결해줘야 한다.</li>
<li>Route53 에 가서 호스팅 영역에 가서 구매한 도메인을 들어가보면 전에 우리가 EC2 에 도메인을 연결했던 것이 있다.</li>
<li>그것을 삭제하고 다시 레코드 생성을 누르고 레코드 이름 , 레코드 유형은 그대로 설정하고 별칭을 활성화 시킨다음에 트래픽 라우팅 대상에서 Application/Classic Load Balancer 에 대한 별칭을 선택하고, 지역은 아시아 태평양(서울) 을 선택한다.</li>
<li>그렇게 하면 우리가 만들었던 로드 밸런서의 주소가 나오고 그 후 레코드 생성을 누르면 된다.</li>
<li>생성이 됐다면 그 구매한 도메인으로 접속할 시 정상적으로 작동한다.</li>
</ul>
<p>즉 ELB 에 도메인을 연결한 것이다.</p>
<h3 id="https-적용을-위해-인증서-발급하기">HTTPS 적용을 위해 인증서 발급하기</h3>
<ul>
<li>AWS Certificate Manager 서비스로 들어가서 인증서 요청 버튼 누르기</li>
<li>퍼블릭 인증서 요청을 선택하고 도메인 이름에는 우리가 사용하려고 하는 도메인 이름을 넣어주면 된다.</li>
<li>그 이후 기본적으로 선택되어 있는 값을 사용하면 된다.
생성하자마자 바로 되는 것이 아니라 도메인에 가보면 검증 대기 중 이라는 상태를 가진다.</li>
<li>그 이유는 아무 인증 없이 인증서를 발급해주면 안 되기 때문에 CNAME 이라는 유형으로 이름과 값을 보내게 된다면 그때 인증을 해주겠다는 것이다. (내가 소유한 도메인이 맞는지 검증하는 단계)</li>
<li>그래서 Reoute 53 에서 레코드 생성 버튼을 누르고 바로 레코드 생성을 누르게 되면 등록할 수 있다.</li>
<li>약간의 시간이 지난후 정상적으로 발급이 성공된다.</li>
</ul>
<h3 id="elb-에-https-설정하기">ELB 에 HTTPS 설정하기</h3>
<ul>
<li>EC2 로 들어가서 로드 밸런서를 클릭한 후 우리가 생성했던 로드 밸런서로 들어간다.</li>
<li>거기서 내려보면 리스너 및 규칙이 있고 리스너 추가 버튼을 누르고 HTTPS 의 리스너를 추가한다. (전에는 HTTP 만 추가 했었다)</li>
<li><blockquote>
<p>ELB 로 요청이 들어오는데 HTTPS 의 프로토콜을 활용한 요청이 들어온다면 그 요청에 대해서 대상 그룹에게 트래픽을 전달해주겠다는 것이다.</p>
</blockquote>
</li>
<li>대상 그룹은 우리가 전에 만들었던 대상 그룹을 선택하고 보안 정책 부분에서 다른 값은 기본 값을 사용하고 그 중에서 인증서는 우리가 바로 위에서 만든 인증서를 선택해주면 된다.</li>
<li>그런후 추가를 누르고 https://도메인명 으로 접속해보면 정상적으로 동작한다.</li>
</ul>
<p>HTTP 는 위에서 설명한 단점이 있기 떄문에 HTTP 로 요청이 들어오더라도 HTTPS 자동으로 전환하도록 해보자.</p>
<ul>
<li>우선 로드 밸런서 세부 정보에서 리스너 및 규칙에서 HTTP:80 의 규칙을 삭제한다.</li>
<li>다시 리스너 추가를 할 때 리스너 구성에서 프로토콜은 HTTP 포트는 80 을 설정하고 기본 작업에서 라우팅 액션을 URL 로 리디렉션을 선택한다.</li>
<li>URL로 리디렉션에서 URI 부분을 선택하고 프로토콜은 HTTPS 포트는 443 으로 설정해준다.</li>
<li>이렇게 설정하면 HTTP:80 으로 요청이 들어온 것에 대해서는 바로 대상 그룹으로 트래픽을 보내는것이 아니라 HTTPS:443 로 다시 리디렉션을 하겠다는 것이다.</li>
<li><blockquote>
<p>즉 HTTP 로 요청을 하더라도 HTTPS 로 접속할 수 있게 한다.</p>
</blockquote>
</li>
</ul>
<p>실제로 http:// 로 접속하더라도 https:// 로 자동으로 바뀌는 것을 확인할 수 있다.</p>
<h3 id="https-연결-시-elb-vs-nginx--certbot">HTTPS 연결 시 ELB vs Nginx , Certbot</h3>
<ul>
<li>현업에서는 ELB 를 활용해서 HTTPS 적용을 더 많이 시킨다.</li>
<li>HTTPS 설정도 쉽고, HTTPS 인증서의 만료기간 갱신도 자동으로 해주기 때문이다.</li>
<li>하지만 가장 큰 단점은 &quot;비용&quot; 인데, Nginx+Certbot 을 활용하면 HTTPS 에 관련된 비용이 일절 들지 않는다.</li>
<li>하지만 ELB 를 사용하는 것 자체로써 비용이 나가기 떄문에 비용에 부담이 되는 상황이라면 Nginx+Certbot 을 하나의 EC2 에 설치해서 사용하면 된다.</li>
</ul>
<h3 id="nginx--certbot-을-활용해-https-연결하기">Nginx , Certbot 을 활용해 HTTPS 연결하기</h3>
<ol>
<li>EC2 생성</li>
</ol>
<ul>
<li>인바운드 보안 그룹 규칙에 SSH , HTTP , HTTPS , 사용자 지정 TCP 추가</li>
<li>Express 서버를 3000번에 띄우려고 하기 때문에 사용자 지정 TCP 에 3000 포트로 지정해주면 된다.</li>
</ul>
<ol start="2">
<li>탄력적 IP 할당 후 만들었던 인스턴스에 탄력적 IP 주소 연결</li>
<li>EC2 인스턴스에 접속 후 Node.js 설치 , git clone , 라이브러리 설치(npm) , .env 파일 생성 , app.js 파일에 들어가서 port 를 3000으로 수정 , pm2 설치 후 서버 기동</li>
</ol>
<ul>
<li>접속할 때 IP주소:3000 으로 접속</li>
</ul>
<ol start="4">
<li>Route53 에서 호스팅 영역에서 있는 도메인 링크에 들어간 후 레코드 생성</li>
</ol>
<ul>
<li>레코드 유형 (A) , 값에는 IP 주소 넣고 레코드 생성 누르면 도메인 주소로 들어가면 IP 주소로 바로 접속이 된다.</li>
<li>이제 접속할 떄 도메인주소:3000 으로 접속하면 된다.</li>
<li>만약 :3000 을 생략하면 기본 포트인 80 으로 접속하게 되는데, 현재 3000 으로 서버를 띄웠기 떄문에 도메인 주소만 입력하면 접속되지 않는다.</li>
</ul>
<ol start="5">
<li>EC2 에 Nginx 설치</li>
</ol>
<ul>
<li>sudo apt update</li>
<li>sudo apt install nginx</li>
<li>sudo service nginx status (Nginx 상태 확인)</li>
<li>IP주소/도메인주소로 접속해보면 Welcome to nginx 문구가 나오면 정상적으로 설치 된 것</li>
</ul>
<ol start="6">
<li>Certbot 설치</li>
</ol>
<ul>
<li>Certbot 의 역할은 HTTPS 인증서 발급을 해준다.</li>
<li>sudo snap install --classic certbot </li>
<li>sudo ln -s /snap/bin/certbot /usr/bin/certbot </li>
</ul>
<ol start="7">
<li>HTTPS 인증서 발급</li>
</ol>
<ul>
<li>Certbot 은 설치를 했고 이제 인증서를 발급 받아보자.</li>
<li>sudo certbot --nginx -d &quot;발급 받았던 도메인 주소&quot; (api.jiwon-test.co.kr)</li>
<li>입력하고 나면 이메일 주소를 넣어주면 된다. (계속 y 누르기)</li>
<li>일정 시간 지나면 정상적으로 발급이 된다.</li>
</ul>
<p>도메인 주소로 접근해보면 HTTPS 가 제대로 동작하는 것을 확인할 수 있다.</p>
<p>80번 포트에는 Nginx 가 동작하고 있는데, 현재 우리는 백엔드 서버를 3000번 포트로 올렸기 떄문에 3000번 포트에 HTTPS 가 동작해야한다.</p>
<p>현재로는 도메인주소:30000 으로 접근하면 HTTPS 가 적용되지 않는 것을 확인할 수 있다.</p>
<ul>
<li>현재는 80번 포트에만 HTTPS 가 적용되고 있다.</li>
</ul>
<p>해결하기 위한 방법은 EC2 인스턴스에서 수정을 해줘야한다.</p>
<ul>
<li>cd /etc/nginx </li>
<li>cd sites-available</li>
<li>vi default (만약 이상하게 나온다면 새로고침 후 다시 접근)</li>
<li>밑으로 내려가다가 server_name 에 우리가 만들었던 도메인 주소가 나오면 location 에서 try_files 부분을 주석처리 후 아래와 같이 수정한다.</li>
<li>proxy_pass <a href="http://localhost:3000/">http://localhost:3000/</a>;</li>
<li>server_name (우리가 설정한 도메인) 으로 요청이 들어온다면 proxy_pass 로 트래픽을 전달해주겠다는 것</li>
<li>저장 후 Nginx 재접속</li>
<li>sudo service nginx restart</li>
<li>다시 접속해보면 백엔드 서버에도 HTTPS 를 적용시킬 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS 배포 하기 - 1 (EC2)]]></title>
            <link>https://velog.io/@support-kim/AWS-%EB%B0%B0%ED%8F%AC-%ED%95%98%EA%B8%B0-1-EC2</link>
            <guid>https://velog.io/@support-kim/AWS-%EB%B0%B0%ED%8F%AC-%ED%95%98%EA%B8%B0-1-EC2</guid>
            <pubDate>Tue, 26 Mar 2024 14:39:51 GMT</pubDate>
            <description><![CDATA[<h2 id="백엔드-api-서버-배포-ec2">백엔드 API 서버 배포 (EC2)</h2>
<p>배포는 다른 사용자들이 인터넷을 통해 사용할 수 있게 만드는 것을 의미한다.</p>
<ul>
<li>혼자서 개발을 할 때는 localhost 라는 주소로 테스트도 하고 개발을 하는데, 이 localhost 는 다른 컴퓨터에서 접근이 불가능한 주소이다.</li>
<li>배포를 하게 되면 IP 나 도메인과 같이 고유의 주소를 부여 받게 되고 다른 컴퓨터에서도 그 주소로 접속할 수 있다.</li>
<li>그것을 해주는 것이 배포라는 것!</li>
</ul>
<h3 id="ec2-란">EC2 란?</h3>
<ul>
<li>Elastic Compute Cloud (EC2)</li>
<li>컴퓨터를 빌려서 원격으로 접속해 사용하는 서비스</li>
<li>서버를 배포하기 위해서는 컴퓨터가 필요한데, 내 컴퓨터로 배포하게 되면 24시간 내 컴퓨터를 계속 켜놔야한다.</li>
<li>그렇게 되면 보안적으로도 위험할 수 있기 때문에 AWS EC2 라는 컴퓨터를 빌려서 사용하면 된다.</li>
</ul>
<p>현업에서도 실제 서버를 배포할 때 EC2 를 아주 많이 사용하며, 백엔드 서버를 배포해야 할 때면 EC2 에 서버를 배포해서 사용한다.</p>
<p>하지만 프론트엔드 웹 페이지를 배포할 떄는 EC2 를 사용하지 않는다.</p>
<ul>
<li>프론트엔드 웹 페이지를 배포할 때는 Vercel , Netlify , AWS S3를 사용해서 주로 배포한다.</li>
</ul>
<h3 id="리전region-선택하기">리전(Region) 선택하기</h3>
<ul>
<li>Region 은 인프라를 지리적으로 나누어 배포한 각각의 데이터 센터를 의미한다.</li>
<li>EC2 를 통해 빌려서 쓸 수 있는 컴퓨터들이 전 세계적으로 다양하게 분포해있는데, 컴퓨터들이 위치한 위치를 보고 AWS 에서는 Region 이라고 한다.</li>
<li>Region 은 애플리케이션의 주된 사용자들의 위치와 지리적으로 가까운 Region 을 사용하면 된다.</li>
<li>즉 한국 유저들이 주로 사용하는 서비스는 한국의 Region 으로 선택하면 된다.</li>
</ul>
<h3 id="ec2-setting">EC2 Setting</h3>
<ol>
<li>OS 선택</li>
</ol>
<ul>
<li>Window / MAc 은 생각보다 용량도 많이 차지하고 성능도 많이 잡아 먹기 떄문에 서버를 배포할 때는 가볍고 성능도 좋은 Ubuntu 를 사용</li>
</ul>
<ol start="2">
<li>인스턴스 유형</li>
</ol>
<ul>
<li>인스턴스 : EC2 를 통해서 빌린 컴퓨터 1대를 의미</li>
<li>인스턴스 유형은 컴퓨터 사양을 의미한다고 생각하면 된다.</li>
<li>t2.micro 는 프리티어 이지만 서비스를 운영하는데 큰 문제가 없는 경우가 많다.</li>
</ul>
<ol start="3">
<li>키 페어(로그인)</li>
</ol>
<ul>
<li>EC2 에 접근할 때 사용하는 비밀번호</li>
</ul>
<ol start="4">
<li>네트워크 설정</li>
</ol>
<ul>
<li>네트워크 설정에서 보안그룹이 중요하기 때문에 보안 그룹에 대해서 알아보자.</li>
</ul>
<h4 id="보안-그룹">보안 그룹</h4>
<ul>
<li>보안그룹이란 AWS 클라우드에서의 네트워크 보안을 의미한다.</li>
<li>EC2 인스턴스를 집이라고 생각하면, 보안 그룹은 집 바깥 쪽에 쳐져있는 울타리와 대문이라고 생각하면 된다.</li>
<li>즉 집에 접근할 때 울타리의 대문에서 접근해도 되는 요청인지 보안 요원이 검사를 하는 것과 비슷하다.</li>
<li>EC2 인스턴스 주위에 방화벽 역할을 할 보안 그룹을 만들고 보안 그룹에 규칙을 지정한다.</li>
</ul>
<ol>
<li>인바운드 트래픽 : 외부에서 EC2 인스턴스로 보내는 트래픽으로 어떤 트래픽만 허용할지 설정할 수 있다.</li>
<li>아웃바운드 트래픽 : EC2 인스턴스에서 외부로 나가는 트래픽으로 어떤 트래픽만 서용할 지 설정할 수 있다.</li>
</ol>
<p>보안 그룹을 설정할 때는 허용할 IP 범위와 포트를 설정할 수 있다.</p>
<ul>
<li>외부에서 EC2 로 접근할 포트는 22번 포트(SSH)와 80번 포트(HTTP)라고 생각해서 이 2가지에 대해 인바운드 보안 그룹을 추가를 한다.</li>
<li>그 이유는 22번 포트는 우리가 EC2 에 원격 접속할 때 사용하는 포트이고, 80번 포트에는 백엔드 서버를 띄우기 때문에 어떤 IP 에서든 전부 접근할 수 있게 만들기 위해 소스 유형은 위치 무관으로 설정한다.</li>
</ul>
<h3 id="ip-와-port-란">IP 와 Port 란?</h3>
<h4 id="ip-란">IP 란?</h4>
<ul>
<li>네트워크 상에서의 컴퓨터를 가리키는 주소</li>
</ul>
<h4 id="port-란">Port 란?</h4>
<ul>
<li>한 컴퓨터 내에서 실행되고 있는 특정 프로그램의 주소</li>
<li>한 컴퓨터 내에서도 카톡 , 인텔리제이 , 디스코드등 여러가지 프로그램이 동시에 실행되고 있는것 처럼 실제 서버를 운영하는 컴퓨터도 동일하다.</li>
<li>만약 Spring Boot 와 서버에 통신을 하고 싶을때 외부에서 IP 주소만 알아서는 실행되고 있는 여러 프로그램 중 어떤 프로그램과 통신을 해야할 지 알수가 없다.</li>
<li>그래서 특정 서버와 통신을 할 떄 IP 주소와 서버가 실행되고 있는 포트 번호까지 알고 있어야 한다.</li>
</ul>
<h4 id="브라우저-창에-포트-번호를-입력하지-않는-이유">브라우저 창에 포트 번호를 입력하지 않는 이유?</h4>
<ul>
<li>분명히 IP 주소와 Port 를 같이 적어줘야 한다고 했는데, 도메인 주소를 보면 IP 주소 뿐이다.</li>
<li>포트 번호를 입력해주지 않았는데도 어떻게 정상적으로 통신을 하는 걸까?</li>
<li>주소창에 도메인 주소를 입력해서 엔터를 누르면 브라우저(크롬 , 사파리)는 기본적으로 80번 포트로 통신을 보내게 섲렁되어 있다.</li>
<li>그래서 포트 번호를 입력해주지 않아도 정상적으로 통신이 됐던것인데, 만약 80번 포트가 아닌 다른 포트 번호로 통신하고 싶다면 직접 입력해주면 된다.</li>
</ul>
<h4 id="잘-알려진-포트well-known-port">잘 알려진 포트(well-known port)</h4>
<ul>
<li>포트 번호는 0~65,533번까지 사용할 수 있는데, 그 중에서 0 ~ 1023번까지의 포트 번호는 주요 통신을 위한 규약에 따라 이미 정해져있다.</li>
<li>이렇게 규약을 통해 역할이 정해져있는 포트 번호를 보고 well-known port 라고 한다.</li>
<li>22번 (SSH , Secure Shell Protocol) : 원격 접속을 위한 포트 번호</li>
<li>80 (HTTP) : HTTP 로 통신을 할 때 사용</li>
<li>443 (HTTPS) : HTTPS 로 통신을 할 때 사용</li>
</ul>
<p>위에서 정해놓은 규약을 꼭 지키지 않아도 되며, 규약으로 정해져 있는 포트 번호와 다르게 사용해도 된다.</p>
<h3 id="ec2-setting-2">EC2 Setting 2</h3>
<ol start="5">
<li>스토리지 구성</li>
</ol>
<ul>
<li>우리가 쓰고 있는 노트북이나 컴퓨터에도 하드디스크를 가지고, 그 하드디스크는 컴퓨터에서 파일을 저장하는 공간이다.</li>
<li>EC2 도 하나의 컴퓨터이기 때문에 여러 파일들을 저장할 저장 공간이 필요하고, 그 공간을 EBS 라고 한다.</li>
<li>EBS(Elastic Block Storage) 는 EC2 안에 부착되어 있는 일종의 하드디스크라고 ㅅ애각하면 된다.</li>
<li>좀 더 포괄적인 의미로 스토리지 , 볼륨이라고 부른다.</li>
<li>프리티어인 경우 30GiB 까지 무료이며, 종류는 gp3 가 가성비가 좋기 때문에 gp3 를 선택한다.</li>
</ul>
<h3 id="ec2-접속하기">EC2 접속하기</h3>
<ul>
<li>우선 만들어진 EC2 인스턴스의 세부정보에서 퍼블릭 IPv4 주소가 있는데 이 주소가 바로 IP 주소이기 때문에 EC2 인스턴스에 접근하려면 이 IP 주소로 접근하면 된다.</li>
<li>보안에 들어가보면 위에서 설정한 보안 그룹에서 인바운드 규칙과 아웃바운드 규칙에 대한 정보도 나온다.</li>
<li>EC2 에 접속하기 위해서는 AWS 에서 바로 접속할 수 있는 방법도 있고 SSH 클라이언트를 이용해서도 접속할 수 있는데 편한 방법으로 접속하면 된다.</li>
</ul>
<h3 id="탄력적-ip-연결하기">탄력적 IP 연결하기</h3>
<ul>
<li>EC2 인스턴스를 생성하면 IP 를 할당받지만, 이렇게 할당받은 IP 는 임시적인 IP 이다.</li>
<li>EC2 인스턴스를 잠깐 중지시켰다가 다시 실행시켜보면 IP가 바뀌어 있다.</li>
<li>EC2 인스턴스를 중지시켰다가 다시 실행시킬 때마다 IP가 바뀌면 굉장히 불편하기 때문에 중지 시켰다가 다시 실행시켜도 바뀌지 않은 고정 IP 를 할당받아야 한다.</li>
<li>그게 바로 탄력적 IP 이다.</li>
<li>탄력적(Elastic) 의 큰 의미는 없고, 유연하게 사용할 수 있다라는 의미로 붙힌것으로 생각하면 된다.</li>
<li>인터넷이 너무 많아지면서 IP 개수가 모자른 경우가 생기기 떄문에 IP 주소를 임시적으로 발급하고 안 쓰는 경우에는 다른 사람에게 다시 발급하는 형식으로 하기 위해서 이런식으로 한다.</li>
</ul>
<p>설정하는 방법은 왼쪽 메뉴에 탄력적 IP 에 들어가고 탄력적 IP 주소 할당에 들어가서 사용할 인스턴스를 지정하고 만들어주면 된다.</p>
<ul>
<li>직관적이게 이름을 설정해주면 더 좋다.</li>
</ul>
<h3 id="express-서버를-ec2-에-배포하기">Express 서버를 EC2 에 배포하기</h3>
<ul>
<li>Ubuntu 환경에서 Express 서버를 실행시키려면 Node.js 가 필요하기 떄문에 설치를 해야한다.</li>
</ul>
<ol>
<li>EC2 에 접속후 Node.js 설치<pre><code>$ sudo su
$ apt-get update &amp;&amp; /
apt-get install -y ca-certificates curl gnupg &amp;&amp; /
mkdir -p /etc/apt/keyrings &amp;&amp; /
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg &amp;&amp; /
NODE_MAJOR=20 &amp;&amp; /
echo &quot;deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main&quot; | sudo tee /etc/apt/sources.list.d/nodesource.list &amp;&amp; /
apt-get update &amp;&amp; /
apt-get install nodejs -y</code></pre></li>
<li>Github 로부터 프로젝트 clone</li>
<li>.env 와 같은 민감한 파일은 git 에 올리지 않기 때문에 .env 파일은 별도로 EC2 인스턴스에 올려줘야 한다.</li>
</ol>
<ul>
<li>.env 파일을 EC2 인스턴스에 올리는 작업보다는 .env 파일을 직접 만드는게 훨씬 간단하다.</li>
<li>vi .env 로 만들고 여기에 값을 입력해주면 된다.</li>
</ul>
<ol start="4">
<li>pm2 설치해서 서버 실행</li>
</ol>
<ul>
<li>Node 기반의 서버는 pm2 를 활용해서 많이 실행한다.</li>
<li>설치한 후 sudo pm2 start app.js 로 실행</li>
</ul>
<p>그런후에 탄력적 IP 주소로 실행해 보면 정상적으로 배포가 된 것을 확인할 수 있다.</p>
<h3 id="spring-boot-서버를-ec2에-배포하기">Spring Boot 서버를 EC2에 배포하기</h3>
<ol>
<li>Ubuntu 환경에서 JDK 설치 (17버전 이상)<pre><code>sudo apt update &amp;&amp; /
sudo apt install openjdk-17-jdk -y</code></pre></li>
<li>Github 로부터 Spring Boot 프로젝트 Clone 하기</li>
<li>application.yml 파일 직접 만들기</li>
</ol>
<ul>
<li>vi application.yml 에다가 필요한 정보 넣기</li>
</ul>
<ol start="4">
<li>서버 실행시키기<pre><code>./gradlew clean build # 기존 빌드된 파일을 삭제하고 새롭게 JAR로 빌드
cd ~/ec2-spring-boot-sample/build/libs
sudo java -jar ec2-spring-boot-sample-0.0.1-SNAPSHOT.jar
</code></pre></li>
</ol>
<h1 id="백그라운드에서-spring-boot-실행시키는-명령어">백그라운드에서 Spring Boot 실행시키는 명령어</h1>
<p>sudo nohup java -jar ec2-spring-boot-sample-0.0.1-SNAPSHOT.jar &amp;</p>
<p>```</p>
<ul>
<li>우선 ./gradlew clean build 로 JAR 파일 생성하고 java -jar 명령어로 그 JAR 파일을 생성해서 서버를 실행시킨다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커와 CI 환경 - 5]]></title>
            <link>https://velog.io/@support-kim/%EB%8F%84%EC%BB%A4%EC%99%80-CI-%ED%99%98%EA%B2%BD-5</link>
            <guid>https://velog.io/@support-kim/%EB%8F%84%EC%BB%A4%EC%99%80-CI-%ED%99%98%EA%B2%BD-5</guid>
            <pubDate>Sat, 16 Mar 2024 05:19:57 GMT</pubDate>
            <description><![CDATA[<p>이번에는 저번에 만들었던 소스 코드를 Gihub 에 먼저 배포를 하고, Travis CI 라는 곳에서 소스를 가져간 후에 그 곳에서 소스 코드가 잘 돌아가는지 Test를 한 후 만약 성공한다면 AWS 에 보내서 배포까지 해보자.</p>
<h2 id="github-에-소스-코드-올리기">Github 에 소스 코드 올리기</h2>
<ul>
<li>git repo 만들고 git init -&gt; git add . -&gt; git commit -&gt; git remote add origin -&gt; git push</li>
<li>이렇게 소스 코드를 github 에 올릴 수 있고 이 소스가 잘 작성된 코드인지 Travis CI 라는 곳에서 확인해 주기 위해서 Travis CI 에서 가져가 줘야 한다.</li>
</ul>
<h2 id="travis-ci">Travis CI</h2>
<ul>
<li>Travis CI 는 Github 에서 진행되는 오픈소스 프로젝트를 위한 지속적인 통합(Continuous Integration) 서비스이다.</li>
<li>원래는 Ruby 언어만 지원했지만 현재는 대부분의 개발 언어를 지원한다.</li>
<li>Travis CI 를 이용하면 Github Repository 에 있는 프로젝트를 특정 이벤트에 따라 자동으로 테스트 , 빌드하거나 배포할 수 있다.</li>
</ul>
<h3 id="흐름">흐름</h3>
<p>Local git -&gt; Github -&gt; Travis CI -&gt; AWS</p>
<ol>
<li>Local Git 에 있는 소스를 Github 저장소에 Push</li>
<li>Github Main 저장소에 소스가 Push 되면 Travis CI 에게 소스가 Push 되었다고 전달</li>
<li>Travis CI 는 업데이터된 소스를 Github 에서 가지고 온다.</li>
<li>Github 에서 가져온 소스의 테스트 코드를 실행</li>
<li>테스트 코드 실행 후 테스트가 성공하면 AWS 같은 호스팅 사이트로 보내서 배포</li>
</ol>
<h2 id="travis-ci-이용-순서">Travis CI 이용 순서</h2>
<ul>
<li>Travis CI 에서 소스를 가져가야 하기 때문에 Github 와 연결을 해야한다.</li>
<li>연결하기 위해서는 Travis CI 사이트로 가서 Github 로 로그인을 한다.</li>
<li>Settings 페이지로 가서 활성화 시킬 저장소를 찾고 활성화를 시킨다.</li>
<li>활성화를 시키게 되면 해당 저장소의 소스가 변경될 때 마다 소스를 가져와서 테스트하고 배포하라고 알려준다.</li>
</ul>
<p>지금까지는 Travis 에 가입하고 어떤 저장소를 관리할 것인지를 설정해줬다.</p>
<p>이제는 Github 에서 Travis CI 로 소스를 어떻게 전달 시킬 것이며 전달 받은 것을 어떻게 Test 하며 그 테스트가 성공했을 때 어떻게 AWS에 전달해서 배포를 할 것인지를 설정해야한다.</p>
<ul>
<li>travis.yml 작성 </li>
</ul>
<p>즉 Docker 에서는 Dockerfile , docker-compose.yml 에서 무엇을 할지를 작성했다면 Travis CI 에서는 travis.yml 파일에서 해준다.</p>
<h2 id="travisyml-파일-작성하기-테스트까지">.travis.yml 파일 작성하기 (테스트까지)</h2>
<ul>
<li>Travis CI 를 이용해서 테스트 코드를 실행하고 애플리케이션을 배포까지 해줘야 한다.</li>
<li>그러기 위해서는 travis.yml 파일을 작성해야 한다.</li>
</ul>
<ol>
<li>테스트를 수행하기 위한 준비</li>
</ol>
<ul>
<li>도커 환경에서 리액트 앱을 실행하고 있기 떄문에 Travis CI 에서도 도커 환경 구성</li>
<li>구성된 도커 환경에서 도커 파일을 이용해 도커 이미지 생성</li>
</ul>
<ol start="2">
<li>테스트 수행하기</li>
</ol>
<ul>
<li>어떻게 테스트를 수행할 것인지 설정</li>
</ul>
<ol start="3">
<li>AWS 로 배포</li>
</ol>
<ul>
<li>어떻게 AWS 소스 코드를 배포할 것인지 설정<pre><code class="language-yml"># .travis.yml
sudo: required    # 관리자 권한 갖기
language: generic    # 언어(플랫폼) 을 선택
services:    # 도커 환경 구성
- docker
</code></pre>
</li>
</ul>
<p>before_install:    # 스크립트를 실행할 수 있는 환경 구성</p>
<ul>
<li>echo &quot;start creating an image with dockerfile&quot;</li>
<li>docker build -t supportkim/docker-react-app -f Dockerfile.dev .</li>
</ul>
<p>script:        # 실행할 스크립트 (테스트 실행)</p>
<ul>
<li>docker run -e CI=true supportkim/docker-react-app npm run test -- --coverage</li>
</ul>
<p>after_success:    # 테스트 성공 후 할일</p>
<ul>
<li>echo &quot;Test success&quot;<pre><code>- script 부분이 테스트를 하는 부분인데, 테스트를 하기 전에 도커 파일을 이용해서 이미지를 생성해야 한다.
- 그래서 before_install 에서 build 를 해준다.
- sciprt 에서 -e CI=true 설정을 하지 않으면 에러가 발생하기 때문에 꼭 넣어줘야 한다.
- -- --coverage 는 테스트 한 것을 상세하게 볼 수 있도록 하는 옵션이다.
</code></pre></li>
</ul>
<p>해당 소스 코드를 Github 에 다시 push 를 하면 Travis CI 가 yml 파일에 있는 것을 수행한다.</p>
<p>테스트는 성공했고, 이제 AWS 에 배포를 해보자.</p>
<h2 id="aws-알아보기">AWS 알아보기</h2>
<ul>
<li>리액트 앱을 배포하기 위해서 EC2 와 EB 를 사용할 것이다.</li>
</ul>
<h3 id="ec2-란">EC2 란?</h3>
<ul>
<li>Elastic Compute Cloud(EC2) 는 AWS 클라우드에서 확장식 컴퓨팅을 제공한다.</li>
<li>EC2 를 통해 원하는 만큼 가상 서버를 구축하고 보안 및 네트워크 구성과 스토리지 관리가 가능하다.</li>
<li>요구 사항이나 갑작스러운 인기 증대 등 변동 사항에 따라 신속하게 규모를 확장하거나 축소할 수 있어 서버 트래픽 예측 필요성이 줄어든다.</li>
<li>쉽게 말하면 한 대의 컴퓨터를 임대한다고 생각하면 되고, 그 컴퓨터에 OS 를 설치하고 웹 서비스를 위한 프로그램(웹서버 , DB) 을 설치해서 사용하면 된다.</li>
</ul>
<h3 id="eb-란">EB 란?</h3>
<ul>
<li>Elastic BeanStalk(EB) 는 Apache , Nginx 같은 친숙한 서버에서 java , Node.js 및 Docker 와 함께 개발된 웹 응용 프로그램 및 서비스를 배포하고 확장하기 쉬운 서비스이다.</li>
<li>&quot;환경&quot; 을 구성하며 만들고 있는 소프트웨어를 업데이트할 때 마다 자동으로 이 환경을 관리해준다.</li>
<li>즉 EC2 인스턴스들이나 데이터베이스 , Auto-Scaling Group , 로드 밸런서 , Security Group 와 같은 것들을 컨트롤을 한다.</li>
</ul>
<p>원래는 그냥 EC2 에만 배포를 해도 되지만 이번에는 EB 라는 것도 이용해보자.</p>
<h2 id="elastic-beanstalk-환경-구성하기">Elastic BeanStalk 환경 구성하기</h2>
<ul>
<li>Elastic BeanStalk 에서 애플리케이션 만들기</li>
</ul>
<p>AWS 에서 EB Dashboard 에 들어가고 애플리케이션 이름을 적어주고 플랫폼에 현재 Docker 를 사용할 것 이기 때문에 Docker 를 지정하고 플랫폼 브랜치는 일반 Linux 로 하고 애플리케이션을 만들면 된다.</p>
<h3 id="현재까지의-상황">현재까지의 상황</h3>
<ul>
<li>브라우저에서 요청이 들어오면 EB 안에 로드 밸런서가 해당 요청을 받고 EC2 인스턴스에 접근할 수 있도록 해준다.</li>
<li>만약 트래픽이 많아진다면 EC2 인스턴스를 동적으로 몇 개 더 만들어주는데, 이런걸 자동으로 해주는게 EB 이다.</li>
</ul>
<h2 id="travisyml-파일-작성하기-배포-부분">.travis.yml 파일 작성하기 (배포 부분)</h2>
<ul>
<li><p>현재 travis.yml 에는 도커 이미지를 생성하고 어플을 실행하여 테스트 하는 부분만 있다.</p>
</li>
<li><p>이제는 테스트에 성공한 소스를 AWS Elastic Beanstalk 에 자동으로 배포하는 부분을 travis 파일에 넣어주면 된다.</p>
<pre><code class="language-yml">deploy: # 배포 관련된 설정
provider: elasticbeanstalk     # 외부 서비스 표시(s3,eb,firebase 등)

region: ap-northeast-2        # 현재 사용하고 있는 AWS 의 물리적 장소

app: docker-react-app            # 생성한 애플리케이션의 이름

env: &quot;DockerReactApp-env&quot;        # eb 에 명시된 env 명

bucket_name: &quot;S3 bucket 이름&quot;    # 해당 EB 을 위한 S3 버킷 이름

bucket_path: &quot;docker-react-app&quot;    # 애플리케이션의 이름과 동일

on:    # 어떤 브랜치에 푸시할 때 AWS 에 배포할 것인지를 설정
  branch: main</code></pre>
</li>
<li><p>travis CI 의 내용이 바로 Elastic Beanstalk 에 가는게 아니라 travis CI 에서 파일을 압축하고 S3 에 보낸다.</p>
</li>
<li><p>그렇기 떄문에 bucket_name 이 필요하다.</p>
</li>
<li><p>하지만 우리는 S3 를 따로 생성한적이 없지만, Elastic Beanstalk 를 생성하면 S3 Bucket 도 자동적으로 생성된다.</p>
</li>
<li><p>그래서 S3 Dashboard 에서 자동으로 생성된 버킷 이름을 가져오면 된다.</p>
</li>
</ul>
<p>하지만 이렇게 아무런 인증 없이 Travis CI 에서 마음대로 AWS 에 파일을 전송할 수 없다.</p>
<p>이제는 Travis CI 가 AWS 에 접근할 수 있도록 하는 방법을 알아보자.</p>
<h2 id="travis-ci-의-aws-접근을-위한-api-생성">Travis CI 의 AWS 접근을 위한 API 생성</h2>
<ul>
<li>현재까지는 travis CI 에서 AWS 에 어떤 파일을 전해줄것이며, AWS 에서 어떤 서비스를 이용할 것이며, 부수적인 설정들을 적어줬다.</li>
<li>하지만 Travis CI 와 AWS 가 실질적으로 소통을 할 수 있게 인증하는 부분은 없다.</li>
<li>그 인증 하는 부분을 알아보자.</li>
</ul>
<h3 id="소스-파일을-전달하기-위한-접근-요건">소스 파일을 전달하기 위한 접근 요건</h3>
<ul>
<li>Github -&gt; Travis CI -&gt; AWS</li>
<li>Github -&gt; Travis CI 부분은 Travis CI 로그인을 Github 으로 했기 때문에 연동이 됐다.</li>
<li>Travis CI -&gt; AWS 는 AWS 에서 제공하는 액세스 키와 비밀 액세스 키를 travis.yml 파일에다가 적어주면 된다.</li>
</ul>
<p>인증을 위해서는 API Key 가 필요한데, 어떻게 API Key 를 얻을 수 있을까?</p>
<ol>
<li>IAM(Identity and Access Management) USER 생성</li>
</ol>
<ul>
<li>IAM 은 AWS 리소스에 대한 액세스를 안전하게 제어할 수 있는 서비스</li>
<li>IAM을 사용하여 리소스를 사용하도록 인증 및 권한 부여된 대상을 제어한다.</li>
<li>우리가 사용하고 있는 AWS 계정은 ROOT 사용자로 서비스 및 리소스에 대한 모든 접근 권한이 있는데, ROOT 사용자를 사용하는 방법은 보안적으로 바람직 하지 않기 떄문에 IAM 유저를 사용해야 한다.</li>
<li>IAM 사용자는 ROOT 사용자가 부여한 권한만 가지고 있을 수 있다.</li>
<li>IAM Dashboard 에 가서 IAM 을 만들 때 AdministatorAccess-AWSElasticBeanstalk 권한 추가한다.</li>
</ul>
<ol start="2">
<li>API 키를 Travis yml 파일에 적어주기 </li>
</ol>
<ul>
<li>1번 과정을 하게 되면 액세스 키 ID , 비밀 액세스 키가 주어진다.</li>
<li>이때 직접 API 키를 travis.yml 파일에 넣게되면 문제가 된다.</li>
<li>travis.yml 은 github 에 올라가기 때문에 다른 사람들에게 유출될 수 있어서 travis.yml 에다가 올리면 안 되고 다른 곳에 적고 그것을 가져오는 형식으로 해야한다.</li>
<li>위에서 말한 다른 곳은  Travis 웹사이트 해당 저장소 대쉬보드에 가면 more options -&gt; settings 에서 아래를 보면 Environment Variables 에다가 넣어주면 된다.</li>
<li>NAME : AWS_ACCESS_KEY VALUE : 액세스 키 ID 넣고 ADD 버튼</li>
<li>NAME : AWS_SECRET_ACCESS_KEY VALUE : 비밀 액세스 키 넣고 ADD 버튼<pre><code class="language-yml">deploy:
provider: elasticbeanstalk
region: ap-northeast-2
app: docker-react-app
env: &quot;DockerReactApp-env&quot;
bucket_name: &quot;S3 bucket 이름&quot;
bucket_path: &quot;docker-react-app&quot;
on:
  branch: main
access_key_id: $AWS_ACCESS_KEY
secret_access_key: $AWS_SECRET_ACCESS_KEY</code></pre>
</li>
<li>위와 같이 적어주면 된다.</li>
</ul>
<p>이렇게 Travis CI 가 AWS 에 접근할 수 있도록 만들어줬다.</p>
<p>git add . -&gt; git commit -&gt; git push 를 하게 되면 Travis CI 대시보드를 가보면 현재 push 한 소스 코드를 가지고 실행을 한다.</p>
<ul>
<li>그런후 EB 에 들어가보면 새로운 소스가 왔기 때문에 그 소스를 가지고 컨테이너를 실행하고 어플리케이션을 실행한다.</li>
</ul>
<p>하지만 에러가 발생한다..</p>
<p>포트 맵핑을 하지 않아서 발생한 에러이다.</p>
<ul>
<li>nginx 는 80 포트번호에서 실행되는데 80 번에 포트 맵핑이 되지 않아서 에러가 발생한다.</li>
<li>Github 에 Main branch 에 소스를 push 하면 자동으로 AWS 에 배포할 수 있게 하는것에 있어서 마지막 포트 매핑을 해야한다.</li>
<li>Dockerfile 에 EXPOSE 80 을 넣어줘서 포트 매핑 문제를 해결할 수 있다.</li>
<li>완벽하게 배포가 되기 위해서 약간의 시간이 소요된다.</li>
</ul>
<p>이제는 Github 에 Main 브랜치에 푸시를 하면 모든 프로세스가 한 번에 돌아가고 앱이 배포가 된다.</p>
<p>작업에서 환경 종료를 눌러야 과금이 생기지 않는다..!</p>
<h2 id="travis-ci-에서-github-action-으로-교체하기-처음부터-끝까지">Travis CI 에서 Github Action 으로 교체하기 (처음부터 끝까지)</h2>
<ul>
<li>Github Action 으로 CI/CD 를 구축해보자.</li>
</ul>
<ol>
<li>npx create-react-app ./</li>
<li>필요한 Dockerfile 작성<pre><code># .dockerignore
package-lock.json
node_modules
</code></pre></li>
</ol>
<h1 id="dockerfiledev">Dockerfile.dev</h1>
<p>FROM node:16-alpine</p>
<p>WORKDIR &#39;/app&#39;</p>
<p>COPY package.json .
RUN npm install</p>
<p>COPY . .</p>
<p>CMD [&quot;npm&quot; , &quot;run&quot; , &quot;start&quot;]</p>
<h1 id="dockerfile">Dockerfile</h1>
<p>FROM node:16-alpine as builder</p>
<p>WORKDIR &#39;/app&#39;</p>
<p>COPY package.json .
RUN npm install
COPY . .
RUN npm run build</p>
<p>FROM nginx
COPY --from=builder /app/build /usr/share/nginx/html</p>
<pre><code>```yml
# docker-compose-dev.yml
version: &#39;3&#39; # 도커 컴포즈 버전
services:   # 이곳에 실행하려는 컨테이너들을 정의
  react:    # 컨테이너 이름
    build:  # 현 디렉토리에 있는 Dockerfile 사용
      context: . # 도커 이미지를 구성하기 위한 파일과 폴더들이 있는 위치 (현재는 Dockerfile.dev 와 같은 경로에 있기 때문에 . 으로 명시)
      dockerfile: Dockerfile.dev # 도커 파일 어떤 것인지 지정
    ports: # 포트 매핑 -&gt; 로컬 포트 : 컨테이너 포트
    - &quot;3000:3000&quot;
    volumes: # 로컬 머신에 있는 파일들 맵핑
      - /usr/src/app/node_modules
      - ./:/usr/src/app
    stdin_open: true # 리액트 앱을 끌때 필요 (버그 수정)
  tests:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - /usr/src/app/node_modules
      - ./:/usr/src/app
    command: [&quot;npm&quot; , &quot;run&quot; , &quot;test&quot;]

# docker-compose.yml
version: &quot;3&quot;
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - &quot;80:80&quot;</code></pre><ol start="3">
<li><p>IAM 설정
AWSElasticBeanstalkMulticontainerDocker<br>AWSElasticBeanstalkWebTier<br>AWSElasticBeanstalkWorkerTier</p>
</li>
<li><p>ElasticBeanstalk 환경 만들기</p>
</li>
</ol>
<ul>
<li>플랫폼에서 Docker , 브랜치는 Linux2</li>
<li>서비스 액세스에서 새 서비스 역할 생성 및 사용으로 하고 EC2 인스턴스 프로파일에서 3번에서 설정한 역할로 지정해주면 된다.</li>
</ul>
<ol start="5">
<li>IAM 유저 생성</li>
</ol>
<ul>
<li>직접 정책 연결 선택</li>
<li>AdministratorAccess-AWSElasticBeanstalk 권한 정책 선택후 유저 생성</li>
<li>생성한 유저에 들어가서 아래로 내리다 보면 액세스 키가 있고 여기서 액세스 키를 만들면 된다. (보안 자격 증명 , 사용 사례는 CLI)</li>
<li>액세스 키와 시크릿 키 보관</li>
</ul>
<p>react 앱을 github 에 올려서 자동으로 테스트를 하고 EB 를 통해 배포를 하도록 한다.</p>
<ol start="6">
<li>Github 저장소 생성</li>
</ol>
<ul>
<li>git init -&gt; git add -&gt; git commit -&gt; git remote add origin -&gt; git push</li>
</ul>
<ol start="7">
<li>Travis CI 가 아닌 Github Action 사용</li>
</ol>
<ul>
<li>react 앱 안에 .github/workflows/deploy.yaml 파일을 만들고 아래와 같이 작성<pre><code class="language-yaml">name: Deploy Frontend
# Main 브랜치에 push 를 했을때만 아래에 있든 task 를 실행
on:
push:
  branches:
    - main
</code></pre>
</li>
</ul>
<p>jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      # docker Login
      - run: echo &quot;${{ secrets.DOCKER_PASSWORD }}&quot; | docker login -u &quot;${{ secrets.DOCKER_USERNAME }}&quot; --password-stdin
      # Dockerfile.dev 로 build
      - run: docker build -t supportkim/react-test -f Dockerfile.dev .</p>
<pre><code>  # 위에서 만든 image 로 run (test)
  - run: docker run -e CI=true supportkim/react-test npm test
  - name: Generate deployment package
    run: zip -r deploy.zip . -x &#39;*.git*&#39;

    # EB 에 배포
  - name: Deploy to EB
    uses: einaregilsson/beanstalk-deploy@v18
    # 필요한 accessKey , secretKey 넣어주기
    with:  
      aws_access_key: ${{ secrets.AWS_ACCESS_KEY }}
      aws_secret_key: ${{ secrets.AWS_SECRET_KEY }}
      application_name: react-docker-gh-test
      environment_name: React-docker-gh-test-env 
      existing_bucket_name: elasticbeanstalk-ap-northeast-2-637423586273
      region: ap-northeast-2
      version_label: ${{ github.sha }}
      deployment_package: deploy.zip</code></pre><p>```</p>
<ul>
<li>Docker 의 정보와 AWS_KEY 에 대한 정보를 넣어줘야 한다.</li>
<li>Github 에서 해당 저장소에서 Settings -&gt; Security -&gt; Secrets and Variables -&gt; Actions -&gt; New Repository Secret 에다가 하나씩 넣어주면 된다.</li>
<li>DOCKER_USERNAME , DOCKER_PASSWORD , AWS_ACCESS_KEY , AWS_SECRET_KEY</li>
</ul>
<p>이렇게 설정하고 나고 push 를 하면 github 에 올라가게 되고 바로 Github Action 을 통해서 테스트와 배포가 자동으로 이루어진다. </p>
<ul>
<li>저장소에서 Action 에 들어가보면 진행 과정도 볼 수 있다.</li>
</ul>
<p>모든 과정이 끝나고 나면 AWS 의 Elastic Beanstalk Dashboard 가보면 환경을 업데이트가 된다.</p>
<p>이제는 Main 브랜치에 push 하기만 하면 알아서 테스트 후 배포까지 된다.</p>
<p><a href="https://www.inflearn.com/course/%EB%94%B0%EB%9D%BC%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8F%84%EC%BB%A4-ci/dashboard">참고자료</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커와 CI 환경 - 4]]></title>
            <link>https://velog.io/@support-kim/%EB%8F%84%EC%BB%A4%EC%99%80-CI-%ED%99%98%EA%B2%BD-4</link>
            <guid>https://velog.io/@support-kim/%EB%8F%84%EC%BB%A4%EC%99%80-CI-%ED%99%98%EA%B2%BD-4</guid>
            <pubDate>Fri, 15 Mar 2024 08:27:22 GMT</pubDate>
            <description><![CDATA[<p>이번에는 개발환경에서는 리액트 앱을 개발하고 만든 리액트 앱을 테스트 및 배포를 해보자.</p>
<p>간단하게 보면 개봘환경에서 개발 -&gt; 개발 된 것을 테스트 -&gt; Production 환경에 배포</p>
<ul>
<li>더 자세한 흐름으로 알아보자.</li>
<li>개발환경에서 개발 -&gt; Github 에 소스를 push -&gt; Featrue 브랜치에서 Main 브랜치에 Pull 요청</li>
<li>Travis CI 에서 Main 브랜치에서 push 된 코드를 가져가서 개발된 소스가 잘 작동하는지 먼저 Test 를 한다.</li>
<li>만약 테스트가 성공하면 호스팅 사이트(AWS , Azure , Google...)로 보내서 배포를 한다.</li>
</ul>
<h2 id="리액트-앱-설치하기">리액트 앱 설치하기</h2>
<ul>
<li>node 를 받으면 리액트는 매우 간단하게 설치할 수 있다.</li>
<li>npx create-react-app DIR_NAME (리액트를 설치하고자 하는 디렉토리 이름)</li>
<li>만약 현재 있는 경로에 설치하고 싶다면 npx create-react-app ./</li>
</ul>
<h3 id="다른-명령어들">다른 명령어들</h3>
<ul>
<li>npm run start : 실행</li>
<li>npm run test : 테스트 (개발 완료 후 문제가 있는지 없는지 확인)</li>
<li>npm run build</li>
<li><blockquote>
<p>테스트도 완료가 됐다면 배포를 해서 다른 사람들도 이용할 수 있도록 하는 배포 명령어</p>
</blockquote>
</li>
<li><blockquote>
<p>실행하면 배포를 할 때 사용할 수 있는 build 폴더와 그 안에 많은 파일들이 생성된다.</p>
</blockquote>
</li>
</ul>
<p>이제는 도커를 이용하여 리액트 앱을 실행해보자.</p>
<h2 id="도커를-이용하여-리액트-앱-실행하기">도커를 이용하여 리액트 앱 실행하기</h2>
<h3 id="도커로-어플을-실행하기-위한-흐름">도커로 어플을 실행하기 위한 흐름</h3>
<ul>
<li>도커 이미지 생성 -&gt; 이미지를 이용해 컨테이너 만들기 -&gt; 컨테이너 안에서 앱을 실행</li>
<li>도커 이미지를 생성하기 위해서는 Dockerfile 을 작성</li>
</ul>
<p>현재까지는 Dockerfile 을 그냥 한 가지만 만들었지만, 실제로는 개발 단계를 위한 Dockerfile 과 실제 배포 이후를 위한 Dockerfile 을 따로 작성하는게 좋다.</p>
<ul>
<li>이제는 그냥 Dockerfile 이 아닌 Dockerfile.dev 라는 파일로 작성해보자.</li>
</ul>
<p>개발 환경에서의 도커 파일 작성은 현재까지 도커 파일 작성했던 것과 똑같이 하면 된다.</p>
<pre><code>#Dockerfile.dev

FROM node:alpine

WORKDIR /usr/src/app

COPY pacage.json ./

RUN npm install

COPY ./ ./

CMD [&quot;npm&quot; , &quot;run&quot; , &quot;start&quot;]</code></pre><ul>
<li>전에 배운 내용이지만 COPY package.json ./ 을 해주는 이유는? </li>
<li>COPY ./ ./ 이후에 npm install 을 하게 되면 약간의 소스 변경을 하고 나서 종속성도 계속 다운로드 받기 때문에 먼저 종속성 부분을 먼저 COPY 해오고 npm install 을 한다.</li>
<li>npm run start 로 실행되기 떄문에 CMD 부분에 넣어준다.</li>
</ul>
<p>이렇게 Dockerfile.dev 를 작성하고 나면 이 도커 파일로 이미지를 만들면 된다.</p>
<ul>
<li>docker build ./ 으로 생성</li>
</ul>
<p>하지만 에러가 발생한다..</p>
<ul>
<li>그 이유는 원래 이미지를 빌드할 때 해당 디렉토리만 정해주면 Dockerfile 을 자동으로 찾아서 빌드를 하지만, 현재는 Dockerfile 이 아닌 Dockerfile.dev 밖에 없다.</li>
<li>그래서 자동으로 도커 파일을 찾지 못해 에러가 발생한다.</li>
<li>이러한 문제를 해결하기 위해서는 build 할 때 어떠한 파일을 참조할지 알려주면 된다.</li>
<li>임의로 알려주는 방법은 빌드를 할 때 그냥 docker build . 이 아닌 -f 옵션을 사용해야 한다.</li>
</ul>
<p>docker run -f Dockerfile.dev ./</p>
<ul>
<li>-f 는 이미지를 빌드 할 때 쓰일 도커 파일을 임의로 지정해준다.</li>
<li>그래서 이런식으로 -f 옵션을 사용하면 정상적으로 이미지 빌드가 된다.</li>
</ul>
<h3 id="팁">팁</h3>
<ul>
<li>현재 디렉토리에 보면 node_modules 라는 폴더가 있는데 이것을 삭제해도 괜찮다.</li>
<li>그 이유는 node_modules 에는 리액트 앱을 실행할 떄 필요한 모듈들이 들어있지만 이미지를 빌드할 때 이미 npm install 로 모든 모듈들을 도커 이미지에 다운 받기 떄문에 굳이 로컬 머신에 node_modules 을 필요로 하지 않는다.</li>
<li>node_modules 자체가 사이즈가 크기 떄문에 지워주도록 하자.</li>
</ul>
<h2 id="생성된-도커-이미지로-리액트-앱-실행해보기">생성된 도커 이미지로 리액트 앱 실행해보기</h2>
<p>docker build -f Dockerfile.dev -t supportkim/docker-react-app ./</p>
<p>docker run supportkim/docker-react-app</p>
<p>리액트는 기본적으로 3000번 포트로 실행되기 때문에 3000번 포트로 실행해보면 에러가 발생한다..</p>
<ul>
<li>이유는 이전과 같이 포트 매핑을 해줘야 하는데 안 해줬기 때문이다.</li>
<li>컨테이너 안에서 실행되고 있는 리액트 앱에 도달하지 못한 것이다.</li>
<li>즉 브라우저의 3000번 포트와 컨테이너 안에 있는 3000번 포트를 매핑해줘야 한다.</li>
</ul>
<p>docker run -it -p 3000:30000 supportkim/docker-react-app</p>
<ul>
<li>리액트쪽에서 업그레이드를 해서 -it 옵션을 꼭 사용해야한다.</li>
<li>실행해보면 정상적으로 동작하는 것을 확인할 수 있다.</li>
</ul>
<h2 id="도커-볼륨을-이용한-소스-코드-변경">도커 볼륨을 이용한 소스 코드 변경</h2>
<ul>
<li>COPY 를 했을때는 로컬에 있던 파일들을 도커 컨테이너에 &quot;복사&quot;</li>
<li>Volume 은 도커 컨테이너에서 로컬에 있는 파일들을 &quot;매핑(참조)&quot;</li>
</ul>
<p>docker run -it -p 3000:3000 -v /usr/src/app/node_modules -v $(pwd):/usr/src/app supportkim/docker-react-app(IMAGE_NAME)</p>
<ul>
<li>첫 번째 -v 는 호스트 디렉토리에 node_modules 는 없기 때문에 매핑하지 말라고 하는 것</li>
<li>두 번째 -v 는 pwd 경로에 있는 디렉토리 혹은 파일을 /usr/src/app 경로에서 참조하라는 것 (WORKDIR 에서 pwd 에 있는 파일들을 참조하자는 것)</li>
</ul>
<p>소스 코드를 바꾸면 빌드하지 않아도 바로 반영 되는 것을 알 수 있다.</p>
<h2 id="도커-컴포즈로-좀-더-간단하게-앱-실행-하기">도커 컴포즈로 좀 더 간단하게 앱 실행 하기</h2>
<ul>
<li>앞서 리액트 앱을 실행할 때 위에 있는 명령어처럼 매우 길어서 불편한 부분이 있다.</li>
<li>이걸 간단히 하기 위해서 Docker Compose 파일을 작성해보자.</li>
<li>Docker compose 파일에서 도커 파일 사용 + 포트 맵핑을 해주면 된다.<pre><code class="language-yml">version: &#39;3&#39; # 도커 컴포즈 버전
services:   # 이곳에 실행하려는 컨테이너들을 정의
react:    # 컨테이너 이름
  build:  # 현 디렉토리에 있는 Dockerfile 사용
    context: . # 도커 이미지를 구성하기 위한 파일과 폴더들이 있는 위치 
                 #(현재는 Dockerfile.dev 와 같은 경로에 있기 때문에 . 으로 명시)
    dockerfile: Dockerfile.dev # 도커 파일 어떤 것인지 지정
  ports: # 포트 매핑 -&gt; 로컬 포트 : 컨테이너 포트
  - &quot;3000:3000&quot;
  volumes: # 로컬 머신에 있는 파일들 맵핑
    - /usr/src/app/node_modules # node_modules 는 참조 X
    - ./:/usr/src/app # WORKDIR 에서 현재 디렉토리 참조 O
  stdin_open: true # 리액트 앱을 끌때 필요 (버그 수정)</code></pre>
</li>
<li>작성한 후 docker-compose up 명령어만 입력하면 어플리케이션이 실행된다.</li>
</ul>
<h2 id="리액트-앱-테스트-하기">리액트 앱 테스트 하기</h2>
<ul>
<li>npm run test : 리액트 앱에서 테스트 진행</li>
</ul>
<h3 id="도커를-이용한-리액트-앱에서-테스트-진행">도커를 이용한 리액트 앱에서 테스트 진행</h3>
<ul>
<li>docker build -f dockerfile.dev .</li>
<li>docker run -it IMAGE_NAME npm run test</li>
</ul>
<p>여기서 좀 더 나아가서 테스트도 소스 코드 변경하면 자동으로 반영되는 것 처럼 테스트 소스도 추가 하면 바로 반영되었으면 좋을텐데 어떻게 할 수 있을까?</p>
<ul>
<li>기존에 있던 테스트 말고 새로운 테스트 소스 코드를 만든후 다시 테스트를 진행해도 추가가 되지 않는다.</li>
<li>어플리케이션을 껐다가 다시 켜도 테스트가 더 추가로 진행되지 않는다.</li>
<li>바로 반영 하기 위해서 똑같이 Volume 을 사용하면 된다.</li>
</ul>
<p>소스 코드 변경을 위해서 Volume 을 이용했듯이 이번에도 Volume 을 이용하면 되는데, Test 를 위한 컨테이너를 Compose 파일에 하나 더 만들어주면 된다.</p>
<pre><code class="language-yml"># 이 부분을 추가
tests:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - /usr/src/app/node_modules
      - ./:/usr/src/app
    command: [&quot;npm&quot; , &quot;run&quot; , &quot;test&quot;]</code></pre>
<ul>
<li>다시 docker-compose up --build 로 켜주면 리액트 컨테이너와 테스트 컨테이너가 켜지고 테스트 2개 모두 진행이 된다.</li>
<li>만약 테스트를 하나 더 늘리고 저장을 하면 바로 3개의 테스트가 진행이 된다.</li>
</ul>
<h2 id="운영환경을-위한-nginx">운영환경을 위한 Nginx</h2>
<p>현재까지는 리액트앱을 개발 환경에서 다뤄봤는데 이제는 운영 환경(배포 후)을 다뤄보자.
-그전에 Nginx 라는 것을 알아야한다.</p>
<h3 id="nginx-가-필요한-이유">Nginx 가 필요한 이유?</h3>
<ul>
<li>먼저 개발환경에서 리액트가 실행되는 과정과 운영 환경에서 리액트가 실행되는 과정이 다르다.</li>
<li>개발 환경에서는 브라우저에서 3000번 포트로 요청이 리액트 컨테이너로 가면 컨테이너 안에 개발 서버에서 해당 요청을 받고 뷰를 보여준다.</li>
<li>하지만 운영 환경에서는 리액트 컨테이너 안에 개발 서버라는 것이 없기 떄문에 뷰를 보여줄 수 없다.</li>
<li>이때 Nginx 라는 웹 서버가 개발 환경의 개발 서버 역할을 해주는 것이다.</li>
</ul>
<p>그렇다면 왜 개발환경 서버와 운영환경 서버를 다른걸 써야할까?</p>
<ul>
<li>개발서버를 그대로 운영 환경에서도 써도 될 것 같은데 왜 그럴까?</li>
<li>개발에서 사용하는 서버는 소스를 변경하면 자동으로 전체 앱을 다시 빌드해서 변경 소스를 반영해주는 것 같이 개발 환경에 특화된 기능들이 있기에 그러한 기능이 없는 Nginx 서버보다 더욱 적합하다.</li>
<li>운영환경에서는 소스를 변경할 떄 다시 반영해줄 필요가 없고 개발에 필요한 기능들이 필요하지 않기에 더 깔끔하고 빠른 Nginx 를 웹 서버로 사용한다.</li>
</ul>
<h2 id="운영-환경-도커-이미지를-위한-dockerfile-작성">운영 환경 도커 이미지를 위한 Dockerfile 작성</h2>
<ul>
<li>위에서 운영 환경에서는 Nginx 가 필요하다는 것을 알게됐기 떄문에 이제는 Nginx 를 포함하는 리액트 운영환경 이미지를 생성해보자.</li>
<li>npm run build 명령어를 실행하면 build 파일이 생성되는데, 이 빌드 파일을 Nginx 서버가 브라우저에서 보일 수 있게 해준다.<pre><code># 여기 FROM 부터 다음 FROM 전까지는 모두 builder stage 라는 것을 명시
FROM node:alpine as builder
</code></pre></li>
</ul>
<p>WORKDIR &#39;/usr/src/app&#39;</p>
<p>COPY package.json .</p>
<p>RUN npm install</p>
<p>COPY ./ ./</p>
<p>RUN npm run build</p>
<p>FROM nginx</p>
<p>COPY --from=builder /usr/src/app/build /usr/share/nginx/html</p>
<p>```</p>
<ul>
<li>위에 Dockerfile 은 크게 두 개로 구분할 수 있다.</li>
<li>첫 번째 단계는 빌드 파일들을 생성하는 Builder Stage</li>
<li>두 번쨰 단계는 Nginx 를 가동하고 첫 번째 단계에서 생성된 빌드 폴더의 파일들을 웹 브라우저의 요청에 따라 제공해주는 Run Stage</li>
<li>첫 번째 FROM 부터 다음 FROM 나오기 직전이 Builder Stage</li>
<li>두 번째 FROM 부터 Run Stage</li>
</ul>
<h3 id="builder-stage">Builder Stage</h3>
<ul>
<li>builder stage 란 빌드 파일들을 생성하는 것인데 생성된 파일과 폴더들은 /usr/src/app/build 로 들어가게 된다.</li>
<li>as builder 으로 다음 FROM 나오기 전까지가 builder stage 라는 것을 알려준다.</li>
</ul>
<h3 id="run-stage">Run Stage</h3>
<ul>
<li>FROM nginx 는 nginx 를 위한 베이스 이미지를 명시해준다.</li>
<li>--from=builder 는 다른 Stage 에 있는 파일을 복사할 때 다른 Stage 이름을 명시해줘야 하기 때문에 위에 있는 builder 를 복사해온다.</li>
<li>builder stage 에서 생성된 파일들은 /usr/src/app/build 에 들어가게 되며 그곳에 저장된 파일들을 /usr/share/nginx/html 로 복사를 시켜줘서 nginx 가 웹 브라우저의 http 요청이 올떄 마다 알맞은 파일을 전해줄 수 있게 만든다.</li>
<li>/usr/share/nginx/html 로 복사해주는 이유는 이 장소에 파일을 넣어 두면 Nginx 가 알아서 Client 에서 요청이 들어올 때 알맞은 정적 파일들을 제공해준다.</li>
<li>설정으로 장소를 바꿀 수 있다.</li>
</ul>
<p>해당 Dockerfile 을 만들고 build 한 후 docker run -p 8080:80 supportkim/docker-react-app 명령어를 실행하면 정상적으로 실행이 된다.</p>
<p>앞으로는 테스트 하고 배포를 해보자!</p>
<p><a href="https://www.inflearn.com/course/%EB%94%B0%EB%9D%BC%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8F%84%EC%BB%A4-ci/dashboard">참고자료</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커와 CI 환경 - 3]]></title>
            <link>https://velog.io/@support-kim/%EB%8F%84%EC%BB%A4%EC%99%80-CI-%ED%99%98%EA%B2%BD-3</link>
            <guid>https://velog.io/@support-kim/%EB%8F%84%EC%BB%A4%EC%99%80-CI-%ED%99%98%EA%B2%BD-3</guid>
            <pubDate>Thu, 14 Mar 2024 12:58:01 GMT</pubDate>
            <description><![CDATA[<p>가장 중점적으로 봐야할 것은 Dockerfile 을 어떤식으로 작성해야 하는지가 중요하다.</p>
<ul>
<li>Node.js APP 을 만들고 도커 이미지를 생성 해서 컨테이너로 실행해보자.</li>
<li>즉 도커 컨테이너 안에서 Node.js 애플리케이션이 실행될 수 있도록 하자.</li>
</ul>
<h2 id="nodejs-app-만들기">Node.js APP 만들기</h2>
<ul>
<li>필요한 2가지 파일</li>
</ul>
<ol>
<li>package.json : 프로젝의 정보와 프로젝트에서 사용 중인 패키지의 의존성을 관리하는 곳</li>
<li>server.js : Node.js 에서 진입점이 되는 파일</li>
</ol>
<p>터미널에서 npm init 명령어로 package.json 만들기</p>
<ul>
<li>npm = Node Package Manager<pre><code>&quot;dependencies&quot;: {
  &quot;express&quot; : &quot;4.18.3&quot;
}</code></pre></li>
<li>만들어진 package.json 에서 express 라는 라이브러리를 추가</li>
<li>express 는 Node.js 를 더 쉽고 유용하게 사용할 수 있도록 해준다.<pre><code class="language-js">// Express 모듈 불러오기
const express = require(&#39;express&#39;);
</code></pre>
</li>
</ul>
<p>// 포트 설정
const PORT = 8080;</p>
<p>// APP
const app = express();
app.get(&#39;/&#39; , (req,res) =&gt; {
    res.send(&quot;Hello World&quot;)
});</p>
<p>// 실행
app.listen(PORT)</p>
<pre><code>## Dockerfile 작성하기
- Node.js APP 을 도커 환경에서 실행하려면 먼저 이미지를 생성하고 그 이미지를 이용해서 컨테이너를 실행한 후 그 컨테이너 안에서 Node.js APP 을 실행해야 한다.
- 그래서 그 이미지를 먼저 생성하기 위해서 Dockerfile 을 먼저 작성해야 한다.
- Dockerfile 만드는 부분은 도커와 CI 환경 - 2 참고

### 기본적인 명령어로 작성해보기</code></pre><p>FROM node:10</p>
<p>RUN npm install express</p>
<p>CMD [&quot;node&quot; , &quot;server.js&quot;]</p>
<pre><code>FROM
- 전에 했었던 것 처럼 FROM alpine 으로 하면 오류가 발생한다.
- 그 이유는 npm 을 위한 파일이 없기 때문에 아래에 RUN 부분을 실행할 수 없는 것이다.
- 그래서 npm 을 사용할 수 있도록 node:10 베이스 이미지를 사용해야 한다.

RUN
- npm 은 Node.js 로 만들어진 모듈을 웹에서 받아서 설치하고 관리해주는 프로그램
- npm install 은 package.json 에 적혀있는 종속성들을 웹에서 자동으로 다운 받아서 설치해주는 명령어
- 결론적으로 현재 Node.js APP 을 만들때 필요한 모듈들을 다운 받아 설치하는 역할을 한다.
- Node.js 에서 npm install 이 실행되면 NPM Registry 모듈들이 저장되어 있는 곳이 있고 우리는 Express 라는 라이브러리를 사용했기 떄문에 이 모듈을 내려 받아서 Node.js APP 에 전달해준다.

CMD
- 노드 웹 서버를 작동시키려면 node + 엔트리 파일 이름을 입력해야하기 때문에 위와 같이 넣어줬다.

위에 처럼 작성하고 docker build ./ 명령어를 실행하면 오류가 발생한다..
- package.json 이 없다는 오류

그 이유가 무엇일까?

## Package.json 파일이 없다고 나오는 이유?
- 우선 결론부터 말하면 COPY 라는 것을 안해서 발생하는 오류이다.

### Dockerfile 을 이미지로 만드는 과정
- 도커와 CI 환경 - 2 에 자세히 있다.
- 간략하게 설명하면 Dockerfile 에 있는 베이스 이미지로 임시 컨테이너를 만들고 그 임시컨테이너에서 이미지를 만들고 임시컨테이너는 삭제된다.
- 해당 예제에서는 Node 베이스 이미지로 임시 컨테이너를 생성하고, 그 임시 컨테이너로 이미지를 만들것이다.
- 하지만 임시 컨테이너에는 package.json 이 Node 이미지 파일 스냅샷에 포함되어 있지 않다.
- 임시 컨테이너 하드디스크 부분에 파일 스냅샷에는 package.json 도 없고 server.js 도 없어서 파일들을 찾지 못해 생기는 오류인 것이다.
- package.json 은 컨테이너 밖에 있는 상황인 것이다.

그래서 도커 컨테이너의 복사를 해줘서 현재 디렉토리에 있는 모든 파일을 컨테이너 안으로 집어넣어줘야 한다.</code></pre><p>FROM node:10</p>
<p>COPY ./ ./</p>
<p>RUN npm install express</p>
<p>CMD [&quot;node&quot; , &quot;server.js&quot;]</p>
<pre><code>- COPY 에서 ./ ./ 부분은 현재 디렉토리 즉 package.json , server.js 파일 모두를 도커 컨테이너에 복사를 해주는 것이다.
- docker build -t supportkim/nodejs
- docker run spportkim/nodejs

오류가 발생하지 않아서 8080 포트로 접속해본 결과 또 접속할 수 없다는 오류가 발생한다..

그 이유는 또 무엇일까?

## 생성한 이미지로 어플리케이션 실행 시 접근이 안 되는 이유
- 결론은 포트 매핑을 하지 않아서 접근이 안 되는 것이다.
- 앞으로는 컨테이너를 실행할 때 다음과 같이 사용해야 한다.
- docker run -p PORT:PORT IMAGE_NAME

### -p PORT:PORT
- 우리가 이미지를 만들 때 로컬에 있던 파일(package.json , server.js) 등을 컨테이너에 복사해줘야 했다.
- 그것과 비슷하게 네트워크도 로컬 네트워크에 있던 것을 컨테이너 내부에 있는 네트워크에 연결을 시켜줘야 한다.

브라우저 -&gt; 로컬호스트 네트워크 -&gt; 컨테이너 네트워크
- 위와 같은 순서를 거쳐서 컨테이너에 잇는 네트워크에 도달하게 된다.
- docker run -p 5000:8080 supportkim/nodejs
- 위 명령어를 실행하고 localhost:5000 으로 접속하면 Hello World 라는 문구가 보이게 된다.
- 즉 브라우저의 5000번 포트와 컨테이너에 있는 8080 포트가 매핑이 된 것이다.
- 5000 포트가 아닌 1234 포트로 설정하게 된다면 localhost:1234 로 접속하면 된다.

## Working Directory 명시해주기
- 도커 파일에 WORKDIR 이라는 부분을 추가해야한다.
- 무엇을 위해서 추가해야할까?

이미지 안에서 애플리케이션 소스 코드를 가지고 있을 디렉토리를 생성하는 것이다.
- 그리고 이 디렉토리가 애플리케이션에 working directory 가 된다.

왜 따로 working directory 를 만들어야 할까?

### 비교
- docker run -it node ls
- docker run -it supportkim/nodejs ls 
- 2개의 명령어 차이를 봐보자.
- 첫 번째는 베이스 이미지인 Node 의 이미지인데 그 이미지 안에는 home , bin , dev 등의 파일들이 있다.
- 두 번째는 현재 우리가 만든 이미지인데 그 이미지 안에는 Dockerfile , package.json , server.js 등 더 많은 파일들이 있다.
- COPY 명령어를 컨테이너 안으로 들어온 것들이다.

### workdir 를 지정하지 않고 그냥 COPY 할 때 생기는 문제점 
1. 혹시 이 중에서 원래 이미지에 있던 파일이 같다면, 원래 있던 폴더가 덮어 써져 버린다.
2. 모든 파일이 한 디렉토리에 들어가기 떄문에 너무 정리 정돈이 되지 않는다.

그래서 모든 어플리케이션을 위한 소스들은 WORK 디렉토리를 따로 만들어서 보관해야 한다.</code></pre><p>FROM node:10</p>
<p>WORKDIR /usr/src/app</p>
<p>COPY ./ ./</p>
<p>RUN npm install express</p>
<p>CMD [&quot;node&quot; , &quot;server.js&quot;]</p>
<pre><code>- WORKDIR 이후에 Working Directory 의 경로를 적어주면 된다.
- 위와 같이 Dockerfile 을 만들고 다시 이미지 생성하고 컨테이너에서 실행한 후 docker run -it IMAGE_NAME sh 명령어를 실행해보자.
- 실행한 후 ls 를 입력하면 Dockerfile , package.json 등 우리가 COPY 한 파일들이 나온다.
- 그 이유는 WORKDIR 을 설정하게 되면 기본적으로 work 디렉토리에서 시작하게 된다.
- 그래서 cd / 로 루트로 들어가본 후 다시 /usr/src/app 을 따라 가보면 애플리케이션과 관련된 소스들이 들어있다 (워크 디렉토리)

## 어플리케이션 소스 변경으로 다시 빌드하는 것에 대한 문제점
- 어플리케이션을 만들다 보면 소스 코드를 계쏙 변경시켜줘야 하며 그에 따라서 변경된 부분을 확인하면서 개발을 해나가야 한다.
- 그렇다면 도커를 이용해서 어떻게 실시간으로 소스가 반영되게 하는지 알아보자.

그전에 만약 Hello World! 가 아닌 반갑습니다. 라는 문구를 화면에 노출시키는 걸로 수정했다고 가정해보자.
- 먼저 도커 파일을 작성하고 나면 이 도커 파일을 도커 이미지로 만들고(build) 도커 이미지를 가지고 컨테이너를 생성(run) 한다.
- 만약 소스 코드가 변경된다면 build -&gt; run 부분을 다시 해줘야 하는 것이다.

이렇게 해야 하는 이유는 COPY 부분에서 server.js 와 같은 파일을 가지고 있기 때문에 다시 COPY 하기 위해서 이미지를 다시 빌드를 해야하는 것
- 즉 모든 모듈에 있는 종속성들까지 다시 다운을 받아줘야한다.
- 소스 하나 변경시켰다고 이미지를 다시 생성하고 다시 컨테이너를 실행시켜줘야 한다니.. 매우 비효율적이다.

어떻게 해결해야할까?

## 어플리케이션 소스 변경으로 재빌드 시 효율적으로 하는 방법
- 우선 위에 있는 문제를 해결하기 위한 방법을 알아보기 전에 재빌드 자체를 할 때 효율적으로 하는 방법부터 알아보자.
- 결론적으로는 아래와 같이 Dockerfile 을 작성하면 된다.</code></pre><p>FROM node:10</p>
<p>WORKDIR /usr/src/app</p>
<p>COPY package.json ./</p>
<p>RUN npm install express</p>
<p>COPY ./ ./</p>
<p>CMD [&quot;node&quot; , &quot;server.js&quot;]</p>
<pre><code>- 달라진 점은 RUN 위에 COPY 가 하나 더 주가되고 원래의 COPY 는 RUN 아래에 작성한다.


### 그 이유는?
- npm install 할 때 불 필요한 다운로드를 피하기 위해서이다.
- 원래 모듈을 다시 받는 것은 모듈에 변화가 생겨야만 다시 받아야 하는데 소스 코드에 조금의 변화만 생겨도 모듈 전체를 다시 받는 문제점이 있다.
- server.js 의 소스가 변해도 변한 부분만 COPY 를 하는게 아니라 npm install 을 실행하여 변하지도 않은 모듈들을 다시 다운로드 하는게 문제다.
- 새롭게 server.js 에 소스를 바꾸고 나서는 npm install 을 실행하고 그 후 바로 한 번더 build 했을때는 cache 를 사용한다.

결국은 RUN npm install 전 단계에서 COPY 할 때 조금이라도 바뀐 것이 있다면 npm install 이 다시 실행된다.

그러기에 RUN npm install 전 단계에서 COPY 할 때는 오직 모듈에 관한 것만 해준다.

그리고 RUN npm install 이후에 다시 모든 파일들을 COPY 한다.

그래서 먼저 package.json 파일을 COPY 하고 그 이후 RUN npm , install COPY ./ ./ 를 해주면서 모듈에 변화가 생길 때만 다시 다운로드하여주며, 소스 코드에 변화가 생길 때 모듈을 다시 받는 현상을 없앨 수 있다.

정리하면 package.json 부터 COPY 하고 여기에 변경사항이 없다면 모듈을 다시 받는 현상을 없애 효율적으로 재빌드 할 수 있게 됐다.

## Docker Volume 이란?
- 이제는 npm install 전에 package.json 만 따로 변경을 해줘서 쓸 때 없이 모듈을 다시 받지는 않아도 된다.
- 하지만 아직도 소스를 변경할 떄 마다 변경된 소스 부분은 COPY 한 후 이미지를 다시 빌드를 해주고 컨테이너를 다시 실행줘야지 변경된 소스가 화면에 반영이 된다.
- 이러한 작업은 너무나 시간 소요가 크고 이밎도 너무나 많이 빌드된다.

이때 Docker Volume 을 사용하면 된다.

### 지금까지 이용한 방식
- 지금은 로컬에 있는 파일들을 도커 컨테이너로 &quot;복사&quot; 를 했다.
- 하지만 Docker Volume 을 사용하면 도커 컨테이너가 로컬에 있는 파일들을 &quot;참조&quot; 한다.

즉 Docker Volume 은 도커 컨테이너에서 호스트 디렉토리에 있는 파일을 참조할 수 있게 한다.

### Volume 을 사용해서 어플리케이션을 실행하는 방법
- docker run -p 5000:8080 -v /usr/src/app/node_modules -v $(pwd):/usr/src/app IMAGE_ID
- 우선 첫번째 -v 부터 살펴보면, node_module 파일은 컨테이너에서 참조(매핑)을 하지 말라고 지정하는 것이다. (WORKDIR 경로 + /node_modules)
- npm install 로 다운 로드 받은 종속성들은 node_module 파일에 들어가는데, 해당 파일은 없기 떄문에 참조하지 말라고 지정하는 것이다.
- 두 번째 -v 는 일단 pwd 경로에 있는 디렉토리 혹은 파일을 /usr/src/app 경로에서 참조할 수 있도록 하는 것이다.

정리하면 일단 node_modules 는 컨테이너에서 참조하지 말고 /usr/src/app 경로에 있는 파일을 참조하나는 것이다.
- /usr/src/app 는 WORKDIR 로 우리가 만들었던 파일들이 존재하는 곳이기 때문에 server.js 와 같은 파일을 &quot;참조&quot; 할 수 있게 된다.

이렇게 Volume 을 이용해서 빌드할 때 소스를 바꾸더라도 stop 했다가 다시 run 만 바로 반영이 된다.
- COPY 로만 만들었을 때는 다시 빌드를 하고 run 을 했어야 했지만 Volume 은 바로 run 만 해도 된다.
- 컨테이너에서 로컬 파일을 &quot;참조&quot; 하고 있기 때문이다.

## Docker Compose 란?
- docker compose 는 다중 컨테이너 도커 애플리케이션을 정의하고 실행하기 위한 도구
- docker compose 에 대해 이해하기 위해서 페이지를 새로고침했을 때 숫자가 1씩 계속 올라가는 간단한 앱을 만들어보면서 배워보자.

전체적인 구조는 컨테이너 2개를 사용한다.
- Node.js APP + Redis

## 애플리케이션 소스 작성
- 새로운 폴더를 만들고 npm init 으로 기본적인 노드 부분을 완성해준다.
- server.js 를 작성해보기 전에 Redis 를 간단하게 알고 작성해보자.

### Redis 란?
- Rdis(Remote Dictionary Server) 는 메모리 기반의 key-value 구조
- 데이터 관리 시스템이며, 모든 데이터를 메모리에 저장한다.

### Redsi 사용 이유?
- 메모리에 저장을 하기 떄문에 MySQL 같은 데이터베이스에 데이터를 저장하는 것과 데이터를 볼러올 때 훨씬 빠르게 처리할 수 있다.
- 비록 메모리에 저장하지만 영속적으로도 보관이 가능하다.
- 그래서 서버를 재부팅해도 데이터를 유지할 수 있는 장점이 있다.

### Node.js 환경에서 Redis 사용 방법
- 먼저 redis-server 를 작동시킨다.
- redis 모듈을 다운받고, 레디스 클라이언트를 생성한다.
- redis server 가 작동하는 곳과 Node.js APP 이 작동하는 곳이 다르다면 host 인자와 port 인자를 명시해줘야한다.

만약 도커를 사용하지 않는 환경이라면 host: &quot;https://redis-server.com&quot; 와 같은 형식으로 넣어주면 되지만, Docer Compose 를 사용할 때 host 옵션을 docker-compose.yml 파일에 명시한 컨테이너 이름으로 주면 된다.
```js
const express = require(&quot;express&quot;);
const redis = require(&quot;redis&quot;);
//레디스 클라이언트 생성 
const client = redis.createClient({
socket: {
    host: &quot;redis-server&quot;,
    port: 6379
    }
});
const app = express();
app.get(&#39;/&#39;, async (req, res) =&gt; {
await client.connect();
let number = await client.get(&#39;number&#39;);
if (number === null) {
    number = 0;
}
console.log(&#39;Number: &#39; + number);
res.send(&quot;숫자가 1씩 올라갑니다. 숫자: &quot; + number)
await client.set(&quot;number&quot;, parseInt(number) + 1)
await client.disconnect();;;
})
app.listen(8080);
console.log(&#39;Server is running&#39;);</code></pre><h2 id="dockerfile-작성하기">Dockerfile 작성하기</h2>
<pre><code>FROM node

WORKDIR /usr/src/app

COPY ./ ./

RUN npm install express

CMD [&quot;node&quot; , &quot;server.js&quot;]</code></pre><ul>
<li>같은 Node.js 를 위한 이미지를 만들기 위해서 Dockerfile 을 만드는 것이기 때문에 전에 만들었던 Dockerfile 과 유사하게 만들면 된다.</li>
</ul>
<h2 id="docker-containers-간-통신-할-때-나타나는-에러">Docker Containers 간 통신 할 때 나타나는 에러</h2>
<ul>
<li>Dockerfile 을 만들었으니 실제로 어플을 실행해보자.</li>
<li>우선 어플이 어떤식으로 실행이 되는지 살펴보자.</li>
</ul>
<p>현재 컨테이너가 2개가 동작하고 있다.</p>
<ul>
<li>(Node.JS APP , Redis Client) + (Redis Server)</li>
<li>컨테이너를 하나씩 실행해보자.</li>
<li>먼저 터미널을 열고 docker run redis 로 Redis Server 를 기동하고 또 다른 터미널에서는 Node.js 를 위한 컨테이너를 실행하면 된다.</li>
<li>build 를 하고 run 을 하면 된다.</li>
</ul>
<p>이때 에러가 발생하는데 이유가 무엇일까?</p>
<ul>
<li>현재 서로 다른 컨테이너에 2개가 있는데, 컨테이너끼리 통신을 할 때 아무런 설정 없이는 접근을 할 수 없다.</li>
<li>즉 Node.js APP 이 Redis Server 에 접근할 수 없다.</li>
</ul>
<p>그러면 어떻게 컨테이너 사이에 통신을 할 수 있을까?</p>
<p>멀티 컨테이너 상황에서 쉽게 네트워크를 연결시켜주기 위해서는 Docker Compose 를 이용하면 됩니다.</p>
<h2 id="docker-compose-파일-작성하기">Docker Compose 파일 작성하기</h2>
<ul>
<li>Docker Compose 가 컨테이너 사이에 네트워크를 연결시켜준다는 것을 알았다.</li>
<li>그렇다면 본격적으로 Docker Compose 파일을 작성해보자.</li>
</ul>
<h3 id="docker-compose-파일-구조">docker-compose 파일 구조</h3>
<ul>
<li>docker-compose 안에 Redis-Server + Node-APP 2개의 컨테이너를 가지도록 한다.<pre><code class="language-yml">version: &quot;3&quot; # 도커 컴포즈 버전
services: # 이곳에 실행하려는 컨테이너들을 정의
redis-server: # 컨테이너 이름
  image: &quot;redis&quot; # 컨테이너에서 사용하려는 이미지 이름
node-app:  # 컨테이너 이름
  build: . # 현 디렉토리에 있는 Dockerfile 사용
  ports: # 포트 맵핑 -&gt; 로컬 포트 : 컨테이너 포트
    - &quot;5000:8080&quot;
</code></pre>
</li>
</ul>
<p>```</p>
<ul>
<li>docekr-compose up 명령어로 실행</li>
</ul>
<h2 id="docker-compose-로-컨테이너-멈추기">Docker Compose 로 컨테이너 멈추기</h2>
<ul>
<li>다른 터미널에서 docker-compose down 으로 컨테이너를 멈출 수 있다.</li>
</ul>
<p>docker compose up : 이미지가 없을 때 이미지를 빌드하고 컨테이너 실행
docker compose up --build : 이미지가 있든 없든 이미지를 빌드하고 컨테이너 실행</p>
<p>만약 다른 터미널을 키지 않고 하나의 터미널로 해결하고 싶다면?</p>
<ul>
<li>docker compose up -d</li>
<li>docker compose up 을 할 때 -d 옵션을 추가해주면 된다.</li>
<li>-d 는 detached 모드로, 앱을 백그라운드에서 실행시키기 때문에 앱에서 나오는 output 을 표출하지 않는다.</li>
</ul>
<p>그래서 -d 모드로 앱을 실행한다면 하나의 터미널에서 앱을 작동시키고 중단시킬 수 있다.</p>
<p><a href="https://www.inflearn.com/course/%EB%94%B0%EB%9D%BC%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8F%84%EC%BB%A4-ci/dashboard">참고자료</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커와 CI 환경 - 2]]></title>
            <link>https://velog.io/@support-kim/%EB%8F%84%EC%BB%A4%EC%99%80-CI-%ED%99%98%EA%B2%BD-2</link>
            <guid>https://velog.io/@support-kim/%EB%8F%84%EC%BB%A4%EC%99%80-CI-%ED%99%98%EA%B2%BD-2</guid>
            <pubDate>Sun, 10 Mar 2024 12:30:40 GMT</pubDate>
            <description><![CDATA[<h2 id="도커-이미지-내부-파일-구조-보기">도커 이미지 내부 파일 구조 보기</h2>
<ul>
<li>docker run IMAGE_NAME ls</li>
<li>docker : 도커 클라이언트 언급</li>
<li>run : 컨테이너 생성 및 실행</li>
<li>IMAGE_NAME : 컨테이너를 위한 이미지</li>
<li>ls : 현재 디렉토리의 파일 리스트 표출</li>
<li>ls 위치에는 원래 이미지가 가지고 있는 시작 명령어를 무시하고 여기에 있는 커맨드를 실행하게 하는 자리다.</li>
</ul>
<h3 id="docker-run-alpine-ls-을-실행했을-때">docker run alpine ls 을 실행했을 때</h3>
<ol>
<li>Alpine 이미지를 이용해서 컨테이너를 생성</li>
<li>생성할 때 Alpine 이미지 안에 들어있던 파일 스냅샷들이 컨테이너 안에 있는 하드 디스크로 다운로드</li>
<li>이미지 이름 뒤에 다른 명령어를 더 붙여서 원래 이미지 안에 들어있는 기본 커맨드는 무시가 되고 ls 명령어가 실행</li>
</ol>
<p>전에 가져왔었던 hello-world 이미지로 ls 명령어를 사용하면 에러가 발생한다.</p>
<ul>
<li>그 이유는 위에 2번에서 파일 스냅샷이 하드 디스크 부분으로 들어가는데 그 부분에 무엇이 있는지에 따라서 쓸 수 있는 커맨드가 있고 쓰지 못 하는 커맨드도 있을 수 있다.</li>
<li>alpine 의 파일 스냅샷 에서는 ls 을 실행할 수 있지만 hello-world 의 파일 스냅샷에는 ls 를 실행할 수 없는 것이다.</li>
</ul>
<h2 id="컨테이너들-나열하기">컨테이너들 나열하기</h2>
<ul>
<li>docker ps</li>
<li>docker : 도커 클라이언트 언급</li>
<li>ps : Process Status</li>
</ul>
<p>docker ps 명령어 실행</p>
<ul>
<li><p>CONTAINER ID : 컨테이너의 고유한 아이디 해쉬값 , 실제로는 더 길지만 일부분만 표출</p>
</li>
<li><p>IMAGE : 컨테이너 생성 시 사용한 도커 이미지</p>
</li>
<li><p>COMMAND : 컨테이너 시작시 실행될 명령어</p>
</li>
<li><blockquote>
<p>대부분 이미지에 내장되어 있기 때문에 별도 설정이 필요 없다.</p>
</blockquote>
</li>
<li><p>CREATED : 컨테이너가 생성된 시간</p>
</li>
<li><p>STATUS : 컨테이너의 상태 (Up , Exited , Pause)</p>
</li>
<li><p>PORTS : 컨테이너가 개방한 포트와 호스트에 연결한 포트</p>
</li>
<li><blockquote>
<p>특별한 설정을 하지 않은 경우 출력되지 않음</p>
</blockquote>
</li>
<li><p>NAMES : 컨테이너 고유한 이름</p>
</li>
<li><blockquote>
<p>컨테이너 생성시 --name 옵션으로 이름을 설정하지 않으면 도커 엔진이 임의로 형용사와 명사를 조합해 설정</p>
</blockquote>
</li>
<li><blockquote>
<p>rename 명령어로 이름을 변경할 수 있다. (docker rename original-name changed-name)</p>
</blockquote>
</li>
</ul>
<h3 id="모든-컨테이너-나열">모든 컨테이너 나열</h3>
<ul>
<li>docker ps -a</li>
<li>a 는 All 을 의미</li>
</ul>
<h2 id="도커-컨테이너의-생명주기">도커 컨테이너의 생명주기</h2>
<ul>
<li>생성 -&gt; 시작 -&gt; 실행 -&gt; 중지 -&gt; 삭제</li>
</ul>
<p>사실 docker run 명령어는 docker create + docker start 의 과정을 한 번에 해준 명령어이다.</p>
<ul>
<li>docker create : 이미지가 만들어지고 이미지 안에 있는 파일 스냅샷이 하드 디스크 부분으로 가는 명령어이다.</li>
<li>docker start : 이미지 안에 있는 시작 시 실행할 명령어가 컨테이너에 들어가면서 실행되는 명령어이다.</li>
</ul>
<p>docker create </p>
<ul>
<li>생성</li>
<li>create 명령어를 실행하면 컨테이너 ID 를 반환한다.</li>
</ul>
<p>docker start</p>
<ul>
<li>시작 -&gt; 실행</li>
</ul>
<p>docker run</p>
<ul>
<li>생성 -&gt; 시작 -&gt; 실행</li>
</ul>
<h3 id="docker-start--a-containerid">docker start -a CONTAINERID</h3>
<ul>
<li>-a 는 attach 를 의미</li>
<li>만약 -a 없이 실행하면 어떻게 될까?</li>
<li>CONTAINERID 만 반환한다.</li>
<li>-a 가 있으면 도커 컨테이너가 실행이 될 때 그 쪽에 붙어 있다가 그곳에서 나오는 아웃풋들을 화면에 노출시켜주는 것이다.</li>
<li>CONTAINERID 에는 ID 전부다 쓰지 않고 일부분만 적어도 정상적으로 실행이 된다.</li>
</ul>
<h2 id="docker-stop-vs-docker-kill">docker stop vs docker kill</h2>
<ul>
<li>이번에는 위에 생명 주기에서 중지 부분을 알아보자.</li>
<li>docker stop 과 docker kill 로 중지할 수 있다.</li>
<li>즉 docker stop 과 docker kill 은 생명주기에서 실행 -&gt; 중지 부분의 해당한다.</li>
</ul>
<h3 id="stop-과-kill-의-공통점과-차이점">stop 과 kill 의 공통점과 차이점</h3>
<ul>
<li>공통점은 둘 다 실행중인 컨테이너를 중지시킨다는 점이다.</li>
<li>차이점으로는 stop 은 자비롭게 중지 시키지만, kill 경우는 어떠한 것도 기다리지 않고 바로 컨테이너를 중지시킨다.</li>
<li>즉 stop 은 그동안 하던 작업들을 완료하고 컨테이너를 중지시키지만, kill 은 바로 중지시킨다는 것이다.</li>
<li>더 자세히 알아보면 docker stop 은 SIGTERM 이라는 정리하는 시간을 거치고 SIGKILL 명령이 실행되지만 docker kill 은 SIGTERM 없이 바로 SIGKILL 이 실행된다.</li>
</ul>
<p>실제로 stop 으로도 중지해보고 kill 로도 중지 시켜봤는데 stop 은 약간의 시간 뒤에 중지가 되지만 kill 은 바로 중지가 된다.</p>
<h2 id="컨테이너-삭제">컨테이너 삭제</h2>
<ul>
<li>이번에는 생명주기 중에서 삭제 부분을 알아보자.</li>
</ul>
<p>중지된 컨테이너를 삭제하고 싶을 때</p>
<ul>
<li>docker rm ID or NAME</li>
<li>실행 중인 컨테이너는 먼저 중지한 후에 삭제를 할 수 있다.</li>
</ul>
<p>모든 컨테이너를 삭제하고 싶을 때</p>
<ul>
<li>docker rm &#39;docker ps -a -p&#39;</li>
</ul>
<p>이미지를 삭제하고 싶을 때</p>
<ul>
<li>docker rmi IAMGE_ID</li>
</ul>
<p>한 번에 사용하지 않는 컨테이너 , 이미지 , 네트워크 모두 삭제하고 싶을 때</p>
<ul>
<li>docker system prune</li>
<li>도커를 쓰지 않을 때 모두 정리하고 싶을 때 사용하면 좋다.</li>
</ul>
<h2 id="실행-중인-컨테이너에-명령어-전달">실행 중인 컨테이너에 명령어 전달</h2>
<ul>
<li>docker exec CONTAINER_ID</li>
<li>실제로 docker exec hello-world 의 CONTAINER_ID ls 명령어 실행</li>
<li>그 결과 해당 컨테이너의 파일 리스트들이 잘 출력이 된다.</li>
<li>하지만 docker run alpine ls 와 같은 결과가 나오는데 의미가 다르다.</li>
</ul>
<p>docker run 은 새로운 컨테이너를 만들어서 실행
docker exec 은 이미 실행 중인 컨테이너에 명령어를 전달</p>
<ul>
<li>실행 중인 컨테이너 안으로 들어가서 ls 명령어를 실행</li>
</ul>
<h2 id="redis-를-이용한-컨테이너-이해">Redis 를 이용한 컨테이너 이해</h2>
<ul>
<li>Redis 를 도커 환경에서 실행을 해보자.</li>
</ul>
<h3 id="redis-동작-원리">Redis 동작 원리</h3>
<ol>
<li>Redis 서버 실행 (docker run redis)</li>
<li>Redis 클라이언트 실행 (redis-cli)</li>
<li>Redis 클라이언트에서 명령어를 실행하면 Redis 서버로 이동</li>
</ol>
<p>즉 Redis 서버를 먼저 실행 시키고 Redis 클라이언트를 통해 서버에 명령어를 전달해야한다.</p>
<ul>
<li>먼저 docker run redis 로 서버를 작동 시키고 그 후 다른 터미널을 열어서 redis-cli 를 실행해본다.</li>
<li>하지만 에러가 발생한다..</li>
<li>에러가 발생하는 이유는 Redis 클라이언트가 Redis 서버가 있는 컨테이너 밖에서 실행을 하려고 하기 때문에 Redis 서버에 접근을 할 수 없는 것이다.</li>
<li>해결하기 위해서는 Redis 클라이언트도 같은 컨테이너 안에서 실행을 해야한다.</li>
</ul>
<p>위에서 배웠던 exec 명령어를 사용하면 된다.</p>
<ul>
<li>docker exec -it CONTAINER_ID redis-cli</li>
<li>정상적으로 실행이 된다.</li>
<li>-it 명령어는 실행 한 후 계속 명령어를 적을 수 있도록 한다.</li>
<li>i 는 Interactive , t 는 terminal</li>
<li>만약 없다면 redis-cli 만 키고 바로 밖으로 나와진다.</li>
</ul>
<h2 id="실행-중인-컨테이너에서-터미널-사용하기">실행 중인 컨테이너에서 터미널 사용하기</h2>
<ul>
<li>지금까지 실행중인 컨테이너에 명령어를 전달할 떄는 다음과 같이 명령어를 작성해야 한다.</li>
<li>docker exec -it CONTAINER_ID 실행시킬 명령어</li>
<li>명령어 하나를 전달하기 위해서 이 모든 것을 계속 입력을 해줘야 하는데 이러한 문제점을 해결하기 위해 컨테이너 안에 쉘이나 터미널 환경으로 접속을 해준다.</li>
<li>마지막 명령어를 sh 로 하게 되면 쉘 환경으로 접속이 된다.</li>
<li>docker exec -it CONTAINER_ID sh</li>
<li>쉘 환경 안에서 ls , touch 등 편하게 명령어를 작성할 수 있다.</li>
<li>exec 가 아닌 run 을 사용해도 된다.</li>
<li>docker run -it IMAGE_NAME 명령어</li>
</ul>
<h2 id="도커-이미지-생성하는-순서">도커 이미지 생성하는 순서</h2>
<p>현재까지는 도커 이미지를 항상 도커 허브에 이미 있던 것들만 가져왔지만, 직접 도커 이미지를 만들어서 사용할 수도 있고 직접 만든 도커 이미지를 도커 허브에 올려서 공유할 수 있다.</p>
<p>컨테이너는 도커 이미지로 만든다고 했는데 그렇다면 도커 이미지는 어떻게 생성할까?</p>
<ul>
<li>도커 파일 작성 -&gt; 도커 클라이언트 -&gt; 도커 서버 -&gt; 이미지 생성</li>
</ul>
<h3 id="도커-파일-도커-클라이언트-도커-서버">도커 파일? 도커 클라이언트? 도커 서버?</h3>
<ul>
<li>도커 파일이란 도커 이미지를 만들기 위한 설정 파일로 컨테이너가 어떻게 행동해야 하는지에 대한 설정을 정의한다.</li>
<li>도커 파일에 입력된 명령들이 도커 클라이언트에 전달되어야 한다.</li>
<li>도커 서버는 도커 클라이언트에 전달된 모든 중요한 작업들을 하는 곳이다.</li>
</ul>
<h2 id="dockerfile-만들기">Dockerfile 만들기</h2>
<ul>
<li>도커 파일이란 도커 이미지를 만들기 위한 설정 파일이며, 컨테이너가 어떻게 행동해야 하는지에 대한 설정들을 정의하는 것이라고 했다.</li>
</ul>
<h3 id="도커-파일-만드는-순서">도커 파일 만드는 순서</h3>
<ul>
<li>우선 도커 이미지가 필요로 하는 것이 무엇인지를 생각해야 한다.</li>
<li>도커 이미지에는 시작 시 실행할 명령어 , 파일 스냅샷이 존재한다.</li>
</ul>
<ol>
<li>베이스 이미지를 명시 (파일 스냅샷에 해당)</li>
<li>추가적으로 필요한 파일을 다운 받기 위한 몇 가지 명령어를 명시 (파일 스냅샷에 해당)</li>
</ol>
<h3 id="베이스-이미지란">베이스 이미지란?</h3>
<ul>
<li>도커 이미지는 여러 개의 레이어들로 되어 있다.</li>
<li>그 중에서 베이스 이미지는 이 이미지의 기반이 되는 부분이다.</li>
<li>즉 베이스 이미지는 간단하게 macOS , Winodw 와 같은 OS 라고 생각하면 된다.</li>
<li>이미지에 무언가를 추가하고 싶다면 레이어라는 단위를 추가하는 것이고 이것을 레이어 캐싱이라고 부른다.</li>
</ul>
<p>우리가 만들 도커 이미지의 목표는 hello 라는 문구를 출력해보자.</p>
<ul>
<li>도커 파일을 만들고 여기에 Dockerfile 생성후 아래와 같은 형식으로 작성하면 된다.<pre><code># 베이스 이미지를 명시해준다.
FROM baseIamge
</code></pre></li>
</ul>
<h1 id="추가적으로-필요한-파일들을-다운로드-받는다">추가적으로 필요한 파일들을 다운로드 받는다.</h1>
<p>RUN command</p>
<h1 id="컨테이너-시작시-실행-될--명령어를-명시해준다">컨테이너 시작시 실행 될  명령어를 명시해준다.</h1>
<p>CMD [ &quot;executable&quot; ]</p>
<pre><code>FROM
- 이미지 생성 시 기반이 되는 이미지 레이어
- 이미지이름:태그 형식으로 작성
- 태그를 안 붙이면 자동적으로 가장 최신 것으로 다운로드 (latest)

RUN
- 도커 이미지가 생성되기 전에 수행할 쉘 명령어

CMD
- 컨테이너가 시작되었을 때 실행할 실행 파일 또는 셸 스크립트로 해당 명령어는 DockerFile 내 한 번만 쓸 수 있다.
</code></pre><h1 id="베이스-이미지를-명시해준다">베이스 이미지를 명시해준다.</h1>
<p>FROM alpine</p>
<h1 id="추가적으로-필요한-파일들을-다운로드-받는다-1">추가적으로 필요한 파일들을 다운로드 받는다.</h1>
<h1 id="run-command">RUN command</h1>
<h1 id="컨테이너-시작시-실행-될--명령어를-명시해준다-1">컨테이너 시작시 실행 될  명령어를 명시해준다.</h1>
<p>CMD [ &quot;echo&quot; , &quot;hello&quot; ]</p>
<p>```</p>
<ul>
<li>hello 를 출력하는 기능에는 사이즈가 큰 베이스 이미지를 사용할 필요가 없기 때문에 사이즈가 작은 alpine 을 사용</li>
<li>문자 출력을 하려면 echo 가 있어야 하는데 이것은 alpine 안에 있기 떄문에 RUN 부분은 생략해도 상관 없다.</li>
</ul>
<p>이렇게 도커 파일은 만들었고 이 파일을 가지고 이미지를 어떻게 만들어야 하는지를 알아보자.</p>
<h2 id="도커-파일로-도커-이미지-만들기">도커 파일로 도커 이미지 만들기</h2>
<ul>
<li>도커 파일 -&gt; 도커 클라이언트 -&gt; 도커 서버 -&gt; 이미지</li>
</ul>
<p>위에서 만든 도커 파일에 입력된 것들을 도커 클라이언트에 전달되어서 도커 서버가 인식하게 해야한다.</p>
<ul>
<li>그렇게 하기 위해서는 docker build . or docker build ./ 명령어를 실행해야 한다.</li>
</ul>
<h3 id="docker-build">docker build</h3>
<ul>
<li>해당 디렉토리 내에서 dockerfile 이라는 파일을 찾아서 도커 클라이언트에 전달시켜준다.</li>
<li>docker build 뒤에 . 이나 / 은 둘 다 현재 디렉토리를 가리킨다.</li>
</ul>
<h3 id="build-과정">build 과정</h3>
<ul>
<li>위에서 작성한 도커 파일을 기준으로 설명</li>
</ul>
<ol>
<li>alpine 이미지를 가져온다.</li>
<li>임시 컨테이너 생성 후 그 컨테이너에 시작 시 사용할 명령어를 포함시킨다.</li>
</ol>
<ul>
<li>생성한 임시 컨테이너를 지우고 새로운 이미지를 만든다.</li>
</ul>
<p>더 자세히 알아보자.</p>
<ol>
<li>먼저 베이스 이미지를 임시 컨테이너에 넣어준다.</li>
</ol>
<ul>
<li>위에서 작성한 도커 파일에서는 베이스 이미지가 alpine 이기 때문에 alpine 을 임시 컨테이너에 넣는다.</li>
<li>만약 베이스 이미지 이외의 이미지도 있다면 그것도 넣어준다.</li>
<li>그 과정에 임시 컨테이너의 하드 디스크에 alpine 파일 시스템 스냅샷을 추가한다.</li>
</ul>
<ol start="2">
<li>도커 파일에 있었던 CMD 에 있는 명령어도 임시 컨테이너에 넣어준다.</li>
</ol>
<ul>
<li>이 부분이 이미지 안에 시작 시 실행할 명령어에 들어가게 된다.</li>
</ul>
<ol start="3">
<li>그 임시 컨테이너를 토대로 새로운 이미지가 만들어진다.</li>
</ol>
<ul>
<li>만들어진 이후에 임시 컨테이너는 지워진다.</li>
</ul>
<h3 id="정리">정리</h3>
<ul>
<li>베이스 이미지에서 다른 종속성이나 새로운 커맨드를 추가할 때는 임시 컨테이너를 만든 후 그 컨테이너를 토대로 새로운 이미지를 만들고 그 임시 컨테이너는 지워준다.</li>
</ul>
<p>이미지 -&gt; 새로운 명령어 + 새로운 파일 스냅샷으로 임시 컨테이너 생성 -&gt; 임시 컨테이너로 새로운 이미지 생성</p>
<h2 id="내가-만든-이미지-기억하기-쉬운-이름으로-추가">내가 만든 이미지 기억하기 쉬운 이름으로 추가</h2>
<ul>
<li>도커 파일로 만든 이미지를 컨테이너에서 실행</li>
<li>docker run -it IMAGE_ID</li>
<li>하지만 IMAGE_ID 를 모두 입력하기에는 너무 길고 기억하기도 어렵다.</li>
</ul>
<p>IP Address 가 기억하기 힘들어서 Domain 에 이름을 새로 만들어서 사용하는 것 처럼 도커 이미지에도 이름을 줄 수 있다.</p>
<ul>
<li>docker build -t supportkim/hello:latest .</li>
<li>docker build -t 나의 도커 아이디 / 저장소 및 프로젝트 이름 : 버전</li>
<li>위와 같은 형태에 맞춰서 이름을 부여하면 된다.</li>
</ul>
<p>위와 같이 설정한 후 docker run -it supportkim/hello 을 실행하면 정상적으로 hello 가 출력된다!</p>
<p><a href="https://www.inflearn.com/course/%EB%94%B0%EB%9D%BC%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8F%84%EC%BB%A4-ci/dashboard">참고자료</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커와 CI 환경 - 1]]></title>
            <link>https://velog.io/@support-kim/%EB%8F%84%EC%BB%A4%EC%99%80-CI-%ED%99%98%EA%B2%BD-1</link>
            <guid>https://velog.io/@support-kim/%EB%8F%84%EC%BB%A4%EC%99%80-CI-%ED%99%98%EA%B2%BD-1</guid>
            <pubDate>Sat, 09 Mar 2024 13:10:47 GMT</pubDate>
            <description><![CDATA[<p>모든 과정을 거치면 프로그램의 일부분을 수정하고 Git 에 push 만 하면 알아서 배포가 되는 자동화 시스템을 구축할 수 있다!</p>
<h2 id="도커를-쓰는-이유">도커를 쓰는 이유?</h2>
<ul>
<li>어떠한 프로그램을 다운로드하는 과정을 간단하게 만들어주기 떄문이다.</li>
</ul>
<p>예를 들어 도커 없이 Redis 를 다운 받는다고 생각해보자.</p>
<ul>
<li>Redis 홈페이지로 이동해서 wget http://... 명령어를 입력해서 실행한 후     파일 압축 해제까지 해야한다.</li>
<li>이때 wget 이 없으면 오류가 발생하고 또 wget 을 받기 위해서 부수적인 것들을 해야한다.</li>
<li>그 과정이 복잡하고 에러도 많이 생길 수 있다.</li>
</ul>
<p>하지만 도커가 있다면?</p>
<ul>
<li>docker run -it redis 명령어 하나로 Redis 를 가져올 수 있다.</li>
</ul>
<p>예상치 못한 에러도 덜 발생하며, 설치하는 과정도 훨씬 간단해지기 때문에 도커를 사용한다.</p>
<h2 id="도커란-무엇인가">도커란 무엇인가?</h2>
<ul>
<li>컨테이너를 사용하여 응용 프로그램을 더 쉽게 만들고 배포하고 실행할 수 있도록 설계된 도구 이며 컨테이너 기반의 오픈소스 가상화 플랫폼</li>
</ul>
<h3 id="서버에서의-컨테이너란">서버에서의 컨테이너란?</h3>
<ul>
<li>다양한 프로그램 , 실행환경을 컨테이너로 추상화하고 동일한 인터페이스를 제공하여 프로그램의 배포 및 관리를 단순하게 해준다.</li>
<li>일반 컨테이너의 개념에서 물건을 손쉽게 운송해주는 것 처럼 프로그램을 손쉽게 이동 배포 관리를 할 수 있게 해준다.</li>
<li><blockquote>
<p>AWS , Azure , Google Cloud 등 어디에서든 실행 가능하게 해준다.</p>
</blockquote>
</li>
</ul>
<h2 id="도커-이미지와-도커-컨테이너-정의">도커 이미지와 도커 컨테이너 정의</h2>
<ul>
<li>컨테이너는 코드와 모든 종속성을 패키지화하여 응용 프로그램이 한 컴퓨팅 환경에서 다른 컴퓨팅 환경으로 빠르고 안정적으로 실행되도록 하는 소프트웨어의 표준 단위이다.</li>
<li><blockquote>
<p>간단하고 편리하게 프로그램을 실행시켜주는 것</p>
</blockquote>
</li>
<li>컨테이너 이미지는 코드 , 런타임 , 시스템 도구 , 시스템 라이브러리 및 설정과 같은 응용 프로그램을 실행하는 데 필요한 모든 것을 포함하는 가볍고 독립적이며 실행 가능한 소프트웨어 패키지 입니다.</li>
<li>컨테이너 이미지는 런타임에 컨테이너가 되고 도커 컨테이너의 경우 도커 엔진에서 실행될 때 이미지가 컨테이너가 된다.</li>
</ul>
<p>간단하게 설명하면 도커 이미지는 프로그램을 실행하는데 필요한 설정이나 종속성을 갖고 있고 도커 이미지를 이용해 컨테이너를 생성하며 도커 컨테이너를 이용하여 프로그램을 실행한다!</p>
<h2 id="도커를-사용할-때-흐름">도커를 사용할 때 흐름</h2>
<ol>
<li>먼저 도커 CLI 에 커맨드를 입력</li>
<li>1번이 실행되면 도커 서버(도커 Daemon) 이 그 커맨드를 받아서 그것에 따라 이미지를 생성하든 컨테이너를 실행하든 모든 작업을 하게된다.</li>
</ol>
<h3 id="실제-cli-커맨드-작성">실제 CLI 커맨드 작성</h3>
<ul>
<li>터미널에서 docker run hello-world 를 실행해보자.</li>
<li>그렇게 되면 맨 처음에는 hello-world 라는 이미지를 찾을 수 없다고 나오다가 Pull complete 가 나오면서 텍스트가 정상적으로 실행된다.</li>
</ul>
<p>docker run hello-world -&gt; 도커 클라이언트 -&gt; 도커 서버 -&gt; 이미지 캐시 보관 장소</p>
<ul>
<li>이미지 캐시 보관 장소에서 hello-world 라는 이미지가 있는지 확인하고, 만약 있다면 바로 가져오지만 만약 없다면 도커 허브에서 가지고 온다.</li>
<li>도커 허브는 이미지들을 보관하는 곳이라고 생각하면 된다.</li>
</ul>
<h2 id="도커와-기존의-가상화-기술과의-차이를-통한-컨테이너-이해">도커와 기존의 가상화 기술과의 차이를 통한 컨테이너 이해</h2>
<h3 id="가상화-기술-나오기-전">가상화 기술 나오기 전</h3>
<ul>
<li>한대의 서버를 하나의 용도로만 사용</li>
<li>남는 서버 공간은 그대로 방치</li>
<li>하나의 서버에 하나의 운영체제 , 하나의 프로그램만을 운영</li>
<li>안정적이기는 하지만 비효율적</li>
</ul>
<h3 id="하이퍼-바이저-기반의-가상화-출현">하이퍼 바이저 기반의 가상화 출현</h3>
<ul>
<li>논리적으로 공간을 분할하여 VM 이라는 독립적인 가상 환경의 서버 이용 가능</li>
<li>하이퍼 바이저는 호스트 시스템에서 다수의 게스트 OS 를 구동할 수 있게 하는 소프트웨어</li>
<li>하드웨어를 가상화하면서 하드웨어와 각각의 VM 을 모니터링 하는 중간 관리자</li>
<li>하이퍼 바이저에 의해 구동되는 VM 은 각 VM 마다 독립된 가상 하드웨어 자원을 할당</li>
<li>논리적으로 분리되어 있어서 한 VM 에 오류가 발생해도 다른 VM 으로 퍼지지 않다는 장점</li>
</ul>
<h3 id="하이퍼-바이저-vm-기술을-가지고-도커로-발전">하이퍼 바이저 VM 기술을 가지고 도커로 발전</h3>
<p><img src="https://velog.velcdn.com/images/support-kim/post/7a5c973a-8e9d-437f-a57d-a8959646a0ab/image.png" alt="">
<a href="https://geekflare.com/docker-vs-virtual-machine/">https://geekflare.com/docker-vs-virtual-machine/</a></p>
<ul>
<li>도커의 컨테이너는 하이퍼바이저와 게스트 OS 가 필요하지 않으므로 더 가볍다.</li>
<li>도커에서는 애플리케이션을 실행할 때 호스트 OS 위에 애플리케이션의 실행 패키지인 이미지를 배포하기만 하면 된다.</li>
<li>하지만 하이퍼 바이저 VM 은 VM 띄우고 자원을 할당한 게스트 OS 를 부팅하여 애플리케이션을 실행해야 해서 훨씬 복잡하고 무겁게 실행해야 한다.</li>
</ul>
<h3 id="공통점과-차이점">공통점과 차이점</h3>
<ul>
<li>공통점은 도커 컨테이너와 가상 머신은 기본 하드웨어에서 격리된 환경 내에 애플리케이션을 배치하는 방법</li>
<li>차이점은 격리된 환경을 얼마나 격리를 시키는지의 차이</li>
</ul>
<h4 id="도커">도커</h4>
<ul>
<li>도커 컨테이너는 돌아가는 애플리케이션은 컨테이너가 제공하는 격리 기능 내부에 샌드박스가 있지만, 여전히 같은 호스트의 다른 컨테이너와 동일한 커널을 공유한다.</li>
<li>만약 도커와 함께 몽고 DB 컨테이너를 시작하면 호스트의 일반 쉘에 ps-e grep 몽고를 실행하면 프로세스 표시가 된다.</li>
</ul>
<h4 id="가상-머신vm">가상 머신(VM)</h4>
<ul>
<li>VM 내부에서 실행되는 모든 것은 호스트 운영체제 또는 하이퍼바이저와 독립되어 있다.</li>
</ul>
<p>즉 컨테이너를 격리시키는다는 것은 만약 컴퓨터에 실행되고 있는 프로세스가 카카오톡 , 노션이 있을때 카카오톡을 실행하기 위한 파일 시스템은 카카오톡의 컨테이너에서만 존재하고, 노션을 실행하기 위한 파일 시스템은 노션의 컨테이너에서만 존재한다는 것이다.</p>
<h3 id="도커-컨테이너-격리-시키는-방법">도커 컨테이너 격리 시키는 방법</h3>
<ul>
<li>리눅스에서 쓰이는 Cgroup(Control groups) 와 네임스페이스에 대해 알아야한다.</li>
</ul>
<h4 id="c-group">C Group</h4>
<ul>
<li>CPU , 메모리 , HD i/o 등 프로세스 그룹의 시스템 리소스 사용량을 관리</li>
<li>어떤 어플이 사용량이 너무 많다면 그 어플리케이션 같은 것을 C gorup 에 집어넣어서 CPU 와 메모리 사용 제한을 할 수 있다.</li>
</ul>
<h4 id="네임스페이스">네임스페이스</h4>
<ul>
<li>하나의 시스템에서 프로세스를 격리시킬 수 있는 가상화 기술</li>
<li>별개의 독립된 공간을 사용하는 것 처럼 격리된 환경을 제공하는 경량 프로세스 가상화 기술</li>
</ul>
<p>카카오톡을 위한 컨테이너가 있다면 여기에 카카오톡 -&gt; 커널 -&gt; 하드디스크 , 네트워크 , CPU 등 프로세스를 작동시키는데 필요한 것들이 있고 하나의 컨테이너끼리 독립적으로 존재한다.</p>
<h2 id="이미지로-컨테이너-만들기">이미지로 컨테이너 만들기</h2>
<p>위에서 이미지는 응용 프로그램을 실행하는데 필요한 모든 것을 포함하고 있다고 했다.</p>
<h3 id="필요한-모든-것">필요한 모든 것?</h3>
<ol>
<li>컨테이너가 시작될 때 실행되는 명령어 : ex) run kakaotalk</li>
<li>파일 스냅샷 : ex) 컨테이너에서 카카오톡을 실행하고 싶다면 카카오톡 파일 스냅샷</li>
</ol>
<h3 id="이미지-컨테이너-만드는-순서">이미지 컨테이너 만드는 순서</h3>
<ol>
<li>Docker 클라이언트에서 docker run IMAGE 입력</li>
</ol>
<ul>
<li>현재 이미지에는 위에서 말한대로 시작시 실행되는 명령어 + 파일 스냅샷이 존재</li>
</ul>
<ol start="2">
<li>도커 이미지에 있는 파일 스냅샷을 컨테이너 하드 디스크에 옮긴다.</li>
</ol>
<ul>
<li>그중 파일 스냅샷이 먼저 컨테이너의 하드 디스크 부분으로 옮겨진다.</li>
</ul>
<ol start="3">
<li>이미지에서 가지고 있는 명령어를 이용해서 카카오톡을 실행</li>
</ol>
<ul>
<li>이미지에 있던 명령어가 컨테이너로 가게되고 그렇게 되면 커널을 통해 2번에서 옮겨진 하드 디스크 부분에 가서 파일 스냅샷에 있는 실행 파일이 실행된다.</li>
</ul>
<p>정리하면 이미지 안에는 위에 있는 필요한 모든것에 해당하는 1번 , 2번이 모두 존재하고 명령어가 시작이 되면 해당 이미지가 실행하기 위해 필요한 파일들이 하드 디스크 부분에 들어가고 명령어가 실행되면서 커널을 통해 하드 디스크에 있던 실행 파일이 실행된다!</p>
<h2 id="c-group--네임스페이스를-도커-환경에서-쓸-수-있는-이유">C-group , 네임스페이스를 도커 환경에서 쓸 수 있는 이유</h2>
<ul>
<li>한 컴퓨터 안에서 카카오톡을 위한 컨테이너 , 노션을 위한 컨테이너로 나눌 수 있도록 해주는것이 C-group , namespace 이라고 했다.</li>
<li>하지만 C-group , namespace 는 리눅스에서만 가능한데 현재 우리는 윈도우나 macOS 를 사용하고 있다.</li>
<li>카카오톡 , 노션이 있는 컨테이너 부분은 리눅스 VM 에 깔려있기 때문에 리눅스 환경에서 돌아가는 것이다.</li>
<li>(카카오톡 , 노션이 있는 컨테이너) 리눅스 커널 -&gt; 리눅스 VM -&gt; MacOS / Window -&gt; 하드웨어</li>
<li>위와 같은 구조로 이루어져있기 떄문에 C-group , namespace 을 사용할 수 있다.</li>
</ul>
<p><a href="https://www.inflearn.com/course/%EB%94%B0%EB%9D%BC%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8F%84%EC%BB%A4-ci/dashboard">참고자료</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git + GitHub (심화)]]></title>
            <link>https://velog.io/@support-kim/Git-GitHub-%EC%8B%AC%ED%99%94</link>
            <guid>https://velog.io/@support-kim/Git-GitHub-%EC%8B%AC%ED%99%94</guid>
            <pubDate>Fri, 08 Mar 2024 08:22:13 GMT</pubDate>
            <description><![CDATA[<p>커밋 객체에는 무엇이 저장되는가</p>
<p>두 사람이 병렬로 커밋을 만들고 싶으면 어떻게 해야할까</p>
<p>두 사람이 만든 버전을 합치는 방법</p>
<p>남이 만든 오픈소스에는 어떻게 기여하는가</p>
<p>소스 트리 사용?</p>
<h2 id="git-에서-커밋이란-">Git 에서 커밋이란 ?</h2>
<ol>
<li>변경 사항의 모음이 아닌 하나의 최종 코드 모음이다.</li>
<li>다만 기존 커밋과 비교해서 변경된 파일이 아니면 &quot;변경되지 않음&quot; 이라고만 저장해서 용량이 무겁지 않다.</li>
</ol>
<ul>
<li>예전에 사용했던 SVN은 바로 이전 커밋과의 변경사항만 저장한다.</li>
<li>그래서 커밋당 용량이 가볍지만 한 버전을 보려면 맨 처음 커밋부터 계산해야하기 때문에 속도가 느리다.</li>
<li>Git 은 바로 이전 커밋만 보면 되기 때문에 빠르다.</li>
</ul>
<p>원리
0. 맨 처음 로컬 저장소를 만들면 파일 상태는 &quot;추적안됨&quot; 상태이고 로컬 저장소 안에 스테이지에는 아무것도 없을 것이다.</p>
<ol>
<li>파일을 올리게 되면 &quot;스테이지&quot; 상태가 되고 git add 를 하면 해당 파일들이 로컬 저장소 안에 스테이지에 올라가게 된다.</li>
<li>git commit 을 하면 스테이지에 있던 파일들이 하나의 커밋 객체가 된다.</li>
<li>git push 를 하면 커밋 객체가 원격 저장소에 올라가게 된다.</li>
<li>만약 올린 파일들을 수정하게 되면 파일 상태에서 수정된 파일은 &quot;수정 없음&quot; , 수정된 파일은 &quot;수정함&quot; , 추적 안 되는 파일은 &quot;추적안됨&quot; 의 상태를 가진다.</li>
<li>똑같이 git add 를 하면 모든 파일들이 스테이지에 올라가게 된다.</li>
<li>git commit 을 하면 똑같이 커밋 객체가 만들어진다.</li>
</ol>
<ul>
<li>이때 수정된 파일 , 변경 되지 않은 파일 모두 스테이지에 올라가고 커밋 객체에도 올라간다.</li>
</ul>
<p>요약</p>
<ol>
<li>Git 으로 추적하는 파일의 4가지 상태 (추적 안됨 , 수정 없음 , 수정함 , 스테이지됨)</li>
<li>작업 공간에 있는 수정함 , 추적 안됨 파일을 스테이지로 올려 스테이지 상태가 된다.</li>
<li>커밋을 하면 수정 없음 상태로 돌아가서 다시 파일을 수정할 수 있다.</li>
</ol>
<h2 id="branch">Branch</h2>
<p>브랜치는 새로운 기능을 추가할 때 사용한다.</p>
<p>A , B 가 서로 다른 로컬 저장소에서 개발을 하고 있다.</p>
<ul>
<li>A 는 a1 , a2 , a3 이라는 커밋을 만들었다.</li>
<li>B 는 a1 , a2 , b3 이라는 커밋을 만들었다.</li>
<li>현재 둘의 로컬 저장소에서 서로 다른 커밋이 생기는게 어떻게 해야할까?</li>
</ul>
<p>이럴때 Branch 를 사용하면 된다.</p>
<ul>
<li>A 는 a1 , a2 , a3 의 커밋을 만들면 되고 B 는 a1 , a2 , b3 의 커밋을 만들면 된다.</li>
<li>즉 병렬의 상태가 되는 것이다.</li>
</ul>
<p>병렬로 커밋을 쌓는다?</p>
<ul>
<li>모든 사람이 한 줄에서 작업하면 충돌이 날 수 있다.</li>
<li>똑같은 코드를 동시에 고칠 가능성이 있다.</li>
<li>그래서 병렬로 하면 충돌이 나더라도 합치는 시점에 명시적으로 충돌을 해결할 수 있다.</li>
</ul>
<p>그래서 필요한 개념이 Branch</p>
<ul>
<li>a1 , a2 , a3 는 A 브랜치</li>
<li>a1 , a2 , b3 는 B 브랜치</li>
</ul>
<p>git push origin main</p>
<ul>
<li>여기서 main 브랜치에 커밋을 푸시하라는 것이다.</li>
</ul>
<h3 id="git-branch-jiwon">git branch jiwon</h3>
<ul>
<li>jiwon 브랜치를 현재 시점에 만드는 명령어</li>
</ul>
<h3 id="git-checkout-jiwon">git checkout jiwon</h3>
<ul>
<li>jiwon 브랜치로 이동하라는 명령어</li>
</ul>
<p>그래서 git brach jiwon , git checkout jiwon 명령어를 순서대로 입력하면 jiwon 브랜치가 만들어지고 jiwon 브랜치로 이동하게 된다.</p>
<p>시나리오</p>
<ul>
<li>a1 , a2 , a3 , a4 라는 커밋들이 있고 현재 main 브랜치는 a4 를 가리키고 있다.</li>
<li>이때 A 브랜치를 만들고 새로운 a5 라는 커밋을 한다.</li>
<li>다시 main 브랜치로 이동을 하고 B 라는 브랜치를 만든다음에 b5 라는 커밋을 한다.</li>
<li>이렇게 되면 현재 a4 는 main 브랜치이지만 병렬 상태로 a5 , b5 커밋이 만들어진다.</li>
</ul>
<h2 id="버전-합치기-merge">버전 합치기 Merge</h2>
<p>다른 브랜치에서 자겅빙 끝나서 main 에 합치고 싶은 경우</p>
<ul>
<li>main 브랜치의 최신 커밋에(base) A 브랜치의 최신 커밋(compare) 을 합치려고 한다.</li>
</ul>
<p>순서</p>
<ol>
<li>먼저 base 가 될 main 브랜치로 이동</li>
<li>compare 브랜치인 A 를 합치기 : git merge A</li>
<li>그 결과는 A 브랜치가 가리키고 있던 곳에 main 도 가리키게 된다.</li>
</ol>
<h2 id="합치는-과정에서-충돌-conflict">합치는 과정에서 충돌 Conflict</h2>
<p>만약 A , B 모두 한 코드를 서로 다르게 변경을 했을 때 Conflict 가 발생한다.</p>
<ul>
<li>해결하기 위해서는 Merge 를 할 때 수동으로 고쳐줘야 한다.</li>
<li>팀원들과 상의를 한 후 수정을 하면 된다.</li>
<li>모두 수정 했으면 다시 commit -&gt; push 를 해주면 된다.</li>
</ul>
<h2 id="저장소-통째로-복제-fork">저장소 통째로 복제 Fork</h2>
<p>저장소에 푸시 권한이 없지만 기능을 더 개발하고 싶을 때 어떻게 해야할까?</p>
<ul>
<li>Fork 기능을 사용하면 된다.</li>
<li>해당 저장소를 통째로 자신의 계정에 복제해온다.</li>
<li>그 저장소에 자유롭게 커밋 , 푸시를 한다.</li>
<li>내 저장소의 브랜치와 기존에 있던 저장소의 브랜치를 머지해달라고 요청하면 된다.</li>
</ul>
<h2 id="코드-머지-요청-pull-request">코드 머지 요청 Pull Request</h2>
<h3 id="a-커밋이랑-b-커밋을-합치고-싶을-때">A 커밋이랑 B 커밋을 합치고 싶을 때</h3>
<ul>
<li>Merge 하고 싶은 2개의 브랜치를 선택</li>
<li>어떤 변경을 했는지 제목과 내용 작성</li>
<li>단일 저장소에서 보낼 수 있고, 포크한 저장소에서도 보낼 수 있다.</li>
</ul>
<ol>
<li>mater 브랜치에 커밋을 잘못 푸시해서 이전 상태로 돌려야할 때?</li>
<li>A 브랜치에서 빨리 고쳐야 하는 버그가 있어서 B 브랜치를 따서 개발하고 나중에 A 에 체리픽을 해야할 때?</li>
</ol>
<h2 id="깜빡하고-수정-못한-파일을-방금-만든-커밋에-추가하고-싶을-때-amend">깜빡하고 수정 못한 파일을 방금 만든 커밋에 추가하고 싶을 때 Amend</h2>
<ul>
<li>소스 트리에서 커밋 메시지를 작성하는 곳 오른쪽 위에 커밋 옵션에서 마지막 커밋 수정을 누르고 커밋을 하면 된다.</li>
<li>git 명령어 : git commit --amend </li>
<li>반드시 혼자 사용하는 branch 에서만 사용해야 한다.</li>
<li>왜냐하면 우리가 미리 올렸던 커밋을 가지고 개발을 하던 사람이 있었다면 Amend 를 했을때 꼬여버리는 상황이 발생하기 때문에 나 혼자 사용하는 branch 에서만 사용해야한다.</li>
</ul>
<h2 id="변경사항을-잠시-킵-해두고-커밋을-안-만들때-stash">변경사항을 잠시 킵 해두고 커밋을 안 만들때 Stash</h2>
<ul>
<li>만약 A 브랜치에서 개발을 하다가 급하게 Main 브랜치에서 변경할 때 있다고 가정해보자.</li>
<li>그렇다고 A 브랜치에서 개발한 것을 커밋하기에는 애매한 상황이다.</li>
<li>이럴때 Stash 를 사용하면 된다.</li>
<li>소스 트리에서는 위에 스태시 라는 버튼을 누르고 메시지를 입력하면 된다.</li>
<li>이렇게 하고 Main 브랜치의 작업이 끝난 후 Stash 를 pop 해서 다시 개발을 하면 된다.</li>
<li>pop 하는 방법은 소스트리 왼쪽 메뉴에 치워두기를 열어두면 우리가 Stash 한 것이 있어서 그것을 꺼내서 사용하면 된다.</li>
</ul>
<p>git 명령어 : git stash
stash 목록 가져오기 : git stash list
stash 가져오기 : git stash apply
stash 제거 : git stash drop</p>
<ul>
<li>apply 는 단순히 stash 를 가져오기만 하기 때문에 스택에 남아있는 stash 를 제거해야한다.</li>
</ul>
<h2 id="옛날-커밋으로-브랜치를-되돌리고-싶을때-reset">옛날 커밋으로 브랜치를 되돌리고 싶을때 Reset</h2>
<ul>
<li>개발했던 기능의 요구사항이 변경되어 그때 브랜치로 돌아가서 다시 개발을 해야할 수 있다.</li>
<li>소스 트리에서는 돌아가고 싶은 커밋에서 마우스 오른쪽 클릭을 하면 &quot;main 을 이 커밋으로 초기화&quot; 메뉴를 누르고 사용중인 모드를 선택하고 확인을 누르면 된다.</li>
<li>A 브랜치에서 옛날 커밋으로 돌아간 후 push 를 할 때 경고가 발생하는데, 강제 푸시를 해야한다는 경고이다.</li>
<li>강제 푸시(force push)는 우리가 이미 원격 저장소에 올렸던 커밋들을 수정할 때 사용한다.</li>
<li>이것도 혼자 쓰는 브랜치에서만 사용해야한다.</li>
<li>소스트리에서는 설정 -&gt; 고급 -&gt; 강제 푸시를 허용 체크</li>
</ul>
<p>git 명령어 : git reset --옵션 커밋ID</p>
<ul>
<li>옵션으로는 soft , mixed , hard 가 존재</li>
</ul>
<h2 id="커밋의-변경사항을-되돌리고-싶을때-revert">커밋의 변경사항을 되돌리고 싶을때 Revert</h2>
<ul>
<li>Reset 과 비슷하게 커밋을 되돌리고 싶을 때 사용한다.</li>
<li>Reset 은 History 를 완전히 초기화를 시켜버리지만, Revert 는 History 를 새로 쌓으면서 변경을 하는 것이다.</li>
<li>만약 main 에 잘못된 커밋을 올렸는데, Reset 을 하고 force push 를 하면 다른 사람들 히스토리에 영향을 준다.</li>
<li>그래서 revert 하는 커밋을 사용하면 된다.</li>
<li>소스트리에서는 돌아가고 싶은 커밋에 오른쪽 클린 후 커밋 되돌리기를 누르면 된다.</li>
</ul>
<p>내 특정한 커밋의 변경사항을 되돌려줘서 새로운 커밋을 만든다고 생각하면 된다.</p>
<ul>
<li>이력을 남기면서 커밋을 되돌릴때</li>
</ul>
<p>git 명령어 : git revert 커밋ID</p>
<ul>
<li><p>그 이후 push 하면 적용</p>
</li>
<li><p>되돌려야 할 commit이 local 에만 존재할 경우 - reset</p>
</li>
<li><p>되돌려야 할 commit이 push 된 경우 - revert</p>
</li>
</ul>
<h2 id="커밋-하나만-떼서-지금-브랜치에-붙이고-싶을때-cherry-pick">커밋 하나만 떼서 지금 브랜치에 붙이고 싶을때 cherry-pick</h2>
<ul>
<li>한 예시로 main 에는 무조건 완벽한 코드들이 들어가고 latest 브랜치는 릴리즈하는 코드들이 들어간다고 가정해보자.</li>
<li>이때 릴리즈한 latest 브랜치에 버그가 있어서 fix/test-bug 브랜치에서 버그를 고친 후 master 에 머지를 하려고 했다.</li>
<li>이때 master 에 다른 수정사항도 많아서 latest 브랜치랑 당장 머지할 수 없는 상황인데, 릴리즈된 lastest 브랜치에 버그 수정 커밋은 들어가야 한다.</li>
<li>이럴때는 버그 수정을 위해 고친 코드가 있는 커밋을 latest 에 붙혀주면 되고 이것을 cherry-pick 이라고 한다.</li>
</ul>
<p>시나리오</p>
<ol>
<li>fix/bug 브랜치에서 A 커밋은 버그 수정 , B 커밋은 기능 추가 1</li>
<li>main 브랜치에서 C 커밋은 기능 추가 2</li>
</ol>
<ul>
<li>각 브랜치에서 커밋들을 만들었는데 현재 하고자 하는게 main 브랜치에서 fix/bug 브랜치중 A 커밋 즉 버그 수정을 위한 커밋만 가지고 오고 싶은 것이다.</li>
<li>이때 가지고 오고 싶은 커밋 즉 A 커밋에 오른쪽 버튼을 누르고 Commit immediately after successful merge(만약 충돌이 없다면 바로 커밋 시키겠다는 것) 를 체크하면 된다.</li>
</ul>
<p>git 명령어 :  git cherry-pick 커밋ID</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Cloud + MSA 애플리케이션 개발 16(애플리케이션 배포 2 - Docker Container)]]></title>
            <link>https://velog.io/@support-kim/Spring-Cloud-MSA-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-16%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC-2-Docker-Container</link>
            <guid>https://velog.io/@support-kim/Spring-Cloud-MSA-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-16%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC-2-Docker-Container</guid>
            <pubDate>Tue, 05 Mar 2024 06:31:57 GMT</pubDate>
            <description><![CDATA[<h2 id="zipkin">Zipkin</h2>
<ul>
<li>zipkin.io 에 들어가보면 quickstart 에서 Docker 부분을 보면 컨테이너로 바로 실행할 수 있는 명령이가 있다.</li>
<li>docker run -d -p 9411:9411 openzipkin/zipkin</li>
<li>여기에 --network , -name 만 넣어주면 된다.</li>
<li>docker run -d -p 9411:9411 --network ecommerce-network --name zipkin openzipkin/zipkin</li>
</ul>
<h2 id="deployed-services">Deployed Services</h2>
<p>앞으로 user-service , order-serivce , catalog-service 를 배포할 것인데 해당 service 들은 Ip Address 는 크게 상관하지 않아도 된다.</p>
<ul>
<li>어차피 apigateway-service 를 통해서 가기 떄문에 IP 주소는 상관없다.</li>
</ul>
<h2 id="user-microservice">User Microservice</h2>
<p>Dockerfile 만들고 Image 만들고 Container 만들기</p>
<p>현재 git 에 올린 user-service.application.yml 에서는 gateway:ip 부분에 실제 apigateway-service 의 ip 주소를 넣어줘야 한다.</p>
<ul>
<li>주소를 알기 위해서 docker inspect apigateway-service 명령어를 실행하면 된다.</li>
<li>그래서 저 정보는 우리가 config-service 에서 가져오는데, 해당 정보는 git 에다가 올리고 사용하기 때문에 git 에 들어가서 user-service.yml 파일을 수정하면 된다.</li>
</ul>
<h3 id="dockerfile">Dockerfile</h3>
<pre><code>FROM openjdk:21-ea-11-slim
VOLUME /tmp
COPY target/user-service-1.0.jar UserService.jar
ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;UserService.jar&quot;]</code></pre><p>mvn comile -&gt; docker build -&gt; docker run</p>
<ol>
<li>mvn clean compile package -DskipTests=true</li>
<li>docker build -t supportkim/user-service:1.0 .</li>
</ol>
<p>-&gt; 필요하다면  docker push supportkim/user-service:1.0
3.  docker run -d --network ecommerce-network --name user-service 
-e &quot;spring.cloud.config.uri=<a href="http://config-service:8888&quot;">http://config-service:8888&quot;</a> 
-e &quot;spring.rabbitmq.host=rabbitmq&quot; 
-e &quot;spring.zipkin.base-url=<a href="http://zipkin:9411&quot;">http://zipkin:9411&quot;</a> 
-e &quot;eureka.client.service-url.defaultZone=<a href="http://discovery-service:8761/eureka/&quot;">http://discovery-service:8761/eureka/&quot;</a> 
-e &quot;logging.file=/api-logs/users-ws.log&quot; 
supportkim/user-service:1.0</p>
<ul>
<li>config-service 와 연동하기 위해 연결</li>
<li>rabbitmq 정보 넘기기</li>
<li>zipkin 정보 넘기기</li>
<li>유레카 서버에 등록하기 위해서 eureka url 넘기기</li>
<li>로그 저장을 위한 파일</li>
<li>supportkim/user-service:1.0 이라는 이미지로 컨테이너 생성</li>
<li>접근할 때 <a href="http://CONATAINER_NAME:PORT%EB%B2%88%ED%98%B8">http://CONATAINER_NAME:PORT번호</a> 으로 접근하면 된다.</li>
</ul>
<p>유레카 서버에 들어가보면 user-service 가 정상적으로 등록된 것을 확인할 수 있다.</p>
<h2 id="order-microservice">Order Microservice</h2>
<p>Dockerfile 만들고 Image 만들고 Container 만들기</p>
<pre><code class="language-java">@EnableKafka
@Configuration
public class KafkaProducerConfig {

    @Bean
    public ProducerFactory&lt;String,String&gt; producerFactory() {
    // 서버 주소 변경
        Map&lt;String,Object&gt; properties = new HashMap&lt;&gt;();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;172.18.0.101:9092&quot;); 
    }
}</code></pre>
<ul>
<li>local 주소를 현재 컨테이너에 올린 kafka IP 주소로 바꿔줘야 한다.</li>
<li>추가로 application.yml 파일에서 datasource 를 h2 에서 mariadb 로 바꿔준다.</li>
</ul>
<h3 id="dockerfile-1">DockerFile</h3>
<pre><code>FROM openjdk:21-ea-11-slim
VOLUME /tmp
COPY target/order-service-1.0.jar OrderService.jar
ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;OrderService.jar&quot;]</code></pre><p>mvn compile -&gt; docker build -&gt; docker run</p>
<p>docker run -d --network ecommerce-network
--name order-service
-e &quot;spring.cloud.config.uri=<a href="http://http://config-service:8888&quot;">http://http://config-service:8888&quot;</a>
-e &quot;spring.rabbitmq.host=rabbitmq&quot;
-e &quot;spring.zipkin.base-url=<a href="http://zipkin:9411&quot;">http://zipkin:9411&quot;</a>
-e &quot;eureka.client.service-url.defaultZone=<a href="http://discovery-service:8761/eureka/">http://discovery-service:8761/eureka/</a>
-e &quot;logging.file=/api-logs/orders-ws.log&quot;
-e &quot;spring.datasource.url=jdbc:mariadb://mariadb:3306/mydb&quot;
supportkim/order-service:1.0</p>
<h2 id="catalog-microservice">Catalog Microservice</h2>
<p>Dockerfile 만들고 Image 만들고 Container 만들기</p>
<pre><code class="language-java">@EnableKafka
@Configuration
public class KafkaConsumerConfig {
    @Bean
    public ConsumerFactory&lt;String,String&gt; consumerFactory() {
        Map&lt;String,Object&gt; properties = new HashMap&lt;&gt;();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;172.18.0.101:9092&quot;);
}</code></pre>
<ul>
<li>order-service 와 비슷한 맥락으로 Consumer 부분을 수정</li>
</ul>
<p>mvn clean compile -&gt; docker build -&gt; docker run</p>
<ul>
<li>docker build -t supportkim/catalog-service:1.0 .</li>
<li>docker push supportkim/catalogs-service:1.0</li>
</ul>
<p>docker run -d --network ecommerce-network
--name catalogs-service
-e &quot;spring.cloud.config.uri=<a href="http://http://config-service:8888&quot;">http://http://config-service:8888&quot;</a>
-e &quot;spring.rabbitmq.host=rabbitmq&quot;
-e &quot;eureka.client.service-url.defaultZone=<a href="http://discovery-service:8761/eureka/">http://discovery-service:8761/eureka/</a>
-e &quot;logging.file=/api-logs/orders-ws.log&quot;
supportkim/catalog-service:1.0</p>
<h2 id="test">Test</h2>
<p>그동안 테스트 했던 것 처럼 POSTMAN 으로 회원가입 , 로그인 , /health , 조회 , 주문등등 다양한 요청이 정상적으로 호출된다.</p>
<p>local 에서 직접 서버를 하나하나씩 띄우지 않고 같은 네트워크안에 컨테이너를 올려서 사용한 것이다!</p>
<h2 id="multi-profiles">Multi Profiles</h2>
<ul>
<li>mvn spring-boot:run -Dspring-boot.run.arguments=--spring.profiles.active=production</li>
<li>java -Dspring.profiles.active=default XXXX.jar</li>
</ul>
<p>개발 환경은 Local IP 로 , 상용 환경은 AWS EC2 로 개발하려고 한다고 가정해보자.</p>
<ul>
<li>전에 했던것 처럼 상용 환경에서 A-dev.yml , A-prod.yml 이런식으로 설정한 다음에 상황에 맞게 원하는 yml 파일을 적용시킬 수 있다.</li>
</ul>
<h3 id="profile">@Profile</h3>
<pre><code class="language-java">@Component
@Profile(&quot;dev&quot;)
public class RedisConnector {
}

@Component
@Profile(&quot;prod&quot;)
public class MysqlConnector {
}</code></pre>
<ul>
<li>위와 같이 @Profile 애노테이션을 사용해도 된다.</li>
</ul>
<p><a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4/dashboard">참고자료</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Cloud + MSA 애플리케이션 개발 15(애플리케이션 배포 1 - Docker Container)]]></title>
            <link>https://velog.io/@support-kim/Spring-Cloud-MSA-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-15%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC-Docker-Container</link>
            <guid>https://velog.io/@support-kim/Spring-Cloud-MSA-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-15%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC-Docker-Container</guid>
            <pubDate>Tue, 05 Mar 2024 04:14:31 GMT</pubDate>
            <description><![CDATA[<p>그동안 만들었던 Microserivce 나 Microserivce 를 지원하는 시스템들 중에서 어떤 것들이 배포 대상이 되는지, 배포 방법을 알아보고 직접 배포까지 해보자.</p>
<h2 id="애플리케이션-배포-구성">애플리케이션 배포 구성</h2>
<p>배포 방법</p>
<ol>
<li>Docker + Local</li>
<li>JAR file + Local</li>
<li>IntelliJ IDEA + Local</li>
<li>Docker + AWS EC2</li>
<li>Docker + Docker Swarm Mode + AWS EC2 (요즘은 많이 사용하지 않음)</li>
<li>Docker + Kubernetes + AWS EC2 (거의 표준으로 사용)</li>
</ol>
<p>원래 기본적으로 Local 에서 하는 방법은 3번의 방식이고 이번에 해볼 방식은 1번의 방식이다.</p>
<ul>
<li>그 외 여러가지 방법으로 배포할 수 있다.</li>
</ul>
<p>Create Bridge Network</p>
<ul>
<li>Bridge network</li>
<li><blockquote>
<p>아무런 설정 없이 사용할 수 있는 것으로 호스트 PC 와는 별도의 가상의 네트워크를 만들고 그 가상의 네트워크에서 우리가 만든 컨테이너를 배치하고 사용하는 방식</p>
</blockquote>
</li>
<li>Host network</li>
<li><blockquote>
<p>우리가 사용하는 호스트 네트워크와 게스트 네트워크와 같이 사용</p>
</blockquote>
</li>
<li>None network</li>
<li><blockquote>
<p>네트워크를 사용하지 않는다.</p>
</blockquote>
</li>
<li><blockquote>
<p>io 네트워크만 사용 , 외부와 단절</p>
</blockquote>
<pre><code>docker entwork create ecommerce-network
docker network ls</code></pre></li>
<li>해당 명령어를 입력하면 bridge network 로 만들어진 것을 확인할 수 있다.</li>
<li>위에서 설명한 bridge , host , none 3개는 기본적으로 만들어져있다.</li>
</ul>
<p>docker system prune 는 중지된 컨테이너 , 사용되지 않는 네트워크 , 불필요한 리소스를 삭제하는 명령어</p>
<pre><code>docker network create --gateway 172.18.0.1 --subnet 172.18.0.0/16 ecommerce-network</code></pre><ul>
<li>gateway 와 subnet 을 지정할 수 있다. (가급적이면 지정하는 것이 좋다)</li>
</ul>
<p>docker network inspect ecommerce-network 는 해당 네트워크 상세 정보를 확인할 수 있는 명령어이다.</p>
<h3 id="컨테이너를-사용하기-위한-네트워크를-만들어서-사용하면-좋은점">컨테이너를 사용하기 위한 네트워크를 만들어서 사용하면 좋은점</h3>
<ul>
<li>일반적인 컨테이너는 하나의 게스트 OS 로 고유한 IP 주소를 가진다.</li>
<li>또한 도커에서는 컨테이너를 배포할 때 IP 주소를 순ㄴ차적으로 할당을 한다. (비어있는 IP 주소부터)</li>
<li>유레카 서버가 172.17.0.2 라고 하고, user-service 가 172.17.0.3 이라고 할 때 만약 유레카 서버의 IP 주소가 달라지게 된다면 유레카 서버와 통신하는 user-service 는 연결을 하지 못한다.</li>
<li>하지만 같은 네트워크를 사용하면 IP 주소가 아닌 Container id or Container name 으로 호출할 수 있다.</li>
</ul>
<p>즉 Container 를 만들 때 --name 옵션을 사용해서 name 을 할당한다면 IP 주소가 달라진다고 하더라도 우리가 필요했던 특정한 서버에 접속하기 위해 name 만으로도 가능하다.</p>
<ul>
<li>즉 IP 주소가 달라지더라도 계속 똑같은 name 으로 접속하면 된다.</li>
<li>이러한 이유로 컨테이너를 사용하기 위한 별도의 네트워크를 만들어야 하는 이유이다.</li>
</ul>
<p>가상의 네트워크를 만들게 되면 Docker Server 가 만들어지고 여기 안에 Container 를 넣고 컨테이너끼리 통신할 때는 name 을 통해서도 할 수 있다.</p>
<h2 id="rabbitmq">RabbitMQ</h2>
<h3 id="run-rabbitmq">Run RabbitMQ</h3>
<pre><code>docker run -d --name rabbitmq --network ecommerce-network -p 15672:15672 -p 5672:5672 -p 15671:15671 -p 5671:5671 -p 4369:436
9 -e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest rabbitmq:management</code></pre><ul>
<li>위에 명령어를 실행하면 RabbitmQ 가 실행된다.</li>
<li>-d 는 백그라운드 옵션</li>
<li>--name 은 컨테이너 구별 및 네트워크에서도 구별하기 위해서 사용</li>
<li>--network ecommerce-network 위에서 만들었던 네트워크를 설정</li>
<li>만약 설정하지 않으면 도커가 가지고 있었던 bridge 네트워크를 사용한다.</li>
<li>이래도 상관 없긴 하지만 현재 ecommerce-network 을 만들었기 때문에 해당하는 네트워크가 아닌 다른 네트워크에 섞여서 저장될 수 있다.</li>
<li>그렇게 되면 통신할 수 있는 방법이 없어지기 떄문에 우리가 생성했던 네트워크를 지정해서 컨테이너를 생성하는게 좋다.</li>
</ul>
<p>-p 는 컨테이너의 포트들 
사용하고 있는 컨테이너 포트와 실제 host pc 에서 사용하는 포트를 포워딩</p>
<p>-e 환경 변수로써 user , pw 를 넘겨준다.</p>
<p>rabbitmq:management 가 호출하려는 이미지 이름이라고 생각하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/support-kim/post/3e517442-e5d6-4c3a-8b1c-57569016f1f4/image.png" alt=""></p>
<ul>
<li>docker network inspect ecommerce-network 명령어로 네트워크 상세 정보를 보면 rabbitmq 라는 컨테이너가 들어있는 것을 확인할 수 있다.</li>
<li>docker ps -a 로 컨테이너를 보면 현재 기동중이며 PORTS 도 알 수 있다.</li>
</ul>
<p>따로 서버를 기동하지 않았는데도 15672 port 로 접속하면 들어가지는 이유</p>
<ul>
<li>run 명령어에 Host 와 컨테이너를 포워딩을 했기 때문에 15672 port 로 접속하면 이와 포워딩된 rabbitmq 컨테이너로 들어가서 접속이 되는 것이다.</li>
</ul>
<h2 id="configuration-service">Configuration Service</h2>
<p>Config Server 를 도커 이미지화 시키기 위해서는 이미지 파일을 만들어야 한다.</p>
<h3 id="해야할-것">해야할 것</h3>
<ul>
<li>Dockerfile 만들고 명령어 넣기</li>
<li><blockquote>
<ul>
<li>config-server 에서는 암호화 하는 것도 했었는데 이때 필요한 .jks 파일까지 프로젝트 안에 넣어줘야 암호를 풀 수 있다.</li>
</ul>
</blockquote>
</li>
<li><blockquote>
<p>그래서 COPY 명령어로 .jks 파일도 복사한다.</p>
</blockquote>
</li>
<li><blockquote>
<p>bootstrap.yml 에 있는 .jks 파일 경로를 적는 부분도file:/apiEncryptionKey.jks 로 바꿔주면 된다.</p>
</blockquote>
</li>
<li>pom.xml 에서 version 1.0 으로 바꾸기</li>
<li>그런후 mvn clean compile 로 jar 파일을 생성한다.</li>
<li>build -&gt; push -&gt; run</li>
</ul>
<h3 id="dockerfile">DockerFile</h3>
<pre><code>FROM openjdk:21-ea-11-slim
VOLUME /tmp
COPY apiEncryptionKey.jks apiEncryptionKey.jks
COPY target/config-service-1.0.jar ConfigServer.jar
ENTRYPOINT [&quot;java&quot; , &quot;-jar&quot; , &quot;ConfigServer.jar&quot;]</code></pre><ul>
<li>FROM jdk21 버전으로 부터 도커 이미지를 만듦</li>
<li>COPY jks 를 루트 디렉토리에 복사</li>
<li>docker build --t supportkim/config-service:1.0 .</li>
<li><blockquote>
<p>마지막 . 은 현재 디렉토리에 도커 파일을 실행하라는 것</p>
</blockquote>
</li>
</ul>
<pre><code class="language-yml">application.yml
spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
# bootstrap.yml
encrypt:
  key-store:
#    location: file://${user.home}/Desktop/keystore/apiEncryptionKey.jks
    location: file:/apiEncryptionKey.jks</code></pre>
<ul>
<li>config-server.application.yml 파일에 Rabbitmq 에 대한 정보가 들어있다.</li>
<li>근데 host 정보가 local 되어 있는데 이렇게 되면 오류가 발생한다.</li>
<li>현재 같은 네트워크 안에 있지만 서로 다른 IP 주소를 가지기 때문에 host 에 rabbitmq 가 가지는 IP 를 넣어줘야 한다.</li>
<li>확인하는 방법은 docker network inspect ecommerce-network 명령어를 실행하면 containers 에 rabbbitmq 가 있고 거기에 IP Address 로 확인하면 된다.</li>
<li>하지만 이런 방법은 rabbitmq 의 IP 주소가 변경되는 경우가 있을 수 있기 때문에 좋지 않은 방법이다.</li>
<li>마지막에 location 을 local 위치가 아닌 프로젝트 안으로 변경해주면 된다.</li>
</ul>
<pre><code>docker run -d -p 8888:8888 --network ecommerce-network -e &quot;spring.rabbitmq.host=rabbitmq&quot; 
-e &quot;spring.profiles.active=default&quot; --name config-service supportkim/config-service:1.0</code></pre><ul>
<li>그래서 -e 옵션을 사용해서 host 에 name 으로 접근할 수 있도록 넣어준다.</li>
<li>-e 옵션에 profile 정보도 넘길 수 있다.</li>
<li>이름을 config-service 로 지정</li>
<li>정상적으로 기동되는 것을 확인할 수 있다.</li>
<li>docker network inspect ecommerce-network 를 확인하면 config-service 도 연결된 것을 확인</li>
</ul>
<pre><code>spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/supportlaver/spring-cloud-config.git</code></pre><ul>
<li>또 배포를 하기 때문에 config 파일들을 git-repo-local 와 같은 로컬 저장소가 아닌 git 에 올려서 사용해야 한다.</li>
<li>그래서 uri: 에 파일의 경로가 아닌 git 주소를 넣어주면 된다.</li>
</ul>
<p>mvn build(mvn compile) -&gt; docker build(다시 쟁성 , 덮어쓰기) -&gt; docker images 
docker images 의 결과를 보면 지금 바로 만든 것도 있지만 none , none 으로 되어 있는 것도 존재한다.</p>
<ul>
<li>같은 이미지로 중복해서 만들게 되면 가장 마지막에 만들어진게 사용되는것 이니까 이전에 만든것은 사용되지 않는 것으로 간주하고 none 으로 되는 것이다.</li>
<li>그래서 없애주기 위해서 이미 실행된 컨테이너를 먼저 중지 시킨다. (docker stop) </li>
<li>docker system prune 명령어로 불필요한 리소스들 , 사용되지 않는 컨테이너 삭제</li>
<li>다시 docker images 를 보면 none 이 없어진 것을 확인</li>
</ul>
<p>다시 위에 있는 docker run 명령어를 실행</p>
<ul>
<li>실행 후 network 에 잘 들어있는지 확인 (docker network inspect)</li>
</ul>
<p>현재 2개의 Container 가 생성되고 실행중이며, 계속 만들었던 서비스를 Container 로 만들어서 실행해보자.</p>
<h2 id="discovery-service">Discovery Service</h2>
<p>위에서 한 것처럼 똑같이 DockerFile 을 만들고 이미지화 시켜서 컨테이너를 만들 수 있도록 해보자.</p>
<pre><code class="language-yml">spring:
  application:
    name: discoveryservice
  cloud:
    config:
      uri: http://127.0.0.1:8888
      name: ecommerce</code></pre>
<ul>
<li>application.yml 에서 위와 같이 수정하면 config 를 할 때 config-service 에서 ecommerce.yml 파일을 참조할 수 있도록 한다.</li>
<li>8888 port 가 config-serivce</li>
<li>하지만 위에서 설명했듯이 127.0.0.1 은 local 환경이기 때문에 이렇게 설정하면 안 되고 config-service:8888 와 같이 name 으로 접근해야 한다.</li>
<li>해당 파라미터는 docker run 할 떄 -e 옵션과 같이 넘겨주면 된다.</li>
</ul>
<h3 id="순서">순서</h3>
<ol start="0">
<li>pom.xml 에서 version 만 변경</li>
<li>DokerFile 생성</li>
<li>build</li>
<li>run</li>
<li>docker logs NAME </li>
</ol>
<ul>
<li>로그 확인</li>
</ul>
<h3 id="container-만들기">Container 만들기</h3>
<ol>
<li>mvn clean compile package</li>
<li>docker build --tag supportkim/discovery-service:1.0 .</li>
</ol>
<p>-&gt; DockerHub 에 올리고 싶다면 docker push NAME:TAG 로 올리면 된다.
-&gt; 올린 후 DockerHub 사이트에 들어가서 로그인을 하면 정상적으로 올라가있다.
-&gt; 나중에 다른쪽에 있는 배포 툴이나 환경에서도 지금 만든 이미지들을 바로 사용할 수 있다.
3. docker run</p>
<ul>
<li>docker run -d -p 8761:8761 --network ecommerce-network -e &quot;spring.cloud.config=<a href="http://config-service:8888&quot;">http://config-service:8888&quot;</a> --name discovery-service supportkim/discovery-service</li>
</ul>
<ol start="4">
<li>docker ps -a</li>
</ol>
<ul>
<li>확인하면 총 3개 (rabbitmq , config-service , discovery-service) 의 컨테이너가 기동</li>
</ul>
<ol start="5">
<li>docker network inspect ecommerce-network</li>
</ol>
<ul>
<li>여기서도 3개의 컨테이너가 해당 네트워크에 있는 것을 확인</li>
</ul>
<h2 id="apigateway-service">Apigateway Service</h2>
<pre><code class="language-yml">eureka:
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://localhost:8761/eureka</code></pre>
<ul>
<li>해당 localhost 가 아닌 같은 네트워크에서 name 으로 접근할 수 있도록 바꿔줘야한다.</li>
</ul>
<h3 id="dockerfile-1">DockerFile</h3>
<pre><code>FROM openjdk:21-ea-11-slim
VOLUME /tmp
COPY target/apigateway-service-1.0.jar ApigatewayService.jar
ENTRYPOINT [&quot;java&quot; , &quot;-jar&quot; , &quot;ApigatewayService.jar&quot;]</code></pre><p>mvn clean compile -&gt; docker build -&gt; docker push -&gt; docker run</p>
<h3 id="docker-run">docker run</h3>
<p> docker run -d -p 8000:8000 --network ecommerce-network
 -e &quot;spring.cloud.config.uri=<a href="http://config-service:8888&quot;">http://config-service:8888&quot;</a> 
 -e &quot;spring.rabbitmq.host=rabbitmq&quot;
 -e &quot;eureka.client.service-url.defaultZone=<a href="http://discovery-service:8761/eureka/&quot;">http://discovery-service:8761/eureka/&quot;</a> 
 --name apigateway-service supportkim/apigateway-service:1.0</p>
<ul>
<li>application.yml 에 들어있는 로컬 환경에 정보를 containerName 으로 접근할 수 있도록 바꿔준다. </li>
<li>해당 apigateway 는 유레카 서버에 등록하기 위해서 eureka.client.service-rul.defaultZone 도 지정을 하기 위해서 discovery-service 라는 Name 을 넣어줬다.</li>
<li>마지막에 supportkim/... 은 여기서 컨테이너를 해당 docker image 를 가지고 와서 만들겠다.</li>
</ul>
<h2 id="mariadb-mysql">MariaDB (MySQL)</h2>
<h3 id="dockerfile-2">DockerFile</h3>
<pre><code>FROM mariadb
ENV MYSQL_ROOT_PASSWORD password
ENV MYSQL_DATABASE mydb
COPY ./mysql_data/data /var/lib/mysql
EXPOSE 3306
ENTRYPOINT [&quot;mysqld&quot;]</code></pre><ul>
<li>ROOT 라는 아이디에 PASSWORD 는 test1357</li>
<li>./mysql 이라는 폴더 전체를 컨테이너의 /var/lib/mysql 에다가 복사하도록 한다.</li>
<li>그래서 mydb 가 있는 mysql 폴더를 일단 아무 폴더를 만들어서 복사한다.</li>
<li><blockquote>
<p>cp -R /mysql경로/... ./복사할경로/...</p>
</blockquote>
</li>
</ul>
<p>DockerFile 있는 곳에서 docker build -&gt; docker run</p>
<p>docker build -t supportkim/my-mysqldb:1.0 .
docker run -d -p 3306:3306 --network ecommerce-network --name mariadb supportkim/my-mysqldb:1.0</p>
<p>docker exec -it mariadb /bin/bash 로 container 접속</p>
<ul>
<li>도커 컨테이너 내부로 들어갈 수 있고 여기에서 mysql -hlocalhost -uroot -p 로 접속하고 password 를 입력하면 DB 에 들어갈 수 있다.</li>
</ul>
<p>docker ps -a 명령어로 정상적으로 컨테이너들이 만들어진 것을 확인</p>
<h2 id="kafka">Kafka</h2>
<p>Zookeper + Kafka Server(Broker) 를 기동해야한다.</p>
<p>docker-compose 로 실행</p>
<ul>
<li>우리가 실행하려고 하는 도커 컨테이너를 하나의 스크립트 파일로 실행할 수 있도록 만들어준다.</li>
<li>여러가지 설정 파일들이 한 곳에 모여있고 명령어 하나로 실행 시켜야할 도커 컨테이너를 한꺼번에 실행한다.</li>
</ul>
<p>order-service 와 같은 곳에서 kafka 를 사용할 때 ProducerFacotry 에서 kafka 서버에 정보를 미리 넣어주면서 초기화를 시켰다.</p>
<ul>
<li>이때 127.0.0.1 로 넘겨줬지만 배포를 하면 local 이 아닌 컨테이너 이름으로 서로 호출을 해야하기 때문에 이 부분을 수정해야 한다.</li>
<li>따라서 우리가 기동하려는 zookeeper , kafka server 의 IP 주소를 직접 지정해야한다.</li>
<li>이러한 파일들을 docker-compose 파일로 만든다.</li>
<li>직접 작성하기 보다는 zookeeper + kafka 를 같이 사용할 수 있는 docker-compose 파일을 제공하는 곳에서 가지고 오면 된다.</li>
<li>git clone <a href="https://github.com/wurstmeister/kafka-docker.git">https://github.com/wurstmeister/kafka-docker.git</a></li>
</ul>
<h3 id="docker-compose-single-brokeryml">docker-compose-single-broker.yml</h3>
<pre><code class="language-yml">version: &#39;2&#39;
services:
  zookeeper:
    image: wurstmeister/zookeeper
    ports:
      - &quot;2181:2181&quot;
    networks:
      my-network:
        ipv4_address: 172.18.0.100
  kafka:
    # build: .
    image: wurstmeister/kafka
    ports:
      - &quot;9092:9092&quot;
    environment:
      KAFKA_ADVERTISED_HOST_NAME: 172.18.0.101
      KAFKA_CREATE_TOPICS: &quot;test:1:1&quot;
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    depends_on:
      - zookeeper
    networks:
      my-network:
        ipv4_address: 172.18.0.101
 networks:
  my-network: 
    external: true
    name: ecommerce-network # 172.18.0.1 ~
</code></pre>
<ul>
<li>zookeeper 는 100번을 사용하고 kafka 는 101 번을 사용하도록 했다.</li>
<li>직접 이렇게 지정을 해줘야 order-service 같은 곳에서 kafka 를 사용할 때 IP 주소를 넣을 수 있다.</li>
<li>networks 지정한 my-network 는 우리가 지금 계속 사용하고 있는 ecommerce-network 를 사용하도록 지정해주면 된다.</li>
<li>저장한 후 docker-compose -f docker-compose-single-broker.yml up -d</li>
<li><blockquote>
<p>현재 docker-compose 파일로 실행하기 때문에 명령어가 달라지고 up 명령어로 실행할 수 있고 down 으로 종료할 수 있다.</p>
</blockquote>
</li>
</ul>
<p><a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4/dashboard">참고자료</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git + GitHub (기본)]]></title>
            <link>https://velog.io/@support-kim/Git-GitHub-%EA%B8%B0%EB%B3%B8</link>
            <guid>https://velog.io/@support-kim/Git-GitHub-%EA%B8%B0%EB%B3%B8</guid>
            <pubDate>Mon, 04 Mar 2024 14:16:09 GMT</pubDate>
            <description><![CDATA[<h2 id="git-과-버전관리">Git 과 버전관리</h2>
<p>버전 관리의 1등 : Git
클라우드 저장소의 1등 :  GitHub</p>
<p>Git + GitHub 를 같이 사용하면 팀 프로젝트 및 회사에서 효율적인 개발 협업을 할 수 있다.</p>
<p>여러명의 개발자가 하나의 서비스를 만든다면?</p>
<ul>
<li>따로 작업을 하다가 내가 원할 때 코드를 합칠 수 있으면 좋겠고, 백업도 좀 쉽게 할 수 있으면 좋을 거 같다..</li>
<li><blockquote>
<p>Git 을 사용하면 된다.</p>
</blockquote>
</li>
</ul>
<h3 id="버전-관리-시스템-git">버전 관리 시스템 Git</h3>
<ul>
<li>단순히 ctrl + z 를 눌러 이전 상태로 되돌리는 것이 아니라 원하는 시점 마다 깃발을 꽂고 (버전을 만들고) 이들 간에 자유롭게 돌아다닐 수 있다.</li>
<li>내가 만든 버전 뿐만 아니라 동료가 만든 버전으로 이동할 수 있고, 동료와 내 버전을 비교해서 최신본으로 코드를 업데이트를 할 수 있다.</li>
</ul>
<h3 id="github-에-올리기">GitHub 에 올리기</h3>
<ul>
<li>컴퓨터 프로젝트 폴더에 Git 을 사용할 것이라고 명령 (git init)</li>
<li>코딩 후 내가 변경한 파일 중 올리길 원하는 것만 선택 (git add)</li>
<li>선택한 파일들을 한 덩어리로 만들고 설명 적어주기 (git commit -m &quot;message&quot;)</li>
<li>GitHub 사이트에서 Repository 만들기</li>
<li>프로젝트 폴더에 GitHub 저장소 주소 알주기 (git remote add)</li>
<li>내 컴퓨터에 만들었던 프로젝트를 GitHub 에 올리기 (git push)</li>
</ul>
<h2 id="git--github-with-cli">Git &amp; GitHub With CLI</h2>
<h3 id="git-init">git init</h3>
<ul>
<li>숨겨진 .git 폴더가 만들어지며, 이것을 로컬 저장소라고 한다.</li>
<li>로컬 저장소에 내가 만든 버전 정보 , 원격 저장소 주소 등이 저장된다.</li>
<li>원격 저장소에서 내 컴퓨터로 코드를 받아오면 로컬 저장소가 자동으로 생긴다.</li>
<li><blockquote>
<p>버전 정보를 가져와야 하기 때문</p>
</blockquote>
</li>
</ul>
<h3 id="git-add">git add</h3>
<ul>
<li>내가 변경한 파일 중 올리길 원하는 것만 선택</li>
</ul>
<h3 id="git-commit--m-message">git commit -m &quot;message&quot;</h3>
<ul>
<li>Commit : 하나의 버전</li>
<li>선택한 파일들을 한 덩어리로 만들고 설명 적어주기</li>
</ul>
<h3 id="git-log">git log</h3>
<ul>
<li>생성한 커밋을 볼 수 있다.</li>
</ul>
<p>커밋은 의미 있는 변동 사항을 묶어서 만든다.</p>
<ul>
<li>어떠한 파일을 수정했다면 이것을 왜 수정했는지를 손쉽게 파악할 수 있다.</li>
<li>커밋 메세지는 매우 중요하다.</li>
</ul>
<p>지금까지는 내 로컬 저장소에서만 버전관리를 하고 있기 떄문에 GitHub 에 올려서 다른 사람들과 함꼐 버전 관리를 해야한다.</p>
<h3 id="git-remote-add-origin">git remote add origin</h3>
<ul>
<li>git remote add origin GitHub 저장소 주소</li>
<li>GitHub 원격 저장소 주소를 로컬 저장소에 알려준다. (origin)</li>
</ul>
<h3 id="git-push-origin">git push origin</h3>
<ul>
<li>git push origin main(master)</li>
</ul>
<p>이번에는 다른 사람이 만든 저장소를 받아와보자.</p>
<h3 id="git-clone">git clone</h3>
<ul>
<li>원격 저장소를 내 컴퓨터에 받아오기</li>
<li>git clone GitHub 저장소 주소 .</li>
<li>마지막에 . 을 찍어줘야 현재 폴더에 내려받을 수 있다.</li>
<li>만약 안 찍는다면 새 폴더에 생성된다.</li>
</ul>
<h3 id="git-pull">git pull</h3>
<ul>
<li>원격 저장소의 데이터 가져오기</li>
<li>개발자A 가 원격 저장소에 올렸는데 개발자B 는 개발자A 가 올린 코드도 가져오고 싶을 때 pull 을 하면 된다.</li>
</ul>
<h3 id="콜라보레이터-추가">콜라보레이터 추가</h3>
<p>GitHub 저장소 -&gt; Settings -&gt; Manage access -&gt; Invite a collaborator -&gt; git hub username  -&gt; 해당 user 의 email 에서  ok 하면 해당 저장소에 push 권한을 가지게된다.</p>
<h3 id="시나리오">시나리오</h3>
<ol>
<li>A 가 A 노트북에 저장소 폴더를 만들고 git init 으로 로컬 저장소를 생성</li>
<li>A 가 로컬 저장소에 파일을 만들고 git add -&gt; git commit -m &quot;message&quot; 로 커밋을 생성</li>
<li>GitHub 에서 Repository 를 만들고 그 주소를 로컬 저장소에 알려주기 위해 git remote add origin 저장소 주소 명령어를 입력
3-1. 로컬 저장소에서 만든 커밋을 원격 저장소에 푸시 하기 위해 git push</li>
<li>B 도 개발을 시작하기 위해서 원격 저장소에 올리온 파일을 git clone 명령어로 가져온다.</li>
<li>개발을 한 후 GitHub 저장소에 push 하기 위해 B 계정을 Collaborator 로 추가 하고 commit 하고 push 한다.</li>
<li>B 가 올린 파일을 A 가 받아오기 위해서 git pull 명령어를 사용한다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Cloud + MSA 애플리케이션 개발 14(애플리케이션 배포를 위한 컨테이너 가상화)]]></title>
            <link>https://velog.io/@support-kim/Spring-Cloud-MSA-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-14%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EA%B0%80%EC%83%81%ED%99%94</link>
            <guid>https://velog.io/@support-kim/Spring-Cloud-MSA-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-14%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EA%B0%80%EC%83%81%ED%99%94</guid>
            <pubDate>Mon, 04 Mar 2024 10:17:37 GMT</pubDate>
            <description><![CDATA[<h2 id="컨테이너-가상화">컨테이너 가상화</h2>
<h3 id="virtualization">Virtualization</h3>
<ul>
<li>물리적인 컴퓨터 리소스를 다른 시스템이나 애플리케이션에서 사용할 수 있도록 제공</li>
<li><blockquote>
<p>플랫폼 가상화</p>
</blockquote>
</li>
<li><blockquote>
<p>리소스 가상화</p>
</blockquote>
</li>
<li>하이퍼바이저 (Hypervisor)</li>
<li><blockquote>
<p>Virtual Machine Manager (VMM)</p>
</blockquote>
</li>
<li><blockquote>
<p>다수의 운영체제를 동시에 실행하기 위한 논리적 플랫폼</p>
</blockquote>
</li>
</ul>
<h3 id="os-virtualization">OS Virtualization</h3>
<ul>
<li>Host OS 위에 Guest OS 전체를 가상화</li>
<li>VMWare , VirtualBox</li>
<li>자유도가 높으나, 시스템에 부하가 많고 느려짐</li>
<li>Host OS 위에 Hypervisor 를 사용해서 Guest OS 를 놓게 되면 매우 무거워지기 때문에 overhead 가 발생한다.</li>
</ul>
<h3 id="container-virtualization">Container Virtualization</h3>
<ul>
<li>Host OS 가 가진 리소스를 적게 사용하며, 필요한 프로세스 실행</li>
<li>최소한의 라이브러리와 도구만 포함</li>
<li>Container 의 생성 속도 빠름</li>
<li>Host OS 바로 위에 컨테이너를 실행할 수 있는 소프트웨어를 놓으면 된다.</li>
<li>Hypervisor , Guest OS 가 들어가지 않기 때문에 매우 빠르다.</li>
<li>이번 예제에서는 Docker Engine 이라는 소프트웨어를 사용한다.</li>
</ul>
<h3 id="container-image">Container Image</h3>
<ul>
<li>Container 실행에 필요한 설정 값</li>
<li><blockquote>
<p>상태값X , Immutable</p>
</blockquote>
</li>
<li>Image 를 가지고 실체화 -&gt; Container</li>
</ul>
<p>예시</p>
<ol>
<li>우분투 이미지 : 우분투를 실행하기 위해서 필요한 파일들</li>
<li>Mysql 이미지 : MySQL 을 실행하기 위해서 필요한 파일 , 명령어 , 포트정보 등등</li>
</ol>
<h3 id="registry">Registry</h3>
<p>그렇다면 어떻게 이미지를 생성하고 다운로드 받을까?</p>
<ul>
<li>이미지는 컨테이너가 실행에 필요한 설정값 , 정보들을 가지고 있다는 것이라고 생각하면 된다.</li>
<li>이러한 이미지를 저장할 수 있는 저장소가 필요한데 그것을 Registry 라고 하고 public , private 으로 나뉠 수 있다.</li>
<li>보통 public Registry 를 Docker hub 라고 한다.</li>
<li>private Registry 도 있다.</li>
</ul>
<h3 id="docker-host">Docker Host</h3>
<ul>
<li>Docker Host 는 이미지를 사용할 수 있는 컨테이너 서버 자체를 의미한다.</li>
<li>호스트에서 실행할 수 있는 Local Repository 가 있다.</li>
<li>Docker Host 에서 public Registry 또는 private Registry 에서 이미지를 다운 받아서 Local Repository 에 저장한다.</li>
<li>위에서 설명한 것을 pull 한다 라고 표현한다.</li>
<li>Local Repository 에 있는 이미지를 가지고 필요한 컨테이너를 생성할 수 있다.</li>
</ul>
<h3 id="pull--create--start--run">pull , create , start , run</h3>
<ul>
<li>pull : Registry 에서 Local Repository 로 이미지를 다운 받는 것</li>
<li>create : Local Repository 의 이미지를 가지고 컨테이너 생성</li>
<li>start : Local Repository 의 이미지를 가지고 컨테이너 생성</li>
<li>run : pull + create + start 를 한 번에 해주는 것</li>
</ul>
<p>외부에서(클라이언트) 사용하기 위해서 적절한 포트가 공개되어 있다면 실제로 사용할 수 있다.</p>
<h3 id="dockerfile">Dockerfile</h3>
<ul>
<li>Docker Image 를 생성하기 위한 스크립트 파일</li>
<li>자체 문법을 사용한다 -&gt; 이미지 생성과정 기술</li>
</ul>
<h3 id="명령어">명령어</h3>
<p>FROM  mysql:5.7 
-&gt; mysql 로부터 서버를 만들겠다는 것
EVN MYSQL_ALLOW_EMPTY_PASSWORD 
EVN MYSQL_DATABASE mydb
-&gt; 이미지 생성에 필요한 환경변수가 있다면 이렇게 설정하면 된다.</p>
<p>ADD
-&gt; 가지고 있었던 로컬에 파일을 이미지 가지고 있는 곳에 저장할 때</p>
<p>EXPOSE
-&gt; 생성된 컨테이너가 외부에 공개할 포트</p>
<p>CMD
-&gt; 이 이미지가 전부다 실행된 뒤 최종적으로 mysqld 라는 커맨드를 실행하라는 것</p>
<p>Docker Destop 을 사용해서 배포를 해보자!</p>
<h2 id="docker-컨테이너">Docker 컨테이너</h2>
<h3 id="컨테이너-실행">컨테이너 실행</h3>
<ul>
<li>docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND][ARG...]</li>
<li>TAG 는 version 처럼 생각하면 된다.</li>
<li><blockquote>
<p>같은 이름에 이미지를 생성하고 여러개의 버전을 만들거나 용도별로 태깅할 때 사용한다.</p>
</blockquote>
</li>
<li>[COMMAND][ARG] 는 추가적으로 파라미터를 전달해서 컨테이너가 생성될 때 필요한 부가적인 정보를 전달할 수 있다.</li>
</ul>
<h3 id="options-종류">OPTIONS 종류</h3>
<ul>
<li>-d : detached mode 흔히 말하는 백그라운드 모드</li>
<li>-p : 호스트와 컨테이너의 포트를 연결 (포워딩)</li>
<li><blockquote>
<p>Host OS 위에 Docker Engine 위에 우리가 원하는 Container 를 올리면 된다.</p>
</blockquote>
</li>
<li><blockquote>
<p> 이때 올린 Container 와 Host OS 를 연결하는 것이라고 생각하면 된다.</p>
</blockquote>
</li>
<li>-v : 호스트와 컨테이너의 디렉토리를 연결 (마운트)</li>
<li>-e : 컨테이너 내에서 사용할 환경 변수 설정</li>
<li>--name : 컨테이너 이름 설정</li>
<li>--rm : 프로세스 종료시 컨테이너 자동 제거</li>
<li>--it : -i 와 -t 를 동시에 사용한 것으로 터미널 입력을 위한 옵션</li>
<li><blockquote>
<p>컨테이너가 실행된 다음에 부가적으로 컨테이너에 명령어를 전달하기 위해 사용하는 것</p>
</blockquote>
</li>
<li>--link : 컨테이너와 연결 [컨테이너명:별칭]</li>
</ul>
<p>예시</p>
<ul>
<li>docker run ubuntu:16.04</li>
<li><blockquote>
<p>OPTIONS 를 사용하지 않고 IMAGE 가 ubuntu 에 TAG 를 16.04 로 지정한 명령어</p>
</blockquote>
</li>
</ul>
<h3 id="dockerhub">DockerHub</h3>
<ul>
<li><a href="https://hub.docker.com/">DockerHub</a> 링크에 가서 ubuntu , mysql 이러한 것들을 검색하면 이미지들을 다운로드 받을 수 있다.</li>
<li>ubuntu 를 검색하고 TAGS 를 들어가보면 해당 버전의 이미지를 가져올 수 있는 명령어를 복사할 수 있는데 pull 로 되어있다.</li>
<li>하지만 run 을 사용하면 pull + create + start 를 해주기 때문에 run 좀 더 편하다.</li>
</ul>
<h3 id="docker-명령어">Docker 명령어</h3>
<pre><code> 이미지 조회
 docker image ls

 컨테이너 조회
 docker container ls

 우분투 이미지 실행
 docker run ubuntu:16.04

실행됐던 컨테이너 조회
docker container ls -a == docker ps -a

컨테이너 삭제
docker container rm CONTAINER_ID</code></pre><p>이미지는 필요한 정보에 따라서 레이어로 구분되어 있어서 그 구분된 레이어대로 다운로드가 된다.</p>
<pre><code>docker run ubuntu:16.04 실행 후 로그
828b35a09f0b: Pull complete
66927c6d1d3d: Pull complete
000560be9165: Pull complete
6225a0253717: Pull complete</code></pre><h2 id="컨테이너-생성과-실행">컨테이너 생성과 실행</h2>
<p>이번에는 도커를 이용해서 mysql database 를 실행해보자.</p>
<ol>
<li>docker run -d -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=true --name mysql mysql:5.7</li>
</ol>
<ul>
<li>-d 는 백그라운드 모드</li>
<li>-p 는 포트 포워딩 ( : 을 기준으로 2가지를 정해주는데, 앞에 있는건 HOST PC 에서 접근하고자 하는것 , 뒤에 있는건 컨테이너에서 응답하기 위한 포트 번호)</li>
<li><blockquote>
<p>mysql 은 기본적으로 3306 포트 번호 사용  </p>
</blockquote>
</li>
<li>-e 는 환경 변수 전달 mysql:5.7 버전을 사용할것인데 이 버전의 특징은 루트 패스워드를 설정해줘야 한다.</li>
<li><blockquote>
<p>그게 만약 아니라면 EMPTY_PASSWORD 를 사용 , 그래서 해당 설정을 넣은 것이다.</p>
</blockquote>
</li>
<li>--name mysql 은 우리가 부여하는 컨테이너 이름</li>
<li><blockquote>
<p>만약 부여하지 않으면 랜덤한 이름이 부여되기 때문에 찾기 어려울 수 있다.</p>
</blockquote>
</li>
</ul>
<ol start="2">
<li>docker exec -it mysql /bin/bash</li>
</ol>
<ul>
<li>exec 는 추가적으로 컨테이너에다가 커맨드에 추가적인 쿼리를 넣고 싶을때</li>
<li>exec -it 실행되어 있는 컨테이너에 키보드와 같은 도구를 이용해서 터미널에 전달하겠다는 것</li>
<li>mysql 은 컨테이너이름 또는 컨테이너 아이디를 넣어주면 된다.</li>
<li>bash 는 어떤 쉘을 이용할꺼냐 여기서는 /bin/bash 쉘을 이용하겠다</li>
</ul>
<p>먼저 1번 명령어를 실행하면 포트 충돌 에러가 발생한다.</p>
<ul>
<li>그렇다면 13306:3306 으로 수정하면 된다.</li>
<li>MacOS 의 13306 port 와 컨테이너에 있는 3306 포트를 연결하겠다.</li>
<li><blockquote>
<p>즉 MacOs 에서 연결하기 위해서는 13306 포트번호로, 컨테이너 내부에서는 3306 포트로 연결</p>
</blockquote>
</li>
</ul>
<p>다시 1번 명령어를 실행하면 또 오류가 발생한다.</p>
<ul>
<li>그 이유는 1번 명령어를 먼저 실행 했었는데 그때 이미 컨테이너가 만들어졌기 때문에 컨테이너 이름이 같아서 오류가 발생한다.</li>
<li>docker container rm 컨테이너이름 명령어로 제거 후 다시 1번 명령어를 실행하면 정상적으로 동작한다.</li>
<li>실행 결과 우분투의 /bin/bash 로 들어올 수 있다.</li>
<li>여기서  mysql -uroot -p 을 실행하면 mysql 을 찾을 수 없다고 한다.</li>
<li>이때 sudo 도 설치가 되지 않았기 때문에 mysql 도 설치할 수 없었다.
```</li>
</ul>
<ol>
<li>apt-get update</li>
<li>apt-get install sudo</li>
<li>sudo apt install -y mysql-server
```</li>
</ol>
<ul>
<li>위에 명령어를 순차적으로 실행한 후 다시 mysql -uroot -p 을 실행하면 mysql 에 접속할 수 있다.</li>
</ul>
<p>여기서 mydb 라는 database 를 만들고 exit 명령어로 나왔다.</p>
<p>그런후 MySQL 워크벤치를 열고 Connection 을 만들 떄 Port 를 13306 으로 바꿔주고 들어가보면 위에서 설치했던 mydb 라는 database 가 있는 것을 확인했다.</p>
<ul>
<li>즉 컨테이너와 Host 가 연결이 된 것</li>
</ul>
<h2 id="docker-이미지-생성과-public-registry-에-push">Docker 이미지 생성과 Public Registry 에 Push</h2>
<p>FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY target/users-ws-0.1.jar users-service.jar
ENTRYPOINT [&quot;java&quot; , &quot;-Djava.security.egd=file:/dev/./urandom&quot; , &quot;-jar&quot;, &quot;users-service.jar&quot;]</p>
<p>FROM : 우리가 앞으로 올린 user-service 와 같은 경우는 java 가 꼭 있어야 하기 때문에 FROM 에 java 로부터 만들어 달라는 명령어가 있어야 한다.</p>
<p>VOLUME : /tmp 라는 가상의 디렉토리 , 컨테이너 안에 tmp 를 넣기 (임시 티렉토리)</p>
<p>COPY : 앞에 있는 것을 뒤에 있는 곳으로 복사해달라 -&gt; target/... 파일을 user-service.jar 파일로 복사해달라는 것</p>
<p>ENTRYPOINT : 어떤 명령어를 가지고 도커를 실행할 것인지에 대한 실행 커맨더들을 명시
-&gt; java , user-service.jar 이런 명령어들이 있으면 도커가 실행</p>
<p>docker build -t supportkim/user-service:1.0 .
-&gt; 이미지를 만들기 위해서 사용되는 커맨드가 build
-&gt; -t 는 어떤 이름으로 만들어달라는건지 설정
-&gt; 마지막에 . 은 현재 디렉토리에 있는 도커 파일을 가지고 이미지를 만들어 달라는 것</p>
<p>docker push supportkim/user-service:1.0
-&gt; Hub 사이트에서 supportkim/user-service:1.0 으로 업로드
docker pull supportkim/user-service:1.0
-&gt; Hub 사이트에서 supportkim/user-service:1.0 으로 다운로드</p>
<p>우선 user-service 에 들어가서 target 와 같은 depth 에 Dockerfile 을 만든다.</p>
<pre><code>FROM openjdk:17-ea-jdk-slim
VOLUME /tmp
COPY target/user-service-1.0.jar UserService.jar
ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;UserSerivce.jar&quot;]</code></pre><ul>
<li>위와 같이 설정을 하고 티미털에서 가서 DockerFile 과 target 이 같이 있는지 확인한다.</li>
<li>같이 있다면 mvn clean compile package -DskipTests=true 명령어를 실행한다.</li>
<li>그러면 /target 파일에 user-service-0.0.1-SNAPSHOT.jar 파일이 만들어지는데 파일명이 너무 길기 때문에 pom.xml 에서 version 부분을 1.0 으로 바꿔준다.</li>
<li>그런후 다시 명령어를 실행하면 user-service-1.0.jar 파일로 만들어진다.</li>
<li>이렇게 위에서 DockerFile 에 만든 커맨드와 똑같이 만들었다. (COPY)</li>
</ul>
<p>docker build --tag supportkim/user-service:1.0 .</p>
<ul>
<li>우리가 정해준 총 4가지의 커맨드가 있기 떄문에 일정한 절차에 의해서 도커 파일이 만들어진다.</li>
<li>다 만들어진 후 docker image ls 명령어를 실행하면 방금 만들었던 supportkim/user-service:1.0 이 만들어진 것을 확인할 수 있다.</li>
</ul>
<p>docker push supportkim/user-service:1.0</p>
<ul>
<li>명령어를 실행하고 난 후 DockerHub 사이트에 들어가보자.</li>
<li>들어가보면 support/kim/user-service 의 이름의 Repository 가 만들어진다.</li>
</ul>
<p>다시 터미널에서 user-service 에 들어간다.</p>
<ul>
<li>docker rmi IMAGE_ID 로 이미지를 삭제한다.</li>
<li>IMAGE_ID 는 docker images 로 알 수 있다.</li>
</ul>
<p>docker pull supportkim/user-service:1.0</p>
<ul>
<li>이 이미지로 다시 가져올 수 있다.</li>
</ul>
<p>build -&gt; push -&gt; pull 로 가지고 와서 docker run supportkim/user-service:1.0 명령어를 실행하면 Spring 서버가 실행된다!!!!</p>
<ul>
<li>에러가 발생하는 이유는 user-service 를 실행하기 전 몇 개 더 실행해야하는 서버가 있어야 하는데 실행하지 않아서 발생한 오류들이다.</li>
<li>docker run -d ... 를 사용해서 백그라운드에서 기동할 수 있다.</li>
</ul>
<p>다른 서비스 , 운영해야할 서비스들을 도커에 이전해서 동작해도록 해보자!</p>
<p><a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4/dashboard">참고자료</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Cloud + MSA 애플리케이션 개발 12(장애 처리와 Microservice 분산 추적)]]></title>
            <link>https://velog.io/@support-kim/Spring-Cloud-MSA-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-12%EC%9E%A5%EC%95%A0-%EC%B2%98%EB%A6%AC%EC%99%80-Microservice-%EB%B6%84%EC%82%B0-%EC%B6%94%EC%A0%81</link>
            <guid>https://velog.io/@support-kim/Spring-Cloud-MSA-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-12%EC%9E%A5%EC%95%A0-%EC%B2%98%EB%A6%AC%EC%99%80-Microservice-%EB%B6%84%EC%82%B0-%EC%B6%94%EC%A0%81</guid>
            <pubDate>Fri, 01 Mar 2024 09:24:31 GMT</pubDate>
            <description><![CDATA[<h2 id="circuitbreaker-와-resilience4j의-사용">CircuitBreaker 와 Resilience4J의 사용</h2>
<p>Microservice 끼리 통신하기 위해서 RestTemplate , FeignClient 를 사용했었다.</p>
<ul>
<li>user-service 에서 order-service 를 호출할 때, order-service 서버 안에서 문제가 발생한다면 호출 자체가 되지 않아서 오류가 발생할 것이다.</li>
<li>하지만 MSA 는 독립적으로 해서 오류도 최소화를 시키기 위해서 사용하는 아키텍처이다.</li>
<li>즉 order-service 에 문제가 생겼다고 하더라도 user-service 에서는 정상인 것 처럼 보여지게 하면 된다.</li>
<li>user-service 에서 해당 user 의 사용자 정보와 주문 내역을 가져오기 위해서 order-service 를 호출한다.</li>
<li>이때 order-service 에 문제가 발생했을 때 주문 내역은 못 가져오더라도 사용자의 정보만큼만이라도 가져오면 된다.</li>
<li>만약 order-service 가 복구가 된다면 주문내역까지 가지고 올 수 있도록 해주면 된다.</li>
</ul>
<p>즉 복구가 된다면 이전처럼 흐름을 바꿔주면 된다.</p>
<ul>
<li>이런 역할을 하는 것이 바로 CircuitBreaker 이다.</li>
</ul>
<h3 id="circuitbreaker">CircuitBreaker</h3>
<ul>
<li>장애가 발생하는 서비스에 반복적인 호출이 되지 못하게 차단</li>
<li>특정 서비스가 정상적으로 동작하지 않을 경우 다른 기능으로 대체 수행 -&gt; 장애 회피</li>
<li>client(user-service) -&gt; CircuitBreaker -&gt; supplier(order-service)</li>
<li>위와 같은 흐름을 가지는데 이때 supplier 에서 응답을 하지 못해서 timeout 이 발생할 경우 CircuitBreaker 에서 가로채서 오류가 발생하지 않도록 해준다.</li>
</ul>
<h3 id="circuitbreaker-closed--open">CircuitBreaker Closed &amp; Open</h3>
<ul>
<li>Microservice 의 통신이 되기 때문에 Closed 상태이다. (정상적일때)</li>
<li>Microservice 의 통신이 되지 않을 때는 Open 상태이다.</li>
<li><blockquote>
<p>Open 일 경우에 우회해서 작업할 수 있도록 한다.</p>
</blockquote>
</li>
</ul>
<p>즉 연쇄적으로 연결되어 있는 Microservice에 문제가 생기더라도 해당하는 Microservice 만큼은 정상적으로 작동할 수 있도록 한다.</p>
<h3 id="의존성-추가">의존성 추가</h3>
<p>원래는 Netflix Hystrix 의존성을 추가하지만 해당 라이브러리는 더이상 수정을 하지 않기 때문에 Resilience4j 라는 의존성을 추가하면 된다.</p>
<h3 id="reilience4j-기능">Reilience4j 기능</h3>
<ul>
<li>CircuitBreaker , ratelimiter , bulkhead , retry , timelimiter , cache</li>
</ul>
<h3 id="해야할-작업">해야할 작업</h3>
<ul>
<li>CircuitBreaker 의 기능을 사용하기 위해서 CircuitBreakerFactory 를 DI 해서 사용하면 된다.</li>
<li>user-service 에서 getOrders() 가 order-service 와 통신하는 method 이다.</li>
<li>circuitBreaker.run() 을 이용해서 만약 오류가 발생하는 경우에는 throwable -&gt; new ArrayList&lt;&gt;() 를 해서 빈 리스트라도 반환할 수 있도록 한다.</li>
</ul>
<h3 id="customizer">Customizer</h3>
<ul>
<li>Resilience4JCircuitBreakerFactory 를 사용해서 조금 더 다양한 옵션을 사용해보자. (Customizer&lt;&gt; 사용)</li>
<li>.failureRateThreshold() : circuitBreaker 를 열지 결정하는 확률이다.</li>
<li><blockquote>
<p>default 는 50 인데 10번중에 5번 실패이면 open 시킨다는 것</p>
</blockquote>
</li>
<li><blockquote>
<p>만약 4 로 하면 100중에 4번 실패하면 open 시킨다는 것</p>
</blockquote>
</li>
<li>.waitDurationInOpenstate() : circuitBreaker 를 open 한 상태를 유지하는 지속 기간을 의미한다.</li>
<li>.slidingWindowType() : 정상적으로 되면 closed 가 되는데 그때 계속 호출했었던 결과 값을 저장하기 위해서 count 기반으로 할건지 time 기반을 할건지 결정 (디폴트는 횟수count)</li>
<li>.slidingWinodwSize() : Type 에서 설정한 크기를 구성</li>
<li>TimeLimiter 는 supplier 의 time limit 을 정하는 API</li>
<li><blockquote>
<p>만약 4로 설정했다면 order-service 가 4초동안 응답이 없을 경우에 문제로 간주한다는 것</p>
</blockquote>
</li>
</ul>
<h2 id="circuitbreaker-적용">circuitBreaker 적용</h2>
<ol>
<li>rabbitmq-server 명령어로 실행</li>
<li>circuitBreaker-resilience4j 의존성 추가<pre><code class="language-java">// UserServiceImpl.getOrder()
</code></pre>
</li>
</ol>
<p>//        List<ResponseOrder> orderList = orderServiceClient.getOrders(userId);</p>
<p>// CircuitBreaker 적용 코드
// DI 해서 사용
CircuitBreakerFactory circuitBreakerFactory; 
// 문제가 생겼을 경우 throwable -&gt; new ArrayList&lt;&gt;()
List<ResponseOrder> orderList =
                circuitBreaker.run(() -&gt; orderServiceClient.getOrders(userId),
                        throwable -&gt; new ArrayList&lt;&gt;());</p>
<pre><code>- 위와 같이 코드를 수정하고 나면 order-service 를 기동하지 않아도 오류가 발생하는 것이 아니라 주문내역(orders) 부분은 빈 리스트로 반환이 되면서 정상 반환이 된다.

### Custom
```java
@Configuration
public class Resilience4JConfig {

    @Bean
    public Customizer&lt;Resilience4JCircuitBreakerFactory&gt; globalCustomConfiguration() {

        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .failureRateThreshold(4)
                .waitDurationInOpenState(Duration.ofMillis(1000))
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
                .slidingWindowSize(2) // 2번의 카운트가 저장된다.
                .build();

        TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
                .timeoutDuration(Duration.ofSeconds(4))
                .build();

        return factory -&gt; factory.configureDefault(id -&gt; new Resilience4JConfigBuilder(id)
                .timeLimiterConfig(timeLimiterConfig)
                .circuitBreakerConfig(circuitBreakerConfig)
                .build()
        );
    }
}</code></pre><ul>
<li>일반적으로 CircuitBreaker 를 DI 받아서 사용해도 되지만 위에 코드 처럼 Custom 해서 사용할 수 있다.</li>
<li>Resilience4JCircuitBreakerFactory 를 반환하면 되는데 이떄 필요한 config 들을 만들고 넣어준 후 반환하면 된다.</li>
<li>CircuitBreakerFactory 를 DI 하면 Custom 한게 있다면 Custom 한 것을 사용하기 때문에 따로 또 추가되는 코드는 없다.</li>
</ul>
<p>Custom 하더라도 똑같이 동작하고, 만약 order-service 서버가 정상적으로 기동되고 있다면 주문내역(orders) 부분은 정상적으로 출력이 된다.</p>
<h2 id="분산-추적의-개요-zipkin-서버-설치">분산 추적의 개요 Zipkin 서버 설치</h2>
<p>더이상 연쇄적인 문제를 발생하지 않도록 CircuitBreaker 를 배워봤다.</p>
<p>여러개의 서비스가 연결되어 있고 필요한 정보는 RestTemplate , FeignClient 을 사용해서 데이터를 공유하고 전달할 수 있었다.</p>
<p>하나의 서비스가 시작이 되고 끝이 날 때 여러 Microservice 가 연결이 될 수 있기 떄문에 누가 누구를 거치고 어디가 문제가 되었는지 확인하는 것이 중요하다.</p>
<p>그런것을 확인하고 log 로 남기는 것이 Zipkin 이라고 생각하면 된다.</p>
<h3 id="zipkin">Zipkin</h3>
<ul>
<li>Twitter 에서 사용하는 분산 환경의 Timing 데이터 수집 , 추적 시스템 (오픈소스)</li>
<li>분산 환경에서의 시스템 병목 현상 파악</li>
<li>Collector , Query Service , Databasem WebUI 로 구성</li>
<li>Span</li>
<li><blockquote>
<p>하나의 요청에 사용되는 작업의 단위 (Unique)</p>
</blockquote>
</li>
<li>Trace</li>
<li><blockquote>
<p>트리 구조로 이뤄진 Span 셋</p>
</blockquote>
</li>
<li><blockquote>
<p>하나의 요청에 대한 같은 Trace ID 발급</p>
</blockquote>
</li>
</ul>
<h3 id="spring-cloud-sleuth">Spring Cloud Sleuth</h3>
<ul>
<li>스프링 부트 애플리케이션을 Zipkin 과 연동해주는 것</li>
<li>Zipkin 에 전달을 해주는 역할</li>
<li>요청 값에 따른 Trace ID , Span ID 부여</li>
<li>Trace 와 Span ids 를 로그에 추가 가능</li>
</ul>
<h3 id="spring-cloud-sleuth--zipkin">SPring Cloud Sleuth + Zipkin</h3>
<ul>
<li>ServiceA -&gt; ServiceB -&gt; ServiceC , ServiceD</li>
</ul>
<ol>
<li>A -&gt; B -&gt; C</li>
</ol>
<ul>
<li>A -&gt; B 일 때 Trace ID : A , Span ID : A (처음에는 같은 걸로 할당)</li>
<li>B -&gt; C 일 때 Trace ID : A , Span ID : B</li>
</ul>
<ol start="2">
<li>A -&gt; B -&gt; D</li>
</ol>
<ul>
<li>A -&gt; B 일 떄 Trace ID : Q , Span ID : V</li>
<li>B -&gt; D 일 때 Trace ID : Q , Span ID : W</li>
</ul>
<p>정리하면 새로운 요청이 있을 때 Trace 는 같지만 Span 은 달라진다.</p>
<p>terminal 에서 zipkin 서버를 설치</p>
<ul>
<li>java -jar zipkin.jar 로 집킨 서버 실행할 수 있다. (9411 Port)</li>
</ul>
<h2 id="spring-cloud-sleuth--zipkin-1">Spring Cloud Sleuth + Zipkin</h2>
<pre><code class="language-yml">        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-actuator&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;io.micrometer&lt;/groupId&gt;
            &lt;artifactId&gt;micrometer-tracing-bridge-brave&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;io.zipkin.reporter2&lt;/groupId&gt;
            &lt;artifactId&gt;zipkin-reporter-brave&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;io.micrometer&lt;/groupId&gt;
            &lt;artifactId&gt;micrometer-registry-prometheus&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;io.github.openfeign&lt;/groupId&gt;
            &lt;artifactId&gt;feign-micrometer&lt;/artifactId&gt;
        &lt;/dependency&gt;</code></pre>
<ul>
<li>의존성 추가</li>
<li>Sleuth 가 Deprecated 가 됐기 떄문에 micometer 로 대신 사용한다.</li>
</ul>
<h2 id="sleuth-deprecated">Sleuth Deprecated</h2>
<ul>
<li>Spring Boot 3 버전 이후로 slueth 가 지원이 안 된다.</li>
</ul>
<h3 id="trace-id-and-span-id">Trace ID and Span ID</h3>
<ul>
<li>user-service 에서 getOrder() 로 실행하면 Log 에 TraceID , SpanID 가 나온다.</li>
<li>user-service 와 order-service 의 TraceID 는 같지만 SpanID 는 다른것을 확인할 수 있다.</li>
</ul>
<p>Zipkin 서버에 가서 TraceID 를 입력해서 조회할 수 있고, service 의 이름을 검색해서 추적하거나 Dependencies 로도 추적할 수 있다.</p>
<p>만약 order-service 에서 장애(예외)가 발생했다면 똑같이 user-service 와 order-service 의 TraceID 는 서로 같지만 SpanID 는 다르다.</p>
<p>장애가 발생했을 때도 어디서 발생했고 log 가 무엇인지 dashboard 로 확인할 수 있다.</p>
<pre><code class="language-yml">management:
  tracing:
    sampling:
      probability: 1.0
    propagation:
      type: b3
  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans

 logging:
  pattern:
    level: &quot;%5p [%X{traceId:-},%X{spanId:-}]&quot;
 spring:
   cloud:
    openfeign:
      micrometer:
        enabled: true</code></pre>
<ul>
<li>order-service 뿐만 아니라 user-service 에도 yml 파일을 수정하고 의존성도 추가해야한다.</li>
<li>sleuth 가 deprecated 가 되면서 사용할 수 없고 여러가지 정보를 추가해야한다.</li>
<li>어차피 Zipkin 서버가 떠있다면 traceId 와 spanId 는 나오기 떄문에 문제 없다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/support-kim/post/12a9740f-620f-43df-ac7a-f20197192867/image.png" alt=""></p>
<p>서버를 기동하고 POSTMAN 으로 주문 , 주문확인의 로직을 거치면 TracdID 와 SpanID 가 정상적으로 나오는 것을 확인할 수 있다.</p>
<ul>
<li>TraceID 는 같고 , SpanID 는 달라지는것도 확인했다.</li>
<li>localhost:9411 로 접속하면 zipkin 의 대시보드가 나오고 여기서 TracdID 로 검색하면 로그를 추적할 수 있고 serviceName 으로도 검색할 수 있고 Dependencies 로도 검색할 수 있다.</li>
</ul>
<p><a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4/dashboard">참고자료</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Cloud + MSA 애플리케이션 개발 11(데이터 동기화를 위한 Kafka 2)]]></title>
            <link>https://velog.io/@support-kim/Spring-Cloud-MSA-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-11%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%8F%99%EA%B8%B0%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-Kafka-2</link>
            <guid>https://velog.io/@support-kim/Spring-Cloud-MSA-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-11%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%8F%99%EA%B8%B0%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-Kafka-2</guid>
            <pubDate>Thu, 29 Feb 2024 12:14:24 GMT</pubDate>
            <description><![CDATA[<h2 id="orderservice--catalogsservice-에-kafka-적용">OrderService , CatalogsService 에 Kafka 적용</h2>
<p>order-service 에서 주문을 하게 되면 catalog-service 에서는 주문 수량이 그만큼 감소를 해야한다.</p>
<ul>
<li>기본적인 Front-Back 방식에서는 공통의 DB 를 사용기 때문에 Table 을 공유해서 상관이 없다.</li>
<li>하지만 Microservice 를 사용하면서 우리는 독립적인 DB 를 가지도록 했기 때문에 order-service 에서는 주문을 했지만 catalog-service 에는 상품 수량에 대한 관리가 이루어지지 않는다.</li>
<li>이러한 문제를 해결하기 위해서 Kafka 를 통해 데이터 동기화를 하려고 한다.</li>
</ul>
<ol>
<li>order-serivce 에 요청된 주문의 수량 정보를 catalogs-service 에 반영</li>
<li>oder-service 에서 kafka Topic 으로 메시지 전송 -&gt; Producer</li>
<li>caltalog-service 에서 Kafka Topic 에 전송 된 메시지 취득 -&gt; Consumer</li>
</ol>
<p>order-service -&gt; Meessage Queuing Service(Kafka) -&gt; catalog-service</p>
<h3 id="consumer-catalogs-service">Consumer (catalogs-service)</h3>
<ol>
<li>spring kafka 의존성 추가</li>
<li>KafkaConsumerConfig 생성</li>
</ol>
<ul>
<li>KafkaConsumerConfig 에는 Topic 에 접속하기 위해 필요한 정보들을 빈으로 등록</li>
<li><blockquote>
<p>데이터를 지정해줄 때 토픽에 저장되는 값 자체가 어떠한 형태로 등록되는지 정해줘야 한다.</p>
</blockquote>
</li>
<li><blockquote>
<p>우리는 JSON 형식으로 등록되기 때문에 Key , Value 가 한 세트가 토픽에 저장되어 있을 때 그 값을 가지고 와서 사용하도록 한다.</p>
</blockquote>
</li>
<li><blockquote>
<p>예제에서는 String 으로 들어가기 때문에 String Deserializer.class 를 등록</p>
</blockquote>
</li>
<li>토픽에 어떤 변경사항이 있을 때 그 값을 캐치할 수 있는 리스너도 등록</li>
</ul>
<ol start="3">
<li>KafkaConsumer 라는 @Service 를 등록</li>
</ol>
<p>-&gt; 토픽에 변경 된 데이터 값을 가지고 와서 실제 DB 에 반영 해야한다.
-&gt; 그 로직이 있는 method 에 @KafkaListener 를 붙혀주면 된다. (@KafkaListner(topcis=&quot;order-topic&quot;)
-&gt; 현재 예제에서는 주문한 상품id , 주문 수량이 넘어올텐데 id 를 가지고 상품을 찾고 현재 상품의 개수에서 주문 수량만큼 빼주면 데이터 동기화가 된다.</p>
<h3 id="producer-order-service">Producer (order-service)</h3>
<ol>
<li>spring kafka 의존성 추가</li>
<li>KafkaProducerConfig 생성</li>
</ol>
<p>-&gt; 카프카 토픽에 데이터를 보내기 위해서 KafkaTemplate 을 등록
3. KafkaProducer 생성 @Service
4. topic 에 전달 할 때 kafkaTemplate 을 통해 보내는데, JSON 형태로 보내야하기 때문에 변환을 하고 보내면 된다.</p>
<ul>
<li>kafkaTemplate.send() 를 통해서 보내면 된다.</li>
<li>정해져있는 타입에 맞춰서 보내야한다. (전에 봤었던 JSON 포맷)</li>
</ul>
<ol start="5">
<li>order-service 에서 createOrder() 부분에 원래 Order 객체를 만드는 부분만 있었는데 여기에 topic 에 보내는 코드까지 포함시키면 된다.</li>
</ol>
<h2 id="catalogs-microservice-수정">Catalogs Microservice 수정</h2>
<ul>
<li>우선 spring-kafka 의존성 추가</li>
</ul>
<pre><code class="language-java">// Kafka 사용
@EnableKafka
@Configuration
public class KafkaConsumerConfig {

    @Bean
    public ConsumerFactory&lt;String,String&gt; consumerFactory() {
        Map&lt;String,Object&gt; properties = new HashMap&lt;&gt;();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;127.0.0.1:9092&quot;); // 서버 주소
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, &quot;consumerGroupId&quot;); // 여러 가지 컨슈머가 있을 경우 groupId 로 구분
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        return new DefaultKafkaConsumerFactory&lt;&gt;(properties);
    }
    @Bean
    public ConcurrentKafkaListenerContainerFactory&lt;String,String&gt; kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory&lt;String,String&gt; kafkaListenerContainerFactory
                = new ConcurrentKafkaListenerContainerFactory&lt;&gt;();
        // 위에서 만든 Factory 등록
        kafkaListenerContainerFactory.setConsumerFactory(consumerFactory());
        return kafkaListenerContainerFactory;
    }
}</code></pre>
<ul>
<li>@EnableKafka 를 등록해주면서 Kafka 를 사용한다고 알려준다.</li>
<li>topic 에 접근하기 위한 정보들을 consumerFactory() 를 Bean 으로 등록</li>
<li>DESERIALIZER (역직렬화) -&gt; 받는 쪽이기 때문에 역직렬화 사용</li>
<li>그렇게 만든 consumerFactory() 를 KafkaListenerContainerFactory 에 넣어준다.<pre><code class="language-java">// KafkaConsumer (@Service)
</code></pre>
</li>
</ul>
<p>// 해당 토픽에 데이터가 전달되면 그 데이터가 값을 가지고 와서 updateQty 가 실행된다.
    @KafkaListener(topics = &quot;example-catalog-topic&quot;)
    public void updateQty(String kafkaMessage) {
        log.info(&quot;Kafka Message: -&gt;&quot; + kafkaMessage);</p>
<pre><code>    Map&lt;Object , Object&gt; map = new HashMap&lt;&gt;();
    ObjectMapper mapper = new ObjectMapper();
    // Kafka Message 는 String 형태로 들어오지만 그것을 Json 으로 변경하기 위한 로직
    // 변경하다가 예외가 발생할 수 있기 때문에 try catch 로 잡아준다.
    try {
        map = mapper.readValue(kafkaMessage, new TypeReference&lt;Map&lt;Object, Object&gt;&gt;() {});
    } catch (JsonProcessingException ex) {
        ex.printStackTrace();
    }

    CatalogEntity entity = repository.findByProductId((String) map.get(&quot;productId&quot;));
    // Null 체크를 하는것도 중요하다.
    // if (entity != null) ...

    if (entity != null) {
        entity.setStock(entity.getStock() - (Integer) map.get(&quot;qty&quot;));
        repository.save(entity);
    }
}</code></pre><pre><code>- topic 을 example-catalog-topic 으로 할 예정이기 때문에 @KafkaListener 인자에 넣어준다.
- KafkaMessage 는 String 으로 넘어오기 때문에 해당 값을 JSON 으로 바꾸고 그 map 객체에 담는다.
- map 객체에서 productId , qty 를 가지고 데이터 동기화를 해주면 된다.

## Order Microservice 수정
- 똑같이 먼저 spring-kafka 의존성 추가
```java
// Kafka 사용
@EnableKafka
@Configuration
public class KafkaProducerConfig {

    @Bean
    public ProducerFactory&lt;String,String&gt; producerFactory() {
        Map&lt;String,Object&gt; properties = new HashMap&lt;&gt;();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;127.0.0.1:9092&quot;); // 서버 주소
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        return new DefaultKafkaProducerFactory&lt;&gt;(properties);
    }

    @Bean
    public KafkaTemplate&lt;String , String&gt; kafkaTemplate() {
        return new KafkaTemplate&lt;&gt;(producerFactory());
    }
}</code></pre><ul>
<li>SERIALIZER (직렬화) -&gt; 먼저 보내는 쪽이기 때문에 직렬화</li>
<li>KafkaTemplate 등록할 때 위에서 만든 Factory 전달</li>
<li><blockquote>
<p>topic 에 접속할 수 있는 정보가 필요하기 떄문</p>
</blockquote>
<pre><code class="language-java">// KafkaProducer.class
</code></pre>
</li>
</ul>
<p>// topic 에 order 의 정보를 보내는 method
    public OrderDto send(String topic , OrderDto orderDto) {
        ObjectMapper mapper = new ObjectMapper();
        String jsonInString = &quot;&quot;;
        try {
            // json format 으로 변경
            jsonInString = mapper.writeValueAsString(orderDto);
        } catch (JsonProcessingException ex) {
            ex.printStackTrace();
        }</p>
<pre><code>    kafkaTemplate.send(topic , jsonInString);
    log.info(&quot;kafka Producer sent data from the Order microservice : &quot; + orderDto);
    return orderDto;
}</code></pre><pre><code>- kafkaTemplate.send() 를 통해 보내면 된다.

## Kafka 를 활용한 데이터 동기화 테스트 1
1. 주키퍼 서버 기동
2. 카프카 서버 기동
3. 유레카 서버 기동
4. config-service 기동
5. apigateway-service 기동
6. order-serivce 기동
7. caltalog-service 기동

모든 서버 및 서비스를 기동한 후 POSTMAN 으로 order-service 에서 order 를 만든다.
- 그런후에 catalog-service 에서 h2 DB 에 들어가보면 order 한 수량 만큼 감소한것을 확인했다.
- 데이터 동기화가 되는 것이다.

만약 orderService 가 1개가 아니라 여러개의 orderService 를 기동하는 경우에는 어떻게 Kafka 를 사용해야하는지 알아보자.

## Multi Order Microservice 사용에 대한 데이터 동기화 문제
- order-service 를 2개 기동
- User 의 요청 분산 처리
- Order 데이터도 분산 저장 -&gt; 동기화 문제

order-service 를 2개 기동 (A,B)
- order-service 에서 5번 order 를 한다고 가정해보자.
- 그렇다면 A-service 로 3번 , B-service 로 2번의 order 데이터가 이동한다.
- 이 이유는 전에 배운 내용이다.

이때 더 큰 문제는 user-service 에서 user-service/users/{uesrId} 를 요청하면 한 번은 A-service 로 가서 3개의 order 데이터를 받고 그 다음은 B-service 로 가서 2개의 order 데이터를 받게 된다.
- 이러한 문제를 해결하기 위해서는 Kafka Connect 를 활용하여 단일 데이터베이스를 사용해야한다.

## Kafka Connect 를 활용한 단일 데이터베이스를 사용
- OrderService 에 요청된 주문 정보를 DB 가 아니라 Kafka Topic 으로 전송
- Kafka Topic 에 설정된 Kafka Sink Connect 를 사용해 단일 DB에 저장 -&gt; 데이터동기화

order-service 의 JPA 데이터베이스 교체
- H2 -&gt; MariaDB 로 바꾸기
- 바꾼 후 orders table 을 생성해야한다.

## Orders Microservice 수정 (DB 변경)
```yml
spring:
  jpa:
    hibernate:
      ddl-auto: update
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: </code></pre><ul>
<li>order-service.application.yml 에서 위와 같이 변경하면 Mysql 로 변경할 수 있다.</li>
<li>mydb 에 접속해서 create table orders 로 orders 라는 테이블도 만들었다.</li>
</ul>
<h2 id="orders-microservice-수정-orders-kafka-topic">Orders Microservice 수정 (Orders Kafka Topic)</h2>
<ul>
<li>아래에 있는 order-service 의 Controller 에서 createOrder() 부분을 수정해야한다.<pre><code class="language-java">// order-service.controller.createOrder() 일부분
OrderDto createdOrder = orderService.createOrder(orderDto);
ResponseOrder responseOrder = modelMapper.map(createdOrder , ResponseOrder.class);</code></pre>
위에 코드는 Order 객체를 만들고 DB 에 저장을 한 다음에 ResponseOrder 로 반환시키는 부분이다.</li>
</ul>
<p>이부분을 Kafka 에다가 메시지를 보내도록 바꾼다.</p>
<ul>
<li>orderProducer 를 구현하고 orderProducer.send() 를 사용하면 된다.</li>
</ul>
<p>우리가 가지고 있던 주문 정보를 어떻게 topic 에 보낼지가 중요하다.</p>
<ul>
<li>왜냐하면 topic 에 쌓였던 메시지들은 Sink Connector 에 의해서 불려지고 그 Sink Connector 가 토픽에 있었던 메시지의 내용들을 열어보고 어떻게 저장되어 있는지 파악을 gkrh JDBC Connector 에 저장한다.</li>
<li>이때 만약 일정한 format 대로 저장되어 있지 않으면 정상적으로 전달되지 않는다.
<img src="https://velog.velcdn.com/images/support-kim/post/a71882f0-dfdd-4863-b953-d5da381674b5/image.png" alt=""></li>
</ul>
<p>그래서 topic 에 보내기 위해서는 위와 같은 format 을 맞춰야한다.</p>
<ul>
<li>Schema : type , fields , optional , name</li>
<li>fields : type , optional , field</li>
<li>payload : order_id , user_id , product_id , qty , total_price , unit_price </li>
<li>Schema , Payload 라는 클래스를 만들고 KafkaOrderDto 라는 곳에 필드로 선언하면 된다.</li>
</ul>
<p>그런후 OrderProducer 를 생성한다. (@Service)</p>
<ul>
<li>여기에 위에서 만든 Schema , List&lt; Field&gt; fields 를 필드에 넣는다.</li>
<li>kafkaTemplate 을 사용해서 topic 에 전달하고 싶은 내용을 보낼 수 있다.</li>
<li>그런후 order-service 에서 Payload 는 OrderDto 로 값을 채워서 만들고 schema 는 OrderProcuder 에서 초기화할 때 넣은 값들을 사용한다.</li>
<li>그렇게 KafkaOrderDto 에 넣고 Dto 를 생성한 후 mapper.wirteValueAsString 으로 객체를 JSON 으로 만든다.</li>
<li>똑같이 kafkaTemplate.send() 로 보낼 수 있다.</li>
</ul>
<p>order-serivce 를 위해서 kafka sink connector 를 기동하고 order-service 를 위한 connector 를 추가해야한다.</p>
<ul>
<li>즉 order-service 들을 데이터를 kafka topic 에 던지고 topic 에 있는 메시지를 sink connector 가 가지고 와서 단일 데이터베이스에 저장을 시켜줄 수 있다.</li>
<li>topics 에 orders 로 해야하는데 이게 table 이름이다.</li>
</ul>
<h2 id="order-microservice-수정-order-kafka-producer">Order Microservice 수정 (Order Kafka Producer)</h2>
<ul>
<li><p>Schema , Field , Payload 클래스를 만들고 format 에 맞는 필드를 선언한다.</p>
<pre><code class="language-java">@Data
@AllArgsConstructor
public class KafkaOrderDto implements Serializable {
  private Schema schema;
  private Payload payload;
}</code></pre>
</li>
<li><p>KafkaOrderDto 가 Schema , Payload 를 가지고 있다.</p>
</li>
<li><p>Schema 안에 List 형태의 Fields 를 가진다.</p>
<pre><code class="language-java">@Service
@Slf4j
public class KafkaOrderProducer {

  private KafkaTemplate&lt;String,String&gt; kafkaTemplate;

  List&lt;Field&gt; fields = Arrays.asList(
          new Field(&quot;string&quot;,true,&quot;order_id&quot;),
          new Field(&quot;string&quot;,true,&quot;user_id&quot;),
          new Field(&quot;string&quot;,true,&quot;product_id&quot;),
          new Field(&quot;int32&quot;,true,&quot;qty&quot;),
          new Field(&quot;int32&quot;,true,&quot;unit_price&quot;),
          new Field(&quot;int32&quot;,true,&quot;total_price&quot;)
  );

  Schema schema = Schema.builder()
          .type(&quot;struct&quot;)
          .fields(fields)
          .optional(false)
          .name(&quot;orders&quot;)
          .build();

  // topic 에 order 의 정보를 보내는 method
  public OrderDto send(String topic , OrderDto orderDto) {

      Payload payload = Payload.builder()
              .order_id(orderDto.getOrderId())
              .product_id(orderDto.getProductId())
              .qty(orderDto.getQty())
              .total_price(orderDto.getTotalPrice())
              .user_id(orderDto.getUserId())
              .unit_price(orderDto.getUnitPrice())
              .build();

      KafkaOrderDto kafkaOrderDto = new KafkaOrderDto(schema,payload);
      // Schema , Payload 가 담긴 메시지를 만들어야 한다.
      ObjectMapper mapper = new ObjectMapper();
      String jsonInString = &quot;&quot;;
      try {
          // json format 으로 변경
          jsonInString = mapper.writeValueAsString(kafkaOrderDto);
      } catch (JsonProcessingException ex) {
          ex.printStackTrace();
      }

      kafkaTemplate.send(topic , jsonInString);
      return orderDto;
  }
}</code></pre>
</li>
<li><p>KafkaOrderProducer 에서 fields 와 Schema 에 대한 정보를 초기화를 하고 send() 에서 Payload 값을 OrderDto 를 통해서 채운다.</p>
</li>
<li><p>schema , payload 를 통해 KafkaOrderDto 를 만들고 그 Dto 를 JSON 으로 바꿔서 kafkaTemplate.send() 에 담아서 보내면 된다.</p>
</li>
<li><p>KafkaOrderDto 는 해당 코드 위에 있는 클래스이다.</p>
</li>
<li><p>이렇게 우리는 orders 라는 topic 에 JSON 형태로 데이터를 던진 상황이고 이때 Sink Connector 를 하나 등록하게 되면 topic 에 있는 데이터를 읽어와서 단일 DB 에 저장하면 된다.</p>
</li>
</ul>
<h3 id="sink-connector-등록">Sink Connector 등록</h3>
<pre><code class="language-json">{
    &quot;name&quot;:&quot;my-order-sink-connector&quot;,
    &quot;config&quot;:{
        &quot;connector.class&quot;:&quot;io.confluent.connect.jdbc.JdbcSinkConnector&quot;,
        &quot;connection.url&quot;:&quot;jdbc:mysql://localhost:3306/mydb&quot;,
        &quot;connection.user&quot;:&quot;root&quot;,
        &quot;connection.password&quot;:
        &quot;auto.create&quot;:&quot;true&quot;,
        &quot;auto.evolve&quot;:&quot;true&quot;,
        &quot;delete.enabled&quot;:&quot;false&quot;,
        &quot;tasks.max&quot;:&quot;1&quot;,
        &quot;topics&quot;:&quot;orders&quot;
    }
}</code></pre>
<ul>
<li>name 은 my-order-sink-connector 로 생성</li>
<li>topics 에 orders 로 지정</li>
<li>127.0.0.1:8083/connectors POST 방식으로 넘기면 된다.</li>
<li>127.0.0.1:8083/connectors GET 방식으로 생성된 Connector 를 확인할 수 있다.</li>
</ul>
<h2 id="kafka-를-활용한-데이터-동기화-테스트-2">Kafka 를 활용한 데이터 동기화 테스트 2</h2>
<p> 이제 다시 order-service 를 2개 기동해본다.</p>
<ul>
<li>다시 order-service/{userId}/orders endpoint 로 주문을 요청한다.</li>
<li>kafka-console-consumer 를 기동하면 어떠한 값들이 들어왔는지 확인할 수 있다.</li>
<li>1번째 , 2번째 주문은 order-serviceA 에서 실행 </li>
<li>3번쨰 주문은 order-serviceB 에서 실행 </li>
<li>하나의 DB 에 값이 저장되는 것을 확인</li>
</ul>
<p>POSTMAN 에서 /order 를 실행해보면 order-service 2개를 번갈아가면서 호출하는 것을 확인했다.</p>
<ul>
<li>하지만 Mysql(MariaDB) 에 들어가서 select * from orders 쿼리를 실행해보면 모든 order 가 나오는 것을 알 수 있다.</li>
<li>그런후 user-service/user/{userId} GET 으로 해당 user 의 주문 내역을 가지고 와보면 모든 주문이 다 들어와있는 것을 알 수 있다.</li>
<li>단일 DB 를 사용하기 때문에 모든 주문을 확인할 수 있다.</li>
</ul>
<p><a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4/dashboard">참고자료</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Cloud + MSA 애플리케이션 개발 10(데이터 동기화를 위한 Kafka 1)]]></title>
            <link>https://velog.io/@support-kim/Spring-Cloud-MSA-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-10%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%8F%99%EA%B8%B0%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-Kafka-1</link>
            <guid>https://velog.io/@support-kim/Spring-Cloud-MSA-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-10%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%8F%99%EA%B8%B0%ED%99%94%EB%A5%BC-%EC%9C%84%ED%95%9C-Kafka-1</guid>
            <pubDate>Wed, 28 Feb 2024 09:08:39 GMT</pubDate>
            <description><![CDATA[<h2 id="apache-kafka">Apache Kafka</h2>
<ul>
<li>링크드인에서 Kafka 를 개발하던 엔지니어들이 Kafka 개발에 집중하기 위해 Confluent 라는 회사 창립</li>
<li>실시간 데이터 피드를 관리하기 위해 통일된 높은 처리량 , 낮은 지연 시간을 지닌 플랫폼 제공</li>
<li>Apple , Netflix , kakao , Uber 등 다양한 회사들에서 사용</li>
</ul>
<h3 id="기존-end-toend-연결-방식의-아키텍처">기존 End-toEnd 연결 방식의 아키텍처</h3>
<p><img src="https://velog.velcdn.com/images/support-kim/post/338b058e-2fac-4cb2-8a5f-e1a302b33dba/image.png" alt=""></p>
<ul>
<li>데이터 연동의 복잡성 증가</li>
<li>서로 다른 데이터 Pipeline 연결 주고</li>
<li>확장이 어려운 구조</li>
</ul>
<h3 id="kafka-적용-효과">Kafka 적용 효과</h3>
<ul>
<li>모든 시스템으로 데이터를 실시간으로 전송하여 처리할 수 있는 시스템</li>
<li>데이터가 많아지더라도 확장이 용이한 시스템</li>
<li>Producer / Consumer 분리</li>
<li>메시지를 여러 Consumer 에게 허용</li>
<li>높은 처리량을 위한 메시지 최적화</li>
<li>Scale-out 가능</li>
</ul>
<p>단순히 데이터 전달 뿐만 아니라 데이터의 지연 시간을 낮춰주는 용도로 사용한다.</p>
<h3 id="kafka-broker">Kafka Broker</h3>
<p><img src="https://velog.velcdn.com/images/support-kim/post/d428b4e5-6543-458f-9f00-bb4e67e001d1/image.png" alt=""></p>
<ul>
<li>실행된 Kafka 애플리케이션 서버를 Kafka Broker 라고 한다.</li>
<li>3대 이상의 Broker Cluster 를 구성</li>
<li>Zookeeper 연동</li>
<li><blockquote>
<p>역할 : 메타데이터 (Broker ID , Controller ID 등) 저장</p>
</blockquote>
</li>
<li><blockquote>
<p>Controller 정보 저장</p>
</blockquote>
</li>
<li>n개 Broker 중 1대는 Controller 기능 수행</li>
<li><blockquote>
<p>Controller 는 각 Broker 에게 담당 파티션 할당을 수행하고, Broker 정상 동작 모니터링 관리를 한다.</p>
</blockquote>
</li>
</ul>
<h2 id="apache-kafka-설치">Apache Kafka 설치</h2>
<p>Apache Kafka 홈페이지에 들어가서 다운로드를 받으면 된다.</p>
<ul>
<li>OS 에 상관없이 같은 파일을 다운로드 받으면 된다.</li>
<li>받은 파일의 압축을 풀어보면 여기에 각 OS 마다 필요한 파일들이 존재한다.</li>
<li>bin-&gt;windows 안에는 window 에서 돌아가는 .bat 파일들이 존재한다.</li>
</ul>
<h2 id="producer--consumer">Producer / Consumer</h2>
<ul>
<li>다양한 방법이 있지만 가장 많이 사용하는 Kafka client 를 사용</li>
<li>A 서비스에서 B , C 서비스에 전달을 할 때 다이렉트로 end-to-end 방식이 아닌 중간에 카프카라는 시스템을 두고 전달을 한다.</li>
<li>topic 에 데이터를 보낸다고 생각하면 된다.</li>
</ul>
<h3 id="kafka-서버-기동">Kafka 서버 기동</h3>
<ul>
<li>Zookeeper 와 Kafka 서버 구동</li>
<li>/bin/zookeeper-server-start.sh /config/zookeeper.properties</li>
<li>/bin/kafka-server-start.sh /config/server.properties</li>
</ul>
<h3 id="topic-생성">Topic 생성</h3>
<ul>
<li>/bin/kafka-topics.sh --create --topic quickstart-events --bootstrap-server localhost:9092 --partitions 1</li>
</ul>
<h3 id="topic-목록-확인">Topic 목록 확인</h3>
<ul>
<li>/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list</li>
</ul>
<h3 id="topic-목록-확인-1">Topic 목록 확인</h3>
<ul>
<li>/bin/kafka-topics.sh --describe --topic quickstart-events --bootstrap-server localhost:9092</li>
</ul>
<h3 id="메시지-생산">메시지 생산</h3>
<ul>
<li>/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic quickstart-events</li>
</ul>
<h3 id="메시지-소비">메시지 소비</h3>
<ul>
<li>/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic quickstart-events --from-beginning
```</li>
</ul>
<ol>
<li><p>주키퍼 실행 (2181 포트)
./bin/zookeeper-server-start.sh ./config/zookeeper.properties</p>
</li>
<li><p>카프카 메인 서버 실행 (9092 포트)
./bin/kafka-server-start.sh ./config/server.properties</p>
</li>
</ol>
<p>토픽 확인
./bin/kafka-topics.sh --bootstrap-server localhost:9092 --list</p>
<ol start="3">
<li>토픽 생성 (토픽명 : quickstart-events)
./bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic quickstart-events --partitions 1</li>
</ol>
<p>토픽 확인 명령어 쳐보면 quickstart-events 토픽이 생성된 것을 확인</p>
<ol start="4">
<li><p>토픽 상세 정보 보기
./bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic quickstart-events</p>
</li>
<li><p>Producer 와 Consumer 역할을 해보자.</p>
</li>
</ol>
<p>Producer 등록
/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic quickstart-events</p>
<p>Consumer 등록
./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic quickstart-events --from-beginning</p>
<p>모든 메시지를 처음부터 얻기 위한 옵션 추가 (--from-beggining)</p>
<pre><code>![](https://velog.velcdn.com/images/support-kim/post/cb208c93-b3dd-44e3-b3e9-84ca659f13f7/image.png)
- 위와 같이 Producer 에서 메시지를 보내면 Consumer 로 메시지가 전달되는 것을 확인할 수 있다.

## Kafka Connect
- kafka Connect 를 통해 Data 를 import / Export 가능
- 코드 없이 Configuration 으로 데이터를 이동
- Standalone mode , Distribution mode 지원
-&gt; Restful API 통해 지원
-&gt; Stream 또는 Batch 형태로 데이터 전송 가능
-&gt; 커스텀 Connecotr 를 통한 다양한 Plugin 제공 (S3 , File , Mysql...)

### 흐름
SourceSystem -&gt; Kafka Connect Source -&gt; Kafka Cluster -&gt; Kafka Connect Sink -&gt; Target System(S3...)

## OrderService 에서 MariaDB 연동

터미널에서 유레카 서비스 폴더로 이동한다.
- mysql.server.start 로 확인
- mysql -uroot -p 실행하고 비밀번호 입력해서 MariaDB 로 이동
- create database DB명 으로 DB 를 만든다.

그런후 order-service 에 가서 Mysql 의존성을 추가한다.
- h2-console 에 접근하고 id , pw 를 입력해서 해당 DB 에 접속한다.
- users 라는 테이블을 만든다.

생성한 테이블에 Kafka Connect 를 이용해서 users 테이블에 데이터가 insert 되면 그 데이터를 감지했다가 새로운 데이터베이스에 옮기는 작업을 해보자.

## Kafka Connect 설치

### Kafka Connect 설치 방법</code></pre><p>curl -O <a href="http://packages.confluent.io/archive/5.5/confluent-community-5.5.2-2.12.tar.gz">http://packages.confluent.io/archive/5.5/confluent-community-5.5.2-2.12.tar.gz</a></p>
<p>curl -O <a href="http://packages.confluent.io/archive/6.1/confluent-community-6.1.0.tar.gz">http://packages.confluent.io/archive/6.1/confluent-community-6.1.0.tar.gz</a></p>
<ul>
<li>tar xvf confluent-community-6.1.0.tar.gz</li>
</ul>
<p>Kafka 폴더에서 명령어로 실행</p>
<p>Kafka Connect 설정 (기본으로 사용)
/config/connect-distributed.properties</p>
<p>Kafka Connect 실행
./bin/connect-distributed ./etc/kafka/connect-distributed.properties</p>
<p>Topic 목록 화인
./bin/kafka-topics.sh --bootstrap-server localhost:9092 --list</p>
<pre><code>
### JDBC Connector 설정
JAVA 에서 관계형 데이터 베이스를 사용하기 위해서 JDBC 라이브러리를 설치해야한다.
- 위에 예제를 해보기 위해서 필요한 작업
- 그래서 JDBC Connector 를 설치해야한다.
- [설치링크](https://www.confluent.io/hub/confluentinc/kafka-connect-jdbc)
- JDBC Conntor 와 Kafka Connector 를 연결하기 위해서는 plugin 정보를 추가해야한다.
- plugin.path = 위에서 설치한 JDBC Connector 폴더
- JdbcSoucrceConnector 에서 MariaDB 를 사용하기 위해서 mariadb 드라이버 복사
- 위에서 의존성을 추가했기 떄문에 그 maven 폴더에서 mariadb.jar 파일을 가져올 수 있다.
- 그것을 /share/java/kafka 에 넣어주면 된다.

주키퍼 서버 + 카프 메인 서버를 모두 실행 시키고 위에 명령어에서 Kafka Connect 를 실행하면 된다.
- 물론 그 전에 Kafka Connect 를 다운받고 tar xvf 명령어로 압축까지 푼 상태여야한다.</code></pre><p>./bin/kafka-topics.sh --bootstrap-server localhost:9092 --list</p>
<p>실행결과
__consumer_offsets
connect-configs
connect-offsets
connect-status
quickstart-events</p>
<pre><code>- 해당 명령어로 topic 을 조회해본 결과 아까 위에서 만든 quickstart-events 말고 4개가 더 추가로 만들어진것을 확인할 수 있다.
- Kafka Connect 가 저장 및 관리하기 위해 만든 topic 이라고 생각하면 된다.

그런후에 설치한 kafka-connect-jdbc 파일에 가서 lib 폴더까지 들어간 후 그 경로를 복사한다.
- kafka connect 압축을 풀었던 confluent-6.1.0 에 들어가서 /etc/kafka/connect-distributed.properties 에서 plugin.path=아까 복사한 경로 의 값으로 바꿔주고 저장하면 된다.

마지막으로 m2/jdbc/mariadb-java-client/version/ 에 들어가서 mariadb-java-client-version.jar 파일을 /confluent-6.1.0/share/java/kafka 로 옮겨주면 된다.

이제 Kafka 를 사용하기 위한 사전 준비는 다 했고 Kafka Source Connect 테스트를 해보자.

## Kafka Source Connect 테스트

### 흐름
SourceSystem -&gt; Kafka Connect Source -&gt; Kafka Cluster -&gt; Kafka Connect Sink -&gt; Target System(S3...)
- 위에서 본 흐름인데, 우선 Kafka Connect Source 에 Database 를 가지도록 하고 그것을 Kafka Cluster (Kafka Topic) 에 저장을 한다.
- 이때 Kafka Topic 에 관심이 있다고 한 Sink 에 전달을 한다.
- 즉 Kafka Connect Sink 는 Target 한 System 에 데이터를 옮겨주는 역할이라고 생각하면 된다.

### Kafka Source Connect 추가 (MariaDB)

### 흐름
1. zookeeper 서버 실행 (kafka file)
2. kafka-main-server 실행 (kafka file)
3. kafka connect 실행 (confluent-6.1.0 file)
- ./bin/connect-distributed ./etc/kafka/connect-distributed.properties 명령어로 실행
```json
{
    &quot;name&quot; : &quot;my-source-connect&quot;,
    &quot;config&quot; : {
        &quot;connector.class&quot; : &quot;io.confluent.connect.jdbc.JdbcSourceConnector&quot;,
        &quot;connection.url&quot;:&quot;jdbc:mysql://localhost:3306/mydb&quot;,
        &quot;connection.user&quot;:&quot;root&quot;,
        &quot;connection.password&quot;:&quot;password&quot;,
        &quot;mode&quot;: &quot;incrementing&quot;,
        &quot;incrementing.column.name&quot; : &quot;id&quot;,
        &quot;table.whitelist&quot;:&quot;users&quot;,
        &quot;topic.prefix&quot; : &quot;my_topic_&quot;,
        &quot;tasks.max&quot; : &quot;1&quot;
    }
}</code></pre><ul>
<li>위에 있는 JOSN 코드를 넣고 127.0.0.1:8083/connectors POST 요청</li>
<li><blockquote>
<p>kafka connect 의 default port 가 8083</p>
</blockquote>
</li>
<li>JSON 에서 mode 는  데이터가 등록이 되면서 데이터가 자동으로 증가시키도록 하고 그 증가되는 컬럼은 id 라는 뜻이다.</li>
<li>whitelist 는 마리아DB 데이터베이스에 특정한 값을 저장을 할 것인데 (insert) 그 database 를 계속 보고 있다가 변경사항이 생긴다면 그 데이터 값을 가지고 와서 topic 에 저장을 한다.</li>
<li><blockquote>
<p>즉 whitelist 에 있는 table 의 값을 가져 온다는 것 (users table)
그 변경사항은 my_topic_users 의 prefix 를 가지는 곳에 넣는다.</p>
</blockquote>
</li>
</ul>
<p>테스트를 할 때 MariaDB 에서 데이터를 추가하면 consumer 를 실행했을 때 그 데이터베이스에서 추가한 내용이 출력이 되는지 확인한다.</p>
<p>그런 후 토픽에 잘 보내졌는가? 실제 소스가 만들어졌는가? 를 확인해보면 된다.</p>
<p>127.0.0.1:8083/connectors GET 으로 source 를 볼 수 있고
127.0.0.1:8083/connectors/source명/status GET 으로 요청하면 더 자세하게 볼 수 있다.</p>
<pre><code>./bin/kafka-topics.sh --bootstrap-server localhost:9092 --list </code></pre><ul>
<li>위에 명령어를 통해 topic 을 확인해보자. (kafka file 에서 명령어 실행)</li>
<li>확인해보면 아직 데이터에 변경 사항이 없어서 따로 topic 이 만들어지지 않았다.</li>
<li>데이터 변경 사항을 만들어서 topic 이 생기도록 해보자.</li>
</ul>
<pre><code class="language-query">insert into users(user_id , pwd , name) values(&#39;user1&#39; , &#39;test1111&#39; , &#39;Username&#39;) </code></pre>
<ul>
<li><p>위와 같은 쿼리를 날린 후 다시 위에 있는 topic 조회 명령어를 실행해보자.</p>
<pre><code>실행 결과
__consumer_offsets
connect-configs
connect-offsets
connect-status
my_topic_users
quickstart-events</code></pre></li>
<li><p>위처럼 my_topic_users 가 생겼다.</p>
<pre><code>./bin/kafka-console-consumer.sh --bootstrap-server 
localhost:9092 --topic my_topic_users --from-beginning

실행결과
{
  &quot;schema&quot;:{
      &quot;type&quot;:&quot;struct&quot;,
      &quot;fields&quot;:[
          {&quot;type&quot;:&quot;int32&quot;,&quot;optional&quot;:false,&quot;field&quot;:&quot;id&quot;},
          {&quot;type&quot;:&quot;string&quot;,&quot;optional&quot;:true,&quot;field&quot;:&quot;user_id&quot;},
          {&quot;type&quot;:&quot;string&quot;,&quot;optional&quot;:true,&quot;field&quot;:&quot;pwd&quot;},
          {&quot;type&quot;:&quot;string&quot;,&quot;optional&quot;:true,&quot;field&quot;:&quot;name&quot;},
          {&quot;type&quot;:&quot;int64&quot;,&quot;optional&quot;:true,&quot;name&quot;:&quot;org.apache.kafka.connect.data.Timestamp&quot;,&quot;version&quot;:1,
          &quot;field&quot;:&quot;created_at&quot;}],
          &quot;optional&quot;:false,
          &quot;name&quot;:&quot;users&quot;},
      &quot;payload&quot;:{
          &quot;id&quot;:1,
          &quot;user_id&quot;:&quot;user1&quot;,
          &quot;pwd&quot;:&quot;test1111&quot;,
          &quot;name&quot;:&quot;Username&quot;,
          &quot;created_at&quot;:1709140788000
          }
      }
}
</code></pre></li>
</ul>
<pre><code>- 위의 명령어로 my_topic_users 의 정보를 가져오면 실행 결과 처럼 나온다.
- MariaDB 에서 insert 쿼리를 보내면 consumer 에서 schema , payload 등 값들이 넘어오는 것을 확인할 수 있다.
- payload 는 실제 전달 되는 데이터라고 생각하면 된다.
- 나중에 Topic 을 이용해서 DB 에 저장하고 싶다면 위와 같은 포맷으로 전달하면 된다.

DB 에 직접 데이터를 전달하지 않고 Topic 에 데이터를 전달하기만 하면 그 값이 DB 에 넣을 수 있도록 해야한다.

위와 같은 작업을 해주는 것이 Sink Connect 이다.

## Kafka Sink Connect 사용
- topic 에다가 데이터가 쌓이게 되는데 이때 Sink Connect 가 topic 에 전달된 데이터를 가지고 와서 사용하는 것이라고 생각하면 된다.

Source Connect 에서 했던 것 처럼 JSON 형태로 요청을 보내는 형식으로 해본다.

MariaDB 에서 데이터를 추가 하면 my_Db.my_topic_users 에도 들어가는지 확인한다.

Kafka Producer 를 이용해서 Kafka Topic 에 데이터 직접 전송
- kafka-console-producer 에서 데이터 전송 -&gt; Topic 에 추가 -&gt; MariaDB 에 추가
</code></pre><p>{
    &quot;name&quot;:&quot;my-sink-connect&quot;,
    &quot;config&quot;:{
        &quot;connector.class&quot;:&quot;io.confluent.connect.jdbc.JdbcSinkConnector&quot;,
        &quot;connection.url&quot;:&quot;jdbc:mysql://localhost:3306/mydb&quot;,
        &quot;connection.user&quot;:&quot;root&quot;,
        &quot;connection.password&quot;:&quot;test1357&quot;,
        &quot;auto.create&quot;:&quot;true&quot;,
        &quot;auto.evolve&quot;:&quot;true&quot;,
        &quot;delete.enabled&quot;:&quot;false&quot;,
        &quot;tasks.max&quot;:&quot;1&quot;,
        &quot;topics&quot;:&quot;my_topic_users&quot;
    }
}</p>
<pre><code>- Source Connect 와 같이 127.0.0.1:8083/connectors POST 로 전달
- Sink Connect 와 연결할 곳은 topics 에 입력하면 된다.
- auto.create 는 topic 와 같은 table 을 생성하겠다는 뜻이다.
- 127.0.0.1:8083/connectors GET 으로 요청해보면 위에서 등록한 source 와 지금 등록한 sink 가 등록된 것을 확인할 수 있다.

sink 가 정상적으로 만들어졌다는 것은 지금 우리가 가지고 있는 데이터베이스에 topic 의 이름과 같은 형태의 테이블이 생성되었다는 것을 확인한다.
- 실제로 show tables; 를 하면 원래 있었던 users 말고 my_topic_users 도 생성된 것을 확인할 수 있었다.
- 실제로 MariaDB 에서 insert 쿼리를 날리면 my_topic_users 테이블에도 데이터가 들어가는 것을 확인할 수 있다.

즉 지금까지 흐름을 보면 MariaDB 에다가 insert 쿼리를 보내면 topic 에 데이터가 전달이 되고 이 topic 에서 다른 Target Table 에 들어가는 것을 확인한 것이다.

이번에는 Producer 에서 직접 콘솔로 데이터를 넣어보자. </code></pre><p>(kafka file)
./bin/kafka-console-producer.sh --broker-list localhost:9092 --topic my_topic_users</p>
<p>```</p>
<ul>
<li>해당 명령어를 실행하면 프롬프트가 변경되는데 여기에 직접 위에서 말한 format 대로 넣어서 데이터를 넣으면 된다.</li>
<li>schema , payload 의 포맷을 맞춰서 보내면 된다.</li>
<li>실제로 넣고 실행한뒤 MariaDB 에서 users 의 테이블을 조회하면 방금 넣었던 데이터가 안 들어가있다.</li>
<li>그 이유는 users 는 topic 으로부터 데이터를 가져오는 것이 아니라 자신이 가지고 있는 데이터를 topic 에 넣는 것이기 때문에 Producer 가 넣었던 데이터가 없다.</li>
<li>하지만 my_topic_users 테이블에는 데이터가 들어간 것을 확인할 수 있다.</li>
<li>Sink Connect 만으로도 Target Source 에 반영되게도 할 수 있다.</li>
</ul>
<p>다음에는 이렇게 배운 Source Connect + Sink Connect 를 이용해서 전에 만들었던 order-service , catalog-service 에도 적용해보자.</p>
<ul>
<li>데이터 동기화 용도</li>
<li>단일 데이터베이스에 여러가지 인스턴스로부터 넘겨받은 데이터를 정리하는 용도</li>
<li>2가지 용도로 사용할 수 있다.</li>
</ul>
<p><a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4/dashboard">참고자료</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Cloud + MSA 애플리케이션 개발 9(Microservice간 통신)]]></title>
            <link>https://velog.io/@support-kim/Spring-Cloud-MSA-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-9Microservice%EA%B0%84-%ED%86%B5%EC%8B%A0</link>
            <guid>https://velog.io/@support-kim/Spring-Cloud-MSA-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-9Microservice%EA%B0%84-%ED%86%B5%EC%8B%A0</guid>
            <pubDate>Tue, 27 Feb 2024 11:13:18 GMT</pubDate>
            <description><![CDATA[<h2 id="communication-types">Communication types</h2>
<ul>
<li>Synchronous HTTP communication</li>
<li><blockquote>
<p>동기 방식 : 요청이 들어오면 이 요청이 끝날때까지는 다른 클라이언트의 요청은 받지 않는다.</p>
</blockquote>
</li>
<li>Asynchronous coomunicatino over AMQP</li>
<li><blockquote>
<p>비동기 방식 : 순차적으로 하는게 아니라 연결되어 있는 모든 클라이언트에게 한 번에 처리할 수 있다.</p>
</blockquote>
</li>
</ul>
<p>user-service 와 order-serivce 가 서로 통신하려면 어떻게 해야할까?</p>
<ul>
<li>user-service 에서 유레카 서버를 통해 order-server 의 위치를 알고 찾은 위치에 요청을 보내면 된다.</li>
</ul>
<p>하지만 Rest Template 을 사용하면 유레카 서버를 통하지 않고 한 번에 통신할 수 있다.</p>
<ul>
<li>클라이언트에서 user-service/user/{userId} 로 요청이 들어왔고, userId 를 가지고 고객의 주문 내역도 보여줄 수 있다.</li>
<li>이때 Rest Template 을 사용하면 넘어온 userId 를 가지고 order-service/{userId}/orders 로 한 번에 통신할 수 있게 해준다.</li>
</ul>
<h2 id="resttemplate-사용">RestTemplate 사용</h2>
<ul>
<li>RestTemplate 를 Bean 으로 등록해서 사용하면 된다.</li>
<li>해당 예제에서 사용할 시나리오는 user-service 에서 회원 정보를 찾을 때 order-service 에서 그 회원의 주문 내역까지 담아서 반환하려고 하는 것이다.</li>
<li>그렇다면 user-service 에서 restTemplate 을 사용해서 order-service 를 호출하면 된다.</li>
<li>restTemplate.exchange() 를 사용하고 파라미터로 주소 , 요청 방식 , requestType , dataType 양식의 정보들을 넘겨주면 된다.</li>
</ul>
<pre><code class="language-java">// UserServiceApplication.class
    // Bean 으로 등록하기 때문에 다른 곳에서 주입 받아 사용할 수 있다.
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }

// UserServiceImpl.class
    @Override
    public UserDto getUserById(String userId) {
        UserEntity userEntity = userRepository.findByUserId(userId);

        if (userEntity==null) {
            throw new UsernameNotFoundException(&quot;User not found&quot;);
        }

        UserDto userDto = new ModelMapper().map(userEntity , UserDto.class);

        List&lt;ResponseOrder&gt; orderList = new ArrayList&lt;&gt;();
        userDto.setOrders(orderList);
        return userDto;
    }

// order-service
    @GetMapping(&quot;/{userId}/orders&quot;)
    public ResponseEntity&lt;List&lt;ResponseOrder&gt;&gt; getOrder(@PathVariable(&quot;userId&quot;) String userId) {
        ModelMapper modelMapper = new ModelMapper();
        Iterable&lt;OrderEntity&gt; orderList = orderService.getOrdersByUserId(userId);

        List&lt;ResponseOrder&gt; result = new ArrayList&lt;&gt;();
        orderList.forEach(v-&gt; {
            result.add(new ModelMapper().map(v,ResponseOrder.class));
        });

        return ResponseEntity.status(HttpStatus.OK).body(result);
    }</code></pre>
<ul>
<li><p>원래는 user-service 에서 주문 내역은 비어있는 List 로만 넘겨줬는데 이제 order-service 와 직접 통신을 해서 주문 내역을 가져와서 넘겨주도록 해보자.</p>
</li>
<li><p>그렇게 하려면 Bean 으로 등록했던 RestTemplate 을 사용하면 된다.</p>
<pre><code class="language-java">  @Override
  public UserDto getUserById(String userId) {
      UserEntity userEntity = userRepository.findByUserId(userId);

      if (userEntity==null) {
          throw new UsernameNotFoundException(&quot;User not found&quot;);
      }

      UserDto userDto = new ModelMapper().map(userEntity , UserDto.class);

      // restTemplate 을 하는 첫 번째 방법
      String orderUrl = &quot;http://127.0.0.1:8000/order-service/%s/orders&quot;;
      ResponseEntity&lt;List&lt;ResponseOrder&gt;&gt; orderListResponse = restTemplate.exchange(orderUrl, HttpMethod.GET, null,
              new ParameterizedTypeReference&lt;List&lt;ResponseOrder&gt;&gt;() {
              });
      List&lt;ResponseOrder&gt; orderList = orderListResponse.getBody();
      userDto.setOrders(orderList);
      return userDto;
  }</code></pre>
</li>
<li><p>orderUrl 에 요청할 endpoint 를 저장하고 restTemplate.exchange() 로 통신하면 된다.</p>
</li>
<li><p>넘겨야할 파라미터 : 요청URL , 요청 방식 , 요청 파라미터 (없으면 null) , 전달 받을 때 어떤 형식으로 전달 받을지 설정</p>
</li>
</ul>
<p>위와 같은 방식으로 service 끼리 통신하면 된다.</p>
<p>현재 orderURL 을 하드코딩을 해놨는데 만약 포트 번호가 바뀌거나 endpoint 가 변경될 경우를 고려해보면 좋은 방식은 아니다.</p>
<ul>
<li>그래서 해당 orderURL 을 config-server 에서 관리하는 파일안에 user-service.yml 에다가 orderURL 을 관리하면 된다.<pre><code class="language-yml"># user-service.yml
order_service:
url: http://127.0.0.1:8000/order-service/%s/orders</code></pre>
<pre><code class="language-java">// UserServiceImpl.class
      String orderUrl = String.format(env.getProperty(&quot;order_service.url&quot;), userId);
      ResponseEntity&lt;List&lt;ResponseOrder&gt;&gt; orderListResponse = restTemplate.exchange(orderUrl, HttpMethod.GET, null,
              new ParameterizedTypeReference&lt;List&lt;ResponseOrder&gt;&gt;() {
              });</code></pre>
</li>
<li>yml 파일에 url 주소를 넣고 사용할 때 Enviroment 를 사용해서 해당 값을 불러오면 된다.</li>
<li>env.getProperty() 를 가져오고 %s 로 되어 있는 값을 userId 로 넣어주면 똑같은 코드가 된다.</li>
</ul>
<p>이때 yml 파일에 127.0.0.1:8000 이 아닌 유레카 서버에 등록한 이름대로 ORDER-SERVICE/order-service 이런식으로 요청하는게 더 좋은 설계이다.</p>
<ul>
<li>위와 똑같은 이유로 포트 번호가 바뀌는 경우에 하드 코딩을 하면 직접 바꿔야하지만 저런식으로 요청하게 되면 변경할 필요가 없다.<pre><code class="language-java">  @Bean
  // @LoadBalanced 를 붙혀주면 /user-service/ 로 접근할 수 있다.
  @LoadBalanced
  public RestTemplate getRestTemplate() {
      return new RestTemplate();
  }</code></pre>
</li>
<li>RestTemplate 을 Bean 으로 등록할 때 @LoadBalanced 를 붙혀주면 된다.<pre><code class="language-yml">order_service:
url: http://ORDER-SERVICE/order-service/%s/orders</code></pre>
</li>
<li>그런 후 user-service.yml 에서 ORDER-SERVICE 로 바꿔주면 된다.</li>
<li>현재 값을 변경했기 때문에 전에 배웠던 spring clooud bus 를 이용하면 한 번에 모든 클라이언트에게 변경된 값을 반영할 수 있다.</li>
<li>그래서 /busrefresh endpoint 를 요청하면 된다.</li>
</ul>
<p>똑같이 동작하지만 변경을 해야할 때 좀 더 편해지는 코드를 작성해봤다.</p>
<h2 id="feignclient-사용">FeignClient 사용</h2>
<ul>
<li>FeignClient -&gt; HTTP Client</li>
<li><blockquote>
<p>REST Call 을 추상화 한 Spring Cloud Netflix 라이브러리</p>
</blockquote>
</li>
<li>사용방법</li>
<li><blockquote>
<p>호출하려는 HTTP Endpoint 에 대한 Interface 를 생성</p>
</blockquote>
</li>
<li><blockquote>
<p>@FeignClient 선언</p>
</blockquote>
</li>
<li>Load balanced 지원</li>
<li>RestTemplate 보다 간단하고 직관적이다.</li>
</ul>
<h3 id="feign-client-적용">Feign Client 적용</h3>
<ul>
<li>Spring Cloud Netflix 라이브러리 추가</li>
<li>@Feign Client Interface 생성</li>
<li>UserServiceImpl.class 에서 Feign Client 사용</li>
</ul>
<p>먼저 user-service 에서 아래에 코드로 라이브러리를 추가한다.</p>
<pre><code class="language-xml">&lt;dependency&gt;
            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-openfeign&lt;/artifactId&gt;
&lt;/dependency&gt;</code></pre>
<pre><code class="language-java">@EnableFeignClients
public class UserServiceApplication { ... }

@FeignClient(name=&quot;order-service&quot;) // microservice 이름 넣기
public interface OrderServiceClient {

    @GetMapping(&quot;/order-service/{userId}/orders&quot;)
    List&lt;ResponseOrder&gt; getOrders(@PathVariable String userId);
}

// UserServiceImpl.class
    @Override
    public UserDto getUserById(String userId) {
        UserEntity userEntity = userRepository.findByUserId(userId);

        if (userEntity==null) {
            throw new UsernameNotFoundException(&quot;User not found&quot;);
        }

        UserDto userDto = new ModelMapper().map(userEntity , UserDto.class);

        /**
         * using feign client
         */
        List&lt;ResponseOrder&gt; orderList = orderServiceClient.getOrders(userId);
        userDto.setOrders(orderList);
        return userDto;
    }</code></pre>
<ul>
<li>먼저 UserServiceApplication 클래스레벨에 @EnableFeignClients 를 추가해준다.</li>
<li>OrdrServiceClient 인터페이스를 만들고 @FeignClient 를 붙혀주고 name 에는 호출할 microservice 이름을 넣어주면 된다.</li>
<li>그런후 메서드를 선언하고 @GetMapping() 에는 order-service 에서 호출할 URL 을 넣어주면 된다.</li>
<li>UserServiceImpl 에서 사용할 때는 OrderServiceClient 를 DI 받고 거기서 선언한 메서드를 호출하면 된다.</li>
</ul>
<h2 id="feignclient-예외-처리">FeignClient 예외 처리</h2>
<ul>
<li>FeignClient 사용시 발생한 로그 추적</li>
<li>yml 파일에서 logging.level 을 설정</li>
<li>UserServiceApplication.class 에서 Logger.Level 를 Bean 으로 등록</li>
<li>이렇게 설정하면 반환값이나 요청에 대한 정보들이 로그에 출력된다.</li>
</ul>
<h3 id="feignexception">FeignException</h3>
<ul>
<li>만약 user-service 에서 order-service 를 호출하려고 하다가 잘못된 endpoint 를 호출했다고 가정해보자.</li>
<li>FeignException 예외가 터져서 500 Internal Server Error 가 발생한다.</li>
<li><blockquote>
<p>물론 현재는 서버의 잘못보다는 클라이언트가 잘못 호출했기 때문에 404 에러가 더 적합하다.</p>
</blockquote>
</li>
<li>이런 Excpeiton 을 처리하기 위해서는 try catch 를 사용하면 된다.</li>
<li>user-service 에서 order-service 를 호출에 문제가 생겼지만 일단 user 의 정보는 반환시키면 좋을것이다.</li>
<li>order 의 정보는 못 가져오더라도 사용자의 정보는 문제가 없기 떄문에 사용자의 정보만이라도 반환하도록 해보자.</li>
</ul>
<pre><code class="language-yml"># application.yml
logging:
  level:
    com.example.userservice.client: DEBUG</code></pre>
<ul>
<li>application.yml 에서 로그 출력할 범위를 정한다.</li>
<li>현재 OrderServiceClient 가 있는 패키지를 선택<pre><code class="language-java">  @Bean
  public Logger.Level feignLoggerLevel() {
      return Logger.Level.FULL;
  }</code></pre>
</li>
<li>Logger.Level 을 Bean 으로 등록</li>
</ul>
<p>OrderServiceClient 에서 @GetMapping 에 있는 Endpoing 를 이상한 문자를 넣는다.</p>
<ul>
<li>order-service 에는 해당 endpoint 가 없기 때문에 오류가 발생한다.</li>
<li>실제로 호출을 해보면 FeignException 예외가 발생한다.</li>
<li>호출을 해볼때 위에서 로그 출력을 추가했기 떄문에 각 정보들이 로그에 출력되는 것을 확인할 수 있다.</li>
</ul>
<pre><code>List&lt;ResponseOrder&gt; orderList = null;
        try {
            orderList = orderServiceClient.getOrders(userId);
        } catch (FeignException ex) {
            log.error(ex.getMessage());
        }</code></pre><ul>
<li>위와 같이 try catch 를 사용해서 예외를 잡으면 된다.</li>
</ul>
<h2 id="errordecoder-를-이용한-예외처리">ErrorDecoder 를 이용한 예외처리</h2>
<ul>
<li><p>FeignErrorDecoder 클래스를 만들고 ErrorDecoder 를 implements 한다.</p>
</li>
<li><p>decode 라는 메서드를 구현하면 된다.</p>
</li>
<li><p>decode 는 FeignClient 에서 발생한 에러가 어떠한 에러가 발생헀는지 상태코드에 따라서 작업할 수 있도록 한다.</p>
</li>
<li><p>이렇게 만든 ErrorDecoder 를 Bean 으로 등록한다.</p>
</li>
<li><p>해당 방법을 사용하면 try catch 구문이 필요없어진다.</p>
<pre><code class="language-java">@Component
public class FeignErrorDecoder implements ErrorDecoder {

  private Environment env;

  public FeignErrorDecoder(Environment env) {
      this.env = env;
  }

  @Override
  public Exception decode(String methodKey, Response response) {
      switch (response.status()) {
          case 400:
              break;
          case 404:
              if (methodKey.contains(&quot;getOrders&quot;)) {
                  // 404 에러로 번환하고 메시지 담아서 새로운 예외 객체 생성
                  return new ResponseStatusException(HttpStatus.valueOf(response.status()),
                          &quot;User&#39;s order is empty&quot;);
                  // env.getProperty(&quot;order_service.exception.orders_is_empty);
              }
          default:
              // 예외가 발생했던 원인에 대해서 출력
              return new Exception(response.reason());
      }
      return null;
  }
}
</code></pre>
</li>
</ul>
<p>// UserServiceImpl.class
// ErrorDecode 는 따로 주입 받지 않아도 된다.
        List<ResponseOrder> orderList = orderServiceClient.getOrders(userId);</p>
<p>```</p>
<ul>
<li>먼저 ErrorDecoder 를 implements 하고 decode 를 구현하면 된다.</li>
<li>switch case 을 사용하고 404 에러이면서 예외가 발생한 method 이름에 getOrders 를 포함하고 있다면 새로운 예외 객체를 만들어서 반환한다.</li>
<li>사용할 때는 따로 주입받고 사용하는 것이 아니라 이미 @Component 으로 Bean 으로 등록했기 때문에 자동으로 예외가 발생하면 로직이 수행된다.</li>
<li>ResponseStatusException 객체를 만들 때 message 에 하드코딩 하는 것이 아니라 설정 정보에 넣고 env 로 가져다가 사용하는 코드가 더 좋은 코드이다.</li>
</ul>
<h2 id="데이터-동기화-문제">데이터 동기화 문제</h2>
<ul>
<li>Orders Service 2개 기동</li>
<li><blockquote>
<p>Users 의 요청 분산 처리</p>
</blockquote>
</li>
<li><blockquote>
<p>Orders 데이터도 분산 저장 -&gt; 동기화 문제</p>
</blockquote>
</li>
</ul>
<p>order service 2개를 기동했을 때 각각의 데이터베이스를 가진다.</p>
<ul>
<li>각각의 데이터베이스를 가지기 때문에 데이터 동기화 문제가 발생한다.</li>
<li>user 가 order 를 첫 번째 order-service 를 통해서 하고 또 다른 order 는 두 번째 order-service 로 했다고 가정해보자.</li>
<li>사용자가 주문 내역을 보면 어쩔때는 첫 번째의 주문이 나오고 어쩔때는 두 번째의 주문이 나오는 문제가 발생한다.</li>
</ul>
<p>해결방법</p>
<ol>
<li>하나의 database 사용</li>
</ol>
<p>-&gt; 이때는 트랜잭션 관리를 잘해야한다. (여러개의 서비스가 하나의 database 를 사용하기 때문)
2. database 간의 동기화
-&gt; 각 서비스는 데이터베이스에 데이터를 저장하는것이 아니라 Message Queuing Server 에 전달을 한다.
-&gt; Meesage Queuing Server 에 구독신청한 또 다른 서비스에게 서버에서 변경된 데이터를 보내주고 업데이트 하도록 한다.
3. kafka connector + db
-&gt; 1번과 2번의 방법을 모두 사용
-&gt; Message Queuing Server 를 미들웨어 즉 중간 매개체로 사용한다.
-&gt; Service 와 database 사이에 Message Queuing Server 를 놓고 관리한다.
-&gt; 1초 안에 수만건을 처리할 수 있도록 설계되어 있다.</p>
<p>order-service 서버를 기동하고 terminal 에 가서 mvn spring-boot:run 명령어로 하나의 서버를 더 기동한다.</p>
<p>유레카 서버에서 각 서비스의 포트 번호를 가지고 h2-console 로 접근해본다.</p>
<ul>
<li>서로 다른 데이터베이스를 가진다.</li>
</ul>
<p>order-serivce/{userId}/orders</p>
<ul>
<li>해당 API 를 사용해서 계속 주문을 하고 order-service 들의 h2-console 로 가본다.</li>
<li>만약 5번을 했다면 한 DB 에 3건 , 또 다른 DB 에 2건이 저장된다.</li>
<li>또한 위에서 restTemplate , FeignClient 를 사용해서 user-service 에서 order-serivce 통신한 로직을 사용해보자.</li>
<li>이때도 한 번은 3건만 출력되고 또 다른 한 번은 2건만 출력이 된다.</li>
<li>번갈아가면서 나오는 방법은 기본적으로 라운드 로빈 방식을 사용하기 때문이다.</li>
</ul>
<p>즉 데이터 동기화에 대한 문제가 있다.</p>
<p>이제 데이터 동기화를 위해 Apache Kafka 를 활용해보자.</p>
<p><a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4/dashboard">참고자료</a></p>
]]></description>
        </item>
    </channel>
</rss>