스타일 가이드

이 문서는 GitLab Helm 차트 개발을 위한 다양한 지침과 모범 사례를 설명합니다.

명명 규칙

우리는 함수 이름과 values.yaml에서 사용되는 속성에 대해 camelCase를 사용합니다.

예: gitlab.assembleHost

템플릿 함수는 관련된 차트에 따라 네임스페이스에 배치되며, 대상 파일에서 영향을 받는 값과 일치하도록 이름이 지정됩니다. 차트 전역 함수는 일반적으로 gitlab.* 네임스페이스에 속합니다.

예시:

  • gitlab.redis.host: gitlab 차트의 일환으로 Redis 서버의 호스트 이름을 제공합니다.
  • registry.minio.url: registry 차트의 일환으로 MinIO 호스트에 대한 URL을 제공합니다.

values.yaml에 대한 일반 구조

많은 차트는 동일한 정보를 제공해야 하며, 예를 들어 Redis 및 PostgreSQL 연결 설정을 여러 차트에 제공해야 합니다. 여기에서는 이러한 설정에 대한 우리의 표준 명명 및 구조를 설명합니다.

다른 서비스에 연결

redis:
  host: redis.example.com
  serviceName: redis
  port: 8080
    sentinels:
    - host: sentinel1.example.com
      port: 26379
  password:
    secret: gitlab-redis
    key: redis-password
  • redis - 현재 차트가 연결해야 하는 대상을 위한 이름
  • host - serviceName의 사용을 재정의하며, 기본적으로 0.0.0.0을 주석 처리하여 사용합니다. Redis Sentinels를 사용하는 경우, host 속성은 sentinel.conf에 지정된 클러스터 이름으로 설정해야 합니다.
  • serviceName - 기본적으로 호스트 대신 사용하도록 되어 있으며, Kubernetes 서비스 이름으로 연결합니다.
  • port - 연결할 포트입니다. 기본적으로 주석 처리되며, 기본 포트를 예로 사용합니다.
  • password - 비밀번호를 포함하는 Kubernetes Secret에 대한 설정을 정의합니다.
  • sentinels.[].host - Redis HA 설정을 위한 Redis Sentinel 서버의 호스트 이름을 정의합니다.
  • sentinels.[].port - Redis Sentinel 서버에 연결할 포트를 정의합니다. 기본값은 26379입니다.

참고: 현재 Redis Sentinel 지원은 GitLab 차트와 별도로 배포된 Sentinels만 지원합니다. 결과적으로 GitLab 차트를 통한 Redis 배포는 redis.install=false로 비활성화해야 합니다. Redis 비밀번호를 포함하는 Secret은 GitLab 차트를 배포하기 전에 수동으로 생성해야 합니다.

비밀번호 공유

우리는 비밀번호와 같은 민감한 정보를 저장하고 서로 다른 차트/pods 간에 공유하기 위해 비밀을 사용합니다.

우리가 사용하는 일반 필드는 다음과 같습니다:

  • TLS/SSL 인증서 - TLS/SSL 인증서 공유
  • 비밀번호 - Redis 비밀번호 공유
  • 인증 토큰 - 서비스 간 인증 토큰 공유
  • 기타 비밀 - JWT 인증서 및 서명 키와 같은 기타 비밀 공유

TLS/SSL 인증서

TLS/SSL 인증서는 유효한 Kubernetes TLS Secret이어야 합니다.

예를 들어, 레지스트리를 설정하려면:

registry:
  tls:
    secretName: <TLS secret name>

TLS 인증서가 차트 간에 공유될 때는 전역 값으로 정의되어야 합니다.

global:
  ingress:
    tls:
      secretName: <TLS secret name>

비밀번호

예를 들어 redis가 소유하는 차트이고, 다른 차트가 redis 비밀번호를 참조해야 할 경우를 생각해보세요.

소유하는 차트는 다음과 같이 비밀번호 시크릿을 정의해야 합니다:

password:
  secret: <secret name>
  key: <key name inside the secret to fetch>

다른 차트는 다음과 같이 동일한 비밀번호 시크릿을 공유해야 합니다:

redis:
  password:
    secret: <secret name>
    key: <key name inside the secret to fetch>

