From 2faacc6a50993038c98789dfa96430a757bdf545 Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Sun, 6 Aug 2017 18:27:45 -0700 Subject: [PATCH] Add concepts, tutorials, and faq docs * Add bare-metal tutorial * Add DigitalOcean tutorial * Add Google Cloud tutorial --- docs/addons/calico.md | 4 + docs/addons/cluo.md | 6 + docs/addons/dashboard.md | 4 + docs/addons/heapster.md | 4 + docs/addons/ingress.md | 4 + docs/addons/overview.md | 12 ++ docs/advanced/customization.md | 6 + docs/bare-metal.md | 362 +++++++++++++++++++++++++++++++++ docs/concepts.md | 114 +++++++++++ docs/digital-ocean.md | 245 ++++++++++++++++++++++ docs/faq.md | 30 +++ docs/google-cloud.md | 241 ++++++++++++++++++++++ docs/img/favicon.ico | Bin 0 -> 370070 bytes docs/img/spin.png | Bin 0 -> 22918 bytes docs/img/spin.svg | 1 + docs/index.md | 118 +++++++++++ mkdocs.yml | 55 +++++ requirements.txt | 4 + 18 files changed, 1210 insertions(+) create mode 100644 docs/addons/calico.md create mode 100644 docs/addons/cluo.md create mode 100644 docs/addons/dashboard.md create mode 100644 docs/addons/heapster.md create mode 100644 docs/addons/ingress.md create mode 100644 docs/addons/overview.md create mode 100644 docs/advanced/customization.md create mode 100644 docs/bare-metal.md create mode 100644 docs/concepts.md create mode 100644 docs/digital-ocean.md create mode 100644 docs/faq.md create mode 100644 docs/google-cloud.md create mode 100644 docs/img/favicon.ico create mode 100644 docs/img/spin.png create mode 100644 docs/img/spin.svg create mode 100644 docs/index.md create mode 100644 mkdocs.yml create mode 100644 requirements.txt diff --git a/docs/addons/calico.md b/docs/addons/calico.md new file mode 100644 index 00000000..9a62ba06 --- /dev/null +++ b/docs/addons/calico.md @@ -0,0 +1,4 @@ +# Calico Policy + +!!! bug "In Progress" + These docs haven't been moved over yet. diff --git a/docs/addons/cluo.md b/docs/addons/cluo.md new file mode 100644 index 00000000..677bce9a --- /dev/null +++ b/docs/addons/cluo.md @@ -0,0 +1,6 @@ +# Container Linux Update Operator + +More aptly named "Container Linux Reboot Coordinator". + +!!! bug "In Progress" + These docs haven't been completed yet. diff --git a/docs/addons/dashboard.md b/docs/addons/dashboard.md new file mode 100644 index 00000000..6acf601b --- /dev/null +++ b/docs/addons/dashboard.md @@ -0,0 +1,4 @@ +# Kubernetes Dashboard + +!!! bug "In Progress" + These docs haven't been completed yet. diff --git a/docs/addons/heapster.md b/docs/addons/heapster.md new file mode 100644 index 00000000..94e4a979 --- /dev/null +++ b/docs/addons/heapster.md @@ -0,0 +1,4 @@ +# Heapster + +!!! bug "In Progress" + These docs haven't been moved over yet. diff --git a/docs/addons/ingress.md b/docs/addons/ingress.md new file mode 100644 index 00000000..32362ea4 --- /dev/null +++ b/docs/addons/ingress.md @@ -0,0 +1,4 @@ +# Ingress Controller + +!!! bug "In Progress" + These docs haven't been moved over yet. diff --git a/docs/addons/overview.md b/docs/addons/overview.md new file mode 100644 index 00000000..0e782d26 --- /dev/null +++ b/docs/addons/overview.md @@ -0,0 +1,12 @@ +# Addons + +Every Typhoon cluster is verified to work well with several post-install addons. + +* Nginx [Ingress Controller](ingress.md) +* Calico [Network Policy](calico.md) +* [Heapster](heapster.md) +* Kubernetes [Dashboard](dashboard.md) +* [CLUO](cluo.md) (Container Linux only) +* Prometheus +* Grafana + diff --git a/docs/advanced/customization.md b/docs/advanced/customization.md new file mode 100644 index 00000000..9fcea4d6 --- /dev/null +++ b/docs/advanced/customization.md @@ -0,0 +1,6 @@ +# Customization + +To customize clusters in ways that aren't supported by input variables, fork the repo and make changes to the Terraform module. Stay tuned for improvements to this strategy since it is beneficial to stay close to this upstream. + +To customize lower-level Kubernetes control plane bootstrapping, see the [poseidon/bootkube-terraform](https://github.com/poseidon/bootkube-terraform) Terraform module. + diff --git a/docs/bare-metal.md b/docs/bare-metal.md new file mode 100644 index 00000000..d542b77f --- /dev/null +++ b/docs/bare-metal.md @@ -0,0 +1,362 @@ +# Bare-Metal + +In this tutorial, we'll network boot and provison a Kubernetes v1.7.3 cluster on bare-metal. + +First, we'll deploy a [Matchbox](https://github.com/coreos/matchbox) service and setup a network boot environment. Then, we'll declare a Kubernetes cluster in Terraform using the Typhoon Terraform module and power on machines. On PXE boot, machines will install Container Linux to disk, reboot into the disk install, and provision themselves as Kubernetes controllers or workers. + +Controllers are provisioned as etcd peers and run `etcd-member` (etcd3) and `kubelet`. Workers are provisioned to run a `kubelet`. A one-time [bootkube](https://github.com/kubernetes-incubator/bootkube) bootstrap schedules an `apiserver`, `scheduler`, `controller-manager`, and `kube-dns` on controllers and runs `kube-proxy` and `flannel` on each node. A generated `kubeconfig` provides `kubectl` access to the cluster. + +## Requirements + +* Machines with 2GB RAM, 30GB disk, PXE-enabled NIC, IPMI +* PXE-enabled [network boot](https://coreos.com/matchbox/docs/latest/network-setup.html) environment +* Matchbox v0.6+ deployment with API enabled +* Matchbox credentials `client.crt`, `client.key`, `ca.crt` +* Terraform v0.9.2+ and [terraform-provider-matchbox](https://github.com/coreos/terraform-provider-matchbox) installed locally + +## Machines + +Collect a MAC address from each machine. For machines with multiple PXE-enabled NICs, pick one of the MAC addresses. MAC addresses will be used to match machines to profiles during network boot. + +* 52:54:00:a1:9c:ae (node1) +* 52:54:00:b2:2f:86 (node2) +* 52:54:00:c3:61:77 (node3) + +Configure each machine to boot from the disk [^1] through IPMI or the BIOS menu. + + +[^1]: Configuring "diskless" workers that always PXE boot is possible, but not in the scope of this tutorial. + +``` +ipmitool -H node1 -U USER -P PASS chassis bootdev disk options=persistent +``` + +During provisioning, you'll explicitly set the boot device to `pxe` for the next boot only. Machines will install (overwrite) the operting system to disk on PXE boot and reboot into the disk install. + +!!! tip "" + Ask your hardware vendor to provide MACs and preconfigure IPMI, if possible. With it, you can rack new servers, `terraform apply` with new info, and power on machines that network boot and provision into clusters. + +## DNS + +Create a DNS A (or AAAA) record for each node's default interface. Create a record that resolves to each controller node (or re-use the node record if there's one controller). + +* node1.example.com (node1) +* node2.example.com (node2) +* node3.example.com (node3) +* myk8s.example.com (node1) + +Cluster nodes will be configured to refer to the control plane and themselves by these fully qualified names and they'll be used in generated TLS certificates. + +## Matchbox + +Matchbox is an open-source app that matches network-booted bare-metal machines (based on labels like MAC, UUID, etc.) to profiles to automate cluster provisioning. + +Install Matchbox on a Kubernetes cluster or dedicated server. + +* Installing on [Kubernetes](https://coreos.com/matchbox/docs/latest/deployment.html#kubernetes) (recommended) +* Installing on a [server](https://coreos.com/matchbox/docs/latest/deployment.html#download) + +!!! tip + Deploy Matchbox as service that can be accessed by all of your bare-metal machines globally. This provides a single endpoint to use Terraform to manage bare-metal clusters at different sites. Typhoon will never include secrets in provisioning user-data so you may even deploy matchbox publicly. + +Matchbox provides a TLS client-authenticated API that clients, like Terraform, can use to manage machine matching and profiles. Think of it like a cloud provider API, but for creating bare-metal instances. + +[Generate TLS](https://coreos.com/matchbox/docs/latest/deployment.html#generate-tls-certificates) client credentials. Save the `ca.crt`, `client.crt`, and `client.key` where they can be referenced in Terraform configs. + +```sh +mv ca.crt client.crt client.key ~/.config/matchbox/ +``` + +Verify the matchbox read-only HTTP endpoints are accessible (port is configurable). + +```sh +$ curl http://matchbox.example.com:8080 +matchbox +``` + +Verify your TLS client certificate and key can be used to access the Matchbox API (port is configurable). + +```sh +$ openssl s_client -connect matchbox.example.com:8081 \ + -CAfile ~/.config/matchbox/ca.crt \ + -cert ~/.config/matchbox/client.crt \ + -key ~/.config/matchbox/client.key +``` + +## PXE Environment + +Create a iPXE-enabled network boot environment. Configure PXE clients to chainload [iPXE](http://ipxe.org/cmd) and instruct iPXE clients to chainload from your Matchbox service's `/boot.ipxe` endpoint. + +For networks already supporting iPXE clients, you can add a `default.ipxe` config. + +```ini +# /var/www/html/ipxe/default.ipxe +chain http://matchbox.foo:8080/boot.ipxe +``` + +For networks with Ubiquiti Routers, you can [configure the router](TODO) itself to chainload machines to iPXE and Matchbox. + +For a small lab, you may wish to checkout the [quay.io/coreos/dnsmasq](https://quay.io/repository/coreos/dnsmasq) container image and [copy-paste examples](https://github.com/coreos/matchbox/blob/master/Documentation/network-setup.md#coreosdnsmasq). + +Read about the [many ways](https://coreos.com/matchbox/docs/latest/network-setup.html) to setup a compliant iPXE-enabled network. There is quite a bit of flexibility: + +* Continue using existing DHCP, TFTP, or DNS services +* Configure specific machines, subnets, or architectures to chainload from Matchbox +* Place Matchbox behind a menu entry (timeout and default to Matchbox) + +!!! note "" + TFTP chainloding to modern boot firmware, like iPXE, avoids issues with old NICs and allows faster transfer protocols like HTTP to be used. + +## Terraform Setup + +Install [Terraform](https://www.terraform.io/downloads.html) v0.9.2+ on your system. + +```sh +$ terraform version +Terraform v0.10.1 +``` + +Add the [terraform-provider-matchbox](https://github.com/coreos/terraform-provider-matchbox) plugin binary for your system. + +```sh +wget https://github.com/coreos/terraform-provider-matchbox/releases/download/v0.2.2/terraform-provider-matchbox-v0.2.2-linux-amd64.tar.gz +tar xzf terraform-provider-matchbox-v0.2.2-linux-amd64.tar.gz +sudo mv terraform-provider-matchbox-v0.2.2-linux-amd64/terraform-provider-matchbox /usr/local/bin/ +``` + +Add the plugin to your `~/.terraformrc`. + +``` +providers { + ct = "/usr/local/bin/terraform-provider-matchbox" +} +``` + +Read [concepts](concepts.md) to learn about Terraform, modules, and organizing resources. Change to your infrastructure repository (e.g. `infra`). + +``` +cd infra/clusters +``` + +## Provider + +Configure the Matchbox provider to use your Matchbox API endpoint and client certificate. + +```tf +provider "matchbox" { + endpoint = "matchbox.example.com:8081" + client_cert = "${file("~/.config/matchbox/client.crt")}" + client_key = "${file("~/.config/matchbox/client.key")}" + ca = "${file("~/.config/matchbox/ca.crt")}" +} +``` + +## Cluster + +Define a Kubernetes cluster using the module `bare-metal/container-linux/kubernetes`. + +```tf +module "bare-metal-mercury" { + source = "git::https://github.com/poseidon/typhoon//bare-metal/container-linux/kubernetes" + + # install + matchbox_http_endpoint = "http://matchbox.example.com" + container_linux_channel = "stable" + container_linux_version = "1465.6.0" + ssh_authorized_key = "ssh-rsa AAAAB3Nz..." + + # cluster + cluster_name = "mercury" + k8s_domain_name = "node1.example.com" + + # machines + controller_names = ["node1"] + controller_macs = ["52:54:00:a1:9c:ae"] + controller_domains = ["node1.example.com"] + worker_names = [ + "node2", + "node3", + ] + worker_macs = [ + "52:54:00:b2:2f:86", + "52:54:00:c3:61:77", + ] + worker_domains = [ + "node2.example.com", + "node3.example.com", + ] + + # output assets dir + asset_dir = "/home/user/.secrets/clusters/mercury" +} +``` + +Reference the [variables docs](#variables) or the [variables.tf](https://github.com/poseidon/typhoon/blob/master/bare-metal/container-linux/kubernetes/variables.tf) source. + +## ssh-agent + +Initial bootstrapping requires `bootkube.service` be started on one controller node. Terraform uses `ssh-agent` to automate this step. Add your SSH private key to `ssh-agent`. + +```sh +ssh-add ~/.ssh/id_rsa +ssh-add -L +``` + +!!! warning + `terrafrom apply` will hang connecting to a controller if `ssh-agent` does not contain the SSH key. + +## Apply + +Initialize the config directory if this is the first use with Terraform. + +```sh +terraform init +``` + +Get or update Terraform modules. + +```sh +$ terraform get # downloads missing modules +$ terraform get --update # updates all modules +Get: git::https://github.com/poseidon/typhoon (update) +Get: git::https://github.com/poseidon/bootkube-terraform.git?ref=v0.6.1 (update) +``` + +Plan the resources to be created. + +```sh +$ terraform plan +Plan: 55 to add, 0 to change, 0 to destroy. +``` + +Apply the changes. Terraform will generate bootkube assets to `asset_dir` and create Matchbox profiles (e.g. controller, worker) and matching rules via the Matchbox API. + +```sh +module.bare-metal-mercury.null_resource.copy-secrets.0: Provisioning with 'file'... +module.bare-metal-mercury.null_resource.copy-secrets.2: Provisioning with 'file'... +module.bare-metal-mercury.null_resource.copy-secrets.1: Provisioning with 'file'... +module.bare-metal-mercury.null_resource.copy-secrets.0: Still creating... (10s elapsed) +module.bare-metal-mercury.null_resource.copy-secrets.2: Still creating... (10s elapsed) +module.bare-metal-mercury.null_resource.copy-secrets.1: Still creating... (10s elapsed) +... +``` + +Apply will then loop until it can successfully copy credentials to each machine and start the one-time Kubernetes bootstrap service. Proceed to the next step while this loops. + +!!! note "" + You may see `terraform apply` fail to `copy-secrets` if it connects before the disk install has completed. Run terraform apply until it reconciles successfully. + +### Power + +Power on each machine with the boot device set to `pxe` for the next boot only. + +```sh +ipmitool -H node1.example.com -U USER -P PASS chassis bootdev pxe +ipmitool -H node1.example.com -U USER -P PASS power on +``` + +Machines will network boot, install Container Linux to disk, reboot into the disk install, and provision themselves as controllers or workers. + +!!! tip "" + If this is the first test of your PXE-enabled network boot environment, watch the SOL console of a machine to spot any misconfigurations. + +### Bootstrap + +Wait for the `bootkube-start` step to finish bootstrapping the Kubernetes control plane. This may take 5-15 minutes depending on your network. + +``` +module.bare-metal-mercury.null_resource.bootkube-start: Still creating... (6m10s elapsed) +module.bare-metal-mercury.null_resource.bootkube-start: Still creating... (6m20s elapsed) +module.bare-metal-mercury.null_resource.bootkube-start: Still creating... (6m30s elapsed) +module.bare-metal-mercury.null_resource.bootkube-start: Still creating... (6m40s elapsed) +module.bare-metal-mercury.null_resource.bootkube-start: Creation complete (ID: 5441741360626669024) + +Apply complete! Resources: 55 added, 0 changed, 0 destroyed. +``` + +To watch the bootstrap process in detail, SSH to the first controller and journal the logs. + +``` +$ ssh node1.example.com +$ journalctl -f -u bootkube +bootkube[5]: Pod Status: pod-checkpointer Running +bootkube[5]: Pod Status: kube-apiserver Running +bootkube[5]: Pod Status: kube-scheduler Running +bootkube[5]: Pod Status: kube-controller-manager Running +bootkube[5]: All self-hosted control plane components successfully started +bootkube[5]: Tearing down temporary bootstrap control plane... +``` + +## Verify + +[Install kubectl](https://coreos.com/kubernetes/docs/latest/configure-kubectl.html) on your system. Use the generated `kubeconfig` credentials to access the Kubernetes cluster and list nodes. + +``` +$ KUBECONFIG=/home/user/.secrets/clusters/mercury/auth/kubeconfig +$ kubectl get nodes +NAME STATUS AGE VERSION +node1.example.com Ready 11m v1.7.3+coreos.0 +node2.example.com Ready 11m v1.7.3+coreos.0 +node3.example.com Ready 11m v1.7.3+coreos.0 +``` + +List the pods. + +``` +$ kubectl get pods --all-namespaces +NAMESPACE NAME READY STATUS RESTARTS AGE +kube-system kube-apiserver-7336w 1/1 Running 0 11m +kube-system kube-controller-manager-3271970485-b9chx 1/1 Running 0 11m +kube-system kube-controller-manager-3271970485-v30js 1/1 Running 1 11m +kube-system kube-dns-1187388186-mx9rt 3/3 Running 0 11m +kube-system kube-etcd-network-checkpointer-q24f7 1/1 Running 0 11m +kube-system kube-flannel-6qp7f 2/2 Running 1 11m +kube-system kube-flannel-gnjrm 2/2 Running 0 11m +kube-system kube-flannel-llbgt 2/2 Running 0 11m +kube-system kube-proxy-50sd4 1/1 Running 0 11m +kube-system kube-proxy-bczhp 1/1 Running 0 11m +kube-system kube-proxy-mp2fw 1/1 Running 0 11m +kube-system kube-scheduler-3895335239-fd3l7 1/1 Running 1 11m +kube-system kube-scheduler-3895335239-hfjv0 1/1 Running 0 11m +kube-system pod-checkpointer-wf65d 1/1 Running 0 11m +kube-system pod-checkpointer-wf65d-node1.example.com 1/1 Running 0 11m +``` + +## Going Further + +Learn about [version pinning](concepts.md#versioning), maintenance, and [addons](addons.md). + +!!! note + On Container Linux clusters, install the `container-linux-update-operator` addon to coordinate reboots and drains when nodes auto-update. Otherwise, updates may not be applied until the next reboot. + +## Variables + +### Required + +| Name | Description | Example | +|:-----|:------------|:--------| +| matchbox_http_endpoint | Matchbox HTTP read-only endpoint | http://matchbox.example.com:8080 | +| container_linux_channel | Container Linux channel | stable, beta, alpha | +| container_linux_version | Container Linux version of the kernel/initrd to PXE and the image to install | 1465.6.0 | +| cluster_name | Cluster name | mercury | +| k8s_domain_name | FQDN resolving to the controller(s) nodes. Workers and kubectl will communicate with this endpoint | "myk8s.example.com" | +| ssh_authorized_key | SSH public key for ~/.ssh/authorized_keys | "ssh-rsa AAAAB3Nz..." | +| controller_names | Ordered list of controller short names | ["node1"] | +| controller_macs | Ordered list of controller identifying MAC addresses | ["52:54:00:a1:9c:ae"] | +| controller_domains | Ordered list of controller FQDNs | ["node1.example.com"] | +| worker_names | Ordered list of worker short names | ["node2", "node3"] | +| worker_macs | Ordered list of worker identifying MAC addresses | ["52:54:00:b2:2f:86", "52:54:00:c3:61:77"] | +| worker_domains | Ordered list of worker FQDNs | ["node2.example.com", "node3.example.com"] | +| asset_dir | Path to a directory where generated assets should be placed (contains secrets) | "/home/user/.secrets/clusters/mercury" | + +### Optional + +| Name | Description | Default | Example | +|:-----|:------------|:--------|:--------| +| cached_install | Whether machines should PXE boot from the Matchbox `/assets` cache. Admin MUST have downloaded Container Linux images into the cache to use this | false | true | +| install_disk | Disk device where Container Linux should be installed | "/dev/sda" | "/dev/sdb" | +| container_linux_oem | Specify alternative OEM image ids for the disk install | "" | "vmware_raw", "xen" | +| experimental_self_hosted_etcd | Self-host etcd as pods on Kubernetes (not recommended) | false | true | +| pod_cidr | CIDR range to assign to Kubernetes pods | "10.2.0.0/16" | "10.22.0.0/16" | +| service_cidr | CIDR range to assgin to Kubernetes services | "10.3.0.0/16" | "10.3.0.0/24" | + diff --git a/docs/concepts.md b/docs/concepts.md new file mode 100644 index 00000000..5f35817b --- /dev/null +++ b/docs/concepts.md @@ -0,0 +1,114 @@ +# Concepts + +Let's cover the concepts you'll need to get started. + +## Kubernetes + +[Kubernetes](https://kubernetes.io/) is an open-source cluster system for deploying, scaling, and managing containerized applications across a pool of compute nodes (bare-metal, droplets, instances). + +#### Nodes + +Cluster nodes provision themselves from a declarative configuration upfront. Nodes run a `kubelet` service and register themselves with the control plane to join the higher order cluster. + +#### Controllers + +Controller nodes are scheduled to run the Kubernetes `apiserver`, `scheduler`, `controller-manager`, `kube-dns`, and `kube-proxy`. A fully qualified domain name (e.g. cluster_name.domain.com) resolving to a network load balancer or round-robin DNS (depends on platform) is used to refer to the control plane. + +#### Workers + +Worker nodes register with the control plane and run application workloads. Workers, like all nodes, run `kube-proxy` and `flannel` pods. + +## Terraform + +Terraform config files declare *resources* that Terraform should manage. Resources include infrastructure components created through a *provider* API (e.g. Compute instances, DNS records) or local assets like TLS certificates and config files. + +```tf +# Declare an instance +resource "google_compute_instance" "pet" { + # ... +} +``` + +The `terraform` tool parses configs, reconciles the desired state with actual state, and updates resources to reach desired state. + +```sh +$ terraform plan +Plan: 4 to add, 0 to change, 0 to destroy. +$ terraform apply +Apply complete! Resources: 4 added, 0 changed, 0 destroyed. +``` + +With Typhoon, you'll be able to manage clusters with Terraform. + +### Modules + +Terraform [modules](https://www.terraform.io/docs/modules/usage.html) allow a collection of resources to be configured and managed together. Typhoon provides a Kubernetes cluster Terraform *module* for each [supported](/#modules) platform and operating system. + +Clusters are declared in Terraform by referencing the module. + +```tf +module "google-cloud-yavin" { + source = "git::https://github.com/poseidon/typhoon//google-cloud/container-linux/kubernetes" + cluster_name = "yavin" + ... +} +``` + +### Versioning + +Modules are updated regularly, set the version to a [release tag](https://github.com/poseidon/typhoon/releases) or [commit](https://github.com/poseidon/typhoon/commits/master) hash. + +```tf +... +source = "git:https://github.com/poseidon/typhoon//google-cloud/container-linux/kubernetes?ref=v1.7.3" +``` + +Module versioning ensures `terraform get --update` only fetches the desired version, so plan and apply don't change cluster resources, unless the version is altered. + +### Organize + +Maintain Terraform configs for "live" infrastructure in a versioned repository. Seek to organize configs to reflect resources that should be managed together in a `terraform apply` invocation. + +You may choose to organize resources all together, by team, by project, or some other scheme. Here's an example that manages three clusters together: + +```sh +.git/ +infra/ +└── terraform + └── clusters + ├── bare-metal-tungsten.tf + ├── google-cloud-yavin.tf + ├── digital-ocean-nemo.tf + ├── providers.tf + ├── terraform.tfvars + └── remote-backend.tf +``` + +By convention, `providers.tf` registers provider APIs, `terraform.tfvars` stores shared values, and state is written to a remote backend. + +### State + +Terraform syncs its state with provider APIs to plan changes to reconcile to the desired state. By default, Terraform writes state data (including secrets!) to a `terraform.tfstate` file. **At a minimum**, add a `.gitignore` file (or equivalent) to prevent state from being committed to your infrastructure repository. + +``` +# .gitignore +*.tfstate +*.tfstate.backup +.terraform/ +``` + +### Remote Backend + +Later, you may wish to checkout Terraform [remote backends](https://www.terraform.io/intro/getting-started/remote.html) which store state in a remote bucket like Google Storage or S3. + +``` +terraform { + backend "gcs" { + credentials = "/path/to/credentials.json" + project = "project-id" + bucket = "bucket-id" + path = "metal.tfstate" + } +} +``` + diff --git a/docs/digital-ocean.md b/docs/digital-ocean.md new file mode 100644 index 00000000..3ac44b56 --- /dev/null +++ b/docs/digital-ocean.md @@ -0,0 +1,245 @@ +# Digital Ocean + +In this tutorial, we'll create a Kubernetes v1.7.3 cluster on Digital Ocean. + +We'll declare a Kubernetes cluster in Terraform using the Typhoon Terraform module. On apply, firewall rules, DNS records, tags, and droplets for Kubernetes controllers and workers will be created. + +Controllers and workers are provisioned to run a `kubelet`. A one-time [bootkube](https://github.com/kubernetes-incubator/bootkube) bootstrap schedules `etcd`, `apiserver`, `scheduler`, `controller-manager`, and `kube-dns` on controllers and runs `kube-proxy` and `flannel` on each node. A generated `kubeconfig` provides `kubectl` access to the cluster. + +## Requirements + +* Digital Ocean Account and Token +* Digital Ocean Domain (registered Domain Name or delegated subdomain) +* Terraform v0.10.1+ and [terraform-provider-ct](https://github.com/coreos/terraform-provider-ct) installed locally + +## Terraform Setup + +Install [Terraform](https://www.terraform.io/downloads.html) v0.10.1+ on your system. + +```sh +$ terraform version +Terraform v0.10.1 +``` + +Add the [terraform-provider-ct](https://github.com/coreos/terraform-provider-ct) plugin binary for your system. + +```sh +wget https://github.com/coreos/terraform-provider-ct/releases/download/v0.2.0/terraform-provider-ct-v0.2.0-linux-amd64.tar.gz +tar xzf terraform-provider-ct-v0.2.0-linux-amd64.tar.gz +sudo mv terraform-provider-ct-v0.2.0-linux-amd64/terraform-provider-ct /usr/local/bin/ +``` + +Add the plugin to your `~/.terraformrc`. + +``` +providers { + ct = "/usr/local/bin/terraform-provider-ct" +} +``` + +Read [concepts](concepts.md) to learn about Terraform, modules, and organizing resources. Change to your infrastructure repository (e.g. `infra`). + +``` +cd infra/clusters +``` + +## Provider + +Login to [DigitalOcean](https://cloud.digitalocean.com) or create an [account](https://cloud.digitalocean.com/registrations/new), if you don't have one. + +Generate a Personal Access Token with read/write scope from the [API tab](https://cloud.digitalocean.com/settings/api/tokens). Write the token to a file that can be referenced in configs. + +```sh +mkdir -p ~/.config/digital-ocean +echo "TOKEN" > ~/.config/digital-ocean/token +``` + +Configure the DigitalOcean provider to use your token in a `providers.tf` file. + +```tf +provider "digitalocean" { + token = "${chomp(file("~/.config/digital-ocean/token"))}" +} +``` + +## Cluster + +Define a Kubernetes cluster using the module `digital-ocean/container-linux/kubernetes`. + +```tf +module "digital-ocean-nemo" { + source = "git::https://github.com/poseidon/typhoon//digital-ocean/container-linux/kubernetes" + + region = "nyc3" + dns_zone = "digital-ocean.example.com" + + cluster_name = "nemo" + image = "coreos-stable" + controller_count = 1 + controller_type = "2gb" + worker_count = 2 + worker_type = "512mb" + ssh_fingerprints = ["d7:9d:79:ae:56:32:73:79:95:88:e3:a2:ab:5d:45:e7"] + + # output assets dir + asset_dir = "/home/user/.secrets/clusters/nemo" +} +``` + +Reference the [variables docs](#variables) or the [variables.tf](https://github.com/poseidon/typhoon/blob/master/digital-ocean/container-linux/kubernetes/variables.tf) source. + +## ssh-agent + +Initial bootstrapping requires `bootkube.service` be started on one controller node. Terraform uses `ssh-agent` to automate this step. Add your SSH private key to `ssh-agent`. + +```sh +ssh-add ~/.ssh/id_rsa +ssh-add -L +``` + +!!! warning + `terrafrom apply` will hang connecting to a controller if `ssh-agent` does not contain the SSH key. + +## Apply + +Initialize the config directory if this is the first use with Terraform. + +```sh +terraform init +``` + +Get or update Terraform modules. + +```sh +$ terraform get # downloads missing modules +$ terraform get --update # updates all modules +Get: git::https://github.com/poseidon/typhoon (update) +Get: git::https://github.com/poseidon/bootkube-terraform.git?ref=v0.6.1 (update) +``` + +Plan the resources to be created. + +```sh +$ terraform plan +Plan: 54 to add, 0 to change, 0 to destroy. +``` + +Apply the changes to create the cluster. + +```sh +$ terraform apply +module.digital-ocean-nemo.null_resource.bootkube-start: Still creating... (30s elapsed) +module.digital-ocean-nemo.null_resource.bootkube-start: Provisioning with 'remote-exec'... +... +module.digital-ocean-nemo.null_resource.bootkube-start: Still creating... (6m20s elapsed) +module.digital-ocean-nemo.null_resource.bootkube-start: Creation complete (ID: 7599298447329218468) + +Apply complete! Resources: 54 added, 0 changed, 0 destroyed. +``` + +In 5-10 minutes, the Kubernetes cluster will be ready. + +## Verify + +[Install kubectl](https://coreos.com/kubernetes/docs/latest/configure-kubectl.html) on your system. Use the generated `kubeconfig` credentials to access the Kubernetes cluster and list nodes. + +``` +$ KUBECONFIG=/home/user/.secrets/clusters/nemo/auth/kubeconfig +$ kubectl get nodes +NAME STATUS AGE VERSION +10.132.110.130 Ready 10m v1.7.3+coreos.0 +10.132.115.81 Ready 10m v1.7.3+coreos.0 +10.132.124.107 Ready 10m v1.7.3+coreos.0 +``` + +List the pods. + +``` +NAMESPACE NAME READY STATUS RESTARTS AGE +kube-system etcd-operator-3329263108-sgsbl 1/1 Running 1 11m +kube-system kube-apiserver-n10qr 1/1 Running 0 11m +kube-system kube-controller-manager-3271970485-37gtw 1/1 Running 1 11m +kube-system kube-controller-manager-3271970485-p52t5 1/1 Running 0 11m +kube-system kube-dns-1187388186-ld1j7 3/3 Running 0 11m +kube-system kube-etcd-0000 1/1 Running 0 9m +kube-system kube-etcd-network-checkpointer-n9xsk 1/1 Running 0 11m +kube-system kube-flannel-1cq1v 2/2 Running 0 11m +kube-system kube-flannel-hq9t0 2/2 Running 1 11m +kube-system kube-flannel-v0g9w 2/2 Running 0 11m +kube-system kube-proxy-6kxjf 1/1 Running 0 11m +kube-system kube-proxy-fh3td 1/1 Running 0 11m +kube-system kube-proxy-k35rc 1/1 Running 0 11m +kube-system kube-scheduler-3895335239-2bc4c 1/1 Running 0 11m +kube-system kube-scheduler-3895335239-b7q47 1/1 Running 1 11m +kube-system pod-checkpointer-pr1lq 1/1 Running 0 11m +kube-system pod-checkpointer-pr1lq-10.132.115.81 1/1 Running 0 10m +``` + +## Going Further + +Learn about [version pinning](concepts.md#versioning), maintenance, and common [addons](addons.md). + +!!! note + On Container Linux clusters, install the `container-linux-update-operator` addon to coordinate reboots and drains when nodes auto-update. Otherwise, updates may not be applied until the next reboot. + +## Variables + +### Required + +| Name | Description | Example | +|:-----|:------------|:--------| +| cluster_name | Unique cluster name (prepended to dns_zone) | nemo | +| region | Digital Ocean region | nyc1, sfo2, fra1, tor1 | +| dns_zone | Digital Ocean domain (i.e. DNS zone) | do.example.com | +| ssh_fingerprints | SSH public key fingerprints | ["d7:9d..."] | +| asset_dir | Path to a directory where generated assets should be placed (contains secrets) | /home/user/.secrets/nemo | + +#### DNS Zone + +Clusters create DNS A records `${cluster_name}.${dns_zone}` to resolve to controller droplets (round robin). This FQDN is used by workers and `kubectl` to access the apiserver. In this example, the cluster's apiserver would be accessible at `nemo.do.example.com`. + +You'll need a registered domain name or subdomain registered in Digital Ocean Domains (i.e. DNS zones). You can set this up once and create many clusters with unqiue names. + +```tf +resource "digitalocean_domain" "zone-for-clusters" { + name = "do.example.com" + # Digital Ocean oddly requires an IP here. You may have to delete the A record it makes. :( + ip_address = "8.8.8.8" +} +``` + +!!! tip "" + If you have an existing domain name with a zone file elsewhere, just carve out a subdomain that can be managed on DigitalOcean (e.g. do.mydomain.com) and [update nameservers](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean). + +#### SSH Fingerprints + +DigitalOcean droplets are created with your SSH public key "fingerprint" (i.e. MD5 hash) to allow access. If your SSH public key is at `~/.ssh/id_rsa`, find the fingerprint with, + +```bash +ssh-keygen -lf ~/.ssh/id_rsa.pub | awk '{print $2}' +d7:9d:79:ae:56:32:73:79:95:88:e3:a2:ab:5d:45:e7 +``` + +If you use `ssh-agent` (e.g. Yubikey for SSH), find the fingerprint with, + +``` +ssh-add -l -E md5 +2048 MD5:d7:9d:79:ae:56:32:73:79:95:88:e3:a2:ab:5d:45:e7 cardno:000603633110 (RSA) +``` + +If you uploaded an SSH key to DigitalOcean (not required), find the fingerprint under Settings -> Security. Finally, if you don't have an SSH key, [create one now](https://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/). + +### Optional + +| Name | Description | Default | Example | +|:-----|:------------|:--------|:--------| +| image | OS image for droplets | "coreos-stable" | coreos-stable, coreos-beta, coreos-alpha | +| controller_count | Number of controllers (i.e. masters) | 1 | 1 | +| controller_type | Digital Ocean droplet size | 2gb | 2gb (min), 4gb, 8gb | +| worker_count | Number of workers | 1 | 3 | +| worker_type | Digital Ocean droplet size | 512mb | 512mb, 1gb, 2gb, 4gb | +| pod_cidr | CIDR range to assign to Kubernetes pods | "10.2.0.0/16" | "10.22.0.0/16" | +| service_cidr | CIDR range to assgin to Kubernetes services | "10.3.0.0/16" | "10.3.0.0/24" | + +!!! warning + Do not choose a `controller_type` smaller than `2gb`. The `1gb` droplet is not sufficient for running a controller and bootstrapping will fail. diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 00000000..92df7040 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,30 @@ +# FAQ + +## Terraform? + +Typhoon provides a Terraform Module for each supported operating system and platform. Terraform is considered a *format* detail, much like a Linux distro might provide images in the qcow2 or ISO format. It is a mechanism for sharing Typhoon in a way that works for many users. + +Formats rise and evolve. Typhoon may choose to adapt the format over time (with lots of forewarning). However, the authors' have built several Kubernetes "distros" before and learned from mistakes - Terraform modules are the right format for now. + +## Self-hosted etcd + +Typhoon clusters on cloud providers run etcd as "self-hosted" pods, managed by the [etcd-operator](https://github.com/coreos/etcd-operator). By contrast, Typhoon bare-metal runs an etcd peer as a systemd `etcd-member.service` on each controller (i.e. on-host). + +In practice, self-hosted etcd has proven to be *ok*, but not ideal. Running the apiserver's etcd atop Kubernetes itself is inherently complex, but works suitably in most cases. It can be opaque to debug if complex edge cases with upstream Kubernetes bugs arise. + +!!! note "" + Typhoon clusters and their defaults power the maintainers' clusters. The edge cases are sufficiently rare that self-hosted etcd is not a pressing issue, but cloud clusters may switch back to on-host etcd in the future. + +## Operating Systems + +Only Container Linux is supported currently. This just due to operational familiarity, rather than intentional exclusion. It's important that another operating system be added, to reduce the risk of making narrowly-scoped design decisions. + +Fedora Cloud will likely be next. + +## Maintainers + +Typhoon clusters are Kubernetes configurations the maintainers use in real-world, production clusters. + +* Maintainers must personally operate a bare-metal and cloud provider cluster and strive to exercise it in real-world scenarios + +We merge features that are along the "blessed path". We minimize options to reduce complexity and matrix size. We remove outdated materials to reduce sprawl. "Skate where the puck is going", but also "wait until the fit is right". No is temporary, yes is forever. diff --git a/docs/google-cloud.md b/docs/google-cloud.md new file mode 100644 index 00000000..faa02432 --- /dev/null +++ b/docs/google-cloud.md @@ -0,0 +1,241 @@ +# Google Cloud + +In this tutorial, we'll create a Kubernetes v1.7.3 cluster on Google Compute Engine (not GKE). + +We'll declare a Kubernetes cluster in Terraform using the Typhoon Terraform module. On apply, a network, firewall rules, managed instance groups of Kubernetes controllers and workers, network load balancers for controllers and workers, and health checks will be created. + +Controllers and workers are provisioned to run a `kubelet`. A one-time [bootkube](https://github.com/kubernetes-incubator/bootkube) bootstrap schedules `etcd`, `apiserver`, `scheduler`, `controller-manager`, and `kube-dns` on controllers and runs `kube-proxy` and `flannel` on each node. A generated `kubeconfig` provides `kubectl` access to the cluster. + +## Requirements + +* Google Cloud Account and Service Account +* Google Cloud DNS Zone (registered Domain Name or delegated subdomain) +* Terraform v0.9.2+ and [terraform-provider-ct](https://github.com/coreos/terraform-provider-ct) installed locally + +## Terraform Setup + +Install [Terraform](https://www.terraform.io/downloads.html) v0.9.2+ on your system. + +```sh +$ terraform version +Terraform v0.10.1 +``` + +Add the [terraform-provider-ct](https://github.com/coreos/terraform-provider-ct) plugin binary for your system. + +```sh +wget https://github.com/coreos/terraform-provider-ct/releases/download/v0.2.0/terraform-provider-ct-v0.2.0-linux-amd64.tar.gz +tar xzf terraform-provider-ct-v0.2.0-linux-amd64.tar.gz +sudo mv terraform-provider-ct-v0.2.0-linux-amd64/terraform-provider-ct /usr/local/bin/ +``` + +Add the plugin to your `~/.terraformrc`. + +``` +providers { + ct = "/usr/local/bin/terraform-provider-ct" +} +``` + +Read [concepts](concepts.md) to learn about Terraform, modules, and organizing resources. Change to your infrastructure repository (e.g. `infra`). + +``` +cd infra/clusters +``` + +## Provider + +Login to your Google Console [API Manager](https://console.cloud.google.com/apis/dashboard) and select a project, or [signup](https://cloud.google.com/free/) if you don't have an account. + +Select "Credentials", and create service account key credentials. Choose the "Compute Engine default service account" and save the JSON private key to a file that can be referenced in configs. + +```sh +mv ~/Downloads/project-id-43048204.json ~/.config/google-cloud/terraform.json +``` + +Configure the Google Cloud provider to use your service account key, project-id, and region in a `providers.tf` file. + +```tf +provider "google" { + credentials = "${file("~/.config/google-cloud/terraform.json")}" + project = "project-id" + region = "us-central1" +} +``` + +Additional configuration options are described in the `google` provider [docs](https://www.terraform.io/docs/providers/google/index.html). + +!!! tip + A project may contain multiple clusters if you wish. Regions are listed in [docs](https://cloud.google.com/compute/docs/regions-zones/regions-zones) or with `gcloud compute regions list`. + +## Cluster + +Define a Kubernetes cluster using the module `google-cloud/container-linux/kubernetes`. + +```tf +module "google-cloud-yavin" { + source = "git::https://github.com/poseidon/typhoon//google-cloud/container-linux/kubernetes" + + # Google Cloud + zone = "us-central1-c" + dns_zone = "example.com" + dns_zone_name = "example-zone" + os_image = "coreos-stable-1465-6-0-v20170817" + + cluster_name = "yavin" + controller_count = 1 + worker_count = 2 + ssh_authorized_key = "ssh-rsa AAAAB3Nz..." + + # output assets dir + asset_dir = "/home/user/.secrets/clusters/yavin" +} +``` + +Reference the [variables docs](#variables) or the [variables.tf](https://github.com/poseidon/typhoon/blob/master/google-cloud/container-linux/kubernetes/variables.tf) source. + +## ssh-agent + +Initial bootstrapping requires `bootkube.service` be started on one controller node. Terraform uses `ssh-agent` to automate this step. Add your SSH private key to `ssh-agent`. + +```sh +ssh-add ~/.ssh/id_rsa +ssh-add -L +``` + +!!! warning + `terrafrom apply` will hang connecting to a controller if `ssh-agent` does not contain the SSH key. + +## Apply + +Initialize the config directory if this is the first use with Terraform. + +```sh +terraform init +``` + +Get or update Terraform modules. + +```sh +$ terraform get # downloads missing modules +$ terraform get --update # updates all modules +Get: git::https://github.com/poseidon/typhoon (update) +Get: git::https://github.com/poseidon/bootkube-terraform.git?ref=v0.6.1 (update) +``` + +Plan the resources to be created. + +```sh +$ terraform plan +Plan: 64 to add, 0 to change, 0 to destroy. +``` + +Apply the changes to create the cluster. + +```sh +$ terraform apply +module.google-cloud-yavin.null_resource.bootkube-start: Still creating... (10s elapsed) +... + +module.google-cloud-yavin.null_resource.bootkube-start: Still creating... (8m30s elapsed) +module.google-cloud-yavin.null_resource.bootkube-start: Still creating... (8m40s elapsed) +module.google-cloud-yavin.null_resource.bootkube-start: Creation complete (ID: 5768638456220583358) + +Apply complete! Resources: 64 added, 0 changed, 0 destroyed. +``` + +In 5-10 minutes, the Kubernetes cluster will be ready. + +## Verify + +[Install kubectl](https://coreos.com/kubernetes/docs/latest/configure-kubectl.html) on your system. Use the generated `kubeconfig` credentials to access the Kubernetes cluster and list nodes. + +``` +$ KUBECONFIG=/home/user/.secrets/clusters/yavin/auth/kubeconfig +$ kubectl get nodes +NAME STATUS AGE VERSION +yavin-controller-1682.c.example-com.internal Ready 6m v1.7.3+coreos.0 +yavin-worker-jrbf.c.example-com.internal Ready 5m v1.7.3+coreos.0 +yavin-worker-mzdm.c.example-com.internal Ready 5m v1.7.3+coreos.0 +``` + +List the pods. + +``` +$ kubectl get pods --all-namespaces +NAMESPACE NAME READY STATUS RESTARTS AGE +kube-system etcd-operator-3329263108-f443m 1/1 Running 1 6m +kube-system kube-apiserver-zppls 1/1 Running 0 6m +kube-system kube-controller-manager-3271970485-gh9kt 1/1 Running 0 6m +kube-system kube-controller-manager-3271970485-h90v8 1/1 Running 1 6m +kube-system kube-dns-1187388186-zj5dl 3/3 Running 0 6m +kube-system kube-etcd-0000 1/1 Running 0 5m +kube-system kube-etcd-network-checkpointer-crznb 1/1 Running 0 6m +kube-system kube-flannel-1cs8z 2/2 Running 0 6m +kube-system kube-flannel-d1l5b 2/2 Running 0 6m +kube-system kube-flannel-sp9ps 2/2 Running 0 6m +kube-system kube-proxy-117v6 1/1 Running 0 6m +kube-system kube-proxy-9886n 1/1 Running 0 6m +kube-system kube-proxy-njn47 1/1 Running 0 6m +kube-system kube-scheduler-3895335239-5x87r 1/1 Running 0 6m +kube-system kube-scheduler-3895335239-bzrrt 1/1 Running 1 6m +kube-system pod-checkpointer-l6lrt 1/1 Running 0 6m +``` + +## Going Further + +Learn about [version pinning](concepts.md#versioning), maintenance, and [addons](addons.md). + +!!! note + On Container Linux clusters, install the `container-linux-update-operator` addon to coordinate reboots and drains when nodes auto-update. Otherwise, updates may not be applied until the next reboot. + +## Variables + +### Required + +| Name | Description | Example | +|:-----|:------------|:--------| +| cluster_name | Unique cluster name (prepended to dns_zone) | "yavin" | +| zone | Google Cloud zone | "us-central1-f" | +| dns_zone | Google Cloud DNS zone | "google-cloud.example.com" | +| dns_zone_name | Google Cloud DNS zone name | "example-zone" | +| ssh_authorized_key | SSH public key for ~/.ssh_authorized_keys | "ssh-rsa AAAAB3NZ..." | +| os_image | OS image for compute instances | "coreos-stable-1465-6-0-v20170817" | +| asset_dir | Path to a directory where generated assets should be placed (contains secrets) | "/home/user/.secrets/clusters/yavin" | + +Check the list of valid [zones](https://cloud.google.com/compute/docs/regions-zones/regions-zones) and list Container Linux [images](https://cloud.google.com/compute/docs/images) with `gcloud compute images list | grep coreos`. + +#### DNS Zone + +Clusters create a DNS A record `${cluster_name}.${dns_zone}` to resolve a network load balancer backed by controller instances. This FQDN is used by workers and `kubectl` to access the apiserver. In this example, the cluster's apiserver would be accessible at `yavin.google-cloud.example.com`. + +You'll need a registered domain name or subdomain registered in a Google Cloud DNS zone. You can set this up once and create many clusters with unqiue names. + +```tf +resource "google_dns_managed_zone" "zone-for-clusters" { + dns_name = "google-cloud.example.com." + name = "example-zone" + description = "Production DNS zone" +} +``` + +!!! tip "" + If you have an existing domain name with a zone file elsewhere, just carve out a subdomain that can be managed on Google Cloud (e.g. google-cloud.mydomain.com) and [update nameservers](https://cloud.google.com/dns/update-name-servers). + +### Optional + +| Name | Description | Default | Example | +|:-----|:------------|:--------|:--------| +| machine_type | Machine type for compute instances | "n1-standard-1" | See below | +| controller_count | Number of controllers (i.e. masters) | 1 | 1 | +| worker_count | Number of workers | 1 | 3 | +| worker_preemptible | If enabled, Compute Engine will terminate controllers randomly within 24 hours | false | true | +| pod_cidr | CIDR range to assign to Kubernetes pods | "10.2.0.0/16" | "10.22.0.0/16" | +| service_cidr | CIDR range to assgin to Kubernetes services | "10.3.0.0/16" | "10.3.0.0/24" | + +Check the list of valid [machine types](https://cloud.google.com/compute/docs/machine-types). + +#### Preemption + +Add `worker_premeptible = "true"` to allow worker nodes to be [preempted](https://cloud.google.com/compute/docs/instances/preemptible) at random, but pay [significantly](https://cloud.google.com/compute/pricing) less. Clusters tolerate stopping instances fairly well (reschedules pods, but cannot drain) and preemption provides a nice reward for running fault-tolerant cluster systems.` + diff --git a/docs/img/favicon.ico b/docs/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1adb0ab0f2583a2c186d0e8aec747be30631881c GIT binary patch literal 370070 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 literal 0 HcmV?d00001 diff --git a/docs/img/spin.png b/docs/img/spin.png new file mode 100644 index 0000000000000000000000000000000000000000..4e9ce9eb8a1768e9e563c07ecc805603babdcee9 GIT binary patch literal 22918 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 literal 0 HcmV?d00001 diff --git a/docs/img/spin.svg b/docs/img/spin.svg new file mode 100644 index 00000000..f150c97b --- /dev/null +++ b/docs/img/spin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..31174bf2 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,118 @@ +# Typhoon + +Typhoon is a minimal and free Kubernetes distribution. + +* Minimal, stable base Kubernetes distribution +* Declarative infrastructure and configuration +* [Free](#social-contract) (freedom and cost) and privacy-respecting +* Practical for labs, datacenters, and clouds + +Typhoon distributes upstream Kubernetes, architectural conventions, and cluster addons, much like a GNU/Linux distribution provides the Linux kernel and userspace components. + +## Features + +* Kubernetes v1.7.3 (upstream, via [kubernetes-incubator/bootkube](https://github.com/kubernetes-incubator/bootkube)) +* Self-hosted control plane, single or multi master, workloads isolated to workers +* On-cluster etcd with TLS, [RBAC](https://kubernetes.io/docs/admin/authorization/rbac/)-enabled +* Ready for Ingress, Metrics, Dashboards, and other optional [addons](addons/overview.md) +* Provided via Terraform Modules + +## Modules + +Typhoon provides a Terraform Module for each supported operating system and platform. + +| Platform | Operating System | Terraform Module | Status | +|---------------|------------------|------------------|--------| +| Bare-Metal | Container Linux | [bare-metal/container-linux/kubernetes](bare-metal.md) | production | +| Digital Ocean | Container Linux | [digital-ocean/container-linux/kubernetes](digital-ocean.md) | beta | +| Google Cloud | Container Linux | [google-cloud/container-linux/kubernetes](google-cloud.md) | production | + +## Usage + +* [Concepts](concepts.md) +* Tutorials + * [Bare-Metal](bare-metal.md) + * [Digital Ocean](digital-ocean.md) + * [Google-Cloud](google-cloud.md) + +## Example + +Define a Kubernetes cluster by using the Terraform module for your chosen platform and operating system. Here's a minimal example. + +```tf +module "google-cloud-yavin" { + source = "git::https://github.com/poseidon/typhoon//google-cloud/container-linux/kubernetes" + + # Google Cloud + zone = "us-central1-c" + dns_zone = "example.com" + dns_zone_name = "example-zone" + os_image = "coreos-stable-1465-6-0-v20170817" + + cluster_name = "yavin" + controller_count = 1 + worker_count = 2 + ssh_authorized_key = "ssh-rsa AAAAB3Nz..." + + # output assets dir + asset_dir = "/home/user/.secrets/clusters/yavin" +} +``` + +Fetch modules, plan the changes to be made, and apply the changes. + +```sh +$ terraform get --update +$ terraform plan +Plan: 64 to add, 0 to change, 0 to destroy. +$ terraform apply +Apply complete! Resources: 64 added, 0 changed, 0 destroyed. +``` + +In 5-10 minutes (varies by platform), the cluster will be ready. This Google Cloud example creates a `yavin.example.com` DNS record to resolve to a network load balancer across controller nodes. + +``` +$ KUBECONFIG=/home/user/.secrets/clusters/yavin/auth/kubeconfig +$ kubectl get nodes +NAME STATUS AGE VERSION +yavin-controller-1682.c.example-com.internal Ready 6m v1.7.3+coreos.0 +yavin-worker-jrbf.c.example-com.internal Ready 5m v1.7.3+coreos.0 +yavin-worker-mzdm.c.example-com.internal Ready 5m v1.7.3+coreos.0 +``` + +List the default pods. + +``` +$ kubectl get pods --all-namespaces +NAMESPACE NAME READY STATUS RESTARTS AGE +kube-system etcd-operator-3329263108-f443m 1/1 Running 1 6m +kube-system kube-apiserver-zppls 1/1 Running 0 6m +kube-system kube-controller-manager-3271970485-gh9kt 1/1 Running 0 6m +kube-system kube-controller-manager-3271970485-h90v8 1/1 Running 1 6m +kube-system kube-dns-1187388186-zj5dl 3/3 Running 0 6m +kube-system kube-etcd-0000 1/1 Running 0 5m +kube-system kube-etcd-network-checkpointer-crznb 1/1 Running 0 6m +kube-system kube-flannel-1cs8z 2/2 Running 0 6m +kube-system kube-flannel-d1l5b 2/2 Running 0 6m +kube-system kube-flannel-sp9ps 2/2 Running 0 6m +kube-system kube-proxy-117v6 1/1 Running 0 6m +kube-system kube-proxy-9886n 1/1 Running 0 6m +kube-system kube-proxy-njn47 1/1 Running 0 6m +kube-system kube-scheduler-3895335239-5x87r 1/1 Running 0 6m +kube-system kube-scheduler-3895335239-bzrrt 1/1 Running 1 6m +kube-system pod-checkpointer-l6lrt 1/1 Running 0 6m +``` + +## Background + +Typhoon powers the author's cloud and colocation clusters. The project has evolved through operational experience and Kubernetes changes. Typhoon is shared under a free license to allow others to use the work freely and contribute to its upkeep. + +Typhoon addresses real world needs, which you may share. It is honest about limitations or areas that aren't mature yet. It avoids buzzword bingo and hype. It does not aim to be the one-solution-fits-all distro. An ecosystem of free (or enterprise) Kubernetes distros is healthy. + +## Social Contract + +Typhoon is not a product, trial, or free-tier. It is not run by a company, does not offer support or services, and does not accept or make any money. It is not associated with any operating system or platform vendor. + +Typhoon clusters will contain only [free](https://www.debian.org/intro/free) components. Cluster components will not collect data on users without their permission. + +*Disclosure: The author works for CoreOS and previously wrote Matchbox and original Tectonic for bare-metal and AWS. This project is not associated with CoreOS.* diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..ce276df6 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,55 @@ +site_name: Typhoon +theme: material +site_favicon: 'img/favicon.ico' +repo_name: 'poseidon/typhoon' +repo_url: 'https://github.com/poseidon/typhoon' +extra: + palette: + primary: 'blue' + accent: 'light blue' + logo: 'img/spin.png' + font: + text: 'Roboto Slab' + code: 'Roboto Mono' + social: + - type: 'github' + link: 'https://github.com/poseidon' + - type: 'twitter' + link: 'https://twitter.com/typhoon8s' +google_analytics: + - 'UA-38995133-6' + - 'auto' +markdown_extensions: + - admonition + - codehilite + - footnotes + - toc(permalink=true) + - pymdownx.arithmatex + - pymdownx.betterem(smart_enable=all) + - pymdownx.caret + - pymdownx.critic + - pymdownx.emoji: + emoji_generator: !!python/name:pymdownx.emoji.to_svg + - pymdownx.inlinehilite + - pymdownx.magiclink + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.superfences + - pymdownx.tasklist(custom_checkbox=true) + - pymdownx.tilde +pages: + - Home: 'index.md' + - 'Concepts': 'concepts.md' + - 'Bare-Metal': 'bare-metal.md' + - 'Digital Ocean': 'digital-ocean.md' + - 'Google Cloud': 'google-cloud.md' + - 'Addons': + - 'Overview': 'addons/overview.md' + - 'Ingress Controller': 'addons/ingress.md' + - 'Network Policy': 'addons/calico.md' + - 'Heapster': 'addons/heapster.md' + - 'Dashboard': 'addons/dashboard.md' + - 'CLUO': 'addons/cluo.md' + - 'FAQ': 'faq.md' + - 'Advanced': + - 'Customization': 'advanced/customization.md' diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..4b3554a1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +mkdocs==0.16.3 +mkdocs-material==1.8.0 +pygments==2.2.0 +pymdown-extensions==3.5