feat: add terraform deployment for K8s invidious

This commit is contained in:
NaeiKinDus 2024-10-17 00:00:00 +00:00
parent ff7c9d8b91
commit 904b067816
Signed by: WoodSmellParticle
GPG key ID: 8E52ADFF7CA8AE56
10 changed files with 572 additions and 0 deletions

6
.gitignore vendored
View file

@ -13,3 +13,9 @@ inventory/inventory.yml
!.gitkeep
galaxy_cache
galaxy_token
# terraform secret files
*.tfstate
*.tfstate.backup
*.tfvars
.terraform
.terraform.lock.hcl

View file

@ -0,0 +1,16 @@
version: '3'
vars:
TF_BINARY:
sh: bash -c 'which tofu || which terraform || (echo "Could not find terraform compatible binary" && exit 1)'
tasks:
deploy:
desc: deploy project using OpenTofu or Terraform
cmds:
- '{{.TF_BINARY}} init'
- '{{.TF_BINARY}} apply -auto-approve'
default:
cmd:
task: deploy

View file

@ -0,0 +1,16 @@
# SPDX-License-Identifier: GPL-2.0-only
resource "kubernetes_secret_v1" "app_secrets" {
metadata {
name = "${var.app_name}-secrets"
annotations = var.secret_annotations
labels = merge({
"app.kubernetes.io/component" = "server"
"app.kubernetes.io/name" = var.app_name
"app.kubernetes.io/version" = var.app_version
"app.kubernetes.io/part-of" = var.app_name
"app.kubernetes.io/managed-by" = "opentofu"
"app.kubernetes.io/instance" = var.app_name
}, var.secret_additional_labels)
}
data = var.app_configuration
}

View file

@ -0,0 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only
data "kubernetes_namespace_v1" "app" {
metadata {
name = var.app_namespace
}
}

View file

@ -0,0 +1,36 @@
# SPDX-License-Identifier: GPL-2.0-only
resource "kubernetes_manifest" "app_ingress_route_tcp" {
count = var.use_ingress && var.ingress_controller == "traefik" ? 1 : 0
manifest = {
apiVersion = "traefik.io/v1alpha1"
kind = "IngressRoute"
metadata = {
name = var.app_name
namespace = data.kubernetes_namespace_v1.app.metadata[0].name
annotations = var.ingress_annotations
labels = merge({
"app.kubernetes.io/component" = "server"
"app.kubernetes.io/name" = var.app_name
"app.kubernetes.io/version" = var.app_version
"app.kubernetes.io/part-of" = var.app_name
"app.kubernetes.io/managed-by" = "opentofu"
"app.kubernetes.io/instance" = var.app_name
}, var.ingress_additional_labels)
}
spec = {
entryPoints = var.traefik_entrypoints
routes = [
{
match = format("Host(`%s`)", var.ingress_host_url)
kind = "Rule"
services = [
{
name = var.app_name
port = var.service_container_port
}
]
}
]
}
}
}

View file

@ -0,0 +1,153 @@
# SPDX-License-Identifier: GPL-2.0-only
terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.25"
}
}
required_version = ">= 1.6.2"
}
provider "kubernetes" {
config_path = var.kubeconfig_path
config_context = var.kubeconfig_context
}
resource "kubernetes_deployment_v1" "app" {
metadata {
name = var.app_name
namespace = data.kubernetes_namespace_v1.app.metadata[0].name
labels = merge({
"app.kubernetes.io/name" = var.app_name
"app.kubernetes.io/version" = var.app_version
"app.kubernetes.io/managed-by" = "opentofu"
"app.kubernetes.io/instance" = var.app_name
}, var.deployment_additional_labels)
annotations = var.deployment_annotations
}
spec {
selector {
match_labels = {
"app.kubernetes.io/name" = var.app_name
}
}
template {
metadata {
annotations = var.pods_annotations
labels = merge({
"app.kubernetes.io/component" = "server"
"app.kubernetes.io/name" = var.app_name
"app.kubernetes.io/version" = var.app_version
"app.kubernetes.io/part-of" = var.app_name
"app.kubernetes.io/managed-by" = "opentofu"
"app.kubernetes.io/instance" = var.app_name
}, var.pods_additional_labels)
}
spec {
service_account_name = var.service_account_name
security_context {
run_as_non_root = true
run_as_group = 1000
run_as_user = 1000
}
## Web service
container {
name = var.app_name
image = var.container_invidious_image
image_pull_policy = var.container_invidious_image_pull_policy
port {
name = "http"
container_port = 3000
protocol = "TCP"
}
security_context {
allow_privilege_escalation = false
privileged = false
capabilities {
drop = ["ALL"]
}
}
readiness_probe {
initial_delay_seconds = 60
failure_threshold = 3
period_seconds = 10
success_threshold = 1
timeout_seconds = 3
http_get {
port = "http"
path = "/"
scheme = "HTTP"
}
}
liveness_probe {
initial_delay_seconds = 60
failure_threshold = 3
period_seconds = 10
success_threshold = 1
timeout_seconds = 5
http_get {
port = "http"
path = "/"
scheme = "HTTP"
}
}
startup_probe {
initial_delay_seconds = 60
failure_threshold = 30
period_seconds = 5
success_threshold = 1
timeout_seconds = 1
http_get {
port = "http"
path = "/"
scheme = "HTTP"
}
}
env_from {
secret_ref {
name = kubernetes_secret_v1.app_secrets.metadata[0].name
optional = false
}
}
# Linked to https://github.com/iv-org/invidious/issues/2970
env {
name = "INVIDIOUS_PORT"
value = 3000
}
resources {
requests = var.container_invidious_resources_requests
}
}
## IV Sig helper
container {
name = "${var.app_name}-sig-helper"
image = var.container_iv_sig_helper_image
image_pull_policy = var.container_iv_sig_helper_image_pull_policy
args = ["--tcp", "127.0.0.1:12999"]
port {
name = "http"
container_port = 12999
protocol = "TCP"
}
security_context {
allow_privilege_escalation = false
privileged = false
read_only_root_filesystem = true
capabilities {
drop = ["ALL"]
}
}
env {
name = "RUST_LOG"
value = "info"
}
resources {
requests = var.container_iv_sig_helper_resources_requests
}
}
}
}
}
}

