본문 바로가기
DevOps/Kubernetes

Helm Chart 유효성 검증과 문서화

by 비어원 2023. 10. 3.
728x90

이전 글에서는 Spring boot 애플리케이션을 배포하는 Helm chart를 직접 만들어보았다.

실제로 values.yaml에 특정 값을 잘 넣어서 배포를 해보면 문제는 없어보인다. 하지만 만약 값을 잘못 넣는다면 어떻게 될까? 예를 들어 아래와 같이 probes 부분을 파드의 readinessProbe, livenessProbe 문법에 맞지 않게 값을 넣어버린다면 어떻게 될까?

app:
  name: todo-api
  replicas: 1
  image: beer1/todo-server-kotlin:0.1.0
  port: 9000

probes:
  readiness: 
   a: 1
  liveness:
   b: 2
$ helm install test ./spring-app -f ./spring-app/ci/test.yaml
W1001 17:12:54.207131   42751 warnings.go:70] unknown field "spec.template.spec.containers[0].livenessProbe.b"
W1001 17:12:54.207142   42751 warnings.go:70] unknown field "spec.template.spec.containers[0].readinessProbe.a"
Error: INSTALLATION FAILED: 1 error occurred:
        * Deployment.apps "todo-api" is invalid: [spec.template.spec.containers[0].livenessProbe: Required value: must specify a handler type, spec.template.spec.containers[0].readinessProbe: Required value: must specify a handler type]

 

helm install은 values.yaml에 선언한 값을 사용하여 매니페스트를 만든 후 쿠버네티스에 적용하는데 probes값을 잘못 넣어버린다면 실제로 쿠버네티스에 적용할 때 오류가 발생하게 된다.

 

 

helm ls 명령어를 사용하여 차트 배포가 실패된 것을 확인할 수 있다. 배포가 실패해도 helm 리스트에 차트가 남아있기 때문에 만약 values.yaml의 값을 수정하여 재배포를 하고싶다면 helm install 명령어 대신 helm upgrade 명령어를 사용해야 한다.

$ helm ls
NAME    NAMESPACE    REVISION    UPDATED                                 STATUS    CHART               APP VERSION
test    default      1           2023-10-01 17:12:54.148999 +0900 KST    failed    spring-app-0.1.0    0.1.0

$ helm upgrade -i test ./spring-app -f ./spring-app/ci/test.yaml

 

Helm 유효성 검증

values.yaml에 값을 잘못 넣는다면 결국에 쿠버네티스 API에서 값 유효성을 검증하여 차트 배포가 실패되겠지만, 차트 매니페스트 생성과정에서 값 유효성을 검증하게 만들 수도 있다. helm 자체에서 values.yaml 값에 대해 직접 제약사항을 걸 수 있어서 값을 더 정교하게 구조화 시킬 수 있을 뿐 아니라 조금 더 안전하게 차트를 배포할 수 있다는 장점이 있다.

 

values.schema.json

Helm 유효성 검증을 직접 구성하려면 차트에 values.schema.json 파일을 직접 만들어야 한다. 이전에 만들었던 spring-app 차트에 values.schema.json 파일을 만들어서 유효성 검증을 구성해보자. values.schema.json의 스키마(작성 방법)는 josn-schema 공식 홈페이지를 기반으로 한다. 보다 자세한 내용은 아래 공식홈페이지를 참고하자.

https://json-schema.org/learn/getting-started-step-by-step#create

 

JSON Schema - Creating your first schema

JSON Schema is a vocabulary that you can use to annotate and validate JSON documents. This tutorial guides you through the process of creating a JSON Schema document, including: After you create the JSON Schema document, you can validate the example data a

json-schema.org

 

예시

일단 필자가 만든 예시를 먼저 보자.

