Kubernetes Operator: Kubernetes クラスターに TeamCity サーバーをデプロイする Kubernetes (英語) は、ダウンタイムなしで更新を行い、安定性、信頼性、拡張性に優れたアプリケーションをデプロイするのに役立つさまざまな機能とツールを提供します。
手動デプロイ Kubernetes クラスターにサーバーをデプロイする場合、次の TeamCity 機能が最終的なセットアップと全体的なアプローチに影響する可能性があります。
マルチノード設定 継続的に実行されるプロセスと同様に、pods と個々のコンテナーは、いずれ障害が発生したり、強制的に削除されたりする可能性があります。 アプリケーションが単一の pod で実行されている場合、Kubernetes 管理者が障害が発生した pod を適切なタイミングで特定し、手動で交換できなかった場合、これらのインシデントによってワークフロー全体が失敗する可能性があります。
この脅威を軽減し、pod の自己修復を促進し、最終的に K8S ワークフローの整合性を確保するため、Kubernetes はレプリカをサポートしています。レプリカとは、単一または複数のノードで同時に実行される、同じ pod の同一インスタンスまたはコピーです。 pod に障害が発生すると、利用可能なレプリカが代わりを担い、Kubernetes は障害が発生した pod を再起動して、必要なレプリカ数を復元します。
TeamCity は、サーバーの完全に同一のインスタンスを同時に実行することをサポートしていません。 複数のインスタンスが存在する場合、TeamCity はそれらを マルチノード設定 の一部として扱い、メインノードまたはセカンダリノードとしてランク付けするよう求めます。 つまり、異なる TEAMCITY_SERVER_OPTS 環境変数値を持つ N 個のデプロイを作成して、正解の ノード責任 を TeamCity ノードに割り当てる必要があります。
共有リソース 実稼働環境では、すべての TeamCity ノードが以下にアクセスできることを確認する必要があります:
データディレクトリは、複数の仮想マシン (K8S ノード) で使用できるように、NFS/SMB ボリュームである必要があります。
例 1: テストセットアップ 次のマニフェストは、単一の TeamCity ノードと外部データベースを使用したシンプルなテストセットアップを示しています。
apiVersion: apps/v1
kind: Deployment
metadata:
name: teamcity-server
spec:
replicas: 1 #always 1
selector:
matchLabels:
app: teamcity-server
template:
metadata:
labels:
app: teamcity-server
spec:
containers:
- name: teamcity-server
image: jetbrains/teamcity-server:latest
env:
- name: TEAMCITY_SERVER_OPTS
value: -Dteamcity.server.rootURL=http://$(POD_NAME).$(POD_NAMESPACE)
- name: TEAMCITY_DATA_PATH
value: /data/teamcity_server/datadir
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
ports:
- containerPort: 8111
volumeMounts:
- name: teamcity-data
mountPath: /data/teamcity_server/datadir
volumes:
- name: teamcity-data
emptyDir: {}
例 2: 一台の仮想マシン上の二つの TeamCity レプリカ 以下のサンプルは、2 つのレプリカと外部データベースを使用した、より安定したソリューションを実装しています。 ただし、セットアップを単純化するため、どちらも同じ仮想マシン上で実行され、ローカルディレクトリを TeamCity データディレクトリ として使用しています。
このデプロイは、二つの同一の TeamCity ノードを宣言しています。 上記の マルチノード設定 ブロックで述べたように、この動作は TeamCity ではサポートされていません。 TeamCity UI に初めてアクセスする際は、二つのレプリカのうち一つをメインノードとしてラベル付けする必要があります。
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
selector:
app: mysql
clusterIP: None
---
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql-initdb-config
data:
init.sql: |
CREATE DATABASE IF NOT EXISTS teamcity;
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: mysql
spec:
volumes:
- name: mysql-initdb
configMap:
name: mysql-initdb-config
containers:
- image: mysql:8
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-initdb
mountPath: /docker-entrypoint-initdb.d
---
#based on database.properties
apiVersion: v1
data:
connectionProperties.password: cGFzc3dvcmQ=
connectionProperties.user: cm9vdA==
connectionUrl: amRiYzpteXNxbDovL215c3FsLmRlZmF1bHQ6MzMwNi90ZWFtY2l0eQ==
kind: Secret
metadata:
name: database-properties
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: teamcity-server
spec:
replicas: 2
selector:
matchLabels:
app: teamcity-server
template:
metadata:
labels:
app: teamcity-server
spec:
containers:
- name: teamcity-server
image: jetbrains/teamcity-server:latest
env:
- name: TEAMCITY_DATA_PATH
value: /data/teamcity_server/datadir
- name: TEAMCITY_DB_USER
valueFrom:
secretKeyRef:
key: connectionProperties.user
name: database-properties
- name: TEAMCITY_DB_PASSWORD
valueFrom:
secretKeyRef:
key: connectionProperties.password
name: database-properties
- name: TEAMCITY_DB_URL
valueFrom:
secretKeyRef:
key: connectionUrl
name: database-properties
ports:
- containerPort: 8111
volumeMounts:
- name: teamcity-data
mountPath: /data/teamcity_server/datadir
volumes:
- name: teamcity-data
persistentVolumeClaim:
claimName: teamcity-node-volume
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: teamcity-node-volume
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: standard
例 3: 責任が異なる複数のノード 以下のサンプルでは、メインノードとセカンダリノードそれぞれに、2 つの独立したデプロイレイヤーを設定しています。 各デプロイの 環境 セクションでは、 TEAMCITY_SERVER_OPTS 環境変数を設定して、各ノードに異なるロールを割り当てています。
#database deployment
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
selector:
app: mysql
clusterIP: None
---
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql-initdb-config
data:
init.sql: |
CREATE DATABASE IF NOT EXISTS teamcity;
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: mysql
spec:
volumes:
- name: mysql-initdb
configMap:
name: mysql-initdb-config
containers:
- image: mysql:8
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-initdb
mountPath: /docker-entrypoint-initdb.d
---
apiVersion: v1
data:
connectionProperties.password: qwerty
connectionProperties.user: johndoe
connectionUrl: foobar
kind: Secret
metadata:
name: database-properties
---
apiVersion: apps/v1
kind: Deployment #Deployment #1, main TeamCity node
metadata:
name: teamcity-server-main
spec:
replicas: 1 #always 1
selector:
matchLabels:
app: teamcity-server
template:
metadata:
labels:
app: teamcity-server
spec:
containers:
- name: teamcity-server
image: jetbrains/teamcity-server:latest
env:
- name: TEAMCITY_SERVER_OPTS
value: -Dteamcity.server.nodeId=main-node -Dteamcity.server.rootURL=http://$(POD_NAME).$(POD_NAMESPACE)
-Dteamcity.server.responsibilities=MAIN_NODE,CAN_PROCESS_BUILD_MESSAGES,CAN_CHECK_FOR_CHANGES,CAN_PROCESS_BUILD_TRIGGERS,CAN_PROCESS_USER_DATA_MODIFICATION_REQUESTS
- name: TEAMCITY_DATA_PATH
value: /data/teamcity_server/datadir
- name: TEAMCITY_DB_USER
valueFrom:
secretKeyRef:
key: connectionProperties.user
name: database-properties
- name: TEAMCITY_DB_PASSWORD
valueFrom:
secretKeyRef:
key: connectionProperties.password
name: database-properties
- name: TEAMCITY_DB_URL
valueFrom:
secretKeyRef:
key: connectionUrl
name: database-properties
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
ports:
- containerPort: 8111
volumeMounts:
- name: teamcity-data
mountPath: /data/teamcity_server/datadir
volumes:
- name: teamcity-data
persistentVolumeClaim:
claimName: teamcity-node-volume
---
apiVersion: apps/v1
kind: Deployment #Deployment #2, secondary TeamCity node
metadata:
name: teamcity-server-additional-node-0
spec:
replicas: 1 #always 1
selector:
matchLabels:
app: teamcity-server
template:
metadata:
labels:
app: teamcity-server
spec:
containers:
- name: teamcity-server
image: jetbrains/teamcity-server:latest
env:
- name: TEAMCITY_SERVER_OPTS
value: -Dteamcity.server.nodeId=secondary-node-0 -Dteamcity.server.rootURL=http://$(POD_NAME).$(POD_NAMESPACE)
-Dteamcity.server.responsibilities=CAN_PROCESS_BUILD_MESSAGES,CAN_CHECK_FOR_CHANGES,CAN_PROCESS_BUILD_TRIGGERS,CAN_PROCESS_USER_DATA_MODIFICATION_REQUESTS
- name: TEAMCITY_DATA_PATH
value: /data/teamcity_server/datadir
- name: TEAMCITY_DB_USER
valueFrom:
secretKeyRef:
key: connectionProperties.user
name: database-properties
- name: TEAMCITY_DB_PASSWORD
valueFrom:
secretKeyRef:
key: connectionProperties.password
name: database-properties
- name: TEAMCITY_DB_URL
valueFrom:
secretKeyRef:
key: connectionUrl
name: database-properties
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
ports:
- containerPort: 8111
volumeMounts:
- name: teamcity-data
mountPath: /data/teamcity_server/datadir
volumes:
- name: teamcity-data
persistentVolumeClaim:
claimName: teamcity-node-volume
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: teamcity-node-volume
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: standard
Helm 経由で TeamCity サーバーをデプロイする Helm(英語) は Kubernetes 用のパッケージマネージャーで、YAML マニフェストのテンプレート化、バージョン管理、設定、再利用を可能にします。 その結果、YAML のコピーと貼り付けを避け、複数の環境 (dev、ステージング、prod など) を簡単に管理し、デプロイの更新/ロールバックをより安全に行えます。
これらの利点は、TeamCity をデプロイする場合にも適用されます。 例: 実際のアプリケーションに最適な サンプル 3 は、上記の次の問題を示しています。
重複 - メインとセカンダリの TeamCity デプロイは約 90% 同一です。
ハードコードされた値 - イメージタグ、ノード ID、責任、ストレージサイズなどの生の値。
緊密な結合 - DB 構成、シークレット、PVC、アプリデプロイがすべて混在します。
スケーラビリティが低い - 追加のセカンダリノードを作成するには、別のデプロイをコピーして貼り付ける必要があります。
これらの問題を解消するには、マニフェストを個別のファイルに分割します。 Helm チャートの構造は次のようになります。
teamcity/
├── Chart.yaml
├── values.yaml # The majority of your edits will happen here
├── templates/
│ ├── mysql/ # The database template
│ │ ├── service.yaml
│ │ ├── deployment.yaml
│ │ └── configmap.yaml
│ ├── teamcity/ # Template for TeamCity nodes
│ │ ├── deployment.yaml
│ │ └── service.yaml
│ ├── secrets.yaml # Sensitive data
│ ├── pvc.yaml # The storage template
│ └── _helpers.tpl
値.yaml このファイルは、変数と一意の値の大部分を別のレイヤーにまとめています。 これらの値 (たとえば、TeamCity サーバーのイメージタグ) は、グローバルマニフェストを編集せずに変更できます。
teamcity:
image: jetbrains/teamcity-server
tag: latest
dataPath: /data/teamcity_server/datadir
mainNode:
enabled: true
nodeId: main-node
responsibilities:
- MAIN_NODE
- CAN_PROCESS_BUILD_MESSAGES
- CAN_CHECK_FOR_CHANGES
- CAN_PROCESS_BUILD_TRIGGERS
- CAN_PROCESS_USER_DATA_MODIFICATION_REQUESTS
secondaryNodes:
- name: secondary-node-0
responsibilities:
- CAN_PROCESS_BUILD_MESSAGES
- CAN_CHECK_FOR_CHANGES
- CAN_PROCESS_BUILD_TRIGGERS
- CAN_PROCESS_USER_DATA_MODIFICATION_REQUESTS
database:
secretName: database-properties
一意でない行でマニフェストを肥大化させることなく、 secondaryNodes セクションの下に新しいエントリを追加するだけで、TeamCity セットアップを簡単にスケールできます…
secondaryNodes:
- name: secondary-node-0
responsibilities:
...
- name: secondary-node-1
responsibilities:
...
- name: secondary-node-2
responsibilities:
...
... そして helm upgrade teamcity ./teamcity コマンドを呼び出します。
テンプレート/teamcity/デプロイ.yaml values.yml ファイルを配置すると、コアノードのデプロイマニフェストは次のように削減されます。
# ---------------------------------------------------------
# Main TeamCity node
# ---------------------------------------------------------
apiVersion: apps/v1
kind: Deployment
metadata:
name: teamcity-server-main
spec:
replicas: 1
selector:
matchLabels:
app: teamcity-server
node: main
template:
metadata:
labels:
app: teamcity-server
node: main
spec:
containers:
- name: teamcity-server
image: {{ .Values.teamcity.image }}:{{ .Values.teamcity.tag }}
env:
- name: TEAMCITY_SERVER_OPTS
value: >
-Dteamcity.server.nodeId={{ .Values.teamcity.mainNode.nodeId }}
-Dteamcity.server.rootURL=http://$(POD_NAME).$(POD_NAMESPACE)
-Dteamcity.server.responsibilities={{ join "," .Values.teamcity.mainNode.responsibilities }}
- name: TEAMCITY_DATA_PATH
value: {{ .Values.teamcity.dataPath }}
- name: TEAMCITY_DB_USER
valueFrom:
secretKeyRef:
name: {{ .Values.database.secretName }}
key: connectionProperties.user
- name: TEAMCITY_DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.database.secretName }}
key: connectionProperties.password
- name: TEAMCITY_DB_URL
valueFrom:
secretKeyRef:
name: {{ .Values.database.secretName }}
key: connectionUrl
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- containerPort: 8111
volumeMounts:
- name: teamcity-data
mountPath: {{ .Values.teamcity.dataPath }}
volumes:
- name: teamcity-data
persistentVolumeClaim:
claimName: teamcity-node-volume
# ---------------------------------------------------------
# Secondary TeamCity nodes (rendered from values.yaml)
# ---------------------------------------------------------
{{- range $index, $node := .Values.teamcity.secondaryNodes }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: teamcity-server-{{ $node.name }}
spec:
replicas: 1
selector:
matchLabels:
app: teamcity-server
node: {{ $node.name }}
template:
metadata:
labels:
app: teamcity-server
node: {{ $node.name }}
spec:
containers:
- name: teamcity-server
image: {{ $.Values.teamcity.image }}:{{ $.Values.teamcity.tag }}
env:
- name: TEAMCITY_SERVER_OPTS
value: >
-Dteamcity.server.nodeId={{ $node.name }}
-Dteamcity.server.rootURL=http://$(POD_NAME).$(POD_NAMESPACE)
-Dteamcity.server.responsibilities={{ join "," $node.responsibilities }}
- name: TEAMCITY_DATA_PATH
value: {{ $.Values.teamcity.dataPath }}
- name: TEAMCITY_DB_USER
valueFrom:
secretKeyRef:
name: {{ $.Values.database.secretName }}
key: connectionProperties.user
- name: TEAMCITY_DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ $.Values.database.secretName }}
key: connectionProperties.password
- name: TEAMCITY_DB_URL
valueFrom:
secretKeyRef:
name: {{ $.Values.database.secretName }}
key: connectionUrl
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- containerPort: 8111
volumeMounts:
- name: teamcity-data
mountPath: {{ $.Values.teamcity.dataPath }}
volumes:
- name: teamcity-data
persistentVolumeClaim:
claimName: teamcity-node-volume
{{- end }}
セカンダリ TeamCity ノードの数に関係なく、この単一の定義は変更されません。 Helm は、このファイルを複数の Kubernetes デプロイ (メインノード用にひとつ、さらに 範囲 により values.yml ファイルで宣言されたリストから N 個のセカンダリノードデプロイ) に自動的にレンダリングします。
テンプレート/mysql/デプロイ.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "teamcity.mysqlName" . }}
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: {{ .Values.mysql.image }}
env:
- name: MYSQL_ROOT_PASSWORD
value: {{ .Values.mysql.rootPassword | quote }}
ports:
- containerPort: 3306
テンプレート/mysql/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql-initdb-config
data:
init.sql: |
CREATE DATABASE IF NOT EXISTS {{ .Values.mysql.database }};
テンプレート/pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: teamcity-node-volume
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.persistence.size }}
storageClassName: {{ .Values.persistence.storageClass }}
_helpers.tpl で定義された Go テンプレートを使用して、このセットアップを最適化し続けることができます。 たとえば、共通のコンテナー構成を作成できます ...
{{- define "teamcity.containerBase" -}}
image: {{ .Values.teamcity.image }}:{{ .Values.teamcity.tag }}
ports:
- containerPort: 8111
volumeMounts:
- name: teamcity-data
mountPath: {{ .Values.teamcity.dataPath }}
env:
- name: TEAMCITY_DATA_PATH
value: {{ .Values.teamcity.dataPath }}
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
{{ include "teamcity.dbEnv" . | indent 2 }}
{{- end -}}
... そして TEAMCITY_SERVER_OPTS ビルダー ...
{{- define "teamcity.serverOpts" -}}
-Dteamcity.server.nodeId={{ .nodeId }}
-Dteamcity.server.rootURL=http://$(POD_NAME).$(POD_NAMESPACE)
-Dteamcity.server.responsibilities={{ join "," .responsibilities }}
{{- end -}}
... deployment.yaml ファイルをさらにきれいにするには:
containers:
- name: teamcity-server
{{ include "teamcity.containerBase" . | indent 4 }}
env:
- name: TEAMCITY_SERVER_OPTS
value: >
{{ include "teamcity.serverOpts" (dict
"nodeId" .Values.teamcity.mainNode.nodeId
"responsibilities" .Values.teamcity.mainNode.responsibilities
) }}
2026 年 9 月 11 日