View file

@ -0,0 +1,5 @@
# SPDX-License-Identifier: GPL-2.0-only
output "app_url" {
value = format("https://%s", var.ingress_host_url)
description = "Website URL"
}

View file

@ -0,0 +1,20 @@
# SPDX-License-Identifier: GPL-2.0-only
resource "kubernetes_service_account_v1" "invidious" {
metadata {
name = var.service_account_name
annotations = merge({
"kubernetes.io/enforce-mountable-secrets" = true
}, var.service_account_additional_annotations)
labels = merge({
"app.kubernetes.io/component" = "server"
"app.kubernetes.io/name" = var.app_name
"app.kubernetes.io/version" = var.app_version
"app.kubernetes.io/part-of" = var.app_name
"app.kubernetes.io/managed-by" = "opentofu"
"app.kubernetes.io/instance" = var.app_name
}, var.service_account_labels)
}
secret {
name = kubernetes_secret_v1.app_secrets.metadata[0].name
}
}

View file

@ -0,0 +1,28 @@
# SPDX-License-Identifier: GPL-2.0-only
resource "kubernetes_service_v1" "app" {
metadata {
name = var.app_name
annotations = {}
labels = merge({
"app.kubernetes.io/component" = "server"
"app.kubernetes.io/name" = var.app_name
"app.kubernetes.io/version" = var.app_version
"app.kubernetes.io/part-of" = var.app_name
"app.kubernetes.io/managed-by" = "opentofu"
"app.kubernetes.io/instance" = var.app_name
}, var.service_additional_labels)
}
spec {
type = var.service_type
selector = {
"app.kubernetes.io/name" = var.app_name
"app.kubernetes.io/instance" = var.app_name
}
port {
name = "http"
protocol = "TCP"
port = 3000
target_port = "http"
}
}
}

View file