인증 토큰

소유하는 차트는 다음과 같이 authToken 시크릿을 정의해야 합니다:

authToken:
  secret: <secret name>
  key: <key name inside the secret to fetch>

다른 차트는 다음과 같이 동일한 비밀번호 시크릿을 공유해야 합니다:

gitaly:
  authToken:
    secret: <secret name>
    key: <key name inside the secret to fetch>

예를 들어, gitaly가 소유하는 차트이고, 다른 차트가 gitaly authToken을 참조해야 할 경우를 생각해보세요.

기타 시크릿

registry의 JWT 서명 인증서 또는 gitaly GPG 서명 키와 같은 기타 시크릿은 authTokenpassword 시크릿과 동일한 형식을 사용합니다.

이러한 시크릿을 한 차트에서 다른 차트로 공유하기 위해, 아래 예제와 유사한 구성을 제공해야 하며, 여기서 registry JWT 서명 인증서를 다른 차트와 공유합니다.

소유하는 차트는 다음과 같이 시크릿을 정의해야 합니다:

certificate:
  secret: <secret name>
  key: <key name inside the secret to fetch>

다른 차트는 다음과 같이 동일한 시크릿을 공유해야 합니다:

registry:
  certificate:
    secret: <secret name>
    key: <key name inside the secret to fetch>

함수 사용에 대한 선호도

우리는 gotmpl, Sprig 및 Helm에서 사용할 수 있는 다양한 함수에 관한 이러한 차트를 개발하기 위한 일련의 선호도를 발전시켰습니다. 다음 섹션에서는 이러한 선호도와 그에 대한 이유를 설명합니다.

indent 대신 nindent 사용

가능한 경우, indent 함수 대신 nindent 함수를 사용하는 것이 좋습니다.

이 선호도는 가독성에 기반하고 있으며, 특히 우리와 같은 복잡한 Helm 차트의 경우 더욱 그러합니다.

nindent 사용이 커뮤니티 전반에 걸쳐 선호되며, 현재는 helm create 명령으로 생성된 템플릿의 기본값이기도 합니다.

다음 두 개의 코드 스니펫 예제를 살펴보세요. 이들은 그 이유를 쉽게 설명합니다:

읽기 쉬운 예

  gitlab.yml.erb: |
    production: &base
      gitlab:
        host: {{ template "gitlab.gitlab.hostname" . }}
        https: {{ hasPrefix "https://" (include "gitlab.gitlab.url" .) }}
        {{- with .Values.global.hosts.ssh }}
        ssh_host: {{ . | quote }}
        {{- end }}
        {{- with .Values.global.appConfig }}
        max_request_duration_seconds: {{ default (include "gitlab.appConfig.maxRequestDurationSeconds" $) .maxRequestDurationSeconds }}
        impersonation_enabled: {{ .enableImpersonation }}
        application_settings_cache_seconds: {{ .applicationSettingsCacheSeconds | int }}
        usage_ping_enabled: {{ eq .enableUsagePing true }}
        username_changing_enabled: {{ eq .usernameChangingEnabled true }}
        issue_closing_pattern: {{ .issueClosingPattern | quote }}
        default_theme: {{ .defaultTheme }}
        {{- include "gitlab.appConfig.defaultProjectsFeatures.configuration" $ | nindent 8 }}
        webhook_timeout: {{ .webhookTimeout }}
        {{- end }}
        trusted_proxies:
        {{- if .Values.trusted_proxies }}
          {{- toYaml .Values.trusted_proxies | nindent 10 }}
        {{- end }}
        time_zone: {{ .Values.global.time_zone | quote }}
        {{- include "gitlab.outgoing_email_settings" . | nindent 8 }}
      {{- with .Values.global.appConfig }}
      {{- if .incomingEmail.enabled }}
      {{- include "gitlab.appConfig.incoming_email" . | nindent 6 }}
      {{- end }}
      {{- include "gitlab.appConfig.cronJobs" . | nindent 6 }}
      gravatar:

읽기 어려운 예

  gitlab.yml.erb: |
    production: &base
      gitlab:
        host: {{ template "gitlab.gitlab.hostname" . }}
        https: {{ hasPrefix "https://" (include "gitlab.gitlab.url" .) }}
{{- with .Values.global.hosts.ssh }}
        ssh_host: {{ . | quote }}
{{- end }}
{{- with .Values.global.appConfig }}
        max_request_duration_seconds: {{ default (include "gitlab.appConfig.maxRequestDurationSeconds" $) .maxRequestDurationSeconds }}
        impersonation_enabled: {{ .enableImpersonation }}
        usage_ping_enabled: {{ eq .enableUsagePing true }}
        username_changing_enabled: {{ eq .usernameChangingEnabled true }}
        issue_closing_pattern: {{ .issueClosingPattern | quote }}
        default_theme: {{ .defaultTheme }}
{{- include "gitlab.appConfig.defaultProjectsFeatures.configuration" $ | indent 8 }}
        webhook_timeout: {{ .webhookTimeout }}
{{- end }}
        trusted_proxies:
{{- if .Values.trusted_proxies }}
{{- toYaml .Values.trusted_proxies | indent 10 }}
{{- end }}
        time_zone: {{ .Values.global.time_zone | quote }}
{{- include "gitlab.outgoing_email_settings" . | indent 8 }}
{{- with .Values.global.appConfig }}
{{- if .incomingEmail.enabled }}
{{- include "gitlab.appConfig.incoming_email" . | indent 6 }}
{{- end }}
{{- include "gitlab.appConfig.cronJobs" . | indent 6 }}
      gravatar:

관련 문제: #729 리팩토링: Helm 템플릿

템플릿에서 toYaml을 활용하는 시기

템플릿 파일에서 기본적으로 toYaml을 활용하는 것은 좋지 않은데, 이는 Kubernetes와 원하는 커뮤니티 구성을 모두 지원하는 부담을 초래하기 때문입니다. 우리는 기본적으로 최소한의 구성을 사용하여 합리적인 기본값을 제공하는 데 주력합니다. 우리의 두 번째 초점은 Kubernetes의 더 고급 사용자가 기본값을 재정의할 수 있는 기능을 제공하는 것입니다. 이는 각 경우에 따라 진행되어야 하며, 경우에 따라 한 옵션이나 다른 옵션 모두 지원하기에 번거로울 수 있거나 지나치게 복잡한 템플릿을 유지하게 만들 수 있습니다.

재정의할 수 있는 합리적인 기본값의 좋은 예시는 레지스트리 서브차트의 수평 포드 자동 조정기(Horizontal Pod Autoscaler) 구성에서 찾을 수 있습니다. 우리는 HPA를 CPU 사용률로 제어하는 특정 구성을 노출하여 쉽게 지원할 수 있는 최소한의 구성을 기본값으로 제공합니다. 커뮤니티에 단 하나의 구성 옵션만 제공하며, 바로 targetAverageUtilization입니다. HPA는 훨씬 더 많은 유연성을 제공할 수 있으므로 더 고급 사용자는 다양한 메트릭을 목표로 할 수 있으며, 이는 사용자가 더 복잡한 HPA 구성을 제공할 수 있도록 허용하는 if 문을 활용할 수 있는 완벽한 예시입니다.

  metrics:
  {{- if not .Values.hpa.customMetrics }}
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          targetAverageUtilization: {{ .Values.hpa.cpu.targetAverageUtilization }}
  {{- else -}}
    {{- toYaml .Values.hpa.customMetrics | nindent 4 -}}
  {{- end -}}

위의 예시에서 최소 구성이란 values.yaml에서 targetAverageUtilization을 업데이트하는 간단한 변경이 될 것입니다.

더 나은 메트릭을 식별한 고급 사용자는 .customMetrics를 설정하여 HPA 메트릭 배열에 대한 Kubernetes API 호환 구성을 포함하는 배열로 이 지나치게 단순한 HPA 구성을 재정의할 수 있습니다.

우리는 더 고급 사용자의 편리함을 유지하여 그들의 구성 파일을 덜 번거롭게 만드는 것이 중요합니다.

템플릿 헬퍼 개발

차트의 템플릿 헬퍼는 templates/_helpers.tpl에 위치합니다. 여기에는 차트 내에서 사용되는 명명된 템플릿이 포함되어 있습니다.

