Организация доступа по SSH к виртуальным машинам Yandex Cloud

yandex cloud ssh ansible

В чём, собственно, проблема зайти на виртуальную машину в облаке по SSH?

Во-первых чтобы зайти на виртуальную машину по SSH, ни о чём не думая, нужно держать на ней белый адрес IP, за который, по естественным в наше время причинам, облачные провайдеры просят отдельных денег. Которые при массовом использовании становятся заметными.

Во-вторых, если мы плюнули и потратились на внешние адреса для всех виртуалок, к которым нужен доступ по SSH, мы оказываемся должны ещё плюнуть и потратиться на организацию и поддержку файрволла для защиты сервисов, установленных на этих виртуалках.

Традиционно обозначенные проблемы решаются при помощи т.н. хоста-бастиона. Заводится хост, на который есть доступ из внешней сети, с которого, в свою очередь, есть доступ во внутреннюю сеть. Ко всем остальным хостам есть доступ только из внутренней сети. Оператор заходит по SSH на бастион и оттуда, уже имея доступ к внутренним адресам, на конечную машину.

В этом подходе вопросы может вызвать задача сопоставления имени внутренней машины её адресу IP. Традиционно заводится сервер DNS с поддержкой внутренней зоны. При разумном количестве статических адресов соответствие можно прописывать прямо при помощи ssh_config. Как мы суть облачные жители, для определения адресов воспользуемся API облака.

Предполагаем, что в каждом каталоге уже создана виртуальная машина под названием bastion-0 и на всех виртуальных машинах облака созданы учётные записи оператора с ключами ssh.

Заведём профиль для каждого каталога нашего облака и настроим SSH так, чтобы на виртуальную машину можно было попасть по псевдоадресу <instance-name>.<profile>.

Для настройки профиля можно запустить yc init и далее идти по диалогам:

> yc init
Welcome! This command will take you through the configuration process.
Pick desired action:
 [1] Re-initialize this profile 'default' with new settings
 [2] Create a new profile
Please enter your numeric choice: 2
Enter profile name. Names start with a lower case letter and contain only lower case letters a-z, digits 0-9, and hyphens '-': prod
Please go to https://oauth.yandex.ru/authorize?response_type=token&client_id=QMbwL7XPUHpyRdEAwr9o2RWVmuc4dyf7 in order to obtain OAuth token.

Please enter OAuth token: zqHooQukL47xAGXL7od68jYnWLGVZOs4q_F0XQ4
You have one cloud available: 'mycloyd' (id = j1rhzWJevebiwI9x40fe). It is going to be used by default.
Please choose folder to use:
 [1] prod (id = zss8DfD5hWRGvmSeZTcq)
 [2] dev (id = guLhrfu4uPfUtQeynPUp)
Please enter your numeric choice: 1
Your current folder has been set to 'prod' (id = zss8DfD5hWRGvmSeZTcq).
Do you want to configure a default Compute zone? [Y/n] n
>

И так для каждого каталога. Если уже известен токен OAuth, возможно, удобнее будет просто отредактировать файл ~/.config/yandex-cloud/config.yaml:

current: dev
profiles:
  prod:
    token: zqHooQukL47xAGXL7od68jYnWLGVZOs4q_F0XQ4
    cloud-id: j1rhzWJevebiwI9x40fe
    folder-id: zss8DfD5hWRGvmSeZTcq
  dev:
    token: zqHooQukL47xAGXL7od68jYnWLGVZOs4q_F0XQ4
    cloud-id: j1rhzWJevebiwI9x40fe
    folder-id: guLhrfu4uPfUtQeynPUp

Для профилей заведём в ~/.ssh/config записи:

Host *.prod
    ProxyCommand yc_ssh_proxy %h %p bastion-0
Host *.dev
    ProxyCommand yc_ssh_proxy %h %p bastion-0

Основной скрипт yc_ssh_proxy разместим по пути, перечисленному в $PATH (мне удобно держать в ~/bin/). Для работы скрипта понадобится python. Так получилось.

#!/bin/sh
#
# Usage: yc_ssh_proxy host.env port bastion
#

HOST=$(echo $1 | cut -d. -f1)
PROFILE=$(echo $1 | cut -d. -f2)

instance() { yc --profile $PROFILE compute instance get $1 --format json; }
prop() { cat | python -c "import json;import sys;print(json.load(sys.stdin)['network_interfaces'][0]['primary_v4_address']$1)"; }

ADDR=$(instance $HOST | prop '["address"]')
BASTION=$(instance $3 | prop '["one_to_one_nat"]["address"]')

exec ssh -W $ADDR:$2 -q $BASTION

Проверим доступность машин облака по ssh:

> yc --profile prod compute instance list
+----------------------+---------------------------+---------------+---------+---------------+-------------+
|          ID          |           NAME            |    ZONE ID    | STATUS  |  EXTERNAL IP  | INTERNAL IP |
+----------------------+---------------------------+---------------+---------+---------------+-------------+
| yisv06r81u30jn925rhp | ut6ikj8lxmgmo4ikmymt-bl7c | ru-central1-a | RUNNING |               | 10.2.0.23   |
| fnyxiigmjy547fmr19l9 | bastion-0                 | ru-central1-a | RUNNING | 152.2.34.233  | 10.4.0.36   |
+----------------------+---------------------------+---------------+---------+---------------+-------------+

> ssh ut6ikj8lxmgmo4ikmymt-bl7c.prod
Welcome to Ubuntu 18.04.3 LTS (GNU/Linux 4.15.0-55-generic x86_64)
 . . .
admin@ut6ikj8lxmgmo4ikmymt-bl7c:~$

Замечу, что при таком подходе статический адрес бастиону не требуется, он будет запрашиваться по Cloud API. Прерываемой же машину делать не стоит, замучаешься адреса подтверждать.

p.s.

Недавно снова нашёл потерянный репозиторий с плагином ansible inventory для облака Яндекс. На радостях добавлю пример настройки inventory для доступа к нодам кластера Managed Kubernetes.

Пока Яндекс определяется, как распространять плагин, положим его в ~/.ansible/plugins/inventory:

mkdir -p ~/.ansible/plugins/inventory
curl https://raw.githubusercontent.com/st8f/community.general/yc_compute/plugins/inventory/yc_compute.py > ~/.ansible/plugins/inventory/yc_compute.py

Inventory запишем в файл yc_compute.yml:

plugin: community.general.yc_compute
folders:
  - zss8DfD5hWRGvmSeZTcq
filters:
  - labels['managed-kubernetes-cluster-id'] == 'catsv06r81u30jn925rh'
auth_kind: oauth
hostnames:
  - "{{ name }}.prod"
keyed_groups:
  - key: labels['managed-kubernetes-node-group-id']
    prefix: node_group

Вся соль здесь в ключе hostnames. Там каждой машине назначается имя, которое распознаёт настроенный нами клиент ssh. filters отбирает только машины заданного кластера (см. yc managed-kubernetes cluster list), а keyed_groups группирует их по ID групп инстансов. Связь проверим командой

> ansible all -i yc_compute.yml -m ping -o
ut6ikj8lxmgmo4ikmymt-bl7c.prod | SUCCESS => {"changed": false,"ping": "pong"}