👋

開発者の認知負荷を下げるGitOps構成の実践例:ArgoCDとHelm Chartで実現するプラットフォーム

に公開

はじめに

こんにちはログラスでSREをしている見形@mekkaです。

ログラスではマルチプロダクト展開を見据えたプラットフォームの整備を進めています。
具体的な取り組みとしては、これまでプロダクトごとにECSで管理していたインフラを、EKSを利用した横断的なプラットフォームへ統合しようと考えています。Kubernetesのエコシステムを活用し、より柔軟な抽象化を提供することが目的です。

プラットフォームエンジニアリングの話をすると議論する部分は沢山ありますが、今回はその中でも根幹の部分であるContinuous Deliveryのところに絞って実践例を紹介したいと思います。

Kubernetesを利用する場合、デリバリーの方式としてGitOpsを選択し、実装方法としてArgoCDを利用するケースが多いと思いますが、具体的なディレクトリ構成や設定値まで踏み込んで解説している事例は少ないように思うので、今回は弊社の構成を紹介しようと思います。

なお、GitOpsやArgoCDについては解説している記事が多数あると思いますので、詳細についてはここでは触れません。

背景と課題

これまではECSとTerraformを利用した運用を行っており、サービスの追加時は開発者が自分でTerraformのコードを書いて適用する流れとなっていました。
一定のモジュール化やパイプラインの整備は行っていましたが、Terraformを触る場合はSREチームへの相談も多く、不慣れなメンバーが苦労している状況も見受けられました。

誤解がないように補足しますが、これはECSやTerraformというツールの問題ではなく、私たちのモジュール設計が考慮不足で、開発者の認知負荷が高い状態にあったのが原因です。

プラットフォーム化を進めるにあたり改めて、開発者の認知負荷を下げる方針で議論しました。その結果、「開発者は 設定のためのファイル(values等のyaml) を入力するだけで済むようにし、裏側の複雑さは SRE が抽象化して隠蔽する」という設計にしました。

クラスタの構成について

ログラスではKubernetesのシングルクラスタで構築し、各プロダクトのサービスごとにNamespaceで分離する構成としています。
ArgoCDなどのクラスタ横断のリソースはSREが管理し、各Namespace内のリソースは開発者が管理しています。
GitOpsの仕組みも上記を意識して、ArgoCDのRBAC等を用いた適切な権限分離などを行っています。

リポジトリの構成について

ログラスでは管理するリソースに合わせてリポジトリを3つに分割して運用しています。

  • Cluster Management Repository: Operatorやクラスタ全域の設定(SRE管理)
  • Application Repository: 個別のアプリケーション設定(開発者管理)
  • Custom Template Repository: 開発者向けの共通Helm Chart管理(SRE管理)

Custom Template Repositoryは共通Chartのみを管理しています。 他2つのリポジトリはApp of Appsパターン[1]を採用し、以下の参照フローで構成を統一しています。

  • root → apps: TopのApplicationが、各サービスのApplicationを参照
  • apps → services: 各サービスのApplicationが、実体(Chart/values)を参照
  • services: 実際にデプロイされるリソース定義(リポジトリによって差異あり)

Custom Template Repositoryは後ほど説明しますので、先に他の2つのリポジトリについてどの様な構成になっているかを説明します。

基本的なフォルダ構成

/repo
├── apps
│   ├── applications
│   ├── charts
│   └── projects
├── root
│   └── charts
└── services
    ├── charts
    └── values

servicesフォルダはリポジトリによって構成に差異があるため、ここでは共通的なrootappsフォルダについて説明し、servicesフォルダはそれぞれのリポジトリごとに説明します。

rootフォルダ

TopのApplicationとAppProjectを管理しており、サービスの追加時も変更は発生しません。
SREが管理しており、原則触らない部分になります。

/root
└── charts
    └── root-app-of-apps
        ├── Chart.yaml
        ├── templates
        │   ├── _helpers.tpl
        │   ├── application.yaml
        │   ├── appproject.yaml
        │   └── NOTES.txt
        └── values.yaml

appsフォルダ

各サービス用のApplicationとProjectを管理しており、サービス追加時には必要なファイルの追加・更新を行います。

