EKS + Cilium (Cluster-Pool IPAM) 環境における APIService と Webhook について

はじめに

EKS とオンプレミス Kubernetes クラスタを Cilium Cluster Mesh (VXLAN) で接続する機会があり、Cluster-Pool IPAM とした際、EKS 上の APIService や Webhook が正常に動作しない問題が発生した
原因としては EKS の API サーバが APIService や Webhook の Pod にリクエストをルーティングできないためだが、そもそも API サーバが APIService と Webhook の IP アドレスをどのように解決しているのかを調査したので、備忘録としてまとめておく

前提

Kubernetes のバージョンは 1.36

APIService

APIService は Kubernetes API を拡張するための仕組みで、API サーバに対して追加の API グループやバージョンを提供することができる
例えば、metrics-server は APIService を使用して Kubernetes API を拡張し、kubectl top コマンドでリソースの使用状況を取得できるようにしている

APIService のコードを見ると、pkg/apiserver/resolvers.goResolveEndpoint 関数で APIService のエンドポイントを解決していることがわかる

func (r *aggregatorEndpointRouting) ResolveEndpoint(namespace, name string, port int32) (*url.URL, error) {
  return proxy.ResolveEndpoint(r.services, r.endpointSliceGetter, namespace, name, port)
}

この関数は proxy.ResolveEndpoint を呼び出しており、k8s.io/apiserver/pkg/util/proxy/proxy.goResolveEndpoint 関数で実装されている

まず、APIService と紐づく Service から EndpointSlice を取得している

svc, err := services.Services(namespace).Get(id)
[...]
svcPort, err := findServicePort(svc, port)
[...]
slices, err := endpointSlices.GetEndpointSlices(namespace, svc.Name)

EndpointSlice から Ready な Endpoint を探し、Endpoint の IP アドレスと Service のポートを使用して URL を構築していることがわかる

for epi := range slice.Endpoints {
  ep := &slice.Endpoints[(epi+offset)%len(slice.Endpoints)]
  if ep.Conditions.Ready == nil || *ep.Conditions.Ready {
    // (Addresses is an array but only Addresses[0] is used.)
    ip := ep.Addresses[0]
    port := int(*slice.Ports[i].Port)
    return &url.URL{
      Scheme: "https",
      Host:   net.JoinHostPort(ip, strconv.Itoa(port)),
    }, nil
  }
}

ここで組み立てられる URL の Host は Service の ClusterIP ではなく、Pod IP そのものであることがポイント
この URL は最終的にリクエストの転送先としてそのまま使われるため、API サーバは Service や kube-proxy を経由せず、Pod IP に対して直接 HTTPS で到達できる必要がある

EKS + Cluster-Pool IPAM の環境では、Pod IP は Cilium が VXLAN でオーバーレイするアドレス空間のものであり、VPC のルートテーブルには存在しない
EKS の API サーバは VPC 内に ENI を持つとはいえ、あくまで VPC のルーティングに従ってパケットを送るため、Cilium ノード間でのみカプセル化・解決可能なオーバーレイ Pod IP には到達できない

このため、Cluster-Pool IPAM 環境で APIService を使う Extension API Server (metrics-server など) は hostNetwork: true で起動し、ノードの実 IP (VPC 上でルーティング可能な IP) を Pod IP として EndpointSlice に載せる必要がある

Webhook

MutatingWebhookConfiguration / ValidatingWebhookConfiguration の clientConfig には urlservice の2通りの指定方法があり、どちらか一方を指定する

service (Namespace / Name / Port の組み合わせ) を指定した場合の解決処理は、k8s.io/apiserver/pkg/util/webhook/serviceresolver.godefaultServiceResolver.ResolveEndpoint で実装されていて、単純に Kubernetes の Service 用 DNS 名を組み立てているだけであることがわかる

func (sr defaultServiceResolver) ResolveEndpoint(namespace, name string, port int32) (*url.URL, error) {
  if len(name) == 0 || len(namespace) == 0 || port == 0 {
    return nil, errors.New("cannot resolve an empty service name or namespace or port")
  }
  return &url.URL{Scheme: "https", Host: fmt.Sprintf("%s.%s.svc:%d", name, namespace, port)}, nil
}

一見すると Webhook は APIService と違い、Pod IP を直接引かず <name>.<namespace>.svc:<port> という DNS 名を経由するだけに見える
ただし、実際にリクエストを送る k8s.io/apiserver/pkg/util/webhook/client.gohookClientConfig を見ると、この DNS 名がそのまま名前解決に使われているわけではないことがわかる

serverName := cc.Service.Name + "." + cc.Service.Namespace + ".svc"
host := net.JoinHostPort(serverName, strconv.Itoa(int(port)))
cfg.Host = "https://" + host
[...]
cfg.Dial = func(ctx context.Context, network, addr string) (net.Conn, error) {
  if addr == host {
    // ServiceResolver.ResolveEndpoint で改めて接続先を解決している
    u, err := cm.serviceResolver.ResolveEndpoint(cc.Service.Namespace, cc.Service.Name, port)
    if err != nil {
      return nil, err
    }
    addr = u.Host
  }
  return delegateDialer(ctx, network, addr)
}

cfg.Dial で実際の接続先を cm.serviceResolver.ResolveEndpoint の戻り値に差し替えており、DNS 名 <name>.<namespace>.svc はダイヤル先を上書きするためのキーとして使われているに過ぎない
つまり service を指定した Webhook も、最終的には APIService と同じ ServiceResolver インターフェース経由で接続先が決まる

この serviceResolver の実体は cmd/kube-apiserver/app/server.gobuildServiceResolver で組み立てられていて、--enable-aggregator-routing フラグの値によって実装が切り替わる

var serviceResolver webhook.ServiceResolver
if enabledAggregatorRouting {
  // APIService と同じ EndpointSlice ベースの ServiceResolver (Pod IP を直接返す)
  serviceResolver = aggregatorapiserver.NewEndpointServiceResolver(
    informer.Core().V1().Services().Lister(),
    endpointSliceGetter,
  )
} else {
  // Service の ClusterIP を返すだけの ServiceResolver
  serviceResolver = aggregatorapiserver.NewClusterIPServiceResolver(
    informer.Core().V1().Services().Lister(),
  )
}

--enable-aggregator-routing=true の場合、service 指定の Webhook も APIService と全く同じ経路 (EndpointSlice → Pod IP へ直接接続) で名前解決されることになる

ノードの外にいる EKS の Control Plane からは ClusterIP 宛への通信は本来届かないが、それでも clientConfig.service の Webhook が動作していることから、EKS では --enable-aggregator-routing=true が指定されていると思われる

一方、url を指定した場合は serviceresolver.go を一切経由せず、hookClientConfig の後半で通常の net.Dialer を使って素直に DNS 解決される

u, err := url.Parse(cc.URL)
[...]
cfg.Host = u.Scheme + "://" + u.Host

cfg.Dial の上書きが行われないため、ここで初めて実際の DNS 解決とネットワーク経路が必要になる
Kubernetes の Documentation にも記載されている

The host might be resolved via external DNS in some API servers (e.g., kube-apiserver cannot resolve in-cluster DNS as that would be a layering violation).

Webhook の Service を type: LoadBalancer にして NLB を配置し、その URL を clientConfig.url に指定すれば、EKS の API サーバ → NLB (VPC 上の実 IP) → 対象ノードの NodePort → Pod、という経路になり、Webhook は機能すると思われる

ただし url 方式は前述の通り実際の DNS 解決が必要になる
NLB が払い出すデフォルトの DNS 名 (*.elb.<region>.amazonaws.com) はパブリックホストゾーンで管理されているため問題なく解決できるが、独自ドメインを使いたい場合に Route 53 のプライベートホストゾーンでレコードを作成すると、EKS の API サーバからは名前解決できない
これは aws/containers-roadmap#1606 で報告されている既知の制限で、EKS の Managed Control Plane は AWS 側で管理されており、ユーザの VPC に関連付けられたプライベートホストゾーンを解決できないためと説明されている
そのため Webhook を独自ドメインで公開したい場合は、内部向け NLB であってもレコード自体はパブリックホストゾーンに作成する必要がある

Cluster-Pool IPAM 環境での対処法は、service のまま Pod を hostNetwork: true にするか、url + NLB の自動アサイン DNS 名 (独自ドメインにしない) にするかの2択になりそう

まとめ

  • APIService は常に EndpointSlice から Pod IP を直接取得して接続するため、Cluster-Pool IPAM のようなオーバーレイ Pod CIDR の環境では hostNetwork: true が実質必須
  • Webhook を service で指定した場合も、EKS のように Control Plane がクラスタ外にある環境では APIService と同じ Pod IP 直接解決になっていると考えられ、同様の制約を受ける
  • Webhook の Service を type: LoadBalancer として NLB を配置すれば url 指定も可能だが、独自ドメインを使う場合は Route 53 のプライベートホストゾーンではなくパブリックホストゾーンにレコードを作成する必要がある

Reference

Karpenter の Node Auto Repair について

はじめに

Karpenter の Node Auto Repair 機能の仕様について調査した結果を備忘録としてまとめておく

前提

EKS のバージョンは 1.33
Karpenter のバージョンは 1.8.0

Node Auto Repair

