commit 24e658b5abcd903effaeb645368fb4ae1ffce9c8 Author: Claude Sonnet 5 Date: Wed Aug 12 19:23:55 2026 +0000 Restore from Gitea ZIP snapshot (12.08.2026) after full instance reinstall Git history was lost when the previous Gitea instance was wiped and reinstalled due to an unresolved corruption bug — this commit is the last known-good file content, exported before the reinstall. Prior commit history is not recoverable through this path. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e18809d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM semaphoreui/semaphore:latest + +USER root + +# Установка kubectl (latest stable на 2025) +RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \ + chmod +x kubectl && \ + mv kubectl /usr/local/bin/kubectl && \ + kubectl version --client + +USER semaphore diff --git a/ansible/change_password.yml b/ansible/change_password.yml new file mode 100644 index 0000000..eeda1a5 --- /dev/null +++ b/ansible/change_password.yml @@ -0,0 +1,18 @@ +--- +- name: Utility - Change User Password + hosts: masters,workers + gather_facts: false + vars: + # Переменная requested_password придет из Survey в Semaphore + target_user: "ubuntu" + tasks: + - name: Update password for {{ target_user }} using chpasswd + become: true + # Мы передаем пароль через стандартный поток ввода (stdin) прямо в системную утилиту + shell: "echo '{{ target_user }}:{{ requested_password }}' | chpasswd" + # Чтобы пароль не светился в логах Semaphore даже в режиме Debug + no_log: true + + - name: Confirm update + debug: + msg: "Password for {{ target_user }} has been updated successfully." \ No newline at end of file diff --git a/ansible/deploy_all.yml b/ansible/deploy_all.yml new file mode 100644 index 0000000..2bd4ea3 --- /dev/null +++ b/ansible/deploy_all.yml @@ -0,0 +1,75 @@ +--- +- name: Stage 1 - Terraform Infrastructure + hosts: localhost + connection: local + gather_facts: false + become: false + vars: + tf_dir: "{{ playbook_dir }}/../terraform" + tasks: + - name: Create terraform mirror config + copy: + dest: "/tmp/.terraformrc" + content: | + provider_installation { + network_mirror { url = "https://terraform-mirror.yandexcloud.net/" } + direct { exclude = ["registry.terraform.io/*/*"] } + } + + - name: Cleanup old terraform files + shell: "rm -rf .terraform .terraform.lock.hcl" + args: + chdir: "{{ tf_dir }}" + + - name: Terraform Init and Apply + shell: | + terraform init -reconfigure -no-color && \ + terraform apply -auto-approve -no-color -lock=false + args: + chdir: "{{ tf_dir }}" + register: tf_result + ignore_errors: true + environment: + TF_CLI_CONFIG_FILE: "/tmp/.terraformrc" + TF_HTTP_ADDRESS: "{{ lookup('env', 'TF_HTTP_ADDRESS') }}" + TF_HTTP_USERNAME: "{{ lookup('env', 'TF_HTTP_USERNAME') }}" + TF_HTTP_PASSWORD: "{{ lookup('env', 'TF_HTTP_PASSWORD') }}" + TF_VAR_proxmox_api_token_id: "{{ lookup('env', 'TF_VAR_proxmox_api_token_id') }}" + TF_VAR_proxmox_api_token_secret: "{{ lookup('env', 'TF_VAR_proxmox_api_token_secret') }}" + TF_VAR_proxmox_api_url: "{{ lookup('env', 'TF_VAR_proxmox_api_url') }}" + + - name: Add Master to memory + add_host: + name: "k8s-master" + groups: ["masters_group", "k8s_nodes"] + ansible_host: "10.33.33.201" + ansible_user: "ubuntu" + ansible_ssh_extra_args: "-o StrictHostKeyChecking=no" + + - name: Add Workers to memory + add_host: + name: "{{ item.name }}" + groups: ["workers_group", "k8s_nodes"] + ansible_host: "{{ item.ip }}" + ansible_user: "ubuntu" + ansible_ssh_extra_args: "-o StrictHostKeyChecking=no" + loop: + - { name: 'k8s-worker-1', ip: '10.33.33.202' } + - { name: 'k8s-worker-2', ip: '10.33.33.203' } + +- name: Stage 2 - Wait for SSH + hosts: k8s_nodes + gather_facts: false + tasks: + - name: Wait for connection + wait_for_connection: + timeout: 300 + +- name: Stage 3 - Install K8s + import_playbook: change_password.yml + +- name: Stage 4 - Install K8s + import_playbook: k8s_setup.yml + +- name: Stage 5 - Final Config + import_playbook: k8s_post_install.yml \ No newline at end of file diff --git a/ansible/deploy_awx_k8s.yml b/ansible/deploy_awx_k8s.yml new file mode 100644 index 0000000..8e52f98 --- /dev/null +++ b/ansible/deploy_awx_k8s.yml @@ -0,0 +1,124 @@ +--- +- name: Deploy latest stable AWX using AWX Operator on Kubernetes + hosts: localhost + connection: local + become: false + gather_facts: false + + vars: + # Основные параметры — переопределяй в Semaphore Variable Group + awx_namespace: awx + awx_instance_name: awx + awx_service_type: NodePort # NodePort / ClusterIP / LoadBalancer + awx_operator_version: 2.19.1 # Последняя стабильная на декабрь 2025 + kubeconfig_path: "/home/semaphore/.kube/config" + awx_storage_class: local-path # Предполагаем k3s или аналогичный кластер с local-path provisioner. Изменить на свой SC + awx_projects_persistence: false # Отключаем persistence для projects для теста (чтобы избежать PVC проблем) + awx_projects_storage_size: 8Gi # Если persistence: true + + tasks: + - name: Fail if kubeconfig not found inside container + ansible.builtin.stat: + path: "{{ kubeconfig_path }}" + register: kubeconfig_stat + failed_when: not kubeconfig_stat.stat.exists + delegate_to: localhost + + - name: Create namespace for AWX + kubernetes.core.k8s: + state: present + kubeconfig: "{{ kubeconfig_path }}" + definition: + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ awx_namespace }}" + + - name: Apply AWX Operator from GitHub kustomize + ansible.builtin.command: + cmd: >- + kubectl apply -k "github.com/ansible/awx-operator/config/default?ref={{ awx_operator_version }}" + environment: + KUBECONFIG: "{{ kubeconfig_path }}" + changed_when: true + register: operator_apply + failed_when: operator_apply.rc != 0 and 'already exists' not in operator_apply.stderr | default('') + + - name: Wait for AWX Operator to be ready + kubernetes.core.k8s_info: + kubeconfig: "{{ kubeconfig_path }}" + api_version: apps/v1 + kind: Deployment + name: awx-operator-controller-manager + namespace: "{{ awx_namespace }}" + register: operator_status + until: >- + operator_status.resources | length > 0 and + operator_status.resources[0].status.readyReplicas is defined and + operator_status.resources[0].status.readyReplicas >= 1 + retries: 40 + delay: 15 + + - name: Ensure default StorageClass for persistence (assume local-path for k3s-like clusters) + kubernetes.core.k8s: + state: present + kubeconfig: "{{ kubeconfig_path }}" + definition: + apiVersion: storage.k8s.io/v1 + kind: StorageClass + metadata: + name: "{{ awx_storage_class }}" + annotations: + storageclass.kubernetes.io/is-default-class: "true" + provisioner: rancher.io/local-path # Для k3s; изменить на свой provisioner (e.g., kubernetes.io/no-provisioner) + reclaimPolicy: Delete + volumeBindingMode: WaitForFirstConsumer # Избежать immediate bind ошибок на multi-node + + - name: Deploy AWX instance + kubernetes.core.k8s: + state: present + kubeconfig: "{{ kubeconfig_path }}" + definition: + apiVersion: awx.ansible.com/v1beta1 + kind: AWX + metadata: + name: "{{ awx_instance_name }}" + namespace: "{{ awx_namespace }}" + spec: + service_type: "{{ awx_service_type }}" + postgres_storage_class: "{{ awx_storage_class }}" # Указываем SC для postgres PVC + projects_persistence: "{{ awx_projects_persistence }}" # false для теста, чтобы избежать дополнительного PVC + projects_storage_class: "{{ awx_storage_class }}" # Если persistence: true + projects_storage_size: "{{ awx_projects_storage_size }}" + + - name: Wait for AWX pods to be running (increased retries for slow storage provisioning) + kubernetes.core.k8s_info: + kubeconfig: "{{ kubeconfig_path }}" + kind: Pod + namespace: "{{ awx_namespace }}" + label_selectors: + - "app.kubernetes.io/managed-by=awx-operator" + register: awx_pods + until: >- + awx_pods.resources | selectattr('status.phase', 'equalto', 'Running') | list | length >= 2 + retries: 90 # Увеличено для ожидания provisioning PVC/PV + delay: 20 + + - name: Retrieve AWX admin password + kubernetes.core.k8s_info: + kubeconfig: "{{ kubeconfig_path }}" + api_version: v1 + kind: Secret + name: "{{ awx_instance_name }}-admin-password" + namespace: "{{ awx_namespace }}" + register: awx_secret + + - name: Display AWX login information + ansible.builtin.debug: + msg: | + AWX deployed successfully! + Access URL: http://: + (get port: kubectl get svc {{ awx_instance_name }}-service -n {{ awx_namespace }}) + Username: admin + Password: {{ awx_secret.resources[0].data.password | b64decode }} + Note: If persistence issues persist, ensure your cluster has a working provisioner (e.g., local-path in k3s) and default StorageClass set. \ No newline at end of file diff --git a/ansible/inventory.ini b/ansible/inventory.ini new file mode 100644 index 0000000..4f47839 --- /dev/null +++ b/ansible/inventory.ini @@ -0,0 +1,10 @@ +[masters] +10.33.33.201 node_name=master-01 + +[workers] +10.33.33.202 node_name=worker-01 +10.33.33.203 node_name=worker-02 + +[all:vars] +ansible_user=ubuntu +ansible_ssh_private_key_file=/tmp/semaphore/keys/your_key_id \ No newline at end of file diff --git a/ansible/k8s_post_install.yml b/ansible/k8s_post_install.yml new file mode 100644 index 0000000..118763d --- /dev/null +++ b/ansible/k8s_post_install.yml @@ -0,0 +1,50 @@ +--- +- name: CNI Fix + hosts: k8s_nodes + become: true + tasks: + - name: 1. Исправление сетевых путей (Все плагины сразу) + shell: | + mkdir -p /usr/lib/cni + ln -s /opt/cni/bin/cilium-cni /usr/lib/cni/cilium-cni + changed_when: true + + - name: 2. Перезапуск Kubelet + shell: systemctl restart kubelet + +- name: Cluster Resources + hosts: masters_group + become: false + tasks: + - name: Install Storage + shell: kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml + + - name: Install MetalLB + shell: kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml + + - name: Wait for MetalLB + shell: kubectl wait --namespace metallb-system --for=condition=ready pod -l app=metallb,component=controller --timeout=120s + ignore_errors: true + + - name: Fix Webhook + shell: kubectl delete validatingwebhookconfiguration metallb-webhook-configuration + ignore_errors: true + + - name: Config IP Pool + shell: | + cat </dev/null 2>&1; do sleep 5; done;" + changed_when: false + + - name: 1. Установка системных зависимостей + apt: + update_cache: yes + name: [apt-transport-https, ca-certificates, curl, gnupg, qemu-guest-agent, socat, conntrack] + state: present + register: apt_res + until: apt_res is success + retries: 20 + delay: 10 + + - name: 2. Настройка модулей и sysctl + shell: | + modprobe overlay && modprobe br_netfilter + echo -e "overlay\nbr_netfilter" > /etc/modules-load.d/k8s.conf + cat < /etc/sysctl.d/k8s.conf + net.bridge.bridge-nf-call-iptables = 1 + net.bridge.bridge-nf-call-ip6tables = 1 + net.ipv4.ip_forward = 1 + EOF + sysctl --system + changed_when: false + + - name: 3. Установка Containerd + apt: + name: containerd + state: present + + - name: 4. Конфигурация Containerd + shell: | + mkdir -p /etc/containerd + containerd config default > /etc/containerd/config.toml + sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml + systemctl restart containerd + changed_when: true + + - name: 5. Добавление репозитория Kubernetes (Tsinghua) + shell: | + curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.32/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg --yes + echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg arch=amd64] https://mirrors.tuna.tsinghua.edu.cn/kubernetes/core:/stable:/v1.32/deb/ /" > /etc/apt/sources.list.d/kubernetes.list + + - name: 6. Установка пакетов Kubernetes + apt: + name: [kubelet, kubeadm, kubectl] + state: present + update_cache: yes + register: k8s_apt + until: k8s_apt is success + retries: 15 + delay: 10 + +- name: Инициализация Master + hosts: masters_group + become: true + tasks: + - name: Kubeadm Init + shell: "kubeadm init --pod-network-cidr=10.244.0.0/16 --skip-phases=addon/kube-proxy" + args: + creates: /etc/kubernetes/admin.conf + + - name: Config for ubuntu user + shell: | + mkdir -p /home/ubuntu/.kube + cp -f /etc/kubernetes/admin.conf /home/ubuntu/.kube/config + chown ubuntu:ubuntu /home/ubuntu/.kube/config + + - name: Generate Join Command + shell: "kubeadm token create --print-join-command" + register: join_cmd + +- name: Join Workers + hosts: workers_group + become: true + tasks: + - name: Join to cluster + shell: "{{ hostvars['k8s-master']['join_cmd']['stdout'] }}" + args: + creates: /etc/kubernetes/kubelet.conf + +- name: Установка Cilium CNI (Слой сети) + hosts: masters_group + become: true + tasks: + - name: Скачивание Cilium CLI + get_url: + url: https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz + dest: /tmp/cilium.tar.gz + register: cilium_dl + until: cilium_dl is success + retries: 5 + delay: 10 + + - name: Распаковка Cilium CLI + shell: tar xzvf /tmp/cilium.tar.gz -C /usr/local/bin + args: + creates: /usr/local/bin/cilium + + - name: Установка Cilium в кластер + # Выполняем как пользователь ubuntu, чтобы иметь доступ к кубеконфигу + become: true + become_user: ubuntu + shell: /usr/local/bin/cilium install --set kubeProxyReplacement=true + ignore_errors: true \ No newline at end of file diff --git a/ansible/templates/ippool.j2 b/ansible/templates/ippool.j2 new file mode 100644 index 0000000..7af194f --- /dev/null +++ b/ansible/templates/ippool.j2 @@ -0,0 +1,14 @@ +apiVersion: metallb.io/v1beta1 +kind: IPAddressPool +metadata: + name: main-pool + namespace: metallb-system +spec: + addresses: + - {{ metallb_ip_range }} +--- +apiVersion: metallb.io/v1beta1 +kind: L2Advertisement +metadata: + name: l2-adv + namespace: metallb-system \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a44f069 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,53 @@ +version: '3.8' + +services: + mysql: + image: mysql:8.0 + restart: unless-stopped + environment: + MYSQL_RANDOM_ROOT_PASSWORD: "yes" + MYSQL_DATABASE: semaphore + MYSQL_USER: semaphore + MYSQL_PASSWORD: pass + volumes: + - mysql-data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + + semaphore: + build: + context: . + dockerfile: Dockerfile + image: semaphoreui/semaphore:latest + restart: unless-stopped + ports: + - "5555:3000" + environment: + SEMAPHORE_DB_USER: semaphore + SEMAPHORE_DB_PASS: pass + SEMAPHORE_DB_HOST: mysql + SEMAPHORE_DB_PORT: 3306 + SEMAPHORE_DB_DIALECT: mysql + SEMAPHORE_DB: semaphore + SEMAPHORE_PLAYBOOK_PATH: /tmp/semaphore/ + SEMAPHORE_ADMIN: admin + SEMAPHORE_ADMIN_PASSWORD: pass + SEMAPHORE_ADMIN_NAME: Admin + SEMAPHORE_ADMIN_EMAIL: 1@top-sysops.ru + SEMAPHORE_ACCESS_KEY_ENCRYPTION: "your-key=" + volumes: + - ./requirements.txt:/etc/semaphore/requirements.txt:ro + - semaphore-config:/etc/semaphore + - semaphore-projects:/projects + - ~/semaphore/config:/home/semaphore/.kube/config + depends_on: + mysql: + condition: service_healthy + +volumes: + mysql-data: + semaphore-config: + semaphore-projects: \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..34a1cab --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +kubernetes>=25.3.0 \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..45c4cf5 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,63 @@ +terraform { + required_providers { + proxmox = { + source = "bpg/proxmox" + version = "0.90.0" + } + } + backend "http" {} +} + +provider "proxmox" { + endpoint = var.proxmox_api_url + api_token = "${var.proxmox_api_token_id}=${var.proxmox_api_token_secret}" + insecure = true + ssh { + agent = false + } +} + +resource "proxmox_virtual_environment_vm" "k8s_nodes" { + for_each = var.vm_nodes + node_name = "pve-main" + + name = each.value.name + vm_id = each.value.id + + clone { + vm_id = 9000 + } + + cpu { + cores = 2 + type = "x86-64-v2-AES" + } + + memory { + dedicated = 4096 + } + + # ИСПРАВЛЕНО: Было network_interface, теперь network_device + network_device { + bridge = "vmbr0" + } + + disk { + datastore_id = "local-lvm" + interface = "scsi0" + size = 20 + } + + initialization { + ip_config { + ipv4 { + address = "${each.value.ip}/24" + gateway = "10.33.33.1" + } + } + user_account { + username = "ubuntu" + keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEEuaCL9fPnGCH4hfxAzzS09aRaj6ptoG685p+oF5vTp semaphore-ansible-key"] + } + } +} \ No newline at end of file diff --git a/terraform/output.tf b/terraform/output.tf new file mode 100644 index 0000000..592eb9d --- /dev/null +++ b/terraform/output.tf @@ -0,0 +1,3 @@ +output "vm_ips" { + value = { for k, v in var.vm_nodes : k => v.ip } +} \ No newline at end of file diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..0dc3160 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,16 @@ +variable "proxmox_api_url" { type = string } +variable "proxmox_api_token_id" { type = string } +variable "proxmox_api_token_secret" { type = string } + +variable "vm_nodes" { + type = map(object({ + id = number + ip = string + name = string + })) + default = { + "master" = { id = 201, ip = "10.33.33.201", name = "k8s-master" } + "worker1" = { id = 202, ip = "10.33.33.202", name = "k8s-worker-1" } + "worker2" = { id = 203, ip = "10.33.33.203", name = "k8s-worker-2" } + } +} \ No newline at end of file