Gitops 기본개념

2021-11-28

.

Data_engineering_TIL(20211127)

[학습자료]

“클라우드 네이티브를 위한 쿠버네티스 실전 프로젝트”책을 읽고 정리한 내용입니다.

  • 동양북스, 아이자와 고지&사토 가즈히코 지음, 박상욱 옮김

참고자료 URL : https://github.com/dybooksIT/k8s-aws-book

“EKS 클러스터를 컨트롤하기 위한 클라이언트 IAM 인증 기본개념”에 이어서 공부한 내용을 정리한 내용임

    • URL : https://minman2115.github.io/DE_TIL310

참고로 실습을 진행하려면 https://minman2115.github.io/DE_TIL251 (Cloudformation으로 간단하게 EKS cluster 구동하기) 부터 https://minman2115.github.io/DE_TIL258 (EKS 클러스터에 프런트앤드 어플리케이션 배포하기) 까지 완료된 상태로 진행해야 함

[학습내용]

  • 깃옵스 파이프라인 개념도

쿠버네티스는 기본적으로 모든 설정정보를 매니페스트라는 야믈 파일로 정의한다. 모든설정을 코드로 관리할 수 있다면 어플리케이션과 마찬가지로 CICD를 구현할 수 있다. 깃옵스 파이프라인은 쿠버네티스의 매니페스트도 레포지토리로 관리하고 pull request 요청기반으로 쿠버네티스 클러스터 운영환경으로 배포할수 있다. 또한 레포지토리의 매니페스트 파일 외에는 업데이트를 인정하지 않는 구조를 도입하여 클러스터 관리를 통제할 수는 구조이다.

1

깃옵스라는 개념이 만들어지면서 쿠버네티스의 설정정보 적용을 자동화해주는 툴도 활발하게 개발되었다.

대표적인 예시)

1) Weave Cloud : 깃옵스를 만든 위브웍스에서 제공하는 CD 툴

2) Spinnaker : 넷플릭스가 자사의 쿠버네티스 환경에서 CD를 구현하기 위해 개발한 툴

3) Skaffold : 구글이 개발한 쿠버네티스용 CD 툴

  • AWS에서 제공하는 CodePipeline 서비스를 이용한 깃옵스 구현

AWS에서도 CodePipeline라는 CI/CD를 구현하는 서비스를 제공한다. 소스코드 작성, 빌드, 배포로 이어지는 일련의 단계를 정의하고 걱종 설정을 부여하여 사용자가 원하는 CI/CD 파이프라인을 구성할 수 있도록 도와주는 서비스이다.

2

STEP 1) 어플리케이션 코드를 수정하고 CodeCommit이라는 서비스에서 제공하는 레포지토리에 push

STEP 2) CodeCommit이 변경사항을 감지하고 파이프라인 동작 시작

STEP 3) next step trigger

STEP 4) application build 수행

STEP 5) 컨테이너 이미지를 생성하여 ECR에 push

STEP 6) next step trigger

STEP 7) ECR의 컨테이너 이미지를 EKS에 자동으로 배포

여기서 유의할점은 어플리케이션 소스코드 레포지토리와 EKS 매니페스트를 관리하는 레포지토리는 별도로 관리해야하고, 어플리케이션 빌드단계와 EKS로의 배포단계도 분리하여 실행해야 한다.

[실습내용]

STEP 1) cloudformation을 이용해서 cicd 관련 리소스들을 생성

아래 그림과 같이 cloudformation을 이용해서 cicd 관련 리소스들을 생성한다. 여기서 주의할 점은 스택이름은 아래 그림과 같이 ‘eks-book-sample-ap-slack’으로 해주고, 파라미터들도 아래 그림과 같이 해주면 된다.

3

STEP 2) EKS 클러스터 액세스 권한에 CodeBuild의 IAM role을 추가

CodePipeline의 CodeBuild 프로젝트 안에서 EKS 클러스터에 매니페스트를 적용한다. 그러기 위해서 Codebuild가 kubectl 관련 명령을 사용해서 설정내용을 적용할 수 있도록 kubectl 명령어의 인증정보를 생성해야 한다. 인증정보 설정은 아래와 같이 ec2 client에서 진행하자.

