visit
In this tutorial, we’ll start by using to build, run, and test locally. We’ll also walk through how to use kubectl
to deploy our application to the cloud. Last, we’ll walk through how we can use to seamlessly deploy our application locally and to the cloud using the same configuration.
As mentioned previously, this part of the tutorial will contain multiple services running on your local machine. You can use docker-compose
to run them all at once and stop them all when you’re ready. Let’s get going!
const kafka = require('kafka-node');
const client = new kafka.KafkaClient({
kafkaHost:
process.env.ENVIRONMENT === 'local'
? process.env.INTERNAL_KAFKA_ADDR
: process.env.EXTERNAL_KAFKA_ADDR,
});
const Producer = kafka.Producer;
const producer = new Producer(client);
producer.on('ready', () => {
setInterval(() => {
const payloads = [
{
topic: process.env.TOPIC,
messages: [`${process.env.TOPIC}_message_${Date.now()}`],
},
];
producer.send(payloads, (err, data) => {
if (err) {
console.log(err);
}
console.log(data);
});
}, 5000);
});
producer.on('error', err => {
console.log(err);
});
Save and close the index. We’ll also need some supporting modules installed in our Docker container when it’s built. Also, in the “publisher” folder, create a package.json
with the JSON here:
{
"name": "publisher",
"version": "0.1.0",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"cors": "2.8.5",
"express": "^4.17.1",
"kafka-node": "^5.0.0",
"winston": "^3.2.1"
}
}
Save and close the package.json. Alongside the last two files, we’ll need a package-lock.json
, which can be created with the following command:
npm i --package-lock-only
FROM node:12-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
CMD [ "npm", "start" ]
node:12-alpine
as the base container image/usr/src/app
. Subsequent commands will be run in this folderpackage.json
and package-lock.json
that was just created into the /usr/src/app
directorynpm install
to install node modules/usr/src/app
. Importantly, this includes the index.js
npm start
in the container. npm
is already installed on the node:12-alpine
image, and the start
script is defined in the package.json
The subscriber service will be built very similarly to the publisher service and will consume messages from the Kafka topic. Messages will be consumed as frequently as they’re published, again, every five seconds in this case. To start, add a file titled index.js
to the “subscriber” folder and add the following code:
const kafka = require('kafka-node');
const client = new kafka.KafkaClient({
kafkaHost:
process.env.ENVIRONMENT === 'local'
? process.env.INTERNAL_KAFKA_ADDR
: process.env.EXTERNAL_KAFKA_ADDR,
});
const Consumer = kafka.Consumer;
const consumer = new Consumer(
client,
[
{
topic: process.env.TOPIC,
partition: 0,
},
],
{
autoCommit: false,
},
);
consumer.on('message', message => {
console.log(message);
});
consumer.on('error', err => {
console.log(err);
});
{
"name": "subscriber",
"version": "0.1.0",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"author": "Architect.io",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"cors": "2.8.5",
"express": "^4.17.1",
"kafka-node": "^5.0.0",
"winston": "^3.2.1"
}
}
npm i --package-lock-only
The subscriber needs one extra file that the publisher doesn’t, and that’s a file we’ll call wait-for-it.js
. Create a file and add the following:
const kafka = require('kafka-node');
const client = new kafka.KafkaClient({
kafkaHost:
process.env.ENVIRONMENT === 'local'
? process.env.INTERNAL_KAFKA_ADDR
: process.env.EXTERNAL_KAFKA_ADDR,
});
const Admin = kafka.Admin;
const child_process = require('child_process');
const admin = new Admin(client);
const interval_id = setInterval(() => {
admin.listTopics((err, res) => {
if (res[1].metadata[process.env.TOPIC]) {
console.log('Kafka topic created');
clearInterval(interval_id);
child_process.execSync('npm start', { stdio: 'inherit' });
} else {
console.log('Waiting for Kafka topic to be created');
}
});
}, 1000);
FROM node:12-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
CMD [ "node", "wait-for-it.js" ]
The subscriber’s Dockerfile is the same as the publisher’s, with the one difference noted above. The command that starts the container uses the wait-for-it.js
file rather than the index. Save and close the Dockerfile.
The docker-compose
file is where the publisher, subscriber, Kafka, and services will be tied together. Zookeeper is a service that is used to synchronize Kafka nodes within a cluster.
Zookeeper deserves a post all of its own, and because we only need one node in this tutorial I won’t be going in-depth on it here. At the root of the project alongside the “subscriber” and “publisher” folders, create a file called docker-compose.yml
and add this configuration:
version: '3'
services:
zookeeper:
ports:
- '50000:2181'
image: jplock/zookeeper
kafka:
ports:
- '50001:9092'
- '50002:9093'
depends_on:
- zookeeper
environment:
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
KAFKA_LISTENERS: 'INTERNAL://:9092'
KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://:9092'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'INTERNAL:PLAINTEXT'
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: '1'
KAFKA_CREATE_TOPICS: 'example-topic:1:1'
KAFKA_ADVERTISED_HOST_NAME: host.docker.internal # change to 172.17.0.1 if running on Ubuntu
image: 'wurstmeister/kafka:2.12-2.4.0'
volumes:
- '/var/run/docker.sock:/var/run/docker.sock'
publisher:
depends_on:
- kafka
environment:
TOPIC: example-topic
ENVIRONMENT: local
INTERNAL_KAFKA_ADDR: 'kafka:9092'
build:
context: ./publisher
subscriber:
depends_on:
- kafka
environment:
TOPIC: example-topic
ENVIRONMENT: local
INTERNAL_KAFKA_ADDR: 'kafka:9092'
build:
context: ./subscriber
volumes: {}
Note that the services
block of the docker-compose
contains four keys under which we define specific properties for each service. Below is a service-by-service walkthrough of what each property and its sub-properties are used for.
The ports
property instructs Zookeeper to expose itself to Kafka on port 2181 inside the Docker network. Zookeeper is also available to the host machine on port 50000.
The image
property instructs the Docker daemon to pull the latest version of the image .
The kafka
service block includes a configuration that will be passed to Kafka running inside the container, among other properties that will enable communication between the Kafka service and other containers.
ports
– Kafka exposes itself on two ports internal to the Docker network, 9092 and 9093. It is also exposed to the host machine on ports 50001 and 50002.depends_on
– Kafka depends on Zookeeper to run, so its key is included in the depends_on
block to ensure that Docker will start Zookeeper before Kafka.environment
– Kafka will pick up the environment variables in this block once the container starts. All configuration options except for KAFKA_CREATE_TOPICS
will be added to a Kafka broker config and applied to the startup. The variable KAFKA_CREATE_TOPICS
is used by the Docker image itself, not Kafka, to make working with Kafka easier. Topics defined by this variable will be created when Kafka starts without any external instructions.image
– This field instructs the Docker daemon to pull version 2.12-2.4.0 of the image wurstmeister/kafka
.volumes
– This is a requirement by the Docker image to use the Docker CLI when starting Kafka locally.
Most configuration in the publisher
block specifies how the publisher should communicate with Kafka.
Note that the depends_on
property ensures that the publisher will start after Kafka.
depends_on
– The publisher service naturally depends on Kafka, so it’s included in the dependency array.environment
– These variables are used by the code in the index.js
of the publisher.TOPIC
– This is the topic that messages will be published to. Note that it matches the topic that will be created by the Kafka container.ENVIRONMENT
– This environment variable determines inside the index file on what port the service will communicate with Kafka. The ternary statement that it is used in exists to use the same code for both local and remote deployments.INTERNAL_KAFKA_ADDR
– The publisher will connect to Kafka on this host and port.build
– The context inside tells the Docker daemon where to locate the Dockerfile that describes how the service will be built and run, along with supporting code and other files that will be used inside of the container.
Most of the docker-compose
configuration for the subscriber service is identical to that of the publisher service. The one difference is that the context tells the Docker daemon to build from the “subscriber” directory, where its Dockerfile and supporting files were created.
docker-compose up
publisher_1 | { 'example-topic': { '0': 0 } }
subscriber_1 | Kafka topic created
subscriber_1 |
subscriber_1 | > @architect-examples/[email protected] start /usr/src/app
subscriber_1 | > node index.js
subscriber_1 |
subscriber_1 | {
subscriber_1 | topic: 'example-topic',
subscriber_1 | value: 'example-topic_message_80',
subscriber_1 | offset: 0,
subscriber_1 | partition: 0,
subscriber_1 | highWaterOffset: 1,
subscriber_1 | key: null
subscriber_1 | }
subscriber_1 | {
subscriber_1 | topic: 'example-topic',
subscriber_1 | value: 'example-topic_message_83',
subscriber_1 | offset: 1,
subscriber_1 | partition: 0,
subscriber_1 | highWaterOffset: 2,
subscriber_1 | key: null
subscriber_1 | }
publisher_1 | { 'example-topic': { '0': 1 } }
New messages will continue to be published and consumed until the docker-compose
process is stopped by pressing ctrl/cmd+C
in the same terminal that it was started in.
docker login
Enter your Docker Hub username (not email) and password when prompted. You should see the message Login Succeeded
, which indicates that you’ve successfully logged in to Docker Hub in the terminal. The next step is to push the images that will need to be used in the Kubernetes cluster.
From the root of the project, navigate to the publisher
directory and build and tag the publisher service with the following command:
docker build . -t <your_docker_hub_username>/publisher:latest
Your local machine now has a Docker image tagged as <your_docker_hub_username>/publisher:latest
, which can be pushed to the cloud. You might have also noticed that the build was faster than the first time the publisher was built. This is because Docker caches image layers locally, and if you didn’t change anything in the publisher service, it doesn’t need to be rebuilt completely.
docker push <your_docker_hub_username>/publisher:latest
Your custom image is now hosted publicly on the internet! Navigate to //hub.docker.com/repository/docker/<your_docker_hub_username>/publisher
and login if you’d like to view it.
Now, navigate to the subscriber
folder and do the same for the subscriber service with two similar commands:
docker build . -t <your_docker_hub_username>/subscriber:latest
docker push <your_docker_hub_username>/subscriber:latest
Once you have a Kubernetes cluster created on Digital Ocean or wherever you prefer, and you’ve downloaded the cluster’s kubeconfig
or set your Kubernetes context, you’re ready to deploy the publisher, consumer, Kafka, and Zookeeper. Be sure that the cluster also has the Kubernetes dashboard installed. On Digital Ocean, the dashboard will be preinstalled.
Deploying to Kubernetes in the next steps will also require the Kubernetes CLI, kubectl
to be installed on your local machine. Once the prerequisites are complete, the next steps will be creating and deploying Kubernetes manifests.
These manifests will be for a , , and . At the root of the project, create a directory called kubernetes
and navigate to that directory. For organization, all manifests will be created here. Start by creating a file called namespace.yml
. Within Kubernetes, the namespace will group all of the resources created in this tutorial.
apiVersion: v1
kind: Namespace
metadata:
name: kafka-example
labels:
name: kafka-example
kubectl create -f namespace.yml --kubeconfig=<kubeconfig_file_for_your_cluster>
If the namespace was created successfully, the message namespace/kafka-example created
will be printed to the console.
These will allow the publisher and subscriber to send traffic to Kafka and Kafka to send traffic to Zookeeper, respectively. Still in the k8s
directory, start by creating a file called kafka-service.yml
with the following yml
:
kind: Service
apiVersion: v1
metadata:
name: example-kafka
namespace: kafka-example
labels:
app: example-kafka
spec:
ports:
- name: external
protocol: TCP
port: 9093
targetPort: 9093
- name: internal
protocol: TCP
port: 9092
targetPort: 9092
selector:
app: example-kafka
type: ClusterIP
sessionAffinity: None
kubectl create -f kafka-service.yml --kubeconfig=<kubeconfig_file_for_your_cluster>
kubectl should confirm that the service has been created. Now, create the other service by first creating a file called zookeeper-service.yml
. Add the following contents to that file:
kind: Service
apiVersion: v1
metadata:
name: example-zookeeper
namespace: kafka-example
labels:
app: example-zookeeper
spec:
ports:
- name: main
protocol: TCP
port: 2181
targetPort: 2181
selector:
app: example-zookeeper
type: ClusterIP
sessionAffinity: None
kubectl create -f zookeeper-service.yml --kubeconfig=<kubeconfig_file_for_your_cluster>
Next, four deployments will need to be created inside the new namespace, one for each service. Start by creating a file called zookeeper-deployment.yml
and add the following yml
:
kind: Deployment
apiVersion: apps/v1
metadata:
name: example-zookeeper
namespace: kafka-example
labels:
app: example-zookeeper
spec:
replicas: 1
selector:
matchLabels:
app: example-zookeeper
template:
metadata:
labels:
app: example-zookeeper
spec:
containers:
- name: example-zookeeper
image: jplock/zookeeper
ports:
- containerPort: 2181
protocol: TCP
imagePullPolicy: IfNotPresent
restartPolicy: Always
dnsPolicy: ClusterFirst
schedulerName: default-scheduler
enableServiceLinks: true
strategy:
type: RollingUpdate
kubectl create -f zookeeper-deployment.yml --kubeconfig=<kubeconfig_file_for_your_cluster>
When the deployment has been created successfully, deployment.apps/example-zookeeper created
will be printed. The next step will be creating and deploying the manifest for Kafka. Create the file kafka-deployment.yml
and add:
kind: Deployment
apiVersion: apps/v1
metadata:
name: example-kafka
namespace: kafka-example
labels:
app: example-kafka
spec:
replicas: 1
selector:
matchLabels:
app: example-kafka
template:
metadata:
labels:
app: example-kafka
spec:
containers:
- name: example-kafka
image: 'wurstmeister/kafka:2.12-2.4.0'
ports:
- containerPort: 9093
protocol: TCP
- containerPort: 9092
protocol: TCP
env:
- name: KAFKA_ADVERTISED_LISTENERS
value: INTERNAL://:9092,EXTERNAL://example-kafka.kafka-example.svc.cluster.local:9093
- name: KAFKA_CREATE_TOPICS
value: example-topic:1:1
- name: KAFKA_INTER_BROKER_LISTENER_NAME
value: INTERNAL
- name: KAFKA_LISTENERS
value: INTERNAL://:9092,EXTERNAL://:9093
- name: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP
value: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
- name: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR
value: '1'
- name: KAFKA_ZOOKEEPER_CONNECT
value: example-zookeeper.kafka-example.svc.cluster.local:2181
imagePullPolicy: IfNotPresent
restartPolicy: Always
dnsPolicy: ClusterFirst
schedulerName: default-scheduler
enableServiceLinks: true
strategy:
type: RollingUpdate
kubectl create -f kafka-deployment.yml --kubeconfig=<kubeconfig_file_for_your_cluster>
deployment.apps/example-kafka created
should have been printed to the console. The last two deployments to be created will be the subscriber and publisher services. Create publisher-deployment.yml
with the contents and be sure to replace <your_docker_hub_username>
with your own username:
kind: Deployment
apiVersion: apps/v1
metadata:
name: example-publisher
namespace: kafka-example
labels:
app: example-publisher
spec:
replicas: 1
selector:
matchLabels:
app: example-publisher
template:
metadata:
labels:
app: example-publisher
spec:
containers:
- name: example-publisher
image: '<your_docker_hub_username>/publisher:latest'
imagePullPolicy: Always
env:
- name: ENVIRONMENT
value: prod
- name: EXTERNAL_KAFKA_ADDR
value: example-kafka.kafka-example.svc.cluster.local:9093
- name: TOPIC
value: example-topic
restartPolicy: Always
dnsPolicy: ClusterFirst
schedulerName: default-scheduler
enableServiceLinks: true
strategy:
type: RollingUpdate
Run kubectl create -f publisher-deployment.yml --kubeconfig=<kubeconfig_file_for_your_cluster>
to create the deployment for the publisher and make sure that kubectl prints a message letting you know that it’s been created.
The last deployment to create is the subscriber, which will be created in the same way as the other services. Create the file subscriber-deployment.yml
and add the following configuration, being sure to replace <your_docker_hub_username>
:
kind: Deployment
apiVersion: apps/v1
metadata:
name: example-subscriber
namespace: kafka-example
labels:
app: example-subscriber
spec:
replicas: 1
selector:
matchLabels:
app: example-subscriber
template:
metadata:
labels:
app: example-subscriber
spec:
containers:
- name: example-subscriber
image: '<your_docker_hub_username>/subscriber:latest'
imagePullPolicy: Always
env:
- name: ENVIRONMENT
value: prod
- name: EXTERNAL_KAFKA_ADDR
value: example-kafka.kafka-example.svc.cluster.local:9093
- name: TOPIC
value: example-topic
restartPolicy: Always
dnsPolicy: ClusterFirst
schedulerName: default-scheduler
enableServiceLinks: true
strategy:
type: RollingUpdate
For the last of the deployments, create the subscriber by running kubectl create -f subscriber-deployment.yml --kubeconfig=<kubeconfig_file_for_your_cluster>
. If you now navigate to the Kubernetes dashboard for your cluster, you should see that all four deployments have been created, which have in turn created four pods. Each pod runs the container referred to by the image
field in its respective deployment.
kubectl delete namespace kafka-example --kubeconfig=<kubeconfig_file_for_your_cluster>
name: examples/kafka
homepage: //github.com/architect-team/architect-cli/tree/master/examples/kafka
services:
zookeeper:
image: jplock/zookeeper
interfaces:
main: 2181
kafka:
image: wurstmeister/kafka:2.12-2.4.0
interfaces:
internal: 9092
external: 9093
environment:
KAFKA_ZOOKEEPER_CONNECT:
${{ services.zookeeper.interfaces.main.host }}:${{ services.zookeeper.interfaces.main.port
}}
KAFKA_LISTENERS:
INTERNAL://:${{ services.kafka.interfaces.internal.port }},EXTERNAL://:${{
services.kafka.interfaces.external.port }}
KAFKA_ADVERTISED_LISTENERS:
INTERNAL://:9092,EXTERNAL://${{ services.kafka.interfaces.external.host }}:${{
services.kafka.interfaces.external.port }}
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_CREATE_TOPICS: architect:1:1
debug:
volumes:
docker:
mount_path: /var/run/docker.sock
host_path: /var/run/docker.sock
environment:
KAFKA_ADVERTISED_HOST_NAME: host.docker.internal # change to 172.17.0.1 if running on Ubuntu
KAFKA_LISTENERS: INTERNAL://:9092
KAFKA_ADVERTISED_LISTENERS: INTERNAL://:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT
publisher:
build:
context: ./publisher/
interfaces:
environment:
EXTERNAL_KAFKA_ADDR:
${{ services.kafka.interfaces.external.host }}:${{ services.kafka.interfaces.external.port
}}
TOPIC: architect
ENVIRONMENT: prod
debug:
environment:
INTERNAL_KAFKA_ADDR:
${{ services.kafka.interfaces.internal.host }}:${{ services.kafka.interfaces.internal.port
}}
ENVIRONMENT: local
subscriber:
build:
context: ./subscriber/
interfaces:
environment:
EXTERNAL_KAFKA_ADDR:
${{ services.kafka.interfaces.external.host }}:${{ services.kafka.interfaces.external.port
}}
TOPIC: architect
ENVIRONMENT: prod
debug:
environment:
INTERNAL_KAFKA_ADDR:
${{ services.kafka.interfaces.internal.host }}:${{ services.kafka.interfaces.internal.port
}}
ENVIRONMENT: local
Copy and paste the yml
above into a file called “architect.yml” in the project’s root directory. To run the Kafka component locally, run the command below in the project’s root directory:
architect dev architect.yml