들어가며
안녕하세요 여러분,
요즘 길거리에 캐럴이랑 트리가 많이 보여요! 연말 느낌이 물씬 나네요.
한 해를 마무리하면서, 스터디도 열심히 하고 있는 Devlos입니다.
이번 포스팅은 CloudNet@ 커뮤니티에서 주최하는 CI/CD 스터디 2주 차 주제인 "Github Action"에 대해서 정리한 내용입니다.
아참! 지난주의 성과는.. 제가 올해 목표였던 kubestronaut가 되었습니다! (조만간 해당 내용에 대한 포스팅도 추가할 예정입니다.)
Github Action 개념
Github Action은 빌드, 테스트 및 배포 파이프라인을 자동화할 수 있는 CI/CD 플랫폼입니다.
공식 설명서 - 링크
Repository에서 이벤트 발행 시 Github의 가상 OS 환경에서 Workflow가 실행됩니다.
출처 - https://docs.github.com/ko/actions/writing-workflows/about-workflows
Github Action은. github/workflow/경로에 있는 yaml 파일을 읽어서 동작합니다.
Github Action을 구성하는 주요 개념은 다음과 같습니다.
개념 | 설명 |
Workflow | - 하나이상의 작업을 실행하는 구성 가능한 자동화 프로세스입니다. - 이벤트 또는 일정에 의해 실행(trigger)됩니다. |
Event | - Workflow 실행을 trigger 하는 소스코드 Repository의 특정 활동을 의미합니다. |
Job | - Job은 함께 동일한 Runner 위에서 실행되는 워크플로우 일련의 단계입니다. 여기서 단계란 쉘 스크립트 또는 Action을 통해 실행될 내용을 의미합니다. - 각 단계가 동일한 Runner에서 실행되므로 단계별 데이터 공유가 가능합니다. |
Action | - 복잡하지만 자주 반복되는 작업을 수행하는 GitHub Actions 플랫폼 용 사용자 지정 애플리케이션입니다. |
Runner | - Workflow를 실행하는 서버를 의미합니다. |
Github Action Workflow의 핵심 문법
Github Action workflow의 핵심 문법은 다음과 같습니다.
name (Optional) - Github Repository의 [작업] 탭에 표시되는 Workflow 이름
name: learn-github-actions
run-name (Optional) - 저장소의 [작업] 탭에 있는 Workflow 실행 목록에 표시
run-name: ${{ github.action }} is learning GitHub Actions
on- Workflow의 트리거 지정
on: [push]
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
jobs - Workflow 실행 작업들을 모두 그룹화
build-and-test:
...
slack-failure-notification:
...
runs-on - 실행기의 종류를 설정
runs-on: ubuntu-latest
steps - Job에서 실행되는 모든 단계를 함께 그룹화
steps:
- name: Source code checkout
- name: Set up JDK 11
...
uses - runner에서 Repository를 확인하여 스크립트나 빌드 및 테스트 도구를 설치/실행하는 작업
- uses: actions/checkout@v3
with - 도구를 설치할 때 전달할 파라미터를 정의
- uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'corretto'
...
-
- Corretto : Amazon Corretto는 아마존 웹 서비스(AWS)에서 제공하는 무료로 배포되는 다중 플랫폼, 프로덕션-레디 Java 개발 키트(JDK) Corretto는 OpenJDK를 기반으로 만들어져 있으며, 아마존에서 직접 사용하고 있는 애플리케이션에 대한 지원과 확장성을 포함 Corretto를 사용하면, Java 애플리케이션을 더욱 안정적으로 실행하고 관리할 수 있음
run - runner에서 명령을 실행하도록 지시하는 용도
- run: npm install -g bats
실습 환경 구성
AWS Cloudformation을 통해 EC2 인스턴스 1대를 생성하여 자주 사용하는 포트(TCP 22, 80)를 보안 그룹을 통해 오픈합니다.
다음으로는 실습을 위한 Python 서버 한대를 실행시키도록 합니다.
CI/CD 실습용 AWS CloudFormation - 링크
AWS EC2에 접속한 후 ssh ubuntu@<EC2 Public PI> 로 접속합니다.
python3 -V
#
cat > server.py <<EOF
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from datetime import datetime
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
now = datetime.now()
response_string = now.strftime("The time is %-I:%M:%S %p, CloudNeta Study.\n")
self.wfile.write(bytes(response_string, "utf-8"))
def startServer():
try:
server = ThreadingHTTPServer(('', 80), RequestHandler)
print("Listening on " + ":".join(map(str, server.server_address)))
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()
if __name__== "__main__":
startServer()
EOF
#
sudo python3 server.py
## 아래 확인 후
CTRL+C 로 실행 취소
# (신규터미널) 서버1 SSH 접속
curl localhost
sudo ss -tnlp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 5 0.0.0.0:80 0.0.0.0:* users:(("python3",pid=3065,fd=3))
서버에 접속하여 서버를 실행한 후 상태를 확인합니다.
이 방식은 우리가 고전적으로 애플리케이션을 배포하는 방법입니다.
1. 직접 개발 후 실행
이제 이러한 방식을 Github repository를 기반으로 Github Action 파이프라인을 통해 자동으로 처리하는 구성을 시작합니다.
시작 이전에 git 원격접속을 위해 토큰을 발급합니다.
repository에 대한 접근권한과 워크플로우를 업데이트할 수 있는 권한으로 토큰을 발급받습니다.
다음으로는 Action을 통해 작업할 새 프라이빗 레포지토리를 구성합니다.
구성을 완료하면 다음과 같이 리포지토리가 생성됩니다.
이제 이 레포지토리를 이용하여 고전적인 서버 배포작업을 수행해 봅니다.
먼저 repository에서 소스코드를 다운로드합니다.
#
GITUSER=<>
GITUSER=gasida
git clone https://github.com/$GITUSER/cicd-2w.git
tree cicd-2w/
cp server.py cicd-2w/
cd cicd-2w/
#
git status
git add .
git commit -m "first commit"
git push origin main
Username for 'https://github.com': <>
Password for 'https://gasida@github.com': <>
다운로드한 코드를 실행해 봅니다.
. gitignore가 설치되어 있으므로 로그파일등을 생성한 것은 리포지토리에 올려지지 않습니다.
다음으로 소스코드를 수정하여 다시 실행해 봅니다.
다음으로는 원격환경에서 수정한 코드를 push 하여 버전 관리를 수행합니다.
다음과 같이 코드를 수정한 내용이 깃허브에 업로드됩니다.
원격 서버에서 수행하긴 했지만, 소스 코드 수정 및 배포를 수행하려면 위에서 설명한 과정들을 매번 반복해야 합니다. 또한 이미지 빌드 및 단위 테스트 등을 수행하려면 엄청난 반복과정이 필요합니다.
따라서 CI/CD 파이프라인을 구축하고, 일련의 과정들을 자동화하는 접근이 매우 필요합니다.
2. Github Actions
다음으로는 Github Action을 통해 자동화 파이프라인을 구축합니다. 먼저 Github Action에서 원격 서버를 접속할 수 있도록 하기 위해 서버 접근 SSH Key와 원격서버 IP를 Github Action에서 제공하는 Secret 기능을 사용하여 등록합니다.
다음으로는 해당 workflow를 리포지토리에 추가합니다.
workflow는 .github/workflows안에 yaml 파일을 추가하여 세팅합니다.
name: CICD1
on:
workflow_dispatch:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Configure the SSH Private Key Secret #원격 서버 접근을 위한 ssh key 파일 생성
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
- name: Set Strict Host Key Checking #Host key checking을 no 로 설정합니다.
run: echo "StrictHostKeyChecking=no" > ~/.ssh/config
- name: Git Pull #Github Secret으로 저장된 서버 접근정보를 가져와 ssh 연결을 수행합니다.
run: |
export MY_HOST="${{ secrets.EC2_PIP }}"
ssh ubuntu@$MY_HOST << EOF
cd /home/ubuntu/cicd-2w || exit 1
git pull origin main || exit 1
EOF
- name: Run service #서버 실행 명령을 수행합니다.
run: |
export MY_HOST="${{ secrets.EC2_PIP }}"
ssh ubuntu@$MY_HOST sudo fuser -k -n tcp 80 || true
ssh ubuntu@$MY_HOST "nohup sudo -E python3 /home/ubuntu/cicd-2w/server.py > /home/ubuntu/cicd-2w/server.log 2>&1 &"
git add . && git commit -m "add workflow" && git push origin main
해당 스크립트를 업로드하면 다음과 같이 레포지토리에 workflow 파일이 추가되고 실행이 됩니다.
서버에 접속하면 접속로그들이 표출됩니다.
ssh키 세팅은 secret으로 설정되어 있기 때문에 ***로 보입니다.
Host Key Checking을 해제하는 부분입니다.
다음으로 gothub에서 소스코드를 pull 해옵니다.
마지막으로, Host 정보를 받아오고 서비스를 실행합니다.
다음과 같이 서버가 정상적으로 배포된 것을 확인할 수 있습니다.
3. Github Action 2
이번 실습의 목표는 Github Action에서 코드를 빌드하고 테스트한 후 대상서버에 전달한 후 실행하는 것입니다.
다음과 같은 수순으로 진행됩니다.
- GitHub Actions에서 코드 가져오기
- GitHub Actions에서 .gitignore 제외된 민감 파일 내용을 을 안전하게 가져와서 사용하기 ⇒ 매번 수동으로 가져오기 불편하다!
- scp로 대상 서버 ec2에 py 파일 전송
- 대상 서버 ec2에 기존 서비스 중지하고 다시 실행
먼저 가볍게 workflow를 통해 Action에서 기동 중인 파이썬 버전을 확인해 봅니다.
#deploy.yaml 업데이트
name: CICD2
on:
workflow_dispatch:
push:
branches:
- main
jobs:
deployfinal:
runs-on: ubuntu-latest
steps:
- name: Test
run: |
python -V || true
python3 -V || true
which python || true
which python3 || true
env
git add . && git commit -m "echo env" && git push origin main
다음으로는 서버 설정에 필요한 정보를. env로 리포지토리에 올리려고 시도합니다.
Gitignore에 . env 파일들은 기본적으로 commit대상에서 제외되도록 설정되어 있기 때문에 수동으로 .env를 만들더라도 commit 대상임을 인지하지 못합니다. 물론 이러한 설정을 .gitignore에서 해제 할 수 있지만, 보안상의 이유로 권고 하지 않습니다.
grep env .gitignore
#
cat > .env <<EOF
ACCESSKEY : 1234
SECRETKEY : 5678
EOF
#
git add .env
git status
rm -f .env
.env 대신 민감정보를 Action에서 Secret을 통해 만듭니다. 예제에서는 MYKEYS라는 Secret을 생성하여 사용했습니다.
다음으로 마켓플레이스에서 ssh 접근을 용이하게 해주는 SSH Remote Command Action을 살펴보았습니다.
Github Action에서는 Jenkins처럼 스크립트로 처리하기 쉽지 않은 부분을 사용자들이 만들어서 익스텐션을 지원합니다.
SSH 접속을 쉽게 해주는 익스텐션인 SSH Remote Commands입니다.
도큐먼트를 확인해 보면 steps 단계에서 에서 uses를 통해 마켓플레이스에 등록된 해당 Action을 사용할 수 있도록 설정하는 내용과, 멀티라인 명령어를 사용할 수 있는 예제를 확인할 수 있습니다.
다음과 같이 시크릿 환경변수 주입이 필요할 때는 env: 블록에서 ${{ }} 문법을 통해 사용합니다.
Github Action 서버에서 스크립트를 실행하여 파일을 사용하도록 구성합니다.
name: CICD2
on:
workflow_dispatch:
push:
branches:
- main
jobs:
ssh-deploy:
runs-on: ubuntu-latest
steps:
- name: Github Repository Checkout
uses: actions/checkout@v4
- name: executing remote ssh commands
uses: appleboy/ssh-action@v1.2.0
env:
AWS_KEYS: ${{ secrets.MYKEYS }}
with:
host: ${{ secrets.EC2_PIP }}
username: ubuntu
key: ${{ secrets.SSH_PRIVATE_KEY }}
envs: AWS_KEYS
script_stop: true
script: |
cd /home/ubuntu/cicd-2w
echo "$AWS_KEYS" > .env
로컬에서 업로드하지 않아도, Action의 Secret을 활용하여 민감한 정보가 자동으로 대상서버에 배포되는 것을 확인할 수 있습니다.
만약 Secret 정보를 수정하여 다시 Action을 실행하면 다음과 같이 자동으로 배포됩니다.
다음으로는 파일을 원격으로 전달하는 SCP Files라는 Action을 사용했습니다.
파이프라인을 다음과 같이 수정합니다.
name: CICD2
on:
workflow_dispatch:
push:
branches:
- main
jobs:
scp-ssh-deploy:
runs-on: ubuntu-latest
steps:
- name: Github Repository Checkout # 소스코드 체크아웃
uses: actions/checkout@v4
- name: executing remote ssh commands # ssh 커맨드 실행
uses: appleboy/ssh-action@v1.2.0
env:
AWS_KEYS: ${{ secrets.MYKEYS }} #시크릿 주입
with:
host: ${{ secrets.EC2_PIP }}
username: ubuntu
key: ${{ secrets.SSH_PRIVATE_KEY }}
envs: AWS_KEYS
script_stop: true
script: | #스크립트 실행
cd /home/ubuntu/cicd-2w
echo "$AWS_KEYS" > .env
sudo fuser -k -n tcp 80 || true
- name: copy file via ssh # server.py 파일을 원격지의 /home/ubuntu/cicd-2w로 복사합니다.
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_PIP }}
username: ubuntu
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: server.py
target: /home/ubuntu/cicd-2w
Workflow 가 실행되면 최신 코드가 체크아웃되고, 디렉터리 와서 기존 실행 서버를 중지시킵니다. 다음으로는 원격서버로 파일을 복사한 다음 대상 디렉터리로 전달합니다.
이를 정리하여 대상 원격지 서버로 서버파일을 복사하고, ssh-action을 통해 실행 명령어를 수행하도록 합니다.
name: CICD2
on:
workflow_dispatch:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Github Repository Checkout
uses: actions/checkout@v4
- name: copy file via ssh
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_PIP }}
username: ubuntu
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: server.py
target: /home/ubuntu
- name: executing remote ssh commands
uses: appleboy/ssh-action@v1.2.0
env:
AWS_KEYS: ${{ secrets.MYKEYS }}
with:
host: ${{ secrets.EC2_PIP }}
username: ubuntu
key: ${{ secrets.SSH_PRIVATE_KEY }}
envs: AWS_KEYS
script_stop: true
script: |
cd /home/ubuntu/cicd-2w
echo "$AWS_KEYS" > .env
sudo fuser -k -n tcp 80 || true
rm server.py
cp /home/ubuntu/server.py ./
nohup sudo -E python3 /home/ubuntu/cicd-2w/server.py > /home/ubuntu/cicd-2w/server.log 2>&1 &
echo "test" >> /home/ubuntu/text.txt
파일을 복사하고, /home/ubuntu 파일에 복사한 후, 기존 서버파일을 죽이고, 홈디렉터리에 있는 서버 파일을 작업파일로 옮긴 후 기동시 킵니다.
정상적으로 실행이 되긴 하는데, 반복적으로 샐 행 되게 되면 다음과 같은 문제점이 발생합니다.
bash script 실행방식이므로, 실행한 이력들이 선언한 형태로 동작하지 않고, 실행되는 결과들이 중복으로 적용이 되게 됩니다.
예제에서는 test 에코를 매번 하나만 저장되도록 하려 했으나, 파이프라인 동작시마다 해당 내용이 중복되어 다수의 test 로그가 찍히도록 연출했습니다.
항상 실행할 때마다 내가 원하는 상태를 유지하고 싶지만, 안 되는 한계가 있는 것입니다.
하지만,
파이프라인 구성을 통해 다음과 같이 배포할 원격지 정보만 변경시켜 주면 구성한 명령 순서대로 쉽게 배포는 가능합니다.
아래의 그림은 기존 원격지(43.203.238.142)에서 사용한 파이프라인을 다른 원격지(52.78.33.36)에 동일한 파이프라인을 적용하여 배포 작업을 쉽게 간소화한 것을 나타냅니다.
4. GitHub Actions with Ansible
마지막으로, 위의 스크립트형 파이프라인의 단점을 극복하기 위해 선언형 인프라 관리를 하는 Ansible을 통한 파이프라인 구성을 실습했습니다.
실습은 파이썬 3.8이라는 특정 파이썬 버전을 설치하고, 앤서블 설치를 한 다음, 앤서블 기본 환경설정을 적용한 다음,
워킹 디렉터리 잡아주고 원격지 정보를 세팅합니다.
이렇게 되면 인벤토리파일에 선언한 내용을 따라 모든 서버에 핑체크를 수행하는 것을 확인할 수 있습니다.
5. 회사 파이프라인 소개
이번 스터디에 배운 Github Action은 제가 회사의 Automation에 사용한 툴이기도 합니다.
현재 같이 스터디를 같이 진행하고 있는 피터님과 개선을 하긴 했지만, 전체적인 골자는 이렇습니다.
- Push new source code: 개발자는 새로운 소스 코드를 버전 관리 시스템에 푸시합니다. 일반적으로 이는 Git과 같은 분산 버전 제어 시스템에서 수행됩니다.
- Notify on push events with new source code: 새로운 소스 코드가 푸시되면, CI/CD 툴은 푸시 이벤트를 감지하고 자동화된 빌드 및 테스트 프로세스를 시작합니다.
- Setup base OS for build: 일반적으로 가상 머신 또는 Docker 컨테이너와 같은 격리된 환경에서 빌드가 수행됩니다. 이 단계에서는 빌드를 위한 기본 운영체제를 설정합니다.
- Set up repository permission: 소스 코드 저장소에 대한 적절한 접근 권한을 설정합니다. 이는 빌드 프로세스가 소스 코드에 접근하고 변경 사항을 가져올 수 있게 합니다.
- Check out source code: 빌드 서버는 최신의 소스 코드를 체크아웃하여 빌드를 수행합니다.
- Set up JDK: Java Development Kit(JDK)을 설정하여 Java 애플리케이션을 빌드합니다.
- Set up Gradle permission: 빌드 도구인 Gradle에 대한 적절한 권한을 설정합니다. 이를 통해 빌드 프로세스는 필요한 의존성을 가져오고 빌드 작업을 수행할 수 있습니다.
- Test new updated source code: 새로 업데이트된 소스 코드를 테스트합니다. 이는 단위 테스트, 통합 테스트 등 다양한 테스트를 포함할 수 있습니다.
- Build & push new image: 테스트가 통과되면, CI/CD 도구는 새로운 Docker 이미지를 빌드하고, Docker 레지스트리에 푸시합니다.
- Check out deployment source code: 배포를 위한 소스 코드(예: Kubernetes 매니페스트 파일)를 체크아웃합니다.
- Set up kustomize: Kustomize는 Kubernetes 리소스를 커스터마이징 하는 도구입니다. 이를 설정하여 배포 매니페스트를 업데이트합니다.
- Update deployment source code: 배포 코드를 새로운 이미지 태그로 업데이트합니다.
- Pull new image: 배포 대상 환경(예: Kubernetes 클러스터)은 새로운 Docker 이미지를 Docker 레지스트리에서 가져옵니다.
- Deploy new image: 마지막으로, 변경된 배포 매니페스트를 사용하여 새 이미지를 배포합니다.
제가 진행하는 프로젝트는 MSA 기반이지만, 각 서비스의 기능들이 많지 않아서 하나의 리포지토리에서 모든 마이크로서비스를 관리하고 있습니다. (저희 팀에 개발자가 많지 않아요 ㅠㅠ)
그래서 한 리포지토리에서 push가 일어났을 때 특정 경로를 통해 이미지를 다르게 빌드하고, kustomize deploy.yaml에 업데이트하는 전략을 사용하고 있습니다.
코드 예시는 다음과 같습니다.
다음과 같이 select-target-service라는 job을 실행시켜 타깃이 되는 서비스 명과 ecr-repository 정보를 outputs으로 세팅합니다.
jobs:
select-target-service:
runs-on: ubuntu-20.04
outputs:
ecr-repository: ${{steps.select-target.outputs.ecr-repository }}
service-name: ${{steps.select-target.outputs.service-name }}
steps:
- name: Checkout source code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Select target
id: select-target
run: |
# Collection Service
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep 'services/collection'
then
echo "ecr-repository=malaysia-collection" >> $GITHUB_OUTPUT
echo "service-name=collection" >> $GITHUB_OUTPUT
# System Service
elif git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep 'services/system'
then
echo "ecr-repository=malaysia-system" >> $GITHUB_OUTPUT
echo "service-name=system" >> $GITHUB_OUTPUT
# Master Info Service
elif git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep 'services/master-info'
then
echo "ecr-repository=malaysia-master-info" >> $GITHUB_OUTPUT
echo "service-name=master-info" >> $GITHUB_OUTPUT
# System Service
elif git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep 'services/waterlevel-image-analyse'
then
echo "ecr-repository=malaysia-waterlevel-image-analyse" >> $GITHUB_OUTPUT
echo "service-name=waterlevel-image-analyse" >> $GITHUB_OUTPUT
# Exception | Unregistered service
else
echo "[Winitech] The unregistered service has changed."
exit 1
fi
echo "[Winitech] service has changed."
다음으로는 output에 정의된 정보를 이용하여 build 및 push를 수행합니다.
build-and-push-image:
needs: select-target-service
runs-on: ubuntu-20.04
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-southeast-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: 17
- name: Grant execute permission for gradlew
run: cd services/${{ needs.select-target-service.outputs.service-name }} && chmod +x gradlew
- name: Build and Push with Gradle
id: build-and-push-to-ncr
run: |
cd services/${{ needs.select-target-service.outputs.service-name }} && ./gradlew jib -x test --image $ECR_REGISTRY/${{ needs.select-target-service.outputs.ecr-repository }}:$IMAGE_TAG
다른 분들은 어떻게 하시는지 궁금하네요.. (해당 프로젝트 규모가 작아서 이런 방법을 사용하는 것일까요? ㅎㅎ)
마치며
이번 시간에는 Github Action을 이용하여 파이프라인을 구성해 보는 스터디를 해보았습니다. 저는 Github Action을 쿠버네티스기반 MSA를 배포하기 위한 목적으로 사용하여 해당 용도에만 관점을 두고 있었던 것 같습니다. 실습을 통해 다양한 활용법과 새로운 영감이 떠오르게 되네요!
이상으로 이번 스터디 정리를 마치도록 하겠습니다.
긴 글 읽어주셔서 감사합니다 :)
마지막 스터디 정리로 뵙겠습니다 :)
감사합니다.
'클라우드 컴퓨팅 & NoSQL > [CICD] CICD 맛보기 스터디' 카테고리의 다른 글
[3주차 - Jenkins CI/ArgoCD + K8S] CI/CD 스터디 (24.12.15) (0) | 2024.12.22 |
---|---|
[1주차] CI/CD 스터디 (24.12.01) (2) | 2024.12.08 |