├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── build.sbt ├── docker-compose.yaml ├── kubernetes ├── etcd.yaml ├── nginx.yaml └── nothotdog.yaml ├── project ├── assembly.sbt └── build.properties └── src └── main ├── resources └── application.conf └── scala └── com └── hootsuite └── akkak8s ├── Backend.scala ├── Boot.scala └── Frontend.scala /.dockerignore: -------------------------------------------------------------------------------- 1 | target/* 2 | !target/scala-2.12/akka-cluster-on-kubernetes-assembly-0.1.jar 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore IDEA project files 2 | *.iml 3 | .idea/ 4 | 5 | # Ignore generated stuff 6 | */target 7 | /target 8 | */project/target 9 | */project/project/target 10 | */project/project/project/target 11 | akka-diagnostics/ 12 | 13 | # Ignore OSX stuff 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8u131 2 | ADD target/scala-2.12/akka-cluster-on-kubernetes-assembly-0.1.jar app.jar 3 | ENTRYPOINT ["java", "-jar", "app.jar"] 4 | CMD ["-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-XX:MaxRAMFraction=1", "-XshowSettings:vm"] 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Akka Cluster on Kubernetes 2 | Sample project for deploying Akka Cluster to Kubernetes. 3 | Presented at Scala Up North on July 21, 2017. Video of presentation: [https://www.youtube.com/watch?v=Esd1UKIpvdU](https://www.youtube.com/watch?v=Esd1UKIpvdU) 4 | 5 | To run the project yourself: 6 | 7 | ## Create a Kubernetes Cluster 8 | Follow [https://cloud.google.com/container-engine/docs/quickstart](https://cloud.google.com/container-engine/docs/quickstart) 9 | to create a Kubernetes cluster in minutes. 10 | 11 | I recommend getting `kubectl` set up locally which means you need to install the 12 | `gcloud` SDK: [https://cloud.google.com/sdk/docs/quickstarts](https://cloud.google.com/sdk/docs/quickstarts) 13 | 14 | Then click the **Connect** button next to your cluster name here: [https://console.cloud.google.com/kubernetes/list](https://console.cloud.google.com/kubernetes/list) to set up `kubectl` 15 | 16 | ## Run Locally Without Etcd 17 | The `master` branch is set up to run with etcd so if you want to run locally 18 | then you'll need to uncomment the `seed-node` config in `src/main/resources/application.conf` 19 | 20 | ```hocon 21 | // src/main/resources/application.conf 22 | ... 23 | cluster { 24 | roles = [frontend, backend] 25 | // uncomment this if running locally 26 | seed-nodes = [ 27 | "akka.tcp://ClusterSystem@127.0.0.1:2551" 28 | ] 29 | } 30 | ``` 31 | 32 | Then you can run the project and curl the HTTP endpoint. 33 | 34 | ``` 35 | ➜ sbt run 36 | [info] Running com.hootsuite.akkak8s.SimpleClusterApp 37 | [INFO] [07/21/2017 13:51:35.643] [run-main-0] [akka.remote.Remoting] Starting remoting 38 | [INFO] [07/21/2017 13:51:36.052] [run-main-0] [akka.remote.Remoting] Remoting started; listening on addresses :[akka.tcp://ClusterSystem@127.0.0.1:2551] 39 | [INFO] [07/21/2017 13:51:36.075] [run-main-0] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Starting up... 40 | [INFO] [07/21/2017 13:51:36.269] [run-main-0] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Registered cluster JMX MBean [akka:type=Cluster] 41 | [INFO] [07/21/2017 13:51:36.269] [run-main-0] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Started up successfully 42 | [INFO] [07/21/2017 13:51:36.365] [ClusterSystem-akka.actor.default-dispatcher-14] [akka.tcp://ClusterSystem@127.0.0.1:2551/system/constructr] Stopping self, because seed-nodes defined 43 | [INFO] [07/21/2017 13:51:36.411] [ClusterSystem-akka.actor.default-dispatcher-3] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Node [akka.tcp://ClusterSystem@127.0.0.1:2551] is JOINING, roles [frontend, backend] 44 | [INFO] [07/21/2017 13:51:36.463] [ClusterSystem-akka.actor.default-dispatcher-3] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Leader is moving node [akka.tcp://ClusterSystem@127.0.0.1:2551] to [Up] 45 | ``` 46 | 47 | And now curl the app 48 | 49 | ```bash 50 | ➜ curl "http://localhost:8080?msg=about+a+hotdog" 51 | Hot Dog! (from fe: xxxxxxxx be: xxxxxxxx)% 52 | ➜ curl "http://localhost:8080?msg=about+a+dog" 53 | Not Hot Dog :( (from fe: xxxxxxxx be: xxxxxxxx)% 54 | ``` 55 | 56 | ## Run Locally With Etcd 57 | In Kubernetes, we need to use the [constructr](https://github.com/hseeberger/constructr) library to discover other seed nodes. 58 | 59 | To test this out locally, comment out the `seed-nodes` config 60 | 61 | ```hocon 62 | roles = [frontend, backend] 63 | // uncomment this if running locally 64 | // seed-nodes = [ 65 | // "akka.tcp://ClusterSystem@127.0.0.1:2551" 66 | // ] 67 | ``` 68 | 69 | And start etcd with [Docker for Mac](https://www.docker.com/docker-mac). 70 | 71 | ```bash 72 | ➜ docker run -d \ 73 | --name etcd \ 74 | --publish 2379:2379 \ 75 | quay.io/coreos/etcd:v2.3.7 \ 76 | --listen-client-urls http://0.0.0.0:2379 \ 77 | --advertise-client-urls http://192.168.99.100:2379 78 | ``` 79 | 80 | Finally, restart the app 81 | 82 | ``` 83 | ➜ sbt run 84 | 85 | [info] Running com.hootsuite.akkak8s.SimpleClusterApp 86 | [INFO] [07/21/2017 13:50:16.051] [run-main-0] [akka.remote.Remoting] Starting remoting 87 | [INFO] [07/21/2017 13:50:16.503] [run-main-0] [akka.remote.Remoting] Remoting started; listening on addresses :[akka.tcp://ClusterSystem@127.0.0.1:2551] 88 | [INFO] [07/21/2017 13:50:16.530] [run-main-0] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Starting up... 89 | [INFO] [07/21/2017 13:50:16.870] [run-main-0] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Registered cluster JMX MBean [akka:type=Cluster] 90 | [INFO] [07/21/2017 13:50:16.870] [run-main-0] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Started up successfully 91 | [INFO] [07/21/2017 13:50:16.933] [ClusterSystem-akka.actor.default-dispatcher-6] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - No seed-nodes configured, manual cluster join required 92 | [INFO] [07/21/2017 13:50:16.951] [ClusterSystem-akka.actor.default-dispatcher-2] [akka.tcp://ClusterSystem@127.0.0.1:2551/system/constructr] Creating constructr-machine, because no seed-nodes defined 93 | [INFO] [07/21/2017 13:50:20.348] [ClusterSystem-akka.actor.default-dispatcher-6] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Node [akka.tcp://ClusterSystem@127.0.0.1:2551] is JOINING, roles [frontend, backend] 94 | [INFO] [07/21/2017 13:50:20.370] [ClusterSystem-akka.actor.default-dispatcher-6] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Leader is moving node [akka.tcp://ClusterSystem@127.0.0.1:2551] to [Up] 95 | ``` 96 | 97 | And now curl the app again 98 | 99 | ```bash 100 | ➜ curl "http://localhost:8080?msg=about+a+hotdog" 101 | Hot Dog! (from fe: xxxxxxxx be: xxxxxxxx)% 102 | ➜ curl "http://localhost:8080?msg=about+a+dog" 103 | Not Hot Dog :( (from fe: xxxxxxxx be: xxxxxxxx)% 104 | ``` 105 | 106 | ## Package into Docker 107 | ```bash 108 | ➜ sbt assembly 109 | # outputs target/scala-2.12/akka-cluster-on-kubernetes-assembly-0.1.jar 110 | # now build that into a docker image 111 | ➜ docker build -t {your namespace}/akka-cluster . 112 | ``` 113 | 114 | ## Test Locally With Docker Compose 115 | We still need etcd but we want it accessible from the same network as our app which 116 | is now running on Docker, not on localhost. To do this we use [Docker Compose](https://docs.docker.com/compose/overview/). 117 | 118 | ``` 119 | ➜ docker-compose up 120 | 121 | Starting akkaclusteronkubernetes_etcd_1 ... 122 | Starting akkaclusteronkubernetes_etcd_1 ... done 123 | Starting akkaclusteronkubernetes_akka_1 ... 124 | Starting akkaclusteronkubernetes_akka_1 ... done 125 | Attaching to akkaclusteronkubernetes_etcd_1, akkaclusteronkubernetes_akka_1 126 | etcd_1 | 2017-07-21 21:08:21.112937 I | etcdmain: etcd Version: 2.3.7 127 | etcd_1 | 2017-07-21 21:08:21.113016 I | etcdmain: Git SHA: fd17c91 128 | etcd_1 | 2017-07-21 21:08:21.113026 I | etcdmain: Go Version: go1.6.2 129 | etcd_1 | 2017-07-21 21:08:21.113041 I | etcdmain: Go OS/Arch: linux/amd64 130 | etcd_1 | 2017-07-21 21:08:21.113052 I | etcdmain: setting maximum number of CPUs to 2, total number of available CPUs is 2 131 | etcd_1 | 2017-07-21 21:08:21.113058 W | etcdmain: no data-dir provided, using default data-dir ./default.etcd 132 | etcd_1 | 2017-07-21 21:08:21.113834 N | etcdmain: the server is already initialized as member before, starting as etcd member... 133 | etcd_1 | 2017-07-21 21:08:21.114360 I | etcdmain: listening for peers on http://localhost:2380 134 | etcd_1 | 2017-07-21 21:08:21.114539 I | etcdmain: listening for peers on http://localhost:7001 135 | etcd_1 | 2017-07-21 21:08:21.114584 I | etcdmain: listening for client requests on http://0.0.0.0:2379 136 | etcd_1 | 2017-07-21 21:08:21.124405 I | etcdserver: name = default 137 | etcd_1 | 2017-07-21 21:08:21.124445 I | etcdserver: data dir = default.etcd 138 | etcd_1 | 2017-07-21 21:08:21.124453 I | etcdserver: member dir = default.etcd/member 139 | etcd_1 | 2017-07-21 21:08:21.124461 I | etcdserver: heartbeat = 100ms 140 | etcd_1 | 2017-07-21 21:08:21.124465 I | etcdserver: election = 1000ms 141 | etcd_1 | 2017-07-21 21:08:21.124470 I | etcdserver: snapshot count = 10000 142 | etcd_1 | 2017-07-21 21:08:21.124505 I | etcdserver: advertise client URLs = http://0.0.0.0:2379 143 | etcd_1 | 2017-07-21 21:08:21.128006 I | etcdserver: restarting member ce2a822cea30bfca in cluster 7e27652122e8b2ae at commit index 424 144 | etcd_1 | 2017-07-21 21:08:21.128122 I | raft: ce2a822cea30bfca became follower at term 10 145 | etcd_1 | 2017-07-21 21:08:21.128165 I | raft: newRaft ce2a822cea30bfca [peers: [], term: 10, commit: 424, applied: 0, lastindex: 424, lastterm: 10] 146 | etcd_1 | 2017-07-21 21:08:21.130984 I | etcdserver: starting server... [version: 2.3.7, cluster version: to_be_decided] 147 | etcd_1 | 2017-07-21 21:08:21.134951 N | etcdserver: added local member ce2a822cea30bfca [http://localhost:2380 http://localhost:7001] to cluster 7e27652122e8b2ae 148 | etcd_1 | 2017-07-21 21:08:21.135076 N | etcdserver: set the initial cluster version to 2.3 149 | etcd_1 | 2017-07-21 21:08:22.431440 I | raft: ce2a822cea30bfca is starting a new election at term 10 150 | etcd_1 | 2017-07-21 21:08:22.431735 I | raft: ce2a822cea30bfca became candidate at term 11 151 | etcd_1 | 2017-07-21 21:08:22.431922 I | raft: ce2a822cea30bfca received vote from ce2a822cea30bfca at term 11 152 | etcd_1 | 2017-07-21 21:08:22.432299 I | raft: ce2a822cea30bfca became leader at term 11 153 | etcd_1 | 2017-07-21 21:08:22.432405 I | raft: raft.node: ce2a822cea30bfca elected leader ce2a822cea30bfca at term 11 154 | etcd_1 | 2017-07-21 21:08:22.433347 I | etcdserver: published {Name:default ClientURLs:[http://0.0.0.0:2379]} to cluster 7e27652122e8b2ae 155 | akka_1 | [INFO] [07/21/2017 21:08:24.309] [main] [akka.remote.Remoting] Starting remoting 156 | akka_1 | [INFO] [07/21/2017 21:08:24.670] [main] [akka.remote.Remoting] Remoting started; listening on addresses :[akka.tcp://ClusterSystem@127.0.0.1:2551] 157 | akka_1 | [INFO] [07/21/2017 21:08:24.696] [main] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Starting up... 158 | akka_1 | [INFO] [07/21/2017 21:08:24.936] [main] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Registered cluster JMX MBean [akka:type=Cluster] 159 | akka_1 | [INFO] [07/21/2017 21:08:24.936] [main] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Started up successfully 160 | akka_1 | [INFO] [07/21/2017 21:08:25.005] [ClusterSystem-akka.actor.default-dispatcher-5] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - No seed-nodes configured, manual cluster join required 161 | akka_1 | [INFO] [07/21/2017 21:08:25.006] [ClusterSystem-akka.actor.default-dispatcher-2] [akka.tcp://ClusterSystem@127.0.0.1:2551/system/constructr] Creating constructr-machine, because no seed-nodes defined 162 | akka_1 | [INFO] [07/21/2017 21:08:27.112] [ClusterSystem-akka.actor.default-dispatcher-4] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Node [akka.tcp://ClusterSystem@127.0.0.1:2551] is JOINING, roles [frontend, backend] 163 | akka_1 | [INFO] [07/21/2017 21:08:27.143] [ClusterSystem-akka.actor.default-dispatcher-4] [akka.cluster.Cluster(akka://ClusterSystem)] Cluster Node [akka.tcp://ClusterSystem@127.0.0.1:2551] - Leader is moving node [akka.tcp://ClusterSystem@127.0.0.1:2551] to [Up] 164 | ``` 165 | ## Deploy to Kubernetes 166 | Finally we're ready to deploy to Kubernetes! 167 | 168 | First deploy etcd 169 | 170 | ```bash 171 | ➜ docker push {your namespace}/akka-cluster . 172 | ``` 173 | 174 | ```bash 175 | ➜ kubectl apply -f kubernetes/etcd.yaml 176 | ``` 177 | 178 | Run a "bounce" pod so you can talk to the cluster easily. 179 | I'm using my colleague's debug container which has a bunch of tools built in: [https://github.com/markeijsermans/docker-debug](https://github.com/markeijsermans/docker-debug). 180 | 181 | ```bash 182 | ➜ kubectl run bounce --image=markeijsermans/debug -it bash 183 | If you don't see a command prompt, try pressing enter. 184 | (21:15 bounce-2304503334-6dqpw:/) curl etcd:2379/health 185 | {"health": "true"} 186 | ``` 187 | 188 | Now deploy the app! If you've been pushing your own Docker images, you'll need to edit the 189 | `kubernetes/nothotdog.yaml` file to use your image. Specifically these lines 190 | 191 | ```yaml 192 | ... 193 | image: lkysow/akka-cluster 194 | ... 195 | ``` 196 | 197 | Push your docker image and then apply the app. 198 | 199 | ```bash 200 | ➜ docker push {your namespace}/akka-cluster 201 | ➜ kubectl apply -f kubernetes/nothotdog.yaml 202 | ``` 203 | 204 | From your bounce pod, you should be able to curl the app! 205 | 206 | ```bash 207 | ➜ curl nothotdog:8080?msg=about-a-hotdog 208 | Hot Dog! (from fe: frontend-3857959296-5x885 be: backend-3899286914-6941z) 209 | ``` 210 | 211 | ## Play Around With Kubernetes 212 | 213 | `curl` the app in a loop from the bounce pod 214 | 215 | ```bash 216 | ➜ while true; do curl -sS -m 1.5 nothotdog:8080?msg=about-a-hotdog; echo ""; sleep 0.5; done 217 | Hot Dog! (from fe: frontend-3857959296-5x885 be: backend-3899286914-6941z) 218 | Hot Dog! (from fe: frontend-3857959296-5x885 be: backend-3899286914-6941z) 219 | Hot Dog! (from fe: frontend-3857959296-5x885 be: backend-3899286914-6941z) 220 | ``` 221 | 222 | Scale the app 223 | 224 | ```bash 225 | ➜ kubectl scale deployment backend --replicas=3 226 | ➜ kubectl scale deployment frontend --replicas=3 227 | ``` 228 | 229 | Add autoscaling 230 | 231 | ```bash 232 | ➜ kubectl autoscale deploy backend --min=1 --max=3 --cpu-percent=5 233 | ➜ kubectl get hpa 234 | ``` 235 | 236 | Add some load from the bounce pod 237 | 238 | ```bash 239 | ➜ slow_cooker -concurrency 10 -qps 300 -interval 5s "http://nothotdog:8080?msg=hotdog" 240 | ``` 241 | 242 | And you're done! Welcome to Akka Cluster on Kubernetes :D 243 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "akka-cluster-on-kubernetes" 2 | 3 | version := "0.1" 4 | 5 | scalaVersion := "2.12.2" 6 | 7 | libraryDependencies ++= Seq( 8 | "com.typesafe.akka" %% "akka-actor" % "2.5.3", 9 | "com.typesafe.akka" %% "akka-cluster" % "2.5.3", 10 | "com.typesafe.akka" %% "akka-http" % "10.0.9", 11 | "com.typesafe.akka" %% "akka-http-spray-json" % "10.0.9", 12 | "de.heikoseeberger" %% "constructr" % "0.17.0", 13 | "de.heikoseeberger" %% "constructr-coordination-etcd" % "0.17.0" 14 | ) 15 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | etcd: 4 | image: quay.io/coreos/etcd:v2.3.7 5 | command: --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://0.0.0.0:2379 6 | akka: 7 | image: lkysow/akka-cluster 8 | ports: ["8080:8080"] 9 | links: [etcd] 10 | environment: 11 | - ETCD_SERVICE_HOST=etcd 12 | -------------------------------------------------------------------------------- /kubernetes/etcd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: etcd 5 | spec: 6 | replicas: 1 7 | template: 8 | metadata: 9 | labels: 10 | app: etcd 11 | spec: 12 | containers: 13 | - name: etcd 14 | args: 15 | - --listen-client-urls=http://0.0.0.0:2379 16 | - --advertise-client-urls=http://etcd:2379 17 | image: quay.io/coreos/etcd:v2.3.7 18 | ports: 19 | - containerPort: 2379 20 | --- 21 | kind: Service 22 | apiVersion: v1 23 | metadata: 24 | name: etcd 25 | spec: 26 | selector: 27 | app: etcd 28 | ports: 29 | - protocol: TCP 30 | port: 2379 31 | targetPort: 2379 32 | -------------------------------------------------------------------------------- /kubernetes/nginx.yaml: -------------------------------------------------------------------------------- 1 | kind: Deployment 2 | apiVersion: apps/v1beta1 3 | metadata: 4 | name: nginx 5 | spec: 6 | replicas: 1 7 | template: 8 | metadata: 9 | labels: 10 | app: nginx 11 | spec: 12 | containers: 13 | - name: nginx 14 | image: nginx 15 | ports: 16 | - containerPort: 80 17 | --- 18 | kind: Service 19 | apiVersion: v1 20 | metadata: 21 | name: nginx 22 | spec: 23 | selector: 24 | app: nginx 25 | ports: 26 | - protocol: TCP 27 | port: 80 28 | targetPort: 80 29 | -------------------------------------------------------------------------------- /kubernetes/nothotdog.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: frontend 5 | spec: 6 | replicas: 1 7 | template: 8 | metadata: 9 | labels: 10 | app: frontend 11 | spec: 12 | containers: 13 | - name: akka-cluster 14 | image: lkysow/akka-cluster 15 | imagePullPolicy: Always 16 | env: 17 | - name: POD_IP 18 | valueFrom: 19 | fieldRef: 20 | fieldPath: status.podIP 21 | - name: CLUSTER_ROLES 22 | value: frontend 23 | ports: 24 | - containerPort: 8080 25 | - containerPort: 2551 26 | resources: 27 | requests: 28 | cpu: 0.5 29 | memory: 512Mi 30 | limits: 31 | cpu: 1.5 32 | memory: 1Gi 33 | livenessProbe: 34 | httpGet: 35 | path: /health 36 | port: 8080 37 | initialDelaySeconds: 30 38 | timeoutSeconds: 2 39 | readinessProbe: 40 | httpGet: 41 | path: /health 42 | port: 8080 43 | initialDelaySeconds: 0 44 | --- 45 | kind: Service 46 | apiVersion: v1 47 | metadata: 48 | name: nothotdog 49 | spec: 50 | selector: 51 | app: frontend 52 | ports: 53 | - protocol: TCP 54 | port: 8080 55 | targetPort: 8080 56 | 57 | --- 58 | apiVersion: apps/v1beta1 59 | kind: Deployment 60 | metadata: 61 | name: backend 62 | spec: 63 | replicas: 1 64 | template: 65 | metadata: 66 | labels: 67 | app: backend 68 | spec: 69 | containers: 70 | - name: akka-cluster 71 | image: lkysow/akka-cluster 72 | imagePullPolicy: Always 73 | env: 74 | - name: POD_IP 75 | valueFrom: 76 | fieldRef: 77 | fieldPath: status.podIP 78 | - name: CLUSTER_ROLES 79 | value: backend 80 | ports: 81 | - containerPort: 2551 82 | resources: 83 | requests: 84 | cpu: 0.5 85 | memory: 512Mi 86 | limits: 87 | cpu: 1.5 88 | memory: 1Gi 89 | readinessProbe: 90 | tcpSocket: 91 | port: 2551 92 | initialDelaySeconds: 5 93 | periodSeconds: 10 94 | livenessProbe: 95 | tcpSocket: 96 | port: 2551 97 | initialDelaySeconds: 15 98 | periodSeconds: 20 99 | -------------------------------------------------------------------------------- /project/assembly.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.5") 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 0.13.15 -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | actor { 3 | provider = "cluster" 4 | } 5 | remote { 6 | log-remote-lifecycle-events = off 7 | netty.tcp { 8 | hostname = 127.0.0.1 9 | hostname = ${?POD_IP} 10 | port = 2551 11 | } 12 | } 13 | cluster { 14 | roles = [frontend, backend] 15 | // seed-nodes = [ 16 | // "akka.tcp://ClusterSystem@127.0.0.1:2551" 17 | // ] 18 | } 19 | } 20 | akka.cluster.metrics.enabled=off 21 | constructr { 22 | coordination { 23 | host = localhost 24 | host = ${?ETCD_SERVICE_HOST} 25 | port = 2379 26 | } 27 | } 28 | akka.extensions = [de.heikoseeberger.constructr.ConstructrExtension] 29 | -------------------------------------------------------------------------------- /src/main/scala/com/hootsuite/akkak8s/Backend.scala: -------------------------------------------------------------------------------- 1 | package com.hootsuite.akkak8s 2 | 3 | import java.net.InetAddress 4 | 5 | import akka.actor.Actor 6 | 7 | class Backend extends Actor { 8 | val hostname = InetAddress.getLocalHost.getHostName 9 | def receive = { 10 | case NotHotDogRequest(message) => 11 | if (message.toLowerCase.contains("hotdog")) { 12 | sender() ! NotHotDogResponse(true, hostname) 13 | } else { 14 | sender() ! NotHotDogResponse(false, hostname) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/com/hootsuite/akkak8s/Boot.scala: -------------------------------------------------------------------------------- 1 | package com.hootsuite.akkak8s 2 | 3 | import java.net.InetAddress 4 | 5 | import akka.actor.{ActorSystem, CoordinatedShutdown, Props} 6 | import akka.cluster.Cluster 7 | import akka.cluster.routing.{ClusterRouterGroup, ClusterRouterGroupSettings} 8 | import akka.http.scaladsl.Http 9 | import akka.http.scaladsl.server.Directives._ 10 | import akka.pattern.ask 11 | import akka.routing.RoundRobinGroup 12 | import akka.stream.ActorMaterializer 13 | import akka.util.Timeout 14 | import com.typesafe.config.ConfigFactory 15 | 16 | import scala.concurrent.Future 17 | import scala.concurrent.duration._ 18 | 19 | object SimpleClusterApp extends App { 20 | 21 | // akka init 22 | val baseConfig = ConfigFactory.load() 23 | val overrideConfig = sys.env.get("CLUSTER_ROLES").map(roles => s"akka.cluster.roles = [$roles]").getOrElse("") 24 | val config = ConfigFactory.parseString(overrideConfig).withFallback(baseConfig) 25 | 26 | implicit val system = ActorSystem("ClusterSystem", config) 27 | implicit val materializer = ActorMaterializer() 28 | implicit val executionContext = system.dispatcher 29 | 30 | // initialize the cluster 31 | val cluster = Cluster(system) 32 | 33 | if (cluster.selfRoles.contains("backend")) { 34 | system.actorOf(Props[Backend], name = "backend") 35 | } 36 | 37 | if (cluster.selfRoles.contains("frontend")) { 38 | val backendRouter = system.actorOf( 39 | ClusterRouterGroup( 40 | RoundRobinGroup(Nil), 41 | ClusterRouterGroupSettings( 42 | totalInstances = 1000, 43 | routeesPaths = List("/user/backend"), 44 | allowLocalRoutees = true, 45 | useRole = Some("backend") 46 | ) 47 | ).props(), 48 | name = "backendRouter") 49 | 50 | val frontend = system.actorOf(Frontend.props(backendRouter), name = "frontend") 51 | val hostname = InetAddress.getLocalHost.getHostName 52 | 53 | // create HTTP routes 54 | val route = 55 | path("") { 56 | get { 57 | parameter("msg") { (message) => 58 | implicit val timeout: Timeout = 1.second 59 | val response: Future[NotHotDogResponse] = (frontend ? NotHotDogRequest(message)).mapTo[NotHotDogResponse] 60 | complete(response.map(r => if (r.hotDog) s"Hot Dog! (from fe: ${hostname} be: ${r.src})" else s"Not Hot Dog :( (from fe: ${hostname} be: ${r.src})")) 61 | } 62 | } 63 | } ~ path("health") { 64 | get { 65 | complete("OK") 66 | } 67 | } 68 | 69 | // start server 70 | val bindingFuture = Http().bindAndHandle(route, "0.0.0.0", 8080) 71 | 72 | // ensure akka-http shuts down cleanly 73 | CoordinatedShutdown(system).addJvmShutdownHook({ 74 | bindingFuture 75 | .flatMap(_.unbind()) 76 | }) 77 | } 78 | } 79 | 80 | final case class NotHotDogResponse(hotDog: Boolean, src: String) 81 | 82 | final case class NotHotDogRequest(message: String) 83 | -------------------------------------------------------------------------------- /src/main/scala/com/hootsuite/akkak8s/Frontend.scala: -------------------------------------------------------------------------------- 1 | package com.hootsuite.akkak8s 2 | 3 | import akka.actor.{Actor, ActorLogging, ActorRef, Props} 4 | 5 | class Frontend(backend: ActorRef) extends Actor with ActorLogging { 6 | def receive = { 7 | case r: NotHotDogRequest => 8 | backend.forward(r) 9 | } 10 | } 11 | 12 | object Frontend { 13 | def props(backend: ActorRef): Props = Props(new Frontend(backend)) 14 | } 15 | --------------------------------------------------------------------------------