Karpenter の Node Auto Repair とは、一言でいうとノードの自動修復機能で、unhealthy と判断されたノードを自動的に置き換える機能
バージョン 1.8.0 では alpha 機能として提供されている

NodePool の内 20% を超えるノードが unhealthy と判断された場合は修復を行わないと書かれており、これはつまりノードが数台しか起動していない環境ではノードの自動修復が働かず、有効にしても意味がないということなのか、気になったので調査してみた
(e.g. NodePool に 3 台のノードがあり、その内 1 台が unhealthy と判断された場合、1/3 = 33.3% > 20% となるため、自動修復は行われない)

To prevent cascading failures, Karpenter includes safety mechanisms: it will not perform repairs if more than 20% of nodes in a NodePool are unhealthy, and for standalone NodeClaims, it evaluates this threshold against all nodes in the cluster.

まずは、20% の閾値が変更可能なのかを調査してみたが、GitHub Issues を見る限り、閾値を変更する方法は提供されていないようだった

一方で GitHub Issues の コメント に以下の記述があったため、pkg/controllers/node/health/controller.go の実装を確認してみた

Karpenter has handling for cases where one node is unhealthy in a small nodepool.

まず、Reconcile 関数内で NodePool の状態を確認する関数 isNodePoolHealthy が実行され、この戻り値が true の場合、deleteNodeClaim 関数により、unhealthy なノードが削除される

func (c *Controller) Reconcile(ctx context.Context, node *corev1.Node) (reconcile.Result, error) {
    ctx = injection.WithControllerName(ctx, "node.health")

    nodeClaim, err := nodeutils.NodeClaimForNode(ctx, c.kubeClient, node)
    if err != nil {
        return reconcile.Result{}, nodeutils.IgnoreDuplicateNodeClaimError(nodeutils.IgnoreNodeClaimNotFoundError(err))
    }
    ctx = log.IntoContext(ctx, log.FromContext(ctx).WithValues("NodeClaim", klog.KObj(nodeClaim)))

    unhealthyNodeCondition, policyTerminationDuration := c.findUnhealthyConditions(node)
    if unhealthyNodeCondition == nil {
        return reconcile.Result{}, nil
    }

    terminationTime := unhealthyNodeCondition.LastTransitionTime.Add(policyTerminationDuration)
    if c.clock.Now().Before(terminationTime) {
        return reconcile.Result{RequeueAfter: terminationTime.Sub(c.clock.Now())}, nil
    }

    nodePoolName, found := nodeClaim.Labels[v1.NodePoolLabelKey]
    if found {
        // ここで NodePool の状態を確認している
        nodePoolHealthy, err := c.isNodePoolHealthy(ctx, nodePoolName)
        if err != nil {
            return reconcile.Result{}, client.IgnoreNotFound(err)
        }
        // nodePoolHealthy が true の場合、deleteNodeClaim が実行される
        if !nodePoolHealthy {
            if err := c.publishNodePoolHealthEvent(ctx, node, nodeClaim, nodePoolName); err != nil {
                return reconcile.Result{}, err
            }
            return reconcile.Result{RequeueAfter: 5 * time.Minute}, nil
        }
    } else {
        clusterHealthy, err := c.isClusterHealthy(ctx)
        if err != nil {
            return reconcile.Result{}, err
        }
        if !clusterHealthy {
            c.recorder.Publish(NodeRepairBlockedUnmanagedNodeClaim(node, nodeClaim, fmt.Sprintf("more then %s nodes are unhealthy in the cluster", allowedUnhealthyPercent.String()))...)
            return reconcile.Result{RequeueAfter: 5 * time.Minute}, nil
        }
    }

    if err := c.annotateTerminationGracePeriod(ctx, nodeClaim); err != nil {
        return reconcile.Result{}, client.IgnoreNotFound(err)
    }
    return c.deleteNodeClaim(ctx, nodeClaim, node, unhealthyNodeCondition)
}

次に isNodePoolHealthy 関数は areNodesHealthy 関数を呼び出していて、unhealthyNodeCount <= threshold を満たす場合に true を返す実装となっている
threshold の計算式には、k8s.io/apimachinery/pkg/util/intstrGetScaledValueFromIntOrPercent 関数が使用されている

func (c *Controller) isNodePoolHealthy(ctx context.Context, nodePoolName string) (bool, error) {
    return c.areNodesHealthy(ctx, client.MatchingLabels(map[string]string{v1.NodePoolLabelKey: nodePoolName}))
}

func (c *Controller) areNodesHealthy(ctx context.Context, opts ...client.ListOption) (bool, error) {
    nodeList := &corev1.NodeList{}
    if err := c.kubeClient.List(ctx, nodeList, append(opts, client.UnsafeDisableDeepCopy)...); err != nil {
        return false, err
    }
    unhealthyNodeCount := lo.CountBy(nodeList.Items, func(node corev1.Node) bool {
        _, found := lo.Find(c.cloudProvider.RepairPolicies(), func(policy cloudprovider.RepairPolicy) bool {
            nodeCondition := nodeutils.GetCondition(lo.ToPtr(node), policy.ConditionType)
            return nodeCondition.Status == policy.ConditionStatus
        })
        return found
    })
    // threshold を計算し、unhealthyNodeCount と比較している
    threshold := lo.Must(intstr.GetScaledValueFromIntOrPercent(lo.ToPtr(allowedUnhealthyPercent), len(nodeList.Items), true))
    return unhealthyNodeCount <= threshold, nil
}

GetScaledValueFromIntOrPercent 関数の実装は以下の通りで、今回 isPercent および roundUp は共に true となるため、閾値は小数点切り上げで計算されることが分かる

仮に NodePool に 3 台のノードがある場合、thresholdceil(20% * 3) = ceil(0.6) = 11 となる
deleteNodeClaim 関数が実行される条件は unhealthyNodeCount <= threshold であるため、3 台のノードのうち 1 台のノードが unhealthy と判断された場合、1 <= 1 が成立し、自動修復が実行されることになる

func GetValueFromIntOrPercent(intOrPercent *IntOrString, total int, roundUp bool) (int, error) {
    if intOrPercent == nil {
        return 0, errors.New("nil value for IntOrString")
    }
    value, isPercent, err := getIntOrPercentValue(intOrPercent)
    if err != nil {
        return 0, fmt.Errorf("invalid value for IntOrString: %v", err)
    }
    if isPercent {
        if roundUp {
            // 小数点切り上げで threshold が計算される
            value = int(math.Ceil(float64(value) * (float64(total)) / 100))
        } else {
            value = int(math.Floor(float64(value) * (float64(total)) / 100))
        }
    }
    return value, nil
}

Node Auto Repair の有効化

Helm Charts を使用して Karpenter をインストールする場合、values.yaml に以下のように設定することで Node Auto Repair 機能を有効化できる

settings:
  featureGates:
    nodeRepair: true

ブログ執筆時点での Karpenter Helm Charts の最新バージョンは 1.8.2

まとめ

Karpenter 1.8.0 において、Node Auto Repair 機能における 20% の閾値は変更不可であることがわかった
ただし、ノード数が少ない NodePool においても、複数台のノードが同時に unhealthy とならなければノードの自動修復は機能すると思われる

Reference

TFLint + GitHub Actions で Problem Matchers を利用する際にハマった点

はじめに

今更ながら TFLint + GitHub Actions で Terraform の静的解析を導入した
解析結果を GitHub Actions の Problem Matchers で表示しようとした際にハマった点があったので、備忘録としてまとめた

前提

TFLint のインストール方法や基本的な使い方は、わかりやすい記事がたくさんあるのでここでは割愛

TFLint のバージョンは 0.59.1 を利用

Terraform を管理しているリポジトリのディレクトリ構成は以下のようなモノレポ構成

.
├── .github
│   ├── workflows
│   │   └── tflint.yml
│   └── tflint-problem-matcher.json
├── application/
└── terraform
    ├── envs
    │   └── dev
    │       └── main.tf
    ├── modules
    │   └── sample_module
    │       └── main.tf
    └── .tflint.hcl

Problem Matchers の設定ファイルは、terraform-linters/setup-tflint の定義 をそのまま利用

{
  "problemMatcher": [
    {
      "owner": "tflint-compact",
      "pattern": [
        {
          "regexp": "^(.+):(\\d+):(\\d+):\\s(Error|Warning|Notice)\\s-\\s(.+)\\s\\((.+)\\)$",
          "file": 1,
          "line": 2,
          "column": 3,
          "severity": 4,
          "message": 5,
          "code": 6
        }
      ]
    }
  ]
}

GitHub Actions のランナーには tflint バイナリがインストール済み (terraform-linters/setup-tflint アクションを利用しない)

ハマった点

以下のようなワークフローを実行し、TFLint で検出された問題を GitHub Annotaions で表示することには成功したのだが、問題を検出した Terraform コードへのリンクが正しく生成されなかった

name: tflint
on:
  pull_request:
    paths:
      - 'terraform/**'
    branches:
      - main
jobs:
  tflint:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: terraform
    steps:
      - name: checkout repository
        uses: actions/checkout@v4
      - name: init tflint
        run: tflint --init
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: register problem matchers
        run: |
          echo "::add-matcher::.github/tflint-problem-matcher.json"
        working-directory: .
      - name: run tflint
        run: |
          tflint --recursive --config $(pwd)/.tflint.hcl --format compact

