Scaling on Kubernetes

Scale a Kubernetes deployment's replica count with a small Oban.Pro.Cloud module built on Req. The API server, token, and deployment are resolved in init/1, and scale/2 patches the deployment's scale subresource.

def deps do
  [
    {:req, "~> 0.5"}
  ]
end

The Module

The Oban.Pro.Cloud.init/1 callback reads the API server and bearer token from the system environment, leaving the namespace and deployment to pass through as options. The Oban.Pro.Cloud.scale/2 callback sends a strategic merge patch to set replicas and returns {:ok, conf} so the scaler can track the response.

defmodule MyApp.Kubernetes do
  @behaviour Oban.Pro.Cloud

  @impl Oban.Pro.Cloud
  def init(opts) do
    opts
    |> Keyword.put_new(:api_server, System.fetch_env!("KUBERNETES_API_SERVER"))
    |> Keyword.put_new(:bearer_token, System.fetch_env!("KUBERNETES_TOKEN"))
    |> Map.new()
  end

  @impl Oban.Pro.Cloud
  def scale(desired, conf) do
    url =
      "#{conf.api_server}/apis/apps/v1/namespaces/#{conf.namespace}/" <>
        "deployments/#{conf.deployment}/scale"

    headers = [{"content-type", "application/strategic-merge-patch+json"}]
    body = JSON.encode!(%{spec: %{replicas: desired}})

    case Req.patch(url, auth: {:bearer, conf.bearer_token}, headers: headers, body: body) do
      {:ok, %{status: status}} when status in 200..299 -> {:ok, conf}
      {:ok, response} -> {:error, response}
      {:error, reason} -> {:error, reason}
    end
  end
end

Running inside the cluster, point api_server at https://kubernetes.default.svc and read the token from the mounted service account at /var/run/secrets/kubernetes.io/serviceaccount/token. The service account needs patch on the deployments/scale subresource through a role binding.

Using It

Point a scaler at the module and pass the deployment it should scale:

{DynamicScaler,
  scalers: [range: 1..5, cloud: {MyApp.Kubernetes, namespace: "default", deployment: "workers"}]}