Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Simplified Couchbase Server Cluster Creation via Docker

DZone's Guide to

Simplified Couchbase Server Cluster Creation via Docker

This straightforward guide will help you run an instance of Couchbase Server cluster inside a Docker container, as well as how to automate the process.

· Cloud Zone ·
Free Resource

Insight into the right steps to take for migrating workloads to public cloud and successfully reducing cost as a result. Read the Guide.

It's pretty straight-forward to run an instance of Couchbase Server in a Docker container. It takes a bit more work to set up and fully configure a cluster. In this post, I'm going to walk through a shell script I wrote to automate the process.

For Couchbase Connect 2017, we built an application that shows off everything from the Couchbase Analytics Service through to real-time mobile data synchronization using Couchbase Mobile. We use an NFC temperature sensing patch, generate alerts through the Vue.js-based web client, alter schemas on the fly, send push notifications, fail over clusters, and more. Check out this video of it in action.


In the demo, Couchbase Server, Sync Gateway, and the web backend are all running in the cloud. We're working on releasing the code for the entire project. As part of that, I want to be able to run a trimmed-down version on a single machine.

That means, ideally, running two Couchbase Server clusters, Sync Gateway, and the Node.js backend application, all simultaneously.

Docker makes running separate instances of Couchbase pretty easy. Completely configuring a whole cluster still takes some work, though. That's where the script I wrote comes in.

A Flexible Cluster Creation Script

The script is written using Bash. I had a few goals in mind while developing it:

  1. Make something general purpose and easy to use.
  2. Allow for flexible configuration, while falling back to sensible defaults.
  3. Minimize dependencies to Bash, common standard utilities (some things are just worth doing in awk), and the Docker CLI.

I've posted the code and other related pieces on Github in this gist. Here's the cluster formation script.

#!/bin/bash

# This script intends to simplify launching, configuring and joining a set of 
# Couchbase Server instances running in Docker containers on a single physical host
# into a cluster.
# Configuration happens via environment variables taken from the runtime environment,
# redirected input, or provided on the command line.

Usage() {
  echo "Usage: $0 [VAR=value] ... [< file]"
}

# Read configuration from stdin when redirected (e.g. $0 < config)
[[ ! -t 0 ]] && source /dev/stdin

# Override configuration based on supplied arguments
until [ -z "$1" ]
do
  [[ "$1" =~ ^[^=]+=[^=]+$ ]] || { echo Malformed argument "$1"; Usage; exit 1; }
  eval "$1" || { echo Failed processing argument "$1"; Usage; exit 1; }
  shift
done

# Use supplied parameters or try for sensible defaults
: ${DOCKER:=docker}
: ${COUCHBASE_NETWORK:=cbnetwork}
: ${COUCHBASE_NODE_NAME:=cbserver}
: ${COUCHBASE_NODE_COUNT:=1}
: ${COUCHBASE_CLUSTER_NAME:=cluster}
: ${COUCHBASE_BUCKET:=default}
: ${COUCHBASE_ADMINISTRATOR_USERNAME:?Please supply an administrator username}
: ${COUCHBASE_ADMINISTRATOR_PASSWORD:?Please supply an administrator password}
: ${COUCHBASE_RBAC_USERNAME:?Please supply an RBAC username}
: ${COUCHBASE_RBAC_PASSWORD:?Please supply an RBAC password}
: ${COUCHBASE_RBAC_NAME:=}
: ${COUCHBASE_RBAC_ROLES:="bucket_admin[*]"}
: ${COUCHBASE_SERVICES:="data,index,query,fts"}
: ${COUCHBASE_SERVER_PORTS:="8091-8094:8091-8094::11210:11210"}

cluster_url="couchbase://127.0.0.1"

read -r -d '' ports_script << EOF || true
{
  split(\$1, maps, /::/)
  for (map in maps) {
    split(maps[map], ranges, /:/)
    count = split(ranges[1], ports, "-")

    for (port in ports) {
      ports[port] += offset
    }

    ranges[1] = ports[1]

    if (count > 1) ranges[1] = ports[1] "-" ports[2]

    printf "-p " ranges[1] ":" ranges[2] " "
  }
}
EOF

for ((node = 0; node < $COUCHBASE_NODE_COUNT; ++node))
do
  echo "Starting node ${COUCHBASE_NODE_NAME}_${node}"
  let offset=${node}*1000 || true
  ports=$(awk -v offset=$offset "$ports_script" <<< "${COUCHBASE_SERVER_PORTS}")
  "$DOCKER" run -d --name "${COUCHBASE_NODE_NAME}_${node}" --network "$COUCHBASE_NETWORK" $ports couchbase
done

sleep 15

# Setup initial cluster/initialize node
"$DOCKER" exec "${COUCHBASE_NODE_NAME}_0" couchbase-cli cluster-init --cluster ${cluster_url} --cluster-name "$COUCHBASE_CLUSTER_NAME" \
  --cluster-username "$COUCHBASE_ADMINISTRATOR_USERNAME" --cluster-password "$COUCHBASE_ADMINISTRATOR_PASSWORD" \
  --services data,index,query,fts --cluster-ramsize 256 \
  --cluster-index-ramsize 256 --cluster-fts-ramsize 256 --index-storage-setting default

