Creating a Pokéball Factory with Microservices
Now that we know a bit more about microservices, let’s illustrate how to write some and have them talk to each other! We’ll task ourselves with creating a Pokéball factory that help us order Pokéballs for inspired trainers. The microservices will be composed of a service written in Python that can take in requests and transmit orders to another service in Go that displays orders to workers on the factory floor.
This tutorial was inspired and takes much code from Usama Ashraf’s post, with a few changes. Be sure to have Docker installed, then git clone this repo and buckle down! We’ll be taking a whirlwind through a few different technologies. Otherwise, see here for the full code.
Setting up Docker Compose
To set up Compose, which allows us to run multi-container Docker applications, we’ll need to specify our application’s services in a YAML file in the root directory. We want to use four services, namely: RabbitMQ, Python, Go, and Redis. So, we’ll have to specify each one in the ‘services’ field. The ‘volumes’ field allows us to store code for the Python and Go files. Open up the file “docker-compose.yml” and you should see the following:
# ‘rabbitmq-server’ will be available as a network reference inside this service
# and this service will start only after the RabbitMQ service has.
# Keep it running.
# Map port 3000 on the host machine to port 3000 of the container.
image: redis# Host volumes used to store code.
A few things to note here:
- The Python and RabbitMQ services have associated ports. The port for the Python Flask application is so that we can send POST requests from the host machine. The port, as well as the username and password both defined as ‘guest’, will be used by Go to access the service.
- The Python and Go services have tty (short for ‘Teletypewriter’) set to true. This allows us to use bash on the container and run the Flask and Go files. We also create volumes for each of these services to store the associated files.
Great! Now Docker will know how to start all of our services from a single command.
The RabbitMQ Service
We have downloaded the image from docker-library (with a few changes by Usama) and Docker simply needs to build it. Not much to do here.
Now we’ll just need to define the services that’ll use the messaging service!
The Python Service
Let’s create the Flask application first. Open up “main.py” and we’ll go through it together. See that we’ve created a connection with Redis? This will allow us to save information about products in a key-value table.
We’ll need to fill in the ‘create’ route and its associated function, which allows us to send a post request to it with the fields: ‘name’ and ‘price’, which will be saved in Redis with the former as the key and the latter as the value. We can then return a 201 success JSON to indicate that the pair has been saved. Your code should look like this:
Next, we’ll have to add a few lines to the “Dockerfile” so Docker will know to install some of our dependencies (namely, pika, Flask, and Redis):
Great! Now we can declare Pokéballs of various types.
Now we just need to send orders to the factory floor with our ‘buy’ action in “main.py”. Create the ‘buy’ route and associated function as shown below. It should be able to take in a request specifying the name of a previously created product and send that to the Go application with RabbitMQ (done through the emit_product_order() function). In the case of a successful order, it will send a 200 status code with a message stating its price.
Finally, we’ll need to define how the Flask application will send messages through the RabbitMQ broker. Go into the “services” folder and find “product_event_handler.py” and we’ll go through it’s logic. First, we create a connection with the service and a channel within it. Connections are made through TCP and channels piggy-back them so as not to create multiple expensive TCP connections.
Next, we’ll need to create an exchange within this channel. While there are multiple types to choose from, we’ll be using the ‘topic’ exchange for this project. This allows services to subscribe to messages sent to that exchange by other services. While RabbitMQ creates a nameless exchange by default, we’ll create a named one to make its usage more explicit. Then, we’ll create something called a “routing key”. Services subscribed to this exchange can receive message with patterns that match the routing key. Under our declaration of the channel, we’ll create the exchange name “product_order” with the routing_key “product.order.update”.
In the line where we declare our exchange within the channel, we want to set the exchange_type to “topic”.
We’re done with the Python application! Now, we can send our JSON with the variable name “new_data” that contains simply the name of the product over to our Go application through the “product_order” channel. We just need to set up the Go service to receive it!
The Go Service
Much of the Go service has already been written. We’ll just need to change a few lines before we can run the whole application!
First, we’ll want to connect Go to the RabbitMQ service using Go’s client package called amqp. In the Dial function, we want to pass the following string like so:
This corresponds with information found in the “docker-compose.yml” file.
Then, we’ll need to make our service listen to the exchange “product_order”, and receive any messages related to product orders. Let’s use the pattern “product.order.*”. We use ‘*’ as a wildcard so that the routing key “product.order.update” will be picked up. In the future, we may want to use a routing key called “product.order.cancel” to cancel product requests, and the wildcard will still pick it up:
Now, we need to state what we want to do with any incoming message. Let’s just print the JSON out to the shell. Add the following “printf” call to near the bottom of the file:
Great! Now we’re done with all the coding. Let’s run our factory!
Demoing our Factory
To see our factory in action, let’s follow the following steps:
- Be sure that Docker is running
- Open a new shell and run:
- Open a new shell and wait for the services to run. Then run the following command to find the container IDs in the first column. You will see an output like the one below
- Open two new shells for the Python and Go services and run the following command into each (enter in the relevant container ID from above)
docker exec -it <python or go container ID> bash
- In the Python container, run
FLASK_APP=main.py python -m flask run -p 3000 -h 0.0.0.0
- In the Go container, run
go run main.go
- In a new shell, let’s create a new product with a cURL request
curl -d ‘name=pokeball&price=200’ -X POST http://localhost:3000/create
- Then, we can request for the product with another request
curl -d ‘name=pokeball’ -X POST http://localhost:3000/buy
- You should now see the following transmissions:
Well done!!! We now have a small but promising Pokéball in the works. Real applications of microservices will surely be more complex than the one we’ve built, but we now see how a message broker like RabbitMQ can form the backbone of any application. Much effort needs to be put into the design of exchanges. It’s up to you to see which one suits your needs!