폐쇄망 K3s에 Gitea 한 번에 설치하기 (Helm + Istio Gateway + Self-signed CA)

폐쇄망 K3s 클러스터를 운영하면서 가장 먼저 부딪힌 벽은, 외부 인터넷 없이 어디서 코드와 컨테이너 이미지를 가져올 것인가 였습니다.

GitHub, Docker Hub, Helm 차트 저장소가 모두 닿지 않는 환경에서, “클러스터 안에 자체 Git 저장소 + Container Registry”가 필요했고, 선택지 중 가장 가벼운 게 Gitea 였습니다.

이번 글에서는 폐쇄망에서 Gitea 를 K3s 위에 한 번에 설치하기 위해, install.sh + Helm values + 이미지 tar + Self-signed CA + Istio Gateway 매니페스트를 묶어둔 air-gap 번들 구조를 정리합니다.


1. 발단

폐쇄망 환경에서 K3s 만 깔린 상태로 시작했고, 그 위에 도는 워크로드는 다음을 의존하고 있었습니다.

  • 소스 코드 저장소 (값 변경, GitOps용)
  • 컨테이너 레지스트리 (자체 이미지 + 외부 이미지 미러)
  • (가능하면) Helm chart 저장소

이걸 컴포넌트별로 따로 띄우는 방법도 있었지만, 폐쇄망에서는 유지보수할 컴포넌트 수가 곧 운영 부담이 됩니다.

Gitea 한 컴포넌트가 위 셋을 모두 커버할 수 있어서, 다음 이유로 Gitea 를 골랐습니다.

비교 항목 Gitea GitLab
메모리 사용량 수백 MB 수 GB
의존성 단순 (DB + Valkey) 많음 (Postgres + Redis + Sidekiq + 다수 컴포넌트)
설치 난이도 Helm 한 번으로 끝 옵션이 많아 튜닝 필요
Container Registry 내장 (OCI) 내장

폐쇄망 운영자 한 명이 다루기에는 Gitea 쪽이 명확히 가벼웠습니다.


2. 무엇을 미리 묶어 가져갈 것인가

폐쇄망의 핵심 제약은 단순합니다.

클러스터가 외부 호스트와 통신할 수 없다. 그래서 설치에 필요한 모든 파일을 번들로 미리 묶어 가져가야 한다.

번들 구조는 이렇게 잡았습니다.

gitea-airgap-bundle/
├── install.sh                # 원샷 설치 스크립트
├── values.yaml               # Helm values
├── registries.yaml.tpl       # containerd registry 설정 템플릿
├── manifests/
│   └── gateway.yaml          # Istio Gateway + VirtualService
├── charts/
│   └── gitea-12.5.3.tgz      # Gitea Helm chart (오프라인 tar)
├── images/
│   └── gitea-images.tar      # gitea + valkey 컨테이너 이미지
└── ca/
    ├── ca.crt                # Private Root CA
    ├── server-fullchain.crt  # server cert + CA chain
    └── server.key            # server private key

이 한 디렉토리만 폐쇄망 호스트에 옮기면 설치가 끝나도록 설계했습니다.

다음 사전 조건이 충족된 상태를 가정합니다.

  • K3s 가 떠 있음 (containerd 기반)
  • Istio 가 깔려 있고 istio-ingressgateway 가 Running
  • 노드에서 kubectl, helm, sudo, python3 사용 가능
  • 내부 DNS 또는 /etc/hostsgitea.example.com 이 노드 IP 로 해석됨

3. install.sh 가 하는 일

스크립트 한 번이면 끝나도록 8단계로 잡았습니다.

1. k3s ctr 로 이미지 import (gitea, valkey)
2. OS 에 CA 신뢰 등록 (update-ca-trust / update-ca-certificates)
3. namespace gitea + admin secret 생성
4. istio-system 에 TLS secret 생성 (Gateway 가 사용할 인증서)
5. helm upgrade --install gitea ./charts/gitea-12.5.3.tgz -n gitea -f values.yaml
6. Istio Gateway + VirtualService 적용
7. 조직(org) 생성 + ArgoCD 용 PAT(Personal Access Token) 발급
8. (선택) ArgoCD repo-creds 등록