# Setup Bucket
"$DOCKER" exec "${COUCHBASE_NODE_NAME}_0" couchbase-cli bucket-create --cluster ${cluster_url} \
  --username "$COUCHBASE_ADMINISTRATOR_USERNAME" --password "$COUCHBASE_ADMINISTRATOR_PASSWORD" \
  --bucket "$COUCHBASE_BUCKET" --bucket-type couchbase --bucket-ramsize 256

# Setup RBAC user using CLI
"$DOCKER" exec "${COUCHBASE_NODE_NAME}_0" couchbase-cli user-manage --cluster ${cluster_url} \
  --username "$COUCHBASE_ADMINISTRATOR_USERNAME" --password "$COUCHBASE_ADMINISTRATOR_PASSWORD" \
  --set --rbac-username "$COUCHBASE_RBAC_USERNAME" --rbac-password "$COUCHBASE_RBAC_PASSWORD" \
  --rbac-name "$COUCHBASE_RBAC_NAME" --roles "$COUCHBASE_RBAC_ROLES" --auth-domain local

# Add nodes
docker_ip() {
  "$DOCKER" inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$@"
}

for ((node = 1; node < $COUCHBASE_NODE_COUNT; ++node))
do  
  "$DOCKER" exec "${COUCHBASE_NODE_NAME}_${node}" couchbase-cli server-add \
    --cluster $(docker_ip "${COUCHBASE_NODE_NAME}_0"):8091 \
    --username "$COUCHBASE_ADMINISTRATOR_USERNAME" --password "$COUCHBASE_ADMINISTRATOR_PASSWORD" \
    --server-add $(docker_ip "${COUCHBASE_NODE_NAME}_${node}"):8091 \
    --server-add-username "$COUCHBASE_ADMINISTRATOR_USERNAME" --server-add-password "$COUCHBASE_ADMINISTRATOR_PASSWORD" \
    --services "$COUCHBASE_SERVICES"
done

# Rebalance (needed to fully enable added nodes)
"$DOCKER" exec "${COUCHBASE_NODE_NAME}_0" couchbase-cli rebalance --cluster ${cluster_url} \
  --username "$COUCHBASE_ADMINISTRATOR_USERNAME" --password "$COUCHBASE_ADMINISTRATOR_PASSWORD" \
  --no-wait


Outline

Roughly speaking, in order, the script takes care of the following:

  • Configuring parameters
  • Starting the requested number of Couchbase Server instances, one per Docker container, using the latest production image
  • Mapping the necessary ports (offsetting each instance to avoid collisions)
  • Setting the administrative account and password
  • Selecting the services available and setting the memory allocations for them
  • Creating a bucket
  • Granting rights to a client account using RBAC
  • Combining the nodes into a cluster
  • Rebalancing the final cluster

I won't go through the script in detail. There are comments that tell which section corresponds to the outline. Feel free to leave a comment here or on Github if you have questions.

Usage

I wrote this to set up clusters on my Mac. I expect it will work equally well for any machine that can run Bash and Docker. The script doesn't have any options. Everything is controlled by supplying parameters as key/value pairs. They're supplied four ways. In order of priority, from lowest to highest,

  • Defaults (written into the script itself)
  • Existing environment variables
  • Lines fed to the standard input
  • Supplied as command line arguments

In the last two instances, parameters are supplied just the way you would define an environment variable. E.g. to request 3 nodes, add COUCHBASE_NODE_COUNT=3, either on the command line or redirected from a file. Look at where the defaults are set to see what you can control.

Port mapping takes a little explanation. Couchbase uses several port ranges. To create a cluster, a number of ports have to both be exposed by Docker and mapped to open ports on the host machine. To do this, specify blocks of ranges and mappings, separated by double colons (::). For example, setting COUCHBASE_SERVER_PORTS="9091-9094:8091-8094::12210:11210 maps the standard Couchbase ports 8091-8094 and 11210 to the host machine ports 9091-9094 and 12210, respectively.

Example

In a typical scenario, you might have the cluster administrator account information assigned in environment variables. I.e.

export COUCHBASE_ADMINISTRATOR_USERNAME=Administrator
export COUCHBASE_ADMINISTRATOR_PASSWORD=password


You might then keep some other general configuration information in a file. The name doesn’t matter. Here’s one I use I call london-cluster

COUCHBASE_NODE_NAME=london
COUCHBASE_CLUSTER_NAME=london-cluster
COUCHBASE_BUCKET=health
COUCHBASE_RBAC_USERNAME=admin 
COUCHBASE_RBAC_PASSWORD=password 
COUCHBASE_RBAC_NAME='J. D. User' 
COUCHBASE_RBAC_ROLES=Admin 
COUCHBASE_SERVER_PORTS="11091-11094:8091-8094::14210:11210"


Finally, to start the cluster, you would invoke something like the following on the command line.

$ ./server COUCHBASE_NODE_COUNT=3 < london-cluster


Related Scripts

In the gist, you can also find a similar script for setting up Sync Gateway. That script is a little more built out in terms of having commands to create a container, start it, stop it, and remove it. Because Sync Gateway requires a configuration file, the script has to work a little harder to do the parameter substitutions. Otherwise, it’s similar to but simpler than the server script.

There’s also a script that sets up the entire demo, including using the previous two scripts. As I mentioned earlier, we’re working on releasing the code for the whole application end-to-end. The setup script gives an idea of what’s involved.

TrueSight Cloud Cost Control provides visibility and control over multi-cloud costs including AWS, Azure, Google Cloud, and others.

Topics:
cloud ,docker ,couchbase server ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}