<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>GG.Lab</title>
    <link>https://gglab.tistory.com/</link>
    <description>GG.Lab</description>
    <language>ko</language>
    <pubDate>Sun, 10 May 2026 20:57:50 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>GG.Lab</managingEditor>
    <image>
      <title>GG.Lab</title>
      <url>https://tistory1.daumcdn.net/tistory/4161963/attach/3ea6b2064dc940568d49518937aab1c5</url>
      <link>https://gglab.tistory.com</link>
    </image>
    <item>
      <title>pgpool2 를 이용한 이중화 셋팅 (k3s)</title>
      <link>https://gglab.tistory.com/57</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;얼마전에 DB 서버 2대에 patroni를 이용하여 이중화를 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;patroni는 서버를 모니터하면서 master 와 slave 를 관리해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 이 서버에 적절히 분배해 주면서 앞단에서 로드밸런서 역할도 하고 쿼리 종류에 따라 Write 는 master 로 Read 는 두 서버를 적절히 분산을... 하기위해 pgpool 을 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리를 분석해서 자동 분기를 해주는 솔루션은 pgpool 이 거의 유일하다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 WAS 의 소스코드 레벨에서 ORM 등을 사용하여 두개의 역할을 나눠서 알아서 나눠서 호출하게 할수는 있지만 이미 만들어진 WAS서버에서 다시 고쳐서 쓰기는 번거롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동으로 해주는 솔루션을 찾다보니 pgpool 을 이용하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일전에 했다가..별로여서 안쓰고 있었는데 학습차원에서 다시 했고 k3s 에 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 굉장히 많은 시행 착오를 겪었다. 오늘하루를 다 썼는데...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 기록을 남기려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 k3s 서버에 아래 두개의 파일을 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;귀찮아서 몇개를 합치긴 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1766748899292&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;drwxr-xr-x 2 root root 4096 Dec 26 10:46 ./
drwxr-xr-x 7 root root 4096 Dec 26 02:38 ../
-rw-r--r-- 1 root root 5471 Dec 26 10:46 deployment.yaml
-rw-r--r-- 1 root root  378 Dec 26 06:33 service.yaml
root@k3s-node1:/k8s/apps/pgpool#&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 디플로이&lt;/p&gt;
&lt;pre id=&quot;code_1766749015766&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;root@k3s-node1:/k8s/apps/pgpool# cat deployment.yaml 
# ---------------------------------------------------------
# 1. ConfigMap 정의 (pool_hba.conf 설정)
# ---------------------------------------------------------
apiVersion: v1
kind: ConfigMap
metadata:
  name: pgpool-hba-config
  namespace: default
data:
  pool_hba.conf: |
    # 로컬 접속 허용
    local   all             all                                     trust
    # 외부 접속 허용 (md5 인증 사용)
    host    all             all             0.0.0.0/0               md5 

---
apiVersion: v1
kind: Secret
metadata:
  name: postgres-secrets
  namespace: default
type: Opaque
stringData:
  postgres-password: &quot;비번&quot;  # 실제 비밀번호로 변경
  replicator-password: &quot;비번&quot;   # 복제용 유저
---
apiVersion: v1
kind: Secret
metadata:
  name: pgpool-passwd
  namespace: default
type: Opaque
stringData:
  pool_passwd: |
    postgres:md~~~   # DB에 저장된 사용자의 암호화된 값
    rep_user:md~~~   # DB에 저장된 사용자의 암호화된 값
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pgpool
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pgpool
  template:
    metadata:
      labels:
        app: pgpool
    spec:
      # initContainer로 Secret 파일을 쓰기 가능한 공간(/config-work)으로 복사
      initContainers:
        - name: setup-config
          image: busybox
          # pool_passwd는 읽어야 하고, pool_hba.conf는 entrypoint가 수정해야 하므로 쓰기 권한도 필요함
          command: ['sh', '-c', 'cp /config-src/* /config-work/ &amp;amp;&amp;amp; chmod 666 /config-work/*']
          volumeMounts:
            - name: pool-passwd-vol
              mountPath: /config-src/pool_passwd
              subPath: pool_passwd
            - name: pool-hba-vol
              mountPath: /config-src/pool_hba.conf
              subPath: pool_hba.conf
            - name: pgpool-config-work
              mountPath: /config-work

      containers:
        - name: pgpool
          image: docker.io/pgpool/pgpool:4.4.3
          volumeMounts:
            # 복사된 쓰기 가능한 볼륨을 마운트
            - mountPath: /opt/pgpool-II/etc/pool_passwd
              name: pgpool-config-work
              subPath: pool_passwd
            - mountPath: /opt/pgpool-II/etc/pool_hba.conf
              name: pgpool-config-work
              subPath: pool_hba.conf
          env:
            - name: PGPOOL_PARAMS_BACKEND_HOSTNAME0
              value: &quot;10.34.1.111&quot;
            - name: PGPOOL_PARAMS_BACKEND_PORT0
              value: &quot;5432&quot;
            - name: PGPOOL_PARAMS_BACKEND_WEIGHT0
              value: &quot;1&quot;
            - name: PGPOOL_PARAMS_BACKEND_FLAG0
              value: &quot;ALLOW_TO_FAILOVER&quot;

            - name: PGPOOL_PARAMS_BACKEND_HOSTNAME1
              value: &quot;10.34.1.112&quot;
            - name: PGPOOL_PARAMS_BACKEND_PORT1
              value: &quot;5432&quot;
            - name: PGPOOL_PARAMS_BACKEND_WEIGHT1
              value: &quot;1&quot;
            - name: PGPOOL_PARAMS_BACKEND_FLAG1
              value: &quot;ALLOW_TO_FAILOVER&quot;

            # ... (기타 설정) ...
            - name: PGPOOL_PARAMS_NUM_INIT_CHILDREN
              value: &quot;100&quot;
            - name: PGPOOL_PARAMS_MAX_POOL
              value: &quot;4&quot;
            - name: PGPOOL_PARAMS_LOAD_BALANCE_MODE
              value: &quot;on&quot;
            - name: PGPOOL_PARAMS_SR_CHECK_PERIOD
              value: &quot;10&quot;
            - name: PGPOOL_PARAMS_SR_CHECK_USER
              value: &quot;rep_user&quot;
            - name: PGPOOL_PARAMS_SR_CHECK_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secrets
                  key: replicator-password
            - name: PGPOOL_PARAMS_SR_CHECK_DATABASE
              value: &quot;postgres&quot;
            - name: PGPOOL_PARAMS_HEALTH_CHECK_PERIOD
              value: &quot;10&quot;
            - name: PGPOOL_PARAMS_HEALTH_CHECK_USER
              value: &quot;postgres&quot;
            - name: PGPOOL_PARAMS_HEALTH_CHECK_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secrets
                  key: postgres-password
            - name: PGPOOL_PARAMS_HEALTH_CHECK_DATABASE
              value: &quot;postgres&quot;

            # [해결책 2] 인증 방식을 다시 SCRAM으로 변경 (백엔드와 통일)
            - name: PGPOOL_PARAMS_ENABLE_POOL_HBA
              value: &quot;on&quot;
            - name: PGPOOL_PARAMS_PWD_ENC_METHOD
              value: &quot;md5&quot;
            - name: PGPOOL_PARAMS_POOL_PASSWD
              value: &quot;/opt/pgpool-II/etc/pool_passwd&quot;
            - name: PGPOOL_PARAMS_PORT
              value: &quot;5432&quot;

          ports:
            - name: postgresql
              containerPort: 5432
            - name: pcp
              containerPort: 9898

          # Liveness/Readiness 등 기존 설정 유지...
          livenessProbe:
            tcpSocket:
              port: 5432
            initialDelaySeconds: 60
          readinessProbe:
            tcpSocket:
              port: 5432
            initialDelaySeconds: 30

      volumes:
        # [해결책 1] 작업을 위한 빈 볼륨 생성
        - name: pgpool-config-work
          emptyDir: {}
        # 원본 Secret 연결
        - name: pool-passwd-vol
          secret:
            secretName: pgpool-passwd
        - name: pool-hba-vol
          configMap:
            name: pgpool-hba-config&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 k3s 서비스&lt;/p&gt;
&lt;pre id=&quot;code_1766749038258&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;root@k3s-node1:/k8s/apps/pgpool# cat service.yaml 
apiVersion: v1
kind: Service
metadata:
  name: pgpool-lb
  namespace: default
  annotations:
    metallb.universe.tf/allow-shared-ip: &quot;shared-lb&quot;
spec:
  type: LoadBalancer
  selector:
    app: pgpool
  ports:
  - name: postgresql
    port: 5432
    targetPort: 5432
    protocol: TCP
  - name: pcp
    port: 9898
    targetPort: 9898
    protocol: TCP
  sessionAffinity: None&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service 에서 shared-lb 는 하나의 아이피로 포트만 다르게 여러 서비스를 하기 위해서 사용중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;metallb 를 이용하여 vip를 사용했는데 계속 해서 ip를 늘려갈수는 없으니까...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내에서 여유있는 ip 대역을 가지고 있다면 상관 없겠지만 클라우드 상에서 아이피를 늘리것은 다 돈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 최대한 아이피를 안 늘리는 상황으로 만들어 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 위와 같이 해서 vip 10.34.1.150 5432 포트로 연결 성공 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나하나 설명은 나도 모르니 패스...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 애를 먹었던 부분은 인증부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client -&amp;gt; pgpool -&amp;gt; db서버&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 인증을 한다고 하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client -&amp;gt; pgpool 을 통과한것 같은데 계속 해서 backend (DB를 말한다.) 에 인증이 안된다는 오류를 오늘 하루종일 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT 에 질문을 몇번을 했는지 모르겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766749367300&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT concat(usename, ':', passwd) FROM pg_shadow WHERE usename IN ('postgres', 'rep_user');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 DB에서 패스워드를 추출하여 상단 deployment.yaml 에 셋팅을 하게 되는데&lt;/p&gt;
&lt;pre id=&quot;code_1766749402041&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;postgres:SCRAM-SHA-256$4096:w1r3Sp4OL+LMrDdcp~~~
rep_user:SCRAM-SHA-256$4096:ngIDzkc8gUxBd6/vE~~~&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이런식으로 값이 나왔다. SCRAM 인증방식이고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두대의 서버 pg_hba 설정 파일에도&lt;/p&gt;
&lt;pre id=&quot;code_1766749443529&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# IPv4 local connections:
host    all             all             127.0.0.1/32            scram-sha-256
# IPv6 local connections:
host    all             all             ::1/128                 scram-sha-256
# Allow replication connections from localhost, by a user with the
# replication privilege.
local   replication     all                                     peer
host    replication     all             127.0.0.1/32            scram-sha-256
host    replication     all             ::1/128                 scram-sha-256&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 아무리 해도 안되더라...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 인증방식을 md5 로 바꿨다.&lt;/p&gt;
&lt;pre id=&quot;code_1766749497798&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 1. 현재 세션의 암호화 방식을 md5로 강제 설정
SET password_encryption = 'md5';

-- 2. postgres 유저 비밀번호 재설정 (다시 MD5로 해시됨)
-- (기존 비밀번호 '비번'를 그대로 입력)
ALTER USER postgres WITH PASSWORD '비번';

-- 3. rep_user 유저 비밀번호 재설정
ALTER USER rep_user WITH PASSWORD '비번';

-- 4. 확인 (rolpassword가 'md5...' 로 시작해야 성공)
SELECT rolname, rolpassword FROM pg_authid WHERE rolname IN ('postgres', 'rep_user');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 실제 값도 바뀌어져 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1766749520109&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;postgres:md5~~
rep_user:md5~~&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 최종적으로 상단의 설정으로 바꿨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 ... 이렇게 해도 안되던데... db서버를 모두 재부팅 해봤는데 적용되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gpt 물어보면 재부팅을 안하고 리로드만 해도 된다고 하는데 리로드를 몇번이나 해봤는지 모르겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;321&quot; data-origin-height=&quot;385&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WtBwB/dJMcahXl4Mh/D08i4uq7keLge2vNws6ghK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WtBwB/dJMcahXl4Mh/D08i4uq7keLge2vNws6ghK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WtBwB/dJMcahXl4Mh/D08i4uq7keLge2vNws6ghK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWtBwB%2FdJMcahXl4Mh%2FD08i4uq7keLge2vNws6ghK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;321&quot; height=&quot;385&quot; data-origin-width=&quot;321&quot; data-origin-height=&quot;385&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 2개의 서버와 pgpool 연결된 1개의 서버를 확인할수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 로드밸런스가 잘 되는지 확인 해볼까?&lt;/p&gt;
&lt;pre id=&quot;code_1766750147202&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for i in {1..10}; do
  kubectl exec deployment/pgpool -- sh -c &quot;export PGPASSWORD='비번'; psql -h 127.0.0.1 -U postgres -d postgres -c 'SELECT inet_server_addr(), pg_is_in_recovery();'&quot;
  sleep 1
done&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드1번에서 이렇게 해봤더니 10번이 모두 1번 서버로 나온다... 뭔가 이상하다.&lt;/p&gt;
&lt;pre id=&quot;code_1766750221084&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;root@k3s-node1:/k8s/apps/pgpool# kubectl exec -it deployment/pgpool -- psql -h 127.0.0.1 -U postgres -c &quot;show pool_nodes;&quot;
Defaulted container &quot;pgpool&quot; out of: pgpool, setup-config (init)
Password for user postgres: 
 node_id |  hostname   | port | status | pg_status | lb_weight |  role   | pg_role | select_cnt | load_balance_node | replication_delay | replication_state | replication_sync_state | last_status_change  
---------+-------------+------+--------+-----------+-----------+---------+---------+------------+-------------------+-------------------+-------------------+------------------------+---------------------
 0       | 10.34.1.111 | 5432 | up     | up        | 0.500000  | primary | primary | 312        | false             | 0                 |                   |                        | 2025-12-26 10:52:34
 1       | 10.34.1.112 | 5432 | up     | up        | 0.500000  | standby | standby | 72         | true              | 0                 |                   |                        | 2025-12-26 10:52:34
(2 rows)

root@k3s-node1:/k8s/apps/pgpool#&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘다 up 이고 모두 정상이라고 GPT는 말하긴 한다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 계속 한쪽으로만 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색을 해보니&lt;/p&gt;
&lt;pre id=&quot;code_1766751191614&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PGPOOL_PARAMS_STATEMENT_LEVEL_LOAD_BALANCE 이 옵션을 켜라고 한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해봤다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 두 서버를 왔다갔다 하더라...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 좀 더 검색을 해보니 pgpool 이 한번 접속한 사용자는 동일하게 한쪽으로 보낸다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션방식이라서..근데 위 옵션을 ON으로 하면 요청 올때마다 로드밸런싱을 한다고 하네..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 나 혼자 하니까 그랬던 것..&lt;/p&gt;</description>
      <category>DB/Postgresql</category>
      <author>GG.Lab</author>
      <guid isPermaLink="true">https://gglab.tistory.com/57</guid>
      <comments>https://gglab.tistory.com/57#entry57comment</comments>
      <pubDate>Fri, 26 Dec 2025 21:16:54 +0900</pubDate>
    </item>
    <item>
      <title>k3s 에 cloudflare ddns 업데이터 적용</title>
      <link>https://gglab.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 dnszi 에서 사용하던 ddns 업데이트 이후 cloudflare 로 옮긴후 적용이 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 k3s 에 인증키 생성&lt;/p&gt;
&lt;pre id=&quot;code_1766185310120&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s/common/cloudflare$ cat ddns-secret.yaml 
apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-api-token
  namespace: default
type: Opaque
stringData:
  token: &quot;api토큰&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;proxy 할것과 안할것을 구분해서 두개의 설정 파일 준비&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[DNS 만 연결]&lt;/p&gt;
&lt;pre id=&quot;code_1766185358106&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s/common/cloudflare$ cat ddns-direct-deployment.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cloudflare-ddns-direct
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cloudflare-ddns
  template:
    metadata:
      labels:
        app: cloudflare-ddns
    spec:
      containers:
      - name: ddns
        image: favonia/cloudflare-ddns:latest
        env:
        - name: CLOUDFLARE_API_TOKEN
          valueFrom:
            secretKeyRef:
              name: cloudflare-api-token
              key: token
        - name: DOMAINS
          value: &quot;도메인주소&quot;
        - name: PROXIED
          value: &quot;false&quot;
        - name: UPDATE_CRON
          value: &quot;@every 1h&quot;
        - name: IP6_PROVIDER
          value: &quot;none&quot;
        - name: DETECTION_TIMEOUT
          value: &quot;15s&quot;
        # 컨테이너 자원 제한 (선택 사항)
        resources:
          limits:
            memory: &quot;64Mi&quot;
            cpu: &quot;100m&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[proxy]&lt;/p&gt;
&lt;pre id=&quot;code_1766185402504&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s/common/cloudflare$ cat ddns-proxied-deployment.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cloudflare-ddns-proxied
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cloudflare-ddns
  template:
    metadata:
      labels:
        app: cloudflare-ddns
    spec:
      containers:
      - name: ddns
        image: favonia/cloudflare-ddns:latest
        env:
        - name: CLOUDFLARE_API_TOKEN
          valueFrom:
            secretKeyRef:
              name: cloudflare-api-token
              key: token
        - name: DOMAINS
          value: &quot;docs.gglab.app,money-dev.gglab.app&quot;
        - name: PROXIED
          value: &quot;true&quot;
        - name: UPDATE_CRON
          value: &quot;@every 1h&quot;
        - name: IP6_PROVIDER
          value: &quot;none&quot;
        - name: DETECTION_TIMEOUT
          value: &quot;15s&quot;
        # 컨테이너 자원 제한 (선택 사항)
        resources:
          limits:
            memory: &quot;64Mi&quot;
            cpu: &quot;100m&quot;
