<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>passion_developer.log</title>
        <link>https://velog.io/</link>
        <description>Backend Developer</description>
        <lastBuildDate>Tue, 02 Jul 2024 13:04:09 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>passion_developer.log</title>
            <url>https://velog.velcdn.com/images/passion_hd/profile/21c6732a-b34b-4873-b45d-3693f0dbae28/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. passion_developer.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/passion_hd" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[JDK 버전 여러개 설치 후 전환 방법]]></title>
            <link>https://velog.io/@passion_hd/JDK-%EB%B2%84%EC%A0%84-%EC%97%AC%EB%9F%AC%EA%B0%9C-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%A0%84%ED%99%98%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@passion_hd/JDK-%EB%B2%84%EC%A0%84-%EC%97%AC%EB%9F%AC%EA%B0%9C-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%A0%84%ED%99%98%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 02 Jul 2024 13:04:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>이 글을 작성하게 된 이유...❓</strong></p>
</blockquote>
<ul>
<li>부트캠프를 진행하며 JDK 11 버전을 사용하여 프로젝트를 진행하였으나, 새로운 프로젝트가 JDK 17 버전으로 만드는 상황이 왔다.</li>
<li>하지만, 이전 프로젝트도 혹시라도 잘못되면 수정해야되는 상황이 올 수 있기에 JDK 11 버전을 아예 안쓸수는 없고, 그때마다 시스템 환경 변수 편집에서 경로를 수정하기엔 너무나도 귀찮아보였다.</li>
<li>그래서 검색한 결과, 역시나 비슷한 상황을 겪은 많은 사람들이 손쉽게 여러 JDK 버전을 사용하는 방법을 공유해주어 나도 정리 차 작성해본다.</li>
</ul>
<hr>
<blockquote>
<p><strong>JDK 버전 여러개 전환하는 법 💡</strong></p>
</blockquote>
<p><strong>1. 먼저 필요한 JDK들을 설치한 후, Java(폴더명은 임의지정) 라는 폴더를 생성하여 저장한다.</strong></p>
<p><strong>2. 다음으로 해당 폴더에 <code>scrips</code> 의 이름으로 폴더를 새로 생성해준다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/c101ab98-bcea-40fd-8fbb-cbdd01f2c3b4/image.png" alt=""></p>
<p><strong>3. 시스템 환경 변수 편집에서 <code>JAVA_HOME</code> 의 변수 경로는 가장 최신 JDK 버전으로 설정해준다.</strong></p>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/cd283d18-7470-4e56-b6fa-cdb8d6300b00/image.png" alt=""></p>
<p><strong>4. Path 변수를 편집하여 <code>%JAVA_HOME%\bin</code> 과 <code>scripts 폴더 경로</code>를 아래와 같이 추가해준다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/6522a579-d8b5-4aa2-8631-8b5938e749cd/image.png" alt=""></p>
<p><strong>5. <code>scripts</code> 폴더 안에 bat 파일을 생성해준다.</strong></p>
<p><strong>&lt; bat 파일 내용 ( JDK 11, 17 기준) &gt;</strong></p>
<blockquote>
<p><strong>@echo off
set JAVA_HOME=C:\Java\jdk-11.0.0.1
set Path=%JAVA_HOME%\bin;%Path%
echo Java 11 activated.
java -version</strong></p>
</blockquote>
<blockquote>
<p><strong>@echo off
set JAVA_HOME=C:\Java\jdk-17
set Path=%JAVA_HOME%\bin;%Path%
echo Java 17 activated.
java -version</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/83c94ebc-6d36-4449-8add-b73f293770fd/image.png" alt=""></p>
<p><strong>6. 마지막으로 cmd 창에서 <code>java11</code> 또는 <code>java17</code> 명령어를 입력하면 정상적으로 JDK 가 
 &nbsp;　변경되는 것을 확인할 수 있다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/578f9c38-d1e7-45d8-ac17-ec508ce2553d/image.png" alt=""></p>
<p>➡ <strong>만약, java 명령어를 찾을 수 없다고 나타난다면 cmd 창을 종료 후 다시 실행시키면 해결될 것이다.</strong></p>
<hr>
<ul>
<li><strong>이제, 내가 원하는 JDK 버전을 그때마다 cmd 창에서 손쉽게 전환하여 적용시킬 수 있다.</strong></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[EC2 프리티어 볼륨 증가하는 방법]]></title>
            <link>https://velog.io/@passion_hd/EC2-%EB%B3%BC%EB%A5%A8-%EC%A6%9D%EA%B0%80%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@passion_hd/EC2-%EB%B3%BC%EB%A5%A8-%EC%A6%9D%EA%B0%80%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 19 Jun 2024 06:13:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>볼륨을 증가하게 된 계기🧐</strong></p>
</blockquote>
<ul>
<li><p>현재 취업을 위해 부트캠프에서 진행했던 BOOTSHELF, LONUA 프로젝트를 EC2로 배포해놓은 상태이다. 하지만, BOOTSHELF 프로젝트만 EC2가 자꾸 뻗는 현상이 발생하는 것이다.</p>
</li>
<li><p>처음에는 메모리가 부족해서 그런줄 알고, SWAP 메모리 설정과 jar 파일 실행 시 메모리 제한까지 걸어서 실행시켰지만 처음엔 해결됬던게 이제는 jar 파일을 실행시키자마자 EC2가 먹통이 되었다.</p>
</li>
<li><p>원인을 찾아보는 중, 볼륨이 100% 로 꽉차서 그렇다는 글을 찾게 되었다. 실제로 볼륨을 확인해보니 정말 100% 꽉차있었고, 크기고 8GB 밖에 되지 않았다.
➡ <strong>볼륨 확인 명령어 : <code>df -h</code></strong></p>
</li>
<li><p>이 문제를 해결하기 위해 프리티어에서 볼륨을 증가하는 방법을 알게되어 정리해본다. <strong>프리티어는 최대 30GB 까지 무료</strong>로 사용할 수 있다고 한다.</p>
</li>
</ul>
<hr>
<blockquote>
<p><strong>볼륨 증가하는 방법 정리</strong>💽</p>
</blockquote>
<p><strong>1. 먼저 AWS에서 EC2를 선택 후 스토리지를 클릭한다.</strong>✅
<img src="https://velog.velcdn.com/images/passion_hd/post/d49d038b-23db-4883-84c1-6976897c8d29/image.png" alt=""></p>
<p>➡ <strong>볼륨의 크기가 8GB 인것을 확인할 수 있을것이다.</strong></p>
<hr>
<p><strong>2. 해당 볼륨을 클릭한 뒤 작업에서 볼륨 수정을 클릭한다.</strong>✅
<img src="https://velog.velcdn.com/images/passion_hd/post/f6874c66-8043-471d-a47c-a834e8a98649/image.png" alt=""></p>
<hr>
<p><strong>3. 30GB 내에서 원하는 크기만큼 볼륨을 수정한다. 늘리는것은 가능하지만 줄이는것은 되지 않아서 나는 
&nbsp;&nbsp;　16GB로 먼저 늘렸다.</strong>✅
<img src="https://velog.velcdn.com/images/passion_hd/post/4b055fa2-290e-43c1-bf71-59ca48957690/image.png" alt=""></p>
<hr>
<p><strong>4. 다음은 EC2를 Putty로 접속해서 아래와 같은 명령어를 입력해준다.</strong>✅</p>
<p>&nbsp;&nbsp;　➡ <code>sudo growpart /dev/xvda 1</code></p>
<p>&nbsp;&nbsp;　➡ <code>sudo resize2fs /dev/xvda1</code></p>
<p>** 만약, 이미 8GB 가 꽉 차서 명령어가 실행이 안될 때는 아래의 방식으로 명령어를 입력한다.**</p>
<p>&nbsp;&nbsp;　➡ <code>sudo mount -o size=10M,rw,nodev,nosuid -t tmpfs tmpfs /tmp</code></p>
<p>&nbsp;&nbsp;　➡ <code>sudo growpart /dev/xvda 1</code></p>
<p>&nbsp;&nbsp;　➡ <code>sudo growpart /dev/xvda 1</code></p>
<p>&nbsp;&nbsp;　➡ <code>sudo umount /tmp</code></p>
<hr>
<p><strong>5. 최종적으로 볼륨이 증가했는지 확인해본다.</strong>✅</p>
<p>&nbsp;&nbsp;　➡ 명령어 입력 : <code>df -h</code>
<img src="https://velog.velcdn.com/images/passion_hd/post/5ab0f2a5-17db-406f-9398-3c60d01128a5/image.png" alt=""></p>
<p>&nbsp;&nbsp;　➡ 명령어 입력 : <code>lsblk</code>
<img src="https://velog.velcdn.com/images/passion_hd/post/b2279135-143d-4225-87ef-dd2d4b246ea7/image.png" alt=""></p>
<hr>
<ul>
<li><p>프리티어를 쓰다보니 불편한점이 많지만...그래도 이기회에 사소할 수 있지만 새로운 내용을 알게되어 좋은 경험이었다.</p>
</li>
<li><p>이제는 뻗지 않길 빌며...글을 마무리한다!!😆</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Generic Web Trigger 설정]]></title>
            <link>https://velog.io/@passion_hd/Generic-Web-Trigger-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@passion_hd/Generic-Web-Trigger-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Tue, 07 May 2024 09:13:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>** Generic WebHook Trigger 를 설정하게 된 배경**❓</p>
</blockquote>
<ul>
<li>본 내용은 부트캠프 파이널 프로젝트 종료 후 적용한 것들을 정리하기 위한 글입니다.</li>
<li>기존에는 CI/CD 환경 구축 시 일반적으로 깃허브에서 <strong>Web Hook 트리거</strong>를 설정하여 젠킨스 파이프라인이 발동되었었다.</li>
<li>하지만, 파이널 프로젝트 시 깃허브 레포지토리 구조가 아래와 같이 하나의 레포지토리에 백엔드와 프론트엔드가 동시에 존재하는 구조로 진행이 되었다.<img src="https://github.com/beyond-sw-camp/be02-fin-BuildUp-KMS/assets/148875644/aacdeed1-f1d5-4965-ab44-3115dbfc15c9" width=100%, height=50%>

</li>
</ul>
<hr>
<ul>
<li><p>이러한 구조로 개발이 되다보니, 만약에 백엔드 코드를 수정 후 <strong>develop 브랜치에 Merge 시</strong> 젠킨스에서는 <strong>백엔드와 프론트엔드 파이프라인이 동시에 실행</strong>되는 문제가 발생하였다.</p>
</li>
<li><p>이 문제를 해결하기 위해 알아보던 중 <strong>Generic WebHook Trigger</strong> 에 대해 알게되어 적용하게 되었다.</p>
</li>
</ul>
<hr>
<blockquote>
<p>** 설정하는 방법** 🧐</p>
</blockquote>
<p><strong>1. 젠킨스 대시보드에서 Generic WebHook Trigger 플러그인을 설치 한다.</strong>
<img src="https://github.com/beyond-sw-camp/be02-fin-BuildUp-KMS/assets/148875644/3346af89-5bfb-47b4-a843-4bf5a83fc764" alt="2"></p>
<hr>
<p><strong>2. 깃허브에서 Generic WebHook Trigger 를 위한 설정을 아래 단계로 진행한다.</strong></p>
<p>&nbsp;　<strong>1) 깃허브 레포지토리에서 Settings - Webhooks를 클릭한다.</strong></p>
<p>&nbsp;　<strong>2) Payload URL</strong>
&nbsp;&nbsp;　　➡ <strong><code>http://공인IP URL:포트번호/generic-webhook-trigger/invoke?token=token</code></strong>
&nbsp;&nbsp;　　➡ <strong>공인IP URL과 포트번호만 변경하고, 나머지는 다 동일하게 작성한다.</strong></p>
<p>&nbsp;　<strong>3) Content type : <code>application/json</code> 으로 선택한다.</strong></p>
<p>&nbsp;　<strong>4) <code>Send me everything</code> 을 선택한다.</strong>
<img src="https://github.com/beyond-sw-camp/be02-fin-BuildUp-KMS/assets/148875644/e15338ac-7220-4bde-985f-66a465532fd3" alt="3"></p>
<p><strong>3. 젠킨스 파이프라인의 구성에서 설정을 아래 단계로 진행한다.</strong></p>
<p>&nbsp;　<strong>1) Build Triggers에서 &quot;Generic Webhook Trigger&quot; 를 선택한다.</strong>
<img src="https://github.com/beyond-sw-camp/be02-fin-BuildUp-KMS/assets/148875644/908b24e4-84eb-44b8-864a-5024d1df251d" alt="4"></p>
<p>&nbsp;　<strong>2) param 추가를 클릭하여 아래와 같이 3개를 추가해준다.</strong>
<img src="https://github.com/beyond-sw-camp/be02-fin-BuildUp-KMS/assets/148875644/38732c2d-530e-42f4-9f7f-34dc147ce7f5" width=50%, height=20%></p>
<p>&nbsp;&nbsp;　　➡ <strong>Merge 가 됬는지 여부를 확인하기 위한 조건이다.</strong> / <code>$.pull_request.merged</code>
<img src="https://github.com/beyond-sw-camp/be02-fin-BuildUp-KMS/assets/148875644/8cf9e654-de2c-47f2-8efc-9254ff457873" width=50%, height=20%></p>
<p>&nbsp;&nbsp;　　➡ <strong>라벨(Label) 이름을 확인하기 위한 조건이다.</strong> / <code>$.pull_request.labels..name</code>
<img src="https://github.com/beyond-sw-camp/be02-fin-BuildUp-KMS/assets/148875644/3abdaa44-251e-45f2-a6f0-f8be5e3ebcfa" width=50%, height=20%>
&nbsp;&nbsp;　　➡ <strong>브랜치명을 확인하기 위한 조건이다.</strong> / <code>$.pull_request.base.ref</code></p>
<hr>
<p>&nbsp;　<strong>3) Token에는 그냥 <code>token</code> 이라고 입력해준다.</strong>
<img src="https://github.com/beyond-sw-camp/be02-fin-BuildUp-KMS/assets/148875644/32cf4c7f-415e-4b5a-bdde-0989a3b4902a" alt="8"></p>
<hr>
<p>&nbsp;　<strong>4) 해당 파이프라인에서 어떠한 조건일때 동작시킬 지 입력해준다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/8b7908cc-64c3-4d28-b5cb-b68d0802aa20/image.png" alt=""></p>
<p>&nbsp;&nbsp;　　➡ <strong>나는 develop 브랜치에 backend를 포함하는 라벨이 Merge 됬을때 트리거가</strong> 
&nbsp;&nbsp;&nbsp;&nbsp;　　　<strong>발동되도록 설정하였다.</strong>
&nbsp;&nbsp;　　➡ <em>* Expression : `(?=.<em>true)(?=.</em>develop)(?=.*backend.</em>).<em>`*</em>
&nbsp;&nbsp;　　➡ ** Text : <code>$MERGED $REF $LABEL</code>**</p>
<hr>
<ul>
<li><p><strong>여기까지 하면 모든 설정이 끝난다.</strong></p>
</li>
<li><p><strong>이제 깃허브에서 develop 브랜치에 &quot;backend&quot; 라벨을 달고 Merge 시 해당 설정을 한 백엔드 파이프라인만 실행이 될 것이다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/052c9a52-2527-46ff-b024-0d15d5bc6f8e/image.png" alt=""></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jenkins - 3 ( Jenkins Pipeline 및 Jenkins CI )]]></title>
            <link>https://velog.io/@passion_hd/Jenkins-3-Jenkins-Pipeline-%EB%B0%8F-Jenkins-CI</link>
            <guid>https://velog.io/@passion_hd/Jenkins-3-Jenkins-Pipeline-%EB%B0%8F-Jenkins-CI</guid>
            <pubDate>Mon, 26 Feb 2024 13:19:31 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>🐸** 데브옵스 프로젝트를 진행하며 작성한 코드 정리 ( Test 코드 제외 )**</p>
</blockquote>
<ul>
<li><p>💡<strong>프론트엔드 Jenkins file</strong></p>
<pre><code class="language-yaml">pipeline {
  agent any

  // ✅ 파이프라인의 실행 단계 정의
  stages {

      // 1. 깃 클론❗
      stage(&#39;git clone&#39;) {
              steps {
                      git branch: &#39;main&#39;, url: &#39;https://github.com/hyungdoyou/devops&#39;
              }
      }

      // 2. 프로젝트에 필요한 패키지 설치❗
      stage(&#39;Install Dependencies&#39;) {
          steps {
              script {
                  sh &#39;npm install&#39;
              }
          }
      }

      // 3. 기존에 생성되어 있는 dist 폴더 삭제❗
      stage(&#39;Delete Exist file&#39;) {
          steps {
              script {
                  sh &#39;rm -rf /var/lib/jenkins/workspace/k8s-frontend/dist&#39;
              }
          }
      }

      // 4. 새로운 프로젝트 빌드❗
      stage(&#39;Build&#39;) {
          steps {
              script {
                  sh &#39;npm run build&#39;
              }
          }
      }

      // 5. Docker 빌드❗
      stage(&#39;Docker Build&#39;) {
          steps {
              script {
                  sh &#39;docker build --tag hyungdoyou/fe:3.$BUILD_NUMBER .&#39;
              }
          }
          post {
              // Docker 빌드 성공 시 슬랙 채널 ( #테스트 ) 로 성공 알림 발송❗
              success {
                  slackSend(
                      channel: &#39;#테스트&#39;, 
                      color: &#39;good&#39;, 
                      message: &quot;Docker image hyungdoyou/fe:3.${BUILD_NUMBER} build success.&quot;, 
                      teamDomain: &#39;jenkins-alert-hq&#39;, 
                      tokenCredentialId: &#39;slack-jenkins-token&#39;)
              }
              // Docker 빌드 실패 시 슬랙 채널 ( #테스트 ) 로 실패 알림 발송❗
              failure {
                  slackSend(
                      channel: &#39;#테스트&#39;, 
                      color: &#39;danger&#39;, 
                      message: &quot;Docker image hyungdoyou/fe:3.${BUILD_NUMBER} build failed.&quot;, 
                      teamDomain: &#39;jenkins-alert-hq&#39;, 
                      tokenCredentialId: &#39;slack-jenkins-token&#39;)
              }
          }
      }

      // 6. Docker Push❗
      stage(&#39;Docker Push&#39;) {
          steps {
              script {
                  // Docker 로그인❗
                  withCredentials([usernamePassword(credentialsId: &#39;docker_credentials&#39;, usernameVariable: &#39;DOCKER_EMAIL&#39;, passwordVariable: &#39;DOCKER_PASSWORD&#39;)]) {
                      sh &quot;docker login -u ${DOCKER_EMAIL} -p ${DOCKER_PASSWORD}&quot;
                  }
                  // Docker Push❗
                  sh &#39;docker push hyungdoyou/fe:3.$BUILD_NUMBER&#39;
              }
          }
          post {
              // Docker Push 성공 시 슬랙 채널 ( #테스트 ) 로 성공 알림 발송❗
              success {
                  slackSend(
                      channel: &#39;#테스트&#39;, 
                      color: &#39;good&#39;, 
                      message: &quot;Docker image hyungdoyou/fe:3.${BUILD_NUMBER} push success.&quot;, 
                      teamDomain: &#39;jenkins-alert-hq&#39;, 
                      tokenCredentialId: &#39;slack-jenkins-token&#39;)
              }
              // Docker Push 실패 시 슬랙 채널 ( #테스트 ) 로 실패 알림 발송❗
              failure {
                  slackSend(
                      channel: &#39;#테스트&#39;, 
                      color: &#39;danger&#39;, 
                      message: &quot;Docker image hyungdoyou/fe:3.${BUILD_NUMBER} push failed.&quot;, 
                      teamDomain: &#39;jenkins-alert-hq&#39;, 
                      tokenCredentialId: &#39;slack-jenkins-token&#39;)
              }
          }
      }
      // 7. SSH 전송❗
      stage(&#39;SSH transfer&#39;) {
          steps {
              // SSH 전송 플러그인 사용
              sshPublisher(
                  // 오류 발생 시 진행을 멈춤
                  continueOnError: false,
                  // 오류 발생 시 파이프라인을 실패시킴
                  failOnError: true,
                  // 전송자 목록
                  publishers: [
                      // SSH 전송 설명
                      sshPublisherDesc(
                          // SSH 서버 설정 이름 지정 ( master 노드 )
                          configName: &quot;k8s-master&quot;,
                          // 자세한 출력 모드 활성화
                          verbose: true,
                          transfers: [
                              sshTransfer(
                                  // 전송할 파일 지정
                                  sourceFiles: &quot;frontend-deployment.yml&quot;,
                                  // 원격 디렉토리 지정 ( 원격서버로 파일을 전송할 위치 )
                                  remoteDirectory: &quot;/root/&quot;,
                                  // 전송 후 야멜 파일의 VERSION을 파이프라인 빌드 숫자로 변경
                                  // backend-deployment 야멜 파일 실행
                                  execCommand: &#39;&#39;&#39;
                                      sed -i &quot;s/VERSION/$BUILD_ID/g&quot; /root/frontend-deployment.yml
                                      kubectl apply -f /root/frontend-deployment.yml
                                  &#39;&#39;&#39;
                              )
                          ]
                      )
                  ]
              )
          }
          post {
              // SSH Transfer 성공 시 슬랙 채널 ( #테스트 ) 로 성공 알림 발송❗
              success {
                  slackSend(
                      channel: &#39;#테스트&#39;, 
                      color: &#39;good&#39;, 
                      message: &quot;frontend-deployment.yml transfer success.&quot;, 
                      teamDomain: &#39;jenkins-alert-hq&#39;, 
                      tokenCredentialId: &#39;slack-jenkins-token&#39;)
              }
              // SSH Transfer 실패 시 슬랙 채널 ( #테스트 ) 로 실패 알림 발송❗
              failure {
                  slackSend(
                      channel: &#39;#테스트&#39;, 
                      color: &#39;danger&#39;, 
                      message: &quot;frontend-deployment.yml transfer failed.&quot;, 
                      teamDomain: &#39;jenkins-alert-hq&#39;, 
                      tokenCredentialId: &#39;slack-jenkins-token&#39;)
              }
          }
      }
      // 8. 슬랙 알림❗
      stage(&#39;Slack Notification&#39;) {
          steps {
              script {
                  // 파이프라인 단계가 끝나고 슬랙으로 최종 알림 발송
                  slackSend(channel: &#39;#젠킨스알림&#39;, message: &quot;Build version : ${env.BUILD_NUMBER} is finally successful!&quot;, teamDomain: &#39;hooks.slack.com&#39;, token: &#39;/services/T06KN5J31HD/B06L8GV2MTL/15TDN2G6g6FfFaq1cgRqrZ5j&#39;)
                  slackSend(
                      channel: &#39;#테스트&#39;, 
                      color: &#39;good&#39;,
                      message: &quot;Build version : ${env.BUILD_NUMBER} is finally successful!&quot;, 
                      teamDomain: &#39;jenkins-alert-hq&#39;, 
                      tokenCredentialId: &#39;slack-jenkins-token&#39;
                      )
              }
          }
      }
  }
}</code></pre>
</li>
</ul>
<hr>
<ul>
<li>💡<strong>백엔드 Jenkins file</strong></li>
</ul>
<pre><code class="language-yaml">pipeline {
    agent any

    // 파이프라인 실행에 필요한 도구로 maven 지정❗
    tools {
        maven &quot;maven-3.9.5&quot;
    }

    // 파이프라인의 실행 단계 정의❗
    stages {

        // 1. 깃 클론❗
        stage(&#39;git clone&#39;) {
            steps {
                git branch: &#39;main&#39;, url: &#39;https://github.com/hyungdoyou/devops-backend&#39;
            }
        }

        // 2. maven 빌드❗
        stage(&#39;Build&#39;) { 
            steps {
                sh &#39;mvn -B -DskipTests clean package&#39; 
            }
        }

        // 3. Docker 빌드❗
        stage(&#39;Docker Build&#39;) {
            steps {
                script {
                    // 도커 이미지 : hyungdoyou/be:2.[빌드 숫자]
                    sh &#39;docker build --tag hyungdoyou/be:2.$BUILD_NUMBER .&#39;
                }
            }
            post {
                // Docker 빌드 성공 시 슬랙 채널 ( #테스트 ) 로 성공 알림 발송❗
                success {
                    slackSend(
                        channel: &#39;#테스트&#39;, 
                        color: &#39;good&#39;, 
                        message: &quot;Docker image hyungdoyou/be:2.${BUILD_NUMBER} build success.&quot;, 
                        teamDomain: &#39;jenkins-alert-hq&#39;, 
                        tokenCredentialId: &#39;slack-jenkins-token&#39;)
                }

                // Docker 빌드 실패 시 슬랙 채널 ( #테스트 ) 로 실패 알림 발송❗
                failure {
                    slackSend(
                        channel: &#39;#테스트&#39;, 
                        color: &#39;danger&#39;, 
                        message: &quot;Docker image hyungdoyou/be:2.${BUILD_NUMBER} build failed.&quot;, 
                        teamDomain: &#39;jenkins-alert-hq&#39;, 
                        tokenCredentialId: &#39;slack-jenkins-token&#39;)
                }
            }
        }

        // 4. Docker Push❗
        stage(&#39;Docker Push&#39;) {
            steps {
                script {
                    // Docker 로그인
                    withCredentials([usernamePassword(credentialsId: &#39;docker_credentials&#39;, usernameVariable: &#39;DOCKER_EMAIL&#39;, passwordVariable: &#39;DOCKER_PASSWORD&#39;)]) {
                        sh &quot;docker login -u ${DOCKER_EMAIL} -p ${DOCKER_PASSWORD}&quot;
                    }
                    // Docker Push❗
                    sh &#39;docker push hyungdoyou/be:2.$BUILD_NUMBER&#39;
                }
            }
            post {
                // Docker Push 성공 시 슬랙 채널 ( #테스트 ) 로 성공 알림 발송❗
                success {
                    slackSend(
                        channel: &#39;#테스트&#39;, 
                        color: &#39;good&#39;, 
                        message: &quot;Docker image hyungdoyou/fe:3.${BUILD_NUMBER} push success.&quot;, 
                        teamDomain: &#39;jenkins-alert-hq&#39;, 
                        tokenCredentialId: &#39;slack-jenkins-token&#39;)
                }
                // Docker Push 실패 시 슬랙 채널 ( #테스트 ) 로 실패 알림 발송❗
                failure {
                    slackSend(
                        channel: &#39;#테스트&#39;, 
                        color: &#39;danger&#39;, 
                        message: &quot;Docker image hyungdoyou/fe:3.${BUILD_NUMBER} push failed.&quot;, 
                        teamDomain: &#39;jenkins-alert-hq&#39;, 
                        tokenCredentialId: &#39;slack-jenkins-token&#39;)
                }
            }
        }

        // 5. SSH 전송❗
        stage(&#39;SSH transfer&#39;) {
            steps {
                // SSH 전송 플러그인 사용
                sshPublisher(
                    // 오류 발생 시 진행을 멈춤
                    continueOnError: false, 
                    // 오류 발생 시 파이프라인을 실패시킴
                    failOnError: true,
                    // 전송자 목록
                    publishers: [
                        // SSH 전송 설명
                        sshPublisherDesc(
                            // SSH 서버 설정 이름 지정 ( master 노드 )
                            configName: &quot;k8s-master&quot;,
                            // 자세한 출력 모드 활성화
                            verbose: true,
                            transfers: [
                                sshTransfer(
                                    // 전송할 파일 지정
                                    sourceFiles: &quot;backend-deployment.yml&quot;,
                                    // 원격 디렉토리 지정 ( 원격서버로 파일을 전송할 위치 )
                                    remoteDirectory: &quot;/root/&quot;,
                                    // 전송 후 야멜 파일의 VERSION을 파이프라인 빌드 숫자로 변경
                                    // backend-deployment 야멜 파일 실행
                                    execCommand: &#39;&#39;&#39;
                                        sed -i &quot;s/VERSION/$BUILD_ID/g&quot; /root/backend-deployment.yml
                                        kubectl apply -f /root/backend-deployment.yml
                                    &#39;&#39;&#39;
                                )
                            ]
                        )
                    ]
                )
            }
            post {
                // SSH Transfer 성공 시 슬랙 채널 ( #테스트 ) 로 성공 알림 발송❗
                success {
                    slackSend(
                        channel: &#39;#테스트&#39;, 
                        color: &#39;good&#39;, 
                        message: &quot;frontend-deployment.yml transfer success.&quot;, 
                        teamDomain: &#39;jenkins-alert-hq&#39;, 
                        tokenCredentialId: &#39;slack-jenkins-token&#39;)
                }
                // SSH Transfer 실패 시 슬랙 채널 ( #테스트 ) 로 실패 알림 발송❗
                failure {
                    slackSend(
                        channel: &#39;#테스트&#39;, 
                        color: &#39;danger&#39;, 
                        message: &quot;frontend-deployment.yml transfer failed.&quot;, 
                        teamDomain: &#39;jenkins-alert-hq&#39;, 
                        tokenCredentialId: &#39;slack-jenkins-token&#39;)
                }
            }
        }
        // 6. 슬랙 알림❗
        stage(&#39;Slack Notification&#39;) {
            steps {
                script {
                    // 파이프라인 단계가 끝나고 슬랙으로 최종 알림 발송
                    slackSend(
                        channel: &#39;#테스트&#39;, 
                        color: &#39;good&#39;,
                        message: &quot;Build version : ${env.BUILD_NUMBER} is finally successful!&quot;, 
                        teamDomain: &#39;jenkins-alert-hq&#39;, 
                        tokenCredentialId: &#39;slack-jenkins-token&#39;
                        )
                }
            }
        }
    }
}</code></pre>
<hr>
<blockquote>
<p>🐮** Jenkins CI를 이용한 Slack 알람 발송**</p>
</blockquote>
<p><strong>1. Slack 채널 오른쪽 클릭 ➡ 채널 세부정보 보기 ➡ 통합 ➡ 앱추가 클릭</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/31ce14c2-e4bf-410e-9d97-f6ca0c3a0c90/image.png" alt=""></p>
<hr>
<p><strong>2. Jenkins CI 앱 추가 클릭</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/04a8437b-aedd-46b3-8f67-0f1c7c96e693/image.png" alt=""></p>
<hr>
<p><strong>3. 채널 선택 후 Jenkins CI 통합 앱 추가 클릭</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/c3b09519-d99e-43b8-acc2-a712ec871545/image.png" alt=""></p>
<hr>
<p><strong>4. 3단계의 &quot;팀 하위 도메인&quot; 과 &quot;통합 토큰 자격 증명 ID&quot; 를 적어두고 저장한다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/0dd7fb97-5f3b-4297-a93a-49f9d7d179ff/image.png" alt=""></p>
<hr>
<p><strong>5. Jenkins 대시보드에서 Jenkins 관리의 Plugins 클릭</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/9cc6eedc-f968-4b41-b26f-ed482b40f57f/image.png" alt=""></p>
<hr>
<p><strong>6. Slack Notification 플러그인을 설치한다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/d8ae2d54-8c13-4872-99fb-43efcf4ddf8f/image.png" alt=""></p>
<hr>
<p><strong>7. Jenkins 관리의 Credentials에서 &quot;Add Credentials&quot;를 클릭한다.</strong>
&nbsp;　➡ <strong>Secret text 선택 후 Secret 에는 위에서 발급받은 토큰 자격 증명 ID를 입력한다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/b6211b90-757e-43ed-af14-50077043e5b9/image.png" alt=""></p>
<hr>
<p><strong>8. Jenkins file 에서 아래와 같이 슬랙 알람 발송 코드를 작성한다.</strong></p>
<pre><code class="language-yaml">                    slackSend(
                        channel: &#39;#젠킨스알림&#39;,  // 슬랙 채널 명
                        color: &#39;good&#39;, // good, warn, danger 등이 있다.
                        message: &quot;Build version : ${env.BUILD_NUMBER} is finally successful!&quot;, 
                        teamDomain: &#39;jenkins-XXXX-XX&#39;, // Jenkins CI 추가 시 3단계에서 저장해놨던 것
                        tokenCredentialId: &#39;test&#39;  // 위에서 생성한 Credentials 이름
                        )</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jenkins - 2 ( Jenkins file 및 k8s 배포 )]]></title>
            <link>https://velog.io/@passion_hd/Jenkins-2-Jenkins-file-%EB%B0%8F-k8s-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@passion_hd/Jenkins-2-Jenkins-file-%EB%B0%8F-k8s-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Thu, 22 Feb 2024 16:21:23 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>Jenkins Freestyle 과 Pipeline</strong> ❓</p>
</blockquote>
<ul>
<li><strong>Jenkins의 Item</strong> 💡</li>
<li><em>Jenkins에서 하나의 CI/CD 프로젝트를 구축하기 위해서는 아이템(Item)을 생성하여야 한다. 하나의 Jenkins 서버에 여러개의 아이템을 만들 수 있고, 각각의 아이템들은 개발자가 설정하는 것에 따라 다르게 동작한다. 
Jenkins에서 아이템을 만드는 방법은 대표적으로 <code>FreeStyle</code>과 <code>Pipeline</code>이 존재한다.*</em></li>
</ul>
<br>

<ul>
<li><p><strong>Freestyle vs Pipeline</strong> </p>
<p>✅ <strong>Freestyle</strong></p>
<p>➡ <strong>장점 : 웹 기반의 GUI를 통해 여러 플러그인을 쉽게 사용할 수 있다.</strong></p>
<p>➡ <strong>단점</strong>
&nbsp;　<strong>1) CI 파이프라인에 변경 사항을 만들기 위해서는 Jenkins에 로그인해서 각각의 
&nbsp;&nbsp;　　프리스타일 Job의 설정을 변경해야만 한다.</strong>
&nbsp;　<strong>2) CI/CD의 과정을 콘솔을 통해서만 확인할 수 있다.</strong>
&nbsp;　<strong>3) 각각의 과정들을 한번에 보기 어렵다.</strong></p>
<hr>
<p>✅ <strong>Pipeline</strong></p>
<p>➡ <strong>장점</strong>
&nbsp;　<strong>1) 코드로 프로젝트 설정을 할 수 있어서 프리스타일과 다르게 Jenkins 웹에 직접 
&nbsp;&nbsp;　　접근하지 않아도 설정 변경이 가능하다. (웹을 통한 설정도 가능하다.)</strong>
&nbsp;　<strong>2) CI/CD 파이프라인 설정을 하나의 스크립트 파일(Jenkins file)로 프로그래밍을 통해 
&nbsp;&nbsp;　　할 수 있다.</strong>
&nbsp;　<strong>3) 일반 코드처럼 버전 관리가 가능하다.</strong>
&nbsp;　<strong>4) GUI를 통해 현재 어떤 과정을 진행중이고 평균적으로 얼마만큼의 시간이 걸렸는지 
&nbsp;&nbsp;　　통계와 피드백을 준다.</strong></p>
<p>➡ <strong>단점 : 프리스타일과 다르게 스크립트를 짜야하는 번거로움이 있다. 즉, 파이프라인 
&nbsp;&nbsp;&nbsp;&nbsp;　　　구축을 위해서는 스크립트 문법을 학습하여야 한다.</strong></p>
</li>
</ul>
<hr>
<blockquote>
<p>🙉 <strong>Jenkins Pipeline으로 프론트엔드 및 백엔드 서버 구축하기</strong></p>
</blockquote>
<ul>
<li><p><strong>시작하기에 앞서, 지난 글에서 Freestyle 로 생성한 item 들의 구성에서 
소스 코드 관리를 None 으로 바꿔준다. ( 동일한 깃허브 레포지토리 사용 불가 )</strong></p>
</li>
<li><p><strong>프론트엔드 서버 구축</strong> 💻</p>
</li>
</ul>
<p><strong>1. 새로운 Item 클릭 ➡ Pipeline 선택 후 OK</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/51f7f13f-3b2f-40af-9223-189525589261/image.png" alt=""></p>
<hr>
<p><strong>2. 파이프라인 구성 설정</strong>
 &nbsp;　<strong>1) GitHub project 선택 후 레포지토리 주소 입력 ( 선택사항 )</strong>
 &nbsp;　<strong>2) Build Triggers 에서 &quot;GitHub hook trigger for GITScm polling&quot; 선택</strong>
 &nbsp;　<strong>3) Pipeline Script에 jenkins file 문법 의거 작성</strong></p>
<pre><code class="language-yaml"> pipeline {
    agent any

    stages {
        stage(&#39;git clone&#39;) {
                steps {
                        git branch: &#39;main&#39;, url: &#39;https://github.com/[깃허브 계정]/[레포지토리명]&#39;
                }
        }
        stage(&#39;Install Dependencies&#39;) {
            steps {
                script {
                    sh &#39;npm install&#39;
                }
            }
        }

        stage(&#39;Delete Exist file&#39;) {
            steps {
                script {
                    sh &#39;rm -rf /var/lib/jenkins/workspace/[파이프라인 이름]/dist&#39;
                }
            }
        }

        stage(&#39;Build&#39;) {
            steps {
                script {
                    sh &#39;npm run build&#39;
                }
            }
        }

        stage(&#39;Archive Dist&#39;) {
            steps {
                script {
                    // dist 디렉토리로 이동
                    dir(&#39;dist&#39;) {

                        // tar로 압축
                        sh &#39;tar -cvf dist.tar ./*&#39;

                        // 압축 파일을 상위 디렉토리로 이동
                        sh &#39;mv /var/lib/jenkins/workspace/[파이프라인 이름]/dist/dist.tar ../&#39;
                    }
                }
            }
        }
        stage(&#39;SSH transfer&#39;) {
            steps {
                sshPublisher(
                    continueOnError: false, 
                    failOnError: true,
                    publishers: [
                        sshPublisherDesc(
                            configName: &quot;test-server&quot;,    // SSH 서버 설정해둔 이름
                            verbose: true,
                            transfers: [
                                sshTransfer(
                                    sourceFiles: &quot;dist.tar&quot;,
                                    remoteDirectory: &quot;/usr/share/nginx/html/&quot;,
                                    execCommand: &quot;tar -xvf /usr/share/nginx/html/dist.tar -C /usr/share/nginx/html/&quot;
                                )
                            ]
                        )
                    ]
                )
            }
        }
    }
}
</code></pre>
<hr>
<p> ➡ <strong>이전 글에서 Freestyle 로 생성 후 구성에서 설정했던 내용들을 코드로 적어주는 것이다.</strong></p>
<p> ➡ <strong>&quot;지금 빌드&quot; 또는 VS Code에서 깃허브로 Push 해보면, 아래와 같이 각 단계 별로 결과를 
 &nbsp;　그림으로 보여준다. 따라서, 어느 단계에서 오류가 발생했는지 알기가 쉽다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/5aa44cc9-eb0b-4cd3-bee3-68f9e48a6a45/image.png" alt=""></p>
<hr>
<ul>
<li><strong>백엔드 서버 구축</strong> 💻</li>
</ul>
<p><strong>1. 새로운 Item 클릭 ➡ Pipeline 선택 후 OK</strong></p>
<p><strong>2. 파이프라인 구성 설정</strong>
 &nbsp;　<strong>1) GitHub project 선택 후 레포지토리 주소 입력 ( 선택사항 )</strong>
 &nbsp;　<strong>2) Build Triggers 에서 &quot;GitHub hook trigger for GITScm polling&quot; 선택</strong>
 &nbsp;　<strong>3) Pipeline Script에 jenkins file 문법 의거 작성</strong></p>
<pre><code class="language-yaml"> pipeline {
    agent any

    tools {
        maven &quot;maven-3.9.5&quot;   // 이전 글에서 설정해줬던 메이븐의 Name
    }

    stages {
        stage(&#39;git clone&#39;) {
            steps {
                git branch: &#39;main&#39;, url: &#39;https://github.com/[깃허브 계정명]/[레포지토리명]&#39;
            }
        }

        stage(&#39;Build&#39;) {   // 메이븐 빌드
            steps {
                sh &#39;mvn -B -DskipTests clean package&#39; 
            }
        }

        stage(&#39;SSH Transfer&#39;) {
            steps {
                script {
                    sshPublisher(
                        continueOnError: false, 
                        failOnError: true,
                        publishers: [
                            sshPublisherDesc(
                                configName: &quot;test-server&quot;,    // SSH 서버 설정해둔 이름
                                verbose: true,
                                transfers: [
                                    sshTransfer(
                                        sourceFiles: &quot;target/lonua-0.0.1-SNAPSHOT.jar&quot;,
                                        removePrefix: &quot;target/&quot;,
                                        remoteDirectory: &quot;/root/&quot;,
                                        execCommand: &#39;&#39;&#39;
export APP_PASSWORD=
export AWS_S3_ACCESS_KEY=
export AWS_S3_SECRET_KEY=
export BRAND_BUCKET=
export CLIENT_ID=
export EXPIRED_TIME=
export JWT_SECRET_KEY=
export MAIL_SENDER=
export MASTER=
export MASTER_PW=
export MASTER_URL=
export PORTONE_KEY=
export PORTONE_SECRETKEY=
export PRODUCT_BUCKET=
export PRODUCT_INTROD_BUCKET=
export REGION=
export REVIEW_BUCKET=
export SLAVE=
export SLAVE_PW=
export SLAVE_URL=

echo &quot;PID Check...&quot; &gt;&gt; /var/log/jenkins_spring.log
CURRENT_PID=$(netstat -anlp | grep 8080 | awk &#39;{print $7}&#39; | cut -d &#39;/&#39; -f1)

echo &quot;Running PID: {$CURRENT_PID}&quot; &gt;&gt; /var/log/jenkins_spring.log

if [ -z $CURRENT_PID ]; then
    echo &quot;Project is not running&quot; &gt;&gt; /var/log/jenkins_spring.log
else
    echo &quot;Project Killing&quot; &gt;&gt; /var/log/jenkins_spring.log
    kill -9 $CURRENT_PID
    sleep 3
fi
echo &quot;Deploy Project....&quot; &gt;&gt; /var/log/jenkins_test_spring.log
nohup java -jar /root/lonua-0.0.1-SNAPSHOT.jar &gt;&gt; /var/log/jenkins_spring.log 2&gt;&amp;1 &amp;
&#39;&#39;&#39;
                                    )
                                ]
                            )
                        ]
                    )
                }
            }
        }
    }
}
</code></pre>
<hr>
<p>  ➡ <strong>실행 결과</strong>
  <img src="https://velog.velcdn.com/images/passion_hd/post/8101347a-5277-4bec-afff-94ec602b2a4f/image.png" alt=""></p>
<hr>
<blockquote>
<p>🐮 ** Docker 설정하기**</p>
</blockquote>
<ul>
<li><strong>플러그인 사용 안하는 경우</strong> ✅</li>
</ul>
<p><strong>1. Jenkins 서버에 도커 설치 및 실행</strong></p>
<p>&nbsp;　➡ <code>yum install -y yum-utils</code></p>
<p>&nbsp;　➡ <code>yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo</code></p>
<p>&nbsp;　➡ <code>yum-config-manager --enable docker-ce-nightly</code></p>
<p>&nbsp;　➡ <code>yum-config-manager --enable docker-ce-test</code></p>
<p>&nbsp;　➡ <code>yum install -y docker-ce docker-ce-cli containerd.io --allowerasing</code></p>
<p>&nbsp;　➡ <code>systemctl restart docker</code></p>
<hr>
<p><strong>2. Jenkins 서버에서 권한 설정</strong></p>
<p>&nbsp;　➡ <strong><code>ls -l /var/run/docker.sock</code> 해보면 권한이 root와 docker 그룹에게만 있다.</strong></p>
<p>&nbsp;　➡ <strong>젠킨스 계정을 도커 그룹에 추가 : <code>usermod -aG docker jenkins</code></strong></p>
<p>&nbsp;　➡ <strong>도커 권한을 젠킨스로 변경 : <code>chown root:jenkins /var/run/docker.sock</code></strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/1464b457-034a-4d7e-8a38-8e71faf408c9/image.png" alt=""></p>
<hr>
<p><strong>3. Jenkins 대시보드에서 도커 허브 계정 변수 설정</strong></p>
<p>&nbsp;　<strong>1) Jenkins 관리 ➡ Credentials 클릭</strong></p>
<p>&nbsp;　<strong>2) Domain의 (global) 클릭 ➡ Add Credentials 클릭</strong></p>
<p>&nbsp;　<strong>3) username : 도커허브 계정 / password : 도커허브 패스워드</strong></p>
<p>&nbsp;　<strong>4) ID : 변수 이름 임의로 지정</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/45b4582c-f463-4ffd-8f24-8d8cb47021a6/image.png" alt=""></p>
<hr>
<p><strong>4. 사용 시 : 파이프라인 구성의 빌드 환경에서 User secret text(s) or file(s) 체크</strong>
&nbsp;　➡ <strong>Add 클릭 후 Username and password (separated ) 클릭</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/9fa233d1-e23e-4059-8216-66447f6a9027/image.png" alt=""></p>
<p>&nbsp;　➡ <strong>Username Variable  : 변수이름 설정  // DOCKER_EMAIL</strong></p>
<p>&nbsp;　➡ <strong>Password Variable : 변수이름 설정 // DOCKER_PASSWORD</strong></p>
<p>&nbsp;　➡ <strong>Build Steps의 Execute shell 작성</strong></p>
<pre><code class="language-yaml">            docker build --tag [도커허브 계정]/[도커허브 레포지토리명]:[버전] .
            docker login -u ${DOCKER_EMAIL} -p ${DOCKER_PASSWORD}
            docker push [도커허브 계정]/[도커허브 레포지토리명]:[버전]
</code></pre>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/51c390dd-8fd5-4111-89be-6f1b75ab53d1/image.png" alt=""></p>
<hr>
<ul>
<li><strong>플러그인 사용하는 경우 ( 사용법은 k8s 배포 시 설명 예정 )</strong> ✅</li>
</ul>
<p><strong>1. Jenkins 관리 ➡ Plugins ➡ Avaiable plugins 클릭 후 Docker 검색</strong></p>
<p><strong>2.  Docker, Docker Commons, Docker Pipeline, docker-build-step 설치</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/ff53c41a-d680-4a90-8277-a8b31a6c987c/image.png" alt=""></p>
<hr>
<blockquote>
<p>🐸 <strong>Freestyle로 생성하여 k8s 배포하기 ( 도커 플러그인 사용 )</strong></p>
</blockquote>
<ul>
<li><p><strong>사전 준비 필요 사항 : DB 디플로이먼트 생성 
&nbsp;　　　　　　　　　　➡ ( 쿠버네티스로 3계층 아키텍처 구성하기 글 참고 )</strong></p>
</li>
<li><p><strong>master 노드에 대한 SSH 등록</strong></p>
</li>
<li><p><em>1) Jenkins 서버에서 등록 : <code>ssh-copy-id root@[master노드 IP]</code>*</em></p>
</li>
<li><p><em>2) Jenkins 관리 ➡ System ➡ Publish over SSH 에 master 노드 추가*</em>
<img src="https://velog.velcdn.com/images/passion_hd/post/ba30dbe5-4a62-44aa-9e18-cc2753a3717f/image.png" alt=""></p>
</li>
<li><p><strong>도커 플러그인 사용을 위한 설정</strong></p>
</li>
<li><p><em>Jenkins 관리 ➡ System ➡ Docker Builder에 URL 등록*</em></p>
</li>
<li><p><em>URL : <code>unix:///var/run/docker.sock</code>*</em></p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/bea754c5-d298-4d37-9157-f6dce2beaf89/image.png" alt=""></p>
<hr>
<ul>
<li><strong>백엔드 서버 구성하기( data.sql 파일 포함 시켜놨음 / 도커 컴포즈 글 참고 )</strong> ✅</li>
</ul>
<p><strong>1. 인텔리제이에서 Dockerfile 생성</strong></p>
<pre><code class="language-yaml">FROM openjdk:11-jdk-slim-stretch
COPY ./target/lonua-0.0.1-SNAPSHOT.jar lonua-0.0.1-SNAPSHOT.jar
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;/lonua-0.0.1-SNAPSHOT.jar&quot;]</code></pre>
<p><strong>2. 인텔리제이에서 backend-deployment.yml 생성</strong></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-deployment  // 디플로이먼트 이름 설정
spec:
  replicas: 2
  strategy:
    type: RollingUpdate     // 디플로이먼트 방식 지정
  minReadySeconds: 10
  selector:
    matchLabels:
      type: backend
  template:
    metadata:
      labels:
        type: backend
    spec:
      containers:
      - name: backend-container
        image: hyungdoyou/be:2.VERSION    // VERSION은 젠킨스에서 변수로 처리 예정
        envFrom:
          - configMapRef:
              name: backendconfig         // 백엔드 Config Map 이름 ( 미리 생성 해놔야 함 )
      terminationGracePeriodSeconds: 5</code></pre>
<hr>
<p><strong>3. 파이프라인 구성하기</strong></p>
<p>&nbsp;　<strong>1) 빌드 유발까지는 동일하다. ( 빌드 환경도 미체크 )</strong></p>
<p>&nbsp;　<strong>2) Build Steps - 1 : Maven 빌드 추가</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/796397e5-0684-4a3a-aa35-21598b3712ce/image.png" alt=""></p>
<hr>
<p>&nbsp;　<strong>3) Build Steps - 2 : 도커 이미지 빌드 추가</strong></p>
<p>&nbsp;&nbsp;　　➡ <strong>Docker command : <code>Create/build image</code> 선택</strong></p>
<p>&nbsp;&nbsp;　　➡ <strong>Build context folder : <code>$WORKSPACE/</code></strong></p>
<p>&nbsp;&nbsp;　　➡ <strong>Tag of the resulting docker image : 
  &nbsp;&nbsp;　　　<code>[도커허브 계정명]/[레포지토리명]:[버전앞숫자지정].$BUILD_NUMBER</code></strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/2cb8f2c9-8495-4d52-bbba-08ec661e08ed/image.png" alt=""></p>
<hr>
<p>&nbsp;　<strong>3) Build Steps - 3 : 도커 허브 Push 추가</strong>
&nbsp;&nbsp;　　➡ <strong>Docker command : <code>Push image</code> 선택</strong>
&nbsp;&nbsp;　　➡ <strong>Name of the image to push : <code>[도커허브 계정명]/[레포지토리명]</code></strong>
&nbsp;&nbsp;　　➡ <strong>Tag : <code>[버전 앞 숫자].$BUILD_NUMBER</code></strong>
&nbsp;&nbsp;　　➡ <strong>Docker registry URL : <code>https://hub.docker.com/</code></strong>
&nbsp;&nbsp;　　➡ <strong>Registry credentials : 생성한 docker-credential 선택</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/353c4050-0334-4797-9e19-42a13314d641/image.png" alt=""></p>
<hr>
<p>&nbsp;　<strong>4) 빌드 후 조치 작성</strong>
&nbsp;&nbsp;　　➡ <strong>SSH Server Name : 등록해둔 master 서버 선택</strong>
&nbsp;&nbsp;　　➡ <strong>Source files : <code>backend-deployment.yml</code></strong> 
&nbsp;&nbsp;　　➡ <strong>Remote directory : <code>/root/</code></strong>
&nbsp;&nbsp;　　➡ <strong>Exec command</strong></p>
<pre><code class="language-yaml">// 젠킨스 빌드 숫자로 VERSION 변수를 변경해주는 명령어
sed -i &quot;s/VERSION/$BUILD_ID/g&quot; /root/backend-deployment.yml  

// 디플로이먼트 실행 명령어
kubectl apply -f /root/backend-deployment.yml</code></pre>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/684c9b4d-a709-4187-9705-5f9516a3495c/image.png" alt=""></p>
<hr>
<p><strong>4. 지금 빌드 또는 깃허브에 Push 후 k8s 대시보드에서 확인</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/bc29dd13-7802-43a9-8408-c66a0ab115e6/image.png" alt=""></p>
<p><strong>5. k8s 대시보드에서 백엔드 서버 Service 생성</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: backend-svc
spec:
  selector:
    type: backend
  ports:
  - port: 8080
    targetPort: 8080</code></pre>
<hr>
<ul>
<li><strong>프론트엔드 서버 구성하기</strong> ✅</li>
</ul>
<p><strong>1. VS Code에서 frontend-deployment.yml 작성</strong></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-deployment
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
  revisionHistoryLimit: 1
  selector:
    matchLabels:
      type: frontend
  template:
    metadata:
      labels:
        type: frontend
    spec:
      containers:
      - name: frontend-container
        image: [도커허브 계정명]/[레포지토리명]:[버전 앞 숫자].VERSION</code></pre>
<p><strong>2. 파이프라인 구성의 Build Step 및 빌드 후 조치는 아래와 같다.</strong></p>
<p>&nbsp;　<strong>1) Build Step - 1</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/cee8cf54-9be9-475b-8942-081de89b5f90/image.png" alt=""></p>
<hr>
<p>&nbsp;　<strong>2) Build Step - 2</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/09be491d-fe40-463c-b0b3-b35a82f6cd05/image.png" alt=""></p>
<hr>
<p>&nbsp;　<strong>3) Build Step - 3</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/1206c977-b36e-4ad5-87c1-e1eb15634b69/image.png" alt=""></p>
<hr>
<p>&nbsp;　<strong>4) 빌드 후 조치</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/04ef8d50-a8f7-4bd1-b6a6-6070ad321534/image.png" alt=""></p>
<hr>
<p><strong>3. k8s 대시보드에서 프론트엔드 서버 Service 생성</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
 name: frontend-svc
spec:
 selector:
   type: frontend
 ports:
  - port: 80
    targetPort: 80
 type: LoadBalancer</code></pre>
<hr>
<blockquote>
<p>❗** Slack Notification 설정하기**❗</p>
</blockquote>
<p><strong>1. Slack 에서 새 워크스페이스 및 채널 생성</strong></p>
<p><strong>2. 채널 오른쪽 클릭 ➡ 채널 세부정보 보기 클릭</strong></p>
<p><strong>3. 통합에서 앱 추가 클릭</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/11b9d2a6-4150-4003-be4f-4fa795d89425/image.png" alt=""></p>
<p><strong>4. Incoming WebHooks 검색 후 설치</strong></p>
<p><strong>5. 채널에 포스트에서 원하는 채널 선택 후 &quot;수신 웹후크 통합 앱 추가&quot; 선택</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/c43615f8-eca7-4b0f-b089-68bf5f7a9a1c/image.png" alt=""></p>
<p>&nbsp;　➡ <strong>웹후크 URL을 Jenkins 대시보드에서 등록할 예정</strong></p>
<hr>
<p><strong>6. Jenkins 관리 ➡ Credentials ➡ (global) ➡ Add Credentials 클릭
&nbsp;　➡ Secret Text 선택</strong>
&nbsp;　➡ <strong>Secret에 위에서 나온 웹후그 URL의 <code>https://hooks.slack.com/services/</code> 뒤부분을 
&nbsp;　&nbsp;　입력</strong>
&nbsp;　➡ <strong>ID 입력 후 생성</strong></p>
<hr>
<p><strong>7. Jenkins 관리 ➡ Plugins ➡ Slack Notification 플러그인 설치</strong></p>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/05d74c80-eb41-45ba-a1ec-b7f32469f968/image.png" alt=""></p>
<hr>
<p><strong>8. 파이프라인 구성의 빌드 후 조치에 아래와 같이 설정 추가</strong></p>
<p>&nbsp;　<strong>1) 기본 설정 전부 체크</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/b8845bb3-bd0c-433a-be76-265b3cb7eef3/image.png" alt=""></p>
<hr>
<p>&nbsp;　<strong>2) 고급 설정 전부 체크 ( Include Custom Message 제외 )</strong></p>
<p>&nbsp;　<strong>3) Override url : <code>https://hooks.slack.com/services/</code></strong></p>
<p>&nbsp;　<strong>4) Credential : 위에서 생성한 slack-credential 선택</strong></p>
<p>&nbsp;　<strong>5) Channel / member id : 알람받을 채널 이름 입력</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/51ed4aed-a275-45c6-ba75-34bfb729bd69/image.png" alt=""></p>
<hr>
<p><strong>9. GitHub Push 시 Slack의 채널에 아래와 같이 알림이 오는지 확인</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/def5bbf9-c8e9-4d2e-a625-2dccb953dc0a/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jenkins - 1 ( 초기환경 설정 및 기본 파이프라인 구성 )]]></title>
            <link>https://velog.io/@passion_hd/Jenkins-1</link>
            <guid>https://velog.io/@passion_hd/Jenkins-1</guid>
            <pubDate>Wed, 21 Feb 2024 14:32:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>🦁 <strong>Jenkins 설치하기</strong></p>
</blockquote>
<ul>
<li><strong>사전 준비사항 : 가상머신 2대 준비 ( 나의 컴퓨터 사양을 고려해서 설정하였다. )</strong>
➡ <strong>Jenkins 서버 : CPU 2, 메모리 4GB</strong>
➡ <strong>배포 서버 : CPU 1, 메모리 2GB</strong>
➡ <strong>IP 설정 및 방화벽 끄기 ( Systemctl stop firewalld / setenforce 0 )</strong></li>
</ul>
<p>✅ <strong>Jenkins 설치 단계</strong></p>
<p><strong>1. 자바 설치 : <code>dnf install -y java-11-openjdk-devel</code></strong></p>
<p><strong>2. 레포지토리 추가</strong>
&nbsp;　➡ <code>rpm --import https://pkg.jenkins.io/redhat/jenkins.io-2023.key</code>
&nbsp;　➡ <code>cd /etc/yum.repos.d/</code>
&nbsp;　➡ <code>curl -O https://pkg.jenkins.io/redhat-stable/jenkins.repo</code></p>
<p><strong>3. 젠킨스 설치 : <code>dnf install jenkins</code></strong></p>
<p><strong>4. 젠킨스 기본 포트 변경 : <code>vi /usr/lib/systemd/system/jenkins.service</code></strong>
&nbsp;　➡ <strong>줄번호 표시 : <code>: set nu</code></strong>
&nbsp;　➡ <strong>70번 째 줄의 <code>Environment=&quot;JENKINS_PORT=8080&quot;</code> 포트번호를 <code>9090</code>으로 변경</strong>
&nbsp;　➡ <strong>사유 : 보통 스프링 부트 서버의 포트번호로 8080을 많이 사용하기 때문</strong></p>
<p>   <img src="https://velog.velcdn.com/images/passion_hd/post/835b3e27-7918-426d-bdc6-2a76c0ed0ab7/image.png" alt=""></p>
<p><strong>5. 젠킨스 시작 ( 컴퓨터 사양에 따라 시작되는데 시간이 다소 걸림 )</strong>
&nbsp;　➡ <strong><code>systemctl daemon-reload</code></strong>
&nbsp;　➡ <strong><code>systemctl restart jenkins</code></strong></p>
<p>** 6. 9090번 포트에 대한 VMWare 에서 포트포워딩 설정**</p>
<p><strong>7. 젠킨스 대시보드에 접속 : <code>젠킨스 서버 가상머신 IP:9090</code></strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/79e18558-9bd9-4c67-b28d-caa1f0fdad27/image.png" alt=""></p>
<p>&nbsp;　➡ <strong>초기 비밀번호 : <code>cat /var/lib/jenkins/secrets/initialAdminPassword</code> 으로 확인</strong></p>
<p>&nbsp;　➡ <strong>&quot; Install Suggested Plugins &quot; 를 클릭</strong>
       <img src="https://velog.velcdn.com/images/passion_hd/post/f4dcd08f-2c5e-4af4-a6f0-124f43d44dba/image.png" alt=""></p>
<hr>
<p>&nbsp;　➡ <strong>관리자 계정 생성 ( 이메일은 아무거나 입력해도 상관 없음 )</strong></p>
<p>&nbsp;　➡ <strong>그러면 아래와 같이 대시보드에 접속이 된다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/80d0153f-4a8f-43fb-9360-305d41f81ab1/image.png" alt=""></p>
<hr>
<blockquote>
<p>🐶** GitHub와 Jenkins 연동 설정하기**</p>
</blockquote>
<ul>
<li><strong>윈도우 컴퓨터 및 Jenkins 가상머신 컴퓨터 IP에 포트포워딩 설정하기 ( 9090 포트 )</strong></li>
</ul>
<p><strong>1. Jenkins에 git 설치 : <code>yum install git</code></strong></p>
<p><strong>2. GitHub에서 토큰 생성하기</strong>
&nbsp;　<strong>1) GitHub에서 오른쪽 상단 계정 아이콘 클릭 ➡ Settings 클릭</strong>
&nbsp;　<strong>2) 왼쪽 메뉴탭 제일 아래에서 &quot; Developer settings &quot; 클릭</strong>
&nbsp;　<strong>3) 왼쪽 메뉴탭에서 &quot; Personal access tokens &quot; 클릭</strong>
&nbsp;　<strong>4) Tokens(classic) ➡ Generate new token ➡ Generate new token(classic) 순으로 클릭</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/ac235109-59a6-4f76-ba3c-55b1e5c4e08e/image.png" alt=""></p>
<hr>
<p>&nbsp;　<strong>5) Note에 토큰 이름 입력 ➡ Expiration 에서 토큰 만료기한 선택 ➡ Select scopes에서 
 &nbsp;&nbsp;　　&quot; repo &quot; 와 &quot; admin:repo_hook &quot; 체크</strong>
 <img src="https://velog.velcdn.com/images/passion_hd/post/432d67c4-c2b4-414a-8dc2-7df30c76eff8/image.png" alt=""></p>
<p> <img src="https://velog.velcdn.com/images/passion_hd/post/1fe3c350-671d-4002-aeab-5eff7098abcd/image.png" alt=""></p>
<hr>
<p>   &nbsp;　<strong>6) Generate Token을 클릭하면 토큰이 생성되고, 생성된 토큰은 저장해 둔다. 
    &nbsp;　　( 잃어버리면 재생성해야됨 )</strong></p>
<hr>
<p><strong>3. Jenkins에서 API Key 생성하기</strong>
&nbsp;　<strong>1) Jenkins 대시보드에서 오른쪽 상단 사용자 프로필 클릭</strong> 
&nbsp;　<strong>2) 왼쪽 메뉴탭에서 설정 클릭 ➡ API Token에서 Add new Token 클릭 후 이름 입력</strong>
&nbsp;　<strong>3) 생성된 토큰 복사 후 Save</strong>
 <img src="https://velog.velcdn.com/images/passion_hd/post/1cf84f3e-30a6-4821-b922-606433becf49/image.png" alt=""></p>
<hr>
<p><strong>4. Jenkins에 GitHub 토큰 등록</strong></p>
<p>&nbsp;　<strong>1) Jenkins 대시보드 ➡ Jenkins 관리 ➡ System 클릭</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/e166a953-761f-470f-ab83-d615ed9730ca/image.png" alt=""></p>
<hr>
<p>&nbsp;　<strong>2) GitHub에서 Add GitHub Server 클릭</strong>
 &nbsp;&nbsp;　　➡ <strong>Name : 원하는 이름으로 적음</strong>
 &nbsp;&nbsp;　　➡ <strong>Add 클릭 후 Jenkins 클릭</strong>
 <img src="https://velog.velcdn.com/images/passion_hd/post/111012e4-e32c-4a23-898b-42a7223ccd1c/image.png" alt=""></p>
<hr>
<p> &nbsp;&nbsp;　　➡ <strong>Kind : Secret text 클릭</strong>
 &nbsp;&nbsp;　　➡ <strong>Secret : GitHub에서 생성한 토큰 입력</strong>
 &nbsp;&nbsp;　　➡ <strong>ID : 원하는대로 입력 후 Add 버튼 클릭</strong></p>
<p> <img src="https://velog.velcdn.com/images/passion_hd/post/ce781df4-1637-4872-82a8-28fe4cca0bdb/image.png" alt=""></p>
<hr>
<p> &nbsp;&nbsp;　　➡ <strong>Credentials 에서 추가한 것 선택 후 Test Connection 클릭</strong>
 &nbsp;&nbsp;　　➡ <strong>테스트 연결 결과가 아래와 같이 정상이면 Save 클릭</strong>  </p>
<p> <img src="https://velog.velcdn.com/images/passion_hd/post/95b62dae-429e-4f9a-b74b-6b61b258ab1a/image.png" alt=""></p>
<hr>
<p><strong>5. GitHub WebHook 설정하기</strong></p>
<p>&nbsp;　<strong>1) GitHub에 레포지토리를 생성 ➡ Settings에서 Webhooks ➡ Add webhook 클릭</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/52812bc4-bca4-486d-b60e-cca3feb4f97f/image.png" alt=""></p>
<p>&nbsp;　<strong>2) Payload URL 입력 : <code>http://[내 윈도우 컴퓨터의 공인 IP]:9090/github-webhook/</code></strong></p>
<p> &nbsp;&nbsp;　　➡ <strong>이때, 반드시 끝에 <code>/</code> 를 붙여줘야 된다.</strong></p>
<p>&nbsp;　<strong>3) Secret 부분에 Jenkins에서 생성한 Key 입력 후, &quot;Add webhook&quot; 클릭</strong></p>
<p>&nbsp;　<strong>4) 정상적으로 연동이 되면 아래처럼 나온다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/e39a7c10-244d-4208-a91b-261cf4d0cc0c/image.png" alt=""></p>
<hr>
<p><strong>6. Jenkins Pipeline 생성하여 테스트해보기</strong>
&nbsp;　<strong>1) Jenkins 대시보드에서 새로운 Item 클릭</strong>
&nbsp;　<strong>2) Pipeline 이름 입력 후 FreeStyle project 클릭 ➡ Ok 버튼 클릭</strong>
 &nbsp;　　➡ <strong>Pipeline 이름은 GitHub 레포지토리 이름과 맞춰주는 것이 좋다.</strong>
 <img src="https://velog.velcdn.com/images/passion_hd/post/41a5bf67-fea1-4339-aeb8-1c6dbd32b73c/image.png" alt=""></p>
<p>&nbsp;　<strong>3) GitHub Project 체크 후 레포지토리 URL 입력</strong>
 &nbsp;　　➡ <strong>왼쪽 메뉴탭에 GitHub 아이콘이 생기고, 클릭하면 입력한 URL로 이동하는 단순한 
  &nbsp;&nbsp;　　　기능이다.</strong></p>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/3e93443c-0b3c-40c9-90c3-b6b380facbab/image.png" alt=""></p>
<hr>
<p>&nbsp;　<strong>4) 소스 코드 관리에서 Git 체크 및 GitHub 레포지토리 주소 입력</strong> 
 &nbsp;　　➡ <strong>입력한 레포지토리에서 소스 코드를 Pull 해오기 때문에 중요한 설정이다.</strong>
 &nbsp;　　➡ <strong>이때 Branches to build 에는 실제로 코드를 Push 할 브랜치명으로 적는다.</strong> 
    <img src="https://velog.velcdn.com/images/passion_hd/post/55d8f522-8e7b-4288-83c5-11662a615238/image.png" alt=""></p>
<hr>
<p>&nbsp;　<strong>5) 빌드 유발에서 GitHub hook trigger for GITScm polling 체크</strong></p>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/d30efdd2-6593-415f-9301-47371197bd87/image.png" alt=""></p>
<hr>
<p>&nbsp;　<strong>6) 빌드 스탭에서 Add build step 클릭 후 Execute shell 클릭</strong>
&nbsp;　　➡ <strong>빌드 스탭은 실제 빌드할때 처리할 작업들을 설정해주는 단계이다.</strong>
&nbsp;　　➡ <strong>Execute shell은 빌드하는 서버, 즉 Jenkins 서버에서 실행할 동작들을 여기에 다 
&nbsp;&nbsp;　　　적어줄 수 있다.</strong>
 &nbsp;　　➡ <strong>테스트하기 위해 <code>echo test</code> 라고 적어보겠다.</strong></p>
<p>&nbsp;　<strong>7) VSCode에서 작성한 프론트엔드 서버 코드를 GitHub로 Push해보고 Jenkins로 알림이 
 &nbsp;　　오는지 확인한다.</strong></p>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/c9d5a584-7b02-43dd-9025-5f3ee9665ab0/image.png" alt=""></p>
<hr>
<blockquote>
<p>🐮 <strong>SSH로 접속하는 환경 설정하기</strong></p>
</blockquote>
<p><strong>1. Jenkins 서버에 플러그인 설치하기</strong>
&nbsp;　<strong>1) Jenkins 대시보드 ➡ Jenkins 관리 ➡ Plugins 클릭</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/8c803380-0498-4429-b66f-3a066f4a7bc9/image.png" alt=""></p>
<p>&nbsp;　<strong>2) Available plugins에서 <code>Publish Over SSH</code> 검색 후 Install 클릭</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/461fd520-0928-40d0-a8ae-9449ef4a5628/image.png" alt=""></p>
<p> &nbsp;　<strong>3) 설치가 끝나고 실행중인 작업이 없으면 Jenkins 재시작 클릭하여 재시작</strong></p>
<hr>
<ul>
<li><strong>EC2에서 서버를 실행할 경우</strong> ✅</li>
</ul>
<p><strong>1. EC2 생성 시 발급받은 <code>.ppk</code> 파일을 <code>.pem</code> 파일 형식으로 변환 ( 변환은 지난 글 참고 )</strong></p>
<p><strong>2. 생성한 키 파일을 Jenkins 서버로 FileZilla 프로그램 이용 옮기기</strong></p>
<p><strong>3. chmod 400 [옮긴 파일명]</strong></p>
<p><strong>4. 접속 테스트 : ssh ubuntu@[EC2 IP 주소] -i [옮긴 파일명]</strong></p>
<p><strong>5. Jenkins 설정 수정</strong></p>
<p>&nbsp;　<strong>1) Jenkins 대시보드 ➡ Jenkins 관리 ➡ System 클릭</strong></p>
<p>&nbsp;　<strong>2) Publish Over SSH 에 아래 내용 추가</strong>
 &nbsp;&nbsp;　　➡ <strong>Key : <code>.pem</code> 파일 내용 붙여넣기</strong></p>
<p>&nbsp;　<strong>3) SSH Servers 에서 추가 클릭</strong>
 &nbsp;&nbsp;　　➡ <strong>Name : 원하는 이름으로 지정 ( 서버의 이름으로 적는게 좋음 )</strong>
 &nbsp;&nbsp;　　➡ <strong>Hostname : EC2 의 IP 주소</strong>
 &nbsp;&nbsp;　　➡ <strong>Username : ubuntu</strong>
    <img src="https://velog.velcdn.com/images/passion_hd/post/9e306917-5cbc-4cd0-b2ff-41c61938ce6a/image.png" alt=""></p>
<p> &nbsp;&nbsp;　　➡ <strong>이 부분은 SSH로 접속할 서버에 대한 설정 부분이다. 나중에 접속할 서버가 
  &nbsp;&nbsp;&nbsp;　　　늘어나면, 여기에 추가해주면 된다.</strong></p>
<hr>
<ul>
<li><strong>CentOS 8 가상머신에서 서버를 실행할 경우</strong> ✅</li>
</ul>
<p><strong>1. Jenkins 서버에서 SSH Key 설정하기&nbsp;　**
&nbsp;　**1) ssh-keygen 입력 ➡ 엔터 3번 클릭</strong>
&nbsp;　<strong>2) ssh-copy-id root@[접속 할 가상머신 IP]</strong>
&nbsp;　<strong>3) yes 입력 후, 접속할 가상머신의 패스워드 입력</strong>
&nbsp;　<strong>4) ssh root@[가상머신 IP] 했을때 접속이 되면 성공한 것</strong>
&nbsp;　<strong>5) 접속해제 : <code>exit</code></strong></p>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/dd24bb29-670f-4c29-9dd8-d6421437f790/image.png" alt=""></p>
<hr>
<p><strong>2. Jenkins 설정 수정</strong>
&nbsp;　<strong>1) 젠킨스 서버에서 키파일 확인 : <code>cat ~/.ssh/id_rsa</code></strong>
&nbsp;&nbsp;　　➡ <strong>SSH 접속을 끊은 상태에서 해야된다.</strong></p>
<p>&nbsp;　<strong>2) Jenkins 대시보드 ➡ Jenkins 관리 ➡ System 클릭</strong></p>
<p>&nbsp;　<strong>3) Publish Over SSH 에 key 파일 내용 붙여넣기</strong></p>
<p>&nbsp;　<strong>4) SSH Servers 에서 추가 클릭</strong>
 &nbsp;&nbsp;　　➡ <strong>Name : 원하는 이름으로 지정 ( 서버의 이름으로 적는게 좋음 )</strong>
 &nbsp;&nbsp;　　➡ <strong>Hostname : 가상머신 IP 주소</strong>
 &nbsp;&nbsp;　　➡ <strong>Username :  root</strong>
 &nbsp;&nbsp;　　➡ <strong>Remote Directory : /</strong></p>
<p>&nbsp;　<strong>5) Test Configuration 클릭 시 <code>Success</code> 라고 뜨면 잘 설정된 것이니 저장한다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/224e1b09-cf1f-4ac8-b6c0-0edaa6dd5159/image.png" alt=""></p>
<hr>
<ul>
<li><strong>그러면 이제 Pipeline 구성에서 빌드 후 조치를 작성할 수 있다.  **
&nbsp;➡ **빌드 후 조치에서 Send build artifacts over SSH 클릭</strong>
&nbsp;➡ <strong>Name : 위에서 설정한 SSH 서버 이름 선택</strong>
&nbsp;➡ <strong>Source files : 옮길 파일명 // SSH 서버로 전송할 파일명을 입력</strong>
&nbsp;➡ <strong>Remove prefix : 파일명의 앞부분 제거하고 싶을때 사용</strong>
&nbsp;　<strong>ex) tartget/test.jar 로 Source files에서 설정했을때, 
&nbsp;　　　Remove prefix로 target/ 이라고 하면 실제로 전송되는 파일명인 test.jar만 
&nbsp;　　　남는다.</strong>
&nbsp;➡ <strong>Remove directiory : SSH 서버의 어떤 경로에 파일을 옮길지 지정</strong>
&nbsp;➡ <strong>Exec command : 파일을 옮기고 실행할 명령어들을 입력</strong></li>
</ul>
<ul>
<li><p><strong>아래처럼 입력 후 지금 빌드를 클릭해봤을때, SSH 서버의 <code>/abc</code> 경로로 파일이 옮겨진 것을 볼 수 있다.</strong> 
 <img src="https://velog.velcdn.com/images/passion_hd/post/c0b66137-a539-40aa-9319-ed424a8b24bf/image.png" alt="">
 <img src="https://velog.velcdn.com/images/passion_hd/post/faf3adf0-46eb-40f1-b784-0fa657793227/image.png" alt=""></p>
</li>
<li><p><strong>여기까지 하면, 기본적인 파이프라인 구성에 대해 알아보았고, 실제로 프론트 엔드 서버와 백엔드 서버를 파이프라인으로 구성해보겠다.</strong></p>
</li>
</ul>
<hr>
<blockquote>
<p>🐻 <strong>프론트엔드 서버 구성하기</strong></p>
</blockquote>
<p><strong>1. Jenkins 서버에 Node.js 설치하기</strong>
 &nbsp;➡ <code>curl -sL https://rpm.nodesource.com/setup_20.x | sudo -E bash -</code>
 &nbsp;➡ <code>sudo yum install -y nodejs</code></p>
<p><strong>2. 배포 서버 컴퓨터에 nginx 설치하기 : <code>yum install -y nginx</code></strong></p>
<p><strong>3. 배포 서버 컴퓨터에서 nginx 설정파일 수정하기 : <code>vi /etc/nginx/nginx.conf</code></strong>
 &nbsp; ➡ <strong>47 ~ 48번째 줄을 아래로 변경</strong></p>
<pre><code class="language-yaml">        location / {
                index index.html;
                try_files $uri $uri/ @rewrites;
        }

        location @rewrites {
                rewrite ^(.+)$ /index.html last;
        }
</code></pre>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/fe2fd9e2-a0fe-4fde-ab5a-ac4ab9d08ccc/image.png" alt=""></p>
<hr>
<p>*<em>4. 배포 서버 컴퓨터에서 nginx 시작 : <code>systemctl start nginx</code> *</em></p>
<p><strong>5. Jenkins 관리의 System에서 Publish over SSH 설정되어 있는지 확인</strong></p>
<p><strong>6. 파이프라인 구성에서 빌드 스탬에 Execute shell 구성에 아래 내용 추가</strong></p>
<pre><code class="language-yaml">npm i
rm -rf /var/lib/jenkins/workspace/[깃허브 레포지토리명]/dist
npm run build
cd /var/lib/jenkins/workspace/[깃허브 레포지토리명]/dist
tar -cvf dist.tar ./*    // dist 폴더를 압축
mv dist.tar ../          // 압축한 파일을 상위 폴더로 이동시킴 
                         // ( 빌드한 폴더라서 파일 전송 시 바로 파일명을 적어주기 위해 )</code></pre>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/bda1ff93-3127-4948-86bb-b2ecdbd1568c/image.png" alt=""></p>
<p> &nbsp;➡  <strong>빌드 후 조치에서 Source files 에 파일명을 적을때 기준 경로는 빌드 위치
 &nbsp;&nbsp;　( <code>var/lib/jenkins/workspace/[깃허브 레포지토리명]</code> ) 이다.</strong></p>
<p>&nbsp;➡  <strong>따라서, 해당 경로를 기준으로 파일명을 적어줘야 되는데, Remove prefix 설정을 
 &nbsp;&nbsp;　안해주기 위해서 위처럼 압축한 폴더를 빌드 위치로 이동시켰다.</strong></p>
<p>&nbsp;➡  <strong>만약, 이동을 안시켰다면 source files에 파일명을 <code>dist/dist.tar</code> 로 적어야 할 것이고,</strong> 
 &nbsp;&nbsp;&nbsp;&nbsp;  <strong>Remove prefix로 <code>dist/</code> 를 적어줘야 된다.</strong>
<br>
<strong>7. 빌드 후 조치 작성</strong>
 &nbsp;　➡  <strong>source files : <code>dist.tar</code></strong>
 &nbsp;　➡  <strong>Remote directory : <code>/usr/share/nginx/html/</code></strong>
 &nbsp;　➡  <strong>Exec command</strong>
 &nbsp;　 &nbsp;　<code>tar -xvf /usr/share/nginx/html/dist.tar -C /usr/share/nginx/html/</code>
 &nbsp;　 &nbsp;　<code>systemctl reload nginx</code>
<img src="https://velog.velcdn.com/images/passion_hd/post/84f1df62-8901-4fdc-9c83-c68d90aaa0e2/image.png" alt=""></p>
<hr>
<p><strong>8. GitHub에 Push해서 코드 수정 한 내용이 서버에 배포되는지 접속(배포서버IP:80)하여 확인</strong>
 &nbsp;　➡  <strong>배포 서버 IP의 80 포트에 대해 포트포워딩 설정 필요</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/7a6a2680-ddd1-4f65-bc62-fd592ab2613e/image.png" alt=""></p>
<hr>
<blockquote>
<p>🐨 ** 백엔드서버 구성하기**</p>
</blockquote>
<p><strong>1. 배포 서버에 openjdk 11 버전 설치하기 : <code>dnf install -y java-11-openjdk-devel</code></strong></p>
<p><strong>2. Jenkins에서 &quot;Maven&quot; 설정하기</strong> 
&nbsp;　<strong>1) Jenkins 관리 ➡ Tools ➡ Maven installations 에서 Add Maven 클릭</strong>
&nbsp;　<strong>2) Name : 원하는 이름 / Version : 선택 ( 나는 가장 최신 한단계 아래인 3.9.5 로 선택함 )</strong> 
<img src="https://velog.velcdn.com/images/passion_hd/post/8578d6ca-b777-4efa-b12c-55bef3211b32/image.png" alt="">
<img src="https://velog.velcdn.com/images/passion_hd/post/2721528e-5e18-4ec1-ad1f-7ebb41cf85f3/image.png" alt=""></p>
<hr>
<p><strong>3. 백엔드 파이프라인 생성 ( Freestyle project )</strong> </p>
<p><strong>4. 빌드 유발까지는 프론트엔드 파이프라인 구성과 동일하게 진행한다.</strong></p>
<p><strong>5. 빌드 스탭에서 Invoke top-level Maven targets 클릭 ( 메이븐 빌드 추가 부분 )</strong>
 &nbsp;　➡ <strong>Maven Version : 위에서 설정한 버전 클릭</strong>
 &nbsp;　➡ *<em>Goals : clean install 입력 *</em> // 기존 빌드 내용을 없애고 새롭게 빌드한다는 내용
<img src="https://velog.velcdn.com/images/passion_hd/post/05dc5ef9-0058-4c84-a79d-3098acbe7fef/image.png" alt=""></p>
<p><strong>6. 빌드 후 조치 작성</strong>
 &nbsp;　➡ <strong>Source files : <code>target/[ jar파일명 ]</code></strong>
 &nbsp;　➡ <strong>Remove prefix : <code>target/</code></strong>
 &nbsp;　➡ <strong>Remote directory : <code>/root/</code></strong>
 &nbsp;　➡ <strong>Exec command</strong></p>
<pre><code class="language-yaml">// 환경 변수 설정하는 부분 
export APP_PASSWORD=
export AWS_S3_ACCESS_KEY=
export AWS_S3_SECRET_KEY=
export BRAND_BUCKET=
export CLIENT_ID=
export EXPIRED_TIME=
export JWT_SECRET_KEY=
export MAIL_SENDER=
export MASTER=
export MASTER_PW=
export MASTER_URL=
export PORTONE_KEY=
export PORTONE_SECRETKEY=
export PRODUCT_BUCKET=
export PRODUCT_INTROD_BUCKET=
export REGION=
export REVIEW_BUCKET=
export SLAVE=
export SLAVE_PW=
export SLAVE_URL=

// 기존에 백그라운드에서 실행중인 프로그램이 있으면 끄고 재시작 시키는 쉘 스크립트
echo &quot;PID Check...&quot; &gt;&gt; /var/log/jenkins_test_spring.log

CURRENT_PID=$(netstat -anlp | grep 8080 | awk &#39;{print $7}&#39; | cut -d &#39;/&#39; -f1)

echo &quot;Running PID: {$CURRENT_PID}&quot; &gt;&gt; /var/log/jenkins_test_spring.log

if [ -z $CURRENT_PID ]; then
    echo &quot;Project is not running&quot; &gt;&gt; /var/log/jenkins_test_spring.log
else
    echo &quot;Project Killing&quot; &gt;&gt; /var/log/jenkins_test_spring.log
    kill -9 $CURRENT_PID
    sleep 3
fi

echo &quot;Deploy Project....&quot; &gt;&gt; /var/log/jenkins_test_spring.log

// 백그라운드에서 jar 파일을 실행시키도록 하는 명령어
nohup java -jar /root/lonua-0.0.1-SNAPSHOT.jar &gt;&gt; /var/log/jenkins_test_spring.log 2&gt;&amp;1 &amp;</code></pre>
<hr>
<p><strong>7. 배포 서버 컴퓨터에서 8080 포트가 사용중인지 확인</strong></p>
<p>&nbsp;　➡ <strong>명령어 설치 : <code>yum install -y lsof</code></strong></p>
<p>&nbsp;　➡ <strong>확인 : <code>lsof -i tcp:8080</code></strong></p>
<p>&nbsp;　➡ <strong>로그 파일 확인 : <code>cat /var/log/jenkins_test_spring.log</code></strong></p>
<p>&nbsp;　➡ <strong>종료 : <code>sudo kill -9 {PID번호}</code></strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Devops - 2 ( 프론트 엔드 서버 CI/CD 구축하기 )]]></title>
            <link>https://velog.io/@passion_hd/Devops-2-GitHub-Action-Selenium</link>
            <guid>https://velog.io/@passion_hd/Devops-2-GitHub-Action-Selenium</guid>
            <pubDate>Tue, 20 Feb 2024 14:42:43 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>** 실습 전 초기구성** 🧐</p>
</blockquote>
<ul>
<li><p>*<em>AWS EC2 1대 준비 *</em></p>
</li>
<li><p><strong>EC2 방화벽 끄기 : <code>sudo ufw disable</code></strong></p>
</li>
<li><p><strong>EC2에 도커 설치</strong></p>
<p><strong>1) 패키지 목록 업데이트 : <code>sudo apt-get update</code></strong></p>
<p><strong>2) 도커에 필요한 패키지 설치</strong>
&nbsp;　➡ <code>sudo apt-get install ca-certificates curl gnupg lsb-release</code></p>
<p><strong>3) curl -fsSL <a href="https://download.docker.com/linux/ubuntu/gpg">https://download.docker.com/linux/ubuntu/gpg</a> | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg</strong></p>
<p><strong>4) 도커 저장소 추가</strong></p>
<blockquote>
</blockquote>
<p>echo <br>&quot;deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] <a href="https://download.docker.com/linux/ubuntu">https://download.docker.com/linux/ubuntu</a> <br>$(lsb_release -cs) stable&quot; | sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null</p>
<p><strong>5) 도커 저장소 추가 후 패키지 목록 재 업데이트 : <code>sudo apt-get update</code></strong></p>
<p><strong>6) 도커 설치 : <code>sudo apt-get install docker-ce docker-ce-cli containerd.io</code></strong></p>
<p><strong>7) 도커가 정상적으로 설치됬는지 확인 : <code>sudo docker --version</code></strong></p>
</li>
</ul>
<hr>
<ul>
<li><p><strong>EC2에 도커 컴포즈 설치</strong></p>
<p><strong>1) 도커 컴포즈 설치</strong></p>
<blockquote>
<p>sudo curl <br>  -L &quot;<a href="https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$">https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$</a>(uname -s)-$(uname -m)&quot; <br>  -o /usr/local/bin/docker-compose</p>
</blockquote>
<p><strong>2) 실행권한 부여 : <code>sudo chmod +x /usr/local/bin/docker-compose</code></strong></p>
<p><strong>3) 도커 컴포즈가 정상적으로 설치됬는지 확인 : <code>docker-compose --version</code></strong></p>
</li>
</ul>
<hr>
<blockquote>
<p>🐮 ** VSCode에서 수정 후 깃허브 Push 시 자동으로 프론트엔드 서버에 배포하기**</p>
</blockquote>
<p><strong>1. 도커 컴포즈 파일 작성 : <code>vi devops.yml</code>  ( 야멜 파일 이름은 원하는 대로 지정 )</strong></p>
<pre><code class="language-yaml">version: &#39;3&#39;
services:
  frontend:
    container_name: frontend  // 컨테이너 이름을 frontend로 설정 ✅
    ports:
      - 8888:80   // 프론트엔드 서버를 원하는 포트에 포트 포워딩 설정 ✅
    // 도커 허브에 있는 이미지, 이때 버전은 latest 로 한다. ✅   
    image: [도커허브 계정명]/[레포지토리명]:latest
    depends_on:
      - backend

  backend:
    container_name: backend  // 컨테이너 이름을 backend로 설정 ✅
    ports:
      - 8080:8080   // 백엔드 서버를 원하는 포트에 포트 포워딩 설정 ✅
    image: [도커허브 계정명]/[레포지토리명]:[버전]
    environment:    // 환경변수 설정 ✅
      APP_PASSWORD: 
      AWS_S3_ACCESS_KEY: 
      AWS_S3_SECRET_KEY: 
      BRAND_BUCKET: 
      CLIENT_ID: 
      EXPIRED_TIME: 
      JWT_SECRET_KEY: 
      MAIL_SENDER: 
      MASTER: 
      MASTER_PW: 
      MASTER_URL: 
      PORTONE_KEY: 
      PORTONE_SECRETKEY: 
      PRODUCT_BUCKET: 
      PRODUCT_INTROD_BUCKET: 
      REGION: 
      REVIEW_BUCKET: 
      SLAVE: 
      SLAVE_PW: 
      SLAVE_URL:
</code></pre>
<hr>
<p><strong>2. EC2 보안그룹 - 인바운드 규칙 편집 **
 &nbsp;　➡ **80번 및 도커 컴포즈 파일에서 프론트서버 포트 포워딩 설정한 포트 ( 8888번 ) 허용</strong>
 <img src="https://velog.velcdn.com/images/passion_hd/post/60e3b1d6-4501-471f-a85a-254232107d4f/image.png" alt=""></p>
<hr>
<p><strong>3. 현재 사용자 도커 그룹에 추가 : <code>sudo usermod -aG docker $USER</code></strong></p>
<p>&nbsp;　➡ <strong>이부분은 실습 간 아래와 같은 오류가 등장하게 되어 해결하게된 내용이다.</strong></p>
<blockquote>
<p><strong>오류 내용</strong>
err: If it&#39;s at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
err: Couldn&#39;t connect to Docker daemon at http+docker://localhost - is it running?
err: 
err: If it&#39;s at a non-standard location, specify the URL with the DOCKER_HOST environment variable.</p>
</blockquote>
<hr>
<p><strong>4. VSCode 에서 백엔드 호출 URL 수정 : <code>const backend = &quot;http://[EC2 IP]:8888/api&quot;;</code></strong></p>
<p><strong>5. VSCode 에서 nginx 폴더 생성 및 nginx 설정파일 ( default.conf ) 생성</strong></p>
<pre><code class="language-yaml">server {
  listen       80;
  server_name  localhost;
  #access_log  /var/log/nginx/host.access.log  main;
  location /api {
      rewrite ^/api(.*)$ $1 break;
      proxy_pass http://backend:8080;  // 실행할 백엔드 컨테이너의 이름 
      proxy_set_header Host $http_host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Real-IP $remote_addr;
  }
  location / {
      alias   /usr/share/nginx/html/;
      try_files $uri $uri/ /index.html;
  }
  #error_page  404              /404.html;
  # redirect server error pages to the static page /50x.html
  #
  error_page   500 502 503 504  /50x.html;
  location = /50x.html {
      root   /usr/share/nginx/html;
  }
  # proxy the PHP scripts to Apache listening on 127.0.0.1:80
  #
  #location ~ \.php$ {
  #    proxy_pass   http://127.0.0.1;
  #}
  # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
  #
  #location ~ \.php$ {
  #    root           html;
  #    fastcgi_pass   127.0.0.1:9000;
  #    fastcgi_index  index.php;
  #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
  #    include        fastcgi_params;
  #}
  # deny access to .htaccess files, if Apache&#39;s document root
  # concurs with nginx&#39;s one
  #
  #location ~ /\.ht {
  #    deny  all;
  #}
}</code></pre>
<ul>
<li><p><strong>항상 문제가 일어나는 nginx의 프록시 설정 부분이다. 이번에도 역시나, 쉽지 않았다.</strong></p>
</li>
<li><p><strong>먼저, 지금까지 EC2에서 서버를 배포했을때, 프론트엔드 서버와 백엔드 서버가 동일한 EC2에서 실행되다 보니, 프록시 설정에서 백엔드 호출 url을 <code>http://localhost:8080</code> 으로 했을때, 호출이 잘되었다.</strong></p>
</li>
<li><p><strong>하지만, 밑에서 GitHub Workflow를 실행시키고, EC2에서 도커 컴포즈 파일이 실행되어 프론트엔드 서버와 백엔드 서버가 정상적으로 동작은 하는데, 백엔드 서버 호출에서 502 Bad GateWay가 등장하였다.</strong></p>
</li>
<li><p><strong>그동안, 많은 실습으로 이 에러는 nginx의 프록시 서버 설정 문제라는 것은 알고 있었다.</strong></p>
</li>
<li><p><strong>그래서, localhost를 테스트겸 EC2의 IP주소로 설정해봤다. 그랬더니, 504 타임아웃 에러가 등장하였다. 프록시 설정 문제는 아니라는 것을 깨닫고, EC2의 인바운드 규칙 편집에서 8080 포트를 허용시켜줘 봤더니 정상적으로 통신이 되는 것이다.</strong></p>
</li>
<li><p><strong>여기서 든 의문, localhost나 EC2의 IP 주소나 어차피 동일한 컴퓨터이기 때문에 똑같은거 아니었나? 그동안 그렇게 이해하고 사용했었는데, 왜 이번에는 안되는 걸까?</strong></p>
</li>
<li><p><strong>그때 문득 머릿속을 스쳐지나가는게 도커 컨테이너를 통한 서버 실행이었다. 
즉, 도커 컴포즈 파일로 서버를 실행시킨다는 것은 EC2에서 서버를 실행시키는게 아니라, EC2 안에서 도커 컨테이너를 실행시켜서 서버를 실행시키는 것이다.</strong></p>
</li>
<li><p>*<em>각각의 도커 컨테이너는 내부 IP 주소를 가지고 있고, 그렇다 보니 localhost를 호출한다는건 프론트엔드 도커 컨테이너 자신을 호출하는 것이다. *</em></p>
<p><strong>그리고, EC2의 IP주소를 직접 호출 했을때는, 8080 포트를 포트포워딩으로 설정하여 백엔드 도커 컨테이너의 내부 IP로 찾아가게 되어 요청이 됬던 것이다.</strong></p>
</li>
<li><p><strong>EC2에서 서버를 실행한다고 생각한 나의 패착이었다... 그래서 지난 도커 실습때로 되돌아가서, 도커 컴포즈로 컨테이너를 실행 시 컨테이너 내부끼리는 컨테이너 이름으로 통신이 가능하다는 것을 알고 있었기에, 컨테이너 이름으로 호출을 해보니 정상적으로 호출이 되는 것을 확인할 수 있었다.</strong></p>
</li>
</ul>
<hr>
<p><strong>6. VSCode에서 Dockerfile 생성 및 작성 ( dist 폴더 안 모든 파일 nginx 서버에 추가 )</strong></p>
<pre><code class="language-yaml">FROM nginx:latest
 ADD ./dist/css /usr/share/nginx/html/css
 ADD ./dist/fonts /usr/share/nginx/html/fonts
 ADD ./dist/img /usr/share/nginx/html/img
 ADD ./dist/js /usr/share/nginx/html/js
 ADD ./dist/styles.css /usr/share/nginx/html/styles.css
 ADD ./dist/logo.png /usr/share/nginx/html/logo.png
 RUN rm -rf /usr/share/nginx/html/index.html
 ADD ./dist/index.html /usr/share/nginx/html/index.html
 RUN rm -rf /etc/nginx/conf.d/default.conf
 ADD ./nginx/default.conf /etc/nginx/conf.d/default.conf
 CMD [&quot;nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]</code></pre>
<hr>
<p><strong>7. VSCode 에서 <code>.github/workflows</code> 폴더 생성 후 GitHub Workflow 작성</strong></p>
<pre><code class="language-yaml">name: frontend devops

on:
  push:
    branches: [main]

jobs:
  deploy:
    name: frontend test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository    // git clone 과 같은 역할 ✅
        uses: actions/checkout@v3

      - name: Install dependencies   // npm i 수행 ✅
        run: npm install

      - name: Build                  // npm run build 수행 ✅
        run: npm run build

      - name: Docker Build and Push   // 도커 이미지 빌드 및 푸쉬 ✅
        script: |
          cd devops
          sudo docker build --tag hyungdoyou/fe:latest .
          sudo docker login -u ${{ secrets.DOCKER_EMAIL }} -p ${{ secrets.DOCKER_PASSWORD }}
          sudo docker push hyungdoyou/fe:latest

      - name: Deploy Docker Compose   // EC2에 접속하여, 도커 컴포즈 파일 실행 ✅
        uses: appleboy/ssh-action@v0.1.3
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.REMOTE_PORT }}
          script: |
            cd devops
            docker-compose -f /home/ubuntu/devops.yml pull
            docker-compose -f /home/ubuntu/devops.yml up --force-recreate
</code></pre>
<p>&nbsp;　➡ <strong>환경변수는 지난 글에서 작성한 4개의 환경변수에 도커 허브 로그인을 위한 변수 2개만 
 &nbsp;&nbsp;　　추가하였다.</strong></p>
<p>&nbsp;　➡ <strong>수업시간에 실습할 때는, 워크플로우를 이렇게 세분화 시키지 않고 EC2 안에서 위의 
 &nbsp;&nbsp;　　모든 과정을 실행토록 했었다. 그랬더니, 무료 EC2를 사용하다 보니 사양이 
 &nbsp;&nbsp;　　좋지 않아서, EC2가 계속 멈췄다.</strong></p>
<p>&nbsp;　➡ <strong>그래서 이 부분을 해결하고자 EC2에서는 도커 컴포즈 파일 실행만 시키고, 나머지는 
 &nbsp;&nbsp;　　깃허브에서 동작토록 작성한 것이다.</strong></p>
<p>&nbsp;　➡ *<em>그 결과, 아주 잘 동작하였다. 하지만 깃허브 액션도 일정 크기 이상 실행시키면 돈을 
 &nbsp;&nbsp;　　내야 된다고 한다. *</em></p>
<hr>
<blockquote>
<p>🐯** Selenium 으로 프론트 엔드 테스트 코드 작성하기**</p>
</blockquote>
<ul>
<li><p><strong>Selenium 이란❓</strong></p>
</li>
<li><p><em>Selenium은 웹 애플리케이션을 자동화하기 위한 도구 중 하나로, 웹 브라우저를 제어하여 테스트를 자동으로 수행할 수 있다. 따라서, Vue.js와 Selenium을 함께 사용하여 Vue.js 애플리케이션을 자동으로 테스트할 수 있다.*</em></p>
</li>
<li><p><strong>Selenium 설치하기 : <code>npm i selenium-webdriver</code></strong></p>
</li>
<li><p><strong>Selenium 실습하기 - 1단계</strong>💡</p>
</li>
</ul>
<p><strong>1. test.js 파일 작성</strong></p>
<pre><code class="language-js">const {Builder} = require(&#39;selenium-webdriver&#39;);

(async function example() {
    let driver = await new Builder()
    .forBrowser(&#39;chrome&#39;)
    .build();
    await driver.get(&#39;https://테스트할 페이지 URL/&#39;);
})();
</code></pre>
<hr>
<p><strong>2. 테스트 파일 실행 : <code>node test.js</code></strong></p>
<p>&nbsp;　➡ <strong>그러면 해당 URL 화면이 등장할 것이다.</strong></p>
<hr>
<p><strong>3. 테스트해볼 페이지로 이동하여 기능들을 테스트 해본다.</strong></p>
<pre><code class="language-js">const {Builder, By} = require(&#39;selenium-webdriver&#39;);

(async function example() {
    let driver = await new Builder()
    .forBrowser(&#39;chrome&#39;)
    .build();
    await driver.get(&#39;https://테스트할 페이지 URL/UserLogIn&#39;);
    // 웹브라우저 화면 크기를 조정하고 싶을때 설정
    await driver.manage().window().setRect({ width: 1200, height: 900 });

    // 해당 id와 동일한 이름을 갖는 요소를 찾음
    const input_id = await driver.findElement(By.id(&#39;custId&#39;));
    // 찾은 부분에 해당 내용을 입력
    input_id.sendKeys(&#39;tester01@gmail.com&#39;);

    const input_pw = await driver.findElement(By.id(&#39;custPw&#39;));
    input_pw.sendKeys(&#39;Tester01!&#39;);

    // 해당 클래스명과 동일한 이름을 갖는 요소들을 찾음
    const login_btn = await driver.findElements(By.className(&#39;btn full_width black&#39;));
    // 찾은 요소들 중 첫번째 요소를 클릭
    login_btn[0].click();

    // await driver.manage().setTimeouts({implicit: 5000});
})();</code></pre>
<p>&nbsp;　➡ <strong>이것은 ID와 PW를 입력 후 로그인 버튼을 클릭하는 테스트 코드이다.</strong></p>
<p> &nbsp;　➡ <strong>중요한것은 DOM과 마찬가지로, 테스트할 부분을 찾아서 처리해주는 것인데, Selenium 
 &nbsp;&nbsp;　　자체적으로 쓰는 문법들이 있어서 공식 홈페이지에서 잘 찾아봐야 되는 것 같다.</strong></p>
<hr>
<ul>
<li><strong>Selenium 실습하기 - 2단계</strong> 💡</li>
</ul>
<p><strong>1. 패키지 설치 : <code>npm i jest</code></strong></p>
<p><strong>2. package.json에서 scripts 마지막 줄에 아래처럼 추가</strong></p>
<pre><code class="language-js">  &quot;scripts&quot;: {
    &quot;serve&quot;: &quot;vue-cli-service serve&quot;,
    &quot;build&quot;: &quot;vue-cli-service build&quot;,
    &quot;lint&quot;: &quot;vue-cli-service lint&quot;,
    &quot;test&quot;: &quot;jest&quot;  // 추가한 부분 🔥
  },</code></pre>
<p><strong>3. 테스트 파일 생성 : <code>login.test.js</code></strong></p>
<p><strong>4. 테스트 파일 작성 ( 로그인 테스트에서 정상 케이스에 대한 테스트 코드 )</strong></p>
<pre><code class="language-js">import { describe, expect, test, beforeAll, afterAll } from &quot;@jest/globals&quot;;
const {Builder, By, until} = require(&#39;selenium-webdriver&#39;);
const chrome = require(&quot;selenium-webdriver/chrome&quot;);

describe(&quot;로그인&quot;, () =&gt; {
    let driver;
    beforeAll(async () =&gt; {
      driver = await new Builder()
        .forBrowser(&quot;chrome&quot;)
        .setChromeOptions(
          new chrome.Options().addArguments(&quot;--headless&quot;)  // 크롬을 띄우지 않도록 설정
        )
        .build();

      await driver.get(&quot;https://테스트할 페이지 URL/UserLogIn&quot;);
      // 이부분은 내가 테스트하는 페이지에 문제가 있어서 설정한 것이라 안해줘도 된다.
      await driver.manage().window().setRect({ width: 1200, height: 900 });
    }, 30000);

    afterAll(async () =&gt; {
      await driver.quit();
    }, 40000);

    test(&quot;정상 케이스 테스트&quot;, async () =&gt; {
            const input_id = await driver.findElement(By.id(&#39;custId&#39;));
            input_id.sendKeys(&#39;tester01@gmail.com&#39;);

            const input_pw = await driver.findElement(By.id(&#39;custPw&#39;));
            input_pw.sendKeys(&#39;Tester01!&#39;);

            const login_btn = await driver.findElements(By.className(&#39;btn full_width black&#39;));
            login_btn[0].click();

            // 이부분은 로그인 성공 시 화면이 너무 빠르게 전환되어
            // 해당 클래스 이름이 화면에 등장할때까지 기다리도록 설정하는 것이다.
            await driver.wait(until.elementLocated(By.className(&#39;category-title&#39;)), 3000); 
            // 로그인에 성공하면 메인 페이지로 이동하고, 
            // 그러면 해당 이름의 클래스가 로드되기 때문에 이것으로 로그인 성공여부를 결정해봤다.
            const main_page = await driver.findElement(By.className(&#39;category-title&#39;));
            // 가져온 요소가 NULL이 아니라면 성공으로 판단
            expect(main_page).not.toBeNull();            
    });
})</code></pre>
<p><strong>5. 테스트 실행 : npm test</strong>
&nbsp;　➡ <strong>지금은 만든 테스트 파일이 <code>login.test.js</code> 1개지만, 이 명령어 하나로 test.js의 이름을 
 &nbsp;　　가지고 있는 모든 테스트 파일이 실행된다.</strong></p>
<hr>
<ul>
<li><strong>Selenium 실습하기 - 3단계</strong> 💡</li>
</ul>
<p><strong>1. 테스트 결과를 파일로 만들어 주는 패키지 설치 : <code>npm i jest-junit</code></strong></p>
<p><strong>2. <code>jest.config.js</code> 파일 작성</strong></p>
<pre><code class="language-js">const config = {
  reporters: [
    &#39;default&#39;,
    [&#39;jest-junit&#39;, {outputDirectory: &#39;test-results&#39;, outputName: &#39;report.xml&#39;}],
  ],
};
module.exports = config;</code></pre>
<p>&nbsp;　➡ <strong>test-results 라는 폴더 밑에 report.xml 파일을 생성토록 하는 설정</strong>
 &nbsp;&nbsp;　　<strong>( 폴더 및 생성되는 파일이름은 설정해주기 나름 )</strong></p>
<p>&nbsp;　➡ <strong>설정한대로 test-results 폴더를 생성한다.</strong></p>
<p><strong>3. 테스트 파일 작성 ( 위의 테스트 파일에 로그인 실패 테스트 파일 추가 )</strong></p>
<pre><code class="language-js">import { describe, expect, test, beforeAll, afterAll } from &quot;@jest/globals&quot;;
const {Builder, By, until} = require(&#39;selenium-webdriver&#39;);
const chrome = require(&quot;selenium-webdriver/chrome&quot;);

describe(&quot;로그인&quot;, () =&gt; {
    let driver;
    beforeAll(async () =&gt; {
      driver = await new Builder()
        .forBrowser(&quot;chrome&quot;)
        .setChromeOptions(
          new chrome.Options().addArguments(&quot;--headless&quot;)
        )
        .build();

      await driver.get(&quot;https://테스트할 페이지 URL/UserLogIn&quot;);
      await driver.manage().window().setRect({ width: 1200, height: 900 });
    }, 30000);

    afterAll(async () =&gt; {
      await driver.quit();
    }, 40000);

    test(&quot;패스워드 오류 테스트&quot;, async () =&gt; {
            const input_id = await driver.findElement(By.id(&#39;custId&#39;));
            input_id.sendKeys(&#39;tester01@gmail.com&#39;);

            const input_pw = await driver.findElement(By.id(&#39;custPw&#39;));
            input_pw.sendKeys(&#39;Tester02!&#39;);

            const login_btn = await driver.findElements(By.className(&#39;btn full_width black&#39;));
            login_btn[0].click();

            // alert 창 뜨는 것 처리
            await driver.wait(until.alertIsPresent());

            let alert = await driver.switchTo().alert();

            // alert 창의 메시지 뽑아냄
            let alertText = await alert.getText();

            // alert 창수락 버튼
            await alert.accept();

            // alert창에 해당 내용을 포함하고 있으면 로그인 실패 테스트 성공
            expect(alertText).toContain(&#39;이메일과 비밀번호가 일치하지 않습니다.&#39;);   
    });
})</code></pre>
<p>&nbsp;　➡ <strong>로그인 실패 시 alert창을 띄우도록 개발하였었는데, 처음에 이 alert창 때문에 테스트가 
 &nbsp;&nbsp;　　계속 실패하였다. 그러다가 공식 홈페이지에서 처리하는 방법을 찾아서 위 처럼 작성한 
 &nbsp;&nbsp;　　것이다.</strong></p>
<p> &nbsp;　➡ <strong>테스트 실행 결과는 아래와 같다.</strong>
 <img src="https://velog.velcdn.com/images/passion_hd/post/993d0e41-5cc7-40a8-b337-0c8a511538e8/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/15cb8bbc-2d1a-4871-97aa-3c19c8764865/image.png" alt=""></p>
<hr>
<ul>
<li><strong>Selenium 실습하기 - 최종</strong> 💡</li>
</ul>
<p><strong>기존에 작성한 GitHub Workflow 에 테스트 관련 내용 추가</strong></p>
<pre><code class="language-yaml">name: frontend devops

on:
  push:
    branches: [main]

permissions: write-all    // 추가한 부분 🔥

jobs:
  deploy:
    name: frontend test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Install Node.js
        uses: actions/setup-node@v3
        with:
          node-version: &quot;v20.8.1&quot;

      - name: Install dependencies
        run: npm install

      - name: Build
        run: npm run build

      - name: result   // 추가한 부분 🔥
        uses: EnricoMi/publish-unit-test-result-action@v1
        with:
          files: &#39;test-results/*.xml&#39;

      - name: Docker Build and Push
        run: |
          sudo docker build --tag hyungdoyou/fe:latest .
          sudo docker login -u ${{ secrets.DOCKER_EMAIL }} -p ${{ secrets.DOCKER_PASSWORD }}
          sudo docker push hyungdoyou/fe:latest

      - name: Deploy Docker Compose
        uses: appleboy/ssh-action@v0.1.3
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.REMOTE_PORT }}
          script: |
            sudo docker-compose -f /home/ubuntu/devops.yml pull
            sudo docker-compose -f /home/ubuntu/devops.yml up --force-recreate
</code></pre>
<br>

<p>&nbsp;　➡ <strong>깃허브로 푸쉬해보면 정상적으로 실행되고, <code>Unit Test Results</code> 를 클릭하면 아래처럼 
 &nbsp;&nbsp;　　테스트 결과까지 추가로 나타나게 된다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/06d6dc7c-1a8e-4e55-b5df-67712dd8ff96/image.png" alt=""></p>
<p>&nbsp;　➡ <strong>지금은 테스트 파일을 2개만 만들었는데, 각 페이지마다 테스트 파일을 만들어 놓으면, 
 &nbsp;&nbsp;　　코드 수정 후 푸쉬 시 자동으로 테스트를 실행하고, 서버를 배포하게 되므로 최종적으로 
 &nbsp;&nbsp;　　프론트엔드 서버의 CI/CD 환경 구축이 완성된다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Devops - 1 ( Ansible, Github Action )]]></title>
            <link>https://velog.io/@passion_hd/Devops-1-Ansible-Github-Action</link>
            <guid>https://velog.io/@passion_hd/Devops-1-Ansible-Github-Action</guid>
            <pubDate>Mon, 19 Feb 2024 14:32:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>** Ansible (앤서블) 이란❓**</p>
</blockquote>
<p><strong>앤서블(Ansible)은 리눅스와 유닉스 기반 시스템의 설정 및 배포 작업을 자동화하기 위한 IT 자동화 도구 중 하나이다. 앤서블은 에이전트(agent)가 필요하지 않으며, YAML 형식으로 작성된 <code>Playbook</code>을 사용하여 간단하게 배포 작업을 수행할 수 있다.</strong></p>
<p><strong>또한 <code>멱등성</code> ( Idempotency, 여러 번 적용해도 결과가 동일하며, 수정된 부분이 있다면 그 부분만 새롭게 반영됨 )의 특징이 있으며, 데이터 전송을 위해 OpenSSH를 이용한다.</strong>
<br>
➡ <strong>에이전트란❓</strong></p>
<p><strong>다른 프로그램 또는 시스템과 통신하여 작업을 수행하거나 정보를 수집하는 소프트웨어이다. 클라이언트가 서버에 요청을 보내고, 서버는 그 요청에 따라 작업을 수행하고 결과를 반환하는데 에이전트는 이러한 요청을 받아서 작업을 수행하는 프로그램을 말한다.</strong></p>
<p><strong>하지만, 앤서블은 SSH를 통해 원격 시스템에 연결 및 관리하므로, 에이전트를 설치하거나 구성할 필요가 없다.</strong></p>
<hr>
<blockquote>
<p><strong>Ansible의 기본 개념 및 용어</strong> 🧐</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/bb5fb11d-0939-42dc-98a8-0e0b3d1113d9/image.png" alt=""></p>
<ul>
<li><p><strong>제어 노드(Control node)</strong> ✅
➡ <strong>앤서블을 실행하는 노드이다. 제어 노드에서 명령어로 매니지드 노드들을 관리한다.</strong></p>
</li>
<li><p><strong>매니지드 노드(Managed node)</strong> ✅
➡ <strong>앤서블로 관리하는 서버 노드이다. 매니지드 노드는 호스트라고도 한다. 
&nbsp;　매니지드 노드에는 앤서블이 설치 되지 않는다.</strong></p>
</li>
<li><p><strong>인벤토리(Inventory)</strong> ✅
➡ <strong>호스트 파일이라고도 하며, 매니지드 노드 목록을 인벤토리라고 한다.</strong>
➡ <strong>앤서블에 의해 제어될 대상을 정의한다. 각 매니지드 노드에 대한 IP 주소, 
&nbsp;　호스트 정보, 변수와 같은 정보를 지정할 수 있다.</strong></p>
<p>➡ <strong>인벤토리 yaml 파일의 예시는 아래와 같다.</strong> 💻</p>
<pre><code class="language-yaml">all:
hosts:                // 그룹을 구성하기 위한 모든 서버의 IP 지정
  77.77.77.200:   
  77.77.77.201:
  77.77.77.202:
children:
  webservers:         // 이름 짓기 나름 ( 그룹 이름 )
    hosts:            // 웹서버가 2개라면 아래처럼 2개 적을 수 있음. 여러개 가능
      77.77.77.200:
      77.77.77.201:
  dbservers:          // 이름 짓기 나름 ( 그룹 이름 )
    hosts:
      77.77.77.202:
</code></pre>
</li>
</ul>
<hr>
<ul>
<li><p><strong>플레이북(Playbook)</strong> ✅</p>
<p>➡ <strong>플레이북은 인벤토리 파일에서 정의한 대상들이 무엇을 수행할 것인지 정의하는 
&nbsp;　역할을하며, yaml 파일 형식으로 작성한다.</strong> </p>
<p>➡ <strong>앤서블을 사용하려면 이 플레이북을 잘 다룰줄 알아야하며, 단독으로 사용되는 것이 
&nbsp;　아닌 인벤토리와 플레이북의 조합으로 같이 사용한다.</strong></p>
<p>➡ <strong>왠만한 명령어에 대한 플레이북은 홈페이지에서 찾아볼 수 있으며, 만약 없다면 
&nbsp;　<code>shell</code> 을 사용하여 명령어를 실행시킬 수 있다.</strong></p>
<p>➡ <strong>플레이북 yaml 파일의 예시는 아래와 같다.</strong>
```yaml</p>
</li>
<li><p>hosts: [&quot;77.77.77.200&quot;]
tasks:</p>
<ul>
<li>name: define hostname   // 적절한 이름 임의 지정
shell: |                // 플레이북 명령어가 없을 때 명령어를 직접 칠때 사용
  hostnamectl set-hostname worker03
become: yes             // 관리자 권한으로 실행 의미</li>
<li>name: stop firewalld    // 플레이북 명령어 사용
service:
  name: firewalld
  state: stopped<pre><code></code></pre></li>
</ul>
</li>
</ul>
<hr>
<ul>
<li><p><strong>태스크(Task)</strong> ✅
➡ <strong>앤서블의 작업 단위. 애드훅(ad-hoc)명령을 사용하여 단일 작업을 한 번 
&nbsp;　실행할 수 있다.</strong></p>
</li>
<li><p><strong>모듈(Module)</strong> ✅
➡ <strong>앤서블이 실행하는 코드 단위. 플레이북에서 Task가 어떻게 수행될지를 정한다.</strong></p>
</li>
</ul>
<hr>
<blockquote>
<p>🦁** Ansible 을 사용하기 위한 기본 설정**</p>
</blockquote>
<ul>
<li><p><strong>앤서블은 SSH로 제어노드와 매니지드 노드가 연결된다.</strong></p>
</li>
<li><p><strong>따라서, 앤서블을 사용하기 전에 각 서버에 <code>authorized_keys</code> 를 추가해준다.</strong></p>
</li>
<li><p><strong>나는 CentOS 8 가상머신 2대를 준비해놨다. ( IP 설정 완료 )</strong>
&nbsp;➡ <strong>매니지드 노드 : nginx / 제어 노드 : ansible</strong></p>
</li>
</ul>
<hr>
<p><strong>1. 제어 노드에서 SSH로 접속한다 : <code>ssh root@[ 매니지드 노드 IP 주소 ]</code></strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/7e6502d8-57d8-4923-baf4-eb45166940fb/image.png" alt=""></p>
<p>➡ <strong>접속에 성공하면 제어노드에서 매니지드 노드로 바뀐것을 위처럼 볼 수 있다.</strong></p>
<hr>
<p><strong>2. 제어노드에서 키를 생성한다.</strong></p>
<ul>
<li><p><strong>생성하기 전, 매니지드 노드에서 키를 확인해보면 없다고 아래처럼 출력되고 있다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/c4a34a2c-effb-49a8-85ef-cbd2a1a0d969/image.png" alt=""></p>
</li>
<li><p><strong>제어노드에서 키 생성 : <code>ssh-keygen</code></strong>
➡ <strong>질문이 2개 정도 나오는데, 그냥 아무것도 입력하지 않고 Enter를 누르면 된다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/ce91ac87-3fea-40b2-8a82-fd3fceb7ef9f/image.png" alt=""></p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/a1c98a8d-10f2-47cc-809b-8ef9a0ca76a7/image.png" alt=""></p>
<hr>
<p><strong>3. 제어노드에서 생성한 키를 복사한다. : <code>ssh-copy-id root@[ 매니지드 노드 IP 주소 ]</code></strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/16ce4c8d-42f5-4d2e-ab55-6618ecc9e00d/image.png" alt=""></p>
<p>➡ <strong>매니지드 노드에서 .ssh 파일을 확인해보면 <code>authorized_keys</code> 가 생겼을 것이다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/a6d017ab-7050-4636-a714-2e21897eead7/image.png" alt=""></p>
<hr>
<blockquote>
<p>🐼 <strong>Ansible 설치하기 ( 제어 노드 )</strong></p>
</blockquote>
<ul>
<li><p><code>dnf -y install centos-release-ansible-29</code></p>
</li>
<li><p><code>sed -i -e &quot;s/enabled=1/enabled=0/g&quot; /etc/yum.repos.d/CentOS-SIG-ansible-29.repo</code></p>
</li>
<li><p><code>dnf --enablerepo=centos-ansible-29 -y install ansible</code></p>
</li>
</ul>
<hr>
<ul>
<li><strong>테스트 해보기</strong></li>
</ul>
<p><strong>1. 제어노드에서 인벤토리 ( server.yml ) 생성</strong></p>
<pre><code class="language-yaml">all:
  hosts: 
    77.77.77.200:   
    77.77.77.201:
  children:
    webservers:
      hosts:
        77.77.77.200:
        77.77.77.201:
</code></pre>
<p><strong>2. 인벤토리 테스트 : <code>ansible webservers -i server.yml -m ping</code></strong></p>
<p>&nbsp;　➡ <strong>webservers : 인벤토리에 작성한 그룹명</strong></p>
<p>&nbsp;　➡ <strong>server.yml : 인벤토리명</strong></p>
<p>&nbsp;　➡ <strong>-m : 모듈 실행</strong></p>
<p>&nbsp;　➡ <strong>ping : 핑 보내보는 명령어</strong></p>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/8460fcc7-992c-47b8-afae-e0a68a54bbbf/image.png" alt=""></p>
<hr>
<p><strong>3. 제어 노드에서 플레이북 작성 ( nginx 설치 ) : <code>vi nginx-playbook.yml</code></strong></p>
<pre><code class="language-yaml">- hosts: [&quot;77.77.77.200&quot;]
  tasks:
    - name: install nginx
      yum:
        name: httpd
        state: latest</code></pre>
<hr>
<p><strong>4. 플레이북 실행 : <code>ansible-playbook -i server.yml ./nginx-playbook.yml</code></strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/ef78ceea-6244-4bcb-b2f9-0582d16b7aa0/image.png" alt=""></p>
<hr>
<blockquote>
<p>🐷** GitHub Action 실습하기**</p>
</blockquote>
<ul>
<li><strong>실습내용 : VS Code에서 커밋 및 푸쉬 시 자동으로 EC2에 nginx 서버 설치(미설치 시) 
&nbsp;　　　　　및 html 파일 적용</strong></li>
</ul>
<br>

<p><strong>1. 깃허브 레포지토리 생성</strong></p>
<p><strong>2. AWS EC2 생성 및 인바운드 규칙 편집 ( 80번 및 443번 포트 )</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/48b473c3-acae-4ced-94c4-e14dd3abe802/image.png" alt=""></p>
<p><strong>3. EC2 생성 시 발급받은 <code>.ppk</code> 키 파일을 <code>.pem</code> 파일로 변환</strong>
&nbsp;　➡ <strong>이유 : <code>.ppk</code> 파일은 Putty에서 사용할 목적으로 생성한 파일이지만, 깃허브 액션은 
 &nbsp;　　　　　리눅스 환경에서 실행되므로, <code>.pem</code> 형식의 키를 사용하여 SSH 연결을 설정한다.</strong></p>
<p> &nbsp;　<strong>1) Puttygen 프로그램 실행</strong></p>
<p> &nbsp;　<strong>2) Load 클릭 후 발급받은 <code>.ppk</code> 파일 입력</strong></p>
<p> <img src="https://velog.velcdn.com/images/passion_hd/post/088ab478-f87d-43f3-8f5e-9206506f5d80/image.png" alt=""></p>
<p> &nbsp;　<strong>3) Conversions 클릭 ➡ Export OpenSSH key 클릭 후 키 저장</strong>
 <img src="https://velog.velcdn.com/images/passion_hd/post/1cb78ecf-939f-4371-b886-ea7436051369/image.png" alt=""></p>
<hr>
<p><strong>2. 사용할 환경변수 등록</strong></p>
<p>&nbsp;　<strong>1) 깃허브 레포지토리에서 Settings 클릭</strong></p>
<p>&nbsp;　<strong>2) 왼쪽 메뉴탭에서 Secrets and variables - Actions 클릭</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/ecdcb0e9-720e-44c1-a498-1f1f39b5dcc9/image.png" alt=""></p>
<hr>
<p>&nbsp;　<strong>3) New repository secret 클릭 - 사용할 변수명과 값 입력</strong>
 &nbsp;&nbsp;　　➡ <strong>사용할 변수만큼 계속 생성하면 된다.</strong>
 <img src="https://velog.velcdn.com/images/passion_hd/post/c4241e37-4092-4b10-8a41-f765c13a7116/image.png" alt=""></p>
<p> &nbsp;&nbsp;　　➡ <strong>나는 위 사진과 같이 ４개의 환경변수를 생성했다.</strong>
 &nbsp;&nbsp;&nbsp;　　　<strong>1) REMOTE_HOST : EC2 IP 주소</strong>
 &nbsp;&nbsp;&nbsp;　　　<strong>2) REMOTE_PORT : 22&nbsp;　　&lt;-- SSH 프로토콜의 포트번호</strong>
 &nbsp;&nbsp;&nbsp;　　　<strong>3) REMOTE_USER : ubuntu</strong>
 &nbsp;&nbsp;&nbsp;　　　<strong>4) SSH_KEY : 발급받은 <code>.pem</code> 키 파일</strong>
 &nbsp;&nbsp;　　　　　➡ <strong>파일의 제일 위 <code>-----BEGIN RSA PRIVATE KEY-----</code> 및 
  &nbsp;&nbsp;&nbsp;　　　　　　제일 아래 <code>-----END RSA PRIVATE KEY-----</code> 도 포함시켜야 된다.</strong></p>
<hr>
<p><strong>3. VS Code 열고, 생성한 깃허브 레포지토리로 원격 저장소 설정</strong></p>
<p><strong>4. test.html 파일 생성</strong></p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;TEST&lt;/title&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
  &lt;/head&gt;
  &lt;body&gt;
    TEST 1111111111
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<hr>
<p><strong>5. <code>.github/workflows</code> 디렉토리 생성</strong></p>
<p><strong>6. nginx 설치 및 실행을 위한 플레이북 작성 ( deploy.yml )</strong></p>
<pre><code class="language-yaml">- hosts: [&quot;EC2 IP 주소&quot;]
  tasks:
    - name: Update APT package cache
      apt:
        update_cache: yes
      become: yes
    - name: Install Nginx
      apt:
        name: nginx
        state: present
      become: yes
    - name: Start Nginx service
      systemd:
        name: nginx
        state: started
        enabled: yes</code></pre>
<hr>
<p><strong>7. EC2에 접속하여 nginx의 페이지를 변경하는 GitHub Action Workflow 작성</strong></p>
<pre><code class="language-yaml">name: devops test

on:
  push:
    branches: [main]   // 깃허브로 Push 할 브랜치 명

jobs:
  deploy:
    name: Deploy to EC2
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository   // Push 한 내용을 Pull 해오기 위한 작업
        uses: actions/checkout@v3

      - name: Run Ansible playbook  // EC2에서 ubuntu 사용자로 접속하기 위한 작업
        uses: dawidd6/action-ansible-playbook@v2.8.0
        with:
          playbook: deploy.yml
          directory: ./
          key: ${{secrets.SSH_KEY}}
          inventory: |
            [all]
            43.200.163.229 ansible_ssh_user=ubuntu   // EC2 IP 주소로 작성

      - name: get code from github    // Push 한 내용으로 nginx 페이지를 교체하기 위한 작업
        uses: appleboy/ssh-action@v0.1.9
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.REMOTE_PORT }}
          script: |
            sudo rm -rf ./action    // 깃허브 레포지토리명
            sudo git clone https://github.com/[계정명]/[레포지토리명]
            sudo mv -f ./action/*.html /var/www/html</code></pre>
<hr>
<p>➡ <strong>생성한 결과는 아래와 같다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/2701b6e8-44a4-4b07-956e-3ba2dd793334/image.png" alt=""></p>
<hr>
<p><strong>8. Commit 및 Push 후 깃허브 액션 - Deploy EC2 완료 여부 확인</strong></p>
<p>➡ <strong>Push와 동시에, EC2에 nginx를 설치 및 실행시키고 깃허브에서 Pull로 파일을 불러와서 
 &nbsp;　해당 html 파일을 nginx 페이지로 대체하게 된다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/628b29df-efc8-4ceb-b515-c6f5d1a675f2/image.png" alt=""></p>
<hr>
<p>➡ <strong>성공적으로 실행되면, 아래와 같이 출력된다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/2064a770-fd65-44c3-bff9-0992d757edd2/image.png" alt=""></p>
<hr>
<p><strong>9. EC2 IP주소:80/test.html URL로 접속하여 VS Code에서 작성한 html 파일이 출력되는지 
 &nbsp;　확인한다.</strong></p>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/1e3d184e-f944-4e00-a4c1-47af5f5f1f90/image.png" alt=""></p>
<hr>
<p><strong>10. test.html 파일을 변경 후, 다시 깃허브로 Push 했을 때, 해당 페이지가 변경되는지 확인</strong></p>
<hr>
<ul>
<li><strong><code>deploy.yml</code> 과 <code>main.yml</code> 을 나눠서 작성한 이유는 향후 복잡한 코드 작성 시 작업의 가독성과 유지 보수성이 향상될 수 있고, 필요한 경우 특정 작업 단계를 수정하거나 재사용하는 것이 가능하기 때문이다.</strong></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jaeger를 이용한 분산 추적 시스템 구축하기]]></title>
            <link>https://velog.io/@passion_hd/Jaeger%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%B6%84%EC%82%B0-%EC%B6%94%EC%A0%81-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@passion_hd/Jaeger%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%B6%84%EC%82%B0-%EC%B6%94%EC%A0%81-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 17 Feb 2024 00:23:12 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>** Jaeger 란 ❓**</p>
</blockquote>
<ul>
<li><p>**&quot;Jaeger&quot;는 분산 시스템의 트래킹 및 분석을 위한 오픈 소스 분산 추적 시스템이다. 
분산 시스템에서 발생하는 트랜잭션의 흐름을 추적하고 이해하기 위한 도구로 사용된다. ** </p>
<p><strong>따라서 주로 MSA 에서 사용되며, 각 서비스 간의 통신 및 의존성을 추적하여 전체 시스템의 성능을 이해하고 최적화하는데 도움이 된다.</strong></p>
<ul>
<li><strong>Jaeger를 사용하는 이유 ❓</strong></li>
</ul>
<p><strong>1) 분산 트랜잭션 모니터링</strong>
&nbsp;　➡ <strong>마이크로 서비스 간의 데이터 이동을 모니터링 하는 기능이 있어서, 개발자가 
&nbsp;　　사전에 문제를 감지하고 해결할 수 있다.</strong></p>
<p><strong>2) 지연 시간 최적화</strong>
&nbsp;　➡ <strong>애플리케이션 속도를 저해하는 마이크로 서비스의 병목 지점을 찾을 수 있다. 
&nbsp;　　즉, 메서드가 실행될때 유독 시간이 오래 걸리는 메서드를 발견할 수 있고, 그러한 
&nbsp;　　문제를 발견하면 해결하기 위해 노력할 수 있다. 그렇게 되면 마이크로 서비스의 
&nbsp;　　속도를 높일 수 있다.</strong></p>
<p><strong>3) 근본 원인 분석</strong>
&nbsp;　➡ <strong>MSA에서는 하나의 문제가 다른 문제로 이어질 수 있는데, 이러한 문제들 속에서 
&nbsp;　　 가장 첫 번째로 문제가 어디서 발생했는지 시작점을 찾을 수 있다.</strong></p>
<p><strong>4) 서비스 종속성 분석</strong>
&nbsp;　➡ <strong>서비스 종속성이란 애플리케이션이 몇 가지 구성 요소의 실행에 의존한다는 것을 
&nbsp;　　의미하는데, 이러한 여러 마이크로 서비스 간의 복잡한 관계를 이해할 수 있다.</strong></p>
</li>
</ul>
<hr>
<ul>
<li><p><strong>Jaeger의 기능</strong> 💻</p>
<p><strong>1) 분산 추적</strong> ✅</p>
<p><strong>분산 추적은 마이크로서비스 간의 이벤트 시퀀스를 모니터링하는 소프트웨어 기술로, 모든 연결을 추적하고 애플리케이션의 요청 경로를 시각화하는 차트와 그래프를 제공한다.</strong></p>
<p>*<em>Jaeger는 각 요청마다 고유 식별자를 할당하고 특정 서비스가 요청을 처리할 때 정보를 수집하여 요청의 이동을 추적한다. *</em></p>
<hr>
<p>*<em>2) OpenTracing *</em> ✅</p>
<p><strong>OpenTracing은 다양한 현대적 소프트웨어 시스템에서 정확한 턴키 분산 추적을 실현할 수 있는 표준을 제공하는, 오픈 소스 또는 무료로 제공되는 프레임워크이다.</strong></p>
<p><strong>Jaeger는 OpenTracing을 사용하여 마이크로서비스 데이터를 수집, 저장, 관리, 분석 및 시각화하는 완전한 솔루션을 제공한다.</strong>  </p>
<hr>
<p><strong>3) OpenTracing 데이터 모델</strong> ✅</p>
<p><strong>OpenTracing 데이터 모델은 여러 구성 요소의 데이터를 연결하는 기본 정의를 제공하는데 Span과 Trace 가 있다.</strong></p>
<p>💡<strong>Span **
➡ **Span은 분산 추적 시스템에서 수행되는 작업의 단일 논리적 단위로, 각각의 Span에는 
&nbsp;　아래와 같은 구성 요소가 있다.</strong></p>
<p> <strong>1. 작업 이름</strong>
 <strong>2. 시작 시간과 중지 시간</strong>
 <strong>3. 개발자가 Span을 분석하는 데 도움이 되는 태그 또는 값</strong>
 <strong>4. 마이크로서비스가 생성하는 메시지를 저장하는 로그</strong>
 <strong>5. Span Context 또는 Span에 대한 추가 설명</strong></p>
<hr>
<p> 💡<strong>Trace</strong>
➡ <strong>Trace는 동일한 프로세스에 속하는 하나 이상의 Span의 모음으로, 특정 시간 동안 
&nbsp;　발생하는 이벤트를 나타낸다. 동일한 Trace에 속하는 Span은 동일한 Trace ID를 
&nbsp;　공유한다.</strong> </p>
<p>➡ <strong>예를 들어 고객이 음식을 주문할 때 생성되는 트레이스에는 다음과 같은 스팬이 
&nbsp;　포함된다.</strong></p>
<p> <strong>1. 고객이 주문 제출</strong>
 <strong>2. 결제가 처리됨</strong>
 <strong>3. 주문 목록이 식당에 제출됨</strong>
 <strong>4. 음식 수령</strong>
 <strong>5. 음식 제공</strong></p>
</li>
</ul>
<hr>
<blockquote>
<p>🐶** Jager 설치 및 실습하기 ( k8s 및 스프링 부트)**</p>
</blockquote>
<p><strong>1. master 컴퓨터에서 아래와 같이 Jaeger를 설치한다.</strong></p>
<pre><code class="language-yaml">docker run --rm --name jaeger \
  -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318 \
  -p 14250:14250 \
  -p 14268:14268 \
  -p 14269:14269 \
  -p 9411:9411 \
  jaegertracing/all-in-one:1.54</code></pre>
<p>➡ <strong>위 내용은 Jaeger에 접속할 수 있도록 포트포워딩 설정을 해서 설치하는 내용이다.</strong></p>
<p>➡ <strong>위의 포트 번호중 아무거나 나중에 스프링 부트 야멜 파일에서 설정해주면 되는데, 
 &nbsp;　나는 6831 번 포트를 사용할 예정이다.</strong></p>
<p>➡ ** 정상적으로 설치가 되면, <code>master 컴퓨터의 IP:16686</code> 으로 접속하면, 아래처럼 
 &nbsp;　Jaeger 화면이 보인다.**</p>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/7bef4550-9ba8-49f7-a712-a0a9ff83e78b/image.png" alt=""></p>
<hr>
<ul>
<li><strong>Jaeger는 동작 상태로 두고, 이제는 스프링 부트에 Jaeger를 적용해보겠다.</strong></li>
</ul>
<p><strong>2. <code>pom.xml</code> 에 라이브러리를 추가한다.</strong></p>
<pre><code class="language-yaml">        &lt;dependency&gt;
            &lt;groupId&gt;io.opentracing.contrib&lt;/groupId&gt;
            &lt;artifactId&gt;opentracing-spring-jaeger-web-starter&lt;/artifactId&gt;
            &lt;version&gt;3.1.2&lt;/version&gt;
        &lt;/dependency&gt;</code></pre>
<hr>
<p> <strong>3. <code>application.yml</code> 파일에 설정을 추가한다.</strong></p>
<pre><code class="language-yaml"> spring:
  application:
    name: lonua-service 

opentracing:
  jaeger:
    service-name: lonua-svc  // Jaeger에 표시될 서비스의 이름 지정
    udp-sender:
      host: 192.168.0.211    // Jaeger가 동작중인 master 컴퓨터의 IP
      port: 6831             // Jaeger 설치 시 포트포워딩 해줬던 포트 중 1개</code></pre>
<hr>
<p> <strong>4. AOP 클래스 작성</strong></p>
<pre><code class="language-java"> @Aspect
@Component
public class JaegerAop {

    private final Tracer tracer;
    private Span spanPhase1;

    public JaegerAop(Tracer tracer) {
        this.tracer = tracer;
    }

    // 어느 경로의 패키지에 적용시킬 것인지 지정
    @Pointcut(&quot;execution(* com.example.lonua..*.*(..))&quot;)
    private void cut() {
        System.out.println(&quot;컷&quot;);
    }

    // 조인 포인트, 어느 시점 즉 메서드가 실행되기 전을 지정
    @Before(&quot;cut()&quot;)
    public void before(JoinPoint joinPoint) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        System.out.println(method.getName() + &quot; 메소드 실행 전&quot;);

        Span parentSpan = tracer.scopeManager().activeSpan();
        spanPhase1 = tracer.buildSpan(&quot;spanPhase_1&quot;).asChildOf(parentSpan).start();

    }

    // 조인 포인트, 어느 시점 즉 메서드가 실행된 후를 지정
    @AfterReturning(value = &quot;cut()&quot;)
    public void afterReturning(JoinPoint joinPoint) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        System.out.println(method.getName() + &quot; 메소드 실행 후&quot;);

        try {
            spanPhase1.log(&quot;Test Micro Service Call.&quot;);
        } finally {
            spanPhase1.finish();
        }
    }
}</code></pre>
<p> ➡ <strong>이렇게 작성하면, 모든 프로젝트 내의 모든 메서드가 실행되기 전에 추적을 시작하고 
  &nbsp;　메서드가 완전히 실행되면 추적을 끝낸다.</strong></p>
<hr>
<ul>
<li><strong>이제, 프로젝트를 실행시키고, Jaeger 웹페이지에서 새로고침을 해보면 아래와 같이 야멜파일에서 설정한 서비스 이름으로 나타난다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/e9b2add7-cef9-430a-adeb-7385a545ddc5/image.png" alt=""></li>
</ul>
<hr>
<ul>
<li><p><strong>이제, 아무 컨트롤러로 요청을 보내본다. ( 포스트맨 또는 웹브라우저 이용 )</strong></p>
</li>
<li><p><strong>상품의 목록을 불러오는 요청을 아래처럼 보냈고,</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/6fd4da71-7b5a-4fa1-a3bc-a226079bd6b9/image.png" alt=""></p>
</li>
<li><p>** Jaeger에서 서비스 선택 후 &quot;Find Traces&quot; 를 클릭해보면 아래처럼 메서드가 실행되는 동안의 추적한 결과가 나타난다.**
<img src="https://velog.velcdn.com/images/passion_hd/post/e49b67da-8b2e-4f75-967f-047c4e069aac/image.png" alt=""></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠버네티스를 이용한 무중단 배포 및 모니터링 시스템 구축하기]]></title>
            <link>https://velog.io/@passion_hd/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC-%EB%B0%8F-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-r8298es2</link>
            <guid>https://velog.io/@passion_hd/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC-%EB%B0%8F-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-r8298es2</guid>
            <pubDate>Thu, 15 Feb 2024 16:18:46 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>** 🐹 실습 전 사전 준비 사항**</p>
</blockquote>
<ul>
<li><p>도커 허브에 <strong>이미지를 3개 만들어서</strong> 버전을 다르게 하여 올려 놓는다.</p>
</li>
<li><p>나는 스프링 부트에서 컨트롤러만 생성하여 <code>/test/version</code> 으로 요청을 보내면, 응답으로 <strong>&quot;V1 / V2 / V3 출력하는 이미지&quot;</strong> 라는 내용을 반환토록 작성하여 이미지를 푸쉬해놨다.</p>
</li>
</ul>
<hr>
<blockquote>
<p>🐶 ** 무중단 배포 방법 실습**</p>
</blockquote>
<ul>
<li><p>지난 글에서 Deployment를 생성할때 <strong>Recreate</strong> 방식을 사용하였는데, Recreate 방식은 버전 업데이트 시 실행중인 모든 파드를 한번에 삭제시키고 새로운 버전의 파드를 생성하기 때문에, 잠시동안 <strong>서버에 다운타임이 발생</strong>하게 되는 문제가 있다.</p>
</li>
<li><p><strong>Recreate 방식 테스트 하기</strong> 💻</p>
</li>
</ul>
<p><strong>1. Recreate 방식으로 Deployment 생성하기</strong></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-deployment
spec:
  replicas: 2
  strategy:
    type: Recreate
  minReadySeconds: 10
  selector:
    matchLabels:
      type: backend
  template:
    metadata:
      labels:
        type: backend
    spec:
      containers:
      - name: backend
        image: [버전1 의 이미지 이름]
      terminationGracePeriodSeconds: 5 </code></pre>
<hr>
<p><strong>2. LoadBalancer 로 서비스 생성하기</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: backend-svc
spec:
  selector:
     type: backend
  ports:
  - port: 8080
    targetPort: 8080
  type: LoadBalancer</code></pre>
<hr>
<p><strong>3. 테스트 명령어 입력 : 1초에 한번씩 생성한 컨트롤러로 요청 보내보기</strong>
➡ <code>while true; do curl http://[서비스 IP]:8080/test/version; sleep 1; done</code></p>
<p><strong>4. 생성한 Deployment의 리소스 편집에서 이미지의 버전을 1.0 ➡ 2.0 으로 수정</strong></p>
<ul>
<li><p>이렇게 하면 처음에는 V1 이 출력되다가, 버전을 수정한 순간 아래처럼 잠시동안 다운타임이 생겼다가 V2가 출력되는 것을 볼 수 있다.
<img src="https://velog.velcdn.com/images/passion_hd/post/72a549cb-43fe-4be9-b75b-63587c9fd0ba/image.png" alt=""></p>
</li>
<li><p>위의 사진을 보면 <strong>총 2가지의 실패 문장</strong>이 있다.</p>
<p><strong>1) <code>No route to host</code> : 이것은 파드가 삭제되고 생성되는 동안 찾아갈 주소가 없어서 
&nbsp;　　　　　　　　　　　발생되는 것이다.</strong></p>
<p><strong>2) <code>Connection refused</code> : 이것은 스프링 부트 서버가 실행되는 동안 발생되는 것이다.</strong></p>
</li>
</ul>
<hr>
<ul>
<li><p>이제 이러한 다운타임을 해결하기 위한 방법으로 Deployment의 또다른 방식인 <code>RollingUpdate</code> 방식으로 생성을 해보겠다.</p>
<p>➡ RollingUpdate 방식은 버전 업데이트 시 새로운 버전의 파드 1개를 먼저 생성하고, 
&nbsp;　생성되면 기존 파드 1개를 삭제하는 방식으로 진행된다. 
&nbsp;　<strong>( 인줄 알았으나, 설정할 수 있는 내용이 있어, 밑에서 추가 설명 예정)</strong></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-deployment
spec:
replicas: 2
strategy:
  type: RollingUpdate
minReadySeconds: 10
selector:
  matchLabels:
    type: backend
template:
  metadata:
    labels:
      type: backend
  spec:
    containers:
    - name: backend
      image: [이미지 이름]
    terminationGracePeriodSeconds: 5</code></pre>
<p>➡ 생성하는 방법은 위처럼 아예 새로 생성해도 되고, 위에서 생성했던 Deployment의 리소스 
&nbsp;　편집에서 type을 Recreate에서 RollingUpdate로 바꿔줘도 된다.</p>
</li>
</ul>
<p>➡  그런다음 위에서 실시한 것처럼 똑같이 테스트를 해보면 아래와 같이 결과가 나타날 
 &nbsp;　것이다.<img src="https://velog.velcdn.com/images/passion_hd/post/75023db3-1db4-4acf-a3b5-78980491bcc3/image.png" alt=""></p>
<hr>
<ul>
<li>위의 사진을 보면 버전 2에서 버전 3으로 바뀔 동안, Recreate 방식에서 나타났던 
<code>No route to host</code> 실패 문장은 사라졌지만, <code>Connection refused</code> 는 여전히 발생하는 것을 확인할 수 있다.</li>
</ul>
<ul>
<li><p>따라서, 이부분까지 해결해주기 위해서는 <strong>Probe</strong> 를 적용할 수 있다.</p>
</li>
<li><p>여러 종류가 있겠지만, 나는 <strong>readinessProbe</strong>를 적용해보겠다. readinessProbe를 적용하여 아래와 같이 Deployment를 생성한다.</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-deployment
spec:
replicas: 2
strategy:
 type: RollingUpdate
minReadySeconds: 10
selector:
 matchLabels:
   type: backend
template:
 metadata:
   labels:
     type: backend
 spec:
   containers:
   - name: backend
     image: [이미지 이름]
     readinessProbe:   // 여기부터 추가 된 내용
       httpGet:
         path: /test/version  // 요청을 보내는 경로 ( 작동하는지 확인 )
         port: 8080
       initialDelaySeconds: 5  // 파드가 시작되고 readinessProbe를 시작하기 전 대기 시간 ( 5초 )
       periodSeconds: 2        // readinessProbe를 수행하는 주기 ( 2초마다 실행 )
       successThreshold: 3     // 성공으로 간주하는 연속 성공 횟수 ( 연속 3번 성공해야 성공 )
   terminationGracePeriodSeconds: 5
</code></pre>
<ul>
<li>readinessProbe는 Pod가 서비스 요청을 수신할 준비가 되었는지 확인하여, 준비가 되면 요청을 보내는 방식이다.</li>
<li>위 처럼 설정하면, <code>/test/version</code> 으로 요청을 보냈을때 연속해서 3번 성공해야 성공한것으로 간주하고 그때부터 본격적으로 요청을 보내게 된다.</li>
<li>그럼 이제, 기존처럼 똑같이 테스트를 해보면 아래와 같이 서버의 다운타임 없이 완전한 무중단 배포 ( 버전 업데이트 ) 가 되는 것을 볼 수 있다.</li>
</ul>
<p>➡ 정상적이라면 아래처럼 출력될 것이다. 하지만, 컴퓨터 사양에 따라 1 ~ 2초 정도 
&nbsp;　<code>Connection refused</code> 가 출력될 수 있는데, 이 부분은 생성할때 조건을 조절하면 
&nbsp;　될 것이다. ( 내가 실제로 테스트 진행 간 그랬다... )
<img src="https://velog.velcdn.com/images/passion_hd/post/f36affb8-f699-4a21-948f-c3bb0fc9e4a7/image.png" alt=""></p>
</li>
</ul>
<hr>
<blockquote>
<p>🔥 <strong>RollngUpdate 설정 알게된 내용 정리 <span style="color:red">(24. 2.17. 추가)</strong></span></p>
</blockquote>
<ul>
<li><p>인프런 일프로님의 강의를 들으면서 알게된 내용이 있어, 다시 정리 한다.</p>
</li>
<li><p>위에서, RollingUpdate로 Deployment를 생성하면, 파드가 1개씩 생성되고 삭제된다고 적었는데 알고보니 이것은 RollingUpdate에서 Pod를 생성하고 삭제하는 설정 내용이 <strong>기본값으로 설정</strong>이 되있어서 그랬던 것이었다.</p>
</li>
<li><p><strong>아래는 설정 내용을 적용한 Deployment 생성 야멜 파일이다.</strong></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-deployment
spec:
replicas: 2
strategy:
 type: RollingUpdate
 rollingUpdate:   &lt;-- 새로 추가한 설정 🔥🔥
   maxUnavailable: 25%   &lt;-- 기본값으로, 별도로 설정해주지 않으면 적용되는 값
   maxSurge: 25%         &lt;-- 기본값으로, 별도로 설정해주지 않으면 적용되는 값
minReadySeconds: 10
selector:
 matchLabels:
   type: backend
template:
 metadata:
   labels:
     type: backend
 spec:
   containers:
   - name: backend
     image: [이미지 이름]
   terminationGracePeriodSeconds: 5</code></pre>
</li>
<li><p><strong>maxUnavailable</strong> : 롤링 업데이트 중에 허용되는 동시에 사용할 수 없는 파드의 최대 비율을 나타낸다. 즉, 업데이트 동안 최대 몇개의 파드를 서비스 중지시킬 것인지 설정하는 것이다.</p>
</li>
<li><p><strong>maxSurge</strong> : 롤링 업데이트 시 최대 몇개까지 파드를 동시에 만들 것인지 설정하는 것이다.</p>
</li>
<li><p><strong>만약, <span style="color:blue">maxUnavailable: 100%, maxSurge: 100%</span> 로 설정한다면, 버전 업데이트 시 전체 파드를 서비스 중지로 만들고, 새로 생성하는 것 또한 전체 파드 개수만큼 한번에 생성하게 된다.</strong></p>
<p><strong>Deployment의 기능을 알고 있다면, 이러한 작동 방식이 Deployment의 Recreate 방식과 동일하다는 것을 알 것이다. 이처럼 직접 설정해 줄 수 있기 때문에</strong></p>
<p><strong>따라서, <span style="color:blue">maxUnavailable: 0%, maxSurge: 100%</span> 로 설정을 한다면, 2개의 파드를 기준으로 했을 때 버전 업데이트 시 새로운 버전의 파드가 2개가 한번에 생성되고, 생성이 완료되면 기존 파드 2개가 1개씩 순차적으로 삭제되는 형태가 된다.</strong></p>
</li>
<li><p><strong>아래는 실습해본 영상이다. ( 실습 코드는 아래와 같음 )</strong></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-deployment
spec:
replicas: 2
strategy:
 type: RollingUpdate
 rollingUpdate:
   maxUnavailable: 0% 
   maxSurge: 100% 
minReadySeconds: 10
selector:
 matchLabels:
   type: backend
template:
 metadata:
   labels:
     type: backend
 spec:
   containers:
   - name: backend
     image: [이미지 이름]
   terminationGracePeriodSeconds: 5</code></pre>
<p><img src="https://github.com/hyungdoyou/LONUA_Project/assets/148875644/d52bcc60-d7e4-4c69-9599-a075b781a255" alt="ezgif-2-719c75a3cc"></p>
</li>
</ul>
<hr>
<ul>
<li><p>지금까지는 Deployment의 2가지 방식인 <strong>Recreate</strong> 와 <strong>RollingUpdate</strong>에 대해서 실습해봤는데, 이번에는 쿠버네티스의 기능을 이용한 무중단 배포 방식인 <code>Blue/Green</code> 방식을 실습해보겠다.</p>
</li>
<li><p><code>Blue/Green</code> 방식은 Deployment를 생성할 때 버전을 명시해주고, 서비스를 생성할때도 버전을 명시해서 나중에 버전 업그레이드 시 업그레이 된 버전의 새로운 Deployment를 생성하고 그 버전으로 서비스도 변경해주는 방식이다.</p>
<p>➡ 버전은 원하는대로 지어준 뒤 서비스와 Deployment 간 매칭만 잘 해주면 된다.</p>
<p><strong>1) Deployment를 생성한다. ( 버전 명시 )</strong></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-deployment-1
spec:
replicas: 2
strategy:
  type: Recreate
revisionHistoryLimit: 1
selector:
  matchLabels:
    type: backend
    version: v1  // 버전 명시 ( 임의 지정 )
template:
  metadata:
    labels:
      type: backend
      version: v1  // 버전 명시 ( 임의 지정 )
  spec:
    containers:
    - name: backend
      image: [이미지 이름]
</code></pre>
<hr>
<p><strong>2) 서비스를 생성한다. ( 버전 명시 )</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
name: backend-svc
spec:
selector:
  type: backend
  version: v1  // 버전 명시 ( 위에서 설정한 이름 )
ports:
- port: 8080
  targetPort: 8080
type: LoadBalancer 
</code></pre>
<hr>
<p><strong>3) 업그레이드 버전의 새로운 Deployment를 생성한다. ( 바뀐 버전 명시 )</strong></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-deployment-2
spec:
replicas: 2
strategy:
  type: Recreate
revisionHistoryLimit: 1
selector:
  matchLabels:
    type: backend
    version: v2  // 바뀐 버전 명시 ( 임의 지정 )
template:
  metadata:
    labels:
      type: backend
      version: v2  // 바뀐 버전 명시 ( 임의 지정 )
  spec:
    containers:
    - name: backend
      image: [이미지 이름]
</code></pre>
<hr>
<p><strong>4) 서비스의 리소스 편집에서 버전을 v1에서 v2로 바꿔준다.</strong></p>
<hr>
<ul>
<li>위의 절차대로 테스트를 하면 아래와 같이 버전이 업데이트 될 때 다운타임 없이 업데이트가 성공적으로 이뤄지는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/passion_hd/post/38b8e897-46b6-46db-a7c2-d7fb22b41562/image.png" alt=""></li>
</ul>
</li>
<li><p>실무에서는 이처럼 Deployment를 2개 생성해 놓고, 운영을 안하는 Deployment는 Pod를 0으로 설정해놨다가, 버전 업데이트 시 해당 Deployment의 Pod를 늘리고, 업데이트 된 버전으로 적용한 다음,</p>
<p>서비스의 버전을 바꾸고, 기존 Deployment의 Pod 개수를 다시 0으로 바꾸는 식으로 계속 Deployment 2개를 바꿔가며 사용하기도 한다고 한다.</p>
</li>
</ul>
<hr>
<blockquote>
<p>🧐** 알고 있으면 유용한 옵션 : revisionHistoryLimit**</p>
</blockquote>
<ul>
<li><p>revisionHistoryLimit 옵션은 버전 업데이트 시 지정한 개수만큼 레플리카 셋을 남겨놓도록 하는 설정이다.</p>
</li>
<li><p>주로, 업데이트를 잘못했을 때 되돌아가기 위해 사용한다고 한다.</p>
</li>
<li><p>버전 히스토리 출력 : <code>kubectl rollout history deployment [디플로이언트 이름]</code>
<img src="https://velog.velcdn.com/images/passion_hd/post/bbb9ed6d-c9bd-409e-8a98-6df11cd8025a/image.png" alt=""></p>
</li>
<li><p>버전 롤백 : 버전 숫자는 히스토리 출력 또는 레플리카 셋을 클릭해보면 볼 수 있다.
<code>kubectl rollout undo deployment [디플로이언트 이름] --to-revision=[버전숫자]</code></p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/2d1653e2-bee7-4794-952d-5175a8bd9ada/image.png" alt=""></p>
<hr>
<blockquote>
<p>🦁 <strong>Istio 설치하기</strong></p>
</blockquote>
<ul>
<li><p>쿠버네티스에서 모니터링 시스템을 구축하는 방법으로는</p>
<p>1) 도커 허브에 올려져 있는 이미지 활용
2) Helm 차트 활용
<strong>3) MSA와 같은 서비스 매쉬에서 사용되는 Istio 활용</strong></p>
<p>이렇게 총 3가지가 있는데, 나는 3번째 방식을 실습하였다.</p>
</li>
<li><p><strong>Istio 란 ❓</strong></p>
<p>쿠버네티스는 도커를 편리하게 컨트롤할 수 있으며, 다양한 이점을 제공한다. 하지만 다수의 컨테이너가 동작할 때 컨테이너의 트래픽을 관찰하고 정상 동작하는지 모니터링하는 것은 그만큼 매우 어렵다. </p>
<p>따라서, 이를 해결해주는 것이 서비스 매쉬란 개념이고, 이에 대한 구현체 중 하나가 Istio이다.</p>
<p>✅ <strong>Istio의 장점</strong>
➡ 쿠버네티스의 복잡성을 감소시킬 수 있다.
➡ 트러블 슈팅과 디버깅이 매우 쉬워져서 운영하는데 큰 도움을 준다.</p>
<p>✅ <strong>Istio의 단점</strong>
➡ 각 Pod당 Sidecar 형태로 컨테이너가 1개씩 더 붙기 때문에 성능이 떨어진다.</p>
</li>
</ul>
<hr>
<ul>
<li><p><strong>Service Mesh 란❓</strong></p>
<p>Mesh란 그물, 망사라는 뜻을 가지고 있으며, Service Mesh는 Serivce들이 그물처럼 엮여있는것을 뜻한다.</p>
<p>MSA를 적용한 시스템의 내부 통신이 그물(Mesh) 네트워크의 형태를 띄는 것에 빗대어 Service Mesh로 불리게 되었다.</p>
<p>애플리케이션 계층이 아닌 인프라 플랫폼 계층에 특정 모듈을 삽입하여 애플리케이션에 대한 라우팅, 보안 및 안정성 기능을 추가하는 도구로, 쿠버네티스와 같은 컨테이너 오케스트레이션 환경에서 일반적으로 애플리케이션 코드(사이드 카 라고 불리는 패턴)와 함께 배치된 확장 가능한 네트워크 프록시 모듈로 구현된다.</p>
<br> </li>
<li><p><em>Service Mesh를 사용하는 이유는❓*</em></p>
<p>서비스 메시 없이 동작하는 마이크로 서비스는 서비스 간 커뮤니케이션을 통제하는 로직으로 코딩해야 하기 때문에 개발자들이 비즈니스 로직에 집중하지 못하게 된다.</p>
<p>또한, 서비스 간 커뮤니케이션을 통제하는 로직이 각 서비스 내부에 숨겨져 있기 때문에 커뮤니케이션 장애를 진단하기 더 어려워진다.</p>
<p>수십 개의 마이크로 서비스가 분리되어 있고, 서비스 간의 통신도 매우 복잡하여 새로운 장애 지점이 계속 나타나게 된다면 서비스 메시 없이는 문제가 발생한 지점을 찾아내기가 어려울 것이다.</p>
<br>
✅ **Service Mesh의 장점**

<p>1) 서비스 간 커뮤니케이션의 모든 부분을 성능 메트릭으로 캡처할 수 있다.
2) 개발자 들은 비즈니스 로직에 집중할 수 있다.
3) 문제를 손쉽게 인식하고 진단할 수 있다.
4) 장애가 발생한 서비스로부터 요청을 재 라우팅 할 수 있기 때문에 애플리케이션 복구 능력이 
&nbsp;　향상된다.
5) 성능 메트릭을 통해 런타임 환경에서 커뮤니케이션을 최적화하는 방법을 제안할 수 있다.</p>
</li>
</ul>
<hr>
<ul>
<li><p><strong>Side Car 패턴이란❓</strong></p>
<p>애플리케이션 컨테이너와 독립적으로 동작하는 별도의 컨테이너를 붙이는 패턴으로, 오토바이에 연결된 사이트와 유사하기 때문에 사이드 카 패턴이라고 불린다.</p>
<p>애플리케이션 컨테이너와 독립적으로 동작하기 때문에, 사이드카 장애 시 애플리케이션이 영향을 받지 않고, 사이드카 적용/변경/제거 등의 경우에 애플리케이션은 수정이 필요가 없다.</p>
</li>
</ul>
<hr>
<ul>
<li><p><strong>설치 방법</strong> 💻 &nbsp;　&nbsp;　&nbsp;　<a href="https://docs.tigera.io/calico/latest/network-policy/istio/app-layer-policy#add-calico-authorization-services-to-the-mesh">설치 문서 바로가기</a></p>
<p><strong>1) Calico 설정 변경</strong></p>
<p>`kubectl patch FelixConfiguration default --type=merge --patch \</p>
<pre><code> &#39;{&quot;spec&quot;: {&quot;policySyncPathPrefix&quot;: &quot;/var/run/nodeagent&quot;}}&#39;</code></pre><h2 id="">`</h2>
<p><code>kubectl patch installation default --type=merge -p &#39;{&quot;spec&quot;: {&quot;flexVolumePath&quot;: &quot;None&quot;}}&#39;</code></p>
<p>&nbsp;　➡ 이때 아래처럼 error 라고 출력되는데 이것은 신경쓰지 않아도 된다.
<img src="https://velog.velcdn.com/images/passion_hd/post/c183e91f-d65d-43c1-a810-41ddf7cb770f/image.png" alt=""></p>
<hr>
<p><strong>2) Calico CSI (Container Storage Interface) 드라이버 설치 : 모든 노드에서 정책 동기화 API 설정</strong></p>
<p><code>kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/csi-driver.yaml</code></p>
<hr>
<p><strong>3) 명령어(프로그램) 설치</strong></p>
<p><code>curl -L https://git.io/getLatestIstio | ISTIO_VERSION=1.15.2 sh -</code></p>
<hr>
<p><code>echo &quot;export PATH=$HOME/istio-1.15.2/bin:$PATH&quot; &gt;&gt; ~/.bashrc</code></p>
<hr>
<p><code>source .bashrc</code></p>
<hr>
<p><strong>4) Istio 컨트롤 플레인 설치</strong></p>
<p>&nbsp;　➡ <code>istioctl install --set components.cni.enabled=true -y</code></p>
<p>&nbsp;　➡ 정상적으로 설치된다면, 아래 사진처럼 출력된다.
<img src="https://velog.velcdn.com/images/passion_hd/post/3067cde2-3746-48f2-95dc-1995c29ae329/image.png" alt=""></p>
<p>&nbsp;　➡ 만약 설치가 제대로 안되면, 아래 명령어로 삭제한 뒤 다시해봐야된다. 
&nbsp;&nbsp;　　➡ <code>istioctl x uninstall --purge</code></p>
<p>&nbsp;&nbsp;　　이때, 완전히 삭제되는지 확인을 해야되고, 가상 컴퓨터 재부팅도 방법이 될 수 </p>
<h2 id="있을-것이다">&nbsp;&nbsp;　　있을 것이다.</h2>
<p><strong>5) namespace 변경</strong></p>
<pre><code class="language-yaml">kubectl create -f - &lt;&lt;EOF
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default-strict-mode
namespace: istio-system
spec:
mtls:
  mode: STRICT
EOF
</code></pre>
<hr>
<p><strong>6) Istio 사이드 카 중 Dikastes 설치</strong></p>
<p><code>curl https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/alp/istio-inject-configmap-1.15.yaml -o istio-inject-configmap.yaml</code></p>
<hr>
<p><code>kubectl patch configmap -n istio-system istio-sidecar-injector --patch &quot;$(cat istio-inject-configmap.yaml)&quot;</code></p>
<hr>
<p><strong>7) addon 설치 ( Istio의 기본 기능 외 추가적인 기능 사용 목적 )</strong></p>
<p>&nbsp;　➡ <strong>Prometheus : 클러스터 내의 모니터링 및 지표 수집을 위한 기능</strong>
&nbsp;　　　　　　　　　　　<strong>설치 명령어 : <code>kubectl apply -f istio-1.15.2/samples/addons/prometheus.yaml</code></strong></p>
<p>&nbsp;　➡ <strong>Grafana : 프로메테우스로부터 수집한 지표를 시각화하고, 대시보드로 표시하기 위한 기능 **
&nbsp;　　　　　　　　　　**설치 명령어 : <code>kubectl apply -f istio-1.15.2/samples/addons/grafana.yaml</code></strong></p>
<p>&nbsp;　➡ <strong>Jaeger : 분산 추적을 지원하기 위한 기능 ( 각 서비스 간의 통신 추적 및 디버깅 )</strong>
&nbsp;　　　　　　　　　<strong>설치 명령어 : <code>kubectl apply -f istio-1.15.2/samples/addons/jaeger.yaml</code></strong></p>
<p>&nbsp;　➡ <strong>Kiali : 서비스 맵, 트래픽 흐름 및 메트릭을 시각화하여 모니터링하는 기능</strong>
&nbsp;　　　　　　　　<strong>설치 명령어 : <code>kubectl apply -f istio-1.15.2/samples/addons/kiali.yaml</code></strong></p>
<hr>
<p><strong>8) Istio를 적용할 라벨 지정</strong>
&nbsp;　➡ <code>kubectl label ns default istio-injection=enabled</code></p>
</li>
</ul>
<hr>
<ul>
<li>여기까지 진행하면 설치가 끝나고, 대쉬보드의 네임스페이스에서 <code>istio-system</code> 으로 들어갔을때, 모든 디플로이먼트가 아래처럼 정상적으로 동작중인 것을 확인하면 된다.
<img src="https://velog.velcdn.com/images/passion_hd/post/75c06202-f609-4750-8906-1f1aa699296d/image.png" alt=""></li>
</ul>
<hr>
<ul>
<li>위처럼 잘 동작중이라면, 서비스로 이동하여 <code>grafana</code> 와 <code>kiali</code>의 리소스 편집에서 type을 <strong>Cluster IP 에서 LoadBalancer 로 변경</strong>해준다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/7974e52e-070d-4d2e-a9b0-2efab7465159/image.png" alt=""></p>
<hr>
<ul>
<li><p>그다음 각각의 외부 엔드포인트로 접속해보면 아래처럼 접속이 정상적으로 된다.
&nbsp;　➡ <code>kiali</code> 는 20001 번 포트로 접속</p>
<br>
✅ **grafana 화면**
![](https://velog.velcdn.com/images/passion_hd/post/7b60facb-24c2-4e9c-8c2a-12dd4834de01/image.png)
</li>
<li><h2 id="그라파나의-대쉬보드는-대쉬보드-사이트-바로가기-여기서-마음에-드는-것을-찾아서-import-시키면-된다">그라파나의 대쉬보드는 <a href="https://grafana.com/grafana/dashboards/">대쉬보드 사이트 바로가기</a> 여기서 마음에 드는 것을 찾아서 Import 시키면 된다.</h2>
<p>✅ <strong>kiali 화면</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/3ee64742-edda-4e1f-bcee-89a34f1e36d5/image.png" alt=""></p>
</li>
</ul>
<hr>
<ul>
<li>이제 <code>default namespace</code> 로 다시 이동하여, Deployment 새로 생성해본다. 
&nbsp;➡ 이때 Probe 설정은 하지 않는다.<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-deployment
spec:
replicas: 2
strategy:
  type: RollingUpdate
minReadySeconds: 10
selector:
  matchLabels:
    type: backend
template:
  metadata:
    labels:
      type: backend
  spec:
    containers:
    - name: backend
      image: [이미지 이름]
    terminationGracePeriodSeconds: 5
</code></pre>
</li>
</ul>
<pre><code>&amp;nbsp;　➡ Pod의 로그 확인 창으로 가보면 기존에는 `backend` 라는 컨테이너 1개만 있었는데, 
 &amp;nbsp;&amp;nbsp;　　Istio를 설치함으로써 자동으로 `istio-proxy` 와 `dikastes` 컨테이너까지 
 &amp;nbsp;&amp;nbsp;　　총 3개의 컨테이너가 생성되는 것을 아래처럼 확인할 수 있을 것이다.
![](https://velog.velcdn.com/images/passion_hd/post/8ea01d3d-bb01-4976-881a-0769044583cd/image.png)

---
- 다음으로 서비스도 생성해본다.
```yaml
apiVersion: v1
kind: Service
metadata:
  name: backend-svc
spec:
  selector:
     type: backend
  ports:
  - port: 8080
    targetPort: 8080
  type: LoadBalancer
</code></pre><hr>
<ul>
<li>그런 다음, 웹브라우저로 요청을 보냈을때 정상적으로 요청이 보내지면 구축이 끝난다. </li>
<li>강의실 노트북으로 했을때는, 정상 동작했는데, 집에서 다시 해보니 동작을 안해서 확인이 필요하다.</li>
<li>정상적으로 되면 아래처럼 요청을 수행할 것이다.
<img src="https://velog.velcdn.com/images/passion_hd/post/45109c5d-a27b-41cc-a4c9-af395c458eef/image.png" alt=""></li>
</ul>
<hr>
<ul>
<li>그랬을때, master 컴퓨터에서 테스트로 위의 URL 과 동일하게 아래처럼 실행시키고, 
<code>kiali</code> 에서 확인해보면 요청을 보내는 것이 애니메이션화 되어 표시된다.
&nbsp;　➡ <code>while true; do curl http://77.77.77.53:8080/test/version; sleep 1; done</code>
<img src="https://velog.velcdn.com/images/passion_hd/post/529c073e-2156-461e-8829-7a4ad72d6ac8/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠버네티스로 3계층 아키텍처 구성하기 ( Deployment 적용 )]]></title>
            <link>https://velog.io/@passion_hd/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4%EB%A1%9C-3%EA%B3%84%EC%B8%B5-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@passion_hd/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4%EB%A1%9C-3%EA%B3%84%EC%B8%B5-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 14 Feb 2024 13:52:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>구성 환경</strong> 🧐</p>
</blockquote>
<ul>
<li><p>도커 실습 시 실시했던 3계층 ( 프론트엔드, 백엔드, DB ) 아키텍처를 쿠버네티스로 구현해보려 한다.</p>
</li>
<li><p>각각의 서버의 Pod 는 <strong>Deployment의 ReCreate 방식을 사용하여 생성</strong>할 예정이다.</p>
</li>
<li><p>쿠버네티스는 지난 글에서 설치한 상태에서 이어서 진행한다.</p>
</li>
</ul>
<hr>
<blockquote>
<p>🐶 ** DB 서버 설정하기**</p>
</blockquote>
<p><strong>1. PV ( Persistent Volume ) 를 생성한다.</strong>
&nbsp;　➡ PV는 원래 스토리지 서버를 따로 두는데, 지금은 그럴 수 없기 때문에 <code>worker01</code>
&nbsp;&nbsp;　　컴퓨터를 스토리지 서버라 생각하고 설정한다.</p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolume
metadata:
  name: db-pv  // 원하는 이름으로 지정
spec:
  capacity:
    storage: 5G  // 컴퓨터 사양 고려 설정
  accessModes:
    - ReadWriteOnce
  local:
    path: /mysql-vol  // 원하는 경로로 지정
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - {key: kubernetes.io/hostname, operator: In, values: [&#39;worker01&#39;]}</code></pre>
<p>&nbsp;　➡ 이때 지정한 <code>/mysql-vol</code> 디렉토리를 실제로 <code>worker01</code> 컴퓨터에서 생성해줘야 된다.</p>
<hr>
<p><strong>2. PVC ( Persistent Volume Claim ) 를 생성한다.</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-pvc   // 원하는 이름으로 지정
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5G  // 컴퓨터 사양 고려 설정</code></pre>
<hr>
<ul>
<li>PV 와 PVC 가 정상적으로 생성됬다면, 아래 그림처럼 각각 설정한 이름으로 연결되어 있다는 것을 볼 수 있다.
<img src="https://velog.velcdn.com/images/passion_hd/post/0c430f6d-721c-4805-8e00-f60c842ad482/image.png" alt=""><img src="https://velog.velcdn.com/images/passion_hd/post/a91bd111-a69e-4186-b045-c08eb842c400/image.png" alt=""></li>
</ul>
<hr>
<p><strong>3. DB 서버용 &quot;ConfigMap&quot; 을 생성한다.</strong></p>
<p>&nbsp;　➡ ConfigMap 은 환경변수들을 설정하기 위한 것이라고 보면 된다.</p>
<p>&nbsp;　➡ 하지만, 암호화 기능이 없어서 비밀번호 등 민감한 변수가 그대로 노출되기 때문에, 
&nbsp;&nbsp;&nbsp;　　Secret을 실제론 사용한다고 한다.</p>
<pre><code class="language-yaml">apiVersion: v1
kind: ConfigMap
metadata:
  name: db-config  // 원하는 이름으로 지정
data:
  MYSQL_ROOT_PASSWORD: qwer1234  // root 계정 원하는 패스워드 입력
  MYSQL_DATABASE: lonua  // 초기 생성 데이터베이스 지정 (백엔드 서버와 연동해주기 위해 설정)</code></pre>
<hr>
<p><strong>4. DB 서버 Deployment 를 생성한다.</strong>
&nbsp;　➡  Deployment에는 ReplicaSet 이 포함되어 있어서, 주로 사용한다고 한다.</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: db-deployment  // 원하는 이름으로 지정
spec:
  replicas: 1  // 초기에 생성할 Pod 개수 입력
  strategy:
    type: Recreate
  revisionHistoryLimit: 1 // 버전 업데이트 시 레플리카 셋을 지정한 개수 만큼 남겨놓도록 하는 옵션 설정
  selector:
    matchLabels:
      type: mysql  // 바로 아래의 &quot;labels&quot; 와 서비스 생성 시에도 동일하게 설정해줄 라벨
  template:
    metadata:
      labels:
        type: mysql  // 바로 위에서 지정한 라벨명과 동일하게 설정
    spec:
      nodeSelector:
        kubernetes.io/hostname: worker01  // 원래는 필요없으나, 스토리지 서버가 없어서 설정
      containers:
        - name: mysql
          image: mysql:latest // 도커 허브의 mysql 이미지
          envFrom:
            - configMapRef:
                name: db-config  // 위에서 생성한 ConfigMap 이름
          volumeMounts:
            - name: db-vol  // 원하는 이름으로 지정하나 아래와 동일하게 맞춰줘야함
              mountPath: /var/lib/mysql  // mysql에서 데이터를 다루는 경로
      terminationGracePeriodSeconds: 5
      volumes:
        - name: db-vol  // 원하는 이름으로 지정하나 위와 동일하게 맞춰줘야함
          persistentVolumeClaim:
            claimName: db-pvc // 2번에서 생성한 PVC 이름</code></pre>
<ul>
<li><p>아래처럼 정상적으로 Deployment가 생성되었고,
<img src="https://velog.velcdn.com/images/passion_hd/post/e2487ad9-23e4-4be0-8c00-e3fcddce714c/image.png" alt=""></p>
</li>
<li><p>생성된 Pod의 bash 창으로 mysql 서버에 로그인해서 데이터베이스를 확인해보면 지정한 &quot;lonua&quot; 데이터베이스가 생성되어 있는 것을 확인할 수 있다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/6f4a87ac-a792-4f49-b3a8-2654943faabb/image.png" alt=""></p>
<hr>
<p><strong>5. DB 서버에 대한 서비스를 생성한다.</strong>
&nbsp;　➡ 백엔드 서버에서 DB 서버로 접근하기 위해 서비스를 생성해준다. 
&nbsp;　➡ 이때, Pod 간의 통신이므로 서비스 유형은 기본값인 <code>Cluster Ip</code>로 구성한다.</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: db-svc  // 원하는 이름으로 지정
spec:
  selector:
    type: mysql  // 위에서 Deployment 생성시 지정한 라벨명과 동일하게 적어준다
  ports:
    - port: 3306
      targetPort: 3306</code></pre>
<ul>
<li>여기까지 하면, DB 서버 설정이 끝난다.</li>
</ul>
<hr>
<blockquote>
<p>🐱** 백엔드 서버 설정하기**</p>
</blockquote>
<p><strong>1. 백엔드 서버용 &quot;ConfigMap&quot; 을 생성한다.</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: ConfigMap
metadata:
  name: backend-config
data:
  APP_PASSWORD: 
  AWS_S3_ACCESS_KEY: 
  AWS_S3_SECRET_KEY: 
  BRAND_BUCKET: 
  CLIENT_ID:
  EXPIRED_TIME: 
  JWT_SECRET_KEY: 
  MAIL_SENDER: 
  MASTER: root
  MASTER_PW: qwer1234
  // 여기서 DB 주소는 Pod 간의 통신이므로 위에서 생성한 DB 서비스의 이름으로 설정해줄 수 있다.
  MASTER_URL: jdbc:mysql://db-svc:3306/lonua  
  PORTONE_KEY: 
  PORTONE_SECRETKEY: 
  PRODUCT_BUCKET: 
  PRODUCT_INTROD_BUCKET: 
  REGION:
  REVIEW_BUCKET: 
  SLAVE: root
  SLAVE_PW: qwer1234
  SLAVE_URL: jdbc:mysql://db-svc:3306/lonua</code></pre>
<p>&nbsp;　➡ 환경변수들을 적어줄때, 숫자로만 되어있는 값은 <code>&quot; &quot;</code> 를 해줘야된다. 예를 들어, 
&nbsp;&nbsp;&nbsp;　　EXPIRED_TIME 을 300000으로 설정하고 싶다면 <code>&quot;300000&quot;</code> 으로 적어야 오류가 안난다.</p>
<hr>
<p><strong>2. 배포할 백엔드 서버 이미지를 생성한다.</strong></p>
<p><strong>3. 생성한 이미지를 도커 허브에 푸쉬한다.</strong></p>
<p>&nbsp;　➡ 이미지를 생성하고 푸쉬하는 방법은 2가지 방법이 있을 것 같다.</p>
<p>&nbsp;　　　1) 컴퓨터 사양때문에 쿠버네티스와 도커 데스크탑을 동시에 운용을 못한다면, 
&nbsp;&nbsp;　　　　쿠버네티스 master 컴퓨터로 jar 파일을 옮기고, Dockerfile을 생성하여 명령어로 
&nbsp;&nbsp;　　　　이미지 생성 및 푸쉬하는 방법</p>
<p>&nbsp;　　　2) 도커 데스크탑을 이용하여 이미지 생성 및 푸쉬하는 방법</p>
<p>&nbsp;　➡  도커 데스크탑을 이용하면 간단하겠지만, 이미지 생성하는 것을 연습하는 차원에서 
&nbsp;　　　1번 방식으로 해보겠다.</p>
<p><strong>1) 먼저 Filezilla 프로그램을 이용하여 jar 파일을 master 리눅스 컴퓨터로 옮긴다.</strong></p>
<p><strong>2) 옮긴 경로에서 도커파일을 생성한다 : <code>vi Dockerfile</code></strong></p>
<pre><code class="language-html">FROM openjdk:11-jdk-slim-stretch
COPY ./lonua-0.0.1-SNAPSHOT.jar lonua-0.0.1-SNAPSHOT.jar
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;/lonua-0.0.1-SNAPSHOT.jar&quot;]</code></pre>
<hr>
<p><strong>3) 이미지를 생성한다 : <code>docker build --tag [도커허브 계정명]/[레포지토리명]:버전 .</code></strong></p>
<p><strong>4) 생성한 이미지를 도커허브에 푸쉬한다</strong></p>
<p>&nbsp;　➡  <code>docker push [도커허브 계정명]/[레포지토리명]:버전</code></p>
<p>&nbsp;　➡  푸쉬하기 위해서는 <strong>docker login</strong> 을 해야지만 가능하다.</p>
<p>&nbsp;　➡  만약, 이메일 계정을 사용하는데 도커 로그인이 안된다면 도커 허브 웹사이트에서 
&nbsp;&nbsp;&nbsp;　　로그인 후, <strong>My Account 클릭 - Change Password</strong>로 비밀번호를 새로 설정해줘야 
&nbsp;&nbsp;&nbsp;　　가능하다.</p>
<hr>
<p><strong>4. 백엔드 서버 Deployment 를 생성한다.</strong></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-deployment
spec:
  replicas: 1
  strategy:
    type: Recreate
  revisionHistoryLimit: 1
  selector:
    matchLabels:
      type: backend
  template:
    metadata:
      labels:
        type: backend
    spec:
      containers:
      - name: backend
        image: [위에서 푸쉬한 이미지]
        envFrom:
        - configMapRef:
            name: backend-config</code></pre>
<ul>
<li><p>정상적으로 실행이 되었고, DB의 배쉬창으로 조회 해보니 데이터도 잘 들어가있었다.</p>
</li>
<li><p>한글이 지원이 안되다 보니 데이터가 ? 로 출력되긴 하는데, 프론트엔드와 연결했을때 잘 불러와지는지 설정을 마치고 테스트 해보겠다.
<img src="https://velog.velcdn.com/images/passion_hd/post/ceb680e0-7756-4659-a30e-1896b593d7bd/image.png" alt=""></p>
</li>
</ul>
<hr>
<p><strong>5. 백엔드 서버 서비스를 생성한다. ( DB와 마찬가지로 Cluster IP 로 생성 )</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: backend-svc
spec:
  selector:
    type: backend
  ports:
  - port: 8080
    targetPort: 8080</code></pre>
<ul>
<li><p>Cluster IP 로 설정하는 사유는, 프론트엔드 서버에서 요청을 하면, 리버스 프록시 설정으로 
백엔드 서버 Pod 로 요청을 보내줘 Pod간의 통신이 되기 때문이다.</p>
</li>
<li><p>여기까지 하면, 백엔드 서버의 설정이 끝난다.</p>
</li>
</ul>
<hr>
<blockquote>
<p>🐹** 프론트엔드 서버 설정하기**</p>
</blockquote>
<p><strong>1. Vue 프로젝트에서 배포하기 위해 빌드 : <code>npm run build</code></strong></p>
<p>&nbsp;➡ 이때, 백엔드 서버를 호출하는 주소는 
<code>http://[아래에서 생성된 로드밸런서 서비스의 엔드포인트 IP]:80/api</code> 로 설정하였다.</p>
<p> &nbsp;➡ 이부분은 순서를 고려해봐야될 것 같다. 여기서 설정되는 프론트서버의 IP는 
 &nbsp;&nbsp;　아래에서 서비스를 생성할때 알 수 있다. 하지만, Deployment의 경우 일단 만들어 
 &nbsp;&nbsp;　놓고, 나중에 백엔드 호출 주소를 수정 후 이미지를 새로 생성해서, 리소스 편집으로 
 &nbsp;&nbsp;　이미지 버전만 바꿔주면 적용되긴 한다.</p>
<hr>
<p><strong>2. 생성된 dist 폴더안의 파일들을 FileZilla를 이용하여 미리 생성해둔 /frontend 경로로 옮겨준다.</strong></p>
<p><strong>3. nginx 설정파일을 생성한다 : <code>vi default.conf</code></strong></p>
<pre><code class="language-html">server {
  listen       80;
  server_name  localhost;
  #access_log  /var/log/nginx/host.access.log  main;
  location /api {
      rewrite ^/api(.*)$ $1 break;
      proxy_pass http://backend-svc:8080;  // 위에서 생성한 백엔드 서버 서비스 이름
      proxy_set_header Host $http_host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Real-IP $remote_addr;
  }
  location / {
      alias   /usr/share/nginx/html/;
      try_files $uri $uri/ /index.html;
  }
  #error_page  404              /404.html;
  # redirect server error pages to the static page /50x.html
  #
  error_page   500 502 503 504  /50x.html;
  location = /50x.html {
      root   /usr/share/nginx/html;
  }
  # proxy the PHP scripts to Apache listening on 127.0.0.1:80
  #
  #location ~ \.php$ {
  #    proxy_pass   http://127.0.0.1;
  #}
  # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
  #
  #location ~ \.php$ {
  #    root           html;
  #    fastcgi_pass   127.0.0.1:9000;
  #    fastcgi_index  index.php;
  #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
  #    include        fastcgi_params;
  #}
  # deny access to .htaccess files, if Apache&#39;s document root
  # concurs with nginx&#39;s one
  #
  #location ~ /\.ht {
  #    deny  all;
  #}
}</code></pre>
<hr>
<p><strong>4. 도커파일을 생성한다 : <code>vi Dockerfile</code></strong></p>
<pre><code class="language-html"> FROM nginx:latest
 ADD ./css /usr/share/nginx/html/css
 ADD ./fonts /usr/share/nginx/html/fonts
 ADD ./img /usr/share/nginx/html/img
 ADD ./js /usr/share/nginx/html/js
 ADD ./styles.css /usr/share/nginx/html/styles.css
 ADD ./logo.png /usr/share/nginx/html/logo.png
 RUN rm -rf /usr/share/nginx/html/index.html
 ADD ./index.html /usr/share/nginx/html/index.html
 RUN rm -rf /etc/nginx/conf.d/default.conf
 ADD ./default.conf /etc/nginx/conf.d/default.conf
 CMD [&quot;nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]</code></pre>
<hr>
<p><strong>5. 이미지를 생성한다 : <code>docker build --tag [도커허브 계정명]/[레포지토리명]:버전 .</code></strong></p>
<p><strong>6. 생성한 이미지를 도커 허브에 푸쉬한다.</strong>
 &nbsp;　➡ <code>docker push [도커허브 계정명]/[레포지토리명]:버전</code></p>
<hr>
<p><strong>7. 프론트엔드 서버 Deployment 생성한다.</strong></p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-deployment
spec:
  replicas: 1
  strategy:
    type: Recreate
  revisionHistoryLimit: 1
  selector:
    matchLabels:
      type: frontend
  template:
    metadata:
      labels:
        type: frontend
    spec:
      containers:
      - name: frontend
        image: [위에서 푸쉬한 이미지]</code></pre>
<hr>
<p><strong>8. 프론트엔드 서버 서비스를 생성한다. ( 운영을 위한 것으로 LoadBalencer 로 생성 )</strong>
 &nbsp;　➡ 개발 및 테스트 목적으로 서비스를 생성할때는 <code>NodePort</code> 유형으로 서비스를 
  &nbsp;&nbsp;　　생성했었는데, 이제는 운영을 위한 목적이라 생각하고 <code>LoadBalencer</code> 유형으로 
  &nbsp;&nbsp;　　생성해보겠다.</p>
<ul>
<li><p>LoadBalencer 설정을 위한 사전 설치 : 무료로 사용 가능한 <code>MetalLB</code> 이용
&nbsp;➡ <a href="https://mlops-for-all.github.io/docs/appendix/metallb/">설치 가이드 바로가기</a> / 가이드에 나와있는 순서대로 진행하면 된다.</p>
<p><strong>1) 현재 모드 확인</strong></p>
<blockquote>
<p>kubectl get configmap kube-proxy -n kube-system -o yaml | <br>grep strictARP</p>
</blockquote>
<h2 id="➡-strictarp-false-라고-출력됨">&nbsp;➡ <strong>strictARP: false 라고 출력됨</strong></h2>
<p><strong>2) 아래를 실행하여 true로 변경</strong></p>
<blockquote>
<p>kubectl get configmap kube-proxy -n kube-system -o yaml | <br>sed -e &quot;s/strictARP: false/strictARP: true/&quot; | <br>kubectl apply -f - -n kube-system</p>
</blockquote>
<hr>
<p>&nbsp;➡  <strong>정상적으로 수행되면 아래와 같이 출력된다.</strong></p>
<blockquote>
<p>Warning: resource configmaps/kube-proxy is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
configmap/kube-proxy configured</p>
</blockquote>
<hr>
<p><strong>3) MetalLB 설치</strong></p>
<p><code>kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.11.0/manifests/namespace.yaml</code></p>
<p><code>kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.11.0/manifests/metallb.yaml</code></p>
<p>&nbsp;➡ 정상설치 확인 : <code>kubectl get pod -n metallb-system</code>  실행 시 전부 &quot;Running&quot; 상태</p>
<h2 id=""><img src="https://velog.velcdn.com/images/passion_hd/post/f6a54c5c-9d9d-436e-b919-2f2184bce4ad/image.png" alt=""></h2>
<p>&nbsp;➡ 만약, 시간이 지나도 계속 아래 사진과 같이 출력된다면
<img src="https://velog.velcdn.com/images/passion_hd/post/1fa50c4f-9f89-4aef-8126-8de1d9d3d067/image.png" alt=""></p>
<ul>
<li>아래 명령어 실행
<code>kubectl create secret generic -n metallb-system memberlist --from-literal=secretkey=&quot;$(openssl rand -base64 128)&quot;</code></li>
</ul>
</li>
</ul>
<hr>
<p><strong>4) 설정파일 생성 : <code>vi metallb_config.yaml</code></strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    address-pools:
    - name: default
      protocol: layer2
      addresses:
      - 77.77.77.50-77.77.77.90  // master와 worker01/02 에서 쓰는 IP 대역 피해서 지정</code></pre>
<p><strong>5) 설정한 내용 적용 : <code>kubectl apply -f metallb_config.yaml</code></strong></p>
<ul>
<li>여기까지하면 MetalLB 설치가 끝나고, 이제 LoadBalancer 유형을 적용한 서비스를 생성할 수 있다.</li>
</ul>
<hr>
<ul>
<li><strong>프론트엔드 서버 서비스 생성</strong></li>
</ul>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
 name: frontend-svc
spec:
 selector:
   type: frontend
 ports:
  - port: 80
    targetPort: 80
 type: LoadBalancer</code></pre>
<p>  &nbsp;➡  로드밸런서로 서비스를 생성하면 아래처럼 <strong>외부 엔드포인트가 설정</strong>되어있다.
  <img src="https://velog.velcdn.com/images/passion_hd/post/aa8bc3c6-cbbb-4c15-b5a0-3deafd0b0c36/image.png" alt=""></p>
<p>  &nbsp;➡  그럼 해당 <strong>엔드포인트로 접속</strong>해보면, 프론트엔드 서버가 정상적으로 접속이 될 것이고 
  &nbsp;&nbsp;　아래처럼 DB에서 상품 데이터를 잘 불러오고 있는 것을 확인할 수 있다.
  <img src="https://velog.velcdn.com/images/passion_hd/post/d8716b37-2c34-4236-8af7-62cf3badc420/image.png" alt=""></p>
<hr>
<ul>
<li>만약 여기서 서버에 트래픽이 몰려서 서버를 늘려줘야 된다면, 디플로이먼트의 </li>
<li>*&quot;스케일만 증가&quot;** 시켜주면 알아서 Pod를 늘려줘서 편하게 서버를 관리할 수 있다.
<img src="https://velog.velcdn.com/images/passion_hd/post/59d548b7-6bd3-418d-917a-29618507b1a3/image.png" alt=""></li>
</ul>
<hr>
<blockquote>
<p>🐮** Metrics Server 설치하기**</p>
</blockquote>
<ul>
<li><p>마지막으로, CPU 와 메모리 사용량에 따라 자동으로 서버를 늘려줄 수 있도록 설정하는 내용이다.</p>
</li>
<li><p>일단, 지금은 아래처럼 CPU 와 메모리가 아무것도 표시되어 있지 않은 상태다.
<img src="https://velog.velcdn.com/images/passion_hd/post/1687a32c-43a5-4b44-ae8f-82d4f70f1efe/image.png" alt=""></p>
</li>
<li><p><strong>이제 Metrics Server를 설치해보겠다.</strong></p>
<p><code>kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.6.3/components.yaml</code></p>
</li>
</ul>
<hr>
<ul>
<li>설치가 됬다면, 대시보드에서 <code>kube-system</code> 네임스페이스로 들어가서 파드를 보면 metrice-server 파드가 있을 것이다. ( 디플로이먼트도 생겨있을 것이다. )
<img src="https://velog.velcdn.com/images/passion_hd/post/19a8f44b-f2e8-4b17-8a63-a659358df1d6/image.png" alt=""></li>
</ul>
<hr>
<ul>
<li>그러면 이제 <strong>디플로이먼트의 metrics-server</strong> 로 들어가서 리소스 편집을 클릭 후 
174번째 줄 아래에 이 내용을 추가한다 : <code>- &#39;--kubelet-insecure-tls&#39;</code>
<img src="https://velog.velcdn.com/images/passion_hd/post/c3995c81-fd0d-4a69-9318-7fbcbbeb09e8/image.png" alt=""></li>
</ul>
<hr>
<ul>
<li>정상적으로 설정이 됬다면, 잠시 기다리다 보면 CPU 와 메모리 사용량이 아래 처럼
보이게 될 것이다.
<img src="https://velog.velcdn.com/images/passion_hd/post/797f9f7b-0d52-4e6a-944a-98929977e78a/image.png" alt=""></li>
</ul>
<hr>
<ul>
<li><p>오늘은 Deployment 의 여러가지 배포 방식 중 ReCreate 를 이용한 방식을 실습해봤다. 하지만 ReCreate 방식은 버전 수정 또는 업그레이드 시 기존 Pod가 삭제되고, 새로 생성되는 동안 서비스가 잠시 중단되는 문제가 있다.</p>
</li>
<li><p>따라서, 이것을 해결하여 무중단 배포를 하기 위한 실습이 이어질 예정이다.</p>
</li>
</ul>
<hr>
<blockquote>
<p>** 실습 진행 간 발생한 오류 내용 정리** ✍</p>
</blockquote>
<ul>
<li>디플로이먼트 생성 시 아래와 같은 오류가 뜨면서, Pod가 2개를 설정했다면 1개밖에 생성이 안됬는데, 그럴때는 문제가 되는 worker 컴퓨터를 재부팅하니 해결되었다.</li>
</ul>
<blockquote>
<p>Failed to create pod sandbox: rpc error: code = Unknown desc = [failed to set up sandbox container &quot;35d064b6feedd643f61af4fb2ae7ff7582b003bb32bf16ffbe7265eb80fc98bf&quot; network for pod &quot;deployment-1-56b547b87f-x8tc6&quot;: networkPlugin cni failed to set up pod &quot;deployment-1-56b547b87f-x8tc6_kube-public&quot; network: error getting ClusterInformation: connection is unauthorized: Unauthorized, failed to clean up sandbox container &quot;35d064b6feedd643f61af4fb2ae7ff7582b003bb32bf16ffbe7265eb80fc98bf&quot; network for pod &quot;deployment-1-56b547b87f-x8tc6&quot;: networkPlugin cni failed to teardown pod &quot;deployment-1-56b547b87f-x8tc6_kube-public&quot; network: error getting ClusterInformation: connection</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠버네티스 초기 환경 구축하기]]></title>
            <link>https://velog.io/@passion_hd/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%B4%88%EA%B8%B0-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-1i34xf16</link>
            <guid>https://velog.io/@passion_hd/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%B4%88%EA%B8%B0-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-1i34xf16</guid>
            <pubDate>Wed, 14 Feb 2024 10:32:46 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>🦁 ** 쿠버네티스 초기 환경 구축하기** </p>
</blockquote>
<ul>
<li><p><strong>사전 준비사항 : VMWare 이용 &quot; CentOs 8 &quot; 가상머신 3대 ( IP 설정 ) 설치</strong></p>
<p>➡ 이름 : master, worker01, worker02</p>
<p>➡ 메모리 및 CPU 설정 후 시작 ( 메모리 : 4GB, CPU : 2개 )
   &nbsp;　➡ 개인 컴퓨터 사양에 따라 다름
   &nbsp;　➡ 설정법 : &quot; Edit virtual machine settings &quot; 에서 설정 ( 가상머신 시작하기 전 )
   <img src="https://velog.velcdn.com/images/passion_hd/post/6d6ef9ec-eeb8-486a-928a-7e8f18e74868/image.png" alt=""></p>
</li>
</ul>
<hr>
<ul>
<li><p>✅ <strong>가상머신 3대 공통 설정</strong></p>
<p><strong>1) 각 컴퓨터의 이름 지정 : <code>vi /etc/hostname</code></strong>
  &nbsp;　➡ master / worker01 / worker02 로 각각 설정</p>
<p><strong>2) 호스트 파일 설정 : <code>vi /etc/hosts</code></strong></p>
<pre><code class="language-html">   [master IP 주소]    master
   [worker1 IP 주소]    worker01
   [worker2 IP 주소]    worker02</code></pre>
<p>*<em>3) 재부팅 실시 : <code>init 6</code> *</em></p>
<img src="https://velog.velcdn.com/images/passion_hd/post/1855c882-c26a-488f-bb15-bb95db0aa09b/image.png">

<p>&nbsp;　➡ 정상적으로 설정이 되었다면, Putty로 접속했을 시 각 컴퓨터의 이름이 바뀌어있을 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;　것이고, 컴퓨터의 이름으로 ping을 보내보면 정상적으로 보내질 것이다.</p>
<p><strong>4) 방화벽 2개를 모두 해제</strong>
  &nbsp;　➡ <code>systemctl stop firewalld</code>
  &nbsp;　➡ 재부팅 시에도 꺼주는 설정 : <code>systemctl disable firewalld</code></p>
<p>&nbsp;　➡ <code>setenforce 0</code>
  &nbsp;　➡ 재부팅 시에도 꺼주는 설정 : <code>sed -i &#39;s/SELINUX=enforcing/SELINUX=disabled/&#39; /etc/selinux/config</code></p>
<hr>
<p><strong>5) 네트워크 브리지 및 iptables 호출 활성화</strong>
   &nbsp;　➡ <code>modprobe br_netfilter</code>
  &nbsp;　➡ <code>echo &#39;1&#39; &gt; /proc/sys/net/bridge/bridge-nf-call-iptables</code></p>
<hr>
<p><strong>6) 도커 설치</strong>
   &nbsp;　➡ <code>yum install -y yum-utils</code>
  &nbsp;　➡ <code>yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo</code>
  &nbsp;　➡ <code>yum-config-manager --enable docker-ce-nightly</code>
  &nbsp;　➡ <code>yum-config-manager --enable docker-ce-test</code>
  &nbsp;　➡ <code>yum install -y docker-ce docker-ce-cli containerd.io --allowerasing</code></p>
<hr>
<p><strong>7) 도커 스토리지 설정파일 수정</strong></p>
<blockquote>
<p><strong>cat &lt;&lt;EOF | sudo tee /etc/docker/daemon.json
{
&quot;exec-opts&quot;: [&quot;native.cgroupdriver=systemd&quot;],
&quot;log-driver&quot;: &quot;json-file&quot;,
&quot;log-opts&quot;: {
&quot;max-size&quot;: &quot;100m&quot;
},
&quot;storage-driver&quot;: &quot;overlay2&quot;
}
EOF</strong></p>
</blockquote>
<hr>
<p><strong>8)  도커 설치를 끝내고, 실행</strong>
&nbsp;　➡ <code>systemctl daemon-reload</code>
&nbsp;　➡ <code>systemctl restart docker</code>
&nbsp;　➡ <code>systemctl enable docker</code></p>
<hr>
<ul>
<li><strong>여기서부터 쿠버네티스 설치 시작</strong></li>
</ul>
<p><strong>9) 쿠버네티스 클러스터를 구성할 때 필요한 네트워크 설정</strong>
<code>cat &lt;&lt;EOF &gt; /etc/sysctl.d/k8s.conf</code>
<code>net.bridge.bridge-nf-call-ip6tables = 1</code>
<code>net.bridge.bridge-nf-call-iptables = 1</code>
<code>EOF</code></p>
</li>
</ul>
<p>&nbsp;　　➡ 설정이 추가되었는지 확인 : <code>sysctl --system</code>
<img src="https://velog.velcdn.com/images/passion_hd/post/9c36a397-e125-422c-b7d3-d85fb587e18e/image.png"></p>
<hr>
<p>&nbsp;　<strong>10) 쿠버네티스 레포지토리 설치</strong></p>
<pre><code class="language-yaml">cat &lt;&lt;EOF &gt; /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOF</code></pre>
<hr>
<p><code>dnf install -y kubelet-1.22.5 kubeadm-1.22.5 kubectl-1.22.5 --disableexcludes=kubernetes</code>    </p>
<p><code>systemctl enable kubelet</code></p>
<p><code>systemctl start kubelet</code></p>
<hr>
<p>&nbsp;　<strong>11) 쿠버네티스 swap 비활성화</strong>
&nbsp;　　　➡ 이유 : 쿠버네티스는 컨테이너를 메모리에서 동작시켜 효율적으로 관리하기 위해 
&nbsp;&nbsp;　　　　　　　사용하는 것인데, swap 메모리는 메모리가 부족할 시 디스크를 이용하여 
&nbsp;&nbsp;　　　　　　　부족한 메모리를 대체하도록 하는 것이다.</p>
<p>&nbsp;&nbsp;　　　　　　　swap 메모리를 활성화 시켜놓으면, 메모리 부족 시 하드디스크에서 
&nbsp;&nbsp;　　　　　　　컨테이너가 동작토록 되는데 그렇게 되면 서비스의 성능이 저하될 것이다. 
&nbsp;&nbsp;　　　　　　　즉, 쿠버네티스를 사용하는 이유가 사라진다.</p>
<p>&nbsp;　　　➡ <code>swapoff -a</code></p>
<p>&nbsp;　　　➡ 부팅 설정 파일에서 편집 : <code>vi /etc/fstab</code> 
&nbsp;　　　　　➡ <code>/dev/mapper/cs-swap none swap defaults 0 0</code> 주석 처리
<img src="https://velog.velcdn.com/images/passion_hd/post/8ac51476-6dfc-4171-b602-e5d5dfbc4ffe/image.png" alt=""></p>
<hr>
<p>&nbsp;　<strong>12) 컴퓨터 종료 및 1차 스냅샷 찍기 : <code>sudo poweroff</code></strong>
&nbsp;　　　➡ 대시보드 설치 후 스냅샷을 찍으니, 대시보드 오류 시 재설치 시 처음부터 다시<br>&nbsp;&nbsp;　　　　설치해야되기 때문에, 여기서 한번 끊어주면서 스냅샷을 찍어주는게 좋은 것 같다.
  <img src="https://velog.velcdn.com/images/passion_hd/post/61c9aef2-28fe-4e8c-8024-3db3dbb1da01/image.png" alt=""></p>
<hr>
<ul>
<li><p>✅ <strong>여기까지 하면, 컴퓨터 3대의 공통 설정이 끝난다. 다음은 <code>master 컴퓨터 설정</code>이다.</strong></p>
<p><strong>1) 쿠버네티스에서 사용할 가상의 네트워크 대역 설정(컴퓨터 IP와 다른 임의의 IP)</strong></p>
<p>&nbsp;　➡ <code>kubeadm init --pod-network-cidr 100.100.100.0/24</code></p>
<p>&nbsp;　➡ 제일 마지막 줄에서 나오는 <code>kubeadm join ~~</code> &nbsp;부분을 복사해 놓는다.
<img src="https://velog.velcdn.com/images/passion_hd/post/69ea1be4-ab2b-4baa-9eb4-bc202fea84f2/image.png" alt=""></p>
<hr>
<p><strong>2) 쿠버네티스 명령어를 편하게 사용하기 위한 설정</strong>
&nbsp;　➡ <code>mkdir -p $HOME/.kube</code>
&nbsp;　➡ <code>cp -i /etc/kubernetes/admin.conf $HOME/.kube/config</code>
&nbsp;　➡ <code>chown $(id -u):$(id -g) $HOME/.kube/config</code></p>
<hr>
<p><strong>3) 설정이 제대로 됬는 지 확인 : <code>kubectl get nodes</code></strong>
&nbsp;　➡ 아래 사진처럼 출력되면 된 것이다.
<img src="https://velog.velcdn.com/images/passion_hd/post/4df885b6-0498-454c-af67-3d2755b8ef49/image.png" alt=""></p>
</li>
</ul>
<hr>
<ul>
<li><p>✅ <strong>이제부터는 worker01 / 02 컴퓨터에 대한 설정이다.</strong></p>
<p><strong>1) 위에서 복사해둔 <code>kubeadm join ~~</code> 을 붙여넣는다.</strong></p>
<p><strong>2) master 컴퓨터에서 설정이 잘 됬는지 확인한다.</strong>
&nbsp;　➡ <code>kubectl get pod -n kube-system</code>  : 1, 2번째 빼고 모두 다 &quot;Running&quot; 이어야 함
&nbsp;　➡ <code>kubectl get nodes</code> : 컴퓨터 3대 모두 &quot;NotReady&quot; 상태
<img src="https://velog.velcdn.com/images/passion_hd/post/8fb09447-cff0-453a-86b6-fed624ce6ce4/image.png" alt=""></p>
</li>
</ul>
<hr>
<ul>
<li><p>✅ <strong>다시 master 컴퓨터에서의 설정이다.</strong></p>
<p><strong>1) 네트워크 CNI 설정 ( Calico )</strong>
&nbsp;　➡ <code>curl https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml -O</code></p>
<hr>
<p><strong>2) yml 파일 수정 : <code>vi calico.yaml</code></strong>
  &nbsp;　➡ 줄번호 표시 : <code>:set nu</code>
  &nbsp;　➡ 4931번째 줄로 이동 : <code>:4931</code>
  &nbsp;　➡ 아래처럼 주석을 제거하고, 설정할 IP를 지정해준다.
  &nbsp;&nbsp;　　( 주석 제거 시 : # 과 띄어쓰기 1칸까지 지워야함 )</p>
<pre><code class="language-linux">  # - name: CALICO_IPV4POOL_CIDR
   #   value: &quot;192.168.0.0/16&quot;

  --------------위 부분을 아래 처럼 주석 제거---------------
  - name: CALICO_IPV4POOL_CIDR
    value: &quot;200.200.200.0/24&quot;</code></pre>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/7da25249-2188-4105-a4a4-edbb729b4759/image.png" alt=""> </p>
<hr>
<p>   <strong>3) 설정한 야멜 파일 적용 : <code>kubectl apply -f calico.yaml</code></strong></p>
<p>   <strong>4) 제대로 적용 됬는지 확인</strong></p>
<p>   &nbsp;　➡ <code>kubectl get pod -n kube-system</code> : 모든 컨테이너들이 &quot;Running&quot; 상태</p>
<p>   &nbsp;　➡ <code>kubectl get nodes</code> : 컴퓨터 3대 모두 &quot;Ready&quot; 상태
   <img src="https://velog.velcdn.com/images/passion_hd/post/08967c05-eabc-4562-9f7c-e6fb6386096e/image.png" alt=""></p>
<hr>
<p>   <strong>5) 대시보드 설정</strong>
   &nbsp;　➡ wget 설치 : <code>yum install -y wget</code>
   &nbsp;　➡ 대시보드 설치 : <code>wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml</code>
   &nbsp;　➡ 야멜 파일 편집 : <code>vi recommended.yaml</code>
   &nbsp;　　　➡ 45번째 줄에 <code>type: NodePort</code> 추가
   <img src="https://velog.velcdn.com/images/passion_hd/post/10882f81-1506-4319-a59f-3bcd0b0ad41a/image.png" alt=""></p>
<hr>
<p>   <strong>6) 야멜 파일 적용 : <code>kubectl apply -f recommended.yaml</code></strong></p>
<p>   <strong>7) 443번 포트와 연결된 포트번호 확인</strong></p>
<p> &nbsp;　➡ <code>kubectl get services -n kubernetes-dashboard</code></p>
<p>   <img src="https://velog.velcdn.com/images/passion_hd/post/3c38efa3-fce3-42b2-b581-fb2db7831859/image.png" alt=""></p>
<hr>
<p>   <strong>8) 웹브라우저로 대시보드 접속 : <code>https://master 컴퓨터 IP:확인한 포트번호</code></strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/08b94462-3f66-464f-97e4-03167229e237/image.png" alt=""></p>
<p> &nbsp;　➡  위처럼, 비공개 설정이 뜨면, 아무대나 마우스 클릭 후 키보드로 
 &nbsp;&nbsp;　　<code>thisisunsafe</code> 입력</p>
<p> &nbsp;　➡  그러면 아래처럼 로그인 창이 등장할 것이다.
 <img src="https://velog.velcdn.com/images/passion_hd/post/08a60945-36a2-4f91-b6f4-45546bf26566/image.png" alt=""></p>
<hr>
<p>  <strong>9) 로그인하기 위한 토큰 생성</strong></p>
<p><code>cat &lt;&lt;EOF | kubectl create -f -</code>
  <code>apiVersion: v1</code>
  <code>kind: ServiceAccount</code>
  <code>metadata:</code>
  &nbsp;&nbsp;&nbsp;<code>name: admin-user</code>
  &nbsp;&nbsp;&nbsp;<code>namespace: kube-system</code>
  <code>EOF</code>
  <br>
  <code>cat &lt;&lt;EOF | kubectl create -f -</code>
  <code>apiVersion: rbac.authorization.k8s.io/v1</code>
  <code>kind: ClusterRoleBinding</code>
  <code>metadata:</code>
  &nbsp;&nbsp;<code>name: admin-user</code>
  <code>roleRef:</code>
  &nbsp;&nbsp;<code>apiGroup: rbac.authorization.k8s.io</code>
  &nbsp;&nbsp;<code>kind: ClusterRole</code>
  &nbsp;&nbsp;<code>name: cluster-admin</code>
  <code>subjects:</code>
  <code>- kind: ServiceAccount</code>
  &nbsp;&nbsp;<code>name: admin-user</code>
  &nbsp;&nbsp;<code>namespace: kube-system</code>
  <code>EOF</code></p>
<hr>
<p>  <strong>10) 토큰 확인</strong></p>
<p><code>kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk &#39;{print $1}&#39;)</code>
  <br>
  <strong>11) 출력된 토큰을 입력하여 대시보드 로그인 ( 아래는 로그인 성공 화면 )</strong>
  <img src="https://velog.velcdn.com/images/passion_hd/post/e7788dd2-04c9-4ac9-a5e5-e99a670862d3/image.png" alt=""></p>
<p>  <strong>12) 모든 컨테이너들이 정상 작동 중인지 확인 : <code>kubectl get pods --all-namespaces</code></strong></p>
<p>  <strong>13) 가상머신 컴퓨터를 완전히 종료하고 2차 스냅샷을 찍어둔다. （ 컴퓨터 ３대 모두 ）
  &nbsp;&nbsp;　( 나중에 문제발생 시 현 상태로 돌아가기 위해 )</strong></p>
<p>  <strong>14) 대시보드 토큰 타임아웃 설정 끄기 ( 선택 사항 )</strong>
  &nbsp;　➡  <code>kubectl edit deployment kubernetes-dashboard -n kubernetes-dashboard</code>
  &nbsp;　➡ 42번째 줄 <code>- --token-ttl=0</code> 추가 후 저장
<img src="https://velog.velcdn.com/images/passion_hd/post/a57b18ab-e233-4d56-821d-faf6f925b69a/image.png" alt=""></p>
<hr>
<ul>
<li><strong>여기까지 하면 쿠버네티스 설치가 완료 된다.</strong></li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker 2일차 실습 정리]]></title>
            <link>https://velog.io/@passion_hd/Docker-2%EC%9D%BC%EC%B0%A8-%EC%8B%A4%EC%8A%B5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@passion_hd/Docker-2%EC%9D%BC%EC%B0%A8-%EC%8B%A4%EC%8A%B5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 09 Feb 2024 12:17:53 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>** 도커 2일 차 실습 정리** 🧐</p>
</blockquote>
<ul>
<li><p><strong>DB에 도커 볼륨 적용하기</strong></p>
<p><strong>1) 도커 데스크탑의 &quot; Volume &quot; 탭에서 볼륨 생성을 클릭한뒤 볼륨 명을 적어준다.<img src="https://velog.velcdn.com/images/passion_hd/post/44869e24-6f92-45f5-bc4f-56c89402294a/image.png" alt=""></strong></p>
<p><strong>2) 사전에 Pull 해놨던 mysql 이미지를 Run 하는데, 이때 생성한 볼륨을 추가해준다.</strong>
➡ 이때 경로는 <code>/var/lib/mysql</code> 로 해준다. 이 경로가 실제 mysql 에서 데이터를 
 &nbsp;　저장해놓는 경로이기 때문이다.
 <img src="https://velog.velcdn.com/images/passion_hd/post/b39ad016-676f-47b7-919f-c57c260ca9d1/image.png" alt=""></p>
<p> <strong>3) 실행된 DB 컨테이너를 MySQL Workbench로 접속한 뒤 <code>test</code> 이름의 
 &nbsp;　데이터베이스를 생성해본다.</strong><img src="https://velog.velcdn.com/images/passion_hd/post/90bdae23-8a51-44fc-a20e-6df5823dd776/image.png" alt=""></p>
<p> <strong>4) 그런 다음, 실행중인 컨테이너를 삭제한뒤, 다시 동일한 방식으로 새로운 컨테이너를 
 &nbsp;　실행시켜본다.</strong></p>
<p>&nbsp;　<strong>이때, 볼륨 설정을 따로 해주지 않았다면, DB에 접속해도 아무것도 생성된 것이 없을 
&nbsp;　것이다. 하지만, 우리는 볼륨 설정을 해줬기 때문에, 이전 컨테이너에서 생성한 <code>test</code> 
&nbsp;　데이터베이스가 접속과 동시에 생성되어 있는 것을 확인할 수 있었다.</strong>
<img src="https://github.com/hyungdoyou/LONUA_Project/assets/148875644/d930aea7-8009-447a-a0c6-818ba78654c8" alt="녹화_2024_02_09_19_56_49_758"></p>
</li>
</ul>
<hr>
<blockquote>
<p>** Docker Compose 작성하기** 💻</p>
</blockquote>
<ul>
<li><p>지난 글에서, 프로트엔드 서버와 백엔드 서버, DB 서버를 각각의 컨테이너로 만들어서 연동시켜봤는데, 이번에는 <strong>Docker Compose 파일을 작성</strong>하여 하나의 컨테이너에서 3개가 동시에 실행되도록 작성해본다.</p>
</li>
<li><p><strong>프론트엔드 설정</strong> ✅</p>
<p>1) 프론트엔드 프로젝트를 VS Code로 연 뒤 <code>package.json</code> 파일로 들어간뒤,
 &nbsp;　7번째 줄의 build 부분에 빌드 시 이미지( 이미지 이름 : frontend / 버전 : 1.0 ) 를 
 &nbsp;　생성토록 아래와 같이 작성해준다.</p>
<pre><code class="language-html">&quot;scripts&quot;: {
  &quot;serve&quot;: &quot;vue-cli-service serve&quot;,
  &quot;build&quot;: &quot;vue-cli-service build &amp;&amp; docker build --tag frontend:1.0 .&quot;,
  &quot;lint&quot;: &quot;vue-cli-service lint&quot;
},</code></pre>
<p>2) <strong>nginx 이름의 폴더를 생성</strong>하여, <strong>default.conf</strong> 파일에 nginx 설정파일을 적어준다.</p>
<pre><code class="language-html">server {
  listen       80;
  server_name  localhost;
  #access_log  /var/log/nginx/host.access.log  main;
  location /api {
      rewrite ^/api(.*)$ $1 break;
      // 이 backend는 나중에 실행될 backend 도커 컨테이너의 IP를 말한다.
      proxy_pass http://backend:8080;     
      proxy_set_header Host $http_host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Real-IP $remote_addr;
  }
  location / {
      alias   /usr/share/nginx/html/;
      try_files $uri $uri/ /index.html;
  }
  #error_page  404              /404.html;
  # redirect server error pages to the static page /50x.html
  #
  error_page   500 502 503 504  /50x.html;
  location = /50x.html {
      root   /usr/share/nginx/html;
  }
  # proxy the PHP scripts to Apache listening on 127.0.0.1:80
  #
  #location ~ \.php$ {
  #    proxy_pass   http://127.0.0.1;
  #}
  # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
  #
  #location ~ \.php$ {
  #    root           html;
  #    fastcgi_pass   127.0.0.1:9000;
  #    fastcgi_index  index.php;
  #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
  #    include        fastcgi_params;
  #}
  # deny access to .htaccess files, if Apache&#39;s document root
  # concurs with nginx&#39;s one
  #
  #location ~ /\.ht {
  #    deny  all;
  #}
}</code></pre>
<p>2) 빌드 시 nginx 실행을 위해, <strong>Dockerfile을 생성</strong>하여 아래와 같이 작성해준다. 여기서 
&nbsp;　적어준 dist 폴더 안 경로들은 뷰 프로젝트를 배포하기 위해 빌드 시 생성되는 dist 
&nbsp;　폴더안의 모든 것들이다.</p>
<pre><code class="language-html">FROM nginx:latest   // 사전에 pull 해논 nginx 이미지
 ADD ./dist/css /usr/share/nginx/html/css
 ADD ./dist/fonts /usr/share/nginx/html/fonts
 ADD ./dist/img /usr/share/nginx/html/img
 ADD ./js /usr/share/nginx/html/js
 ADD ./dist/styles.css /usr/share/nginx/html/styles.css
 ADD ./dist/logo.png /usr/share/nginx/html/logo.png
 RUN rm -rf /usr/share/nginx/html/index.html
 ADD ./dist/index.html /usr/share/nginx/html/index.html
 RUN rm -rf /etc/nginx/conf.d/default.conf
 ADD ./nginx/default.conf /etc/nginx/conf.d/default.conf
 CMD [&quot;nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]</code></pre>
<p><strong>3) 다음으로 도커허브에서 프론트엔드 서버용 레포지토리를 생성한다.</strong>
&nbsp;　➡ 나는 <code>[레포지토리명]/fe</code> 로 생성하였다.
<img src="https://velog.velcdn.com/images/passion_hd/post/8a2fe97f-1421-4410-bc2c-40195626febd/image.png" alt=""></p>
<hr>
<p><strong>4) VS Code 에서 터미널 창을 열고 프로젝트를 build 하여 이미지로 만든 후 생성한 
&nbsp;　레포지토리로 Push 해준다.</strong>
&nbsp;　➡ build : <code>docker build --tag [레포지토리명]/frontend:1.0 .</code>
&nbsp;　➡ push 방법 1 : <code>docker push [레포지토리명]/frontend:1.0</code>
&nbsp;　➡ push 방법 2 : <code>docker desktop의 이미지 탭에서 생성된 이미지 클릭 후 push</code></p>
<p>주의할점은 레포지토리에 같은 이름의 레포지토리가 생성되어 있지 않으면 
<code>Push to Hub</code>가 활성화 되어 있지 않는다.</p>
</li>
</ul>
<p>  <img src="https://velog.velcdn.com/images/passion_hd/post/16398a0e-1910-4867-b4b4-d970400f9e8d/image.png" alt=""></p>
<p> 참고용으로, 이미지 생성 시에 아래와 같은 에러가 발생했는데, 홈페이지 로고 파일 이름을 <code>그림01.png</code>로 했었는데, 한글이 들어가다 보니깐 에러가 났던 것 같다. 파일명을 <code>logo.png</code> 로 하니깐 이미지가 잘 생성되었다. </p>
<p>  하지만, 수업 시에도 동일하게 했었는데 그때 당시에는 에러 없이 잘 생성이 되었어가지고, 이부분은 확인이 필요할것 같다.</p>
<blockquote>
<p>ERROR: failed to solve: Internal: rpc error: code = Internal desc = rpc error: code = Internal desc = header key &quot;followpaths&quot; contains value with non-printable ASCII characters</p>
</blockquote>
<p> <strong>5)  이 다음부터는 docker build --tag 명령어가 아닌 <code>npm run build</code> 만 하면 수정된 
 &nbsp;　부분만 반영되어 이미지가 생성된다.</strong></p>
<p> &nbsp;　<strong>이미지를 생성할때는 각각의 레이어들이 모여서 이미지가 생성되는데, 이것을 
 &nbsp;　<code>docker build --tag</code> 명령어로 이미지를 생성하면 수정된 부분이 아닌 모든 이미지 
 &nbsp;　레이어 위에 또 이미지 레이어를 쌓아가는 형식이 되어버린다. 내가 그렇게 해서 
 &nbsp;　이미지를 5번 만드니, 레이어가 77개까지 쌓여있었다.</strong></p>
<hr>
<ul>
<li><p><strong>백엔드 설정</strong> ✅</p>
<p>1) 백엔드 용 레포지토리를 생성해준다. : 나는 <code>[레포지토리명]/be</code> 로 생성</p>
<p><strong>2) <code>pom.xml</code> 파일에 도커 플러그인을 추가해준다.</strong></p>
<pre><code class="language-html">          &lt;plugin&gt;
              &lt;groupId&gt;io.fabric8&lt;/groupId&gt;
              &lt;artifactId&gt;docker-maven-plugin&lt;/artifactId&gt;
              &lt;version&gt;0.43.4&lt;/version&gt;
              &lt;configuration&gt;
                  &lt;images&gt;
                      &lt;image&gt;
                          &lt;name&gt;레포지토리명/be:1.0&lt;/name&gt;
                          &lt;build&gt;
                              &lt;dockerFileDir&gt;${basedir}&lt;/dockerFileDir&gt;
                          &lt;/build&gt;
                      &lt;/image&gt;
                  &lt;/images&gt;
              &lt;/configuration&gt;
              &lt;executions&gt;
                  &lt;execution&gt;
                      &lt;id&gt;docker-build&lt;/id&gt;
                      &lt;phase&gt;package&lt;/phase&gt;
                      &lt;goals&gt;
                          &lt;goal&gt;build&lt;/goal&gt;
                      &lt;/goals&gt;
                  &lt;/execution&gt;
              &lt;/executions&gt;
          &lt;/plugin&gt;</code></pre>
<hr>
<p>** 3) Dockerfile을 작성해준다.**</p>
<pre><code class="language-html">FROM openjdk:11-jdk-slim-stretch   // 자바 11버전을 사용하기 때문
  COPY ./target/lonua-0.0.1-SNAPSHOT.jar lonua-0.0.1-SNAPSHOT.jar  // 백엔드 서버 실행 jar 파일
  CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;/lonua-0.0.1-SNAPSHOT.jar&quot;]  // 실행 명령어</code></pre>
<hr>
<p><strong>4) <code>resources</code> 폴더 밑에 <code>data.sql</code> 파일을 생성 후 도커 컴포즈 실행 시 실행시킬 
 &nbsp;　sql 쿼리문을 적어준다.</strong> </p>
<p> <strong>도커 컴포즈 파일 실행만으로도 모든 테이블과 테이블 안 데이터들이 들어가 있어야 하기때문에, 테이블 삭제, 생성 및 데이터 Insert 문을 테이블 간 연관관계를 고려하여 작성해주면 된다.</strong></p>
<p> <strong>여기서 우리가 보통 <code>application.yml</code> 파일에 <code>ddl-auto : create</code> 로 설정하면, 기존에 있던 테이블들을 지우고 새로 생성하도록 설정하는 것이었는데, 도커 컴포즈로 실행 시에는 해당 설정이 동작을 하지 않기 때문에, <code>ddl-auto : none</code> 으로 설정해주고, 직접 테이블 삭제 및 생성하는 쿼리문을 작성해주는 것이다.</strong></p>
<p> <strong>이것은, 데이터를 담아놨다면, MySQL Worknebch에서 Data Export를 한 뒤, 생성된 sql 파일을 열어보면 다 적혀져있다.</strong>
 <img src="https://velog.velcdn.com/images/passion_hd/post/eb5df63b-2a72-490c-9291-9f6f907828e7/image.png" alt=""></p>
<hr>
<pre><code class="language-sql"> DROP TABLE IF EXISTS `Cart`;

CREATE TABLE `Cart` (
`cartIdx` int NOT NULL AUTO_INCREMENT,
`createdAt` varchar(255) NOT NULL,
`status` bit(1) NOT NULL,
`updatedAt` varchar(255) NOT NULL,
`Product_idx` int DEFAULT NULL,
`User_idx` int DEFAULT NULL,
PRIMARY KEY (`cartIdx`),
KEY `FKpogyyegw24ppobcvs41xrk714` (`Product_idx`),
KEY `FKgx3aftoes5r8y032qkhvd44v4` (`User_idx`),
CONSTRAINT `FKgx3aftoes5r8y032qkhvd44v4` FOREIGN KEY (`User_idx`) REFERENCES `User` (`userIdx`),
CONSTRAINT `FKpogyyegw24ppobcvs41xrk714` FOREIGN KEY (`Product_idx`) REFERENCES `Product` (`productIdx`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;</code></pre>
<hr>
<p> <strong>5) <code>application.yml</code> 파일에서 작성한 data.sql 파일이 실행되도록 설정을 추가해준다.</strong></p>
<pre><code class="language-yml"> spring:
     sql:
      init:
        mode: always</code></pre>
<hr>
<p><strong>6) Maven의 Lifecycle 에서 <code>package</code>를 클릭하여, 이미지를 생성해준다.</strong></p>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/e714f008-3b68-4cf4-8bcd-200fe88978ca/image.png" alt=""></p>
</li>
</ul>
<hr>
<p><strong>7) docker desktop 에서 생성된 이미지를 레포지토리에 푸쉬해준다. ( 방법은 이전과 동일)</strong></p>
<hr>
<p>  <strong>8) <code>docker-compose.yml</code> 파일 생성 후 아래와 같이 작성한다.</strong></p>
<pre><code class="language-yml">  version: &#39;3&#39;
services:
  frontend:
    ports:   // 프론트서버 포트번호 설정
      - 8888:80
    image: [레포지토리명]/fe:1.0
    depends_on:  // 백엔드가 실행되고 나서 실행토록 설정
      - backend

  backend:
    image: [레포지토리명]/be:1.0
    depends_on:  // 백엔드 서버는 db 서버가 실행되고 나서 실행토록 설정
      db:
        condition: service_healthy
    ports:
      - 8080:8080
    environment:   // 환경 변수들 작성
      APP_PASSWORD: dddddd
  db:
    image: mysql:latest
    ports:
      - 3306:3306
      - 33060:33060
    volumes:
      - db-vol:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: qwer1234
      MYSQL_DATABASE: test  // DB 생성 시 test 데이터베이스를 생성토록 설정
    healthcheck:
      test: [ &quot;CMD&quot;, &quot;mysqladmin&quot;, &quot;ping&quot;, &quot;-h&quot;, &quot;localhost&quot;,&quot;-pqwer1234&quot; ]
      interval: 10s
      timeout: 5s
      retries: 3
volumes:
  db-vol:
    driver: local</code></pre>
<hr>
<p>  ** 9) 도커 컴포즈 파일을 실행시키면, 프론트 서버, 백엔드 서버, DB 서버 총 3개의 컨테이너가 
  &nbsp;　실행될 것이다.**</p>
<p>  &nbsp;　<strong>프론트 서버 ( <code>localhost:8888</code> ) 로 접속해보면 정상적으로 접속이 될 것이고, 백엔드 
  &nbsp;　서버와의 통신 및 DB에 넣어둔 데이터들도 정상적으로 보일 것이다.</strong></p>
<p>  &nbsp;　<strong>이렇게 되면, 이 컴포즈 파일 하나만 있으면, 어떤 환경에서도 내가 설정한 서버환경을 
  &nbsp;　구동하는것이 가능해진다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker 1일차 실습 정리]]></title>
            <link>https://velog.io/@passion_hd/Docker-1%EC%9D%BC%EC%B0%A8-%EC%8B%A4%EC%8A%B5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@passion_hd/Docker-1%EC%9D%BC%EC%B0%A8-%EC%8B%A4%EC%8A%B5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 07 Feb 2024 14:09:19 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>** 자주쓰는 명령어 정리** ✍</p>
</blockquote>
<ul>
<li><p>이미지 받아오기 : <code>docker pull [이미지 이름]</code></p>
</li>
<li><p>받아온 이미지를 가지고 컨테이너 실행 + bash 창 연결 
: <code>docker run -it mysql bash</code></p>
<p>➡ &quot; -it 옵션 &quot; : 쉘을 실행 시킬때 사용하는 옵션
➡ &quot; -d 옵션 &quot; : 백그라운드에서 실행하는 옵션</p>
</li>
<li><p>실행중인 컨테니어 확인 : <code>docker ps</code></p>
</li>
<li><p>컨테이너 삭제 : <code>docker rm [ 컨테이너 이름 ]</code></p>
</li>
<li><p>컨테이저 전체 삭제 : <code>docker rm -f $(docker ps -a -q)</code></p>
</li>
<li><p>컨테이너 실행 : <code>docker run --name webserver -d nginx</code>
➡ &quot; --name &quot; : 컨테이너 이름
➡ &quot; -p &quot; : 포트포워딩 설정
  <code>ex) docer run --name webserver -p 1111:80 -d nginx</code></p>
</li>
<li><p>bash 창에서 나오기
➡ <code>exit</code> : 컨테이너를 아예 끄면서 나옴
➡ <code>Ctrl + P + Q</code> : 컨테이너를 끄지 않은채 나옴</p>
</li>
<li><p>bash 다시 실행 시키기 : <code>docker exec -it [컨테이너명] bash</code></p>
</li>
<li><p>Dockerfile 명령어</p>
<pre><code class="language-java">  FROM  alpine:latest                # FROM 베이스 이미지 지정
  RUN   apk update &amp;&amp; apk add figlet        # RUN 컨테이너에서 실행할 명령어 지정
  ADD   ./message /message            # ADD 컨테이너에 추가할 파일, 현재 디렉토리의 message 파일을 컨테이너의 / 디렉토리에 배치
  CMD   cat /message | figlet            # CMD 컨테이너가 실행 된 후 실행할 명령어 지정

</code></pre>
</li>
</ul>
<pre><code>&lt; 그 밖에 많이 쓰는 커맨드 &gt;

COPY [원본] [사본]            # 컨테이너 내 파일을 컨테이너의 다른 곳에 복사
ENV [변수]=[값]                # 환경 변수 설정
EXPOSE [포트]                # 공개 포트 설정
WORKDIR [경로]                # 컨테이너 내에서 작업 디렉토리 지정, cd 같은 것
MAINTAINER [이름]            # 이미지에 대한 작성자 추가</code></pre><pre><code>---
- 이미지 빌드 : `docker build --tag hello:1.0 [도커파일 경로]`

- 실행 및 확인 : `docker images` / `docker run hello:1.0`

---
&gt; ** 도커 1일 차 실습 정리** 🧐

- 오늘부터 도커 수업을 3일 간 진행한다. 오늘 배운 실습 내용만 정리해 놓으려고 한다.

- 오늘 한 실습은 Docker Desktop에 프론트 엔드 서버, DB 서버, 백엔드 서버 3개의 도커 컨테이너를 생성하여 서로 간의 통신이 정상적으로 이뤄지도록 설정하는 것이었다.

&lt;br&gt;

✅ **DB 서버**

**1) mysql 을 도커 허브에서 Pull 하여 받는다. **
![](https://velog.velcdn.com/images/passion_hd/post/8ba2f673-0e92-4171-9cb2-39c070188646/image.png)

---
**2) 불러오기가 끝났다면, Images 탭을 누른 뒤 받아온 mysql 이미지를 실행시켜준다.**

![](https://velog.velcdn.com/images/passion_hd/post/a934dec4-3795-4fb2-9054-3771f954d9cc/image.png)

---

**3) 컨테이너 이름은 원하는대로 적어주고, 3306과 33060 포트에 대해 
 &amp;nbsp;　포트포워딩을 설정해준다. 그리고 root로 접속할 시 비밀번호를 설정해주면 
 &amp;nbsp;　된다. 이때 `MYSQL_ROOT_PASSWORD`는 반드시 이렇게 적어줘야 되는 것이다.**

  ![](https://velog.velcdn.com/images/passion_hd/post/2d414306-6928-4e63-84ac-0e163d8ff94f/image.png)

---
**4) 정상적으로 생성이 됬다면, Containers 탭을 눌러보면 설정해준 DB가 
 &amp;nbsp;　&quot;Running&quot; 이라고 나와있을 것이고, MySQL Workbench로 접속을 시도해보면 
  &amp;nbsp;　정상적으로 접속이 될 것이다.**

![](https://velog.velcdn.com/images/passion_hd/post/f585093e-c366-4e93-b9be-8634f655aa8a/image.png)
  &amp;nbsp;　**Workbench로 접속할때 IP 주소는 내 현재 윈도우 컴퓨터의 `사설 IP 주소`를 
  &amp;nbsp;　입력해주면 되고 hostname은` root`, 패스워드는 도커 이미지 실행 시 
  &amp;nbsp;　설정해준 값으로 해주면 된다.**
![](https://velog.velcdn.com/images/passion_hd/post/94d5b032-6436-4fe5-88c9-a66011e963f1/image.png)

---
** 5) 백엔드 서버와 연동할 데이터베이스를 생성해준다: `CREATE DATABASE [ DB명 ]`**

---
✅ **백엔드 서버**

**1) 백엔드 서버를 배포하기 위한 단계로는 `openjdk` 설치, 환경변수 설정, `jar` 
 &amp;nbsp;　파일 실행 순이었다.**

**2) 먼저, 설치하고자 하는 openjdk를 도커 허브에서 찾는다. 나는 11버전을
 &amp;nbsp;　사용하고 있어, 해당 버전으로 찾았다.**

**3) 인텔리제이에서 도커 플러그인을 설치 해준다.**
![](https://velog.velcdn.com/images/passion_hd/post/7696956d-66e8-407a-97da-78ac76887e39/image.png)

---
**4) Dockerfile 이름으로 새로운 파일을 생성한 뒤 도커 이미지 생성을 위한 
 &amp;nbsp;　내용들을 적어준다.**

```java
// 도커 허브에서 찾은 openjdk 11 버전
FROM openjdk:11-jdk-slim-stretch
// jar 파일을 추가하기 위해 기존 jar 파일을 도커 컨테이너의 새로운 경로에 추가
ADD target/lonua-0.0.1-SNAPSHOT.jar /lonua-0.0.1-SNAPSHOT.jar
// jar 파일 실행 명령어
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;/lonua-0.0.1-SNAPSHOT.jar&quot;]
// 8080 포트 포워딩 설정
EXPOSE 8080</code></pre><p>➡  이때, 중요한 것은 포트 포워딩 설정이다. 도커도 하나의 가상 컴퓨터이기 
 &nbsp;　때문에, 도커에서 실행되는 백엔드 서버로 요청을 보내기 위해서는 8080 포트를 
 &nbsp;　찾아올 수 있게끔 설정을 해줘야 된다.</p>
<p>➡  다 적어줬다면, <code>Build image on Dockerfile</code> 를 클릭하여 이미지를 생성해준다.<img src="https://velog.velcdn.com/images/passion_hd/post/36b32466-df99-4bab-b54e-965fc9acd6a1/image.png" alt=""></p>
<hr>
<p><strong>5) 이제 DB 서버 생성 시와 마찬가지로 Images에서 생성한 이미지를 실행시켜주면 
 &nbsp;　되는데, 이때 8080 포트에 대해 포트포워딩 설정을 해주고, <code>application.yml</code> 
 &nbsp;　파일에 있던 모든 환경변수를 입력해줘야 된다.</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/e5fd8a67-8f62-44da-910b-68c777422202/image.png" alt="">  </p>
<p><strong>6) 설정이 정상적으로 됬다면, 스프링 부트가 실행되고, DB로 접속해보면 생성한
 &nbsp;　데이터베이스에 테이블들이 생성된 것을 확인할 수 있을 것이다.</strong></p>
<hr>
<p>✅ <strong>프론트 엔드 서버</strong>
 &nbsp;&nbsp;　➡ <strong>프론트 엔드 서버는 도커의 볼륨을 이용하여 손쉽게 배포가 가능했다.</strong></p>
<p><strong>1)  배포하기에 앞서, 나는 프론트 엔드 서버를 Vue로 만들었고, 백엔드로 요청을 
 &nbsp;　보내는 URL을 설정했었는데, 보통 테스트 환경에서는 <code>localhost:8080</code> / 
 &nbsp;　서버를 배포했을때는 <code>도메인 주소/api</code> 로 설정했었다.</strong></p>
<p>&nbsp;　➡  <strong>그렇다면, 도커의 컨테이너로 실행중인 백엔드 서버로 요청을 보내려면 
&nbsp;&nbsp;&nbsp;　　어떻게 해야될까?</strong> </p>
<p>&nbsp;&nbsp;　　*<em>첫번째는, 현재 윈도우 컴퓨터의 8080 포트, 즉 localhost:8080 로 보내면 된다. 
 &nbsp;&nbsp;　　그러면 설정해둔 8080 포트로 도커에서 실행중인 백엔드 서버를 찾아갈 수 
 &nbsp;&nbsp;　　있게 될 것이다. *</em></p>
<p> &nbsp;&nbsp;　　<strong>이 방법은 사실 백엔드로 직접 요청을 보내는 것으로 nginx의 설정에서 프록시 설정을 
 &nbsp;&nbsp;　　해줄 필요가 없다. 따라서, nginx의 프록시 설정을 하기 위해서는 nginx의 주소로 
 &nbsp;&nbsp;　　요청을 보내야 한다. 예를들어, nginx의 포트번호가 <code>9999</code>로 설정하였다면 
 &nbsp;&nbsp;　　<code>localhost:9999/api</code> 로 요청을 보내면, 프록시 설정으로 인해 백엔드 주소로 변경되어 
 &nbsp;&nbsp;　　요청이 들어가게 되는 것이다.</strong></p>
<p> &nbsp;&nbsp;　　<strong>아래는 프록시 설정 버전의 nginx 설정 파일이다.</strong></p>
<p><strong>2) 2번째로 nginx의 설정 파일을 수정하여 옮겨야 하는데, 수정하는 방식은 기존의 
&nbsp;&nbsp;　도커 nginx 설정파일의 경로로 이동하여 설정 파일 내용을 복사한 뒤, 내가
 &nbsp;　원하는 아무 위치에서 해당 파일의 내용과 같지만 일부 다른 내용으로 파일을 
 &nbsp;　하나 생성하면 된다.</strong></p>
<pre><code class="language-html">server {
    listen       80;
    server_name  localhost;
    #access_log  /var/log/nginx/host.access.log  main;

—--------------------------✅ 변경 전—-------------------------------
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
—--------------------------여기 까지—--------------------------------

—--------------------------✅ 변경 후—-------------------------------
    location / {
        alias   /usr/share/nginx/html/;
        try_files $uri $uri/ /index.html;
    }

    location /api {
        rewrite ^/api(.*)$ $1? break;
        proxy_pass http://172.18.0.3:8080; // 도커 백엔드 컨테이너 IP 주소
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
    }
    }
—--------------------------여기 까지—--------------------------------    

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache&#39;s document root
    # concurs with nginx&#39;s one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}
</code></pre>
<hr>
<p><strong>3) nginx 를 DB 컨테이너 생성 시 와 마찬가지로 도커 허브에서 Pull 해온다.</strong></p>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/4effa3ec-908d-40e6-b5ee-6dffff8e85a6/image.png" alt=""></p>
<p><strong>4) 그 다음, Run 으로 실행 시켜주는데, 이때 Vue의 dist 폴더 안 파일들은 
 &nbsp;　<code>/usr/share/nginx/html</code>로, 생성해준 default.cof 파일은 <code>/etc/nginx/conf.d</code> 
 &nbsp;　폴더로 볼륨 설정을 해주면 끝이다.</strong></p>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/46ca706a-7416-48db-b2a8-cdf90c228cc8/image.png" alt=""></p>
<hr>
<p><strong>그러면 이제, 프론트 엔드 서버를 실행하여, 해당 페이지에서 요청을 보내면 정상적으로 동작되는 것을 확인 할 수 있을 것이다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS RDS에 Data Import 하는 법]]></title>
            <link>https://velog.io/@passion_hd/AWS-RDS%EC%97%90-Data-Import-%ED%95%98%EB%8A%94-%EB%B2%95</link>
            <guid>https://velog.io/@passion_hd/AWS-RDS%EC%97%90-Data-Import-%ED%95%98%EB%8A%94-%EB%B2%95</guid>
            <pubDate>Wed, 07 Feb 2024 12:51:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>** AWS RDS에 Data Import 하기**</p>
</blockquote>
<ul>
<li><p>도커 수업 중, 실수로 기존 진행하던 프로젝트의 RDS로 CREATE 를 하는 상황이 나왔다. 다행히 SQL 파일들을 백업해놔서 Data를 Import 만 하면 되는 문제라서 별 대수롭지 않게 생각했다.</p>
</li>
<li><p>그러다가, Import를 하는 순간 바로 등장하는 에러들... 바로 권한이 없다는 에러였다.</p>
</li>
</ul>
<p><strong>에러 내용</strong></p>
<ul>
<li><code>User ERROR 1227 (42000) at line 18: Access denied; you need (at least one of) the SUPER, 
SYSTEM_VARIABLES_ADMIN or SESSION_VARIABLES_ADMIN privilege(s) for this operation</code></li>
</ul>
<hr>
<ul>
<li><p>인터넷에서 찾아보니 파라미터 그룹을 변경하는 방법이 있었다. default로 설정된 파라미터가 아닌, 새로운 파라미터 그룹을 생성하여, <code>log_bin_trust_function_creators</code> 값을 0에서 1로 설정하고, 데이터베이스의 파라미터를 생성한 파라미터로 바꾸면 SUPER 권한을 획득할 수 있다고 나와있는 글이 많았다.</p>
</li>
<li><p>하지만, 그렇게 해서 Data를 Import 하는 순간, 권한 에러는 동일하게 등장하였다. 그래서 더 검색을 하다가 찾은 방법이 있어서 정리해놓는다.</p>
</li>
<li><p>밑에는 sql 파일 예시이다. 여기서 내가 표시한 부분을 주석처리해준뒤 다시 시도하면 정상적으로 Import가 될 것이다.</p>
</li>
</ul>
<hr>
<ul>
<li>방법은 백업한 sql 파일을 열어서 특정 <strong>4개 줄을 주석 처리</strong>해주면 되는 거였다.
```sql</li>
<li><ul>
<li>MySQL dump 10.13  Distrib 8.0.34, for Win64 (x86_64)</li>
</ul>
</li>
<li>-</li>
<li><ul>
<li>Host:     Database: </li>
</ul>
</li>
</ul>
<hr>
<p>-- Server version    8.0.33</p>
<p>/<em>!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/</em>!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS <em>/;
/</em>!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION <em>/;
/</em>!50503 SET NAMES utf8 <em>/;
/</em>!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE <em>/;
/</em>!40103 SET TIME_ZONE=&#39;+00:00&#39; <em>/;
/</em>!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 <em>/;
/</em>!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 <em>/;
/</em>!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=&#39;NO_AUTO_VALUE_ON_ZERO&#39; <em>/;
/</em>!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
-- SET @MYSQLDUMP_TEMP_LOG_BIN = @@SESSION.SQL_LOG_BIN;    🔥주석처리🔥
-- SET @@SESSION.SQL_LOG_BIN= 0;                           🔥주석처리🔥</p>
<p>--
-- GTID state at the beginning of the backup 
--</p>
<p>-- SET @@GLOBAL.GTID_PURGED=/<em>!80000 &#39;+&#39;</em>/ &#39;&#39;;             🔥주석처리🔥</p>
<p>--
-- Table structure for table <code>Branch</code>
--</p>
<p>DROP TABLE IF EXISTS <code>Branch</code>;
/<em>!40101 SET @saved_cs_client     = @@character_set_client */;
/</em>!50503 SET character_set_client = utf8mb4 <em>/;
CREATE TABLE <code>Branch</code> (
  <code>branchIdx</code> int NOT NULL AUTO_INCREMENT,
  <code>branchAddress</code> varchar(100) DEFAULT NULL,
  <code>branchName</code> varchar(30) DEFAULT NULL,
  <code>Brand_idx</code> int DEFAULT NULL,
  PRIMARY KEY (<code>branchIdx</code>),
  UNIQUE KEY <code>UK_h45erxkc1j4l8opvjqqiy8gg4</code> (<code>branchAddress</code>),
  UNIQUE KEY <code>UK_cgvrh0291250ua04j1ajx1rgw</code> (<code>branchName</code>),
  KEY <code>FKtnn3wlycc2cy5j4rxa7gv74bi</code> (<code>Brand_idx</code>),
  CONSTRAINT <code>FKtnn3wlycc2cy5j4rxa7gv74bi</code> FOREIGN KEY (<code>Brand_idx</code>) REFERENCES <code>Brand</code> (<code>brandIdx</code>)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/</em>!40101 SET character_set_client = @saved_cs_client */;</p>
<p>--
-- Dumping data for table <code>Branch</code>
--</p>
<p>LOCK TABLES <code>Branch</code> WRITE;
/<em>!40000 ALTER TABLE <code>Branch</code> DISABLE KEYS */;
/</em>!40000 ALTER TABLE <code>Branch</code> ENABLE KEYS <em>/;
UNLOCK TABLES;
-- SET @@SESSION.SQL_LOG_BIN = @MYSQLDUMP_TEMP_LOG_BIN;  🔥주석처리🔥
/</em>!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;</p>
<p>/<em>!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/</em>!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS <em>/;
/</em>!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS <em>/;
/</em>!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT <em>/;
/</em>!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS <em>/;
/</em>!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION <em>/;
/</em>!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;</p>
<p>-- Dump completed on 2024-02-04 17:09:01</p>
<p>```</p>
<hr>
<ul>
<li><p>앞으로도 AWS의 RDS를 사용할 일이 많을텐데, 사람은 실수하기 마련이기 때문에, 백업도 하고 하지만 별도의 자동백업 설정을 해놓지 않았다면 나처럼 sql 파일들을 그때그때 보관하고 있을 것이다.</p>
</li>
<li><p>그럴때, 해결하기 위해 이 내용을 정리해 놓는다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Nginx Reverse Proxy 설정하는 법 정리]]></title>
            <link>https://velog.io/@passion_hd/Nginx-Reverse-Proxy-%EC%84%A4%EC%A0%95%ED%95%98%EB%8A%94-%EB%B2%95-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@passion_hd/Nginx-Reverse-Proxy-%EC%84%A4%EC%A0%95%ED%95%98%EB%8A%94-%EB%B2%95-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 27 Jan 2024 07:41:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>** Nginx Reverse Proxy 란 ❓**</p>
</blockquote>
<ul>
<li><p>먼저, 설정에 앞서 <strong>Nginx Reverse Proxy를 설정하는 이유</strong>에 대해 생각해보고자 한다.</p>
</li>
<li><p><strong>Proxy 란 ❓</strong>
다른 서버에서 리소스를 찾는 클라이언트의 요청에 대한 중개자 역할을 하는 서버이다. 프록시 서버는 클라이언트와 클라이언트가 찾고 있는 데이터를 호스팅하는 실제 서버 사이에 위치한다. </p>
<br>

</li>
</ul>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/11e8c800-caf1-4949-b0fd-02d3d04769aa/image.png" alt=""></p>
<ul>
<li><p><strong>Nginx Reverse Proxy 란 ❓</strong>
리버스 프록시란 클라이언트와 웹 서버 간의 중개자 역할을 하는 서버로, 클라이언트로부터의 요청을 대신 받아 웹 서버에 전달하고, 웹 서버의 응답을 클라이언트에게 전달하는 역할을 한다. 이를 통해 리버스 프록시는 웹 서버의 부하를 분산시키고, 보안을 강화하는 등 다양한 기능을 수행할 수 있다.</p>
<p>기본 작동 원리는 클라이언트가 Reverse Proxy에 요청을 보내면, Reverse Proxy는 요청을 웹 서버에 전달한다. 그다음, 웹 서버는 요청된 데이터를 처리한 후 응답을 보낸다. 그리고 Reverse Proxy는 웹 서버로부터 받은 응답을 클라이언트에게 전달하는 방식으로 동작한다.</p>
<br></li>
<li><p><strong>Nginx Reverse Proxy의 필요성</strong> 🧐</p>
<p>✅ <strong>서버 부하 분산 (Load balancing)</strong>
웹 서비스에 동시에 많은 사용자가 접속할 경우, 서버에 부하가 집중되어 성능 저하 및 서비스 중단이 발생할 수 있다. 리버스 프록시는 들어오는 요청을 여러 대의 서버로 분산시켜 각 서버의 부하를 줄이고, 서버의 가용성을 높여 안정적인 서비스 제공이 가능하도록 한다.</p>
<p>✅ <strong>보안 강화</strong>
리버스 프록시는 외부에서 직접 서버에 접근하지 못하도록 하여 웹 서비스의 보안을 강화한다. 클라이언트 요청은 먼저 리버스 프록시를 거쳐 서버로 전달되며, 이 과정에서 리버스 프록시는 악성 요청 필터링, 접근 제한 등의 역할을 수행하여 서버를 보호한다.</p>
<p>✅ <strong>캐싱 및 가속화</strong>
리버스 프록시는 자주 사용되는 정적 파일들(이미지, CSS, JavaScript 등)을 캐시에 저장하여 빠르게 제공할 수 있다. 이로 인해 서버의 부하를 줄이고 응답 시간을 단축시켜 웹 서비스의 성능을 향상시킬 수 있다.</p>
</li>
</ul>
<hr>
<ul>
<li><p><strong>리버스 프록시의 장점</strong> 🔥</p>
<p><strong>1) 로드 밸런싱</strong> : 여러 서버에 트래픽을 분산시켜 서버 부하를 줄이고 가용성을 높일 수 
&nbsp;　　&nbsp;　　&nbsp;&nbsp;&nbsp;　　있다.
<strong>2) 보안 강화</strong> : 리버스 프록시는 외부 요청을 필터링하여 보안을 강화한다.
<strong>3) 캐싱</strong> : 정적 콘텐츠를 캐시하여 응답 시간을 개선하고 서버 부하를 줄인다.
<strong>4) 웹 서버 최적화</strong> : 리버스 프록시를 사용하여 웹 서버의 설정 및 성능을 최적화할 수 
&nbsp;　　&nbsp;　　&nbsp;&nbsp;&nbsp;&nbsp;　　　있다.</p>
<br></li>
<li><p><strong>리버스 프록시의 단점</strong> 🔥</p>
<p><strong>1) 추가적인 서버 설정과 관리</strong> : 리버스 프록시를 사용하려면 추가적인 서버 설정과 
&nbsp;&nbsp;　　　　　　　　　　　　　　관리가 필요하다.
<strong>2) 네트워크 지연</strong> : 리버스 프록시를 통과하는 모든 요청에 대해 약간의 네트워크 지연이 
&nbsp;&nbsp;&nbsp;　　　　　　　　발생할 수 있다.
<strong>3) 복잡성 증가</strong> : 리버스 프록시가 있는 아키텍처는 일부 경우에 복잡성이 증가할 수 
&nbsp;&nbsp;&nbsp;　　　　　　　있다.</p>
</li>
</ul>
<hr>
<blockquote>
<p>** 사전 준비사항** 💻</p>
</blockquote>
<ul>
<li><p>실습은 수업때 진행되는 내용을 바탕으로 진행하다 보니, 아래와 같이 사전에 진행한 내용이 있었다.</p>
</li>
<li><p>AWS 1개의 <strong>EC2에 백엔드 서버를 배포</strong>하고, 해당 EC2 퍼블릭 IP 주소에 대한 <strong>도메인을 발급</strong> 받아 놓는다. ( 인증까지 완료하면 좋다. )
➡ <a href="https://velog.io/@passion_hd/Day50-%EC%8A%A4%ED%94%84%EB%A7%81-15">설정법 참고</a></p>
</li>
</ul>
<hr>
<blockquote>
<p>🦁** Nginx Reverse Proxy 설정하기**</p>
</blockquote>
<ul>
<li><p>먼저 프론트 엔드의 백엔드로 호출하는 JS 코드들을 모아놓은 JS 파일에 백엔드로 요청하는 URL을 아래와 같이 발급 받은 도메인 주소/api로 변경한다.</p>
<p>➡ <code>const backend = &#39;https://www.test.kro.kr/api&#39;;</code></p>
<p>➡ 그러면 웹페이지에서 요청이 나갈때 <code>https://www.test.kro.kr/api/user/signup</code> 과 
&nbsp;&nbsp;　같은 URL로 요청이 보내질 것이다.</p>
<ul>
<li><p>다음으로, 프론트 페이지들을 만든 것을 Nginx 에 적용시켜야 된다. 그러기 위해선 예전에 이용한 <strong>FileZilla</strong> 프로그램을 사용하였다.</p>
</li>
<li><p>사용하면서 문제가 됬던 것은 작성한 HTML, CSS, JS 파일들을 모두 nginx 의 <code>/var/www/html</code> 폴더로 옮겨야 되는데, <strong>FileZilla에서 자꾸 전송을 실패</strong>하는 것이다. 
알고보니 해당 폴더로 파일을 옮기려면 <strong>root 계정으로 우분투 서버를 연결해야 가능했다.</strong></p>
<br>
✅ ** root 계정으로 연결하는 방법**

</li>
</ul>
<p><strong>1) 일단 Putty를 이용하여 접속 후 root 권한으로 바꿔준다 : <code>sudo su - root</code></strong></p>
<p> <strong>2) SSH 설정 파일을 편집해 줘야 한다 : <code>vi /etc/ssh/sshd_config</code></strong></p>
<p> <strong>3) 설정 파일의 33번째 줄 밑에 아래와 같이 <code>PermitRootLogin yes</code> 를 추가해준다.</strong>
 <img src="https://velog.velcdn.com/images/passion_hd/post/f822c289-6fe5-476c-8a28-d3943b24e252/image.png" alt=""></p>
<p> <strong>4) 그런다음 암호키를 복사한다
 &nbsp;　➡ <code>sudo cp /home/ubuntu/.ssh/authorized_keys   /root/.ssh</code></strong></p>
<p> <strong>5) 다음으로 SSH를 재시작한다 : <code>sudo service ssh restart</code></strong></p>
<p> <strong>6) FileZilla에서 root 로 연결한다.</strong></p>
<pre><code>  &amp;nbsp;　➡ **연결은 기존 방식과 동일하나 기존에는 사용자를 `ubuntu`로 했다면, 이번엔 
          &amp;nbsp;&amp;nbsp;&amp;nbsp;　　`root`로 적으면 된다.**</code></pre><p><img src="https://velog.velcdn.com/images/passion_hd/post/89d8a8a5-d01a-4f75-8d2e-9203591a0818/image.png" alt=""></p>
<p> <strong>7) 연결이 성공적으로 됬다면, 리모트 사이트에서 <code>/var/www/html</code> 을 검색하여 해당 
 &nbsp;　디렉토리에 작성한 HTML, CSS, JS 파일들을 드래그하여 옮겨주면 된다.</strong>
 <img src="https://velog.velcdn.com/images/passion_hd/post/df1c9792-1ff8-4bbc-a2bd-1711244e3019/image.png" alt=""></p>
</li>
</ul>
<hr>
<ul>
<li><p><strong>마지막으로 nginx 서버 설정에서 Reverse Proxy 설정 및 nginx 의 디폴트 페이지를 바꿔주면 된다.</strong></p>
</li>
<li><p><strong>서버 설정 파일 열기</strong> : <code>sudo vi /etc/nginx/sites-available/default</code></p>
</li>
<li><p><strong>디폴트 페이지 설정 ( 도메인주소로 접속했을때 가장 먼저 뜨는 페이지 지정 )</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/78d38b9c-ad01-4eff-8b16-8c9837879d0e/image.png" alt=""></p>
<p>➡ 44번째 줄의 <code>index.html</code> 글자 앞에 내가 넣어주고 싶은 페이지를 적으면 된다. 나는<br>&nbsp;&nbsp;　<code>index.html</code> 을 그냥 한번 더 적어봤다.</p>
</li>
<li><p>마지막으로, <strong>Reverse Proxy 설정</strong>은 위의 그림의 박스 친 것처럼 설정해주면 된다. 코드는 아래에 있다.</p>
</li>
<li><p><strong>그림에는 rewrite 줄에 <code>?$args</code> 가 포함되어 있지만 이것은 오류가 있어 빼야된다!! 아래 코드를 사용하자❗</strong></p>
<pre><code class="language-html">      location /api {
              rewrite ^/api(.*)$ $1 break;
              proxy_pass   http://localhost:8080;
              proxy_set_header Host $http_host;
              proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
              proxy_set_header X-Real-IP $remote_addr;
      }</code></pre>
<p>&nbsp;&nbsp;　　➡ 해당 설정이 의미하는 내용은 <code>/api</code> 를 달고 들어오는 요청을 받아서 <strong>proxy_pass 에 
&nbsp;&nbsp;　　　　지정한 url</strong> 로 /api를 빼고 보낸다는 의미이다.</p>
</li>
</ul>
<p>&nbsp;&nbsp;　　➡ 이와 같이 밑으로 여러개를 더 추가할 수 있다. <strong>예를 들어 /api 가 아닌 /test를 달고 
&nbsp;&nbsp;　　　　오는 url</strong>을 처리해주기 위해서는 <code>location /test</code> 로 설정을 추가해주면 된다.</p>
<hr>
<ul>
<li>이렇게 설정하면, 이제 클라이언트가 웹페이지에서 서버로 요청을 보내면 nginx에서 Reverse Proxy로 요청이 가고, url을 통해 해당하는 백엔드 서버를 찾아서 그 주소로 요청을 보내게 된다.</li>
</ul>
<ul>
<li><p>Reverse Proxy 설정 간에 프록시 설정이 잘 되지 않아서, 502 Bad GateWay 에러가 처음에는 계속 떴었다. 그 이유는 백엔드 서버의 주소를 <code>http://localhost:8080</code> 이 아닌 동일한 EC2에서 백엔드 서버를 운영하고 있으니, 해당 도메인 주소를 써보고, 퍼블릭 ip 도 써보고 했었는데, 알고보니 localhost를 그냥 쓰면 되는 거였다.</p>
</li>
<li><p>나는 <code>localhost:8080</code> 이 EC2 가 아닌 내 원래 컴퓨터의 로컬 IP 주소를 의미하는 것으로 착각해서 EC2의 백엔드 서버로 계속 요청을 보내야되는데로 고민을 길게 했던 것이다.</p>
</li>
<li><p>그때, EC2가 무엇일까란 생각이 들면서 결국 EC2가 가상의 컴퓨터를 만들어준다는것이 생각났다.</p>
</li>
<li><p>결국, localhost 라는 것이 어떤 시스템에서든 자기 자신을 나타내는 호스트 이름으로 웹서버를 실행하는 해당 서버 자체를 가리킨다는 것을 말하는데, EC2 인스턴스는 가상의 컴퓨터이고, 그곳에서 백엔드 서버를 실행중이라면, 이 서버에서 사용한 <strong>localhost가 EC2 인스턴스 IP 인 것이나 다름없는 것이다.</strong></p>
</li>
</ul>
<hr>
<blockquote>
<p><strong>EC2 접속 종료시에도 서버를 유지하는 방법</strong></p>
</blockquote>
<ul>
<li><p>실습을 마무리하고, 갑자기 든 생각으로 Putty 접속을 종료했을 때, 동작중인 백엔드 서버가 꺼지기 때문에, <strong>그럼 어떻게 컴퓨터를 종료하고도 항상 서버가 유지되도록 하지?</strong> 라는 의문이 들었다. 그것을 가능하게 해주는 것이 <code>nohup</code> 명령어라는 것을 알았다.</p>
</li>
<li><p><code>nohup</code> 명령어는 로그아웃 등과 같이 터미널과의 세션 연결이 끊기더라도, 프로세스가 계속 동작되도록 해준다.</p>
</li>
<li><p>일반적으로 터미널과의 세션 연결이 끊기게 되면, 리눅스에서는 해당 세션에서 실행된 프로세스들에게 HUP(Hang Up,끊다) 시그널을 전달하여 프로세스들이 종료되도록 한다. </p>
<p>이 때, nohup 명령은 <strong>“세션이 종료되더라도 계속 실행하게 하고 싶은 프로세스에는 HUP 시그널을 전달하지 않도록(No Hang Up,끊지마) 한다&quot;</strong>는 의미이다.</p>
</li>
<li><p>사용방법은 아주 간단합니다. 프로그램 실행 명령어 앞에 <code>nohup</code> 만 붙여주면 된다.</p>
<p>➡ 기존 실행 명령어 : <code>java -jar [jar 파일명]</code></p>
<p>➡ <strong>백그라운에서 실행 시</strong> : <code>nohup java -jar [jar 파일명] &amp;</code></p>
<br></li>
<li><p>실행하면 스프링 부트 서버가 백그라운드에서 동작하게 된다. 따라서 에러가 떠도 로그가 출력되지 않는데, 이것은 명령어를 실행한 경로에 <strong>&quot;nohup.out&quot;</strong> 이라는 log파일이 생성되기 때문에 해당 로그 파일을 조회하면 로그를 볼 수가 있다.</p>
<p>✅ <strong>로그 확인 명령어 정리</strong></p>
<p>1) 로그 조회 : <code>cat nohup.out</code>
　2) 실시간 로그 출력 : <code>tail -f nohup.out</code>
　3) 마지막 10줄 조회 : <code>tail -n 10 nohup.out</code>
　4) 파일의 10번째 줄 이후 부터 출력 : <code>tail -n +10 nohup.out</code> </p>
</li>
<li><p>그러나, 필요 이상의 로그를 화면에 계속해서 출력하게 되면 nohup.out 파일의 용량이 매우 커지기 때문에, 디스크 공간을 낭비하게 될 수도 있다고 한다. 따라서, 꼭 필요한 로그만 출력하거나 로그를 남기는 것이 불필요한 경우 nohup.out 파일을 생성하지 않도록 하는 것이 좋을 수도 있다.</p>
<p>➡ <strong>nohup.out 파일 생성하지 않고 백그라운드 실행 명령어</strong>
&nbsp;　<code>nohup [실행파일명] 1&gt;/dev/null 2&gt;&amp;1 &amp;</code></p>
<p>&nbsp;　// 0 : 표준 입력,&nbsp;&nbsp;&nbsp;&nbsp; 1 : 표준 출력,&nbsp;&nbsp;&nbsp;&nbsp; 2 : 표준 에러
&nbsp;　// 1&gt; /dev/null : 표준 출력(1)의 결과를 /dev/null(버림)으로 전달
&nbsp;　// 2&gt;&amp;1 : 표준 에러(2)를 표준 출력(1)이 전달되는 곳(/dev/null)으로 동일하게 전달</p>
<p>➡ <strong>표준 출력과, 표준 에러 로그를 따로 관리하고 싶을 때</strong>
&nbsp;　<code>nohup [실행파일] 1&gt;[파일1] 2&gt;[파일2] &amp;</code></p>
<br></li>
<li><p>마지막으로, 실행한 <strong>프로세스를 종료하기 위해서는 PID 값을 찾아서 종료</strong> 시켜줘야된다.
➡ PID 찾기 ( java -jar 로 된 것 ) : <code>ps -ef | grep [실행파일명]</code>
➡ 프로세스 종료 : <code>sudo kill -9 [찾은 PID 번호]</code></p>
</li>
</ul>
<hr>
<blockquote>
<p><strong>Vue 프로젝트 배포 시 nginx 설정 파일 (24. 2.10. 추가)</strong> ✍</p>
</blockquote>
<ul>
<li>Vue로 배포할때는 nginx 설정파일이 또 달라지는데 그 내용은 아래와 같다.</li>
</ul>
<pre><code class="language-java">
        location / {
                index index.html;
                try_files $uri $uri/ @rewrites;
        }

        location @rewrites {
                rewrite ^(.+)$ /index.html last;
        }

        location /api {
                rewrite ^/api(.*)$ $1 break;
                proxy_pass   http://localhost:8080;
                proxy_set_header Host $http_host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Real-IP $remote_addr;
        }
</code></pre>
<hr>
<ul>
<li>여기서 중요한게 <code>rewrite</code> 부분이라고 생각된다. <code>/api</code> 를 달고 오는 요청에 대한 
처리인데 이부분을 조금만 잘못 적어도 요청이 제대로 처리가 되지 않았다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day_57 ( HTML / CSS 문법 정리 )]]></title>
            <link>https://velog.io/@passion_hd/Day57-HTML-CSS-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@passion_hd/Day57-HTML-CSS-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 18 Jan 2024 11:49:10 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>🐻 ** VSCode 기본적으로 설치해주면 좋은 플러그인**</p>
</blockquote>
<ul>
<li><strong>Prettier Code Formatter</strong> : 단축키 Alt + Shift + F 로 자동 포맷 맞춤</li>
<li><strong>Auto Rename Tag</strong></li>
<li><strong>Auto Close Tag</strong></li>
<li><strong>Live Server</strong> : 코드를 바꾼 뒤 저장하면 자동으로 웹브라우저에서 새로고침되어 적용됨</li>
</ul>
<hr>
<blockquote>
<p>🐶 ** HTML 문법 정리하기**</p>
</blockquote>
<ul>
<li><p><strong>HTML의 기본 구조</strong></p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;meta name=&quot;내가 넣고 싶을때 넣는 정보&quot; content=&quot;넣고 싶은 내용&quot; /&gt;
  &lt;title&gt;탭에 출력될 정보&lt;/title&gt;

  &lt;!-- css 파일을 별도로 저장시켜놓고, 페이지 접속 시 css 파일의 경로를 불러와서 보여주는 식--&gt;
  &lt;link rel=&quot;stylesheet&quot; href=&quot;css파일의 경로&quot; /&gt;
&lt;/head&gt;
&lt;body&gt;
  화면에 보여질 내용을 작성
  띄워쓰기는 1칸만 가능
  개행은 그냥 하면 안된다

  &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;  &lt;!--&lt;div&gt;&lt;/div&gt;--&gt;
  &lt;script&gt;
    자바스크립트 코드 작성
  &lt;/script&gt;
  &lt;!--자바 스크립트 코드는 가장 마지막에 넣어주는 것이 가장 좋은 형태라고 알려져서 사용중이다--&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
</li>
</ul>
<pre><code>---
- **h 태그 : 제목 태그이며, 글자 크기에 따라 1부터 6까지 설정 가능**
```html
    &lt;h1&gt;제목 태그&lt;/h1&gt;
    &lt;h2&gt;제목 태그&lt;/h2&gt;
    &lt;h3&gt;제목 태그&lt;/h3&gt;
    &lt;h4&gt;제목 태그&lt;/h4&gt;
    &lt;h5&gt;제목 태그&lt;/h5&gt;
    &lt;h6&gt;제목 태그&lt;/h6&gt;</code></pre><hr>
<ul>
<li><p><strong>a 태그 : 클릭 시 해당 주소로 이동 ( 자주 사용됨 )</strong></p>
<pre><code class="language-html">  &lt;a href=&quot;https://www.google.co.kr/&quot;&gt;구글&lt;/a&gt;
  &lt;a href=&quot;https://www.naver.com/&quot;&gt;네이버&lt;/a&gt;

  &lt;a href=&quot;/03_h1%20태그.html?id=test&amp;pw=qwer&quot;&gt;h1 태그&lt;/a&gt;

  &lt;a href=&quot;/a/test.html&quot;&gt;절대 경로&lt;/a&gt;
  &lt;a href=&quot;./a/test.html&quot;&gt;상대 경로&lt;/a&gt;</code></pre>
</li>
</ul>
<hr>
<ul>
<li><p><strong>img 태그 : CSS로 처리하기 때문에 잘 쓰이진 않는다.</strong></p>
<pre><code class="language-html">  &lt;img src=&quot;https://s.pstatic.net/shopping.phinf/20240102_26/a6d473c3-bb1d-456a-8594-54958c012648.jpg?type=f294_378&quot;&gt;
  &lt;img src=&quot;./common.jpeg&quot; width=&quot;200&quot;&gt;
  &lt;img src=&quot;./common.jpeg&quot; style=&quot;width: 200px;&quot;&gt;

  &lt;!--그림이 출력되지 않을 때 설명--&gt;
  &lt;img src=&quot;./a/common.jpeg&quot; alt=&quot;그림 설명&quot; width=&quot;200&quot;&gt;

   &lt;!--이미지에서 사각형의 픽셀값을 지정해서 해당 부분 클릭하면 지정한 링크로 이동--&gt;
  &lt;picture&gt;
  &lt;img usemap=&quot;#testmap&quot; src=&quot;https://static.coupangcdn.com/aa/cmg_paperboy/image/1704872362232/P-Top%2C-R1_PC.jpg&quot;&gt;
  &lt;map name=&quot;testmap&quot;&gt;
      &lt;area shape=&quot;rect&quot; coords=&quot;40,45,270,180&quot; title=&quot;watch&quot; href=&quot;www.google.co.kr&quot;&gt;
      &lt;area shape=&quot;rect&quot; coords=&quot;290, 170, 333, 250&quot; title=&quot;phone&quot; href=&quot;www.naver.co.kr&quot;&gt;
  &lt;/map&gt;
  &lt;/picture&gt;

  &lt;!-- 반응형 화면 지정(모바일, 웹페이지 등에서 볼때 화면에 따라 달라짐) 대부분다 css로 처리함--&gt;
  &lt;picture&gt;
      &lt;source media=&quot;(min-width:650px)&quot; srcset=&quot;https://s.pstatic.net/shopping.phinf/20240102_26/a6d473c3-bb1d-456a-8594-54958c012648.jpg?type=f294_378&quot;&gt;
      &lt;source media=&quot;(min-width:450px)&quot; srcset=&quot;https://static.coupangcdn.com/aa/cmg_paperboy/image/1704872362232/P-Top%2C-R1_PC.jpg&quot;&gt;
      &lt;source media=&quot;(min-width:250px)&quot; srcset=&quot;/common.jpeg&quot;&gt;
      &lt;img src=&quot;/common.jpeg&quot;  width=&quot;width:auto;&quot;&gt;
  &lt;/picture&gt;</code></pre>
</li>
</ul>
<hr>
<ul>
<li><p><strong>테이블 태그</strong></p>
<pre><code class="language-html">  &lt;table border=&quot;1px&quot;&gt;
      &lt;th&gt;
          &lt;td&gt;칼럼 이름1&lt;/td&gt;
          &lt;td&gt;칼럼 이름2&lt;/td&gt;
      &lt;/th&gt;
      &lt;tbody&gt;
          &lt;tr&gt;
              &lt;td&gt;1 - 1&lt;/td&gt;
              &lt;td&gt;1 - 2&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;2 - 1&lt;/td&gt;
              &lt;td&gt;2 - 2&lt;/td&gt;
          &lt;/tr&gt;
          &lt;tr&gt;
              &lt;td&gt;3 - 1&lt;/td&gt;
              &lt;td&gt;3 - 2&lt;/td&gt;
          &lt;/tr&gt;
      &lt;/tbody&gt;

  &lt;/table&gt;</code></pre>
</li>
</ul>
<hr>
<ul>
<li><p><strong>리스트 태그</strong></p>
<pre><code class="language-html">  &lt;!--정렬 안된 목록--&gt;
  &lt;ul&gt;
      &lt;li&gt;아이템1&lt;/li&gt;
      &lt;li&gt;아이템2&lt;/li&gt;
      &lt;li&gt;아이템3&lt;/li&gt;
  &lt;/ul&gt;

  &lt;!--정렬 된 목록--&gt;
  &lt;ol&gt;
      &lt;li&gt;아이템1&lt;/li&gt;
      &lt;li&gt;아이템2&lt;/li&gt;
      &lt;li&gt;아이템3&lt;/li&gt;
  &lt;/ol&gt;</code></pre>
</li>
</ul>
<hr>
<ul>
<li><p><strong>p 태그와 span 태그</strong></p>
<pre><code class="language-html">  &lt;!--해당 글자만 강조하고 싶을때 사용 --&gt;
  안녕하세요 저는 &lt;p&gt;테스터&lt;/p&gt; 입니다 

  &lt;!--테스터라는 글자 자체만 스타일을 부여하고 싶을때 사용--&gt;
  안녕하세요 저는 &lt;span&gt;테스터&lt;/span&gt; 입니다 </code></pre>
</li>
</ul>
<hr>
<ul>
<li><p><strong>form 태그 ( 백엔드 개발자한테 중요 )</strong></p>
<pre><code class="language-html">  &lt;form method=&quot;post&quot; enctype=&quot;multipart/form-data&quot; action=&quot;http://localhost:8080/test/data&quot;&gt;
      이메일 : &lt;input type=&quot;text&quot; name=&quot;email&quot;&gt;  &lt;br&gt;
      비밀번호 : &lt;input type=&quot;password&quot; name=&quot;pw&quot;&gt; &lt;br&gt;
      사진 : &lt;input type=&quot;file&quot; multiple accept=&quot;image/*&quot; name=&quot;files&quot;&gt; &lt;br&gt;
      개인정보 제공 : &lt;input type=&quot;radio&quot; name=&quot;accept&quot; value=&quot;동의&quot;&gt; 동의
      &lt;input type=&quot;radio&quot; name=&quot;accept&quot; value=&quot;비동의&quot;&gt; 비동의
      &lt;br&gt;

      점심 메뉴 : &lt;input type=&quot;checkbox&quot; name=&quot;lunch&quot; value=&quot;치킨&quot;&gt; 치킨
      &lt;input type=&quot;checkbox&quot; name=&quot;lunch&quot; value=&quot;피자&quot;&gt; 피자
      &lt;input type=&quot;checkbox&quot; name=&quot;lunch&quot; value=&quot;삼겹살&quot;&gt; 삼겹살
      &lt;br&gt;
      예약 날짜 : &lt;input type=&quot;date&quot; name=&quot;date&quot;&gt;
      &lt;br&gt;
      &lt;input type=&quot;reset&quot; value=&quot;초기화버튼&quot;&gt;
      &lt;br&gt;
      &lt;input type=&quot;button&quot; value=&quot;기능없는버튼&quot;&gt;
      &lt;br&gt;
      &lt;!-- submit 버튼을 누르면 input 태그들에 입력한 내용을 form 태그에 지정한 서버로 전송 --&gt;
      &lt;input type=&quot;submit&quot; value=&quot;전송하기버튼&quot;&gt; 
      &lt;br&gt;
      &lt;button&gt;전송하기버튼&lt;/button&gt;
  &lt;/form&gt;</code></pre>
<p>➡ 전송하기 버튼을 클릭하면, <strong>action</strong> 에서 지정한 주소로 요청이 들어 오게 된다. 이때 
&nbsp;&nbsp;　컨트롤러에서 위에서 지정한 각각의 <strong>name과 동일한 이름으로 변수를 만들어 놓으면</strong> 해당 
&nbsp;&nbsp;　요청이 각 변수로 매핑되어 들어오는 것을 확인 할 수 있었다.</p>
</li>
</ul>
<hr>
<blockquote>
<p>🐯 ** CSS 문법 정리하기**</p>
</blockquote>
<ul>
<li><p><strong>CSS 적용하는 법</strong></p>
<pre><code class="language-css">  &lt;!-- 1번째 방법 : style 태그 사용 --&gt;
  &lt;style&gt;
      /* CSS 코드 */
  &lt;/style&gt;

  &lt;!-- 2번째 방법 : css 파일 사용 (많이 사용) --&gt;
  &lt;link rel=&quot;stylesheet&quot; href=&quot;./style.css(css 파일의 경로)&quot; /&gt;</code></pre>
</li>
</ul>
<hr>
<ul>
<li><p><strong>CSS 기본 구조</strong></p>
<pre><code class="language-css">      선택자 {
          디자인 코드 작성
      }

      선택자 : 태그이름, #아이디, .클래스이름
  ------------------------------------------------    
   예시)
    .class2 {
      background-color: red;
    }

    .class1 {
      color: green;
    }

    #p1 {
      color: blue;
    }

    #p2 {
      color: red;
    }

&lt;body&gt;
  &lt;p id=&quot;p1&quot;&gt;안녕하세요&lt;/p&gt;     // #p1 으로 설정된 부분의 스타일 적용
  &lt;p id=&quot;p2&quot;&gt;안녕하세요&lt;/p&gt;     // #p2 으로 설정된 부분의 스타일 적용
  &lt;p class=&quot;class1&quot;&gt;안녕하세요&lt;/p&gt;   // .class1 으로 설정된 부분의 스타일 적용
  &lt;p class=&quot;class1 class2&quot;&gt;안녕하세요&lt;/p&gt; // .class1 으로 설정된 부분의 스타일 적용 후 class2 스타일 적용
&lt;/body&gt;        </code></pre>
</li>
</ul>
<hr>
<ul>
<li><strong>backgroud(뒷 배경) 설정</strong><pre><code class="language-css">    body {
      background-color: green;
    }
    div {
      background-image: url(&#39;1.png&#39;);
      background-position: right;
      background-repeat: no-repeat; /* 작은 이미지를 여러번 반복해서 박스를 채우도록 하는 설정*/
      width: 100%;
      height: 70%;
    }</code></pre>
</li>
</ul>
<hr>
<ul>
<li><p><strong>border(테두리) 설정</strong></p>
<pre><code class="language-css">  .div1 {
    border: 5px solid red;
  }

  .div2 {
    border: 3px dotted green;
  }

  .div3 {
    border: 1px solid gray;

    /* 전부 */
    border-radius: 5px;

    /* 왼쪽 위, 오른쪽 아래 / 오른쪽 위, 왼쪽 아래 */
    border-radius: 5px 10px;

    /* 왼쪽 위/ 오른쪽 위, 왼쪽 아래, / 오른쪽 아래 */
    border-radius: 5px 10px 15px;

    /* 왼쪽 위/ 오른쪽 위 / 오른쪽 아래 / 왼쪽 아래 */
    border-radius: 5px 10px 15px 20px;
  }</code></pre>
</li>
</ul>
<hr>
<ul>
<li><p><strong>padding 과 margin 설정</strong></p>
<pre><code class="language-css">    .div1 {
      margin: 10px;
      padding: 10px;
      width: 50px;
      border: 1px solid black;
      border-radius: 50px;
    }
    .div2 {
      margin-left: 20px;
      margin-right: 20px;
      margin-top: 20px;
      margin-bottom: 20px;

      padding: 5px 10px 15px 20px; /* 위에부터 시계방향 순*/
      padding: 5rem, 10rem, 15rem, 20rem; /* 화면의 비율로 픽셀을 지정*/

      padding-left: 40px;
      padding-right: 50px;
      padding-top: 30px;
      padding-bottom: 30px;

      width: 50px;
      height: 50px;

      border: 1px solid red;
      border-radius: 50px;
    }</code></pre>
</li>
</ul>
<hr>
<ul>
<li><p><strong>링크 설정</strong></p>
<pre><code class="language-css">    a {
      color: red; /* a 태그 글자 색깔 */
      text-decoration: none; /* a 태그 밑줄 삭제 */
      background-color: black;
      text-decoration: none;
    }

    a:visited {
      color: green; /* 한번 클릭했을 때 글자 색깔 */
    }

    a:active {
      color: pink; /* 클릭 했을 때 링크 상태 */
    }

    a:hover {
      color: blue; /* 마우스 위로 올렸을때 보이게하는 색 */
    }</code></pre>
</li>
</ul>
<hr>
<ul>
<li><p><strong>리스트 설정</strong></p>
<pre><code class="language-css">    ul {
      list-style-type: none; /* 리스트 앞 표시(기본 : 동그라미) 지정 */
      background-color: blue;
    }

    li {
      margin-top: 5px;
      margin-bottom: 5px;
      background-color: yellow;
    }

    li:nth-child(2) {   /* 리스트의 첫번째 기준으로 몇번째 자식이냐 */
      margin-top: 5px;
      margin-bottom: 5px;
      background-color: red;
    }

    li:nth-child(3n) {  /* 3, 6, 9 순으로 3의 배수에만 적용 */
      margin-top: 5px;
      margin-bottom: 5px;
      background-color: pink;
    }

    li:nth-child(2n + 2) { /* 지정해 주는 연산에 따라 자식 순번에들에게 적용 가능 */
      background-color: green;
    }

    li:last-child { /* 마지막 번째 자식에 적용 */
      background-color: white;
    }</code></pre>
</li>
</ul>
<hr>
<ul>
<li><p><strong>아이콘 설정 ( 무료 아이콘 템플릿 사용 )</strong></p>
<p>➡ <a href="https://fonts.google.com/icons?selected=Material+Symbols+Outlined:favorite:FILL@0;wght@400;GRAD@0;opsz@24">구글 폰트</a>
➡ <a href="https://fontawesome.com/">Font Awesome</a></p>
<pre><code class="language-css">&lt;!--font awesome 아이콘 적용 시 적용시킬 css 파일--&gt;
</code></pre>
</li>
</ul>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />

<pre><code>  // 아이콘 설정 예시
  &lt;a href=&quot;https://www.youtube.com/&quot;&gt;
    &lt;i class=&quot;btn fa-brands fa-youtube&quot;&gt;&lt;/i&gt;
  &lt;/a&gt;

  &lt;a href=&quot;https://www.instagram.com/&quot;&gt;
    &lt;i class=&quot;btn fa-brands fa-instagram&quot;&gt;&lt;/i&gt;
  &lt;/a&gt;</code></pre><pre><code>---
- **display 설정**
```css
      .container {
        border: 2px blue solid;
        margin-bottom: 30px;
      }
      .content {
        padding: 8px;
        margin: 8px;
        border: 2px red solid;
      }

      #c1 &gt; .content {
        /* block : 한줄을 다 차지는 설정 */
        display: block;
      }

      #c2 &gt; .content {
        /* inline : 내용만 감싸는 설정, 안에 내용을 글자로 취급, 박스 취급 X */
        display: inline;
      }

      #c3 &gt; .content {
        /* inline-block : 내용만 감싸는 설정, 박스 취급 */
        display: inline-block;
      }

      #c4 &gt; .content {
        /* none : 화면에서 사라짐, 자리 차지 X */
        display: none;
      }

      #c5 &gt; .content {
        /* hidden : 화면에서 사라짐, 자리 차지 O */
        visibility: hidden;
      }

      #c6, #c7, #c8 {
        /* flex : 화면 크기에 따라서 배치가 달라지는 박스 */
        display: flex;
      }

      .flex-wrap {
        /* display가 flex인 태그에서 wrap은 줄바꿈으로 내용 정렬 */
        flex-wrap: wrap;
      }

      .flex-nowrap {
        /* display가 flex인 태그에서 wrap은 줄바꿈을 안함 */
        flex-wrap: nowrap;
      }

      .flex-wrap-reverse {
        /* display가 flex인 태그에서 wrap-reverse은 줄바꿈 하고 역순 정렬 */
        flex-wrap: wrap-reverse;
      }

      .flex-container {
        display: flex;
      }
      .flex-container &gt; .content {
        width: 50px;
      }
      .flex-start {
        /* 왼쪽으로 정렬 */
        justify-content: flex-start;
      }
      .flex-center {
        /* 가운데 정렬 */
        justify-content: center;
      }
      .flex-end {
        /* 오른쪽으로 정렬 */
        justify-content: flex-end;
      }
      .flex-space-around {
        /* 균일한 상/하, 좌/우 간격 벌림 */
        justify-content: space-around;
      }
      .flex-space-between {
        /* 균일한 좌/우 간격 벌림 */
        justify-content: space-between;
      }</code></pre><hr>
<ul>
<li><p><strong>반응형 설정 ( 화면 픽셀 크기에 따라 다르게 보이도록 설정 )</strong></p>
<pre><code class="language-css">  div {
    border: solid green;
    width: 100px;
  }
  #div1 {
    width: 1000px;
  }
  #div2 {
    width: 100%;
    max-width: 1000px;
  }
  #div3 {
    min-width: 800px;
  }

  /* 세로 모드 모바일 */
  @media (min-width: 375px) {
    #div4 {
      background-color: yellow;
      display: none;
    }
  }

  /* 가로 모드 모바일 */
  @media (min-width: 576px) {
    #div4 {
      background-color: red;
      display: none;
    }
  }

  /* 일반 태블릿 */
  @media (min-width: 768px) {
    #div4 {
      background-color: blue;
      display: block;
    }
  }
  /* 일반 데스크탑 */
  @media (min-width: 992px) {
    #div4 {
      background-color: black;
    }
  }</code></pre>
</li>
</ul>
<hr>
<ul>
<li><p><strong>position 설정</strong></p>
<pre><code class="language-css">    div {
      border: solid red;
    }
    #div1 {
      /* 위치 지정 X */
      position: static;
    }
    #div2 {
      /* 위치를 상대적으로 지정 */
      display: inline-block;
      position: relative;
      left: 200px;
      top: 100px;
    }
    #div3 {
      /* 브라우저에서 고정된 위치로 지정 */
      position: fixed;
      right: 30px;
      bottom: 20%;
    }
    #div4 {
      /* 절대값으로 위치 지정 */
      position: absolute;
      right: 300px;
      bottom: 200px;
    }

    #div5 {
      /* 화면을 벗어났을 때 fixed 처럼 동작 */
      position: sticky;
      top: 0px;
      left: 50px;
      width: 300px;
    }</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day_53 ( MSA - 3)]]></title>
            <link>https://velog.io/@passion_hd/Day53-MSA-3</link>
            <guid>https://velog.io/@passion_hd/Day53-MSA-3</guid>
            <pubDate>Sun, 14 Jan 2024 12:38:12 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>🦁 JWT를 활용한 로그인 기능 구현</strong></p>
</blockquote>
<ul>
<li><strong>GateWay MSA</strong>
인증을 위한 커스텀 필터와 JWT 토큰을 발급해주는 클래스를 만든다.</li>
</ul>
<p>💻 <strong>인증 필터</strong></p>
<pre><code class="language-java">@Component
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory&lt;AuthorizationHeaderFilter.Config&gt; {
    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    public AuthorizationHeaderFilter() {
        super(Config.class);
    }

    public static class Config {
        // application.yml 파일에서 지정한 filer의 Argument값을 받는 부분
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -&gt; {
            String token = exchange.getRequest().getHeaders().get(&quot;Authorization&quot;).get(0).substring(7);   // 헤더의 토큰 파싱 (Bearer 제거)
            String userId = jwtTokenProvider.getUserId(token);

            addAuthorizationHeaders(exchange.getRequest(), userId);

            return chain.filter(exchange);
        };
    }

    // 성공적으로 검증이 되었기 때문에 인증된 헤더로 요청을 변경해준다. 서비스는 해당 헤더에서 아이디를 가져와 사용한다.
    private void addAuthorizationHeaders(ServerHttpRequest request, String userId) {
        request.mutate()
                .header(&quot;X-Authorization-Id&quot;, userId)
                .build();
    }

    // 토큰 검증 요청을 실행하는 도중 예외가 발생했을 때 예외처리하는 핸들러
    @Bean
    public ErrorWebExceptionHandler tokenValidation() {
        return new JwtTokenExceptionHandler();
    }
    // 실제 토큰이 null, 만료 등 예외 상황에 따른 예외처리
    public class JwtTokenExceptionHandler implements ErrorWebExceptionHandler {
        private String getErrorCode(int errorCode) {
            return &quot;{\&quot;errorCode\&quot;:&quot; + errorCode + &quot;}&quot;;
        }

        @Override
        public Mono&lt;Void&gt; handle(
                ServerWebExchange exchange, Throwable ex) {
            int errorCode = 500;
            if (ex.getClass() == NullPointerException.class) {
                errorCode = 100;
            } else if (ex.getClass() == ExpiredJwtException.class) {
                errorCode = 200;
            }

            byte[] bytes = getErrorCode(errorCode).getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
            return exchange.getResponse().writeWith(Flux.just(buffer));
        }
    }
}</code></pre>
<hr>
<p>💻 <strong>JWT 토큰 발급 클래스</strong></p>
<pre><code class="language-java">@Component
public class JwtTokenProvider {
    @Value(&quot;${jwt.secret-key}&quot;)
    private String secretKey;

    public Key getSignKey(String secretKey) {
        return Keys.hmacShaKeyFor(secretKey.getBytes());
    }

    // 사용자 id 가져오는 메서드
    public String getUserId(String token) {
        return extractAllClaims(token).get(&quot;id&quot;).toString();
    }

    // 토근에서 정보를 가져오는 코드가 계속 중복되어 사용되기 때문에 별도의 메서드로 만들어서 사용하기 위한 것
    public Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSignKey(secretKey))
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}</code></pre>
<hr>
<ul>
<li><strong>Member MSA에서 로그인 요청부터 토큰 발급까지의 과정은 아래와 같다.</strong></li>
</ul>
<p><strong>1) Web Adapter로 로그인 요청이 들어온다.</strong></p>
<pre><code class="language-java">// 요청 Dto
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class LoginMemberReq {

    private String email;
    private String password;
}

// 검증용 Dto
@Getter
@Setter
@Builder
public class LoginMemberCommand {
    @NotNull
    private final String email;
    @NotNull
    private final String password;

    public LoginMemberCommand(String email, String password) {
        this.email = email;
        this.password = password;
    }
}

// Web Adapter
@RestController
@RequiredArgsConstructor
@WebAdapter
public class LoginMemberController {

    private final LoginMemberUseCase loginMemberUseCase;
    @RequestMapping(method = RequestMethod.POST, value = &quot;/member/login&quot;)
    public ResponseEntity login(@RequestBody LoginMemberReq loginMemberReq) {

        LoginMemberCommand loginMemberCommand = LoginMemberCommand.builder()
                .email(loginMemberReq.getEmail())
                .password(loginMemberReq.getPassword())
                .build();

        return ResponseEntity.ok().body(loginMemberUseCase.loginMember(loginMemberCommand));
    }
}</code></pre>
<hr>
<p><strong>2) Input Port(UseCase) 를 통해 서비스를 호출한다.</strong></p>
<pre><code class="language-java">// Input Port
public interface LoginMemberUseCase {
    JwtToken loginMember(LoginMemberCommand loginMemberCommand);
}

// Jwt 도메인
@Getter
@AllArgsConstructor
public class JwtToken {
    private Long Id;
    private String accessToken;
    private String refreshToken;

    public static JwtToken generateJetToken(Long id,String accessToken, String refreshToken){
        return new JwtToken(id, accessToken, refreshToken);
    }
}

// 로그인 서비스
@Service
@RequiredArgsConstructor
public class LoginMemberService implements LoginMemberUseCase {

    private final EmailPasswordCheckPort emailPasswordCheckPort;
    private final CreateJwtPort createJwtPort;

    @Override
    public JwtToken loginMember(LoginMemberCommand loginMemberCommand) {
        Member member = Member.builder()
                .email(loginMemberCommand.getEmail())
                .password(loginMemberCommand.getPassword())
                .build();

        // DB에 저장된 회원의 패스워드와 일치하는지 확인
        MemberJpaEntity memberJpaEntity = emailPasswordCheckPort.emailPasswordCheck(member);

        // 만약 일치한다면, 아래에서 jwt 토큰 발급 후 반환
        if(memberJpaEntity != null) {
            Member loginMember = Member.builder()
                    .id(memberJpaEntity.getId())
                    .email(memberJpaEntity.getEmail())
                    .nickname(memberJpaEntity.getNickname())
                    .build();

            String accessToken = createJwtPort.generateAccessToken(loginMember);
            String refreshToken = createJwtPort.generateRefreshToken(loginMember);

            return JwtToken.generateJetToken(loginMember.getId(), accessToken, refreshToken);
        }
        return null;
    }
}</code></pre>
<hr>
<p><strong>3) 사용자의 아이디와 패스워드가 일치하는지 DB에서 확인하기 위해 OutPut Port를 통해 
 &nbsp;　영속성 어댑터를 호출한다.</strong></p>
<pre><code class="language-java"> // OutPut Port
 public interface EmailPasswordCheckPort {
        MemberJpaEntity emailPasswordCheck(Member member);
}

// 영속성 어댑터
@Component
@RequiredArgsConstructor
@PersistenceAdapter
public class MemberPersistenceAdapter implements RegisterMemberPort, ModifyMemberPort, ModifyMemberStatusPort, EmailPasswordCheckPort {
    private final MemberJpaRepository memberJpaRepository;

    @Override
    public MemberJpaEntity createMember(Member member) {
        MemberJpaEntity memberJpaEntity = MemberJpaEntity.builder()
                .email(member.getEmail())
                .nickname(member.getNickname())
                .password(member.getPassword())
                .status(member.getStatus())
                .build();
        memberJpaRepository.save(memberJpaEntity);
        System.out.println(memberJpaEntity);

        return memberJpaEntity;
    }

        @Override
    public MemberJpaEntity modifyMember(Member member) {
        Optional&lt;MemberJpaEntity&gt; result = memberJpaRepository.findById(member.getId());
        if(result.isPresent()) {

            MemberJpaEntity memberJpaEntity = result.get();

            memberJpaEntity.update(member.getEmail(), member.getPassword(), member.getNickname(), member.getStatus());
            memberJpaRepository.save(memberJpaEntity);
            return memberJpaEntity;
        } else {
            return null;
        }
    }

    @Override
    public Boolean modifyMemberStatus(Member member) {
        Optional&lt;MemberJpaEntity&gt; result = memberJpaRepository.findByEmail(member.getEmail());

        MemberJpaEntity memberJpaEntity = result.get();
        memberJpaEntity.setStatus(true);
        memberJpaRepository.save(memberJpaEntity);

        return true;
    }

    // 여기부터 추가한 이메일, 패스워드 확인 메서드
    @Override
    public MemberJpaEntity emailPasswordCheck(Member member) {
        Optional&lt;MemberJpaEntity&gt; result = memberJpaRepository.findByEmail(member.getEmail());

        if(result.isPresent()) {
            MemberJpaEntity memberJpaEntity = result.get();

            if(memberJpaEntity.getPassword().equals(member.getPassword()) &amp;&amp; memberJpaEntity.getStatus()) {
                return memberJpaEntity;
            }
        }
        return null;
    }
}</code></pre>
<hr>
<p> <strong>4) 이메일, 패스워드 확인이 정상적으로 끝나면 JWT 토큰 발급을 위해 OutPut Port를 통해서 
 &nbsp;　토큰 발급 어댑터로 이동한다.</strong></p>
<pre><code class="language-java">// OutPut Port
public interface CreateJwtPort {
    String generateAccessToken(Member member);
    String generateRefreshToken(Member member);
    boolean validateToken(String token);
    String parseMemberIdFromToken(String token);
}

// 토큰 발급 어댑터
@WebAdapter
@RequiredArgsConstructor
public class createJwtAdapter implements CreateJwtPort {

    @Value(&quot;${jwt.secret-key}&quot;)
    private String secretKey;

    @Value(&quot;${jwt.token.access-expired-time-ms}&quot;)
    private Long accessTokenExpiredTimeMs;

    @Value(&quot;${jwt.token.refresh-expired-time-ms}&quot;)
    private Long refreshTokenExpiredTimeMs;

    @Override
    public String generateAccessToken(Member member) {

        Claims claims = Jwts.claims();
        claims.put(&quot;id&quot;, member.getId());
        claims.put(&quot;email&quot;, member.getEmail());
        claims.put(&quot;nickname&quot;, member.getNickname());

        byte[] secretBytes = secretKey.getBytes();

        String accessToken = Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + accessTokenExpiredTimeMs))
                .signWith(Keys.hmacShaKeyFor(secretBytes), SignatureAlgorithm.HS256)
                .compact();
        return accessToken;
    }

    @Override
    public String generateRefreshToken(Member member) {

        Claims claims = Jwts.claims();
        claims.put(&quot;id&quot;, member.getId());
        claims.put(&quot;email&quot;, member.getEmail());
        claims.put(&quot;nickname&quot;, member.getNickname());

        byte[] secretBytes = secretKey.getBytes();

        String refreshToken = Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + refreshTokenExpiredTimeMs))
                .signWith(Keys.hmacShaKeyFor(secretBytes), SignatureAlgorithm.HS256)
                .compact();
        return refreshToken;
    }
}</code></pre>
<hr>
<ul>
<li><p><strong>여기까지 하면 회원이 로그인할 때 토큰 발급이 완료된다.</strong></p>
</li>
<li><p><strong>다음으로는 상품을 등록할 때 상품의 이미지를 AWS에 업로드 하는 내용이다. MSA는 
&quot;상품 MSA&quot; 와 &quot;상품 이미지 MSA&quot; 가 있다.</strong></p>
</li>
</ul>
<p><strong>1) 상품 등록 요청이 상품 MSA의 Web Adapter로 들어온다.</strong></p>
<pre><code class="language-java">// 요청 Dto
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CreateProductReq {
    private String name;
    private Integer price;
}

// 검증용 Dto
@Getter
@Builder
public class CreateProductCommand {
    @NotNull
    private final String name;

    @NotNull
    private final Integer price;

    @NotNull
    private final Long brandId;

    @NotNull
    private final MultipartFile[] files;

    public CreateProductCommand(String name, Integer price, Long brandId, @NotNull MultipartFile[] files) {
        this.name = name;
        this.price = price;
        this.brandId = brandId;
        this.files = files;
    }
}

// Web Adapter
@RestController
@WebAdapter
@RequiredArgsConstructor
public class CreateProductController {

    private final CreateProductUseCase createProductUseCase;

    @RequestMapping(method = RequestMethod.POST, value = &quot;/product/create&quot;)
    public ResponseEntity create(
            @RequestHeader(value = &quot;X-Authorization-Id&quot;, required = true) Long id,
            @RequestPart(value = &quot;product&quot;) CreateProductReq createProductReq,
            @RequestPart(value = &quot;files&quot;) MultipartFile[] files) {
        CreateProductCommand createProductCommand = CreateProductCommand.builder()
                .brandId(id)
                .name(createProductReq.getName())
                .price(createProductReq.getPrice())
                .files(files)
                .build();

        return ResponseEntity.ok().body(createProductUseCase.createProduct(createProductCommand));

    }
}</code></pre>
<p>➡ <code>@RequestPart</code> 어노테이션은 <strong>&quot;value&quot;</strong> 의 해당하는 이름으로 데이터를 보내면 해당 
 &nbsp;　value 값에 요청이 매핑되도록 해준다.</p>
<p>➡ 위에서는 <code>X-Authorization-id</code> 는 발급 받은 토큰에 있는 정보이고, 이것은 만들어 놓은 
 &nbsp;　커스텀 필터를 통과하면서 자연스럽게 값이 들어가게 된다. </p>
<p>➡ 다음으로 <strong>&quot;product&quot;</strong> 이름으로 Dto 객체의 내용을 JSON 형식으로 보내주고, 
 &nbsp;　<strong>&quot;files&quot;</strong> 이름으로 상품 사진 파일들을 넣어서 요청을 보내면 된다.</p>
<hr>
<p><strong>2) Web Adapter가 Input Port(UseCase)를 통해 서비스를 호출한다.</strong></p>
<pre><code class="language-java">// Input Port
public interface CreateProductUseCase {
    Product createProduct(CreateProductCommand createProductCommand);
}

// 상품 도메인
@Getter
@AllArgsConstructor
@Builder
public class Product {
    private final Long id;
    private final Long brandId;
    private final String name;
    private final Integer price;
}

// 상품 이미지 도메인
@Builder
@Getter
@AllArgsConstructor
public class ProductImages {
    private final Long productId;
    private final MultipartFile[] files;
}

// 상품 서비스
@UseCase
@RequiredArgsConstructor
public class ProductService implements CreateProductUseCase, GetProductUseCase {
    private final CreateProductPort createProductPort;
    private final UploadProductImagePort uploadProductImagePort;
    private final GetProductPort getProductPort;

    @Override
    public Product createProduct(CreateProductCommand createProductCommand) {

        Product product = Product.builder()
                .brandId(createProductCommand.getBrandId())
                .name(createProductCommand.getName())
                .price(createProductCommand.getPrice())
                .build();

        // 상품 정보를 DB에 저장하기 위한 작업
        product = createProductPort.createProduct(product);


        ProductImages productImages = ProductImages.builder()
                .productId(product.getId())
                .files(createProductCommand.getFiles())
                .build();

        // 상품 이미지 업로드를 위한 작업        
        uploadProductImagePort.uploadProductImagePort(productImages);

        return product;
    }
}</code></pre>
<hr>
<p><strong>3) 먼저 상품 데이터를 DB에 저장하기 위해 OutPut Port를 통해 영속성 어댑터를 호출한다.</strong></p>
<pre><code class="language-java">// Output Port
public interface CreateProductPort {
    Product createProduct(Product product);
}

// 상품 엔티티
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class ProductJpaEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long brandId;
    private String name;
    private Integer price;
}

// 상품 레포지터리
@Repository
public interface ProductJpaRepository extends JpaRepository&lt;ProductJpaEntity, Long&gt; {
}

// 영속성 어댑터
@Component
@RequiredArgsConstructor
@PersistenceAdapter
public class ProductPersistenceAdapter implements CreateProductPort, GetProductPort {
    private final ProductJpaRepository productJpaRepository;

    @Override
    public Product createProduct(Product product) {
        ProductJpaEntity productJpaEntity = ProductJpaEntity.builder()
                .brandId(product.getBrandId())
                .name(product.getName())
                .price(product.getPrice())
                .build();

        productJpaEntity = productJpaRepository.save(productJpaEntity);

        return Product.builder()
                .id(productJpaEntity.getId())
                .brandId(productJpaEntity.getBrandId())
                .name(productJpaEntity.getName())
                .price(productJpaEntity.getPrice())
                .build();
    }
}</code></pre>
<hr>
<p><strong>4) 다음으로 상품 이미지 업로드를 위해 이미지 업로드 어댑터를 호출한다.</strong></p>
<pre><code class="language-java">// OutPut Port
public interface UploadProductImagePort {
      void uploadProductImagePort(ProductImages productImages);
}

// 상품 이미지 업로드 어댑터
@WebAdapter
@RequiredArgsConstructor
public class UploadProductImageServiceAdapter implements UploadProductImagePort {
    private final OpenFeignUploadProductImage openFeignUploadProductImage;
    @Override
    public void uploadProductImagePort(ProductImages productImages) {
        try {
            openFeignUploadProductImage.call(productImages.getProductId(), productImages.getFiles());
            System.gc();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// 상품 이미지 MSA로 동기 요청을 보내기 위한 인터페이스
@FeignClient(name = &quot;ProductImage&quot;, url = &quot;http://localhost:8084/productimage&quot;)
public interface OpenFeignUploadProductImage {
    @PostMapping(value = &quot;/upload/{productId}&quot;, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    void call(@PathVariable Long productId, @RequestPart(&quot;files&quot;) MultipartFile[] files);
}</code></pre>
<hr>
<p><strong>5) 상품 이미지 MSA에서 요청을 받기 위해 Web Adapter를 만들어준다.</strong></p>
<pre><code class="language-java">// 요청 Dto
@Data
@Builder
public class RegisterProductImageRequest {
    private final MultipartFile[] files;
}

// 검증용 Dto
@Builder
@Data
public class RegisterProductCommand {
    private final Long productId;
    private final MultipartFile[] files;
}

// Web Adapter
@WebAdapter
@RestController
@RequiredArgsConstructor
public class RegisterProductController {

    private final ProductImageUseCase productInputPort;

    @RequestMapping(method = RequestMethod.POST, value = &quot;/productimage/upload/{productId}&quot;, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity registerProductImage(@PathVariable Long productId, @RequestPart(&quot;files&quot;) MultipartFile[] files) {

        RegisterProductCommand command = RegisterProductCommand.builder()
                .productId(productId)
                .files(files)
                .build();
        return ResponseEntity.ok().body(productInputPort.registerProductImage(command));
    }
}</code></pre>
<hr>
<p><strong>6) InputPort(UseCase)를 통해 서비스를 호출한다.</strong></p>
<pre><code class="language-java">// InputPort
public interface ProductImageUseCase {
    List&lt;ProductImage&gt; registerProductImage(RegisterProductCommand command);
}

// 상품 이미지 도메인
@Builder
@Getter
@AllArgsConstructor
public class ProductImage {
    private final Long id;
    private final Long productId;
    private final String imagePath;
}

// 상품 이미지 서비스
@Service
@RequiredArgsConstructor
public class ProductImageService implements ProductImageUseCase, GetProductImageUseCase {
    private final ProductImagePort productImagePort;
    private final ProductImageUploadPort productImageUploadPort;
    private final GetProductImagePort getProductImagePort;

    @Override
    public List&lt;ProductImage&gt; registerProductImage(RegisterProductCommand command) {
        List&lt;ProductImage&gt; productImages = new ArrayList&lt;&gt;();
        for (MultipartFile file : command.getFiles()) {

            // 상품 이미지 업로드용 어댑터
            String imagePath = productImageUploadPort.uploadProductImage(file);
            ProductImage productImage = ProductImage.builder()
                    .productId(command.getProductId())
                    .imagePath(imagePath)
                    .build();

            // 상품 이미지 DB 저장용 어댑터        
            productImage = productImagePort.registerProductImage(productImage);
            productImages.add(productImage);
        }

        return productImages;
    }
}</code></pre>
<hr>
<p><strong>7) 먼저 상품 이미지 업로드를 위한 어댑터를 호출한다.</strong></p>
<pre><code class="language-java">// Output Port
public interface ProductImageUploadPort {
    String uploadProductImage(MultipartFile file);
}

// 어댑터
@ExternalSystemAdapter
@RequiredArgsConstructor
public class ProductImageUploadAdapter implements ProductImageUploadPort {

    @Value(&quot;${cloud.aws.s3.bucket}&quot;)
    private String bucket;

    private final AmazonS3 s3;

    @Override
    public String uploadProductImage(MultipartFile file) {
        String imagePath = uplopadFile(file);

        return imagePath;
    }


    public String makeFolder() {
        String str = LocalDate.now().format(DateTimeFormatter.ofPattern(&quot;yyyy/MM/dd&quot;));
        String folderPath = str.replace(&quot;/&quot;, File.separator);

        return folderPath;
    }
    public String uplopadFile(MultipartFile file) {
        String originalName = file.getOriginalFilename();
        String folderPath = makeFolder();
        String uuid = UUID.randomUUID().toString();
        String saveFileName = folderPath + File.separator + uuid + &quot;_&quot; + originalName;
        InputStream input = null;
        try {
            input = file.getInputStream();
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentLength(file.getSize());
            metadata.setContentType(file.getContentType());


            s3.putObject(bucket, saveFileName.replace(File.separator, &quot;/&quot;), input, metadata);

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                input.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return s3.getUrl(bucket, saveFileName.replace(File.separator, &quot;/&quot;)).toString();
    }
}</code></pre>
<hr>
<p><strong>8) 다음으로 상품 이미지 정보를 DB에 저장하기 위한 어댑터를 호출한다.</strong></p>
<pre><code class="language-java">// Output Port
public interface ProductImagePort {
    ProductImage registerProductImage(ProductImage product);
}

// 상품 이미지 엔티티
@Getter
@Setter
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProductImageJpaEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long productId;
    private String imagePath;
}

// 상품 이미지 레포지터리
public interface ProductImageJpaRepository extends JpaRepository&lt;ProductImageJpaEntity,Long&gt; {
    List&lt;ProductImageJpaEntity&gt; findAllByProductId(Long productId);
}

// 영속성 어댑터
@PersistenceAdapter
@RequiredArgsConstructor
public class ProductImagePersistenceAdapter implements ProductImagePort, GetProductImagePort {
    private final ProductImageJpaRepository productImageJpaRepository;

    @Override
    public ProductImage registerProductImage(ProductImage product) {
        ProductImageJpaEntity productImageJpaEntity = ProductImageJpaEntity.builder()
                .productId(product.getProductId())
                .imagePath(product.getImagePath())
                .build();
        productImageJpaEntity = productImageJpaRepository.save(productImageJpaEntity);
        return ProductImage.builder()
                .id(productImageJpaEntity.getId())
                .productId(productImageJpaEntity.getProductId())
                .imagePath(productImageJpaEntity.getImagePath())
                .build();
    }
}</code></pre>
<hr>
<ul>
<li><strong>여기까지 하면 상품의 등록 절차가 끝이난다.</strong></li>
</ul>
<hr>
<blockquote>
<p><strong>서킷 브레이커 란❓</strong></p>
</blockquote>
<ul>
<li><p>MSA를 도입하면서 단일 서비스 컴포넌트는 여러개로 쪼개져 서로 호출하는/호출당하는 관계를 가진다. 이런 경우 대두되는 문제중 하나가 서비스 간 장애 전파 이다.</p>
<p>하나의 서비스 컴포넌트에 장애가 발생하면 그걸 호출하는 또다른 컴포넌트까지 장애를 전파받는다.</p>
<blockquote>
<p><strong>Service 1 -- 호출 --&gt; Service 2</strong> ✅</p>
</blockquote>
<p>위와 같은 관계에 놓였을 때 Service 2 의 응답속도가 매우 느려졌다고 가정해보겠다.
이때 <strong>Service 1 의 모든 쓰레드가 2 의 응답을 기다리고만 있다면 다른 요청을 처리할 수 없게 되니 상태는 더 악화된다.</strong></p>
<p>이런 식으로 Service 2 의 상태가 Service 1 에 영향을 주는 경우, <strong>&quot;서비스 간 장애가 전파되었다&quot;</strong> 고 표현한다.</p>
<p>Service 1 과 같이, 2 를 호출하는 또다른 서비스 컴포넌트가 존재한다면 해당 서비스도 장애 전파가 불가피하다. 이런 상황은 자칫 전체 시스템에 영향을 줄 수도 있다.</p>
</li>
</ul>
<br>

<ul>
<li><p>🐶 <strong>Circuit Breaker 패턴</strong>
이런 문제를 해결하는 디자인 패턴 중 하나이다.</p>
<blockquote>
<p><strong>Service 1 --&gt; |Circuit Breaker| --&gt; Service 2</strong> ✅</p>
</blockquote>
<p>기본적으로 Service 1 -&gt; Service 2 호출 사이에 Circuit Breaker 를 설치하는 개념이다.</p>
</li>
<li><p><em>Service 2 로의 모든 호출은 이 Circuit Breaker 를 통하게되고*</em>, 
Service 2 가 정상적인 상황에서는 트래픽이 문제없이 통과한다.</p>
<p>하지만 Circuit Breaker 측에서 <strong>Service 2 에 문제가 생김을 감지</strong>했다면 무슨 일이 발생할까?</p>
<p><strong>Service 2 로의 호출을 강제로 끊어서</strong> Service 1 의 쓰레드들이 더이상 요청에 대한 응답을 기다리지 않도록, <strong>장애가 전파되는걸 방지한다.</strong></p>
</li>
</ul>
<hr>
<blockquote>
<p>🐼 ** 프로젝트에 서킷 브레이커 적용하기**</p>
</blockquote>
<ul>
<li><p><strong><code>pom.xml</code> 파일에 라이브러리 추가</strong></p>
<pre><code class="language-java">      &lt;dependency&gt;
          &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
          &lt;artifactId&gt;spring-cloud-starter-circuitbreaker-resilience4j&lt;/artifactId&gt;
          &lt;version&gt;2.1.7&lt;/version&gt;
      &lt;/dependency&gt;</code></pre>
</li>
<li><p><strong>서킷 브레이커 적용하기</strong></p>
<pre><code class="language-java">@WebAdapter
@RequiredArgsConstructor
public class GetProductServiceAdapter implements GetProductPort {
  private final OpenFeignGetProduct openFeignGetProduct;

  // ✅ 서킷 브레이커 적용을 위한 서킷브레이커 팩토리 의존성 주입 
  private final CircuitBreakerFactory circuitBreakerFactory;

  // ✅ 서킷 브레이커 적용
  @Override
  public Product getProductPort(Long id) {

      CircuitBreaker circuitBreaker = circuitBreakerFactory.create(&quot;getProduct&quot;);

      Product product = circuitBreaker.run(() -&gt;
              openFeignGetProduct.call(id)
              , throwable -&gt; null
      );
      return product;
  }
}</code></pre>
</li>
</ul>
<hr>
<ul>
<li><p>이렇게 하면, 상품 이미지 MSA가 에러가 발생했을 때, 상품의 정보는 그대로 웹페이지에 출력되도록 되고, 상품의 이미지만 안나오게 된다.</p>
</li>
<li><p>만약, 서킷 브레이커 적용을 안해준다면, 상품 이미지 MSA 가 에러 터지면, 상품 MSA도 영향을 받게 되어 모든 서비스가 작동을 안하게 되는 것이다.</p>
</li>
<li><p>따라서, MSA로 구현을 할 때 서비스와 서비스 간 동기 방식으로 HTTP 요청/응답을 보내는 부분이라면, 서킷 브레이커를 적용시켜주는 것이 서비스를 운영하는데 효율적일 것이다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day_52 ( MSA - 2 )]]></title>
            <link>https://velog.io/@passion_hd/Day52-MSA-2</link>
            <guid>https://velog.io/@passion_hd/Day52-MSA-2</guid>
            <pubDate>Thu, 11 Jan 2024 14:15:56 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>** 🦁 Kafka를 이용한 이메일 인증 기능 구축하기**</p>
</blockquote>
<ul>
<li><p>어제 헥사고날 아키텍처와, Kafka 서버 설치 및 테스트하는 것을 진행해 봤고,</p>
</li>
<li><p>오늘은 회원가입 시 회원의 상태인 <strong>&quot;Status&quot;</strong> 를 <strong>false</strong> 로 회원 DB에 저장되도록 하고, 이메일 인증을 통해서 인증이 완료되면 <strong>&quot;Status&quot;</strong> 를 <strong>true</strong>로 변경해주는 기능을 구현해봤다.</p>
</li>
<li><p>먼저, MSA로 구현을 할 때 고려해야될점이 각각의 쪼개진 서비스들끼리 통신을 하는 방식이다. 서비스와 서비스 간 통신을 하는 방법으로는 <strong>&quot;동기 방식&quot;</strong> 과 <strong>&quot;비동기 방식&quot;</strong>이 있다.
➡ <strong>동기 방식</strong> : HTTP 프로토콜을 통한 요청/응답 사용
➡ <strong>비동기 방식</strong> : Kafka를 이용한 메시지 큐 방식 사용</p>
</li>
<li><p>회원의 이메일 인증과 관련되서는 <strong>회원 서비스</strong> 와 <strong>이메일 서비스</strong> 간 통신을 통해 이루어지는데, 이때 회원 가입 시 회원가입 시 입력한 이메일로 인증 메일을 보내는 과정은 <strong>비동기 방식</strong>이 될 것이다. 왜냐하면, 인증 이메일을 보내고 그에 대한 응답을 기다리고 있을 필요가 없기 때문이다.</p>
</li>
<li><p>반면에, 인증 메일을 받고 <strong>인증 절차를 처리하는 과정은 동기 방식</strong>이 될 것이다. 왜냐하면, 인증과 관련된 요청을 보내고 그에 대한 응답을 받아야지만 인증 처리를 할 수 있기 때문이다.</p>
</li>
<li><p>이처럼, MSA를 할 때는 각각의 서비스가 상호 통신하는 과정에서 어떤 방식으로 구현할지를 고려해봐야 할 것이다.</p>
</li>
</ul>
<hr>
<ul>
<li><p>먼저, 이메일 인증 기능을 구현한 코드는 아래와 같다. ( 헥사고날 아키텍처로 구현 )
코드의 순서를 회원가입 요청부터 이메일 인증을 거쳐서 회원 DB에 저장된 회원의 <strong>Status를 false에서 true로 바뀌는 과정</strong>을 순서대로 적어보겠다.</p>
<p><strong>1) 회원 MSA 와 이메일 MSA의 <code>pom.xml</code> 파일에 필요한 라이브러리 추가</strong>
 &nbsp;　➡ 기존의 파일에 오늘 수업 간 새로 추가한 라이브러리만 적어본다.</p>
<p>✅ <strong>회원 MSA</strong></p>
<pre><code class="language-java">     &lt;dependency&gt;
          &lt;groupId&gt;org.springframework.kafka&lt;/groupId&gt;
          &lt;artifactId&gt;spring-kafka&lt;/artifactId&gt;
          &lt;version&gt;2.8.11&lt;/version&gt;
      &lt;/dependency&gt;</code></pre>
<p>✅ <strong>이메일 MSA</strong></p>
<pre><code class="language-java">     &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-mail&lt;/artifactId&gt;
     &lt;/dependency&gt;

      &lt;dependency&gt;
          &lt;groupId&gt;org.springframework.kafka&lt;/groupId&gt;
          &lt;artifactId&gt;spring-kafka&lt;/artifactId&gt;
          &lt;version&gt;2.8.11&lt;/version&gt;
      &lt;/dependency&gt;</code></pre>
<hr>
<p><strong>2) 회원 MSA 와 이메일 MSA의 <code>application.yml</code> 파일에 필요한 설정을 추가</strong></p>
</li>
</ul>
<p>✅ <strong>회원 MSA</strong></p>
<pre><code class="language-java">server:
  port: 8081

spring:
  kafka:
    producer:
      bootstrap-servers: 77.77.77.114:9092  // Kafka 브로커 서버 IP</code></pre>
<hr>
<p>✅ <strong>이메일 MSA</strong></p>
<pre><code class="language-java">server:
  port: 8082

spring:
  kafka:
    consumer:
      bootstrap-servers: 77.77.77.114:9092  // Kafka 브로커 서버 IP
  mail:
    host: smtp.gmail.com
    port: 587
    username: [구글 계정명]
    password: [앱 비밀번호]
    properties:
      mail:
        smtp:
          starttls:
            enable: true
            required: true
          auth: true
          connectiontimeout: 5000
          timeout: 5000
          writetimeout: 5000  </code></pre>
<hr>
<p>  <strong>3) 클라이언트가 회원가입 요청(&quot;/member/register&quot;)을 보내면 가장 먼저 회원 MSA의 
  &nbsp;　Web Adapter로 요청이 들어온다.</strong></p>
<pre><code class="language-java">  // 요청을 받는 Dto 객체
  @Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class RegisterMemberReq {

    private String email;
    private String password;
    private String nickname;
}

// 검증용 Dto 객체
@Getter
@Setter
@Builder
public class RegisterMemberCommand {
    @NotNull
    private final String email;

    @NotNull
    private final String password;

    @NotNull
    private final String nickname;

    public RegisterMemberCommand(String email, String password, String nickname) {
        this.email = email;
        this.password = password;
        this.nickname = nickname;

        // TODO : 검증하는 코드 추가 예정
    }
}


// Web 어댑터 ( 컨트롤러 )
@RestController
@RequiredArgsConstructor
@WebAdapter
public class RegisterMemberController {

    private final RegisterMemberUseCase registerMemberUseCase;

    @RequestMapping(method = RequestMethod.POST, value = &quot;/member/register&quot;)
    public Member registerMember(@RequestBody RegisterMemberReq registerMemberReq) {
        RegisterMemberCommand registerMemberCommand = RegisterMemberCommand.builder()
                .email(registerMemberReq.getEmail())
                .password(registerMemberReq.getPassword())
                .nickname(registerMemberReq.getNickname())
                .build();

        Member result = registerMemberUseCase.registerMember(registerMemberCommand);
        return result;
    }
}</code></pre>
<hr>
<p><strong>4) 요청을 받은 &quot;WebAdapter&quot; 는 &quot;Input Port(UseCase)&quot; 를 통해 애플리케이션 서비스로 
 &nbsp;　요청을 전달한다.</strong></p>
<pre><code class="language-java">// Input Port 인터페이스  (회원 가입)
public interface RegisterMemberUseCase {
    Member registerMember(RegisterMemberCommand registerMemberCommand);
}

// Member 도메인
@Getter
@Setter
@AllArgsConstructor
@Builder
public class Member {
    private final Long id;
    private final String email;
    private final String password;
    private final String nickname;
    private final Boolean status;
}

// 회원가입 서비스
@Service
@RequiredArgsConstructor
public class RegisterMemberService implements RegisterMemberUseCase {

    private final RegisterMemberPort registerMemberPort;
    private final CreateEmailCertEventPort createEmailCertEventPort;
    @Override
    public Member registerMember(RegisterMemberCommand registerMemberCommand) {

        Member member = Member.builder()
                .email(registerMemberCommand.getEmail())
                .password(registerMemberCommand.getPassword())
                .nickname(registerMemberCommand.getNickname())
                .status(false)
                .build();

        MemberJpaEntity memberJpaEntity = registerMemberPort.createMember(member);

        // ------------- 여기까지 회원가입에 대한 어댑터 호출 및 결과 반환-----------------

        // 이메일 전송을 위한 어댑터 호출
        createEmailCertEventPort.createEmailCertEvent(member);

        return Member.builder()
                .id(memberJpaEntity.getId())
                .email(memberJpaEntity.getEmail())
                .password(memberJpaEntity.getPassword())
                .nickname(memberJpaEntity.getNickname())
                .status(memberJpaEntity.getStatus())
                .build();
    }
}</code></pre>
<hr>
<p>5) 애플리케이션 서비스에서는 <strong>회원 가입과 관련되서 PersistenceAdapter</strong>를 호출하는것과 
&nbsp;　이메일 보내는 것과 관련해서 <strong>CreateEmailCertEventAdapter</strong>를 호출하는 것 2개의 처리가 
&nbsp;　있을 것이다.
&nbsp;　➡ <strong>각각의 어댑터를 호출하기 위해서는 &quot;Output Port&quot; 를 통해서 가야한다.</strong></p>
<p>✅ <strong>회원가입 처리</strong></p>
<pre><code class="language-java">// Output Port
public interface RegisterMemberPort {
    MemberJpaEntity createMember(Member member);
}

// Persistence 어댑터 ( DB에 접근하여 회원정보 저장 )
@Component
@RequiredArgsConstructor
@PersistenceAdapter
public class MemberPersistenceAdapter implements RegisterMemberPort {
    private final MemberJpaRepository memberJpaRepository;

    @Override
    public MemberJpaEntity createMember(Member member) {
        MemberJpaEntity memberJpaEntity = MemberJpaEntity.builder()
                .email(member.getEmail())
                .nickname(member.getNickname())
                .password(member.getPassword())
                .status(member.getStatus())
                .build();
        memberJpaRepository.save(memberJpaEntity);

        return memberJpaEntity;
    }
}

// 회원 엔티티
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class MemberJpaEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;
    private String password;
    private String nickname;
    private Boolean status;

}

// 회원 레포지토리
@Repository
public interface MemberJpaRepository extends JpaRepository&lt;MemberJpaEntity, Long&gt; {
    public Optional&lt;MemberJpaEntity&gt; findByEmail(String email);
}</code></pre>
<p>✅ <strong>인증메일 발송 처리를 위한 요청</strong></p>
<pre><code class="language-java">// Output Port
public interface CreateEmailCertEventPort {
    void createEmailCertEvent(Member member);
}

// 인증메일 발송 어댑터 ( 카프카 라이브러리 사용 )
@ExternalSystemAdapter
@RequiredArgsConstructor
public class CreateEmailCertEventAdapter implements CreateEmailCertEventPort {
    private final KafkaTemplate kafkaTemplate;
    @Override
    public void createEmailCertEvent(Member member) {
        ProducerRecord&lt;String, String&gt; record = new ProducerRecord&lt;&gt;(&quot;emailcert&quot;, &quot;email&quot;, member.getEmail());

        kafkaTemplate.send(record);
    }
}</code></pre>
<hr>
<p><strong>6) 여기까지 하면 회원가입 요청에 따라 회원정보가 DB에 저장(Status=false)되었을 것이고,
 &nbsp;　인증메일 발송을 위해서 Kafka의 브로커 서버로 회원의 이메일 주소를 포함하여 메시지를 
 &nbsp;　보낸 것이다.</strong>
 <img src="https://velog.velcdn.com/images/passion_hd/post/7bbcf7e0-bdc9-40e5-9a60-b8a410bdca7e/image.png" alt=""></p>
<p> <img src="https://velog.velcdn.com/images/passion_hd/post/334c32f0-f6c0-4d00-8f61-85a02984f764/image.png" alt=""></p>
<p> &nbsp;　<strong>그럼 이제, 이메일 MSA에서 회원 MSA가 브로커 서버로 보낸 메시지를 수신받도록 
 &nbsp;　작성해준다.</strong></p>
<pre><code class="language-java"> // 메시지를 수신받기 위한 컨트롤러 역할 클래스 작성
 @ExternalSystemAdapter  // 커스텀 어댑터(별도 기능이 있는것은 아님)
@RequiredArgsConstructor
public class CreateEmailCertConsumer {

    private final CreateEmailCertUseCase createEmailCertUseCase;

    @KafkaListener(topics = &quot;emailcert&quot;, groupId = &quot;emailcert-group-00&quot;)
    void createEmailCert(ConsumerRecord&lt;String, String&gt; record) {

        CreateEmailCommand createEmailCommand = CreateEmailCommand.builder()
                .email(record.value())
                .build();

        createEmailCertUseCase.createEmailCert(createEmailCommand);
    }
}</code></pre>
<p> ➡ 이 코드는 Kafka 라이브러리에 구현되어 있는 코드들이다. 중요한것은 프로듀서에서 
 &nbsp;　<strong>메시지를 보낼때의 토픽</strong>과 <strong>컨슈머가 메시지를 받을때 토픽을 반드시 같게</strong> 적어줘야된다는 
 &nbsp;　점이다.</p>
<p>&nbsp;　수신한 <strong>&quot;record&quot;</strong> 에는 사용자의 email 주소가 value 값으로 들어있다.</p>
<hr>
<p><strong>7) WebAdapter에서 Input Port(UseCase)를 통해 애플리케이션 서비스로 요청을 보낸다.</strong></p>
<pre><code class="language-java">// 검증용 Dto객체
@Getter
@Setter
@Builder
public class CreateEmailCommand {
    @NotNull
    private final String email;

    public CreateEmailCommand(String email) {
        this.email = email;
    }
}

// Input Port
public interface CreateEmailCertUseCase {
    EmailCert createEmailCert(CreateEmailCommand createEmailCommand);
}

// EmailCert 도메인
@Getter
@Setter
@AllArgsConstructor
@Builder
public class EmailCert {
    private final String email;
    private final String uuid;
}

// 인증메일 발송 서비스
@Service
@RequiredArgsConstructor
public class CreateEmailCertService implements CreateEmailCertUseCase {

    private final CreateEmailCertPort createEmailCertPort;
    private final SendEmailPort sendEmailPort;
    @Override
    public EmailCert createEmailCert(CreateEmailCommand createEmailCommand) {

        String uuid = UUID.randomUUID().toString();

        EmailCert emailCert = EmailCert.builder()
                .email(createEmailCommand.getEmail())
                .uuid(uuid)
                .build();

        EmailCertJpaEntity emailCertJpaEntity = createEmailCertPort.createEmailCert(emailCert);
        // ------------- 여기까지 이메일 저장을 위한 DB 접근 어댑터 호출 및 결과 반환-----------------

        // 이메일 발송 어댑터 호출
        sendEmailPort.sendEmail(emailCert);

        return EmailCert.builder()
                .id(emailCertJpaEntity.getId())
                .email(emailCertJpaEntity.getEmail())
                .uuid(emailCertJpaEntity.getUuid())
                .build();
    }
}</code></pre>
<hr>
<p>8) 애플리케이션 서비스에서는 <strong>추후 이메일 검증을 위한 이메일과 UUID(중복되지 않은 
 &nbsp;　랜덤한 문자열)을 DB에 저장하기 위해 PersistenceAdapter</strong>를 호출하는것과 
 &nbsp;　이메일 발송하는 것과 관련해서 <strong>SendEmailAdapter</strong>를 호출하는 것 2개의 처리가 
 &nbsp;　있을 것이다.
 &nbsp;　➡ <strong>각각의 어댑터를 호출하기 위해서는 &quot;Output Port&quot; 를 통해서 가야한다.</strong></p>
<p> ✅ <strong>이메일 인증을 위한 정보 DB 저장 처리</strong></p>
<pre><code class="language-java"> // Output Port
 public interface CreateEmailCertPort {
    EmailCertJpaEntity createEmailCert(EmailCert emailCert);
}

// Persistence 어댑터
@Component
@RequiredArgsConstructor
@PersistenceAdapter
public class EmailCertPersistenceAdapter implements CreateEmailCertPort, VerifyEmailCertPort {
    private final EmailCertJpaRepository emailCertJpaRepository;


    @Override
    public EmailCertJpaEntity createEmailCert(EmailCert emailCert) {

        EmailCertJpaEntity emailCertJpaEntity = EmailCertJpaEntity.builder()
                .email(emailCert.getEmail())
                .uuid(emailCert.getUuid())
                .build();

        emailCertJpaRepository.save(emailCertJpaEntity);

        return emailCertJpaEntity;
    }
}

// 인증 관련 엔티티
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class EmailCertJpaEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;
    private String uuid;
}

// 인증 관련 레포지토리
@Repository
public interface EmailCertJpaRepository extends JpaRepository&lt;EmailCertJpaEntity, Long&gt; {
    public Optional&lt;EmailCertJpaEntity&gt; findByEmail(String email);
}</code></pre>
 <br>

<p>✅ ** 인증메일 발송을 위한 처리**</p>
<pre><code class="language-java">// Output Port
public interface SendEmailPort {
    void sendEmail(EmailCert emailCert);
}

// SendEmail 어댑터
@RequiredArgsConstructor
@ExternalSystemAdapter
public class SendEmailAdapter implements SendEmailPort {
    private final JavaMailSender emailSender;

    public void sendEmail(EmailCert emailCert) {
        SimpleMailMessage message = new SimpleMailMessage();

        message.setTo(emailCert.getEmail());
        message.setSubject(&quot;회원가입을 완료하기 위해서 이메일 인증을 진행해 주세요&quot;); // 메일 제목
        message.setText(&quot;http://localhost:8081/member/verify?email=&quot; + emailCert.getEmail() + &quot;&amp;uuid=&quot; + emailCert.getUuid());    // 메일 내용

        emailSender.send(message);
    }
}
</code></pre>
<hr>
<p><strong>9) 여기까지 하면 이메일 검증을 위한 이메일과 UUID(중복되지 않은 랜덤한 문자열)가 
 &nbsp;　DB에 저장되었을 것이고, 회원의 이메일로 인증메일이 발송 되었을 것이다.</strong></p>
<p> <img src="https://velog.velcdn.com/images/passion_hd/post/40c166b4-30d1-4993-838e-c5d5a20560bc/image.png" alt=""></p>
<p> <img src="https://velog.velcdn.com/images/passion_hd/post/9cd3aab8-94e8-4e73-9282-d42ae01ac977/image.png" alt=""></p>
<p> &nbsp;　<strong>그렇다면 이제 회원이 수신한 인증메일에 포함되어 있는 URL을 클릭하면 이메일 인증을 
  &nbsp;　위한 GET 방식의 HTTP 요청이 다시 &quot;회원 MSA의 Web Adapter&quot; 로 전달받게끔 만든다.</strong></p>
<pre><code class="language-java">// 요청을 받는 Dto
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class VerifyMemberReq {
    private String email;
    private String uuid;
}

// 검증용 Dto
@Getter
@Setter
@Builder
public class VerifyMemberCommand {
    @NotNull
    private final String email;
    @NotNull
    private final String uuid;

    public VerifyMemberCommand(String email, String uuid) {
        this.email = email;
        this.uuid = uuid;
    }
}

// Web Adapter
@RestController
@RequiredArgsConstructor
@WebAdapter
public class VerifyMemberController {
    private final VerifyMemberUseCase verifyMemberUseCase;

    @RequestMapping(method = RequestMethod.GET, value = &quot;/member/verify&quot;)
    public Boolean verifyEmail(VerifyMemberReq verifyMemberReq) {
        VerifyMemberCommand verifyMemberCommand = VerifyMemberCommand.builder()
                .email(verifyMemberReq.getEmail())
                .uuid(verifyMemberReq.getUuid())
                .build();

        Boolean result = verifyMemberUseCase.verifyMember(verifyMemberCommand);

        return result;
    }
}
</code></pre>
<hr>
<p><strong>10) WebAdapter는 InputPort(UseCase)를 통해 애플리케이션 서비스를 호출한다.</strong></p>
<pre><code class="language-java">// Input Port
public interface VerifyMemberUseCase {
    Boolean verifyMember(VerifyMemberCommand verifyMemberCommand);
}

// 인증용 VerifyMember 도메인
@Getter
@Setter
@AllArgsConstructor
@Builder
public class VerifyMember {
    private final String email;
    private final String uuid;
}

// 인증 서비스
@Service
@RequiredArgsConstructor
public class VerifyMemberService implements VerifyMemberUseCase {
    private final VerifyMemberPort verifyMemberPort;


    @Override
    public Boolean verifyMember(VerifyMemberCommand verifyMemberCommand) {

        VerifyMember verifyMember = VerifyMember.builder()
                .email(verifyMemberCommand.getEmail())
                .uuid(verifyMemberCommand.getUuid())
                .build();

        return verifyMemberPort.verifyMember(verifyMember);
    }
}</code></pre>
<p>➡ 여기서 기존의 Member 도메인이 아닌 <strong>새로운 VerifyMember 도메인을 생성</strong>하였는데, 
 &nbsp;　이것에 대한 고민이 많았다. </p>
<p> &nbsp;　도메인이란 개념을 내가 이해한 바로는 하나의 MSA 서비스에 해당하는 비지니스 로직들을 
 &nbsp;　포괄적으로 처리할 수 있는 모든 변수들을 포함하고 있다고 생각하여, 처음에는 기존의 
 &nbsp;　Member 도메인에 인증을 위한 <strong>&quot;uuid&quot; 변수만 추가</strong>해줘서 Member 도메인을 쓰면 되지 
 &nbsp;　않을까? 하는 생각을 하였다.</p>
<p> &nbsp;　하지만, MSA를 구현하는데 있어서 의존성을 최소화 시키기 위해 작업을 하고 있는데 
 &nbsp;　기존의 Member 도메인에 갑자기 새로운 변수를 추가하면 Member 도메인과 연관되는 
 &nbsp;　비지니스 로직 처리가 <strong>회원가입과 이메일 인증 2가지</strong>가 되어버리는 것이다.</p>
<p> &nbsp;　따라서, 이것을 별도의 도메인으로 분리해주는게 보다 더 MSA적으로 구현하는 것이라는 
 &nbsp;　것을 깨달았다. 하지만, 또 너무 세분화되어 도메인을 계속 만드는 것도 그리 좋지는 않을 
 &nbsp;　것이다. 따라서 위의 상황에서 &quot;uuid&quot; 1개 정도는 변수로 저장해서 처리하는 등의 방법이 
 &nbsp;　있을 수 있듯이 명확한 정답은 없고, 최대한 클린 아키텍처의 목적을 얼마나 따르는지를 
 &nbsp;　생각해보면 좋을 것 같다.</p>
<hr>
<p><strong>11) 인증서비스는 입력받은 회원의 email과 UUID 값이 DB에 저장된 값과 일치하는지 
 &nbsp;&nbsp;&nbsp;　확인하기 위해 OutPut Port를 통해 이메일 MSA로 확인 요청을 보내게 된다.</strong></p>
<pre><code class="language-java"> // OutputPort
 public interface VerifyMemberPort {
    Boolean verifyMember(VerifyMember verifyMember);
}

// 인증 확인 요청을 보내기 위한 어댑터
@WebAdapter
@RequiredArgsConstructor
@RestController
public class VerifyMemberAdapter implements VerifyMemberPort {

    private final OpenFeignVerifyEmailCertClient openFeignVerifyEmailCertClient;
    private final ModifyMemberStatusPort modifyMemberStatusPort;

    @Override
    public Boolean verifyMember(VerifyMember verifyMember) {

        Boolean response = openFeignVerifyEmailCertClient.call(verifyMember.getEmail(), verifyMember.getUuid());
// ------------------------여기까지 인증 확인 요청을 이메일 MSA로 보냄------------------

// ----여기부터는 인증 확인 결과를 이메일 MSA로 부터 받아서 회원의 상태를 바꾸기 위한 요청---
        if(response == true) {
            return modifyMemberStatusPort.modifyMemberStatus(Member.builder()
                    .email(verifyMember.getEmail())
                    .status(true)
                    .build());
        } else {
            return false;
        }
    }
}</code></pre>
<p> ➡ 여기서, 이제 회원 MSA에서 이메일 MSA로 동기 방식의 HTTP 요청을 보내야 되는데, 
 &nbsp;　그것을 쉽게 보낼 수 있게 스프링 부트에 라이브러리가 또 있다. 바로 <strong>&quot;OpenFeign&quot;</strong> 이다.</p>
<p> ➡ 회원 MSA의 <code>pom.xml</code> 파일에 라이브러리를 추가해주고,</p>
<pre><code class="language-java">         &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-openfeign&lt;/artifactId&gt;
            &lt;version&gt;3.1.3&lt;/version&gt;
        &lt;/dependency&gt;</code></pre>
<p> ➡  회원 MSA의 메인 애플리케이션 클래스에 <code>@EnableFeignClients</code> 을 달아준다.</p>
<p> ➡  그런 다음 위의 어댑터에서 구현한 <code>OpenFeignVerifyEmailCertClient</code> 는 아래와 같이 
  &nbsp;　작성할 수 있다.</p>
<pre><code class="language-java"> @FeignClient(name=&quot;EmailCert&quot;, url=&quot;http://localhost:8082/emailcert/verify&quot;)
public interface OpenFeignVerifyEmailCertClient {

    @GetMapping
    Boolean call(@RequestParam String email, @RequestParam String uuid);
}</code></pre>
<hr>
<p> <strong>12) 그러면 다시 이메일 MSA의 Web Adapter에 이 GET 방식의 HTTP 요청을 받을 수 있도록 
   &nbsp;&nbsp;&nbsp;　작성해준다.</strong></p>
<pre><code class="language-java">// 요청받을 Dto
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class VerifyEmailCertReq {
    private String email;
    private String uuid;
}

// 검증용 Dto
@Getter
@Setter
@Builder
public class VerifyEmailCertCommand {
    @NotNull
    private final String email;

    @NotNull
    private final String uuid;

    public VerifyEmailCertCommand(String email, String uuid) {
        this.email = email;
        this.uuid = uuid;
    }
}

// Web Adapter
@RestController
@RequiredArgsConstructor
@WebAdapter
public class VerifyEmailCertController {
    private final VerifyEmailCertUseCase verifyEmailCertUseCase;

    @RequestMapping(method = RequestMethod.GET, value = &quot;/emailcert/verify&quot;)
    public Boolean verifyEmailCert(VerifyEmailCertReq verifyEmailCertReq) {
            VerifyEmailCertCommand verifyEmailCertCommand = VerifyEmailCertCommand.builder()
                    .email(verifyEmailCertReq.getEmail())
                    .uuid(verifyEmailCertReq.getUuid())
                    .build();

        Boolean result = verifyEmailCertUseCase.verifyEmailCert(verifyEmailCertCommand);

        return result;
    }
}
</code></pre>
<hr>
<p><strong>13) Web Adapter는 Input Port(UseCase)를 통해 인증 서비스를 호출한다.</strong></p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class VerifyEmailCertService implements VerifyEmailCertUseCase {
    private final VerifyEmailCertPort verifyEmailCertPort;
    @Override
    public Boolean verifyEmailCert(VerifyEmailCertCommand verifyEmailCertCommand) {
        EmailCert emailCert = EmailCert.builder()
                .email(verifyEmailCertCommand.getEmail())
                .uuid(verifyEmailCertCommand.getUuid())
                .build();

        return verifyEmailCertPort.verifyEmailCert(emailCert);
    }
}</code></pre>
<hr>
<p><strong>14) 인증 서비스는 Output Port를 통해 Persistence 어댑터를 호출한다.</strong></p>
<pre><code class="language-java">// Output Port
public interface VerifyEmailCertPort {
    Boolean verifyEmailCert(EmailCert emailCert);
}

// 기존 rsistence 어댑터에 인증 기능 추가
@Component
@RequiredArgsConstructor
@PersistenceAdapter
public class EmailCertPersistenceAdapter implements CreateEmailCertPort, VerifyEmailCertPort {
    private final EmailCertJpaRepository emailCertJpaRepository;


    @Override
    public EmailCertJpaEntity createEmailCert(EmailCert emailCert) {

        EmailCertJpaEntity emailCertJpaEntity = EmailCertJpaEntity.builder()
                .email(emailCert.getEmail())
                .uuid(emailCert.getUuid())
                .build();

        emailCertJpaRepository.save(emailCertJpaEntity);

        return emailCertJpaEntity;
    }

    //--------------------여기부터 추가한 부분-------------------------
    @Override
    public Boolean verifyEmailCert(EmailCert emailCert) {

        Optional&lt;EmailCertJpaEntity&gt; result = emailCertJpaRepository.findByEmail(emailCert.getEmail());

        if(result.isPresent()) {
            EmailCertJpaEntity emailCertJpaEntity = result.get();

            if(emailCert.getUuid().equals(emailCertJpaEntity.getUuid())) {
                return true;
            } else {
                return false;
            }
        }
        return false;
    }
}</code></pre>
<hr>
<p><strong>15) Persistence 어댑터는 DB로 접근하여 회원의 이메일에 해당하는 데이터가 있는지 
 &nbsp;&nbsp;　확인하고, 있으면 저장된 UUID값과 요청이 들어온 값이 일치하면 true를 반환 시켜주고, 
 &nbsp;&nbsp;　일치하지 않으면 false를 반환시켜준다.</strong></p>
<p> <img src="https://velog.velcdn.com/images/passion_hd/post/df8491a7-588f-4982-9526-6f829d80373c/image.png" alt=""></p>
<p> &nbsp;&nbsp;　<strong>반환한 값은 11번의 &quot;VerifyMember 어댑터&quot; 로 들어오게 되고 &quot;true&quot; 가 반환됬다면, 
 &nbsp;&nbsp;　Output Port를 통해서 다시 Persistence 어댑터를 호출하게 된다.</strong></p>
<pre><code class="language-java"> // Output Port
 public interface ModifyMemberStatusPort {
        Boolean modifyMemberStatus(Member member);
}

// 기존 Persistence 어댑터에 내용 추가
@Component
@RequiredArgsConstructor
@PersistenceAdapter
public class MemberPersistenceAdapter implements RegisterMemberPort, ModifyMemberPort, ModifyMemberStatusPort {
    private final MemberJpaRepository memberJpaRepository;

    @Override
    public MemberJpaEntity createMember(Member member) {
        MemberJpaEntity memberJpaEntity = MemberJpaEntity.builder()
                .email(member.getEmail())
                .nickname(member.getNickname())
                .password(member.getPassword())
                .status(member.getStatus())
                .build();
        memberJpaRepository.save(memberJpaEntity);
        System.out.println(memberJpaEntity);

        return memberJpaEntity;
    }

    // -------------------- 여기부터 추가한 부분------------------------------
    @Override
    public Boolean modifyMemberStatus(Member member) {
        Optional&lt;MemberJpaEntity&gt; result = memberJpaRepository.findByEmail(member.getEmail());

        MemberJpaEntity memberJpaEntity = result.get();
        memberJpaEntity.setStatus(true);
        memberJpaRepository.save(memberJpaEntity);

        return true;
    }
}</code></pre>
<hr>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/60a85f48-99ff-41d5-8aa7-59b7a8aa27ef/image.png" alt=""></p>
<ul>
<li><p>여기까지 해서 회원가입부터 이메일 인증 과정까지 끝이 난다. 딱 봐도, MSA 적으로 구현하는 것이 쉽지 않은 것을 볼 수 있었다. 하지만 코드를 자세히 보면 원리는 동일하게 동작하고 있는 것을 알 수있다.</p>
</li>
<li><p>헥사고날 아키텍처의 개념과 구조를 잘 이해하고 있다면, 코드적으로 구현하는 것은 크게 어려운 일은 아닐 것이다. 하지만 MSA는 아직까지도 명확한 답이 없듯이, 고려해야할 부분도 많고, 파고 들어가면 아직 해결하지 못한 부분도 많이 발생할 것이라 생각한다.</p>
</li>
</ul>
<hr>
<blockquote>
<p>** 🐶 Eureka 설치 및 설정하기**</p>
</blockquote>
<ul>
<li><p><strong>Eureka 란❓</strong></p>
<p>Eureka는 AWS와 같은 클라우드 시스템에서 서비스의 로드 밸런싱과 실패처리 등을 유연하게 가져가 위해 각 서비스들의 IP / Port / InstanceId를 가지고 있는 REST 기반의 미들웨어 서버이다.</p>
<p>Eureka는 마이크로 서비스 기반의 아키텍처의 핵심 원칙 중 하나인 Service Discovery의 역할을 수행한다. MSA에서는 Service의 IP와 Port가 일정하지 않고 지속적을 변화하기 때문에, Client에 Service의 정보를 수동으로 입력하는 것은 한계가 분명하다</p>
<p>하지만, Eureka를 사용하면 디스커버리 서버에 IP와 관련된 값들을 저장 시켜놓고, 각각의 MSA에서는 디스커버리 서버에서 필요한 URL을 불러와서 사용하는 것이 가능해진다.</p>
<ul>
<li><strong>Eureka 설정방법</strong>
현재 나는 member-service, email-cert-service, gateway 가 각각 모듈로 구성되어 있고 여기에 discovery 모듈을 추가해준다.</li>
</ul>
<p>💻 <strong>Eureka 서버 설정</strong> </p>
<p>1)  Eureka 서버 역할을 할 discovery 모듈의 <code>pom.xml</code> 파일에 라이브러리를 추가한다.</p>
<pre><code class="language-java">     &lt;dependency&gt;
          &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
          &lt;artifactId&gt;spring-cloud-starter-netflix-eureka-server&lt;/artifactId&gt;
          &lt;version&gt;3.1.3&lt;/version&gt;
      &lt;/dependency&gt;</code></pre>
<hr>
<p> 2) <code>application.yml</code> 파일에 설정을 추가한다.</p>
<pre><code class="language-java"> server:
   port: 8761
  spring:
    application:
      name: discovery-server
  eureka:
    client:
      register-with-eureka: false
      fetch-registry: false</code></pre>
<p> 3) <code>DiscoveryApplication</code> 메인 클래스에 <code>@EnableEurekaServer</code> 어노테이션을 </p>
<h2 id="달아준다">&nbsp;　 달아준다.</h2>
<p>💻 ** member-service / email-cert-service / gateway 각각에 공통 설정**</p>
<p> 1) <code>pom.xml</code> 파일에 클라이언트 라이브러리 추가</p>
<pre><code class="language-java">     &lt;dependency&gt;
          &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
          &lt;artifactId&gt;spring-cloud-starter-netflix-eureka-client&lt;/artifactId&gt;
          &lt;version&gt;3.1.3&lt;/version&gt;
          &lt;exclusions&gt;
              &lt;exclusion&gt;
                  &lt;groupId&gt;javax.servlet&lt;/groupId&gt;
                  &lt;artifactId&gt;javax.servlet-api&lt;/artifactId&gt;
              &lt;/exclusion&gt;
          &lt;/exclusions&gt;
      &lt;/dependency&gt;</code></pre>
<p> 2) <code>application.yml</code> 파일에 설정 추가</p>
<pre><code class="language-java"> // member-service
 eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:8761/eureka
spring:
   application:
     name: MEMBER-SERVICE

 // email-cert-service    
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:8761/eureka
spring:
   application:
     name: EMAIL-CERT-SERVICE

 // gateway
 server:
   port: 9999

spring:
   application:
     name: gateway
cloud:
  gateway:
    routes:
      - id: member-service
        uri: lb://MEMBER-SERVICE    // lb : 로드 밸런서 의미
        predicates:
          - Path=/member/**
      - id: email-cert-service
        uri: lb://EMAIL-CERT-SERVICE
        predicates:
          - Path=/emailcert/**</code></pre>
<hr>
<p> 3) 메인 Application 클래스에 <code>@EnableDiscoveryClient</code> 달아준다.</p>
</li>
</ul>
<hr>
<ul>
<li>여기까지 하고, <code>localhost:8761</code> 로 접속하면 아래와 같이 출력된다.<br><img src="https://velog.velcdn.com/images/passion_hd/post/a5042cea-5a74-4e72-82fe-ad9131ffba01/image.png" alt=""><br></li>
<li>그러면 이제 모든 요청을 게이트웨이의 포트인 <strong>9999번으로 보내고</strong>, 게이트웨이에서 요청을 보낼때 디스커버리 설정에 추가되있는 url을 불러올 수 있게 된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/a27298d4-1055-46c1-bbc5-614d571c6017/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Day_51 ( MSA - 1 )]]></title>
            <link>https://velog.io/@passion_hd/Day51-MSA-1</link>
            <guid>https://velog.io/@passion_hd/Day51-MSA-1</guid>
            <pubDate>Wed, 10 Jan 2024 14:11:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>MSA(MicroService Architecture) 란 ❓</strong></p>
</blockquote>
<ul>
<li><p>MSA는 작고, 독립적으로 배포가 가능한 각각의 기능을 수행하는 서비스로 구성된 프레임워크를 말하는 것으로 애플리케이션을 <strong>&quot;느슨하게 결합&quot;</strong> 된 서비스의 모임으로 구조화하는 <strong>서비스 지향 아키텍처(SOA)</strong> 스타일의 일종인 소프트웨어 개발 기법이다.</p>
</li>
<li><p><strong>MSA의 등장배경</strong> 🧐</p>
<p>MSA는 기존의 <strong>모놀리식 아키텍처의 한계를 개선</strong>하기 위해 등장하게 되었다. 모놀리식 아키텍처는 모든 구성요소가 한 프로젝트에 통합되어 있는 서비스를 말한다.</p>
<p>웹 개발을 예로 들어보면 지난 글에서도 실습하였듯, 웹 서비스를 배포하기 위해 하나의 스프링 부트 프로젝트를 <code>jar</code> 파일로 패키징하여 배포하는 것을 생각해보면 된다.</p>
<p>소규모의 프로젝트에서는 모놀리식 형태가 간단하고, 유지보수가 편하여 선호되고 있지만, <strong>일정 규모 이상을 넘어가면 한계점을 맞닥드리게 된다.</strong></p>
<br></li>
<li><p><strong>모놀리식 아키텍처의 한계</strong> 🤔</p>
<p><strong>1) 부분 장애가 전체 서비스의 장애로 확대될 수 있다.</strong>
&nbsp;　➡ ex) 회원의 리뷰기능에 문제가 생겼을때, 서비스 전체가 정지되게 된다.</p>
<p><strong>2) 부분적인 &quot;Scale-out(여러 서버로 나눠 일을 처리하는 방식)&quot; 이 어렵다.</strong></p>
<p><strong>3) 서비스의 변경이 어렵고, 수정 시 장애의 영향도 파악이 힘들다.</strong></p>
<p><strong>4) 빌드 시간 및 테스트, 배포하는데 걸리는 시간이 오래 걸린다.</strong></p>
<p><strong>5) 하나의 프레임워크와 언어에 종속적이다.</strong>
&nbsp;　➡ ex) 스프링 프레임워크를 사용할 경우 특정 기능에 한해서 다른 언어를 사용하면 
&nbsp;　　　　기능을 보다 더 쉽게 개발할 수 있는 방법이 있지만, 자바를 사용할 수 밖에 
&nbsp;　　　　없다.</p>
<br>
- 🐼 **MSA의 특징**
이러한 모놀리식 한계점을 개선하여 MSA는 API를 통해서만 모든 구성요소들이 상호작용한다는 특징을 가지고 있다. 제대로 설계 된 MSA는 하나의 비즈니스 범위에 맞춰서 만들어지므로 하나의 기능한 수행하게 된다.

<p>✅ <strong>장&nbsp;　점</strong></p>
<p>1) 서비스별 <strong>개별 배포가 가능</strong>하며 (배포시 전체 서비스의 중단이 없음), 특정 서비스의 
&nbsp;&nbsp;　요구사항만을 반영하여, 빠르게 배포도 가능하다.
 
2) 특정 서비스에 대한 <strong>확장성(scale-out)이 유리</strong>하여, 클라우드 기반 서비스 사용에 
&nbsp;&nbsp;　적합하다.
 
3) 일부 기능에서 발생한 장애가 전체 서비스로 확장될 가능성이 적어 <strong>부분적으로 
&nbsp;&nbsp;　발생하는 장애에 대한 처리가 수월</strong>하다. </p>
<p>4) <strong>새로운 기술을 적용하는 것에 유연</strong>하다. (특정 서비스만 별도의 기술 또는 언어로 구현 </p>
<h2 id="가능">&nbsp;&nbsp;　가능)</h2>
<p>✅ <strong>단&nbsp;　점</strong></p>
<p>1) MSA는 모놀리식 아키텍처에 비해 <strong>상대적으로 많이 복잡</strong>하다. 서비스가 모두 분산되어 
&nbsp;　있기 때문에 개발자는 내부 시스템의 통신을 어떻게 가져가야 할지 정해야하며, 
&nbsp;　통신의 장애와 서버의 부하 등이 있을 경우 어떻게 transaction을 유지할지 결정하고 
&nbsp;　구현해야한다.</p>
<p>&nbsp;　MSA에서는 비즈니스에 대한 DB를 가지고 있는 서비스도 각기 다르고, 서비스의 
&nbsp;　연결을 위해서는 통신이 포함되기 때문에 <strong>트랜잭션을 유지하는게 어렵다.</strong> </p>
<p>2) <strong>통합 테스트가 어렵다.</strong> 개발 환경과 실제 운영환경을 동일하게 가져가는 것이 쉽지 
&nbsp;　않다. </p>
<p>3) <strong>실제 운영환경에 대해서 배포하는 것이 쉽지 않다.</strong> MSA의 경우 서비스 1개를 재배포 
&nbsp;　한다고 하면, 다른 서비스들과의 연계가 정상적으로 이루어지고 있는지도 
&nbsp;　확인해야한다. </p>
<p>4) <strong>데이터가 여러 서비스에 분산되어 있어 관리하기 어렵다</strong>.</p>
</li>
</ul>
<hr>
<blockquote>
<p>** 헥사고날 아키텍처(Hexagonal Architecture) 란 ❓**</p>
</blockquote>
<ul>
<li><p><strong>레이어드 아키텍처(Layered Architecture)</strong> 🧐</p>
<p>지금까지 실습해오면서 작성한 방식은 레이어드 아키텍처를 사용한 방식이었다. 레이어드 아키텍처는 모듈화와 계층화를 통해 시스템을 구성하는 것을 주요 목표로 한다.</p>
<p>따라서, 각 계층은 특정 역할을 수행하며, 계층 간의 의존성은 일반적으로 하위계층에서 상위 계층으로 향한다. 또한, 단일 책임 원칙을 중시하며, 각 계층은 특정 책임에 집중한다는 특징을 가지고 있다.</p>
<p>➡ 장점 : 모듈화 및 유지보수가 용이하고, 각 계층이 명확하게 정의되어 있어, 개발자 간 
&nbsp;　　　　협업이 편리하다.</p>
<p>➡ 단점 : 새로운 요구사항이나 변경이 발생할 경우 전체 계층을 수정해야 할 수 있어서 
&nbsp;　　　　유연성이 부족하다.</p>
<br>
- **헥사고날 아키텍처(Hexagonal Architecture)** ✍

</li>
</ul>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/240d9bf8-a573-492a-adc6-bc1c56936a69/image.png" alt=""></p>
<ul>
<li><p>헥사고날 아키텍처, 또는 <strong>포트와 어댑터 아키텍처(Ports and Adapters Architecture)</strong>는 소프트웨어 아키텍처 중 하나로 주요 목표는 응용 프로그램의 비즈니스 로직을 외부 세계로부터 격리시켜, 유연하고 테스트하기 쉬운 구조를 만드는 것이다.</p>
<p>이를 위해 핵심 비즈니스 로직은 중앙의 <strong>도메인 영역에 위치</strong>하며, 입력과 출력을 처리하는 <strong>포트와 어댑터를 통해 외부와 소통</strong>하는 구조이다.</p>
</li>
<li><p>헥사고날 아키텍처는 내부 구현의 변경이 외부에 미치는 영향을 최소화하여 <strong>유연성을 높이고</strong>, 책임이 분리되어 있어 코드의 이해와 수정이 용이하여 <strong>유지보수성이 좋다.</strong>
또한 각 컴포넌트를 <strong>독립적으로 테스트할 수 있고</strong>, 외부 의존성 없이 테스트를 할 수 있어서 서비스 품질 향상과 개발 속도 향상에 도움이 된다.</p>
</li>
<li><p>하지만 그만큼 <strong>구현하는 것이 복잡하다</strong>는 단점을 가지고 있다.</p>
</li>
<li><p>그렇기 때문에 <strong>헥사고날 아키텍처가 좋다, 레이어드 아키텍처가 좋다는 것은 없으며</strong> 각각의 서비스 특성에 따라 어떠한 아키텍처로 구성하는 것이 효율적인지 생각해 봐야 한다. 따라서, 프로젝트의 규모, 복잡성, 요구사항 등을 분석하여 <strong>적절한 아키텍처를 선택하는 것이 중요</strong>할 것이다.</p>
</li>
</ul>
<hr>
<blockquote>
<p>🦁 ** 헥사고날 아키텍처 구성해보기**</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/passion_hd/post/7109b1eb-a2e0-4d27-af8f-05a7b24ab6a8/image.webp" alt=""></p>
<ul>
<li><p><strong>요청이 들어왔을 때 동작 순서</strong></p>
<p>1) <strong>Web Adapter</strong> 로 클라이언트의 요청이 들어온다.</p>
<p>2) Web Adapter는 <strong>Input Port(UseCase)를 구현한 Application Service를 호출</strong>한다.</p>
<p>3) Application Service로 들어온 요청은 <strong>Domain Model로 전달</strong>한다.</p>
<p>4) <strong>Domain Model은 비즈니스 로직을 처리</strong>하고 <strong>Output Port</strong>를 구현한 <strong>외부 
&nbsp;　시스템과 연결된 Adapter를 호출</strong>하여 처리된 데이터를 외부로 저장하거나 
&nbsp;　외부의 데이터를 가져온다.</p>
<p>5) 필요 시 Application Service는 <strong>Domain의 처리 결과를 받아서 클라이언트에게 
 &nbsp;　반환</strong>한다.</p>
<br></li>
<li><p><strong>패키지 구성도 ( 회원 등록과 수정 기능 )</strong>
<img src="https://velog.velcdn.com/images/passion_hd/post/9dca3509-e3f4-486b-bdd2-be9d18fa7396/image.png" alt=""></p>
</li>
<li><p><strong>이해가 안되어 정리하는 부분 ( 도메인과 엔티티의 차이 )</strong></p>
<p>✍ <strong>도메인(Domain)</strong>
도메인은 소프트웨어 시스템이 다루는 실제 문제 영역이나 비즈니스 도메인을 나타낸다. 비즈니스 규칙, 프로세스, 용어 등을 포함하며, 소프트웨어가 해결해야 하는 실제 문제들을 반영한다.
➡ ex) 은행 서비스의 도메인 : 고객, 계좌, 거래와 같은 은행 업무와 관련된 모든 영역</p>
<p>✍ <strong>엔티티(Entity)</strong>
엔티티는 도메인에서 <strong>특정한 객체</strong>를 나타내는 개념입니다. 고유한 식별자를 가지며, 상태와 행동을 가진 객체로 구현된다.
➡ ex) 은행 도메인에서의 <strong>&quot;계좌&quot;는 엔티티</strong>이다. 각 계좌는 고유한 계좌 번호로 식별되며, 
&nbsp;　　　잔액과 같은 상태를 가지고 입금, 출금과 같은 행동을 수행한다.</p>
</li>
</ul>
<hr>
<p>💻 <strong>도메인</strong></p>
<pre><code class="language-java">@Getter
@Setter
@AllArgsConstructor
@Builder
public class Member {
    private final Long id;
    private final String email;
    private final String password;
    private final String nickname;
    private final Boolean status;
}</code></pre>
<hr>
<p>💻 <strong>Web Adapter 클래스 ( Primary Adapter )</strong></p>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
@WebAdapter  // 단순 WebAdapter라는 이름을 보여주는 커스텀 어노테이션 (기능은 없다)
public class RegisterMemberController {

    private final RegisterMemberUseCase registerMemberUseCase;

    @RequestMapping(method = RequestMethod.POST, value = &quot;/member/register&quot;)
    Member registerMember(@RequestBody RegisterMemberReq registerMemberReq) {
        RegisterMemberCommand registerMemberCommand = RegisterMemberCommand.builder()
                .email(registerMemberReq.getEmail())
                .password(registerMemberReq.getPassword())
                .nickname(registerMemberReq.getNickname())
                .status(true)
                .build();

        Member result =  registerMemberUseCase.registerMember(registerMemberCommand);
        return result;
    }
}</code></pre>
<hr>
<p>💻 <strong>RegisterMemberReq DTO</strong></p>
<pre><code class="language-java">@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class RegisterMemberReq {

    private String email;
    private String password;
    private String nickname;
}
</code></pre>
<p>💻 <strong>@WebAdapter (커스텀 어노테이션)</strong></p>
<pre><code class="language-java">@Target({ElementType.TYPE}) // 클래스나 인터페이스 등에 적용한다
@Component
@Retention(RetentionPolicy.RUNTIME) // 런타임까지 어노테이션이 유지된다
@Documented // 어노테이션을 사용한 곳의 문서에 이 어노테이션 정보가 나타난다.
public @interface WebAdapter { // 어노테이션의 이름을 설정한다

    // Component 어노테이션의 Value 속성과 동일하게 적용시키겠다 (@어노테이션이름(&quot;test&quot;) == @Component(&quot;test&quot;)
    @AliasFor(annotation = Component.class)
    String value() default &quot;&quot;;
}</code></pre>
<hr>
<p>💻 <strong>Input Port ( UseCase )</strong></p>
<pre><code class="language-java">// Application Service에 의해 구현되는 인터페이스
public interface RegisterMemberUseCase {

    Member registerMember(RegisterMemberCommand registerMemberCommand);
}</code></pre>
<p>💻 <strong>Application Service 클래스</strong></p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class RegisterMemberService implements RegisterMemberUseCase {

    private final RegisterMemberPort registerMemberPort;
    @Override
    public Member registerMember(RegisterMemberCommand registerMemberCommand) {

        Member member = Member.builder()
                .email(registerMemberCommand.getEmail())
                .password(registerMemberCommand.getPassword())
                .nickname(registerMemberCommand.getNickname())
                .status(registerMemberCommand.getStatus())
                .build();

        MemberJpaEntity memberJpaEntity = registerMemberPort.createMember(member);

        return Member.builder()
                .id(memberJpaEntity.getId())
                .email(memberJpaEntity.getEmail())
                .password(memberJpaEntity.getPassword())
                .nickname(memberJpaEntity.getNickname())
                .status(memberJpaEntity.getStatus())
                .build();
    }
}</code></pre>
<hr>
<p>💻 <strong>검증용 DTO</strong></p>
<pre><code class="language-java">@Getter
@Setter
@Builder
public class RegisterMemberCommand {
    @NonNull
    private final String email;

    @NonNull
    private final String password;

    @NonNull
    private final String nickname;

    private final Boolean status;

    public RegisterMemberCommand(String email, String password, String nickname, Boolean status) {
        this.email = email;
        this.password = password;
        this.nickname = nickname;
        this.status = status;

        // TODO : 검증하는 코드 추가 예정
    }
}</code></pre>
<hr>
<p>💻 <strong>Output Port</strong></p>
<pre><code class="language-java">// Persistence Adapter에 의해 구현되는 인터페이스
public interface RegisterMemberPort {
    MemberJpaEntity createMember(Member member);
}</code></pre>
<p>💻 <strong>Persistence Adapter</strong></p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
@PersistenceAdapter
public class MemberPersistenceAdapter implements RegisterMemberPort {
    private final SpringDataMemberRepository springDataMemberRepository;

    @Override
    public MemberJpaEntity createMember(Member member) {
        MemberJpaEntity memberJpaEntity = MemberJpaEntity.builder()
                .email(member.getEmail())
                .nickname(member.getNickname())
                .password(member.getPassword())
                .status(member.getStatus())
                .build();
        springDataMemberRepository.save(memberJpaEntity);
        System.out.println(memberJpaEntity);

        return memberJpaEntity;
    }
 }   </code></pre>
<hr>
<p>💻 *<em>엔티티 객체 *</em></p>
<pre><code class="language-java">@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class MemberJpaEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;
    private String password;
    private String nickname;
    private Boolean status;

}
</code></pre>
<p>💻 <strong>Secondary Adapter ( 외부와 통신이 필요한 도메인 모델을 연결 )</strong></p>
<pre><code class="language-java">@Repository
public interface MemberJpaRepository extends JpaRepository&lt;MemberJpaEntity, Long&gt; {
    public Optional&lt;MemberJpaEntity&gt; findByEmail(String email);
}</code></pre>
<hr>
<blockquote>
<p>** Api GateWay 란 ❓**</p>
</blockquote>
<ul>
<li><p>MSA를 사용하면 애플리케이션의 다양한 기능을 개발, 배포 및 유지 관리하기에 좋지만, 고객이 애플리케이션에 빠르고 안전하게 액세스하기가 더 어려워질 수도 있다. 이러한 문제를 해결해주는 것이 API 게이트웨이다. </p>
<p>고객이 각 MSA에 대한 액세스를 개별적으로 요청하는 대신, 게이트웨이가 모든 요청에 대한 단일 진입 점의 역할을 하여, 각각의 요청을 적절한 서비스에 전달하고 그 결과를 클라이언트에게 다시 전달한다.</p>
<p>따라서 API Gateway를 이용하면 통합적으로 엔드포인트와 REST API를 관리할 수 있다.
API 게이트웨이를 등록해주면, 모든 클라이언트는 각 서비스의 엔드포인트 대신 API Gateway로 요청을 전달하여 관리가 용이해 진다. 사용자가 설정한 라우팅 설정에 따라 각 엔드포인트로 클라이언트를 대리하여 요청하고 응답을 받으면 다시 클라이언트에게 전달하는 프록시(proxy) 역할을 하기 때문이다.
 
API Gateway 서비스는 단순히 api 경유지 역할 뿐만 아니라, 엔드포인트 서버에서 공통으로 필요한 인증/인가, 사용량 제어, 요청/응답 변조 등의 다양한 기능을 플러그인 형태로 제공하고 있다.</p>
<p>이러한 플러그인을 API 게이트웨이에서 사용하면, 각 엔드포인트의 서버마다 위의 기능들을 구현하지 않아도 되기 때문에 개발자 입장에서는 개발 비용을 줄일 수 있다는 효과도 있다.</p>
<br></li>
<li><p>🐻 <strong>Api GateWay 설정하기</strong></p>
<p>1) <code>pom.xml</code> 파일에 라이브러리 추가</p>
<pre><code class="language-java">      &lt;dependency&gt;
          &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
          &lt;artifactId&gt;spring-cloud-starter-gateway&lt;/artifactId&gt;
          &lt;version&gt;3.1.3&lt;/version&gt;
      &lt;/dependency&gt;</code></pre>
<p>2) <code>application.yml</code> 파일에 설정 추가</p>
<pre><code class="language-java">server:
port: 9999

# SCG(스프링 클라우드 게이트웨이)
# route : 어떤 URL로 오면 어떤 서버를 실행시켜 주겠다. predicates와 filter로 구성되어 있고, predicates에 일치하는 요청을 URI로 전달
# predicates : SCG로 들어온 요청에서 확인할 조건
# filter : SCG로 들어오는 요청에 대해 선처리 및 후처리할 때 사용하는 기능

spring:
application:
  name: gateway
cloud:
  gateway:
    routes:
      - id: member-service
        uri: http://localhost:8080
        predicates:
          - Path=/member/**</code></pre>
<ul>
<li><p>이렇게 설정을 하면 클라이언트가 <code>http://localhost:9999</code> 로 회원 서비스와 관련된 
요청을 보내면, <code>http://localhost:8080</code> 을 호출하여 요청을 처리한다.</p>
</li>
<li><p>아래는 포스트맨으로 API 요청을 보낸 결과이다.
<img src="https://velog.velcdn.com/images/passion_hd/post/db63ed47-2a96-4425-a67b-2dc52225608d/image.png" alt=""></p>
</li>
</ul>
</li>
</ul>
<hr>
<blockquote>
<p>** Apache Kafka 란 ❓**</p>
</blockquote>
<ul>
<li><p>카프카(Kafka)는 높은 확장성과 내결함성, 대용량 데이터 처리, 실시간 데이터 처리에 특화되어 있는 오픈소스 메시징 시스템이다. Pub-Sub 모델의 <strong>메시지 큐</strong> 형태로 동작하며 분산환경에 특화되어 있다.</p>
</li>
<li><p><strong>메시지 큐(Message Queue, MQ) 란 ❓</strong>
메시지 큐는 메시지 지향 미들웨어(MOM : Message Oriented Middleware)를 구현한 시스템으로 프로그램(프로세스) 간의 데이터를 교환할 때 사용하는 기술이다.
<img src="https://velog.velcdn.com/images/passion_hd/post/f80627f2-6620-4f9f-a44f-b387372247f9/image.png" alt="">
➡ <strong>Producer :</strong> 정보를 제공하는 자
➡ <strong>Consumer :</strong> 정보를 제공받아서 사용하려는 자
➡ <strong>Queue :</strong> Producer의 데이터를 임시 저장 및 Consumer에 제공하는 곳</p>
<p>➡ <strong>Broker :</strong> 실행된 카프카 서버를 말한다.
&nbsp;　　　　　프로듀서와 컨슈머는 별도의 애플리케이션으로 구성되는 반면, 브로커는 
&nbsp;　　　　　카프카 자체이다.
&nbsp;　　　　　Broker(각 서버)는 Kafka Cluster 내부에 존재하며, 서버 내부에 메시지를 
&nbsp;　　　　　저장하고 관리하는 역할을 수행한다.</p>
<p>➡ <strong>Zookeeper :</strong> 분산 애플리케이션 관리를 위한 코디네이션 시스템으로, 분산 
&nbsp;　　　　　　　메시지큐의 메타 정보를 중앙에서 관리하는 역할을 수행한다.</p>
</li>
<li><p>이러한 카프카를 MSA에서 사용하는 가장 큰 이유는 비동기로 데이터를 처리하기 위함으로, 이벤트/데이터가 발생했다면 발생 주체가 카프카로 해당 이벤트/데이터를 전달한다. 그리고 해당 이벤트/데이터가 필요한 곳에서 직접 가져다 사용한다.</p>
</li>
<li><p>예를 들어 회원 서비스에서 새로운 회원이 가입되었다는 메시지를 카프카로 전달한다. 이 메시지를 멤버십 서비스가 컨슘하여 새로운 회원에게 가입 축하 멤버십 포인트를 생성해 부여한다. 동시에 하둡은 이 메시지를 컨슘하여 해당 유저에 대한 데이터를 빅데이터에 저장해 분석한다. 또한 동시에 로그 스태시(로그 수집 시스템)는 이 메시지를 컨슘하여 개발자가 디버깅할 때 사용할 수 있도록 로그를 생성한다.</p>
<p>카프카가 없었다면 회원 서비스가 멤버십 서비스, 하둡, 로그 스태시로 각각 다른 데이터 파이프라인을 통해 데이터를 전송해야 했을 것이다. 이에 반해 카프카를 사용하여 데이터 흐름을 중앙화한다면, 복잡도가 드라마틱하게 낮아지는 것을 확인할 수 있다.</p>
</li>
</ul>
<hr>
<blockquote>
<p>🐷 ** Kafka 설치 및 브로커 서버 작동 테스트하기**</p>
</blockquote>
<ul>
<li><p>준비사항 : 리눅스 가상머신 3대 ( 1대 : 주키퍼/브로커 서버, 2대 : 프로듀서, 컨슈머 서버)</p>
</li>
<li><p>✅ <strong>Kafka 설치방법</strong></p>
<p>1) 리눅스 서버 3대 모두 IP 설정 및 방화벽을 꺼준다.</p>
<p>2) 공식홈페이지에서 Kafka 파일을 다운받는다. 
(<a href="https://www.apache.org/dyn/closer.cgi?path=/kafka/3.2.1/kafka_2.13-3.2.1.tgz">https://www.apache.org/dyn/closer.cgi?path=/kafka/3.2.1/kafka_2.13-3.2.1.tgz</a>)</p>
<p>3) 리눅스 서버 3대에 다운로드한 파일을 옮긴다. ( 다음 순서들도 3대 전부 동일 )
&nbsp;　➡ 윈도우 CMD 창 : <code>scp [파일명] [리눅스계정명]@[IP주소]:[경로]</code></p>
</li>
</ul>
<p>  4) 옮긴 <code>tar</code> 파일의 압축을 해제한다 : <code>tar -xvzf [파일명]</code></p>
<p>  5) 압축해제된 폴더로 이동한다 : <code>cd [파일명]</code></p>
<p>  6) open jdk 11 설치 : <code>yum install java-11-openjdk-devel.x86_64</code>
    &nbsp;　➡ 환경변수 설정 : <code>vi /etc/profile</code>
    &nbsp;　➡ 제일 밑에 <strong>JAVA_HOME</strong> 설정 추가 : 
    <code>export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-11.0.18.0.9-0.3.ea.el8.x86_64</code>
    &nbsp;　➡ 설정 후 변경사항 적용 : <code>source /etc/profile</code></p>
<p>  7) 주키퍼/브로커 서버에서 주키퍼 실행
      &nbsp;　➡ <code>./bin/zookeeper-server-start.sh ./config/zookeeper.properties</code></p>
<p>  8) 주키퍼/브로커 서버의 PUTTY 창을 1개 더 띄워서 브로커 서버 설정
      &nbsp;　➡ <code>vi config/server.properties</code>
      &nbsp;　➡ 38번째 수정 : <code>advertised.listeners=PLAINTEXT://[브로커 서버 IP로 수정]:9092</code>
      &nbsp;　➡ 브로커 서버 실행 : <code>./bin/kafka-server-start.sh ./config/server.properties</code></p>
<p>  9) 프로듀서 서버 실행
 &nbsp;　➡ <code>./bin/kafka-console-producer.sh --topic test --bootstrap-server [브로커서버ip:9092]</code></p>
<p> 10) 컨슈머 서버 실행
 &nbsp;　➡ <code>./bin/kafka-console-consumer.sh --topic test --bootstrap-server [브로커 서버ip:9092]</code></p>
<p> 11) 프로듀서 서버에서 텍스트를 입력했을때, 컨슈머 서버에 입력한 테스트가 출력되면 
  &nbsp;&nbsp;　정상적으로 브로커 서버가 동작하고 있는 것이다.
  <img src="https://velog.velcdn.com/images/passion_hd/post/a8e75394-f0c4-4049-89fa-68dfb20f33c4/image.png" alt=""></p>
<ul>
<li>프로듀서 서버와 컨슈머 서버는 브로커 서버의 동작을 테스트해보기 위해 설치 후 실행해본 것이고, 실제로는 브로커 서버 1대만 있으면 된다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>