# codebuild가 EKS 클러스터를 조작하기 위한 인증정보 설정
# --arn 파라미터는 cloudformation의 eks-book-sample-ap-slack 스택 'output' 탭에 표시된 ARN 값을 복붙해주면 된다.
[ec2-user@ip-10-10-1-91 ~]$ eksctl create iamidentitymapping --region ap-northeast-2 --username codebuild --group system:masters --cluster eks-work-cluster --arn arn:aws:iam::xxxxxxxxxxxxx:role/EKS-SampleAP-CodeBuildServiceRole
2021-11-28 06:26:42 [ℹ]  eksctl version 0.75.0
2021-11-28 06:26:42 [ℹ]  using region ap-northeast-2
2021-11-28 06:26:42 [ℹ]  adding identity "arn:aws:iam::xxxxxxxxxxxxx:role/EKS-SampleAP-CodeBuildServiceRole" to auth ConfigMap

EKS인증은 IAM 인증과 통합되어 있다. 다시말해서 codebuild가 사용하는 IAM role의 ARN을 EKS 클러스터에 추가하는 형태이다. 또 codebuild가 사용하는 IAM 역할의 ARN은 cloudformation의 eks-book-sample-ap-slack 스택의 output 탭의 yourcodebuildservicerolearn 항목에서 확인할 수 있다.

STEP 3) 예제 소스코드를 AWS CodeCommit에 push 하도록 설정

당연한 얘기겠지만 https://github.com/dybooksIT/k8s-aws-book.git 레포를 실습에 바로 사용할 수 없다. 실습용 AWS CodeCommit를 사용하면 되는데 먼저 CodeCommit용 깃 인증정보를 생성해야 한다. IAM 페이지에서 생성할 수 있다. 현재 eks client에 박혀있는 크레딧 키 IAM 사용자 콘솔로 접속해서 아래와 같이 설정해준다.

4

그런 다음에 소스코드 레포지토리 내부 깃설정을 codecommit으로 변경해줘야 한다. client에 클론한 예제 어플리케이션 코드의 push 대상을, 생성한 codecommit 레포지토리로 변경하는 것이다. eks client에서 아래와 같이 명령어를 실행해준다.

[ec2-user@ip-10-10-1-91 ~]$ cd k8s-aws-book/

# 예제 레포지토리의 리모트 설정 변경 
[ec2-user@ip-10-10-1-91 k8s-aws-book]$ git remote rename origin upstream

# origin의 리모트 대상을 새로 생성한 codecommit의 레포지토리로 변경
# url 값은 cloudformation에서 output 탭의 yourcodecommitrepositoryurl 항목에서 확인할 수 있다.
[ec2-user@ip-10-10-1-91 k8s-aws-book]$ git remote add origin https://git-codecommit.ap-northeast-2.amazonaws.com/v1/repos/eks-work-cicd-repo-pms

STEP 4) 예제 어플리케이션 수정

[ec2-user@ip-10-10-1-91 k8s-aws-book]$ cd backend-app/src/main/java/k8sbook/sampleapp/presentation/dto

[ec2-user@ip-10-10-1-91 dto]$ ll
total 20
-rw-rw-r-- 1 ec2-user ec2-user  243 Nov 28 05:35 HealthDto.java
-rw-rw-r-- 1 ec2-user ec2-user 1154 Nov 28 05:35 LocationDto.java
-rw-rw-r-- 1 ec2-user ec2-user  546 Nov 28 05:35 LocationsDto.java
-rw-rw-r-- 1 ec2-user ec2-user  678 Nov 28 05:35 RegionDto.java
-rw-rw-r-- 1 ec2-user ec2-user  512 Nov 28 05:35 RegionsDto.java

[ec2-user@ip-10-10-1-91 dto]$ vim RegionDto.java

#################################################################
# ASIS
#################################################################

package k8sbook.sampleapp.presentation.dto;


import k8sbook.sampleapp.domain.model.Region;

public class RegionDto {

    private Integer regionId;

    private String regionName;

    public RegionDto() {
    }

    public RegionDto(Region region) {
        this.regionId = region.getRegionId();
        this.regionName = region.getRegionName();   // 이 부분을 수정
    }

    public Integer getRegionId() {
        return regionId;
    }

    public void setRegionId(Integer regionId) {
        this.regionId = regionId;
    }

    public String getRegionName() {
        return regionName;
    }

    public void setRegionName(String regionName) {
        this.regionName = regionName;
    }
}

#################################################################
# TOBE
#################################################################

[ec2-user@ip-10-10-1-91 dto]$ vim RegionDto.java
package k8sbook.sampleapp.presentation.dto;


import k8sbook.sampleapp.domain.model.Region;

public class RegionDto {

    private Integer regionId;

    private String regionName;

    public RegionDto() {
    }

    public RegionDto(Region region) {
        this.regionId = region.getRegionId();
        this.regionName = "*" + region.getRegionName();
    }

    public Integer getRegionId() {
        return regionId;
    }