{
    "$schema": "https://json-schema.org/draft-07/schema#",
    "properties": {
      "app": {
        "description": "app",
        "type": "object",
        "properties": {
            "name": {
                "type": "string"
            },
            "replicas": {
                "type": "integer",
                "minimum": 0
            },
            "image": {
                "type": "string"
            },
            "port": {
                "type": "integer",
                "minimum": 0,
                "maximum": 65535
            },
            "customCommand": {
                "type": "array",
                "items": {
                    "type": "string"
                }    
            },
            "customArgs": {
                "type": "array",
                "items": {
                    "type": "string"
                }    
            },
            "env": {
                "type": "array",
                "items": {
                    "type": "object"
                }    
            },
            "resources": {
                "type": "object"
            }
        },
        "required": [
            "name",
            "image",
            "port"
        ]
      },
      "podAnnotations": {
        "description": "pod annotations",
        "type": "object"
      },
      "podLabels": {
        "description": "pod labels",
        "type": "object"
      },
      "probes": {
        "description": "pod probes",
        "properties": {
            "rediness": {
                "type": "object"
            },
            "liveness": {
                "type": "object"
            }
        }
      },
      "service": {
        "description": "service",
        "properties": {
            "enabled": {
                "type": "boolean"
            },
            "port": {
                "type": "integer",
                "minimum": 0,
                "maximum": 65535
            }
        }
      },
      "rbac": {
        "description": "rbac",
        "properties": {
            "enabled": {
                "type": "boolean"
            },
            "roleBinding": {
                "properties": {
                    "autoCreate": {
                        "type": "boolean"
                    },
                    "additionalRoles": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        } 
                    },
                    "additionalClusterRoles": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        } 
                    }
                }
            },
            "clusterRoleBinding": {
                "properties": {
                    "clusterRoles": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        } 
                    }
                }
            }
        }
      },
      "applicationYamlConfig": {
        "description": "spring boot application.yaml",
        "properties": {
            "name": {
                "type": "string"
            },
            "value": {
                "type": "string"
            }
        }
      },
      "applicationYamlSecret": {
        "description": "spring boot application.yaml",
        "properties": {
            "name": {
                "type": "string"
            },
            "value": {
                "type": "string"
            }
        }
      },
      "ingress": {
        "description": "ingress",
        "properties": {
            "enabled": {
                "type": "boolean"
            },
            "className": {
                "type": "string"
            },
            "host": {
                "type": "string"
            },
            "path": {
                "type": "string"
            },
            "pathType": {
                "type": "string",
                "enum": [ "Prefix", "Exact" ]
            }
        }
      }
    },
    "required": [
      "app"
    ],
    "title": "Values",
    "type": "object"
}

일단 루트에 있는 필드들만 살펴보자.

{
    "$schema": "https://json-schema.org/draft-07/schema#",
    "title": "Values",
    "type": "object",
    "properties": { },
    "required": [
      "app"
    ]
}  

 

루트에는 총 5가지의 필드가 있다.

Field Type Description
$schema String 스키마가 준수하는 JSON 스키마 표준 초안을 지정한다.
title String 스키마의 의도를 명시한다.
type String 프로퍼티에 타입, "integer", "number", "boolean", "string", "object", "array", "null" 중 하나
properties Object Object 타입의 경우 하위 프로퍼티에 대한 정보
required Array 필수인 프로퍼티 이름을 Array 형태로 기술

 

title은 굳이 Values로 안해도 스키마 검증이 잘 이루어지는데, 아무 이름으로 해도 상관없을 것 같다. 대신 템플릿 변수에서는 {{ .Values.XXX }}로 접근하기 때문에 관례상 Values로 적어두는 것이 좋을 것 같다.

루트의 properties 변수에서는 values.yaml에 기술하는 템플릿 변수들에 대한 유효성을 구성하면 된다.

 

app

먼저 app 필드에 대한 유효성을 구성해보자. 일단 app 필드 구조는 다음과 같다고 하자.

app:
  name: todo-api
  replicas: 1
  image: beer1/todo-server-kotlin:0.1.0
  port: 9000
  customCommand: []
  customArgs: []
  env: []
  resources: {}

