Skip to content

Setting up Controllers

Controllers are the brains of Drove cluster. For HA, at least 2 controllers should be set up.

Upgrading from 1.33.x?

1.34.x will not start on an old config. Go through the migration guide checklist before rolling out new images.

Please note the following behaviour about controllers: Please note the following behaviour about controllers:

  • Only one controller is leader at a time. A leader controller does not relinquish control till the process is stopped or dies
  • A controller process will die when it loses connectivity to Zookeeper
  • The process/container for controller should keep restarting till it gets connectivity
  • All decisions for the cluster are taken by the leader controller only
  • During maintenance and package upgrades etc, it is better to roll changes out on non-leaders first and then do the leader at the end
  • The controller process holds all metadata about the cluster, the current states and other information in memory.
    • Some of this information is backed by Zookeeper based storage layer
    • The other information is recreated dynamically based on updates from executors
  • Controllers being down does not affect running containers on executors

Controller Configuration File Reference

The Drove Controller is written on the Dropwizard framework. The configuration to the service is set using a YAML file which needs to be injected into the container. A typical controller configuration file will look like the following:

server: #(1)!
  applicationConnectors: #(2)!
    - type: http
      port: 10000
  adminConnectors: #(3)!
    - type: http
      port: 4001
  applicationContextPath: / #(4)!
  requestLog: #(5)!
    appenders:
      - type: console
        timeZone: ${DROVE_TIMEZONE}
      - type: file
        timeZone: ${DROVE_TIMEZONE}
        currentLogFilename: /logs/drove-controller-access.log
        archivedLogFilenamePattern: /logs/drove-controller-access.log-%d-%i
        archivedFileCount: 3
        maxFileSize: 100MiB


logging: #(6)!
  level: INFO
  loggers:
    com.phonepe.drove: ${DROVE_LOG_LEVEL}

  appenders:
    - type: console #(7)!
      threshold: ALL
      timeZone: ${DROVE_TIMEZONE}
      logFormat: "%(%-5level) [%date] [%logger{0} - %X{appId}] %message%n"
    - type: file #(8)!
      threshold: ALL
      timeZone: ${DROVE_TIMEZONE}
      currentLogFilename: /logs/drove-controller.log
      archivedLogFilenamePattern: /logs/drove-controller.log-%d-%i
      archivedFileCount: 3
      maxFileSize: 100MiB
      logFormat: "%(%-5level) [%date] [%logger{0} - %X{appId}] %message%n"
      archive: true


zookeeper: #(9)!
  connectionString: ${ZK_CONNECTION_STRING}

clusterAuth: #(10)!
  secrets:
  - nodeType: CONTROLLER
    secret: ${DROVE_CONTROLLER_SECRET}
  - nodeType: EXECUTOR
    secret: ${DROVE_EXECUTOR_SECRET}

userAuth: #(11)!
  enabled: true
  users:
    - username: admin
      password: ${DROVE_ADMIN_PASSWORD}
      role: EXTERNAL_READ_WRITE
    - username: guest
      password: ${DROVE_GUEST_PASSWORD}
      role: EXTERNAL_READ_ONLY

instanceAuth: #(12)!
  secret: ${DROVE_INSTANCE_AUTH_SECRET}

options: #(13)!
  maxStaleInstancesCount: 3
  staleCheckInterval: 1m
  staleAppAge: 1d
  staleInstanceAge: 18h
  staleTaskAge: 1d
  clusterOpParallelism: 4
  1. Server listener configuration. See Dropwizard Server Configuration for the different options.
  2. Main port configuration. This is where the UI and APIs will be exposed. Check connector configuration docs for details.
  3. Admin port. You can take thread dumps, metrics, run healthchecks on the Drove controller on this port.
  4. Base path for UI. Keep this as is.
  5. Access logs configuration. See requestLog docs.
  6. Main logging configuration. See logging docs.
  7. Log to console. Useful in docker-compose.
  8. Log to rotating files. Useful for running servers.
  9. Configure how to connect to Zookeeper See Zookeeper Config for details.
  10. Configuration for authentication between nodes in the cluster. Please check intra node auth config for details.
  11. Configure user authentication to access the cluster. Please check User auth config for details.
  12. Signing secret for JWT to be embedded in application and task instances. Check Instance auth config for details.
  13. Special options to configure controller behaviour. See Controller Options for details.

