<?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>Thu, 19 Mar 2026 05:47:07 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[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>
        <item>
            <title><![CDATA[redis/sentinel/longhorn]]></title>
            <link>https://velog.io/@os_js/redissentinellonghorn</link>
            <guid>https://velog.io/@os_js/redissentinellonghorn</guid>
            <pubDate>Sat, 24 Jan 2026 13:29:07 GMT</pubDate>
            <description><![CDATA[<h3 id="파일-설정">파일 설정</h3>
<ul>
<li>이전글에서 사용한다. redis -&gt; sentinel 적용시 사용했던 부분에서
하단의 volumeClaimTemplates 부분만이 추가되었다.</li>
</ul>
<pre><code>apiVersion: v1
kind: ConfigMap
metadata:
  name: redis-config
data:
  redis.conf: |
    port 6379
    bind 0.0.0.0
    dir /data
  sentinel.conf: |
    port 26379
    bind 0.0.0.0
    # 핵심: redis-0.redis 주소를 사용하며, 쿼럼(2)을 설정합니다.
    sentinel monitor mymaster redis-0.redis.default.svc.cluster.local 6379 2
    sentinel down-after-milliseconds mymaster 5000
    sentinel failover-timeout mymaster 60000
    sentinel parallel-syncs mymaster 1
    sentinel resolve-hostnames yes

---
apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  clusterIP: None # Headless Service: redis-0.redis 주소를 가능하게 함
  publishNotReadyAddresses: true
  selector:
    app: redis
  ports:
    - name: redis
      port: 6379
    - name: sentinel
      port: 26379

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  serviceName: redis
  replicas: 3
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      # Sentinel은 설정 파일에 쓰기 권한이 필요하므로 초기화 컨테이너로 복사합니다.
      initContainers:
      - name: config-init
        image: redis:6.2-alpine
        command:
        - sh
        - -c
        - |
          cp /readonly-conf/redis.conf /conf/redis.conf
          cp /readonly-conf/sentinel.conf /conf/sentinel.conf
        volumeMounts:
        - name: readonly-config
          mountPath: /readonly-conf
        - name: conf
          mountPath: /conf
      containers:
      - name: redis
        image: redis:6.2-alpine
        command:
        - sh
        - -c
        - |
          echo &quot;My host name ${HOSTNAME}&quot;
          # 2. 만약 내가 0번 포드가 아니라면, 0번을 마스터로 설정합니다.
          if [ &quot;${HOSTNAME}&quot; != &quot;redis-0&quot; ]; then
            echo &quot;I am a slave. Pointing to redis-0.redis...&quot;
            # --replicaof 옵션을 사용해 실행 시점에 관계를 맺습니다.
            exec redis-server /conf/redis.conf --replicaof redis-0.redis 6379
          else
            echo &quot;I am the master (redis-0).&quot;
            exec redis-server /conf/redis.conf
          fi

        ports:
        - containerPort: 6379
        volumeMounts:
        - name: conf
          mountPath: /conf
        - name: data
          mountPath: /data
      - name: sentinel
        image: redis:6.2-alpine
        command:
        - sh
        - -c  
        - | 
          # redis-0.redis가 IP를 반환할 때까지 무한 루프 (최대 30초 권장)
          echo &quot;Waiting for DNS...&quot;
          sleep 10
          nslookup redis-0.redis.default.svc.cluster.local
          echo &quot;DNS is ready! Starting Sentinel...&quot;
          exec redis-sentinel /conf/sentinel.conf
        ports:
        - containerPort: 26379
        volumeMounts:
        - name: conf
          mountPath: /conf
        - name: data
          mountPath: /data
      volumes:
      - name: readonly-config
        configMap:
          name: redis-config
      - name: conf
        emptyDir: {} # 여기에 설정파일을 복사해서 쓰기 권한을 확보함

  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ &quot;ReadWriteOnce&quot; ]
      storageClassName: longhorn # 설치한 Longhorn 스토리지 클래스 이름
      resources:
        requests:
          storage: 1Gi # 용량은 필요에 따라 조절하세요
</code></pre><h3 id="정상-작동-테스트">정상 작동 테스트</h3>
<ul>
<li>마찬가지로 이전글에서 longhorn 설정시 대시보드에 접속이 가능해졌다면 대시보드에서 간단하게 확인이 가능하다.</li>
<li>PVC 상태 확인: <code>kubectl get pvc</code>
내가 작성한 replica 수만큼 동작이 되고있는지 확인.
<code>STORAGECLASS</code>가 longhorn으로 표시되는지 확인</li>
<li>포드와 마운트 확인: <code>kubectl describe pod redis-0</code>
출력 내용 중 Volumes: 섹션 아래에 data가 pvc-xxxx (Longhorn 볼륨)와 연결되어 있는지 확인</li>
<li>데이터 쓰기: <code>kubectl exec -it redis-0 -- redis-cli set mykey &quot;Longhorn_is_working&quot;</code></li>
<li>포드 삭제 : <code>kubectl delete pod redis-0</code></li>
<li>포드가 살아난 후 데이터 확인 : <code>kubectl exec -it redis-0 -- redis-cli get mykey</code></li>
</ul>
<h3 id="데이터-저장소-구분">데이터 저장소 구분</h3>
<pre><code>apiVersion: v1
kind: ConfigMap
metadata:
  name: redis-config
data:
  redis.conf: |
    port 6379
    bind 0.0.0.0
    dir /data
  sentinel.conf: |
    port 26379
    bind 0.0.0.0
    # 핵심: redis-0.redis 주소를 사용하며, 쿼럼(2)을 설정합니다.
    sentinel monitor mymaster redis-0.redis.default.svc.cluster.local 6379 2
    sentinel down-after-milliseconds mymaster 5000
    sentinel failover-timeout mymaster 60000
    sentinel parallel-syncs mymaster 1
    sentinel resolve-hostnames yes

---
apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  clusterIP: None # Headless Service: redis-0.redis 주소를 가능하게 함
  publishNotReadyAddresses: true
  selector:
    app: redis
  ports:
    - name: redis
      port: 6379
    - name: sentinel
      port: 26379

---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: longhorn-redis-special
provisioner: driver.longhorn.io
allowVolumeExpansion: true
parameters:
  numberOfReplicas: &quot;3&quot;
  staleReplicaTimeout: &quot;2880&quot;
  # 핵심: 아까 노드에 설정한 태그를 여기서 지정합니다.
  nodeSelector: &quot;redis-data&quot; 
  # 필요하다면 데이터 디스크 태그도 추가 가능
  # diskSelector: &quot;ssd&quot;
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  serviceName: redis
  replicas: 3
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      # Sentinel은 설정 파일에 쓰기 권한이 필요하므로 초기화 컨테이너로 복사합니다.
      initContainers:
      - name: config-init
        image: redis:6.2-alpine
        command:
        - sh
        - -c
        - |
          cp /readonly-conf/redis.conf /conf/redis.conf
          cp /readonly-conf/sentinel.conf /conf/sentinel.conf
        volumeMounts:
        - name: readonly-config
          mountPath: /readonly-conf
        - name: conf
          mountPath: /conf
      containers:
      - name: redis
        image: redis:6.2-alpine
        command:
        - sh
        - -c
        - |
          echo &quot;My host name ${HOSTNAME}&quot;
          # 2. 만약 내가 0번 포드가 아니라면, 0번을 마스터로 설정합니다.
          if [ &quot;${HOSTNAME}&quot; != &quot;redis-0&quot; ]; then
            echo &quot;I am a slave. Pointing to redis-0.redis...&quot;
            # --replicaof 옵션을 사용해 실행 시점에 관계를 맺습니다.
            exec redis-server /conf/redis.conf --replicaof redis-0.redis 6379
          else
            echo &quot;I am the master (redis-0).&quot;
            exec redis-server /conf/redis.conf
          fi

        ports:
        - containerPort: 6379
        volumeMounts:
        - name: conf
          mountPath: /conf
        - name: data
          mountPath: /data
      - name: sentinel
        image: redis:6.2-alpine
        command:
        - sh
        - -c  
        - | 
          # redis-0.redis가 IP를 반환할 때까지 무한 루프 (최대 30초 권장)
          echo &quot;Waiting for DNS...&quot;
          sleep 10
          nslookup redis-0.redis.default.svc.cluster.local
          echo &quot;DNS is ready! Starting Sentinel...&quot;
          exec redis-sentinel /conf/sentinel.conf
        ports:
        - containerPort: 26379
        volumeMounts:
        - name: conf
          mountPath: /conf
        - name: data
          mountPath: /data
      volumes:
      - name: readonly-config
        configMap:
          name: redis-config
      - name: conf
        emptyDir: {} # 여기에 설정파일을 복사해서 쓰기 권한을 확보함

  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ &quot;ReadWriteOnce&quot; ]
      storageClassName: longhorn-redis-special # 설치한 Longhorn 스토리지 클래스 이름
      resources:
        requests:
          storage: 1Gi # 용량은 필요에 따라 조절하세요
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Ingress (Nginx) - longhorn 대시보드 접속]]></title>
            <link>https://velog.io/@os_js/Ingress-Nginx-longhorn-%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C-%EC%A0%91%EC%86%8D</link>
            <guid>https://velog.io/@os_js/Ingress-Nginx-longhorn-%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C-%EC%A0%91%EC%86%8D</guid>
            <pubDate>Sat, 24 Jan 2026 12:31:47 GMT</pubDate>
            <description><![CDATA[<p>쿠버네티스  사용시 외부에서 들어오는 요청을 어디로 보낼것인가에 대한 답을 정해주는 역할.
longhorn 대시보드 접속하기.</p>
<ol>
<li>설치<pre><code>kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/baremetal/deploy.yaml</code></pre><img src="https://velog.velcdn.com/images/os_js/post/5ff91d39-bade-4e31-ae3c-9af0a05237f4/image.png" alt=""></li>
</ol>
<p>1-1. replica 설정
위 명령어로는  pod가 하나만 실행</p>
<pre><code>kubectl scale deployment ingress-nginx-controller -n ingress-nginx --replicas=3</code></pre><ol start="2">
<li><p>포트 확인</p>
<pre><code>kubectl get svc -n ingress-nginx ingress-nginx-controller</code></pre><p><img src="https://velog.velcdn.com/images/os_js/post/a3961643-e3b7-4c89-93d5-edac09998f36/image.png" alt=""></p>
</li>
<li><p>haproxy 설정 수정</p>
</li>
</ol>
<p>위의 사진에서 나오는 것처럼 외부의 80포트로 들어오면 32142 내부 번호로 연결이 된다.
아래의 내용을 haproxy 설정에 추가</p>
<pre><code>frontend k8s_http
    bind 10.0.2.10:80  # VIP의 80포트로 접속하면
    mode http
    default_backend ingress_nginx

backend ingress_nginx
    mode http
    balance roundrobin
    # 각 노드(VM)의 실제 IP와 확인한 NodePort를 적습니다.
    server node1 &lt;IP&gt;:32142 check
    server node2 &lt;IP&gt;:32142 check
    server node3 &lt;IP&gt;:32142 check</code></pre><ol start="4">
<li><p>longhorn 설치</p>
<pre><code>kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.5.3/deploy/longhorn.yaml</code></pre></li>
<li><p>longhorn ingress yml 파일</p>
<pre><code>apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: longhorn-ingress
namespace: longhorn-system
annotations:
 kubernetes.io/ingress.class: nginx
spec:
ingressClassName: nginx
rules:
- http:
   paths:
   - path: /
     pathType: Prefix
     backend:
       service:
         name: longhorn-frontend
         port:
           number: 80
</code></pre></li>
</ol>
<pre><code>
6. 이렇게 진행하면 이제 외부에서 포트포워딩이 필요할 경우 따로 설정후 
http://&lt;IP&gt;:80 으로 접속하면 대시보드 볼 수 있음.
  ![](https://velog.velcdn.com/images/os_js/post/13086a18-d578-49b2-ad29-25579ffa059e/image.png)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Haproxy/keepalived Ansible 사용]]></title>
            <link>https://velog.io/@os_js/Haproxykeepalived-Ansible-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@os_js/Haproxykeepalived-Ansible-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Fri, 23 Jan 2026 15:38:09 GMT</pubDate>
            <description><![CDATA[<p>kubernetes를 사용해보기위해 작업 진행중 외부에서 접속하는 vip가 필요.
vm을 활용하여 사용하기에 3대의 가상머신만 생성을 해서 haproxy/keepalived를 사용했어야 했다. 그런데 하나의 머신에서 모든 설정을 관리하기위한 방법으로 ansible을 사용하기로 하였음.
유지보수가 쉬워서.</p>
<h2 id="설치">설치</h2>
<pre><code># Ansible 설치(여러대중 하나만 설치하면됨)
sudo apt update &amp;&amp; sudo apt install -y ansible

# 인벤토리 파일 생성 (관리할 노드 목록)
sudo vi /etc/ansible/hosts
# 나의경우 위의 방법으로 수행했을때 파일을 쓸 수 없다고 하여.
# sudo touch /etc/ansible/hosts 를 통해서 파일을 생성하였다.
# 당연히 ansible폴더가 있어야한다.</code></pre><h3 id="etcansiblehosts">/etc/ansible/hosts</h3>
<pre><code>[masters]
node1 ansible_host=&lt;node1 IP&gt; k_priority=80
node2 ansible_host=&lt;node2 IP&gt; k_priority=100
node3 ansible_host=&lt;node3 IP&gt; k_priority=90

[masters:vars]
ansible_user=&lt;계정이름&gt;</code></pre><h3 id="ansible-계정-home에-위치haproxycfgj2">ansible 계정 home에 위치(haproxy.cfg.j2)</h3>
<pre><code>frontend k8s-api
    bind 10.0.2.10:6444 # loadbalancer server를 따로 두고있지 않으니 포트를 6444로 변경
    default_backend k8s-masters

backend k8s-masters
    balance roundrobin
    server node1 10.0.2.5:6443 check
    server node2 10.0.2.6:6443 check
    server node3 10.0.2.4:6443 check</code></pre><h3 id="ansible-계정-home에-위치keepalivedconfj2">ansible 계정 home에 위치(keepalived.conf.j2)</h3>
<pre><code>vrrp_instance VI_1 {
    state &quot;BACKUP&quot;
    interface enp0s3  # 실제 네트워크 인터페이스명으로 수정하세요
    virtual_router_id 51
    priority {{ k_priority }}
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass 1234
    }
    virtual_ipaddress {
        10.0.2.10  # 사용할 가상 IP
    }
}</code></pre><h3 id="ansible-계정-home에-위치-ha-setupyml">ansible 계정 home에 위치 ha-setup.yml</h3>
<pre><code>- hosts: masters
  become: yes
  tasks:
    - name: HAProxy 및 Keepalived 설치
      apt:
        name: [haproxy, keepalived]
        state: present
        update_cache: yes

    - name: HAProxy 설정 파일 배포
      template:
        src: ./haproxy.cfg.j2
        dest: /etc/haproxy/haproxy.cfg
      notify: restart haproxy

    - name: Keepalived 설정 파일 배포
      template:
        src: ./keepalived.conf.j2
        dest: /etc/keepalived/keepalived.conf
      notify: restart keepalived

    - name: Keepalived # 서비스를 재시작하고 부팅 시 자동 시작 설정
      systemd:
        name: keepalived
        state: restarted    # 서비스를 다시 시작함
        enabled: yes        # 부팅 시 자동으로 켜지게 함 (이게 안 되어 있었을 겁니다)
        daemon_reload: yes  # systemd 설정을 새로고침함

    - name: HAProxy #서비스를 재시작하고 부팅 시 자동 시작 설정
      systemd:
        name: haproxy
        state: restarted
        enabled: yes
        daemon_reload: yes
  handlers:
    - name: restart haproxy
      service: name=haproxy state=restarted
    - name: restart keepalived
      service: name=keepalived state=restarted</code></pre><pre><code># ansible 계정 생성(모든 노드 설정)
sudo useradd -m -s /bin/bash &lt;계정이름&gt;

# ansible 계정 비밀번호 설정 (초기 1회) (모든 노드 설정)
# 실행시 비밀번호 치는 구간이 나옴..
sudo passwd ansible

# sudo 권한 부여 (비밀번호 없이 sudo 가능하게 설정) (모든 노드 설정)
echo &quot;ansible ALL=(ALL) NOPASSWD:ALL&quot; | sudo tee /etc/sudoers.d/ansible


# ssh 인증 설정
# 1. ansible 계정으로 전환 (이미 전환했다면 패스)
sudo su - ansible

# 2. SSH 키 생성 (이미 만들었다면 패스)
# 중요!!!
# *****엔터만 계속 입력*******
ssh-keygen -t rsa

# 3. 각 노드에 내 열쇠(Public Key)를 전달 (비밀번호를 물어보면 아까 ansible 계정 passwd로 정한걸 입력하세요)
ssh-copy-id ansible@10.0.2.4
ssh-copy-id ansible@10.0.2.5
ssh-copy-id ansible@10.0.2.6


# ansible 계정 상태에서 실행
export ANSIBLE_HOST_KEY_CHECKING=False
ansible-playbook -i /etc/ansible/hosts ha-setup.yml


# 상태확인
ansible masters -m shell -a &quot;systemctl is-active haproxy&quot;
ansible masters -m shell -a &quot;systemctl is-active keepalived&quot;

# 특정노드만 실행
# ansible 계정에서 실행
ansible-playbook -i /etc/ansible/hosts ha-setup.yml --limit node1


# 중지
# 모든 마스터 노드의 keepalived와 haproxy 중지
ansible masters -m systemd -a &quot;name=keepalived state=stopped&quot; --become
ansible masters -m systemd -a &quot;name=haproxy state=stopped&quot; --become
</code></pre><h2 id="문제-발생">문제 발생.</h2>
<h3 id="haproxy">haproxy</h3>
<ul>
<li>haproxy를 실행하면서 vip가 할당되는 가상머신을 제외하고는 모두 haproxy가 꺼진다.</li>
<li>리눅스상에서 할당되지 않은 ip를 이용하여 포트를 열려고 시도하면 막아버리기 때문에 옵션을 통한 설정이 필요.<pre><code>sudo sysctl -w net.ipv4.ip_nonlocal_bind=1</code></pre></li>
<li>명령을 통해서 성공적인수행 가능.</li>
</ul>
<h3 id="파일-수정">파일 수정</h3>
<ul>
<li><p>위에서 작성한 ha-setup.yml 을 아래와 같이 수정한다.
```</p>
</li>
<li><p>hosts: masters
become: yes
tasks:</p>
<ul>
<li><p>name: 비로컬 IP 바인딩 허용 (운영 필수 설정)
sysctl:
  name: net.ipv4.ip_nonlocal_bind
  value: &#39;1&#39;
  state: present
  reload: yes</p>
</li>
<li><p>name: HAProxy 및 Keepalived 설치
apt:
  name: [haproxy, keepalived]
  state: present
  update_cache: yes</p>
</li>
<li><p>name: HAProxy 설정 파일 배포
template:
  src: ./haproxy.cfg.j2
  dest: /etc/haproxy/haproxy.cfg
notify: restart haproxy</p>
</li>
<li><p>name: Keepalived 설정 파일 배포
template:
  src: ./keepalived.conf.j2
  dest: /etc/keepalived/keepalived.conf
notify: restart keepalived</p>
</li>
<li><p>name: Keepalived # 서비스를 재시작하고 부팅 시 자동 시작 설정
systemd:
  name: keepalived
  state: restarted    # 서비스를 다시 시작함
  enabled: yes        # 부팅 시 자동으로 켜지게 함 (이게 안 되어 있었을 겁니다)
  daemon_reload: yes  # systemd 설정을 새로고침함</p>
</li>
<li><p>name: HAProxy #서비스를 재시작하고 부팅 시 자동 시작 설정
systemd:
  name: haproxy
  state: restarted
  enabled: yes
  daemon_reload: yes
handlers:</p>
</li>
<li><p>name: restart haproxy
service: name=haproxy state=restarted</p>
</li>
<li><p>name: restart keepalived
service: name=keepalived state=restarted</p>
</li>
</ul>
</li>
</ul>
<p>```</p>
<h2 id="참고">참고</h2>
<ul>
<li>해당 내용들은 사용자가 직접 명령을 통해서 끄거나 ansible-playbook 명령을 통해 실행시 실패하더라도 재시작 해주는 파일이 아닙니다.</li>
<li>기능을 구현하기 위해서는 따로 설정을 바꿔줘야하니 참고 바랍니다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>