    public void setRegionId(Integer regionId) {
        this.regionId = regionId;
    }

    public String getRegionName() {
        return regionName;
    }

    public void setRegionName(String regionName) {
        this.regionName = regionName;
    }
}

# 그런 다음에 아래와 같이 어플리케이션 버전 정보도 바꿔준다.

[ec2-user@ip-10-10-1-91 dto]$ cd /home/ec2-user/k8s-aws-book/backend-app

[ec2-user@ip-10-10-1-91 backend-app]$ ll
total 28
drwxrwxr-x 9 ec2-user ec2-user  113 Nov 28 05:37 build
-rw-rw-r-- 1 ec2-user ec2-user 1290 Nov 28 05:35 build.gradle
-rw-rw-r-- 1 ec2-user ec2-user  395 Nov 28 05:35 Dockerfile
drwxrwxr-x 3 ec2-user ec2-user   21 Nov 28 05:35 gradle
-rw-rw-r-- 1 ec2-user ec2-user  101 Nov 28 05:35 gradle.properties
-rwxr-xr-x 1 ec2-user ec2-user 5305 Nov 28 05:35 gradlew
-rw-rw-r-- 1 ec2-user ec2-user 2185 Nov 28 05:35 gradlew.bat
drwxrwxr-x 2 ec2-user ec2-user  103 Nov 28 05:35 scripts
-rw-rw-r-- 1 ec2-user ec2-user   65 Nov 28 05:35 settings.gradle
drwxrwxr-x 4 ec2-user ec2-user   30 Nov 28 05:35 src

[ec2-user@ip-10-10-1-91 backend-app]$ cat build.gradle

#################################################################
# ASIS
#################################################################

plugins {
    id 'org.springframework.boot' version '2.2.5.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}

group = 'k8sbook'
version = '1.0.0'                  // 이 버전정보만 수정
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation project(':sample-app-common')
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.apache.commons:commons-lang3:3.9'
    runtimeOnly 'org.postgresql:postgresql'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'com.ninja-squad:DbSetup:2.1.0'
    testImplementation 'org.assertj:assertj-db:1.3.0'
}

test {
    useJUnitPlatform {
        excludeTags 'DBRequired'
    }
    environment "DB_URL", localDbUrl
    environment "DB_USERNAME", localDbUsername
    environment "DB_PASSWORD", localDbPassword
}

task testWithDatabase(type: Test, dependsOn: testClasses) {
    useJUnitPlatform {
    }
    environment "DB_URL", localDbUrl
    environment "DB_USERNAME", localDbUsername
    environment "DB_PASSWORD", localDbPassword
}

bootJar {
    launchScript()
}

#################################################################
# TOBE
#################################################################

plugins {
    id 'org.springframework.boot' version '2.2.5.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}

group = 'k8sbook'
version = '1.0.1'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation project(':sample-app-common')
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.apache.commons:commons-lang3:3.9'
    runtimeOnly 'org.postgresql:postgresql'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'com.ninja-squad:DbSetup:2.1.0'
    testImplementation 'org.assertj:assertj-db:1.3.0'
}

test {
    useJUnitPlatform {
        excludeTags 'DBRequired'
    }
    environment "DB_URL", localDbUrl
    environment "DB_USERNAME", localDbUsername
    environment "DB_PASSWORD", localDbPassword
}

task testWithDatabase(type: Test, dependsOn: testClasses) {
    useJUnitPlatform {
    }
    environment "DB_URL", localDbUrl
    environment "DB_USERNAME", localDbUsername
    environment "DB_PASSWORD", localDbPassword
}

bootJar {
    launchScript()
}

STEP 5) ECR의 URL과 버전정보를 업데이트하도록 매니페스트 파일 수정

EKS 클러스터에 매니페스트를 적용하고 새로운 도커 이미지를 ECR에 pull하여 어플리케이션을 배포를 해보자. 먼저 아래와 같이 kustomization.yaml에서 .images.newTag와 .images.newName값을 설정해준다.

[ec2-user@ip-10-10-1-91 backend-app]$ cd /home/ec2-user/k8s-aws-book/cicd/kustomization/prod

[ec2-user@ip-10-10-1-91 prod]$ ll
total 4
-rw-rw-r-- 1 ec2-user ec2-user 305 Nov 28 05:35 kustomization.yaml

[ec2-user@ip-10-10-1-91 prod]$ vim kustomization.yaml

#################################################################
# ASIS
#################################################################

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

bases:
- ../base  # 기반이 되는 매니페스트 장소

images:
  - name: backend-app-image  # 여기는 바꾸지 않음
    newTag: x.x.x  # 애플리케이션 버전 번호
    newName: <Your_registry_URI> # ECR 레지스트리 URI

