├── SECURITY.md ├── templates ├── upgrade-job.yml ├── single-node.yml └── three-node.yml ├── LICENSE.txt ├── README.md └── CODE_OF_CONDUCT.md /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) 4 | as soon as it is discovered. This library limits its runtime dependencies in 5 | order to reduce the total cost of ownership as much as can be, but all consumers 6 | should remain vigilant and have their security stakeholders review all third-party 7 | products (3PP) like this one and their dependencies. 8 | -------------------------------------------------------------------------------- /templates/upgrade-job.yml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: 5 | namespace: 6 | spec: 7 | template: 8 | spec: 9 | securityContext: 10 | runAsUser: 999 11 | fsGroup: 998 12 | fsGroupChangePolicy: "OnRootMismatch" 13 | terminationGracePeriodSeconds: 120 14 | hostname: 15 | containers: 16 | - name: tableau-server-in-a-container-upgrade-container 17 | image: 18 | env: 19 | - name: LICENSE_KEY 20 | value: 21 | resources: 22 | requests: 23 | memory: 32Gi 24 | cpu: 8 25 | ports: 26 | - containerPort: 8080 27 | - containerPort: 8443 28 | volumeMounts: 29 | - name: datamount 30 | mountPath: /var/opt/tableau 31 | imagePullPolicy: IfNotPresent 32 | volumes: 33 | - name: datamount 34 | persistentVolumeClaim: 35 | claimName: 36 | restartPolicy: Never 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tableau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/single-node.yml: -------------------------------------------------------------------------------- 1 | # tableau-server-in-a-container-pod.yml 2 | --- 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: tableau-server-in-a-container-deployment 7 | namespace: 8 | labels: 9 | app: 10 | spec: 11 | ports: 12 | - port: 8080 13 | targetPort: 8080 14 | protocol: TCP 15 | name: http 16 | selector: 17 | app: 18 | --- 19 | apiVersion: v1 20 | kind: ConfigMap 21 | metadata: 22 | name: configfile 23 | namespace: 24 | data: 25 | config.json: |- 26 | { 27 | "configEntities": { 28 | "identityStore": { 29 | "_type": "identityStoreType", 30 | "type": "local" 31 | } 32 | } 33 | } 34 | --- 35 | apiVersion: v1 36 | kind: PersistentVolumeClaim 37 | metadata: 38 | name: datadir 39 | namespace: 40 | spec: 41 | accessModes: 42 | - ReadWriteOnce 43 | resources: 44 | requests: 45 | storage: 20Gi 46 | --- 47 | apiVersion: apps/v1 48 | kind: Deployment 49 | metadata: 50 | name: 51 | namespace: 52 | spec: 53 | progressDeadlineSeconds: 1800 54 | selector: 55 | matchLabels: 56 | app: 57 | replicas: 1 58 | template: 59 | metadata: 60 | labels: 61 | app: 62 | spec: 63 | securityContext: 64 | runAsUser: 999 65 | fsGroup: 998 66 | fsGroupChangePolicy: "OnRootMismatch" 67 | terminationGracePeriodSeconds: 120 68 | hostname: 69 | containers: 70 | - name: 71 | image: 72 | env: 73 | - name: LICENSE_KEY 74 | valueFrom: 75 | secretKeyRef: 76 | name: tableau-server-in-a-container-secrets 77 | key: license_key 78 | - name: TABLEAU_USERNAME 79 | valueFrom: 80 | secretKeyRef: 81 | name: tableau-server-in-a-container-secrets 82 | key: tableau_username 83 | - name: TABLEAU_PASSWORD 84 | valueFrom: 85 | secretKeyRef: 86 | name: tableau-server-in-a-container-secrets 87 | key: tableau_password 88 | resources: 89 | requests: 90 | memory: 32Gi 91 | cpu: 8 92 | limits: 93 | memory: 32Gi 94 | cpu: 8 95 | ports: 96 | - containerPort: 8080 97 | volumeMounts: 98 | - name: configmount 99 | mountPath: /docker/config/config.json 100 | subPath: config.json 101 | - name: datamount 102 | mountPath: /var/opt/tableau 103 | imagePullPolicy: IfNotPresent 104 | readinessProbe: 105 | exec: 106 | command: 107 | - /bin/sh 108 | - -c 109 | - /docker/server-ready-check 110 | initialDelaySeconds: 360 111 | periodSeconds: 30 112 | timeoutSeconds: 20 113 | livenessProbe: 114 | exec: 115 | command: 116 | - /bin/sh 117 | - -c 118 | - /docker/alive-check 119 | initialDelaySeconds: 600 120 | periodSeconds: 60 121 | timeoutSeconds: 30 122 | dnsPolicy: ClusterFirst 123 | volumes: 124 | - name: configmount 125 | configMap: 126 | name: configfile 127 | - name: datamount 128 | persistentVolumeClaim: 129 | claimName: datadir 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tableau Server In Kubernetes 2 | [![Community Supported](https://img.shields.io/badge/Support%20Level-Community%20Supported-457387.svg)](https://www.tableau.com/support-levels-it-and-developer-tools) 3 | 4 | This project consists of documentation and examples demonstrating how to run Tableau Server in an existing Kubernetes cluster. Make sure the Tableau Server image used is sourced from the [Tableau Server in a Container Project](https://help.tableau.com/current/server-linux/en-us/server-in-container.htm). 5 | 6 | # Project Resources 7 | Kubernetes example templates are stored in the `templates/` directory of this project. There are single-node, multi-node, and upgrade job templates that can be used as starting points. 8 | 9 | # Requirements 10 | There are many different kinds of Kubernetes deployments which can affect how Tableau Server might be deployed in a container orchestration system. To account for this variability and give you the information to make the best decision for how to deploy Tableau Server, this section will go over the high-level requirements for what Tableau Server needs in order to run properly in a container orchestration system. 11 | 12 | ## Single Node Requirements 13 | ### Network 14 | Hostnames inside the container must be static and consistent. This means on every restart of the container or pod, the hostname in the container must stay the same. This is one of the primary reasons we recommend using the StatefulSet workload API object to deploy the Tableau Server pod. 15 | Persistent volumes that store Tableau Server state also expect to be used with the same container hostname. 16 | Tableau Server containers receive client traffic on port 8080 by default (8443 for TLS). 17 | 18 | ## Multi-Node Requirements 19 | ### Network 20 | Container short hostnames must be DNS resolvable; Tableau Server containers in a cluster will self-register and discover each other by their short hostname. This means a standard Kubernetes deployment using core-dns or kube-dns will require customizing the container's DNS policy. Check the example Kubernetes multinode configuration to see one way of handling this. The [Kubernetes documentation on DNS lookups](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/) provides more information on this topic. 21 | Using a Headless Service is recommended because every Tableau Server pod will get a DNS entry with that deployment model. The [Kubernetes documentation on headless services](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services) provides more details on how this works. 22 | 23 | ### Bootstrap file 24 | A bootstrap file must be shared between the initial node and all subsequent workers. We recommend using an NFS mount to facilitate the sharing of this file (this would enable the multi-node deployment to be fully automated). If you are using AWS you can use EFS to the same effect. Check the Multinode bootstrap file section for more details. The Kubernetes multinode template shows one possible way of handling this. 25 | 26 | # Documentation 27 | ## Status 28 | There are two status checks provided for Tableau Server. These can be used by an orchestration system, like Kubernetes, to determine whether Tableau Server is starting or not. 29 | 30 | ### Aliveness Check 31 | The aliveness check indicates whether or not TSM services are running. This means it will indicate whether the orchestrated services of Tableau Server are operating and are functioning. This check is callable here: 32 | ``` 33 | /docker/alive-check 34 | ``` 35 | Another option is to expose the TSM Controller service (running on port 8850) to provide administrative functions through a web browser. One could periodically check the health of the service by checking the health of the service through TCP health checks. 36 | 37 | ### Readiness Check 38 | The readiness check indicates whether Tableau Server is running and business services are ready to receive traffic. This can be determined using the following script: 39 | ``` 40 | /docker/server-ready-check 41 | ``` 42 | Another option is to use TCP health checks against port 8080 (or whatever port Tableau Server is bound to receive traffic). Sometimes this kind of TCP health check is more reliable than the server-ready-check, as the server-ready-check is based on service status reported to TSM which can sometimes be delayed as service state is updated. 43 | 44 | ## Resource Limits 45 | We strongly recommend that deployments to Kubernetes set appropriate resource limits for the deployed containers. Note that Tableau Server has significant resource requirements, make sure that the resource limits you specify matching at least the [Tableau Server resource requirements for testing and production](https://help.tableau.com/current/server-linux/en-us/server_hardware_min.htm). 46 | 47 | ## Network Properties 48 | Tableau Server does not handle container hostname changes well, so it is important to specify the container's internal hostname so it is consistent between container runs. 49 | 50 | Tableau Server nodes in a cluster communicate by between nodes by registering their container hostname amongst the other Tableau Server nodes. This means the container hostname must be resolvable by DNS. 51 | 52 | ## Deployment Properties 53 | We recommend using StatefulSets and persistent volume claims when deploying Tableau Server in Kubernetes. At the moment Tableau Server is a stateful application so appropriate measures should be taken to preserve and backup Tableau's application state. Future advancements will loosen these requirements. 54 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Salesforce Open Source Community Code of Conduct 2 | 3 | ## About the Code of Conduct 4 | 5 | Equality is a core value at Salesforce. We believe a diverse and inclusive 6 | community fosters innovation and creativity, and are committed to building a 7 | culture where everyone feels included. 8 | 9 | Salesforce open-source projects are committed to providing a friendly, safe, and 10 | welcoming environment for all, regardless of gender identity and expression, 11 | sexual orientation, disability, physical appearance, body size, ethnicity, nationality, 12 | race, age, religion, level of experience, education, socioeconomic status, or 13 | other similar personal characteristics. 14 | 15 | The goal of this code of conduct is to specify a baseline standard of behavior so 16 | that people with different social values and communication styles can work 17 | together effectively, productively, and respectfully in our open source community. 18 | It also establishes a mechanism for reporting issues and resolving conflicts. 19 | 20 | All questions and reports of abusive, harassing, or otherwise unacceptable behavior 21 | in a Salesforce open-source project may be reported by contacting the Salesforce 22 | Open Source Conduct Committee at ossconduct@salesforce.com. 23 | 24 | ## Our Pledge 25 | 26 | In the interest of fostering an open and welcoming environment, we as 27 | contributors and maintainers pledge to making participation in our project and 28 | our community a harassment-free experience for everyone, regardless of gender 29 | identity and expression, sexual orientation, disability, physical appearance, 30 | body size, ethnicity, nationality, race, age, religion, level of experience, education, 31 | socioeconomic status, or other similar personal characteristics. 32 | 33 | ## Our Standards 34 | 35 | Examples of behavior that contributes to creating a positive environment 36 | include: 37 | 38 | * Using welcoming and inclusive language 39 | * Being respectful of differing viewpoints and experiences 40 | * Gracefully accepting constructive criticism 41 | * Focusing on what is best for the community 42 | * Showing empathy toward other community members 43 | 44 | Examples of unacceptable behavior by participants include: 45 | 46 | * The use of sexualized language or imagery and unwelcome sexual attention or 47 | advances 48 | * Personal attacks, insulting/derogatory comments, or trolling 49 | * Public or private harassment 50 | * Publishing, or threatening to publish, others' private information—such as 51 | a physical or electronic address—without explicit permission 52 | * Other conduct which could reasonably be considered inappropriate in a 53 | professional setting 54 | * Advocating for or encouraging any of the above behaviors 55 | 56 | ## Our Responsibilities 57 | 58 | Project maintainers are responsible for clarifying the standards of acceptable 59 | behavior and are expected to take appropriate and fair corrective action in 60 | response to any instances of unacceptable behavior. 61 | 62 | Project maintainers have the right and responsibility to remove, edit, or 63 | reject comments, commits, code, wiki edits, issues, and other contributions 64 | that are not aligned with this Code of Conduct, or to ban temporarily or 65 | permanently any contributor for other behaviors that they deem inappropriate, 66 | threatening, offensive, or harmful. 67 | 68 | ## Scope 69 | 70 | This Code of Conduct applies both within project spaces and in public spaces 71 | when an individual is representing the project or its community. Examples of 72 | representing a project or community include using an official project email 73 | address, posting via an official social media account, or acting as an appointed 74 | representative at an online or offline event. Representation of a project may be 75 | further defined and clarified by project maintainers. 76 | 77 | ## Enforcement 78 | 79 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 80 | reported by contacting the Salesforce Open Source Conduct Committee 81 | at ossconduct@salesforce.com. All complaints will be reviewed and investigated 82 | and will result in a response that is deemed necessary and appropriate to the 83 | circumstances. The committee is obligated to maintain confidentiality with 84 | regard to the reporter of an incident. Further details of specific enforcement 85 | policies may be posted separately. 86 | 87 | Project maintainers who do not follow or enforce the Code of Conduct in good 88 | faith may face temporary or permanent repercussions as determined by other 89 | members of the project's leadership and the Salesforce Open Source Conduct 90 | Committee. 91 | 92 | ## Attribution 93 | 94 | This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant-home], 95 | version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. 96 | It includes adaptions and additions from [Go Community Code of Conduct][golang-coc], 97 | [CNCF Code of Conduct][cncf-coc], and [Microsoft Open Source Code of Conduct][microsoft-coc]. 98 | 99 | This Code of Conduct is licensed under the [Creative Commons Attribution 3.0 License][cc-by-3-us]. 100 | 101 | [contributor-covenant-home]: https://www.contributor-covenant.org (https://www.contributor-covenant.org/) 102 | [golang-coc]: https://golang.org/conduct 103 | [cncf-coc]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md 104 | [microsoft-coc]: https://opensource.microsoft.com/codeofconduct/ 105 | [cc-by-3-us]: https://creativecommons.org/licenses/by/3.0/us/ 106 | -------------------------------------------------------------------------------- /templates/three-node.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: 6 | namespace: 7 | spec: 8 | publishNotReadyAddresses: true 9 | selector: 10 | app: 11 | clusterIP: None 12 | --- 13 | apiVersion: v1 14 | kind: ConfigMap 15 | metadata: 16 | name: configfile 17 | namespace: 18 | data: 19 | config.json: |- 20 | { 21 | "configEntities": { 22 | "identityStore": { 23 | "_type": "identityStoreType", 24 | "type": "local" 25 | } 26 | }, 27 | "topologyVersion" : { 28 | "nodes" : { 29 | 30 | } 31 | } 32 | } 33 | --- 34 | apiVersion: v1 35 | kind: PersistentVolumeClaim 36 | metadata: 37 | name: datadir 38 | namespace: 39 | spec: 40 | accessModes: 41 | - ReadWriteOnce 42 | resources: 43 | requests: 44 | storage: 45 | --- 46 | apiVersion: v1 47 | kind: PersistentVolumeClaim 48 | metadata: 49 | name: workerdir 50 | namespace: 51 | spec: 52 | accessModes: 53 | - ReadWriteOnce 54 | resources: 55 | requests: 56 | storage: 57 | --- 58 | apiVersion: v1 59 | kind: PersistentVolumeClaim 60 | metadata: 61 | name: workerdir2 62 | namespace: 63 | spec: 64 | accessModes: 65 | - ReadWriteOnce 66 | resources: 67 | requests: 68 | storage: 69 | --- 70 | apiVersion: apps/v1 71 | kind: StatefulSet 72 | metadata: 73 | name: primary 74 | namespace: 75 | labels: 76 | app: 77 | spec: 78 | selector: 79 | matchLabels: 80 | app: 81 | replicas: 1 82 | serviceName: 83 | template: 84 | metadata: 85 | labels: 86 | app: 87 | spec: 88 | securityContext: 89 | runAsUser: 999 90 | fsGroup: 998 91 | fsGroupChangePolicy: "OnRootMismatch" 92 | terminationGracePeriodSeconds: 120 93 | dnsPolicy: "None" 94 | dnsConfig: 95 | nameservers: 96 | - 97 | searches: 98 | - ..svc.. 99 | - .svc.. 100 | - svc.. 101 | - . 102 | options: 103 | - name: ndots 104 | value: "5" 105 | containers: 106 | - name: 107 | image: 108 | env: 109 | - name: LICENSE_KEY 110 | valueFrom: 111 | secretKeyRef: 112 | name: tableau-server-in-a-container-secrets 113 | key: license_key 114 | - name: TABLEAU_USERNAME 115 | valueFrom: 116 | secretKeyRef: 117 | name: tableau-server-in-a-container-secrets 118 | key: tableau_username 119 | - name: TABLEAU_PASSWORD 120 | valueFrom: 121 | secretKeyRef: 122 | name: tableau-server-in-a-container-secrets 123 | key: tableau_password 124 | resources: 125 | requests: 126 | memory: 32Gi 127 | cpu: 8 128 | limits: 129 | memory: 32Gi 130 | cpu: 8 131 | ports: 132 | - containerPort: 8080 133 | volumeMounts: 134 | - name: configmount 135 | mountPath: /docker/config/config.json 136 | subPath: config.json 137 | - name: datamount 138 | mountPath: /var/opt/tableau 139 | - name: nfs-volume 140 | mountPath: /docker/config/bootstrap 141 | imagePullPolicy: IfNotPresent 142 | readinessProbe: 143 | exec: 144 | command: 145 | - /bin/sh 146 | - -c 147 | - /docker/server-ready-check 148 | initialDelaySeconds: 360 149 | periodSeconds: 30 150 | timeoutSeconds: 20 151 | livenessProbe: 152 | exec: 153 | command: 154 | - /bin/sh 155 | - -c 156 | - /docker/alive-check 157 | initialDelaySeconds: 600 158 | periodSeconds: 60 159 | timeoutSeconds: 30 160 | volumes: 161 | - name: configmount 162 | configMap: 163 | name: configfile 164 | - name: datamount 165 | persistentVolumeClaim: 166 | claimName: datadir 167 | - name: nfs-volume 168 | nfs: 169 | server: 170 | path: / 171 | --- 172 | apiVersion: apps/v1 173 | kind: StatefulSet 174 | metadata: 175 | name: worker 176 | namespace: 177 | labels: 178 | app: 179 | spec: 180 | selector: 181 | matchLabels: 182 | app: 183 | replicas: 1 184 | serviceName: 185 | template: 186 | metadata: 187 | labels: 188 | app: 189 | spec: 190 | securityContext: 191 | runAsUser: 999 192 | fsGroup: 998 193 | terminationGracePeriodSeconds: 120 194 | dnsPolicy: "None" 195 | dnsConfig: 196 | nameservers: 197 | - 198 | searches: 199 | - ..svc.. 200 | - .svc.. 201 | - svc.. 202 | - . 203 | options: 204 | - name: ndots 205 | value: "5" 206 | containers: 207 | - name: 208 | image: 209 | env: 210 | - name: LICENSE_KEY 211 | valueFrom: 212 | secretKeyRef: 213 | name: tableau-server-in-a-container-secrets 214 | key: license_key 215 | - name: BOOTSTRAP_INSTALL 216 | value: "1" 217 | resources: 218 | requests: 219 | memory: 32Gi 220 | cpu: 8 221 | limits: 222 | memory: 32Gi 223 | cpu: 8 224 | ports: 225 | - containerPort: 8080 226 | imagePullPolicy: IfNotPresent 227 | volumeMounts: 228 | - name: nfs-volume 229 | mountPath: /docker/config/bootstrap 230 | - name: workermount 231 | mountPath: /var/opt/tableau 232 | volumes: 233 | - name: nfs-volume 234 | nfs: 235 | server: 236 | path: / 237 | - name: workermount 238 | persistentVolumeClaim: 239 | claimName: workerdir 240 | --- 241 | apiVersion: apps/v1 242 | kind: StatefulSet 243 | metadata: 244 | name: worker2 245 | namespace: 246 | labels: 247 | app: 248 | spec: 249 | selector: 250 | matchLabels: 251 | app: 252 | replicas: 1 253 | serviceName: 254 | template: 255 | metadata: 256 | labels: 257 | app: 258 | spec: 259 | securityContext: 260 | runAsUser: 999 261 | fsGroup: 998 262 | terminationGracePeriodSeconds: 120 263 | dnsPolicy: "None" 264 | dnsConfig: 265 | nameservers: 266 | - 267 | searches: 268 | - ..svc.. 269 | - .svc.. 270 | - svc.. 271 | - . 272 | options: 273 | - name: ndots 274 | value: "5" 275 | containers: 276 | - name: 277 | image: 278 | env: 279 | - name: LICENSE_KEY 280 | valueFrom: 281 | secretKeyRef: 282 | name: tableau-server-in-a-container-secrets 283 | key: license_key 284 | - name: BOOTSTRAP_INSTALL 285 | value: "1" 286 | resources: 287 | requests: 288 | memory: 32Gi 289 | cpu: 8 290 | limits: 291 | memory: 32Gi 292 | cpu: 8 293 | ports: 294 | - containerPort: 8080 295 | imagePullPolicy: IfNotPresent 296 | volumeMounts: 297 | - name: nfs-volume 298 | mountPath: /docker/config/bootstrap 299 | - name: workermount 300 | mountPath: /var/opt/tableau 301 | volumes: 302 | - name: nfs-volume 303 | nfs: 304 | server: 305 | path: / 306 | - name: workermount 307 | persistentVolumeClaim: 308 | claimName: workerdir2 309 | --------------------------------------------------------------------------------