이 8단계의 핵심은 재실행이 안전(idempotent) 해야 한다는 점입니다.

  • 이미지 import 는 이미 있으면 skip
  • CA 신뢰 등록은 같은 파일이면 변화 없음
  • namespace / secret 은 kubectl apply / create --dry-run | apply 패턴
  • helm 은 upgrade --install 로 멱등
  • Gateway/VS 도 kubectl apply

같은 번들로 ./install.sh 를 다시 돌려도 결과가 같아야, 설치 중 어디서 멈춰도 그냥 다시 돌리면 되는 흐름이 잡힙니다.


4. Self-signed CA 와 TLS 처리

폐쇄망에서는 Let’s Encrypt 같은 외부 CA 를 사용할 수 없습니다. 대신 Private Root CA 를 만들어 두고, 클러스터 안의 모든 도메인을 그 CA 의 인증서로 묶었습니다.

4-1. OS 신뢰 등록

스크립트가 호스트 OS 에 CA 를 등록합니다.

sudo cp ca/ca.crt /etc/pki/ca-trust/source/anchors/your-private-ca.crt
sudo update-ca-trust

이렇게 두면 curl https://gitea.example.com:30443/ 같은 호출이 “unknown CA” 에러 없이 통과합니다.

4-2. Istio Gateway 용 TLS secret

kubectl create secret tls your-wildcard-tls \
  -n istio-system \
  --cert=ca/server-fullchain.crt \
  --key=ca/server.key \
  --dry-run=client -o yaml | kubectl apply -f -

Istio Gateway 의 tls.credentialName 으로 이 secret 을 가리키면, HTTPS termination 이 Gateway 단에서 처리됩니다.

같은 wildcard 인증서 하나로 여러 호스트(gitea, registry, argocd 등)를 한 번에 커버할 수 있습니다.


5. Istio Gateway + VirtualService

Gitea 는 다음 세 경로로 외부에 노출되어야 합니다.

용도 경로
Web UI https://gitea.example.com:<NodePort>/
OCI Container Registry gitea.example.com:<NodePort>/<org>/<image>:tag
Git SSH ssh://git@gitea.example.com:<SshNodePort>/<org>/<repo>.git

HTTPS 트래픽은 Istio Gateway + VirtualService 로 처리합니다.

apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: gitea-gateway
  namespace: gitea
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 443
        name: https
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: your-wildcard-tls
      hosts:
        - gitea.example.com
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: gitea-vs
  namespace: gitea
spec:
  hosts:
    - gitea.example.com
  gateways:
    - gitea-gateway
  http:
    - route:
        - destination:
            host: gitea-http.gitea.svc.cluster.local
            port:
              number: 3000

SSH 트래픽은 별도 NodePort (예: 32022) 로 노출했습니다. 단순 TCP 라 Istio 없이 그냥 Service NodePort 면 충분합니다.


6. 설치 후 접속

Web:        https://gitea.example.com:<NodePort>
Registry:   gitea.example.com:<NodePort>/<org>/<image>:tag
SSH Git:    ssh://git@gitea.example.com:<SshNodePort>/<org>/<repo>.git
Admin:      gitea_admin / <admin-password>

설치 직후 클러스터에서 이미지를 pull 하려면 K3s 가 이 레지스트리를 인식해야 합니다.

sudo cp registries.yaml.tpl /etc/rancher/k3s/registries.yaml
sudo systemctl restart k3s

registries.yaml 에 Gitea 도메인 + auth + Self-signed CA 를 함께 잡아두면, 이 시점 이후로 클러스터 안의 모든 pod 가 Gitea 레지스트리에서 이미지를 가져올 수 있습니다.


7. ArgoCD 연동까지 한 번에

번들의 마지막 단계로 ArgoCD repo-creds 등록을 함께 처리했습니다.

GITEA_PAT=$(gitea-api 로 PAT 발급)
kubectl -n argocd create secret generic gitea-repo-creds \
  --from-literal=type=git \
  --from-literal=url=http://gitea-http.gitea.svc.cluster.local:3000 \
  --from-literal=username=gitea_admin \
  --from-literal=password=$GITEA_PAT \
  --dry-run=client -o yaml \
| kubectl apply -f -

