Education*
Devops
Architecture
F/B End
B.Chain
Basic
Others
CLOSE
Search For:
Search
BY TAGS
linux
HTTP
golang
flutter
java
fintech
개발환경
kubernetes
network
Docker
devops
database
tutorial
cli
분산시스템
www
블록체인
AWS
system admin
bigdata
보안
금융
msa
mysql
redis
Linux command
dns
javascript
CICD
VPC
FILESYSTEM
S3
NGINX
TCP/IP
ZOOKEEPER
NOSQL
IAC
CLOUD
TERRAFORM
logging
IT용어
Kafka
docker-compose
Dart
Golang Application을 docker compose로 개발하기
Recommanded
Free
YOUTUBE Lecture:
<% selectedImage[1] %>
yundream
2023-10-02
2023-10-02
1686
![docker-compose and golang](https://docs.google.com/drawings/d/e/2PACX-1vT6umBFoVj_wMrf86iaPd-jBabtyzUVDSfyOe1_5kVUl0yZGUs6P0PX-OO6UG1L7jrR_ot5feVzsEi_/pub?w=1856&h=900) ### 소개 앞서 [Golang Application docker 빌드](https://www.joinc.co.kr/w/golang_docker) 에서 Golang 애플리케이션을 Docker 에서 빌드하고 Docker 이미지 형태로 배포하는 것을 살펴봤다. 하지만 대부분의 애플리케이션은 데이터베이스 혹은 다른 애플리케이션과 통신을 해야 한다. 이 경우 단일 docker 이미지 형태로 배포하면 테스트하기가 쉽지 않다. docker compose를 이용하면, Docker 네트워크를 통해서 여러 개의 컨테이너로 구성된 서비스를 구축할 수 있다. 여기에서는 docker compose를 이용해서 MySQL 데이터베이스를 사용하는 Go 애플리케이션을 만들어 볼 것이다. ## 조건 이 문서를 따라하기 위해서는 아래의 조건이 갖춰져 있어야 한다. * Ubuntu 리눅스: Ubuntu 리눅스가 설치된 로컬 PC 혹은 가상 머신 * Docker 설치 * Docker-compose 설치: [Ubuntu Linux에 docker compose 설치하기](https://www.joinc.co.kr/w/docker_compose_starter) 참고. * [Golang Application docker 빌드](https://www.joinc.co.kr/w/golang_docker): GoLang 애플리케이션을 dockerizing 하는 법. * git 설치: 예제는 github에서 다운로드(clone)할 수 있다. ## 애플리케이션 아키텍처 우리는 **counter** 애플리케이션을 만들려고 한다. 이 애플리케이션은 Go로 만들어진 count API 서버와 count 정보를 저장하는 MySQL 데이터베이스로 구성된다. 이 애플리케이션은 최종적으로 Kubernetes에 배포될 건데, 배포 전에 로컬에서 테스트 환경을 갖추길 원하고 있다. 개발팀은 docker-compose를 이용해서 로컬 빌드/개발 환경을 구축하기로 했다. 아키텍처는 아래와 같다. ![애플리케이션 아키텍처](https://docs.google.com/drawings/d/e/2PACX-1vSC3rWDZBjYvx2Rr5iziM5v488CwgUUJFmdFJzyiIUz557AZOnCeWFLRPYQRzXEMnOZ5sJ3cbn22RzO/pub?w=616&h=147) GoLang 애플리케이션은 2개의 API를 제공한다. 1. GET /api/count: 현재 count 값을 읽어서 리턴한다. 2. PUT /api/count: count의 값을 1만큼 증가 시킨다. ## GoLang 애플리케이션 ```shell $ git clone https://github.com/yundream/joinc-go-hello ``` 전체 예제 코드는 https://github.com/yundream/joinc-go-hello 에서 다운로드 할 수 있다. ```go package main import ( "fmt" "net/http" "os" "time" "github.com/gorilla/mux" "gorm.io/driver/mysql" "gorm.io/gorm" ) var ( db *gorm.DB ) type Counter struct { gorm.Model Name string `gorm:"size:16"` Count int } func main() { _, err := GetDatabaseInstance() if err != nil { fmt.Println(">> ", err.Error()) time.Sleep(2 * time.Second) os.Exit(1) } r := mux.NewRouter() fmt.Println("Server start!!") r.HandleFunc("/api/count", getCounter).Methods("GET") r.HandleFunc("/api/count", addCounter).Methods("POST") http.ListenAndServe(":8000", r) } func addCounter(w http.ResponseWriter, r *http.Request) { tx := db.Exec("UPDATE counters SET count=count+1 WHERE name='count'") if tx.Error != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Error:%s", tx.Error.Error()) return } } func getCounter(w http.ResponseWriter, r *http.Request) { readCounter := &Counter{} tx := db.Where("name=?", "count").First(&readCounter) if tx.Error != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Error:%s", tx.Error.Error()) return } fmt.Fprintf(w, "%d", readCounter.Count) } func GetDatabaseInstance() (*gorm.DB, error) { var ( retry int database *gorm.DB err error ) retry = 0 dbUser := os.Getenv("MYSQL_USER") dbPass := os.Getenv("MYSQL_PASSWORD") dbName := os.Getenv("MYSQL_DATABASE") dbHost := os.Getenv("MYSQL_HOST") dbPort := os.Getenv("MYSQL_PORT") createDBDsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/", dbUser, dbPass, dbHost, dbPort, ) for retry < 3 { time.Sleep(10 * time.Second) database, err = gorm.Open(mysql.Open(createDBDsn), &gorm.Config{}) if err == nil { break } retry++ } if err != nil { fmt.Println(">> ", err.Error(), createDBDsn) return nil, err } tx := database.Exec("CREATE DATABASE IF NOT EXISTS " + dbName + ";") if tx.Error != nil { return nil, tx.Error } dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", dbUser, dbPass, dbHost, dbPort, dbName, ) db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { fmt.Println(err.Error()) return nil, err } isTable := db.Migrator().HasTable(&Counter{}) fmt.Println("isTable", isTable) err = db.AutoMigrate(&Counter{}) if err == nil { if !isTable { db.Create(&Counter{Name: "count", Count: 0}) } } return db, nil } ``` 예제 코드 치고는 코드 길이가 제법된다. 중요 부분을 하나씩 떼어내서 살펴보자. ```go import( "github.com/gorilla/mux" "gorm.io/driver/mysql" "gorm.io/gorm" ) ``` API 개발을 위해서 **gorilla 웹 툴킷**을 이용했다. gin, Revel 같은 웹 프레임워크에 비교해서 기능은 부족하지만 간단하게 사용 할 수 있어서 널리 사용하고 있다. 데이터베이스를 관리하기 위해서 ORM 라이브러리인 **gorm**을 사용했다. docker-compose로 애플리케이션을 개발 할경우, 데이터베이스 스키마를 만들어줘야 하는데 gorm의 automigration을 이용해서 자동으로 테이블을 생성할 수 있다. ```go type Counter struct { gorm.Model Name string `gorm:"size:16"` Count int } ``` gorm이 사용할 Counter 구조체다. 나중에 gorm은 이 구조체를 기반으로 테이블 스키마를 만들고, 쿼리를 실행한다. ```go func main() { _, err := GetDatabaseInstance() if err != nil { fmt.Println(">> ", err.Error()) time.Sleep(2 * time.Second) os.Exit(1) } r := mux.NewRouter() fmt.Println("Server start!!") r.HandleFunc("/api/count", getCounter).Methods("GET") r.HandleFunc("/api/count", addCounter).Methods("POST") http.ListenAndServe(":8000", r) } ``` GET /api/count과 POST /api/count 2개의 API를 만들었다. ```go func addCounter(w http.ResponseWriter, r *http.Request) { tx := db.Exec("UPDATE counters SET count=count+1 WHERE name='count'") if tx.Error != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Error:%s", tx.Error.Error()) return } } func getCounter(w http.ResponseWriter, r *http.Request) { readCounter := &Counter{} tx := db.Where("name=?", "count").First(&readCounter) if tx.Error != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Error:%s", tx.Error.Error()) return } fmt.Fprintf(w, "%d", readCounter.Count) } ``` GET /api/count 과 POST /api/count를 위한 handler 함수다. getCounter는 count 값을 읽어서 리턴하고, addCounter는 count를 1 증가시킨다. ```go func GetDatabaseInstance() (*gorm.DB, error) { var ( retry int database *gorm.DB err error ) retry = 0 dbUser := os.Getenv("MYSQL_USER") dbPass := os.Getenv("MYSQL_PASSWORD") dbName := os.Getenv("MYSQL_DATABASE") dbHost := os.Getenv("MYSQL_HOST") dbPort := os.Getenv("MYSQL_PORT") createDBDsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/", dbUser, dbPass, dbHost, dbPort, ) ``` 데이터베이스와 관련된 정보를 환경변수를 통해서 관리하기로 했다. 환경변수를 이용해서 관리 하면, 별도의 환경파일을 관리 할 필요가 없고, Docker 컨테이너를 실행할 때, 컨테이너 변수로 데이터베이스 설정 값을 넘길 수 있다는 장점이 있다. 나중에 ECS, Kubernetes 등으로 배포할 때도 환경변수를 이용하여 좀 더 쉽게 배포 할 수 있다. ```go for retry < 3 { time.Sleep(10 * time.Second) database, err = gorm.Open(mysql.Open(createDBDsn), &gorm.Config{}) if err == nil { break } retry++ } if err != nil { fmt.Println(">> ", err.Error(), createDBDsn) return nil, err } tx := database.Exec("CREATE DATABASE IF NOT EXISTS " + dbName + ";") if tx.Error != nil { return nil, tx.Error } ``` docker-compose로 실행 할 경우, mysql 컨테이너와 counter app 컨테이너를 따로 실행하게 된다. 이때 mysql 컨테이너가 실행 된 후 counter app 컨테이너를 실행하도록 설정을 할 것이다. 그런데 mysql의 경우 컨테이너가 실행된 뒤에도 실제 연결이 가능할 때까지 시간이(약 10초) 걸리게 된다. 이때 연결을 시도하면 에러가 발생하므로 10초 간격으로 3번 연결 시도를 하도록 코드를 변경했다. 참고로 이 예제코드는 코드안에서 데이터베이스 연결을 retry 체크하는데, 이렇게 하지 않고 docker-compose 설정을 변경해서 컨테이너가 종료되면 컨테이너를 다시 시작하도록 할 수 있다. 아마 이 방법이 더 편할 건데, docker-compose 설정파일 부분에서 자세히 살펴보도록 하겠다. DB 연결이 끝났다면 "CREATE DATABASE IF NOT EXISTS dbName" 쿼리를 이용해서 **dbName 데이터베이스가 없을 경우** 새로 데이터베이스를 생성하도록 했다. ```go dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", dbUser, dbPass, dbHost, dbPort, dbName, ) db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { fmt.Println(err.Error()) return nil, err } isTable := db.Migrator().HasTable(&Counter{}) fmt.Println("isTable", isTable) err = db.AutoMigrate(&Counter{}) if err == nil { if !isTable { db.Create(&Counter{Name: "count", Count: 0}) } } return db, nil ``` 이제 환경 변수 값을 이용해서 dsn 을 만들고 mysql 데이터베이스에 연결한다. 연결하고 나면 **db.Migrator()** 를 호출해서 Counter 구조체를 기반으로 테이블을 마이그레이션 한다. 만약 테이블이 없건나, 테이블 스키마가 변경됐다면 자동으로 테이블을 생성 혹은 변경해준다. 테이블을 새로 만드는 경우 **db.Create**를 호출해서 count 를 0으로 설정한다. ## 예제 코드를 docker 이미지로 만들기 Make가 설치돼 있으면 "make"를 실행하면 된다. 혹은 build 명령을 이용해서 직접 이미지를 만들어도 된다. ```shell $ docker build -t joinc/counter:0.1 -t joinc/counter:latest . ``` ## docker-compose.yml 설정파일 이제 docker-compose.yml 파일을 분석해 보자. ```yaml version: "3.0" networks: joinc-nw: services: counter-app: image: joinc/counter:latest ports: - "8000:8000" depends_on: - counter-db networks: - joinc-nw environment: MYSQL_USER: "root" MYSQL_PASSWORD: "1234" MYSQL_DATABASE: "counter" MYSQL_PORT: "3306" MYSQL_HOST: "counter-db" counter-db: image: mysql:latest ports: - "3306" environment: MYSQL_ROOT_PASSWORD: 1234 networks: - joinc-nw ``` joinc-nw 를 만들었다. driver를 따로 설정하지 않았는데, 이 경우 bridge driver가 설정된다. 위 설정은 아래와 같이 바꿀 수 있다. ```yaml networks: joinc-nw: driver: bridge ``` counter-app 서비스 설정을 보자. ```yml counter-app: image: joinc/counter:latest ports: - "8000:8000" depends_on: - counter-db networks: - joinc-nw environment: MYSQL_USER: "root" MYSQL_PASSWORD: "1234" MYSQL_DATABASE: "counter" MYSQL_PORT: "3306" MYSQL_HOST: "counter-db" ``` * image: joinc/counter:latest 이미지로 컨테이너를 만든다. * ports: 8000 번 포트로 포트포워딩 했다. * depends_on: counter-mysql 컨테이너가 실행되고 나면, 실행한다. * networks: joinc-nw 네트워크를 사용한다. * environment: 컨테이너가 시작될 때 설정될 환경 변수들이다. Go 애플리케이션은 이 환경변수를 읽어서 데이터베이스에 연결할 수 있다. **MYSQL_HOST**는 데이터베이스 서버의 이름인데, services 블록의 서비스 이름 "counter-db"으로 서버를 찾을 수 있다. MySQL 데이터베이스의 service 블록 정보를 보자. ```yml counter-db: image: mysql:latest ports: - "3306" environment: MYSQL_ROOT_PASSWORD: 1234 networks: - joinc-nw ``` * image: mysql 최신 이미지로 컨테이너를 실행한다. * ports: mysql의 기본 포트 3306을 사용한다. * envorionment: MySQL 서버의 root 패스워드를 설정한다. * networks: joinc-nw를 사용한다. ## docker-compose 실행 docker-compose up 명령으로 애플리케이션을 실행해보자. ``` $ docker-compose up [+] Running 2/0 ✔ Container docker-compose-counter-db-1 Created 0.0s ✔ Container docker-compose-counter-app-1 Recreated 0.0s Attaching to docker-compose-counter-app-1, docker-compose-counter-db-1 ``` docker-compose ps 명령으로 컨테이너를 확인해 보자. ![docker-compose ps](https://docs.google.com/drawings/d/e/2PACX-1vQQ8quCJkg8glC0fDqS_T2Hq9Tz-LmTHcYJe4qYGtbWsC6PDuBuBHlY2ERuhsofcrfF3WEW0fSLVAdk/pub?w=1409&h=100) curl 을 이용해서 GET /api/count 와 POST /api/count가 잘 작동하는지 테스트해보자. ``` $ curl localhost:8000/api/count 0 $ curl -XPOST localhost:8000/api/count $ curl localhost:8000/api/count 1 ``` 잘 작동하는 걸 확인할 수 있다. ## 정리 이 문서에서 실행한 docker-compose 애플리케이션 아키텍처는 아래와 같이 묘사할 수 있을 것이다. ![docker-compose 애플리케이션 아키텍처](https://docs.google.com/drawings/d/e/2PACX-1vSNSuARywTVY87ryNVwEsYt3VrdW95Ow7aHixvr8dQPqhqo3ZOxskZnIIKZ1ZRew-0stFArfYkADDQA/pub?w=624&h=221) services는 **counter-app** 과 **docunter-db** 컨테이너로 구성된다. 이들 컨테이너는 **joinc-nw**로 서로 연결된다. 각 컨테이너는 IP가 아닌 service name 으로 찾을 수 있기 때문에 IP를 설정할 필요가 없다. Kubernetes 나 ECS와 같은 컨테이너 기반으로 애플리케이션을 실행 할 경우, 로컬 테스트 환경 구성이 까다로워질 수 있는데, docker-compose.yml을 함께 배포하면 로컬 개발환경도 깔끔하게 구성 할 수 있다. 조금 더 부지런하면 [minikube](https://www.joinc.co.kr/w/kubernetes_minikube_index)로 구축할 수도 있는데, 개발 팀이 minikube를 이용해서 개발환경을 구축하는 것은 너무 복잡하다. docker-compose를 검토해 보자.
Recent Posts
생성 AI 모델 Flux.1 설치 및 사용
GPT를 이용한 Reranker 테스트
5분만에 만들어보는 Streamlit 챗봇
Let's encrypt로 SSL 인증서 관리하기
Upscayl을 이용한 이미지 업스케일링
스테이블 디퓨전 설치 및 사용해보기
Elasticsearch 설치
AI / LLM에 대한 친절한 소개
SLA 다운타임 계산기
Docker로 GitLab 설치하기
Archive Posts
Tags
devops
docker
docker-compose
golang
Copyrights © -
Joinc
, All Rights Reserved.
Inherited From -
Yundream
Rebranded By -
Joonphil
Recent Posts
Archive Posts
Tags