gglabadmin@k3s-node1:/k8s/common/cloudflare$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 올라왔나?&lt;/p&gt;
&lt;pre id=&quot;code_1766185430586&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s/common/cloudflare$ sudo kubectl get pods -l app=cloudflare-ddns
NAME                                      READY   STATUS    RESTARTS   AGE
cloudflare-ddns-direct-689fbfbf9c-nhn5r   1/1     Running   0          7m49s
cloudflare-ddns-proxied-d766fb866-59pk6   1/1     Running   0          7m44s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파드는 1개씩만 있으면 되겠지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1766185490548&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s/common/cloudflare$ sudo kubectl logs -f cloudflare-ddns-direct-689fbfbf9c-nhn5r
  Cloudflare DDNS (v1.15.1-0-ga0938af)
  Reading settings . . .
     Use default IP4_PROVIDER=cloudflare.trace
     Use default UPDATE_ON_START=true
     Use default DELETE_ON_STOP=false
     Use default CACHE_EXPIRATION=6h0m0s
     Use default TTL=1
     Use default UPDATE_TIMEOUT=30s
  Checking settings . . .
  Current settings:
     Domains, IP providers, and WAF lists:
        IPv4-enabled domains:    도메인
        IPv4 provider:           cloudflare.trace
        WAF lists:               (none)
     Scheduling:
        Timezone:                UTC (currently UTC+00)
        Update schedule:         @every 1h
        Update on start?         true
        Delete on stop?          false
        Cache expiration:        6h0m0s
     Parameters of new DNS records and WAF lists:
        TTL:                     1 (auto)
        Proxied domains:         (none)
        Unproxied domains:       도메인
        DNS record comment:      (empty)
        WAF list description:    (empty)
     Timeouts:
        IP detection:            15s
        Record/list updating:    30s

  Detected the IPv4 address 116.40.240.162
  The A records of 도메인 are already up to date
⏰ Checking the IP addresses in about 59m58s (23:55) . . .&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1766185542555&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s/common/cloudflare$ sudo kubectl logs -f cloudflare-ddns-proxied-d766fb866-59pk6
  Cloudflare DDNS (v1.15.1-0-ga0938af)
  Reading settings . . .
     Use default IP4_PROVIDER=cloudflare.trace
     Use default UPDATE_ON_START=true
     Use default DELETE_ON_STOP=false
     Use default CACHE_EXPIRATION=6h0m0s
     Use default TTL=1
     Use default UPDATE_TIMEOUT=30s
  Checking settings . . .
  Current settings:
     Domains, IP providers, and WAF lists:
        IPv4-enabled domains:    도메인
        IPv4 provider:           cloudflare.trace
        WAF lists:               (none)
     Scheduling:
        Timezone:                UTC (currently UTC+00)
        Update schedule:         @every 1h
        Update on start?         true
        Delete on stop?          false
        Cache expiration:        6h0m0s
     Parameters of new DNS records and WAF lists:
        TTL:                     1 (auto)
        Proxied domains:         도메인
        Unproxied domains:       (none)
        DNS record comment:      (empty)
        WAF list description:    (empty)
     Timeouts:
        IP detection:            15s
        Record/list updating:    30s

  Detected the IPv4 address 116.40.240.162
  The A records of 도메인1 are already up to date
  The A records of 도메인2 are already up to date
⏰ Checking the IP addresses in about 59m58s (23:55) . . .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘된다.&lt;/p&gt;</description>
      <category>인프라/Cloudflare</category>
      <author>GG.Lab</author>
      <guid isPermaLink="true">https://gglab.tistory.com/56</guid>
      <comments>https://gglab.tistory.com/56#entry56comment</comments>
      <pubDate>Sat, 20 Dec 2025 08:06:32 +0900</pubDate>
    </item>
    <item>
      <title>Cloudflare Proxy 적용</title>
      <link>https://gglab.tistory.com/55</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;운영중인 함쓰 가계부 앱의 사용자에게 앱이 자꾸 로그인이 풀린다는 제보가 들어왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심할때는 로그인이 풀린다음 재로그인을 10번정도 시도해야 된다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카페에 남긴 이 내용을 보고 처음 든생각은??&amp;nbsp; &quot;나는 한번도 안끊기는데???&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폰의 영향인가? 최신 업데이트된 OS 탓인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침 최근 IOS도 26이 나오고 안드로이드도 16이 나오다 보니.. 여러 가능성이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 마침 댓글에... &quot;나만 끊기는게 아니었네요~&quot; 라는 글이 달렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오잉... 그런사람이 많다고??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함쓰는 JWT 토큰 인증 방식을 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AccessToken 은 15분 RefreshToken 은 2주를 셋팅하여 사용중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 차원에서 AccessToken 이 만료되거나 가까워지면 RefreshToken 을 이용해서 자동으로 업데이트 되고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갱신되는지 모르고 사용하고 있는 샘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 언제 끊기는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 API 를 호출 하는 모든 순간에 토큰을 사용하기 때문에 앱을 한참 사용중에 끊기게 되면 화면의 갱신이 안된다. 즉 오류처럼 보이게 될것이다. (잘 구성된 앱은 이 경우에도 자연스럽게 로그인 화면으로 이동할 것이다.) 그럼 명시적으로 로그인화면으로 이동하는 순간은 언제인가? 바로 앱이 처음 동작하는 스플래시 화면이다. 이때는 인증정보를 체크하고 권한이 없으면 바로 로그인 화면으로 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 여기까지 정보를 보았을때는 특정사용자의 DB정보에 잘못된 무언가가 있나? 고민했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 뒤 사용자가 해외에 있다고 하여 .. 환경의 문제로 접근했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPN 을 사용하는지 물어봤다. 그런데 아니라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제부터는 사용자에게 이것저것 물어가면서 확인이 어려워진다. 접속이 안되는 사용자는 이미 카페에 남기는 글자에서 답답함과 불쾌감이 나타나고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쩌겠는가.. 앱에 네트워크 체크 기능을 만들고 업데이트 하여 눌러봐 달라고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국에 서버를 둔 API 연결은 통신 실패!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이어베이스는 통신 성공!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오호라... 서버 위치가 문제구나..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나도 최근 이사를 했을때 해외망이 저녁 9시부터 12시까지 너무 느려서 통신사에 제보하여 라인을 바꾼 경험이 있다. SK망에서 LG망으로 바꿨나? 암튼 라인 자체를 바꿨더니 개선이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 사용자의 인터넷 환경이 문제라고 나름대로 결정을 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 해결책은?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT에 물어봤더니.. 무료 상품으로도 dns 뿐만 아니라 ssl 인증서, proxy 까지 가능하다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼마나 큰 차이가 있을까 싶으나 유의미 한 것 같기는 한다. DDOS 도 막아준다고 하니 나쁠건 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 사용자의 환경은 해당 사용자의 인터넷 ISP -&amp;gt; 해외망 이 어떨지 모르겠으나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바꾸면 사용자의 ISP -&amp;gt; Cloudflare 의 가장 빠른 엣지 서버 -&amp;gt; 엣지서버에서 한국으로 접속&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되기 때문에 해외망의 라우팅 자체가 달라지게 되고 전세계에서 아주 많이 사용하고 있는 서비스로 신뢰할 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서론이 길었으니 이 글은 cloudflare proxy 를 적용하기 위한 과정을 기록으로 남기고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현 서버 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 서버는 vultr 라는 클라우드 서비스를 한국리전으로 운영중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;인터넷 -&amp;gt; 최초 Haproxy 접속 -&amp;gt; 이중화 API 서버 로드 밸런스&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSL 인증서는 Haproxy 에서 일괄처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부의 Docker 컨테이너로 연결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방안&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cloudflare 옮기기 위해서 해야하는 작업 절차는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Cloudflare 에 기존 DNS 업체 정보 모두 옮기기 (현재는 무료 DNSZI 를 쓰고 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Cloudflare 로 DNS 서버 이전 (proxy 모드로)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Cloudflare 로 SSL 인증서 셋팅&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 기존 Haproxy 를 살려두고 Cloudflare -&amp;gt; Haproxy 통신이 가능하도록 Haproxy 에 Cloudflare 통신용 SSL 셋팅&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 2,3,4 는 동시에 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 계획은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;0. 함쓰 사용자에게 작업 공지 (앱 공지)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Cloudflare 에 기존 DNS 업체 정보 모두 옮기기 (현재는 무료 DNSZI 를 쓰고 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Cloudflare 로 DNS 서버 이전 (proxy 모드로)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Cloudflare 로 SSL 인증서 셋팅&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 기존 Haproxy 를 살려두고 Cloudflare -&amp;gt; Haproxy 통신이 가능하도록 Haproxy 에 Cloudflare 통신용 SSL 셋팅&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 될것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS가 변경되면 전세계 전파되는 시간이 좀 걸릴 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함쓰 앱에서 SSL 인증서가 안마장도 왠지 서비스가 될것 같기는 한데.. 일단 시도를 해봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cloudflare 에 도메인 등록&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 먼저 도메인 등록을 시도해 본다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1778&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beKLUt/dJMcafZtkXa/YQVRxfLgET8i89iORKukNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beKLUt/dJMcafZtkXa/YQVRxfLgET8i89iORKukNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beKLUt/dJMcafZtkXa/YQVRxfLgET8i89iORKukNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeKLUt%2FdJMcafZtkXa%2FYQVRxfLgET8i89iORKukNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1778&quot; height=&quot;598&quot; data-origin-width=&quot;1778&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 자동으로 검색된 호스트가 아래로 쭉~ 뜬다. 이중에 안나오는 애들을 내가 수동으로 등록 해줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록하고 나니 기존 DNS공급자에서 고치라고 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;2014&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qs22D/dJMcafLWsQD/KJW2GkXCzt4d9ZHaD8yTlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qs22D/dJMcafLWsQD/KJW2GkXCzt4d9ZHaD8yTlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qs22D/dJMcafLWsQD/KJW2GkXCzt4d9ZHaD8yTlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqs22D%2FdJMcafLWsQD%2FKJW2GkXCzt4d9ZHaD8yTlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1508&quot; height=&quot;2014&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;2014&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;1728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIIV66/dJMcaa4VVeq/AgRf26l1ldSe1AFPXK1Oy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIIV66/dJMcaa4VVeq/AgRf26l1ldSe1AFPXK1Oy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIIV66/dJMcaa4VVeq/AgRf26l1ldSe1AFPXK1Oy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIIV66%2FdJMcaa4VVeq%2FAgRf26l1ldSe1AFPXK1Oy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1996&quot; height=&quot;1728&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;1728&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;godaddy 에 접속해서 네임서버를 바꿨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #313131; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DNSSEC 를 끄라는 안내가 있어서. 찾아봤는데 이미 꺼져있는 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3488&quot; data-origin-height=&quot;1438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb3NFi/dJMcagqxU3O/7hImNxvV57kCMTVH0cnJa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb3NFi/dJMcagqxU3O/7hImNxvV57kCMTVH0cnJa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb3NFi/dJMcagqxU3O/7hImNxvV57kCMTVH0cnJa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb3NFi%2FdJMcagqxU3O%2F7hImNxvV57kCMTVH0cnJa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3488&quot; height=&quot;1438&quot; data-origin-width=&quot;3488&quot; data-origin-height=&quot;1438&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 아침일찍 새벽에... 한번 해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;0. 함쓰 사용자에게 작업 공지 (앱 공지) =&amp;gt; 불필요&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;1. Cloudflare 에 기존 DNS 업체 정보 모두 옮기기 (현재는 무료 DNSZI 를 쓰고 있다.) =&amp;gt; 완료&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;2. Cloudflare 로 DNS 서버 이전 (proxy 모드로) =&amp;gt; 완료&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;3. Cloudflare 로 SSL 인증서 셋팅 =&amp;gt; 완료&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4. 기존 Haproxy 를 살려두고 Cloudflare -&amp;gt; Haproxy 통신이 가능하도록 Haproxy 에 Cloudflare 통신용 SSL 셋팅 =&amp;gt; 이것도 완료인듯&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일단 공지 없이 해봤는데.. 잘돼네?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API 접근 포트 확인&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하나씩 잘 되는지를 확인해보자.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일단 api 서버로의 22 번 포트 접근을 해보니까. 안된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;확인해보니 Proxy 는 80, 443만 해준다고 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프록시를 꺼보자. 끄니까 잘된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;작업하다보니.. 한가지 주의했어야 하는것이 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;무료버전 cloudflare 는 80, 443 만 프록시 한다는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;즉, 다른포트로 서비스를 하고 있었다면 차단된다는 것이다. 함쓰의 경우 최신 버전은 443을 사용하지만 이제야 배포하기 시작했고&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;기존 서비스는 9443으로 서비스를 했다. 즉 Proxy 를 켜두면 바라보는 IP가 바뀌기 때문에 proxy 되지 않는다는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;그래서 일단 api 서버는 proxy 는 꺼두었다. 최신 버전이 어느정도 배포 되고 나면 그때 켜야겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;켜기만 하면 문제 없다는 뜻.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Haproxy 로그 확인&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3956&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oMLdn/dJMcag49W1z/93f3j0Ykmgpu5xuRu7c9RK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oMLdn/dJMcag49W1z/93f3j0Ykmgpu5xuRu7c9RK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oMLdn/dJMcag49W1z/93f3j0Ykmgpu5xuRu7c9RK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoMLdn%2FdJMcag49W1z%2F93f3j0Ykmgpu5xuRu7c9RK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3956&quot; height=&quot;496&quot; data-origin-width=&quot;3956&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;money.gglab.app 사이트의 haproxy 로그를 확인해봤다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우리집의 아이피가 찍히지 않고 알수없는 아이피가 찍혔다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;검색해보니 cloudflare 아이피 대역이 맞다고 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일단 통과&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;기존 서버에서의 Certbot 동작 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 certbot 이 잘 동작하는지 확인을 해봐야겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1766181051982&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;root@ggmoney-haproxy:/etc/haproxy/certs# ./ssl_renew_money.gglab.app.sh 
checking expiration date for money.gglab.app...
the certificate for money.gglab.app will be updated
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator standalone, Installer None
Renewing an existing certificate for money.gglab.app

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   *****/money.gglab.app/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/money.gglab.app/privkey.pem
   Your certificate will expire on 2026-03-19. To obtain a new or
   tweaked version of this certificate in the future, simply run
   certbot again. To non-interactively renew *all* of your
   certificates, run &quot;certbot renew&quot;
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;강제로 갱신해봤는데 잘 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;개발서버 DDNS 자동 업데이트 셋팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영서버는 문제 없지만 개발 서버는 집에 놓고 쓰기 때문에 IP가 유동IP이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경험상 잘 바뀌지는 않지만 혹시나 바뀌었을 경우를 대비해서 자동 업데이트를 만들어본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cloudflare API 토큰을 먼저 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;2018&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bv38eB/dJMcai9GV9H/znMkm4UDaRVgv3pkelPNc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bv38eB/dJMcai9GV9H/znMkm4UDaRVgv3pkelPNc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bv38eB/dJMcai9GV9H/znMkm4UDaRVgv3pkelPNc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbv38eB%2FdJMcai9GV9H%2FznMkm4UDaRVgv3pkelPNc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1432&quot; height=&quot;2018&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;2018&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요기서 Edit Zone dns 템플릿으로 선택&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 내 개발 서버쪽에서 docker 를 하나 띄워서 자동 업데이트를 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1766183534878&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  # 그룹 1: 프록시 사용 (웹 서비스용)
  ddns-proxied:
    image: favonia/cloudflare-ddns:latest
    restart: always
    environment:
      - CLOUDFLARE_API_TOKEN=[발급받은토큰]
      - DOMAINS=ggmoney-api-dev.gglab.app,jenkins.gglab.app
      - PROXIED=true
      - UPDATE_CRON=@every 5m&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요런식으로 처리한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 새 버전 앱이 배포되기를 기다렸다가.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;api Proxy 를 ON 하면 될것 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;구버전을 사용하는 사람들을 한번에 업데이트 유도할 수 있을것 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안된다고 제보가 올 것이니까.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>인프라/Cloudflare</category>
      <author>GG.Lab</author>
      <guid isPermaLink="true">https://gglab.tistory.com/55</guid>
      <comments>https://gglab.tistory.com/55#entry55comment</comments>
      <pubDate>Sat, 20 Dec 2025 06:52:18 +0900</pubDate>
    </item>
    <item>
      <title>k3s 에 consul 적용</title>
      <link>https://gglab.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 내 서버에 10.34.1.151, 152, 153 3개 노드에 k3s 를 설치해 두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10.34.1.150 에 vip 를 설정하고 metallb 도 해둔 상태인데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 실습하면 consul 서비스를 사용하려면 3개 노드가 필요하다고 하여.. k3s 에 가상화를 적용하지 않고 직접 설치를 했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 기존에 존재하는 k3s 에 설치하는 방법을 찾아보았고 잘 되는 것 같아 기록을 남긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 k3s 기본으로 같이 설치된 traefik 설정을 변경한다.&lt;/p&gt;
&lt;pre id=&quot;code_1765284843525&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo kubectl edit svc traefik -n kube-system&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1506&quot; data-origin-height=&quot;878&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG4JZF/dJMcafkOvKf/OOf4qfEigwiUPkaulRPccK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG4JZF/dJMcafkOvKf/OOf4qfEigwiUPkaulRPccK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG4JZF/dJMcafkOvKf/OOf4qfEigwiUPkaulRPccK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG4JZF%2FdJMcafkOvKf%2FOOf4qfEigwiUPkaulRPccK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1506&quot; height=&quot;878&quot; data-origin-width=&quot;1506&quot; data-origin-height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1765284879009&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;metadata:
  annotations:
    meta.helm.sh/release-name: traefik
    meta.helm.sh/release-namespace: kube-system
    metallb.io/ip-allocated-from-pool: default-pool
    metallb.universe.tf/allow-shared-ip: shared-lb # 요부분&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 80, 443 을 traefik 이 사용중인데 같은 아이피의 다른 포트를 사용하기 위해서 추가해야한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 나머지는&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1765284964943&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s/apps/consul$ cat consul-statefulset.yaml 
# Headless Service for Consul cluster communication
apiVersion: v1
kind: Service
metadata:
  name: consul
  labels:
    app: consul
spec:
  clusterIP: None
  ports:
    - name: http
      port: 8500
      targetPort: 8500
    - name: dns
      port: 8600
      targetPort: 8600
    - name: server
      port: 8300
      targetPort: 8300
    - name: serf-lan
      port: 8301
      targetPort: 8301
    - name: serf-wan
      port: 8302
      targetPort: 8302
  selector:
    app: consul

---
# UI Service for external access
apiVersion: v1
kind: Service
metadata:
  name: consul-ui
  labels:
    app: consul
spec:
  type: NodePort
  ports:
    - name: http
      port: 8500
      targetPort: 8500
      nodePort: 30850
  selector:
    app: consul

