애플리케이션을 개발하는 작업은, 시간이 오래 걸리고 힘들다.
빌드, 테스트, 배포와 같은 작업은 개발이 아니면서도 매우 힘들고 시간이 드는데,
CI/CD 파이프라인을 통해서 이러한 프로세스를 자동화하여 개발자가 개발에만 집중할 수 있도록 해준다.
또한, 빌드, 테스트, 배포의 과정 속에서 안정성, 자동화, 효율성을 극대화시킬 수 있다.
지속적인 통합(CI)는 공유된 리포지토리에 코드를 자주 커밋하는 것을 요구하는 관행이다.
코드 커밋을 자주 하는 것은 오류를 더 빨리 감지하고 오류의 근원을 찾을 때 디버그해야 하는 코드의 양을 줄여준다.
빈번한 코드 업데이트는 또한 소프트웨어 개발팀의 다른 구성원의 변경 사항을 더 쉽게 병합할 수 있게 해준다.
이는 코드 작성에 더 많은 시간을 할애하고, 오류 디버깅이나 병합 충돌 해결에 더 적은 시간을 할애할 수 있는 개발자에게 적합하다.
지속적인 제공(또는 배포)는 자동화된 테스트를 통과한 후 코드 변경 사항을 다양한 환경에 자동으로 제공하여 CI 프로세스를 확장한다.
지속적 제공은 변경 사항을 프로덕션에 자동으로 푸시하지 않으나, 변경 사항이 사용자에게 배포되기 전에 일종의 수동 개입에 의존하는 통제된 릴리스 프로세스를 제공한다.
소프트웨어는 항상 배포 가능한 상태로 유지되므로, 언제든지 릴리스할 수 있다.
지속적 배포는 수동 개입 없이 자동화된 테스트를 통과하는 즉시 코드 변경 사항을 프로덕션에 자동으로 배포하여 자동화 수준을 더욱 높인다.
아래는 오늘 만들어볼 CI/CD 파이프라인의 다이어그램이다.

.env.vault에 올린다.이 글에서는 다루지 않지만, 프로젝트에서 구현해놔서 다음 글들에서 파이프라인에 추가할 것들은 다음과 같다:
아래는 이 프로젝트에서 다루지 않는 것들이다.
Elastic Compute Cloud(EC2)는 Amazon에서 제공하는 유연하게 컴퓨팅 자원을 사용하게 해주는 클라우드 서비스이다.
Amazon Linux와 Ubuntu를 포함한 다양한 리눅스, Widows, macOS 운영체제를 지원한다.
인스턴스는 EC2 서비스의 클라우드 가상 서버의 단위이다.
AWS에 로그인 한 후, 새 인스턴스 추가를 눌러준다. 그리고 이름을 작성한다.
사용 목적과 선호에 맞는 OS 이미지를 사용하면 되는데, 나는 Ubuntu로 골랐다.
인스턴스 유형은 t2.micro가 대표적인 프리 티어의 인스턴스 유형으로 쓰인다.
키 페어를 이용하여 인스턴스에 안전하게 연결할 수 있는데, 우측의[새 키 페어 생성]을 눌러서 캐를 발급받고 입력하자. 생성을 누르면 현재 컴퓨터에서 키 파일을 받게 된다.
Windows의 경우 PuTTY를 이용할 것이므로, .ppk로 받으면 되고,
MacOS와 Linux는 SSH를 이용하므로 .pem을 받으면 된다.
보안 그룹은 인스턴스에 대한 방화벽 규칙세트라고 하는데, 한 보안 그룹을 만들어놓으면 여러 개의 인스턴스에 같은 보안 그룹을 적용시킬 수 있다.
네트워크 설정에서는 SSH트래픽을 허용시킨다.

스토리지 구성으로는 무료로 30GB를 쓸 수 있다.