이 템플릿을 사용할 때 Go 템플릿 문법과 관련하여 명심해야 할 몇 가지 사항이 있습니다.

비인쇄 값 사로잡기

Go 템플릿 문법에서 모든 동작({{ }}로 표시됨)은 문자열을 인쇄할 것으로 기대되며, 제어 구조(define, if, with, range)와 변수 할당을 제외합니다.

즉, 인쇄되지 않는 출력을 포착하기 위해 때때로 변수 할당을 사용할 필요가 있습니다.

예를 들어:

{{- $details := .Values.details -}}
{{- $_ := set $details "serviceName" "example" -}}
{{ template "serviceHost" $details }}

위의 예에서 우리는 템플릿 함수에 출력하기 전에 맵에 추가 데이터를 추가하고 싶습니다. set 함수의 출력을 $_ 변수에 할당하여 사로잡았습니다. 이 할당 없이는 템플릿이 set의 결과(수정된 맵)를 문자열로 출력하려고 할 것입니다.

제어 구조 간 변수 전달

Go 템플릿 문법은 초기화(:=)와 할당(=)을 강하게 구분합니다, 그리고 이는 범위에 영향을 미칩니다.

결과적으로 제어 구조(if/with/range) 외부에 존재했던 변수를 재초기화할 수 있지만, 제어 구조 내에서 선언된 변수는 외부에서 사용할 수 없습니다.

예를 들어:

{{- define "exampleTemplate" -}}
{{- $someVar := "default" -}}
{{- if true -}}
{{-   $someVar := "desired" -}}
{{- end -}}
{{- $someVar -}}
{{- end -}}

위의 예에서 exampleTemplate를 호출하면 항상 default를 반환합니다. 왜냐하면 desired를 포함한 변수는 if 제어 구조 내에서만 접근 가능했기 때문입니다.

이 문제를 해결하려면 여러 범위에서 변경할 값들을 보관하기 위해 사전을 사용하는 방식으로 문제를 회피하거나 할당 연산자 (=:=의 차이)를 명시적으로 사용해야 합니다.

문제를 피하는 예시:

{{- define "exampleTemplate" -}}
{{- if true -}}
{{-   "desired" -}}
{{- else -}}
{{-   "default" -}}
{{- end -}}

사전을 사용하는 예시:

{{- define "exampleTemplate" -}}
{{- $result := dict "value" "default" -}}
{{- if true -}}
{{-   $_ := set $result "value" "desired" -}}
{{- end -}}
{{- $result.value -}}
{{- end -}}

할당과 초기화의 차이 예시(잘 살펴보세요!):

{{- define "exampleTemplate" -}}
{{- $someVar := "default" -}}
{{- if true -}}
{{-   $someVar = "desired" -}}
{{- end -}}
{{- $someVar -}}
{{- end -}}

템플릿을 사용하는 예시:

{{- define "exampleTemplate" -}}
foo:
  bar:
   baz: bat
{{- end -}}

그런 다음 위 내용을 변수와 구성을 통해 가져옵니다:

{{- $fooVar := include "exampleTemplate" . | fromYaml -}}
{{- $barVar := merge $.Values.global.some.config $fooVar -}}
config:
{{ $barVar }}

템플릿 구성 파일

이 차트는 클라우드 네이티브 GitLab 컨테이너를 사용합니다.

이 컨테이너는 ERB 또는 gomplate의 사용을 지원합니다.

가이드라인:

  1. ConfigMaps 내에서 템플릿 파일을 사용하세요 (예: gitlab.yml.erb, config.toml.tpl)

    • 항목은 템플릿으로 처리될 수 있도록 예상된 확장자를 사용해야 합니다.
  2. 마운트된 파일 위치에서 비밀 내용을 채우기 위해 템플릿을 사용하세요. (예: GitLab Pages config)

  3. ERB(.erb)는 런타임 실행 중 Ruby를 사용하는 모든 컨테이너에서 사용할 수 있습니다.

  4. gomplate(.tpl)는 모든 컨테이너에서 사용할 수 있습니다.

ERB 사용법:

우리는 표준 ERB를 사용하며, jsonyaml 모듈이 미리 로드되었을 것으로 예상할 수 있습니다.

gomplate 사용법:

우리는 컨테이너 내 Ruby의 크기와 노출을 제거하기 위해 gomplate를 사용합니다. gomplate 구문을 대해 Helm의 {{ }} 사용과 충돌하지 않도록 {% %}의 대체 구문으로 구성합니다.

민감한 콘텐츠 템플릿화

비밀은 적절하게 인코딩되거나 인용되지 않으면 잘못된 YAML을 초래할 수 있는 문자를 포함할 가능성이 있습니다. 특히 복잡한 비밀번호의 경우, 이러한 문자열이 다양한 구성 형식에 추가되는 방식에 주의해야 합니다.

가이드라인:

  1. ERB / Gomplate 출력에서 인용하지만, 둘러싸지 마세요.

  2. 가능할 경우 형식 고유 인코더를 사용하세요.

    • 렌더링된 YAML의 경우, YAML은 JSON의 상위 집합이므로 JSON 문자열을 사용하세요.

    • 렌더링된 TOML의 경우, TOML 문자열은 유사하게 이스케이프 되므로 JSON 문자열을 사용하세요.

  3. 데이터베이스 연결 문자열과 같이 인용된 문자열 _내부_에 인용된 문자열과 같은 복잡성에 주의하세요.

비밀번호 인코딩 예시

Gitaly의 클라이언트 비밀 토큰을 예시로 사용하겠습니다. 이 값은 gitaly_token이며, YAML 및 TOML 모두에 템플릿화됩니다.

my"$pec!@l"p#assword%'를 예로 들어 보겠습니다:

# YAML
gitaly:
  token: "<%= File.read('gitaly_token').strip =>"

# TOML
[auth]
token = "<%= File.read('gitaly_token').strip %>"

YAML과 TOML에서 잘못된 포맷으로 렌더링됩니다.

# YAML
gitaly:
  token: "my"$pec!@l"p#assword%'"

(<unknown>): did not find expected key while parsing a block mapping at line 3 column 3

[auth]
token = "my"$pec!@l"p#assword%'"

Error on line 2: Expected Comment, Newline, Whitespace, or end of input but "$" found.

이것을 <%= File.read('gitaly_token').strip.to_json %>로 변경하면 YAML 및 TOML에 대해 유효한 콘텐츠 형식을 얻을 수 있습니다. <% %> 외부의 "가 제거된 것을 주목하세요.

gitaly:
  token: "my\"$pec!@l\"p#assword%'"

gomplate와 동일한 방식으로 할 수 있습니다: {% file.Read "gitaly_token" | strings.TrimSpace | data.ToJSON %}

gitaly:
  # gomplate
  token: {% file.Read "./token" | strings.TrimSpace | data.ToJSON %}
  # ERB
  token: <%= File.read('gitaly_token').strip.to_json %>

템플릿 차트 노트 (NOTES.txt)

Helm의 차트 노트 기능은 차트 설치 및 업그레이드 후 유용한 정보와 후속 지침을 제공합니다.

이 노트는 templates/NOTES.txt에 배치됩니다.

이 노트와 작업할 때, 출력이 가독성이 있으며 실행 가능하도록 스타일과 관련된 몇 가지 사항을 염두에 두어야 합니다.

노트 카테고리 선택

두 가지 카테고리, WARNINGNOTICE는 노트 출력의 각 항목 유형을 나타냅니다.

  • WARNING은 설치 최적화를 위해 추가 작업이 필요함을 나타냅니다.
  • NOTICE는 반드시 추가 작업이 필요하지 않은 중요한 알림을 강조합니다.

NOTES.txt의 각 항목은 이 두 카테고리 중 하나로 시작해야 합니다. 예를 들어:

{{- if eq true .Values.some.setting }}
{{ $WARNING }}
 메시지는 경고입니다.
{{- end }}

{{- if eq true .Values.some.other.setting }}
{{ $NOTICE }}
 메시지는 알림입니다.
{{- end }}

이 예시는 출력에서 각 항목 사이의 제목과 간격을 일관되게 유지하는 두 개의 미리 정의된 변수를 사용합니다.