Tip

In case you do not want to expose admin apis to outside the host, please set bindHost in the admin connectors section.

adminConnectors:
  - type: http
    port: 10001
    bindHost: 127.0.0.1

Zookeeper Connection Configuration

The following details can be configured.

Name Option Description
Connection String connectionString The connection string of the form: zkserver:2181,zkserver2:2181...
Data namespace namespace The top level node inside which all Drove data will be scoped. Defaults to drove if not set.

Sample

zookeeper:
  connectionString: "192.168.3.10:2181,192.168.3.11:2181,192.168.3.12:2181"
  namespace: drovetest

Intra Node Authentication Configuration

Communication between controller and executor is protected by a shared-secret based authentication. The following configuration is meant to configure this. This section consists of a list of 2 members:

  • Config for controller to talk to executors
  • Config for executors to talk to controller

Each section consists of the following:

Name Option Description
Node Type nodeType Type of node in the cluster. Can be CONTROLLER or EXECUTOR
Secret secret The actual secret to be passed.

Sample

clusterAuth:
  secrets:
  - nodeType: CONTROLLER
    secret: ControllerSecretValue
  - nodeType: EXECUTOR
    secret: ExecutorSecret

Danger

The values are passed in the header as is. Please manage the config file ownership to ensure that the files are not world readable.

Tip

You can use pwgen -s 32 to generate secure random strings for usage as secrets.

User Authentication Configuration

This section is used to configure user details for human and other systems that need to call Drove APIs or access the Drove UI. This is implemented using basic auth.

The configuration consists of:

Name Option Description
Enabled enabled Enable basic auth for the cluster
Encoding encoding The actual encoding of the password. Can be PLAIN or CRYPT
Caching cachingPolicy Caching policy for the authentication and authorization of the user. Please check CaffeineSpec docs for more details. Set to maximumSize=500, expireAfterAccess=30m by default
List of users users A list of users recognized by the system

Each entry in the user list consists of:

Name Option Description
User Name username The actual login username
Password password The password for the user. Needs to be set to bcrypt string of the actual password if encoding is set to CRYPT in the parent section.
User Role role The role of the user in the cluster. Can be EXTERNAL_READ_WRITE for users who have both read and write permissions or EXTERNAL_READ_ONLY for users with read-only permissions.

Sample

userAuth:
  enabled: true
  encoding: CRYPT
  users:
    - username: admin
      password: "$2y$10$pfGnPkYrJEGzasvVNPjRu.IJldV9TDa0Vh.u1UdimILWDuhvapc2O"
      role: EXTERNAL_READ_WRITE
    - username: guest
      password: "$2y$10$uCJ7WxIvd13C.1oOTs28p.xpJShGiTWuDLY/sGH9JE8nrkSGBFkc6"
      role: EXTERNAL_READ_ONLY
    - username: noread
      password: "$2y$10$8mr/zXL5rMW/s/jlBcgXHu0UvyzfdDDvyc.etfuoR.991sn9UOX/K"

No authentication

To configure a cluster without authentication, remove this section entirely.

Operator role

If role is not set, the user will be able to access the UI, but will not have access to application logs. This comes in handy to provide access to other teams to explore your deployment topology, but not get access to your logs that might contain sensitive information.

Password Hashing

We strongly recommend using bcrypt passwords for authentication. You can use the following command to generate hashed password strings:

htpasswd -nbBC 10 <username> <password>|cut -d ':' -f2

Instance Authentication Configuration

All application and task instances, get access to an unique JWT that is injected into it by Drove as the environment variable DROVE_APP_INSTANCE_AUTH_TOKEN. This token is signed using a secret. This secret can be configured by setting the secret parameter in the instanceAuth section.

Sample

instanceAuth:
  secret: RandomSecret

Controller Options

The following options can be set to influence the behavior of the Drove cluster and the controller.

