# Install WireGuard with Gitlab and Terraform

<https://habr.com/ru/articles/547298/>

Всем привет.

Есть такие люди, которые работают с облачной инфраструктурой и не используют автоматизацию, потому что это долго, нужно вникать, а им надо фичи пилить. Накликали что-то там в 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, но дополнительные инструменты, которые можно поставить отдельно облегчают жизнь, поэтому поставим и их.

Весь код модуля [выложен](https://github.com/vainkop/terraform-aws-wireguard) [здесь](https://github.com/vainkop/terraform-aws-wireguard).

Также этот модуль опубликован в реестре модулей [здесь](https://registry.terraform.io/modules/vainkop/wireguard/aws/latest).

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

Итак, создаются необходимые для работы 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"]
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://book.konstantinsecurity.com/readme/architect/ci-cd/install-wireguard-with-gitlab-and-terraform.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