#################################################################
# TOBE
# newName은 ECR에서 k8sbook/backend-app 항목의 URL을 넣어주면 된다.
#################################################################

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

bases:
- ../base

images:
  - name: backend-app-image
    newTag: 1.0.1
    newName: xxxxxxxxxx.dkr.ecr.ap-northeast-2.amazonaws.com/k8sbook/backend-app

STEP 6) CodeCommit에 push

아래와 같이 변경내용을 commit하고 codecommit 레포지토리로 push해준다.

[ec2-user@ip-10-10-1-91 prod]$ cd /home/ec2-user/k8s-aws-book

[ec2-user@ip-10-10-1-91 k8s-aws-book]$ git status
On branch master
Your branch is up to date with 'upstream/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   backend-app/build.gradle
        modified:   backend-app/src/main/java/k8sbook/sampleapp/presentation/dto/RegionDto.java
        modified:   cicd/kustomization/prod/kustomization.yaml
        modified:   frontend-app/package-lock.json
        modified:   frontend-app/package.json

no changes added to commit (use "git add" and/or "git commit -a")

[ec2-user@ip-10-10-1-91 k8s-aws-book]$ git add -A

[ec2-user@ip-10-10-1-91 k8s-aws-book]$ git commit -m "pms test commit"
[master a7d3b11] pms test commit
 Committer: EC2 Default User <ec2-user@ip-10-10-1-91.ap-northeast-2.compute.internal>
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly. Run the
following command and follow the instructions in your editor to edit
your configuration file:

    git config --global --edit

After doing this, you may fix the identity used for this commit with:

    git commit --amend --reset-author

 5 files changed, 8260 insertions(+), 10923 deletions(-)

# push할때 깃 인증정보를 물어보면 아까 codecommit 크레딧 액셀파일 다운로드 받은거 열어서 아이디 패스워드 복붙해주면 된다.
[ec2-user@ip-10-10-1-91 k8s-aws-book]$ git push origin master
Username for 'https://git-codecommit.ap-northeast-2.amazonaws.com': minsupark-at-xxxxxxxxx
Password for 'https://minsupark-at-xxxxxxxxx@git-codecommit.ap-northeast-2.amazonaws.com':
Enumerating objects: 455, done.
Counting objects: 100% (455/455), done.
Delta compression using up to 2 threads
Compressing objects: 100% (247/247), done.
Writing objects: 100% (455/455), 1.85 MiB | 12.46 MiB/s, done.
Total 455 (delta 143), reused 443 (delta 139), pack-reused 0
To https://git-codecommit.ap-northeast-2.amazonaws.com/v1/repos/eks-work-cicd-repo-pms
 * [new branch]      master -> master

STEP 7) 자동으로 EKS에 배포된 것을 확인

아래 그림과 같이 CodePipeline 콘솔에 가보면 파이프라인이 구동되는 것을 알 수 있다. 배포시간은 약 7분정도 소요된다.

5

# 신규 파드 2개가 생성된 것을 확인할 수 있다.
[ec2-user@ip-10-10-1-91 k8s-aws-book]$ kubectl get pod
NAME                           READY   STATUS    RESTARTS   AGE
backend-app-56bdf84ddf-9sglb   1/1     Running   0          4m28s
backend-app-56bdf84ddf-mb66z   1/1     Running   0          2m30s

# 파드 2개 모두 버전이 1.0.0 --> 1.0.1로 변경된 것을 알 수 있다.
[ec2-user@ip-10-10-1-91 k8s-aws-book]$ kubectl describe pod | grep Image:
    Image:          161461013751.dkr.ecr.ap-northeast-2.amazonaws.com/k8sbook/backend-app:1.0.1
    Image:          161461013751.dkr.ecr.ap-northeast-2.amazonaws.com/k8sbook/backend-app:1.0.1

아래 그림과 같이 프런트 앤드 페이지에 접속을 해보니 어플리케이션이 실제로 업데이트 된 것을 확인할 수 있다.

6

  • kustomize를 이용한 실전배포

실무 개발 프로세스에 맞춰 파이프라인을 만들려고 하면 환경에 따라 다른 정보들을 동적으로 변경시켜서 사용하고자 할 것이다. 쿠버네티스 매니페스트는 선언적인 상태를 정의한다는 특징이 있기 때문에 동적으로 값을 변경하는 문법은 사실 따로 없다. 하지만 kustomize는 동적으로 값을 변경하고 싶은 요구사항을 도와주는 구조를 제공한다.

