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.podIPNote
ERL_DIST_PORTis 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-erlA 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-erlA 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.yamlNow 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.