Всем привет.

Есть такие люди, которые работают с облачной инфраструктурой и не используют автоматизацию, потому что это долго, нужно вникать, а им надо фичи пилить. Накликали что-то там в UI, подключились по ssh, поставили всякого с помощью apt и т.д. и конфигурационные файлы ещё вручную поменяли. Документации конечно же написать времени не хватило или в ней много разных хитрых шагов и повторить настройку этой инфраструктуры в точности уже нельзя или очень сложно, а сервисы крутятся в проде. А потом человек забыл что и как делал в точности или вообще уволился.

Хочу показать на небольшом примере, что автоматизировать инфраструктуру, например в AWS, может быть достаточно просто и приятно, а получившийся результат достаточно прозрачен и сам по себе является документацией, т.к. это инфраструктура как код. Если конечно есть знания Terraform или желание его немного изучить.

К слову, крайне рекомендую для автоматизации много чего, но в особенности облачных провайдеров вроде AWS / GCP / Azure и т.д. использовать именно Terraform, т.к. это достаточно зрелый инструмент, у него большое сообщество и кроме всего прочего он поддерживает автоматизацию далеко не только каких-то облачных провайдеров, но и практически всего у чего есть API. К тому же инструмент open source и при желании можно реализовать что угодно самостоятельно. Для таких облаков, как AWS не рекомендую пытаться реализовывать автоматизации с помощью чистого питона и запросов к AWS API с помощью cli или Cloudformation.

Также у Terraform есть удобная возможность организовывать код в блоки называемые модулями и передавая в них только параметры легко создавать необходимое с другими настройками.

Для тех, кто совсем не знаком с Terraform упомяну, что если в одну папку положить несколько файлов с расширением .tf и запустить Terraform в этой папке, то Terraform прочитает и использует код из всех файлов, а не только одного. Это в том числе позволяет разбивать один большой main.tf файл с кучей ресурсов на какие-то удобные вам логические блоки.

Итак, например, встала задача развернуть vpn серверы WireGuard на базе Ubuntu 20.04 в нескольких регионах + немного мониторинга. Поддержка WireGuard сейчас есть в ядре linux, но дополнительные инструменты, которые можно поставить отдельно облегчают жизнь, поэтому поставим и их.

Весь код модуля выложен здесь.

Также этот модуль опубликован в реестре модулей здесь.

Не буду разжёвывать код построчно, попробую описать архитектуру в целом, а код попробуйте разобрать самостоятельно. Если возникнут неразрешимые проблемы или вопросы попробую ответить.

Итак, создаются необходимые для работы iam политики, роль и т.п.

Используем elastic ip, отдельный для сервера в каждом регионе, которые нужно прописать в dns для того, чтобы пользователь мог использовать единое имя для подключения к vpn серверу. Планировал использовать geo dns route53, чтобы при местоположении пользователя в определённом регионе ему бы отдавался ip vpn сервера в его регионе, но т.к. на этом проекте route53 пока не используется, то создание записей в нём пока не автоматизировал.

Создаются security groups с правилами, которые позволяют подключиться к vpn серверу извне по udp (Wireguard работает только по udp) + ssh + несколько портов для prometheus exporter'ов.

Создаётся собственно сервер / ec2 машина, но не просто отдельно стоящая, а входящая в auto scaling group, в данном примере в единственном варианте. Это сделано для того, чтобы если с сервером что-то не так, то Амазон автоматом пересоздаст его. Self healing.

Позже немного допилив конфигурацию и добавив в неё load balancer можно добиться того, для чего auto scaling groups отлично подходят: при повышенной нагрузке на какой-то из ресурсов сервера, например на cpu, можно реализовать автоматическое создание дополнительных vpn серверов, а соответственно при падении нагрузки уменьшать их количество.

Этот модуль можно использовать просто с Terraform, но лучше использовать Terragrunt, который позволяет делать некоторые удобные вещи и местами реализовывать концепцию Keep your Terraform code DRY, например параметризуя некоторые вещи в backend блоке, чего сам Terraform пока не умеет. Terraform хранит состояние инфраструктуры в специальном файле и принято хранить его не локально, а, чаще всего, в S3 бакете. Также, если вы работаете с этим кодом не в одиночку, то принято хранить локи в Dynamodb, чтобы случайно не применить какое-то изменение инфраструктуры несогласованно и не поломать всё.

Именно пример такого использования я привожу в примере здесь.