Name Option Description
Stale Check Interval staleCheckInterval Interval at which Drove checks for stale application and task metadata for cleanup. Defaults to 1 hour. Expressed in duration.
Stale App Age staleAppAge Apps in MONITORING state are cleaned up after some time by Drove. This variable can be used to control the max time for which such apps are maintained in the cluster. Defaults to 7 days. Expressed in duration.
Stale App Instances Count maxStaleInstancesCount Maximum number of application instances metadata for stopped or lost instances to be maintained in the cluster. Defaults to 100.
Stale Instance Age staleInstanceAge Maximum age for a stale application instance to be retained. Defaults to 7 days. Expressed in duration.
Stale Task Age staleTaskAge Maximum time for which metadata for a finished task is retained on the cluster. Defaults to 2 days. Expressed in duration.
Event Storage Duration maxEventsStorageDuration Maximum time for which cluster events are retained on the cluster. Defaults to 1 hour. Expressed in duration.
Default Operation Timeout clusterOpTimeout Timeout for operations that are initiated by drove itself. For example, instance spin up in case of executor failure, instance migrations etc. Defaults to 5 minutes. Expressed in duration.
Operation threads clusterOpParallelism Signified the parallelism for operations internal to the cluster. Defaults to: 1. Range: 1-512.
Audited Methods auditedHttpMethods Drove prints an audit log with user details when an api is called by an user. Defaults to ["POST", "PUT"].
Allowed mount directories allowedMountDirs If provided, Drove will ensure that application and task spec can mount only the directories mentioned in this set on executor host.
Disable read-only auth disableReadAuth When userAuth is enabled, setting this option, will enforce authorization only on write operations.
Stale executor age staleExecutorAge Maximum age of executor heartbeat data before the controller considers it stale and excludes it from active topology calculations. Defaults to 45 seconds.
Stale service age staleServiceAge Maximum time for which metadata for a stale local service is retained on the cluster. Defaults to 7 days. Expressed in duration.
Job retry count jobRetryCount Number of retries for operations internal to the cluster, for example instance start or task start. Defaults to 2. Range: 0-32.
Job retry interval jobRetryInterval Interval between retries for operations internal to the cluster. Defaults to 1 second. Expressed in duration.
Instance state check retry interval instanceStateCheckRetryInterval Interval between retries when the controller checks the state of a new instance. Defaults to 3 seconds. Expressed in duration.
Disable command line arguments disableCmdlArgs When true, passing command line arguments to application and task containers is disabled. Default: false. Used to build the controller default security profile.
Enable raw device access enableRawDeviceAccess When true, application and task containers can access raw devices on the host. Default: false. Used to build the controller default security profile.
Data update parallelism dataUpdateParallelism Parallelism for the executor state update pipeline. Defaults to 16. Range: 1-256.
Compiled rule cache count compiledRuleCacheCount Number of compiled RULE_BASED placement rule programs cached by the controller. Defaults to 100. Range: 0-1000.
Min scaled CPUs minScaledCpus Minimum core count for which the controller applies performance scaling. A request below this threshold is used as is. Defaults to 2. Range: 0-4096.
Min scaled memory minScaledMemoryMB Minimum memory in MB for which the controller applies performance scaling. A request below this threshold is used as is. Defaults to 128.

Sample

options:
  staleCheckInterval: 5m
  staleAppAge: 2d
  maxStaleInstancesCount: 20
  staleInstanceAge: 1d
  staleTaskAge: 2d
  maxEventsStorageDuration: 30m
  clusterOpParallelism: 32
  allowedMountDirs:
   - /mnt/scratch

Stale data cleanup

In order to keep internal memory footprint low, reduce the amount of data stored on Zookeeper, and provide a faster experience on the UI,Drove keeps cleaning up data for stale applications, application instances, task instances and cluster events.

The retention for such metadata can be controlled using the following config options:

  • staleAppAge
  • maxStaleInstancesCount
  • staleInstanceAge
  • staleTaskAge
  • maxEventsStorageDuration

Warning

Configuration changes done to these parameters will have direct impact on memory usage by the controller and memory and disk utilization on the Zookeeper cluster.