---
# StatefulSet for Consul servers
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: consul
spec:
  serviceName: consul
  replicas: 3
  selector:
    matchLabels:
      app: consul
  template:
    metadata:
      labels:
        app: consul
    spec:
      containers:
      - name: consul
        image: hashicorp/consul:1.17.0        
        args:
          - &quot;agent&quot;
          - &quot;-server&quot;
          - &quot;-bootstrap-expect=3&quot;
          - &quot;-ui&quot;
          - &quot;-data-dir=/consul/data&quot;
          - &quot;-bind=0.0.0.0&quot;
          - &quot;-client=0.0.0.0&quot;
          - &quot;-retry-join=consul-0.consul.default.svc.cluster.local&quot;
          - &quot;-retry-join=consul-1.consul.default.svc.cluster.local&quot;
          - &quot;-retry-join=consul-2.consul.default.svc.cluster.local&quot;
          - &quot;-domain=cluster.local&quot;
          - &quot;-datacenter=dc1&quot;
        ports:
          - containerPort: 8500
            name: ui-port
          - containerPort: 8400
            name: alt-port
          - containerPort: 53
            name: udp-port
          - containerPort: 8443
            name: https-port
          - containerPort: 8080
            name: http-port
          - containerPort: 8301
            name: serflan
          - containerPort: 8302
            name: serfwan
          - containerPort: 8600
            name: consuldns
          - containerPort: 8300
            name: server
        env:
          - name: POD_IP
            valueFrom:
              fieldRef:
                fieldPath: status.podIP
          - name: NAMESPACE
            valueFrom:
              fieldRef:
                fieldPath: metadata.namespace
        volumeMounts:
          - name: consul-data
            mountPath: /consul/data
        livenessProbe:
          httpGet:
            path: /v1/status/leader
            port: 8500
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /v1/status/peers
            port: 8500
          initialDelaySeconds: 10
          periodSeconds: 5
  volumeClaimTemplates:
  - metadata:
      name: consul-data
    spec:
      accessModes: [ &quot;ReadWriteOnce&quot; ]
      resources:
        requests:
          storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
  name: consul-lb
  labels:
    app: consul
  annotations:
    metallb.universe.tf/allow-shared-ip: &quot;shared-lb&quot;
spec:
  type: LoadBalancer
  loadBalancerIP: 10.34.1.150
  ports:
    - name: http
      port: 8500
      targetPort: 8500
      protocol: TCP
    - name: dns-tcp
      port: 8600
      targetPort: 8600
      protocol: TCP
    - name: dns-udp
      port: 8600
      targetPort: 8600
      protocol: UDP
  selector:
    app: consul&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지는 이렇게 했더니 잘 동작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gpt 설명을 남기고 마무리 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. consul (Headless Service)&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;yaml&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: consul
spec:
  clusterIP: None  # Headless Service&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Consul 클러스터 내부 통신&lt;/b&gt; 전용&lt;/li&gt;
&lt;li&gt;clusterIP: None으로 설정되어 있어서 &lt;b&gt;로드밸런싱 없이&lt;/b&gt; 각 Pod에 직접 연결&lt;/li&gt;
&lt;li&gt;StatefulSet의 각 Pod에 고유한 DNS 이름 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DNS 이름&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;consul-0.consul.default.svc.cluster.local&lt;/li&gt;
&lt;li&gt;consul-1.consul.default.svc.cluster.local&lt;/li&gt;
&lt;li&gt;consul-2.consul.default.svc.cluster.local&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포트 역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;8500&lt;/b&gt; (http): Consul HTTP API&lt;/li&gt;
&lt;li&gt;&lt;b&gt;8600&lt;/b&gt; (dns): Consul DNS 서버&lt;/li&gt;
&lt;li&gt;&lt;b&gt;8300&lt;/b&gt; (server): Consul 서버 간 RPC 통신&lt;/li&gt;
&lt;li&gt;&lt;b&gt;8301&lt;/b&gt; (serf-lan): 로컬 네트워크 가십 프로토콜 (노드 발견, 헬스체크)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;8302&lt;/b&gt; (serf-wan): WAN 가십 프로토콜 (다른 데이터센터 통신)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StatefulSet args에서 사용:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;yaml&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;haml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;- &quot;-retry-join=consul-0.consul.default.svc.cluster.local&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요약&lt;/b&gt;: Consul 서버들끼리 서로 찾고 통신하기 위한 내부 서비스&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. consul-ui (NodePort Service)&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;yaml&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: consul-ui
spec:
  type: NodePort
  nodePort: 30850&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Consul UI에 직접 접근&lt;/b&gt;하기 위한 서비스&lt;/li&gt;
&lt;li&gt;클러스터의 &lt;b&gt;모든 노드 IP&lt;/b&gt;에서 접근 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;접근 방법&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;bash&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;dts&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;# 어떤 노드 IP로도 접근 가능
http://&amp;lt;node1-ip&amp;gt;:30850
http://&amp;lt;node2-ip&amp;gt;:30850
http://&amp;lt;node3-ip&amp;gt;:30850&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;8500&lt;/b&gt; &amp;rarr; Consul HTTP API/UI&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LoadBalancer 없이도 외부 접근 가능&lt;/li&gt;
&lt;li&gt;간단한 테스트/디버깅용으로 유용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;포트 번호(30850)를 기억해야 함&lt;/li&gt;
&lt;li&gt;프로덕션에서는 잘 사용 안 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요약&lt;/b&gt;: 노드 IP + 고정 포트로 Consul UI에 접근하는 간단한 방법&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. consul-lb (LoadBalancer Service)&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;yaml&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: consul-lb
  annotations:
    metallb.universe.tf/allow-shared-ip: &quot;shared-lb&quot;
spec:
  type: LoadBalancer
  loadBalancerIP: 10.34.1.150&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프로덕션 환경&lt;/b&gt;에서 Consul에 접근하기 위한 서비스&lt;/li&gt;
&lt;li&gt;**고정 IP (VIP)**를 통해 접근&lt;/li&gt;
&lt;li&gt;MetalLB를 통해 traefik과 같은 IP 공유&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;접근 방법&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;bash&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;# 고정 IP로 접근
http://10.34.1.150:8500  # Consul UI/API
dig @10.34.1.150 -p 8600 # Consul DNS
```

### 포트
- **8500** (TCP): Consul HTTP API/UI
- **8600** (TCP): Consul DNS over TCP
- **8600** (UDP): Consul DNS over UDP

### IP 공유
```
10.34.1.150:80   &amp;rarr; traefik (HTTP)
10.34.1.150:443  &amp;rarr; traefik (HTTPS)
10.34.1.150:8500 &amp;rarr; consul-lb (Consul UI/API)
10.34.1.150:8600 &amp;rarr; consul-lb (Consul DNS)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기억하기 쉬운 고정 IP&lt;/li&gt;
&lt;li&gt;표준 포트 사용 (80, 443, 8500)&lt;/li&gt;
&lt;li&gt;프로덕션 레벨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요약&lt;/b&gt;: 프로덕션에서 사용하는 메인 접근 방법 (고정 IP + 표준 포트)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 구조 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스타입용도접근 방법사용 목적&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;consul&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Headless&lt;/td&gt;
&lt;td&gt;내부 통신&lt;/td&gt;
&lt;td&gt;consul-0.consul.default.svc.cluster.local&lt;/td&gt;
&lt;td&gt;Consul 클러스터 구성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;consul-ui&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;NodePort&lt;/td&gt;
&lt;td&gt;개발/테스트&lt;/td&gt;
&lt;td&gt;&amp;lt;node-ip&amp;gt;:30850&lt;/td&gt;
&lt;td&gt;간단한 외부 접근&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;consul-lb&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;LoadBalancer&lt;/td&gt;
&lt;td&gt;프로덕션&lt;/td&gt;
&lt;td&gt;10.34.1.150:8500&lt;/td&gt;
&lt;td&gt;실제 서비스 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 사용 시나리오&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 1: Consul 클러스터 부팅&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;yaml&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;vala&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;# consul (Headless) 사용
- &quot;-retry-join=consul-0.consul.default.svc.cluster.local&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 각 서버가 서로를 찾아서 클러스터 형성&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 2: 개발자가 빠르게 UI 확인&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;bash&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;# consul-ui (NodePort) 사용
curl http://192.168.1.100:30850/ui/&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 노드 IP만 알면 바로 접근&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 3: 애플리케이션에서 Consul 사용&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;bash&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;# consul-lb (LoadBalancer) 사용
export CONSUL_HTTP_ADDR=&quot;http://10.34.1.150:8500&quot;
consul members&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 고정 IP로 안정적으로 접근&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 4: 다른 Pod에서 Service Discovery&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;yaml&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;dts&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;# consul (Headless) 사용
env:
  - name: CONSUL_HTTP_ADDR
    value: &quot;http://consul:8500&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 클러스터 내부에서는 서비스 이름으로 접근&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리: 각 서비스를 언제 사용할까?&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;consul (Headless)&lt;/b&gt;: Consul 서버들끼리 통신, StatefulSet Pod들의 고유 DNS&lt;/li&gt;
&lt;li&gt;&lt;b&gt;consul-ui (NodePort)&lt;/b&gt;: 빠른 테스트, 디버깅, 개발 환경&lt;/li&gt;
&lt;li&gt;&lt;b&gt;consul-lb (LoadBalancer)&lt;/b&gt;: 실제 서비스 제공, 프로덕션 환경, 외부 접근&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>인프라/Kubernetes</category>
      <author>GG.Lab</author>
      <guid isPermaLink="true">https://gglab.tistory.com/54</guid>
      <comments>https://gglab.tistory.com/54#entry54comment</comments>
      <pubDate>Tue, 9 Dec 2025 22:00:21 +0900</pubDate>
    </item>
    <item>
      <title>(연습) postgresql 이중화 (Failover)</title>
      <link>https://gglab.tistory.com/53</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 작성한 연습글을 보면 이중화까지 완료를 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;patroni 를 이용하여 master / slave 설정이 끝났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 한번 master 에 데이터베이스를 만들고 동기화가 잘되는지 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 현재 설정을 확인해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상환경에서 실행했으니 가상환경을 띄우고 아래와 같이 해보면&lt;/p&gt;
&lt;pre id=&quot;code_1765109482153&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@postgresql2:/opt/patroni_venv/venv_patroni$ sudo source venv_patroni/bin/activate
gglabadmin@postgresql2:/opt/patroni_venv/venv_patroni$ patronictl -c /etc/patroni/patroni.yaml list postgres-cluster

