diff --git a/.gitignore b/.gitignore index 874ce7ae90..42cf88a725 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ builder/image/bin/yaml2json-procfile cache/image/bin client/dist/ client/makeself/ +contrib/azure/azure-user-data contrib/bumpver/bumpver deisctl/deisctl deisctl/dist/ diff --git a/contrib/azure/README.md b/contrib/azure/README.md new file mode 100644 index 0000000000..75d58f1eae --- /dev/null +++ b/contrib/azure/README.md @@ -0,0 +1,3 @@ +# Deis on Microsoft Azure + +Please refer to the instructions at http://docs.deis.io/en/latest/installing_deis/azure/. diff --git a/contrib/azure/azure-coreos-cluster b/contrib/azure/azure-coreos-cluster new file mode 100755 index 0000000000..8f1bf9c75f --- /dev/null +++ b/contrib/azure/azure-coreos-cluster @@ -0,0 +1,287 @@ +#!/usr/bin/env python + +from azure import * +from azure.servicemanagement import * +import argparse +import urllib2 +import time +import base64 +import os +import subprocess + +parser = argparse.ArgumentParser(description='Create a CoreOS cluster on Microsoft Azure.') +parser.add_argument('--version', action='version', version='azure-coreos-cluster 0.1') +parser.add_argument('cloud_service_name', + help='cloud service name') +parser.add_argument('--ssh-cert', + help='certificate file with public key for ssh, in .cer format') +parser.add_argument('--ssh-thumb', + help='thumbprint of ssh cert') +parser.add_argument('--subscription', required=True, + help='required Azure subscription id') +parser.add_argument('--azure-cert', required=True, + help='required path to Azure cert pem file') +parser.add_argument('--blob-container-url', required=True, + help='required url to blob container where vm disk images will be created, including /, ex: https://patcoreos.blob.core.windows.net/vhds/') +parser.add_argument('--vm-size', default='Small', + help='optional, VM size [Small]') +parser.add_argument('--vm-name-prefix', default='coreos', + help='optional, VM name prefix [coreos]') +parser.add_argument('--availability-set', default='coreos-as', + help='optional, name of availability set for cluster [coreos-as]') +parser.add_argument('--location', default='West US', + help='optional, [West US]') +parser.add_argument('--ssh', default=22001, type=int, + help='optional, starts with 22001 and +1 for each machine in cluster') +parser.add_argument('--coreos-image', default='2b171e93f07c4903bcad35bda10acf22__CoreOS-Stable-522.6.0', + help='optional, [2b171e93f07c4903bcad35bda10acf22__CoreOS-Stable-522.6.0]') +parser.add_argument('--num-nodes', default=3, type=int, + help='optional, number of nodes to create (or add), defaults to 3') +parser.add_argument('--virtual-network-name', + help='optional, name of an existing virtual network to which we will add the VMs') +parser.add_argument('--subnet-names', + help='optional, subnet name to which the VMs will belong') +parser.add_argument('--custom-data', + help='optional, path to your own cloud-init file') +parser.add_argument('--discovery-service-url', + help='optional, url for an existing cluster discovery service. Else we will generate one.') +parser.add_argument('--pip', action='store_true', + help='optional, assigns public instance ip addresses to each VM') +parser.add_argument('--deis', action='store_true', + help='optional, automatically opens http and controller endpoints') +parser.add_argument('--data-disk', action='store_true', + help='optional, attaches a data disk to each VM') + +cloud_init_template = """#cloud-config + +coreos: + etcd: + # generate a new token for each unique cluster from https://discovery.etcd.io/new + discovery: {0} + # deployments across multiple cloud services will need to use $public_ipv4 + addr: $private_ipv4:4001 + peer-addr: $private_ipv4:7001 + units: + - name: etcd.service + command: start + - name: fleet.service + command: start +""" + +args = parser.parse_args() + +# Create SSH cert if it's not given +if not args.ssh_cert and not args.ssh_thumb: + print 'SSH arguments not given, generating certificate' + with open(os.devnull, 'w') as shutup: + subprocess.call('openssl req -x509 -nodes -days 365 -newkey rsa:2048 -config cert.conf -keyout ssh-cert.key -out ssh-cert.pem', shell=True, stdout=shutup, stderr=shutup) + subprocess.call('chmod 600 ssh-cert.key', shell=True, stdout=shutup, stderr=shutup) + subprocess.call('openssl x509 -outform der -in ssh-cert.pem -out ssh-cert.cer', shell=True, stdout=shutup, stderr=shutup) + thumbprint = subprocess.check_output('openssl x509 -in ssh-cert.pem -sha1 -noout -fingerprint | sed s/://g', shell=True) + args.ssh_thumb = thumbprint.split('=')[1].replace('\n', '') + args.ssh_cert = './ssh-cert.cer' + print 'Generated SSH certificate with thumbprint ' + args.ssh_thumb + +# Setup custom data +if args.custom_data: + with open(args.custom_data, 'r') as f: + if not os.path.exists(args.custom_data): + print "Couldn't find the user-data file. Did you remember to run `create-azure-user-data`?" + sys.exit(1) + cloud_init = f.read() + f.closed +else: + if args.discovery_service_url: + cloud_init = cloud_init_template.format(args.discovery_service_url) + else: + response = urllib2.urlopen('https://discovery.etcd.io/new') + discovery_url = response.read() + cloud_init = cloud_init_template.format(discovery_url) + +SERVICE_CERT_FORMAT = 'pfx' + +with open(args.ssh_cert) as f: + service_cert_file_data = base64.b64encode(f.read()) +f.closed + +def wait_for_async(request_id, timeout): + count = 0 + result = sms.get_operation_status(request_id) + while result.status == 'InProgress': + count = count + 1 + if count > timeout: + print('Timed out waiting for async operation to complete.') + return + time.sleep(5) + print('.'), + sys.stdout.flush() + result = sms.get_operation_status(request_id) + if result.error: + print(result.error.code) + print(vars(result.error)) + print result.status + ' in ' + str(count*5) + 's' + +def linux_config(hostname, args): + pk = PublicKey(args.ssh_thumb, + u'/home/core/.ssh/authorized_keys') + system = LinuxConfigurationSet(hostname, 'core', None, True, + custom_data=cloud_init) + system.ssh.public_keys.public_keys.append(pk) + system.disable_ssh_password_authentication = True + return system + +def endpoint_config(name, port, probe=False): + endpoint = ConfigurationSetInputEndpoint(name, 'tcp', port, port, name) + if probe: + endpoint.load_balancer_probe = probe + return endpoint + +def load_balancer_probe(path, port, protocol): + load_balancer_probe = LoadBalancerProbe() + load_balancer_probe.path = path + load_balancer_probe.port = port + load_balancer_probe.protocol = protocol + return load_balancer_probe + +def network_config(subnet_name=None, port='59913', public_ip_name=None): + network = ConfigurationSet() + network.configuration_set_type = 'NetworkConfiguration' + network.input_endpoints.input_endpoints.append( + ConfigurationSetInputEndpoint('ssh', 'tcp', port, '22')) + if subnet_name: + network.subnet_names.append(subnet_name) + if public_ip_name: + network.public_ips.public_ips.append(PublicIP(name=public_ip_name)) + if args.deis: + # create web endpoint with probe checking /health-check + network.input_endpoints.input_endpoints.append(endpoint_config('web', '80', load_balancer_probe('/health-check', '80', 'http'))) + # create builder endpoint with no health check + network.input_endpoints.input_endpoints.append(endpoint_config('builder', '2222', load_balancer_probe(None, '2222', 'tcp'))) + return network + +def data_hd(target_container_url, target_blob_name, target_lun, target_disk_size_in_gb): + media_link = target_container_url + target_blob_name + data_hd = DataVirtualHardDisk() + data_hd.disk_label = target_blob_name + data_hd.logical_disk_size_in_gb = target_disk_size_in_gb + data_hd.lun = target_lun + data_hd.media_link = media_link + return data_hd + +sms = ServiceManagementService(args.subscription, args.azure_cert) + +#Create the cloud service +try: + print 'Creating the hosted service...', + sys.stdout.flush() + sms.create_hosted_service( + args.cloud_service_name, label=args.cloud_service_name, location=args.location) + print('Successfully created hosted service ' + args.cloud_service_name) + sys.stdout.flush() + time.sleep(2) +except WindowsAzureConflictError: + print "Hosted service {} already exists. Delete it or try again with a different name.".format(args.cloud_service_name) + sys.exit(1) + +#upload ssh cert to cloud-service +print 'Uploading SSH certificate...', +sys.stdout.flush() +result = sms.add_service_certificate(args.cloud_service_name, + service_cert_file_data, SERVICE_CERT_FORMAT, '') +wait_for_async(result.request_id, 15) + +def get_vm_name(args, i): + return args.cloud_service_name + '-' + args.vm_name_prefix + '-' + str(i) + +vms =[] + +#Create the VMs +for i in range(args.num_nodes): + ssh_port = args.ssh +i + vm_name = get_vm_name(args, i) + if args.pip: + pip_name = vm_name + else: + pip_name = None + media_link = args.blob_container_url + vm_name + os_hd = OSVirtualHardDisk(media_link=media_link, + source_image_name=args.coreos_image) + system = linux_config(vm_name, args) + network = network_config(subnet_name=args.subnet_names, port=ssh_port, public_ip_name=pip_name) + #specifiy the data disk, important to start at lun = 0 + if args.data_disk: + data_disk = data_hd(args.blob_container_url, vm_name + '-data.vhd', 0, 100) + data_disks = DataVirtualHardDisks() + data_disks.data_virtual_hard_disks.append(data_disk) + else: + data_disks = None + + try: + if i == 0: + result = sms.create_virtual_machine_deployment( + args.cloud_service_name, deployment_name=args.cloud_service_name, + deployment_slot='production', label=vm_name, + role_name=vm_name, system_config=system, os_virtual_hard_disk=os_hd, + role_size=args.vm_size, network_config=network, data_virtual_hard_disks=data_disks) + else: + result = sms.add_role( + args.cloud_service_name, deployment_name=args.cloud_service_name, + role_name=vm_name, + system_config=system, os_virtual_hard_disk=os_hd, + role_size=args.vm_size, network_config=network, data_virtual_hard_disks=data_disks) + except WindowsAzureError as e: + if "Forbidden" in str(e): + print "Unable to use this CoreOS image. This usually means a newer image has been published." + print "See https://coreos.com/docs/running-coreos/cloud-providers/azure/ for the latest stable image," + print "and supply it to this script with --coreos-image. If it works, please open a pull request to update this script." + sys.exit(1) + else: + pass + + print 'Creating VM ' + vm_name + '...', + sys.stdout.flush() + wait_for_async(result.request_id, 30) + vms.append({'name':vm_name, + 'host':args.cloud_service_name + '.cloudapp.net', + 'port':ssh_port, + 'user':'core', + 'identity':args.ssh_cert.replace('.cer','.key')}) + +#get the ip addresses +def get_ips(service_name, deployment_name): + result = sms.get_deployment_by_name(service_name, deployment_name) + for instance in result.role_instance_list: + ips.append(instance.public_ips[0].address) + return ips + +#print dns config +if args.pip: + ips = [] + ips = get_ips(args.cloud_service_name, args.cloud_service_name) + print '' + print '-------' + print "You'll need to configure DNS records for a domain you wish to use with your Deis cluster." + print 'For convenience, the public IP addresses are printed below, along with sane DNS timeouts.' + print '' + for ip in ips: + print '@ 10800 IN A ' + ip + print '* 10800 IN CNAME @' + print '-------' + print 'For more information, see: http://docs.deis.io/en/latest/managing_deis/configure-dns/' + print '' + +#print ~/.ssh/config +print '' +print '-------' +print "Instances on Azure don't use typical SSH ports. It is recommended to configure ~/.ssh/config" +print 'so the instances can easily be referenced when logging in via SSH. For convenience, the config' +print 'directives for your instances are below:' +print '' +for vm in vms: + print 'Host ' + vm['name'] + print ' HostName ' + vm['host'] + print ' Port ' + str(vm['port']) + print ' User ' + vm['user'] + print ' IdentityFile ' + vm['identity'] +print '-------' +print '' diff --git a/contrib/azure/azure-user-data-template b/contrib/azure/azure-user-data-template new file mode 100644 index 0000000000..8eb1eac9b9 --- /dev/null +++ b/contrib/azure/azure-user-data-template @@ -0,0 +1,27 @@ +#cloud-config +coreos: + units: + - name: format-ephemeral.service + command: start + content: | + [Unit] + Description=Formats the ephemeral drive + ConditionPathExists=!/etc/azure-formatted + [Service] + Type=oneshot + RemainAfterExit=yes + ExecStart=/usr/sbin/wipefs -f /dev/sdc + ExecStart=/usr/sbin/mkfs.btrfs -f /dev/sdc + ExecStart=/bin/touch /etc/azure-formatted + - name: var-lib-docker.mount + command: start + content: | + [Unit] + Description=Mount ephemeral to /var/lib/docker + Requires=format-ephemeral.service + After=format-ephemeral.service + Before=docker.service + [Mount] + What=/dev/sdc + Where=/var/lib/docker + Type=btrfs diff --git a/contrib/azure/cert.conf b/contrib/azure/cert.conf new file mode 100644 index 0000000000..27c58fc633 --- /dev/null +++ b/contrib/azure/cert.conf @@ -0,0 +1,14 @@ +[ req ] +prompt = no +default_bits = 2048 +encrypt_key = no +distinguished_name = req_distinguished_name + +string_mask = utf8only + +[ req_distinguished_name ] +O=My Company +L=San Francisco +ST=CA +C=US +CN=www.test.com diff --git a/contrib/azure/create-azure-user-data b/contrib/azure/create-azure-user-data new file mode 100755 index 0000000000..256232f398 --- /dev/null +++ b/contrib/azure/create-azure-user-data @@ -0,0 +1,78 @@ +#!/usr/bin/env python +""" +Create CoreOS user-data by merging contrib/coreos/user-data and azure-user-data + +Usage: create-azure-user-data.py + +Arguments: + This is the CoreOS etcd discovery URL. You should generate + a new URL at https://discovery.etcd.io/new and pass it as the argument. +""" + +import sys +import os +import re +import yaml +import collections + + +def combine_dicts(orig_dict, new_dict): + for key, val in new_dict.iteritems(): + if isinstance(val, collections.Mapping): + tmp = combine_dicts(orig_dict.get(key, {}), val) + orig_dict[key] = tmp + elif isinstance(val, list): + orig_dict[key] = (val + orig_dict[key]) + else: + orig_dict[key] = new_dict[key] + return orig_dict + + +def get_file(name, mode="r", abspath=False): + current_dir = os.path.dirname(__file__) + + if abspath: + return file(os.path.abspath(os.path.join(current_dir, name)), mode) + else: + return file(os.path.join(current_dir, name), mode) + + +def main(): + try: + url = sys.argv[1] + except (NameError, IndexError): + print __doc__ + return 1 + + url_pattern = 'http[|s]\:[\/]{2}discovery\.etcd\.io\/[0-9a-f]{32}' + + m = re.match(url_pattern, url) + + if not m: + print "Discovery URL invalid." + return 1 + + azure_user_data = get_file("azure-user-data", "w", True) + azure_template = get_file("azure-user-data-template") + coreos_template = get_file("../coreos/user-data.example") + + configuration_coreos_template = yaml.safe_load(coreos_template) + configuration_azure_template = yaml.safe_load(azure_template) + + configuration = combine_dicts(configuration_coreos_template, + configuration_azure_template) + + configuration["coreos"]["etcd"]["discovery"] = url + + with azure_user_data as outfile: + try: + outfile.write("#cloud-config\n\n" + yaml.dump(configuration, default_flow_style=False)) + except (IOError, ValueError): + print "There was an issue writing to file " + azure_user_data.name + return 1 + else: + print "Wrote file \"%s\" with url \"%s\"" %\ + (azure_user_data.name, url) + +if __name__ == "__main__": + sys.exit(main()) diff --git a/contrib/azure/generate-mgmt-cert.sh b/contrib/azure/generate-mgmt-cert.sh new file mode 100755 index 0000000000..05e365e966 --- /dev/null +++ b/contrib/azure/generate-mgmt-cert.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# generate management cert + +openssl req -x509 -nodes -days 365 -newkey rsa:2048 -config cert.conf -keyout azure-cert.pem -out azure-cert.pem -config cert.conf +openssl x509 -outform der -in azure-cert.pem -out azure-cert.cer diff --git a/docs/installing_deis/azure.rst b/docs/installing_deis/azure.rst new file mode 100644 index 0000000000..e1bb080b90 --- /dev/null +++ b/docs/installing_deis/azure.rst @@ -0,0 +1,114 @@ +:title: Installing Deis on Microsoft Azure +:description: How to provision a multi-node Deis cluster on Microsoft Azure + +.. _deis_on_azure: + +Microsoft Azure +=============== + +This section will show you how to create a 3-node Deis cluster on Microsoft Azure. + +Before you start, :ref:`get the Deis source ` and change directory into `contrib/azure`_ +while following this documentation. + + +Install Python and Azure SDK for Python +--------------------------------------- + +The cluster creation tool uses Python and the Python Azure library to create a CoreOS cluster. +If you haven't already, install these on your development machine: + +.. code-block:: console + + $ brew install python + $ sudo pip install azure pyyaml + +Generate Certificates +--------------------- + +The azure-coreos-cluster creation tool uses the Azure management REST API to create the CoreOS +cluster which uses a management certificate to authenticate. + +If you don't have a management certificate already configured, the script generate-mgmt-cert.sh can +create this certificate for you. Otherwise, you can skip to the next section. + +If you need to create a certificate, edit cert.conf in contrib/azure with your company's details and then run: + +.. code-block:: console + + $ ./generate-mgmt-cert.sh + +Upload Management Cert +---------------------- + +If you haven't uploaded your management certificate to Azure (azure-cert.cer if you used the script +in the previous section), do that now using the `management certificates tab`_ of the +Azure portal's Settings. + +Also copy the Azure subscription id from this table and save it for the cluster creation script below. + +Create Cluster Cloud Config +--------------------------- + +Before we can create a cluster, we need to create a cloud config for it. The script +``create-azure-user-data`` does this for you. This script takes the stock cluster instance config +in ``../coreos/user-data.example``, customizes it for Azure, and inserts a unique cluster discovery +endpoint: + +.. code-block:: console + + $ ./create-azure-user-data $(curl -s https://discovery.etcd.io/new) + +This will create a azure-user-data cloud config file. We'll use this with the script in the next +section during cluster creation. + +Create CoreOS Cluster +--------------------- + +With the management certificate and cloud config in place, we are ready to create our cluster. + +* Create a container called ``vhds`` within a storage account in the same region as your cluster using the Azure portal. Note the URL of the container for the cluster creation script below. +* Choose a cloud service name for your Deis cluster for the script below. The script will automatically create this cloud service for you. +* Choose an Azure `region`_ to use. Supply it in quotes with the ``--location`` parameter. The default is "West US". + +With that, let's run the azure-coreos-cluster script which will create the CoreOS cluster. Fill in the bracketed values with the values for your deployment you created above. + +.. code-block:: console + + $ ./azure-coreos-cluster [cloud service name] + --subscription [subscription id] + --azure-cert azure-cert.pem + --num-nodes 3 + --location "[location]" + --vm-size Large + --pip + --deis + --blob-container-url https://[blob container].blob.core.windows.net/vhds/ + --data-disk + --custom-data azure-user-data + +This script will by default provision a 3 node cluster but you can increase this with the +``--num-nodes`` parameter. Likewise, you can increase the VM size using ``--vm-size``. +It is not recommended that you use smaller than Large (A3) sized instances. + +Note that for scheduling to work properly, clusters must consist of at least 3 nodes and always +have an odd number of members. For more information, see `etcd disaster recovery`_. + + +Configure DNS +------------- + +See :ref:`configure-dns` for more information on properly setting up your DNS records with Deis. + + +Install Deis Platform +--------------------- + +Now that you've finished provisioning a cluster, please refer to :ref:`install_deis_platform` to +start installing the platform. + +.. _`management certificates tab`: https://manage.windowsazure.com/#Workspaces/AdminTasks/ListManagementCertificates +.. _`contrib/azure`: https://github.com/deis/deis/tree/master/contrib/azure +.. _`etcd`: https://github.com/coreos/etcd +.. _`etcd disaster recovery`: https://github.com/coreos/etcd/blob/master/Documentation/admin_guide.md#disaster-recovery +.. _`region`: http://azure.microsoft.com/en-us/regions/ diff --git a/docs/installing_deis/index.rst b/docs/installing_deis/index.rst index ac25cc57ba..31c31f88bd 100644 --- a/docs/installing_deis/index.rst +++ b/docs/installing_deis/index.rst @@ -23,6 +23,7 @@ with CoreOS. quick-start system-requirements aws + azure digitalocean gce rackspace