<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발 끄적임 노트</title>
    <link>https://smink.tistory.com/</link>
    <description>AWS INFRA, Flutter, SpringBoot, Firebase 등
풀스택으로 열심히 살아가는 초보 개발자 블로그입니다!</description>
    <language>ko</language>
    <pubDate>Fri, 4 Sep 2026 00:01:41 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>smink</managingEditor>
    <image>
      <title>개발 끄적임 노트</title>
      <url>https://tistory1.daumcdn.net/tistory/7842205/attach/74e626d9f48d49eb97aba38c2c664f00</url>
      <link>https://smink.tistory.com</link>
    </image>
    <item>
      <title>RDS 인스턴스 업그레이드 후 Firebase Functions 연결 장애 - HAProxy 트러블슈팅</title>
      <link>https://smink.tistory.com/entry/RDS-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%ED%9B%84-Firebase-Functions-%EC%97%B0%EA%B2%B0-%EC%9E%A5%EC%95%A0-HAProxy-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</link>
      <description>&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;RDS 인스턴스 업그레이드 후 Firebase Functions 연결 장애 &amp;mdash; HAProxy DNS 캐싱 트러블슈팅&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-12 오전 9.54.01.png&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biMa32/dJMcabqEC72/SkJdDWestWewMGTaBPgyw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biMa32/dJMcabqEC72/SkJdDWestWewMGTaBPgyw1/img.png&quot; data-alt=&quot;HAProxy 트러블 슈팅&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biMa32/dJMcabqEC72/SkJdDWestWewMGTaBPgyw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiMa32%2FdJMcabqEC72%2FSkJdDWestWewMGTaBPgyw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1121&quot; height=&quot;478&quot; data-filename=&quot;스크린샷 2026-05-12 오전 9.54.01.png&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;HAProxy 트러블 슈팅&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS 인스턴스를 &lt;code&gt;t4g.xlarge&lt;/code&gt;에서 &lt;code&gt;m7g.2xlarge&lt;/code&gt;로 업그레이드했다. 업그레이드 직후부터 Firebase Functions에서 다음 에러가 발생하기 시작했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Unhandled error Error: Connection lost: The server closed the connection.
    at Object.createConnection (mysql2/promise.js:253:31)
    ...
    code: 'PROTOCOL_CONNECTION_LOST'&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;증상 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;상태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EKS 서비스 &amp;rarr; RDS (Spring Boot, Ruby)&lt;/td&gt;
&lt;td&gt;✅ 정상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bastion &amp;rarr; RDS 직접 접속&lt;/td&gt;
&lt;td&gt;✅ 정상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NLB Target Group 헬스체크&lt;/td&gt;
&lt;td&gt;✅ 정상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firebase Functions &amp;rarr; RDS&lt;/td&gt;
&lt;td&gt;❌ 연결 끊김&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NLB 활성 플로우 수&lt;/td&gt;
&lt;td&gt;한 자리수로 급감&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS에서 실행 중인 Spring Boot, Ruby 서버는 RDS에 직접 연결하기 때문에 이번 장애에서 &lt;b&gt;완전히 영향을 받지 않았다.&lt;/b&gt; Firebase Functions 경유 기능만 중단된 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인을 찾아가는 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: 어디서 끊기는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EKS 서비스와 Bastion에서 RDS 직접 접속이 모두 정상이었으므로, &lt;b&gt;RDS 자체는 문제없다&lt;/b&gt;는 것이 명확했다. 장애 범위가 Firebase Functions &amp;rarr; HAProxy &amp;rarr; RDS 경로로 좁혀졌다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Firebase Functions &amp;rarr; NLB ✅ &amp;rarr; HAProxy EC2 ✅ &amp;rarr; RDS ✅
                                    &amp;uarr;
                               여기서 뭔가 잘못됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NLB 헬스체크도 정상이었다. HAProxy 프로세스 자체는 살아있다는 의미다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: HAProxy 설정 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HAProxy EC2에 직접 접속해서 설정을 확인했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;backend be_service_db
    mode tcp
    option tcp-check
    server rds [RDS_ENDPOINT]:3306 check&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헬스체크 주기가 별도로 설정되어 있지 않았다. HAProxy 기본값은 2초마다 헬스체크를 수행하고 &lt;code&gt;rise 2&lt;/code&gt;(연속 2회 성공 시 UP)이므로, 이론상 RDS가 복구된 후 최대 4초 안에 자동 감지가 되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 자동 복구가 되지 않았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계: 처음 세운 가설 &amp;mdash; TCP/MySQL 타이밍 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;option tcp-check&lt;/code&gt;는 3306 포트가 열렸는지만 확인한다. RDS 재시작 시 TCP 포트가 먼저 열리고 MySQL이 나중에 준비되는 타이밍 차이 때문에 자동 복구가 안 됐을 거라고 생각했다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;RDS 재시작
    &amp;darr;
TCP 포트 3306 오픈    &amp;larr; HAProxy &quot;살아났다!&quot; 판정
    &amp;darr;
MySQL 초기화 진행 중  &amp;larr; 실제로는 아직 준비 안 됨
    &amp;darr;
연결 즉시 끊김&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럴듯한 가설이었지만, 실제 로그를 확인해보니 달랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TCP 포트 자체가 열리지 않았다.&lt;/b&gt; TCP/MySQL 타이밍 차이 문제가 발생하려면 TCP 포트가 먼저 열려야 하는데, 이번 경우는 포트 자체에 도달조차 못한 상황이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 원인: HAProxy DNS 캐싱&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RDS 인스턴스 클래스 변경 시 내부 IP가 바뀐다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS 인스턴스 클래스를 변경하면 DNS 엔드포인트 주소는 동일하게 유지되지만, &lt;b&gt;내부 IP 주소는 변경된다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;변경 전: [RDS_ENDPOINT].rds.amazonaws.com &amp;rarr; x.x.x.10
변경 후: [RDS_ENDPOINT].rds.amazonaws.com &amp;rarr; x.x.x.55  &amp;larr; IP 변경&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 &quot;엔드포인트가 같으니 문제없겠지&quot;라고 생각했지만, 문제는 엔드포인트가 아니라 &lt;b&gt;HAProxy가 그 엔드포인트를 어떻게 다루는가&lt;/b&gt;에 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HAProxy는 DNS를 기동 시점에 1회만 조회한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HAProxy는 서버 항목의 DNS를 &lt;b&gt;기동 시점에 단 한 번만 조회&lt;/b&gt;하여 IP를 내부적으로 고정 저장한다. 이는 &lt;a href=&quot;https://www.haproxy.com/documentation/haproxy-configuration-tutorials/proxying-essentials/dns-resolution/&quot;&gt;HAProxy 공식 문서에도 명시된 기본 동작&lt;/a&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;HAProxy 기동
    &amp;darr;
[RDS_ENDPOINT] &amp;rarr; x.x.x.10 (구 IP) 조회 및 고정 저장
    &amp;darr;
RDS 인스턴스 클래스 변경 &amp;rarr; 내부 IP가 x.x.x.55 로 변경
    &amp;darr;
HAProxy는 여전히 x.x.x.10 으로 헬스체크 및 연결 시도
    &amp;darr;
TCP 포트 자체에 도달하지 못함 &amp;rarr; Connection Refused 반복
    &amp;darr;
아무리 시간이 지나도 자동 복구 불가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헬스체크가 2초마다 정상적으로 돌고 있었지만, &lt;b&gt;이미 사라진 구 IP로 계속 확인하고 있었기 때문에&lt;/b&gt; 영원히 복구될 수 없는 상태였던 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;reload가 즉시 복구된 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sudo systemctl reload haproxy&lt;/code&gt;가 즉시 복구 트리거가 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reload는 HAProxy가 설정을 재적용하면서 &lt;b&gt;DNS를 새로 조회&lt;/b&gt;하는 동작을 수반한다. 설정 변경 없이 reload만으로 즉시 복구됐다는 사실 자체가 DNS 캐싱이 원인이었음을 역으로 증명한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NLB 헬스체크 vs HAProxy 내부 헬스체크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 장애에서 헷갈렸던 포인트가 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;헬스체크 주체&lt;/th&gt;
&lt;th&gt;대상&lt;/th&gt;
&lt;th&gt;확인 내용&lt;/th&gt;
&lt;th&gt;이번 결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;NLB TG 헬스체크&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;NLB &amp;rarr; HAProxy 포트 9000&lt;/td&gt;
&lt;td&gt;HAProxy 프로세스가 살아있는지&lt;/td&gt;
&lt;td&gt;✅ 정상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;HAProxy 내부 헬스체크&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;HAProxy &amp;rarr; RDS 포트 3306&lt;/td&gt;
&lt;td&gt;RDS IP로 연결 가능한지&lt;/td&gt;
&lt;td&gt;❌ 구 IP로 시도 중&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NLB 헬스체크가 정상이어도 HAProxy 내부에서 잘못된 IP로 연결을 시도하면 실제 트래픽은 전부 실패한다. NLB 헬스체크 정상이 곧 DB 연결 정상을 의미하지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;재발 방지: resolvers 설정 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헬스체크 주기 조정이 아닌, HAProxy에 &lt;b&gt;DNS 동적 재조회 설정(&lt;code&gt;resolvers&lt;/code&gt;)&lt;/b&gt; 을 추가하는 것이 근본 해결책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/etc/haproxy/haproxy.cfg&lt;/code&gt; 수정:&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;resolvers awsdns
    nameserver dns1 169.254.169.253:53   # AWS VPC 내부 DNS, 모든 VPC에 기본 제공하는 공용 DNS 서버 주소
    resolve_retries 3
    timeout resolve 1s
    timeout retry   1s
    hold valid 10s                        # DNS 캐시 10초만 유지

backend be_service_db
    mode tcp
    option tcp-check
    default-server inter 2s rise 2 fall 3 init-addr none resolvers awsdns resolve-prefer ipv4
    server rds [RDS_ENDPOINT]:3306 check&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 설정:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;resolvers awsdns&lt;/code&gt;: AWS VPC DNS(169.254.169.253)를 사용하는 resolver 블록 정의&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hold valid 10s&lt;/code&gt;: DNS 캐시를 10초만 유지, 이후 재조회&lt;/li&gt;
&lt;li&gt;&lt;code&gt;init-addr none&lt;/code&gt;: 기동 시 DNS 조회 결과를 고정하지 않음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resolve-prefer ipv4&lt;/code&gt;: IPv4 주소 우선 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 문법 검사
sudo haproxy -c -f /etc/haproxy/haproxy.cfg

# 무중단 리로드
sudo systemctl reload haproxy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정 적용 후에는 향후 RDS IP가 변경되더라도 HAProxy가 자동으로 감지하여 수동 개입 없이 복구된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;교훈&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;RDS 인스턴스 클래스 변경 시 내부 IP가 바뀐다&lt;/b&gt;: DNS 엔드포인트가 동일해도 IP는 변경된다. HAProxy처럼 DNS를 캐싱하는 미들웨어가 끼어 있다면 반드시 고려해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HAProxy는 기본적으로 DNS를 캐싱한다&lt;/b&gt;: 공식 문서에 명시된 기본 동작이다. &lt;code&gt;resolvers&lt;/code&gt; 설정 없이는 IP 변경을 자동으로 감지하지 못한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그럴듯한 가설도 로그로 검증해야 한다&lt;/b&gt;: TCP/MySQL 타이밍 문제를 의심했지만, 실제로는 TCP 포트 자체에 도달하지 못하는 상황이었다. 증상만 보고 원인을 단정하지 말 것.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;reload의 동작 원리를 알면 원인이 보인다&lt;/b&gt;: reload가 즉시 복구 트리거가 됐다는 사실 자체가 DNS 캐싱이 원인이었음을 증명한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NLB 헬스체크 정상 &amp;ne; HAProxy 내부 연결 정상 &amp;ne; DB 연결 정상&lt;/b&gt;: 헬스체크 레이어가 여러 단계임을 인식해야 한다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/DevOps</category>
      <category>AWS</category>
      <category>error</category>
      <category>handshake</category>
      <category>HAProxy</category>
      <category>NLB</category>
      <category>RDS</category>
      <category>TCP</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/17</guid>
      <comments>https://smink.tistory.com/entry/RDS-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%ED%9B%84-Firebase-Functions-%EC%97%B0%EA%B2%B0-%EC%9E%A5%EC%95%A0-HAProxy-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85#entry17comment</comments>
      <pubDate>Sat, 9 May 2026 09:30:55 +0900</pubDate>
    </item>
    <item>
      <title>Firebase Functions와 AWS RDS 연결(HAProxy + NLB 구조 설계)</title>
      <link>https://smink.tistory.com/entry/Firebase-Functions%EC%99%80-AWS-RDS-%EC%97%B0%EA%B2%B0HAProxy-NLB-%EA%B5%AC%EC%A1%B0-%EC%84%A4%EA%B3%84</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-08 오후 5.06.27.png&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnKqyZ/dJMcacbYcKZ/9llE3bHYNFGqD8dvxTsI4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnKqyZ/dJMcacbYcKZ/9llE3bHYNFGqD8dvxTsI4k/img.png&quot; data-alt=&quot;HAProxy를 통한 Firebase Functions와 AWS RDS 연결&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnKqyZ/dJMcacbYcKZ/9llE3bHYNFGqD8dvxTsI4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnKqyZ%2FdJMcacbYcKZ%2F9llE3bHYNFGqD8dvxTsI4k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1144&quot; height=&quot;606&quot; data-filename=&quot;스크린샷 2026-05-08 오후 5.06.27.png&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;HAProxy를 통한 Firebase Functions와 AWS RDS 연결&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자사 서비스의 핵심 백엔드는 EKS(Kubernetes) 위에서 운영되며, EKS 애플리케이션들은 프라이빗 서브넷 내에서 RDS에 직접 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 일부 기능은 &lt;b&gt;Firebase Functions&lt;/b&gt;로 운영되고 있다. Firebase Functions는 Google Cloud 위에서 실행되기 때문에 AWS VPC 안에 있는 RDS에 직접 접근할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Firebase Functions에서 AWS RDS로 접근하기 위해 &lt;b&gt;HAProxy + NLB 구조&lt;/b&gt;를 설계한 과정을 다룬다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 직접 연결이 안 되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 &lt;b&gt;프라이빗 서브넷&lt;/b&gt;에 위치해 있다. 외부 인터넷에서 직접 접근할 수 없도록 의도적으로 격리된 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firebase Functions는 Google Cloud의 서버리스 환경에서 실행되며, 고정 IP가 없고 AWS VPC 내부 네트워크에 속하지 않는다. 따라서 VPC 내부 리소스인 RDS에 직접 붙는 것이 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 VPC 안에 있는 EKS 노드는 직접 연결이 가능하지만, Google Cloud에서 실행되는 Firebase Functions는 별도의 경로가 필요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;선택한 구조: HAProxy + NLB&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Firebase Functions (Google Cloud)
        │
        │  TCP :13306 (인터넷)
        ▼
┌───────────────────────────┐
│   NLB (공개 서브넷)          │  &amp;larr; 인터넷에서 접근 가능한 진입점
│   포트 13306 수신           │
└──────────────┬────────────┘
               │  TCP :13306 (VPC 내부)
               ▼
┌───────────────────────────┐
│   HAProxy EC2 (ASG)       │  &amp;larr; 프라이빗 앱 서브넷
│   프라이빗 서브넷             │
│   13306 &amp;rarr; 3306 포워딩       │
└──────────────┬────────────┘
               │  TCP :3306 (VPC 내부)
               ▼
┌───────────────────────────┐
│   RDS MySQL               │  &amp;larr; 프라이빗 DB 서브넷
│   프라이빗 DB 서브넷          │
└───────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;각 구성요소 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NLB (Network Load Balancer)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터넷에 공개된 진입점&lt;/li&gt;
&lt;li&gt;포트 13306으로 들어오는 TCP 트래픽을 수신&lt;/li&gt;
&lt;li&gt;HAProxy EC2 인스턴스로 포워딩&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HAProxy EC2 (Auto Scaling Group)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프라이빗 서브넷에 위치 &amp;mdash; 외부 직접 접근 불가&lt;/li&gt;
&lt;li&gt;NLB에서 받은 13306 트래픽을 RDS의 3306으로 포워딩&lt;/li&gt;
&lt;li&gt;ASG로 구성되어 있어 인스턴스 장애 시 자동 교체&lt;/li&gt;
&lt;li&gt;포트 9000: NLB 헬스체크 전용 엔드포인트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RDS MySQL&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프라이빗 DB 서브넷에 위치&lt;/li&gt;
&lt;li&gt;Security Group으로 접근 제어&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HAProxy 설정&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;frontend fe_service_db
    bind *:13306
    mode tcp
    timeout client 30s
    default_backend be_service_db

backend be_service_db
    mode tcp
    option tcp-check
    server rds [RDS_ENDPOINT]:3306 check&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트 9000은 NLB 헬스체크를 위한 HTTP 엔드포인트다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;frontend fe_healthcheck
    bind *:9000
    mode http
    http-request return status 200 content-type &quot;text/plain&quot; lf-string &quot;OK\n&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;포트 번호를 3306이 아닌 13306으로 쓴 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표준 MySQL 포트(3306)를 외부에 그대로 노출하면 자동화된 포트 스캔 공격의 타겟이 되기 쉽다. 13306은 동일한 TCP 프록시 역할을 하되, 포트 번호만 다르게 운영하는 &lt;b&gt;보안 관행&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NLB 헬스체크 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NLB는 HAProxy의 &lt;b&gt;포트 9000&lt;/b&gt;을 헬스체크 대상으로 사용한다. HAProxy가 정상 실행 중이면 200 OK를 응답한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 HAProxy 프로세스가 죽으면 NLB가 해당 인스턴스를 자동으로 Unhealthy 처리하고, ASG가 새 인스턴스로 교체한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보안 설계&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구간&lt;/th&gt;
&lt;th&gt;허용 트래픽&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;인터넷 &amp;rarr; NLB&lt;/td&gt;
&lt;td&gt;TCP 13306 전체 허용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NLB &amp;rarr; HAProxy EC2&lt;/td&gt;
&lt;td&gt;TCP 13306, 9000 (VPC CIDR 내부)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HAProxy EC2 &amp;rarr; RDS&lt;/td&gt;
&lt;td&gt;TCP 3306 (Security Group)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EKS 노드 &amp;rarr; RDS&lt;/td&gt;
&lt;td&gt;TCP 3306 (Security Group)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bastion &amp;rarr; RDS&lt;/td&gt;
&lt;td&gt;TCP 3306 (관리용)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS Security Group은 HAProxy EC2, EKS 노드, Bastion에서 오는 3306 트래픽만 허용한다. Firebase Functions는 HAProxy를 경유하므로 별도 허용 없이 접근이 가능하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firebase Functions처럼 외부 클라우드 환경에서 프라이빗 VPC 리소스에 접근해야 할 때, VPN이나 VPC Peering 없이도 &lt;b&gt;NLB + HAProxy 조합&lt;/b&gt;으로 안전하게 중간 브릿지를 구성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 이 구조에서 RDS 인스턴스 업그레이드 후 발생한 장애와 해결 과정을 다룰 예정이다.&lt;/p&gt;</description>
      <category>개발/DevOps</category>
      <category>AWS</category>
      <category>firebase</category>
      <category>Functions</category>
      <category>HAProxy</category>
      <category>NLB</category>
      <category>RDS</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/16</guid>
      <comments>https://smink.tistory.com/entry/Firebase-Functions%EC%99%80-AWS-RDS-%EC%97%B0%EA%B2%B0HAProxy-NLB-%EA%B5%AC%EC%A1%B0-%EC%84%A4%EA%B3%84#entry16comment</comments>
      <pubDate>Fri, 8 May 2026 17:08:26 +0900</pubDate>
    </item>
    <item>
      <title>AWS 전체 인프라 구조 (Terraform 모듈 기반)</title>
      <link>https://smink.tistory.com/entry/AWS-%EC%A0%84%EC%B2%B4-%EC%9D%B8%ED%94%84%EB%9D%BC-%EA%B5%AC%EC%A1%B0-Terraform-%EB%AA%A8%EB%93%88-%EA%B8%B0%EB%B0%98</link>
      <description>&lt;h1&gt;AWS 인프라 전체 구조 &amp;mdash; Terraform으로 관리하는 VPC 설계&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;921&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkBp34/dJMcahEj3Bi/KEiShKUka5mJ2l3W4KviCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkBp34/dJMcahEj3Bi/KEiShKUka5mJ2l3W4KviCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkBp34/dJMcahEj3Bi/KEiShKUka5mJ2l3W4KviCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkBp34%2FdJMcahEj3Bi%2FKEiShKUka5mJ2l3W4KviCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1088&quot; height=&quot;921&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;921&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 인프라는 &lt;b&gt;Terraform&lt;/b&gt;으로 관리되며, 총 13개의 모듈로 구성되어 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;cloudfront / ec2-bastion / eks / eks_node_alarm / eks-addon