여기서 한 가지 디테일이 있습니다. url클러스터 내부 DNS (gitea-http.gitea.svc.cluster.local:3000) 으로 잡으면, ArgoCD 가 외부 도메인을 거치지 않고 직접 pod 에 접근합니다.

폐쇄망에서 DNS / 인증서 문제로 외부 도메인 호출이 흔들리는 경우에도, 이 한 줄 덕분에 ArgoCD ↔ Gitea 통신은 안정적으로 유지됩니다.

또 한 가지: url 을 host-level (포트 포함 prefix) 로만 두면, 같은 Gitea 인스턴스 안의 모든 repo 에 이 credential 이 자동으로 적용됩니다. repo 한 개씩 등록할 필요가 없습니다.


8. 짚어두고 싶은 것들

8-1. air-gap 번들의 첫 번째 미덕은 멱등성

폐쇄망 설치는 한 번 실패하면 외부 자료를 다시 받아오기가 어렵습니다. 그래서 번들 안의 모든 스크립트는 두 번 실행해도 결과가 같은 형태로 짜두는 게 가장 중요합니다.

이번 install.sh 는 모든 kubectl 단계에서 --dry-run=client | apply 패턴을 사용하고, helm 은 upgrade --install, 이미지 import 는 already-exists 체크 후 skip 형태로 잡혀 있습니다.

8-2. CA 는 한 번 만들어 길게 쓰자

Private Root CA 의 유효기간을 짧게 잡으면, 만료될 때마다 클러스터 전체 인증서를 다시 발급해야 합니다. 폐쇄망에서는 그 작업이 의외로 비용이 큽니다.

이번에는 CA 유효기간을 20년으로 잡아 두고 (CN=Your Private Root CA, Not After=2046-XX-XX), 중간 인증서(server cert) 만 짧게 회전하는 패턴을 썼습니다.

8-3. 도메인 이름은 미리 정해두자

설치 후에 도메인을 바꾸면 다음을 모두 다시 손대야 합니다.

  • TLS secret (인증서 SAN)
  • Istio Gateway hosts
  • VirtualService hosts
  • ArgoCD repo URL
  • registries.yaml mirror endpoint
  • 호스트 /etc/hosts

처음 한 번에 도메인을 길게 잡고 시작하는 게 안전합니다.

8-4. 같은 인증서로 여러 호스트 묶기

*.example.com 같은 wildcard 인증서를 하나 발급해서 Gitea, Registry, ArgoCD, Grafana 등의 호스트를 모두 같은 인증서로 묶으면, 한 번의 인증서 교체로 모든 서비스가 갱신됩니다.

폐쇄망 운영 부담을 줄이는 가장 단순한 방법 중 하나입니다.

8-5. Container Registry 자체로도 쓸 수 있다

Gitea 의 Container Registry 는 OCI 호환이라, Docker Hub / quay.io / ghcr.io 같은 외부 레지스트리도 mirror 로 받아 둘 수 있습니다.

이 패턴은 별도 글로 정리할 만한 분량이라, 다음 글에서 이어가려고 합니다.


9. 마무리

폐쇄망 운영의 어려움은 보통 “외부 자료가 닿지 않는다”는 첫 줄에 모입니다. 이걸 정면으로 푸는 가장 단순한 답은, 필요한 모든 자료를 미리 한 디렉토리에 묶어 들고 들어가는 방식이었습니다.

폐쇄망 install 번들의 핵심은 멱등성과 단일 디렉토리 자급식 구조. 두 번 실행해도 결과가 같고, 다른 외부 자료에 의존하지 않아야 한다.

이번 Gitea 설치를 한 번 코드로 박아둔 뒤로는, 새로운 폐쇄망 클러스터를 만들 때마다 같은 번들로 30분 안에 코드 저장소 + 이미지 레지스트리가 준비됩니다.

다음 글에서는 이렇게 띄운 Gitea Container Registry 를 외부 이미지 미러(docker.io / quay.io / ghcr.io) 로 활용하는 흐름, 즉 registries.yaml 의 mirror 설정과 외부 이미지를 어떻게 Gitea 안에 ctr push 로 옮겨두는지를 정리해보려고 합니다.


This site uses Just the Docs, a documentation theme for Jekyll.