At Mattermost, we aim to develop a product that is simple to deploy and manage.
To this end, we recently examined Kubernetes, today’s most popular orchestration platform that uses containers.
After determining the platform would make it even easier to deploy, manage and scale our product, we decided to migrate our “virtual office”—our Mattermost instance where we interact with customers and members of our community—from AWS to Kubernetes. That way, we’d be able to see what improvements we needed to make in order to make our platform more cloud-native.
Here’s how we did the migration:
- We decided where to run Kubernetes
- We set up and configured Ingress
- We set up the database
- We set up Mattermost
Let’s explore each step in detail.
1. Decide where to run Kubernetes
First, we installed Kubernetes on the latest version of Mattermost.
Next, we needed to figure out where to run Kubernetes. We had several options, including on-premises, AWS, Azure, GCP or DigitalOcean.
We decided to go with AWS EKS because we currently don’t have the overhead to manage the entire Kubernetes cluster, only the worker nodes; AWS manages the master nodes.
(For the purposes of this post, we won’t focus on how to deploy Kubernetes on other platforms. If you need help with that, check this out.)
2. Set up and configure Ingress
After we deployed our Kubernetes cluster, we installed NGINX Ingress using this documentation.
To reduce WebSocket errors, we added cache and keep-alive in our ConfigMap:
`kind: ConfigMap apiVersion: v1 metadata: name: nginx-configuration namespace: ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx data: use-proxy-protocol: "true" http-snippet: "proxy_cache_path /cache/mattermost levels=1:2 keys_zone=mattermost_cache:10m max_size=3g inactive=120m use_temp_path=off;" keep-alive: "3600"`
The Kubernetes service will look like this:
`kind: Service apiVersion: v1 metadata: name: ingress-nginx namespace: ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx annotations: service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*" service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "3600" spec: type: LoadBalancer selector: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx ports: - name: http port: 80 targetPort: http - name: https port: 443 targetPort: https`
We also needed to add volume to store the cache to the deployment. So we updated the deployment manifest:
`... volumeMounts: - mountPath: /cache/mattermost name: mattermost-cache ... volumes: - emptyDir: {} name: mattermost-cache`
To get the TLS certificates—which are not required but are recommended for security reasons—we installed a cert manager using the helm chart. For more information, follow the instructions here.
3. Set up the database
In our legacy deployment (i.e., running servers on EC2), we had our database set up in AWS using RDS, a managed service by AWS.
While we transitioned from EC2 to Kubernetes, we decided to keep using RDS for our database. We do, however, plan to move the database to Kubernetes as well. It’s only a matter of time.
Here, we had to make a few configuration changes to the security groups and VPCs in order to allow the Kubernetes cluster to talk to the database.
4. Set up Mattermost
To set up Mattermost Enterprise Edition, we first had to set up a deployment manifest. (If you use Team Edition, use the official Mattermost helm chart for help with installation.)
Here’s what our manifest looks like:
` apiVersion: apps/v1 kind: Deployment metadata: name: mattermost-community-daily-app labels: app.kubernetes.io/name: mattermost-community-daily-app app.kubernetes.io/component: "mattermost-community-daily-app" spec: replicas: 2 strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 selector: matchLabels: app.kubernetes.io/name: mattermost-community-daily-app app.kubernetes.io/component: "mattermost-community-daily-app" template: metadata: labels: app.kubernetes.io/name: mattermost-community-daily-app app.kubernetes.io/component: "mattermost-community-daily-app" annotations: prometheus.io/scrape: "true" prometheus.io/port: "8067" prometheus.io/path: "/metrics" spec: initContainers: - name: init-config image: busybox imagePullPolicy: IfNotPresent command: - sh - "-c" - | set -ex rm -rfv /mattermost/config/lost+found cp /tmp/config/config.json /mattermost/config/config.json volumeMounts: - mountPath: /tmp/config/config.json name: mattermost-init-config-json subPath: config.json - mountPath: /mattermost/config/ name: mattermost-config - mountPath: /tmp/onelogin/ name: mattermost-onelogin - name: init-plugins-config image: busybox imagePullPolicy: IfNotPresent command: - sh - "-c" - | cp /mnt/plugins/init-plugins.sh /tmp && cd /tmp && chmod +x init-plugins.sh ls -la ./init-plugins.sh ls -la /mattermost/plugins volumeMounts: - name: mattermost-init-plugins mountPath: /mnt/plugins/ - name: mattermost-plugins mountPath: /mattermost/plugins/ - name: mattermost-plugins-client mountPath: /mattermost/client/plugins/ containers: - name: mattermost-community-daily-app image: "mattermost/mattermost-enterprise-edition:5.5.0" imagePullPolicy: Always terminationMessagePolicy: "FallbackToLogsOnError" ports: - containerPort: 8000 name: api - containerPort: 8067 name: metrics - containerPort: 8075 name: cluster - containerPort: 8074 name: gossip livenessProbe: initialDelaySeconds: 90 timeoutSeconds: 5 periodSeconds: 15 httpGet: path: /api/v4/system/ping port: 8000 readinessProbe: initialDelaySeconds: 15 timeoutSeconds: 5 periodSeconds: 15 httpGet: path: /api/v4/system/ping port: 8000 volumeMounts: - mountPath: /mattermost/plugins/ name: mattermost-plugins - mountPath: /mattermost/client/plugins/ name: mattermost-plugins-client - mountPath: /mattermost/config/ name: mattermost-config volumes: - name: mattermost-init-config-json configMap: name: mattermost-community-daily-init-config-json items: - key: config.json path: config.json - name: mattermost-init-plugins configMap: name: mattermost-community-daily-init-plugins - name: mattermost-onelogin secret: secretName: mattermost-community-daily-secret-onelogin - name: mattermost-plugins emptyDir: {} - name: mattermost-plugins-client emptyDir: {} - name: mattermost-config emptyDir: {} ``` `
JobServer is a service that indexes Elasticsearch, syncs events, and more. Our JobServer deployment looks like this:
` apiVersion: extensions/v1beta1 kind: Deployment metadata: name: mattermost-community-daily-jobserver labels: app: mattermost-community-daily-jobserver spec: replicas: 1 template: metadata: labels: app: mattermost-community-daily-jobserver component: "jobserver" spec: initContainers: - name: "init-mattermost-app" image: "appropriate/curl:latest" imagePullPolicy: "IfNotPresent" command: ["sh", "-c", "until curl --max-time 5 https://mattermost-community.community:8000/api/v4/system/ping ; do echo waiting for Mattermost App come up; sleep 5; done; echo init-mattermost-app finished"] containers: - name: mattermost-community-daily-jobserver image: "mattermost/mattermost-enterprise-edition:5.5.0" imagePullPolicy: Always command: ["mattermost", "jobserver"] volumeMounts: - mountPath: /mattermost/config/config.json name: config-json subPath: config.json volumes: - name: config-json configMap: name: mattermost-community-daily-init-config-json items: - key: config.json path: config.json ``` `
And the services manifest is as follows:
` apiVersion: v1 kind: Service metadata: name: mattermost-community-daily labels: app: mattermost-community-daily spec: selector: app: mattermost-community-daily type: ClusterIP ports: - port: 8000 targetPort: 8000 protocol: TCP name: mattermost-community-daily - port: 8067 targetPort: 8067 protocol: TCP name: mattermost-community-daily-app-metrics ``` `
The ConfigMap, which holds the Mattermost config, can be customized to your needs (additional setting configurations can be found here):
` apiVersion: v1 kind: ConfigMap metadata: name: mattermost-community-daily-init-config-json labels: app: mattermost-community-daily data: config.json: | "ServiceSettings": { .... ... ``` `
To set up plugins, we added an InitContainer, as you can see in the StatefulSet above.
Here’s the ConfigMap that holds the plugin definitions and the script required to download and configure the plugins:
` apiVersion: v1 kind: ConfigMap metadata: name: mattermost-community-daily-init-plugins labels: app: mattermost-community-daily data: init-plugins.sh: | #!/bin/sh PLUGINS_TAR="hovercardexample.tar.gz" PLUGINS_TAR="${PLUGINS_TAR} jira2.tar.gz" PLUGINS_TAR="${PLUGINS_TAR} memes.tar.gz" PLUGINS_TAR="${PLUGINS_TAR} mattermost-github-plugin-linux-amd64.tar.gz" PLUGINS_TAR="${PLUGINS_TAR} mattermost-plugin-autolink-linux-amd64.tar.gz" PLUGINS_TAR="${PLUGINS_TAR} mattermost-zoom-plugin-linux-amd64.tar.gz" PLUGINS_TAR="${PLUGINS_TAR} mattermost-jira-plugin-linux-amd64.tar.gz" PLUGINS_TAR="${PLUGINS_TAR} mattermost-plugin-autotranslate-linux-amd64.tar.gz" PLUGINS_TAR="${PLUGINS_TAR} com.github.matterpoll.matterpoll.tar.gz" PLUGINS_TAR="${PLUGINS_TAR} com.mattermost.welcomebot.tar.gz" for plugin_tar in ${PLUGINS_TAR}; do wget https://mattermost-public-plugins-kubernetes.s3-website-us-east-1.amazonaws.com/${plugin_tar} -P /mattermost/plugins cd /mattermost/plugins tar -xzvf ${plugin_tar} rm -f ${plugin_tar} done ``` `
Learn more about how to migrate Mattermost to Kubernetes
Moving forward, we plan to make some additional changes to Mattermost to make it even easier to deploy and manage.
Stay tuned for updates—or, better yet, join the Kubernetes channel on our community server and take part in the discussion. You can find me as @cpanato there.
(Editor’s note: This post was written by Carlos Panato, Staff Software Engineer at Mattermost, Inc. If you have any feedback or questions about How to migrate Mattermost to Kubernetes, please let us know.)