몇 주 전에 저는 Travis CI라는 인기 있는 서비스를 사용하여 Go 프로그래밍 언어로 작성된 애플리케이션을 지속적으로 배포하는 방법에 대해 작성한 적이 있습니다. 이 예제에서는 애플리케이션을 만드는 데 카우치베이스 NoSQL 데이터베이스, 단위 테스트 생성, Golang 지속적 통합 파이프라인에서 해당 테스트 실행, 마지막으로 모든 것이 성공하면 원격 서버에 애플리케이션을 배포합니다.
이러한 기능을 제공하는 서비스는 트래비스 CI만이 아닙니다. 실제로 Jenkins를 사용하여 자체 CI/CD 서비스를 호스팅할 수 있습니다.
지속적인 통합과 지속적인 배포를 가능하게 하는 Golang 애플리케이션의 파이프라인에 Jenkins를 사용하는 방법을 살펴보겠습니다.
아직 읽지 않으셨다면 이전 트래비스 CI 튜토리얼을 사용한 골랑유용한 설명이 많으니 꼭 읽어보시길 추천합니다. 여기에는 동일한 자료가 많이 표시되지만 설명이 다르므로 두 가지 설명이 도움이 될 수 있습니다.
이 Jenkins with Golang 튜토리얼을 제대로 경험하려면 다음이 필요합니다. 카우치베이스 서버 를 어딘가에 설치합니다. 목표는 애플리케이션이 배포된 후 해당 데이터베이스 인스턴스를 실행하고 사용하도록 하는 것입니다.
카우치베이스 애플리케이션으로 Go 개발하기
이 튜토리얼을 성공적으로 수행하려면 테스트하고 배포할 Go 애플리케이션이 필요합니다. 미리 시작하고 싶으시다면 기능적인 프로젝트를 다음 주소에 업로드했습니다. GitHub. 실제로 트래비스 CI 예시와 동일한 프로젝트입니다.
프로젝트를 직접 살펴보고 싶으시다면 잠시 시간을 내어 살펴보세요.
어딘가에 $GOPATH 라는 파일을 만듭니다. main.go 를 클릭하고 다음 Go 코드를 포함하세요. 나중에 자세히 설명하겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
패키지 메인 가져오기 ( "fmt" "os" gocb "gopkg.in/couchbase/gocb.v1" ) 유형 버킷 인터페이스 인터페이스 { Get(키 문자열, 값 인터페이스{}) (gocb.Cas, 오류) 삽입(키 문자열, 값 인터페이스{}, 만료 uint32) (gocb.Cas, 오류) } 유형 데이터베이스 구조체 { 버킷 버킷 인터페이스 } 유형 사람 구조체 { 유형 문자열 `json:"type"` 이름 문자열 `json:"이름"` 성 문자열 `json:"성"` } func (d 데이터베이스) GetPersonDocument(키 문자열) (인터페이스{}, 오류) { var 데이터 인터페이스{} _, err := d.버킷.Get(키, &데이터) 만약 err != nil { 반환 nil, err } 반환 데이터, nil } func (d 데이터베이스) 사람 문서 만들기(키 문자열, 데이터 인터페이스{}) (인터페이스{}, 오류) { _, err := d.버킷.삽입(키, 데이터, 0) 만약 err != nil { 반환 nil, err } 반환 데이터, nil } func 메인() { fmt.Println("애플리케이션 시작 중...") var 데이터베이스 데이터베이스 클러스터, _ := gocb.연결("couchbase://" + os.Getenv("DB_HOST")) 클러스터.인증(gocb.비밀번호 인증기{사용자 이름: os.Getenv("DB_USER"), 비밀번호: os.Getenv("DB_PASS")}) 데이터베이스.버킷, _ = 클러스터.OpenBucket(os.Getenv("DB_BUCKET"), "") fmt.Println(데이터베이스.GetPersonDocument("8eaf1065-5bc7-49b5-8f04-c6a33472d9d5")) 데이터베이스.사람 문서 만들기("blawson", 사람{유형: "사람", 이름: "Brett", 성: "Lawson"}) } |
이 애플리케이션은 많은 기능을 제공하지는 않지만 많은 일이 진행되고 있습니다.
가져오기에서는 Go용 Couchbase SDK를 사용한 것을 확인할 수 있습니다. 이 프로젝트를 컴파일하려면 SDK를 다운로드해야 합니다. 다음 명령어로 다운로드할 수 있습니다:
1 |
go get gopkg.in/카우치베이스/gocb.v1 |
코드를 살펴보기 전에 한 걸음 물러나서 이 애플리케이션이 어떻게 작동해야 하는지 알아볼 필요가 있습니다.
여기서 목표는 NoSQL 데이터베이스인 Couchbase에 연결하여 일부 데이터를 검색하고 일부 데이터를 생성하는 것입니다. 물론 이 작업은 SDK를 통해 매우 쉽게 수행할 수 있지만, 애플리케이션에 대한 단위 테스트를 만들고 싶습니다. 단위 테스트에서 데이터베이스를 대상으로 테스트하지 않는 것이 가장 좋습니다. 통합 테스트를 위해 저장해 두세요. 데이터베이스를 대상으로 테스트하지 않는다면 모의 시나리오를 만들어야 합니다.
실제 시나리오와 모의 시나리오를 구분하는 가장 좋은 방법은 Go로 두 가지 모두에 대한 인터페이스를 만드는 것입니다. 메인 애플리케이션은 실제 클래스를 인터페이스의 일부로 사용하고 테스트는 모의 클래스를 사용합니다.
따라서 Couchbase Go SDK를 위한 인터페이스를 만들어야 합니다. 버킷
컴포넌트입니다.
1 2 3 4 |
유형 버킷 인터페이스 인터페이스 { Get(키 문자열, 값 인터페이스{}) (gocb.Cas, 오류) 삽입(키 문자열, 값 인터페이스{}, 만료 uint32) (gocb.Cas, 오류) } |
카우치베이스 버킷에는 다음보다 훨씬 더 많은 기능이 있습니다. Get
그리고 삽입
를 사용할 수 있지만 이 예제에서는 이 함수만 사용합니다. 애플리케이션의 뒷부분에서 간단하게 하기 위해 이 예제에서는 구조체
새로운 인터페이스를 사용하세요.
1 2 3 |
유형 데이터베이스 구조체 { 버킷 버킷 인터페이스 } |
이 예제에는 데이터 모델이 하나만 사용됩니다. 이 예제에서는 다음을 기반으로 하는 데이터 모델을 사용하겠습니다. 사람
데이터 구조를 사용합니다. 애플리케이션에 영향을 주지 않고 자유롭게 변경할 수 있습니다.
결국 단위 테스트를 하게 될 함수 중 하나를 살펴보겠습니다:
1 2 3 4 5 6 7 8 |
func (d 데이터베이스) GetPersonDocument(키 문자열) (인터페이스{}, 오류) { var 데이터 인터페이스{} _, err := d.버킷.Get(키, &데이터) 만약 err != nil { 반환 nil, err } 반환 데이터, nil } |
에서 GetPersonDocument
함수를 사용하는 경우, 우리는 버킷 인터페이스
를 사용하여 문서 키로 특정 문서를 가져옵니다.
마찬가지로 데이터를 생성하려면 다음과 같이 해야 합니다:
1 2 3 4 5 6 7 |
func (d 데이터베이스) 사람 문서 만들기(키 문자열, 데이터 인터페이스{}) (인터페이스{}, 오류) { _, err := d.버킷.삽입(키, 데이터, 0) 만약 err != nil { 반환 nil, err } 반환 데이터, nil } |
반복해서 말씀드려야 할 것 같지만, 이 기능은 필요 이상으로 복잡하게 설계되었습니다. 몇 가지 테스트를 시연하기 위해 이렇게 만든 것입니다. 기분이 나아질 수 있다면 단순한 기능보다는 좀 더 복잡한 기능을 추가해 보세요. Get
그리고 삽입
기능을 제공합니다.
마지막으로 런타임에 실행되는 다음 코드가 있습니다:
1 2 3 4 5 6 7 8 9 |
func 메인() { fmt.Println("애플리케이션 시작 중...") var 데이터베이스 데이터베이스 클러스터, _ := gocb.연결("couchbase://" + os.Getenv("DB_HOST")) 클러스터.인증(gocb.비밀번호 인증기{사용자 이름: os.Getenv("DB_USER"), 비밀번호: os.Getenv("DB_PASS")}) 데이터베이스.버킷, _ = 클러스터.OpenBucket(os.Getenv("DB_BUCKET"), "") fmt.Println(데이터베이스.GetPersonDocument("8eaf1065-5bc7-49b5-8f04-c6a33472d9d5")) 데이터베이스.사람 문서 만들기("blawson", 사람{유형: "사람", 이름: "Brett", 성: "Lawson"}) } |
애플리케이션이 실행되면 환경 변수를 사용하여 Couchbase에 연결을 설정합니다. 열린 버킷은 우리의 버킷 인터페이스
를 클릭한 다음 두 함수가 실행됩니다.
그렇다면 이를 어떻게 테스트할 수 있을까요?
프로젝트에 다음과 같은 파일을 만듭니다. main_test.go 를 다음 코드로 대체합니다:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
패키지 메인 가져오기 ( "encoding/json" "os" "testing" "github.com/mitchellh/mapstructure" gocb "gopkg.in/couchbase/gocb.v1" ) 유형 모의 버킷 구조체{} var 테스트 데이터베이스 데이터베이스 func 변환(시작 인터페이스{}, 끝 인터페이스{}) 오류 { 바이트, err := json.마샬(시작) 만약 err != nil { 반환 err } err = json.언마샬(바이트, 끝) 만약 err != nil { 반환 err } 반환 nil } func (b 모의 버킷) Get(키 문자열, 값 인터페이스{}) (gocb.Cas, 오류) { 스위치 키 { case "nraboy": err := 변환(사람{유형: "사람", 이름: "Nic", 성: "라보이"}, 값) 만약 err != nil { 반환 0, err } 기본값: 반환 0, gocb.ErrKeyNotFound } 반환 1, nil } func (b 모의 버킷) 삽입(키 문자열, 값 인터페이스{}, 만료 uint32) (gocb.Cas, 오류) { 스위치 키 { case "nraboy": 반환 0, gocb.ErrKeyExists } 반환 1, nil } func TestMain(m *테스트.M) { 테스트 데이터베이스.버킷 = &모의 버킷{} os.종료(m.실행()) } func TestGetPersonDocument(t *테스트.T) { 데이터, err := 테스트 데이터베이스.GetPersonDocument("nraboy") 만약 err != nil { t.Fatalf("'오류'가 `%s`가 될 것으로 예상했지만 `%s`가 발생했습니다.", "nil", err) } var 사람 사람 지도 구조.디코딩(데이터, &사람) 만약 사람.유형 != "사람" { t.Fatalf("'유형'이 %s가 될 것으로 예상했지만 %s를 얻었습니다.", "사람", 사람.유형) } } func TestCreatePersonDocument(t *테스트.T) { _, err := 테스트 데이터베이스.사람 문서 만들기("blawson", 사람{유형: "사람", 이름: "Brett", 성: "Lawson"}) 만약 err != nil { t.Fatalf("'오류'가 `%s`가 될 것으로 예상했지만 `%s`가 발생했습니다.", "nil", err) } } |
이 파일이 상당히 길고 다른 사용자 정의 패키지도 포함되어 있다는 것을 알 수 있습니다. 코드를 분석하기 전에 해당 패키지를 다운로드해 보겠습니다. 명령줄에서 다음을 실행합니다:
1 |
go get github.com/mitchellh/지도 구조 |
맵구조 패키지를 사용하면 지도를 가져와서 다음과 같은 실제 데이터 구조로 변환할 수 있습니다. 사람
데이터 구조로 변경했습니다. 이는 기본적으로 우리가 할 수 있는 일에 약간의 유연성을 제공합니다.
맵구조 패키지에 대해 자세히 알아보려면 이전에 작성한 글을 참조하세요, 맵 값을 네이티브 Golang 구조로 디코딩하기.
종속성이 설치되었으므로 이제 코드를 살펴볼 수 있습니다. 메인 코드에서 Go SDK의 버킷을 어떻게 사용했는지 기억하시나요? 테스트 코드에서는 그렇게 하지 않겠습니다.
1 2 3 |
유형 모의 버킷 구조체{} var 테스트 데이터베이스 데이터베이스 |
테스트 코드에서는 빈 구조체
로 설정되어 있지만 버킷 인터페이스
에서 데이터베이스
데이터 구조가 메인 코드에서 생성되었습니다.
데이터 구조의 실제 설정은 TestMain
함수는 다른 모든 테스트보다 먼저 실행됩니다:
1 2 3 4 |
func TestMain(m *테스트.M) { 테스트 데이터베이스.버킷 = &모의 버킷{} os.종료(m.실행()) } |
이제 우리는 모의 버킷
에서 제공하는 모든 기능을 제공하지는 않습니다. gocb.Bucket
가 있었을지도 모릅니다. 대신, 우리는 버킷 인터페이스
정의.
우리는 Get
및 삽입
함수를 인터페이스에 정의된 대로 사용할 수 있습니다.
부터 시작하여 Get
함수에는 다음이 있습니다:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
func 변환(시작 인터페이스{}, 끝 인터페이스{}) 오류 { 바이트, err := json.마샬(시작) 만약 err != nil { 반환 err } err = json.언마샬(바이트, 끝) 만약 err != nil { 반환 err } 반환 nil } func (b 모의 버킷) Get(키 문자열, 값 인터페이스{}) (gocb.Cas, 오류) { 스위치 키 { case "nraboy": err := 변환(사람{유형: "사람", 이름: "Nic", 성: "라보이"}, 값) 만약 err != nil { 반환 0, err } 기본값: 반환 0, gocb.ErrKeyNotFound } 반환 1, nil } |
사용 중인 경우 모의 버킷
그리고 우리는 Get
키가 하나만 유효할 것으로 예상하고 있습니다. 이것은 테스트이므로 규칙을 정한다는 점을 기억하세요. 만약 nraboy
를 키로 사용하면 일부 모의 데이터를 반환하고, 그렇지 않으면 키를 찾을 수 없음 오류가 발생했습니다. 잠재적으로 여러 유형의 데이터로 작업하고 있으므로 데이터를 변환하려면 변환
함수를 사용합니다. 기본적으로 인터페이스를 JSON으로 마샬링한 다음 다시 마샬링하는 것입니다.
이제 그 모의 사례를 살펴보겠습니다. 삽입
함수입니다.
1 2 3 4 5 6 7 |
func (b 모의 버킷) 삽입(키 문자열, 값 인터페이스{}, 만료 uint32) (gocb.Cas, 오류) { 스위치 키 { case "nraboy": 반환 0, gocb.ErrKeyExists } 반환 1, nil } |
모의 버킷을 사용하여 데이터를 삽입하려고 하면 키가 다음과 같지 않을 것으로 예상됩니다. nraboy
를 반환하고, 그렇지 않으면 오류를 발생시킵니다.
인터페이스 함수가 생성되었으므로 이제 메인 Go 코드의 함수를 테스트하는 실제 테스트에 집중할 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 |
func TestGetPersonDocument(t *테스트.T) { 데이터, err := 테스트 데이터베이스.GetPersonDocument("nraboy") 만약 err != nil { t.Fatalf("'오류'가 `%s`가 될 것으로 예상했지만 `%s`가 발생했습니다.", "nil", err) } var 사람 사람 지도 구조.디코딩(데이터, &사람) 만약 사람.유형 != "사람" { t.Fatalf("'유형'이 %s가 될 것으로 예상했지만 %s를 얻었습니다.", "사람", 사람.유형) } } |
그리고 TestGetPersonDocument
는 실제 버킷에서 모의 버킷을 사용할 것입니다. GetPersonDocument
함수를 호출합니다. 인터페이스를 사용하고 있으므로 실제 Couchbase Go SDK 함수든 우리가 사용한 모의 함수든 어떤 인터페이스 함수를 사용할지 Go가 알아낼 것입니다. 결과에 따라 테스트가 진행됩니다.
1 2 3 4 5 6 |
func TestCreatePersonDocument(t *테스트.T) { _, err := 테스트 데이터베이스.사람 문서 만들기("blawson", 사람{유형: "사람", 이름: "Brett", 성: "Lawson"}) 만약 err != nil { t.Fatalf("'오류'가 `%s`가 될 것으로 예상했지만 `%s`가 발생했습니다.", "nil", err) } } |
그리고 TestCreatePersonDocument
는 이전과 다르지 않습니다. 우리는 실제 사람 문서 만들기
하지만 우리는 모의 버킷과 함께 모의 삽입
함수입니다.
이 시점에서 우리는 테스트가 완료된 기능적인 Go 애플리케이션을 보유하고 있으며 지속적인 통합 및 지속적인 배포를 위한 준비가 완료되었습니다.
SSH 및 Golang 배포를 위한 Jenkins 설치 및 구성하기
다음 단계에서는 배포를 받을 준비가 된 원격 서버가 있다고 가정합니다. 저는 그렇지 않았기 때문에 Ubuntu로 Docker 컨테이너를 만들었습니다. 실제로 제 Jenkins 설치와 원격 서버 모두 Docker를 사용하고 있습니다.
제가 한 방법을 따라하고 싶다면 여기를 확인하세요. 명령줄에서 다음을 실행하여 Ubuntu 컨테이너를 시작합니다:
1 |
도커 실행 -it --이름 우분투 우분투 /bin/bash |
위의 명령은 Ubuntu 컨테이너를 배포하고 이름을 지정합니다. 우분투
. 배포가 완료되면 대화형 터미널을 통해 연결됩니다. 컨테이너 간 통신에는 매핑된 포트가 필요하지 않으므로 포트를 열지 않았습니다.
Ubuntu 컨테이너에는 SSH 서버를 사용할 수 없으므로 이를 설치해야 합니다. Ubuntu 셸에서 다음을 실행합니다:
1 2 3 |
apt-get 업데이트 apt-get 설치 openssh-서버 서비스 ssh 다시 시작 |
위의 명령은 다음을 설치합니다. openssh-server
를 클릭하고 시작합니다. 이왕 하는 김에 Jenkins가 사용할 공개 키와 비공개 키 조합을 만들어야 합니다.
Ubuntu 셸에서 다음을 실행합니다:
1 |
ssh-keygen -t rsa |
완료되면 ~/.ssh/id_rsa.pub 콘텐츠로 ~/.ssh/authorized_keys 를 사용할 것이므로 Jenkins 서버에서 개인 키를 사용할 것입니다.
저는 Jenkins를 Docker 컨테이너로도 사용하고 있다는 점을 기억하세요. 원하지 않는다면 컨테이너를 사용하지 않아도 됩니다. 모든 것이 잘 번역될 것입니다.
Docker를 사용하는 경우 다음을 실행하여 Jenkins 컨테이너를 스핀업합니다:
1 |
도커 실행 -d -p 8080:8080 -p 50000:50000 --이름 jenkins jenkins |
위의 명령은 분리된 모드로 Jenkins를 배포하고 일부 포트를 매핑합니다.
방문 시 http://localhost:8080 웹 브라우저에서 마법사의 단계를 따라 권장 플러그인 설치를 선택했는지 확인합니다.
기본 Jenkins 대시보드에 도달하면 다음을 선택합니다. 젠킨스 관리 -> 플러그인 관리 몇 가지를 다운로드해야 하기 때문입니다.
Go 코드를 컴파일할 수 있는 방법이 필요하므로 이동 플러그인이 필요합니다. 빌드를 위해 자체 사용자 정의 스크립트를 실행해야 하므로 포스트빌드스크립트 플러그인을 설치합니다. 마지막으로, 원격 서버에 게시하고 명령을 실행할 수 있도록 하려면 SSH를 통한 게시 다른 플러그인과 함께 제공되는 플러그인입니다.
플러그인 다운로드가 완료되면 전역적으로 구성해야 합니다.
기본 Jenkins 대시보드에서 다음을 선택합니다. Jenkins 관리 -> 글로벌 도구 구성 를 클릭하고 이동 섹션을 검색합니다.
사용할 수 있는 Go 버전을 정의해야 합니다. 이 프로젝트에서는 1.8 버전만 필요하지만 나머지는 사용자가 결정할 수 있습니다.
다음 단계는 배포를 위한 SSH 키를 구성하는 것입니다. 아직 워크플로를 만드는 것이 아니라 Jenkins를 전체적으로 구성하는 것뿐이라는 점을 기억하세요.
기본 Jenkins 대시보드에서 다음을 선택합니다. Jenkins 관리 -> 시스템 구성 를 클릭하고 SSH 섹션을 찾습니다.
개인 키와 서버 연결 정보를 제공해야 합니다. Jenkins와 원격 서버가 모두 내 네트워크와 동일한 네트워크에 있는 Docker 컨테이너인 경우 로컬 호스트가 아닌 컨테이너 IP 주소 또는 호스트 이름을 사용하는 것을 잊지 마세요.
모든 구성이 완료되면 다음을 선택합니다. 새 항목 를 클릭합니다.
이름을 지정하고 다음을 선택해야 합니다. 프리스타일 프로젝트 를 추가하여 자신만의 워크플로를 추가할 수 있습니다. 이름이 빌드되는 프로젝트 바이너리가 되므로 이름을 메모해 두세요.
이제 워크플로우를 정의할 수 있습니다.
먼저 소스 코드 관리 섹션을 참조하세요. 이 프로젝트는 GitHub이 있으므로 반드시 활용해야 합니다.
이 예제의 Jenkins는 도메인이 아닌 로컬 호스트에서 실행 중이므로 빌드 트리거를 사용하여 실제로 아무것도 할 수 없습니다. 이 예제에서는 수동으로 트리거하겠습니다.
스크립트를 실행하기 전에 앞서 지정한 Go 버전으로 빌드 환경을 설정해야 합니다.
워크플로우가 시작되면 테스트 또는 빌드를 실행하기 전에 해당 버전의 Go를 다운로드하여 설치합니다.
빌드 단계에서는 작업의 흐름을 매우 깔끔하게 유지하기 위해 세 가지 단계를 분리하여 수행합니다.
첫 번째 빌드 단계는 Go 패키지를 다운로드하는 것입니다. 패키지가 준비되면 테스트를 실행할 수 있습니다. 테스트를 실행한 후에는 빌드 시작
를 사용하여 바이너리를 생성합니다. 이 단계 중 하나라도 실패하면 전체 빌드가 실패하는 것이 당연한 이치입니다.
마지막 단계는 배포입니다. 배포는 빌드 후 작업, 바이너리를 SSH를 통해 전송하고 실행하려고 합니다.
이 프로세스에는 실제로 두 개의 전송 세트가 포함됩니다.
첫 번째 단계는 소스 파일(바이너리)을 가져와서 이전에 만든 SSH 프로필을 사용하여 전송하는 것입니다. 파일이 전송되면 파일을 실행할 수 있도록 권한을 변경합니다.
파일이 업로드된 후, 실제로 다른 전송 세트. 두 번째 세트에는 소스 파일 대신 명령어만 있습니다:
1 |
DB_HOST=ec2-34-226-41-140.compute-1.amazonaws.com DB_USER=데모 DB_PASS=123456 DB_BUCKET=예제 ./Golang_CI |
애플리케이션에서 환경 변수로 사용할 변수를 전달하고 있는 것을 주목하세요. 사용 중인 변수로 바꾸거나 보안을 위해 서버에 이러한 변수를 설정하는 등 다른 접근 방식을 고려해 보세요.
이론적으로 웹 애플리케이션을 배포할 때 이 마지막 명령은 연결 정보로 서버를 시작하는 데 사용됩니다.
결론
방금 구성하는 방법을 보셨습니다. 다음을 위한 파이프라인의 Jenkins 및 골랑 지속적인 배포를 지원합니다. 무엇보다도 저희는 실제로 카우치베이스 를 사용하여 원격 서버와 젠킨스 서버를 처리하는 예제입니다. 설정은 다를 수 있지만 단계는 거의 동일합니다.
Couchbase와 함께 Jenkins 및 Go를 사용하는 방법에 대해 자세히 알아보고 싶다면 카우치베이스 개발자 포털.