├── .gitignore ├── .vscode └── settings.json ├── Gruntfile.js ├── LICENSE ├── README.md ├── azure-architecture.png ├── generator.html ├── metadata.json ├── package-lock.json ├── package.json └── sources ├── app.source.js ├── arm.source.js ├── azure-vm-sizes.json ├── cloud-config.source.js ├── cloud-config ├── create-raid-array.service ├── create-raid-array.sh ├── docker-mariadb-galera.service ├── docker-mariadb-galera.sh ├── docker-mariadb-waiter.service ├── docker-mariadb-waiter.sh ├── etcd-waiter.service ├── etcd-waiter.sh ├── mnt-data.mount └── mysql_server.cnf ├── forms.source.js ├── generator.source.html ├── templates ├── base.template.json ├── storage-account.template.json └── unmanaged-disk.template.json ├── yaml-arm.source.js └── yaml.source.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Intermediary build files 2 | sources/app.build.js 3 | sources/cloud-config.pack.json 4 | 5 | 6 | # Created by https://www.gitignore.io/api/node,osx,windows,linux 7 | 8 | ### Node ### 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules 36 | jspm_packages 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional REPL history 42 | .node_repl_history 43 | 44 | 45 | ### OSX ### 46 | .DS_Store 47 | .AppleDouble 48 | .LSOverride 49 | 50 | # Icon must end with two \r 51 | Icon 52 | 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | 65 | # Directories potentially created on remote AFP share 66 | .AppleDB 67 | .AppleDesktop 68 | Network Trash Folder 69 | Temporary Items 70 | .apdisk 71 | 72 | 73 | ### Windows ### 74 | # Windows image file caches 75 | Thumbs.db 76 | ehthumbs.db 77 | 78 | # Folder config file 79 | Desktop.ini 80 | 81 | # Recycle Bin used on file shares 82 | $RECYCLE.BIN/ 83 | 84 | # Windows Installer files 85 | *.cab 86 | *.msi 87 | *.msm 88 | *.msp 89 | 90 | # Windows shortcuts 91 | *.lnk 92 | 93 | 94 | ### Linux ### 95 | *~ 96 | 97 | # temporary files which can be created if a process still has a handle open of a deleted file 98 | .fuse_hidden* 99 | 100 | # KDE directory preferences 101 | .directory 102 | 103 | # Linux trash folder which might appear on any partition or disk 104 | .Trash-* 105 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "node_modules/": true, 4 | "sources/app.build.js": true, 5 | "sources/cloud-config.pack.json": true 6 | }, 7 | "files.trimTrailingWhitespace": true, 8 | "editor.tabSize": 4, 9 | "files.insertFinalNewline": true 10 | } 11 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let co = require('co') 4 | let fs = require('mz/fs') 5 | 6 | module.exports = function(grunt) { 7 | grunt.initConfig({ 8 | pack: { 9 | dist: { 10 | src: ['sources/cloud-config/*'], 11 | dest: 'sources/cloud-config.pack.json' 12 | } 13 | }, 14 | browserify: { 15 | dist: { 16 | files: { 17 | 'sources/app.build.js': ['sources/app.source.js'] 18 | }, 19 | options: { 20 | } 21 | } 22 | }, 23 | htmlbuild: { 24 | dist: { 25 | src: 'sources/generator.source.html', 26 | dest: 'generator.html', 27 | options: { 28 | scripts: { 29 | app: 'sources/app.build.js', 30 | yaml: 'node_modules/js-yaml/dist/js-yaml.min.js' 31 | } 32 | } 33 | } 34 | }, 35 | watch: { 36 | scripts: { 37 | files: ['sources/*.source.*', 'sources/cloud-config/*', 'sources/templates/*'], 38 | tasks: ['default'] 39 | } 40 | } 41 | }) 42 | 43 | grunt.loadNpmTasks('grunt-browserify') 44 | grunt.loadNpmTasks('grunt-html-build') 45 | grunt.loadNpmTasks('grunt-contrib-watch') 46 | 47 | grunt.registerMultiTask('pack', function() { 48 | let task = this 49 | let taskDone = task.async() 50 | let files = this.files 51 | let options = this.options() 52 | 53 | co(function*() { 54 | let readPromisesList = {} 55 | for(let i = 0; i < files.length; i++) { 56 | let f = files[i] 57 | 58 | if(f.src.length) { 59 | let dest = f.dest 60 | let done = 0 61 | 62 | for(let j = 0; j < f.src.length; j++) { 63 | let filename = f.src[j].split('/').pop() 64 | let promise = Promise.resolve(true).then(res => { 65 | return fs.readFile(f.src[j], 'utf8') 66 | }) 67 | readPromisesList[filename] = promise 68 | } 69 | 70 | let pack = yield readPromisesList 71 | 72 | let packStr = JSON.stringify(pack) 73 | yield fs.writeFile(dest, packStr) 74 | } 75 | } 76 | 77 | taskDone() 78 | }) 79 | }) 80 | 81 | grunt.registerTask('default', ['pack', 'browserify', 'htmlbuild']) 82 | } 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Alessandro Segala 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MariaDB + Galera Cluster on CoreOS with Docker 2 | 3 | > **The generator web app is now hosted on GitHub pages at the URL: [http://egoalesum.github.io/mariadb-cluster/generator.html](http://egoalesum.github.io/mariadb-cluster/generator.html)** 4 | 5 | This repository contains a generator app for the Cloud Config and the Azure Resource Manager (ARM) template files, to create a MariaDB database with Galera Cluster, running inside Docker containers on CoreOS. You can read more about the architecture, and the choices behind it, on [this blog post](http://withblue.ink/2016/03/09/galera-cluster-mariadb-coreos-and-docker-part-1.html). The ARM template is a JSON file that can be deployed automatically with just a few clicks on Azure and it includes a tailored Cloud Config file. 6 | 7 | The starting point is the generator app, which is a static HTML file that runs inside any modern web browser to create ad-hoc Cloud Config and Azure ARM template files. You can use the [hosted generator app](http://egoalesum.github.io/mariadb-cluster/generator.html), or you can clone the repository locally and open `generator.html` with any web browser. 8 | 9 | 10 | ## Getting started 11 | 12 | You can use the generator app hosted on GitHub pages or clone the repository and run it locally: 13 | 14 | - **Recommended:** Use the [hosted generator app](http://egoalesum.github.io/mariadb-cluster/generator.html) 15 | - If you prefer to run the app locally, clone this repository in your local machine, then open the `generator.html` file with a web browser. Using one of the "evergreen browsers" (Edge, Chrome, Safari, Firefox) is recommended. You will need to be connected to the Internet for the web application to work properly. Please note that if you're having issues with the page not working while opening it from your local disk, you might need to manually set the "etcd2 Discovery URL" parameter, or put the page on a web server. 16 | 17 | The web application offers two modes: 18 | - **Azure Resource Manager**: in this mode, an ARM template (a JSON document) is generated, ready to be deployed to Azure. The resulting ARM template includes the Cloud Config file too. 19 | - **Only cloud-config.yaml**: generates only the Cloud Config file, in plaintext and base64-encoded. This file can be used to startup the Galera Cluster on any public/private cloud. 20 | 21 | ## Deploying to Azure 22 | 23 | The ARM template allows you to deploy a MariaDB + Galera Cluster (based on CoreOS) with a few clicks, running on the Microsoft Azure cloud. 24 | 25 | ### How to deploy the template 26 | 27 | 1. Ensure you have an active Azure subscription. You can also get a [free trial](http://azure.com/free). 28 | 2. Using the [hosted generator app](http://egoalesum.github.io/mariadb-cluster/generator.html) or the `generator.html` page on your machine, create the Azure Resource Manager template, properly configured. 29 | 3. Open the [Azure Portal](https://portal.azure.com), then press "+ New" on the top left corner, search for "Template deployment" and select the result with the same name. Then click on the "Create" button. 30 | 4. In the "Template" blade, paste the "Azure Resource Manager template" JSON document generated with the HTML app. 31 | 5. In the "Parameters" blade, leave all values to their default (the JSON you pasted has all your parameters already hardcoded as default values). 32 | 6. Select the subscription you want to deploy the cluster into, then create a new Resource Group (or choose an existing one) and pick in what Azure region you want the deployment to happen. Lastly, accept the mandatory legal terms and press Create. 33 | 7. Azure will deploy your VMs and linked resources, and then MariaDB and Galera Cluster will be started in all the VMs automatically. The duration of the setup depends a lot on the size of the attached disks; with small disks (2-4), it should last around 5 minutes. 34 | 35 | ### Architecture of deployment on Azure 36 | 37 | On the Microsoft Azure platform, the JSON template is deploying the following: 38 | 39 | ![Architecture of deployment on Azure](azure-architecture.png) 40 | 41 | 1. A Virtual Network named after the Resource Group (not in the diagram) with address space `10.0.0.0/16`. 42 | 2. A subnet `10.0.1.0/24` named `mariadb-subnet`. 43 | 3. An Azure Internal Load Balancer for the MySQL endpoints (port `3306`). The Internal Load Balancer has always the address `10.0.1.4` and no public IP. 44 | 4. The 3 or 5 nodes running the application. All VMs are named `mariadb-node-N` (where N is a number between 0 and 4), with addresses automatically assigned by DHCP (generally, the first one to deploy obtains `10.0.1.5`, and the others follow in sequence). Nodes do not have a public IP, and Network Security Group rules allow traffic to only port `3306` (MySQL) and `22` (SSH), and only from within the Virtual Network. All VMs are also deployed in an Availability Set, in order to achieve high availability. 45 | 46 | Your application can connect to the MariaDB Galera Cluster on the IP `10.0.1.4` (Internal Load Balancer) on port `3306`. Using Network Security Group rules, connections to the database are allowed only from within the Virtual Network. Connecting to the cluster using the IP of the Internal Load Balancer is recommended because it handles failover automatically; however, it's still possible to connect to individual nodes directly, for example for debug purposes. In case you need to administer the VMs using SSH, you can do so by connecting to each instance on port `22`, from another machine inside the Virtual Network, and authenticating using the public key method. 47 | 48 | The default password for the `root` user in the database is **`my-secret-pw`**; it's recommended to change it as soon as possible, using the following SQL statement: 49 | 50 | ````sql 51 | SET PASSWORD FOR 'root'@'%' = PASSWORD('newpass'); 52 | ```` 53 | 54 | Note: when using Galera Cluster, it's important not to edit the `mysql` system database, because those changes won't be replicated across the nodes. To edit users, leverage SQL statements such as `CREATE USER`, `SET PASSWORD`, etc; do not alter the `mysql.user` table directly. 55 | 56 | 57 | ## Using the Cloud Config mode 58 | 59 | If you use the "Cloud Config mode", the generator app will create only a `cloud-config.yaml` file (in plaintext and base64-encoded). You can use that file to spin up your own cluster, in any public or private cloud. 60 | 61 | There are only a few restrictions to keep in mind when designing your architecture: 62 | 63 | 1. The `cloud-config.yaml` file generated is meant to be used with CoreOS 899+ (latest Stable release as of writing); it has not been tested with any other Linux distribution, and it's likely not to work. 64 | 2. With these scripts, you can deploy up to 5 nodes in the cluster, and your nodes must be named `mariadb-node-0`, `mariadb-node-1`, etc, until `mariadb-node-4`. All VMs in the cluster must be able to connect to each other using those names, so you need to ensure that a naming resolution service exists in your infrastructure. Indeed, in the current version, the MariaDB configuration file has hardcoded the hostnames of the VMs; this design choice may change in the future, however. 65 | 3. It's strongly advised to use an odd number of nodes to avoid the risk of "split-brain conditions" (please see the [official Galera documentation](http://galeracluster.com/documentation-webpages/weightedquorum.html)). 66 | 4. The default password for the `root` user in the database is **`my-secret-pw`**; it's recommended to change it as soon as possible. 67 | 68 | > **Note:** when in "Cloud Config mode", the generated YAML file is slightly different than the one generated in "Azure Resource Manager template" mode. In the latter case, a few more units and scripts are added to attach and format data disks attached to the Azure Virtual Machines. 69 | 70 | 71 | ## Notes on parameters for the generator 72 | 73 | ### etcd2 Discovery URL 74 | 75 | An optional parameter in the generator app is the Discovery URL for etcd2. etcd2 is a distributed key/value storage that is shipped with CoreOS and on which the deployment scripts in this repository rely on. 76 | 77 | **Most users should leave the Discovery URL field empty**. When the field is not set, the generator app will request a new Discovery URL automatically on your behalf, using `http://discovery.etcd.io/`. You will need to manually set a value for this field if you are re-deploying the template in an existing, running cluster. 78 | 79 | ### SSH key 80 | 81 | The generator app requires you to specify a **SSH RSA public key**. 82 | 83 | **Linux and Mac** users can use the built-in `ssh-keygen` command line utility, which is pre-installed in OSX and most Linux distributions. Execute the following command, and when prompted save to the default location (`~/.ssh/id_rsa`): 84 | 85 | $ ssh-keygen -t rsa -b 4096 86 | 87 | Your **public** key will be located in `~/.ssh/id_rsa.pub`. 88 | 89 | **Windows** users can generate compatible keys using PuTTYgen, as shown in [this article](https://winscp.net/eng/docs/ui_puttygen). Please make sure you select "SSH-2 RSA" as type, and use 4096 bits as size for best security. 90 | 91 | 92 | ## For developers 93 | 94 | If you want to modify the generator app (for example because you want to alter the deployment scripts, systemd units, etc), you can re-compile it using Grunt. 95 | 96 | 1. Ensure Node.js 5.0 or higher is installed (it will probably work with Node.js 4.x too, but it's not tested). You can download the latest version from the [Node.js website](https://nodejs.org/). 97 | 2. Clone this git repository on your machine: `$ git clone https://github.com/EgoAleSum/mariadb-cluster.git` 98 | 3. Inside the directory where you cloned the repository, install the required npm modules: `$ npm install` 99 | 4. Rebuild the generator app with: `$ grunt`. 100 | 5. To watch for changes to source files and re-compile automatically, you can use `$ grunt watch`. 101 | 102 | Structure of the repository: 103 | 104 | - The `generator.html` app is built from files in the `sources` folder. 105 | - Inside the source folder, the `cloud-config` directory contains the raw deployment scripts, systemd units and configuration files to be copied on the VMs. Those files are then merged in a single JSON document by Grunt at "compile time". 106 | - The entry-point for the JavaScript code is the `app.source.js` file. Using Browserify, Grunt merges all JavaScript code into `app.build.js`. 107 | - Lastly, Grunt inlines all JavaScript code inside the `generator.html` file using html-build. Please note that certain third-party dependencies, such as jQuery, Bootstrap and highlight.js, are linked externally (over the Internet). 108 | - Because the `discovery.etcd.io` service doesn't support CORS (see [this issue on GitHub](https://github.com/coreos/discovery.etcd.io/issues/12)), in order for the automatic generation of Discovery URLs to work we need to proxy the request. Without a backend server for the generator app, the best solution is to use a third-party service like [CrossOrigin.me](https://crossorigin.me/). etcd2 Discovery URLs aren't particularly sensitive information, so risks associated with using an external service are minimal. If you're concerned about security, you can deploy your own CORS proxy using the open source CrossOrigin.me code on your own machines, and change the url in the `cloud-config.source.js` file. 109 | 110 | 111 | ## TODO 112 | 113 | - [ ] Support Azure Premium Storage and Managed Disks 114 | - [ ] Switch WSREP engine from rsync to xtrabackup (see https://github.com/docker-library/mariadb/pull/47) 115 | -------------------------------------------------------------------------------- /azure-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItalyPaleAle/mariadb-cluster/d63c6c50edb80e5385869fab12ab77718164d499/azure-architecture.png -------------------------------------------------------------------------------- /generator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Template generator 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 21 | 22 |
23 |

