컴퓨터를 공부하다보면 정말 다양한 프레임워크와 툴들이 존재한다.
그런 것들을 잘 활용하면 많은 이점이 있지만 적응하기까지 조금 시간이 걸린다. Git과 GitHub도 그랬고, Spring Boot 도 그랬다.
이번 2024 오픈소스 컨트리뷰션 아카데미를 하면서 나는 ArgoCD에 대한 컨트리뷰션을 해볼 수 있게 되었다. ArgoCD를 이해하기 위해선 배경지식이 필요한데 그것이 Docker 와 Kubernetes 이다.
계속 공부해야지 해야지 하면서 미루다가 2주차 과제로 'Helm을 이용해서 k8s에 이미지 배포해보기' 가 나오면서 더이상 미룰 수 없게 되었다.
Docker 는 큐시즘 29기 백엔드로 활동하면서 사용해본적 있다. 하지만 개념도 모르고 그냥 Dockerfile 복붙해서 GitHub Action 으로 돌린 것이 다라서 이번에 좀 전체적인 환경을 이해해보고자 한다.
우선 이번 공부의 최종목적은 ArgoCD에 대한 이해이기 때문에 Docker 와 Kubernetes에 대해서 깊이 있게 공부하지는 않고 환경, 구성등만 이해할 것이다. Git의 코드를 뜯어보는 것이 아니라 환경 이해 정도로만 공부할 예정이다. 만약 나중에 DevOps나 SRE 분야에 대해 배울 기회가 생긴다면 그때 다시 세세히 뜯어 보려고 한다.
Docker 란
Docker 란 컨테이너를 관리하는 오픈 소스 툴이다.
컨테이너란 무엇일까?
먄약 내가 jvm이 깔려있지 않은 컴퓨터에서 java spring boot로 만들어진 프로그램을 배포해야한다면 나는 jvm을 다시 깔아야 한다. 또 다른 예시로 내가 윈도우 OS를 사용하고 있는데, 여기에 리눅스 환경에서 동작하는 어플리케이션 코드를 동작시켜야 한다고 하자.
그러면 환경 문제로 어플리케이션이 실행되지 않거나 원하는 대로 동작하지 않는 문제가 발생할 수 있다.
개발 환경은 정말 다양하고 무궁무진하기 때문에 이런 일은 비일비재하다. 만약 환경과 서비스를 독립시킬 수 있다면 개발 생산성이 많이 향상될 것이다.
그래서 처음에 나온 개념이 가상머신이다.
가상머신은 현재 내 OS위에 독립적인 환경을 하나 더 올리는 것이다. 만약 윈도우에서 리눅스를 사용한다면 가상머신을 설치해서 가상머신에서 리눅스를 설치하는 것이다. 시스템 프로그래밍에서 환경설정했던 것과 동일하다.
하지만 가상머신은 리소스를 많이 차지한다는 단점이 있다. 아무래도 기존 OS 위에 새로운 OS를 올리는 것이기 때문에 부담이 많이 된다. 더군다나 서로 다른 환경에서 동작하는 어플리케이션을 여러 대 동작시킨다고 하면 가상머신은 여러 개 필요하다.
Docker는 가상머신의 장점을 유지하면서도 단점을 해결할 수 있다. Docker는 컴퓨터에 Docker만 깔려있으면 Docker image를 빌드/실행할 수 있다. Docker image에는 실행될 환경, 버전과 함께 실행될 어플리케이션이 들어있다. 이 image를 Docker에 실행시키게 되면 별도의 환경을 구축하지 않아도 Docker 에서 image에 명시된 환경에 따라 image의 어플리케이션을 실행한다. 이렇게 실행된 어플리케이션의 프로세스 단위가 컨테이너인 것이다.
즉 Docker는 image를 생성/빌드하고, 컨테이너에 담아 실행하는 오픈소스 툴인 것이다.
Docker 사용하기
나는 맥이므로 맥 OS 기준으로 설명하겠다.
우선 Docker를 설치한다. 사용자가 Docker를 제어하는 것은 두 가지 방법이 있다. CLI 환경이랑 UI이다. 나는 CLI가 더 편하긴 하지만 일단 UI 인터페이스도 제공해주는 Docker desktop을 homebrew로 설치해주었다.
Docker에 컨테이너를 올려보기 위해서는 image가 있어야 한다. 우선 image를 생성해보자.
image는 내가 image로 만들고자 하는 프로젝트의 최상위 디렉토리에 명시된 Dockerfile 에 명시된 대로 생성된다. Dockerfile에서 사용하는 문법이나 언어는 따로 정리하지 않겠다. 각 행마다 Docker image의 Layer가 생성되므로 이를 줄이면 Docker container 생성시간을 줄일 수 있다고도 간략하게 들었다.
아래는 내가 이번 2주차 미션때 작성한 Dockerfile 이다.
# 빌드 단계
FROM openjdk:17-jdk-slim AS build
WORKDIR /kwon
COPY ./kwon .
RUN ./gradlew build
# 실행 단계
FROM openjdk:17-jdk-slim AS production
WORKDIR /app
COPY --from=build /kwon/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
이걸 작성하고 아래 명령어를 입력하면 Dockerfile 에 명시된 대로 Docker image가 생성된다.
docker build -t [이미지명] .
해당 이미지를 원격으로 전달하고 싶으면 DockerHub에 업로드해도 된다. (like GitHub) 이 이미지를 컨테이너에 담아서 실행하고 싶으면 아래 명령어를 실행하면 된다.
docker run [옵션] 이미지명[:태그] [인수] // image 실행
docker ps -a //실행 중인 docker image 목록을 볼 수 있는 명령어
docker logs -f {docker} // 실행 중인 docker image에서 console 내용을 확인할 수 있는 명령어
Kubernetes 란
Kubernetes란 (k8s라고도 함) container 실행/관리를 해주는 오픈소스 툴이다. 즉 이런 프로그램을 컨테이너 오케스트레이션 시스템이라고 부르는데 대표적으로 많이 쓰는 것이 Kubernetes인 것이다.
아니 그냥 docker run 해서 실행하면 되잖아?
실제 현업에서는 보통 서버를 하나만 사용하지 않는다. 여러 개를 띄워놓고 load balancing을 하거나, 장애, 무중단 배포를 위해 바꾸어가면서 서버를 돌리게 된다. 혹은 마이크로서비스 아키텍쳐를 사용하게 될 수도 있다. 여러 개의 복잡한 구조의 컨테이너들을 실행하는 것을 수동으로 진행하면 굉장히 복잡한 일이 될 것이다. k8s는 이를 자동화 시켜준다. 또 load balancing, 장애 대응, 무중단 배포 등을 손쉽게 할 수 있도록 지원해준다. (그래서 로고도 배에서 쓰는 조타기이다)
즉 Docker 는 컨테이너를 위한 기술, k8s는 컨테이너를 실행/관리 하는 도구라고 생각하면 좋다. 목적성에 따라 나뉜다.
Kubernetes는 선언적 API라는 장점이 있다.
내가 실행하고자 하는 컨테이너의 상태를 알려주면 동작원리를 모르지만 k8s는 그 상태를 유지시키기 위해 알아서 컨테이너를 동작시킨다. 개발자는 내가 원하는 상태를 명시하기만 하면 내부 동작 원리를 신경쓰지 않아도 k8s가 알아서 해준다는 점이다. 이게 k8s가 가지는 장점 중 하나이다.
Kubernetes 사용하기
k8s는 kubectl 이라는 별도의 CLI를 통해 명령을 내릴 수 있다.
명령의 구조는 다음과 같다.
kubectl [command] [자원 타입] [자원 이름] [flags]
k8s로 container를 실행시켜 보자.
k8s로 container를 실행시키는 방법은 크게 2가지가 있다.
1️⃣ CLI 명령어로 직접 실행하기 (kubectl run) 2️⃣ YAML 파일에 template로 지정해두고 YAML 파일 실행하기
1️⃣ CLI 명령어로 직접 실행하기 (kubectl run)
kubectl run [디폴로이먼트 이름] --image [실행할 이미지 이름] --port=[컨테이너가 쓸 포트번호]
위의 명령어로 k8s 클러스터에 명령을 하면 k8s는 도커 허브에서 이미지를 끌어와서 컨테이너로 지정된 포트를 설정하고 실행한다.
k8s는 deployments 라는 것이 있다. 이것은 pod를 생성하기 위한 틀 같은 느낌이다. 즉 하나의 deployment로 여러 개의 pod를 실행할 수 있다. 실제로 실행 중인 어플리케이션은 pod라는 형태로 실행되는 것이다. 위의 명령어는 deployment를 생성하는 명령어이고 디폴트로 pod는 하나가 생성된다. 현재 실행 중인 pod와 deployment를 확인하는 명령어는 각각 다음과 같다.
kubectl get pods //현재 실행 중인 pod 목록 출력
kubectl get deployments //현재 실행 중인 deployment 목록 출력
kubectl scale deploy [디폴로이먼트 이름] --replicas=2 //지정한 디폴로이먼트로 몇개의 pod를 생성할지를 정하는 명령어, replicas가 pod의 개수
kubectl delete deployment [디폴로이먼트 이름] //지정한 디폴로이먼트 삭제 --> 해당 디폴로이먼트로 생성한 pod들도 같이 삭제됨.
2️⃣ YAML 파일에 template로 지정해두고 YAML 파일 실행하기
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-app
labels:
app: nginx-app
spec:
replicas: 1
selector:
matchLabels:
app: nginx-app
template:
metadata:
labels:
app: nginx-app
spec:
containers:
- name: nginx-app
image: nginx
ports:
- containerPort: 80
kubectl apply -f nginx-app.yaml
위의 yaml 파일을 생성해두고 위의 명령어를 입력하면 kubenetes가 해당 yaml 파일을 보고 명시된 대로 컨테이너를 실행한다. 우리 과제에서는 Helm charts를 사용했다.
Cluster 외부에서 Cluster 접근하기
k8s는 내부 network가 외부와 분리되어서 외부에서 내부 pod로 올라간 어플리케이션에 접근하려면 k8s에서 제공하는 Service라는 것을 이용해야 한다.
k8s에서 제공하는 Service에는 4가지 type이 있다. 1️⃣ ClusterIP 2️⃣ NodePort 3️⃣ LoadBalancer 4️⃣ ExternalName
나는 가장 보편적인 NodePort를 사용하기로 했다. NodePort는 Service 하나에 모든 노드의 지정된 포트를 할당하는 방식이다. Service를 생성하고 목록을 확인하는 명령어는 아래와 같다.
kubectl expose deployment [외부로 노출시킬 컨테이너 이름] --type=[Service 타입]
OSSCA 2주차 과제
Docker 와 Kubernetes에 대한 적응은 여기까지 하도록 하자. 각각 책 한권 나올 정도로 양이 어마무시하고 엄청 방대하다. 현재 나의 목표는 ArgoCD 에 대한 코드를 기여해보는 것이다. 물론 ArgoCD에 대해서 깊은 기여는 이 정도의 이해로는 무리이다. 하지만 ArgoCD를 사용해볼 정도로만 하면 간단한 코드 기여를 할 수 있을 것 같다. 그렇게 코드 구조를 뜯어보다보면 깊이 있는 공부가 되지 않을까 싶다.
이제는 멘토님이 내주신 2주차 과제를 해보려고 한다.
나는 웹서비스를 java spring boot로 제작했다. 정말 간단하게 controller에서 바로 String 을 반환하는 형식으로 짰다. 그리고 나서 Dockerfile을 작성했다. Dockerfile도 직접 작성해본 건 처음이다. 아래는 작성한 Dockerfile 내용이다.
# 빌드 단계
FROM openjdk:17-jdk-slim AS build
WORKDIR /kwon
COPY ./kwon .
RUN ./gradlew build
# 실행 단계
FROM openjdk:17-jdk-slim AS production
WORKDIR /app
COPY --from=build /kwon/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
처음에 jar파일을 제대로 지정해주지 않아서 docker image가 제대로 생성되지 않아서 한 차례 고비가 있었다. 어디가 문제인지 몰라서 애꿎은 Helm charts만 만지작 거렸다. local에서 Docker image가 제대로 생성되는지 확인하지 못했던 것이 아쉽다. 결국엔 해결을 잘했다.
그 다음에는 k8s가 container를 실행할 수 있도록 YAML template를 작성해주었다. k8s가 container를 실행할 수 있도록 하는 YAML 구조에는 여러 툴들이 있는데 그중 대표적으로 많이 사용하는 것이 Helm charts이다. 이번 과제도 Helm 을 이용하는 것이므로 Helm을 썼다.
이건 deployment를 생성하는 yaml 파일이다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "charts.fullname" . }}
labels:
app: {{ .Release.Name }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "charts.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "charts.labels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.name }}"
ports:
- containerPort: {{ .Values.service.port }}
readinessProbe:
httpGet:
path: /healthcheck
port: 8080
livenessProbe:
httpGet:
path: /healthcheck
port: 8080
apiVersion: v1
kind: Service
metadata:
name: {{ include "charts.fullname" . }}
labels:
{{- include "charts.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.service.port }}
protocol: TCP
name: http
nodePort: {{ .Values.service.nodePort }}
selector:
{{- include "charts.selectorLabels" . | nindent 4 }}
각각 중요한 값이 비워져 있는데 중괄호 두 개 있는 곳 안에는 goLang으로 변수를 어디서 들고올지를 정하고 있다.
각 변수는 value.yml 파일에서 일괄적으로 관리하고 있다.
아래는 value.yml 파일이다. Helm charts를 default로 생성하면 기본적으로 주어지는 내용에 내가 사용하는 값들만 수정했다.
# Default values for charts.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: nginx
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
name: imscow11253/helm_test:latest
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Automatically mount a ServiceAccount's API credentials?
automount: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podLabels: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: NodePort
port: 8080
nodePort : 30080
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
livenessProbe:
httpGet:
path: /healthcheck
port: 8080
readinessProbe:
httpGet:
path: /healthcheck
port: 8080
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false
# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
nodeSelector: {}
tolerations: []
affinity: {}
이렇게 하니 과제를 수행하는 데에는 문제가 없었다.
Docker, Kubernetes, Helm을 사용만 해보았고 내부 동작 원리를 구체적으로 이해하지는 않았다. 이정도면 ArgoCD 프로젝트 구조를 이해하는 데에는 문제가 없을 것 같다. 일단 목표는 UI, CLI 부분에 대한 부분부터 기여를 하면서 내부 코드를 뜯어보며 프로젝트에 대한 이해를 높일 것이다. 그러다가 점차 어려운 Issue에 기여를 해보고 싶다.
일단은 첫 PR부터...
'오픈소스' 카테고리의 다른 글
GitHub PR (with DCO 봇, GPG 서명, Issue 연결) (0) | 2024.07.18 |
---|---|
2024 오픈소스 컨트리뷰션 아카데미 발대식 (1) | 2024.07.15 |
2024 오픈소스 컨트리뷰션 아카데미 (2) | 2024.07.15 |