View Source Kubernetes Examples
An example showing the use of gen_cluster
for creating an Erlang cluster in
Kubernetes with the 2 types of builtin DNS support, A
and SRV
records.
IPs
This build is done with the rebar3
profile ip_prod
and a separate Dockerfile
Dockerfile.ip
.
Setup
Under examples/k8s_erlang_cluster
there is an example project that builds an
Erlang release with kustomize configuration to deploy a
cluster to Kubernetes. The example's config/sys.config
contains configuration
for gen_cluster
which is started on boot of the release:
{discovery, {dns, #{domain => "k8s-erlang-cluster.k8s-erlang-cluster"}}}
This tells gen_cluster
to use the DNS module for finding peers which defaults
to looking up A
records and using the IPs as the host part of each node name.
The domain
is set to <service>.<namespace>
which resolves to records for
the service k8s-erlang-cluster
in a namespace k8s-erlang-cluster
.
To set the node name properly there are 3 pieces of configuration. First,
config/vm.args.src
sets -name
to use an environment variable POD_IP
:
-name k8s_erlang_cluster@${NODE_HOST}
Next, the environment of the container that runs the release is made to set the
environment variable POD_IP
to the IP address of that Pod in
deployment/base/deployment.yaml
:
env:
- name: ERL_DIST_PORT
value: "39135"
- name: NODE_HOST
valueFrom:
fieldRef:
fieldPath: status.podIP
Note
ERL_DIST_PORT
is also set in the environment. This tells the rebar3 release start script to configure the VM to listen on that port number for distribution connections, as well as what port to connect to other nodes on. Setting the variable will disable EPMD as it is not needed if we know the port to use and the port used by all other nodes in the cluster.
Lastly, a headless
service
is used to expose the IPs of these Pods through DNS. A headless service is
created when clusterIP
is set to None
in the Service
resource, found in
deployment/base/service.yaml
:
clusterIP: None
ports:
- protocol: TCP
port: 39135
targetPort: 39135
name: dist-erl
A Service without clusterIP: None
would have a single IP to load balance
requests to the Pods through. The headless service results in an Endpoint
per-Pod, see below in the steps to setup the cluster in
kind for an example.
Run
The example can be installed to any Kubernetes cluster but comes with a
configuration (examples/k8s_erlang_cluster/cluster.yaml
) for creating the
cluster with kind via the tool
ctlptl:
$ ctlptl apply -f cluster.yaml
The cluster.yaml
configuration creates a kind cluster with a Docker registry
on localhost port 5005. This allows for the publishing of a Docker image for the
example release locally in a place the kind cluster can pull from:
$ docker build -f Dockerfile.ip -t localhost:5005/k8s_erlang_cluster:ip-0.1.0
$ docker push -f Dockerfile.ip localhost:5005/k8s_erlang_cluster:ip-0.1.0
The kustomize configuration can be used to install the
release with kubectl apply -k
:
$ kubectl apply -k deployment/base
namespace/k8s-erlang-cluster created
configmap/k8s-erlang-cluster created
service/k8s-erlang-cluster created
deployment.apps/k8s-erlang-cluster created
With get endpoints
we can view the results of the creation of a headless
Service that matches 3 Pods:
$ kubectl -n k8s-erlang-cluster get endpoints k8s-erlang-cluster
NAME ENDPOINTS AGE
k8s-erlang-cluster 10.244.0.5:39135,10.244.0.6:39135,10.244.0.7:39135 7m11s
To see that the cluster is formed run a command against a single Pod in the
Deployment with exec
:
$ kubectl exec -n k8s-erlang-cluster deploy/k8s-erlang-cluster -- bin/k8s_erlang_cluster eval 'nodes().'
['k8s_erlang_cluster@10.244.0.5', 'k8s_erlang_cluster@10.244.0.7']
SRV
This section builds on the previous section on using IPs for the host
name part of a node name and DNS queries for A records. The necessary changes to
the Kubernetes deployment are done in a kustomize overlay,
deployment/overlays/srv
.
The SRV specific build is done with the rebar3
profile srv_prod
and a
separate Dockerfile Dockerfile.srv
. It also includes a necessary change to the
CoreDNS ConfigMap in the Kubernetes cluster.
Setup
To us SRV
records we set record_type
to srv
in the gen_cluster
configuration. A separate sys.config
file is used by the rebar3
profile
named srv_sys.config
:
[{discovery, {dns, #{record_type => srv,
domain => "_dist-erl._tcp.k8s-erlang-cluster.k8s-erlang-cluster"}}}]},
This tells gen_cluster
to use the DNS module for finding peers by looking up
the SRV
records and using the results as the host part of each node name. The
domain
is set to _<port name>._<protocol>.<service>.<namespace>
which
resolves to records for the service k8s-erlang-cluster
in a namespace
k8s-erlang-cluster
for the port named dist-erl
and using protocol TCP
.
This is setup in the base/service.yaml
the same as is used for the IP
version of discovery above, but made use of here:
kind: Service
apiVersion: v1
metadata:
name: k8s-erlang-cluster
spec:
clusterIP: None
ports:
- protocol: TCP
port: 39135
targetPort: dist-erl
name: dist-erl
A separate vm.args
is used because we want to set no host part of the node
name after -name
:
-name k8s_erlang_cluster
This way Erlang essentially uses the result of the command hostname --fqdn
. By
default in Kubernetes this will not include any Service subdomain and the FQDN
will be the same as the short name. So subdoman
must be added to the
Pod spec of the Deployment resource:
subdomain: "k8s-erlang-cluster"
This is done in a kustomize patch file added to
deployment/overlays/srv/kustomization.yaml
:
patches:
- path: deployment.yaml
Now hostname --fqdn
on the container in the Pod would result in <pod name>.k8s-erlang-cluster.svc.cluster.local
Nothing is changed in the env
of deployment.yaml
but the only part used in
this container is the ERL_DIST_PORT
:
env:
- name: ERL_DIST_PORT
value: "39135"
While the same headless
service
is used to expose the IPs of these Pods through DNS, by default the DNS records
for a SRV
query would look like:
_dist-erl._tcp.k8s-erlang-cluster.k8s-erlang-cluster.svc.cluster.local service
= 0 33 39135 10-244-0-15.k8s-erlang-cluster.k8s-erlang-cluster.svc.cluster.local.
Notice the 10-244-0-15
part of the name. This is the IP of the corresponding
Pod. Using the Pod name instead of the IP with A records or the IP based domain
could be more useful in logs and other telemetry about a node. Also note that,
as far as I've ever found, there is no way to actually get the IP based name
easily into an environment variable to use as a name in the Erlang node name.
In order for DNS to use the name of the Pod we can add an option to the configuration of CoreDNS through its ConfigMap:
$ kubectl -n kube-system edit configmap coredns
...
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
endpoint_pod_names
}
...
$ kubectl -n kube-system rollout restart deployment coredns
This will restart the Pods in the CoreDNS Deployment and pickup the modified
ConfigMap with endpoint_pod_names
set.
In the example a ConfigMap resource is provided
(deployments/overlays/srv/coredns-configmap.yaml
) and applied with kubectl
,
but for CoreDNS to pickup the configuration change the deployment must still be
restarted:
$ kubectl -n kube-system rollout restart deployment coredns
I first learned about this CoreDNS option from the blog posts K8s Erlang
Clustering by Luis
Rascão and CoreDNS:
endpoint_pod_names
by Roger Lipscombe.
Run
The example can be installed to any Kubernetes cluster but comes with a
configuration (examples/k8s_erlang_cluster/cluster.yaml
) for creating the
cluster with kind via the tool
ctlptl:
$ ctlptl apply -f cluster.yaml
The cluster.yaml
configuration creates a kind cluster with a Docker registry
on localhost port 5005. This allows for the publishing of a Docker image for the
example release locally in a place the kind cluster can pull from:
$ docker build -f Dockerfile.srv -t localhost:5005/k8s_erlang_cluster:srv-0.1.0
$ docker push -f Dockerfile.srv localhost:5005/k8s_erlang_cluster:srv-0.1.0
The kustomize srv
overlay is used to install the
release with kubectl apply -k
:
$ kubectl apply -k deployment/overlays/srv
namespace/k8s-erlang-cluster created
Warning: resource configmaps/coredns is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
configmap/coredns configured
service/k8s-erlang-cluster created
deployment.apps/k8s-erlang-cluster created
CoreDNS must now be manually restarted to pickup the new configuration:
$ kubectl rollout restart -n kube-system deployment coredns
It may take a few seconds for the cluster to form because we had to restart DNS
and wait for a gen_cluster
refresh. To see that the cluster has formed and the
host part of the node name is an FQDN of <pod name>.<service>.<namespace>.svc.cluster.local
run a command against a single
Pod in the Deployment with exec
:
$ kubectl exec -n k8s-erlang-cluster deploy/k8s-erlang-cluster -- bin/k8s_erlang_cluster eval 'nodes().'
['k8s_erlang_cluster@k8s-erlang-cluster-5f4954c6b5-kzcwl.k8s-erlang-cluster.k8s-erlang-cluster.svc.cluster.local', 'k8s_erlang_cluster@k8s-erlang-cluster-5f4954c6b5-6ts2w.k8s-erlang-cluster.k8s-erlang-cluster.svc.cluster.local']
StatefulSet
Another option for host names that work with SRV records is a
StatefulSet.
These provide a stable host name for each Pod <statefulset name>-<ordinal>
.
The same steps in the SRV section will work to configure the release and
discovery with the key difference that no configuration changes to CoreDNS are
necessary for this to work.
But there are limitations to use of a StatefulSet and they should only be used where you truly need the stable network name or persistent storage. See the Kubernetes docs for more on StatefulSets.