/apps
├── applications
│   ├── app-A
│   │   ├── values-dev.yaml
│   │   └── values-prd.yaml
│   └── app-B
│       ├── values-dev.yaml
│       └── values-prd.yaml
├── charts
│   └── detail-app-of-apps
│       ├── Chart.yaml
│       ├── templates
│       │   ├── _helpers.tpl
│       │   ├── applicationset.yaml
│       │   ├── appproject.yaml
│       │   └── NOTES.txt
│       └── values.yaml
└── projects
    ├── values-dev.yaml
    └── values-prd.yaml

projects/values-xxx.yaml

環境ごとのAppProjectの設定が格納されており、AppProjectはvaluesの内容から動的に作成しています。
AppProjectの設定はArgoCDの操作権に関わるのでSREが管理しています。

values-xxx.yaml
global:
  env: dev  # 環境の定義

projects:
  projectlists:
    - name: project-A # AppProject名
      destinations: # deploy先の情報
        - namespace: app-A
          server: https://kubernetes.default.svc
      roles: # AppProjectごとのRoleとTeamの定義
        - name: developer
          org: loglass
          groups:
            - team-A
          applications:
            - app-A

applications/xxx/values-xxx.yaml

Applicationを作成するために必要なパラメータが設定されており、Applicationはvaluesの内容から動的に作成しています。
サービス追加時にファイルを作成するため、開発者が管理します。

values-xxx.yaml
applications:
  name: app-A # Application名
  project: project-A # 紐づけるAppProject名
  source: # Application内から参照するChartの情報
    chart:
      targetRevision: main
    values:
      targetRevision: main
  destination: # deploy先の情報
    server: https://kubernetes.default.svc
    namespace: app-A

charts/template/applicationset.yaml

Applicationの作成はApplicationSetを利用しており、Git Generatorを利用しています。
Git Generatorでapplications/apps-x/values-xxx.yamlの内容を参照して、Applicationを作成するために必要な情報を取得しています。

applicationset.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata
  ・・・
spec:
  - git: # 自身のリポジトリを参照
      repoURL: https://github.com/loglass/sample.git
      revision: main
      files:
        - path: "apps/applications/*/values-dev.yaml"
  template: # 以下でvalues-xxx.yamlから取得した情報を参照
    spec:
      ・・・

Cluster Management Repository(SRE管理)

Operatorやクラスタ全域の設定を管理するリポジトリとなっており、SREのみが利用します。
Operatorや設定を追加する場合は、appsフォルダ配下に必要なAppProjectやApplicationの設定を行った上で、servicesフォルダ配下にOperatorなどのChartを配置しています。

servicesフォルダ

  • charts: OperatorごとにChartが格納されています。Umbrella Chartを利用しており、追加するリソースをtemplates配下に定義しています。
  • values: Operatorに与えるvaluesで環境差異がある部分はvalues配下に定義しています。
./services
├── charts
│   ├── argocd
│   │   ├── Chart.yaml
│   │   ├── templates
│   │   │   └── ingress.yaml
│   │   └── values.yaml
└── values
    └── argocd
        ├── values-dev.yaml
        └── values-prd.yaml

Umbrella Chart(ArgoCDを例に)

Chart.yamlでdependenciesで参照先のChart(ArgoCD)を定義します。
values.yamlではargo-cdの様にsubchart名を記載することで、参照先のChart(ArgoCD)にvaluesの値を引き渡しています。
ApplicationでArgoCDなどのHelmリポジトリを直接参照する方法もありますが、リソースを追加で加えたい場合(例えばingress)、Umbrella Chartの方法を取ることで、参照先のChartと追加で定義したリソースを1つのApplication内に収めることができ、UI上の視認性が良くなります。

Chart.yaml
apiVersion: v2
name: argocd-wrapper
description: A Helm chart that wraps ArgoCD with custom configurations
type: application
version: x.x.x
appVersion: "x.x.x"

dependencies:
  - name: argo-cd
    version: "x.x.x"
    repository: "https://argoproj.github.io/argo-helm"

values.yaml
# ArgoCD subchart configuration
# All values under 'argo-cd' will be passed to the ArgoCD subchart
argo-cd:
  crds:
    enabled: false

  configs:
    params:
      ・・・

Application Repository(開発者管理)

サービス提供のための具体的なリソース(Deployment, Service, Ingress・・・etc)を管理するリポジトリとなっており、開発者が利用します。
サービスを追加する場合、appsフォルダ配下に必要な設定を行うところは同様ですが、servicesフォルダ配下の構成に差異があり、Chartは配置しておらずvaluesのみ配置しています。
サービス用のChartはCustom Template Repositoryで共通化してSREが管理しています。