С помощью файла terragrunt.hcl в корне репозитория (https://github.com/vainkop/terraform-aws-wireguard/blob/master/example/terragrunt.hcl) я могу, например, задать место для хранения state для всех поддиректорий, а потом ссылаться на этот файл в других terragrunt.hcl с помощью функции find_in_parent_folders() https://github.com/vainkop/terraform-aws-wireguard/blob/master/example/us-east-1/terragrunt.hcl#L2

При этом key, т.е. файл, где будет храниться состояние инфраструктуры в конкретном регионе будет храниться отдельно, что достигается с помощью функции path_relative_to_include() https://github.com/vainkop/terraform-aws-wireguard/blob/master/example/terragrunt.hcl#L11

Также я реализовал хранение/чтение переменных в yaml формате, что мне кажется более удобочитаемым с помощью функции yamldecode(file(...)) https://github.com/vainkop/terraform-aws-wireguard/blob/master/example/eu-central-1/terragrunt.hcl#L9

Вот так выглядит пример передаваемых в модуль уникальных параметров (конечно YOUR_... нужно заменить на реальные значения): https://github.com/vainkop/terraform-aws-wireguard/blob/master/example/us-east-1/values.yaml

Иногда удобно реализовать использование имени папки в качестве параметра, например в приведённом примере это мог бы быть параметр region и реализуется это с помощью, например, функций basename(get_terragrunt_dir()) и задавать его в values.yaml не пришлось бы, но по определённым причинам решил этого не делать.

В итоге в вашем приватном репозитории код из которого применяете либо вы, либо какой-то ci cd runner может лежать только содержимое похожее на мою папку example, т.е. только terragrunt.hcl и yaml файлы с параметрами, а модуль можно использовать как публичный и хорошо поддерживаемый, так и написать свой. Это позволяет отдать "пользователям" только задание параметров в yaml и в принципе ничего не знать про Terraform код.

Отдельно советую: по-возможности, не нужно изобретать велосипед и на каждое действие пытаться писать свой Terraform код или тем более модуль. Для большинства вещей модули уже написаны и в их разработке и поддержке участвует множество грамотных людей, поэтому берите и пользуйтесь.

Для того, чтобы изменения в коде open source модулей, как впрочем и в частных, не повлияли на работу вашей автоматизации принято фиксировать версии используемых модулей, например в моём коде это сделано с помощью source = "github.com/vainkop/terraform-aws-wireguard?ref=v1.2.0" здесь https://github.com/vainkop/terraform-aws-wireguard/blob/master/example/eu-central-1/terragrunt.hcl#L6

Ну а если вы всё же не хотите зависеть от open source и чувствуете в себе силы поддерживать и развивать свой модуль самостоятельно, всегда можно форкнуть общественный и сделать всё, что хочется.

Например я реализовал cloud-init скрипт, который осуществляет предварительную установку и настройку софта на свежеразвёрнутый сервер и делает это каждый раз, когда сервер пересоздаётся в auto scaling group, что очень удобно: https://github.com/vainkop/terraform-aws-wireguard/blob/master/templates/user-data.txt

Ближе к концу скрипта устанавливается 2 prometheus exporter'а, которые позволяют как мониторить метрики самой ec2 машины, так и базовые метрики самого WireGuard, на основании которых можно построить удобные Dashboards и соответственно определённые alerts и т.п.

В частности я реализовал это для того, чтобы видеть к какому из vpn серверов подключён клиент, чтобы, например, была возможность подключиться к нему именно из его региона, т.к. связности между этими vpn серверами нет. Т.к. клиентские публичные ключи зашиты в каждый из серверов и серверные ключи одинаковые, то клиент будет автоматически переключаться между ними путешествуя между регионами на основании geo ip route53.

Также привожу пример кода из .gitlab-ci.yml и Dockerfile где можно увидеть какие команды используются для применения всего этого хозяйства с помощью Gitlab runner'а и какой docker контейнер можно использовать для этого runner'а.

$ cat .gitlab-ci.yml

stages:
  - build
  - plan
  - apply
  - destroy

variables:
  GIT_DEPTH: 1

.aws_configure: &aws_configure
  before_script:
    - aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
    - aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
    - aws configure set default.region $AWS_DEFAULT_REGION


build-terraform:
  image: docker:19.03.15
  services:
    - docker:19.03.15-dind
  stage: build
  variables:
    DOCKER_TLS_CERTDIR: ""
    DOCKER_HOST: tcp://docker:2375
    DOCKER_DRIVER: overlay2
    TERRAFORM_VERSION: "0.13.6"
    TERRAGRUNT_VERSION: "v0.28.9"
  before_script:
    - printenv
    - docker info
    - echo $CI_REGISTRY_PASSWORD | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
  script:
    - cd docker
    - docker build --build-arg TERRAFORM_VERSION=$TERRAFORM_VERSION --build-arg TERRAGRUNT_VERSION=$TERRAGRUNT_VERSION -t $CI_REGISTRY_IMAGE:$TERRAFORM_VERSION .
    - docker push $CI_REGISTRY_IMAGE:$TERRAFORM_VERSION
  rules:
    - changes:
        - docker/*


plan-us-east-1:
  image:
    name: registry.gitlab.com/vainkop/terraform:0.13.6
    entrypoint: [""]
  stage: plan
  <<: *aws_configure
  script:
    - cd wireguard/us-east-1
    - terragrunt run-all plan --terragrunt-non-interactive -out $CI_PROJECT_DIR/wireguard/us-east-1/tfplan-$CI_COMMIT_SHA
  artifacts:
    paths:
    - $CI_PROJECT_DIR/wireguard/us-east-1/tfplan-$CI_COMMIT_SHA
    expire_in: 1 month
  rules:
    - changes:
        - wireguard/us-east-1/*
      allow_failure: true

plan-eu-central-1:
  image:
    name: registry.gitlab.com/vainkop/terraform:0.13.6
    entrypoint: [""]
  stage: plan
  <<: *aws_configure
  script:
    - cd wireguard/eu-central-1
    - terragrunt run-all plan --terragrunt-non-interactive -out $CI_PROJECT_DIR/wireguard/eu-central-1/tfplan-$CI_COMMIT_SHA
  artifacts:
    paths:
    - $CI_PROJECT_DIR/wireguard/eu-central-1/tfplan-$CI_COMMIT_SHA
    expire_in: 1 month
  rules:
    - changes:
        - wireguard/eu-central-1/*
      allow_failure: true

apply-us-east-1:
  image:
    name: registry.gitlab.com/vainkop/terraform:0.13.6
    entrypoint: [""]
  stage: apply
  <<: *aws_configure
  script:
    - cd wireguard/us-east-1
    - terragrunt run-all apply --terragrunt-non-interactive -auto-approve $CI_PROJECT_DIR/wireguard/us-east-1/tfplan-$CI_COMMIT_SHA
  rules:
    - changes:
        - wireguard/us-east-1/*
      when: manual
      allow_failure: true

apply-eu-central-1:
  image:
    name: registry.gitlab.com/vainkop/terraform:0.13.6
    entrypoint: [""]
  stage: apply
  <<: *aws_configure
  script:
    - cd wireguard/eu-central-1
    - terragrunt run-all apply --terragrunt-non-interactive -auto-approve $CI_PROJECT_DIR/wireguard/eu-central-1/tfplan-$CI_COMMIT_SHA
  rules:
    - changes:
        - wireguard/eu-central-1/*
      when: manual
      allow_failure: true

destroy-us-east-1:
  image:
    name: registry.gitlab.com/vainkop/terraform:0.13.6
    entrypoint: [""]
  stage: destroy
  <<: *aws_configure
  script:
    - cd wireguard/us-east-1
    - terragrunt run-all destroy --terragrunt-non-interactive -auto-approve
  rules:
    - changes:
        - wireguard/us-east-1/*
      when: manual
      allow_failure: true

destroy-eu-central-1:
  image:
    name: registry.gitlab.com/vainkop/terraform:0.13.6
    entrypoint: [""]
  stage: destroy
  <<: *aws_configure
  script:
    - cd wireguard/eu-central-1
    - terragrunt run-all destroy --terragrunt-non-interactive -auto-approve
  rules:
    - changes:
        - wireguard/eu-central-1/*
      when: manual
      allow_failure: true
$ cat docker/Dockerfile

FROM ubuntu:20.04

USER root

ARG DEBIAN_FRONTEND=noninteractive

ARG TERRAFORM_VERSION
ENV TERRAFORM_VERSION=$TERRAFORM_VERSION

ARG TERRAGRUNT_VERSION
ENV TERRAGRUNT_VERSION=$TERRAGRUNT_VERSION

RUN set -x &&     apt-get update &&     apt-get install -y     apt-transport-https     ca-certificates     software-properties-common     unzip     net-tools     wget     curl     python3     python3-pip     jq     gettext-base     git &&     rm -rf /var/lib/apt/lists/*

RUN set -x &&     apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CC86BB64 &&     add-apt-repository ppa:rmescandon/yq &&     apt update &&     apt install -y yq &&     rm -rf /var/lib/apt/lists/*

RUN set -x &&     pip3 install -U --no-cache-dir setuptools shyaml

RUN set -x &&     ln -sf /usr/bin/python3 /usr/bin/python && ln -sf /usr/bin/pip3 /usr/bin/pip

RUN set -x &&     curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" &&     unzip awscliv2.zip &&     ./aws/install &&     rm -rf awscliv2.zip &&     /aws     /usr/local/aws-cli/v2/*/dist/awscli/examples

RUN set -x &&     cd /tmp &&     curl -O https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip &&     unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip -d /usr/local/bin &&     chmod +x /usr/local/bin/terraform &&     rm /tmp/terraform_${TERRAFORM_VERSION}_linux_amd64.zip

RUN set -x &&     wget "https://github.com/gruntwork-io/terragrunt/releases/download/${TERRAGRUNT_VERSION}/terragrunt_linux_amd64" &&     mv terragrunt_linux_amd64 /usr/local/bin/terragrunt &&     chmod +x /usr/local/bin/terragrunt

RUN set -x &&     curl --version &&     envsubst --version &&     python --version &&     pip --version &&     shyaml --version &&     jq -V &&     yq -V &&     aws --version &&     terraform --version &&     terragrunt --version

ENTRYPOINT ["/bin/bash", "-c"]

За код не ругайте, написал за несколько часов и решил поделиться.

Если есть конкретные замечания/предложения, то готов их выслушать либо в комментариях, либо в личке, например, в телеграм: @vainkop

Прошу также учитывать, что это моя первая публикация на Хабре.