Internal Operations

Drove may need to create and issue operations on applications and tasks to manage cluster stability, for maintenance and other reasons. The following parameters can be used to control the speed and parallelism of such operations:

  • clusterOpTimeout
  • clusterOpParallelism

Tip

The default value of 1 for the clusterOpParallelism parameter is generally too low for most clusters. Unless there is a specific problem, it would be advisable to set this to at least 4. If number of instances is quite high for applications (order of tens or hundreds), feel free to set this to 32.

Increasing clusterOpParallelism will make recovery faster in case of executor failures, but it will increase cpu utilization on the controller by a little bit.

The auditedHttpMethods parameter contains a list of all HTTP methods that need to be audited. This means that if the auditedHttpMethods contains POST and PUT, any drove HTTP POST or PUT apis being called will lead to a audit in the controller logs with the details of the user that made the call.

Warning

It would be advisable to not add GET to the list. This is because the UI keeps making calls to GET apis on drove to fetch data to render. These calls are automated and happen every few seconds from the browser. This will blow up controller logs size.

The allowedMountDirs option whitelists only some directories to be mounted on containers. If this is not provided, containers will be able to mount any directory on the executors.

Danger

It is highly recommended to set allowedMountDirs to a designated directory that containers might want to use as scratch space if needed. Keeping this empty will almost definitely cause security issues in the long run.

Security policies

How security profiles work is explained in Security Profiles.

The securityPolicies top-level key defines container security profiles per group of deployables. Each entry has a match rule list and a security profile. For every new instance, the controller finds the first policy that matches and applies its profile. If no policy matches, the controller default profile built from the allowedMountDirs, disableCmdlArgs and enableRawDeviceAccess options is used.

Each match rule is a string of the form (jobtype|*)/(*|name regex):

Part Description
jobtype One of SERVICE, COMPUTATION or LOCAL_SERVICE. Use * to match all job types.
* or name regex * matches all names. Any other value is a Java regex matched against the deployable name.

The name used for matching depends on the deployable type:

Deployable Match name
Application The application name from the spec.
Task sourceAppName/taskId from the task spec.
Local service The local service name from the spec.
Name Option Description
Policy name policyName Name for the profile. Required. Used in logs to identify the policy that was applied.
Allowed mount directories allowedMountDirs Directories this policy allows to be mounted on the container. Overrides the controller default.
Disable command line arguments disableCmdlArgs When true, command line arguments are disabled for matched containers. Default: false.
Privileged mode enablePrivilegedMode When true, containers matched by this policy run in privileged mode. Default: false.
Raw device access enableRawDeviceAccess When true, containers matched by this policy can access raw devices on the host. Default: false.
Capabilities capabilities.addedCapabilities Linux capabilities added to the container in addition to the default set. For example SYS_ADMIN.
Capabilities capabilities.droppedCapabilities Linux capabilities dropped from the container.
Max open files maxOpenFiles Maximum number of open file descriptors for the container. Default: 470,000.
Seccomp profile seccompProfile Path to a custom seccomp profile for the container. Empty means the default.

Note

When no securityPolicies entry matches a deployable, the controller default profile is used. The default profile is built from the allowedMountDirs, disableCmdlArgs and enableRawDeviceAccess controller options.

Sample

securityPolicies:
  - match:
      - "COMPUTATION/.*"
    profile:
      policyName: "tasks-locked-down"
      disableCmdlArgs: true
      maxOpenFiles: 100000
  - match:
      - "SERVICE/ml-training"
    profile:
      policyName: "ml-privileged"
      enablePrivilegedMode: true
      capabilities:
        addedCapabilities:
          - "SYS_ADMIN"
  - match:
      - "*/*"
    profile:
      policyName: "default"
      allowedMountDirs:
        - /mnt/scratch

Note

Policies are evaluated in the order they are listed. The first matching policy wins. Place specific rules before generic ones.

Relevant directories

  • /etc/drove/controller/ - Configuration files
  • /var/log/drove/controller/ - Logs

We shall be volume mounting the config and log directories with the same name.

Prerequisite Setup

