commit 8351a2c667f5834178b4acdba6a3e72ccca06fce Author: ITQ Date: Sat Oct 4 08:54:34 2025 +0300 init: initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce2030d --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Terraform files +**/.terraform/* +*.tfstate +*.tfstate.* +crash.log +crash.*.log +*.tfvars +*.tfvars.json +override.tf +override.tf.json +*_override.tf +*_override.tf.json +.terraformrc +terraform.rc + +# Env files +.env + +# Yandex Cloud authorized key +key.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..17d86a0 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Terraform + +## Prerequisites + +Ensure you have the following installed on your system: + +- [Terraform](https://developer.hashicorp.com/terraform) (latest version recommended) + +## Main (`./main`) + +Main Terraform module for infrastructure + +## Service (`./service`) + +Terraform module to create service account for [Main](#main-main) terraform module. + +## CheatSheet + +```bash +# Init Terraform module +../scripts/init + +# Set required variables for terraform from dotenv file +export $(grep -v '^#' ./.env | xargs) + +# Unset required variables for terraform from dotenv file +unset $(grep -v '^#' ./.env | sed 's/=.*//' | xargs) +``` diff --git a/main/.env.template b/main/.env.template new file mode 100644 index 0000000..6853bb8 --- /dev/null +++ b/main/.env.template @@ -0,0 +1,20 @@ +# This is just a template file, create .env file when running init script +# Below all environment variables + +YC_SERVICE_ACCOUNT_KEY_FILE= +YC_CLOUD_ID= +YC_FOLDER_ID= + +# Must be the same as YC_FOLDER_ID and YC_CLOUD_ID +TF_VAR_cloud_id= +TF_VAR_folder_id= +TF_VAR_domain= +TF_VAR_ssh_public_key_path= + +AWS_BUCKET= +AWS_BUCKET_KEY= +AWS_ENDPOINT= +AWS_USE_PATH_STYLE= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION= diff --git a/main/.terraform.lock.hcl b/main/.terraform.lock.hcl new file mode 100644 index 0000000..7abbed5 --- /dev/null +++ b/main/.terraform.lock.hcl @@ -0,0 +1,23 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/random" { + version = "3.7.2" + hashes = [ + "h1:356j/3XnXEKr9nyicLUufzoF4Yr6hRy481KIxRVpK0c=", + ] +} + +provider "registry.terraform.io/hashicorp/template" { + version = "2.2.0" + hashes = [ + "h1:94qn780bi1qjrbC3uQtjJh3Wkfwd5+tTtJHOb7KTg9w=", + ] +} + +provider "registry.terraform.io/yandex-cloud/yandex" { + version = "0.148.0" + hashes = [ + "h1:sih4+ex4J/T0hiVzE+TB0mJUUr2NAb7C9f+Ya5J+qec=", + ] +} diff --git a/main/api_gateway.tf b/main/api_gateway.tf new file mode 100644 index 0000000..6468df0 --- /dev/null +++ b/main/api_gateway.tf @@ -0,0 +1,21 @@ +resource "yandex_api_gateway" "frontend" { + name = "frontend" + description = "API Gateway for frontend" + execution_timeout = "300" + + connectivity { + network_id = yandex_vpc_network.default.id + } + + custom_domains { + fqdn = var.domain + certificate_id = yandex_cm_certificate.default.id + } + + log_options { + log_group_id = yandex_logging_group.default.id + min_level = "INFO" + } + + spec = data.template_file.api_gateway_spec.rendered +} diff --git a/main/backends.tf b/main/backends.tf new file mode 100644 index 0000000..8e7fce7 --- /dev/null +++ b/main/backends.tf @@ -0,0 +1,13 @@ +terraform { + backend "s3" { + use_lockfile = true + max_retries = 5 + retry_mode = "adaptive" + + skip_region_validation = true + skip_credentials_validation = true + skip_metadata_api_check = true + skip_requesting_account_id = true + skip_s3_checksum = true + } +} diff --git a/main/certificate.tf b/main/certificate.tf new file mode 100644 index 0000000..13b06fe --- /dev/null +++ b/main/certificate.tf @@ -0,0 +1,11 @@ +resource "yandex_cm_certificate" "default" { + name = "default" + description = "Default certificate for all resources" + deletion_protection = true + + domains = tolist([var.domain, "*.${var.domain}"]) + + managed { + challenge_type = "DNS_CNAME" + } +} diff --git a/main/configs/api_gateway/spec.yaml b/main/configs/api_gateway/spec.yaml new file mode 100644 index 0000000..0e670e7 --- /dev/null +++ b/main/configs/api_gateway/spec.yaml @@ -0,0 +1,141 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: API Gateway + +paths: + /health: + get: + summary: Health Check + description: Endpoint to confirm the API Gateway service is operational. + operationId: healthCheck + x-yc-apigateway-integration: + type: dummy + http_code: 200 + http_headers: + "Content-Type": "application/json" + content: + "*": '{"status": "ok"}' + responses: + "200": + description: Service is healthy. + + /config: + get: + summary: Config + description: Implementation of frontend dynamic configuration. + operationId: config + x-yc-apigateway-integration: + type: dummy + http_code: 200 + http_headers: + "Content-Type": "application/json" + content: + "*": '{"backend_url": "https://backend.interview.yandex.itqdev.xyz"}' + responses: + "200": + description: Config returned. + + /assets/{path+}: + get: + summary: Get Static File + description: Serves static files from the S3 bucket. + operationId: getAssetsFile + parameters: + - name: path + in: path + required: true + schema: + type: string + x-yc-apigateway-integration: + type: object_storage + bucket: "${bucket_name}" + object: "current/assets/{path}" + error_object: "old/assets/{path}" + responses: + "200": + description: Static file content. + "404": + description: Asset not found. + + /img/{path+}: + get: + summary: Get Static File + description: Serves static files from the S3 bucket. + operationId: getImgFile + parameters: + - name: path + in: path + required: true + schema: + type: string + x-yc-apigateway-integration: + type: object_storage + bucket: "${bucket_name}" + object: "current/img/{path}" + error_object: "old/img/{path}" + responses: + "200": + description: Static file content. + "404": + description: Asset not found. + + /favicon.svg: + get: + summary: Get Static File + description: Serves static files from the S3 bucket. + operationId: getFaviconFile + parameters: + - name: path + in: path + required: true + schema: + type: string + x-yc-apigateway-integration: + type: object_storage + bucket: "${bucket_name}" + object: "current/favicon.svg" + error_object: "old/favicon.svg" + responses: + "200": + description: Static file content. + "404": + description: Asset not found. + + /: + get: + summary: Serve SPA Entrypoint (Root) + description: Serves the main 'index.html' file for requests to the root of the domain. + operationId: getSpaIndexRoot + x-yc-apigateway-integration: + type: object_storage + bucket: "${bucket_name}" + object: "current/index.html" + error_object: "old/index.html" + responses: + "200": + description: Main HTML page of the SPA. + "404": + description: "'index.html' not found in the bucket." + + /{proxy+}: + get: + summary: Serve SPA Entrypoint (Client-Side Routes) + description: Catches all other application paths and serves 'index.html', enabling client-side routing. + operationId: getSpaIndexCatchAll + parameters: + - name: proxy + in: path + required: true + schema: + type: string + x-yc-apigateway-integration: + type: object_storage + bucket: "${bucket_name}" + object: "current/index.html" + error_object: "old/index.html" + responses: + "200": + description: Main HTML page of the SPA. + "404": + description: "'index.html' not found in the bucket." diff --git a/main/configs/cloud_init/main_master.yaml b/main/configs/cloud_init/main_master.yaml new file mode 100644 index 0000000..4e0be65 --- /dev/null +++ b/main/configs/cloud_init/main_master.yaml @@ -0,0 +1,59 @@ +#cloud-config + +ssh_pwauth: false +package_update: true +package_upgrade: true + +growpart: + mode: growpart + devices: ["/"] + ignore_growroot_disabled: true + +users: + - name: ubuntu + sudo: ALL=(ALL) NOPASSWD:ALL + lock_passwd: true + shell: /bin/bash + +write_files: + - path: /var/lib/rancher/credentialprovider/config.yaml + permissions: "0600" + encoding: b64 + content: ${k3s_credential_provider_config} + - path: /var/lib/rancher/credentialprovider/bin/yc-credential-provider + permissions: "0700" + encoding: b64 + content: ${k3s_credential_provider} + +runcmd: + - | + export HOME=/root + curl https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash -s -- -i /tmp/yc -n + mv /tmp/yc/bin/yc /usr/bin/ + yc config set instance-service-account true + yc config set cloud-id ${yc_cloud_id} + - | + DISK=/dev/disk/by-id/virtio-data + K3S_DIR=${k3s_dir} + mkdir -p $K3S_DIR + if ! blkid $DISK > /dev/null; then + mkfs.ext4 $DISK + fi + echo "$DISK $K3S_DIR ext4 defaults 0 0" >> /etc/fstab + mount -a + - | + curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=stable INSTALL_K3S_EXEC="server \ + --cluster-init \ + --token=${k3s_token} \ + --node-taint CriticalAddonsOnly=true:NoExecute \ + --write-kubeconfig ${k3s_dir}/kubeconfig/config \ + --image-credential-provider-bin-dir /var/lib/rancher/credentialprovider/bin \ + --image-credential-provider-config /var/lib/rancher/credentialprovider/config.yaml \ + --data-dir=${k3s_dir} \ + --default-local-storage-path=${k3s_dir}/storage" sh - + - | + curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + - | + mkdir -p /home/ubuntu/.kube + cp ${k3s_dir}/kubeconfig/config /home/ubuntu/.kube/config + chown -R ubuntu:ubuntu /home/ubuntu/ diff --git a/main/configs/cloud_init/master.yaml b/main/configs/cloud_init/master.yaml new file mode 100644 index 0000000..5459497 --- /dev/null +++ b/main/configs/cloud_init/master.yaml @@ -0,0 +1,57 @@ +#cloud-config + +ssh_pwauth: false +package_update: true +package_upgrade: true + +growpart: + mode: growpart + devices: ["/"] + ignore_growroot_disabled: true + +users: + - name: ubuntu + sudo: ALL=(ALL) NOPASSWD:ALL + lock_passwd: true + shell: /bin/bash + +write_files: + - path: /var/lib/rancher/credentialprovider/config.yaml + permissions: "0600" + encoding: b64 + content: ${k3s_credential_provider_config} + - path: /var/lib/rancher/credentialprovider/bin/yc-credential-provider + permissions: "0700" + encoding: b64 + content: ${k3s_credential_provider} + +runcmd: + - | + export HOME=/root + curl https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash -s -- -i /tmp/yc -n + mv /tmp/yc/bin/yc /usr/bin/ + yc config set instance-service-account true + yc config set cloud-id ${yc_cloud_id} + - | + DISK=/dev/disk/by-id/virtio-data + K3S_DIR=${k3s_dir} + mkdir -p $K3S_DIR + if ! blkid $DISK > /dev/null; then + mkfs.ext4 $DISK + fi + echo "$DISK $K3S_DIR ext4 defaults 0 0" >> /etc/fstab + mount -a + - | + curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=stable INSTALL_K3S_EXEC="server \ + --server=https://${k3s_master_ip}:6443 \ + --token=${k3s_token} \ + --node-name=`hostname -f` \ + --write-kubeconfig ${k3s_dir}/kubeconfig/config \ + --image-credential-provider-bin-dir /var/lib/rancher/credentialprovider/bin \ + --image-credential-provider-config /var/lib/rancher/credentialprovider/config.yaml \ + --data-dir=${k3s_dir} \ + --default-local-storage-path=${k3s_dir}/storage" sh - + - | + mkdir -p /home/ubuntu/.kube + cp ${k3s_dir}/kubeconfig/config /home/ubuntu/.kube/config + chown -R ubuntu:ubuntu /home/ubuntu/ diff --git a/main/configs/cloud_init/worker.yaml b/main/configs/cloud_init/worker.yaml new file mode 100644 index 0000000..60ba79b --- /dev/null +++ b/main/configs/cloud_init/worker.yaml @@ -0,0 +1,55 @@ +#cloud-config + +ssh_pwauth: false +package_update: true +package_upgrade: true + +growpart: + mode: growpart + devices: ["/"] + ignore_growroot_disabled: true + +users: + - name: ubuntu + sudo: ALL=(ALL) NOPASSWD:ALL + lock_passwd: true + shell: /bin/bash + +write_files: + - path: /var/lib/rancher/credentialprovider/config.yaml + permissions: "0600" + encoding: b64 + content: ${k3s_credential_provider_config} + - path: /var/lib/rancher/credentialprovider/bin/yc-credential-provider + permissions: "0700" + encoding: b64 + content: ${k3s_credential_provider} + +runcmd: + - | + export HOME=/root + curl https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash -s -- -i /tmp/yc -n + mv /tmp/yc/bin/yc /usr/bin/ + yc config set instance-service-account true + yc config set cloud-id ${yc_cloud_id} + - | + DISK=/dev/disk/by-id/virtio-data + K3S_DIR=${k3s_dir} + mkdir -p $K3S_DIR + if ! blkid $DISK > /dev/null; then + mkfs.ext4 $DISK + fi + echo "$DISK $K3S_DIR ext4 defaults 0 0" >> /etc/fstab + mount -a + - | + curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=stable INSTALL_K3S_EXEC="agent \ + --server=https://${k3s_master_ip}:6443 \ + --token=${k3s_token} \ + --node-name=`hostname -f` \ + --image-credential-provider-bin-dir /var/lib/rancher/credentialprovider/bin \ + --image-credential-provider-config /var/lib/rancher/credentialprovider/config.yaml \ + --data-dir=${k3s_dir}" sh - + - | + mkdir -p /home/ubuntu/.kube + cp ${k3s_dir}/kubeconfig/config /home/ubuntu/.kube/config + chown -R ubuntu:ubuntu /home/ubuntu/ diff --git a/main/configs/k3s/credentialprovider.yaml b/main/configs/k3s/credentialprovider.yaml new file mode 100644 index 0000000..e722b40 --- /dev/null +++ b/main/configs/k3s/credentialprovider.yaml @@ -0,0 +1,10 @@ +apiVersion: kubelet.config.k8s.io/v1beta1 +kind: CredentialProviderConfig +providers: + - name: yc-credential-provider + apiVersion: credentialprovider.kubelet.k8s.io/v1 + matchImages: + - "cr.yandex" + - "cr.cloud.yandex.net" + - "container-registry.cloud.yandex.net" + defaultCacheDuration: "1h" diff --git a/main/configs/k3s/yc-credential-provider b/main/configs/k3s/yc-credential-provider new file mode 100644 index 0000000..a9afffb --- /dev/null +++ b/main/configs/k3s/yc-credential-provider @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -euo pipefail + +command -v jq >/dev/null || { echo "jq required" >&2; exit 1; } +command -v yc >/dev/null || { echo "yc required" >&2; exit 1; } + +req=$(cat) +image=$(printf '%s' "$req" | jq -r '.image // empty') + +if [[ -z "$image" ]]; then + echo "no image in request" >&2 + exit 1 +fi + +registry=${image%%/*} +if [[ -z "$registry" ]]; then + registry="$image" +fi + +creds_json=$(printf '%s' "$image" | yc container docker-credential get 2>/dev/null) || { + echo "yc docker-credential failed" >&2 + exit 1 +} + +username=$(printf '%s' "$creds_json" | jq -r '.Username // empty') +secret=$(printf '%s' "$creds_json" | jq -r '.Secret // empty') + +if [[ -z "$username" || -z "$secret" ]]; then + echo "failed to parse yc output" >&2 + exit 1 +fi + +jq -n \ + --arg api "credentialprovider.kubelet.k8s.io/v1" \ + --arg kind "CredentialProviderResponse" \ + --arg reg "$registry" \ + --arg user "$username" \ + --arg pass "$secret" \ + --arg cache "6h" \ + --arg keyType "Registry" \ +'{ + apiVersion: $api, + kind: $kind, + auth: { ($reg): { username: $user, password: $pass } }, + cacheDuration: $cache, + cacheKeyType: $keyType +}' diff --git a/main/disk.tf b/main/disk.tf new file mode 100644 index 0000000..664a204 --- /dev/null +++ b/main/disk.tf @@ -0,0 +1,23 @@ +resource "yandex_compute_disk" "master_disk" { + name = "master_disk" + description = "Disk for master persistent storage" + + zone = "ru-central1-d" + size = 10 + type = "network-hdd" + block_size = 4096 + + allow_recreate = false +} + +resource "yandex_compute_disk" "worker_minecraft_disk" { + name = "worker_minecraft_disk" + description = "Disk for worker persistent storage" + + zone = "ru-central1-d" + size = 20 + type = "network-ssd" + block_size = 4096 + + allow_recreate = false +} diff --git a/main/iam.tf b/main/iam.tf new file mode 100644 index 0000000..f8373cf --- /dev/null +++ b/main/iam.tf @@ -0,0 +1,85 @@ +resource "yandex_iam_service_account" "instance_group_sa" { + name = "instance-group-sa" + description = "Service account for instance group" +} + +resource "yandex_resourcemanager_folder_iam_member" "instance_group_sa_compute_editor" { + folder_id = var.folder_id + role = "compute.editor" + member = "serviceAccount:${yandex_iam_service_account.instance_group_sa.id}" +} + +resource "yandex_resourcemanager_folder_iam_member" "instance_group_sa_lb_editor" { + folder_id = var.folder_id + role = "load-balancer.editor" + member = "serviceAccount:${yandex_iam_service_account.instance_group_sa.id}" +} + +resource "yandex_iam_service_account" "instance_group_vm_sa" { + name = "instance-group-vm-sa" + description = "Service account for instance group vm" +} + +resource "yandex_iam_service_account_iam_binding" "instance_group_vm_sa_admin" { + service_account_id = yandex_iam_service_account.instance_group_vm_sa.id + role = "admin" + + members = [ + "serviceAccount:${yandex_iam_service_account.instance_group_sa.id}", + ] +} + +resource "yandex_iam_service_account" "master_vm_sa" { + name = "master-vm-sa" + description = "Service account for master vm" +} + +resource "yandex_compute_instance_iam_binding" "master_vm_sa_editor" { + instance_id = yandex_compute_instance.master.id + role = "editor" + members = [ + "serviceAccount:${yandex_iam_service_account.master_vm_sa.id}" + ] +} + +resource "yandex_resourcemanager_folder_iam_member" "master_vm_sa_viewer" { + folder_id = var.folder_id + role = "compute.viewer" + member = "serviceAccount:${yandex_iam_service_account.master_vm_sa.id}" +} + +resource "yandex_resourcemanager_folder_iam_member" "master_vm_sa_oslogin" { + folder_id = var.folder_id + role = "compute.osAdminLogin" + member = "serviceAccount:${yandex_iam_service_account.master_vm_sa.id}" +} + +resource "yandex_iam_service_account" "registry_push_sa" { + name = "registry-push-sa" + description = "Service account for CI" +} + +resource "yandex_iam_service_account_key" "registry_push_sa_key" { + service_account_id = yandex_iam_service_account.registry_push_sa.id + description = "Key for CI" +} + +resource "yandex_iam_service_account" "registry_pull_sa" { + name = "registry-pull-sa" + description = "Service account for CD" +} + +resource "yandex_iam_service_account_key" "registry_pull_sa_key" { + service_account_id = yandex_iam_service_account.registry_pull_sa.id + description = "Key for CD" +} + +resource "yandex_iam_service_account" "frontend_push_sa" { + name = "frontend-push-sa" + description = "Service account for frontend CD" +} + +resource "yandex_iam_service_account_key" "frontend_push_sa_key" { + service_account_id = yandex_iam_service_account.frontend_push_sa.id + description = "Key for frontend CD" +} diff --git a/main/instance.tf b/main/instance.tf new file mode 100644 index 0000000..2d21266 --- /dev/null +++ b/main/instance.tf @@ -0,0 +1,143 @@ +resource "yandex_compute_instance" "master" { + name = "master" + hostname = "master" + description = "Salt, ansible and kubernetes master, provisions other nodes" + platform_id = "standard-v3" + zone = "ru-central1-d" + service_account_id = yandex_iam_service_account.master_vm_sa.id + + allow_recreate = true + allow_stopping_for_update = true + + labels = { + salt = "master" + ansible = "master" + } + + resources { + cores = 2 + memory = 2 + core_fraction = 20 + gpus = 0 + } + + boot_disk { + initialize_params { + image_id = data.yandex_compute_image.ubuntu_2404.id + description = "Boot disk for master" + block_size = 4096 + size = 10 + type = "network-hdd" + } + auto_delete = true + mode = "READ_WRITE" + } + + secondary_disk { + disk_id = yandex_compute_disk.master_disk.id + device_name = "data" + mode = "READ_WRITE" + auto_delete = false + } + + network_interface { + subnet_id = yandex_vpc_subnet.default_ru_central1_d.id + ip_address = local.master_ip + ipv4 = true + nat = true + dns_record { + fqdn = local.master_fqdn + ptr = true + ttl = 10 + } + } + network_acceleration_type = "standard" + + metadata = { + enable-oslogin = true + serial-port-enable = 1 + install-unified-agent = 0 + user-data = data.template_file.main_master_cloud_init.rendered + } + + metadata_options { + aws_v1_http_endpoint = 1 + aws_v1_http_token = 2 + gce_http_endpoint = 1 + gce_http_token = 1 + } + placement_policy { + host_affinity_rules = [] + placement_group_partition = 0 + } +} + +resource "yandex_compute_instance" "worker-minecraft" { + name = "worker-minecraft" + hostname = "worker-minecraft.internal" + description = "Worker node for Kubernetes cluster" + labels = { + "kubernetes" = "worker" + "role" = "worker" + "salt" = "minion" + } + + service_account_id = yandex_iam_service_account.instance_group_vm_sa.id + platform_id = "standard-v3" + zone = "ru-central1-d" + + resources { + core_fraction = 100 + cores = 2 + gpus = 0 + memory = 8 + } + + boot_disk { + initialize_params { + image_id = data.yandex_compute_image.ubuntu_2404.id + description = "Boot disk" + size = 10 + type = "network-hdd" + } + mode = "READ_WRITE" + } + + secondary_disk { + disk_id = yandex_compute_disk.worker_minecraft_disk.id + device_name = "data" + mode = "READ_WRITE" + auto_delete = false + } + + network_interface { + subnet_id = yandex_vpc_subnet.default_ru_central1_d.id + ipv4 = true + nat = false + } + network_acceleration_type = "standard" + + + metadata = { + enable-oslogin = true + serial-port-enable = 1 + install-unified-agent = 0 + user-data = data.template_file.worker_cloud_init.rendered + } + + metadata_options { + aws_v1_http_endpoint = 1 + aws_v1_http_token = 2 + gce_http_endpoint = 1 + gce_http_token = 1 + } + placement_policy { + host_affinity_rules = [] + placement_group_partition = 0 + } + + depends_on = [ + yandex_compute_instance.master, + yandex_iam_service_account.instance_group_vm_sa + ] +} diff --git a/main/instance_group.tf b/main/instance_group.tf new file mode 100644 index 0000000..8c18938 --- /dev/null +++ b/main/instance_group.tf @@ -0,0 +1,233 @@ +resource "yandex_compute_instance_group" "master" { + name = "master" + description = "Masters instance group for Kubernetes cluster" + service_account_id = yandex_iam_service_account.instance_group_sa.id + deletion_protection = true + labels = { + "kubernetes" = "cluster" + "role" = "control-plane" + } + + instance_template { + name = "master-{instance.index_in_zone}-{instance.zone_id}" + hostname = "master-{instance.index_in_zone}.{instance.zone_id}" + labels = { + "instance-group" = "master" + "kubernetes" = "control-plane" + "salt" = "minion" + } + service_account_id = yandex_iam_service_account.instance_group_vm_sa.id + + platform_id = "standard-v3" + resources { + core_fraction = 50 + cores = 2 + gpus = 0 + memory = 4 + } + + boot_disk { + initialize_params { + image_id = data.yandex_compute_image.ubuntu_2404.id + description = "Boot disk" + size = 10 + type = "network-hdd" + } + mode = "READ_WRITE" + } + secondary_disk { + device_name = "data" + initialize_params { + description = "Data disk" + size = 20 + type = "network-hdd" + } + mode = "READ_WRITE" + } + + network_interface { + network_id = yandex_vpc_network.default.id + subnet_ids = [ + yandex_vpc_subnet.default_ru_central1_a.id, + yandex_vpc_subnet.default_ru_central1_b.id, + yandex_vpc_subnet.default_ru_central1_d.id, + ] + nat = false + } + network_settings { + type = "STANDARD" + } + + metadata_options { + aws_v1_http_endpoint = 1 + aws_v1_http_token = 2 + gce_http_endpoint = 1 + gce_http_token = 1 + } + metadata = { + enable-oslogin = true + serial-port-enable = 1 + install-unified-agent = 0 + user-data = data.template_file.master_cloud_init.rendered + } + } + + scale_policy { + fixed_scale { + size = 2 + } + } + allocation_policy { + zones = ["ru-central1-a", "ru-central1-b"] + } + deploy_policy { + max_creating = 3 + max_deleting = 3 + max_expansion = 0 + max_unavailable = 1 + startup_duration = 300 + strategy = "proactive" + } + + load_balancer { + target_group_name = "master-instance-group" + target_group_description = "Load balancer target group for k8s masters" + max_opening_traffic_duration = 600 + ignore_health_checks = true + } + health_check { + healthy_threshold = 2 + unhealthy_threshold = 2 + interval = 2 + timeout = 1 + + tcp_options { + port = 6443 + } + } + max_checking_health_duration = 600 + + depends_on = [ + yandex_compute_instance.master, + yandex_iam_service_account.instance_group_sa, + yandex_resourcemanager_folder_iam_member.instance_group_sa_compute_editor, + yandex_resourcemanager_folder_iam_member.instance_group_sa_lb_editor + ] +} + +resource "yandex_compute_instance_group" "worker" { + name = "worker" + description = "Workers instance group for Kubernetes cluster" + service_account_id = yandex_iam_service_account.instance_group_sa.id + deletion_protection = true + labels = { + "kubernetes" = "cluster" + "role" = "worker" + } + + instance_template { + name = "worker-{instance.index_in_zone}-{instance.zone_id}" + hostname = "worker-{instance.index_in_zone}.{instance.zone_id}.internal" + labels = { + "instance-group" = "worker" + "kubernetes" = "worker" + "salt" = "minion" + } + service_account_id = yandex_iam_service_account.instance_group_vm_sa.id + + platform_id = "standard-v3" + resources { + core_fraction = 50 + cores = 2 + gpus = 0 + memory = 2 + } + + boot_disk { + initialize_params { + image_id = data.yandex_compute_image.ubuntu_2404.id + description = "Boot disk" + size = 10 + type = "network-hdd" + } + mode = "READ_WRITE" + } + secondary_disk { + device_name = "data" + initialize_params { + description = "Data disk" + size = 20 + type = "network-hdd" + } + mode = "READ_WRITE" + } + + network_interface { + network_id = yandex_vpc_network.default.id + subnet_ids = [ + yandex_vpc_subnet.default_ru_central1_a.id, + yandex_vpc_subnet.default_ru_central1_b.id, + yandex_vpc_subnet.default_ru_central1_d.id, + ] + nat = false + } + network_settings { + type = "STANDARD" + } + + metadata_options { + aws_v1_http_endpoint = 1 + aws_v1_http_token = 2 + gce_http_endpoint = 1 + gce_http_token = 1 + } + metadata = { + enable-oslogin = true + serial-port-enable = 1 + install-unified-agent = 0 + user-data = data.template_file.worker_cloud_init.rendered + } + } + + scale_policy { + fixed_scale { + size = 0 + } + } + allocation_policy { + zones = ["ru-central1-a", "ru-central1-b", "ru-central1-d"] + } + deploy_policy { + max_creating = 3 + max_deleting = 3 + max_expansion = 0 + max_unavailable = 1 + startup_duration = 300 + strategy = "proactive" + } + + load_balancer { + target_group_name = "worker-instance-group" + target_group_description = "Load balancer target group for k8s workers" + max_opening_traffic_duration = 600 + ignore_health_checks = true + } + health_check { + healthy_threshold = 2 + unhealthy_threshold = 2 + interval = 2 + timeout = 1 + + tcp_options { + port = 80 + } + } + max_checking_health_duration = 600 + + depends_on = [ + yandex_compute_instance.master, + yandex_iam_service_account.instance_group_sa, + yandex_resourcemanager_folder_iam_member.instance_group_sa_compute_editor, + yandex_resourcemanager_folder_iam_member.instance_group_sa_lb_editor + ] +} diff --git a/main/loadbalancer.tf b/main/loadbalancer.tf new file mode 100644 index 0000000..00be048 --- /dev/null +++ b/main/loadbalancer.tf @@ -0,0 +1,36 @@ +# resource "yandex_lb_network_load_balancer" "instance_group_lb" { +# name = "instance-group-lb" +# allow_zonal_shift = true +# deletion_protection = true + +# listener { +# name = "test" +# port = 8080 +# external_address_spec { +# # address = yandex_vpc_address.default.external_ipv4_address[0].address +# ip_version = "ipv4" +# } +# } + +# attached_target_group { +# target_group_id = yandex_compute_instance_group.default.load_balancer[0].target_group_id + +# healthcheck { +# name = "http" +# http_options { +# port = 8080 +# path = "/ping" +# } +# } +# } +# } + +resource "yandex_lb_target_group" "main_master" { + name = "main-master" + region_id = "ru-central1" + + target { + subnet_id = yandex_vpc_subnet.default_ru_central1_d.id + address = yandex_compute_instance.master.network_interface.0.ip_address + } +} diff --git a/main/locals.tf b/main/locals.tf new file mode 100644 index 0000000..b1ad54c --- /dev/null +++ b/main/locals.tf @@ -0,0 +1,7 @@ +locals { + master_ip = "10.4.0.3" + master_fqdn = "master." + + k3s_data_dir = "/mnt/k3s" + k3s_channel = "stable" +} diff --git a/main/logging.tf b/main/logging.tf new file mode 100644 index 0000000..ce2acc2 --- /dev/null +++ b/main/logging.tf @@ -0,0 +1,5 @@ +resource "yandex_logging_group" "default" { + name = "default" + + retention_period = "336h0m0s" +} diff --git a/main/main.tf b/main/main.tf new file mode 100644 index 0000000..42c9397 --- /dev/null +++ b/main/main.tf @@ -0,0 +1,3 @@ +data "yandex_compute_image" "ubuntu_2404" { + family = "ubuntu-2404-lts-oslogin" +} diff --git a/main/network.tf b/main/network.tf new file mode 100644 index 0000000..c5a1849 --- /dev/null +++ b/main/network.tf @@ -0,0 +1,80 @@ +resource "yandex_vpc_network" "default" { + name = "default" + description = "Default network" + labels = { + default = true + } +} + +resource "yandex_vpc_default_security_group" "default" { + network_id = yandex_vpc_network.default.id + description = "Default security group for default network" + + egress { + protocol = "ANY" + v4_cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + protocol = "ANY" + v4_cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "yandex_vpc_subnet" "default_ru_central1_a" { + name = "default-ru-central1-a" + description = "Default subnet for ru-central1-a" + + network_id = yandex_vpc_network.default.id + route_table_id = yandex_vpc_route_table.default.id + v4_cidr_blocks = ["10.1.0.0/16"] + zone = "ru-central1-a" +} + +resource "yandex_vpc_subnet" "default_ru_central1_b" { + name = "default-ru-central1-b" + description = "Default subnet for ru-central1-b" + + network_id = yandex_vpc_network.default.id + route_table_id = yandex_vpc_route_table.default.id + v4_cidr_blocks = ["10.2.0.0/16"] + zone = "ru-central1-b" +} + +resource "yandex_vpc_subnet" "default_ru_central1_d" { + name = "default-ru-central1-d" + description = "Default subnet for ru-central1-d" + + network_id = yandex_vpc_network.default.id + route_table_id = yandex_vpc_route_table.default.id + v4_cidr_blocks = ["10.4.0.0/16"] + zone = "ru-central1-d" +} + +resource "yandex_vpc_address" "nlb" { + name = "nlb" + description = "External IP address for network lb" + deletion_protection = true + + external_ipv4_address { + zone_id = "ru-central1-d" + } +} + +resource "yandex_vpc_gateway" "default" { + name = "default" + description = "Default Gateway for all instances" + labels = {} + + shared_egress_gateway {} +} + +resource "yandex_vpc_route_table" "default" { + name = "default" + network_id = yandex_vpc_network.default.id + + static_route { + destination_prefix = "0.0.0.0/0" + gateway_id = yandex_vpc_gateway.default.id + } +} diff --git a/main/object_storage.tf b/main/object_storage.tf new file mode 100644 index 0000000..d6408e7 --- /dev/null +++ b/main/object_storage.tf @@ -0,0 +1,34 @@ +resource "yandex_storage_bucket" "frontend" { + bucket = var.domain + default_storage_class = "STANDARD" + force_destroy = false + max_size = 104857600 + + anonymous_access_flags { + config_read = false + list = true + read = true + } +} + +resource "yandex_storage_bucket" "frontend_develop" { + bucket = "dev.${var.domain}" + default_storage_class = "STANDARD" + force_destroy = false + max_size = 104857600 + + anonymous_access_flags { + config_read = false + list = true + read = true + } +} + +resource "yandex_storage_bucket_iam_binding" "frontend_bucket_push" { + bucket = yandex_storage_bucket.frontend.bucket + role = "storage.editor" + + members = [ + "serviceAccount:${yandex_iam_service_account.frontend_push_sa.id}", + ] +} diff --git a/main/outputs.tf b/main/outputs.tf new file mode 100644 index 0000000..28e9cf1 --- /dev/null +++ b/main/outputs.tf @@ -0,0 +1,51 @@ +output "registry_data" { + value = { + url = "cr.yandex/${yandex_container_registry.default.id}" + } +} + +output "api_gateway_data" { + value = { + domain = yandex_api_gateway.frontend.domain + } +} + +output "registry_push_sa_key" { + value = jsonencode({ + id = yandex_iam_service_account_key.registry_push_sa_key.id + service_account_id = yandex_iam_service_account_key.registry_push_sa_key.service_account_id + created_at = yandex_iam_service_account_key.registry_push_sa_key.created_at + key_algorithm = yandex_iam_service_account_key.registry_push_sa_key.key_algorithm + public_key = yandex_iam_service_account_key.registry_push_sa_key.public_key + private_key = yandex_iam_service_account_key.registry_push_sa_key.private_key + }) + sensitive = true +} + +output "registry_pull_sa_key" { + value = jsonencode({ + id = yandex_iam_service_account_key.registry_pull_sa_key.id + service_account_id = yandex_iam_service_account_key.registry_pull_sa_key.service_account_id + created_at = yandex_iam_service_account_key.registry_pull_sa_key.created_at + key_algorithm = yandex_iam_service_account_key.registry_pull_sa_key.key_algorithm + public_key = yandex_iam_service_account_key.registry_pull_sa_key.public_key + private_key = yandex_iam_service_account_key.registry_pull_sa_key.private_key + }) + sensitive = true +} + +output "frontend_push_sa_key" { + value = jsonencode({ + id = yandex_iam_service_account_key.frontend_push_sa_key.id + service_account_id = yandex_iam_service_account_key.frontend_push_sa_key.service_account_id + created_at = yandex_iam_service_account_key.frontend_push_sa_key.created_at + key_algorithm = yandex_iam_service_account_key.frontend_push_sa_key.key_algorithm + public_key = yandex_iam_service_account_key.frontend_push_sa_key.public_key + private_key = yandex_iam_service_account_key.frontend_push_sa_key.private_key + }) + sensitive = true +} + +output "lets_encrypt_challenges" { + value = yandex_cm_certificate.default.challenges +} diff --git a/main/password.tf b/main/password.tf new file mode 100644 index 0000000..148d849 --- /dev/null +++ b/main/password.tf @@ -0,0 +1,4 @@ +resource "random_password" "k3s_token" { + length = 32 + special = false +} diff --git a/main/provider.tf b/main/provider.tf new file mode 100644 index 0000000..6fc66a3 --- /dev/null +++ b/main/provider.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + yandex = { + source = "yandex-cloud/yandex" + } + } + required_version = ">= 0.13" +} + +provider "yandex" { + max_retries = 3 + insecure = false +} diff --git a/main/registry.tf b/main/registry.tf new file mode 100644 index 0000000..32cd70e --- /dev/null +++ b/main/registry.tf @@ -0,0 +1,32 @@ +resource "yandex_container_registry" "default" { + name = "default" + + labels = {} +} + +resource "yandex_container_registry_iam_binding" "default_push" { + registry_id = yandex_container_registry.default.id + role = "container-registry.images.pusher" + + members = [ + "serviceAccount:${yandex_iam_service_account.registry_push_sa.id}", + ] +} + +resource "yandex_container_registry_iam_binding" "default_pull" { + registry_id = yandex_container_registry.default.id + role = "container-registry.images.puller" + + members = [ + "serviceAccount:${yandex_iam_service_account.registry_pull_sa.id}", + ] +} + +resource "yandex_container_registry_iam_binding" "instance_group_vm_sa_pull" { + registry_id = yandex_container_registry.default.id + role = "container-registry.images.puller" + + members = [ + "serviceAccount:${yandex_iam_service_account.instance_group_vm_sa.id}", + ] +} diff --git a/main/templates.tf b/main/templates.tf new file mode 100644 index 0000000..3c30af0 --- /dev/null +++ b/main/templates.tf @@ -0,0 +1,41 @@ +data "template_file" "main_master_cloud_init" { + template = file("${path.module}/configs/cloud_init/main_master.yaml") + vars = { + k3s_credential_provider_config = base64encode(file("${path.module}/configs/k3s/credentialprovider.yaml")) + k3s_credential_provider = base64encode(file("${path.module}/configs/k3s/yc-credential-provider")) + k3s_dir = local.k3s_data_dir + k3s_token = random_password.k3s_token.result + yc_cloud_id = var.cloud_id + } +} + +data "template_file" "master_cloud_init" { + template = file("${path.module}/configs/cloud_init/master.yaml") + vars = { + k3s_credential_provider_config = base64encode(file("${path.module}/configs/k3s/credentialprovider.yaml")) + k3s_credential_provider = base64encode(file("${path.module}/configs/k3s/yc-credential-provider")) + k3s_dir = local.k3s_data_dir + k3s_token = random_password.k3s_token.result + k3s_master_ip = local.master_ip + yc_cloud_id = var.cloud_id + } +} + +data "template_file" "worker_cloud_init" { + template = file("${path.module}/configs/cloud_init/worker.yaml") + vars = { + k3s_credential_provider_config = base64encode(file("${path.module}/configs/k3s/credentialprovider.yaml")) + k3s_credential_provider = base64encode(file("${path.module}/configs/k3s/yc-credential-provider")) + k3s_dir = local.k3s_data_dir + k3s_token = random_password.k3s_token.result + k3s_master_ip = local.master_ip + yc_cloud_id = var.cloud_id + } +} + +data "template_file" "api_gateway_spec" { + template = file("${path.module}/configs/api_gateway/spec.yaml") + vars = { + bucket_name = yandex_storage_bucket.frontend.bucket + } +} diff --git a/main/variables.tf b/main/variables.tf new file mode 100644 index 0000000..3301ddd --- /dev/null +++ b/main/variables.tf @@ -0,0 +1,18 @@ +variable "folder_id" {} + +variable "cloud_id" {} + +variable "resources_prefix" { + type = string + default = "" +} + +variable "domain" { + description = "Domain to issue TLS certificates for with Let's Encrypt" + type = string +} + +variable "ssh_public_key_path" { + description = "This public key will be placed on all nodes" + type = string +} diff --git a/scripts/init b/scripts/init new file mode 100755 index 0000000..7a86226 --- /dev/null +++ b/scripts/init @@ -0,0 +1,15 @@ +#!/bin/bash + +if [ -f ./.env ]; then + export $(grep -v '^#' ./.env | xargs) +else + echo ".env file not found!" + exit 1 +fi + +terraform init \ + -backend-config="endpoint=${AWS_ENDPOINT}" \ + -backend-config="bucket=${AWS_BUCKET}" \ + -backend-config="key=${AWS_BUCKET_KEY}" \ + -backend-config="use_path_style=${AWS_USE_PATH_STYLE}" \ + -migrate-state diff --git a/service/.env.template b/service/.env.template new file mode 100644 index 0000000..771fc05 --- /dev/null +++ b/service/.env.template @@ -0,0 +1,17 @@ +# This is just a template file, create environment variables when applying +# Below all environment variables + +YC_TOKEN= +YC_CLOUD_ID= +YC_FOLDER_ID= + +# Must be the same as YC_FOLDER_ID +TF_VAR_folder_id= + +AWS_BUCKET= +AWS_BUCKET_KEY= +AWS_ENDPOINT= +AWS_USE_PATH_STYLE= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION= diff --git a/service/.terraform.lock.hcl b/service/.terraform.lock.hcl new file mode 100644 index 0000000..13f02e4 --- /dev/null +++ b/service/.terraform.lock.hcl @@ -0,0 +1,9 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/yandex-cloud/yandex" { + version = "0.147.0" + hashes = [ + "h1:Lqw21/fIKjYUzc+tIcy8nSa8YAbpiYpDrjBMFwbjRYQ=", + ] +} diff --git a/service/backends.tf b/service/backends.tf new file mode 100644 index 0000000..8e7fce7 --- /dev/null +++ b/service/backends.tf @@ -0,0 +1,13 @@ +terraform { + backend "s3" { + use_lockfile = true + max_retries = 5 + retry_mode = "adaptive" + + skip_region_validation = true + skip_credentials_validation = true + skip_metadata_api_check = true + skip_requesting_account_id = true + skip_s3_checksum = true + } +} diff --git a/service/main.tf b/service/main.tf new file mode 100644 index 0000000..61528a5 --- /dev/null +++ b/service/main.tf @@ -0,0 +1,15 @@ +resource "yandex_iam_service_account" "sa" { + name = "terraform-provider" + description = "Service account for Terraform" +} + +resource "yandex_resourcemanager_folder_iam_member" "sa_admin" { + folder_id = var.folder_id + role = "admin" + member = "serviceAccount:${yandex_iam_service_account.sa.id}" +} + +resource "yandex_iam_service_account_key" "sa_key" { + service_account_id = yandex_iam_service_account.sa.id + description = "Key for Terraform" +} diff --git a/service/outputs.tf b/service/outputs.tf new file mode 100644 index 0000000..d329d5e --- /dev/null +++ b/service/outputs.tf @@ -0,0 +1,11 @@ +output "service_account_key_json" { + value = jsonencode({ + id = yandex_iam_service_account_key.sa_key.id + service_account_id = yandex_iam_service_account_key.sa_key.service_account_id + created_at = yandex_iam_service_account_key.sa_key.created_at + key_algorithm = yandex_iam_service_account_key.sa_key.key_algorithm + public_key = yandex_iam_service_account_key.sa_key.public_key + private_key = yandex_iam_service_account_key.sa_key.private_key + }) + sensitive = true +} diff --git a/service/provider.tf b/service/provider.tf new file mode 100644 index 0000000..6fc66a3 --- /dev/null +++ b/service/provider.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + yandex = { + source = "yandex-cloud/yandex" + } + } + required_version = ">= 0.13" +} + +provider "yandex" { + max_retries = 3 + insecure = false +} diff --git a/service/variables.tf b/service/variables.tf new file mode 100644 index 0000000..74f7513 --- /dev/null +++ b/service/variables.tf @@ -0,0 +1 @@ +variable "folder_id" {}