diff --git a/.github/workflows/promote-portfolio-image.yml b/.github/workflows/promote-portfolio-image.yml new file mode 100644 index 0000000..5fd7c0d --- /dev/null +++ b/.github/workflows/promote-portfolio-image.yml @@ -0,0 +1,40 @@ +name: Promote Portfolio Image + +on: + workflow_dispatch: + inputs: + image_tag: + description: Docker tag to promote into the production overlay. + required: true + type: string + +permissions: + contents: write + +jobs: + promote: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Update production image tag + env: + IMAGE_TAG: ${{ inputs.image_tag }} + run: | + sed -i "s/^ newTag: .*/ newTag: ${IMAGE_TAG}/" apps/portfolio/overlays/prod/kustomization.yaml + + - name: Commit manifest update + env: + IMAGE_TAG: ${{ inputs.image_tag }} + run: | + if git diff --quiet -- apps/portfolio/overlays/prod/kustomization.yaml; then + echo "No manifest change detected." + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add apps/portfolio/overlays/prod/kustomization.yaml + git commit -m "chore(portfolio): promote image to ${IMAGE_TAG}" + git push diff --git a/.github/workflows/validate-gitops.yml b/.github/workflows/validate-gitops.yml new file mode 100644 index 0000000..703f8a1 --- /dev/null +++ b/.github/workflows/validate-gitops.yml @@ -0,0 +1,31 @@ +name: Validate GitOps Manifests + +on: + pull_request: + push: + branches: + - main + +jobs: + kustomize-build: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Install kustomize + uses: imranismail/setup-kustomize@v2 + + - name: Build bootstrap manifests + run: | + kustomize build bootstrap/home-mgmt/argocd-install >/dev/null + kustomize build bootstrap/home-mgmt/root-app >/dev/null + + - name: Build Argo CD application set + run: | + kustomize build argocd >/dev/null + + - name: Build OCI platform and workload manifests + run: | + kustomize build clusters/oci-prod >/dev/null + kustomize build apps/portfolio/overlays/prod >/dev/null diff --git a/Jenkins/.env b/Jenkins/.env deleted file mode 100644 index afadd0b..0000000 --- a/Jenkins/.env +++ /dev/null @@ -1,5 +0,0 @@ -DOCKER_GROUP_ID=1001 -JENKINS_HOME=/home/youngwon/jenkins_home -DOCKER_SOCK=/var/run/docker.sock -SECRETS_ENV_FILE=/home/youngwon/secrets/serverSecrets -PORT=8080 \ No newline at end of file diff --git a/Jenkins/Dockerfile b/Jenkins/Dockerfile deleted file mode 100644 index c293a26..0000000 --- a/Jenkins/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM jenkins/jenkins:lts - -USER root -RUN apt-get update \ - && apt-get install ca-certificates curl gnupg \ - && install -m 0755 -d /etc/apt/keyrings \ - && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ - && chmod a+r /etc/apt/keyrings/docker.gpg - -RUN echo \ - "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \ - "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ - tee /etc/apt/sources.list.d/docker.list > /dev/null - -RUN apt-get update && apt-get install -y docker-ce-cli - -ARG DOCKER_GROUP_ID -RUN groupadd -g ${DOCKER_GROUP_ID} docker && usermod -aG docker jenkins - -USER jenkins \ No newline at end of file diff --git a/Jenkins/docker-compose.yml b/Jenkins/docker-compose.yml deleted file mode 100644 index fbd6706..0000000 --- a/Jenkins/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -services: - jenkins: - image: yw7148/jenkins:latest - container_name: jenkins - build: - context: . - dockerfile: Dockerfile - args: - - DOCKER_GROUP_ID=${DOCKER_GROUP_ID} - restart: unless-stopped - volumes: - - ${JENKINS_HOME}:/var/jenkins_home - - ${DOCKER_SOCK}:/var/run/docker.sock - env_file: - - ${SECRETS_ENV_FILE} - ports: - - ${PORT}:8080 - - 50000:50000 \ No newline at end of file diff --git a/Nginx/.env b/Nginx/.env deleted file mode 100644 index 7de1467..0000000 --- a/Nginx/.env +++ /dev/null @@ -1,3 +0,0 @@ -CONFD_DIR=/home/opc/server/Nginx/conf.d -NGINX_CONF_FILE=/home/opc/server/Nginx/nginx.conf -SSL_DIR=/home/opc/ssl \ No newline at end of file diff --git a/Nginx/docker-compose.yml b/Nginx/docker-compose.yml deleted file mode 100644 index ecff0ac..0000000 --- a/Nginx/docker-compose.yml +++ /dev/null @@ -1,17 +0,0 @@ -services: - nginx: - image: nginx:latest - container_name: nginx - volumes: - - ${NGINX_CONF_FILE}:/etc/nginx/nginx.conf - - ${CONFD_DIR}:/etc/nginx/conf.d - - ${SSL_DIR}:/etc/nginx/ssl - networks: - - youngwon - ports: - - 80:80 - - 443:443 - -networks: - youngwon: - external: true \ No newline at end of file diff --git a/Nginx/nginx.conf b/Nginx/nginx.conf deleted file mode 100644 index 5f03fd6..0000000 --- a/Nginx/nginx.conf +++ /dev/null @@ -1,29 +0,0 @@ -events {} - -http { - server { - listen 80; - return 301 https://$host$request_uri; - } - - server { - listen 443 ssl; - server_name youngwon.me; - - ssl_certificate /etc/nginx/ssl/youngwon.me_202411031E137.crt.pem; - ssl_certificate_key /etc/nginx/ssl/youngwon.me_202411031E137.key.pem; - - location = / { - return 301 http://youngwon.me/portfolio; - } - - location /portfolio { - proxy_pass http://158.180.88.84:9001; - } - - location /nginx { - add_header Content-Type text/html; - return 200 'Nginx'; - } - } -} \ No newline at end of file diff --git a/README.md b/README.md index 9c0cc45..fe409f6 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,65 @@ # server Everything About my Oracle Cloud Server Setting ![server-arch](https://github.com/yw7148/server/assets/71220342/a857d7df-631f-4a1e-9723-24d352748b1a) -## WAS Server (150.230.252.102) +## Current Direction -## DevOps server (158.180.85.209) -### Network -``` -docker network create youngwon -``` +이 리포는 Jenkins/Nginx 기반 운영에서 Home node의 Argo CD 관리 클러스터와 OCI k3s 런타임 클러스터로 전환한 상태를 관리한다. -### Nginx -``` -docker run -d \ - -p 80:80 -p 443:443\ - -v {PATH_TO_NGINX_CONF}:/etc/nginx/nginx.conf \ - --network youngwon \ - --name nginx \ - nginx:latest -``` - - My Server: -``` -docker run -d \ - -p 80:80 -p 443:443\ - -v /home/opc/server/Nginx/nginx.conf:/etc/nginx/nginx.conf \ - --network youngwon \ - --name nginx \ - nginx:latest -``` -or with docker-compose: -``` -docker compose -f Nginx/docker-compose.yml up -d -``` -## Local Server - > Moved from Oracle Cloud server because of server performance issue. +- 설계 문서: `docs/k3s-argocd-migration-design.md` +- OCI 적용 가이드: `docs/oci-k3s-apply-guide.md` +- Argo CD bootstrap: `bootstrap/home-mgmt/` +- Argo CD application/app-of-apps: `argocd/` +- OCI 공통 플랫폼: `clusters/oci-prod/` +- 서비스별 앱 배포 선언: `apps/` -### Jenkins - > To use host docker engine in jenkins container, build Jenkins/Dockerfile with {HOST_DOCKER_GROUP_ID} -``` -docker build --build-arg DOCKER_GROUP_ID={HOST_DOCKER_GROUP_ID} -t yw7148/jenkins:latest Jenkins/. -``` -``` -docker run -d \ - -p 50000:50000 -p 8080:8080 \ - -v {PATH_TO_JENKINS_HOME}:/var/jenkins_home \ - -v /var/run/docker.sock:/var/run/docker.sock \ - --name jenkins \ - --env-file {SECRETS_ENV_PATH}\ - yw7148/jenkins:latest -``` -- My Server (WSL): -``` -docker build --build-arg DOCKER_GROUP_ID=1001 -t yw7148/jenkins:latest Jenkins/. -``` -``` -docker run -d \ - -p 50000:50000 -p 8080:8080 \ - -v /home/youngwon/jenkins_home:/var/jenkins_home \ - -v /var/run/docker.sock:/var/run/docker.sock \ - --name jenkins \ - --env-file /home/youngwon/secrets/serverSecrets \ - --restart unless-stopped \ - yw7148/jenkins:latest -``` -or with docker-compose -``` -docker compose -f Jenkins/docker-compose.yml up -d --build -``` -#### To support multi-platform image (linux/amd64, linux/arm64, ...) -``` -docker buildx create --name multiplatform --bootstrap --use -``` -#### To deploy with docker hub ( [Youngwon's DockerHub](https://hub.docker.com/repositories/yw7148) ) - - login to docker hub -> If you get "Error saving credentials: error storing credentials" error, open ~/.docker/config.json file and set "credsStore": "". -``` -docker login -``` - - create docker context to deploy more easiliy -``` -docker context create jenkins_was --docker host='ssh://jenkins@150.230.250.174' -docker context create jenkins_devops --docker host='ssh://jenkins@152.67.206.246' -``` - - now deploy to server with +## Apply To OCI + +상세 절차는 `docs/oci-k3s-apply-guide.md`를 보면 된다. + +핵심 순서는 다음과 같다. + +1. OCI `instance-control`, `instance-worker-1`, `instance-worker-2`에 k3s cluster를 구성한다. +2. Home node에 management k3s와 Argo CD를 설치한다. +3. Home node의 Argo CD에 OCI cluster를 `oci-prod`로 등록한다. +4. root app을 적용해 `cert-manager`와 애플리케이션을 GitOps로 배포한다. +5. `youngwon.me` DNS를 OCI ingress public IP로 전환한다. + +주의: + +- `instance-control`에서는 `server` 설치만 실행한다. +- `instance-worker-1`, `instance-worker-2`에서만 `K3S_URL=...` join 명령을 실행한다. +- `--node-ip`는 현재 로그인한 노드의 private IP와 정확히 일치해야 한다. + +빠른 시작 명령: + +```bash +kubectl apply --server-side --force-conflicts -k bootstrap/home-mgmt/argocd-install +argocd cluster add --name oci-prod +kubectl apply -k bootstrap/home-mgmt/root-app ``` -docker --context {jenkins_was|jenkins_devops} run {yw7148/image} -``` \ No newline at end of file + +## Portfolio Deployment + +`portfolio`는 Docker Hub 이미지 `yw7148/portfolio`로 배포한다. + +- base manifests: `apps/portfolio/base/` +- production overlay: `apps/portfolio/overlays/prod/` +- prod ingress host/path: `youngwon.me/portfolio` +- 기본 태그 승격 workflow: `.github/workflows/promote-portfolio-image.yml` + +## Multi-Service Structure + +현재 구조는 `portfolio` 한 개만 들어가 있지만, 여러 서비스를 추가할 수 있게 나눠져 있다. + +- 서비스별 매니페스트: `apps//base/` +- 환경별 오버레이: `apps//overlays/prod/` +- Argo CD 앱 선언: `argocd/applications/-prod.yaml` +- 서비스별 namespace: `clusters/oci-prod/namespaces/.yaml` + +예시는 `apps/README.md`에 정리해두었다. + +민감한 값 주의: + +- 실제 `K3S_TOKEN`, Argo CD 비밀번호, kubeconfig, 운영용 공인 IP는 문서에 하드코딩하지 않는다. +- 설치 예시는 항상 ``, ``, `` 같은 placeholder를 사용한다. diff --git a/apps/README.md b/apps/README.md new file mode 100644 index 0000000..7806e37 --- /dev/null +++ b/apps/README.md @@ -0,0 +1,20 @@ +# Apps Layout + +현재 구조는 서비스별로 독립적인 Kubernetes 매니페스트를 추가할 수 있게 잡아두었다. + +패턴: + +```text +apps//base/ +apps//overlays/prod/ +argocd/applications/-prod.yaml +clusters/oci-prod/namespaces/.yaml +``` + +새 서비스를 추가할 때는 아래 순서를 따른다. + +1. `clusters/oci-prod/namespaces/` 아래에 namespace를 추가한다. +2. `apps//base/`에 Deployment, Service, Ingress를 추가한다. +3. `apps//overlays/prod/`에서 이미지 태그와 prod 설정을 덮어쓴다. +4. `argocd/applications/` 아래에 새 Application을 추가한다. +5. `argocd/kustomization.yaml`에 새 Application을 등록한다. diff --git a/apps/portfolio/base/deployment.yaml b/apps/portfolio/base/deployment.yaml new file mode 100644 index 0000000..1eca1d5 --- /dev/null +++ b/apps/portfolio/base/deployment.yaml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: portfolio + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/part-of: youngwon-platform +spec: + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: portfolio + template: + metadata: + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/part-of: youngwon-platform + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node-role.kubernetes.io/control-plane + operator: DoesNotExist + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: portfolio + topologyKey: kubernetes.io/hostname + containers: + - name: portfolio + image: yw7148/portfolio:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9001 + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 30 + periodSeconds: 20 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi diff --git a/apps/portfolio/base/ingress.yaml b/apps/portfolio/base/ingress.yaml new file mode 100644 index 0000000..a2a973c --- /dev/null +++ b/apps/portfolio/base/ingress.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: portfolio + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - hosts: + - youngwon.me + secretName: youngwon-me-tls + rules: + - host: youngwon.me + http: + paths: + - path: /portfolio + pathType: Prefix + backend: + service: + name: portfolio + port: + number: 80 diff --git a/apps/portfolio/base/kustomization.yaml b/apps/portfolio/base/kustomization.yaml new file mode 100644 index 0000000..1d4f62e --- /dev/null +++ b/apps/portfolio/base/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml + - service.yaml + - ingress.yaml diff --git a/apps/portfolio/base/service.yaml b/apps/portfolio/base/service.yaml new file mode 100644 index 0000000..adc3254 --- /dev/null +++ b/apps/portfolio/base/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: portfolio + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/part-of: youngwon-platform +spec: + selector: + app.kubernetes.io/name: portfolio + ports: + - name: http + port: 80 + targetPort: http diff --git a/apps/portfolio/overlays/prod/kustomization.yaml b/apps/portfolio/overlays/prod/kustomization.yaml new file mode 100644 index 0000000..c33f419 --- /dev/null +++ b/apps/portfolio/overlays/prod/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: portfolio + +resources: + - ../../base + +images: + - name: yw7148/portfolio + newTag: latest diff --git a/argocd/applications/oci-base.yaml b/argocd/applications/oci-base.yaml new file mode 100644 index 0000000..0638a1d --- /dev/null +++ b/argocd/applications/oci-base.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: oci-base + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "0" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: oci-platform + source: + repoURL: https://github.com/yw7148/server.git + targetRevision: HEAD + path: clusters/oci-prod + destination: + name: oci-prod + namespace: cert-manager + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/argocd/applications/portfolio-prod.yaml b/argocd/applications/portfolio-prod.yaml new file mode 100644 index 0000000..91347b2 --- /dev/null +++ b/argocd/applications/portfolio-prod.yaml @@ -0,0 +1,24 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: portfolio-prod + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "1" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: oci-platform + source: + repoURL: https://github.com/yw7148/server.git + targetRevision: HEAD + path: apps/portfolio/overlays/prod + destination: + name: oci-prod + namespace: portfolio + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/argocd/kustomization.yaml b/argocd/kustomization.yaml new file mode 100644 index 0000000..3e443bd --- /dev/null +++ b/argocd/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - projects/oci-platform.yaml + - applications/oci-base.yaml + - applications/portfolio-prod.yaml diff --git a/argocd/projects/oci-platform.yaml b/argocd/projects/oci-platform.yaml new file mode 100644 index 0000000..94cd15d --- /dev/null +++ b/argocd/projects/oci-platform.yaml @@ -0,0 +1,18 @@ +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: oci-platform + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-1" +spec: + description: OCI k3s platform and workloads managed from the Home node Argo CD instance. + sourceRepos: + - https://github.com/yw7148/server.git + - git@github.com:yw7148/server.git + destinations: + - server: '*' + namespace: '*' + clusterResourceWhitelist: + - group: '*' + kind: '*' diff --git a/bootstrap/home-mgmt/argocd-install/kustomization.yaml b/bootstrap/home-mgmt/argocd-install/kustomization.yaml new file mode 100644 index 0000000..85aec5d --- /dev/null +++ b/bootstrap/home-mgmt/argocd-install/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: argocd + +resources: + - namespace.yaml + - https://raw.githubusercontent.com/argoproj/argo-cd/v3.3.6/manifests/install.yaml diff --git a/bootstrap/home-mgmt/argocd-install/namespace.yaml b/bootstrap/home-mgmt/argocd-install/namespace.yaml new file mode 100644 index 0000000..a040f2b --- /dev/null +++ b/bootstrap/home-mgmt/argocd-install/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: argocd diff --git a/bootstrap/home-mgmt/root-app/kustomization.yaml b/bootstrap/home-mgmt/root-app/kustomization.yaml new file mode 100644 index 0000000..9a43af5 --- /dev/null +++ b/bootstrap/home-mgmt/root-app/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - server-root.yaml diff --git a/bootstrap/home-mgmt/root-app/server-root.yaml b/bootstrap/home-mgmt/root-app/server-root.yaml new file mode 100644 index 0000000..b2cbf58 --- /dev/null +++ b/bootstrap/home-mgmt/root-app/server-root.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: server-root + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: https://github.com/yw7148/server.git + targetRevision: HEAD + path: argocd + destination: + server: https://kubernetes.default.svc + namespace: argocd + syncPolicy: + automated: + prune: true + selfHeal: true diff --git a/clusters/oci-prod/cert-manager/clusterissuer-letsencrypt-prod.yaml b/clusters/oci-prod/cert-manager/clusterissuer-letsencrypt-prod.yaml new file mode 100644 index 0000000..0ddbe9f --- /dev/null +++ b/clusters/oci-prod/cert-manager/clusterissuer-letsencrypt-prod.yaml @@ -0,0 +1,14 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + email: ops@youngwon.me + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: letsencrypt-prod + solvers: + - http01: + ingress: + ingressClassName: traefik diff --git a/clusters/oci-prod/cert-manager/kustomization.yaml b/clusters/oci-prod/cert-manager/kustomization.yaml new file mode 100644 index 0000000..31545ce --- /dev/null +++ b/clusters/oci-prod/cert-manager/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - https://github.com/cert-manager/cert-manager/releases/download/v1.20.1/cert-manager.yaml + - clusterissuer-letsencrypt-prod.yaml diff --git a/clusters/oci-prod/kustomization.yaml b/clusters/oci-prod/kustomization.yaml new file mode 100644 index 0000000..14c104f --- /dev/null +++ b/clusters/oci-prod/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespaces + - cert-manager diff --git a/clusters/oci-prod/namespaces/kustomization.yaml b/clusters/oci-prod/namespaces/kustomization.yaml new file mode 100644 index 0000000..ed24f15 --- /dev/null +++ b/clusters/oci-prod/namespaces/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - portfolio.yaml diff --git a/clusters/oci-prod/namespaces/portfolio.yaml b/clusters/oci-prod/namespaces/portfolio.yaml new file mode 100644 index 0000000..f48ed69 --- /dev/null +++ b/clusters/oci-prod/namespaces/portfolio.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: portfolio + labels: + app.kubernetes.io/part-of: youngwon-platform + app.kubernetes.io/managed-by: argocd diff --git a/docs/k3s-argocd-migration-design.md b/docs/k3s-argocd-migration-design.md new file mode 100644 index 0000000..6c181e5 --- /dev/null +++ b/docs/k3s-argocd-migration-design.md @@ -0,0 +1,367 @@ +# k3s / Argo CD Migration Design + +## Summary + +이 문서는 현재 `Jenkins` + `Nginx` 중심 운영 구조를 `Home node`의 Argo CD 관리 클러스터와 `OCI`의 k3s 런타임 클러스터로 전환하기 위한 기준 설계 문서다. + +최종 목표는 다음과 같다. + +- Home node에 단일 노드 k3s management cluster를 두고 Argo CD를 설치한다. +- OCI의 `instance-control` 1대와 `instance-worker-1`, `instance-worker-2` 2대로 k3s workload cluster를 구성한다. +- 외부 노출은 OCI k3s의 Traefik ingress를 사용한다. +- TLS는 `cert-manager`와 ACME를 사용해 자동화한다. +- CI는 GitHub Actions로, CD는 Argo CD GitOps로 분리한다. +- 기존 `youngwon.me/portfolio` 경로는 초기 마이그레이션 동안 그대로 유지한다. + +## Current State + +현재 리포와 Terraform 기준으로 운영 상태는 다음과 같다. + +- Home node + - Jenkins가 Docker socket 바인딩 기반으로 로컬 컨테이너에서 동작한다. +- OCI + - `instance-control` + - `instance-worker-1` + - `instance-worker-2` + - 세 인스턴스 모두 같은 OCI subnet에 있으며 public IP가 할당되어 있다. +- Public edge + - Nginx가 `youngwon.me`의 TLS 종료를 수행한다. + - `/portfolio` 요청은 OCI의 별도 public endpoint `:9001`로 프록시된다. + +현재 구조의 한계는 다음과 같다. + +- Jenkins가 Home node의 Docker 환경에 강하게 결합되어 있다. +- Nginx와 실제 애플리케이션 런타임이 분리되어 있어 경로, 인증서, 백엔드 주소를 수동으로 맞춰야 한다. +- 배포 선언이 GitOps 형태로 정리되어 있지 않아 변경 이력과 목표 상태 추적이 어렵다. +- OCI 노드가 있어도 클러스터 관점의 일관된 운영 모델이 없다. + +## Goals + +- Argo CD를 중심으로 원하는 상태를 Git에 선언하고 자동 동기화한다. +- OCI 3노드를 하나의 k3s 클러스터로 운영한다. +- Home node는 관리 plane, OCI는 workload plane으로 역할을 분리한다. +- 기존 도메인과 서비스 경로를 크게 바꾸지 않고 무중단에 가깝게 전환한다. +- Jenkins 제거 이후에도 이미지 빌드부터 배포까지의 경로가 끊기지 않게 한다. + +## Non-Goals + +- 이번 설계는 multi-control-plane HA k3s까지 다루지 않는다. +- OCI Load Balancer 신규 도입은 이번 기본안에 포함하지 않는다. +- Secret manager 신규 도입은 이번 기본안에 포함하지 않는다. +- 모든 애플리케이션의 동시 이전은 목표가 아니다. 우선순위는 `portfolio`다. + +## Final Decisions + +- Argo CD는 Home node의 단일 노드 k3s management cluster 위에 둔다. +- OCI k3s cluster는 `instance-control` 1대의 control plane과 `instance-worker-1`, `instance-worker-2` 2대의 worker로 구성한다. +- 일반 애플리케이션 workload는 기본적으로 두 worker에 배치하고, `instance-control`은 control plane 역할을 우선한다. +- 이 `server` 리포를 GitOps source of truth로 사용한다. +- 외부 ingress는 k3s 기본 Traefik를 사용한다. +- TLS 발급과 갱신은 `cert-manager` + Let's Encrypt 기준으로 설계한다. +- Jenkins 대체 CI는 GitHub Actions로 한다. +- Argo CD UI는 기본적으로 public exposure 없이 private access를 원칙으로 한다. + +## Target Architecture + +```mermaid +flowchart TD + DEV[Developer / Operator] + GH[GitHub Repository
server] + GHA[GitHub Actions
Build + Push] + HOME[Home node
k3s management cluster] + ARGO[Argo CD] + OCICTL[instance-control
k3s server] + OCIW1[instance-worker-1
k3s agent] + OCIW2[instance-worker-2
k3s agent] + TRAEFIK[Traefik Ingress] + CERT[cert-manager] + APP[portfolio workload] + + DEV --> GH + GH --> GHA + GH --> ARGO + HOME --> ARGO + ARGO --> OCICTL + ARGO --> OCIW1 + ARGO --> OCIW2 + CERT --> TRAEFIK + TRAEFIK --> APP +``` + +## Node Roles + +### Home node + +- 단일 노드 k3s management cluster를 운영한다. +- Argo CD를 `argocd` namespace에 설치한다. +- OCI workload cluster를 remote cluster로 등록해 GitOps 컨트롤러 역할만 수행한다. +- 외부 서비스 트래픽의 public edge 역할은 맡지 않는다. + +### OCI `instance-control` + +- k3s server 역할을 맡는다. +- Kubernetes API endpoint의 기준 노드가 된다. +- 운영 원칙상 control plane 역할이 우선이며, 일반 workload는 가급적 worker에 배치한다. + +현재 Terraform 기준 사양: + +- Shape: `VM.Standard.A1.Flex` +- OCPU: `1` +- Memory: `6GB` +- Private IP: `10.0.0.195` + +### OCI `instance-worker-1` + +- k3s agent 역할을 맡는다. +- 일반 application workload의 기본 배치 대상이다. + +현재 Terraform 기준 사양: + +- Shape: `VM.Standard.A1.Flex` +- OCPU: `2` +- Memory: `9GB` +- Private IP: `10.0.0.149` + +### OCI `instance-worker-2` + +- k3s agent 역할을 맡는다. +- 일반 application workload의 두 번째 배치 대상이다. + +현재 Terraform 기준 사양: + +- Shape: `VM.Standard.A1.Flex` +- OCPU: `1` +- Memory: `9GB` +- Private IP: `10.0.0.81` + +## Networking, DNS, and Security + +### Traffic Model + +- 외부 사용자 요청은 `youngwon.me`를 통해 OCI k3s ingress로 직접 유입된다. +- 초기 기본안은 DNS를 `instance-control`의 public IP로 연결한다. +- 이후 필요 시 다중 A record 또는 OCI Load Balancer로 확장할 수 있다. +- `/portfolio` 경로는 Traefik ingress rule로 유지한다. + +### TLS + +- `cert-manager`가 Let's Encrypt certificate를 발급한다. +- `youngwon.me` 인증서는 Kubernetes secret으로 관리된다. +- HTTP-01 challenge를 기본값으로 두고 ingress class는 `traefik`를 사용한다. + +### Required Ports + +- Internet -> OCI ingress + - `80/tcp` + - `443/tcp` +- Home node -> OCI control plane + - `6443/tcp` +- OCI node internal communication + - k3s overlay 및 agent/server 통신에 필요한 내부 포트 허용 +- Admin access + - `22/tcp`는 운영자 IP 범위로 제한 + +### Argo CD Access + +- Argo CD UI는 public exposure를 하지 않는다. +- 접속은 Home node에서 직접 접근하거나 SSH tunnel 같은 private path를 사용한다. + +## GitOps Repository Model + +이 리포는 문서, 플랫폼 선언, 애플리케이션 선언을 같이 관리하는 단일 source of truth가 된다. + +예상 구조: + +```text +docs/ +bootstrap/ + home-mgmt/ +argocd/ + projects/ + applications/ +clusters/ + oci-prod/ +apps/ + portfolio/ +.github/ + workflows/ +``` + +역할 분리는 다음과 같다. + +- `docs/` + - 아키텍처, 운영 규칙, 마이그레이션 문서 +- `bootstrap/home-mgmt/` + - Home management cluster 초기 설치용 자산 +- `argocd/projects/` + - Argo CD AppProject +- `argocd/applications/` + - OCI cluster에 배포할 child application +- `clusters/oci-prod/` + - namespace, cluster issuer, 공통 platform manifest +- `apps/portfolio/` + - `portfolio` 배포 선언 +- `.github/workflows/` + - 이미지 빌드, 푸시, 배포용 manifest promotion workflow + +## CI/CD Operating Model + +### CI + +- GitHub Actions가 애플리케이션 이미지를 빌드한다. +- 빌드된 이미지는 Docker Hub `yw7148` namespace로 push한다. +- 이미지 태그는 immutable tag를 사용한다. +- 기본안은 Git SHA 또는 release tag 기반 tag를 사용한다. + +### CD + +- GitHub Actions는 배포 대상 manifest의 이미지 태그를 업데이트한다. +- Argo CD는 Git 변경을 감지해 OCI cluster와 동기화한다. +- v1에서는 Argo CD Image Updater 대신 GitHub Actions가 manifest promotion을 수행한다. + +이 방식의 장점은 다음과 같다. + +- 빌드 책임과 배포 책임이 분리된다. +- 실제 배포 상태가 항상 Git에 남는다. +- Jenkins의 Docker socket 의존성을 제거할 수 있다. + +## Bootstrap Sequence + +### 1. OCI Infrastructure Verification + +- `/home/youngwon/terraform` 리포 기준으로 `instance-control`, `instance-worker-1`, `instance-worker-2`가 정상 상태인지 확인한다. +- 세 노드 모두 SSH 접근과 내부 네트워크 통신이 가능한지 확인한다. + +### 2. Home Management Cluster Bootstrap + +- Home node에 k3s single-node cluster를 설치한다. +- Argo CD를 설치한다. +- Argo CD admin 초기 접근 경로와 비밀번호 변경 절차를 설정한다. + +### 3. OCI Workload Cluster Bootstrap + +- `instance-control`에 k3s server를 설치한다. +- `instance-worker-1`, `instance-worker-2`를 k3s agent로 join한다. +- control plane 노드에는 운영 원칙상 일반 workload를 최대한 배치하지 않도록 taint 또는 scheduling rule을 적용한다. + +### 4. Cluster Registration + +- Home node의 Argo CD에서 OCI cluster를 remote cluster로 등록한다. +- cluster name은 `oci-prod`를 기본값으로 사용한다. + +### 5. Base Platform Installation + +- OCI cluster에 아래 공통 자산을 배포한다. + - namespace + - `cert-manager` + - `ClusterIssuer` + - 공통 ingress policy + +### 6. First Workload Onboarding + +- `portfolio`를 첫 번째 GitOps-managed workload로 등록한다. +- `/portfolio` 경로와 TLS가 정상 동작하는지 확인한다. + +## Portfolio Migration Plan + +### Before Cutover + +- 현재 Nginx가 프록시하는 `portfolio` backend의 동작 포트와 health check 경로를 확인한다. +- OCI k3s cluster에 동일 이미지를 배포해 병렬 검증한다. +- Traefik ingress로 `/portfolio`가 서비스 가능한지 먼저 검증한다. + +### Cutover + +- `youngwon.me/portfolio`의 실제 ingress를 OCI k3s로 전환한다. +- 기존 Nginx 설정과 새 ingress를 일정 시간 병행 검증한다. +- 응답 코드, 정적 리소스 경로, HTTPS 동작을 확인한다. + +### After Cutover + +- 일정 기간 관찰 후 기존 Nginx의 `/portfolio` 프록시를 제거한다. +- Jenkins가 더 이상 배포 경로에 필요 없음을 확인한 뒤 종료한다. + +## Rollback Plan + +### Rollback Point 1 + +- Home node management cluster 구축 전 단계 +- 이 시점에서는 기존 Jenkins/Nginx 운영을 그대로 유지한다. + +### Rollback Point 2 + +- OCI k3s cluster bootstrap 완료 후 +- 아직 public cutover 전이면 ingress만 비활성화하고 기존 경로를 유지한다. + +### Rollback Point 3 + +- `portfolio` cutover 직후 +- 문제가 있으면 DNS 또는 ingress route를 기존 Nginx 경로로 즉시 되돌린다. + +### Rollback Point 4 + +- Jenkins/Nginx 종료 직전 +- 최소 1회 이상 정상 배포와 재배포를 GitHub Actions + Argo CD로 검증한 뒤에만 종료한다. + +## Operational Rules + +- Argo CD는 운영 기준 source of truth가 Git임을 전제로 한다. +- emergency hotfix도 가능하면 Git을 통해 반영한다. +- cluster 안에서 수동 변경이 발생하면 반드시 Git 선언으로 되돌려 정합성을 맞춘다. +- Home node와 OCI의 책임을 섞지 않는다. + - Home node는 management + - OCI는 runtime + +## Risks and Mitigations + +### Single Control Plane + +위험: + +- `instance-control` 장애 시 Kubernetes API 운영성이 떨어진다. + +대응: + +- 이번 단계에서는 단순성을 우선하고 single control plane을 유지한다. +- 이후 필요 시 multi-server k3s 또는 외부 datastore 구성을 검토한다. + +### Public Ingress Direct to OCI + +위험: + +- 초기에는 OCI Load Balancer 없이 단일 ingress 진입점에 의존한다. + +대응: + +- 먼저 구조 단순화를 우선한다. +- 서비스 증가 시 다중 A record 또는 OCI LB를 도입한다. + +### Secret Management + +위험: + +- 초기 bootstrap에서 수동 secret 관리가 남을 수 있다. + +대응: + +- Git에는 secret을 저장하지 않는다. +- 추후 SOPS 또는 Sealed Secrets 도입을 후속 과제로 둔다. + +## Acceptance Criteria + +아래 조건을 만족하면 이번 전환 설계가 유효한 것으로 본다. + +- 운영자가 Argo CD, OCI k3s, GitHub Actions의 역할 경계를 한 번에 설명할 수 있다. +- Home node와 OCI node 각각에 무엇이 올라가는지 모호하지 않다. +- `youngwon.me/portfolio`를 어떤 순서로 옮길지 절차가 분명하다. +- Jenkins 종료 전 필요한 검증 단계가 정의되어 있다. +- rollback 시점과 기준이 명확하다. + +## Assumptions + +- OCI 런타임 클러스터의 최종 노드는 `instance-control`, `instance-worker-1`, `instance-worker-2`다. +- `portfolio`는 OCI k3s 위로 먼저 이전할 대표 workload다. +- Docker Hub `yw7148` repository를 계속 사용할 수 있다. +- Domain은 계속 `youngwon.me`를 사용한다. +- Terraform 리포는 인프라 source of truth이고, 이 리포는 운영 선언과 GitOps source of truth 역할을 맡는다. + diff --git a/docs/oci-k3s-apply-guide.md b/docs/oci-k3s-apply-guide.md new file mode 100644 index 0000000..869c48d --- /dev/null +++ b/docs/oci-k3s-apply-guide.md @@ -0,0 +1,472 @@ +# OCI k3s Apply Guide + +## Summary + +이 문서는 현재 리포의 Kubernetes/Argo CD 설정을 실제 OCI 인스턴스와 Home node에 적용하는 절차를 정리한다. + +대상 노드: + +- Home node: Argo CD 관리 클러스터 +- OCI `instance-control`: k3s server +- OCI `instance-worker-1`: k3s agent +- OCI `instance-worker-2`: k3s agent + +## 1. OCI Security Rules + +OCI security list 또는 NSG에서 최소한 아래 포트를 허용한다. + +- Internet -> `instance-control` + - `80/tcp` + - `443/tcp` + - `6443/tcp` +- Operator IP -> all nodes + - `22/tcp` +- OCI private subnet internal + - all node-to-node traffic for k3s overlay and agent/server communication + +`6443/tcp`는 Home node의 Argo CD가 OCI 클러스터에 접근하기 위해 필요하다. + +권장: + +- `6443/tcp`는 전체 인터넷에 열지 말고 Home node의 공인 IP로만 제한한다. +- Home node에서 OCI kubeconfig가 가리키는 API 서버 주소는 실제로 Home node와 Argo CD pod가 모두 도달 가능한 주소여야 한다. + +## 2. Install k3s on OCI + +중요: + +- `instance-control`에서는 `server` 설치만 실행한다. +- `instance-worker-1`, `instance-worker-2`에서는 `agent` join만 실행한다. +- `--node-ip` 값은 "지금 접속해 있는 머신"에 실제로 붙어 있는 private IP와 반드시 같아야 한다. +- `K3S_URL`을 주면 설치 스크립트는 agent로 동작한다. 따라서 `instance-control`에서 `K3S_URL=...` 형태의 join 명령을 실행하면 안 된다. + +실행 전 빠른 확인: + +```bash +hostname +ip -4 addr show | grep "10.0.0." +``` + +예상 결과: + +- `instance-control`에서는 `10.0.0.195` +- `instance-worker-1`에서는 `10.0.0.149` +- `instance-worker-2`에서는 `10.0.0.81` + +### 2.1 Control Plane on `instance-control` + +`instance-control`에서 실행: + +```bash +curl -sfL https://get.k3s.io | \ + INSTALL_K3S_EXEC="server \ + --write-kubeconfig-mode 644 \ + --node-ip 10.0.0.195 \ + --advertise-address 10.0.0.195 \ + --tls-san " \ + sh - +``` + +토큰 확인: + +```bash +sudo cat /var/lib/rancher/k3s/server/node-token +``` + +### 2.2 Join `instance-worker-1` + +`instance-worker-1`에서 실행: + +먼저 control plane 연결 확인: + +```bash +curl -k https://10.0.0.195:6443/cacerts +``` + +여기서 CA 인증서가 내려오지 않으면 join 전에 아래를 먼저 확인한다. + +- `instance-control`에서 `sudo systemctl status k3s --no-pager -l` +- OCI security list / NSG 에서 worker -> control plane `6443/tcp` 허용 여부 +- OS firewall 에서 `6443/tcp` 허용 여부 + +정상 응답이 오면 join: + +```bash +curl -sfL https://get.k3s.io | \ + K3S_URL=https://10.0.0.195:6443 \ + K3S_TOKEN= \ + INSTALL_K3S_EXEC="agent --node-ip 10.0.0.149" \ + sh - +``` + +### 2.3 Join `instance-worker-2` + +`instance-worker-2`에서 실행: + +먼저 control plane 연결 확인: + +```bash +curl -k https://10.0.0.195:6443/cacerts +``` + +정상 응답이 오면 join: + +```bash +curl -sfL https://get.k3s.io | \ + K3S_URL=https://10.0.0.195:6443 \ + K3S_TOKEN= \ + INSTALL_K3S_EXEC="agent --node-ip 10.0.0.81" \ + sh - +``` + +### 2.4 Verify OCI Cluster + +`instance-control`에서 실행: + +```bash +sudo kubectl get nodes -o wide +``` + +정상 결과는 노드 3대가 모두 `Ready` 상태여야 한다. + +## 2.5 Troubleshooting Wrong-Node Join + +증상 예시: + +- `instance-control`에서 worker join 명령을 실행했다. +- `systemctl status k3s-agent`가 실패한다. +- `--node-ip 10.0.0.149` 같은 worker IP를 control plane 노드에서 사용했다. + +이 경우 원인은 대체로 다음 둘 중 하나다. + +- control plane 노드에 agent를 설치하려고 했다. +- 현재 노드에 없는 IP를 `--node-ip`로 넘겼다. + +복구 절차는 아래와 같다. + +### `instance-control`에서 agent를 잘못 실행한 경우 + +```bash +sudo /usr/local/bin/k3s-agent-uninstall.sh +sudo rm -f /etc/systemd/system/k3s-agent.service /etc/systemd/system/k3s-agent.service.env +sudo systemctl daemon-reload +``` + +그다음 control plane 명령만 다시 실행: + +```bash +curl -sfL https://get.k3s.io | \ + INSTALL_K3S_EXEC="server \ + --write-kubeconfig-mode 644 \ + --node-ip 10.0.0.195 \ + --advertise-address 10.0.0.195 \ + --tls-san " \ + sh - +``` + +확인: + +```bash +sudo systemctl status k3s --no-pager +sudo kubectl get nodes -o wide +sudo cat /var/lib/rancher/k3s/server/node-token +``` + +### worker에서 join이 꼬인 경우 + +```bash +sudo /usr/local/bin/k3s-agent-uninstall.sh +sudo rm -f /etc/systemd/system/k3s-agent.service /etc/systemd/system/k3s-agent.service.env +sudo rm -rf /etc/rancher/node /etc/rancher/k3s /var/lib/rancher/k3s +sudo systemctl daemon-reload +``` + +그다음 해당 worker의 올바른 private IP로 다시 join: + +```bash +curl -sfL https://get.k3s.io | \ + K3S_URL=https://10.0.0.195:6443 \ + K3S_TOKEN= \ + INSTALL_K3S_EXEC="agent --node-ip " \ + sh - +``` + +만약 이전에 같은 hostname으로 join 시도가 있었다면, control plane에서 기존 node 정보를 먼저 지우는 편이 안전하다. + +```bash +sudo /usr/local/bin/k3s kubectl delete node instance-worker-1 || true +sudo /usr/local/bin/k3s kubectl delete node instance-worker-2 || true +``` + +## 2.6 Worker Join Failure Checklist + +worker 로그에 아래 같은 메시지가 보이면: + +```text +Failed to validate connection to cluster at https://10.0.0.195:6443 +failed to get CA certs +Get "https://127.0.0.1:6444/cacerts": read tcp 127.0.0.1:*->127.0.0.1:6444: read: connection reset by peer +``` + +대개 원인은 아래 셋 중 하나다. + +- `instance-control`의 `k3s.service`가 실제로 떠 있지 않다. +- worker에서 `10.0.0.195:6443`에 접근할 수 없다. +- worker에 남아 있는 이전 join 상태가 새 시도와 충돌한다. + +확인 순서: + +1. `instance-control` + +```bash +sudo systemctl status k3s --no-pager -l +sudo ss -lntp | grep 6443 +sudo /usr/local/bin/k3s kubectl get nodes -o wide +``` + +2. worker + +```bash +curl -k https://10.0.0.195:6443/cacerts +sudo journalctl -u k3s-agent -n 100 --no-pager -l +``` + +3. Oracle Linux firewall 사용 중이면 `instance-control`에서: + +```bash +sudo firewall-cmd --permanent --add-port=6443/tcp +sudo firewall-cmd --permanent --zone=trusted --add-source=10.42.0.0/16 +sudo firewall-cmd --permanent --zone=trusted --add-source=10.43.0.0/16 +sudo firewall-cmd --reload +``` + +필요하면 worker 재join 전에 OS firewall도 꺼둘 수 있다. + +```bash +sudo systemctl disable firewalld --now +``` + +## 3. Prepare Kubeconfig for Home node + +Home node에서 OCI kubeconfig를 가져온다. + +```bash +mkdir -p ~/.kube +ssh opc@ "sudo cat /etc/rancher/k3s/k3s.yaml" > ~/.kube/oci-prod.yaml +sed -i "s/127.0.0.1//" ~/.kube/oci-prod.yaml +``` + +중요: + +- 여기서 ``는 Home node에서 실제로 접근 가능한 IP여야 한다. +- `argocd cluster add`는 이 kubeconfig의 server 주소로 직접 접속해 `kube-system`에 `argocd-manager` service account를 만든다. +- 따라서 Home node에서 아래 테스트가 먼저 성공해야 한다. + +```bash +curl -k https://:6443/cacerts +``` + +이 테스트가 실패하면 `argocd cluster add`도 실패한다. + +컨텍스트 이름을 정리한다. + +```bash +KUBECONFIG=~/.kube/oci-prod.yaml kubectl config rename-context default oci-prod +KUBECONFIG=~/.kube/oci-prod.yaml kubectl get nodes +``` + +## 4. Install Management Cluster on Home node + +Home node에서 관리용 k3s를 설치한다. + +```bash +curl -sfL https://get.k3s.io | \ + INSTALL_K3S_EXEC="server --write-kubeconfig-mode 644" \ + sh - +``` + +기본 kubeconfig 확인: + +```bash +sudo kubectl get nodes +``` + +Home node에서 사용할 `kubectl` 기본 컨텍스트는 management cluster로 두는 편이 안전하다. + +## 5. Install Argo CD on Home node + +리포 루트에서 실행: + +```bash +sudo kubectl apply --server-side --force-conflicts -k bootstrap/home-mgmt/argocd-install +sudo kubectl -n argocd get pods +``` + +중요: + +- Argo CD의 `ApplicationSet` CRD는 크기가 커서 일반 `kubectl apply`로는 `metadata.annotations: Too long` 오류가 날 수 있다. +- 이 경우 설치 명령은 반드시 `--server-side --force-conflicts` 옵션을 사용한다. + +초기 admin 비밀번호 확인: + +```bash +sudo kubectl -n argocd get secret argocd-initial-admin-secret \ + -o jsonpath="{.data.password}" | base64 -d && echo +``` + +필요 시 로컬 포트포워딩: + +```bash +sudo kubectl -n argocd port-forward svc/argocd-server 8080:443 +``` + +## 6. Register OCI Cluster to Argo CD + +Home node에 Argo CD CLI가 없다면 설치: + +```bash +VERSION=v3.3.6 +curl -sSL -o /tmp/argocd-linux-amd64 \ + https://github.com/argoproj/argo-cd/releases/download/${VERSION}/argocd-linux-amd64 +sudo install -m 0755 /tmp/argocd-linux-amd64 /usr/local/bin/argocd +``` + +Argo CD 로그인: + +```bash +argocd login localhost:8080 --username admin --password --insecure +``` + +OCI 클러스터 등록: + +```bash +KUBECONFIG=~/.kube/oci-prod.yaml argocd cluster add oci-prod --name oci-prod --yes +``` + +만약 아래처럼 timeout이 나면: + +```text +failed to create service account "argocd-manager" in namespace "kube-system" +dial tcp :6443: i/o timeout +``` + +아래를 순서대로 확인한다. + +1. Home node에서 API 서버 도달성 확인 + +```bash +curl -k https://:6443/cacerts +``` + +2. `instance-control`에서 API 서버 리스닝 확인 + +```bash +sudo systemctl status k3s --no-pager -l +sudo ss -lntp | grep 6443 +``` + +3. OCI security list / NSG에서 `6443/tcp`가 Home node 공인 IP에 대해 허용돼 있는지 확인 + +4. `instance-control`의 OS firewall에서 `6443/tcp` 허용 여부 확인 + +```bash +sudo systemctl status firewalld --no-pager +sudo firewall-cmd --list-ports +``` + +등록 확인: + +```bash +argocd cluster list +``` + +## 7. Apply GitOps Root Application + +리포 루트에서 실행: + +```bash +sudo kubectl apply -k bootstrap/home-mgmt/root-app +sudo kubectl -n argocd get applications +``` + +몇 분 뒤 아래 두 앱이 보여야 한다. + +- `oci-base` +- `portfolio-prod` + +동기화 강제 실행: + +```bash +argocd app sync server-root +argocd app sync oci-base +argocd app sync portfolio-prod +``` + +## 8. Verify OCI Runtime Resources + +OCI cluster 기준으로 확인: + +```bash +KUBECONFIG=~/.kube/oci-prod.yaml kubectl get ns +KUBECONFIG=~/.kube/oci-prod.yaml kubectl get pods -A +KUBECONFIG=~/.kube/oci-prod.yaml kubectl get ingress -A +KUBECONFIG=~/.kube/oci-prod.yaml kubectl -n portfolio get deploy,svc,pods +``` + +중점 확인 항목: + +- `cert-manager` pod 정상 기동 +- `portfolio` deployment replica 2개 정상 기동 +- `portfolio` ingress에 `youngwon.me`와 `/portfolio` rule 반영 + +## 9. DNS Cutover + +현재 `youngwon.me` DNS를 기존 Nginx가 아닌 OCI ingress 진입 public IP로 변경한다. + +기본안: + +- `A` record `youngwon.me` -> `` + +변경 후 확인: + +```bash +curl -I http://youngwon.me/portfolio +curl -I https://youngwon.me/portfolio +``` + +처음에는 cert-manager 인증서 발급까지 수 분이 걸릴 수 있다. + +## 10. Promote a New Portfolio Image + +기본 배포 이미지는 `apps/portfolio/overlays/prod/kustomization.yaml`에서 관리한다. + +새 이미지 태그 반영 방법: + +```bash +gh workflow run promote-portfolio-image.yml -f image_tag= +``` + +또는 직접 파일 수정: + +```bash +sed -i 's/^ newTag: .*/ newTag: /' apps/portfolio/overlays/prod/kustomization.yaml +git commit -am "chore(portfolio): promote image" +git push +``` + +push 후 Argo CD가 자동 동기화한다. + +## 11. Add More Services + +현재 구조는 `portfolio` 하나만 배포하지만, 여러 서비스가 추가될 수 있게 이미 분리돼 있다. + +새 서비스를 추가할 때 필요한 최소 경로는 아래다. + +```text +apps//base/ +apps//overlays/prod/ +argocd/applications/-prod.yaml +clusters/oci-prod/namespaces/.yaml +``` + +같은 패턴으로 서비스별 namespace, deployment, service, ingress, overlay를 독립적으로 늘릴 수 있다.