kustomize는 kubectl의 서브 명령이다. kubectl kustomize 명령을 사용하면 여러개의 매니페스트나 환경에 의존적인 설정값을 합해 매니페스트 하나로 만들어준다. 먼저 기본이 되는 매니페스트 파일을 준비한다. 그리고 디렉토리 각각에 kustomization.yaml 이라는 설정파일과 환경별로 변경할 설정정보를 담은 매니페스트 파일을 준비한다.

# 디렉토리 각각에 kustomization.yaml 이라는 설정파일과 환경별로 변경할 설정정보를 담은 매니페스트 파일이 준비된 예시
# k8s-aws-book 레포에 이미 다 구현되어 있음
[ec2-user@ip-10-10-1-91 k8s-aws-book]$ tree cicd/kustomization
cicd/kustomization
├── base
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
├── prod
│   └── kustomization.yaml
└── test
    ├── deployment.yaml
    └── kustomization.yaml

3 directories, 6 files

[ec2-user@ip-10-10-1-91 k8s-aws-book]$ cat cicd/kustomization/test/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

bases:
- ../base
patches:
- deployment.yaml

nameSuffix: -test
commonLabels:
  app: backend-app-test

images:
  - name: centos
    newTag: x.x.x  # 애플리케이션 버전 번호
    newName: <yourregistry_name>  # ECR 레지스트리 URI

[ec2-user@ip-10-10-1-91 k8s-aws-book]$ cat cicd/kustomization/test/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-app
spec:
  replicas: 1

[ec2-user@ip-10-10-1-91 k8s-aws-book]$ cat cicd/kustomization/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

bases:
- ../base

images:
  - name: backend-app-image
    newTag: 1.0.1
    newName: xxxxxxxxx.dkr.ecr.ap-northeast-2.amazonaws.com/k8sbook/backend-app

kustomization.yaml 내에는 기본 매니페스트가 어디에 있는지 등의 기본설정을 작성한다. prod 디렉토리 안에 kustomization.yaml은 cicd 파이프라인 설정정보가 작성되어 있다. 기본 매니페스트는 ../base 디렉토리 아래로 하고 이미지 저장소인 ECR의 URL 버전번호 태그를 작성해둠으로써 매니페스트 자체 내용을 덮어쓰기 한다. 또한 test 디렉토리의 kustomization.yaml에는 기본 매니페스트가 ../base 디렉토리 아래에 있다는 내용뿐만 아니라 거기에 있는 deployment.yaml 내용을 덮어쓰도록 정의도어 있다. 그리고 테스트 환경용 리소스임을 알수 있도록 공용레이블을 붙이고 생성할 리소스에는 -test라는 공통의 접미사를 붙이도록 하고 있다.

아래와 같이 cicd/kustomization 디렉토리에서 kubectl apply -k test 명령어를 실행하면 테스트 환경용으로 설정된 매니페스트를 클러스터에 적용할 수 있다. 또 적용하기 전에 동적으로 생성된 매니페스트를 확인하고 싶을 경우 kubectl kustomize test 명령어를 실행하면 된다. 표준 출력으로 매니페스트를 표시할 수 있다.

[ec2-user@ip-10-10-1-91 k8s-aws-book]$ cd cicd/kustomization/

[ec2-user@ip-10-10-1-91 kustomization]$ kubectl kustomize test
apiVersion: v1
kind: Service
metadata:
  labels:
    app: backend-app-test
  name: backend-app-service-test
spec:
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    app: backend-app-test
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: backend-app-test
  name: backend-app-test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend-app-test
  template:
    metadata:
      labels:
        app: backend-app-test
    spec:
      containers:
      - env:
        - name: DB_URL
          valueFrom:
            secretKeyRef:
              key: db-url
              name: db-config
        - name: DB_USERNAME
          valueFrom:
            secretKeyRef:
              key: db-username
              name: db-config
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              key: db-password
              name: db-config
        image: backend-app-image
        imagePullPolicy: Always
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 30
        name: backend-app
        ports:
        - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 30
        resources:
          limits:
            cpu: 250m
            memory: 768Mi
          requests:
            cpu: 100m
            memory: 512Mi

이러한 구조를 파이프라인에 도입하면 환경에 따라 변경되는 값들을 동적으로 변경하는 파이프라인을 구성할 수 있다. 예를 들어서 test 브랜치에 push 했을때 test 디렉토리 설정내용을 적용하고, master(또는 main) 브랜치에 push된 경우 prod 디렉토리의 설정내용을 적용하도록 파이프라인을 만들어두면, 실제 개발환경의 프로세스에 맞춘 CI/CD 환경을 구현할 수 있다.

7