├── README.adoc ├── demo-app.yaml ├── demo-app_lb.yaml ├── neo4j.sh ├── neo4j.yaml ├── neo4j_core.yaml ├── neo4j_env.yaml ├── neo4j_lb.yaml ├── neo4j_rr.yaml ├── neo4j_svc.yaml └── src └── main └── java └── org └── neo4j └── kubernetes └── SampleApp.java /README.adoc: -------------------------------------------------------------------------------- 1 | = Neo4j on Kubernetes 2 | 3 | This is a first attempt at getting Neo4j 3.1 to run on Kubernetes. 4 | 5 | You can either setup Kubernetes locally using link:https://github.com/kubernetes/minikube[minikube] or on one of the cloud providers e.g. link:http://kubernetes.io/docs/getting-started-guides/gce/[Google Compute Engine] 6 | 7 | Once you've got that setup you can run the following command to see where Kubernetes is running. 8 | e.g. on my machine 9 | 10 | ``` 11 | $ kubectl cluster-info 12 | Kubernetes master is running at https://192.168.99.100:8443 13 | KubeDNS is running at https://192.168.99.100:8443/api/v1/proxy/namespaces/kube-system/services/kube-dns 14 | kubernetes-dashboard is running at https://192.168.99.100:8443/api/v1/proxy/namespaces/kube-system/services/kubernetes-dashboard 15 | 16 | To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'. 17 | ``` 18 | 19 | Ok. 20 | Now we're ready to install Neo4j. 21 | 22 | == Core Servers 23 | 24 | First let's install the Core servers. 25 | We'd typically have a small number of Core servers in a cluster and they can handle both read and write traffic. 26 | 27 | Run the following commands to create the Core servers: 28 | 29 | ``` 30 | $ ./neo4j.sh 31 | ... 32 | persistentvolume "pv0" created 33 | persistentvolumeclaim "datadir-neo4j-core-0" created 34 | persistentvolume "pv1" created 35 | persistentvolumeclaim "datadir-neo4j-core-1" created 36 | persistentvolume "pv2" created 37 | persistentvolumeclaim "datadir-neo4j-core-2" created 38 | ``` 39 | 40 | ``` 41 | $ kubectl create -f neo4j_svc.yaml 42 | service "neo4j" created 43 | ``` 44 | 45 | ``` 46 | $ kubectl create -f neo4j_core.yaml 47 | statefulset "neo4j-core" created 48 | ``` 49 | 50 | We can check that those servers have spun up by running: 51 | 52 | ``` 53 | $ kubectl get pods 54 | NAME READY STATUS RESTARTS AGE 55 | neo4j-core-0 1/1 Running 0 23s 56 | neo4j-core-1 1/1 Running 0 19s 57 | neo4j-core-2 1/1 Running 0 16s 58 | ``` 59 | 60 | And we can check that Neo4j is up and running by running: 61 | 62 | ``` 63 | $ kubectl logs neo4j-core-2 64 | Starting Neo4j. 65 | 2016-12-07 11:23:23.133+0000 INFO Starting... 66 | 2016-12-07 11:23:24.551+0000 INFO Bolt enabled on 0.0.0.0:7687. 67 | 2016-12-07 11:23:24.566+0000 INFO Initiating metrics... 68 | 2016-12-07 11:23:24.820+0000 INFO Waiting for other members to join cluster before continuing... 69 | 2016-12-07 11:23:45.546+0000 INFO Started. 70 | 2016-12-07 11:23:45.700+0000 INFO Mounted REST API at: /db/manage 71 | 2016-12-07 11:23:46.683+0000 INFO Remote interface available at http://neo4j-core-2.neo4j.default.svc.cluster.local:7474/ 72 | ``` 73 | 74 | Neo4j also exposes the cluster topology via a procedure: 75 | 76 | ``` 77 | $ kubectl exec neo4j-core-0 -- bin/cypher-shell "CALL dbms.cluster.overview()" 78 | id, addresses, role 79 | "484178c4-e7ae-46c9-a7d5-aaf6250efc7f", ["bolt://neo4j-core-0.neo4j.default.svc.cluster.local:7687", "http://neo4j-core-0.neo4j.default.svc.cluster.local:7474"], "FOLLOWER" 80 | "0acdb8dd-3bb1-4c76-bbe7-213530af1d23", ["bolt://neo4j-core-1.neo4j.default.svc.cluster.local:7687", "http://neo4j-core-1.neo4j.default.svc.cluster.local:7474"], "LEADER" 81 | "ca9b954c-c245-418f-9803-7790d36efdd9", ["bolt://neo4j-core-2.neo4j.default.svc.cluster.local:7687", "http://neo4j-core-2.neo4j.default.svc.cluster.local:7474"], "FOLLOWER" 82 | 83 | Bye! 84 | ``` 85 | 86 | == Read Replicas 87 | 88 | Now that we've got the Core Servers up and running let's add some Read Replicas. 89 | Read Replicas are used to scale out reads and they don't accept any write operations. 90 | 91 | Run the following command to create a single Read Replica: 92 | 93 | ``` 94 | $ kubectl create -f neo4j_rr.yaml 95 | replicationcontroller "neo4j-replica" created 96 | ``` 97 | 98 | If we query for the list of pods now we'll see that it's been added to the list: 99 | 100 | ``` 101 | $ kubectl get pods 102 | NAME READY STATUS RESTARTS AGE 103 | neo4j-core-0 1/1 Running 0 4m 104 | neo4j-core-1 1/1 Running 0 4m 105 | neo4j-core-2 1/1 Running 0 4m 106 | neo4j-replica-rdwsh 1/1 Running 0 9s 107 | ``` 108 | 109 | Let's check if the replica has joined the cluster and is ready to go: 110 | 111 | ``` 112 | $ kubectl logs neo4j-replica-rdwsh 113 | Starting Neo4j. 114 | 2016-12-07 11:27:31.388+0000 INFO Starting... 115 | 2016-12-07 11:27:33.072+0000 INFO Bolt enabled on 0.0.0.0:7687. 116 | 2016-12-07 11:27:33.094+0000 INFO Initiating metrics... 117 | 2016-12-07 11:27:39.693+0000 INFO Started. 118 | 2016-12-07 11:27:39.878+0000 INFO Mounted REST API at: /db/manage 119 | 2016-12-07 11:27:41.172+0000 INFO Remote interface available at http://neo4j-replica-rdwsh:7474/ 120 | ``` 121 | 122 | Yep, looks good! 123 | 124 | Let's check that Neo4j knows about our new server: 125 | 126 | ``` 127 | $ kubectl exec neo4j-core-0 -- bin/cypher-shell "CALL dbms.cluster.overview()" 128 | id, addresses, role 129 | "484178c4-e7ae-46c9-a7d5-aaf6250efc7f", ["bolt://neo4j-core-0.neo4j.default.svc.cluster.local:7687", "http://neo4j-core-0.neo4j.default.svc.cluster.local:7474"], "FOLLOWER" 130 | "0acdb8dd-3bb1-4c76-bbe7-213530af1d23", ["bolt://neo4j-core-1.neo4j.default.svc.cluster.local:7687", "http://neo4j-core-1.neo4j.default.svc.cluster.local:7474"], "LEADER" 131 | "ca9b954c-c245-418f-9803-7790d36efdd9", ["bolt://neo4j-core-2.neo4j.default.svc.cluster.local:7687", "http://neo4j-core-2.neo4j.default.svc.cluster.local:7474"], "FOLLOWER" 132 | "00000000-0000-0000-0000-000000000000", ["bolt://neo4j-replica-rdwsh:7687", "http://neo4j-replica-rdwsh:7474"], "READ_REPLICA" 133 | 134 | Bye! 135 | ``` 136 | 137 | It does indeed. 138 | 139 | Now let's scale up to 3 read replicas: 140 | 141 | ``` 142 | $ kubectl scale rc neo4j-replica --replicas=3 143 | replicationcontroller "neo4j-replica" scaled 144 | ``` 145 | 146 | And give it a few seconds and Neo4j will know about those servers as well: 147 | 148 | ``` 149 | $ kubectl exec neo4j-core-0 -- bin/cypher-shell "CALL dbms.cluster.overview()" 150 | id, addresses, role 151 | "484178c4-e7ae-46c9-a7d5-aaf6250efc7f", ["bolt://neo4j-core-0.neo4j.default.svc.cluster.local:7687", "http://neo4j-core-0.neo4j.default.svc.cluster.local:7474"], "FOLLOWER" 152 | "0acdb8dd-3bb1-4c76-bbe7-213530af1d23", ["bolt://neo4j-core-1.neo4j.default.svc.cluster.local:7687", "http://neo4j-core-1.neo4j.default.svc.cluster.local:7474"], "LEADER" 153 | "ca9b954c-c245-418f-9803-7790d36efdd9", ["bolt://neo4j-core-2.neo4j.default.svc.cluster.local:7687", "http://neo4j-core-2.neo4j.default.svc.cluster.local:7474"], "FOLLOWER" 154 | "00000000-0000-0000-0000-000000000000", ["bolt://neo4j-replica-48r7z:7687", "http://neo4j-replica-48r7z:7474"], "READ_REPLICA" 155 | "00000000-0000-0000-0000-000000000000", ["bolt://neo4j-replica-j28g0:7687", "http://neo4j-replica-j28g0:7474"], "READ_REPLICA" 156 | "00000000-0000-0000-0000-000000000000", ["bolt://neo4j-replica-rdwsh:7687", "http://neo4j-replica-rdwsh:7474"], "READ_REPLICA" 157 | 158 | Bye! 159 | ``` 160 | 161 | == Bit of explanation 162 | 163 | For the core servers we're using a Kubernetes beta feature introduced in 1.5.0 called link:https://kubernetes.io/docs/concepts/abstractions/controllers/statefulsets/[StatefulSets]. 164 | This is helpful because Neo4j Core Servers need to discover each other so that they can take participate in a consensus commit algorithm. 165 | We therefore need to know the hostnames of these servers so that we can specify it in the config file. 166 | 167 | 168 | For the read replicas we don't have this constraint so we can use the standard link:http://kubernetes.io/docs/user-guide/replication-controller/[replication controller] approach. 169 | Read Replicas list the hostnames of Core Servers in their config file. 170 | -------------------------------------------------------------------------------- /demo-app.yaml: -------------------------------------------------------------------------------- 1 | # Replicas 2 | apiVersion: v1 3 | kind: ReplicationController 4 | metadata: 5 | name: neo4j-demo-app 6 | spec: 7 | replicas: 1 8 | selector: 9 | app: neo4j-demo-app 10 | template: 11 | metadata: 12 | labels: 13 | app: neo4j-demo-app 14 | spec: 15 | containers: 16 | - name: markhneedham 17 | image: "markhneedham/neo4j-demo-app" 18 | env: 19 | - name: NEO4J_CONNECTION_STRING 20 | value: "bolt+routing://neo4j-core-0.neo4j.default.svc.cluster.local:7687" 21 | imagePullPolicy: Always 22 | securityContext: 23 | privileged: true 24 | volumes: 25 | - name: confdir 26 | -------------------------------------------------------------------------------- /demo-app_lb.yaml: -------------------------------------------------------------------------------- 1 | # Load balancer so we can access machines from outside 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: demo-app-public 6 | labels: 7 | app: neo4j-demo-app 8 | spec: 9 | type: LoadBalancer 10 | ports: 11 | - port: 4567 12 | targetPort: 4567 13 | name: app 14 | selector: 15 | app: neo4j-demo-app 16 | -------------------------------------------------------------------------------- /neo4j.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -exuo pipefail 4 | 5 | # Clean up anything from a prior run: 6 | kubectl delete statefulsets,pods,persistentvolumes,persistentvolumeclaims,services -l app=neo4j 7 | 8 | # Make persistent volumes and (correctly named) claims. We must create the 9 | # claims here manually even though that sounds counter-intuitive. For details 10 | # see https://github.com/kubernetes/contrib/pull/1295#issuecomment-230180894. 11 | for i in $(seq 0 2); do 12 | cat < /work-dir/neo4j.conf" ], 33 | "volumeMounts": [ 34 | { 35 | "name": "confdir", 36 | "mountPath": "/work-dir" 37 | } 38 | ] 39 | } 40 | ]' 41 | labels: 42 | app: neo4j 43 | role: core 44 | spec: 45 | containers: 46 | - name: neo4j 47 | image: "neo4j:3.1.0-enterprise" 48 | imagePullPolicy: Always 49 | ports: 50 | - containerPort: 5000 51 | name: discovery 52 | - containerPort: 6000 53 | name: tx 54 | - containerPort: 7000 55 | name: raft 56 | - containerPort: 7474 57 | name: browser 58 | - containerPort: 7687 59 | name: bolt 60 | securityContext: 61 | privileged: true 62 | volumeMounts: 63 | - name: datadir 64 | mountPath: /data 65 | - name: confdir 66 | mountPath: /conf 67 | volumes: 68 | - name: confdir 69 | volumeClaimTemplates: 70 | - metadata: 71 | name: datadir 72 | annotations: 73 | volume.alpha.kubernetes.io/storage-class: anything 74 | spec: 75 | accessModes: [ "ReadWriteOnce" ] 76 | resources: 77 | requests: 78 | storage: 1Gi 79 | -------------------------------------------------------------------------------- /neo4j_core.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: "apps/v1beta1" 2 | kind: StatefulSet 3 | metadata: 4 | name: neo4j-core 5 | spec: 6 | serviceName: neo4j 7 | replicas: 3 8 | template: 9 | metadata: 10 | labels: 11 | app: neo4j 12 | role: core 13 | spec: 14 | containers: 15 | - name: neo4j 16 | image: "neo4j:3.1.0-enterprise" 17 | imagePullPolicy: Always 18 | env: 19 | - name: NEO4J_causalClustering_initialDiscoveryMembers 20 | value: "neo4j-core-0.neo4j.default.svc.cluster.local:5000,neo4j-core-1.neo4j.default.svc.cluster.local:5000,neo4j-core-2.neo4j.default.svc.cluster.local:5000" 21 | - name: NEO4J_dbms_mode 22 | value: CORE 23 | command: ["/bin/bash", "-c", 'export NEO4J_dbms_connectors_defaultAdvertisedAddress=$(hostname -f) && export NEO4J_causalClustering_discoveryAdvertisedAddress=$(hostname -f)::5000 && 24 | export NEO4J_causalClustering_transactionAdvertisedAddress=$(hostname -f):6000 && export NEO4J_causalClustering_raftAdvertisedAddress=$(hostname -f):7000 && 25 | exec /docker-entrypoint.sh "neo4j"'] 26 | ports: 27 | - containerPort: 5000 28 | name: discovery 29 | - containerPort: 6000 30 | name: tx 31 | - containerPort: 7000 32 | name: raft 33 | - containerPort: 7474 34 | name: browser 35 | - containerPort: 7687 36 | name: bolt 37 | securityContext: 38 | privileged: true 39 | volumeMounts: 40 | - name: datadir 41 | mountPath: /data 42 | volumeClaimTemplates: 43 | - metadata: 44 | name: datadir 45 | annotations: 46 | volume.alpha.kubernetes.io/storage-class: anything 47 | spec: 48 | accessModes: [ "ReadWriteOnce" ] 49 | resources: 50 | requests: 51 | storage: 1Gi 52 | -------------------------------------------------------------------------------- /neo4j_env.yaml: -------------------------------------------------------------------------------- 1 | # Headless service to provide DNS lookup 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | labels: 6 | app: neo4j 7 | name: neo4j 8 | spec: 9 | clusterIP: None 10 | ports: 11 | - port: 7474 12 | targetPort: 7474 13 | selector: 14 | app: neo4j 15 | --- 16 | # Core servers 17 | apiVersion: "apps/v1beta1" 18 | kind: StatefulSet 19 | metadata: 20 | name: neo4j-core 21 | spec: 22 | serviceName: neo4j 23 | replicas: 3 24 | template: 25 | metadata: 26 | labels: 27 | app: neo4j 28 | role: core 29 | spec: 30 | containers: 31 | - name: neo4j 32 | image: "neo4j:3.1.0-enterprise" 33 | imagePullPolicy: Always 34 | env: 35 | - name: NEO4J_dbms_mode 36 | value: "CORE" 37 | - name: NEO4J_causalClustering_initialDiscoveryMembers 38 | value: "neo4j-core-0.neo4j.default.svc.cluster.local:5000,neo4j-core-1.neo4j.default.svc.cluster.local:5000,neo4j-core-2.neo4j.default.svc.cluster.local:5000" 39 | command: ["/bin/sh","-c"] 40 | args: ["export NEO4J_dbms_connectors_defaultAdvertisedAddress=`hostname -f` && \ 41 | export NEO4J_causalClustering_discoveryAdvertisedAddress=`hostname -f`:5000 && \ 42 | export NEO4J_causalClustering_transactionAdvertisedAddress=`hostname -f`:6000 && \ 43 | export NEO4J_causalClustering_raftAdvertisedAddress=`hostname -f`:7000 && \ 44 | exec /docker-entrypoint.sh \"neo4j\""] 45 | ports: 46 | - containerPort: 5000 47 | name: discovery 48 | - containerPort: 6000 49 | name: tx 50 | - containerPort: 7000 51 | name: raft 52 | - containerPort: 7474 53 | name: browser 54 | - containerPort: 7687 55 | name: bolt 56 | securityContext: 57 | privileged: true 58 | volumeMounts: 59 | - name: datadir 60 | mountPath: /data 61 | volumeClaimTemplates: 62 | - metadata: 63 | name: datadir 64 | annotations: 65 | volume.alpha.kubernetes.io/storage-class: anything 66 | spec: 67 | accessModes: [ "ReadWriteOnce" ] 68 | resources: 69 | requests: 70 | storage: 1Gi 71 | -------------------------------------------------------------------------------- /neo4j_lb.yaml: -------------------------------------------------------------------------------- 1 | # Load balancer so we can access machines from outside 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: neo4j-public 6 | labels: 7 | app: neo4j 8 | spec: 9 | type: LoadBalancer 10 | ports: 11 | - port: 7687 12 | targetPort: 7687 13 | name: bolt 14 | - port: 7474 15 | targetPort: 7474 16 | name: browser 17 | selector: 18 | app: neo4j 19 | -------------------------------------------------------------------------------- /neo4j_rr.yaml: -------------------------------------------------------------------------------- 1 | # Replicas 2 | apiVersion: v1 3 | kind: ReplicationController 4 | metadata: 5 | name: neo4j-replica 6 | spec: 7 | replicas: 1 8 | selector: 9 | app: neo4j-replica 10 | template: 11 | metadata: 12 | labels: 13 | app: neo4j-replica 14 | role: replica 15 | spec: 16 | containers: 17 | - name: neo4j 18 | image: "neo4j:3.1.0-enterprise" 19 | imagePullPolicy: Always 20 | env: 21 | - name: NEO4J_causalClustering_initialDiscoveryMembers 22 | value: "neo4j-core-0.neo4j.default.svc.cluster.local:5000,neo4j-core-1.neo4j.default.svc.cluster.local:5000,neo4j-core-2.neo4j.default.svc.cluster.local:5000" 23 | - name: NEO4J_dbms_mode 24 | value: READ_REPLICA 25 | command: ["/bin/bash", "-c", 'export NEO4J_dbms_connectors_defaultAdvertisedAddress=`hostname -f` && exec /docker-entrypoint.sh "neo4j"'] 26 | ports: 27 | - containerPort: 7474 28 | name: browser 29 | - containerPort: 7687 30 | name: bolt 31 | securityContext: 32 | privileged: true 33 | -------------------------------------------------------------------------------- /neo4j_svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: neo4j 6 | name: neo4j 7 | spec: 8 | clusterIP: None 9 | ports: 10 | - port: 7474 11 | targetPort: 7474 12 | selector: 13 | app: neo4j 14 | -------------------------------------------------------------------------------- /src/main/java/org/neo4j/kubernetes/SampleApp.java: -------------------------------------------------------------------------------- 1 | package org.neo4j.kubernetes; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import spark.Route; 6 | import spark.Spark; 7 | 8 | import org.neo4j.driver.v1.AccessMode; 9 | import org.neo4j.driver.v1.Driver; 10 | import org.neo4j.driver.v1.GraphDatabase; 11 | import org.neo4j.driver.v1.Record; 12 | import org.neo4j.driver.v1.Session; 13 | import org.neo4j.driver.v1.StatementResult; 14 | import org.neo4j.driver.v1.Transaction; 15 | import org.neo4j.driver.v1.Values; 16 | 17 | import static spark.Spark.*; 18 | 19 | public class SampleApp 20 | { 21 | public static void main( String[] args ) 22 | { 23 | Logger logger = LoggerFactory.getLogger(SampleApp.class); 24 | 25 | // String uri = "bolt://@192.168.1.3:7687"; 26 | String connectionString = System.getenv( "NEO4J_CONNECTION_STRING" ); 27 | Driver driver = GraphDatabase.driver( connectionString ); 28 | 29 | logger.warn("Connecting to: " + connectionString + " [" + driver + "]"); 30 | 31 | get( "/users", users( driver ) ); 32 | get( "/create-user", createUser( driver ) ); 33 | 34 | Spark.exception( Exception.class, ( exception, request, response ) -> 35 | { 36 | exception.printStackTrace(); 37 | } ); 38 | } 39 | 40 | private static Route createUser( Driver driver ) 41 | { 42 | return (req, res) -> 43 | { 44 | String bookmark; 45 | try ( Session session = driver.session( AccessMode.WRITE ) ) 46 | { 47 | try(Transaction tx = session.beginTransaction()) { 48 | 49 | tx.run( "CREATE (:User {screen_name: 'A.P. Cojones ' + rand()})" ); 50 | tx.success(); 51 | } 52 | 53 | bookmark = session.lastBookmark(); 54 | } 55 | return String.format( "User created [%s]", bookmark ); 56 | }; 57 | } 58 | 59 | private static Route users( Driver driver ) 60 | { 61 | return ( req, res ) -> 62 | { 63 | StringBuilder builder = new StringBuilder(); 64 | 65 | try ( Session session = driver.session( AccessMode.READ ) ) 66 | { 67 | String query = "MATCH (u:User) RETURN u.screen_name AS screenName"; 68 | 69 | StatementResult result = session.run( query ); 70 | 71 | while ( result.hasNext() ) 72 | { 73 | Record row = result.next(); 74 | String screenName = row.get( "screenName" ).asString(); 75 | builder.append( screenName ).append( "
" ); 76 | } 77 | } 78 | return builder.toString(); 79 | }; 80 | } 81 | } 82 | --------------------------------------------------------------------------------