If not done already, please complete the prerequisite setup on all machines earmarked for the cluster.

Setup the config file

Create a relevant configuration file in /etc/drove/controller/controller.yml.

Sample

server:
  applicationConnectors:
    - type: http
      port: 10000
  adminConnectors:
    - type: http
      port: 10001
  requestLog:
    appenders:
      - type: file
        timeZone: IST
        currentLogFilename: /var/log/drove/controller/drove-controller-access.log
        archivedLogFilenamePattern: /var/log/drove/controller/drove-controller-access.log-%d-%i
        archivedFileCount: 3
        maxFileSize: 100MiB

logging:
  level: INFO
  loggers:
    com.phonepe.drove: INFO


  appenders:
    - type: file
      threshold: ALL
      timeZone: IST
      currentLogFilename: /var/log/drove/controller/drove-controller.log
      archivedLogFilenamePattern: /var/log/drove/controller/drove-controller.log-%d-%i
      archivedFileCount: 3
      maxFileSize: 100MiB
      logFormat: "%(%-5level) [%date] [%logger{0} - %X{appId}] %message%n"

zookeeper:
  connectionString: "192.168.56.10:2181"

clusterAuth:
  secrets:
  - nodeType: CONTROLLER
    secret: "0v8XvJrDc7r86ZY1QCByPTDPninI4Xii"
  - nodeType: EXECUTOR
    secret: "pOd9sIEXhv0wrGOVc7ebwNvR7twZqyTN"

userAuth:
  enabled: true
  encoding: CRYPT
  users:
    - username: admin
      password: "$2y$10$pfGnPkYrJEGzasvVNPjRu.IJldV9TDa0Vh.u1UdimILWDuhvapc2O"
      role: EXTERNAL_READ_WRITE
    - username: guest
      password: "$2y$10$uCJ7WxIvd13C.1oOTs28p.xpJShGiTWuDLY/sGH9JE8nrkSGBFkc6"
      role: EXTERNAL_READ_ONLY


instanceAuth:
  secret: "bd2SIgz9OMPG2L8wA6zxj21oLVLbuLFC"

options:
  maxStaleInstancesCount: 3
  staleCheckInterval: 1m
  staleAppAge: 2d
  staleInstanceAge: 1d
  staleTaskAge: 1d
  clusterOpParallelism: 4
  allowedMountDirs:
   - /dev/null

Setup required environment variables

Environment variables need to run the drove controller are setup in /etc/drove/controller/controller.env.

CONFIG_FILE_PATH=/etc/drove/controller/controller.yml
JAVA_PROCESS_MIN_HEAP=2g
JAVA_PROCESS_MAX_HEAP=2g
ZK_CONNECTION_STRING="192.168.3.10:2181"
CONTROLLER_SECRET="0v8XvJrDc7r86ZY1QCByPTDPninI4Xii"
EXECUTOR_SECRET="pOd9sIEXhv0wrGOVc7ebwNvR7twZqyTN"
ADMIN_PASSWORD="<admin user password>"
GUEST_PASSWORD="<guest user password>"
INSTANCE_AUTH_SECRET="bd2SIgz9OMPG2L8wA6zxj21oLVLbuLFC"
JAVA_OPTS="-Xlog:gc:/var/log/drove/controller/gc.log -Xlog:gc:::filecount=3,filesize=10M -Xlog:gc::time,level,tags -XX:+UseNUMA -XX:+ExitOnOutOfMemoryError -Djava.security.egd=file:/dev/urandom -Dfile.encoding=utf-8 -Djute.maxbuffer=0x9fffff"

Container environment variables

The controller container entrypoint reads the following environment variables:

Variable Description Default
CONFIG_FILE_PATH Path to the config file inside the container config.yml
ZK_CONNECTION_STRING Zookeeper connection string. Mandatory. The container exits if this is not set. –
CONTROLLER_SECRET Secret for intra-node authentication, controller side ControllerSecret
EXECUTOR_SECRET Secret for intra-node authentication, executor side ExecutorSecret
ADMIN_PASSWORD Password for the admin user admin
GUEST_PASSWORD Password for the guest user guest
INSTANCE_AUTH_SECRET Signing secret for instance auth JWTs InstanceAuthSecret
LOG_LEVEL Log level for com.phonepe.drove INFO
TZ Timezone for logs UTC
DEBUG Print environment and diagnostics at startup 0
GC_ALGO JVM garbage collection algorithm UseG1GC
JAVA_PROCESS_MIN_HEAP JVM minimum heap size 1g
JAVA_PROCESS_MAX_HEAP JVM maximum heap size 1g
JAVA_OPTS Extra JVM options –

Default secrets are rejected

The controller container refuses to start when any security variable (ADMIN_PASSWORD, GUEST_PASSWORD, CONTROLLER_SECRET, EXECUTOR_SECRET, INSTANCE_AUTH_SECRET) is left at its default value. Set all security variables to strong values. You can use pwgen -s 32 to generate them.

For dev and test clusters only, set ALLOW_DEFAULT_SECRETS=true to explicitly acknowledge the risk and start with default values.

Note

The entrypoint validates the security variables even when your config file contains the secrets directly. If your controller.yml has literal secrets, you still need to set the security variables to non-default values, or set ALLOW_DEFAULT_SECRETS=true.

Note

When DEBUG=1, the startup log masks the values of all security variables before printing the environment.

Image hardening and supply chain

The controller and executor images are built from a minimal ubuntu:noble base with a headless JRE. The build installs only the required packages, removes documentation, locale data and setuid binaries, and uses COPY instead of ADD for image contents.

Every release image pushed to the registry by the release workflow ships with two attestations:

  • SBOM - the complete list of packages inside the image.
  • Provenance - a SLSA attestation that records how the image was built, from which source revision, and by which workflow.

Note

Attestations are attached to images pushed by the release workflow. Images you build locally do not carry them.

You can inspect the attestations for a released image with docker buildx:

docker buildx imagetools inspect ghcr.io/phonepe/drove-controller:latest

The output shows the image manifest along with the sbom and provenance attestation manifests.

If you use a tool like cosign, you can verify the attestations against the image digest:

cosign verify-attestation \
    --type slsaprovenance \
    ghcr.io/phonepe/drove-controller@<digest>
    ghcr.io/phonepe/drove-controller@<digest>

Tip

Tip

If your organization pins images by digest, record the digest at deploy time. The attestation is bound to the digest, so the digest is all you need to audit what runs in your cluster.

Create systemd file

[Unit]
Description=Drove Controller Service
After=docker.service
Requires=docker.service

[Service]
User=drove
Group=docker
TimeoutStartSec=0
Restart=always
ExecStartPre=-/usr/bin/docker pull ghcr.io/phonepe/drove-controller:latest
ExecStart=/usr/bin/docker run  \
    --env-file /etc/drove/controller/controller.env \
    --volume /etc/drove/controller:/etc/drove/controller:ro \
    --volume /var/log/drove/controller:/var/log/drove/controller \
    --publish 10000:10000  \
    --publish 10001:10001 \
    --hostname %H \
    --rm \
    --name drove.controller \
    ghcr.io/phonepe/drove-controller:latest

[Install]
WantedBy=multi-user.target

Verify the file with the following command:

systemd-analyze verify drove.controller.service

Set permissions

chmod 664 /etc/systemd/system/drove.controller.service

Start the service on all servers

Use the following to start the service:

systemctl daemon-reload
systemctl enable drove.controller
systemctl start drove.controller

You can tail the logs at /var/log/drove/controller/drove-controller.log.

The console would be available at http://<ip>:10000 and admin functionality will be available on http://<ip>:10001 according to the above config.

Health checks can be performed by running a curl as follows:

curl http://localhost:10001/healthcheck

Note

  • The healthcheck check api is available on admin port.
  • HTTP status is 200/OK if things are fine.

Once controllers are up, one of them will become the leader. You can check the leader by running the following command:

curl http://<ip>:10000/apis/v1/ping

Only on the leader you should get the following response along with a HTTP status 200/OK:

{
    "status":"SUCCESS",
    "data":"pong",
    "message":"success"
}