์ด๋ฒ ๊ธ์์๋ Operator SDK๋ฅผ ํตํด Elasticsearch Cluster๋ฅผ ํธ๋ฆฌํ๊ฒ ๊ด๋ฆฌํ ์ ์๋ Elasticsearch Operator๋ฅผ ๋ง๋ค์ด๋ณด๊ฒ ์ต๋๋ค. Operator๊ฐ ๋ฌด์์ธ์ง, Operator SDK์ ๋ํ ๊ธฐ๋ณธ์ ์ธ ์ฌ์ฉ๋ฐฉ๋ฒ์ด ๊ถ๊ธํ์ ๋ถ๊ป์๋ ์ด์ ๊ธ์ ์ฐธ๊ณ ํด์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค.
Elasticsearch Cluster
๋จผ์ Operator๋ก ๊ด๋ฆฌํ Elasticsearch Operator์ ๋ํ ์คํ์ ๊ฐ๋จํ ์์๋ณด๊ฒ ์ต๋๋ค. Cluster ๊ตฌ์ฑ์ ์ ๊ฐ ์ด์ ์ ์์ฑํ๋ Elasticsearch Helm Chart์ ๊ตฌ์ฑ์ ์ฌ์ฉํ์์ต๋๋ค.
- Master Node & Master Eligible Node
๋ง์คํฐ ๋ ธ๋์ ๋ง์คํฐ ๋ ธ๋๊ฐ ๋ค์ด๋์ ๋ ์น๊ฒฉ๋ ์ ์๋ ํ๋ณด ๋ ธ๋์ ๋๋ค.
Hot Data Node
Elasticsearch์ Hot-Warm ์ํคํ ์ณ๋ฅผ ๊ตฌ์ถํ๊ธฐ ์ํด Node์ ๋์คํฌ ํ์ ์ผ๋ก Data Node๋ฅผ ๋๋์์ต๋๋ค. Hot Data Node๋ ๋น ๋ฅธ Disk I/O ์ฑ๋ฅ์ด ํ์ํ๊ธฐ ๋๋ฌธ์ SSD๋ฅผ ๊ฐ์ง ๋ ธ๋์๋ง ๋ฐฐ์น๋์ด์ผ ํฉ๋๋ค.Warm Data Node
Warm Data Node๋ Disk I/O ์ฑ๋ฅ์ด ์ค์ํ์ง ์๊ธฐ ๋๋ฌธ์ ๊ฐ๊ฒฉ์ด ๋น์ผ SSD๋ฅผ ์ฌ์ฉํ์ง ์์๋ ๋ฉ๋๋ค. ๋ฐ๋ผ์ HDD๋ฅผ ๊ฐ์ง ๋ ธ๋์๋ง ๋ฐฐ์น๋์ด์ผ ํฉ๋๋ค.Coordinating Node
์์ฒญ ๋ฐ ๋ฐ์ดํฐ๋ฅผ ๋ผ์ฐํ ํ๋ ๊ธฐ๋ฅ์ ํ๋ ๋ ธ๋์ ๋๋ค.Cerebro
Elasticsearch Cluster ๋ฐ Index ๊ด๋ฆฌ๋ฅผ ์ํ ๋๊ตฌ์ ๋๋ค.Kibana
Elasticsearch์ ์ ์ฅ๋ ๋ฐ์ดํฐ๋ฅผ ์๊ฐํํ๊ธฐ ์ํ ๋๊ตฌ์ ๋๋ค.
Elasticsearch Cluster์ ๋ํด ๊ถ๊ธํ์ ๋ถ๋ค์ Elasticsearch ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์ฌ ์์ธํ ๋ ธ๋์ ์ญํ ์ ํ์ธํ์ค ์ ์์ต๋๋ค.
Elasticsearch Operator
CRD API
๋จผ์ CRD๋ฅผ ์์ฑํด์ผ ํฉ๋๋ค. Elasticsearch CRD๊ฐ ๊ฐ์ง๊ฒ ๋ ๋ฐ์ดํฐ๋ api/v1alpha1/elasticsearch_types.go
์ ์กด์ฌํฉ๋๋ค.
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ElasticsearchStatus defines the observed state of Elasticsearch
type ElasticsearchStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
State string `json:"state,omitempty"`
Message string `json:"message,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// Elasticsearch is the Schema for the elasticsearches API
type Elasticsearch struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ElasticsearchSpec `json:"spec,omitempty"`
Status ElasticsearchStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// ElasticsearchList contains a list of Elasticsearch
type ElasticsearchList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Elasticsearch `json:"items"`
}
// ElasticsearchSpec defines the desired state of Elasticsearch
type ElasticsearchSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
//Master Node Replicas
MasterReplicas int32 `json:"master-replicas"`
//Client Node Replicas
ClientReplicas int32 `json:"client-replicas"`
//Hot Data Node Replica
HotDataReplicas int32 `json:"hot-data-replicas"`
//Warm Data Node Replica
WarmDataReplicas int32 `json:"warm-data-replicas"`
//NodeSelector for the pod to be eligible to run on a node (ex. Hot-Warm Architecture)
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
//Annotations
Annotations map[string]string `json:"annotations,omitempty"`
//Disk Size of Hot Data Node
HotDataDiskSize string `json:"hot-data-volume"`
//Disk Size of Warm Data Node
WarmDataDiskSize string `json:"warm-data-volume"`
//Elasticsearch Image
ElasticsearchImage string `json:"elasticsearch-image"`
//Elasticsearch Cluster Name
ElasticsearchClusterName string `json:"elasticsearch-cluster-name"`
//MasterJavaOpt
MasterJavaOpts string `json:"master-javaOpts"`
//ClientJavaOpt
ClientJavaOpts string `json:"client-javaOpts"`
//HotDataJavaOpt
HotDataJavaOpts string `json:"hot-data-javaOpts"`
//WarmDataJavaOpt
WarmDataJavaOpts string `json:"warm-data-javaOpts"`
//Cerebro
Cerebro Cerebro `json:"cerebro"`
//Kibana
Kibana Kibana `json:"kibana"`
//Curator
Curator Curator `json:"curator"`
}
// Kibana properties (Optional)
type Kibana struct {
// Defines the image to use for deploying kibana
Image string `json:"image"`
}
// Cerebro properties (Optional)
type Cerebro struct {
// Defines the image to use for deploying Cerebro
Image string `json:"image"`
}
// Curator properties (Optional)
type Curator struct {
// Defines the image to use for deploying Curator
Image string `json:"image"`
}
func init() {
SchemeBuilder.Register(&Elasticsearch{}, &ElasticsearchList{})
}
Elasticsearch Cluster์ Spec์ ๋ค์๊ณผ ๊ฐ์ ์ ๋ณด๋ฅผ ํฌํจํฉ๋๋ค.
- Elasticsearch Image
- Node์ Replica ์ (Master / Hot / Warm / Coordinating)
- Data Node Disk ํฌ๊ธฐ (Hot / Warm)
- Java Options (Master / Hot / Warm / Coordinating)
- Cerebro Image
- Kibana Image
์์ ์ ๋ณด๋ฅผ ์ฌ์ฉํด ์์ฑ๋ CRD๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. (config/crd/bases
ํด๋ ์๋ ์กด์ฌํฉ๋๋ค.)
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.3.0
creationTimestamp: null
name: elasticsearches.sphong.com.my.domain
spec:
group: sphong.com.my.domain
names:
kind: Elasticsearch
listKind: ElasticsearchList
plural: elasticsearches
singular: elasticsearch
scope: Namespaced
subresources:
status: {}
validation:
openAPIV3Schema:
description: Elasticsearch is the Schema for the elasticsearches API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: ElasticsearchSpec defines the desired state of Elasticsearch
properties:
annotations:
additionalProperties:
type: string
description: Annotations
type: object
cerebro:
description: Cerebro
properties:
image:
description: Defines the image to use for deploying Cerebro
type: string
required:
- image
type: object
client-javaOpts:
description: ClientJavaOpt
type: string
client-replicas:
description: Client Node Replicas
format: int32
type: integer
curator:
description: Curator
properties:
image:
description: Defines the image to use for deploying Curator
type: string
required:
- image
type: object
elasticsearch-cluster-name:
description: Elasticsearch Cluster Name
type: string
elasticsearch-image:
description: Elasticsearch Image
type: string
hot-data-javaOpts:
description: HotDataJavaOpt
type: string
hot-data-replicas:
description: Hot Data Node Replica
format: int32
type: integer
hot-data-volume:
description: Disk Size of Hot Data Node
type: string
kibana:
description: Kibana
properties:
image:
description: Defines the image to use for deploying kibana
type: string
required:
- image
type: object
master-javaOpts:
description: MasterJavaOpt
type: string
master-replicas:
description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
Important: Run "make" to regenerate code after modifying this file
Master Node Replicas'
format: int32
type: integer
nodeSelector:
additionalProperties:
type: string
description: NodeSelector for the pod to be eligible to run on a node
(ex. Hot-Warm Architecture)
type: object
warm-data-javaOpts:
description: WarmDataJavaOpt
type: string
warm-data-replicas:
description: Warm Data Node Replica
format: int32
type: integer
warm-data-volume:
description: Disk Size of Warm Data Node
type: string
required:
- cerebro
- client-javaOpts
- client-replicas
- curator
- elasticsearch-cluster-name
- elasticsearch-image
- hot-data-javaOpts
- hot-data-replicas
- hot-data-volume
- kibana
- master-javaOpts
- master-replicas
- warm-data-javaOpts
- warm-data-replicas
- warm-data-volume
type: object
status:
description: ElasticsearchStatus defines the observed state of Elasticsearch
properties:
message:
type: string
state:
description: 'INSERT ADDITIONAL STATUS FIELD - define observed state
of cluster Important: Run "make" to regenerate code after modifying
this file'
type: string
type: object
type: object
version: v1alpha1
versions:
- name: v1alpha1
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
Controller
๋ค์์ผ๋ก Controller์ ๋ํด์ ์ดํด๋ณด๊ฒ ์ต๋๋ค. ๋จผ์ SetupWithManager
๋ฉ์๋๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
func (r *ElasticsearchReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&sphongcomv1alpha1.Elasticsearch{}).
Owns(&v1.Deployment{}).
Owns(&v1.StatefulSet{}).
WithOptions(controller.Options{
MaxConcurrentReconciles: 3,
}).Complete(r)
}
- ํํ ๋ฆฌ์ผ๊ณผ ์ ์ฌํ์ง๋ง Elasticsearch Operator์์ ๊ด์ฐฐํ ๋ฆฌ์์ค๊ฐ Deployment ์ด์ธ์๋ StatefulSet๋ ํ์ํ๊ธฐ ๋๋ฌธ์
Owns(&v1.StatefulSet{})
์ ์ถ๊ฐํ์์ต๋๋ค.
์์ฑํ Elasticsearch CR์ ๋ํด Reconcile Loop๋ฅผ ์ํํ๋ ๊ณณ์ Reconcil
ํจ์์
๋๋ค.
//Fetch the Elasticsearch Cluster
elasticsearch := &sphongcomv1alpha1.Elasticsearch{}
err := r.Get(ctx, req.NamespacedName, elasticsearch)
if err != nil {
if errors.IsNotFound(err) {
// Case 1
log.Info("Elasticsearch Resource Not Found!. Ignore since object must be deleted")
return ctrl.Result{}, nil
}
// Case 2
log.Error(err, "Failed to get Elasticsearch")
return ctrl.Result{}, err
}
Case 1์ ๊ฒฝ์ฐ๋ Request๊ฐ ๋ฐ์ํ์๋๋ฐ ์ฐพ์ ์ ์๋ ๊ฒฝ์ฐ ์ฆ, ์ญ์ ๋ ๊ฒฝ์ฐ๋ฅผ ์๋ฏธํ๋ค. ๊ธฐ์กด ์กด์ฌํ๋ ๋ฆฌ์์ค๋ ์๋์ผ๋ก GC๋๊ณ ํ์ํ๋ค๋ฉด ์ด ๋ถ๋ถ์ ๋ณ๋์ ์ญ์ ๋ก์ง์ ๊ตฌํํ ์ ์๋ค. ์ญ์ ๊ฐ์ ๊ฒฝ์ฐ, ์ด๋ฒคํธ ํ์ ๋ค์ Request๋ฅผ ๋ฃ์ง ์์ผ๋ฏ๋ก nil์ ๋ฐํํ๋ค.
Case 2์ ๊ฒฝ์ฐ๋ Request๋ฅผ ์ ์์ ์ผ๋ก ์ฝ์ง ๋ชปํ์ ๋ ๋ฐ์ํ๋ ์๋ฌ ์ํฉ์ ๋๋ค. ์ด ๊ฒฝ์ฐ์๋ Request๋ฅผ ์ด๋ฒคํธ ํ์ ๋ค์ ๋ฃ์ด์ค์ผ ํ๋ฏ๋ก err์ ๋ฐํํฉ๋๋ค.
๋ค์ ์ฝ๋๋ Elasticsearch Cluster์ ํ์ํ ๋ฆฌ์์ค๋ค์ด ์์ฑ๋์ด ์๋์ง ํ์ธํ๋ ๋ถ๋ถ์ ๋๋ค.
...
//Create Discovery Service
foundMasterSvc := &v12.Service{}
err = r.Get(ctx, types.NamespacedName, foundMasterSvc)
if err != nil && errors.IsNotFound(err) {
creationErr := r.createMasterService(elasticsearch)
if creationErr != nil {
return reconcile.Result{}, err
}
}
...
- ๋ง์ฝ ์กด์ฌํ์ง ์๋๋ค๋ฉด ์๋ก์ด ๋ฆฌ์์ค๋ฅผ ์์ฑํฉ๋๋ค. ๋ง์ฝ ์์ฑ ์ค ์๋ฌ๊ฐ ๋ฐ์ํ๊ฒ ๋๋ฉด request๋ฅผ ์ด๋ฒคํธ ํ์ ๋ค์ ๋ฃ์ด์ค์ผ ํ๋ฏ๋ก creationErr๋ฅผ ๋ฐํํฉ๋๋ค.
๋ค์ ๋ถ๋ถ์ Spec๊ณผ Status๊ฐ ๋์ผํ์ง ํ์ธํ๋ ๋ถ๋ถ์ ๋๋ค.
//scaling status's size => spec's size
hotDataSize := elasticsearch.Spec.HotDataReplicas
if *foundHotData.Spec.Replicas != hotDataSize {
foundHotData.Spec.Replicas = &hotDataSize
err = r.Client.Update(context.TODO(), foundHotData)
if err != nil {
log.Error(err, "Failed to update Deployment's Size (Hot Data Node Replica)")
return reconcile.Result{}, err
}
}
warmDataSize := elasticsearch.Spec.WarmDataReplicas
if *foundWarmData.Spec.Replicas != warmDataSize {
foundWarmData.Spec.Replicas = &warmDataSize
err = r.Client.Update(context.TODO(), foundWarmData)
if err != nil {
log.Error(err, "Failed to update Deployment's Size")
return reconcile.Result{}, err
}
}
- ์ ๊ฐ ๋ง๋ Elasticsearch Operator์์๋ Hot / Warm Data Node ์๋ง ๊ฒ์ฌํ์ฌ Spec๊ณผ ๋์ผํ์ง ํ์ธํฉ๋๋ค. ๋ง์ฝ ๋์ผํ์ง ์๋ค๋ฉด Request๋ฅผ ์ด๋ฒคํธ ํ์ ๋ค์ ๋ฃ๊ฒ ๋ฉ๋๋ค.
๋ง์ง๋ง์ผ๋ก Hot/ Warm Data Node์ Status๋ฅผ ๊ฐ Pod์ ์ด๋ฆ์ผ๋ก ๊ฐฑ์ ํฉ๋๋ค.
hotPodList := &v12.PodList{}
hotListOpts := []client.ListOption{
client.InNamespace(elasticsearch.Namespace),
client.MatchingLabels(labelsForWarmData()),
}
if err = r.List(ctx, hotPodList, hotListOpts...); err != nil {
log.Error(err, "Failed to list pods")
return ctrl.Result{}, err
}
warmPodList := &v12.PodList{}
warmListOpts := []client.ListOption{
client.InNamespace(elasticsearch.Namespace),
client.MatchingLabels(labelsForWarmData()),
}
if err = r.List(ctx, warmPodList, warmListOpts...); err != nil {
log.Error(err, "Failed to list pods")
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
Deploy Operator
์ด๋ฒ์๋ ๋ก์ปฌ ํด๋ฌ์คํฐ๊ฐ ์๋ ์ธ๋ถ ํด๋ฌ์คํฐ์ operator๋ฅผ ๋ฐฐํฌํด๋ณด๊ฒ ์ต๋๋ค. ์ ๋ GKE๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐฐํฌ๋ฅผ ์งํํ์์ต๋๋ค.
๋จผ์ , ๋์ปค ์ด๋ฏธ์ง๋ฅผ ๋น๋ํ๊ณ GCS์ ํธ์ํฉ๋๋ค.
> make docker-build IMG=<GCS Image>
> make docker-push IMG=<GCS Image>
๋ค์์ผ๋ก kustomize์ namespace์ ๋ํ ์ค์ ์ ๋ฐฐํฌํ namespace๋ก ๋ณ๊ฒฝํด์ค๋๋ค. ์ ๋ default namespace
์ ๋ฐฐํฌ๋ฅผ ์งํํ์์ต๋๋ค.
> cd config/default/ && kustomize edit set namespace "default" && cd ../..
๋ง์ง๋ง์ผ๋ก ๋ฐฐํฌ๋ฅผ ์งํํฉ๋๋ค.
> make deploy IMG=<GCS Image>
kubectl๋ก ํ์ธํด๋ณด๋ฉด elasticsearch operator๊ฐ ์ ์์ ์ผ๋ก ๋์ํ๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
> kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
elasticsearch-operator-controller-manager 1/1 1 1 1m
Create Elasticsearch CR
config/samples/
ํด๋ ์๋์ ์กด์ฌํ๋ CR์ ์
๋ฐ์ดํธ ํ ํ ์ ์ฉํด๋ณด๊ฒ ์ต๋๋ค. ๋จผ์ ์ํ๋ ํด๋ฌ์คํฐ ์คํ์ ์
๋ ฅํฉ๋๋ค.
apiVersion: sphong.com.my.domain/v1alpha1
kind: Elasticsearch
metadata:
name: elasticsearch-sample
namespace: default
spec:
cerebro:
image: asia.gcr.io/--/cerebro:6.8.2
client-javaOpts: ""
client-replicas: 1
curator:
image: asia.gcr.io/--/cerebro:6.8.2
elasticsearch-cluster-name: es-cluster
elasticsearch-image: asia.gcr.io/--/elasticsearch-base:6.8.2
hot-data-javaOpts: ""
hot-data-replicas: 1
hot-data-volume: 1Gi
kibana:
image: asia.gcr.io/--/kibana:6.8.2
master-javaOpts: ""
master-replicas: 3
warm-data-javaOpts: ""
warm-data-replicas: 1
warm-data-volume: 1Gi
kubectl
์ ํตํด ํด๋น CR์ ์์ฑํฉ๋๋ค.
$ kubectl apply -f config/samples/elasticsearch-customresource.yaml
๋ง์ง๋ง์ผ๋ก ์์ฑ๋ Elasticsearch Cluster๋ฅผ ํ์ธํฉ๋๋ค.
$ kubectl get all
NAME READY UP-TO-DATE AVAILABLE AGE
elasticsearch-operator-controller-manager 1/1 1 1 8m
elasticsearch-master 3/3 3 3 1m
elasticsearch-hot-data-0 3/3 3 3 1m
...
์ด์์ผ๋ก Elasticsearch Cluster๋ฅผ ๋ณด๋ค ์์ฝ๊ฒ ๊ด๋ฆฌํ ์ ์๊ณ ๊ฐ๋จํ๊ฒ Hot / Warm Data Node ์์ ๋ณ๊ฒฝ์ ๊ฐ์งํ๋ Elasticsearch Operator๋ฅผ ๊ฐ๋ฐํ์์ต๋๋ค.
์ ์ฒด ์ฝ๋๋ Github์ ์กด์ฌํ๋ฉฐ ๊ธ์ ๋ด์ฉ ์ค ํ๋ฆฐ ๋ถ๋ถ์ด๋ ๋ ์ข์ ์๊ฒฌ์ด ์๋ค๋ฉด ์ธ์ ๋ ์ง ํผ๋๋ฐฑ ๋ถํ๋๋ฆฝ๋๋ค. :)
์ฐธ๊ณ ์๋ฃ
'๐ Kubernetes' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Dynamic Admission Controller ์ฌ์ฉํ๊ธฐ (0) | 2021.04.29 |
---|---|
[Kubernetes ๋ด๋ถ ๊ตฌ์กฐ ์ดํดํ๊ธฐ] 1. ์ฟ ๋ฒ๋คํฐ์ค ํด๋ฌ์คํฐ ๊ตฌ์ฑ ์์ (0) | 2021.04.12 |
Pinpoint Agent Helm Chart ์์ฑํ๊ธฐ (0) | 2021.03.06 |
Kubernetes Operator (feat. Operator SDK) (0) | 2020.12.18 |
Argo Project๋ฅผ ์ฌ์ฉํ์ฌ CI/CD ๊ตฌ์ถํ๊ธฐ (0) | 2020.12.18 |
๋๊ธ