Terraform を管理しているディレクトリ terraform を job の defaults.run.working-directory に設定してしまったため、Problem Matchers の正規表現でマッチした際に表示されるファイルパスから terraform/ が省略されてしまい、GitHub 上で正しいリンクが生成されなかったことにある

ワークフローを以下のように修正したことで、正しいリンクが生成されるようになった

name: tflint
on:
  pull_request:
    paths:
      - 'terraform/**'
    branches:
      - main
jobs:
  tflint:
    runs-on: ubuntu-latest
    # defaults.run.working-directory を削除
    steps:
      - name: checkout repository
        uses: actions/checkout@v4
      - name: init tflint
        run: tflint --init
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        # defaults の working-directory がルートディレクトリとなったため、このステップでは working-directory で terraform を指定
        working-directory: terraform
      # defaults の working-directory がルートディレクトリとなったため、このステップの working-directory を削除
      - name: register problem matchers
        run: |
          echo "::add-matcher::.github/tflint-problem-matcher.json"
      - name: run tflint
        # --chdir terraform の追加と --config オプションで $(pwd)/terraform/.tflint.hcl を指定するように修正
        run: |
          tflint --chdir terraform --recursive --config $(pwd)/terraform/.tflint.hcl --format compact

まとめ

TFLint + GitHub Actions で Problem Matchers を利用する際に working-directory を設定していると、GitHub 上で正しいリンクが生成されなかったため、回避方法をまとめた

kro (Kube Resource Orchestrator) について調べてみた

はじめに

以前からちょっと気になっていた kro (Kube Resource Orchestrator) というツールについて調べてみた

kro とは

kro は Kube Resource Orchestrator の略称で、Kubernetes リソースやクラウドプロバイダのリソースをまとめて Kubernetes カスタム API として定義・管理できるツールらしい

Reference: kro.run

現在の最新バージョンは 0.3.0

クラウドリソースを操作できる、かつリソース管理もできるので Helm + Crossplane みたいなイメージを持ったけど、Kubernetes カスタム API としてリソースを管理するので、使用感はちょっと違う?
(Crossplane は触ったことないのでわかりません)

Google Cloud, AWS, Microsoft Azure の共同プロジェクトとのこと

発音は crow (カラス) らしい

試してみる

Getting started をやってみる

まずは kind でクラスタを起動する

$ kind create cluster --name kro

続いて kro をインストールする
kro のインストールには Helm が必要らしい

$ export KRO_VERSION=$(curl -sL \
    https://api.github.com/repos/kro-run/kro/releases/latest | \
    jq -r '.tag_name | ltrimstr("v")'
  )
$ echo $KRO_VERSION
0.3.0

$ helm install kro oci://ghcr.io/kro-run/kro/kro \
  --namespace kro \
  --create-namespace \
  --version=${KRO_VERSION}

$ helm -n kro list
NAME    NAMESPACE   REVISION    UPDATED                                 STATUS      CHART       APP VERSION
kro     kro         1          2025-06-22 09:30:03.163727 +0900 JST    deployed    kro-0.3.0 0.3.0      

Pod が作成されて、おそらくこれがコントローラーだと思われる

$ kubectl get po -n kro
NAME                  READY   STATUS    RESTARTS   AGE
kro-c8fb7b586-w5qnw   1/1     Running   0          3m42s

次に Kubernetes のカスタム API を定義するための ResourceGraphDefinition を作成する

$ cat <<'EOF' | kubectl apply -f -
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: my-application
spec:
  # kro uses this simple schema to create your CRD schema and apply it
  # The schema defines what users can provide when they instantiate the RGD (create an instance).
  schema:
    apiVersion: v1alpha1
    kind: Application
    spec:
      # Spec fields that users can provide.
      name: string
      image: string | default="nginx"
      ingress:
        enabled: boolean | default=false
    status:
      # Fields the controller will inject into instances status.
      deploymentConditions: ${deployment.status.conditions}
      availableReplicas: ${deployment.status.availableReplicas}

  # Define the resources this API will manage.
  resources:
    - id: deployment
      template:
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: ${schema.spec.name} # Use the name provided by user
        spec:
          replicas: 3
          selector:
            matchLabels:
              app: ${schema.spec.name}
          template:
            metadata:
              labels:
                app: ${schema.spec.name}
            spec:
              containers:
                - name: ${schema.spec.name}
                  image: ${schema.spec.image} # Use the image provided by user
                  ports:
                    - containerPort: 80

    - id: service
      template:
        apiVersion: v1
        kind: Service
        metadata:
          name: ${schema.spec.name}-service
        spec:
          selector: ${deployment.spec.selector.matchLabels} # Use the deployment selector
          ports:
            - protocol: TCP
              port: 80
              targetPort: 80

    - id: ingress
      includeWhen:
        - ${schema.spec.ingress.enabled} # Only include if the user wants to create an Ingress
      template:
        apiVersion: networking.k8s.io/v1
        kind: Ingress
        metadata:
          name: ${schema.spec.name}-ingress
          annotations:
            kubernetes.io/ingress.class: alb
            alb.ingress.kubernetes.io/scheme: internet-facing
            alb.ingress.kubernetes.io/target-type: ip
            alb.ingress.kubernetes.io/healthcheck-path: /health
            alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
            alb.ingress.kubernetes.io/target-group-attributes: stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=60
        spec:
          rules:
            - http:
                paths:
                  - path: "/"
                    pathType: Prefix
                    backend:
                      service:
                        name: ${service.metadata.name} # Use the service name
                        port:
                          number: 80
EOF

$ kubectl get resourcegraphdefinition
NAME             APIVERSION   KIND          STATE    AGE
my-application   v1alpha1     Application   Active   54s

最後にインスタンスと呼ばれてるものを作成する

$ cat <<'EOF' | kubectl apply -f -
apiVersion: kro.run/v1alpha1
kind: Application
metadata:
  name: my-application-instance
spec:
  name: my-awesome-app
  ingress:
    enabled: false
EOF

$ kubectl get application
NAME                      STATE    SYNCED   AGE
my-application-instance   ACTIVE   True     16s

すると、Deployment (Pod) と Service が作成されていることが確認できた

$ kubectl get all
NAME                                  READY   STATUS    RESTARTS   AGE
pod/my-awesome-app-7bd85c5d65-f6xz9   1/1     Running   0          54s
pod/my-awesome-app-7bd85c5d65-kxvbh   1/1     Running   0          54s
pod/my-awesome-app-7bd85c5d65-r4mzz   1/1     Running   0          54s

NAME                             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/kubernetes               ClusterIP   10.96.0.1       <none>        443/TCP   7m
service/my-awesome-app-service   ClusterIP   10.96.192.166   <none>        80/TCP    44s

NAME                             READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/my-awesome-app   3/3     3            3           54s

NAME                                        DESIRED   CURRENT   READY   AGE
replicaset.apps/my-awesome-app-7bd85c5d65   3         3         3       54s

ResourceGraphDefinition とは

先ほどの my-awesome-app という Kubernetes リソースを作成するための Kubernetes カスタム API を定義しているのが ResourceGraphDefinition
実際にカスタムリソース定義を確認してみると、resourcegraphdefinitions.kro.runapplications.kro.run の 2 種類存在する
resourcegraphdefinitions.kro.runapplications.kro.run を定義し、applications.kro.run で定義される Kubernetes API を呼び出して、Kubernetes にリソースを作成している

$ kubectl get crd
NAME                               CREATED AT
applications.kro.run               2025-06-22T00:38:50Z
resourcegraphdefinitions.kro.run   2025-06-22T00:30:01Z

ResourceGraphDefinition で定義できるのはドキュメント によると、以下の 5 つ

  • schema: ユーザがインスタンスを作成する際に指定できるフィールドを定義する
  • resources: 作成されるリソースを定義する
  • dependencies: リソース間の依存関係を定義する
  • conditions: リソースを作成する際の条件を定義する
  • status: カスタムリソースが公開するステータスフィールドを定義する

先ほど作成した ResourceGraphDefinition と照らし合わせて見ていく

まずは schema から

ここでは apiVersion, kind および spec といった、よくある Kubernetes API のスキーマを定義していて、これが複数の Kubernetes リソースをまとめて作成するための Kubernetes API になる
spec は Key/Value 形式で Key と Type を指定していて、default も指定できる

# kro uses this simple schema to create your CRD schema and apply it
# The schema defines what users can provide when they instantiate the RGD (create an instance).
schema:
  apiVersion: v1alpha1
  kind: Application
  spec:
    # Spec fields that users can provide.
    name: string
    image: string | default="nginx"
    ingress:
      enabled: boolean | default=false

status には deploymentConditionsavailableReplicas が定義されていて、Deployment ステータスを参照していることがわかる

# kro uses this simple schema to create your CRD schema and apply it
# The schema defines what users can provide when they instantiate the RGD (create an instance).
schema
  status:
    # Fields the controller will inject into instances status.
    deploymentConditions: ${deployment.status.conditions}
    availableReplicas: ${deployment.status.availableReplicas}

先ほど作成した Deployment のステータスを確認してみると、conditionsavailableReplicas が含まれていることがわかる

