[{"content":" Obsidian으로 이관한 뒤, 실제 사용에 맞게 폴더 노트와 정렬 방식을 맞춘 기록\nNotion에서 Obsidian으로 넘어온 이유는 이전 글에 따로 정리했다. 이 글은 그 다음 단계에 가깝다.\nObsidian으로 넘어온 뒤 실제로 쓰려면 노트 구조를 먼저 정해야 했다. 파일만 계속 쌓이면 시간이 지날수록 찾기 어렵고, 나중에 블로그 글로 옮길 때도 정리가 번거로워진다.\n그래서 폴더 노트를 기본 단위로 잡고, 첨부 파일 위치와 파일 탐색기 정렬 방식까지 같이 맞췄다. 이 글에서는 동기화 설정이 아니라, Obsidian 안에서 노트를 어떤 구조로 관리할지에만 집중한다.\n1. 구성 방향 전체 방향은 단순하다.\n노트는 폴더 노트 구조로 관리한다. 첨부 파일은 각 노트의 attachments 폴더에 둔다. 상위 노트의 링크 순서를 하위 노트 정렬 순서로 사용한다. 정렬 파일은 자동으로 갱신한다. 노트는 오래 쌓일수록 처음 정한 구조의 영향을 크게 받는다. 그래서 처음에는 조금 번거롭더라도 구조를 일관되게 가져가는 편이 낫다고 판단했다.\n2. 폴더 노트 구조 노트는 가능한 한 아래 형태로 만든다.\n1 2 3 페이지명/ ├── 페이지명.md └── attachments/ 예를 들어 홈서버라는 주제가 있으면 홈서버.md 파일만 하나 두는 것이 아니라, 홈서버/홈서버.md 형태로 둔다. 이미지나 캡처 파일은 같은 폴더의 attachments 아래에 저장한다.\n이렇게 하면 한 주제에 대한 본문과 첨부 파일을 한 단위로 다룰 수 있다. 나중에 글로 옮기거나, 다른 주제로 분리하거나, 오래된 자료를 정리할 때도 판단 기준이 명확해진다.\n3. 사용하는 플러그인 현재 구조에서 핵심이 되는 플러그인은 아래와 같다.\n플러그인 용도 Folder Notes 폴더와 같은 이름의 대표 노트를 사용 Templater 새 폴더 노트 생성 자동화 Custom File Explorer Sorting sortspec.md 기준으로 파일 탐색기 정렬 Dataview 메타데이터 기반 목록이나 대시보드 작성 Importer 외부 자료를 Obsidian으로 가져올 때 사용 목차는 별도 플러그인을 추가하지 않고, Obsidian 기본 기능인 개요(Outline)를 먼저 사용하기로 했다. 데스크톱에서는 떠 있는 목차가 편할 수 있지만, 모바일에서는 화면을 가릴 가능성이 있어서 기본 기능으로 시작하는 편이 더 안정적이다.\n4. 새 폴더 노트 생성 새 노트를 만들 때마다 폴더, 본문 파일, 첨부 파일 폴더를 직접 만드는 것은 번거롭다. 그래서 Templater로 새 폴더 노트 생성 템플릿을 만들어 사용한다.\n동작 흐름은 아래와 같다.\n새 노트 이름 입력 현재 위치에 같은 이름의 폴더 생성 폴더 안에 같은 이름의 .md 파일 생성 attachments 폴더 생성 현재 노트에 새 노트 wikilink 삽입 결과적으로 아래 구조가 자동으로 만들어진다.\n1 2 3 입력한 이름/ ├── 입력한 이름.md └── attachments/ 이 방식의 장점은 상위 노트에서 하위 노트 링크를 먼저 만들고, 그 링크 순서를 그대로 정렬 기준으로 사용할 수 있다는 점이다.\n5. 상위 노트의 링크 순서를 정렬 기준으로 사용 Obsidian에서 파일 탐색기 순서를 직접 관리하다 보면, 실제 글의 흐름과 파일 목록의 순서가 어긋나는 경우가 많다.\n그래서 상위 노트에 적은 wikilink 순서를 기준으로 하위 노트의 탐색기 순서를 맞추도록 했다.\n예를 들어 상위 노트에 아래처럼 적으면,\n1 2 3 4 5 [[첫 번째 글]] [[두 번째 글]] [[세 번째 글]] 하위 폴더의 sortspec.md도 같은 순서로 만들어진다.\n1 2 3 4 5 6 --- sorting-spec: | 첫 번째 글 두 번째 글 세 번째 글 --- 이렇게 하면 문서 안에서 보는 목차 순서와 파일 탐색기에서 보는 순서가 같아진다.\n6. sortspec.md 자동 갱신 Custom File Explorer Sorting은 sortspec.md를 기준으로 파일 탐색기 순서를 바꿔준다. 문제는 이 파일을 매번 직접 관리하기 어렵다는 점이다.\n그래서 서버에서 작은 자동화 스크립트를 돌려 폴더 노트가 수정될 때마다 sortspec.md를 자동으로 갱신하도록 했다.\n동작 방식은 아래와 같다.\n일정 주기로 vault의 폴더 노트를 확인한다. 폴더 이름과 같은 이름의 .md 파일을 폴더 노트로 판단한다. 폴더 노트 안의 wikilink를 읽는다. 실제 존재하는 하위 노트나 폴더만 정렬 대상에 넣는다. 링크에 없는 하위 항목은 알파벳순으로 뒤에 붙인다. 정렬할 하위 항목이 없으면 빈 sortspec.md를 남기지 않는다. 정렬 파일은 아래 형식을 사용한다.\n1 2 3 4 5 --- sorting-spec: | 항목1 항목2 --- sorting-spec: |만 있고 아래 항목이 비어 있으면 플러그인에서 파싱 문제가 생길 수 있다. 그래서 하위 항목이 없는 폴더에는 sortspec.md를 만들지 않는 쪽으로 정리했다.\n7. 정리 기준 Obsidian에는 정리된 글뿐 아니라 임시 메모, 복사해온 자료, 나중에 볼 링크도 같이 들어온다. 다만 모든 자료를 오래 보관할 필요는 없다.\n복사해온 자료나 오래된 도구에 묶인 자료가 현재 작업 흐름과 맞지 않으면 정리 대상이다. 특히 다시 볼 가능성이 낮고 직접 정리한 내용도 아니라면 따로 보관하기보다 삭제하는 편이 낫다.\n반대로 직접 작업하면서 얻은 설정, 문제 해결 과정, 운영 기준은 최대한 설명형 글로 남긴다. 나중에 블로그 글로 옮길 수 있도록 처음부터 공개 가능한 문장 구조에 가깝게 작성하는 편이 좋다.\n8. 정리 이번 설정의 핵심은 Obsidian을 단순히 파일을 저장하는 공간으로 두지 않고, 오래 쓸 수 있는 노트 구조로 맞추는 것이다.\n폴더 노트로 주제 단위를 만들고, 상위 노트의 링크 순서를 정렬 기준으로 사용하고, 반복되는 정렬 파일 관리는 자동화했다.\n아직 완성된 구조라기보다는 계속 다듬어가는 운영 방식에 가깝다. 그래도 폴더 구조, 첨부 파일 위치, 정렬 기준이 정해져 있으니 노트가 늘어나도 덜 무너지는 형태가 됐다.\n","date":"2026-05-19T09:00:00+09:00","permalink":"https://joonlog.github.io/p/obsidian-custom-%EC%84%A4%EC%A0%95/","title":"Obsidian Custom 설정"},{"content":" Syncthing으로 홈서버 LXC ↔ Windows ↔ 모바일 vault 동기화\n0. 구조 1 2 3 4 5 Windows (SyncTrayzor) ↕ mgt LXC (Syncthing, 허브) ← Claude Code가 직접 접근 ↕ 모바일 (Syncthing-Fork) 모든 기기가 mgt LXC를 허브로 동기화한다. LXC는 항상 켜져 있기 때문에 각 기기가 켜지는 순간 동기화된다.\n1. LXC에 Syncthing 설치 Rocky Linux 9는 EPEL 저장소를 통해 설치한다.\n1 2 3 dnf install -y epel-release dnf install -y syncthing systemctl enable --now syncthing@root 2. LXC Syncthing 웹 UI 접근 Syncthing 웹 UI는 기본적으로 localhost:8384에서만 열린다. SSH 포트 포워딩으로 로컬에서 접근한다.\n1 ssh -L 8384:localhost:8384 mgt 브라우저에서 http://localhost:8384 접속\n3. Windows에 SyncTrayzor 설치 SyncTrayzor는 Windows용 Syncthing 래퍼로, 트레이 아이콘으로 백그라운드 실행된다.\n설치 후 동작 → 기기 아이디 보기 에서 Windows Device ID 확인 4. 기기 페어링 LXC 웹 UI에서 다른 기기 추가 클릭 → Windows Device ID 입력 → 저장\nWindows SyncTrayzor에 수락 요청이 뜨면 수락한다.\n5. 폴더 공유 설정 LXC 웹 UI에서 폴더 추가 클릭\n일반 탭: 폴더 경로 입력 (/root/obsidian) 공유 탭: Windows 기기 체크 저장하면 Windows SyncTrayzor에 폴더 수락 요청이 뜬다. 수락 시 Windows vault 경로를 지정한다.\n6. 모바일 연동 Android: Syncthing-Fork 설치 동일하게 Device ID 교환 후 페어링 폴더 공유 설정에서 모바일 기기 추가 체크 Obsidian 모바일에서 동기화된 폴더를 vault로 열기 ","date":"2026-05-03T09:00:00+09:00","permalink":"https://joonlog.github.io/p/syncthing%EC%9C%BC%EB%A1%9C-obsidian-vault-%EB%8F%99%EA%B8%B0%ED%99%94%ED%95%98%EA%B8%B0/","title":"Syncthing으로 Obsidian vault 동기화하기"},{"content":" Notion을 쓰다가 Obsidian으로 넘어온 이유\n홈서버가 이미 있었기 때문에 가능한 구조\n0. 배경 노션을 꽤 오래 써왔다. 메모, 자료 정리, 파일 첨부까지 하나의 앱에서 다 됐기 때문에 불편함이 없었다.\n바꾸게 된 계기는 AI를 더 적극적으로 활용하고 싶어서였다. 내가 쌓아온 데이터에 AI가 언제든지 접근할 수 있었으면 했다.\n1. Notion에서 AI 접근이 어려운 이유 Notion에 AI가 접근하려면 Notion API를 써야 한다.\n페이지마다 API 호출이 따로 필요 Rate limit: 초당 3건 수백 페이지를 실시간으로 뒤지는 건 현실적으로 불가능 Notion MCP도 결국 Notion API 위에 얹는 것이라 근본적인 제약은 동일하다.\n원하는 건 단순했다. AI가 내 데이터에 언제든지, 비용 없이, 즉시 접근할 수 있는 구조.\n2. Obsidian으로 넘어온 이유 Obsidian의 vault는 그냥 로컬 폴더다. 안에 있는 건 전부 마크다운 파일이다.\nAI 입장에서는:\n파일 직접 읽기 전체 검색 API 없음, Rate limit 없음, 비용 없음 파일이 로컬에 있는 한 AI는 즉시, 제한 없이 접근할 수 있다.\n부가적으로 Notion 무료 플랜의 파일 첨부 5MB 제한도 없어지고, 오프라인에서도 쓸 수 있다.\n3. 홈서버가 있었기 때문에 가능했다 Obsidian vault가 로컬 파일이라는 건 장점이지만, 그러려면 파일이 항상 어딘가에 있어야 한다.\nAI가 언제든지 접근하려면 파일이 항상 살아있어야 함 PC나 모바일은 꺼질 수 있음 홈서버에 mgt LXC가 이미 구축돼 있었다. 항상 켜져 있고, 내 네트워크 안에 있고, Claude Code가 직접 파일에 접근할 수 있다. 따로 뭔가를 더 구축할 필요 없이 vault를 여기 두면 됐다.\n홈서버가 없었다면 이 구조를 생각하지 않았을 것 같다.\n4. 구성한 구조 1 2 3 4 5 6 7 외부 PC / 모바일 └─ Obsidian + Syncthing ↕ 동기화 mgt LXC (홈서버, 항상 켜짐) ├─ /root/obsidian (vault 폴더) ├─ Syncthing (동기화 허브) └─ Claude Code (vault 직접 접근) Syncthing이 모든 기기와 mgt LXC를 동기화한다. PC나 모바일에서 메모를 쓰면 즉시 LXC에 반영되고, AI는 LXC의 파일을 직접 읽는다.\n동기화 도구로 Syncthing을 선택한 이유는 단순하다.\n무료, 오픈소스 E2E 암호화 홈서버가 허브 역할을 하기 때문에 외부 릴레이 서버 없이 직접 연결 가능 기기 추가는 Device ID 교환으로 간단히 확장 실제 Syncthing 설정 방법은 다음 글에서 다룬다.\n","date":"2026-05-02T09:00:00+09:00","permalink":"https://joonlog.github.io/p/notion%EC%97%90%EC%84%9C-obsidian%EC%9C%BC%EB%A1%9C-%EB%84%98%EC%96%B4%EC%98%A8-%EC%9D%B4%EC%9C%A0/","title":"Notion에서 Obsidian으로 넘어온 이유"},{"content":" ~/.ssh/config SSH 접속 편의성\n매번 ssh -i key user@host -p port 처럼 옵션을 직접 입력하는 대신, ~/.ssh/config에 설정을 저장해두면 ssh \u0026lt;Host명\u0026gt; 만으로 접속 가능하다.\nWindows: C:\\\\Users\\\\\u0026lt;계정명\u0026gt;\\\\.ssh\\\\config Linux/Mac: ~/.ssh/config 1. 기본 구조 1 2 Host \u0026lt;별칭\u0026gt; 옵션 값 Host에 지정한 별칭으로 ssh \u0026lt;별칭\u0026gt; 접속 와일드카드 사용 가능 (Host *, Host node*) 2. 주요 옵션 HostName 실제 접속 대상 주소 (IP 또는 도메인) Host에 별칭을 쓰고 실제 주소는 여기에 기입 1 2 Host mgt HostName ssh-mgt.joonlog.store User 접속할 계정명 설정하지 않으면 현재 로컬 계정으로 접속 시도 1 User root IdentityFile 사용할 개인키 파일 경로 설정하지 않으면 ~/.ssh/id_rsa 등 기본 키 파일을 순서대로 시도 1 IdentityFile ~/.ssh/pve Port 접속할 SSH 포트 기본값은 22이며 커스텀 포트 사용 시 지정 1 Port 2222 ServerAliveInterval / ServerAliveCountMax 연결 유지 설정 ServerAliveInterval: 지정한 초마다 서버에 alive 패킷 전송 ServerAliveCountMax: 응답 없을 때 최대 재시도 횟수, 초과 시 연결 종료 idle 상태에서 연결이 끊기는 것을 방지 1 2 ServerAliveInterval 60 ServerAliveCountMax 3 ProxyCommand SSH 연결 전에 실행할 명령어로 연결을 중계 Cloudflare Tunnel SSH 접속 시 사용 1 ProxyCommand cloudflared access ssh --hostname %h %h는 HostName 값으로 자동 치환 ProxyJump 점프 호스트를 거쳐서 접속할 때 사용 VSCode Remote SSH나 내부망 서버 접속 시 유용 ProxyJump \u0026lt;Host별칭\u0026gt; 형식으로 이미 config에 정의된 Host 재사용 가능 1 2 3 4 5 Host node1 HostName \u0026lt;IP\u0026gt; User root IdentityFile ~/.ssh/pve ProxyJump mgt 위 설정 시 ssh node1 하면 mgt를 경유해서 node1에 접속 ","date":"2026-05-01T09:00:00+09:00","permalink":"https://joonlog.github.io/p/ssh-config-%EC%98%B5%EC%85%98-%EC%A0%95%EB%A6%AC/","title":"SSH Config 옵션 정리"},{"content":" 홈서버 작업 공간 분리를 위한 관리용 LXC 구성\nPVE는 하이퍼바이저만, 작업은 mgt LXC에서\n0. 관리용 LXC를 따로 만드는 이유 PVE는 VM과 LXC를 관리하는 하이퍼바이저다.\nClaude Code, git, kubectl 같은 도구를 PVE에 직접 설치해서 쓰는 것도 가능은 하지만, PVE는 하이퍼바이저 역할만 하도록 두는 게 맞다.\nPVE는 모든 VM/LXC의 상위 계층이기 때문에, PVE가 오염되면 모든 서비스에 영향 작업 도구 설치, 패키지 업데이트 등으로 하이퍼바이저 환경이 오염될 가능성이 있음 LXC는 가볍고 빠르게 생성/삭제 가능하기 때문에 작업 환경으로 적합 따라서 mgt라는 이름의 관리용 LXC를 별도로 만들어서, 이 LXC를 외부에서 SSH 접근하는 Bastion 겸 작업 공간으로 사용한다.\n1. PVE에서 LXC 생성 PVE 웹 콘솔 우측 상단 Create CT 버튼으로 생성\nGeneral 탭 CT ID: 103 (순서대로 부여) Hostname: mgt Unprivileged container: 체크 (보안상 권장) Nesting: 체크 (Docker 등 컨테이너 사용 시 필요) Password: 설정 Template 탭 Storage: local Template: 원하는 OS 선택 여기서는 Rocky Linux 9 사용 나머지 Disks / CPU / Memory / Network / DNS 탭은 용도에 맞게 설정 후 생성\n2. Cloudflare SSH Route 변경 기존에 PVE에 설치된 cloudflared가 SSH 터널을 localhost:22로 라우팅하고 있었다.\n즉, 외부에서 SSH 접속 시 PVE로 직접 붙는 구조였는데, 이를 mgt LXC로 변경한다.\n기존: localhost:22 → PVE 루트 쉘 변경: 192.168.100.10:22 → mgt LXC 루트 쉘 Cloudflare 대시보드 → Networks → Tunnels → 해당 터널 → Public Hostname → SSH Route 편집\nURL을 localhost:22 → 192.168.100.10:22 로 변경 변경 후 외부에서 SSH 접속 시 mgt LXC로 바로 접속된다.\n","date":"2026-04-30T09:00:00+09:00","permalink":"https://joonlog.github.io/p/proxmox%EC%97%90-%EA%B4%80%EB%A6%AC%EC%9A%A9-lxc-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0/","title":"Proxmox에 관리용 LXC 구성하기"},{"content":" 계층형 카테고리 커스텀 이후 발견한 버그 및 불필요한 코드 정리\n이전 포스트에서 구현한 계층형 카테고리 시스템에서 3가지 문제를 발견\n포스트 categories frontmatter의 오타로 인해 카테고리가 잘못 생성되고 있었음 generate-categories.go에서 Go의 slugify()와 Hugo의 urlize 동작 불일치로 인한 잠재적 404 위험 테마 사이드바 위젯 오버라이드 파일이 테마 기본값과 완전히 동일한데도 계속 유지되어, 테마 자동 업데이트 내용이 반영 안 되는 문제 1. 포스트 카테고리 오타 수정 frontmatter의 categories 배열에 오타가 있으면 카테고리 페이지가 잘못된 이름으로 생성됨 발견된 오타 5건 수정 파일 변경 2025-11-04-AnsiblePlaybookDockerInstall \u0026quot;Ansilbe\u0026quot; → \u0026quot;Ansible\u0026quot; 2025-11-05-AnsiblePlaybookcmdlogrotateopenfilesar \u0026quot;Ansilbe\u0026quot; → \u0026quot;Ansible\u0026quot; 2026-02-04-MiniPCPromoxInstall \u0026quot;Promox\u0026quot; → \u0026quot;Proxmox\u0026quot; 2026-02-23-PromoxNetworkVMSettings \u0026quot;Promox\u0026quot; → \u0026quot;Proxmox\u0026quot; 2025-09-16-troubleshootinghaproxyrestartfault \u0026quot;LoadBalancer\u0026quot; → \u0026quot;Load Balancer\u0026quot; 2. generate-categories.go 개선 2.1 ioutil 제거 io/ioutil 패키지는 Go 1.16부터 deprecated os.ReadFile / os.WriteFile으로 직접 대체 1 2 3 4 5 6 7 8 // 변경 전 import \u0026#34;io/ioutil\u0026#34; content, err := ioutil.ReadFile(path) ioutil.WriteFile(path, data, 0644) // 변경 후 content, err := os.ReadFile(path) os.WriteFile(path, data, 0644) 2.2 url 필드 추가 문제: Go의 slugify() 결과와 Hugo의 urlize 함수 결과가 불일치하는 경우 발생 가능 예: \u0026quot;Load Balancer\u0026quot; → slugify() → \u0026quot;load-balancer\u0026quot; vs Hugo urlize → \u0026quot;load-balancer\u0026quot; (일치하는 경우도 있지만, 특수문자·한글 혼용 시 불일치 가능) 레이아웃 템플릿에서 urlize로 생성한 링크와 실제 생성된 페이지 URL이 달라지면 404 해결: 생성되는 _index.md와 .md 파일에 url: 필드를 명시적으로 추가 Hugo가 url: 필드를 최우선으로 사용하므로 slugify 결과와 완전히 동일해짐 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // Primary _index.md primarySlug := slugify(primary) primaryContent := fmt.Sprintf(`--- title: \u0026#34;%s\u0026#34; description: \u0026#34;%s 카테고리의 모든 포스트\u0026#34; primary_category: \u0026#34;%s\u0026#34; layout: \u0026#34;category-primary\u0026#34; url: \u0026#34;/categories/%s/\u0026#34; --- `, primary, primary, primary, primarySlug) // Secondary .md secondarySlug := slugify(secondary) secondaryContent := fmt.Sprintf(`--- title: \u0026#34;%s\u0026#34; description: \u0026#34;%s \u0026gt; %s 카테고리의 포스트\u0026#34; primary_category: \u0026#34;%s\u0026#34; secondary_category: \u0026#34;%s\u0026#34; layout: \u0026#34;category-secondary\u0026#34; url: \u0026#34;/categories/%s/%s/\u0026#34; --- `, secondary, primary, secondary, primary, secondary, primarySlug, secondarySlug) 생성 결과 예시 1 2 3 4 5 6 7 8 9 10 11 12 13 content/categories/linux/_index.md --- title: \u0026#34;Linux\u0026#34; url: \u0026#34;/categories/linux/\u0026#34; ... --- content/categories/linux/load-balancer.md --- title: \u0026#34;Load Balancer\u0026#34; url: \u0026#34;/categories/linux/load-balancer/\u0026#34; ... --- 3. 사이드바 위젯 오버라이드 삭제 layouts/partials/widget/categories.html 테마 Stack v3의 기본 위젯 파일을 오버라이드하고 있었음 내용 비교 결과 테마 기본값과 완전히 동일 유지할 이유가 없는 상태 매일 실행되는 테마 자동 업데이트 워크플로우가 있는데, 이 파일이 존재하면 테마에서 해당 위젯이 변경되어도 반영이 안 됨 불필요한 오버라이드 파일은 삭제하는 것이 테마 독립성 유지에 적합 해당 파일 삭제 ","date":"2026-04-27T09:00:00+09:00","permalink":"https://joonlog.github.io/p/hugo-stack-%EA%B3%84%EC%B8%B5%ED%98%95-%EC%B9%B4%ED%85%8C%EA%B3%A0%EB%A6%AC-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81/","title":"Hugo Stack 계층형 카테고리 리팩토링"},{"content":" Cloudflare Tunnel을 사용해 SSH 접근하기 위한 설정\nCloudflare WARP를 사용해도 서버에 통신 가능하긴 하지만, 목적이 SSH라면 WARP는 좋은 방안이 되지 못한다.\nWARP는 UDP 기반 VPN 서비스이기 때문에 통신이 계속 끊어지기 때문\nSSH용 Routing + cloudflared를 사용해서 서버에 접속한다.\n0. 사전준비 Cloudflare Connectors 설정 https://joonlog.github.io/p/cloudflare-tunnel을-사용한-홈서버-promox-pve-콘솔-접근-설정/#1-cloudflared-설치-및-연동 1. Cloudflared Routes 설정 연동한 tunnel을 통해서 연결할 도메인 경로 및 라우팅 경로 설정\nHostname: 보유 도메인 기입 Type: SSH URL: localhost:22 여기까지 설정 완료 시 기입한 서브도메인으로 SSH 접근 가능\n2. Cloudflare Access 설정 서브도메인에 대한 인증 설정 Routes에 설정한 도메인 입력 Self-hosted 설명에 나와 있듯이 Policy 생성\nPVE 접속 도메인 접근 시 Email 인증하도록 Policy 설정 설정한 Policy 할당 3. Cloudflared 설치 windows\n1 winget install Cloudflare.cloudflared Rocky 8.10\n1 dnf install -y cloudflared 4. SSH 접근 Cloudflared 로그인\n1 cloudflared access login \u0026lt;SSH 접근 도메인\u0026gt; 이메일 인증 요구하는 화면 출력됨 SSH 접근\n이메일 인증 완료 후 SSH 접근 1 cloudflared access ssh --hostname \u0026lt;SSH 접근 도메인\u0026gt; 접근 성공! SSH alias\nssh 명령어로 접근하기 위한 alias Windows C:\\Users`\u0026lt;계정명\u0026gt;`.ssh\\config Linux ~/.ssh/config 1 2 3 4 Host pve HostName \u0026lt;SSH 접근 도메인\u0026gt; User root ProxyCommand cloudflared access ssh --hostname %h 접근 성공! ","date":"2026-04-20T09:00:00+09:00","permalink":"https://joonlog.github.io/p/cloudflare%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%84%9C%EB%B2%84-ssh-%EC%A0%91%EA%B7%BC/","title":"Cloudflare를 사용한 서버 SSH 접근"},{"content":" SSH가 끊겨도 터미널 세션과 실행 중인 작업을 서버에서 계속 유지시켜주는 세션 관리자\ntmux란 하나의 SSH/터미널 안에서 여러 터미널 세션을 관리하는 터미널 멀티플렉서 Claude Code 같이 긴 시간 세션을 유지해야 할 경우 효과적 SSH가 끊겨도 세션이 계속 살아있고, 다시 같은 세션에 재접속 가능 명령어 새 세션 생성\n1 tmux new -s 세션이름 세션 목록 확인\n1 tmux ls 기존 세션 접속\n1 tmux attach -t 세션이름 세션에서 빠져나오기\n종료 아님 1 Ctrl+b d 세션 종료\n1 tmux kill-session -t 세션이름 ","date":"2026-03-31T09:00:00+09:00","permalink":"https://joonlog.github.io/p/tmux-%EB%AA%85%EB%A0%B9%EC%96%B4/","title":"Tmux 명령어"},{"content":" Cloudflare용 UDP VPN\nCloudflare WARP를 사용하면 Tunnel 내부 네트워크로 직접 접근이 가능하다.\n다만 WARP는 UDP 통신이기 때문에 연결이 간헐적으로 끊어질 수 있다.\n사전 준비 Cloudflare Tunnel 활성화 Cloudflare WARP Cloudflare에서 제공하는 클라이언트 기반 WireGuard VPN 에이전트 디바이스에서 발생하는 트래픽을 Cloudflare 네트워크로 전달하여 보안 정책을 적용하거나 내부 네트워크에 접근할 수 있게 한다 1 2 3 4 클라이언트 → WARP Client → Cloudflare Edge → 인터넷 일반적인 WARP는 이렇지만, Zero Trust WARP 환경에서는 다음과 같이 동작한다.\n1 2 3 4 5 6 클라이언트 → WARP Client → Cloudflare Edge → Cloudflare Zero Trust 정책(보안 인증) → Cloudflare Tunnel → Private Network 따라서 Cloudflare Tunnel을 뚫어서 Zero Trust를 사전에 구성한 경우에는 WARP로 Tunnel 내부망에 접근이 가능하다.\n1. Private Network Route 등록 WARP가 접근할 내부망 CIDR을 등록\n1 2 3 4 Zero Trust → Networks → Routes → Add route 설정\n이 설정을 통해 Cloudflare는 다음 경로를 인식\n1 2 3 4 WARP Client → Cloudflare Edge → Tunnel Connector → 192.168.100.0/24 2. WARP 클라이언트 설치 WARP 클라이언트를 설치하고 Zero Trust에 연결되면 자동으로 device가 등록된다.\nZero Trust WARP 접근 시\n팀 이름 입력 후 사전 설정했던 인증 절차에 따라 연결 완료 확인 경로\n1 2 3 Zero Trust → Team \u0026amp; Resources → Devices 3. Device Profile 설정 WARP 트래픽 정책은 Device profile에서 관리된다.\nSplit Tunnels 설정\n현재 목적이 외부 장치에서 홈서버인 PVE 하위에 있는 노드로의 직접 접근이기 때문에, 192.168.100.x/24로의 라우팅만 설정\nInclude IPs and domains\nManage Type: Address Value: 192.168.100.0/24 해당 대역으로의 트래픽 전송 시에만 WARP를 사용하도록 설정 5. 정상 동작 확인 설정 적용 후 WARP 클라이언트를 설치한 장치에서 확인\ncmd에서: route print\n정상이라면 다음 경로가 생성\n1 2 3 4 5 인터페이스 목록 Cloudflare WARP Interface IPv4 테이블 192.168.100.0 255.255.255.0 이후 내부 노드 통신 가능\n정리 Cloudflare Tunnel만으로는 내부망 접근이 되지 않는다.\nWARP를 사용하려면 다음 설정이 필요하다.\n1 2 3 4 1. Private Network Route 등록 2. WARP Device 등록 3. Device Profile 정책 적용 4. Split Tunnel에 내부망 CIDR 추가 특히 Split Tunnel 설정이 맞지 않으면 WARP routing이 정상 동작하지 않으므로 반드시 확인해야 한다.\n","date":"2026-03-24T09:00:00+09:00","permalink":"https://joonlog.github.io/p/cloudflare-warp%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-tunnel-%EC%A0%91%EA%B7%BC/","title":"Cloudflare WARP를 사용한 Tunnel 접근"},{"content":" 공개된 GitHub에 민감한 정보가 올라갔을 경우 히스토리에서 민감 정보 제거하는 방법\ngit-filter-repo 설치\n1 2 curl https://raw.githubusercontent.com/newren/git-filter-repo/main/git-filter-repo -o /usr/local/bin/git-filter-repo chmod +x /usr/local/bin/git-filter-repo 교체할 문자열 파일 작성\n1 echo \u0026#34;삭제할문자열==\u0026gt;대체할문자열\u0026#34; \u0026gt; /tmp/passwords.txt mirror 리포에서 작업\n1 2 git clone --mirror \u0026lt;repo url\u0026gt; cd \u0026lt;repo\u0026gt;.git 히스토리 재작성\n1 git filter-repo --replace-text /tmp/passwords.txt --force origin 재등록\ngit filter-repo는 기본적으로 origin을 제거하기 때문에 재등록 1 git remote add origin \u0026lt;GITHUB URL\u0026gt; Force 푸시\n1 git push --force --mirror origin 주의사항\n히스토리를 건드렸기 때문에 모든 커밋 해시가 변경됨 팀 작업 중이면 모든 팀원이 git fetch \u0026ndash;all 후 로컬 브랜치 재설정 필요 GitHub에 캐시되었을 때나 PR, Fork에 남아 있을 경우 하기 url을 통해 수정 필요 https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository ","date":"2026-03-16T09:00:00+09:00","permalink":"https://joonlog.github.io/p/github-%EB%82%B4%EC%9D%98-%EB%AF%BC%EA%B0%90%ED%95%9C-%EC%A0%95%EB%B3%B4-%EC%A0%9C%EA%B1%B0/","title":"GitHub 내의 민감한 정보 제거"},{"content":" Promox 내부 VM을 위한 사설 네트워크 인터페이스 생성 필요\nPromox 네트워크 설정 현재 랜선이 그대로 미니 PC에 연결되어 있어 pve의 네트워크 인터페이스는 공인 IP만 존재한다\n따라서 Promox 내부 VM을 사설 IP로 관리하기 위한 네트워크 인터페이스가 필요하다\nNI 생성\n1 vim /etc/network/interfaces auto: 부팅 시 NI 활성\naddress: 고정 IP 설정\n이 IP가 하위 VM들의 Gateway 역할 post-up: 인터넷 연결\n내부 VM에서 외부 네트워크로의 라우팅 허용 출발지가 192.168.100.0/24 대역인 트래픽이 vmbr0으로 나갈 때 vmbr0 IP로 출발지 IP를 SNAT post-down: 트래픽이 vmbr1으로 내려갈때는 위 NAT 규칙 삭제\n1 2 3 4 5 6 7 8 9 10 auto vmbr1 iface vmbr1 inet static address 192.168.100.1/24 bridge_ports none bridge_stp off bridge_fd 0 post-up echo 1 \u0026gt; /proc/sys/net/ipv4/ip_forward post-up iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -o vmbr0 -j MASQUERADE post-down iptables -t nat -D POSTROUTING -s 192.168.100.0/24 -o vmbr0 -j MASQUERADE 신규 생성한 NI 기동\n1 2 systemctl restart networking ifup vmbr1 VM에서 인터넷 통신 필요 시 pve에서 nftables 활성화 필수\n기본 설정에선 vmbr1에서 vmbr0으로의 FORWARD를 막고 있음 Promox에서 VM 생성 rocky 8.10 이미지 다운로드\n1 2 cd /var/lib/vz/template/iso/ wget https://download.rockylinux.org/pub/rocky/8.10/isos/x86_64/Rocky-8.10-x86_64-minimal.iso VM 생성\n좌측 pve 우클릭 - Create VM q35 설정\nBridge에서 위에서 만든 NI 할당\nPromox VM 설정 Start at boot를 활성화 해야지 promox 부팅될 때 해당 VM도 부팅\nVM NI 설정\n사설 IP 수동 할당 및 게이트웨이, dns 설정 1 2 3 4 nmcli con mod enp6s18 ipv4.method manual ipv4.addresses 192.168.100.11/24 ipv4.gateway 192.168.100.1 ipv4.dns 8.8.8.8 nmcli con mod enp6s18 connection.autoconnect yes nmcli con down enp6s18 nmcli con up enp6s18 ping 8.8.8.8 성공 시 설정 완료\n","date":"2026-02-23T09:00:00+09:00","permalink":"https://joonlog.github.io/p/promox-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%84%A4%EC%A0%95-%EB%B0%8F-vm-%EC%83%9D%EC%84%B1/","title":"Promox 네트워크 설정 및 VM 생성"},{"content":"목표 현재 외부에서 홈서버 Promox PVE 콘솔에 접근 시 공인 IP로 직접 접근하는 중 PVE 접속용 도메인 설정 외부에서 도메인 접근 시 Cloudflare 인증을 요구하도록 설정하고, 인증 성공 시에만 PVE 콘솔에 접근하도록 설정 0. 사전 준비 Cloudflare에 등록할 도메인 준비 도메인 네임서버를 Cloudflare로 설정 Cloudflare 회원가입 및 로그인\n로그인 시 나오는 네임서버 도메인 2개를 도메인 등록기관의 네임서버 설정에 등록\n초록색 체크 표시 뜨면 OK!\n1. Cloudflared 설치 및 연동 Networks - Connectors - Add a tunnel Cloudflared 선택 tunnel 이름 기입 OS 확인 및 Cloudflared 설치\nPromox는 Debian 리눅스 64bit cat /etc/os-release + uname -m 명령어로 확인 가능 root 사용자로 진행 Promox는 sudo가 설치되어 있지 않음 1 2 3 4 5 6 7 8 9 # Add cloudflare gpg key mkdir -p --mode=0755 /usr/share/keyrings curl -fsSL https://pkg.cloudflare.com/cloudflare-public-v2.gpg | tee /usr/share/keyrings/cloudflare-public-v2.gpg \u0026gt;/dev/null # Add this repo to your apt repositories echo \u0026#39;deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main\u0026#39; | tee /etc/apt/sources.list.d/cloudflared.list # install cloudflared apt-get update \u0026amp;\u0026amp; apt-get install cloudflared apt-get update 시 enterprise.proxmox.com 패키지 Unauthorized 오류가 나는 경우\nPromox의 기본 패키지는 유료 구독형이기 때문에 비활성화 필요 1 2 3 4 cd /etc/apt/sources.list.d mv -v pve-enterprise.sources pve-enterprise.sources.disabled mv -v ceph.sources ceph.sources.disabled 일반 패키지 리포 추가 1 2 3 4 5 6 7 cat \u0026gt; /etc/apt/sources.list.d/pve-no-subscription.sources \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; Types: deb URIs: http://download.proxmox.com/debian/pve Suites: trixie Components: pve-no-subscription Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg EOF 하단의 토큰 값 유출되지 않도록 유의\n서버가 시작될때마다 Tunnel이 자동으로 기동되게 할 경우\n1 cloudflared service install \u0026lt;토큰\u0026gt; 현재 터미널에서만 Tunnel 연결할 경우\n1 cloudflared tunnel run --token \u0026lt;토큰\u0026gt; 2. Cloudflare Routes 설정 연동한 tunnel을 통해서 연결할 도메인 경로 및 라우팅 경로 설정\nHostname: 보유 도메인 기입 Type: HTTPS URL: localhost:8006 Promox 서버에서 https://localhost:8006로 접근 시 TLS 해제 필요\n여기까지 설정 완료 시 기입한 서브도메인으로 PVE 접근 가능\n3. Cloudflare Access 설정 서브도메인에 대한 인증 설정 Self-hosted 설명에 나와 있듯이 Policy 먼저 생성\nPVE 접속 도메인 접근 시 Email 인증하도록 Policy 설정 설정한 Policy 할당 PVE 접속 도메인 접근 시 이메일 인증 요구하는 화면 출력 확인 설정 완료! 3. Promox 인바운드 접근 차단 Cloudflare Tunnel이 뚫렸기 때문에, 기존에 anyopen되어 있던 Promox 공인 IP에 대한 접근을 전부 차단\nZero-Inbound 설정\nPVE 콘솔 → Datacenter → Firewall → Options → Firewall 활성화\n설정 즉시 공인 IP를 통한 PVE 콘솔 차단 완료!\n결과 홈서버로서의 취약한 보안 조치 완료 홈 공인IP 숨기기 관리자용 페이지 접근 시 이메일 인증 적용 ","date":"2026-02-06T09:00:00+09:00","permalink":"https://joonlog.github.io/p/cloudflare-tunnel%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%9C-%ED%99%88%EC%84%9C%EB%B2%84-promox-pve-%EC%BD%98%EC%86%94-%EC%A0%91%EA%B7%BC-%EC%84%A4%EC%A0%95/","title":"Cloudflare Tunnel을 사용한 홈서버 Promox PVE 콘솔 접근 설정"},{"content":" 외부에서 홈서버에 안전하게 접근하기 위한 설정\n홈서버를 디폴트 상태로 외부에서 접근하기 위해선 IP와 Port로 접근이 필요하다.\n클라우드라면 Security Group / ACL 등등으로 제어가 쉽기 때문에 상관이 없지만, 홈서버는 집에서 사용하는 공유기 공인 IP를 외부에 오픈해야하기 때문에 보안적으로 위험하다.\n기본적으로 홈 IP는 AnyOpen되어 있고, 외부에서 접근한다는 것 자체가 Wifi든 모바일이든 매번 접근하는 IP가 가변적이기 때문에 IP로 접근을 차단하는 것에도 무리가 있다.\n특히 Proxmox 관리 콘솔(PVE)은 관리자 권한 탈취될 시 홈 네트워크 전체에 대한 통제권을 넘길 수 있기 때문에 매우 위험하다.\n따라서 Cloudflare Tunnel로 공인 IP를 숨기고, Cloudflare Access로 인증된 사용자만 접근할 수 있게 하려고 한다.\n1. Cloudflare Tunnel IP와 포트를 공개하지 않고도 외부에서 서버로 접속할 수 있게 하는 서비스 외부에서 Cloudflare를 거쳐서 서버에 접속 기존 인바운드+포트 포워딩 기반 접근과 다른 아웃바운드 기반 리버스 프록시 연결 Cloudflare Tunnel을 쓰는 이유 홈서버는 가정용 공유기에 부여된 공인 IP로 접근해야 하기 때문에 보안에 취약 기업에서 쓰는 서버들의 경우 WAF, IPS, F/W 등 방어 솔루션이 많지만, 홈서버는 사실상 방어 수단이 전무 Cloudflare를 거치면서 레이턴시가 늘어나더라도, 홈서버는 보안이 최우선 따라서 Zero-Inbound를 유지하고도 운영이 가능한 Cloudflare Tunnel 사용 기존 포트포워딩 방식 포트포워딩은 외부에서 들어오는 Inbound 연결을 서버가 직접 받아들이는 구조\n1 외부 클라이언트 → 공유기(포트포워딩) → 홈서버 이 방식에서는 서버가 외부에서 접근 가능한 상태가 되며, 열려 있는 포트는 스캔이나 공격의 대상이 될 수 있어서 보안적으로 매우 취약 Cloudflare Tunnel 방식 Cloudflare Tunnel은 서버가 먼저 Cloudflare로 Outbound 연결을 생성\n1 2 [항상 유지되는 연결] 홈서버(cloudflared) → Cloudflare cloudflared 데몬은 Cloudflare Edge와 여러 개의 TLS 연결을 지속적으로 유지 이 시점에는 외부 요청이 없어도 연결은 계속 생존 즉, 요청이 올 때마다 연결을 여는 구조가 아니다 외부 사용자가 서비스에 접근하면 요청은 먼저 Cloudflare에 도달\n1 외부 클라이언트 → Cloudflare 이 단계에서 Cloudflare는 아래 접근 제어를 수행\nCloudflare Access 기반 인증 (로그인, OTP, SSO, 서비스 토큰 등) IP, 국가, 정책 기반 접근 제어 인증 실패 시 요청 차단 인증과 접근 제어는 홈서버와 분리된 상태에서 Cloudflare가 전담 인증 이후 실 연결\n1 Cloudflare → 기존 Tunnel → 홈서버 인증을 통과한 요청만 이미 열려 있는 터널 연결을 통해 홈서버로 전달됨 2. Cloudflare Access Cloudflare tunnel만 사용한다면, 최초 목적 중 공인 IP 숨기기만 가능하다.\nPVE 콘솔에 대한 접근 자체를 막기 위해선 Cloudflare Access를 사용한 인증 절차 도입이 필요하다.\nCloudflare Access 도입 후의 구조 위 Cloudflare Tunnel의 2번째 단계에서 Cloudflare Access 추가\n1 외부 클라이언트 → Cloudflare Access → Cloudflare 정리 Cloudflare Tunnel은 요청을 역방향으로 전달하는 Outbound 기반 리버스 프록시 구조 홈서버 입장에서는 외부 클라이언트와 직접 통신하지 않는다 Cloudflare와 유지 중인 Outbound 연결만 사용하며, 외부에서 서버로 들어오는 Inbound 포트는 존재하지 않는다 따라서 Zero-Inbound로 외부에서 서비스 접근이 가능 내가 외부에서 접근해야 하지만 공개적으로 오픈하면 안 될 경우 Cloudflare Access 사용 실제 작업 과정은 다음 글 확인 ","date":"2026-02-05T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%ED%99%88%EC%84%9C%EB%B2%84-%EC%A0%91%EA%B7%BC-%EC%9A%A9%EB%8F%84%EB%A1%9C-cloudflare-tunnel%EC%9D%84-%EC%93%B0%EB%8A%94-%EC%9D%B4%EC%9C%A0/","title":"홈서버 접근 용도로 Cloudflare Tunnel을 쓰는 이유"},{"content":" 개발/인프라/K8S 연습용 홈서버로 Mini PC 구매\nMini PC에 Proxmox 설치\nMini PC 모델: Chatreey AN3P 1. Promox 이미지 준비 ISO 이미지로 설치 가능한 USB를 만들 때 사용 https://rufus.ie/ko/\n2. Mini PC 부팅 미니 PC 부팅하니 바이오스 화면 진입 가상화 기능을 써야 하니 관련 설정이 정상적인지 확인 Advanced → CPU Configuration → SVM / Virtualization / AMD-V 관련 항목은 무조건 Enabled USB를 통한 Promox 설치 시 막힐 수 있어서 Secure Boot 해제 Boot → Boot Configuration → Secure Boot → Disabled Boot → Boot Option 순서에서 Promox 설치 USB를 첫번째 순서로 조정 설정 저장 및 재기동 3. Promox 설정 부팅 후 Promox 기본 설정대로 설치 진행\n인터페이스는 일단 DHCP로 설정\n모든 설정 후 재부팅하면 설치 완료\n4. Promox PVE 접근 확인 재부팅하면 Promox CLI로 부팅됨 ID는 root, PWD는 설정한 비밀번호로 로그인 CLI에 출력되는 공인IP:Port로 외부에서 PVE(Promox 관리 콘솔) 접근 가능 PVE 콘솔 접근 정보는 계정 접근 정보와 동일 접근 성공! ","date":"2026-02-04T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%ED%99%88%EC%84%9C%EB%B2%84%EC%9A%A9-mini-pc%EC%97%90-promox-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0/","title":"홈서버용 Mini PC에 Promox 설치하기"},{"content":" 테스트 스크립트 작성 방법 및 결과값 해석에 대한 정리\nJS 파일로 시나리오를 작성 후 k6 명령어로 실행 k6 run test.js 로그인 없이 테스트 100명의 사용자가 1분동안 1초마다 재요청하는 테스트 vus: 동시에 실행되는 가상 사용자 수 duration: 테스트 총 수행 시간 sleep(1): 사용자 요청 간 간격을 모델링 tags.name: 요청 유형별 메트릭 분리를 위한 식별자 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import http from \u0026#39;k6/http\u0026#39;; import { sleep, check } from \u0026#39;k6\u0026#39;; export let options = { vus: 100, // 동시 사용자 duration: \u0026#39;1m\u0026#39;, // 테스트 시간 }; export default function () { let homeRes = http.get( \u0026#39;\u0026lt;URL\u0026gt;\u0026#39;, { tags: { name: \u0026#39;home\u0026#39;, page: \u0026#39;home\u0026#39; } } ); check(homeRes, { \u0026#39;home 200\u0026#39;: r =\u0026gt; r.status === 200, }); sleep(1); } 1 k6 run test.js 세션 로그인 성능 테스트 10명의 사용자가 1분동안 1초마다 재요청하는 테스트 세션 기반 로그인 성능 테스트 JWT, SSO 기반 로그인과는 스크립트 각각 다름 로그인 / 로그인 후 동작으로 분리 로그인 url에 POST로 계정정보를 전달 계정정보는 환경변수 LOGIN_ID, LOGIN_PW에 사전 세팅 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import http from \u0026#39;k6/http\u0026#39;; import { check, sleep } from \u0026#39;k6\u0026#39;; export let options = { vus: 10, duration: \u0026#39;1m\u0026#39;, }; export default function () { // 로그인 요청 let loginRes = http.post( \u0026#39;\u0026lt;로그인 URL\u0026gt;\u0026#39;, { id: __ENV.LOGIN_ID, password: __ENV.LOGIN_PW, }, { headers: { \u0026#39;Content-Type\u0026#39;: \u0026#39;application/x-www-form-urlencoded\u0026#39;, }, tags: { name: \u0026#39;login\u0026#39; }, } ); check(loginRes, { \u0026#39;login status 200\u0026#39;: r =\u0026gt; r.status === 200, }); // 로그인 이후 접근 페이지 let afterLoginRes = http.get( \u0026#39;\u0026lt;URL\u0026gt;\u0026#39;, { tags: { name: \u0026#39;after_login\u0026#39; }, } ); check(afterLoginRes, { \u0026#39;after login 200\u0026#39;: r =\u0026gt; r.status === 200, }); sleep(1); } 1 k6 run logintest.js 결과 http_req_duration avg: 평균 응답시간 p95: 상위 95% 요청의 응답시간 max: 최장 응답시간. 이상치(outlier) 존재 여부 판단 http_req_waiting 요청 전송 및 응답 수신을 제외한 서버 처리 지연 http_req_duration과 거의 동일한 경우 서버 처리 지연 http_req_failed HTTP 오류(4xx/5xx), 타임아웃, 네트워크 실패 포함 iteration_duration 여러 요청 + sleep을 포함한 시나리오 1회 수행 시간 정상 패턴 avg, p95가 완만하게 증가 오류율 0 유지 iteration_duration 선형 증가 비정상 패턴 특정 VU 구간에서 p95 급증 http_req_failed 증가 iteration_duration 비선형 증가 K6 시각화 보고서 생성 1 2 3 $env:K6_WEB_DASHBOARD=\u0026#34;true\u0026#34; $env:K6_WEB_DASHBOARD_EXPORT=\u0026#34;report.html\u0026#34; k6 run test.js 테스트 결과를 시각화된 html로 생성 ","date":"2026-01-19T09:00:00+09:00","permalink":"https://joonlog.github.io/p/k6-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%B0%8F-%EA%B2%B0%EA%B3%BC%EA%B0%92-%ED%95%B4%EC%84%9D/","title":"K6 스크립트 및 결과값 해석"},{"content":"K6 VU(가상 사용자) 기반의 HTTP 부하 생성 툴 스크립트에 정의된 시나리오를 수행 K6 설치 K6 GitHub 최신 Release에서 실행 파일을 다운로드\nhttps://github.com/grafana/k6/releases\nWindow winget을 사용해 설치 가능\n1 2 winget install k6 k6 version GitHub Release로도 설치 가능\nLinux GitHub Release에 있는 OS에 맞는 파일을 설치\nex) k6-v1.5.0-linux-amd64.tar.gz 1 2 3 tar -xvzf k6-v1.5.0-linux-amd64.tar.gz mv k6-v1.5.0-linux-amd64/k6 /usr/local/bin/k6 k6 version ","date":"2026-01-12T09:00:00+09:00","permalink":"https://joonlog.github.io/p/k6-%EC%84%A4%EC%B9%98-%EB%B0%A9%EB%B2%95/","title":"K6 설치 방법"},{"content":" KT Cloud에서의 루트 디스크 증설 방법\nKT Cloud 루트 디스크 증설 정책 root 디스크:\nlinux: 20GB 50GB, 80GB, 100GB로만 증설 가능 증설 이후 감설 불가능 사용 중이던 서버라면 Resize 수행 전 VM 스냅샷, 이미지 필요 windows 50GB 루트 디스크 증설 방법 루트 디스크 증설 위해 추가 작업 필요\nhttps://manual.cloud.kt.com/kt/compute-server-disk 서버 정지 후 디스크 사이즈 변경 필요 LVM 환경경에서의 루트 디스크 증설 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 lsblk fdisk -l # 파티션 삭제 및 생성 및 LVM으로 코드 변경 fdisk /dev/xvda p d 2 n p 2 p t 2 8e w lsblk pvresize /dev/xvda2 lvdisplay lvreize -l +20480 /dev/centos/root lvdisplay xfs_growfs(resize2fs) /dev/mapper/centos-root df -TH NON-LVM 환경경에서의 루트 디스크 증설 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 lsblk fdisk -l fdisk /dev/xvda p d 3 d 2 p # 파티션 생성 fdisk /dev/xvda n p 2 +97G n p 3 p w # swap 설정 fdisk /dev/xvda t 3 82 p w 재기동 resize2fs /dev/xvda2 lsblk mkswap /dev/xvda3 swapon /dev/xvda3 # UUID 추가 echo \u0026#34;UUID=\u0026lt;UUID\u0026gt; swap swap defaults 0 0\u0026#34; \u0026gt;\u0026gt; /etc/fstab ","date":"2025-12-26T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kt-cloud%EC%97%90%EC%84%9C-%EB%A3%A8%ED%8A%B8-%EB%94%94%EC%8A%A4%ED%81%AC-%EC%A6%9D%EC%84%A4%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95/","title":"KT Cloud에서 루트 디스크 증설하는 방법"},{"content":"SEO SEO는 Search Engine Optimization, 즉 검색 엔진 최적화로 구글이나 네이버 같은 검색 엔진에서 내 블로그가 더 잘 노출되도록 하는 작업이다.\n블로그를 운영하더라도, 검색에 노출되지 않으면 방문자가 내 블로그 도메인을 직접 입력해야만 접근 가능하다. 사실상 필수 설정.\nGitHub Pages + Hugo 환경 SEO 설정 Sitemap 자동 생성\nHugo는 별도 플러그인 없이 sitemap.xml 파일을 자동으로 생성 블로그 글 전체를 포함한 sitemap이 빌드 시 자동 생성됨 배포 후 https://\u0026lt;블로그주소\u0026gt;/sitemap.xml 로 접근 가능하면 성공 robots.txt 생성\n검색 엔진 크롤러에게 사이트 크롤링 규칙을 알려주는 파일 static/robots.txt 파일 생성 1 2 3 4 User-agent: * Allow: / Sitemap: https://\u0026lt;블로그주소\u0026gt;/sitemap.xml Google Search Console 등록\nGoogle Search Console 접속\nURL 접두어 방식 선택 → https://\u0026lt;블로그주소\u0026gt; 입력\nHTML 파일 다운로드 후 static/ 폴더에 저장\n1 2 static/ └── \u0026lt;verifications값\u0026gt;.html 배포 후 Google Search Console 내 소유권 확인\n생성된 사이트맵 Google Search Console에 등록\n등록 후에 즉시 활성화가 되는 것은 아님\n하루 정도 대기 필요\n","date":"2025-12-23T09:00:00+09:00","permalink":"https://joonlog.github.io/p/hugo-stack%EC%97%90%EC%84%9C-seo-%EC%84%A4%EC%A0%95google-searche-console/","title":"Hugo Stack에서 SEO 설정(Google Searche Console)"},{"content":"1. GitHub Discussions 활성화 GitHub Blog Repository 접속\nSettings 탭 클릭\nGeneral 섹션에서 Features 찾기\nDiscussions 체크박스 활성화\n2. Giscus 앱 설치 https://github.com/apps/giscus 접속\nInstall 클릭\njoonlog/joonlog.github.io 저장소 선택\n3. Giscus 설정값 생성 https://giscus.app/ko 접속\n다음 정보 입력:\n저장소: joonlog/joonlog.github.io\n페이지 ↔ Discussions 연결: 경로\nDiscussion 카테고리: Announcements 또는 General 선택\ngiscus 사용에서 출력된 값을 확인\ndata-repo data-repo-id data-category data-category-id 4. Hugo 설정 파일 수정 config/_default/params.toml 파일 수정\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 [comments] enabled = true provider = \u0026#34;giscus\u0026#34; # disqus에서 giscus로 변경 [comments.giscus] repo = \u0026#34;joonlog/joonlog.github.io\u0026#34; repoID = \u0026#34;\u0026lt;repo_id\u0026gt;\u0026#34; category = \u0026#34;\u0026lt;category_이름\u0026gt;\u0026#34; categoryID = \u0026#34;\u0026lt;category_id\u0026gt;\u0026#34; mapping = \u0026#34;pathname\u0026#34; lightTheme = \u0026#34;light\u0026#34; darkTheme = \u0026#34;dark\u0026#34; reactionsEnabled = 1 emitMetadata = 0 5. Git 커밋 후 재배포 추가 성공!\n","date":"2025-12-19T09:00:00+09:00","permalink":"https://joonlog.github.io/p/github-blog%EC%97%90%EC%84%9C-giscuss%EB%A1%9C-%EB%8C%93%EA%B8%80-%EA%B8%B0%EB%8A%A5-%EC%B6%94%EA%B0%80/","title":"GitHub Blog에서 Giscuss로 댓글 기능 추가"},{"content":" NLB 사용 시 Source IP 유지로 인해 발생한 ACG 방화벽 정책 이슈 해결\n문제 상황 NCP에서 WEB → NLB → WAS 구조로 서비스 구성 구축 단계에서는 아웃바운드 ANY OPEN으로 사이트 정상 동작 운영 전환을 위해 아웃바운드 제한하니 사이트에서 503 오류 발생 인바운드는 그대로였으므로 아웃바운드 정책 문제로 확인 WEB 서버에서 NLB의 Private IP로 아웃바운드를 허용했던 상황 원인 분석 WAS에서 tcpdump 확인 Source IP가 NLB Private IP일 줄 알았으나, 덤프 확인 시 WEB 서버 Private IP로 출력됨 트래픽을 단순 포워딩만 하고 클라이언트 IP를 그대로 전달 결과적으로 WAS는 NLB가 아닌 WEB 서버와 직접 통신하는 형태가 됨 동작 방식은 AWS에서의 NLB도 동일하며 L4 LB의 정상적인 구조로 확인 조치 WEB 서버의 아웃바운드에 WAS 서버의 Private IP 추가 후 정상 동작 확인 결론 L4 LB인 NLB NAT를 하지 않기 때문에 WAS는 항상 앞단 서버의 Real IP를 보게 된다.\n따라서 ACG 정책도 NLB가 아닌 실제 통신 경로(WEB → WAS) 기반으로 설정해야 한다.\n","date":"2025-12-14T09:00:00+09:00","permalink":"https://joonlog.github.io/p/ncp%EC%97%90%EC%84%9C-nlb-%EA%B5%AC%EC%84%B1-%ED%9B%84-503-%EB%B0%9C%EC%83%9D-troubleshooting/","title":"NCP에서 NLB 구성 후 503 발생 TroubleShooting"},{"content":" TroubleShooting - XEN 기반 Rocky Linux 8 업데이트 후 부팅 이슈\nNCP의 Xen 기반 Rocky 8 서버에서 커널 관련 업데이트 후 부팅이 되지 않을 때의 해결 방법\n환경 CSP: NCP 하이퍼바이저: Xen OS: Rocky 8.10 문제 상황 서버에 3rd 파티 솔루션 설치가 필요했던 상황 솔루션 설치 간 다량의 패키지 확인 및 설치가 필요한데, 이 과정 중 grub 업데이트가 포함되어 있었음 패키지 설치 후 재부팅 시 부팅이 되지 않음 NCP 콘솔에서 원격 접속 시 grub으로 빠지는 것을 확인 해결 방법 NCP 콘솔에서 서버 연격 접속 클릭\n파일 시스템 정상 확인\nls로 grub이 인식하는 디스크 파티션 확인 ls (hd0,msdos1)/ 같은 명령으로 OS 파일(커널, initramfs 등)이 실제로 존재하는지 확인 부팅할 파일 확인\n/boot 디렉터리에 있는 커널(vmlinuz-*)과 initramfs(initramfs-*) 파일이 있는지 확인\nex) vmlinuz-4.18, initramfs-4.18\nGrub 명령어로 부팅\n커널을 직접 로드하고 강제 부팅 1 2 3 4 grub\u0026gt; set root=(hd0,msdos1) grub\u0026gt; linux /vmlinuz-4.18.0-553.53.1.el8_10.x86_64 root=/dev/xvda2 ro grub\u0026gt; initrd /initramfs-4.18.0-553.53.1.el8_10.x86_64.img grub\u0026gt; boot 정상 부팅 확인 후 Grub 파일 재설치\n1 grub2-install /dev/xvda NCP 콘솔에서 서버 강제 정지 후 시작\nNCP 콘솔에서 서버가 운영중 상태로 표시되면 성공 이 방법으로 부팅 되지 않으면 스냅샷 생성 및 데이터 이동을 통한 복구 필요 참고 NCP 가이드: https://guide.ncloud-docs.com/docs/xen-rocky8-grub-boot-issue\n","date":"2025-12-06T09:00:00+09:00","permalink":"https://joonlog.github.io/p/ncp%EC%97%90%EC%84%9C-xen-%EA%B8%B0%EB%B0%98-rocky-linux-8-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%ED%9B%84-%EB%B6%80%ED%8C%85-%EC%9D%B4%EC%8A%88-troubleshooting/","title":"NCP에서 XEN 기반 Rocky Linux 8 업데이트 후 부팅 이슈 TroubleShooting"},{"content":" GitLab Omnibus 설정 간 URL에 공인/사설 IP 혼용 시 Docker Push 실패 문제 해결\n문제 상황 GitLab 웹 콘솔은 외부에서 접근해야 하니 공인 IP로 접근 필요 Jenkins나 다른 WAS 서버에서 이미지 Pull/Push할때 사설 IP로 접근 필요 그래서 gitlab.rb 파일에 external_url은 공인 IP, registry_external_url은 사설 IP 로 설정한 상황 여기서 docker push 테스트 시 푸시가 중간에서 끊김 원인 분석 GitLab Container Registry는 push 과정에서 이미지 blob을 업로드할 때, GitLab의 canonical URL(=external_url)을 기준으로 내부 리다이렉트 동작을 수행 동작 과정 사용자는 사설 IP(registry_external_url)로 push 요청을 보냄 Registry는 manifest 처리 과정에서 GitLab API 엔드포인트를 호출함 → 이때 GitLab의 external_url(공인 IP)을 참조 사설 → 공인으로 트래픽 루프가 발생 클라우드 서버라 루프백이 막혀 있어 트래픽이 중간에 끊김 즉 GitLab의 두 URL이 서로 다른 네트워크 망을 가리킴으로 인해 redirect loop 구조가 발생 조치 external_url, registry_external_url을 IP가 아닌 도메인으로 설정 도메인은 공인 IP와 매핑하고, 사설 IP가 필요한 WAS 서버나 Jenkins 서버에서는 hosts 설정으로 IP를 사설 IP로 지정되도록 설정 결론 GitLab Container Registry는 push 과정에서 canonical URL을 기반으로 내부 API 호출 및 redirect가 발생하기 때문에 external_url과 registry_external_url 이 서로 다른 네트워크를 가리키면 push 실패가 발생할 수 있다.\n이를 해결하려면 url을 도메인으로 설정해서 리다이렉트가 발생하지 않도록 해야 한다.\n","date":"2025-12-04T09:00:00+09:00","permalink":"https://joonlog.github.io/p/gitlab-container-registry%EC%97%90-docker-push-%EC%8B%A4%ED%8C%A8-troubleshooting/","title":"GitLab Container Registry에 Docker Push 실패 TroubleShooting"},{"content":" Nginx 리버스 프록시 템플릿\nHTTPS 리다이렉트 / SSL Termination / PHP-FPM / Java WAS 공통 패턴 정리\n아래 상황에 대한 Nginx 설정 정리 HTTP → HTTPS 리다이렉트 SSL termination + WAS 프록시 PHP-FPM 앞단의 Nginx (fastcgi) Java Tomcat 앞단의 Nginx (proxy_pass) LB를 통해 거쳐오는 실제 클라이언트 IP 식별 로그 포맷 / gzip / timeout 등 공통 설정 1. Nginx 전체 구성 개요 HTTPS 리다이렉트나 SSL Termination 설정은 별도 server블럭으로 분리\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 user nginx; worker_processes auto; error_log /var/log/nginx/error.log; pid /run/nginx.pid; include /usr/share/nginx/modules/*.conf; events { worker_connections 4096; } http { # 공통 설정 (X-Forwarded-For, 로그, gzip, timeout 등) # HTTPS 리다이렉트 + SSL 서버 블록 # PHP-FPM 서버 # Java Tomcat 서버 } 2. HTTPS 리다이렉트 80 포트 HTTP로 들어온 요청을 443 포트 HTTPS로 리다이렉션\n1 2 3 4 5 6 7 server { listen 80; server_name \u0026lt;도메인\u0026gt;; # 모든 HTTP 요청을 HTTPS로 강제 이동 return 301 https://$host$request_uri; } 3. HTTPS 서버 + SSL Termination + WAS 프록시 SSL Termination 수행\n클라이언트가 HTTPS로 접속 → 인증서 로드 후 TLS 핸드셰이크 → Nginx가 복호화된 HTTP를 WAS로 전달 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 server { listen 443 ssl http2; server_name \u0026lt;도메인\u0026gt;; ssl_certificate /etc/nginx/ssl/\u0026lt;crt 파일\u0026gt;; ssl_certificate_key /etc/nginx/ssl/\u0026lt;key 파일\u0026gt;; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers \u0026#39;ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256\u0026#39;; ssl_prefer_server_ciphers on; location / { # WAS로는 HTTP로 프록시 (Termination 이후) proxy_pass http://\u0026lt;WAS 서버 IP\u0026gt;:\u0026lt;포트\u0026gt;; # LB를 통해 트래픽이 들어올 경우 LB 정보가 아닌 실제 클라이언트 정보를 수신하도록 설정 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_http_version 1.1; proxy_set_header Connection \u0026#34;\u0026#34;; # 버퍼/타임아웃 (앱에 맞게 조정) proxy_buffers 8 16k; proxy_buffer_size 32k; proxy_connect_timeout 300; proxy_send_timeout 600; proxy_read_timeout 600; send_timeout 600; } } 4. HTTP 블록 공통 설정 Real IP / 로그 / gzip / timeout / buffer 등\n모든 server 블록이 공유할 설정 set_real_ip_from / real_ip_header LB 뒤에서 실제 클라이언트 IP 로그 복원, 안 하면 전부 LB IP로 찍힘 timeout / buffer는 서비스에 맞춰서 조정 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 http { include /etc/nginx/mime.types; default_type application/octet-stream; # LB를 통해 트래픽이 들어올 경우 LB 정보가 아닌 실제 클라이언트 정보를 수신하도록 설정 set_real_ip_from \u0026lt;LB IP 대역\u0026gt;/24; real_ip_header X-Forwarded-For; real_ip_recursive on; # 로그 포맷 log_format main \u0026#39;$remote_addr - $remote_user [$time_local] \u0026#34;$request\u0026#34; \u0026#39; \u0026#39;$status $body_bytes_sent \u0026#34;$http_referer\u0026#34; \u0026#39; \u0026#39;\u0026#34;$http_user_agent\u0026#34; \u0026#34;$http_x_forwarded_for\u0026#34; \u0026#34;$request_time\u0026#34;\u0026#39;; access_log /var/log/nginx/access.log main; # gzip 압축 gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml; gzip_min_length 1000; # 버퍼/타임아웃 (앱에 맞게 조정) keepalive_timeout 15; keepalive_requests 200; send_timeout 30s; client_body_timeout 30s; proxy_read_timeout 60s; proxy_connect_timeout 5s; proxy_send_timeout 60s; proxy_buffering on; proxy_buffer_size 4k; proxy_buffers 8 4k; } 5. PHP-FPM 앞단 Nginx 패턴 SCRIPT_FILENAME 잘못 설정하면 빈 화면 404 에러 발생\nfastcgi_param으로 IP/스킴 안 넘기면 로그인/도메인 등 의존 코드가 꼬임\n정적 리소스 캐싱 안걸어두면 모든 js/css 요청이 PHP로 들어가서 부하 증가\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 server { listen 80; server_name \u0026lt;도메인\u0026gt;; root /home/\u0026lt;프로젝트\u0026gt;/public; index index.php index.html; # 보안 헤더 add_header X-UA-Compatible \u0026#34;IE=Edge,chrome=1\u0026#34;; add_header X-XSS-Protection \u0026#34;1; mode=block\u0026#34;; add_header X-Frame-Options \u0026#34;SAMEORIGIN\u0026#34;; add_header X-Content-Type-Options \u0026#34;nosniff\u0026#34;; # 정적 리소스 캐싱 location ~* \\.(jpg|jpeg|png|gif|css|pdf|js|ico|svg|woff|woff2|ttf|eot|otf|map)$ { access_log off; log_not_found off; try_files $uri =404; expires 1M; add_header Cache-Control \u0026#34;public\u0026#34;; } charset utf-8; error_page 404 /error/404; # PHP-FPM FastCGI 연동 location / { include fastcgi_params; fastcgi_pass \u0026lt;WAS 서버 IP\u0026gt;:9000; fastcgi_index index.php; # PHP에서 실제 실행할 스크립트 경로 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; # 버퍼 fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; # X-Forwarded 설정 fastcgi_param X-Real-IP $remote_addr; fastcgi_param X-Forwarded-For $proxy_add_x_forwarded_for; fastcgi_param X-Forwarded-Proto $scheme; fastcgi_param X-Forwarded-Host $host; fastcgi_param X-Forwarded-Port $server_port; } } 6. Java Tomcat 앞단 Nginx 패턴 Java WAS는 proxy_pass로 전달하는 구조라 PHP보다 단순하지만 X-Forwarded 헤더가 중요 Spring Security, OAuth redirect 등 6-1. Upstream 정의 1 2 3 4 upstream was_backend { server \u0026lt;WAS 서버 IP\u0026gt;:8080; keepalive 200; } 6-2. server 블록 X-Forwarded-Proto 로 넘기지 않으면 Spring이 redirect URL을 http로 만들어서 https 접속인데 http로 튕기는 현상 발생\nHost를 내부 IP로 넘기면 외부 링크/Redirect URI에 내부 IP가 노출됨\n/healthz 같은 헬스체크 URL이 없으면 LB가 임의 URL(예: /) 헬스체크하면서 불필요 부하 발생\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 server { listen 80; server_name \u0026lt;도메인\u0026gt;; # --- 헬스체크 엔드포인트 --- location = /healthz { add_header Content-Type text/plain; return 200 \u0026#39;ok\u0026#39;; } # --- 기본 프록시 --- location / { proxy_pass http://was_backend; # 원본 정보 전달 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_http_version 1.1; proxy_set_header Connection \u0026#34;\u0026#34;; # 파일 업로드/다운로드가 크다면 필요에 따라 조정 # client_max_body_size 50m; } } ","date":"2025-12-01T09:00:00+09:00","permalink":"https://joonlog.github.io/p/nginx-%EB%A6%AC%EB%B2%84%EC%8A%A4-%ED%94%84%EB%A1%9D%EC%8B%9C-%ED%85%9C%ED%94%8C%EB%A6%BF/","title":"Nginx 리버스 프록시 템플릿"},{"content":" 20GB 추가디스크가 서버에 부탁됐을 때 디스크를 LVM으로 /data 경로에 마운트하는 방법\n0. 디스크 초기화 1 2 fdisk /dev/xvdb n p 1 enter enter t 8e 2 1. PV 생성 1 pvcreate /dev/xvdb1 2. VG 생성 1 vgcreate vgdata /dev/xvdb1 3. LV 생성 1 2 3 4 5 # 용량 지정해서 사용 lvcreate -L 20G -n lvdata vgdata # 남은 전체 공간 사용 lvcreate -l 100%FREE -n lvdata vgdata 4. 파일시스템 생성 1 mkfs.ext4 /dev/vgdata/lvdata 5. 마운트 및 fstab 등록 1 2 3 4 mkdir -p /data mount /dev/vgdata/lvdata /data echo \u0026#34;/dev/vgdata/lvdata /data ext4 defaults 0 2\u0026#34; \u0026gt;\u0026gt; /etc/fstab ","date":"2025-11-28T09:00:00+09:00","permalink":"https://joonlog.github.io/p/linux%EC%97%90%EC%84%9C-%EB%94%94%EC%8A%A4%ED%81%AC-%EC%B6%94%EA%B0%80-%EC%8B%9C%EC%97%90-lvm-%EC%A6%9D%EC%84%A4/","title":"Linux에서 디스크 추가 시에 LVM 증설"},{"content":" 기존 Apache가 설치된 환경에서 버전 업그레이드\nApache 2.4.63 신규 버전을 소스 컴파일로 설치하고 심볼릭 링크 기반 전환으로 서비스 다운타임 최소화\n1 2 3 httpd -t: 설정 문법을 테스트 httpd -S: 가상호스트 등 서버 설정을 확인 httpd -V: 컴파일 옵션과 버전을 조회 기존 apache 경로: /etc/httpd\n신규 apache 경로: /usr/local/apache\n0. 고려사항 기존 Apache 패키지 경로: /etc/httpd\n새 버전은 /usr/local/apache 아래에 설치해 충돌 방지 심볼릭 링크 기반 버전 관리\n/usr/sbin/httpd → /usr/local/apache/bin/httpd 로 링크하여 서비스 파일 수정 최소화\nconf, modules, workers.properties 등 핵심 설정만 이관\nlogs 경로는 기존 경로를 그대로 사용\n/usr/local/apache/logs → /etc/httpd/logs 로 링크\n업그레이드 중 필수 검증:\nhttpd -t 설정 문법 체크 httpd -S 가상호스트 및 include 파일 존재 여부 확인 없는 파일은 cp -arup 로 보완 1. 신규 버전 다운로드 및 배포 준비 /usr/local에 신규 파일 배치\n1 2 3 4 cd /usr/local tar -zxvf apache.2.4.63.tar.gz mv apache apache-2.4.63 ln -sfn /usr/local/apache-2.4.63 /usr/local/apache 2. 기존 설정 및 모듈 이관 conf / conf.d / conf.modules.d 파일 복사\n신규 버전에 기본 제공되지 않는 파일(proxy-html.conf 등)은 httpd -S에서 오류가 나는지 확인하고 불필요하면 삭제 1 2 3 4 5 6 7 8 # httpd.conf cp -arup /etc/httpd/conf/httpd.conf /usr/local/apache/conf/ # conf.d → extra/ 로 이관 cp -arup /etc/httpd/conf.d/* /usr/local/apache/conf/extra/ # conf.modules.d cp -arup /etc/httpd/conf.modules.d/ /usr/local/apache/conf.modules.d 모듈 복사\n1 2 3 4 5 6 7 cp -arup /etc/httpd/modules/mod_jk.so /usr/local/apache/modules/ cp -arup /etc/httpd/modules/mod_ssl.so /usr/local/apache/modules/ cp -arup /etc/httpd/modules/mod_rpaf.so /usr/local/apache/modules/ cp -arup /etc/httpd/modules/mod_proxy_http2.so /usr/local/apache/modules/ cp -arup /etc/httpd/modules/mod_brotli.so /usr/local/apache/modules/ cp -arup /etc/httpd/modules/mod_suexec.so /usr/local/apache/modules/ cp -arup /etc/httpd/modules/mod_systemd.so /usr/local/apache/modules/ workers.properties\n1 cp -arup /etc/httpd/conf/workers.properties /usr/local/apache/conf/ 3. 로그 경로 유지 기존 경로 재사용\n1 2 rm -rf /usr/local/apache/logs ln -s /etc/httpd/logs /usr/local/apache/logs 4. httpd.conf 설정 변경 ServerRoot 경로 수정 및 모듈 적용\n1 2 3 4 5 6 7 8 9 10 11 vim /usr/local/apache/conf/httpd.conf ServerRoot \u0026#34;/usr/local/apache\u0026#34; ServerSignature Off ServerTokens Prod LoadModule jk_module modules/mod_jk.so LoadModule rpaf_module modules/mod_rpaf.so Include conf.modules.d/*.conf 5. 설정 검증 Missing file 오류가 나오면 해당 파일을 /etc/httpd/ 기준으로 찾아 복사\nsyntax 오류가 나면 conf 파일 수정 후 재검증\n1 2 /usr/local/apache/bin/httpd -t # 설정 문법 검증 /usr/local/apache/bin/httpd -S # 가상호스트 및 include 파일 확인 6. systemd 서비스 연결 기존 서비스 파일(/usr/lib/systemd/system/httpd.service)은 수정하지 않고, 바이너리만 신규 httpd 로 연결\n1 2 3 4 5 6 7 service httpd stop cd /usr/sbin mv httpd httpd.bak ln -s /usr/local/apache/bin/httpd httpd service httpd start 7. 확인 필요 사항 httpd -t 정상 httpd -S 에 missing 파일 없음 mod_jk, mod_ssl, mod_rpaf 등 필수 모듈 로딩 정상 서비스 기동 후 /etc/httpd/logs/error_log 애플리케이션 로그 SSL 인증서 로드 정상 여부 가상호스트 매핑 정상 작동 여부 원복 필요 시 /usr/sbin/httpd.bak 로 즉시 롤백 ","date":"2025-11-27T09:00:00+09:00","permalink":"https://joonlog.github.io/p/apache-%EB%B2%84%EC%A0%84-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C/","title":"Apache 버전 업그레이드"},{"content":" App-DB 연동 간 사설 IP로 연동 불가 + VPN 사용 불가한 상황에서 공인 IP를 통한 암호화 통신 설정하는 방법\nApp 서버에서 DB 서버로 공인 IP로 통신할 경우 평문으로 통신하기에 데이터 탈취 위험이 존재 SSL 인증서를 사용한 암호화 통신으로 데이터 보호 환경 App 서버: PHP-FPM 8.3\nDB 서버: MariaDB 10.3\n연결 방식: App 서버 → DB 서버 공인 IP\n작업 과정 DB 서버에서 SSL 인증서로 사용할 CA 인증서 생성 DB에서 인증서를 사용하도록 설정 생성한 인증서를 앱 서버로 복사 앱 서버에서 인증서를 사용하도록 설정 PHP DB 연동 설정 + DB 연동 소스 코드 수정 필요 전체 과정 1. DB 서버 - SSL 인증서 생성 및 설정 디렉토리 생성\n1 2 mkdir -p /etc/mysql/ssl cd /etc/mysql/ssl CA Private Key / 인증서 생성\n1 2 openssl genrsa 2048 \u0026gt; ca-key.pem openssl req -new -x509 -days 3650 -key ca-key.pem -out ca-cert.pem -subj \u0026#34;/CN=MariaDB-CA\u0026#34; 서버용 인증서 생성\n1 2 3 4 openssl genrsa 2048 \u0026gt; server-key.pem openssl req -new -key server-key.pem -out server-req.pem -subj \u0026#34;/CN=MariaDB-Server\u0026#34; openssl x509 -req -in server-req.pem -CA ca-cert.pem -CAkey ca-key.pem \\ -CAcreateserial -out server-cert.pem -days 3650 권한 설정\n1 2 chmod 600 server-key.pem chown mysql:mysql server-key.pem server-cert.pem ca-cert.pem MariaDB 설정 추가\n/etc/my.cnf.d/server.cnf 1 2 3 4 [mysqld] ssl-ca=/etc/mysql/ssl/ca-cert.pem ssl-cert=/etc/mysql/ssl/server-cert.pem ssl-key=/etc/mysql/ssl/server-key.pem MariaDB 재시작\n1 systemctl restart mariadb SSL 활성화 확인\n1 2 3 SHOW VARIABLES LIKE \u0026#39;have_ssl\u0026#39;; SHOW SESSION STATUS LIKE \u0026#39;Ssl_cipher\u0026#39;; SHOW SESSION STATUS LIKE \u0026#39;Ssl_version\u0026#39;; 2. 앱 서버 - CA 인증서 적용 및 PHP SSL 사용 설정 디렉토리 생성\n1 mkdir -p /etc/mysql/ssl DB 서버에서 CA 인증서 복사\n/etc/mysql/ssl/ca-cert.pem 환경파일 설정\n.env 파일일 경우 1 2 3 DB_SSL=true DB_SSL_CA=/etc/mysql/ssl/ca-cert.pem DB_SSL_VERIFY=false PHP SSL 옵션 적용\nPDO로 DB와 연동할 경우\nDB.php 1 2 3 4 5 6 7 8 if (getenv(\u0026#39;DB_SSL\u0026#39;) === \u0026#39;true\u0026#39;) { if ($ca = getenv(\u0026#39;DB_SSL_CA\u0026#39;)) { $options[\\PDO::MYSQL_ATTR_SSL_CA] = $ca; } if (getenv(\u0026#39;DB_SSL_VERIFY\u0026#39;) === \u0026#39;false\u0026#39;) { $options[\\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false; } } php-fpm 재시작\n3. SSL 적용 검증 DB 계정에 SSL 강제 적용하도록 설정\n애플리케이션이 SSL 적용이 정상적이지 않다면 에러 발생 EX) SQLSTATE[HY000] [1045] Access denied for user 1 2 ALTER USER \u0026#39;gill\u0026#39;@\u0026#39;%\u0026#39; REQUIRE SSL; FLUSH PRIVILEGES; ","date":"2025-11-22T09:00:00+09:00","permalink":"https://joonlog.github.io/p/ssl-%EC%9D%B8%EC%A6%9D%EC%84%9C%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-mysql/mariadb-%EC%95%94%ED%98%B8%ED%99%94-%ED%86%B5%EC%8B%A0/","title":"SSL 인증서를 사용한 MySQL/MariaDB 암호화 통신"},{"content":" 컨테이너 환경에서 서버와 컨테이너의 MTU 불일치로 인한 네트워크 이슈를 정리한 글\n환경 NCP 서버 3대 구조: Nginx 컨테이너 서버 → PHP-FPM 컨테이너 서버 → DB 서버 MariaDB 10.3 문제 상황 구축한 사이트 이용 간 아무 패턴 없이 간헐적으로 타임아웃 발생 이슈 발생 주기는 완전 랜덤 사이트 내 모든 경로에서 간헐적으로 발생 서버 리소스나 부하는 모두 정상 에러 분석 처음에는 애플리케이션 단 문제로 생각해서 점검 세션 분리는 이미 되어 있는 상태 가비지 콜렉션 설정은 없어서 GC 설정 했으나 증상 동일 DB쪽 지표 모두 정상 원인 네트워크 쪽을 점검해보니 서버와 컨테이너의 MTU가 차이가 많이 나는 것을 확인 NCP는 서버의 기본 MTU가 8590이라고 함 컨테이너는 기본값 사용 중이었으니 1500 MTU 값 불일치로 발생했던 이슈로 확인 크기가 작은 요청은 정상이다가 일부 큰 패킷들이 드랍 → php 세션 끊어짐 → PHP가 요청을 끝까지 처리하지 못해서 타임아웃 해결 Docker 데몬 MTU 수정\n/etc/docker/daemon.json\n수정 후 docker 재기동 필요 1 2 3 { \u0026#34;mtu\u0026#34;: 8950 } 컨테이너 네트워크 MTU 수정\ncompose의 경우 mtu 값 명시\n1 2 3 4 5 networks: default: driver: bridge driver_opts: com.docker.network.driver.mtu: 8950 ","date":"2025-11-21T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-mtu-%EB%B6%88%EC%9D%BC%EC%B9%98%EB%A1%9C-%EC%9D%B8%ED%95%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%83%80%EC%9E%84%EC%95%84%EC%9B%83-%EC%9D%B4%EC%8A%88-troubleshooting/","title":"컨테이너 환경에서 MTU 불일치로 인한 네트워크 타임아웃 이슈 TroubleShooting"},{"content":" PHP 애플리케이션에 대한 CI/CD 파이프라인 설정 방법\n환경 Git 리포지토리 및 컨테이너 레지스트리: GitLab CI/CD: Jenkins PHP 버전: 8.3 파이프라인을 컨테이너에서 동작시키기 위한 Jenkins Agent 도커 설정 파이프라인 설정 Git 리포지토리 연동 Definition: Pipeline script from SCM\n파이프라인 구성에 필요한 스크립트를 SCM에서 가져오도록 설정 SCM: Git\nRepository URL: Git 리포지토리 URL\nCredentials: Git 리포지토리에 접근 가능하도록 저장한 Credential\nBranch Specifier: */main\nCI 파이프라인 컨테이너 이미지를 빌드하기 위한 Dockerfile도 Git 리포지토리에 있어야 함 PHP는 빌드 과정이 필요 없는 인터프리터 언어이기 때문에 소스 빌드 없이 이미지 빌드 → 이미지 푸시 과정만 필요 CI 완료 전 CD 파이프라인을 트리거 1. 환경 agent any로 jenkins agent 컨테이너를 파이프라인으로 사용 1 2 3 4 5 6 7 8 9 pipeline { agent any environment { APP_NAME = \u0026#39;\u0026lt;애플리케이션 명\u0026gt;\u0026#39; GITLAB_REGISTRY = \u0026#39;\u0026lt;컨테이너 레지스트리 URL\u0026gt;\u0026#39; GITLAB_PROJECT = \u0026#39;\u0026lt;Git 리포지토리 명\u0026gt;\u0026#39; GITLAB_CREDENTIAL_ID = \u0026#39;\u0026lt;저장한 Git PAT 토큰 ID\u0026gt;\u0026#39; } 2. Git 체크아웃 Git 리포지토리의 최신 커밋을 체크아웃 파이프라인 로그에 커밋 정보 출력 1 2 3 4 5 6 7 stages { stage(\u0026#39;Checkout\u0026#39;) { steps { checkout scm sh \u0026#39;git log -1 --oneline\u0026#39; } } 3. 컨테이너 이미지 빌드 및 레지스트리 푸시 Git 리포지토리에 있는 Dockerfile을 기반으로 이미지 빌드 Git 커밋 해시와 Jenkins 빌드 번호를 조합해서 태그 생성 GitLab PAT 토큰으로 로그인 후 푸시 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 stage(\u0026#39;Docker Build \u0026amp; Push\u0026#39;) { steps { script { def gitCommit = sh(returnStdout: true, script: \u0026#39;git rev-parse --short HEAD\u0026#39;).trim() def imageTag = \u0026#34;${env.BUILD_NUMBER}-${gitCommit}\u0026#34; def fullImage = \u0026#34;${GITLAB_REGISTRY}/${GITLAB_PROJECT}/${APP_NAME}\u0026#34; sh \u0026#34;\u0026#34;\u0026#34; docker build --no-cache -t ${fullImage}:${imageTag} . docker tag ${fullImage}:${imageTag} ${fullImage}:latest \u0026#34;\u0026#34;\u0026#34; withCredentials([usernamePassword(credentialsId: \u0026#34;${GITLAB_CREDENTIAL_ID}\u0026#34;, usernameVariable: \u0026#39;GITLAB_USER\u0026#39;, passwordVariable: \u0026#39;GITLAB_TOKEN\u0026#39;)]) { sh \u0026#34;\u0026#34;\u0026#34; echo \u0026#34;\\$GITLAB_TOKEN\u0026#34; | docker login ${GITLAB_REGISTRY} -u \u0026#34;\\$GITLAB_USER\u0026#34; --password-stdin docker push ${fullImage}:${imageTag} docker push ${fullImage}:latest docker logout ${GITLAB_REGISTRY} \u0026#34;\u0026#34;\u0026#34; } echo \u0026#34;Docker image pushed: ${fullImage}:${imageTag}\u0026#34; echo \u0026#34;Docker image pushed: ${fullImage}:latest\u0026#34; } } 4. CD 파이프라인 트리거 CI 파이프라인이 완료 되기 전 CD 파이프라인을 동작하도록 트리거 1 2 3 4 5 6 7 8 9 10 11 12 stage(\u0026#39;Trigger CD\u0026#39;) { steps { build job: \u0026#39;\u0026lt;CD 파이프라인 이름\u0026gt;\u0026#39;, parameters: [ string(name: \u0026#39;IMAGE_TAG\u0026#39;, value: \u0026#39;latest\u0026#39;), string(name: \u0026#39;DEPLOY_TARGET\u0026#39;, value: \u0026#39;ALL\u0026#39;) ], wait: true, // CD 끝날 때까지 기다릴지 propagate: true // CD 실패하면 이 Job도 실패로 처리할지 } } } 5. 파이프라인 결과 체크 1 2 3 4 5 6 7 8 9 post { success { echo \u0026#34;Build #${env.BUILD_NUMBER} succeeded for ${APP_NAME}\u0026#34; } failure { echo \u0026#34;Build #${env.BUILD_NUMBER} failed - check console output\u0026#34; } } } CD 파이프라인 Jenkins 서버에서 배포할 서버에 SSH로 접근 후 docker compose 파일을 기동하여 컨테이너 레지스트리의 이미지를 가져오는 방식으로 배포 배포할 서버가 여러 개일 경우를 고려해서 구성 환경 agent any로 jenkins agent 컨테이너를 파이프라인으로 사용 DB 접속정보 등 민감한 정보가 있는 .env 파일은 Jenkins Credential로 관리 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pipeline { agent any parameters { string(name: \u0026#39;IMAGE_TAG\u0026#39;, defaultValue: \u0026#39;latest\u0026#39;, description: \u0026#39;Docker image tag to deploy\u0026#39;) choice(name: \u0026#39;DEPLOY_TARGET\u0026#39;, choices: [\u0026#39;WAS1\u0026#39;, \u0026#39;ALL\u0026#39;], description: \u0026#39;WAS1: \u0026lt;WAS 서버 IP\u0026gt; | ALL: 전체 WAS 배포\u0026#39;) } environment { WAS1_HOST = \u0026#39;\u0026lt;WAS1 서버 IP\u0026gt;\u0026#39; // WAS2_HOST = \u0026#39;\u0026lt;WAS2 서버 IP\u0026gt;\u0026#39; // TODO: WAS2 추가 시 활성화 DEPLOY_PATH = \u0026#39;\u0026lt;배포 경로\u0026gt;\u0026#39; GITLAB_REGISTRY = \u0026#39;\u0026lt;컨테이너 레지스트리 URL\u0026gt;\u0026#39; GITLAB_PROJECT = \u0026#39;\u0026lt;Git 리포지토리명\u0026gt;\u0026#39; IMAGE_NAME = \u0026#39;\u0026lt;컨테이너 이미지 명\u0026gt;\u0026#39; DEPLOY_CREDENTIAL_ID = \u0026#39;\u0026lt;배포 서버 SSH 계정명\u0026gt;\u0026#39; GITLAB_CREDENTIAL_ID = \u0026#39;\u0026lt;저장한 Git PAT 토큰 ID\u0026gt;\u0026#39; TEACHER_ENV_FILE_CREDENTIAL_ID = \u0026#39;\u0026lt;Credential에 저장한 .env 파일 ID\u0026gt;\u0026#39; COMPOSE_FILE_LOCAL = \u0026#39;docker-compose.yaml\u0026#39; } 1. Git 체크아웃 파이프라인이 “Pipeline script from SCM” 으로 되어 있기 때문에 가능 docker-compose.yaml 파일을 가져오기 위함 1 2 3 4 5 6 7 stage(\u0026#39;Checkout\u0026#39;) { steps { // CD Job이 Pipeline from SCM로 설정돼 있다는 전제 checkout scm sh \u0026#34;ls -al ${COMPOSE_FILE_LOCAL} || echo \u0026#39;compose 파일 경로 확인 필요\u0026#39;\u0026#34; } } 2. 준비 배포할 이미지 태그 / 배포 대상 서버를 선택할 수 있게 설정 deployHosts: 배포 대상 호스트 목록 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 stages { stage(\u0026#39;Prepare\u0026#39;) { steps { script { echo \u0026#34;Deploying image: ${GITLAB_REGISTRY}/${GITLAB_PROJECT}/${IMAGE_NAME}:${params.IMAGE_TAG}\u0026#34; echo \u0026#34;Deploy target: ${params.DEPLOY_TARGET}\u0026#34; def deployHosts = [] if (params.DEPLOY_TARGET == \u0026#39;WAS1\u0026#39;) { deployHosts = [WAS1_HOST] } else if (params.DEPLOY_TARGET == \u0026#39;ALL\u0026#39;) { deployHosts = [WAS1_HOST] // TODO: WAS2 추가 시 -\u0026gt; deployHosts = [WAS1_HOST, WAS2_HOST] } env.DEPLOY_HOSTS = deployHosts.join(\u0026#39;,\u0026#39;) echo \u0026#34;Target servers: ${env.DEPLOY_HOSTS}\u0026#34; } } } 3. 배포 컨테이너 레지스트리에서 이미지 풀링 .env 파일을 Credential에서 배포 서버로 복사 git으로 체크아웃한 docker-compose 파일을 배포 서버로 복사 배포 경로에서 docker compose 명령어를 통한 애플리케이션 재기동 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 stage(\u0026#39;Deploy\u0026#39;) { steps { script { def hosts = env.DEPLOY_HOSTS.split(\u0026#39;,\u0026#39;) withCredentials([ usernamePassword(credentialsId: \u0026#34;${DEPLOY_CREDENTIAL_ID}\u0026#34;, usernameVariable: \u0026#39;DEPLOY_USER\u0026#39;, passwordVariable: \u0026#39;DEPLOY_PASS\u0026#39;), usernamePassword(credentialsId: \u0026#34;${GITLAB_CREDENTIAL_ID}\u0026#34;, usernameVariable: \u0026#39;GITLAB_USER\u0026#39;, passwordVariable: \u0026#39;GITLAB_TOKEN\u0026#39;), // .env 전체를 파일로 받음 file(credentialsId: \u0026#34;${TEACHER_ENV_FILE_CREDENTIAL_ID}\u0026#34;, variable: \u0026#39;\u0026lt;env 변수명\u0026gt;\u0026#39;) ]) { for (host in hosts) { echo \u0026#34;Deploying to ${host}...\u0026#34; sh \u0026#34;\u0026#34;\u0026#34; # 0) 임시 파일 경로(충돌 방지용) TMP_ENV=/tmp/env.\\$(date +%s) TMP_COMPOSE=/tmp/compose.\\$(date +%s).yaml # 1) .env / docker-compose.yaml 둘 다 /tmp 로 복사 sshpass -p \u0026#34;\\$DEPLOY_PASS\u0026#34; scp -o StrictHostKeyChecking=no \u0026#34;\\$\u0026lt;env 변수명\u0026gt;\u0026#34; \u0026#34;\\$DEPLOY_USER\u0026#34;@${host}:\u0026#34;\\$TMP_ENV\u0026#34; sshpass -p \u0026#34;\\$DEPLOY_PASS\u0026#34; scp -o StrictHostKeyChecking=no \u0026#34;${COMPOSE_FILE_LOCAL}\u0026#34; \u0026#34;\\$DEPLOY_USER\u0026#34;@${host}:\u0026#34;\\$TMP_COMPOSE\u0026#34; # 2) 서버 안에서 sudo로 최종 위치로 이동 + 권한 조정 + 컨테이너 재기동 sshpass -p \u0026#34;\\$DEPLOY_PASS\u0026#34; ssh -o StrictHostKeyChecking=no \u0026#34;\\$DEPLOY_USER\u0026#34;@${host} \u0026#34; echo \u0026#39;Apply .env \u0026amp; docker-compose.yaml...\u0026#39; # .env 교체 sudo mv \\$TMP_ENV ${DEPLOY_PATH}/.env sudo chown nginx:nginx ${DEPLOY_PATH}/.env # docker-compose.yaml 교체 sudo mv \\$TMP_COMPOSE ${DEPLOY_PATH}/docker-compose.yaml sudo chown root:root ${DEPLOY_PATH}/docker-compose.yaml echo \u0026#39;Logging in to GitLab Registry...\u0026#39; echo \u0026#39;\\$GITLAB_TOKEN\u0026#39; | sudo docker login ${GITLAB_REGISTRY} -u \u0026#39;\\$GITLAB_USER\u0026#39; --password-stdin echo \u0026#39;Pulling image: ${GITLAB_REGISTRY}/${GITLAB_PROJECT}/${IMAGE_NAME}:${params.IMAGE_TAG}\u0026#39; sudo docker pull ${GITLAB_REGISTRY}/${GITLAB_PROJECT}/${IMAGE_NAME}:${params.IMAGE_TAG} echo \u0026#39;Redeploying containers...\u0026#39; export IMAGE_TAG=${params.IMAGE_TAG} sudo docker compose -f ${DEPLOY_PATH}/docker-compose.yaml down sudo docker compose -f ${DEPLOY_PATH}/docker-compose.yaml up -d sudo docker logout ${GITLAB_REGISTRY} \u0026#34; \u0026#34;\u0026#34;\u0026#34; echo \u0026#34;Deployment to ${host} completed.\u0026#34; } } } } } 4. 헬스 체크 배포 완료 후 컨테이너 상태 점검 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 stage(\u0026#39;Health Check\u0026#39;) { steps { script { echo \u0026#34;Waiting for application to start...\u0026#34; sleep 10 def hosts = env.DEPLOY_HOSTS.split(\u0026#39;,\u0026#39;) withCredentials([ usernamePassword(credentialsId: \u0026#34;${DEPLOY_CREDENTIAL_ID}\u0026#34;, usernameVariable: \u0026#39;DEPLOY_USER\u0026#39;, passwordVariable: \u0026#39;DEPLOY_PASS\u0026#39;) ]) { for (host in hosts) { echo \u0026#34;Health checking ${host}...\u0026#34; sh \u0026#34;\u0026#34;\u0026#34; sshpass -p \u0026#34;\\$DEPLOY_PASS\u0026#34; ssh -o StrictHostKeyChecking=no \u0026#34;\\$DEPLOY_USER\u0026#34;@${host} \u0026#34; # 컨테이너 상태 확인 sudo docker ps | grep pj-1 || exit 1 \u0026#34; \u0026#34;\u0026#34;\u0026#34; echo \u0026#34;${host} health check passed.\u0026#34; } } } } } } 5. 파이프라인 결과 체크 1 2 3 4 5 6 7 8 9 post { success { echo \u0026#34;Deployment succeeded - ${IMAGE_NAME}:${params.IMAGE_TAG} deployed to ${env.DEPLOY_HOSTS}\u0026#34; } failure { echo \u0026#34;Deployment failed - check console output\u0026#34; } } } ","date":"2025-11-20T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-jenkins%EC%9D%98-php-cicd-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0/","title":"컨테이너 환경에서 Jenkins의 PHP CICD 파이프라인 구축하기"},{"content":" Java 1.8 애플리케이션에 대한 CI/CD 파이프라인 설정 방법\n환경 Git 리포지토리 및 컨테이너 레지스트리: GitLab CI/CD: Jenkins Java 버전: 1.8 빌드 툴: Maven 파이프라인을 컨테이너에서 동작시키기 위한 Jenkins Agent 도커 설정 파이프라인 설정 Git 리포지토리 연동 Definition: Pipeline script from SCM\n파이프라인 구성에 필요한 스크립트를 SCM에서 가져오도록 설정 SCM: Git\nRepository URL: Git 리포지토리 URL\nCredentials: Git 리포지토리에 접근 가능하도록 저장한 Credential\nBranch Specifier: */main\nCI 파이프라인 컨테이너 이미지를 빌드하기 위한 Dockerfile도 Git 리포지토리에 있어야 함 Java는 빌드 과정이 필요한 컴파일 언어이기 때문에 소스 빌드 → 이미지 빌드 → 이미지 푸시 과정이 필요 환경 agent any로 jenkins agent 컨테이너를 파이프라인으로 사용 1 2 3 4 5 6 7 8 9 10 11 12 13 14 pipeline { agent any environment { WAR_FILE = \u0026#39;\u0026lt;WAR파일명\u0026gt;\u0026#39; BUILD_PROFILE = \u0026#39;\u0026lt;개발/운영\u0026gt;\u0026#39; MAVEN_OPTS = \u0026#39;-Xmx1024m -XX:MaxPermSize=256m\u0026#39; // Docker \u0026amp; GitLab Registry settings DOCKER_IMAGE = \u0026#39;\u0026lt;이미지명\u0026gt;\u0026#39; GITLAB_REGISTRY = \u0026#39;\u0026lt;컨테이너 레지스트리 URL\u0026gt;\u0026#39; GITLAB_PROJECT = \u0026#39;\u0026lt;Git 리포지토리명\u0026gt;\u0026#39; GITLAB_CREDENTIAL_ID = \u0026#39;\u0026lt;저장한 Git PAT 토큰 ID\u0026gt;\u0026#39; } 소스코드 체크아웃 Git 리포지토리의 최신 커밋을 체크아웃 파이프라인 로그에 커밋 정보 출력 1 2 3 4 5 6 7 stages { stage(\u0026#39;Checkout\u0026#39;) { steps { checkout scm sh \u0026#39;git log -1 --oneline\u0026#39; } } Maven 빌드 Maven 컨테이너를 사용해서 소스 빌드 로컬 ~/.m2 디렉토리를 컨테이너하고 공유해서 의존성 캐시 재사용 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 stage(\u0026#39;Build\u0026#39;) { agent { docker { image \u0026#39;maven:3.9.11-eclipse-temurin-8\u0026#39; args \u0026#39;-v $HOME/.m2:/root/.m2\u0026#39; reuseNode true } } steps { script { def settingsParam = fileExists(\u0026#39;settings.xml\u0026#39;) ? \u0026#39;-s settings.xml\u0026#39; : \u0026#39;\u0026#39; sh \u0026#34;\u0026#34;\u0026#34; mvn clean package -P ${BUILD_PROFILE} -DskipTests=true ${settingsParam} \\ -Dmaven.compiler.source=1.8 \\ -Dmaven.compiler.target=1.8 \u0026#34;\u0026#34;\u0026#34; sh \u0026#34;\u0026#34;\u0026#34; test -f target/${WAR_FILE} || (echo \u0026#34;WAR file not found\u0026#34; \u0026amp;\u0026amp; exit 1) ls -lh target/${WAR_FILE} \u0026#34;\u0026#34;\u0026#34; } } } Artifact 보관 빌드 메타데이터(commit, branch, build_time)를 build.properties로 저장\n산출물을 Artifacts로 아카이빙\nJenkins 콘솔에서 바로 WAR 파일 다운로드 가능 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 stage(\u0026#39;Archive\u0026#39;) { steps { script { def gitCommit = sh(returnStdout: true, script: \u0026#39;git rev-parse --short HEAD\u0026#39;).trim() def gitBranch = sh(returnStdout: true, script: \u0026#39;git rev-parse --abbrev-ref HEAD\u0026#39;).trim() def buildTime = sh(returnStdout: true, script: \u0026#39;date \u0026#34;+%Y-%m-%d %H:%M:%S\u0026#34;\u0026#39;).trim() writeFile file: \u0026#39;build.properties\u0026#39;, text: \u0026#34;\u0026#34;\u0026#34;BUILD_NUMBER=${env.BUILD_NUMBER} BUILD_PROFILE=${BUILD_PROFILE} GIT_COMMIT=${gitCommit} GIT_BRANCH=${gitBranch} BUILD_TIME=${buildTime} \u0026#34;\u0026#34;\u0026#34; } archiveArtifacts artifacts: \u0026#34;target/${WAR_FILE}, build.properties\u0026#34;, fingerprint: true // WAR 파일을 workspace root로 복사 (Docker 빌드용) sh \u0026#34;cp target/${WAR_FILE} .\u0026#34; } } 컨테이너 이미지 빌드 및 레지스트리 푸시 Git 리포지토리에 있는 Dockerfile을 기반으로 이미지 빌드 Git 커밋 해시와 Jenkins 빌드 번호를 조합해서 태그 생성 GitLab PAT 토큰으로 로그인 후 푸시 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 stage(\u0026#39;Docker Build \u0026amp; Push\u0026#39;) { steps { script { def gitCommit = sh(returnStdout: true, script: \u0026#39;git rev-parse --short HEAD\u0026#39;).trim() def imageTag = \u0026#34;${env.BUILD_NUMBER}-${gitCommit}\u0026#34; def fullImageName = \u0026#34;${GITLAB_REGISTRY}/${GITLAB_PROJECT}/${DOCKER_IMAGE}\u0026#34; // Docker 이미지 빌드 sh \u0026#34;\u0026#34;\u0026#34; docker build -t ${fullImageName}:${imageTag} . docker tag ${fullImageName}:${imageTag} ${fullImageName}:latest \u0026#34;\u0026#34;\u0026#34; // GitLab Container Registry 로그인 및 푸시 withCredentials([usernamePassword(credentialsId: \u0026#34;${GITLAB_CREDENTIAL_ID}\u0026#34;, usernameVariable: \u0026#39;GITLAB_USER\u0026#39;, passwordVariable: \u0026#39;GITLAB_TOKEN\u0026#39;)]) { sh \u0026#34;\u0026#34;\u0026#34; echo \u0026#34;\\$GITLAB_TOKEN\u0026#34; | docker login ${GITLAB_REGISTRY} -u \u0026#34;\\$GITLAB_USER\u0026#34; --password-stdin docker push ${fullImageName}:${imageTag} docker push ${fullImageName}:latest docker logout ${GITLAB_REGISTRY} \u0026#34;\u0026#34;\u0026#34; } echo \u0026#34;Docker image pushed: ${fullImageName}:${imageTag}\u0026#34; echo \u0026#34;Docker image pushed: ${fullImageName}:latest\u0026#34; } } } } } CD 파이프라인 Jenkins 서버에서 배포할 서버에 SSH로 접근 후 docker compose 파일을 기동하여 컨테이너 레지스트리의 이미지를 가져오는 방식으로 배포 배포할 서버가 여러 개일 경우를 고려해서 구성 환경 agent any로 jenkins agent 컨테이너를 파이프라인으로 사용 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 pipeline { agent any parameters { string(name: \u0026#39;IMAGE_TAG\u0026#39;, defaultValue: \u0026#39;latest\u0026#39;, description: \u0026#39;Docker image tag to deploy\u0026#39;) choice(name: \u0026#39;DEPLOY_TARGET\u0026#39;, choices: [\u0026#39;WAS1\u0026#39;, \u0026#39;ALL\u0026#39;], description: \u0026#39;WAS1: \u0026lt;IP/HOSTNAME\u0026gt; | ALL: 전체 WAS 배포\u0026#39;) } environment { WAS1_HOST = \u0026#39;\u0026lt;WAS1 서버 IP\u0026gt;\u0026#39; // WAS2_HOST = \u0026#39;\u0026lt;WAS2 서버 IP\u0026gt;\u0026#39; // TODO: WAS2 추가 시 활성화 DEPLOY_PATH = \u0026#39;\u0026lt;배포 경로\u0026gt;\u0026#39; GITLAB_REGISTRY = \u0026#39;\u0026lt;컨테이너 레지스트리 URL\u0026gt;\u0026#39; GITLAB_PROJECT = \u0026#39;\u0026lt;Git 리포지토리명\u0026gt;\u0026#39; IMAGE_NAME = \u0026#39;\u0026lt;컨테이너 이미지 명\u0026gt;\u0026#39; DEPLOY_CREDENTIAL_ID = \u0026#39;\u0026lt;배포 서버 SSH 계정명\u0026gt;\u0026#39; GITLAB_CREDENTIAL_ID = \u0026#39;\u0026lt;저장한 Git PAT 토큰 ID\u0026gt;\u0026#39; } 준비 배포할 이미지 태그 / 배포 대상 서버를 선택할 수 있게 설정 deployHosts: 배포 대상 호스트 목록 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 stages { stage(\u0026#39;Prepare\u0026#39;) { steps { script { echo \u0026#34;Deploying image: ${GITLAB_REGISTRY}/${GITLAB_PROJECT}/${IMAGE_NAME}:${params.IMAGE_TAG}\u0026#34; echo \u0026#34;Deploy target: ${params.DEPLOY_TARGET}\u0026#34; def deployHosts = [] if (params.DEPLOY_TARGET == \u0026#39;WAS1\u0026#39;) { deployHosts = [WAS1_HOST] } else if (params.DEPLOY_TARGET == \u0026#39;ALL\u0026#39;) { deployHosts = [WAS1_HOST] // TODO: WAS2 추가 시 -\u0026gt; deployHosts = [WAS1_HOST, WAS2_HOST] } env.DEPLOY_HOSTS = deployHosts.join(\u0026#39;,\u0026#39;) echo \u0026#34;Target servers: ${env.DEPLOY_HOSTS}\u0026#34; } } } 배포 컨테이너 레지스트리에서 이미지 풀링 배포 경로에서 docker compose 명령어를 통한 애플리케이션 재기동 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 stage(\u0026#39;Deploy\u0026#39;) { steps { script { def hosts = env.DEPLOY_HOSTS.split(\u0026#39;,\u0026#39;) withCredentials([ usernamePassword(credentialsId: \u0026#34;${DEPLOY_CREDENTIAL_ID}\u0026#34;, usernameVariable: \u0026#39;DEPLOY_USER\u0026#39;, passwordVariable: \u0026#39;DEPLOY_PASS\u0026#39;), usernamePassword(credentialsId: \u0026#34;${GITLAB_CREDENTIAL_ID}\u0026#34;, usernameVariable: \u0026#39;GITLAB_USER\u0026#39;, passwordVariable: \u0026#39;GITLAB_TOKEN\u0026#39;) ]) { for (host in hosts) { echo \u0026#34;Deploying to ${host}...\u0026#34; sh \u0026#34;\u0026#34;\u0026#34; sshpass -p \u0026#34;\\$DEPLOY_PASS\u0026#34; ssh -o StrictHostKeyChecking=no \\$DEPLOY_USER@${host} \u0026#34; cd ${DEPLOY_PATH} # GitLab Registry 로그인 echo \u0026#39;Logging in to GitLab Registry...\u0026#39; echo \u0026#34;\\$GITLAB_TOKEN\u0026#34; | sudo docker login ${GITLAB_REGISTRY} -u \u0026#34;\\$GITLAB_USER\u0026#34; --password-stdin # 이미지 Pull echo \u0026#39;Pulling image: ${GITLAB_REGISTRY}/${GITLAB_PROJECT}/${IMAGE_NAME}:${params.IMAGE_TAG}\u0026#39; sudo docker pull ${GITLAB_REGISTRY}/${GITLAB_PROJECT}/${IMAGE_NAME}:${params.IMAGE_TAG} # 컨테이너 재배포 echo \u0026#39;Redeploying containers...\u0026#39; export IMAGE_TAG=${params.IMAGE_TAG} sudo docker compose down sudo docker compose up -d # 배포 확인 echo \u0026#39;Checking deployment status...\u0026#39; sudo docker compose ps # 로그아웃 sudo docker logout ${GITLAB_REGISTRY} \u0026#34; \u0026#34;\u0026#34;\u0026#34; echo \u0026#34;Deployment to ${host} completed.\u0026#34; } } } } } 헬스 체크 배포 완료 후 컨테이너 상태 점검 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 stage(\u0026#39;Health Check\u0026#39;) { steps { script { echo \u0026#34;Waiting for application to start...\u0026#34; sleep 10 def hosts = env.DEPLOY_HOSTS.split(\u0026#39;,\u0026#39;) withCredentials([usernamePassword(credentialsId: \u0026#34;${DEPLOY_CREDENTIAL_ID}\u0026#34;, usernameVariable: \u0026#39;DEPLOY_USER\u0026#39;, passwordVariable: \u0026#39;DEPLOY_PASS\u0026#39;)]) { for (host in hosts) { echo \u0026#34;Health checking ${host}...\u0026#34; sh \u0026#34;\u0026#34;\u0026#34; sshpass -p \u0026#34;\\$DEPLOY_PASS\u0026#34; ssh -o StrictHostKeyChecking=no \\$DEPLOY_USER@${host} \u0026#34; # 컨테이너 상태 확인 sudo docker ps | grep main # 헬스체크 (옵션) # curl -f http://localhost:8080/health || exit 1 \u0026#34; \u0026#34;\u0026#34;\u0026#34; echo \u0026#34;${host} health check passed.\u0026#34; } } } } } } } ","date":"2025-11-13T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-jenkins%EC%9D%98-java-cicd-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0/","title":"컨테이너 환경에서 Jenkins의 Java CICD 파이프라인 구축하기"},{"content":" 기존 Hugo Stack 테마에 계층적 카테고리 시스템을 커스텀\n개요 기존 카테고리 기존 카테고리는 정확하게 태그와 동일한 역할을 했었음 Hugo Taxonomy가 자동으로 /categories/linux, /categories/middleware 페이지 생성 각 카테고리를 독립적인 태그처럼 평면적으로면 취급함 각 포스트들에 [”Linux”, “Milldeware”]로 설정이 되어있더라도 Linux, Milldeware 두개의 카테고리가 포스팅에 부여되는 식으로만 동작 이전 사용하던 Jekyll Chirpy 테마처럼 계층적 카테고리를 구현하고 싶었음 커스텀 카테고리 메인 카테고리와 서브 카테고리의 2단계 계층 구조 자동 카테고리 페이지 생성 코드로 /categories/, /categories/메인/, /categories/메인/서브/ 3가지 페이지 생성 /categories 전체 카테고리 트리 페이지 모든 포스트 스캔 → 메인/서브 카테고리 추출 → 트리 구조로 표시 layouts/categories/list.html /categories/메인/ 메인 카테고리 아래의 서브 카테고리 목록 페이지 categories[0] == \u0026quot;메인\u0026quot;인 서브 카테고리들만 표시 category-primary.html /categories/메인/서브/ 메인+서브 카테고리 조건을 만족하는 포스트 목록 페이지 categories[0] == \u0026quot;메인\u0026quot; AND categories[1] == \u0026quot;서브”인 포스트만 표시 category-secondary.html 1. 카테고리 페이지 자동 생성 스크립트 scripts/generate-categories.go Hugo 빌드 전에 실행되어야 하는 전처리 스크립트 GitHub Actions에서 go run scripts/generate-categories.go 명령으로 실행 코드 구조 및 역할 1.1 기본 구조체 정의 각 포스트에서 추출한 카테고리 정보를 저장할 구조체\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;path/filepath\u0026#34; \u0026#34;regexp\u0026#34; \u0026#34;strings\u0026#34; ) type CategoryInfo struct { Primary string Secondary string } 1.2 main 함수 - 디렉토리 초기화 content/categories 디렉토리를 매번 새로 생성\n기존 것 삭제 후 재생성 포스트가 삭제되면 해당 카테고리 페이지도 자동으로 삭제되어야 하기 때문 1 2 3 4 5 6 func main() { contentDir := \u0026#34;content/post\u0026#34; categoriesDir := \u0026#34;content/categories\u0026#34; os.RemoveAll(categoriesDir) os.MkdirAll(categoriesDir, 0755) 1.3 포스트 스캔 및 카테고리 수집 content/post 디렉토리의 모든 마크다운 파일 스캔\n각 파일에서 categories: [\u0026quot;메인\u0026quot;, \u0026quot;서브\u0026quot;] 형태로 정의된 카테고리 추출\ncategoryMap 구조: map[메인]map[서브]bool\n예: {\u0026quot;Linux\u0026quot;: {\u0026quot;Middleware\u0026quot;: true, \u0026quot;Shell\u0026quot;: true}} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 categoryMap := make(map[string]map[string]bool) filepath.Walk(contentDir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() || !strings.HasSuffix(path, \u0026#34;.md\u0026#34;) { return nil } content, err := os.ReadFile(path) if err != nil { return nil } cats := extractCategories(string(content)) if len(cats) \u0026gt;= 2 { primary := cats[0] secondary := cats[1] if categoryMap[primary] == nil { categoryMap[primary] = make(map[string]bool) } categoryMap[primary][secondary] = true } return nil }) extractCategories 함수\n정규식으로 front matter에서 categories: [...] 부분 찾기\n쌍따옴표 제거하고 쉼표로 분리\n예: [\u0026quot;Linux\u0026quot;, \u0026quot;Middleware\u0026quot;] → []string{\u0026quot;Linux\u0026quot;, \u0026quot;Middleware\u0026quot;}\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func extractCategories(content string) []string { re := regexp.MustCompile(`categories:\\\\s*\\\\[([^\\\\]]+)\\\\]`) matches := re.FindStringSubmatch(content) if len(matches) \u0026lt; 2 { return nil } categoriesStr := matches[1] categoriesStr = strings.ReplaceAll(categoriesStr, `\u0026#34;`, \u0026#34;\u0026#34;) parts := strings.Split(categoriesStr, \u0026#34;,\u0026#34;) var result []string for _, part := range parts { trimmed := strings.TrimSpace(part) if trimmed != \u0026#34;\u0026#34; { result = append(result, trimmed) } } return result } 1.4 메인 카테고리 페이지 생성 메인 카테고리마다 content/page/categories/메인/_index.md 생성\nlayout: \u0026quot;category-primary\u0026quot; → Hugo가 layouts/_default/category-primary.html 템플릿 사용하도록 설정\nprimary_category 파라미터로 필터링에 사용할 카테고리 이름 전달\n1 2 3 4 5 6 7 8 9 10 11 12 for primary, secondaries := range categoryMap { primaryDir := filepath.Join(categoriesDir, strings.ToLower(primary)) os.MkdirAll(primaryDir, 0755) primaryIndexPath := filepath.Join(primaryDir, \u0026#34;_index.md\u0026#34;) primaryContent := fmt.Sprintf(`--- title: \u0026#34;%s\u0026#34; layout: \u0026#34;category-primary\u0026#34; primary_category: \u0026#34;%s\u0026#34; --- `, primary, primary) os.WriteFile(primaryIndexPath, []byte(primaryContent), 0644) 1.5 서브 카테고리 페이지 생성 서브 카테고리마다 content/page/categories/메인/서브.md 생성\nlayout: \u0026quot;category-secondary\u0026quot; → layouts/_default/category-secondary.html 사용하도록 설정\n두 개의 파라미터로 AND 조건 필터링 가능\n1 2 3 4 5 6 7 8 9 10 11 12 for secondary := range secondaries { secondaryPath := filepath.Join(primaryDir, strings.ToLower(secondary)+\u0026#34;.md\u0026#34;) secondaryContent := fmt.Sprintf(`--- title: \u0026#34;%s\u0026#34; layout: \u0026#34;category-secondary\u0026#34; primary_category: \u0026#34;%s\u0026#34; secondary_category: \u0026#34;%s\u0026#34; --- `, secondary, primary, secondary) os.WriteFile(secondaryPath, []byte(secondaryContent), 0644) } } 생성 결과 예시 1 2 3 4 5 6 7 8 content/page/categories/ ├── linux/ │ ├── _index.md (primary_category: \u0026#34;Linux\u0026#34;) │ ├── middleware.md (primary: \u0026#34;Linux\u0026#34;, secondary: \u0026#34;Middleware\u0026#34;) │ └── shell.md (primary: \u0026#34;Linux\u0026#34;, secondary: \u0026#34;Shell\u0026#34;) └── scm/ ├── _index.md (primary_category: \u0026#34;SCM\u0026#34;) └── kubernetes.md (primary: \u0026#34;SCM\u0026#34;, secondary: \u0026#34;Kubernetes\u0026#34;) 2. Categories 페이지 템플릿 layouts/categories/list.html Hugo의 템플릿 우선순위: layouts/categories/list.html \u0026gt; themes/.../layouts/categories/list.html /categories/ URL은 Hugo가 자동으로 categories taxonomy 페이지로 인식 이 위치에 파일을 두면 테마의 기본 템플릿을 override 템플릿 구조 및 역할 2.1 계층 구조 빌드 테마의 baseof.html에서 {{ block \u0026quot;main\u0026quot; . }}를 이 내용으로 대체\n모든 포스트를 순회하며 계층 구조 빌드\n$hierarchy 구조: {\u0026quot;Linux\u0026quot;: [\u0026quot;Middleware\u0026quot;, \u0026quot;Shell\u0026quot;], \u0026quot;SCM\u0026quot;: [\u0026quot;Kubernetes\u0026quot;]}\n.Site.RegularPages: Hugo의 모든 일반 페이지\n.Params.categories: 각 포스트의 front matter에서 categories 배열\nindex $cats 0: 배열의 첫 번째 요소 (메인 카테고리)\nmerge: Go template의 딕셔너리 병합 함수\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 {{ define \u0026#34;main\u0026#34; }} {{ $hierarchy := dict }} {{ range .Site.RegularPages }} {{ $cats := .Params.categories }} {{ if and $cats (ge (len $cats) 2) }} {{ $main := index $cats 0 }} {{ $sub := index $cats 1 }} {{ if not (index $hierarchy $main) }} {{ $hierarchy = merge $hierarchy (dict $main (slice)) }} {{ end }} {{ $subs := index $hierarchy $main }} {{ $subs = $subs | append $sub }} {{ $hierarchy = merge $hierarchy (dict $main $subs) }} {{ end }} {{ end }} 2.2 메인 카테고리별 포스트 수 계산 uniq: 중복 제거\n같은 서브 카테고리가 여러 포스트에 있을 수 있음 $totalPosts: 해당 메인 카테고리에 속한 전체 포스트 수 계산\n1 2 3 4 5 6 7 8 9 10 11 12 {{ range $mainCat, $subCats := $hierarchy }} {{ $subCats = uniq $subCats | sort }} {{ $totalPosts := 0 }} {{ range $.Site.RegularPages }} {{ $cats := .Params.categories }} {{ if and $cats (ge (len $cats) 1) }} {{ if eq (index $cats 0) $mainCat }} {{ $totalPosts = add $totalPosts 1 }} {{ end }} {{ end }} {{ end }} 2.3 토글 UI \u0026lt;details\u0026gt;: 토글\nopen 속성: 기본적으로 펼쳐진 상태\nurlize: URL 안전 문자열로 변환\n\u0026ldquo;Linux Kernel\u0026rdquo; → \u0026ldquo;linux-kernel\u0026rdquo; /categories/linux/ → scripts/generate-categories.go가 생성한 _index.md 페이지로 이동\n1 2 3 4 5 6 7 8 \u0026lt;details class=\u0026#34;category-group\u0026#34; open\u0026gt; \u0026lt;summary class=\u0026#34;main-cat\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;main-cat-title\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;icon\u0026#34;\u0026gt;📁\u0026lt;/span\u0026gt; \u0026lt;a href=\u0026#34;{{ printf \u0026#34;/categories/%s/\u0026#34; ($mainCat | urlize) }}\u0026#34;\u0026gt;{{ $mainCat }}\u0026lt;/a\u0026gt; \u0026lt;/span\u0026gt; \u0026lt;span class=\u0026#34;post-count-badge\u0026#34;\u0026gt;{{ $totalPosts }}\u0026lt;/span\u0026gt; \u0026lt;/summary\u0026gt; 2.4 서브 카테고리 목록 각 서브 카테고리의 포스트 개수 계산\nAND 조건: categories[0] == mainCat AND categories[1] == subCat\n/categories/linux/middleware/ → generate-categories.go가 생성한 middleware.md 페이지로 이동\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;div class=\u0026#34;sub-cats\u0026#34;\u0026gt; {{ range $subCat := $subCats }} {{ $count := 0 }} {{ range $.Site.RegularPages }} {{ $cats := .Params.categories }} {{ if and $cats (ge (len $cats) 2) }} {{ if and (eq (index $cats 0) $mainCat) (eq (index $cats 1) $subCat) }} {{ $count = add $count 1 }} {{ end }} {{ end }} {{ end }} \u0026lt;a href=\u0026#34;{{ printf \u0026#34;/categories/%s/%s/\u0026#34; ($mainCat | urlize) ($subCat | urlize) }}\u0026#34; class=\u0026#34;sub-cat-link\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;sub-cat-name\u0026#34;\u0026gt;{{ $subCat }}\u0026lt;/span\u0026gt; \u0026lt;span class=\u0026#34;count\u0026#34;\u0026gt;({{ $count }})\u0026lt;/span\u0026gt; \u0026lt;/a\u0026gt; {{ end }} \u0026lt;/div\u0026gt; \u0026lt;/details\u0026gt; {{ end }} 2.5 스타일링 인라인 CSS: 이 페이지에만 적용되는 스타일\nvar(--accent-color): 테마에서 정의한 CSS 변수 사용 (다크 모드 대응)\n1 2 3 4 5 6 7 8 9 \u0026lt;style\u0026gt; .page-header { margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 2px solid var(--accent-color); } /* ... 나머지 CSS ... */ \u0026lt;/style\u0026gt; {{ end }} 3. 메인 카테고리 페이지 템플릿 파일 위치: layouts/_default/category-primary.html generate-categories.go가 생성한 _index.md에서 layout: \u0026quot;category-primary\u0026quot; 지정 Hugo는 layouts/_default/ 디렉토리에서 커스텀 레이아웃을 찾음 파일명이 category-primary.html이므로 layout: \u0026quot;category-primary\u0026quot;와 매칭 템플릿 구조 및 역할 3.1 포스트 필터링 .Params.primary_category: generate-categories.go가 front matter에 추가한 파라미터\n필터링 조건: categories[0] == $primaryCat\n$posts: 필터링된 포스트 목록 (slice)\n1 2 3 4 5 6 7 8 9 10 11 {{ $primaryCat := .Params.primary_category }} {{ $posts := slice }} {{ range .Site.RegularPages }} {{ $cats := .Params.categories }} {{ if and $cats (ge (len $cats) 1) }} {{ if eq (index $cats 0) $primaryCat }} {{ $posts = $posts | append . }} {{ end }} {{ end }} {{ end }} 3.2 서브 카테고리 추출 필터링된 포스트들에서 서브 카테고리 목록 추출\nin: Go template의 배열 포함 여부 확인 함수\n알파벳 순으로 정렬\n1 2 3 4 5 6 7 8 9 10 11 {{ $subCategories := slice }} {{ range $posts }} {{ $cats := .Params.categories }} {{ if ge (len $cats) 2 }} {{ $sub := index $cats 1 }} {{ if not (in $subCategories $sub) }} {{ $subCategories = $subCategories | append $sub }} {{ end }} {{ end }} {{ end }} {{ $subCategories = $subCategories | uniq | sort }} 3.3 서브 카테고리별 그룹핑 서브 카테고리별로 그룹핑하여 표시\n각 그룹의 포스트 개수 배지로 표시\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 {{ range $subCat := $subCategories }} {{ $postCount := 0 }} {{ range $posts }} {{ $cats := .Params.categories }} {{ if and (ge (len $cats) 2) (eq (index $cats 1) $subCat) }} {{ $postCount = add $postCount 1 }} {{ end }} {{ end }} \u0026lt;details class=\u0026#34;sub-category-group\u0026#34; open\u0026gt; \u0026lt;summary class=\u0026#34;sub-category-title\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;title-text\u0026#34;\u0026gt;{{ $subCat }}\u0026lt;/span\u0026gt; \u0026lt;span class=\u0026#34;post-count-badge\u0026#34;\u0026gt;{{ $postCount }}\u0026lt;/span\u0026gt; \u0026lt;/summary\u0026gt; 3.4 포스트 목록 렌더링 각 서브 카테고리 내의 포스트 목록 표시\n.RelPermalink: 포스트의 상대 URL\nfirst 3: 태그가 많을 경우 처음 3개만 표시\n{{ with }}: 값이 존재할 때만 실행\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 \u0026lt;div class=\u0026#34;post-list\u0026#34;\u0026gt; {{ range $posts }} {{ $cats := .Params.categories }} {{ if and (ge (len $cats) 2) (eq (index $cats 1) $subCat) }} \u0026lt;article class=\u0026#34;post-item\u0026#34;\u0026gt; \u0026lt;a href=\u0026#34;{{ .RelPermalink }}\u0026#34; class=\u0026#34;post-link\u0026#34;\u0026gt; \u0026lt;h3 class=\u0026#34;post-title\u0026#34;\u0026gt;{{ .Title }}\u0026lt;/h3\u0026gt; \u0026lt;div class=\u0026#34;post-meta\u0026#34;\u0026gt; \u0026lt;time\u0026gt;{{ .Date.Format \u0026#34;2006-01-02\u0026#34; }}\u0026lt;/time\u0026gt; {{ with .Params.tags }} \u0026lt;span class=\u0026#34;tags\u0026#34;\u0026gt; {{ range first 3 . }} \u0026lt;span class=\u0026#34;tag\u0026#34;\u0026gt;{{ . }}\u0026lt;/span\u0026gt; {{ end }} \u0026lt;/span\u0026gt; {{ end }} \u0026lt;/div\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;/article\u0026gt; {{ end }} {{ end }} \u0026lt;/div\u0026gt; \u0026lt;/details\u0026gt; {{ end }} 4. 서브 카테고리 페이지 템플릿 layouts/_default/category-secondary.html generate-categories.go가 생성한 middleware.md에서 layout: \u0026quot;category-secondary\u0026quot; 지정 layouts/_default/category-secondary.html과 매칭 템플릿 구조 및 역할 4.1 Breadcrumb 네비게이션 Breadcrumb 네비게이션: Linux › Middleware\n메인 카테고리 클릭 시 메인 카테고리 페이지로 이동\n1 2 3 4 5 6 7 8 9 10 \u0026lt;header class=\u0026#34;page-header\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;breadcrumb\u0026#34;\u0026gt; \u0026lt;a href=\u0026#34;{{ printf \u0026#34;/categories/%s/\u0026#34; (.Params.primary_category | urlize) }}\u0026#34;\u0026gt; {{ .Params.primary_category }} \u0026lt;/a\u0026gt; \u0026lt;span class=\u0026#34;separator\u0026#34;\u0026gt;›\u0026lt;/span\u0026gt; \u0026lt;span class=\u0026#34;current\u0026#34;\u0026gt;{{ .Title }}\u0026lt;/span\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;h1\u0026gt;{{ .Title }}\u0026lt;/h1\u0026gt; \u0026lt;/header\u0026gt; 4.2 AND 조건 필터링 AND 조건 필터링: categories[0] == primary AND categories[1] == secondary\n두 파라미터 모두 generate-categories.go가 front matter에 추가\n1 2 3 4 5 6 7 8 9 10 11 12 {{ $primaryCat := .Params.primary_category }} {{ $secondaryCat := .Params.secondary_category }} {{ $posts := slice }} {{ range .Site.RegularPages }} {{ $cats := .Params.categories }} {{ if and $cats (ge (len $cats) 2) }} {{ if and (eq (index $cats 0) $primaryCat) (eq (index $cats 1) $secondaryCat) }} {{ $posts = $posts | append . }} {{ end }} {{ end }} {{ end }} 4.3 포스트 목록 필터링된 포스트 목록만 표시\n서브 카테고리 그룹핑 없이 플랫한 리스트\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 \u0026lt;div class=\u0026#34;post-count\u0026#34;\u0026gt; 총 {{ len $posts }}개의 포스트 \u0026lt;/div\u0026gt; \u0026lt;section class=\u0026#34;category-posts\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;post-list\u0026#34;\u0026gt; {{ range $posts }} \u0026lt;article class=\u0026#34;post-item\u0026#34;\u0026gt; \u0026lt;a href=\u0026#34;{{ .RelPermalink }}\u0026#34; class=\u0026#34;post-link\u0026#34;\u0026gt; \u0026lt;h3 class=\u0026#34;post-title\u0026#34;\u0026gt;{{ .Title }}\u0026lt;/h3\u0026gt; \u0026lt;div class=\u0026#34;post-meta\u0026#34;\u0026gt; \u0026lt;time\u0026gt;{{ .Date.Format \u0026#34;2006-01-02\u0026#34; }}\u0026lt;/time\u0026gt; {{ with .Params.tags }} \u0026lt;span class=\u0026#34;tags\u0026#34;\u0026gt; {{ range first 3 . }} \u0026lt;span class=\u0026#34;tag\u0026#34;\u0026gt;{{ . }}\u0026lt;/span\u0026gt; {{ end }} \u0026lt;/span\u0026gt; {{ end }} \u0026lt;/div\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;/article\u0026gt; {{ end }} \u0026lt;/div\u0026gt; \u0026lt;/section\u0026gt; 5. GitHub Actions 워크플로우 수정 .github/workflows/deploy.yml\n추가한 부분\ngenerate-categories.go 실행 → content/page/categories/ 디렉토리 생성 Hugo 빌드 → 생성된 카테고리 페이지들을 포함하여 정적 사이트 생성 1 2 3 4 5 - name: Generate category pages run: go run scripts/generate-categories.go - name: Build run: hugo --minify --gc 6. 메뉴 설정 config/_default/menu.toml\nidentifier: Hugo가 이 메뉴 항목을 식별하는 고유 ID name: 사용자에게 표시될 메뉴 이름 url: 클릭 시 이동할 경로 weight: 메뉴 정렬 순서 (낮을수록 위에 표시) icon: 테마의 assets/icons/categories.svg 아이콘 사용 1 2 3 4 5 6 7 [[main]] identifier = \u0026#34;categories\u0026#34; name = \u0026#34;Categories\u0026#34; url = \u0026#34;/categories/\u0026#34; weight = 2 [main.params] icon = \u0026#34;categories\u0026#34; Archives weight 조정\ncontent/page/archives/index.md 1 2 3 menu: main: weight: 3 # 기존 2에서 3으로 변경 구현 화면 메뉴\n/categories 페이지\n/categories/메인 페이지\n/categories/메인/서브 페이지\n","date":"2025-11-12T09:00:00+09:00","permalink":"https://joonlog.github.io/p/hugo-stack-%ED%85%8C%EB%A7%88%EC%97%90%EC%84%9C-%EA%B3%84%EC%B8%B5%ED%98%95-%EC%B9%B4%ED%85%8C%EA%B3%A0%EB%A6%AC-%EC%BB%A4%EC%8A%A4%ED%85%80%ED%95%98%EA%B8%B0/","title":"Hugo Stack 테마에서 계층형 카테고리 커스텀하기"},{"content":" Jekyll Chirpy 테마로 포스팅했던 글들을 Hugo Stack으로 이관\n자세한 Hugo Stack 구축 과정은 이 글 참고\nJekyll Chirpy에서 Hugo Stack으로 이관한 이유 Jekyll은 Liquid를 사용해서, 포스팅에 {%, {{ 같은 글자가 있으면 Liquid 템플릿 엔진이 이걸 Liquid 글로 인식을 해서 저 문자 자체로 사용하려면 {% raw %} 태그로 감싸야 했음 Jekyll은 Ruby 언어를 베이스로 짜여져 있고, 계층적 카테고리 구성과 같이 블로그를 커스터마이징할 일이 종종 있는데, 코드를 수정하기에는 Ruby에 대해 아는 바가 없음 최근에 Go를 배우기 시작한 상황에서 Go를 베이스로 한 Hugo로 블로그를 구성하면 좋겠다 싶었음 현재 Jekyll보다 Hugo가 더 점유율이 높고, 업데이트가 자주 있음 직접 구성해보니 구조 자체도 Hugo가 훨씬 간단 빌드부터 배포까지 Hugo가 더 빠름 기존 Chirpy 테마와 비슷하면서 사용자가 많은 테마인 Stack을 선택 블로그 이관 과정 기존 Jekyll 소스코드 별도 브랜치로 백업 Jekyll 브랜치 생성 후 백업 Hugo Stack Starter로 뼈대만 있는 블로그 배포 기존 블로그 글 이관 107개의 블로그 글 이관 전체 이관 전에 글 1개로 테스트 이관 시 포스팅 글 변경점 Front Matter 날짜 형식\n첫 공백을 T로 변경하고, timezone 앞 공백 제거 1 2 3 4 5 # Jekyll date: 2024-12-09 18:08:00 +09:00 # Hugo date: 2024-12-09T18:08:00+09:00 Categories/Tags 배열 따옴표 추가\n1 2 3 4 5 6 7 # Jekyll categories: [Linux, Middleware] tags: [rocky8, apache, tomcat] # Hugo categories: [\u0026#34;Linux\u0026#34;, \u0026#34;Middleware\u0026#34;] tags: [\u0026#34;rocky8\u0026#34;, \u0026#34;apache\u0026#34;, \u0026#34;tomcat\u0026#34;] 폴더 구조\n각 포스팅마다 \u0026lt;포스팅 이름\u0026gt;.md가 아닌, \u0026lt;포스팅 이름\u0026gt; 폴더를 만들고 그 아래 index.md가 있는 구조 1 2 3 4 5 6 7 8 # Jekyll _posts/2024-12-09-Rocky8ApacheTomcat.md # Hugo content/post/2024-12-09-rocky8apachetomcat/ ├── index.md ├── Rocky8ApacheTomcat1.png └── Rocky8ApacheTomcat2.png 이미지 경로 변경\nJekyll에서는 assests/img 경로에 모든 사진을 넣었지만, Hugo에서는 각 포스팅 폴더 안에 각각의 이미지를 넣음 1 2 3 4 5 # Jekyll /img/linux/Rocky8ApacheTomcat1.png # Hugo Rocky8ApacheTomcat1.png 이후 예정 작업 Hugo Theme에는 계층적 카테고리를 지원하는 테마가 없다 이 기능을 사용하기 위해선 별도 커스텀 필요 ","date":"2025-11-11T09:00:00+09:00","permalink":"https://joonlog.github.io/p/jekyll-chirpy-%ED%85%8C%EB%A7%88%EC%97%90%EC%84%9C-hugo-stack-%ED%85%8C%EB%A7%88%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9D%B4%EA%B4%80%ED%95%98%EA%B8%B0/","title":"Jekyll Chirpy 테마에서 Hugo Stack 테마로 블로그 이관하기"},{"content":" Redhat 계열 리눅스에서 Ansible로 시스템 로그 및 openfile, sar 설정 자동화\n환경 Rocky 8.10 Ansible 설정 01. CMD 로그 설정 cmdlog_rogrotate_openfile.yaml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 --- - name: Configure cmd logging, logrotate, and open files on Rocky 8.10 hosts: prod become: true handlers: - name: restart rsyslog ansible.builtin.systemd: name: rsyslog state: restarted enabled: true tasks: - name: 01. Ensure directories exist ansible.builtin.file: path: \u0026#34;{{ item }}\u0026#34; state: directory mode: \u0026#39;0755\u0026#39; loop: - /home/ncloud24/scripts - /var/log/backup - name: 02. Deploy logMoveDelete.sh ansible.builtin.copy: src: files/logMoveDelete.sh dest: /home/ncloud24/scripts/logMoveDelete.sh owner: root group: root mode: \u0026#39;0700\u0026#39; - name: 03. Register daily cron for logMoveDelete.sh (01:00) ansible.builtin.cron: name: \u0026#34;Move and cleanup .tar.gz logs\u0026#34; user: root minute: \u0026#34;0\u0026#34; hour: \u0026#34;1\u0026#34; job: \u0026#34;/home/ncloud24/scripts/logMoveDelete.sh\u0026#34; - name: 04. Deploy /etc/logrotate.conf (backup original) ansible.builtin.copy: src: files/logrotate.conf dest: /etc/logrotate.conf owner: root group: root mode: \u0026#39;0644\u0026#39; backup: true # 기존 /etc/logrotate.d/syslog 목록에 /var/log/cmd.log 추가 - name: 05. Ensure /var/log/cmd.log included in /etc/logrotate.d/syslog list ansible.builtin.lineinfile: path: /etc/logrotate.d/syslog regexp: \u0026#39;^/var/log/cmd\\.log$\u0026#39; line: \u0026#39;/var/log/cmd.log\u0026#39; insertafter: \u0026#39;^/var/log/secure\u0026#39; backup: true - name: 06. Deploy /etc/profile.d/cmd_logging.sh ansible.builtin.copy: src: files/cmd_logging.sh dest: /etc/profile.d/cmd_logging.sh owner: root group: root mode: \u0026#39;0700\u0026#39; - name: 07. Ensure rsyslog rule for local1.notice -\u0026gt; /var/log/cmd.log ansible.builtin.lineinfile: path: /etc/rsyslog.conf regexp: \u0026#39;^local1\\.notice\u0026#39; line: \u0026#39;local1.notice /var/log/cmd.log\u0026#39; insertafter: EOF backup: true notify: restart rsyslog # limits.conf 끝에 설정 (중복 방지) - name: 08. Append soft nofile to limits.conf ansible.builtin.lineinfile: path: /etc/security/limits.conf line: \u0026#39;* soft nofile 65535\u0026#39; regexp: \u0026#39;^\\* +soft +nofile +\\d+\u0026#39; insertafter: EOF create: true state: present - name: 09. Append hard nofile to limits.conf ansible.builtin.lineinfile: path: /etc/security/limits.conf line: \u0026#39;* hard nofile 65535\u0026#39; regexp: \u0026#39;^\\* +hard +nofile +\\d+\u0026#39; insertafter: EOF create: true state: present # user.conf 끝에 설정 (중복 방지) - name: 10. Ensure DefaultLimitNOFILE=65535 at end of /etc/systemd/user.conf ansible.builtin.lineinfile: path: /etc/systemd/user.conf regexp: \u0026#39;^#?DefaultLimitNOFILE=\u0026#39; line: \u0026#39;DefaultLimitNOFILE=65535\u0026#39; insertafter: EOF create: true state: present - name: 11. Reload systemd daemon ansible.builtin.systemd: daemon_reload: true task01: 로그/스크립트 디렉터리 생성 task02: logMoveDelete.sh 배포 및 권한 설정 task03: 로그 관리 스크립트 cron 등록 task04: /etc/logrotate.conf 정책 설정 task05: /etc/logrotate.d/syslog 목록 설정 task06: /etc/profile.d/cmd_logging.sh 배포 task07: rsyslog.conf 설정 추가 + rsyslog 재시작 task08/09: limits.conf에 nofile 65535 soft/hard 설정(중복 방지) task10: DefaultLimitNOFILE 설정 task11: systemd daemon-reload files/cmd_logging.sh\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #!/bin/bash # bash 대화형 세션에서 명령을 로깅 function logging { stat=\u0026#34;$?\u0026#34; cmd=$(history|tail -1) remoteaddr=$(who am i) if [ \u0026#34;$cmd\u0026#34; != \u0026#34;$cmd_old\u0026#34; ]; then logger -p local1.notice \u0026#34;[2] STAT=$stat\u0026#34; logger -p local1.notice \u0026#34;[1] $USER=$remoteaddr, PID=$$, PWD=$PWD, CMD=$cmd\u0026#34; fi cmd_old=$cmd } trap logging DEBUG files/logMoveDelete.sh\n1 2 3 4 5 6 7 8 9 10 11 #!/bin/bash PATH=/usr/sbin:/usr/bin:/sbin:/bin source_dir=\u0026#34;/var/log\u0026#34; backup_dir=\u0026#34;/var/log/backup\u0026#34; # .tar.gz 파일을 백업 디렉토리로 이동 mv \u0026#34;$source_dir\u0026#34;/*.tar.gz \u0026#34;$backup_dir\u0026#34; 2\u0026gt;/dev/null || true # 1년이 지난 파일 삭제 find \u0026#34;$backup_dir\u0026#34; -name \u0026#34;*.tar.gz\u0026#34; -type f -mtime +365 -exec rm {} \\; files/logrotate.conf\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 daily rotate 365 create dateext compress compresscmd /bin/gzip compressext .gz include /etc/logrotate.d /var/log/wtmp { monthly create 0664 root utmp minsize 1M rotate 1 } /var/log/btmp { missingok monthly create 0600 root utmp rotate 1 } 02. SAR 로그 설정 sysstat 활성화 및 관리 주기 설정 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 --- - name: Enable and configure sysstat (sar) hosts: prod become: true handlers: - name: restart sysstat ansible.builtin.systemd: name: sysstat state: restarted enabled: true tasks: - name: 01. Ensure sysstat package is installed ansible.builtin.package: name: sysstat state: present - name: 02. Set HISTORY=365 in /etc/sysconfig/sysstat ansible.builtin.lineinfile: path: /etc/sysconfig/sysstat regexp: \u0026#39;^HISTORY=\u0026#39; line: \u0026#39;HISTORY=365\u0026#39; insertafter: EOF backup: true notify: restart sysstat - name: 03. Set COMPRESSAFTER=90 in /etc/sysconfig/sysstat ansible.builtin.lineinfile: path: /etc/sysconfig/sysstat regexp: \u0026#39;^COMPRESSAFTER=\u0026#39; line: \u0026#39;COMPRESSAFTER=90\u0026#39; insertafter: EOF notify: restart sysstat - name: 04. Set ZIP=\u0026#34;gzip\u0026#34; in /etc/sysconfig/sysstat ansible.builtin.lineinfile: path: /etc/sysconfig/sysstat regexp: \u0026#39;^ZIP=\u0026#39; line: \u0026#39;ZIP=\u0026#34;gzip\u0026#34;\u0026#39; insertafter: EOF notify: restart sysstat - name: 05. Install /etc/cron.d/sysstat ansible.builtin.copy: dest: /etc/cron.d/sysstat owner: root group: root mode: \u0026#39;0644\u0026#39; content: | */1 * * * * root /usr/lib64/sa/sa1 1 1 53 23 * * * root /usr/lib64/sa/sa2 -A notify: restart sysstat - name: 06. Ensure sysstat service is enabled and started ansible.builtin.systemd: name: sysstat enabled: true state: started task01: sysstat 패키지 설치 확인 task02, 03, 04: /etc/sysconfig/sysstat 설정 task05: /etc/cron.d/sysstat 생성 task06: sysstat 활성화 ","date":"2025-11-05T09:00:00+09:00","permalink":"https://joonlog.github.io/p/ansible-playbook%EC%9C%BC%EB%A1%9C-cmd-%EB%A1%9C%EA%B7%B8-/-logrotate-/-openfile-/-sar-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0/","title":"Ansible Playbook으로 CMD 로그 / logrotate / openfile / SAR 설정하기"},{"content":" RedHat 계열 리눅스에서 Ansible로 도커 설치하는 방법\nAnsible 설정 install_docker.yaml\n10대 이상의 리눅스에 도커를 설치하는 playbook 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 - name: Install Docker CE on web/was hosts hosts: webwas become: true gather_facts: yes vars: conflict_pkgs: - docker.io - docker-doc - docker-compose - docker-compose-v2 - podman-docker - containerd - runc docker_pkgs: - docker-ce - docker-ce-cli - containerd.io - docker-buildx-plugin - docker-compose-plugin tasks: - name: 01. Remove conflicting packages (ignore if not present) package: name: \u0026#34;{{ conflict_pkgs }}\u0026#34; state: absent - name: 02. Install prerequisites package: name: yum-utils state: present when: ansible_os_family == \u0026#34;RedHat\u0026#34; - name: 03. Add Docker CE yum repository (official) yum_repository: name: docker-ce-stable description: Docker CE Stable - $basearch baseurl: \u0026#34;https://download.docker.com/linux/centos/$releasever/$basearch/stable\u0026#34; gpgcheck: yes gpgkey: https://download.docker.com/linux/centos/gpg enabled: yes when: ansible_os_family == \u0026#34;RedHat\u0026#34; - name: 04. Install Docker CE packages package: name: \u0026#34;{{ docker_pkgs }}\u0026#34; state: present - name: 05. Enable and start Docker service: name: docker state: started enabled: yes task01: 기존 충돌 패키지 제거 task02: 필수 패키지 설치 task03: Docker CE 패키지 리포지토리 추가 OS 버전에 맞는 리포지토리 자동 설정 task04: 도커 설치 task05: 도커 서비스 시작 및 자동 실행 ","date":"2025-11-04T09:00:00+09:00","permalink":"https://joonlog.github.io/p/ansible-playbook%EC%9C%BC%EB%A1%9C-docker-%EC%84%A4%EC%B9%98/","title":"Ansible Playbook으로 Docker 설치"},{"content":" GitHub Pages로 Hugo 템플릿의 Stack Theme를 사용해서 깃허브 블로그를 구축\n별도의 컨텐츠 설정 없이 배포까지 정상적인지 확인\nHugo Stack 구조 config Hugo Stack 설정 폴더 사이트 url, 언어, 페이지네이션, footer/sidebar 파라미터, 메뉴, 모듈 등을 설정 content 포스팅 글, 아카이브, 검색, 링크, 카테고리 등의 페이지가 존재 static 정적 파일들이 있는 폴더 assets 빌드에 포함할 커스텀 css, 이미지를 넣는 폴더 .github GitHub Action 배포에 사용할 deploy.yml이 있는 폴더 go.mod go 모듈 Hugo Stack 구축 깃허브 소스 클론\n1 git clone https://github.com/CaiJimmy/hugo-theme-stack-starter.git config 수정\nconfig/_default/config.toml\n1 2 3 4 5 baseurl = \u0026#34;https://joonlog.github.io\u0026#34; languageCode = \u0026#34;ko-kr\u0026#34; title = \u0026#34;Joonlog\u0026#34; defaultContentLanguage = \u0026#34;ko\u0026#34; hasCJKLanguage = true footer, sidebar 수정\nconfig/_default/params.toml\n1 2 3 since = 2024 emoji = \u0026#34;🐕\u0026#34; subtitle = \u0026#34;인프라/개발 기술 블로그\u0026#34; 배포 설정 배포 설정 수정\nmaster 브랜치에 푸시될때 자동 배포하는 설정을 main 브랜치로 수정\n.github/workflows/deploy.yml\n1 2 3 4 5 on: push: branches: [main] pull_request: branches: [main] 배포 방식 변경\nstarter 리포에 있는 기존 deploy.yml은 gh-pages 방식 배포 과정: main 브랜치로 코드 푸시 빌드 빌드한 결과물을 gh-pages 브랜치에 푸시 GitHub Pages가 gh-pages 브랜치에서 블로그 서빙 2개의 브랜치에 거렻서 배포되기 때문에 느림 GitHub Actions Artifact 방식으로 변경 배포 과정 main 브랜치로 코드 푸시 빌드 빌드한 결과물을 artifact에 업로드 GitHub Pages가 artifact에서 직접 서빙 GitHub Pages를 사용할 때의 최신 배포 방식 gh-pages 브랜치 관리 불필요 빠름 .github/workflows/deploy.yml gh-pages 브랜치에 푸시할 필요가 없어져서 contentes 권한을 write로 유지할 필요가 없어짐\nGitHub Pages에 직접 배포하기 위한 pages:write 권한\nOIDC 보안을 위한 id-token:write\n1 2 3 4 permissions: contents: read pages: write id-token: write 기존 코드는 gh-pages 브랜치에 푸시하는 코드\n대신 artifact에 푸시 + 푸시한 artifact를 GitHub Pages로 배포\n1 2 3 4 5 6 7 8 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: ./public - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 GitHub Pages 설정 Repository 생성 GitHub에서 \u0026lt;username\u0026gt;.github.io Repository 생성 깃허브 블로그로 사용하기 위해선 반드시 위 이름으로 Repository 생성 필요 GitHub Pages 활성화 Settings - Pages - Build and deployment GitHub Actions 선택 배포 확인 소스 푸시\n1 2 3 git add . git commit -m \u0026#34;First Push\u0026#34; git push 배포 성공!\n참고 Hugo Stack GitHub: https://github.com/CaiJimmy/hugo-theme-stack Hugo Stack Starter GitHub: https://github.com/CaiJimmy/hugo-theme-stack-starter Hugo Theme: https://themes.gohugo.io/ ","date":"2025-11-04T09:00:00+09:00","permalink":"https://joonlog.github.io/p/hugo-%ED%85%9C%ED%94%8C%EB%A6%BF-stack-%ED%85%8C%EB%A7%88%EB%A1%9C-github-blog-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0/","title":"Hugo 템플릿 Stack 테마로 GitHub Blog 배포하기"},{"content":" Redhat 계열 리눅스에서 시스템 로그 및 openfile, sar 등 기본 설정하는 방법\n환경 OS: Rocky 8.10 개요 명령어 실행 기록은 history로도 볼수있지만 각 계정마다 저장 위치가 다르고 없어질 수도 있음 명령어 추적을 위해 CMD 로그를 활성화 로그 파일 용량 문제를 방지하기 위해 logrotate 설정 운영 중 파일이 동시에 열릴 수 있는 파일 디스크럽터 값 증설 open file limit 설정 01. CMD 로그 설정 /etc/profile.d/cmd_logging.sh\ncmd 로그 생성 스크립트 1 2 3 4 5 6 7 8 9 10 11 12 function logging { stat=\u0026#34;$?\u0026#34; cmd=$(history|tail -1) remoteaddr=$(who am i) if [ \u0026#34;$cmd\u0026#34; != \u0026#34;$cmd_old\u0026#34; ]; then logger -p local1.notice \u0026#34;[2] STAT=$stat\u0026#34; logger -p local1.notice \u0026#34;[1] $USER=$remoteaddr, PID=$$, PWD=$PWD, CMD=$cmd\u0026#34; fi cmd_old=$cmd } trap logging DEBUG 권한 설정\n1 2 chmod 700 /etc/profile.d/cmd_logging.sh source /etc/profile.d/cmd_logging.sh rsyslog 설정 변경\n1 cp -aurp /etc/rsyslog.conf /home/ncloud24/rsyslog.conf.$(date +%Y%m%d) /etc/rsyslog.conf\n파일 하단에 추가 1 local1.notice /var/log/cmd.log rsyslog 재기동\n1 systemctl restart rsyslog 02. Logrotate 설정 1 cp /etc/logrotate.conf /etc/logrotate.conf.bak vim /etc/logrotate.conf\n1 2 3 4 5 6 7 8 daily # 매일 회전 rotate 365 # 365일(1년) 보관 create # 회전 후 새 파일 생성 dateext # 파일명에 날짜 확장자 사용 compress # 압축 활성화 compresscmd /bin/gzip compressext .gz include /etc/logrotate.d 로그 회전 대상에 cmd.log 추가\nvim /etc/logrotate.d/syslog\n1 2 3 4 5 6 7 8 9 10 11 12 13 /var/log/cron /var/log/maillog /var/log/messages /var/log/secure /var/log/spooler /var/log/cmd.log { missingok sharedscripts postrotate /usr/bin/systemctl -s HUP kill rsyslog.service \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 || true endscript } 03. 로그 관리 설정 로그 백업 디렉토리 추가\n1 mkdir -p /var/log/backup /home/scripts/logMoveDelete.sh\n로그 관리 스크립트 1 2 3 4 5 6 7 8 #!/bin/bash PATH=/usr/sbin:/usr/bin:/sbin:/bin source_dir=\u0026#34;/var/log\u0026#34; backup_dir=\u0026#34;/var/log/backup\u0026#34; mv \u0026#34;$source_dir\u0026#34;/*.tar.gz \u0026#34;$backup_dir\u0026#34; 2\u0026gt;/dev/null || true find \u0026#34;$backup_dir\u0026#34; -name \u0026#34;*.tar.gz\u0026#34; -type f -mtime +365 -exec rm {} \\; 권한 설정 1 chmod 700 /home/scripts/logMoveDelete.sh cron 등록\n1 2 3 crontab -e 0 1 * * * /home/scripts/logMoveDelete.sh 04. open file limit 설정 로그 및 서비스에서 파일 디스크립터 제한이 발생하지 않도록 상한값을 조정\n1 2 echo \u0026#39;* soft nofile 65535\u0026#39; | sudo tee -a /etc/security/limits.conf echo \u0026#39;* hard nofile 65535\u0026#39; | sudo tee -a /etc/security/limits.conf /etc/systemd/user.conf\n1 DefaultLimitNOFILE=65535 05. sar log 설정 sysstat 설정 변경\n/etc/sysconfig/sysstat\n1 2 3 HISTORY=365 # 데이터 보존 기간 (일 단위) COMPRESSAFTER=90 # 90일 경과 시 gzip 압축 ZIP=\u0026#34;gzip\u0026#34; # 압축 방식 /etc/cron.d/sysstat\n1 2 */1 * * * * root /usr/lib64/sa/sa1 1 1 # 1분마다 sa1 실행 53 23 * * * root /usr/lib64/sa/sa2 -A # 매일 23:53에 sa2 실행 (일간 요약) sysstat 서비스 활성화\n1 systemctl enable --now sysstat ","date":"2025-10-30T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EB%A6%AC%EB%88%85%EC%8A%A4-cmd-%EB%A1%9C%EA%B7%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%A1%9C%EA%B7%B8-logrotate-openfile-sar-%EB%A1%9C%EA%B7%B8-%EC%84%A4%EC%A0%95/","title":"리눅스 CMD 로그, 시스템 로그 logrotate, openfile, sar 로그 설정"},{"content":"문제 재발견 이전에 Jekyll Chirpy 테마 카테고리 중복/누락 TroubleShooting 글에서 _layouts/categories.html Liquid 템플릿을 수정해서 카테고리 문제를 해결했다고 생각지만, 계속해서 버그가 확인됨 카테고리 목록 페이지(/categories/)는 제대로 표시됨 하지만 개별 카테고리 페이지(/categories/kubernetes/)에 들어가면 메인 카테고리와 서브 카테고리가 전부 섞여서 표시됨 예시: Certification \u0026gt; Kubernetes (서브카테고리): 1개 포스트 Kubernetes \u0026gt; Architecture (메인카테고리): 34개 포스트 /categories/kubernetes/ 접속 시: 35개 포스트 전부 표시 근본 원인 이전 해결책은 _layouts/categories.html (목록 페이지)만 고친 것 진짜 문제는 Jekyll Archives 플러그인 Jekyll Archives 플러그인의 동작 방식\ncategories: [Certification, Kubernetes] 형태의 포스트가 있으면 site.categories['Certification']에도 포함되고 site.categories['Kubernetes']에도 포함됨 /categories/kubernetes/ 와 같은 1단계 경로로만 페이지를 생성하고 /categories/certification/kubernetes/ 같이 2단계 이상으로는 페이지를 생성하지 않음 _layouts/category.html의 한계\n1 {% raw %}{% for post in page.posts %}{% endraw %} page.posts는 Jekyll Archives가 이미 생성한 데이터 Liquid 템플릿에서는 이미 섞인 데이터를 받음 페이지 생성 자체는 Ruby 플러그인이 하기 때문에 Liquid로는 근본 해결 불가 해결 방법 커스텀 Ruby 플러그인을 작성해서 Jekyll Archives를 대체 1. Jekyll Archives의 categories 비활성화 _config.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 변경 전 jekyll-archives: enabled: [categories, tags] layouts: category: category tag: tag permalinks: tag: /tags/:name/ category: /categories/:name/ # 변경 후 jekyll-archives: enabled: [tags] layouts: tag: tag permalinks: tag: /tags/:name/ 2. 카테고리 플러그인 작성 _plugins/hierarchical-categories.rb\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 # frozen_string_literal: true module Jekyll # Generates hierarchical category pages # Primary categories: /categories/primary/ # Secondary categories: /categories/primary/secondary/ class HierarchicalCategoryPage \u0026lt; Page def initialize(site, base, primary, secondary = nil) @site = site @base = base @dir = if secondary File.join(\u0026#39;categories\u0026#39;, primary.downcase.gsub(\u0026#39; \u0026#39;, \u0026#39;-\u0026#39;)) else \u0026#39;categories\u0026#39; end @name = if secondary \u0026#34;#{secondary.downcase.gsub(\u0026#39; \u0026#39;, \u0026#39;-\u0026#39;)}.html\u0026#34; else \u0026#34;#{primary.downcase.gsub(\u0026#39; \u0026#39;, \u0026#39;-\u0026#39;)}.html\u0026#34; end self.process(@name) self.read_yaml(File.join(base, \u0026#39;_layouts\u0026#39;), \u0026#39;category.html\u0026#39;) # Set category name for display self.data[\u0026#39;title\u0026#39;] = secondary || primary self.data[\u0026#39;category_name\u0026#39;] = secondary || primary self.data[\u0026#39;primary_category\u0026#39;] = primary self.data[\u0026#39;secondary_category\u0026#39;] = secondary # Filter posts based on category hierarchy if secondary # Secondary category: posts where categories[0] == primary AND categories[1] == secondary self.data[\u0026#39;posts\u0026#39;] = site.posts.docs.select do |post| post.data[\u0026#39;categories\u0026#39;] \u0026amp;\u0026amp; post.data[\u0026#39;categories\u0026#39;][0] == primary \u0026amp;\u0026amp; post.data[\u0026#39;categories\u0026#39;][1] == secondary end else # Primary category: posts where categories[0] == primary self.data[\u0026#39;posts\u0026#39;] = site.posts.docs.select do |post| post.data[\u0026#39;categories\u0026#39;] \u0026amp;\u0026amp; post.data[\u0026#39;categories\u0026#39;][0] == primary end end end end class HierarchicalCategoryPageGenerator \u0026lt; Generator safe true priority :low def generate(site) return unless site.layouts.key? \u0026#39;category\u0026#39; # Collect all primary and secondary categories primary_categories = {} site.posts.docs.each do |post| next unless post.data[\u0026#39;categories\u0026#39;] categories = post.data[\u0026#39;categories\u0026#39;] primary = categories[0] secondary = categories[1] if categories.length \u0026gt; 1 next unless primary primary_categories[primary] ||= Set.new primary_categories[primary] \u0026lt;\u0026lt; secondary if secondary end # Generate pages for each primary category primary_categories.each do |primary, secondaries| # Generate primary category page site.pages \u0026lt;\u0026lt; HierarchicalCategoryPage.new(site, site.source, primary) # Generate secondary category pages secondaries.each do |secondary| next if secondary.nil? site.pages \u0026lt;\u0026lt; HierarchicalCategoryPage.new(site, site.source, primary, secondary) end end end end end 핵심 로직:\n메인 카테고리: post.categories[0]만 매칭 서브 카테고리: post.categories[0] == primary AND post.categories[1] == secondary 동시 매칭 URL 구조: 메인 카테고리: /categories/kubernetes/ 서브 카테고리: /categories/certification/kubernetes/ 3. 카테고리 목록 페이지 링크 수정 _layouts/categories.html\n서브카테고리 URL을 /categories/sub/에서 /categories/primary/sub/로 수정 URL 마지막 / 제거 (Jekyll이 .html 파일을 디렉토리로 인식하는 문제 방지) 56줄 메인 카테고리 링크\n1 2 3 4 5 # 변경 전 {% raw %}{% capture _category_url %}/categories/{{ category_name | slugify | url_encode }}/{% endcapture %}{% endraw %} # 변경 후 {% raw %}{% capture _category_url %}/categories/{{ category_name | slugify | url_encode }}{% endcapture %}{% endraw %} 121줄 서브 카테고리 링크\n1 2 3 4 5 # 변경 전 {% raw %}{% capture _sub_ctg_url %}/categories/{{ sub_category | slugify | url_encode }}/{% endcapture %}{% endraw %} # 변경 후 {% raw %}{% capture _sub_ctg_url %}/categories/{{ category_name | slugify | url_encode }}/{{ sub_category | slugify | url_encode }}{% endcapture %}{% endraw %} 결과 메인 카테고리 페이지: /categories/kubernetes/ 메인이 카테고리가 kubernetes인 모든 포스트 표시 서브 카테고리 페이지: /categories/certification/kubernetes/ 메인 카테고리가 Certification, 서브 카테고리가 Kubernetes인 포스트만 표시 카테고리 목록 페이지에서 클릭 시 올바른 URL로 이동 카테고리 버그 해결! ","date":"2025-10-29T18:00:00+09:00","permalink":"https://joonlog.github.io/p/jekyll-chirpy-%ED%85%8C%EB%A7%88-%EC%B9%B4%ED%85%8C%EA%B3%A0%EB%A6%AC-%EC%97%90%EB%9F%AC-troubleshooting2/","title":"Jekyll Chirpy 테마 카테고리 에러 TroubleShooting(2)"},{"content":" Jenkins Agent가 도커로 사용되도록 설정 Jenkins Server-Agent 분리\nBuilt-In-Node excutor 설정\nJenkins Agent를 도커에서 동적으로 실행할거라 built-in-node의 Number of executors값을 0으로 설정\nAgent Node 추가\nManage Jenkins - Nodes - System Configuration - New Node\nNode 설정\nNumber of excutors: Jenkins Agent가 최대로 실행 가능한 파이프라인 수 Remote root directory: Jenkins Agent 컨테이너 내부의 루트 디렉토리 Agent 설치 스크립트 확인\n이후에 컨테이너 기동에 필요한 secret 값이 포함되어 있으니 확인 필수 Dockerfile\n파이프라인용 에이전트이므로 최소로 컨테이너 기동 1 2 3 4 5 6 FROM jenkins/inbound-agent:latest-jdk21 USER root RUN apt-get update \\ \u0026amp;\u0026amp; apt-get install -y docker.io ca-certificates openssh-client \\ \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* USER jenkins docker-compose.yaml\n변수들은 .env 파일을 통해 별도로 설정 JENKINS_URL: 도커 네트워크 인터페이스 docker0의 ip인 172.17.0.1를 기입 JENKINS_SECRET: 위 콘솔에 나온 secret 값을 기입 JENKINS_AGENT_NAME: 위에서 설정한 이름 JENKINS_AGENT_WORKDIR: 위에서 설정한 Jenkins Agent가 컨테이너 내부에서 사용할 경로 AGENT_WORKDIR: Jenkins Agent 컨테이너가 호스트에서 사용하는 실제 경로 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 version: \u0026#34;3.8\u0026#34; services: agent: build: context: . dockerfile: Dockerfile container_name: jenkins-agent-docker restart: unless-stopped init: true environment: JENKINS_URL: \u0026#34;${JENKINS_URL}\u0026#34; JENKINS_SECRET: \u0026#34;${JENKINS_SECRET}\u0026#34; JENKINS_AGENT_NAME: \u0026#34;${JENKINS_AGENT_NAME}\u0026#34; JENKINS_AGENT_WORKDIR: \u0026#34;${JENKINS_AGENT_WORKDIR:-/home/jenkins/agent}\u0026#34; DOCKER_BUILDKIT: \u0026#34;1\u0026#34; JENKINS_WEB_SOCKET: \u0026#34;true\u0026#34; volumes: - /var/run/docker.sock:/var/run/docker.sock - ${AGENT_WORKDIR:-/var/lib/jenkins_agent}:${JENKINS_AGENT_WORKDIR:-/home/jenkins/agent} group_add: - \u0026#34;${DOCKER_GID}\u0026#34; 컨테이너 권한 설정\n호스트의 도커 소켓을 사용하는 DooD 방식이기 때문에 컨테이너에서도 호스트의 도커 GID 동기화 필요 /var/lib/jenkins_agent: 호스트에 컨테이너 데이터가 저장될 경로. 컨테이너 내부에서도 기록해야하니 기본 사용자인 1000으로 설정 1 2 echo \u0026#34;DOCKER_GID=$(stat -c %g /var/run/docker.sock)\u0026#34; \u0026gt;\u0026gt; .env mkdir -p /var/lib/jenkins_agent \u0026amp;\u0026amp; chown 1000:1000 /var/lib/jenkins_agent Jenkins Agent 빌드\n1 docker compose up -d --build Node 연결 확인\n파이프라인 테스트\nhello world 테스트 성공! 참고 Jenkins Agent 노드 추가: https://github.com/jenkinsci/docker-agent/blob/master/README_inbound-agent.md ","date":"2025-10-27T09:00:00+09:00","permalink":"https://joonlog.github.io/p/jenkins-agent%EB%A5%BC-docker%EB%A1%9C-%EB%8F%99%EC%9E%91%EC%8B%9C%ED%82%A4%EA%B8%B0-%EC%9C%84%ED%95%9C-%EB%B0%A9%EB%B2%95/","title":"Jenkins Agent를 Docker로 동작시키기 위한 방법"},{"content":" 로컬 리눅스 환경에서 GitLab Omnibus를 설치하는 방법\n컨테이너 이미지를 저장하기 위한 이미지 레지스트리 사용\n환경 OS: Rocky 8.10 설치 패키지 설치\n1 dnf install -y ca-certificates tzdata policycoreutils-python-utils GitLab Omnibus 설치\nComunity Edition 1 2 3 curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | sudo bash EXTERNAL_URL=\u0026#34;http://\u0026lt;IP/도메인\u0026gt;\u0026#34; dnf install -y gitlab-ce GitLab 설정\n/etc/gitlab/gitlab.rb\n콘솔 접근 경로\n1 2 # 기본 외부 URL 설정 external_url \u0026#34;http://\u0026lt;IP/도메인\u0026gt;\u0026#34; 이미지 레지스트리\n1 2 gitlab_rails[\u0026#39;registry_enabled\u0026#39;] = true registry_external_url \u0026#34;http://\u0026lt;IP/도메인\u0026gt;:5050\u0026#34; 레지스트리 경로가 https가 아닌 http이므로, 이미지를 푸시할 때 해당 서버에서 docker insecure 설정이 필요함\n1 2 3 4 5 6 7 vim /etc/docker/daemon.json --------------------------- { \u0026#34;insecure-registries\u0026#34;: [ \u0026#34;\u0026lt;ip/도메인\u0026gt;:5050\u0026#34; ] } (선택) 리포지토리 저장 위치 변경\n1 2 3 4 5 6 7 8 gitaly[\u0026#39;configuration\u0026#39;] = { storage: [ { name: \u0026#39;default\u0026#39;, path: \u0026#39;/data/gitlab/repositories\u0026#39;, }, ], } (선택) 이미지 레지스트리 저장 위치 변경\n1 gitlab_rails[\u0026#39;registry_path\u0026#39;] = \u0026#34;/data/gitlab/registry\u0026#34; 설정 적용\n1 gitlab-ctl reconfigure 루트 비밀번호 확인\n루트 비밀번호가 저장된 파일은 24시간 뒤에 자동 삭제 1 cat /etc/gitlab/initial_root_password http://로 콘솔 접근\n정상 접근 확인! 참고 GitLab Omnibus Install 공식 문서: https://docs.gitlab.com/install/package/almalinux/ ","date":"2025-10-25T09:00:00+09:00","permalink":"https://joonlog.github.io/p/linux-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-gitlab-omnibus-%EC%84%A4%EC%B9%98/","title":"Linux 환경에서 GitLab Omnibus 설치"},{"content":" 로컬 리눅스 환경에서 Jenkins 설치하는 방법\n환경 OS: Rocky 8.10 java: OpenJDK 21 설치 패키지 설치\n1 2 3 4 wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io-2023.key yum install fontconfig java-21-openjdk (선택) OpenJDK 21이 아닌 다른 버전으로 선택되어 있을 시 변경 필요 1 2 3 4 java --version # java 21 선택 alternatives --config java java --version jenkins 설치\n1 2 3 yum install jenkins systemctl daemon-reload systemctl enable --now jenkins http://:8080으로 콘솔 접근\nInstall wizard 진행\njenkins는 최초 접근 시 unlock 되어 있음\n비밀번호 확인 필요 1 cat /var/lib/jenkins/secrets/initialAdminPassword 이후 자동 설치 진행\n참고 Jenkins Install 공식 문서: https://www.jenkins.io/doc/book/installing/linux/ ","date":"2025-10-23T09:00:00+09:00","permalink":"https://joonlog.github.io/p/linux-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-jenkins-%EC%84%A4%EC%B9%98/","title":"Linux 환경에서 Jenkins 설치"},{"content":" 공식 rogueserver 코드의 쿠키 설정 버그로 인해 구글 OAuth 로그인이 작동하지 않는 이슈\n환경 Vite 기반 React 프론트엔드와 Go 기반 백엔드 그리고 Mariadb로 구성 K8S + HAProxy를 통한 외부 접근 문제 상황 일반 계정 로그인은 정상 작동 구글/디스코드 계정 연결 시도 시 에러: 일반 로그인 후 설정 메뉴에서 구글 연결 클릭 구글 OAuth 인증 완료 메인 화면으로 리다이렉트되지만 설정이 저장되지 않고 새로고침만 됨 로그아웃 후 구글로 직접 로그인 시도 시 로그인 실패 에러 분석 1. 백엔드 로그 분석 1 kubectl logs -n pokerogue pokerogue-server 1 2 3 2025/10/14 23:02:24 [DEBUG] Google token response status: 200 2025/10/14 23:02:24 [DEBUG] Successfully extracted userId: 102118711772610730702 2025/10/14 23:02:47 /account/info: missing token 구글 OAuth 토큰 교환은 성공 (userId 추출 완료) DB에 googleId도 정상 저장됨 확인 하지만 세션 토큰이 누락되어 로그인 상태 유지 실패 2. 브라우저 네트워크 탭 분석 1 2 3 4 Request: GET /api/auth/google/callback Response: 303 See Other Set-Cookie: pokerogue_sessionId=...; SameSite=Strict; Domain=pokerogue.net Sec-Fetch-Site: cross-site 문제점 발견:\nDomain=pokerogue.net 공식 pokerogue URL로 하드코딩 되어 있어 현재 도메인과 불일치 SameSite=Strict 구글에서 돌아오는 요청이 cross-site이므로 쿠키가 전송되지 않음 OAuth 리다이렉트 흐름: pokerogue.\u0026lt;도메인\u0026gt; → accounts.google.com → pokerogue.\u0026lt;도메인\u0026gt;/callback SameSite=Strict는 같은 사이트 내부 요청에만 쿠키 전송 구글에서 돌아오는 것은 cross-site 요청으로 간주되어 쿠키 차단 해결 방법 api/endpoints.go 파일의 쿠키 설정 수정: 1 2 3 4 5 6 7 8 9 http.SetCookie(w, \u0026amp;http.Cookie{ Name: \u0026#34;pokerogue_sessionId\u0026#34;, Value: sessionToken, Path: \u0026#34;/\u0026#34;, Secure: true, SameSite: http.SameSiteLaxMode, // Strict → Lax 변경 Domain: \u0026#34;\u0026#34;, // \u0026#34;pokerogue.net\u0026#34; → \u0026#34;\u0026#34; 변경 Expires: time.Now().Add(time.Hour * 24 * 30 * 3), }) 변경 사항 Domain: \u0026quot;\u0026quot; 빈 문자열로 설정하여 현재 도메인 자동 적용 SameSite: http.SameSiteLaxMode Strict → Lax Lax는 안전한 cross-site GET 요청(OAuth 콜백 포함)에서 쿠키 전송 허용 OAuth 표준에서 권장하는 설정 여전히 CSRF 공격 방어 가능 (위험한 POST 요청은 차단) 결과 일반 로그인 → 구글 계정 연결 → 로그아웃 → 구글 직접 로그인 모두 정상 작동 세션 쿠키가 올바르게 설정되어 로그인 상태 유지 브라우저 네트워크 탭에서 Set-Cookie: ...; SameSite=Lax 확인 참고 OAuth를 사용하는 많은 프로젝트에서 동일한 이슈가 발생했으며, 모두 SameSite=Lax로 해결\nhttps://github.com/oauth2-proxy/oauth2-proxy/issues/1663 https://github.com/grafana/grafana/pull/18332 ","date":"2025-10-22T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EA%B9%83%ED%97%88%EB%B8%8C-%EC%BD%94%EB%93%9C%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%9C-pokerogue-%EC%95%B1%EC%97%90%EC%84%9C-%EA%B5%AC%EA%B8%80/%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%99%B8%EB%B6%80-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%B4-%EC%8B%A4%ED%8C%A8%ED%95%98%EB%8A%94-%EC%9D%B4%EC%8A%88/","title":"깃허브 코드로 배포한 Pokerogue 앱에서 구글/디스코드 외부 로그인이 실패하는 이슈"},{"content":" 매니지드 K8S가 아닌 자체 관리형 K8S에서의 Pokerogue구축 방법\nUI 접근을 위한 외부 통신으로 MetalLB+HAproxy+Nginx Controller가 구성 되었다는 전제 하에 구축\nCSP 환경이라면 Ingress 설정에서 AWS ALB Controller와 같은 로드밸런서를 사용해 훨씬 간편하게 구축 가능\nPokerogue 공식문서: https://github.com/pagefaultgames/pokerogue https://github.com/pagefaultgames/rogueserver 아키텍쳐 인프라 구조: Pokerogue WEB(Frontend) - Pokerogue Rogueserver(Backend) - MariaDB(DB)\nWEB과 API 통신 경로를 분리\n분리 이유 SPA 특성상 Frontend가 / 경로로 모든 요청을 받아야 함 Backend는 /api/* 경로를 /*로 rewrite된 상태로 요청을 받아야 함 Ingress는 같은 Host(도메인)에 대해 두 개의 서로 다른 규칙 적용 불가 따라서 HAproxy 설정에서 Frontend는 Ingress, Backend는 LoadBalancer타입 Service로 라우팅되도록 구성 WEB 통신 구조: Client(HTTPS) → HAproxy(SSL Termination) → Nginx Ingress Controller → Ingress → Service(pokerogue-web ClusterIP) → Workload(pokerogue-web) API 통신 구조: Client JS(HTTPS/api/account/info) → HAproxy(SSL Termination + 경로 rewrite ⇒ HTTP /account/info) → Service(pokerogue-server LoadBalancer) → Workloads(pokerogue-server) → Service(pokerogue-db ClusterIP) → Workload(MariaDB) 구글 Oauth 로그인 흐름\nClient가 설정에서 Link Google → pokerogue-web에서 /api/auth/google?mode=link 요청 → rogueserver에서 Google OAuth URL 생성 → 브라우저에서 구글 로그인 → 구글에서 도메인/api/auth/google/callback?code=...\u0026amp;state=... 로 라우팅 → HAproxy(rogueserver-service)로 라우팅 → rogueserver에서 /auth/google/callback로 수신한 code로 구글 ID 조회 + 로그인한 사용자 ID 조회 → DB에서 해당 사용자에 구글 ID값 추가 → 도메인으로 리다이렉트 디스코드 OAuth 로그인도 동일한 흐름 Pokerogue 구축 Pokerogue 네임스페이스 생성\n1 2 kubectl create ns pokerogue kubectl label namespace teleport-cluster \u0026#39;pod-security.kubernetes.io/enforce=baseline\u0026#39; mariadb-pvc 생성\nmariadb-pvc.yaml longhorn pv 사용 1 2 3 4 5 6 7 8 9 10 11 12 apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pokerogue-db-pvc namespace: pokerogue spec: accessModes: - ReadWriteOnce storageClassName: longhorn resources: requests: storage: 10Gi mariadb-secret 생성\nDB 인증정보 secret MYSQL_ROOT_PASSWORD MYSQL_DATABASE MYSQL_USER MYSQL_PASSWORD mariadb 배포\nmariadb-deployment.yaml mariadb secret 참조 mysqladmin ping 명령어로 헬스 체크 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 apiVersion: apps/v1 kind: Deployment metadata: name: pokerogue-db namespace: pokerogue labels: app: pokerogue-db spec: replicas: 1 selector: matchLabels: app: pokerogue-db template: metadata: labels: app: pokerogue-db spec: containers: - name: mariadb image: mariadb:11.2 ports: - containerPort: 3306 name: mysql env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: pokerogue-db-secret key: MYSQL_ROOT_PASSWORD - name: MYSQL_DATABASE valueFrom: secretKeyRef: name: pokerogue-db-secret key: MYSQL_DATABASE - name: MYSQL_USER valueFrom: secretKeyRef: name: pokerogue-db-secret key: MYSQL_USER - name: MYSQL_PASSWORD valueFrom: secretKeyRef: name: pokerogue-db-secret key: MYSQL_PASSWORD volumeMounts: - name: db-data mountPath: /var/lib/mysql resources: requests: memory: \u0026#34;512Mi\u0026#34; cpu: \u0026#34;250m\u0026#34; limits: memory: \u0026#34;1Gi\u0026#34; cpu: \u0026#34;500m\u0026#34; livenessProbe: exec: command: - /usr/bin/mariadb-admin - ping - -h - localhost - -u - root - -p${MYSQL_ROOT_PASSWORD} initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: exec: command: - /usr/bin/mariadb-admin - ping - -h - localhost - -u - root - -p${MYSQL_ROOT_PASSWORD} initialDelaySeconds: 5 periodSeconds: 5 volumes: - name: db-data persistentVolumeClaim: claimName: pokerogue-db-pvc mariadb-service 생성\nmariadb-service.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 apiVersion: v1 kind: Service metadata: name: pokerogue-db namespace: pokerogue spec: selector: app: pokerogue-db ports: - protocol: TCP port: 3306 targetPort: 3306 type: ClusterIP OAuth-secret 생성\nrogueserver oauth 로그인을 위한 secret GOOGLE_CLIENT_ID GOOGLE_SECRET_ID DISCORD_CLIENT_ID DISCORD_CLIENT_SECRET rogueserver 배포\nrogueserver-deployment.yaml initcontainer로 DB가 준비될 때까지 대기 DB secret 참조 OAuth secret 참조 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 apiVersion: apps/v1 kind: Deployment metadata: name: pokerogue-server namespace: pokerogue labels: app: pokerogue-server spec: replicas: 1 selector: matchLabels: app: pokerogue-server template: metadata: labels: app: pokerogue-server spec: # DB가 준비될 때까지 대기 initContainers: - name: wait-for-db image: busybox:1.36 command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#39;until nc -z pokerogue-db 3306; do echo waiting for db; sleep 2; done;\u0026#39;] containers: - name: rogueserver image: \u0026lt;registry명\u0026gt;/rogueserver:latest imagePullPolicy: Always ports: - containerPort: 8001 name: api env: - name: debug value: \u0026#34;true\u0026#34; - name: dbaddr value: \u0026#34;pokerogue-db:3306\u0026#34; - name: dbuser valueFrom: secretKeyRef: name: pokerogue-db-secret key: MYSQL_USER - name: dbpass valueFrom: secretKeyRef: name: pokerogue-db-secret key: MYSQL_PASSWORD - name: dbname valueFrom: secretKeyRef: name: pokerogue-db-secret key: MYSQL_DATABASE - name: gameurl value: \u0026#34;https://pokerogue.\u0026lt;도메인\u0026gt;:48443\u0026#34; - name: callbackurl value: \u0026#34;https://pokerogue.\u0026lt;도메인\u0026gt;:48443/api\u0026#34; - name: googleclientid valueFrom: secretKeyRef: name: pokerogue-oauth-secret key: GOOGLE_CLIENT_ID - name: googlesecretid valueFrom: secretKeyRef: name: pokerogue-oauth-secret key: GOOGLE_SECRET_ID - name: discordclientid valueFrom: secretKeyRef: name: pokerogue-oauth-secret key: DISCORD_CLIENT_ID - name: discordsecretid valueFrom: secretKeyRef: name: pokerogue-oauth-secret key: DISCORD_CLIENT_SECRET resources: requests: memory: \u0026#34;512Mi\u0026#34; cpu: \u0026#34;250m\u0026#34; limits: memory: \u0026#34;1Gi\u0026#34; cpu: \u0026#34;1000m\u0026#34; rogueserver-service생성\nrogueserver-service.yaml type을 LoadBalancer로 만들어서 pokerogue-web용 ingrss에 할당할 IP와 다른 IP를 MetalLB로부터 할당 /api로의 통신은 haproxy에서 rogueserver Service(LB)로 직접 라우팅 1 2 3 4 5 6 7 8 9 10 11 12 13 apiVersion: v1 kind: Service metadata: name: pokerogue-server namespace: pokerogue spec: selector: app: pokerogue-server ports: - protocol: TCP port: 8001 targetPort: 8001 type: LoadBalancer web 배포\nweb-deployment.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 apiVersion: apps/v1 kind: Deployment metadata: name: pokerogue-web namespace: pokerogue labels: app: pokerogue-web spec: replicas: 1 selector: matchLabels: app: pokerogue-web template: metadata: labels: app: pokerogue-web spec: containers: - name: web image: \u0026lt;registry명\u0026gt;/pokerogue-web:latest imagePullPolicy: Always ports: - containerPort: 80 name: http resources: requests: memory: \u0026#34;512Mi\u0026#34; cpu: \u0026#34;500m\u0026#34; limits: memory: \u0026#34;1Gi\u0026#34; cpu: \u0026#34;1000m\u0026#34; web-service생성\nweb-service.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 apiVersion: v1 kind: Service metadata: name: pokerogue-web namespace: pokerogue spec: selector: app: pokerogue-web ports: - protocol: TCP port: 80 targetPort: 80 type: ClusterIP web-ingress 생성\npokerogue-web-ingress.yaml Frontend 라우팅만 처리 nginx.ingress.kubernetes.io/ssl-redirect nginx.ingress.kubernetes.io/force-ssl-redirect SSL Redirect 비활성화 HAproxy에서 SSL Termination 처리하기 때문 Ingress는 HTTP 트래픽만 수신 Host 기반 라우팅 pokerogue.\u0026lt;도메인\u0026gt;으로 들어오는 요청만 처리 HAproxy가 Host 헤더 기반으로 이 Ingress로 라우팅 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: pokerogue-unified-ingress namespace: pokerogue annotations: nginx.ingress.kubernetes.io/ssl-redirect: \u0026#34;false\u0026#34; nginx.ingress.kubernetes.io/force-ssl-redirect: \u0026#34;false\u0026#34; spec: ingressClassName: nginx # TLS is handled by HAProxy, not by Ingress # This allows HAProxy to terminate SSL and forward HTTP to Ingress rules: - host: pokerogue.\u0026lt;도메인\u0026gt; http: paths: - path: / pathType: Prefix backend: service: name: pokerogue-web port: number: 80 SSL 인증서 적용\n인증서 발급 /etc/letsencrypt/live/pokerogue.\u0026lt;도메인\u0026gt;/fullchain.pem /etc/letsencrypt/live/pokerogue.\u0026lt;도메인\u0026gt;/privkey.pem 1 2 3 certbot certonly --manual --preferred-challenges dns \\ -d pokerogue.\u0026lt;도메인\u0026gt; \\ --key-type ecdsa haproxy용 인증서 생성 1 2 3 cat /etc/letsencrypt/live/pokerogue.\u0026lt;도메인\u0026gt;/fullchain.pem \\ /etc/letsencrypt/live/pokerogue.\u0026lt;도메인\u0026gt;/privkey.pem \\ \u0026gt; /etc/haproxy/pokerogue.pem HAproxy 설정\ningress, service를 조회하여 MetalLB로부터 부여받은 IP 확인\nweb: 172.27.1.100 backend: 172.27.1.104 1 2 3 4 5 6 # kubectl get ingress -n pokerogue NAME CLASS HOSTS ADDRESS PORTS AGE pokerogue-unified-ingress nginx pokerogue.\u0026lt;도메인\u0026gt; 172.27.1.100 80 6d21h # kubectl get svc -n pokerogue NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE pokerogue-server LoadBalancer 10.233.16.211 172.27.1.104 8001:30946/TCP 9d 확인한 IP를 HAproxy 설정에 추가\nHTTPS 트래픽 처리\nSSL Termination SSL인증서와 개인키 파일을 결합한 pokerogue.pem파일 사용 X-Forwarded-Port 구글/디스코드 같은 외부 로그인을 위한 Backend OAuth callback URL 생성 시 경로 기반 라우팅 /api/* 요청 시 rogueserver-service로 직접 라우팅 나머지 모든 요청은 pokerogue-ingress로 라우팅 API 서버(rogueserver-service)로의 라우팅을 위해 /api/* 요청을 /* 요청으로 재작성하는 설정 적용\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 frontend pokerogue_frontend bind *:443 ssl crt /etc/haproxy/pokerogue.pem mode http option forwardfor # Set X-Forwarded headers for HTTPS http-request set-header X-Forwarded-Host %[req.hdr(host)] http-request set-header X-Forwarded-Proto https http-request set-header X-Forwarded-Port 48443 # Path-based routing: /api -\u0026gt; Backend API, others -\u0026gt; Frontend use_backend pokerogue_api_backend if { path_beg /api } default_backend metallb_backend_pokerogue backend metallb_backend_pokerogue # Frontend only (no rewrite) server pokerogue 172.27.1.100:80 backend pokerogue_api_backend # Backend API: direct to LoadBalancer Service (MetalLB managed) # Path rewrite: /api/* -\u0026gt; /* http-request set-path %[path,regsub(^/api,)] server pokerogue-server-lb 172.27.1.104:8001 haproxy 적용\n1 2 haproxy -c -f /etc/haproxy/haproxy.cfg systemctl reload haproxy 접근 성공!\n","date":"2025-10-20T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes%EC%97%90%EC%84%9C-pokerogue-%EA%B2%8C%EC%9E%84-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0/","title":"Kubernetes에서 Pokerogue 게임 배포하기"},{"content":" Pokerogue 이미지 빌드\nPokerogue 게임을 컨테이너 이미지로 빌드하기 위한 방법\n개요 Pokerogue 게임을 K8S 환경에서 동작하게 하기 위해서 약간의 소스코드 수정과 Dockerfile 작성이 필요\n공식 GitHub에는 로컬 기준으로 코드가 작성된 부분이 많음 Pokerogue는 Vite 기반 React 프론트엔드와 Go 기반 백엔드 그리고 Mariadb로 구성\nFrontend 공식 GitHub: https://github.com/pagefaultgames/pokerogue TypeScript로 작성 Vite 번들러를 사용하는 React SPA Pokerogue의 UI, 정적파일, 로직 및 랜더링 제공 /api/*로 백엔드 API 호출 OAuth 외부 로그인 UI 제공 Frontend 이미지 빌드 Dockerfile\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ARG NODE_VERSION=22.14 ARG OS=alpine # Build stage FROM node:${NODE_VERSION}-${OS} AS build RUN apk add --no-cache git rsync WORKDIR /app RUN corepack enable \u0026amp;\u0026amp; corepack prepare pnpm@10.14.0 --activate COPY . . # Initialize git submodules RUN git submodule update --init --recursive # Install dependencies with cache RUN --mount=type=cache,target=/root/.pnpm-store \\ pnpm install --frozen-lockfile # Production build RUN pnpm run build # Production stage FROM nginx:alpine COPY nginx.conf /etc/nginx/conf.d/default.conf # Copy built files from build stage COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 80 CMD [\u0026#34;nginx\u0026#34;, \u0026#34;-g\u0026#34;, \u0026#34;daemon off;\u0026#34;] 공식 GitHub에는 beta 브랜치에만 Dockerfile이 존재 development mode로 실행되게 작성되어 있어서 Dockerfile은 새로 전부 작성 Build Stage와 Production Stage 분리\nBuild Stage Node.js 정적 파일 빌드 Git submodule 초기화 pnpm dependencies 설치 pnpm 빌드로 /app/dist 생성 Production Stage Nginx Alpine 이미지 빌드된 정적 파일만 복사 Node.js 런타임 포함하지 않음 Git Submodule 처리\nPokerogue는 assets와 locales를 Git submodule로 관리 최종 이미지에는 빌드된 dist 포함되므로 빌드에 사용된 .git은 제외됨 Nginx 사용\nSPA 라우팅 https://pokerogue.com/ 이외에 /game, /home 등 제공하지 않는 경로로 접근 시 index.html을 제공하도록 설정 gzip 압축 JavaScript, CSS 파일들을 gzip으로 압축하여 전송 보안 헤더 적용 정적 파일 캐싱 이미지, 폰트, JS, CSS 등을 1년간 브라우저 캐시 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; # SPA routing - all routes go to index.html location / { try_files $uri $uri/ /index.html; } gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; add_header X-Frame-Options \u0026#34;SAMEORIGIN\u0026#34; always; add_header X-Content-Type-Options \u0026#34;nosniff\u0026#34; always; add_header X-XSS-Protection \u0026#34;1; mode=block\u0026#34; always; location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control \u0026#34;public, immutable\u0026#34;; } } .env 파일 관리\n환경변수들을 빌드 시점에 포함시켜야함\n.dockeringnore 파일에 명시되어 있던 .env 주석처리\n1 2 3 4 5 6 7 # .dockerignore node_modules *.log *.md .gitignore Dockerfile #.env Vite의 .env 우선순위\nVite는 다음 순서로 .env 파일 로드 나중에 로드된 파일이 이전 파일의 변수값을 덮어씀 .env - 모든 환경에서 로드 .env.[mode] - 특정 모드 (예: .env.production) 따라서 pnpm run build 실행 시 production 모드로 실행되므로 최종적으로 .env.production에 있는 환경변수를 참조하게 됨 .env | .env.production\nVITE_SERVER_URL: 실제 게임을 서비스할 도메인으로 변경 VITE_DISCORD_CLIENT_ID: Discord OAuth 로그인을 위한 본인 값 입력 VITE_GOOGLE_CLIENT_ID: Google OAuth 로그인을 위한 본인 값 입력 1 2 3 4 5 6 VITE_BYPASS_LOGIN=0 VITE_BYPASS_TUTORIAL=0 VITE_SERVER_URL=https://pokerogue.\u0026lt;도메인\u0026gt;:\u0026lt;포트\u0026gt;/api VITE_DISCORD_CLIENT_ID=\u0026lt;discord client id\u0026gt; VITE_GOOGLE_CLIENT_ID=\u0026lt;google client id\u0026gt; VITE_I18N_DEBUG=0 이미지 빌드\n1 2 docker build -t ksi05298/pokerogue-web:latest . docker push ksi05298/pokerogue-web:latest Backend 공식 GitHub: https://github.com/pagefaultgames/rogueserver Go로 작성 사용자 인증, 게임 데이터 저장, 조회, 세션 관리 등을 수행하는 RESTFUL API 서버 /account/*, /gamedata/*, /auth/* 엔드포인트 제공 MariaDB와 연동 Backend 이미지 빌드 Dockerfile\nDockerfile은 공식 GitHub에 있는 파일 그대로 사용 scratch 이미지를 사용해서 정적 바이너리만 포함 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 # SPDX-FileCopyrightText: 2024-2025 Pagefault Games # # SPDX-License-Identifier: AGPL-3.0-or-later ARG GO_VERSION=1.22 FROM docker.io/library/golang:${GO_VERSION} AS builder WORKDIR /src COPY ./go.mod /src/ COPY ./go.sum /src/ RUN go mod download \u0026amp;\u0026amp; go mod verify COPY . /src/ RUN CGO_ENABLED=0 \\ go build -tags=devsetup -o rogueserver RUN chmod +x /src/rogueserver # --------------------------------------------- FROM scratch WORKDIR /app COPY --from=builder /src/rogueserver . COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ EXPOSE 8001 ENTRYPOINT [\u0026#34;./rogueserver\u0026#34;] OAuth 쿠키 설정 수정\n공식 rogueserver 코드에서 구글/디스코드 로그인에 필요한 OAuth 쿠키 설정에 문제 있어 수정 필요 api/endpoints.go 파일의 658줄 handleProviderCallback 함수 내부 수정 전\n1 2 3 4 5 6 7 8 9 http.SetCookie(w, \u0026amp;http.Cookie{ Name: \u0026#34;pokerogue_sessionId\u0026#34;, Value: sessionToken, Path: \u0026#34;/\u0026#34;, Secure: true, SameSite: http.SameSiteStrictMode, Domain: \u0026#34;pokerogue.net\u0026#34;, Expires: time.Now().Add(time.Hour * 24 * 30 * 3), // 3 months }) 수정 후\n1 2 3 4 5 6 7 8 9 http.SetCookie(w, \u0026amp;http.Cookie{ Name: \u0026#34;pokerogue_sessionId\u0026#34;, Value: sessionToken, Path: \u0026#34;/\u0026#34;, Secure: true, SameSite: http.SameSiteLaxMode, Domain: \u0026#34;\u0026#34;, Expires: time.Now().Add(time.Hour * 24 * 30 * 3), // 3 months }) 변경 사항\nSameSite: SameSiteStrictMode → SameSiteLaxMode OAuth callback은 cross-site 요청이 될 수 밖에 없음 (google.com → 내 도메인) Strict로 설정하면 cross-site 쿠키 전송이 차단돼서 CSRF오류가 발생됨 Lax로 변경하여 안전한 도메인에서의 GET 요청(oauth callback)에서 쿠키 전송 허용 Domain: “pokerogue.net” → “” pokerogue 공식 도메인에서 빈 문자열로 변경해서 현재 도메인이 자동 적용되도록 설정 이미지 빌드 및 푸시\n1 2 docker build -t ksi05298/rogueserver:latest . docker push ksi05298/rogueserver:latest 참고 Vite 환경변수 가이드:\nhttps://vitejs.dev/guide/env-and-mode.html OAuth SameSite Cookie 설정:\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite ","date":"2025-10-19T09:00:00+09:00","permalink":"https://joonlog.github.io/p/pokerogue-%EA%B2%8C%EC%9E%84%EC%9D%84-container-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A1%9C-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%9C-%EB%B0%A9%EB%B2%95/","title":"Pokerogue 게임을 Container 이미지로 빌드하기 위한 방법"},{"content":" Kubernetes 환경에서 Jenkins 파이프라인으로 CI를 수행하도록 구성한 Jenkinsfile 정리\nJenkins와 GitLab이 같은 K8S에 있는 환경인 가정하에 유효\nPython, PHP처럼 애플리케이션 빌드 과정이 필요 없는 인터프리터 언어 기반인 경우에 유효\n1. Kubernetes Pod 템플릿 Jenkins 에이전트를 파드 단위로 생성하는 파드 템플릿 정의\ndocker, dind 컨테이너로 구성\ndocker-daemon 컨테이너는 Docker in Docker(DIND)를 사용해서 도커 데몬 실행 workspace-volume와 docker-socket 볼륨 공유\nhostNetwork: true 설정으로 파드의 컨테이너가 호스트 노드(현재 워커 노드)의 네트워크 네임스페이스를 사용할 수 있도록 설정\nhostAliases 설정으로 사설 IP로 GitLab 서버에 접근할 수 있도록 설정 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 agent { kubernetes { yaml \u0026#34;\u0026#34;\u0026#34; apiVersion: v1 kind: Pod spec: serviceAccountName: jenkins hostNetwork: true hostAliases: - ip: \u0026#34;\u0026lt;마스터 서버 사설 IP\u0026gt;\u0026#34; hostnames: - \u0026#34;registry.\u0026lt;도메인\u0026gt;\u0026#34; - \u0026#34;gitlab.\u0026lt;도메인\u0026gt;\u0026#34; containers: - name: docker image: docker command: - sleep args: - 99d tty: true volumeMounts: - name: workspace-volume mountPath: /home/jenkins/agent - name: docker-socket mountPath: /var/run - name: docker-daemon image: docker:dind securityContext: privileged: true env: - name: DOCKER_TLS_CERTDIR value: \u0026#34;\u0026#34; args: - --insecure-registry=registry.\u0026lt;도메인\u0026gt;:8080 volumeMounts: - name: docker-socket mountPath: /var/run volumes: - name: workspace-volume emptyDir: {} - name: docker-socket emptyDir: {} \u0026#34;\u0026#34;\u0026#34; } } 2. Environment Jenkinsfile에서 사용될 전역 환경 변수 정의\nGITLAB_REGISTRY: GitLab Registry 주소 GITLAB_TOKEN: Jenkins Credentials에 저장된 GitLab 토큰 1 2 3 4 5 6 7 8 environment { APP_NAME = \u0026#34;\u0026lt;앱 이름\u0026gt;\u0026#34; RELEASE = \u0026#34;1.0.0\u0026#34; GITLAB_REGISTRY = \u0026#34;registry.\u0026lt;도메인\u0026gt;:8080\u0026#34; GITLAB_TOKEN = credentials(\u0026#39;\u0026lt;credentials 이름\u0026gt;\u0026#39;) IMAGE_NAME = \u0026#34;${GITLAB_REGISTRY}/root/${APP_NAME}\u0026#34; IMAGE_TAG = \u0026#34;${RELEASE}-${BUILD_NUMBER}\u0026#34; } 3. Stages Stage 1\nSCM에서 소스코드 체크아웃 SCM의 url은 K8S Service의 FQDN으로 사용 1 2 3 4 5 6 7 8 9 10 11 12 stage(\u0026#34;Checkout from SCM\u0026#34;) { steps { script { sh \u0026#39;pwd\u0026#39; sh \u0026#39;ls -al\u0026#39; git credentialsId: \u0026#39;\u0026lt;credentials 이름\u0026gt;\u0026#39;, url: \u0026#39;http://gitlab-webservice-default.gitlab.svc.cluster.local:8181/root/\u0026lt;리포지토리명\u0026gt;.git\u0026#39;, branch: \u0026#39;main\u0026#39; sh \u0026#39;ls -al\u0026#39; } } } Stage 2\nDocker 이미지 빌드 DinD 컨테이너에서 이미지 빌드 및 태그 추가 1 2 3 4 5 6 7 8 9 10 11 12 stage(\u0026#39;Build Docker Image\u0026#39;) { steps { container(\u0026#39;docker\u0026#39;) { script { sh \u0026#39;pwd\u0026#39; sh \u0026#39;ls -al\u0026#39; sh \u0026#39;docker build --no-cache -t ${IMAGE_NAME}:${IMAGE_TAG} .\u0026#39; sh \u0026#39;docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest\u0026#39; } } } } Stage 3\nGitLab Registry 이미지 푸시 GitLab Container Registry에 로그인 후 이미지 푸시 GITLAB_TOKEN 환경변수에서 주입받은 credential로 GITLAB_TOKEN_PSW, GITLAB_TOKEN_USR 변수 사용 가능 1 2 3 4 5 6 7 8 9 10 11 stage(\u0026#39;Push to GitLab Registry\u0026#39;) { steps { container(\u0026#39;docker\u0026#39;) { script { sh \u0026#39;echo $GITLAB_TOKEN_PSW | docker login ${GITLAB_REGISTRY} -u $GITLAB_TOKEN_USR --password-stdin\u0026#39; sh \u0026#39;docker push ${IMAGE_NAME}:${IMAGE_TAG}\u0026#39; // Docker 이미지 푸시 sh \u0026#39;docker push ${IMAGE_NAME}:latest\u0026#39; // Docker 이미지 푸시(latest 태그) } } } } 4. 정리 파이프라인 종료 전 도커 리소스 정리 및 로그아웃\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 post { always { // 작업 완료 후 정리 container(\u0026#39;docker\u0026#39;) { script { sh \u0026#39;docker logout ${GITLAB_REGISTRY} || true\u0026#39; sh \u0026#39;docker system prune -f || true\u0026#39; } } } success { echo \u0026#34;Pipeline succeeded! Image ${IMAGE_NAME}:${IMAGE_TAG} pushed to GitLab Registry\u0026#34; } failure { echo \u0026#34;Pipeline failed!\u0026#34; } } 전체 Jenkinsfile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 pipeline { agent { kubernetes { yaml \u0026#34;\u0026#34;\u0026#34; apiVersion: v1 kind: Pod spec: serviceAccountName: jenkins hostNetwork: true hostAliases: - ip: \u0026#34;\u0026lt;마스터 서버 사설 IP\u0026gt;\u0026#34; hostnames: - \u0026#34;registry.\u0026lt;도메인\u0026gt;\u0026#34; - \u0026#34;gitlab.\u0026lt;도메인\u0026gt;\u0026#34; containers: - name: docker image: docker command: - sleep args: - 99d tty: true volumeMounts: - name: workspace-volume mountPath: /home/jenkins/agent - name: docker-socket mountPath: /var/run - name: docker-daemon image: docker:dind securityContext: privileged: true env: - name: DOCKER_TLS_CERTDIR value: \u0026#34;\u0026#34; args: - --insecure-registry=registry.\u0026lt;도메인\u0026gt;:8080 volumeMounts: - name: docker-socket mountPath: /var/run volumes: - name: workspace-volume emptyDir: {} - name: docker-socket emptyDir: {} \u0026#34;\u0026#34;\u0026#34; } } environment { APP_NAME = \u0026#34;\u0026lt;앱 이름\u0026gt;\u0026#34; RELEASE = \u0026#34;1.0.0\u0026#34; // 릴리즈 버전 GITLAB_REGISTRY = \u0026#34;registry.\u0026lt;도메인\u0026gt;:8080\u0026#34; // GitLab Registry 주소 GITLAB_TOKEN = credentials(\u0026#39;\u0026lt;credentials 이름\u0026gt;\u0026#39;) // Jenkins Credentials에 저장된 GitLab 토큰 IMAGE_NAME = \u0026#34;${GITLAB_REGISTRY}/root/${APP_NAME}\u0026#34; // 이미지 이름 IMAGE_TAG = \u0026#34;${RELEASE}-${BUILD_NUMBER}\u0026#34; // 이미지 태그 } stages { stage(\u0026#34;Checkout from SCM\u0026#34;) { // 소스 코드 관리(SCM)에서 체크아웃 단계 steps { script { sh \u0026#39;pwd\u0026#39; sh \u0026#39;ls -al\u0026#39; git credentialsId: \u0026#39;\u0026lt;credentials 이름\u0026gt;\u0026#39;, // GitLab 자격증명 사용 url: \u0026#39;http://gitlab-webservice-default.gitlab.svc.cluster.local:8181/root/\u0026lt;리포지토리명\u0026gt;.git\u0026#39;, // GitLab 리포지토리 URL branch: \u0026#39;main\u0026#39; // 브랜치 이름 sh \u0026#39;ls -al\u0026#39; } } } stage(\u0026#39;Build Docker Image\u0026#39;) { // Docker 이미지 빌드 단계 steps { container(\u0026#39;docker\u0026#39;) { script { sh \u0026#39;pwd\u0026#39; sh \u0026#39;ls -al\u0026#39; sh \u0026#39;docker build --no-cache -t ${IMAGE_NAME}:${IMAGE_TAG} .\u0026#39; // Docker 이미지 빌드 sh \u0026#39;docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest\u0026#39; // latest 태그 추가 } } } } stage(\u0026#39;Push to GitLab Registry\u0026#39;) { // GitLab Registry에 이미지 푸시 단계 steps { container(\u0026#39;docker\u0026#39;) { script { sh \u0026#39;echo $GITLAB_TOKEN_PSW | docker login ${GITLAB_REGISTRY} -u $GITLAB_TOKEN_USR --password-stdin\u0026#39; // GitLab Registry 로그인 sh \u0026#39;docker push ${IMAGE_NAME}:${IMAGE_TAG}\u0026#39; // Docker 이미지 푸시 sh \u0026#39;docker push ${IMAGE_NAME}:latest\u0026#39; // Docker 이미지 푸시(latest 태그) } } } } } post { always { // 작업 완료 후 정리 container(\u0026#39;docker\u0026#39;) { script { sh \u0026#39;docker logout ${GITLAB_REGISTRY} || true\u0026#39; sh \u0026#39;docker system prune -f || true\u0026#39; } } } success { echo \u0026#34;Pipeline succeeded! Image ${IMAGE_NAME}:${IMAGE_TAG} pushed to GitLab Registry\u0026#34; } failure { echo \u0026#34;Pipeline failed!\u0026#34; } } } ","date":"2025-10-17T09:00:00+09:00","permalink":"https://joonlog.github.io/p/k8s%EC%97%90-%EA%B5%AC%EC%B6%95%EB%90%9C-jenkins-gitlab-ci-jenkinsfile-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B9%8C%EB%93%9C-%ED%95%84%EC%9A%94-%EC%97%86%EB%8A%94-%EA%B2%BD%EC%9A%B0/","title":"K8S에 구축된 Jenkins-GitLab CI Jenkinsfile - 애플리케이션 빌드 필요 없는 경우"},{"content":"파이프라인 설정 Definition: Pipeline script from SCM SCM: Git Jenkinsfile을 Git으로 읽도록 설정 git repository url: http://gitlab-webservice-default.gitlab.svc.cluster.local:8181/root/test.git Jenkins 서버와 GitLab 서버가 모두 같은 k8s 클러스터에 있으므로, 외부 서버에서 url 설정하던 것처럼 하긴 어려움 GitLab UI Service의 FQDN인 gitlab-webservice-default.gitlab.svc.cluster.local 을 사용 Credentials: GitLab PAT GitLab의 사용자/리포지토리에서 발급한 PAT를 Jenkins Credentials에 등록해서 사용 Branches to build: */main 빌드할 브랜치 선택 ","date":"2025-10-16T09:00:00+09:00","permalink":"https://joonlog.github.io/p/k8s%EC%97%90-%EA%B5%AC%EC%B6%95%EB%90%9C-jenkins-gitlab-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%84%A4%EC%A0%95/","title":"K8S에 구축된 Jenkins-GitLab 파이프라인 설정"},{"content":"Teleport 사용자 생성 member Role 생성\nmember.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 kind: role version: v7 metadata: name: member spec: allow: kubernetes_groups: [\u0026#34;system:masters\u0026#34;] kubernetes_labels: \u0026#39;*\u0026#39;: \u0026#39;*\u0026#39; kubernetes_resources: - kind: \u0026#39;*\u0026#39; namespace: \u0026#39;*\u0026#39; name: \u0026#39;*\u0026#39; verbs: [\u0026#39;*\u0026#39;] 1 kubectl exec -i deployment/teleport-auth -n teleport-cluster -- tctl create -f \u0026lt; member.yaml myuser 사용자 생성\n출력된 링크를 브라우저에서 접속 1 kubectl exec -it deployment/teleport-auth -n teleport-cluster -- tctl users add myuser --roles=member,access,editor 로그인 성공!\ntsh 로그인 tsh login\n1 tsh login --proxy=teleport.\u0026lt;도메인\u0026gt;:8443 --user=myuser 사용자에 할당된 사용 가능한 k8s cluster 목록\n1 2 3 4 # tsh kube ls Kube Cluster Name Labels Selected ---------------------- ------ -------- teleport.\u0026lt;domain\u0026gt; * k8s cluster login\n1 tsh kube login teleport.\u0026lt;domain\u0026gt; kubectl 성공!\n1 2 3 4 5 6 7 # kubectl get nodes NAME STATUS ROLES AGE VERSION master1 Ready control-plane 51d v1.31.4 master2 Ready control-plane 51d v1.31.4 master3 Ready control-plane 51d v1.31.4 node1 Ready \u0026lt;none\u0026gt; 51d v1.31.4 node2 Ready \u0026lt;none\u0026gt; 51d v1.31.4 ","date":"2025-10-11T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes%ED%99%98%EA%B2%BD-teleport%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%EC%9E%90-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0/","title":"Kubernetes환경 Teleport에서 사용자 관리하기"},{"content":" 매니지드 K8S가 아닌 자체 관리형 K8S에서의 Teleport구축 방법\nUI 접근을 위한 외부 통신으로 MetalLB+HAproxy+Nginx Controller가 구성 되었다는 전제 하에 구축\nCSP 환경이라면 Ingress 설정에서 AWS ALB Controller와 같은 로드밸런서를 사용해 훨씬 간편하게 구축 가능\nTeleport 공식문서: https://goteleport.com/docs/ https://goteleport.com/docs/zero-trust-access/deploy-a-cluster/helm-deployments/kubernetes-cluster/ Teleport 설치 Helm 리포지토리 추가\n1 2 helm repo add teleport https://charts.releases.teleport.dev helm repo update teleport-cluster 네임스페이스 생성\n네임스페이스에 baseline 보안 수준 적용 1 2 kubectl create ns teleport-cluster kubectl label namespace teleport-cluster \u0026#39;pod-security.kubernetes.io/enforce=baseline\u0026#39; pvc 생성\nteleportpvc.yaml kubectl apply -f teleportpvc.yaml 1 2 3 4 5 6 7 8 9 10 11 apiVersion: v1 kind: PersistentVolumeClaim metadata: name: teleport-pvc namespace: teleport-cluster spec: accessModes: [\u0026#34;ReadWriteOnce\u0026#34;] storageClassName: longhorn resources: requests: storage: 10Gi SSL 인증서 Secret 등록\n인증서 발급 /etc/letsencrypt/live/teleport.\u0026lt;도메인\u0026gt;/fullchain.pem /etc/letsencrypt/live/teleport.\u0026lt;도메인\u0026gt;/privkey.pem 1 2 3 certbot certonly --manual --preferred-challenges dns \\ -d teleport.\u0026lt;도메인\u0026gt; \\ --key-type ecdsa secret 생성 1 2 3 kubectl -n teleport-cluster create secret tls teleport-tls \\ --cert=/etc/letsencrypt/live/teleport.\u0026lt;도메인\u0026gt;/fullchain.pem \\ --key=/etc/letsencrypt/live/teleport.\u0026lt;도메인\u0026gt;/privkey.pem teleport 설치\nhttps://goteleport.com/docs/reference/helm-reference/\nvalues.yaml 수정\nhelm으로 설치 1 2 3 4 5 6 7 8 9 10 helm show values teleport/teleport-cluster \u0026gt; values.yaml vim values.yaml helm install teleport teleport/teleport-cluster \\ -f values.yaml \\ -n teleport-cluster helm upgrade --install teleport teleport/teleport-cluster \\ -f values.yaml \\ -n teleport-cluster 클러스터 이름 기입\n1 clusterName: teleport.\u0026lt;도메인\u0026gt; 외부 접근 주소\n1 2 publicAddr: [\u0026#39;teleport.\u0026lt;도메인\u0026gt;:8443\u0026#39;] tunnelPublicAddr: [\u0026#39;teleport.\u0026lt;도메인\u0026gt;:8443\u0026#39;] 프록시가 443 포트 하나에서 ALPN으로 Web UI/SSH 리버스 터널/Kubernetes 프록시를 다 처리\n기본값이 seprate인데 이렇게 되면 3024/3025/3026 등등 각각 포트 사용해야함 외부 주소는 publicAddr로 노출됨 1 proxyListenerMode: \u0026#34;multiplex\u0026#34; Proxy Service\n1 2 3 4 5 service: type: LoadBalancer spec: # MetalLB에서 고정 IP를 쓰고 싶을 때만 지정 # loadBalancerIP: 172.27.1.103 ACME 비활성화\n1 acme: false TLS 인증서 Secret 지정\ncert-manager가 발급한 Secret 이름으로 설정 1 2 tls: existingSecretName: teleport-tls Auth 스토리지 PV 설정\npv는 longhorn이 자동으로 프로비저닝 1 2 3 4 persistence: enabled: true existingClaimName: \u0026#34;teleport-pvc\u0026#34; volumeSize: 10Gi HA replica 설정\nHA 안쓰면 1 1 2 highAvailability: replicaCount: 1 Teleport UI 접근 Teleport는 443포트에서 HTTPS/SSH 리버스 터널/K8S Access Proxy 등을 ALPN으로 동시에 처리하기 때문에, 다른 애플리케이션처럼 ingress로 프록시하면 안됨 ingress가 처리하면 ALPN 정보가 사라져서 Teleport Proxy는 트래픽의 소스를 알 수 없게 됨 web ui는 접근 되더라도 tsh login, tctl, kubectl 등은 모두 실패할 것 Teleport의 Proxy 서비스를 Load Balancer 타입 서비스로 생성해야 함 마찬가지 이유로 haproxy 설정에서도 L7 프록시 말고 L4 프록시가 필요 HAproxy 설정 values로 생성한 load balancer를 조회하여 MetalLB로부터 부여받은 IP 확인\n현재는 172.27.1.103 1 2 3 # kubectl get svc -n teleport-cluster NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE teleport LoadBalancer 10.233.62.211 172.27.1.103 443:30523/TCP,3023:31718/TCP,3026:31539/TCP,3024:31393/TCP,3036:30139/TCP 56s 확인한 IP를 HAproxy 설정에 추가\nHAproxy 서버 공인 IP로 접근 시 teleport의 LB Service로 통신되도록 설정 1 2 3 vim /etc/haproxy/haproxy.cfg haproxy -c -f /etc/haproxy/haproxy.cfg systemctl reload haproxy 1 2 3 4 5 6 7 8 9 10 11 12 frontend unified_frontend_8443 bind *:8443 mode tcp option tcplog tcp-request inspect-delay 5s tcp-request content accept if { req_ssl_hello_type 1 } use_backend metallb_backend_teleport if { req.ssl_sni -i teleport.\u0026lt;도메인\u0026gt; } default_backend metallb_backend_teleport backend metallb_backend_teleport mode tcp server teleport 172.27.1.103:443 접근 성공! ","date":"2025-10-10T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EC%9E%90%EC%B2%B4-%EA%B4%80%EB%A6%AC%ED%98%95-kubernetes%EC%97%90%EC%84%9C%EC%9D%98-teleport-%EA%B5%AC%EC%B6%95/","title":"자체 관리형 Kubernetes에서의 Teleport 구축"},{"content":"Teleport 18 이미지 빌드 도커 이미지 빌드 Dockerfile 빌드한 바이너리 파일의 teleport/tctl/tsh/tbot을 복사해서 이미지 빌드 1 2 3 4 5 6 7 8 9 10 11 12 FROM ubuntu:20.04 RUN apt-get update \u0026amp;\u0026amp; apt-get install -y \\ ca-certificates dumb-init libfido2-1 \u0026amp;\u0026amp; \\ apt-get clean \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* COPY build/teleport /usr/local/bin/teleport COPY build/tctl /usr/local/bin/tctl COPY build/tsh /usr/local/bin/tsh COPY build/tbot /usr/local/bin/tbot ENTRYPOINT [\u0026#34;/usr/bin/dumb-init\u0026#34;, \u0026#34;teleport\u0026#34;, \u0026#34;start\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;/etc/teleport/teleport.yaml\u0026#34;] 이미지 빌드 및 도커허브 푸시 1 2 3 4 docker build -t teleport18-custom . docker login -u \u0026lt;username\u0026gt; docker tag teleport18-custom \u0026lt;username\u0026gt;/teleport18:v1.0.0 docker push \u0026lt;username\u0026gt;/teleport18:v1.0.0 Teleport 18 배포 Auth+Proxy 분리 구축\n디렉토리 구조 1 2 3 4 5 6 7 8 9 10 11 # tree . ├── auth │ ├── config │ │ └── teleport.yaml │ └── data ├── docker-compose.yaml └── proxy ├── config │ └── teleport.yaml └── data 1. docker-compose.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 version: \u0026#39;3.8\u0026#39; services: auth: image: \u0026lt;username\u0026gt;/teleport18:v1.0.0 container_name: teleport-auth hostname: teleport-auth restart: unless-stopped volumes: - ./auth/config:/etc/teleport - ./auth/data:/var/lib/teleport entrypoint: [\u0026#34;/usr/local/bin/teleport\u0026#34;] command: [\u0026#34;start\u0026#34;, \u0026#34;--config=/etc/teleport/teleport.yaml\u0026#34;] expose: - \u0026#34;3025\u0026#34; networks: - teleport-net proxy: image: \u0026lt;username\u0026gt;/teleport18:v1.0.0 container_name: teleport-proxy hostname: teleport-proxy restart: unless-stopped ports: - \u0026#34;3080:3080\u0026#34; # Web UI \u0026amp; public HTTPS - \u0026#34;3023:3023\u0026#34; # Proxy SSH - \u0026#34;3024:3024\u0026#34; # Reverse tunnel - \u0026#34;3026:3026\u0026#34; # Kube reverse tunnel volumes: - ./proxy/config:/etc/teleport - ./proxy/data:/var/lib/teleport - /etc/letsencrypt:/etc/letsencrypt:ro environment: - TELEPORT_CDN_BASE_URL=https://cdn.cloud.gravitational.io entrypoint: [\u0026#34;/usr/local/bin/teleport\u0026#34;] command: [ \u0026#34;start\u0026#34;, \u0026#34;--config=/etc/teleport/teleport.yaml\u0026#34;, \u0026#34;--roles=proxy\u0026#34;, \u0026#34;--token=teleportclustertoken\u0026#34;, \u0026#34;--auth-server=teleport-auth:3025\u0026#34; ] networks: - teleport-net depends_on: - auth networks: teleport-net: driver: bridge 2. auth/teleport.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 teleport: nodename: teleport-auth data_dir: /var/lib/teleport log: output: stderr severity: INFO auth_service: enabled: yes cluster_name: cluster listen_addr: 0.0.0.0:3025 tokens: - \u0026#34;proxy:teleportclustertoken\u0026#34; proxy_service: enabled: no ssh_service: enabled: no 3. proxy/teleport.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 teleport: nodename: teleport-proxy data_dir: /var/lib/teleport log: output: stderr severity: INFO auth_servers: - teleport-auth:3025 auth_service: enabled: no proxy_service: enabled: yes web_listen_addr: 0.0.0.0:3080 tunnel_listen_addr: 0.0.0.0:3024 kube_listen_addr: 0.0.0.0:3026 public_addr: \u0026lt;도메인\u0026gt;:3080 https_keypairs: - cert_file: /etc/letsencrypt/live/\u0026lt;도메인\u0026gt;/fullchain.pem key_file: /etc/letsencrypt/live/\u0026lt;도메인\u0026gt;/privkey.pem ssh_service: enabled: no ","date":"2025-10-09T09:00:00+09:00","permalink":"https://joonlog.github.io/p/teleport-18-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B9%8C%EB%93%9C-%EB%B0%8F-%EB%B0%B0%ED%8F%AC/","title":"Teleport 18 이미지 빌드 및 배포"},{"content":" Teleport 18을 도커 환경에서 빌드하는 과정에서 발생한 오류 정리\n1. UID 0 is not unique 오류 1 2 3 4 5 6 7 wget https://github.com/gravitational/teleport/archive/refs/tags/v18.0.0.tar.gz mkdir teleport \u0026amp;\u0026amp; tar -xzf v18.0.0.tar.gz -C teleport --strip-components=1 cd teleport rm -rf .github/dependabot.yml rm -rf e make -C build.assets build-binaries 에러\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 =\u0026gt; CANCELED [git2 1/2] RUN yum-builddep -y git 3.8s =\u0026gt; ERROR [buildbox 1/27] RUN (groupadd ci --gid=0 -o \u0026amp;\u0026amp; useradd ci --uid=0 --gi 3.7s ------ \u0026gt; [buildbox 1/27] RUN (groupadd ci --gid=0 -o \u0026amp;\u0026amp; useradd ci --uid=0 --gid=0 --create-home --shell=/bin/sh \u0026amp;\u0026amp; mkdir -p -m0700 /var/lib/teleport \u0026amp;\u0026amp; chown -R ci /var/lib/teleport): 3.000 useradd: UID 0 is not unique ------ Dockerfile-centos7:218 -------------------- 217 | ARG GID 218 | \u0026gt;\u0026gt;\u0026gt; RUN (groupadd ci --gid=$GID -o \u0026amp;\u0026amp; useradd ci --uid=$UID --gid=$GID --create-home --shell=/bin/sh \u0026amp;\u0026amp; \\ 219 | \u0026gt;\u0026gt;\u0026gt; mkdir -p -m0700 /var/lib/teleport \u0026amp;\u0026amp; chown -R ci /var/lib/teleport) 220 | -------------------- ERROR: failed to solve: process \u0026#34;/bin/sh -c (groupadd ci --gid=$GID -o \u0026amp;\u0026amp; useradd ci --uid=$UID --gid=$GID --create-home --shell=/bin/sh \u0026amp;\u0026amp; mkdir -p -m0700 /var/lib/teleport \u0026amp;\u0026amp; chown -R ci /var/lib/teleport)\u0026#34; did not complete successfully: exit code: 4 exit status 1 make: *** [Makefile:213: buildbox-centos7] Error 1 make: Leaving directory \u0026#39;/data/teleport/build.assets\u0026#39; root사용자로 작업하던 상황에서 Dockerfile 내 ARG UID 코드로 인해 uid, gid가 0으로 설정됨\n소스 코드 디렉토리 권한 부여 + UID, GID 지정해서 다시실행 1 2 chown -R 1000:1000 ./* nohup make -C build.assets build-binaries UID=1000 GID=1000 \u0026gt; build.log 2\u0026gt;\u0026amp;1 \u0026amp; 2. CentOS7 환경 빌드 실패 make -C build.assets build-binaries 실행 시 centos7에서 실행 ⇒ 버전이 낮아서 빌드 실패 nohup make -C build.assets release-ng \u0026gt; build.log 2\u0026gt;\u0026amp;1\nrelease-ng를 사용한 make가 권장되지면 teleport-buildbox-thirtparty가 18버전 이미지가 없어서 에러 발생\nhttps://github.com/gravitational/teleport/pkgs/container/teleport-buildbox-thirdparty 1 failed to pull teleport-buildbox-thirdparty:teleport18 로컬에서 thirdparty 이미지 빌드 필요\n이미지 빌드에 1시간 이상 걸리니 주의 1 2 3 4 docker buildx build --platform=linux/amd64 --load --tag test-thirdparty:manual -f build.assets/buildbox/Dockerfile-thirdparty build.assets/buildbox docker tag test-thirdparty:manual \u0026lt;username\u0026gt;/teleport-buildbox-thirdparty:teleport18 docker push \u0026lt;username\u0026gt;/teleport-buildbox-thirdparty:teleport18 thirdparty 이미지 빌드 후에는 아래 명령어로 바이너리 빌드\n1 nohup make -C build.assets release-ng UID=1000 GID=1000 BUILDBOX_THIRDPARTY=\u0026lt;username\u0026gt;/teleport-buildbox-thirdparty:teleport18 \u0026gt; build.log 2\u0026gt;\u0026amp;1 \u0026amp; 3. error obtaining VCS status: exit status 128 에러 컨테이너 내부에 .git 디렉토리가 있을 경우 go build가 VCS 정보를 가져오다 실패\nhttps://www.linkedin.com/pulse/addressing-error-obtaining-vcs-status-issue-leo-liu-rhuye/\n1 2 3 4 5 6 7 8 9 10 error obtaining VCS status: exit status 128 Use -buildvcs=false to disable VCS stamping. make[2]: *** [Makefile:388: build/teleport] Error 1 make[2]: Leaving directory \u0026#39;/home/teleport\u0026#39; make[1]: *** [Makefile:354: all] Error 2 make[1]: Leaving directory \u0026#39;/home/teleport\u0026#39; make: *** [Makefile:507: full] Error 2 exit status 2 make[1]: *** [Makefile:624: release-ng] Error 2 make[1]: Leaving directory \u0026#39;/data/teleport/build.assets\u0026#39; 루트 Makefile 상단에 GOFLAGS ?= -buildvcs=false 를 통해 비활성화\n4. 정상 동작 1 nohup make -C build.assets release-ng UID=1000 GID=1000 BUILDBOX_THIRDPARTY=\u0026lt;username\u0026gt;/teleport-buildbox-thirdparty:teleport18 \u0026gt; build.log 2\u0026gt;\u0026amp;1 \u0026amp; 빌드 완료 ","date":"2025-10-08T09:00:00+09:00","permalink":"https://joonlog.github.io/p/teleport-18%EB%B2%84%EC%A0%84-%EB%8F%84%EC%BB%A4-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EC%86%8C%EC%8A%A4-%EB%B9%8C%EB%93%9C-%EC%98%A4%EB%A5%98-troubleshooting/","title":"Teleport 18버전 도커 환경에서의 소스 빌드 오류 TroubleShooting"},{"content":" Teleport 18버전을 소스 빌드하면서 겪은 의존성 관련 문제를 정리 Node, Rust, Corepack 등의 버전이 중요\n1. Corepack is not installed 오류 소스 코드 다운로드 후 make 1 2 3 4 5 6 7 wget https://github.com/gravitational/teleport/archive/refs/tags/v18.0.0.tar.gz mkdir teleport \u0026amp;\u0026amp; tar -xzf v18.0.0.tar.gz -C teleport --strip-components=1 cd teleport rm -rf .github/dependabot.yml rm -rf e make full make full 시 오류 발생\n1 2 3 4 5 6 Error: Corepack is not installed, cannot enable pnpm. See the installation guide https://pnpm.io/installation#using-corepack make[2]: *** [Makefile:1779: ensure-js-package-manager] Error 1 make[2]: Leaving directory \u0026#39;/root/teleport\u0026#39; make[1]: *** [Makefile:1803: ensure-js-deps] Error 2 make[1]: Leaving directory \u0026#39;/root/teleport\u0026#39; make: *** [Makefile:1762: ensure-webassets] Error 2 Teleport 웹 UI는 React 기반 pnpm 패키지 매니저를 Corepack으로 관리 build.assets/versions.mk에 로컬 설치 시 필요 버전 명시되어 있음\n1 2 3 4 GOLANG_VERSION ?= go1.24.4 NODE_VERSION ?= 22.14.0 RUST_VERSION ?= 1.81.0 WASM_PACK_VERSION ?= 0.12.1 nodejs 22에선 아래 명령어로 해결 1 2 corepack enable corepack prepare pnpm@10.12.4 --activate 2. Rust + wasm-pack 버전 에러 Rust 1.72 미만 또는 wasm-pack 설치 누락 시 오류 발생\n1 2 3 4 5 6 7 ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL @gravitational/shared@1.0.0 build-wasm: `node ../../scripts/clean-up-ironrdp-artifacts.mjs \u0026amp;\u0026amp; RUST_MIN_STACK=16777216 wasm-pack build ./libs/ironrdp --target web` Exit status 1 ELIFECYCLE Command failed with exit code 1. /root/teleport/web/packages/teleport: ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL @gravitational/teleport@1.0.0 build: `pnpm build-wasm \u0026amp;\u0026amp; vite build` Exit status 1 ELIFECYCLE Command failed with exit code 1. wasm-pack 설치 후에도 wasm-opt 버전이 낮으면 빌드 실패\nwasm-opt 123 버전 필요 1 2 3 curl -LO https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-x86_64-linux.tar.gz tar -xzf binaryen-version_123-x86_64-linux.tar.gz mv binaryen-version_123/bin/* /usr/local/bin/ 이후 캐시된 wasm-pack 바이너리 덮어쓰기 1 2 3 4 mv ~/.cache/.wasm-pack/wasm-opt-*/bin/wasm-opt \\ ~/.cache/.wasm-pack/wasm-opt-*/bin/wasm-opt.bak mv /usr/local/bin/wasm-opt ~/.cache/.wasm-pack/wasm-opt-*/bin/wasm-opt chmod +x ~/.cache/.wasm-pack/wasm-opt-*/bin/wasm-opt 3. libfido2 관련 경고 libfido2-devel 패키지 미설치 오류\nTeleport의 MFA 기능을 포함하려면 설치 필요 1 2 3 4 5 Warning: Building tctl without libfido2. Install libfido2 to have access to MFA. GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -tags \u0026#34; kustomize_disable_go_plugin_support\u0026#34; -o build/tctl -ldflags \u0026#39;-w -s -X k8s.io/component-base/version.gitVersion=v1.33.2\u0026#39; -trimpath -buildmode=pie ./tool/tctl Warning: Building tsh without libfido2. Install libfido2 to have access to MFA. GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -tags \u0026#34; kustomize_disable_go_plugin_support\u0026#34; -o build/tsh -ldflags \u0026#39;-w -s -X k8s.io/component-base/version.gitVersion=v1.33.2\u0026#39; -trimpath -buildmode=pie ./tool/tsh GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags \u0026#34; kustomize_disable_go_plugin_support\u0026#34; -o build/tbot -ldflags \u0026#39;-w -s -X k8s.io/component-base/version.gitVersion=v1.33.2\u0026#39; -trimpath ./tool/tbot libfido2-devel 패키지 설치\n1 yum install libfido2-devel -y 4. 디렉토리 여유 공간 확보 Teleport 빌드 시 go cache, cargo, rustup, node_modules 등의 캐시 파일들이 쌓이며 최소 10GB 이상의 여유 공간 필요 ","date":"2025-10-07T09:00:00+09:00","permalink":"https://joonlog.github.io/p/teleport-18%EB%B2%84%EC%A0%84-%EB%A1%9C%EC%BB%AC-%EC%86%8C%EC%8A%A4-%EB%B9%8C%EB%93%9C-%EC%98%A4%EB%A5%98-troubleshooting/","title":"Teleport 18버전 로컬 소스 빌드 오류 TroubleShooting"},{"content":"로컬 소스 빌드 Teleport 18 소스 코드를 로컬에서 빌드하기 위한 환경 세팅\nhttps://github.com/gravitational/teleport 1. 기본 패키지 설치 1 dnf groupinstall -y \u0026#34;Development Tools\u0026#34; 2. Node.js 22.14.0 1 2 3 4 curl -fsSL https://rpm.nodesource.com/setup_22.x | bash - dnf install -y nodejs node -v 3. Corepack ≥ 0.31 및 pnpm ≥ 10.11.0 1 2 3 4 corepack enable corepack prepare pnpm@10.12.4 --activate corepack -v 4. Python 3.8 1 2 3 4 5 dnf install -y python38 ln -s /usr/bin/python3.8 /usr/bin/python export PYTHON=$(which python3.8) python --version 5. Rust ≥ 1.72 및 wasm-pack 0.12.1 1 2 3 4 5 curl https://sh.rustup.rs -sSf | sh -s -- -y source $HOME/.cargo/env cargo install wasm-pack --locked --version 0.12.1 rustc --version 6. (선택) wasm-opt 123 (Binaryen) wasm-opt로 인한 빌드 오류 발생 시 최신 버전으로 교체 111 버전은 오류 발생 1 2 3 4 5 6 7 cd /usr/local/bin curl -LO https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-x86_64-linux.tar.gz tar -xzf binaryen-version_123-x86_64-linux.tar.gz mv binaryen-version_123/bin/* /usr/local/bin/ rm -rf binaryen-version_123* wasm-opt --version 7. libfido2-devel 1 dnf install -y libfido2-devel 8. Teleport 소스코드 다운로드 및 빌드 1 2 3 4 5 6 wget https://github.com/gravitational/teleport/archive/refs/tags/v18.0.0.tar.gz mkdir teleport \u0026amp;\u0026amp; tar -xzf v18.0.0.tar.gz -C teleport --strip-components=1 cd teleport make clean make full 도커 소스 빌드 Teleport 18 소스 빌드를 도커 환경에서 하기 위한 방법\n1. 소스코드 Pull 1 2 3 4 5 6 wget https://github.com/gravitational/teleport/archive/refs/tags/v18.0.0.tar.gz mkdir teleport \u0026amp;\u0026amp; tar -xzf v18.0.0.tar.gz -C teleport --strip-components=1 cd teleport rm -rf .github/dependabot.yml rm -rf e chown -R pjt:pjt ./* 2. go VCS 오류 해결 ./Makefile 최상단에 하기 설정 추가 필요 1 GOFLAGS ?= -buildvcs=false 3. 도커 make teleport-buildbox-thirdparty:teleport18 이미지 제작\nhttps://github.com/gravitational/teleport/pkgs/container/teleport-buildbox-thirdparty 경로에 18버전 이미지가 없어서 에러 로컬에서 이미지 빌드 필요 이미지 빌드에 1시간 이상 걸리니 주의 1 2 3 4 docker buildx build --platform=linux/amd64 --load --tag test-thirdparty:manual -f build.assets/buildbox/Dockerfile-thirdparty build.assets/buildbox docker tag test-thirdparty:manual ksi05298/teleport-buildbox-thirdparty:teleport18 docker push ksi05298/teleport-buildbox-thirdparty:teleport18 ksi05298/teleport-buildbox-thirdparty:teleport18\n1 nohup make -C build.assets release-ng BUILDBOX_THIRDPARTY=ksi05298/teleport-buildbox-thirdparty:teleport18 GOFLAGS=\u0026#34;-buildvcs=false\u0026#34; \u0026gt; build.log 2\u0026gt;\u0026amp;1 \u0026amp; ","date":"2025-10-06T09:00:00+09:00","permalink":"https://joonlog.github.io/p/teleport-18-%EC%86%8C%EC%8A%A4-%EB%B9%8C%EB%93%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95/","title":"Teleport 18 소스 빌드 환경 설정"},{"content":" IDE 환경에서는 잘 작동하던 Context Path 설정이 Docker Tomcat에서는 무시되어 발생한 문제 Tomcat의 WAR 파일 배포 메커니즘과 appBase/docBase 개념을 이해해야 해결 가능한 이슈\n환경 Java 1.8 Tomcat 9.0.41 Spring 기반 CMS Docker로 Tomcat WAS 구성 문제 상황 빌드한 app.war를 Docker Tomcat으로 배포 후 /app/index.do 접근 시 무한 리다이렉트 발생 /app/index.do → /index.html → /app/index.do 반복 context path가 /로 설정됐어야 하는데 app으로 설정되어 있음 server.xml 설정이 무시됨 초기 설정 Dockerfile\n1 2 COPY app.war ${CATALINA_HOME}/webapps/app.war COPY server.xml ${CATALINA_HOME}/conf/server.xml server.xml\n1 2 3 4 \u0026lt;Host name=\u0026#34;localhost\u0026#34; appBase=\u0026#34;webapps\u0026#34; unpackWARs=\u0026#34;true\u0026#34; autoDeploy=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;Context path=\u0026#34;\u0026#34; docBase=\u0026#34;app\u0026#34; reloadable=\u0026#34;false\u0026#34; /\u0026gt; \u0026lt;/Host\u0026gt; 에러 분석 무한 리다이렉트 발생 이유 애플리케이션에 index.do를 받을 수 있는 2개의 컨트롤러 존재 @RequestMapping(\u0026quot;/index.do\u0026quot;) index.html로 리다이렉트 @RequestMapping(\u0026quot;/{siteId}/index.do\u0026quot;) 실제 사이트 로직 처리 브라우저에서 /app/index.do로 접근 시 2번으로 가야하는데 계속 1번으로만 처리됨 context path가 /가 아닌 app으로 되어 있다는 증거 /app/index.do로 요청 Tomcat이 context path 제거 → Servlet Path = /index.do Spring이 /index.do 매핑 찾음 → 1번 컨트롤러 매칭 redirect:/index.html 반환 /index.html 내부에 location.href = \u0026quot;/app/index.do\u0026quot; 스크립트 1번으로 돌아가서 무한 루프 server.xml 설정이 무시된 이유 Tomcat의 WAR 배포 우선순위 autoDeploy=\u0026quot;true\u0026quot; 설정 시 webapps 디렉토리 자동 스캔 app.war 발견 → 파일명 기준으로 자동 배포 Context Path = /app server.xml의 \u0026lt;Context\u0026gt; 설정은 자동 배포에 의해 무시됨 IDE 환경과의 차이가 발생한 이유 IDE UI에서 Context Path를 /로 직접 지정 가능 WAR 파일명과 무관하게 IDE 내부적으로 Context 설정 파일 생성 Docker Tomcat WAR 파일명이 Context Path를 결정 Tomcat 자체 규칙만 적용 해결 방법 1 war 파일을 ROOT.war로 배포\n가장 간단하고 확실한 방법\nContext Path가 ROOT.war 파일명에 따라 /로 설정됨\nDockerfile\n1 COPY app.war ${CATALINA_HOME}/webapps/ROOT.war /app/index.do로 요청 Tomcat이 context path 제거 → Servlet Path = /app/index.do Spring이 /{siteId}/index.do 패턴 매칭 → 2번 컨트롤러 매칭 (siteId=app) 정상 동작 해결 방법 2 Context xml 파일 설정 app.war 파일명을 유지하면서 Context Path를 변경하는 방법\nappBase: Tomcat이 애플리케이션을 찾는 작업 공간\n기본: webapps docBase: 특정 애플리케이션의 실제 파일 위치\nTomcat 9 보안 정책: appBase 내부의 WAR를 docBase로 지정하면 무시됨\nwar 파일을 appBase와 다른곳으로 이동\nDockerfile\n1 2 3 4 5 6 7 8 9 10 # WAR를 appBase 밖에 배치 RUN mkdir -p ${CATALINA_HOME}/apps COPY app.war ${CATALINA_HOME}/apps/app.war # Context 설정 파일 생성 RUN mkdir -p ${CATALINA_HOME}/conf/Catalina/localhost COPY ROOT.xml ${CATALINA_HOME}/conf/Catalina/localhost/ROOT.xml # server.xml 수정 (autoDeploy 비활성화) COPY server.xml ${CATALINA_HOME}/conf/server.xml ROOT.xml\n1 2 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;Context docBase=\u0026#34;${catalina.home}/apps/app.war\u0026#34; /\u0026gt; server.xml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;Server port=\u0026#34;8005\u0026#34; shutdown=\u0026#34;SHUTDOWN\u0026#34;\u0026gt; \u0026lt;Service name=\u0026#34;Catalina\u0026#34;\u0026gt; \u0026lt;Connector port=\u0026#34;8080\u0026#34; protocol=\u0026#34;HTTP/1.1\u0026#34; connectionTimeout=\u0026#34;20000\u0026#34; redirectPort=\u0026#34;8443\u0026#34; URIEncoding=\u0026#34;UTF-8\u0026#34; /\u0026gt; \u0026lt;Engine name=\u0026#34;Catalina\u0026#34; defaultHost=\u0026#34;localhost\u0026#34;\u0026gt; \u0026lt;Host name=\u0026#34;localhost\u0026#34; appBase=\u0026#34;webapps\u0026#34; unpackWARs=\u0026#34;true\u0026#34; autoDeploy=\u0026#34;false\u0026#34; deployOnStartup=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;/Host\u0026gt; \u0026lt;/Engine\u0026gt; \u0026lt;/Service\u0026gt; \u0026lt;/Server\u0026gt; 동작 순서\nserver.xml의 autoDeploy=\u0026quot;false\u0026quot; 설정으로 webapps의 WAR자동 배포 비활성화 server.xml의 deployOnStartup=\u0026quot;true\u0026quot; 설정으로 Context XML 파일 읽기 활성화 ROOT.xml → Context Path / 지정 파일명이 ROOT이므로 /로 설정됨 ROOT.xml의 docBase=\u0026quot;/opt/tomcat/apps/app.war\u0026quot; 설정으로 appBase 밖의 경로에서 war참조 Tomcat이 apps/app.war를 webapps/ROOT/로 압축 해제 후 실행 정상 동작 정리 Tomcat의 WAR 배포 규칙\nWAR 파일명: Context Path\n1 2 3 ROOT.war -\u0026gt; / admin.war -\u0026gt; /admin my-app.war -\u0026gt; /my-app server.xml의 Context 설정은 비권장\nautoDeploy=\u0026quot;true\u0026quot; 환경에서는 대부분 무시됨 설정 변경 시 Tomcat 전체 재시작 필요 appBase / docBase\nappBase 내부의 WAR를 docBase로 지정하면 Tomcat 9에서 무시됨\n항목 appBase docBase 정의 Tomcat의 작업 공간 앱의 소스 위치 기본값 webapps - 내용 WAR + 압축 해제된 코드 WAR 또는 디렉토리 경로 자동 배포 O X Context Path / Servlet Path\n애플리케이션 라우팅 로직이 Servlet Path를 기준으로 설계되었다면 Context Path가 라우팅에 영향을 줄 수 있음\n요청 URL: /app/index.do\n1 2 Context Path: / Servlet Path: /app/index.do 요청 URL: /app/index.do\n1 2 Context Path: /app Servlet Path: /index.do 참고 https://tomcat.apache.org/tomcat-9.0-doc/config/context.html https://tomcat.apache.org/tomcat-9.0-doc/config/host.html#Automatic_Application_Deployment https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-requestmapping.html ","date":"2025-10-04T09:00:00+09:00","permalink":"https://joonlog.github.io/p/docker-tomcat%EC%97%90%EC%84%9C-context-path-%EC%84%A4%EC%A0%95-%EB%AC%B4%EC%8B%9C-troubleshooting/","title":"Docker Tomcat에서 Context Path 설정 무시 TroubleShooting"},{"content":" /etc/cron.daily/logrotate에 설정된 스크립트를 통해 자동으로 동작 예시 mariadb general.log 대상 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 vim /etc/logrotate.d/mariadb /data/mariadb_log/general.log { # Depends on a mysql@localhost unix_socket authenticated user with RELOAD privilege su mysql mysql # If any of the files listed above is missing, skip them silently without # emitting any errors missingok # If file exists but is empty, don\u0026#39;t rotate it notifempty # 달에 1번 보관 monthly # 6개월 보관 rotate 6 # 500M 초과할 경우 달에 상관없이 즉시 보관 maxsize 500M # 50M 미만일 경우 달에 상관없이 보관 X minsize 50M # 압축해서 보관 compress # 가장 최근 보관한 로그는 압축하지 않음 delaycompress # Don\u0026#39;t run the postrotate script for each file configured in this file, but # run it only once if one or more files were rotated sharedscripts # redhat 계열 flush postrotate if command -v mariadb-admin \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then mariadb-admin --user=root --password=\u0026#39;\u0026lt;PASSWORD\u0026gt;\u0026#39; --host=localhost \\ --local flush-error-log flush-engine-log flush-general-log flush-slow-log fi endscript } ","date":"2025-10-03T09:00:00+09:00","permalink":"https://joonlog.github.io/p/linux-logrotate%EB%A5%BC-%ED%86%B5%ED%95%9C-%EB%A1%9C%EA%B7%B8-%EA%B4%80%EB%A6%AC/","title":"Linux logrotate를 통한 로그 관리"},{"content":" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # 현재 스왑 메모리 확인 free -mh # Swap 파일 생성 fallocate -l 2G .swapfile # fallocate 안되면 dd dd if=/dev/zero of=/swapfile2 bs=1M count=2048 # Swap 파일 권한 변경 chmod 600 .swapfile # Swap 파일 포맷 설정 및 Swap 파일 등록 mkswap .swapfile swapon .swapfile # Swap Memory 가 정상적으로 등록 되었는지 확인 swapon -s free -m # 부팅시에도 Swap 연결이 되도록 Swap 파일 영구적 마운트 하는 방법 vim /etc/fstab .swapfile swap swap defaults 0 0 # Swap Memory 비활성화 및 삭제 swapoff .swapfile rm .swapfile # Swap 파일 마운트 해제 vim /etc/fstab [swap파일경로] swap swap defaults 0 0 \u0026lt;\u0026lt; 문구 주석 처리 ","date":"2025-10-02T09:00:00+09:00","permalink":"https://joonlog.github.io/p/linux-swap-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%A6%9D%EC%84%A4-%EB%B0%A9%EB%B2%95/","title":"Linux Swap 메모리 증설 방법"},{"content":" kubernetes는 노드에 리소스가 부족할 때, Pod의 QoS 클래스에 따라 어떤 Pod를 우선적으로 Eviction(퇴출)할지를 결정\nQoS 클래스 종류 Guaranteed Pod 내 모든 컨테이너의 CPU/Memory Request와 Limit 값이 동일한 경우 가장 높은 우선순위를 가짐 (Eviction 대상에서 가장 안전함) Burstable Guaranteed와 BestEffort에 속하지 않는 모든 경우 예: Request \u0026lt; Limit 인 경우 우선순위는 중간 BestEffort 리소스 Request와 Limit을 전혀 지정하지 않은 경우 가장 낮은 우선순위 (Eviction 1순위) Eviction 동작 원리 노드에 리소스가 부족해지면, kubelet은 다음과 같은 순서로 Pod를 Eviction\n우선순위: Guaranteed \u0026gt; Burstable \u0026gt; BestEffort BestEffort 항상 Eviction 대상에 포함됨 Burstable CPU/Memory Request보다 실제 사용량이 큰 경우 Eviction 대상 Guaranteed Eviction 대상에서 가장 안전 주의: Request와 Limit 차이를 너무 크게 잡으면 Burstable로 분류되어 Eviction 대상\n따라서 Request와 Limit은 가급적 동일하게 잡는 것이 안정적\nCPU vs Memory CPU → 압축 가능한(compressible) 리소스 Request보다 더 쓰더라도 kubelet이 throttle(스로틀링) 해서 CPU 사용량을 줄일 수 있음 즉, CPU 경합이 발생해도 프로세스가 죽지는 않음 Memory, Storage → 압축 불가능한(incompressible) 리소스 Memory 사용이 Request를 초과하면, kubelet은 해당 Pod를 Eviction시켜야 함 Storage도 마찬가지로 강제 종료가 필요할 수 있음 EX) Guaranteed Pod 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 apiVersion: v1 kind: Pod metadata: name: guaranteed-pod spec: containers: - name: nginx image: nginx resources: requests: memory: \u0026#34;512Mi\u0026#34; cpu: \u0026#34;500m\u0026#34; limits: memory: \u0026#34;512Mi\u0026#34; cpu: \u0026#34;500m\u0026#34; Request = Limit 이므로 QoS = Guaranteed\nEX) Burstable Pod 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 apiVersion: v1 kind: Pod metadata: name: burstable-pod spec: containers: - name: nginx image: nginx resources: requests: memory: \u0026#34;256Mi\u0026#34; cpu: \u0026#34;250m\u0026#34; limits: memory: \u0026#34;512Mi\u0026#34; cpu: \u0026#34;500m\u0026#34; Request \u0026lt; Limit 이므로 QoS = Burstable EX) BestEffort Pod 1 2 3 4 5 6 7 8 apiVersion: v1 kind: Pod metadata: name: besteffort-pod spec: containers: - name: nginx image: nginx Request/Limit 미지정 → QoS = BestEffort Pod QoS Class 확인 방법 1 kubectl get pod {POD_NAME} -o jsonpath=\u0026#39;{ .status.qosClass}{\u0026#34;\\n\u0026#34;}\u0026#39; 정리 QoS 클래스는 Pod의 리소스 보장 수준 Eviction 순서: Guaranteed \u0026gt; Burstable \u0026gt; BestEffort CPU는 throttle 가능(죽지 않음), Memory/Storage는 Eviction 발생 안정적인 서비스 운영을 위해 Request와 Limit을 동일하게 설정하는 것이 가장 안전 참고 자료 Kubernetes 공식 문서 - QoS Kubernetes 공식 문서 - QoS 설정 예제 No Easy Dev 블로그 ssup2 - Pod Eviction 분석 https://jenakim47.tistory.com/96 ","date":"2025-09-30T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-qos-overview/","title":"Kubernetes QoS Overview"},{"content":" GitLab을 Helm Chart로 구축 + 브라우저에서 GitLab 접근 시 80 포트가 아닌 다른 포트로 접근해야 할 경우 발생하는 이슈 GitLab Helm Chart의 포트 설정이 80/443으로 고정되어 있기 때문에 발생\n환경 자체 관리형 kubernetes이기에 외부용 L7 로드밸런서로 haproxy를 사용 중인 경우 helm chart로 GitLab 서버를 구축한 경우 외부에서 브라우저로 GitLab UI 접근 시 80포트가 아닌 8080과 같은 별도의 포트로만 접근해야하는 제약사항이 있는 경우 문제 상황 helm chart로 GitLab 서버 구축 후 \u0026lt;도메인\u0026gt;:\u0026lt;포트\u0026gt;로 접근 시 포트가 제거된 후 도메인으로만 강제 리다이렉션 되는 현상 발생 404 에러 브라우저 → HAProxy(8080) → GitLab(80) → 리다이렉트 응답 해결 방법 HAproxy 설정을 통해 포트를 포함해서 리다이렉트 하도록 설정\nhttp-response replace-value Location ^http://gitlab\\.\u0026lt;도메인\u0026gt;/(.*)$ http://gitlab.\u0026lt;도메인\u0026gt;:8080/\\\\1\n정규식 분석: ^http://gitlab.\u0026lt;도메인\u0026gt;/(.*)$ - 매칭 패턴 http://gitlab.\u0026lt;도메인\u0026gt;:8080/\\1 - 치환 결과 \\1 = 첫 번째 그룹 (.*) 내용 (URL 경로) 변환 예시: 원본: Location: http://gitlab.\u0026lt;도메인\u0026gt;/users/sign_in 변환: Location: http://gitlab.\u0026lt;도메인\u0026gt;/users/sign_in 외부 브라우저에서 GitLab 접근 시 포트를 붙여서 접근해야 될 경우 반드시 설정 설정하지 않으면 접근 시 자동으로 포트가 제거된 상태로 리다이렉션되어 404 에러 1 2 3 4 5 6 7 8 9 10 11 12 13 frontend unified_frontend_8080 bind *:8080 mode http option forwardfor # Host 기반 라우팅 ... use_backend metallb_backend_gitlab if { hdr(host) -i gitlab.\u0026lt;도메인\u0026gt; } ... backend metallb_backend_gitlab http-response replace-value Location ^http://gitlab\\.\u0026lt;도메인\u0026gt;/(.*)$ http://gitlab.\u0026lt;도메인\u0026gt;:8080/\\\\1 server gitlab 172.27.1.102:80 결과 브라우저가 8080포트를 포함해서 리다이렉트 HAProxy를 통한 접속 유지 ","date":"2025-09-27T09:00:00+09:00","permalink":"https://joonlog.github.io/p/k8s%EC%97%90-%EA%B5%AC%EC%B6%95%ED%95%9C-gitlab-registry%EC%97%90-container-image-%ED%91%B8%EC%8B%9C-%EC%8B%9C-blob-upload-unknown-%EC%97%90%EB%9F%AC-troubleshooting/","title":"K8S에 구축한 GitLab Registry에 Container Image 푸시 시 blob upload unknown 에러 TroubleShooting"},{"content":" K8S 환경의 Jenkins에서 docker build가 Jenkins 파이프라인 파드의 dind 컨테이너에서 실행되기 때문에 발생한 이슈\n환경 Jenkins 서버를 K8S에서 구축하고, 파이프라인을 K8S 파드에서 동작하도록 설정한 환경 Jenkinsfile에서 K8S 파드 템플릿으로 파이프라인 실행하게 명시 문제 상황 Jenkinsfile의 docker build 단계에서 파이프라인 빌드 실패 Dockerfile의 composer install 레이어에서 Could not fetch https://api.github.com/repos/ 에러 발생 외부에서 데이터를 받아오는 명령어에서 전부 timeout 에러 발생 에러 분석 Jenkins 파이프라인이 K8S 파드의 docker 컨테이너에서 실행되고, docker build 작업은 dind 컨테이너에서 실행됨 dind 컨테이너 입장에서 네트워크가 격리되어 있어 GitHub API 서버로의 아웃바운드 연결 불가 따라서 composer install 와 같은 명령어 실행 시 연결 실패 에러 발생 해결 방법 Jenkinsfile의 k8s 파드 템플릿에 hostNetwork: true를 설정해서 파드의 컨테이너가 호스트 노드(현재 워커 노드)의 네트워크 네임스페이스를 사용할 수 있도록 설정 파드 템플릿이 아닌 docker build --network=host로 명령어 마다 옵션을 명시해서 해결도 가능하지만, 파드 레벨에서 해결하는게 관리에 용이 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 kubernetes { yaml \u0026#34;\u0026#34;\u0026#34; apiVersion: v1 kind: Pod spec: serviceAccountName: jenkins hostNetwork: true hostAliases: - ip: \u0026#34;172.27.1.9\u0026#34; hostnames: - \u0026#34;registry.\u0026lt;도메인\u0026gt;\u0026#34; - \u0026#34;gitlab.\u0026lt;도메인\u0026gt;\u0026#34; containers: - name: docker image: docker command: - sleep args: - 99d tty: true volumeMounts: - name: workspace-volume mountPath: /home/jenkins/agent - name: docker-socket mountPath: /var/run - name: docker-daemon image: docker:dind securityContext: privileged: true env: - name: DOCKER_TLS_CERTDIR value: \u0026#34;\u0026#34; args: - --insecure-registry=registry.\u0026lt;도메인\u0026gt;:8080 volumeMounts: - name: docker-socket mountPath: /var/run volumes: - name: workspace-volume emptyDir: {} - name: docker-socket emptyDir: {} \u0026#34;\u0026#34;\u0026#34; } ","date":"2025-09-27T09:00:00+09:00","permalink":"https://joonlog.github.io/p/k8s%EC%97%90-%EA%B5%AC%EC%B6%95%ED%95%9C-jenkins-ci-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8%EC%9D%98-dind-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%97%90%EB%9F%AC-troubleshooting/","title":"K8S에 구축한 Jenkins CI 파이프라인의 Dind 네트워크 에러 TroubleShooting"},{"content":" GitLab을 Helm Chart로 구축 + 브라우저에서 GitLab 접근 시 80 포트가 아닌 다른 포트로 접근해야 할 경우 발생하는 이슈 GitLab Helm Chart의 포트 설정이 80/443으로 고정되어 있기 때문에 발생\n환경 자체 관리형 kubernetes이기에 외부용 L7 로드밸런서로 haproxy를 사용 중인 경우 helm chart로 GitLab 서버를 구축한 경우 외부에서 브라우저로 GitLab UI 접근 시 80포트가 아닌 8080과 같은 별도의 포트로만 접근해야하는 제약사항이 있는 경우 문제 상황 helm chart로 GitLab 서버 구축 후 \u0026lt;도메인\u0026gt;:\u0026lt;포트\u0026gt;로 접근 시 포트가 제거된 후 도메인으로만 강제 리다이렉션 되는 현상 발생 404 에러 브라우저 → HAProxy(8080) → GitLab(80) → 리다이렉트 응답 해결 방법 HAproxy 설정을 통해 포트를 포함해서 리다이렉트 하도록 설정\nhttp-response replace-value Location ^http://gitlab\\.\u0026lt;도메인\u0026gt;/(.*)$ http://gitlab.\u0026lt;도메인\u0026gt;:8080/\\\\1\n정규식 분석: ^http://gitlab.\u0026lt;도메인\u0026gt;/(.*)$ - 매칭 패턴 http://gitlab.\u0026lt;도메인\u0026gt;:8080/\\1 - 치환 결과 \\1 = 첫 번째 그룹 (.*) 내용 (URL 경로) 변환 예시: 원본: Location: http://gitlab.\u0026lt;도메인\u0026gt;/users/sign_in 변환: Location: http://gitlab.\u0026lt;도메인\u0026gt;/users/sign_in 외부 브라우저에서 GitLab 접근 시 포트를 붙여서 접근해야 될 경우 반드시 설정 설정하지 않으면 접근 시 자동으로 포트가 제거된 상태로 리다이렉션되어 404 에러 1 2 3 4 5 6 7 8 9 10 11 12 13 frontend unified_frontend_8080 bind *:8080 mode http option forwardfor # Host 기반 라우팅 ... use_backend metallb_backend_gitlab if { hdr(host) -i gitlab.\u0026lt;도메인\u0026gt; } ... backend metallb_backend_gitlab http-response replace-value Location ^http://gitlab\\.\u0026lt;도메인\u0026gt;/(.*)$ http://gitlab.\u0026lt;도메인\u0026gt;:8080/\\\\1 server gitlab 172.27.1.102:80 결과 브라우저가 8080포트를 포함해서 리다이렉트 HAProxy를 통한 접속 유지 ","date":"2025-09-25T09:00:00+09:00","permalink":"https://joonlog.github.io/p/k8s%EC%97%90-%EA%B5%AC%EC%B6%95%ED%95%9C-gitlab-ui-%EC%A0%91%EA%B7%BC-%EC%8B%9C-%EC%A0%91%EA%B7%BC-%ED%8F%AC%ED%8A%B8%EA%B0%80-%EC%A0%9C%EA%B1%B0%EB%90%9C-%EC%83%81%ED%83%9C%EB%A1%9C-%EA%B0%95%EC%A0%9C-%EB%A6%AC%EB%8B%A4%EC%9D%B4%EB%A0%89%EC%85%98-troubleshooting/","title":"K8S에 구축한 GitLab UI 접근 시 접근 포트가 제거된 상태로 강제 리다이렉션 TroubleShooting"},{"content":" 매니지드 K8S가 아닌 자체 관리형 K8S에서의 GitLab 구축 방법\nUI 접근을 위한 외부 통신으로 MetalLB+HAproxy+Nginx Controller가 구성 되었다는 전제 하에 구축\nCSP 환경이라면 Ingress 설정에서 AWS ALB Controller와 같은 로드밸런서를 사용해 훨씬 간편하게 구축 가능\nGitLab 공식문서: ****https://docs.gitlab.com/install/install_methods/#helm-chart GitLab Artifacthub 문서: https://artifacthub.io/packages/helm/gitlab/gitlab GitLab 설치 Helm 리포지토리 추가\n1 2 helm repo add gitlab https://charts.gitlab.io/ helm repo update gitlab 네임스페이스 생성\n1 kubectl create ns gitlab gitlab-pv 생성(StorageClass 없을 시에만 진행)\n기존에 사용하던 Ceph, Longhorn, EBS Driver, Harbor 같은 StorageClass가 없을 경우에 로컬 경로를 사용한 PV 할당 방법 StorageClass가 있다면 PVC 생성 시 자동으로 PV가 할당되기 때문에 이 과정은 불필요 1 2 vim gitlab-01-volume.yaml kubectl apply -f gitlab-01-volume.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 apiVersion: v1 kind: PersistentVolume metadata: name: gitlab-pv spec: storageClassName: gitlab-pv accessModes: - ReadWriteOnce capacity: storage: 20Gi persistentVolumeReclaimPolicy: Retain hostPath: path: /data/gitlab-volume/ --- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: gitlab-pv provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer gitlab 설치\nvalues.yaml 수정 helm으로 설치 1 2 3 4 helm show values gitlab/gitlab \u0026gt; values.yaml vim values.yaml helm install gitlab -n gitlab -f values.yaml gitlab/gitlab 소스코드 리포지토리 설정 1 2 3 4 5 6 # GitLab Gitaly (Git 저장소) gitlab: gitaly: persistence: storageClass: \u0026#34;longhorn\u0026#34; size: 20Gi postgresql, redis 설정 postgresql: gitlab 전체의 메타데이터 저장 redis: 사용자 세션, 백그라운드 작업, 캐시, 알림 등에 사용 1 2 3 4 5 6 7 8 9 10 11 12 13 # PostgreSQL postgresql: primary: persistence: storageClass: \u0026#34;longhorn\u0026#34; size: 8Gi # Redis redis: master: persistence: storageClass: \u0026#34;longhorn\u0026#34; size: 2Gi minio설정 https://docs.gitlab.com/charts/charts/globals#configure-minio-settings S3와 호환되는 오브젝트 스토리지 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 global: # minio 서비스 활성화 minio: enabled: true # 바이너리 파일들이 오브젝트 스토리지를 사용할 수 있도록 설정 appConfig: object_store enabled: true # Container Registry가 minio의 registry 버킷을 사용 registry: bucket: registry # minio가 Longhorn을 통한 PV 사용 minio: persistence: enabled: true size: 10Gi storageClass: longhorn 불필요한 기능 비활성화 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 global: lfs: enabled: false uploads: enabled: false toolbox: enabled: false # GitLab KAS 비활성화 (Kubernetes Agent) global: kas: enabled: false # CI/CD 관련 비활성화 global: appConfig: artifacts: enabled: false packages: enabled: false # 모니터링 비활성화 prometheus: install: false # GitLab Runner 비활성화 gitlab-runner: install: false 도메인, 에디션(CE/EE) 1 2 3 4 global: edition: ce hosts: domain: \u0026lt;도메인\u0026gt; http 접근을 위한 설정 1 2 3 4 5 6 7 8 global: hosts: https: false ingress: configureCertmanager: false tls: enabled: false installCertmanager: false GitLab UI GitLab Helm Chart를 통해 ingress는 생성 완료!\nvalues.yaml 내에 ingress 생성을 위한 nginx 설정이 포함되어 있음\nHAproxy 설정 ingress를 조회하여 MetalLB로부터 부여받은 IP 확인\n현재는 172.27.1.102 1 2 3 4 # kubectl get ingress -n gitlab NAME CLASS HOSTS ADDRESS PORTS AGE gitlab-registry gitlab-nginx registry.\u0026lt;도메인\u0026gt; 172.27.1.102 80 61m gitlab-webservice-default gitlab-nginx gitlab.\u0026lt;도메인\u0026gt; 172.27.1.102 80 61m 확인한 IP를 HAproxy 설정에 추가\nHAproxy 서버 공인 IP로 접근 시 gitlab의 Ingress로 통신되도록 설정 http-response replace-value Location ^http://gitlab\\.\u0026lt;도메인\u0026gt;/(.*)$ http://gitlab.\u0026lt;도메인\u0026gt;:\u0026lt;포트\u0026gt;/\\\\1 두 조건을 만족할 경우 설정 필요 외부 브라우저에서 GitLab 접근 시 포트를 붙여서 접근해야 될 경우 애플리케이션 내부적으로 url로 응답이 나오도록 설정되어 있을 경우 EX) GitLab 로그인 시 GitLab에 설정된 url 응답 설정하지 않으면 접근 시 자동으로 포트가 제거된 상태로 리다이렉션되어 404 에러 1 2 3 vim /etc/haproxy/haproxy.cfg haproxy -c -f /etc/haproxy/haproxy.cfg systemctl reload haproxy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 frontend unified_frontend_8080 bind *:8080 mode http option forwardfor http-request set-header X-Forwarded-Host %[req.hdr(host)] http-request set-header X-Forwarded-Proto http http-request set-header X-Forwarded-Port %[dst_port] # Host 기반 라우팅 use_backend metallb_backend_gitlab if { hdr(host) -m sub gitlab } # GitLab Container Registry를 위한 경로 use_backend metallb_backend_gitlab if { hdr(host) -m sub registry } backend metallb_backend_gitlab http-response replace-value Location ^http://gitlab\\.\u0026lt;도메인\u0026gt;/(.*)$ http://gitlab.\u0026lt;도메인\u0026gt;:\u0026lt;포트\u0026gt;/\\\\1 server gitlab 172.27.1.100:80 접근 성공! gitlab 관리자(root) 비밀번호 확인\n1 kubectl get secret gitlab-gitlab-initial-root-password -n gitlab -o jsonpath=\u0026#34;{.data.password}\u0026#34; | base64 --decode ","date":"2025-09-24T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EC%9E%90%EC%B2%B4-%EA%B4%80%EB%A6%AC%ED%98%95-kubernetes%EC%97%90%EC%84%9C%EC%9D%98-gitlab-%EA%B5%AC%EC%B6%95/","title":"자체 관리형 Kubernetes에서의 GitLab 구축"},{"content":" kubernetes에서 kubectl 같이 클라이언트나 파드 내부에서 API 서버에 접근할 때는 인증과 권한이 필요\n접근 제어 User/Group → 인증\n인증서는 클라이언트 신원을 확인하는 데 사용 ex) kubectl get nodes 실행 시 사용자 인증 필요 인증서(certificate)를 kubeconfig에 등록하여 사용 ServiceAccount → 권한\n파드 내부에서 쿠버네티스 API를 호출할 때 사용 Pod 실행 시 자동으로 토큰이 /var/run/secrets/kubernetes.io/serviceaccount/token 에 마운트 파드 내부에서 자동으로 마운트되거나 Secret을 통해 주입 사용자 생성 절차 개인키 생성\nCSR 생성\nCSR 요청 및 승인\n1 2 kubectl apply -f csr.yaml kubectl certificate approve myuser-csr Role/RoleBinding 생성\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: namespace: default name: pod-reader rules: - apiGroups: [\u0026#34;\u0026#34;] resources: [\u0026#34;pods\u0026#34;] verbs: [\u0026#34;get\u0026#34;, \u0026#34;list\u0026#34;] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: read-pods namespace: default subjects: - kind: User name: myuser roleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.io kubeconfig에 사용자 추가\n1 kubectl config set-credentials myuser --client-certificate=user.crt --client-key=user.key context 변경\n1 2 kubectl config set-context myuser-context --cluster=mycluster --namespace=default --user=myuser kubectl config use-context myuser-context 서비스 어카운트 생성 절차 서비스 어카운트 생성\n1 kubectl create serviceaccount mysa -n default Rolebinding 생성\n1 2 3 4 5 6 7 8 9 10 11 12 13 kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: sa-read-pods namespace: default subjects: - kind: ServiceAccount name: mysa namespace: default roleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.io pod 실행시 서비스 어카운트 지정\n1 2 3 4 5 6 7 8 9 10 apiVersion: v1 kind: Pod metadata: name: test-pod spec: serviceAccountName: mysa containers: - name: curl image: curlimages/curl command: [\u0026#34;sleep\u0026#34;, \u0026#34;3600\u0026#34;] 권한 관리(RBAC) Role 네임스페이스 단위 권한 ex) default namespace에서 pod 읽기 RoleBinding Role을 특정 User/Group/ServiceAccount에 바인딩 ClusterRole 클러스터 전체 권한 ex) 모든 namespace에서 pod 읽기 ClusterRoleBinding ClusterRole을 User/Group/ServiceAccount에 바인딩 ","date":"2025-09-23T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-%EC%9D%B8%EC%A6%9D-/-%EA%B6%8C%ED%95%9C-%EA%B4%80%EB%A6%AC-overview/","title":"Kubernetes 인증 / 권한 관리 Overview"},{"content":" kubectl top node node1과 같이 즉각적인 매트릭 자원 확인이 가능하도록 설정\nGitHub: https://github.com/kubernetes-sigs/metrics-server?tab=readme-ov-file Artifacthub: https://artifacthub.io/packages/helm/metrics-server/metrics-server helm 리포지토리 추가 1 2 helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/ helm upgrade values.yaml 수정 1 2 helm show values metrics-server/metrics-server \u0026gt; metrics-server-values.yaml vim metrics-server-values.yaml metrics server가 tls 인증서를 검증하지 않도록 설정 1 2 args: - --kubelet-insecure-tls metrics server 설치 1 helm upgrade --install metrics-server metrics-server/metrics-server -f metrics-server-values.yaml ","date":"2025-09-22T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EC%9E%90%EC%B2%B4-%EA%B4%80%EB%A6%AC%ED%98%95-kubernetes%EC%97%90%EC%84%9C-metrics-server%EB%A5%BC-%ED%86%B5%ED%95%9C-%EB%A7%A4%ED%8A%B8%EB%A6%AD-%EC%9E%90%EC%9B%90-%ED%99%95%EC%9D%B8/","title":"자체 관리형 Kubernetes에서 Metrics Server를 통한 매트릭 자원 확인"},{"content":" k8s scheduler가 어떤 기준으로 파드를 노드에 배치하는지에 대한 정리\nNode Affinity 지정된 조건에 맞는 노드에 파드를 배포\ndisktype=ssd label이 있는 노드에만 스케줄링 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 apiVersion: v1 kind: Pod metadata: name: web-pod spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disktype operator: In values: - ssd containers: - name: web image: nginx Pod Affinity pod를 가까이 배치(ex: 같은 노드에 배치)\napp=db label이 붙은 파드와 같은 노드에 배치 반대로 podAntiAffinity를 쓰면 다른 노드에 배치 가능 1 2 3 4 5 6 7 8 9 podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - db topologyKey: \u0026#34;kubernetes.io/hostname\u0026#34; Taint \u0026amp; Toleration nodeselector: 노드레이블이 지정된 노드에 파드를 배포할 때 사용\nnode taint: 노드에 파드를 배포되지 않도록 할 때 사용\nnode taint 추가 명령어 1 kubectl taint nodes node1 key=value:NoSchedule pod toleration: node taint가 설정된 노드에 배포하고 싶을 때 사용\npod 매니페스트 내 toleration 설정 이 파드는 위 node1에 배치 가능 1 2 3 4 5 tolerations: - key: \u0026#34;key\u0026#34; operator: \u0026#34;Equal\u0026#34; value: \u0026#34;value\u0026#34; effect: \u0026#34;NoSchedule\u0026#34; Cordon/Uncordon \u0026amp; Drain cordon: 지정된 노드에 파드 배포 금지\n1 kubectl cordon node1 uncordon: 지정된 노드에 파드 배포 금지 해제\n1 kubectl uncordon node1 drain: 지정된 노드에 모든 파드 삭제\n1 kubectl drain node1 --ignore-daemonsets ","date":"2025-09-19T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-pod-scheduling-overview/","title":"Kubernetes Pod Scheduling Overview"},{"content":" Jenkins 파이프라인이 K8S pod로 사용되도록 설정\nHelm으로 설치 시에는 하기 설정들이 자동으로 세팅되어 있으니 설정 불필요\nJenkins에 Kubernetes 플러그인 설치 확인\nJenkins가 K8S API와 통신해서 pod(파이프라인 에이전트)를 생성/삭제 가능하도록 설정 Built-In-Node excutor 설정\n파이프라인 에이전트를 k8s pod에서 동적으로 실행할거라 built-in-node(jenkins 서버 pod)의 Number of executors을 0으로 설정 K8S Configure\nManage Jenkins \u0026gt; Clouds \u0026gt; New Cloud / Configure K8S URL로 Test Connection 확인 Jenkins URL/Tunnel Jenkins URL: http://jenkins.jenkins.svc.cluster.local:8080 Jenkins tunnel: jenkins-agent.jenkins.svc.cluster.local:50000 jenkins tunnel은 jnlp(파이프라인 에이전트 관리 pod)로 연결되는 FQDN 입력 jnlp 포트가 50000 url, tunnel에 도메인을 넣으면 IP가 바뀌더라도 연결 가능 K8S FQDN: 클러스터 DNS 규칙에 의해 만들어짐\nFQDN 룰: \u0026lt;Service명\u0026gt;.\u0026lt;Namespace명\u0026gt;.svc. cluster domain 기본값은 cluster.local FQDN 예시\njenkins FQDN: jenkins.jenkins.svc.cluster.local jenkins-agent FQDN: jenkins-agent.jenkins.svc.cluster.local 1 2 3 4 # kubectl get svc -n jenkins NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE jenkins ClusterIP 10.233.54.119 \u0026lt;none\u0026gt; 8080/TCP 11d jenkins-agent ClusterIP 10.233.57.212 \u0026lt;none\u0026gt; 50000/TCP 11d Pod Template\n파이프라인 에이전트가 K8S에서 Pod로 관리될 때의 Pod 설정을 정의\n","date":"2025-09-17T09:00:00+09:00","permalink":"https://joonlog.github.io/p/jenkins-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8%EC%9D%84-k8s-pod%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%9C-%EC%84%A4%EC%A0%95/","title":"Jenkins 파이프라인을 K8S Pod로 사용하기 위한 설정"},{"content":" 두 서버의 HAProxy를 동시에 재기동하는 과정에서 VIP FAULT 현상이 발생\nkeepalived/corosync/pacemaker 같은 클러스터 프로세스가 VIP 소유권을 제어하는 앱에서 자주 있는 일 VIP(Virtual IP) 소유권 충돌이 발생하여 이로 인해 VIP가 정상적으로 할당되지 않고 모두 FAULT 상태로 전환 이후 BACKUP 상태로 변경됨 이후 순차적으로 keepalived를 재기동해서 정상적으로 Master 상태로 전환 확인 systemctl restart keepalived 위 현상 방지하기 위해 한 HAproZxy 재기동 후, 재기동하지 않은 서버가 MASTER 승격되었는지, VIP 활성화 되었는지 확인 필수\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # HAProxy 재기동 시 로그 Reloading HAProxy Load Balancer... Stopping proxy haproxy-ssl... Stopping proxy haproxy... Keepalived_vrrp[####]: (VI_1) received an invalid passwd! Keepalived_vrrp[####]: (VI_1) Dropping received VRRP packet... Keepalived_vrrp[####]: VRRP_Instance(VI_1) Entering FAULT STATE Keepalived_vrrp[####]: VRRP_Script(chk_haproxy) failed Keepalived_vrrp[####]: VRRP_Instance(VI_1) Now in FAULT state Keepalived_vrrp[####]: VRRP_Instance(VI_1) Entering BACKUP STATE # haproxy_test.sh 실행 결과 /keepalived/haproxy_test.sh exited with status 1 /keepalived/haproxy_test.sh exited with status 0 # BACKUP 전환 확인 Keepalived_vrrp[####]: VRRP_Instance(VI_1) Entering BACKUP STATE ","date":"2025-09-16T09:00:00+09:00","permalink":"https://joonlog.github.io/p/haproxy-%EB%8F%99%EC%8B%9C-%EC%9E%AC%EA%B8%B0%EB%8F%99-%EC%98%A4%EB%A5%98-troubleshooting/","title":"HAproxy 동시 재기동 오류 TroubleShooting"},{"content":" 흔히 Kubernetes 환경에서 매트릭 모니터링으로 Prometheus를 많이 사용한다는데, 왜 많이 쓰는지, 정확한 이유를 알아보고자 작성한 글입니다.\nK8S에서 Prometheus를 쓰는 이유 Prometheus 구축을 kube-prometheus-stack Helm Chart로만 구축해봤다 보니 처음에는 Helm에 그 이유가 있을 줄 알았는데, 알고 보니 Prometheus 자체가 K8S와의 호환성이 좋았다. Helm은 Prometheus를 구축하기 편하게 Chart 형태로만 패키징 해놓은 것. 이 Helm Chart의 베이스 엔진인 Prometheus-Operator도 이유 중 하나로, CRD(podmonitor, servicemonitor)로 promtheus를 관리할 수 있다.\n결론적으로, 크게 아래 두 이유 때문에 K8S에서 Prometheus를 사용한다고 볼 수 있다.\nK8S 환경에서의 호환성 K8S 환경에서의 관리 용이성 K8S 환경에서의 Prometheus 호환성 Prometheus는 K8S 서비스 디스커버리를 지원\n내장된 서비스 디스커버리로 인해 K8S API(서비스/파드/노드/인그레스 등)가 스케일인/아웃, 신규 네임스페이스 추가되는 등등의 상황에서도 동적으로 탐색 가능\n서비스 디스커버리: 엔드포인트를 미리 하드코딩하지 않고, 런타임에 자동으로 찾아 쓰는 패턴 K8S에서는 kubelet 프로세스가 cAdvisor를 통해서 지표를 수집하는데, 이 데이터를 Prometheus 서버가 읽어서 신규 서비스/파드 추가 시 모니터링 대상으로 자동으로 편입 가능\nprometheus.yml에서 kubelet의 엔트포인트인 /metrics, /metrics/cadvisor를 스크랩하도록 설정할 경우에 Prometheus가 자동으로 읽음 바닐라 prometheus를 사용할 경우의 prometheus.yml 예시 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 scrape_configs: - job_name: kubelet scheme: https metrics_path: /metrics kubernetes_sd_configs: - role: node bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token tls_config: ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt relabel_configs: - source_labels: [__meta_kubernetes_node_address_InternalIP] target_label: __address__ replacement: $1:10250 - job_name: kubelet-cadvisor scheme: https metrics_path: /metrics/cadvisor kubernetes_sd_configs: - role: node bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token tls_config: ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt relabel_configs: - source_labels: [__meta_kubernetes_node_address_InternalIP] target_label: __address__ replacement: $1:10250 K8S 환경에서의 Prometheus 관리 용이성 Prometheus Operator: https://github.com/prometheus-operator/prometheus-operator Prometheus Operator는 Prometheus를 K8S에서 사용하기 편하게 패키징한 툴 CRD 기반의 선언형으로 Prometheus 관리 가능 CRD 예: ServiceMonitor, PodMonitor, PrometheusRule 등의 커스텀 리소스 CRD에서의 Label Selector, Namespace Selector 설정으로 신규 파드 추가 시 자동 모니터링 편입 kubelet을 ServiceMonitor로 생성할 경우 kubelet의 cadvisor가 수집한 지표를 이 kubelet CRD로 인해 Prometheus 서버가 읽고, 신규 서비스/파드 추가 시 모니터링 대상으로 자동으로 편입 가능 Prometheus CRD로 Alerting 룰 분리 kube-prometheus-stack prometheus operator부터 grafana까지 모니터링에 필요한 올인원 Helm 패키지\n구조\n1 2 3 4 5 6 7 8 kube-prometheus-stack ├── Prometheus Operator (핵심 엔진) ├── Prometheus 인스턴스 ├── Grafana (시각화 대시보드) ├── AlertManager (알람 관리) ├── Node Exporter (Node 매트릭 수집) ├── kube-state-metrics (K8S 리소스 매트릭 수집) └── 기본 ServiceMonitors/PrometheusRules (정책) 기본 생성 servicemonitor 목록\n아래 리스트가 전부 생성되므로 사실상 kube-prometheus-stack을 설치하는 것만으로 기본 매트릭 모니터링은 끝 1 2 3 4 5 6 7 8 9 10 11 12 13 prometheus-grafana prometheus-kube-prometheus-alertmanager prometheus-kube-prometheus-apiserver prometheus-kube-prometheus-coredns prometheus-kube-prometheus-kube-controller-manager prometheus-kube-prometheus-kube-etcd prometheus-kube-prometheus-kube-proxy prometheus-kube-prometheus-kube-scheduler prometheus-kube-prometheus-kubelet prometheus-kube-prometheus-operator prometheus-kube-prometheus-prometheus prometheus-kube-state-metrics prometheus-prometheus-node-exporter ","date":"2025-09-12T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-prometheus%EB%A5%BC-%EB%A7%8E%EC%9D%B4-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0/","title":"Kubernetes 환경에서 Prometheus를 많이 사용하는 이유"},{"content":" 매니지드 K8S가 아닌 자체 관리형 K8S에서의 Prometheus + Grafana 구축 방법\nUI 접근을 위한 외부 통신으로 MetalLB+HAproxy+Nginx Controller가 구성 되었다는 전제 하에 구축\nCSP 환경이라면 Ingress 설정에서 AWS ALB Controller와 같은 로드밸런서를 사용해 훨씬 간편하게 구축 가능\nPrometheus 공식문서: https://prometheus.io/docs/prometheus/latest/installation/ Grafana 공식문서: https://grafana.com/docs/grafana/latest/setup-grafana/installation/ 개요 kube-prometheus-stack helm chart 구조\n1 2 3 4 5 6 7 8 kube-prometheus-stack ├── Prometheus Operator (핵심 엔진) ├── Prometheus 인스턴스 ├── Grafana (시각화 대시보드) ├── AlertManager (알람 관리) ├── Node Exporter (Node 매트릭 수집) ├── kube-state-metrics (K8S 리소스 매트릭 수집) └── 기본 ServiceMonitors/PrometheusRules (정책) Prometheus Operator란\nCRD 기반 Prometheus 관리 도구 Prometheus, ServiceMonitor, PrometheusRule 등의 Custom Resource 제공 선언적 방식으로 Prometheus 설정 관리 Prometheus + Grafana 설치 깃허브: https://github.com/prometheus-community/helm-charts helm 리포지토리 추가\n1 2 3 helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo add grafana https://grafana.github.io/helm-charts helm repo update kube-prometheus-stack 설치\nprometheus + grafana 합쳐진 패키지 values.yaml 수정 1 helm show values prometheus-community/kube-prometheus-stack \u0026gt; values.yaml grafana pvc 설정 1 2 3 4 5 6 7 persistence: enabled: true type: sts storageClassName: \u0026#34;longhorn\u0026#34; accessModes: - ReadWriteOnce size: 5Gi prometheus pvc 설정 selector: {} 삭제 longhorn은 selector 지원하지 않음 1 2 3 4 5 6 7 8 storageSpec: volumeClaimTemplate: spec: storageClassName: longhorn accessModes: [\u0026#34;ReadWriteOnce\u0026#34;] resources: requests: storage: 40Gi alertmanager pvc 설정 selector: {} 삭제 longhorn은 selector 지원하지 않음 1 2 3 4 5 6 7 8 storage: volumeClaimTemplate: spec: storageClassName: longhorn accessModes: [\u0026#34;ReadWriteOnce\u0026#34;] resources: requests: storage: 2Gi kube-prometheus-stack 설치 1 helm install prometheus prometheus-community/kube-prometheus-stack -f values.yaml --namespace monitoring --create-namespace Grafana UI 접근 ingress 생성\n8082포트를 사용하는 grafana service로 연결 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 cat \u0026lt;\u0026lt;EOF \u0026gt; grafana-ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: grafana-ingress namespace: monitoring annotations: nginx.ingress.kubernetes.io/backend-protocol: \u0026#34;HTTP\u0026#34; nginx.ingress.kubernetes.io/proxy-body-size: \u0026#34;50m\u0026#34; spec: ingressClassName: nginx rules: - host: \u0026#34;grafana.\u0026lt;도메인\u0026gt;\u0026#34; http: paths: - path: / pathType: Prefix backend: service: name: prometheus-grafana port: number: 80 EOF kubectl -n monitoring apply -f grafana-ingress.yaml HAproxy 설정\ningress를 조회하여 MetalLB로부터 부여받은 IP 확인\n현재는 172.27.1.100 1 2 3 4 # kubectl get ingress -n monitoring # kubectl get ingress -n monitoring NAME CLASS HOSTS ADDRESS PORTS AGE grafana-ingress nginx grafana.\u0026lt;도메인\u0026gt; 172.27.1.100 80 9s 확인한 IP를 HAproxy 설정에 추가\nHAproxy 서버 공인 IP로 접근 시 grafana의 Ingress로 통신되도록 설정 1 2 3 vim /etc/haproxy/haproxy.cfg haproxy -c -f /etc/haproxy/haproxy.cfg systemctl reload haproxy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 frontend unified_frontend_8080 bind *:8080 mode http option forwardfor http-request set-header X-Forwarded-Host %[req.hdr(host)] http-request set-header X-Forwarded-Proto http http-request set-header X-Forwarded-Port %[dst_port] # Host 기반 라우팅 use_backend metallb_backend_grafana if { hdr(host) -m sub grafana } backend metallb_backend_grafana server grafana 172.27.1.100:80 접근 성공!\nGrafana 관리자 비밀번호 확인\n1 kubectl --namespace monitoring get secrets prometheus-grafana -o jsonpath=\u0026#34;{.data.admin-password}\u0026#34; | base64 -d ; echo 대시보드 그래프 origin not allowed 에러 발생 시 IP주소로의 직접 접근을 Grafana의 CSRF 정책이 요청 차단 도메인을 통해 접근해서 CSRF 정책 통과\ningrass, haproxy에 설정되어있던 grafana.local을 새로 설정한 도메인으로 변경 values.yaml 수정 1 2 3 4 5 6 7 8 9 10 11 12 13 grafana: ... adminPassword: \u0026lt;GRAFANA_PASSWORD\u0026gt; grafana.ini: server: domain: \u0026lt;도메인\u0026gt;:48082 root_url: http://\u0026lt;도메인\u0026gt;:48082 security: csrf_trusted_origins: \u0026#34;\u0026lt;도메인\u0026gt;:48082\u0026#34; csrf_cookies_secure: false cookie_secure: false cookie_samesite: lax ... 설정 반영\n1 2 3 4 helm uninstall prometheus --namespace monitoring kubectl --namespace monitoring delete pvc --all helm install prometheus prometheus-community/kube-prometheus-stack -f values.yaml --namespace monitoring Prometheus UI 접근 ingress 생성\n8080포트를 사용하는 prometheus service로 연결 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 cat \u0026lt;\u0026lt;EOF \u0026gt; prometheus-ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: prometheus-ingress namespace: monitoring annotations: nginx.ingress.kubernetes.io/backend-protocol: \u0026#34;HTTP\u0026#34; nginx.ingress.kubernetes.io/proxy-body-size: \u0026#34;50m\u0026#34; spec: ingressClassName: nginx rules: - host: \u0026#34;prometheus.local\u0026#34; http: paths: - path: / pathType: Prefix backend: service: name: prometheus-kube-prometheus-prometheus port: number: 9090 EOF kubectl -n monitoring apply -f prometheus-ingress.yaml HAproxy 설정\ningress를 조회하여 MetalLB로부터 부여받은 IP 확인\n현재는 172.27.1.100 1 2 3 # kubectl get ingress -n monitoring NAME CLASS HOSTS ADDRESS PORTS AGE prometheus-ingress nginx prometheus.\u0026lt;도메인\u0026gt; 172.27.1.100 80 9s 확인한 IP를 HAproxy 설정에 추가\nHAproxy 서버 공인 IP로 접근 시 prometheus의 Ingress로 통신되도록 설정 1 2 3 vim /etc/haproxy/haproxy.cfg haproxy -c -f /etc/haproxy/haproxy.cfg systemctl reload haproxy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 frontend unified_frontend_8080 bind *:8080 mode http option forwardfor http-request set-header X-Forwarded-Host %[req.hdr(host)] http-request set-header X-Forwarded-Proto http http-request set-header X-Forwarded-Port %[dst_port] # Host 기반 라우팅 use_backend metallb_backend_prometheus if { hdr(host) -m sub prometheus } backend metallb_backend_prometheus server prometheus 172.27.1.100:80 접근 성공!\n","date":"2025-09-12T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EC%9E%90%EC%B2%B4-%EA%B4%80%EB%A6%AC%ED%98%95-kubernetes%EC%97%90%EC%84%9C%EC%9D%98-prometheus%EC%99%80-grafana-%EA%B5%AC%EC%B6%95/","title":"자체 관리형 Kubernetes에서의 Prometheus와 Grafana 구축"},{"content":" https://joonlog.github.io/posts/TroubleShootingHikariCPConnectionError/에서 이슈가 됐던 설정 중 최적 커넥션 풀 설정 방법에 대한 글 발견\n출처: https://www.threads.com/@codevillains/post/DFj2-wQSREJ?xmt=AQF0jSXFspf2rIJ-PoZij1A2Xy3Ak_g0vjc0SNxC2kYoOA\n원본: https://github.com/brettwooldridge/HikariCP\n서버 스펙으로 계산 Pool Size = (( CPU 코어 수 * 2) + 스토리지 디스크 (또는 SSD) 갯수) 코어가 8개고 스토리지 디스크가 2개라면 ((8*2)+2) = 18 가 최적의 커넥션 풀 사이즈 TPS로 계산 (TPS × 평균 응답 시간) / (1 - DB 사용률) 500TPS에 평균 응답시간이 0.05초 일 경우 서버 자원 사용률이 70퍼라고 가정하면 500 * 0.05 / 1-0.7 이 되고, 83.3 개가 최적의 커넥션 풀 사이즈 ","date":"2025-09-11T09:00:00+09:00","permalink":"https://joonlog.github.io/p/hikaricp-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%A0%9C%EC%95%88%ED%95%9C-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80-%EC%82%AC%EC%9D%B4%EC%A6%88-%EA%B3%B5%EC%8B%9D/","title":"HikariCP 개발자가 제안한 커넥션 풀 사이즈 공식"},{"content":" MySQL은 wait_timeout 설정값에 따라 커넥션 중 일정 시간이상 사용하지 않은 커넥션을 종료 HikariCP는 maxLifetime 설정값에 따라 스스로 미사용된 커넥션을 제거하고 새로 생성하는 방식으로 동작 커넥션 누수 발생 시 connection closed 되지 않으므로 Hikari는 커넥션을 activate 상태로 파악하여 maxLifetime 에 따른 커넥션 갱신하지 않음 따라서 wait_timeout 으로 인해 닫힌 커넥션을 재사용하려다 connection is closed 오류가 발생 HikariCP는 maxLifetime값을 DB의 wait_timeout보다 몇 초 정도 짧게 설정하라고 권장 connection is closed 에러 히스토리 connection pool 수치 점검\ndefault 10개, 필요시 20~30개로 설정 1 spring.datasource.hikari.maximum-pool-size=30 현재 실행 중인 쿼리의 Que 값이 connection Pool 최대 값까지 도달하여 다음 실행할 쿼리를 처리하지 못해 발생 APM 솔루션으로 Connection Pool 자원 모니터링을 통해 증설이 필요할 시 즉시 증설, 쿼리 최적화 필요 언급 이건 임시 조치만 반복할 뿐, 커넥션 누수는 코드를 수정해야 함 db 파라미터 wait_timeout = 500 설정\n이후 재발\n1 2 3 4 5 6 7 spring.datasource.hikari.minimum-idle=10 // 최소 커넥션 수 spring.datasource.hikari.idle-timeout=300000 // 유휴 커넥션 타임아웃 (5분) spring.datasource.hikari.max-lifetime=1800000 // 최대 커넥션 생명 주기 (30분) spring.datasource.hikari.connection-timeout=30000 // 커넥션 대기 시간 (30초) # db 파라미터 max_connection = 2000으로 확인됨 spring.datasource.hikari.maximum-pool-size=60 // 30에서 60으로 변경 이후 재발\nHikariCP는 maxLifetime값을 DB의 wait_timeout보다 몇 초 정도 짧게 설정하라고 권장\n1 spring.datasource.hikari.max-lifetime=497000 // 최대 커넥션 생명 주기 (497초) 이후 재발\nwas 서버 로그 확인 시 maxLifetime 값을 더 짧게 설정하는 것을 권장하는 로그 확인\n1 [WARN ] [PoolBase.javalisConnectionAlive(184) : HikariPool-1 - Failed to connection org.mariadb.jdbc.MariaDbConnection@14441ec9 ((conn=8360) Connection.setNetworkTimeout cannot be called on a closed connection). Possibly consider using a shorter maxLifetime value. maxLifetime 값 변경\n1 spring.datasource.hikari.max-lifetime=470000 // 최대 커넥션 생명 주기 (470초) 동일오류 재발\nspring.datasource.hikari.connection-init-sql 프로퍼티를 추가하여 세션 wait_timeout을 설정\nwait_timeout 값을 설정된 hikari MaxLifetime 값 + 5초 형식으로 설정\n1 spring.datasource.hikari.connection-init-sql=SET wait_timeout = 500 커넥션 누수 사례 https://jaehoney.tistory.com/337\nwait_timeout 이 15초로 설정되어 있는데 레거시 코드 때문에 높이지 못함\n따라서 세션의 wait_timeout을 설정\n1 spring.datasource.hikari.connection-init-sql=SET wait_timeout = 500 https://do-study.tistory.com/97\nEntityManager를 Bean으로 설정함 EntityManager는 스레드간 절대 공유하면 안됨 QueryDSL 구현 Resitory에서 특정 datasource의 EntityManager를 받기 위해 setEntityManager를 재정의하지 않고 별도 setting 메소드 작성 ","date":"2025-09-10T09:00:00+09:00","permalink":"https://joonlog.github.io/p/hikaricp-connection-is-closed-%EC%97%90%EB%9F%AC-troubleshooting/","title":"HikariCP connection is closed 에러 TroubleShooting"},{"content":" 매니지드 K8S가 아닌 자체 관리형 K8S에서의 Longhorn 구축 방법\nUI 접근을 위한 외부 통신으로 MetalLB+HAproxy+Nginx Controller가 구성 되었다는 전제 하에 구축\nCSP 환경이라면 Ingress 설정에서 AWS ALB Controller와 같은 로드밸런서를 사용해 훨씬 간편하게 구축 가능\n개요 자체 관리형 K8S에서 애플리케이션의 데이터를 고가용성으로 운영하려면 Ceph, Longhorn 같은 분산 블록 스토리지 툴이 필요 노드마다 데이터를 복제해서 저장하는 분산 블록 스토리지 Ceph, Longhorn 같은 툴이 없다면 파드는 워커노드의 로컬 경로에만 데이터를 저장해야 하는데, 이러면 워커노드 장애가 발생할 경우 데이터 가용성 보장이 안됨 NFS를 StorageClass로 사용할 수 도 있지만, NFS는 단일 서버, 단일 네트워크 I/O에 의존하기 때문에, 성능과 장애 내구성이 떨어져 PV 데이터 저장 용도로는 부적합 Ceph 퍼포먼스가 더 뛰어나지만 리소스 자원을 꽤 점유하는 툴이므로, 현재 환경에서는 Longhorn을 사용 워커 노드가 5대 이상, 8vCPU 16GB 이상인 스펙이어도 Longhorn으로 충분히 운영 가능 Longhorn을 쓰면 PVC를 생성하는 것 만으로도 PV가 자동으로 프로비저닝됨!!!\nAWS라면 EBS같은 스토리지를 통해 고가용성 확보 가능 ALB Controller와 마찬가지로 CSP 관리형 K8S를 쓰는 또하나의 이유 구조 PVC가 생성되면 Longhorn Volume을 관리하는 Engine Pod가 생성되고, 하위에 Replica Pod들이 각 노드에 생성됨 Replica Pod는 해당 노드의 특정 경로에 전체 데이터 replica를 저장 쓰기 I/O는 Engine Pod가 받아서 모든 Replica Pod에 동기 복제한 뒤 성공을 리턴 Pod가 노드1에서 노드2로 옮겨지면 Longhorn이 볼륨을 노드1에서 detatch 했다가 노드2로 attatch 이때 노드2에 Replica가 있으면 그대로 사용, 없으면 다른 Replica에서 동기받아서 새 Replica 생성 Longhorn 설치 Longhorn 공식 문서: https://longhorn.io/docs/1.9.1/deploy/install/install-with-helm/ https://longhorn.io/docs/1.9.1/advanced-resources/deploy/customizing-default-settings/#using-helm helm 리포지토리 추가s\n1 2 helm repo add longhorn https://charts.longhorn.io helm repo update values.yaml 수정\n1 2 3 helm pull longhorn/longhorn --untar cd longhorn vim values.yaml 1 2 3 4 5 6 7 8 persistence: ... defaultClassReplicaCount: 2 defaultSettings: ... defaultDataPath: /data defaultReplicaCount: 2 helm 설치\n1 2 3 4 5 helm install longhorn longhorn/longhorn \\ --namespace longhorn-system \\ --create-namespace \\ --values values.yaml kubectl -n longhorn-system get pod Longhorn UI 접속 https://longhorn.io/docs/1.9.1/deploy/accessing-the-ui/ auth 파일 생성\n반드시 auth라는 이름으로 생성 필요 1 USER=\u0026lt;USERNAME_HERE\u0026gt;; PASSWORD=\u0026lt;PASSWORD_HERE\u0026gt;; echo \u0026#34;${USER}:$(openssl passwd -stdin -apr1 \u0026lt;\u0026lt;\u0026lt; ${PASSWORD})\u0026#34; \u0026gt;\u0026gt; auth secret 생성\n1 kubectl -n longhorn-system create secret generic basic-auth --from-file=auth ingress 생성\n80포트를 사용하는 longhorn service로 연결\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 cat \u0026lt;\u0026lt;EOF \u0026gt; longhorn-ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: longhorn-ingress namespace: longhorn-system annotations: # type of authentication nginx.ingress.kubernetes.io/auth-type: basic # prevent the controller from redirecting (308) to HTTPS nginx.ingress.kubernetes.io/ssl-redirect: \u0026#39;false\u0026#39; # name of the secret that contains the user/password definitions nginx.ingress.kubernetes.io/auth-secret: basic-auth # message to display with an appropriate context why the authentication is required nginx.ingress.kubernetes.io/auth-realm: \u0026#39;Authentication Required \u0026#39; # custom max body size for file uploading like backing image uploading nginx.ingress.kubernetes.io/proxy-body-size: 10000m spec: ingressClassName: nginx rules: - host: \u0026#34;longhorn.local\u0026#34; http: paths: - path: / pathType: Prefix backend: service: name: longhorn-frontend port: number: 80 EOF kubectl -n longhorn-system apply -f longhorn-ingress.yaml HAproxy 설정 ingress를 조회하여 MetalLB로부터 부여받은 IP 확인\n현재는 172.27.1.100 1 2 3 # kubectl get ingress -n longhorn-system NAME CLASS HOSTS ADDRESS PORTS AGE longhorn-ingress nginx longhorn.local 172.27.1.100 80 8s 확인한 IP를 HAproxy 설정에 추가\nHAproxy 서버 공인 IP로 접근 시 Longhorn의 Ingress로 통신되도록 설정 1 2 3 vim /etc/haproxy/haproxy.cfg haproxy -c -f /etc/haproxy/haproxy.cfg systemctl reload haproxy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 frontend unified_frontend_8080 bind *:8080 mode http option forwardfor http-request set-header X-Forwarded-Host %[req.hdr(host)] http-request set-header X-Forwarded-Proto http http-request set-header X-Forwarded-Port %[dst_port] # Host 기반 라우팅 use_backend metallb_backend_longhorn if { hdr(host) -m sub longhorn } backend metallb_backend_longhorn server longhorn 172.27.1.100:80 접근 성공!\n","date":"2025-09-07T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EC%9E%90%EC%B2%B4-%EA%B4%80%EB%A6%AC%ED%98%95-kubernetes%EC%97%90%EC%84%9C%EC%9D%98-%EB%B6%84%EC%82%B0-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80-longhorn-%EA%B5%AC%EC%B6%95/","title":"자체 관리형 Kubernetes에서의 분산 스토리지 Longhorn 구축"},{"content":" containerd만 단독으로 설치 했을 경우 CRI의 기능만 제공하며, docker처럼 기능을 사용하기 위해선 플러그인 설치가 필요\nhttps://containerd.io/downloads/ https://medium.com/@DannielWhatever/using-containerd-without-docker-9d08332781b4 containerd 설치\n1 2 3 4 for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo dnf remove $pkg -y; done dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo dnf install -y containerd.io containerd config default | sudo tee /etc/containerd/config.toml containerd 디렉토리 설정(선택)\n1 2 3 4 5 6 7 8 ### containerd root 디렉토리 외에 data 디렉토리에서 동작하도록 설정 sed -i \u0026#39;s|^root = .*|root = \u0026#34;/data/container/containerd\u0026#34;|\u0026#39; /etc/containerd/config.toml sed -i \u0026#39;s|^state = .*|state = \u0026#34;/data/container/containerd-state\u0026#34;|\u0026#39; /etc/containerd/config.toml mkdir -p /data/container/containerd mkdir -p /data/container/containerd-state mkdir -p /data/container/nerdctl echo \u0026#39;export NERDCTL_DATA_ROOT=/data/container/nerdctl\u0026#39; \u0026gt;\u0026gt; ~/.bashrc source ~/.bashrc nerdctl 설치\n도커 명령어처럼 containerd를 관리하기 위한 툴 ex) nerdctl ps -a, nerdctl images 1 2 3 4 # nerdctl 설치 wget https://github.com/containerd/nerdctl/releases/download/v0.17.0/nerdctl-0.17.0-linux-amd64.tar.gz tar Cxzvvf /usr/bin nerdctl-0.17.0-linux-amd64.tar.gz echo \u0026#34;source \u0026lt;(nerdctl completion bash)\u0026#34; \u0026gt;\u0026gt; ~/.bashrc CNI 설치\n컨테이너 네트워크를 관리하기 위한 툴 컨테이너의 ip 등의 네트워크를 지정해서 설정 가능 1 2 3 4 # CNI 설치 wget https://github.com/containernetworking/plugins/releases/download/v1.0.1/cni-plugins-linux-amd64-v1.0.1.tgz mkdir -p /opt/cni/bin tar Cxzvvf /opt/cni/bin cni-plugins-linux-amd64-v1.0.1.tgz buildkit 설치\ncontainerd에서 image를 빌드하기 위한 툴 1 2 3 wget https://github.com/moby/buildkit/releases/download/v0.10.0-rc1/buildkit-v0.10.0-rc1.linux-amd64.tar.gz tar Cxzvvf /usr/bin buildkit-v0.10.0-rc1.linux-amd64.tar.gz nohup /usr/bin/builkitd \u0026lt; /dev/null \u0026gt; /var/log/buildkitd 2\u0026gt;\u0026amp;1 \u0026amp; containerd 실행\n1 systemctl enable --now containerd ","date":"2025-09-05T09:00:00+09:00","permalink":"https://joonlog.github.io/p/containerd-%EC%84%A4%EC%B9%98-%EB%B0%8F-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0/","title":"Containerd 설치 및 환경 세팅하기"},{"content":" Tibero DB에서 스키마를 복제할 경우 기존 DB와의 테이블스페이스 및 데이터파일 중복을 고려해서 작업 필요\n복제 원본 스키마 정보 확인\n대상 스키마에서 사용하는 테이블스페이스, 데이터파일, 크기 확인 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 -- 원본 스키마가 쓰는 테이블스페이스 목록 SELECT DISTINCT tablespace_name FROM dba_segments WHERE owner = \u0026lt;대상 스키마명\u0026gt; ORDER BY 1; -- 각 TS에 대해 현재 데이터파일 경로와 크기 SELECT tablespace_name, file_id, file_name, ROUND(bytes/1024/1024) AS size_mb, autoextensible, ROUND(maxbytes/1024/1024) AS max_mb FROM dba_data_files WHERE tablespace_name IN ( SELECT DISTINCT tablespace_name FROM dba_segments WHERE owner=\u0026lt;대상 스키마명\u0026gt; ) ORDER BY tablespace_name, file_id; -- 각 TS에서 대상 스키마가 실제 사용 중인 용량(세그먼트 기준) SELECT tablespace_name, ROUND(SUM(bytes)/1024/1024) AS used_mb FROM dba_segments WHERE owner=\u0026lt;대상 스키마명\u0026gt; GROUP BY tablespace_name ORDER BY tablespace_name; 대상(복제) 스키마 구성\n대상 스키마 생성, 권한 부여, 쿼터 부여 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 -- 대상 테이블 스페이스 생성 CREATE TABLESPACE \u0026lt;대상TS1\u0026gt; DATAFILE \u0026#39;\u0026lt;경로\u0026gt;/\u0026lt;대상TS1\u0026gt;.dbf\u0026#39; SIZE \u0026lt;사이즈\u0026gt;M \u0026lt;여부 예: AUTOEXTEND ON\u0026gt;; CREATE TABLESPACE \u0026lt;대상TS2\u0026gt; DATAFILE \u0026#39;\u0026lt;경로\u0026gt;/\u0026lt;대상TS2\u0026gt;.dbf\u0026#39; SIZE \u0026lt;사이즈\u0026gt;M \u0026lt;여부 예: AUTOEXTEND ON\u0026gt;; -- 복제 적용할 대상 전부 동일하게 구성(원본 스키마와 테이블 스페이스를 공유할 경우 적용X) -- 대상 스키마 생성 CREATE USER \u0026lt;대상 스키마명\u0026gt; IDENTIFIED BY \u0026#34;\u0026lt;패스워드\u0026gt;\u0026#34; DEFAULT TABLESPACE \u0026lt;대상TS, 예: \u0026lt;복제 스키마\u0026gt;\u0026gt; TEMPORARY TABLESPACE TEMP; -- 권한 부여(필요시) GRANT CONNECT, RESOURCE TO \u0026lt;대상 스키마명\u0026gt;; -- 대상 스키마에 각 대상 TS 쿼터 부여 ALTER USER \u0026lt;대상 스키마명\u0026gt; QUOTA UNLIMITED ON \u0026lt;대상TS\u0026gt;; -- 복제 적용할 대상 전부 구성 tbexport 백업 파일 생성\n테이블스페이스 리맵 적용 1 2 3 4 5 6 7 tbexport \\ username=\u0026lt;계정명\u0026gt; password=\u0026lt;비밀번호\u0026gt; sid=\u0026lt;DB명\u0026gt; \\ file=\u0026lt;덤프 파일 경로 예: /data/db_dump/src_full_remap.dmp\u0026gt; \\ user=\u0026lt;원본 스키마명\u0026gt; \\ remap_tablespace=\u0026lt;원본TS1\u0026gt;:\u0026lt;대상TS1\u0026gt;,\u0026lt;원본TS2\u0026gt;:\u0026lt;대상TS2\u0026gt; -- 원본 스키마와 테이블 스페이스를 분리할 테이블 스페이스만 적용 tbimport 데이터 적재\n1 2 3 4 5 tbimport \\ username=\u0026lt;계정명\u0026gt; password=\u0026lt;비밀번호\u0026gt; sid=\u0026lt;DB명\u0026gt; \\ file=\u0026lt;덤프 파일 경로 예: /data/db_dump/src_full_remap.dmp\u0026gt; \\ fromuser=\u0026lt;원본 스키마명\u0026gt; touser=\u0026lt;대상 스키마명\u0026gt; \\ rows=y constraint=n index=n trigger=n ignore=Y tbimport 메타데이터 적재\n1 2 3 4 5 6 tbimport \\ username=\u0026lt;계정명\u0026gt; password=\u0026lt;비밀번호\u0026gt; sid=\u0026lt;DB명\u0026gt; \\ file=\u0026lt;DUMP_FILE\u0026gt; \\ log=\u0026lt;LOG_META\u0026gt; \\ fromuser=\u0026lt;원본 스키마명\u0026gt; touser=\u0026lt;대상 스키마명\u0026gt; \\ rows=n constraint=y index=y trigger=y ignore=Y 검증\n1 2 3 4 5 6 7 -- 테이블 수 비교 SELECT COUNT(*) FROM dba_tables WHERE owner=\u0026lt;대상 스키마\u0026gt;; SELECT COUNT(*) FROM dba_tables WHERE owner=\u0026lt;복제 스키마\u0026gt;; -- 인덱스 수 비교 SELECT COUNT(*) FROM dba_indexes WHERE owner=\u0026lt;대상 스키마\u0026gt;; SELECT COUNT(*) FROM dba_indexes WHERE owner=\u0026lt;복제 스키마\u0026gt;; 외부 스키마의 권한 부여(선택)\n복제 스키마 외에 다른 스키마가 존재하고, 해당 스키마가 복제한 스키마를 참조할 경우 권한을 별도로 부여해야 함 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 1) 오브젝트 권한(테이블/뷰/시퀀스/프로시저/펑션/패키지 등) -- \u0026lt;대상 스키마\u0026gt;에는 있고 \u0026lt;복제 스키마\u0026gt;에는 없는 권한 SELECT privilege, owner, table_name FROM dba_tab_privs WHERE grantee=\u0026lt;대상 스키마\u0026gt; MINUS SELECT privilege, owner, table_name FROM dba_tab_privs WHERE grantee=\u0026lt;복제 스키마\u0026gt;; 2) 컬럼 단위 권한(있을 수 있음: SELECT/UPDATE 컬럼 권한) -- \u0026lt;대상 스키마\u0026gt;에는 있고 \u0026lt;복제 스키마\u0026gt;에는 없는 컬럼 권한 SELECT privilege, owner, table_name, column_name FROM dba_col_privs WHERE grantee=\u0026lt;대상 스키마\u0026gt; MINUS SELECT privilege, owner, table_name, column_name FROM dba_col_privs WHERE grantee=\u0026lt;복제 스키마\u0026gt;; 3) 시스템 권한(필요시) -- \u0026lt;대상 스키마\u0026gt;에는 있고 \u0026lt;복제 스키마\u0026gt;에는 없는 시스템 권한 SELECT privilege FROM dba_sys_privs WHERE grantee=\u0026lt;대상 스키마\u0026gt; MINUS SELECT privilege FROM dba_sys_privs WHERE grantee=\u0026lt;복제 스키마\u0026gt;; 4) 롤(역할) 부여 비교(필요시) -- \u0026lt;대상 스키마\u0026gt;에는 있고 \u0026lt;복제 스키마\u0026gt;에는 없는 롤 SELECT granted_role FROM dba_role_privs WHERE grantee=\u0026lt;대상 스키마\u0026gt; MINUS SELECT granted_role FROM dba_role_privs WHERE grantee=\u0026lt;복제 스키마\u0026gt;; ","date":"2025-09-04T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EA%B0%99%EC%9D%80-tibero-db-%EC%95%88%EC%97%90%EC%84%9C-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%B3%B5%EC%A0%9C%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95/","title":"같은 Tibero DB 안에서 스키마 복제하는 방법"},{"content":" 백업 모드 시작 - 데이터파일 복사 - 백업 모드 종료의 구조로 백업하는 방식 백업 스크립트 DB내 존재하는 테이블스페이스 리스트를 읽어 반복문으로 풀 백업 pigz 명령어를 사용한 멀티스레드 압축 다수의 대용량 데이터 파일을 압축해야하는 현재 상황에서는 pigz를 사용하지 않고 tar로만 압축하면 24시간 이상 소요 ⇒ pigz 사용 시 6시간 소요 백업 파일과 DB 메타데이터 비교를 통한 누락 파일 감지 이전 백업 파일 및 로그 정리 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 #!/bin/bash set -euo pipefail ########## 환경설정 ########## export TB_HOME=/tibero/tibero6 export TB_SID=\u0026lt;sid명\u0026gt; export PATH=\u0026#34;$TB_HOME/client/bin:/usr/bin:/bin\u0026#34; export LD_LIBRARY_PATH=\u0026#34;$TB_HOME/client/lib:${LD_LIBRARY_PATH:-}\u0026#34; TBSQL=\u0026#34;$TB_HOME/client/bin/tbsql\u0026#34; USER=\u0026lt;계정명\u0026gt; PASS=\u0026lt;비밀번호\u0026gt; DATE=$(date +%Y%m%d) BACKUP_ROOT=\u0026#34;/data/db_dump\u0026#34; BACKUP_DIR=\u0026#34;$BACKUP_ROOT/$DATE\u0026#34; DATA_DIR=\u0026#34;/data/\u0026lt;db명\u0026gt;\u0026#34; ARCHIVE_DIR=\u0026#34;/data/archive\u0026#34; LOGFILE=\u0026#34;$BACKUP_ROOT/\u0026lt;db명\u0026gt;_backup_$DATE.log\u0026#34; LOG_DIR=\u0026#34;$BACKUP_ROOT\u0026#34; LOG_FILE=\u0026#34;$BACKUP_ROOT/\u0026lt;db명\u0026gt;_backup_check_$DATE.log\u0026#34; PIGZ=\u0026#34;/usr/bin/pigz\u0026#34; export LC_ALL=C mkdir -p \u0026#34;$BACKUP_DIR\u0026#34; ########## 유틸 ########## log() { echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] $*\u0026#34; | tee -a \u0026#34;$LOGFILE\u0026#34; ; } tbq() { echo \u0026#34;$1\u0026#34; | \u0026#34;$TBSQL\u0026#34; \u0026#34;$USER/$PASS\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOGFILE\u0026#34; 2\u0026gt;\u0026amp;1 ; } ########## 1) DB에서 TS,파일 경로 Spool ########## MAP_FILE=\u0026#34;$BACKUP_DIR/_filemap_${DATE}.csv\u0026#34; # 원본 Spool MAP_CLEAN=\u0026#34;$BACKUP_DIR/_filemap_${DATE}.clean.csv\u0026#34; # 정제본 log \u0026#34;===== DBA_DATA_FILES Spool 시작 =====\u0026#34; \u0026#34;$TBSQL\u0026#34; \u0026#34;$USER/$PASS\u0026#34; \u0026lt;\u0026lt;EOF \u0026gt;\u0026gt; \u0026#34;$LOGFILE\u0026#34; 2\u0026gt;\u0026amp;1 SET ECHO OFF SET PAGESIZE 0 SET FEEDBACK OFF SET HEADING OFF SET TRIMSPOOL ON SPOOL $MAP_FILE SELECT TRIM(tablespace_name) || \u0026#39;,\u0026#39; || TRIM(file_name) FROM dba_data_files ORDER BY tablespace_name, file_name; SPOOL OFF EXIT; EOF # 정제: CR 제거 → 앞의 \u0026#34;SQL\u0026gt; [번호]\u0026#34; 제거 → 공백 Trim → 패턴 필터링 sed -E \u0026#39;s/\\r$//\u0026#39; \u0026#34;$MAP_FILE\u0026#34; \\ | sed -E \u0026#39;s/^SQL\u0026gt;[[:space:]]*([0-9[:space:]]+)?//\u0026#39; \\ | awk -F, \u0026#39;NF==2 { gsub(/^[[:space:]]+|[[:space:]]+$/,\u0026#34;\u0026#34;,$1); gsub(/^[[:space:]]+|[[:space:]]+$/,\u0026#34;\u0026#34;,$2); if ($1 ~ /^[A-Z0-9_]+$/ \u0026amp;\u0026amp; $2 ~ /^\\//) print $1\u0026#34;,\u0026#34;$2 }\u0026#39; \u0026gt; \u0026#34;$MAP_CLEAN\u0026#34; if [[ ! -s \u0026#34;$MAP_CLEAN\u0026#34; ]]; then log \u0026#34;ERROR: 파일 맵 정제 결과가 비었습니다. ($MAP_CLEAN)\u0026#34; exit 1 fi log \u0026#34;Spool 원본 라인수: $(wc -l \u0026lt; \u0026#34;$MAP_FILE\u0026#34;), 정제 후 라인수: $(wc -l \u0026lt; \u0026#34;$MAP_CLEAN\u0026#34;)\u0026#34; log \u0026#34;===== Spool 완료(정제 적용): $(wc -l \u0026lt; \u0026#34;$MAP_CLEAN\u0026#34;) rows =====\u0026#34; ########## 2) TS별 파일 목록 구성 ########## declare -A TS_FILES declare -a TS_ORDER while IFS=, read -r TS F; do [[ -z \u0026#34;$TS\u0026#34; || -z \u0026#34;$F\u0026#34; ]] \u0026amp;\u0026amp; continue [[ \u0026#34;$TS\u0026#34; =~ ^[A-Z0-9_]+$ ]] || continue [[ \u0026#34;$F\u0026#34; =~ ^/.*$ ]] || continue if [[ -z \u0026#34;${TS_FILES[$TS]+_}\u0026#34; ]]; then TS_FILES[$TS]=\u0026#34;$F\u0026#34; TS_ORDER+=(\u0026#34;$TS\u0026#34;) else TS_FILES[$TS]=\u0026#34;${TS_FILES[$TS]} $F\u0026#34; fi done \u0026lt; \u0026#34;$MAP_CLEAN\u0026#34; log \u0026#34;감지된 테이블스페이스 수: ${#TS_ORDER[@]}\u0026#34; ########## 3) TS 하나 백업(복사/압축) ########## backup_one_ts() { local TS=\u0026#34;$1\u0026#34;; shift local FILES=(\u0026#34;$@\u0026#34;) local BASENAMES=() local MANIFEST=\u0026#34;${TS}${DATE}.manifest\u0026#34; local TGZ=\u0026#34;${TS}_${DATE}.tar.gz\u0026#34; log \u0026#34;===== ${TS} BEGIN BACKUP =====\u0026#34; tbq \u0026#34;ALTER TABLESPACE ${TS} BEGIN BACKUP;\u0026#34; for f in \u0026#34;${FILES[@]}\u0026#34;; do if [[ -r \u0026#34;$f\u0026#34; ]]; then cp \u0026#34;$f\u0026#34; \u0026#34;$BACKUP_DIR/\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOGFILE\u0026#34; 2\u0026gt;\u0026amp;1 BASENAMES+=(\u0026#34;$(basename \u0026#34;$f\u0026#34;)\u0026#34;) else log \u0026#34;WARN: ${TS} 데이터파일 읽기 불가: $f\u0026#34; fi done tbq \u0026#34;ALTER TABLESPACE ${TS} END BACKUP;\u0026#34; if (( ${#BASENAMES[@]} == 0 )); then log \u0026#34;WARN: ${TS} 복사된 파일이 없어 압축을 생략합니다.\u0026#34; return 0 fi ( cd \u0026#34;$BACKUP_DIR\u0026#34; || exit 1 tar --index-file=\u0026#34;$MANIFEST\u0026#34; -cvf - \u0026#34;${BASENAMES[@]}\u0026#34; 2\u0026gt;\u0026gt;\u0026#34;$LOGFILE\u0026#34; \\ | \u0026#34;$PIGZ\u0026#34; -p 8 \u0026gt; \u0026#34;$TGZ\u0026#34; rm -f \u0026#34;${BASENAMES[@]}\u0026#34; 2\u0026gt;\u0026gt;\u0026#34;$LOGFILE\u0026#34; || true ) \u0026gt;\u0026gt; \u0026#34;$LOGFILE\u0026#34; 2\u0026gt;\u0026amp;1 \u0026amp; } ########## 4) 병렬 폭 조절(PARALLEL=2: 두 개씩) ########## PARALLEL=2 running=0 for TS in \u0026#34;${TS_ORDER[@]}\u0026#34;; do read -r -a arr \u0026lt;\u0026lt;\u0026lt; \u0026#34;${TS_FILES[$TS]}\u0026#34; backup_one_ts \u0026#34;$TS\u0026#34; \u0026#34;${arr[@]}\u0026#34; ((++running)) if (( running \u0026gt;= PARALLEL )); then wait running=0 log \u0026#34;===== 직전 ${PARALLEL}개 TS 백업/압축 완료(wait) =====\u0026#34; fi done wait log \u0026#34;===== 모든 TS 백업/압축 완료 =====\u0026#34; ########## 5) MISC 묶음 ########## MISC_DIR=\u0026#34;$BACKUP_DIR/.misc_tmp\u0026#34; mkdir -p \u0026#34;$MISC_DIR\u0026#34; log \u0026#34;===== 컨트롤파일 SQL 백업 =====\u0026#34; tbq \u0026#34;ALTER DATABASE BACKUP CONTROLFILE TO TRACE AS \u0026#39;${MISC_DIR}/crectl.sql\u0026#39; REUSE NORESETLOGS;\u0026#34; log \u0026#34;===== 컨트롤파일 물리 백업 =====\u0026#34; cp /data/\u0026lt;db명\u0026gt;/c1.ctl \u0026#34;$MISC_DIR/\u0026#34; 2\u0026gt;\u0026gt; \u0026#34;$LOGFILE\u0026#34; || true cp /tibero/tibero6/config/c2.ctl \u0026#34;$MISC_DIR/\u0026#34; 2\u0026gt;\u0026gt; \u0026#34;$LOGFILE\u0026#34; || true log \u0026#34;===== 아카이브 로그 스위치 및 복사 =====\u0026#34; tbq \u0026#34;ALTER SYSTEM SWITCH LOGFILE;\u0026#34; cp \u0026#34;$ARCHIVE_DIR\u0026#34;/*.arc \u0026#34;$MISC_DIR/\u0026#34; 2\u0026gt;\u0026gt; \u0026#34;$LOGFILE\u0026#34; || true log \u0026#34;===== 리두로그/.passwd 백업 =====\u0026#34; cp /data/\u0026lt;db명\u0026gt;/log*.log \u0026#34;$MISC_DIR/\u0026#34; 2\u0026gt;\u0026gt; \u0026#34;$LOGFILE\u0026#34; || true cp /data/\u0026lt;db명\u0026gt;/.passwd \u0026#34;$MISC_DIR/\u0026#34; 2\u0026gt;\u0026gt; \u0026#34;$LOGFILE\u0026#34; || true tar -czf \u0026#34;${BACKUP_DIR}/MISC_${DATE}.tar.gz\u0026#34; -C \u0026#34;$MISC_DIR\u0026#34; . \u0026gt;\u0026gt; \u0026#34;$LOGFILE\u0026#34; 2\u0026gt;\u0026amp;1 rm -rf \u0026#34;$MISC_DIR\u0026#34; log \u0026#34;===== 전체 백업 완료(MISC 포함) =====\u0026#34; ########## 6) LABEL 생성 및 검증(오탐 방지) ########## : \u0026gt; \u0026#34;$BACKUP_DIR/LABEL${DATE}.manifest\u0026#34; cat \u0026#34;$BACKUP_DIR\u0026#34;/*\u0026#34;${DATE}.manifest\u0026#34; 2\u0026gt;/dev/null \\ | awk -F/ \u0026#39;{print $NF}\u0026#39; \\ | sort -u \u0026gt; \u0026#34;$BACKUP_DIR/LABEL${DATE}.manifest\u0026#34; if [[ ! -s \u0026#34;$BACKUP_DIR/LABEL${DATE}.manifest\u0026#34; ]]; then log \u0026#34;ERROR: LABEL${DATE}.manifest is empty (no fallback by policy)\u0026#34; exit 1 fi : \u0026gt; \u0026#34;$LOG_DIR/backup_filelist.txt\u0026#34; awk -F/ \u0026#39;{print $NF}\u0026#39; \u0026#34;$BACKUP_DIR/LABEL${DATE}.manifest\u0026#34; | sort -u \u0026gt; \u0026#34;$LOG_DIR/backup_filelist.txt\u0026#34; # expect 목록 재생성 (tbsql 출력은 메인 로그만) $TBSQL \u0026#34;$USER/$PASS\u0026#34; \u0026lt;\u0026lt;EOF \u0026gt;\u0026gt; \u0026#34;$LOGFILE\u0026#34; 2\u0026gt;\u0026amp;1 SET ECHO OFF SET PAGESIZE 0 SET FEEDBACK OFF SET HEADING OFF SET TRIMSPOOL ON SPOOL $LOG_DIR/expected_filelist_full.txt SELECT file_name FROM dba_data_files ORDER BY 1; SPOOL OFF EXIT; EOF awk -F/ \u0026#39;{print $NF}\u0026#39; \u0026#34;$LOG_DIR/expected_filelist_full.txt\u0026#34; | sort \u0026gt; \u0026#34;$LOG_DIR/expected_filelist.txt\u0026#34; # 체크 로그 초기화 후 누락만 기록 : \u0026gt; \u0026#34;$LOG_FILE\u0026#34; comm -23 \u0026#34;$LOG_DIR/expected_filelist.txt\u0026#34; \u0026#34;$LOG_DIR/backup_filelist.txt\u0026#34; \u0026gt; \u0026#34;$LOG_FILE\u0026#34; if [[ -s \u0026#34;$LOG_FILE\u0026#34; ]]; then log \u0026#34;누락된 데이터파일이 있습니다. ($LOG_FILE 참조)\u0026#34; else log \u0026#34;DB expect 목록 대비 누락 없음\u0026#34; fi rm -f \u0026#34;$LOG_DIR/backup_filelist.txt\u0026#34; \\ \u0026#34;$LOG_DIR/expected_filelist.txt\u0026#34; \\ \u0026#34;$LOG_DIR/expected_filelist_full.txt\u0026#34; ########## 7) 하우스키핑 ########## log \u0026#34;Deleting .arc archive logs older than 7 days...\u0026#34; find \u0026#34;$ARCHIVE_DIR\u0026#34; -type f -name \u0026#34;*.arc\u0026#34; -mtime +7 -print -exec rm -f {} \\; \u0026gt;\u0026gt; \u0026#34;$LOGFILE\u0026#34; 2\u0026gt;\u0026amp;1 log \u0026#34;Deleting .log files older than 1 day...\u0026#34; find \u0026#34;$LOG_DIR\u0026#34; -maxdepth 1 -type f -name \u0026#34;*.log\u0026#34; -mtime +1 -print -exec rm -f {} \\; \u0026gt;\u0026gt; \u0026#34;$LOGFILE\u0026#34; 2\u0026gt;\u0026amp;1 YESTERDAY=$(date -d \u0026#34;1 day ago\u0026#34; +%Y%m%d) if [[ -d \u0026#34;$BACKUP_ROOT/$YESTERDAY\u0026#34; ]]; then log \u0026#34;Deleting backup directory: $BACKUP_ROOT/$YESTERDAY\u0026#34; rm -rf \u0026#34;$BACKUP_ROOT/$YESTERDAY\u0026#34; \u0026gt;\u0026gt; \u0026#34;$LOGFILE\u0026#34; 2\u0026gt;\u0026amp;1 else log \u0026#34;No backup directory to delete for $YESTERDAY\u0026#34; fi 백업 복구 방법 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ### 파일 이름은 백업본 생성 일자에 따라 변경 필요 # DB 중지 tbdown immediate # 압축 해제 tar -xzf /data/db_dump/20250806.tar.gz -C /data/db_dump # 파일 복구(운영 경로로 이동) cp /data/db_dump/\u0026lt;날짜\u0026gt;/*.arc /data/archive/ cp /data/db_dump/\u0026lt;날짜\u0026gt;/*.dtf /data/\u0026lt;SID명\u0026gt;/ cp /data/db_dump/\u0026lt;날짜\u0026gt;/*.dbf /data/\u0026lt;SID명\u0026gt;/ cp /data/db_dump/\u0026lt;날짜\u0026gt;/.passwd /data/\u0026lt;SID명\u0026gt;/ cp /data/db_dump/\u0026lt;날짜\u0026gt;/*.log /data/\u0026lt;SID명\u0026gt;/ #인스턴스 NOMOUNT tbboot nomount #컨트롤파일 재생성 @/data/db_dump/\u0026lt;날짜\u0026gt;/crectl.sql #MOUNT로 변경 tbdown tbboot mount #복구 ALTER DATABASE RECOVER AUTOMATIC DATABASE; #오픈 ALTER DATABASE OPEN RESETLOGS; ","date":"2025-09-03T09:00:00+09:00","permalink":"https://joonlog.github.io/p/tibero-db%EC%97%90%EC%84%9C-begin/end%EB%AA%A8%EB%93%9C%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%AC%BC%EB%A6%AC%EC%A0%81-%EB%B0%B1%EC%97%85-%EB%B0%8F-%EB%B3%B5%EA%B5%AC-%EB%B0%A9%EB%B2%95/","title":"Tibero DB에서 begin/end모드를 사용한 물리적 백업 및 복구 방법"},{"content":" 백업한 데이터파일을 압축 및 변형하지 않고, 지속적으로 증분하여 백업할 경우에 유용한 방식\n백업 스크립트\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #!/bin/bash # 오늘 날짜 today=$(date +%Y%m%d) # 백업 루트 디렉토리 backup_dir=\u0026#34;/data/db_dump\u0026#34; # 오늘 백업 디렉토리 및 압축 파일 경로 today_dir=\u0026#34;$backup_dir/$today\u0026#34; today_tar=\u0026#34;$backup_dir/\u0026lt;db명\u0026gt;_backup_${today}.tar.gz\u0026#34; # 아카이브 로그 디렉토리 archive_log_dir=\u0026#34;/data/archive\u0026#34; # 로그 기록용 파일 log_file=\u0026#34;$backup_dir/\u0026lt;sid명\u0026gt;_backup_${today}.log\u0026#34; echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] === Tibero Hot Backup Script Start ===\u0026#34; | tee -a \u0026#34;$log_file\u0026#34; # 1. 오늘 날짜 디렉토리 생성 mkdir -p \u0026#34;$today_dir\u0026#34; # 2. 백업 실행 echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] Starting backup to $today_dir\u0026#34; | tee -a \u0026#34;$log_file\u0026#34; tbrmgr backup --compress=HIGH -u -o \u0026#34;$today_dir\u0026#34; du -sh \u0026#34;$today_dir\u0026#34; | tee -a \u0026#34;$log_file\u0026#34; if [ $? -eq 0 ]; then echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] Backup succeeded. Compressing...\u0026#34; | tee -a \u0026#34;$log_file\u0026#34; tar -czf \u0026#34;$today_tar\u0026#34; -C \u0026#34;$backup_dir\u0026#34; \u0026#34;$today\u0026#34; if [ $? -eq 0 ]; then echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] Compression successful: $today_tar\u0026#34; | tee -a \u0026#34;$log_file\u0026#34; rm -rf \u0026#34;$today_dir\u0026#34; else echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] Compression failed!\u0026#34; | tee -a \u0026#34;$log_file\u0026#34; fi else echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] Backup failed!\u0026#34; | tee -a \u0026#34;$log_file\u0026#34; exit 1 fi # 3. 1일 초과된 백업 파일 삭제 (.tar.gz) echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] Deleting .tar.gz backups older than 1 days...\u0026#34; | tee -a \u0026#34;$log_file\u0026#34; find \u0026#34;$backup_dir\u0026#34; -maxdepth 1 -type f -name \u0026#34;*.tar.gz\u0026#34; -mtime +1 -print -exec rm -f {} \\; \u0026gt;\u0026gt; \u0026#34;$log_file\u0026#34; 2\u0026gt;\u0026amp;1 du -sh \u0026#34;$today_tar\u0026#34; | tee -a \u0026#34;$log_file\u0026#34; # 4. 7일 초과된 아카이브 로그(.arc) 삭제 echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] Deleting .arc archive logs older than 7 days from $archive_log_dir...\u0026#34; | tee -a \u0026#34;$log_file\u0026#34; find \u0026#34;$archive_log_dir\u0026#34; -type f -name \u0026#34;*.arc\u0026#34; -mtime +7 -print -exec rm -f {} \\; \u0026gt;\u0026gt; \u0026#34;$log_file\u0026#34; 2\u0026gt;\u0026amp;1 # 5. 1일 초과된 백업 로그 파일 삭제 (.log) echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] Deleting .log files older than 1 day...\u0026#34; | tee -a \u0026#34;$log_file\u0026#34; find \u0026#34;$backup_dir\u0026#34; -maxdepth 1 -type f -name \u0026#34;*.log\u0026#34; -mtime +1 -print -exec rm -f {} \\; \u0026gt;\u0026gt; \u0026#34;$log_file\u0026#34; 2\u0026gt;\u0026amp;1 echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] === Tibero Hot Backup Script Complete ===\u0026#34; | tee -a \u0026#34;$log_file\u0026#34; ### 백업스크립트 완료 복구 스크립트\n1 2 3 4 5 압축 해제 tar -xvzf /data/db_dump/\u0026lt;db명\u0026gt;_backup_\u0026lt;날짜\u0026gt;.tar.gz -C /data/db_dump/ 복구 실행 tbrmgr recover -o /data/db_dump/\u0026lt;날짜\u0026gt;/ ","date":"2025-09-03T09:00:00+09:00","permalink":"https://joonlog.github.io/p/tibero-db%EC%97%90%EC%84%9C-tbrmgr-%EB%AA%85%EB%A0%B9%EC%96%B4%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%AC%BC%EB%A6%AC%EC%A0%81-%EB%B0%B1%EC%97%85-%EB%B0%8F-%EB%B3%B5%EA%B5%AC-%EB%B0%A9%EB%B2%95/","title":"Tibero DB에서 tbrmgr 명령어를 사용한 물리적 백업 및 복구 방법"},{"content":" Tomcat 9.0 버전대에서 9.0.108 버전으로의 마이너 버전 업그레이드 마이너 버전 업그레이드는 전/후 버전의 설정이 대부분 호환됨 0. 고려사항 심볼릭 링크 방식으로 버전을 관리해서 롤백을 용이하게 설정 conf, lib, bin 일부 스크립트만 선별적으로 이관 로그/웹앱 경로는 기존 디렉토리를 링크해 데이터 손실을 방지 업그레이드 후에는 반드시 catalina.out과 애플리케이션 로그를 확인해 정상 구동 여부를 검증 1. 새 버전 다운로드 및 설치 1 2 3 4 cd /usr/local curl -O https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.108/bin/apache-tomcat-9.0.108.tar.gz tar xzf apache-tomcat-9.0.108.tar.gz mv apache-tomcat-9.0.108 tomcat-9.0.108 2. 설정 및 배포물 이관 업그레이드 시 필요한 설정/라이브러리만 이관\nconf 웬만하면 전부 복사 bin 이전 버전에서 catalina.sh 파일이 수정된 흔적이 있을 경우 반드시 파악해서 setenv.sh에 이관 setenv.sh로 관리하면 추후 작업시에도 이 파일만 옮기면 됨 setenv.sh 파일 존재할 시 복사 lib 이전 버전에서 기본 라이브러리 외에 추가된 라이브러리가 있다면 복사 1 2 3 rsync -a /usr/local/tomcat/conf/ /usr/local/tomcat-9.0.108/conf/ rsync -a /usr/local/tomcat/bin/setenv.sh /usr/local/tomcat-9.0.108/bin/ cp /usr/local/tomcat/lib/tibero6-jdbc.jar /usr/local/tomcat-9.0.108/lib/ 기존 버전에서 log, webapps 경로가 심볼릭 링크로 되어 있다면 신규 버전에서도 동일하게 설정\n1 2 rmdir /usr/local/tomcat-9.0.108/logs ln -sfn /data/tomcat/logs /usr/local/tomcat-9.0.108/ 3. 서비스 전환 구버전 종료 후 새 버전으로 심볼릭 링크 전환\n1 2 3 4 5 6 7 su - tomcat /usr/local/tomcat/bin/shutdown.sh mv /usr/local/tomcat /usr/local/tomcat-9.0.102 ln -sfn /usr/local/tomcat-9.0.108 /usr/local/tomcat /usr/local/tomcat/bin/startup.sh ","date":"2025-09-02T09:00:00+09:00","permalink":"https://joonlog.github.io/p/tomcat-%EB%A7%88%EC%9D%B4%EB%84%88-%EB%B2%84%EC%A0%84-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-tomcat-9/","title":"Tomcat 마이너 버전 업그레이드 (Tomcat 9)"},{"content":" https://docs.docker.com/engine/install RedHat 계열 1 2 3 4 5 6 7 8 9 10 11 12 13 # 기존 설치되어있는 컨테이너 런타임 삭제 for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo dnf remove $pkg -y; done # docker repo 추가 yum install -y yum-utils yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo # docker 설치 yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # 현재 사용자를 docker 그룹에 추가해서 root 권한 없어도 docker 사용 가능하게 설정 usermod -a -G docker \u0026lt;user\u0026gt; systemctl enable --now docker Ubuntu 계열 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 기존 설치되어있는 컨테이너 런타임 삭제 for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done # docker repo 설정 apt-get update apt-get install ca-certificates curl install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc # docker repo 추가 echo \\ \u0026#34;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \\ $(. /etc/os-release \u0026amp;\u0026amp; echo \u0026#34;${UBUNTU_CODENAME:-$VERSION_CODENAME}\u0026#34;) stable\u0026#34; | \\ sudo tee /etc/apt/sources.list.d/docker.list \u0026gt; /dev/null sudo apt-get update # docker 설치 sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # 현재 사용자를 docker 그룹에 추가해서 root 권한 없어도 docker 사용 가능하게 설정 usermod -a -G docker \u0026lt;user\u0026gt; systemctl enable --now docker ","date":"2025-09-01T09:00:00+09:00","permalink":"https://joonlog.github.io/p/linux-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-docker-%EC%84%A4%EC%B9%98-redhat/ubuntu-%EA%B3%84%EC%97%B4/","title":"Linux 환경에서 Docker 설치 (RedHat/Ubuntu 계열)"},{"content":"문제 확인 깃허브 블로그 템플릿으로 Jekyll Chirpy 테마를 사용 중이었는데, 쓰다 보니 카테고리가 내가 설정한 것과 다르게 동작하는 것을 발견했다.\nEX 1) AWS를 상위 카테고리로 설정한 글을 작성했는데 실제 블로그에 AWS 카테고리가가 없는 경우 EX 2) Kubernetes는 상위 카테고리로만 설정했었는데, Kubernetes 하위 카테고리로 Kubernetes가 생기고 이전 글들이 똑같이 복사돼서 들어가 있는 경우 EX 3) 하위 카테고리로 AWS를 정했는데 AWS 상위 카테고리에 글이 들어가 있는 경우 원인 Jekyll Chirpy의 _layouts/categories.html 템플릿에서 site.categories를 사용하는 방식의 문제 site.categories의 동작 방식 site.categories는 모든 포스트의 모든 카테고리를 수집\ncategories : [Certification, AWS]인 글이 있으면 site.categories['AWS']에도 포함됨\n결과적으로 같은 글이 여러 상위 카테고리에 중복 표시\n1 2 3 4 \u0026amp;#123;% for category in sort_categories %\u0026amp;#125; \u0026amp;#123;% assign category_name = category | first %\u0026amp;#125; \u0026amp;#123;% assign posts_of_category = category | last %\u0026amp;#125; \u0026amp;#123;% if category_name == first_post.categories[0] %\u0026amp;#125; 해결 방법 _layouts/categories.html 상위 카테고리 로직\nsite.categories 대신 site.posts 직접 순회 첫 번째 카테고리(post.categories[0])만 상위 카테고리로 수집 해당 상위 카테고리를 가진 글들만 대상으로 처리 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 \u0026amp;#123;% assign group_index = 0 %\u0026amp;#125; \u0026amp;#123;% comment %\u0026amp;#125; Collect all primary categories from posts \u0026amp;#123;% endcomment %\u0026amp;#125; \u0026amp;#123;% assign primary_categories = \u0026#39;\u0026#39; | split: \u0026#39;\u0026#39; %\u0026amp;#125; \u0026amp;#123;% for post in site.posts %\u0026amp;#125; \u0026amp;#123;% assign primary_cat = post.categories[0] %\u0026amp;#125; \u0026amp;#123;% if primary_cat %\u0026amp;#125; \u0026amp;#123;% unless primary_categories contains primary_cat %\u0026amp;#125; \u0026amp;#123;% assign primary_categories = primary_categories | push: primary_cat %\u0026amp;#125; \u0026amp;#123;% endunless %\u0026amp;#125; \u0026amp;#123;% endif %\u0026amp;#125; \u0026amp;#123;% endfor %\u0026amp;#125; \u0026amp;#123;% assign primary_categories = primary_categories | sort %\u0026amp;#125; \u0026amp;#123;% for category_name in primary_categories %\u0026amp;#125; \u0026amp;#123;% comment %\u0026amp;#125; Get posts that have this as primary category \u0026amp;#123;% endcomment %\u0026amp;#125; \u0026amp;#123;% assign posts_of_category = \u0026#39;\u0026#39; | split: \u0026#39;\u0026#39; %\u0026amp;#125; \u0026amp;#123;% for post in site.posts %\u0026amp;#125; \u0026amp;#123;% if post.categories[0] == category_name %\u0026amp;#125; \u0026amp;#123;% assign posts_of_category = posts_of_category | push: post %\u0026amp;#125; \u0026amp;#123;% endif %\u0026amp;#125; \u0026amp;#123;% endfor %\u0026amp;#125; _layouts/categories.html 하위 카테고리 로직\n해당 상위 카테고리를 첫 번째로 가진 글들의 두 번째 카테고리만 수집\n중복 제거 로직 유지\n1 2 3 4 5 6 7 8 9 10 \u0026amp;#123;% comment %\u0026amp;#125; Collect subcategories \u0026amp;#123;% endcomment %\u0026amp;#125; \u0026amp;#123;% assign sub_categories = \u0026#39;\u0026#39; | split: \u0026#39;\u0026#39; %\u0026amp;#125; \u0026amp;#123;% for post in posts_of_category %\u0026amp;#125; \u0026amp;#123;% assign second_category = post.categories[1] %\u0026amp;#125; \u0026amp;#123;% if second_category %\u0026amp;#125; \u0026amp;#123;% unless sub_categories contains second_category %\u0026amp;#125; \u0026amp;#123;% assign sub_categories = sub_categories | push: second_category %\u0026amp;#125; \u0026amp;#123;% endunless %\u0026amp;#125; \u0026amp;#123;% endif %\u0026amp;#125; \u0026amp;#123;% endfor %\u0026amp;#125; _layouts/categories.html 포스트 수 계산 로직 site.categories[category_name] 대신 실제 해당 카테고리 글 개수 계산\n하위 카테고리도 정확한 개수 표시\n1 2 3 4 5 6 7 8 9 10 11 # Primary category \u0026amp;#123;% assign top_posts_size = posts_of_category | size %\u0026amp;#125; # Subcategory \u0026amp;#123;% comment %\u0026amp;#125; Count posts for this subcategory under current primary category \u0026amp;#123;% endcomment %\u0026amp;#125; \u0026amp;#123;% assign posts_size = 0 %\u0026amp;#125; \u0026amp;#123;% for post in posts_of_category %\u0026amp;#125; \u0026amp;#123;% if post.categories[1] == sub_category %\u0026amp;#125; \u0026amp;#123;% assign posts_size = posts_size | plus: 1 %\u0026amp;#125; \u0026amp;#123;% endif %\u0026amp;#125; \u0026amp;#123;% endfor %\u0026amp;#125; 빌드 에러 발생 시 Liquid syntax error (line 150): \u0026rsquo;endif\u0026rsquo; is not a valid delimiter for for tags. use endfor\n불필요한 \u0026amp;#123;% endif %\u0026amp;#125; 제거 (for 루프에는 \u0026amp;#123;% endfor %\u0026amp;#125;만 필요)\n1 2 \u0026amp;#123;% assign group_index = group_index | plus: 1 %\u0026amp;#125; \u0026amp;#123;% endfor %\u0026amp;#125; 해결 각 카테고리가 첫 번째 상위 카테고리 기준으로만 분류하도록 수정 중복 포스팅 문제 해결! ","date":"2025-08-30T09:00:00+09:00","permalink":"https://joonlog.github.io/p/jekyll-chirpy-%ED%85%8C%EB%A7%88-%EC%B9%B4%ED%85%8C%EA%B3%A0%EB%A6%AC-%EC%97%90%EB%9F%AC-troubleshooting1/","title":"Jekyll Chirpy 테마 카테고리 에러 TroubleShooting(1)"},{"content":" 매니지드 K8S가 아닌 자체 관리형 K8S에서의 Jenkins 구축 방법\nUI 접근을 위한 외부 통신으로 MetalLB+HAproxy+Nginx Controller가 구성 되었다는 전제 하에 구축\nCSP 환경이라면 Ingress 설정에서 AWS ALB Controller와 같은 로드밸런서를 사용해 훨씬 간편하게 구축 가능\nJenkins 공식문서: ****https://www.jenkins.io/doc/book/installing/kubernetes/ Jenkins 설치 Helm 리포지토리 추가\n1 2 helm repo add jenkinsci https://charts.jenkins.io helm repo update jenkins-ci 네임스페이스 생성\n1 kubectl create ns jenkins jenkins-pv 생성(기존 StorageClass 없을 시에만 진행)\n기존에 사용하던 Ceph, Longhorn, EBS Driver, Harbor 같은 StorageClass가 없을 경우에 로컬 경로를 사용한 PV 할당 방법 StorageClass가 있다면 PVC 생성 시 자동으로 PV가 할당되기 때문에 이 과정은 불필요 1 2 vim jenkins-01-volume.yaml kubectl apply -f jenkins-01-volume.yaml https://raw.githubusercontent.com/jenkins-infra/jenkins.io/master/content/doc/tutorials/kubernetes/installing-jenkins-on-kubernetes/jenkins-01-volume.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 apiVersion: v1 kind: PersistentVolume metadata: name: jenkins-pv spec: storageClassName: jenkins-pv accessModes: - ReadWriteOnce capacity: storage: 20Gi persistentVolumeReclaimPolicy: Retain hostPath: path: /data/jenkins-volume/ --- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: jenkins-pv provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer ServiceAccount 생성\n1 2 vim jenkins-02-sa.yaml kubectl apply -f jenkins-02-sa.yaml https://raw.githubusercontent.com/jenkins-infra/jenkins.io/master/content/doc/tutorials/kubernetes/installing-jenkins-on-kubernetes/jenkins-02-sa.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 --- apiVersion: v1 kind: ServiceAccount metadata: name: jenkins namespace: jenkins --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: annotations: rbac.authorization.kubernetes.io/autoupdate: \u0026#34;true\u0026#34; labels: kubernetes.io/bootstrapping: rbac-defaults name: jenkins rules: - apiGroups: - \u0026#39;*\u0026#39; resources: - statefulsets - services - replicationcontrollers - replicasets - podtemplates - podsecuritypolicies - pods - pods/log - pods/exec - podpreset - poddisruptionbudget - persistentvolumes - persistentvolumeclaims - jobs - endpoints - deployments - deployments/scale - daemonsets - cronjobs - configmaps - namespaces - events - secrets verbs: - create - get - watch - delete - list - patch - update - apiGroups: - \u0026#34;\u0026#34; resources: - nodes verbs: - get - list - watch - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: annotations: rbac.authorization.kubernetes.io/autoupdate: \u0026#34;true\u0026#34; labels: kubernetes.io/bootstrapping: rbac-defaults name: jenkins roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: jenkins subjects: - apiGroup: rbac.authorization.k8s.io kind: Group name: system:serviceaccounts:jenkins jenkins 설치\nhttps://github.com/jenkinsci/helm-charts/tree/main/charts/jenkins\nhelm으로 설치 values.yaml 수정 1 2 3 4 wget https://raw.githubusercontent.com/jenkinsci/helm-charts/main/charts/jenkins/values.yaml vim values.yaml helm install jenkins -n jenkins -f values.yaml jenkinsci/jenkins helm을 통해 pvc 자동 생성\npv는 longhorn이 자동으로 프로비저닝 SA는 2번에서 생성한 리소스 사용 1 2 3 4 5 6 7 persistence: storageClass: longhorn size: \u0026#34;20Gi\u0026#34; serviceAccount: create: false name: jenkins Jenkins UI 접근 ingress 생성\n8080포트를 사용하는 jenkins service로 연결 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 cat \u0026lt;\u0026lt;EOF \u0026gt; jenkins-ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: jenkins-ingress namespace: jenkins annotations: nginx.ingress.kubernetes.io/backend-protocol: \u0026#34;HTTP\u0026#34; nginx.ingress.kubernetes.io/proxy-body-size: \u0026#34;50m\u0026#34; spec: ingressClassName: nginx rules: - host: \u0026#34;jenkins.\u0026lt;도메인\u0026gt;\u0026#34; http: paths: - path: / pathType: Prefix backend: service: name: jenkins port: number: 8080 EOF kubectl -n jenkins apply -f jenkins-ingress.yaml HAproxy 설정\ningress를 조회하여 MetalLB로부터 부여받은 IP 확인\n현재는 172.27.1.100 1 2 3 # kubectl get ingress -n jenkins NAME CLASS HOSTS ADDRESS PORTS AGE jenkins-ingress nginx jenkins.\u0026lt;도메인\u0026gt; 172.27.1.100 80 26h 확인한 IP를 HAproxy 설정에 추가\nHAproxy 서버 공인 IP로 접근 시 jenkins의 Ingress로 통신되도록 설정 http-response replace-value Location ^http://jenkins\\.\u0026lt;도메인\u0026gt;/(.*)$ http://jenkins.\u0026lt;도메인\u0026gt;:\u0026lt;포트\u0026gt;/\\\\1 두 조건을 만족할 경우 설정 필요 외부 브라우저에서 Jenkins 접근 시 포트를 붙여서 접근해야 될 경우 애플리케이션 내부적으로 url로 응답이 나오도록 설정되어 있을 경우 EX) Jenkins 파이프라인 빌드 시 파라미터를 기입해야 할 경우 Jenkins 설정된 url 응답 설정하지 않으면 접근 시 자동으로 포트가 제거된 상태로 리다이렉션되어 404 에러 1 2 3 vim /etc/haproxy/haproxy.cfg haproxy -c -f /etc/haproxy/haproxy.cfg systemctl reload haproxy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 frontend unified_frontend_8080 bind *:8080 mode http option forwardfor http-request set-header X-Forwarded-Host %[req.hdr(host)] http-request set-header X-Forwarded-Proto http http-request set-header X-Forwarded-Port %[dst_port] # Host 기반 라우팅 use_backend metallb_backend_jenkins if { hdr(host) -m sub jenkins } backend metallb_backend_jenkins http-response replace-value Location ^http://jenkins\\.\u0026lt;도메인\u0026gt;/(.*)$ http://jenkins.\u0026lt;도메인\u0026gt;:\u0026lt;포트\u0026gt;/\\\\1 server jenkins 172.27.1.100:80 접근 성공!\nJenkins 관리자 비밀번호 확인\n1 2 3 jsonpath=\u0026#34;{.data.jenkins-admin-password}\u0026#34; secret=$(kubectl get secret -n jenkins jenkins -o jsonpath=$jsonpath) echo $(echo $secret | base64 --decode) ","date":"2025-08-29T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EC%9E%90%EC%B2%B4-%EA%B4%80%EB%A6%AC%ED%98%95-kubernetes%EC%97%90%EC%84%9C%EC%9D%98-jenkins-%EA%B5%AC%EC%B6%95/","title":"자체 관리형 Kubernetes에서의 Jenkins 구축"},{"content":" Terrafrom으로 AWS를 컨트롤하기 위한 사전 작업 Terraform 설치 1 2 3 sudo yum install -y yum-utils shadow-utils sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo sudo yum -y install terraform awscli 설치 1 2 3 curl \u0026#34;https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip\u0026#34; -o \u0026#34;awscliv2.zip\u0026#34; unzip awscliv2.zip sudo ./aws/install Terraform, AWS 환경 설정 terraform 자동완성, alias 설정 aws 자동완성 설정 사전 발급한 aws access key 설정 1 2 3 4 5 6 7 8 9 10 11 12 13 cat \u0026lt;\u0026lt; EOF \u0026gt;\u0026gt; ~/.bashrc complete -C \u0026#39;/usr/local/bin/aws_completer\u0026#39; aws complete -C /usr/bin/terraform terraform complete -C /usr/bin/terraform tf alias tf=\u0026#39;terraform\u0026#39; alias tfap=\u0026#39;tf apply --auto-approve\u0026#39; export AWS_\u0026lt;ACCESS_KEY_ID\u0026gt;=\u0026lt;ACCESS_KEY_ID\u0026gt; export AWS_\u0026lt;SECRET_ACCESS_KEY\u0026gt;=\u0026lt;SECRET_ACCESS_KEY\u0026gt; export AWS_DEFAULT_REGION=ap-northeast-2 EOF . ~/.bashrc ","date":"2025-08-25T09:00:00+09:00","permalink":"https://joonlog.github.io/p/terraform%EC%9C%BC%EB%A1%9C-aws-%EB%A6%AC%EC%86%8C%EC%8A%A4%EB%A5%BC-%EC%BB%A8%ED%8A%B8%EB%A1%A4%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%9C-%EC%84%B8%ED%8C%85/","title":"Terraform으로 AWS 리소스를 컨트롤하기 위한 세팅"},{"content":" 외부에서의 로컬 K8S 클러스터 접근을 위해 MetalLB와 HAproxy를 사용 K8S 클러스터 내부의 로드밸런서로는 nginx controller를 사용 중인 상태 AWS라면, ALB Controller 하나만으로 MetalLB+HAproxy+nginxcontroller 기능을 커버 가능 사실상의 CSP의 K8S를 쓰는 이유?? MetalLB https://metallb.universe.tf/installation/\n클러스터 내부용 로드밸런서 오픈소스 CSP의 LB를 사용하지 못하는 경우(내/외부 양쪽에서 사용가능한 LB)에, 내부용 L4 로드밸런서로 사용하기 위해 주로 MetalLB를 사용 사전 준비 kube-proxy의 configmap에서 strictARP 활성화 1 2 3 4 5 6 7 kubectl edit configmap -n kube-system kube-proxy apiVersion: kubeproxy.config.k8s.io/v1alpha1 kind: KubeProxyConfiguration mode: \u0026#34;ipvs\u0026#34; ipvs: strictARP: true MetalLB 설치 helm으로 설치 1 2 helm repo add metallb https://metallb.github.io/metallb helm install metallb metallb/metallb MetalLB 설정 IP Pool 설정 1 2 3 4 5 6 7 8 9 10 cat \u0026lt;\u0026lt;EOF | kubectl apply -f - apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: default-pool namespace: default spec: addresses: - 172.27.1.100-172.27.1.110 EOF L2Advertisement 설정 1 2 3 4 5 6 7 8 9 10 cat \u0026lt;\u0026lt;EOF | kubectl apply -f - apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: default namespace: default spec: ipAddressPools: - default-pool EOF HAproxy haproxy 설치 1 2 yum install -y haproxy systemctl enable --now haproxy haproxy 설정 Jenkins를 예시로 구성\n1 2 3 # kubectl get ingress -n jenkins NAME CLASS HOSTS ADDRESS PORTS AGE jenkins-ingress nginx jenkins.local 172.27.1.100 80 26h 파드가 MetalLB로부터 부여받은 IP가 172.27.1.100일 경우 하기와 같이 설정 백엔드 헬스체크는 MetalLB에서 하고 있으니 HAproxy에선 설정하지 않음 http-request set-header Host 클라이언트에서 오는 모든 HTTP 요청의 Host 헤더를 jenkins.local로 변경 http-request del-header X-Forwarded-Host 이전 프록시에서 설정한 Host 헤더 제거 http-request del-header X-Forwarded-Proto 이전 프록시에서 설정한 프로토콜 정보 제거 1 2 3 4 5 6 7 8 tee /etc/haproxy/haproxy.cfg \u0026gt; /dev/null \u0026lt;\u0026lt;EOF frontend jenkins_frontend bind *:8080 default_backend metallb_backend backend metallb_backend server metallb 172.27.1.100:80 EOF 통신 구조 jenkins 파드가 하기와 같이 구성되어 있다 가정할 때 통신 구조 현재 외부에서의 접근 시 80/443 포트를 사용하지 못하고 별도의 포트를 사용해야 하는 환경이기 때문에 8080포트를 사용 1 2 3 4 5 6 7 8 9 10 11 12 13 # kubectl get ingress -n jenkins NAME CLASS HOSTS ADDRESS PORTS AGE jenkins-ingress nginx jenkins.local 172.27.1.100 80 26h # kubectl get all -n jenkins NAME READY STATUS RESTARTS AGE pod/jenkins-0 2/2 Running 0 9d NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/jenkins ClusterIP 10.233.5.87 \u0026lt;none\u0026gt; 8080/TCP 9d service/jenkins-agent ClusterIP 10.233.28.70 \u0026lt;none\u0026gt; 50000/TCP 9d NAME READY AGE statefulset.apps/jenkins 1/1 9d 브라우저에서 \u0026lt;도메인\u0026gt;:8080으로 접속 DNS 해석되어 공인IP:8080으로 라우팅 공인IP:8080는 HAproxy 서버 HAproxy frontend 설정으로 8080포트로 들어오는 요청을 HAproxy backend로 라우팅 HAproxy backend는 172.27.1.100:80 172.27.1.100 IP는 MetalLB의 IP 풀에서부터 할당받은 Nginx Controller의 IP Nginx Ingress Controller는 LoadBalancer 타입 Service Nginx Ingress Controller는 HAproxy backend로부터 받은 트래픽의 Host 헤더가 Jenkins Ingress에 정의된 Host인 jenkins.local인지 확인 Jenkins Ingress에 정의된 규칙에 따라 Jenkins Service로 라우팅 Jenkins Service에서 Jenkins Pod로 라우팅 ","date":"2025-08-24T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EC%99%B8%EB%B6%80%EC%97%90%EC%84%9C-%EC%9E%90%EC%B2%B4-%EA%B4%80%EB%A6%AC%ED%98%95-kubernetes-%EC%A0%91%EA%B7%BC%EC%9D%84-%EC%9C%84%ED%95%9C-metallb/haproxy-%EC%84%A4%EC%A0%95%EA%B3%BC-%ED%86%B5%EC%8B%A0-%EA%B5%AC%EC%A1%B0/","title":"외부에서 자체 관리형 Kubernetes 접근을 위한 MetalLB/HAproxy 설정과 통신 구조"},{"content":"오토스케일링 종류 클러스터 레벨 오토스케일링 노드 풀의 VM 개수를 오토스케일링 클라우드 프로바이더와 같은 환경에서만 가능(EKS, GKE, AKS 등) 파드 레벨 오토스케일링 파드의 개수를 오토스케일링 수평적 파드 오토스케일링(HPA) metrics-server 애드온이 필요 각 노드/파드가 사용하는 매트릭(CPU/MEM) 모니터링하고 데이터 수집하는 역할 동작원리\nHPA 컨트롤러가 metrics-server에서 Pod 매트릭을 주기적으로 수집하고, 사전에 설정한 임계치 조건을 만족하면 파드의 개수를 자동으로 조정 HPA 동작 조건\n기본값 30초 간격으로 Pod 사용량을 점검\n임계값을 초과하면 Pod scale out\n확장된 이후 3분 대기, scale in은 확장 이후 5분 대기\n예시\nmyapp Deployment가 CPU 사용 평균 50%를 넘기면 Pod 개수가 늘어남 만약 CPU 사용률이 낮아지면, 5분 이후 Pod 개수를 줄임 Pod 개수는 최소 2개, 최대 10개 사이에서 조정됨 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: myapp-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: myapp minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 50 애플리케이션 지표 기반 오토스케일링도 가능하지만, prometheus와 같은 툴과 연동 필요\n","date":"2025-08-22T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-autoscaling-overview/","title":"Kubernetes Autoscaling Overview"},{"content":" Pod에서 볼륨 선언 → 마운트가 기본 패턴 spec.volumes에서 볼륨 선언 후 spec.containers.volumeMounts 에서 마운트 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: v1 kind: Pod metadata: name: demo-pod spec: containers: - name: app image: nginx:1.25 volumeMounts: - name: html mountPath: /usr/share/nginx/html volumes: - name: html hostPath: path: /home/ type: DirectoryOrCreate 볼륨 유형 타입 사용 용도 비고 emptyDir Pod 수명=데이터 수명. 임시 공간 Pod 종료 시 데이터 없어짐 hostPath 노드 로컬 경로를 Pod에 마운트 데이터가 특정 워커노드에 종속됨 configMap / secret 변수를 파일로 주입 주로 읽기 전용으로 마운트 nfs NAS 마운트 멀티 Pod 간 데이터 공유에 사용 CSI 기반 블록 / 파일 AWS EDB, GCE PD 등 대부분 CSI 드라이버로 제공 동적 프로비저닝 가능 예시: emptyDir / configMap / secret 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # emptyDir volumes: - name: work emptyDir: {} # configMap volumes: - name: app-config configMap: name: my-config # secret volumes: - name: app-secret secret: secretName: db-cred PV \u0026amp; PVC PV(PersistentVolume): 클러스터에 영구 스토리지 리소스를 생성 PVC(PersistentVolumeClaim): 생성한 PV에서 자원 할당 요청 접근 모드\nReadWriteOnce(RWO): 한 노드에서 읽기/쓰기 ReadOnlyMany(ROX): 여러 노드에서 읽기 전용 ReadWriteMany(RWX): 여러 노드에서 읽기/쓰기 ex) NFS, EFS pv/pvc 예시 persistentVolumeReclaimPolicy 기본값은 동적 프로비저닝이면 Delete인 경우가 많고, 중요한 데이터면 Retain 고려 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 1) PV: NFS 예시 apiVersion: v1 kind: PersistentVolume metadata: name: pv-nfs spec: capacity: storage: 20Gi accessModes: [ReadWriteMany] persistentVolumeReclaimPolicy: Retain nfs: path: /export/apps server: 10.0.0.10 --- # 2) PVC: 위 PV에 바인딩될 요청 apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc-nfs spec: accessModes: [ReadWriteMany] resources: requests: storage: 20Gi StorageClass 디스크 타입, 복제/백업 정책, 토폴로지 등의 프로비저닝에 필요한 파라미터를 묶어서 제공 사용자는 PVC에서 StorageClassName에만 지정하면 볼륨이 동적 프로비저닝됨 사용을 위해선 NFS Provisioner, EBS Driver와 같은 툴 설치 필요 ","date":"2025-08-21T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-storage-overview/","title":"Kubernetes Storage Overview"},{"content":"Kubernetes kube-proxy: iptables vs IPVS K8S에서 Service를 만들면, 클러스터 내부 Pod는 ClusterIP를 통해 접근 가능 이때 트래픽을 실제 Pod로 보내주는 역할을 하는 게 kube-proxy kube-proxy는 iptables 모드와 IPVS 모드 두가지 모드 중 하나로 동작 가능 iptables 모드 K8S 초창기부터 기본으로 사용된 방식\n동작 원리 service/endpoint 정보를 iptables 규칙에 직접 기록 커널이 NAT 체인을 따라가면서 Pod로 트래픽을 전송 장점 별도 모듈 설치 없이 바로 사용 가능 오래 쓰여온 만큼 안정적이고, 문제 생겼을 때 iptables -t nat -L -n 으로 바로 확인 가능 단점 엔드포인트 수가 많아지면 규칙도 폭발적으로 늘어나 성능 저하가 생길 수 있음 규칙을 갱신할 때 전체 체인을 다시 써야 해서 순간 부하 발생 IPVS 모드 리눅스 커널의 IPVS 기능을 사용\n동작 원리 ip_vs 모듈을 이용해 L4 로드밸런서처럼 동작 kube-proxy는 service/endpoint를 커널에 등록만 해두고, 실제 트래픽 처리는 커널이 빠르게 처리 장점 수천~수만 엔드포인트까지 안정적으로 처리 가능 엔드포인트 변경 시 필요한 부분만 업데이트 → 부하 적음 라운드로빈, 세션 해시 등 다양한 로드밸런싱 알고리즘 지원 단점 커널 모듈(ip_vs)과 유틸리티(ipvsadm, ipset) 설치 필요 iptables에 비해 디버깅이 낯설 수 있음 (ipvsadm -Ln 확인 필요) 선택 기준 iptables 모드 수십~수백 노드, 수천 개 엔드포인트 수준 운영 단순성, 문제 대응 속도 중시 IPVS 모드 수천 노드 이상, 서비스 엔드포인트 수가 수만 단위 대규모 SaaS 환경, 트래픽이 매우 많은 서비스 고급 로드밸런싱(세션 어피니티 등) 필요한 경우 kubespray는 기본적으로 IPVS로 동작\n","date":"2025-08-19T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kube-proxy-%EB%AA%A8%EB%93%9Ciptables-vs-ipvs/","title":"kube-proxy 모드(iptables vs IPVS)"},{"content":" Kubernetes에서 애플리케이션을 배포할 때 환경변수나 설정 등을 관리하는 방법 Pod yaml에 직접 값을 적을수도 있지만 재사용/보안을 위해 ConfigMap/Secret을 사용 Configmap key-value 쌍으로 정의\nDB 호스트, 포트, 로그 레벨 등의 민감하지 않은 설정 값을 저장할 때 사용\n예시 - 환경변수\nConfigMap\n1 2 3 4 5 6 7 8 apiVersion: v1 kind: ConfigMap metadata: name: app-config namespace: demo data: APP_MODE: \u0026#34;production\u0026#34; LOG_LEVEL: \u0026#34;debug\u0026#34; Pod\n1 2 3 4 5 6 7 8 9 10 11 12 apiVersion: v1 kind: Pod metadata: name: sample-pod namespace: demo spec: containers: - name: sample-app image: nginx envFrom: - configMapRef: name: app-config 예시 - 파일 형태로 마운트\nConfigMap\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 apiVersion: v1 kind: ConfigMap metadata: name: nginx-config data: nginx.conf: | events {} http { server { listen 80; location / { return 200 \u0026#39;Hello ConfigMap!\u0026#39;; } } } Pod\n1 2 3 4 5 6 7 8 9 10 11 volumes: - name: config-volume configMap: name: nginx-config containers: - name: nginx image: nginx volumeMounts: - mountPath: /etc/nginx/nginx.conf subPath: nginx.conf name: config-volume Secret key-value 쌍으로 정의\nBase64로 인코딩하여 저장\n비밀번호, 인증 토큰, TLS 키 등 민감한 데이터를 저장할 때 사용\nAWS Secrets Manager, Vault와 같은 Secret CSI Driver을 연동해서 사용 가능\n예시 - 환경변수\nConfigMap\n1 2 3 4 5 6 7 8 apiVersion: v1 kind: Secret metadata: name: db-secret type: Opaque data: username: ZGJ1c2Vy # \u0026#34;dbuser\u0026#34; → base64 인코딩 password: c2VjcmV0MTIz # \u0026#34;secret123\u0026#34; Pod\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 containers: - name: db-client image: postgres env: - name: DB_USER valueFrom: secretKeyRef: name: db-secret key: username - name: DB_PASS valueFrom: secretKeyRef: name: db-secret key: password 예시 - 도커 레지스트리 인증 정보\nSecret\n1 2 3 4 5 6 7 apiVersion: v1 kind: Secret metadata: name: regcred type: kubernetes.io/dockerconfigjson data: .dockerconfigjson: \u0026lt;Base64-encoded-json\u0026gt; ","date":"2025-08-17T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-configmap%EA%B3%BC-secret/","title":"Kubernetes ConfigMap과 Secret"},{"content":"LoadBalancer L4 - NLB + Service의 LoadBalancer\n웹서버가 아닌 백엔드 서비스(API, DB 프록시 등)에 대한 부하 분산에 적합\nTCP/UDP 레벨에서 패킷을 라우팅\nL7 - ALB + Ingress\n웹 애플리케이션 HTTP/HTTPS 트래픽 부하 분산에 적합\nURL 경로, 호스트명 등 애플리케이션 계층의 규칙 기반 라우팅 가능\nIngress 트래픽 부하 분산 (HTTP/HTTPS)\n경로 기반 라우팅\n예: /, /home, /pay, /login\n도메인 기반 라우팅\n예: www.example.com, www.test.com\nSSL/TLS 인증서 처리\nHTTPS 통신을 위한 인증서 설치 및 자동 갱신(Let\u0026rsquo;s Encrypt 등과 연계 가능)\n경로 기반 라우팅 예시\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: path-based-ingress spec: ingressClassName: nginx rules: - host: shop.example.com http: paths: - path: /products pathType: Prefix backend: service: name: product-service port: number: 80 - path: /orders pathType: Prefix backend: service: name: order-service port: number: 80 호스트 기반 라우팅 예시\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: host-based-ingress spec: ingressClassName: nginx rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: api-service port: number: 80 - host: admin.example.com http: paths: - path: / pathType: Prefix backend: service: name: admin-service port: number: 80 Ingress Architecture Client(HTTP/HTTPS) → Ingress Controller(Ingress Rules 라우팅) → Service(ClusterIP) → Pod\nIngress Controller Ingress 리소스에 정의된 Ingress Rules를 읽고 동작 Input: Output: :80 실제 트래픽은 Service(ClusterIP)를 통해 Pod로 전달 Service (ClusterIP) Input: :80 Output: \u0026lt;pod’s label\u0026gt;:80 Deployment(ReplicaSet, Pod) 여러 Pod를 레플리카로 생성해 무중단 운영 가능 Ingress Controller 종류 로컬(Kubernetes On-Prem, Minikube 등) → NGINX Ingress Controller 사용 AWS EKS → AWS Load Balancer Controller(ALB 기반) 사용 ALB와 연동해 L7 라우팅 처리 ACM(AWS Certificate Manager)로 TLS 인증서 관리 가능 ","date":"2025-08-17T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-label%EA%B3%BC-annotation/","title":"Kubernetes Label과 Annotation"},{"content":"LoadBalancer L4 - NLB + Service의 LoadBalancer\n웹서버가 아닌 백엔드 서비스(API, DB 프록시 등)에 대한 부하 분산에 적합\nTCP/UDP 레벨에서 패킷을 라우팅\nL7 - ALB + Ingress\n웹 애플리케이션 HTTP/HTTPS 트래픽 부하 분산에 적합\nURL 경로, 호스트명 등 애플리케이션 계층의 규칙 기반 라우팅 가능\nIngress 트래픽 부하 분산 (HTTP/HTTPS)\n경로 기반 라우팅\n예: /, /home, /pay, /login\n도메인 기반 라우팅\n예: www.example.com, www.test.com\nSSL/TLS 인증서 처리\nHTTPS 통신을 위한 인증서 설치 및 자동 갱신(Let\u0026rsquo;s Encrypt 등과 연계 가능)\n경로 기반 라우팅 예시\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: path-based-ingress spec: ingressClassName: nginx rules: - host: shop.example.com http: paths: - path: /products pathType: Prefix backend: service: name: product-service port: number: 80 - path: /orders pathType: Prefix backend: service: name: order-service port: number: 80 호스트 기반 라우팅 예시\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: host-based-ingress spec: ingressClassName: nginx rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: api-service port: number: 80 - host: admin.example.com http: paths: - path: / pathType: Prefix backend: service: name: admin-service port: number: 80 Ingress Architecture Client(HTTP/HTTPS) → Ingress Controller(Ingress Rules 라우팅) → Service(ClusterIP) → Pod\nIngress Controller Ingress 리소스에 정의된 Ingress Rules를 읽고 동작 Input: Output: :80 실제 트래픽은 Service(ClusterIP)를 통해 Pod로 전달 Service (ClusterIP) Input: :80 Output: \u0026lt;pod’s label\u0026gt;:80 Deployment(ReplicaSet, Pod) 여러 Pod를 레플리카로 생성해 무중단 운영 가능 Ingress Controller 종류 로컬(Kubernetes On-Prem, Minikube 등) → NGINX Ingress Controller 사용 AWS EKS → AWS Load Balancer Controller(ALB 기반) 사용 ALB와 연동해 L7 라우팅 처리 ACM(AWS Certificate Manager)로 TLS 인증서 관리 가능 ","date":"2025-08-13T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-ingress-overview/","title":"Kubernetes Ingress Overview"},{"content":" Pod 그룹에 대해 단일 진입점(VIP) 제공 외부에서 클러스터 내에 파드 그룹에 접근할 수 있도록 기능 제공 서비스 종류 ClusterIP ⇒ (외부 → 내부) VIP NodePort ⇒ (외부 → 내부) VIP + NodePort LoadBalancer ⇒ (외부 → 내부) VIP + NodePort + LB IP ExternalName ⇒ (내부 → 외부) 도메인 매핑 ClusterIP Pod그룹에 대해 단일 진입점(VIP)을 제공\nVIP는 사설 IP로 제공됨\n~/root/kubespray/inventory/mycluster/group_vars/k8s_cluster\n1 2 kube_service_addresses: 10.233.0.0/18 kube_pods_subnet: 10.233.64.0/ 예시\n1 2 3 4 5 6 7 8 9 10 11 apiVersion: v1 kind: Service metadata: name: my-clusterip-service spec: selector: app: myapp ports: - port: 80 targetPort: 8080 type: ClusterIP NodePort Pod그룹에 대해 단일 진입점(VIP)을 제공\n노드의 IP:Port(30000 ~ 32727)로 요청하면 Pod 그룹(같은 Label)에 대한 부하 분산을 제공\n예시\n1 2 3 4 5 6 7 8 9 10 11 12 apiVersion: v1 kind: Service metadata: name: my-nodeport-service spec: selector: app: myapp ports: - port: 80 targetPort: 8080 nodePort: 30080 type: NodePort LoadBalancer Pod그룹에 대해 단일 진입점(VIP)을 제공\n노드의 IP:Port(30000 ~ 32727)로 요청하면 Pod 그룹(같은 Label)에 대한 부하 분산을 제공\n클라우드 환경(AWS ELB, GCP LB, Azure LB) 또는 MetalLB/HAProxy 등의 On-premise LB 사용\n예시\n1 2 3 4 5 6 7 8 9 10 11 apiVersion: v1 kind: Service metadata: name: my-lb-service spec: selector: app: myapp ports: - port: 80 targetPort: 8080 type: LoadBalancer ExternalName - DNS CNAME 역할 (내부 → 외부/내부) DNS CNAME 등록 작업(서비스 이름 ← 매핑 → DNS 이름) (pod 예) .default.pod.cluster.local ← 매핑 → google.com (service 예) .default.svc.cluster.local ← 매핑 → google.com\n예시\n1 2 3 4 5 6 7 apiVersion: v1 kind: Service metadata: name: my-external-service spec: type: ExternalName externalName: google.com 헤드리스 서비스 단일 진입점이 필요 없는 경우에 사용 (clusterIP: None)\n각 Pod의 IP가 DNS A 레코드로 직접 등록됨\n주로 StatefulSet(DB, Kafka 등)에서 사용\n예시\n1 2 3 4 5 6 7 8 9 10 11 apiVersion: v1 kind: Service metadata: name: my-headless-service spec: clusterIP: None selector: app: myapp ports: - port: 80 targetPort: 8080 네트워크 kube-proxy worker node안에 pod에 연결할 때 사용하는 네트워크를 담당 Masquerading (IP 변환) Load Balancing (포트포워딩) kube-proxy 모드 iptables 모드 ⇒ iptables CMD ipvs 모드 ⇒ ipvsadm CMD ","date":"2025-08-12T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-service-overview/","title":"Kubernetes Service Overview"},{"content":"ReplicaSet 동일한 스펙의 Pod를 지정된 개수만큼 유지 집합 기반 셀렉터(Set-based selector) rolling update 지원 직접 쓰기보다는 Deployment 내부에서 주로 사용됨 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 apiVersion: apps/v1 kind: ReplicaSet metadata: name: my-replicaset spec: replicas: 3 selector: matchExpressions: - key: app operator: In values: [\u0026#34;web\u0026#34;, \u0026#34;api\u0026#34;] template: metadata: labels: app: web spec: containers: - name: nginx image: nginx:1.27 Deployment ReplicaSet을 관리하며 Pod의 개수를 보장 Pod에 대한 rolling update/rollback 기능 지원 무중단 파드 배포를 위해 사용 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 apiVersion: apps/v1 kind: Deployment metadata: name: my-deployment spec: replicas: 3 selector: matchLabels: app: web strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 maxSurge: 1 template: metadata: labels: app: web spec: containers: - name: nginx image: nginx:1.27 DaemonSet 하나의 노드당 1개의 Pod 실행(controlplane 제외) Rolling Update / Rollback 기능 지원 데몬셋 용도: (관리) ex) 모니터링 Pod 실행 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: apps/v1 kind: DaemonSet metadata: name: node-monitor spec: selector: matchLabels: app: node-monitor template: metadata: labels: app: node-monitor spec: containers: - name: node-exporter image: prom/node-exporter:latest StatefulSet State가 필요한 워크로드에 사용(Pod 이름, 스토리지 고정) 고유 네트워크 ID, 안정적인 스토리지, 순차 배포/종료 보장 주로 DB, 메시지 큐, Zookeeper, Kafka 등에 사용 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 apiVersion: apps/v1 kind: StatefulSet metadata: name: web-stateful spec: serviceName: \u0026#34;web\u0026#34; replicas: 3 selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: - name: nginx image: nginx:1.27 volumeClaimTemplates: - metadata: name: www spec: accessModes: [\u0026#34;ReadWriteOnce\u0026#34;] resources: requests: storage: 1Gi Job 트랜잭션의 단위 잡이 성공적으로 종료 될 때 까지 지속적으로 파드 실행 잡이 성공하면 completed 잡은 실패하면 종료 후 다시 실행 1 2 3 4 5 6 7 8 9 10 11 12 13 apiVersion: batch/v1 kind: Job metadata: name: data-migration spec: backoffLimit: 3 template: spec: containers: - name: migrate image: busybox command: [\u0026#34;sh\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;echo Running migration... \u0026amp;\u0026amp; sleep 10\u0026#34;] restartPolicy: OnFailure CronJob Job 컨트롤러를 사용하여 애플리케이션 파드를 주기적으로 반복해서 실행 Job이 종료되면 스케쥴링에 의해 주기적으로 실행 Job이 비정상 종료되면 다시 실행 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 apiVersion: batch/v1 kind: CronJob metadata: name: db-backup spec: schedule: \u0026#34;0 2 * * *\u0026#34; # 매일 새벽 2시 jobTemplate: spec: template: spec: containers: - name: backup image: busybox command: [\u0026#34;sh\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;echo Backing up DB... \u0026amp;\u0026amp; sleep 5\u0026#34;] restartPolicy: OnFailure ","date":"2025-08-11T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-workload-overview/","title":"Kubernetes Workload Overview"},{"content":"Pod 컨테이너를 추상화 시켜 놓은 단위 k8s에서 배포하는 최소 단위 하나의 Pod에는 여러 Container가 들어 갈 수 있다 Pod는 하나의 IP를 갖는다 Multi-Container Pod\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Liveness Probe - httpGet - livenessProbe: httpGet: path: / port: 80 - tcpSocket - livenessProbe: tcpSocket: port: 22 - exec: - livenessProbe: exec: command: - ls - /tmp/datafile Liveness Probe Parameter - initialDelaySeconds: 0 - periodSeconds: 5 - timeoutSeconds: 1 - successThreshold: 1 - failureThreshold: 2 initcontainer 앱 컨테이너가 동작하기 위한 전초 작업을 위한 컨테이너\n(WEB-DB) 환경의 WEB 컨테이너에서 DB 컨테이너가 기동되었는지를 위한 작업 (Network) 네트워크 인프라 점검을 위한 전초 작업 1 2 3 4 5 6 7 8 9 10 11 12 spec: containers: - name: myapp-container image: busybox:1.28 command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#39;echo The app is running! \u0026amp;\u0026amp; sleep 3600\u0026#39;] initContainers: - name: init-myservice image: busybox:1.28 command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#34;until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done\u0026#34;] - name: init-mydb image: busybox:1.28 command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#34;until nslookup mydb.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 2; done\u0026#34;] Infra/pause container 앱컨테이너의 인프라를 생성 및 삭제를 담당하는 컨테이너 Static Pod /var/lib/kubelet/config.yaml staticPodPath: /etc/kubernetes/manifests Pod 자원 할당 자원 할당\ncpu(단위: 1core/1vCPU = 1000m) mem(단위: 500Mi, 1Gi, ..) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 apiVersion: v1 kind: Pod metadata: name: nginx-pod-resources spec: containers: - name: nginx-container image: nginx:1.14 ports: - containerPort: 80 protocol: TCP resources: requests: cpu: 200m memory: 250Mi limits: cpu: 1 memory: 1Gi Pod 환경 변수 환경 변수 설정 1 2 3 4 5 6 7 8 9 10 11 12 13 14 kind: Pod apiVersion: v1 metadata: name: mysql-env-pod spec: containers: - name: mysql-container image: mysql ports: - containerPort: 3306 protocol: TCP env: - name: MYSQL_ROOT_PASSWORD value: \u0026#34;password\u0026#34; 디자인 패턴 사이드카 - 협업 어댑터 - 입력 표준화 앰버서더 - 출력 표준화 ","date":"2025-08-06T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-pod-overview/","title":"Kubernetes Pod Overview"},{"content":"Nginx Ingress Controller k8s 외부에서 들어오는 http/https 트래픽을 1차적으로 처리하는 Entry Point\nIngress Controller가 없다면 NodePort Service 만으로 Pod를 열어야한다.\nIngress 리소스를 통해 내부 서비스로 라우팅\nTLS Termination, 리버스 프록시, 로드 밸린싱 등 수행 가능\nhttps://docs.nginx.com/nginx-ingress-controller/installation/installing-nic/installation-with-helm/\nhelm을 통한 설치 1 helm install my-release oci://ghcr.io/nginx/charts/nginx-ingress --version 2.2.1 helm을 통한 설치 시 옵션 수정이 필요한 경우 하기 명령어로 pull한 helm chart의 values.yaml에서 옵션 수정 1 helm pull oci://ghcr.io/nginx/charts/nginx-ingress --untar --version 2.2.1 수정한 옵션 적용하여 설치 1 helm install my-release . ","date":"2025-08-05T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90/","title":"Kubernetes 아키텍쳐"},{"content":"Master Node kube-apiserver Kubernetes 클러스터의 프론트엔드입니다. REST API를 통해 클러스터 상태를 읽고 조작하는 모든 요청을 처리합니다. kubectl kubectl은 kube-apiserver에 요청을 보냅니다. (예: kubectl get nodes) 또는, kubectl을 사용하지 않고도 직접 kube-apiserver에 요청을 보낼 수 있습니다. controller-manager 클러스터의 상태를 지속적으로 모니터링하고, 원하는 상태를 유지합니다. 다양한 컨트롤러들(잡, 노드, 서비스 계정, 엔드포인트)을 관리하여 리소스의 상태를 조정합니다. kube-scheduler 스케줄러는 새로 생성된 Pod를 워커 노드에 배치하는 역할을 합니다. Pod의 리소스 요구사항과 각 노드의 리소스 가용성을 고려해 최적의 노드에 배치합니다. ETCD Kubernetes의 클러스터 데이터를 저장하는 분산 key-value 데이터베이스입니다. 클러스터의 현재 상태, 구성 정보, 시크릿, ConfigMap 등을 저장하는 데 사용됩니다. Worker Node kubelet 각 워커 노드에서 실행되며, API 서버와 통신하면서 Pod의 라이프사이클을 관리합니다. kube-scheduler가 할당한 작업을 워커 노드에서 실행합니다. kube-proxy 네트워크 트래픽을 라우팅하고 로드밸런싱하는 데 사용되는 기본 네트워크 프록시입니다. Pod과 서비스 간의 통신 규칙을 설정합니다. container runtime 실제 컨테이너를 실행하는 소프트웨어입니다. Containerd, CRI-O 등이 Kubernetes에서 사용될 수 있으며, 컨테이너 이미지를 다운로드하고 실행, 관리하는 역할을 합니다. Addons CoreDNS 클러스터 내에서 서비스 디스커버리와 DNS 네임 리졸버를 제공합니다 이를 통해 서비스 이름을 클러스터 내부의 IP로 변환할 수 있습니다. CNI 네트워크를 관리하기 위한 표준 인터페이스입니다. 각 Pod에 IP 주소를 할당해서, 다른 Pod와의 통신을 가능하게 만듭니다. 대표적인 플러그인으로는 Calico, Flannel 등이 있습니다. ","date":"2025-08-04T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90/","title":"Kubernetes 아키텍쳐"},{"content":" Kubernetes에서 사용하는 여러 플러그인 및 툴의 yaml 파일들을 패키징하여 쉽게 설치할 수 있는 툴 values.yaml을 통해 툴들의 옵션 조정 가능 1 2 3 4 cd curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 chmod 700 get_helm.sh ./get_helm.sh --version v3.17.4 3.18.5 버전에서 helm 설치 시에 values.schema.json이 원격 $ref(HTTPS)로 참조하는 JSON Schema를 불러오면 검증 단계에서 실패하는 이슈가 있어서 3.17.4 버전 명시\nhttps://github.com/helm/helm/issues/31136?utm_source=chatgpt.com 1 2 3 4 5 6 [root@master1 ~]# helm install my-release oci://ghcr.io/nginx/charts/nginx-ingress --version 2.2.1 Pulled: ghcr.io/nginx/charts/nginx-ingress:2.2.1 Digest: sha256:00f7a7017799eafc7f109cca10204a9dc2bc2d2f0feafb8eff328d13e2cedd12 Error: INSTALLATION FAILED: values don\u0026#39;t meet the specifications of the schema(s) in the following chart(s): nginx-ingress: failing loading \u0026#34;https://raw.githubusercontent.com/nginxinc/kubernetes-json-schema/master/v1.33.1/_definitions.json\u0026#34;: invalid file url: https://raw.githubusercontent.com/nginxinc/kubernetes-json-schema/master/v1.33.1/_definitions.json ","date":"2025-08-03T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubernetes-helm-%EC%84%A4%EC%B9%98/","title":"Kubernetes Helm 설치"},{"content":" AWS의 경우 EBS Driver를 통해 pvc를 위한 StorageClass를 사용 가능 로컬 Kubernetes에서도 StorageClass를 사용하기 위해 NFS 서버를 구축해서 사용 NFS 서버 구축 1 2 3 4 5 6 7 8 9 dnf install -y nfs-utils systemctl enable --now nfs-server mkdir -p /exports/k8s-pv chown -R nobody:nobody /exports/k8s-pv chmod 777 /exports/k8s-pv echo \u0026#34;/exports/k8s-pv *(rw,sync,no_subtree_check,no_root_squash)\u0026#34; | sudo tee -a /etc/exports exportfs -rav Helm을 통한 NFS Provisioner 설치 1 2 3 4 helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/ helm repo update helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner --set nfs.server=172.27.1.9 --set nfs.path=/exports/k8s-pv --set storageClass.name=nfs-sc --set storageClass.defaultClass=true pvc 생성 1 2 3 4 5 6 7 8 9 10 11 apiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-pvc spec: accessModes: - ReadWriteMany resources: requests: storage: 1Gi storageClassName: nfs-sc pod 생성 1 2 3 4 5 6 7 8 9 10 11 apiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-pvc spec: accessModes: - ReadWriteMany resources: requests: storage: 1Gi storageClassName: nfs-sc ","date":"2025-07-31T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EC%9E%90%EC%B2%B4-%EA%B4%80%EB%A6%AC%ED%98%95-kubernetes%EC%97%90%EC%84%9C-nfs%EB%A5%BC-%ED%86%B5%ED%95%9C-storageclass-%EC%82%AC%EC%9A%A9/","title":"자체 관리형 Kubernetes에서 NFS를 통한 StorageClass 사용"},{"content":" Containerd 사용 Git 소스 코드 1 2 dnf install -y git-all git clone https://github.com/scline/docker-cacti.git Containerd 환경 구축 https://containerd.io/downloads/ 여기서 다운로드도 가능\nhttps://medium.com/@DannielWhatever/using-containerd-without-docker-9d08332781b4\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo dnf remove $pkg -y; done dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo dnf install -y containerd.io containerd config default | sudo tee /etc/containerd/config.toml ### containerd root 디렉토리 외에 data 디렉토리에서 동작하도록 설정 sed -i \u0026#39;s|^root = .*|root = \u0026#34;/data/container/containerd\u0026#34;|\u0026#39; /etc/containerd/config.toml sed -i \u0026#39;s|^state = .*|state = \u0026#34;/data/container/containerd-state\u0026#34;|\u0026#39; /etc/containerd/config.toml mkdir -p /data/container/containerd mkdir -p /data/container/containerd-state mkdir -p /data/container/nerdctl echo \u0026#39;export NERDCTL_DATA_ROOT=/data/container/nerdctl\u0026#39; \u0026gt;\u0026gt; ~/.bashrc source ~/.bashrc # nerdctl 설치 wget https://github.com/containerd/nerdctl/releases/download/v0.17.0/nerdctl-0.17.0-linux-amd64.tar.gz tar Cxzvvf /usr/bin nerdctl-0.17.0-linux-amd64.tar.gz echo \u0026#34;source \u0026lt;(nerdctl completion bash)\u0026#34; \u0026gt;\u0026gt; ~/.bashrc # CNI 설치 wget https://github.com/containernetworking/plugins/releases/download/v1.0.1/cni-plugins-linux-amd64-v1.0.1.tgz mkdir -p /opt/cni/bin tar Cxzvvf /opt/cni/bin cni-plugins-linux-amd64-v1.0.1.tgz # buildkit 설치 wget https://github.com/moby/buildkit/releases/download/v0.10.0-rc1/buildkit-v0.10.0-rc1.linux-amd64.tar.gz tar Cxzvvf /usr/bin buildkit-v0.10.0-rc1.linux-amd64.tar.gz nohup /usr/bin/builkitd \u0026lt; /dev/null \u0026gt; /var/log/buildkitd 2\u0026gt;\u0026amp;1 \u0026amp; systemctl enable --now containerd Cacti Container 구조 cacti master와 master용 DB\ncacti poller와 poller용 DB\ndocker-compose-master.yaml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 version: \u0026#39;3.5\u0026#39; services: cacti-master: image: \u0026#34;smcline06/cacti\u0026#34; container_name: cacti_master hostname: cactimaster depends_on: - db-master ports: - \u0026#34;80:80\u0026#34; - \u0026#34;443:443\u0026#34; environment: - DB_NAME=cacti_master - DB_USER=cactiuser - DB_PASS=cactipassword - DB_HOST=db-master - DB_PORT=3306 - DB_ROOT_PASS=rootpassword - INITIALIZE_DB=1 - TZ=Asia/Seoul volumes: - cacti-master-data:/cacti - cacti-shared-rra:/cacti/rra - cacti-master-spine:/spine - cacti-master-backups:/backups networks: - cacti-net db-master: image: \u0026#34;mariadb:10.3\u0026#34; container_name: cacti_master_db hostname: db-master ports: - \u0026#34;3306:3306\u0026#34; command: - mysqld - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --max_connections=200 - --max_heap_table_size=128M - --max_allowed_packet=32M - --tmp_table_size=128M - --join_buffer_size=128M - --innodb_buffer_pool_size=1G - --innodb_doublewrite=ON - --innodb_flush_log_at_timeout=3 - --innodb_read_io_threads=32 - --innodb_write_io_threads=16 - --innodb_buffer_pool_instances=9 - --innodb_file_format=Barracuda - --innodb_large_prefix=1 - --innodb_io_capacity=5000 - --innodb_io_capacity_max=10000 environment: - MYSQL_ROOT_PASSWORD=rootpassword - TZ=Asia/Seoul volumes: - cacti-db-master:/var/lib/mysql networks: - cacti-net volumes: cacti-db-master: cacti-master-data: cacti-shared-rra: cacti-master-spine: cacti-master-backups: networks: cacti-net: name: cacti-net external: true docker-compose-poller.yaml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 version: \u0026#39;3.5\u0026#39; services: cacti-poller: image: \u0026#34;smcline06/cacti\u0026#34; container_name: cacti_poller hostname: cactipoller depends_on: - db-poller ports: - \u0026#34;8080:80\u0026#34; - \u0026#34;8443:443\u0026#34; environment: - DB_NAME=cacti_poller - DB_USER=cactipolleruser - DB_PASS=cactipollerpassword - DB_HOST=db-poller - DB_PORT=3306 - RDB_NAME=cacti_master - RDB_USER=cactiuser - RDB_PASS=cactipassword - RDB_HOST=db-master - RDB_PORT=3306 - DB_ROOT_PASS=rootpassword - REMOTE_POLLER=1 - INITIALIZE_DB=1 - TZ=Asia/Seoul volumes: - cacti-poller-data:/cacti - cacti-shared-rra:/cacti/rra - cacti-poller-spine:/spine - cacti-poller-backups:/backups networks: - cacti-net db-poller: image: \u0026#34;mariadb:10.3\u0026#34; container_name: cacti_poller_db hostname: db-poller command: - mysqld - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --max_connections=200 - --max_heap_table_size=128M - --max_allowed_packet=32M - --tmp_table_size=128M - --join_buffer_size=128M - --innodb_buffer_pool_size=1G - --innodb_doublewrite=ON - --innodb_flush_log_at_timeout=3 - --innodb_read_io_threads=32 - --innodb_write_io_threads=16 - --innodb_buffer_pool_instances=9 - --innodb_file_format=Barracuda - --innodb_large_prefix=1 - --innodb_io_capacity=5000 - --innodb_io_capacity_max=10000 environment: - MYSQL_ROOT_PASSWORD=rootpassword - TZ=Asia/Seoul volumes: - cacti-db-poller:/var/lib/mysql networks: - cacti-net volumes: cacti-db-poller: cacti-poller-data: cacti-shared-rra: name: containerd-cacti_cacti-shared-rra external: true cacti-poller-spine: cacti-poller-backups: networks: cacti-net: name: cacti-net external: true cacti-net CNI\nip 지정해서 쓰기 위한 사전 세팅 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 cat \u0026lt;\u0026lt;EOF \u0026gt; /etc/cni/net.d/cacti-net.conflist { \u0026#34;cniVersion\u0026#34;: \u0026#34;0.4.0\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;cacti-net\u0026#34;, \u0026#34;nerdctlID\u0026#34;: 2, \u0026#34;nerdctlLabels\u0026#34;: { \u0026#34;com.docker.compose.network\u0026#34;: \u0026#34;cacti-net\u0026#34;, \u0026#34;com.docker.compose.project\u0026#34;: \u0026#34;cacti\u0026#34; }, \u0026#34;plugins\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;bridge\u0026#34;, \u0026#34;bridge\u0026#34;: \u0026#34;cactibr0\u0026#34;, \u0026#34;isGateway\u0026#34;: true, \u0026#34;ipMasq\u0026#34;: true, \u0026#34;ipam\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;host-local\u0026#34;, \u0026#34;ranges\u0026#34;: [ [ { \u0026#34;subnet\u0026#34;: \u0026#34;10.99.0.0/24\u0026#34;, \u0026#34;gateway\u0026#34;: \u0026#34;10.99.0.1\u0026#34; } ] ], \u0026#34;routes\u0026#34;: [ { \u0026#34;dst\u0026#34;: \u0026#34;0.0.0.0/0\u0026#34; } ] } }, { \u0026#34;type\u0026#34;: \u0026#34;portmap\u0026#34;, \u0026#34;capabilities\u0026#34;: { \u0026#34;portMappings\u0026#34;: true } }, { \u0026#34;type\u0026#34;: \u0026#34;firewall\u0026#34; }, { \u0026#34;type\u0026#34;: \u0026#34;tuning\u0026#34; } ] } EOF cacti 컨테이너 실행 1 2 3 4 5 nerdctl compose -f docker-compose-master.yaml up -d nerdctl compose -f docker-compose-poller.yaml up -d # netstat으로 port 확인 안됨 iptables -t nat -L -n -v SNMP 체크 1 snmpwalk -v2c -c public \u0026lt;ip\u0026gt;:\u0026lt;port\u0026gt; system ","date":"2025-07-30T09:00:00+09:00","permalink":"https://joonlog.github.io/p/cacti-conainer-%EA%B5%AC%EC%B6%95/","title":"Cacti Conainer 구축"},{"content":"Backup 백업\n1 docker exec \u0026lt;docker image ID or name\u0026gt; ./backup.sh 루트 Cacti/Spine 디렉토리 복사 및 cacti DB 덤프 수행 cacti container의 /backups 디렉토리 아래에 tar.gz 형식으로 압축 백업 저장 BACKUP_RETENTION 값 조정. 기본값 7개 백업 복원\n1 docker exec \u0026lt;docker image ID or name\u0026gt; ./restore.sh /backups/\u0026lt;filename\u0026gt; 백업 목록 조회\n1 docker exec \u0026lt;docker image ID or name\u0026gt; ls /backups Update 1 docker exec \u0026lt;docker image ID or name\u0026gt; ./upgrade.sh 버전 지정 시 upgrade.sh 수정 Cacti Version Links Spine Version Links Template 시작 시 스크립트가 가져와서 설치됨 1 2 3 4 5 6 7 8 9 10 11 ├── templates │ ├── template_name.xml │ ├── resource │ │ └── script_queries │ │ └── ... │ │ └── script_server │ │ └── ... │ │ └── snmp_queries │ │ └── ... │ ├── scripts │ │ └── ... Plugins plugins 디렉토리에 플러그인을 넣는 것으로 부팅 시 자동 로드 Cacti GUI에서 플러그인 활성화 필요 컨테이너 동작 후 플러그인 추가하려면 docker volume을 통해 마운트 필요 Settings sql 변경사항을 settings 디렉토리 아래에 배치하여 초기 설치 시 설정 가능 start.sh가 설치 시 모든 *.sql을 병합하기 때문\nex) settings/spine.sql은 spine 활성화하기 위한 sql\n1 2 3 4 5 6 7 -- -- Enable spine poller from docker installation -- REPLACE INTO `%DB_NAME%`.`settings` (`name`, `value`) VALUES(\u0026#39;path_spine\u0026#39;, \u0026#39;/spine/bin/spine\u0026#39;); REPLACE INTO `%DB_NAME%`.`settings` (`name`, `value`) VALUES(\u0026#39;path_spine_config\u0026#39;, \u0026#39;/spine/etc/spine.conf\u0026#39;); REPLACE INTO `%DB_NAME%`.`settings` (`name`, `value`) VALUES(\u0026#39;poller_type\u0026#39;, \u0026#39;2\u0026#39;); ","date":"2025-07-29T09:00:00+09:00","permalink":"https://joonlog.github.io/p/cacti-conainer%EC%9D%98-script-tools/","title":"Cacti Conainer의 Script Tools"},{"content":"Cacti Master 환경 변수 기능 DB_NAME MySQL DB 이름. Cacti, Spine Poller 공유 DB_USER MySQL DB 사용자. Cacti, Spine Poller 공유 DB_PASS MySQL DB 비밀번호. Cacti, Spine Poller 공유 DB_HOST DB IP/FQDN/호스트/컨테이너이름 DB_PORT MySQL DB 포트. 기본값 3306 DB_ROOT_PASS INITIALIZE_DB가 1로 설정된 경우 필요 INITIALIZE_DB 0: false / true: 1. true면 컨테이너는 대상 데이터베이스에 대한 DB_ROOT_PASS를 요구. DB 초기화 설정 TZ 타임존 설정 BACKUP_RETENTION 보관할 백업 파일의 수 REMOTE_POLLER 0: false / true: 1 PHP_MEMORY_LIMIT PHP 메모리 제한 조정, 기본값은 128M PHP_MAX_EXECUTION_TIME PHP 최대 실행 시간 조정, 기본값은 30초 PHP_SNMP 0으로 설정하면 PHP-SNMP 제거. 기본값은 php-snmp 활성화. 일부 스크립트나 snmpv3가 제대로 동작하기 위해 필요 Cacti Pollers 환경 변수 기능 DB_NAME MySQL DB 이름. Cacti, Spine Poller 공유 DB_USER MySQL DB 사용자. Cacti, Spine Poller 공유 DB_PASS MySQL DB 비밀번호. Cacti, Spine Poller 공유 DB_HOST MySQL DB IP/FQDN/호스트/컨테이너이름 DB_PORT MySQL DB 포트. 기본값 3306 CACTI_URL_PATH 기본값은 cacti. ex) http:///cacti. 루트 URL 변경만 허용되므로 “cacti” 만 변경 가능. “ccc/bb” 같은 것 불가능 DB_ROOT_PASS INITIALIZE_DB가 1로 설정된 경우 필요 INITIALIZE_DB 0: false / true: 1. true면 컨테이너는 대상 데이터베이스에 대한 DB_ROOT_PASS를 요구. DB 초기화 설정 TZ 타임존 설정 BACKUP_RETENTION 보관할 백업 파일의 수 REMOTE_POLLER 0: false / true: 1 RDB_NAME Cacti Master MySQL DB 이름. Cacti, Spine Poller 공유 RDB_USER Cacti Master MySQL DB 사용자 RDB_PASS Cacti Master MySQL DB 사용자의 비밀번호 RDB_HOST Cacti Master MySQL DB가 사용하는 IP/FQDN/호스트/컨테이너이름 RDB_PORT Cacti Master MySQL DB 사용 포트 DB 파라미터 MySQL 변수 추천 값 설명 Version \u0026gt;= 5.6 MySQL 5.6+ 및 MariaDB 10.0+은 훌륭한 릴리스 버전으로, 안정성과 성능이 뛰어납니다. 특히 최신 릴리스는 네트워킹 문제를 해결하여 Spine의 신뢰성을 개선합니다. collation_server utf8mb4_unicode_ci 영어 이외의 언어를 사용할 경우, 일부 문자가 1바이트 이상을 차지하므로 utf8mb4_unicode_ci 정렬 타입을 사용하는 것이 중요합니다. character_set_client utf8mb4 영어 이외의 언어를 사용할 경우, 일부 문자가 1바이트 이상을 차지하므로 utf8mb4 문자셋을 사용하는 것이 중요합니다. max_connections \u0026gt;= 100 Spine 데이터 수집기와 로그인 수에 따라 많은 MySQL 연결이 필요할 수 있습니다. Spine의 연결 수 계산법은 다음과 같습니다: total_connections = total_processes * (total_threads + script_servers + 1). 사용자 연결을 위한 여유 공간을 남겨야 합니다. max_heap_table_size \u0026gt;= 10% RAM Cacti 성능 향상 기능을 사용하여 메모리 스토리지 엔진을 선택할 경우, 시스템 메모리 테이블 공간이 부족하기 전에 Performance Booster 버퍼를 플러시해야 합니다. 이 값은 시스템 메모리의 10%로 설정하는 것이 권장되지만, SSD 디스크를 사용하거나 작은 시스템에서는 무시할 수도 있습니다. max_allowed_packet \u0026gt;= 16777216 원격 폴링 기능을 사용할 때, 많은 데이터가 메인 서버에서 원격 폴러로 동기화됩니다. 따라서 이 값을 16M 이상으로 설정해야 합니다. tmp_table_size \u0026gt;= 64M 서브쿼리를 실행할 때, 임시 테이블 크기를 크게 설정하면 메모리 내에 임시 테이블을 유지할 수 있습니다. join_buffer_size \u0026gt;= 64M 조인 연산이 이 크기보다 작으면 메모리에 저장되어 임시 파일로 쓰여지지 않습니다. innodb_file_per_table ON InnoDB 스토리지를 사용할 때, 테이블 공간을 분리하여 관리가 더 간편해집니다. 현재 OFF로 설정되어 있다면 이 기능을 활성화하고 InnoDB 테이블에 대해 ALTER 명령을 실행하여 마이그레이션할 수 있습니다. innodb_file_format Barracuda innodb_file_per_table을 사용할 때, 파일 형식을 Barracuda로 설정해야 합니다. 이는 특정 Cacti 테이블에 필요한 긴 인덱스를 허용합니다. innodb_large_prefix 1 테이블에 매우 큰 인덱스가 있는 경우, Barracuda 파일 형식과 함께 이 값을 1로 설정해야 합니다. 그렇지 않으면 일부 플러그인이 테이블을 제대로 생성하지 못할 수 있습니다. innodb_buffer_pool_size \u0026gt;= 25% RAM InnoDB는 가능한 한 많은 테이블과 인덱스를 시스템 메모리에 보관합니다. 이 값을 충분히 크게 설정하여 테이블과 인덱스를 메모리에 저장하세요. /var/lib/mysql/cacti 디렉토리의 크기를 확인하면 적절한 값을 설정하는 데 도움이 됩니다. innodb_doublewrite ON ZFS 또는 FusionI/O와 같은 내부 저널링 기능을 가진 파일 시스템을 사용하는 경우가 아니라면 이 값을 ON으로 유지하세요. 하지만 시스템 안정성이 매우 높고 백업이 잘 되어 있다면 OFF로 설정하여 데이터베이스 성능을 약 50% 개선할 수 있습니다. innodb_lock_wait_timeout \u0026gt;= 50 비정상적인 쿼리로 인해 데이터베이스가 다른 사용자에게 응답하지 못하는 것을 방지합니다. innodb_flush_log_at_timeout \u0026gt;= 3 높은 I/O 시스템에서 이 값을 1초 이상으로 설정하면 디스크 I/O가 더 순차적으로 작동할 수 있습니다. innodb_read_io_threads \u0026gt;= 32 최신 SSD 스토리지에서는 높은 I/O 특성을 가진 애플리케이션에 다중 읽기 I/O 스레드가 유리합니다. innodb_write_io_threads \u0026gt;= 16 최신 SSD 스토리지에서는 높은 I/O 특성을 가진 애플리케이션에 다중 쓰기 I/O 스레드가 유리합니다. innodb_buffer_pool_instances \u0026gt;= 9 InnoDB는 innodb_buffer_pool을 메모리 영역으로 분리하여 성능을 향상합니다. 1GB 이하에서는 pool size / 128MB를 사용하고, 최대값은 64로 설정합니다. innodb_io_capacity 5000 SSD 디스크에서는 5000을 사용하고, 물리적 디스크에서는 활성 드라이브 수에 200을 곱한 값을 사용합니다. NVMe 또는 PCIe 플래시의 경우 최대 100,000까지 설정 가능합니다. innodb_io_capacity_max 10000 SSD 디스크에서는 10,000을 사용하고, 물리적 디스크에서는 활성 드라이브 수에 2000을 곱한 값을 사용합니다. NVMe 또는 PCIe 플래시의 경우 최대 200,000까지 설정 가능합니다. memory_limit \u0026gt;= 800M 메모리 제한은 최소 800MB로 설정하세요. max_execution_time \u0026gt;= 60 PHP 스크립트 실행 시간을 최소 60초로 설정하세요. ","date":"2025-07-28T09:00:00+09:00","permalink":"https://joonlog.github.io/p/cacti-conainer-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98/","title":"Cacti Conainer 환경변수"},{"content":" https://github.com/scline/docker-cacti\nDockerfile https://github.com/scline/docker-cacti/blob/master/Dockerfile Rocky 9.0을 베이스 이미지로 사용 환경 변수로 DB 접속 정보, URL 경로, PHP 기본값 설정 start.sh 복사 및 실행 cacti + spine tar.gz 복사 서포트 파일(configs, configs/crontab, backup, upgrade, restore) 복사 RUN sh 파일 실행 권한 추가 볼륨 디렉토리 /backups, /cacti, /spine 생성 패키지 업데이트 및 설치(php, mariadb, 소스 설치 도구 등) start.sh https://github.com/scline/docker-cacti/blob/master/start.sh 초기 환경 설정 PHP 설정, 시스템 TZ 설정 새 설치 여부 판단 /cacti/install.lock 파일의 존재 여부로 이미 설치된 상태인지 판단 Cacti와 Spine 파일 압축 해제, 필요한 디렉토리로 이동 템플릿 파일, 플러그인, 설정 파일들을 적절한 위치에 복사하여 기본 환경을 구성 DB가 준비될 때까지 대기 후, 데이터베이스 생성 및 사용자 권한 설정, 그리고 cacti.sql 파일을 DB에 적용하여 초기 데이터를 세팅합니다. REMOTE_POLLER가 1이면, 마스터와의 설정 연동을 위한 추가 작업(예, config.php 수정, 파일 권한 조정 등)을 수행합니다. 서비스 시작 crond, snmpd, php-fpm, apache 실행 Compose 구조 https://github.com/scline/docker-cacti/blob/master/docker-compose/cacti_multi_shared.yml\nCacti Master+ Cacti Master DB + Poller + Poller DB\nCacti\n위 Dockerfile로 생성한 cacti 이미지 사용 웹 인터페이스 및 기본 Cacti 기능용 주 컨테이너 환경변수로 db 접속 정보 설정 /cacti, /spine, /backups 볼륨 공유 Poller\n위 Dockerfile로 생성한 cacti 이미지 사용 환경변수 REMOTE_POLLER=1가 설정되어 원격 풀러(데이터 수집 전용)로 동작 cacti_poller DB 접속 정보와 동시에 마스터 DB 접속 정보를 전달받아, 마스터로부터 설정을 가져와서 폴링 작업만 수행하도록 구성 /cacti, /spine, /backups 볼륨 공유 DB\nMariaDB 10.3 이미지 사용하며, Cacti와 Poller가 각각 DB 사용 Network\n4개 컨테이너가 같은 네트워크공유 Volume\nCacti와 Poller가 호스트의 /cacti, /spine, /backups 볼륨 공유 /var/lib/mysql를 db volume으로 사용 ","date":"2025-07-27T09:00:00+09:00","permalink":"https://joonlog.github.io/p/cacti-conainer-%EA%B5%AC%EC%A1%B0/","title":"Cacti Conainer 구조"},{"content":"참고: https://ko.linux-console.net/?p=2502\nApache 웹서버\n1 2 sudo dnf install httpd -y sudo systemctl enable --now httpd MariaDB\n1 2 sudo dnf install -y mariadb-server mariadb sudo systemctl enable --now mariadb PHP\n1 2 3 4 5 6 7 8 9 10 11 12 sudo dnf install dnf-utils http://rpms.remirepo.net/enterprise/remi-release-8.rpmmi sudo dnf module reset php sudo dnf module enable php:remi-7.4 sudo dnf install @php sudo dnf install -y php php-{mysqlnd,curl,gd,intl,pear,recode,ldap,xmlrpc,snmp,mbstring,gettext,gmp,json,xml,common} sudo systemctl enable --now php-fpm sudo vim /etc/opt/remi/php74/php.ini date.timezone = Africa/Nairobi memory_limit = 512M max_execution_style = 60 SNMP \u0026amp; RRDtool\n1 2 sudo dnf install -y net-snmp net-snmp-utils net-snmp-libs rrdtool sudo systemctl enable --now snmpd Cacti DB 생성\ncacti 사용자 생성 및 cacti DB ALL 권한 부여 cacti 사용자에 timezone 조회 권한 부여 1 2 3 4 5 6 7 8 9 10 11 12 mysql -u root -p MariaDB [(none)]\u0026gt; CREATE DATABASE cactidb; MariaDB [(none)]\u0026gt; GRANT ALL ON cactidb.* TO cacti_user@localhost IDENTIFIED BY \u0026#39;passwd123\u0026#39;; MariaDB [(none)]\u0026gt; FLUSH PRIVILEGES; MariaDB [(none)]\u0026gt; EXIT; mysql -u root -p mysql \u0026lt; /usr/share/mariadb/mysql_test_data_timezone.sql MariaDB [(none)]\u0026gt; GRANT SELECT ON mysql.time_zone_name TO cacti_user@localhost; MariaDB [(none)]\u0026gt; FLUSH PRIVILEGES; MariaDB [(none)]\u0026gt; EXIT; 선택 - DB 파라미터 튜닝 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 sudo vi /etc/my.cnf.d/mariadb-server.cnf collation-server=utf8mb4_unicode_ci character-set-server=utf8mb4 max_heap_table_size=32M tmp_table_size=32M join_buffer_size=64M 25% Of Total System Memory innodb_buffer_pool_size=1GB pool_size/128 for less than 1GB of memory innodb_buffer_pool_instances=10 innodb_flush_log_at_timeout=3 innodb_read_io_threads=32 innodb_write_io_threads=16 innodb_io_capacity=5000 innodb_file_format=Barracuda innodb_large_prefix=1 innodb_io_capacity_max=10000 Cacti 설치 및 테이블 생성\n1 2 3 4 5 6 7 sudo dnf install epel-release -y sudo dnf install cacti -y rpm -qi cacti cacti 1.12.17 rpm -ql cacti | grep cacti.sql /usr/share/doc/cacti/cacti.sql mysql -u root -p cactidb \u0026lt; /usr/share/doc/cacti/cacti.sql Cacti MariaDB 연동\n1 2 3 4 5 6 7 8 9 10 11 sudo vim /usr/share/cacti/include/config.php $database_type = \u0026#39;mysql\u0026#39;; $database_default = \u0026#39;cactidb\u0026#39;; $database_hostname = \u0026#39;localhost\u0026#39;; $database_username = \u0026#39;cacti_user\u0026#39;; $database_password = \u0026#39;\u0026lt;PASSWORD\u0026gt;\u0026#39;; $database_port = \u0026#39;3306\u0026#39;; $database_retries = 5; $database_ssl = false; $database_ssl_key = \u0026#39;\u0026#39;; Cacti 풀링 설정\n1 2 3 sudo vim /etc/cron.d/cacti */5 * * * * apache /usr/bin/php /usr/share/cacti/poller.php \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 Cacti 웹페이지 설정\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 sudo vim /etc/httpd/conf.d/cacti.conf Alias /cacti /usr/share/cacti \u0026lt;Directory /usr/share/cacti/\u0026gt; \u0026lt;IfModule mod_authz_core.c\u0026gt; # httpd 2.4 # Require all granted Require ip 192.168.0.0/24 \u0026lt;/IfModule\u0026gt; \u0026lt;IfModule !mod_authz_core.c\u0026gt; # httpd 2.2 Order deny,allow Deny from all # Allow from all Allow from 192.168.0.0/24 \u0026lt;/IfModule\u0026gt; \u0026lt;/Directory\u0026gt; \u0026lt;Directory /usr/share/cacti/install\u0026gt; \u0026lt;/Directory\u0026gt; sudo systemctl restart httpd sudo systemctl restart php-fpm sudo firewall-cmd --permanent --add-service=http sudo firewall-cmd --reload Cacti 웹페이지 접근\n1 2 http://server-ip/cacti admin/admin ","date":"2025-07-26T09:00:00+09:00","permalink":"https://joonlog.github.io/p/rocky8.10-cacti-%EC%84%A4%EC%B9%98/","title":"Rocky8.10 Cacti 설치"},{"content":"https://github.com/kubernetes-sigs/kubespray\nKubespray를 사용해 설치 kubespray 2.27 버전을 사용하여 Kubernetes 1.31버전 클러스터 구성 최소 4대가 필요 배포 서버 1EA 도커로 Kubespray를 배포하고, Kubernetes CRI로 containerd를 사용할 거기 때문에 Master Node에서 배포 불가해서 별도의 배포 서버 필요 Master Node 1EA Worker Node 2EA 사전작업 모든 Master, Worker Node에 작업\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # vim 설정 cat \u0026lt;\u0026lt; EOF \u0026gt;\u0026gt; ~/.bashrc alias vi=\u0026#39;/usr/bin/vim\u0026#39; EOF cat \u0026lt;\u0026lt; EOF \u0026gt;\u0026gt; ~/.vimrc syntax on autocmd FileType yaml setlocal ai nu ts=2 sw=2 et autocmd Filetype python setlocal ai nu ts=2 sw=2 et EOF . ~/.bashrc # DNS 서버가 없으므로 /etc/hosts 파일 사용 cat \u0026lt;\u0026lt; EOF \u0026gt;\u0026gt; /etc/hosts 172.27.1.9 master.k8s.com master 172.27.0.182 node1.k8s.com node1 172.27.1.35 node2.k8s.com node2 EOF # cri 전부 제거 for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo dnf remove $pkg -y; done ","date":"2025-07-25T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kubespray%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-kubernetes-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EC%B2%B4-%EA%B4%80%EB%A6%AC%ED%98%95-kubernetes/","title":"Kubespray를 사용한 Kubernetes 클러스터 배포 (자체 관리형 Kubernetes)"},{"content":"문제 개요 최근 특정 새벽 시간에 웹 서비스 접근 불가 현상이 반복 발생 Tomcat 로그 확인 시 Thread Stuck으로 요청 처리가 지연되었으며, 일정 시간 지난 후 자동 해소 1 2 WARNING argo.server.valves.LenaStuckThreadDetectionValve.notifyStuckThreadDetected ... at com.tmax.tibero.jdbc.common.TbStream.readMsgDs(Unknown Source) 원인 추적 왜 Thread Stuck이 발생했는가?\nTibero DB를 사용 중인 DB 서버의 문제 발생 시간대에 sys.log에서 대량의 임시 테이블이 생성된 것을 확인 매일 01시에 tbexport full=Y 명령어를 수행하는 cronjob 설정된 상태 Full 백업 시 전체 용량이 몇백 GB가 넘는데, 백업 간 DB 리소스 고갈로 인해 Tomcat의 스레드가 응답 받지 못해 Thread Stuck이 발생한 것으로 확인 조치 백업 방식을 아래와 같이 논리적(tbexport) → 물리적 백업으로 전환 대용량의 DB 백업 시에는 물리적 백업이 모든 면에서 유리 무중단 Hot Backup 가능 단, Hot Backup은 archive log 모드에서만 가능 내부 자원 사용 최소화 복구 및 운영 안정성 향상 물리적 백업 방안 tbrmgr 사용해보니 매일 풀백업을 사용하는 현재의 환경에선 맞지 않고, 백업한 파일을 압축하지 않고 그대로 보관하면서 증분 백업을 통해 보관하는 환경에서 맞는 방안인 것으로 확인 현재는 매일 풀백업 + 풀백업한 파일 압축하여 보관하는 환경에서 사용 가능한 방법 필요 begin/end 백업 백업 모드 시작 - 데이터파일 복사 - 백업 모드 종료의 구조로 백업하는 방식 이 방식으로 해결 ","date":"2025-07-24T09:00:00+09:00","permalink":"https://joonlog.github.io/p/tomcat-thread-stuck%EC%9C%BC%EB%A1%9C-%EC%9D%B8%ED%95%9C-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%A0%91%EA%B7%BC-%EB%B6%88%EA%B0%80-troubleshooting/","title":"Tomcat Thread Stuck으로 인한 사이트 접근 불가 TroubleShooting"},{"content":" /etc/yum.repos.d/CentOS-Base.rep 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 [base] name=CentOS-$releasever - Base baseurl=http://vault.centos.org/7.9.2009/os/$basearch/ gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 [updates] name=CentOS-$releasever - Updates baseurl=http://vault.centos.org/7.9.2009/updates/$basearch/ gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 [extras] name=CentOS-$releasever - Extras baseurl=http://vault.centos.org/7.9.2009/extras/$basearch/ gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 [centosplus] name=CentOS-$releasever - Plus baseurl=http://vault.centos.org/7.9.2009/centosplus/$basearch/ gpgcheck=1 enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 ","date":"2025-07-23T09:00:00+09:00","permalink":"https://joonlog.github.io/p/centos-7.9-yum-repository/","title":"CentOS 7.9 Yum Repository"},{"content":" /etc/yum.repos.d/CentOS-Base.rep 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 [base] name=CentOS-$releasever - Base baseurl=http://centos.mirror.cdnetworks.com/7/os/x86_64 gpgcheck=1 enabled=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 #released updates [updates] name=CentOS-$releasever - Updates baseurl=http://centos.mirror.cdnetworks.com/7/updates/x86_64 gpgcheck=1 enabled=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 #additional packages that may be useful [extras] name=CentOS-$releasever - Extras baseurl=http://centos.mirror.cdnetworks.com/7/extras/x86_64 gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 #additional packages that extend functionality of existing packages [centosplus] name=CentOS-$releasever - Plus baseurl=http://centos.mirror.cdnetworks.com/7/centosplus/x86_64 gpgcheck=1 enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 ","date":"2025-07-22T09:00:00+09:00","permalink":"https://joonlog.github.io/p/centos-7.6-yum-repository/","title":"CentOS 7.6 Yum Repository"},{"content":" /etc/yum.repos.d/CentOS-Base.repo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 [base] name=CentOS-\\$releasever - Base baseurl=http://centos.mirror.cdnetworks.com/6/os/x86_64/ gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6 #released updates [updates] name=CentOS-\\$releasever - Updates baseurl=http://centos.mirror.cdnetworks.com/6/updates/x86_64/ gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6 #additional packages that may be useful [extras] name=CentOS-\\$releasever - Extras baseurl=http://centos.mirror.cdnetworks.com/6/extras/x86_64/ gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6 #additional packages that extend functionality of existing packages [centosplus] name=CentOS-\\$releasever - Plus baseurl=http://centos.mirror.cdnetworks.com/6/centosplus/x86_64/ gpgcheck=1 enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6 #contrib - packages by Centos Users [contrib] name=CentOS-\\$releasever - Contrib baseurl=http://centos.mirror.cdnetworks.com/6/contrib/x86_64/ gpgcheck=1 enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6 EOF ","date":"2025-07-21T09:00:00+09:00","permalink":"https://joonlog.github.io/p/centos-6.5-yum-repository/","title":"CentOS 6.5 Yum Repository"},{"content":" 언어 중립적, 플랫폼 중립적, 확장 가능한 데이터 직렬화(serialization) 포맷 gRPC의 기본 포맷 구조화된 데이터를 효율적으로 저장하고 전송하기 위한 방식 데이터 타입이 명확히 정의되어 있음 (int32, string 등) .proto 파일에서 데이터 구조를 정의하고, protoc 컴파일러로 각 언어로 변환해 사용 protoc --go_out=. person.proto JSON vs protobuf json보다 훨씬 적은 용량으로 전송 가능(binary 포맷) 1 { \u0026#34;name\u0026#34;: \u0026#34;Alice\u0026#34;, \u0026#34;age\u0026#34;: 30 } 1 2 3 4 5 message Person { string name = 1; int32 age = 2; string email = 3; } 필드 번호 기반으로 동작해서, 일부 필드가 빠져도 괜찮음 (optional처럼 동작)\n위에처럼 protobuf가 정의되어 있을 때 1, 2, 3이 각 필드의 번호 아래처럼 데이터를 보낼 때, 필드 3이 없어도 동작 이런 동작 방식 덕분에 새 필드를 추가해도 이전 클라이언트는 무시하고 정상 작동 1 2 3 4 5 Person { name: \u0026#34;Alice\u0026#34; age: 30 // email은 없음 } Go에서의 protobuf protobuf를 Go 코드로 변환할 때의 추가 설정 옵션 1 string InitScript = 16 [(gogoproto.jsontag) = \u0026#34;init_script,omitempty\u0026#34;]; gogoproto: gogo/protobuf라는 성능 최적화된 Protobuf 구현체에서 제공하는 옵션 jsontag: 필드가 Go 구조체로 변환될 때, JSON 직렬화 시 사용될 struct tag를 직접 지정하는 설정입니다. ","date":"2025-07-20T09:00:00+09:00","permalink":"https://joonlog.github.io/p/go-%EC%96%B8%EC%96%B4%EC%97%90%EC%84%9C%EC%9D%98-protocal-buffers/","title":"Go 언어에서의 Protocal Buffers"},{"content":"문제 상황 웹 서비스 사용 중 세션이 갑자기 만료되며 강제 로그아웃되는 현상 발생 원인 분석 Catalina 로그에서 ClassNotFoundException 확인\n1 2 3 4 5 6 7 SEVERE [Catalina-utility-1] org.apache.catalina.session.StoreBase.processExpires Error processing session expiration for key [BA8BFEFE...JVM1] java.lang.ClassNotFoundException: egovframework.com.utl.slm.EgovHttpSessionBindingListener at org.apache.catalina.session.StandardSession.doReadObject(StandardSession.java:1268) at org.apache.catalina.session.StandardSession.readObjectData(StandardSession.java:846) at org.apache.catalina.session.FileStore.load(FileStore.java:203) at org.apache.catalina.session.StoreBase.processExpires(StoreBase.java:138) Tomcat의 세션 직렬화 기능으로 인해, 서버가 재기동되거나 일정 시간이 경과한 후 세션 정보가 디스크에 저장된 파일(SESSIONS.ser)을 통해 복원 중\nSESSIONS.ser에는 EgovHttpSessionBindingListener라는 클래스가 포함되어 있었으나, 현재 애플리케이션에는 이 클래스가 존재하지 않아 역직렬화 과정에서 오류 발생한 것으로 추정\nTomcat이 해당 세션을 복원하지 못해 세션을 강제 만료 처리 → 사용자 입장에서는 강제 로그아웃으로 인식됨\n조치 내용 세션 복원 실패에 따른 반복적인 오류를 차단하기 위해 다음과 같은 조치 수행\nTomcat 세션 파일 SESSIONS.ser 삭제 Tomcat 재기동 → 이후 세션 관련 오류 재발 없음, 사용자 세션 정상 유지 확인\n결론 Tomcat은 세션 정보를 디스크에 저장 후 복원하는 과정에서, 클래스 누락 등으로 인해 세션 역직렬화에 실패할 수 있음 이 경우 해당 세션이 강제 만료되는 문제 발생 이후에도 세션 직렬화 기능을 사용한다면, 애플리케이션 배포 전 ser 파일 삭제 권장 ","date":"2025-06-23T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EC%A7%81%EB%A0%AC%ED%99%94-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EC%82%AC%EC%9A%A9%EC%A4%91%EC%9D%B8-tomcat%EC%9D%98-%EC%84%B8%EC%85%98-%EB%A7%8C%EB%A3%8C%EB%A1%9C-%EC%9D%B8%ED%95%9C-%EA%B0%95%EC%A0%9C-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83-troubleshooting/","title":"직렬화 기능을 사용중인 Tomcat의 세션 만료로 인한 강제 로그아웃 Troubleshooting"},{"content":" 계정 내 전체 자원 삭제 시 AWS 콘솔에서 일일이 삭제하지 않고도 명령어 만으로 전체 삭제 가능 1. AWS CLI 설치 설치 (Rocky Linux 8.1 기준) 1 2 3 4 5 6 dnf install -y unzip curl \u0026#34;https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip\u0026#34; -o \u0026#34;awscliv2.zip\u0026#34; unzip awscliv2.zip ./aws/install aws --version 2. 인증 설정: 루트 계정 키 생성 및 CLI 등록 루트 계정으로 AWS 콘솔 로그인 후 키 생성 보안 자격 증명 페이지 접속 Access Key 생성 키값 저장 (Secret Key는 재확인 불가) 액세스 키 등록 1 2 3 4 5 aws configure --profile default # Access Key ID: [입력] # Secret Access Key: [입력] # Region: ap-northeast-2 # Output: json 3. 계정 ID 및 인증 확인 1 2 aws sts get-caller-identity --profile default # 결과의 \u0026#34;Account\u0026#34; → 계정 ID 확인 (예: 934697152199) 4. Go 설치 및 aws-nuke 빌드 1 dnf remove -y golang 최신 Go 수동 설치 (1.24.3) 1 2 3 cd /usr/local curl -LO https://go.dev/dl/go1.24.3.linux-amd64.tar.gz tar -C /usr/local -xzf go1.24.3.linux-amd64.tar.gz 환경변수 설정 1 2 3 echo \u0026#39;export PATH=/usr/local/go/bin:$PATH\u0026#39; \u0026gt;\u0026gt; ~/.bashrc source ~/.bashrc go version # → go1.24.3 aws-nuke clone + build 1 2 3 git clone https://github.com/rebuy-de/aws-nuke.git cd aws-nuke make build 실행 파일 위치 1 2 ./dist/aws-nuke # 여기에 실행 파일 생성됨 cp ./dist/aws-nuke /usr/local/bin/ 5. 계정 alias 설정 (필수) CLI로 설정 가능 alias는 중복되면 생성 실패하니 유니크하게 생성 1 aws iam create-account-alias --account-alias delete-\u0026lt;계정ID\u0026gt; 6. nuke-config.yaml 작성 해당 설정은 전체 삭제 yaml 삭제하고 싶은 자원 지정해서 사용 가능 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 regions: - global - us-east-1 - us-west-1 - ap-northeast-2 - ap-southeast-2 - ap-northeast-1 - eu-west-1 - eu-central-1 account-blocklist: - \u0026#34;000000000000\u0026#34; # 더미 ID, 필수 accounts: \u0026#34;계정ID 여기 입력\u0026#34;: filters: {} 7. 삭제 실행 1 aws-nuke -c nuke-config.yaml --profile default --no-dry-run ❗ 중간에:\nEnter account alias to continue: → delete-934697152199 입력\n일부 자원은 한 번에 안 지워질 수 있음 → 반복 실행하면 정리 완료\n❗❗❗ 주의 ❗❗❗ EC2, ELB, RDS 등 리소스 삭제 방지 기능이 걸려 있는 경우, aws-nuke는 어떠한 경우에도 해당 자원을 삭제할 수 없음 ","date":"2025-06-20T09:00:00+09:00","permalink":"https://joonlog.github.io/p/aws-%EC%9E%90%EC%9B%90%EC%97%90-%ED%95%B5%ED%8F%AD%ED%83%84-%EB%82%A0%EB%A6%AC%EA%B8%B0-aws-nuke/","title":"AWS 자원에 핵폭탄 날리기 (AWS Nuke)"},{"content":"문제 상황 웹 서비스 접근 시 500 에러 출력 원인 분석 웹 서비스에 DB 서버 2대를 이중화하여 구성 중, 2번 서버로 DB Failover된 후 장애 발생\napplication log 확인\n1 2 3 4 org.springframework.jdbc.support.SQLErrorCodesFactory] Error while extracting database product name - falling back to empty error codes org.springframework.jdbc.support.SQLStateSQLExceptionTranslator] Extracted SQL state class \u0026#39;JD\u0026#39; from value \u0026#39;JDBC-90405:ERRJD\u0026#39; org.springframework.transaction.interceptor.TransactionInterceptor] Application exception overridden by rollback exception org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [...] threw exception [...] with root cause Tomcat에서 Failover 이전 DB 서버로의 Connection을 계속 사용해서 SQLException 오류 발생하여 500 에러 출력\n조치 내용 즉각적인 웹 서비스 정상화를 위한 임시 조치\nTomcat 재기동 후 웹 서비스 정상 접근 확인 추가 조치 방안 Tomcat의 server.xml 혹은 context.xml에서 testWhileIdle 설정 testWhileIdl: Tomcat이 Connection을 반환하기 전, 주기적으로 유휴 Connection 생존 여부를 검사 timeBetweenEvictionRunsMillis: 30초마다 검사 1 2 testWhileIdle=\u0026#34;true\u0026#34; timeBetweenEvictionRunsMillis=\u0026#34;30000\u0026#34; 결론 Tomcat의 JDBC Connection Pool은 testWhileIdle 옵션이 false일 경우, 장애 이후에도 죽은 Connection을 계속 사용할 수 있어 장애가 지속됨. 이 설정을 true로 변경하고 timeBetweenEvictionRunsMillis를 설정해서 DB Failover 시 자동적으로 Connection을 복구하도록 설정\n참고 https://blog.naver.com/wasgosu-2010/90189771881 ","date":"2025-06-19T09:00:00+09:00","permalink":"https://joonlog.github.io/p/%EC%A3%BD%EC%9D%80-connection-%EC%82%AC%EC%9A%A9-%EC%A4%91%EC%9D%B8-tomcat-troubleshooting/","title":"죽은 Connection 사용 중인 Tomcat Troubleshooting"},{"content":"변수 및 함수 이름 camelCase 사용 축약어는 중간이나 끝에 (예: userID, httpServer) 간결하고 의미 있는 이름 권장 (err, ok, i, r 등) 1 2 var userID int func getUserInfo() {} 타입 및 인터페이스 타입 이름은 PascalCase 인터페이스는 동작 기반 이름 사용 (Reader, Closer 등) 1 2 3 4 5 6 7 type FileReader interface { Read(p []byte) (n int, err error) } type User struct { Name string } 에러 처리 if err != nil 패턴이 기본 에러 발생 즉시 처리하고 빠르게 반환 에러 메시지는 소문자로 시작하고 마침표는 생략 1 2 3 4 f, err := os.Open(\u0026#34;file.txt\u0026#34;) if err != nil { return err } 패키지 이름 규칙 항상 소문자 단수형 의미는 명확하되 짧게 (strings, math, http 등) 밑줄(_), 복수형, 접두사 등은 지양 주석 스타일 공개 함수나 타입에는 주석 필수 함수명으로 시작하는 문장형 주석 사용 1 2 3 4 5 6 7 // Add는 두 숫자의 합 반환 /* Add는 두 숫자의 합 반환 */ func Add(a, b int) int { return a + b } ","date":"2025-06-17T09:00:00+09:00","permalink":"https://joonlog.github.io/p/go-%EC%BD%94%EB%94%A9-%EA%B7%9C%EC%B9%99idiomatic-go/","title":"Go 코딩 규칙(Idiomatic Go)"},{"content":"컴파일 명령어 go build\nmain 패키지면 현재 디렉터리에 실행 파일 생성 main 패키지 없다면 실행 파일 생성 안 됨 (컴파일만 됨) go install\nmain 패키지면 실행 파일이 $GOBIN 또는 $GOPATH/bin 에 생성됨\nmain 패키지 없다면 실행 파일 없음, 캐시에만 저장 아래 코드처럼 실행 가능한 go로 작성된 cli 툴들 가져오기 가능(GOBIN에 등록됐을 시)\n1 go install github.com/user/tool@latest 환경 변수 설정\n1 2 3 4 5 6 7 8 # GOBIN 기본 경로 확인 go env GOBIN # 없으면 GOPATH/bin이 기본 go env GOPATH # 실행 파일 전역 사용을 위해 PATH 설정 echo \u0026#39;export PATH=$PATH:$(go env GOPATH)/bin\u0026#39; \u0026gt;\u0026gt; ~/.bashrc source ~/.bashrc 모듈 라이브러리 명령어 go get\nmain 패키지가 없는 모듈 라이브러리(go.mod)에 추가하는 명령어 1 go get github.com/user/package go mod tidy\ngo.mod에 필요 없는 모듈 제거 누락된 의존성 자동 추가 코드 스타일 정리 들여쓰기, 공백, import 정렬 등을 표준화\n1 2 3 go fmt # 현재 디렉토리 전체 포맷 go fmt file.go # 특정 파일만 포맷 go fmt ./... # 하위 디렉토리까지 전체 포맷. bash 문법이 아닌 go 문법. ","date":"2025-06-16T09:00:00+09:00","permalink":"https://joonlog.github.io/p/go-%EA%B8%B0%EB%B3%B8-%EB%AA%85%EB%A0%B9%EC%96%B4/","title":"Go 기본 명령어"},{"content":"증상 웹 접속 시 503 오류 발생\nmod_jk.log에 AJP 연결 실패 로그 다수 출력\n1 2 3 [error] ajp_connect_to_endpoint::jk_ajp_common.c (127.0.0.1:8009) (errno=111) [error] jk_open_socket::jk_connect.c Failed socket to (127.0.0.1:8009) [error] ajp_send_request::jk_ajp_common.c (worker1) Connecting to backend failed netstat 기준으로 Tomcat은 AJP 포트(8009)를 정상적으로 Listen 중\n1 tcp 0 0 127.0.0.1:8009 ::1 LISTEN [java 프로세스] workers.properties 설정 기존 설정\n1 2 worker.worker1.type=ajp13 worker.worker1.host=localhost host가 정상적으로 localhost로 되어 있는데, mod_jk.log에서는 ajp 연동 오류가 나오고, netstat은 IPv6에서만 LISTEN중 /etc/hosts에도 정상적으로 localhost와 127.0.0.1이 명시된 것으로 확인 localhost를 127.0.0.1로 변경 이후 설정\n1 2 worker.worker1.type=ajp13 worker.worker1.host=127.0.0.1 host 값 변경 후 httpd 재기동 시 503 에러 해결 결론 로그를 종합했을 때, Tomcat은 127.0.0.1:8009에서 AJP를 수신하고 있었지만, workers.properties에 설정된 localhost가 내부적으로 ::1(IPv6)로 해석되어 연결이 실패한 것으로 추정 ","date":"2025-06-07T09:00:00+09:00","permalink":"https://joonlog.github.io/p/apache-ajp-localhost-%EC%97%B0%EA%B2%B0-%EC%97%90%EB%9F%AC-troubleshooting/","title":"Apache AJP Localhost 연결 에러 TroubleShooting"},{"content":" Go 언어는 public, private 키워드 없이 이름의 첫 글자 대소문자만으로 식별자의 공개 여부를 결정 함수, 변수, 인터페이스 등 모든 식별자에 적용되는 규칙\n공개(Public) 식별자와 비공개(Private) 식별자 대문자로 시작하는 이름 → 해당 패키지 외부에서도 접근 가능 (공개)\n소문자로 시작하는 이름 → 해당 패키지 내부에서만 사용 가능 (비공개)\n1 2 3 4 5 6 7 8 9 // 공개 함수 func Reverse(s string) string { return reverseTwo(s) } // 비공개 함수 func reverseTwo(s string) string { // 문자열 뒤집기 } Public 함수 접근 예시 패키지 구조\n1 2 3 4 5 6 GolangTraining/ ├── 02_library/ │ └── stringutil/ │ └── reverse.go └── 01_helloWorld/ └── hello.go reverse.go (패키지 내부 함수 정의)\n1 2 3 4 5 6 7 8 9 10 11 12 13 package stringutil func Reverse(s string) string { return reverseTwo(s) } func reverseTwo(s string) string { r := []rune(s) for i, j := 0, len(r)-1; i \u0026lt; len(r)/2; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] } return string(r) } hello.go (외부 패키지에서 import)\n1 2 3 4 5 6 7 8 9 10 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/goestoeleven/GolangTraining/02_library/stringutil\u0026#34; ) func main() { fmt.Println(stringutil.Reverse(\u0026#34;Hello, Go!\u0026#34;)) } 실행 결과\n1 !oG ,olleH Private 함수에 접근하려 할 경우 reverseTwo는 stringutil 내부에서만 사용 가능하므로, 아래와 같이 사용하면 컴파일 에러 발생\n1 fmt.Println(stringutil.reverseTwo(\u0026#34;Hello\u0026#34;)) 컴파일 에러\n1 undefined: stringutil.reverseTwo ","date":"2025-05-31T09:00:00+09:00","permalink":"https://joonlog.github.io/p/go-public-/-private-%EC%8B%9D%EB%B3%84%EC%9E%90/","title":"Go Public / Private 식별자"},{"content":" Go에서 _는 사용하지 않는 값 또는 결과를 버리는 용도로 사용 읽기 전용이며, 어떤 값도 저장하거나 참조 불가 실제 메모리에 변수로 존재하지 않음 주요 사용 예 다중 리턴값 중 일부 무시할 때 1 val, _ := someFunc() // 두 번째 리턴값 무시 반복문에서 인덱스나 값을 무시할 때 1 2 3 for _, v := range list { fmt.Println(v) // 인덱스는 필요 없을 때 } 인터페이스 구현 시, 사용하지 않는 메서드 인자 무시 1 2 3 func handler(_ int) { // 인자를 사용하지 않음 } import된 패키지를 코드에서 직접 사용하지 않을 때 1 import _ \u0026#34;net/http/pprof\u0026#34; // side-effect용 import 변수 선언 후 사용하지 않아 에러가 날 때 회피용 1 2 msg := \u0026#34;hello\u0026#34; _ = msg ","date":"2025-05-30T09:00:00+09:00","permalink":"https://joonlog.github.io/p/go-blank-identifier/","title":"Go Blank Identifier"},{"content":" Go 특유의 상수용 자동 증가 매크로 키워드\nconst 블록에서만 사용 가능 첫 번째 상수는 0부터 시작 아래로 한 줄씩 내려갈 때마다 1씩 자동 증가 기본 예제 1 2 3 4 5 const ( A = iota // 0 B // 1 C // 2 ) iota는 행 기준으로 증가 → 위 예제에서 A=0, B=1, C=2\n중간에 값을 지정해도 다음 iota는 계속 증가 1 2 3 4 5 const ( A = iota // 0 B = 100 // 고정값 C = iota // 2 ) 비트 연산자와 조합 (플래그) 1 2 3 4 5 const ( Read = 1 \u0026lt;\u0026lt; iota // 1 \u0026lt;\u0026lt; 0 → 1 Write // 1 \u0026lt;\u0026lt; 1 → 2 Exec // 1 \u0026lt;\u0026lt; 2 → 4 ) → Read=1, Write=2, Exec=4 → 비트 플래그 용도로 자주 사용\n여러 개 동시에 선언할 때도 동작 1 2 3 4 const ( x, y = iota, iota // x=0, y=0 a, b = iota, iota // a=1, b=1 ) ","date":"2025-05-29T09:00:00+09:00","permalink":"https://joonlog.github.io/p/go-iota/","title":"Go iota"},{"content":" 정수형 int, int8, int16, int32, int64 uint 계열은 양수 전용 보통은 그냥 int 사용 실수형 float32, float64 (기본은 float64) 소수점 숫자 저장용 문자형 rune: 유니코드 문자 하나 (int32) 'A', '한' , 이모티콘을 포함한 문자 저장 가능 문자열 string: UTF-8 인코딩 문자열 불변(immutable) 슬라이싱, 인덱싱 가능 Boolean bool: true / false 조건문 등에 사용 Byte byte는 uint8의 alias 문자열 인코딩 관련 작업에서 자주 사용 1 2 3 4 5 6 var i int = 10 var f float64 = 3.14 var ch rune = \u0026#39;가\u0026#39; var s string = \u0026#34;hello\u0026#34; var b bool = true var b byte = \u0026#39;A\u0026#39; // byte는 uint8 기본값 Go에서는 변수 선언 시 초기값을 주지 않으면 자료형에 맞는 기본값(zero value)으로 초기화됨 1 2 3 4 var i int // 0 var s string // \u0026#34;\u0026#34; var b bool // false var f float64 // 0.0 출력 포맷 (fmt.Printf) 포맷 설명 예시 %d 정수 fmt.Printf(\u0026quot;%d\u0026quot;, 10) %f 실수 fmt.Printf(\u0026quot;%.2f\u0026quot;, 3.14) %s 문자열 fmt.Printf(\u0026quot;%s\u0026quot;, \u0026quot;go\u0026quot;) %c 문자 fmt.Printf(\u0026quot;%c\u0026quot;, 'A') %v 기본 포맷 fmt.Printf(\u0026quot;%v\u0026quot;, 변수) %T 타입 fmt.Printf(\u0026quot;%T\u0026quot;, 변수) 형 변환 Go는 자동 형 변환 없음 → 반드시 명시적 변환 필요 정수 ↔ 실수, 정수 ↔ uint 변환 시 주의 1 2 3 var a int = 42 var b float64 = float64(a) var c uint = uint(b) rune \u0026lt;===\u0026gt; string 인덱싱 시 주의\nstring[index]는 byte 리턴 → 유니코드 문자는 rune으로 변환해야 제대로 출력 가능 1 2 3 s := \u0026#34;가나다\u0026#34; fmt.Printf(\u0026#34;%c\\n\u0026#34;, s[0]) // 깨짐 fmt.Printf(\u0026#34;%c\\n\u0026#34;, []rune(s)[0]) // 가 단축 선언 (:=) 타입 추론과 동시에 변수 선언 주로 함수 내부에서 사용 1 2 3 4 name := \u0026#34;Go\u0026#34; // string score := 95 // int rate := 4.5 // float64 valid := true // bool ","date":"2025-05-28T09:00:00+09:00","permalink":"https://joonlog.github.io/p/go%EC%9D%98-%EA%B8%B0%EB%B3%B8-%EC%9E%90%EB%A3%8C%ED%98%95%EA%B3%BC-%EB%8B%A8%EC%B6%95-%EC%84%A0%EC%96%B8/","title":"Go의 기본 자료형과 단축 선언"},{"content":"공식 문서: https://go.dev/doc/install\n설치\n1 2 3 wget https://go.dev/dl/go1.24.3.linux-amd64.tar.gz tar -xvzf go1.24.3.linux-amd64.tar.gz sudo mv go /usr/local/ .bashrc에 영구 적용\n1 2 3 4 5 6 7 go env GOROOT # 위 명령어에서 나온 값 변수로 저장 # 기본값: /usr/local/go echo \u0026#39;export GOROOT=$(go env GOROOT)\u0026#39; \u0026gt;\u0026gt; ~/.bashrc echo \u0026#39;export PATH=$PATH:$GOROOT/bin\u0026#39; \u0026gt;\u0026gt; ~/.bashrc source ~/.bashrc 설치 확인\n1 go version go 모듈 초기화\nGo 프로젝트 시작 시 go.mod 파일을 만들고 의존성 관리 가능하게 함 보통 하나의 프로젝트(리포지토리)당 한 번만 실행 모듈 이름은 GitHub 경로처럼 짓는 것이 관례이며, 실제로 GitHub에 올릴 경우 import 경로로 사용됨 1 2 mkdir go-basic \u0026amp;\u0026amp; cd go-basic go mod init github.com/joonlog/go-basic 이 명령은 현재 디렉토리를 github.com/joonlog/go-basic이라는 이름의 모듈로 선언함\n→ GitHub에 올리지 않아도 작동하지만, 나중에 배포하려면 실제 리포지토리 경로와 맞춰야 함\ngo.mod 파일 생성됨\n1 2 3 4 go-workspace/ └── myproject/ ← 모듈 디렉터리 ├── go.mod ← 모듈 정보 └── main.go main.go 작성\npackage main의 목적: go run / go build로 실행 가능한 바이너리 생성 반드시 func main 존재 필요\n1 2 3 4 5 6 7 package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;Hello, Go!\u0026#34;) } 실행\n1 go run main.go ","date":"2025-05-26T09:00:00+09:00","permalink":"https://joonlog.github.io/p/go-%EC%84%A4%EC%B9%98%EC%99%80-%EC%8B%A4%ED%96%89/","title":"Go 설치와 실행"},{"content":" NCP의 Backup 서비스는 서버 내 파일/DB를 백업할 수 있는 상품 Backup 설정은 5단계로 구성 Resource: 백업 대상 서버 등록 Storage: 백업 데이터 저장소 Policy: 보관 정책 정의 Job: 백업 작업 정의 Schedule: 백업 실행 일정 Resource 존, 서버, 에이전트 유형(Data, DB) 선택 아이디/비밀번호는 서버 root/root비밀번호 입력 Resource 설정 시 위 정보가 정상적이라면 자동으로 Agent 설치 주의) DB 백업을 위한 에이전트 추가 이후 DB 인스턴스 추가에 DB 버전 제한이 있으니 확인 필수\nDB 인스턴스 추가에 실패해도 별도 에러 메세지 없어서 콘솔 상 실패 이유 확인 불가 /var/log/commvault/Log_Files/MySqlBrowseAgent.log DB 인스턴스 추가 시 로그는 위 로그에 기록됨 https://guide.ncloud-docs.com/docs/backup-spec Storage DB 백업용 스토리지 스토리지에 별도 접근 불가 Policy 생성 전 Storage 생성 필수 Storage 생성하지 안고 Policy 생성 시 별다른 에러 메세지 없이 실패 Policy 백업을 보관할 Storage, 보관 기간 설정 Job Data 백업 시에 백업 대상 경로 선택 DB 백업 시에 백업 DBMS, 개별 백업 DB 선택 DB 백업은 mysqldump를 원격으로 하는 형태 Schedule 백업 주기, 백업 시간 설정 증분 백업과 전체 백업이 겹치면 오류 따라서 하루 전체 백업, 6일 증분 백업으로 보통 정책 7개 이상 생성 주로 ncp cli로 크론탭에 스크립트 만들어 사용 ","date":"2025-05-24T09:00:00+09:00","permalink":"https://joonlog.github.io/p/ncp-backup/","title":"NCP Backup"},{"content":" KT Cloud는 Zone 별로 구조가 상이하니 Zone 간 차이에 대해 이해가 필요하다. G1/G2(A, B, M1)(Cloudstack) G1 존의 경우 VR 밑에서 서버가 생성된다. 이에 Subnet(tier)을 나눠서 구성이 불가하다. VR에서 방화벽의 역할을 해주나 블랙리스트가 아닌 화이트 리스트만 적용이되어 보안적으로 취약하다. NAS 연결시 NAS용 네트워크에 NAS가 생성되어 CIP를 통해 통신이 된다. 이에 CIP 대역을 서버에 붙여줘야된다. 기본 디스크가 HDD이다. SSD는 SSD 서버 상품 구성을 해야한다. KT Load Balancer는 VR 상단에 위치하게 된다. (Server를 LB로 구성한 것이다.) LB도 NAS처럼 존이 따로 존재한다. VR 방화벽 : 공격이 서버에 도달하지 않게 막아주는 역할을 한다. 다만 화이트 리스트 적용으로서의 역할만 한다. Port forwarding : G1존은 VR에 할당 된 기본 공인 IP를 통해 모든 서버들이 통신한다. 이에 VR에서 서버를 구분하여 서비스 할 수 있게 포트 포워딩을 해준다. 서버 생성/ 삭제 : 서버 생성/삭제에 관여한다. 이에 서버 비밀번호, IP도 부여한다. VR에서 다양한 역할을 하지만 서버이기에 서버에서 발생하는 문제(DISK full, CPU 등) 되면 모든 자원에 문제가 생기게 된다는 단점이 있다 또한 VR의 경우 대역폭이 정해져 있어 과도한 서비스 사용시 VR에 부하가 발생하여 서비스에 문제가 될 수 있다. 이에 고급형 VR을 판다. 한 개의 서버에 KT 만의 프로그램인 router 기능을 넣어 사용자에게 제공한다. Master-Slave 구성으로 생성된다. G1(G-cloud, Ent-cloud)(Cloudstack) DMZ/Private zone으로 물리적으로 분리되어 있음 Subnet(tier) 생성 불가능 DMZ zone와 Private zone간에는 DMZ F/W 과 Private F/W 에 의해 차단되며 방화벽 설정을 통해 연동 DMZ zone에는 Web, Private zone에는 WAS 및 DB 등 배치 웹서비스를 이용하는 최종 사용자는 DMZ F/W - IPS - DMZ LB (옵션) - VR - VM 의 경로로 접근 G2(M2, Eenterprise Security)(Cloudstack) https://manual.cloud.kt.com/kt/enterprise-security-intro\nDMZ/Private zone을 최대 13개 티어로 구성 가능 Dx(Dx-M1, Dx-central, Dx-G**)**(OpenStack) scale up, scale out 지원 월 추가 비용 필요(30?) DMZ/Private zone으로 분리 VR 대신 VDOM을 사용하여 여러 Tier을 관리 multi-tenant로 논리적 방화벽 분리 적용 내부적으로 서로 다른 Tier라도 CIP로 통신하는 것이 아닌 vdom으로 통신 가능 화이트 리스트, 블랙 리스트를 선택 가능 Tier 분리를 했기에 NAS를 다른 Tier와 공유 가능 각 티어와 외부 연동에는 각각의 Firewall이 존재 ( DMZ F/W, Private F/W, 외부 F/W ) 최종 사용자는 DMZ F/W \u0026gt; IPS \u0026gt; (DMZ LB) \u0026gt; VDOM \u0026gt; VM으로 접근 ","date":"2025-05-23T09:00:00+09:00","permalink":"https://joonlog.github.io/p/kt-cloud-zone-%EA%B5%AC%EC%A1%B0/","title":"KT Cloud Zone 구조"},{"content":"1. SSH 키 생성 GitHub에서 사용할 SSH 키를 생성 최신 보안 권장사항에 따라 ed25519 알고리즘 사용 1 ssh-keygen -t ed25519 -C \u0026#34;your_email@example.com\u0026#34; 이메일은 GitHub 계정에 등록된 이메일로 입력 생성 시 파일명을 github_ssh.txt와 같이 지정 가능 키는 기본적으로 ~/.ssh 디렉토리에 생성 예시 출력:\n1 Enter file in which to save the key (/home/user/.ssh/id_ed25519): github_ssh.txt 2. 공개키 GitHub에 등록 생성된 공개키 확인:\n1 cat ~/.ssh/github_ssh.txt.pub GitHub 접속 → Settings → SSH and GPG keys → New SSH key 클릭\n위 명령어로 확인한 공개키 내용을 붙여넣기\n3. SSH config 설정 ~/.ssh/config 파일을 열어 GitHub 접속 시 사용할 키를 지정\n1 2 3 4 echo \u0026#34;Host github.com HostName github.com User git IdentityFile ~/.ssh/github_ssh.txt\u0026#34; \u0026gt;\u0026gt; ~/.ssh/config 여러 Git 계정을 사용할 경우 Host github.com-\u0026lt;계정명\u0026gt; 등으로 이름을 다르게 지정하면 병행 사용도 가능 4. SSH 연결 테스트 1 ssh -T git@github.com 성공 시 아래와 같은 메시지를 출력 1 Hi \u0026lt;GitHub 사용자명\u0026gt;! You\u0026#39;ve successfully authenticated, but GitHub does not provide shell access. 5. 저장소 클론 SSH를 통해 저장소를 클론 가능\n1 git clone git@github.com:joonlog/\u0026lt;내-리포지토리\u0026gt;.git ","date":"2025-05-22T09:00:00+09:00","permalink":"https://joonlog.github.io/p/github-ssh-%EC%84%A4%EC%A0%95/","title":"GitHub SSH 설정"},{"content":"방법 1: 수동으로 저장소 구성 후 설치 MariaDB의 서명 키와 저장소를 수동으로 등록한 뒤 패키지를 설치하는 방식 1 2 3 sudo apt-get install apt-transport-https curl sudo mkdir -p /etc/apt/keyrings sudo curl -o /etc/apt/keyrings/mariadb-keyring.pgp \u0026#39;https://mariadb.org/mariadb_release_signing_key.pgp\u0026#39; APT 저장소 설정 파일을 /etc/apt/sources.list.d/mariadb.sources에 생성 1 2 3 4 5 6 X-Repolib-Name: MariaDB Types: deb URIs: https://tw1.mirror.blendbyte.net/mariadb/repo/11.4/ubuntu Suites: jammy Components: main main/debug Signed-By: /etc/apt/keyrings/mariadb-keyring.pgp 리눅스 커널 관련 패키지 자동 업데이트를 방지하고 설치 1 2 apt-mark hold linux-image-generic linux-headers-generic apt-get install mariadb-server 방법 2: MariaDB 공식 설치 스크립트 사용 MariaDB에서 제공하는 설치 스크립트를 통해 저장소 등록과 설치를 한 번에 처리하는 방식 1 2 3 4 curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version=11. apt-mark hold linux-image-generic linux-headers-generic apt install mariadb-server mariadb-client -y 초기 공통 설정 mysql_secure_installation 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 mysql_secure_installation Enter current password for root (enter for none): # 현재 root 계정의 비밀번호 # 비밀번호가 설정되어 있지 않은 경우 엔터 Switch to unix_socket authentication [Y/n]: # root 로그인을 비밀번호가 아니라 시스템 계정(sudo)으로만 허용할지 결정 # Y를 선택하면 root로 로그인할 때 비밀번호 대신 sudo mysql로 접속 # 일반적으로는 Y(보안성 향상). 비밀번호로 로그인할 수 있게 하려면 n 입력. Change the root password? [Y/n]: # root 계정의 비밀번호를 변경할지 여부 # 새로 설정하거나, 이전 비밀번호가 없는 경우 새로 설정하는 데 사용 Remove anonymous users? [Y/n]: # 익명 사용자 계정을 삭제할지 여부 # 익명 계정은 인증 없이 접속할 수 있으므로 Y 입력 권장 Disallow root login remotely? [Y/n]: # root 계정으로 원격 접속을 막을지 여부 # 보안상 Y 권장. 필요한 경우 n 선택 후 방화벽 등으로 제한하는 방식 고려 Remove test database and access to it? [Y/n]: # 테스트용 test 데이터베이스를 삭제할지 여부 # 기본으로 생성되는 샘플 데이터베이스로, 불필요하므로 Y 입력 권장 Reload privilege tables now? [Y/n]: # 앞서 설정한 변경 사항들을 즉시 적용할지 여부 # Y 선택하면 권한 테이블이 즉시 갱신 계정 생성 및 권한 부여 1 2 3 4 5 mysql -u root CREATE DATABASE test; CREATE USER \u0026#39;admin\u0026#39;@\u0026#39;localhost\u0026#39; IDENTIFIED BY \u0026#39;비밀번호\u0026#39;; GRANT ALL PRIVILEGES ON *.* TO \u0026#39;admin\u0026#39;@\u0026#39;localhost\u0026#39; WITH GRANT OPTION; FLUSH PRIVILEGES; binary log 활성화 1 2 3 4 5 6 7 8 9 vim /etc/mysql/my.cnf [mysqld] log-bin = mariadb-bin binlog-format = MIXED server-id = 1 systemctl restart mariadb SHOW VARIABLES LIKE \u0026#39;log_bin\u0026#39;; MariaDB를 완전 제거 1 2 apt purge mariadb-server mariadb-client rm -rf /etc/mysql /var/lib/mysql ","date":"2025-05-21T09:00:00+09:00","permalink":"https://joonlog.github.io/p/ubuntu22-mariadb-11.4.5-%EB%B2%84%EC%A0%84-%EC%84%A4%EC%B9%98/","title":"Ubuntu22 MariaDB 11.4.5 버전 설치"},{"content":"문제 요약 Ubuntu 24.04, Amazon Linux 2023 등 최신 리눅스에서 기존 RSA 키로 접속 불가 특히 SecureCRT는 ssh-rsa 알고리즘만 사용해서 오류 발생 Putty는 rsa-sha2-256 지원 → 같은 키로도 접속 가능 원인 최신 OpenSSH는 ssh-rsa (SHA-1 기반) 비활성화 → 보안상 위험 Amazon Linux 2023은 /etc/crypto-policies/back-ends/opensshserver.config를 통해 강제로 막음 Ubuntu는 기본 정책에선 막히진 않지만, OpenSSH 자체가 기본으로 제외시킴 해결 방법 (서버 측 설정 변경) 1 vim /etc/ssh/sshd_config 아래 두 줄을 추가 Amazon Linux 2023: 반드시 Include보다 위쪽에 Ubuntu 24.04: 위치는 무관 (보통 맨 아래 추가) 1 2 HostkeyAlgorithms +ssh-rsa PubkeyAcceptedAlgorithms +ssh-rsa 1 2 3 # 설정 확인 및 sshd 재시작 sshd -t systemctl restart sshd 주의사항 ssh-rsa는 보안 취약점 있음 (SHA-1 기반) 이 설정은 임시 방편일 뿐, 장기적으론 권장되지 않음 대안: 새 키 생성 시 최신 알고리즘 사용 1 2 ssh-keygen -t ed25519 -C \u0026#34;your@email.com\u0026#34; 추후 key 생성 시 RSA 보단 ESDSA 같은 알고리즘 사용 필요\n","date":"2025-05-20T09:00:00+09:00","permalink":"https://joonlog.github.io/p/rsa-key%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-ssh-%EB%B6%88%EA%B0%80-troubleshooting/","title":"RSA Key를 사용한 SSH 불가 TroubleShooting"},{"content":"반복문과 조건문 반복문\n단순 반복문: with_items 중첩 반복문: with_nested 기타 반복 지시문: with_file with_fileglob with_sequence with_random_choice 반복문에서 사용되는 변수는 List 형식으로 정의\n각 항목은 key-value 형식으로 사용 가능\n1 2 3 4 5 6 - name: 단순 반복 예시 debug: msg: \u0026#34;{{ item }}\u0026#34; with_items: - value1 - value2 조건문\nwhen 구문 사용 조건식은 Jinja2 문법을 사용하지만, 중괄호({})는 쓰지 않음 1 2 3 4 - name: 조건문 예시 debug: msg: \u0026#34;x86_64 시스템입니다.\u0026#34; when: ansible_machine == \u0026#34;x86_64\u0026#34; 자주 사용하는 조건 예시: ansible_machine == \u0026quot;x86_64\u0026quot; max_memory == 512 min_memory != 512 min_memory \u0026lt; 120 min_memory is defined min_memory is not defined memory_available not memory_available \u0026quot;ny special user\u0026quot; in superusers 반복문+조건문\n1 2 3 4 5 - name: 특정 조건에 맞는 마운트 포인트 처리 debug: msg: \u0026#34;{{ item.mount }} 마운트에 여유 공간 있음\u0026#34; with_items: \u0026#34;{{ mount_list }}\u0026#34; when: item.mount == \u0026#34;/\u0026#34; and item.size_available \u0026gt; 300000000 핸들러 구현 notify는 해당 task가 changed 상태일 때만 트리거됨 handlers는 모든 task가 끝난 후 실행됨 1 2 3 4 5 6 7 8 9 10 11 12 tasks: - name: Task1 file: src: httpd.conf.template dest: /etc/httpd/conf/httpd.conf notify: - restart httpd handlers: - name: restart httpd service: name: httpd state: restarted 작업 오류 처리 실패 무시하기\n1 2 3 - name: 실패해도 무시Z command: /bin/false ignore_errors: yes 실패 조건 지정\n1 2 3 4 - name: 실패 조건 직접 지정 shell: some_command register: result failed_when: \u0026#34;\u0026#39;error\u0026#39; in result.stderr\u0026#34; Changed 상태 지정\n1 2 3 - name: 강제로 Changed 처리 shell: echo \u0026#34;Force change\u0026#34; changed_when: true 핸들러 강제 실행\n1 2 3 - name: 전체 플레이북에 핸들러 강제 실행 설정 hosts: all force_handlers: yes Ansible 블록 및 오류 처리 - block ~ rescue ~ always\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 - name: block-rescue-always 예시 hosts: all tasks: - name: 중요한 작업 실행 block: - name: 작업 1 shell: /bin/false - name: 작업 2 shell: echo \u0026#34;이건 실행 안됨\u0026#34; rescue: - name: 에러 발생 시 대체 작업 debug: msg: \u0026#34;문제가 발생했지만 복구 작업 실행\u0026#34; always: - name: 항상 실행되는 작업 debug: msg: \u0026#34;이 작업은 성공/실패 여부와 무관하게 항상 실행됨\u0026#34; ","date":"2025-05-18T09:00:00+09:00","permalink":"https://joonlog.github.io/p/ansible-%EC%A1%B0%EA%B1%B4%EB%AC%B8-/-%EB%B0%98%EB%B3%B5%EB%AC%B8-/-%ED%95%B8%EB%93%A4%EB%9F%AC-/-%EC%98%A4%EB%A5%98-%EC%B2%98%EB%A6%AC/","title":"Ansible 조건문 / 반복문 / 핸들러 / 오류 처리"},{"content":" 서비스 운영 중에 DB 백업/복원 작업할 때 주의할 점 정리 옵션에 따라 백업/복원 기능이 다르니 주의 필요 1. DB 접근 1 mysql -u 사용자 -p -h 도메인명 2. DB 덤프 (백업) 1 2 3 4 5 6 mysqldump -h 도메인명 -u 사용자 -p \\ --single-transaction \\ --routines \\ --triggers \\ --events \\ BASE_DB \u0026gt; DB명_날짜.sql 옵션 설명 -single-transaction: InnoDB 전용. 덤프 중 트랜잭션 시작해서 일관된 스냅샷 유지. 서비스 중에도 락 없이 가능. -routines: 저장 프로시저, 함수 포함. -triggers: 각 테이블의 트리거 포함. -events: 이벤트 스케줄러 이벤트 포함. 3. 덤프 파일 확인 (USE 문 유무) mysqldump 시 --databases 옵션 사용하면 .sql 파일에 CREATE DATABASE + USE DB명; 구문 포함됨 복원 시 다른 DB에 덮어씌어짐 반드시 확인 필요\n1 head -n 30 DB명_날짜.sql | grep -i \u0026#34;^USE\u0026#34; 만약 USE DB명; 이 있을 경우 → 주석 처리 1 2 -- Before: USE BASE_DB; -- After : -- USE BASE_DB; 4. 덤프 파일을 통한 복원 1 mysql -h 도메인명 -u 사용자 -p 생성한DB명 \u0026lt; DB명_날짜.sql 신규 DB는 복원 전 DB 생성 필요 5. 기존/신규 DB 확인 기존/신규 DB 내 테이블의 rows 수가 동일한지 확인 1 2 3 mysql\u0026gt; SELECT 기존DB명 AS source, COUNT(*) FROM 기존DB명.PTNT_RSRV_HIST_BACK -\u0026gt; UNION ALL -\u0026gt; SELECT 신규DB명, COUNT(*) FROM 신규DB명.PTNT_RSRV_HIST_BACK; 주의사항 요약 mysqldump에 -databases 옵션 쓰면 USE 문 포함됨 → 다른 DB 덮어씌우는 원인. 클라우드 DB는 SUPER 권한 없을 수 있음 → -set-gtid-purged=OFF 넣어야 에러 안남. 복원할 DB명과 .sql 파일 내 USE 구문 다를 경우 조심해야 함. ","date":"2025-05-16T09:00:00+09:00","permalink":"https://joonlog.github.io/p/mysql-/-mariadb-%EB%8D%A4%ED%94%84-%EB%B0%8F-%EB%B3%B5%EC%9B%90/","title":"MySQL / MariaDB 덤프 및 복원"},{"content":"문제 상황 운영 중인 CentOS 서버에서 너무 많은 수의 httpd 프로세스가 CPU를 과점유하고, Load Average가 30 이상까지 치솟는 현상이 발생 원인 분석 access_log 확인 결과, 루프백 주소(::1)에서 OPTIONS * HTTP/1.0 요청이 수 초 간격으로 다수 발생 User-Agent는 \u0026quot;Apache/... (internal dummy connection)\u0026quot;으로, Apache 자체가 생성하는 dummy connection임을 확인 이 요청은 Apache의 internal dummy connection이며, 일반적으로는 MPM이 유휴 프로세스를 관리하기 위해 보내는 내부 테스트용 요청 하지만 일정 이상 발생하게 되면, 해당 요청을 처리하기 위한 httpd 프로세스가 과도하게 생성되고 정리되지 않아, 리소스를 점유 1 2 3 4 5 6 7 8 # tail -n 1000 /var/log/httpd/access_log | grep \u0026#34;::1\u0026#34; ::1 - - [22/Apr/2025:07:01:55 +0000] \u0026#34;OPTIONS * HTTP/1.0\u0026#34; 200 - \u0026#34;-\u0026#34; \u0026#34;Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips (internal dummy connection)\u0026#34; ::1 - - [22/Apr/2025:07:02:34 +0000] \u0026#34;OPTIONS * HTTP/1.0\u0026#34; 200 - \u0026#34;-\u0026#34; \u0026#34;Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips (internal dummy connection)\u0026#34; ::1 - - [22/Apr/2025:07:02:35 +0000] \u0026#34;OPTIONS * HTTP/1.0\u0026#34; 200 - \u0026#34;-\u0026#34; \u0026#34;Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips (internal dummy connection)\u0026#34; ::1 - - [22/Apr/2025:07:02:36 +0000] \u0026#34;OPTIONS * HTTP/1.0\u0026#34; 200 - \u0026#34;-\u0026#34; \u0026#34;Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips (internal dummy connection)\u0026#34; ::1 - - [22/Apr/2025:07:02:42 +0000] \u0026#34;OPTIONS * HTTP/1.0\u0026#34; 200 - \u0026#34;-\u0026#34; \u0026#34;Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips (internal dummy connection)\u0026#34; ::1 - - [22/Apr/2025:07:02:56 +0000] \u0026#34;OPTIONS * HTTP/1.0\u0026#34; 200 - \u0026#34;-\u0026#34; \u0026#34;Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips (internal dummy connection)\u0026#34; 1 2 3 4 # httpd -V | grep MPM [Fri Apr 25 04:35:44.665826 2025] [so:warn] [pid 31382] AH01574: module rewrite_module is already loaded, skipping Server MPM: prefork MPM 관련 설정(StartServers, MaxRequestWorkers)이 명시되지 않은 상태 Apache 기본값(최대 256개 프로세스)이 적용된 것으로 추정 조치 내용 하기 내용은 Apache 설정을 수정하지 않고 즉시 대응 가능한 임시 조치\nIPv6 루프백 dummy connection 차단을 위한 ip6tables 규칙 추가 루프백(::1)에서 들어오는 OPTIONS * 요청만 차단 외부 요청이나 정상적인 HTTP 트래픽에는 영향 없음 1 ip6tables -I INPUT -s ::1 -p tcp --dport 80 -m string --algo bm --string \u0026#34;OPTIONS *\u0026#34; -j DROP Apache 재기동 이미 생성되어 남아 있는 과도한 httpd 프로세스를 정리하고 초기화 신규 요청부터는 차단 규칙이 적용된 상태로 운영 1 systemctl restart httpd CPU 사용률 및 Load Average 수 분 내로 정상화 결론 httpd 프로세스 수 안정화, 더 이상 루프백 OPTIONS * 요청 발생하지 않음 추가 조치 방안 문제의 근본 원인을 해결하기 위해서는 Apache 설정 최적화 필요 특히, 현재 서버는 prefork MPM을 사용하고 있으며, 기본 설정값으로 운영중 이로 인해 부하 상황에서 과도한 프로세스 생성 및 dummy connection 폭주 현상이 발생한 것으로 분석됨 mpm 설정 추가 MaxRequestWorkers를 서버 사양에 맞게 제한 1 2 3 4 5 6 7 8 9 # /etc/httpd/conf.modules.d/00-mpm.conf \u0026lt;IfModule mpm_prefork_module\u0026gt; StartServers 5 MinSpareServers 5 MaxSpareServers 10 MaxRequestWorkers 150 ServerLimit 150 \u0026lt;/IfModule\u0026gt; ","date":"2025-04-25T09:00:00+09:00","permalink":"https://joonlog.github.io/p/apache-options-%EB%A3%A8%ED%94%84%EB%B0%B1-%EC%9A%94%EC%B2%AD%EC%9C%BC%EB%A1%9C-%EC%9D%B8%ED%95%9C-cpu-%EA%B3%BC%EB%B6%80%ED%95%98-troubleshooting/","title":"Apache OPTIONS * 루프백 요청으로 인한 CPU 과부하 TroubleShooting"},{"content":"SEO SEO는 Search Engine Optimization, 즉 검색 엔진 최적화로 구글이나 네이버 같은 검색 엔진에서 내 블로그가 더 잘 노출되도록 하는 작업이다.\n블로그를 운영하더라도, 검색에 노출되지 않으면 방문자가 내 블로그 도메인을 직접 입력해야만 접근 가능하다. 사실상 필수 설정.\nGitHub Pages + Jekyll 환경 SEO 설정 jekyll-seo-tag 플러그인 설치\n블로그에 필요한 SEO 메타 태그들을 자동으로 생성해주는 공식 플러그인 Gemfile에 추가 1 gem \u0026#39;jekyll-seo-tag\u0026#39; _config.yml에 추가 1 2 plugins: - jekyll-seo-tag jekyll-sitemap 플러그인 설치\n블로그 글 전체를 포함한 sitemap.xml 파일을 자동을 생성해주는 플러그인 구글, 네이버 같은 검색엔진에 내 사이트 구조를 전달하는 목적의 파일 1 gem \u0026#39;jekyll-sitemap\u0026#39; 1 2 plugins: - jekyll-sitemap 배포 후 https://\u0026lt;블로그주소\u0026gt;/sitemap.xml 로 접근 가능하면 성공 Google Search Console 등록\nGoogle Search Console 접속\nURL 접두어 방식 선택 → https://\u0026lt;블로그주소\u0026gt; 입력\n생성된 verifications 값 _config.yml에 입력 1 2 webmaster_verifications: google: \u0026#34;\u0026lt;값 입력\u0026gt;\u0026#34; 배포 후 Google Search Console 내 소유권 확인 2번에서 생성된 사이트맵 Google Search Console에 등록 등록 후에 즉시 활성화가 되는 것은 아님 하루 정도 대기 필요 참고 SEO: https://standing-o.github.io/posts/jekyll-seo/\n","date":"2025-04-25T09:00:00+09:00","permalink":"https://joonlog.github.io/p/github-blogjekyll-chirpy-seo-%EC%84%A4%EC%A0%95/","title":"GitHub Blog(Jekyll Chirpy) SEO 설정"},{"content":"1. Variable 변수 이름 지정\n변수 이름은 문자, 숫자 ,밑줄 가능 변수 이름은 문자로 시작해야 한다. 변수 적용 볌위\n변수 적용 범위는 좁은 범위가 넓은 범위보다 우선한다. 플레이북에서 변수 정의\n변수 정의\n플레이북내에 변수를 지정하는 경우\n1 2 3 4 5 6 # $ cat site.yaml ------------- - hosts: web1 vars: user: user01 home: /home/user01 별도의 파일로 변수를 설정하고 플레이북에서 지정하는 경우\n1 2 3 4 5 6 7 8 9 # $ cat vars/users.yaml ------------- user: user01 home: /home/user01 --------------- # $ cat site.yaml - hosts: web1 var_files: - vars/users.yaml 변수 사용\n변수의 사용은 “{{ variable }}” 형식으로 사용한다. 변수의 종류\n호스트 변수 그룹 변수 [참고] 변수의 우선 순위\n명령어 라인에서 지정하는 변수(ex: ansible -e ….) 플레이북에서 정의하는 변수(play.yaml(vars, var_files)) 호스트 변수(host_vars/*) 그룹 변수(group_vars/*) 배열 변수\n1 2 3 4 5 6 7 8 9 # $ cat vars.yaml ---------------- users: user01: name: lee # =\u0026gt; users.user01.name shell: /bin/bash # =\u0026gt; users[\u0026#39;user01\u0026#39;][\u0026#39;name\u0026#39;] user02: name: kim shell: /bin/tsch register 구문\nregister 구문을 사용하여 명령 출력을 캡쳐할 수 있다. register 구문은 일반적으로 디버깅 용도로 사용한다 ansible-playbook -v … 1 2 3 4 5 6 7 tasks: - yum: name: httpd state: present register: VAR - debug: var: VAR 2. Secret ansible-vault 암호화된 새파일 만들기 $ ansible-vault create secret.yaml 암호화된 파일 보기 $ ansible-vault view secret.yaml 암호화된 파일 편집하기 $ ansible-vault edit secret.yaml 평문 파일 ↔ 암호화 $ ansible-vault encrypt secret.yaml $ ansible-vault decrypt secret.yaml 암호화 키 변경하기 $ ansible-vault rekey secret.yaml ansible-vault와 playbook $ ansible-playbook \u0026ndash;help | grep password $ ansible-playbook \u0026ndash;ask-vault-password playbook.yaml $ ansible-playbook \u0026ndash;vault-password-file-vault-pass playbook.yaml 3. Facts 앤서블 호스트에 대해 자동으로 모아지는 변수 내용\nFacts\nansible_hostname andible_fqdn ansible_default_ipv4.address … Facts 조회 방법\nsetup 모듈을 사용한 ansible ad-hoc 명령어로 facts 조회\n$ ansible localhost -m setup -a ‘filter=ansible_*’ $ ansible localhost -m setup -a ‘filter=ansible_interface’\n⇒ Facts 사용 ”{{ansible_hostname}}”, “{{ansible_default_ipv4.address}}” ”{{ansible_facts[\u0026lsquo;hostname\u0026rsquo;]}}”, “{{ansible_default[\u0026lsquo;ipv4.address\u0026rsquo;]}}”\nFacts 끄는 방법\n1 2 3 4 5 6 # $ vi playbook.yaml ---------------- - hosts: large_farm gather_facts: no tasks: ... 사용자 정의 Facts\n/etc/ansible/facts.d/*.fact (INI 형식 or JSON 형식) 1 2 3 4 5 6 7 8 9 10 { \u0026#34;packages\u0026#34;: { \u0026#34;web_package\u0026#34;: \u0026#34;httpd\u0026#34;, \u0026#34;db_packages\u0026#34;: \u0026#34;mariadb-server\u0026#34; }, \u0026#34;users\u0026#34;: { \u0026#34;user1\u0026#34;: \u0026#34;user01\u0026#34;, \u0026#34;user2\u0026#34;: \u0026#34;user02\u0026#34; } } 사용: playbook에서 사용하는 예 ansible localhost -m setup -a ‘filter=ansible_local’ ⇒ ansible_local[’local’][’packages’][’web_package’] ⇒ ansible_local.local.packages.web_package\n매직변수(Magic Variable)\nhostvars\ngroup_names\ngroups\ninventory_hostname\n매직변수 조회 방법\ndebug 모듈을 사용한 ad-hoc 명령어로 조회 $ ansible localhost -m debug -a ‘var=hostvars’ $ ansible localhost -m debug -a ‘var=group_names’ $ ansible localhost -m debug -a ‘var=inventory_hostname’\n4. include vars, include tasks 관리 include Overview\n작업 포함: include_tasks, import_tasks 변수 포함: include_vars\ninclude_tasks\n1 2 3 4 5 6 7 8 9 10 # $ cat tasks/main.yaml ------ - name: Task1 yum: ... # $ cat play.yaml ------ tasks: - include_tasks: tasks/main.yaml include_vars\n1 2 3 4 5 6 7 8 # $ cat include_vars ------ web_pkg: httpd # $ cat play.yaml ------ tasks: - include_vars: vars/vars.yaml ","date":"2025-04-22T09:00:00+09:00","permalink":"https://joonlog.github.io/p/ansible-variables-/-secrets-/-facts/","title":"Ansible Variables / Secrets / Facts"},{"content":"\n대량의 서버 인프라 관리를 자동화하는 IaC 도구 컨트롤 노드에서 앤서블 실행 시 스크립트에 작성된 내용에 따라 각 노드에 작업 자동화 Ansible 구조 ansible.cfg + inventory + playbook\nansible.cfg ansible 기본 설정 파일\ninventory 경로 지정 remote_user: 각 서버에 ssh 접근 시 어떤 계정으로 접근할 지 지정 inventory에 각 서버마다 접근할 계정 지정할 수도 있지만 단일 계정으로 접근 권장 become = true: 리모트 노드에서 Ansible 동작 시 sudo 권한으로 동작하도록 설정 become 설정은 ansible.cfg에서 명시하지 않아도 각 playbook마다 개별 설정 가능 host_key_checking = False: 처음 SSH 접속하는 서버는 fingerprint를 남겨야 하지만 이 옵션으로 무시하고 접속 remote_tmp = /tmp/.ansible-{USER}: Ansible은 명령어 실행 시 임시 파일을 접속 디렉토리에 작성하는데, 접속 서버에서 계정이 디렉토리에 대한 쓰기 권한이 없을때 경로를 임시 파일 작성 경로를 /tmp로 우회 1 2 3 4 5 6 7 8 [defaults] inventory = /home/ncloud24/ansible/inventory remote_user = \u0026lt;서버 계정\u0026gt; host_key_checking = False remote_tmp = /tmp/.ansible-{USER} [privilege_escalation] become = true inventory ansible 동작 시 대상 호스트 및 변수를 정의하는 파일\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 [prod] prod1 ansible_host=192.168.0.1 ansible_user=user2 prod2 ansible_host=192.168.0.2 ansible_user=user3 prod3 ansible_host=192.168.0.3 [dev] dev1 ansible_host=192.168.1.1 dev2 ansible_host=192.168.1.2 dev3 ansible_host=192.168.1.3 dev4 ansible_host=192.168.1.4 dev5 ansible_host=192.168.1.5 [all:vars] ansible_ssh_private_key_file=/home/rocky/key.pem 이 설정에서는 각 리모트 노드들의 alias와 ip를 지정해서 동작 ansible_host는 ansible 내부적으로 ip로 인식하는 예약 변수 inventory에서 ansible_host을 명시하지 않는다면 /etc/hosts에서 도메인, IP 작성 ⇒ inventory에 도메인 명시하면 동일하게 동작 가능 ansible_user 변수로 서버별 접속 계정을 다르게 설정 가능 각 그룹마다 변수를 다르게 설정 가능 all:vars를 사용해서 전체 호스트에 대한 변수 설정 ansible_ssh_private_key_file 변수로 키 파일 위치 지정 가능 playbook 어떤 서버, 어떤 명령어를 실행할지 정의하는 yaml 파일\n모든 서버의 /home/ansible 경로에 0755의 권한을 가진 pkg 디렉토리 생성 yaml\n1 2 3 4 5 6 7 8 9 10 --- - name: Playbook 테스트 hosts: all:!prod1 tasks: - name: 디렉토리 생성 file: path: /home/ansible/pkg state: directory mode: \u0026#39;0755\u0026#39; host에 inventory에 정의한 alias, group 등등으로 대상 리모트 노드 지정 예시 prod ⇒ prod1, prod2, prod3 총 3개 서버 대상 prod1 ⇒ 개별 서버 지정 가능 all ⇒ inventory에 기록된 모든 서버 대상 all:!prod1 ⇒ prod1 서버를 제외한 모든 서버 대상 task에 어떤 모듈을 어떻게 동작시킬지 정의 task에 여러 모듈 실행 가능 한 yaml에서 여러 task도 실행 가능 이 설정에서는 file 모듈 실행 ","date":"2025-04-16T09:00:00+09:00","permalink":"https://joonlog.github.io/p/ansible-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90/","title":"Ansible 기본 개념"},{"content":"\n올해 초에 들은 클라우드 교육 과정 중 Ansible이 있었는데, 기왕 RHCSA를 가지고 있는 거 RHCE까지 따기로 했다. 아마 RHCSA를 취득해놓은 상태가 아니라면 굳이 RHCE까지는 따려고 하지 않았을 것 같다. RHCSA와 마찬가지로 1번의 retake 기회가 있고, 자격증 유효 기간은 3년이다.\n할인 쿠폰 시험 비용이 꽤 비싼 편인데, 할인 쿠폰도 많이 없고 할인율도 15%로 크진 않다.\n이 쿠폰은 RedHat 시험에 합격하면 나오는 쿠폰으로 1달? 3달? 동안 3명까지 쓸 수 있다. 그래서 종종 사람들이 커뮤니티에 공유하기도 하는데, 나는 redhat reddit에서 주워서 사용했다.\nredhat discount reddit으로 검색하면 쿠폰 공유하는 게시물을 찾을 수 있을 것이다.\n공부 방법 및 기간 나에게는 RHCE가 RHCSA보다 쉽게 느껴졌다. 문제 자체도 더 심화된 리눅스 문제가 나오는 게 아닌, RHCSA 문제에 Ansible 문법만 더한 정도였다. 만약 RHCSA를 가지고 있고, 어떤 교육이든 강의든 Ansible을 배워 봤다면 쉽게 취득할 수 있을 거라고 생각한다. 하루 4시간씩 2주 정도 공부했던 것 같다.\n시험 환경 여권 준비 필수\n시험 장소는 에티버스러닝이었는데, 칸막이가 쳐진 작은 방에 2명이 시험 볼 수 있게 되어 있었다. 입장해서 준비된 노트북에 로그인을 하면 감독관이 채팅으로 말을 걸어온다. 감독관 요구에 따라 웹캠에 여권을 보여주고 방 안을 검사 맡으면 된다.\n시험 시간은 4시간으로 매우 긴 편이다. ansible로 여러 노드를 제어해야 되기 때문이다. 연습을 충분히 했다면, 꼼꼼하게 풀고 에러도 거의 없을 때를 기준으로 2시간 반 정도면 전부 풀 수 있다.\n시험 팁 모든 문제 풀이들은 재부팅 후에도 동작해야 한다.\n채점할 때 모든 노드를 재부팅 후 채점하기 때문에 service가 enable 되어 있는지, 디스크가 마운트되어 있는지, 파일들이 정상적으로 생성되었는지 잘 체크해야 한다.\ndoc를 보면서 풀어라.\nAnsible은 설치 시에 docs가 내장되어 있다. 문제 풀기 전 내가 사용할 모듈의 docs를 확인하는 습관을 들인다면 오류를 줄일 수 있을 것 이다. 나는 모든 문제를 doc를 열고 보면서 풀었다.\nansible-doc \u0026lt;모듈명\u0026gt;으로 문서를 열고 /EXAMPLES를 통해 예시 검색 alias 지정이 시간 단축이 도움이 된다.\n자주 입력해야 하는 ansible, ansible-playbook, ansible-playbook --syntax-check, ansible-doc 등의 명령어들을 ~/.bashrc에 alias ans='ansible', alias anp=’ansible-playbook’과 같이 지정해 놓으면 좋다.\nansible-navigator 사용 가능하다.\nRHCE9 버전부터 사용 가능한 걸로 알고 있다. 내 경우엔 ansible-navigator로 배우지 않아 navigator가 더 어색해서 쓰지 않았지만, 처음부터 navigator을 쓰는 걸로 배웠다면 tui 환경이니 더 직관적으로 쉬울 것 같다.\n변수는 inventory에 직접 지정하는 게 더 빠를 때가 있다.\n문제에서 ip와 같은 각 노드마다 고정된 변수를 가지고 ansible 작업을 해야할 때, 팩트와 매직 변수에서 필요한 변수를 찾기 어렵다면, 노가다식으로 inventory 각 노드에 변수를 직접 지정하는게 빠르다.\n어떻게든 문제를 푸는 게 더 중요하다.\n팩트, 매직 변수에서 필요한 정보를 어떻게 찾는지 연습해야 한다.\n팩트 검색 예시: ansible localhost -m setup -a 'filter=ansible_devices'\n매직변수 검색 예시: ansible localhost -m debug -a 'var=hostvars[\u0026quot;ansible1.example.com\u0026quot;]'\n출제 문제 Ansible 설치 및 구성 Repository 구성 컬렉션 설치 패키지 설치 rhel-system-roles 사용 Role 생성 및 사용 Ansible Galaxy로 Role 설치 Ansible Galaxy로 Role 사용 파티션 생성 및 사용 when 조건 사용 hosts 파일 생성 매직변수 사용 파일 배포 inventory 변수 사용 웹 컨텐츠 디렉터리 배포 파일 생성 후 setype이 httpd_sys_content_t와 같이 웹서버 디렉터리 맞는지 확인 하드웨어 리포트 배포 팩트 사용 ansible vault 생성 사용자 계정 생성 위에서 생성한 vault 사용 패스워드에 password_hash 걸었는지 체크 vault 키 변경 cronjob 생성 ","date":"2025-01-05T09:00:00+09:00","permalink":"https://joonlog.github.io/p/rhce9-%ED%9B%84%EA%B8%B0/","title":"RHCE9 후기"},{"content":"\n자격증들은 보통 깊은 기술을 요구하기 보단 넓고 얕게 아는 걸 요구하기 때문에 자격증 취득은 그 기술에 대해 어느 정도 알아보는 데 도움이 된다고 생각한다. 관련해서 쿠버네티스도 CKA 자격증 취득을 쿠버네티스 공부의 첫 목표로 잡았다.\nCKA는 시험에 불합격 하더라도 retake 기회를 1회 제공하고, 자격증 유효 기간은 내가 합격 했을 땐 3년이었지만 지금은 2년으로 알고 있다.\n할인 코드 Linux Foundation은 40% 할인 쿠폰을 1년에 두세번 정도 배포한다. 40%가 아니더라도 30%정도는 상시 할인하는 느낌이니 쿠폰은 반드시 적용해야 한다.\n공부 방법 및 기간 CKA를 공부했다면 모두가 아는 mumshad 선생님의 udemy 강의(https://www.udemy.com/course/certified-kubernetes-administrator-with-practice-tests/?couponCode=KEEPLEARNING)를 통해 기초를 잡았다. 외에도 따배쿠 유튜브도 많이 보는 것 같다.\nCKA를 신청하면 Linux Foundation에서 killer.sh 문제를 풀어 볼 수 있게 제공했던 걸로 기억하는데, 다른 블로그에서 하나같이 시험 난이도에 비해 killer.sh가 훨씬 어렵다고 해서 한번도 풀어보지 않았다. 대신 https://www.itexams.com/exam/CKA 여기 있는 문제들과 다른 블로그들을 참고해서 연습용 문제들을 골랐다.\n공부 기간은 3주동안 매일 4시간 정도였는데, 그때는 리눅스를 약간 다룰 수 있는 정도였고 K8S는 하나도 몰랐으니 K8S를 다뤄 봤다면 더 짧아질 수 있을 것이다.\n연습 환경 문제 풀이 실습들은 https://killercoda.com/cka에서 하는 걸 적극 추천한다. CKA는 쿠버네티스 구축에 대한 역량은 고려하지 않기 때문에 VM같은 걸로 K8S 구축하는데 에너지를 쏟기 보다 killercoda를 사용하는 게 좋다.\n이 사이트는 K8S를 테스트 하는데 최적화되어 있는데, 무료는 1개 세션에서 클러스터 1개와 노드 1개를 1시간 동안 사용할 수 있지만 이정도면 충분하다. 1시간이 지나더라도 그 세션만 초기화 되고 다시 사용할 수 있기 때문에 한문제 풀고 초기화하고 또 풀면 된다.\n시험 환경 CKA는 정해진 시험장이 없기 때문에 보통 노트북을 가지고 시험을 본다. 노트북은 캠과 오디오가 동작해야 한다. 시험 신청을 하면 안내 메일이 오는데, 여기에 시험 환경을 테스트 할 수 있는 링크가 있다. 그 링크에서 캠/오디오 등등을 체크 해야 시험 당일에 곤란하지 않을 것이다. 또 감독관 쪽에서 내 노트북을 원격으로 감시해야 하는데, 혹시 몰라서 윈도우 방화벽도 내렸다.\n시험 시작 30분 전부터 시험 환경인 PSI 브라우저를 설치할 수 있다. 접속하면 감독관이 본인 확인과 방 안 검사를 요구하는데, 이 시간 안에 감독관한테 ok가 나와야 이후 시험에 지장이 가지 않는다. 본인 확인은 여권을 지참해야 한다.\n정해진 시험장이 없는 만큼 주위 환경도 깨끗해야 한다. 노트북을 제외한 어떤 것도 시험장에 있으면 안되고, 벽에 안내문 같은 종이 붙어 있다면 어떻게든 가려야 한다. 나는 집에서는 이 환경을 구성하기가 어려울 것 같아, 공유 오피스에서 최대한 조용한 구석으로 자리를 잡았다.\n시험 팁 모든 문제는 그 문제만의 k8s context가 명시되어 있다. 따라서 문제 풀기 전에 반드시 context 체크부터 해야 한다. PSI 브라우저에서 https://kubernetes.io/docs/home/ 공식 문서에 접근이 가능하다. 즉 오픈북 시험이란 것. 대부분의 문제에 대한 구축 과정을 공식 문서에서 확인이 가능하니 그 문서를 빨리 찾을 수 있게 연습하는 것도 좋을 것이다. 특히 K8S Upgrade나 ETCD Backup 문제 같은 경우 공식 문서를 참조하는 걸 추천한다. 시험 변경점 문제들은 처음 나왔을 때부터 지금까지 변경이 거의 없는 것으로 알고 있는데, 올해 1월 15부터 변경이 있다고 한다.\nhttps://www.youtube.com/watch?v=fvvgM3QmKGo\nhttps://www.inflearn.com/notices/1375488\n실무에서 사용하는 기술들을 많이 반영한 것 같은데, 대표적으로\nHelm 차트 Kustomize CRD 등등이 추가되고 기존 문제도 강화된다고 한다.\n","date":"2025-01-04T09:00:00+09:00","permalink":"https://joonlog.github.io/p/cka-%ED%9B%84%EA%B8%B0/","title":"CKA 후기"},{"content":"\nAWS도 본지가 1년이 넘었는데 이제야 후기를 남긴다.\nAWS-SAA는 쉽게 땄던 걸로 기억한다.\n사실 공부해야할 게 정말 많은 시험이지만 자격증 취득 자체가 목적이었기 때문에 udemy 강의(https://www.udemy.com/course/best-aws-certified-solutions-architect-associate/?couponCode=KEEPLEARNING) 1회독 하고 바로 문제풀이만 계속했다.\n가장 좋은 건 실제로 구축해보고 느낀 점을 찾아보는 걸 테지만 범위가 정말 넓다. 대학생 입장에서 크레딧 없이 AWS 서비스들을 사용하는 건 좀 부담스러웠고, 클라우드에 대해 처음 하는 공부였기 때문에 일단 이론적으로만 공부하고 나중에 프로젝트를 하면서 깊게 공부하기로 했다.\n문제풀이만으로 가능한 이유는 당연히 같은 유형의 문제가 계속 나오기 때문이다. 체감상 한 7-80%는 문제가 거의 동일하고 나머지 10% 정도만 처음 보는 유형이었다.\n문제는 Examtopics에 있는 AWS-SAA 문제를 4회독 정도 한 것 같다.\n여기가 좋은 이유는 문제 밑에 사람들이 자기가 맞다고 생각하는 답을 가지고 토론하는 댓글이 달리는데, 이것 덕분에 어떤 과정을 통해 답이 도출되는지 이해 할 수 있다. 또 사이트에서 체크한 정답 밑에 이렇게 사람들이 댓글로 기입한 답을 퍼센티지로 보여준다.\n단점은 답이 틀린 문제가 정말 많다. 틀렸다면 사람들이 기입한 답을 보고 정확한 답을 알 수 있지만 이건 퍼센티지로 나타나기 때문에 예를 들어 3번(55%) 이렇게 나오면 돌아버릴 것 같다. 이런 문제가 있다면 댓글 토론을 전부 확인하고 정확한 답을 찾아야 한다. 요즘은 GPT가 있으니 별로 문제가 안될 것 같기도?\n비용은 15만원 정도였는데 aws associate chanllenge를 신청해서 50% 할인을 받았던 걸로 기억한다.\n자격증 유효 기간은 3년이다.\n","date":"2025-01-01T09:00:00+09:00","permalink":"https://joonlog.github.io/p/aws-saa-c03-%ED%9B%84%EA%B8%B0/","title":"AWS-SAA C03 후기"},{"content":"\n작년에 본 시험에 대해 이제야 후기를 남긴다\n당시 나는 이제 막 인프라로 진로를 튼 상황이었기 때문에 첫 목표를 RHCSA 취득으로 정했다.\n내 리눅스 실력은 기본 명령어만 약간 아는 정도였고 서비스 구축은 할 줄 몰랐다.\nudemy 강의(https://www.udemy.com/course/unofficial-linux-redhat-certified-system-administrator-rhcsa-8/?couponCode=KEEPLEARNING)를 들으면서 기본 지식을 쌓았고, 문제들은 다른 블로그들을 전부 뒤져봤다.\n영어로 되어 있지만 요즘은 크롬 확장 프로그램 trancy 사용하면 한글로 더 편하게 볼 수 있을 것이다.\nRHCSA가 절대적인 난이도로 보면 취득한 자격증 중에 가장 난이도가 낮지만, 난 이 시험이 가장 어려웠다. 1번 탈락하여 재시험 보기도 했고.\n원래는 재시험 기회가 없었지만, 2023년부터 정책이 바뀐건지 retake 기회가 1번 주어졌고, 여기서 합격할 수 있었다.\n자격증 유효 기간은 3년이다.\n할인 쿠폰 시험 비용이 꽤 비싼 편인데, 할인 쿠폰도 많이 없고 할인율도 15%로 크진 않다.\n이 쿠폰은 RedHat 시험에 합격하면 나오는 쿠폰으로 1달? 3달? 동안 3명까지 쓸 수 있다. 그래서 종종 사람들이 커뮤니티에 공유하기도 하는데, 나는 redhat reddit에서 주워서 사용했다.\nredhat discount reddit으로 검색하면 쿠폰 공유하는 게시물을 찾을 수 있을 것이다.\n시험 환경 여권 준비 필수\n시험 장소는 에티버스러닝이었는데, 칸막이가 쳐진 작은 방에 2명이 시험 볼 수 있게 되어 있었다. 입장해서 준비된 노트북에 로그인을 하면 감독관이 채팅으로 말을 걸어온다. 준비된 웹캠에 여권을 보여주고 웹캠을 가지고 방 안을 꼼꼼히 살펴보도록 요구한다.\n이후 시험을 시작하고 VM으로 된 노드 2개를 가지고 작업하면 된다.\n유의사항 모든 문제 풀이들은 재부팅 후에도 동작해야 한다.\n예를 들어 httpd 서비스를 기동했다면, systemctl start httpd만 해선 안되고 반드시 enable 까지 해줘야 한다. 그래서 start와 enable을 같이 해주는 systemctl enable \u0026ndash;now httpd를 사용하면 좋다.\n서비스 이외에도 디스크 마운트, podman 문제도 마찬가지다. 따라서 문제를 다 풀었다면 노드들을 재부팅하고 잘 동작하는지 확인해야 한다.\n출제 문제 RHCSA : https://www.redhat.com/ko/services/training/ex200-red-hat-certified-system-administrator-rhcsa-exam?section=개요\n작년에는 RHCSA 8버전으로 시험을 봤었고 지금은 아마 9버전으로만 볼 수 것이다. 그때가 8에서 9로 넘어가는 과도기였는데 9버전 후기들로 파악할 수 있는 문제가 많지 않아 8버전으로 시험을 봤었다. 문제가 크게 바뀌진 않은 것 같지만 2~3문제정도에서 사용하는 서비스가 아예 달랐던 것으로 기억한다.\n구버전 문제기도 하고, 문제 풀이들은 다른 블로그에도 많으니 어떤 문제가 나왔는지 정도만 공유하겠다.\n루트 비밀번호 초기화\n부팅 프로세스 rd.break로 인터럽트 /sysroot 마운트 후 /sysroot 루트로 설정 후 비번 재설정 yum repository 구성\n문제에서 주어지는 baseurl을 가지고 repo파일 생성 dnf repolist all로 설정 됐는지 확인 ip, gateway, dns등 네트워크 인터페이스 구성\nnmtui 사용 NetworkManager 서비스 재시작 nmcli con show, nmcli dev status 같은 명령어로 잘 설정 됐는지 확인 hostname 설정\ncronjob 생성\nuser/group 관리 명령어\nuser에 group 추가, user nologin 설정 파일/디렉토리 권한 명령어\n파일/디렉토리 user:group 권한 설정 setgid 같은 파일/디렉토리 특수권한 설정 chrony 설정\ntimedatectl 사용 chrony.conf 설정 grep 후 리다이렉션 명령어\nselinux 켜져있고 82번 포트로 httpd 동작중일 때 오류 해결\nsemanage port 명령어로 http프로토콜 82포트 허용 방화벽에도 82포트 등록 httpd, firewall 재시작 디스크 생성 후 마운트\nfdisk로 디스크 붙이고 vg, lv, ext4 초기화, 마운트 작업 swap 파티션 만들고, 재부팅하면 자동으로 on 되게 설정\nfdisk, fstab, swapon 작업 vdo 구축\nRHCSA 9 에는 안나오는 걸로 알고 있다. autofs 구축\nRHCSA 9 에는 안나오는 걸로 알고 있다. podman 루트리스 방식으로 호스트의 디렉터리와 연결된 컨테이너를 재부팅 후 자동으로 실행되게 설정\n쉘 스크립트 파일 작성\n참고 블로그: https://not-to-be-reset.tistory.com/491\nhttps://blog.naver.com/PostView.naver?blogId=asd7005201\u0026logNo=222412331243\u0026parentCategoryNo=\u0026categoryNo=15\u0026viewDate=\u0026isShowPopularPosts=true\u0026from=search\nhttps://whitestudy.tistory.com/83\nhttps://blog.naver.com/moonlitare/222418066071\n","date":"2024-12-31T09:00:00+09:00","permalink":"https://joonlog.github.io/p/rhcsa-8-%ED%9B%84%EA%B8%B0/","title":"RHCSA 8 후기"},{"content":"Jekyll - Chirpy 테마 로컬 테스트 오류 Unable to monitor directories for changes because iNotify max watches exceeded.\nLinux의 inotify 파일 시스템 감시자의 최대 수 제한을 초과했기 때문에 발생\n1코어 1기가로 실행 중이기 때문에 제한인 것으로 추정\ninotify 값 변경해서 해결\n1 2 3 cat /proc/sys/fs/inotify/max_user_watches sudo sysctl fs.inotify.max_user_watches=524288 GitHub Action 의존성 오류 git push 후에 github action에서 발생한 오류 의존성 누락으로 인해 _sass/main.bundle.scss에서 @use \u0026lsquo;vendors/bootstrap\u0026rsquo; 참조 불가\n1 2 3 4 5 6 7 8 9 10 11 12 13 Error: Can\u0026#39;t find stylesheet to import. ╷ 1 │ @use \u0026#39;vendors/bootstrap\u0026#39;; │ ^^^^^^^^^^^^^^^^^^^^^^^^ ╵ main.bundle.scss 1:1 @use /home/runner/work/joonlog.github.io/joonlog.github.io/assets/css/jekyll-theme-chirpy.scss 1:1 root stylesheet Conversion error: Jekyll::Converters::Scss encountered an error while converting \u0026#39;assets/css/jekyll-theme-chirpy.scss\u0026#39;: Can\u0026#39;t find stylesheet to import. ------------------------------------------------ Jekyll 4.3.4 Please append `--trace` to the `build` command for any additional information or backtrace. ------------------------------------------------ 참고: https://github.com/cotes2020/jekyll-theme-chirpy/discussions/1809\nci 파일에 npm 의존성 주입 추가로 해결 가능 아래 코드를 jekyll.yml의 Build with Jekyll Step 위에 작성 1 2 name: npm build run: npm install \u0026amp;\u0026amp; npm run build GitHub Action 의존성 오류2 git push 후에 github action에서 발생한 오류 7시간 전까지 정상 동작 했는데 GitHub Action 중 오류\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ... npm warn EBADENGINE Unsupported engine { npm warn EBADENGINE package: \u0026#39;@semantic-release/npm@12.0.1\u0026#39;, npm warn EBADENGINE required: { node: \u0026#39;\u0026gt;=20.8.1\u0026#39; }, npm warn EBADENGINE current: { node: \u0026#39;v18.20.5\u0026#39;, npm: \u0026#39;10.8.2\u0026#39; } npm warn EBADENGINE } npm warn EBADENGINE Unsupported engine { npm warn EBADENGINE package: \u0026#39;@semantic-release/release-notes-generator@14.0.1\u0026#39;, npm warn EBADENGINE required: { node: \u0026#39;\u0026gt;=20.8.1\u0026#39; }, npm warn EBADENGINE current: { node: \u0026#39;v18.20.5\u0026#39;, npm: \u0026#39;10.8.2\u0026#39; } npm warn EBADENGINE } ... [js] [!] Error: Cannot find module \u0026#39;/home/runner/work/joonlog.github.io/joonlog.github.io/node_modules/@jridgewell/gen-mapping/dist/gen-mapping.umd.js\u0026#39; [js] at createEsmNotFoundErr (node:internal/modules/cjs/loader:1177:15) [js] at finalizeEsmResolution (node:internal/modules/cjs/loader:1165:15) [js] at resolveExports (node:internal/modules/cjs/loader:590:14) [js] at Function.Module._findPath (node:internal/modules/cjs/loader:664:31) [js] at Function.Module._resolveFilename (node:internal/modules/cjs/loader:1126:27) [js] at Function.Module._load (node:internal/modules/cjs/loader:981:27) [js] at Module.require (node:internal/modules/cjs/loader:1231:19) [js] at require (node:internal/modules/helpers:177:18) [js] at Object.\u0026lt;anonymous\u0026gt; (/home/runner/work/joonlog.github.io/joonlog.github.io/node_modules/@jridgewell/source-map/dist/source-map.cjs:6:18) [js] at Module._compile (node:internal/modules/cjs/loader:1364:14) 정상 동작 하던 커밋으로 롤백했는데도 같은 에러 발생 ⇒ GitHub 쪽 버전 문제?\nwarn 발생하는 node 문제인가 싶어 jekyll.yml에 아래 코드 추가해서 node 22이상으로 사용 후 해결됨 - npm 설치 후 빌드 시 node 20 이상이 필요하게 변경된 것으로 추정 ⇒ 버전 지정 안하면 깃허브는 node를 18버전으로 사용\n1 2 3 4 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: \u0026#39;22.x\u0026#39; # Node 22 이상 사용 ","date":"2024-12-29T09:00:00+09:00","permalink":"https://joonlog.github.io/p/github-%EB%B8%94%EB%A1%9C%EA%B7%B8jekyll-chirpy-%EC%98%A4%EB%A5%98-troubleshooting/","title":"GitHub 블로그(Jekyll Chirpy) 오류 TroubleShooting"},{"content":"커밋을 해도 깃허브에 잔디가 보이지 않아서 찾아보니 잔디가 심어지는 것에 조건이 있었다.\ngithub 이메일 계정과 로컬의 이메일 정보가 같아야 한다 branch는 main 혹은 gh-pages 둘 중 하나에서 커밋해야 한다 forked repo가 아니어야 한다 내 경우에는 git config에 이메일이 등록되어 있지 않아서 잔디가 심어지지 않았다.\n1 2 git config --global --list git config --global user.email “\u0026lt;email\u0026gt;” 이메일 추가 후 해결\n참고 공식문서: https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/managing-contribution-settings-on-your-profile/why-are-my-contributions-not-showing-up-on-my-profile\n","date":"2024-12-27T09:00:00+09:00","permalink":"https://joonlog.github.io/p/github-%EC%9E%94%EB%94%94-%EB%88%84%EB%9D%BD-troubleshooting/","title":"GitHub 잔디 누락 TroubleShooting"},{"content":" 깃허브 리포지토리 clone 1 git clone \u0026lt;repo-url\u0026gt; 깃허브 Pull / Push 1 2 3 4 git pull origin main git add . git commit -am \u0026#34;commit message\u0026#34; git push origin main 깃허브 rebase 1 2 3 4 5 6 7 8 9 10 11 # git pull 할 때 rebase 방식을 사용하도록 설정 # 로컬 브랜치의 변경 사항을 원격 브랜치의 끝에 위치 # 기본 설정은 merge git config pull.rebase true # 다른 브랜치의 커밋을 main 브랜치로 이동 # 커밋 히스토리를 정리하거나 브랜치를 재구성하는 데 사용 git rebase main # rebase 작성 취소 git rebase --abort 깃허브 리포지토리 관리 1 2 git remote add origin \u0026lt;repo-url\u0026gt; git remote set-url origin \u0026lt;repo-url\u0026gt; 깃허브 브랜치 변경 1 2 3 git show-ref # default branch가 master로 되어있을 경우 git branch -m main 커밋 히스토리 1 git log --online 최근 커밋 취소 1 2 3 4 5 # 코드까지 돌아감 git reset --hard HEAD~2 # 코드는 그대로 두고 커밋만 삭제 git reset --soft HEAD~1 ","date":"2024-12-26T09:00:00+09:00","permalink":"https://joonlog.github.io/p/github-%EB%AA%85%EB%A0%B9%EC%96%B4-%EC%A0%95%EB%A6%AC/","title":"GitHub 명령어 정리"},{"content":"Jekyll Chirpy 테마 https://github.com/cotes2020/jekyll-theme-chirpy\nJekyll Chirpy 사용 이유 기술 블로그용으로 가장 적합 다른 Jekyll 테마들은 스타트업 블로그 스타일이나 쇼핑몰에서 사용할 것 같은 많은데, 개인용 기술블로그 테마로는 가장 깔끔하다고 느꼈다 카테고리, 타임라인 별로 작성한 글을 볼 수 있고, 블로그 내 검색 기능이 빨라서 좋았다 게시글 내에 있는 항목 별 바로가기 기능과, 게시글마다 읽는데 얼마나 걸리는지 나와있는 것도 맘에 들었다 많은 사용자 커스터마이징 할 때 오류가 발생해도 사용자가 많으면 고치기가 수월하다 특히 한국인 사용자가 많은 테마로 더 편한 면이 있다 Jekyll Chirpy 구조 _config.yml Jekyll 블로그의 기본 설정 파일 사이트 제목, 설명, URL, 언어, 플러그인 등 전반적인 설정을 정의 _data 언어 설정, 사이드바 구성, 카테고리 또는 태그와 같은 반복적인 데이터 관리에 사용 _includes 재사용이 가능한 header, footer, banner와 같은 HTML을 저장 _layouts 페이지 레이아웃 템플릿을 저장 default.html, post.html 등의 파일이 포함되어 각 페이지의 구조를 정의 _posts 블로그 글을 작성하는 공간 각 파일은 YYYY-MM-DD-title.md 형식으로 저장 Markdown 형식으로 작성하며, YAML front matter로 메타데이터(제목, 날짜, 태그, 카테고리)를 지정 _site Jekyll 빌드 결과물이 저장되는 디렉터리 사용자가 직접 수정하지 않으며, GitHub Actions 또는 로컬 빌드 시 생성 _tabs 블로그의 탭 네비게이션을 구성 about.md, archive.md와 같은 파일을 포함 탭 메뉴에 표시되는 페이지의 내용과 경로를 정의 assets 정적 파일(css, 이미지, JS 파일 등)을 저장 css/, img/, js/ 등의 하위 디렉터리가 포함 _sass Sass 파일을 저장 Chirpy 테마의 CSS를 수정하려면 이 디렉터리를 편집 .github GitHub Actions 설정이 위치 workflows/ 디렉터리에는 Chirpy의 CI/CD 파이프라인 설정이 저장됨 깃허브 블로그 구축 과정 블로그 Repo 생성 저장소 생성 깃허브에서 username.github.io Repo 생성 GitHub Pages 활성화 저장소 설정 \u0026gt; Pages 섹션에서 브랜치 선택 후 활성화 블로그 업로드 작성 후 저장소에 Push → GitHub Pages가 자동으로 GitHub Actions 사용해 배포 Jekyll - Chirpy 테마 적용 Chirpy 복사 chirpy를 단순 fork하면 이후에 깃허브 잔디 심기가 곤란하니까 clone 후 copy 1 2 3 4 git clone git@github.com:joonlog/joonlog.github.io.git git clone https://github.com/cotes2020/jekyll-theme-chirpy.git chirpy cp -r chirpy/* joonlog cp -r chirpy/.* joonlog Git 설정 1 2 3 4 cd joonlog git remote set-url origin git@github.com:joonlog/joonlog.github.io.git git pull origin main 로컬 테스트(선택) 의존성 설치 1 2 3 4 5 6 7 8 9 sudo dnf install ruby ruby-devel gcc make -y sudo dnf groupinstall \u0026#34;Development Tools\u0026#34; sudo gem install bundler bundle install npm install bundle exec jekyll serve http://localhost:4000 깃허브 블로그 배포 Git Push 1 2 3 git add . git commit -am \u0026#34;test\u0026#34; git push origin main Repo - 저장소 설정 - 페이지 섹션 - 배포 형식을 GitHub Actions로 변경 GitHub Actions 말고 기존 껄로 배포하면 index.html만 나타남 - Configure 후 npm 의존성을 “Build with Jekyll” 단계 전에 추가 후에 Commit ```bash name: npm build run: npm install \u0026amp;\u0026amp; npm run build ``` - ./github/workflows/starter/pages-deploy.yml 삭제 - 삭제 안하면 pages-deploy.yml도 같이 실행되어서 오류 발생 여기까지 하고 브라우저에서 username.github.io로 접속하면 블로그에 테마가 적용된 게 보일 것이다.\n글 포스팅 구분 새 페이지 추가 새 게시물 추가 목적 고정 콘텐츠(정적 페이지) 작성 블로그 글(동적 콘텐츠) 작성 위치 프로젝트 루트 또는 서브 디렉토리 _posts/ 디렉토리 파일명 규칙 자유로운 이름 (about.md) YYYY-MM-DD-title.md 형식 URL /about, /contact 등 /year/month/day/title 형식 목록 표시 목록에 포함되지 않음 블로그 목록에 자동 포함 형식 : yyyy-mm-dd-제목.md 확장자는 .md 또는 .markdown 중간에 공백을 넣지 않음 작성한 파일은 _posts 디렉토리에 위치 Front Matter 사이트의 게시물에 대해 제목 및 레이아웃과 같은 변수와 메타데이터를 설정하기 위해 Markdown 또는 HTML 파일의 맨 위에 YAML 전문을 추가\n파일명: YYYY-MM-DD-title.md\n1 2 3 4 5 6 --- title : Rocky8 MySQL Replication \u0026amp; HAproxy 연동 date : 2024-12-10 09:00:00 +09:00 categories : [Linux, DB] tags : [rocky8, mysql, haproxy, replication] #소문자만 가능 --- 블로그에 포스팅이 되었는지 확인\n참고 공식문서: https://docs.github.com/ko/pages\njekyll chirpy 테마: https://github.com/cotes2020/jekyll-theme-chirpy\n","date":"2024-12-25T09:00:00+09:00","permalink":"https://joonlog.github.io/p/github-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EA%B5%AC%EC%B6%95-jekyll-chirpy/","title":"GitHub 블로그 구축 - Jekyll Chirpy"},{"content":"목표 pub 1대, priv 1대 서버 생성 후 yum proxy 통하여 priv 서버에서 yum 설치 가능하게 설정 외에도 pub 서버에서 yumdownloader 명령어 통해서 rpm 빼와서 priv 서버로 옮긴 후 rpm 통한 설치 mariadb 최신버전 설치로 테스트 환경 설정 OS: Rocky Linux 8.10 작업 과정 1. Yum Proxy 1) Public 서버 사용자 생성 1 2 useradd -m -G wheel yumpub echo \u0026#34;yumpub\u0026#34; | passwd --stdin yumpub 방화벽 설정 1 2 3 sudo systemctl enable --now firewalld sudo firewall-cmd --permanent --add-port=3128/tcp sudo firewall-cmd --reload squid 설치 1 2 3 sudo dnf update --exclude=kernel* -y sudo dnf install -y squid sudo systemctl enable --now squid squid.conf 수정 1 2 3 4 5 6 7 8 sudo vi /etc/squid/squid.conf # private server 허용 acl priv_network src \u0026lt;pri-server-ip\u0026gt;/24 http_access allow priv_network http_access deny all sudo systemctl restart squid 2) Private 서버 사용자 생성 1 2 3 4 5 6 7 ssh root@\u0026lt;pri-server-ip\u0026gt; useradd -m -G wheel yumpri echo \u0026#34;yumpri\u0026#34; | passwd --stdin yumpri exit ssh yumpri@\u0026lt;pri-server-ip\u0026gt; 방화벽 설정 1 2 sudo systemctl enable --now firewalld sudo firewall-cmd --reload yum.conf 수정 pub 서버를 프록시로 사용 1 2 3 sudo vi /etc/yum.conf proxy=http://\u0026lt;public-server-ip\u0026gt;:3128 기존에 하던 대로 dnf update 혹은 repo 생성해서 패키지 설치 repo 생성 1 2 3 4 5 6 7 8 9 sudo vi /etc/yum.repos.d/Rocky-Extras.repo [extras] name=Rocky Linux $releasever - Extras mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch\u0026amp;repo=extras-$releasever gpgcheck=1 enabled=1 countme=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rockyofficial extras의 패키지 epel-release 검색 1 2 3 sudo dnf clean all sudo dnf repolist sudo dnf search epel-release 2. yumdownloader 1) Public 서버 yumdownloader로 rpm 다운로드 1 2 3 4 5 6 sudo dnf update --exclude=kernel* -y sudo dnf install -y yum-utils perl # --resolve는 의존성 포함 yumdownloader --resolve mariadb yumdownloader --resolve mariadb-server yumdownloader --resolve perl-DBD-MySQL perl-DBI rpm 파일 private 서버로 복사 1 2 scp mariadb*.rpm yumpri@\u0026lt;priv-server-ip\u0026gt;:/tmp scp perl*.rpm yumpri@\u0026lt;priv-server-ip\u0026gt;:/tmp 2) Private 서버 rpm 파일로 mariadb 설치 1 2 3 4 sudo rpm -ivh /tmp/perl*.rpm sudo rpm -ivh /tmp/mariadb*.rpm sudo rpm -qa | grep mariadb MariaDB 테스트 1 2 3 4 5 6 7 mysql --version sudo systemctl enable --now mariadb mysql_secure_installation mysql -u root 로그 확인 squid 로그 1 sudo tail -f /var/log/squid/access.log 트러블슈팅 yum proxy 실패\n/var/log/squid/access.log 확인 시 priv 서버 ip 가 없고 모르는 사설 ip가 있음 tail 걸고 priv에서 yum search했을 때 로그가 발생한걸로 봐서 모르는 사설 ip가 priv 서버의 트래픽인 것으로 확인 /etc/yum.conf에 공인 IP로 설정해서 공인 IP에서 pub 서버로 트래픽이 이동하는 과정에서 자동으로 NAT되어 새로운 사설 ip가 할당 된 것으로 보임 애초에 yum.conf에 공인 IP 말고 사설 IP를 설정해야 했지만 proxy가 안되는 원인은 아님 yum은 자신이 활성화한 repo만 squid 프록시를 통해 접근 priv에 repo가 없어도 pub에 있는 repo를 통해 priv에 설치가 되는 줄 앎 그게 아니고, squid는 단순히 proxy만 해주고 pub는 squid 서버의 역할(NAT 서버) 기존에 하던 대로 dnf update 하거나 repo 만들어서 해결 참고 yum proxy: https://hgko1207.github.io/2020/09/28/linux-3/\nyumdownloader: https://wldnjd2.tistory.com/53\n","date":"2024-12-20T09:00:00+09:00","permalink":"https://joonlog.github.io/p/yum-proxy-%EC%84%A4%EC%A0%95/","title":"Yum Proxy 설정"},{"content":"목표 Linux 서버에 OpenVPN 서버를 구축 OpenVPN 클라이언트와 서버 간의 연결 테스트 수행 서버와 동일한 랜(KT)의 윈도우에서 연결 윈도우 호스트(내 컴퓨터)에서 연결 AWS 리눅스(외부 서브넷)에서 연결 연결 후 VPN 서버와 동일한 랜의 리눅스로 SSH 환경 설정 OS Rocky 8.10 AL 2023 Window 2019 VPN: openvpn 2.4.12 개념 및 설정 OpenVPN VPN 클라이언트와 서버 간 보안 터널 제공 포트 변경: 1194/UDP → 443/TCP HTTPS 트래픽으로 위장 (차단 회피) 기업 네트워크나 공공 네트워크(호텔, 카페 등)에서 VPN 연결을 허용하지 않는 경우, VPN 트래픽이 HTTPS 트래픽처럼 보이도록 설정할 수 있어, 네트워크 방화벽이 VPN을 차단하는 것을 방지 TCP 기반 트래픽을 허용 UDP(기본 1194)보다 느리긴 하지만, 패킷 손실이나 불안정한 네트워크에서도 더 신뢰할 수 있는 통신을 제공 VPN 서버 방화벽에서 NAT 설정 필요 SNAT: 서버의 특정 NI를 통해 나가는 트래픽의 **소스 IP**를 해당 NI의 IP로 변경 DNAT: 외부에서 서버의 특정 NI로 들어오는 트래픽의 **목적지 IP**를 원래의 내부 IP로 복원 OpenVPN 서버에서 필요한 파일 /etc/openvpn/server ca.crt,server.key,server.crt,ta.key ,server.conf OpenVPN 클라이언트에서 필요한 파일 client1.ovpn Window: C:\\Program Files\\OpenVPN\\config\\client1.ovpn Linux: openvpn --config \u0026lt;경로\u0026gt;/client1.ovpn --daemon 관련 파일 파일 경로 설명 생성 명령어 ca.key 인증서 서명을 위한 개인 키 ./easyrsa init-pki ca.crt 공개 인증서 (클라이언트/서버 공통) ./easyrsa init-pki server.key 서버 개인 키 ./easyrsa gen-req server nopass server.req 서버 인증서 서명 요청 (CSR) ./easyrsa gen-req server nopass server.crt 서버 인증서 ./easyrsa gen-req server nopass ta.key 사전 공유 키. OpenVPN의 TLS용 추가 보안. server.conf에 사용 설정 openvpn --genkey --secret ta.key client1.key 클라이언트 개인 키 ./easyrsa gen-req client1 nopass client1.req 클라이언트 인증서 서명 요청 (CSR) ./easyrsa gen-req client1 nopass client1.crt 클라이언트 인증서 ./easyrsa gen-req client1 nopass client1.ovpn base.conf, ca.crt, client1.key, client1.crt, ta.key 클라이언트 설정 파일과 4개의 인증 파일을 사용하는 OpenVPN 클라이언트 파일 쉘 스크립트 사용 server.conf 서버 설정 파일 /usr/share/doc/openvpn/sample/sample-config-files/ 복사 base.conf 클라이언트 설정 파일 /usr/share/doc/openvpn/sample/sample-config-files/ 복사 작업 과정 1. CA 인증서/키 생성 사용자 생성 1 2 useradd -m -G wheel vpnlin echo \u0026#34;vpnlin\u0026#34; | passwd --stdin vpnlin 패키지 설치 1 2 3 4 sudo dnf update --exclude=kernel* -y sudo dnf install -y epel-release sudo dnf install -y easy-rsa easy-rsa 설정 루트 인증서 ca.crt, 개인 키 ca.key 생성 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 mkdir ~/easy-rsa ln -s /usr/share/easy-rsa/3/* ~/easy-rsa/ chmod 700 ~/easy-rsa cd ~/easy-rsa ./easyrsa init-pki vi vars set_var EASYRSA_REQ_COUNTRY \u0026#34;KR\u0026#34; set_var EASYRSA_REQ_PROVINCE \u0026#34;Seoul\u0026#34; set_var EASYRSA_REQ_CITY \u0026#34;Seoul\u0026#34; set_var EASYRSA_REQ_ORG \u0026#34;ncloud24\u0026#34; set_var EASYRSA_REQ_EMAIL \u0026#34;\u0026lt;Domain\u0026gt;\u0026#34; set_var EASYRSA_REQ_OU \u0026#34;Cloud\u0026#34; set_var EASYRSA_ALGO \u0026#34;ec\u0026#34; set_var EASYRSA_DIGEST \u0026#34;sha512\u0026#34; ./easyrsa build-ca CA Passphrase: capass 2. VPN 서버 인증서 서명 패키지 설치 1 sudo dnf install -y openvpn 1) VPN 서버 키 생성 개인키 server.key, 인증서 서명 요청(CSR) server.req 생성 1 2 3 ./easyrsa gen-req server nopass sudo cp ~/easy-rsa/pki/private/server.key /etc/openvpn/server/ 2) CA서버에서 VPN CSR 서명 CSR 서명 1 2 3 4 5 6 cd ~/easy-rsa # ca와 vpn이 같은 서버에 있다면 import 불필요 # ./easyrsa import-req ~/easy-rsa/pki/reqs/server.req server ./easyrsa sign-req server server CA 개인키 암호 입력 3) VPN 서버에서 OpenVPN 암호화 자료 구성 사전공유키 ta.key 생성 사전공유키 ta.key도 위 경로로 복사 현재 /etc/openvpn/server 경로의 파일들 서버 개인키 server.key 서버 인증서 server.crt 루트 인증서 ca.crt 사전공유키 ta.key 1 2 3 4 5 6 cd ~/easy-rsa openvpn --genkey --secret ta.key sudo cp ta.key /etc/openvpn/server sudo cp ~/easy-rsa/pki/issued/server.crt /etc/openvpn/server sudo cp ~/easy-rsa/pki/ca.crt /etc/openvpn/server 3. VPN 클라이언트 인증서 서명 1) VPN 서버에서 클라이언트 키 생성 VPN 서버에서 클라이언트 설정 client1.key, client1.req 생성 1 2 3 4 5 6 7 mkdir -p ~/client-configs/keys chmod -R 700 ~/client-configs cd ~/easy-rsa ./easyrsa gen-req client1 nopass cp pki/private/client1.key ~/client-configs/keys/ 2) CA 서버에서 VPN 클라이언트 CSR 서명 CSR 서명 1 2 3 4 5 6 cd ~/easy-rsa # ca와 vpn이 같은 서버에 있다면 import 불필요 # ./easyrsa import-req ~/easy-rsa/pki/reqs/client1.req client1 ./easyrsa sign-req client client1 CA 개인키 암호 입력 3) VPN 서버에서 VPN 클라이언트용 키 관리 ~/client-configs/keys 밑의 파일들 클라이언트 개인키 client1.key 클라이언트 인증서 client1.crt 루트 인증서 ca.crt 사전공유키 ta.key 서버에서 사용하는 파일들이 아닌, 클라이언트에 전달하기 전 모으기 1 2 3 4 5 cp ~/easy-rsa/ta.key ~/client-configs/keys/ cp ~/easy-rsa/pki/issued/client1.crt ~/client-configs/keys/ sudo cp /etc/openvpn/server/ca.crt ~/client-configs/keys/ sudo chown vpnlin.vpnlin ~/client-configs/keys/* 4. VPN 서버 설정 1) 서버 server.conf 설정 VPN 가상 서브넷 대역은 기본 설정인 \u0026lt;VPN-IP\u0026gt;/24로 설정 1 2 sudo cp /usr/share/doc/openvpn/sample/sample-config-files/server.conf /etc/openvpn/server/ sudo vi /etc/openvpn/server/server.conf 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 ;tls-auth ta.key 0 # tls 보안 설정. crypt는 핸드셰이크 패킷의 암호화 인증도 수행 tls-crypt ta.key ;cipher AES-256-CBC cipher AES-256-GCM # SHA256 알고리즘 추가 auth SHA256 # Diffie-Hellman 제거 ;dh dh2048.pem dh none # 권한 없이 실행 user nobody group nobody ### 클라이언트에게 172 대역의 트래픽을 VPN을 경유하도록 라우팅 규칙 추가 ### 아래의 redirect-gateway가 활성화 되어 있다면 의미 없음 # push \u0026#34;route \u0026lt;Private IP\u0026gt; 255.255.0.0\u0026#34; # VPN 클라이언트 기본 게이트웨이를 VPN 게이트웨이로 변경하여 모든 트래픽을 VPN 서버를 통해 리디렉션하도록 강제 # VPN 클라이언트에서 인바운드는 제약이 되지 않음 # 아웃바운드가 VPN 게이트웨이를 사용하게 되는 것 고려(SSH) push \u0026#34;redirect-gateway def1 bypass-dhcp\u0026#34; # 클라이언트에게 OpenDNS resolvers 사용하라고 지시 # VPN 서버에서 신뢰할 수 있는 DNS 서버 지정 push \u0026#34;dhcp-option DNS 208.67.222.222\u0026#34; push \u0026#34;dhcp-option DNS 208.67.220.220\u0026#34; # 클라이언트 간 통신 허용 시 주석 해제 ;client-to-client # OpenVPN은 기본적으로 1194/UDP 사용. 포트 변경 가능 # 443/tcp를 사용한다면 아래 설정 port 443 proto tcp ;proto udp # tcp 사용하면 0로 변경 explicit-exit-notify 0 # 키 발급 다른 이름으로 했다면 수정 ca ca.crt cert server.crt key server.key # 로그 위치 변경 # 기본 경로는 /run/openvpn/openvpn-status.log인데 selinux가 활성화되어있다면 문제 발생 status /var/log/openvpn-status.log 2) 네트워킹 구성 조정 VPN 서버가 내부 네트워크(10…)와 실제 네트워크(172…)를 연결할 수 있도록 IP 포워딩을 활성화 1 2 3 4 sudo vi /etc/sysctl.conf net.ipv4.ip_forward=1 sudo sysctl -p 3) 방화벽 구성 openvpn 방화벽 설정 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 sudo systemctl enable --now firewalld # VPN 가상 인터페이스 tun0의 트래픽은 VPN 클라이언트에서 온 트래픽으로 암호화 되어 있다고 판단됨 # trusted 존은 지정된 NI나 IP 대역에 대해 방화벽 필터링을 생략 sudo firewall-cmd --get-active-zones sudo firewall-cmd --permanent --zone=trusted --add-interface=tun0 sudo firewall-cmd --permanent --zone=trusted --add-service openvpn sudo firewall-cmd --reload # 모든 트래픽에 대해 클라이언트와 외부 네트워크 간의 트래픽을 중계 # 어떤 트래픽이든 목적지 ip로의 sudo firewall-cmd --permanent --add-masquerade ### eth0인 NI로의 NAT 설정하는 명령어 ### 다른 NI로의 NAT은 허용하지 않게 됨 =\u0026gt; 현재 환경에서는 불필요한 명령어 # DEVICE=$(ip route | awk \u0026#39;/^default via/ {print $5}\u0026#39;) # sudo firewall-cmd --permanent --direct --passthrough ipv4 -t nat -A POSTROUTING -s 10.8.0.0/24 -o $DEVICE -j MASQUERADE ### NAT 규칙 확인 # sudo iptables -t nat -L POSTROUTING -n -v sudo firewall-cmd --permanent --add-port=443/tcp sudo firewall-cmd --reload 4) OpenVPN 서버 시작 1 sudo systemctl enable --now openvpn-server@server 5) VPN 클라이언트 설정 클라이언트 base.conf 설정 1 2 3 mkdir -p ~/client-configs/files cp /usr/share/doc/openvpn/sample/sample-config-files/client.conf ~/client-configs/base.conf vi ~/client-configs/base.conf 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 서버 ip/port 지정 remote \u0026lt;공인 IP\u0026gt; 443 proto TCP # 권한 없이 실행 user nobody group nobody # client1.ovpn 파일에 인증 파일들을 다 포함시키기 때문에 주석처리 ;ca ca.crt ;cert client.crt ;key client.key ;tls-auth ta.key 1 cipher AES-256-GCM auth SHA256 key-direction 1 # 비밀번호 캐싱 방지 auth-nocache 6) client1.ovpn 파일 생성 client1.opvn 파일 생성하는 쉘 스크립트 작성 기본 구성 파일 base.conf \u0026lt;ca\u0026gt; 태그 사이에 서버의 루트 인증서 ca.crt \u0026lt;cert\u0026gt; 태그 사이에 클라이언트 인증서 client1.crt \u0026lt;key\u0026gt; 태그 사이에 클라이언트 키 client1.key \u0026lt;tls-crypt\u0026gt; 태그 사이에 사전공유키 ta.key 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 vi ~/client-configs/make_config.sh #!/bin/bash # 첫 번째 인수: 클라이언트 식별자 # ex) ./make_config.sh client1 KEY_DIR=~/client-configs/keys OUTPUT_DIR=~/client-configs/files BASE_CONFIG=~/client-configs/base.conf TEMP_FILE=/tmp/temp_ovpn_config cat ${BASE_CONFIG} \u0026gt; ${TEMP_FILE} echo -e \u0026#39;\u0026lt;ca\u0026gt;\u0026#39; \u0026gt;\u0026gt; ${TEMP_FILE} cat ${KEY_DIR}/ca.crt \u0026gt;\u0026gt; ${TEMP_FILE} echo -e \u0026#39;\u0026lt;/ca\u0026gt;\\n\u0026lt;cert\u0026gt;\u0026#39; \u0026gt;\u0026gt; ${TEMP_FILE} cat ${KEY_DIR}/${1}.crt \u0026gt;\u0026gt; ${TEMP_FILE} echo -e \u0026#39;\u0026lt;/cert\u0026gt;\\n\u0026lt;key\u0026gt;\u0026#39; \u0026gt;\u0026gt; ${TEMP_FILE} cat ${KEY_DIR}/${1}.key \u0026gt;\u0026gt; ${TEMP_FILE} echo -e \u0026#39;\u0026lt;/key\u0026gt;\\n\u0026lt;tls-crypt\u0026gt;\u0026#39; \u0026gt;\u0026gt; ${TEMP_FILE} cat ${KEY_DIR}/ta.key \u0026gt;\u0026gt; ${TEMP_FILE} echo -e \u0026#39;\u0026lt;/tls-crypt\u0026gt;\u0026#39; \u0026gt;\u0026gt; ${TEMP_FILE} # 최종 파일 생성 mv ${TEMP_FILE} ${OUTPUT_DIR}/${1}.ovpn client1.opvn 생성 1 2 3 chmod 700 ~/client-configs/make_config.sh . ~/client-configs/make_config.sh client1 5. VPN 클라이언트 연결 테스트 1) KT Cloud 같은 공인 ip를 사용하는 윈도우 Window OpenVpn 설치\nsamba opvn 파일 전송\nRocky8 Samba 파일 공유 client1.ovpn /samba로 복사 1 2 sudo cp ~/client-configs/files/client1.ovpn /samba sudo chown smbuser1.smbuser1 /samba/client1.ovpn client1.ovpn C:\\Program Files\\OpenVPN\\config로 복사\nOpenVPN 연결 성공\n💡 연결됐지만 같은 공인 ip를 쓰기 때문에 암호화 통신 이외에 다른 이점이 전무\n2) 호스트 윈도우 VPN 클라이언트 SSH → VPN 서버 → Private 서버 SSH 패킷 흐름 VPN 클라이언트 VPN 연결로 가상 IP 부여받음 Private 서버로 SSH 요청 VPN 서버 NAT 규칙이 없다면, 클라이언트의 소스 IP를 그대로 전달 Private 서버는 가상 IP\u0026lt;VPN IP\u0026gt; 대역을 인식하지 못하므로, 응답을 클라이언트로 되돌릴 방법이 없음 (SNAT) NAT는 클라이언트 패킷의 소스 IP를 VPN 서버의 사설 IP로 변경 Private 서버 VPN 서버의 사설 IP에서 오는 패킷으로 인식하고 응답을 다시 VPN 서버로 반환 VPN 서버 (NAT 복원) 패킷을 받아 다시 NAT를 통해 원래의 클라이언트 IP로 변경한 후 클라이언트로 전달 연결 성공 3) AWS 리눅스 VPN 서버 server.conf 수정 VPN 클라이언트에서 내 PC IP로의 트래픽은 기본 게이트웨이를 사용하도록 설정 내 PC에서 VPN 클라이언트로 SSH하기 위함 1 push \u0026#34;route \u0026lt;회사 IP\u0026gt; 255.255.255.255 net_gateway\u0026#34; VPN 서버에서 VPN 클라이언트 AWS Linux에 client1.ovpn 전송 1 scp -i pjt-keypair.pem ~/client-configs/files/client1.ovpn ec2-user@\u0026lt;AWS IP\u0026gt;:~ AWS에서 생성한 linux에서 클라이언트 연결 VPN 클라이언트 AWS Linux에서 openvpn 연결 1 2 3 4 sudo dnf install -y openvpn sudo openvpn --config client1.ovpn --daemon ps aux | grep openvpn VPN 클라이언트에서 VPN 서버를 통해 CSP의 Private 서버로 SSH 내 PC → (SSH 연결) → AWS Linux → (VPN 연결) → VPN 서버 1 ssh root@\u0026lt;VPN 서버 위치의 다른 서버 IP\u0026gt; 연결 성공 로그 확인 명령어 server.conf에서 수정한 경로로 로그 확인 1 tail -f /var/log/openvpn-status.log 트러블슈팅 openvpn 실행 시 오류\nThu Nov 28 10:49:47 2024 disabling NCP mode (\u0026ndash;ncp-disable) because not in P2MP client or server mode Options error: You must define TUN/TAP device (\u0026ndash;dev)\nclinet1.ovpn 파일에 base.conf 가 누락돼서 발생한 오류 클라이언트 연결 경고\nWARNING: this configuration may cache passwords in memory \u0026ndash; use the auth-nocache option to prevent this\n기본적으로 OpenVPN은 클라이언트 인증 시 사용된 인증 정보를 메모리에 캐시 base.conf에 auth-nocache 추가하여 해결 sudo firewall-cmd --permanent --direct --passthrough ipv4 -t nat -A POSTROUTING -s \u0026lt;VPN-IP\u0026gt;/24 -o $DEVICE -j MASQUERADE 방화벽 NAT 룰 추가 시 명령어로 삭제 불가\n/etc/firewalld/direct.xml 파일 열고 직접 삭제로 해결 vpn 연결 시 클라이언트에서 서버의 사설 ip대역의 다른 서버로 접속 불가\nvpn 가상 사설 ip 10 대역에서 vpn 서버 사설 ip 172 로의 NAT 설정이 안돼 있기 때문 ~~sudo firewall-cmd --permanent --direct --passthrough ipv4 -t nat -A POSTROUTING -s \u0026lt;VPN-IP\u0026gt;/24 -d \u0026lt;Private-IP\u0026gt;/24 -o $DEVICE -j MASQUERADE 방화벽 NAT 추가~~ sudo firewall-cmd --permanent --add-masquerade 로 해결 ~~server.conf에서 push “route \u0026lt;Private-IP\u0026gt; 255.255.0.0” 라우팅 추가~~ push \u0026quot;redirect-gateway def1 bypass-dhcp” 명령어로 인해 위의 route 규칙 추가는 무의미 ssh로 접속한 aws linux에서 openvpn 실행 시 ssh 연결 끊김\nvpn을 실행하면 vpn 클라이언트의 디폴트 게이트웨이 설정이 변경되어 모든 트래픽이 VPN 게이트웨이를 통해 라우팅됨 push \u0026quot;redirect-gateway def1 bypass-dhcp” 이 설정 때문 ssh 세션의 응답 패킷이 vpn 게이트웨이를 통해 전송됨 SSH 요청은 공인 IP → AWS 리눅스 SSH 응답은 VPN 터널 → VPN 서버 → 잘못된 경로 요청은 잘 가는데 응답이 VPN게이트웨이로 가는 게 문제 push \u0026quot;route \u0026lt;회사-IP\u0026gt; 255.255.255.255 net_gateway\u0026quot; AWS 리눅스(VPN 클라이언트)에 PC 공인IP로의 트래픽은 기본 게이트웨이를 사용하도록 라우팅하여 해결 참고 OpenVPN 설치 https://openvpn.net/community-resources/installing-openvpn/\nOpenVPN 구축 https://www.howtoforge.com/how-to-install-and-configure-openvpn-server-on-rocky-linux-9/\n","date":"2024-12-19T09:00:00+09:00","permalink":"https://joonlog.github.io/p/openvpn-%EA%B5%AC%EC%B6%95/","title":"OpenVPN 구축"},{"content":"목표 특정 포트를 포워딩하여 외부 클라이언트가 내부 리소스에 접근 가능하도록 구성 특정 웹사이트에 대한 외부 접속을 방화벽 규칙을 통해 차단 환경 설정 OS: Rocky 8.10 개념 및 설정 FirewallD firewalld는 내부적으로 iptables를 사용하여 규칙을 관리 iptables 명령으로 추가한 규칙은 firewalld에서 생성한 규칙보다 우선 적용됨 AllowZoneDrifting은 하나의 NI가 여러 Zone에 속한 경우 Zone 간 규칙을 드리프트 Zone 경계 간 규칙을 느슨하게 만들어 의도치 않은 트래픽 허용 가능성 이후 버전에서는 제거될 예정 L4 방화벽이기 때문에 NAT를 통해 ip가 변환되는 공인 ip들은 제어하기 어려움 firewalld의 한계 작업 과정 1. Firewall 포트포워딩 1) FW1 방화벽 설정 사용자 생성 1 2 useradd -m -G wheel fw1 echo \u0026#34;fw1\u0026#34; | passwd --stdin fw1 패키지 설치 1 2 3 4 sudo dnf update --exclude=kernel* -y echo \u0026#34;\u0026lt;FW1-IP\u0026gt; FW1\u0026#34; | sudo tee /var/www/html/index.html sudo systemctl enable --now httpd 방화벽 설정 1 2 3 sudo systemctl enable --now firewalld sudo firewall-cmd --permanent --add-service=http sudo firewall-cmd --reload 2) FW2 방화벽 설정 사용자 생성 1 2 useradd -m -G wheel fw2 echo \u0026#34;fw2\u0026#34; | passwd --stdin fw2 패키지 설치 1 2 3 4 sudo dnf update --exclude=kernel* -y echo \u0026#34;\u0026lt;FW2-IP\u0026gt; FW2 \u0026#34; | sudo tee /var/www/html/index.html sudo systemctl enable --now httpd 방화벽 설정 1 2 3 sudo systemctl enable --now firewalld sudo firewall-cmd --permanent --add-service=http sudo firewall-cmd --reload 3) 포트포워딩 방화벽 설정 1 2 3 sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=80:toaddr=\u0026lt;FW2-IP\u0026gt; sudo firewall-cmd --permanent --add-masquerade sudo firewall-cmd --reload 포트포워딩 테스트 1 2 3 sudo firewall-cmd --list-all curl \u0026lt;공인 IP\u0026gt;:8080 curl \u0026lt;공인 IP\u0026gt;:8081 두 curl 모두 FW2 서버가 응답 4) 웹사이트 외부 접속 차단 - rich rule 1. FW2에서 목적지가 FW2 ip인 모든 트래픽 거부 FW2로의 모든 트래픽 거부 1 2 3 4 5 6 7 # FW2 sudo firewall-cmd --permanent --add-rich-rule=\u0026#39;rule family=\u0026#34;ipv4\u0026#34; destination address=\u0026#34;\u0026lt;FW2-IP\u0026gt;\u0026#34; reject\u0026#39; sudo firewall-cmd --reload # 위에서 포트포워딩 했기 때문에 아래 명령어 둘 모두 차단됨 curl \u0026lt;Domain\u0026gt;:8080 curl \u0026lt;Domain\u0026gt;:8081 FW2 ip로의 접속 차단됨 2. 출발지가 FW2 사설 ip인 모든 트래픽 거부 FW1에서 FW2 사설 ip로 포트포워딩된 공인 ip로 요청한다면 차단 안됨 트러블슈팅 참고 1 2 3 4 5 6 7 8 9 10 # FW1 포트포워딩 제거 sudo firewall-cmd --permanent --remove-forward-port=port=80:proto=tcp:toport=80:toaddr=\u0026lt;FW2-IP\u0026gt; sudo firewall-cmd --reload # FW2 sudo firewall-cmd --permanent --add-rich-rule=\u0026#39;rule family=\u0026#34;ipv4\u0026#34; source address=\u0026#34;\u0026lt;FW1-IP\u0026gt;\u0026#34; reject’ sudo firewall-cmd --reload # FW1 curl \u0026lt;Domain\u0026gt;:8080 FW1에서 FW2 사설 ip로의 접속 차단됨 3. FW2에서 목적지가 FW1인 트래픽 거부 트러블슈팅 참고 iptable 사용 1 2 3 4 5 6 7 8 # FW2 sudo iptables -I OUTPUT -s \u0026lt;FW2-IP\u0026gt; -d \u0026lt;FW1-IP\u0026gt; -j REJECT # FW2 # 포트포어딩후 NAT으로 ip가 달라지기 때문에 차단 불가 curl \u0026lt;Domain:8080 # 차단 성공 curl \u0026lt;FW1-IP\u0026gt; 트러블슈팅 참고 firewalld 사용 1 2 3 4 5 6 7 8 9 10 11 sudo vi /etc/firewalld/firewalld.conf AllowZoneDrifting=no sudo systemctl restart firewalld sudo firewall-cmd --reload sudo firewall-cmd --permanent --add-rich-rule=\u0026#39;rule family=\u0026#34;ipv4\u0026#34; source address=\u0026#34;\u0026lt;FW2-IP\u0026gt;\u0026#34; destination address=\u0026#34;\u0026lt;FW1-IP\u0026gt;\u0026#34; reject\u0026#39; sudo firewall-cmd --reload # 차단 성공 curl \u0026lt;FW1-IP\u0026gt; 로그 확인 명령어 1 2 3 4 5 6 7 8 # 패킷 캡쳐 sudo tcpdump -i eth0 host \u0026lt;FW1-IP\u0026gt; -n # Connection Tracking 초기화 sudo conntrack -F # firewall-cmd --reload 적용시 로그 발생 가능성 sudo tail -f /var/log/messages 트러블슈팅 KT VM에 포트포워딩중인 공인 ip를 sudo firewall-cmd --permanent --add-rich-rule='rule family=\u0026quot;ipv4\u0026quot; destination address=\u0026quot;공인ip\u0026quot; reject'로 차단 시 여전히 curl \u0026lt;Domain\u0026gt;:8080 이 동작\n공인 ip로 요청이 오면 사설 ip로 변환된 후 vm에 전달됨 firewalld는 VM 내부 트래픽에만 영향을 미침 따라서 공인 ip를 차단하지 못함 FW2 에서 sudo firewall-cmd --permanent --add-rich-rule='rule family=\u0026quot;ipv4\u0026quot; source address=\u0026quot;\u0026lt;FW1-IP\u0026gt;\u0026quot; reject’ 로 FW1 ip차단 시 FW1에서 curl \u0026lt;FW2-IP\u0026gt; 은 차단 되지만 curl \u0026lt;Domain\u0026gt;:8081 / curl \u0026lt;공인 IP\u0026gt;:8081 은 차단되지 않음\n공인ip:8081은 포트포워딩 되어있기 때문에 kt cloud가 요청이 FW2 에 도달하기 전에 NAT 수행 NAT는 출발지 ip를 공인ip가 아닌 kt cloud에서 자체적으로 사설 ip를 임의로 설정 firewalld 만으로는 FW2 에 매핑된 공인 ip를 통한 트래픽은 차단하기 어려움 sudo tcpdump -i eth0 port 80 -n 명령어로 NAT변환된 ip를 추가 차단하거나 zone을 drop으로 변경해서 화이트리스트 방식으로 변경하거나 kt가 nat하는 ip의 대역을 전체 차단하거나 FW2 에서 sudo firewall-cmd --permanent --add-rich-rule='rule family=\u0026quot;ipv4\u0026quot; destination address=\u0026quot;\u0026lt;FW1-IP\u0026gt;\u0026quot; reject’ 설정 시 curl \u0026lt;FW1-IP\u0026gt; 차단 불가\n아웃바운드 트래픽 차단 불가 iptable 명령어 sudo iptables -I OUTPUT -d \u0026lt;FW1-IP\u0026gt; -j ACCEPT 는 정상 작동 /var/log/messages 로그 확인해보니 \u0026ndash;reload 시에 WARNING: AllowZoneDrifting is enabled. This is considered an insecure configuration option. It will be removed in a future release. Please consider disabling it now. 로그 발생. AllowZoneDrifting 설정 때문에 발생하는 문제로 판단 firewalld.conf 에 AllowZoneDrifting no로 설정 후 해결 참고 firewalld 사용법: https://www.freekb.net/Article?id=2141\n","date":"2024-12-18T09:00:00+09:00","permalink":"https://joonlog.github.io/p/firewalld/","title":"FirewallD"},{"content":"목표 리눅스 1대와 윈도우 1대를 Samba로 연동 리눅스/윈도우 모두 같은 사용자가 공유 폴더 제어하게 설정 환경 설정 OS Rocky 8.10 Windows 2019 SAMBA: SMB 4.19.4 개념 및 설정 Samba smbd(139/tcp, 445/tcp) nmbd(137/udp, 138/udp) /etc/samba/smb.conf\n주설정파일 /etc/samba/lmhosts\nsamba 호스트파일 작업 과정 1. Samba Linux 사용자 생성 1 2 useradd -m -G wheel smblinux echo \u0026#34;smblinux\u0026#34; | passwd --stdin smblinux 1) Samba Linux 설정 패키지 설치 1 2 3 4 sudo dnf update --exclude=kernel* -y sudo dnf install -y samba samba-client cifs-utils sudo systemctl enable --now smb nmb 방화벽 설정 1 2 3 sudo systemctl enable --now firewalld sudo firewall-cmd --permanent --add-service=samba sudo firewall-cmd --reload smb.cnf 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 cd /etc/samba sudo cp -p smb.conf.example smb.conf sudo vi /etc/samba/smb.conf workgroup = WORKGROUP server string = server1 hosts allow = 127. \u0026lt;Client-IP\u0026gt; ... # samba-share: window에서 뜨는 폴더명 # 윈도우 접속시: \\\\ip\\samba-share # 계정명: smbuser1 [samba-share] comment = Samba Test path = /samba public = yes writable = yes printable = no valid users = smbuser1 smb.conf 파일 설정 확인 1 testparm -s 공유 디렉토리 설정 1 2 sudo mkdir -p /samba sudo echo \u0026#34;test written in linux\u0026#34; \u0026gt;\u0026gt; /samba/test.txt Samba 사용자 생성 1 2 3 4 5 6 7 sudo useradd -M -s /sbin/nologin smbuser1 sudo smbpasswd -a smbuser1 sudo pdbedit -L sudo chown -R smbuser1:smbuser1 /samba sudo systemctl restart smb nmb Samba 테스트 1 2 smbclient -L localhost -N smbclient -L localhost -U smbuser1 2. Samba Windows 1) 원격 데스크탑 연결 ip: 공인 ip 사용자: Administrator 2) Samba 서버 공유 디렉토리 연결 smb.cnf에서 설정한 대로 네트워크 드라이브 연결 윈도우에서 작성한 파일 리눅스에서 확인 3. Samba Server에서 Client 설정 서버에서 클라이언트 설정 하는 이유 samba 사용자 smbuser1을 보안을 위해 nologin으로 생성 따라서 서버에서 공유 디렉토리에 작업하면 사용자가 smbuser1이 될 수 없지만 윈도우에선 smbuser1으로 작업하게 되므로 권한이 달라짐 서버에서 smbuser1으로 작업하기 위해 Samba 클라이언트 설정 1) Samba 서버 테스트 1 2 3 smbclient //\u0026lt;samba-server-ip\u0026gt;/samba-share -U smbuser1 smb: \\\u0026gt; ls smb: \\\u0026gt; get linux1.txt 2) Samba 마운트 1 2 3 sudo mkdir -p /mnt/smb sudo mount -t cifs //\u0026lt;Server-IP\u0026gt;/samba-share /mnt/smb -o username=smbuser1,password=\u0026lt;password\u0026gt; 3) Samba 영구 마운트 noauto,x-systemd.automount 옵션\nsamba가 disable일 때 재부팅 시 위 옵션이 설정되어 있지 않다면 부팅 에러 발생 noauto: 부팅 시에 자동으로 마운트 X x-systemd.automount: 파일/디렉토리에 접근할 때 자동 마운트 ⇒ 부팅 시에 마운트하지 않고, 접근 시에 자동 마운트\n1 2 3 4 5 6 7 8 9 10 11 12 sudo umount /mnt/smb sudo vi /etc/samba/credentials username=smbuser1 password=\u0026lt;password\u0026gt; sudo chmod 600 /etc/samba/credentials sudo vi /etc/fstab # samba mount //\u0026lt;Server-IP\u0026gt;/samba-share /mnt/smb cifs credentials=/etc/samba/credentials,noauto,x-systemd.automount 0 0 x-systemd.automount 동작 1 2 3 sudo systemctl daemon-reload sudo systemctl restart mnt-smb.automount systemctl status mnt-smb.automount 마운트된 디렉터리 /mnt/smb에서 작업해야지만 smbuser1 사용자로 작업 취급 참고 samba: https://ujia.tistory.com/70#google_vignette\nx-systemd automount: https://docs.redhat.com/ko/documentation/red_hat_enterprise_linux/8/html/managing_file_systems/proc_using-systemd-automount-to-mount-a-file-system-on-demand-with-etc-fstab_mounting-file-systems-on-demand\n","date":"2024-12-17T09:00:00+09:00","permalink":"https://joonlog.github.io/p/samba-%ED%8C%8C%EC%9D%BC-%EA%B3%B5%EC%9C%A0-%EA%B5%AC%EC%B6%95/","title":"Samba 파일 공유 구축"},{"content":"목표 Munin 서버 1대, 클라이언트 1대로 모니터링 구축 웹 인터페이스를 통해 실시간 데이터 시각화 환경 설정 OS: Rocky 8.10 Monitoring: munin 2.0.73 개념 및 설정 Munin /etc/munin/munin.conf\n주 설정 파일 모니터링할 노드 설정 /etc/httpd/conf.d/munin.conf\nmunin용 Apache 설정 비밀번호 설정 /etc/munin/munin-node.conf\n클라이언트에서의 서버 연결 설정 파일 /etc/cron.d/munin\nmunin 모니터링 주기 설정 작업 과정 1. Munin Server 사용자 생성 1 2 useradd -m -G wheel moserver echo \u0026#34;moserver\u0026#34; | passwd --stdin moserver 1) Munin 설정 패키지 설치 1 2 3 4 5 sudo dnf update --exclude=kernel* -y sudo dnf config-manager --set-enabled powertools sudo dnf install -y epel-release sudo dnf install -y munin munin-node sudo dnf install -y httpd munin.conf 설정 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 sudo vi /etc/munin/munin.conf # 주석해제 dbdir /var/lib/munin htmldir /var/www/html/munin logdir /var/log/munin rundir /var/run/munin [localhost] address 127.0.0.1 use_node_name yes [pjt-moclient] address \u0026lt;Client-IP\u0026gt; use_node_name yes 2) Munin 웹 인터페이스 설정 munin 패스워드 설정 1 sudo htpasswd -s -c /etc/munin/munin-htpasswd muninadmin apache munin.conf 생성 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 sudo vi /etc/httpd/conf.d/munin.conf # /munin으로 온 요청 /var/www/html/munin으로 매핑 alias /munin /var/www/html/munin # 이 코드가 있어야 상세 그래프 페이지에서 그래프가 나타남 ScriptAlias /munin-cgi/munin-cgi-graph /var/www/html/munin/cgi/munin-cgi-graph \u0026lt;Directory /var/www/html/munin\u0026gt; AuthUserFile /etc/munin/munin-htpasswd AuthName munin-auth AuthType Basic \u0026lt;RequireAll\u0026gt; Require valid-user Require ip 127.0.0.1 \u0026lt;내 현재 IP\u0026gt; \u0026lt;/RequireAll\u0026gt; \u0026lt;/Directory\u0026gt; 3) Munin Node(localhost) 설정 munin-node.conf 수정 서버도 모니터링 할 시 추가 1 2 3 4 5 6 7 8 9 # vi 편집기를 이용해 수정 vi /etc/munin/munin-node.conf # 해당 부분 아래에 모니터링 서버 IP를 추가 allow ^127\\.0\\.0\\.1$ allow ^::1$ allow ^172\\.27\\.0\\.74$ sudo systemctl enable --now munin-node 4) Munin Cron 설정 1 2 3 4 5 6 7 8 9 10 11 sudo vi /etc/cron.d/munin # cron 작업 출력 메세지를 root에게 메일로 보냄 MAILTO=root # 1분마다 동작 # munin 사용자 권한으로 실행 # test -x /usr/bin/munin-cron: 파일이 실행 가능한 상태인지 확인 * * * * * munin test -x /usr/bin/munin-cron \u0026amp;\u0026amp; /usr/bin/munin-cron sudo systemctl restart crond sudo systemctl enable --now httpd 방화벽 1 2 3 4 sudo systemctl enable --now firewalld sudo firewall-cmd --permanent --add-port=80/tcp sudo firewall-cmd --permanent --add-port=4949/tcp sudo firewall-cmd --reload 2. Munin Client 사용자 생성 1 2 useradd -m -G wheel moclient echo \u0026#34;moclient\u0026#34; | passwd --stdin moclient 1) Munin Node(Client) 설정 패키지 설치 1 2 3 4 sudo dnf update --exclude=kernel* -y sudo dnf config-manager --set-enabled powertools sudo dnf install -y epel-release sudo dnf install -y munin-node munin-node.conf 수정 1 2 3 4 5 6 7 8 9 # vi 편집기를 이용해 수정 vi /etc/munin/munin-node.conf # 해당 부분 아래에 모니터링 서버 IP를 추가 allow ^127\\.0\\.0\\.1$ allow ^::1$ allow ^172\\.27\\.0\\.74$ sudo systemctl enable --now munin-node 방화벽 1 2 3 sudo systemctl enable --now firewalld sudo firewall-cmd --permanent --add-port=4949/tcp sudo firewall-cmd --reload 로그 확인 명령어 1 2 sudo tail -f /var/log/munin/munin-update.log sudo tail -f /var/log/munin-node/munin-node.log 트러블슈팅 epel-release 설치 후 perl 의존성 에러 발생\nepel 공식문서 참조 https://docs.fedoraproject.org/en-US/epel/getting-started/ sudo dnf config-manager --set-enabled powertools 명령어로 powertools 활성화 필요 localhost와 client의 그래프가 동일하게 나오는 현상\n1 2 3 [moserver@pjt-moserver ~]$ sudo tail -f /var/log/munin/munin-update.log 2024/11/20 08:40:01 [INFO] node pjt-moclient advertised itself as pjt-moooooo instead. 2024/11/20 08:40:01 [INFO] node localhost advertised itself as pjt-moooooo instead. /etc/munin/munin.conf의 address를 172를 설정해야 하는데 127로 기입하는 오타로 인해 생긴 문제 munin 페이지에 htpasswd가 적용되지 않는 문제\nRequire ip가 Require valid-user보다 우선되어 Require valid-user는 검사를 안하는 상황 ReruireAll 태그 안으로 넣어 모든 조건 만족하도록 변경 후 해결 1 2 3 4 5 6 7 8 9 \u0026lt;Directory /var/www/html/munin\u0026gt; AuthUserFile /etc/munin/munin-htpasswd AuthName munin-auth AuthType Basic \u0026lt;RequireAll\u0026gt; Require valid-user Require ip 127.0.0.1 \u0026lt;내 현재 IP\u0026gt; \u0026lt;/RequireAll\u0026gt; \u0026lt;/Directory\u0026gt; 참고 munin 설치: https://guide.munin-monitoring.org/en/latest/installation/install.html\nepel-release 공식문서: https://docs.fedoraproject.org/en-US/epel/getting-started/\n","date":"2024-12-16T09:00:00+09:00","permalink":"https://joonlog.github.io/p/munin-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95/","title":"Munin 모니터링 구축"},{"content":"목표 DNS master 1대, slave1대 구조로 네임서버 구성 DNS 서버 고가용성 및 부하분산 도메인 하나 IP 변경했을때 정상적으로 변경돼서 나오는 지 확인 환경 설정 OS: Rocky 8.10 개념 및 설정 /etc/named.conf\nDNS 메인 설정 파일 named.rfc1912.zones 파일 지정 포워딩(forwarding) 및 캐싱 정책 설정 ACL 및 보안 관련 정책 정의 /etc/named.rfc1912.zones\n로컬 도메인 및 내부 네트워크용 기본 샘플 존 설정 파일 도메인 등록 (pjt.com) zone 파일 지정 (pjt.com.zone) 각 서버에서 Master/Slave 연동 설정 /var/named/pjt.com.zone\nDNS 레코드 설정 주 네임서버는 zone 데이터의 원본을 관리 zone 데이터는 다른 권한 있는 네임서버로 복사됨 권한 있는 네임서버는 도메인의 레코드 정보를 제공 Serial 값이 변경(증가)되면 Slave의 slaves/pjt.com.zone과 동기화 /var/named/slaves/pjt.com.zone\npjt.com 도메인의 Slave 존 파일 Master의 pjt.com.zone에서 데이터를 복제 DNS 레코드 유형 레코드 유형 설명 형식 예시 NS 도메인에 대한 권한을 가진 네임서버를 정의 \u0026lt;domain\u0026gt; IN NS \u0026lt;nameserver\u0026gt; @ IN NS ns1.example.com. A 도메인 이름을 IPv4 주소와 매핑 \u0026lt;hostname\u0026gt; IN A \u0026lt;IPv4 address\u0026gt; @ IN A 192.168.1.10 AAAA 도메인 이름을 IPv6 주소와 매핑 \u0026lt;hostname\u0026gt; IN AAAA \u0026lt;IPv6 address\u0026gt; www IN AAAA 2001:0db8:85a3::8a2e:0370:7334 CNAME 도메인 이름에 대한 별칭을 설정 \u0026lt;alias\u0026gt; IN CNAME \u0026lt;canonical name\u0026gt; mail IN CNAME www.example.com. MX 메일 서버를 정의하며, 우선순위는 preference로 결정 \u0026lt;domain\u0026gt; IN MX \u0026lt;preference\u0026gt; \u0026lt;mail server\u0026gt; @ IN MX 10 mail.example.com. TXT 도메인에 대한 텍스트 정보를 저장 \u0026lt;domain\u0026gt; IN TXT \u0026quot;\u0026lt;text\u0026gt;\u0026quot; @ IN TXT \u0026quot;v=spf1 ip4:192.168.1.0/24 -all\u0026quot; PTR IP 주소를 도메인 이름으로 매핑 (Reverse DNS) \u0026lt;IP-in-reverse\u0026gt;.in-addr.arpa. IN PTR \u0026lt;domain\u0026gt; 10.1.168.192.in-addr.arpa. IN PTR example.com. SRV 특정 서비스의 위치를 정의 \u0026lt;service\u0026gt;.\u0026lt;protocol\u0026gt;.\u0026lt;domain\u0026gt; IN SRV \u0026lt;priority\u0026gt; \u0026lt;weight\u0026gt; \u0026lt;port\u0026gt; \u0026lt;target\u0026gt; _sip._tcp.example.com. IN SRV 10 60 5060 sipserver.example.com. 작업 과정 1. DNS Master 설정 사용자 생성 1 2 useradd -m -G wheel dnsmaster echo \u0026#34;dnsmaster\u0026#34; | passwd --stdin dnsmaster bind 설치 1 2 sudo dnf update --exclude=kernel* -y sudo dnf install -y bind bind-chroot bind-utils 방화벽 설정 1 2 3 sudo systemctl enable --now firewalld sudo firewall-cmd --add-port=53/tcp --permanent sudo firewall-cmd --reload 1) named.conf 수정 dns로 접근할 수 있도록 any로 수정 1 2 3 4 5 6 sudo vi /etc/named.conf options { listen-on port 53 { any; }; ... allow-query { any; }; 2) named.rfc1912.zones에 zone 등록 dns에 zone 파일 등록 1 2 3 4 5 6 7 8 sudo vi /etc/named.rfc1912.zones zone \u0026#34;pjt.com\u0026#34; IN { type master; # Zone 파일 경로. /var/named 밑에 저장됨 file \u0026#34;pjt.com.zone\u0026#34;; also-notify { \u0026lt;Slave IP\u0026gt;; }; }; 3) zone 파일 생성 zone 업데이트 시 serial 값이 증가해야 slave에 반영 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 sudo vi /var/named/pjt.com.zone # Primary NS: ns1.pjt.com # Email: admin.pjt.com. 관리자 이메일 주소 (`@`는 `.`으로 표기. 실제: admin@pjt.com) $TTL 86400 ; 1일 @ IN SOA ns1.pjt.com. admin.pjt.com. ( 2024111301 ; Serial. Zone 파일의 버전 번호. 이 번호가 변경될 때 Slave 서버가 Zone 파일을 동기화 3600 ; Refresh (1시간). Slave 서버가 Master 서버로부터 Zone 데이터를 주기적으로 업데이트하는 주기 1800 ; Retry (30분). Slave 서버가 Master 서버와 연결에 실패했을 때 재시도하는 주기 1209600 ; Expire (2주). Master 서버와 연결이 되지 않을 때 Slave 서버가 Zone 정보를 유효하게 유지할 기간 86400 ; Minimum TTL (1일). Negative caching을 위한 기본 TTL 값 ) # Authorized NS(네임서버) 설정 @ IN NS ns1.pjt.com. @ IN NS ns2.pjt.com. # A 레코드 설정 pjt.com / www.pjt.com @ IN A 192.168.1.10 www IN A 192.168.1.10 # 네임서버 A 레코드 ns1.pjt.com / ns2.pjt.com ns1 IN A 192.168.1.10 ns2 IN A 192.168.100.100 dns 파일 검증 1 2 3 4 5 6 ## named.conf 파일 검증 named-checkconf /etc/named.conf ## zone 파일 검증 #named-checkzone [domain] [zone file] named-checkzone nirsa.com nirsa.zone dns 재시작 1 2 3 sudo named-checkconf sudo named-checkzone pjt.com /var/named/pjt.com.zone sudo systemctl restart named 2. DNS Slave 설정 사용자 생성 1 2 useradd -m -G wheel dnsslave echo \u0026#34;dnsslave\u0026#34; | passwd --stdin dnsslave bind 설치 1 2 sudo dnf update --exclude=kernel* -y sudo dnf install -y bind bind-chroot bind-utils 방화벽 설정 1 2 3 sudo systemctl enable --now firewalld sudo firewall-cmd --add-port=53/tcp --permanent sudo firewall-cmd --reload 1) named.conf 수정 dns로 접근할 수 있도록 any로 수정 1 2 3 4 5 6 sudo vi /etc/named.conf options { listen-on port 53 { any; }; ... allow-query { any; }; 2) named.rfc1912.zones에 zone 등록 dns에 zone 파일 등록 1 2 3 4 5 6 7 8 sudo vi /etc/named.rfc1912.zones zone \u0026#34;pjt.com\u0026#34; IN { type slave; # Zone 파일 경로. /var/named 밑에 저장됨 file \u0026#34;slaves/pjt.com.zone\u0026#34;; masters { \u0026lt;Master IP\u0026gt;; }; }; slave 파일 경로 생성 1 sudo mkdir -p /var/named/slaves dns 시작 1 2 sudo named-checkconf sudo systemctl enable --now named master-slave 동기화 확인 1 sudo ls -l /var/named/slaves 3. DNS 테스트 dig 명령어로 dns 쿼리 1 2 3 4 dig @\u0026lt;Master IP\u0026gt; pjt.com dig @\u0026lt;Slave IP\u0026gt; pjt.com dig @\u0026lt;Slave IP\u0026gt; ns1.pjt.com 1) IP 변경 후 테스트 pjt.com.zone 파일 serial 넘버 변경 ns2.pjt.com ip 변경 1 2 3 sudo vi /var/named/pjt.com.zone dig @\u0026lt;Slave IP\u0026gt; ns2.pjt.com 트러블슈팅 named.conf의 listen 기본값은 127.0.0.1이다.\nmaster-slave 구성 후 slave에서 /var/named/slaves에 zone 파일 동기화되지 불가 /var/log/messages 확인 결과 failed to connect: connection refused가 발생 named.conf의 listen on이 로컬호스트인 127.0.0.1 설정되어 있어 any로 변경하여 해결 named.conf의 allow-transfer 설정은 Serial 값이 변경되어도 Slave에 즉각적으로 동기화하지 않음\nip 변경 후 Serial 넘버 변경(증가)해도 slave에 반영이 안되는 문제가 발생 조사해보니, allow-transfer은 Master-Slave 연동을 허용하기만 하는 설정이고, 동기화 시기는 .zone 파일의 SOA 레코드의 refresh 설정에 따라 달라짐 allow-transfer 대신 serial변경 후 slave에 즉각적으로 알려주는 also-notify로 변경하여 해결 also-notify 설정이 되어 있다면 refresh가 쓸모 없다고 생각할 수 있음 그래도 refresh는 Serial 값 변경 시 장애가 발생했을 때를 위한 백업 설정으로 여전히 유효 참고 https://m.blog.naver.com/host365/222983239445\nhttps://nirsa.tistory.com/128\nalso-notify 설정: https://bono915.tistory.com/entry/Linux-named-DNS-MasterSlave-서버-구축-및-동기화-설정-방법\n","date":"2024-12-13T09:00:00+09:00","permalink":"https://joonlog.github.io/p/dns-master-slave-%EA%B5%AC%EC%B6%95/","title":"DNS Master-Slave 구축"},{"content":"목표 Nagios 서버 1대, 클라이언트 1대, private 클라이언트 1대로 시스템 모니터링 환경 구성 NRPE와 check_tcp 플러그인을 활용한 포트 및 기본 시스템 리소스 모니터링 구현 Nagios 웹 인터페이스를 통한 모니터링 결과 시각화 커스텀 플러그인 작성을 통한 특정 메모리 모니터링 기능 추가 Nagios 서버에서 bastion 노드를 거쳐 private 노드 모니터링 환경 설정 OS: Rocky 8.10 Monitoring: nagios 4.4.6 nagios-plugins 2.2.1 nrpe 4.1.0 개념 및 설정 Nagios /usr/local/nagios/var/nagios.log\n메인 로그 파일 웹에서 alerts history, notifications, Event log가 이 파일 참조 /usr/local/nagios/etc/nagios.cfg\n메인 설정 파일 /usr/local/nagios/etc/objects/localhost.cfg\n로컬호스트 설정 파일 /usr/local/nagios/etc/objects/commands.cfg\n설정 파일에서 사용하는 command 정의 파일 /usr/local/nagios/libexec/\ncommand의 실제 쉘 스크립트가 저장되어 있는 경로 NRPE /etc/nagios/nrpe.cfg\n서버와의 연결 설정 클라이언트에서 사용할 /usr/local/nagios/libexec/ 경로의 플러그인 스크립트를 command로 지정 NRPE 플러그인 적용 흐름 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 아래 파일들을 참조해 check_nrpe!check_load가 동작 가능 # 서버 client.cfg define service { use generic-service host_name moclient service_description CPU Load check_command check_nrpe!check_load } # checp_nrpe는 아래 경로 참조 # 서버 commands.cfg define command { command_name check_nrpe command_line /usr/local/nagios/libexec/check_nrpe -H $HOSTADDRESS$ -c $ARG1$ } # check_load는 아래 경로 참조 # 클라이언트 nrpe.cfg command[check_load]=/usr/local/nagios/libexec/check_load -r -w .15,.10,.05 -c .30,.25,.20 작업 과정 1. Nagios Server 설정 사용자 생성 1 2 useradd -m -G wheel moserver echo \u0026#34;moserver\u0026#34; | passwd --stdin moserver 소스 설치 준비 1 2 sudo dnf update --exclude=kernel* -y sudo dnf install -y httpd httpd-tools php gcc glibc glibc-common gd gd-devel make net-snmp 유저, 그룹 생성 nagios 유저 없으면 make install 에서 오류 위에서 설치한 httpd 사용자 apache 유저에게 nagios 접근 권한을 nagcmd 그룹으로 부여 nagios 웹 인터페이스에서 nagios 작업할 때 apache가 nagios 외부 파일에 접근해야 함 1 2 3 4 5 sudo groupadd nagcmd sudo useradd nagios sudo usermod -G nagcmd nagios sudo usermod -G nagcmd apache nagios 패키지 다운로드 nagios, nagios 플러그인 패키지 다운로드 1 2 3 4 5 6 7 mkdir /home/moserver/nagios \u0026amp;\u0026amp; cd /home/moserver/nagios sudo wget https://assets.nagios.com/downloads/nagioscore/releases/nagios-4.4.6.tar.gz sudo wget https://nagios-plugins.org/download/nagios-plugins-2.2.1.tar.gz sudo tar -xvf nagios-4.4.6.tar.gz sudo tar -xvf nagios-plugins-2.2.1.tar.gz 1) Nagios 소스 설치 1 2 3 4 5 6 7 8 9 10 11 12 13 14 cd nagios-4.4.6/ # 소스파일에 대한 환경설정을 해주는 명령어. 서버환경에 맞춰 makefile 생성 sudo ./configure --with-command-group=nagcmd # 소스 컴파일 sudo make all # 컴파일한 소스 설치 sudo make install # /etc/init.d 에 등록됨 sudo make install-init # nagios라는 user가 없으면 여기서 에러 발생 sudo make install-commandmode # /usr/local/nagios 에 설정파일이 생성됨 sudo make install-config nagios 웹 인터페이스 1 2 3 4 5 6 7 # /etc/httpd/conf.d/nagios.conf 생성됨 sudo make install-webconf sudo htpasswd -s -c /usr/local/nagios/etc/htpasswd.users nagiosadmin sudo systemctl enable --now httpd sudo systemctl enable --now nagios 방화벽 설정 1 2 3 sudo systemctl enable --now firewalld sudo firewall-cmd --add-port=80/tcp --permanent sudo firewall-cmd --reload (선택) 이메일 알림 1 vi /usr/local/nagios/etc/objects/contacts.cfg 웹 인터페이스 접근 1 http://\u0026lt;domain\u0026gt;:8080/nagios/ 플러그인 설치 안하면 critical로 표시됨 2) Nagios 플러그인 소스 설치 1 2 3 4 5 6 7 8 9 10 11 cd /home/moserver/nagios/nagios-plugins-2.2.1/ # 소스파일에 대한 환경설정을 해주는 명령어. 서버환경에 맞춰 makefile 생성 sudo ./configure --with-nagios-user=nagios --with-nagios-group=nagios # 소스 컴파일 sudo make all # 컴파일한 소스 설치 sudo make install # nagios 설정 파일 검사 sudo /usr/local/nagios/bin/nagios -v /usr/local/nagios/etc/nagios.cfg index.html 생성 1 echo \u0026#34;http check\u0026#34; | sudo tee /var/www/html/index.html nagios 재시작 1 2 sudo systemctl restart nagios sudo systemctl restart httpd 플러그인 반영까지 수 분 소요 2. Nagios Client 설정 1) Server NRPE 소스 설치 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 sudo cd /home/moserver/nagios sudo wget https://github.com/NagiosEnterprises/nrpe/releases/download/nrpe-4.1.0/nrpe-4.1.0.tar.gz sudo tar zxvf nrpe-4.1.0.tar.gz sudo cd nrpe-4.1.0 # -–enable-command-args 옵션을 사용하면 NRPE를 통해 인수를 전달할 수 있습니다. # 생략하면 인수를 nrpe 구성 파일 nrpe.cfg에 명시적으로 설정해야 합니다. # -–with-nrpe-user=nagios –with-nrpe-group=nagios는 구성 파일의 소유권을 설정합니다. sudo ./configure --enable-command-args --with-nrpe-user=nagios --with-nrpe-group=nagios sudo make install-groups-users sudo make all sudo make install 방화벽 설정 1 2 sudo firewall-cmd --add-port=5666/tcp --permanent sudo firewall-cmd --reload nrpe 설정 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 sudo vi /usr/local/nagios/etc/nagios.cfg # 주석 제거 cfg_dir=/usr/local/nagios/etc/servers sudo mkdir -p /usr/local/nagios/etc/servers sudo vi /usr/local/nagios/etc/servers/client.cfg define host{ use linux-server host_name moclient alias moclient address \u0026lt;Client-IP\u0026gt; max_check_attempts 5 check_period 24x7 notification_interval 30 notification_period 24x7 } sudo systemctl enable --now nrpe 2) Client NRPE 설정 사용자 생성 1 2 useradd -m -G wheel moclient echo \u0026#34;moclient\u0026#34; | passwd --stdin moclient 1 sudo dnf install -y nrpe nrpe 설정 1 2 3 4 sudo vi /etc/nagios/nrpe.cfg allowed_hosts=127.0.0.1,::1,\u0026lt;Server IP\u0026gt; sudo systemctl enable --now nrpe 3) Client Nagios 플러그인 소스 설치 소스 설치 준비 1 2 sudo dnf update --exclude=kernel* -y sudo dnf install -y httpd httpd-tools php gcc glibc glibc-common gd gd-devel make net-snmp nagios 플러그인 패키지 다운로드 1 2 3 4 5 mkdir /home/moclient/nagios \u0026amp;\u0026amp; cd /home/moclient/nagios sudo wget https://nagios-plugins.org/download/nagios-plugins-2.2.1.tar.gz sudo tar -xvf nagios-plugins-2.2.1.tar.gz 1 2 3 4 5 6 7 8 cd /home/moclient/nagios/nagios-plugins-2.2.1/ # 소스파일에 대한 환경설정을 해주는 명령어. 서버환경에 맞춰 makefile 생성 sudo ./configure --with-nagios-user=nagios --with-nagios-group=nagios # 소스 컴파일 sudo make all # 컴파일한 소스 설치 sudo make install 방화벽 설정 1 2 3 4 sudo systemctl enable --now firewalld sudo firewall-cmd --add-port=5666/tcp --permanent sudo firewall-cmd --reload 3. Nagios 모니터링 추가 설정 1) Server local check_tcp 모니터링 추가 period, max_check_attemps 등 명시 안하면 templates.cfg 에 명시된 기본값 사용됨 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 sudo vi /usr/local/nagios/etc/objects/localhost.cfg define service { use local-service host_name localhost service_description HTTP Port check_command check_tcp!80 } define service { use local-service host_name localhost service_description SSH Port check_command check_tcp!22 } service에서 HTTP Port, SSH Port 항목 OK 확인 2) Client 여러 서비스 모니터링 추가 Server에서 check_nrpe 플러그인 command 등록 1 2 3 4 5 6 sudo vi /usr/local/nagios/etc/objects/commands.cfg define command { command_name check_nrpe command_line /usr/local/nagios/libexec/check_nrpe -H $HOSTADDRESS$ -c $ARG1$ } Server의 client.cfg 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 sudo vi /usr/local/nagios/etc/servers/client.cfg define service { use generic-service host_name moclient service_description CPU Load check_command check_nrpe!check_load } define service { use generic-service host_name moclient service_description Disk Usage check_command check_nrpe!check_disk } define service { use generic-service host_name moclient service_description Memory Usage check_command check_nrpe!check_mem } define service { use generic-service host_name moclient service_description SSH Port check_command check_tcp!22 } Client의 nrpe.cfg 1 2 3 4 5 6 sudo vi /etc/nagios/nrpe.cfg command[check_load]=/usr/local/nagios/libexec/check_load -r -w .15,.10,.05 -c .30,.25,.20 command[check_disk]=/usr/local/nagios/libexec/check_disk -w 20% -c 10% -p / # 커스텀 플러그인 command[check_mem]=/usr/local/nagios/libexec/custom_check_mem 80 90 custom_check_mem 플러그인 커스텀 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 sudo vi /usr/local/nagios/libexec/custom_check_mem #!/bin/bash # Memory check script for Nagios # Set warning and critical thresholds WARN=$1 CRIT=$2 # Get total and free memory TOTAL_MEM=$(free -m | grep Mem: | awk \u0026#39;{print $2}\u0026#39;) USED_MEM=$(free -m | grep Mem: | awk \u0026#39;{print $3}\u0026#39;) USED_MEM_PERCENT=$((USED_MEM * 100 / TOTAL_MEM)) if [ \u0026#34;$USED_MEM_PERCENT\u0026#34; -ge \u0026#34;$CRIT\u0026#34; ]; then echo \u0026#34;CRITICAL - Memory usage is ${USED_MEM_PERCENT}% | memory=${USED_MEM_PERCENT}%\u0026#34; exit 2 elif [ \u0026#34;$USED_MEM_PERCENT\u0026#34; -ge \u0026#34;$WARN\u0026#34; ]; then echo \u0026#34;WARNING - Memory usage is ${USED_MEM_PERCENT}% | memory=${USED_MEM_PERCENT}%\u0026#34; exit 1 else echo \u0026#34;OK - Memory usage is ${USED_MEM_PERCENT}% | memory=${USED_MEM_PERCENT}%\u0026#34; exit 0 fi sudo chmod +x /usr/local/nagios/libexec/custom_check_mem sudo systemctl restart nrpe 4. Private Host 모니터링 1) Private NRPE 설정 서버에서 ssh 접속 1 ssh root@\u0026lt;Private IP\u0026gt; 사용자 생성 1 2 useradd -m -G wheel moprivate echo \u0026#34;moprivate\u0026#34; | passwd --stdin moprivate 1 2 3 sudo dnf update --exclude=kernel* -y sudo dnf install -y epel-release sudo dnf install -y nrpe nrpe 설정 1 2 3 4 sudo vi /etc/nagios/nrpe.cfg allowed_hosts=127.0.0.1,::1,\u0026lt;Server IP\u0026gt; sudo systemctl enable --now nrpe 2) Private Nagios 플러그인 소스 설치 소스 설치 준비 1 sudo dnf install -y httpd httpd-tools php gcc glibc glibc-common gd gd-devel make net-snmp nagios 플러그인 패키지 다운로드 1 2 3 4 5 mkdir /home/moprivate/nagios \u0026amp;\u0026amp; cd /home/moprivate/nagios sudo wget https://nagios-plugins.org/download/nagios-plugins-2.2.1.tar.gz sudo tar -xvf nagios-plugins-2.2.1.tar.gz 1 2 3 4 5 6 7 8 cd /home/moprivate/nagios/nagios-plugins-2.2.1/ # 소스파일에 대한 환경설정을 해주는 명령어. 서버환경에 맞춰 makefile 생성 sudo ./configure --with-nagios-user=nagios --with-nagios-group=nagios # 소스 컴파일 sudo make all # 컴파일한 소스 설치 sudo make install 방화벽 설정 1 2 3 4 sudo systemctl enable --now firewalld sudo firewall-cmd --add-port=5666/tcp --permanent sudo firewall-cmd --reload 3) Server Private 등록 서버에 private host 설정 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 sudo vi /usr/local/nagios/etc/servers/private.cfg define host{ use linux-server host_name moprivate alias moprivate address \u0026lt;Private IP\u0026gt; max_check_attempts 5 check_period 24x7 notification_interval 30 notification_period 24x7 } sudo systemctl enable --now nagios 4) Private 모니터링 등록 Server의 client.cfg 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 sudo vi /usr/local/nagios/etc/servers/private.cfg define service { use generic-service host_name moprivate service_description CPU Load check_command check_nrpe!check_load } define service { use generic-service host_name moprivate service_description Disk Usage check_command check_nrpe!check_disk } define service { use generic-service host_name moprivate service_description Memory Usage check_command check_nrpe!check_mem } define service { use generic-service host_name moprivate service_description SSH Port check_command check_tcp!22 } sudo systemctl restart nrpe Private 의 nrpe.cfg 1 2 3 4 5 6 sudo vi /etc/nagios/nrpe.cfg command[check_load]=/usr/local/nagios/libexec/check_load -r -w .15,.10,.05 -c .30,.25,.20 command[check_disk]=/usr/local/nagios/libexec/check_disk -w 20% -c 10% -p / # 커스텀 플러그인 command[check_mem]=/usr/local/nagios/libexec/custom_check_mem 80 90 custom_check_mem 플러그인 커스텀 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 sudo vi /usr/local/nagios/libexec/custom_check_mem #!/bin/bash # Memory check script for Nagios # Set warning and critical thresholds WARN=$1 CRIT=$2 # Get total and free memory TOTAL_MEM=$(free -m | grep Mem: | awk \u0026#39;{print $2}\u0026#39;) USED_MEM=$(free -m | grep Mem: | awk \u0026#39;{print $3}\u0026#39;) USED_MEM_PERCENT=$((USED_MEM * 100 / TOTAL_MEM)) if [ \u0026#34;$USED_MEM_PERCENT\u0026#34; -ge \u0026#34;$CRIT\u0026#34; ]; then echo \u0026#34;CRITICAL - Memory usage is ${USED_MEM_PERCENT}% | memory=${USED_MEM_PERCENT}%\u0026#34; exit 2 elif [ \u0026#34;$USED_MEM_PERCENT\u0026#34; -ge \u0026#34;$WARN\u0026#34; ]; then echo \u0026#34;WARNING - Memory usage is ${USED_MEM_PERCENT}% | memory=${USED_MEM_PERCENT}%\u0026#34; exit 1 else echo \u0026#34;OK - Memory usage is ${USED_MEM_PERCENT}% | memory=${USED_MEM_PERCENT}%\u0026#34; exit 0 fi sudo chmod +x /usr/local/nagios/libexec/custom_check_mem sudo systemctl restart nrpe 임의로 스트레스 주입 private host에서 uptime 시에 0.00, 0.02, 0.11이 나왔기 때문에 설정한 15분 평균 0.05보다 크므로 warning 발생 5. 회사 플러그인 추가 Server의 private.cfg 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 sudo vi /usr/local/nagios/etc/servers/private.cfg define service { use generic-service host_name moprivate service_description Ncloud24 CPU Usage check_command check_nrpe!check_cpu_usage } define service { use generic-service host_name moprivate service_description Ncloud24 MEM Usage check_command check_nrpe!check_mem_usage } sudo systemctl restart nagios Private에서 플러그인 설치 1 2 3 4 5 6 7 8 9 10 11 12 13 wget http://service.ncloud24.com/app/nagios/check_mem.pl wget http://service.ncloud24.com/app/nagios/check_stats.pl sudo mv check_mem.pl /usr/local/nagios/libexec/ sudo mv check_stats.pl /usr/local/nagios/libexec/ sudo chmod 755 /usr/local/nagios/libexec/check_mem.pl sudo chmod 755 /usr/local/nagios/libexec/check_stats.pl sudo vi /usr/local/nagios/libexec/check_stats.pl # 플러그인 경로 추가 use lib \u0026#34;/usr/local/nagios/libexec\u0026#34;; sudo yum install perl-Sys-Statistics-Linux -y Private 의 nrpe.cfg 1 2 3 4 5 6 7 sudo vi /etc/nagios/nrpe.cfg # 회사 플러그인 command[check_cpu_usage]=/usr/local/nagios/libexec/check_stats.pl -C -w 90 -c 95 command[check_mem_usage]=/usr/local/nagios/libexec/check_mem.pl -u -C -w 85 -c 90 sudo systemctl restart nrpe 트러블슈팅 Nagios nagios 웹 인터페이스 services - HTTP warning 표시\nHTTP WARNING: HTTP/1.1 403 Forbidden - 7928 bytes in 0.001 second response time\nHTTP status는 /usr/local/nagios/etc/objects/localhost.cfg 의 check_http가 관리 /var/www/html/ 경로에 파일이 없어서 못읽기 때문 check_http는 nagios 사용자 권한으로 Apache에 접근하기 때문에 index.html페이지가 없을 때 나오는 Apache 기본 페이지를 제공받지 못하고 403 오류 반환 index.html 생성으로 해결 브라우저에서 http://:8080/ 이 경로는 timeout 오류\n도메인을 Apache의 virtualhost servername에 등록하지 않은 경우 도메인을 처리할 수 없음 하지만 check_http!-H -u / 이건 정상 작동 도메인을 Apache에서 해석하냐, Nagios에서 해석하냐의 차이 NRPE dnf install nrpe 실행 시 클라이언트용 nrpe만 설치 되고 check_nrpe 플러그인은 설치되지 않음\nnrpe 소스 설치 클라이언트의 서비스 모니터링 작성 시 UNKNOWN / NRPE: Unable to read output 오류\nnrpe 설치 시 경로는 /usr/local nrpe.cfg 파일의 command 경로를 /usr/lib64/nagios/plugins/에서 /usr/local/nagios/libexec/로 수정 회사 플러그인 추가 회사 플러그인 check_stats.pl 추가 후 실행 시 Can't locate utils.pm in @INC (you may need to install the utils module) (@INC contains: /usr/lib64/nagios/plugins /usr/local/lib64/perl5 /usr/local/share/perl5 /usr/lib64/perl5/vendor_perl /usr/share/perl5/vendor_perl /usr/lib64/perl5 /usr/share/perl5) at /usr/local/nagios/libexec/check_stats.pl line 33.\n내가 설치한 플러그인들의 경로가 /usr/local/nagios/libexec 이기 때문에 check_stats.pl에서 해당 경로를 찾지 못했기 때문에 발생한 문제 경로 수정 없이 플러그인 설치했는데 다른 사람들과 플러그인 경로가 다름 소스 설치 경로는 /usr/local/nagios/libexec 패키지 설치 경로는 /usr/lib64/nagios/plugins 소스 설치 시 ./configure --libexecdir=/usr/lib64/nagios/plugins 명령어로 경로 수정 가능 플러그인에 경로 추가로 해결 위 경로 추가 후 재실행 시 Can't locate Sys/Statistics/Linux.pm in @INC (you may need to install the Sys::Statistics::Linux module) (@INC contains: /usr/local/nagios/libexec /usr/lib64/nagios/plugins /usr/local/lib64/perl5 /usr/local/share/perl5 /usr/lib64/perl5/vendor_perl /usr/share/perl5/vendor_perl /usr/lib64/perl5 /usr/share/perl5) at /usr/local/nagios/libexec/check_stats.pl line 37.\nSys:Statistics::Linux 모듈이 없기 때문에 발생한 오류 sudo yum install perl-Sys-Statistics-Linux -y 설치 후 해결 참고 nagios 설치: https://wiki.crowncloud.net/?how_to_install_nagios_on_rockylinux8 상세 해설: https://www.oss.kr/storage/app/public/oss/22/df/[Nagios] Solution Guide V0.95.pdf\nnrpe 설치: https://kifarunix.com/how-to-install-nagios-nrpe-agent-on-rhel-centos-oracle-linux/\n","date":"2024-12-12T09:00:00+09:00","permalink":"https://joonlog.github.io/p/nagios-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%B6%95/","title":"Nagios 모니터링 구축"},{"content":"목표 Let’s encrypt에서 ssl 인증서 발급해 전송 보안 강화 apache virtual host로 http -\u0026gt; https 리디렉션 설정 웹사이트의 모든 트래픽이 안전한 프로토콜을 사용하도록 강제 인증서의 자동 갱신을 설정하여 만료되지 않도록 유지 환경 설정 OS: Rocky 8.10 WEB: Apache 2.4.6 SSL: Let’s encrypt certbot 개념 및 설정 Let’s encrypt HTTPS Everywhere 를 추구하는 비영리 프로젝트 IdenTrust cross-sign 됨 SSL 인증서 100% 무료화 인증기관 연장 및 인증서 재발급 무료 콘솔상에서 인증서 발급/갱신/설치/자동화 멀티도메인 지원, SAN 기능(여러 도메인을 한 인증서로 묶어주는 기능) 지원 SSL 인증서 인증 방식 Webroot\ncertbot이 지정된 웹 서버의 루트 디렉토리에 인증 파일을 생성 Let\u0026rsquo;s Encrypt가 도메인에 HTTP 요청을 보내서 인증 파일을 확인 기존 웹 서버가 실행 중이어야 함 무중단 설정 가능 Webserver\nCertbot이 Apache/Nginx 서버 플러그인을 사용해 직접 인증 파일 관리, HTTP 요청 처리 인증 요청을 처리하는 동안 certbot이 웹 서버를 재구성하거나 중단할 수 있음 설정/리다이렉션 자동화 Standalone\ncertbot이 자체적으로 HTTP 서버를 실행하여 인증 요청을 처리. 주로 웹서버 없이 테스트 용도로 사용 웹서버가 실행중이면 중단해야 함 DNS\nDNS에 TXT 레코드 추가로 소유권 확인 와일드카드 인증서 사용 가능 Private 네트워크에서도 사용 가능 해당 과제에서는 webserver 방식 사용\nBind /etc/httpd/conf.d/\u0026lt;Domain\u0026gt;.conf\n도메인용의 VirtualHost 파일 VirtualHost 태그에서 Apache가 수신할 트래픽의 IP/Domain, Port 를 설정 HTTP 설정만 있는 파일에 발급받은 SSL 인증서를 사용해 HTTPS 리다이렉션 설정 /etc/cron.d/certbot-renew\ncron을 사용해 인증서 발급 자동화 작업 과정 1. HTTP 실행 사용자 생성 1 2 useradd -m -G wheel sslhttps echo \u0026#34;sslhttps\u0026#34; | passwd --stdin sslhttps apache 설치 1 2 3 sudo dnf update --exclude=kernel* -y sudo dnf install httpd -y sudo systemctl enable --now httpd 방화벽 설정 1 2 3 4 sudo systemctl enable --now firewalld sudo firewall-cmd --permanent --add-service=http sudo firewall-cmd --permanent --add-service=https sudo firewall-cmd --reload httpd-vhosts.conf 설정 1 sudo vi /etc/httpd/conf.d/\u0026lt;Domain\u0026gt;.conf 1 2 3 4 \u0026lt;VirtualHost *:80\u0026gt; ServerName \u0026lt;Domain\u0026gt; DocumentRoot /var/www/html \u0026lt;/VirtualHost\u0026gt; http 동작 테스트 1 sudo systemctl restart httpd 2. SSL 실행 certbot 설치 1 2 sudo dnf install epel-release -y sudo dnf install certbot python3-certbot-apache -y SSL 인증서 생성 webserver 방식 1 sudo certbot --apache -d \u0026lt;Domain\u0026gt; 3. HTTPS 실행 httpd-vhosts.conf 설정 1 sudo vi /etc/httpd/conf.d/\u0026lt;Domain\u0026gt;.conf 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 \u0026lt;VirtualHost *:80\u0026gt; ServerName \u0026lt;Domain\u0026gt; DocumentRoot /var/www/html # http를 https로 리다이렉션 Redirect permanent / https://\u0026lt;Domain\u0026gt;/ \u0026lt;/VirtualHost\u0026gt; \u0026lt;VirtualHost *:443\u0026gt; ServerName \u0026lt;Domain\u0026gt; DocumentRoot /var/www/html # SSL 인증서 적용 SSLEngine on SSLCertificateFile /etc/letsencrypt/live/\u0026lt;Domain\u0026gt;/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/\u0026lt;Domain\u0026gt;/privkey.pem \u0026lt;Directory /var/www/html\u0026gt; AllowOverride All Require all granted \u0026lt;/Directory\u0026gt; \u0026lt;/VirtualHost\u0026gt; Apache 서버 재시작 1 2 sudo apachectl configtest sudo systemctl restart httpd 4. SSL 인증서 갱신 자동화 만료 기간 3개월 crontab으로 자동 갱신 설정 만료일 30일 이내일 때 인증서 갱신 가능 갱신 테스트 1 sudo certbot renew --dry-run 갱신 스크립트 작성 1 2 3 4 sudo vi /etc/cron.d/certbot-renew # 매일 새벽 3시 갱신 시도 0 3 * * * root /usr/bin/certbot renew --quiet --deploy-hook \u0026#34;systemctl reload httpd\u0026#34; 트러블슈팅 Let’s Encrypt를 사용한 HTTP1.1 방식의 인증서 발급은 도메인에 80포트로 접근 가능해야 한다.\n공인 ip에 8080→80으로 포워딩해서 진행했었는데, ssl 인증서 발급에 오류가 발생 알아보니 Let’s Encrypt 인증 기관에서 HTTP1.1(webserver) 방식을 사용한 인증서 발급에는 80포트로 고정됨 80 외에 다른 포트를 사용하려면 아래 세 방법 뿐 DNS TXT 레코드를 사용한 인증 사용 이미 HTTPS가 적용되어 있다면 HTTPS를 사용하는 ALPN-01 인증 방식을 사용 다른 인증 기관을 사용 이후에 포워딩을 80→80으로 수정해서 해결 참고 https://www.owl-dev.me/blog/42#\n","date":"2024-12-11T18:00:00+09:00","permalink":"https://joonlog.github.io/p/apache-ssl-https-redirection/","title":"Apache SSL HTTPS Redirection"},{"content":"목표 mysql master 1대, slave 2대로 replication 구성 haproxy + mysql 연동 HAProxy Stat 페이지와 Sysbench 부하 테스트를 통해 시스템 성능과 트래픽 분산이 올바르게 작동하는지 확인 애플리케이션 로직에서 읽기/쓰기 요청을 3306/3307 포트로 분리해서 요청한다고 가정하고 테스트 환경 설정 OS: Rocky 8.10 DB: mysql 8.0.40 LB: haproxy 1.8.27 개념 및 설정 MySQL DB 작업은 쓰기(삽입, 삭제, 수정)는 드물고, 읽기(조회) 작업이 대부분 이를 해소할 수 있는 것이 읽기/쓰기 분리를 적용한 Master-Slave Replication Master에는 모든 작업을 가능하게 설정하고, Slave에는 읽기 작업만 수행 MySQL의 Binary Log를 이용한 Replication 연동 /etc/my.cnf\nMySQL의 Binary Log, 식별 id, 읽기 전용 여부 등 설정 Master와 Slave의 id는 달라야 함 HAproxy L7 로드밸런싱도 가능하지만 여기선 L4기능만 사용 IP, Port를 기준으로 로드밸런싱 애플리케이션 단에서 분리한 읽기/쓰기 요청을 각각 라우팅 /etc/haproxy/haproxy.cfg\nip, port를 통해 분리하여 수신한 트래픽을 처리하는 frontend frontend에서 수신한 트래픽을 ip, port를 통해 로드밸런싱하는 backend Slave 서버 간의 로드밸런싱 Master와 Slave의 연결 상태, 트래픽 분산 모니터링하는 stat 작업 과정 1. MySQL Master 설정 1) Mysql 설치 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 useradd -m -G wheel mysqladmin echo \u0026#34;mysqladmin\u0026#34; | passwd --stdin mysqladmin sudo dnf update --exclude=kernel* -y sudo dnf install -y https://dev.mysql.com/get/mysql80-community-release-el8-9.noarch.rpm # rhel 8 이상은 mysql 기본 모듈이 활성화되어 있어 모듈 리셋 sudo dnf module reset mysql sudo dnf module disable mysql -y sudo dnf install -y mysql-community-server sudo systemctl enable --now mysqld # 초기화 시 생성되는 임시 비밀번호 출력 sudo grep \u0026#39;temporary password\u0026#39; /var/log/mysqld.log # mysql 보안 설치 mysql_secure_installation 방화벽 설정 1 2 3 sudo systemctl enable --now firewalld sudo firewall-cmd --add-port=3306/tcp --permanent sudo firewall-cmd --reload 2) MySQL 설정 Mysql DB, 계정, slave 설정 1 2 3 4 5 6 7 8 9 10 11 12 13 mysql -u root -p CREATE DATABASE replica_db DEFAULT CHARACTER SET utf8; # 레플리케이션 전용 계정 CREATE USER \u0026#39;replica_user\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED BY \u0026#39;\u0026lt;PWD\u0026gt;\u0026#39;; GRANT REPLICATION SLAVE ON *.* TO \u0026#39;replica_user\u0026#39;@\u0026#39;%\u0026#39;; FLUSH PRIVILEGES; # 관리자 계정 CREATE USER \u0026#39;admin_user\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED BY \u0026#39;\u0026lt;PWD\u0026gt;\u0026#39;; GRANT ALL PRIVILEGES ON *.* TO \u0026#39;admin_user\u0026#39;@\u0026#39;%\u0026#39; WITH GRANT OPTION; FLUSH PRIVILEGES; Mysql Master 설정 1 2 3 4 5 6 7 8 sudo vi /etc/my.cnf # master와 slave의 이름 통일 log-bin=mysql-bin # master id을 1로 설정 server-id=1 systemctl restart mysqld master 서버 정보 확인 1 2 3 # slave에서 replication 할 때 필요한 값 show master status; mysql-bin.000001 | 1833 mysql dump로 binary log를 생성하여 Slave 서버에 전송 1 2 3 mysqldump -u admin_user -p replica_db \u0026gt; replica_db.sql1 scp -P 22 replica_db.sql mysqladmin@\u0026lt;Slave1 IP\u0026gt;:/home/mysqladmin scp -P 22 replica_db.sql mysqladmin@\u0026lt;Slave2 IP\u0026gt;:/home/mysqladmin 2. MySQL Slave1,2 설정 1) Mysql 설치 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 useradd -m -G wheel mysqladmin echo \u0026#34;mysqladmin\u0026#34; | passwd --stdin mysqladmin sudo dnf update --exclude=kernel* -y sudo dnf install -y https://dev.mysql.com/get/mysql80-community-release-el8-9.noarch.rpm # rhel 8 이상은 mysql 기본 모듈이 활성화되어 있어 모듈 리셋 sudo dnf module reset mysql sudo dnf module disable mysql -y sudo dnf install -y mysql-community-server sudo systemctl enable --now mysqld # 초기화 시 생성되는 임시 비밀번호 출력 sudo grep \u0026#39;temporary password\u0026#39; /var/log/mysqld.log # mysql 보안 설치 mysql_secure_installation 방화벽 설정 1 2 3 sudo systemctl enable --now firewalld sudo firewall-cmd --add-port=3306/tcp --permanent sudo firewall-cmd --reload 2) Mysql DB 1 2 3 mysql -u root -p CREATE DATABASE replica_db DEFAULT CHARACTER SET utf8; Mysql Slave 설정 1 2 3 4 5 6 7 8 sudo vi /etc/my.cnf # slave1은 2, slave2는 3으로 설정 server-id = 2 # 읽기 전용으로 설정 (쓰기 불가능, root예외) read_only = 1 max_binlog_size = 100M expire_logs_days = 10 Master에서 전송받은 binary log로 MySQL 복원 1 2 mysql -u root -p replica_db \u0026lt; replica_db.sql mysql -u root -p slave에서 master로 연결 위의 master에서 show master status; 결과로 나온 log, Position 입력 1 2 3 4 5 6 change master to master_host=\u0026#39;\u0026lt;Master IP\u0026gt;\u0026#39;, master_user=\u0026#39;replica_user\u0026#39;, master_password=\u0026#39;\u0026lt;Master PW\u0026gt;\u0026#39;, master_log_file=\u0026#39;mysql-bin.000001\u0026#39;, master_log_pos=1833; 1 systemctl restart mysqld slave 설정 확인 Slave_IO_Running/Slave_SQL_Running Yes인지 확인 Seconds_Behind_Master이 0인지 확인 1 2 3 4 show slave status\\G; # root 포함 read_only 설정 SET GLOBAL super_read_only = 1; (선택) caching_sha2_password 에러 발생시 조치 1 2 ALTER USER \u0026#39;replica_user\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED WITH \u0026#39;mysql_native_password\u0026#39; BY \u0026#39;\u0026lt;PASSWORD\u0026gt;\u0026#39;; FLUSH PRIVILEGES; 3. Replication 테스트 Master에서 데이터를 추가/변경하여 Slave 서버에서 동기화가 잘 이루어지는지 확인 Master에서 데이터 생성 1 2 3 4 USE replica_db; CREATE TABLE test_replication (id INT PRIMARY KEY, value VARCHAR(50)); INSERT INTO test_replication (id, value) VALUES (4, \u0026#39;Replication Test4\u0026#39;); select * from test_replication; Slave에서 데이터 조회 1 2 USE replica_db; select * from test_replication; 4. HAproxy 설정 haproxy 설치 1 2 3 4 5 useradd -m -G wheel haproxy echo \u0026#34;haproxy\u0026#34; | passwd --stdin haproxy sudo dnf update --exclude=kernel* -y sudo dnf install -y haproxy haproxy.cfg 설정 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 sudo vi /etc/haproxy/haproxy.cfg # defaults: 이후 section에 적용되는 공통 설정 # frontend: 클라이언트 연결에 관한 설정 # bakend: frontend 에서 접속된 트래픽을 전달할 프록시 서버에 대한 설정 과 HealthCheck 등의 설정 # listen: 프론트엔드와 백엔드의 기능이 결합된 완전한 프록시를 정의 #--------------------- # default tcp #--------------------- defaults tcp mode tcp log global option tcplog retries 3 timeout connect 10s timeout client 1m timeout server 1m timeout check 10s maxconn 3000 #----------------- # haproxy stat 페이지 #----------------- listen stats # 모든 NI에서 9000포트로의 트래픽 수신 bind *:9000 mode http option dontlog-normal stats enable stats refresh 60s stats realm Haproxy\\ Statistics stats uri /haproxy #--------------------------------------------------------------------- # HAproxy #--------------------------------------------------------------------- # 위에서 정의한 tcp defaults 사용 defaults tcp # C/U/D (Master) frontend mysql_write # 모든 NI에서 3306포트로의 트래픽 수신 bind *:3306 default_backend mysql_master backend mysql_master balance roundrobin option tcp-check # 수신한 트래픽을 Master 서버로 라우팅 server mysql-master \u0026lt;Master IP\u0026gt;:3306 check # R (Slave) frontend mysql_read # 모든 NI에서 3307포트로의 트래픽 수신 bind *:3307 default_backend mysql_slaves backend mysql_slaves balance roundrobin option tcp-check # 수신한 트래픽을 Slave 서버로 로드밸런싱 하면서 라우팅 server mysql-slave1 \u0026lt;Slave1 IP\u0026gt;:3306 check server mysql-slave2 \u0026lt;Slave2 IP\u0026gt;:3306 check haproxy 로그 설정 1 2 3 4 5 6 7 sudo vi /etc/rsyslog.conf # HAProxy 로그 설정 local2.* /var/log/haproxy.log sudo systemctl restart rsyslog sudo systemctl restart haproxy 방화벽 설정 1 2 3 4 5 6 7 8 sudo systemctl enable --now firewalld # 3306 포트 허용 (쓰기 전용) sudo firewall-cmd --add-port=3306/tcp --permanent # 3307 포트 허용 (읽기 전용) sudo firewall-cmd --add-port=3307/tcp --permanent # 9000 포트 허용 (스탯) sudo firewall-cmd --add-port=9000/tcp --permanent sudo firewall-cmd --reload haproxy 실행 1 2 3 sudo systemctl enable --now haproxy http://\u0026lt;공인 IP\u0026gt;:9000/haproxy 5. 부하 테스트 sysbench 설치 1 2 sudo dnf install -y epel-release sudo dnf install -y sysbench sysbench 테스트 1 2 3 4 5 CREATE DATABASE test_db; sysbench --db-driver=mysql --mysql-host=\u0026lt;HAproxy IP\u0026gt; --mysql-port=3306 \\ --mysql-db=test_db oltp_read_write prepare \\ --mysql-user=admin_user --mysql-password=\u0026lt;password\u0026gt; 쓰기 테스트 (HAProxy 3306 포트, 마스터로 라우팅 확인) 1 2 3 4 5 6 # oltp_write_only: 쓰기 작업만 수행하여 마스터에 부하가 걸리는지 확인 # time=60: 테스트를 60초 동안 수행 # threads=10: 10개의 쓰레드로 동시에 요청 sysbench --db-driver=mysql --mysql-host=\u0026lt;HAproxy IP\u0026gt; --mysql-port=3306 \\\\ --mysql-db=test_db oltp_write_only --time=60 --threads=10 run \\\\ --mysql-user=admin_user --mysql-password=\u0026lt;password\u0026gt; 읽기 테스트 (HAProxy 3307 포트, 슬레이브로 라우팅 확인) 1 2 3 4 # **oltp_read_only**: 읽기 작업만 수행하여 슬레이브에 부하가 걸리는지 확인 sysbench --db-driver=mysql --mysql-host=\u0026lt;HAproxy IP\u0026gt; --mysql-port=3307 \\ --mysql-db=test_db oltp_read_only --time=60 --threads=10 run \\ --mysql-user=admin_user --mysql-password=\u0026lt;password\u0026gt; 결과 분석 sysbench 결과가 HAproxy stat 페이지에 반영 되는지 확인 쓰기 요청은 마스터로, 읽기 요청은 슬레이브로 분배되는지, 세션 수와 데이터 전송량 등을 확인 로그 확인 haproxy 로그 확인 1 sudo haproxy -c -f /etc/haproxy/haproxy.cfg 트러블슈팅 Replication change master to 로 Slave 설정 후 오류 Last_IO_Error: Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection.\n마스터에서 사용하는 인증 플러그인(caching_sha2_password)으로 인해 슬레이브가 인증에 실패 Master에서 기본 인증 플러그인이 caching_sha2_password로 설정된 경우 발생할 수 있다. Slave는 기본적으로 mysql_native_password 플러그인을 사용하므로, 인증 불일치 문제 발생 계정 인증 방식을 변경하고 재시작 필요 1 2 3 4 5 6 7 # Master ALTER USER \u0026#39;replica_user\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED WITH \u0026#39;mysql_native_password\u0026#39; BY \u0026#39;\u0026lt;비밀번호\u0026gt;\u0026#39;; FLUSH PRIVILEGES; # Slave STOP SLAVE; START SLAVE; Slave에서 my.cnf의 read_only 설정은 root를 제외한 제약이다.\n만약 root로 데이터를 생성하면 replication 설정이 끊김 SET GLOBAL super_read_only = 1; 를 MySQL에서 입력해 root를 포함해 읽기 전용 설정 HAproxy haproxy.cfg의 defaults는 사용 전 정의\nlisten, front/backend 부분에 tcp를 사용중인데 defaults가 http만 있다면 에러가 발생 haproxy.cfg에서 defaults는 listen, front/backend보다 상위에 위치해야 함\nhaproxy stats 모니터링 시에 sysbench로 테스트한 내용이 반영이 안되는 문제 발생 defaults tcp가 listen stat보다 아래에 위치해서 제대로 반영되지 않았기 때문 참고 mysql 설치: https://www.servermon.kr/m/board.html?code=servermon_board2\u0026page=3\u0026type=v\u0026board_cate=\u0026num1=999619\u0026num2=00000\u0026number=363\u0026lock=N\nmysql replication: https://wooj-coding-fordeveloper.tistory.com/63 https://www.didim365.com/blog/20200424b-blog/\nhaproxy: https://velog.io/@parrineau/MySQL-HAproxy-MHA\nhaproxy 상세: https://hoing.io/archives/2196\n","date":"2024-12-10T09:00:00+09:00","permalink":"https://joonlog.github.io/p/mysql-replication-haproxy-%EC%97%B0%EB%8F%99/","title":"MySQL Replication \u0026 HAproxy 연동"},{"content":"목표 Apache-Tomcat을 사용한 WEB-WAS 연동 Proxy / AJP 방식 둘 모두 수행 AJP는 mod_jk 사용 환경 설정 OS: Rocky 8.10 WEB: Apache 2.4.6 WAS: Tomcat 9.0.97 개념 및 설정 Proxy / AJP 비교 연동 방식 Proxy AJP (Apache JServ Protocol) 프로토콜 유형 HTTP (텍스트 기반), HTTPS 가능 AJP (바이너리 프로토콜) 성능 HTTP 오버헤드로 인해 상대적으로 느림 바이너리 프로토콜로 더 빠르고 효율적임 보안 설정 HTTPS로 SSL/TLS 암호화 가능 AJP 자체 암호화 미지원, secret 옵션으로 보안 강화 주 사용 목적 간단한 설정, 외부 요청 처리 내부 네트워크에서의 고성능 요청 처리 호환성 Tomcat 외 서버와도 연동 가능 Tomcat 전용 프로토콜 Apache /etc/httpd/conf/httpd.conf\nApache 메인 설정 파일 proxy-vhosts.conf, ajp-vhosts.conf, httpd-modjk.conf 등 하위 파일 Include mod_jk 와 같은 모듈 로드 /etc/httpd/conf/extra/proxy-vhosts.conf /etc/httpd/conf/extra/ajp-vhosts.conf\nProxy / AJP 용으로 생성한 VirtualHost 파일 VirtualHost 태그에서 Apache가 수신할 트래픽의 IP/Domain, Port 를 설정 여기서 도메인 / 경로 / 포트 기반 라우팅을 설정 이 실습에선 포트 기반 라우팅 사용 ProxyPass에서 트래픽을 전달할 WAS 경로, IP/Domain 등을 설정 AJP는 JkMount에서 WAS 경로, 워커를 설정 Apache-Tomcat 연동에 필요한 proxy_module, proxy_http_module 모듈은 기본 설치\nAJP는 mod_jk, mod_proxy_ajp 두 모듈 중 하나만 있으면 연동 가능\n모듈 mod_jk mod_proxy_ajp 설치 별도 설치 필요 Apache 기본 모듈로 제공 설정 복잡 (별도 설정 파일 필요) 간단 (Apache 설정 파일에서 구성) 로드 밸런싱 강력 (다양한 알고리즘 지원) 제한적 (별도 모듈 필요) 클러스터링 세션 스티키니스 및 고급 기능 지원 제한적 성능 고성능 (대규모 환경에 적합) 단순 연동 시 적합 사용 사례 대규모 클러스터, 고급 로드 밸런싱이 필요한 경우 간단한 AJP 연동 및 소규모 환경 /etc/httpd/conf/extra/httpd-modjk.conf\\\\\nmod_jk 모듈 설정 파일 태그로 Apache에 mod_jk가 로드된 경우에만 적용 workers.properties 파일 경로 지정 로그 경로, 로그 레벨 지정 /etc/httpd/conf/extra/workers.properties\nmod_jk 워커(worker1 포함)의 IP, Port 등을 설정하는 파일 여러 워커 설정 가능 Tomcat /etc/systemd/system/tomcat.service\nrocky 8.1에서는 Tomcat이 systemd에 등록된 패키지가 없음 Tomcat을 systemd 서비스로 관리하기 위한 설정 /usr/local/apache-tomcat-9.0.97/conf/server.xml\nProxy Connector은 기본 설정 사용 AJP Connector 포트, 허용 IP, Secret 설정 이 실습에서 Secret은 false로 설정 Secret을 사용 한다면 Apache에도 Secret 설정 필수 작업 과정 1. Proxy 방식 1) Apache 서버 설정 Apache 설치/실행 1 2 3 sudo dnf update --exclude=kernel* sudo dnf install httpd -y sudo systemctl enable --now httpd 모듈 활성화 확인 (proxy_module, proxy_http_module) 1 httpd -M | grep proxy Apache 서버에서 HTTP 트래픽을 허용하도록 방화벽 설정 추가 1 2 3 sudo systemctl enable --now firewalld sudo firewall-cmd --permanent --add-port=8080/tcp sudo firewall-cmd --reload proxy-vhosts.conf 설정 파일 추가 1 sudo vi /etc/httpd/conf/httpd.conf 1 2 # 별도의 파일에서 Virtual Host 설정 관리 Include conf/extra/proxy-vhosts.conf Apache가 Tomcat 서버로 프록시 역할을 수행하도록 설정 1 2 sudo mkdir -p /etc/httpd/conf/extra sudo vi /etc/httpd/conf/extra/proxy-vhosts.conf 1 2 3 4 5 6 7 8 9 10 # Apache가 8080 포트에서 수신된 요청 처리 \u0026lt;VirtualHost *:8080\u0026gt; # Apache 서버의 IP 또는 도메인 지정 ServerName \u0026lt;WEB IP\u0026gt; # Apache가 수신한 요청을 WAS 8080 포트로 프록시 처리 ProxyRequests Off ProxyPass / http://\u0026lt;WAS IP\u0026gt;:8080/ ProxyPassReverse / http://\u0026lt;WAS IP\u0026gt;:8080/ \u0026lt;/VirtualHost\u0026gt; Apache 서버 재시작 1 sudo systemctl restart httpd 2) Tomcat 서버 설정 Java 설치 1 2 3 4 sudo dnf update --exclude=kernel* sudo dnf install java-11-openjdk -y java -version ~/.bashrc 환경 변수 추가 1 2 export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java)))) export PATH=$PATH:$JAVA_HOME/bin Tomcat 설치 1 2 3 wget \u0026lt;https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.97/bin/apache-tomcat-9.0.97.tar.gz\u0026gt; tar xvzf apache-tomcat-9.0.97.tar.gz apache-tomcat-9.0.97 sudo mv apache-tomcat-9.0.97 /usr/local tomcat 사용자 설정 1 2 3 sudo groupadd tomcat sudo useradd -g tomcat -d /usr/local/apache-tomcat-9.0.97 -s /bin/false tomcat sudo chown -R tomcat:tomcat /usr/local/apache-tomcat-9.0.97 Tomcat 서비스 등록 1 vi /etc/systemd/system/tomcat.service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [Unit] Description=tomcat 9 After=network.target syslog.target [Service] Type=forking # JDK와 Tomcat 설치 경로 지정 Environment=\u0026#34;JAVA_HOME=/usr/lib/jvm/java-11-openjdk-11.0.25.0.9-2.el8.x86_64\u0026#34; Environment=\u0026#34;CATALINA_HOME=/usr/local/apache-tomcat-9.0.97\u0026#34; User=tomcat Group=tomcat ExecStart=/usr/local/apache-tomcat-9.0.97/bin/startup.sh ExecStop=/usr/local/apache-tomcat-9.0.97/bin/shutdown.sh [Install] WantedBy=multi-user.target Tomcat 실행 1 2 3 sudo systemctl daemon-reload sudo systemctl enable --now tomcat sudo systemctl status tomcat 방화벽 HTTP 8080 포트 허용 1 2 3 sudo systemctl enable --now firewalld sudo firewall-cmd --permanent --add-port=8080/tcp sudo firewall-cmd --reload 3) 설정 확인 브라우저에서 Apache 서버의 IP 주소로 접근하여 Tomcat의 기본 페이지가 나타나는지 확인 http://\u0026lt;공인 ip\u0026gt;:8080\n2. AJP 방식 1) Apache 서버 설정 Apache 설치/실행 1 2 3 sudo dnf update --exclude=kernel* sudo dnf install httpd -y sudo systemctl enable --now httpd tomcat-connectors 설치(mod_jk) 1 2 3 4 5 6 7 8 9 10 11 12 sudo yum install autoconf libtool httpd-devel -y wget \u0026lt;https://dlcdn.apache.org/tomcat/tomcat-connectors/jk/tomcat-connectors-1.2.50-src.tar.gz\u0026gt; tar -zxvf tomcat-connectors-1.2.50-src.tar.gz cd tomcat-connectors-1.2.50-src/native ./configure --with-apxs=/usr/bin/apxs sudo dnf install make -y sudo dnf install redhat-rpm-config -y make sudo make install 모듈 활성화 확인 1 2 sudo systemctl restart httpd httpd -M | grep jk Apache 서버에서 HTTP 트래픽을 허용하도록 방화벽 설정 추가 1 2 3 sudo systemctl enable --now firewalld sudo firewall-cmd --permanent --add-port=8090/tcp sudo firewall-cmd --reload httpd.conf에 mod_jk 모듈 추가 httpd.conf에 httpd-modjk.conf 설정 파일 추가 1 sudo vi /etc/httpd/conf/httpd.conf 1 2 3 4 5 # 설치한 mod_jk 모듈 로드 LoadModule jk_module modules/mod_jk.so # ajp 설정 파일 포함 Include conf/extra/httpd-modjk.conf vhosts.conf 에 jkmount 추가 1 2 sudo mkdir -p /etc/httpd/conf/extra sudo vi /etc/httpd/conf/extra/ajp-vhosts.conf 1 2 3 4 5 6 7 8 # Apache가 8090 포트에서 수신된 요청 처리 \u0026lt;VirtualHost *:8090\u0026gt; # AJP 요청을 수신할 Apache 서버 IP 지정 ServerName \u0026lt;WEB IP\u0026gt; # 모든 요청(/*)을 worker1으로 라우팅 JkMount /* worker1 \u0026lt;/VirtualHost\u0026gt; httpd-modjk.conf 파일 설정 로그 레벨 debug로 변경 후 mod_jk.log에서 로그 확인 가능 1 sudo vi /etc/httpd/conf/extra/httpd-modjk.conf 1 2 3 4 5 6 7 8 9 10 11 \u0026lt;IfModule jk_module\u0026gt; # ajp worker 설정 파일 지정 JkWorkersFile conf/extra/workers.properties # 상태 정보 파일 경로 지정 JkShmFile logs/mod_jk.shm # 로그 파일 경로 지정 JkLogFile logs/mod_jk.log # 로그 레벨 지정 JkLogLevel debug JkLogStampFormat \u0026#34;[%a %b %d %H:%M:%S %Y]\u0026#34; \u0026lt;/IfModule\u0026gt; workers.properties 파일 설정 1 sudo vi /etc/httpd/conf/extra/workers.properties 1 2 3 4 5 6 7 8 9 # 워커 목록 worker.list=worker1 # AJP 포트 worker.worker1.port=8009 # Tomcat 서버 IP worker.worker1.host=\u0026lt;WAS IP\u0026gt; # 프로토콜 버전 지정 worker.worker1.type=ajp13 workers.properties, httpd-modjk.conf 권한 설정 1 2 sudo chmod 755 /etc/httpd/conf/extra/workers.properties sudo chmod 755 /etc/httpd/conf/extra/httpd-modjk.conf Apache 서버 재시작 1 sudo systemctl restart httpd 2) Tomcat 서버 설정 Java 설치 1 2 3 4 sudo dnf update --exclude=kernel* sudo dnf install java-11-openjdk -y java -version ~/.bashrc 환경 변수 추가 1 2 export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java)))) export PATH=$PATH:$JAVA_HOME/bin Tomcat 설치 1 2 3 wget \u0026lt;https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.97/bin/apache-tomcat-9.0.97.tar.gz\u0026gt; tar xvzf apache-tomcat-9.0.97.tar.gz apache-tomcat-9.0.97 sudo mv apache-tomcat-9.0.97 /usr/local tomcat 사용자 설정 1 2 3 sudo groupadd tomcat sudo useradd -g tomcat -d /usr/local/apache-tomcat-9.0.97 -s /bin/false tomcat sudo chown -R tomcat:tomcat /usr/local/apache-tomcat-9.0.97 Tomcat 서비스 등록 1 vi /etc/systemd/system/tomcat.service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [Unit] Description=tomcat 9 After=network.target syslog.target [Service] Type=forking # JDK와 Tomcat 설치 경로 지정 Environment=\u0026#34;JAVA_HOME=/usr/lib/jvm/java-11-openjdk-11.0.25.0.9-2.el8.x86_64\u0026#34; Environment=\u0026#34;CATALINA_HOME=/usr/local/apache-tomcat-9.0.97\u0026#34; User=tomcat Group=tomcat ExecStart=/usr/local/apache-tomcat-9.0.97/bin/startup.sh ExecStop=/usr/local/apache-tomcat-9.0.97/bin/shutdown.sh [Install] WantedBy=multi-user.target Tomcat AJP 커넥터 설정 Tomcat의 server.xml 파일을 열어 AJP 커넥터 활성화 1 sudo vi /usr/local/apache-tomcat-9.0.97/conf/server.xml 1 2 3 # AJP를 수신을 허용할 Source IP 지정 # AJP 키 인증 비활성화 \u0026lt;Connector port=\u0026#34;8009\u0026#34; protocol=\u0026#34;AJP/1.3\u0026#34; redirectPort=\u0026#34;8443\u0026#34; address=\u0026#34;\u0026lt;WAS IP\u0026gt;\u0026#34; secretRequired=\u0026#34;false\u0026#34;/\u0026gt; Tomcat 실행 1 2 3 sudo systemctl daemon-reload sudo systemctl enable --now tomcat sudo systemctl status tomcat 방화벽 AJP 8080 포트 허용 1 2 3 sudo systemctl enable --now firewalld sudo firewall-cmd --permanent --add-port=8009/tcp sudo firewall-cmd --reload 3) 설정 확인 브라우저에서 Apache 서버의 IP 주소로 접근하여 Tomcat의 기본 페이지가 나타나는지 확인 http://\u0026lt;공인ip\u0026gt;:8090\n로그 확인 연동 시의 오류도 tomcat에 찍히기 때문에 catalina 로그를 가장 많이 확인\nApache 로그 확인 1 2 3 4 5 6 # 에러 로그 tail -f /var/log/httpd/error_log # 액세스 로그 tail -f /var/log/httpd/access_log # mod_jk 로그 tail -f /var/log/httpd/mod_jk.log Tomcat 로그 확인 1 2 3 4 # tomcat 로그 tail -f /usr/local/apache-tomcat-9.0.97/logs/catalina.out # localhost 로그 tail -f /usr/local/apache-tomcat-9.0.97/logs/localhost.log 트러블슈팅 VirtualHost에서 / 경로는 webapps/ROOT를 가리킨다.\n/ajp/* 와 같이 경로를 지정하면 apache-tomcat-9.0.97/webapps/ajp 폴더에 페이지가 존재해야 함 Apache의 httpd.conf는 모듈이 앞에 있지 않으면 해당 모듈을 사용하는 설정은 무시된다.\nhttpd.conf에서 LoadModule이 Include보다 앞에 위치해야 함 JkMount는 VirtualHost 내부에 위치해야 한다.\n*https://ineastdev.tistory.com/entry/apache-tomcat-을-modjk-로-연동시-주의사항*\nVirtualHost를 설정한 적이 없다면 httpd.conf에 Include된 파일 중에 VirtualHost가 있는지 확인 필수 이 실습에서는 proxy 연동을 동시에 진행했기 때문에 proxy_vhosts.conf파일에 VirtualHost가 존재했고, AJP 연동에서 VirtualHost 없이 JkMount만 작성했기 때문에 오류가 발생 Apache는 /ajp 를 입력받으면 내부적으로 /ajp/ 경로로 리다이렉션 한다.\n경로 끝에 / 가 없는 URL을 입력할 경우, Apache에서는 DirectorySlash Off 설정으로 인해, 이 경로를 디렉토리로 간주하여 / 을 추가하여 리다이렉션 한 후에 내부에서 처리 만약 실제 디렉토리가 존재한다면 / 을 추가한 상태로 리다이렉션을 응답하고, 존재하지 않는다면 다른 리소스로 처리(기존 경로 응답) DirectorySlash Off 설정으로 수정할 수 있지만, 대부분의 WEB의 기본값이기 때문에 수정을 권장하지 않음 따라서 JkMount의 경로에 /ajp 대신 /ajp/* 로 기입해 해결 만약 실제 디렉토리가 존재한다면 / 을 추가한 상태로 리다이렉션을 응답하고, 존재하지 않는다면 다른 리소스로 처리(기존 경로 응답) DirectorySlash Off 설정으로 수정할 수 있지만, 대부분의 WEB의 기본값이기 때문에 수정을 권장하지 않음 따라서 JkMount의 경로에 /ajp 대신 /ajp/* 로 기입해 해결 참고 proxy 방식:\nhttps://velog.io/@ela__gin/리눅스-서버에서-ApacheTomcat-mod-proxy-방식-연동-웹소켓-푸시-알림-연동-포함\najp 방식:\nhttps://nahosung.tistory.com/121\nhttps://lilo.tistory.com/83\nhttps://ineastdev.tistory.com/entry/apache-tomcat-을-modjk-로-연동시-주의사항\ntomcat 서비스 등록: https://getchu.tistory.com/14\n","date":"2024-12-09T18:08:00+09:00","permalink":"https://joonlog.github.io/p/apache-tomcat-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%97%B0%EB%8F%99/","title":"Apache-Tomcat 설치 및 연동"}]