일단 app 필드 자체는 객체 타입으로 구성되어 있고, 프로퍼티로는 name, replicas 등이 있다. 그리고 프로퍼티 중 name, image는 문자열이여야 하며 replicas, port는 숫자타입이어야 한다. 그리고 customCommand, customArgs, env는 배열타입이고, resources는 객체타입이어야 한다. 이런 제약조건들을 json으로 나타내면 다음과 같다.

 

{
    "$schema": "https://json-schema.org/draft-07/schema#",
    "properties": {
      "app": {
        "description": "app",
        "type": "object",
        "properties": {
            "name": {
                "type": "string"
            },
            "replicas": {
                "type": "integer",
            },
            "image": {
                "type": "string"
            },
            "port": {
                "type": "integer",
            },
            "customCommand": {
                "type": "array", 
            },
            "customArgs": {
                "type": "array",
            },
            "env": {
                "type": "array",
            },
            "resources": {
                "type": "object"
            }
        }
      }
    },
    "title": "Values",
    "type": "object"
}    

 

배열 요소 타입 제약사항

그런데 customCommand, customArgs, env는 문자열 타입의 배열타입이어야 한다. 배열 안 요소에 대한 타입을 검증할 수 있으면 좋겠다. 만약 프로퍼티 타입이 array인 경우, items.type 필드를 통해 배열 요소에 대한 타입을 제약사항으로 추가할 수 있다.

"customCommand": {
    "type": "array",
    "items": {
        "type": "string"
    }    
},
"customArgs": {
    "type": "array",
    "items": {
        "type": "string"
    }    
},
"env": {
    "type": "array",
    "items": {
        "type": "string"
    }    
}

 

customCommand, customArgs, env 모두 문자열 타입 배열이기 때문에 items.type은 string으로 설정하였다. 만약 이 때 values.yaml에 app.customArgs를 숫자형 타입으로 줄 경우 아래 에러가 발생한다.

app:
  name: todo-api
  replicas: 1
  image: beer1/todo-server-kotlin:0.1.0
  port: 9000
  customArgs:
  - 1
  - 2
$  helm template test ./spring-app -f ./spring-app/ci/test.yaml
Error: values don't meet the specifications of the schema(s) in the following chart(s):
spring-app:
- app.customArgs.0: Invalid type. Expected: string, given: integer
- app.customArgs.1: Invalid type. Expected: string, given: integer

 

숫자타입 최솟값, 최댓값

추가로, 더 디테일하게 replicas, port에 대한 제약사항도 추가해보자. 먼저 replicas는 0이상의 정수여야 한다. 그리고 port는 0부터 65535까지 가능하기 때문에 port의 최솟값, 최댓값을 지정해주면 더 좋다. 이러한 제약사항을 json으로 나타내면 다음과 같다.

"replicas": {
    "type": "integer",
    "minimum": 0
},
"port": {
    "type": "integer",
    "minimum": 0,
    "maximum": 65535
}

 

마찬가지로 필드의 최솟값이나 최댓값을 넘어버리는 값을 선언했을 경우에는 다음과 같은 에러가 발생한다.

app:
  name: todo-api
  replicas: 1
  image: beer1/todo-server-kotlin:0.1.0
  port: 90002
$ helm template test ./spring-app -f ./spring-app/ci/test.yaml
Error: values don't meet the specifications of the schema(s) in the following chart(s):
spring-app:
- app.port: Must be less than or equal to 65535

 

필수 여부

그 다음 프로퍼티의 필수 여부를 설정해보자. 필수값으로 들어가야하는 프로퍼티 이름을 required 배열 안에 넣기만 하면 된다. app 프로퍼티의 필수값은 name, image, port만이 필요하다고 가정하자. 최종적으로 app에 대해서는 다음과 같다.

