1 { config, lib, pkgs, ... }:
20 cfg = config.services.cassandra;
22 atLeast3 = versionAtLeast cfg.package.version "3";
23 atLeast3_11 = versionAtLeast cfg.package.version "3.11";
24 atLeast4 = versionAtLeast cfg.package.version "4";
26 defaultUser = "cassandra";
28 cassandraConfig = flip recursiveUpdate cfg.extraConfig (
30 commitlog_sync = "batch";
31 commitlog_sync_batch_window_in_ms = 2;
32 start_native_transport = cfg.allowClients;
33 cluster_name = cfg.clusterName;
34 partitioner = "org.apache.cassandra.dht.Murmur3Partitioner";
35 endpoint_snitch = "SimpleSnitch";
36 data_file_directories = [ "${cfg.homeDir}/data" ];
37 commitlog_directory = "${cfg.homeDir}/commitlog";
38 saved_caches_directory = "${cfg.homeDir}/saved_caches";
39 } // optionalAttrs (cfg.seedAddresses != [ ]) {
42 class_name = "org.apache.cassandra.locator.SimpleSeedProvider";
43 parameters = [{ seeds = concatStringsSep "," cfg.seedAddresses; }];
46 } // optionalAttrs atLeast3 {
47 hints_directory = "${cfg.homeDir}/hints";
51 cassandraConfigWithAddresses = cassandraConfig // (
52 if cfg.listenAddress == null
53 then { listen_interface = cfg.listenInterface; }
54 else { listen_address = cfg.listenAddress; }
56 if cfg.rpcAddress == null
57 then { rpc_interface = cfg.rpcInterface; }
58 else { rpc_address = cfg.rpcAddress; }
61 cassandraEtc = pkgs.stdenv.mkDerivation {
62 name = "cassandra-etc";
64 cassandraYaml = builtins.toJSON cassandraConfigWithAddresses;
65 cassandraEnvPkg = "${cfg.package}/conf/cassandra-env.sh";
66 cassandraLogbackConfig = pkgs.writeText "logback.xml" cfg.logbackConfig;
68 passAsFile = [ "extraEnvSh" ];
69 inherit (cfg) extraEnvSh package;
74 echo "$cassandraYaml" > "$out/cassandra.yaml"
75 ln -s "$cassandraLogbackConfig" "$out/logback.xml"
77 ( cat "$cassandraEnvPkg"
78 echo "# lines from services.cassandra.extraEnvSh: "
80 ) > "$out/cassandra-env.sh"
82 # Delete default JMX Port, otherwise we can't set it using env variable
83 sed -i '/JMX_PORT="7199"/d' "$out/cassandra-env.sh"
85 # Delete default password file
86 sed -i '/-Dcom.sun.management.jmxremote.password.file=\/etc\/cassandra\/jmxremote.password/d' "$out/cassandra-env.sh"
88 ${lib.optionalString atLeast4 ''
89 cp $package/conf/jvm*.options $out/
96 (left: right: left + right) ""
97 (map (role: "${role.username} ${role.password}") cfg.jmxRoles);
101 ++ optionals (cfg.jmxRoles != [ ]) [
102 "-Dcom.sun.management.jmxremote.authenticate=true"
103 "-Dcom.sun.management.jmxremote.password.file=${cfg.jmxRolesFile}"
104 ] ++ optionals cfg.remoteJmx [
105 "-Djava.rmi.server.hostname=${cfg.rpcAddress}"
106 ] ++ optionals atLeast4 [
107 # Historically, we don't use a log dir, whereas the upstream scripts do
108 # expect this. We override those by providing our own -Xlog:gc flag.
109 "-Xlog:gc=warning,heap*=warning,age*=warning,safepoint=warning,promotion*=warning"
113 # Sufficient for cassandra 2.x, 3.x
114 CASSANDRA_CONF = "${cassandraEtc}";
116 # Required since cassandra 4
117 CASSANDRA_LOGBACK_CONF = "${cassandraEtc}/logback.xml";
122 options.services.cassandra = {
124 enable = mkEnableOption ''
125 Apache Cassandra – Scalable and highly available database
128 clusterName = mkOption {
130 default = "Test Cluster";
132 The name of the cluster.
133 This setting prevents nodes in one logical cluster from joining
134 another. All nodes in a cluster must have the same value.
140 default = defaultUser;
141 description = "Run Apache Cassandra under this user.";
146 default = defaultUser;
147 description = "Run Apache Cassandra under this group.";
152 default = "/var/lib/cassandra";
154 Home directory for Apache Cassandra.
158 package = mkPackageOption pkgs "cassandra" {
159 example = "cassandra_3_11";
163 type = types.listOf types.str;
166 Populate the `JVM_OPT` environment variable.
170 listenAddress = mkOption {
171 type = types.nullOr types.str;
172 default = "127.0.0.1";
175 Address or interface to bind to and tell other Cassandra nodes
176 to connect to. You _must_ change this if you want multiple
177 nodes to be able to communicate!
179 Set {option}`listenAddress` OR {option}`listenInterface`, not both.
181 Leaving it blank leaves it up to
182 `InetAddress.getLocalHost()`. This will always do the "Right
183 Thing" _if_ the node is properly configured (hostname, name
184 resolution, etc), and the Right Thing is to use the address
185 associated with the hostname (it might not be).
187 Setting {option}`listenAddress` to `0.0.0.0` is always wrong.
191 listenInterface = mkOption {
192 type = types.nullOr types.str;
196 Set `listenAddress` OR `listenInterface`, not both. Interfaces
197 must correspond to a single address, IP aliasing is not
202 rpcAddress = mkOption {
203 type = types.nullOr types.str;
204 default = "127.0.0.1";
207 The address or interface to bind the native transport server to.
209 Set {option}`rpcAddress` OR {option}`rpcInterface`, not both.
211 Leaving {option}`rpcAddress` blank has the same effect as on
212 {option}`listenAddress` (i.e. it will be based on the configured hostname
215 Note that unlike {option}`listenAddress`, you can specify `"0.0.0.0"`, but you
216 must also set `extraConfig.broadcast_rpc_address` to a value other
219 For security reasons, you should not expose this port to the
220 internet. Firewall it if needed.
224 rpcInterface = mkOption {
225 type = types.nullOr types.str;
229 Set {option}`rpcAddress` OR {option}`rpcInterface`, not both. Interfaces must
230 correspond to a single address, IP aliasing is not supported.
234 logbackConfig = mkOption {
237 <configuration scan="false">
238 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
240 <pattern>%-5level %date{HH:mm:ss,SSS} %msg%n</pattern>
245 <appender-ref ref="STDOUT" />
248 <logger name="com.thinkaurelius.thrift" level="ERROR"/>
252 XML logback configuration for cassandra
256 seedAddresses = mkOption {
257 type = types.listOf types.str;
258 default = [ "127.0.0.1" ];
260 The addresses of hosts designated as contact points in the cluster. A
261 joining node contacts one of the nodes in the seeds list to learn the
262 topology of the ring.
263 Set to `[ "127.0.0.1" ]` for a single node cluster.
267 allowClients = mkOption {
271 Enables or disables the native transport server (CQL binary protocol).
272 This server uses the same address as the {option}`rpcAddress`,
273 but the port it uses is not `rpc_port` but
274 `native_transport_port`. See the official Cassandra
275 docs for more information on these variables and set them using
276 {option}`extraConfig`.
280 extraConfig = mkOption {
285 commitlog_sync_batch_window_in_ms = 3;
288 Extra options to be merged into {file}`cassandra.yaml` as nix attribute set.
292 extraEnvSh = mkOption {
295 example = literalExpression ''"CLASSPATH=$CLASSPATH:''${extraJar}"'';
297 Extra shell lines to be appended onto {file}`cassandra-env.sh`.
301 fullRepairInterval = mkOption {
302 type = types.nullOr types.str;
306 Set the interval how often full repairs are run, i.e.
307 {command}`nodetool repair --full` is executed. See
308 <https://cassandra.apache.org/doc/latest/operating/repair.html>
309 for more information.
311 Set to `null` to disable full repairs.
315 fullRepairOptions = mkOption {
316 type = types.listOf types.str;
318 example = [ "--partitioner-range" ];
320 Options passed through to the full repair command.
324 incrementalRepairInterval = mkOption {
325 type = types.nullOr types.str;
329 Set the interval how often incremental repairs are run, i.e.
330 {command}`nodetool repair` is executed. See
331 <https://cassandra.apache.org/doc/latest/operating/repair.html>
332 for more information.
334 Set to `null` to disable incremental repairs.
338 incrementalRepairOptions = mkOption {
339 type = types.listOf types.str;
341 example = [ "--partitioner-range" ];
343 Options passed through to the incremental repair command.
347 maxHeapSize = mkOption {
348 type = types.nullOr types.str;
352 Must be left blank or set together with {option}`heapNewSize`.
353 If left blank a sensible value for the available amount of RAM and CPU
356 Override to set the amount of memory to allocate to the JVM at
357 start-up. For production use you may wish to adjust this for your
358 environment. `MAX_HEAP_SIZE` is the total amount of memory dedicated
359 to the Java heap. `HEAP_NEWSIZE` refers to the size of the young
362 The main trade-off for the young generation is that the larger it
363 is, the longer GC pause times will be. The shorter it is, the more
364 expensive GC will be (usually).
368 heapNewSize = mkOption {
369 type = types.nullOr types.str;
373 Must be left blank or set together with {option}`heapNewSize`.
374 If left blank a sensible value for the available amount of RAM and CPU
377 Override to set the amount of memory to allocate to the JVM at
378 start-up. For production use you may wish to adjust this for your
379 environment. `HEAP_NEWSIZE` refers to the size of the young
382 The main trade-off for the young generation is that the larger it
383 is, the longer GC pause times will be. The shorter it is, the more
384 expensive GC will be (usually).
386 The example `HEAP_NEWSIZE` assumes a modern 8-core+ machine for decent pause
387 times. If in doubt, and if you do not particularly want to tweak, go with
388 100 MB per physical CPU core.
392 mallocArenaMax = mkOption {
393 type = types.nullOr types.int;
397 Set this to control the amount of arenas per-thread in glibc.
401 remoteJmx = mkOption {
405 Cassandra ships with JMX accessible *only* from localhost.
406 To enable remote JMX connections set to true.
408 Be sure to also enable authentication and/or TLS.
409 See: <https://wiki.apache.org/cassandra/JmxSecurity>
417 Specifies the default port over which Cassandra will be available for
419 For security reasons, you should not expose this port to the internet.
420 Firewall it if needed.
424 jmxRoles = mkOption {
427 Roles that are allowed to access the JMX (e.g. {command}`nodetool`)
428 BEWARE: The passwords will be stored world readable in the nix store.
429 It's recommended to use your own protected file using
430 {option}`jmxRolesFile`
432 Doesn't work in versions older than 3.11 because they don't like that
435 type = types.listOf (types.submodule {
437 username = mkOption {
439 description = "Username for JMX";
441 password = mkOption {
443 description = "Password for JMX";
449 jmxRolesFile = mkOption {
450 type = types.nullOr types.path;
453 then pkgs.writeText "jmx-roles-file" defaultJmxRolesFile
455 defaultText = literalMD ''generated configuration file if version is at least 3.11, otherwise `null`'';
456 example = "/var/lib/cassandra/jmx.password";
458 Specify your own jmx roles file.
460 Make sure the permissions forbid "others" from reading the file if
461 you're using Cassandra below version 3.11.
466 config = mkIf cfg.enable {
469 assertion = (cfg.listenAddress == null) != (cfg.listenInterface == null);
470 message = "You have to set either listenAddress or listenInterface";
473 assertion = (cfg.rpcAddress == null) != (cfg.rpcInterface == null);
474 message = "You have to set either rpcAddress or rpcInterface";
477 assertion = (cfg.maxHeapSize == null) == (cfg.heapNewSize == null);
478 message = "If you set either of maxHeapSize or heapNewSize you have to set both";
481 assertion = cfg.remoteJmx -> cfg.jmxRolesFile != null;
483 If you want JMX available remotely you need to set a password using
484 <literal>jmxRoles</literal> or <literal>jmxRolesFile</literal> if
485 using Cassandra older than v3.11.
489 users = mkIf (cfg.user == defaultUser) {
490 users.${defaultUser} = {
494 uid = config.ids.uids.cassandra;
495 description = "Cassandra service user";
497 groups.${defaultUser}.gid = config.ids.gids.cassandra;
500 systemd.services.cassandra = {
501 description = "Apache Cassandra service";
502 after = [ "network.target" ];
503 environment = commonEnv // {
504 JVM_OPTS = builtins.concatStringsSep " " fullJvmOptions;
505 MAX_HEAP_SIZE = toString cfg.maxHeapSize;
506 HEAP_NEWSIZE = toString cfg.heapNewSize;
507 MALLOC_ARENA_MAX = toString cfg.mallocArenaMax;
508 LOCAL_JMX = if cfg.remoteJmx then "no" else "yes";
509 JMX_PORT = toString cfg.jmxPort;
511 wantedBy = [ "multi-user.target" ];
515 ExecStart = "${cfg.package}/bin/cassandra -f";
516 SuccessExitStatus = 143;
520 systemd.services.cassandra-full-repair = {
521 description = "Perform a full repair on this Cassandra node";
522 after = [ "cassandra.service" ];
523 requires = [ "cassandra.service" ];
524 environment = commonEnv;
531 "${cfg.package}/bin/nodetool"
534 ] ++ cfg.fullRepairOptions);
538 systemd.timers.cassandra-full-repair =
539 mkIf (cfg.fullRepairInterval != null) {
540 description = "Schedule full repairs on Cassandra";
541 wantedBy = [ "timers.target" ];
543 OnBootSec = cfg.fullRepairInterval;
544 OnUnitActiveSec = cfg.fullRepairInterval;
549 systemd.services.cassandra-incremental-repair = {
550 description = "Perform an incremental repair on this cassandra node.";
551 after = [ "cassandra.service" ];
552 requires = [ "cassandra.service" ];
553 environment = commonEnv;
560 "${cfg.package}/bin/nodetool"
562 ] ++ cfg.incrementalRepairOptions);
566 systemd.timers.cassandra-incremental-repair =
567 mkIf (cfg.incrementalRepairInterval != null) {
568 description = "Schedule incremental repairs on Cassandra";
569 wantedBy = [ "timers.target" ];
571 OnBootSec = cfg.incrementalRepairInterval;
572 OnUnitActiveSec = cfg.incrementalRepairInterval;
578 meta.maintainers = with lib.maintainers; [ roberth ];