An introduction to GraalVM (with examples!)
Door Frits Berger / jun 2019 / 1 Min
Door Geert Liet / / 3 min
In my previous blogpost I talked about creating robust services. Because of an inhouse request for an application I got the chance to practice some of my preaching for our company instead of a customer. Keep in mind this blog is not about the best way to solve a problem but just a description of how I globally handled our problem.
For inhouse purposes we wanted to connect two applications which we use extensively. For one application an add-on will be created. Creating this add-on is basically the same as starting something from scratch only with a lifecycle which is enforced externally. The other application will be used as a web-service only.
When I get the chance to greenfield with a new application my intention is to learn something new. Since I really like Go and not being fully adept at the time, the language choice was easy. To make my life a little less complicated I also used Docker. Consul helps in keeping track of my services and registrator makes sure my services are registered in consul.
I created my Go Docker images with the smallest image I could find and with compiling the Go binary beforehand for Linux, so no Go installation is needed on the image. The command I used for compilation is :
GOOS=linux go build -ldflags '-w' .
Which cross compiles on my Mac to be used in my Docker image. The variable GOOS sets the target OS, in this case to Linux. Ldflags strips the binary of debugging symbols, which you might not want to do if you think debugging is needed ^^. It saves around 2MB of file size, which seems quite a small benefit considering the whole.
The simple Docker file used to created the image is as simple as this:
FROM scratchMAINTAINER somebodyADD build/out/application-go-client-linux-amd64 application-go-clientENTRYPOINT ["/application-go-client"]
It is based on the image scratch which is a very lean image which is very well suited for single binary constructions. There is a little problem when connecting to HTTPS Addresses, since the image doesn't contain any certificates you will have to add them manually. In my case I added them directly to the HTTP Client. Within Go I defined the certs in a separate file, I added them to the TLSClientConfig.
client = http.Client{Transport: http.Transport{TLSClientConfig: tls.Config{RootCAs: pool}}}
An other option would be copying the certs to the image.
Consider the following setup:
In this picture the starting point application is A. Application A is a cloud service which can be used by multiple customers. This application has a user interface in which a new customer can be configured. This configuration triggers a service call to the client service A. The Client service for application A is a Go Docker image as described above with the following compose file-part:
application-client: image: application-client-a restart: always ports: - "8081:8080" environment: - applicationConfigurationDirectory=/opt/config - apiPort=8080 - contextRoot=/application - SERVICE_NAME=applicationclientA - SERVICE_TAGS=applicationclientA volumes: - $PWD/application-go-client/config:/opt/config:ro networks: - client depends_on: - registrator
This client service creates an additional service for every registered customer, creating a dedicated communication line for every customer from application A to application B. Every call intended from the multitenant application A is in its turn routed through its own client service creating isolation and scalability.
There are a couple of challenges when using this approach. Application A does not know about the client services for application B. In order to be able to send a message to one of these services it needs to know where these services are running, in this case it would be nice to know on which port and IP the services run and which URL paths are available. Consul helps me here with the possibility to register these services.
The client service B is created dynamically based on the same concept of the Go Docker image as described earlier. All specifics are past on through environment variables, like Customer ID. The creation is done by the Go Docker API using a separate service. The port is assigned dynamically by Docker.
The next thing I needed was the registration of all services in a system. This system should store all parameters I need to be able to find them again based on keywords. These keywords need to be known in the application requesting the service, in this case application A, so the user configuration details will be used as part of these keywords.
Consul advices for productions systems to have at least three nodes on which you run Consul. In my case to simplify things I used only one node which is an existing Docker image, gliderlabs/consul-server. This image has some pre-configuration done to quickstart this project. I had it configured in a Docker-compose file as follows:
consul: image: gliderlabs/consul-server:latest container_name: consul hostname: consul networks: - moreclient ports: - "8400:8400" - "8500:8500" - "53:53/udp" - "8301-8302/udp" - "8300-8300/tcp" command: "-server -bootstrap"
The most important port is 8500 on which registration and retrieval can be done. The starting commands ensure the server starts in bootstrap mode. The rest of the compose configuration is mostly naming done for convenience.
However this only solves a part of my problem. I could register the created services every time they are created. But who will take care of the de-registration and do I have to do this for every type of service? It would be great if there was a service which knows what is being created and destroyed. This is where Registrator comes into play. Chosen mostly because it's easy to use and works seamlessly with Consul.
The port on which the dynamically created services will be running is known after creation. So there has to be so sort of dynamic discovery, or another manual port retrieval call to Docker. Registrator automatically registers and deregisters the services for me with Consul. To get the de-registration functionality working, I needed to open port 53 on the Consul Docker container as you can see in the code block. However binding port 53 can be a problem since it requires elevated privileges, to solve this you could forward the dns queries,
https://www.consul.io/docs/guides/forwarding.html. The SERVICE_NAME and SERVICE_TAGS environment variables are used for passing additional information to registrator which it can't figure out otherwise.
The Docker compose part is very simple and looks like this:
registrator: image: gliderlabs/registrator volumes: - /var/run/docker.sock:/tmp/docker.sock networks: - moreclient command: "-cleanup -resync 30 consul://consul:8500" depends_on: - consul
Registrator needs to know where Consul is running. There is the option to start clean and resync after a fixed interval.
I really enjoyed working with these tools; it was really easy to configure and run. Of course more configuration / programming is needed when the availability of the program has to level up but in our case it works well enough. The Golang Docker images are about 5 MB - 7MB in size which allows for very fast deployment and nice portability.
| Go
Door Geert Liet / jun 2023
Dan denken we dat dit ook wat voor jou is.