$ kubectl get deploy my-awesome-app -o yaml | yq eval '.status' -
availableReplicas: 3   # ここと
conditions:   # ここ
  - lastTransitionTime: "2025-06-30T01:31:12Z"
    lastUpdateTime: "2025-06-30T01:31:12Z"
    message: Deployment has minimum availability.
    reason: MinimumReplicasAvailable
    status: "True"
    type: Available
  - lastTransitionTime: "2025-06-30T01:31:01Z"
    lastUpdateTime: "2025-06-30T01:31:12Z"
    message: ReplicaSet "my-awesome-app-7bd85c5d65" has successfully progressed.
    reason: NewReplicaSetAvailable
    status: "True"
    type: Progressing
observedGeneration: 1
readyReplicas: 3
replicas: 3
updatedReplicas: 3

インスタンスの status を確認してみると、それらがそれぞれ deploymentConditionsavailableReplicas として定義されていることがわかる

$ kubectl get application my-application-instance -o yaml | yq eval '.status' -
availableReplicas: 3   # ここと
conditions:
  - lastTransitionTime: "2025-06-30T01:31:14Z"
    message: Instance reconciled successfully
    observedGeneration: 1
    reason: ReconciliationSucceeded
    status: "True"
    type: InstanceSynced
deploymentConditions:   # ここ
  - lastTransitionTime: "2025-06-30T01:31:12Z"
    lastUpdateTime: "2025-06-30T01:31:12Z"
    message: Deployment has minimum availability.
    reason: MinimumReplicasAvailable
    status: "True"
    type: Available
  - lastTransitionTime: "2025-06-30T01:31:01Z"
    lastUpdateTime: "2025-06-30T01:31:12Z"
    message: ReplicaSet "my-awesome-app-7bd85c5d65" has successfully progressed.
    reason: NewReplicaSetAvailable
    status: "True"
    type: Progressing
state: ACTIVE

status は Argo CD の Custom Health Checks と連携してリソースの状態を監視したり、additionalPrinterColumns を利用して、kubectl get application の出力をカスタマイズするなどが主な用途?

続いて resources

Kubernetes リソースの Deployment, Service, Ingress が定義されていて、インスタンスを作成すると、これら 3 つのリソースをコントローラーが作成してくれる

そして ${schema.spec.name}${schema.spec.image} など schema で定義したフィールドだったり、${deployment.spec.selector.matchLabels}${service.metadata.name} など他リソースのフィールドを参照することもできる

# Define the resources this API will manage.
resources:
  - id: deployment
    template:
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: ${schema.spec.name} # Use the name provided by user
      spec:
        replicas: 3
        selector:
          matchLabels:
            app: ${schema.spec.name}
        template:
          metadata:
            labels:
              app: ${schema.spec.name}
          spec:
            containers:
              - name: ${schema.spec.name}
                image: ${schema.spec.image} # Use the image provided by user
                ports:
                  - containerPort: 80

  - id: service
    template:
      apiVersion: v1
      kind: Service
      metadata:
        name: ${schema.spec.name}-service
      spec:
        selector: ${deployment.spec.selector.matchLabels} # Use the deployment selector
        ports:
          - protocol: TCP
            port: 80
            targetPort: 80

  - id: ingress
    includeWhen:
      - ${schema.spec.ingress.enabled} # Only include if the user wants to create an Ingress
    template:
      apiVersion: networking.k8s.io/v1
      kind: Ingress
      metadata:
        name: ${schema.spec.name}-ingress
        annotations:
          kubernetes.io/ingress.class: alb
          alb.ingress.kubernetes.io/scheme: internet-facing
          alb.ingress.kubernetes.io/target-type: ip
          alb.ingress.kubernetes.io/healthcheck-path: /health
          alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
          alb.ingress.kubernetes.io/target-group-attributes: stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=60
      spec:
        rules:
          - http:
              paths:
                - path: "/"
                  pathType: Prefix
                  backend:
                    service:
                      name: ${service.metadata.name} # Use the service name
                      port:
                        number: 80

試しに spec.imagehttpd を指定してインスタンスを作成してみる

$ cat <<'EOF' | kubectl apply -f -
apiVersion: kro.run/v1alpha1
kind: Application
metadata:
  name: my-httpd-application-instance
spec:
  name: my-httpd-app
  image: httpd
  ingress:
    enabled: false
EOF

$ kubectl get application
NAME                            STATE    SYNCED   AGE
my-application-instance         ACTIVE   True     29m
my-httpd-application-instance   ACTIVE   True     15s

imagehttpd として Deployment が作成できている

$ kubectl get deploy my-httpd-app -o yaml | yq eval '.spec.template.spec.containers[]' -
image: httpd
imagePullPolicy: Always
name: my-httpd-app
ports:
  - containerPort: 80
    protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File

さらに、includeWhen を使うことで、Ingress リソースを作成するかどうかを制御でき、これが前述の conditions に該当する
今回 ingress.enabledfalse としてインスタンスを作成したので、Ingress リソースは作成されなかった

# Define the resources this API will manage.
resources:
  - id: ingress
    includeWhen:
      - ${schema.spec.ingress.enabled} # Only include if the user wants to create an Ingress
    template:
      apiVersion: networking.k8s.io/v1
      kind: Ingress
      metadata:
        name: ${schema.spec.name}-ingress
        annotations:
          kubernetes.io/ingress.class: alb
          alb.ingress.kubernetes.io/scheme: internet-facing
          alb.ingress.kubernetes.io/target-type: ip
          alb.ingress.kubernetes.io/healthcheck-path: /health
          alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
          alb.ingress.kubernetes.io/target-group-attributes: stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=60
      spec:
        rules:
          - http:
              paths:
                - path: "/"
                  pathType: Prefix
                  backend:
                    service:
                      name: ${service.metadata.name} # Use the service name
                      port:
                        number: 80

最後に dependencies だが、ドキュメントを読む限り明示的に定義する必要はなく、自動的に依存関係が解決されるものと思われる
あまり自信はないが、実際に処理されているのはこのあたりだろう

アクセス制御

コントローラーの権限管理は Kubernetes の RBAC が利用でき、2 種類のモードがある

  • unrestricted
  • aggregation

デフォルトは unrestricted モードで、クラスタ内のすべてのリソースタイプに対し完全なアクセス権を付与する ClusterRole が作成されるとのこと

$ kubectl get clusterrole kro-cluster-role -o yaml | yq eval '.rules' -
- apiGroups:
    - '*'
  resources:
    - '*'
  verbs:
    - '*'

一方、aggregation モードでは、rbac.kro.run/aggregate-to-controller: "true" というラベルが付与された ClusterRole のルールを動的に取り込む、集約 ClusterRole が作成される
ちょっとややこしいが、複数の ClusterRole を組み合わせて、コントローラーが必要とする権限を提供する仕組み

aggregation モードで最初に付与される権限は以下の 2 つ

  • ResourceGraphDefinition へのフルアクセス
  • CustomResourceDefinition へのフルアクセス

Helm で rbac.modeaggregation を指定することで、有効にできるとのことなので試してみる

$ helm upgrade kro oci://ghcr.io/kro-run/kro/kro \
  --namespace kro \
  --set rbac.mode=aggregation

2 つの ClusterRole が作成された

$ kubectl get clusterrole | (head -n 1 && grep kro)
NAME                                                                   CREATED AT
kro:controller                                                         2025-06-30T02:05:23Z
kro:controller:static                                                  2025-06-30T02:05:23Z

ServiceAccount と紐づいているのは kro:controller で、前述の権限を持っている

$ kubectl get clusterrolebinding | (head -n 1 && grep kro)
NAME                                                            ROLE                                                                               AGE
kro:controller                                                  ClusterRole/kro:controller                                                         2m15s

$ kubectl get clusterrole kro:controller -o yaml | yq eval '.rules' -
- apiGroups:
    - kro.run
  resources:
    - resourcegraphdefinitions
  verbs:
    - create
    - delete
    - get
    - list
    - patch
    - update
    - watch
- apiGroups:
    - kro.run
  resources:
    - resourcegraphdefinitions/finalizers
  verbs:
    - update
- apiGroups:
    - kro.run
  resources:
    - resourcegraphdefinitions/status
  verbs:
    - get
    - patch
    - update
- apiGroups:
    - apiextensions.k8s.io
  resources:
    - customresourcedefinitions
  verbs:
    - get
    - list
    - watch
    - patch
    - update
    - delete

kro:controller:static には rbac.kro.run/aggregate-to-controller: "true" というラベルが付与されていて

$ kubectl get clusterrole kro:controller:static -o yaml | yq eval '.metadata.labels' -
app.kubernetes.io/component: controller
app.kubernetes.io/instance: kro
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: kro
app.kubernetes.io/part-of: kro
app.kubernetes.io/version: 0.3.0
helm.sh/chart: kro-0.3.0
rbac.kro.run/aggregate-to-controller: "true"   # ここ

ServiceAccount と紐づいている kro:controller の権限は kro:controller:static でコントロールしている

$ kubectl get clusterrole kro:controller:static -o yaml | yq eval '.rules' -
- apiGroups:
    - kro.run
  resources:
    - resourcegraphdefinitions
  verbs:
    - create
    - delete
    - get
    - list
    - patch
    - update
    - watch