+ Cluster: postgres-cluster (7580890216573599783) +----+-------------+-----+------------+-----+
| Member      | Host        | Role    | State     | TL | Receive LSN | Lag | Replay LSN | Lag |
+-------------+-------------+---------+-----------+----+-------------+-----+------------+-----+
| postgresql1 | 10.34.1.111 | Replica | streaming |  2 |   0/5000398 |   0 |  0/5000398 |   0 |
| postgresql2 | 10.34.1.112 | Leader  | running   |  2 |             |     |            |     |
+-------------+-------------+---------+-----------+----+-------------+-----+------------+-----+
(venv_patroni) gglabadmin@postgresql2:/opt/patroni_venv$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번이 리더인것을 알수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번에서 데이터베이스를 만들어 볼려고 하면??&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbAaZU/dJMcaaRkL28/8NRosiPdQh3EPfK06ilYWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbAaZU/dJMcaaRkL28/8NRosiPdQh3EPfK06ilYWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbAaZU/dJMcaaRkL28/8NRosiPdQh3EPfK06ilYWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbAaZU%2FdJMcaaRkL28%2F8NRosiPdQh3EPfK06ilYWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1230&quot; height=&quot;320&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요런식으로 안된다고 알람이 온다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞지.. 읽기 전용이지 그럼 2번에서 해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvl2mr/dJMcad1xhom/kexeV29wrZGVmSv9Ryluuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvl2mr/dJMcad1xhom/kexeV29wrZGVmSv9Ryluuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvl2mr/dJMcad1xhom/kexeV29wrZGVmSv9Ryluuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcvl2mr%2FdJMcad1xhom%2FkexeV29wrZGVmSv9Ryluuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;982&quot; height=&quot;278&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 잘 생성되고 레코드도 잘 들어갔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;586&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IKG1m/dJMcaaX6bp6/1SpTjrlf08mLgl74rMGIik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IKG1m/dJMcaaX6bp6/1SpTjrlf08mLgl74rMGIik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IKG1m/dJMcaaX6bp6/1SpTjrlf08mLgl74rMGIik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIKG1m%2FdJMcaaX6bp6%2F1SpTjrlf08mLgl74rMGIik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1390&quot; height=&quot;586&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;586&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번에도 잘 복제가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼이제 2번 DB를 다운시켜보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 완전 내려보겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1765109958654&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@postgresql1:~$ sudo journalctl -u patroni -f
Dec 07 12:18:51 postgresql1 patroni[21503]: 2025-12-07 12:18:51.460 UTC [21503] LOG:  selected new timeline ID: 3
Dec 07 12:18:51 postgresql1 patroni[21503]: 2025-12-07 12:18:51.532 UTC [21503] LOG:  archive recovery complete
Dec 07 12:18:51 postgresql1 patroni[21499]: 2025-12-07 12:18:51.548 UTC [21499] LOG:  database system is ready to accept connections
Dec 07 12:18:52 postgresql1 patroni[20506]: 2025-12-07 12:18:52,471 INFO: no action. I am (postgresql1), the leader with the lock
Dec 07 12:18:52 postgresql1 patroni[21501]: 2025-12-07 12:18:52.515 UTC [21501] LOG:  restartpoint complete: wrote 932 buffers (5.7%); 0 WAL file(s) added, 0 removed, 0 recycled; write=46.399 s, sync=0.027 s, total=46.464 s; sync files=303, longest=0.012 s, average=0.001 s; distance=4289 kB, estimate=13700 kB; lsn=0/5432A80, redo lsn=0/5430970
Dec 07 12:18:52 postgresql1 patroni[21501]: 2025-12-07 12:18:52.515 UTC [21501] LOG:  recovery restart point at 0/5430970
Dec 07 12:18:52 postgresql1 patroni[21501]: 2025-12-07 12:18:52.515 UTC [21501] DETAIL:  Last completed transaction was at log time 2025-12-07 12:16:32.41573+00.
Dec 07 12:18:52 postgresql1 patroni[21501]: 2025-12-07 12:18:52.515 UTC [21501] LOG:  checkpoint starting: immediate force wait
Dec 07 12:18:52 postgresql1 patroni[21501]: 2025-12-07 12:18:52.538 UTC [21501] LOG:  checkpoint complete: wrote 2 buffers (0.0%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.005 s, sync=0.003 s, total=0.023 s; sync files=2, longest=0.002 s, average=0.002 s; distance=8 kB, estimate=12330 kB; lsn=0/5432B98, redo lsn=0/5432B60
Dec 07 12:18:52 postgresql1 patroni[20506]: 2025-12-07 12:18:52,555 INFO: no action. I am (postgresql1), the leader with the lock
Dec 07 12:19:02 postgresql1 patroni[20506]: 2025-12-07 12:19:02,570 INFO: no action. I am (postgresql1), the leader with the lock&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;patroni 가 자동으로 인식해서 스스로를 leader 로 인식했다고 하는것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼한번 클라이언트에서 insert 를 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1800&quot; data-origin-height=&quot;1286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eaTobN/dJMcacBCldq/djVoacNTnNwdabWRFSdkZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eaTobN/dJMcacBCldq/djVoacNTnNwdabWRFSdkZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eaTobN/dJMcacBCldq/djVoacNTnNwdabWRFSdkZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeaTobN%2FdJMcacBCldq%2FdjVoacNTnNwdabWRFSdkZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1800&quot; height=&quot;1286&quot; data-origin-width=&quot;1800&quot; data-origin-height=&quot;1286&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 db에서 잘 들어간다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우와 정말 편하네... 이정도면 이중화 할만하네...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 2번을 켜보자...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 다운되었던 db를 켜면 자동으로 클러스터로 묶이는지를 봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어제 테스트 해볼때는 네트워크 어댑터만 뺐다가 꼈더니... 클러스가 묶이기는 했지만..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임라인 차이로 수동으로 초기화를 해야하는 부분이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에도 차이가 있는지 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3218&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blig7s/dJMcahiCehX/0lBvQxbKQcWJErf7Aj2f7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blig7s/dJMcahiCehX/0lBvQxbKQcWJErf7Aj2f7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blig7s/dJMcahiCehX/0lBvQxbKQcWJErf7Aj2f7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fblig7s%2FdJMcahiCehX%2F0lBvQxbKQcWJErf7Aj2f7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3218&quot; height=&quot;804&quot; data-origin-width=&quot;3218&quot; data-origin-height=&quot;804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오잉 켜지자 마자. 그냥 붙는것 같다.....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은데???&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 동기화도 되었나 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;1402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/matV7/dJMcaaw1RXj/Ip8YQh3bLsN0Kpiagubja0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/matV7/dJMcaaw1RXj/Ip8YQh3bLsN0Kpiagubja0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/matV7/dJMcaaw1RXj/Ip8YQh3bLsN0Kpiagubja0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmatV7%2FdJMcaaw1RXj%2FIp8YQh3bLsN0Kpiagubja0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1836&quot; height=&quot;1402&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;1402&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오... 레코드 여러개 넣어놨던것도 잘 들어왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기화가 바로 된 모양이다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크만 차단했을때는 왜... 바로 안되었을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 DB가 완전히 내려갔는데.. 네트워크만 차단된경우는 DB서비스가 내부에 살아 있으니까... 개별적인 타임라인으로 DB가 동작을 해버려서 서로 달라졌을수도 있다고 판단하는 로직이 있는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 이정도면... 아주 좋은데?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓸만할것 같다.&lt;/p&gt;</description>
      <category>DB/Postgresql</category>
      <category>patroni</category>
      <category>PostgreSQL</category>
      <category>postgresql이중화</category>
      <author>GG.Lab</author>
      <guid isPermaLink="true">https://gglab.tistory.com/53</guid>
      <comments>https://gglab.tistory.com/53#entry53comment</comments>
      <pubDate>Sun, 7 Dec 2025 21:25:23 +0900</pubDate>
    </item>
    <item>
      <title>(연습) postgreSQL 이중화 (consul, patroni)</title>
      <link>https://gglab.tistory.com/52</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;postgresql 을 이전에 이중화를 해보려고 글을 쓴적이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시.. pgpool2 로 진행하다가 실패했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공을 못한건 아니었지만 운영을 해보았더니.. 자동 failover 가 문제가 있었고..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 장애가 나서 처리하는데 애를 꽤 먹었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 현재 운영중인 서비스는 단일 구성에 하루 2회 백업을 진행하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇년이 지난 지금 검색을 해보니 그때와는 다르게 HA 구성을 하는 듯 하여 다른 방법으로 도전을 해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 이번에는 patroni 라는 솔루션을 사용해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZJEzr/dJMcagqrFJu/hA1UcMNo57RHBx3svTg7xk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZJEzr/dJMcagqrFJu/hA1UcMNo57RHBx3svTg7xk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZJEzr/dJMcagqrFJu/hA1UcMNo57RHBx3svTg7xk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZJEzr%2FdJMcagqrFJu%2FhA1UcMNo57RHBx3svTg7xk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;516&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 사용자 입장에서 접근을 한다고 생각해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 일단 app 서버에 접근할것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 내 서버는 nestjs 에 prisma orm 을 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 아무런 HA구성이 없다면 DB url을 찾아 바로 접속할 것이고..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 HA구성을 했다고 친다면..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 postgresql 처럼 N개의 노드중 primary 는 Read / Write 를 담당하고 , 나머지는 Read 만 한다고 가정을 하면..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱서버가 DB에 연결할때 어디로 가지??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CURD 중에서 R를 제외하면 무조건 Primary 로 가야할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 나머지는 어디로 가도 상관이 없을 것이고...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 용도에 맞게 잘 분배를 해야할것이다. 이거는 단순 로드밸런싱이 아니라 쿼리 종류에 따라 다르게 서버를 연결해야 한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 두가지를 고려해볼수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Application 레벨에서 두개의 용도를 분기해서 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Application 은 그대로 두고 요청을 분석해서 알아서 분기해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 1번의 경우는 기존의 Application 을 모두 수정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐.. 개발자가 현재 api 가 쓰기용인지 읽기용인지 코드로 분기해서 골라줘야 한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 만들어진 서비스에 적용하기에는 상당히 번거롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번의 경우는 .. 개발자에게 너무 행복하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔루션이 알아서 해줄테니까... 일전에 시도해 보았던 pgpool2 가 이런역할을 한다. 이것을 Query-based routing 이라고 한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT를 통해서 물어보니.. 역시나 pgpool 이 가장 먼저 언급되었고 유일한 솔루션이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 계속 검색을 해보니... Odyssey 라는 것이 있다고 한다. 차세대 pgool 이라고 하네???&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 나는 이번 포스팅에 Odyssey 기반으로 해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 포스팅이 별로 없다. GPT를 이용해 일단 해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Postgresql 설치 (2개노드)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/download/linux/ubuntu/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.postgresql.org/download/linux/ubuntu/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1764976852424&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;PostgreSQL: Linux downloads (Ubuntu)&quot; data-og-description=&quot;Linux downloads (Ubuntu) PostgreSQL is available in all Ubuntu versions by default. However, Ubuntu &amp;quot;snapshots&amp;quot; a specific version of PostgreSQL that is then supported throughout the lifetime of that Ubuntu version. The PostgreSQL project maintains an Apt &quot; data-og-host=&quot;www.postgresql.org&quot; data-og-source-url=&quot;https://www.postgresql.org/download/linux/ubuntu/&quot; data-og-url=&quot;https://www.postgresql.org/download/linux/ubuntu/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.postgresql.org/download/linux/ubuntu/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.postgresql.org/download/linux/ubuntu/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;PostgreSQL: Linux downloads (Ubuntu)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Linux downloads (Ubuntu) PostgreSQL is available in all Ubuntu versions by default. However, Ubuntu &quot;snapshots&quot; a specific version of PostgreSQL that is then supported throughout the lifetime of that Ubuntu version. The PostgreSQL project maintains an Apt&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.postgresql.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식사이트에서 확인하고 설치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음..그냥 기본 명령으로 된다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764977049975&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo apt install postgresql
[sudo] password for gglabadmin: 
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  libcommon-sense-perl libjson-perl libjson-xs-perl libllvm17t64 libpq5 libtypes-serialiser-perl postgresql-16 postgresql-client-16
  postgresql-client-common postgresql-common ssl-cert
Suggested packages:
  postgresql-doc postgresql-doc-16
The following NEW packages will be installed:
  libcommon-sense-perl libjson-perl libjson-xs-perl libllvm17t64 libpq5 libtypes-serialiser-perl postgresql postgresql-16 postgresql-client-16
  postgresql-client-common postgresql-common ssl-cert
0 upgraded, 12 newly installed, 0 to remove and 41 not upgraded.
Need to get 43.6 MB of archives.
After this operation, 175 MB of additional disk space will be used.
Do you want to continue? [Y/n] y

블라블라~

Running kernel seems to be up-to-date.

No services need to be restarted.

No containers need to be restarted.

No user sessions are running outdated binaries.

No VM guests are running outdated hypervisor (qemu) binaries on this host.

gglabadmin@postgresql2:~$ psql -V
psql (PostgreSQL) 16.11 (Ubuntu 16.11-0ubuntu0.24.04.1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우와 설치가....1분이 안걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 사용하는 mssql 에 비교하면... 이건 정말 축복인것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;16버전이 설치 되었다. 이거면 됐다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로케일 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1765063352317&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt-get install -y language-pack-ko
sudo locale-gen ko_KR.UTF-8
sudo update-locale&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;계정설정&lt;/h3&gt;
&lt;pre id=&quot;code_1764981186194&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo -u postgres psql

# 슈퍼유저 계정 비번설정
ALTER USER postgres WITH PASSWORD '비밀번호';

# 복제용 계정
CREATE ROLE rep_user WITH REPLICATION LOGIN PASSWORD '비밀번호';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Patroni가 복제를 위해 rep_user로 접속할 수 있도록 pg_hba.conf 파일에 접근을 허용&lt;/p&gt;
&lt;pre id=&quot;code_1764981411592&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo vi /etc/postgresql/16/main/pg_hba.conf

# 복제 사용자 접근 허용 (DB 서버 IP 대역)
host    replication  rep_user        10.34.1.0/24            md5 
host    replication  rep_user        127.0.0.1/32            md5

sudo systemctl restart postgresql&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 마지막에 상기 내용을 추가하고 재시작&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DCS 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 Distributed Configuration Store 를 설치할 차례..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 뭐냐면.. patroni 라는 HA 관리자가 나의 상태를 저장할 저장소&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 N대중.. 누가 primary 이고 아닌지.. 이런 상태값을 저장하여 HA구성을 할수 있도록 해주는 것...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런것이 왜 필요할까? DB서버에 직접하면.. 하나가 장애나면 그 저장소를 잃어버리니까...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일종의 공유 스토리지 역할을 하는것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안내에 따르면.. 3대를 설치하라고 한다. DCS 자체도 HA 구성을 해야한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 다른 서버를 설치하고 싶지 않은데....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 3대나 필요한지 GPT선생님과 많은 대화를 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 patroni 는 정합성을 보장한 설정 데이터를 요구한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 확실하게 믿어야 하는 상황을 무조건 만들어야 한다는건데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DCS 3대가 있고 각각이 동일한 데이터를 가지고 있을것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홀수가 필요한 이유는 데이터를 저장하고 있는 3개의 노드가 서로 내 데이터가 맞다고 주장할시에 합의를 통해 정확한 데이터만 남길수 있다는 것이다. 2대만 하면 서로 내 데이터가 맞아! 라고 하면 누가 맞는지 중재를 할수 없다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Quorum 이라고 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음.. 처음엔 2대에만 설치하려고 했는데 기존에 설치해두었던 k3s 를 사용해보면 어떨까 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k3s 에는 기본적으로 etcd 가 설치되어 있어서 바로 활용 가능할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 쿼럼 Quorum 구조를 갖추고 있으니 연결만 하면 바로 사용할듯 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서.. 일단 시도를 해봤는데 쉽게 되지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내장 etcd 를 외부에서 호출하는 것을 추천하지 않고 인증때문에 간단히 셋팅이안되는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼. 결국 k3s 서버 노드 3개를 활용하여 개별 설치를 해야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consul 이라는 툴로 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버간 방화벽 열기&lt;/p&gt;
&lt;pre id=&quot;code_1765057717165&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo ufw allow 8300/tcp
sudo ufw allow 8301/tcp
sudo ufw allow 8301/udp
sudo ufw allow 8500/tcp
sudo ufw allow 8600/tcp
sudo ufw allow 8600/udp
sudo ufw reload&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요 디렉토리 만들기&lt;/p&gt;
&lt;pre id=&quot;code_1765058132316&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo mkdir -p /var/lib/consul
sudo chown -R consul:consul /var/lib/consul
sudo chmod 750 /var/lib/consul&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k3s 각노드에서 consul 설치&lt;/p&gt;
&lt;pre id=&quot;code_1765056591187&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo &quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?&amp;lt;=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main&quot; | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update &amp;amp;&amp;amp; sudo apt install consul

gglabadmin@k3s-node1:~$ sudo systemctl status consul
○ consul.service - &quot;HashiCorp Consul - A service mesh solution&quot;
     Loaded: loaded (/usr/lib/systemd/system/consul.service; disabled; preset: enabled)
     Active: inactive (dead)
       Docs: https://developer.hashicorp.com/
       
gglabadmin@k3s-node1:~$ consul --version
Consul v1.22.1
Revision 3831febf
Build Date 2025-11-26T05:53:08Z
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol &amp;gt;2 when speaking to compatible agents)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consul 설정 (3개 노드 모두 설정)&lt;/p&gt;
&lt;pre id=&quot;code_1765057031331&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo mv /etc/consul.d/consul.hcl /etc/consul.d/consul.hcl.old
consul keygen &amp;lt;== 아래 encrypt 부분에 넣어준다.
sudo vi /etc/consul.d/consul.hcl

server = true
bootstrap_expect = 3
datacenter = &quot;dc1&quot;  # 3개 노드 모두 동일하게
data_dir = &quot;/var/lib/consul&quot;

# 모든 인터페이스 허용
client_addr = &quot;0.0.0.0&quot;
bind_addr = &quot;{{ GetInterfaceIP \&quot;eth0\&quot; }}&quot;

retry_join = [
  &quot;10.34.1.151&quot;,
  &quot;10.34.1.152&quot;,
  &quot;10.34.1.153&quot;
]

ui = true

encrypt = &quot;&amp;lt;여기에 gossip_encryption_key # 3개 노드 모두 동일한 값으로&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 다 끝나면 아래처럼 나온다.&lt;/p&gt;
&lt;pre id=&quot;code_1765058288465&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node3:~$ consul members
Node       Address           Status  Type    Build   Protocol  DC   Partition  Segment
k3s-node1  10.34.1.151:8301  alive   server  1.22.1  2         dc1  default    &amp;lt;all&amp;gt;
k3s-node2  10.34.1.152:8301  alive   server  1.22.1  2         dc1  default    &amp;lt;all&amp;gt;
k3s-node3  10.34.1.153:8301  alive   server  1.22.1  2         dc1  default    &amp;lt;all&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Patroni 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 서버 모두에서 설치를 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Python, pip, PostgreSQL 클라이언트 개발 파일 설치 (OS에 따라 다를 수 있음)&lt;/h3&gt;
&lt;pre id=&quot;code_1764979686106&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@postgresql2:~$ sudo apt install -y python3 python3-pip python3-dev libpq-dev
[sudo] password for gglabadmin: 
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
python3 is already the newest version (3.12.3-0ubuntu2.1).
python3 set to manually installed.
The following additional packages will be installed:
  binutils binutils-common binutils-x86-64-linux-gnu build-essential bzip2 cpp cpp-13 cpp-13-x86-64-linux-gnu cpp-x86-64-linux-gnu dpkg-dev
  fakeroot g++ g++-13 g++-13-x86-64-linux-gnu g++-x86-64-linux-gnu gcc gcc-13 gcc-13-base gcc-13-x86-64-linux-gnu gcc-x86-64-linux-gnu
  javascript-common libalgorithm-diff-perl libalgorithm-diff-xs-perl libalgorithm-merge-perl libasan8 libatomic1 libbinutils libcc1-0
  libctf-nobfd0 libctf0 libdpkg-perl libexpat1-dev libfakeroot libfile-fcntllock-perl libgcc-13-dev libgomp1 libgprofng0 libhwasan0 libisl23
  libitm1 libjs-jquery libjs-sphinxdoc libjs-underscore liblsan0 libmpc3 libpython3-dev libpython3.12-dev libquadmath0 libsframe1 libssl-dev
  libstdc++-13-dev libtsan2 libubsan1 lto-disabled-list make python3-wheel python3.12-dev zlib1g-dev
Suggested packages:
  binutils-doc gprofng-gui bzip2-doc cpp-doc gcc-13-locales cpp-13-doc debian-keyring g++-multilib g++-13-multilib gcc-13-doc gcc-multilib
  autoconf automake libtool flex bison gdb gcc-doc gcc-13-multilib gdb-x86-64-linux-gnu apache2 | lighttpd | httpd bzr postgresql-doc-16
  libssl-doc libstdc++-13-doc make-doc
The following NEW packages will be installed:
  binutils binutils-common binutils-x86-64-linux-gnu build-essential bzip2 cpp cpp-13 cpp-13-x86-64-linux-gnu cpp-x86-64-linux-gnu dpkg-dev
  fakeroot g++ g++-13 g++-13-x86-64-linux-gnu g++-x86-64-linux-gnu gcc gcc-13 gcc-13-base gcc-13-x86-64-linux-gnu gcc-x86-64-linux-gnu
  javascript-common libalgorithm-diff-perl libalgorithm-diff-xs-perl libalgorithm-merge-perl libasan8 libatomic1 libbinutils libcc1-0
  libctf-nobfd0 libctf0 libdpkg-perl libexpat1-dev libfakeroot libfile-fcntllock-perl libgcc-13-dev libgomp1 libgprofng0 libhwasan0 libisl23
  libitm1 libjs-jquery libjs-sphinxdoc libjs-underscore liblsan0 libmpc3 libpq-dev libpython3-dev libpython3.12-dev libquadmath0 libsframe1
  libssl-dev libstdc++-13-dev libtsan2 libubsan1 lto-disabled-list make python3-dev python3-pip python3-wheel python3.12-dev zlib1g-dev
0 upgraded, 61 newly installed, 0 to remove and 41 not upgraded.
Need to get 78.6 MB of archives.
After this operation, 283 MB of additional disk space will be used.

블라블라~


Processing triggers for libc-bin (2.39-0ubuntu8.6) ...
Scanning processes...                                                                                                                               
Scanning linux images...                                                                                                                            

Running kernel seems to be up-to-date.

No services need to be restarted.

No containers need to be restarted.

No user sessions are running outdated binaries.

No VM guests are running outdated hypervisor (qemu) binaries on this host.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Python 가상 환경 (venv) 생성 및 활성화&lt;/h3&gt;
&lt;pre id=&quot;code_1764980025089&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@postgresql1:~$ sudo apt install python3.12-venv -y
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  python3-pip-whl python3-setuptools-whl
The following NEW packages will be installed:
  python3-pip-whl python3-setuptools-whl python3.12-venv
0 upgraded, 3 newly installed, 0 to remove and 41 not upgraded.
Need to get 2,429 kB of archives.
After this operation, 2,777 kB of additional disk space will be used.
Get:1 http://kr.archive.ubuntu.com/ubuntu noble-updates/universe amd64 python3-pip-whl all 24.0+dfsg-1ubuntu1.3 [1,707 kB]
Get:2 http://kr.archive.ubuntu.com/ubuntu noble-updates/universe amd64 python3-setuptools-whl all 68.1.2-2ubuntu1.2 [716 kB]
Get:3 http://kr.archive.ubuntu.com/ubuntu noble-updates/universe amd64 python3.12-venv amd64 3.12.3-1ubuntu0.9 [5,674 B]
Fetched 2,429 kB in 0s (7,165 kB/s)             
Selecting previously unselected package python3-pip-whl.
(Reading database ... 92801 files and directories currently installed.)
Preparing to unpack .../python3-pip-whl_24.0+dfsg-1ubuntu1.3_all.deb ...
Unpacking python3-pip-whl (24.0+dfsg-1ubuntu1.3) ...
Selecting previously unselected package python3-setuptools-whl.
Preparing to unpack .../python3-setuptools-whl_68.1.2-2ubuntu1.2_all.deb ...
Unpacking python3-setuptools-whl (68.1.2-2ubuntu1.2) ...
Selecting previously unselected package python3.12-venv.
Preparing to unpack .../python3.12-venv_3.12.3-1ubuntu0.9_amd64.deb ...
Unpacking python3.12-venv (3.12.3-1ubuntu0.9) ...
Setting up python3-setuptools-whl (68.1.2-2ubuntu1.2) ...
Setting up python3-pip-whl (24.0+dfsg-1ubuntu1.3) ...
Setting up python3.12-venv (3.12.3-1ubuntu0.9) ...
Scanning processes...                                                                                                                               
Scanning linux images...                                                                                                                            

Running kernel seems to be up-to-date.

No services need to be restarted.

No containers need to be restarted.

No user sessions are running outdated binaries.

No VM guests are running outdated hypervisor (qemu) binaries on this host.
gglabadmin@postgresql1:~$ sudo mkdir -p /opt/patroni_venv
gglabadmin@postgresql1:~$ sudo chown gglabadmin:gglabadmin /opt/patroni_venv
gglabadmin@postgresql1:~$ cd /opt/patroni_venv
gglabadmin@postgresql1:/opt/patroni_venv$ python3 -m venv venv_patroni
gglabadmin@postgresql1:/opt/patroni_venv$ source venv_patroni/bin/activate
(venv_patroni) gglabadmin@postgresql1:/opt/patroni_venv$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Patroni 및 의존성 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1764979819363&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pip3 install patroni[consul,psycopg2]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Patroni 설정 디렉토리 생성&lt;/h3&gt;
&lt;pre id=&quot;code_1764980715262&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 설정 디렉토리 생성
sudo mkdir -p /etc/patroni

# postgres 사용자가 접근할 수 있도록 권한 설정
sudo chown -R postgres:postgres /etc/patroni&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Patroni 설정 파일 (patroni.yml) 작성&lt;/h3&gt;
&lt;pre id=&quot;code_1764983162424&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1. 클러스터 기본 설정
scope: postgres-cluster      # 클러스터 이름 (두 노드 동일)
name: postgresql1            # 서버 이름 (hostname과 일치시킵니다)
# use_sname_as_hostname: true # name 필드를 사용하여 hostname을 오버라이드할 경우 사용

# 2. REST API 설정
restapi:
  listen: 0.0.0.0:8008 # Patroni 관리 API 포트

# 3. DCS (Distributed Configuration Store) 설정
consul:
  # 여러 Consul 서버 주소를 콤마로 구분하여 입력합니다.
  host: 10.34.1.151:8500,10.34.1.152:8500,10.34.1.153:8500
  
bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10

  # PostgreSQL 초기화 설정 (Patroni가 PostgreSQL 클러스터를 처음 구성할 때 사용)
  initdb:
    - encoding: UTF8
    - locale: ko_KR.UTF-8
    - data-checksums

  # pg_hba.conf 설정 (Patroni가 자동으로 설정합니다)
  pg_hba:
    - host replication rep_user 10.34.1.0/24 md5
    - host all all 0.0.0.0/0 md5
    - host all all ::/0 md5

  # Patroni가 관리할 사용자 정보 (초기 DB 생성 시 사용)
  users:
    rep_user:
      password: '' # &amp;lt;&amp;lt;&amp;lt;--- 복제 사용자 비밀번호
      options:
        - replication
        - bypassrls
    admin:
      password: '' # &amp;lt;&amp;lt;&amp;lt;--- Patroni 관리용 슈퍼유저 비밀번호
      options:
        - superuser
        - createdb
        - createrole

# 4. PostgreSQL 인스턴스 설정
postgresql:
  # 이 노드의 IP와 Port를 명시합니다.
  listen: 10.34.1.111:5432       #   **postgresql1의 IP**
  connect_address: 10.34.1.111:5432 #   **postgresql1의 IP**
  port: 5432
  bin_dir: /usr/lib/postgresql/16/bin
  data_dir: /var/lib/postgresql/16/main
  parameters:
    unix_socket_directories: '/tmp'
  # Patroni가 postgres 계정으로 DB에 접속하기 위한 인증 정보
  authentication:
    replication:
      username: rep_user
      password: ''
    superuser:
      username: postgres
      password: '' # &amp;lt;&amp;lt;&amp;lt;--- 기존 postgres 사용자 비밀번호&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 파일을 서비스로 등록&lt;/p&gt;
&lt;pre id=&quot;code_1764999733601&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Unit]
Description=Patroni PostgreSQL High Availability
After=network.target
Wants=network-online.target

