mirror of
https://github.com/lukaszraczylo/rpi-home-cluster-setup.git
synced 2026-10-20 01:37:07 +00:00
Home K8S setup
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
step1/static/alpine-aarch64.tar.gz
|
||||||
|
step2/static/token_file
|
||||||
|
step2/static/kubectl.conf
|
||||||
|
#
|
||||||
|
|
||||||
|
*/traefik-static-config.yaml
|
||||||
|
*terraform.tfstate*
|
||||||
|
*.log
|
||||||
|
*/.terraform/
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Raspberry Pi kubernetes cluster on Alpine linux setup
|
||||||
|
|
||||||
|
This repository is supposed to help if I'd EVER AGAIN destroy the whole cluster by accident.
|
||||||
|
Code is a result of 6 hours coding marathon, but will be improved and patched in future.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Memory card preparations [step1]
|
||||||
|
|
||||||
|
To prepare memory cards check the **step1** directory.
|
||||||
|
If your Pi memory card is present under /dev/disk5 - you don't need to change anything, otherwise change it to the device of your choice.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PI_CARD="/dev/disk5"
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the following script which will:
|
||||||
|
* Format memory card
|
||||||
|
* Split it into two partitions (1G + remaining)
|
||||||
|
* Copy basic Alpine system onto 1G partition
|
||||||
|
* Add an overlay allowing ethernet interface get up and SSH root access without the password
|
||||||
|
|
||||||
|
```bash
|
||||||
|
001-prepare-card.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cluster preparations [step2]
|
||||||
|
|
||||||
|
#### Before
|
||||||
|
|
||||||
|
* Modify pi-hosts.txt file and adjust it to your setup.
|
||||||
|
* Modify address class in step2/static/k8s-metallb-dashboard-config.yaml to suit your network
|
||||||
|
|
||||||
|
Add following to your ~/.ssh/config file
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Host pi?
|
||||||
|
User root
|
||||||
|
Hostname %h.local
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Prepare your nodes for Ansible
|
||||||
|
|
||||||
|
```bash
|
||||||
|
001-prepare-ansible.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Run the playbook
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ansible-playbook rpi.yaml -f 10
|
||||||
|
```
|
||||||
|
|
||||||
|
### K8S definitions [step3]
|
||||||
|
|
||||||
|
Use makefile from step3 to apply / destroy resources
|
||||||
|
|
||||||
|
#### Required environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TRAEFIK_API_KEY // Traefik API key
|
||||||
|
GH_USER // GitHub user
|
||||||
|
GH_PAT // GitHub Personal Access Token
|
||||||
|
```
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
* Raspberry Pi cluster running Alpine linux as a base
|
||||||
|
* MetalLB for load balancing in your LAN
|
||||||
|
* Kubernetes dashboard installed and exposed via MetalLB
|
||||||
|
* Traefik managing the.. traffic
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This script
|
||||||
|
# - prepares raspberry pi memory card ( /dev/disk5 )
|
||||||
|
# - partitions the memory card
|
||||||
|
# - unpacks RPI rpi base system
|
||||||
|
# - applies headless boot configuration with network interface and ssh agent accepting connections without password
|
||||||
|
|
||||||
|
PI_CARD="/dev/disk5"
|
||||||
|
ALPINE_BASE_IMAGE="https://dl-cdn.alpinelinux.org/alpine/v3.13/releases/aarch64/alpine-rpi-3.13.2-aarch64.tar.gz"
|
||||||
|
# [[ -f /Volumes/RPI0 ]] && echo "Deleting leftover directory"; rm -fr /Volumes/RPI0 || echo "Clean and dandy on the Volumes front"
|
||||||
|
[[ ! -f static/alpine-aarch64.tar.gz ]] && wget $ALPINE_BASE_IMAGE -O static/alpine-aarch64.tar.gz || echo "Alpine baseimg already exists"
|
||||||
|
sudo diskutil eraseDisk FAT32 RPI MBRFormat $PI_CARD
|
||||||
|
sudo diskutil partitionDisk /dev/$PI_CARD MBRFormat FAT32 RPI0 1g FREE RPISYS 0b
|
||||||
|
sudo tar xf static/alpine-aarch64.tar.gz -C /Volumes/RPI0
|
||||||
|
sudo cp static/headless.tar.gz /Volumes/RPI0/headless.apkovl.tar.gz
|
||||||
Binary file not shown.
@@ -0,0 +1,16 @@
|
|||||||
|
KEYMAPOPTS="us us-intl"
|
||||||
|
HOSTNAMEOPTS="-n alpine-test"
|
||||||
|
|
||||||
|
INTERFACESOPTS="auto lo
|
||||||
|
iface lo inet loopback
|
||||||
|
|
||||||
|
auto eth0
|
||||||
|
iface eth0 inet dhcp
|
||||||
|
hostname alpine-test
|
||||||
|
"
|
||||||
|
|
||||||
|
DNSOPTS="-d local"
|
||||||
|
TIMEZONEOPTS="-z UTC"
|
||||||
|
APKREPOSOPTS="-f"
|
||||||
|
SSHDOPTS="-c openssh"
|
||||||
|
NTPOPTS="-c openntpd"
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
AVAILABLE_HOSTS=$(grep "\." pi-hosts.txt | awk '{print $2}' | cut -d "=" -f2)
|
||||||
|
for HOST in $AVAILABLE_HOSTS; do
|
||||||
|
echo "Working on host $HOST"
|
||||||
|
ssh -t root@$HOST 'setup-ntp -c chrony; setup-apkrepos -f; apk update; apk add python3'
|
||||||
|
done
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
all:
|
||||||
|
./001-prepare-ansible.sh
|
||||||
|
ansible-playbook rpi.yaml -f 10
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[defaults]
|
||||||
|
host_key_checking = False
|
||||||
|
remote_user = root
|
||||||
|
inventory = ./pi-hosts.txt
|
||||||
|
interpreter_python = auto_silent
|
||||||
|
|
||||||
|
[ssh_connection]
|
||||||
|
pipelining = True
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[masters]
|
||||||
|
pi0 ansible_host=192.168.50.132 # Pi0
|
||||||
|
|
||||||
|
[workers]
|
||||||
|
pi1 ansible_host=192.168.50.135 # Pi1
|
||||||
|
pi3 ansible_host=192.168.50.60 # Pi3
|
||||||
|
pi4 ansible_host=192.168.50.36 # Pi4
|
||||||
|
pi2 ansible_host=192.168.50.85 # Pi2
|
||||||
|
pi5 ansible_host=192.168.50.230 # Pi5
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
- name: Taint nodes
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
kubectl taint nodes --all node-role.kubernetes.io/master:NoSchedule- || true
|
||||||
|
|
||||||
|
- name: Install py3-pip
|
||||||
|
ansible.builtin.package:
|
||||||
|
name:
|
||||||
|
- py3-pip
|
||||||
|
- helm
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Install yaml package
|
||||||
|
pip:
|
||||||
|
name: PyYAML
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
# - name: Github authentication
|
||||||
|
# ansible.builtin.shell:
|
||||||
|
# cmd: |
|
||||||
|
# kubectl create secret docker-registry docker-ghcr --docker-server=https://ghcr.io --docker-username=lukaszraczylo --docker-password=$GITHUB_TOKEN [email protected]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
# - include: k8s-general-setup.yml
|
||||||
|
- include: k8s-github.yml
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
- name: Install packages - filesystem
|
||||||
|
ansible.builtin.package:
|
||||||
|
name:
|
||||||
|
- e2fsprogs
|
||||||
|
- parted
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create partition for perm data
|
||||||
|
community.general.parted:
|
||||||
|
device: /dev/mmcblk0
|
||||||
|
number: 2
|
||||||
|
fs_type: ext4
|
||||||
|
part_start: 1GiB
|
||||||
|
part_end: 100%
|
||||||
|
flags: [ boot ]
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Fomat perm data partition
|
||||||
|
community.general.filesystem:
|
||||||
|
dev: /dev/mmcblk0p2
|
||||||
|
fstype: ext4
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: Re-mount old data partition
|
||||||
|
ansible.posix.mount:
|
||||||
|
path: /media/mmcblk0p1
|
||||||
|
src: /dev/mmcblk0p1
|
||||||
|
opts: rw
|
||||||
|
state: remounted
|
||||||
|
|
||||||
|
- name: Mount perm data partition
|
||||||
|
ansible.posix.mount:
|
||||||
|
path: /mnt
|
||||||
|
src: /dev/mmcblk0p2
|
||||||
|
opts: rw
|
||||||
|
fstype: ext4
|
||||||
|
state: mounted
|
||||||
|
|
||||||
|
- name: Overwrite setup-disk
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: static/setup-disk
|
||||||
|
dest: /sbin/setup-disk
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0744'
|
||||||
|
backup: yes
|
||||||
|
|
||||||
|
- name: Setup new disk as system
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: setup-disk -m sys /mnt
|
||||||
|
creates: /mnt/bin/date
|
||||||
|
|
||||||
|
- name: Housekeeping
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
rm -f /media/mmcblk0p1/boot/*
|
||||||
|
cd /mnt
|
||||||
|
rm boot/boot
|
||||||
|
mv boot/* /media/mmcblk0p1/boot/
|
||||||
|
rm -Rf boot
|
||||||
|
mkdir media/mmcblk0p1
|
||||||
|
ln -s media/mmcblk0p1/boot boot
|
||||||
|
removes: /mnt/boot/boot
|
||||||
|
|
||||||
|
- name: Add mountpoints to fstab
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
echo "/dev/mmcblk0p1 /media/mmcblk0p1 vfat defaults 0 0" >> etc/fstab
|
||||||
|
echo "/dev/mmcblk0p1 /media/mmcblk0p1 vfat defaults 0 0" >> /etc/fstab
|
||||||
|
sed -i '/cdrom/d' etc/fstab
|
||||||
|
sed -i '/floppy/d' etc/fstab
|
||||||
|
sed -i '/usbdisk/d' etc/fstab
|
||||||
|
touch /mnt/.nvm_mountpoints
|
||||||
|
chdir: /mnt
|
||||||
|
creates: /mnt/.nvm_mountpoints
|
||||||
|
|
||||||
|
- name: Enable repositories and cgroups
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
sed -i '/edge/s/^#//' /mnt/etc/apk/repositories
|
||||||
|
sed -i 's/^/root=\/dev\/mmcblk0p2 /' /media/mmcblk0p1/cmdline.txt
|
||||||
|
echo "cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1" >> /media/mmcblk0p1/cmdline.txt
|
||||||
|
sed -i ':a;N;$!ba;s/\n/ /g' /media/mmcblk0p1/cmdline.txt
|
||||||
|
touch /mnt/.nvm_cgroups
|
||||||
|
creates: /mnt/.nvm_cgroups
|
||||||
|
|
||||||
|
- name: Commit changes
|
||||||
|
ansible.builtin.shell:
|
||||||
|
chdir: /media/mmcblk0p1
|
||||||
|
cmd: lbu_commit -d || true
|
||||||
|
|
||||||
|
- name: Reboot
|
||||||
|
reboot:
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
- name: Pull basic images
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: kubeadm config images pull
|
||||||
|
|
||||||
|
- name: Install helm
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
|
||||||
|
creates: /etc/kubernetes/kubelet.conf
|
||||||
|
register: k8sinit_output
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
- name: Check for base system config lockfile
|
||||||
|
stat: path="/mnt/.nvm_mountpoints"
|
||||||
|
register: basic_config_lock
|
||||||
|
- include: filesystem.yml
|
||||||
|
when: basic_config_lock.stat.exists == False
|
||||||
|
- include: networking.yml
|
||||||
|
- include: kubernetes.yml
|
||||||
|
- include: nfs.yml
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
- name: Set hostname
|
||||||
|
ansible.builtin.hostname:
|
||||||
|
name: '{{ inventory_hostname }}'
|
||||||
|
|
||||||
|
- name: "Build hosts file"
|
||||||
|
lineinfile:
|
||||||
|
dest: /etc/hosts
|
||||||
|
regexp: '.*{{ item }}$'
|
||||||
|
line: "{{hostvars[item]['ansible_default_ipv4']['address']}} {{item}} {{item}}.local"
|
||||||
|
state: present
|
||||||
|
register: etchostsupdate
|
||||||
|
when: hostvars[item]['ansible_facts']['default_ipv4'] is defined
|
||||||
|
with_items:
|
||||||
|
- "{{ groups['all'] }}"
|
||||||
|
|
||||||
|
- name: Install packages for networking
|
||||||
|
ansible.builtin.package:
|
||||||
|
name:
|
||||||
|
- dbus
|
||||||
|
- avahi
|
||||||
|
- kubernetes
|
||||||
|
- kubectl
|
||||||
|
- docker
|
||||||
|
- cni-plugins
|
||||||
|
- kubelet
|
||||||
|
- kubeadm
|
||||||
|
- sudo
|
||||||
|
- curl
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Enable service dbus
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: dbus
|
||||||
|
state: started
|
||||||
|
enabled: yes
|
||||||
|
|
||||||
|
- name: Overwrite avahi config
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: static/avahi-config
|
||||||
|
dest: /etc/avahi/avahi-daemon.conf
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0644'
|
||||||
|
backup: yes
|
||||||
|
|
||||||
|
- name: Enable service avahi
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: avahi-daemon
|
||||||
|
state: restarted
|
||||||
|
enabled: yes
|
||||||
|
|
||||||
|
- name: Enable service docker
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: docker
|
||||||
|
state: started
|
||||||
|
enabled: yes
|
||||||
|
|
||||||
|
- name: Enable service kubelet
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: kubelet
|
||||||
|
state: started
|
||||||
|
enabled: yes
|
||||||
|
|
||||||
|
- name: Enable promisc mode for ARP requests
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
ip link set eth0 promisc on
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
- name: Install nfsprogs
|
||||||
|
ansible.builtin.package:
|
||||||
|
name:
|
||||||
|
- nfs-utils
|
||||||
|
|
||||||
|
- name: Prepare nfs mountpoint
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
mkdir -p /media/nfs
|
||||||
|
|
||||||
|
- name: Mount NFS resources
|
||||||
|
ansible.posix.mount:
|
||||||
|
src: "{{settings.nfs_host}}:{{settings.nfs_multi_share_path}}"
|
||||||
|
path: /media/nfs
|
||||||
|
fstype: nfs
|
||||||
|
opts: nofail,noatime,nolock,tcp,actimeo=1800
|
||||||
|
state: mounted
|
||||||
|
|
||||||
|
- name: Mount NFS torrent
|
||||||
|
ansible.posix.mount:
|
||||||
|
src: "{{settings.nfs_host}}:{{settings.nfs_torrent_path}}"
|
||||||
|
path: /mnt/drobo-storage/docker-volumes/torrent
|
||||||
|
fstype: nfs
|
||||||
|
opts: nofail,noatime,nolock,tcp,actimeo=1800
|
||||||
|
state: mounted
|
||||||
|
|
||||||
|
- name: Mount NFS adguard
|
||||||
|
ansible.posix.mount:
|
||||||
|
src: "{{settings.nfs_host}}:{{settings.nfs_adguard_path}}"
|
||||||
|
path: /mnt/drobo-storage/docker-volumes/adguard
|
||||||
|
fstype: nfs
|
||||||
|
opts: nofail,noatime,nolock,tcp,actimeo=1800
|
||||||
|
state: mounted
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
- include: master-setup.yml
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
- name: Initialize cluster
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
kubeadm reset -f
|
||||||
|
kubeadm init --pod-network-cidr=10.244.0.0/16
|
||||||
|
creates: /etc/kubernetes/kubelet.conf
|
||||||
|
register: k8sinit_output
|
||||||
|
|
||||||
|
- name: Storing Logs and Generated token for nodes config
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: '{{ k8sinit_output.stdout }}'
|
||||||
|
dest: cluster_join_file
|
||||||
|
when: '"token" in k8sinit_output.stdout'
|
||||||
|
|
||||||
|
- name: Pulling the token file for setup purposes
|
||||||
|
ansible.builtin.fetch:
|
||||||
|
flat: yes
|
||||||
|
src: cluster_join_file
|
||||||
|
dest: static/token_file
|
||||||
|
when: '"token" in k8sinit_output.stdout'
|
||||||
|
|
||||||
|
- name: Copying kubeconfig file
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
mkdir -p $HOME/.kube
|
||||||
|
cp -f /etc/kubernetes/admin.conf $HOME/.kube/config
|
||||||
|
chown $(id -u):$(id -g) $HOME/.kube/config
|
||||||
|
|
||||||
|
- name: Save kubeconfig locally
|
||||||
|
ansible.builtin.fetch:
|
||||||
|
flat: yes
|
||||||
|
src: /etc/kubernetes/admin.conf
|
||||||
|
dest: static/kubectl.conf
|
||||||
|
fail_on_missing: yes
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
- include: worker-setup.yml
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
- name: Copy token file to workers
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: static/token_file
|
||||||
|
dest: /root/cluster_token_file
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Joining worker nodes with kubernetes master
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
kubeadm reset -f
|
||||||
|
cat /root/cluster_token_file | tail -2 > out.sh
|
||||||
|
sh out.sh
|
||||||
|
creates: out.sh
|
||||||
|
|
||||||
|
- name: Copy kubectl config to workers
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: static/kubectl.conf
|
||||||
|
dest: /root/kubectl-config
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Copying kubeconfig file
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
mkdir -p $HOME/.kube
|
||||||
|
cp -f /root/kubectl-config $HOME/.kube/config
|
||||||
|
chown $(id -u):$(id -g) $HOME/.kube/config
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
- hosts: all
|
||||||
|
vars:
|
||||||
|
settings:
|
||||||
|
nfs_host: 192.168.50.5
|
||||||
|
nfs_torrent_path: "/mnt/drobo-storage/docker-volumes/torrent"
|
||||||
|
nfs_adguard_path: "/mnt/drobo-storage/docker-volumes/adguard"
|
||||||
|
nfs_multi_share_path: "/mnt/drobo-storage/docker-volumes/nfs-share"
|
||||||
|
roles:
|
||||||
|
- common
|
||||||
|
|
||||||
|
- hosts: masters
|
||||||
|
roles:
|
||||||
|
- kubemaster
|
||||||
|
|
||||||
|
- hosts: workers
|
||||||
|
roles:
|
||||||
|
- kubeworkers
|
||||||
|
|
||||||
|
- hosts: masters
|
||||||
|
roles:
|
||||||
|
- basics
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
[server]
|
||||||
|
#host-name=foo
|
||||||
|
domain-name=local
|
||||||
|
#browse-domains=0pointer.de, zeroconf.org
|
||||||
|
use-ipv4=yes
|
||||||
|
use-ipv6=yes
|
||||||
|
#allow-interfaces=eth0
|
||||||
|
#deny-interfaces=eth1
|
||||||
|
#check-response-ttl=no
|
||||||
|
#use-iff-running=no
|
||||||
|
#enable-dbus=yes
|
||||||
|
#disallow-other-stacks=no
|
||||||
|
#allow-point-to-point=no
|
||||||
|
#cache-entries-max=4096
|
||||||
|
#clients-max=4096
|
||||||
|
#objects-per-client-max=1024
|
||||||
|
#entries-per-entry-group-max=32
|
||||||
|
ratelimit-interval-usec=1000000
|
||||||
|
ratelimit-burst=1000
|
||||||
|
|
||||||
|
[wide-area]
|
||||||
|
enable-wide-area=yes
|
||||||
|
|
||||||
|
[publish]
|
||||||
|
#disable-publishing=no
|
||||||
|
#disable-user-service-publishing=no
|
||||||
|
#add-service-cookie=no
|
||||||
|
#publish-addresses=yes
|
||||||
|
publish-hinfo=no
|
||||||
|
publish-workstation=no
|
||||||
|
#publish-domain=yes
|
||||||
|
#publish-dns-servers=192.168.50.1, 192.168.50.2
|
||||||
|
#publish-resolv-conf-dns-servers=yes
|
||||||
|
#publish-aaaa-on-ipv4=yes
|
||||||
|
#publish-a-on-ipv6=no
|
||||||
|
|
||||||
|
[reflector]
|
||||||
|
#enable-reflector=no
|
||||||
|
#reflect-ipv=no
|
||||||
|
#reflect-filters=_airplay._tcp.local,_raop._tcp.local
|
||||||
|
|
||||||
|
[rlimits]
|
||||||
|
#rlimit-as=
|
||||||
|
#rlimit-core=0
|
||||||
|
#rlimit-data=8388608
|
||||||
|
#rlimit-fsize=0
|
||||||
|
#rlimit-nofile=768
|
||||||
|
#rlimit-stack=8388608
|
||||||
|
#rlimit-nproc=3
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
**.terraform
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
ADDITIONAL_ARGS=-var 'traefik_api_key=$(TRAEFIK_API_KEY)' -var "github_user=$(GH_USER)" -var "github_pat=$(GH_TOKEN)"
|
||||||
|
|
||||||
|
apply:
|
||||||
|
cd infrastructure; terraform apply $(ADDITIONAL_ARGS) -auto-approve -var-file ../variables.tfvars
|
||||||
|
|
||||||
|
plan:
|
||||||
|
cd infrastructure; terraform plan $(ADDITIONAL_ARGS) -var-file ../variables.tfvars
|
||||||
|
|
||||||
|
destroy:
|
||||||
|
cd infrastructure; terraform destroy $(ADDITIONAL_ARGS) -var-file ../variables.tfvars
|
||||||
|
|
||||||
|
refresh:
|
||||||
|
cd infrastructure; terraform refresh $(ADDITIONAL_ARGS) -var-file ../variables.tfvars
|
||||||
|
|
||||||
|
init:
|
||||||
|
cd infrastructure; rm -fr .terraform; terraform init
|
||||||
|
|
||||||
|
import:
|
||||||
|
cd infrastructure; terraform import $(ADDITIONAL_ARGS) -var-file ../variables.tfvars $(ARGS)
|
||||||
|
|
||||||
|
lint:
|
||||||
|
terraform fmt -recursive infrastructure/
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
resource "kubernetes_storage_class" "adguard_nfs" {
|
||||||
|
metadata {
|
||||||
|
name = "nfs-adguard"
|
||||||
|
}
|
||||||
|
storage_provisioner = "local"
|
||||||
|
reclaim_policy = "Retain"
|
||||||
|
allow_volume_expansion = true
|
||||||
|
volume_binding_mode = "Immediate"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_namespace" "adguard_ns" {
|
||||||
|
metadata {
|
||||||
|
name = "adguard"
|
||||||
|
labels = {
|
||||||
|
app = "adguard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_ingress" "adguard_ingress" {
|
||||||
|
metadata {
|
||||||
|
name = "traefik-adguard"
|
||||||
|
annotations = {
|
||||||
|
"kubernetes.io/ingress.class" : "traefik"
|
||||||
|
}
|
||||||
|
namespace = kubernetes_namespace.adguard_ns.metadata.0.name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
rule {
|
||||||
|
host = "adguard.local"
|
||||||
|
http {
|
||||||
|
path {
|
||||||
|
path = "/"
|
||||||
|
backend {
|
||||||
|
service_name = kubernetes_service.adguard_svc.metadata.0.name
|
||||||
|
service_port = "adguard-http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service" "adguard_svc" {
|
||||||
|
depends_on = [kubernetes_config_map.metallb_cfg_map]
|
||||||
|
metadata {
|
||||||
|
namespace = kubernetes_namespace.adguard_ns.metadata.0.name
|
||||||
|
name = "adguard-fwd"
|
||||||
|
annotations = {
|
||||||
|
"metallb.universe.tf/address-pool" : "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spec {
|
||||||
|
port {
|
||||||
|
name = "adguard-dns-tcp"
|
||||||
|
port = 53
|
||||||
|
target_port = 53
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
name = "adguard-dns-tls"
|
||||||
|
port = 853
|
||||||
|
target_port = 853
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
name = "adguard-http"
|
||||||
|
port = 8080
|
||||||
|
target_port = 8080
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
name = "adguard-init"
|
||||||
|
port = 3000
|
||||||
|
target_port = 3000
|
||||||
|
}
|
||||||
|
load_balancer_ip = "${var.network_subnet}.${var.adguard_catchall_host}"
|
||||||
|
selector = {
|
||||||
|
"app" : "adguard"
|
||||||
|
}
|
||||||
|
session_affinity = "ClientIP"
|
||||||
|
type = "LoadBalancer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service" "adguard_udp_svc" {
|
||||||
|
depends_on = [kubernetes_config_map.metallb_cfg_map]
|
||||||
|
metadata {
|
||||||
|
namespace = kubernetes_namespace.adguard_ns.metadata.0.name
|
||||||
|
name = "adguard-fwd-udp"
|
||||||
|
annotations = {
|
||||||
|
"metallb.universe.tf/address-pool" : "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spec {
|
||||||
|
port {
|
||||||
|
name = "adguard-dns-udp"
|
||||||
|
port = 53
|
||||||
|
target_port = 53
|
||||||
|
protocol = "UDP"
|
||||||
|
}
|
||||||
|
load_balancer_ip = "${var.network_subnet}.${var.adguard_host}"
|
||||||
|
selector = {
|
||||||
|
"app" : "adguard"
|
||||||
|
}
|
||||||
|
type = "LoadBalancer"
|
||||||
|
session_affinity = "ClientIP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_persistent_volume" "adguard_shared_nfs" {
|
||||||
|
metadata {
|
||||||
|
name = "adguard-pv"
|
||||||
|
labels = {
|
||||||
|
name = "type"
|
||||||
|
value = "local"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spec {
|
||||||
|
capacity = {
|
||||||
|
storage = "2Gi"
|
||||||
|
}
|
||||||
|
storage_class_name = kubernetes_storage_class.adguard_nfs.metadata.0.name
|
||||||
|
access_modes = ["ReadWriteMany"]
|
||||||
|
persistent_volume_source {
|
||||||
|
host_path {
|
||||||
|
path = var.nfs_adguard_path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_persistent_volume_claim" "adguard_shared_nfs_claim" {
|
||||||
|
metadata {
|
||||||
|
name = "adguard-pvc"
|
||||||
|
namespace = kubernetes_namespace.adguard_ns.metadata.0.name
|
||||||
|
}
|
||||||
|
spec {
|
||||||
|
storage_class_name = kubernetes_storage_class.adguard_nfs.metadata.0.name
|
||||||
|
access_modes = ["ReadWriteMany"]
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
storage = "2Gi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
volume_name = kubernetes_persistent_volume.adguard_shared_nfs.metadata.0.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
## adguard deployment
|
||||||
|
|
||||||
|
resource "kubernetes_deployment" "adguard" {
|
||||||
|
depends_on = [kubernetes_persistent_volume_claim.adguard_shared_nfs_claim]
|
||||||
|
metadata {
|
||||||
|
name = "adguard"
|
||||||
|
labels = {
|
||||||
|
app = "adguard"
|
||||||
|
}
|
||||||
|
namespace = kubernetes_namespace.adguard_ns.metadata.0.name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = 1
|
||||||
|
selector {
|
||||||
|
match_labels = {
|
||||||
|
app = "adguard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = {
|
||||||
|
app = "adguard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
volume {
|
||||||
|
name = "adguard-v-nfs"
|
||||||
|
persistent_volume_claim {
|
||||||
|
claim_name = kubernetes_persistent_volume_claim.adguard_shared_nfs_claim.metadata.0.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container {
|
||||||
|
name = "adguard"
|
||||||
|
image = "adguard/adguardhome:arm64-edge"
|
||||||
|
port {
|
||||||
|
container_port = 53
|
||||||
|
protocol = "TCP"
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
container_port = 53
|
||||||
|
protocol = "UDP"
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
container_port = 853
|
||||||
|
protocol = "TCP"
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
container_port = 3000
|
||||||
|
protocol = "TCP"
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
container_port = 8080
|
||||||
|
protocol = "TCP"
|
||||||
|
}
|
||||||
|
volume_mount {
|
||||||
|
mount_path = "/opt/adguardhome/work"
|
||||||
|
sub_path = "work-k8s"
|
||||||
|
name = "adguard-v-nfs"
|
||||||
|
}
|
||||||
|
volume_mount {
|
||||||
|
mount_path = "/opt/adguardhome/conf"
|
||||||
|
sub_path = "conf-k8s"
|
||||||
|
name = "adguard-v-nfs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "null_resource" "adguard_crd" {
|
||||||
|
|
||||||
|
provisioner "local-exec" {
|
||||||
|
when = create
|
||||||
|
command = "kubectl apply -f ./static/crd-adguard-traefik-udp-53.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "local-exec" {
|
||||||
|
when = destroy
|
||||||
|
command = "kubectl delete -f ./static/crd-adguard-traefik-udp-53.yaml"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
## Install metrics server
|
||||||
|
module "dashboard_metrics" {
|
||||||
|
depends_on = [kubernetes_service.traefik_svc]
|
||||||
|
source = "./modules/dashboard-metrics"
|
||||||
|
}
|
||||||
|
|
||||||
|
## Install dashboard
|
||||||
|
resource "helm_release" "k8s_dashboard" {
|
||||||
|
name = "k8s-dashboard"
|
||||||
|
namespace = "default"
|
||||||
|
repository = "https://kubernetes.github.io/dashboard/"
|
||||||
|
chart = "kubernetes-dashboard"
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "protocolHttp"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
name = "ingress.enabled"
|
||||||
|
value = "false"
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
name = "rbac.create"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
name = "serviceAccount.create"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
name = "service.externalPort"
|
||||||
|
value = "1337"
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
name = "networkPolicy.enabled"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
name = "podLabels.app"
|
||||||
|
value = "k8s-dashboard"
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
name = "metrics-server.enabled"
|
||||||
|
value = "false"
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
name = "metricsScraper.enabled"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
name = "metricsScraper.image.repository"
|
||||||
|
value = "kubernetesui/metrics-scraper-arm"
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
name = "metricsScraper.image.tag"
|
||||||
|
value = "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_cluster_role_binding" "k8s_dashboard_role" {
|
||||||
|
metadata {
|
||||||
|
name = "kubernetes-dashboard"
|
||||||
|
}
|
||||||
|
role_ref {
|
||||||
|
api_group = "rbac.authorization.k8s.io"
|
||||||
|
kind = "ClusterRole"
|
||||||
|
name = "cluster-admin"
|
||||||
|
}
|
||||||
|
subject {
|
||||||
|
kind = "ServiceAccount"
|
||||||
|
name = "k8s-dashboard-kubernetes-dashboard"
|
||||||
|
namespace = "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Commented out as traffic runs via traefik anyway.
|
||||||
|
|
||||||
|
# resource "kubernetes_service" "k8s_dashboard_svc" {
|
||||||
|
# depends_on = [ kubernetes_config_map.metallb_cfg_map ]
|
||||||
|
# metadata {
|
||||||
|
# namespace = "default"
|
||||||
|
# name = "dashboard"
|
||||||
|
# annotations = {
|
||||||
|
# "metallb.universe.tf/address-pool": "default"
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# spec {
|
||||||
|
# port {
|
||||||
|
# port = 80
|
||||||
|
# target_port = 9090
|
||||||
|
# }
|
||||||
|
# load_balancer_ip = "192.168.50.200"
|
||||||
|
# selector = {
|
||||||
|
# "app.kubernetes.io/instance": "k8s-dashboard"
|
||||||
|
# }
|
||||||
|
# type = "LoadBalancer"
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
resource "kubernetes_ingress" "traefik_dashboard_routing" {
|
||||||
|
metadata {
|
||||||
|
name = "traefik-k8s-dashboard"
|
||||||
|
annotations = {
|
||||||
|
"kubernetes.io/ingress.class" : "traefik"
|
||||||
|
}
|
||||||
|
namespace = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
rule {
|
||||||
|
host = "k8s.local"
|
||||||
|
http {
|
||||||
|
path {
|
||||||
|
path = "/"
|
||||||
|
backend {
|
||||||
|
service_name = "k8s-dashboard-kubernetes-dashboard"
|
||||||
|
service_port = 1337
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
provider "kubernetes" {
|
||||||
|
config_path = "~/.kube/config"
|
||||||
|
config_context = "home-k8s"
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "helm" {
|
||||||
|
kubernetes {
|
||||||
|
config_path = "~/.kube/config"
|
||||||
|
config_context = "home-k8s"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
|
||||||
|
|
||||||
|
resource "null_resource" "metrics_server" {
|
||||||
|
|
||||||
|
provisioner "local-exec" {
|
||||||
|
when = create
|
||||||
|
command = "kubectl apply -f ./static/metrics-server.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "local-exec" {
|
||||||
|
when = destroy
|
||||||
|
command = "kubectl delete -f ./static/metrics-server.yaml"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
|
||||||
|
|
||||||
|
resource "null_resource" "flannel_networking" {
|
||||||
|
|
||||||
|
provisioner "local-exec" {
|
||||||
|
when = create
|
||||||
|
command = "kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "local-exec" {
|
||||||
|
when = destroy
|
||||||
|
command = "kubectl delete -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
resource "null_resource" "metallb_networking" {
|
||||||
|
|
||||||
|
provisioner "local-exec" {
|
||||||
|
when = create
|
||||||
|
command = "kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/main/manifests/metallb.yaml; kubectl get configmap kube-proxy -n kube-system -o yaml | sed -e \"s/strictARP: false/strictARP: true/\" | kubectl apply -f - -n kube-system"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "local-exec" {
|
||||||
|
when = destroy
|
||||||
|
command = "kubectl delete -f https://raw.githubusercontent.com/metallb/metallb/main/manifests/metallb.yaml"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Basic networking
|
||||||
|
|
||||||
|
module "networking_flannel" {
|
||||||
|
source = "./modules/networking-flannel"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_namespace" "metallb_system" {
|
||||||
|
metadata {
|
||||||
|
name = "metallb-system"
|
||||||
|
labels = {
|
||||||
|
app = "metallb"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "networking_metallb" {
|
||||||
|
depends_on = [kubernetes_namespace.metallb_system]
|
||||||
|
source = "./modules/networking-metallb"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_string" "metallb_secret_string" {
|
||||||
|
length = 128
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_secret" "metallb_secret" {
|
||||||
|
type = "generic"
|
||||||
|
metadata {
|
||||||
|
name = "memberlist"
|
||||||
|
namespace = "metallb-system"
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
secretkey = base64encode(random_string.metallb_secret_string.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_config_map" "metallb_cfg_map" {
|
||||||
|
metadata {
|
||||||
|
name = "config"
|
||||||
|
namespace = "metallb-system"
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
config = <<CFGMAP
|
||||||
|
address-pools:
|
||||||
|
- name: default
|
||||||
|
protocol: layer2
|
||||||
|
addresses:
|
||||||
|
- ${var.network_subnet}.200-${var.network_subnet}.250
|
||||||
|
CFGMAP
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# resource "null_resource" "prometheus_pv" {
|
||||||
|
|
||||||
|
# provisioner "local-exec" {
|
||||||
|
# when = create
|
||||||
|
# command = "kubectl apply -f static/prometheus-pv.yaml"
|
||||||
|
# }
|
||||||
|
|
||||||
|
# provisioner "local-exec" {
|
||||||
|
# when = destroy
|
||||||
|
# command = "kubectl delete -f static/prometheus-pv.yaml"
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
resource "helm_release" "prometheus" {
|
||||||
|
# depends_on = [ null_resource.prometheus_pv ]
|
||||||
|
name = "prometheus"
|
||||||
|
namespace = "default"
|
||||||
|
chart = "kube-prometheus-stack"
|
||||||
|
repository = "https://prometheus-community.github.io/helm-charts"
|
||||||
|
wait = false
|
||||||
|
# set {
|
||||||
|
# name = "kube-state-metrics.image.repository"
|
||||||
|
# value = "carlosedp/kube-state-metrics"
|
||||||
|
# }
|
||||||
|
|
||||||
|
# set {
|
||||||
|
# name = "kube-state-metrics.image.tag"
|
||||||
|
# value = "v1.9.6"
|
||||||
|
# }
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "null_resource" "prometheus_patch" {
|
||||||
|
depends_on = [helm_release.prometheus]
|
||||||
|
provisioner "local-exec" {
|
||||||
|
when = create
|
||||||
|
command = <<EOF
|
||||||
|
kubectl patch ds prometheus-prometheus-node-exporter --type json -p '[{"op": "remove", "path" : "/spec/template/spec/containers/0/volumeMounts/2/mountPropagation"}]' || true
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_ingress" "traefik_grafana_routing" {
|
||||||
|
metadata {
|
||||||
|
name = "traefik-k8s-grafana"
|
||||||
|
annotations = {
|
||||||
|
"kubernetes.io/ingress.class" : "traefik"
|
||||||
|
}
|
||||||
|
namespace = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
rule {
|
||||||
|
host = "grafana.local"
|
||||||
|
http {
|
||||||
|
path {
|
||||||
|
path = "/"
|
||||||
|
backend {
|
||||||
|
service_name = "prometheus-grafana"
|
||||||
|
service_port = 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRouteUDP
|
||||||
|
metadata:
|
||||||
|
name: adguard-traefik-udp
|
||||||
|
namespace: adguard
|
||||||
|
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- dnsentry
|
||||||
|
routes:
|
||||||
|
- match: HostSNI(`*`)
|
||||||
|
services:
|
||||||
|
- name: adguard-fwd-udp
|
||||||
|
port: 53
|
||||||
|
protocol: "UDP"
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
k8s-app: metrics-server
|
||||||
|
name: metrics-server
|
||||||
|
namespace: kube-system
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
k8s-app: metrics-server
|
||||||
|
rbac.authorization.k8s.io/aggregate-to-admin: "true"
|
||||||
|
rbac.authorization.k8s.io/aggregate-to-edit: "true"
|
||||||
|
rbac.authorization.k8s.io/aggregate-to-view: "true"
|
||||||
|
name: system:aggregated-metrics-reader
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- metrics.k8s.io
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
- nodes
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
k8s-app: metrics-server
|
||||||
|
name: system:metrics-server
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
- nodes
|
||||||
|
- nodes/stats
|
||||||
|
- namespaces
|
||||||
|
- configmaps
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
k8s-app: metrics-server
|
||||||
|
name: metrics-server-auth-reader
|
||||||
|
namespace: kube-system
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: extension-apiserver-authentication-reader
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: metrics-server
|
||||||
|
namespace: kube-system
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
k8s-app: metrics-server
|
||||||
|
name: metrics-server:system:auth-delegator
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: system:auth-delegator
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: metrics-server
|
||||||
|
namespace: kube-system
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
k8s-app: metrics-server
|
||||||
|
name: system:metrics-server
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: system:metrics-server
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: metrics-server
|
||||||
|
namespace: kube-system
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
k8s-app: metrics-server
|
||||||
|
name: metrics-server
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: https
|
||||||
|
port: 443
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: https
|
||||||
|
selector:
|
||||||
|
k8s-app: metrics-server
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
k8s-app: metrics-server
|
||||||
|
name: metrics-server
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
k8s-app: metrics-server
|
||||||
|
strategy:
|
||||||
|
rollingUpdate:
|
||||||
|
maxUnavailable: 0
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
k8s-app: metrics-server
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- args:
|
||||||
|
- --cert-dir=/tmp
|
||||||
|
- --secure-port=4443
|
||||||
|
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
|
||||||
|
- --kubelet-use-node-status-port
|
||||||
|
- --kubelet-insecure-tls
|
||||||
|
- --authorization-always-allow-paths=/livez,/readyz
|
||||||
|
image: k8s.gcr.io/metrics-server/metrics-server:v0.4.2
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
livenessProbe:
|
||||||
|
failureThreshold: 3
|
||||||
|
httpGet:
|
||||||
|
path: /livez
|
||||||
|
port: https
|
||||||
|
scheme: HTTPS
|
||||||
|
periodSeconds: 10
|
||||||
|
name: metrics-server
|
||||||
|
ports:
|
||||||
|
- containerPort: 4443
|
||||||
|
name: https
|
||||||
|
protocol: TCP
|
||||||
|
readinessProbe:
|
||||||
|
failureThreshold: 3
|
||||||
|
httpGet:
|
||||||
|
path: /readyz
|
||||||
|
port: https
|
||||||
|
scheme: HTTPS
|
||||||
|
periodSeconds: 10
|
||||||
|
securityContext:
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1000
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /tmp
|
||||||
|
name: tmp-dir
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/os: linux
|
||||||
|
priorityClassName: system-cluster-critical
|
||||||
|
serviceAccountName: metrics-server
|
||||||
|
volumes:
|
||||||
|
- emptyDir: {}
|
||||||
|
name: tmp-dir
|
||||||
|
---
|
||||||
|
apiVersion: apiregistration.k8s.io/v1
|
||||||
|
kind: APIService
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
k8s-app: metrics-server
|
||||||
|
name: v1beta1.metrics.k8s.io
|
||||||
|
spec:
|
||||||
|
group: metrics.k8s.io
|
||||||
|
groupPriorityMinimum: 100
|
||||||
|
insecureSkipTLSVerify: true
|
||||||
|
service:
|
||||||
|
name: metrics-server
|
||||||
|
namespace: kube-system
|
||||||
|
version: v1beta1
|
||||||
|
versionPriority: 100
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
finalizers:
|
||||||
|
- kubernetes.io/pv-protection
|
||||||
|
name: prometheus-server
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
capacity:
|
||||||
|
storage: 8Gi
|
||||||
|
claimRef:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
name: prometheus-server
|
||||||
|
namespace: monitoring
|
||||||
|
local:
|
||||||
|
path: /data/localstorage/prometheus-server
|
||||||
|
nodeAffinity:
|
||||||
|
required:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: kubernetes.io/hostname
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- pi-worker
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: local-storage
|
||||||
|
volumeMode: Filesystem
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
finalizers:
|
||||||
|
- kubernetes.io/pv-protection
|
||||||
|
name: prometheus-alertmanager
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
capacity:
|
||||||
|
storage: 2Gi
|
||||||
|
claimRef:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
name: prometheus-alertmanager
|
||||||
|
namespace: monitoring
|
||||||
|
local:
|
||||||
|
path: /data/localstorage/prometheus-alertmanager
|
||||||
|
nodeAffinity:
|
||||||
|
required:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: kubernetes.io/hostname
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- pi-worker
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: local-storage
|
||||||
|
volumeMode: Filesystem
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
providers:
|
||||||
|
file:
|
||||||
|
filename: /custom-data/traefik-config.yaml
|
||||||
|
kubernetesIngress: {}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Activate Pilot integration
|
||||||
|
pilot:
|
||||||
|
enabled: true
|
||||||
|
token: "${traefik_api_key_value}"
|
||||||
|
|
||||||
|
additionalArguments:
|
||||||
|
# - --entryPoints.tcpentry.address=:3333/tcp
|
||||||
|
# - --entryPoints.udpentry.address=:3333/udp
|
||||||
|
- --entryPoints.dnsentry.address=:53/udp
|
||||||
|
- --providers.file.filename=/custom-data/traefik-config.yaml
|
||||||
|
- --providers.file.watch=true
|
||||||
|
- --providers.kubernetesingress=true
|
||||||
|
- --metrics.prometheus=true
|
||||||
|
- --metrics.prometheus.entryPoint=metrics
|
||||||
|
- --metrics.prometheus.addServicesLabels=true
|
||||||
|
- --metrics.prometheus.addEntryPointsLabels=true
|
||||||
|
- --entryPoints.metrics.address=:8082
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- mountPath: /custom-data
|
||||||
|
name: traefik-config
|
||||||
|
type: configMap
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
resource "kubernetes_storage_class" "torrent_nfs" {
|
||||||
|
metadata {
|
||||||
|
name = "nfs-torrent"
|
||||||
|
}
|
||||||
|
storage_provisioner = "local"
|
||||||
|
reclaim_policy = "Retain"
|
||||||
|
allow_volume_expansion = true
|
||||||
|
volume_binding_mode = "Immediate"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_namespace" "torrent_ns" {
|
||||||
|
metadata {
|
||||||
|
name = "torrent"
|
||||||
|
labels = {
|
||||||
|
app = "torrent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_ingress" "torrent_ingress" {
|
||||||
|
metadata {
|
||||||
|
name = "traefik-k8s"
|
||||||
|
annotations = {
|
||||||
|
"kubernetes.io/ingress.class" : "traefik"
|
||||||
|
}
|
||||||
|
namespace = kubernetes_namespace.torrent_ns.metadata.0.name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
rule {
|
||||||
|
host = "torrent.local"
|
||||||
|
http {
|
||||||
|
path {
|
||||||
|
path = "/"
|
||||||
|
backend {
|
||||||
|
service_name = kubernetes_service.torrent_svc.metadata.0.name
|
||||||
|
service_port = 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service" "torrent_svc" {
|
||||||
|
depends_on = [kubernetes_config_map.metallb_cfg_map]
|
||||||
|
metadata {
|
||||||
|
namespace = kubernetes_namespace.torrent_ns.metadata.0.name
|
||||||
|
name = "torrent-fwd"
|
||||||
|
annotations = {
|
||||||
|
"metallb.universe.tf/address-pool" : "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spec {
|
||||||
|
port {
|
||||||
|
name = "torrent-rpc"
|
||||||
|
port = 5000
|
||||||
|
target_port = 5000
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
name = "torrent-tcp"
|
||||||
|
port = 23340
|
||||||
|
target_port = 23340
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
name = "torrent-http"
|
||||||
|
port = 3000
|
||||||
|
target_port = 3000
|
||||||
|
}
|
||||||
|
load_balancer_ip = "${var.network_subnet}.${var.rtorrent_rpc_host}"
|
||||||
|
selector = {
|
||||||
|
"app" : "torrent"
|
||||||
|
}
|
||||||
|
type = "LoadBalancer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_persistent_volume" "torrent_shared_nfs" {
|
||||||
|
metadata {
|
||||||
|
name = "torrent-pv"
|
||||||
|
labels = {
|
||||||
|
name = "type"
|
||||||
|
value = "local"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spec {
|
||||||
|
capacity = {
|
||||||
|
storage = "8Ti"
|
||||||
|
}
|
||||||
|
storage_class_name = kubernetes_storage_class.torrent_nfs.metadata.0.name
|
||||||
|
access_modes = ["ReadWriteMany"]
|
||||||
|
persistent_volume_source {
|
||||||
|
host_path {
|
||||||
|
path = var.nfs_torrent_path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_persistent_volume_claim" "torrent_shared_nfs_claim" {
|
||||||
|
metadata {
|
||||||
|
name = "torrent-pvc"
|
||||||
|
namespace = kubernetes_namespace.torrent_ns.metadata.0.name
|
||||||
|
}
|
||||||
|
spec {
|
||||||
|
storage_class_name = kubernetes_storage_class.torrent_nfs.metadata.0.name
|
||||||
|
access_modes = ["ReadWriteMany"]
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
storage = "8Ti"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
volume_name = kubernetes_persistent_volume.torrent_shared_nfs.metadata.0.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
## rTorrent deployment
|
||||||
|
|
||||||
|
resource "kubernetes_deployment" "flood-torrent" {
|
||||||
|
depends_on = [kubernetes_persistent_volume_claim.torrent_shared_nfs_claim]
|
||||||
|
metadata {
|
||||||
|
name = "flood-torrent"
|
||||||
|
labels = {
|
||||||
|
app = "torrent"
|
||||||
|
}
|
||||||
|
namespace = kubernetes_namespace.torrent_ns.metadata.0.name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = 1
|
||||||
|
selector {
|
||||||
|
match_labels = {
|
||||||
|
app = "torrent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = {
|
||||||
|
app = "torrent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
volume {
|
||||||
|
name = "torrent-v-nfs"
|
||||||
|
persistent_volume_claim {
|
||||||
|
claim_name = kubernetes_persistent_volume_claim.torrent_shared_nfs_claim.metadata.0.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container {
|
||||||
|
name = "flood-torrent"
|
||||||
|
image = "ghcr.io/lukaszraczylo/flood:1.2292.7"
|
||||||
|
env {
|
||||||
|
name = "FLOOD_DISABLE_AUTH"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
args = ["--rthost", "${var.network_subnet}.${var.rtorrent_rpc_host}",
|
||||||
|
"--rtport", "5000", "-e", "HOME=/config", "--auth", "none", "-d", "/config/flood",
|
||||||
|
"--allowedpath", "/config",
|
||||||
|
"--allowedpath", "/data",
|
||||||
|
"--rtorrent", "--rtconfig", "/data/rtorrent/.rtorrent.rc"]
|
||||||
|
port {
|
||||||
|
container_port = 3000
|
||||||
|
protocol = "TCP"
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
container_port = 5000
|
||||||
|
protocol = "TCP"
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
container_port = 23340
|
||||||
|
protocol = "TCP"
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
container_port = 23340
|
||||||
|
protocol = "UDP"
|
||||||
|
}
|
||||||
|
volume_mount {
|
||||||
|
mount_path = "/data"
|
||||||
|
name = "torrent-v-nfs"
|
||||||
|
}
|
||||||
|
volume_mount {
|
||||||
|
mount_path = "/mnt/storage/torrent/downloaded"
|
||||||
|
sub_path = "downloaded"
|
||||||
|
name = "torrent-v-nfs"
|
||||||
|
}
|
||||||
|
volume_mount {
|
||||||
|
mount_path = "/config"
|
||||||
|
sub_path = "flood"
|
||||||
|
name = "torrent-v-nfs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
resource "helm_release" "traefik" {
|
||||||
|
# depends_on = [ helm_release.prometheus ]
|
||||||
|
name = "traefik"
|
||||||
|
namespace = "default"
|
||||||
|
chart = "traefik/traefik"
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "deployment.replicas"
|
||||||
|
value = "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "service.spec.loadBalancerIP"
|
||||||
|
value = "${var.network_subnet}.234"
|
||||||
|
}
|
||||||
|
|
||||||
|
values = [
|
||||||
|
templatefile("static/traefik-static-config.tmpl", { traefik_api_key_value = var.traefik_api_key })
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_config_map" "traefic_cfg_map" {
|
||||||
|
metadata {
|
||||||
|
name = "traefik-config"
|
||||||
|
namespace = "default"
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"traefik-config.yaml" = file("static/traefik-cfg.yaml")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service" "traefik_svc" {
|
||||||
|
depends_on = [kubernetes_config_map.metallb_cfg_map, helm_release.traefik]
|
||||||
|
metadata {
|
||||||
|
namespace = "default"
|
||||||
|
name = "traefik-dashboard"
|
||||||
|
annotations = {
|
||||||
|
"metallb.universe.tf/address-pool" : "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spec {
|
||||||
|
port {
|
||||||
|
port = 80
|
||||||
|
target_port = 9000
|
||||||
|
}
|
||||||
|
selector = {
|
||||||
|
"app.kubernetes.io/instance" : "traefik"
|
||||||
|
}
|
||||||
|
type = "LoadBalancer"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
variable "network_subnet" {}
|
||||||
|
variable "rtorrent_rpc_host" {}
|
||||||
|
variable "nfs_torrent_path" {}
|
||||||
|
variable "nfs_adguard_path" {}
|
||||||
|
variable "adguard_host" {}
|
||||||
|
variable "adguard_catchall_host" {}
|
||||||
|
variable "traefik_api_key" {}
|
||||||
|
variable "github_user" {}
|
||||||
|
variable "github_pat" {}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 0.13"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
kubernetes = {
|
||||||
|
source = "hashicorp/kubernetes"
|
||||||
|
version = "~> 2.0.2"
|
||||||
|
}
|
||||||
|
|
||||||
|
http = {
|
||||||
|
source = "hashicorp/http"
|
||||||
|
version = "~> 2.1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
helm = {
|
||||||
|
source = "hashicorp/helm"
|
||||||
|
version = "~> 2.0.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
null = {
|
||||||
|
source = "hashicorp/null"
|
||||||
|
version = "~> 3.1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
random = {
|
||||||
|
source = "hashicorp/random"
|
||||||
|
version = "~> 3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Variables used for barebone kubernetes setup
|
||||||
|
network_subnet = "192.168.50"
|
||||||
|
rtorrent_rpc_host = "245"
|
||||||
|
adguard_host = "240"
|
||||||
|
adguard_catchall_host = "249"
|
||||||
|
|
||||||
|
nfs_torrent_path = "/mnt/drobo-storage/docker-volumes/torrent"
|
||||||
|
nfs_adguard_path = "/mnt/drobo-storage/docker-volumes/adguard"
|
||||||
|
|
||||||
|
# ENV variable: TRAEFIK_API_KEY sets traefik_api_key
|
||||||
|
# ENV variable: GH_USER, GH_PAT for authentication with private containers
|
||||||
Reference in New Issue
Block a user