- apiGroups:
    - kro.run
  resources:
    - resourcegraphdefinitions/finalizers
  verbs:
    - update
- apiGroups:
    - kro.run
  resources:
    - resourcegraphdefinitions/status
  verbs:
    - get
    - patch
    - update
- apiGroups:
    - apiextensions.k8s.io
  resources:
    - customresourcedefinitions
  verbs:
    - get
    - list
    - watch
    - patch
    - update
    - delete

この状態でインスタンスを作成しても、コントローラーに十分な権限がなくリソースは作成されない

$ cat <<'EOF' | kubectl apply -f -
apiVersion: kro.run/v1alpha1
kind: Application
metadata:
  name: my-application-instance-2
spec:
  name: my-awesome-app-2
  ingress:
    enabled: false
EOF

$ kubectl get application
NAME                            STATE    SYNCED   AGE
my-application-instance         ACTIVE   True     38m
my-application-instance-2                         63s   # STATE も SYNCED も更新されない
my-httpd-application-instance   ACTIVE   True     9m42s

ラベル rbac.kro.run/aggregate-to-controller: "true" を持つ、Deployment, Service, Ingress および ResourceGraphDefinition で定義した Kubernetes API へのフルアクセス権限をもった ClusterRole を作成する

$ cat <<'EOF' | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: kro:custom-role
  labels:
    rbac.kro.run/aggregate-to-controller: "true"
rules:
  - apiGroups:
      - apps
    resources:
      - deployments
    verbs:
      - '*'
  - apiGroups:
      - ''
    resources:
      - services
    verbs:
      - '*'
  - apiGroups:
      - networking.k8s.io
    resources:
      - ingresses
    verbs:
      - '*'
  - apiGroups:
      - kro.run
    resources:
      - applications
      - applications/status
    verbs:
      - '*'
EOF

想定通り ClusterRole kro:controller が更新される

$ kubectl get clusterrole kro:controller -o yaml | yq eval '.rules' -
- apiGroups:
    - kro.run
  resources:
    - resourcegraphdefinitions
  verbs:
    - create
    - delete
    - get
    - list
    - patch
    - update
    - watch
- apiGroups:
    - kro.run
  resources:
    - resourcegraphdefinitions/finalizers
  verbs:
    - update
- apiGroups:
    - kro.run
  resources:
    - resourcegraphdefinitions/status
  verbs:
    - get
    - patch
    - update
- apiGroups:
    - apiextensions.k8s.io
  resources:
    - customresourcedefinitions
  verbs:
    - get
    - list
    - watch
    - patch
    - update
    - delete
# ここから下が追加された
- apiGroups:
    - apps
  resources:
    - deployments
  verbs:
    - '*'
- apiGroups:
    - ""
  resources:
    - services
  verbs:
    - '*'
- apiGroups:
    - networking.k8s.io
  resources:
    - ingresses
  verbs:
    - '*'
- apiGroups:
    - kro.run
  resources:
    - applications
    - applications/status
  verbs:
    - '*'

権限が付与されたため、先ほど作成したインスタンスから Deployment と Service が作成された

$ kubectl get application
NAME                            STATE    SYNCED   AGE
my-application-instance         ACTIVE   True     43m
my-application-instance-2       ACTIVE   True     5m55s
my-httpd-application-instance   ACTIVE   True     14m

$ kubectl get deploy
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
my-awesome-app     3/3     3            3           43m
my-awesome-app-2   3/3     3            3           2m57s
my-httpd-app       3/3     3            3           14m

$ kubectl get svc
NAME                       TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes                 ClusterIP   10.96.0.1       <none>        443/TCP   50m
my-awesome-app-2-service   ClusterIP   10.96.246.77    <none>        80/TCP    3m1s
my-awesome-app-service     ClusterIP   10.96.192.166   <none>        80/TCP    43m
my-httpd-app-service       ClusterIP   10.96.99.242    <none>        80/TCP    14m

コントローラーの利用する RBAC はもちろん重要
ただ、実際に利用する際は ResourceGraphDefinition で作成する Kubernetes カスタム API をテンプレートのように活用するはずなので、こちらの RBAC を適切にコントロールするのも重要
もちろん ResourceGraphDefinition を自由に作成されてしまうと困るため、ResourceGraphDefinition の RBAC もきちんとコントロールする必要があると思う

クラウドプロバイダのリソース管理

冒頭でも触れたように、kro では、Kubernetes リソースだけでなく、クラウドプロバイダのリソースも管理することができるらしい (大変そうだったので検証はしてない)
公式の Examples によると、例えば Google Cloud であれば、KCC を利用して GKE Cluster などを管理できるとのこと

Reference: kro.run

まとめ

冒頭で Helm + Crossplane みたいなイメージと記載したが、触ってみた感じ Helm とは全くの別物という感想
(Crossplane は存在を知っているが、触れたことはないのでわかりません)

Helm が豊富なチャートを活用して Kubernetes のエコシステム全体の管理に強みを持つのに対し、kro は Kubernetes リソースをカスタム API としてカプセル化し、特定のサービスやプロダクトに最適化されたテンプレートを通じて、アプリケーションを管理するのに特化している印象です
Helm でも同様のことはできなくないと思うが、私自身は Go Template を書くよりも、YAML を書く方がまだいい
ただし、現状はループ処理などが扱えないなどの制約もありそうなので、複雑なことを求めると大変かもしれない

大量の ResourceGraphDefinition と戦う日がくるのか・・・

References

FIAT ドブロの故障について

はじめに

今年、新車で FIAT のドブロを購入したのだが、数ヶ月後に燃料系のトラブルが発生した
故障の概要と、修理から車が戻ってくるまでの経緯を備忘録として残しておく
ドブロの購入を考えている方の参考になればと思う

FIAT ドブロ

家族構成が変わるため、日常で 5 人乗りできる車を探していた
デザイン的な理由で国産ミニバンを避けたいという思いと、2 列目のシートが独立しており、チャイルドシート (新生児用) を 1 つ載せても、残り 2 人の子供が余裕を持って座れる点に魅力を感じ、ドブロを選んだ
ディーゼルエンジンなので燃料は軽油となり、このご時世、燃料コストを抑えられる点も非常に魅力的だった

ベルランゴやリフターではなくドブロを選んだ理由は、以前からなんとなく FIAT の車に憧れがあったこともあるが、何よりディーラーが一番近かったからというのが大きい

動かなくなったドブロ

車の購入からおよそ 3 ヶ月後のことだった
車を運転していると、ダッシュボードにエラーメッセージが表示された
慌てて車を停めたのを最後に、何度ボタンを押してもエンジンはかからなくなった

ディーラーに連絡してみたが、エラーメッセージだけでは原因も対処方法もわからないとのことで、車を見てもらうことになった

車をディーラーに運び入れ、状態を見てもらったが、すぐには原因はわからないとのことで、その日は代車で帰宅することになった
代車は 500X だった

数日後に営業から電話があり、燃料タンクからエンジンまでうまく燃料が流れないのが原因でエンジンがかからなくなっているとのこと
走行距離も少なかったため、いわゆる初期不良のようなものだと思う
燃料タンク周りのパーツを総交換することになったが、在庫がディーラーにはなく、メーカーから取り寄せができ次第、修理することになった

せいぜい 1 ヶ月もあれば車は返ってくるだろうと思っていたが、甘かった
メーカーにも在庫がないとのことで、取り寄せがなかなか進まず、結果として車を預けてから返ってくるまでに 5 ヶ月を要することになった

この間、代車生活を送ることになるのだが、 500X は家族 5 人で乗るには小さく、家族で車を利用したお出かけはほとんどできなかった
燃料がハイオクで、かつお世辞にも燃費がいいとはいえず、車の利用は生活する上で必要最低限となった

これは 500X が悪いわけではなく、あくまで我が家には合わなかったということだ

修理費用や代車の費用は一切かからなかったのが、不幸中の幸いである

おわりに

車が返ってきておよそ 1 ヶ月が経過したが、特に不調は見られず、気になる点も特にない
FIAT ドブロは個性的で魅力的な車だが、やはり「外車」であるのだと身をもって学んだ

Argo CD で Kustomize の --enable-helm オプションを利用する

はじめに

Argo CD から Kustomize と Helm でソフトウェアをデプロイする機会があり、デフォルトだと Kustomize の --enable-helm オプションが利用できなかったため、オプションの有効化方法を調べて kind で検証した際のメモ

各種ソフトウェアのバージョンは下記の通り

  • kind : 0.22.0
  • Helm : 3.15.2
  • Argo CD : 2.11.3

Argo CD のインストール

まずは kind でクラスタを起動する

$ cat << EOF | kind create cluster --config -
apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
nodes:
  - role: control-plane
  - role: worker
EOF

$ kubectl get po -n kube-system
NAME                                         READY   STATUS    RESTARTS   AGE
coredns-76f75df574-bz4jp                     1/1     Running   0          110s
coredns-76f75df574-t4q6b                     1/1     Running   0          110s
etcd-kind-control-plane                      1/1     Running   0          2m5s
kindnet-49mww                                1/1     Running   0          111s
kindnet-x2dg6                                1/1     Running   0          106s
kube-apiserver-kind-control-plane            1/1     Running   0          2m5s
kube-controller-manager-kind-control-plane   1/1     Running   0          2m5s
kube-proxy-w4c2m                             1/1     Running   0          106s
kube-proxy-xsk29                             1/1     Running   0          111s
kube-scheduler-kind-control-plane            1/1     Running   0          2m5s