servicesフォルダ

共通のChartに対応したvaluesがサービスごとに配置されており、サービスに合わせて各開発者が設定しています。
valuesの内容はCustom Templateの内容を説明する際に触れるため、ここでは割愛します。

./services
├── app-A
│   ├── values-dev.yaml
│   └── values-prd.yaml
└── app-B
    ├── values-dev.yaml
    └── values-prd.yaml

charts/template/applicationset.yaml

サービスをデプロイするには複数のリポジトリからChartとvaluesを参照する必要があり、この部分はArgoCDのMultiple Sources[2]を利用しており、ApplicationSet内のApplicationの定義に含まれています。

applicationset.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  ・・・
spec:
  - git:
      repoURL: https://github.com/loglass/sample.git
      revision: main
      files:
        - path: "apps/applications/*/values-dev.yaml"
  template:
    metadata:
     ・・・
    spec:
      project: '{{.applications.project}}'
      sources:
        - repoURL: https://github.com/loglass/custom-template.git # Custom Template Repoを参照
          targetRevision: v0.1.0 # SRE側でバージョニングして管理
          path: charts/custom-templates
          helm:
            valueFiles:
              - "$values/services/{{.applications.name}}/values-dev.yaml"
        - repoURL: https://github.com/loglass/application.git # Application Repositoryを参照
          targetRevision: main
          ref: values
      destination:
        ・・・

Custom Template(共通のChart)

開発者がサービスを公開する際に必要なリソースがtemplateとして纏まっています。DeploymentやServiceなどをなるべく簡単に設定出来るようにしていますが、加えて、PriorityClassやNetworkPolicyなど開発者が気にしづらい部分はSRE側がデフォルト設定を組み込んでいます。

./repo
├── charts
│   └── custom-templates
│       ├── Chart.yaml
│       ├── templates
│       │   ├── _helpers.tpl
│       │   ├── configmap.yaml
│       │   ├── deployment.yaml
│       │   ├── eventbus.yaml
│       │   ├── eventSource.yaml
│       │   ├── ・・・ # 必要なリソースは都度、追加している
│       └── values.yaml # デフォルトの値をまとめて設定
└── values
    ├── values-sample-deployment.yaml # 用途に合わせたサンプルのvalues
    └── values-template.yaml # 全設定値が記載されたvalues

custom-templates/templates/xxx.yamlの構造

凝ったことはしていませんが、開発者が定義するvaluesは必要なリソースのみON/OFFしたり、Application内にリソースを複数定義したりする可能性があるため、Chart側もそれを意識した作りとなっています。
パラメータは可能な範囲で柔軟に設定出来る様にしていますが、デフォルトのvaluesで設定している箇所が大半です。

deployment.yaml
{{- range .Values.services }}
{{- if and .deployment .deployment.enabled }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .name }}
  labels:
    {{- include "custom-template.customLabels" (dict "name" .name "root" $) | nindent 4 }}
spec:
  {{- with (coalesce .deployment.revisionHistoryLimit $.Values.deployment.revisionHistoryLimit) }}
  revisionHistoryLimit: {{ . }}
  {{- end }}
  {{- with (coalesce .deployment.strategy $.Values.deployment.strategy) }}
  strategy:
    type: {{ .type }}
    {{- with .rollingUpdate }}
    rollingUpdate:
      {{- toYaml . | nindent 6 }}
    {{- end }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "custom-template.customSelectorLabels" (dict "name" .name "root" $) | nindent 6 }}
  template:
    metadata:
  ・・・
{{- end }}
{{- end }}

開発者はvaluesのtemplateを取得して必要な部分のみ記載します。
※templateが肥大化してきており、Chartを複数に分割することも検討しています
実用性のない例ですが、以下はdev環境にDeploymentとserviceのセットを配置しています。

values-dev.yaml
#########################################################
### Global settings
#########################################################
global:
  # required for env
  env: "dev"

services:
  sample-app:
    # required for service name
    name: sample-app

    #########################################################
    ### Deployment required for deployment enabled
    #########################################################
    deployment:
      enabled: true
      replicaCount: 3
      containers:
        sample-app:
          name: sample-app
          image:
            repository: nginx
            tag: "latest"