[Service]
User=patroni
Group=patroni
Type=simple

WorkingDirectory=/etc/patroni
ExecStart=/opt/patroni_venv/venv_patroni/bin/patroni /etc/patroni/patroni.yaml

Restart=always
Environment=&quot;PATH=/opt/patroni_venv/venv_patroni/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin&quot;

RestartSec=5

# Patroni가 종료될 때 PostgreSQL도 정상 종료되도록 함
TimeoutStopSec=30
KillMode=process

[Install]
WantedBy=multi-user.target&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-end=&quot;118&quot; data-start=&quot;88&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Patroni 설정 파일 권한 점검&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1765001157118&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo chown -R patroni:patroni /etc/patroni

# postgre 의 main 이 최초 비어 있어야 한다고 한다.

sudo rm -rf /var/lib/postgresql/16/main
sudo mkdir -p /var/lib/postgresql/16/main
sudo chown -R patroni:patroni /var/lib/postgresql/16
sudo chmod -R 700 /var/lib/postgresql/16&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Patroni 실행&lt;/h3&gt;
&lt;pre id=&quot;code_1765001530533&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl status patroni

○ patroni.service - Patroni PostgreSQL High Availability
     Loaded: loaded (/etc/systemd/system/patroni.service; disabled; preset: enabled)
     Active: inactive (dead)
     
sudo systemctl enable patroni
sudo systemctl start patroni&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 여러 오류들이 있었고 해결하면서 상위 코드들을 일부 보완하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 consul 연결이 가장 오래 걸렸는데...&lt;/p&gt;
&lt;pre id=&quot;code_1765064136985&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -X DELETE http://10.34.1.151:8500/v1/kv/service/postgres-cluster?recurse&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 시도된 정보가 남아 있어서 상기와 같이 정보를 날려주고 다시 하니까 되는 경우가 종종있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 서버가 정상 동작시 아래와 같은 로그가 찍힌다.&lt;/p&gt;
&lt;pre id=&quot;code_1765064180050&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(venv_patroni) gglabadmin@postgresql1:~$ sudo journalctl -u patroni -f
Dec 06 23:34:30 postgresql1 patroni[20506]: 2025-12-06 23:34:30,302 INFO: no action. I am (postgresql1), the leader with the lock
Dec 06 23:34:40 postgresql1 patroni[20506]: 2025-12-06 23:34:40,302 INFO: no action. I am (postgresql1), the leader with the lock
Dec 06 23:34:50 postgresql1 patroni[20506]: 2025-12-06 23:34:50,302 INFO: no action. I am (postgresql1), the leader with the lock
Dec 06 23:35:00 postgresql1 patroni[20506]: 2025-12-06 23:35:00,302 INFO: no action. I am (postgresql1), the leader with the lock
Dec 06 23:35:10 postgresql1 patroni[20506]: 2025-12-06 23:35:10,302 INFO: no action. I am (postgresql1), the leader with the lock
Dec 06 23:35:20 postgresql1 patroni[20506]: 2025-12-06 23:35:20,302 INFO: no action. I am (postgresql1), the leader with the lock
Dec 06 23:35:30 postgresql1 patroni[20506]: 2025-12-06 23:35:30,302 INFO: no action. I am (postgresql1), the leader with the lock
Dec 06 23:35:40 postgresql1 patroni[20506]: 2025-12-06 23:35:40,302 INFO: no action. I am (postgresql1), the leader with the lock
Dec 06 23:35:50 postgresql1 patroni[20506]: 2025-12-06 23:35:50,302 INFO: no action. I am (postgresql1), the leader with the lock
Dec 06 23:36:00 postgresql1 patroni[20506]: 2025-12-06 23:36:00,302 INFO: no action. I am (postgresql1), the leader with the lock
^C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번서버가 잘 동작할때는 아래와 같이 나왔다.&lt;/p&gt;
&lt;pre id=&quot;code_1765064219735&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@postgresql2:/usr/lib/postgresql/16/bin$ journalctl -u patroni -f
Dec 06 23:32:54 postgresql2 systemd[1]: patroni.service: Main process exited, code=exited, status=1/FAILURE
Dec 06 23:32:54 postgresql2 systemd[1]: patroni.service: Failed with result 'exit-code'.
Dec 06 23:32:55 postgresql2 systemd[1]: Stopped patroni.service - Patroni PostgreSQL High Availability.
Dec 06 23:33:04 postgresql2 systemd[1]: Started patroni.service - Patroni PostgreSQL High Availability.
Dec 06 23:33:04 postgresql2 patroni[22557]: 2025-12-06 23:33:04,894 INFO: No PostgreSQL configuration items changed, nothing to reload.
Dec 06 23:33:04 postgresql2 patroni[22557]: 2025-12-06 23:33:04,896 INFO: Systemd integration is not supported
Dec 06 23:33:04 postgresql2 patroni[22557]: 2025-12-06 23:33:04,897 INFO: Lock owner: postgresql1; I am postgresql2
Dec 06 23:33:04 postgresql2 patroni[22557]: 2025-12-06 23:33:04,916 INFO: trying to bootstrap from leader 'postgresql1'
Dec 06 23:33:04 postgresql2 patroni[22557]: 2025-12-06 23:33:04,920 INFO: Lock owner: postgresql1; I am postgresql2
Dec 06 23:33:04 postgresql2 patroni[22557]: 2025-12-06 23:33:04,930 INFO: bootstrap from leader 'postgresql1' in progress
Dec 06 23:33:14 postgresql2 patroni[22557]: 2025-12-06 23:33:14,918 INFO: Lock owner: postgresql1; I am postgresql2
Dec 06 23:33:14 postgresql2 patroni[22557]: 2025-12-06 23:33:14,919 INFO: bootstrap from leader 'postgresql1' in progress
Dec 06 23:33:24 postgresql2 patroni[22557]: 2025-12-06 23:33:24,918 INFO: Lock owner: postgresql1; I am postgresql2
Dec 06 23:33:24 postgresql2 patro&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consul l에 의해서 slave 로 patroni 클러스터 참여되어 1번을 복제중이라는 의미로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;db서버 상호간 방화벽 열기 및 patroni 계정에 db 폴더 사용권한 주기&lt;/p&gt;
&lt;pre id=&quot;code_1765064438894&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo ufw allow from 10.34.1.0/24 to any port 5432
sudo ufw reload