その後、Argo CD をインストールする
この時、values.yamlconfigs.cmpConfig Management Plugins を有効にするの忘れないこと
Plugin は argocd-repo-server のサイドカーを介して有効化されるため、repoServer.extraContainersrepoServer.volumes も設定すること
詳細な設定方法は上記ドキュメントにまとまっているため要参照

$ kubectl create namespace argocd

$ helm repo add argo https://argoproj.github.io/argo-helm

$ cat << EOF > values.yaml
configs:
  cmp:
    create: true
    plugins:
      enable-helm:     // ConfigMap `argocd-cmp-cm``enable-helm.yaml` という名前の ConfigManagementPlugin リソース定義が作成される
        generate:
          command: ['sh', '-c']
          args: ['kustomize build --enable-helm']     // `--enable-helm` オプションを有効化している
repoServer:
  extraContainers:
    - name: enable-helm
      command: ['/var/run/argocd/argocd-cmp-server']
      image: 'quay.io/argoproj/argocd:v2.11.3'
      securityContext:
        runAsNonRoot: true
        runAsUser: 999
      volumeMounts:
        - name: var-files
          mountPath: /var/run/argocd
        - name: plugins
          mountPath: /home/argocd/cmp-server/plugins
        - name: argocd-cmp-cm     // ConfigMap `argocd-cmp-cm``enable-helm.yaml` をマウントしている
          mountPath: /home/argocd/cmp-server/config/plugin.yaml
          subPath: enable-helm.yaml
        - name: cmp-tmp
          mountPath: /tmp
  volumes:
    - name: argocd-cmp-cm     // ConfigMap `argocd-cmp-cm` のマウント
      configMap:
        name: argocd-cmp-cm
    - name: cmp-tmp
      emptyDir: {}
EOF

$ helm install argocd argo/argo-cd \
  -n argocd \
  -f values.yaml

Argo CD のインストール後、ConfigMap argocd-cmp-cm を確認してみると ConfigManagementPlugin リソース定義が確認できる

$ kubectl get po -n argocd
NAME                                                READY   STATUS      RESTARTS   AGE
argocd-application-controller-0                     1/1     Running     0          84s
argocd-applicationset-controller-64f8f4bcb4-5vdqc   1/1     Running     0          84s
argocd-dex-server-7775c58c9b-lr8hc                  1/1     Running     0          84s
argocd-notifications-controller-9657c4684-5djvq     1/1     Running     0          84s
argocd-redis-84495849c9-gtmvm                       1/1     Running     0          84s
argocd-redis-secret-init-jf4xc                      0/1     Completed   0          97s
argocd-repo-server-7c44fb476f-6g5dc                 2/2     Running     0          84s
argocd-server-bb7955d9c-zdtqv                       1/1     Running     0          84s

$ kubectl get cm -n argocd argocd-cmp-cm -o yaml | yq eval '.data' -
enable-helm.yaml: |
  apiVersion: argoproj.io/v1alpha1
  kind: ConfigManagementPlugin
  metadata:
    name: enable-helm
  spec:
    generate:
      args:
      - kustomize build --enable-helm
      command:
      - sh
      - -c

Gitea のインストール

Git リポジトリが欲しいので Gitea をインストールする

$ kubectl create ns gitea

$ cat << EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: gitea
  namespace: argocd
spec:
  project: default
  source:
    repoURL: 'https://dl.gitea.io/charts/'
    chart: gitea
    targetRevision: 10.2.0
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: gitea
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
EOF

$ kubectl get po -n gitea
NAME                                          READY   STATUS    RESTARTS   AGE
gitea-7fb557df44-wmlcv                        1/1     Running   0          2m7s
gitea-postgresql-ha-pgpool-5ccc77b4df-9tzgd   1/1     Running   0          2m7s
gitea-postgresql-ha-postgresql-0              1/1     Running   0          2m7s
gitea-postgresql-ha-postgresql-1              1/1     Running   0          2m7s
gitea-postgresql-ha-postgresql-2              1/1     Running   0          2m7s
gitea-redis-cluster-0                         1/1     Running   0          2m7s
gitea-redis-cluster-1                         1/1     Running   0          2m7s
gitea-redis-cluster-2                         1/1     Running   0          2m7s

$ kubectl get svc -n gitea
NAME                                      TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)              AGE
gitea-http                                ClusterIP   None           <none>        3000/TCP             106s
gitea-postgresql-ha-pgpool                ClusterIP   10.96.52.215   <none>        5432/TCP             106s
gitea-postgresql-ha-postgresql            ClusterIP   10.96.173.57   <none>        5432/TCP             106s
gitea-postgresql-ha-postgresql-headless   ClusterIP   None           <none>        5432/TCP             106s
gitea-redis-cluster                       ClusterIP   10.96.243.12   <none>        6379/TCP             106s
gitea-redis-cluster-headless              ClusterIP   None           <none>        6379/TCP,16379/TCP   106s
gitea-ssh                                 ClusterIP   None           <none>        22/TCP               106s

ポートフォワードして、localhost:3000 にブラウザからアクセスし、ユーザ登録とリポジトリ作成を行う

$ kubectl port-forward svc/gitea-http -n gitea 3000:3000

作成したリポジトリは Argo CD に登録する

$ kubectl port-forward svc/argocd-server -n argocd 8000:80

$ argocd login localhost:8000

$ argocd repo add http://gitea-http.gitea.svc.cluster.local:3000/sample/sample.git --server localhost:8000 --username username --password password
Repository 'http://gitea-http.gitea.svc.cluster.local:3000/sample/sample.git' added

Prometheus のインストール

--enable-helm オプションが有効かどうかを確認するのに Kustomize + Helm で Prometheus をインストールしてみる

まずは kustomization.yaml ファイルを Gitea にプッシュする

$ git clone http://localhost:3000/sample/sample
$ cd sample

$ cat << EOF > kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
helmCharts:
  - name: prometheus
    repo: 'https://prometheus-community.github.io/helm-charts'
    version: 25.22.0
    releaseName: prometheus
    namespace: prometheus
EOF

$ git add .
$ git commit -m 'first commit'
$ git push origin

Application リソースを作成し、Prometheus をインストールする
この時 spec.source.plugin の指定を忘れないこと

$ kubectl create ns prometheus

$ cat << EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: prometheus
  namespace: argocd
spec:
  project: default
  source:
    repoURL: 'http://gitea-http.gitea.svc.cluster.local:3000/sample/sample.git'
    targetRevision: HEAD
    path: .
    plugin:
      name: enable-helm     // ここで Plugin を指定している
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: prometheus
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
EOF

Pod が正常に起動したのでポートフォワードして localhost:8080 にアクセスしてみると、Prometheus がインストールできたことを確認できる

$ kubectl get po -n prometheus
NAME                                                 READY   STATUS    RESTARTS   AGE
prometheus-alertmanager-0                            1/1     Running   0          103s
prometheus-kube-state-metrics-d7875bd57-l6bp7        1/1     Running   0          103s
prometheus-prometheus-node-exporter-d45q6            1/1     Running   0          103s
prometheus-prometheus-node-exporter-rl5l2            1/1     Running   0          103s
prometheus-prometheus-pushgateway-56985ddc76-qrxft   1/1     Running   0          103s
prometheus-server-67d6d48456-k9qmv                   2/2     Running   0          103s

$ kubectl get app -n argocd
NAME         SYNC STATUS   HEALTH STATUS
gitea        Synced        Healthy
prometheus   Synced        Healthy

$ kubectl port-forward svc/prometheus-server -n prometheus 8080:80

ちなみに、Application で spec.source.plugin を指定しなかった場合、must specify --enable-helm というエラーメッセージが出力されており、--enable-helm オプションが無効であることを確認できる

$ cat << EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: prometheus
  namespace: argocd
spec:
  project: default
  source:
    repoURL: 'http://gitea-http.gitea.svc.cluster.local:3000/sample/sample.git'
    targetRevision: HEAD
    path: .
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: prometheus
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
EOF

$ kubectl logs -l app.kubernetes.io/name=argocd-repo-server -c repo-server -n argocd
[...]
time="2024-06-28T03:51:41Z" level=error msg="finished unary call with code Unknown" error="Manifest generation error (cached): `kustomize build <path to cached source> failed exit status 1: Error: trouble configuring builtin HelmChartInflationGenerator with config: `\nname: prometheus\nnamespace: prometheus\nreleaseName: prometheus\nrepo: https://prometheus-community.github.io/helm-charts\nversion: 25.22.0\n`: must specify --enable-helm" grpc.code=Unknown grpc.method=GenerateManifest grpc.service=repository.RepoServerService grpc.start_time="2024-06-28T03:51:41Z" grpc.time_ms=33.461 span.kind=server system=grpc
[...]

eBPF の入門として Cilium Network Policy について調べてみた

はじめに

eBPF の勉強として Cilium Network Policy がどのように反映され、どのように制御されているかソースコードを追いかけてみたのと、実際に L7 の CiliumNetworkPolicy を試してみたのでそのまとめ

