<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sunny.log</title>
        <link>https://velog.io/</link>
        <description>IT</description>
        <lastBuildDate>Wed, 08 Jul 2026 07:45:18 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. sunny.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sunny-10" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[WireGuard VPN 세팅]]></title>
            <link>https://velog.io/@sunny-10/WireGuard-VPN-%EC%84%B8%ED%8C%85-g2casg59</link>
            <guid>https://velog.io/@sunny-10/WireGuard-VPN-%EC%84%B8%ED%8C%85-g2casg59</guid>
            <pubDate>Wed, 08 Jul 2026 07:45:18 GMT</pubDate>
            <description><![CDATA[<h1 id="proxmox-lxc에-wireguard-vpn-구성하기">Proxmox LXC에 WireGuard VPN 구성하기</h1>
<h2 id="1-구성-목적">1. 구성 목적</h2>
<p>회사에서 특정 IP만 접속 허용하는 환경이 있을 때, 집 공인 IP를 회사에 등록해두면 집에서는 정상적으로 접속할 수 있다.</p>
<p>하지만 카페, 외부 장소, 다른 집에서 노트북을 사용하면 접속 출발지 IP가 바뀌기 때문에 회사 시스템에 접속할 수 없다.</p>
<p>이 문제를 해결하기 위해 집에 VPN 서버를 구성하고, 외부에서는 VPN을 통해 집 네트워크를 경유하도록 만들었다.</p>
<p>최종 목표는 다음과 같다.</p>
<pre><code class="language-text">외부 노트북 / 휴대폰
  ↓ WireGuard VPN
집 Proxmox 서버
  ↓
집 공인 IP
  ↓
회사 시스템 접속</code></pre>
<p>즉, 외부에서 접속하더라도 회사 입장에서는 항상 <strong>집 공인 IP</strong>에서 접속한 것처럼 보이게 하는 구조다.</p>
<hr>
<h2 id="2-최종-구성도">2. 최종 구성도</h2>
<p>이번에 구성한 환경은 다음과 같다.</p>
<pre><code class="language-text">인터넷
  ↓
Proxmox Host
  - vmbr0: 공인망
    IP: 49.172.xxx.xxx/27
    GW: 49.172.xxx.1

  - vmbr1: 내부망
    IP: 10.10.10.1/24

  - LXC 102: wg-vpn
    IP: 10.10.10.20/24
    GW: 10.10.10.1
    WireGuard Port: UDP 51820</code></pre>
<p>WireGuard VPN 내부 대역은 별도로 아래처럼 구성했다.</p>
<pre><code class="language-text">WireGuard Server: 10.8.0.1/24
WireGuard Client: 10.8.0.2/32</code></pre>
<p>최종 트래픽 흐름은 다음과 같다.</p>
<pre><code class="language-text">휴대폰 LTE / 외부 노트북
  ↓
WireGuard Client
  ↓
Proxmox 공인 IP:51820/udp
  ↓
Proxmox iptables DNAT
  ↓
LXC wg-vpn:10.10.10.20:51820
  ↓
WireGuard wg0
  ↓
집 공인 IP로 외부 인터넷 접속</code></pre>
<hr>
<h2 id="3-사전-확인">3. 사전 확인</h2>
<p>먼저 Proxmox 호스트에서 네트워크 상태를 확인했다.</p>
<pre><code class="language-bash">ip -4 addr show vmbr0
ip -4 addr show vmbr1
ip route
curl -4 ifconfig.me</code></pre>
<p>확인 결과는 다음과 같았다.</p>
<pre><code class="language-text">vmbr0: 49.172.xxx.xxx/27
vmbr1: 10.10.10.1/24
default gateway: 49.172.xxx.1
curl ifconfig.me: 49.172.xxx.xxx</code></pre>
<p>여기서 중요한 점은 <code>vmbr0</code>의 IP와 <code>curl ifconfig.me</code> 결과가 동일했다는 것이다.</p>
<p>즉, Proxmox 호스트가 사설 IP 뒤에 있는 것이 아니라 <strong>공인 IP를 직접 가지고 있는 상태</strong>였다.</p>
<p>또한 내부망 NAT가 이미 설정되어 있었다.</p>
<pre><code class="language-bash">iptables -t nat -S</code></pre>
<p>기존 NAT 룰은 다음과 같은 형태였다.</p>
<pre><code class="language-text">-A POSTROUTING -s 10.10.10.0/24 -o vmbr0 -j MASQUERADE</code></pre>
<p>IP Forward 상태도 확인했다.</p>
<pre><code class="language-bash">sysctl net.ipv4.ip_forward</code></pre>
<p>결과:</p>
<pre><code class="language-text">net.ipv4.ip_forward = 1</code></pre>
<p>이 값이 <code>1</code>이어야 내부망 또는 VPN 클라이언트의 트래픽을 외부로 라우팅할 수 있다.</p>
<p>만약 <code>0</code>이라면 아래처럼 활성화한다.</p>
<pre><code class="language-bash">echo &quot;net.ipv4.ip_forward=1&quot; &gt; /etc/sysctl.d/99-ipforward.conf
sysctl -p /etc/sysctl.d/99-ipforward.conf</code></pre>
<hr>
<h2 id="4-debian-12-lxc-생성">4. Debian 12 LXC 생성</h2>
<p>WireGuard는 매우 가볍기 때문에 별도의 VM보다 LXC로 구성했다.</p>
<p>Proxmox GUI에서 <code>Create CT</code>를 선택하고 다음과 같이 생성했다.</p>
<pre><code class="language-text">CT ID: 102
Hostname: wg-vpn
Template: Debian 12
Unprivileged container: 체크
Disk: 5GB
CPU: 1 Core
Memory: 512MB
Swap: 512MB
Bridge: vmbr1
IPv4: 10.10.10.20/24
Gateway: 10.10.10.1
DNS: 1.1.1.1</code></pre>
<p>DNS 설정 시 <code>search domain</code>은 비워두었다.</p>
<hr>
<h2 id="5-lxc-tun-장치-허용">5. LXC TUN 장치 허용</h2>
<p>WireGuard는 <code>/dev/net/tun</code> 장치를 사용한다.
LXC에서 WireGuard를 사용하기 위해 Proxmox 호스트에서 다음 설정을 추가했다.</p>
<p>CT ID는 <code>102</code> 기준이다.</p>
<pre><code class="language-bash">pct stop 102
pct set 102 -features nesting=1

mkdir -p /dev/net
[ -c /dev/net/tun ] || mknod /dev/net/tun c 10 200
chmod 666 /dev/net/tun

grep -q &quot;c 10:200&quot; /etc/pve/lxc/102.conf || echo &quot;lxc.cgroup2.devices.allow: c 10:200 rwm&quot; &gt;&gt; /etc/pve/lxc/102.conf

grep -q &quot;/dev/net&quot; /etc/pve/lxc/102.conf || echo &quot;lxc.mount.entry: /dev/net dev/net none bind,create=dir&quot; &gt;&gt; /etc/pve/lxc/102.conf

pct start 102</code></pre>
<p>이후 LXC에 접속했다.</p>
<pre><code class="language-bash">pct enter 102</code></pre>
<p>네트워크 확인:</p>
<pre><code class="language-bash">ip a
ip route
ping -c 3 1.1.1.1
apt update</code></pre>
<p>정상적으로 아래와 같이 확인되었다.</p>
<pre><code class="language-text">eth0: 10.10.10.20/24
default via 10.10.10.1
ping 1.1.1.1 성공
apt update 성공</code></pre>
<hr>
<h2 id="6-wireguard-설치">6. WireGuard 설치</h2>
<p>LXC 내부에서 WireGuard를 설치했다.</p>
<pre><code class="language-bash">apt install -y wireguard wireguard-tools iptables curl</code></pre>
<p>설치 확인:</p>
<pre><code class="language-bash">wg --version</code></pre>
<hr>
<h2 id="7-lxc-내부-ip-forward-활성화">7. LXC 내부 IP Forward 활성화</h2>
<p>LXC 내부에서도 VPN 클라이언트 트래픽을 전달해야 하므로 IP Forward를 활성화했다.</p>
<pre><code class="language-bash">cat &gt; /etc/sysctl.d/99-wireguard.conf &lt;&lt; &#39;EOF&#39;
net.ipv4.ip_forward=1
EOF

sysctl -p /etc/sysctl.d/99-wireguard.conf</code></pre>
<p>정상 결과:</p>
<pre><code class="language-text">net.ipv4.ip_forward = 1</code></pre>
<hr>
<h2 id="8-wireguard-키-생성">8. WireGuard 키 생성</h2>
<p>LXC 내부에서 서버 키와 클라이언트 키를 생성했다.</p>
<pre><code class="language-bash">mkdir -p /etc/wireguard
cd /etc/wireguard
umask 077

wg genkey | tee server_private.key | wg pubkey &gt; server_public.key
wg genkey | tee client_laptop_private.key | wg pubkey &gt; client_laptop_public.key</code></pre>
<p>파일 확인:</p>
<pre><code class="language-bash">ls -l /etc/wireguard</code></pre>
<p>생성된 파일은 다음과 같다.</p>
<pre><code class="language-text">server_private.key
server_public.key
client_laptop_private.key
client_laptop_public.key</code></pre>
<p>주의할 점은 <code>private.key</code> 파일은 절대 외부에 공개하면 안 된다.</p>
<hr>
<h2 id="9-wireguard-서버-설정">9. WireGuard 서버 설정</h2>
<p>LXC 내부에서 <code>/etc/wireguard/wg0.conf</code> 파일을 생성했다.</p>
<pre><code class="language-bash">SERVER_PRIVATE_KEY=$(cat /etc/wireguard/server_private.key)
CLIENT_PUBLIC_KEY=$(cat /etc/wireguard/client_laptop_public.key)

cat &gt; /etc/wireguard/wg0.conf &lt;&lt; EOF
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = ${SERVER_PRIVATE_KEY}

PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE

[Peer]
PublicKey = ${CLIENT_PUBLIC_KEY}
AllowedIPs = 10.8.0.2/32
EOF</code></pre>
<p>권한 설정:</p>
<pre><code class="language-bash">chmod 600 /etc/wireguard/wg0.conf</code></pre>
<p>여기서 중요한 부분은 다음이다.</p>
<pre><code class="language-ini">Address = 10.8.0.1/24
ListenPort = 51820</code></pre>
<p>WireGuard 서버는 <code>10.8.0.1</code>을 사용하고, UDP <code>51820</code> 포트에서 대기한다.</p>
<p>클라이언트는 다음 IP를 사용한다.</p>
<pre><code class="language-ini">AllowedIPs = 10.8.0.2/32</code></pre>
<hr>
<h2 id="10-wireguard-서비스-시작">10. WireGuard 서비스 시작</h2>
<p>WireGuard를 시작하고 자동 시작 설정을 추가했다.</p>
<pre><code class="language-bash">systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0</code></pre>
<p>상태 확인:</p>
<pre><code class="language-bash">systemctl status wg-quick@wg0 --no-pager</code></pre>
<p>정상 상태:</p>
<pre><code class="language-text">active (exited)
enabled</code></pre>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/2fc34aa4-a2d9-424e-95cb-7a7d8c73ff3f/image.png" alt=""></p>
<p>WireGuard 상태 확인:</p>
<pre><code class="language-bash">wg</code></pre>
<p>정상적으로 아래와 같은 정보가 확인된다.</p>
<pre><code class="language-text">interface: wg0
  listening port: 51820

peer:
  allowed ips: 10.8.0.2/32</code></pre>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/78f52cd9-b4c6-43d8-a657-e80f7f5343cc/image.png" alt=""></p>
<hr>
<h2 id="11-proxmox-호스트에서-포트포워딩-설정">11. Proxmox 호스트에서 포트포워딩 설정</h2>
<p>LXC는 내부망인 <code>10.10.10.20</code>에 있기 때문에, 외부에서 Proxmox 공인 IP의 UDP 51820 포트로 들어온 트래픽을 LXC로 전달해야 한다.</p>
<p>LXC에서 나와 Proxmox 호스트에서 작업한다.</p>
<pre><code class="language-bash">exit</code></pre>
<p>Proxmox 호스트에서 아래 명령을 실행했다.</p>
<pre><code class="language-bash">iptables -t nat -A PREROUTING -i vmbr0 -p udp --dport 51820 -j DNAT --to-destination 10.10.10.20:51820

iptables -A FORWARD -i vmbr0 -o vmbr1 -p udp -d 10.10.10.20 --dport 51820 -j ACCEPT

iptables -A FORWARD -i vmbr1 -o vmbr0 -s 10.10.10.20 -j ACCEPT</code></pre>
<p>확인:</p>
<pre><code class="language-bash">iptables -t nat -S | grep 51820
iptables -S FORWARD | grep 10.10.10.20</code></pre>
<p>정상적으로 다음과 같이 확인된다.</p>
<pre><code class="language-text">-A PREROUTING -i vmbr0 -p udp -m udp --dport 51820 -j DNAT --to-destination 10.10.10.20:51820

-A FORWARD -d 10.10.10.20/32 -i vmbr0 -o vmbr1 -p udp -m udp --dport 51820 -j ACCEPT
-A FORWARD -s 10.10.10.20/32 -i vmbr1 -o vmbr0 -j ACCEPT</code></pre>
<hr>
<h2 id="12-클라이언트-설정-파일-생성">12. 클라이언트 설정 파일 생성</h2>
<p>다시 LXC에 접속한다.</p>
<pre><code class="language-bash">pct enter 102</code></pre>
<p>클라이언트 설정 파일을 생성한다.</p>
<pre><code class="language-bash">PUBLIC_IP=$(curl -4 -s ifconfig.me)
SERVER_PUBLIC_KEY=$(cat /etc/wireguard/server_public.key)
CLIENT_PRIVATE_KEY=$(cat /etc/wireguard/client_laptop_private.key)

cat &gt; /root/client-laptop.conf &lt;&lt; EOF
[Interface]
PrivateKey = ${CLIENT_PRIVATE_KEY}
Address = 10.8.0.2/32
DNS = 1.1.1.1

[Peer]
PublicKey = ${SERVER_PUBLIC_KEY}
Endpoint = ${PUBLIC_IP}:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
EOF</code></pre>
<p>확인:</p>
<pre><code class="language-bash">cat /root/client-laptop.conf</code></pre>
<p>설정 파일 형태는 다음과 같다.</p>
<pre><code class="language-ini">[Interface]
PrivateKey = 클라이언트_개인키
Address = 10.8.0.2/32
DNS = 1.1.1.1

[Peer]
PublicKey = 서버_공개키
Endpoint = 49.172.xxx.xxx:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25</code></pre>
<p>여기서 <code>AllowedIPs = 0.0.0.0/0</code>의 의미는 클라이언트의 모든 IPv4 트래픽을 WireGuard VPN으로 보내겠다는 뜻이다.</p>
<p>즉 외부에서 VPN을 켜면 인터넷 접속도 집 Proxmox 서버를 경유하게 된다.</p>
<hr>
<h2 id="13-휴대폰으로-테스트">13. 휴대폰으로 테스트</h2>
<p>처음에는 외부 노트북으로 바로 테스트하기 어려워서 휴대폰 LTE로 테스트했다.</p>
<p>휴대폰에 WireGuard 앱을 설치한 뒤, <code>client-laptop.conf</code> 내용을 등록했다. (QR 버전으로 등록 완료)</p>
<p>QR 코드로 conf 등록하는 방법은 다음과 같다.</p>
<pre><code class="language-bash">apt install -y qrencode

qrencode -t ansiutf8 &lt; /root/client-laptop.conf
# 터미널에서 QR코드가 나옴</code></pre>
<pre><code class="language-text">WireGuard 앱에서 +
→ Create from QR code
→ 화면의 QR 코드 스캔</code></pre>
<p>테스트 조건은 다음과 같다.</p>
<pre><code class="language-text">휴대폰 Wi-Fi OFF
LTE 상태
WireGuard ON</code></pre>
<p>브라우저에서 아래 사이트에 접속했다.</p>
<pre><code class="language-text">https://ifconfig.me</code></pre>
<p>결과가 집 공인 IP로 확인되었다.</p>
<pre><code class="language-text">IP Address: 49.172.xxx.xxx</code></pre>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/9fd6f683-56a5-43b8-84d3-1d09b9cddbef/image.png" alt=""></p>
<p>WireGuard를 Off하게되면 휴대폰 LTE IP로 변경되는 것을 확인했다.
<img src="https://velog.velcdn.com/images/sunny-10/post/7f9caaa3-b86c-4a6d-b453-0a894359e5ef/image.png" alt=""></p>
<p>즉, 휴대폰 LTE 상태에서도 VPN을 통해 집 공인 IP로 나가는 것을 확인했다.</p>
<p>서버에서도 확인했다.</p>
<pre><code class="language-bash">wg</code></pre>
<p>정상 연결 시 아래 항목이 보인다.</p>
<pre><code class="language-text">latest handshake: 몇 초 전
transfer: xx KiB received, xx KiB sent</code></pre>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/f4f76871-436e-4001-bc3f-712c0c3c722d/image.png" alt=""></p>
<hr>
<h2 id="14-중복-iptables-룰-정리">14. 중복 iptables 룰 정리</h2>
<p>작업 중 같은 iptables 룰이 중복으로 들어간 경우가 있었다.</p>
<p>전체 NAT 룰 확인:</p>
<pre><code class="language-bash">iptables -t nat -S</code></pre>
<p>FORWARD 룰 확인:</p>
<pre><code class="language-bash">iptables -S FORWARD</code></pre>
<p>중복된 NAT MASQUERADE 룰은 다음 명령으로 하나 제거했다.</p>
<pre><code class="language-bash">iptables -t nat -D POSTROUTING -s 10.10.10.0/24 -o vmbr0 -j MASQUERADE</code></pre>
<p>중복된 DNAT 룰은 다음 명령으로 하나 제거했다.</p>
<pre><code class="language-bash">iptables -t nat -D PREROUTING -i vmbr0 -p udp --dport 51820 -j DNAT --to-destination 10.10.10.20:51820</code></pre>
<p>중복된 FORWARD 룰은 다음 명령으로 하나씩 제거했다.</p>
<pre><code class="language-bash">iptables -D FORWARD -d 10.10.10.20/32 -i vmbr0 -o vmbr1 -p udp --dport 51820 -j ACCEPT

iptables -D FORWARD -s 10.10.10.20/32 -i vmbr1 -o vmbr0 -j ACCEPT</code></pre>
<p>최종적으로 남겨야 할 핵심 룰은 다음과 같다.</p>
<pre><code class="language-text">nat 테이블

-A PREROUTING -i vmbr0 -p udp -m udp --dport 51820 -j DNAT --to-destination 10.10.10.20:51820
-A POSTROUTING -s 10.10.10.0/24 -o vmbr0 -j MASQUERADE</code></pre>
<pre><code class="language-text">filter 테이블

-A FORWARD -d 10.10.10.20/32 -i vmbr0 -o vmbr1 -p udp -m udp --dport 51820 -j ACCEPT
-A FORWARD -s 10.10.10.20/32 -i vmbr1 -o vmbr0 -j ACCEPT</code></pre>
<p>정리 후 휴대폰 LTE에서 다시 WireGuard를 켜고 <code>ifconfig.me</code>로 확인했으며, 정상적으로 집 공인 IP가 나왔다.</p>
<hr>
<h2 id="15-proxmox-저장소-이슈-해결">15. Proxmox 저장소 이슈 해결</h2>
<p>iptables 영구화를 위해 <code>apt update</code>를 실행했는데 다음과 같은 에러가 발생했다.</p>
<pre><code class="language-text">401 Unauthorized
https://enterprise.proxmox.com/debian/pve
https://enterprise.proxmox.com/debian/ceph-squid</code></pre>
<p>원인은 Proxmox 유료 구독용 Enterprise Repository가 활성화되어 있었기 때문이다.</p>
<p>구독을 사용하지 않는 홈랩 환경에서는 Enterprise 저장소를 비활성화하고 No-Subscription 저장소를 사용하면 된다.</p>
<p>현재 환경에서는 다음 저장소를 사용했다.</p>
<pre><code class="language-text">deb http://download.proxmox.com/debian/pve trixie pve-no-subscription</code></pre>
<p>GUI에서는 다음 경로에서 설정할 수 있다.</p>
<pre><code class="language-text">pve 노드 선택
→ Updates
→ Repositories
→ Enterprise Repository 비활성화
→ No-Subscription Repository 추가</code></pre>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/e28c153a-72ab-4bc5-af68-a56b4be46cbc/image.png" alt=""></p>
<p>이후 다시 <code>apt update</code>를 실행하니 정상적으로 완료되었다.</p>
<hr>
<h2 id="16-iptables-영구화">16. iptables 영구화</h2>
<p>iptables 룰은 기본적으로 재부팅하면 사라질 수 있다.</p>
<p>따라서 <code>iptables-persistent</code>를 설치해 영구화했다.</p>
<pre><code class="language-bash">apt update
apt install -y iptables-persistent</code></pre>
<p>설치 중 질문은 다음과 같이 선택했다.</p>
<pre><code class="language-text">Save current IPv4 rules? → Yes
Save current IPv6 rules? → No</code></pre>
<p>설치 후 현재 룰을 저장했다.</p>
<pre><code class="language-bash">netfilter-persistent save</code></pre>
<p>저장 파일 확인:</p>
<pre><code class="language-bash">cat /etc/iptables/rules.v4 | grep 51820
cat /etc/iptables/rules.v4 | grep &quot;10.10.10.0/24&quot;
cat /etc/iptables/rules.v4 | grep &quot;10.10.10.20&quot;</code></pre>
<p>정상적으로 다음과 같은 룰이 저장되어 있어야 한다.</p>
<pre><code class="language-text">-A PREROUTING -i vmbr0 -p udp -m udp --dport 51820 -j DNAT --to-destination 10.10.10.20:51820
-A POSTROUTING -s 10.10.10.0/24 -o vmbr0 -j MASQUERADE
-A FORWARD -d 10.10.10.20/32 -i vmbr0 -o vmbr1 -p udp -m udp --dport 51820 -j ACCEPT
-A FORWARD -s 10.10.10.20/32 -i vmbr1 -o vmbr0 -j ACCEPT</code></pre>
<p>서비스 활성화 확인:</p>
<pre><code class="language-bash">systemctl is-enabled netfilter-persistent</code></pre>
<p>정상 결과:</p>
<pre><code class="language-text">enabled</code></pre>
<hr>
<h2 id="17-lxc-자동-시작-설정">17. LXC 자동 시작 설정</h2>
<p>Proxmox 서버가 재부팅되었을 때 VPN LXC도 자동으로 시작되도록 설정했다.</p>
<pre><code class="language-bash">pct set 102 -onboot 1</code></pre>
<p>확인:</p>
<pre><code class="language-bash">pct config 102 | grep onboot</code></pre>
<p>정상 결과:</p>
<pre><code class="language-text">onboot: 1</code></pre>
<p>또는 GUI에서 다음 경로로 설정할 수 있다.</p>
<pre><code class="language-text">CT 102(wg-vpn)
→ Options
→ Start at boot
→ Yes</code></pre>
<p>LXC 내부 WireGuard 서비스도 자동 시작 상태인지 확인했다.</p>
<pre><code class="language-bash">pct enter 102
systemctl is-enabled wg-quick@wg0</code></pre>
<p>정상 결과:</p>
<pre><code class="language-text">enabled</code></pre>
<hr>
<h2 id="18-최종-점검-명령어">18. 최종 점검 명령어</h2>
<p>Proxmox 호스트에서 확인할 명령어:</p>
<pre><code class="language-bash">iptables -t nat -S | grep 51820
iptables -t nat -S | grep &quot;10.10.10.0/24&quot;
iptables -S FORWARD | grep &quot;10.10.10.20&quot;
systemctl is-enabled netfilter-persistent
pct config 102 | grep onboot</code></pre>
<p>LXC 내부에서 확인할 명령어:</p>
<pre><code class="language-bash">systemctl status wg-quick@wg0 --no-pager
wg</code></pre>
<p>클라이언트에서 확인할 것:</p>
<pre><code class="language-text">WireGuard ON
https://ifconfig.me 접속
집 공인 IP로 보이는지 확인</code></pre>
<hr>
<h2 id="19-최종-결과">19. 최종 결과</h2>
<p>최종적으로 다음 상태가 되었다.</p>
<pre><code class="language-text">Proxmox Host
  - 공인 IP: 49.172.xxx.xxx
  - vmbr0: 공인망
  - vmbr1: 내부망 10.10.10.1/24
  - iptables-persistent 활성화

WireGuard LXC
  - CT ID: 102
  - Hostname: wg-vpn
  - IP: 10.10.10.20/24
  - WireGuard Server IP: 10.8.0.1/24
  - Listen Port: UDP 51820
  - Start at boot: enabled

Client
  - VPN IP: 10.8.0.2/32
  - AllowedIPs: 0.0.0.0/0</code></pre>
<p>휴대폰 LTE 환경에서 WireGuard를 켰을 때 <code>ifconfig.me</code> 결과가 집 공인 IP로 확인되었으므로, VPN 구성은 정상적으로 완료되었다.</p>
<hr>
<h2 id="20-주의사항">20. 주의사항</h2>
<p>첫 번째, <code>client-laptop.conf</code> 파일에는 클라이언트 개인키가 들어 있다.
블로그나 외부 문서에 절대 그대로 올리면 안 된다.</p>
<pre><code class="language-ini">PrivateKey = 절대_공개_금지</code></pre>
<p>두 번째, 같은 클라이언트 설정을 휴대폰과 노트북에서 동시에 사용하면 안 된다.</p>
<p>예를 들어 휴대폰과 노트북이 둘 다 아래 설정을 동시에 사용하면 충돌이 발생할 수 있다.</p>
<pre><code class="language-text">같은 PrivateKey
같은 Address = 10.8.0.2/32</code></pre>
<p>테스트 후 휴대폰에서 터널을 삭제하고 노트북에서 같은 설정을 사용하는 것은 괜찮다.
하지만 운영용으로는 기기별로 Peer를 분리하는 것이 좋다.</p>
<pre><code class="language-text">노트북: 10.8.0.2/32
휴대폰: 10.8.0.3/32</code></pre>
<p>세 번째, 가정용 인터넷의 공인 IP는 유동 IP일 수 있다.
집 공인 IP가 바뀌면 회사에 등록된 허용 IP도 다시 변경해야 한다.</p>
<p>네 번째, Proxmox 호스트와 LXC를 재부팅해도 VPN이 자동 복구되려면 아래 세 가지가 모두 되어 있어야 한다.</p>
<pre><code class="language-text">1. netfilter-persistent enabled
2. CT 102 onboot enabled
3. wg-quick@wg0 enabled</code></pre>
<hr>
<h2 id="마무리">마무리</h2>
<p>이번 구성으로 외부에서도 WireGuard VPN을 통해 집 Proxmox 서버를 경유할 수 있게 되었다.</p>
<p>결과적으로 카페나 외부 장소에서 노트북을 사용하더라도 회사 시스템에는 항상 집 공인 IP로 접속하는 구조가 완성되었다.</p>
<pre><code class="language-text">외부 노트북
  → WireGuard
  → 집 Proxmox
  → 집 공인 IP
  → 회사 시스템</code></pre>
<p>리소스도 매우 적게 사용한다.</p>
<pre><code class="language-text">WireGuard LXC
CPU: 1 Core
Memory: 512MB
Disk: 5GB</code></pre>
<p>따라서 작은 홈서버나 Proxmox 환경에서도 부담 없이 운영할 수 있는 구성이라고 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Proxmox 세팅]]></title>
            <link>https://velog.io/@sunny-10/Proxmox-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@sunny-10/Proxmox-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Wed, 24 Jun 2026 04:22:28 GMT</pubDate>
            <description><![CDATA[<h3 id="proxmox-란">Proxmox 란?</h3>
<p>하나의 물리 서버를 통해 다수의 가상 머신(VM) 및 컨테이너를 생성하고 관리할 수 있는 오픈소스 가상화 플랫폼</p>
<h3 id="설치">설치</h3>
<h4 id="1-설치-전-준비">1. 설치 전 준비</h4>
<ul>
<li>설치할 PC 또는 서버 준비</li>
<li>설치 대상 디스크 확인</li>
<li>기존 데이터 백업</li>
<li>BIOS/UEFI 진입</li>
<li>가상화 옵션 활성화
Intel: VT-x
AMD: SVM
Secure Boot는 문제가 생기면 비활성화</li>
</ul>
<h4 id="2-proxmox-ve-iso-다운로드">2. Proxmox VE ISO 다운로드</h4>
<ul>
<li>Proxmox 공식 사이트에서 Proxmox VE ISO 다운로드
<a href="https://www.proxmox.com/en/downloads/proxmox-virtual-environment/iso">https://www.proxmox.com/en/downloads/proxmox-virtual-environment/iso</a></li>
</ul>
<h4 id="3-부팅-usb-생성">3. 부팅 USB 생성</h4>
<ul>
<li>Rufus 또는 Ventoy 사용</li>
<li>USB에 Proxmox ISO 적용</li>
<li>설치할 장비에 USB 연결</li>
</ul>
<h4 id="4-usb로-부팅">4. USB로 부팅</h4>
<ul>
<li>부팅 메뉴 진입</li>
<li>USB 선택</li>
<li>Proxmox VE Installer 실행</li>
<li><em>참고: 그래픽 화면에서 멈추거나 검은 화면이 나오면 부팅 옵션에 nomodeset을 추가*</em>
<img src="https://velog.velcdn.com/images/sunny-10/post/8dd6e1bd-a378-4b8d-bdc9-f9e0802e2058/image.jpg" alt=""><pre><code>옵션 설정 순서
</code></pre></li>
</ul>
<p>Proxmox 설치 USB로 부팅
Proxmox 부팅 메뉴에서 Install Proxmox VE 선택 위치로 이동
바로 Enter 누르지 말고 키보드에서 e 입력
부팅 옵션 편집 화면이 열림
linux 또는 linuxefi로 시작하는 줄을 찾기
해당 줄 끝부분에 nomodeset 추가</p>
<pre><code>
#### 5. Proxmox 설치 시작
- Install Proxmox VE 선택
- 라이선스 동의
- 설치 대상 디스크 선택
- 여기서 선택한 디스크는 초기화되므로 반드시 확인
![](https://velog.velcdn.com/images/sunny-10/post/f12f7d00-3b67-4f4e-8601-dbf179ce9cef/image.jpg)


#### 6. 디스크 구성 선택
- 단일 디스크 테스트용이면 기본값으로 진행 가능
- 운영 또는 홈랩 장기 사용이면 ZFS, RAID 구성 여부 검토

#### 7. 국가/시간대/키보드 설정
- Country: Korea
- Time zone: Asia/Seoul
- Keyboard: 보통 English 또는 사용하는 키보드에 맞게 선택

#### 8. 관리자 계정 설정
- root 비밀번호 입력
- 관리자 이메일 입력
- 로그인 계정은 기본적으로 root@pam

#### 9. 네트워크 설정
- Management Interface 선택
- Hostname 설정 예시: pve.local
- IP 주소 설정
- Gateway 설정
- DNS 설정

#### 10. 설치 진행 및 재부팅
- 설정 확인 후 Install
- 설치 완료 후 USB 제거
- Reboot

#### 11. 웹 콘솔 접속
- 같은 네트워크의 PC에서 브라우저 접속
  https://Proxmox_IP:8006
- 인증서 경고는 자체 서명 인증서라서 처음에는 정상적으로 보일 수 있음
- 로그인 계정: root@pam
![](https://velog.velcdn.com/images/sunny-10/post/d2242fc5-8953-4fa6-ac0e-0ae358173001/image.png)


#### 12. 설치 후 기본 설정
- 업데이트 저장소 확인
- 구독이 없는 테스트/홈랩 환경이면 no-subscription 저장소를 사용할 수 있음
- 공식 문서상 no-subscription 저장소는 구독키 없이 사용할 수 있지만, 테스트/비운영 용도에 가깝고 운영용으로는 Enterprise Repository가 권장

#### 13. 업데이트 실행
- Proxmox 웹 콘솔 또는 Shell에서 업데이트
```apt update
apt full-upgrade -y</code></pre><ul>
<li>기본 확인</li>
<li>노드 상태 확인</li>
<li>스토리지 확인</li>
<li>네트워크 브리지 확인
보통 기본 브리지는 vmbr0</li>
<li>CPU, 메모리, 디스크 인식 확인</li>
</ul>
<h4 id="14-vm-생성-준비">14. VM 생성 준비</h4>
<ul>
<li>ISO 업로드</li>
<li>VM 생성</li>
<li>CPU, Memory, Disk 설정</li>
<li>Windows VM이면 VirtIO ISO도 함께 준비</li>
<li>Linux VM이면 일반 ISO만으로도 설치 가능</li>
</ul>
<p><strong>본 구성에서는 Windows VM 세팅</strong>
<img src="https://velog.velcdn.com/images/sunny-10/post/ce321406-65b5-4d22-b4eb-ecd27f98b90c/image.png" alt=""></p>
<p><strong>Windows VM 설치가 안될 경우,</strong>
Options &gt; Boot Order 에 진입하여 Boot 순서 확인
virtio가 1순위일 경우 부팅이 제대로 되지 않음
<img src="https://velog.velcdn.com/images/sunny-10/post/0aba74a4-905c-4766-bfa7-d5e1cf1ad290/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/9eb845be-6620-4abb-900c-b5d3a74b1d6c/image.png" alt=""></p>
<p><strong>화면 배율 설정 방법</strong>
Windows 배경화면에서 디스플레이 설정 시 
해상도 설정이 안될 경우 </p>
<p>Boot Device 화면 진입
EFI Firmware Setup &gt; Device Manager &gt; OVMF Platform Configuration &gt; Preferred Resolution 
을 통해 원하는 해상도 설정(1920 x 1080) 이후 재시작</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CloudForet SCP Provider 및 Custom Collector 구성 핵심 정리]]></title>
            <link>https://velog.io/@sunny-10/CloudForet-SCP-Provider-%EB%B0%8F-Custom-Collector-%EA%B5%AC%EC%84%B1-%ED%95%B5%EC%8B%AC-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@sunny-10/CloudForet-SCP-Provider-%EB%B0%8F-Custom-Collector-%EA%B5%AC%EC%84%B1-%ED%95%B5%EC%8B%AC-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 16 Jun 2026 06:25:02 GMT</pubDate>
            <description><![CDATA[<h2 id="1-전체-목표">1. 전체 목표</h2>
<p>CloudForet에서 기본 지원하지 않는 Samsung Cloud Platform(SCP)의 Virtual Server 정보를 수집하기 위해 Custom Provider, Secret Schema, Plugin, Service Account, Collector를 구성하고, SCP OpenAPI를 호출하는 Custom Inventory Collector를 제작하였다.</p>
<p>최종 목표는 SCP OpenAPI에서 조회한 VM 정보를 CloudForet의 <code>inventory.CloudService</code> 리소스로 변환하여 CloudForet 콘솔에서 SCP 서버 목록을 확인하는 것이다.</p>
<hr>
<h2 id="2-cloudforet-수집-구조의-핵심-구성요소">2. CloudForet 수집 구조의 핵심 구성요소</h2>
<p>CloudForet에서 외부 CSP 리소스를 수집하려면 다음 구성요소가 필요하다.</p>
<h3 id="provider">Provider</h3>
<p>Provider는 CloudForet에서 CSP 또는 외부 플랫폼을 구분하는 기준이다.</p>
<p>이번 실습에서는 <code>scp</code> Provider를 생성하여 Samsung Cloud Platform을 CloudForet 내 독립 Provider로 인식하게 했다.</p>
<p>핵심 값:</p>
<pre><code class="language-text">provider: scp
name: Samsung Cloud Platform</code></pre>
<p>Provider가 없으면 Service Account, Secret, Collector가 어느 CSP에 속하는지 구분할 수 없다.</p>
<hr>
<h3 id="secret-schema">Secret Schema</h3>
<p>Secret Schema는 해당 Provider에 접속하기 위해 어떤 인증 정보가 필요한지 정의하는 구조다.</p>
<p>이번 SCP Collector에서는 SCP OpenAPI 호출을 위해 다음 값이 필요했다.</p>
<pre><code class="language-text">access_key
access_secret_key
project_id
client_type
language
resource_type
base_endpoint
region_code
ssl_verify</code></pre>
<p>핵심은 <code>access_key</code>, <code>access_secret_key</code>, <code>project_id</code>가 필수 인증값이라는 점이다.</p>
<p>Secret Schema는 UI 입력 폼과 Secret 저장 구조의 기준이 된다.</p>
<hr>
<h3 id="repository-plugin">Repository Plugin</h3>
<p>Repository Plugin은 CloudForet가 실제로 실행할 Collector Plugin 이미지 정보를 정의한다.</p>
<p>이번 실습에서는 Docker Hub에 Custom Collector 이미지를 push하고 CloudForet Plugin으로 등록했다.</p>
<p>핵심 값:</p>
<pre><code class="language-text">plugin_id: scp-vm-collector
provider: scp
service_type: inventory.Collector
image: ypjs09/scp-vm-collector
registry_type: DOCKER_HUB
version: 0.1.9</code></pre>
<p>중요한 점은 Docker Hub에 이미지를 push하는 것과 CloudForet Repository에 Plugin 버전이 등록되는 것은 별개라는 것이다.</p>
<p>CloudForet가 모르는 버전으로 Collector를 생성하면 다음과 같은 오류가 발생한다.</p>
<pre><code class="language-text">version does not exist</code></pre>
<hr>
<h3 id="service-account">Service Account</h3>
<p>Service Account는 특정 Provider의 계정 또는 프로젝트 단위를 CloudForet에 등록하는 객체다.</p>
<p>이번 실습에서는 SCP 프로젝트를 하나의 Service Account로 등록했다.</p>
<p>핵심 값:</p>
<pre><code class="language-text">provider: scp
service_account_id: sa-d88be3973e9a
scope: PROJECT
project_id: project-2bcfd61ce098</code></pre>
<p>Service Account는 “어떤 SCP 프로젝트를 수집할 것인가”를 나타낸다.</p>
<hr>
<h3 id="secret">Secret</h3>
<p>Secret은 Service Account에 연결되는 실제 인증 정보다.</p>
<p>이번 실습에서 중요한 트러블슈팅 포인트는 Service Account는 있었지만 Secret이 없어서 Collector JobTask가 생성되지 않았던 점이다.</p>
<p>즉, CloudForet 수집 흐름에서는 다음 연결이 반드시 필요하다.</p>
<pre><code class="language-text">Provider
  → Service Account
    → Secret
      → Collector</code></pre>
<p>Secret이 없으면 Collector가 어떤 인증 정보로 수집해야 하는지 알 수 없다.</p>
<hr>
<h3 id="collector">Collector</h3>
<p>Collector는 특정 Plugin을 이용해서 실제 수집 작업을 수행하는 객체다.</p>
<p>이번 실습의 최종 Collector는 다음과 같다.</p>
<pre><code class="language-text">collector_id: collector-71452b876d46
name: SCP VM Collector 019
provider: scp
plugin_id: scp-vm-collector
version: 0.1.9
secret_filter:
  service_accounts:
    - sa-d88be3973e9a</code></pre>
<p>Collector의 핵심은 <code>plugin_info</code>와 <code>secret_filter</code>다.</p>
<p><code>plugin_info</code>는 어떤 Collector Plugin 이미지를 실행할지 결정하고, <code>secret_filter</code>는 어떤 Service Account/Secret을 대상으로 수집할지 결정한다.</p>
<hr>
<h2 id="3-구성-순서">3. 구성 순서</h2>
<p>이번 실습에서 성공한 전체 순서는 다음과 같다.</p>
<pre><code class="language-text">1. SCP Provider 생성
2. SCP Secret Schema 생성
3. Docker Hub에 Custom Collector 이미지 push
4. Repository Plugin 등록
5. SCP Service Account 생성
6. SCP Secret 생성
7. Collector 생성
8. Collect Data 실행
9. Job / JobTask 확인
10. CloudService 수집 결과 확인</code></pre>
<p>이 순서 중 하나라도 빠지면 수집이 실패한다.</p>
<p>특히 중요한 순서는 다음이다.</p>
<pre><code class="language-text">Service Account 생성 후 반드시 Secret을 생성해야 함
Collector 생성 시 Secret Filter에 Service Account를 연결해야 함
Collector Plugin 버전은 CloudForet가 인식하는 버전이어야 함</code></pre>
<hr>
<h2 id="4-custom-collector-코드에서-가장-중요했던-부분">4. Custom Collector 코드에서 가장 중요했던 부분</h2>
<p>CloudForet Inventory Collector는 단순히 데이터를 반환하는 것이 아니라, CloudForet가 이해할 수 있는 응답 구조로 반환해야 한다.</p>
<p>이번 실습에서 Collector가 반환해야 했던 핵심 구조는 다음이다.</p>
<pre><code class="language-python">{
    &quot;resource_type&quot;: &quot;inventory.CloudService&quot;,
    &quot;resource&quot;: resource,
    &quot;match_rules&quot;: {
        &quot;1&quot;: [&quot;reference.resource_id&quot;]
    }
}</code></pre>
<p>CloudServiceType은 다음 기준으로 매칭했다.</p>
<pre><code class="language-python">{
    &quot;resource_type&quot;: &quot;inventory.CloudServiceType&quot;,
    &quot;resource&quot;: resource,
    &quot;match_rules&quot;: {
        &quot;1&quot;: [&quot;name&quot;, &quot;provider&quot;, &quot;group&quot;]
    }
}</code></pre>
<p>핵심 포인트는 다음이다.</p>
<pre><code class="language-text">resource_type은 최상위에 있어야 함
resource 내부에만 resource_type이 있으면 인식하지 못함
match_rules가 없으면 ERROR_MATCH_RULE 발생
match_rules는 list가 아니라 dict 형태여야 함</code></pre>
<hr>
<h2 id="5-주요-트러블슈팅-정리">5. 주요 트러블슈팅 정리</h2>
<h3 id="1-job은-생성되지만-jobtask가-생성되지-않음">1) Job은 생성되지만 JobTask가 생성되지 않음</h3>
<p>원인:</p>
<pre><code class="language-text">SCP Service Account는 있었지만 연결된 Secret이 없었음</code></pre>
<p>해결:</p>
<pre><code class="language-text">secret.Secret 생성
schema: scp_openapi
service_account_id: SCP Service Account ID
project_id: CloudForet Project ID</code></pre>
<hr>
<h3 id="2-resource_type-none-오류">2) resource_type None 오류</h3>
<p>에러:</p>
<pre><code class="language-text">collector can not find resource_type: None</code></pre>
<p>원인:</p>
<pre><code class="language-text">Collector 응답에서 resource_type이 최상위가 아니라 resource 내부에만 존재했음</code></pre>
<p>해결:</p>
<pre><code class="language-text">to_primitive()에서 resource_type을 최상위 필드로 이동</code></pre>
<hr>
<h3 id="3-error_match_rule">3) ERROR_MATCH_RULE</h3>
<p>에러:</p>
<pre><code class="language-text">No match rule</code></pre>
<p>원인:</p>
<pre><code class="language-text">CloudForet가 리소스를 생성/업데이트할 기준을 찾지 못함</code></pre>
<p>해결:</p>
<pre><code class="language-python">match_rules = {
    &quot;1&quot;: [&quot;reference.resource_id&quot;]
}</code></pre>
<p>CloudServiceType의 경우:</p>
<pre><code class="language-python">match_rules = {
    &quot;1&quot;: [&quot;name&quot;, &quot;provider&quot;, &quot;group&quot;]
}</code></pre>
<hr>
<h3 id="4-match_rules-파싱-오류">4) match_rules 파싱 오류</h3>
<p>에러:</p>
<pre><code class="language-text">Failed to parse match_rules field
Struct must be in a dict</code></pre>
<p>원인:</p>
<pre><code class="language-python">match_rules = [[&quot;reference.resource_id&quot;]]</code></pre>
<p>처럼 list 형태로 반환했기 때문이다.</p>
<p>해결:</p>
<pre><code class="language-python">match_rules = {
    &quot;1&quot;: [&quot;reference.resource_id&quot;]
}</code></pre>
<p>처럼 dict 형태로 수정했다.</p>
<hr>
<h3 id="5-collector-plugin-버전-변경이-안-됨">5) Collector Plugin 버전 변경이 안 됨</h3>
<p>문제:</p>
<pre><code class="language-text">기존 Collector를 update해도 plugin_info.version이 0.1.8에서 0.1.9로 바뀌지 않음</code></pre>
<p>원인:</p>
<pre><code class="language-text">inventory.Collector update API는 plugin_info 변경을 지원하지 않음</code></pre>
<p>해결:</p>
<pre><code class="language-text">기존 Collector를 수정하는 대신 새 Collector를 생성</code></pre>
<hr>
<h3 id="6-version-does-not-exist-오류">6) version does not exist 오류</h3>
<p>에러:</p>
<pre><code class="language-text">version:0.1.10 of scp-vm-collector does not exist</code></pre>
<p>원인:</p>
<pre><code class="language-text">Docker Hub에는 0.1.10 이미지를 push했지만 CloudForet Repository Plugin 버전으로는 등록되지 않았음</code></pre>
<p>해결:</p>
<pre><code class="language-text">CloudForet가 이미 알고 있는 0.1.9 태그를 재빌드/재푸시하여 사용</code></pre>
<hr>
<h3 id="7-error_grpc_connection">7) ERROR_GRPC_CONNECTION</h3>
<p>에러:</p>
<pre><code class="language-text">Channel is not ready
Server is unavailable</code></pre>
<p>원인:</p>
<pre><code class="language-text">동적 Collector Pod가 Ready 되기 전에 Job이 수집을 시도하거나,
Service는 존재하지만 Endpoints가 비어 있었음</code></pre>
<p>해결:</p>
<pre><code class="language-text">Service selector에 맞는 수동 Deployment를 생성하여 Endpoint를 강제로 유지</code></pre>
<p>확인 포인트:</p>
<pre><code class="language-bash">kubectl get endpoints -n spaceone-plugin scp-vm-collector-wjdnuksxfqujxcoe -o wide</code></pre>
<p>정상 예시:</p>
<pre><code class="language-text">10.42.0.45:50051</code></pre>
<hr>
<h2 id="6-운영-관점에서-배운-핵심">6. 운영 관점에서 배운 핵심</h2>
<p>이번 실습의 핵심은 Collector 코드보다 CloudForet 내부 수집 흐름을 이해한 것이다.</p>
<p>CloudForet Inventory 수집은 다음 순서로 동작한다.</p>
<pre><code class="language-text">Collector 실행
→ 연결된 Service Account 조회
→ 연결된 Secret 조회
→ Plugin Pod 실행
→ gRPC로 Collector Plugin 호출
→ collect() 결과 수신
→ resource_type 확인
→ match_rules 기준으로 기존 리소스 매칭
→ CloudService 생성 또는 업데이트</code></pre>
<p>따라서 장애를 볼 때는 다음 순서로 확인해야 한다.</p>
<pre><code class="language-text">1. Collector가 올바른 plugin version을 보고 있는가
2. Service Account가 연결되어 있는가
3. Secret이 존재하는가
4. Plugin Pod가 뜨는가
5. Service Endpoint가 있는가
6. gRPC 연결이 되는가
7. collect 응답에 resource_type이 있는가
8. match_rules가 올바른 dict 구조인가
9. CloudService 필드 구조가 CloudForet 스키마와 맞는가</code></pre>
<hr>
<h2 id="7-최종-성과">7. 최종 성과</h2>
<p>최종적으로 SCP OpenAPI를 통해 조회한 Virtual Server 정보를 CloudForet Inventory 리소스로 변환하여 수집에 성공했다.</p>
<p>수집 대상은 SCP Virtual Server이며, CloudForet에서는 다음 구조로 표현된다.</p>
<pre><code class="language-text">Provider: scp
Cloud Service Group: VirtualServer
Cloud Service Type: VM
Resource Type: inventory.CloudService</code></pre>
<p>이번 작업을 통해 CloudForet에서 기본 지원하지 않는 CSP도 Custom Provider와 Custom Collector를 통해 확장할 수 있음을 검증했다.</p>
<p>CloudForet Custom Collector 개발의 핵심은 API 호출 자체보다, CloudForet Inventory Worker가 해석할 수 있는 resource_type, resource, match_rules 구조를 정확히 맞추는 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CloudForet 활용(심화)]]></title>
            <link>https://velog.io/@sunny-10/CloudForet-%ED%99%9C%EC%9A%A9%EC%8B%AC%ED%99%94</link>
            <guid>https://velog.io/@sunny-10/CloudForet-%ED%99%9C%EC%9A%A9%EC%8B%AC%ED%99%94</guid>
            <pubDate>Fri, 12 Jun 2026 06:14:15 GMT</pubDate>
            <description><![CDATA[<p>앞서 최소사양으로 CloudForet을 구성 및 AWS와 연동
다만, 멀티 클라우드 관리에 대한 확인을 하고 싶어 심화 과정 시작</p>
<h3 id="구성">구성</h3>
<p>현재 AWS는 연동되어 있는 상태
SCP를 추가적으로 연동하여 멀티 클라우드 관리 진행</p>
<p>SCP Plugin이 없으므로 아래와 같이 진행하기로 결정</p>
<pre><code>1단계: SCP API로 VM 목록 조회하는 Python 스크립트 성공
2단계: 조회 결과를 CloudForet Inventory CloudService 포맷으로 변환
3단계: SpaceONE Collector Plugin 구조로 패키징
4단계: Docker 이미지 빌드
5단계: CloudForet에 Plugin 등록
6단계: Collector 생성 후 SCP Service Account 연결
7단계: Cloud Service 화면에 SCP VM 표시 확인</code></pre><h4 id="1단계">1단계</h4>
<p>SCP API로 VM 목록 조회하는 Python 스크립트 성공</p>
<pre><code>===============================================================================
🔍 SCP Virtual Server 목록 조회 시작
================================================================================
스크립트 위치: c:\Users\user\Desktop\MZC\Z-mzc\OpenAPI 작업
인증정보 파일: c:\Users\user\Desktop\MZC\Z-mzc\OpenAPI 작업\scp_credentials.json
결과 저장 파일: c:\Users\user\Desktop\MZC\Z-mzc\OpenAPI 작업\scp_vm_list.parsed.json

================================================================================
⚙️ 설정 요약
================================================================================
OpenAPI Endpoint: https://openapi.samsungsdscloud.com
Project ID: PROJECT-
Client Type: OpenApi
Resource Type: PROJECT
Language: en-US
SSL Verify: False
Access Key: sBAM****

📄 페이지 조회 중: page=0, size=100
   HTTP Status Code: 200
   전체 VM 개수: 5
   현재까지 조회된 VM 수: 5

================================================================================
✅ SCP VM 조회 완료: 5개
================================================================================
[1] name=bastion-001, id=INSTANCE-H0n--iGlIgfhed, state=STOPPED, private_ip=192.168, image_id=IMAGE-TYn75EWMtgcTM7qZBaL7-a, service=Virtual Server, created_at=2026-05-28T11:23:17.263794+09:00
[2] name=backup-02-001, id=INSTANCE-TsS62YOMYpJVTyd, state=STOPPED, private_ip=192.168, image_id=IMAGE-k4Vwue-usTcVS9tp0chgGa, service=Virtual Server, created_at=2026-05-28T11:21:47.861308+09:00
[3] name=backup-01-001, id=INSTANCE-_L-p8BkBiZSgZuYCm, state=STOPPED, private_ip=192.168, image_id=IMAGE-TYn75EWMtgcTM7qZBaL7-a, service=Virtual Server, created_at=2026-05-28T11:19:48.349298+09:00
[4] name=vm-pms-test, id=INSTANCE-4rNcjYmNs7dji4Amd, state=RUNNING, private_ip=192.168, image_id=IMAGE-25Z0imVfs2mMGxiBMnKmop, service=Virtual Server, created_at=2026-04-20T14:51:47.746695+09:00
[5] name=test-001, id=INSTANCE-7W1BHSAZtvfH15gj, state=STOPPED, private_ip=192.168, image_id=IMAGE-25Z0imVfs2mMGxiBMnKmop, service=Virtual Server, created_at=2026-03-06T13:45:30.489579+09:00

================================================================================
💾 파일 저장 완료
================================================================================
파일 경로: c:\Users\user\Desktop\MZC\Z-mzc\OpenAPI 작업\scp_vm_list.parsed.json
파일 크기: 5,399 bytes

================================================================================
✅ 작업 완료
================================================================================
PS C:\Users\user\Desktop\MZC\Z-mzc\OpenAPI 작업&gt; 
</code></pre><h4 id="2단계">2단계</h4>
<p>SCP Collector Plugin 생성</p>
<ul>
<li>예상 구조<pre><code>scp-vm-inven-collector/
├── Dockerfile
├── requirements.txt
├── setup.py
└── src/
  └── spaceone/
      └── inventory/
          └── plugin/
              └── collector/
                  ├── service.py
                  ├── connector/
                  │   └── scp_connector.py
                  └── manager/
                      └── vm_manager.py</code></pre></li>
<li>현재 구조<pre><code>src/spaceone/inventory/conf/global_conf.py
src/spaceone/inventory/conf/__init__.py
src/spaceone/inventory/connector/__init__.py
src/spaceone/inventory/connector/__pycache__/__init__.cpython-311.pyc
src/spaceone/inventory/connector/__pycache__/scp_connector.cpython-311.pyc
src/spaceone/inventory/connector/scp_connector.py
src/spaceone/inventory/__init__.py
src/spaceone/inventory/interface/grpc/collector.py
src/spaceone/inventory/interface/grpc/__init__.py
src/spaceone/inventory/interface/grpc/__pycache__/collector.cpython-311.pyc
src/spaceone/inventory/interface/grpc/__pycache__/__init__.cpython-311.pyc
src/spaceone/inventory/interface/__init__.py
src/spaceone/inventory/manager/__init__.py
src/spaceone/inventory/manager/__pycache__/vm_manager.cpython-311.pyc
src/spaceone/inventory/manager/vm_manager.py
src/spaceone/inventory/__pycache__/__init__.cpython-311.pyc
src/spaceone/inventory/service/collector_service.py
src/spaceone/inventory/service/__init__.py
src/spaceone/inventory/service/__pycache__/collector_service.cpython-311.pyc
src/spaceone/inventory/service/__pycache__/__init__.cpython-311.pyc</code></pre></li>
</ul>
<h4 id="3단계">3단계</h4>
<p>spacectl를 이용한 SCP Provider 등록</p>
<ul>
<li>사전에 SCP Provider에서 사용할 Service Account 생성 필요</li>
</ul>
<h5 id="trouble-shooting">Trouble Shooting</h5>
<p>콘솔에서 SCP용 Service Account 생성 시
Add 버튼 비활성으로 SA 생성 실패 → spacectl을 이용하여 생성</p>
<pre><code>kubectl exec -i -n spaceone spacectl -- sh -c &#39;cat &gt; /tmp/scp_service_account.yaml&#39; &lt;&lt;EOF
---
name: oliver_scp
provider: scp
project_id: ${CF_PROJECT_ID}
service_account_type: GENERAL
data:
  access_key: &quot;${SCP_ACCESS_KEY}&quot;
  access_secret_key: &quot;${SCP_SECRET_KEY}&quot;
  project_id: &quot;${SCP_PROJECT_ID}&quot;
  client_type: &quot;OpenApi&quot;
  language: &quot;en-US&quot;
  resource_type: &quot;PROJECT&quot;
  base_endpoint: &quot;https://openapi.samsungsdscloud.com&quot;
  region_code: &quot;scp-commercial&quot;
  ssl_verify: false
tags: {}
EOF

kubectl exec -n spaceone spacectl -- \
  spacectl exec create identity.ServiceAccount -f /tmp/scp_service_account.yaml</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/78b62fd0-8ee7-4374-b4d3-5129cbf4ca30/image.png" alt=""></p>
<p>콘솔에서 SCP용 Collector 생성 시
Collector 생성 실패 → spacectl을 이용하여 생성</p>
<pre><code>kubectl exec -i -n spaceone spacectl -- sh -c &#39;cat &gt; /tmp/scp_collector.yaml&#39; &lt;&lt;&#39;EOF&#39;
---
name: SCP VM Collector
provider: scp
plugin_info:
  plugin_id: scp-vm-collector
  version: &quot;0.1.8&quot;
  upgrade_mode: MANUAL
schedule:
  state: DISABLED
secret_filter:
  state: ENABLED
  service_accounts:
    - sa-d88be3973e9a
tags:
  description: SCP VM Collector created by CLI
EOF

kubectl exec -n spaceone spacectl -- \
  spacectl exec create inventory.Collector -f /tmp/scp_collector.yaml</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/90fe969f-f950-4e98-89db-1d8115f3e23c/image.png" alt=""></p>
<h3 id="결과">결과</h3>
<p>기존에 없던 삼성클라우드플랫폼(SCP) Provider 생성 후
AWS와 SCP의 멀티클라우드 관리 가능 확인</p>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/0c663b9e-93fc-4458-8da4-a569293fcc6c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/3b1e4d61-a073-4e76-a1c8-71409fa8bead/image.png" alt=""></p>
<p><strong>자세한 과정은 다음 게시글에서 핵심 정리</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CloudForet 활용 (최소 사양)]]></title>
            <link>https://velog.io/@sunny-10/Cloud-Foret-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@sunny-10/Cloud-Foret-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Mon, 08 Jun 2026 08:16:37 GMT</pubDate>
            <description><![CDATA[<h2 id="cloudforet-란">CloudForet 란</h2>
<p>CloudForet는 한마디로 말하면 오픈소스 멀티클라우드 관리 플랫폼(CMP, Cloud Management Platform)</p>
<p>예전에는 SpaceONE이라는 이름으로 시작되었고, 현재는 CloudForet라는 이름으로 발전했으며, Linux Foundation 프로젝트로 운영됨
AWS, Azure, GCP, OpenStack, Kubernetes 등 여러 클라우드 환경을 하나의 화면에서 통합 관리할 수 있도록 해줌</p>
<h2 id="전체-구성">전체 구성</h2>
<p>현재 서버는 아래와 같이 구성</p>
<ul>
<li>1core / 2Memory / 100GB / Rocky 8.10 / 1대
최소 구동 확인용으로 시작</li>
</ul>
<p>다만, 1C / 2GB는 공식 최소 사양보다 낮기 때문에 “CloudForet 콘솔 접속 + 리소스 몇 개 조회 테스트” 목적
공식 개발용 권장은 2Core / 8GB / 30GB Disk, 최소 설치 예제도 3GB 메모리 이상을 전제로 함</p>
<pre><code>Rocky 8.10 VM
 ├─ Swap 4GB
 ├─ k3s
 ├─ kubectl
 ├─ helm
 └─ CloudForet
     ├─ console
     ├─ inventory
     ├─ identity
     ├─ mongodb
     └─ redis</code></pre><h3 id="사전-설정">사전 설정</h3>
<p>Swap 4GB 추가 / k3s 설치 / helm 설치 / CloudForet Helm Repo 추가 / Namespace 생성 / RBAC 적용 </p>
<p>1) 최소 설치용 values 파일 준비
1C/2GB에서는 기본 설치하면 Pod가 너무 많이 떠서 거의 버거움
CloudForet 최소 설치 예제에서는 cost-analysis, notification 등을 끄고 scheduler/worker를 줄이는 방식을 사용</p>
<pre><code>wget https://raw.githubusercontent.com/cloudforet-io/charts/master/examples/values/minimum.yaml -O minimum.yaml</code></pre><pre><code>cp minimum.yaml minimum-1c2g.yaml
vi minimum-1c2g.yaml</code></pre><pre><code>global:
  shared_conf:
    LOG_LEVEL: INFO

cost-analysis:
  enabled: false

notification:
  enabled: false

marketplace-assets:
  enabled: false

plugin:
  enabled: true
  scheduler: false
  worker: false

inventory:
  enabled: true
  scheduler: false
  worker: false

monitoring:
  enabled: false

statistics:
  enabled: false</code></pre><p>2) CloudForet 설치</p>
<pre><code>helm install cloudforet cloudforet/spaceone \
  -n spaceone \
  -f minimum-1c2g.yaml</code></pre><pre><code>kubectl get pods -n spaceone -w</code></pre><p>3) 초기화 작업</p>
<pre><code>wget https://raw.githubusercontent.com/cloudforet-io/charts/master/examples/initializer.yaml -O initializer.yaml

helm install cloudforet-initializer cloudforet/spaceone-initializer \
  -n spaceone \
  -f initializer.yaml</code></pre><pre><code>kubectl get pods -n spaceone | grep initialize</code></pre><pre><code>kubectl logs -n spaceone $(kubectl get pod -n spaceone | grep initialize | awk &#39;{print $1}&#39;)</code></pre><p>로그 끝 부분 확인 시 &#39;FINISH SPACEONE INITIALIZE&#39; 문구 확인</p>
<h3 id="콘솔-접속">콘솔 접속</h3>
<p>폐쇄망 환경으로 SSH Port Forwarding 진행</p>
<ul>
<li><p>VM 서버</p>
<pre><code>nohup kubectl port-forward -n spaceone svc/console 8080:80 &gt;/tmp/console.log 2&gt;&amp;1 &amp;
nohup kubectl port-forward -n spaceone svc/console-api 8081:80 &gt;/tmp/console-api.log 2&gt;&amp;1 &amp;
nohup kubectl port-forward -n spaceone svc/console-api-v2-rest 8082:80 &gt;/tmp/console-api-v2.log 2&gt;&amp;1 &amp;</code></pre></li>
<li><p>접속 PC 터미널</p>
<pre><code>ssh -i pemkey -L 8080:127.0.0.1:8080 -L 8081:127.0.0.1:8081  -L 8082:127.0.0.1:8082 vmuser@VMIP</code></pre></li>
<li><p>콘솔 접속
도메인  : root
초기 ID : admin
초기 PW : Admin123!@#</p>
</li>
</ul>
<p>최소 사양으로 구성하여 서비스 접속 및 로그인에 초점을 둠
<img src="https://velog.velcdn.com/images/sunny-10/post/85f9491f-d638-413d-80aa-4b11a1a7fe40/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/deff2ef6-8816-4207-baf1-2efe0ba714cc/image.png" alt=""></p>
<h3 id="구성-확장">구성 확장</h3>
<p>minimum.yaml 수정
플러그인 및 인벤토리 worker 활성화 &gt; helm update -f </p>
<pre><code>plugin:
  enabled: true
  scheduler: false
  worker: true

inventory:
  enabled: true
  scheduler: false
  worker: true</code></pre><p>1) 유저 추가
root 도메인으로 admin 접속
[IAM] +add 통한 유저 추가
초기 ID : 설정
초기 PW : 설정
<img src="https://velog.velcdn.com/images/sunny-10/post/05fc4f19-d46c-41e0-8e84-a3532efa999b/image.png" alt=""></p>
<p>2) 신규 유저 계정으로 접속
도메인 변경 (root 도메인X)
<img src="https://velog.velcdn.com/images/sunny-10/post/b1fbd756-e026-4684-a8f2-5c12aedef010/image.png" alt=""></p>
<p>3) 현재 상황
✅ Root Domain 생성
✅ User Domain(spaceone) 생성
✅ Managed Repository 생성
✅ Domain Admin User 생성
✅ Role / Policy 생성
✅ Console ↔ Repository 연동 성공
✅ Dashboard 메뉴 활성화
✅ Asset Inventory 메뉴 활성화
✅ Cost Explorer 메뉴 활성화
✅ Alert Manager 메뉴 활성화</p>
<p>4) Service Account 생성
<img src="https://velog.velcdn.com/images/sunny-10/post/05973f21-72ad-414b-9144-f8f7a2260c15/image.png" alt=""></p>
<p>5) Collector 생성
<img src="https://velog.velcdn.com/images/sunny-10/post/fc669766-2c7d-4d6d-a564-c706231483c6/image.png" alt=""></p>
<p>6) Collector Data 확인
AWS에서 신규 생성한 볼륨을 SpaceOne에서 <strong>확인 완료 (연동)</strong>
<img src="https://velog.velcdn.com/images/sunny-10/post/cf46e54e-1fff-4692-ad04-82701b5d80e7/image.png" alt=""></p>
<h2 id="trouble-shooting">Trouble Shooting</h2>
<h3 id="1-github-다운로드-실패">1) Github 다운로드 실패</h3>
<pre><code>curl -sfL https://get.k3s.io | sh - 
[INFO] Finding release for channel stable 
[INFO] Using v1.35.5+k3s1 as release 
[INFO] Downloading hash https://github.com/k3s-io/k3s/releases/download/v1.35.5%2Bk3s1/sha256sum-amd64.txt [ERROR] Download failed</code></pre><p>확인 사항</p>
<pre><code># 1. DNS 확인
nslookup github.com

# 2. GitHub HTTPS 접근 확인
curl -Iv https://github.com

# 3. 실제 실패한 파일 직접 확인
curl -IvL &quot;https://github.com/k3s-io/k3s/releases/download/v1.35.5%2Bk3s1/sha256sum-amd64.txt&quot;

# 4. 프록시 환경변수 확인
env | grep -i proxy</code></pre><p>확인 결과
DNS 정상 / TCP 443 정상 / TLS 인증서 정상 / GitHub 응답은 오는데 HTTP/2 504 발생</p>
<p>아래 명령어를 통해 해결</p>
<pre><code>curl -4 --http1.1 -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.32.6+k3s1 sh -</code></pre><p>원인</p>
<pre><code>GitHub 접속 자체는 가능
↓
HTTP/2 환경에서 GitHub Release 다운로드 실패 (504)
↓
k3s 최신버전(v1.35.5) 다운로드 실패
↓
구버전(v1.32.6) + HTTP/1.1 경로로 우회 성공</code></pre><h3 id="2-github-다운로드-실패2">2) Github 다운로드 실패2</h3>
<pre><code>Error: INSTALLATION FAILED: failed to fetch https://github.com/cloudforet-io/charts/releases/download/spaceone-1.12.15/spaceone-1.12.15.tgz : 504 Gateway Timeout</code></pre><p>로컬에서 수동으로 다운로드 → SCP를 통해 VM 업로드하여 해결</p>
<h3 id="3-404-에러">3) 404 에러</h3>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/fe431f90-0b87-4533-a9e8-d2c903632aa2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/f430225d-7610-40a7-826f-939a7c9e27f5/image.png" alt=""></p>
<h4 id="3-1-증상">3-1) 증상</h4>
<p>CloudForet Console 접속 시 로그인 화면이 아니라 아래 화면으로 이동함.</p>
<p><a href="http://localhost:8080/error-page">http://localhost:8080/error-page</a>
404
Sorry, we can’t seem to find what you’re looking for.</p>
<p>처음에는 Console 정적 페이지는 뜨지만, 내부 API 호출 실패로 인해 /error-page로 이동하는 상태</p>
<h4 id="3-2-실제-원인">3-2) 실제 원인</h4>
<p>원인 1.</p>
<pre><code>본체: cloudforet/*:2.0.dev*
initializer: cloudforet/spacectl:1.12.1

cloudforet/spacectl:1.12.1  ← 존재
cloudforet/spacectl:1.12.12 ← 없음
cloudforet/spacectl:1.12.13 ← 없음
cloudforet/spacectl:1.12.15 ← 없음
cloudforet/spacectl:latest  ← 없음</code></pre><p>spaceone-1.12.15 chart를 사용했을 때는 본체 이미지가 2.0.dev 계열로 배포되어 spacectl:1.12.1과 API 버전이 맞지 않음</p>
<p>No module named &#39;spaceone.api.identity.v2&#39;</p>
<p>그래서 본체 chart를 1.12.12로 낮춰서 spacectl:1.12.1과 호환되도록 맞췄음</p>
<p>원인 2.
Console API Endpoint가 예제값으로 남아 있었음
console-conf ConfigMap 안의 production.json이 아래처럼 되어 있었음</p>
<pre><code>&quot;CONSOLE_API&quot;: {
  &quot;ENDPOINT&quot;: &quot;https://console.api.example.com&quot;
},
&quot;CONSOLE_API_V2&quot;: {
  &quot;ENDPOINT&quot;: &quot;https://console-v2.api.example.com&quot;
}</code></pre><p>즉 브라우저가 실제 VM의 API가 아니라 존재하지 않는 예제 주소로 요청
그래서 Console 화면은 뜨지만 API 호출이 실패했고, 결과적으로 404 페이지로 이동</p>
<p>원인 3. 
로컬 브라우저 기준 8081, 8082 터널링이 빠졌음</p>
<p>Console 설정을 아래처럼 변경</p>
<pre><code>&quot;CONSOLE_API&quot;: {
  &quot;ENDPOINT&quot;: &quot;http://localhost:8081&quot;
},
&quot;CONSOLE_API_V2&quot;: {
  &quot;ENDPOINT&quot;: &quot;http://localhost:8082&quot;
}</code></pre><p>그런데 브라우저에서 보는 localhost는 VM의 localhost가 아니라 내 PC의 localhost이므로 VM에서만 포트포워딩을 해서는 부족</p>
<p>구조는 아래와 같아야 함</p>
<pre><code>내 PC 브라우저 localhost:8080
  → SSH 터널
  → VM localhost:8080
  → Kubernetes svc/console

내 PC 브라우저 localhost:8081
  → SSH 터널
  → VM localhost:8081
  → Kubernetes svc/console-api

내 PC 브라우저 localhost:8082
  → SSH 터널
  → VM localhost:8082
  → Kubernetes svc/console-api-v2-rest</code></pre><p>8080만 SSH 터널링하면 Console 정적 화면은 뜨지만, API 호출은 실패</p>
<p>원인 4.
Initializer가 제대로 완료되지 않아 초기 데이터가 불완전했음
처음 initializer 실행 시 아래 오류가 발생했음</p>
<pre><code>channel = identity:50051
Channel is not ready</code></pre><p>원인은 identity 서비스가 완전히 준비되기 전에 initializer가 실행되었기 때문</p>
<p>또 한 번은 namespace가 맞지 않아 initializer가 다른 namespace에서 identity:50051을 찾으면서 실패</p>
<pre><code>global:
  namespace: cloudforet</code></pre><p>CloudForet 본체는 spaceone namespace에 있었기 때문에 아래처럼 수정</p>
<pre><code>global:
  namespace: spaceone</code></pre><h4 id="3-3-해결-과정">3-3) 해결 과정</h4>
<p>1단계. CloudForet 본체 버전 정리
기존 1.12.15 제거 후 1.12.12로 재설치</p>
<pre><code>helm uninstall cloudforet-initializer -n spaceone
helm uninstall cloudforet -n spaceone
curl -4 --http1.1 -L -o spaceone-1.12.12.tgz \
https://github.com/cloudforet-io/charts/releases/download/spaceone-1.12.12/spaceone-1.12.12.tgz
helm install cloudforet ./spaceone-1.12.12.tgz \
  -n spaceone \
  -f minimum-1c2g.yaml</code></pre><p>2단계. Initializer namespace 수정
initializer.yaml을 수정</p>
<pre><code>global:
  namespace: spaceone

설치:
helm install cloudforet-initializer cloudforet/spaceone-initializer \
  -n spaceone \
  -f initializer.yaml</code></pre><p>3단계. Console API endpoint 수정
console-conf의 production.json을 수정</p>
<pre><code>kubectl patch cm console-conf -n spaceone --type merge -p \
&#39;{&quot;data&quot;:{&quot;production.json&quot;:&quot;{\&quot;AMCHARTS_LICENSE\&quot;:{\&quot;CHARTS\&quot;:\&quot;\&quot;,\&quot;MAPS\&quot;:\&quot;\&quot;,\&quot;TIMELINE\&quot;:\&quot;\&quot;},\&quot;ASSET_PATH\&quot;:{},\&quot;CONSOLE_API\&quot;:{\&quot;ENDPOINT\&quot;:\&quot;http://localhost:8081\&quot;},\&quot;CONSOLE_API_V2\&quot;:{\&quot;ENDPOINT\&quot;:\&quot;http://localhost:8082\&quot;},\&quot;DOMAIN_IMAGE\&quot;:{},\&quot;DOMAIN_NAME\&quot;:\&quot;spaceone\&quot;,\&quot;DOMAIN_NAME_REF\&quot;:\&quot;config\&quot;,\&quot;GTAG_ID\&quot;:\&quot;DISABLED\&quot;,\&quot;GTM_ID\&quot;:\&quot;DISABLED\&quot;,\&quot;MOCK\&quot;:{\&quot;ALL\&quot;:false,\&quot;ENDPOINT\&quot;:\&quot;\&quot;}}&quot;}}&#39;

Console Pod 재시작:
kubectl delete pod -n spaceone -l app.kubernetes.io/name=console

확인:
kubectl exec -n spaceone deploy/console -- cat /var/www/config/production.json
</code></pre><p>4단계. VM 내부 Kubernetes 포트포워딩</p>
<p>VM에서 실행:</p>
<pre><code>kubectl port-forward -n spaceone svc/console 8080:80
kubectl port-forward -n spaceone svc/console-api 8081:80
kubectl port-forward -n spaceone svc/console-api-v2-rest 8082:80</code></pre><p>백그라운드 실행 버전:</p>
<pre><code>nohup kubectl port-forward -n spaceone svc/console 8080:80 &gt;/tmp/console.log 2&gt;&amp;1 &amp;
nohup kubectl port-forward -n spaceone svc/console-api 8081:80 &gt;/tmp/console-api.log 2&gt;&amp;1 &amp;
nohup kubectl port-forward -n spaceone svc/console-api-v2-rest 8082:80 &gt;/tmp/console-api-v2.log 2&gt;&amp;1 &amp;</code></pre><p>5단계. 로컬 PC에서 SSH 터널링</p>
<p>로컬 PC에서 실행:</p>
<pre><code>ssh -i pms-test2.pem \
  -L 8080:127.0.0.1:8080 \
  -L 8081:127.0.0.1:8081 \
  -L 8082:127.0.0.1:8082 \
  vmuser@123.37.28.73</code></pre><p>콘솔 접속 : <a href="http://localhost:8080/sign-in">http://localhost:8080/sign-in</a></p>
<h4 id="3-4-확인했던-주요-증거">3-4) 확인했던 주요 증거</h4>
<p>Console 정적 파일은 정상
GET / 200
GET /assets/index...js 200
GET /config/production.json 200</p>
<p>즉 nginx나 console Pod 자체 문제는 아니었음</p>
<p>API endpoint가 example.com으로 되어 있었음
&quot;CONSOLE_API&quot;:{&quot;ENDPOINT&quot;:&quot;<a href="https://console.api.example.com&quot;%7D">https://console.api.example.com&quot;}</a>
/reflection 요청이 실패했음</p>
<p>브라우저 DevTools에서 확인:
Request URL: <a href="http://localhost:8081/api/reflection">http://localhost:8081/api/reflection</a></p>
<p>Console API 로그:
Token is not set. (Headers.authorization: Bearer <token>)</p>
<p>이 자체는 로그인 전이면 발생 가능하지만, 로그인 화면으로 정상 진입하지 못하는 상태에서는 API endpoint/초기화 문제 확인 포인트가 되었음</p>
<p>Initializer 실패 로그
ERROR_GRPC_CONNECTION
channel = identity:50051
Channel is not ready</p>
<p>그리고 버전 불일치 시:
No module named &#39;spaceone.api.identity.v2&#39;</p>
<h4 id="3-5-최종-결론">3-5) 최종 결론</h4>
<p>이번 404 에러의 핵심 원인은 다음과 같다.</p>
<ol>
<li>Console 설정의 API endpoint가 example.com으로 남아 있었음</li>
<li>로컬 브라우저 기준 8081/8082 SSH 터널링이 빠져 있었음</li>
<li>Initializer가 identity 준비 전 실행되거나 namespace/version 불일치로 초기화가 불완전했음</li>
</ol>
<p>최종 해결은 다음 조합으로 완료함</p>
<pre><code>CloudForet chart: 1.12.12
Initializer namespace: spaceone
Console API endpoint: localhost:8081 / localhost:8082
VM port-forward: 8080, 8081, 8082
Local SSH tunnel: 8080, 8081, 8082
Browser cache/site data 초기화 후 /sign-in 접속</code></pre><h3 id="collector-생성-시-에러">Collector 생성 시 에러</h3>
<p>Collector 신규 생성 시 
Service Account 선택(oliver) 후 
Additional Options Reload 발생
<img src="https://velog.velcdn.com/images/sunny-10/post/bbb12a9f-bcf2-4e19-874e-4d0b57765a1a/image.png" alt=""></p>
<p>원인
AWS Collector 플러그인을 실행할 Supervisor가 등록되어 있지 않아 발생</p>
<p>→ TOKEN 오류
   spaceone 서비스 계정에서 토큰 생성하여 conf 파일에 추가, 업데이트
→ RBAC 403 오류</p>
<ol>
<li>supervisor가 바라보는 spaceone-plugin 네임스페이스 생성</li>
<li>spaceone-plugin 네임스페이스에 Role 생성</li>
<li>spaceone 네임스페이스의 default ServiceAccount를 그 Role에 RoleBinding</li>
</ol>
<pre><code>문제:
supervisor-scheduler는 플러그인을 만들려고 했지만
spaceone-plugin 네임스페이스 접근 권한이 없었음

해결:
spaceone:default ServiceAccount에
spaceone-plugin 네임스페이스 조작 권한을 RoleBinding으로 부여

결과:
AWS Collector 플러그인 Pod 4개 생성 성공</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[SSH Local Port Forwarding]]></title>
            <link>https://velog.io/@sunny-10/SSH-Local-Port-Forwarding</link>
            <guid>https://velog.io/@sunny-10/SSH-Local-Port-Forwarding</guid>
            <pubDate>Mon, 08 Jun 2026 05:31:14 GMT</pubDate>
            <description><![CDATA[<h3 id="환경-제약">환경 제약</h3>
<ol>
<li>Kubernetes 내부 Service는 외부에서 직접 접근 불가</li>
<li>Worker Node가 Private 성격</li>
<li>보안그룹/방화벽에서 일부 포트만 허용</li>
<li>Grafana/ArgoCD/Kiali는 ClusterIP 또는 내부 서비스</li>
<li>내 PC는 Kubernetes 내부망에 직접 붙어있지 않음</li>
</ol>
<h3 id="ssh-터널링--port-forwarding-활용">SSH 터널링 / Port Forwarding 활용</h3>
<p>실습 환경에서는 Kubernetes 내부 서비스가 외부에 직접 노출되어 있지 않았기 때문에,
Grafana, ArgoCD, Kiali, Prometheus와 같은 관리 UI에 직접 접근할 수 없었음</p>
<p>이를 해결하기 위해 kubectl port-forward와 SSH Local Port Forwarding을 함께 사용</p>
<p>예를 들어 Grafana의 경우 Kubernetes 내부 Service를 로컬 포트로 포워딩</p>
<pre><code class="language-bash">kubectl port-forward -n monitoring svc/monitoring-grafana 3000:80</code></pre>
<p>이후 내 PC에서 직접 접근이 어려운 경우 SSH 터널링을 사용하여 서버의 localhost 포트를 내 PC로 가져옴</p>
<pre><code>ssh -L 3000:localhost:3000 root@&lt;서버공인IP&gt;</code></pre><p>이 구조를 통해 외부에 서비스를 직접 노출하지 않고도 안전하게 내부 관리 UI에 접근할 수 있었음</p>
<p>불필요하게 NodePort나 LoadBalancer를 열지 않아도 된다는 장점이 있음</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes 스킬업 실습 과정]]></title>
            <link>https://velog.io/@sunny-10/Kubernetes-%EC%8A%A4%ED%82%AC%EC%97%85-%EC%8B%A4%EC%8A%B5-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@sunny-10/Kubernetes-%EC%8A%A4%ED%82%AC%EC%97%85-%EC%8B%A4%EC%8A%B5-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Fri, 05 Jun 2026 09:19:25 GMT</pubDate>
            <description><![CDATA[<h1 id="kubernetes-스킬업-실습-전체-과정-정리">Kubernetes 스킬업 실습 전체 과정 정리</h1>
<h2 id="0-실습-환경">0. 실습 환경</h2>
<ul>
<li><p>실습 목적:
Kubernetes를 운영 엔지니어 관점에서 직접 구축하고 장애까지 경험하는 것</p>
</li>
<li><p>서버:
vm-pms-test  - Control Plane - 192.168.1.254
vm-pms-test2 - Worker Node   - 192.168.1.206
(테스트 환경 이슈로 Worker Node는 1대로 수행)</p>
</li>
<li><p>OS:
Ubuntu 24.04</p>
</li>
<li><p>Runtime:
containerd</p>
</li>
<li><p>CNI:
Calico</p>
</li>
<li><p>특징:
폐쇄망/제한망 유사 환경
Proxy 경유
Security Group 영향
Ingress, Monitoring, GitOps, Istio까지 확장</p>
</li>
<li><p>핵심 학습:
Kubernetes 장애는 YAML만 보는 게 아니라
노드, 네트워크, 프록시, 방화벽, 런타임까지 함께 봐야 함</p>
</li>
</ul>
<h2 id="1-kubernetes-기본-클러스터-구성">1. Kubernetes 기본 클러스터 구성</h2>
<p>실습 목표</p>
<ul>
<li>kubeadm 기반 클러스터 구축</li>
<li>Master/Worker 구조 이해</li>
<li>Kubernetes 핵심 컴포넌트 이해</li>
</ul>
<p>구성
Master : vm-pms-test (192.168.1.254)
Worker : vm-pms-test2 (192.168.1.206)</p>
<p>설치 요소</p>
<ul>
<li>containerd</li>
<li>kubeadm</li>
<li>kubelet</li>
<li>kubectl</li>
</ul>
<p>필수 설정</p>
<ul>
<li>swapoff</li>
<li>br_netfilter</li>
<li>ip_forward</li>
</ul>
<p>핵심 명령</p>
<ul>
<li>kubeadm init</li>
<li>kubeadm join</li>
<li>kubectl get nodes</li>
<li>kubectl get pods -A</li>
</ul>
<p>결과</p>
<ul>
<li>클러스터 생성 완료</li>
<li>Worker Join 완료</li>
<li><strong>아직 CNI 미설치 상태</strong></li>
<li>Node 상태 NotReady</li>
</ul>
<p>핵심 학습</p>
<ul>
<li>Kubernetes 설치는 끝이 아니라 시작</li>
</ul>
<h2 id="2-service--busybox-통신-테스트">2. Service + Busybox 통신 테스트</h2>
<p>실습 목표</p>
<ul>
<li>Pod 네트워크 이해</li>
<li>Service 이해</li>
<li>DNS 이해</li>
<li>Pod 간 통신 확인</li>
</ul>
<p>수행 내용</p>
<ol>
<li>Calico 설치</li>
<li>Node Ready 확인</li>
<li>nginx Deployment 생성</li>
<li>ClusterIP Service 생성</li>
<li>Busybox 생성</li>
<li>nslookup 테스트</li>
<li>HTTP 호출 테스트</li>
</ol>
<p>주요 컴포넌트</p>
<ul>
<li>Calico</li>
<li>CoreDNS</li>
<li>Service</li>
<li>Deployment</li>
<li>Busybox</li>
</ul>
<h3 id="트러블슈팅">트러블슈팅</h3>
<p>1) Pod가 떠도 통신이 안됨
확인 (Calico 원인 가능)</p>
<pre><code>kubectl get pods -o wide</code></pre><p>2) Worker 통신 실패
확인 (0이면 1로 변경)</p>
<pre><code>sysctl net.ipv4.ip_forward</code></pre><p>3) iptables 문제
확인 (FORWARD DROP 상태면 통신 불가)</p>
<pre><code>iptables -L -n</code></pre><p>4) 보안그룹 문제
확인 (나는 해당 문제로 통신이 불가했었음)
통신에 필요한 포트들은 사전에 허용 및 테스트를 통하여 추가 필요</p>
<p>핵심 학습:
Kubernetes 통신의 시작은 Service와 DNS,</p>
<p>Pod가 Running이라고 끝이 아니라
실제 통신이 되는지 반드시 검증해야 함</p>
<h2 id="3-ingress-구성">3. Ingress 구성</h2>
<p>실습 목표</p>
<ul>
<li>외부 접근 구현</li>
<li>Ingress 이해</li>
<li>도메인 라우팅 이해</li>
</ul>
<p>수행 내용</p>
<ol>
<li>NodePort 이해</li>
<li>ingress-nginx 설치</li>
<li>Ingress 생성</li>
<li>hosts 설정</li>
<li>nginx.test.local 접속</li>
</ol>
<p>주요 컴포넌트</p>
<ul>
<li>Ingress Controller</li>
<li>Service</li>
<li>CoreDNS</li>
<li>nginx</li>
</ul>
<p>핵심 학습:
Service는 내부 연결
Ingress는 외부 진입점
운영 환경에서는 IP보다 도메인이 중요
사용자 → Ingress → Service → Pod 흐름 이해</p>
<h2 id="4-volume--pv--pvc-실습">4. Volume / PV / PVC 실습</h2>
<p>실습 목표</p>
<ul>
<li>데이터 영속성 이해</li>
<li>PV/PVC 이해</li>
<li>Pod 삭제 후 데이터 유지 확인</li>
</ul>
<p>수행 내용</p>
<ol>
<li>PV 생성</li>
<li>PVC 생성</li>
<li>Pod 연결</li>
<li>파일 생성</li>
<li>Pod 삭제</li>
<li>Pod 재생성</li>
<li>데이터 유지 확인</li>
</ol>
<p>주요 개념</p>
<ul>
<li>PV</li>
<li>PVC</li>
<li>Volume</li>
<li>Bound</li>
<li>Persistent Storage</li>
</ul>
<p>핵심 학습 :
Pod는 언제든 죽을 수 있음
데이터는 Pod가 아니라 Volume에 저장해야 함
Stateful 서비스 운영의 기초는 PV/PVC </p>
<h2 id="5-resource-request--limit-실습">5. Resource Request / Limit 실습</h2>
<p>실습 목표</p>
<ul>
<li>CPU/Memory Resource 이해</li>
<li>Request 이해</li>
<li>Limit 이해</li>
<li>Scheduler 동작 이해</li>
</ul>
<p>수행 내용</p>
<ol>
<li>Resource 설정 Deployment 생성</li>
<li>CPU 단위 학습</li>
<li>Memory 단위 학습</li>
<li>Request 설정</li>
<li>Limit 설정</li>
<li>QoS 개념 이해</li>
</ol>
<p>주요 개념</p>
<ul>
<li>CPU</li>
<li>Memory</li>
<li>Request</li>
<li>Limit</li>
<li>Scheduler</li>
<li>QoS</li>
</ul>
<p>핵심 학습
Request는 최소 보장
Limit은 최대 허용
Scheduler는 Request를 보고 Node를 선택함
운영자는 Pod보다 Resource를 먼저 봐야 함</p>
<h2 id="6-cpu-부하-및-metrics-server">6. CPU 부하 및 Metrics Server</h2>
<p>실습 목표</p>
<ul>
<li>Metrics Server 설치</li>
<li>CPU 사용량 확인</li>
<li>Node 사용량 확인</li>
<li>stress 테스트</li>
</ul>
<p>수행 내용</p>
<ol>
<li>Metrics Server 설치</li>
<li>Metrics API 오류 해결</li>
<li>kubectl top 사용</li>
<li>stress 설치</li>
<li>CPU 부하 생성</li>
<li>Pod 사용량 확인</li>
<li>Node 사용량 확인</li>
</ol>
<p>주요 개념</p>
<ul>
<li>Metrics Server</li>
<li>kubectl top</li>
<li>CPU Usage</li>
<li>Memory Usage</li>
<li>CPU Throttling</li>
</ul>
<h3 id="트러블슈팅-1">트러블슈팅</h3>
<ul>
<li>Metrics API not available</li>
<li>kubelet 인증서 문제</li>
<li>--kubelet-insecure-tls 적용</li>
</ul>
<p>핵심 학습
설정(Request/Limit)만으로는 부족함
운영자는 실제 사용량을 볼 수 있어야 함
Metrics Server는 Kubernetes 관측의 시작점</p>
<h2 id="7-rolling-update-실습">7. Rolling Update 실습</h2>
<p>실습 목표</p>
<ul>
<li>Rolling Update 이해</li>
<li>무중단 배포 이해</li>
<li>Rollback 이해</li>
</ul>
<p>수행 내용</p>
<ol>
<li>Deployment 확인</li>
<li>이미지 변경</li>
<li>Rollout 진행</li>
<li>Pod 변화 관찰</li>
<li>Revision 확인</li>
<li>Rollback 수행</li>
</ol>
<p>주요 개념</p>
<ul>
<li>Deployment</li>
<li>ReplicaSet</li>
<li>Rolling Update</li>
<li>Rollback</li>
<li>Revision</li>
</ul>
<p>주요 명령
kubectl rollout status
kubectl rollout history
kubectl rollout undo
kubectl get pods -w</p>
<p>핵심 학습 :
Deployment는 단순 Pod 생성기가 아님
Rolling Update는 무중단 배포의 핵심
운영자는 장애 발생 시 Rollback을 할 수 있어야 함</p>
<h2 id="8-helm-실습">8. Helm 실습</h2>
<p>실습 목표</p>
<ul>
<li>Helm 이해</li>
<li>Repository 이해</li>
<li>Chart 이해</li>
<li>Release 이해</li>
</ul>
<p>수행 내용</p>
<ol>
<li>Helm 설치</li>
<li>Helm Repository 추가</li>
<li>Chart 검색</li>
<li>Ingress-Nginx 설치</li>
<li>Helm Upgrade</li>
<li>Helm Rollback</li>
<li>Helm 삭제</li>
</ol>
<p>주요 개념</p>
<ul>
<li>Chart</li>
<li>Repository</li>
<li>Release</li>
<li>Values.yaml</li>
<li>Upgrade</li>
<li>Rollback</li>
</ul>
<p>핵심 학습 :
Kubernetes의 패키지 매니저는 Helm
복잡한 애플리케이션을 한 줄로 설치
이후 Monitoring, ArgoCD, Istio의 기반이 됨</p>
<h2 id="9-monitoring--grafana--loki">9. Monitoring / Grafana / Loki</h2>
<p>실습 목표</p>
<ul>
<li>Monitoring 구축</li>
<li>Metrics 확인</li>
<li>Log 확인</li>
<li>Dashboard 활용</li>
</ul>
<p>수행 내용</p>
<ol>
<li>Prometheus 설치</li>
<li>Grafana 설치</li>
<li>Loki 설치</li>
<li>Promtail 설치</li>
<li>Dashboard 확인</li>
<li>Loki 연동</li>
<li>로그 조회</li>
</ol>
<p>주요 개념</p>
<ul>
<li>Prometheus</li>
<li>Grafana</li>
<li>Loki</li>
<li>Promtail</li>
<li>Observability</li>
</ul>
<h3 id="트러블슈팅-2">트러블슈팅</h3>
<ul>
<li>Loki Datasource 연결 실패</li>
<li>Service 이름 확인</li>
<li>Endpoint 확인</li>
<li>Grafana Datasource 수정</li>
</ul>
<p>핵심 학습 :
운영자는 서비스가 아니라 데이터를 봄
Metrics와 Logs를 함께 봐야 장애 원인을 찾을 수 있음
Grafana는 운영자의 메인 대시보드</p>
<h2 id="10-hpa-실습">10. HPA 실습</h2>
<p>실습 목표</p>
<ul>
<li>HPA 이해</li>
<li>Auto Scaling 이해</li>
<li>CPU 기반 확장 이해</li>
</ul>
<p>수행 내용</p>
<ol>
<li>HPA 생성</li>
<li>CPU 목표 설정</li>
<li>Load Generator 실행</li>
<li>CPU 증가 확인</li>
<li>Pod 자동 증가 확인</li>
<li>부하 제거</li>
<li>Pod 자동 감소 확인</li>
</ol>
<p>주요 개념</p>
<ul>
<li>HPA</li>
<li>Scale Out</li>
<li>Scale In</li>
<li>Metrics Server</li>
<li>Deployment</li>
<li>ReplicaSet</li>
</ul>
<p>주요 명령
kubectl autoscale
kubectl get hpa
kubectl describe hpa
kubectl get pods -w</p>
<p>핵심 학습 :
Kubernetes는 스스로 확장할 수 있음
HPA는 Metrics Server와 Request를 기반으로 동작함
자동 확장은 Kubernetes의 핵심 기능 중 하나</p>
<h2 id="11-gitops--argocd">11. GitOps / ArgoCD</h2>
<p>실습 목표</p>
<ul>
<li>GitOps 이해</li>
<li>ArgoCD 구축</li>
<li>GitHub 연동</li>
<li>Auto Sync</li>
<li>Self Heal</li>
</ul>
<p>수행 내용</p>
<ol>
<li>ArgoCD 설치</li>
<li>UI 접속</li>
<li>Git Repository 등록</li>
<li>Application 생성</li>
<li>Sync 수행</li>
<li>Auto Sync 활성화</li>
<li>Self Heal 확인</li>
</ol>
<p>주요 개념</p>
<ul>
<li>GitOps</li>
<li>ArgoCD</li>
<li>Application</li>
<li>Sync</li>
<li>Auto Sync</li>
<li>Self Heal</li>
</ul>
<h3 id="트러블슈팅-3">트러블슈팅</h3>
<ul>
<li>GitHub 접근 실패 / Repo Server 통신 문제
ㄴ 프록시 설정 (폐쇄망 환경에 따른 에러 발생)</li>
<li>GHCR InvalidImageName
ㄴ 이미지 경로 대소문자 구분</li>
</ul>
<p>핵심 학습 : 
Cluster를 수정하는 것이 아니라 Git을 수정함
ArgoCD는 Git 상태를 지속적으로 유지함</p>
<h2 id="12-https-ingress--tls">12. HTTPS Ingress / TLS</h2>
<p>실습 목표</p>
<ul>
<li>HTTPS 이해</li>
<li>TLS 이해</li>
<li>인증서 이해</li>
<li>Secret 활용</li>
</ul>
<p>수행 내용</p>
<ol>
<li>Self-Signed 인증서 생성</li>
<li>TLS Secret 생성</li>
<li>Ingress TLS 설정</li>
<li>HTTPS 접속</li>
<li>curl 테스트</li>
</ol>
<p>주요 개념</p>
<ul>
<li>HTTPS</li>
<li>TLS</li>
<li>Secret</li>
<li>Certificate</li>
<li>TLS Termination</li>
</ul>
<h3 id="트러블슈팅-4">트러블슈팅</h3>
<ul>
<li>Self-Signed 경고
ㄴ curl -k 옵션을 통해 확인 (-k | 인증서 검증 무시)</li>
<li>Host 불일치
ㄴ secret 재생성 후 Ingress 재적용</li>
<li>Secret 누락
ㄴ 위 방법으로 해결</li>
</ul>
<p>핵심 학습 :
실제 서비스는 HTTPS가 기본
인증서는 Secret으로 관리함
Ingress는 TLS 종료 지점이 될 수 있음</p>
<h2 id="13-cicd--jenkins--kaniko">13. CI/CD / Jenkins / Kaniko</h2>
<p>실습 목표</p>
<ul>
<li>CI/CD 구조 이해</li>
<li>Jenkins Pipeline 이해</li>
<li>Kaniko 기반 이미지 빌드 이해</li>
<li>Docker Daemon 없는 빌드 방식 이해</li>
<li>GHCR Push/Pull 구조 이해</li>
<li>ArgoCD와 역할 분리 이해</li>
</ul>
<p>수행 내용</p>
<ol>
<li>Jenkins Pipeline 구성</li>
<li>Kaniko Agent Pod 사용</li>
<li>Dockerfile 기반 이미지 빌드</li>
<li>GHCR Push</li>
<li>Kubernetes Deployment 이미지 반영</li>
<li>ArgoCD 배포 흐름과 연결</li>
</ol>
<p>주요 개념</p>
<ul>
<li>CI</li>
<li>CD</li>
<li>Jenkins</li>
<li>Jenkinsfile</li>
<li>Kaniko</li>
<li>Registry</li>
<li>GHCR</li>
<li>imagePullSecret</li>
<li>GitOps 연계</li>
</ul>
<p>핵심 학습 : 
Jenkins는 이미지를 만들고, ArgoCD는 배포
Kaniko는 Docker Daemon 없이 이미지를 빌드할 수 있음
CI/CD 장애는 Git → Build → Push → Pull → Deploy 단계로 나눠서 봐야 함</p>
<h2 id="14-istio--service-mesh">14. Istio / Service Mesh</h2>
<p>실습 목표</p>
<ul>
<li>Service Mesh 이해</li>
<li>Istio 설치</li>
<li>Sidecar 이해</li>
<li>mTLS 이해</li>
</ul>
<p>수행 내용</p>
<ol>
<li>Istio 설치</li>
<li>Namespace Label 적용</li>
<li>Sidecar Injection</li>
<li>Envoy 확인</li>
<li>서비스 호출 생성</li>
<li>Prometheus 메트릭 확인</li>
</ol>
<p>주요 개념</p>
<ul>
<li>Service Mesh</li>
<li>Istio</li>
<li>Sidecar</li>
<li>Envoy</li>
<li>mTLS</li>
<li>Observability</li>
</ul>
<h3 id="트러블슈팅-5">트러블슈팅</h3>
<ul>
<li>istio_requests_total 미수집
ㄴ kubectl label namespace / Pod 재생성</li>
<li>트래픽 부족
ㄴ curl을 통한 요청 반복 수행</li>
</ul>
<p>핵심 학습 :
서비스 간 통신도 관리 대상
Envoy가 모든 트래픽을 관찰함
mTLS는 서비스 간 통신을 보호함</p>
<h2 id="15-kiali--prometheus-연동">15. Kiali / Prometheus 연동</h2>
<p>실습 목표</p>
<ul>
<li>Kiali 이해</li>
<li>Service Graph 이해</li>
<li>Prometheus 연동</li>
</ul>
<p>수행 내용</p>
<ol>
<li>Kiali 설치</li>
<li>접속</li>
<li>Prometheus 연동 확인</li>
<li>트래픽 생성</li>
<li>Graph 확인</li>
<li>mTLS 상태 확인</li>
</ol>
<p>주요 개념</p>
<ul>
<li>Kiali</li>
<li>Service Graph</li>
<li>Edge</li>
<li>Traffic</li>
<li>Error Rate</li>
<li>Response Time</li>
</ul>
<p>핵심 학습 :
Kiali는 Istio의 눈
Prometheus 데이터를 기반으로 서비스 관계를 보여줌
운영자는 서비스 흐름을 시각적으로 확인할 수 있음</p>
<h2 id="16-장애-대응--cordon--drain--pdb">16. 장애 대응 / cordon / drain / PDB</h2>
<p>실습 목표</p>
<ul>
<li>Node 유지보수</li>
<li>Cordon</li>
<li>Drain</li>
<li>PDB</li>
<li>장애 대응</li>
</ul>
<p>수행 내용</p>
<ol>
<li>Cordon 수행</li>
<li>Drain 수행</li>
<li>Pod 재배치 확인</li>
<li>PDB 생성</li>
<li>Eviction 확인</li>
<li>Uncordon 수행</li>
</ol>
<p>주요 개념</p>
<ul>
<li>Cordon</li>
<li>Drain</li>
<li>PDB</li>
<li>Eviction</li>
<li>Availability</li>
</ul>
<p>핵심 학습 :
노드를 끄기 전에 반드시 Drain
PDB는 서비스 가용성을 보호함
Kubernetes 유지보수는 Cordon → Drain → 작업 → Uncordon 순서</p>
<h2 id="17-node--cluster-upgrade-흐름">17. Node / Cluster Upgrade 흐름</h2>
<p>실습 목표</p>
<ul>
<li>Kubernetes Upgrade 이해</li>
<li>Node Upgrade 이해</li>
<li>Rolling Update 이해</li>
</ul>
<p>수행 내용</p>
<ol>
<li>Cluster Upgrade</li>
<li>Node Pool Upgrade</li>
<li>신규 Node 생성</li>
<li>Drain 수행</li>
<li>Pod 재배치</li>
<li>기존 Node 제거</li>
<li>신규 Node 운영</li>
</ol>
<p>주요 개념</p>
<ul>
<li>Cluster Upgrade</li>
<li>Node Pool Upgrade</li>
<li>Rolling Update</li>
<li>Cordon</li>
<li>Drain</li>
<li>PDB</li>
</ul>
<p>핵심 학습 :
Control Plane과 Node Upgrade는 별개
Node Pool Version과 Node Version은 다를 수 있음
업그레이드의 목표는 버전 상승이 아니라 서비스 무중단 유지</p>
<h2 id="18-전체-트러블슈팅-정리">18. 전체 트러블슈팅 정리</h2>
<p>실습 중 경험한 대표 장애</p>
<ul>
<li>Metrics API not available</li>
<li>Ingress 접근 불가</li>
<li>Proxy 문제</li>
<li>ArgoCD Repository 연결 실패</li>
<li>InvalidImageName</li>
<li>ImagePullBackOff</li>
<li>Sidecar 미주입</li>
<li>Kiali No Data</li>
<li>TLS Secret 누락</li>
<li>Self-Signed 인증서 경고</li>
<li>PDB 위반</li>
<li>Standalone Pod Eviction 실패</li>
<li>DaemonSet Drain 실패</li>
</ul>
<p>가장 중요한 학습</p>
<ol>
<li>증상 → 원인 → 분석 → 조치 순서로 접근</li>
<li>kubectl describe 와 kubectl logs 활용</li>
<li>네트워크와 Proxy 문제 분석 능력</li>
<li>운영자는 구축보다 문제 해결 능력이 중요</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Gitea 세팅 후 계정 접근 실패]]></title>
            <link>https://velog.io/@sunny-10/-g6jo8zbi</link>
            <guid>https://velog.io/@sunny-10/-g6jo8zbi</guid>
            <pubDate>Thu, 21 May 2026 05:39:37 GMT</pubDate>
            <description><![CDATA[<p>Gitea 세팅 후 계정 접근 실패</p>
<h3 id="🔍원인">🔍원인</h3>
<p>Gitea 초기 세팅 완료 후 admin 계정으로 접근 시 비밀번호 접근 실패</p>
<ol>
<li>Gitea Pod 내부 접속하여 비밀번호 변경 시도<pre><code>kubectl exec -it &lt;gitea-pod-name&gt; -- /bin/bash
gitea admin user change-password -u admin -p &quot;새로운비밀번호&quot;
</code></pre></li>
</ol>
<p>Command error: user does not exist [uid: 0, name: admin]</p>
<pre><code>- admin 계정이 존재하지 않아 비밀번호 변경 실패

&lt;br&gt;

2. 애초 admin 계정이 생성되지 않음
&lt;br&gt;

### 📝결과
1. CLI를 통해 관리자 계정 직접 생성</code></pre><p>gitea admin user create --admin --username admin --password &quot;1234&quot; --email &quot;<a href="mailto:admin@example.com">admin@example.com</a>&quot;</p>
<pre><code>- CLI를 통해 admin 계정 생성하여 접근 성공 확인
&lt;br&gt;

2. 근본적인 에러발생 이유
values.yaml 생성 시 Password 설정 오류
```Yaml
#수정 전
admin:
  username: admin
  password: 1234

#수정 후
admin:
  username: admin
  password: &quot;1234&quot;</code></pre><ul>
<li>Gitea 초기화 스크립트의 특징 (실패 시 재시도 안 함)
Gitea Pod가 처음 뜰 때 실행되는 내부 로직은 대략 다음과 같은 순서로 동작</li>
</ul>
<p>1) 데이터베이스(SQLite) 파일 생성 및 기본 테이블 구조(스키마) 생성 ➔ (성공)</p>
<p>2) values.yaml에 적힌 정보를 바탕으로 admin 유저 생성 명령어 실행 ➔ (문자열/형식 오류로 인해 실패)</p>
<p>3) 여기서 중요한 점은 유저 생성 명령어가 실패하더라도 전체 시스템이 멈추지 않고 다음 단계(Gitea 웹 서버 실행)로 그냥 넘어감 
게다가 1번 단계에서 DB 파일이 이미 만들어졌기 때문에 Gitea는 시스템이 &#39;이미 완전히 초기화되었다&#39;고 판단
<br></p>
<ul>
<li>결론적으로 발생한 상황
DB는 만들어졌지만 관리자 계정은 없는 상태</li>
</ul>
<p>Gitea는 첫 구동 때만 이 자동 생성을 시도하므로 이후에 Pod를 껐다 켜거나 values.yaml을 수정해서 배포해도 이미 DB가 존재하기 때문에 두 번 다시 values.yaml의 admin 정보를 읽어 계정 생성을 시도하지 않음</p>
<p>비밀번호 변경 명령어를 쳤을 때 user does not exist(유저가 존재하지 않음) 에러가 났던 것이고 계정을 &#39;직접 새로 생성&#39;해 주어야만 해결.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Terraform 401 인증 에러]]></title>
            <link>https://velog.io/@sunny-10/Terraform-401-%EC%9D%B8%EC%A6%9D-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@sunny-10/Terraform-401-%EC%9D%B8%EC%A6%9D-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Thu, 21 May 2026 04:43:44 GMT</pubDate>
            <description><![CDATA[<p>Terraform 401 인증 에러 발생</p>
<p>You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.</p>
<blockquote>
<p>Error: Failed to get endpoint list
  with provider[&quot;registry.terraform.io/samsungsdscloud/samsungcloudplatformv2&quot;],
  on provider.tf line 25, in provider &quot;samsungcloudplatformv2&quot;:
  25: provider &quot;samsungcloudplatformv2&quot; {</p>
<p>Error details: {&quot;status&quot;:&quot;UNAUTHORIZED&quot;,&quot;name&quot;:&quot;Request was rejected by
gateway&quot;,&quot;message&quot;:&quot;[Security Configuration] Access key not allowed.&quot;}</p>
</blockquote>
<h3 id="🔍원인">🔍원인</h3>
<ol>
<li>API 호출 IP 접근 제어 (가장 유력함)
Terraform plan 결과에 출력된 현재 퍼블릭 IP가 SCP 콘솔의 API 접근 허용 IP 목록에 등록되어 있는지 확인</li>
</ol>
<ul>
<li>확인하였으나 허용IP 목록에 등록되어 있음</li>
</ul>
<br>

<ol start="2">
<li>Access Key 및 Secret Key 오류
provider.tf 파일이나 환경 변수에 입력한 인증 정보(Access Key, Secret Key 등)가 잘못 입력되었거나 만료되었을 수 있음</li>
</ol>
<ul>
<li>Access Key 및 Secret Key 재발급</li>
<li>provider.tf 파일에 인증정보 넣지 않고 conf 디렉토리의 config.json으로 변수 선언</li>
<li>export를 통해 직접 Access Key 및 Secret Key 설정해 보았으나 동일하게 인증 에러 발생<br>
</li>
</ul>
<ol start="3">
<li>프로젝트 및 권한 부족
해당 Access Key를 발급받은 계정에 현재 작업하려는 프로젝트(Project ID)에 대한 접근 권한이나 인프라 생성/조회 권한이 부여되지 않았을 수 있음</li>
</ol>
<ul>
<li>운영자 권한 확인<br>

</li>
</ul>
<h3 id="✅확인">✅확인</h3>
<ol>
<li>credential.json 파일 확인</li>
</ol>
<ul>
<li>credential.json 파일의 auth_url 재확인 &gt; 이상없음</li>
</ul>
<br>


<ol start="2">
<li>Terraform 상세 로그 활성화<pre><code>export TF_LOG=DEBUG</code></pre></li>
</ol>
<ul>
<li>DEBUG 로그 확인 시 API Gateway 통신은 정상, 엔드포인트 조회 문제</li>
<li>네트워크 차단 문제가 아닌 Access Key 권한 문제 확률로 확인</li>
</ul>
<br>

<h3 id="📝결과">📝결과</h3>
<p>콘솔의 인증키 보안설정 재확인</p>
<blockquote>
<p>수정 전 : 임시키 인증 | 접근 허용 IP 미사용
수정 후 : 인증키 인증 | 접근 허용 IP 미사용</p>
<p>테스트를 위해 접근 허용 IP는 미사용으로 시도</p>
</blockquote>
<p>*인증키 보안 설정이 인증키 인증이 아닌 임시키로 되어 있어 발생했던 인증 에러</p>
<br>


]]></description>
        </item>
        <item>
            <title><![CDATA[sslip.io 접근 불가]]></title>
            <link>https://velog.io/@sunny-10/sslip.io-%EC%A0%91%EA%B7%BC-%EB%B6%88%EA%B0%80</link>
            <guid>https://velog.io/@sunny-10/sslip.io-%EC%A0%91%EA%B7%BC-%EB%B6%88%EA%B0%80</guid>
            <pubDate>Tue, 19 May 2026 06:12:02 GMT</pubDate>
            <description><![CDATA[<h2 id="sslipio-접근-불가">sslip.io 접근 불가</h2>
<h3 id="🔍현상">🔍현상</h3>
<p>테스트 용도의 <a href="https://publicip.sslip.io">https://publicip.sslip.io</a> 접근 시 접근불가</p>
<br>

<h3 id="✅확인">✅확인</h3>
<ol>
<li><p>클라우드 방화벽 확인
HTTPS(443)/HTTP(80) 포트의 0.0.0.0/0 Inbound Allow 확인</p>
</li>
<li><p>sslip.io 도메인 차단 우회</p>
</li>
</ol>
<ul>
<li><a href="https://publicip">https://publicip</a> 접근 불가</li>
<li><a href="http://publicip">http://publicip</a> 접근 <strong>가능</strong> </li>
</ul>
<ol start="3">
<li>서버 내 서비스 확인
```
ubuntu@web-ide:~$ curl -vk <a href="https://localhost">https://localhost</a></li>
</ol>
<ul>
<li>Host localhost:443 was resolved.</li>
<li>IPv6: ::1</li>
<li>IPv4: 127.0.0.1</li>
<li>Trying [::1]:443...</li>
<li>connect to ::1 port 443 from ::1 port 52466 failed: Connection refused</li>
<li>Trying 127.0.0.1:443...</li>
<li>connect to 127.0.0.1 port 443 from 127.0.0.1 port 59452 failed: Connection refused</li>
<li>Failed to connect to localhost port 443 after 2 ms: Couldn&#39;t connect to server</li>
<li>Closing connection
curl: (7) Failed to connect to localhost port 443 after 2 ms: Couldn&#39;t connect to server<pre><code></code></pre></li>
</ul>
<pre><code>docker ps
CONTAINER ID   IMAGE                      COMMAND                  CREATED          STATUS          PORTS                                                                                  NAMES
0330a5a4c896   cwj3688/code-server-hol3   &quot;/usr/bin/entrypoint…&quot;   14 minutes ago   Up 14 minutes   0.0.0.0:3000-&gt;3000/tcp, [::]:3000-&gt;3000/tcp, 0.0.0.0:80-&gt;8080/tcp, [::]:80-&gt;8080/tcp   code-server-hol3</code></pre><br>

<h3 id="📝해결">📝해결</h3>
<p>서비스 확인 시 443포트로 오픈되어 있지 않음</p>
<ol>
<li>docker-compose.yaml 수정으로 Caddyfile 재배포</li>
</ol>
<ul>
<li><p>Yaml 수정 전</p>
<pre><code class="language-yaml">services:
code-server:
  image: cwj3688/code-server-hol3
  container_name: code-server-hol3
  restart: always
  ports:
    - &quot;3000:3000&quot;
    - &quot;80:8080&quot;
  volumes:
    - /home/ubuntu/project:/home/coder/project
    - /var/run/docker.sock:/var/run/docker.sock
    - /home/ubuntu/.scp:/home/coder/.scp
    - /home/ubuntu/.scpconf:/home/coder/.scpconf
    - /home/ubuntu/.kube:/home/coder/.kube
    - /home/ubuntu/.config:/home/coder/.config
    - /home/ubuntu/.local:/home/coder/.local

  environment:
    # - PASSWORD=admin12345!!
    - PASSWORD=${PASSWORD}
  command: /home/coder/project
  # command: --auth none /home/coder/project
  group_add:
    # - $(shell getent group docker | cut -d: -f3)
    - ${DOCKER_GID}</code></pre>
</li>
</ul>
<ul>
<li><p>Yaml 수정 후
Caddy(HTTPS 프록시) 추가</p>
<pre><code class="language-yaml">services:
code-server:
  image: cwj3688/code-server-hol3
  container_name: code-server-hol3
  restart: always
  ports:
    - &quot;3000:3000&quot;

  volumes:
    - /home/ubuntu/project:/home/coder/project
    - /var/run/docker.sock:/var/run/docker.sock
    - /home/ubuntu/.scp:/home/coder/.scp
    - /home/ubuntu/.scpconf:/home/coder/.scpconf
    - /home/ubuntu/.kube:/home/coder/.kube
    - /home/ubuntu/.config:/home/coder/.config
    - /home/ubuntu/.local:/home/coder/.local
  environment:
    - PASSWORD=${PASSWORD}
  command: /home/coder/project
  group_add:
    - ${DOCKER_GID}

# 새롭게 추가된 Caddy(HTTPS 프록시) 컨테이너
caddy:
  image: caddy:alpine
  container_name: caddy-proxy
  restart: always
  ports:
    - &quot;80:80&quot;
    - &quot;443:443&quot;
  command: caddy reverse-proxy --from 123.41.35.83.sslip.io --to http://code-server:8080
  volumes:
    - caddy_data:/data
</code></pre>
</li>
</ul>
<p>volumes:
  caddy_data:</p>
<pre><code>&lt;br&gt;

2. 컨테이너 재배포</code></pre><h1 id="1-기존-컨테이너-종료">1. 기존 컨테이너 종료</h1>
<p>sudo docker compose down</p>
<h1 id="2-비밀번호-및-권한-변수-다시-불러오기">2. 비밀번호 및 권한 변수 다시 불러오기</h1>
<p>PASSWORD=$(cat /home/ubuntu/project/env-info.txt | grep &quot;Password:&quot; | awk &#39;{print $NF}&#39;)
DOCKER_GID=$(getent group docker | cut -d: -f3)</p>
<h1 id="3-새로운-구성code-server--caddy으로-띄우기">3. 새로운 구성(Code-Server + Caddy)으로 띄우기</h1>
<p>sudo PASSWORD=$PASSWORD DOCKER_GID=$DOCKER_GID docker compose up -d</p>
<pre><code>- 비밀번호 및 권한 변수(PASSWORD, DOCKER_GID)를 다시 불러온 이유
리눅스의 환경 변수는 &#39;휘발성 메모리&#39;와 같음 
특정 터미널 창이나 스크립트가 실행되는 동안에만 존재하고, 창을 닫거나 스크립트가 끝나면 싹 사라짐

&lt;br&gt;

3. Caddy 란?
Caddy는 Nginx나 Apache와 같은 웹 서버 프로그램
하지만 다른 웹 서버들과 차별화되는 엄청난 강력한 무기가 하나 있는데, 
바로 자동 HTTPS(SSL 인증서) 발급 기능

- 리버스 프록시(Reverse Proxy) 역할
위 내용 &#39;[[확인](https://velog.io/@sunny-10/sslip.io-%EC%A0%91%EA%B7%BC-%EB%B6%88%EA%B0%80#%ED%99%95%EC%9D%B8)]&#39; 부분 확인 시 Code-Server 컨테이너는 HTTP(8080 포트)로만 통신
그래서 Caddy가 외부에서 들어오는 HTTPS 요청을 자기가 대신 받아서, 
내부의 Code-Server에게 HTTP로 안전하게 토스

- 무료 보안 인증서 자동 세팅
원래 https:// 자물쇠 마크를 띄우려면 인증 기관에 가서 도메인을 증명하고 복잡한 인증서를 발급받아 서버에 세팅해야 함
하지만 Caddy는 Publicip.sslip.io 라는 주소만 알려주면, 
자기가 알아서 Let&#39;s Encrypt 같은 기관과 통신해 무료 보안 인증서를 받아오고 주기적으로 갱신까지 해줌
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[VM Init 스크립트 주의사항]]></title>
            <link>https://velog.io/@sunny-10/VM-Init-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%A3%BC%EC%9D%98%EC%A0%90</link>
            <guid>https://velog.io/@sunny-10/VM-Init-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%A3%BC%EC%9D%98%EC%A0%90</guid>
            <pubDate>Tue, 19 May 2026 05:11:04 GMT</pubDate>
            <description><![CDATA[<h2 id="vm-init-스크립트-주의점">VM Init 스크립트 주의점</h2>
<h3 id="🔍현상">🔍현상</h3>
<p>정상적으로 작성한 VM Init 스크립트를 통해 VM 생성하였음에도
스크립트 인식을 하지 못하는 경우</p>
<h3 id="📝해결">📝해결</h3>
<p>bash 쉘은 줄바꿈이 있으면 새로운 명령어로 인식하기에
한줄로 이어쓰거나 역슬래시(&#39;&#39;)를 활용</p>
<p>주로 Copy&amp;Paste 오류 또는 형식에 맞지않게 가져오는 경우 발생
<br></p>
<h3 id="✅예시">✅예시</h3>
<h4 id="문제가-발생한-init-스크립트">문제가 발생한 Init 스크립트</h4>
<pre><code>#!/bin/bash

# 스크립트 실행 중 오류 발생시 즉시 중단
set -e

# 웹 IDE 초기화를 위한 저장소 URL과 디렉토리, 이미지 이름 설정
REPO_URL=&quot;https://github.com/xxx/init-webide.git&quot;
REPO_DIR=&quot;init-webide&quot;
IMAGE_NAME=&quot;xxx/code-server-xxx&quot;
HOME_DIR=&quot;/home/ubuntu&quot;
PROJECT_DIR=&quot;${HOME_DIR}/project&quot; # HOME_DIR 변수를 사용하여 경로 설정

# 저장소 클론 및 디렉토리 이동
git clone &quot;$REPO_URL&quot;
cd &quot;$REPO_DIR&quot;

# Docker 설치 스크립트 실행
chmod +x install_docker.sh
./install_docker.sh

# Public IP 업데이트 스크립트 실행
chmod +x update_ip.sh
./update_ip.sh

# Docker 이미지 다운로드
docker pull &quot;$IMAGE_NAME&quot;

# 프로젝트 및 설정 디렉토리 생성
mkdir -p &quot;$PROJECT_DIR&quot;
mkdir -p &quot;${HOME_DIR}/.scp&quot; &quot;${HOME_DIR}/.scpconf&quot; 
&quot;${HOME_DIR}/.kube&quot; &quot;${HOME_DIR}/.config&quot; &quot;${HOME_DIR}/.local&quot; # 🔴줄바꿈

# 랜덤 패스워드 생성 및 파일에 저장
PASSWORD=$(openssl rand -base64 12)
echo &quot;Your Web-IDE Password: ${PASSWORD}&quot; &gt; 
&quot;${PROJECT_DIR}/env-info.txt&quot; # 🔴줄바꿈

# Docker 그룹 ID 및 패스워드를 환경 변수로 전달하여 컨테이너 실행
DOCKER_GID=$(getent group docker | cut -d: -f3) 
PASSWORD=${PASSWORD} # 🔴줄바꿈 
docker compose up -d

# 권한 설정 (모든 경로에 HOME_DIR 변수 적용)
chown -R 1000:1000 &quot;$HOME_DIR&quot;

# 콘솔에 패스워드 및 저장 경로 출력 (변수명 오류 수정)
echo &quot;================================================================&quot;
echo &quot;   A new password for the Web-IDE has been generated.&quot;
echo &quot;   Password: ${PASSWORD}&quot;
echo &quot;   It has been saved to: ${PROJECT_DIR}/env-info.txt&quot;
echo &quot;================================================================&quot;

echo &quot;Web-IDE setup is complete!&quot;</code></pre><br>

<h4 id="문제를-해결한-init-스크립트">문제를 해결한 Init 스크립트</h4>
<pre><code>#!/bin/bash

# 스크립트 실행 중 오류 발생시 즉시 중단
set -e

# 웹 IDE 초기화를 위한 저장소 URL과 디렉토리, 이미지 이름 설정
REPO_URL=&quot;https://github.com/xxx/init-webide.git&quot;
REPO_DIR=&quot;init-webide&quot;
IMAGE_NAME=&quot;xxx/code-server-xxx&quot;
HOME_DIR=&quot;/home/ubuntu&quot;
PROJECT_DIR=&quot;${HOME_DIR}/project&quot; # HOME_DIR 변수를 사용하여 경로 설정

# 저장소 클론 및 디렉토리 이동
git clone &quot;$REPO_URL&quot;
cd &quot;$REPO_DIR&quot;

# Docker 설치 스크립트 실행
chmod +x install_docker.sh
./install_docker.sh

# Public IP 업데이트 스크립트 실행
chmod +x update_ip.sh
./update_ip.sh

# Docker 이미지 다운로드
docker pull &quot;$IMAGE_NAME&quot;

# 프로젝트 및 설정 디렉토리 생성
mkdir -p &quot;$PROJECT_DIR&quot;
mkdir -p &quot;${HOME_DIR}/.scp&quot; &quot;${HOME_DIR}/.scpconf&quot; &quot;${HOME_DIR}/.kube&quot; &quot;${HOME_DIR}/.config&quot; &quot;${HOME_DIR}/.local&quot;

# 랜덤 패스워드 생성 및 파일에 저장
PASSWORD=$(openssl rand -base64 12)
echo &quot;Your Web-IDE Password: ${PASSWORD}&quot; &gt; &quot;${PROJECT_DIR}/env-info.txt&quot;

# Docker 그룹 ID 및 패스워드를 환경 변수로 전달하여 컨테이너 실행
DOCKER_GID=$(getent group docker | cut -d: -f3) PASSWORD=${PASSWORD} docker compose up -d

# 권한 설정 (모든 경로에 HOME_DIR 변수 적용)
chown -R 1000:1000 &quot;$HOME_DIR&quot;

# 콘솔에 패스워드 및 저장 경로 출력 (변수명 오류 수정)
echo &quot;================================================================&quot;
echo &quot;   A new password for the Web-IDE has been generated.&quot;
echo &quot;   Password: ${PASSWORD}&quot;
echo &quot;   It has been saved to: ${PROJECT_DIR}/env-info.txt&quot;
echo &quot;================================================================&quot;

echo &quot;Web-IDE setup is complete!&quot;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[vi Tiny Version 활용 개선]]></title>
            <link>https://velog.io/@sunny-10/-</link>
            <guid>https://velog.io/@sunny-10/-</guid>
            <pubDate>Tue, 19 May 2026 04:28:11 GMT</pubDate>
            <description><![CDATA[<h2 id="vi-편집-시-tiny-version으로-인한-편집-어려움">vi 편집 시 Tiny Version으로 인한 편집 어려움</h2>
<h3 id="🔍현상">🔍현상</h3>
<p>vi/vim에서 INSERT MODE(i) 상태에서 방향키를 눌렀는데,
터미널이 방향키 escape sequence(A B C D)를 제대로 해석하지 못해서 발생하는 현상</p>
<ul>
<li>최소 설치된 Rocky/RHEL/CentOS</li>
<li>vim이 아니라 vi(tiny vi) 사용 중</li>
<li>TERM 환경변수 이상</li>
<li>SSH/콘솔 터미널 호환 문제</li>
<li>vim-enhanced 미설치</li>
</ul>
<h3 id="📝해결-방법">📝해결 방법</h3>
<p>최우선으로 vi Version 확인</p>
<pre><code>vi --version</code></pre><p>Tiny Version일 경우</p>
<pre><code>echo &quot;set nocompatible&quot; &gt;&gt; ~/.vimrc
echo &quot;set esckeys&quot; &gt;&gt; ~/.vimrc
echo &quot;set ttimeout&quot; &gt;&gt; ~/.vimrc
echo &quot;set ttimeoutlen=100&quot; &gt;&gt; ~/.vimrc
echo &quot;set backspace=indent,eol,start&quot; &gt;&gt; ~/.vimrc</code></pre><p>1) echo &quot;set nocompatible&quot; &gt;&gt; ~/.vimrc
vim을 옛날 vi 호환 모드가 아니라 vim 방식으로 동작하게 하는 설정.
방향키, 백스페이스, 편집 동작이 더 자연스러워짐.</p>
<p>2) echo &quot;set esckeys&quot; &gt;&gt; ~/.vimrc
Insert 모드에서 방향키 같은 특수 키를 사용할 수 있게 하는 설정.
방향키가 A B C D로 찍히는 문제와 관련 있음.</p>
<p>3) echo &quot;set ttimeout&quot; &gt;&gt; ~/.vimrc
방향키, ESC, Delete 같은 키 입력을 구분할 때 대기 시간을 사용하겠다는 뜻.</p>
<p>4) echo &quot;set ttimeoutlen=100&quot; &gt;&gt; ~/.vimrc
특수 키 입력을 기다리는 시간을 100ms로 설정.
ESC를 눌렀을 때 너무 늦게 반응하거나, 방향키가 문자로 입력되는 문제를 줄여줌.</p>
<p>5) echo &#39;set backspace=indent,eol,start&#39; &gt;&gt; ~/.vimrc
Backspace가 정상적으로 지울 수 있는 범위를 넓혀주는 설정.</p>
<p>indent = 자동 들여쓰기 삭제 가능
eol    = 줄 끝에서 이전 줄로 이어서 삭제 가능
start  = insert 모드 시작 지점 이전 문자도 삭제 가능</p>
<p>이 설정 없으면 Insert 모드에서 백스페이스가 일부 위치에서 안 먹을 수 있음.</p>
<h3 id="👍권장사항">👍권장사항</h3>
<p>Tiny Version 보단 Full Version 권장</p>
<pre><code>(Ubuntu 기준)
apt update &amp;&amp; apt install -y vim</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[oh-my-zsh 가이드]]></title>
            <link>https://velog.io/@sunny-10/oh-my-zsh-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@sunny-10/oh-my-zsh-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Tue, 03 Jun 2025 00:33:44 GMT</pubDate>
            <description><![CDATA[<h2 id="1oh-my-zsh-설치">1.oh my zsh 설치</h2>
<pre><code>$ sudo apt update            # 패키지 목록 업데이트
$ sudo apt install -y zsh        # zsh 설치
$ sh -c &quot;$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;
$ vi ~/.zshrc                    # 테마가 여러 개 있음 마음에 드는 테마를 선택 가능

&#39;
&#39;
  ZSH_THEME=&quot;agnoster&quot;
&#39;
&#39;</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/8ec93f4f-7355-4365-8ef5-c7e2c9fbdfc5/image.png" alt=""></p>
<pre><code>$ source ~/.zshrc # zsh 적용</code></pre><p>아래 링크로 더 많은 테마를 확인할 수 있음</p>
<p><a href="https://github.com/ohmyzsh/ohmyzsh/wiki/Themes">https://github.com/ohmyzsh/ohmyzsh/wiki/Themes</a></p>
<p>가이드에서 사용할 테마는 &#39; agnoster &#39; 
<img src="https://velog.velcdn.com/images/sunny-10/post/1dcf60bb-2a10-4db5-ba74-165863692774/image.png" alt=""></p>
<h2 id="2-터미널-폰트가-깨져있을-경우">2. 터미널 폰트가 깨져있을 경우</h2>
<p>아래 링크로 폰트를 다운 받을 수 있음
<a href="https://github.com/powerline/fonts">https://github.com/powerline/fonts</a>
가이드에서 사용할 폰트는 &#39; Meslo slashed &#39; 
다운로드 받은 후 L M S 마음에 드는 사이즈 선택하여 모두 설치해주면 됨
<img src="https://velog.velcdn.com/images/sunny-10/post/0d43636a-dc4c-491d-a63b-b5a5afa39bad/image.png" alt=""></p>
<p>터미널 → 환경설정 → 프로파일 에서 서체 등록 후 터미널 재시작 하면 폰트 적용
<img src="https://velog.velcdn.com/images/sunny-10/post/9c1be1ae-5050-4423-925a-01190cf493ed/image.png" alt=""></p>
<h2 id="3-zsh-autosuggestions-설치-선택적">3. zsh-autosuggestions 설치 (선택적)</h2>
<p>더 많은 정보를 노출 시켜주는 것 뿐만 아니라 막강한 기능들이 있음
<a href="https://github.com/zsh-users/zsh-autosuggestions">https://github.com/zsh-users/zsh-autosuggestions</a> (기능확인)</p>
<pre><code>$ git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
$ git clone https://github.com/zsh-users/zsh-completions ${ZSH_CUSTOM:-${ZSH:-~/.oh-my-zsh}/custom}/plugins/zsh-completions

$ vi ~/.zshrc                     # vi 편집
&#39;
&#39;
plugins=(
        git
        zsh-autosuggestions
        zsh-completions
)
&#39;
&#39;</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/bf2b652e-bd39-4eec-8622-a22c197d9eb5/image.png" alt=""></p>
<pre><code>$ source ~/.zshrc # zsh 적용</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/0b0bafa1-cc9e-42e6-90c2-c82f17cb359c/image.png" alt=""></p>
<p>적용이 된 걸 확인할 수 있음
(전에 사용했던 명령을 그대로 사용할 시 &#39;→&#39; 방향키를 눌러 바로 적용가능)</p>
<h2 id="4-멀티라인-작성-선택적">4. 멀티라인 작성 (선택적)</h2>
<p>커맨드를 입력 시 가시적인 불편함 해소</p>
<pre><code>$ vi ~/.oh-my-zsh/themes/agnoster.zsh-theme

( 빈 곳에 추가 )
prompt_newline() {
  if [[ -n $CURRENT_BG ]]; then
    echo -n &quot;%{%k%F{$CURRENT_BG}%}$SEGMENT_SEPARATOR
%{%k%F{blue}%}$SEGMENT_SEPARATOR&quot;
  else
    echo -n &quot;%{%k%}&quot;
  fi

  echo -n &quot;%{%f%}&quot;
  CURRENT_BG=&#39;&#39;
}

( 추가 후 build prompt 에 추가 작성 )

&#39;
&#39;
  prompt_newline
&#39;
&#39;</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/f44b909e-302c-4cca-8b34-81b10c6f1d0f/image.png" alt=""></p>
<h2 id="5-색상-변경-선택적">5. 색상 변경 (선택적)</h2>
<p>agnoster 색상 변경</p>
<pre><code>$ vi ~/.oh-my-zsh/themes/agnoster.zsh-theme

prompt_dir() {
  prompt_segment 39d $CURRENT_FG &#39;%~&#39;      # 39d로 변경 (하늘색)
}</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/498f7d92-695b-436f-a148-6fe6b8e4da50/image.png" alt=""></p>
<p>VM 재접속 하면 적용
<img src="https://velog.velcdn.com/images/sunny-10/post/20550820-15cf-43e5-baaf-b1228cbb1590/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[k9s 설치 및 사용]]></title>
            <link>https://velog.io/@sunny-10/k9s-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@sunny-10/k9s-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Tue, 03 Jun 2025 00:29:42 GMT</pubDate>
            <description><![CDATA[<ol>
<li>k9s 란?</li>
</ol>
<p>k9s는 터미널에서 Kubernetes 클러스터를 관리할 수 있는 CLI 도구</p>
<p>kubectl도 같이 사용할 수 있어 같은 컨텍스트에서  kubectl과 k9s를 오가며 사용할 수 있는 장점이 있음</p>
<ol start="2">
<li>k9s 설치</li>
</ol>
<p>Ubuntu 환경에서 k9s 설치하기</p>
<pre><code>$ wget https://github.com/derailed/k9s/releases/download/v0.26.7/k9s_Linux_x86_64.tar.gz 
$ tar -zxvf ./k9s_Linux_x86_64.tar.gz 
$ mkdir -p ~/.local/bin
$ mv ./k9s ~/.local/bin &amp;&amp; chmod +x ~/.local/bin/k9s                   # 바이너리 이동 및 실행권한 추가
$ rm ./k9s_Linux_x86_64.tar.gz LICENSE README.md                       # 불필요한 코드 제거
$ export PATH=$PATH:$HOME/.local/bin                                   # ~/.bashrc 파일에 경로 추가
$ source ~/.bashrc                                                     # 변경사항 적용</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/78458b9e-0a58-4cfe-b140-a5e6dceff867/image.png" alt=""></p>
<ol start="3">
<li>k9s 사용법</li>
</ol>
<pre><code>$ k9s info
$ k9s                                           # 기본 k9s 실행
$ k9s -n default                                # 지정된 namespace에서 k9s 실행
$ k9s -c pod                                    # k9s를 실행 후 pod 명령을 통한 pod 보기
$ k9s --readonly                                # 읽기 전용 모드로 k9s 실행되어 수정 명령이 비활성</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/2b8bb9ca-d6ef-4176-84db-cc499ce5fff4/image.png" alt=""></p>
<p>원하는 명령을 입력하면 아래와 같은 상태창을 확인할 수 있음
<img src="https://velog.velcdn.com/images/sunny-10/post/a31f743a-b11b-4a12-8515-4a965c95a1e9/image.png" alt=""></p>
<p>k9s 사용법은 아래 빨간 상자 내 명령어를 통해 확인할 수 있음
<img src="https://velog.velcdn.com/images/sunny-10/post/30593c94-2b46-49be-b8f9-3df9012f2b51/image.png" alt=""></p>
<hr>
<p>참고 링크 
k9s 란? - <a href="https://blog.outsider.ne.kr/1649">https://blog.outsider.ne.kr/1649</a>
k9s를 Ubuntu에 설치하기 - <a href="https://jusths.tistory.com/267">https://jusths.tistory.com/267</a>
k9s 사용법 - <a href="https://peterica.tistory.com/276">https://peterica.tistory.com/276</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[서비스 메쉬 관측 플랫폼 구성]]></title>
            <link>https://velog.io/@sunny-10/%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A9%94%EC%89%AC-%EA%B4%80%EC%B8%A1-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EA%B5%AC%EC%84%B1</link>
            <guid>https://velog.io/@sunny-10/%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A9%94%EC%89%AC-%EA%B4%80%EC%B8%A1-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EA%B5%AC%EC%84%B1</guid>
            <pubDate>Sat, 03 May 2025 21:42:08 GMT</pubDate>
            <description><![CDATA[<h2 id="개념">[개념]</h2>
<h3 id="1-istio">1. Istio</h3>
<p>역할
서비스 메쉬를 구성해 트래픽 관리, 보안, 모니터링, 트레이싱을 지원.
서비스 앞에 Envoy proxy(Sidecar) 를 주입해 네트워크 트래픽을 제어함.
세부 기능:
Ingress Gateway: 외부 트래픽 진입점
VirtualService &amp; DestinationRule: 트래픽 라우팅, 버전 분기 등
Telemetry: Envoy proxy가 Prometheus, OpenTelemetry로 메트릭 및 트레이싱을 수집함
Security: mTLS, 인증/인가, JWT 처리 등</p>
<h3 id="2-opentelemetry-collector">2. OpenTelemetry Collector</h3>
<p>역할
Istio/Envoy가 수집한 트레이싱 데이터(Span) 를 수신해서 다양한 백엔드로 전달.
Jaeger, Zipkin, NewRelic, OTLP 등으로 멀티 백엔드 export가 가능.
세부 기능
receivers: 데이터를 수신 (예: OTLP, Zipkin)
processors: 가공 및 필터링 (선택사항)
exporters: 백엔드로 전달 (예: Jaeger, Prometheus Remote Write)
service.pipelines: 위 구성들을 연결</p>
<h3 id="3-kiali">3. Kiali</h3>
<p>역할
Istio 서비스 메쉬의 시각화 도구. 실시간 트래픽 흐름, 라우팅 정책, 오류율, 성능 등을 볼 수 있음.
세부 기능
실시간 서비스 맵
요청 수, 지연 시간, 오류율 등 시각화
Istio 설정(VirtualService, DestinationRule 등) 검토 가능
서비스 간 의존성 확인 (디버깅에 매우 유용)</p>
<h2 id="구성">[구성]</h2>
<h3 id="1-기본-istio-환경-구축--샘플-앱-배포">1. 기본 Istio 환경 구축 + 샘플 앱 배포</h3>
<pre><code># brew로 istioctl 설치
brew install istioctl

# istioctl 버전 확인을 진행하여 설치 확인
istioctl version

# Istio demo 프로파일로 설치
# → Prometheus, Kiali, Jaeger, Grafana 포함
istioctl install --set profile=demo -y
# Tech CS 분들은 여기서 오류 발생 가능! VPN 연결 후 재시도해 보세요

# 카카오클라우드로 진행 시 failed to call Webhook 발생
# https://kko.kakao.com/NwPwmdCDcA
# deploy 전체 spec 설정이 아닌 pod spec 설정에서 수정해 주세요.
kubectl edit deployment -n istio-system istiod
#***
#spec:
#    hostNetwork: true # 추가
#    dnsPolicy: ClusterFirstWithHostNet
#containers:
#***

# default 네임스페이스에 istio-injection label 추가
kubectl label namespace default istio-injection=enabled

# Bookinfo 샘플 앱 배포
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.21/samples/bookinfo/platform/kube/bookinfo.yaml

# Gateway + 트래픽 라우팅 정의 적용
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.21/samples/bookinfo/networking/bookinfo-gateway.yaml

# Istio Ingress Gateway의 EXTERNAL-IP 확인
# 주소 확인 후 웹브라우저에서 접속
kubectl get svc istio-ingressgateway -n istio-system
# 예: http://&lt;EXTERNAL-IP&gt;/productpage</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/b705567d-cd1b-4808-9f82-1aeecdfd6558/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>필요 조건</th>
<th>추천</th>
</tr>
</thead>
<tbody><tr>
<td>단순한 웹 트래픽 라우팅 (e.g. ingress + path 기반)</td>
<td>NGINX Ingress Controller</td>
</tr>
<tr>
<td>복잡한 트래픽 분할, 인증(mTLS), 정책 제어, 전체 서비스 메시</td>
<td>Istio Ingress Gateway</td>
</tr>
<tr>
<td>A/B 테스트, 카나리 배포, 헤더 기반 라우팅, 가시성 확보가 필요한 환경</td>
<td>Istio</td>
</tr>
<tr>
<td>빠르게 설정하고 운영 단순화가 중요한 경우</td>
<td>NGINX</td>
</tr>
</tbody></table>
<p>NGINX Ingress Controller는 &quot;단순한 출입문&quot;,
Istio + Ingress Gateway는 &quot;보안·관제·스마트 제어까지 가능한 첨단 통제소&quot;</p>
<h3 id="2-kiali-prometheus-grafana로-메트릭-시각화">2. Kiali, Prometheus, Grafana로 메트릭 시각화</h3>
<p>Prometheus : Istio에서 수집한 메트릭 데이터를 저장
Grafana : Prometheus 데이터를 시각화 (대시보드)
Kiali : Istio 메시에 연결된 서비스 간 관계/트래픽 흐름 시각화</p>
<pre><code># observability 네임스페이스 생성 (선택)
kubectl create namespace observability
# 필요 시 생성하고, 아래의 파드 생성 시 선택적으로 네임스페이스 변경하여 진행.
# 기본적으로 istio-system 네임스페이스에 생성됨. 변경하는 법은 따로 기재하지 않음.

# Istio에서 제공하는 애드온 매니페스트 적용
# Prometheus, Grafana, Kiali 포함
# istio-system 네임스페이스에 관련 Pod들을 배포
# prometheus는 이미 설치되어 있을 수 있음!
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.18/samples/addons/prometheus.yaml
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.18/samples/addons/grafana.yaml
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.18/samples/addons/kiali.yaml

# 모두 running 되고 있는지 확인
kubectl get pods -n istio-system

# 접근 테스트 (포트포워딩)
# 내부 서비스이기에 서비스화 시키지 않고 포트포워딩 하여 사용합니다!
# 이는 비슷한 구성은 카카오클라우드 grafana 기술문서에도 나와있습니다.

# Kiali (서비스 맵 보기)
# http://localhost:20001
kubectl port-forward svc/kiali -n istio-system 20001:20001
# Grafana (대시보드)
# http://localhost:3000
kubectl port-forward svc/grafana -n istio-system 3000:3000
# Prometheus (메트릭 확인)
# http://localhost:9090
kubectl port-forward svc/prometheus -n istio-system 9090:9090</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/00108599-2ace-4d8f-a45b-bf69a5d97f17/image.png" alt=""></p>
<h3 id="3-트레이스-추적">3. 트레이스 추적</h3>
<p>OpenTelemetry Collector : Istio에서 수집한 trace 데이터를 수집/가공/전송
Jaeger : 트레이스 데이터를 저장하고 시각화하는 UI 제공
Istio : Envoy 사이드카에서 trace 생성, OpenTelemetry에 전달</p>
<pre><code># Jaeger 배포
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.18/samples/addons/jaeger.yaml

# OpenTelemetry Collector 배포
# 이 구성은 OpenTelemetry Collector가 4317 포트로 OTLP 데이터를 수신하고, 
# 수신한 트레이스를 Jaeger에 전달하도록 설정되어 있음
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.18/samples/addons/extras/opentelemetry.yaml

# 포트포워딩으로 UI 확인
# http://localhost:16686
kubectl port-forward svc/jaeger -n istio-system 16686:16686

# 테스트 명령어 -&gt; 몇번 실행 후 Jaeger UI에서 서비스 간 호출 경로, 요청 지연 시간 등 확인 가능
kubectl exec &quot;$(kubectl get pod -l app=productpage -o jsonpath={.items..metadata.name})&quot; \
    -c productpage -- curl -s http://localhost:9080/productpage</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/ea021c27-7e69-4075-afcf-f1e75345d000/image.png" alt=""></p>
<hr>
<p>(참고)</p>
<ul>
<li><a href="https://blog.serialexperiments.co.uk/posts/kubernetes-port-forward-already-in-use/">https://blog.serialexperiments.co.uk/posts/kubernetes-port-forward-already-in-use/</a></li>
<li><a href="https://nginxstore.com/blog/istio/istio-prometheus-%EB%A9%94%ED%8A%B8%EB%A6%AD-%EC%88%98%EC%A7%91-%EB%B0%8F-grafana-%EC%97%B0%EB%8F%99/">https://nginxstore.com/blog/istio/istio-prometheus-%EB%A9%94%ED%8A%B8%EB%A6%AD-%EC%88%98%EC%A7%91-%EB%B0%8F-grafana-%EC%97%B0%EB%8F%99/</a></li>
<li><a href="https://istio.io/latest/docs/ops/integrations/jaeger/#installation">https://istio.io/latest/docs/ops/integrations/jaeger/#installation</a></li>
<li><a href="https://istio.io/latest/docs/tasks/observability/distributed-tracing/opentelemetry/">https://istio.io/latest/docs/tasks/observability/distributed-tracing/opentelemetry/</a></li>
<li><a href="https://velog.io/@rudclthe/istio-kiali">https://velog.io/@rudclthe/istio-kiali</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker & K8S offline Install]]></title>
            <link>https://velog.io/@sunny-10/Docker-K8S-offline-Install</link>
            <guid>https://velog.io/@sunny-10/Docker-K8S-offline-Install</guid>
            <pubDate>Sat, 03 May 2025 20:21:48 GMT</pubDate>
            <description><![CDATA[<p>[본 게시글은 22년 기준으로 작성되었음]
테스트 목적 : 폐쇄망에서 Docker 와 K8S를 설치하여 각 서비스의 구조 파악</p>
<h2 id="1-환경-세팅">1. 환경 세팅</h2>
<p>VM 3대를 준비 (master 1 + worker 2)
Offline 환경을 구성하기 위해 Outbound정책 Port에 제한을 둠</p>
<p>(단, KIC 환경에서는 VM 생성 시 아웃바운드 정책 Port를 모두 열고 VM생성이 끝나면 SG를 변경해 줄 것)</p>
<p>6443 - api server
2379 - etcd
2380 - etcd
4443 - metric server
179 - calico
10250 - kubelet API
<img src="https://velog.velcdn.com/images/sunny-10/post/dc2c03a7-549b-40f3-99d0-7052d15cdcb9/image.png" alt=""></p>
<h2 id="2-docker-설치">2. Docker 설치</h2>
<h3 id="21-docker-설치에-필요한-패키지를-준비">2.1 Docker 설치에 필요한 패키지를 준비</h3>
<p>URL을 통해 받거나 Docker가 설치되어 있는 VM에서 구해도 됨 </p>
<p>focal 은 ubuntu 20.xx
bionic 은 ubuntu 18.xx
URL : <a href="https://download.docker.com/linux/ubuntu/dists/bionic/pool/stable/amd64/">https://download.docker.com/linux/ubuntu/dists/bionic/pool/stable/amd64/</a></p>
<p>VM 경로 : /var/cache/apt/archives 확인</p>
<p>필요 패키지 - yohan guide (버전을 맞추어 설치)
containerd~
docker-ce-cli~
docker-ce-rootless-extra~
docker-ce~
docker-compose~
docker-scan-plugin~
libltdl~
pigz~</p>
<p>(본 테스트에서는 containerd, docker-ce, docker-cli 로만 구성)</p>
<h3 id="22-scp-를-통한-패키지-이동">2.2 scp 를 통한 패키지 이동</h3>
<p>로컬에서 패키지를 받았으면
로컬 → GateWay(oliver) → offline vm 으로 이동
통신가능한 VM에서 패키지를 받았으면
online vm → GateWay(oliver) → offline vm 으로 이동
<img src="https://velog.velcdn.com/images/sunny-10/post/12f716c1-cb44-4deb-a8e2-34a3073c3bd0/image.png" alt=""></p>
<h3 id="23-dpkg-를-통한-설치">2.3 dpkg 를 통한 설치</h3>
<p>.deb가 있는 디렉토리에서 dpkg 를 통한 패키지 설치</p>
<pre><code>$ sudo dpkg -i *.deb</code></pre><hr>
<h4 id="trouble-shooting❗️">Trouble Shooting❗️</h4>
<ul>
<li>containerd 버전문제로 1.4.1 이상으로 해결</li>
</ul>
<p>dpkg: error processing package docker-ce (–install): dependency problems - leaving unconfigured</p>
<p>의존성 트러블은 패키지가 부족하면 수도 없이 나옴</p>
<hr>
<h3 id="24-docker-설정-및-croup-변경">2.4 docker 설정 및 croup 변경</h3>
<h4 id="241-docker-설정-선택적">2.4.1 docker 설정 (선택적)</h4>
<pre><code>$ sudo usermod -aG docker $USER 
$ 재시작 하면 적용
사용자 그룹을 추가한다면 sudo 를 안붙여도 됨</code></pre><h4 id="242-cgroup-변경">2.4.2 cgroup 변경</h4>
<pre><code>cat &lt;&lt;EOF | sudo tee /etc/docker/daemon.json
{
  &quot;exec-opts&quot;: [&quot;native.cgroupdriver=systemd&quot;],
  &quot;log-driver&quot;: &quot;json-file&quot;,
  &quot;log-opts&quot;: {
    &quot;max-size&quot;: &quot;100m&quot;
  },
  &quot;storage-driver&quot;: &quot;overlay2&quot;
}
EOF

$ sudo systemctl daemon-reload
$ sudo systemctl restart docker</code></pre><h3 id="25-docker-설치-확인">2.5 Docker 설치 확인</h3>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/4518f44b-cddb-45eb-b246-bbea45b1f8af/image.png" alt=""></p>
<h2 id="3-k8s-설치">3. K8S 설치 </h2>
<p>kube-apiserver 는 최대 1단계의 마이너 버전 차이까지 가능
ex) 하나의 kube-apiserver 의 버전이 1.21 라면 다른 kube-apiserver 는 1.20 or 1.21 이여야 함</p>
<p>kubelet 은 kube-apiserver 보다 최신이여서는 안됨 그리고 최대 2단계 이전의 마이너 버전 차이까지 가능
ex) kube-apiserver 가 1.21 이라면 kubelet 은 1.21 or 1.20 or 1.19 까지 사용가능</p>
<p>kubectl 은 kube-apiserver 과 최대 1단계의 마이너 버전 차이까지 가능
ex) kube-apiserver 가 1.21 이라면 kubectl 은 1.22 or 1.21 or 1.20 까지 사용가능</p>
<h3 id="31-환경-설정">3.1 환경 설정</h3>
<p>필요한 패키지파일과 이미지 파일을 준비
패키지 파일은 2.1 과 동일하게 구할 수 있음
이미지의 경우 docker save로 .tar 저장</p>
<p>필요 패키지
kubeadm~
kubectl~
kubelet~
kubernetes-cni~
conntrack~
cri-tools~
socat~</p>
<p>필요 이미지
kube-proxy
kube-apiserver
kube-controller-manager
kube-scheduler
etcd
coredns
pause
calico/cni                           (CNI 설치 시 사용)
calico/node                       (CNI 설치 시 사용)
calico/kube-controllers   (CNI 설치 시 사용)
<img src="https://velog.velcdn.com/images/sunny-10/post/ac18fd16-5a66-4190-be53-f6d29e46b132/image.png" alt=""></p>
<h3 id="32-k8s-설치-master-node">3.2 K8S 설치 (master node)</h3>
<h4 id="321-설치">3.2.1 설치</h4>
<p>준비한 이미지 파일을 docker load 로 이미지 업로드 
준비한 패키지 파일을 dpkg 로 설치</p>
<pre><code>$ sudo dpkg -i *.deb</code></pre><h4 id="322-설정">3.2.2 설정</h4>
<pre><code>$ sudo swapoff -a
$ sudo sed -i &#39;/ swap / s/^\(.*\)$/#\1/g&#39; /etc/fstab
$ sudo kubeadm init</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/8931c6c4-7387-4781-b8c3-c576ba5d300d/image.png" alt=""></p>
<pre><code>(master node)
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

(확인)
$ kubectl get nodes </code></pre><p>CNI 가 미설치 되어 NotReady  확인 가능 </p>
<h3 id="33-cni-설치">3.3 CNI 설치</h3>
<p>calico 를 사용
앞 서 준비한 calico 이미지 사용
<a href="https://projectcalico.docs.tigera.io/manifests/calico.yaml">https://projectcalico.docs.tigera.io/manifests/calico.yaml</a>
의 셀스크립트를 calico.yaml 파일 저장</p>
<pre><code>$ kubectl apply -f calico.yaml

(확인)
$ kubectl get all -A
$ kubectl get nodes</code></pre><h3 id="34-worker-node-구축">3.4 worker node 구축</h3>
<p>2.docker 설치와 3.k8s설치 내용대로 설치 (단, init 하지말 것)
worker node의 k8s 설치에 사용 했던 이미지</p>
<p>kube-proxy
coredns
pause
calico/cni                           (CNI 설치 시 사용)
calico/node                       (CNI 설치 시 사용)</p>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/2b939fc3-081d-4e52-b10a-a986ece20b4e/image.png" alt=""></p>
<pre><code>(worker node)
master node 에서 init 했을 때 받은 토큰으로 조인
$ sudo kubeadm join 172.30.3.97:6443 --token gdpgce.j5mvth8gt7qu4u86 \
--discovery-token-ca-cert-hash sha256:ed4b205d09552dc4c957fb0e08c962560af5c9c93b0e3f222dfd7e29af7796e0 

(master node 에서 확인)
$ kubectl get all -A
$ kubectl get nodes</code></pre><h3 id="35-k8s-설치-결과">3.5 k8s 설치 결과</h3>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/d4d49603-fcb9-4002-b11a-f6367180526e/image.png" alt=""></p>
<h2 id="4-k8s-multi-master-구성">4. k8s Multi Master 구성</h2>
<h3 id="41-vm-세팅">4.1 VM 세팅</h3>
<p>앞선 Docker &amp; K8S offline install을 통해 master node 1대, worker node 2대가 구성이 되어 있을 것 (재사용)
같은 방법으로 master node 2대 추가, 기존의 node들은 kubeadm reset 처리
총, master node 3대 worker node 2대를 연결예정</p>
<h3 id="42-main-master-token-얻기">4.2 Main Master Token 얻기</h3>
<p>$ sudo kubeadm init --token-ttl 0  --control-plane-endpoint &quot;[Loadbalancer IP]:6443&quot; --upload-certs --pod-network-cidr=192.168.50.0/24 &gt;&gt; kubeadm-init-result.txt</p>
<p>(pod network cidr 를 지정하지 않으면 추 후 조인 시에 중복으로 인해 에러발생할 수 있음)
<img src="https://velog.velcdn.com/images/sunny-10/post/de46a531-7b47-4fe8-9253-874c66af68cb/image.png" alt=""></p>
<p>빨간 상자= control-plane token
노란 상자 = worker token</p>
<h3 id="43-각-노드-별-필요한-token으로-join">4.3 각 노드 별 필요한 Token으로 Join</h3>
<p>( Controll-plane Node )
 4.1 VM 세팅이 끝난 후 kubeadm init 대신 Controll-plane Token 값 적용 (위 빨간 상자 확인)</p>
<p>(Worker Node)
 4.1 VM 세팅이 끝난 후 Worker Token 값 적용 (위 노란 상자 확인)</p>
<h3 id="44-연결-확인">4.4 연결 확인</h3>
<p>Master node에서 확인</p>
<pre><code>$ kubectl get all -A
$ kubectl get nodes</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/85f0ad11-dbca-4881-b735-bc41afa21e49/image.png" alt=""><img src="https://velog.velcdn.com/images/sunny-10/post/3e2e7c74-2fbd-4990-879c-4059455070c6/image.png" alt="">
Main Master 에서만 kubectl apply -f calico.yaml 하면 됨 ( Sub Master 에서 실행 X )
각 Node 들은 필요 이미지만 들고 있으면 됨</p>
<hr>
<p>(참고)
Docker 참고 주소</p>
<ul>
<li>get.docker.com</li>
<li><a href="https://choco-life.tistory.com/40">https://choco-life.tistory.com/40</a></li>
<li><a href="https://docs.cyberwatch.fr/deploy/en/2_deploy_cyberwatch/offline/swarm/install_docker.html">https://docs.cyberwatch.fr/deploy/en/2_deploy_cyberwatch/offline/swarm/install_docker.html</a></li>
</ul>
<p>K8S 참고 주소</p>
<ul>
<li><a href="https://velog.io/@seokbin/Kubernetes-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EC%84%A4%EC%B9%98-kubeadm-offline-%ED%99%98%EA%B2%BD">https://velog.io/@seokbin/Kubernetes-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EC%84%A4%EC%B9%98-kubeadm-offline-%ED%99%98%EA%B2%BD</a></li>
<li><a href="https://github.com/tmax-cloud/install-k8s">https://github.com/tmax-cloud/install-k8s</a></li>
<li><a href="https://docs.genesys.com/Documentation/GCXI/latest/Dep/DockerOffline">https://docs.genesys.com/Documentation/GCXI/latest/Dep/DockerOffline</a></li>
<li><a href="https://www.sobyte.net/post/2022-06/k8s-intranet/">https://www.sobyte.net/post/2022-06/k8s-intranet/</a></li>
<li><a href="https://www.centlinux.com/2019/04/install-kubernetes-k8s-offline-on-centos-7.html">https://www.centlinux.com/2019/04/install-kubernetes-k8s-offline-on-centos-7.html</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[k8s 내 Ceph Storage Cluster 구성]]></title>
            <link>https://velog.io/@sunny-10/k8s-%EB%82%B4-Ceph-Storage-Cluster-%EA%B5%AC%EC%84%B1</link>
            <guid>https://velog.io/@sunny-10/k8s-%EB%82%B4-Ceph-Storage-Cluster-%EA%B5%AC%EC%84%B1</guid>
            <pubDate>Sat, 03 May 2025 19:52:21 GMT</pubDate>
            <description><![CDATA[<p>Kubernetes는 여러 스토리지 선택지가 있지만, 
그중 온프레미스 환경에서 관리가 쉽고 강력한 성능을 내는 Ceph이 있습니다. </p>
<p>Ceph Storage Cluster는 안정적이며, 높은 성능과 Block, File, Object 스토리지로 다양하게 사용이 가능합니다. 
Kubernetes Cluster내에 Ceph Storage Cluster를 구성하여 이를 PVC로 사용하는 테스트 입니다.</p>
<h3 id="k8s-구성-환경">k8s 구성 환경</h3>
<p>k8s node 1 : 8vCPU / 32 GiB Memory / 볼륨 100GB + 50GB
k8s node 2 : 8vCPU / 32 GiB Memory / 볼륨 100GB + 50GB
k8s node 3 : 8vCPU / 32 GiB Memory / 볼륨 100GB + 50GB
k8s node 4 : 8vCPU / 32 GiB Memory / 볼륨 100GB + 50GB
Ceph 클러스터를 위해 2개의 MGR / 3개의 MON / 디스크 수만큼의 OSD 데몬이 실행되어야 합니다.
<br></p>
<h3 id="1-rook-예제-및-소스파일-다운로드">1. Rook 예제 및 소스파일 다운로드</h3>
<p>Rook 예제 파일은 거의 모든 환경에서 대응할 수 있는 예제코드를 제공</p>
<pre><code>$ git clone https://github.com/rook/rook.git/</code></pre><br>

<h3 id="2-helm-활용-rook-오퍼레이터-구성">2. helm 활용 rook 오퍼레이터 구성</h3>
<p>helm 으로 간단하게 rook-ceph 구성</p>
<pre><code>(repo 추가) 
$ helm repo add rook-release https://charts.rook.io/release 
$ helm search repo rook-ceph </code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/304f50be-1544-4cfd-a9e0-35393118bf20/image.png" alt=""></p>
<pre><code>(rook 오퍼레이터 설치) 
$ kubectl create namespace rook-ceph 
(repo 추가) 
$ helm install --namespace rook-ceph rook-ceph rook-release/rook-ceph 
$ kubectl get all -n rook-ceph</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/9b8f02e3-d404-46d1-bda5-f34d035f9ede/image.png" alt="">
<br></p>
<h3 id="3-ceph-클러스터-구성">3. Ceph 클러스터 구성</h3>
<p>기본으로 구성할 경우, 2개의 MGR / 3개의 MON / 빈디스크 수만큼의 OSD 데몬이 생성되며 
최소 3개 노드이상일 경우 기본구성으로 설치하면 적당함 </p>
<p>아래 설치하기 전에 위에 구성한 오퍼레이터가 꼭 동작하고 있어야 하며, 
만약 오퍼레이터가 설치 중이라면 설치완료 후 진행</p>
<pre><code>$ helm install --namespace rook-ceph rook-ceph-cluster --set operatorNamespace=rook-ceph rook-release/rook-ceph-cluster 
$ kubectl get all -n rook-ceph</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/c9951c1f-6715-4c77-b885-73e388a07456/image.png" alt=""><img src="https://velog.velcdn.com/images/sunny-10/post/5dbf4211-246e-494a-934b-73ee0ccbc987/image.png" alt=""></p>
<hr>
<h4 id="trouble-shooting❗️">Trouble Shooting❗️</h4>
<p>rook-ceph-osd-prepare-[host명] 파드는 Completed 되었으나 정작 osd 파드가 생성되지 않음</p>
<pre><code>kubectl -n rook-ceph logs &lt;ceph-osd-prepare-pod&gt;</code></pre><p>를 통해 로그 확인 시 
&#39;cephosd: skipping OSD configuration as no devices matched the storage settings for this nod&#39; 가 발생하는 것을 확인</p>
<p>각 노드별 볼륨을 추가하여 해결</p>
<hr>
<br>

<h3 id="4-toolbox-설치">4. Toolbox 설치</h3>
<p>rook/deploy/examples/ 에 toolbox.yaml 파일을 사용하여 구성 
toolbox를 통해 ceph cli를 사용할 수 있음</p>
<pre><code>$ kubectl apply -f toolbox.yaml 
$ kubectl get deploy rook-ceph-tools -n rook-ceph 
$ kubectl -n rook-ceph exec -it 
(kubectl -n rook-ceph get pod -l &quot;app=rook-ceph-tools&quot; -o jsonpath=&#39;{.items[0].metadata.name}&#39;) bash 
(toolbox)$ ceph -s 
(toolbox)$ ceph osd status</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/ea77fca0-8eda-437e-ab58-383071edae26/image.png" alt="">
<br></p>
<h3 id="5-dashboard-사용">5. Dashboard 사용</h3>
<p>CephCluster의 Dashboard 서비스를 ssl을 사용하지 않도록 변경하고, 기본 url을 변경</p>
<pre><code>$ kubectl get svc -n rook-ceph 
$ kubectl edit CephCluster rook-ceph -n rook-ceph 
(수정전) 
dashboard : 
  enabled : true 
  ssl : true 

(수정후) 
dashboard : 
  enabled : true 
  ssl : false 
  urlPrefix : /ceph-dashboard</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/bf4d282d-197c-4083-a0dd-9008852ebf8b/image.png" alt=""><img src="https://velog.velcdn.com/images/sunny-10/post/f7518306-3d0c-48e7-a3c4-d61d527b3635/image.png" alt=""></p>
<p>대시보드가 ssl을 사용한다면 8443 포트이지만, 
ssl을 사용하지 않도록 구성하니 7000 포트로 고정 
rook-ceph 예제파일 중 dashboard-loadbalancer.yaml 파일을 수정하고 적용</p>
<pre><code>$ vi dashboard-loadbalancer.yaml 
(수정전) 
port : 8443 
targetPort : 8443 

(수정후) 
port : 7000 
targetPort : 7000 

$ kubectl apply -f dashboard-loadbalancer.yaml</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/f1333ae8-e4d9-46e3-930c-46fedb30dbbc/image.png" alt="">
이제 rook-ceph-mgr-dashboard-loadbalancer라는 서비스가 LoadBalancer 타입으로 만들어지고 외부 IP가 확인됨
(본 테스트에서는 카카오클라우드 LB를 사용하여 접속)
<img src="https://velog.velcdn.com/images/sunny-10/post/bc8be0b5-7d11-4433-aeaf-efc6cc0d63aa/image.png" alt=""></p>
<p>브라우저에서 <a href="http://PublicIP:7000/ceph-dashboard/">http://PublicIP:7000/ceph-dashboard/</a> 경로로 접속 
계정은 admin이고 패스워드는 아래 명령으로 확인할 수 있음</p>
<pre><code># 패스워드 확인 
$ kubectl get secret rook-ceph-dashboard-password -n rook-ceph -o yaml | grep &quot;password:&quot; | awk &#39;{print $2}&#39; | base64 --decode</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/92c3f009-c6fb-447b-adb0-f2b01d195dcd/image.png" alt="">
(아래 접속 화면은 pvc 생성 후 캡처)
<img src="https://velog.velcdn.com/images/sunny-10/post/856d1d83-a4b7-4030-9495-e11d1f1de2c4/image.png" alt="">
<br></p>
<h3 id="6-ceph-filesystem-pvc-생성">6. (Ceph-filesystem) PVC 생성</h3>
<p>PVC를 생성하면 자동으로 PV도 함께 생성되고 연결됨</p>
<pre><code>$ vi cephfs-pvc01.yaml 
--- 
apiVersion : v1
kind : PersistentVolumeClaim
metadata :
  name : cephfs-pvc01
spec :
  accessModes :
    - ReadWriteMany
  resources :
    requests :
      storage : 1Gi
  storageClassName : ceph-filesystem

$ kubectl apply -f cephfs-pvc01.yaml 
$ kubectl get pvc  </code></pre><br>

<h3 id="7-ceph-block-pvc-생성">7. (ceph-block) PVC 생성</h3>
<p>CephFS와 가장 큰 차이점은 PVC 생성 시 accessModes값을 ReadWriteOnce로 구성 
RBD는 ReadWriteMany로 구성할 수 없음</p>
<pre><code>$ vi cephblock-pvc01.yaml 
---
apiVersion : v1
kind : PersistentVolumeClaim
metadata :
  name : rbd-pvc
spec :
  accessModes :
    - ReadWriteOnce
  resources :
    requests :
      storage : 10Gi
  storageClassName : ceph-block

$ kubectl apply -f cephblock-pvc01.yaml 
$ kubectl get pvc</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/c5ad34f2-5c82-4962-92f0-dcfa9682ff8c/image.png" alt="">
<br></p>
<h3 id="8-test-pod-생성-및-확인">8. Test pod 생성 및 확인</h3>
<p>pod를 만들어 마운트가 되는지 테스트</p>
<pre><code>$ vi ceph-test-pod.yaml 
---
apiVersion: v1
kind: Pod
metadata:
  name: ceph-test-pod
spec:
  containers:
    - name: web-server
      image: nginx
      volumeMounts:
        - name: ceph-filesystem-01
          mountPath: /data1
        - name: ceph-block-01
          mountPath: /data2
  volumes:
    - name: ceph-filesystem-01
      persistentVolumeClaim:
        claimName: cephfs-pvc01
        readOnly: false
    - name: ceph-block-01
      persistentVolumeClaim:
        claimName: rbd-pvc
        readOnly: false

$ kubectl apply -f ceph-test-pod.yaml

(확인)                    
$ kubectl exec -ti ceph-test-pod /bin/bash                    
rdb-test-pod01&gt;df -h</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/49df5812-2cf4-44d0-8f4e-69b76b8d5e25/image.png" alt="">
설정한 마운트 설정에 맞게 마운트가 된 것을 볼 수 있음</p>
<hr>
<p>(참고)</p>
<ul>
<li><a href="https://itmaya.co.kr/wboard/view.php?wb=tech&amp;idx=35">https://itmaya.co.kr/wboard/view.php?wb=tech&amp;idx=35</a></li>
<li><a href="https://jay-chamber.tistory.com/entry/rook-ceph-trouble-shooting-OSD%EA%B0%80-%EC%83%9D%EC%84%B1%EB%90%98%EC%A7%80-%EC%95%8A%EC%95%84%EC%9A%94-Try-1">https://jay-chamber.tistory.com/entry/rook-ceph-trouble-shooting-OSD%EA%B0%80-%EC%83%9D%EC%84%B1%EB%90%98%EC%A7%80-%EC%95%8A%EC%95%84%EC%9A%94-Try-1</a></li>
<li><a href="https://rook.io/docs/rook/v1.9/ceph-teardown.html#zapping-devices">https://rook.io/docs/rook/v1.9/ceph-teardown.html#zapping-devices</a></li>
<li>24단계 실습으로 끝내는 쿠버네티스(Rook-ceph) <a href="https://naver.me/I55O7bmc">https://naver.me/I55O7bmc</a></li>
<li><a href="https://computing-jhson.tistory.com/112">https://computing-jhson.tistory.com/112</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[최종 프로젝트 테라폼 소스]]></title>
            <link>https://velog.io/@sunny-10/%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%85%8C%EB%9D%BC%ED%8F%BC-%EC%86%8C%EC%8A%A4</link>
            <guid>https://velog.io/@sunny-10/%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%85%8C%EB%9D%BC%ED%8F%BC-%EC%86%8C%EC%8A%A4</guid>
            <pubDate>Fri, 23 Dec 2022 02:43:48 GMT</pubDate>
            <description><![CDATA[<p><a href="https://github.com/Sunny-1030/effective-eureka/tree/main/terraform">Github Sunny</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Miniproject(22.06.07~22.06.14)]]></title>
            <link>https://velog.io/@sunny-10/test</link>
            <guid>https://velog.io/@sunny-10/test</guid>
            <pubDate>Mon, 13 Jun 2022 19:14:23 GMT</pubDate>
            <description><![CDATA[<h1 id="miniproject-_4조">Miniproject _4조</h1>
<p><code>Cloud Infra ochestration
: Tool 구성을 aws로 진행</code></p>
<table>
<thead>
<tr>
<th>팀원</th>
<th>강재민</th>
<th>김효진</th>
<th>박민선</th>
<th>박지연</th>
<th>임재헌</th>
</tr>
</thead>
<tbody><tr>
<td>역할</td>
<td>Ansible 구축</td>
<td>Jenkins 구축</td>
<td>Terraform 구축</td>
<td>문서작성</td>
<td>Ansible 구축</td>
</tr>
</tbody></table>
<p><br/><br/><br/></p>
<h2 id="1-프로젝트-개요">1. 프로젝트 개요</h2>
<h3 id="1-1-workflow">1-1. WorkFlow</h3>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/239f2126-1bc3-4277-a19d-ef68b2484c6e/image.png" alt=""></p>
<h3 id="1-2-기술스택과-도구">1-2. 기술스택과 도구</h3>
<p>|스택 및 도구|
|:-:|:-:|
|jenkins|<img src="https://velog.velcdn.com/images/sunny-10/post/fa7e8136-b5fc-4ba0-a2ff-2622c8ea3990/image.png" alt="">
|Ansible|<img src="https://velog.velcdn.com/images/sunny-10/post/692c7530-5745-4553-aac3-8b3ca4697c61/image.png" alt="">
|Terraform|<img src="https://velog.velcdn.com/images/sunny-10/post/26fbb57b-4cf9-43b5-b0ff-b6ca3f354416/image.png" alt="">
|Docker|<img src="https://velog.velcdn.com/images/sunny-10/post/7351466d-56c3-40b3-9515-108a04721761/image.png" alt="">
|Kubernetes|<img src="https://velog.velcdn.com/images/sunny-10/post/e499284c-bcf7-4471-8eee-b8bdf421f575/image.png" alt="">
|Git|<img src="https://velog.velcdn.com/images/sunny-10/post/8f3531a1-5c8b-4e95-9fc0-14e20f3ee89d/image.png" alt=""></p>
<hr>
<h2 id="2-프로젝트-구성">2. 프로젝트 구성</h2>
<p><code>초기 Architecture 설계도</code>
<img src="https://velog.velcdn.com/images/sunny-10/post/192e1c6a-0863-469a-a713-a024c2674755/image.png" alt=""></p>
<p><code>네트워크</code>|<code>IPv4 CIDR</code>
|:--:|:--:|
VPC|<code>10.0.0.0/16</code>
public subnet|<code>10.0.10.0/24</code>
private subnet|<code>10.0.30.0/24</code>
</br></p>
<table>
<thead>
<tr>
<th></th>
<th>Public</th>
<th>Private</th>
</tr>
</thead>
<tbody><tr>
<td>Jenkins</td>
<td>13.125.140.70</td>
<td>10.0.10.206</td>
</tr>
<tr>
<td>Ansible</td>
<td>13.209.20.29</td>
<td>10.0.10.209</td>
</tr>
<tr>
<td>Docker</td>
<td>3.38.107.141</td>
<td>10.0.10.95</td>
</tr>
<tr>
<td>Kubernetes</td>
<td>3.38.192.209</td>
<td>10.0.10.248</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-cicd를-위한-기본-인프라-구성">3. CI/CD를 위한 기본 인프라 구성</h2>
<h3 id="3-1-kubernetes-cluster-구성">3-1. Kubernetes Cluster 구성</h3>
<p><code></code>|<code>kubeadm</code>|<code>kubelet</code>|<code>kubectl</code>
|:--:|:--:|:--:|:--:|
<code>버전</code>|<code>1.22.8</code>|<code>1.22.8</code>|<code>1.22.8</code></p>
<br/>

<p>구성|설정값
|:--:|:--:|
설치방법|Kubeadm
pod-network-cidr|172.16.0.0/16</p>
<br/>

<h3 id="3-2-build-및-deployment-serverjenkins-ansible-docker-구성">3-2. Build 및 Deployment Server(Jenkins, Ansible, Docker) 구성</h3>
<h4 id="3-2-1-기본-terraform-구성">3-2-1) 기본 terraform 구성</h4>
<p><code>vi provider.tf</code> : Infrastructure의 type</p>
<pre><code>terraform {
  required_providers {
    aws = {
      source  = &quot;hashicorp/aws&quot;
      version = &quot;~&gt; 3.0&quot;
    }
  }
}

provider &quot;aws&quot; {
  region = &quot;ap-northeast-2&quot;
}</code></pre><br/>



<p><code>vi vpc.tf</code> : 가용영역1개, public2개, private1개 생성</p>
<pre><code>module &quot;project1_vpc&quot; {
  source = &quot;terraform-aws-modules/vpc/aws&quot;

  name = &quot;project1_vpc&quot;

  cidr = &quot;10.0.0.0/16&quot;

  azs             = [&quot;ap-northeast-2a&quot;]
  public_subnets  = [&quot;10.0.10.0/24&quot;, &quot;10.0.20.0/24&quot;]
  private_subnets = [&quot;10.0.30.0/24&quot;]

  create_database_subnet_group = true

  create_igw = true

  enable_nat_gateway = true
  single_nat_gateway = true

}
</code></pre><br/>



<p><code>vi ec2.tf</code> : 4대의 ec2 생성</p>
<pre><code>resource &quot;aws_key_pair&quot; &quot;project1_key&quot; {
  key_name   = &quot;project1_key&quot;
  public_key = file(&quot;/home/vagrant/.ssh/id_rsa.pub&quot;)
}

resource &quot;aws_instance&quot; &quot;jenkins&quot; {
  ami                    = &quot;ami-058165de3b7202099&quot;
  availability_zone      = module.project1_vpc.azs[0]
  instance_type          = &quot;t2.medium&quot;
  vpc_security_group_ids = [aws_security_group.all-sg.id]
  subnet_id              = module.project1_vpc.public_subnets[0]
  key_name               = aws_key_pair.project1_key.key_name

  tags = {
    Name = &quot;jenkins&quot;
  }
}


resource &quot;aws_instance&quot; &quot;ansible&quot; {
  ami                    = &quot;ami-058165de3b7202099&quot;
  availability_zone      = module.project1_vpc.azs[0]
  instance_type          = &quot;t2.micro&quot;
  vpc_security_group_ids = [aws_security_group.all-sg.id]
  subnet_id              = module.project1_vpc.public_subnets[0]
  key_name               = aws_key_pair.project1_key.key_name

  tags = {
    Name = &quot;ansible&quot;
  }
}

resource &quot;aws_instance&quot; &quot;docker&quot; {
  ami                    = &quot;ami-058165de3b7202099&quot;
  availability_zone      = module.project1_vpc.azs[0]
  instance_type          = &quot;t2.micro&quot;
  vpc_security_group_ids = [aws_security_group.all-sg.id]
  subnet_id              = module.project1_vpc.public_subnets[0]
  key_name               = aws_key_pair.project1_key.key_name

  tags = {
    Name = &quot;docker&quot;
  }
}


resource &quot;aws_instance&quot; &quot;k8s&quot; {
  ami                    = &quot;ami-058165de3b7202099&quot;
  availability_zone      = module.project1_vpc.azs[0]
  instance_type          = &quot;t2.medium&quot;
  vpc_security_group_ids = [aws_security_group.all-sg.id]
  subnet_id              = module.project1_vpc.public_subnets[0]
  key_name               = aws_key_pair.project1_key.key_name

  tags = {
    Name = &quot;k8s&quot;
  }
}
</code></pre><br/>



<p><code>vi sg.tf</code> : 모든 port를 열어 놓음</p>
<pre><code>resource &quot;aws_security_group&quot; &quot;all-sg&quot; {
  name        = &quot;all-sg&quot;
  description = &quot;Allow all &quot;
  vpc_id      = module.project1_vpc.vpc_id

  ingress {
    cidr_blocks = [&quot;0.0.0.0/0&quot;]
    from_port   = 0
    to_port     = 0
    protocol    = &quot;-1&quot;
  }

  egress {
    cidr_blocks = [&quot;0.0.0.0/0&quot;]
    from_port   = 0
    protocol    = &quot;-1&quot;
    to_port     = 0
  }
}</code></pre><br/>



<h4 id="3-2-2-ansible구성">3-2-2) Ansible구성</h4>
<ul>
<li>bastionhost 
-- hosts.ini
-- playbook
   ㄴ installJenkins.yml
   ㄴ installDoker.yaml
   ㄴ installk8s.yaml<br/>

</li>
</ul>
<h4 id="3-2-3-jenkins-build">3-2-3) Jenkins build</h4>
<pre><code>- hosts: jenkins_host

  tasks:
    - shell: sudo apt-get update
      ignore_errors: yes
    - shell: sudo apt install -y openjdk-11-jdk
    - shell: curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io.key | sudo tee /usr/share/keyrings/jenkins-keyring.asc &gt; /dev/null
    - shell: echo &quot;deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/&quot; | sudo tee /etc/apt/sources.list.d/jenkins.list &gt; /dev/null
    - shell: sudo apt-get update
      ignore_errors: yes
    - command: apt install -y fontconfig jenkins
    - command: apt install -y maven</code></pre><br/>

<h4 id="3-2-4-docker-build">3-2-4) Docker build</h4>
<pre><code>- name: Docker VM Provisioning
  hosts: docker_host
  gather_facts: false

  tasks:
    - command: apt update
      # 사용 가능한 패키지와 그 버전 리스트 업데이트

    - command: apt install -y ca-certificates curl gnupg lsb-release
      # docker 설치

    - command: apt install -y python3-pip
      # python3용 pip 설치 및 python 모듈 구축에 필요한 모든 종속성 설치

    - shell: curl https://get.docker.com | sh
      # docker 설치

    - shell: usermod -aG docker ubuntu
      ubuntu 사용자를 doker 그룹에 추가

    - pip:
        name:
          - docker
          - docker-compose
          # pip를 사용한 docker, docker-compose 설치
</code></pre><br/>

<h4 id="3-2-5-ansible-build">3-2-5) ansible build</h4>
<pre><code>- name: Ansible VM Provisioning
  hosts: ansible_host
  gather_facts: false

  tasks:
    - command: apt update
    - command: apt install -y ca-certificates curl gnupg lsb-release
    - command: apt install -y python3-pip
    - shell: curl https://get.docker.com | sh
    - shell: usermod -aG docker ubuntu
    - pip:
        name:
          - docker
          - docker-compose
    - command: apt install -y ansible
    - command: apt install -y python3-pip
    - shell: sed -i &#39;s/PasswordAuthentication no/PasswordAuthentication yes/g&#39; /etc/ssh/sshd_config
    - shell: pip install openshift==0.11
    - shell: echo &#39;ubuntu:ubuntu&#39; | chpasswd
    - shell: sudo systemctl restart ssh
    - shell: mkdir /home/ubuntu/.kube
    - shell: curl -LO https://dl.k8s.io/release/v1.22.8/bin/linux/amd64/kubectl
    - shell: sudo install kubectl /usr/local/bin/
   #- shell: scp ~/.kube/config .kube/config</code></pre><br/>

<h4 id="3-2-6-k8s-build">3-2-6) k8s build</h4>
<pre><code>- name: Contorl-Plane VM Provisioning
  hosts: controlplane_host
  gather_facts: false

  tasks:
    - command: apt update
    - command: apt install -y ca-certificates curl gnupg lsb-release
    - command: apt install -y python3-pip
    - shell: curl https://get.docker.com | sh
    - shell: usermod -aG docker ubuntu
    - pip:
        name:
          - docker
          - docker-compose
    - command: apt-get install -y apt-transport-https ca-certificates curl
    - shell: curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg
    - shell: echo &quot;deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main&quot; | sudo tee /etc/apt/sources.list.d/kubernetes.list
    - command: apt-get update
    - shell: sudo apt-get install kubeadm=1.22.8-00 kubelet=1.22.8-00 kubectl=1.22.8-00 -y
    - copy:
        src: &quot;/home/ubuntu/daemon.json&quot;
        dest: &quot;/etc/docker/&quot;
    - shell: sudo systemctl restart docker
    - shell: sudo systemctl daemon-reload &amp;&amp; sudo systemctl restart kubelet
    - shell: IPADDR=`ip addr | tail -n 8 | head -n 1 | cut -f 6 -d&#39; &#39; | cut -f 1 -d &#39;/&#39;`
    - shell: sudo kubeadm init --control-plane-endpoint &quot;{{ lookup(&#39;env&#39;, &#39;IPADDR&#39;) }}&quot; --pod-network-cidr 172.16.0.0/16 --apiserver-advertise-address &quot;{{ lookup(&#39;env&#39;, &#39;IPADDR&#39;) }}&quot;
    - shell: mkdir -p /home/ubuntu/.kube
    - shell: sudo cp -i /etc/kubernetes/admin.conf /home/ubuntu/.kube/config
    - shell: sudo chown ubuntu:ubuntu /home/ubuntu/.kube/config
    - fetch:
        src: &quot;/home/ubuntu/.kube/config&quot;
        dest: &quot;/home/ubuntu/.kube/config&quot;
        flat: yes
   #- shell: kubectl create -f https://projectcalico.docs.tigera.io/manifests/tigera-operator.yaml
    - shell: curl https://projectcalico.docs.tigera.io/manifests/custom-resources.yaml -O
    - replace:
        path: /home/ubuntu/custom-resources.yaml
        regexp: 192.168
        replace: 172.16
   #- shell: kubectl create -f custom-resources.yaml</code></pre><h4 id="3-2-6-worker-node-build">3-2-6) worker node build</h4>
<pre><code>- name: Contorl-Plane VM Provisioning
  hosts: controlplane_host
  gather_facts: false

  tasks:
    - command: apt update
    - command: apt install -y ca-certificates curl gnupg lsb-release
    - command: apt install -y python3-pip
    - shell: curl https://get.docker.com | sh
    - shell: usermod -aG docker ubuntu
    - pip:
        name:
          - docker
          - docker-compose
    - command: apt-get install -y apt-transport-https ca-certificates curl
    - shell: curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg
    - shell: echo &quot;deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main&quot; | sudo tee /etc/apt/sources.list.d/kubernetes.list
    - command: apt-get update
    - shell: sudo apt-get install kubeadm=1.22.8-00 kubelet=1.22.8-00 kubectl=1.22.8-00 -y
    - copy:
        src: &quot;/home/ubuntu/daemon.json&quot;
        dest: &quot;/etc/docker/&quot;
    - shell: sudo systemctl restart docker
    - shell: sudo systemctl daemon-reload &amp;&amp; sudo systemctl restart kubelet</code></pre><pre><code>#! /bin/sh
sudo kubeadm join 10.0.10.248:6443 --token u3adz9.flbop6nslkaupqrq --discovery-token-ca-cert-hash sha256:70f23d516ea80a39c784d129bddb13d6f71a96865b97acac573054443183b355</code></pre><br/>

<hr>
<h2 id="4-cicd-구현">4. ci/cd 구현</h2>
<h3 id="4-1-kubernetes-cluster-pod-배포를-위한-ansible-playbook">4-1. Kubernetes Cluster Pod 배포를 위한 Ansible Playbook</h3>
<h4 id="4-1-1-kubernetes-playbook">4-1-1) kubernetes playbook</h4>
<p><code>vi docker_build_and_push.yaml</code> : DockerHub에서 image build 및 push</p>
<pre><code>- name: Docker Image Build and Push
  hosts: docker_host
  gather_facts: false

  tasks:
    - command: docker image build -t repush/cicdproject:&quot;{{ lookup(&#39;env&#39;, &#39;BUILD_NUMBER&#39;) }}&quot; ~/
    - command: docker login -u repush -p &quot;{{ lookup(&#39;env&#39;, &#39;TOKEN&#39;) }}&quot;
    - command: docker push repush/cicdproject:&quot;{{ lookup(&#39;env&#39;, &#39;BUILD_NUMBER&#39;) }}&quot;
    - command: docker logout</code></pre><ul>
<li>첫번째 command: image build command, 변수를 통해 버전을 지정 (중복이 되지 않도록 build 횟수로 버전 생성)</li>
<li>두번째 command: DockerHub login command, 변수를 통해 Token을 지정</li>
<li>세번째 command: push command, 변수를 통해 버전을 지정 (중복이 되지 않도록 build 횟수로 버전 생성)</li>
<li>네번째 command: logout command
<br/><br/></li>
</ul>
<p><code>vi kube_deploy.yaml</code> : k8s deployment 및 service</p>
<pre><code>- hosts: ansible_host
  gather_facts: no

  tasks:
    #- command: kubectl apply -f java-hello-world/kube_manifest/
    - name: Create Deployment                        # container 배포
      k8s:
        state: present
        definition:
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: java-hello
            namespace: default
          spec:
            replicas: 6                              # pod 개수 지정
            selector:
              matchLabels:
                app: java-hello
            template:
              metadata:
                labels:
                  app: java-hello
              spec:
                containers:
                  - name: java-hello
                    image: &quot;repush/cicdproject:{{ lookup(&#39;env&#39;, &#39;BUILD_NUMBER&#39;) }}&quot;       # DockerHub에 push된 image 
                    imagePullPolicy: Always
                    ports:
                      - containerPort: 8080           # containerPort지정
    - name: Create Service                            # service 배포
      k8s:
        state: present
        definition:
          apiVersion: v1
          kind: Service
          metadata:
            name: java-hello-svc
            namespace: default
          spec:
            type: NodePort
            selector:
              app: java-hello
            ports:
              - port: 80
                targetPort: 8080
                nodePort: 31313                        # nodePort 지정</code></pre><p><br/><br/></p>
<h3 id="4-2-docker-image-build를-위한-이미지용-dockerfile">4-2. Docker Image Build를 위한 이미지용 Dockerfile</h3>
<h4 id="4-2-1-dockerfile">4-2-1) Dockerfile</h4>
<p><code>vi Dockerfile</code></p>
<pre><code>FROM tomcat:9.0-jre11-openjdk                # tomcat:9.0-jre11-openjdk 이미지를 가져옴

COPY webapp.war /usr/local/tomcat/webapps    # 현재 경로의 war파일을 이미지 안에 /usr/local/tomcat/ 경로에 복사</code></pre><br/>


<h3 id="4-3-cicd-구현을-위한-jenkins-job-구성">4-3. ci/cd 구현을 위한 Jenkins Job 구성</h3>
<h4 id="4-3-1-사용된-플러그인">4-3-1) <strong>사용된 플러그인</strong></h4>
<ul>
<li>Maven Integration plugin 3.19</li>
<li>Maven Invoker plugin 2.4</li>
<li>Publish Over SSH 1.24</li>
</ul>
<h4 id="4-3-2-java-project-build를-위한-jdk환경변수-세팅">4-3-2) <strong>JAVA Project Build를 위한 JDK환경변수 세팅</strong></h4>
<p><em>Jenkins 관리 - Global Tool Configuration - JDK</em>
<img src="https://velog.velcdn.com/images/skyvault05/post/fba8a09f-0b29-4bf4-88da-0cbde91497a3/image.png" alt=""></p>
<h4 id="4-3-3-java-project-build를-위한-maven환경변수-세팅">4-3-3) <strong>JAVA Project Build를 위한 Maven환경변수 세팅</strong></h4>
<p><em>Jenkins 관리 - Global Tool Configuration - Maven</em></p>
<p><img src="https://velog.velcdn.com/images/skyvault05/post/e372ae5a-f218-4f38-9777-081e28ac0f43/image.png" alt=""></p>
<h4 id="4-3-4-cicd-구현을-위한-junkins-job-설정">4-3-4) <strong>ci/cd 구현을 위한 Junkins Job 설정</strong></h4>
<p><em>새로운 Item - Pipeline</em></p>
<h5 id="1-dockerhub-로그인을-위한-로그인-토큰-설정"><strong>1) DockerHub 로그인을 위한 로그인 토큰 설정</strong></h5>
<p><em>General</em></p>
<ul>
<li>Default Value: DockerHub 계정의 Token
<img src="https://velog.velcdn.com/images/skyvault05/post/9111f9a6-79cb-4ffa-921f-db10b3599626/image.png" alt=""></li>
</ul>
<h5 id="2-프로젝트의-브랜치-변화-감지를-위한-build-triggers-설정"><strong>2) 프로젝트의 브랜치 변화 감지를 위한 Build Triggers 설정</strong></h5>
<p><em>Build Triggers</em></p>
<ul>
<li>매 분마다 Github의 Push Event감지하는 Polling설정
<img src="https://velog.velcdn.com/images/skyvault05/post/5304c730-1b28-4ef8-9379-93b5a9e28745/image.png" alt=""></li>
</ul>
<h5 id="3-pipeline과-jenkins파일-구성"><strong>3) Pipeline과 Jenkins파일 구성</strong></h5>
<p><em>Pipiline - Definition - SCM</em></p>
<p><img src="https://velog.velcdn.com/images/skyvault05/post/f734cbf3-f65f-4cda-a1d0-a0ae31825f45/image.png" alt=""></p>
<p><em>Pipiline - Definition - Script Path</em></p>
<ul>
<li>Github Repository 내의 jenkinsfile 상대경로</li>
</ul>
<p><img src="https://velog.velcdn.com/images/skyvault05/post/9a87b42d-1ed8-4346-b010-713fb62ed5c4/image.png" alt=""></p>
<p><em>Jenkinsfile 구성</em></p>
<ul>
<li>Java Build stage: Maven을 통해 Java Project Archive파일 빌드</li>
<li>Docekr Image Build With Remote Ansible Server AND Remote Docker Server Using Publish Over SSH Module: Publish Over SSH Module모듈로 Docker Image 빌드를 위한 파일을 Ansible서버에 전송 후 Docker서버에 전송, Ansible 서버에 Docker Image 빌드와 Project 배포 실행을 위한 Ansible Playbook 전송 및 실행.</li>
</ul>
<p><code>jenkinsfile</code></p>
<pre><code>pipeline {
    agent any

    tools {
        // Install the Maven version configured as &quot;M3&quot; and add it to the path.
        maven &quot;M2_HOME&quot;
    }

    stages {
        stage(&#39;Java Build&#39;) {
            steps {
                // Run Maven on a Unix agent.
                sh &quot;mvn -Dmaven.test.failure.ignore=true clean package -f pom.xml&quot;
            }
        }
        stage(&#39;Docekr Image Build With Remote Ansible Server AND Remote Docker Server Using Publish Over SSH Module&#39;) {
            steps {
                sshPublisher(publishers: [sshPublisherDesc(configName: &#39;ansible-host&#39;, transfers: [sshTransfer(cleanRemote: false, excludes: &#39;&#39;, execCommand: &#39;&#39;, execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: &#39;[, ]+&#39;, remoteDirectory: &#39;java-hello-world&#39;, remoteDirectorySDF: false, removePrefix: &#39;webapp/target/&#39;, sourceFiles: &#39;webapp/target/webapp.war&#39;), sshTransfer(cleanRemote: false, excludes: &#39;&#39;, execCommand: &#39;&#39;, execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: &#39;[, ]+&#39;, remoteDirectory: &#39;java-hello-world&#39;, remoteDirectorySDF: false, removePrefix: &#39;docker/&#39;, sourceFiles: &#39;docker/Dockerfile&#39;), sshTransfer(cleanRemote: false, excludes: &#39;&#39;, execCommand: &#39;&#39;&#39;scp java-hello-world/Dockerfile 13.125.234.12:~/
scp java-hello-world/webapp.war 13.125.234.12:~/
TOKEN=`echo $TOKEN` BUILD_NUMBER=`echo $BUILD_NUMBER` ansible-playbook java-hello-world/docker_build_and_push.yaml
BUILD_NUMBER=`echo $BUILD_NUMBER` ansible-playbook java-hello-world/kube_deploy.yaml&#39;&#39;&#39;, execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: &#39;[, ]+&#39;, remoteDirectory: &#39;java-hello-world&#39;, remoteDirectorySDF: false, removePrefix: &#39;playbook/&#39;, sourceFiles: &#39;playbook/*.yaml&#39;)], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
            }
        }
    }
}</code></pre><p><br/><br/></p>
<h2 id="5-구현-결과">5. 구현 결과</h2>
<h3 id="5-1-jenkins-build">5-1. Jenkins build</h3>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/2bafaa74-b194-48a8-ac83-723e3466d022/image.png" alt=""></p>
<h3 id="5-2-로컬에서-접근">5-2. 로컬에서 접근</h3>
<p><img src="https://velog.velcdn.com/images/repush/post/d05b8532-8703-41f9-9712-0f45b4d57360/image.png" alt=""></p>
<h3 id="5-3-웹사이트에서-접근">5-3. 웹사이트에서 접근</h3>
<p><img src="https://velog.velcdn.com/images/repush/post/67da4a67-d3d5-4810-9a41-87a3fe292efd/image.png" alt=""></p>
<h2 id="6-결론">6. 결론</h2>
<h3 id="6-1-as-is">6-1. As-is</h3>
<ul>
<li><p>기본적으로 이번 프로젝트에서 <code>Jenkins</code>를 통한 <code>GitOps</code> <code>CI/CD</code>는 성공했다.</p>
</li>
<li><p><code>Terraform</code>과 <code>Ansible</code>을 사용하여 빌드를 <code>자동화</code>하는데 어느정도 구축은 했다.</p>
</li>
</ul>
<br/>

<h3 id="62-to-be">6.2. To-be</h3>
<ul>
<li>앤서블 플레이북 작성시 몇몇 명령어가 실행되지 못한 점이 아쉬웠다.</li>
<li>앤서블 플레이북을 지금은 <code>shell</code>과 <code>command</code>위주로 사용했는데 <code>모듈</code>을 사용하면 더 완성도있는 결과를 낼 수 있었을것같다.</li>
<li>로드밸드밸런서와 오토스케일링을 활용한 웹서비스 배포를 <code>GUI</code>환경에서는 성공했지만 마지막에 <code>IaC</code>로 구현한 인프라에서는 작동하지 못한 점이 아쉬웠다.</li>
<li>보안그룹을 지금은 간단하게 열어놓았지만 좀더 최적화해서 보안그룹을 구성하면 더 좋을 것 같다.</li>
<li>workernode를 오토스케일링 하여 자동으로 추가하는 작업도 <code>GUI</code>환경에서는 성공했지만 <code>IaC</code>로 구현한 인프라에서는 작동하지 못한 점 또한 아쉽게 되었다.</li>
<li>배스천 호스트를 오토스케일링하고 네트워크 로드밸런서로 구성하여 도메인 네임 엔드포인트로 ssh는 것도 <code>GUI</code>환경에서 성공했지만 <code>IaC</code>로 구현하면서 시간이 부족하여 구성하지 못한 점도 아쉽게 되었다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[DevOps 실현을 위한 CI/CD 구축 (22.06.07)]]></title>
            <link>https://velog.io/@sunny-10/22.06.07</link>
            <guid>https://velog.io/@sunny-10/22.06.07</guid>
            <pubDate>Tue, 07 Jun 2022 06:46:27 GMT</pubDate>
            <description><![CDATA[<h1 id="argocd">ArgoCD</h1>
<p>GitOps의 구현체</p>
<p>Argo공식site:
<a href="https://argo-cd.readthedocs.io/en/stable/">https://argo-cd.readthedocs.io/en/stable/</a></p>
<h4 id="1-install-argo-cd">1) Install Argo CD</h4>
<p>Argo설치를 위해선 (kubernetes 필요)</p>
<ul>
<li>Installed kubectl command-line tool.</li>
<li>Have a kubeconfig file (default location is ~/.kube/config).<pre><code>kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml</code></pre><img src="https://velog.velcdn.com/images/sunny-10/post/61767e06-aec4-4f05-a4b4-21067cb414b0/image.PNG" alt=""></li>
</ul>
<h4 id="2-download-argo-cd-cli">2) Download Argo CD CLI</h4>
<pre><code>curl -sSL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64

chmod +x /usr/local/bin/argocd</code></pre><p>cli 설치 링크: 
<a href="https://argo-cd.readthedocs.io/en/stable/cli_installation/">https://argo-cd.readthedocs.io/en/stable/cli_installation/</a></p>
<pre><code>kubectl get all -n argocd
# argocd-server    Cluster-IP 확인</code></pre><p>외부에서 접근하도록 설정 필요.
<img src="https://velog.velcdn.com/images/sunny-10/post/27f3305a-227c-4d79-8e6f-9bcc9df1820c/image.PNG" alt=""></p>
<h4 id="3-argo-cd-서비스-노출">3) argo CD 서비스 노출</h4>
<ul>
<li>로드밸런서 타입으로 변경<pre><code>kubectl patch svc argocd-server -n argocd -p &#39;{&quot;spec&quot;: {&quot;type&quot;: &quot;LoadBalancer&quot;}}&#39;
</code></pre></li>
</ul>
<p>kubectl describe svc argocd-server -n argocd</p>
<h1 id="load-balancer-port-확인">Load Balancer Port 확인</h1>
<pre><code>![](https://velog.velcdn.com/images/sunny-10/post/4ec8b3c9-3615-4bc1-986f-7aae01182714/image.PNG)

#### 4) 웹 브라우저에서 login
웹브라우저에서 [master_IP]:[port]
    예)  192.168.59.10:31958
https 로 redirect됨.


web UI의 로그인 계정은 admin
암호는 확인해서 입력</code></pre><p>kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath=&quot;{.data.password}&quot; | base64 -d; echo</p>
<pre><code>[문자들이 나옴]= 암호
![](https://velog.velcdn.com/images/sunny-10/post/2f83bc7a-d04a-4689-a868-67008faeb379/image.PNG)![](https://velog.velcdn.com/images/sunny-10/post/ad26c7d7-1fe7-4b01-9a67-030bbe6b3f43/image.PNG)


#### 5) CLI도 argocd 로그인</code></pre><p>argocd 엔터
argocd login --insecure <ARGOCD_SERVER_DOMAIN>:<PORT>
                       192.168.100.100:65001</p>
<pre><code>login id : admin
password : (위에서 확인한 문자)

![](https://velog.velcdn.com/images/sunny-10/post/31d5e93c-21b1-4d6f-9a19-447fe0a8bbd7/image.PNG)

#### 6) application 생성하기 - CLI</code></pre><p>argocd app create guestbook --repo <a href="https://github.com/argoproj/argocd-example-apps.git">https://github.com/argoproj/argocd-example-apps.git</a> --path guestbook --dest-server <a href="https://kubernetes.default.svc">https://kubernetes.default.svc</a> --dest-namespace default</p>
<pre><code>source : (git) --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook 

destination ; local kubernetes cluster
    --dest-server https://kubernetes.default.svc --dest-namespace 
![](https://velog.velcdn.com/images/sunny-10/post/5439a4bf-4e1b-4b34-aca6-b8f3f4aed58f/image.PNG)


#### 7) CLI로 application 확인</code></pre><p>argocd app get guestbook</p>
<pre><code>webui로 확인하시면 노란색 (동기화 안됨)

CLI 동기화 하기 </code></pre><p>argocd app sync guestbook
argocd app get guestbook</p>
<pre><code>![](https://velog.velcdn.com/images/sunny-10/post/d995350c-f1bd-430a-8aa4-e0c79994ee91/image.PNG)![](https://velog.velcdn.com/images/sunny-10/post/52925320-8fda-4b6c-b80e-fec7fdebb9be/image.PNG)


ArgoCD web UI에서 guestbook application을 삭제해보기
kubernetes 확인


#### 8) guestbook appliname을 WEB UI에서 생성하기
web UI에서 동기화


git------------------kubernetes cluster
        applicaton

git 로그인 repository 생성

guestbook-ui-deployment.yaml</code></pre><p>apiVersion: apps/v1
kind: Deployment
metadata:
  name: guestbook-ui
spec:
  replicas: 1
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: guestbook-ui
  template:
    metadata:
      labels:
        app: guestbook-ui
    spec:
      containers:
      - image: gcr.io/heptio-images/ks-guestbook-demo:0.2
        name: guestbook-ui
        ports:
        - containerPort: 80</p>
<pre><code>guestbook-ui-svc.yaml</code></pre><p>apiVersion: v1
kind: Service
metadata:
  name: guestbook-ui
spec:
  ports:</p>
<ul>
<li>port: 80
targetPort: 80
selector:
app: guestbook-ui<pre><code>![](https://velog.velcdn.com/images/sunny-10/post/46028ac5-29a5-4d09-a0e3-57333c7ca862/image.PNG)
/guestbook/ 하위에 등록
</code></pre></li>
</ul>
<h4 id="9-git-repo와-kubernetes-cluster를-연동하는-application을-생성해보기">9) git repo와 kubernetes cluster를 연동하는 application을 생성해보기</h4>
<p>source : git URL 
https  :  <a href="https://github.com/soyoung-2020/argocd.git">https://github.com/soyoung-2020/argocd.git</a>  (**)
ssh   : <a href="mailto:git@github.com">git@github.com</a>:Sunny-1030/argocd.git
 PATH :  repository 하위 디렉토리 명 . guestbook
    repository 하위에 바로 yaml 파일이있을 경우에는  .
    project : default</p>
<p>destination
   <a href="https://kuberntes.default.svc">https://kuberntes.default.svc</a>
   namespace : default (다른 이름이 지정이 가능하나, 미리 생성해야함)</p>
<p>application name : myguestbook</p>
<p>myguestbook을 kubernetes cluster와 동기화 </p>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/00fb5e3a-7a2e-4ca6-801b-03cd648c6eed/image.PNG" alt=""></p>
<h4 id="10-git-repository---guestbook-ui-deploymentyaml">10) git repository - guestbook-ui-deployment.yaml</h4>
<p>1차 변경
replicas: 1    ---&gt;   2 개<br>local pc라면 commit &gt;  push</p>
<p>git site에서 편집</p>
<p><img src="https://velog.velcdn.com/images/sunny-10/post/a8a57ca7-86c4-4607-a390-3e1cab7a8004/image.PNG" alt=""><img src="https://velog.velcdn.com/images/sunny-10/post/e4a17846-18d1-42cc-a94c-5118d323fff8/image.PNG" alt=""></p>
<h4 id="11-argocd-webui에서--git소스live소스를-diff-확인---동기화sync">11) argoCD webui에서  git소스/live소스를 diff 확인  &gt; 동기화sync</h4>
<pre><code>kubectl get all -n default</code></pre><p>Argocd WebUI  - application 내에서 상단에 “HISTORY”</p>
<p>현시점 외에 과거로 rollback 해보기(2개,1개),  동기화 하면 다시 현재로.(2개)
<img src="https://velog.velcdn.com/images/sunny-10/post/39a65546-d107-452b-9383-bc424b79d2ac/image.PNG" alt=""><img src="https://velog.velcdn.com/images/sunny-10/post/a3c7b55d-1503-4884-be78-2d7b546d1b8b/image.PNG" alt=""><img src="https://velog.velcdn.com/images/sunny-10/post/f9a5b933-cc2a-4ace-9b66-920b52755600/image.PNG" alt=""><img src="https://velog.velcdn.com/images/sunny-10/post/46ea6ad1-727b-4efb-bb33-0c4f874b07ad/image.PNG" alt=""></p>
<p>rollback 시 이전(1개) 으로 돌아가며 sync(동기화)하면 원 상태(2개)로 복구됨</p>
<h4 id="12-kubernetes-manifest-를-동기화-해보자">12) kubernetes manifest 를 동기화 해보자.</h4>
<p>자신의 git에 repository 생성</p>
<p>nginx-svc.yaml</p>
<pre><code>apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  type:  LoadBalancer    # 서비스 타입
  ports:
  - port: 8080       # 서비스 포트
    targetPort: 80   # 타켓, 즉 pod의 포트
    protocol: TCP
    name: http
  selector:
    app: nginx</code></pre><p>nginx-deployment.yaml</p>
<pre><code>apiVersion: apps/v1           # 쿠버네티스 api 버전
kind: Deployment              # 생성할 오브젝트 종류
metadata:                
  name: nginx-deployment      # deployment의 이름
  labels:
    app: nginx                # label 지정
spec:                         # deployment의 스펙을 정의
  replicas: 3                 # 3개의 pod 설정
  selector:                   # deployment가 관리할 pod를 찾는 방법을 정의
    matchLabels:     
      app: nginx
  template:
    metadata:
      labels:                 # pod의 label
        app: nginx
    spec:
      containers:             # 컨테이너 설정
      - name: nginx          
        image: nginx:1.14.2
        ports:
        - containerPort: 80</code></pre><p><img src="https://velog.velcdn.com/images/sunny-10/post/267151a3-716f-47ab-a00c-4b9ca4b98554/image.PNG" alt=""><img src="https://velog.velcdn.com/images/sunny-10/post/c0b1559e-8d50-4213-beac-eb4eaf812b3a/image.PNG" alt=""><img src="https://velog.velcdn.com/images/sunny-10/post/179969d5-1d02-474f-a329-1ad6611d1704/image.PNG" alt=""><img src="https://velog.velcdn.com/images/sunny-10/post/7e47f2bd-6109-45dd-aa93-4f40cb52ea50/image.PNG" alt=""></p>
<h4 id="13-templates-이용하는-방법----helm-helm-chart">13) templates 이용하는 방법  - helm (helm chart)</h4>
<p>application 추가
repository :    git  -&gt; helm</p>
<p>repository : <a href="https://github.com/argoproj/argocd-example-apps.git">https://github.com/argoproj/argocd-example-apps.git</a>
path  : apps
chart : repository를 인식하면 자동으로 목록 뜸.  Chart.yaml 선택</p>
<p>하단에 helm 설정파트가 있음 values: values.yaml (또는 values.yaml파일을 보제하여 값을 변경하고자하는 셋팅가능)</p>
<p>repository 
-HTTPS git연결
-SSH git 연결 
    key 생성
    github : public key 등록
    argocd : private 등록, repository 등록 -&gt; application 연결에 사용</p>
<hr>
]]></description>
        </item>
    </channel>
</rss>