Cursive REPL output window. Just listing the buckets using my repl scratch file.
Introduction
At my new company Metosin I have been implementing AWS infrastructure using Pulumi and implementing applications using Clojure. We are deploying applications into a Kubernetes cluster and to verify that our applications run with the infrastructure in a dockerized environment as they should we are running tests locally using Drone pipeline (see my previous blog post about it: Using Drone CI ) which spins the required infrastructure using docker containers (Postgres and Minio) and the actual application as another docker container. This week I was wondering that one application behaved a bit differently running tests in my local workstation hitting containers using localhost and running tests inside a docker container. I was discussing this with one Metosin colleague and he told me how to add an nrepl server inside the application so that I can connect to the application running in a docker container and experiment with the running application there using my IntelliJ IDEA / Cursive REPL. Because this trick was pretty neat I thought that I write a short blog post about it. (By the way, I haven’t regretted a second joining Metosin - one of the best places to be if you want to learn Clojure with the best clojurians on the planet, see more: My First Weeks at Metosin!)
Setting Up Nrepl Server Inside a Clojure Application
Our Clojure applications use Integrant as a state management library and we have our configuration in an Aero configuration file. Setting up an nrepl server inside the application is pretty straightforward using these libraries. First the configuration file:
...
:backend/nrepl {:bind #profile {:prod nil
:local "localhost"
:docker "0.0.0.0"
}
:port 3177}
...
So, in prod don’t use nrepl, in local
profile when running application locally use localhost
and when running application inside a docker container use 0.0.0.0
address. Nrepl server listens to 3177
port.
Then in your Integrant configuration provide functions so that Integrant knows what to do when you want to init, halt, suspend and resume your state:
...
(defmethod ig/init-key :backend/nrepl [_ {:keys [bind port]}]
(if (and bind port)
(nrepl/start-server :bind bind :port port)
nil))
(defmethod ig/halt-key! :backend/nrepl [_ this]
(if this
(nrepl/stop-server this)))
(defmethod ig/suspend-key! :backend/nrepl [_ this]
this)
(defmethod ig/resume-key :backend/nrepl [_ _ _ old-impl]
old-impl)
...
So, in init
we start the nrepl server and in halt
, we stop it. In suspend
we store the nrepl instance and in resume
we use the old nrepl instance - i.e. when you keep hitting that Integrant reset
hot key (mine is Alt-J) to reset your state you don’t want to stop the nrepl server.
Docker Configuration
You can use e.g. Docker Compose when providing the docker configuration for your application. Example follows:
version: '3.7'
services:
arkisto_image:
image: local-arkisto:latest
ports:
- "3077:3077"
- "3177:3177"
networks:
[clojure-network]
container_name: arkisto_image
environment:
PROFILE: docker
DB_USERNAME: arkisto
DB_PASSWORD: arkisto
DB_HOST: clojuredb
DB_PORT: 5432
MINIO_HOST: clojures3
AWS_ACCESS_KEY_ID: minioadmin
AWS_SECRET_ACCESS_KEY: minioadmin
networks:
clojure-network:
name: clojure-network
The important trick here is to port forward the nrepl port 3177
from docker container to your host (your workstation) so that Cursive REPL can connect to it. (NOTE: This is just a test image configuration, I use another Dockerfile for building the real application image running in Kubernetes, of course - therefore using dummy passwords.)
If you wonder why we have the MINIO_HOST in this yaml file, let’s see the Aero configuration once more:
; Endpoint only used in dev - default value that of Minio.
:endpoint #profile {:prod nil
:dev {:protocol :http
:hostname #or [#env "MINIO_HOST" "localhost"]
:port 9000}}
… so, in production the endpoint configuration is nil (not needed, the Kube EKS uses AWS IAM to get access to the S3 buckets…), but in other environments, we simulate S3 using Minio and thus providing the endpoint to Minio server.
Connecting to the Nrepl Server From Your IDE
I use excellent Cursive REPL Clojure IDE as an example, but you can connect to the nrepl server pretty much same way using any Clojure IDE.
Cursive REPL configuration.
Then just click the IntelliJ IDEA “Start configuration” button and Cursive REPL connects immediately into your application’s nrepl port. Then you can do in the application code whatever you do also locally: reset state, compile namespaces, etc. The picture at the beginning of the blog post shows one REPL output from my repl scratch file while checking if I can connect to the Minio server running in another container in the same docker network.
Let’s try another trick here. Add something like this in your project’s deps.edn
or in ~/.clojure/deps.edn
:
:aliases {
:kari {:extra-paths ["scratch"]
:extra-deps {hashp {:mvn/version "0.1.1"}
...
This is the excellent Hashp debugging print tool.
And then in your clojure scratch file send the following form to repl:
(require '[hashp.core])
Then you can add #p
in front of any form you want to have debugging information. Let’s add some printouts and see if we are using either AWS Profile or access key / secret key in that docker environment:
(defn get-aws-credentials
"Get aws credentials. If AWS_PROFILE provided, use it. If AWS basic credentials, use them.
These are only for testing purposes. In real EKS environment do not provide any credentials
(returns nil) which means that pod uses role based authorization provided in the Pulumi
configuration."
[aws-profile aws-basic-credentials]
(let [{access-key-id :access-key-id
secret-access-key :secret-access-key} aws-basic-credentials]
(cond
aws-profile #p (credentials/profile-credentials-provider aws-profile)
(and access-key-id secret-access-key) #p (credentials/basic-credentials-provider aws-basic-credentials))))
Then send the namespace to repl and reset Integrant state (in my Cursive IDE I have the following hot keys: Alt-N: switch to namespace your cursor is, Alt-M: send namespace to repl, Alt-J: reset Integrant state - takes less than a second to click these three buttons). I see this in my Cursive REPL output windows:
Loading src/clj/arkisto/backend/main.clj... done
(integrant.repl/reset)
:reloading ()
#p[arkisto.backend.main/get-aws-credentials:39] (credentials/basic-credentials-provider aws-basic-credentials) => #<cognitect.aws.credentials$basic_credentials_provider$reify__23486@3c5cb0f5>
=> :resumed
… so, we are using access key / secret key (as you could see in the docker compose configuration file earlier).
How About If I Want to Connect to a Clojure Application inside Kubernetes Pod?
For connecting to a nrepl server running in an application running inside Kubernetes cluster I created a simple Justfile recipe so that my team members can easily connect to the applications:
# forward nrepl port from kube pod
@kube-nrepl +args='':
./infra/scripts/run-nrepl-port-forward.sh
Just recipe delegates task to the run-nrepl-port-forward.sh
which does something like this:
...
MYNS="XXX-${MYAPP}-master" # provide namespace
MYPODNAME=$(kubectl get pods -n $MYNS -o=jsonpath='{.items[0].metadata.name}') # parse pod name
echo "Creating port forward from $MYPODNAME nrepl port $REMOTE_PORT to local port $MYPORT..."
kubectl -n $MYNS port-forward $MYPODNAME $MYPORT:$REMOTE_PORT # forward port
… and when I run the Just recipe:
λ> just kube-nrepl arkisto 6677
Getting pod for arkisto...
Creating port forward from arkisto-master-897d9bd60c-6tm46 nrepl port 3177 to local port 6677...
Forwarding from 127.0.0.1:6677 -> 3177
Forwarding from [::1]:6677 -> 3177
… and then provide the same configuration in your Clojure IDE as provided earlier, but this time the local port is 6677
. You can run both repls at the same time and use repl to examine the differences, e.g.
; Sending form in repl connected to Docker...
(user/system)
=>
#:backend{:s3 #object[cognitect.aws.client.Client 0x432cd41f "cognitect.aws.client.Client@432cd41f"],
:bucket {:use-s3 nil,
:bucket "arkisto-mainsystem-bucket",
...
; Sending form in repl connected to Kubernetes...
(user/system)
=>
#:backend{:s3 #object[cognitect.aws.client.Client 0x37f83fa "cognitect.aws.client.Client@37f83fa"],
:bucket {:use-s3 true,
:bucket "my-very-secret-arkisto-bucket-xxxx", ; NOTE: bucket name changed in this blog!
...
So, in my docker test environment I’m not using real AWS S3 but Minio which provides exactly the same api interface so that the clojure code actually doesn’t know if we are using real AWS S3 service or a simulated environment (Minio). In test environment I use some bucket name for development purposes (creates a bucket inside Minio), in real life the application gets the bucket name from AWS Parameter Store. The use-s3
is just for the Integrant state to create the bucket in Minio if we are running in a test environment (in real AWS environment the bucket is already there, of course):
...
; NOTE: Pulumi manages infra creation (S3 bucket).
(when-not use-s3
(aws/invoke s3-client {:op :CreateBucket :request {:Bucket bucket}}))
{:use-s3 use-s3 :bucket bucket :s3-client s3-client :clean clean})
...
Conclusions
If you want to learn an excellent functional language which is ready for the cloud era look no further: Clojure.
The writer is working at Metosin using Clojure in cloud projects. If you are interested to start a Clojure project in Finland or you are interested to get Clojure training in Finland you can contact me by sending email to my Metosin email address or contact me via LinkedIn.
Kari Marttila
- Kari Marttila’s Home Page in LinkedIn: https://www.linkedin.com/in/karimarttila/
Top comments (0)