Mode

24 |
25 | Azure Resource Manager template 26 | Only cloud-config.yaml 27 |
28 | 29 |

Parameters

30 |
31 | 32 |
33 | 37 |
38 |
39 |
40 | 41 |
42 | 43 |
44 |
45 |
46 | 47 |
48 | 49 |
Stripe across N data disks, each one with 4,095GB max size and 500 IOPs. Each VM size has a limit on number of data disks; please note that this setting may limit your ability to scale down a node.
50 |
51 |
52 |
53 | 54 |
55 | 56 |
Must be 1-7 characters, lowercase characters and numbers only
57 |
58 |
59 |
60 | 61 |
62 | 63 |
ssh-keygen format
64 |
65 |
66 |
67 | 68 |
69 | 70 |
Lowercase characters, numbers and _- only. Must start with a character or underscore.
71 |
72 |
73 | 82 |
83 | 84 |
85 | 86 |
Initial number of nodes required by etcd2 for bootstrapping.
87 |
88 |
89 |
90 | 91 |
92 | 93 |
Number of vCPUs in each VM, for optimizing wsrep slave threads. If unsure, set this value to 0 (will use 1 slave thread).
94 |
95 |
96 |
97 | 98 |
99 | 103 |
You can pick a major version only; the Docker container will always pick the latest updates.
104 |
105 |
106 | 107 |

Optional parameters

108 |
109 | 110 |
111 | 112 |
Leave empty to generate automatically (recommended for new clusters). Can be obtained from http://discovery.etcd.io/new?size=x (replace "x" with the initial size of the etcd2 cluster, as necessary for bootstrapping).
113 |
114 |
115 | 116 |
117 |
118 | 119 |
120 |
121 |
122 |
123 | 124 |
125 | 128 | 129 | 130 | 131 |
132 |

Azure Resource Manager template

133 | 134 |
135 |
136 | 137 |
138 |

cloud-config.yaml

139 | 140 |
141 |
142 | 143 |
144 |

cloud-config.yaml (base64-encoded)

