From 37ca993542c1a4d5cfe17b451bb6945fa9dda49a Mon Sep 17 00:00:00 2001 From: MengLinMaker Date: Sat, 21 Dec 2024 15:50:12 +1100 Subject: [PATCH 01/10] move variables to variables.tf --- terraform/proxmox.tf | 12 ------------ terraform/talos.tf | 10 ---------- terraform/variables.tf | 21 +++++++++++++++++++++ 3 files changed, 21 insertions(+), 22 deletions(-) create mode 100644 terraform/variables.tf diff --git a/terraform/proxmox.tf b/terraform/proxmox.tf index 4845def..f94b970 100644 --- a/terraform/proxmox.tf +++ b/terraform/proxmox.tf @@ -1,15 +1,3 @@ -variable "proxmox_config" { - description = "Proxmox configuration" - type = object({ - name = string - endpoint = string - username = string - password = string - image_store_id = string - vm_store_id = string - }) -} - provider "proxmox" { endpoint = var.proxmox_config.endpoint username = var.proxmox_config.username diff --git a/terraform/talos.tf b/terraform/talos.tf index 54477dc..cff9c38 100644 --- a/terraform/talos.tf +++ b/terraform/talos.tf @@ -1,13 +1,3 @@ -variable "talos_cluster_config" { - description = "Talos cluster configuration" - type = object({ - name = string - gateway = string - control_plane_ip = string - worker_ip = string - }) -} - resource "talos_machine_secrets" "this" {} data "talos_client_configuration" "this" { diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..528ee82 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,21 @@ +variable "proxmox_config" { + description = "Proxmox configuration" + type = object({ + name = string + endpoint = string + username = string + password = string + image_store_id = string + vm_store_id = string + }) +} + +variable "talos_cluster_config" { + description = "Talos cluster configuration" + type = object({ + name = string + gateway = string + control_plane_ip = string + worker_ip = string + }) +} From 158948cfff235f134245f493563f352a99f52d8d Mon Sep 17 00:00:00 2001 From: MengLinMaker Date: Sat, 21 Dec 2024 16:03:42 +1100 Subject: [PATCH 02/10] spread docs --- README.md | 71 ++++++++++----------------------------------- docs/fan.md | 35 ++++++++++++++++++++++ docs/tailscale.md | 32 ++++++++++++++++++++ terraform/README.md | 18 ++++++++++++ 4 files changed, 100 insertions(+), 56 deletions(-) create mode 100644 docs/fan.md create mode 100644 docs/tailscale.md create mode 100644 terraform/README.md diff --git a/README.md b/README.md index f3be159..b316b10 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,24 @@ -# Homelab -My homelab setup with Dell PowerEdge R730 running Proxmox and K0s +# MengLinMaker's homelab +My homelab setup with Dell PowerEdge R730 running Proxmox, Talos and K0s, provisioned with kubernetes. -## Fan control -To silence the fans, install `ipmitool` +## Quick start +1. Install dependencies ```bash -apt install ipmitool -y +brew install terraform tfsec +pnpm i ``` - -Enable manual fan control -```bash -ipmitool raw 0x30 0x30 0x01 0x00 -``` - -Disable manual fan control +2. Deploy ```bash -ipmitool raw 0x30 0x30 0x01 0x01 +pnpm dep ``` -Turn fan to 10% - 0xA is 10 +_Note: may need to login to terraform_ ```bash -ipmitool raw 0x30 0x30 0x02 0xff 0xA +terraform login ``` -Turn fan to 20% - 0x19 is 20 -```bash -ipmitool raw 0x30 0x30 0x02 0xff 0x19 -``` - -Monitor some relevant metrics -```bash -ipmitool sdr elist full -``` - -## Tailscale VPN -Setup tailscale to access network externally - -### Proxmox access -1. [Install tailscale client on access devices](https://tailscale.com/download) -2. [Install tailscale on target device](https://tailscale.com/kb/1031/install-linux) -```bash -curl -fsSL https://tailscale.com/install.sh | sh -``` -3. Access target device by enabling tailscale VPN and access via assigned IPv4 or IPv6 - -### LXC access -Instructions taken from https://dustri.org/b/running-tailscale-inside-of-a-proxmox-container.html - -1. Add some lxc config in proxmox specifying correct ID - 100 in this example -```bash -echo 'lxc.cgroup.devices.allow: c 10:200 rwm' >> /etc/pve/lxc/100.conf -echo 'lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file' >> /etc/pve/lxc/100.conf -``` -2. Enter lxc container -```bash -pct enter 100 -``` -3. Install and configure tailscale -```bash -curl -fsSL https://tailscale.com/install.sh | sh -echo 'net.ipv4.ip_forward = 1' | tee -a /etc/sysctl.conf -echo 'net.ipv6.conf.all.forwarding = 1' | tee -a /etc/sysctl.conf -sysctl -p /etc/sysctl.conf -reboot -``` +## Layout +Folder structure: +- `docs` - helpful commands +- `kubernetes` - all the kubernetes related configs +- `terraform` - config for deploying infra on proxmox diff --git a/docs/fan.md b/docs/fan.md new file mode 100644 index 0000000..a825cb5 --- /dev/null +++ b/docs/fan.md @@ -0,0 +1,35 @@ +# Fan control +The fans of the r730 is quite loud. So here are some commands to lower fan speeds. + +## Install +To silence the fans, install `ipmitool` +```bash +apt install ipmitool -y +``` + +## Enable control +Enable manual fan control +```bash +ipmitool raw 0x30 0x30 0x01 0x00 +``` + +Disable manual fan control +```bash +ipmitool raw 0x30 0x30 0x01 0x01 +``` + +# Change speed +Turn fan to 10% - 0xA is 10 +```bash +ipmitool raw 0x30 0x30 0x02 0xff 0xA +``` + +Turn fan to 20% - 0x19 is 20 +```bash +ipmitool raw 0x30 0x30 0x02 0xff 0x19 +``` + +Monitor some relevant metrics +```bash +ipmitool sdr elist full +``` diff --git a/docs/tailscale.md b/docs/tailscale.md new file mode 100644 index 0000000..34e1f9e --- /dev/null +++ b/docs/tailscale.md @@ -0,0 +1,32 @@ + +# Tailscale VPN +Access the homelab on a different network, using tailscale. + +## Proxmox access +1. [Install tailscale client on access devices](https://tailscale.com/download) +2. [Install tailscale on target device](https://tailscale.com/kb/1031/install-linux) +```bash +curl -fsSL https://tailscale.com/install.sh | sh +``` +3. Access target device by enabling tailscale VPN and access via assigned IPv4 or IPv6 + +## LXC access +Instructions taken from https://dustri.org/b/running-tailscale-inside-of-a-proxmox-container.html + +1. Add some lxc config in proxmox specifying correct ID - 100 in this example +```bash +echo 'lxc.cgroup.devices.allow: c 10:200 rwm' >> /etc/pve/lxc/100.conf +echo 'lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file' >> /etc/pve/lxc/100.conf +``` +2. Enter lxc container +```bash +pct enter 100 +``` +3. Install and configure tailscale +```bash +curl -fsSL https://tailscale.com/install.sh | sh +echo 'net.ipv4.ip_forward = 1' | tee -a /etc/sysctl.conf +echo 'net.ipv6.conf.all.forwarding = 1' | tee -a /etc/sysctl.conf +sysctl -p /etc/sysctl.conf +reboot +``` diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 0000000..7bf5311 --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,18 @@ +# Terraform +This folder holds all the Terraform scripts for provisioning the VMs. + +## Deployment +1. Install dependencies +```bash +brew install terraform tfsec +pnpm i +``` +2. Deploy +```bash +pnpm dep +``` + +_Note: may need to login to terraform_ +```bash +terraform login +``` From 07d16c0ff2e7b0abecb7b72b93212b5726ac339a Mon Sep 17 00:00:00 2001 From: MengLinMaker Date: Sat, 21 Dec 2024 16:09:22 +1100 Subject: [PATCH 03/10] specify need to provide tfvars --- README.md | 10 ++++------ terraform/README.md | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b316b10..dd804f4 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,14 @@ My homelab setup with Dell PowerEdge R730 running Proxmox, Talos and K0s, provis brew install terraform tfsec pnpm i ``` -2. Deploy +2. Provide env variables: + - `terraform login` + - Create `terraform/terraform.tfvars` according the `terraform/variables.tf` +3. Deploy ```bash pnpm dep ``` -_Note: may need to login to terraform_ -```bash -terraform login -``` - ## Layout Folder structure: - `docs` - helpful commands diff --git a/terraform/README.md b/terraform/README.md index 7bf5311..42131cb 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -7,12 +7,10 @@ This folder holds all the Terraform scripts for provisioning the VMs. brew install terraform tfsec pnpm i ``` -2. Deploy +2. Provide env variables: + - `terraform login` + - Create `terraform.tfvars` according the `variables.tf` +3. Deploy ```bash pnpm dep ``` - -_Note: may need to login to terraform_ -```bash -terraform login -``` From 73d0935e91920d647147bfb0971166810ed82eff Mon Sep 17 00:00:00 2001 From: MengLinMaker Date: Sat, 21 Dec 2024 16:11:45 +1100 Subject: [PATCH 04/10] attribute blog for terraform config --- README.md | 3 +++ terraform/README.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dd804f4..3aa85e2 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,6 @@ Folder structure: - `docs` - helpful commands - `kubernetes` - all the kubernetes related configs - `terraform` - config for deploying infra on proxmox + +## Attribution: +- Terraform setup inspired by [this blog](https://olav.ninja/talos-cluster-on-proxmox-with-terraform) diff --git a/terraform/README.md b/terraform/README.md index 42131cb..33648dc 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -1,5 +1,5 @@ # Terraform -This folder holds all the Terraform scripts for provisioning the VMs. +This folder holds all the Terraform scripts for provisioning the VMs. The config is inspired by [this blog](https://olav.ninja/talos-cluster-on-proxmox-with-terraform) ## Deployment 1. Install dependencies From 4dc785fbbbd59681b33e8b0375b1d8bfe44753f1 Mon Sep 17 00:00:00 2001 From: MengLinMaker Date: Sat, 21 Dec 2024 16:45:43 +1100 Subject: [PATCH 05/10] specify install talosctl --- README.md | 3 ++- terraform/README.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3aa85e2..6f280e0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ My homelab setup with Dell PowerEdge R730 running Proxmox, Talos and K0s, provis ## Quick start 1. Install dependencies ```bash -brew install terraform tfsec +brew install terraform tfsec siderolabs/tap/talosctl + pnpm i ``` 2. Provide env variables: diff --git a/terraform/README.md b/terraform/README.md index 33648dc..ffca163 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -4,7 +4,8 @@ This folder holds all the Terraform scripts for provisioning the VMs. The config ## Deployment 1. Install dependencies ```bash -brew install terraform tfsec +brew install terraform tfsec siderolabs/tap/talosctl + pnpm i ``` 2. Provide env variables: From 796395e0e695b2b56df475ca05121572f557c9e5 Mon Sep 17 00:00:00 2001 From: MengLinMaker Date: Sat, 21 Dec 2024 20:53:02 +1100 Subject: [PATCH 06/10] ignore .DS_Store --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 47bf8e4..66727ad 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ # Node node_modules + +# Misc +.DS_Store From 1e2b1fc7f5bb1d39b2c4e30afaef482c4f8e9f3a Mon Sep 17 00:00:00 2001 From: MengLinMaker Date: Sat, 21 Dec 2024 20:53:18 +1100 Subject: [PATCH 07/10] deploy save config --- terraform/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/package.json b/terraform/package.json index e208402..c91fa47 100644 --- a/terraform/package.json +++ b/terraform/package.json @@ -4,6 +4,6 @@ "format": "terraform fmt --recursive", "lint": "terraform validate && tfsec . --tfvars-file=terraform.tfvars", "plan": "terraform state pull terraform.tfstate && terraform plan", - "dep": "terraform state pull terraform.tfstate && terraform apply -auto-approve && terraform state push terraform.tfstate" + "dep": "terraform state pull terraform.tfstate && terraform apply -auto-approve && terraform state push terraform.tfstate && terraform output -raw kube_config > ~/.kube/config && terraform output -raw talos_config > ~/.talos/config" } } From cd862da2ca69e226fe31bd9b46361d95bdbe7a75 Mon Sep 17 00:00:00 2001 From: MengLinMaker Date: Sat, 21 Dec 2024 21:23:55 +1100 Subject: [PATCH 08/10] fix unhealthy kalos cluster by redeploying --- terraform/qcow2_images.tf | 29 ----------------------------- terraform/talos_image.tf | 9 +++++++++ terraform/talos_services.tf | 23 +++++++++++------------ terraform/talos_vms.tf | 26 +++++++++++++++----------- 4 files changed, 35 insertions(+), 52 deletions(-) delete mode 100644 terraform/qcow2_images.tf create mode 100644 terraform/talos_image.tf diff --git a/terraform/qcow2_images.tf b/terraform/qcow2_images.tf deleted file mode 100644 index 7404eeb..0000000 --- a/terraform/qcow2_images.tf +++ /dev/null @@ -1,29 +0,0 @@ -# talos - with extensions `qemu-guest-agent` + `tailscale` + `glibc` -resource "proxmox_virtual_environment_download_file" "talos_qcow2" { - content_type = "iso" - datastore_id = var.proxmox_config.image_store_id - node_name = var.proxmox_config.name - file_name = "talos.qcow2.img" - url = "https://factory.talos.dev/image/9ee49bb5f44200889652309e9af03195a9ed7a13049dd310180aa00e5ed3a7c2/v1.9.0/nocloud-amd64.qcow2" - overwrite = false -} - -# debian -resource "proxmox_virtual_environment_download_file" "debian_qcow2" { - content_type = "iso" - datastore_id = var.proxmox_config.image_store_id - node_name = var.proxmox_config.name - file_name = "debian.qcow2.img" - url = "https://cdimage.debian.org/images/cloud/bookworm/20241201-1948/debian-12-nocloud-amd64-20241201-1948.qcow2" - overwrite = false -} - -# almalinux -resource "proxmox_virtual_environment_download_file" "almalinux_qcow2" { - content_type = "iso" - datastore_id = var.proxmox_config.image_store_id - node_name = var.proxmox_config.name - file_name = "almalinux.qcow2.img" - url = "https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/AlmaLinux-9-GenericCloud-9.5-20241120.x86_64.qcow2" - overwrite = false -} diff --git a/terraform/talos_image.tf b/terraform/talos_image.tf new file mode 100644 index 0000000..456ab4f --- /dev/null +++ b/terraform/talos_image.tf @@ -0,0 +1,9 @@ +# talos - with extensions `qemu-guest-agent` + `glibc` +resource "proxmox_virtual_environment_download_file" "talos_qcow2" { + content_type = "iso" + datastore_id = var.proxmox_config.image_store_id + node_name = var.proxmox_config.name + file_name = "talos.qcow2.img" + url = "https://factory.talos.dev/image/a98370e8bc36e336e1de99db6bbc23b8a0ae03820a474d8a2e964cfeaece9922/v1.9.0/nocloud-amd64.qcow2" + overwrite = true +} diff --git a/terraform/talos_services.tf b/terraform/talos_services.tf index 73192cc..52985b4 100644 --- a/terraform/talos_services.tf +++ b/terraform/talos_services.tf @@ -1,17 +1,16 @@ -# data "talos_cluster_health" "this" { -# depends_on = [talos_machine_configuration_apply.control_plane_config_apply, talos_machine_configuration_apply.worker_config_apply] -# client_configuration = data.talos_client_configuration.this.client_configuration -# control_plane_nodes = [var.talos_cluster_config.control_plane_ip] -# worker_nodes = [var.talos_cluster_config.worker_ip] -# endpoints = data.talos_client_configuration.this.endpoints -# timeouts = { -# read = "30s" -# } -# } +data "talos_cluster_health" "this" { + depends_on = [talos_machine_configuration_apply.control_plane_config_apply, talos_machine_configuration_apply.worker_config_apply] + client_configuration = data.talos_client_configuration.this.client_configuration + control_plane_nodes = [var.talos_cluster_config.control_plane_ip] + worker_nodes = [var.talos_cluster_config.worker_ip] + endpoints = data.talos_client_configuration.this.endpoints + timeouts = { + read = "10m" + } +} resource "talos_cluster_kubeconfig" "talos_kubeconfig" { - # depends_on = [talos_machine_bootstrap.talos_bootstrap, data.talos_cluster_health.this] - depends_on = [talos_machine_bootstrap.talos_bootstrap] + depends_on = [talos_machine_bootstrap.talos_bootstrap, data.talos_cluster_health.this] client_configuration = talos_machine_secrets.this.client_configuration node = var.talos_cluster_config.control_plane_ip } diff --git a/terraform/talos_vms.tf b/terraform/talos_vms.tf index 174852e..ee153bc 100644 --- a/terraform/talos_vms.tf +++ b/terraform/talos_vms.tf @@ -5,10 +5,13 @@ resource "proxmox_virtual_environment_vm" "talos_control_plane_vm" { node_name = var.proxmox_config.name vm_id = 800 - on_boot = true + bios = "seabios" + machine = "q35" + on_boot = true + stop_on_destroy = true cpu { cores = 4 - type = "x86-64-v2-AES" + type = "host" } memory { dedicated = 4000 @@ -17,14 +20,13 @@ resource "proxmox_virtual_environment_vm" "talos_control_plane_vm" { agent { enabled = true } - stop_on_destroy = true network_device { bridge = "vmbr0" } disk { datastore_id = var.proxmox_config.vm_store_id file_id = proxmox_virtual_environment_download_file.talos_qcow2.id - file_format = "raw" + file_format = "qcow2" interface = "virtio0" size = 10 } @@ -53,26 +55,28 @@ resource "proxmox_virtual_environment_vm" "talos_worker_vm" { node_name = var.proxmox_config.name vm_id = 900 - on_boot = true + bios = "seabios" + machine = "q35" + on_boot = true + stop_on_destroy = true cpu { - cores = 12 - type = "x86-64-v2-AES" + cores = 16 + type = "host" } memory { - dedicated = 64000 - floating = 64000 # enable memory ballooning + dedicated = 32000 + floating = 32000 # enable memory ballooning } agent { enabled = true } - stop_on_destroy = true network_device { bridge = "vmbr0" } disk { datastore_id = var.proxmox_config.vm_store_id file_id = proxmox_virtual_environment_download_file.talos_qcow2.id - file_format = "raw" + file_format = "qcow2" interface = "virtio0" size = 10 } From eec965e614c77a9e1bfa1ad9f19d1eeab06ea6f5 Mon Sep 17 00:00:00 2001 From: MengLinMaker Date: Sat, 21 Dec 2024 23:56:28 +1100 Subject: [PATCH 09/10] better subtitles for fan doc --- docs/fan.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/fan.md b/docs/fan.md index a825cb5..c393e88 100644 --- a/docs/fan.md +++ b/docs/fan.md @@ -18,7 +18,7 @@ Disable manual fan control ipmitool raw 0x30 0x30 0x01 0x01 ``` -# Change speed +## Change speed Turn fan to 10% - 0xA is 10 ```bash ipmitool raw 0x30 0x30 0x02 0xff 0xA @@ -29,6 +29,7 @@ Turn fan to 20% - 0x19 is 20 ipmitool raw 0x30 0x30 0x02 0xff 0x19 ``` +## Monitor Monitor some relevant metrics ```bash ipmitool sdr elist full From 69a15374fc2a4345e93a9dec36ac7245c775a499 Mon Sep 17 00:00:00 2001 From: MengLinMaker Date: Sat, 21 Dec 2024 23:56:51 +1100 Subject: [PATCH 10/10] example kubernetes application from google --- kubernetes/guestbook-go/.gitignore | 1 + kubernetes/guestbook-go/Dockerfile | 30 ++ kubernetes/guestbook-go/Makefile | 36 +++ kubernetes/guestbook-go/README.md | 271 ++++++++++++++++++ .../guestbook-go/guestbook-controller.yaml | 21 ++ kubernetes/guestbook-go/guestbook-page.png | Bin 0 -> 40028 bytes .../guestbook-go/guestbook-service.yaml | 13 + kubernetes/guestbook-go/main.go | 91 ++++++ kubernetes/guestbook-go/public/index.html | 34 +++ kubernetes/guestbook-go/public/script.js | 46 +++ kubernetes/guestbook-go/public/style.css | 61 ++++ .../guestbook-go/redis-master-controller.yaml | 24 ++ .../guestbook-go/redis-master-service.yaml | 14 + .../redis-replica-controller.yaml | 24 ++ .../guestbook-go/redis-replica-service.yaml | 14 + 15 files changed, 680 insertions(+) create mode 100644 kubernetes/guestbook-go/.gitignore create mode 100644 kubernetes/guestbook-go/Dockerfile create mode 100644 kubernetes/guestbook-go/Makefile create mode 100644 kubernetes/guestbook-go/README.md create mode 100644 kubernetes/guestbook-go/guestbook-controller.yaml create mode 100644 kubernetes/guestbook-go/guestbook-page.png create mode 100644 kubernetes/guestbook-go/guestbook-service.yaml create mode 100644 kubernetes/guestbook-go/main.go create mode 100644 kubernetes/guestbook-go/public/index.html create mode 100644 kubernetes/guestbook-go/public/script.js create mode 100644 kubernetes/guestbook-go/public/style.css create mode 100644 kubernetes/guestbook-go/redis-master-controller.yaml create mode 100644 kubernetes/guestbook-go/redis-master-service.yaml create mode 100644 kubernetes/guestbook-go/redis-replica-controller.yaml create mode 100644 kubernetes/guestbook-go/redis-replica-service.yaml diff --git a/kubernetes/guestbook-go/.gitignore b/kubernetes/guestbook-go/.gitignore new file mode 100644 index 0000000..a45a95c --- /dev/null +++ b/kubernetes/guestbook-go/.gitignore @@ -0,0 +1 @@ +guestbook_bin diff --git a/kubernetes/guestbook-go/Dockerfile b/kubernetes/guestbook-go/Dockerfile new file mode 100644 index 0000000..bf25e7d --- /dev/null +++ b/kubernetes/guestbook-go/Dockerfile @@ -0,0 +1,30 @@ +# Copyright 2016 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM golang:1.10.0 +RUN go get github.com/codegangsta/negroni \ + github.com/gorilla/mux \ + github.com/xyproto/simpleredis/v2 +WORKDIR /app +ADD ./main.go . +RUN CGO_ENABLED=0 GOOS=linux go build -o main . + +FROM scratch +WORKDIR /app +COPY --from=0 /app/main . +COPY ./public/index.html public/index.html +COPY ./public/script.js public/script.js +COPY ./public/style.css public/style.css +CMD ["/app/main"] +EXPOSE 3000 diff --git a/kubernetes/guestbook-go/Makefile b/kubernetes/guestbook-go/Makefile new file mode 100644 index 0000000..105130e --- /dev/null +++ b/kubernetes/guestbook-go/Makefile @@ -0,0 +1,36 @@ +# Copyright 2016 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Build the guestbook-go example + +# Usage: +# [VERSION=v3] [REGISTRY="staging-k8s.gcr.io"] make build +VERSION?=v3 +REGISTRY?=staging-k8s.gcr.io + +release: clean build push clean + +# builds a docker image that builds the app and packages it into a minimal docker image +build: + docker buildx build --load -t ${REGISTRY}/guestbook:${VERSION} . + +# push the image to an registry +push: + docker buildx build --push -t ${REGISTRY}/guestbook:${VERSION} . + +# remove previous images and containers +clean: + docker rm -f ${REGISTRY}/guestbook:${VERSION} 2> /dev/null || true + +.PHONY: release clean build push diff --git a/kubernetes/guestbook-go/README.md b/kubernetes/guestbook-go/README.md new file mode 100644 index 0000000..f136d47 --- /dev/null +++ b/kubernetes/guestbook-go/README.md @@ -0,0 +1,271 @@ +## Guestbook Example + +This example shows how to build a simple multi-tier web application using Kubernetes and Docker. The application consists of a web front end, Redis master for storage, and replicated set of Redis replicas, all for which we will create Kubernetes replication controllers, pods, and services. + +If you are running a cluster in Google Container Engine (GKE), instead see the [Guestbook Example for Google Container Engine](https://cloud.google.com/container-engine/docs/tutorials/guestbook). + +##### Table of Contents + + * [Step Zero: Prerequisites](#step-zero) + * [Step One: Create the Redis master pod](#step-one) + * [Step Two: Create the Redis master service](#step-two) + * [Step Three: Create the Redis replica pods](#step-three) + * [Step Four: Create the Redis replica service](#step-four) + * [Step Five: Create the guestbook pods](#step-five) + * [Step Six: Create the guestbook service](#step-six) + * [Step Seven: View the guestbook](#step-seven) + * [Step Eight: Cleanup](#step-eight) + +### Step Zero: Prerequisites + +This example assumes that you have a working cluster. See the [Getting Started Guides](https://kubernetes.io/docs/setup/) for details about creating a cluster. + +**Tip:** View all the `kubectl` commands, including their options and descriptions in the [kubectl CLI reference](https://kubernetes.io/docs/user-guide/kubectl-overview/). + +### Step One: Create the Redis master pod + +Use the `examples/guestbook-go/redis-master-controller.yaml` file to create a [replication controller](https://kubernetes.io/docs/concepts/workloads/controllers/replicationcontroller/) and Redis master [pod](https://kubernetes.io/docs/concepts/workloads/pods/pod-overview/). The pod runs a Redis key-value server in a container. Using a replication controller is the preferred way to launch long-running pods, even for 1 replica, so that the pod benefits from the self-healing mechanism in Kubernetes (keeps the pods alive). + +1. Use the [redis-master-controller.yaml](redis-master-controller.yaml) file to create the Redis master replication controller in your Kubernetes cluster by running the `kubectl create -f` *`filename`* command: + + ```console + $ kubectl create -f examples/guestbook-go/redis-master-controller.yaml + + ``` + +2. To verify that the redis-master controller is up, list the replication controllers you created in the cluster with the `kubectl get rc` command(if you don't specify a `--namespace`, the `default` namespace will be used. The same below): + + ```console + $ kubectl get rc + CONTROLLER CONTAINER(S) IMAGE(S) SELECTOR REPLICAS + redis-master redis-master gurpartap/redis app=redis,role=master 1 + ... + ``` + + Result: The replication controller then creates the single Redis master pod. + +3. To verify that the redis-master pod is running, list the pods you created in cluster with the `kubectl get pods` command: + + ```console + $ kubectl get pods + NAME READY STATUS RESTARTS AGE + redis-master-xx4uv 1/1 Running 0 1m + ... + ``` + + Result: You'll see a single Redis master pod and the machine where the pod is running after the pod gets placed (may take up to thirty seconds). + +4. To verify what containers are running in the redis-master pod, you can SSH to that machine with `gcloud compute ssh --zone` *`zone_name`* *`host_name`* and then run `docker ps`: + + ```console + me@workstation$ gcloud compute ssh --zone us-central1-b kubernetes-node-bz1p + + me@kubernetes-node-3:~$ sudo docker ps + CONTAINER ID IMAGE COMMAND CREATED STATUS + d5c458dabe50 redis "/entrypoint.sh redis" 5 minutes ago Up 5 minutes + ``` + + Note: The initial `docker pull` can take a few minutes, depending on network conditions. + +### Step Two: Create the Redis master service + +A Kubernetes [service](https://kubernetes.io/docs/concepts/services-networking/service/) is a named load balancer that proxies traffic to one or more pods. The services in a Kubernetes cluster are discoverable inside other pods via environment variables or DNS. + +Services find the pods to load balance based on pod labels. The pod that you created in Step One has the label `app=redis` and `role=master`. The selector field of the service determines which pods will receive the traffic sent to the service. + +1. Use the [redis-master-service.yaml](redis-master-service.yaml) file to create the service in your Kubernetes cluster by running the `kubectl create -f` *`filename`* command: + + ```console + $ kubectl create -f examples/guestbook-go/redis-master-service.yaml + + ``` + +2. To verify that the redis-master service is up, list the services you created in the cluster with the `kubectl get services` command: + + ```console + $ kubectl get services + NAME CLUSTER_IP EXTERNAL_IP PORT(S) SELECTOR AGE + redis-master 10.0.136.3 6379/TCP app=redis,role=master 1h + ... + ``` + + Result: All new pods will see the `redis-master` service running on the host (`$REDIS_MASTER_SERVICE_HOST` environment variable) at port 6379, or running on `redis-master:6379`. After the service is created, the service proxy on each node is configured to set up a proxy on the specified port (in our example, that's port 6379). + + +### Step Three: Create the Redis replica pods + +The Redis master we created earlier is a single pod (REPLICAS = 1), while the Redis read replicas we are creating here are 'replicated' pods. In Kubernetes, a replication controller is responsible for managing the multiple instances of a replicated pod. + +1. Use the file [redis-replica-controller.yaml](redis-replica-controller.yaml) to create the replication controller by running the `kubectl create -f` *`filename`* command: + + ```console + $ kubectl create -f examples/guestbook-go/redis-replica-controller.yaml + + ``` + +2. To verify that the redis-replica controller is running, run the `kubectl get rc` command: + + ```console + $ kubectl get rc + CONTROLLER CONTAINER(S) IMAGE(S) SELECTOR REPLICAS + redis-master redis-master redis app=redis,role=master 1 + redis-replica redis-replica registry.k8s.io/redis-slave:v2 app=redis,role=replica 2 + ... + ``` + + Result: The replication controller creates and configures the Redis replica pods through the redis-master service (name:port pair, in our example that's `redis-master:6379`). + + Example: + The Redis replicas get started by the replication controller with the following command: + + ```console + redis-server --replicaof redis-master 6379 + ``` + +3. To verify that the Redis master and replicas pods are running, run the `kubectl get pods` command: + + ```console + $ kubectl get pods + NAME READY STATUS RESTARTS AGE + redis-master-xx4uv 1/1 Running 0 18m + redis-replica-b6wj4 1/1 Running 0 1m + redis-replica-iai40 1/1 Running 0 1m + ... + ``` + + Result: You see the single Redis master and two Redis replica pods. + +### Step Four: Create the Redis replica service + +Just like the master, we want to have a service to proxy connections to the read replicas. In this case, in addition to discovery, the Redis replica service provides transparent load balancing to clients. + +1. Use the [redis-replica-service.yaml](redis-replica-service.yaml) file to create the Redis replica service by running the `kubectl create -f` *`filename`* command: + + ```console + $ kubectl create -f examples/guestbook-go/redis-replica-service.yaml + + ``` + +2. To verify that the redis-replica service is up, list the services you created in the cluster with the `kubectl get services` command: + + ```console + $ kubectl get services + NAME CLUSTER_IP EXTERNAL_IP PORT(S) SELECTOR AGE + redis-master 10.0.136.3 6379/TCP app=redis,role=master 1h + redis-replica 10.0.21.92 6379/TCP app-redis,role=replica 1h + ... + ``` + + Result: The service is created with labels `app=redis` and `role=replica` to identify that the pods are running the Redis replicas. + +Tip: It is helpful to set labels on your services themselves--as we've done here--to make it easy to locate them later. + +### Step Five: Create the guestbook pods + +This is a simple Go `net/http` ([negroni](https://github.com/codegangsta/negroni) based) server that is configured to talk to either the replica or master services depending on whether the request is a read or a write. The pods we are creating expose a simple JSON interface and serves a jQuery-Ajax based UI. Like the Redis replica pods, these pods are also managed by a replication controller. + +1. Use the [guestbook-controller.yaml](guestbook-controller.yaml) file to create the guestbook replication controller by running the `kubectl create -f` *`filename`* command: + + ```console + $ kubectl create -f examples/guestbook-go/guestbook-controller.yaml + + ``` + + Tip: If you want to modify the guestbook code open the `_src` of this example and read the README.md and the Makefile. If you have pushed your custom image be sure to update the `image` accordingly in the guestbook-controller.yaml. + +2. To verify that the guestbook replication controller is running, run the `kubectl get rc` command: + + ```console + $ kubectl get rc + CONTROLLER CONTAINER(S) IMAGE(S) SELECTOR REPLICAS + guestbook guestbook registry.k8s.io/guestbook:v3 app=guestbook 3 + redis-master redis-master redis app=redis,role=master 1 + redis-replica redis-replica registry.k8s.io/redis-replica:v2 app=redis,role=replica 2 + ... + ``` + +3. To verify that the guestbook pods are running (it might take up to thirty seconds to create the pods), list the pods you created in cluster with the `kubectl get pods` command: + + ```console + $ kubectl get pods + NAME READY STATUS RESTARTS AGE + guestbook-3crgn 1/1 Running 0 2m + guestbook-gv7i6 1/1 Running 0 2m + guestbook-x405a 1/1 Running 0 2m + redis-master-xx4uv 1/1 Running 0 23m + redis-replica-b6wj4 1/1 Running 0 6m + redis-replica-iai40 1/1 Running 0 6m + ... + ``` + + Result: You see a single Redis master, two Redis replicas, and three guestbook pods. + +### Step Six: Create the guestbook service + +Just like the others, we create a service to group the guestbook pods but this time, to make the guestbook front end externally visible, we specify `"type": "LoadBalancer"`. + +1. Use the [guestbook-service.yaml](guestbook-service.yaml) file to create the guestbook service by running the `kubectl create -f` *`filename`* command: + + ```console + $ kubectl create -f examples/guestbook-go/guestbook-service.yaml + ``` + + +2. To verify that the guestbook service is up, list the services you created in the cluster with the `kubectl get services` command: + + ```console + $ kubectl get services + NAME CLUSTER_IP EXTERNAL_IP PORT(S) SELECTOR AGE + guestbook 10.0.217.218 146.148.81.8 3000/TCP app=guestbook 1h + redis-master 10.0.136.3 6379/TCP app=redis,role=master 1h + redis-replica 10.0.21.92 6379/TCP app-redis,role=replica 1h + ... + ``` + + Result: The service is created with label `app=guestbook`. + +### Step Seven: View the guestbook + +You can now play with the guestbook that you just created by opening it in a browser (it might take a few moments for the guestbook to come up). + + * **Local Host:** + If you are running Kubernetes locally, to view the guestbook, navigate to `http://localhost:3000` in your browser. + + * **Remote Host:** + 1. To view the guestbook on a remote host, locate the external IP of the load balancer in the **IP** column of the `kubectl get services` output. In our example, the internal IP address is `10.0.217.218` and the external IP address is `146.148.81.8` (*Note: you might need to scroll to see the IP column*). + + 2. Append port `3000` to the IP address (for example `http://146.148.81.8:3000`), and then navigate to that address in your browser. + + Result: The guestbook displays in your browser: + + ![Guestbook](guestbook-page.png) + + **Further Reading:** + If you're using Google Compute Engine, see the details about limiting traffic to specific sources at [Google Compute Engine firewall documentation][gce-firewall-docs]. + +[cloud-console]: https://console.developer.google.com +[gce-firewall-docs]: https://cloud.google.com/compute/docs/networking#firewalls + +### Step Eight: Cleanup + +After you're done playing with the guestbook, you can cleanup by deleting the guestbook service and removing the associated resources that were created, including load balancers, forwarding rules, target pools, and Kubernetes replication controllers and services. + +Delete all the resources by running the following `kubectl delete -f` *`filename`* command: + +```console +$ kubectl delete -f examples/guestbook-go +guestbook-controller +guestbook +redid-master-controller +redis-master +redis-replica-controller +redis-replica +``` + +Tip: To turn down your Kubernetes cluster, follow the corresponding instructions in the version of the +[Getting Started Guides](https://kubernetes.io/docs/getting-started-guides/) that you previously used to create your cluster. + + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/examples/guestbook-go/README.md?pixel)]() + diff --git a/kubernetes/guestbook-go/guestbook-controller.yaml b/kubernetes/guestbook-go/guestbook-controller.yaml new file mode 100644 index 0000000..b1831a4 --- /dev/null +++ b/kubernetes/guestbook-go/guestbook-controller.yaml @@ -0,0 +1,21 @@ +kind: ReplicationController +apiVersion: v1 +metadata: + name: guestbook + labels: + app: guestbook +spec: + replicas: 3 + selector: + app: guestbook + template: + metadata: + labels: + app: guestbook + spec: + containers: + - name: guestbook + image: registry.k8s.io/guestbook:v3 + ports: + - name: http-server + containerPort: 3000 diff --git a/kubernetes/guestbook-go/guestbook-page.png b/kubernetes/guestbook-go/guestbook-page.png new file mode 100644 index 0000000000000000000000000000000000000000..776835fd2dc2202b21f663a17b49916e040d84dc GIT binary patch literal 40028 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 P)gV2%KCI}rRp|c#23x-E literal 0 HcmV?d00001 diff --git a/kubernetes/guestbook-go/guestbook-service.yaml b/kubernetes/guestbook-go/guestbook-service.yaml new file mode 100644 index 0000000..677bde4 --- /dev/null +++ b/kubernetes/guestbook-go/guestbook-service.yaml @@ -0,0 +1,13 @@ +kind: Service +apiVersion: v1 +metadata: + name: guestbook + labels: + app: guestbook +spec: + ports: + - port: 3000 + targetPort: http-server + selector: + app: guestbook + type: LoadBalancer diff --git a/kubernetes/guestbook-go/main.go b/kubernetes/guestbook-go/main.go new file mode 100644 index 0000000..7a57664 --- /dev/null +++ b/kubernetes/guestbook-go/main.go @@ -0,0 +1,91 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "encoding/json" + "net/http" + "os" + "strings" + + "github.com/codegangsta/negroni" + "github.com/gorilla/mux" + "github.com/xyproto/simpleredis/v2" +) + +var ( + masterPool *simpleredis.ConnectionPool + replicaPool *simpleredis.ConnectionPool +) + +func ListRangeHandler(rw http.ResponseWriter, req *http.Request) { + key := mux.Vars(req)["key"] + list := simpleredis.NewList(replicaPool, key) + members := HandleError(list.GetAll()).([]string) + membersJSON := HandleError(json.MarshalIndent(members, "", " ")).([]byte) + rw.Write(membersJSON) +} + +func ListPushHandler(rw http.ResponseWriter, req *http.Request) { + key := mux.Vars(req)["key"] + value := mux.Vars(req)["value"] + list := simpleredis.NewList(masterPool, key) + HandleError(nil, list.Add(value)) + ListRangeHandler(rw, req) +} + +func InfoHandler(rw http.ResponseWriter, req *http.Request) { + info := HandleError(masterPool.Get(0).Do("INFO")).([]byte) + rw.Write(info) +} + +func EnvHandler(rw http.ResponseWriter, req *http.Request) { + environment := make(map[string]string) + for _, item := range os.Environ() { + splits := strings.Split(item, "=") + key := splits[0] + val := strings.Join(splits[1:], "=") + environment[key] = val + } + + envJSON := HandleError(json.MarshalIndent(environment, "", " ")).([]byte) + rw.Write(envJSON) +} + +func HandleError(result interface{}, err error) (r interface{}) { + if err != nil { + panic(err) + } + return result +} + +func main() { + masterPool = simpleredis.NewConnectionPoolHost("redis-master:6379") + defer masterPool.Close() + replicaPool = simpleredis.NewConnectionPoolHost("redis-replica:6379") + defer replicaPool.Close() + + r := mux.NewRouter() + r.Path("/lrange/{key}").Methods("GET").HandlerFunc(ListRangeHandler) + r.Path("/rpush/{key}/{value}").Methods("GET").HandlerFunc(ListPushHandler) + r.Path("/info").Methods("GET").HandlerFunc(InfoHandler) + r.Path("/env").Methods("GET").HandlerFunc(EnvHandler) + + n := negroni.Classic() + n.UseHandler(r) + n.Run(":3000") +} diff --git a/kubernetes/guestbook-go/public/index.html b/kubernetes/guestbook-go/public/index.html new file mode 100644 index 0000000..f525f4b --- /dev/null +++ b/kubernetes/guestbook-go/public/index.html @@ -0,0 +1,34 @@ + + + + + + + + Guestbook + + + + +
+

Waiting for database connection...

+
+ +
+
+ + Submit +
+
+ +
+

+

/env + /info

+
+ + + + diff --git a/kubernetes/guestbook-go/public/script.js b/kubernetes/guestbook-go/public/script.js new file mode 100644 index 0000000..a0a545b --- /dev/null +++ b/kubernetes/guestbook-go/public/script.js @@ -0,0 +1,46 @@ +$(document).ready(function() { + var headerTitleElement = $("#header h1"); + var entriesElement = $("#guestbook-entries"); + var formElement = $("#guestbook-form"); + var submitElement = $("#guestbook-submit"); + var entryContentElement = $("#guestbook-entry-content"); + var hostAddressElement = $("#guestbook-host-address"); + + var appendGuestbookEntries = function(data) { + entriesElement.empty(); + $.each(data, function(key, val) { + entriesElement.append("

" + val + "

"); + }); + } + + var handleSubmission = function(e) { + e.preventDefault(); + var entryValue = entryContentElement.val() + if (entryValue.length > 0) { + entriesElement.append("

...

"); + $.getJSON("rpush/guestbook/" + entryValue, appendGuestbookEntries); + } + return false; + } + + // colors = purple, blue, red, green, yellow + var colors = ["#549", "#18d", "#d31", "#2a4", "#db1"]; + var randomColor = colors[Math.floor(5 * Math.random())]; + (function setElementsColor(color) { + headerTitleElement.css("color", color); + entryContentElement.css("box-shadow", "inset 0 0 0 2px " + color); + submitElement.css("background-color", color); + })(randomColor); + + submitElement.click(handleSubmission); + formElement.submit(handleSubmission); + hostAddressElement.append(document.URL); + + // Poll every second. + (function fetchGuestbook() { + $.getJSON("lrange/guestbook").done(appendGuestbookEntries).always( + function() { + setTimeout(fetchGuestbook, 1000); + }); + })(); +}); diff --git a/kubernetes/guestbook-go/public/style.css b/kubernetes/guestbook-go/public/style.css new file mode 100644 index 0000000..fd1c393 --- /dev/null +++ b/kubernetes/guestbook-go/public/style.css @@ -0,0 +1,61 @@ +body, input { + color: #123; + font-family: "Gill Sans", sans-serif; +} + +div { + overflow: hidden; + padding: 1em 0; + position: relative; + text-align: center; +} + +h1, h2, p, input, a { + font-weight: 300; + margin: 0; +} + +h1 { + color: #BDB76B; + font-size: 3.5em; +} + +h2 { + color: #999; +} + +form { + margin: 0 auto; + max-width: 50em; + text-align: center; +} + +input { + border: 0; + border-radius: 1000px; + box-shadow: inset 0 0 0 2px #BDB76B; + display: inline; + font-size: 1.5em; + margin-bottom: 1em; + outline: none; + padding: .5em 5%; + width: 55%; +} + +form a { + background: #BDB76B; + border: 0; + border-radius: 1000px; + color: #FFF; + font-size: 1.25em; + font-weight: 400; + padding: .75em 2em; + text-decoration: none; + text-transform: uppercase; + white-space: normal; +} + +p { + font-size: 1.5em; + line-height: 1.5; +} diff --git a/kubernetes/guestbook-go/redis-master-controller.yaml b/kubernetes/guestbook-go/redis-master-controller.yaml new file mode 100644 index 0000000..338c76f --- /dev/null +++ b/kubernetes/guestbook-go/redis-master-controller.yaml @@ -0,0 +1,24 @@ +kind: ReplicationController +apiVersion: v1 +metadata: + name: redis-master + labels: + app: redis + role: master +spec: + replicas: 1 + selector: + app: redis + role: master + template: + metadata: + labels: + app: redis + role: master + spec: + containers: + - name: redis-master + image: redis + ports: + - name: redis-server + containerPort: 6379 diff --git a/kubernetes/guestbook-go/redis-master-service.yaml b/kubernetes/guestbook-go/redis-master-service.yaml new file mode 100644 index 0000000..c69456e --- /dev/null +++ b/kubernetes/guestbook-go/redis-master-service.yaml @@ -0,0 +1,14 @@ +kind: Service +apiVersion: v1 +metadata: + name: redis-master + labels: + app: redis + role: master +spec: + ports: + - port: 6379 + targetPort: redis-server + selector: + app: redis + role: master diff --git a/kubernetes/guestbook-go/redis-replica-controller.yaml b/kubernetes/guestbook-go/redis-replica-controller.yaml new file mode 100644 index 0000000..5e10e7d --- /dev/null +++ b/kubernetes/guestbook-go/redis-replica-controller.yaml @@ -0,0 +1,24 @@ +kind: ReplicationController +apiVersion: v1 +metadata: + name: redis-replica + labels: + app: redis + role: replica +spec: + replicas: 2 + selector: + app: redis + role: replica + template: + metadata: + labels: + app: redis + role: replica + spec: + containers: + - name: redis-replica + image: registry.k8s.io/redis-slave:v2 + ports: + - name: redis-server + containerPort: 6379 diff --git a/kubernetes/guestbook-go/redis-replica-service.yaml b/kubernetes/guestbook-go/redis-replica-service.yaml new file mode 100644 index 0000000..191db0b --- /dev/null +++ b/kubernetes/guestbook-go/redis-replica-service.yaml @@ -0,0 +1,14 @@ +kind: Service +apiVersion: v1 +metadata: + name: redis-replica + labels: + app: redis + role: replica +spec: + ports: + - port: 6379 + targetPort: redis-server + selector: + app: redis + role: replica