<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>os_js.log</title>
        <link>https://velog.io/</link>
        <description>조금씩 앞으로</description>
        <lastBuildDate>Tue, 14 Jul 2026 01:19:53 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>os_js.log</title>
            <url>https://velog.velcdn.com/images/os_js/profile/23e18a04-65aa-4494-93a3-445d5cb37eb3/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. os_js.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/os_js" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[파일 무결성 검사]]></title>
            <link>https://velog.io/@os_js/%ED%8C%8C%EC%9D%BC-%EB%AC%B4%EA%B2%B0%EC%84%B1-%EA%B2%80%EC%82%AC</link>
            <guid>https://velog.io/@os_js/%ED%8C%8C%EC%9D%BC-%EB%AC%B4%EA%B2%B0%EC%84%B1-%EA%B2%80%EC%82%AC</guid>
            <pubDate>Tue, 14 Jul 2026 01:19:53 GMT</pubDate>
            <description><![CDATA[<h1 id="agent에서-사용하는-파일-무결성-검증-방식">Agent에서 사용하는 파일 무결성 검증 방식</h1>
<p>Agent에서 맵 파일을 배포할 때는 파일을 다운로드한 뒤 무결성 검증을 수행한다.</p>
<p>여기서 무결성 검증이란 <strong>Agent가 다운로드한 파일이 서버에서 전달한 원본과 동일한지 확인하는 과정</strong>이다.</p>
<p>파일이 네트워크를 통해 전송되는 동안 일부 데이터가 손상되거나 잘못된 파일이 전달되면, 이후 압축 해제나 맵 로드 과정에서 문제가 발생할 수 있다.</p>
<p>이를 방지하기 위해 Agent는 파일을 다운로드한 직후 무결성을 확인한다. 검증에 실패하면 압축 해제나 맵 적용 단계로 넘어가지 않고 배포를 중단한다.</p>
<pre><code class="language-text">맵 파일 다운로드
        ↓
파일 무결성 검증
        ↓
검증 성공 여부 확인
   ┌────┴────┐
   │         │
 성공       실패
   │         │
압축 해제   배포 중단
   │
맵 로드</code></pre>
<hr>
<h2 id="파일-무결성을-확인하는-방법">파일 무결성을 확인하는 방법</h2>
<p>파일 무결성을 확인하는 방법에는 여러 가지가 있다.</p>
<p>대표적으로 다음과 같은 방식을 생각해볼 수 있다.</p>
<ul>
<li>CRC32와 같은 단순 체크섬</li>
<li>BLAKE3와 같은 암호학적 해시</li>
<li>Merkle Tree 기반 검증</li>
</ul>
<p>각 방식은 검증 목적과 파일 전달 구조에 따라 장단점이 다르다.</p>
<hr>
<h2 id="단순-체크섬">단순 체크섬</h2>
<p>체크섬은 파일 내용을 바탕으로 짧은 검증 값을 계산하는 방식이다.</p>
<p>서버가 원본 파일의 체크섬을 미리 계산하고, Agent가 파일을 다운로드한 뒤 같은 방식으로 다시 체크섬을 계산한다.</p>
<pre><code class="language-text">서버의 체크섬 값
        ↕ 비교
Agent가 계산한 체크섬 값</code></pre>
<p>두 값이 같으면 파일이 정상적으로 전달된 것으로 판단하고, 값이 다르면 전송 과정에서 파일이 손상되었거나 다른 파일이 전달된 것으로 판단한다.</p>
<p>CRC32와 같은 단순 체크섬은 계산 속도가 빠르고 구현이 간단하다. 따라서 네트워크 전송 중 발생할 수 있는 우발적인 데이터 손상을 확인하는 용도로 널리 사용된다.</p>
<p>하지만 파일 동일성을 더 강하게 확인해야 하는 상황에서는 한계가 있다.</p>
<p>단순 체크섬은 충돌 가능성이 상대적으로 높고, 의도적으로 변경된 파일을 검증하는 용도로 설계된 방식도 아니다.</p>
<p>따라서 중요한 배포 파일을 검증할 때는 일반적으로 암호학적 해시를 사용하는 것이 더 적합하다.</p>
<hr>
<h2 id="blake3">BLAKE3</h2>
<p>BLAKE3는 파일 내용을 기반으로 고정된 길이의 해시 값을 계산하는 암호학적 해시 함수다.</p>
<p>파일 내용이 아주 조금만 달라져도 이전과 전혀 다른 해시 값이 생성된다.</p>
<p>예를 들어 파일에서 한 바이트만 변경되더라도 계산 결과는 크게 달라진다.</p>
<pre><code class="language-text">원본 파일
→ 7b4c...

일부가 변경된 파일
→ a19f...</code></pre>
<p>서버와 Agent가 같은 파일을 대상으로 BLAKE3 해시를 계산했을 때 결과가 같다면, 두 파일의 내용이 동일하다고 판단할 수 있다.</p>
<p>BLAKE3의 주요 장점은 다음과 같다.</p>
<ul>
<li>대용량 파일에서도 빠르게 계산할 수 있다.</li>
<li>파일 내용 변경에 민감하다.</li>
<li>병렬 처리를 고려해 설계되었다.</li>
<li>단순 체크섬보다 충돌 가능성이 매우 낮다.</li>
<li>전체 파일 동일성 검증에 적합하다.</li>
</ul>
<p>즉, BLAKE3는 파일 검증의 정확성을 유지하면서도 처리 시간을 줄이기 좋은 방식이다.</p>
<hr>
<h2 id="merkle-tree">Merkle Tree</h2>
<p>Merkle Tree는 파일 전체를 하나의 값으로만 검증하지 않고, 파일을 여러 개의 블록으로 나눠 각각의 해시를 계산하는 방식이다.</p>
<p>각 블록의 해시는 다시 상위 해시로 결합되고, 최종적으로 하나의 Root Hash가 만들어진다.</p>
<pre><code class="language-text">             Root Hash
            /         \
       Hash A         Hash B
       /   \           /   \
  Block 1 Block 2  Block 3 Block 4</code></pre>
<p>Merkle Tree의 가장 큰 장점은 부분 검증이 가능하다는 것이다.</p>
<p>예를 들어 대용량 파일의 일부만 변경되었거나 손상된 경우, 전체 파일을 다시 비교하지 않고 문제가 있는 블록만 찾아낼 수 있다.</p>
<p>다음과 같은 환경에서는 Merkle Tree가 유리하다.</p>
<ul>
<li>파일 일부만 다시 다운로드하는 구조</li>
<li>블록 또는 Chunk 단위 데이터 검증</li>
<li>여러 노드에 데이터가 분산된 저장소</li>
<li>데이터 일부를 병렬로 검증하는 구조</li>
<li>변경된 부분만 동기화하는 시스템</li>
</ul>
<p>하지만 Merkle Tree를 사용하려면 파일 분할 기준, 블록별 해시, 트리 구성 정보 등을 별도로 관리해야 한다.</p>
<p>전체 파일을 한 번에 다운로드한 뒤 정상 여부만 확인하는 구조라면, 이러한 복잡도에 비해 얻을 수 있는 이점이 크지 않을 수 있다.</p>
<hr>
<h2 id="현재-agent에서-사용하는-방식">현재 Agent에서 사용하는 방식</h2>
<p>현재 Agent는 맵 파일의 무결성을 확인하기 위해 <strong>BLAKE3 기반 해시 검증</strong>을 사용한다.</p>
<p>검증 흐름은 다음과 같다.</p>
<pre><code class="language-text">1. 서버가 원본 맵 파일의 BLAKE3 해시를 계산한다.
2. Agent가 맵 파일과 해시 값을 전달받는다.
3. Agent가 다운로드한 파일의 BLAKE3 해시를 계산한다.
4. 서버에서 받은 해시와 Agent가 계산한 해시를 비교한다.
5. 값이 다르면 배포를 중단한다.
6. 값이 같으면 압축 해제와 맵 로드를 진행한다.</code></pre>
<p>검증은 압축 파일을 해제하기 전에 수행한다.</p>
<pre><code class="language-text">파일 다운로드
      ↓
BLAKE3 해시 계산
      ↓
서버 해시와 비교
      ↓
   일치 여부
   ┌──┴──┐
   │     │
 일치   불일치
   │     │
압축 해제 배포 실패
   │
맵 로드</code></pre>
<p>파일이 손상되었거나 원본과 다른 경우에는 후속 작업을 실행하지 않는다.</p>
<p>이를 통해 잘못된 파일이 실제 배포 경로로 진입하는 것을 방지할 수 있다.</p>
<hr>
<h2 id="단순-체크섬을-사용하지-않은-이유">단순 체크섬을 사용하지 않은 이유</h2>
<p>CRC32와 같은 단순 체크섬은 빠르고 구현하기 쉽다.</p>
<p>하지만 현재 Agent가 검증하는 파일은 단순한 로그나 임시 데이터가 아니다. 실제 Robot의 맵 배포에 사용되는 중요한 파일이다.</p>
<p>맵 파일에 문제가 생기면 단순히 압축 해제에 실패하는 것으로 끝나지 않을 수 있다.</p>
<ul>
<li>잘못된 맵이 로드될 수 있다.</li>
<li>기존 맵과 다른 데이터가 적용될 수 있다.</li>
<li>Robot의 위치 추정이나 주행에 영향을 줄 수 있다.</li>
<li>배포된 장비마다 서로 다른 파일이 적용될 수 있다.</li>
</ul>
<p>따라서 단순히 전송 과정에서 데이터가 깨졌는지를 확인하는 것보다, <strong>Agent가 받은 파일이 서버의 원본과 동일한지를 확인하는 것</strong>이 중요하다.</p>
<p>BLAKE3는 단순 체크섬보다 강한 동일성 검증을 제공하면서도 대용량 파일을 빠르게 처리할 수 있다.</p>
<p>이러한 이유로 현재 배포 구조에서는 속도와 검증 신뢰성 사이의 균형이 좋은 선택이라고 판단했다.</p>
<hr>
<h2 id="merkle-tree를-사용하지-않은-이유">Merkle Tree를 사용하지 않은 이유</h2>
<p>Merkle Tree는 부분 검증과 Chunk 단위 동기화에 강한 방식이다.</p>
<p>하지만 현재 Agent의 파일 배포 구조에서는 파일 일부만 다운로드하거나 변경된 블록만 다시 전송하지 않는다.</p>
<p>서버에서 하나의 배포 파일을 다운로드한 뒤, 파일 전체가 정상인지 확인하고 다음 단계로 넘어간다.</p>
<p>현재 Agent에 필요한 검증은 다음과 같다.</p>
<pre><code class="language-text">다운로드한 전체 파일이 서버의 원본과 같은가?</code></pre>
<p>반면 Merkle Tree는 다음과 같은 요구사항에서 더 큰 장점을 가진다.</p>
<pre><code class="language-text">파일의 어느 부분이 변경되었는가?
어떤 Chunk만 다시 다운로드해야 하는가?
일부 데이터만 빠르게 검증할 수 있는가?</code></pre>
<p>현재 구조에 Merkle Tree를 적용하면 블록별 해시와 트리 데이터를 추가로 관리해야 하지만, 실제 배포 과정에서 얻는 이점은 제한적이다.</p>
<p>따라서 Agent에서는 더 단순하고 관리하기 쉬운 BLAKE3 기반 전체 파일 검증을 선택했다.</p>
<hr>
<h2 id="blake3만으로-모든-보안-문제가-해결되는가">BLAKE3만으로 모든 보안 문제가 해결되는가</h2>
<p>BLAKE3 해시가 일치하면 Agent가 받은 파일의 내용이 서버에서 계산한 파일과 동일하다는 것을 확인할 수 있다.</p>
<p>다만 해시 값과 파일이 모두 신뢰할 수 없는 경로를 통해 함께 전달된다면, 공격자가 파일과 해시를 동시에 바꾸는 상황까지 막을 수는 없다.</p>
<pre><code class="language-text">파일만 변경
→ 해시 비교로 감지 가능

파일과 해시를 함께 변경
→ 별도의 신뢰 수단이 필요</code></pre>
<p>악의적인 파일 교체까지 방지해야 한다면 다음과 같은 방법을 추가로 고려할 수 있다.</p>
<ul>
<li>HTTPS 또는 TLS를 통한 전송 구간 보호</li>
<li>HMAC을 이용한 해시 인증</li>
<li>전자서명을 이용한 배포 파일 검증</li>
<li>신뢰할 수 있는 별도 경로로 해시 전달</li>
</ul>
<p>현재 BLAKE3 검증의 주된 목적은 다운로드 과정에서 발생한 손상이나 잘못된 파일 전달을 확인하는 것이다.</p>
<p>향후 배포 파일의 출처까지 검증해야 한다면 전자서명이나 HMAC 기반 검증을 추가할 수 있다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>파일 무결성을 확인하는 대표적인 방식으로는 단순 체크섬, BLAKE3, Merkle Tree가 있다.</p>
<p>단순 체크섬은 빠르고 구현하기 쉽지만, 주로 우발적인 데이터 손상을 감지하는 데 적합하다.</p>
<p>Merkle Tree는 파일을 여러 블록으로 나눠 부분적으로 검증할 수 있다는 장점이 있지만, 전체 파일을 하나의 단위로 배포하는 현재 구조에서는 복잡도가 높다.</p>
<p>현재 Agent는 맵 파일을 하나의 배포 단위로 다운로드한 뒤, 서버가 전달한 원본과 동일한지만 확인하면 된다.</p>
<p>따라서 Agent에서는 다음과 같은 이유로 BLAKE3를 사용한다.</p>
<ul>
<li>대용량 파일을 빠르게 처리할 수 있다.</li>
<li>파일의 작은 변경도 감지할 수 있다.</li>
<li>단순 체크섬보다 강한 동일성 검증이 가능하다.</li>
<li>Merkle Tree보다 구현과 운영이 단순하다.</li>
<li>현재의 전체 파일 배포 방식과 잘 맞는다.</li>
</ul>
<p>결국 현재 Agent의 무결성 검증은 복잡한 검증 시스템을 구축하기 위한 것이 아니다.</p>
<p>배포 과정에서 가장 중요한 다음 질문에 빠르고 정확하게 답하기 위한 방식이다.</p>
<blockquote>
<p>Agent가 다운로드한 파일은 서버가 전달한 원본 파일과 동일한가?</p>
</blockquote>
<h1 id="테스트">테스트..</h1>
<ul>
<li>merkle tree는 애초에 우리가 사용하고 있던 방식과 다르기 때문에 테스트를 진행하지 않았다.</li>
<li>이후 더욱 고도화를 위한 방식으로는 생각해볼 수 있다.
<img src="https://velog.velcdn.com/images/os_js/post/40ac7b52-f5b2-4eec-b0e1-3a7740c7feaa/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[zenoh 사용(nestjs)]]></title>
            <link>https://velog.io/@os_js/zenoh-%EC%82%AC%EC%9A%A9nestjs</link>
            <guid>https://velog.io/@os_js/zenoh-%EC%82%AC%EC%9A%A9nestjs</guid>
            <pubDate>Mon, 13 Jul 2026 05:39:36 GMT</pubDate>
            <description><![CDATA[<h1 id="nestjs에서-zenoh-remote-api와-websocket으로-통신하는-방식">NestJS에서 Zenoh Remote API와 WebSocket으로 통신하는 방식</h1>
<p>현재 NestJS 서버는 Zenoh 네트워크와 통신하기 위해 <code>zenoh-plugin-remote-api</code>를 사용한다.</p>
<p>여기서 중요한 점은 NestJS가 Zenoh Router의 Native WebSocket Listener에 직접 연결되는 것이 아니라, <strong>Remote API가 제공하는 WebSocket Endpoint에 연결한다는 것</strong>이다.</p>
<p>전체적인 구조는 다음과 같다.</p>
<pre><code class="language-text">NestJS
  │
  │ WebSocket
  ▼
zenoh-plugin-remote-api
  │
  ▼
Zenoh Router
  │
  ▼
Robot / Edge Device</code></pre>
<p>NestJS에서는 <code>@eclipse-zenoh/zenoh-ts</code> 패키지를 사용해 Zenoh Session을 생성한다.</p>
<p>코드상으로는 NestJS가 Zenoh Session을 직접 사용하는 것처럼 보이지만, 내부적으로는 Remote API Endpoint와 WebSocket 연결을 생성하고 그 연결을 통해 Zenoh 요청과 메시지를 주고받는다.</p>
<hr>
<h2 id="왜-websocket을-사용하는가">왜 WebSocket을 사용하는가</h2>
<p><code>@eclipse-zenoh/zenoh-ts</code>는 Node.js와 TypeScript 환경에서 Zenoh를 사용하기 위한 클라이언트 라이브러리다.</p>
<p>현재 프로젝트에서는 이 클라이언트가 <code>zenoh-plugin-remote-api</code>의 WebSocket Endpoint에 연결하는 방식으로 Zenoh를 사용한다.</p>
<p>따라서 NestJS 코드에서 <code>Session.open()</code>을 호출하면 내부적으로 다음과 같은 과정이 진행된다.</p>
<pre><code class="language-text">Session.open()
  │
  ▼
Remote API WebSocket 연결 생성
  │
  ▼
Zenoh Session 생성
  │
  ▼
Zenoh 기능 사용 가능</code></pre>
<p>Session이 생성된 이후에는 해당 Session을 통해 다음과 같은 Zenoh 기능을 사용할 수 있다.</p>
<ul>
<li>Publish</li>
<li>Subscribe</li>
<li>Query</li>
<li>Liveliness</li>
</ul>
<p>즉, 애플리케이션에서는 Zenoh API를 사용하지만 실제 통신은 Remote API와 연결된 WebSocket 위에서 이루어진다.</p>
<hr>
<h2 id="nestjs에서는-어떻게-연결하는가">NestJS에서는 어떻게 연결하는가</h2>
<p>NestJS에서는 <code>RobotZenohService</code>가 Zenoh 연결을 담당한다.</p>
<p>이 서비스는 <code>@eclipse-zenoh/zenoh-ts</code>를 불러오고, 설정된 Remote API 주소를 기반으로 Zenoh Session을 생성한다.</p>
<p>전체적인 초기화 흐름은 다음과 같다.</p>
<pre><code class="language-text">RobotZenohService 초기화
  │
  ▼
zenoh-ts 로드
  │
  ▼
Remote API WebSocket Endpoint 설정
  │
  ▼
Session.open()
  │
  ▼
Subscriber 등록
  │
  ▼
Robot 메시지 수신 시작</code></pre>
<p><code>Session.open()</code>이 성공하면 NestJS는 Zenoh Session을 사용할 수 있는 상태가 된다.</p>
<p>이후 해당 Session을 이용해 Robot이 보내는 데이터를 구독하거나 Robot으로 명령을 발행한다.</p>
<p>예를 들어 Robot의 상태 정보를 수신하기 위해 다음과 같은 Key Expression을 구독할 수 있다.</p>
<pre><code class="language-text">robot/{robotId}/state
robot/{robotId}/navigation/state</code></pre>
<p>서버에서 Robot으로 명령을 전달할 때도 같은 Session을 사용한다.</p>
<pre><code class="language-text">robot/{robotId}/command/move
robot/{robotId}/command/path</code></pre>
<p>이 토픽들은 실제 프로젝트 토픽이 아니라 통신 구조를 설명하기 위한 예시다.</p>
<p>NestJS 코드에서는 일반적인 Zenoh API를 사용하는 것처럼 보이지만, 실제 메시지는 Remote API와 연결된 WebSocket을 통해 전달된다.</p>
<hr>
<h2 id="websocket-연결-상태를-별도로-감지하는-이유">WebSocket 연결 상태를 별도로 감지하는 이유</h2>
<p>Remote API 연결은 WebSocket을 기반으로 하기 때문에 네트워크 문제나 Remote API 재시작 등으로 인해 연결이 끊길 수 있다.</p>
<p>일반적으로는 Zenoh Session 상태를 통해 연결 종료를 감지할 수 있다. 하지만 일부 비정상적인 단절 상황에서는 Session 상태만으로 연결 문제를 즉시 확인하기 어려울 수 있다.</p>
<p>이를 보완하기 위해 현재 구조에서는 <code>zenoh-ts</code> 내부에서 사용하는 WebSocket의 <code>close</code> 및 <code>error</code> 이벤트도 함께 감지한다.</p>
<pre><code class="language-text">Zenoh Session 상태 확인
  │
  ├─ Session 오류 감지
  │
  └─ WebSocket close/error 감지
             │
             ▼
       연결 종료 처리</code></pre>
<p>이처럼 Session과 내부 WebSocket 상태를 함께 확인하면 Remote API 연결이 비정상적으로 종료된 경우에도 NestJS가 빠르게 문제를 감지할 수 있다.</p>
<hr>
<h2 id="연결이-끊기면-어떻게-처리하는가">연결이 끊기면 어떻게 처리하는가</h2>
<p>Remote API와의 WebSocket 연결이 끊기면 기존 Zenoh 리소스를 정리하고 새로운 연결을 생성한다.</p>
<p>재연결 과정은 다음과 같다.</p>
<pre><code class="language-text">WebSocket close/error 감지
  │
  ▼
기존 Zenoh Session 종료
  │
  ▼
등록된 Subscriber 정리
  │
  ▼
일정 시간 대기
  │
  ▼
Remote API Endpoint로 재연결
  │
  ▼
Subscriber 재등록</code></pre>
<p>기존 Session과 Subscriber를 먼저 정리하는 이유는 재연결 과정에서 동일한 Subscriber가 중복으로 등록되는 것을 방지하기 위해서다.</p>
<p>Subscriber가 중복으로 등록되면 하나의 메시지가 여러 번 처리되거나 동일한 이벤트가 반복해서 발생할 수 있다.</p>
<p>따라서 재연결 전에는 기존 연결과 관련된 리소스를 확실하게 정리해야 한다.</p>
<hr>
<h2 id="재연결-간격을-두는-이유">재연결 간격을 두는 이유</h2>
<p>연결이 실패했다고 해서 즉시 무한 반복으로 재연결을 시도하지는 않는다.</p>
<p>Remote API나 Zenoh Router가 재시작 중이라면 짧은 시간 동안 연결할 수 없는 상태가 계속될 수 있다. 이때 반복적으로 연결을 시도하면 불필요한 CPU 및 네트워크 사용이 발생한다.</p>
<p>이를 방지하기 위해 재연결 시도 사이에 대기 시간을 두는 Backoff 방식을 사용한다.</p>
<pre><code class="language-text">첫 번째 실패
  ↓
짧은 시간 대기

연속 실패
  ↓
대기 시간 증가

연결 성공
  ↓
대기 시간 초기화</code></pre>
<p>처음에는 짧은 간격으로 재연결을 시도하고, 실패가 반복되면 점차 대기 시간을 늘린다.</p>
<p>연결에 성공하면 재연결 횟수와 대기 시간을 초기화하고 정상적인 메시지 구독을 다시 시작한다.</p>
<hr>
<h2 id="health-check를-함께-사용하는-이유">Health Check를 함께 사용하는 이유</h2>
<p>WebSocket의 <code>close</code>와 <code>error</code> 이벤트만으로 모든 연결 장애를 감지할 수 있는 것은 아니다.</p>
<p>TCP 또는 WebSocket 연결이 형식적으로는 남아 있지만, 실제로 메시지를 주고받을 수 없는 비정상 상태가 발생할 수 있기 때문이다.</p>
<p>이를 확인하기 위해 NestJS에서는 주기적으로 가벼운 Zenoh 요청을 보내 연결 상태를 검사한다.</p>
<pre><code class="language-text">주기적으로 Health Check 요청
  │
  ├─ 성공 → 연결 유지
  │
  └─ 실패 → 연결 이상으로 판단
                 │
                 ▼
          Session 정리 및 재연결</code></pre>
<p>Health Check는 WebSocket 이벤트가 발생하지 않은 채 연결이 비정상적으로 멈춰 있는 상황을 감지하기 위한 보조 수단이다.</p>
<p>따라서 현재 구조에서는 다음과 같은 신호를 함께 사용해 연결 상태를 판단한다.</p>
<ul>
<li>Zenoh Session 오류</li>
<li>WebSocket <code>close</code> 이벤트</li>
<li>WebSocket <code>error</code> 이벤트</li>
<li>Health Check 실패</li>
<li>Publish 또는 Query 실패</li>
</ul>
<p>여러 감지 방식을 함께 사용하면 네트워크 장애나 Remote API 재시작 상황에 더 안정적으로 대응할 수 있다.</p>
<hr>
<h2 id="전체-연결-흐름">전체 연결 흐름</h2>
<p>NestJS와 Zenoh 사이의 전체 연결 흐름을 정리하면 다음과 같다.</p>
<pre><code class="language-text">┌──────────────────────────┐
│          NestJS          │
│                          │
│  RobotZenohService       │
│  Session / Subscriber    │
└────────────┬─────────────┘
             │
             │ WebSocket
             ▼
┌──────────────────────────┐
│  zenoh-plugin-remote-api │
└────────────┬─────────────┘
             │
             │ Zenoh
             ▼
┌──────────────────────────┐
│       Zenoh Router       │
└────────────┬─────────────┘
             │
             ▼
┌──────────────────────────┐
│   Robot / Edge Device    │
└──────────────────────────┘</code></pre>
<p>NestJS가 직접 Zenoh Router의 Native Transport에 연결되는 것은 아니다.</p>
<p>NestJS는 Remote API가 제공하는 WebSocket Endpoint에 연결하고, Remote API가 NestJS의 요청을 Zenoh 네트워크로 전달한다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>현재 NestJS 서버는 Zenoh와 통신하기 위해 <code>zenoh-plugin-remote-api</code>의 WebSocket Endpoint를 사용한다.</p>
<p>NestJS에서는 <code>@eclipse-zenoh/zenoh-ts</code>를 통해 Session을 생성하며, 생성된 Session으로 Publish, Subscribe, Query 등의 Zenoh 기능을 사용한다.</p>
<p>코드에서는 일반적인 Zenoh API를 사용하는 것처럼 보이지만, 실제 통신은 다음 경로를 거친다.</p>
<pre><code class="language-text">NestJS
  ↓
Remote API WebSocket
  ↓
Zenoh Router
  ↓
Robot / Edge Device</code></pre>
<p>또한 WebSocket 연결은 네트워크 문제나 Remote API 재시작으로 끊길 수 있기 때문에 다음과 같은 연결 감지 및 복구 기능을 함께 사용한다.</p>
<ul>
<li>Zenoh Session 오류 감지</li>
<li>내부 WebSocket의 <code>close</code> 및 <code>error</code> 이벤트 감지</li>
<li>주기적인 Health Check</li>
<li>기존 Session과 Subscriber 정리</li>
<li>Backoff 기반 자동 재연결</li>
<li>재연결 이후 Subscriber 재등록</li>
</ul>
<p>이 구조의 핵심은 <strong>NestJS가 Zenoh Router에 직접 연결되는 것이 아니라, Remote API의 WebSocket 연결을 통해 Zenoh 네트워크에 접근한다는 점</strong>이다.</p>
<h3 id="번외">번외..</h3>
<p>zenoh remote api websocket연결 이벤트 핸들러 등록...</p>
<ul>
<li>우리가 zenoh remote api와 websocket 연결을 할때, 기본적인 websocket 연결에 대한 이벤트 제어를 할 수 없다.왜냐하면 websocket관련 로직은 zenoh-ts 모듈에 존재하기 때문이다.</li>
<li>session연결시 단순 <code>zenoh.session()</code> 형태로 사용되기 때문에 우리는 zenoh session값을 받을뿐 websocket에 대한 내용을 가져올 순 없다.</li>
<li>여기서 우리는 전역으로 websocket을 만들때, proxy로 감싸서 websocket 생성시 websocket 자체를 우리가 다룰 수 있게 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Zenoh]]></title>
            <link>https://velog.io/@os_js/Zenoh</link>
            <guid>https://velog.io/@os_js/Zenoh</guid>
            <pubDate>Sun, 12 Jul 2026 07:05:40 GMT</pubDate>
            <description><![CDATA[<h1 id="zenoh란-무엇인가-로봇과-iot-통신에서-주목받는-이유">Zenoh란 무엇인가? 로봇과 IoT 통신에서 주목받는 이유</h1>
<p>로봇이나 IoT 시스템을 개발하다 보면 여러 장치와 서비스 사이에서 데이터를 실시간으로 전달해야 하는 상황이 자주 발생한다.</p>
<p>예를 들어 자율주행 로봇 시스템에서는 다음과 같은 데이터가 끊임없이 오간다.</p>
<ul>
<li>로봇의 현재 위치</li>
<li>배터리 상태</li>
<li>이동 명령과 실행 결과</li>
<li>장애물 및 센서 데이터</li>
<li>장치 연결 상태</li>
<li>지도와 작업 정보</li>
</ul>
<p>처음에는 HTTP나 WebSocket만으로도 시스템을 구성할 수 있다. 하지만 연결되는 로봇과 서비스가 많아지면 서버가 각 연결과 메시지 전달 경로를 직접 관리해야 하고, 요청·응답과 연결 상태 확인을 위한 별도의 규칙도 필요해진다.</p>
<p>이러한 분산 환경에서 사용할 수 있는 통신 프로토콜 중 하나가 <strong>Zenoh</strong>다.</p>
<h2 id="zenoh란">Zenoh란?</h2>
<p>Zenoh는 데이터를 발행하고 구독하는 <code>Pub/Sub</code> 방식과 필요한 데이터를 요청하는 <code>Query</code> 방식을 하나의 프로토콜에서 제공한다.</p>
<p>Zenoh 공식 문서에서는 Zenoh를 다음 요소를 통합하는 Pub/Sub/Query 프로토콜로 설명한다.</p>
<ul>
<li>이동 중인 데이터</li>
<li>저장된 데이터</li>
<li>데이터에 대한 연산</li>
</ul>
<p>즉, 단순히 메시지를 전달하는 통신 채널을 넘어 실시간 데이터 전달, 데이터 조회, 저장소 연동을 하나의 데이터 공간에서 다루도록 설계된 프로토콜이다.</p>
<p>Zenoh라는 이름은 <strong>Zero Overhead Network Protocol</strong>에서 유래했다. 여기서 Zero Overhead는 실제 오버헤드가 완전히 0이라는 의미보다는, 다양한 네트워크와 장치 환경에서 통신 비용을 작게 유지하겠다는 설계 방향을 나타낸다고 이해하는 편이 적절하다.</p>
<p>Zenoh는 Eclipse Foundation에서 관리되는 오픈소스 프로젝트이며, 데이터센터와 클라우드뿐만 아니라 엣지 장치, 로봇, IoT 장비와 같은 제한된 환경까지 연결하는 것을 목표로 한다. 실제 활용 분야로는 로보틱스, 자동차, IoT, 엣지 컴퓨팅 등이 소개되고 있다.</p>
<h2 id="zenoh의-기본-데이터-구조">Zenoh의 기본 데이터 구조</h2>
<p>Zenoh에서는 데이터를 경로 형태의 키로 구분한다.</p>
<p>예를 들어 로봇의 상태를 다음과 같은 키로 표현할 수 있다.</p>
<pre><code class="language-text">robot/A001/status
robot/A001/battery
robot/A001/position
robot/A001/move/result</code></pre>
<p>Zenoh는 이러한 키와 값을 기반으로 데이터를 전달한다. 값에는 문자열, JSON, 바이너리 데이터 등 다양한 형식을 사용할 수 있다.</p>
<p>특정 로봇의 배터리 상태를 받고 싶다면 다음 키를 구독할 수 있다.</p>
<pre><code class="language-text">robot/A001/battery</code></pre>
<p>모든 로봇의 상태를 받고 싶다면 와일드카드가 포함된 Key Expression을 사용할 수 있다.</p>
<pre><code class="language-text">robot/*/status</code></pre>
<p>또는 로봇과 관련된 모든 데이터를 구독할 수도 있다.</p>
<pre><code class="language-text">robot/**</code></pre>
<p>Zenoh Router는 메시지의 Key Expression을 기준으로 데이터를 필요한 Subscriber에게 전달한다. 이 덕분에 Publisher는 데이터를 누가 받는지 직접 알 필요가 없다.</p>
<h2 id="zenoh의-주요-통신-방식">Zenoh의 주요 통신 방식</h2>
<h3 id="1-publishsubscribe">1. Publish/Subscribe</h3>
<p>Publisher가 특정 키로 데이터를 발행하면 해당 키를 구독하고 있는 Subscriber가 데이터를 전달받는다.</p>
<pre><code class="language-text">AMR
  └─ robot/A001/status 발행
                  ├─ 관제 서버
                  ├─ 상태 관리 서비스
                  ├─ 로그 수집 서비스
                  └─ 모니터링 UI</code></pre>
<p>로봇은 자신의 데이터를 누가 사용하는지 알 필요가 없다. 새로운 서비스가 추가되더라도 로봇 프로그램을 수정하지 않고 동일한 키를 구독하면 된다.</p>
<p>이러한 구조를 사용하면 데이터 생산자와 소비자 사이의 의존성을 낮출 수 있다.</p>
<h3 id="2-queryreply">2. Query/Reply</h3>
<p>Pub/Sub는 실시간으로 발생하는 데이터를 전달하는 데 적합하다. 하지만 특정 시점의 정보가 필요할 때는 Query를 사용할 수 있다.</p>
<p>예를 들어 서버가 특정 로봇의 현재 네트워크 설정을 조회한다고 가정해 보자.</p>
<pre><code class="language-text">Query:
robot/A001/network/current</code></pre>
<p>로봇이나 Agent는 해당 키에 Queryable을 등록하고 요청을 받았을 때 결과를 반환한다.</p>
<p>WebSocket으로 같은 기능을 구현하려면 일반적으로 요청 ID, 응답 매칭, 타임아웃, 오류 응답 규칙을 직접 설계해야 한다. Zenoh에서는 Query와 Queryable이라는 추상화를 통해 요청·응답 통신을 제공한다.</p>
<h3 id="3-storage">3. Storage</h3>
<p>Zenoh Storage는 특정 Key Expression을 구독하여 데이터를 저장하고, Query 요청이 들어왔을 때 저장된 최신 값을 반환할 수 있다.</p>
<p>예를 들어 로봇이 주기적으로 상태를 발행한다고 가정해 보자.</p>
<pre><code class="language-text">robot/A001/status</code></pre>
<p>이 데이터를 Storage에 저장하면 새롭게 접속한 서비스가 Query를 통해 최근 상태를 조회할 수 있다. 이후에는 Subscriber를 통해 실시간 변경을 계속 전달받을 수 있다.</p>
<p>Zenoh의 Storage는 Subscriber와 Queryable을 결합한 구조로 설명할 수 있다. 다만 Zenoh Router를 실행한다고 해서 데이터가 자동으로 영구 저장되는 것은 아니며, Storage Plugin이나 별도의 저장소 구성이 필요하다.</p>
<h2 id="zenoh의-장점">Zenoh의 장점</h2>
<h3 id="데이터-생산자와-소비자를-분리할-수-있다">데이터 생산자와 소비자를 분리할 수 있다</h3>
<p>WebSocket 기반 시스템에서는 서버가 연결된 클라이언트를 직접 관리하는 경우가 많다.</p>
<pre><code class="language-text">robotId → WebSocket 연결</code></pre>
<p>특정 로봇의 데이터를 여러 서비스에 전달하려면 서버가 수신자를 찾아 각각 메시지를 보내야 한다.</p>
<p>Zenoh에서는 Publisher가 Key Expression을 기준으로 데이터를 발행하고 Subscriber가 필요한 데이터를 선택해서 구독한다.</p>
<pre><code class="language-text">Publisher → Zenoh Network → Subscriber</code></pre>
<p>Publisher는 Subscriber의 주소, 개수 또는 위치를 몰라도 된다. 새로운 모니터링 서비스나 분석 서비스가 추가되어도 기존 Publisher를 수정할 필요가 줄어든다.</p>
<h3 id="pubsub와-요청·응답을-함께-사용할-수-있다">Pub/Sub와 요청·응답을 함께 사용할 수 있다</h3>
<p>분산 시스템에서는 실시간 데이터뿐 아니라 현재 상태를 조회해야 하는 경우도 많다.</p>
<p>일반적인 시스템에서는 역할에 따라 여러 기술을 조합한다.</p>
<pre><code class="language-text">실시간 상태 전달 → WebSocket
상태 조회       → HTTP API
메시지 전달     → Message Broker
최근 값 조회    → Redis 또는 Database</code></pre>
<p>Zenoh에서는 Pub/Sub, Query/Reply, Storage를 동일한 Key/Value 데이터 공간에서 사용할 수 있다.</p>
<p>물론 Zenoh가 데이터베이스나 HTTP API를 모두 대체한다는 의미는 아니다. 다만 장치와 서비스 간 통신에서 사용해야 하는 프로토콜과 데이터 모델을 줄일 수 있다는 장점이 있다.</p>
<h3 id="다양한-네트워크-구조를-지원한다">다양한 네트워크 구조를 지원한다</h3>
<p>Zenoh 애플리케이션은 시스템 환경에 따라 Client, Peer, Router 중심 구조로 배치할 수 있다.</p>
<pre><code class="language-text">Peer ↔ Peer</code></pre>
<pre><code class="language-text">Client → Router ← Client</code></pre>
<pre><code class="language-text">Robot → Edge Router → Central Router → Server</code></pre>
<p>Peer 모드에서는 다른 Peer 또는 Router를 탐색하여 연결할 수 있다. Client 모드에서는 하나 이상의 Router Endpoint를 지정할 수 있다.</p>
<p>이러한 구조는 로봇 내부 네트워크, 공장 내부 네트워크, 원격 서버가 함께 존재하는 시스템에서 유용하다.</p>
<p>다만 Zenoh를 사용한다고 해서 자동으로 장애에 강한 시스템이 되는 것은 아니다. 하나의 Router에 모든 Client가 의존한다면 그 Router는 여전히 단일 장애 지점이 될 수 있다. 고가용성이 필요하다면 여러 Router 연결과 재접속 정책을 별도로 설계해야 한다.</p>
<h3 id="계층적인-key-expression을-사용할-수-있다">계층적인 Key Expression을 사용할 수 있다</h3>
<p>Zenoh에서는 데이터를 단순한 문자열 Topic이 아니라 계층적인 Key Expression으로 관리할 수 있다.</p>
<pre><code class="language-text">factory/line1/robot/A001/status
factory/line1/robot/A002/status
factory/line2/robot/A003/status</code></pre>
<p>이 구조를 사용하면 서비스마다 필요한 범위만 구독할 수 있다.</p>
<pre><code class="language-text">factory/line1/robot/*/status
factory/**/robot/A001/**
factory/line2/**</code></pre>
<p>로봇 수와 서비스 수가 증가해도 데이터의 의미와 범위를 비교적 일관된 규칙으로 관리할 수 있다.</p>
<h3 id="장치와-서비스의-연결-상태를-표현할-수-있다">장치와 서비스의 연결 상태를 표현할 수 있다</h3>
<p>Zenoh는 Liveliness 기능을 제공한다. 애플리케이션이 특정 Key Expression에 Liveliness Token을 선언하면 다른 서비스가 해당 Token의 생성과 삭제를 관찰할 수 있다.</p>
<p>예를 들어 다음과 같은 키를 사용할 수 있다.</p>
<pre><code class="language-text">robot/A001/liveliness</code></pre>
<p>이를 통해 로봇이나 Agent가 현재 Zenoh 네트워크에 참여하고 있는지 추적할 수 있다. Zenoh의 접근 제어 문서에서도 Liveliness Token이 별도의 메시지 유형으로 다뤄지고 있다.</p>
<p>다만 실제 운영 환경에서는 Liveliness 하나만으로 모든 장애를 판별하기보다 다음 방법을 함께 사용하는 것이 안전하다.</p>
<pre><code class="language-text">Liveliness
+ 마지막 메시지 수신 시간
+ Heartbeat
+ 애플리케이션 Watchdog</code></pre>
<p>전원 강제 종료나 네트워크 단절에서는 장애 감지까지 시간이 걸릴 수 있기 때문이다.</p>
<h3 id="엣지-장치와-저사양-장치를-고려할-수-있다">엣지 장치와 저사양 장치를 고려할 수 있다</h3>
<p>Zenoh는 Rust, Python, C, C++, Java, Kotlin 등의 API를 제공하며, 제한된 장치를 위한 C 기반 Zenoh-Pico도 제공한다.</p>
<p>따라서 중앙 서버뿐 아니라 로봇 제어 PC, 임베디드 장치, 센서 노드 등 다양한 환경에서 같은 데이터 모델을 사용할 수 있다.</p>
<h2 id="websocket과는-무엇이-다른가">WebSocket과는 무엇이 다른가?</h2>
<p>WebSocket은 하나의 TCP 연결에서 클라이언트와 서버가 양방향 메시지를 주고받기 위한 프로토콜이다. HTTP 기반 핸드셰이크 후 기본적인 메시지 프레이밍을 제공한다.</p>
<p>반면 Zenoh는 메시지를 주고받는 연결뿐 아니라 다음 기능을 함께 제공한다.</p>
<ul>
<li>Key Expression 기반 라우팅</li>
<li>Publisher와 Subscriber</li>
<li>Query와 Queryable</li>
<li>분산 Storage 연동</li>
<li>Peer 및 Router 기반 배포</li>
<li>Liveliness</li>
<li>Discovery</li>
</ul>
<p>따라서 둘은 완전히 같은 계층의 기술이라고 보기 어렵다.</p>
<pre><code class="language-text">WebSocket
= 연결된 두 프로그램 사이의 양방향 통신 채널

Zenoh
= 다수의 장치와 서비스를 연결하기 위한 분산 데이터 통신 체계</code></pre>
<p>WebSocket에서도 Pub/Sub나 요청·응답 기능을 구현할 수 있다. 하지만 연결 관리, Topic 규칙, 요청 ID, 타임아웃, 재연결, 메시지 라우팅 등을 애플리케이션이나 서버에서 직접 설계해야 한다.</p>
<p>Zenoh는 이러한 분산 통신 기능을 프로토콜 수준의 추상화로 제공한다.</p>
<p>반대로 브라우저와 웹 서버 사이의 단순한 실시간 통신에서는 WebSocket이 더 간단하고 적합할 수 있다. 브라우저는 WebSocket을 기본적으로 지원하지만, Zenoh를 웹 환경에서 사용하려면 REST Plugin이나 별도의 Bridge가 필요할 수 있다. Zenoh는 REST Plugin을 통해 Key/Value 공간에 HTTP로 접근하는 방법도 제공한다.</p>
<p>따라서 모든 WebSocket을 Zenoh로 교체하기보다는 시스템의 역할에 따라 기술을 구분하는 것이 좋다.</p>
<pre><code class="language-text">로봇·Agent·백엔드 서비스 간 통신
→ Zenoh

브라우저 UI와 백엔드 간 실시간 통신
→ WebSocket 또는 Socket.IO

대용량 파일 다운로드
→ HTTP 또는 Object Storage

파일 배포 명령과 진행 상태 전달
→ Zenoh</code></pre>
<h2 id="zenoh가-적합한-경우">Zenoh가 적합한 경우</h2>
<p>Zenoh는 다음과 같은 환경에서 특히 유용하다.</p>
<ul>
<li>여러 대의 로봇이나 IoT 장치가 연결되는 시스템</li>
<li>하나의 데이터를 여러 서비스가 소비하는 시스템</li>
<li>실시간 상태 전달과 요청·응답이 모두 필요한 시스템</li>
<li>로컬 네트워크와 원격 서버를 연결해야 하는 시스템</li>
<li>중앙 서버뿐 아니라 엣지 장치 간 통신이 필요한 시스템</li>
<li>서비스가 지속해서 추가되거나 변경되는 분산 시스템</li>
</ul>
<p>반대로 서버 한 대와 브라우저 몇 개가 연결되는 단순한 시스템이라면 WebSocket만으로도 충분할 수 있다. Zenoh를 도입하면 Router 운영, Key Expression 설계, 네트워크 모드 설정 등 추가로 관리해야 할 요소가 생기기 때문이다.</p>
<h2 id="마무리">마무리</h2>
<p>Zenoh는 단순히 WebSocket보다 빠른 통신 기술이라고 설명하기에는 부족하다.</p>
<p>Zenoh의 핵심적인 장점은 <strong>다수의 장치와 서비스가 데이터를 발행하고, 구독하고, 조회할 수 있는 공통 데이터 공간을 제공한다는 점</strong>이다.</p>
<p>WebSocket이 연결된 두 프로그램 사이의 통신 채널이라면 Zenoh는 로봇, Agent, 서버, Storage와 같은 여러 구성 요소를 느슨하게 연결하는 데이터 통신 백본에 가깝다.</p>
<p>특히 로봇과 IoT 시스템처럼 장치 수가 많고 네트워크 구조가 복잡하며, 동일한 데이터를 여러 서비스에서 사용해야 하는 환경에서는 Zenoh의 Pub/Sub, Query, Key Expression, Liveliness 기능이 시스템 구조를 단순화하는 데 도움이 될 수 있다.</p>
<p>다만 Zenoh를 도입한다고 해서 연결 복구, 데이터 유실 처리, Router 이중화와 같은 운영 문제가 자동으로 해결되는 것은 아니다. Zenoh가 제공하는 기능을 이해하고 시스템 특성에 맞는 장애 처리와 데이터 정책을 함께 설계해야 한다.</p>
<p>결국 Zenoh의 가치는 단순한 메시지 전송 성능보다 다음 문장으로 정리할 수 있다.</p>
<blockquote>
<p>Zenoh는 분산된 장치와 서비스가 데이터의 위치와 상대방의 연결 구조에 덜 의존하면서 통신할 수 있도록 만들어 주는 프로토콜이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[scp window to rhel]]></title>
            <link>https://velog.io/@os_js/scp-window-to-rhel</link>
            <guid>https://velog.io/@os_js/scp-window-to-rhel</guid>
            <pubDate>Thu, 19 Mar 2026 05:47:07 GMT</pubDate>
            <description><![CDATA[<h1 id="윈도우-to-rhel-파일-전송">윈도우 to RHEL 파일 전송.</h1>
<h2 id="문제-발생">문제 발생.</h2>
<p>윈도우에서 <code>scp</code> 를 통해서 원격 서버의 특정 폴더로 파일들을 전송하려고 하였음.
하지만 아래와 같은 오류 발생
<img src="https://velog.velcdn.com/images/os_js/post/d582cf6c-37fc-4b44-b52e-26f0e0a83cce/image.png" alt="">
원격 서버에서의 폴더 구조를 확인했으나 폴더 자체는 존재하는걸로 확인.</p>
<h2 id="문제-해결">문제 해결</h2>
<p><code>GPT</code> 답변</p>
<ul>
<li>윈도우의 scp가 기본적으로 SFTP 방식으로 동작하면서 원격 경로를 엄격하게 검사하는 경우입니다. OpenSSH 9.0부터 scp는 기본적으로 legacy SCP가 아니라 SFTP 프로토콜을 사용합니다. 그래서 예전에는 되던 경로가 지금은 realpath, path canonicalization failed 같은 에러로 실패할 수 있습니다. -O 옵션을 주면 예전 SCP 방식으로 강제할 수 있습니다.</li>
</ul>
<p>내가 찾은 답변</p>
<ul>
<li><a href="https://stackoverflow.com/questions/26346277/scp-files-from-local-to-remote-machine-error-no-such-file-or-directory">https://stackoverflow.com/questions/26346277/scp-files-from-local-to-remote-machine-error-no-such-file-or-directory</a></li>
</ul>
<p>두 답변 모두 <code>-O</code> 옵션을 통해서 문제 해결 제시.</p>
<h2 id="결과">결과</h2>
<p><code>-O</code> 옵션을 통해서 문제 해결 가능했음.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS (7) - S3, CloudFront]]></title>
            <link>https://velog.io/@os_js/AWS-7-S3-CloudFront</link>
            <guid>https://velog.io/@os_js/AWS-7-S3-CloudFront</guid>
            <pubDate>Fri, 20 Feb 2026 16:34:19 GMT</pubDate>
            <description><![CDATA[<h1 id="cloudfront">CloudFront</h1>
<h2 id="cloudfront란">CloudFront란?</h2>
<ul>
<li>AWS에서 정적 웹 사이트를 배포할때는 <code>EC2</code>, <code>ELB</code> 대신 <code>S3</code>와 <code>CloudFront</code>를 사용할 수있음.</li>
<li>두개의 서비스는 정적 콘텐츠를 더 효율적으로 관리하고 제공할 수 있기 때문.</li>
<li>정적 웹사이트 호스팅의 한계점<ul>
<li>S3를 통해서 웹사이트를 호스팅 할 수 있지만 사용자 위치에 따른 콘텐츠 전송속도가 달라질 수 있다. 만약, 버킷이 한국 리전에 있다면 한국이 아닌 다른 나라에 있는 사용자는 파일을 받기 위해 좀 더 오랜 시간을 기다려야한다.</li>
</ul>
</li>
<li>AWS에서는 이러한 문제를 해결하기 위해 전 세계에 파일의 복사본을 저장하는 임시 저장소를 구축했다. 사용자는 가까운 임시 저장소에서 파일을 가져올 수 있어 컨텐츠를 빠르게 받을 수 있다.</li>
<li>이러한 서비스를 CDN(Content Delivery Network)라고 한다.</li>
<li>AWS에서 CloudFront라는 CDN 서비스를 제공함.</li>
<li>CloudFront는 HTTPS를 사용하여 데이터를 전송할때 암호활르 제공함으로써 보안을 강화할 수 있으며, ACM과 연동하여 SSL/TLS 인증서를 무료로 발급하고 갱신할 수 있다.</li>
</ul>
<h2 id="정적-웹사이트-호스팅용-s3-버킷-준비">정적 웹사이트 호스팅용 S3 버킷 준비</h2>
<ul>
<li>S3 버킷 생성
이전 버킷 생성편을 참고하여 
<code>버킷 이름</code> : 고유한 이름
<code>객체 소유권</code> : ACL 비활성화
<code>모든 퍼블릭 액세스 차단</code> : 모두해제
<code>버킷 버전 관리</code> : 비활성화
<img src="https://velog.velcdn.com/images/os_js/post/aaf6343f-7087-4fcb-ae98-4f279ab1c7ae/image.png" alt=""></li>
<li>버킷 정책 설정
<code>서비스</code> : S3
<code>작업</code> : GetObject
<code>리소스</code> : 버킷내 모든 객체
<code>보안주체</code> : 모든 사용자(*)
<img src="https://velog.velcdn.com/images/os_js/post/bc76d4ab-fa27-4efe-ae7b-85e82365294e/image.png" alt=""><h2 id="s3-정적-웹사이트-호스팅">S3 정적 웹사이트 호스팅</h2>
</li>
<li>데스크탑 또는 노트북 로컬 파일 <code>index.html</code> 생성 후 업로드
<img src="https://velog.velcdn.com/images/os_js/post/421b66f5-c00b-4b18-bbb0-2ab93abe9a0b/image.png" alt="">
<img src="https://velog.velcdn.com/images/os_js/post/2757b617-b190-46a2-8ec1-edf858f7b474/image.png" alt="">
<img src="https://velog.velcdn.com/images/os_js/post/ef1eb492-21c5-422e-9ef6-8b68bba20ec2/image.png" alt=""></li>
<li>버킷 상세 페이지로 들어가서 -&gt; <code>속성</code> 탭 클릭
<img src="https://velog.velcdn.com/images/os_js/post/ab482778-c613-4261-98fe-5ca50400920f/image.png" alt=""></li>
<li>하단에 <code>정적 웹사이트 호스팅</code> -&gt; 우측 <code>편집</code>클릭
<img src="https://velog.velcdn.com/images/os_js/post/6af037fb-3dbb-4981-ac09-445bb00118c5/image.png" alt="">
<img src="https://velog.velcdn.com/images/os_js/post/4854ce09-32c9-452f-95d9-e19cd2a11557/image.png" alt=""></li>
<li>하단의 <code>변경사항 저장</code> 클릭 후 마무리</li>
<li>다시 버킷 상세 페이지에서 <code>속성</code> 탭 -&gt; 가장 하단의 <code>정적 웹사이트 호스팅</code>에 보면 접속 url이 존재한다.
<img src="https://velog.velcdn.com/images/os_js/post/1bc6533b-6b78-4bd4-9db5-bf4e2595a935/image.png" alt=""></li>
<li>정상적으로 나오는거 확인
<img src="https://velog.velcdn.com/images/os_js/post/ab7f25a6-cbc4-46b8-9bc5-dde328de3288/image.png" alt=""></li>
</ul>
<h2 id="cloudfront-구성">CloudFront 구성</h2>
<ul>
<li><p><code>cloudFront</code> 콘솔 접속
<img src="https://velog.velcdn.com/images/os_js/post/12a216c5-e59f-4279-b40b-9a00a5acfd31/image.png" alt=""></p>
</li>
<li><p><code>배포 생성</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/c14fd6af-4ceb-4d59-937b-ef1a8e846f11/image.png" alt=""></p>
</li>
<li><p><code>요금제</code> 선택(무료가 있으니 무료 선택)
<img src="https://velog.velcdn.com/images/os_js/post/1c8b5b66-a98d-4a5e-b830-d89a70260d61/image.png" alt=""></p>
</li>
<li><p>도메인 정보 및 태그 정보 입력(가장 상단의 <code>Distribution name</code>만 입력후 나머지 비워둔 후 진행)
<img src="https://velog.velcdn.com/images/os_js/post/0cb313be-d5ff-48fe-9979-eff1cd94bc4d/image.png" alt=""></p>
</li>
<li><p><code>Origin Type</code> 우리가 사용할 것은 <code>S3</code>이므로 <code>S3</code> 선택
<img src="https://velog.velcdn.com/images/os_js/post/778ae826-cd26-45f5-b51b-456729cce2ed/image.png" alt=""></p>
</li>
<li><p><code>Origin</code> 우측 <code>Browse S3</code> 클릭후 원하는 버킷 선택후 <code>Choose</code> 선택
<img src="https://velog.velcdn.com/images/os_js/post/baece20d-15d2-4197-897d-8603705d3e72/image.png" alt="">
<img src="https://velog.velcdn.com/images/os_js/post/bfbbbf9b-bce7-478c-9ecc-b1d2080f953e/image.png" alt=""></p>
</li>
<li><p>정상적으로 주소가 입력되는 것 확인후 <code>웹 사이트 엔드포인트 사용</code> 을 눌러서 cloudFront가 웹 사이트 엔드 포인트를 사용해서 버킷에 접근하도록 설정.
<img src="https://velog.velcdn.com/images/os_js/post/ba48bfb9-10ce-4211-8993-baa1e511dd73/image.png" alt=""></p>
</li>
<li><p><code>Next</code> 클릭</p>
</li>
<li><p><code>Enable Security</code> -&gt; 그대로 <code>Next</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/c8ffdb52-2db6-4dda-8636-a44f0a4e7604/image.png" alt=""></p>
</li>
<li><p><code>create distribution</code> 클릭 후 마무리</p>
</li>
<li><p><code>대표 도메인</code>을 복사해서 접속하면 <code>index.html</code>파일 내용이 정상적으로 뜸.
<img src="https://velog.velcdn.com/images/os_js/post/adec3aad-e683-427d-ad59-d00389d75ab6/image.png" alt=""></p>
</li>
</ul>
<h2 id="새로운-도메인-https-적용">새로운 도메인 HTTPS 적용</h2>
<h3 id="https-적용을-위한-인증서-발급">HTTPS 적용을 위한 인증서 발급</h3>
<ul>
<li>ACM 콘솔 진입
<img src="https://velog.velcdn.com/images/os_js/post/bbf98f35-c87b-4555-9726-8e197da223bb/image.png" alt=""></li>
<li>CloudFront에서 HTTPS를 적용하려면 인증서를 <code>버지니아 북부</code>에서 발급 받아야 함.
리전을 변경
<img src="https://velog.velcdn.com/images/os_js/post/99caedb6-6e43-4090-8d1d-850a97dda58e/image.png" alt=""></li>
<li>우측의 <code>요청</code> 버튼을 통해서 인증서 요청
<code>도메인 이름</code> : 가지고 있는 도메인.
<code>검증 방법</code> : DNS
<code>키 알고리즘</code> : RSA 2048<h3 id="cloudfront-인증서-적용">CloudFront 인증서 적용</h3>
</li>
<li>CloudFront 콘솔 메뉴에서 <code>배포</code> -&gt; <code>배포 ID</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/52650b5b-d6ad-4e44-a44a-4926a93e2251/image.png" alt=""></li>
<li><code>일반</code> 탭의 <code>설정</code>에 있는 <code>편집</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/e4ba0803-1721-416e-8347-51b4e9baf195/image.png" alt=""></li>
<li><code>Alternative Domain name</code>에 SSl/TLS 인증서를 발급당르 때 썼던 도메인을 그대로 작성.
이 도메인은 기본으로 제공되는 도메인 대신 사용할 대체 도메인을 의미함.
그리고 <code>Custom SSL certificate</code>에서는 ACM에서 발급받은 SSL/TLS 인증서를 선택.
<img src="https://velog.velcdn.com/images/os_js/post/0f31664d-6cd4-4e4a-aff5-8a29689fee83/image.png" alt=""></li>
<li>나머지는 그대로 두고 우측 하단 <code>변경 사항 저장</code> 클릭</li>
<li>CloudFront에 도메인을 연결하기 위해서 <code>Route 53</code> 콘솔에서 해당 도메인의 레코드 목록 화면으로 이동한 뒤 <code>레코드 생성</code> 버튼 클릭.
<img src="https://velog.velcdn.com/images/os_js/post/68ad852a-ffaf-4585-bc38-4d2dc3810fd1/image.png" alt=""></li>
<li><code>별칭</code> 옵션 활성화 -&gt; <code>트래픽 라우팅 대상</code>에서 <code>CloudFront 배포에 대한 별칭</code> 선택 -&gt; 앞에서 만든 CloudFront 선택후 <code>레코드 생성</code>클릭
<img src="https://velog.velcdn.com/images/os_js/post/f91431c3-89ad-4f77-9fbf-b504719e4b1c/image.png" alt=""></li>
<li>이렇게 설정하면 CloudFront 의 배포 도메인과 Route53 의 도메인 접속시 동일하게 나오는걸 확인할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS (6) - S3, IAM]]></title>
            <link>https://velog.io/@os_js/AWS-6-S3-IAM</link>
            <guid>https://velog.io/@os_js/AWS-6-S3-IAM</guid>
            <pubDate>Wed, 18 Feb 2026 15:46:50 GMT</pubDate>
            <description><![CDATA[<h1 id="s3">S3</h1>
<h2 id="s3란">S3란?</h2>
<ul>
<li>서비스를 배포하고 운영하면 백엔드 서버에서 이미지나 문서같은 파일을 저장하고 관리하면서 안정적인 저장소가 필요하다. AWS 에서는 이러한 파일 저장소로 S3를 활용한다.</li>
<li>S3는 파일 용량에 제한이 없고 사용자의 필요에따라 자동으로 확장됨.</li>
<li>데이터를 여러 물리적 위치에 분산하여 저장하기 때문에 데이터가 손실될 확률이 아주 희박.</li>
</ul>
<h3 id="버킷">버킷</h3>
<ul>
<li>구글 드라이브에서 공유 드라이브를 여러 개 만들 수 있는 것처럼 S3 에서도 저장소를 여러 개 만들 수 있다. S3에서는 이렇게 만들어진 저장소를 버킷(bucket)이라고 한다.</li>
</ul>
<h3 id="객체">객체</h3>
<ul>
<li>버킷에 업로드한 파일을 객체(object)라고 한다. 객체는 키-값으로 이루어지는데, 여기서 키는 객체에 할당한 이름, 값은 업로드한 콘텐츠 자체를 의미한다.
객체의 값은 바이트 형태로 저장됨.</li>
</ul>
<h2 id="이미지-업로드-과정">이미지 업로드 과정</h2>
<ol>
<li>사용자가 EC2 인스턴스에 동작중인 백엔드 서버로 업로드 요청.</li>
<li>EC2에서 S3로 이미지를 업로드</li>
<li>S3에서 저장된 URL 주소 반환</li>
<li>EC2에서 RDS로 반환된 URL 주소 저장.</li>
</ol>
<h2 id="이미지-다운로드-과정">이미지 다운로드 과정</h2>
<ol>
<li>이미지 조회 API EC2로 전송.</li>
<li>EC2에서 RDS로 이미지 조회 쿼리 전송.</li>
<li>RDS에서 EC2로 이미지 URL 반환.</li>
<li>사용자에게 이미지 URL 반환.</li>
<li>사용자가 URL을 통해 다운로드시 S3를 통해 직접 다운로드.</li>
</ol>
<ul>
<li>백엔드 서버 자체에서 이미지를 전달해주는게 아니라 S3에서 직접 다운로드 받습니다.</li>
</ul>
<h2 id="s3-버킷-만들기">S3 버킷 만들기</h2>
<ul>
<li><p>S3 콘솔 이동
<img src="https://velog.velcdn.com/images/os_js/post/57acef1b-440b-4f6a-b29b-ba7fc4817995/image.png" alt=""></p>
</li>
<li><p>우측 상단 리전 메뉴 <code>서울</code> 선택</p>
</li>
<li><p><code>버킷 만들기</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/36a3d243-c750-488f-a01d-7931e4d0b60f/image.png" alt=""></p>
</li>
<li><p>버킷 이름 작성(AWS 전체 내에서 공유해야함 - 이제까지 다른 이름들도 마찬가지)
<img src="https://velog.velcdn.com/images/os_js/post/0d40f81c-9ca2-497d-a0ea-c9497e45fcf1/image.png" alt=""></p>
</li>
<li><p><code>퍼블릭 액세스 차단</code> 해제
익명의 사용자도 S3에서 객체를 내려받을 수 있도록 하기 위한 절차.
<img src="https://velog.velcdn.com/images/os_js/post/4bd8d0dd-f4fa-4726-bd43-5a9f49988b8a/image.png" alt=""></p>
</li>
<li><p>버킷 만들기 클릭
<img src="https://velog.velcdn.com/images/os_js/post/539cb54b-7964-4c6c-a2eb-0d498cf85df5/image.png" alt=""></p>
</li>
<li><p>버킷 생성 완료
<img src="https://velog.velcdn.com/images/os_js/post/839ea000-f9e6-404a-977f-c984ba78fbca/image.png" alt="">
아직 사용자들이 해당 버킷에 접근이 불가능한 상태.</p>
</li>
</ul>
<h2 id="버킷-정책-추가">버킷 정책 추가</h2>
<ul>
<li>생성한 버킷 들어가기
<img src="https://velog.velcdn.com/images/os_js/post/6b6265a1-ddb3-4019-8882-8b4b5d59b886/image.png" alt=""></li>
<li><code>권한</code> -&gt; <code>버킷 정책</code> -&gt; <code>편집</code>
<img src="https://velog.velcdn.com/images/os_js/post/c04738b9-f5ab-4db2-aafe-9b92c0cc2cde/image.png" alt=""></li>
<li>정책 추가(<code>새 문 추가</code> 클릭)
<img src="https://velog.velcdn.com/images/os_js/post/b8ea368f-bea0-4d3e-bbcb-0fd96e030b70/image.png" alt=""></li>
<li>왼쪽에 JSON 형식 이 나타나고, 오른쪽에는 <code>작업 추가</code> 화면이 나옴.
S3 관련된 권한 추가를 위해 <code>서비스 선택</code> 검색 창에 <code>s3</code> 입력하여, <code>S3</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/9a2de40b-e1b4-4630-8f89-c6749b86e152/image.png" alt=""></li>
<li>우측 검색창에 <code>GetObject</code> 입력후 아래 <code>액세스 수준 - 읽기</code> 에서 <code>GetObject</code> 선택후 <code>리소스 추가</code>의 <code>추가</code> 클릭
GetObject는 객체를 조회 하겠다는 뜻이며, 파일을 내려받을 수 있는 권한을 허용하는 문구를 추가한 것임.
<img src="https://velog.velcdn.com/images/os_js/post/0ad5c522-3a99-4362-872e-9c64abf29e1b/image.png" alt=""></li>
<li>리소스 추가하기
<code>리소스 유형</code>에 object를 선택.
(S3에는 여러 버킷이 있고, 하나의 버킷 안에는 다양한 객체가 존재하는데 경우에 따라 특정 버킷 또는 특정 객체에만 접근하게 설정할 수 있음.)
<img src="https://velog.velcdn.com/images/os_js/post/a9415655-3cd1-4f50-b027-7f95cf028c5a/image.png" alt="">
<code>object</code>로 선택하면 아래 <code>리소스 ARN</code>이 <code>arn:aws:s3:::{BucketName}/{ObjectName}</code> 으로 변경되는데, 
<code>BucketName</code> : 접근 허용 버킷
<code>ObjectName</code> : 접근 허용할 특정 객체(<code>*</code> 를 쓰면 버킷의 모든 객체 허용)</li>
</ul>
<p>여기서는 <code>*</code> 로 작성
<img src="https://velog.velcdn.com/images/os_js/post/470e758c-763d-45bd-8929-52e229dfa01f/image.png" alt=""></p>
<ul>
<li><code>리소스 추가</code> 버튼을 클릭하면 JSON 에 <code>Resource</code> 부분 이 추가된걸 확인
<img src="https://velog.velcdn.com/images/os_js/post/319e9b70-b3b6-46e6-b2e1-8bc2237e1c52/image.png" alt=""></li>
<li>누구에게 권한을 부여할 것인가. 부여대상을 설정하기 위한 속성이 <code>Principal</code>.
여기서 우리는 모든 사용자에게 이미지를 볼 수 있게 하기위해 <code>*</code> 로 변경.
<img src="https://velog.velcdn.com/images/os_js/post/e2305aea-2454-4923-af44-e3013147c116/image.png" alt=""></li>
<li>우측 하단 <code>변경사항 저장</code> 으로 마무리
<img src="https://velog.velcdn.com/images/os_js/post/e8c519cb-e4ea-481d-be9b-c4ca25896a6d/image.png" alt=""></li>
</ul>
<h1 id="iam">IAM</h1>
<h2 id="iam-이란">IAM 이란?</h2>
<ul>
<li>앞에서 S3의 설정으로 사용자들은 S3에 저장된 파일을 내려받을 수 있게 되었다.</li>
<li>하지만 백엔드 서버는 S3에 접근할 수 없는데, AWS SDK 라이브러리를 사용해 S3와 같은 AWS의 서비스에 요청을 보내는데, 이때 서비스에 접근할 수 있는 권한이 필요하기 때문이다.</li>
<li>IAM(Identity and Access Management) AWS 자원에 대한 접근 권한을 제어하는 서비스.</li>
<li>IAM의 사용자에게 액세스 키, 비밀 액세스 키 가 주어진다.
액세스키는 <code>아이디</code> 같은 역할, 비밀 액세스키는 <code>비밀번호</code> 같은 역할을 함.<h2 id="iam-액세스키-발급-받기">IAM 액세스키 발급 받기</h2>
</li>
<li>IAM 콘솔 진입
<img src="https://velog.velcdn.com/images/os_js/post/5a42ff8b-d8c7-4271-9ab8-15d2f8e9b871/image.png" alt=""></li>
<li><code>사용자</code> 메뉴 -&gt; <code>사용자 생성</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/3e287884-4c14-47e2-90f6-bd164c5a3ce7/image.png" alt=""></li>
<li><code>사용자 이름</code> 작성(각 사용자 구분 가능한 이름으로)
<img src="https://velog.velcdn.com/images/os_js/post/7461c5d7-2b92-4f9b-8c28-32b22e24618c/image.png" alt=""></li>
<li>권한 설정 -&gt; 직정 정책 연결 선택
권한 정책 -&gt; s3에 대한 접근 권한이 필요하므로 <code>s3full</code> 검색 -&gt; 체크박스 체크 -&gt; <code>다음</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/9519ab59-1448-4640-96ad-1da54ee126b8/image.png" alt=""></li>
<li>세부정보 확인후 <code>사용자 생성</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/03b63eb8-80f0-42dd-a0b7-865841a258bf/image.png" alt=""></li>
<li><code>사용자</code> 메뉴로 들어가서 <code>사용자 이름</code> 클릭<img src="https://velog.velcdn.com/images/os_js/post/31227495-d5bb-4c2b-b854-097d678c8dfd/image.png" alt=""></li>
<li><code>보안자격 증명</code> 탭 -&gt; <code>액세스키 만들기</code> 클릭(우측, 중앙 중 아무거나 동일)
<img src="https://velog.velcdn.com/images/os_js/post/d77e3281-3fa1-45e5-9f99-861bf17cfbd3/image.png" alt=""></li>
<li><code>액세스키 모범 사례 및 대안</code> 에서 우리는 AWS EC2에서 실행되는 서비스가 접근할 예정이므로, <code>AWS 컴퓨팅 서비스에서 실행되는 애플리케이션</code> 선택 후 <code>다음</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/2c81d99d-38d4-4178-a920-7fe5b5dcaba5/image.png" alt=""></li>
<li><code>설명 태그 설정</code> -&gt; <code>설정 태그 값</code> 은 비워두고 -&gt; <code>엑세스 키 만들기</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/69665556-e671-40ee-973b-193656354c7c/image.png" alt=""></li>
<li>사진과 같이 생성된 <code>액세스 키</code>가 보임.<blockquote>
<p><code>비밀 액세스키</code>의 경우 위에 알림이 뜨는것과 같이 해당 페이지에서 벗어나면 다시는 볼 수 없어서 새로 액세스키를 만들어야 하니 복사해 놓아야 함
<img src="https://velog.velcdn.com/images/os_js/post/f9629e71-0830-436f-b9f7-b73aba776090/image.png" alt=""></p>
</blockquote>
</li>
</ul>
<h2 id="백엔드-연동">백엔드 연동</h2>
<ul>
<li><p>프로젝트 다운로드</p>
<pre><code>$ cd ~
$ git clone https://github.com/JSCODE-BOOK/aws-s3-springboot.git
$ cd aws-s3-springboot/src/main/resources
$ vi application.yml</code></pre></li>
<li><p>application.yml 내부</p>
<pre><code>server:
port: 80
spring:
datasource:
  url: jdbc:mysql://_________:3306/&lt;DB이름&gt; # RDS 인스턴스 엔드포인트 / DB이름
  username: ______ # (RDS 인스턴스 생성 시 셋팅한) RDS 마스터 사용자 이름
  password: ______ # (RDS 인스턴스 생성 시 셋팅한) RDS 마스터 비밀번호
  driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
  hibernate:
    ddl-auto: create
  show-sql: true
cloud:
  aws:
    credentials:
      access-key: _________ # IAM 통해서 발급받은 액세스 키
      secret-key: _________ # IAM 통해서 발급받은 비밀 액세스 키
    s3:
      bucket: _________ # 생성한 버킷명
    region:
      static: ap-northeast-2</code></pre></li>
<li><p>빌드 및 배포</p>
<pre><code>$ cd ~/aws-s3-springboot
$ ./gradlew clean build -x test # 스프링 부트 프로젝트 빌드
$ cd build/libs
$ sudo nohup java -jar aws-s3-springboot-0.0.1-SNAPSHOT.jar &amp; # JAR파일 실행</code></pre></li>
<li><p><code>https://api.&lt;도메인주소&gt;/health</code> 입력
<img src="https://velog.velcdn.com/images/os_js/post/2b809e9e-35ac-4d0f-83df-f8fba6a2071b/image.png" alt=""></p>
</li>
<li><p><code>PostMan</code> 을 사용할 겁니다.
<img src="https://velog.velcdn.com/images/os_js/post/00f8456a-85b0-407f-9b64-b86dce15948d/image.png" alt=""></p>
</li>
<li><p>전송시 사진과 같은 결과가 반환됨.
<img src="https://velog.velcdn.com/images/os_js/post/2e025ed7-e899-4843-9ea3-eb0a54ea802a/image.png" alt=""></p>
</li>
<li><p>S3 메뉴에서 버킷 클릭시 사진이 업로드 된 걸 확인 가능.
<img src="https://velog.velcdn.com/images/os_js/post/58e2607e-c98d-4447-bcd4-0301586bbd96/image.png" alt=""></p>
</li>
<li><p>게시글 조회를 위한 GET 요청시 정상 반환 확인
<img src="https://velog.velcdn.com/images/os_js/post/18db3e6b-b2e3-4f45-a4b5-3617cb6d27c8/image.png" alt=""></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS (5) - RDS]]></title>
            <link>https://velog.io/@os_js/AWS-5-RDS</link>
            <guid>https://velog.io/@os_js/AWS-5-RDS</guid>
            <pubDate>Wed, 18 Feb 2026 15:46:38 GMT</pubDate>
            <description><![CDATA[<h1 id="rds">RDS</h1>
<h2 id="rds란">RDS란?</h2>
<ul>
<li>RDS(Relational Database Service)의 줄임말로, AWS로부터 관계형 데이터베이스르 빌려서 사용할 수 있는 서비스이다.</li>
<li>MariaDB, Postgresql 등 다양한 데이터베이스를 지원한다.</li>
<li>또한 백업, 업데이트, 자동 확장 기능을 제공함.</li>
</ul>
<h3 id="rds-인스턴스">RDS 인스턴스</h3>
<ul>
<li>데이터베이스가 설치되어있는 컴퓨터 한 대를 RDS 인스턴스라고 함.</li>
<li>엔진 유형 : MySQL, Postgresql, MariaDB, Amazon Aurora 등이 존재.
인스턴스 클래스: RDS에서 인스턴스 클래스란 컴퓨터의 성능을 뜻함. EC2 서비스에서 인스턴스 유형과 비슷한 뜻으로 고성능 컴퓨터일수록 많은 양의 데이터를 처리할 수 있음.
스토리지 : 데이터베이스가 데이터를 저장하는 공간.</li>
</ul>
<h3 id="rds-인스턴스-생성">RDS 인스턴스 생성</h3>
<ul>
<li>RDS 콘솔 진입.
<img src="https://velog.velcdn.com/images/os_js/post/cda5d189-6871-44c7-a0b5-7a73b714710b/image.png" alt=""></li>
<li>리전 <code>서울</code> 선택</li>
<li><code>데이터베이스 생성</code>클릭
<img src="https://velog.velcdn.com/images/os_js/post/d0fb9523-014e-4c2c-9e6e-b10394f2ba58/image.png" alt=""></li>
<li>데이터 생성 방식 및 엔진 설정
<img src="https://velog.velcdn.com/images/os_js/post/565cee6e-881a-4b7e-9d76-31fc41a1669e/image.png" alt=""></li>
<li>템플릿 선택
<img src="https://velog.velcdn.com/images/os_js/post/893eca47-9f75-419e-9e8b-70d6ff0006bf/image.png" alt=""></li>
<li>설정 정보 입력
<img src="https://velog.velcdn.com/images/os_js/post/fa75657a-0c20-46eb-bea6-3bb738ef4b9a/image.png" alt=""></li>
<li>인스턴스 유형 선택
<img src="https://velog.velcdn.com/images/os_js/post/828bf8e6-ff08-404d-b64f-7d023b2007df/image.png" alt=""></li>
<li>스토리지 선택
<img src="https://velog.velcdn.com/images/os_js/post/3bf482cd-618d-492e-a1e0-b4d28e7e4007/image.png" alt=""></li>
<li>연결 설정<blockquote>
<p>RDS에서 퍼블릭 액세스를 사용할경우 시간당 요금이 부과됩니다. RDS 인스턴스를 사용하지 않을때는 인스턴스 삭제를 추천합니다.</p>
</blockquote>
</li>
</ul>
<p>퍼블릭 액세스를 통해서 외부에서 데이터베이스에 접근할 수 있게 만들 수 있습니다.
또한 데이터 베이스에 접근하는 트래픽을 허용하기 위해 보안그룹을 새로 생성합니다.
<img src="https://velog.velcdn.com/images/os_js/post/a76491c9-1450-41b0-a69d-afd81b2bc985/image.png" alt=""></p>
<ul>
<li>하단의 <code>추가 구성</code>을 펼쳐서 <code>초기 데이터 베이스 이름</code>을 작성
<img src="https://velog.velcdn.com/images/os_js/post/425cf179-4418-4d7c-9a53-a0bad5e9fc38/image.png" alt=""></li>
<li>가장 밑에 <code>데이터베이스 생성</code> 버튼으로 생성 마무리
<img src="https://velog.velcdn.com/images/os_js/post/b1e604b2-5f23-4935-8061-0f2ec2142d7c/image.png" alt=""></li>
<li>데이터베이스의 <code>DB 식별자</code> 를 클릭하여 상세 페이지로 이동</li>
</ul>
<h3 id="rds-보안그룹-수정">RDS 보안그룹 수정</h3>
<ul>
<li>상세 페이지로 이동했으면, 하단의 <code>연결 및 보안</code> 부분에서 <code>VPC 보안 그룹</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/1ca7d32d-327d-4c33-9d68-969993194aa6/image.png" alt=""></li>
<li>보안그룹에 대한 인바운드 규칙 편집
<img src="https://velog.velcdn.com/images/os_js/post/4fb0063b-c24d-459a-8c5f-3a4499b0a021/image.png" alt=""></li>
<li>소스를 <code>Anywhere-IPv4</code>로 수정후 규칙 저장
<img src="https://velog.velcdn.com/images/os_js/post/8621319d-e9d2-42ee-9aaf-2c57c3fff0fd/image.png" alt=""></li>
</ul>
<h3 id="rds-인스턴스-접속">RDS 인스턴스 접속</h3>
<ul>
<li>RDS 메뉴에서 <code>데이터베이스</code> 클릭후 -&gt; <code>DB식별자</code>를 클릭 후 -&gt; 연결 보안 -&gt; 엔드포인트 선택
<img src="https://velog.velcdn.com/images/os_js/post/bddf655e-82ae-4bb1-b553-7d15b87bd58d/image.png" alt=""></li>
<li><code>엔드포인트</code> 와 <code>포트</code>를 사용하여 접속 시도.</li>
<li>HediSQL을 통해서 접속했음.
<img src="https://velog.velcdn.com/images/os_js/post/d39703d5-32e0-49ed-8938-fa4b8b9766ac/image.png" alt="">
데이터베이스 생성하면서 만들었던 <code>integration</code> 데이터베이스도 존재함.</li>
</ul>
<h2 id="파라미터-그룹-설정">파라미터 그룹 설정</h2>
<ul>
<li>데이터베이스의 저장되는 날짜 데이터를 한국시간을 기준으로 저장하거나 인코딩 방식을 설정한다거나 하는 경우. RDS에서는 파라미터 그룹을 사용하여 데이터베이스 설정이 가능하다.</li>
<li>RDS 메뉴에서 <code>파라미터 그룹</code> -&gt; <code>파라미터 그룹 생성</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/9a067b91-c839-48e3-aa6a-954bdc29af93/image.png" alt=""></li>
<li>사진과 같이 입력후 생성
파라미터 그룹이나 설명은 알아서 작성.<blockquote>
<p>여기서 중요한점은 위에서 생성한 Mysql 버전이 8.4.7 이었기 때문에 <code>파라미터 그룹 패밀리</code> 항목을 <code>mysql8.4</code>로 변경해줘야 함.</p>
</blockquote>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/os_js/post/57169ae6-165f-4f6b-adfb-29a149a337e8/image.png" alt=""></p>
<ul>
<li>생성된 파라미터 그룹의 이름을 클릭하여 세부정보로 들어감
<img src="https://velog.velcdn.com/images/os_js/post/677b7f6c-3db7-4844-9682-4ded0860a59f/image.png" alt=""></li>
<li>편집 클릭
<img src="https://velog.velcdn.com/images/os_js/post/92c4221e-6876-42c9-bf78-34b17406e9ca/image.png" alt=""></li>
<li><code>수정 가능한 파라미터</code>에 <code>character_set</code> 입력후 검색
<img src="https://velog.velcdn.com/images/os_js/post/4fa04582-2058-4af2-9ec1-9cecbfaf9956/image.png" alt=""></li>
<li>utf8보다는 utf8mb4를 통해서 한글뿐 아니라 이모티콘도 같이 지원할 수 있도록 함.
<code>파라미터에 대한 값 입력</code> 부분에 <code>utf8mb4</code> 작성후 변경사항 저장
<img src="https://velog.velcdn.com/images/os_js/post/fef048e5-55cf-4dfd-a2ea-169ba99dd6af/image.png" alt=""></li>
<li>다시 편집을 눌러 <code>collation</code> 검색후 나온 값들에 대해서 <code>utf8mb4_unicode_ci</code> 입력후 저장</li>
<li>다시 편집을 눌러 <code>time_zone</code> 검색후 <code>Asia/Seoul</code> 입력후 저장.
(시간대를 한국 기준으로 맞추기 위함)</li>
</ul>
<h3 id="rds-파라미터-그룹-변경">RDS 파라미터 그룹 변경</h3>
<ul>
<li><p>RDS 메뉴에서 <code>데이터베이스</code> 선택후 -&gt; 수정 클릭
<img src="https://velog.velcdn.com/images/os_js/post/a1f3433a-f879-4e44-b778-96f72c9af7f8/image.png" alt=""></p>
</li>
<li><p><code>추가구성</code> 항목에서 위에서 생성한 파라미터 그룹을 선택 후 저장.
<img src="https://velog.velcdn.com/images/os_js/post/9c78f746-ce03-4dc0-82c4-115c9f386481/image.png" alt=""></p>
</li>
<li><p>즉시 적용
<img src="https://velog.velcdn.com/images/os_js/post/4ab3c933-62e1-4539-af1c-b9341fb3df89/image.png" alt=""></p>
</li>
<li><p>데이터 베이스가 <code>수정중</code>에서 <code>사용가능</code>으로 변경되면 우측 상단의 <code>작업</code> 에서 <code>재부팅</code> 클릭.
이 과정을 진행해야 적용되므로 반드시 수행.
<img src="https://velog.velcdn.com/images/os_js/post/0e417242-6e12-45dc-963c-f1bdc6a94d9c/image.png" alt="">
<img src="https://velog.velcdn.com/images/os_js/post/313941aa-1c59-46b9-975c-431a41746782/image.png" alt=""></p>
</li>
<li><p><code>사용가능</code> 상태가 되면 <code>DB 식별자</code> 클릭 후 세부 사항으로 이동.
<img src="https://velog.velcdn.com/images/os_js/post/e5d88902-d73f-459b-865c-46c36156d716/image.png" alt=""></p>
</li>
<li><p>하단의 <code>구성</code>탭을 누르면, <code>DB 인스턴스 파라미터 그룹</code>이 내가 선택한 그룹이 된다.
<img src="https://velog.velcdn.com/images/os_js/post/63bed42f-6f24-4b06-85a3-4c985632f3ec/image.png" alt=""></p>
</li>
</ul>
<h3 id="백엔드와-연동">백엔드와 연동</h3>
<ul>
<li>예제 프로젝트 가져오기
<code>git clone https://github.com/JSCODE-BOOK/aws-rds-springboot.git</code></li>
<li><code>cd aws-rds-springboot/src/main/resources</code>
<code>vi application.yml</code><pre><code>server:
port: 80
spring:
datasource:
  url: jdbc:mysql://___________:3306/instagram # RDS 인스턴스 엔드포인트,
  # 가장 끝에 instagram은 본인이 만든 데이터베이스 이름으로 수정
  username: ______ # RDS 마스터 사용자 이름
  password: ______ # RDS 마스터 비밀번호
  driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
  hibernate:
    ddl-auto: update
  show-sql: true</code></pre><ul>
<li>빌드 및 실행<pre><code>$ sudo kill {PID 값} # 80번 포트에서 실행되는 프로세스가 있다면 종료
$ cd ~/aws-rds-springboot
$ ./gradlew clean build -x test # 스프링 부트 프로젝트 빌드
$ cd build/libs 
$ sudo nohup java -jar aws-rds-springboot-0.0.1-SNAPSHOT.jar &amp; # JAR 파일 실행</code></pre></li>
</ul>
</li>
<li>결과
<img src="https://velog.velcdn.com/images/os_js/post/52aa0b62-8880-4392-affc-ae3c6a9a77c4/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS - 삭제]]></title>
            <link>https://velog.io/@os_js/AWS-%EC%82%AD%EC%A0%9C</link>
            <guid>https://velog.io/@os_js/AWS-%EC%82%AD%EC%A0%9C</guid>
            <pubDate>Wed, 18 Feb 2026 09:18:20 GMT</pubDate>
            <description><![CDATA[<h1 id="ec2">EC2</h1>
<ul>
<li>인스턴스 종료 후에도 콘솔에 표시될 수 있다고 함.</li>
</ul>
<p><a href="https://repost.aws/ko/questions/QUGrEJVMt3QTiSUDHhVfn3KQ/%EC%A2%85%EB%A3%8C-%ED%9B%84%EC%97%90%EB%8F%84-%EB%AA%A9%EB%A1%9D%EC%97%90-%EA%B3%84%EC%86%8D-%EB%82%A8%EC%95%84%EC%9E%88%EB%8A%94-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%82%AD%EC%A0%9C%ED%95%98%EA%B8%B0">참고 링크</a></p>
<h1 id="탄력적-ip">탄력적 IP</h1>
<ul>
<li>EC2 인스턴스 <code>삭제</code> 후 릴리즈 가능</li>
</ul>
<h1 id="cloudfront">CloudFront</h1>
<ul>
<li>비활성화 후 삭제가 가능한데, 바로 <code>삭제</code>가 가능하진 않고 좀 시간을 두고 기다리면 삭제 버튼이 활성화 되는데. 바로 또 되지도 않음.
<img src="https://velog.velcdn.com/images/os_js/post/64b879dc-9508-473a-866d-8daff97b3357/image.png" alt=""></li>
<li>배포시에 요금제를 선택하는데, 이 요금제를 취소하고 플랜기간이 지나면 그때 삭제가 가능하다고함.</li>
<li>플랜 취소는 이름 클릭해서 <code>세부 사항</code> 에 <code>Manage Plan</code>이 있다. 누르면 우측에 창이 뜨는데 거기서 취소하면됨. 그러면 아래 사진처럼 뜬다.
<img src="https://velog.velcdn.com/images/os_js/post/a0b94893-c0f7-49f2-b0c9-c6c3fe2dfae3/image.png" alt="">
난 3월1일 이후에 삭제가 가능.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS (4) - ELB, ACM]]></title>
            <link>https://velog.io/@os_js/AWS-4-ELB-ACM</link>
            <guid>https://velog.io/@os_js/AWS-4-ELB-ACM</guid>
            <pubDate>Wed, 18 Feb 2026 03:21:31 GMT</pubDate>
            <description><![CDATA[<h1 id="elb">ELB</h1>
<h2 id="elb란">ELB란?</h2>
<ul>
<li>일반적인 백엔드와 프론트엔드는 서로 데이터를 주고 받는다. 이때주고받는 데이터가 외부에 노출되지 않도록 하려면 HTTPS를 사용하여 통신할 수 있게 해줘야 한다. 우리는 HTTPS를 적용하기 위해 ELB 서비스를 이용한다.</li>
<li>ELB(Elastic Load Balancing)은 AWS에서 제공하는 로드 밸런서 서비스를 말한다.</li>
<li>ELB에서는 트래픽 분산 기능을 제공하고, 추가적으로 특정 포트에서 HTTPS요청을 처리하도록 설정할 수 있으므로 보안이 필요한 웹사이트나 API 서버에서도 많이 사용함.</li>
</ul>
<h2 id="구성요소">구성요소</h2>
<ul>
<li>학습전 리전은 항상 <code>서울</code>로 선택<h3 id="리스너">리스너</h3>
</li>
<li>ELB로 들어오는 요청을 어떻게 처리할지 결정하는 규칙을 관리함.</li>
<li>특정 포트와 프로토콜을 사용하여 클라이언트의 요청을 기다리고, 해당 요청을 ELB에서 설정된 규칙에 따라 적절한대상 그룹으로 전달.</li>
</ul>
<h3 id="대상그룹">대상그룹</h3>
<ul>
<li>ELB가 수신한 트래픽을 전달할 서버들의 집합을 의미.</li>
<li>ELB로 들어온 요청을 어떤 곳으로 전달할지 정해야 하는데, 여기서 어떤 곳을 ELB 에서는 대상 그룹이라고 표현함.</li>
<li>또한 ELB는 트래픽의 전달 대상이 되는 서버들의 health check를 진행하여 전달 목적지를 정한다. 대상 그룹을 만들때 상태 검사를 할 경로와 포트를 지정한다.</li>
</ul>
<h4 id="대상그룹-만들기">대상그룹 만들기</h4>
<ul>
<li><p><code>ec2</code>메뉴 접속후 대상그룹 메뉴 선택 -&gt; 우측 상단 <code>대상 그룹 생성</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/af4f45ec-536a-4171-b5e1-88c108765406/image.png" alt=""></p>
</li>
<li><p>대상 유형 : 이전에 생성했던 <code>EC2 인스턴스</code>에 연결할 목적이니 <code>인스턴스</code> 선택
대상 그룹 이름 : 직관적인 이름으로 작성
트래픽 전달 방식 : 현재 <code>EC2 인스턴스</code>는 IPv4 주소를 가지며, 백엔드 서버는 80 포트를 사용중이고, HTTP로 통신한다. 이에 맞춰 설정해준다.
<img src="https://velog.velcdn.com/images/os_js/post/3db7ba61-7eda-4226-92fb-7313bd5be775/image.png" alt="">
<img src="https://velog.velcdn.com/images/os_js/post/fffdb303-f54d-48e4-8bf8-adfc871b5c57/image.png" alt=""></p>
</li>
<li><p>상태 검사 설정
대상에 일정한 주기로 전송할 상태 검사를 설정하는 단계. HTTP에 /health 입력
<img src="https://velog.velcdn.com/images/os_js/post/50460b83-9b5d-48ae-ba13-5665576a5005/image.png" alt="">
이렇게하면 대상 그룹의 EC2 인스턴스에 HTTP 프로토콜을 사용하여 GET /health 요청을 일정한 주기로 전송.</p>
</li>
<li><ul>
<li>정상적으로 작동하기 위해서는 따로 API를 구축해야함 **</li>
</ul>
</li>
<li><p>나머지는 그대로 두고 <code>다음</code> 클릭.</p>
</li>
<li><p>대상 등록
등록할 인스턴스 좌측 체크박스 클릭후.
<code>선택한 인스턴스를 위한 포트</code>는 ELB로 들어온 요청을 대상에 추가한 인스턴스의 80번 포트로 전달하겠다는 의미.
하단의 <code>아래에 보류 중인 것으로 포함</code>을 눌러 인스턴스 대상에 추가.
<img src="https://velog.velcdn.com/images/os_js/post/a659b211-3017-4493-ace6-7d0b76597e4e/image.png" alt=""></p>
</li>
<li><p><code>다음</code> 클릭</p>
</li>
<li><p>지금까지 등록한 내용을 전체적으로 보여주는 것이니 확인 후 이상이 없다면 <code>대상 그룹 생성</code> 클릭</p>
</li>
<li><p>좌측 <code>대상 그룹</code> 메뉴에서 확인
<img src="https://velog.velcdn.com/images/os_js/post/034670ce-9f2a-40a4-bf8e-8a00b7377fe5/image.png" alt=""></p>
</li>
</ul>
<h3 id="로드밸런서-생성">로드밸런서 생성</h3>
<ul>
<li>학습전 리전은 항상 <code>서울</code>로 선택</li>
<li><code>EC2</code> 메뉴에서 <code>로드 밸런서</code> 메뉴 클릭 -&gt; 우측 상단 <code>로드 밸런서 생성</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/6c1306b6-7af8-42b0-bfb7-1ad46c0dcba7/image.png" alt=""></li>
<li>우리는 HTTP/HTTPS 기반 트래픽 처리가 필요하므로, <code>Application Load Balancer</code> 선택
<img src="https://velog.velcdn.com/images/os_js/post/23ffab38-efa5-4f28-b6ad-ecce2af26246/image.png" alt=""></li>
<li>기본구성 설정
<img src="https://velog.velcdn.com/images/os_js/post/e363ee8b-7d8a-4923-b43b-3102c555cdd8/image.png" alt=""></li>
<li>네트워크 매핑
<code>가용영역 및 서브넷</code> 모두 체크
<img src="https://velog.velcdn.com/images/os_js/post/9488face-5bca-4a7a-b914-20805e14c7c1/image.png" alt=""></li>
<li>보안그룹 부분에서 <code>새 보안 그룹 생성</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/4e1f8e8e-c16d-475f-8ced-d52c08ed0bb0/image.png" alt=""></li>
<li>기본 정보 입력
<img src="https://velog.velcdn.com/images/os_js/post/f3759037-9e5c-4d8a-84b1-dad170aaf9ea/image.png" alt=""></li>
<li>HTTP/HTTPS 트래픽 허용 인바운드 규칙 추가
<img src="https://velog.velcdn.com/images/os_js/post/a2eb68bd-1ac8-4e39-b2fe-4be2ee2e11e2/image.png" alt=""></li>
<li>우측 하단의 <code>보안그룹 생성</code> 클릭후 다시 돌아오기</li>
<li>이전 <code>보안 그룹</code> 에서 방금 추가한 <code>보안 그룹</code> 선택</li>
<li><code>리스너 및 라우팅</code>에서 <code>대상그룹</code>은 우리가 이전에 추가했던 그룹으로 선택
<img src="https://velog.velcdn.com/images/os_js/post/805b283f-1111-4cfb-a59a-edcee16fae2a/image.png" alt=""></li>
<li>가장 하단의 <code>로드밸런서 생성</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/e54dbb08-8d27-4b39-b6c0-e73fdaa79d60/image.png" alt=""></li>
<li>좌측 <code>로드밸런서</code> 메뉴에서 항목 추가된걸 확인
<img src="https://velog.velcdn.com/images/os_js/post/def92cef-6546-492a-9f80-dedea443ba56/image.png" alt=""></li>
</ul>
<h3 id="상태검사-api-추가">상태검사 API 추가.</h3>
<ul>
<li><p>이전에 로드밸런서를 생성할때 상태검사를 할 수 있도록 경로를 지정하였다.</p>
</li>
<li><p>하지만 상태 검사에 사용되는 API를 추가하지 않으면 로드밸런서는 서버가 현재 정상적이지 않다고 판단합니다.</p>
</li>
<li><p>예제 파일 다운로드
<code>git clone https://github.com/JSCODE-BOOK/aws-elb-springboot.git</code></p>
</li>
<li><p>코드 확인(GET /health 요청시 응답하도록 되어있음)</p>
<pre><code>AppController.java
@RestController
public class AppController {
</code></pre></li>
</ul>
<p>// GET /health 요청 시 이 메서드 호출
 @GetMapping(“health”) 
public ResponseEntity<String> healthCheck() {</p>
<p>// HTTP 200 OK 응답과 메시지 반환
 return ResponseEntity.ok().body(“Success Health Check”); 
 }
}</p>
<pre><code>- 인스턴스에서 아래 명령들 실행</code></pre><p>$ sudo lsof -i:80 # 80번 포트에서 실행되는 프로세스 확인
$ sudo kill {PID 값} # 80번 포트에서 실행되는 프로세스가 있다면 종료
$ cd ~/aws-elb-springboot
$ ./gradlew clean build -x test # 스프링 부트 프로젝트 빌드
$ cd build/libs 
$ sudo nohup java -jar aws-elb-springboot-0.0.1-SNAPSHOT.jar &amp; # JAR 파일 실행
$ sudo lsof -i:80 # 80번 포트에서 실행되는 프로세스 조회</p>
<pre><code>- 결과
![](https://velog.velcdn.com/images/os_js/post/b37250c3-bfd6-4710-954d-fb65e57f77be/image.png)

### 로드 밸런서로 확인
- 좌측 `로드밸런서` 메뉴 클릭후 로드밸런서 이름을 클릭
![](https://velog.velcdn.com/images/os_js/post/b4427434-b9aa-43f6-ae9a-11e7efe0224d/image.png)

- 세부정보가 나타나는데 우측 하단의 `DNS 이름`을 통해서도 접속이 가능하다.
`http://&lt;dns 이름&gt;/health`
![](https://velog.velcdn.com/images/os_js/post/f1603627-ac81-4963-88a1-0a080f19376d/image.png)

- 정상적으로 `Success Health Check` 가 뜬다면 성공.

## 로드 밸런서에 도메인 연결
### EC2와 연결된 Route 53 레코드 편집
- Route 53 메뉴로 들어가서 편집할 레코드 체크.
![](https://velog.velcdn.com/images/os_js/post/259272b0-7dae-4d26-8aed-ee205f07acc1/image.png)
- 우측에 `레코드 편집` 클릭
![](https://velog.velcdn.com/images/os_js/post/b1e084cc-aa9f-4865-9459-4e1fd99097b7/image.png)
- 레코드 편집(별칭 메뉴 옵션을 활성화 하면 하단이 변경된다.)
![](https://velog.velcdn.com/images/os_js/post/35fac4a4-4be8-43c4-8ca1-ab1b6f7b5410/image.png)
- 이제 도메인을 통해서 로드밸런서로 전달된다.
![](https://velog.velcdn.com/images/os_js/post/b03a1741-aee4-4725-8938-3d3e75f6822a/image.png)

## HTTPS 적용
### 적용 단계
1. SSL/TLS 인증서 발급받기
2. 로드 밸런서에 HTTPS용 리스너 추가하기
3. HTTP에서 HTTPS로 리디렉션 설정

### SSL/TLS 인증서 발급받기
- AWS에서 ACM(AWS Certificate Manager)라는 서비스를 활용하여 인증서 발급.
- ACM을 통해서 발급받는 인증서는 대부분 무료.
자세한건 [공식페이지](https://aws.amazon.com/ko/certificate-manager/pricing/) 를 통해서 확인
- ACM 콘솔 접속
![](https://velog.velcdn.com/images/os_js/post/a7055c56-9cce-415b-9b14-0f8bc5150025/image.png)
- 리전을 서울로 꼭 설정하세요.
- 인증서 `요청` 클릭
![](https://velog.velcdn.com/images/os_js/post/6fab6573-d59c-428d-ae74-28383811ea69/image.png)
- 그대로 `퍼블릭 인증서 요청`에 두고 `다음` 클릭
![](https://velog.velcdn.com/images/os_js/post/a4985159-4689-4eb4-ba94-094af8c546fe/image.png)

- 이전에 만들어뒀던 `도메인 주소` 입력
![](https://velog.velcdn.com/images/os_js/post/14e52ea3-3d41-45ad-abbe-ce76a2c56f7d/image.png)
- 나머지 그대로 둔 후 `요청` 클릭
![](https://velog.velcdn.com/images/os_js/post/7ef519fa-1f17-4155-bf4b-0654dcf08b03/image.png)
- 좌측`인증서 나열` 메뉴 클릭후 항목 추가된 것 확인
(정상적인 사용자가 발급한 것인지 확인 후 완료됨)
![](https://velog.velcdn.com/images/os_js/post/001b01ef-b407-43b8-bba2-45dee9a1ba48/image.png)
- 인증서 목록에서 ID 클릭후 상세화면으로 넘어가기.
- 도메인 소유자라는 사실을 인증하기 위해 ACM에서 제공하는 CNAME 이름과 값을 도메인의 레코드에 입력해야함.
`레코드 생성` 클릭을 통해서 쉽게생성할 수 있지만 직접 레코드를 수동으로 생성.
![](https://velog.velcdn.com/images/os_js/post/a3ce2c4a-8d15-4666-bddd-74e1758725f1/image.png)
- `ROUTE 53` 메뉴에서 `호스팅 영역` 클릭후 나오는 도메인 주소 항목중 하나의 ID 클릭후 우측의 `레코드 생성`클릭
![](https://velog.velcdn.com/images/os_js/post/f9e6da5f-5fe8-463a-9bcb-6257166dc826/image.png)
- 레코드 생성 화면에서 `레코드 유형`을 `CNAME`으로 선택.
이전 과정에서 ACM에서 제공되는 `CNAME이름`을 `레코드 이름`에 `CNAME값`을 `값` 에 작성한 후 생성.
&gt; 주의점 : `CNAME 이름`을 복사 붙여넣기 할때, 전부다 붙여넣으면 안되고 도메인이 정상적으로 되도록 해야함.
예를들어, `CNAME 이름`이 `2343535_A.B.C.com` 이라고 치자. 그렇다면 레코드 생성때 뒤에 `B.C.com` 이런식으로 편집 불가능한 메인 도메인이 작성되어 있을 것이다.
그럼 `CNAME 이름`에서 `2343535_A` 부분만 붙여 넣으면 된다. `CNAME 값`은 그대로 작성.

![](https://velog.velcdn.com/images/os_js/post/8c43e911-5a9f-499a-9f06-053f5d0b926d/image.png)
- 다시 `ACM ` 메뉴로 들어와서 검증이 완료되었는지 확인.
(10분이 지나도 안된다면 제대로 입력했는지 레코드 생성값 확인)
![](https://velog.velcdn.com/images/os_js/post/4618c36e-47de-419c-8744-0fe63a70afb9/image.png)

### 로드 밸런서에 HTTPS용 리스너 추가.
- 로드 밸런서에서 HTTP 요청을 받을 수 있는 리스너를 만든 것처럼 HTTPS도 요청을 받을 수 있는 리스너를 추가해야한다.
- `EC2` 콘솔에서 `로드밸런싱` -&gt; `로드 밸런서` 메뉴 이동. 이전에 생성한 로드 밸런서 이름 클릭
- 하단에 `리스너 추가 ` 클릭
![](https://velog.velcdn.com/images/os_js/post/c0184059-3c3c-407f-baf6-700a147a6813/image.png)
- 리스너 구성
![](https://velog.velcdn.com/images/os_js/post/b2576d81-9e76-45c3-b13a-a7e3bfb70b36/image.png)
- 보안 리스너 설정. 이전에 만들었던 인증서 선택
![](https://velog.velcdn.com/images/os_js/post/ba6d9442-2cfa-4133-b359-2af3e2dc4910/image.png)
- 아래로 내려서 `추가` 버튼 클릭으로 생성 마무리.
- 그러면 `리스너 규칙`항목에 추가 됨.
![](https://velog.velcdn.com/images/os_js/post/9a226fe6-5788-47be-bf38-3156c30124ca/image.png)
- 이제 http://&lt;도메인명&gt;/health 뿐만 아니라 https://&lt;도메인명&gt;/health 도 정상 동작함.
![](https://velog.velcdn.com/images/os_js/post/4805c80f-c9d1-4143-8809-e581df958ddf/image.png)

### HTTP에서 HTTPS로 리디렉션 설정.
- HTTPS를 통해서 접속이 가능하더라도 HTTP로 접속또한 가능하다. 이렇게되면 보안이 취약하기때문에 HTTP로 접속하더라도 강제로 HTTPS로 접속하게 수정.
- 기존 HTTP 리스너 삭제
![](https://velog.velcdn.com/images/os_js/post/c6d008ef-d70e-45f2-946f-213512a0b7bf/image.png)
- `리스너 추가` 클릭
HTTP 80 포트로 들어오면 HTTPS 443 포트로 연결 되도록 설정
![](https://velog.velcdn.com/images/os_js/post/4621a7b1-b4a1-4ee9-836d-e3c11f4e2e09/image.png)
- `추가` 버튼으로 생성 마무리
- 이제 http로 접속해도 https로 연결되는걸 확인할 수 있음.

</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[AWS(3) - Route 53]]></title>
            <link>https://velog.io/@os_js/AWS3-Route-53</link>
            <guid>https://velog.io/@os_js/AWS3-Route-53</guid>
            <pubDate>Wed, 18 Feb 2026 03:21:18 GMT</pubDate>
            <description><![CDATA[<h1 id="route-53">Route 53</h1>
<h2 id="route-53-이란">Route 53 이란?</h2>
<ul>
<li>우리가 보통 특정 웹사이트를 이용하기 위해서는 192.x.x.x 와 같이 IP를 사용하여 사이트에 접속하지 않고, <code>https://www.google.com/</code> 와 같이 이름을 통해서 접속하게 된다.</li>
<li>이전에 실습한 <code>EC2 인스턴스</code>에 접속할때 IP를 사용하여 접속을 테스트하였지만 실제 서비스를 배포하게 된다면 IP를 통해서 사용자들이 접속하도록 하진 않을것이다.</li>
<li>그렇다면 우리도 <code>도메인 주소</code>를 사용하여 접속할 수 있도록 해야하고, AWS 에서는 이러한 도메인을 관리할 때 사용하는 것이 <code>Route 53</code>이다.</li>
</ul>
<h3 id="도메인">도메인</h3>
<h4 id="도메인이란">도메인이란?</h4>
<ul>
<li><code>문자로된 컴퓨터 주소</code>라고 보시면 되겠습니다. 위에서도 말씀드렸듯이 숫자로만 이루어진 접속 방법은 사용자에게 불편함을 주기때문에 조금 더 쉽게 접속이 가능하도록 만들어진 개념입니다.</li>
</ul>
<h4 id="서브-도메인">서브 도메인</h4>
<ul>
<li>우리가 네이버를 사용하다보면 <code>naver.com</code>, <code>www.naver.com</code>, <code>map.naver.com</code>과 같은 주소를 보셨을 텐데, xxx.naver.com 형태의 도메인을 서브 도메인이라고 합니다.</li>
<li>서브 도메인은 하나의 도메인 아래에서 여러 서비스를 구분하여 관리할때 사용됩니다.</li>
<li>이름이 다르다고해서 각각 구매하지 않아도 된다는 장점이 있습니다.</li>
</ul>
<h4 id="도메인을-사용하는-이유">도메인을 사용하는 이유</h4>
<ul>
<li>위에서 언급한 것처럼 사용자가 좀 더 쉽게 기억하고 접속할 수 있도록 하는 용도도 있지만, 서비스를 개발할때 API를 호출하게 되는경우 일반적으로 IP 주소는 HTTPS를 적용할 수 없기 때문에 도메인을 사용합니다.</li>
</ul>
<h3 id="그렇다면-aws에-서비스를-구축하면-route-53-을-사용해야-하는가">그렇다면 AWS에 서비스를 구축하면 Route 53 을 사용해야 하는가?</h3>
<ul>
<li>그렇지 않음.</li>
<li>DNS마다 제공하는 도메인의 종류와 가격이 다르기 때문에 굳이 Route 53 만을 사용할 필요는 없음.</li>
<li>본인이 원하는 가격 및 도메인 종류에 따라서 다른 DNS를 사용해도 됨.</li>
</ul>
<h2 id="실습">실습</h2>
<h3 id="도메인-구매">도메인 구매</h3>
<blockquote>
<p>유료 플랜으로 변경해야 도메인 구매 가능..
그리고 따로 프리티어랑 관계없이 도메인 구매 비용이 청구되니 이점 유의...</p>
</blockquote>
<ul>
<li><p><code>Route 53</code> 콘솔 접속
<img src="https://velog.velcdn.com/images/os_js/post/afe2baeb-ea10-496c-bf6e-4bfae08852b6/image.png" alt=""></p>
</li>
<li><p>우측의 <code>도메인 등록</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/2103500e-265e-4b6b-a60b-b656e1c61a81/image.png" alt=""></p>
</li>
<li><p>사용할 도메인 구매(아래 표준 요금을 통해서 싼 도메인을 구매하고싶으면 확인 후 구매)</p>
<blockquote>
<p>결제 진행시 자동 갱신은 본인의 상황에 맞게 체크
<img src="https://velog.velcdn.com/images/os_js/post/edeb28d6-dd6f-4630-8307-d1b12b04ec3b/image.png" alt=""></p>
</blockquote>
</li>
</ul>
<ul>
<li><p>구매 완료 후
<img src="https://velog.velcdn.com/images/os_js/post/9dc5c082-218d-46af-a61a-a69f37af4135/image.png" alt=""></p>
</li>
<li><p>구매후 바로 사용 가능한 것은 아니고, 좌측 메뉴의 <code>요청</code> 메뉴를 통해 보면 현재 진행상태가 처리중 이라는 것을 볼 수 있다.
빠르면 10 분 늦어도 12시간 안으로 완료 된다고 함.
<img src="https://velog.velcdn.com/images/os_js/post/09e1384f-1776-4afe-a7c3-aab4f1410137/image.png" alt="">
<img src="https://velog.velcdn.com/images/os_js/post/cfe5e223-8e13-4b55-a431-1c5cdf5bf749/image.png" alt=""></p>
</li>
<li><p>등록 성공
<img src="https://velog.velcdn.com/images/os_js/post/6c16f0bc-1333-4bc3-8515-bb26d74795ca/image.png" alt=""></p>
</li>
</ul>
<h3 id="도메인-연결하기">도메인 연결하기</h3>
<ul>
<li><p>레코드 생성을 위해
좌측 Route 53 메뉴에서 <code>호스팅 영역</code>메뉴 클릭후 <code>호스팅 영역 이름</code> 영역에 나와있는 도메인 이름을 클릭
<img src="https://velog.velcdn.com/images/os_js/post/1b8d4388-a2a3-4b0b-a90e-fa103cd002ba/image.png" alt=""></p>
</li>
<li><p>레코드 생성 클릭
<img src="https://velog.velcdn.com/images/os_js/post/da621f20-dad8-4f27-8c91-e17021a17471/image.png" alt=""></p>
</li>
<li><p>레코드 이름 : 서브 도메인 입력. 용도에 맞게 쓰면 되는데 우선 백엔드의 api 서버의 도메인 주소를 설정하기 위해</p>
</li>
<li><p>레코드 유형 : 레코드와 EC2 인스턴스의 퍼블릭  IP를 연결하기 위해 <code>A 레코드</code>로 선택.</p>
</li>
<li><p>값 : 이전에 생성했던 <code>EC2 인스턴스의 퍼블릭 IP</code> 작성
<img src="https://velog.velcdn.com/images/os_js/post/310c5005-afa8-4c45-8e7a-6ca749ca699b/image.png" alt=""></p>
</li>
<li><p>레코드 생성 클릭</p>
</li>
<li><p>목록에 생성한 레코드 등록 확인
<img src="https://velog.velcdn.com/images/os_js/post/77b45ac0-3928-4395-bbcc-2cde6df0c869/image.png" alt=""></p>
</li>
</ul>
<h3 id="도메인-연결-확인">도메인 연결 확인</h3>
<ul>
<li>이전에 <code>EC2 인스턴스</code>에서 실행했던 스프링 부트 서버를 실행 후 IP, 도메인으로 모두 접속이 가능한지 확인.</li>
<li>결과
<img src="https://velog.velcdn.com/images/os_js/post/4bf10501-f60e-475a-a40a-7a976567f7fd/image.png" alt=""></li>
</ul>
<h2 id="무료-도메인">무료 도메인</h2>
<ul>
<li><code>Route 53</code>을 통해서 도메인을 구매하면 비용이 발생한다.</li>
<li>무료로 도메인을 구매할 방법이 없을까?</li>
</ul>
<h3 id="발급">발급</h3>
<ul>
<li><p><code>내도메인한국</code> 사이트 접속
<img src="https://velog.velcdn.com/images/os_js/post/06363809-58fe-41c4-8e04-469bb4d6f99b/image.png" alt=""></p>
</li>
<li><p>회원가입후 <code>일반 도메인 검색</code>에 원하는 도메인 이름 검색.
<img src="https://velog.velcdn.com/images/os_js/post/260353ff-72d9-418e-aeb1-089ec9b61a75/image.png" alt=""></p>
</li>
<li><p>검색 클릭시 아래와 같이 사용 가능한 도메인 확인 가능
<code>등록 하기</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/f50009f0-6937-4a02-938a-2588e11412b2/image.png" alt=""></p>
</li>
<li><p>보안코드 입력 후 <code>등록 하기</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/45e49ca5-a54b-4423-874c-688d4af27a65/image.png" alt=""></p>
</li>
<li><p>DNS 레코드 설정
<img src="https://velog.velcdn.com/images/os_js/post/5fbb3c0f-832d-47ce-8d29-7e7b3f489970/image.png" alt=""></p>
</li>
<li><p><code>고급설정</code> 에서 <code>IP연결(A)</code> 에 서브도메인 작성후 우측에는 <code>EC2 퍼블릭 IP</code> 작성
<img src="https://velog.velcdn.com/images/os_js/post/405fc50f-df4a-492b-9bf4-d0db445506bb/image.png" alt=""></p>
</li>
<li><p>접속 확인
<img src="https://velog.velcdn.com/images/os_js/post/1d53e687-33f5-4610-a2ea-74ea23230cb1/image.png" alt=""></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS (2) - EC2, 보안그룹]]></title>
            <link>https://velog.io/@os_js/AWS-2-EC2-%EB%B3%B4%EC%95%88%EA%B7%B8%EB%A3%B9</link>
            <guid>https://velog.io/@os_js/AWS-2-EC2-%EB%B3%B4%EC%95%88%EA%B7%B8%EB%A3%B9</guid>
            <pubDate>Tue, 17 Feb 2026 08:05:40 GMT</pubDate>
            <description><![CDATA[<h1 id="ec2란">EC2란?</h1>
<ul>
<li>AWS의 EC2(Elastic Compute Cloud)는 가상 서버를 제공하는 서비스.</li>
</ul>
<h2 id="제공-기능">제공 기능</h2>
<ul>
<li><p>인스턴스
가상 서버.</p>
</li>
<li><p>Amazon Machine Images (AMIs)
서버에 필요한 구성 요소(운영 체제와 추가 소프트웨어 포함)를 패키징하는 인스턴스용 사전 구성 템플릿.</p>
</li>
<li><p>인스턴스 타입
인스턴스의 다양한 CPU, 메모리, 스토리지, 네트워킹 용량 및 그래픽 하드웨어 구성.</p>
</li>
<li><p>Amazon EBS 볼륨
Amazon Elastic Block Store(Amazon EBS)를 사용하는 데이터에 대한 영구 스토리지 볼륨.</p>
</li>
<li><p>인스턴스 스토어 볼륨
인스턴스를 중단, 최대 절전 모드로 전환 또는 종료할 때 삭제되는 임시 데이터용 스토리지 볼륨.</p>
</li>
<li><p>키 페어
인스턴스에 대한 보안 로그인 정보. AWS는 퍼블릭 키를 저장하고 사용자는 프라이빗 키를 안전한 장소에 저장합니다.</p>
</li>
<li><p>보안 그룹
인스턴스에 도달할 수 있는 프로토콜, 포트 및 소스 IP 범위와 인스턴스가 연결할 수 있는 대상 IP 범위를 지정할 수 있는 가상 방화벽.</p>
</li>
</ul>
<h2 id="인스턴스-생성">인스턴스 생성</h2>
<ul>
<li>검색창에 <code>ec2</code> 검색 후 대시보드 접속
<img src="https://velog.velcdn.com/images/os_js/post/7ab94087-1a37-4f2b-8495-97e8a4201910/image.png" alt=""></li>
<li>우측상단의 리전 선택을 <code>서울</code>로 변경
<img src="https://velog.velcdn.com/images/os_js/post/ae556a18-62f7-44ac-bf5b-1fe7d4ce4d72/image.png" alt=""></li>
<li>인스턴스 시작
<img src="https://velog.velcdn.com/images/os_js/post/cd03916f-28d7-4f00-9e3e-755250d5e9e1/image.png" alt="">
<img src="https://velog.velcdn.com/images/os_js/post/1d5d852d-76c6-4974-8abc-758ab355173e/image.png" alt=""></li>
<li>OS 설정(필요에 따라 변경) [Ubuntu]
<img src="https://velog.velcdn.com/images/os_js/post/92550b65-7f22-421f-a42b-1c9456bb8dab/image.png" alt=""></li>
<li>인스턴스 유형(자동으로 선택되긴 합니다만 이것도 필요에 따라서 설정)
<img src="https://velog.velcdn.com/images/os_js/post/d13445a2-729c-4a2b-ab64-5368aa1971f3/image.png" alt=""></li>
<li>키페어(현재 진행에서는 따로 사용안함, 필요에따라 진행)
<img src="https://velog.velcdn.com/images/os_js/post/e8742e1d-b55d-439e-ac35-da14229a6eaa/image.png" alt=""></li>
<li>네트워크 설정(현재는 외부에서 80 포트로 진입할 수 있도록 해당ㄴ ㅐ용만 추가)
<img src="https://velog.velcdn.com/images/os_js/post/c8b78d79-5456-48c7-b0d3-9ebb6b073ac3/image.png" alt=""></li>
<li>스토리지 설정
<img src="https://velog.velcdn.com/images/os_js/post/fa498d8a-37b4-4fc9-a81d-eecbfce94670/image.png" alt=""></li>
<li>인스턴스 시작
<img src="https://velog.velcdn.com/images/os_js/post/82fe445e-e57f-4fb5-a3e5-46c5f2c68688/image.png" alt=""></li>
</ul>
<h3 id="인스턴스-확인">인스턴스 확인</h3>
<ul>
<li><p>좌측 메뉴의 <code>인스턴스</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/2254fbdb-6c7f-40bf-8309-8c585061d67c/image.png" alt=""></p>
</li>
<li><p>생성된 <code>인스턴스</code> 확인 (현재 사용중이 아니라 종료. - 바로 생성하고나면 실행중)
<img src="https://velog.velcdn.com/images/os_js/post/f3eb9fb6-9489-4264-8f00-9b3e6ad65214/image.png" alt=""></p>
</li>
<li><p>항목에서 <code>인스턴스 ID</code> 클릭. (ip주소 및 현재 상태, 우측 상단의 <code>인스턴스 상태</code>를 통해 중지 및 시작 가능)
<img src="https://velog.velcdn.com/images/os_js/post/763fe337-e9db-4edf-95d4-cf6bbae649b3/image.png" alt=""></p>
</li>
<li><p>밑에 보시면 인스턴스에 대한 <code>상세 메뉴</code> 들이 존재
여기서 사용량 및 성능지표를 확인할 수 있음.
<img src="https://velog.velcdn.com/images/os_js/post/3f9875e1-949f-4eae-abc2-a0c09d5db2d7/image.png" alt=""></p>
</li>
</ul>
<h3 id="인스턴스-접속">인스턴스 접속</h3>
<ul>
<li><p>인스턴스가 실행중인 경우 우측 상단에서 <code>연결</code> 버튼을 통해서 인스턴에 연결이 가능함.
<img src="https://velog.velcdn.com/images/os_js/post/88792c8f-4737-4f07-92b8-a80b10ede00b/image.png" alt=""></p>
</li>
<li><p>그리고 우측 하단의 <code>연결</code>을 클릭
<img src="https://velog.velcdn.com/images/os_js/post/154faf36-0eaf-4ed0-8511-6b0b78984593/image.png" alt=""></p>
</li>
<li><p>접속이 정상적이면 아래와 같이 접속됨.
<img src="https://velog.velcdn.com/images/os_js/post/eb6ffad9-d8d6-495a-b1e7-3ed12c6167c5/image.png" alt=""></p>
</li>
</ul>
<h2 id="탄력적-ip">탄력적 IP</h2>
<h3 id="탄력적-ip란">탄력적 IP란?</h3>
<ul>
<li>탄력적 IP란 동적 클라우드 컴퓨팅을 위해 고안된 정적 IPv4주소를 말합니다.</li>
<li>위에서 보신 퍼블릭 IP의 경우 인스턴스를 중지 후 다시 시작하게 되면 이전 IP와 달라지게 됩니다. 그렇다면 해당 인스턴스와 통신하던 다른 서비스나 인스턴의경우 매번 IP를 변경해줘야 하는데, 이러한 문제가 일어나지 않기 위해 정적 Ip가 필요합니다.</li>
</ul>
<h3 id="할당">할당</h3>
<ul>
<li><p>좌측 메뉴 <code>탄력적 IP</code> 선택 -&gt; 우측 상단의 <code>탄력적 IP 주소 할당</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/ccf4dcc7-ba3f-4715-ab6b-66f469fce117/image.png" alt=""></p>
</li>
<li><p>할당 받기
<img src="https://velog.velcdn.com/images/os_js/post/67fb0b4a-2740-4588-926f-9a9e2856aec1/image.png" alt="">
<img src="https://velog.velcdn.com/images/os_js/post/70b4f0b2-3007-407b-b739-3c85ffd2c925/image.png" alt=""></p>
</li>
<li><p>좌측의 체크박스 선택후 우측의 <code>작업</code> 클릭 -&gt; <code>탄력적 IP 주소 연결</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/a3171a63-caea-4b13-857f-deb25551ccc9/image.png" alt=""></p>
</li>
<li><p>인스턴스 선택후 우측 하단 <code>연결</code> 클릭
<img src="https://velog.velcdn.com/images/os_js/post/f6450b60-b9ad-4269-b8c3-f53093195fa9/image.png" alt=""></p>
</li>
<li><p>아래와 같은 알람이 뜨면 정상 연결된 것.
<img src="https://velog.velcdn.com/images/os_js/post/b6ff8fd6-40e5-453e-a035-460ce103144d/image.png" alt=""></p>
</li>
<li><p>이후 다시 <code>인스턴스</code> 메뉴로 돌아가서 IP를 확인해보면 할당받은 IP로 연결된 것을 확인할 수 있음.</p>
</li>
</ul>
<h2 id="간단하게-스프링부트-서버-배포하기">간단하게 스프링부트 서버 배포하기</h2>
<h3 id="jdk-설치">JDK 설치</h3>
<ul>
<li>스프링 부트 3.x.x 버전</li>
<li>JDK 17 버전</li>
<li>설치<pre><code>sudo apt update
sudo apt install openjdk-17-jdk -y
java -version</code></pre></li>
<li>결과
<img src="https://velog.velcdn.com/images/os_js/post/94b13eae-a280-48b5-b93d-a4f0a8035d4e/image.png" alt=""></li>
</ul>
<h3 id="스프링-부트-프로젝트">스프링 부트 프로젝트</h3>
<ul>
<li>예제 파일 다운로드
<code>git clone https://github.com/JSCODE-BOOK/aws-ec2-springboot.git</code>
<code>cd aws-ec2-springboot/src/main/resources</code></li>
<li>서버 접속 환경 설정
<code>vi application.yml</code><pre><code>server:
port: 80</code></pre></li>
<li>빌드<pre><code>cd ~/aws-ec2-springboot/ # build를 위한 이동
./gradlew clean build -x test # build 시작
cd build/libs # jar위치 이동
sudo nohup java -jar aws-ec2-springboot-0.0.1-SNAPSHOT.jar &amp; # jar 실행 시작
sudo lsof -i:80 # 실제 실행중인지 확인</code></pre></li>
<li>확인<blockquote>
<p>혹시 연결할 수 없다고 나온다면 앞에 http 인지 확인 해보세요.</p>
</blockquote>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/os_js/post/b2343060-1065-430b-a99c-b5fcba1c7030/image.png" alt=""></p>
<ul>
<li>종료
<code>sudo lsof -i:80</code> 결과에서 PID 칼럼을 확인.
<code>sudo kill &lt;PID&gt;</code> 종료
<code>sudo lsof -i:80</code> 아무것도 나오지 않으면 성공.</li>
</ul>
<h1 id="보안그룹security-group">보안그룹(Security Group)</h1>
<h2 id="보안그룹-이란">보안그룹 이란?</h2>
<ul>
<li>기본적으로 아무나 접근하지 못하도록 규칙을 정해놓을 수 있음.</li>
<li>EC2, RDS등 AWS의 다양한 자원이 수신/발신하는 트래픽을 제어하는 가상 방화벽 역할.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS (1)]]></title>
            <link>https://velog.io/@os_js/AWS-1</link>
            <guid>https://velog.io/@os_js/AWS-1</guid>
            <pubDate>Tue, 17 Feb 2026 08:05:06 GMT</pubDate>
            <description><![CDATA[<h1 id="클라우드-컴퓨팅">클라우드 컴퓨팅</h1>
<ul>
<li>서버를 운영하기 위해서는 물리적인 리소스가 필요하다.
이러한 리소스를 직접 구성하여 운영하려면 현재 필요한 리소스나 이후에 확장을 고려한 서버 설계를 필요로 한다.</li>
<li>클라우드 컴퓨팅은 사용자가 물리적인 리소스 구축을 생각하지 않고 초기에 적은 비용으로 서버를 구축할 수 있게 해주는 서비스이다.</li>
<li>하지만 클라우드가 무조건 적으로 좋다는건 아니니 상황에 맞게 사용해야할 것이다.</li>
</ul>
<h2 id="on-premise-vs-cloud">on-premise vs cloud</h2>
<h3 id="on-premise-온프레미스">On-premise (온프레미스)</h3>
<ul>
<li><p>장점</p>
<ul>
<li>보안성: 물리적 접근을 제어할 수 있어, 데이터 유출에 대한 위험을 최소화할 수 있습니다. 특히 중요한 데이터를 다루는 경우, 외부와의 연결을 끊어둘 수 있어 더 안전합니다.</li>
<li>완전한 통제: 서버와 네트워크, 데이터베이스 등 시스템의 모든 부분을 완전히 제어할 수 있습니다. 변경 사항이나 업데이트도 자유롭게 할 수 있어, 기업의 요구에 맞는 맞춤화가 가능합니다.</li>
<li>규제 준수: 특정 산업에서는 법적 규제를 준수해야 하므로, 데이터를 외부 클라우드로 옮길 수 없는 경우가 많습니다. 온프레미스를 사용하면 이러한 규제를 더 쉽게 충족할 수 있습니다.</li>
<li>장기적인 비용 절감: 초기 투자 비용은 크지만, 장기적으로는 클라우드 서비스의 지속적인 요금 지불을 피할 수 있어 비용이 절감될 수 있습니다.</li>
</ul>
</li>
<li><p>단점</p>
<ul>
<li>초기 설치 및 유지관리 비용: 서버와 하드웨어를 구입하고 유지하는 비용이 발생합니다. 이에는 시스템 설치, 전기, 냉각, 관리인력 등이 포함됩니다.</li>
<li>확장성 제한: 서버의 용량이 한정적이므로, 트래픽이 급격하게 증가하거나, 더 많은 자원이 필요할 경우 확장이 어려울 수 있습니다. 이를 위해 추가 하드웨어를 구입하거나 업그레이드해야 하므로 비용과 시간이 많이 듭니다.</li>
<li>재해 복구 문제: 자연재해나 서버 고장으로 인한 데이터 손실을 방지하기 위해서는 별도의 백업 시스템과 복구 계획이 필요합니다.</li>
<li>운영 및 관리의 복잡성: 서버를 운영하기 위해서는 IT 관리 팀이 필요하며, 시스템이 정상적으로 작동하도록 지속적으로 모니터링하고 문제를 해결해야 합니다.</li>
</ul>
</li>
</ul>
<h3 id="cloud-클라우드">Cloud (클라우드)</h3>
<ul>
<li><p>장점</p>
<ul>
<li>비용 효율성: 초기 설치 비용이 없고, 필요한 자원을 필요에 따라 확장할 수 있어 매우 유연합니다. 사용한 만큼만 비용을 지불하기 때문에, 예산 관리가 용이합니다.</li>
<li>확장성: 클라우드는 빠르게 확장이 가능합니다. 트래픽이 증가하거나 자원이 부족할 경우, 쉽게 용량을 늘릴 수 있습니다.</li>
<li>높은 가용성: 클라우드 서비스 제공자는 여러 지역에 데이터 센터를 운영하며, 서비스가 다운되는 일이 거의 없도록 여러 단계의 이중화와 백업을 제공합니다.</li>
<li>자동화 및 관리 편의성: 많은 클라우드 서비스는 자동화된 관리 도구와 모니터링 시스템을 제공하여, 서버 유지보수와 관리가 용이합니다. IT 관리자가 직접 모든 것을 신경 쓸 필요가 없습니다.</li>
</ul>
</li>
<li><p>단점</p>
<ul>
<li>보안 문제: 클라우드에 데이터를 저장하면, 그 데이터에 대한 외부 접근이 있을 수 있습니다. 보안이 잘 관리되지 않으면, 데이터 유출이나 해킹의 위험이 존재합니다.</li>
<li>의존성: 클라우드 제공 업체에 의존하게 되며, 서비스 수준이나 가격 정책이 변경될 수 있어 장기적인 계획에 불확실성이 생길 수 있습니다.</li>
<li>규제 및 법적 문제: 일부 산업에서는 클라우드를 사용하는 것에 제한이 있을 수 있습니다. 예를 들어, 특정 국가에서 운영되는 데이터 센터에 데이터가 저장되면, 법적으로 문제가 될 수 있습니다.</li>
<li>인터넷 의존성: 클라우드 서비스는 인터넷 연결에 의존하므로, 네트워크 장애가 발생하면 서비스 이용에 큰 영향을 미칠 수 있습니다.</li>
</ul>
</li>
</ul>
<h2 id="aws-리전">AWS 리전</h2>
<h3 id="리전이란">리전이란?</h3>
<ul>
<li>AWS가 전 세계에서 데이터 센터를 클러스터링하는 물리적 위치를 리전이라고 한다.
(컴퓨터들을 설치해놓은 물리적 위치)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI Agent (5) 음성 텍스트 변환-2]]></title>
            <link>https://velog.io/@os_js/AI-Agent-5-%EC%9D%8C%EC%84%B1-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%B3%80%ED%99%98-2</link>
            <guid>https://velog.io/@os_js/AI-Agent-5-%EC%9D%8C%EC%84%B1-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%B3%80%ED%99%98-2</guid>
            <pubDate>Tue, 17 Feb 2026 02:34:29 GMT</pubDate>
            <description><![CDATA[<h1 id="기능-추가">기능 추가</h1>
<h2 id="pyannotespeaker-diarization-31">pyannote/speaker-diarization-3.1</h2>
<ul>
<li>해당 모델도 음성을 텍스트로 변환해주는 모델인데, 음성에서 나오는 대상을 구분하여 나타낼 수 있다는 차이점이 있다.</li>
<li>사용하기위해서는 HuggingFace에 접속하여, 회원가입 및 로그인 후 우측 상단의 프로필을 클릭하여 <code>Access Token</code> 항목을 들어가서 토큰을 발급해야 한다.(아직 발급하지말고 아래 읽고 진행)</li>
<li>토큰 발급시 <code>https://huggingface.co/pyannote/segmentation-3.0</code>, <code>https://huggingface.co/pyannote/speaker-diarization-3.1</code>, <code>https://huggingface.co/pyannote/speaker-diarization-community-1</code> 에 접속하여 소속 기관 및 웹사이트 정보를 입력해야 하는데, 연습용 이므로 [company/university]부분에 <code>Personal</code>, [WebSite]에 <code>Nothing</code>이라고 적어둔다.</li>
<li>이후, <code>Access Token</code> 메뉴에 들어가서 
<img src="https://velog.velcdn.com/images/os_js/post/f41d7ced-eded-47e7-a65f-0af0d52ac552/image.png" alt="">
사진과 같이 보이는 부분을 모두 선택한 후 발급을 마친다.</li>
<li>발급을 마치면 토큰은 저장해야 한다.</li>
</ul>
<h2 id="코드-작성">코드 작성</h2>
<ul>
<li>예제 코드<pre><code># instantiate the pipeline
from pyannote.audio import Pipeline
pipeline = Pipeline.from_pretrained(
&quot;pyannote/speaker-diarization-3.1&quot;,
use_auth_token=&quot;HUGGINGFACE_ACCESS_TOKEN_GOES_HERE&quot;)
</code></pre></li>
</ul>
<h1 id="run-the-pipeline-on-an-audio-file">run the pipeline on an audio file</h1>
<p>diarization = pipeline(&quot;audio.wav&quot;)</p>
<h1 id="dump-the-diarization-output-to-disk-using-rttm-format">dump the diarization output to disk using RTTM format</h1>
<p>with open(&quot;audio.rttm&quot;, &quot;w&quot;) as rttm:
    diarization.write_rttm(rttm)</p>
<pre><code>- 내가 작성한 코드(예제와 다르니 참고)</code></pre><h1 id="instantiate-the-pipeline">instantiate the pipeline</h1>
<p>from pyannote.audio import Pipeline
import torch
pipeline = Pipeline.from_pretrained(
  &quot;pyannote/speaker-diarization-3.1&quot;,token=token
  )</p>
<p>if torch.cuda.is_available() :
  pipeline.to(torch.device(&quot;cuda&quot;))
  print(&#39;use cuda&#39;)
else :
  print(&quot;not use cuda&quot;)</p>
<h1 id="pydub을-이용한-변환-예시-설치-pip-install-pydub">pydub을 이용한 변환 예시 (설치: pip install pydub)</h1>
<p>from pydub import AudioSegment</p>
<p>audio = AudioSegment.from_file(&lt;파일경로&gt;)
audio = audio.set_frame_rate(16000).set_channels(1) # 16kHz, 모노로 변환
audio.export(&quot;temp_audio.wav&quot;, format=&quot;wav&quot;)</p>
<h1 id="이후-pipeline에는-변환된-파일을-넣습니다">이후 pipeline에는 변환된 파일을 넣습니다.</h1>
<p>diarization = pipeline(&quot;temp_audio.wav&quot;)
ann = diarization.speaker_diarization  </p>
<h1 id="dump-the-diarization-output-to-disk-using-rttm-format-1">dump the diarization output to disk using RTTM format</h1>
<p>with open(&quot;audio.rttm&quot;, &quot;w&quot;, encoding=&#39;utf-8&#39;) as rttm:
    ann.write_rttm(rttm)</p>
<pre><code>
- 오류 해결
1. `user_auth_token` 부분도 오류가 발생해서 `token`으로 변경 후 해결.
2. `.mp3` 파일을 그대로 사용하려 했으나 `file resulted in 439895 samples instead of the expected 441000 samples.` 오류가 지속적으로 발생하여 `.wav`파일로 변환하여 진행하였다.
3. `AttributeError: &#39;DiarizeOutput&#39; object has no attribute &#39;write_rttm&#39;` 오류 발생. 내가 사용하는 `pyannote.audio` 버전이 `4.0.4` 이다.
해당 버전은 파이프라인이 `DiarizeOutput`객체를 반환한다.
그래서 DiarizeOutput으로 부터 `Annotation` 을 가져올 수 있도록 코드를 수정했다.

- 결과
실제로 아래와 같이 결과가 정상적으로 나오는 걸 볼 수 있다.
![](https://velog.velcdn.com/images/os_js/post/36d20518-8021-4b54-9214-56eaa8a835bc/image.png)


## 화자별 텍스트 나누기
- 지금까지 작성한 내용을 바탕으로 음성파일에서 텍스트를 추출하고, 화자를 나누어 알맞은 텍스트를 csv 형태로 생성하도록 한다.</code></pre><p>import os
import pandas as pd
from pathlib import Path
os.environ[&quot;PATH&quot;] += os.pathsep + r&quot;D:\systems\ffmpeg-7.1.1-full_build-shared\bin&quot;
os.add_dll_directory(str(Path(r&quot;D:\systems\ffmpeg-7.1.1-full_build-shared\bin&quot;)))</p>
<p>from dotenv import load_dotenv
import torch
from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor, pipeline
from pyannote.audio import Pipeline
from pydub import AudioSegment</p>
<h1 id="from-datasets-import-load_dataset">from datasets import load_dataset</h1>
<p>load_dotenv()
token = os.getenv(&quot;HF_TOKEN&quot;)</p>
<p>def whisper_stt(audio_file_path:str, output_file_path:str = &quot;./output.csv&quot;) :
    device = &quot;cuda:0&quot; if torch.cuda.is_available() else &quot;cpu&quot;
    torch_dtype =  torch.float32</p>
<pre><code>model_id = &quot;openai/whisper-large-v3-turbo&quot;

model = AutoModelForSpeechSeq2Seq.from_pretrained(
    model_id, torch_dtype=torch_dtype, low_cpu_mem_usage=True, use_safetensors=True
)
model.to(device)

processor = AutoProcessor.from_pretrained(model_id)

pipe = pipeline(
    &quot;automatic-speech-recognition&quot;,
    model=model,
    tokenizer=processor.tokenizer,
    feature_extractor=processor.feature_extractor,
    torch_dtype=torch_dtype,
    device=device,
    return_timestamps=True
)

# dataset = load_dataset(&quot;distil-whisper/librispeech_long&quot;, &quot;clean&quot;, split=&quot;validation&quot;)


result = pipe(audio_file_path)
print(result[&quot;text&quot;])

df = whisper_to_dataframe(result, output_file_path)

return result, df</code></pre><p>def whisper_to_dataframe(result, output_file_path) :
    start_end_text = []</p>
<pre><code>for chunk in result[&#39;chunks&#39;] :
    start = chunk[&quot;timestamp&quot;][0]
    end = chunk[&quot;timestamp&quot;][1]
    text = chunk[&quot;text&quot;].strip()
    start_end_text.append([start, end,text])
    df = pd.DataFrame(start_end_text, columns=[&quot;start&quot;, &quot;end&quot;, &quot;text&quot;])
    df.to_csv(output_file_path, index=False, sep=&quot;|&quot;)

return df</code></pre><p>def speaker_diarization(audio_file_path:str, output_rttm_file_path:str, output_csv_file_path:str) :</p>
<pre><code>pipeline = Pipeline.from_pretrained(
    &quot;pyannote/speaker-diarization-3.1&quot;,token=token
)

if torch.cuda.is_available() :
    pipeline.to(torch.device(&quot;cuda&quot;))
    print(&#39;use cuda&#39;)
else :
    print(&quot;not use cuda&quot;)

audio = AudioSegment.from_file(audio_file_path)
audio = audio.set_frame_rate(16000).set_channels(1) # 16kHz, 모노로 변환
audio.export(&quot;temp_audio.wav&quot;, format=&quot;wav&quot;)

# 이후 pipeline에는 변환된 파일을 넣습니다.
diarization = pipeline(&quot;temp_audio.wav&quot;)
ann = diarization.speaker_diarization  
# dump the diarization output to disk using RTTM format
with open(output_rttm_file_path, &quot;w&quot;, encoding=&#39;utf-8&#39;) as rttm:
    ann.write_rttm(rttm)


df_rttm = pd.read_csv(output_rttm_file_path,
                    sep=&#39; &#39;,
                    header=None,
                    names=[&#39;type&#39;, &#39;file&#39;, &#39;chnl&#39;, &#39;start&#39;, &#39;duration&#39;, &#39;C1&#39;, &#39;C2&#39;, &#39;speaker_id&#39;, &#39;C3&#39;, &#39;C4&#39;])

df_rttm[&#39;end&#39;] = df_rttm[&#39;start&#39;] + df_rttm[&#39;duration&#39;]
df_rttm[&#39;number&#39;] = None

df_rttm.at[0, &quot;number&quot;] = 0

for i in range(1, len(df_rttm)) :
    if df_rttm.at[i, &quot;speaker_id&quot;] != df_rttm.at[i-1, &quot;speaker_id&quot;]:
        df_rttm.at[i, &quot;number&quot;] = df_rttm.at[i-1, &quot;number&quot;] + 1
    else :
        df_rttm.at[i, &quot;number&quot;] = df_rttm.at[i-1, &quot;number&quot;]


df_rttm_grouped = df_rttm.groupby(&quot;number&quot;).agg(
    start=pd.NamedAgg(column=&#39;start&#39;, aggfunc=&#39;min&#39;),
    end=pd.NamedAgg(column=&#39;end&#39;, aggfunc=&#39;max&#39;),
    speaker_id=pd.NamedAgg(column=&#39;speaker_id&#39;, aggfunc=&#39;first&#39;)
)

df_rttm_grouped[&quot;duration&quot;] = df_rttm_grouped[&quot;end&quot;] + df_rttm_grouped[&quot;start&quot;]

df_rttm_grouped.to_csv(
    output_csv_file_path,
    index=False,
    encoding=&#39;utf-8&#39;
)

return df_rttm_grouped</code></pre><p>def stt_to_rttm(
        audio_file_path: str,
        stt_output_file_path: str,
        rttm_file_path: str,
        rttm_csv_file_path: str,
        final_output_csv_file_path: str
    ):</p>
<pre><code>result, df_stt = whisper_stt(
    audio_file_path, 
    stt_output_file_path
) 

df_rttm = speaker_diarization(
    audio_file_path,
    rttm_file_path,
    rttm_csv_file_path
) 

df_rttm[&quot;text&quot;] = &quot;&quot; 

for i_stt, row_stt in df_stt.iterrows(): 
    overlap_dict = {}
    for i_rttm, row_rttm in df_rttm.iterrows(): 
        overlap = max(0, min(row_stt[&quot;end&quot;], row_rttm[&quot;end&quot;]) - max(row_stt[&quot;start&quot;], row_rttm[&quot;start&quot;]))
        overlap_dict[i_rttm] = overlap

    max_overlap = max(overlap_dict.values())
    max_overlap_idx = max(overlap_dict, key=overlap_dict.get)

    if max_overlap &gt; 0: 
        df_rttm.at[max_overlap_idx, &quot;text&quot;] += row_stt[&quot;text&quot;] + &quot;\n&quot;

df_rttm.to_csv(
    final_output_csv_file_path,
    index=False,    # 인덱스는 저장하지 않음
    sep=&#39;|&#39;,
    encoding=&#39;utf-8&#39;
)  # ④
return df_rttm</code></pre><p>if <strong>name</strong> == &quot;<strong>main</strong>&quot; :</p>
<pre><code>audio_file_path = r&quot;초기 입력 음성 데이터&quot;
stt_otuput_file_path = r&quot;./stt.csv&quot;
rttm_file_path = &quot;./stt.rttm&quot;
rttm_csv_file_path=&#39;stt_rttm.csv&#39;
final_csv_file_path = &quot;final.csv&quot;

# result, df = whisper_stt(audio_file_path, &quot;./stt.csv&quot;)
# df_rttm = speaker_diarization(audio_file_path, rttm_file_path, rttm_csv_file_path)
# print(df_rttm)

df_rttm = stt_to_rttm(
    audio_file_path,
    stt_otuput_file_path,
    rttm_file_path,
    rttm_csv_file_path,
    final_csv_file_path
)

print(df_rttm)</code></pre><pre><code>- 결과
![](https://velog.velcdn.com/images/os_js/post/0ba0e189-cca3-426e-b6f6-45ce6c8b30ea/image.png)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[AI Agent (5) 음성 텍스트 변환-1]]></title>
            <link>https://velog.io/@os_js/AI-Agent-5-%EC%9D%8C%EC%84%B1-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%B3%80%ED%99%98-1</link>
            <guid>https://velog.io/@os_js/AI-Agent-5-%EC%9D%8C%EC%84%B1-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%B3%80%ED%99%98-1</guid>
            <pubDate>Mon, 16 Feb 2026 08:31:47 GMT</pubDate>
            <description><![CDATA[<h1 id="openai의-whisper-api">openai의 whisper API</h1>
<h2 id="코드-작성">코드 작성</h2>
<pre><code>from openai import OpenAI
from dotenv import load_dotenv
import os

load_dotenv()
api_key = os.getenv(&quot;OPEN_API_KEY&quot;)
client = OpenAI(api_key=api_key)

audio_file = &lt;파일 경로&gt;

with open(audio_file, &#39;rb&#39;) as audio :
    transcription = client.audio.transcriptions.create(
        model=&#39;whisper-1&#39;,
        file=audio
    )
print(transcription)</code></pre><h1 id="hugginface-openaiwhisper-large-v3-turbo">HugginFace openai/whisper-large-v3-turbo</h1>
<h2 id="모델-사용하기">모델 사용하기</h2>
<ul>
<li><p><a href="https://huggingface.co/">https://huggingface.co/</a> 에 접속하여 <code>openai/whisper-large-v3-turbo</code> 검색.</p>
</li>
<li><p>예제를 통해서 실행하면 되지만 기본적으로 ffmpeg이 설치되어 있어야함.</p>
</li>
<li><p><a href="https://www.gyan.dev/ffmpeg/builds/">https://www.gyan.dev/ffmpeg/builds/</a> 에 접속하여 <code>ffmpeg-release-full-shared.7z</code> 다운로드</p>
</li>
<li><p>압축해제후 <code>import torch</code> 전 라인에 <code>os.add_dll_directory(str(Path(&lt;압축 해제한 ffmpeg 폴더의 bin 폴더 경로&gt;)))</code> 추가.</p>
<blockquote>
<p>파이썬 3.8 버전부터는 Windows 보안 강화로 인해 os.environ[&#39;PATH&#39;]에 경로를 추가하는 것만으로는 DLL 파일을 불러올 수 없습니다.
경로내 dll파일이 존재해야합니다.</p>
</blockquote>
</li>
<li><p>huggingface의 예제를 그대로 가져와 코드를 추가후. 실행합니다.</p>
<blockquote>
<p>혹시, 오류가 발생하지 않으나 결과값이 비어있거나 이상하게 나온다면 <code>torch_dtype = torch.float16 if torch.cuda.is_available() else torch.float32</code> 부분에서 torch.float32로 변경하여 사용해보시기 바랍니다.</p>
</blockquote>
</li>
<li><p>정상 작동 코드</p>
<pre><code>import os
from pathlib import Path
os.environ[&quot;PATH&quot;] += os.pathsep + r&quot;&lt;ffmpeg bin 폴더 경로&gt;&quot;
os.add_dll_directory(str(Path(r&quot;&lt;ffmpeg bin 폴더 경로&gt;&quot;)))
import torch
from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor, pipeline
from datasets import load_dataset

</code></pre></li>
</ul>
<p>device = &quot;cuda:0&quot; if torch.cuda.is_available() else &quot;cpu&quot;
torch_dtype =  torch.float32</p>
<p>model_id = &quot;openai/whisper-large-v3-turbo&quot;</p>
<p>model = AutoModelForSpeechSeq2Seq.from_pretrained(
    model_id, torch_dtype=torch_dtype, low_cpu_mem_usage=True, use_safetensors=True
)
model.to(device)</p>
<p>processor = AutoProcessor.from_pretrained(model_id)</p>
<p>pipe = pipeline(
    &quot;automatic-speech-recognition&quot;,
    model=model,
    tokenizer=processor.tokenizer,
    feature_extractor=processor.feature_extractor,
    torch_dtype=torch_dtype,
    device=device,
    return_timestamps=True
)</p>
<p>dataset = load_dataset(&quot;distil-whisper/librispeech_long&quot;, &quot;clean&quot;, split=&quot;validation&quot;)
sample = dataset[0][&quot;audio&quot;]</p>
<p>result = pipe(sample)
print(result[&quot;text&quot;])</p>
<pre><code>
### csv로 저장
- 이렇게 뽑아낸 결과를 통해서 언제 어떤 말을 했는지 알 수 있다.
- csv로 저장해보자.</code></pre><p>start_end_text = []</p>
<p>for chunk in result[&quot;chunks&quot;] :
    start = chunk[&quot;timestamp&quot;][0]
    end = chunk[&quot;timestamp&quot;][1]
    text = chunk[&quot;text&quot;]
    start_end_text.append([start, end, text])</p>
<p>import pandas as pd</p>
<p>df = pd.DataFrame(start_end_text, columns=[&quot;start&quot;, &#39;end&#39;, &#39;text&#39;])
df.to_csv(&quot;lsy_audio_2023_58s.csv&quot;, index=False, sep=&quot;|&quot;)
display(df)</p>
<p>```</p>
<ul>
<li>결과
<img src="https://velog.velcdn.com/images/os_js/post/4a94afbb-6dac-49c5-8aab-b2ae6a4bbb97/image.png" alt=""></li>
</ul>
<h2 id="오류-해결">오류 해결</h2>
<ul>
<li>torchcodec 호환성 문제. </li>
<li>다른 기능이 정상적으로 동작하지 않아. torch 버전을 2.8.0 으로 낮췄다. 당연히 다른 것들도..<blockquote>
<p>호환성 참고 : <a href="https://pytorch.kr/get-started/compatibility/">https://pytorch.kr/get-started/compatibility/</a></p>
</blockquote>
</li>
<li>여기서 계속 ffmpeg 오류가 발생한 것이다.</li>
<li>내가 사용중인 ffmpeg 버전이 8.0.1 이었고, 다른 곳에서 보니 7.1.1 버전으로 사용했다고 해서 ffmpeg을 7.1.1 버전으로 설치후 동작했더니 정상 작동하였다.<blockquote>
<p>이슈 참고 : <a href="https://github.com/meta-pytorch/torchcodec/issues/1108">https://github.com/meta-pytorch/torchcodec/issues/1108</a>
<a href="https://github.com/meta-pytorch/torchcodec/issues/912#issuecomment-3450048176">https://github.com/meta-pytorch/torchcodec/issues/912#issuecomment-3450048176</a></p>
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ AI Agent(4) 매우 간단한 챗봇]]></title>
            <link>https://velog.io/@os_js/AI-Agent4-%EC%B1%97%EB%B4%87-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</link>
            <guid>https://velog.io/@os_js/AI-Agent4-%EC%B1%97%EB%B4%87-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</guid>
            <pubDate>Mon, 16 Feb 2026 08:06:09 GMT</pubDate>
            <description><![CDATA[<h1 id="챗봇-만들기">챗봇 만들기</h1>
<ul>
<li>OpenAI API를 활용하여 챗봇을 만든다.</li>
</ul>
<h2 id="간단-예제">간단 예제</h2>
<ul>
<li>코드 작성<pre><code>from openai import OpenAI
import os
from dotenv import load_dotenv
</code></pre></li>
</ul>
<p>load_dotenv()</p>
<p>client = OpenAI(api_key=os.environ.get(&quot;OPEN_API_KEY&quot;))</p>
<p>def chatbot_response(user_message: str) :
    result = client.responses.create(model=&quot;gpt-5-mini&quot;, input=user_message)
    return result</p>
<p>if <strong>name</strong> == &quot;<strong>main</strong>&quot; :
    while True :
        user_message = input(&quot;메시지: &quot;)
        if user_message.lower() == &quot;exit&quot; :
            print(&quot;대화를 종료합니다.&quot;)</p>
<pre><code>        break



    result = chatbot_response(user_message)
    print(&quot;챗봇 : &quot; + result.output_text)</code></pre><pre><code>- 결과
![](https://velog.velcdn.com/images/os_js/post/2119041e-8867-472a-b910-228811126a17/image.png)
첨부한 결과 이미지와 같이 위 코드로는 챗봇이 이전 대화를 기억하지 못한다.

## 대화 기억
- id를 사용하여 이전에 나누었던 대화를 기억할 수 있도록 코드 작성</code></pre><p>from openai import OpenAI
import os
from dotenv import load_dotenv</p>
<p>load_dotenv()</p>
<p>client = OpenAI(api_key=os.environ.get(&quot;OPEN_API_KEY&quot;))</p>
<p>def chatbot_response(user_message: str, previous_response_id=None) :
    result = client.responses.create(model=&quot;gpt-5-mini&quot;, input=user_message, previous_response_id=previous_response_id)
    return result</p>
<p>if <strong>name</strong> == &quot;<strong>main</strong>&quot; :
    previous_response_id = None
    while True :
        user_message = input(&quot;메시지: &quot;)
        if user_message.lower() == &quot;exit&quot; :
            print(&quot;대화를 종료합니다.&quot;)</p>
<pre><code>        break



    result = chatbot_response(user_message, previous_response_id)
    previous_response_id = result.id
    print(&quot;챗봇 : &quot; + result.output_text)</code></pre><pre><code>- 결과
![](https://velog.velcdn.com/images/os_js/post/9123c24c-ed0f-4691-a4ec-7edffdd8c1d4/image.png)

- 이제까지 API를 사용한 이력에 대해서는 
https://platform.openai.com/logs 를 통해서 확인 가능하다.

### 어린왕자 페르소나 추가
</code></pre><p>from openai import OpenAI
from dotenv import load_dotenv</p>
<p>import os</p>
<p>load_dotenv()
api_key = os.environ.get(&quot;OPEN_API_KEY&quot;)</p>
<p>openai_client = OpenAI(api_key=api_key)</p>
<h1 id="어린왕자-페르소나">어린왕자 페르소나</h1>
<p>LITTLE_PRINCE_PERSONA = &quot;&quot;&quot;
당신은 생텍쥐페리의 &#39;어린 왕자&#39;입니다. 다음 특성을 따라주세요:</p>
<ol>
<li>순수한 관점으로 세상을 바라봅니다.</li>
<li>&quot;어째서?&quot;라는 질문을 자주 하며 호기심이 많습니다.</li>
<li>철학적 통찰을 단순하게 표현합니다.</li>
<li>&quot;어른들은 참 이상해요&quot;라는 표현을 씁니다.</li>
<li>B-612 소행성에서 왔으며 장미와의 관계를 언급합니다.</li>
<li>여우의 &quot;길들임&quot;과 &quot;책임&quot;에 대한 교훈을 중요시합니다.</li>
<li>&quot;중요한 것은 눈에 보이지 않아&quot;라는 문장을 사용합니다.</li>
<li>공손하고 친절한 말투를 사용합니다. </li>
<li>비유와 은유로 복잡한 개념을 설명합니다.</li>
</ol>
<p>항상 간결하게 답변하세요. 길어야 2-3문장으로 응답하고, 어린 왕자의 순수함과 지혜를 담아내세요. 
복잡한 주제도 본질적으로 단순화하여 설명하세요.
&quot;&quot;&quot;</p>
<p>def chatbot_response(user_message: str, previous_id = None) :</p>
<pre><code>result = openai_client.responses.create(
    model = &quot;gpt-5-mini&quot;,
    reasoning={&quot;effort&quot; : &quot;low&quot;},
    instructions=LITTLE_PRINCE_PERSONA,
    input=user_message,
    previous_response_id=previous_id
)

return result</code></pre><p>if <strong>name</strong> == &quot;<strong>main</strong>&quot; :
    previous_id = None
    while True :
        user_message = input(&quot;메시지 : &quot;)
        if user_message.lower() == &quot;exit&quot;:
            break</p>
<pre><code>    result = chatbot_response(user_message, previous_id)
    previous_id = result.id

    print(f&quot;챗봇 응답 : {result.output_text}&quot;)</code></pre><p>```</p>
<ul>
<li><code>instructions</code> 옵션을 통해서 시스템 프롬프트를 추가할 수 있게 해준다.
<code>input</code>을 통해서 넣어줄 수 있지만 그건 토큰을 사용하기에 <code>instrctions</code>에 넣는걸 추천합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ AI Agent(3) API 호출 개선]]></title>
            <link>https://velog.io/@os_js/AI-Agent3-API-%ED%98%B8%EC%B6%9C-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@os_js/AI-Agent3-API-%ED%98%B8%EC%B6%9C-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Mon, 16 Feb 2026 08:05:59 GMT</pubDate>
            <description><![CDATA[<h1 id="비동기-호출">비동기 호출</h1>
<ul>
<li>이전 api 호출을 통한 AI응답을 받는 코드를 작성했었다.
그러나, 한 번의 응답을 받기 위해서는 짧지 않은 시간이 소요된다는걸 알게되었을 것이다.
하나의 질문에 대해서만 응답이 필요하다면 상관 없지만 여러 질문을 하거나 서비스로 제공하여 사용자가 많아진다면 질문의 수 만큼 대기시간이 늘어나게 된다.</li>
<li>이러한 문제를 개선하기위해 비동기로 호출할 수 있도록 코드를 재 작성하자.<pre><code>import asyncio
import os
from dotenv import load_dotenv
</code></pre></li>
</ul>
<p>from openai import AsyncOpenAI</p>
<p>load_dotenv()</p>
<p>openai_client = AsyncOpenAI(api_key=os.environ.get(&quot;OPEN_API_KEY&quot;))</p>
<p>async def call_async_openai(prompt:str, model:str=&quot;gpt-5-mini&quot;) -&gt; str :
    response = await openai_client.chat.completions.create(
        model=model,
        messages=[{&quot;role&quot;:&quot;user&quot;, &quot;content&quot;:prompt}]
    )</p>
<pre><code>return response.choices[0].message.content</code></pre><p>async def main():
    print(&quot;동시에 2번 API  호출하기&quot;)
    prompt = &quot;비동기 프로그래밍에 대해 두세 문장으로 설명해주세요.&quot;</p>
<pre><code># 비동기 함수 호출시 코루틴 객체 반환(실행은 안됨)
openai_task = call_async_openai(prompt)
openai_task2 = call_async_openai(prompt)

# 두 API 호출 병려렬로 실행하고 완료될 때까지 대기.    
openai_response, openai_response2 = await asyncio.gather(openai_task, openai_task2)
print(f&quot;OpenAI 응답 1 : {openai_response}&quot;)
print(f&quot;OpenAI 응답 2 : {openai_response2}&quot;)</code></pre><p>if <strong>name</strong> == &quot;<strong>main</strong>&quot; :
    asyncio.run(main()) # 비동기 메인 함수를 이벤트 루프에서 실행</p>
<pre><code>- 간단하게 asyncio를 통한 비동기 호출을 만들었다.
실행해보면 응답시간이 질문한 개수만큼 늘어나지 않는다는걸 알 수 있다.

# 오류 처리
- 당연히 API호출이 모두 성공할 수 없다. 그렇다면 실패했을 때의 로직도 필요하다.
- 모듈 설치
```pip install tenacity```
- 코드 작성 (위에서 작성했던 코드에 추가된 버전)</code></pre><p>import asyncio
import os, logging, random
from dotenv import load_dotenv
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from openai import AsyncOpenAI</p>
<h1 id="logging">logging</h1>
<p>logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(<strong>name</strong>)</p>
<p>load_dotenv()</p>
<p>openai_client = AsyncOpenAI(api_key=os.environ.get(&quot;OPEN_API_KEY&quot;))</p>
<p>@retry(
    stop=stop_after_attempt(3), # 최대 3번시도
    wait=wait_exponential(multiplier=1, min=2, max=10), # 지수 백오프 : 2초, 4초, 8초....
    retry=retry_if_exception_type(), # 모든 예외에 대해 재시도
    before_sleep=lambda retry_state : logger.warning(
        f&quot;API 호출 실패: {retry_state.outcome.exception()}, {retry_state.attempt_number} 번쨰 시도중...&quot;
    )</p>
<p>)
async def call_async_openai(prompt:str, model:str=&quot;gpt-5-mini&quot;) -&gt; str :</p>
<pre><code>logger.info(f&quot;OpenAI API 호출 시작 : {model}&quot;)

await simulate_random_failure()

response = await openai_client.chat.completions.create(
    model=model,
    messages=[{&quot;role&quot;:&quot;user&quot;, &quot;content&quot;:prompt}]
)

logger.info(f&quot;OpenAI API 호출  성공 &quot;)

return response.choices[0].message.content</code></pre><h1 id="인위적-실패-생성-함수">인위적 실패 생성 함수</h1>
<p>async def simulate_random_failure() :
    if random.random() &lt; 0.5 :
        logger.warning(&quot;인위적 API 호출 실패 생성&quot;)
        raise ConnectionError(&quot;인위적 연결 오류 발생&quot;)</p>
<pre><code>await asyncio.sleep(random.uniform(0.1, 0.5))</code></pre><p>async def main():
    try :
        print(&quot;동시에 2번 API  호출하기&quot;)
        prompt = &quot;비동기 프로그래밍에 대해 두세 문장으로 설명해주세요.&quot;</p>
<pre><code>    # 비동기 함수 호출시 코루틴 객체 반환(실행은 안됨)
    openai_task = call_async_openai(prompt)
    openai_task2 = call_async_openai(prompt)

    # 두 API 호출 병려렬로 실행하고 완료될 때까지 대기.    
    openai_response, openai_response2 = await asyncio.gather(openai_task, openai_task2)
    print(f&quot;OpenAI 응답 1 : {openai_response}&quot;)
    print(f&quot;OpenAI 응답 2 : {openai_response2}&quot;)
except Exception as e:
    logger.error(f&quot;처리되지 않은 오류 발생 {e} &quot;)</code></pre><p>if <strong>name</strong> == &quot;<strong>main</strong>&quot; :
    asyncio.run(main()) # 비동기 메인 함수를 이벤트 루프에서 실행</p>
<p>```</p>
<ul>
<li>실행시켜보면 실패 되었을때, 바로 종료되지 않고 retry 어노테이션에 설정된 값과 같이 재시도를 진행하는걸 확인할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI Agent (2) API]]></title>
            <link>https://velog.io/@os_js/AI-Agent-2-API</link>
            <guid>https://velog.io/@os_js/AI-Agent-2-API</guid>
            <pubDate>Fri, 13 Feb 2026 12:18:18 GMT</pubDate>
            <description><![CDATA[<h1 id="api">API</h1>
<h2 id="chat-completions-api">Chat Completions API</h2>
<ul>
<li><p>사용자 입력에 대한 직접적인 응답 생성</p>
</li>
<li><p>대화 문맥을 개발자가 직접 관리.(새로운 대화 시작시 이전 대화 내용을 개발자가 직접 제공해야함)</p>
</li>
<li><p>실제 사용 예시:</p>
<ul>
<li>고객 지원: 고객의 질문에 대해 실시간으로 응답하는 챗봇 시스템.</li>
<li>대화형 학습 시스템: 학생이 질문을 던지면, 모델이 적절한 답변을 제공.</li>
<li>AI 기반 비서: 사용자의 요청에 맞춰 정보를 제공하거나 작업을 처리하는 비서형 응답.</li>
</ul>
</li>
</ul>
<h3 id="사용">사용</h3>
<ul>
<li>python 모듈 설치<pre><code>pip install openai==1.70.0
pip install dotenv</code></pre></li>
<li>파일 작성<pre><code>import os
from dotenv import load_dotenv
from openai import OpenAI
</code></pre></li>
</ul>
<h1 id="env파일에서-환경변수-로드">.env파일에서 환경변수 로드</h1>
<p>load_dotenv()</p>
<p>api_key = os.environ.get(&#39;OPEN_API_KEY&#39;)
client = OpenAI(api_key=api_key)</p>
<p>def get_chat_completion(prompt, model=&quot;gpt-5-mini&quot;) :
    # OpenAI 챗 컴플리션 API를 사용하여 AI의 응답을 받는 함수</p>
<pre><code>response = client.chat.completions.create(
    model=model,
    messages=[
        {&quot;role&quot; : &quot;system&quot;, &quot;content&quot; : &quot;당신은 친절하고 도움이 되는 AI 비서 입니다.&quot;},
        {&quot;role&quot;:&quot;user&quot;, &quot;content&quot;:prompt}
    ]
)

return response.choices[0].message.content</code></pre><p>if <strong>name</strong> == &quot;<strong>main</strong>&quot; :
    user_prompt = input(&quot;AI에게 물어볼 질문을 입력하세요: &quot;)
    response = get_chat_completion(user_prompt)
    print(&quot;\nAI 응답 : &quot;)
    print(response)</p>
<pre><code>- 결과
![](https://velog.velcdn.com/images/os_js/post/276216ac-3726-48df-9578-98e8b75bbf1a/image.png)

## Responses API
- 복잡한 AI 어시스턴트 구축을 지원.
- 대화 스레드를 통해 자동으로 문맥을 관리함.
- 웹 검색, 파일검색, 컴퓨터 사용등 다양한 내장 도구 지원.

- 실제 사용 예시:
    - 자율적인 AI 에이전트 개발
    - 멀티모달 입력이 필요한 애플리케이션
    - 대화 상태 관리가 필요한 복잡한 대화형 서비스
    - 웹 검색, 파일 분석 등 외부 데이터 활용이 필요한 시스템

### 사용</code></pre><p>import os
from dotenv import load_dotenv
from openai import OpenAI</p>
<h1 id="env파일에서-환경변수-로드-1">.env파일에서 환경변수 로드</h1>
<p>load_dotenv()</p>
<p>api_key = os.environ.get(&#39;OPEN_API_KEY&#39;)
client = OpenAI(api_key=api_key)</p>
<p>def get_reponses(prompt, model=&quot;gpt-5-mini&quot;) :
    # OpenAI responses API를 사용하여 AI의 응답을 받는 함수</p>
<pre><code>response = client.responses.create(
    model=model,
    tools=[{&quot;type&quot; : &quot;web_search_preview&quot;}], # 웹 검색 도구 활성화
    input=prompt
)

return response.output_text</code></pre><p>if <strong>name</strong> == &quot;<strong>main</strong>&quot; :
    prompt = &#39;<a href="https://platform.openai.com/docs/api-reference/responses/create">https://platform.openai.com/docs/api-reference/responses/create</a> 를 읽어서 리스폰스에 대해 요약 정리해주세요&#39;
    response = get_reponses(prompt)
    print(&quot;\nAI 응답 : &quot;)
    print(response) </p>
<pre><code>
 - 결과
 ![](https://velog.velcdn.com/images/os_js/post/50b7084a-fff3-4f5d-9aa7-4fdf01f4f3f6/image.png)

## Stream
- 이전 예제는 AI가 응답한 내용을 한 번에 보여주는 방식이었으나, 스트리밍을 통해서 결과값을 바로바로 보여주는 방식도 존재.
- 모듈 설치
``` pip install rich```
- 코드 작성</code></pre><p>import os
from dotenv import load_dotenv
from openai import OpenAI
import rich</p>
<h1 id="env파일에서-환경변수-로드-2">.env파일에서 환경변수 로드</h1>
<p>load_dotenv()</p>
<p>api_key = os.environ.get(&#39;OPEN_API_KEY&#39;)
client = OpenAI(api_key=api_key)</p>
<p>default_model=&quot;gpt-5-mini&quot;
def stream_chat_completion(prompt, model=default_model) :
    # OpenAI 챗 컴플리션 API를 사용하여 AI의 응답을 받는 함수</p>
<pre><code>response = client.chat.completions.create(
    model=model,
    messages=[
        {&quot;role&quot; : &quot;system&quot;, &quot;content&quot; : &quot;당신은 친절하고 도움이 되는 AI 비서 입니다.&quot;},
        {&quot;role&quot;:&quot;user&quot;, &quot;content&quot;:prompt}            
    ],
    stream=True
)

for chunk in response :
    content = chunk.choices[0].delta.content
    if content is not None:
        print(content, end=&quot;&quot;)    </code></pre><p>def stream_response(prompt, model=default_model) :
    with client.responses.stream(model=model, input=prompt) as stream :
        for event in stream:
            if &quot;output_text&quot; in event.type :
                rich.print(event)</p>
<pre><code>rich.print(stream.get_final_response())</code></pre><p>if <strong>name</strong> == &quot;<strong>main</strong>&quot; :
    stream_chat_completion(&quot;스트리밍이 무엇인가요?&quot;)
    stream_response(&quot;저녁 메뉴 추천 10글자로 제한&quot;)
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[mqtt HA 설정(kubernetes)]]></title>
            <link>https://velog.io/@os_js/mqtt-HA-%EC%84%A4%EC%A0%95kubernetes</link>
            <guid>https://velog.io/@os_js/mqtt-HA-%EC%84%A4%EC%A0%95kubernetes</guid>
            <pubDate>Fri, 13 Feb 2026 12:11:43 GMT</pubDate>
            <description><![CDATA[<ol>
<li>mqtt setup.yml<pre><code># 1. Longhorn StorageClass 정의 (레플리카 3개 설정)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: emqx-longhorn-sc
provisioner: driver.longhorn.io
allowVolumeExpansion: true
parameters:
numberOfReplicas: &quot;3&quot;      # 레플리카 수를 3으로 명시 (노드가 3개이므로 최적)
staleReplicaTimeout: &quot;30&quot;  # 비정상 레플리카 제거 대기 시간(분)
fromBackup: &quot;&quot;</code></pre></li>
</ol>
<hr>
<p>apiVersion: v1
kind: ConfigMap
metadata:
  name: tcp-services
  namespace: ingress-nginx
data:
  &quot;1883&quot;: &quot;default/emqx-service:1883&quot;</p>
<hr>
<h1 id="2-rbac-emqx-클러스터-검색용-권한">2. RBAC (EMQX 클러스터 검색용 권한)</h1>
<p>apiVersion: v1
kind: ServiceAccount
metadata:
  name: emqx-sa</p>
<hr>
<p>apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: emqx-role
rules:</p>
<ul>
<li>apiGroups: [&quot;&quot;]
resources: [&quot;endpoints&quot;]
verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]</li>
</ul>
<hr>
<p>apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: emqx-rb
subjects:</p>
<ul>
<li>kind: ServiceAccount
name: emqx-sa
roleRef:
kind: Role
name: emqx-role
apiGroup: rbac.authorization.k8s.io</li>
</ul>
<hr>
<h1 id="3-service-discovery-및-통신용">3. Service (Discovery 및 통신용)</h1>
<p>apiVersion: v1
kind: Service
metadata:
  name: emqx-headless
spec:
  clusterIP: None
  ports:
    - name: cluster-rpc
      port: 5369
  selector:
    app: emqx</p>
<hr>
<p>apiVersion: v1
kind: Service
metadata:
  name: emqx-service
spec:
  ports:
    - name: mqtt
      port: 1883
    - name: dashboard
      port: 18083
  selector:
    app: emqx</p>
<hr>
<h1 id="4-statefulset-emqx--정의한-storageclass-사용">4. StatefulSet (EMQX + 정의한 StorageClass 사용)</h1>
<p>apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: emqx
spec:
  serviceName: &quot;emqx-headless&quot;
  replicas: 3
  selector:
    matchLabels:
      app: emqx
  template:
    metadata:
      labels:
        app: emqx
    spec:
      # --- 이 부분을 반드시 추가하세요 ---
      securityContext:
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000   # Longhorn 볼륨의 소유권을 emqx 유저 권한으로 강제 조정
      # -----------------------------
      serviceAccountName: emqx-sa
      containers:
      - name: emqx
        image: emqx/emqx:5.3.0
        env:
          - name: EMQX_CLUSTER__DISCOVERY_STRATEGY
            value: &quot;k8s&quot;
          - name: EMQX_CLUSTER__K8S__ADDRESS_TYPE
            value: &quot;hostname&quot;
          - name: EMQX_CLUSTER__K8S__NAMESPACE
            value: &quot;default&quot;
          - name: EMQX_CLUSTER__K8S__SERVICE_NAME
            value: &quot;emqx-headless&quot;
          - name: EMQX_CLUSTER__K8S__SUFFIX
            value: &quot;svc.cluster.local&quot;
        volumeMounts:
          - name: emqx-data
            mountPath: /opt/emqx/data
  volumeClaimTemplates:</p>
<ul>
<li>metadata:
  name: emqx-data
spec:
  accessModes: [ &quot;ReadWriteOnce&quot; ]
  storageClassName: &quot;emqx-longhorn-sc&quot; # 위에서 정의한 SC 사용
  resources:<pre><code>requests:
  storage: 1Gi</code></pre></li>
</ul>
<pre><code>
2. 기존 ingress  파일 변경</code></pre><p>apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: longhorn-ingress
  namespace: longhorn-system
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  ingressClassName: nginx
  rules:</p>
<ul>
<li>host: longhorn.local
http:
  paths:<ul>
<li>path: /
pathType: Prefix
backend:
  service:<pre><code>name: longhorn-frontend
port:
  number: 80</code></pre></li>
</ul>
</li>
</ul>
<hr>
<p>apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: default-ingress
  namespace: default
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/affinity: &quot;cookie&quot;
    nginx.ingress.kubernetes.io/session-cookie-name: &quot;route&quot;
    nginx.ingress.kubernetes.io/session-cookie-hash: &quot;sha1&quot;
spec:
  ingressClassName: nginx
  rules:</p>
<ul>
<li>host: influxdb.local
http:
  paths:<ul>
<li>path: /
pathType: Prefix
backend:
  service:<pre><code>name: influxdb-service # ▒▒▒▒ ▒▒▒▒ ▒▒▒▒ ▒̸▒
port:
  number: 8086</code></pre></li>
</ul>
</li>
<li>host: mqtt.local
http:
  paths:<ul>
<li>path: /
pathType: Prefix
backend:
  service:<pre><code>name: emqx-service # ▒▒▒▒ ▒▒▒▒ ▒▒▒▒ ▒̸▒
port:
  number: 18083</code></pre></li>
</ul>
</li>
</ul>
<pre><code>```kubectl apply -f 파일.yml``` 하면 적용됨.
3. ingress-nginx-controller 서비스 파일 백업 및 수정
```kubectl get svc ingress-nginx-controller -n ingress-nginx -o yaml &gt; ingress-svc-update.yaml```
</code></pre><p>apiVersion: v1
kind: Service
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {&quot;apiVersion&quot;:&quot;v1&quot;,&quot;kind&quot;:&quot;Service&quot;,&quot;metadata&quot;:{&quot;annotations&quot;:{},&quot;labels&quot;:{&quot;app.kubernetes.io/component&quot;:&quot;controller&quot;,&quot;app.kubernetes.io/instance&quot;:&quot;ingress-nginx&quot;,&quot;app.kubernetes.io/name&quot;:&quot;ingress-nginx&quot;,&quot;app.kubernetes.io/part-of&quot;:&quot;ingress-nginx&quot;,&quot;app.kubernetes.io/version&quot;:&quot;1.8.1&quot;},&quot;name&quot;:&quot;ingress-nginx-controller&quot;,&quot;namespace&quot;:&quot;ingress-nginx&quot;},&quot;spec&quot;:{&quot;ipFamilies&quot;:[&quot;IPv4&quot;],&quot;ipFamilyPolicy&quot;:&quot;SingleStack&quot;,&quot;ports&quot;:[{&quot;appProtocol&quot;:&quot;http&quot;,&quot;name&quot;:&quot;http&quot;,&quot;port&quot;:80,&quot;protocol&quot;:&quot;TCP&quot;,&quot;targetPort&quot;:&quot;http&quot;},{&quot;appProtocol&quot;:&quot;https&quot;,&quot;name&quot;:&quot;https&quot;,&quot;port&quot;:443,&quot;protocol&quot;:&quot;TCP&quot;,&quot;targetPort&quot;:&quot;https&quot;}],&quot;selector&quot;:{&quot;app.kubernetes.io/component&quot;:&quot;controller&quot;,&quot;app.kubernetes.io/instance&quot;:&quot;ingress-nginx&quot;,&quot;app.kubernetes.io/name&quot;:&quot;ingress-nginx&quot;},&quot;type&quot;:&quot;NodePort&quot;}}
  creationTimestamp: &quot;2026-01-24T11:27:50Z&quot;
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
    app.kubernetes.io/version: 1.8.1
  name: ingress-nginx-controller
  namespace: ingress-nginx
  resourceVersion: &quot;55756&quot;
  uid: cacbc931-e466-4a19-a1cf-d6a59e5f3053
spec:
  clusterIP: 10.100.46.190
  clusterIPs:</p>
<ul>
<li>10.100.46.190
externalTrafficPolicy: Cluster
internalTrafficPolicy: Cluster
ipFamilies:</li>
<li>IPv4
ipFamilyPolicy: SingleStack
ports:</li>
<li>appProtocol: http
name: http
nodePort: 32142
port: 80
protocol: TCP
targetPort: http</li>
<li>appProtocol: https
name: https
nodePort: 31171
port: 443
protocol: TCP
targetPort: https<h1 id="추가">추가</h1>
</li>
<li>name: mqtt 
port: 1883
targetPort: 1883
protocol: TCP
nodePort: 32257 #외부 노출 포트<h3 id="추가-끝">추가 끝</h3>
selector:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
sessionAffinity: None
type: NodePort
status:
loadBalancer: {}<pre><code></code></pre></li>
</ul>
<pre><code># 3. 수정된 설정 반영
kubectl apply -f 파일이름.yaml

# 4. 포트가 정상적으로 열렸는지 확인
kubectl get svc ingress-nginx-controller -n ingress-nginx</code></pre><ol start="4">
<li><p>haproxy 접속 설정.</p>
<pre><code>frontend mqtt_dash
 bind *:18083
 mode http
 http-request set-header Host mqtt.local

 default_backend ingress_nginx
</code></pre></li>
</ol>
<p>frontend mqtt_front
    bind *:1883
    mode tcp</p>
<pre><code>default_backend mqtt_backend</code></pre><h1 id="2-mqtt-전용-백엔드-포트-번호-32257">2. MQTT 전용 백엔드 (포트 번호: 32257)</h1>
<p>backend mqtt_backend
    mode tcp
    balance roundrobin
    # kubectl에서 확인한 NodePort 32257을 사용합니다.
    server node3 10.0.2.4:32257 check #위에서 설정한 node port
    server node2 10.0.2.6:32257 check
    server node1 10.0.2.5:32257 check</p>
<p>frontend influx_front
    bind *:8086
    mode http</p>
<pre><code># [핵심] 외부에서 IP로 들어와도 Ingress가 알아먹을 수 있게 이름표를 갈아끼웁니다.
http-request set-header Host influxdb.local

default_backend ingress_nginx</code></pre><p>frontend longhorn_front
    bind *:80
    mode http</p>
<pre><code># [핵심] 80포트로 들어오면 롱혼 이름표를 붙입니다.
http-request set-header Host longhorn.local

default_backend ingress_nginx</code></pre><p>backend ingress_nginx
    mode http
    balance roundrobin
    # 각 노드(VM)의 실제 IP와 확인한 NodePort를 적습니다.
    server node3 10.0.2.4:32142 check
    server node2 10.0.2.6:32142 check
    server node1 10.0.2.5:32142 check</p>
<p>frontend k8s-api
    bind 10.0.2.10:6444
    default_backend k8s-masters</p>
<p>backend k8s-masters
    balance roundrobin
    server node3 10.0.2.4:6443 check
    server node1 10.0.2.5:6443 check
    server node2 10.0.2.6:6443 check</p>
<pre><code>

6. TCP 설정 옵션 주입
- 명령을 통한 주입
```kubectl patch deployment ingress-nginx-controller -n ingress-nginx --type=&#39;json&#39; -p=&#39;[{&quot;op&quot;: &quot;add&quot;, &quot;path&quot;: &quot;/spec/template/spec/containers/0/args/-&quot;, &quot;value&quot;: &quot;--tcp-services-configmap=ingress-nginx/tcp-services&quot;}]&#39;```
- 파일을 통한 주입
```kubectl get deployment -n ingress-nginx ingress-nginx-controller -o yaml &gt; ingress-controller.yaml```
 여기에서 뽑아낸 ingress-controller.yml 파일을 수정후 apply를 통해 시작</code></pre><p>apiVersion: apps/v1
kind: Deployment
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:</p>
<h1 id="-생략-">... (생략) ...</h1>
<p>  template:
    spec:
      containers:
      - name: controller
        image: ...
        args: # &lt;--- 바로 여기입니다!
        - /nginx-ingress-controller
        - --election-id=ingress-nginx-leader
        - --controller-class=k8s.io/ingress-nginx
        - --ingress-class=nginx
        - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
        # 아래 줄을 리스트의 맨 마지막이나 적당한 곳에 추가하세요.
        - --tcp-services-configmap=ingress-nginx/tcp-services</p>
<pre><code>
- 적용 확인
```kubectl describe deploy -n ingress-nginx ingress-nginx-controller | grep -A 7 &quot;Args&quot;```
- 결과확인</code></pre><h1 id="인그레스-컨트롤러-포드-이름-다시-가져오기">인그레스 컨트롤러 포드 이름 다시 가져오기</h1>
<p>INGRESS_POD=$(kubectl get pods -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx -o jsonpath=&#39;{.items[0].metadata.name}&#39;)</p>
<h1 id="포드-내부에서-1883-리스닝-확인">포드 내부에서 1883 리스닝 확인</h1>
<p>kubectl exec -n ingress-nginx $INGRESS_POD -- netstat -tuln | grep 1883</p>
<p>``` </p>
<ul>
<li>1883에 대한 정보가 떠야한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI Agent (1) GhatGPT API Key 발급]]></title>
            <link>https://velog.io/@os_js/AI-Agent-1-GhatGPT-API-Key-%EB%B0%9C%EA%B8%89</link>
            <guid>https://velog.io/@os_js/AI-Agent-1-GhatGPT-API-Key-%EB%B0%9C%EA%B8%89</guid>
            <pubDate>Fri, 13 Feb 2026 11:06:36 GMT</pubDate>
            <description><![CDATA[<h2 id="ai-agent-개발">AI Agent 개발</h2>
<ul>
<li>AI Agent 개발 시작.</li>
</ul>
<h1 id="chatgpt-api-key-발급">ChatGPT API Key 발급</h1>
<ul>
<li><a href="https://openai.com/ko-KR/">https://openai.com/ko-KR/</a> 접속
<img src="https://velog.velcdn.com/images/os_js/post/fabdc365-0b4d-4075-8933-674a6888b309/image.png" alt=""></li>
<li>api key 생성
<img src="https://velog.velcdn.com/images/os_js/post/6ef2e684-7bc2-4c2a-ba76-8a782ab5af3d/image.png" alt="">
<img src="https://velog.velcdn.com/images/os_js/post/cb274433-105b-4eac-85a8-5adc9fed77e2/image.png" alt=""></li>
<li>API key를 발급받은 후 저장해 놓으시길 바랍니다.
만약, 저장을 못하셨다면 재발급 받으세요.
<img src="https://velog.velcdn.com/images/os_js/post/686d584f-3519-4877-8db1-dfeffc38792b/image.png" alt=""></li>
<li>GPT Pro 또는 Plus 구독을 했더라도 API 키 사용을 위해서는 반드시 별도의 크레딧 충전이 필요합니다.
<a href="https://platform.openai.com/settings/organization/billing/overview">https://platform.openai.com/settings/organization/billing/overview</a></li>
<li>위 화면에서 0.0달러로 되어있다면 최소 5달러를 충전해야 API 사용 가능.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[kubernetes(influxdb HA)]]></title>
            <link>https://velog.io/@os_js/kubernetesinfluxdb-HA</link>
            <guid>https://velog.io/@os_js/kubernetesinfluxdb-HA</guid>
            <pubDate>Sun, 25 Jan 2026 12:16:38 GMT</pubDate>
            <description><![CDATA[<ol>
<li><p>haproxy 설정 파일 변경(각각의 서비스 구분을 위한 header 변경)</p>
<pre><code>frontend influx_front
 bind *:8086
 mode http

 # [핵심] 외부에서 IP로 들어와도 Ingress가 알아먹을 수 있게 이름표를 갈아끼웁니다.
 http-request set-header Host influxdb.local

 default_backend ingress_nginx
</code></pre></li>
</ol>
<p>frontend longhorn_front
    bind *:80
    mode http</p>
<pre><code># [핵심] 80포트로 들어오면 롱혼 이름표를 붙입니다.
http-request set-header Host longhorn.local

default_backend ingress_nginx</code></pre><p>backend ingress_nginx
    mode http
    balance roundrobin
    # SERVERID라는 쿠키를 삽입하여 한 번 접속한 노드로 계속 고정합니다.
    cookie SERVERID insert indirect nocache
    # 각 노드(VM)의 실제 IP와 확인한 NodePort를 적습니다.
    server node3 10.0.2.4:32142 check
    server node2 10.0.2.6:32142 check
    server node1 10.0.2.5:32142 check</p>
<p>frontend k8s-api
    bind 10.0.2.10:6444
    default_backend k8s-masters</p>
<p>backend k8s-masters
    balance roundrobin
    server node3 10.0.2.4:6443 check
    server node1 10.0.2.5:6443 check
    server node2 10.0.2.6:6443 check</p>
<pre><code>
2. influxdb(telegraf)</code></pre><p>apiVersion: v1
kind: ConfigMap
metadata:
  name: telegraf-config
  labels:
    app: influxdb-ha
data:
  telegraf.conf: |
    [agent]
      interval = &quot;10s&quot;
      round_interval = true</p>
<pre><code>[[inputs.influxdb_v2_listener]]
  ## 외부에서 데이터를 받을 포트 (Relay 역할)
  service_address = &quot;:8080&quot;

[[outputs.influxdb_v2]]
  ## 첫 번째 InfluxDB 노드 (StatefulSet 0번)
  urls = [&quot;http://influxdb-0.influxdb-service:8086&quot;]
  token = &quot;${INFLUX_TOKEN}&quot;
  organization = &quot;my-org&quot;
  bucket = &quot;my-bucket&quot;

[[outputs.influxdb_v2]]
  ## 두 번째 InfluxDB 노드 (StatefulSet 1번)
  urls = [&quot;http://influxdb-1.influxdb-service:8086&quot;]
  token = &quot;${INFLUX_TOKEN}&quot;
  organization = &quot;my-org&quot;
  bucket = &quot;my-bucket&quot;

[[outputs.influxdb_v2]]
  ## 두 번째 InfluxDB 노드 (StatefulSet 1번)
  urls = [&quot;http://influxdb-2.influxdb-service:8086&quot;]
  token = &quot;${INFLUX_TOKEN}&quot;
  organization = &quot;my-org&quot;
  bucket = &quot;my-bucket&quot;</code></pre><hr>
<p>apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: longhorn-influxdb
provisioner: driver.longhorn.io
allowVolumeExpansion: true
parameters:</p>
<h1 id="longhorn에서-데이터를-몇-군데-복제할지-결정-보통-3-권장">Longhorn에서 데이터를 몇 군데 복제할지 결정 (보통 3 권장)</h1>
<p>  numberOfReplicas: &quot;3&quot;</p>
<h1 id="데이터-기록이-완료되었다고-판단하는-기준">데이터 기록이 완료되었다고 판단하는 기준</h1>
<p>  staleReplicaTimeout: &quot;30&quot;</p>
<h1 id="노드-장애-시-데이터-자동-재균형">노드 장애 시 데이터 자동 재균형</h1>
<h2 id="frombackup">  fromBackup: &quot;&quot;</h2>
<p>apiVersion: v1
kind: Service
metadata:
  name: influxdb-service
spec:</p>
<h1 id="statefulset의-개별-pod에-접근하기-위해-headless-service-권장">StatefulSet의 개별 Pod에 접근하기 위해 Headless Service 권장</h1>
<h1 id="clusterip-none-내부에서만-ㅅ쓸때-설정">clusterIP: None #내부에서만 ㅅ쓸때 설정</h1>
<p>  type: NodePort #외부 포트를 열떄 필요.
  selector:
    app: influxdb
  ports:
    - name: influxdb
      port: 8086
      targetPort: 8086
      nodePort: 30086</p>
<hr>
<p>apiVersion: v1
kind: Service
metadata:
  name: influxdb-write-proxy
spec:</p>
<h1 id="쓰기-전용-엔드포인트-telegraf-relay로-연결">쓰기 전용 엔드포인트 (Telegraf Relay로 연결)</h1>
<p>  selector:
    app: telegraf
  ports:
    - name: write
      port: 8080
      targetPort: 8080</p>
<hr>
<p>apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: influxdb
spec:
  serviceName: &quot;influxdb-service&quot;
  replicas: 1 #두개이상시 충돌
  selector:
    matchLabels:
      app: influxdb
  template:
    metadata:
      labels:
        app: influxdb
    spec:<br>      affinity:
        podAntiAffinity:
          # required... 는 &quot;무조건 지켜라&quot;라는 뜻입니다. (Hard anti-affinity)
          # 만약 노드가 2개뿐인데 replicas가 3이면, 1개 Pod는 배포되지 않고 Pending 상태가 됩니다.
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - influxdb
              topologyKey: &quot;kubernetes.io/hostname&quot;<br>      containers:
        - name: influxdb
          image: influxdb:2.7 # 2.x 최신 안정 버전
          ports:
            - containerPort: 8086
          env:
            # 초기 설정 자동화 (최초 실행 시에만 적용)
            - name: DOCKER_INFLUXDB_INIT_MODE
              value: setup
            - name: DOCKER_INFLUXDB_INIT_USERNAME
              value: <ID>
            - name: DOCKER_INFLUXDB_INIT_PASSWORD
              value: <password>
            - name: DOCKER_INFLUXDB_INIT_ORG
              value: my-org
            - name: DOCKER_INFLUXDB_INIT_BUCKET
              value: my-bucket
            - name: DOCKER_INFLUXDB_INIT_ADMIN_TOKEN
              value: my-super-secret-token
          volumeMounts:
            - name: influxdb-data
              mountPath: /var/lib/influxdb2
  volumeClaimTemplates:
    - metadata:
        name: influxdb-data
      spec:
        accessModes: [ &quot;ReadWriteOnce&quot; ]
        storageClassName: &quot;longhorn-influxdb&quot;
        resources:
          requests:
            storage: 2Gi # 스토리지 용량 설정</p>
<hr>
<p>apiVersion: apps/v1
kind: Deployment
metadata:
  name: telegraf-relay
spec:
  replicas: 3
  selector:
    matchLabels:
      app: telegraf
  template:
    metadata:
      labels:
        app: telegraf
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - telegraf
              topologyKey: &quot;kubernetes.io/hostname&quot;
      containers:
        - name: telegraf
          image: telegraf:latest
          ports:
            - containerPort: 8080
          env:
            # ConfigMap에서 사용할 토큰 (InfluxDB와 동일하게 설정)
            - name: INFLUX_TOKEN
              value: my-super-secret-token
          volumeMounts:
            - name: config
              mountPath: /etc/telegraf/telegraf.conf
              subPath: telegraf.conf
      volumes:
        - name: config
          configMap:
            name: telegraf-config</p>
<pre><code>
3. ingress 설정 파일</code></pre><p>apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: longhorn-ingress
  namespace: longhorn-system
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  ingressClassName: nginx
  rules:</p>
<ul>
<li>host: longhorn.local
http:
  paths:<ul>
<li>path: /
pathType: Prefix
backend:
  service:<pre><code>name: longhorn-frontend
port:
  number: 80</code></pre></li>
</ul>
</li>
</ul>
<hr>
<p>apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: default-ingress
  namespace: default
  annotations:
    kubernetes.io/ingress.class: nginx
    #influxdb 대시보드 접속시 세션의 유실 문제를 해결하기 위해 아래 3줄 추가
    nginx.ingress.kubernetes.io/affinity: &quot;cookie&quot;
    nginx.ingress.kubernetes.io/session-cookie-name: &quot;route&quot;
    nginx.ingress.kubernetes.io/session-cookie-hash: &quot;sha1&quot;
spec:
  ingressClassName: nginx
  rules:</p>
<ul>
<li>host: influxdb.local
http:
  paths:<ul>
<li>path: /
pathType: Prefix
backend:
  service:<pre><code>name: influxdb-service # 이전에 만든 서비스 이름
port:
  number: 8086</code></pre></li>
</ul>
</li>
</ul>
<pre><code>
### 문제발생
- influxdb가 떠있는 노드를 강제로 종료시켰을때, Pod가 끝나지 않고 계속 terminating 상태이다.
</code></pre><h1 id="influxdb-statefulset-spec-내부에-추가">influxdb statefulset spec 내부에 추가</h1>
<p>tolerations:</p>
<ul>
<li>key: &quot;node.kubernetes.io/unreachable&quot;
operator: &quot;Exists&quot;
effect: &quot;NoExecute&quot;
tolerationSeconds: 30  # 30초만 응답 없으면 바로 다른 노드로 이사 준비</li>
<li>key: &quot;node.kubernetes.io/not-ready&quot;
operator: &quot;Exists&quot;
effect: &quot;NoExecute&quot;
tolerationSeconds: 30 <pre><code>
</code></pre></li>
</ul>
<ul>
<li><p>정상 작동 setup yml 파일</p>
<pre><code>apiVersion: v1
kind: ConfigMap
metadata:
name: telegraf-config
labels:
 app: influxdb-ha
data:
telegraf.conf: |
 [agent]
   interval = &quot;10s&quot;
   round_interval = true

 [[inputs.influxdb_v2_listener]]
   ## 외부에서 데이터를 받을 포트 (Relay 역할)
   service_address = &quot;:8080&quot;

 [[outputs.influxdb_v2]]
   ## 첫 번째 InfluxDB 노드 (StatefulSet 0번)
   urls = [&quot;http://influxdb-0.influxdb-service:8086&quot;]
   token = &quot;${INFLUX_TOKEN}&quot;
   organization = &quot;my-org&quot;
   bucket = &quot;my-bucket&quot;

 [[outputs.influxdb_v2]]
   ## 두 번째 InfluxDB 노드 (StatefulSet 1번)
   urls = [&quot;http://influxdb-1.influxdb-service:8086&quot;]
   token = &quot;${INFLUX_TOKEN}&quot;
   organization = &quot;my-org&quot;
   bucket = &quot;my-bucket&quot;

 [[outputs.influxdb_v2]]
   ## 두 번째 InfluxDB 노드 (StatefulSet 1번)
   urls = [&quot;http://influxdb-2.influxdb-service:8086&quot;]
   token = &quot;${INFLUX_TOKEN}&quot;
   organization = &quot;my-org&quot;
   bucket = &quot;my-bucket&quot;

</code></pre></li>
</ul>
<hr>
<p>apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: longhorn-influxdb
provisioner: driver.longhorn.io
allowVolumeExpansion: true
parameters:</p>
<h1 id="longhorn에서-데이터를-몇-군데-복제할지-결정-보통-3-권장-1">Longhorn에서 데이터를 몇 군데 복제할지 결정 (보통 3 권장)</h1>
<p>  numberOfReplicas: &quot;3&quot;</p>
<h1 id="데이터-기록이-완료되었다고-판단하는-기준-1">데이터 기록이 완료되었다고 판단하는 기준</h1>
<p>  staleReplicaTimeout: &quot;30&quot;</p>
<h1 id="노드-장애-시-데이터-자동-재균형-1">노드 장애 시 데이터 자동 재균형</h1>
<h2 id="frombackup-1">  fromBackup: &quot;&quot;</h2>
<p>apiVersion: v1
kind: Service
metadata:
  name: influxdb-service
spec:</p>
<h1 id="statefulset의-개별-pod에-접근하기-위해-headless-service-권장-1">StatefulSet의 개별 Pod에 접근하기 위해 Headless Service 권장</h1>
<h1 id="clusterip-none-내부에서만-ㅅ쓸때-설정-1">clusterIP: None #내부에서만 ㅅ쓸때 설정</h1>
<p>  type: NodePort #외부 포트를 열떄 필요.
  selector:
    app: influxdb
  ports:
    - name: influxdb
      port: 8086
      targetPort: 8086
      nodePort: 30086</p>
<hr>
<p>apiVersion: v1
kind: Service
metadata:
  name: influxdb-write-proxy
spec:</p>
<h1 id="쓰기-전용-엔드포인트-telegraf-relay로-연결-1">쓰기 전용 엔드포인트 (Telegraf Relay로 연결)</h1>
<p>  selector:
    app: telegraf
  ports:
    - name: write
      port: 8080
      targetPort: 8080</p>
<hr>
<p>apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: influxdb
spec:
  serviceName: &quot;influxdb-service&quot;
  replicas: 1 # 2개의 노드를 유지 (이중화)
  selector:
    matchLabels:
      app: influxdb
  template:
    metadata:
      labels:
        app: influxdb
    spec:<br>      tolerations:
      - key: &quot;node.kubernetes.io/unreachable&quot;
        operator: &quot;Exists&quot;
        effect: &quot;NoExecute&quot;
        tolerationSeconds: 30  # 10초만 응답 없으면 바로 다른 노드로 이사 준비
      - key: &quot;node.kubernetes.io/not-ready&quot;
        operator: &quot;Exists&quot;
        effect: &quot;NoExecute&quot;
        tolerationSeconds: 30
      affinity:
        podAntiAffinity:
          # required... 는 &quot;무조건 지켜라&quot;라는 뜻입니다. (Hard anti-affinity)
          # 만약 노드가 2개뿐인데 replicas가 3이면, 1개 Pod는 배포되지 않고 Pending 상태가 됩니다.
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - influxdb
              topologyKey: &quot;kubernetes.io/hostname&quot;<br>      containers:
        - name: influxdb
          image: influxdb:2.7 # 2.x 최신 안정 버전
          ports:
            - containerPort: 8086
          env:
            # 초기 설정 자동화 (최초 실행 시에만 적용)
            - name: DOCKER_INFLUXDB_INIT_MODE
              value: setup
            - name: DOCKER_INFLUXDB_INIT_USERNAME
              value: <ID> # 아무거나
            - name: DOCKER_INFLUXDB_INIT_PASSWORD
              value: <password> # 아무거나
            - name: DOCKER_INFLUXDB_INIT_ORG
              value: my-org
            - name: DOCKER_INFLUXDB_INIT_BUCKET
              value: my-bucket
            - name: DOCKER_INFLUXDB_INIT_ADMIN_TOKEN
              value: my-super-secret-token
          volumeMounts:
            - name: influxdb-data
              mountPath: /var/lib/influxdb2
  volumeClaimTemplates:
    - metadata:
        name: influxdb-data
      spec:
        accessModes: [ &quot;ReadWriteOnce&quot; ]
        storageClassName: &quot;longhorn-influxdb&quot;
        resources:
          requests:
            storage: 2Gi # 스토리지 용량 설정</p>
<hr>
<p>apiVersion: apps/v1
kind: Deployment
metadata:
  name: telegraf-relay
spec:
  replicas: 3
  selector:
    matchLabels:
      app: telegraf
  template:
    metadata:
      labels:
        app: telegraf
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - telegraf
              topologyKey: &quot;kubernetes.io/hostname&quot;
      containers:
        - name: telegraf
          image: telegraf:latest
          ports:
            - containerPort: 8080
          env:
            # ConfigMap에서 사용할 토큰 (InfluxDB와 동일하게 설정)
            - name: INFLUX_TOKEN
              value: my-super-secret-token
          volumeMounts:
            - name: config
              mountPath: /etc/telegraf/telegraf.conf
              subPath: telegraf.conf
      volumes:
        - name: config
          configMap:
            name: telegraf-config</p>
<p>```</p>
]]></description>
        </item>
    </channel>
</rss>