オンプレk8s上でicebergを使うための環境構築

# はじめに
- icebergの検証も兼ねてある程度省エネ構成で作ってみたので雑にメモを投下します。
- 間違いや改善点があれば是非是非ご指摘ください。
- 自分と同じ初学者向けに書きたかったのですが、事前にmetallbやdns、ingress-controller、storageClassを用意している前提の手順になっています。
  - それらの用意がない場合はhost名を`<service名>.<namespace>.svc.cluster.local:<port番号>`にしてクラスタ内でアクセスできるようにしたり、`kubectl port-forward`でアクセスしたりといった代替手段を適宜使ってください。


# 環境構成
- Rancher
  - v2.9.3
  - k8s version: v1.30.6 +rke2r1
  - rancherでk8sクラスタを立ててますが、kindでもdockerでも大丈夫だと思います
  - 事前にingress-controllerとnfsのcsi-driverだけ入れました
- Minio
  -  [minio-operator](https://artifacthub.io/packages/helm/minio-operator/operator): Chart version 6.0.4を利用
- Hive Metastore
  - [postgresql](https://artifacthub.io/packages/helm/bitnami/postgresql): Chart version 16.2.3を利用
  - ~~hive-metastoreはChartがいくつか公開されているがメンテされてなさそうなのでimageから作成する~~
  - apache:hive イメージを利用する
- Trino
  - [trino](https://artifacthub.io/packages/helm/trino/trino): Chart version 1.35.0を利用


# 構築手順
## minio-operatorを構築
minio-operatorのhelm chartからデプロイします
- デプロイはrancherのweb UIから行ったので実際に使ったコマンドではないですが、helmを使う場合はだいたいこんな感じだろうという手順を書いています。
- 事前準備
```
helm repo add minio https://operator.min.io
helm repo update
helm show values minio/operator > values.yaml
```
- values.yamlはingress部分だけ変更してほぼデフォルト
  - ingress classは事前に用意したtraefikを利用
```
ingress:
  enabled: true
  host: minio-operator.mynet
  ingressClassName: traefik
  path: /
  pathTyep: Prefix
```
- デプロイ
```
helm upgrade minio . \
    --cleanup-on-fail \
    --install \
    --namespace minio-operator \
    --create-namespace \
    -f values.yml
```
一通りpodが立てばオッケー

## minio tenantを構築
- ここまでで立てたminio-operatorを元にテナントを作成する
- コマンドでも出来るみたいだけど今回はmanifestを作成
  - 内容は以下のような感じ
  - 主な設定箇所
    - MINIO_ROOT_USERとMINIO_ROOT_PASSWORDは適当に設定してください
    - requestAutoCertは不要なのでfalseに設定。トラブル回避のためtlsは極力無効化します
    - ingressとstorageClassは事前に作成したものを設定
      - minioとminio-consoleというserviceが立つみたいで、minio-consoleの方にアクセスすればwebUIに入れる
      - 一つのingressで設定する場合、traefikだとMiddleWareが必要だなどとめんどくさそうなことを言われたので、ingressは２つ立てた。細かいことはこだわらない(笑)
```minio-tenant.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: minio-tenant
---
apiVersion: v1
kind: Secret
metadata:
  name: storage-configuration
  namespace: minio-tenant
stringData:
  config.env: |-
    export MINIO_ROOT_USER="*****"
    export MINIO_ROOT_PASSWORD="********"
    export MINIO_STORAGE_CLASS_STANDARD="EC:2"
    export MINIO_BROWSER="on"
type: Opaque
---
apiVersion: v1
data:
  CONSOLE_ACCESS_KEY: Y29uc29sZQ==
  CONSOLE_SECRET_KEY: Y29uc29sZTEyMw==
kind: Secret
metadata:
  name: storage-user
  namespace: minio-tenant
type: Opaque
---
apiVersion: minio.min.io/v2
kind: Tenant
metadata:
  annotations:
    prometheus.io/path: /minio/v2/metrics/cluster
    prometheus.io/port: "9000"
    prometheus.io/scrape: "true"
  labels:
    app: minio
  name: minio
  namespace: minio-tenant
spec:
  certConfig: {}
  externalCertSecret:
    - name: minio-tenant-cert
      type: cert-manager.io/v1
  configuration:
    name: storage-configuration
  env: []
  externalCaCertSecret: []
  externalCertSecret: []
  externalClientCertSecrets: []
  features:
    bucketDNS: false
    domains: {}
  image: quay.io/minio/minio:RELEASE.2024-10-02T17-50-41Z
  imagePullSecret: {}
  mountPath: /export
  podManagementPolicy: Parallel
  pools:
  - affinity:
      nodeAffinity: {}
      podAffinity: {}
      podAntiAffinity: {}
    containerSecurityContext:
      allowPrivilegeEscalation: false
      capabilities:
        drop:
        - ALL
      runAsGroup: 1000
      runAsNonRoot: true
      runAsUser: 1000
      seccompProfile:
        type: RuntimeDefault
    name: pool-0
    nodeSelector: {}
    resources: {}
    securityContext:
      fsGroup: 1000
      fsGroupChangePolicy: OnRootMismatch
      runAsGroup: 1000
      runAsNonRoot: true
      runAsUser: 1000
    servers: 3
    tolerations: []
    topologySpreadConstraints: []
    volumeClaimTemplate:
      apiVersion: v1
      kind: persistentvolumeclaims
      metadata: {}
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 1Ti
        storageClassName: nfs-csi
      status: {}
    volumesPerServer: 3
  priorityClassName: ""
  requestAutoCert: false
  serviceAccountName: ""
  serviceMetadata:
    consoleServiceAnnotations: {}
    consoleServiceLabels: {}
    minioServiceAnnotations: {}
    minioServiceLabels: {}
  subPath: ""
  users:
  - name: storage-user
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: minio-console-ingress
  namespace: minio-tenant
spec:
  ingressClassName: "traefik"
  rules:
  - host: minio-console.mynet
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: minio-console
            port:
              number: 9090
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: minio-ingress
  namespace: minio-tenant
spec:
  ingressClassName: "traefik"
  rules:
  - host: minio.mynet
    http:
      paths:
      - backend:
          service:
            name: minio
            port:
              number: 80
        path: /
        pathType: Prefix
```
- デプロイ
```
kubectl apply -f minio-tenant.yaml
```
### web UIからバケットとkeyを作成
- minio-consoleにログイン
  - ingressを設定してない場合は
    ```kubectl port-forward svc/minio-console 9090:9090```
    でhttp://localhost:9090 からアクセスできるようになります
  - 
- `buckets`>`Create Bucket`でバケットを作成します
    - 今回はicebergという名前にしました
    - ついでに`Object Browser`からicebergバケット以下に`warehouse`というパスを作成しています
        - もし作成しない場合は以降のs3aなどでアクセス先としてwarehouseを指定していますがこれは不要になります
    - アクセス設定をしてもいいですが、面倒なのでaccess policyをpublicに設定
- access keyを作成
    - minio-consoleのweb UIの`Access Keys`>`Create access key`を押して作成


## postgresqlを構築
minio-operatorの構築と大体同じ感じです。postgresqlのhelm chartからデプロイします
- デプロイにはrancherのweb UIから行ったので実際に使ったコマンドではないですが、helmを使う場合はだいたいこんな感じだろうという手順を書いています
- 事前準備
```
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm show values bitnami/postgresql > values.yaml
```
- values.yamlを修正
  - auth.usernameとauth.passwordに適当な値をいれる
  - storageClassを一通り設定
  - primary.typeをLoadBalancerに、primary.loadBalancerIPを192.168.0.151(適当なipアドレス)に設定
    - metallbがない場合はほかのtypeを選択してください
    - postgresqlはingressが使えないみたい
- デプロイ
```
helm upgrade postgresql . \
    --cleanup-on-fail \
    --install \
    --namespace postgresql \
    --create-namespace \
    -f values.yml
```
一通りpodが立てばオッケー


## hive-metastoreの構築
- 先ほど構築したpostgresqlを利用する

### hive-metastoreのpodのデプロイ
- metastore.warehouse.dir
  - minio側でデータを作成するデフォルトのディレクトリを指定
- javax.jdo.option.ConnectionURL
  - postgresqlのipやhost名とカタログ用のスキーマを指定
- javax.jdo.option.ConnectionUserName
  - postgresql構築時に設定したユーザ名
- javax.jdo.option.ConnectionPassword
    - postgresql構築時に設定したパスワード
- fs.s3a.endpoint
  - minio-tenantで作成したminioという名前のserviceを指定
  - ingressを立ててない場合は、minio.<namespace>.svc.clustor.local:80を指定すればアクセス可能
- fs.s3a.access.key
  - minio-tenant構築時にminio-consoleのweb UIの`Access Keys`>`Create access key`を押して作成したものを入力
- fs.s3a.secret.key
  - minio-tenant構築時にminio-consoleのweb UIの`Access Keys`>`Create access key`を押して作成したものを入力
```hive-metastore.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hive-metastore
  namespace: hive-metastore
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hive-metastore
  template:
    metadata:
      labels:
        app: hive-metastore
    spec:
      containers:
        - name: metastore
          image: "apache/hive:3.1.3"
          ports:
            - containerPort: 9083
          env:
            - name: SERVICE_NAME
              value: metastore
            - name: HADOOP_CLASSPATH
              value: "/opt/hadoop/share/hadoop/common/lib/*:/opt/hadoop/share/hadoop/tools/lib/*"
            - name: HADOOP_HOME
              value: "/opt/hadoop"
          volumeMounts:
            - name: metastore-cfg-vol
              mountPath: /opt/hive-metastore/conf/metastore-site.xml
              subPath: metastore-site.xml
            - name: metastore-cfg-vol
              mountPath: /opt/hadoop/etc/hadoop/core-site.xml
              subPath: core-site.xml
            - name: metastore-cfg-vol
              mountPath: /opt/hive-metastore/conf/hive-site.xml
              subPath: hive-site.xml
            - name: hadoop-dir
              mountPath: /opt/hadoop/share/hadoop/common/lib
      initContainers:
        - name: download-hadoop-libs
          image: busybox
          command:
            - sh
            - -c
            - |
              set -e
              mkdir -p /opt/hadoop/share/hadoop/common/lib
              mkdir -p /opt/hadoop/share/hadoop/tools/lib
              curl -sSL https://archive.apache.org/dist/hadoop/core/hadoop-3.3.1/hadoop-3.3.1.tar.gz \
                | tar xz -C /opt/ && \
                ln -s /opt/hadoop-3.3.1 /opt/hadoop && \
                rm -rf /opt/hadoop/share/doc
              ln -s /opt/hadoop/share/hadoop/tools/lib/hadoop-aws* /opt/hadoop/share/hadoop/common/lib/ && \
              ln -s /opt/hadoop/share/hadoop/tools/lib/aws-java-sdk* /opt/hadoop/share/hadoop/common/lib/ && \
              ln -s /opt/hadoop/share/hadoop/tools/lib/hadoop-common* /opt/hadoop/share/hadoop/common/lib/ && \
              ln -s /opt/hadoop/share/hadoop/tools/lib/hadoop-thirdparty* /opt/hadoop/share/hadoop/common/lib/

          volumeMounts:
            - name: hadoop-dir
              mountPath: /opt/hadoop/share/hadoop/common/lib
      volumes:
        - name: metastore-cfg-vol
          configMap:
            name: metastore-cfg
        - name: hadoop-dir
          emptyDir: {}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: metastore-cfg
  namespace: hive-metastore
data:
  metastore-site.xml: |
    <configuration>
      <property>
        <name>hive.metastore.uris</name>
        <value>thrift://0.0.0.0:9083</value>
      </property>
      <property>
        <name>javax.jdo.option.ConnectionDriverName</name>
        <value>org.postgresql.Driver</value>
      </property>
      <property>
        <name>javax.jdo.option.ConnectionURL</name>
        <value>jdbc:postgresql://postgresql.mynet:5432/iceberg_catalog</value>
      </property>
      <property>
        <name>javax.jdo.option.ConnectionUserName</name>
        <value>postgres</value>
      </property>
      <property>
        <name>javax.jdo.option.ConnectionPassword</name>
        <value>*********</value>
      </property>
      <property>
        <name>metastore.warehouse.dir</name>
        <value>s3a://iceberg/warehouse/</value>
      </property>
      <property>
        <name>metastore.thrift.port</name>
        <value>9083</value>
      </property>
      <property>
        <name>metastore.task.threads.always</name>
        <value>org.apache.hadoop.hive.metastore.events.EventCleanerTask</value>
      </property>
      <property>
        <name>metastore.expression.proxy</name>
        <value>org.apache.hadoop.hive.metastore.DefaultPartitionExpressionProxy</value>
      </property>
      <property>
        <name>metastore.log4j.file</name>
        <value>/opt/hive-metastore/conf/metastore-log4j2.properties</value>
      </property>
      <property>
        <name>fs.s3a.path.style.access</name>
        <value>true</value>
      </property>
      <property>
        <name>fs.s3a.impl</name>
        <value>org.apache.hadoop.fs.s3a.S3AFileSystem</value>
      </property>
    </configuration>
  core-site.xml: |
    <configuration>
      <property>
        <name>fs.s3a.connection.ssl.enabled</name>
        <value>false</value>
      </property>
      <property>
        <name>fs.s3a.endpoint</name>
        <value>http://minio.mynet</value>
      </property>
      <property>
        <name>fs.s3a.fast.upload</name>
        <value>true</value>
      </property>
      <property>
        <name>fs.s3a.access.key</name>
        <value>F72sBC7LtIo8jAPVzauh</value>
      </property>
      <property>
        <name>fs.s3a.secret.key</name>
        <value>ATH91Cm2NFMyc6N45xBRmRsYBOOAlpnepeQPk9bk</value>
      </property>
      <property>
        <name>fs.s3a.path.style.access</name>
        <value>true</value>
      </property>
      <property>
        <name>fs.s3a.impl</name>
        <value>org.apache.hadoop.fs.s3a.S3AFileSystem</value>
      </property>
    </configuration>
---
apiVersion: v1
kind: Service
metadata:
  name: hive-metastore
  namespace: hive-metastore
spec:
  selector:
    app: hive-metastore
  ports:
    - port: 9083
  type: ClusterIP
```
- hive-metastoreを起動する
```
kubectl apply -f hive-metastore.yaml
```

## trinoを構築
- お疲れ様です。最後にtrinoを立てれば終了です。
- trinoのhelm chartからデプロイします
- デプロイにはrancherのweb UIから行ったので実際に使ったコマンドではないですが、helmを使う場合はだいたいこんな感じだろうという手順を書いています。
### 事前準備
```
helm repo add trino https://trinodb.github.io/charts/
helm repo update
helm show values trino/trino > values.yaml
```
### valuesを修正
- icebergカタログの設定を追加
    - s3.endpointはminio-tenant構築時に作成したminio serviceを指定している
    - s3.regionはminioの場合はとりあえずus-east-1を入れておけばいいらしい
    - s3.aws-access-keyとs3.aws-secret-keyはhive-metastoreの設定時にminioのweb UIで作成したものを書く
    - file-formatは好きなものを指定してください
```values.yaml
  iceberg: >
    connector.name=iceberg
    iceberg.catalog.type=hive_metastore
    hive.metastore.uri=thrift://hive-metastore.hive-metastore.svc.cluster.local:9083
    fs.native-s3.enabled=true
    s3.path-style-access=true
    s3.endpoint=http://minio.mynet
    s3.region=us-east-1
    s3.aws-access-key=******
    s3.aws-secret-key=******
    iceberg.file-format=PARQUET
```
- ingress部分も必要に応じて設定してください
```values.yaml
ingress:
  annotations: {}
  className: traefik
  enabled: true
  hosts:
    - host: trino.mynet
      paths:
        - path: /
          pathType: Prefix
  tls: null
```
- 認証やtlsは設定していませんが必要に応じて設定してください
### デプロイ
```
helm upgrade trino . \
    --cleanup-on-fail \
    --install \
    --namespace trino \
    --create-namespace \
    -f values.yml
```


# 動作確認
- trinoに接続
```
trino --server http://trino.mynet --catalog iceberg --debug
```
- スキーマを作成
```
trino> create schema iceberg.sample_schema
    -> with (location = 's3a://iceberg/warehouse/sample_schema');
CREATE SCHEMA
```
- テーブルを作成
```
trino> CREATE TABLE sample_schema.sample_table (
    ->     c1 INTEGER,
    ->     c2 DATE,
    ->     c3 DOUBLE
    -> )
    -> WITH (
    ->     format = 'PARQUET',
    ->     partitioning = ARRAY['c1', 'c2'],
    ->     sorted_by = ARRAY['c3'],
    ->     location = 's3a://iceberg/warehouse/sample_schema'
    -> );
CREATE TABLE
```
- 確認
```
trino> show tables from iceberg.sample_schema;
     Table
---------------
 sample_table
(1 row)

Query 20241221_092930_00027_qir66, FINISHED, 3 nodes
http://trino.mynet/ui/query.html?20241221_092930_00027_qir66
Splits: 24 total, 24 done (100.00%)
CPU Time: 0.0s total,    76 rows/s, 2.7KiB/s, 54% active
Per Node: 0.0 parallelism,     1 rows/s,    57B/s
Parallelism: 0.1
Peak Memory: 358B
0.21 [1 rows, 36B] [4 rows/s, 172B/s]
```
- minio側にもバケットにファイルが作られている。