新しいサービスを追加するまでの流れ

ここまでに説明した諸々の部品を利用して、開発者がサービスを追加するまでの流れを説明します。

AppProjectの追加

最初にAppProjectを管理しているvaluesに情報を追加します。
SREがOWNERとなっているため、必要な設定を行ってPRを出します。

/repo
├── apps
│   ├── applications
│   ├── charts
│   └── projects
│       └── values-dev.yaml # ファイルを更新
├── root
└── services
# global values
global:
  env: dev

# projects to deploy
projects:
  projectlists:
    - name: xxx
      ・・・
    - name: sample-app # AppProject名
      destinations: # デプロイ先の情報
        - namespace: sample-app
          server: https://kubernetes.default.svc
      roles: # AppProjectとApplicationに関する権限
        - name: developer # Role名(選択可能なものはSREで別途管理)
          org: loglass # GitHubのOrganization
          groups: # GitHubのTeam
            - sampleGroup
          applications: # 紐づけるApplication
            - sample-app

Applicationの追加

AppProjectを追加した後、紐づくApplicationを設定します。
開発者の管理となるため、新たにファイルを作成します。

/repo
├── apps
│   ├── applications
│   │   ├── sample-app
│   │   │   └── values-dev.yaml # ファイルを追加
│   ├── charts
│   └── projects
│       └── values-dev.yaml
├── root
└── services
# applications to deploy
applications:
  name: sample-app # Application名
  project: sample-app # 紐づけるAppProject名
  source: # Chartに関する情報
    chart:
      targetRevision: v0.1.0 # Custom TemplateのVersion
    values:
      targetRevision: main # valuesを参照するリポジトリの情報
  destination: # デプロイ先の情報
    server: https://kubernetes.default.svc
    namespace: sample-app

Custom Template用のvaluesの追加

Applicationを追加した後、Custom Template用のvaluesを設定します。
開発者の管理となるため、新たにファイルを作成します。

/repo
├── apps
│   ├── applications
│   │   ├── sample-app
│   │   │   └── values-dev.yaml
│   ├── charts
│   └── projects
│       └── values-dev.yaml
├── root
└── services
    └── sample-app
        └── values-dev.yaml # ファイルを追加
values-dev.yaml
#########################################################
### Global settings
#########################################################
global:
  # required for env
  env: "dev"

services:
  sample-app:
    # required for service name
    name: sample-app

    #########################################################
    ### Deployment required for deployment enabled
    #########################################################
    deployment:
      enabled: true
      replicaCount: 3
      containers:
        sample-app:
          name: sample-app
          image:
            repository: nginx
            tag: "latest"
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          resources:
            limits:
              cpu: 100m
              memory: 128Mi
            requests:
              cpu: 50m
              memory: 128Mi

    #########################################################
    ### Service required for service enabled
    #########################################################
    service:
      enabled: true
      type: ClusterIP
      ports:
        - name: http
          port: 80
          targetPort: 80
          protocol: TCP

作業手順としては以上です。
PRを出してMergeされればArgoCDが検知してリソースが作成されます。
開発者はArgoCDなどでの操作は行わず、Gitへ必要なファイルを追加するのみです。

まとめ

数年ぶりにGitOpsの構成を考えましたが、これまでの経験で学んだことを盛り込んだつもりです。
それでも、実際に開発者に触ってもらうと色々な意見が出てきているため、継続して改善していく必要があると感じています。
この継続的に改善するというところがとても重要なポイントなので、意見を出してくれる開発者のみんなに感謝しつつ、一緒に育てていきたいと思っています。

プラットフォームエンジニアリングの一部を紹介させて頂きましたが、全く人手が足りません!
これから作っていくフェーズなのであれこれ考えることも多く良い経験が出来る場だと思っています。
興味ある方は是非、カジュアル面談から気軽にお声がけください!

https://pitta.me/matches/ylFteeLptNyn

https://hrmos.co/pages/loglass/jobs/Eng-SRE--202509

脚注
  1. App of Appsパターンについて https://argo-cd.readthedocs.io/en/latest/operator-manual/cluster-bootstrapping/ ↩︎

  2. Multiple Sourcesについて https://argo-cd.readthedocs.io/en/latest/user-guide/multiple_sources/ ↩︎

株式会社ログラス テックブログ

Discussion