The Pain of Setting Up Development Environments

How much time have you lost configuring your environment to run multiple applications at the same time? Maybe an hour? Or even a whole month? Then, after coding for weeks, your computer crashes, and the only solution is a hard reset. Now, you find yourself in the same situation, rebuilding everything from scratch. A personal nightmare, right? 😅

Fortunately, VSCode offers a solution: running your application inside a Docker container. With this approach, the only setup you need is Docker. Like magic! ✨

What is a DevContainer?

The DevContainer feature in VSCode allows developers to configure their application environment once and run it anywhere. In this article, I'll show you how to build a Go application with Kafka using a DevContainer.

Why Go and Kafka?

Go and Kafka make a powerful combination for building high-performance microservices. Kafka allows you to distribute workloads using partitions, while Go's lightweight goroutines help maximize performance. Imagine allocating threads based on Kafka partitions—Kafka efficiently manages message distribution while Go ensures optimal processing.


🚀 Let's Get Started

1️⃣ Initialize Your Go Project

First, create a project folder and initialize a Go module:

mkdir go-with-kafka
cd go-with-kafka && go mod init go-with-kafka

2️⃣ Set Up a DevContainer

Open your project in VSCode and create a .devcontainer folder. This will contain all the Docker configurations needed for development.

Create boot.sh

This script initializes the environment and sets up a Kafka topic:

#!/bin/zsh

# Update package list
sudo apt update

# Install Go dependencies
go mod tidy

# Change ownership of Go directories to vscode user
sudo chown -R vscode:vscode .

# Creating a Kafka topic
echo "Waiting for Kafka to start..."
sleep 10

kafka-topics --create \
  --bootstrap-server kafka:29092 \
  --replication-factor 1 \
  --partitions 1 \
  --topic example-topic || echo "The topic already exists."

echo "Topic created."

Create docker-compose.yml

This file defines services for your Go application, Kafka, and Zookeeper:

version: '3'
services:
  app:
    build:
      context: ..
      dockerfile: .devcontainer/Dockerfile
    environment:
      - VISUAL="code --wait"
      - APPNAME=go-with-kafka
    volumes:
      - ../..:/workspaces:cached
      - ~/.ssl:/home/vscode/.ssl:ro
    command: sleep infinity

  zookeeper:
    image: confluentinc/cp-zookeeper:latest
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000

  kafka:
    image: confluentinc/cp-kafka:latest
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:29092,OUTSIDE://localhost:9092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE
      KAFKA_AUTO_CREATE_TOPICS_ENABLE: true

Create Dockerfile

FROM mcr.microsoft.com/devcontainers/go

# [Optional] Uncomment this section to install additional OS packages.
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
    && apt-get -y install --no-install-recommends

🏗️ Building a Simple Kafka Pub/Sub in Go

Create main.go

package main

import (
    "context"
    "log"
    "os"

    "github.com/segmentio/kafka-go"
)

func main() {
    if len(os.Args) < 2 {
        log.Fatalf("Usage: %s [pub|sub]", os.Args[0])
    }

    mode := os.Args[1]
    brokers := []string{"kafka:29092"}
    topic := "example-topic"

    switch mode {
    case "pub":
        publish(brokers[0], topic)
    case "sub":
        subscribe(brokers[0], topic)
    default:
        log.Fatalf("Invalid mode: %s. Use 'pub' to publish or 'sub' to consume.", mode)
    }
}

func publish(broker string, topic string) {
    writer := kafka.Writer{
        Addr:     kafka.TCP(broker),
        Topic:    topic,
        Balancer: &kafka.LeastBytes{},
    }

defer writer.Close()

    message := kafka.Message{
        Key:   []byte("Key"),
        Value: []byte("Hello, Kafka with kafka-go!"),
    }

    err := writer.WriteMessages(context.Background(), message)
    if err != nil {
        log.Fatalf("Error sending message: %v", err)
    }

    log.Println("Message sent successfully!")
}

func subscribe(broker string, topic string) {
    reader := kafka.NewReader(kafka.ReaderConfig{
        Brokers:   []string{broker},
        Topic:     topic,
        Partition: 0,
        MinBytes:  10e3, // 10KB
        MaxBytes:  10e6, // 10MB
    })

defer reader.Close()

    log.Println("Waiting for messages...")

    for {
        message, err := reader.ReadMessage(context.Background())
        if err != nil {
            log.Fatalf("Error reading message: %v", err)
        }
        log.Printf("Message received: key=%s value=%s\n", string(message.Key), string(message.Value))
    }
}

🎯 Running the Application

In VSCode, press Ctrl + Shift + P (or Cmd + Shift + P on macOS) and select Rebuild and Reopen in Container. Wait for the setup to complete. Once inside the container, open a terminal and run:

Start the subscriber:

go run main.go sub

Publish a message:

go run main.go pub

Kafka in action

If everything works, you now have a Kafka-based Go application running inside a DevContainer! 🎉


🌟 Why Use DevContainers?

DevContainers are powerful for developers who don’t want to waste time configuring their local machine. Using containers ensures that your setup is portable and consistent across different environments.

🔗 Check out more DevContainer templates: GitHub - devcontainers/templates