sudo chown -R patroni:patroni /var/lib/postgresql/16
sudo chmod 700 /var/lib/postgresql/16&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 조정하니 아래와 같이 잘 설정 되었다.&lt;/p&gt;
&lt;pre id=&quot;code_1765064833865&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Dec 06 23:45:31 postgresql2 patroni[23184]: 10.34.1.112:5432 - accepting connections
Dec 06 23:45:31 postgresql2 patroni[22557]: 2025-12-06 23:45:31,567 INFO: Lock owner: postgresql1; I am postgresql2
Dec 06 23:45:31 postgresql2 patroni[22557]: 2025-12-06 23:45:31,567 INFO: establishing a new patroni heartbeat connection to postgres
Dec 06 23:45:31 postgresql2 patroni[22557]: 2025-12-06 23:45:31,634 INFO: no action. I am (postgresql2), a secondary, and following a leader (postgresql1)
Dec 06 23:45:41 postgresql2 patroni[22557]: 2025-12-06 23:45:41,805 INFO: no action. I am (postgresql2), a secondary, and following a leader (postgresql1)
Dec 06 23:45:51 postgresql2 patroni[22557]: 2025-12-06 23:45:51,757 INFO: no action. I am (postgresql2), a secondary, and following a leader (postgresql1)
Dec 06 23:46:02 postgresql2 patroni[22557]: 2025-12-06 23:46:02,068 INFO: no action. I am (postgresql2), a secondary, and following a leader (postgresql1)
Dec 06 23:46:11 postgresql2 patroni[22557]: 2025-12-06 23:46:11,931 INFO: no action. I am (postgresql2), a secondary, and following a leader (postgresql1)
Dec 06 23:46:21 postgresql2 patroni[22557]: 2025-12-06 23:46:21,614 INFO: no action. I am (postgresql2), a secondary, and following a leader (postgresql1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 서버의 네트워크를 잠깐 내려봤다.&lt;/p&gt;
&lt;pre id=&quot;code_1765065469019&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Dec 06 23:56:41 postgresql2 patroni[22557]: 2025-12-06 23:56:41,655 INFO: no action. I am (postgresql2), a secondary, and following a leader (postgresql1)
Dec 06 23:56:51 postgresql2 patroni[22557]: 2025-12-06 23:56:51,952 INFO: no action. I am (postgresql2), a secondary, and following a leader (postgresql1)
Dec 06 23:57:01 postgresql2 patroni[22557]: 2025-12-06 23:57:01,700 INFO: no action. I am (postgresql2), a secondary, and following a leader (postgresql1)
Dec 06 23:57:10 postgresql2 patroni[22557]: 2025-12-06 23:57:10,288 INFO: establishing a new patroni restapi connection to postgres
Dec 06 23:57:10 postgresql2 patroni[22557]: 2025-12-06 23:57:10,306 INFO: Got response from postgresql1 http://0.0.0.0:8008/patroni: {&quot;state&quot;: &quot;running&quot;, &quot;postmaster_start_time&quot;: &quot;2025-12-06 23:45:30.568628+00:00&quot;, &quot;role&quot;: &quot;replica&quot;, &quot;server_version&quot;: 160011, &quot;xlog&quot;: {&quot;received_location&quot;: 50331976, &quot;replayed_location&quot;: 50331976, &quot;replayed_timestamp&quot;: null, &quot;paused&quot;: false}, &quot;timeline&quot;: 1, &quot;replication_state&quot;: &quot;streaming&quot;, &quot;cluster_unlocked&quot;: true, &quot;dcs_last_seen&quot;: 1765065430, &quot;database_system_identifier&quot;: &quot;7580890216573599783&quot;, &quot;patroni&quot;: {&quot;version&quot;: &quot;4.1.0&quot;, &quot;scope&quot;: &quot;postgres-cluster&quot;, &quot;name&quot;: &quot;postgresql2&quot;}}
Dec 06 23:57:10 postgresql2 patroni[22557]: 2025-12-06 23:57:10,334 INFO: promoted self to leader by acquiring session lock
Dec 06 23:57:10 postgresql2 patroni[23251]: server promoting
Dec 06 23:57:10 postgresql2 patroni[23180]: 2025-12-06 23:57:10.335 UTC [23180] LOG:  received promote request
Dec 06 23:57:10 postgresql2 patroni[23181]: 2025-12-06 23:57:10.336 UTC [23181] FATAL:  terminating walreceiver process due to administrator command
Dec 06 23:57:10 postgresql2 patroni[23180]: 2025-12-06 23:57:10.337 UTC [23180] LOG:  invalid record length at 0/3000148: expected at least 24, got 0
Dec 06 23:57:10 postgresql2 patroni[23180]: 2025-12-06 23:57:10.337 UTC [23180] LOG:  redo done at 0/3000110 system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 699.72 s
Dec 06 23:57:10 postgresql2 patroni[23180]: 2025-12-06 23:57:10.347 UTC [23180] LOG:  selected new timeline ID: 2
Dec 06 23:57:10 postgresql2 patroni[23180]: 2025-12-06 23:57:10.416 UTC [23180] LOG:  archive recovery complete
Dec 06 23:57:10 postgresql2 patroni[23178]: 2025-12-06 23:57:10.430 UTC [23178] LOG:  checkpoint starting: force
Dec 06 23:57:10 postgresql2 patroni[23176]: 2025-12-06 23:57:10.431 UTC [23176] LOG:  database system is ready to accept connections
Dec 06 23:57:10 postgresql2 patroni[23178]: 2025-12-06 23:57:10.478 UTC [23178] LOG:  checkpoint complete: wrote 2 buffers (0.0%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.010 s, sync=0.005 s, total=0.049 s; sync files=2, longest=0.003 s, average=0.003 s; distance=0 kB, estimate=14745 kB; lsn=0/30001B0, redo lsn=0/3000178
Dec 06 23:57:11 postgresql2 patroni[22557]: 2025-12-06 23:57:11,362 INFO: no action. I am (postgresql2), the leader with the lock&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10초만에 2번에서 감지하고 리더로 올라온것이 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번서버를 다시 살렸더니...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안되길래. 아래와 같이 reinit 을 해야한다고 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1765066022853&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@postgresql1:~$ sudo /opt/patroni_venv/venv_patroni/bin/patronictl -c /etc/patroni/patroni.yaml reinit postgres-cluster postgresql1
+ Cluster: postgres-cluster (7580890216573599783) ---+-------------+-----+------------+-----+
| Member      | Host        | Role    | State   | TL | Receive LSN | Lag | Replay LSN | Lag |
+-------------+-------------+---------+---------+----+-------------+-----+------------+-----+
| postgresql1 | 10.34.1.111 | Replica | running |  1 |   0/3000148 |   0 |  0/30001C0 |   0 |
| postgresql2 | 10.34.1.112 | Leader  | running |  2 |             |     |            |     |
+-------------+-------------+---------+---------+----+-------------+-----+------------+-----+
Are you sure you want to reinitialize members postgresql1? [y/N]:y  
Success: reinitialize for member postgresql1
gglabadmin@postgresql1:~$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 설정 된것 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1765066095335&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@postgresql1:~$ sudo journalctl -u patroni -f
Dec 07 00:06:23 postgresql1 patroni[20506]: 2025-12-07 00:06:23,365 INFO: establishing a new patroni heartbeat connection to postgres
Dec 07 00:06:23 postgresql1 patroni[20506]: 2025-12-07 00:06:23,403 INFO: no action. I am (postgresql1), a secondary, and following a leader (postgresql2)
Dec 07 00:06:33 postgresql1 patroni[20506]: 2025-12-07 00:06:33,413 INFO: no action. I am (postgresql1), a secondary, and following a leader (postgresql2)
Dec 07 00:06:43 postgresql1 patroni[20506]: 2025-12-07 00:06:43,059 INFO: no action. I am (postgresql1), a secondary, and following a leader (postgresql2)
Dec 07 00:06:53 postgresql1 patroni[20506]: 2025-12-07 00:06:53,551 INFO: no action. I am (postgresql1), a secondary, and following a leader (postgresql2)
Dec&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 1번이 왜 다시 올라오지 못했나?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 보면 1번에서 자동으로 스스로를 secondary 로 인식까지는 잘 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 스스로가 죽었던 타임라인과 2번이 새로 리더가 되면서 만들어진 타임라인이 달라서 강제로 데이터를 쓰지 않기 위해서 스스로 reinit 을 하지 않는다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시모를 1번 서버의 데이터를 지키기 위해서 인것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 failover 가 잘되고 무엇보다 일전에 해봣던 pgpool 보다는 간편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 이제 쿼리에 따른 자동 분기를 해야하는데 오디세이라는 툴이 가능한지 이어서 연습을 해야겠다.&lt;/p&gt;</description>
      <category>DB/Postgresql</category>
      <author>GG.Lab</author>
      <guid isPermaLink="true">https://gglab.tistory.com/52</guid>
      <comments>https://gglab.tistory.com/52#entry52comment</comments>
      <pubDate>Sun, 7 Dec 2025 08:47:50 +0900</pubDate>
    </item>
    <item>
      <title>k3s Cert-Manager 에서 외부 서비스로 Proxy</title>
      <link>https://gglab.tistory.com/51</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 별도의 우분투로 설치된 문서 관리 서비스가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;outline 오픈소스로 되어 있었고 중간에 좀 복잡하게 인증서 처리를 했는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 시놀로지 dsm 의 인증서와 역방향 프록시를 그대로 이용하는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이번에 망분리를 이용하면서 이용할수 없게 되었는데 k3s 에 설치한 certmanager 를 이용하여 인증서를 관리하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ingress 를 이용하여 외부 서버로의 연결이 되도록 설정을 해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 k3s 에 certmanager 구성이 되어 있다고 가정한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;k3s 서비스 구성&lt;/h2&gt;
&lt;pre id=&quot;code_1764458262176&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s/apps/docs$ cat service.yaml 
apiVersion: v1
kind: Service
metadata:
  name: outline-external
  namespace: default
spec:
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
    name: http
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: outline-external
  namespace: default
  labels:
    kubernetes.io/service-name: outline-external
addressType: IPv4
ports:
- name: http
  port: 80
  protocol: TCP
endpoints:
- addresses:
  - &quot;10.34.1.155&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;k3s ingress 구성&lt;/h2&gt;
&lt;pre id=&quot;code_1764458283727&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s/apps/docs$ cat ingress.yaml 
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: outline-ingress
  namespace: default
  annotations:
    cert-manager.io/cluster-issuer: &quot;letsencrypt-prod&quot;
    traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
    traefik.ingress.kubernetes.io/router.tls: &quot;true&quot;
    traefik.ingress.kubernetes.io/websocket: &quot;true&quot;
spec:
  ingressClassName: traefik
  tls:
  - hosts:
    - docs.gglab.app
    secretName: docs-tls
  rules:
  - host: docs.gglab.app
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: outline-external
            port:
              number: 80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 웹소켓 기능이 있어서 그것도 true 로 넣어줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;traefik ingress 에의하여 docs.gglab.app 도메인으로 들어오는 모든 https 트래픽이 80 포트로 10.34.1.155 로 가도록 구성되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1576&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MxrUL/dJMcahJExoz/CaNC8jHUr4MMlaBrZiSRm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MxrUL/dJMcahJExoz/CaNC8jHUr4MMlaBrZiSRm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MxrUL/dJMcahJExoz/CaNC8jHUr4MMlaBrZiSRm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMxrUL%2FdJMcahJExoz%2FCaNC8jHUr4MMlaBrZiSRm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1576&quot; height=&quot;632&quot; data-origin-width=&quot;1576&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘된다.&lt;/p&gt;</description>
      <category>인프라/Kubernetes</category>
      <category>external-svc</category>
      <category>k3s</category>
      <category>k3s외부서비스</category>
      <category>K8s</category>
      <category>Kubernetes</category>
      <category>traefik</category>
      <author>GG.Lab</author>
      <guid isPermaLink="true">https://gglab.tistory.com/51</guid>
      <comments>https://gglab.tistory.com/51#entry51comment</comments>
      <pubDate>Sun, 30 Nov 2025 08:21:00 +0900</pubDate>
    </item>
    <item>
      <title>k3s 에서 dnszi 유동아이피 연동 스케쥴 추가하기</title>
      <link>https://gglab.tistory.com/50</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 서버에서는 crontab 을 사용해서 내가 사용하던 서버중 하나를 이용해 crontab 작업을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k3s 로 옮기면서 이 작업을 변경할 필요가 있었는데 k3s 를 사용하니까 역시 pod로 만들 방법을 gpt 에 물어보니 깔끔하게 잘된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/k8s/common/dnszi 디렉토리를 만들고 안에 다음 두 파일을 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. URL 들을 저장하는 yaml&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 이것을 반복해서 스케쥴 돌려주는 conjob 용 yaml&lt;/p&gt;
&lt;pre id=&quot;code_1764453561346&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s/common/dnszi$ ll
total 16
drwxr-xr-x 2 root root 4096 Nov 29 21:51 ./
drwxr-xr-x 5 root root 4096 Nov 29 21:46 ../
-rw-r--r-- 1 root root 1363 Nov 29 21:44 dnszi-updater-cronjob.yaml
-rw-r--r-- 1 root root  372 Nov 29 21:51 dnszi-urls-config.yaml
gglabadmin@k3s-node1:/k8s/common/dnszi$&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1764453630843&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s/common/dnszi$ cat dnszi-urls-config.yaml 
apiVersion: v1
kind: ConfigMap
metadata:
  name: dnszi-url-list
data:
  urls: |
    https://ddns.dnszi.com/set.html?user=[dnszi의 id]&amp;amp;auth=[dnszi의 연동키]=gglab.app&amp;amp;record=docs
    https://ddns.dnszi.com/set.html?user=[dnszi의 id]&amp;amp;auth=[dnszi의 연동키]&amp;amp;domain=gglab.app&amp;amp;record=dev
    https://ddns.dnszi.com/set.html?user=[dnszi의 id]&amp;amp;auth=[dnszi의 연동키]&amp;amp;domain=gglab.app&amp;amp;record=money-dev
gglabadmin@k3s-node1:/k8s/common/dnszi$&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1764453655473&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s/common/dnszi$ cat dnszi-updater-cronjob.yaml 
apiVersion: batch/v1
kind: CronJob
metadata:
  name: dnszi-ip-updater
spec:
  schedule: &quot;*/5 * * * *&quot; 
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: curl-updater
            image: curlimages/curl:latest
            command: [&quot;/bin/sh&quot;, &quot;-c&quot;]
            args:
            - |
              echo &quot;Reading URLs from ConfigMap...&quot;
              # ConfigMap에서 마운트된 파일(urls)을 줄 단위로 읽어 반복 실행
              while IFS= read -r URL
              do
                if [ -n &quot;$URL&quot; ]; then
                  echo &quot;Attempting to update: $URL&quot;
                  curl -s &quot;$URL&quot;
                  if [ $? -eq 0 ]; then
                    echo &quot;SUCCESS: $URL&quot;
                  else
                    echo &quot;FAILURE: $URL&quot;
                  fi
                fi
              done &amp;lt; /etc/config/urls  # 마운트된 ConfigMap 파일 경로
              echo &quot;All updates finished.&quot;
            
            volumeMounts:
            - name: url-config-volume
              mountPath: /etc/config/  # ConfigMap이 마운트될 경로
          
          # ConfigMap을 볼륨으로 정의
          volumes:
          - name: url-config-volume
            configMap:
              name: dnszi-url-list # 위에서 생성한 ConfigMap 이름
          
          restartPolicy: OnFailure
gglabadmin@k3s-node1:/k8s/common/dnszi$&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행확인&lt;/h2&gt;
&lt;pre id=&quot;code_1764453753149&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo kubectl get jobs | grep dnszi-ip-updater
dnszi-ip-updater-29407555   Complete   1/1           5s         10m
dnszi-ip-updater-29407560   Complete   1/1           5s         5m27s
dnszi-ip-updater-29407565   Complete   1/1           5s         27s
gglabadmin@k3s-node1:/k8s/common/dnszi$ sudo kubectl logs $(sudo kubectl get pods --selector=job-name=dnszi-ip-updater-29407560 -o jsonpath='{.items[0].metadata.name}')
Reading URLs from ConfigMap...
Attempting to update: https://ddns.dnszi.com/set.html?user=&amp;amp;auth=&amp;amp;domain=gglab.app&amp;amp;record=docs
SUCCESS: https://ddns.dnszi.com/set.html?user=&amp;amp;auth=&amp;amp;domain=gglab.app&amp;amp;record=docs
Attempting to update: https://ddns.dnszi.com/set.html?user=&amp;amp;auth=&amp;amp;domain=gglab.app&amp;amp;record=dev
SUCCESS: https://ddns.dnszi.com/set.html?user=&amp;amp;auth=&amp;amp;domain=gglab.app&amp;amp;record=dev
Attempting to update: https://ddns.dnszi.com/set.html?user=&amp;amp;auth=&amp;amp;domain=gglab.app&amp;amp;record=money-dev
SUCCESS: https://ddns.dnszi.com/set.html?user=&amp;amp;auth=&amp;amp;domain=gglab.app&amp;amp;record=money-dev
All updates finished.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 실행되는것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 실행이 끝나면 pods 에 기록이 쌓이는 구조라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 1개만 남기도록 셋팅을 추가 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764454111035&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s/common/dnszi$ cat dnszi-updater-cronjob.yaml 
apiVersion: batch/v1
kind: CronJob
metadata:
  name: dnszi-ip-updater
spec:
  schedule: &quot;*/5 * * * *&quot; 
  successfulJobsHistoryLimit: 1 # 성공한 Job의 기록을 1개만 보존
  failedJobsHistoryLimit: 3     # 실패한 Job의 기록은 3개 보존
  jobTemplate:
    spec:
      template:
      
      ~~~~ 아래는 동일 ~~~~&lt;/code&gt;&lt;/pre&gt;</description>
      <category>인프라/Kubernetes</category>
      <category>crontab</category>
      <category>dnszi</category>
      <category>k3s</category>
      <category>K8s</category>
      <category>Kubernetes</category>
      <author>GG.Lab</author>
      <guid isPermaLink="true">https://gglab.tistory.com/50</guid>
      <comments>https://gglab.tistory.com/50#entry50comment</comments>
      <pubDate>Sun, 30 Nov 2025 07:08:57 +0900</pubDate>
    </item>
    <item>
      <title>pfSense 와 CertManager 연동 오류</title>
      <link>https://gglab.tistory.com/49</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;집안의 홈서버에서 k3s 3node 를 이용한 HA 구성 연습을 하면서..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일주일동안 진전이 없던 부분이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 traefik 기본 설정된 ACME 에서는 HA 구성이 안된다는 것을 알고 별도의 CertManager 를 이용하는 것이었는데..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 해도 동작이 안되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색해본 모든 포스트를 해보고 각종 AI 의 솔루션들을 모두 적용해본것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lets Encrypt 에서 내 도메인으로 인증시 .well-known/~~~ 주소로 들어와서 확인을 하게 되는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 지속적으로 404 에러와 함께 동작하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 부분을 기록으로 남겨보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 서버는 일단 pfSense 가 공인아이피를 받아서 내부ip로 NAT 를 하는 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 그 밑에는 k3s 3개 노드가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이피는 10.34.1.151~153까지이고&amp;nbsp; 10.34.1.150 으로 VIP를 설정해 두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치는&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* k3s with traefik&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* metallb&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 샘플 웹서비스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* cert-manager&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이슈상황&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1884&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rPErn/dJMcagjETHZ/h1ufQ06TPaaKa6hoVdZgvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rPErn/dJMcagjETHZ/h1ufQ06TPaaKa6hoVdZgvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rPErn/dJMcagjETHZ/h1ufQ06TPaaKa6hoVdZgvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrPErn%2FdJMcagjETHZ%2Fh1ufQ06TPaaKa6hoVdZgvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1884&quot; height=&quot;1058&quot; data-origin-width=&quot;1884&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 기본 실전 편에 작성된 내용이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AgVkB/dJMcafkKEP1/sS2cjFNsKOCrZmV2Wg0Uwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AgVkB/dJMcafkKEP1/sS2cjFNsKOCrZmV2Wg0Uwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AgVkB/dJMcafkKEP1/sS2cjFNsKOCrZmV2Wg0Uwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAgVkB%2FdJMcafkKEP1%2FsS2cjFNsKOCrZmV2Wg0Uwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1094&quot; height=&quot;674&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;674&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증서도 이런식으로 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 상세 로그는 어떻게 되어 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2bBK7/dJMcafrwi28/VpNnduj4wAsUaeJfIOlSxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2bBK7/dJMcafrwi28/VpNnduj4wAsUaeJfIOlSxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2bBK7/dJMcafrwi28/VpNnduj4wAsUaeJfIOlSxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2bBK7%2FdJMcafrwi28%2FVpNnduj4wAsUaeJfIOlSxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1520&quot; height=&quot;228&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 pending 이 되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764393223758&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s$ sudo kubectl describe challenge filmlife-net-tls-1-975396145-3416726947
Name:         filmlife-net-tls-1-975396145-3416726947
Namespace:    default
Labels:       &amp;lt;none&amp;gt;
Annotations:  &amp;lt;none&amp;gt;
API Version:  acme.cert-manager.io/v1
Kind:         Challenge
Metadata:
  Creation Timestamp:  2025-11-29T05:08:21Z
  Finalizers:
    acme.cert-manager.io/finalizer
  Generation:  1
  Owner References:
    API Version:           acme.cert-manager.io/v1
    Block Owner Deletion:  true
    Controller:            true
    Kind:                  Order
    Name:                  filmlife-net-tls-1-975396145
    UID:                   f12104d5-7dc8-4698-9dba-1239ad77afaa
  Resource Version:        2561
  UID:                     a332f39e-4f8b-4520-9b84-ebe67612f56e
Spec:
  Authorization URL:  https://acme-staging-v02.api.letsencrypt.org/acme/authz/246692703/20467262633
  Dns Name:           filmlife.net
  Issuer Ref:
    Group:  cert-manager.io
    Kind:   ClusterIssuer
    Name:   letsencrypt-staging
  Key:      49xTkDoY-6f9xWD7a0cRlD42YxoP6YbKZan9qXdzk3E.Hlx_gzxyxrU8i0M-dfpCkbRXI1EL21pGiBbC4v_vsKU
  Solver:
    http01:
      Ingress:
        Class:  traefik
  Token:        49xTkDoY-6f9xWD7a0cRlD42YxoP6YbKZan9qXdzk3E
  Type:         HTTP-01
  URL:          https://acme-staging-v02.api.letsencrypt.org/acme/chall/246692703/20467262633/TcD2yw
  Wildcard:     false
Status:
  Presented:   true
  Processing:  true
  Reason:      Waiting for HTTP-01 challenge propagation: wrong status code '404', expected '200'
  State:       pending
Events:
  Type    Reason     Age   From                     Message
  ----    ------     ----  ----                     -------
  Normal  Started    5m7s  cert-manager-challenges  Challenge scheduled for processing
  Normal  Presented  5m7s  cert-manager-challenges  Presented challenge using HTTP-01 challenge mechanism&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 보면 status reason 부분을 보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Waiting&amp;nbsp;for&amp;nbsp;HTTP-01&amp;nbsp;challenge&amp;nbsp;propagation:&amp;nbsp;wrong&amp;nbsp;status&amp;nbsp;code&amp;nbsp;'404',&amp;nbsp;expected&amp;nbsp;'200'&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 나온다. lests encrypt 에서 인증서 발급을 하기 위해 도메인 인증을 하고 내 서버에 접근했을때 특정 url 에 토큰을 넘겼을때 200 으로 리턴을 하게 되고 그것을 검증하는 과정이 있는데 그때 200 이 아니라 404가 온다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 아는 모든 gpt 에 물어보니...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http 80 으로 요청이 올때 certmanager pod 가 응답을 해야하는데 내 샘플 웹서비스로 응답이 들어온다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 잘못된 라우팅을 자꾸 한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참 이상했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764393380494&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;% curl -I http://filmlife.net/.well-known/acme-challenge/49xTkDoY-6f9xWD7a0cRlD42YxoP6YbKZan9qXdzk3E
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, must-revalidate
Content-Length: 87
Content-Type: text/plain; charset=utf-8
Date: Sat, 29 Nov 2025 05:16:08 GMT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상한건.. 내 노트북에서 호출하면 이렇게 잘 나오는데???&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜그럴까????&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lets encrypt 인증서를 발급할때는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. letsencrypt 측에서 출발해서 내 서버에 검증&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 내부에서 다시한번 인증 검증&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 두 단계를 모두 수행하게 된다고 하는데.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 curl 이 잘되는 것은 1번의 이유때문이고 이 부분은 문제가 없었던 것인데.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부에서 검증을 시도하려고 할때는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간에 있는 pfsense 방화벽에서.. filmlife.net 의 도메인을 보고.. 어 내꺼네?? 하고 무시해버린다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래 설정을 켜면 정상적으로 라우팅을 해준다고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;pfSense 설정 변경&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2306&quot; data-origin-height=&quot;1304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx1oeD/dJMcahJEgtN/BrDQiqremlMN8b2rq9rlgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx1oeD/dJMcahJEgtN/BrDQiqremlMN8b2rq9rlgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx1oeD/dJMcahJEgtN/BrDQiqremlMN8b2rq9rlgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx1oeD%2FdJMcahJEgtN%2FBrDQiqremlMN8b2rq9rlgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2306&quot; height=&quot;1304&quot; data-origin-width=&quot;2306&quot; data-origin-height=&quot;1304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨위에 pure Nat 로 하고 아래 automatic outbound Nat for Reflection 을 체크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하니까 해결되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764394540530&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s/apps/filmlife$ sudo kubectl get challenges 
NAME                                      STATE     DOMAIN         AGE
filmlife-net-tls-1-975396145-1653609454   pending   filmlife.net   3m23s
gglabadmin@k3s-node1:/k8s/apps/filmlife$ sudo kubectl get challenges 
NAME                                      STATE   DOMAIN         AGE
filmlife-net-tls-1-975396145-1653609454   valid   filmlife.net   3m25s
gglabadmin@k3s-node1:/k8s/apps/filmlife$ sudo kubectl get challenges 
No resources found in default namespace.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pending -&amp;gt; valid -&amp;gt; 사라짐&lt;/p&gt;</description>
      <category>인프라/Kubernetes</category>
      <category>k3s</category>
      <category>Kubernetes</category>
      <category>pfsense</category>
      <author>GG.Lab</author>
      <guid isPermaLink="true">https://gglab.tistory.com/49</guid>
      <comments>https://gglab.tistory.com/49#entry49comment</comments>
      <pubDate>Sat, 29 Nov 2025 14:26:00 +0900</pubDate>
    </item>
    <item>
      <title>(실전) k8s 설치부터 https 서비스 포팅까지</title>
      <link>https://gglab.tistory.com/48</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 몇가지 실험과 실습을 통해 내가 서비스하고 있는 docker image 하나를 올려보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그동안 docker 만 이용해 봤는데 꽤 재미 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간에 시행 착오가 있었는데 이 글을 통해 전체 흐름을 한번에 정리해서 처음부터 끝까지 다시 해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목표 흐름&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 356px;&quot; border=&quot;1&quot; data-path-to-node=&quot;3&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style10&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;구성 요소&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;위치/타입&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;IP 주소 및 포트&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;주요 기능 및 역할&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;b&gt;HA (고가용성) 역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot; data-path-to-node=&quot;3,1,0,0&quot;&gt;&lt;b&gt;1. pfSense&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot; data-path-to-node=&quot;3,1,1,0&quot;&gt;외부 방화벽/라우터&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot; data-path-to-node=&quot;3,1,2,0&quot;&gt;&lt;b&gt;공인 IP&lt;/b&gt; &amp;rarr; &lt;b&gt;10.34.1.150&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot; data-path-to-node=&quot;3,1,3,0&quot;&gt;외부 트래픽을 내부 K8s 클러스터의 &lt;b&gt;VIP로 포워딩&lt;/b&gt;하는 관문 역할.&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot; data-path-to-node=&quot;3,1,4,0&quot;&gt;외부에서 클러스터로 들어오는 트래픽의 &lt;b&gt;단일 진입점&lt;/b&gt; 제공.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,2,0,0&quot;&gt;&lt;b&gt;2. MetalLB&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,2,1,0&quot;&gt;K8s 애드온 (Speaker)&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,2,2,0&quot;&gt;&lt;b&gt;10.34.1.150 (VIP)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,2,3,0&quot;&gt;K8s LoadBalancer Service에 외부 접근 가능한 &lt;b&gt;VIP를 할당&lt;/b&gt;하고, ARP/NDP 프로토콜로 이 VIP를 네트워크에 광고.&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,2,4,0&quot;&gt;VIP가 &lt;b&gt;항상 살아있는 노드&lt;/b&gt;로 라우팅되도록 보장 (노드 장애 시 자동 인계).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,3,0,0&quot;&gt;&lt;b&gt;3. Traefik Service&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,3,1,0&quot;&gt;K8s LoadBalancer Service&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,3,2,0&quot;&gt;10.34.1.150 &amp;rarr; Cluster IP&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,3,3,0&quot;&gt;MetalLB가 할당한 VIP를 받아 외부 트래픽을 Traefik Pod로 전달.&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,3,4,0&quot;&gt;MetalLB 덕분에 &lt;b&gt;VIP를 통해 트래픽을 끊김 없이 수신&lt;/b&gt;할 수 있게 함.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,4,0,0&quot;&gt;&lt;b&gt;4. Traefik Pod&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,4,1,0&quot;&gt;K8s Ingress Controller&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,4,2,0&quot;&gt;10.34.1.150 &amp;rarr; Traefik Pod IP&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,4,3,0&quot;&gt;&lt;s&gt;1. &lt;b&gt;TLS 종료:&lt;/b&gt; HTTPS 인증서 처리 (&lt;b&gt;ACME 자동 갱신&lt;/b&gt;).&lt;/s&gt; 2. &lt;b&gt;레이어 7 라우팅:&lt;/b&gt; &lt;b&gt;Host 헤더&lt;/b&gt;(money-dev.gglab.app)를 기반으로 백엔드 Service 결정.&lt;br /&gt;인증서는 CertManager 를 통해 발급 / 갱신&lt;/td&gt;
&lt;td style=&quot;height: 60px;&quot; data-path-to-node=&quot;3,4,4,0&quot;&gt;Pod 장애 시 다른 Traefik Pod로 트래픽이 대체됨.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 56px;&quot;&gt;
&lt;td style=&quot;height: 56px;&quot; data-path-to-node=&quot;3,5,0,0&quot;&gt;&lt;b&gt;5. K8s Service&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 56px;&quot; data-path-to-node=&quot;3,5,1,0&quot;&gt;K8s Service (ClusterIP)&lt;/td&gt;
&lt;td style=&quot;height: 56px;&quot; data-path-to-node=&quot;3,5,2,0&quot;&gt;Cluster IP (예: 10.43.x.x)&lt;/td&gt;
&lt;td style=&quot;height: 56px;&quot; data-path-to-node=&quot;3,5,3,0&quot;&gt;Traefik으로부터 트래픽을 받아 해당 Deployment의 &lt;b&gt;모든 Pod로 부하를 분산&lt;/b&gt;시키는 로드 밸런서 역할.&lt;/td&gt;
&lt;td style=&quot;height: 56px;&quot; data-path-to-node=&quot;3,5,4,0&quot;&gt;Pod 레벨의 &lt;b&gt;로드 밸런싱 및 장애 감지&lt;/b&gt;를 수행.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot; data-path-to-node=&quot;3,6,0,0&quot;&gt;&lt;b&gt;6. 최종 Pod&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot; data-path-to-node=&quot;3,6,1,0&quot;&gt;K8s Pod&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot; data-path-to-node=&quot;3,6,2,0&quot;&gt;Pod IP&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot; data-path-to-node=&quot;3,6,3,0&quot;&gt;실제 요청을 처리하고 응답을 생성. (예: ggmoney-web-dev 애플리케이션 실행)&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot; data-path-to-node=&quot;3,6,4,0&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 목표하는 데이터의 흐름은 위와 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 것들을 하기 위해서 다음과 같은 절차로 설정을 해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;진행순서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 클러스터의 노드 3개 준비 (ubuntu)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. k3s HA 설치 (전부 master 모드)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. metallb 를 통해 VIP 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;- metaillb 동작 확인 (node를 죽여서 failover 확인)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 샘플 서비스 이미지 pod 배포&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;- 샘플 서비스 서비스 생성&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;- 샘플 서비스 ssl 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;5. traefik 을 통한 통한 lets encrypt 설정&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. Cert Manager 를 통한 SSL 인증 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. node failover 테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;- 1번 죽여보고,&amp;nbsp;살려보고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;- 2번도 죽여보고, 살려보고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이정도로 진행해 보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;클러스터 노드 준비&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VIP : 10.34.1.150&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node IP : 10.34.1.151 ~ 10.34.1.152 총 3개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 위와 같이 3대를 proxmox 를 이용하여 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계정을 gglabadmin 으로 만들었고 ip는 10.34.1.151 고정으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;host는 k3s-node1 로 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1244&quot; data-origin-height=&quot;781&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMrc1m/dJMcabJnTZU/RVi6sZmFhKGYpuRScmKcm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMrc1m/dJMcabJnTZU/RVi6sZmFhKGYpuRScmKcm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMrc1m/dJMcabJnTZU/RVi6sZmFhKGYpuRScmKcm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMrc1m%2FdJMcabJnTZU%2FRVi6sZmFhKGYpuRScmKcm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1244&quot; height=&quot;781&quot; data-origin-width=&quot;1244&quot; data-origin-height=&quot;781&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성이 되고 나면 2개를 복제하여 ip와 node host 를 변경하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해보니까...디스크 50G 로 했더니 복제보다. 새로 설치가 빠르다. -.-&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 끝나면 22번 ssh 도 열어준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;165&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uj5FL/dJMcad1tH8i/eElBQmtIEbnfkbbn0XbjPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uj5FL/dJMcad1tH8i/eElBQmtIEbnfkbbn0XbjPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uj5FL/dJMcad1tH8i/eElBQmtIEbnfkbbn0XbjPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuj5FL%2FdJMcad1tH8i%2FeElBQmtIEbnfkbbn0XbjPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;530&quot; height=&quot;165&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;165&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;k3s 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치를 하기 전에 필요한 방화벽을 셋팅합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pfSense + MetalLB + Traefik&lt;/b&gt; 기반 K3s 클러스터 구조를 위해 외부 및 내부 방화벽에서 반드시 열어야 할 포트 목록&lt;/p&gt;
&lt;hr data-path-to-node=&quot;1&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  1. 외부 방화벽 (pfSense) 설정&lt;/h3&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;pfSense 방화벽은 &lt;b&gt;공인 IP&lt;/b&gt;로 들어오는 트래픽을 클러스터의 VIP인 **10.34.1.150**으로 포트 포워딩(Port Forwarding)하는 규칙을 설정해야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;4&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;소스 IP&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;프로토콜&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;외부 포트 (Destination)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;내부 포트 (Target)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;내부 IP (Target)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;목적&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;4,1,0,0&quot;&gt;Any&lt;/td&gt;
&lt;td data-path-to-node=&quot;4,1,1,0&quot;&gt;TCP&lt;/td&gt;
&lt;td data-path-to-node=&quot;4,1,2,0&quot;&gt;&lt;b&gt;80&lt;/b&gt; (HTTP)&lt;/td&gt;
&lt;td data-path-to-node=&quot;4,1,3,0&quot;&gt;&lt;b&gt;80&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;4,1,4,0&quot;&gt;&lt;b&gt;10.34.1.150&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;4,1,5,0&quot;&gt;Let's Encrypt &lt;b&gt;ACME 챌린지&lt;/b&gt; 및 HTTP 트래픽&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;4,2,0,0&quot;&gt;Any&lt;/td&gt;
&lt;td data-path-to-node=&quot;4,2,1,0&quot;&gt;TCP&lt;/td&gt;
&lt;td data-path-to-node=&quot;4,2,2,0&quot;&gt;&lt;b&gt;443&lt;/b&gt; (HTTPS)&lt;/td&gt;
&lt;td data-path-to-node=&quot;4,2,3,0&quot;&gt;&lt;b&gt;443&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;4,2,4,0&quot;&gt;&lt;b&gt;10.34.1.150&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;4,2,5,0&quot;&gt;최종 &lt;b&gt;암호화된 웹 서비스&lt;/b&gt; 트래픽&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Traefik 기본 포트 설정 시 주의 사항&lt;/h3&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;Traefik은 기본적으로 web (80) 및 websecure (443) 엔트리포트를 사용합니다. 만약 Traefik이 &lt;b&gt;기본 포트(80, 443)가 아닌 다른 포트&lt;/b&gt;로 설정되어 있다면, pfSense에서도 해당 포트를 10.34.1.150의 Traefik 서비스 포트로 포워딩해야 합니다. (예: Traefik이 8000/8443을 사용한다면, pfSense 포워딩도 80/443 &amp;rarr; 8000/8443으로 변경)&lt;/p&gt;
&lt;hr data-path-to-node=&quot;7&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  2. 내부 방화벽 (K3s 노드) 설정&lt;/h3&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;K3s를 설치할 때 기본적으로 필요한 포트는 k3s-node1, k3s-node2, k3s-node3 &lt;b&gt;모든 노드&lt;/b&gt; 간에 허용되어야 합니다. K3s는 대부분의 포트를 자동으로 관리하지만, 기본적으로 알아야 할 필수 포트 목록입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;A. K3s 서버(컨트롤 플레인) 간 통신 포트&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;11&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;포트 번호&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;프로토콜&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;통신 방향&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;목적&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;11,1,0,0&quot;&gt;&lt;b&gt;6443&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;11,1,1,0&quot;&gt;TCP&lt;/td&gt;
&lt;td data-path-to-node=&quot;11,1,2,0&quot;&gt;Inbound (모든 노드 &amp;rarr; 서버)&lt;/td&gt;
&lt;td data-path-to-node=&quot;11,1,3,0&quot;&gt;&lt;b&gt;Kubernetes API Server&lt;/b&gt; 통신 (HA 서버/에이전트 통신 포함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;11,2,0,0&quot;&gt;&lt;b&gt;2379-2380&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;11,2,1,0&quot;&gt;TCP&lt;/td&gt;
&lt;td data-path-to-node=&quot;11,2,2,0&quot;&gt;Inbound (서버 간)&lt;/td&gt;
&lt;td data-path-to-node=&quot;11,2,3,0&quot;&gt;&lt;b&gt;내장 etcd/dqlite 데이터베이스 통신&lt;/b&gt; (HA 구성 시 필수)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;B. 네트워크 및 서비스 관련 포트&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;13&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;포트 번호&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;프로토콜&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;통신 방향&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;목적&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;13,1,0,0&quot;&gt;&lt;b&gt;8472&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;13,1,1,0&quot;&gt;UDP&lt;/td&gt;
&lt;td data-path-to-node=&quot;13,1,2,0&quot;&gt;Bidirectional&lt;/td&gt;
&lt;td data-path-to-node=&quot;13,1,3,0&quot;&gt;&lt;b&gt;Flannel VXLAN&lt;/b&gt; 터널링 통신 (Pod 네트워킹)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;13,2,0,0&quot;&gt;&lt;b&gt;10250&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;13,2,1,0&quot;&gt;TCP&lt;/td&gt;
&lt;td data-path-to-node=&quot;13,2,2,0&quot;&gt;Inbound&lt;/td&gt;
&lt;td data-path-to-node=&quot;13,2,3,0&quot;&gt;&lt;b&gt;Kubelet API&lt;/b&gt; (Control Plane &amp;rarr; 노드 Pod 관리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;13,3,0,0&quot;&gt;&lt;b&gt;30000-32767&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;13,3,1,0&quot;&gt;TCP/UDP&lt;/td&gt;
&lt;td data-path-to-node=&quot;13,3,2,0&quot;&gt;Inbound&lt;/td&gt;
&lt;td data-path-to-node=&quot;13,3,3,0&quot;&gt;&lt;b&gt;NodePort&lt;/b&gt; 서비스 범위 (일반적으로 사용자가 직접 열 필요는 없음)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;C. MetalLB 관련 포트 (BGP 모드인 경우)&lt;/h4&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;MetalLB가 BGP 모드로 설정되었다면 BGP 피어링 포트가 필요합니다. Layer 2(ARP/NDP) 모드라면 추가 포트가 필요하지 않습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;16&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;포트 번호&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;프로토콜&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;통신 방향&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;목적&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;16,1,0,0&quot;&gt;&lt;b&gt;179&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;16,1,1,0&quot;&gt;TCP&lt;/td&gt;
&lt;td data-path-to-node=&quot;16,1,2,0&quot;&gt;Bidirectional&lt;/td&gt;
&lt;td data-path-to-node=&quot;16,1,3,0&quot;&gt;&lt;b&gt;BGP 피어링 통신&lt;/b&gt; (라우터와 MetalLB Speaker 간)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT 가 모두 정리해 주었다. 너무 편한데?..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 우분투에 실행해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763773480894&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# -----------------------------------------------------------
# 1. UFW 설치 및 활성화 (필요한 경우)
# -----------------------------------------------------------
# sudo apt update
# sudo apt install ufw -y

# 22번은 이미 설정 했을테니까 참고만 하고 넘어가자
sudo ufw enable
sudo ufw 22/tcp

# -----------------------------------------------------------

# 2. 외부에서 들어오는 HTTP/HTTPS 트래픽 허용 (Traefik 및 ACME 챌린지용)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# 3. 내부 네트워크 대역(10.34.1.0/24)에서 들어오는 필수 클러스터 통신 허용

# Kubernetes API Server (6443)
sudo ufw allow from 10.34.1.0/24 to any port 6443 proto tcp

# Flannel Pod 네트워킹 (8472)
sudo ufw allow from 10.34.1.0/24 to any port 8472 proto udp

# Kubelet API (10250)
sudo ufw allow from 10.34.1.0/24 to any port 10250 proto tcp

# NodePort 서비스 범위 (30000-32767)
sudo ufw allow from 10.34.1.0/24 to any port 30000:32767 proto tcp
sudo ufw allow from 10.34.1.0/24 to any port 30000:32767 proto udp

# -----------------------------------------------------------
# 4. HA 서버 노드 (Control Plane)에서만 추가 실행할 명령어
#    (HA 데이터베이스 통신 2379-2380)
# -----------------------------------------------------------
# 만약 서버 노드와 에이전트 노드가 분리되어 있다면, 
# 이 명령은 서버 노드에서만 실행해야 합니다.
sudo ufw allow from 10.34.1.0/24 to any port 2379:2380 proto tcp

# -----------------------------------------------------------
# 5. 설정 적용 및 확인
# -----------------------------------------------------------
sudo ufw reload
sudo ufw status verbose&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;537&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E9wDI/dJMcadNUr7u/xvyGgZFkhikfOcE8jfwRRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E9wDI/dJMcadNUr7u/xvyGgZFkhikfOcE8jfwRRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E9wDI/dJMcadNUr7u/xvyGgZFkhikfOcE8jfwRRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE9wDI%2FdJMcadNUr7u%2FxvyGgZFkhikfOcE8jfwRRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;537&quot; height=&quot;292&quot; data-origin-width=&quot;537&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3개 노드가 모두 동일하게 master 이므로 똑같이 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k3s를 설치해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1763774421196&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#node 1 에서 실행
sudo curl -sfL https://get.k3s.io | sh -s - server \
  --cluster-init \
  --tls-san 10.34.1.150 \
  --tls-san 10.34.1.151 \
  --disable servicelb&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;K3s 서버 설치 명령어 옵션 분석&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;2&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;옵션&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;설정 값&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;역할 및 기능&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;사용자 환경에서의 의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;2,1,0,0&quot;&gt;&lt;b&gt;server&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,1,1,0&quot;&gt;(없음)&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,1,2,0&quot;&gt;K3s를 &lt;b&gt;서버 (Control Plane)&lt;/b&gt; 역할로 실행하도록 지정합니다.&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,1,3,0&quot;&gt;클러스터 관리, API 서버, 스케줄러 등을 담당하는 노드가 됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;2,2,0,0&quot;&gt;&lt;b&gt;--cluster-init&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,2,1,0&quot;&gt;(없음)&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,2,2,0&quot;&gt;새로운 HA 클러스터를 &lt;b&gt;처음 부트스트랩&lt;/b&gt;할 때 사용합니다.&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,2,3,0&quot;&gt;이 명령을 실행하는 노드가 클러스터의 &lt;b&gt;첫 번째 서버 노드&lt;/b&gt;가 됩니다. (다른 노드는 이 서버에 합류합니다.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;2,3,0,0&quot;&gt;&lt;b&gt;--tls-san&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,3,1,0&quot;&gt;10.34.1.150&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,3,2,0&quot;&gt;쿠버네티스 &lt;b&gt;API 서버의 TLS 인증서&lt;/b&gt;에 추가할 주체 대체 이름(Subject Alternative Name)을 지정합니다.&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,3,3,0&quot;&gt;&lt;b&gt;MetalLB VIP&lt;/b&gt;를 통해 API 서버(6443 포트)에 접속할 때 인증서 오류를 방지합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;2,4,0,0&quot;&gt;&lt;b&gt;--disable&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,4,1,0&quot;&gt;servicelb&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,4,2,0&quot;&gt;K3s에 내장된 &lt;b&gt;간단한 LoadBalancer 구현체&lt;/b&gt;를 비활성화합니다.&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,4,3,0&quot;&gt;&lt;b&gt;MetalLB&lt;/b&gt;와 같은 외부 LoadBalancer 솔루션을 사용하기 위해 필수적으로 내장 LoadBalancer의 충돌을 방지합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;2,5,0,0&quot;&gt;&lt;b&gt;--server&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,5,1,0&quot;&gt;https://[IP]:6443&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,5,2,0&quot;&gt;(두 번째 노드부터 사용) 클러스터에 &lt;b&gt;합류할 서버의 주소&lt;/b&gt;를 지정합니다.&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,5,3,0&quot;&gt;이전에 설정된 첫 번째 서버 노드의 API 주소를 지정하여 클러스터에 참여합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;2,6,0,0&quot;&gt;&lt;b&gt;--token&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,6,1,0&quot;&gt;[토큰 값]&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,6,2,0&quot;&gt;(두 번째 노드부터 사용) 클러스터의 &lt;b&gt;인증 토큰&lt;/b&gt;을 지정합니다.&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,6,3,0&quot;&gt;이 토큰을 통해 새 서버 노드가 기존 HA 클러스터에 합법적으로 합류할 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;2,7,0,0&quot;&gt;&lt;b&gt;--cluster-cidr&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,7,1,0&quot;&gt;10.42.0.0/16&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,7,2,0&quot;&gt;&lt;b&gt;Pod가 할당받을 IP 주소 대역&lt;/b&gt;을 지정합니다.&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,7,3,0&quot;&gt;Pod 간의 통신에 사용되는 사설 네트워크 대역을 정의합니다. (기본값 사용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-path-to-node=&quot;2,8,0,0&quot;&gt;&lt;b&gt;--service-cidr&lt;/b&gt;&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,8,1,0&quot;&gt;10.43.0.0/16&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,8,2,0&quot;&gt;&lt;b&gt;Service가 할당받을 Cluster IP 대역&lt;/b&gt;을 지정합니다.&lt;/td&gt;
&lt;td data-path-to-node=&quot;2,8,3,0&quot;&gt;클러스터 내부에서 Service에 접근할 때 사용되는 대역을 정의합니다. (기본값 사용)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2, 3번 노드 설치를 위한 토큰 확인&lt;/p&gt;
&lt;pre id=&quot;code_1763774858402&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#node 1번에서 실행

gglabadmin@k3s-node1:~$ sudo cat /var/lib/rancher/k3s/server/node-token
K1061da67ae6927dd477b0f98be1e70b2b9897f271e312b7bcb4a6ad8c9230d1b28::server:488d8a96f522680f47ee571cdc32fc6b
gglabadmin@k3s-node1:~$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2, 3번 노드에 설치&lt;/p&gt;
&lt;pre id=&quot;code_1763774925745&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#node 2, 3 에서 실행
sudo curl -sfL https://get.k3s.io | sh -s - server \
  --server https://10.34.1.151:6443 \
  --token K10678f8aac02dd03640881822ba1b5e9ad982ecce40ff945c9b82b798d7c42b6b3::server:248fe529f4a1655e9a55b634e78d702e \
  --tls-san 10.34.1.150 \
  --tls-san 10.34.1.151 \
  --disable servicelb&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 설치되었는지 확인&lt;/p&gt;
&lt;pre id=&quot;code_1763775004094&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:~$ sudo kubectl get nodes
NAME        STATUS   ROLES                       AGE     VERSION
k3s-node1   Ready    control-plane,etcd,master   9m25s   v1.33.5+k3s1
k3s-node2   Ready    control-plane,etcd,master   20s     v1.33.5+k3s1
k3s-node3   Ready    control-plane,etcd,master   9s      v1.33.5+k3s1
gglabadmin@k3s-node1:~$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3개 노드가 잘 설치 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HA 구성을 위해 모두 master 로 설치 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;metallb 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MetalLB의 주요 역할은 &lt;b&gt;쿠버네티스 클러스터에 외부 접근 가능한 로드 밸런서 서비스(Service Type: LoadBalancer)를 제공&lt;/b&gt;하는 것&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3999&quot; data-origin-height=&quot;2615&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clvmXS/dJMcabCBQic/9AX5nwvkZ6xx0LmKVL7UT1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clvmXS/dJMcabCBQic/9AX5nwvkZ6xx0LmKVL7UT1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clvmXS/dJMcabCBQic/9AX5nwvkZ6xx0LmKVL7UT1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclvmXS%2FdJMcabCBQic%2F9AX5nwvkZ6xx0LmKVL7UT1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3999&quot; height=&quot;2615&quot; data-origin-width=&quot;3999&quot; data-origin-height=&quot;2615&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10.34.1.151~153 의 아이피를 갖는 노드들중에서 master 에 vip 10.34.1.150 으로 셋팅해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 외부에서 직접 151, 152, 153 을 지정해주지 않아도 항상 살아있는 노드로 접근하도록 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이것이 없다면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;traefik 에서 loadbalancer 가 아닌 nodeport 타입으로 설정해서 각노드로 직접 지정을 해야하고 추후 노드가 추가/삭제될때 수정으로 제어를 해야한다. 물론 한번 하면 문제가 없겠지만 장애가 났을때 자동으로 해주지는 못할 것이므로 셋팅으르 해주는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763776493990&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# MetalLB 네임스페이스 생성 및 설치
sudo kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.9/config/manifests/metallb-native.yaml

# 설치 확인 (모든 Pod가 Running 상태가 될 때까지 대기)
sudo kubectl get pods -n metallb-system

gglabadmin@k3s-node1:/k8s$ sudo kubectl get pods -n metallb-system
NAME                         READY   STATUS    RESTARTS   AGE
controller-bb5f47665-dcznh   1/1     Running   0          51s
speaker-8skn7                1/1     Running   0          51s
speaker-knwf6                1/1     Running   0          51s
speaker-zz5bb                1/1     Running   0          51s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 설치되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드2, 3에서 확인해도 잘 된걸로 나온다.&lt;/p&gt;
&lt;pre id=&quot;code_1763776729843&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node3:~$ sudo kubectl get pods -n metallb-system
[sudo] password for gglabadmin: 
NAME                         READY   STATUS    RESTARTS   AGE
controller-bb5f47665-flkd5   1/1     Running   0          5m16s
speaker-7xmff                1/1     Running   0          5m16s
speaker-pbjzd                1/1     Running   0          5m16s
speaker-r8vzt                1/1     Running   0          5m16s
gglabadmin@k3s-node3:~$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 설정 파일을 만들어보자.&lt;/p&gt;
&lt;pre id=&quot;code_1763776780358&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s$ pwd
/k8s
gglabadmin@k3s-node1:/k8s$ sudo vi metallb-config.yaml 
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: default-pool
  namespace: metallb-system
spec:
  addresses:
  - 10.34.1.150/32
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: default
  namespace: metallb-system
spec:
  ipAddressPools:
  - default-pool
  
gglabadmin@k3s-node1:/k8s$ sudo kubectl apply -f metallb-config.yaml 
ipaddresspool.metallb.io/default-pool created&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;metallb failover test&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 하고 외부에서 ping 을 날려보았다. 궁금해서&lt;/p&gt;
&lt;pre id=&quot;code_1763850964432&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;% ping 10.34.1.150                       
PING 10.34.1.150 (10.34.1.150): 56 data bytes
92 bytes from 10.34.1.151: Time to live exceeded
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 5400 691e   0 0000  01  01 3aca 10.8.0.2  10.34.1.150 

Request timeout for icmp_seq 0
92 bytes from 10.34.1.151: Time to live exceeded
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 5400 4740   0 0000  01  01 5ca8 10.8.0.2  10.34.1.150 

Request timeout for icmp_seq 1
92 bytes from 10.34.1.151: Time to live exceeded
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 5400 e5de   0 0000  01  01 be09 10.8.0.2  10.34.1.150 

Request timeout for icmp_seq 2
92 bytes from 10.34.1.151: Time to live exceeded
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 5400 3822   0 0000  01  01 6bc6 10.8.0.2  10.34.1.150 

^C
--- 10.34.1.150 ping statistics ---&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가... 151 로 전달되었다가 lose 로 리턴이 온것같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;metallb 가 1번 노드로 보내고 있는듯 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 아직 생성된 서비스가 하나도 없다보니. 저렇게 나오는것으로 추정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 서비스로의 연결을 traefik 이 해주는 것이겠지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이번에 강제로 네트워크 어댑터를 제거하여 node 1번을 죽여봐야겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1763851147094&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Request timeout for icmp_seq 36
Request timeout for icmp_seq 37
Request timeout for icmp_seq 38
Request timeout for icmp_seq 39
92 bytes from 10.34.1.153: Destination Host Unreachable
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 5400 39db   0 0000  3e  01 2d0d 10.8.0.2  10.34.1.150 

92 bytes from 10.34.1.153: Destination Host Unreachable
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 5400 5b35   0 0000  3e  01 0bb3 10.8.0.2  10.34.1.150 

92 bytes from 10.34.1.153: Destination Host Unreachable
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 5400 17a6   0 0000  3e  01 4f42 10.8.0.2  10.34.1.150 

92 bytes from 10.34.1.153: Destination Host Unreachable
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 5400 e04e   0 0000  3e  01 8699 10.8.0.2  10.34.1.150 

Request timeout for icmp_seq 40
Request timeout for icmp_seq 41
Request timeout for icmp_seq 42
Request timeout for icmp_seq 43
92 bytes from 10.34.1.153: Destination Host Unreachable
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 5400 9fb1   0 0000  3e  01 c736 10.8.0.2  10.34.1.150 

92 bytes from 10.34.1.153: Destination Host Unreachable
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 5400 581a   0 0000  3e  01 0ece 10.8.0.2  10.34.1.150 

92 bytes from 10.34.1.153: Destination Host Unreachable
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 5400 c678   0 0000  3e  01 a06f 10.8.0.2  10.34.1.150 

92 bytes from 10.34.1.153: Destination Host Unreachable
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 5400 46fe   0 0000  3e  01 1fea 10.8.0.2  10.34.1.150&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한참동안 timeout 만 나더니 153번으로 바뀐아이피로 뭔가가 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 아까와는 다르게 나왔다 안나왔다. 반복한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 출력 순서가 잘못 된것 같다.이번엔 ... 두개를 죽여볼까? 1, 3번을 죽여봐야겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;1686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LwZcK/dJMcahW8KvY/1ST3swI4qm6sPKHnNvQlK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LwZcK/dJMcahW8KvY/1ST3swI4qm6sPKHnNvQlK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LwZcK/dJMcahW8KvY/1ST3swI4qm6sPKHnNvQlK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLwZcK%2FdJMcahW8KvY%2F1ST3swI4qm6sPKHnNvQlK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1188&quot; height=&quot;1686&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;1686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘다 죽였더니 약 20초 후에 152도 붙기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;metallb 가 잘 동작하고 있다고 봐야겠지?&lt;/p&gt;
&lt;pre id=&quot;code_1763851454503&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;% curl -k http://10.34.1.150/
404 page not found&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 80 접속시에도 404 가 나오면 잘 나온다는 의미라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;traefik 이 잘 동작하지 않으면 connection refused 나 timeout 이 나온다고 하니.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 보면 일단 metallb, traefik 의 기본설정은 잘 된것으로 보면 될것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;샘플 서비스 배포&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 함쓰 개발시 사용하는 개발 이미지를 배포 해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/k8s 에 앱 디렉토리를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연습때는 /k8s/system/apps 이렇게 했는데 그냥 system 은 빼고 해야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;config.yaml&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvS6vI/dJMcadUGvWe/aslLXwYCJvf4BBaVcdPfuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvS6vI/dJMcadUGvWe/aslLXwYCJvf4BBaVcdPfuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvS6vI/dJMcadUGvWe/aslLXwYCJvf4BBaVcdPfuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvS6vI%2FdJMcadUGvWe%2FaslLXwYCJvf4BBaVcdPfuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1338&quot; height=&quot;566&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용은 비밀&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 이미지 배포용 파일&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deployment.yaml&lt;/p&gt;
&lt;pre id=&quot;code_1763852002839&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ggmoney-web-deployment
  labels:
    app: ggmoney-web
spec:
  # restart: always는 Kubernetes의 기본 설정이므로 replica 설정만 합니다.
  replicas: 2 # 안정성을 위해 2개로 시작
  selector:
    matchLabels:
      app: ggmoney-web
  template:
    metadata:
      labels:
        app: ggmoney-web
    spec:
      imagePullSecrets:
      - name: vultr-registry-secret
      containers:
      - name: ggmoney-web-container
        image: icn.vultrcr.com/gglab/ggmoney-web:latest # 이미지 설정
        ports:
        - containerPort: 3000 # 컨테이너 내부 포트
        
        # 환경 변수 설정 (ConfigMap 연결)
        envFrom:
        - configMapRef:
            name: ggmoney-web-config # 위에서 만든 ConfigMap 연결&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용을 자세히 보면 vultr-registry-secret 라는 부분이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 docker hub 에서 땡겨오겠지만 나는 vultr 의 container registry 를 사용하고 있으므로 인증 설정을 미리 해두어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vultr cloud 설정에 보면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z0bJV/dJMcacar6ue/PAddKa1eJKRO9KbdSNvfD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z0bJV/dJMcacar6ue/PAddKa1eJKRO9KbdSNvfD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z0bJV/dJMcacar6ue/PAddKa1eJKRO9KbdSNvfD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz0bJV%2FdJMcacar6ue%2FPAddKa1eJKRO9KbdSNvfD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;916&quot; height=&quot;254&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 인증 아이디와 패스워들 알려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 정보를 이용하여 vultr-registry-secret 를 미리 만들어 둔다.&lt;/p&gt;
&lt;pre id=&quot;code_1763852272169&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo kubectl create secret docker-registry vultr-registry-secret \
  --docker-server=icn.vultrcr.com/gglab \
  --docker-username=&amp;lt;vulr 에서 확인&amp;gt; \
  --docker-password='&amp;lt;vultr에서 확인&amp;gt;' \
  --namespace=default&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 service&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service.yaml&lt;/p&gt;
&lt;pre id=&quot;code_1763852333504&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: ggmoney-web-service
  labels:
    app: ggmoney-web
spec:
  type: ClusterIP 
  selector:
    app: ggmoney-web # Deployment의 Pod 레이블과 일치
  ports:
    - protocol: TCP
      port: 80 # Service의 포트 (클러스터 내부에서 접근할 포트)
      targetPort: 3000 # Pod 내부 컨테이너 포트 (Docker Compose의 컨테이너 포트)
      # NodePort는 K3s가 30000-32767 범위에서 자동으로 할당&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 눈여겨 볼 부분은 spec: type 이었던것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 서비스는 클러스터 내부에서만 통신한다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;traefik 을 거쳐서 올테니까 당연하겠지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 ingress.yaml&lt;/p&gt;
&lt;pre id=&quot;code_1763852815206&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ggmoney-web-ingress
  namespace: default
  annotations:
    kubernetes.io/ingress.class: traefik
    cert-manager.io/cluster-issuer: &quot;letsencrypt-prod&quot;

spec:
  ingressClassName: traefik
  # ----------------------------------------------------
  # 443 포트 활성화 및 인증서가 저장될 Secret 이름 지정
  # ----------------------------------------------------
  tls:
  - hosts:
    - money-dev.gglab.app
    secretName: money-dev-tls-secret
  # ----------------------------------------------------
  rules:
  - host: money-dev.gglab.app
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            # 연결할 ClusterIP 서비스 이름
            name: ggmoney-web-service
            # 서비스의 포트 (Service.yaml의 port: 80)
            port:
              number: 80&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 좀 어려운 부분인것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스로의 진입점인 traefik 을 사용하면 host 기반 라우팅을 할때 어디로 갈지 모르게 될것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 인증서 부분이 있는데 저렇게 해주면 letsencrypt 를 이용하여 자동으로 ssl 인증서 갱신도 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 갱신할때 사용할 traefik 기본설정을 해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 서비스의 도메인은 .app 도메인인데 이 도메인은 https 필수 도메인이다. 즉 http 로는 테스트 자체가 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 막아버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cert-Manager 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cert-manager 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1763879590055&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl apply -f kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ClusterIssuer 생성 (Let's Encrypt 설정)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cert-manager 설치 후에 서비스가 올라와야 하니까 30초 쯤뒤에 아래 파일도 적용시켜준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cluster-issuer.yaml&lt;/p&gt;
&lt;pre id=&quot;code_1763879864993&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo vi /k8s/cluster-issuer.yaml

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging # 테스트용 (Rate Limit 방지)
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: gglab.app@gmail.com # 사용자님의 이메일 주소
    privateKeySecretRef:
      name: letsencrypt-staging-key
    solvers:
    - http01:
        ingress:
          class: traefik # Traefik 인그레스 컨트롤러를 사용하도록 명시
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod # 실제 프로덕션용
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: gglab.app@gmail.com
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
    - http01:
        ingress:
          class: traefik

$ sudo kubectl apply -f cluster-issuer.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 yaml 준비가 모두 되었으니 하나씩 적용해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1763853279143&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gglabadmin@k3s-node1:/k8s$ sudo kubectl apply -f cluster-issuer.yaml
helmchartconfig.helm.cattle.io/traefik created
gglabadmin@k3s-node1:/k8s$ cd apps/ggmoney-web/
gglabadmin@k3s-node1:/k8s/apps/ggmoney-web$ sudo kubectl apply -f config.yaml 
configmap/ggmoney-web-config created
gglabadmin@k3s-node1:/k8s/apps/ggmoney-web$ sudo kubectl apply -f deployment.yaml 
deployment.apps/ggmoney-web-deployment created
gglabadmin@k3s-node1:/k8s/apps/ggmoney-web$ sudo kubectl apply -f service.yaml 
service/ggmoney-web-service created
gglabadmin@k3s-node1:/k8s/apps/ggmoney-web$ sudo kubectl apply -f ingress.yaml 
ingress.networking.k8s.io/ggmoney-web-ingress created&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 잘 적용되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 한번 브라우저 테스트를 해볼까.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q2HNh/dJMcah3T0kk/eOfkqrWHwGzkEkDRqhKze1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q2HNh/dJMcah3T0kk/eOfkqrWHwGzkEkDRqhKze1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q2HNh/dJMcah3T0kk/eOfkqrWHwGzkEkDRqhKze1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ2HNh%2FdJMcah3T0kk%2FeOfkqrWHwGzkEkDRqhKze1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;704&quot; height=&quot;708&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㅋㅋ 역시 한방에 되면 이상하지..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증서 문제가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cddUQq/dJMcaf57JiK/hNJkeYiAQNwHXF3aBfnJak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cddUQq/dJMcaf57JiK/hNJkeYiAQNwHXF3aBfnJak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cddUQq/dJMcaf57JiK/hNJkeYiAQNwHXF3aBfnJak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcddUQq%2FdJMcaf57JiK%2FhNJkeYiAQNwHXF3aBfnJak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1094&quot; height=&quot;674&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;674&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저의 인증서를 열어보면 이런식으로 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발급 자체가 안되었다는 건데...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에서 일주일이 넘게 고생을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도 포스팅을 연결하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gglab.tistory.com/49&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://gglab.tistory.com/49&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Node Failover&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 잘 된 정리된 버전으로 github 에 소스로 올려두고 마무리.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>인프라/Kubernetes</category>
      <category>k3s</category>
      <category>K8s</category>
      <category>Kubernetes</category>
      <category>쿠버네티스</category>
      <author>GG.Lab</author>
      <guid isPermaLink="true">https://gglab.tistory.com/48</guid>
      <comments>https://gglab.tistory.com/48#entry48comment</comments>
      <pubDate>Sun, 23 Nov 2025 08:35:29 +0900</pubDate>
    </item>
  </channel>
</rss>