対象の Cilium バージョンは 1.15.4

CiliumNetworkPolicy リソースの反映

まずは CiliumNetworkPolicy リソースを作成した際、どのように反映されるかを調べてみた

pkg/k8s/watchers/cilium_network_policy.goK8sWatcher.ciliumNetworkPoliciesInit() で CiliumNetworkPolicy リソースの追加、更新、削除イベントを検知している

ポリシーの反映にはエンドポイント再構築 (eBPF プログラムの再ロードと eBPF Map の更新) を伴うようで、cilium/daemon/cmd/policy.goDaemon.policyAdd() でポリシーリポジトリにポリシーを配布し、エンドポイントの再構築をキュー経由でトリガーしている

エンドポイントの再構築は cilium/pkg/endpoint/policy.goEndpoint.regenerate() で行われている

Cilium Network Policy は eBPF で実装されているので、eBPF でルールを扱えるように pkg/endpoint/bpf.goEndpoint.addPolicyKey() で eBPF Map へ変換され、書き込みは pkg/bpf/map_linux.goMap.Update() で行われている

Go からみた eBPF Map の構造体は pkg/maps/policymap/policymap.goPolicyKey で定義されている

type PolicyKey struct {
    Prefixlen        uint32 `align:"lpm_key"`
    Identity         uint32 `align:"sec_label"`
    TrafficDirection uint8  `align:"egress"`
    Nexthdr          uint8  `align:"protocol"`
    DestPortNetwork  uint16 `align:"dport"` // In network byte-order
}

eBPF からみた eBPF Map の構造体は bpf/lib/common.hpolicy_key で定義されている

struct policy_key {
    struct bpf_lpm_trie_key lpm_key;
    __u32       sec_label;
    __u8        egress:1,
                pad:7;
    __u8        protocol; /* can be wildcarded if 'dport' is fully wildcarded */
    __u16       dport; /* can be wildcarded with CIDR-like prefix */
};

これで L3/L4 の Cilium Network Policy は eBPF で評価できるはず

Cilium Network Policy は L7 にも対応しており、eBPF ではなく Envoy で制御されている
Envoy へポリシーを配布しているのは pkg/envoy/xds_server.goxdsServer.UpdateNetworkPolicy()

if policy != nil {
    policyRevision := policy.Revision
    callback = func(err error) {
        if err == nil {
            go ep.OnProxyPolicyUpdate(policyRevision)
        }
    }
}

proxyPolicyRevision を更新して Envoy に反映している

func (e *Endpoint) OnProxyPolicyUpdate(revision uint64) {
    // NOTE: unconditionalLock is used here because this callback has no way of reporting an error
    e.unconditionalLock()
    if revision > e.proxyPolicyRevision {
        e.proxyPolicyRevision = revision
    }
    e.unlock()
}

Cilium Network Policy による評価

続いては Cilium Network Policy でパケットをどのように評価し、パスしたりドロップしたりしているか、Endpoint to Endpoint (同一ホスト上の Pod to Pod) 通信、かつ IPv4 の Egress Policy (通信元でのポリシー評価) ケースでの実装を追ってみる

Endpoint to Endpoint においてパケットがどのように処理されるかの概要は 公式ドキュメント がわかりやすい

Endpoint to Endpoint 通信の場合、コンテナからパケットが出る際の tc でフックされ bpf/bpf_lxc.ccil_from_container() が呼び出される

L3/L4 のポリシーを評価しているのは bpf/lib/policy.h__policy_can_access() で、パケットから得られた情報を eBPF Map と同じ構造にセットし、システムコール bpf_map_lookup_elem で eBPF Map と突き合わせを行いポリシーを評価している

struct policy_key key = {
    .lpm_key = { POLICY_FULL_PREFIX, {} }, /* always look up with unwildcarded data */
    .sec_label = remote_id,
    .egress = !dir,
    .pad = 0,
    .protocol = proto,
    .dport = dport,
};

[...]

policy = map_lookup_elem(map, &key);

if (likely(policy && !policy->wildcard_dport)) {
    cilium_dbg3(ctx, DBG_L4_CREATE, remote_id, local_id,
            dport << 16 | proto);
    *match_type = POLICY_MATCH_L3_L4;       /* 1. id/proto/port */
    goto check_policy;
}

[...]

check_policy:
    return __account_and_check(ctx, policy, ext_err, proxy_port);

L7 のポリシーを評価しているのは Envoy で、eBPF からのパケットリダイレクトには Linux カーネルの transparent proxy (tproxy) を利用しているとのこと

tproxy が有効な場合、bpf/lib/proxy.h__ctx_redirect_to_proxy() から bpf_sk_assign() ヘルパー関数を呼び出し、tproxy のソケットをアサインしてパケットをリダイレクトしている

#ifdef ENABLE_TPROXY
static __always_inline int
assign_socket_tcp(struct __ctx_buff *ctx,
          struct bpf_sock_tuple *tuple, __u32 len, bool established)
{
    int result = DROP_PROXY_LOOKUP_FAILED;
    struct bpf_sock *sk;
    __u32 dbg_ctx;

    sk = skc_lookup_tcp(ctx, tuple, len, BPF_F_CURRENT_NETNS, 0);
    if (!sk)
        goto out;

    if (established && sk->state == BPF_TCP_TIME_WAIT)
        goto release;
    if (established && sk->state == BPF_TCP_LISTEN)
        goto release;

    dbg_ctx = sk->family << 16 | ctx->protocol;
    result = sk_assign(ctx, sk, 0);
    cilium_dbg(ctx, DBG_SK_ASSIGN, -result, dbg_ctx);
    if (result == 0)
        result = CTX_ACT_OK;
    else
        result = DROP_PROXY_SET_FAILED;
release:
    sk_release(sk);
out:
    return result;
}

試してみる

Envoy へのパケットのリダイレクトを除き、概ね仕組みがわかったので実際に試してみる

まずは、Kind でさくっとクラスタを立ち上げて Cilium をインストールする

$ cat << EOF > cluster.yaml
apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
nodes:
  - role: control-plane
    extraPortMappings:
      - containerPort: 30599
        hostPort: 8080
        listenAddress: 127.0.0.1
  - role: worker
  - role: worker
  - role: worker
networking:
  disableDefaultCNI: true
  podSubnet: 10.10.0.0/16
  serviceSubnet: 10.11.0.0/16
  apiServerAddress: 127.0.0.1
  apiServerPort: 6443
EOF

$ kind create cluster --config cluster.yaml

$ helm install cilium cilium/cilium --version 1.15.4 \
    --namespace kube-system \
    --set envoy.enabled=true

$ kubectl get po -n kube-system
NAME                                         READY   STATUS    RESTARTS   AGE
cilium-8d4qp                                 1/1     Running   0          22m
cilium-envoy-2676w                           1/1     Running   0          22m
cilium-envoy-bhbrn                           1/1     Running   0          22m
cilium-envoy-thlmq                           1/1     Running   0          22m
cilium-envoy-wzkvw                           1/1     Running   0          22m
cilium-jnptw                                 1/1     Running   0          22m
cilium-operator-5d64788c99-rhl76             1/1     Running   0          22m
cilium-operator-5d64788c99-v7dtk             1/1     Running   0          22m
cilium-tpmkw                                 1/1     Running   0          22m
cilium-twp5s                                 1/1     Running   0          22m
coredns-76f75df574-dsb9n                     1/1     Running   0          23m
coredns-76f75df574-xgx7z                     1/1     Running   0          23m
etcd-kind-control-plane                      1/1     Running   0          23m
kube-apiserver-kind-control-plane            1/1     Running   0          23m
kube-controller-manager-kind-control-plane   1/1     Running   0          23m
kube-proxy-fq296                             1/1     Running   0          22m
kube-proxy-j2wfc                             1/1     Running   0          22m
kube-proxy-lppbc                             1/1     Running   0          23m
kube-proxy-pdztg                             1/1     Running   0          22m
kube-scheduler-kind-control-plane            1/1     Running   0          23m

テスト用のクライアントとサーバアプリケーションを起動する

$ kubectl create ns sample
$ kubectl run client --image alpine --labels 'app=client' -n sample --command -- sleep infinity
$ kubectl exec client -c client -n sample -- sh -c 'apk --update add curl ca-certificates'
$ kubectl run server --image hashicorp/http-echo --labels 'app=server' --port 5678 -n sample -- -text='hello world'
$ kubectl get po -n sample
NAME     READY   STATUS    RESTARTS   AGE
client   1/1     Running   0          10m
server   1/1     Running   0          3m35s

$ kubectl expose po server --port=5678 --target-port=5678 -n sample
$ kubectl get svc -n sample
NAME     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
server   ClusterIP   10.11.129.123   <none>        5678/TCP   3s

$ kubectl exec client -n sample -- curl -s server.sample.svc.cluster.local:5678
hello world

ここから L7 の CiliumNetworkPolicy を作成していくのだが、その前に現状のポリシーの様子を確認してみる
今回は Egress Policy を検証するため、client アプリケーションのエンドポイントを確認する

$ kubectl get po -n sample -o wide
NAME     READY   STATUS    RESTARTS   AGE   IP          NODE           NOMINATED NODE   READINESS GATES
client   1/1     Running   0          33m   10.0.2.42   kind-worker2   <none>           <none>
server   1/1     Running   0          32m   10.0.1.97   kind-worker3   <none>           <none>