맨 아래의 인스턴스 시작을 누르면, EC2 인스턴스가 생성된다.
SSH 클라이언트에서 프라이빗 키페어의 파일이 있는 경로에서 한다고 치자.
아래 명령어로 키를 파일 소유자에게만 읽기-전용(Read-Only)으로 만든다.
sudo chmod 400 your-key.pem #키이름을 넣으면 된다.
그리고, 퍼블릭 IPv4 DNS를 이용하여 EC2 인스턴스로 접속하면 된다.
인스턴스의 정보들은 [EC2] > [인스턴스] > [인스턴스ID]에서 얻을 수 있다.
ssh -i your-key.pem 사용자명@(퍼블릭 IPv4 DNS주소) ## ubuntu의 경우, 기본 사용자명이 ubuntu이다.
터미널에 아래처럼 나오면 성공이다:
Welcome to Ubuntu 24.04 LTS (GNU/Linux 6.8.0-1014-aws x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
System information as of Wed Sep 11 11:20:46 UTC 2024
System load: 0.12 Processes: 130
Usage of /: 73.0% of 37.70GB Users logged in: 0
Memory usage: 22% IPv4 address for enX0: ***
Swap usage: 0%
* Ubuntu Pro delivers the most comprehensive open source security and
compliance features.
https://ubuntu.com/aws/pro
Expanded Security Maintenance for Applications is not enabled.
92 updates can be applied immediately.
To see these additional updates run: apt list --upgradable
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
Last login: Tue Sep 10 05:14:51 2024 from ***
Docker는 리눅스의 응용 프로그램들을 프로세스 격리 기술을 이용해서 독립적인 컨테이너로 실행하고 관리하는 오픈 소스 제품이다.
도커 컨테이너는 일종의 소프트웨어를 소프트웨어의 실행에 필요한 모든 것들을 포함하는 완전한 파일시스템 안에 감싼다.
Docker Compose는 다중 컨테이너 애플리케이션을 정의하고 실행시켜준다.
Docker Compose는 애플리케이션들의 서비스, 네트워크, 볼륨을 쉽게 관리하게 하고, 간단한 커맨드로 조정할 수 있게 한다.
Docker Hub는 Docker 컨테이너의 이미지들이 저장되어있는 곳이다. Docker Hub를 통해서 자신이 만든 이미지를 받을 수 있고, 이미 퍼블릭으로 존재하는 프로그램의 이미지를 받아와서 컨테이너로 실행시킬 수도 있다.
우선, Docker의 apt 저장소를 확보해줄 필요가 있다.
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /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의 설치가 잘 되었는지 확인한다.
sudo systemctl status docker

이제, Docker Compose를 받아보자.
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
curl -SL https://github.com/docker/compose/releases/download/v2.29.2/docker-compose-linux-x86_64 -o $DOCKER_CONFIG/cli-plugins/docker-compose
이 커맨드는 Docker Compose를 받아서 현재 사용자의 home 디렉토리에 설치한다.
버전이나 아키텍처는 원하면 수정하면 된다.
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
실행가능한 권한을 할당한다.
docker-compose version ##또는 아래
docker compose version
## Docker Compose version vx.x.x
버전을 확인해주면서 설치확인을 해주면 된다.
https://hub.docker.com/
Docker Hub에서 회원가입 및 로그인을 한다.
Create repository를 눌러서 레포지토리를 하나 만든다.
네임스페이스와 이름을 입력하고, Public/Private을 고른다.
무료 계정은 Public만 가능하다.
이제, [설정] -> [프로필 변경] -> [개인 액세스토큰]으로 이동한다.

Generate New Token을 누르고, 토큰에 대한 설명과 권한을 주면 된다.
여기서는 작은 프로젝트이니 Read, Write, Delete 모두 할당하지만, 실제 프로덕션에서는 권한을 더 작게 나누는 것이 좋겠다.

발급된 액세스토큰은 안전하게 보관한다.
아래 보이는 액세스토큰은 이미 지워지고 사용할 수 없을 것이다.

Go 애플리케이션의 이미지를 빌드하기 위한 작업을 하려면, Dockerfile이 있어야한다.
Dockerfile은 Docker의 엔진이 읽고 이미지를 빌드하는데 도움을 줄 청사진의 역할을 한다.
정확히는 Docker의 엔진에게 컨테이너 빌드 시의 따라야 할 스크립트이다.
이는 프로젝트의 루트 디렉터리에 있어야 한다.
FROM golang:1.21
WORKDIR /your-work-dir ## 이 부분은 알맞게 변경
COPY go.mod go.sum ./
RUN go mod download
COPY . ./
RUN CGO_ENABLED=0 GOOS=linux go build -o /application-name ##실행파일 이름을 알맞게 변경
EXPOSE 8080
CMD ["/application-name"] ## 실행파일 이름을 알맞게 변경
From golang:1.21: Go 1.21이 설치된 공식 Golang Docker 이미지를 기반으로 사용한다.WORKDIR /my-dir: 컨테이너 내에서 작업 디렉터리를 /my-dir로설정한다.COPY go.mod go.sum ./: 로컬 머신의 go.mod 및 go.sum파일. 즉, 의존성 관리 파일을 현재 워크디렉토리에 복사한다.Run go mod download: go.mod와 go.sum을 기반으로 의존성 라이브러리들을 설치한다.COPY . ./: 현재 디렉터리의 모들 파일들을 컨테이너의 작업 디렉터리로 복사한다.Run CGO_ENABLED=0 GOOS=linux go build -o /application-name: Go 애플리케이션을 빌드한다.CGO_ENABLED=0: CGO를 비활성화하여 순수 Go 코드만으로 애플리케이션을 빌드한다.(외부 C 라이브러리 의존성 제거)GOOS=linux: 빌드할 애플리케이션의 운영 체제를 Linux로 정의한다.-o /application-name: 빌드된 바이너리의 이름을 application-name으로 한다.EXPOSE 8080: 컨테이너의 8080포트를 외부로 노출한다.CMD ["/application-name"]: 컨테이너가 실행될 때의 기본 명령어를 지정한다..dockerignore파일을 만들어서, 빌드 시 제외할 파일 및 디렉토리를 명시한다.
예를 들면, 아래와 같다.
.git
.env*
docker compose의 compose 파일은 여러 개의 컨테이너를 관리하는 데 필요한 정보를 제공한다.
version: '3.8'
services:
app:
image: namespace/image-name:latest
ports:
- "8080:8080"
docker hub에 있는 이미지를 받아서 실행하고, 8080 내부포트를 외부 8080으로 연결시킨다는 의미이다.
내 로컬 프로젝트에서 docker hub에 올려보자.
$ docker login -u [Docker Hub 사용자 이름]
Password: <비밀번호 입력..>
Docker에 로그인하고, 아래 스크립트를 실행하자.
docker build -t [이미지_이름]:[태그] . ## 이미지 빌드
docker tag [이미지_이름]:[태그] [Docker Hub 사용자 이름]/[이미지_이름]:[태그] ## 빌드한 이미지에 태그 붙이기
docker push [Docker Hub 사용자 이름]/[이미지_이름]:[태그] ## 이미지 Docker Hub에 업로드
EC2 인스턴스에서 다음과 같이 해보자.:
git clone [GitHub 주소]
sudo docker login -u [Docker Hub 사용자 이름] ## 로그인
(비밀번호 입력..)
sudo docker pull [Docker Hub 사용자 이름]/[이미지 이름][태그] ## docker 이미지 가져오기
cd [클론해온 GitHub 리포지토리 경로]
sudo docker-compose up -d
docker-compose.yml이 있기 때문이다.모든 docker 컨테이너들을 조회해보자:
sudo docker ps -a
다만, 아직은 미완성이다. 환경 변수는 옮겨지지 않았기 때문이다.
dotenv-vault로 안전하게 환경변수를 프로덕션으로 옮겨보자.
dotenv-vault는 최근에 dotenv에서 강력하게 권장하는 환경변수 버전 컨트롤 및 배포 도구이다.
환경 변수를 안전하게 관리하고 배포하는 일은 까다로운 일인데, dotenv-vault가 큰 도움을 준다.
안전하게 환경 변수를 관리해서 프로덕션까지 올리는 데까지 도움을 주고, 간단한 명령어와 편리한 UI를 제공하여 최상의 환경변수 관리 경험을 제공한다.
거기다가 CI/CD에 편하게 적용할 수 있다.
우선, 우리는 .env파일을 프로젝트 루트 폴더에 가지고 있을 것이라 가정한다.
핵심은 .env.vault파일인데, 이는 고유의 git URL과 같은 느낌을 준다.
이 파일은 프로젝트를 식별하여 팀원들이 dotenv-vault에서 올바른 .env파일을 가져오도록 돕는다.
아래 명령어로 dotenv-vault에 대한 파일을 만들고, 로그인해보자.
npx dotenv-vault@latest new
npx dotenv-vault@latest login
아래 명령어로 현재 dotenv-vault에서 프로덕션용 환경변수가 어떻게 보관되고 있는지를 볼 수 있다.
npx dotenv-vault@latest open production

환경변수를 수정하는 방법은 두 가지가 있는데,
하나는 open을 한 후 열리는 브라우저에서 직접 변경하는 방법이 있다.
또 하나는 로컬에서 직접 수정해서 바꾸는 방법이 있다.
프로덕션용 환경변수를 수정하고 싶다면, env.production에서 수정하면 된다.
그 뒤, 아래 명령어를 실행하면 된다.
npx dotenv-vault push production

두 가지 방법 중 하나로 수정 후, 아래 명령어를 실행하면 된다.
npx dotenv-vault pull production
저장소에서 저장된 production의 환경변수들을 로컬에 불러온다는 의미이다.
아래 명령을 통해서 .env.vault파일을 새로 빌드한다.
.env.vault 파일에는 환경변수들에 대한 암호화된 정보와 id가 들어있다.
npx dotenv-vault@latest build
Git에 추가하고 커밋한다.
.env.vault는 버전 컨트롤에 포함되어도 안전하다. DOTENV_KEY만 유출되지 않으면 된다.
git add .env.vault
git commit -am "Build encrypted .env.vault file for deploy"
.env.vault를 복호화하기 위한 키를 아래 명령어로 받을 수 있다. 이 키를 보관하고 있자.
npx dotenv-vault@latest keys production
실행할 때 이 키를 환경변수로 주어주면, dotenv-vault 패키지는 프로덕션 환경변수를 가져온다.
키가 없으면 기본적으로 현재 디렉토리의 .env를 찾는다고 한다.
DOTENV_KEY='dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=production' go run index.go
이를 Docker에서는 docker-compose.yml을 아래와 같이 수정해야 한다.
version: '3.8'
services:
app:
image: namespace/image-name:latest
ports:
- "8080:8080"
environment:
- DOTENV_KEY=${DOTENV_KEY}
그리고 서버에서 실행할 때 다음과 같이 해주면 된다:
export DOTENV_KEY='dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=production'
sudo docker-compose up -d
EC2 인스턴스에서 빌드된 docker 이미지를 받아와서 설치하고, 환경 변수까지 로딩하는 것은 성공했다.
그러나, 수동으로 매번 빌드 및 배포마다 이 과정을 반복하려면, 꽤 피곤한 일이다.
이를 자동화하기 위해서 CI/CD 자동화 도구를 사용해보려고 하는데, 여러 도구가 있지만, GitHub Actions를 사용해보려고 한다.
GitHub Actions는 CI/CD(Continuous Integration / Continuous delivery) 플랫폼이다.
빌드, 테스트, 배포 파이프라인을 자동화시켜준다.
빌드 및 테스트를 매번 리포지토리에 pull request를 할 때마다, 해주거나, 또는 병합된 PR을 프로덕션으로 자동 배포해준다.
GitHub Actions는 DevOps를 넘어서 당신의 리포지토리에 이벤트가 발생했을 때 워크플로우를 실행하도록 해준다.
예를 들어서, 당신은 누군가가 당신의 리포지토리에 새로운 이슈를 만들었을 때 적절한 라벨을 자동으로 추가하기 위해 워크플로우를 자동시킬 수 있게 해준다.
GitHub는 Linux, Windows, macOS가상머신을 지원하고, 자신의 데이터센터 또는 인프라에 동작시킬 수 있다.
레포지토리에 PR이나 이슈가 생겼을 때와 같은 이벤트가 발생할 때 마다 실행되도록 GitHub Actions를 설정할 수 있다.
워크플로우는 1개 이상의 작업들을 가지고 있고, 이들은 순차적 또는 병렬적으로 실행될 수 있다.
각 작업은 가상 머신 실행기, 또는 컨테이너에서 실행할 것이고, 워크플로우를 단순화하기 위한 재사용가능 확장인 당신이 지정한 스크립트를 실행할 수 있다.

워크플로우는 1개 이상의 작업을 할 수 있는 설정가능한 자동화된 프로세스이다.
워크플로우는 YAML파일로 정의되어서 레포지토리에 이벤트가 발생되었을 때 또는 수동 실행, 정해진 스케줄에 따라 작업이 실행된다.
워크플로우는 레포지토리의 .github/workflows로 정의되고, 한 레포지토리는 서로 다른 작업들을 가진 여러 워크플로우를 가질 수 있다.
예를 들어서, 빌드 및 테스트 PR을 하는 워크플로우가 있고, 애플리케이션을 배포하는 워크플로우가 있다. 이슈에 라벨을 달아주는 워크플로우도 있을 수도 있겠다.
다른 워크플로우에 대한 참조도 가능하다.
이벤트는 레포지토리의 워크플로우를 동작시키는 특정한 활동이다.
예를 들어서, PR생성, 이슈 열기, 커밋 푸시 등이 있다.
REST API post를 이용하여 정해진 시간에도 가능하다.
Job은 한 Runner에서 실행되는 워크플로의 단계집합이다.
각 단계는 실행될 쉘 스크립트이거나, 실행될 actions일 수 있다.
각 단계는 순서대로 실행되고, 서로 의존된다.
같은 Runner에서 각 스텝이 실행되기 때문에, 스텝 간 데이터 전송이 가능하다.
예를 들어서, 애플리케이션을 빌드하는 순서를 스텝화 할 수 있다.
action은 GitHub Actions 플랫폼의 커스텀 애플리케이션인데, 복잡하고 자주 쓰이는 작업들을 실행한다.
action으로 워크플로우 파일에 작성하는 수많은 반복 코드를 줄이는데 사용할 수 있다.
action은 GitHub에서 git 리포지토리에 pull 할 수 있다.
빌드 환경에 맞는 툴체인을 설정하고, 클라우드 제공자 인증설정 등을 할 수 있다.
나만의 actions를 만들 수 있고, 또는 마켓플레이스에서 찾을 수 있다.
runner는 당신의 워크플로우가 트리거될 시 동작하는 서버이다.
각 러너는 하나의 작업만 처리가능하고, GitHub는 Ubuntu Linux, Microsoft Windows, macOS runner를 제공한다.
각 워크플로우는 새롭게 제공된 가상 머신에서 실행한다.
GitHub은 또한 큰 runner를 제공할 수 있고, 당신만의 runner를 실행시킬 수도 있다.
GitHub Actions는 워크플로우 정의를 위해 YAML 문법을 사용한다.
각 워크플로우는 .github/workflows라는 리포지토리의 디렉토리에 저장된다.
예시 워크플로우로 코드가 푸시될 때 마다 일련의 커맨드를 사용하도록 해보자.
bats라는 테스팅 프레임워크를 사용해서 버전을 확인하는 bats -v를 실행해보자.
.github/workflows디렉토리를 만든다..github/workflows에서 learn-github-actions.yml에 다음 코드를 추가한다.name: learn-github-actions
run-name: ${{ github.actor }} is learning GitHub Actions
on: [push]
jobs:
check-bats-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install -g bats
- run: bats -v
새로운 GitHub Actions 워크플로우 파일은 레포지토리에 설치되어 레포지토리에 누군가 푸시될 때 마다 자동으로 실행된다.
name: learn-github-actionsrun-name: ${{ [github.actor](http://github.actor) }} is learning GitHub Actionson: [push]jobs:check-bats-version:runs-on: ubuntu-lateststeps:-uses: actions/checkout@v4-uses: actions/setup-node@v4with: node-version: ‘20’-run: npm install -g batsbats를 설치시킨다.-run: bats -vbats의 버전확인 명령어를 실행시킨다.
워크플로우가 실행되면, 런 프로세스의 시각적 그래프를 볼 수 있다.
name: CI/CD
on:
push:
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/hallym-club-festival-be:latest
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.EC2_KEY }}
- name: Transfer Docker Compose template to EC2
run: |
scp -o StrictHostKeyChecking=no ./${{github.event.repository.name}}/docker-compose.yml.template ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }}:/home/${{ secrets.EC2_USER }}/docker-compose.yml.template
- name: Deploy to EC2
run: |
ssh -o StrictHostKeyChecking=no ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} << 'EOF'
export GO_ENV=production
export DOTENV_KEY="${{ secrets.DOTENV_KEY }}"
export CERT_CACHE="${{secrets.CERT_CACHE_DIR}}"
export LOGFILE_PATH="${{secrets.LOGFILE_PATH}}"
echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | sudo docker login --username ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
envsubst < docker-compose.yml.template > docker-compose.yml
sudo docker-compose down --rmi all
sudo docker-compose up --build -d
EOF
맨 뒤에 envsubst < docker-compose.yml.template > docker-compose.yml에 대해서 설명하자면, 탬플릿 파일을 이용해서 docker-compose.yml파일을 생성하는 것이다.
현재 내 프로젝트의 docker-compose.yml.template은 아래와 같다.
app:
image: riveroverflow/hallym-club-festival-be:latest
ports:
- "8080:8080"
network_mode: "host"
environment:
- GO_ENV=${GO_ENV}
- DOTENV_KEY=${DOTENV_KEY}
- CERT_CACHE=${CERT_CACHE}
- LOGFILE_PATH=${LOGFILE_PATH}
volumes:
- ${CERT_CACHE}:${CERT_CACHE}
- ${LOGFILE_PATH}=${LOGFILE_PATH}
https를 위한 인증서 파일의 저장위치를 컨테이너 외부에 연결하기 위해서 docker-compose.yml에 명시한 모습이다.
또한 로그파일 역시 컨테이너가 아닌 현재 호스트의 파일시스템에 저장시키려고 볼륨을 지정해놓은 것이다.
다음 글에서 더 알아보도록 하자..
https://www.servicenow.com/kr/products/devops/what-is-cicd.html#what-is-a-typical-cicd-workflow
https://docs.docker.com/engine/install/ubuntu/
https://docs.docker.com/compose/install/linux/#install-using-the-repository
https://docs.docker.com/guides/language/golang/configure-ci-cd/
https://www.dotenv.org/docs/quickstart
https://docs.github.com/ko/actions/about-github-actions/understanding-github-actions