irsa / network / nodegroup / notifications / rds
rds_alarm / rds-proxy-server / s3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스의 핵심 트래픽은 &lt;b&gt;EKS Cluster&lt;/b&gt;에서 처리되며, 모든 애플리케이션은 프라이빗 DB 서브넷의 RDS MySQL에 직접 연결된다. Firebase Functions는 별도의 HAProxy 경유 경로를 사용한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VPC 네트워크 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리전&lt;/b&gt;: ap-northeast-2 (서울)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서브넷 구성&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;서브넷&lt;/th&gt;
&lt;th&gt;종류&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SBN-pub-a, SBN-pub-c&lt;/td&gt;
&lt;td&gt;공개 (Public)&lt;/td&gt;
&lt;td&gt;Bastion, NLB, NAT Gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SBN-priv-APP-a, SBN-priv-APP-c&lt;/td&gt;
&lt;td&gt;프라이빗 (App)&lt;/td&gt;
&lt;td&gt;EKS 노드, HAProxy EC2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SBN-priv-DB-a, SBN-priv-DB-c&lt;/td&gt;
&lt;td&gt;프라이빗 (DB)&lt;/td&gt;
&lt;td&gt;RDS MySQL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라우팅&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공개 서브넷: Internet Gateway를 통해 인터넷과 직접 통신&lt;/li&gt;
&lt;li&gt;프라이빗 서브넷: NAT Gateway를 통해 아웃바운드 인터넷 통신 (인바운드 차단)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴포넌트별 상세&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. EKS Cluster (메인 서비스)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 기반 서비스를 운영하는 쿠버네티스 클러스터다. 인터넷 사용자의 모든 요청이 이곳에서 처리된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위치: 프라이빗 앱 서브넷&lt;/li&gt;
&lt;li&gt;인증: API 모드 (ConfigMap 비활성화)&lt;/li&gt;
&lt;li&gt;IRSA: OIDC Provider를 통해 파드별 IAM 역할 부여&lt;/li&gt;
&lt;li&gt;Bastion에서 kubectl로 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;노드 그룹 구성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 3개의 노드 그룹이 역할별로 분리 운영된다. Bastion 포함 상시 5개 인스턴스가 구동된다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;노드 그룹&lt;/th&gt;
&lt;th&gt;배포 서비스&lt;/th&gt;
&lt;th&gt;RDS 연결&lt;/th&gt;
&lt;th&gt;Desired&lt;/th&gt;
&lt;th&gt;Min&lt;/th&gt;
&lt;th&gt;Max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App&lt;/td&gt;
&lt;td&gt;Spring Boot API, Ruby API&lt;/td&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;td&gt;Spring Boot API, React&lt;/td&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test&lt;/td&gt;
&lt;td&gt;테스트 서버&lt;/td&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 애플리케이션(Spring Boot, Ruby 등)은 프라이빗 앱 서브넷에서 프라이빗 DB 서브넷의 RDS에 직접 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;App 노드 그룹 (Desired: 2)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핵심 API 서버 2종이 함께 운영되어 Desired를 2로 설정&lt;/li&gt;
&lt;li&gt;트래픽 증가 시 최대 4개까지 자동 확장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Web 노드 그룹 (Desired: 1)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;홈페이지 서버와 React 정적 서빙을 담당&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Test 노드 그룹 (Desired: 1)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스테이징/테스트 목적으로 분리 운영&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. CloudFront + S3&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 파일(이미지, 에셋 등)을 제공하는 CDN 레이어다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Origin: S3 버킷, OAC(Origin Access Control)로 CloudFront에서만 접근 가능&lt;/li&gt;
&lt;li&gt;HTTPS: ACM 인증서 적용, TLSv1.2 이상 강제&lt;/li&gt;
&lt;li&gt;캐싱: 커스텀 캐시 정책 (TTL, 쿼리스트링/헤더/쿠키 설정 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Bastion EC2&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPC 내부 리소스 관리를 위한 점프 서버다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위치: 공개 서브넷, Elastic IP 보유&lt;/li&gt;
&lt;li&gt;역할: RDS 직접 접속, EKS kubectl 관리&lt;/li&gt;
&lt;li&gt;EKS Admin 권한으로 등록되어 kubectl 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. RDS Proxy Server (HAProxy + NLB)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firebase Functions와 RDS를 연결하는 보조 경로다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NLB: 인터넷 facing, 공개 서브넷, 포트 13306&lt;/li&gt;
&lt;li&gt;HAProxy EC2: Auto Scaling Group, 13306 &amp;rarr; 3306 포워딩 (Firebase 전용 경로)&lt;/li&gt;
&lt;li&gt;포트 9000: NLB 헬스체크 전용 HTTP 엔드포인트&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. RDS MySQL&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 전체 애플리케이션이 공유하는 메인 데이터베이스다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위치: 프라이빗 DB 서브넷 (2 AZ)&lt;/li&gt;
&lt;li&gt;인스턴스: m7g.2xlarge&lt;/li&gt;
&lt;li&gt;접근 경로:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EKS 애플리케이션 (Spring Boot, Ruby 등) &amp;rarr; 직접 연결&lt;/li&gt;
&lt;li&gt;Firebase Functions &amp;rarr; NLB &amp;rarr; HAProxy &amp;rarr; RDS&lt;/li&gt;
&lt;li&gt;Bastion &amp;rarr; 직접 연결 (관리용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deletion_protection = true&lt;/code&gt;, &lt;code&gt;performance_insights_enabled = true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;삭제 시 final snapshot 자동 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보안 설계 원칙&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;공개 서브넷        &amp;rarr;  IGW 통해 인터넷 직접 통신
프라이빗 앱 서브넷  &amp;rarr;  NAT GW 아웃바운드만, 인바운드는 EKS Ingress / NLB / Bastion 경유
프라이빗 DB 서브넷  &amp;rarr;  NAT GW 아웃바운드만, 인바운드는 앱 서브넷만 허용&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RDS: HAProxy EC2, EKS 노드, Bastion만 3306 허용&lt;/li&gt;
&lt;li&gt;NLB: 포트 13306만 외부 공개&lt;/li&gt;
&lt;li&gt;EKS: 퍼블릭 엔드포인트는 허용 CIDR 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모니터링 &amp;amp; 알람&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모듈&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rds_alarm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RDS CPU, 연결 수, 스토리지 CloudWatch 알람&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eks_node_alarm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;EKS 노드 CPU, 메모리 알람&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;notifications&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SNS/슬랙 등 알림 발송&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Terraform 모듈 구조&lt;/h2&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;network          &amp;rarr; VPC, 서브넷, IGW, NAT GW, 라우팅 테이블
ec2-bastion      &amp;rarr; Bastion EC2, EIP, IAM Role, SG
rds              &amp;rarr; RDS 인스턴스, Subnet Group, SG
rds-proxy-server &amp;rarr; HAProxy EC2 (ASG + Launch Template), NLB, TG
eks              &amp;rarr; EKS 클러스터, IAM, OIDC Provider
nodegroup        &amp;rarr; 워커 노드 그룹 (app / web / test)
eks-addon        &amp;rarr; EKS 애드온 (CoreDNS, kube-proxy, VPC CNI 등)
irsa             &amp;rarr; 서비스 어카운트별 IAM Role
cloudfront       &amp;rarr; CloudFront Distribution, 캐시 정책
s3               &amp;rarr; S3 버킷
rds_alarm        &amp;rarr; RDS CloudWatch 알람
eks_node_alarm   &amp;rarr; EKS 노드 알람
notifications    &amp;rarr; SNS, 알람 라우팅&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발/DevOps</category>
      <category>AWS</category>
      <category>Bastion</category>
      <category>EC2</category>
      <category>EKS</category>
      <category>Infra</category>
      <category>RDS</category>
      <category>S3</category>
      <category>Terraform</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/15</guid>
      <comments>https://smink.tistory.com/entry/AWS-%EC%A0%84%EC%B2%B4-%EC%9D%B8%ED%94%84%EB%9D%BC-%EA%B5%AC%EC%A1%B0-Terraform-%EB%AA%A8%EB%93%88-%EA%B8%B0%EB%B0%98#entry15comment</comments>
      <pubDate>Fri, 8 May 2026 16:51:47 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes,K8s,K3s ImagePullBackOff - ECR 인증(regcred) 오류 원인, 해결법</title>
      <link>https://smink.tistory.com/entry/Kubernetes-ImagePullBackOff-ECR-%EC%9D%B8%EC%A6%9Dregcred-%EC%98%A4%EB%A5%98-%EC%9B%90%EC%9D%B8%EA%B3%BC-%ED%95%9C-%EC%A4%84-%ED%95%B4%EA%B2%B0%EB%B2%95</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-start=&quot;440&quot; data-end=&quot;448&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 서버에서 특정 Deployment의 Pod가 ImagePullBackOff 상태로 Pending, kubectl describe pod에서 반복적인 Back-off pulling image 이벤트를 확인하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfqrKB/dJMcahJCJYp/C53Kj7FDcspXfRLd80U6r1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfqrKB/dJMcahJCJYp/C53Kj7FDcspXfRLd80U6r1/img.png&quot; data-alt=&quot;kubectl get pods&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfqrKB/dJMcahJCJYp/C53Kj7FDcspXfRLd80U6r1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfqrKB%2FdJMcahJCJYp%2FC53Kj7FDcspXfRLd80U6r1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;790&quot; height=&quot;110&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;kubectl get pods&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ImagePullBackOff는 원인이 다양하기 때문에 먼저 아래 항목을 확인했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1712&quot; data-start=&quot;1292&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1400&quot; data-start=&quot;1292&quot;&gt;Deployment에 imagePullSecrets가 설정되어 있는가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1400&quot; data-start=&quot;1292&quot;&gt;spec.template.spec.imagePullSecrets에 regcred가 있어야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1475&quot; data-start=&quot;1401&quot;&gt;Secret(regcred)이 올바른 네임스페이스에 있는가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1475&quot; data-start=&quot;1401&quot;&gt;Pod와 Secret은 동일 namespace여야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1560&quot; data-start=&quot;1476&quot;&gt;Secret 내부 .dockerconfigjson 구조가 정확한가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1560&quot; data-start=&quot;1476&quot;&gt;auths에 레지스트리 도메인이 정확히 들어가 있어야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1621&quot; data-start=&quot;1561&quot;&gt;이미지가 레지스트리에 실제로 존재하는가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1621&quot; data-start=&quot;1561&quot;&gt;ECR에 해당 태그가 push 되어 있는지 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1712&quot; data-start=&quot;1622&quot;&gt;노드(EC2)의 IAM 역할에 ECR Pull 권한이 있는가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1712&quot; data-start=&quot;1622&quot;&gt;AmazonEC2ContainerRegistryReadOnly 등 권한 필요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Pod가 ImagePullBackOff 상태에 빠졌고, 원인은 Kubernetes가 사용하는 imagePullSecret(regcred)에 들어간 Docker 인증 정보가 잘못된 파일을 참조한 것이었다. 원인은 Docker가 snap으로 설치되어 config.json 경로가 달라졌기 때문이며, 올바른 config.json으로 regcred를 다시 생성하고 Deployment를 재시작하여 문제를 해결했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;448&quot; data-start=&quot;440&quot; data-ke-size=&quot;size26&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;확인 과정&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pod 상태 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1764048488160&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl describe pod &amp;lt;pod-name&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Deployment 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1764048510960&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl get deploy &amp;lt;deployment-name&amp;gt; -o yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(여기서 imagePullSecrets: - name: regcred 가 정상 존재함을 확인)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Secret의 실제 내용(디코딩) 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1764048563075&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl get secret regcred -o jsonpath='{.data.\.dockerconfigjson}' | base64 --decode&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 출력에서 auths에 들어간 도메인이 Deployment 이미지 도메인과 정확히 일치하는지 확인.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker 로그인 과정 확인(로그 메세지에서 단서 발견): GitHub Actions 또는 EC2에서 docker login 로그에서 다음과 같은 경고 발견&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1764048601152&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WARNING! Your credentials are stored unencrypted in '/home/***/snap/docker/3377/.docker/config.json'.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이는 Docker가 snap으로 설치되어 실제 config.json 경로가 /home/ubuntu/snap/docker/current/.docker/config.json 등 snap 경로를 사용한다는 뜻.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 snap 디렉터리 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1764048674441&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ls -al /home/ubuntu/snap/docker/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; current가 3377으로 링크되어 있었음. 즉 실제 config 파일은 .../snap/docker/current/.docker/config.json 에 존재.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;✅ &lt;b&gt;ImagePullBackOff는 Kubernetes가 잘못된 Docker 인증 파일(config.json) 경로로 만들어진 regcred를 사용해 ECR 인증에 실패했기 때문에 발생&lt;/b&gt;했다. (구체적으로, Docker가 snap 경로의 config.json을 이용했음에도 regcred를 일반 경로 파일로 만들어 인증 정보가 비어 있거나 잘못된 상태였음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;448&quot; data-start=&quot;440&quot; data-ke-size=&quot;size26&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;해결 방법 ( 실제 진행 절차)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 잘못 생성된 regcred 삭제&lt;/p&gt;
&lt;pre id=&quot;code_1764048803584&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl delete secret regcred -n default&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. snap 경로의 실제 config.json을 사용해 regcred 재생성&lt;/p&gt;
&lt;pre id=&quot;code_1764048861781&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl create secret generic regcred \
  --from-file=.dockerconfigjson=/home/ubuntu/snap/docker/current/.docker/config.json \
  --type=kubernetes.io/dockerconfigjson \
  --namespace=default&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 생성된 secret 내용 확인(디코딩)&lt;/p&gt;
&lt;pre id=&quot;code_1764048878748&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl get secret regcred -n default -o jsonpath='{.data.\.dockerconfigjson}' | base64 --decode&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 출력에서 auths 아래에 &amp;lt;AWS_ACCOUNT_ID&amp;gt;.dkr.ecr.ap-northeast-2.amazonaws.com 같은 도메인이 정확히 있는지 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Deployment 재시작(새 Pod가 새로운 secret을 사용하게 함)&lt;/p&gt;
&lt;pre id=&quot;code_1764048913806&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl rollout restart deployment &amp;lt;deployment-name&amp;gt; -n default&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 복구 확인&lt;/p&gt;
&lt;pre id=&quot;code_1764048947261&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl get pods -n default
kubectl describe pod &amp;lt;new-pod-name&amp;gt; -n default&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 이상 ImagePullBackOff가 뜨지 않고 Running 상태로 전환됨을 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;448&quot; data-start=&quot;440&quot; data-ke-size=&quot;size26&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;예방 방법(다시는 같은 문제를 안 겪으려면)&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;4655&quot; data-start=&quot;4225&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;4288&quot; data-start=&quot;4225&quot;&gt;Docker 설치 방식(snap vs apt) 확인하고 config.json의 실제 경로를 파악할 것&lt;/li&gt;
&lt;li data-end=&quot;4357&quot; data-start=&quot;4289&quot;&gt;regcred 생성은 항상 해당 호스트에서 실제로 Docker가 사용하는 config.json으로 만들 것&lt;/li&gt;
&lt;li data-end=&quot;4486&quot; data-start=&quot;4358&quot;&gt;CI/CD(예: GitHub Actions)에서 Secret을 생성&amp;middot;업데이트한다면 그 과정의 config.json을 별도로 보관하거나, K8s에 직접 kubectl create secret을 실행하도록 자동화할 것&lt;/li&gt;
&lt;li data-end=&quot;4591&quot; data-start=&quot;4487&quot;&gt;kubectl get secret regcred -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d 로 생성 직후 내용 확인 습관화&lt;/li&gt;
&lt;li data-end=&quot;4655&quot; data-start=&quot;4592&quot;&gt;배포 자동화 시 kubectl rollout restart를 자동 트리거하여 새 Secret 반영을 보장&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;448&quot; data-start=&quot;440&quot; data-ke-size=&quot;size26&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;결론&lt;/h2&gt;
&lt;p data-end=&quot;4857&quot; data-start=&quot;4675&quot; data-ke-size=&quot;size16&quot;&gt;ImagePullBackOff는 다양한 원인이 있으나, 이번 케이스는 &lt;b&gt;잘못된 config.json 경로로 인해 regcred가 올바른 인증정보를 담지 못한 문제&lt;/b&gt;였다. snap으로 설치된 Docker의 config 위치를 정확히 반영해 regcred를 재생성하고 Deployment를 재시작하면 문제가 해결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764049218277&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1) 기존 secret 삭제
kubectl delete secret regcred -n default

# 2) snap 경로의 config.json으로 secret 생성
kubectl create secret generic regcred \
  --from-file=.dockerconfigjson=/home/ubuntu/snap/docker/current/.docker/config.json \
  --type=kubernetes.io/dockerconfigjson \
  --namespace=default

# 3) secret 내용 확인
kubectl get secret regcred -n default -o jsonpath='{.data.\.dockerconfigjson}' | base64 --decode

# 4) 배포 재시작
kubectl rollout restart deployment &amp;lt;deployment-name&amp;gt; -n default

# 5) 상태 확인
kubectl get pods -n default
kubectl describe pod &amp;lt;pod-name&amp;gt; -n default&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/DevOps</category>
      <category>docker</category>
      <category>ecr</category>
      <category>imagepullbackoff</category>
      <category>k3s</category>
      <category>k8s</category>
      <category>kubectl</category>
      <category>kubernetes</category>
      <category>regcred</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/14</guid>
      <comments>https://smink.tistory.com/entry/Kubernetes-ImagePullBackOff-ECR-%EC%9D%B8%EC%A6%9Dregcred-%EC%98%A4%EB%A5%98-%EC%9B%90%EC%9D%B8%EA%B3%BC-%ED%95%9C-%EC%A4%84-%ED%95%B4%EA%B2%B0%EB%B2%95#entry14comment</comments>
      <pubDate>Tue, 25 Nov 2025 15:36:28 +0900</pubDate>
    </item>
    <item>
      <title>[Docker Hub, GitHub Actions] ERROR: failed to build: failed to solve: openjdk:17-jdk: failed to resolve source metadata for docker.io/library/openjdk:17-jdk: docker.io/library/openjdk:17-jdk: not found</title>
      <link>https://smink.tistory.com/entry/Docker-Hub-GitHub-Actions-ERROR-failed-to-build-failed-to-solve-openjdk17-jdk-failed-to-resolve-source-metadata-for-dockeriolibraryopenjdk17-jdk-dockeriolibraryopenjdk17-jdk-not-found</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYMXu7/dJMcad79L9h/jpxZC0PBuWI7acBrlRqfj1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYMXu7/dJMcad79L9h/jpxZC0PBuWI7acBrlRqfj1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYMXu7/dJMcad79L9h/jpxZC0PBuWI7acBrlRqfj1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYMXu7%2FdJMcad79L9h%2FjpxZC0PBuWI7acBrlRqfj1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;200&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 동작중이던 GitHub Action이 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;오늘 갑자기&lt;span&gt; &lt;/span&gt;&lt;/span&gt;아래와 같은이유로 실패를 하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1762243682975&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#0 building with &quot;default&quot; instance using docker driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 188B done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/openjdk:17-jdk
#2 ...
#3 [auth] library/openjdk:pull token for registry-1.docker.io
#3 DONE 0.0s
#2 [internal] load metadata for docker.io/library/openjdk:17-jdk
#2 ERROR: docker.io/library/openjdk:17-jdk: not found
------
 &amp;gt; [internal] load metadata for docker.io/library/openjdk:17-jdk:
------
Dockerfile:1
--------------------
   1 | &amp;gt;&amp;gt;&amp;gt; FROM openjdk:17-jdk
   2 |     
   3 |     COPY build/libs/*SNAPSHOT.jar app.jar
--------------------
ERROR: failed to build: failed to solve: openjdk:17-jdk: failed to resolve source metadata for docker.io/library/openjdk:17-jdk: docker.io/library/openjdk:17-jdk: not found
Error: Process completed with exit code 1.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762245574839&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;% docker manifest inspect openjdk:17-jdk                 
no such manifest: docker.io/library/openjdk:17-jdk&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어를 통해 해당 이미지가 사라진지 확인해보았고, 찾을 수 없다는것 까지도 알게되었다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hub.docker.com/_/openjdk&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://hub.docker.com/_/openjdk&lt;/a&gt; 에서 확인을 해보니, 17버전에 대한 지원 중단 이야기가 몇년전에 나와있었고, 오늘에서야 지원이 중단된 듯 하다.&lt;/p&gt;
&lt;figure id=&quot;og_1762244887359&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;openjdk - Official Image | Docker Hub&quot; data-og-description=&quot;Docker Official Images are a curated set of Docker open source and drop-in solution repositories. Why Official Images? These images have clear documentation, promote best practices, and are designed for the most common use cases.&quot; data-og-host=&quot;hub.docker.com&quot; data-og-source-url=&quot;https://hub.docker.com/_/openjdk&quot; data-og-url=&quot;https://hub.docker.com/_/openjdk&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://hub.docker.com/_/openjdk&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hub.docker.com/_/openjdk&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;openjdk - Official Image | Docker Hub&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Docker Official Images are a curated set of Docker open source and drop-in solution repositories. Why Official Images? These images have clear documentation, promote best practices, and are designed for the most common use cases.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hub.docker.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그 올라온 것들을 보면 특정 버전으론 여전히 이미지가 제공되고 있는것 같아보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 이를 해결하기 위해서 기존의 openjdk 대신 amazoncorretto로 변경을 하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1762245444963&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# FROM openjdk:17-jdk # 과거 사용중이던 openjdk image -&amp;gt; 중단됨
FROM amazoncorretto:17 # 새롭게 바꾼 amazoncorretto image

COPY build/libs/*SNAPSHOT.jar app.jar

EXPOSE 8080

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에 다시 Github Action을 동작시켰고, 바로 문제없이 성공한걸 확인할 수 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1762245298695&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Run docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
#0 building with &quot;default&quot; instance using docker driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 191B done
#1 DONE 0.0s

#2 [internal] load metadata for docker.io/library/amazoncorretto:17
#2 ...

#3 [auth] library/amazoncorretto:pull token for registry-1.docker.io
#3 DONE 0.0s

#2 [internal] load metadata for docker.io/library/amazoncorretto:17
#2 DONE 0.9s

# 생략...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/DevOps</category>
      <category>actions</category>
      <category>amazoncorretto</category>
      <category>docker</category>
      <category>docker hub</category>
      <category>Github</category>
      <category>OpenJDK</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/13</guid>
      <comments>https://smink.tistory.com/entry/Docker-Hub-GitHub-Actions-ERROR-failed-to-build-failed-to-solve-openjdk17-jdk-failed-to-resolve-source-metadata-for-dockeriolibraryopenjdk17-jdk-dockeriolibraryopenjdk17-jdk-not-found#entry13comment</comments>
      <pubDate>Tue, 4 Nov 2025 17:42:13 +0900</pubDate>
    </item>
    <item>
      <title>[AWS RDS] pt-query-digest를 활용한 slow query 분석</title>
      <link>https://smink.tistory.com/entry/AWS-RDS-pt-query-digest%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-slow-query-%EB%B6%84%EC%84%9D</link>
      <description>&lt;h2 data-end=&quot;448&quot; data-start=&quot;440&quot; data-ke-size=&quot;size26&quot;&gt;  문제 상황&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;600&quot; data-start=&quot;450&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;511&quot; data-start=&quot;450&quot;&gt;RDS에서 slow_query_log를 켜둔 상태였는데, 로그가 수만 건 이상 쌓여 관리가 어려워짐&lt;/li&gt;
&lt;li data-end=&quot;552&quot; data-start=&quot;512&quot;&gt;로그를 직접 보면 조건값이 조금씩 달라서 전부 다른 쿼리처럼 보임&lt;/li&gt;
&lt;li data-end=&quot;600&quot; data-start=&quot;553&quot;&gt;실제로는 같은 쿼리 패턴인데 단순 값 차이 때문에 중복 로그처럼 쌓이고 있었음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;656&quot; data-start=&quot;602&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;비슷한 쿼리를 묶어서 패턴화하지 않으면&lt;/b&gt; 개선 포인트를 찾기 어려운 상황&lt;/p&gt;
&lt;p data-end=&quot;656&quot; data-start=&quot;602&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;671&quot; data-start=&quot;663&quot; data-ke-size=&quot;size26&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;해결 방법&lt;/h2&gt;
&lt;p data-end=&quot;704&quot; data-start=&quot;673&quot; data-ke-size=&quot;size16&quot;&gt;저는 크게 두 가지 단계를 거쳐 문제를 해결했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;968&quot; data-start=&quot;706&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;861&quot; data-start=&quot;706&quot;&gt;&lt;b&gt;로그 파일 다운로드&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;861&quot; data-start=&quot;724&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;861&quot; data-start=&quot;824&quot;&gt;AWS Console에서 RDS 인스턴스를 선택하고 '로그 및 이벤트' 탭으로 이동&lt;/li&gt;
&lt;li data-end=&quot;861&quot; data-start=&quot;824&quot;&gt;slowquery/mysql-slowquery.log 파일을 다운로드&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;968&quot; data-start=&quot;863&quot;&gt;&lt;b&gt;로그 분석&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;968&quot; data-start=&quot;881&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;937&quot; data-start=&quot;881&quot;&gt;Percona Toolkit의 pt-query-digest를 이용해 쿼리를 패턴화 &amp;amp; 집계&lt;/li&gt;
&lt;li data-end=&quot;968&quot; data-start=&quot;941&quot;&gt;상위 몇 개의 문제 쿼리만 집중적으로 튜닝&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;998&quot; data-start=&quot;975&quot; data-ke-size=&quot;size26&quot;&gt;  pt-query-digest 사용하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDrN3e/btsQfmrSqAe/klqkmSjWnT9u8eRM6W2suK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDrN3e/btsQfmrSqAe/klqkmSjWnT9u8eRM6W2suK/img.png&quot; data-alt=&quot;pt-query-digest&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDrN3e/btsQfmrSqAe/klqkmSjWnT9u8eRM6W2suK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDrN3e%2FbtsQfmrSqAe%2FklqkmSjWnT9u8eRM6W2suK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;241&quot; height=&quot;282&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;790&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;pt-query-digest&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-end=&quot;1006&quot; data-start=&quot;1000&quot; data-ke-size=&quot;size23&quot;&gt;1. 설치&lt;/h3&gt;
&lt;p data-end=&quot;1042&quot; data-start=&quot;1007&quot; data-ke-size=&quot;size16&quot;&gt;MacOS/Linux 환경에서 아래 명령어를 통해 설치&lt;/p&gt;
&lt;pre id=&quot;code_1756691157053&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew install percona-toolkit
또는
sudo apt-get install percona-toolkit&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3 data-end=&quot;1143&quot; data-start=&quot;1137&quot; data-ke-size=&quot;size23&quot;&gt;2. 실행&lt;/h3&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1756691177041&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pt-query-digest slowquery.log &amp;gt; slowquery_report.txt&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1246&quot; data-start=&quot;1210&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;단 한 줄로 수만 건의 로그가 깔끔한 리포트로 변환됩니다  &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1246&quot; data-start=&quot;1210&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1246&quot; data-start=&quot;1210&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1265&quot; data-start=&quot;1253&quot; data-ke-size=&quot;size26&quot;&gt;  분석 리포트&lt;/h2&gt;
&lt;p data-end=&quot;1302&quot; data-start=&quot;1267&quot; data-ke-size=&quot;size16&quot;&gt;아래는 실제 일부 로그만을 pt-query-digest 통해 분석한 결과의 일부입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uOiSH/btsQd0QBvPm/wsXwgM5u6BKhJiNkMkgFK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uOiSH/btsQd0QBvPm/wsXwgM5u6BKhJiNkMkgFK1/img.png&quot; data-alt=&quot;pt-query-digest 통한 분석 결과 일부&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uOiSH/btsQd0QBvPm/wsXwgM5u6BKhJiNkMkgFK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuOiSH%2FbtsQd0QBvPm%2FwsXwgM5u6BKhJiNkMkgFK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;627&quot; height=&quot;167&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;pt-query-digest 통한 분석 결과 일부&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;524&quot; data-start=&quot;331&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;362&quot; data-start=&quot;331&quot;&gt;&lt;b&gt;27 total&lt;/b&gt; &amp;rarr; 총 27번 실행된 쿼리&lt;/li&gt;
&lt;li data-end=&quot;419&quot; data-start=&quot;363&quot;&gt;&lt;b&gt;1 unique&lt;/b&gt; &amp;rarr; 서로 다른 쿼리 패턴은 딱 1개뿐 (즉, 같은 쿼리가 반복 실행됨)&lt;/li&gt;
&lt;li data-end=&quot;468&quot; data-start=&quot;420&quot;&gt;&lt;b&gt;0.03 QPS&lt;/b&gt; &amp;rarr; 초당 평균 0.03건 실행 (거의 1분에 2건 정도)&lt;/li&gt;
&lt;li data-end=&quot;524&quot; data-start=&quot;469&quot;&gt;&lt;b&gt;0.04x concurrency&lt;/b&gt; &amp;rarr; 평균적으로 DB 연결 리소스를 점유한 수준이 낮음&lt;/li&gt;
&lt;li data-end=&quot;767&quot; data-start=&quot;747&quot;&gt;실행 시간은 &lt;b&gt;평균 1~2초&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;791&quot; data-start=&quot;768&quot;&gt;보내는 행은 &lt;b&gt;200여 개&lt;/b&gt;뿐이지만&lt;/li&gt;
&lt;li data-end=&quot;818&quot; data-start=&quot;792&quot;&gt;실제 스캔한 행은 &lt;b&gt;80만 개&lt;/b&gt; 이상&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;575&quot; data-start=&quot;526&quot; data-ke-size=&quot;size16&quot;&gt; &amp;nbsp; &lt;b&gt;필요한 결과는 200행인데, 백만 단위의 row를 뒤져서 찾고 있음&amp;nbsp;&lt;/b&gt;(인덱스 부재 가능성 높음)&lt;/p&gt;
&lt;p data-end=&quot;575&quot; data-start=&quot;526&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;575&quot; data-start=&quot;526&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;575&quot; data-start=&quot;526&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;663&quot; data-end=&quot;671&quot; data-ke-size=&quot;size26&quot;&gt; 문제 쿼리&amp;nbsp;&lt;/h2&gt;
&lt;p data-end=&quot;575&quot; data-start=&quot;526&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756703697736&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT new CounselorSearchResponseDto(...)
FROM Counselors c
LEFT JOIN Users u ON u.id = c.user_id
LEFT JOIN CounselorGrades cg ON cg.id = c.counselor_grade_id
LEFT JOIN CounselorFavorite f ON c.id = f.counselor_id AND f.user_id = :userId
LEFT JOIN CounselorBadge cb ON cb.counselor_id = c.id
WHERE c.status NOT IN ('inactive','terminated','unapproved')
  AND c.user_id NOT IN (
      SELECT ub.user_id 
      FROM UserBlock ub 
      WHERE ub.reporter_id = :userId 
        AND NOT (ub.is_deleted IS NULL OR ub.is_deleted = 1)
  )
ORDER BY c.sequence DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2588&quot; data-start=&quot;2557&quot;&gt;여러 테이블 &lt;b&gt;LEFT JOIN + 서브쿼리&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2642&quot; data-start=&quot;2589&quot;&gt;Rows_examined가 80만인데, 실제 반환은 200행 &amp;rarr; &lt;b&gt;인덱스 미사용&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2710&quot; data-start=&quot;2643&quot;&gt;문자열 비교(availableStartTime, availableEndTime)로 인해 인덱스 사용 어려움&lt;/li&gt;
&lt;li data-end=&quot;2756&quot; data-start=&quot;2711&quot;&gt;정렬(ORDER BY c.sequence) 포함 &amp;rarr; 인덱스 없으면 느림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1916&quot; data-start=&quot;1904&quot; data-ke-size=&quot;size26&quot;&gt;  개선 포인트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스 후보&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1756703753181&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- Counselors 테이블
CREATE INDEX idx_c_status_user_sequence ON Counselors(status, user_id, sequence);

-- UserBlocks 테이블
CREATE INDEX idx_ub_reporter_user_deleted ON UserBlocks(reporter_id, user_id, is_deleted);

-- Reviews, ChatRooms
CREATE INDEX idx_reviews_counselor ON Reviews(counselor_id);
CREATE INDEX idx_chatrooms_counselor ON ChatRooms(counselor_id);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;쿼리 구조 개선&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서브쿼리 &amp;rarr; JOIN + GROUP BY 변환 가능&lt;/li&gt;
&lt;li&gt;문자열 비교 &amp;rarr; 정규화된 숫자/시간 컬럼 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;2194&quot; data-start=&quot;2191&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2201&quot; data-start=&quot;2196&quot; data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2373&quot; data-start=&quot;2203&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2243&quot; data-start=&quot;2203&quot;&gt;slow_query_log 자체를 그대로 보는 건 의미가 없다.&lt;/li&gt;
&lt;li data-end=&quot;2282&quot; data-start=&quot;2244&quot;&gt;&lt;b&gt;쿼리 패턴별로 묶어서 집계&lt;/b&gt;해야 진짜 문제를 알 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;2334&quot; data-start=&quot;2283&quot;&gt;pt-query-digest를 활용하면 수만 건 로그도 단 몇 초 만에 정리 가능하다.&lt;/li&gt;
&lt;li data-end=&quot;2373&quot; data-start=&quot;2335&quot;&gt;상위 몇 개의 문제 쿼리만 개선해도 전체 성능이 크게 개선된다.&lt;/li&gt;
&lt;li data-end=&quot;2150&quot; data-start=&quot;2097&quot;&gt;이 결과는 &quot;분석한 느린 쿼리 중 단 하나의 쿼리 패턴이 원인&quot;임을 알 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;2219&quot; data-start=&quot;2151&quot;&gt;문제는 200여 건의 결과를 위해 매번 &lt;b&gt;80만 row를 스캔&lt;/b&gt;하는 구조 &amp;rarr; 인덱스 최적화 및 쿼리 리팩토링이 필요하다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/DB</category>
      <category>AWS</category>
      <category>index</category>
      <category>MySQL</category>
      <category>pt-query-digest</category>
      <category>Query</category>
      <category>RDS</category>
      <category>Slow query</category>
      <category>SlowQuery</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/12</guid>
      <comments>https://smink.tistory.com/entry/AWS-RDS-pt-query-digest%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-slow-query-%EB%B6%84%EC%84%9D#entry12comment</comments>
      <pubDate>Mon, 1 Sep 2025 14:30:00 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot에서 Caffeine Cache 적용과 캐시 관리 API 구현하기</title>
      <link>https://smink.tistory.com/entry/Spring-Boot%EC%97%90%EC%84%9C-Caffeine-Cache-%EC%A0%81%EC%9A%A9%EA%B3%BC-%EC%BA%90%EC%8B%9C-%EA%B4%80%EB%A6%AC-API-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-end=&quot;145&quot; data-start=&quot;135&quot; data-ke-size=&quot;size26&quot;&gt;  문제 상황&lt;/h2&gt;
&lt;p data-end=&quot;222&quot; data-start=&quot;146&quot; data-ke-size=&quot;size16&quot;&gt;우리 서비스에서는 메인 배너, 서브 배너, 카테고리 등과 같이 &lt;b&gt;자주 변하지 않는 데이터를 계속해서 조회하는 문제&lt;/b&gt;가 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;270&quot; data-start=&quot;224&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, 메인 페이지에 들어올 때마다 DB에서 배너 정보를 매번 조회한다면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;326&quot; data-start=&quot;271&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;292&quot; data-start=&quot;271&quot;&gt;불필요하게 DB 부하가 커지고,&lt;/li&gt;
&lt;li data-end=&quot;326&quot; data-start=&quot;293&quot;&gt;트래픽이 많을 경우 성능 저하로 이어질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;393&quot; data-start=&quot;328&quot; data-ke-size=&quot;size16&quot;&gt;특히 배너나 카테고리처럼 &lt;b&gt;한 번 가져오면 크게 변하지 않는 데이터&lt;/b&gt;를 매번 불러오는 건 낭비에 가까웠습니다.&lt;/p&gt;
&lt;p data-end=&quot;393&quot; data-start=&quot;328&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1262&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/msGW6/btsP7dJbrty/K5c5kWZ6VWV7OYhHOkSwz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/msGW6/btsP7dJbrty/K5c5kWZ6VWV7OYhHOkSwz1/img.png&quot; data-alt=&quot;Spring Boot + Caffeine Cache&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/msGW6/btsP7dJbrty/K5c5kWZ6VWV7OYhHOkSwz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmsGW6%2FbtsP7dJbrty%2FK5c5kWZ6VWV7OYhHOkSwz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1262&quot; height=&quot;484&quot; data-origin-width=&quot;1262&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring Boot + Caffeine Cache&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-end=&quot;418&quot; data-start=&quot;395&quot; data-ke-size=&quot;size26&quot;&gt;  해결 방법: 스프링부트 캐시 도입&lt;/h2&gt;
&lt;p data-end=&quot;478&quot; data-start=&quot;419&quot; data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 선택한 방법은 &lt;b&gt;Spring Boot의 캐시(Cache)&lt;/b&gt; 기능이었습니다.&lt;/p&gt;
&lt;p data-end=&quot;549&quot; data-start=&quot;480&quot; data-ke-size=&quot;size16&quot;&gt;스프링 캐시는 단순히 @Cacheable, @CacheEvict 등의 어노테이션만으로 손쉽게 적용할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;658&quot; data-start=&quot;551&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;604&quot; data-start=&quot;551&quot;&gt;@Cacheable &amp;rarr; 캐시에 값이 없을 경우 메서드를 실행하고, 결과를 캐시에 저장&lt;/li&gt;
&lt;li data-end=&quot;658&quot; data-start=&quot;605&quot;&gt;@CacheEvict &amp;rarr; 특정 캐시를 비워서 새로운 값으로 다시 채울 수 있도록 만듦&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;744&quot; data-start=&quot;660&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 적용하면,&lt;br /&gt;한 번 조회된 데이터는 캐시에 보관되고, 같은 요청이 들어와도 DB를 다시 조회하지 않고 캐시된 값을 반환할 수 있게 됩니다.&lt;/p&gt;
&lt;h2 data-end=&quot;770&quot; data-start=&quot;746&quot; data-ke-size=&quot;size26&quot;&gt;  캐시 삭제 기능도 함께 구현한 이유&lt;/h2&gt;
&lt;p data-end=&quot;846&quot; data-start=&quot;771&quot; data-ke-size=&quot;size16&quot;&gt;처음엔 @Cacheable만 적용했을 때는 성능이 확실히 좋아졌습니다.&lt;br /&gt;하지만 문제는 &lt;b&gt;데이터가 변경되었을 때&lt;/b&gt;였습니다.&lt;/p&gt;
&lt;p data-end=&quot;916&quot; data-start=&quot;848&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 관리자가 메인 배너를 교체했는데,&lt;br /&gt;캐시에 기존 데이터가 남아있다면 유저는 새로운 배너를 볼 수 없습니다.&lt;/p&gt;
&lt;p data-end=&quot;955&quot; data-start=&quot;918&quot; data-ke-size=&quot;size16&quot;&gt;따라서, 필요할 때 캐시를 강제로 비워주는 기능이 필요했습니다.&lt;/p&gt;
&lt;p data-end=&quot;990&quot; data-start=&quot;957&quot; data-ke-size=&quot;size16&quot;&gt;이를 위해 &lt;b&gt;캐시 삭제 API&lt;/b&gt;를 따로 만들었습니다.&lt;/p&gt;
&lt;p data-end=&quot;765&quot; data-start=&quot;639&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;808&quot; data-start=&quot;782&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #0a0a0a; text-align: start;&quot;&gt;  &lt;/span&gt;코드 분석&lt;/h3&gt;
&lt;h3 data-end=&quot;808&quot; data-start=&quot;782&quot; data-ke-size=&quot;size23&quot;&gt;1. 캐시 설정 (CacheConfig)&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1756270914346&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();

        CaffeineCache mainBannersCache = new CaffeineCache(&quot;mainBanners&quot;,
                Caffeine.newBuilder()
                        .expireAfterWrite(5, TimeUnit.HOURS)
                        .maximumSize(5)
                        .build());

        CaffeineCache categoriesCache = new CaffeineCache(&quot;categories&quot;,
                Caffeine.newBuilder()
                        .expireAfterWrite(1, TimeUnit.DAYS)
                        .maximumSize(5)
                        .build());
                        
        CaffeineCache recommendCounselorsCache = new CaffeineCache(&quot;recommendCounselors&quot;,
                Caffeine.newBuilder()
                        .expireAfterWrite(60, TimeUnit.SECONDS) // 기본값 60s
                        .maximumSize(100) // 기본값 100
                        .build());

        // ... (다른 캐시 설정들 생략)

        cacheManager.setCaches(Arrays.asList(
                mainBannersCache,
                categoriesCache,
                recommendCounselorsCache,
                // ...
        ));

        return cacheManager;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1978&quot; data-start=&quot;1713&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1752&quot; data-start=&quot;1713&quot;&gt;@EnableCaching : Spring 캐시 기능 활성화&lt;/li&gt;
&lt;li data-end=&quot;1832&quot; data-start=&quot;1753&quot;&gt;CaffeineCache : 캐시 이름, 만료 시간(expireAfterWrite), 최대 크기(maximumSize) 지정&lt;/li&gt;
&lt;li data-end=&quot;1978&quot; data-start=&quot;1833&quot;&gt;예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1978&quot; data-start=&quot;1842&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1884&quot; data-start=&quot;1842&quot;&gt;mainBanners &amp;rarr; 5시간 동안 캐시 유지, 최대 5개 저장&lt;/li&gt;
&lt;li data-end=&quot;1928&quot; data-start=&quot;1887&quot;&gt;categories &amp;rarr; 하루 동안 캐시 유지, 최대 5개 저장&lt;/li&gt;
&lt;li data-end=&quot;1978&quot; data-start=&quot;1931&quot;&gt;recommendCounselors &amp;rarr; 60초 TTL, 최대 100개 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2030&quot; data-start=&quot;1980&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 캐시 특성에 맞게 &lt;b&gt;데이터 성격별로 캐시 만료 정책&lt;/b&gt;을 다르게 설정했습니다.&lt;/p&gt;
&lt;p data-end=&quot;2030&quot; data-start=&quot;1980&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;2056&quot; data-start=&quot;2037&quot; data-ke-size=&quot;size23&quot;&gt;2. 캐시 적용 (서비스 계층)&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1756271021569&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Cacheable(value = &quot;mainBanners&quot;)
public List&amp;lt;Banner&amp;gt; getMainBanners() {
    return bannerRepository.findMainBanners();
}

@Cacheable(value = &quot;categories&quot;)
public List&amp;lt;Category&amp;gt; getCategories() {
    return categoryRepository.findAll();
}

// ... 나머지 생략&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2405&quot; data-start=&quot;2309&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2372&quot; data-start=&quot;2309&quot;&gt;@Cacheable(&quot;캐시명&quot;) : 처음 호출 시 DB에서 가져와 캐시에 저장, 이후에는 캐시에서 반환&lt;/li&gt;
&lt;li data-end=&quot;2405&quot; data-start=&quot;2373&quot;&gt;변경 주기가 긴 데이터(배너, 카테고리 등)에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;2428&quot; data-start=&quot;2412&quot; data-ke-size=&quot;size23&quot;&gt;3. 캐시 삭제 기능&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1756271118049&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class CacheService {

    private final CacheManager cacheManager;

    public CacheService(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    // 특정 캐시 저장소의 모든 데이터를 삭제
    public void evictAllCacheValues(String cacheName) {
        Cache cache = cacheManager.getCache(cacheName);
        if (cache != null) {
            cache.clear();
        }
    }

    // 모든 캐시 저장소의 모든 데이터를 삭제
    public void evictAllCaches() {
        cacheManager.getCacheNames().stream()
                .forEach(cacheName -&amp;gt; evictAllCacheValues(cacheName));
    }

    // 모든 캐시 저장소 이름 조회
    public List&amp;lt;String&amp;gt; getAllCacheNames() {
        Collection&amp;lt;String&amp;gt; cacheNames = cacheManager.getCacheNames();
        return cacheNames.stream().collect(Collectors.toList());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2884&quot; data-start=&quot;2758&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2161&quot; data-start=&quot;2102&quot;&gt;cacheManager.getCache(cacheName) : 캐시 이름으로 특정 캐시 가져오기&lt;/li&gt;
&lt;li data-end=&quot;2195&quot; data-start=&quot;2162&quot;&gt;cache.clear() : 해당 캐시 전체 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2219&quot; data-start=&quot;2202&quot; data-ke-size=&quot;size26&quot;&gt;4. 캐시 삭제 API&lt;/h2&gt;
&lt;pre id=&quot;code_1756272257906&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/cache&quot;)
public class CacheController {

    @Autowired
    private CacheService cacheService;

    // 모든 캐시를 삭제하는 API
    @DeleteMapping(&quot;/evict-all&quot;)
    public String evictAllCaches() {
        cacheService.evictAllCaches();
        return &quot;All caches have been cleared.&quot;;
    }

    // 특정 캐시를 삭제하는 API
    @DeleteMapping(&quot;/evict/{cacheName}&quot;)
    public String evictSpecificCache(@PathVariable String cacheName) {
        cacheService.evictAllCacheValues(cacheName);
        return String.format(&quot;Cache '%s' has been cleared.&quot;, cacheName);
    }

    // 현재 존재하는 모든 캐시 이름 조회 API
    @GetMapping(&quot;/names&quot;)
    public List&amp;lt;String&amp;gt; getCacheNames() {
        return cacheService.getAllCacheNames();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2752&quot; data-start=&quot;2624&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2693&quot; data-start=&quot;2624&quot;&gt;@DeleteMapping(&quot;/evict/{cacheName}&quot;) : REST API를 통해 특정 캐시 삭제 가능&lt;/li&gt;
&lt;li data-end=&quot;2752&quot; data-start=&quot;2694&quot;&gt;사용 예: DELETE /cache/evict/mainBanner &amp;rarr; 메인 배너 캐시만 삭제됨&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2901&quot; data-start=&quot;2891&quot; data-ke-size=&quot;size26&quot;&gt;  결과 및 정리&lt;/h2&gt;
&lt;p data-end=&quot;2939&quot; data-start=&quot;2902&quot; data-ke-size=&quot;size16&quot;&gt;이번 캐시 적용을 통해 다음과 같은 효과를 얻을 수 있었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;3163&quot; data-start=&quot;2941&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2991&quot; data-start=&quot;2941&quot;&gt;&lt;b&gt;DB 부하 감소&lt;/b&gt; &amp;rarr; 잦은 동일 조회가 캐시로 대체되어 DB 호출 횟수 절감&lt;/li&gt;
&lt;li data-end=&quot;3044&quot; data-start=&quot;2992&quot;&gt;&lt;b&gt;응답 속도 향상&lt;/b&gt; &amp;rarr; 캐시에서 바로 데이터를 꺼내오기 때문에 조회 속도가 빨라짐&lt;/li&gt;
&lt;li data-end=&quot;3100&quot; data-start=&quot;3045&quot;&gt;&lt;b&gt;운영 편의성 확보&lt;/b&gt; &amp;rarr; 캐시 삭제 API를 통해 수정된 데이터가 즉시 반영될 수 있음&lt;/li&gt;
&lt;li data-end=&quot;3163&quot; data-start=&quot;3101&quot;&gt;&lt;b&gt;데이터 특성에 맞는 TTL 설정&lt;/b&gt; &amp;rarr; 배너/카테고리/상담사 추천 등 데이터 성격별 만료 정책 적용&lt;/li&gt;
&lt;li data-end=&quot;3163&quot; data-start=&quot;3101&quot;&gt;&lt;b&gt;캐시 + 캐시 무효화 전략을 함께 가져감&lt;/b&gt;으로써, 성능 최적화와 데이터 일관성 두 마리 토끼를 잡을 수 있었음&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>개발/SpringBoot</category>
      <category>cache</category>
      <category>CacheConfig</category>
      <category>Caffeine</category>
      <category>caffeinecache</category>
      <category>Java</category>
      <category>Spring</category>
      <category>SpringBoot</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/11</guid>
      <comments>https://smink.tistory.com/entry/Spring-Boot%EC%97%90%EC%84%9C-Caffeine-Cache-%EC%A0%81%EC%9A%A9%EA%B3%BC-%EC%BA%90%EC%8B%9C-%EA%B4%80%EB%A6%AC-API-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0#entry11comment</comments>
      <pubDate>Wed, 27 Aug 2025 14:32:49 +0900</pubDate>
    </item>
    <item>
      <title>Firebase Functions onWrite와 Too Many Connections 오류</title>
      <link>https://smink.tistory.com/entry/Firebase-Functions-onWrite%EC%99%80-Too-Many-Connections-%EC%98%A4%EB%A5%98</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ehYC7D/btsPAYZUVjV/vYKxUUdHeJMbQgMdfMoBM1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ehYC7D/btsPAYZUVjV/vYKxUUdHeJMbQgMdfMoBM1/img.webp&quot; data-alt=&quot;MySQL too many connections error&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ehYC7D/btsPAYZUVjV/vYKxUUdHeJMbQgMdfMoBM1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FehYC7D%2FbtsPAYZUVjV%2FvYKxUUdHeJMbQgMdfMoBM1%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;736&quot; height=&quot;288&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MySQL too many connections error&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firebase를 이용한 서비스에서 데이터베이스를 MySQL로 마이그레이션하는 과정은 효율적인 데이터 동기화가 핵심입니다. 특히 &lt;b&gt;Firebase Functions의 onWrite 트리거를 사용하여 Firestore 데이터를 MySQL로 실시간 동기화&lt;/b&gt;할 때, 대규모 데이터 업데이트는 예상치 못한 Too Many Connections 오류를 유발할 수 있었습니다. 본 글에서는 마이그레이션 과정에서 겪었던 이러한 문제 상황과 그 해결 과정을 공유합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 상황: 베스트 리뷰 선정과 대규모 onWrite 트리거&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 서비스에는 특정 리뷰를 '베스트 리뷰'로 설정하는 기능이 있습니다. 이 기능은 다음과 같은 로직으로 작동하도록 설계되었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;특정 리뷰를 베스트 리뷰로 설정&lt;/b&gt;하게되면&amp;nbsp;해당 리뷰의 &lt;b&gt;today_index가 0으로 설정&lt;/b&gt;됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기존 베스트 리뷰들의 today_index 증가&lt;/b&gt;: 이전에 베스트 리뷰로 선정되었던 리뷰들의 today_index 값이 1씩 증가합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 이 과정에서 &lt;b&gt;Firebase DB의 베스트 리뷰 관련 문서가 1,300개 이상 동시에 업데이트되었다는 점&lt;/b&gt;이었습니다. 저희는 Firebase Functions의 onWrite 트리거를 활용하여 Firestore의 모든 쓰기 작업을 감지하고 이를 MySQL 데이터베이스에 동기화하는 방식을 사용하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 1,300개가 넘는 문서가 동시에 업데이트되자, &lt;b&gt;1,300개 이상의 Firebase Functions 인스턴스가 동시에 실행되어 MySQL 데이터베이스에 연결을 시도&lt;/b&gt;하게 되었습니다. MySQL 서버는 동시에 수락할 수 있는 최대 연결 수(max_connections)가 제한되어 있는데, 이 한도를 초과하면서 &lt;b&gt;Error: Too many connections 오류&lt;/b&gt;가 빈번하게 발생하기 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 오류는 데이터 동기화 실패뿐만 아니라, 데이터베이스 서버에 과도한 부하를 주어 서비스 전반의 안정성에도 부정적인 영향을 미쳤습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 과정: Spring Server API를 통한 우회 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 저는 다음과 같은 전략을 적용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심&lt;/b&gt;: 베스트 리뷰 설정과 같이 &lt;b&gt;대규모 onWrite를 유발하는 특정 기능에 대해서는 Firebase DB를 직접 수정하는 방식 대신, Spring Server API를 통해 MySQL 데이터를 직접 업데이트하도록 변경&lt;/b&gt;하는 방법을 택했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Firebase Functions onWrite 우회&lt;/b&gt;: '베스트 리뷰' 설정 시 Firebase Firestore의 리뷰 문서를 직접 업데이트하지 않도록 로직을 수정했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring Server API 호출&lt;/b&gt;: 대신, 클라이언트나 백엔드에서 Spring Server API를 호출하도록 변경했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MySQL 직접 업데이트&lt;/b&gt;: Spring Server API는 MySQL 데이터베이스에 직접 접근하여, 해당 리뷰의 today_index를 0으로 설정하고, 기존 베스트 리뷰들의 today_index를 1씩 증가시키는 작업을 수행하도록 구현했습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이 방법이 효과적이었던 이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;커넥션 풀(Connection Pool) 활용&lt;/b&gt;: Spring Server는 일반적으로 JDBC 커넥션 풀(예: HikariCP)을 사용하여 데이터베이스 연결을 관리합니다. 커넥션 풀은 미리 정해진 수의 연결을 생성해두고 재사용하므로, 동시에 많은 요청이 들어와도 MySQL 서버에 대한 연결 수가 max_connections를 초과하지 않도록 효과적으로 제어할 수 있었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단일 작업으로 처리&lt;/b&gt;: Spring Server API 내부에서는 1,300개의 업데이트 작업을 &lt;b&gt;하나의 트랜잭션 또는 효율적인 배치 쿼리&lt;/b&gt;로 처리할 수 있었습니다. 이는 Firebase Functions에서 1,300번의 개별 연결 및 트랜잭션을 시도하는 것보다 훨씬 효율적이었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버리스 한계 극복&lt;/b&gt;: Firebase Functions는 강력하지만, 짧은 시간에 폭발적인 동시 실행이 필요한 대규모 배치 작업에는 Too Many Connections와 같은 리소스 한계에 직면할 수 있습니다. Spring Server와 같은 전통적인 백엔드 서버는 이러한 시나리오에 더 적합한 대안이 될 수 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firebase Functions의 onWrite 트리거는 실시간 데이터 동기화에 매우 유용하지만, 대규모 데이터 업데이트가 빈번하게 발생하는 특정 기능에 대해서는 Too Many Connections와 같은 문제를 야기할 수 있다는 점을 경험했습니다. 이러한 경우에는 &lt;b&gt;애플리케이션의 특정 로직을 백엔드 서버(예: Spring Server)로 분리하고, 해당 서버에서 데이터베이스 커넥션 풀과 효율적인 쿼리 방식을 활용하여 MySQL을 직접 업데이트하는 전략&lt;/b&gt;이 효과적인 해결책이 될 수 있음을 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정은 시스템의 안정성을 확보하고, 각 기술 스택의 장점을 최대한 활용하는 중요한 경험이 되었습니다.&lt;/p&gt;</description>
      <category>개발/회고</category>
      <category>ConnectionPool</category>
      <category>connections</category>
      <category>firebase</category>
      <category>firebasefunctions</category>
      <category>Firestore</category>
      <category>MySQL</category>
      <category>onwrite</category>
      <category>Spring</category>
      <category>SpringBoot</category>
      <category>toomanyconnections</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/10</guid>
      <comments>https://smink.tistory.com/entry/Firebase-Functions-onWrite%EC%99%80-Too-Many-Connections-%EC%98%A4%EB%A5%98#entry10comment</comments>
      <pubDate>Mon, 28 Jul 2025 16:57:26 +0900</pubDate>
    </item>
    <item>
      <title>AWS EC2 인스턴스 용량 확장 및 추가 Swap Memory 설정</title>
      <link>https://smink.tistory.com/entry/AWS-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%9A%A9%EB%9F%89-%ED%99%95%EC%9E%A5-%EB%B0%8F-%EC%B6%94%EA%B0%80-Swap-Memory-%EC%84%A4%EC%A0%95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;제가 운영 중인 AWS EC2 인스턴스는 메모리와 디스크 상황이 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1753144997928&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;df -h
/dev/root        30G   25G  4.8G  84% /

free -h
              total        used        free      shared  buff/cache   available
Mem:          3.8Gi       3.0Gi       127Mi       9.0Mi       705Mi       557Mi
Swap:         2.0Gi       2.0Gi        40Mi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;175&quot; data-start=&quot;132&quot;&gt;디스크 사용량: 30GB 중 25GB 사용 (84%) &amp;rarr; 여유가 크진 않음&lt;/li&gt;
&lt;li data-end=&quot;224&quot; data-start=&quot;176&quot;&gt;메모리: 3.8GiB 중 3.0GiB 사용, swap 2GiB 거의 다 사용 중&lt;/li&gt;
&lt;li data-end=&quot;254&quot; data-start=&quot;225&quot;&gt;사용 가능한 메모리: 557Mi 정도로 매우 낮은 상황&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이런 상황에서 안정적인 서비스 운영을 위해서:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;686&quot; data-start=&quot;565&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;618&quot; data-start=&quot;565&quot;&gt;EC2 인스턴스의 EBS 볼륨 용량을 30GB &amp;rarr; 100GB로 늘려 디스크 공간을 확보하고,&lt;/li&gt;
&lt;li data-end=&quot;686&quot; data-start=&quot;619&quot;&gt;OOM(Out of Memory) 방지를 위해 기존 스왑 메모리 외에 추가적인 스왑 메모리를 설정하기로 결정했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;364&quot; data-start=&quot;333&quot; data-ke-size=&quot;size26&quot;&gt;1️⃣ EC2 EBS Volume 크기 확장하기&lt;/h2&gt;
&lt;p data-end=&quot;464&quot; data-start=&quot;366&quot; data-ke-size=&quot;size16&quot;&gt;AWS EC2 인스턴스를 사용하다 보면 디스크 용량 부족 문제가 자주 발생합니다.&lt;br /&gt;다행히 AWS에서는 &lt;b&gt;EC2 중단 없이&lt;/b&gt; 디스크(EBS 볼륨) 크기를 늘릴 수 있어요!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-end=&quot;495&quot; data-start=&quot;471&quot; data-ke-size=&quot;size23&quot;&gt;✅ 1단계 : 현재 디스크 상태 확인&lt;/h3&gt;
&lt;p data-end=&quot;536&quot; data-start=&quot;496&quot; data-ke-size=&quot;size16&quot;&gt;SSH로 EC2에 접속해서 다음 명령어로 현재 디스크 정보를 확인합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1753145258223&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;df -h&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;303&quot; data-origin-height=&quot;61&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8cRgb/btsPsqosAkL/55UqM05xDXonKXiIjOQcD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8cRgb/btsPsqosAkL/55UqM05xDXonKXiIjOQcD1/img.png&quot; data-alt=&quot;df -h&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8cRgb/btsPsqosAkL/55UqM05xDXonKXiIjOQcD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8cRgb%2FbtsPsqosAkL%2F55UqM05xDXonKXiIjOQcD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;303&quot; height=&quot;61&quot; data-origin-width=&quot;303&quot; data-origin-height=&quot;61&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;df -h&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;612&quot; data-start=&quot;557&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;612&quot; data-start=&quot;557&quot;&gt;/dev/root 또는 /dev/xvda1 같이 표시되는 루트 디스크가 주로 대상입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;617&quot; data-start=&quot;614&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;652&quot; data-start=&quot;619&quot; data-ke-size=&quot;size23&quot;&gt;✅ 2단계 : AWS 콘솔에서 EBS 볼륨 크기 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ AWS Management Console 접속&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2️⃣ EC2 &amp;gt; Elastic Block Store &amp;gt; Volumes 메뉴 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-22 오전 9.55.48.png&quot; data-origin-width=&quot;452&quot; data-origin-height=&quot;1264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsSWcr/btsPsdbBnUe/SF303YJgjKSQ0fYp40cIK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsSWcr/btsPsdbBnUe/SF303YJgjKSQ0fYp40cIK1/img.png&quot; data-alt=&quot;EC2_EBS_volumn&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsSWcr/btsPsdbBnUe/SF303YJgjKSQ0fYp40cIK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsSWcr%2FbtsPsdbBnUe%2FSF303YJgjKSQ0fYp40cIK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;142&quot; height=&quot;397&quot; data-filename=&quot;스크린샷 2025-07-22 오전 9.55.48.png&quot; data-origin-width=&quot;452&quot; data-origin-height=&quot;1264&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;EC2_EBS_volumn&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3️⃣ 대상 볼륨 선택 (인스턴스와 연결된 볼륨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4️⃣ 볼륨 &amp;gt; 우측 상단 수정 버튼 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-22 오전 10.00.59.png&quot; data-origin-width=&quot;2462&quot; data-origin-height=&quot;998&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzvXX2/btsPtGwPWP4/5aVs8pSIC8q3A0E8Y6jiIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzvXX2/btsPtGwPWP4/5aVs8pSIC8q3A0E8Y6jiIK/img.png&quot; data-alt=&quot;볼륨&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzvXX2/btsPtGwPWP4/5aVs8pSIC8q3A0E8Y6jiIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzvXX2%2FbtsPtGwPWP4%2F5aVs8pSIC8q3A0E8Y6jiIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2462&quot; height=&quot;998&quot; data-filename=&quot;스크린샷 2025-07-22 오전 10.00.59.png&quot; data-origin-width=&quot;2462&quot; data-origin-height=&quot;998&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;볼륨&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;5️⃣ 새로운 크기 입력 (예: 기존 30GiB &amp;rarr; 100GiB)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-22 오전 10.05.16.png&quot; data-origin-width=&quot;3002&quot; data-origin-height=&quot;1028&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUcXCi/btsPthxiEte/mmYENUral7lVyJvV7lvz40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUcXCi/btsPthxiEte/mmYENUral7lVyJvV7lvz40/img.png&quot; data-alt=&quot;볼륨 수정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUcXCi/btsPthxiEte/mmYENUral7lVyJvV7lvz40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUcXCi%2FbtsPthxiEte%2FmmYENUral7lVyJvV7lvz40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3002&quot; height=&quot;1028&quot; data-filename=&quot;스크린샷 2025-07-22 오전 10.05.16.png&quot; data-origin-width=&quot;3002&quot; data-origin-height=&quot;1028&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;볼륨 수정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 크기로 수정 후 오른쪽 하단 '수정' 버튼 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-22 오전 10.07.44.png&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFHajZ/btsPsVO08Jx/0DVM2ucJz476B9qyjqSWX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFHajZ/btsPsVO08Jx/0DVM2ucJz476B9qyjqSWX0/img.png&quot; data-alt=&quot;볼륨을 수정하시겠습니까?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFHajZ/btsPsVO08Jx/0DVM2ucJz476B9qyjqSWX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFHajZ%2FbtsPsVO08Jx%2F0DVM2ucJz476B9qyjqSWX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;323&quot; height=&quot;187&quot; data-filename=&quot;스크린샷 2025-07-22 오전 10.07.44.png&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;볼륨을 수정하시겠습니까?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-end=&quot;859&quot; data-start=&quot;833&quot; data-ke-style=&quot;style2&quot;&gt;이 작업은 인스턴스 중단 없이 즉시 적용됩니다.&lt;br /&gt;일정 시간 이후 볼륨이 수정된 것을 확인할 수 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;900&quot; data-start=&quot;866&quot; data-ke-size=&quot;size23&quot;&gt;✅ 3단계 : EC2 인스턴스 안에서 파일 시스템 확장&lt;/h3&gt;
&lt;p data-end=&quot;957&quot; data-start=&quot;901&quot; data-ke-size=&quot;size16&quot;&gt;AWS 콘솔에서 크기를 늘리면, EC2 OS에서는 파일 시스템 확장을 해줘야 실제 사용이 가능합니다.&lt;/p&gt;
&lt;p data-end=&quot;983&quot; data-start=&quot;959&quot; data-ke-size=&quot;size16&quot;&gt;파일 시스템 종류에 따라 다르게 진행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;983&quot; data-start=&quot;959&quot;&gt;df -T 명령어 또는 lsblk -f 명령어를 사용하여 파일 시스템 타입을 바로 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;561&quot; data-origin-height=&quot;59&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cH0TMO/btsPrP26MsO/8GcXwbHTPICPSUDgcCzzd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cH0TMO/btsPrP26MsO/8GcXwbHTPICPSUDgcCzzd0/img.png&quot; data-alt=&quot;df -T를 사용한 파일 시스템 타입 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cH0TMO/btsPrP26MsO/8GcXwbHTPICPSUDgcCzzd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcH0TMO%2FbtsPrP26MsO%2F8GcXwbHTPICPSUDgcCzzd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;561&quot; height=&quot;59&quot; data-origin-width=&quot;561&quot; data-origin-height=&quot;59&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;df -T를 사용한 파일 시스템 타입 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1009&quot; data-start=&quot;990&quot; data-ke-size=&quot;size20&quot;&gt;ext4 파일 시스템&lt;/h4&gt;
&lt;pre id=&quot;code_1753148862425&quot; class=&quot;awk&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# growpart를 이용해 파티션 확장
sudo growpart /dev/xvda 1

# 파일 시스템 확장
sudo resize2fs /dev/xvda1&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot; data-start=&quot;1116&quot; data-end=&quot;1134&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot; data-start=&quot;1116&quot; data-end=&quot;1134&quot;&gt;xfs 파일 시스템&lt;/h4&gt;
&lt;pre id=&quot;code_1753148871262&quot; class=&quot;nginx&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo xfs_growfs /&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;대부분 Amazon Linux 2나 최근 Ubuntu에서는 /가 /dev/xvda1 또는 /dev/nvme0n1p1로 잡혀있습니다.&lt;br /&gt;정확한 디바이스 이름은 lsblk로 확인 가능&lt;/blockquote&gt;
&lt;hr data-end=&quot;1303&quot; data-start=&quot;1300&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1322&quot; data-start=&quot;1305&quot; data-ke-size=&quot;size23&quot;&gt;✅ 4단계 : 최종 확인&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1753147993765&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;df -h&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;407&quot; data-origin-height=&quot;61&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0ixtw/btsProxWome/Lg1a3D3Jz7MAyAbgH7Gykk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0ixtw/btsProxWome/Lg1a3D3Jz7MAyAbgH7Gykk/img.png&quot; data-alt=&quot;df -h 최종 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0ixtw/btsProxWome/Lg1a3D3Jz7MAyAbgH7Gykk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0ixtw%2FbtsProxWome%2FLg1a3D3Jz7MAyAbgH7Gykk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;407&quot; height=&quot;61&quot; data-origin-width=&quot;407&quot; data-origin-height=&quot;61&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;df -h 최종 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1371&quot; data-start=&quot;1341&quot; data-ke-size=&quot;size16&quot;&gt;확장된 디스크 용량이 정상적으로 반영됐는지 확인합니다.&lt;/p&gt;
&lt;p data-end=&quot;1371&quot; data-start=&quot;1341&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1371&quot; data-start=&quot;1341&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1371&quot; data-start=&quot;1341&quot; data-ke-size=&quot;size26&quot;&gt;2️⃣ 추가 스왑 메모리(Swap Memory) 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 EC2에 스왑 메모리가 설정되어 있지만,&lt;br /&gt;메모리가 부족하다면 &lt;b&gt;추가적인 스왑 파일&lt;/b&gt;을 설정해서 Out of Memory(OOM) 상황을 방지할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-end=&quot;1531&quot; data-start=&quot;1511&quot; data-ke-size=&quot;size23&quot;&gt;✅ 1단계 : 기존 스왑 확인&lt;/h3&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;pre id=&quot;code_1753148240419&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;free -h

# 또는

swapon --show

# 기존에 설정된 스왑 파일이 보입니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;79&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cg11o/btsPst6jXfo/W7IYmAGDe0o2MkZtx59pQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cg11o/btsPst6jXfo/W7IYmAGDe0o2MkZtx59pQK/img.png&quot; data-alt=&quot;free -h&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cg11o/btsPst6jXfo/W7IYmAGDe0o2MkZtx59pQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCg11o%2FbtsPst6jXfo%2FW7IYmAGDe0o2MkZtx59pQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;649&quot; height=&quot;79&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;79&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;free -h&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;1629&quot; data-start=&quot;1626&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1654&quot; data-start=&quot;1631&quot; data-ke-size=&quot;size23&quot;&gt;✅ 2단계 : 추가 스왑 파일 생성&lt;/h3&gt;
&lt;p data-end=&quot;1687&quot; data-start=&quot;1655&quot; data-ke-size=&quot;size16&quot;&gt;저는 4GiB 스왑 파일을 추가로 설정해 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1753148288817&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 스왑 파일 생성
sudo fallocate -l 4G /swapfile2

# 권한 설정
sudo chmod 600 /swapfile2

# 스왑 영역으로 설정
sudo mkswap /swapfile2

# 스왑 활성화
sudo swapon /swapfile2&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;1853&quot; data-start=&quot;1850&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1872&quot; data-start=&quot;1855&quot; data-ke-size=&quot;size23&quot;&gt;✅ 3단계 : 영구 적용&lt;/h3&gt;
&lt;p data-end=&quot;1916&quot; data-start=&quot;1873&quot; data-ke-size=&quot;size16&quot;&gt;인스턴스를 재부팅해도 자동으로 적용되도록 /etc/fstab에 추가합니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1753148378165&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;echo '/swapfile2 none swap sw 0 0' | sudo tee -a /etc/fstab&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;1994&quot; data-start=&quot;1991&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2013&quot; data-start=&quot;1996&quot; data-ke-size=&quot;size23&quot;&gt;✅ 4단계 : 최종 확인&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1753148392947&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;free -h&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2057&quot; data-start=&quot;2034&quot; data-ke-size=&quot;size16&quot;&gt;추가된 스왑 메모리가 합산되어 표시됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;79&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mhwb0/btsPs3e3Ls2/2NyzNJwXAgM6mpzBhypJtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mhwb0/btsPs3e3Ls2/2NyzNJwXAgM6mpzBhypJtK/img.png&quot; data-alt=&quot;추가 스왑파일 설정 후 free -h&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mhwb0/btsPs3e3Ls2/2NyzNJwXAgM6mpzBhypJtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMhwb0%2FbtsPs3e3Ls2%2F2NyzNJwXAgM6mpzBhypJtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;649&quot; height=&quot;79&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;79&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;추가 스왑파일 설정 후 free -h&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 현재 EC2 인스턴스의 메모리와 디스크 상황이 여유가 거의 없는 상태여서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;212&quot; data-start=&quot;115&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;150&quot; data-start=&quot;115&quot;&gt;&lt;b&gt;EBS 볼륨 용량을 늘려&lt;/b&gt; 디스크 공간을 확보하고,&lt;/li&gt;
&lt;li data-end=&quot;212&quot; data-start=&quot;151&quot;&gt;&lt;b&gt;추가 스왑 메모리&lt;/b&gt;를 설정해 OOM(Out of Memory) 상황을 예방하는 방법을 정리해봤습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;307&quot; data-start=&quot;214&quot; data-ke-size=&quot;size16&quot;&gt;EBS 볼륨 증설은 인스턴스 중단 없이 진행할 수 있어 편리하지만,&lt;br /&gt;스왑 메모리는 임시방편일 뿐, 과도하게 사용하면 성능 저하가 생길 수 있으니 주의가 필요합니다.&lt;/p&gt;
&lt;p data-end=&quot;370&quot; data-start=&quot;309&quot; data-ke-size=&quot;size16&quot;&gt;장기적으로는 &lt;b&gt;인스턴스 타입 업그레이드&lt;/b&gt;나 &lt;b&gt;메모리 최적화&lt;/b&gt; 등도 함께 검토하는 것이 좋겠습니다.&lt;/p&gt;</description>
      <category>개발/DevOps</category>
      <category>AWS</category>
      <category>DevOps</category>
      <category>EBS</category>
      <category>EC2</category>
      <category>ElasticBlockStore</category>
      <category>Infra</category>
      <category>swap</category>
      <category>swapfile</category>
      <category>swapmemory</category>
      <category>Volumn</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/9</guid>
      <comments>https://smink.tistory.com/entry/AWS-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%9A%A9%EB%9F%89-%ED%99%95%EC%9E%A5-%EB%B0%8F-%EC%B6%94%EA%B0%80-Swap-Memory-%EC%84%A4%EC%A0%95#entry9comment</comments>
      <pubDate>Tue, 22 Jul 2025 11:00:07 +0900</pubDate>
    </item>
    <item>
      <title>JMeter를 이용한 Spring Boot API 부하 테스트</title>
      <link>https://smink.tistory.com/entry/JMeter%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-Spring-Boot-API-%EB%B6%80%ED%95%98-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
      <description>&lt;h3 data-end=&quot;217&quot; data-start=&quot;200&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;1️⃣ &lt;/span&gt;&lt;span&gt;JMeter &lt;/span&gt;&lt;span&gt;설치&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;511&quot; data-origin-height=&quot;511&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DReZd/btsNTyouUMT/wDwMtiBETYZqRMAFyqbkB1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DReZd/btsNTyouUMT/wDwMtiBETYZqRMAFyqbkB1/img.jpg&quot; data-alt=&quot;JMeter&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DReZd/btsNTyouUMT/wDwMtiBETYZqRMAFyqbkB1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDReZd%2FbtsNTyouUMT%2FwDwMtiBETYZqRMAFyqbkB1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;241&quot; height=&quot;241&quot; data-origin-width=&quot;511&quot; data-origin-height=&quot;511&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JMeter&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;373&quot; data-start=&quot;219&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;305&quot; data-start=&quot;219&quot;&gt;&lt;b&gt;&lt;span&gt;JMeter &lt;/span&gt;&lt;span&gt;최신 &lt;/span&gt;&lt;span&gt;버전&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;다운로드&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;305&quot; data-start=&quot;219&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://jmeter.apache.org/download_jmeter.cgi&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jmeter.apache.org/download_jmeter.cgi&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1747097698251&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Apache JMeter
          -
          Download Apache JMeter&quot; data-og-description=&quot;Download Apache JMeter We recommend you use a mirror to download our release builds, but you must verify the integrity of the downloaded files using signatures downloaded from our main distribution directories. Recent releases (48 hours) may not yet be ava&quot; data-og-host=&quot;jmeter.apache.org&quot; data-og-source-url=&quot;https://jmeter.apache.org/download_jmeter.cgi&quot; data-og-url=&quot;https://jmeter.apache.org/download_jmeter.cgi&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://jmeter.apache.org/download_jmeter.cgi&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jmeter.apache.org/download_jmeter.cgi&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Apache JMeter - Download Apache JMeter&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Download Apache JMeter We recommend you use a mirror to download our release builds, but you must verify the integrity of the downloaded files using signatures downloaded from our main distribution directories. Recent releases (48 hours) may not yet be ava&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jmeter.apache.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;373&quot; data-start=&quot;219&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;373&quot; data-start=&quot;306&quot;&gt;&lt;span&gt;압축 &lt;/span&gt;&lt;span&gt;해제 &lt;/span&gt;&lt;span&gt;후 &lt;/span&gt;bin/jmeter.bat&lt;span&gt; (&lt;/span&gt;&lt;span&gt;Windows) &lt;/span&gt;&lt;span&gt;또는 &lt;/span&gt;bin/jmeter&lt;span&gt; (&lt;/span&gt;&lt;span&gt;Mac/&lt;/span&gt;&lt;span&gt;Linux) &lt;/span&gt;&lt;span&gt;실행&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;744&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDccyv/btsNTyIviyW/2PjYYoRKg1FKW8ikk1Dgb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDccyv/btsNTyIviyW/2PjYYoRKg1FKW8ikk1Dgb1/img.png&quot; data-alt=&quot;JMeter 실행 실패시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDccyv/btsNTyIviyW/2PjYYoRKg1FKW8ikk1Dgb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDccyv%2FbtsNTyIviyW%2F2PjYYoRKg1FKW8ikk1Dgb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;338&quot; height=&quot;313&quot; data-origin-width=&quot;744&quot; data-origin-height=&quot;688&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JMeter 실행 실패시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 data-end=&quot;284&quot; data-start=&quot;251&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;✅ &lt;/span&gt;&lt;span&gt;해결 &lt;/span&gt;&lt;span&gt;방법 (&lt;/span&gt;&lt;span&gt;macOS&lt;/span&gt;&lt;span&gt;에서 &lt;/span&gt;&lt;span&gt;JMeter &lt;/span&gt;&lt;span&gt;실행 &lt;/span&gt;&lt;span&gt;허용)&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-end=&quot;307&quot; data-start=&quot;286&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;1️⃣ &lt;/span&gt;&lt;span&gt;보안 &lt;/span&gt;&lt;span&gt;설정에서 &lt;/span&gt;&lt;span&gt;수동 &lt;/span&gt;&lt;span&gt;허용&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;547&quot; data-start=&quot;308&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;359&quot; data-start=&quot;308&quot;&gt;&lt;span&gt;JMeter &lt;/span&gt;&lt;span&gt;실행을 &lt;/span&gt;&lt;span&gt;시도한 &lt;/span&gt;&lt;span&gt;후, &lt;/span&gt;&lt;span&gt;위와 &lt;/span&gt;&lt;span&gt;같은 &lt;/span&gt;&lt;span&gt;경고가 &lt;/span&gt;&lt;span&gt;뜨면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;취소&quot;&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;를 &lt;/span&gt;&lt;span&gt;클릭합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;448&quot; data-start=&quot;360&quot;&gt;&lt;span&gt;상단 &lt;/span&gt;&lt;span&gt;메뉴에서 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Apple &lt;/span&gt;&lt;span&gt;로고 &amp;gt; &lt;/span&gt;&lt;span&gt;시스템 &lt;/span&gt;&lt;span&gt;설정(&lt;/span&gt;&lt;span&gt;또는 &lt;/span&gt;&lt;span&gt;환경설정)&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;b&gt;&lt;span&gt;개인정보 &lt;/span&gt;&lt;span&gt;보호 &lt;/span&gt;&lt;span&gt;및 &lt;/span&gt;&lt;span&gt;보안&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;b&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;앱 &lt;/span&gt;&lt;span&gt;실행 &lt;/span&gt;&lt;span&gt;허용&quot;&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;섹션으로 &lt;/span&gt;&lt;span&gt;이동합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;509&quot; data-start=&quot;449&quot;&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;jmeter &lt;/span&gt;&lt;span&gt;was &lt;/span&gt;&lt;span&gt;blocked&amp;hellip;&quot; &lt;/span&gt;&lt;span&gt;경고 &lt;/span&gt;&lt;span&gt;메시지가 &lt;/span&gt;&lt;span&gt;보이면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;그래도 &lt;/span&gt;&lt;span&gt;열기&quot;&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;버튼을 &lt;/span&gt;&lt;span&gt;클릭합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;547&quot; data-start=&quot;510&quot;&gt;&lt;span&gt;다시 &lt;/span&gt;&lt;span&gt;실행하면 &lt;/span&gt;&lt;span&gt;한 &lt;/span&gt;&lt;span&gt;번만 &lt;/span&gt;&lt;span&gt;확인 &lt;/span&gt;&lt;span&gt;창이 &lt;/span&gt;&lt;span&gt;나오고 &lt;/span&gt;&lt;span&gt;이후로는 &lt;/span&gt;&lt;span&gt;실행됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;552&quot; data-start=&quot;549&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;584&quot; data-start=&quot;554&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;2️⃣ &lt;/span&gt;&lt;span&gt;터미널 &lt;/span&gt;&lt;span&gt;명령어로 &lt;/span&gt;&lt;span&gt;강제 &lt;/span&gt;&lt;span&gt;허용 (&lt;/span&gt;&lt;span&gt;보안 &lt;/span&gt;&lt;span&gt;해제)&lt;/span&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;bash&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;복사&lt;span data-state=&quot;closed&quot;&gt;편집&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;span&gt;xattr -d com.apple.quarantine /path/to/jmeter/bin/jmeter &lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;711&quot; data-start=&quot;656&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 &lt;/span&gt;&lt;span&gt;들어, &lt;/span&gt;&lt;span&gt;JMeter&lt;/span&gt;&lt;span&gt;가 &lt;/span&gt;~/Downloads/apache-jmeter-5.6.3/&lt;span&gt;에 &lt;/span&gt;&lt;span&gt;있다면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;bash&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;복사&lt;span data-state=&quot;closed&quot;&gt;편집&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;span&gt;xattr -d com.apple.quarantine ~/Downloads/apache-jmeter-5.6.3/bin/jmeter &lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;809&quot; data-start=&quot;799&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그 &lt;/span&gt;&lt;span&gt;후 &lt;/span&gt;&lt;span&gt;다시 &lt;/span&gt;&lt;span&gt;실행:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;bash&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;복사&lt;span data-state=&quot;closed&quot;&gt;편집&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;cd&lt;/span&gt;&lt;/span&gt;&lt;span&gt; ~/Downloads/apache-jmeter-5.6.3/bin ./jmeter &lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;875&quot; data-start=&quot;872&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;885&quot; data-start=&quot;877&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;참고&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1008&quot; data-start=&quot;887&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;944&quot; data-start=&quot;887&quot;&gt;&lt;span&gt;위 &lt;/span&gt;&lt;span&gt;방법은 &lt;/span&gt;&lt;b&gt;&lt;span&gt;공식 &lt;/span&gt;&lt;span&gt;JMeter&lt;/span&gt;&lt;span&gt;를 &lt;/span&gt;&lt;span&gt;apache.&lt;/span&gt;&lt;span&gt;org&lt;/span&gt;&lt;span&gt;에서 &lt;/span&gt;&lt;span&gt;직접 &lt;/span&gt;&lt;span&gt;다운로드했을 &lt;/span&gt;&lt;span&gt;경우에만&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;안전합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1008&quot; data-start=&quot;945&quot;&gt;&lt;span&gt;GitHub &lt;/span&gt;&lt;span&gt;등에서 &lt;/span&gt;&lt;span&gt;받은 &lt;/span&gt;&lt;span&gt;미확인 &lt;/span&gt;&lt;span&gt;JMeter &lt;/span&gt;&lt;span&gt;패키지에는 &lt;/span&gt;&lt;span&gt;악성코드 &lt;/span&gt;&lt;span&gt;위험이 &lt;/span&gt;&lt;span&gt;있을 &lt;/span&gt;&lt;span&gt;수 &lt;/span&gt;&lt;span&gt;있으니 &lt;/span&gt;&lt;span&gt;출처 &lt;/span&gt;&lt;span&gt;확인 &lt;/span&gt;&lt;span&gt;필수입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;378&quot; data-start=&quot;375&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;403&quot; data-start=&quot;380&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;2️⃣ &lt;/span&gt;&lt;span&gt;테스트 &lt;/span&gt;&lt;span&gt;플랜 &lt;/span&gt;&lt;span&gt;구성 (&lt;/span&gt;&lt;span&gt;GUI)&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-end=&quot;430&quot; data-start=&quot;405&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1) &lt;/span&gt;&lt;b&gt;&lt;span&gt;Test &lt;/span&gt;&lt;span&gt;Plan &lt;/span&gt;&lt;span&gt;생성&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;495&quot; data-start=&quot;431&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;495&quot; data-start=&quot;431&quot;&gt;Test Plan&lt;span&gt; &amp;rarr; &lt;/span&gt;&lt;span&gt;우클릭 &amp;rarr; &lt;/span&gt;Add&lt;span&gt; &amp;rarr; &lt;/span&gt;Threads (Users)&lt;span&gt; &amp;rarr; &lt;/span&gt;Thread Group&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSZ2JV/btsNUCKeQBP/6chBfUPb7rSAiBwiZ7vHO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSZ2JV/btsNUCKeQBP/6chBfUPb7rSAiBwiZ7vHO0/img.png&quot; data-alt=&quot;Test Plan 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSZ2JV/btsNUCKeQBP/6chBfUPb7rSAiBwiZ7vHO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSZ2JV%2FbtsNUCKeQBP%2F6chBfUPb7rSAiBwiZ7vHO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;506&quot; height=&quot;328&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1564&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Test Plan 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-end=&quot;525&quot; data-start=&quot;497&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2) &lt;/span&gt;&lt;b&gt;&lt;span&gt;Thread &lt;/span&gt;&lt;span&gt;Group &lt;/span&gt;&lt;span&gt;설정&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;662&quot; data-start=&quot;526&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;569&quot; data-start=&quot;526&quot;&gt;&lt;b&gt;&lt;span&gt;Number &lt;/span&gt;&lt;span&gt;of &lt;/span&gt;&lt;span&gt;Threads (&lt;/span&gt;&lt;span&gt;users)&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 20&lt;/span&gt;&lt;span&gt;0 (&lt;/span&gt;&lt;span&gt;사용자 &lt;/span&gt;&lt;span&gt;수)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;626&quot; data-start=&quot;570&quot;&gt;&lt;b&gt;&lt;span&gt;Ramp-&lt;/span&gt;&lt;span&gt;Up &lt;/span&gt;&lt;span&gt;Period (&lt;/span&gt;&lt;span&gt;seconds)&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 5&lt;/span&gt;&lt;span&gt;&amp;nbsp;(&lt;/span&gt;&lt;span&gt;사용자 20&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;명을 5&lt;/span&gt;&lt;span&gt;초에 &lt;/span&gt;&lt;span&gt;걸쳐 &lt;/span&gt;&lt;span&gt;증가)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;662&quot; data-start=&quot;627&quot;&gt;&lt;b&gt;&lt;span&gt;Loop &lt;/span&gt;&lt;span&gt;Count&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 5&lt;/span&gt;&lt;span&gt;0 (&lt;/span&gt;&lt;span&gt;각 &lt;/span&gt;&lt;span&gt;사용자당 &lt;/span&gt;&lt;span&gt;요청 &lt;/span&gt;&lt;span&gt;횟수)&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6oP1y/btsNSLauoRc/mURupR6cx3R0ZZVKbKF5Ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6oP1y/btsNSLauoRc/mURupR6cx3R0ZZVKbKF5Ck/img.png&quot; data-alt=&quot;Thread&amp;amp;nbsp;Group&amp;amp;nbsp;설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6oP1y/btsNSLauoRc/mURupR6cx3R0ZZVKbKF5Ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6oP1y%2FbtsNSLauoRc%2FmURupR6cx3R0ZZVKbKF5Ck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;480&quot; height=&quot;311&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1564&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Thread&amp;nbsp;Group&amp;nbsp;설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-end=&quot;696&quot; data-start=&quot;664&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3) &lt;/span&gt;&lt;b&gt;&lt;span&gt;HTTP &lt;/span&gt;&lt;span&gt;Request &lt;/span&gt;&lt;span&gt;샘플러 &lt;/span&gt;&lt;span&gt;추가&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;925&quot; data-start=&quot;697&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;756&quot; data-start=&quot;697&quot;&gt;Thread Group&lt;span&gt; &amp;rarr; &lt;/span&gt;&lt;span&gt;우클릭 &amp;rarr; &lt;/span&gt;Add&lt;span&gt; &amp;rarr; &lt;/span&gt;Sampler&lt;span&gt; &amp;rarr; &lt;/span&gt;HTTP Request&lt;/li&gt;
&lt;li data-end=&quot;799&quot; data-start=&quot;757&quot;&gt;&lt;b&gt;&lt;span&gt;Method&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;GET&lt;span&gt; &lt;/span&gt;&lt;span&gt;또는 &lt;/span&gt;POST&lt;span&gt; (&lt;/span&gt;&lt;span&gt;API&lt;/span&gt;&lt;span&gt;에 &lt;/span&gt;&lt;span&gt;맞게 &lt;/span&gt;&lt;span&gt;선택)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;861&quot; data-start=&quot;800&quot;&gt;&lt;b&gt;&lt;span&gt;Server &lt;/span&gt;&lt;span&gt;Name &lt;/span&gt;&lt;span&gt;or &lt;/span&gt;&lt;span&gt;IP&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;api.example.com&lt;span&gt; (&lt;/span&gt;&lt;span&gt;또는 &lt;/span&gt;&lt;span&gt;EC2 &lt;/span&gt;&lt;span&gt;IP/&lt;/span&gt;&lt;span&gt;domain)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;888&quot; data-start=&quot;862&quot;&gt;&lt;b&gt;&lt;span&gt;Path&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;/api/posts&lt;span&gt; &lt;/span&gt;&lt;span&gt;등&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;925&quot; data-start=&quot;889&quot;&gt;&lt;b&gt;&lt;span&gt;Body &lt;/span&gt;&lt;span&gt;Data&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;일 &lt;/span&gt;&lt;span&gt;경우 &lt;/span&gt;&lt;span&gt;JSON &lt;/span&gt;&lt;span&gt;본문 &lt;/span&gt;&lt;span&gt;입력&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;925&quot; data-start=&quot;889&quot;&gt;&lt;span&gt;&lt;b&gt;Content encoding&lt;/b&gt; : application/json&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p73S5/btsNSLhgntm/I61T2LGuqkXcnrbJKBWPg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p73S5/btsNSLhgntm/I61T2LGuqkXcnrbJKBWPg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p73S5/btsNSLhgntm/I61T2LGuqkXcnrbJKBWPg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp73S5%2FbtsNSLhgntm%2FI61T2LGuqkXcnrbJKBWPg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;525&quot; height=&quot;340&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1564&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-end=&quot;951&quot; data-start=&quot;927&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;4) &lt;/span&gt;&lt;b&gt;&lt;span&gt;헤더 &lt;/span&gt;&lt;span&gt;추가 (&lt;/span&gt;&lt;span&gt;필요시)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1100&quot; data-start=&quot;952&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1100&quot; data-start=&quot;952&quot;&gt;Thread Group&lt;span&gt; &amp;rarr; &lt;/span&gt;&lt;span&gt;우클릭 &amp;rarr; &lt;/span&gt;Config Element&lt;span&gt; &amp;rarr; &lt;/span&gt;HTTP Header Manager&lt;/li&gt;
&lt;li data-end=&quot;1100&quot; data-start=&quot;1020&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;span&gt;Content-Type: application/json Authorization: Bearer &amp;lt;토큰&amp;gt; &lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;1142&quot; data-start=&quot;1102&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;5) &lt;/span&gt;&lt;b&gt;&lt;span&gt;View &lt;/span&gt;&lt;span&gt;Results &lt;/span&gt;&lt;span&gt;Tree &lt;/span&gt;&lt;span&gt;추가 (&lt;/span&gt;&lt;span&gt;디버깅용)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1200&quot; data-start=&quot;1143&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1200&quot; data-start=&quot;1143&quot;&gt;Thread Group&lt;span&gt; &amp;rarr; &lt;/span&gt;&lt;span&gt;우클릭 &amp;rarr; &lt;/span&gt;Listener&lt;span&gt; &amp;rarr; &lt;/span&gt;View Results Tree&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x3Gef/btsNUCKfLLn/22h1E74SRoLfVwxuat7Vx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x3Gef/btsNUCKfLLn/22h1E74SRoLfVwxuat7Vx1/img.png&quot; data-alt=&quot;View Results Tree&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x3Gef/btsNUCKfLLn/22h1E74SRoLfVwxuat7Vx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx3Gef%2FbtsNUCKfLLn%2F22h1E74SRoLfVwxuat7Vx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;483&quot; height=&quot;313&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1564&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;View Results Tree&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-end=&quot;1224&quot; data-start=&quot;1202&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;6) &lt;/span&gt;&lt;b&gt;&lt;span&gt;요약 &lt;/span&gt;&lt;span&gt;리포트 &lt;/span&gt;&lt;span&gt;추가&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1280&quot; data-start=&quot;1225&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1280&quot; data-start=&quot;1225&quot;&gt;Listener&lt;span&gt; &amp;rarr; &lt;/span&gt;Summary Report&lt;span&gt; / &lt;/span&gt;Aggregate Report&lt;span&gt; &lt;/span&gt;&lt;span&gt;추가&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzqUT/btsNVi5iASF/tTuJuiYvryYEqLDfa6gyd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzqUT/btsNVi5iASF/tTuJuiYvryYEqLDfa6gyd0/img.png&quot; data-alt=&quot;Summary Report &amp;amp;nbsp;/&amp;amp;nbsp; Aggregate Report &amp;amp;nbsp; 추가&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzqUT/btsNVi5iASF/tTuJuiYvryYEqLDfa6gyd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkzqUT%2FbtsNVi5iASF%2FtTuJuiYvryYEqLDfa6gyd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;571&quot; height=&quot;370&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1564&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Summary Report &amp;nbsp;/&amp;nbsp; Aggregate Report &amp;nbsp; 추가&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;1285&quot; data-start=&quot;1282&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1309&quot; data-start=&quot;1287&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;3️⃣ &lt;/span&gt;&lt;span&gt;테스트 &lt;/span&gt;&lt;span&gt;실행 &lt;/span&gt;&lt;span&gt;및 &lt;/span&gt;&lt;span&gt;결과 &lt;/span&gt;&lt;span&gt;확인&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1450&quot; data-start=&quot;1311&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1330&quot; data-start=&quot;1311&quot;&gt;&lt;span&gt;▶️ &lt;/span&gt;&lt;span&gt;버튼 &lt;/span&gt;&lt;span&gt;클릭 &amp;rarr; &lt;/span&gt;&lt;span&gt;테스트 &lt;/span&gt;&lt;span&gt;실행&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKM7wt/btsNS8QPI1f/LxVw41osRjDsq12obxO7ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKM7wt/btsNS8QPI1f/LxVw41osRjDsq12obxO7ek/img.png&quot; data-alt=&quot;▶️&amp;amp;nbsp;버튼&amp;amp;nbsp;클릭 &amp;amp;rarr;&amp;amp;nbsp;테스트&amp;amp;nbsp;실행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKM7wt/btsNS8QPI1f/LxVw41osRjDsq12obxO7ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKM7wt%2FbtsNS8QPI1f%2FLxVw41osRjDsq12obxO7ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;356&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1564&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;▶️&amp;nbsp;버튼&amp;nbsp;클릭 &amp;rarr;&amp;nbsp;테스트&amp;nbsp;실행&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1450&quot; data-start=&quot;1311&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1450&quot; data-start=&quot;1331&quot;&gt;&lt;span&gt;결과:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1450&quot; data-start=&quot;1339&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1363&quot; data-start=&quot;1339&quot;&gt;&lt;b&gt;&lt;span&gt;Throughput&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;초당 &lt;/span&gt;&lt;span&gt;처리량&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1389&quot; data-start=&quot;1366&quot;&gt;&lt;b&gt;&lt;span&gt;Average&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;평균 &lt;/span&gt;&lt;span&gt;응답 &lt;/span&gt;&lt;span&gt;시간&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1413&quot; data-start=&quot;1392&quot;&gt;&lt;b&gt;&lt;span&gt;Error %&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;오류 &lt;/span&gt;&lt;span&gt;발생률&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1450&quot; data-start=&quot;1416&quot;&gt;&lt;b&gt;&lt;span&gt;Latency / &lt;/span&gt;&lt;span&gt;Min / &lt;/span&gt;&lt;span&gt;Max&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;지연시간 &lt;/span&gt;&lt;span&gt;지표&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1450&quot; data-start=&quot;1416&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;90% &lt;/span&gt;&lt;span&gt;Line&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;사용자의 &lt;/span&gt;&lt;span&gt;90%&lt;/span&gt;&lt;span&gt;가 &lt;/span&gt;&lt;span&gt;느끼는 &lt;/span&gt;&lt;span&gt;응답속도, &lt;/span&gt;&lt;span&gt;SLA &lt;/span&gt;&lt;span&gt;기준으로 &lt;/span&gt;&lt;span&gt;자주 &lt;/span&gt;&lt;span&gt;사용됨&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;1455&quot; data-start=&quot;1452&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1483&quot; data-start=&quot;1457&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;4️⃣ &lt;/span&gt;&lt;span&gt;테스트 &lt;/span&gt;&lt;span&gt;파일 &lt;/span&gt;&lt;span&gt;저장 &lt;/span&gt;&lt;span&gt;및 &lt;/span&gt;&lt;span&gt;CLI &lt;/span&gt;&lt;span&gt;실행&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1541&quot; data-start=&quot;1485&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;GUI &lt;/span&gt;&lt;span&gt;설정이 &lt;/span&gt;&lt;span&gt;끝나면 &lt;/span&gt;.jmx&lt;span&gt; &lt;/span&gt;&lt;span&gt;파일로 &lt;/span&gt;&lt;span&gt;저장할 &lt;/span&gt;&lt;span&gt;수 &lt;/span&gt;&lt;span&gt;있고, &lt;/span&gt;&lt;span&gt;아래처럼 &lt;/span&gt;&lt;span&gt;CLI&lt;/span&gt;&lt;span&gt;에서도 &lt;/span&gt;&lt;span&gt;실행할 &lt;/span&gt;&lt;span&gt;수 &lt;/span&gt;&lt;span&gt;있습니다:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;&lt;span&gt;jmeter -n -t test-plan.jmx -l result.jtl -e -o report &lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1682&quot; data-start=&quot;1610&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1627&quot; data-start=&quot;1610&quot;&gt;-n&lt;span&gt;: &lt;/span&gt;&lt;span&gt;GUI &lt;/span&gt;&lt;span&gt;없이 &lt;/span&gt;&lt;span&gt;실행&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1645&quot; data-start=&quot;1628&quot;&gt;-t&lt;span&gt;: &lt;/span&gt;&lt;span&gt;테스트 &lt;/span&gt;&lt;span&gt;플랜 &lt;/span&gt;&lt;span&gt;파일&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1659&quot; data-start=&quot;1646&quot;&gt;-l&lt;span&gt;: &lt;/span&gt;&lt;span&gt;결과 &lt;/span&gt;&lt;span&gt;저장&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1682&quot; data-start=&quot;1660&quot;&gt;-e -o&lt;span&gt;: &lt;/span&gt;&lt;span&gt;HTML &lt;/span&gt;&lt;span&gt;리포트 &lt;/span&gt;&lt;span&gt;생성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1687&quot; data-start=&quot;1684&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1698&quot; data-start=&quot;1689&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;✅ &lt;/span&gt;&lt;span&gt;팁&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1923&quot; data-start=&quot;1700&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1762&quot; data-start=&quot;1700&quot;&gt;&lt;span&gt;테스트 &lt;/span&gt;&lt;span&gt;중 &lt;/span&gt;k3s&lt;span&gt;의 &lt;/span&gt;&lt;b&gt;kubectl top pods&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;명령어로 &lt;/span&gt;&lt;span&gt;CPU, &lt;/span&gt;&lt;span&gt;메모리 &lt;/span&gt;&lt;span&gt;리소스도 &lt;/span&gt;&lt;span&gt;함께 &lt;/span&gt;&lt;span&gt;관찰하세요.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1836&quot; data-start=&quot;1763&quot;&gt;&lt;span&gt;EC2&lt;/span&gt;&lt;span&gt;에서 &lt;/span&gt;&lt;span&gt;JMeter&lt;/span&gt;&lt;span&gt;를 &lt;/span&gt;&lt;span&gt;실행할 &lt;/span&gt;&lt;span&gt;경우, &lt;/span&gt;&lt;span&gt;애플리케이션 &lt;/span&gt;&lt;span&gt;서버와 &lt;/span&gt;&lt;span&gt;다른 &lt;/span&gt;&lt;span&gt;인스턴스를 &lt;/span&gt;&lt;span&gt;사용하는 &lt;/span&gt;&lt;span&gt;것이 &lt;/span&gt;&lt;span&gt;정확한 &lt;/span&gt;&lt;span&gt;측정을 &lt;/span&gt;&lt;span&gt;위한 &lt;/span&gt;&lt;span&gt;이상적 &lt;/span&gt;&lt;span&gt;환경입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1923&quot; data-start=&quot;1837&quot;&gt;&lt;span&gt;부하 &lt;/span&gt;&lt;span&gt;테스트 &lt;/span&gt;&lt;span&gt;결과와 &lt;/span&gt;&lt;span&gt;함께 &lt;/span&gt;&lt;b&gt;&lt;span&gt;GC &lt;/span&gt;&lt;span&gt;로그&lt;/span&gt;&lt;/b&gt;&lt;span&gt;, &lt;/span&gt;&lt;b&gt;&lt;span&gt;애플리케이션 &lt;/span&gt;&lt;span&gt;로그&lt;/span&gt;&lt;/b&gt;&lt;span&gt;, &lt;/span&gt;&lt;b&gt;&lt;span&gt;Spring &lt;/span&gt;&lt;span&gt;Boot &lt;/span&gt;&lt;span&gt;actuator &lt;/span&gt;&lt;span&gt;metrics&lt;/span&gt;&lt;/b&gt;&lt;span&gt;도 &lt;/span&gt;&lt;span&gt;확인하면 &lt;/span&gt;&lt;span&gt;좋습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/DevOps</category>
      <category>AWS</category>
      <category>CPU</category>
      <category>DevOps</category>
      <category>EC2</category>
      <category>Java</category>
      <category>jmeter</category>
      <category>loadtesting</category>
      <category>Server</category>
      <category>SpringBoot</category>
      <category>부하테스트</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/8</guid>
      <comments>https://smink.tistory.com/entry/JMeter%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-Spring-Boot-API-%EB%B6%80%ED%95%98-%ED%85%8C%EC%8A%A4%ED%8A%B8#entry8comment</comments>
      <pubDate>Tue, 13 May 2025 10:52:13 +0900</pubDate>
    </item>
    <item>
      <title>GitHub Actions를 이용한 Vue 프로젝트 Firebase Hosting 자동 배포</title>
      <link>https://smink.tistory.com/entry/GitHub-Actions%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-Vue-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Firebase-Hosting-%EC%9E%90%EB%8F%99-%EB%B0%B0%ED%8F%AC</link>
      <description>&lt;h2 data-end=&quot;236&quot; data-start=&quot;230&quot; data-ke-size=&quot;size26&quot;&gt;  GitHub Actions를 이용한 Vue 프로젝트 Firebase Hosting 자동 배포&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;firebase_hosting.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;465&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LPfAk/btsNJxvBgr5/2ObU7SuvO4ufS5PJUzUpq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LPfAk/btsNJxvBgr5/2ObU7SuvO4ufS5PJUzUpq0/img.png&quot; data-alt=&quot;firebase hosting&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LPfAk/btsNJxvBgr5/2ObU7SuvO4ufS5PJUzUpq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLPfAk%2FbtsNJxvBgr5%2F2ObU7SuvO4ufS5PJUzUpq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;530&quot; height=&quot;241&quot; data-filename=&quot;firebase_hosting.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;465&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;firebase hosting&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-end=&quot;236&quot; data-start=&quot;230&quot; data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-end=&quot;236&quot; data-start=&quot;230&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는 GitHub Actions를 활용하여 main 브랜치에 코드를 push하면 자동으로 Vue 프로젝트가 Firebase Hosting에 배포되도록 구성하는 방법을 설명합니다. CI/CD를 통해 반복적인 배포 작업을 자동화하고, 안정적인 배포 흐름을 구축할 수 있습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;236&quot; data-start=&quot;230&quot; data-ke-size=&quot;size26&quot;&gt;  workflow/firebase-hosting.yaml&lt;/h2&gt;
&lt;pre id=&quot;code_1746171178094&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Deploy to Firebase Hosting

on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

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

    - name: Install Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '21.5.0'  # 프로젝트 Node 버전에 맞게 수정

    - name: Install and Build admin app
      working-directory: firebase/admin
      run: |
        npm install
        npm run build

    - name: Write Firebase service account key
      run: |
        echo '${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_JSON }}' &amp;gt; ${HOME}/firebase-key.json
        echo &quot;GOOGLE_APPLICATION_CREDENTIALS=${HOME}/firebase-key.json&quot; &amp;gt;&amp;gt; $GITHUB_ENV

    - name: Deploy to Firebase Hosting (admin)
      working-directory: firebase
      run: |
        npm install -g firebase-tools
        firebase deploy --only hosting:admin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;420&quot; data-start=&quot;402&quot; data-ke-size=&quot;size26&quot;&gt;1. Workflow 개요&lt;/h2&gt;
&lt;pre id=&quot;code_1746171265237&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Deploy to Firebase Hosting

on:
  push:
    branches:
      - main&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;638&quot; data-start=&quot;508&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;557&quot; data-start=&quot;508&quot;&gt;&lt;b&gt;동작 조건&lt;/b&gt;: main 브랜치에 push가 발생하면 워크플로우가 실행됩니다.&lt;/li&gt;
&lt;li data-end=&quot;638&quot; data-start=&quot;558&quot;&gt;&lt;b&gt;목표&lt;/b&gt;: firebase/admin 디렉토리의 Vue 앱을 빌드하고, hosting:admin으로 Firebase에 배포합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;643&quot; data-start=&quot;640&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;669&quot; data-start=&quot;645&quot; data-ke-size=&quot;size26&quot;&gt;2. Job 정의 및 빌드 환경 설정&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1746171283293&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jobs:
  build-and-deploy:
    runs-on: ubuntu-latest&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;840&quot; data-start=&quot;737&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;788&quot; data-start=&quot;737&quot;&gt;GitHub Actions의 &lt;b&gt;Ubuntu 최신 버전&lt;/b&gt; 환경에서 Job이 실행됩니다.&lt;/li&gt;
&lt;li data-end=&quot;840&quot; data-start=&quot;789&quot;&gt;단일 Job(build-and-deploy) 내에서 모든 빌드 및 배포가 처리됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;845&quot; data-start=&quot;842&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;862&quot; data-start=&quot;847&quot; data-ke-size=&quot;size26&quot;&gt;3. Step별 설명&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-start=&quot;1875&quot; data-end=&quot;1930&quot;&gt;&lt;b&gt;&lt;br /&gt;  Github Secrets&lt;/b&gt;&lt;br /&gt;GitHub 저장소의 Secrets에 아래 항목을 추가해야 합니다:&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;1932&quot; data-end=&quot;2010&quot;&gt;
&lt;li data-start=&quot;1932&quot; data-end=&quot;2010&quot;&gt;&lt;b&gt;GOOGLE_APPLICATION_CREDENTIALS_JSON&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-start=&quot;1932&quot; data-end=&quot;2010&quot;&gt;Firebase 서비스 계정 JSON 전체를 문자열로 복사해 저장&lt;/li&gt;
&lt;li data-start=&quot;1932&quot; data-end=&quot;2010&quot;&gt;Firebase 콘솔 &amp;gt; 프로젝트 설정 &amp;gt; 서비스 계정 &amp;gt; 새 비공개 키 발급&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;880&quot; data-start=&quot;864&quot; data-ke-size=&quot;size20&quot;&gt;✅ 소스코드 체크아웃&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1746171306359&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: Checkout source
  uses: actions/checkout@v3&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;992&quot; data-start=&quot;947&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;992&quot; data-start=&quot;947&quot;&gt;현재 리포지토리의 코드를 GitHub Actions 실행 환경으로 가져옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;1012&quot; data-start=&quot;994&quot; data-ke-size=&quot;size20&quot;&gt;  Node.js 설치&lt;/h4&gt;
&lt;pre id=&quot;code_1746171321028&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: Install Node.js
  uses: actions/setup-node@v3
  with:
    node-version: '21.5.0'&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1171&quot; data-start=&quot;1116&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1171&quot; data-start=&quot;1116&quot;&gt;해당 Node 버전(21.5.0)으로 설정합니다. 프로젝트에 맞는 버전으로 변경해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;1198&quot; data-start=&quot;1173&quot; data-ke-size=&quot;size20&quot;&gt; ️ Vue 프로젝트 설치 및 빌드&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1746171335834&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: Install and Build admin app
  working-directory: firebase/admin
  run: |
    npm install
    npm run build&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1456&quot; data-start=&quot;1328&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1402&quot; data-start=&quot;1328&quot;&gt;firebase/admin 경로에서 Vue 앱을 설치(npm install)하고 빌드(npm run build)합니다.&lt;/li&gt;
&lt;li data-end=&quot;1456&quot; data-start=&quot;1403&quot;&gt;이때 dist 폴더가 생성되고, 해당 폴더가 Firebase Hosting에 배포됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;1482&quot; data-start=&quot;1458&quot; data-ke-size=&quot;size20&quot;&gt;  Firebase 인증 키 작성&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1746171354255&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: Write Firebase service account key
  run: |
    echo '${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_JSON }}' &amp;gt; ${HOME}/firebase-key.json
    echo &quot;GOOGLE_APPLICATION_CREDENTIALS=${HOME}/firebase-key.json&quot; &amp;gt;&amp;gt; $GITHUB_ENV&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1873&quot; data-start=&quot;1722&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1782&quot; data-start=&quot;1722&quot;&gt;GitHub Secrets에 저장된 &lt;b&gt;Firebase 서비스 계정 키 JSON&lt;/b&gt;을 파일로 저장하고, 환경변수로 등록합니다.&lt;/li&gt;
&lt;li data-end=&quot;1873&quot; data-start=&quot;1783&quot;&gt;환경변수 GOOGLE_APPLICATION_CREDENTIALS에 해당 키 파일 경로를 설정하여 Firebase CLI에서 인증에 사용할 수 있게 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;2039&quot; data-start=&quot;2012&quot; data-ke-size=&quot;size20&quot;&gt;  Firebase Hosting 배포&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1746171499409&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: Deploy to Firebase Hosting (admin)
  working-directory: firebase
  run: |
    npm install -g firebase-tools
    firebase deploy --only hosting:admin&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2344&quot; data-start=&quot;2211&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2261&quot; data-start=&quot;2211&quot;&gt;Firebase CLI를 설치하고, hosting:admin에만 배포를 실행합니다.&lt;/li&gt;
&lt;li data-end=&quot;2344&quot; data-start=&quot;2262&quot;&gt;Firebase 프로젝트 설정의 firebase.json에서 hosting에 target: &quot;admin&quot; 설정이 되어 있어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2349&quot; data-start=&quot;2346&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2383&quot; data-start=&quot;2351&quot; data-ke-size=&quot;size26&quot;&gt;4. Firebase Hosting 타겟 설정 예시&lt;/h2&gt;
&lt;p data-end=&quot;2404&quot; data-start=&quot;2385&quot; data-ke-size=&quot;size16&quot;&gt;firebase.json 예시:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1746171549369&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;hosting&quot;: [
    {
      &quot;target&quot;: &quot;admin&quot;,
      &quot;public&quot;: &quot;admin/dist&quot;,
      &quot;ignore&quot;: [&quot;firebase.json&quot;, &quot;**/.*&quot;, &quot;**/node_modules/**&quot;]
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2590&quot; data-start=&quot;2573&quot; data-ke-size=&quot;size16&quot;&gt;.firebaserc 예시:&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746171564252&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;projects&quot;: {
    &quot;default&quot;: &quot;your-firebase-project-id&quot;
  },
  &quot;targets&quot;: {
    &quot;your-firebase-project-id&quot;: {
      &quot;hosting&quot;: {
        &quot;admin&quot;: [&quot;your-site-id&quot;]
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;2794&quot; data-start=&quot;2791&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2806&quot; data-start=&quot;2796&quot; data-ke-size=&quot;size26&quot;&gt;  마무리&lt;/h2&gt;
&lt;p data-end=&quot;2934&quot; data-start=&quot;2808&quot; data-ke-size=&quot;size16&quot;&gt;이제 main 브랜치에 코드를 push하면 GitHub Actions가 자동으로 Vue 앱을 빌드하고 Firebase에 배포합니다.&lt;/p&gt;
&lt;p data-end=&quot;2934&quot; data-start=&quot;2808&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 반복적인 개발-테스트-배포 과정을 수동으로 작업하지 않고, 빠르고 안정적인 배포 환경을 구축할 수 있습니다.&lt;/p&gt;</description>
      <category>개발/DevOps</category>
      <category>actions</category>
      <category>CICD</category>
      <category>deploy</category>
      <category>DevOps</category>
      <category>firebase</category>
      <category>firebasehosting</category>
      <category>Github</category>
      <category>Hosting</category>
      <category>Secrets</category>
      <category>vue</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/7</guid>
      <comments>https://smink.tistory.com/entry/GitHub-Actions%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-Vue-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Firebase-Hosting-%EC%9E%90%EB%8F%99-%EB%B0%B0%ED%8F%AC#entry7comment</comments>
      <pubDate>Fri, 2 May 2025 16:48:03 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot CI/CD with Github Actions, ECR, Docker, K3S</title>
      <link>https://smink.tistory.com/entry/Spring-Boot-CICD-with-Github-Actions-ECR-Docker-K3S</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring Boot CI/CD with Github Actions, ECR, Docker, K3s&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Github Actions&lt;/b&gt;를 사용하여 &lt;b&gt;Spring Boot 프로젝트&lt;/b&gt;를 &lt;b&gt;AWS EC2 환경에 자동 배포하는 CI/CD 파이프라인을 구축&lt;/b&gt;하는 과정을 작성해보겠습니다. &lt;b&gt;ECR(Elastic Container Registry)&lt;/b&gt;을 이용하여 Docker 이미지를 관리하고, 경량 Kubernetes 배포판인 &lt;b&gt;K3s&lt;/b&gt;를 통해 EC2 인스턴스에서 Spring Boot 애플리케이션을 컨테이너화하여 배포합니다. 전체 배포 과정은 Github main 브랜치에 코드가 병합되는 것을 트리거로 하여 자동으로 진행됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;cicd.png&quot; data-origin-width=&quot;1461&quot; data-origin-height=&quot;629&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm85qO/btsNvcM0MIO/9byTXUdz7BXO22e5Af49j0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm85qO/btsNvcM0MIO/9byTXUdz7BXO22e5Af49j0/img.png&quot; data-alt=&quot;CICD_Architecture&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm85qO/btsNvcM0MIO/9byTXUdz7BXO22e5Af49j0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm85qO%2FbtsNvcM0MIO%2F9byTXUdz7BXO22e5Af49j0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1461&quot; height=&quot;629&quot; data-filename=&quot;cicd.png&quot; data-origin-width=&quot;1461&quot; data-origin-height=&quot;629&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CICD_Architecture&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간략한 계획은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-sourcepos=&quot;9:1-15:0&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-sourcepos=&quot;9:1-9:78&quot;&gt;&lt;b&gt;Github main branch merge:&lt;/b&gt; 코드 변경 사항이 main 브랜치에 병합되면 CI/CD 파이프라인이 시작됩니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;10:1-10:70&quot;&gt;&lt;b&gt;./gradlew clean build:&lt;/b&gt; Spring Boot 프로젝트를 Gradle을 사용하여 빌드합니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;11:1-11:63&quot;&gt;&lt;b&gt;docker build -t springBootPJT .:&lt;/b&gt; Docker 이미지를 빌드합니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;12:1-12:60&quot;&gt;&lt;b&gt;AWS ECR로 푸시하기:&lt;/b&gt; 빌드된 Docker 이미지를 AWS ECR 리포지토리에 푸시합니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;13:1-13:76&quot;&gt;&lt;b&gt;ssh 를 통해 EC2에 접속하기:&lt;/b&gt; Github Actions Runner에서 SSH를 통해 EC2 인스턴스에 접속합니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;14:1-15:0&quot;&gt;&lt;b&gt;K3s를 통해 프로젝트 배포하기:&lt;/b&gt; EC2 인스턴스에서 kubectl 명령어를 사용하여 Kubernetes 매니페스트 파일들을 적용하고 Spring Boot 애플리케이션을 배포합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Github Actions&lt;/h2&gt;
&lt;pre id=&quot;code_1745376407012&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Spring Boot CI/CD

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'

    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    - name: Build with Gradle
      run: ./gradlew clean build -x test

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ secrets.AWS_REGION }}

    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v2

    - name: Build and push Docker image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY_NAME }}
        IMAGE_TAG: ${{ github.sha }}
      run: |
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

    - name: Copy Kubernetes manifests to EC2
      uses: appleboy/scp-action@v0.1.7
      with:
        host: ${{ secrets.EC2_HOST }}
        username: ${{ secrets.EC2_USERNAME }}
        key: ${{ secrets.EC2_PRIVATE_KEY }}
        port: ${{ secrets.EC2_PORT }}
        source: &quot;./spring-deployment.yaml, ./spring-service.yaml&quot;
        target: user_path # 경로 변경!

    - name: SSH to EC2 and deploy to K3s
      uses: appleboy/ssh-action@v1
      with:
        host: ${{ secrets.EC2_HOST }}
        username: ${{ secrets.EC2_USERNAME }}
        key: ${{ secrets.EC2_PRIVATE_KEY }}
        port: ${{ secrets.EC2_PORT }}
        script: |
          echo &quot;Logging in to EC2...&quot;

          ECR_REGISTRY=&quot;${{ steps.login-ecr.outputs.registry }}&quot;
          AWS_ACCESS_KEY_ID=&quot;${{ secrets.AWS_ACCESS_KEY_ID }}&quot;
          IMAGE_FULL=&quot;${ECR_REGISTRY}/${{ secrets.ECR_REPOSITORY_NAME }}:${{ github.sha }}&quot;
          
          # Docker Registry 인증 정보를 담는 Kubernetes Secret 생성 또는 업데이트
          aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $ECR_REGISTRY
          sudo kubectl --kubeconfig=/etc/rancher/k3s/k3s.yaml create secret generic regcred \
            --from-file=.dockerconfigjson=/home/ubuntu/.docker/config.json \
            --type=kubernetes.io/dockerconfigjson \
            --namespace=default \
            --dry-run=client -o yaml | sudo kubectl --kubeconfig=/etc/rancher/k3s/k3s.yaml apply -f -

          # deployment.yaml 파일 내용 변경 (이미지 업데이트)
          sudo sed -i &quot;s|image: REPLACE_IMAGE|image: $IMAGE_FULL|g&quot; user_path/spring-deployment.yaml # 경로 변경!

          echo &quot;Applying Kubernetes manifest...&quot;
          sudo kubectl apply -f user_path/spring-deployment.yaml # 경로 변경!
          sudo kubectl apply -f user_path/spring-service.yaml # 경로 변경!
          echo &quot;Deployment completed!&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-sourcepos=&quot;73:1-73:23&quot; data-ke-size=&quot;size26&quot;&gt;Github Secrets 환경 변수&lt;/h2&gt;
&lt;p data-sourcepos=&quot;75:1-75:124&quot; data-ke-size=&quot;size16&quot;&gt;Github Repository의 &quot;Settings&quot; -&amp;gt; &quot;Secrets and variables&quot; -&amp;gt; &quot;Actions&quot; -&amp;gt; &quot;New repository secret&quot; 메뉴를 통해 다음 환경 변수들을 등록해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;77:1-85:0&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;77:1-77:40&quot;&gt;AWS_ACCESS_KEY_ID: AWS Access Key ID&lt;/li&gt;
&lt;li data-sourcepos=&quot;78:1-78:48&quot;&gt;AWS_SECRET_ACCESS_KEY: AWS Secret Access Key&lt;/li&gt;
&lt;li data-sourcepos=&quot;79:1-79:48&quot;&gt;AWS_REGION: AWS Region (예: ap-northeast-2)&lt;/li&gt;
&lt;li data-sourcepos=&quot;80:1-80:60&quot;&gt;ECR_REPOSITORY_NAME: ECR 리포지토리 이름&lt;/li&gt;
&lt;li data-sourcepos=&quot;81:1-81:36&quot;&gt;EC2_HOST: EC2 인스턴스의 IP 주소 또는 도메인&lt;/li&gt;
&lt;li data-sourcepos=&quot;82:1-82:57&quot;&gt;EC2_USERNAME: EC2 접속 사용자 이름 (예: ec2-user, ubuntu)&lt;/li&gt;
&lt;li data-sourcepos=&quot;83:1-83:64&quot;&gt;EC2_PRIVATE_KEY: EC2 접속을 위한 SSH Private Key (주의: 보안에 매우 중요!)&lt;/li&gt;
&lt;li data-sourcepos=&quot;84:1-85:0&quot;&gt;EC2_PORT: SSH 포트 (기본값은 22)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dockerfile&lt;/h2&gt;
&lt;pre id=&quot;code_1745459202106&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM openjdk:17-jdk

CMD [&quot;./gradlew&quot;, &quot;clean&quot;, &quot;build&quot;]

COPY build/libs/*SNAPSHOT.jar app.jar

EXPOSE 8080

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-sourcepos=&quot;86:1-86:30&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-sourcepos=&quot;86:1-86:30&quot; data-ke-size=&quot;size26&quot;&gt;spring-deployment.yaml (K3s deployment)&lt;/h2&gt;
&lt;p data-sourcepos=&quot;88:1-88:58&quot; data-ke-size=&quot;size16&quot;&gt;Github Repository의 디렉토리에 위치해야 하며, 내용은 다음과 같습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;YAML&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment

metadata:
  name: spring-deployment

# Deployment 세부 정보
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backend-app

  # 배포할 Pod 정의
  template:
    metadata:
      labels:
        app: backend-app
    spec:
      imagePullSecrets:
        - name: regcred
      containers:
        - name: spring-container
          image: REPLACE_IMAGE
          ports:
            - containerPort: 8080
          env:
            - name: MYSQL_URL
              valueFrom:
                secretKeyRef:
                  key: mysql-url
                  name: spring-secret
            - name: MYSQL_USERNAME
              valueFrom:
                secretKeyRef:
                  key: mysql-username
                  name: spring-secret
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  key: mysql-password
                  name: spring-secret&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-sourcepos=&quot;143:1-143:31&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-sourcepos=&quot;143:1-143:31&quot; data-ke-size=&quot;size26&quot;&gt;spring-service.yaml (K3s Service)&lt;/h2&gt;
&lt;p data-sourcepos=&quot;145:1-145:49&quot; data-ke-size=&quot;size16&quot;&gt;EC2 인스턴스에 K3s 클러스터 내에 이미 다음과 같은 Service가 존재해야 합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;YAML&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1745400358712&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service

# Service 기본 정보
metadata:
  name: spring-service # Service 이름

# Service 세부 정보
spec:
  type: NodePort # Service의 종류
  selector:
    app: backend-app # 실행되고 있는 파드 중 'app: backend-app' 이라는 값을 가진 파드와 서비스를 연결
  ports:
    - protocol: TCP # Service에 접속하기 위한 프로토콜
      port: 8080 # 쿠버네티스 내부에서 Service에 접속하기 위한 포트 번호
      targetPort: 8080 # 매핑하기 위한 파드의 포트 번호
      nodePort: 30000 # 외부에서 사용자들이 접근하게 될 포트 번호
      # The Service &quot;spring-service&quot; is invalid: spec.ports[0].nodePort: Invalid value: 8080: provided port is not in the valid range. The range of valid ports is 30000-32767
      # nodePort는 30000-32767 이내의 포트를 사용해야함&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-sourcepos=&quot;143:1-143:31&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-sourcepos=&quot;143:1-143:31&quot; data-ke-size=&quot;size26&quot;&gt;spring-secret.yaml (K3s Secret)&lt;/h2&gt;
&lt;p data-sourcepos=&quot;145:1-145:49&quot; data-ke-size=&quot;size16&quot;&gt;EC2 인스턴스에 K3s 클러스터 내에 이미 다음과 같은 Secret이 존재해야 합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;YAML&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Secret

# Secret 기본 정보
metadata:
  name: spring-secret

# Key, Value 형식으로 설정값 저장
stringData:
  mysql-url: user_MySQL_url
  mysql-username: username
  mysql-password: myPassword123&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-sourcepos=&quot;162:1-162:8&quot; data-ke-size=&quot;size26&quot;&gt;추가 설명&lt;/h2&gt;
&lt;p data-sourcepos=&quot;20:1-20:118&quot; data-ke-size=&quot;size16&quot;&gt;이 워크플로우는 Spring Boot 프로젝트의 CI/CD 파이프라인을 자동화하여 개발자가 코드를 main 브랜치에 푸시하는 것만으로 EC2 인스턴스에 최신 버전의 애플리케이션을 배포할 수 있도록 구성되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;22:1-33:0&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;22:1-28:95&quot;&gt;&lt;b&gt;Workflow:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;23:5-28:95&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;23:5-23:32&quot;&gt;main 브랜치에 푸시되면 자동으로 실행됩니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;24:5-24:60&quot;&gt;Java 17 환경을 설정하고 Gradle을 사용하여 Spring Boot 프로젝트를 빌드합니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;25:5-25:83&quot;&gt;AWS 인증 정보를 설정하고 ECR에 로그인하여 Docker 이미지를 빌드하고 푸시합니다. 이미지 태그는 Git 커밋 SHA를 사용합니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;26:5-26:101&quot;&gt;SSH를 통해 EC2 인스턴스에 접속하여 Kubernetes Secret (regcred)을 생성/업데이트하여 ECR로부터 이미지를 pull 할 수 있도록 인증합니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;27:5-27:171&quot;&gt;EC2 인스턴스에 Kubernetes Deployment (spring-deployment.yaml) 및 Service (spring-service.yaml) 매니페스트 파일을 복사하고, sed 명령어를 사용하여 Deployment의 이미지 이름을 최신 이미지 URI로 업데이트합니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;28:5-28:95&quot;&gt;kubectl apply 명령어를 사용하여 Kubernetes 매니페스트 파일들을 K3s 클러스터에 적용하여 Spring Boot 애플리케이션을 배포합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-sourcepos=&quot;29:1-29:82&quot;&gt;&lt;b&gt;Github Secrets:&lt;/b&gt; 워크플로우에서 AWS 및 EC2 접근 정보를 안전하게 관리하기 위해 Github Secrets를 사용합니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;30:1-30:177&quot;&gt;&lt;b&gt;spring-deployment.yaml:&lt;/b&gt; Spring Boot 애플리케이션 파드를 관리하는 Kubernetes Deployment 설정 파일입니다. 레포지토리에는 이미지 업데이트를 위한 플레이스홀더(REPLACE_IMAGE)가 포함되어 있으며, 워크플로우 실행 중 실제 이미지 URI로 변경됩니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;31:1-31:203&quot;&gt;&lt;b&gt;spring-service.yaml:&lt;/b&gt; Spring Boot 애플리케이션을 외부로 노출하는 Kubernetes Service 설정 파일입니다. NodePort 타입을 사용하여 특정 포트(30000)를 통해 접근할 수 있도록 구성되어 있습니다. 주의: nodePort는 유효한 범위(30000-32767) 내의 포트를 사용해야 합니다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;32:1-33:0&quot;&gt;&lt;b&gt;spring-secret.yaml:&lt;/b&gt; Spring Boot 애플리케이션이 사용할 MySQL 연결 정보를 담고 있는 Kubernetes Secret 설정 파일입니다. K3s 클러스터에 미리 존재해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-sourcepos=&quot;34:1-34:105&quot; data-ke-size=&quot;size16&quot;&gt;이 설정을 통해 Github Actions 파이프라인은 main 브랜치에 코드가 푸시될 때 자동으로 빌드, Docker 이미지 푸시, EC2 인스턴스에 배포까지의 전체 과정을 자동화합니다.&lt;/p&gt;</description>
      <category>개발/DevOps</category>
      <category>actions</category>
      <category>CICD</category>
      <category>docker</category>
      <category>EC2</category>
      <category>ecr</category>
      <category>github_actions</category>
      <category>k3s</category>
      <category>k8s</category>
      <category>kubernetes</category>
      <category>Spring</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/6</guid>
      <comments>https://smink.tistory.com/entry/Spring-Boot-CICD-with-Github-Actions-ECR-Docker-K3S#entry6comment</comments>
      <pubDate>Thu, 24 Apr 2025 11:53:25 +0900</pubDate>
    </item>
    <item>
      <title>Docker-hub, AWS ECR 비교하기</title>
      <link>https://smink.tistory.com/entry/Docker-hub-AWS-ECR-%EB%B9%84%EA%B5%90%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-end=&quot;235&quot; data-start=&quot;230&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s3nnk/btsNnvD7DMO/6mrLyixYuGlLgbavIt4L3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s3nnk/btsNnvD7DMO/6mrLyixYuGlLgbavIt4L3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s3nnk/btsNnvD7DMO/6mrLyixYuGlLgbavIt4L3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs3nnk%2FbtsNnvD7DMO%2F6mrLyixYuGlLgbavIt4L3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;314&quot; height=&quot;209&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;345&quot; data-start=&quot;237&quot; data-ke-size=&quot;size16&quot;&gt;현재는 GitHub Actions를 통해 main 브랜치에 merge되었을 때, EC2 인스턴스에 작성해 둔 Shell Script를 실행하여 Spring Boot API 서버를 배포하고 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;406&quot; data-start=&quot;347&quot; data-ke-size=&quot;size16&quot;&gt;이러한 방식은 &lt;b&gt;간단하게 배포를 자동화&lt;/b&gt;할 수 있다는 장점이 있지만, 다음과 같은 &lt;b&gt;문제점&lt;/b&gt;이 있었습니다.&lt;/p&gt;
&lt;hr data-end=&quot;411&quot; data-start=&quot;408&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;429&quot; data-start=&quot;413&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;기존 배포 방식의 문제점&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;726&quot; data-start=&quot;431&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;513&quot; data-start=&quot;431&quot;&gt;&lt;b&gt; 무중단 배포의 어려움&lt;/b&gt;&lt;br /&gt;서버를 중지하고 다시 시작하는 방식이기 때문에, 배포 시 사용자에게 순간적인 장애가 발생할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;575&quot; data-start=&quot;515&quot;&gt;&lt;b&gt;롤백의 어려움&lt;/b&gt;&lt;br /&gt;문제가 생겼을 때 이전 버전으로 되돌리는 과정이 수동적이고 번거롭습니다.&lt;/li&gt;
&lt;li data-end=&quot;657&quot; data-start=&quot;577&quot;&gt;&lt;b&gt;트래픽 증가 대응의 어려움&lt;/b&gt;&lt;br /&gt;EC2 인스턴스 하나에서 모든 요청을 처리하기 때문에, 트래픽이 늘어날 경우 확장이 어렵습니다.&lt;/li&gt;
&lt;li data-end=&quot;726&quot; data-start=&quot;659&quot;&gt;&lt;b&gt;환경 간 일관성 부족&lt;/b&gt;&lt;br /&gt;로컬과 운영 환경의 설정 차이로 인해 예기치 못한 문제가 발생할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;731&quot; data-start=&quot;728&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;761&quot; data-start=&quot;733&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Docker &amp;amp; Kubernetes 도입 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;818&quot; data-start=&quot;763&quot; data-ke-size=&quot;size16&quot;&gt;이러한 문제점을 해결하기 위해 &lt;b&gt;Docker와 Kubernetes&lt;/b&gt; 도입을 검토하게 되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1157&quot; data-start=&quot;820&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;927&quot; data-start=&quot;820&quot;&gt;&lt;b&gt;Docker&lt;/b&gt;는 이미지 기반의 배포 방식으로, 애플리케이션을 &lt;b&gt;환경에 구애받지 않고&lt;/b&gt; 실행할 수 있습니다. 버전 태그를 통해 &lt;b&gt;빠른 롤백&lt;/b&gt;이 가능하고, 동일한 환경 구성을 보장합니다.&lt;/li&gt;
&lt;li data-end=&quot;1157&quot; data-start=&quot;929&quot;&gt;&lt;b&gt;Kubernetes(K8s)&lt;/b&gt;는 Rolling Update, Blue-Green Deployment 등의 배포 전략을 통해 &lt;b&gt;무중단 배포&lt;/b&gt;를 지원하며, HPA(Horizontal Pod Autoscaler)를 통해 &lt;b&gt;자동 스케일링&lt;/b&gt;도 가능합니다. 또한, Pod 상태를 지속적으로 체크하여 문제가 발생한 경우 자동으로 재시작하거나 교체하여 &lt;b&gt;모니터링 및 장애 대응&lt;/b&gt;도 용이합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1162&quot; data-start=&quot;1159&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1200&quot; data-start=&quot;1164&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이미지 저장소 선택: Docker Hub vs AWS ECR&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1327&quot; data-start=&quot;1202&quot; data-ke-size=&quot;size16&quot;&gt;오늘은 Spring Boot 프로젝트의 Docker 이미지를 생성한 후, 해당 이미지를 저장하고 배포할 저장소로 &lt;b&gt;Docker Hub&lt;/b&gt;와 &lt;b&gt;AWS ECR&lt;/b&gt; 중 어떤 것을 선택하는 것이 좋을지에 대해 정리해보려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  Docker Hub vs   AWS ECR 비교&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 200px;&quot; border=&quot;1&quot; data-end=&quot;902&quot; data-start=&quot;289&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;Docker Hub&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;AWS ECR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;429&quot; data-start=&quot;351&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;364&quot; data-start=&quot;351&quot;&gt;&lt;b&gt;설정 난이도&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;395&quot; data-start=&quot;364&quot;&gt;설정이 간단하고 직관적이며, CI/CD 연동도 쉬움&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;429&quot; data-start=&quot;395&quot;&gt;IAM 설정, 권한 부여 등 약간의 사전 설정이 필요함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot; data-end=&quot;538&quot; data-start=&quot;430&quot;&gt;
&lt;td style=&quot;height: 40px;&quot; data-end=&quot;445&quot; data-start=&quot;430&quot;&gt;&lt;b&gt;속도 및 접근성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot; data-end=&quot;483&quot; data-start=&quot;445&quot;&gt;퍼블릭 레지스트리로 글로벌 CDN을 통해 빠른 이미지 접근 가능&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot; data-end=&quot;538&quot; data-start=&quot;483&quot;&gt;같은 리전에 있을 경우 빠른 이미지 Pull이 가능함 (특히 AWS 서비스와 통합 시 강점)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot; data-end=&quot;643&quot; data-start=&quot;539&quot;&gt;
&lt;td style=&quot;height: 40px;&quot; data-end=&quot;548&quot; data-start=&quot;539&quot;&gt;&lt;b&gt;보안&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot; data-end=&quot;603&quot; data-start=&quot;548&quot;&gt;퍼블릭 기본, Private는 유료 제한 있음 (무료 계정은 private repo 수 제한)&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot; data-end=&quot;643&quot; data-start=&quot;603&quot;&gt;기본적으로 private, IAM과 연계된 세밀한 권한 설정 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;728&quot; data-start=&quot;644&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;653&quot; data-start=&quot;644&quot;&gt;&lt;b&gt;비용&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;690&quot; data-start=&quot;653&quot;&gt;퍼블릭은 무료, Private 저장소는 제한적 무료 또는 유료&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;728&quot; data-start=&quot;690&quot;&gt;AWS 프리 티어 제공, 사용량 초과 시 S3 기반 과금 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot; data-end=&quot;831&quot; data-start=&quot;729&quot;&gt;
&lt;td style=&quot;height: 40px;&quot; data-end=&quot;744&quot; data-start=&quot;729&quot;&gt;&lt;b&gt;CI/CD 연동&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot; data-end=&quot;779&quot; data-start=&quot;744&quot;&gt;GitHub Actions 등 외부 도구와 쉽게 연동 가능&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot; data-end=&quot;831&quot; data-start=&quot;779&quot;&gt;GitHub Actions 또는 CodePipeline 등 AWS 생태계 연동이 뛰어남&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;902&quot; data-start=&quot;832&quot;&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;842&quot; data-start=&quot;832&quot;&gt;&lt;b&gt;사용성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;873&quot; data-start=&quot;842&quot;&gt;전 세계적으로 널리 쓰이며 커뮤니티 및 문서가 풍부&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot; data-end=&quot;902&quot; data-start=&quot;873&quot;&gt;AWS를 기반으로 하는 환경에 최적화되어 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; Docker-hub&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클라우드 기반 레포지토리.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;도커 유저들은 도커 허브를 통해 컨테이너 이미지를 생성, 테스트, 저장 그리고 배포할 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;간편한 사용&lt;/b&gt;: Docker 명령어로 간단히 이미지를 푸시하고 풀(pull)할 수 있어 사용이 매우 직관적이고 쉽습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;무료 사용&lt;/b&gt;: 개인용 공개 저장소는 무료로 제공되며, 기본적인 기능은 무료로 이용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;광범위한 지원&lt;/b&gt;: Docker와의 자연스러운 호환성으로, 다양한 Docker화된 애플리케이션을 쉽게 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;속도&lt;/b&gt;: 글로벌 서버에 있는 Docker Hub의 이미지를 풀하는 속도가 지역에 따라 느릴 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제한된 개인 저장소&lt;/b&gt;: 무료 계정에서는 개인 저장소의 수나 푸시 횟수 등에 제한이 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안 문제&lt;/b&gt;: 공개 저장소의 이미지는 누구나 접근할 수 있어, 보안 취약점이 있는 이미지를 사용할 위험이 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비용&lt;/b&gt;: 개인 저장소의 수나 프라이빗 저장소 기능을 확장하려면 유료 플랜을 이용해야 하며, 비용이 발생합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;권한 관리&lt;/b&gt;: 권한 관리를 체계적으로 하지 못합니다.&lt;/li&gt;
&lt;li&gt;클라우드&amp;nbsp;서비스로&amp;nbsp;AWS를&amp;nbsp;사용하지만,&amp;nbsp;컨테이너&amp;nbsp;이미지&amp;nbsp;관리는&amp;nbsp;도커&amp;nbsp;허브에서&amp;nbsp;하기&amp;nbsp;때문에&amp;nbsp;&lt;b&gt;사용자가 관리해야 하는 서비스가 많습니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; AWS ECR&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안전하고 확장 가능하고 신뢰할 수 있는 AWS 관리형 컨테이너 이미지 레지스트리 서비스입니다.&lt;br /&gt;AWS IAM을 사용하여 리소스 기반 권한을 가진 프라이빗 레포지토리를 지원합니다.&lt;br /&gt;컨테이너 이미지를 Amazon S3에 저장합니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AWS와의 통합&lt;/b&gt;: AWS의 다른 서비스들(예: ECS, EKS, Lambda)과 매끄럽게 통합되어, 관리가 용이하고 배포가 간편합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안&lt;/b&gt;: IAM(Identity and Access Management)을 사용해 사용자 및 권한 관리를 할 수 있어 보안이 강화됩니다. 이미지의 보안도 Amazon GuardDuty나 ECR Repository 정책을 통해 관리 가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고가용성&lt;/b&gt;: AWS의 글로벌 인프라에 의해 높은 가용성과 안정성을 보장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동화&lt;/b&gt;: ECR과 연동된 CI/CD 파이프라인을 쉽게 구성할 수 있어 자동화가 가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클라우드&amp;nbsp;관리&amp;nbsp;용이성&lt;/b&gt; : 클라우드 서비스를 이용하는 AWS에서 컨테이너 관리도 함께 하기 때문에 사용자가 관리하는 서비스가 적습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨테이너&amp;nbsp;관리&amp;nbsp;용이성&lt;/b&gt; :&amp;nbsp;S3를&amp;nbsp;이용하여&amp;nbsp;컨테이너를&amp;nbsp;쉽게&amp;nbsp;관리할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비용&lt;/b&gt;: 저장 용량과 데이터 전송에 따른 요금이 부과됨. 사용량에 따라 가격이 올라갈 수 있어 비용 관리를 철저히 해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AWS 종속성&lt;/b&gt;: AWS 생태계에 종속되므로, AWS 밖의 환경에서 사용하기 어려운 점이 다소 존재합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;복잡한 설정&lt;/b&gt;: AWS의 복잡한 설정과 구성이 초보자에게는 다소 어려울 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;비용&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-end=&quot;141&quot; data-start=&quot;95&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; AWS ECR (Amazon Elastic Container Registry)&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-end=&quot;161&quot; data-start=&quot;143&quot; data-ke-size=&quot;size20&quot;&gt;1. &lt;b&gt;스토리지 비용&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;285&quot; data-start=&quot;162&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;201&quot; data-start=&quot;162&quot;&gt;&lt;span&gt;&lt;b&gt;요금&lt;/b&gt;: $0.10/GB/월&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;285&quot; data-start=&quot;202&quot;&gt;&lt;span&gt;&lt;b&gt;예시&lt;/b&gt;: 1GB 이미지 저장 시 월 $0.10&lt;/span&gt;​&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;314&quot; data-start=&quot;287&quot; data-ke-size=&quot;size20&quot;&gt;2. &lt;b&gt;데이터 수신 비용 (Push)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;398&quot; data-start=&quot;315&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;398&quot; data-start=&quot;315&quot;&gt;&lt;span&gt;&lt;b&gt;요금&lt;/b&gt;: 무료&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;427&quot; data-start=&quot;400&quot; data-ke-size=&quot;size20&quot;&gt;3. &lt;b&gt;데이터 송신 비용 (Pull)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;551&quot; data-start=&quot;428&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;467&quot; data-start=&quot;428&quot;&gt;&lt;span&gt;&lt;b&gt;동일 리전 내 EC2, ECS, Fargate 등에서 Pull 시&lt;/b&gt;: 무료&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;551&quot; data-start=&quot;468&quot;&gt;&lt;span&gt;&lt;b&gt;인터넷 또는 다른 리전으로의 Pull 시&lt;/b&gt;: $0.09/GB&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;574&quot; data-start=&quot;553&quot; data-ke-size=&quot;size20&quot;&gt;4. &lt;b&gt;예상 비용 (월간)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;746&quot; data-start=&quot;575&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;616&quot; data-start=&quot;575&quot;&gt;&lt;span&gt;&lt;b&gt;스토리지&lt;/b&gt;: 1GB &amp;times; $0.10 = $0.10&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;658&quot; data-start=&quot;617&quot;&gt;&lt;span&gt;&lt;b&gt;데이터 송신&lt;/b&gt;: 동일 리전 내에서 Pull 시 무료&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;746&quot; data-start=&quot;659&quot;&gt;&lt;span&gt;&lt;b&gt;총합&lt;/b&gt;: 약 $0.10/월&lt;/span&gt;​&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;751&quot; data-start=&quot;748&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;Docker Hub&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-end=&quot;786&quot; data-start=&quot;768&quot; data-ke-size=&quot;size20&quot;&gt;1. &lt;b&gt;스토리지 비용&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;916&quot; data-start=&quot;787&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;828&quot; data-start=&quot;787&quot;&gt;&lt;span&gt;&lt;b&gt;무료 플랜&lt;/b&gt;: 1개의 개인(private) 저장소, 최대 2GB 저장 용량&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;916&quot; data-start=&quot;829&quot;&gt;&lt;span&gt;&lt;b&gt;Pro 플랜&lt;/b&gt;: $9/월, 무제한 개인 저장소 및 추가 기능 제공&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;945&quot; data-start=&quot;918&quot; data-ke-size=&quot;size20&quot;&gt;2. &lt;b&gt;데이터 수신 비용 (Push)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1033&quot; data-start=&quot;946&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1033&quot; data-start=&quot;946&quot;&gt;&lt;span&gt;&lt;b&gt;요금&lt;/b&gt;: 무료&lt;/span&gt;​&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;1062&quot; data-start=&quot;1035&quot; data-ke-size=&quot;size20&quot;&gt;3. &lt;b&gt;데이터 송신 비용 (Pull)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1192&quot; data-start=&quot;1063&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1104&quot; data-start=&quot;1063&quot;&gt;&lt;span&gt;&lt;b&gt;무료 플랜&lt;/b&gt;: 인증된 사용자 기준 6시간당 100회 Pull 제한&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1192&quot; data-start=&quot;1105&quot;&gt;&lt;span&gt;&lt;b&gt;Pro 플랜 이상&lt;/b&gt;: Pull 횟수 무제한&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;1215&quot; data-start=&quot;1194&quot; data-ke-size=&quot;size20&quot;&gt;4. &lt;b&gt;예상 비용 (월간)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1345&quot; data-start=&quot;1216&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1257&quot; data-start=&quot;1216&quot;&gt;&lt;span&gt;&lt;b&gt;무료 플랜&lt;/b&gt;: Pull 제한으로 인해 주 50회 이상 사용 시 제한에 걸릴 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1345&quot; data-start=&quot;1258&quot;&gt;&lt;span&gt;&lt;b&gt;Pro 플랜&lt;/b&gt;: $9/월&lt;/span&gt;​&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비교 요약&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2370&quot; data-start=&quot;1362&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;항목&lt;/td&gt;
&lt;td&gt;AWS ECR&lt;/td&gt;
&lt;td&gt;Docker Hub (Pro 플랜)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1674&quot; data-start=&quot;1520&quot;&gt;
&lt;td data-end=&quot;1562&quot; data-start=&quot;1520&quot;&gt;&lt;span&gt;&lt;b&gt;스토리지 요금&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;1620&quot; data-start=&quot;1562&quot;&gt;&lt;span&gt;$0.10/GB/월&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;1674&quot; data-start=&quot;1620&quot;&gt;&lt;span&gt;$9/월 (2GB 포함)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1854&quot; data-start=&quot;1675&quot;&gt;
&lt;td data-end=&quot;1721&quot; data-start=&quot;1675&quot;&gt;&lt;span&gt;&lt;b&gt;Push 요금&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;1787&quot; data-start=&quot;1721&quot;&gt;&lt;span&gt;무료&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;1854&quot; data-start=&quot;1787&quot;&gt;&lt;span&gt;무료&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2003&quot; data-start=&quot;1855&quot;&gt;
&lt;td data-end=&quot;1901&quot; data-start=&quot;1855&quot;&gt;&lt;span&gt;&lt;b&gt;Pull 요금&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;1954&quot; data-start=&quot;1901&quot;&gt;&lt;span&gt;동일 리전 내 무료&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;2003&quot; data-start=&quot;1954&quot;&gt;&lt;span&gt;무제한 (Pro 플랜 기준)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2166&quot; data-start=&quot;2004&quot;&gt;
&lt;td data-end=&quot;2050&quot; data-start=&quot;2004&quot;&gt;&lt;span&gt;&lt;b&gt;Pull 제한&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;2116&quot; data-start=&quot;2050&quot;&gt;&lt;span&gt;없음&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;2166&quot; data-start=&quot;2116&quot;&gt;&lt;span&gt;무료 플랜은 제한 있음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2370&quot; data-start=&quot;2167&quot;&gt;
&lt;td data-end=&quot;2210&quot; data-start=&quot;2167&quot;&gt;&lt;span&gt;&lt;b&gt;총 예상 비용&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;2258&quot; data-start=&quot;2210&quot;&gt;&lt;span&gt;약 $0.10/월 (1GB 기준)&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;2323&quot; data-start=&quot;2258&quot;&gt;&lt;span&gt;$9/월&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2683&quot; data-start=&quot;2598&quot; data-ke-size=&quot;size16&quot;&gt;기업 환경에서 EC2, RDS 등 &lt;b&gt;AWS 인프라를 이미 사용 중이기 때문에&lt;/b&gt; &lt;u&gt;&lt;b&gt;AWS ECR(Amazon Elastic Container Registry)&lt;/b&gt;&lt;/u&gt;는 다음과 같은 이유로 가장 적합하다고 생각하였습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1071&quot; data-start=&quot;187&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;375&quot; data-start=&quot;187&quot;&gt;&lt;b&gt;AWS 서비스와의 강력한 통합성&lt;/b&gt;&lt;br /&gt;ECR은 EC2, ECS, Fargate, CodePipeline, IAM 등 다양한 AWS 서비스와 자연스럽게 연동되므로, 별도 설정 없이도 이미지 빌드부터 배포까지의 전체 워크플로우를 AWS 내에서 통합적으로 구성할 수 있습니다. 이는 유지보수 및 관리 효율성을 크게 높여줍니다.&lt;/li&gt;
&lt;li data-end=&quot;586&quot; data-start=&quot;377&quot;&gt;&lt;b&gt;리전 내 데이터 송수신 무료 혜택&lt;/b&gt;&lt;br /&gt;동일 리전 내에서 EC2나 ECS와 같은 AWS 서비스가 ECR에서 이미지를 Pull 할 경우, &lt;b&gt;네트워크 요금이 발생하지 않기 때문에&lt;/b&gt; 운영 비용을 절감할 수 있습니다. 외부 레지스트리(Docker Hub 등)에서 이미지를 Pull 하면 인터넷 송신 요금이 발생할 수 있어 장기적으로는 비용 부담이 커질 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;740&quot; data-start=&quot;588&quot;&gt;&lt;b&gt;보안 및 접근 제어&lt;/b&gt;&lt;br /&gt;ECR은 AWS IAM과 연동되어, 세부적인 권한 관리(예: 특정 사용자에게만 Push 권한 부여 등)가 가능합니다. 또한, &lt;b&gt;이미지 스캔 기능을 통해 취약점 분석&lt;/b&gt;을 수행할 수 있어 보안이 중요한 기업 환경에 매우 적합합니다.&lt;/li&gt;
&lt;li data-end=&quot;858&quot; data-start=&quot;742&quot;&gt;&lt;b&gt;CI/CD 자동화에 최적화&lt;/b&gt;&lt;br /&gt;AWS CodeBuild, CodePipeline과의 연계를 통해 자동 빌드 및 배포 파이프라인을 쉽게 구성할 수 있어 DevOps 환경에 최적화되어 있습니다.&lt;br /&gt;하지만 Github Actions를 통한 CI/CD를 고려하고 있기 때문에 해당 항목은 크게 고려되지 않았습니다.&lt;/li&gt;
&lt;li data-end=&quot;1071&quot; data-start=&quot;860&quot;&gt;&lt;b&gt;예상 가능하고 합리적인 비용&lt;/b&gt;&lt;br /&gt;Docker Hub는 일정 횟수 이상 이미지 Pull이 발생하면 Pro 요금제 이상의 유료 플랜을 이용해야 하며, 일부 기업 환경에서는 비용이 예측 불가능하게 증가할 수 있습니다. 반면, ECR은 저장 용량 기준으로만 요금이 부과되고, AWS 내부 통신에는 별도의 비용이 들지 않아 &lt;b&gt;비용 구조가 단순하고 예측 가능합니다&lt;/b&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;1213&quot; data-start=&quot;1073&quot; data-ke-size=&quot;size16&quot;&gt;따라서, &lt;span&gt;기업 환경에서 AWS의 EC2 및 RDS를 사용하고 있으며, 동일 리전 내에서의 이미지 Pull이 주로 발생하는 경우, &lt;b&gt;AWS ECR&lt;/b&gt;이 비용 효율적이고 AWS 서비스와의 통합 측면에서도 유리합니다.&lt;/span&gt; &lt;span&gt;특히, 초기 개발 환경 설정 시 빈번한 이미지 Pull이 예상된다면, Pull 제한이 없는 AWS ECR이 적합하다고 생각하였습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발/DevOps</category>
      <category>AWS</category>
      <category>aws-ecr</category>
      <category>CICD</category>
      <category>docker</category>
      <category>docker-hub</category>
      <category>dockerhub</category>
      <category>ecr</category>
      <category>k8s</category>
      <category>kubernetes</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/5</guid>
      <comments>https://smink.tistory.com/entry/Docker-hub-AWS-ECR-%EB%B9%84%EA%B5%90%ED%95%98%EA%B8%B0#entry5comment</comments>
      <pubDate>Wed, 16 Apr 2025 12:28:04 +0900</pubDate>
    </item>
    <item>
      <title>도커(Docker) 배우기 (3) - 볼륨 (Volume)</title>
      <link>https://smink.tistory.com/entry/%EB%8F%84%EC%BB%A4Docker-%EB%B0%B0%EC%9A%B0%EA%B8%B0-3-%EB%B3%BC%EB%A5%A8-Volume</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Docker Volume(도커 볼륨)&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;643&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xIvYF/btsNhOK4Cov/6FbgPKfgEXa9Bad3UKh3Ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xIvYF/btsNhOK4Cov/6FbgPKfgEXa9Bad3UKh3Ek/img.png&quot; data-alt=&quot;도커 볼륨&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xIvYF/btsNhOK4Cov/6FbgPKfgEXa9Bad3UKh3Ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxIvYF%2FbtsNhOK4Cov%2F6FbgPKfgEXa9Bad3UKh3Ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;593&quot; height=&quot;298&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;643&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;도커 볼륨&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅&amp;nbsp;컨테이너가 가진 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 활용하면 특정 프로그램을 컨테이너로 띄울 수 있다. 이 프로그램에 기능이 추가되면 새로운 이미지를 만들어서 컨테이너를 실행시켜야 한다. 이 때, Docker는 기존 컨테이너에서 변경된 부분을 수정하지 않고, 새로운 컨테이너를 만들어서 통째로 갈아끼우는 방식으로 교체를 한다. 이게 효율적이라고 생각했던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 특징 때문에 기존 컨테이너를 새로운 컨테이너로 교체하면, 기존 컨테이너 내부에 있던 데이터도 같이 삭제된다. 만약 이 컨테이너가 MySQL을 실행시키는 컨테이너였다면 MySQL에 저장된 데이터도 같이 삭제 돼버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;u&gt;컨테이너 내부에 저장된 데이터가 삭제되면 안 되는 경우&lt;/u&gt;에는 &lt;u&gt;&lt;b&gt;볼륨(Volume)&lt;/b&gt;&lt;/u&gt;이라는 개념을 활용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(ex. MySQL과 같은 DB는 해당 컨테이너 환경에 데이터가 저장이 되는데, 그런데 이 컨테이너를 통째로 교체한다고 하면 해당 컨테이너에 저장되어있던 데이터가 같이 삭제되어버리는 문제가 발생한다.)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅&amp;nbsp;Docker Volume(도커 볼륨)이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;도커의 볼륨(Volume)&lt;/b&gt;&lt;/u&gt;이란 &lt;b&gt;도커 컨테이너에서 데이터를 영속적으로 저장하기 위한 방법&lt;/b&gt;이다. 볼륨(Volume)은 컨테이너 자체의 저장 공간을 사용하지 않고, 호스트 자체의 저장 공간을 공유해서 사용하는 형태이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;-&amp;gt; 컨테이너 지울 때 데이터 같이 안 날라가게 하는 방법!&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;726&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0e4Yl/btsNhkwR1pP/cA3JNT8J9kKX7NnGc2S5U0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0e4Yl/btsNhkwR1pP/cA3JNT8J9kKX7NnGc2S5U0/img.png&quot; data-alt=&quot;도커 볼륨&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0e4Yl/btsNhkwR1pP/cA3JNT8J9kKX7NnGc2S5U0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0e4Yl%2FbtsNhkwR1pP%2FcA3JNT8J9kKX7NnGc2S5U0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;576&quot; height=&quot;327&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;726&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;도커 볼륨&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅&amp;nbsp;볼륨(Volume)을 사용하는 명령어&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ docker run -v [호스트의 디렉토리 절대경로]:[컨테이너의 디렉토리 절대경로] [이미지명]:[태그명]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;[호스트의 디렉토리 절대 경로]에 디렉토리가 이미 존재할 경우, 호스트의 디렉터리가 컨테이너의 디렉터리를 덮어씌운다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEl6Sg/btsNh3HDpVP/8Jkwa0LUPHJzl4sJ9hkJxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEl6Sg/btsNh3HDpVP/8Jkwa0LUPHJzl4sJ9hkJxK/img.png&quot; data-alt=&quot;절대경로에 디렉토리가 존재할 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEl6Sg/btsNh3HDpVP/8Jkwa0LUPHJzl4sJ9hkJxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEl6Sg%2FbtsNh3HDpVP%2F8Jkwa0LUPHJzl4sJ9hkJxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;575&quot; height=&quot;323&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;절대경로에 디렉토리가 존재할 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;[호스트의 디렉토리 절대 경로]에 디렉토리가 존재하지 않을 경우, 호스트의 디렉터리 절대 경로에 디렉터리를 새로 만들고 컨테이너의 디렉터리에 있는 파일들을 호스트의 디렉터리로 복사해온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cI7iyJ/btsNhWhJRlM/nxKBCzNIkb5FhLenKQZ6C0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cI7iyJ/btsNhWhJRlM/nxKBCzNIkb5FhLenKQZ6C0/img.png&quot; data-alt=&quot;절대경로에 디렉토리가 존재하지 않을 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cI7iyJ/btsNhWhJRlM/nxKBCzNIkb5FhLenKQZ6C0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcI7iyJ%2FbtsNhWhJRlM%2FnxKBCzNIkb5FhLenKQZ6C0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;576&quot; height=&quot;325&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;722&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;절대경로에 디렉토리가 존재하지 않을 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/DevOps</category>
      <category>CICD</category>
      <category>Container</category>
      <category>docker</category>
      <category>Server</category>
      <category>volume</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/4</guid>
      <comments>https://smink.tistory.com/entry/%EB%8F%84%EC%BB%A4Docker-%EB%B0%B0%EC%9A%B0%EA%B8%B0-3-%EB%B3%BC%EB%A5%A8-Volume#entry4comment</comments>
      <pubDate>Fri, 11 Apr 2025 11:05:15 +0900</pubDate>
    </item>
    <item>
      <title>도커(Docker) 배우기 (2) - 자주 쓰는 명령어(Docker CLI)</title>
      <link>https://smink.tistory.com/entry/%EB%8F%84%EC%BB%A4Docker-%EB%B0%B0%EC%9A%B0%EA%B8%B0-2-%EC%9E%90%EC%A3%BC-%EC%93%B0%EB%8A%94-%EB%AA%85%EB%A0%B9%EC%96%B4Docker-CLI</link>
      <description>&lt;h1&gt;&lt;strong&gt;도커(Docker) 배우기 (2) - 자주 쓰는 명령어(Docker CLI)&lt;/strong&gt;&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;70%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tTnuK/btsNayOvI4v/Z1UMHSUBxsCT8Np5IkAbsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tTnuK/btsNayOvI4v/Z1UMHSUBxsCT8Np5IkAbsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tTnuK/btsNayOvI4v/Z1UMHSUBxsCT8Np5IkAbsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtTnuK%2FbtsNayOvI4v%2FZ1UMHSUBxsCT8Np5IkAbsk%2Fimg.png&quot; width=&quot;70%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;도커 이미지(Docker Image) 관련 명령어&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;이미지(Image)를 다운&lt;/strong&gt; 받는 명령어&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull {ImageName} docker pull {ImageName}:{TagName}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;이미지 목록 조회&lt;/strong&gt; (Image List)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker image ls&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;이미지 삭제&lt;/strong&gt; (Image Remove)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker image rm {ImageID} docker image rm {ImageName}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;Id가 너무 길고 복잡하기 때문에 &lt;strong&gt;일부분만 타이핑해도 삭제가 된다!&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;실행중인 컨테이너의 이미지는 삭제하지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;중단시킨 컨테이너에서 이미지를 사용중이라면&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker image rm -f {ImageID}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;컨테이너에서 &lt;strong&gt;사용하지 않는 전체 이미지 삭제&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker image rm $(docker images -q) docker image rm -f $(docker images -q)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;도커 컨테이너(Docker Container) 관련 명령어&lt;/strong&gt;&lt;/h2&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;(실행중인) &lt;strong&gt;컨테이너 조회&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker ps&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;-   \-a : 중단된 컨테이너도 조회&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;컨테이너 생성&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker create {ImageName}&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;-   Image가 없으면 알아서 찾아보고 다운로드까지 해줌&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;컨테이너 실행&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker start {Container ID}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;컨테이너 중단&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker stop {Container ID}
docker kill {Container ID}&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;-   stop : 시스템 종료 버튼을 통해 정상적으로 종료하는 방법
-   kill : 컴퓨터 전원버튼 꾹 눌러 끄듯이 강제로 끄는 방법&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;컨테이너 삭제&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker rm {Container ID}
docker rm -f {Container ID}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;컨테이너 생성 &amp;amp; 실행&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker run {ImageName}
docker run --name {ContainerName} -d -p {HostPort}:{ContainerPort} {ImageName}\[:TagName\]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;컨테이너 로그 조회&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;특정 컨테이너의 모든 로그 조회&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# docker logs [컨테이너 ID 또는 컨테이너명]
$ docker run -d nginx
$ docker logs [nginx가 실행되고 있는 컨테이너 ID]&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;최근 로그 10줄만 조회&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# dokcer logs --tail [로그 끝부터 표시할 줄 수] [컨테이너 ID 또는 컨테이너명]
$ dokcer logs --tail 10 [컨테이너 ID 또는 컨테이너명]&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;기존 로그 조회 + 생성되는 로그를 실시간으로 보고 싶은 경우&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# docker logs -f [컨테이너 ID 또는 컨테이너명]
# Nginx의 컨테이너에 실시간으로 쌓이는 로그 확인하기
$ docker run -d -p 80:80 nginx
$ docker logs -f&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;-f : follow의 약어&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;기존 로그는 조회하지 않기 + 생성되는 로그를 실시간으로 보고 싶은 경우&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ docker logs --tail 0 -f [컨테이너 ID 또는 컨테이너명]&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;실행 중인 컨테이너 내부에 접속하기&lt;/strong&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# docker exec -it 컨테이너명[또는 컨테이너 ID] bash

$ docker run -d nginx 
$ docker exec -it [Nginx가 실행되고 있는 컨테이너 ID] bash 
$ ls # 컨테이너 내부 파일 조회 
$ cd /etc/nginx 
$ cat nginx.conf&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;컨테이너 내부에서 나오려면 Ctrl + D 또는 exit을 입력하면 된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bash&lt;/code&gt; : 쉘(Shell)의 일종&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-it&lt;/code&gt; : &lt;code&gt;-it&lt;/code&gt;옵션을 사용해야 명령어를 입력하고 결과를 확인할 수 있다. &lt;code&gt;-it&lt;/code&gt;옵션을 적지 않으면 명령어를 1번만 실행시키고 종료되어 버린다. 즉, &lt;code&gt;-it&lt;/code&gt; 옵션을 적어야 계속해서 명령어를 입력할 수 있다.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;/li&gt;
&lt;li&gt;도커 이미지를 어디서 다운받았나?&lt;br&gt;Docker hub 에서 {이미지 명} 검색 -&amp;gt; 들어가보면 거기에 있는 이미지를 다운받은 것!&lt;/li&gt;
&lt;li&gt;Image에서 Tags 를 누르면 각 Image의 version을 볼 수 있다.&lt;/li&gt;
&lt;li&gt;Docker hub : GitHub 처럼 도커 이미지를 저장, 다운로드 할 수 있는 곳.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/DevOps</category>
      <category>CLI</category>
      <category>command</category>
      <category>Container</category>
      <category>docker</category>
      <category>image</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/3</guid>
      <comments>https://smink.tistory.com/entry/%EB%8F%84%EC%BB%A4Docker-%EB%B0%B0%EC%9A%B0%EA%B8%B0-2-%EC%9E%90%EC%A3%BC-%EC%93%B0%EB%8A%94-%EB%AA%85%EB%A0%B9%EC%96%B4Docker-CLI#entry3comment</comments>
      <pubDate>Fri, 4 Apr 2025 14:10:03 +0900</pubDate>
    </item>
    <item>
      <title>도커(Docker) 배우기 (1) - 컨테이너(Container), 이미지(Image) 란?</title>
      <link>https://smink.tistory.com/entry/%EB%8F%84%EC%BB%A4Docker-%EB%B0%B0%EC%9A%B0%EA%B8%B0-1-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88Container-%EC%9D%B4%EB%AF%B8%EC%A7%80Image-%EB%9E%80</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  Docker&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;docker.svg&quot; data-origin-width=&quot;659&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3Kr9a/btsM82xdNJF/jgpZJXQRJ9G2xGkWsDz9C1/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3Kr9a/btsM82xdNJF/jgpZJXQRJ9G2xGkWsDz9C1/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3Kr9a/btsM82xdNJF/jgpZJXQRJ9G2xGkWsDz9C1/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3Kr9a%2FbtsM82xdNJF%2FjgpZJXQRJ9G2xGkWsDz9C1%2Ftfile.svg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;398&quot; height=&quot;266&quot; data-filename=&quot;docker.svg&quot; data-origin-width=&quot;659&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;컨테이너(Container)&lt;/b&gt;&lt;/u&gt;를 사용하여 각각의 프로그램을 &lt;u&gt;&lt;b&gt;분리된 환경&lt;/b&gt;&lt;/u&gt;에서 실행 및 관리할 수 있는 툴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 컨테이너 (Container)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 컴퓨터 환경 내에서 미니 컴퓨터 &lt;b&gt;환경을 여러개 만들어서 분리해서 사용&lt;/b&gt;할 수 있도록 해둔 형태&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;ex) 윈도우에 사용자1, 사용자2가 있을 때 사용자1이 카톡을 깔았다고해서 사용자2한테 카톡이 안깔리는것 처럼 독립적인 환경&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;☑️ 컨테이너(Container)의 독립성&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;디스크 (저장 공간)&lt;/b&gt; : 각 컨테이너 마다 각자의 저장 공간 보유. 일반적으로 A 컨테이너 에서 B 컨테이너 내부 파일 접근 불가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크 (IP, Port)&lt;/b&gt; : 각 컨테이너 마다 고유의 네트워크를 가지고 있다. 컨테이너는 각자의 IP 주소를 가지고있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 이미지 (Image)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이미지(Image)&lt;/b&gt;는 프로그램을 실행하는 데 필요한 &lt;b&gt;설치 과정, 설정, 버전 정보 등을 포함&lt;/b&gt;하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; &lt;u&gt;&lt;b&gt;프로그램을 실행하는 데 필요한 모든 것을 포함&lt;/b&gt;&lt;/u&gt;하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;ex1) 닌텐도 게임 칩 처럼 하나의 게임기에 여러개 칩을 꽂아서 다양한 게임을 즐기는 것과 유사함&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;ex2)&amp;nbsp; SpringBoot 프로젝트를 이미지로 만든다면, 이 이미지를 Docker로 실행시키면 SpringBoot Server가 컨테이너(Container) 환경에서 복잡한 설치 과정을 거칠 필요 없이 손쉽게 실행 된다. &amp;nbsp;(= SpringBoot Server가 미니 컴퓨터 환경에서 실행된다.)&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발/DevOps</category>
      <category>Container</category>
      <category>docker</category>
      <category>image</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/2</guid>
      <comments>https://smink.tistory.com/entry/%EB%8F%84%EC%BB%A4Docker-%EB%B0%B0%EC%9A%B0%EA%B8%B0-1-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88Container-%EC%9D%B4%EB%AF%B8%EC%A7%80Image-%EB%9E%80#entry2comment</comments>
      <pubDate>Thu, 3 Apr 2025 17:19:19 +0900</pubDate>
    </item>
    <item>
      <title>Github Actions EC2 CI/CD 서버 멈춤 현상</title>
      <link>https://smink.tistory.com/entry/Github-Actions-EC2-CICD-%EC%84%9C%EB%B2%84-%EB%A9%88%EC%B6%A4-%ED%98%84%EC%83%81</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;githubActions.jpeg&quot; data-origin-width=&quot;1117&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmoLCz/btsM8VxUIOF/NjV3PgrkrPP8GVBjUk46Tk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmoLCz/btsM8VxUIOF/NjV3PgrkrPP8GVBjUk46Tk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmoLCz/btsM8VxUIOF/NjV3PgrkrPP8GVBjUk46Tk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmoLCz%2FbtsM8VxUIOF%2FNjV3PgrkrPP8GVBjUk46Tk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;472&quot; height=&quot;304&quot; data-filename=&quot;githubActions.jpeg&quot; data-origin-width=&quot;1117&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions으로 SpringBoot API Server의 CI/CD를 구현해놓았다.&lt;br /&gt;그런데 Merge를 하고나서 얼마안가 서버가 계속 멈추는 현상이 발생했다.&lt;br /&gt;사유는 다음과 같았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 발생 원인&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. EC2 RAM 사용량&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에서 평소 사용중인 메모리가 4기가 중에서 1.8~2.1기가 정도이다.&lt;br /&gt;Github Action을 통해 서버 내에서 SpringBoot 프로젝트를 빌드하는 과정에서 메모리 사용량이 늘어나면서 3기가 이상으로 늘어나면서 생긴 문제였다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. CI/CD 과정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 아직 초보 개발자라 이게 올바른 CI/CD 방법인가에 대한 고민은 한가득이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떠한 방법이 맞는지에 대한 정답?은 없다고들 하지만 최선은 있지않은가... 고수님들 알려주세요...ㅠㅠ&lt;br /&gt;현재 배포하는 과정은 다음과 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Main branch Merge -&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;&lt;b&gt;Github Actions&lt;/b&gt;&lt;/summary&gt;
&lt;div&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;name: Deploy Spring Boot App

on:
  push:
    branches: [ &quot;main&quot; ]

env:
  SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
  SERVER_USER: ${{ secrets.SERVER_USER }}
  SERVER_HOST: ${{ secrets.SERVER_HOST }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Execute deploy script on server
        run: |
          echo &quot;${SSH_PRIVATE_KEY}&quot; &amp;gt; private_key
          chmod 400 private_key
          ssh -i private_key -o StrictHostKeyChecking=no ${SERVER_USER}@${SERVER_HOST} 'bash /home/ubuntu/appServerDeploy.sh'
      - name: Clean up
        run: rm -f private_key&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;&lt;b&gt;Github Actions에 의한 deploy.sh 실행&lt;/b&gt;&lt;/summary&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash


# 소스 코드 최신화

cd ~/{projectRoot}/

echo &quot;git pull origin main&quot;

git pull origin main

echo &quot;springBoot Build &amp;amp;&amp;amp; nohup Deploy&quot;




# Spring Boot 프로젝트 빌드

cd ~/{projectRoot}

./gradlew build



# 기존 실행 중인 Spring Boot 애플리케이션 종료

SPRING_PORT=8080
SPRING_PID=$(lsof -t -i:$SPRING_PORT)
if [ -n &quot;$SPRING_PID&quot; ]; then
  echo &quot;SPRING_PID : $SPRING_PID&quot;
  echo &quot;Stopping existing Spring Boot application on port $SPRING_PORT (PID: $SPRING_PID)&quot;
  kill -9 $SPRING_PID
else
  echo &quot;No application running on port $SPRING_PORT&quot;
fi

# Spring Boot 프로젝트 실행
nohup java -jar ~/{projectRoot}/build/libs/app-0.0.1-SNAPSHOT.jar &amp;amp;



# 실행 중인 Spring Boot 애플리케이션 확인

echo &quot;Verifying Spring Boot application...&quot;
sleep 5
ps -ef | grep app-0.0.1-SNAPSHOT.jar&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이렇게 서버에서 빌드를 하고 배포를 하다보니 빌드 과정에서 서버의 메모리를 일시적으로 많이 쓴다는걸 확인했다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 프로젝트 빌드 시점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째로 생각한 해결 방법은 프로젝트 빌드를 AWS EC2 서버에서 하는것이 아니라, Github Action 내부에서 빌드 후 서버로 .jar를 옮겨주는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;.jar 파일을 GitHub Actions에서 빌드 후 서버로 옮겨주면 좋은 점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;EC2 서버의 메모리 부담 감소&lt;/b&gt;: 현재는 EC2에서 빌드 과정에서 메모리를 많이 사용하여 서버가 멈추는 문제가 발생했다. GitHub Actions에서 빌드하면 EC2의 메모리 사용량이 줄어든다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포 속도 향상&lt;/b&gt;: EC2에서 빌드를 진행하면 CPU와 메모리를 사용하면서 시간이 오래 걸릴 수 있다. GitHub Actions의 빌드 서버는 고성능 환경이므로 더 빠르게 빌드할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관된 빌드 환경 유지&lt;/b&gt;: 로컬 개발 환경, EC2 환경이 달라 발생할 수 있는 빌드 이슈를 방지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ &lt;b&gt;하지만 이렇게 하지 않고 기존 방식(EC2에서 빌드) 유지한 이유&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitHub Actions에서 빌드하려면 &lt;b&gt;빌드 결과물을 EC2로 전송하는 과정&lt;/b&gt;이 추가된다. (scp 명령어)&lt;/li&gt;
&lt;li&gt;EC2에서 바로 빌드하면 소스 코드와 빌드 결과물이 같은 환경에 존재하므로 관리가 편하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 스왑 메모리 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 설정을 보니 스왑메모리가 설정되어있지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스왑 메모리를 사용하여 RAM이 부족하더라도 서버가 멈추지 않게 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;스왑 메모리를 설정하면 좋은 점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메모리 부족 문제 완화&lt;/b&gt;: RAM이 부족할 때 스왑 메모리를 사용하면 프로세스가 종료되지 않고 지속적으로 실행될 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버 안정성 증가&lt;/b&gt;: 빌드 과정에서 순간적으로 메모리 사용량이 급증하는 경우에도 서버가 멈추지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비용 절감&lt;/b&gt;: EC2의 인스턴스 타입을 업그레이드하는 것보다 스왑 설정이 비용 측면에서 효율적일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ &lt;b&gt;하지만 스왑 메모리를 설정할 때 고려할 점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스왑은 디스크(I/O)를 사용하기 때문에 &lt;b&gt;성능이 저하될 가능성&lt;/b&gt;이 있다.&lt;/li&gt;
&lt;li&gt;스왑을 너무 크게 설정하면 불필요한 디스크 I/O가 증가하여 오히려 성능이 떨어질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 내가 선택한 해결 방법&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;스왑 메모리 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 스왑 메모리를 설정한 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선적으로는 기존 배포 프로세스를 변경하지 않고 간단한 설정 변경만으로 해결 가능했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 빌드뿐만 아니라 다른 작업에서도 언젠가는 발생할 수 있는 문제였기에 어느 시점에서는 꼭 필요한 작업이었다.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;&lt;b&gt;스왑 메모리 설정 방법&lt;/b&gt;&lt;/summary&gt;
&lt;div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;스왑 파일 생성&lt;/b&gt;&lt;br /&gt;먼저, 2GB 크기의 스왑 파일을 생성한다.&lt;br /&gt;&lt;code&gt;sudo fallocate -l 2G /swapfile&lt;/code&gt;&lt;br /&gt;만약 fallocate가 지원되지 않는 경우, dd 명령어를 사용할 수도 있다.&lt;br /&gt;&lt;code&gt;sudo dd if=/dev/zero of=/swapfile bs=1M count=2048&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스왑 파일 권한 변경&lt;/b&gt;&lt;br /&gt;보안 강화를 위해 스왑 파일의 권한을 600으로 설정한다.&lt;br /&gt;&lt;code&gt;sudo chmod 600 /swapfile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스왑 영역으로 설정&lt;/b&gt;&lt;br /&gt;생성한 파일을 스왑 영역으로 지정한다.&lt;br /&gt;&lt;code&gt;sudo mkswap /swapfile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스왑 활성화&lt;/b&gt;&lt;br /&gt;&lt;code&gt;sudo swapon /swapfile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스왑 적용 확인&lt;/b&gt;&lt;br /&gt;스왑이 정상적으로 설정되었는지 확인한다.&lt;br /&gt;&lt;code&gt;free -h  
또는
swapon --show &lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재부팅 후에도 유지되도록 설정&lt;/b&gt;&lt;br /&gt;EC2는 인스턴스를 재부팅하면 스왑 설정이 사라지므로, /etc/fstab에 설정을 추가해야 한다.&lt;br /&gt;&lt;code&gt;echo '/swapfile swap swap defaults 0 0' | sudo tee -a /etc/fstab&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 왜 deploy.sh와 GitHub Actions의 workflow를 변경하지 않았는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에서 빌드하는 방식도 충분히 가능하며, 단순히 메모리 문제를 해결하는 것이므로 굳이 구조를 변경할 필요가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 지금은 개발 초기단계에서 적용하는 부분에서 편리하기 위해 구현된 간단한 CI/CD이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;향후 Docker와 Kubernetes를 도입해 자동 복구(Self Recovery)를 구현할 계획이었기에 배포 스크립트와 github actions에 큰 힘을 쏟지 않아도 된다고 생각했다.&lt;/p&gt;</description>
      <category>개발/DevOps</category>
      <category>actions</category>
      <category>AWS</category>
      <category>CICD</category>
      <category>deploy</category>
      <category>EC2</category>
      <category>Github</category>
      <category>shellscript</category>
      <category>swapmemory</category>
      <author>smink</author>
      <guid isPermaLink="true">https://smink.tistory.com/1</guid>
      <comments>https://smink.tistory.com/entry/Github-Actions-EC2-CICD-%EC%84%9C%EB%B2%84-%EB%A9%88%EC%B6%A4-%ED%98%84%EC%83%81#entry1comment</comments>
      <pubDate>Wed, 2 Apr 2025 20:24:50 +0900</pubDate>
    </item>
  </channel>
</rss>