[작성자:] d_dive

  • Alibaba Cloud 와 AWS VPN 연결 가이드(Feat. Site-to-Site VPN Connection)

    Alibaba Cloud 와 AWS VPN 연결 가이드(Feat. Site-to-Site VPN Connection)

    🧷 Alibaba Cloud 환경

    Region : Singapore

    VPC : 172.16.0.0/16

    Subnet 1, 2 : 172.16.1.0/24, 172.16.2.0/24

    🧷 AWS 환경

    Region : South Korea(Seoul)

    VPC : 192.168.0.0/16

    Subnet 1, 2 : 192.168.10.0/24, 192.168.20.0/24


    🧷 알리바바 클라우드 VPN Gateway, AWS Virtual Private Gateway 생성

    멀티 클라우드 환경을 구축하기 위해 알리바바 클라우드와 AWS 클라우드를 VPN Gateway로 연결하려 합니다.

    먼저 알리바바 클라우드 VPN Gateway를 생성합니다.

    생성하면, 알리바바 클라우드에 IPsec Address IP가 2개 생성됩니다.(듀얼터널)

    그 다음, AWS 클라우드에서도 Virtual Private gateway를 생성합니다.

    Virtual Private Gateway 이름은 선택사항이고, Autonomous System Number(ASN)은 Amazon 기본 ASN으로 생성하셔도, Custom으로 다르게 설정해도 됩니다.

    그 이후, 생성한 Virtual Private Gateway에 연결하려는 VPC를 Attach 시켜줍니다.


    AWS Customer Gateway 생성

    앞서 만들어 두었던 Alibaba Cloud의 VPN Gateway의 2개의 IP를 AWS 클라우드 쪽 Customer Gateway로 생성합니다.

    예를 들어, Alibaba Cloud 쪽 2개의 IP가 (1,1,1,1 / 2.2.2.2)이라면 아래 생성화면에서 IP address 란에 입력해주면 됩니다.

    • BGP ASN은 기본값으로 두어도 되고, 다른 값으로 변경해서 넣어도 된다.

    🚨 AWS 에서 Customer gateway 생성할 때 하나의 IP밖에 등록하지 못하기 때문에 Customer gateway를 2개 만들어 줍니다.


    AWS Site-to-site VPN Connection 생성

    • Target gateway type : 미리 만들어 두었던 Virtual private gateway를 선택해줍니다.
    • Customer gateway : 미리 만들어 두었던 Customer gateway를 선택합니다.
      • customer gateway를 2개를 만들어 두었지만 하나만 설정이 가능합니다.
    • Routing Options : BGP를 요구한다면 Dynamic, 아니면 Static 으로 설정해도 상관없다.
    • Tunnel option은 default로 설정해도 됩니다. 다른 옵션을 원하면 Edit tunnel options를 선택해줍니다.

    site-to-site VPN Connection을 생성할 때 customer gateway를 하나만 붙일 수 있는데, Alibaba Cloud 쪽 VPN gateway는 듀얼 터널 모드로 2개를 연결해야 고가용성(HA)으로 구축이 가능하다.

    이런 경우, AWS site-to-site VPN Connection을 2개를 만들어서 각 Connection의 Tunnel #1 IP만 Alibaba Cloud의 VPN Gateway의 듀얼 터널IP로 연결하면 듀얼터널로 구성이 가능합니다.

    알리바바 클라우드 쪽 Customer Gateway에 AWS 쪽 Site-to-Site VPN의 IP를 등록해야하는데 Site-to-Site VPN를 2개 만들어 각 Tunnel#1의 IP만 등록해서 연결해주면 된다.


    Alibaba cloud 쪽 IPsec connection으로 최종연결

    앞서 만들었던 AWS Site-to-Site VPN을 만들고 나면 해당 VPN에 Configuration을 다운 받을 수 있는데 Generic으로 다운을 받고 나면 안쪽에 설정값들이 나옵니다.

    이 설정값들 기반으로 Alibaba Cloud 쪽 IPsec Connection 설정을 동일하게 해주고 완료하고 나면 최종적으로 완료할 수 있습니다.

    여기에 나와있는 설정값들을 AWS 쪽과 동일하게 설정해주면 됩니다.

    • LocalId : Alibab Cloud 쪽 IP
    • RemoteId : AWS 쪽 IP

    문제 없이 설정완료가 되면, 아래 이미지와 같이 Tunnel(Primary), Tunnel(Backup) UP 상태가 됩니다.


    정리

    Alibaba Cloud VPN Gateway는 듀얼터널모드로 생성이 가능하여, AWS VPN 과 연결할 때 AWS 쪽 Customer Gateway를 하나만 등록이 가능하여 헷갈린 부분이 많았습니다. 위에 정리한 내용을 토대로 설정한다면 조금이나마 도움이 될 수 있을 것 같습니다.

  • Alibaba Cloud | NAT Gateway – DNAT, SNAT 가이드

    Alibaba Cloud | NAT Gateway – DNAT, SNAT 가이드

    🧩 NAT Gateway란?

    NAT(Network Address Translation) Gateway는
    VPC 내부의 Private IP ↔ Public 네트워크 간의 주소 변환을 수행하는 서비스입니다.

    즉,

    • 내부 → 외부 통신할 때 IP를 바꿔줌
    • 외부 → 내부 통신할 때 IP를 바꿔줌

    그런데 이때 사용되는 기술이 바로

    • SNAT (Source NAT)
    • DNAT (Destination NAT)

    입니다.


    🟦 SNAT (Source NAT) — 내부 → 외부 통신

    ✔ 핵심 정의

    VPC 내부(Private IP)에서 외부(인터넷)로 나갈 때
    출발지 IP(Private → Public)로 변환해주는 기능.

    ✔ 왜 필요한가?

    Private Subnet에 있는 ECS들은 보통 Public IP가 없음
    그래서 그냥은 인터넷에 못 나감.

    내부 IP(10.x.x.x)로는 절대로 공인 인터넷에서 응답을 줄 수 없음

    그럼 어떻게 해야 할까?

    → NAT Gateway의 SNAT 규칙을 만들어
    내부 IP가 NAT Gateway가 보유한 Public IP로 변환되어 나가게 함.

    ⚙ 동작 흐름

    예시: Private ECS → Internet (yum update)

    1. ECS(10.0.1.10)에서 yum update 요청
    2. NAT GW가 SNAT 규칙에 의해 출발지 IP를 공인 IP로 변경
      → 예: 47.xxx.xxx.xxx
    3. 인터넷으로 전송
    4. 응답은 NAT GW 공인 IP로 돌아옴
    5. NAT GW가 다시 Private IP(10.0.1.10)로 복원
    6. ECS에 전달

    📦 실제 규칙 예시

    SNAT:
    VPC subnet 10.0.1.0/24 → EIP 47.xxx.xxx.xxx

    🟥 DNAT (Destination NAT) — 외부 → 내부 통신

    ✔ 핵심 정의

    외부(인터넷)에서 들어오는 패킷의
    목적지 IP(공인 → 사설)로 변환해주는 기능.

    ✔ 왜 필요한가?

    보통 ECS는 Private Subnet에 있고
    직접 Public IP를 붙이지 않을 때가 많음.

    하지만 외부에서 특정 서비스를 들어와야 할 때가 있죠?

    예:

    • 웹서버 접속(HTTP/HTTPS)
    • SSH 접속
    • 특정 Port 기반 API 요청

    → 이때 사용하는 것이 DNAT 규칙(Port Forwarding)

    ⚙ 동작 흐름

    예: 외부 클라이언트 → ECS의 22번 포트로 SSH 접속

    1. 외부에서 47.xxx.xxx.xxx:22로 요청
    2. NAT GW의 DNAT 규칙을 확인
    3. 목적지 IP를 Private IP(10.0.1.10:22)로 변환
    4. 패킷 내부로 전달
    5. 응답도 NAT GW를 통해 역 변환해서 외부로 나감

    📦 실제 규칙 예시

    DNAT:
    EIP 47.xxx.xxx.xxx:22 → 10.0.1.10:22

    🟩 언제 어떤 NAT를 써야 할까?

    ✔ SNAT이 필요한 경우

    • Private ECS에서 인터넷 나가는 용도
    • Docker 이미지 pull
    • yum/apt update
    • 외부 API 호출
    • 소규모 백엔드 서버 구성

    ✔ DNAT이 필요한 경우

    • Public IP 없이 웹서비스를 노출하고 싶을 때
    • 특정 Private ECS에 SSH해야 할 때
    • 특정 Port로 들어오는 요청을 내부 포트로 보내고 싶을 때
      (Nginx, Game Server, Custom API 등)

    🧾DNAT 구성할 때 쉽게 안되었던 부분

    DNAT구성은 다음과 같이 했다.

    PC -> EIP -> NAT-GW(DNAT) -> ECS(Private IP)

    위와 같은 형태로 Private IP만 붙은 서버 앞단에 NAT-GW(EIP)만 붙여서 ssh(22)로 접속하려 했으나, 통신이 안되는 현상 발생했다.

    그 이유는,

    ✅ ICMP Retrieval 옵션이 “켜져 있으면” DNAT 트래픽이 차단될 수 있다.

    📌 ICMP Retrieval 이란?

    Alibaba Cloud Internet NAT Gateway에서 제공하는 특별한 기능으로,

    ICMP(Echo Request/Reply) 트래픽을 NAT Gateway가 역방향으로 되찾아(retrieve) 전달하도록 강제하는 옵션

    이다.

    일반적인 NAT는 TCP/UDP 위주의 state tracking을 하지만,
    ICMP는 별도의 처리가 필요하다.

    • ping 요청 (Echo Request)
    • ping 응답 (Echo Reply)

    이 두 개를 NAT 테이블에서 올바르게 매칭해야만 왕복이 성립된다.

    그래서 ICMP Retrieval은 내부 → 외부 방향의 ping(SNAT 기반)에서
    ICMP 패킷의 state tracking을 보다 안정적으로 처리하기 위한 기능이다.

    이 옵션이 DNAT 규칙(inbound 트래픽)과 충돌을 일으킨다는 점.

    📌 ICMP Retrieval을 켜면 DNAT가 안되는 이유

    Alibaba Cloud 공식 문서에서도 다음과 같이 설명한다.

    ICMP Retrieval은 SNAT(ICMP outbound) 전용 기능이며,
    DNAT inbound 트래픽과 NAT Translation entry를 공유한다.
    이때 충돌이 발생하면 DNAT 트래픽이 NAT GW에서 Drop된다.

    즉, 흐름이 이렇게 꼬인다:

    PC → EIP → NAT Gateway
    (여기서 ICMP Retrieval이 state를 먼저 가로챔)
    → DNAT 룰 적용 전 Drop
    → ECS까지 도달하지 못함

    📌 결론 정리

    ICMP Retrieval = SNAT 기반 ICMP 보조 기능
    ✔ DNAT inbound 트래픽과 충돌 가능성 있음
    ✔ 켜져 있으면 DNAT 기반 SSH/HTTP까지 실패할 수 있음
    끄면 DNAT가 즉시 정상 동작함 (정상적인 현상)
    ✔ Alibaba Cloud NAT Gateway의 설계 구조상 발생하는 특성임

  • Alibaba Cloud | 서로 다른 리전 VPN Gateway(IPsec-VPN)로 연결 가이드

    Alibaba Cloud | 서로 다른 리전 VPN Gateway(IPsec-VPN)로 연결 가이드

    구성

    Korea(Seoul) 리전

    VPC: 192.168.0.0/16

    Subnet-01(Zone A): 192.168.0.0/24

    Subnet-02(Zone B): 192.168.1.0/24

    ECS(Zone A): 1개

    VPN Gateway: 1개

    Singapore 리전

    VPC: 172.31.0.0/16

    Subnet-01(Zone A): 172.31.1.0/24

    Subnet-02(Zone B): 172.31.2.0/24

    ECS(Zone B): 1개

    VPN Gateway: 1개

    목표

    1. 한국 리전 ↔ 싱가포르 리전
    2. 각 리전의 VPN Gateway를 이용하여 IPSec Site-to-Site VPN 연결
    3. Dual Tunnel(Primary/Secondary) 구성

    각 리전에서 VPN GW 생성

    VPN GW 생성 시 필요사항

    1. VPN-GW Name
    2. Resource Group (선택)
    3. Gateway Type : Standard
    4. Network Type : Public
    5. Tunnels : Dual-tunnel
    6. VPC
    7. Subnet01
    8. Subnet02(서브넷 01, 02는 서로 다른 AZ로 구성해야 함.)
    9. Maximum Bandwidth
    10. Traffic
    11. IPsec-VPN : Enable
    12. SSL-VPN : Disable, Enable
    13. Duration : By Hour

    이렇게 생성 후, 듀얼 터널이기 때문에 VPN-GW IPSec Address는 2개가 할당된다.

    위와 마찬가지로, VPN-GW를 연결하려는 다른 리전에도 똑같이 생성해야 함


    각 리전에서 Custom Gateway 생성

    생성 시 필요사항

    1. Customer Gateway – Name
    2. IP Address(연결하려는 리전의 VPN GW IP) 예를 들어, 한국리전에서 Customer Gateway를 만든다면, 싱가포르 리전 VPN-GW IP
    3. ASN(Autonomous System Number) – BGP를 안쓰면 공백으로 나둬도 된다.
    4. Resource Group(선택)

    ASN(Autonomous System Number)란?

    “인터넷에서 하나의 큰 네트워크 그룹을 구분하기 위한 번호”

    인터넷은 수많은 네트워크가 연결되어 구성되고, 이 각각의 네트워크를 AS(Autonomous System, 자율시스템)이라고 부르고, 그 AS를 구분하기 위해 부여되는 번호가 ASN

    왜 ASN이 필요할까?

    “BGP 라우팅을 하기 위해서”

    위와 마찬가지로, Custom-GW를 연결하려는 다른 리전에도 똑같이 생성해야 함


    각 리전에서 IPsec Connections 생성

    각 리전에서 미리 만들었던 VPN gateway와 customer Gateway를 연결 해야 한다.

    IPsec-VPN 생성 시 필요 사항

    1. IPsec Connection Name
    2. Region
    3. Resource Group
    4. Bind VPN GW (IPsec Connection 생성 하려는 리전의 VPN GW)
    5. 라우팅 모드: 목적지 라우팅 모드, 보호된 데이터 흐름
    6. BGP 활성화
    7. 터널 1
      • customer GW
      • Pre-Shared Key
        • Encryption Conf
          • Version
          • Negotiation Mode
          • Encryption Algorithm
          • Authentication Algorithm
          • DH Group (Perfect Forward Secrecy)
          • SA Life Cycle (seconds)
          • LocalId(VPNGW IP)
          • RemoteId(Customer-GW IP)
        • IPsec Configurations
          • Encryption Algorithm
          • Authentication Algorithm
          • DH Group (Perfect Forward Secrecy)
          • SA Life Cycle (seconds)
        • DPD On/Off
          • IPsec VPN에서 상대방(피어)이 죽었는지 살아있는지 확인하는 기능
        • NAT Traversal On/Off
          • “NAT 환경에서도 IPsec VPN 터널이 문제없이 동작하도록 만들어주는 기술”
    8. 터널 2
      • 터널 1과 동일

    다른 리전에도 똑같이 IPsec Connections 생성


    Gateway Route 설정, Subnet-RT 설정

    각 리전에서 VPN GW를 타고 나갈 수 있도록 Route를 잡아줘야 함.

    ECS -> Subnet-RT -> VPN GW(A Region) -> VPN GW – Route -> VPN GW(B Region)..


    마지막으로, 잘 구성되었다면 통신 테스트 해보면 된다.(Ping)


    BGP 활성화 세팅법

    1. Custom GW 생성할 때, ASN 값 넣고 생성
    2. IPsec Connection 생성할 때, BGP conf
      • Tunnel CIDR Block
        • X.X.X.X/30
      • local BGP IP Address
        • X.X.X.X /32

    BGP conf 값 넣을 때 각 리전에서 연결할 터널

  • 쿼리 라우팅 설정하기: ProxySQL 활용법

    쿼리 라우팅 설정하기: ProxySQL 활용법

    ⚙️ Version

    OS: rocky 9.5

    ProxySQL 3.0.2

    백앤드 DB: MySQL 8.4.X 버전

    서비스 API 에서 붙어야하는 백앤드 DB(master[Writer/Read], ReadReplica[Read])가 두개였다. 서비스 API 에서 보내는 쿼리 CRUD에 맞게 라우팅을 해야하는 상황이었다.

    🔖 사전정보

    • Admin 포트 6032 (기본 계정 admin/admin)
    • MySQL 프록시 포트 6033
    • ProxySQL 설정은 메모리(Runtime) ↔ 디스크(SQLite, /var/lib/proxysql/proxysql.db) 레이어로 관리하고, 필요시 config 파일도 사용. Admin에서 변경 후 LOAD … TO RUNTIME; SAVE … TO DISK;가 관례.

    ⚙️ 설치 (Rocky/Alma/RHEL 8/9 계열)

    📌 3.0.2 설치 & 기동

    # 최신 릴리스 페이지에서 버전 확인
    # https://github.com/sysown/proxysql/releases
    
    # 1) wget으로 rpm 다운로드
    wget https://github.com/sysown/proxysql/releases/download/v3.0.2/proxysql-3.0.2-1-centos8.x86_64.rpm
    
    # 2) 설치
    sudo dnf install -y ./proxysql-3.0.2-1-centos8.x86_64.rpm
    
    # 3) 서비스 등록/시작
    sudo systemctl enable --now proxysql
    
    # 4) 버전 확인
    proxysql --version
    # ProxySQL version 3.0.2 ... 나와야 정상

    📌 관리자 접속 & 기본 보안

    mysql -h 127.0.0.1 -P6032 -u admin -padmin --prompt='Admin> '

    📌 백엔드(MySQL) 서버 등록 (HG10: Writer, HG20: Reader) + SSL

    -- 백앤드 서버 등록 하는 법
    DELETE FROM mysql_servers;
    INSERT INTO mysql_servers (hostgroup_id, hostname, port, use_ssl, max_connections)
    VALUES
      (10, '0.0.0.0', 3306, 1, 2000),   -- Writer
      (20, '0.0.0.0' , 3306, 1, 2000);   -- Reader
    
    LOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK;
    
    -- (선택) 헬스체크 튜닝
    SET mysql-monitor_username = 'monuser';
    SET mysql-monitor_password = 'monpass!';
    LOAD MYSQL VARIABLES TO RUNTIME; SAVE MYSQL VARIABLES TO DISK;
    
    -- 모니터링 계정은 백엔드 MySQL에도 생성 필요(각 노드DB에서):
    -- CREATE USER 'monuser'@'%' IDENTIFIED BY 'monpass!';
    -- GRANT USAGE, REPLICATION CLIENT ON *.* TO 'monuser'@'%';
    
    -- 연결 확인
    SELECT hostgroup_id, hostname, port, status, use_ssl FROM runtime_mysql_servers ORDER BY hostgroup_id, hostname;

    📌 애플리케이션 계정(ProxySQL 프런트 사용자) 생성

    -- 앱이 ProxySQL:6033 으로 들어올 자격
    DELETE FROM mysql_users WHERE username='app_user';
    INSERT INTO mysql_users (username,password,active,default_hostgroup,transaction_persistent)
    VALUES ('app_user','StrongAppPW!',1,10,1);
    
    -- default_hostgroup=10(master), 트랜잭션 중엔 Writer로 붙게 룰이 조정됨
    -- (필요 시 'frontend SSL'도 설정 가능)
    
    LOAD MYSQL USERS TO RUNTIME;
    SAVE MYSQL USERS TO DISK;
    
    -- 앱 쪽 DSN 예:
    -- mysql -h 127.0.0.1 -P6033 -u app_user -pStrongAppPW!

    ⚠️ 백앤드 접근 유저 등록 체크하는 법

    -- admin 에서(6032포트)
    
    SELECT username, default_hostgroup, transaction_persistent, active
        -> FROM mysql_users
        -> ORDER BY username;

    📌 글로벌 변수 (정규식/다이제스트/멀티플렉싱)

    SET mysql-query_processor_regex = 1;
    SET mysql-query_digests         = true;
    SET mysql-multiplexing          = 1;
    
    LOAD MYSQL VARIABLES TO RUNTIME; SAVE MYSQL VARIABLES TO DISK;

    ⚙️ 쿼리 라우팅 룰

    • 읽기(Read 전용 DB)
    • 쓰기(Writer 전용 DB)
    • 트랜잭션 중 쓰기 작업 발생(Reader DB ➡️ Writer DB) 이후 트랜잭션이 끝날때 까지 이후 리드 작업(Writer DB)
    DELETE FROM mysql_query_rules;
    DELETE FROM mysql_query_rules_fast_routing;
    
    -- 10) WRITE류 → HG10, flag=2 + next_query_flagIN=2 (다음 쿼리에 플래그 확실히 전달)
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,re_modifiers,
     destination_hostgroup,flagOUT,next_query_flagIN,comment,multiplex)
    VALUES
    (10,1,1,'^[[:space:]]*(/\\*.*?\\*/[[:space:]]*)*(INSERT|UPDATE|DELETE|REPLACE|MERGE)','CASELESS',
     10,2,2,'WRITE → HG10, flag=2 + next_query_flagIN=2',0);
    
    -- 20/21) TX 시작/종료 → Writer (TX 중 SELECT 강제용 flag=1/0)
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,re_modifiers,
     destination_hostgroup,flagOUT,comment,multiplex)
    VALUES
    (20,1,1,'^[[:space:]]*(/\\*.*?\\*/[[:space:]]*)*(BEGIN|START[[:space:]]+TRANSACTION)','CASELESS',
     10,1,'BEGIN/START → HG10, flag=1',0),
    (21,1,1,'^[[:space:]]*(/\\*.*?\\*/[[:space:]]*)*(COMMIT|ROLLBACK)','CASELESS',
     10,0,'COMMIT/ROLLBACK → HG10, flag reset',0);
    
    -- 30) SELECT ... FOR UPDATE/SHARE → Writer
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,re_modifiers,
     destination_hostgroup,comment,multiplex)
    VALUES
    (30,1,1,'^[[:space:]]*(/\\*.*?\\*/[[:space:]]*)*(SELECT|WITH)(/\\*.*?\\*/|[[:space:]])*.*FOR[[:space:]]+(UPDATE|SHARE)','CASELESS',
     10,'SELECT/CTE ... FOR UPDATE|SHARE → HG10',0);
    
    -- 40) TX 중 READ(SELECT/WITH) → Writer (flagIN=1)
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,re_modifiers,
     destination_hostgroup,flagIN,comment,multiplex)
    VALUES
    (40,1,1,'^[[:space:]]*(/\\*.*?\\*/[[:space:]]*)*(SELECT|WITH)(/\\*.*?\\*/|[[:space:]])*','CASELESS',
     10,1,'flag=1(TX) READ → HG10',0);
    
    -- 41) 쓰기 직후 ‘첫 READ(SELECT/WITH)’ 1회 → Writer & reset (flagIN=2)
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,re_modifiers,
     destination_hostgroup,flagIN,flagOUT,comment,multiplex)
    VALUES
    (41,1,1,'^[[:space:]]*(/\\*.*?\\*/[[:space:]]*)*(SELECT|WITH)(/\\*.*?\\*/|[[:space:]])*','CASELESS',
     10,2,0,'flag=2 → next READ(SELECT/WITH) is HG10 & reset',0);
    
    -- 42) 쓰기 직후 SELECT 전에 끼는 임의 쿼리(SET/SHOW/USE 등) → Writer (flag 유지)
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,re_modifiers,
     destination_hostgroup,flagIN,flagOUT,comment,multiplex)
    VALUES
    (42,1,1,'^','CASELESS',
     10,2,2,'flag=2 → non-SELECT goes HG10 (keep flag=2)',0);
    
    -- 50) 일반 READ(SELECT/WITH) → HG20 (공백/주석 유무 무관)
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,re_modifiers,
     destination_hostgroup,comment)
    VALUES
    (50,1,1,'^[[:space:]]*(/\\*.*?\\*/[[:space:]]*)*(SELECT|WITH)(/\\*.*?\\*/|[[:space:]])*','CASELESS',
     20,'READ(SELECT/CTE) → HG20');
    
    -- 90) 폴백 → HG10
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,destination_hostgroup,comment)
    VALUES
    (90,1,1,'.*',10,'fallback → HG10');
    
    -- 적용/저장
    LOAD MYSQL QUERY RULES TO RUNTIME;
    SAVE MYSQL QUERY RULES TO DISK;
    
    -- (확인) 런타임 상태
    SELECT rule_id, active, apply, match_pattern, re_modifiers,
           flagIN, flagOUT, next_query_flagIN, multiplex, destination_hostgroup, comment
    FROM runtime_mysql_query_rules
    ORDER BY rule_id;

    🔖 검증 체크리스트(Port: 6033)

    -- 기본 Reader
    SELECT @@read_only AS ro0, @@hostname AS h0;
    
    -- 쓰기 → Writer (10)
    INSERT INTO test_rw_split VALUES (1) ON DUPLICATE KEY UPDATE id=id;
    
    -- 첫 READ → Writer & reset (41)
    SELECT@@read_only AS ro1, @@hostname AS h1;
    
    -- 다음 READ → Reader (50)
    SELECT@@read_only AS ro2, @@hostname AS h2;
    
    -- 트랜잭션 중 READ → Writer (40)
    BEGIN;
    SELECT@@read_only AS ro_tx, @@hostname AS h_tx;
    COMMIT;
    SELECT@@read_only AS ro_after, @@hostname AS h_after;

    관리자 포트에서 확인(6032):

    SELECT rule_id, hits FROM stats_mysql_query_rules ORDER BY rule_id;
    -- 기대: 10 → (중간 쿼리 있었으면 42) → 41 → 50,  그리고 TX 경로는 20 → 40 → 21 → 50

    🔗 운영 체크리스트

    • 전역 멀티플렉싱: 운영 기본은 1(ON) — 우리가 확인 끝낸 동작은 이 전제에서 정상 동작
    • HG20 리더의 ConnOK 증가 확인:
    SELECT hostgroup, srv_host, status, ConnOK, ConnERR
    FROM stats_mysql_connection_pool ORDER BY hostgroup, srv_host;
  • OCI 클라우드 원격 피어링(RPC)을 통한 다른 리전 VCN 간 통신 설정하기

    OCI 클라우드 원격 피어링(RPC)을 통한 다른 리전 VCN 간 통신 설정하기

    ⚠️ 상황

    OCI(Oracle Cloud Infrastructure)에서는 기본적으로 한 리전 내에서만 VCN(Virtual Cloud Network) 간의 통신이 가능합니다.

    하지만 기업 환경에서는 서울 리전과 미국 리전처럼 서로 다른 리전에 있는 VCN을 연결해야 하는 경우가 많습니다.

    이럴 때 사용하는 방법이 바로 Remote VCN Peering(원격 VCN 피어링)입니다.


    🧱 원격 피어링(Remote Peering)이란.

    OCI 네트워킹에서 다른 리전 간 VCN을 연결할 때 사용하는 주요 리소스는 다음과 같습니다:

    • VCN (Virtual Cloud Network): 가상 네트워크
    • DRG (Dynamic Routing Gateway): 외부 네트워크와 VCN을 연결하는 게이트웨이
    • RPC (Remote Peering Connection): DRG 안에서 다른 리전 DRG와 연결하기 위한 논리적 링크

    즉, 원격 피어링은 각 리전에 DRG를 만들고 → DRG 안에 RPC를 만들고 → 두 RPC를 연결하는 방식입니다.

    연결이 완료되면 양쪽 VCN의 프라이빗 IP로 직접 통신할 수 있습니다.


    🔗 사전준비

    • 두 개의 VCN이 서로 다른 리전에 존재해야 함 (예: 서울, 미국 애시번).
    • 각 리전에 DRG(Dynamic Routing Gateway)를 생성해야 함.
    • 동일한 테넌시(계정) 내에 있으면 가장 간단하며, 서로 다른 테넌시 간에도 가능.

    🔗 DRG 생성 및 VCN 연결

    • 리전 A (예: 서울) → DRG 생성 → VCN에 Attach
    • 리전 B (예: 미국) → DRG 생성 → VCN에 Attach

    🔗 RPC(Remote Peering Connnection) 생성

    • 리전 A의 DRG에서 RPC-Korea 생성
    • 리전 B의 DRG에서 RPC-US-East 생성

    🔗 RPC 피어링 연결

    • 리전 A RPC-Korea “피어링 시작” 선택
      • 대상 RPC OCID 입력 (리전 B의 RPC-US)
      • 피어 리전(us-ashburn-1 등) 선택
    • 리전 B 피어링 상태 확인

    ⚙️ 라우팅 설정

    VCN이 상대방 네트워크로 갈 수 있도록 라우팅 규칙을 설정해야 합니다.

    • 리전 A VCN ➡️ Route Table에 10.20.0.0/16 ➡️ DRG
    • 리전 B VCN ➡️ Route Table에 10.10.0.0/16 ➡️ DRG

    ⚙️ 보안 설정

    VCN 내의 Subnet 그룹의 Security List에 VCN CIDR 대역을 허용해야 합니다.

  • DB 실행계획 차이: 원인 및 해결책

    DB 실행계획 차이: 원인 및 해결책

    🚨 문제 상황 소개

    • 개발 DB와 운영 DB, 같은 버전/같은 테이블/같은 쿼리인데 실행계획이 달라졌다.
    • 한쪽은 인덱스를 타고, 다른 한쪽은 풀스캔 + filesort 발생
    • 원인 추적 과정 간단히 설명 (EXPLAIN 결과 비교)

    🧱 실행계획 차이 원인

    • 옵티마이저는 비용 기반(cost-based) 으로 플랜을 선택.
    • 이때 “선택도(selectivity)” 추정이 중요.
    • 그런데 추정 방식이 통계값만 보느냐, 실제 인덱스를 내려가 보느냐에 따라 결과가 달라짐.

    ⚠️ 인덱스 다이브(Index Dive)란?

    • 개념: 옵티마이저가 실제 인덱스 레벨에서 값 분포를 확인해 정확한 선택도를 계산하는 과정.
    • 장점: 더 정확한 실행계획.
    • 단점: 옵티마이저가 실행계획 계산에 시간이 조금 더 걸림.

    ⚙️ EQ_RANGE_INDEX_DIVE_LIMIT 파라미터

    • 역할: 인덱스 다이브를 몇 개 값까지 허용할지 결정.
    • 0 → 인덱스 다이브 안 함 → 통계 의존 → 잘못된 플랜 위험 ↑
    • 200(기본) → 200개 값까지 다이브 → 더 정확한 플랜 선택
    • 왜 운영 DB만 “0”으로 되어 있었는지, 확인 과정 설명.
    ### 해결방법
    
    -- 현재 값 확인
    SHOW VARIABLES LIKE 'eq_range_index_dive_limit';
    
    -- 세션/글로벌에서 조정
    SET SESSION eq_range_index_dive_limit = 200;
    SET GLOBAL eq_range_index_dive_limit = 200;
    
    -- 영구 적용(MySQL 8 이상, MariaDB 10.3.30 version)
    SET PERSIST eq_range_index_dive_limit = 200;

    🔖 정리

    • 실행계획이 달라질 수 있는 이유는 데이터 분포 차이만 있는 게 아니라, 옵티마이저 파라미터 값 차이도 있다.
    • 운영/개발 환경 비교할 때는 SHOW VARIABLES 로 변수 값부터 확인.
    • 실행계획 이슈 정리 체크리스트:
      1. EXPLAIN 플랜 비교
      2. SHOW INDEX로 카디널리티 확인
      3. 관련 옵티마이저 변수(eq_range_index_dive_limit, innodb_stats_*, optimizer_switch 등) 확인
  • Ubuntu22.04: Docker 설치

    Ubuntu22.04: Docker 설치


    패키지 업데이트 & 필수 패키지 설치

    sudo apt update
    sudo apt install -y apt-transport-https ca-certificates curl software-properties-common gnupg lsb-release

    Docker 공식 GPG 키 추가

    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg

    Docker 저장소 추가

    echo \
      "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
      $(lsb_release -cs) stable" \
      | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

    Docker 설치

    sudo apt update
    sudo apt install -y docker-ce docker-ce-cli containerd.io

    (선택) sudo 없이 docker 명령어 사용하기

    sudo usermod -aG docker $USER
    newgrp docker

  • Nginx: 마이크로캐싱(Micro-Caching) 적용하기

    Nginx: 마이크로캐싱(Micro-Caching) 적용하기

    마이크로 캐싱이란?

    마이크로 캐싱은 아주 짧은 TTL(보통 1~10초) 동안 응답을 캐시에 저장해 두고, 같은 요청이 들어오면 백엔드 대신 캐시에서 바로 응답하는 기법입니다. 짧은 TTL이라 실시간성은 유지하면서도, 순간 트래픽 폭주(새로고침, 인기 API 호출 등)를 효과적으로 흡수할 수 있습니다.

    필요한 이유

    • 사용자가 새로고침을 여러 번 하거나, 다수 유저가 동시에 동일 API를 호출하면 백엔드(DB, 애플리케이션)에 부하가 급증합니다.
    • 대부분의 API/페이지는 몇 초 단위로 변경되지 않기 때문에 짧게라도 캐시하면 부하를 크게 줄일 수 있습니다.

    ⚙️ 동작원리

    1. 첫 요청 → 캐시에 없음 → 백엔드 호출 → 응답 저장
    2. TTL(예: 3초) 안의 동일 요청 → 캐시에서 바로 응답
    3. TTL 만료 → 백엔드 재호출 후 캐시 갱신

    Nginx는 $upstream_cache_status 값으로 캐시 상태를 알려줍니다.

    $upstream_cache_status 값과 의미

    의미
    MISS캐시에 없음, 백엔드 호출
    HIT캐시에서 응답
    EXPIRED캐시 있었지만 TTL 만료, 백엔드 호출 후 갱신
    BYPASS캐시 우회 조건에 해당(Authorization, Cookie 등)
    STALETTL 지났지만 오래된 캐시로 응답(갱신 중)

    기본 설정 예시 (API)

    • /etc/nginx/nginx.conf ➡️ http{} 블록
    # 캐시 저장소
    proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=my_cache:50m max_size=1g inactive=10m use_temp_path=off;
    
    # 인증 헤더 감지(개인화 요청은 캐시 제외)
    map $http_authorization $auth_cache_bypass {
        default 1;
        ""      0;
    }
    • server{} 블록
    # API만 마이크로 캐싱
    location ^~ /api/ {
        proxy_pass http://localhost:port;
    
        proxy_cache my_cache;
        proxy_cache_key "$scheme$host$request_uri";
        proxy_cache_valid 200 3s;          # TTL 3초
        proxy_cache_lock on;               # 동시 폭주 방지
        proxy_cache_use_stale updating;    # 갱신 중에는 이전 캐시 사용
    
        # 안전장치: 인증 요청/쿠키 발행 응답은 캐시 제외
        proxy_no_cache       $auth_cache_bypass $upstream_http_set_cookie;
        proxy_cache_bypass   $auth_cache_bypass $upstream_http_set_cookie;
    
        add_header X-Cache-Status $upstream_cache_status always;
    }

    🧱 테스트 방법

    # 첫 호출 → MISS
    curl -I https://example.com/api/data | grep -i x-cache-status
    
    # TTL 내 재호출 → HIT
    curl -I https://example.com/api/data | grep -i x-cache-status

    🚨 주의

    • 개인화 데이터(로그인 사용자별 데이터)는 절대 캐시하면 안 됨 → Authorization/Cookie 조건으로 캐시 우회
    • POST, PUT 요청은 Nginx 기본 설정상 캐시되지 않음
    • TTL이 너무 길면 데이터 최신성이 떨어지고, 너무 짧으면 효과가 적음 → 2~3초부터 시작해서 조정
    • 쿼리스트링이 다르면 별도 캐시로 저장되므로, 불필요한 랜덤 파라미터는 제거

    ⛺️ 확장: 정적 파일 + 마이크로 캐싱

    왜 정적 파일에도 마이크로 캐싱을 걸까?

    • 백엔드가 정적 파일(JS, CSS, 이미지 등)을 직접 서빙하는 경우, 많은 유저가 동시에 요청하면 백엔드 리소스를 불필요하게 소모합니다.
    • 특히 빌드 시 해시(app.abc123.js)가 붙은 파일은 변경 시 파일명이 바뀌므로 오래 캐시해도 안전합니다.
    • Nginx가 서버 앞단 캐시(프록시 캐시)로 한 번만 백엔드에 요청하게 하면, 다수의 요청을 빠르게 처리할 수 있습니다.

    🧱 설계 원칙

    1. 요청에 쿠키가 있어도 캐시 허용
      • 정적 파일에는 보통 개인화 데이터가 없음.
    2. 응답에 Set-Cookie가 있으면 캐시 금지
      • 혹시라도 백엔드가 정적 파일 응답에 세션 쿠키를 발행하는 경우 안전하게 우회.
    3. 브라우저 캐시도 함께 설정
      • 해시가 있는 파일은 1년 + immutable 권장.
    4. 서버 캐시 TTL은 짧게
      • 마이크로 캐싱 용도로 1분~10분 정도가 적당.


    ⚙️ Nginx 설정 예시

    # 정적 파일(해시 포함) + 마이크로 캐싱
    location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
        proxy_pass http://localhost:port;                # 백엔드 서빙 유지
    
        # --- 서버 앞단 캐시(Nginx 프록시 캐시) ---
        proxy_cache my_cache;
        proxy_cache_key "$scheme$host$request_uri";
        proxy_cache_valid 200 10m;                        # TTL: 10분 (해시 없는 파일은 더 짧게)
        proxy_cache_lock on;                              # 동시 폭주 방지
        proxy_cache_use_stale updating;
    
        # 요청 쿠키는 허용, 응답 Set-Cookie 시 캐시 제외
        proxy_no_cache       $auth_cache_bypass $upstream_http_set_cookie;
        proxy_cache_bypass   $auth_cache_bypass $upstream_http_set_cookie;
    
        # --- 브라우저 캐시 ---
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable" always;
    
        # 상태 확인용 헤더
        add_header X-Cache-Status $upstream_cache_status always;
    }

    ⚠️ 해시 없는 파일(예: /styles/main.css)이라면 proxy_cache_valid를 더 짧게(예: 1~5분) 두고, 브라우저 캐시도 max-age=300 정도로 조정하세요.


    🧱 테스트 방법

    # 첫 요청: MISS
    curl -I https://example.com/_next/static/chunks/app.abc123.js | grep -i x-cache-status
    
    # TTL 내 재요청: HIT
    curl -I https://example.com/_next/static/chunks/app.abc123.js | grep -i x-cache-status
    
    # 브라우저 캐시 헤더 확인
    curl -I https://example.com/_next/static/chunks/app.abc123.js | egrep -i 'cache-control|expires'

    🧤 운영 TIP

    • 정적 전용 서브도메인(예: static.example.com)을 사용하면 브라우저가 쿠키를 아예 붙이지 않아 캐시 효율 ↑.
    • 빌드 툴(Next.js, React, Vue 등)의 filename hashing을 켜두면 캐시 무효화 걱정 없이 TTL을 길게 가져갈 수 있음.
    • Nginx 프록시 캐시 + 브라우저 캐시를 동시에 쓰면 최초 1회만 백엔드 요청, 그 이후는 대부분 브라우저 또는 Nginx에서 응답.

    🧤 알아두어야 할 것

    🖥 서버 캐싱 (서버 단 캐시)

    • 위치: Nginx(리버스 프록시)나 CDN, API Gateway 같은 서버 측
    • 동작:
      1. 첫 요청 → 서버가 백엔드에 요청해 응답 저장
      2. TTL 안의 재요청 → 서버 캐시에서 바로 응답
      3. TTL 지나면 백엔드 재요청
    • 장점:
      • 백엔드 부하 감소 (API·정적 파일 모두 가능)
      • TTL 내에 여러 유저 요청을 합쳐서 처리 가능
      • 브라우저 캐시를 무시하고도 동작 가능 (모든 클라이언트 공통)
    • 단점:
      • TTL 안에서는 변경된 데이터가 바로 반영 안 될 수 있음
      • 서버 디스크/메모리 사용

    예시: proxy_cache (Nginx), CloudFront, Varnish, Redis 기반 캐시

    🌐 브라우저 캐싱 (클라이언트 단 캐시)

    • 위치: 사용자의 웹 브라우저 로컬 저장소
    • 동작:
      1. 첫 요청 시 응답을 브라우저가 저장
      2. TTL 또는 조건부 요청(ETag, Last-Modified) 안에서는 서버에 재요청 없이 바로 로컬에서 로드
      3. TTL 지나면 서버에 새로 요청
    • 장점:
      • 요청 자체를 줄여 네트워크 속도 향상
      • 서버에 트래픽이 아예 가지 않음
    • 단점:
      • 브라우저별·사용자별로 캐시가 따로 있음
      • TTL 안에 새 버전 배포 시 즉시 반영 안 됨
      • 캐시 무효화(무효화 정책, 파일명 변경) 필요

    예시: Cache-Control: max-age=31536000, immutable, ETag + If-None-Match


    🔥 빌드할때 왜 해시 기반 파일명으로 할까?

    📌 해시 기반 파일명의 핵심 목적: 

    캐시 무효화(Cache Busting)

    • 브라우저 캐싱은 빠른 로딩을 위해 오래 저장하지만, 이게 변경 반영에는 치명적일 수 있음.
    • 파일명이 고정이면 (app.js), 브라우저는 TTL 동안 절대 새로 다운로드 안 함.
    • 그래서 파일 내용이 바뀔 때마다 내용 기반 해시(MD5/SHA1 등)를 파일명에 포함시켜 버전처럼 사용.
    빌드 전빌드 후
    /static/app.js/static/app.9fceb3d2.js
    /static/style.css/static/style.ae3d9c9f.css

    🛠 동작 방식

    1. 빌드 시 JS/CSS 내용의 해시를 계산
    2. 해시를 파일명에 삽입
    3. HTML에서 그 새 파일명을 참조
    4. 브라우저는 이름이 다르면 다른 파일로 인식 → 즉시 새로 다운로드

    ✅ 장점

    1. 1년 캐시 가능
      • 파일명이 바뀌면 브라우저는 무조건 새로 받음 → TTL 무시
    2. 불필요한 네트워크 요청 제거
      • 내용이 안 바뀌면 계속 기존 캐시 사용
    3. CDN/프록시 캐시 효율 극대화
      • 전 세계 캐시 서버에 동일한 파일명이 유지

    ⚠️ 단점

    • 빌드 과정에서 HTML/JS 내부의 파일 경로를 전부 갱신해야 함
    • 해시 값이 조금이라도 달라지면 완전히 새 파일로 인식 → 캐시 100% 무효화 (좋기도 하고, 비효율일 수도 있음)

    📍 정리

    • 해시 기반 파일명 덕분에 Cache-Control: max-age=31536000, immutable 같은 1년 브라우저 캐싱 정책을 안전하게 쓸 수 있음.
    • 해시 없이 고정 파일명이라면, 이렇게 길게 캐싱하면 배포 때 문제 생김.

  • GO 언어로 동일한 데이터베이스 테이블 Row 비교하기: 고루틴 이용

    GO 언어로 동일한 데이터베이스 테이블 Row 비교하기: 고루틴 이용

     package main
    • Go 프로그램의 시작점입니다. main 패키지를 정의하면 이 파일은 실행 가능한 프로그램(entry point)을 만든다는 의미
    import (
    	"database/sql"
    	"fmt"
    	"sync"
    
    	_ "github.com/go-sql-driver/mysql"
    )

     구문:

    • Go 코드에서 외부 패키지를 불러올 때 사용합니다.

    각 줄 설명:

    • “database/sql”
      • Go에서 SQL 데이터베이스를 사용하기 위한 표준 인터페이스입니다. MySQL뿐 아니라 PostgreSQL, SQLite 등 다양한 DB 드라이버와 함께 사용할 수 있게 도와줘요.
    • “fmt”
      • 출력 관련 함수들이 있는 패키지입니다. 예: fmt.Println().
    • “sync”
      • 동시성 제어(멀티스레드)를 위한 패키지예요. 예: sync.WaitGroup, sync.Mutex 등.
    • _ “github.com/go-sql-driver/mysql”
      • MySQL 드라이버를 임포트하지만 직접 사용하지는 않겠다는 의미로 “_”를 붙입니다.
      • 이건 “드라이버를 등록만 하겠다”는 뜻입니다. → sql.Open(“mysql”, …)을 쓸 수 있게 만들어주는 역할을 합니다.
    var (
       username:password@tcp(ip:port)/database_name
    )

    접속할 DB 형식을 변수로 생성합니다.


    🔖 var 란?

    Go 언어에서 var는 변수를 선언할 때 사용하는 키워드입니다. 쉽게 말해, 어떤 값을 담아두는 “그릇”을 만드는 역할이에요.

    🔖 기본 문법

    var 변수명 타입 = 값
    
    // example
    // var name string = "HHHH"
    // var age int = 26

    🧤 타입은 생략 가능

    Go는 타입을 자동으로 알아낼 수 있기 때문에, 아래처럼 써도 됩니다:

    var name = "HHHH"
    var age = 26

    또는, 함수 안 에서만 간단히:

    name := "HHHH" //(:= 단축 선언)

    var tables = []string{
    	"table1", "Table2", ...
    }
    • tables: 비교할 테이블 이름들을 배열에 담아놓았습니다. 이 배열의 모든 테이블에 대해 row 수를 DB에서 조회할 수 있어요.

    func getRowCount(db *sql.DB, table string) (int, error) {
    	var count int
    	query := fmt.Sprintf("SELECT COUNT(*) FROM `%s`", table)
    	err := db.QueryRow(query).Scan(&count)
    	return count, err
    }
    • getRowCount: 주어진 DB와 table을 이용해 SELECT COUNT(*) 쿼리를 날리고 결과를 Count에 저장해요
    • fmt.Sprintf(…): 문자열 포맷팅입니다. 테이블 이름을 SQL에 삽입해요
    • QueryRow(…).Scan(…): 쿼리 결과의 첫 번째 행의 값을 변수에 저장해요

    func main() {
    • 프로그램의 시작점입니다
    db1, err := sql.Open("mysql", db1DSN)
    if err != nil {
        panic(err)
    }
    defer db1.Close()
    • DB1에 접속합니다.
    • 오류가 나면 프로그램 종료(panic)
    • defer db1.Close():main() 함수가 끝날 때 DB 연결을 닫습니다.

    fmt.Printf("%-50s | %10s | %10s\n", "Table Name", "DB1 Count", "DB2 Count")
    fmt.Println("--------------------------------------------------------------------------")
    • %-50s: 왼쪽 정렬로 50자 공간 확보
    • %10s: 오른쪽 정렬로 10자 공간 확보

    var wg sync.WaitGroup
    var mu sync.Mutex
    • wg: 여러 고루틴의 완료를 기다리기 위한 WaitGroup
    • mu: 출력 시 여러 고루틴이 동시에 fmt.Printf를 사용하면 출력이 꼬일 수 있기 때문에 Mutex로 동기화

    for _, table := range tables {
        wg.Add(1)
    • 테이블 목록을 하나씩 순회하면서 고루틴을 실행할 준비를 합니다.
    • wg.Add(1): 하나의 고루틴이 추가됨을 알림

    go func(table string) {
        defer wg.Done()
    • go func() {…}(table): 각 테이블마다 비동기(병렬)로 고루틴을 실행합니다.
    • defer wg.Done(): 이 고루틴이 끝나면 WaitGroup에 완료 알림

    count1, err1 := getRowCount(db1, table)
    count2, err2 := getRowCount(db2, table)
    • 해당 테이블의 row 수를 DB1 ,DB2 각각에서 조회합니다.
    mu.Lock()
    • 여러 고루틴이 동시에 출력하면 글자가 겹치므로, 출력 전 락을 겁니다.

    if err1 != nil || err2 != nil {
        fmt Printf(""%-50s | %10v | %10v\n", table, "err", "err")
    } else {
        fmt.Printf("%-50s | %10d | %10d\n", table, count1, count2)
    }
    • 에러가 있으면 “err” 라고 출력
    • 정상일 경우에는 두 DB의 행 개수를 출력합니다.
    mu.Unlock()
    • 출력이 끝났으면 락을 해제합니다.

    전체 Code

    package main
    
    import (
    	"database/sql"
    	"fmt"
    	"sync"
    
    	_ "github.com/go-sql-driver/mysql"
    )
    
    var (
    	db1DSN = "username:password@tcp(ip:port)/database_name"
    	db2DSN = "username:password@tcp(ip:port)/database_name"
    )
    
    var tables = []string{
    	"example_table", "example_table", "example_table", "example_table"}
    
    func getRowCount(db *sql.DB, table string) (int, error) {
    	var count int
    	query := fmt.Sprintf("SELECT COUNT(*) FROM `%s`", table)
    	err := db.QueryRow(query).Scan(&count)
    	return count, err
    }
    
    func main() {
    	db1, err := sql.Open("mysql", db1DSN)
    	if err != nil {
    		panic(err)
    	}
    	defer db1.Close()
    
    	db2, err := sql.Open("mysql", db2DSN)
    	if err != nil {
    		panic(err)
    	}
    	defer db2.Close()
    
    	fmt.Printf("%-50s | %10s | %10s\n", "Table Name", "DB1 Count", "DB2 Count")
    	fmt.Println("--------------------------------------------------------------------------")
    
    	var wg sync.WaitGroup
    	var mu sync.Mutex
    
    	for _, table := range tables {
    		wg.Add(1)
    
    		go func(table string) {
    			defer wg.Done()
    
    			count1, err1 := getRowCount(db1, table)
    			count2, err2 := getRowCount(db2, table)
    
    			// 병렬 출력 충돌 방지
    			mu.Lock()
    			if err1 != nil || err2 != nil {
    				fmt.Printf("%-50s | %10v | %10v\n", table, "err", "err")
    			} else {
    				fmt.Printf("%-50s | %10d | %10d\n", table, count1, count2)
    			}
    			mu.Unlock()
    		}(table)
    	}
    
    	wg.Wait()
    }
  • 하나의 서버에서 두 도메인 SSL 설정 방법

    하나의 서버에서 두 도메인 SSL 설정 방법

    하나의 서버에서 2개의 웹 프론트 서버가 서로 다른 도메인으로 SSL(443포트) 사용 가능하다. 단, 아래 조건을 만족해야 한다.


    ✅ 전제 조건

    1. 도메인이 서로 다름
      • 예: a.example.com, b.example.com
    2. 각 도메인에 대해 유효한 SSL 인증서
      • Let’s Encrypt 등에서 도메인 별 인증서 발급
    3. 리버스 프록시(Web 서버)가 하나만 443 포트를 바인딩
      • Nginx나 Apache 같은 리버스 프록시를 사용해 도메인에 따라 백엔드 서버로 트래픽 분기

    🧱 구조 예시

    [Client]
       |
    443 포트 요청 (도메인 A 또는 B)
       ↓
    [Nginx Reverse Proxy]  (SSL 인증서 설정, 443 포트 점유)
       ├──> http://localhost:3001 (도메인 A용 웹 앱)
       └──> http://localhost:3002 (도메인 B용 웹 앱)
    # nginx conf 예시
    
    # a.example.com에 대한 설정
    server {
        listen 443 ssl;
        server_name a.example.com;
    
        ssl_certificate     /etc/ssl/certs/a_cert.pem;
        ssl_certificate_key /etc/ssl/private/a_key.pem;
    
        location / {
            proxy_pass http://localhost:3001;
        }
    }
    
    # b.example.com에 대한 설정
    server {
        listen 443 ssl;
        server_name b.example.com;
    
        ssl_certificate     /etc/ssl/certs/b_cert.pem;
        ssl_certificate_key /etc/ssl/private/b_key.pem;
    
        location / {
            proxy_pass http://localhost:3002;
        }
    }

    📝 요약

    항목가능 여부설명
    443 포트 공유 가능Nginx나 Apache 하나만 점유하고 도메인 분기로 분리
    서로 다른 도메인 사용server_name으로 분기 가능
    SSL 인증서 동시 적용도메인별 인증서 각각 설정 필요

    📌 SSL 없이도 가능? (HTTP:80)

    SSL 인증서를 사용하지 않고 하나의 서버에서 두 개의 웹 프론트 서버를 서로 다른 도메인으로 띄우는 것도 가능하다. 이 경우엔 443 포트가 아니라 80포트로 사용하게 된다.

    ✅ 요약: SSL 없이 가능한가?

    조건가능 여부설명
    SSL 없이 HTTP만 사용할 경우80 포트는 여러 도메인에서 공유 가능
    도메인 별로 서비스 가능server_name으로 도메인 분기 가능
    보안 (HTTPS) 없음모든 트래픽이 암호화되지 않음 (보안 취약)

    ⚠️ 주의할 점

    • 중간자 공격(MITM)에 취약 → 로그인, 개인정보 송수신이 있다면 반드시 SSL 사용 권장
    • 일부 브라우저에서는 HTTPS 필수로 요구 → 특히 PWA, 모바일 앱, OAuth, API 연동 등에서 문제 발생 가능

    💬 결론

    도메인이 있다면 포트를 나눌 필요 없음. Nginx 같은 리버스 프록시로 하나의 포트(80/443)에서 라우팅 가능

    도메인이 없으면 포트를 나눠야 함. 각 서비스가 독립된 포트를 차지해야 하므로