145 | 146 |
147 |
148 |
149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 158 | 159 | 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "itemDisplayName": "MariaDB 10.1 and Galera Cluster with CoreOS and Docker", 3 | "description": "Do not deploy this template; generate a tailored one using the \"generator.html\" web application instead! Deploy a MySQL-compatible, 3-5 nodes MariaDB cluster (using Galera Cluster), which runs in Docker container on CoreOS hosts. The cluster uses official Docker images for MariaDB 10.1, and the OS and database software update themselves regularly. The cluster is self-healing and can safely tollerate the loss of nodes.", 4 | "summary": "Self-managing, MySQL-compatible database, with 3-5 MariaDB 10.1 nodes running Galera Cluster inside Docker containers on CoreOS", 5 | "githubUsername": "EgoAleSum", 6 | "dateUpdated": "2016-04-03" 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autobuild", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Auto-build tools for the template", 6 | "author": "Alessandro Segala", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "./node_modules/.bin/grunt default", 10 | "watch": "./node_modules/.bin/grunt default watch" 11 | }, 12 | "dependencies": { 13 | "co": "^4.6.0", 14 | "grunt": "^1.0.0", 15 | "grunt-cli": "^1.2.0", 16 | "grunt-browserify": "^5.2.0", 17 | "grunt-contrib-watch": "^1.0.0", 18 | "grunt-html-build": "^0.7.0", 19 | "js-yaml": "^3.10.0", 20 | "mz": "^2.7.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sources/app.source.js: -------------------------------------------------------------------------------- 1 | // Main application 2 | 3 | 'use strict' 4 | 5 | var forms = require('./forms.source.js') 6 | var cloudConfig = require('./cloud-config.source.js') 7 | var arm = require('./arm.source.js') 8 | 9 | var _clipboard = null 10 | 11 | $(document).ready(function() { 12 | // Hide the output panel 13 | $('[gen-role="page"][gen-page="output"]').hide() 14 | 15 | // Restart button action 16 | $('[gen-role="restart-button"]').on('click', function() { 17 | $('[gen-role="page"]').show() 18 | .filter('[gen-page="output"]').hide() 19 | }) 20 | 21 | // Form mode: prepare, and set default to Azure Resource Manager template 22 | forms.prepareFormMode() 23 | forms.setFormMode('arm') 24 | 25 | // Populate all node sizes in the select and bind action to change event to populate data disk select 26 | forms.nodeSize() 27 | 28 | // Callback to show output 29 | var showOutput = function(armTemplate, yamlString, yamlB64) { 30 | // Restore clipboard.js 31 | if(_clipboard) { 32 | _clipboard.destroy() 33 | } 34 | 35 | // Restore button 36 | $('[gen-role="generate-button"]').prop('disabled', false).text('Generate') 37 | 38 | // Show the output panel 39 | $('[gen-role="page"]').hide() 40 | .filter('[gen-page="output"]').show() 41 | 42 | // Azure Resource Manager template 43 | if(armTemplate) { 44 | $('[gen-role="output"][gen-content="arm"]').show() 45 | $('[gen-role="output"][gen-content="arm"] code').text(armTemplate) 46 | } 47 | else { 48 | $('[gen-role="output"][gen-content="arm"]').hide() 49 | } 50 | 51 | // cloud-config.yaml 52 | $('[gen-role="output"][gen-content="yaml"] code').text(yamlString) 53 | $('[gen-role="output"][gen-content="yamlb64"] code').text(yamlB64) 54 | 55 | // Highlight syntax 56 | $('pre code').each(function(i, block) { 57 | hljs.highlightBlock(block) 58 | }) 59 | 60 | // Clipboard 61 | _clipboard = new Clipboard('.clipboard-btn') 62 | } 63 | 64 | // Bind to form submit action 65 | forms.formSubmit(function(formValues) { 66 | // Generate the cloud-config.yaml file 67 | cloudConfig(formValues, function(yamlString, yamlB64) { 68 | // Generate the Azure Resource Manager template if needed 69 | var armTemplate = false 70 | if(formValues.mode == 'arm') { 71 | armTemplate = arm(formValues, yamlB64) 72 | } 73 | 74 | // Show output 75 | showOutput(armTemplate, yamlString, yamlB64) 76 | }) 77 | }, function() { 78 | // Disable button on click 79 | $('[gen-role="generate-button"]').prop('disabled', true).text('Generating… Please wait') 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /sources/arm.source.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var template = require('./templates/base.template.json') 4 | var dataDiskTemplate = require('./templates/unmanaged-disk.template.json') 5 | var storageAccountTemplate = require('./templates/storage-account.template.json') 6 | 7 | // Generate Azure Resource Manager template 8 | module.exports = function(formValues, yamlB64) { 9 | var result = JSON.parse(JSON.stringify(template)) 10 | 11 | // Node size 12 | result.variables.vmSize = formValues.nodeSize 13 | 14 | // Node count 15 | result.variables.numberOfNodes = formValues.nodeCount 16 | 17 | // Storage account name prefix 18 | result.variables.storageAccountNamePrefix = formValues.storageAccountPrefix 19 | 20 | // Admin username 21 | result.variables.adminUserName = formValues.adminUsername 22 | 23 | // SSH key 24 | result.variables.sshKeyData = formValues.sshKey 25 | 26 | // cloud-config.yaml (base64) 27 | result.variables.cloudConfig = yamlB64 28 | 29 | // Data disks: create a storage account per each node, since each account can hold 40 VHDs 30 | result.resources.push(JSON.parse(JSON.stringify(storageAccountTemplate))) 31 | 32 | // Add the disks to the VM resource 33 | for(var i = 0; i < result.resources.length; i++) { 34 | var res = result.resources[i] 35 | if(res && res.type && res.type == 'Microsoft.Compute/virtualMachines') { 36 | // Dependency on the storage accounts 37 | if(!res.dependsOn) { 38 | res.dependsOn = [] 39 | } 40 | res.dependsOn.push("[concat('Microsoft.Storage/storageAccounts/', toLower( concat( copyindex(), variables('storageAccountNamePrefix'), 'vhd', uniqueString(resourceGroup().id) ) ) )]") 41 | 42 | // Attach data disks 43 | //console.log(res.properties.storageProfile) 44 | if(!res.properties.storageProfile.dataDisks) { 45 | res.properties.storageProfile.dataDisks = [] 46 | } 47 | var dataDisks = res.properties.storageProfile.dataDisks 48 | for(var lun = 0; lun < formValues.dataDisks; lun++) { 49 | var attach = JSON.parse(JSON.stringify(dataDiskTemplate)) 50 | attach.name = attach.name.replace('*', lun) 51 | attach.lun = lun 52 | attach.vhd.uri = attach.vhd.uri.replace('*', lun) 53 | dataDisks.push(attach) 54 | } 55 | 56 | break 57 | } 58 | } 59 | 60 | // Return JSON string 61 | return JSON.stringify(result, false, 2) 62 | } 63 | -------------------------------------------------------------------------------- /sources/azure-vm-sizes.json: -------------------------------------------------------------------------------- 1 | { 2 | "Standard_B1s": { "cores": 1, "memory": 1, "disks": 2, "premiumStorage": true, "series": "B" }, 3 | "Standard_B1ms": { "cores": 1, "memory": 2, "disks": 2, "premiumStorage": true, "series": "B" }, 4 | "Standard_B2s": { "cores": 2, "memory": 4, "disks": 4, "premiumStorage": true, "series": "B" }, 5 | "Standard_B2ms": { "cores": 2, "memory": 8, "disks": 4, "premiumStorage": true, "series": "B" }, 6 | "Standard_B4ms": { "cores": 4, "memory": 16, "disks": 8, "premiumStorage": true, "series": "B" }, 7 | "Standard_B8ms": { "cores": 8, "memory": 32, "disks": 16, "premiumStorage": true, "series": "B" }, 8 | 9 | "Standard_D2_v3": { "cores": 2, "memory": 8, "disks": 4, "series": "Dv3" }, 10 | "Standard_D4_v3": { "cores": 4, "memory": 16, "disks": 8, "series": "Dv3" }, 11 | "Standard_D8_v3": { "cores": 8, "memory": 32, "disks": 16, "series": "Dv3" }, 12 | "Standard_D16_v3": { "cores": 16, "memory": 64, "disks": 32, "series": "Dv3" }, 13 | "Standard_D32_v3": { "cores": 32, "memory": 128, "disks": 32, "series": "Dv3" }, 14 | "Standard_D64_v3": { "cores": 64, "memory": 256, "disks": 32, "series": "Dv3" }, 15 | 16 | "Standard_D2s_v3": { "cores": 2, "memory": 8, "disks": 4, "premiumStorage": true, "series": "Dsv3" }, 17 | "Standard_D4s_v3": { "cores": 4, "memory": 16, "disks": 8, "premiumStorage": true, "series": "Dsv3" }, 18 | "Standard_D8s_v3": { "cores": 8, "memory": 32, "disks": 16, "premiumStorage": true, "series": "Dsv3" }, 19 | "Standard_D16s_v3": { "cores": 16, "memory": 64, "disks": 32, "premiumStorage": true, "series": "Dsv3" }, 20 | "Standard_D32s_v3": { "cores": 32, "memory": 128, "disks": 32, "premiumStorage": true, "series": "Dsv3" }, 21 | "Standard_D64s_v3": { "cores": 64, "memory": 256, "disks": 32, "premiumStorage": true, "series": "Dsv3" }, 22 | 23 | "Standard_A1_v2": { "cores": 1, "memory": 2, "disks": 2, "series": "Av2" }, 24 | "Standard_A2_v2": { "cores": 2, "memory": 4, "disks": 4, "series": "Av2" }, 25 | "Standard_A4_v2": { "cores": 4, "memory": 8, "disks": 8, "series": "Av2" }, 26 | "Standard_A8_v2": { "cores": 8, "memory": 16, "disks": 16, "series": "Av2" }, 27 | "Standard_A2m_v2": { "cores": 2, "memory": 16, "disks": 2, "series": "Av2" }, 28 | "Standard_A4m_v2": { "cores": 4, "memory": 32, "disks": 4, "series": "Av2" }, 29 | "Standard_A8m_v2": { "cores": 8, "memory": 64, "disks": 16, "series": "Av2" }, 30 | 31 | "Standard_F2s_v2": { "cores": 2, "memory": 4, "disks": 4, "premiumStorage": true, "series": "Fsv2" }, 32 | "Standard_F4s_v2": { "cores": 4, "memory": 8, "disks": 8, "premiumStorage": true, "series": "Fsv2" }, 33 | "Standard_F8s_v2": { "cores": 8, "memory": 16, "disks": 16, "premiumStorage": true, "series": "Fsv2" }, 34 | "Standard_F16s_v2": { "cores": 16, "memory": 32, "disks": 32, "premiumStorage": true, "series": "Fsv2" }, 35 | "Standard_F32s_v2": { "cores": 32, "memory": 64, "disks": 32, "premiumStorage": true, "series": "Fsv2" }, 36 | "Standard_F64s_v2": { "cores": 64, "memory": 128, "disks": 32, "premiumStorage": true, "series": "Fsv2" }, 37 | "Standard_F72s_v2": { "cores": 72, "memory": 144, "disks": 32, "premiumStorage": true, "series": "Fsv2" }, 38 | 39 | "Standard_E2s_v3": { "cores": 2, "memory": 16, "disks": 4, "premiumStorage": true, "series": "Esv3" }, 40 | "Standard_E4s_v3": { "cores": 4, "memory": 32, "disks": 8, "premiumStorage": true, "series": "Esv3" }, 41 | "Standard_E8s_v3": { "cores": 8, "memory": 64, "disks": 16, "premiumStorage": true, "series": "Esv3" }, 42 | "Standard_E16s_v3": { "cores": 16, "memory": 128, "disks": 32, "premiumStorage": true, "series": "Esv3" }, 43 | "Standard_E32s_v3": { "cores": 32, "memory": 256, "disks": 32, "premiumStorage": true, "series": "Esv3" }, 44 | "Standard_E64s_v3": { "cores": 64, "memory": 432, "disks": 32, "premiumStorage": true, "series": "Esv3" }, 45 | 46 | "Standard_E2_v3": { "cores": 2, "memory": 16, "disks": 4, "series": "Ev3" }, 47 | "Standard_E4_v3": { "cores": 4, "memory": 32, "disks": 8, "series": "Ev3" }, 48 | "Standard_E8_v3": { "cores": 8, "memory": 64, "disks": 16, "series": "Ev3" }, 49 | "Standard_E16_v3": { "cores": 16, "memory": 128, "disks": 32, "series": "Ev3" }, 50 | "Standard_E32_v3": { "cores": 32, "memory": 256, "disks": 32, "series": "Ev3" }, 51 | "Standard_E64_v3": { "cores": 64, "memory": 432, "disks": 32, "series": "Ev3" }, 52 | 53 | "Standard_M64s": { "cores": 64, "memory": 1024, "disks": 64, "premiumStorage": true, "series": "M" }, 54 | "Standard_M64ms": { "cores": 64, "memory": 1792, "disks": 64, "premiumStorage": true, "series": "M" }, 55 | "Standard_M128s": { "cores": 128, "memory": 2048, "disks": 64, "premiumStorage": true, "series": "M" }, 56 | "Standard_M128ms": { "cores": 128, "memory": 3800, "disks": 64, "premiumStorage": true, "series": "M" }, 57 | 58 | "Standard_G1": { "cores": 2, "memory": 28, "disks": 8, "series": "G" }, 59 | "Standard_G2": { "cores": 4, "memory": 56, "disks": 16, "series": "G" }, 60 | "Standard_G3": { "cores": 8, "memory": 112, "disks": 32, "series": "G" }, 61 | "Standard_G4": { "cores": 16, "memory": 224, "disks": 64, "series": "G" }, 62 | "Standard_G5": { "cores": 32, "memory": 448, "disks": 64, "series": "G" }, 63 | 64 | "Standard_GS1": { "cores": 2, "memory": 28, "disks": 8, "premiumStorage": true, "series": "Gs" }, 65 | "Standard_GS2": { "cores": 4, "memory": 56, "disks": 16, "premiumStorage": true, "series": "Gs" }, 66 | "Standard_GS3": { "cores": 8, "memory": 112, "disks": 32, "premiumStorage": true, "series": "Gs" }, 67 | "Standard_GS4": { "cores": 16, "memory": 224, "disks": 64, "premiumStorage": true, "series": "Gs" }, 68 | "Standard_GS5": { "cores": 32, "memory": 448, "disks": 64, "premiumStorage": true, "series": "Gs" }, 69 | 70 | "Standard_H8": { "cores": 8, "memory": 56, "disks": 32, "series": "H" }, 71 | "Standard_H16": { "cores": 16, "memory": 112, "disks": 64, "series": "H" }, 72 | "Standard_H8m": { "cores": 8, "memory": 112, "disks": 32, "series": "H" }, 73 | "Standard_H16m": { "cores": 16, "memory": 224, "disks": 64, "series": "H" }, 74 | "Standard_H16r": { "cores": 16, "memory": 112, "disks": 64, "series": "H" }, 75 | "Standard_H16mr": { "cores": 16, "memory": 224, "disks": 64, "series": "H" }, 76 | 77 | "Standard_L4s": { "cores": 4, "memory": 32, "disks": 16, "premiumStorage": true, "series": "Ls" }, 78 | "Standard_L8s": { "cores": 8, "memory": 64, "disks": 32, "premiumStorage": true, "series": "Ls" }, 79 | "Standard_L16s": { "cores": 16, "memory": 128, "disks": 64, "premiumStorage": true, "series": "Ls" }, 80 | "Standard_L32s": { "cores": 32, "memory": 256, "disks": 64, "premiumStorage": true, "series": "Ls" }, 81 | 82 | "Standard_A1": { "cores": 1, "memory": 1.75, "disks": 2, "series": "A" }, 83 | "Standard_A2": { "cores": 2, "memory": 3.5, "disks": 4, "series": "A" }, 84 | "Standard_A3": { "cores": 4, "memory": 7, "disks": 8, "series": "A" }, 85 | "Standard_A4": { "cores": 8, "memory": 14, "disks": 16, "series": "A" }, 86 | "Standard_A5": { "cores": 2, "memory": 14, "disks": 4, "series": "A" }, 87 | "Standard_A6": { "cores": 4, "memory": 28, "disks": 8, "series": "A" }, 88 | "Standard_A7": { "cores": 8, "memory": 56, "disks": 16, "series": "A" }, 89 | "Standard_A8": { "cores": 8, "memory": 56, "disks": 16, "series": "A" }, 90 | "Standard_A9": { "cores": 16, "memory": 112, "disks": 16, "series": "A" }, 91 | "Standard_A10": { "cores": 8, "memory": 56, "disks": 16, "series": "A" }, 92 | "Standard_A11": { "cores": 16,"memory": 112, "disks": 16, "series": "A" }, 93 | 94 | "Standard_D1_v2": { "cores": 1, "memory": 3.5, "disks": 2, "series": "Dv2" }, 95 | "Standard_D2_v2": { "cores": 2, "memory": 7, "disks": 4, "series": "Dv2" }, 96 | "Standard_D3_v2": { "cores": 4, "memory": 14, "disks": 8, "series": "Dv2" }, 97 | "Standard_D4_v2": { "cores": 8, "memory": 28, "disks": 16, "series": "Dv2" }, 98 | "Standard_D5_v2": { "cores": 16, "memory": 56, "disks": 32, "series": "Dv2" }, 99 | "Standard_D11_v2": { "cores": 2, "memory": 14, "disks": 4, "series": "Dv2" }, 100 | "Standard_D12_v2": { "cores": 4, "memory": 28, "disks": 8, "series": "Dv2" }, 101 | "Standard_D13_v2": { "cores": 8, "memory": 56, "disks": 16, "series": "Dv2" }, 102 | "Standard_D14_v2": { "cores": 16, "memory": 112, "disks": 32, "series": "Dv2" }, 103 | "Standard_D15_v2": { "cores": 20, "memory": 140, "disks": 40, "series": "Dv2" }, 104 | 105 | "Standard_DS1_v2": { "cores": 1, "memory": 3.5, "disks": 2, "premiumStorage": true, "series": "DSv2" }, 106 | "Standard_DS2_v2": { "cores": 2, "memory": 7, "disks": 4, "premiumStorage": true, "series": "DSv2" }, 107 | "Standard_DS3_v2": { "cores": 4, "memory": 14, "disks": 8, "premiumStorage": true, "series": "DSv2" }, 108 | "Standard_DS4_v2": { "cores": 8, "memory": 28, "disks": 16, "premiumStorage": true, "series": "DSv2" }, 109 | "Standard_DS5_v2": { "cores": 16, "memory": 56, "disks": 32, "premiumStorage": true, "series": "DSv2" }, 110 | "Standard_DS11_v2": { "cores": 2, "memory": 14, "disks": 4, "premiumStorage": true, "series": "DSv2" }, 111 | "Standard_DS12_v2": { "cores": 4, "memory": 28, "disks": 8, "premiumStorage": true, "series": "DSv2" }, 112 | "Standard_DS13_v2": { "cores": 8, "memory": 56, "disks": 16, "premiumStorage": true, "series": "DSv2" }, 113 | "Standard_DS14_v2": { "cores": 16, "memory": 112, "disks": 32, "premiumStorage": true, "series": "DSv2" }, 114 | "Standard_DS15_v2": { "cores": 20, "memory": 140, "disks": 40, "premiumStorage": true, "series": "DSv2" }, 115 | 116 | "Standard_D1": { "cores": 1, "memory": 3.5, "disks": 2, "series": "D" }, 117 | "Standard_D2": { "cores": 2, "memory": 7, "disks": 4, "series": "D" }, 118 | "Standard_D3": { "cores": 4, "memory": 14, "disks": 8, "series": "D" }, 119 | "Standard_D4": { "cores": 8, "memory": 28, "disks": 16, "series": "D" }, 120 | "Standard_D11": { "cores": 2, "memory": 14, "disks": 4, "series": "D" }, 121 | "Standard_D12": { "cores": 4, "memory": 28, "disks": 8, "series": "D" }, 122 | "Standard_D13": { "cores": 8, "memory": 56, "disks": 16, "series": "D" }, 123 | "Standard_D14": { "cores": 16, "memory": 112, "disks": 32, "series": "D" }, 124 | 125 | "Standard_DS1": { "cores": 1, "memory": 3.5, "disks": 2, "premiumStorage": true, "series": "DS" }, 126 | "Standard_DS2": { "cores": 2, "memory": 7, "disks": 4, "premiumStorage": true, "series": "DS" }, 127 | "Standard_DS3": { "cores": 4, "memory": 14, "disks": 8, "premiumStorage": true, "series": "DS" }, 128 | "Standard_DS4": { "cores": 8, "memory": 28, "disks": 16, "premiumStorage": true, "series": "DS" }, 129 | "Standard_DS11": { "cores": 2, "memory": 14, "disks": 4, "premiumStorage": true, "series": "DS" }, 130 | "Standard_DS12": { "cores": 4, "memory": 28, "disks": 8, "premiumStorage": true, "series": "DS" }, 131 | "Standard_DS13": { "cores": 8, "memory": 56, "disks": 16, "premiumStorage": true, "series": "DS" }, 132 | "Standard_DS14": { "cores": 16, "memory": 112, "disks": 32, "premiumStorage": true, "series": "DS" }, 133 | 134 | "Standard_F1": { "cores": 1, "memory": 2, "disks": 2, "series": "F" }, 135 | "Standard_F2": { "cores": 2, "memory": 4, "disks": 4, "series": "F" }, 136 | "Standard_F4": { "cores": 4, "memory": 8, "disks": 8, "series": "F" }, 137 | "Standard_F8": { "cores": 8, "memory": 16, "disks": 16, "series": "F" }, 138 | "Standard_F16": { "cores": 16, "memory": 32, "disks": 32, "series": "F" }, 139 | 140 | "Standard_F1s": { "cores": 1, "memory": 2, "disks": 2, "premiumStorage": true, "series": "Fs" }, 141 | "Standard_F2s": { "cores": 2, "memory": 4, "disks": 4, "premiumStorage": true, "series": "Fs" }, 142 | "Standard_F4s": { "cores": 4, "memory": 8, "disks": 8, "premiumStorage": true, "series": "Fs" }, 143 | "Standard_F8s": { "cores": 8, "memory": 16, "disks": 16, "premiumStorage": true, "series": "Fs" }, 144 | "Standard_F16s": { "cores": 16, "memory": 32, "disks": 32, "premiumStorage": true, "series": "Fs" } 145 | } -------------------------------------------------------------------------------- /sources/cloud-config.source.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var forms = require('./forms.source.js') 4 | var pack = require('./cloud-config.pack.json') 5 | var yamlSource = require('./yaml.source.js') 6 | var yamlARMSource = require('./yaml-arm.source.js') 7 | var azureVMSizes = require('./azure-vm-sizes.json') 8 | 9 | // URL for the CORS proxy service, to circument the Same-Origin Policy 10 | // By default we're using the public service by CORS Anywhere 11 | // You can grab the source code for the app at https://github.com/Rob--W/cors-anywheree and run your own instance of the CORS proxy, then change the URL below 12 | var corsProxyUrl = 'https://cors-anywhere.herokuapp.com/' 13 | 14 | // Entry-point for the unit. Generate the cloud-config.yaml file 15 | var cloudConfig = function(formValues, done) { 16 | // Check if the etcd2 discovery URL has been passed 17 | if(!formValues.discoveryUrl) { 18 | // Need to request discovery url from https://discovery.etcd.io/new?size=X 19 | var nodes = formValues.nodeCount || formValues.etcdNodeCount 20 | $.ajax({ 21 | method: 'GET', 22 | url: corsProxyUrl + 'https://discovery.etcd.io/new?size='+nodes+'&_='+Date.now(), 23 | success: function(result) { 24 | // result should contain the etcd discovery url 25 | if(!result || !result.match(/^https\:\/\/discovery\.etcd\.io\/[a-f0-9]{32}$/)) { 26 | alert('Invalid response from discovery.etcd.io') 27 | forms.restoreButton() 28 | } 29 | else { 30 | // Build YAML file 31 | formValues.discoveryUrl = result 32 | buildYaml(formValues, done) 33 | } 34 | }, 35 | error: function(error) { 36 | alert('Error while requesting etcd token - try requesting it manually from https://discovery.etcd.io/new?size=X') 37 | forms.restoreButton() 38 | }, 39 | timeout: 5000 40 | }) 41 | } 42 | else { 43 | // Go straight to building the YAML file 44 | buildYaml(formValues, done) 45 | } 46 | } 47 | 48 | // Build the cloud-config.yaml file 49 | var buildYaml = function(formValues, done) { 50 | // Select the proper template 51 | var template = (formValues.mode == 'arm') ? yamlARMSource : yamlSource 52 | 53 | // Create the tree 54 | var yamlTree = JSON.parse(JSON.stringify(template.tree)) // Deep clone the object 55 | 56 | // etcd2 discovery url 57 | yamlTree.coreos.etcd2.discovery = formValues.discoveryUrl 58 | 59 | // Number of cores 60 | var vcpus = 0 61 | if(formValues.nodeSize) { 62 | var nodeSize = azureVMSizes[formValues.nodeSize] 63 | if(nodeSize && nodeSize.cores) { 64 | vcpus = nodeSize.cores 65 | } 66 | } 67 | else if(formValues.vcpuCount) { 68 | vcpus = formValues.vcpuCount 69 | } 70 | 71 | // Read files to be created and append the data to the yaml tree 72 | for(var k in template.readFiles) { 73 | if(template.readFiles.hasOwnProperty(k)) { 74 | var push = JSON.parse(JSON.stringify(template.readFiles[k])) // Deep clone the object 75 | push.content = pack[k] 76 | 77 | if(k == 'mysql_server.cnf') { 78 | push.content = push.content.replace('{WSREP_SLAVE_THREADS}', (2 * vcpus) || 1) 79 | } 80 | else if(k == 'docker-mariadb-galera.sh' || k == 'docker-mariadb-waiter.sh') { 81 | push.content = push.content.replace('{MARIADB_VERSION}', formValues.mariaDBVersion) 82 | } 83 | 84 | yamlTree.write_files.push(push) 85 | } 86 | } 87 | 88 | // systemd units 89 | for(var i = 0, len = template.units.length; i < len; i++) { 90 | var unit = template.units[i] 91 | 92 | var push = JSON.parse(JSON.stringify(unit)) // Deep clone the object 93 | delete push.source 94 | delete push['drop-ins-source'] 95 | 96 | if(unit['drop-ins-source']) { 97 | push['drop-ins'] = [] 98 | for(var k in unit['drop-ins-source']) { 99 | if(unit['drop-ins-source'].hasOwnProperty(k)) { 100 | push['drop-ins'].push({ 101 | name: k, 102 | content: pack[k] 103 | }) 104 | } 105 | } 106 | } 107 | if(unit.source) { 108 | push.content = pack[unit.source] 109 | } 110 | 111 | yamlTree.coreos.units.push(push) 112 | } 113 | 114 | // Generate YAML document 115 | var yamlString = "#cloud-config\n\n" + jsyaml.safeDump(yamlTree, {lineWidth: -1}) 116 | 117 | // Convert to base64 (note: this does NOT support UTF-8) 118 | var yamlB64 = btoa(yamlString) 119 | 120 | done(yamlString, yamlB64) 121 | } 122 | 123 | module.exports = cloudConfig 124 | -------------------------------------------------------------------------------- /sources/cloud-config/create-raid-array.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Create RAID array on data disks 3 | 4 | Before=mnt-data.mount 5 | 6 | [Service] 7 | Type=oneshot 8 | RemainAfterExit=yes 9 | ExecStart=/usr/bin/bash /opt/bin/create-raid-array.sh 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | RequiredBy=mnt-data.mount 14 | -------------------------------------------------------------------------------- /sources/cloud-config/create-raid-array.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "create-raid-array.sh started" 4 | 5 | # Execute this only if the md0 raid array doesn't exist already 6 | lsblk | grep \\-md0 7 | if [ $? -ne 0 ]; then 8 | 9 | echo "Need to create raid array" 10 | 11 | # List disks that need to be formatted 12 | DISKS=$(lsblk | awk '/^sd/ { a[$1] = 1 } /^.-/ { sub(/[`|]-/, "", $1) ; sub(/[0-9]/, "", $1) ; a[$1] = 0 } END { for(x in a) { if(a[x]) { print x }} } ') 13 | COUNT=$(echo $DISKS | wc -w) 14 | 15 | echo "$COUNT disks: $DISKS \nRun fdisk" 16 | 17 | # Format all disks 18 | for i in $DISKS; do 19 | echo "n 20 | p 21 | 1 22 | 23 | 24 | t 25 | fd 26 | w 27 | " | fdisk "/dev/$i"; done 28 | 29 | # Create the raid array md0 30 | PARTITIONS="" 31 | for i in $DISKS; do 32 | PARTITIONS+="/dev/${i}1 " 33 | done 34 | echo "Run mdadm on $PARTITIONS" 35 | mdadm --create /dev/md0 --level 0 --raid-devices $COUNT $PARTITIONS 36 | 37 | # Crete an ext4 partition on md0 38 | echo "Create ext4 volume" 39 | # TODO: Test this: "-E lazy_itable_init" 40 | mkfs -t ext4 /dev/md0 41 | 42 | echo "Done!" 43 | 44 | # If md0 exists already 45 | else 46 | 47 | echo "Raid array created already. Nothing to do here" 48 | 49 | fi 50 | -------------------------------------------------------------------------------- /sources/cloud-config/docker-mariadb-galera.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=MariaDB with Galera Cluster 3 | 4 | Requires=docker.service 5 | Wants=network-online.target 6 | After=docker.service network-online.target 7 | 8 | [Service] 9 | Restart=always 10 | RestartSec=3 11 | TimeoutStartSec=0 12 | 13 | ExecStartPre=/usr/bin/chmod +x /opt/bin/docker-mariadb-galera.sh 14 | ExecStart=/opt/bin/docker-mariadb-galera.sh 15 | 16 | ExecStop=/bin/bash -c " \ 17 | HOSTNAME=$(/bin/hostname) \ 18 | HOSTNUM=$(/bin/expr $HOSTNAME : '.*\\([0-9]\\)') \ 19 | /usr/bin/docker stop mariadb-container-$HOSTNUM || true \ 20 | /usr/bin/docker rm mariadb-container-$HOSTNUM || true \ 21 | " 22 | 23 | [Install] 24 | WantedBy=multi-user.target 25 | -------------------------------------------------------------------------------- /sources/cloud-config/docker-mariadb-galera.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Parameters 4 | DATA_DIR="/mnt/data" 5 | IMAGE_NAME="mariadb:{MARIADB_VERSION}" 6 | 7 | # Get the hostname of the VM 8 | HOSTNAME=$(hostname) 9 | 10 | # Get the ID of the VM from the hostname (single-digit) 11 | HOSTNUM=$(expr "$HOSTNAME" : '.*\([0-9]\)') 12 | 13 | # Get the IP of the eth0 interface 14 | HOSTIP=$(ip -4 addr ls eth0 | awk '/inet / {print $2}' | cut -d"/" -f1) 15 | 16 | # Ensure the folder for MariaDB exists 17 | mkdir -p $DATA_DIR 18 | 19 | # Remove pre-existing containers 20 | docker kill mariadb-container-$HOSTNUM || true 21 | docker rm mariadb-container-$HOSTNUM || true 22 | 23 | # Pull the latest version of the Docker image 24 | docker pull $IMAGE_NAME 25 | 26 | # Check if the cluster is marked as initialized 27 | curl --silent --fail http://127.0.0.1:4001/v2/keys/mariadb-galera/initialized > /dev/null 28 | if [ $? -ne 0 ]; then 29 | echo "Cluster needs to be initialized" 30 | 31 | # Check if this is the first VM (hostname ends in "node-0") that will initialize the cluster 32 | if [[ "$HOSTNAME" == *node-0 ]]; then 33 | echo "VM $HOSTNAME is the first node ($HOSTIP)" 34 | 35 | # Start the Docker container 36 | echo "Starting Docker container (first node)" 37 | 38 | docker run \ 39 | --name mariadb-container-$HOSTNUM \ 40 | -v /opt/mysql.conf.d:/etc/mysql/conf.d \ 41 | -v $DATA_DIR:/var/lib/mysql \ 42 | -e MYSQL_INITDB_SKIP_TZINFO=yes \ 43 | -e MYSQL_ROOT_PASSWORD=my-secret-pw \ 44 | -p 3306:3306 \ 45 | -p 4567:4567/udp \ 46 | -p 4567-4568:4567-4568 \ 47 | -p 4444:4444 \ 48 | $IMAGE_NAME \ 49 | --wsrep-new-cluster \ 50 | --wsrep_node_address=$HOSTIP 51 | else 52 | echo "This is not the first node: $HOSTNAME ($HOSTIP)" 53 | 54 | # Wait for the cluster to be initialized 55 | until curl --fail http://127.0.0.1:4001/v2/keys/mariadb-galera/initialized; do 56 | echo "Waiting for initialization..." 57 | sleep 2 58 | done 59 | 60 | # Touch the mysql data folder so the database is not re-initialized 61 | mkdir -p $DATA_DIR/mysql 62 | 63 | # Start container 64 | echo "Starting Docker container" 65 | docker run \ 66 | --name mariadb-container-$HOSTNUM \ 67 | -v /opt/mysql.conf.d:/etc/mysql/conf.d \ 68 | -v $DATA_DIR:/var/lib/mysql \ 69 | -p 3306:3306 \ 70 | -p 4567:4567/udp \ 71 | -p 4567-4568:4567-4568 \ 72 | -p 4444:4444 \ 73 | $IMAGE_NAME \ 74 | --wsrep_node_address=$HOSTIP 75 | fi 76 | else 77 | echo "Cluster is already initialized. Node: $HOSTNAME ($HOSTIP)" 78 | 79 | echo "Starting Docker container" 80 | docker run \ 81 | --name mariadb-container-$HOSTNUM \ 82 | -v /opt/mysql.conf.d:/etc/mysql/conf.d \ 83 | -v $DATA_DIR:/var/lib/mysql \ 84 | -p 3306:3306 \ 85 | -p 4567:4567/udp \ 86 | -p 4567-4568:4567-4568 \ 87 | -p 4444:4444 \ 88 | $IMAGE_NAME \ 89 | --wsrep_node_address=$HOSTIP 90 | fi 91 | -------------------------------------------------------------------------------- /sources/cloud-config/docker-mariadb-waiter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Wait for MariaDB to be initialized, then update status in etcd 3 | 4 | Requires=docker.service docker-mariadb-galera.service 5 | Wants=network-online.target 6 | After=docker.service docker-mariadb-galera.service network-online.target 7 | 8 | [Service] 9 | Type=simple 10 | RemainAfterExit=true 11 | ExecStartPre=/usr/bin/chmod +x /opt/bin/docker-mariadb-waiter.sh 12 | ExecStart=/usr/bin/bash /opt/bin/docker-mariadb-waiter.sh 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /sources/cloud-config/docker-mariadb-waiter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script updates the status in etcd only after MariaDB is actually initialized and ready to accept connections 4 | 5 | # Parameters 6 | IMAGE_NAME="mariadb:{MARIADB_VERSION}" 7 | 8 | # Check if the cluster is marked as initialized. If it's already initialized, then just exit successfully 9 | curl --silent --fail http://127.0.0.1:4001/v2/keys/mariadb-galera/initialized > /dev/null 10 | if [ $? -ne 0 ]; then 11 | sleep 10 12 | 13 | while ! docker run --rm $IMAGE_NAME mysqladmin ping -h mariadb-node-0 --silent; do 14 | sleep 5 15 | done 16 | 17 | # Wait 5 more seconds before sending the green light 18 | sleep 5 19 | 20 | # Server is ready: other nodes can now connect 21 | curl -L http://127.0.0.1:4001/v2/keys/mariadb-galera/initialized -XPUT -d value="true" 22 | fi 23 | -------------------------------------------------------------------------------- /sources/cloud-config/etcd-waiter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Wait for etcd2 to be online 3 | 4 | Wants=network-online.target etcd2.service 5 | After=network-online.target etcd2.service 6 | Before=docker.service fleet.service 7 | 8 | [Service] 9 | Type=oneshot 10 | RemainAfterExit=true 11 | ExecStartPre=/usr/bin/chmod +x /opt/bin/etcd-waiter.sh 12 | ExecStart=/usr/bin/bash /opt/bin/etcd-waiter.sh 13 | 14 | [Install] 15 | RequiredBy=docker.service 16 | -------------------------------------------------------------------------------- /sources/cloud-config/etcd-waiter.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/bash 2 | until curl http://127.0.0.1:4001/v2/machines; do sleep 2; done 3 | -------------------------------------------------------------------------------- /sources/cloud-config/mnt-data.mount: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Mount RAID array in /mnt/data 3 | 4 | Requires=create-raid-array.service 5 | After=create-raid-array.service 6 | Before=docker-mariadb-galera.service 7 | 8 | [Mount] 9 | What=/dev/md0 10 | Where=/mnt/data 11 | Type=ext4 12 | 13 | [Install] 14 | RequiredBy=docker-mariadb-galera.service 15 | -------------------------------------------------------------------------------- /sources/cloud-config/mysql_server.cnf: -------------------------------------------------------------------------------- 1 | [server] 2 | bind-address=0.0.0.0 3 | binlog_format=row 4 | default_storage_engine=InnoDB 5 | innodb_autoinc_lock_mode=2 6 | innodb_locks_unsafe_for_binlog=1 7 | query_cache_size=0 8 | query_cache_type=0 9 | 10 | [galera] 11 | wsrep_on=ON 12 | wsrep_provider="/usr/lib/galera/libgalera_smm.so" 13 | wsrep_cluster_address="gcomm://mariadb-node-0,mariadb-node-1,mariadb-node-2,mariadb-node-3,mariadb-node-4" 14 | wsrep-sst-method=rsync 15 | 16 | # Optional setting 17 | wsrep_slave_threads={WSREP_SLAVE_THREADS} 18 | #innodb_flush_log_at_trx_commit=0 19 | -------------------------------------------------------------------------------- /sources/forms.source.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var azureVMSizes = require('./azure-vm-sizes.json') 4 | 5 | var formMode = false 6 | 7 | var setFormMode = function(newMode) { 8 | // Reset visual state, then set the proper one 9 | $('[gen-role="mode"]').removeClass('active') 10 | .filter('[gen-mode="'+newMode+'"]').addClass('active') 11 | $('[gen-role="conditional-param"]').hide() 12 | .filter('[gen-mode="'+newMode+'"]').show() 13 | $('[gen-role="conditional-param"] input').prop('disabled', true) 14 | .filter('[gen-mode="'+newMode+'"] input').prop('disabled', false) 15 | $('[gen-role="conditional-param"] select').prop('disabled', true) 16 | .filter('[gen-mode="'+newMode+'"] select').prop('disabled', false) 17 | $('[gen-role="conditional-param"] textarea').prop('disabled', true) 18 | .filter('[gen-mode="'+newMode+'"] textarea').prop('disabled', false) 19 | 20 | // Set value 21 | formMode = newMode 22 | } 23 | 24 | var prepareFormMode = function() { 25 | // Add callbacks 26 | $('[gen-role="mode"]').on('click', function(event) { 27 | event.preventDefault() 28 | 29 | setFormMode($(this).attr('gen-mode')) 30 | }) 31 | } 32 | 33 | var dataDiskUpdate = function(sizeName) { 34 | // Maximum number of data disks, limited to 40 (max for a storage account) 35 | var max = 2 36 | var size = azureVMSizes[sizeName] 37 | if(size && size.disks) { 38 | max = size.disks 39 | } 40 | if(max > 40) { 41 | max = 40 42 | } 43 | 44 | // Add options 45 | var $dataDiskSelect = $('#data-disks') 46 | $dataDiskSelect.empty() 47 | for(var i = 2; i <= max; i++) { 48 | $dataDiskSelect.append('') 49 | } 50 | 51 | // Set value to the maximum one 52 | $dataDiskSelect.val(max) 53 | } 54 | 55 | var nodeSize = function() { 56 | // Populate all nodes in the select 57 | var $nodeSizeSelect = $('#node-size') 58 | var lastSeries = '' 59 | var allOptions = '' 60 | for(var k in azureVMSizes) { 61 | if(azureVMSizes.hasOwnProperty(k)) { 62 | if(azureVMSizes[k].series != lastSeries) { 63 | if(lastSeries) { 64 | allOptions += '' 65 | } 66 | lastSeries = azureVMSizes[k].series 67 | allOptions += '' 68 | } 69 | allOptions += '' 70 | } 71 | } 72 | allOptions += '' 73 | $nodeSizeSelect.append(allOptions) 74 | 75 | // Bind action to change event, to update select for data disk count 76 | $nodeSizeSelect.on('change', function() { 77 | dataDiskUpdate($nodeSizeSelect.val()) 78 | }) 79 | 80 | // Initial population of data disk count 81 | dataDiskUpdate($nodeSizeSelect.val()) 82 | } 83 | 84 | var formSubmit = function(done, click) { 85 | var $form = $('#generator-form') 86 | $form.submit(function(event) { 87 | // Prevent submission 88 | event.preventDefault() 89 | 90 | // Remove error messages from all groups 91 | $('.form-group').removeClass('has-error') 92 | 93 | // Collect all values and make sure they're correct 94 | var formValues = { 95 | mode: formMode 96 | } 97 | 98 | // For mode ARM 99 | if(formMode == 'arm') { 100 | // Number of nodes 101 | var $nodeCount = $('#node-count', $form) 102 | formValues.nodeCount = parseInt($nodeCount.val()) 103 | if(!(~[3,5].indexOf(formValues.nodeCount))) { 104 | $nodeCount.parents('.form-group').addClass('has-error') 105 | return false 106 | } 107 | 108 | // Node size 109 | var $nodeSize = $('#node-size', $form) 110 | formValues.nodeSize = $nodeSize.val() + '' 111 | if(!(~Object.keys(azureVMSizes).indexOf(formValues.nodeSize))) { 112 | $nodeSize.parents('.form-group').addClass('has-error') 113 | return false 114 | } 115 | 116 | // Data disks 117 | var $dataDisks = $('#data-disks', $form) 118 | formValues.dataDisks = parseInt($dataDisks.val()) 119 | if(formValues.dataDisks < 2 || formValues.dataDisks > azureVMSizes[formValues.nodeSize].disks || formValues.dataDisks > 40) { 120 | $dataDisks.parents('.form-group').addClass('has-error') 121 | return false 122 | } 123 | 124 | // Storage account name prefix 125 | var $storageAccountPrefix = $('#storage-account-prefix', $form) 126 | formValues.storageAccountPrefix = ($storageAccountPrefix.val() + '').trim() 127 | if(!formValues.storageAccountPrefix.match(/^[0-9a-z]{1,7}$/)) { 128 | $storageAccountPrefix.parents('.form-group').addClass('has-error') 129 | return false 130 | } 131 | 132 | // SSH key 133 | var $sshKey = $('#ssh-key', $form) 134 | formValues.sshKey = ($sshKey.val() + '').trim() 135 | if(!formValues.sshKey.match(/^ssh-(rsa|dss) AAAA[0-9A-Za-z+/]+[=]{0,3}/)) { 136 | $sshKey.parents('.form-group').addClass('has-error') 137 | return false 138 | } 139 | 140 | // Admin username 141 | var $adminUsername = $('#admin-username', $form) 142 | formValues.adminUsername = ($adminUsername.val() + '').trim() 143 | if(!formValues.adminUsername.match(/^[a-z_][a-z0-9_-]*$/)) { 144 | $adminUsername.parents('.form-group').addClass('has-error') 145 | return false 146 | } 147 | } 148 | else if(formMode == 'cloudconfig') { 149 | // vCPU count 150 | var $vcpuCount = $('#vcpu-count', $form) 151 | formValues.vcpuCount = parseInt($vcpuCount.val()) 152 | if(formValues.vcpuCount < 0 || formValues.vcpuCount > 32) { 153 | $vcpuCount.parents('.form-group').addClass('has-error') 154 | return false 155 | } 156 | 157 | // etcd2 node count 158 | var $etcdNodeCount = $('#etcd-node-count', $form) 159 | formValues.etcdNodeCount = parseInt($etcdNodeCount.val()) 160 | if(formValues.etcdNodeCount < 1) { 161 | $etcdNodeCount.parents('.form-group').addClass('has-error') 162 | return false 163 | } 164 | } 165 | 166 | // MariaDB Version 167 | var $mariaDBVersion = $('#mariadb-version', $form) 168 | formValues.mariaDBVersion = ($mariaDBVersion.val() + '').trim() 169 | if(!formValues.mariaDBVersion) { 170 | $mariaDBVersion.parents('.form-group').addClass('has-error') 171 | return false 172 | } 173 | 174 | // etcd discovery URL 175 | var $discoveryUrl = $('#etcd-discovery-url', $form) 176 | formValues.discoveryUrl = ($discoveryUrl.val() + '').trim() 177 | if(!formValues.discoveryUrl) { 178 | // Empty = auto-generate 179 | formValues.discoveryUrl = false 180 | } 181 | else if(!formValues.discoveryUrl.match(/^https\:\/\/discovery\.etcd\.io\/[a-f0-9]{32}$/)) { 182 | $discoveryUrl.parents('.form-group').addClass('has-error') 183 | return false 184 | } 185 | 186 | // Invoke click callback if necessary 187 | if(click) { 188 | click() 189 | } 190 | 191 | // Invoke callback 192 | if(done) { 193 | done(formValues) 194 | } 195 | 196 | // Prevent submission 197 | return false 198 | }) 199 | } 200 | 201 | var restoreButton = function() { 202 | $('[gen-role="generate-button"]').prop('disabled', false).text('Generate') 203 | } 204 | 205 | module.exports = { 206 | prepareFormMode: prepareFormMode, 207 | setFormMode: setFormMode, 208 | nodeSize: nodeSize, 209 | formSubmit: formSubmit, 210 | restoreButton: restoreButton 211 | } 212 | -------------------------------------------------------------------------------- /sources/generator.source.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Template generator 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 21 | 22 |
23 |

Mode

24 |
25 | Azure Resource Manager template 26 | Only cloud-config.yaml 27 |
28 | 29 |

Parameters

30 |
31 | 32 |
33 | 37 |
38 |
39 |
40 | 41 |
42 | 43 |
44 |
45 |
46 | 47 |
48 | 49 |
Stripe across N data disks, each one with 4,095GB max size and 500 IOPs. Each VM size has a limit on number of data disks; please note that this setting may limit your ability to scale down a node.
50 |
51 |
52 |
53 | 54 |
55 | 56 |
Must be 1-7 characters, lowercase characters and numbers only
57 |
58 |
59 |
60 | 61 |
62 | 63 |
ssh-keygen format
64 |
65 |
66 |
67 | 68 |
69 | 70 |
Lowercase characters, numbers and _- only. Must start with a character or underscore.
71 |
72 |
73 | 82 |
83 | 84 |
85 | 86 |
Initial number of nodes required by etcd2 for bootstrapping.
87 |
88 |
89 |
90 | 91 |
92 | 93 |
Number of vCPUs in each VM, for optimizing wsrep slave threads. If unsure, set this value to 0 (will use 1 slave thread).
94 |
95 |
96 |
97 | 98 |
99 | 103 |
You can pick a major version only; the Docker container will always pick the latest updates.
104 |
105 |
106 | 107 |

Optional parameters

108 |
109 | 110 |
111 | 112 |
Leave empty to generate automatically (recommended for new clusters). Can be obtained from http://discovery.etcd.io/new?size=x (replace "x" with the initial size of the etcd2 cluster, as necessary for bootstrapping).
113 |
114 |
115 | 116 |
117 |
118 | 119 |
120 |
121 |
122 |
123 | 124 |
125 | 128 | 129 | 130 | 131 |
132 |

Azure Resource Manager template

133 | 134 |
135 |
136 | 137 |
138 |

cloud-config.yaml

139 | 140 |
141 |
142 | 143 |
144 |

cloud-config.yaml (base64-encoded)

145 | 146 |
147 |
148 |
149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /sources/templates/base.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "variables": { 5 | "vmSize": "", 6 | "storageAccountNamePrefix": "", 7 | "numberOfNodes": 0, 8 | "adminUserName": "", 9 | "sshKeyData": "", 10 | "cloudConfig": "", 11 | "securityGroupName": "mariadb-nsg", 12 | "virtualNetworkName": "[concat( resourceGroup().name, '-VNet' )]", 13 | "addressPrefix": "10.0.0.0/16", 14 | "subnet1Name": "mariadb-subnet", 15 | "subnet1Prefix": "10.0.1.0/24", 16 | "subnet1Ref": "[concat(resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName')), '/subnets/', variables('subnet1Name'))]", 17 | "vmNamePrefix": "mariadb-node-", 18 | "osImageSku": "Stable", 19 | "nicNamePrefix": "mariadb-nic-", 20 | "availabilitySetName": "mariadb-as", 21 | "vhdStorageAccountName": "[toLower( concat( variables('storageAccountNamePrefix'), 'vhdo', uniqueString(resourceGroup().id) ) )]", 22 | "vhdStorageAccountType": "Standard_LRS", 23 | "diagStorageAccountName": "[toLower( concat( variables('storageAccountNamePrefix'), 'diag', uniqueString(resourceGroup().id) ) )]", 24 | "diagStorageAccountType": "Standard_LRS", 25 | "sshKeyPath": "[concat('/home/', variables('adminUsername'),'/.ssh/authorized_keys')]", 26 | "loadBalancerIp": "10.0.1.4", 27 | "loadBalancerName": "mariadb-lb", 28 | "backendPoolName": "BackendPoolMySQL", 29 | "backendPoolRef": "[concat(resourceId('Microsoft.Network/loadBalancers/', variables('loadBalancerName')), '/backendAddressPools/', variables('backendPoolName'))]", 30 | "lbRef": "[resourceId('Microsoft.Network/loadBalancers', variables('loadBalancerName'))]", 31 | "lbRuleName": "MySQL-Rule", 32 | "lbProbeName": "MySQL-Probe" 33 | }, 34 | "resources": [ 35 | { 36 | "apiVersion": "2015-06-15", 37 | "type": "Microsoft.Storage/storageAccounts", 38 | "name": "[variables('vhdStorageAccountName')]", 39 | "location": "[resourceGroup().location]", 40 | "properties": { 41 | "accountType": "[variables('vhdStorageAccountType')]" 42 | } 43 | }, 44 | { 45 | "apiVersion": "2015-06-15", 46 | "type": "Microsoft.Storage/storageAccounts", 47 | "name": "[variables('diagStorageAccountName')]", 48 | "location": "[resourceGroup().location]", 49 | "properties": { 50 | "accountType": "[variables('diagStorageAccountType')]" 51 | } 52 | }, 53 | { 54 | "apiVersion": "2015-06-15", 55 | "type": "Microsoft.Network/networkSecurityGroups", 56 | "name": "[variables('securityGroupName')]", 57 | "location": "[resourceGroup().location]", 58 | "properties": { 59 | "securityRules": [ 60 | { 61 | "name": "SSH", 62 | "properties": { 63 | "description": "Allows SSH traffic from the Virtual Network", 64 | "protocol": "Tcp", 65 | "sourcePortRange": "*", 66 | "sourceAddressPrefix": "VirtualNetwork", 67 | "destinationPortRange": "22", 68 | "destinationAddressPrefix": "*", 69 | "access": "Allow", 70 | "priority": 100, 71 | "direction": "Inbound" 72 | } 73 | }, 74 | { 75 | "name": "MySQL", 76 | "properties": { 77 | "description": "Allows MySQL traffic from the Virtual Network", 78 | "protocol": "Tcp", 79 | "sourcePortRange": "*", 80 | "sourceAddressPrefix": "VirtualNetwork", 81 | "destinationPortRange": "3306", 82 | "destinationAddressPrefix": "*", 83 | "access": "Allow", 84 | "priority": 200, 85 | "direction": "Inbound" 86 | } 87 | } 88 | ] 89 | } 90 | }, 91 | { 92 | "apiVersion": "2015-06-15", 93 | "type": "Microsoft.Network/virtualNetworks", 94 | "name": "[variables('virtualNetworkName')]", 95 | "location": "[resourceGroup().location]", 96 | "properties": { 97 | "addressSpace": { 98 | "addressPrefixes": [ 99 | "[variables('addressPrefix')]" 100 | ] 101 | }, 102 | "subnets": [ 103 | { 104 | "name": "[variables('subnet1Name')]", 105 | "properties": { 106 | "addressPrefix": "[variables('subnet1Prefix')]" 107 | } 108 | } 109 | ] 110 | } 111 | }, 112 | { 113 | "apiVersion": "2015-06-15", 114 | "type": "Microsoft.Network/networkInterfaces", 115 | "name": "[concat(variables('nicNamePrefix'), copyindex())]", 116 | "copy": { 117 | "name": "nicLoop", 118 | "count": "[variables('numberOfNodes')]" 119 | }, 120 | "location": "[resourceGroup().location]", 121 | "dependsOn": [ 122 | "[concat('Microsoft.Network/loadBalancers/', variables('loadBalancerName'))]", 123 | "[concat('Microsoft.Network/networkSecurityGroups/', variables('securityGroupName'))]", 124 | "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" 125 | ], 126 | "properties": { 127 | "ipConfigurations": [ 128 | { 129 | "name": "ipconfig1", 130 | "properties": { 131 | "privateIPAllocationMethod": "Dynamic", 132 | "subnet": { 133 | "id": "[variables('subnet1Ref')]" 134 | }, 135 | "loadBalancerBackendAddressPools": [ 136 | { 137 | "id": "[variables('backendPoolRef')]" 138 | } 139 | ] 140 | } 141 | } 142 | ], 143 | "networkSecurityGroup": { 144 | "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('securityGroupName'))]" 145 | } 146 | } 147 | }, 148 | { 149 | "apiVersion": "2015-06-15", 150 | "type": "Microsoft.Compute/availabilitySets", 151 | "name": "[variables('availabilitySetName')]", 152 | "location": "[resourceGroup().location]" 153 | }, 154 | { 155 | "apiVersion": "2015-06-15", 156 | "type": "Microsoft.Network/loadBalancers", 157 | "name": "[variables('loadBalancerName')]", 158 | "location": "[resourceGroup().location]", 159 | "dependsOn": [ 160 | "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" 161 | ], 162 | "properties": { 163 | "frontendIPConfigurations": [ 164 | { 165 | "name": "LoadBalancerFrontend", 166 | "properties": { 167 | "subnet": { 168 | "id": "[variables('subnet1Ref')]" 169 | }, 170 | "privateIPAddress": "[variables('loadBalancerIp')]", 171 | "privateIPAllocationMethod": "Static" 172 | } 173 | } 174 | ], 175 | "backendAddressPools": [ 176 | { 177 | "name": "[variables('backendPoolName')]" 178 | } 179 | ], 180 | "loadBalancingRules": [ 181 | { 182 | "name": "[variables('lbRuleName')]", 183 | "properties": { 184 | "frontendIPConfiguration": { 185 | "id": "[concat(variables('lbRef'), '/frontendIpConfigurations/LoadBalancerFrontend')]" 186 | }, 187 | "backendAddressPool": { 188 | "id": "[concat(variables('lbRef'), '/backendAddressPools/', variables('backendPoolName'))]" 189 | }, 190 | "probe": { 191 | "id": "[concat(variables('lbRef'), '/probes/', variables('lbProbeName'))]" 192 | }, 193 | "protocol": "Tcp", 194 | "frontendPort": 3306, 195 | "backendPort": 3306, 196 | "idleTimeoutInMinutes": 15 197 | } 198 | } 199 | ], 200 | "probes": [ 201 | { 202 | "name": "[variables('lbProbeName')]", 203 | "properties": { 204 | "protocol": "Tcp", 205 | "port": 3306, 206 | "intervalInSeconds": 15, 207 | "numberOfProbes": 2 208 | } 209 | } 210 | ] 211 | } 212 | }, 213 | { 214 | "apiVersion": "2015-06-15", 215 | "type": "Microsoft.Compute/virtualMachines", 216 | "name": "[concat(variables('vmNamePrefix'), copyindex())]", 217 | "copy": { 218 | "name": "vmLoop", 219 | "count": "[variables('numberOfNodes')]" 220 | }, 221 | "location": "[resourceGroup().location]", 222 | "dependsOn": [ 223 | "[concat('Microsoft.Storage/storageAccounts/', variables('vhdStorageAccountName'))]", 224 | "[concat('Microsoft.Storage/storageAccounts/', variables('diagStorageAccountName'))]", 225 | "[concat('Microsoft.Compute/availabilitySets/', variables('availabilitySetName'))]", 226 | "[concat('Microsoft.Network/networkInterfaces/', variables('nicNamePrefix'), copyindex())]" 227 | ], 228 | "properties": { 229 | "availabilitySet": { 230 | "id": "[resourceId('Microsoft.Compute/availabilitySets',variables('availabilitySetName'))]" 231 | }, 232 | "hardwareProfile": { 233 | "vmSize": "[variables('vmSize')]" 234 | }, 235 | "osProfile": { 236 | "computerName": "[concat(variables('vmNamePrefix'), copyindex())]", 237 | "adminUsername": "[variables('adminUsername')]", 238 | "customData": "[variables('cloudConfig')]", 239 | "linuxConfiguration": { 240 | "disablePasswordAuthentication": "true", 241 | "ssh": { 242 | "publicKeys": [ 243 | { 244 | "path": "[variables('sshKeyPath')]", 245 | "keyData": "[variables('sshKeyData')]" 246 | } 247 | ] 248 | } 249 | } 250 | }, 251 | "storageProfile": { 252 | "imageReference": { 253 | "publisher": "CoreOS", 254 | "offer": "CoreOS", 255 | "sku": "[variables('osImageSku')]", 256 | "version": "latest" 257 | }, 258 | "osDisk": { 259 | "name": "[concat(variables('vmNamePrefix'), copyindex())]", 260 | "vhd": { 261 | "uri": "[concat('http://',variables('vhdStorageAccountName'),'.blob.core.windows.net/vhds/',variables('vmNamePrefix'), copyindex(),'-OS', '.vhd')]" 262 | }, 263 | "caching": "ReadWrite", 264 | "createOption": "FromImage" 265 | } 266 | }, 267 | "networkProfile": { 268 | "networkInterfaces": [ 269 | { 270 | "id": "[resourceId('Microsoft.Network/networkInterfaces',concat(variables('nicNamePrefix'), copyindex()))]" 271 | } 272 | ] 273 | }, 274 | "diagnosticsProfile": { 275 | "bootDiagnostics": { 276 | "enabled": "true", 277 | "storageUri": "[concat('http://',variables('diagStorageAccountName'),'.blob.core.windows.net')]" 278 | } 279 | } 280 | } 281 | } 282 | ] 283 | } 284 | -------------------------------------------------------------------------------- /sources/templates/storage-account.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "2015-06-15", 3 | "type": "Microsoft.Storage/storageAccounts", 4 | "name": "[toLower( concat(copyindex(), variables('storageAccountNamePrefix'), 'vhd', uniqueString(resourceGroup().id) ) )]", 5 | "copy": { 6 | "name": "storageLoop", 7 | "count": "[variables('numberOfNodes')]" 8 | }, 9 | "location": "[resourceGroup().location]", 10 | "properties": { 11 | "accountType": "[variables('vhdStorageAccountType')]" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sources/templates/unmanaged-disk.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DataDisk*", 3 | "diskSizeGB": "4095", 4 | "lun": 0, 5 | "vhd": { 6 | "uri": "[concat('http://', toLower( concat( copyindex(), variables('storageAccountNamePrefix'), 'vhd', uniqueString(resourceGroup().id) ) ), '.blob.core.windows.net/vhds/',variables('vmNamePrefix'), copyindex(),'-DataDisk*', '.vhd')]" 7 | }, 8 | "caching": "None", 9 | "createOption": "Empty" 10 | } 11 | -------------------------------------------------------------------------------- /sources/yaml-arm.source.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Source for the cloud-config.yaml file 4 | module.exports = { 5 | // JSON structure for the cloud-config.yaml file 6 | tree: { 7 | "write_files": [], 8 | "coreos": { 9 | "update": { 10 | "reboot-strategy": "etcd-lock" 11 | }, 12 | "etcd2": { 13 | "discovery": false, 14 | "advertise-client-urls": "http://$private_ipv4:2379,http://$private_ipv4:4001", 15 | "initial-advertise-peer-urls": "http://$private_ipv4:2380", 16 | "listen-client-urls": "http://0.0.0.0:2379,http://0.0.0.0:4001", 17 | "listen-peer-urls": "http://$private_ipv4:2380" 18 | }, 19 | "units": [] 20 | } 21 | }, 22 | 23 | // List of files for write_files 24 | readFiles: { 25 | 'docker-mariadb-galera.sh': { 26 | 'path': '/opt/bin/docker-mariadb-galera.sh', 27 | 'owner': 'root', 28 | 'permissions': '0755' 29 | }, 30 | 'docker-mariadb-waiter.sh': { 31 | 'path': '/opt/bin/docker-mariadb-waiter.sh', 32 | 'owner': 'root', 33 | 'permissions': '0755' 34 | }, 35 | 'etcd-waiter.sh': { 36 | 'path': '/opt/bin/etcd-waiter.sh', 37 | 'owner': 'root', 38 | 'permissions': '0755' 39 | }, 40 | 'mysql_server.cnf': { 41 | 'path': '/opt/mysql.conf.d/mysql_server.cnf', 42 | 'owner': 'root', 43 | 'permissions': '0644' 44 | }, 45 | 'create-raid-array.sh': { 46 | 'path': '/opt/bin/create-raid-array.sh', 47 | 'owner': 'root', 48 | 'permissions': '0755' 49 | }, 50 | }, 51 | 52 | // List of systemd units 53 | units: [ 54 | { 'name': 'etcd.service', 'command': 'stop', 'mask': true }, 55 | { 'name': 'docker.service', 'command': 'start' }, 56 | { 'name': 'etcd2.service', 'command': 'start' }, 57 | { 'name': 'docker-mariadb-galera.service', 'command': 'start', 'source': 'docker-mariadb-galera.service' }, 58 | { 'name': 'docker-mariadb-waiter.service', 'command': 'start', 'source': 'docker-mariadb-waiter.service' }, 59 | { 'name': 'etcd-waiter.service', 'command': 'start', 'source': 'etcd-waiter.service', 'enable': true }, 60 | { 'name': 'create-raid-array.service', 'command': 'start', 'source': 'create-raid-array.service', 'enable': true }, 61 | { 'name': 'mnt-data.mount', 'command': 'start', 'source': 'mnt-data.mount', 'enable': true }, 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /sources/yaml.source.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Source for the cloud-config.yaml file 4 | module.exports = { 5 | // JSON structure for the cloud-config.yaml file 6 | tree: { 7 | "write_files": [], 8 | "coreos": { 9 | "update": { 10 | "reboot-strategy": "etcd-lock" 11 | }, 12 | "etcd2": { 13 | "discovery": false, 14 | "advertise-client-urls": "http://$private_ipv4:2379,http://$private_ipv4:4001", 15 | "initial-advertise-peer-urls": "http://$private_ipv4:2380", 16 | "listen-client-urls": "http://0.0.0.0:2379,http://0.0.0.0:4001", 17 | "listen-peer-urls": "http://$private_ipv4:2380" 18 | }, 19 | "units": [] 20 | } 21 | }, 22 | 23 | // List of files for write_files 24 | readFiles: { 25 | 'docker-mariadb-galera.sh': { 26 | 'path': '/opt/bin/docker-mariadb-galera.sh', 27 | 'owner': 'root', 28 | 'permissions': '0755' 29 | }, 30 | 'docker-mariadb-waiter.sh': { 31 | 'path': '/opt/bin/docker-mariadb-waiter.sh', 32 | 'owner': 'root', 33 | 'permissions': '0755' 34 | }, 35 | 'etcd-waiter.sh': { 36 | 'path': '/opt/bin/etcd-waiter.sh', 37 | 'owner': 'root', 38 | 'permissions': '0755' 39 | }, 40 | 'mysql_server.cnf': { 41 | 'path': '/opt/mysql.conf.d/mysql_server.cnf', 42 | 'owner': 'root', 43 | 'permissions': '0644' 44 | } 45 | }, 46 | 47 | // List of systemd units 48 | units: [ 49 | { 'name': 'etcd.service', 'command': 'stop', 'mask': true }, 50 | { 'name': 'docker.service', 'command': 'start' }, 51 | { 'name': 'etcd2.service', 'command': 'start' }, 52 | { 'name': 'docker-mariadb-galera.service', 'command': 'start', 'source': 'docker-mariadb-galera.service' }, 53 | { 'name': 'docker-mariadb-waiter.service', 'command': 'start', 'source': 'docker-mariadb-waiter.service' }, 54 | { 'name': 'etcd-waiter.service', 'command': 'start', 'source': 'etcd-waiter.service', 'enable': true }, 55 | ] 56 | } 57 | --------------------------------------------------------------------------------