From 6b744c244d6c2c56444e8baae6fcc794659d30a7 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Thu, 18 Apr 2024 20:38:52 +0200 Subject: [PATCH] doc: Include samples (#1279) * sample for Deploying Akka Cluster to Kubernetes * zip download from docs --- .../workflows/integration-tests-samples.yml | 62 +++++++ .github/workflows/release.yml | 1 + .gitignore | 3 + docs/release-train-issue-template.md | 4 +- docs/src/main/paradox/bootstrap/recipes.md | 10 +- .../paradox/kubernetes-deployment/index.md | 4 +- .../preparing-for-production.md | 4 +- .../test.sh | 63 ++++++++ integration-test/scripts/kubernetes-test.sh | 2 +- .../README.md | 58 +++++++ .../kubernetes/akka-cluster.yml | 82 ++++++++++ .../kubernetes/namespace.json | 11 ++ .../pom.xml | 152 ++++++++++++++++++ .../akka/cluster/bootstrap/demo/DemoApp.java | 73 +++++++++ .../bootstrap/demo/DemoHealthCheck.java | 22 +++ .../src/main/resources/application.conf | 36 +++++ .../src/main/resources/logback.xml | 21 +++ .../README.md | 60 +++++++ .../build.sbt | 46 ++++++ .../kubernetes/akka-cluster.yml | 78 +++++++++ .../kubernetes/namespace.json | 11 ++ .../project/build.properties | 1 + .../project/plugins.sbt | 4 + .../src/main/resources/application.conf | 31 ++++ .../src/main/resources/logback.xml | 21 +++ .../sample/cluster/kubernetes/DemoApp.scala | 37 +++++ .../cluster/kubernetes/DemoHealthCheck.scala | 16 ++ scripts/prepare-downloads.sh | 73 +++++++++ 28 files changed, 973 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/integration-tests-samples.yml create mode 100755 integration-test/akka-sample-cluster-kubernetes-scala/test.sh create mode 100644 samples/akka-sample-cluster-kubernetes-java/README.md create mode 100644 samples/akka-sample-cluster-kubernetes-java/kubernetes/akka-cluster.yml create mode 100644 samples/akka-sample-cluster-kubernetes-java/kubernetes/namespace.json create mode 100644 samples/akka-sample-cluster-kubernetes-java/pom.xml create mode 100644 samples/akka-sample-cluster-kubernetes-java/src/main/java/akka/cluster/bootstrap/demo/DemoApp.java create mode 100644 samples/akka-sample-cluster-kubernetes-java/src/main/java/akka/cluster/bootstrap/demo/DemoHealthCheck.java create mode 100644 samples/akka-sample-cluster-kubernetes-java/src/main/resources/application.conf create mode 100644 samples/akka-sample-cluster-kubernetes-java/src/main/resources/logback.xml create mode 100644 samples/akka-sample-cluster-kubernetes-scala/README.md create mode 100644 samples/akka-sample-cluster-kubernetes-scala/build.sbt create mode 100644 samples/akka-sample-cluster-kubernetes-scala/kubernetes/akka-cluster.yml create mode 100644 samples/akka-sample-cluster-kubernetes-scala/kubernetes/namespace.json create mode 100644 samples/akka-sample-cluster-kubernetes-scala/project/build.properties create mode 100644 samples/akka-sample-cluster-kubernetes-scala/project/plugins.sbt create mode 100644 samples/akka-sample-cluster-kubernetes-scala/src/main/resources/application.conf create mode 100644 samples/akka-sample-cluster-kubernetes-scala/src/main/resources/logback.xml create mode 100644 samples/akka-sample-cluster-kubernetes-scala/src/main/scala/akka/sample/cluster/kubernetes/DemoApp.scala create mode 100644 samples/akka-sample-cluster-kubernetes-scala/src/main/scala/akka/sample/cluster/kubernetes/DemoHealthCheck.scala create mode 100755 scripts/prepare-downloads.sh diff --git a/.github/workflows/integration-tests-samples.yml b/.github/workflows/integration-tests-samples.yml new file mode 100644 index 000000000..31269881b --- /dev/null +++ b/.github/workflows/integration-tests-samples.yml @@ -0,0 +1,62 @@ +name: Integration test for akka-sample-cluster-kubernetes-scala + +on: + pull_request: + push: + branches: + - main + - release-* + tags-ignore: [ v.* ] + schedule: + - cron: '0 2 * * *' # every day 2am + +permissions: + contents: read + +jobs: + integration-test: + name: Integration Tests for akka-sample-cluster-kubernetes-scala + runs-on: ubuntu-22.04 + steps: + - name: Checkout + # https://github.com/actions/checkout/releases + # v4.1.1 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + with: + fetch-depth: 0 + + - name: Checkout GitHub merge + if: github.event.pull_request + run: |- + git fetch origin pull/${{ github.event.pull_request.number }}/merge:scratch + git checkout scratch + + - name: Cache Coursier cache + # https://github.com/coursier/cache-action/releases + # v6.4.5 + uses: coursier/cache-action@1ff273bff02a8787bc9f1877d347948af647956d + + - name: Set up JDK 17 + # https://github.com/coursier/setup-action/releases + # v1.3.5 + uses: coursier/setup-action@7bde40eee928896f074dbb76d22dd772eed5c65f + with: + jvm: temurin:1.17.0 + + - name: Setup Minikube + # https://github.com/manusa/actions-setup-minikube/releases + # v2.7.1 + uses: manusa/actions-setup-minikube@4582844dcacbf482729f8d7ef696f515d2141bb9 + with: + minikube version: 'v1.32.0' + kubernetes version: 'v1.28.4' + driver: docker + start args: '--addons=ingress' + + - name: Run Integration Tests + run: |- + ./integration-test/akka-sample-cluster-kubernetes-scala/test.sh + + - name: Print logs on failure + if: ${{ failure() }} + run: find . -name "*.log" -exec ./scripts/cat-log.sh {} \; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ce4cac5e..07bb83d13 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,6 +76,7 @@ jobs: - name: Publish run: |- + scripts/prepare-downloads.sh eval "$(ssh-agent -s)" echo $AKKA_RSYNC_GUSTAV | base64 -d > .github/id_rsa chmod 600 .github/id_rsa diff --git a/.gitignore b/.gitignore index c35fcc96c..cc2ca53ee 100755 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ target/ .metals .vscode + +# attachments created by scripts/prepare-downloads.sh +docs/src/main/paradox/attachments diff --git a/docs/release-train-issue-template.md b/docs/release-train-issue-template.md index 6d6030cc5..0e3104417 100644 --- a/docs/release-train-issue-template.md +++ b/docs/release-train-issue-template.md @@ -15,6 +15,7 @@ Variables to be expanded in this template: - [ ] Check that open PRs and issues assigned to the milestone are reasonable - [ ] Update the Change date and version in the LICENSE file. +- [ ] Update the Akka Management version in the samples to $VERSION$, otherwise the published zip files of the samples will have the old version. - [ ] Create a new milestone for the [next version](https://github.com/akka/akka-management/milestones) - [ ] Close the [$VERSION$ milestone](https://github.com/akka/akka-management/milestones?direction=asc&sort=due_date) - [ ] Make sure all important PRs have been merged @@ -46,7 +47,6 @@ For important patch releases, and only if critical issues have been fixed: - [ ] Send a release notification to [Lightbend discuss](https://discuss.akka.io) - [ ] Tweet using the [@akkateam](https://twitter.com/akkateam/) account (or ask someone to) about the new release -- [ ] Announce on [Gitter akka/akka](https://gitter.im/akka/akka) - [ ] Announce internally (with links to Tweet, discuss) For minor or major releases: @@ -56,6 +56,6 @@ For minor or major releases: ### Afterwards - [ ] Update [akka-dependencies bom](https://github.com/lightbend/akka-dependencies) and version for [Akka module versions](https://doc.akka.io/docs/akka-dependencies/current/) in [akka-dependencies repo](https://github.com/akka/akka-dependencies) -- [ ] Update [Akka Guide samples](https://github.com/akka/akka-platform-guide) +- [ ] Update [Akka Guide samples](https://github.com/lightbend/akka-guide) - Close this issue diff --git a/docs/src/main/paradox/bootstrap/recipes.md b/docs/src/main/paradox/bootstrap/recipes.md index a38514096..40aff8b65 100644 --- a/docs/src/main/paradox/bootstrap/recipes.md +++ b/docs/src/main/paradox/bootstrap/recipes.md @@ -3,11 +3,6 @@ A set of integration tests projects can be found in [integration-test folder of the Akka Management project](https://github.com/akka/akka-management/tree/main/integration-test). These test various Akka management features together in various environments such as Kubernetes. -The following samples exist as standalone projects: - -* [Akka Cluster bootstrap using the Kubernetes API with Java/Maven](https://github.com/akka/akka-sample-cluster-kubernetes-java) -* [Akka Cluster bootstrap using DNS in Kubernetes](https://github.com/akka/akka-sample-cluster-kubernetes-dns-java) - ## Local To run Bootstrap locally without any dependencies such as DNS or Kubernetes see the @ref[`local` example](local-config.md) @@ -25,7 +20,10 @@ The recommended approach is to: ### Example project -To get started, it might be helpful to have a look at the [Akka Cluster on Kubernetes](https://developer.lightbend.com/start/?group=akka&project=akka-sample-cluster-kubernetes-java) example project. +To get started, it might be helpful to have a look at the example projects. + +* [Akka Cluster bootstrap using the Kubernetes API with Java/Maven](../attachments/akka-sample-cluster-kubernetes-java.zip) +* [Akka Cluster bootstrap using the Kubernetes API with Scala/sbt](../attachments/akka-sample-cluster-kubernetes-scala.zip) ### Kubernetes Deployment diff --git a/docs/src/main/paradox/kubernetes-deployment/index.md b/docs/src/main/paradox/kubernetes-deployment/index.md index d355f0a8a..9c1c4bb12 100644 --- a/docs/src/main/paradox/kubernetes-deployment/index.md +++ b/docs/src/main/paradox/kubernetes-deployment/index.md @@ -3,8 +3,8 @@ For this guide we will be using the Akka Cluster in Kubernetes sample. It is available for both: - * [Java](https://developer.lightbend.com/start/?group=akka&project=akka-sample-cluster-kubernetes-java) - * [Scala](https://developer.lightbend.com/start/?group=akka&project=akka-sample-cluster-kubernetes-scala) +* [Java](../attachments/akka-sample-cluster-kubernetes-java.zip) +* [Scala](../attachments/akka-sample-cluster-kubernetes-scala.zip) @@toc { depth=2 } diff --git a/docs/src/main/paradox/kubernetes-deployment/preparing-for-production.md b/docs/src/main/paradox/kubernetes-deployment/preparing-for-production.md index 50338c96d..79d0a8b96 100644 --- a/docs/src/main/paradox/kubernetes-deployment/preparing-for-production.md +++ b/docs/src/main/paradox/kubernetes-deployment/preparing-for-production.md @@ -8,8 +8,8 @@ In preparation for production, we need to do two main things: The final configuration file and deployment spec are in the sample application. In this guide we will show snippets. Locations of the samples: -* [Java](https://developer.lightbend.com/start/?group=akka&project=akka-sample-cluster-kubernetes-java) -* [Scala](https://developer.lightbend.com/start/?group=akka&project=akka-sample-cluster-kubernetes-scala) +* [Java](../attachments/akka-sample-cluster-kubernetes-java.zip) +* [Scala](../attachments/akka-sample-cluster-kubernetes-scala.zip) ## Deployment Spec diff --git a/integration-test/akka-sample-cluster-kubernetes-scala/test.sh b/integration-test/akka-sample-cluster-kubernetes-scala/test.sh new file mode 100755 index 000000000..5a130f873 --- /dev/null +++ b/integration-test/akka-sample-cluster-kubernetes-scala/test.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +set -exu + +export NAMESPACE=appka-1 +export APP_NAME=appka +export PROJECT_DIR=samples/akka-sample-cluster-kubernetes-scala +export DEPLOYMENT=kubernetes/akka-cluster.yml + +eval $(minikube -p minikube docker-env) +cd $PROJECT_DIR +sbt Docker/publishLocal + +docker images | head + +kubectl create namespace $NAMESPACE || true +kubectl -n $NAMESPACE apply -f $DEPLOYMENT + +for i in {1..10} +do + echo "Waiting for pods to get ready..." + kubectl get pods -n $NAMESPACE + [ `kubectl get pods -n $NAMESPACE | grep Running | wc -l` -eq 3 ] && break + sleep 4 +done + +if [ $i -eq 10 ] +then + echo "Pods did not get ready" + kubectl -n $NAMESPACE events $APP_NAME + kubectl -n $NAMESPACE describe deployment $APP_NAME + exit -1 +fi + +POD=$(kubectl get pods -n $NAMESPACE | grep $APP_NAME | grep Running | head -n1 | awk '{ print $1 }') + +for i in {1..15} +do + echo "Checking for MemberUp logging..." + kubectl logs $POD -n $NAMESPACE | grep MemberUp || true + [ `kubectl logs -n $NAMESPACE $POD | grep MemberUp | wc -l` -eq 3 ] && break + sleep 3 +done + +echo "Logs" +echo "==============================" +for POD in $(kubectl get pods -n $NAMESPACE | grep $APP_NAME | grep Running | awk '{ print $1 }') +do + echo "Logging for $POD" + kubectl logs $POD -n $NAMESPACE +done + + +if [ $i -eq 15 ] +then + echo "No 3 MemberUp log events found" + echo "==============================" + + exit -1 +fi + + + diff --git a/integration-test/scripts/kubernetes-test.sh b/integration-test/scripts/kubernetes-test.sh index e7bdd45f8..c8d90f812 100755 --- a/integration-test/scripts/kubernetes-test.sh +++ b/integration-test/scripts/kubernetes-test.sh @@ -5,7 +5,7 @@ sbt $PROJECT_NAME/Docker/publishLocal docker images | head -kubectl create namespace akka-bootstrap-demo-ns || true +kubectl create namespace $NAMESPACE || true kubectl -n $NAMESPACE apply -f $DEPLOYMENT for i in {1..10} diff --git a/samples/akka-sample-cluster-kubernetes-java/README.md b/samples/akka-sample-cluster-kubernetes-java/README.md new file mode 100644 index 000000000..7e896ebc9 --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-java/README.md @@ -0,0 +1,58 @@ +# akka-sample-cluster-kubernetes-java +akka sample cluster with kubernetes discovery in scala + +This is an example SBT project showing how to create an Akka Cluster on +Kubernetes. + +## Kubernetes Instructions + +## Starting + +First, package the application and make it available locally as a docker image: + + mvn clean package docker:build + +Then `akka-cluster.yml` should be sufficient to deploy a 2-node Akka Cluster, after +creating a namespace for it: + + kubectl apply -f kubernetes/namespace.json + kubectl config set-context --current --namespace=appka-1 + kubectl apply -f kubernetes/akka-cluster.yml + +To check what you have done in Kubernetes so far, you can do: + + kubectl get deployments + kubectl get pods + kubectl get replicasets + kubectl cluster-info dump + kubectl logs appka-79c98cf745-abcdee # pod name + +Finally, create a service so that you can then test [http://127.0.0.1:8080](http://127.0.0.1:8080) +for 'hello world': + + kubectl expose deployment appka --type=LoadBalancer --name=appka-service + kubectl port-forward svc/appka-service 8080:8080 + +To wipe everything clean and start over, do: + + kubectl delete namespaces appka-1 + +## Running in a real Kubernetes cluster + +#### Publish to a registry the cluster can access e.g. Dockerhub with the kubakka user + +The app image must be in a registry the cluster can see. The build.sbt uses DockerHub by default. +Use `mvn -Ddocker.registry=$DOCKER_REPO_URL/$NAMESPACE` if your cluster can't access DockerHub. + +To push an image to docker hub run: + + mvn -am -pl bootstrap-demo-kubernetes-api package docker:push + +And remove the `imagePullPolicy: Never` from the deployments. Then you can use the same `kubectl` commands +as described in the [Starting](#starting) section. + +## How it works + +This example uses [Akka Cluster Bootstrap](https://doc.akka.io/docs/akka-management/current/bootstrap/index.html) +to initialize the cluster, using the [Kubernetes API discovery mechanism](https://doc.akka.io/docs/akka-management/current/discovery/index.html#discovery-method-kubernetes-api) +to find peer nodes. diff --git a/samples/akka-sample-cluster-kubernetes-java/kubernetes/akka-cluster.yml b/samples/akka-sample-cluster-kubernetes-java/kubernetes/akka-cluster.yml new file mode 100644 index 000000000..6e3995e31 --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-java/kubernetes/akka-cluster.yml @@ -0,0 +1,82 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: appka + name: appka + namespace: appka-1 +spec: + replicas: 3 + selector: + matchLabels: + app: appka + template: + metadata: + labels: + app: appka + actorSystemName: appka + spec: + containers: + - name: appka + image: akka-sample-cluster-kubernetes:latest + # remove for real clusters, useful for minikube + imagePullPolicy: Never + readinessProbe: + httpGet: + path: /ready + port: management + periodSeconds: 10 + failureThreshold: 3 + initialDelaySeconds: 10 + livenessProbe: + httpGet: + path: "/alive" + port: management + periodSeconds: 10 + failureThreshold: 5 + initialDelaySeconds: 20 + ports: + # akka remoting + - name: remoting + containerPort: 2552 + protocol: TCP + # akka-management and bootstrap + - name: management + containerPort: 8558 + protocol: TCP + - name: http + containerPort: 8080 + protocol: TCP + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: REQUIRED_CONTACT_POINT_NR + value: "2" + +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: pod-reader + namespace: appka-1 +rules: + - apiGroups: [""] # "" indicates the core API group + resources: ["pods"] + verbs: ["get", "watch", "list"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: read-pods + namespace: appka-1 +subjects: + # Note the `name` line below. The first default refers to the namespace. The second refers to the service account name. + # For instance, `name: system:serviceaccount:myns:default` would refer to the default service account in namespace `myns` + - kind: User + name: system:serviceaccount:appka-1:default +roleRef: + kind: Role + name: pod-reader + apiGroup: rbac.authorization.k8s.io diff --git a/samples/akka-sample-cluster-kubernetes-java/kubernetes/namespace.json b/samples/akka-sample-cluster-kubernetes-java/kubernetes/namespace.json new file mode 100644 index 000000000..466d18c92 --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-java/kubernetes/namespace.json @@ -0,0 +1,11 @@ +{ + "kind": "Namespace", + "apiVersion": "v1", + "metadata": { + "name": "appka-1", + "labels": { + "name": "appka-1" + } + } +} + diff --git a/samples/akka-sample-cluster-kubernetes-java/pom.xml b/samples/akka-sample-cluster-kubernetes-java/pom.xml new file mode 100644 index 000000000..8f1ff2be5 --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-java/pom.xml @@ -0,0 +1,152 @@ + + 4.0.0 + com.lightbend + akka-sample-cluster-kubernetes + 1.0 + akka-sample-cluster-kubernetes-java + Akka Sample for forming a Cluster in Kubernetes + + + 17 + 17 + UTF-8 + 2.9.2 + 1.5.1 + 10.6.2 + 2.13 + ${git.commit.time}-${git.commit.id.abbrev} + + + + + akka-repository + Akka library repository + https://repo.akka.io/maven + + + + + akka-repository + Akka library repository + https://repo.akka.io/maven + + + + + + com.typesafe.akka + akka-http_${scala.binary.version} + ${akka-http.version} + + + com.typesafe.akka + akka-http-spray-json_${scala.binary.version} + ${akka-http.version} + + + com.typesafe.akka + akka-cluster-typed_${scala.binary.version} + ${akka.version} + + + com.typesafe.akka + akka-cluster-sharding-typed_${scala.binary.version} + ${akka.version} + + + com.typesafe.akka + akka-slf4j_${scala.binary.version} + ${akka.version} + + + com.typesafe.akka + akka-stream-typed_${scala.binary.version} + ${akka.version} + + + com.typesafe.akka + akka-discovery_${scala.binary.version} + ${akka.version} + + + ch.qos.logback + logback-classic + 1.2.13 + + + com.lightbend.akka.management + akka-management-cluster-bootstrap_${scala.binary.version} + ${akka-management.version} + + + com.lightbend.akka.discovery + akka-discovery-kubernetes-api_${scala.binary.version} + ${akka-management.version} + + + com.lightbend.akka.management + akka-management-cluster-http_${scala.binary.version} + ${akka-management.version} + + + + + + io.fabric8 + docker-maven-plugin + 0.42.0 + + + + %a + + docker.io/library/eclipse-temurin:17.0.8.1_1-jre + + ${version.number} + + + + java + -cp + /maven/* + akka.cluster.bootstrap.demo.DemoApp + + + + artifact-with-dependencies + + + + + + + + build-docker-image + package + + build + + + + + + pl.project13.maven + git-commit-id-plugin + 4.0.0 + + + validate + + revision + + + + + yyyyMMdd-HHmmss + ${project.basedir}/.git + false + + + + + diff --git a/samples/akka-sample-cluster-kubernetes-java/src/main/java/akka/cluster/bootstrap/demo/DemoApp.java b/samples/akka-sample-cluster-kubernetes-java/src/main/java/akka/cluster/bootstrap/demo/DemoApp.java new file mode 100644 index 000000000..272c480bc --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-java/src/main/java/akka/cluster/bootstrap/demo/DemoApp.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017 Lightbend Inc. + */ +package akka.cluster.bootstrap.demo; + +import akka.actor.typed.ActorRef; +import akka.actor.typed.ActorSystem; +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.Adapter; +import akka.actor.typed.javadsl.Behaviors; +import akka.cluster.ClusterEvent; +import akka.cluster.typed.Cluster; +import akka.cluster.typed.Subscribe; +import akka.http.javadsl.ConnectHttp; +import akka.http.javadsl.Http; +import static akka.http.javadsl.server.Directives.*; +import akka.management.cluster.bootstrap.ClusterBootstrap; +import akka.management.scaladsl.AkkaManagement; +import akka.stream.Materializer; + +public class DemoApp { + + static class MemberEventLogger { + public static Behavior create() { + return Behaviors.setup(context -> { + Cluster cluster = Cluster.get(context.getSystem()); + + context.getLog().info("Started [{}], cluster.selfAddress = {})", + context.getSystem(), + cluster.selfMember().address()); + + cluster.subscriptions().tell(new Subscribe<>(context.getSelf(), ClusterEvent.MemberEvent.class)); + + return Behaviors.receiveMessage(event -> { + context.getLog().info("MemberEvent: {}", event); + return Behaviors.same(); + }); + }); + } + } + + static class Guardian { + public static Behavior create() { + return Behaviors.setup(context -> { + final akka.actor.ActorSystem classicSystem = Adapter.toClassic(context.getSystem()); + Materializer mat = Materializer.matFromSystem(classicSystem); + + Http.get(classicSystem).bindAndHandle(complete("Hello world") + .flow(classicSystem, mat), ConnectHttp.toHost("0.0.0.0", 8080), mat) + .whenComplete((binding, failure) -> { + if (failure == null) { + classicSystem.log().info("HTTP server now listening at port 8080"); + } else { + classicSystem.log().error(failure, "Failed to bind HTTP server, terminating."); + classicSystem.terminate(); + } + }); + + context.spawn(MemberEventLogger.create(), "listener"); + + AkkaManagement.get(classicSystem).start(); + ClusterBootstrap.get(classicSystem).start(); + + return Behaviors.empty(); + }); + } + } + + public static void main(String[] args) { + ActorSystem.create(Guardian.create(), "Appka"); + } + +} \ No newline at end of file diff --git a/samples/akka-sample-cluster-kubernetes-java/src/main/java/akka/cluster/bootstrap/demo/DemoHealthCheck.java b/samples/akka-sample-cluster-kubernetes-java/src/main/java/akka/cluster/bootstrap/demo/DemoHealthCheck.java new file mode 100644 index 000000000..b42430d92 --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-java/src/main/java/akka/cluster/bootstrap/demo/DemoHealthCheck.java @@ -0,0 +1,22 @@ +package akka.cluster.bootstrap.demo; + +import akka.actor.ActorSystem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Supplier; + +public class DemoHealthCheck implements Supplier> { + private final Logger log = LoggerFactory.getLogger(getClass()); + + public DemoHealthCheck(ActorSystem system) { + } + + @Override + public CompletionStage get() { + log.info("DemoHealthCheck called"); + return CompletableFuture.completedFuture(true); + } +} diff --git a/samples/akka-sample-cluster-kubernetes-java/src/main/resources/application.conf b/samples/akka-sample-cluster-kubernetes-java/src/main/resources/application.conf new file mode 100644 index 000000000..8ab258e39 --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-java/src/main/resources/application.conf @@ -0,0 +1,36 @@ +akka { + loglevel = "DEBUG" + actor.provider = cluster + + coordinated-shutdown.exit-jvm = on + + cluster { + shutdown-after-unsuccessful-join-seed-nodes = 60s + downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider" + } +} + +#management-config +akka.management { + cluster.bootstrap { + contact-point-discovery { + # For the kubernetes API this value is substributed into the %s in pod-label-selector + service-name = "appka" + + # pick the discovery method you'd like to use: + discovery-method = kubernetes-api + + required-contact-point-nr = 2 + required-contact-point-nr = ${?REQUIRED_CONTACT_POINT_NR} + } + } +} +#management-config + +akka.management { + health-checks { + readiness-checks { + example-ready = "akka.cluster.bootstrap.demo.DemoHealthCheck" + } + } +} diff --git a/samples/akka-sample-cluster-kubernetes-java/src/main/resources/logback.xml b/samples/akka-sample-cluster-kubernetes-java/src/main/resources/logback.xml new file mode 100644 index 000000000..058a3ff5e --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-java/src/main/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + System.out + + [%date{ISO8601}] [%level] [%logger] [%marker] [%thread] - %msg MDC: {%mdc}%n + + + + + 8192 + true + + + + + + + + diff --git a/samples/akka-sample-cluster-kubernetes-scala/README.md b/samples/akka-sample-cluster-kubernetes-scala/README.md new file mode 100644 index 000000000..4df9ea723 --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-scala/README.md @@ -0,0 +1,60 @@ +# akka-sample-cluster-kubernetes-scala +akka sample cluster with kubernetes discovery in scala + +This is an example SBT project showing how to create an Akka Cluster on +Kubernetes. + +## Kubernetes Instructions + +## Starting + +First, package the application and make it available locally as a docker image: + + sbt Docker/publishLocal + +Then `akka-cluster.yml` should be sufficient to deploy a 2-node Akka Cluster, after +creating a namespace for it: + + kubectl apply -f kubernetes/namespace.json + kubectl config set-context --current --namespace=appka-1 + kubectl apply -f kubernetes/akka-cluster.yml + +To check what you have done in Kubernetes so far, you can do: + + kubectl get deployments + kubectl get pods + kubectl get replicasets + kubectl cluster-info dump + kubectl logs appka-79c98cf745-abcdee # pod name + +Finally, create a service so that you can then test [http://127.0.0.1:8080](http://127.0.0.1:8080) +for 'hello world': + + kubectl expose deployment appka --type=LoadBalancer --name=appka-service + kubectl port-forward svc/appka-service 8080:8080 + +To wipe everything clean and start over, do: + + kubectl delete namespaces appka-1 + +## Running in a real Kubernetes cluster + +#### Publish to a registry the cluster can access e.g. Dockerhub with the kubakka user + +The app image must be in a registry the cluster can see. The build.sbt uses DockerHub by default. +Start with `sbt -Ddocker.registry=your-registry` if your cluster can't access DockerHub. + +The user for the registry is defined with `sbt -Ddocker.username=your-user` + +To push an image to docker hub run: + + `sbt -Ddocker.username=your-user docker:publish` + +And remove the `imagePullPolicy: Never` from the deployments. Then you can use the same `kubectl` commands +as described in the [Starting](#starting) section. + +## How it works + +This example uses [Akka Cluster Bootstrap](https://doc.akka.io/docs/akka-management/current/bootstrap/index.html) +to initialize the cluster, using the [Kubernetes API discovery mechanism](https://doc.akka.io/docs/akka-management/current/discovery/index.html#discovery-method-kubernetes-api) +to find peer nodes. diff --git a/samples/akka-sample-cluster-kubernetes-scala/build.sbt b/samples/akka-sample-cluster-kubernetes-scala/build.sbt new file mode 100644 index 000000000..a7e9c24ac --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-scala/build.sbt @@ -0,0 +1,46 @@ +ThisBuild / organization := "com.lightbend" + +name := "akka-sample-cluster-kubernetes" + +scalaVersion := "2.13.13" +lazy val akkaHttpVersion = "10.6.2" +lazy val akkaVersion = "2.9.2" +lazy val akkaManagementVersion = "1.5.1" + +// make version compatible with docker for publishing +ThisBuild / dynverSeparator := "-" + +scalacOptions := Seq("-feature", "-unchecked", "-deprecation", "-encoding", "utf8") +classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.AllLibraryJars +run / fork := true +Compile / run / fork := true +Compile / run / mainClass := Some("akka.sample.cluster.kubernetes.DemoApp") + +enablePlugins(JavaServerAppPackaging, DockerPlugin) + +dockerExposedPorts := Seq(8080, 8558, 25520) +dockerUpdateLatest := true +dockerUsername := sys.props.get("docker.username") +dockerRepository := sys.props.get("docker.registry") +dockerBaseImage := "adoptopenjdk:11-jre-hotspot" + +resolvers += "Akka library repository".at("https://repo.akka.io/maven") + +libraryDependencies ++= { + Seq( + "com.typesafe.akka" %% "akka-http" % akkaHttpVersion, + "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion, + "com.typesafe.akka" %% "akka-cluster-typed" % akkaVersion, + "com.typesafe.akka" %% "akka-cluster-sharding-typed" % akkaVersion, + "com.typesafe.akka" %% "akka-stream-typed" % akkaVersion, + "com.typesafe.akka" %% "akka-discovery" % akkaVersion, + "ch.qos.logback" % "logback-classic" % "1.2.13", + "com.lightbend.akka.discovery" %% "akka-discovery-kubernetes-api" % akkaManagementVersion, + "com.lightbend.akka.management" %% "akka-management-cluster-bootstrap" % akkaManagementVersion, + "com.lightbend.akka.management" %% "akka-management-cluster-http" % akkaManagementVersion, + "com.typesafe.akka" %% "akka-testkit" % akkaVersion % "test", + "com.typesafe.akka" %% "akka-actor-testkit-typed" % akkaVersion % Test, + "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpVersion % Test, + "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test, + "com.typesafe.akka" %% "akka-stream-testkit" % akkaVersion % Test) +} diff --git a/samples/akka-sample-cluster-kubernetes-scala/kubernetes/akka-cluster.yml b/samples/akka-sample-cluster-kubernetes-scala/kubernetes/akka-cluster.yml new file mode 100644 index 000000000..703777919 --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-scala/kubernetes/akka-cluster.yml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: appka + name: appka + namespace: appka-1 +spec: + replicas: 3 + selector: + matchLabels: + app: appka + template: + metadata: + labels: + app: appka + actorSystemName: appka + spec: + containers: + - name: appka + image: akka-sample-cluster-kubernetes:latest + # remove for real clusters, useful for minikube + imagePullPolicy: Never + readinessProbe: + httpGet: + path: /ready + port: management + periodSeconds: 10 + failureThreshold: 3 + initialDelaySeconds: 10 + livenessProbe: + httpGet: + path: "/alive" + port: management + periodSeconds: 10 + failureThreshold: 5 + initialDelaySeconds: 20 + ports: + # akka-management and bootstrap + - name: management + containerPort: 8558 + protocol: TCP + - name: http + containerPort: 8080 + protocol: TCP + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: REQUIRED_CONTACT_POINT_NR + value: "3" + +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: pod-reader + namespace: appka-1 +rules: +- apiGroups: [""] # "" indicates the core API group + resources: ["pods"] + verbs: ["get", "watch", "list"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: read-pods + namespace: appka-1 +subjects: +# Note the `name` line below. The first default refers to the namespace. The second refers to the service account name. +# For instance, `name: system:serviceaccount:myns:default` would refer to the default service account in namespace `myns` +- kind: User + name: system:serviceaccount:appka-1:default +roleRef: + kind: Role + name: pod-reader + apiGroup: rbac.authorization.k8s.io diff --git a/samples/akka-sample-cluster-kubernetes-scala/kubernetes/namespace.json b/samples/akka-sample-cluster-kubernetes-scala/kubernetes/namespace.json new file mode 100644 index 000000000..466d18c92 --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-scala/kubernetes/namespace.json @@ -0,0 +1,11 @@ +{ + "kind": "Namespace", + "apiVersion": "v1", + "metadata": { + "name": "appka-1", + "labels": { + "name": "appka-1" + } + } +} + diff --git a/samples/akka-sample-cluster-kubernetes-scala/project/build.properties b/samples/akka-sample-cluster-kubernetes-scala/project/build.properties new file mode 100644 index 000000000..abbbce5da --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-scala/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.9.8 diff --git a/samples/akka-sample-cluster-kubernetes-scala/project/plugins.sbt b/samples/akka-sample-cluster-kubernetes-scala/project/plugins.sbt new file mode 100644 index 000000000..8bab11d60 --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-scala/project/plugins.sbt @@ -0,0 +1,4 @@ +resolvers += "Akka library repository".at("https://repo.akka.io/maven") + +addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16") +addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.0.1") diff --git a/samples/akka-sample-cluster-kubernetes-scala/src/main/resources/application.conf b/samples/akka-sample-cluster-kubernetes-scala/src/main/resources/application.conf new file mode 100644 index 000000000..3c8624381 --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-scala/src/main/resources/application.conf @@ -0,0 +1,31 @@ +akka { + loglevel = "DEBUG" + actor.provider = cluster + + coordinated-shutdown.exit-jvm = on + + cluster { + shutdown-after-unsuccessful-join-seed-nodes = 60s + } +} + +#management-config +akka.management { + cluster.bootstrap { + contact-point-discovery { + # pick the discovery method you'd like to use: + discovery-method = kubernetes-api + + required-contact-point-nr = ${REQUIRED_CONTACT_POINT_NR} + } + } +} +#management-config + +akka.management { + health-checks { + readiness-checks { + example-ready = "akka.sample.cluster.kubernetes.DemoHealthCheck" + } + } +} diff --git a/samples/akka-sample-cluster-kubernetes-scala/src/main/resources/logback.xml b/samples/akka-sample-cluster-kubernetes-scala/src/main/resources/logback.xml new file mode 100644 index 000000000..058a3ff5e --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-scala/src/main/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + System.out + + [%date{ISO8601}] [%level] [%logger] [%marker] [%thread] - %msg MDC: {%mdc}%n + + + + + 8192 + true + + + + + + + + diff --git a/samples/akka-sample-cluster-kubernetes-scala/src/main/scala/akka/sample/cluster/kubernetes/DemoApp.scala b/samples/akka-sample-cluster-kubernetes-scala/src/main/scala/akka/sample/cluster/kubernetes/DemoApp.scala new file mode 100644 index 000000000..8ce065843 --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-scala/src/main/scala/akka/sample/cluster/kubernetes/DemoApp.scala @@ -0,0 +1,37 @@ +package akka.sample.cluster.kubernetes + +import akka.actor.typed.ActorSystem +import akka.actor.typed.scaladsl.Behaviors +import akka.cluster.ClusterEvent +import akka.cluster.typed.{ Cluster, Subscribe } +import akka.http.scaladsl.Http +import akka.http.scaladsl.server.Directives._ +import akka.management.cluster.bootstrap.ClusterBootstrap +import akka.management.javadsl.AkkaManagement +import akka.{ actor => classic } + +object DemoApp extends App { + + ActorSystem[Nothing](Behaviors.setup[Nothing] { context => + import akka.actor.typed.scaladsl.adapter._ + implicit val classicSystem: classic.ActorSystem = context.system.toClassic + implicit val ec = context.system.executionContext + + val cluster = Cluster(context.system) + context.log.info("Started [" + context.system + "], cluster.selfAddress = " + cluster.selfMember.address + ")") + + Http().newServerAt("0.0.0.0", 8080).bind(complete("Hello world")) + + // Create an actor that handles cluster domain events + val listener = context.spawn(Behaviors.receive[ClusterEvent.MemberEvent]((ctx, event) => { + ctx.log.info("MemberEvent: {}", event) + Behaviors.same + }), "listener") + + Cluster(context.system).subscriptions ! Subscribe(listener, classOf[ClusterEvent.MemberEvent]) + + AkkaManagement.get(classicSystem).start() + ClusterBootstrap.get(classicSystem).start() + Behaviors.empty + }, "appka") +} diff --git a/samples/akka-sample-cluster-kubernetes-scala/src/main/scala/akka/sample/cluster/kubernetes/DemoHealthCheck.scala b/samples/akka-sample-cluster-kubernetes-scala/src/main/scala/akka/sample/cluster/kubernetes/DemoHealthCheck.scala new file mode 100644 index 000000000..f29de3e67 --- /dev/null +++ b/samples/akka-sample-cluster-kubernetes-scala/src/main/scala/akka/sample/cluster/kubernetes/DemoHealthCheck.scala @@ -0,0 +1,16 @@ +package akka.sample.cluster.kubernetes + +import scala.concurrent.Future + +import akka.actor.ActorSystem +import org.slf4j.LoggerFactory + +// Enabled in application.conf +class DemoHealthCheck(system: ActorSystem) extends (() => Future[Boolean]) { + private val log = LoggerFactory.getLogger(getClass) + + override def apply(): Future[Boolean] = { + log.info("DemoHealthCheck called") + Future.successful(true) + } +} diff --git a/scripts/prepare-downloads.sh b/scripts/prepare-downloads.sh new file mode 100755 index 000000000..6b59a0707 --- /dev/null +++ b/scripts/prepare-downloads.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +set -e + +declare -r samples_sources="${PWD}/samples" +declare -r docs_attachments="${PWD}/docs/src/main/paradox/attachments" +declare -r target_temporal_attachments="${PWD}/target/docs/_attachments" + +declare -r temporal_folder="${PWD}/target/zips" + +function sed_command() { + local platform="$(uname -s | tr '[:upper:]' '[:lower:]')" + + if [ "${platform}" != "darwin" ]; then + echo "sed" + else + # using gnu-sed on Mac + echo "gsed" + fi +} + +## Remove the tags used by Paradox snippets from the codebase in the current folder +function removeTags() { + ## remove tags from code + find . -type f -print0 | xargs -0 $(sed_command) -i "s/\/\/ #.*//g" +} + + +## Cleanup the temporal folder from previous executions +function prepareTemporalFolder() { + rm -rf ${temporal_folder} + mkdir -p ${temporal_folder} +} + +## Copy a folder with some code into the temporal folder. The +## copied folder will be renamed to the folder name we want the +## user to see when unzipping the file. +## source_name -> folder in `examples` +## target_name -> folder name the user should see (must not use a numeric prefix of a laguage suffix) +function fetchProject() { + source_name=$1 + target_name=$2 + echo "Fetching content from [$1] to [$2]" + cp -a ${source_name} ${temporal_folder}/${target_name} + rm -rf ${temporal_folder}/${target_name}/target +} + +## Zip the contents in $temporal_folder and create the +## attachment file (aka, the zip file on the appropriate location) +function zipAndAttach() { + zip_name=$1 + temporal_attachments=$2 + echo "Preparing zip $1" + pushd ${temporal_folder} + removeTags + zip --quiet -r ${zip_name} * + cp ${zip_name} ${temporal_attachments} + echo "Prepared attachment at ${zip_name}" + popd +} + +mkdir -p ${docs_attachments} +mkdir -p ${target_temporal_attachments} + +## akka-sample-cluster-kubernetes-java zip file +prepareTemporalFolder +fetchProject ${samples_sources}/akka-sample-cluster-kubernetes-java akka-sample-cluster-kubernetes +zipAndAttach ${docs_attachments}/akka-sample-cluster-kubernetes-java.zip ${target_temporal_attachments} + +## akka-sample-cluster-kubernetes-scala zip file +prepareTemporalFolder +fetchProject ${samples_sources}/akka-sample-cluster-kubernetes-scala akka-sample-cluster-kubernetes +zipAndAttach ${docs_attachments}/akka-sample-cluster-kubernetes-scala.zip ${target_temporal_attachments}