{
    "$schema": "https://json-schema.org/draft-07/schema#",
    "properties": {
      "app": {
        "description": "app",
        "type": "object",
        "properties": {
            "name": {
                "type": "string"
            },
            "replicas": {
                "type": "integer",
                "minimum": 0
            },
            "image": {
                "type": "string"
            },
            "port": {
                "type": "integer",
                "minimum": 0,
                "maximum": 65535
            },
            "customCommand": {
                "type": "array",
                "items": {
                    "type": "string"
                }    
            },
            "customArgs": {
                "type": "array",
                "items": {
                    "type": "string"
                }    
            },
            "env": {
                "type": "array",
                "items": {
                    "type": "string"
                }    
            },
            "resources": {
                "type": "object"
            }
        },
        "required": [
            "replicas",
            "image",
            "port"
        ]
      }
    },
    "title": "Values",
    "type": "object"
}

 

그리고 차트에 있는 기본 values.yaml에서 app.name, app.image, app.port를 없애보자. 그 다음에 실제 배포용으로 사용할 values.yaml의 app.image를 주석처리하여 배포하면 다음과 같은 에러가 발생할 것이다.

app:
  name: todo-api
  replicas: 1
  #image: beer1/todo-server-kotlin:0.1.0
  port: 9000
$ helm template test ./spring-app -f ./spring-app/ci/test.yaml
Error: values don't meet the specifications of the schema(s) in the following chart(s):
spring-app:
- app: image is required

 

사실 spring-app 차트는 spring 애플리케이션을 배포하기 위해 사용하는 차트이기 때문에 애플리케이션 고유값인 app.name, app.image, app.port는 기본 values.yaml에서 선언하여 기본값을 부여해주는 것은 바람직하지 않고 실제 배포용 values.yaml에서 선언하는 것이 맞다. 이렇게 애플리케이션 고유값을 필수값으로 제약사항을 걸고 기본값을 제공해주지 않으면서 사용자가 실제 배포할 때 고유값을 깜빡하는 일이 없도록 경고문을 띄워줄 수 있다.

 

나머지 필드도 반복

app을 제외한 나머지 필드도 이와 비슷하게 제약사항을 걸어둘 수 있다. 직접 몇번 해보다보면 쉽게 제약사항을 적용해볼 수 있을 것이다. 필자가 설명한 제약사항 외에도 추가로 더 디테일한 기능을 제공해주기 때문에 (문자열 정규식 제약사항 등) 필요한 것이 있다면 공식문서를 참고해보는 것도 좋은 방법이 될 수 있다.

 

Enum 타입

추가로, ingress 필드를 보면 ingress.pathType 이라는 필드가 있다. 이 필드는 사실 쿠버네티스에서는 ExactPrefix 중 하나의 값으로만 선언되어야 한다. 이렇게 특정 몇 개의 문자열 값만 허용되는 필드는 Enum 타입으로 간주된다. 특정 필드를 Enum 타입으로 제한시키고, 허용되는 Enum값을 정하는 것도 가능하다.

"ingress": {
    "description": "ingress",
    "properties": {
        ...
        "pathType": {
            "type": "string",
            "enum": [ "Prefix", "Exact" ]
        }
    }
}

 

문서화

차트 제약사항을 걸어주는 것 뿐만이 아니라 values.yaml에 들어갈 수 있는 값에 대한 문서를 작성해주면 더욱 사용자에게 친절한 차트가 될 것이다. 차트에 대한 values.yaml 문서는 보통 README.md에 작성한다. 문서 작성법은 정해진 것은 없으며 마크다운 형식으로 자유롭게 작성해주면 된다. 대신 사용자 친화적으로 잘 작성하기 위해 고민한다면 이미 잘 알려져있는 차트의 문서를 보고 따라하는 것도 좋은 방법이 될 수 있다.

 

대표적으로는 bitnami의 helm chart가 잘 알려져있는 차트이다. 차트의 깃 레포지토리로 들어가보면 문서를 어떻게 작성했는지 볼 수 있기 때문에 참고하면 좋다.

 

https://github.com/bitnami/charts/tree/main/bitnami

 

 

spring-app 차트의 문서의 완성본은 github에 게시하였다.

https://github.com/beer-one/charts/blob/main/spring-app

 

728x90

댓글