$ kubectl get po -n kube-system -l k8s-app=cilium -o wide | grep kind-worker2
cilium-jnptw   1/1     Running   0          64m   172.18.0.3   kind-worker2         <none>           <none>

client アプリケーションのエンドポイントを確認してみると 1955 であるとわかる

$ kubectl exec cilium-jnptw -c cilium-agent -n kube-system -- cilium-dbg endpoint list
ENDPOINT   POLICY (ingress)   POLICY (egress)   IDENTITY   LABELS (source:key[=value])                                             IPv6   IPv4        STATUS
           ENFORCEMENT        ENFORCEMENT
191        Disabled           Disabled          4          reserved:health                                                                10.0.2.26   ready
1402       Disabled           Disabled          1          reserved:host                                                                              ready
1955       Disabled           Enabled           28112      k8s:app=client                                                                 10.0.2.42   ready
                                                           k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=sample
                                                           k8s:io.cilium.k8s.policy.cluster=default
                                                           k8s:io.cilium.k8s.policy.serviceaccount=default
                                                           k8s:io.kubernetes.pod.namespace=sample

CiliumNetworkPolicy を作成してないエンドポイントの状態

$ kubectl exec cilium-jnptw -c cilium-agent -n kube-system -- cilium-dbg bpf policy get -n 1955
POLICY   DIRECTION   IDENTITY   PORT/PROTO   PROXY PORT   AUTH TYPE   BYTES   PACKETS   PREFIX
Allow    Ingress     0          ANY          NONE         disabled    0       0         0
Allow    Ingress     1          ANY          NONE         disabled    0       0         0
Allow    Egress      0          ANY          NONE         disabled    0       0         0

CiliumNetworkPolicy を作成してみる

$ cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: sample
  namespace: sample
spec:
  endpointSelector:
    matchLabels:
      app: client
  egress:
  - toEndpoints:
    - matchLabels:
        app: server
    toPorts:
    - ports:
      - port: '5678'
        protocol: TCP
      rules:
        http:
        - method: GET
          path: /
  - toEndpoints:
    - matchLabels:
        k8s:io.kubernetes.pod.namespace: kube-system
        k8s-app: kube-dns
EOF

Egress Policy が増えていることが確認できる

$ kubectl exec cilium-jnptw -c cilium-agent -n kube-system -- cilium-dbg bpf policy get -n 1955
POLICY   DIRECTION   IDENTITY   PORT/PROTO   PROXY PORT   AUTH TYPE   BYTES   PACKETS   PREFIX
Allow    Ingress     0          ANY          NONE         disabled    0       0         0
Allow    Ingress     1          ANY          NONE         disabled    0       0         0
Allow    Egress      8600       ANY          NONE         disabled    0       0         0
Allow    Egress      11584      5678/TCP     12440        disabled    0       0         24
Allow    Egress      15567      ANY          NONE         disabled    0       0         0

cilium-agent のログを確認してみると、ソースコードを追いかけてわかったように、k8s-watcher で CiliumNetworkPolicy リソースを検知したのち、ポリシーがエンドポイントにインポートされ、eBPF プログラムと eBPF Map の再構築が行われている様子がわかる

$ kubectl logs cilium-jnptw -n kube-system
time="2024-06-07T06:32:28Z" level=info msg="Policy Add Request" ciliumNetworkPolicy="[&{EndpointSelector:{\"matchLabels\":{\"any:app\":\"client\",\"k8s:io.kubernetes.pod.namespace\":\"sample\"}} NodeSelector:{} Ingress:[] IngressDeny:[] Egress:[{EgressCommonRule:{ToEndpoints:[{\"matchLabels\":{\"any:app\":\"server\",\"k8s:io.kubernetes.pod.namespace\":\"sample\"}}] ToRequires:[] ToCIDR: ToCIDRSet:[] ToEntities:[] ToServices:[] ToGroups:[] aggregatedSelectors:[]} ToPorts:[{Ports:[{Port:5678 Protocol:TCP}] TerminatingTLS:<nil> OriginatingTLS:<nil> ServerNames:[] Listener:<nil> Rules:0xc0003b51f0}] ToFQDNs:[] ICMPs:[] Authentication:<nil>} {EgressCommonRule:{ToEndpoints:[{\"matchLabels\":{\"any:k8s-app\":\"kube-dns\",\"k8s:io.kubernetes.pod.namespace\":\"kube-system\"}}] ToRequires:[] ToCIDR: ToCIDRSet:[] ToEntities:[] ToServices:[] ToGroups:[] aggregatedSelectors:[]} ToPorts:[] ToFQDNs:[] ICMPs:[] Authentication:<nil>}] EgressDeny:[] Labels:[k8s:io.cilium.k8s.policy.derived-from=CiliumNetworkPolicy k8s:io.cilium.k8s.policy.name=sample k8s:io.cilium.k8s.policy.namespace=sample k8s:io.cilium.k8s.policy.uid=7756e978-2aa8-4baf-a4ed-9cfe64d01088] Description:}]" policyAddRequest=5eb088ab-a5a0-4cc0-b319-3af097f3ce51 subsys=daemon
time="2024-06-07T06:32:28Z" level=info msg="Imported CiliumNetworkPolicy" ciliumNetworkPolicyName=sample k8sApiVersion= k8sNamespace=sample subsys=k8s-watcher
time="2024-06-07T06:32:28Z" level=info msg="Policy imported via API, recalculating..." policyAddRequest=5eb088ab-a5a0-4cc0-b319-3af097f3ce51 policyRevision=26 subsys=daemon
time="2024-06-07T06:32:29Z" level=info msg="Re-pinning map with ':pending' suffix" bpfMapName=cilium_calls_01955 bpfMapPath=/sys/fs/bpf/tc/globals/cilium_calls_01955 subsys=bpf
time="2024-06-07T06:32:29Z" level=info msg="Unpinning map after successful recreation" bpfMapName=cilium_calls_01955 bpfMapPath="/sys/fs/bpf/tc/globals/cilium_calls_01955:pending" subsys=bpf
time="2024-06-07T06:32:29Z" level=info msg="Rewrote endpoint BPF program" ciliumEndpointName=sample/client containerID=7f7d63e703 containerInterface= datapathPolicyRevision=25 desiredPolicyRevision=26 endpointID=1955 identity=28112 ipv4=10.0.2.42 ipv6= k8sPodName=sample/client subsys=endpoint

せっかくなので、ポリシーの eBPF Map を確認しようとしたがキャッシュが無効化されており参照できないとのこと

$ kubectl exec cilium-jnptw -c cilium-agent -n kube-system -- cilium-dbg map list
Name                      Num entries   Num errors   Cache enabled
cilium_tunnel_map         3             0            true
cilium_lb4_services_v2    12            0            true
cilium_lb4_reverse_nat    5             0            true
cilium_policy_01402       0             0            false
cilium_policy_01955       0             0            false
cilium_runtime_config     0             0            false
cilium_ipcache            18            0            true
cilium_lb4_backends_v3    7             0            true
cilium_lb4_source_range   0             0            true
cilium_policy_00191       0             0            false
cilium_lxc                4             0            true
cilium_auth_map           0             0            false
cilium_node_map           0             0            false
cilium_metrics            0             0            false
cilium_l2_responder_v4    0             0            false

$ kubectl exec cilium-jnptw -c cilium-agent -n kube-system -- cilium-dbg map get cilium_policy_01955
Cache is disabled

念の為、アプリケーションの疎通確認

$ kubectl exec client -n sample -- curl -s server.sample.svc.cluster.local:5678
hello world

CiliumNetworkPolicy での許可メソッドを POST に変更すると、しっかりと拒否されることも確認できた

$ cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: sample
  namespace: sample
spec:
  endpointSelector:
    matchLabels:
      app: client
  egress:
  - toEndpoints:
    - matchLabels:
        app: server
    toPorts:
    - ports:
      - port: '5678'
        protocol: TCP
      rules:
        http:
        - method: POST
          path: /
  - toEndpoints:
    - matchLabels:
        k8s:io.kubernetes.pod.namespace: kube-system
        k8s-app: kube-dns
EOF

$ kubectl exec client -n sample -- curl -s server.sample.svc.cluster.local:5678
Access denied

さいごに

まだまだ細かいところで何をしているかはわからないけど、Cilium の中で eBPF がどのように利用されているのかが大枠で把握でき、今後業務で利用することがあるかはわからないが、Cilium Network Policy で何かトラブルが起きた時にソースコードから原因分析するというアプローチが取れそう
eBPF については eBPF Map への書き込みや参照がどのように行われているかを把握でてきた程度だが、とりあえず入門としては十分なんじゃないかと思う
どこかで実際に eBPF プログラムのサンプルを書いて動かすということをやってみたい

なお、Cilium 自体への興味も出てきて、kube-proxy を置き換えることができたり、サービスメッシュとしても振る舞ったりするとのことなので、その辺りも深ぼって調査してみたい

さいごに、普段アプリケーションの読み書きをすることがほとんどないためソースコードを追いかけるのに非常に時間がかかったけど、思ったよりも楽しめたのはよかった (次はもっと効率的に作業できるとうれしい)

Reference