@ -0,0 +1,286 @@
# SPDX-License-Identifier: GPL-2.0-only
## Providers
variable "kubeconfig_path" {
default = "~/.kube/config"
description = "Path to the kubeconfig file"
type = string
nullable = false
}
variable "kubeconfig_context" {
default = "default"
description = "Context to use to access the cluster"
type = string
nullable = false
}
## Application
variable "app_name" {
default = "invidious"
description = "Application name, used by various resources such as deployment, ingress, container, ..."
type = string
nullable = false
validation {
condition = length(regexall("[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*", var.app_name)) > 0
error_message = "Invalid value for 'app_name', must respect RFC 1123"
}
}
variable "app_configuration" {
default = {
INVIDIOUS_CONFIG = <<EOC
captcha_enabled: false
channel_threads: 1
db:
dbname: invidious
host: changeme
password: 'changeme'
port: 5432
user: changeme
signature_server: 127.0.0.1:12999
visitor_data: changeme
po_token: changeme
hmac_key: changeme
domain: changeme
external_port: 443
port: 3000
https_only: true
feed_threads: 1
full_refresh: true
popular_enabled: false
default_user_preferences:
autoplay: true
captions:
- French
- English
- English (auto-generated)
continue: true
continue_autoplay: true
dark_mode: dark
default_home: Subscriptions
feed_menu:
- Subscriptions
- Playlists
quality: dash
quality_dash: best
region: FR
save_player_pos: true
volume: 75
EOC
}
description = "Invidious configuration passed as an environment variable called INVIDIOUS_CONFIG"
type = object({INVIDIOUS_CONFIG=string})
nullable = false
validation {
condition = !strcontains(var.app_configuration.INVIDIOUS_CONFIG, "changeme")
error_message = "Some required variables are not correctly set; review DB configuration and values marked 'changeme'"
}
}
variable "app_version" {
default = "latest"
description = "Version of the application"
type = string
}
variable "app_namespace" {
default = "default"
description = "Namespace used to deploy app resources"
type = string
nullable = false
}
## Deployment
variable "deployment_annotations" {
default = {}
description = "Annotations for the deployment resource"
type = map(any)
}
variable "deployment_additional_labels" {
default = {}
description = "Additionnal labels for the deployment resource"
type = map(any)
}
## Pods
variable "pods_annotations" {
default = {}
description = "Annotations for the deployment resource"
type = map(any)
}
variable "pods_additional_labels" {
default = {}
description = "Additionnal labels for the deployment resource"
type = map(any)
}
## Containers
variable "container_invidious_image" {
default = "quay.io/invidious/invidious:latest"
description = "Image to use for the web app"
type = string
nullable = false
}
variable "container_invidious_image_pull_policy" {
default = "IfNotPresent"
description = "Pull policy; valid values are 'Always', 'IfNotPresent', 'Never'"
type = string
validation {
condition = contains(["Always", "IfNotPresent", "Never"], var.container_invidious_image_pull_policy)
error_message = "Invalid value for 'image_pull_policy'"
}
}
variable "container_iv_sig_helper_image" {
default = "quay.io/invidious/inv-sig-helper:latest"
description = "Image to use for the IV Sig helper service"
type = string
nullable = false
}
variable "container_iv_sig_helper_image_pull_policy" {
default = "IfNotPresent"
description = "Pull policy; valid values are 'Always', 'IfNotPresent', 'Never'"
type = string
validation {
condition = contains(["Always", "IfNotPresent", "Never"], var.container_iv_sig_helper_image_pull_policy)
error_message = "Invalid value for 'image_pull_policy'"
}
}
variable "container_invidious_resources_requests" {
default = {
cpu = "1500m"
memory = "4096Mi"
}
description = "Resources requests for the app container; supports 'cpu', 'memory', 'hugepages-2Mi' and 'hugepages-1Gi'"
type = object(
{
cpu = optional(string)
memory = optional(string)
hugepages-2Mi = optional(string)
hugepages-1Gi = optional(string)
}
)
}
variable "container_iv_sig_helper_resources_requests" {
default = {
cpu = "500m"
memory = "256Mi"
}
description = "Resources requests for the sig helper container; supports 'cpu', 'memory', 'hugepages-2Mi' and 'hugepages-1Gi'"
type = object(
{
cpu = optional(string)
memory = optional(string)
hugepages-2Mi = optional(string)
hugepages-1Gi = optional(string)
}
)
}
## Configuration
variable "secret_annotations" {
default = {}
description = "Annotations for the Secret resource"
type = map(any)
}
variable "secret_additional_labels" {
default = {}
description = "Additional app Secret labels"
type = map(any)
}
## Service
variable "service_container_port" {
default = 3000
description = "HTTP port used by the container"
type = number
nullable = false
}
variable "service_additional_labels" {
default = {}
description = "Additional labels for the service resource"
type = map(any)
}
variable "service_type" {
default = "ClusterIP"
description = "Type of the service resource"
type = string
}
## Ingress
variable "use_ingress" {
default = true
description = "Whether to use an ingress or not"
type = bool
}
variable "ingress_controller" {
default = "traefik"
description = "Type of ingress controller used; only traefik is supported at the moment"
type = string
nullable = false
validation {
condition = can(contains(["traefik"], var.ingress_controller))
error_message = "Invalid value for 'ingress_controller'"
}
}
variable "ingress_annotations" {
default = {}
description = "Ingress resource annotations"
type = map(any)
}
variable "ingress_additional_labels" {
default = {}
description = "Ingress resource annotations"
type = map(any)
}
variable "ingress_host_url" {
description = "Host used for the app, without the protocol prefix"
type = string
nullable = false
}
variable "traefik_entrypoints" {
default = ["websecure"]
description = "List of entrypoints used for the IngressTCP Traefik CRD"
type = list(string)
nullable = false
}
## Service account
variable "service_account_name" {
default = "invidious"
description = "Service account used for web app"
type = string
nullable = false
validation {
condition = length(regexall("[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*", var.service_account_name)) > 0
error_message = "Invalid value for 'service_account_name', must respect RFC 1123"
}
}
variable "service_account_additional_annotations" {
default = {}
description = "Additional annotations for the app's service account"
type = map(any)
}
variable "service_account_labels" {
default = {}
description = "Labels for the service account used by the app"
type = map(any)
}