Доброго времени суток. Очередная заметка из моего опыта. В этот раз поверхностно о базовой инфраструктуре, которую использую, если надо что-то выгрузить, а рядом нет devOps ребят. Но текущий уровень абстракции, в технологиях, позволяет уже около года жить с этой инфраструктурой, поднятой за ночь, используя интернет и готовые вещи.
Ключевые слова — AWS + Terraform + kops . Если это полезно мне — возможно будет полезно кому-нибудь еще. Добро пожаловать в комментарии.
-1. То, с чем имеем дело
Классическая ситуация — проект написан до такой стадии, когда его необходимо куда-то выгружать и начинать использовать. И проект сложней, чем простая html страница. Хотелось бы возможности горизонтального масштабирования, идентичности окружения на локальных, тестовых, прод стендах и более-менее нормальный процесс деплоя.
Речь пойдет о приложение на Laravel, чтобы показать от начала и до конца весь процесс. Но аналогичным образом можно деплоить россыпь сервисов на go, python-приложения, небольшие сайты на WP, html-страницы и много всего. До какого-то уровня этого достаточно, а затем уже и в команде появляется отдельный человек, который улучшит и дополнит.Последнее время я пришел к тому, что на локальных машинах устанавливаю GoLand, PhpStorm, Docker, Git и полностью готов к работе. Да и управлять с одной машины вы можете россыпью кластеров, поэтому весь процесс буду описывать без учета ОС, на которой работаете, упаковывая все вещи в докер контейнер.
0. Готовимся к работе.
Давайте представим, что мы уже зарегистрировали аккаунт на AWS, попросили через техподдержку увеличить лимиты аккаунта на количество одновременно запущенных серверов, создали IAM пользователя и теперь у нас есть Access Key + Secret Key. Зона — us-east-1.
Что нам понадобится на локальном компьютере? AWS CLI, Terraform для декларативного управления AWS, kubectl, kops для настройки кластера и Helm, для развертывания некоторых сервисов. Собираем Dockerfile (который я давно нашел где-то на просторах гитхаба, но не могу найти где). Пишем свой docker-compose.yml для маунта директорий и Makefile для алиасов.
Dockerfile
FROM ubuntu:16.04
ARG AWSCLI_VERSION=1.12.1
ARG HELM_VERSION=2.8.2
ARG ISTIO_VERSION=0.6.0
ARG KOPS_VERSION=1.9.0
ARG KUBECTL_VERSION=1.10.1
ARG TERRAFORM_VERSION=0.11.0
# Install generally useful things
RUN apt-get update \
&& apt-get -y --force-yes install --no-install-recommends \
curl \
dnsutils \
git \
jq \
net-tools \
ssh \
telnet \
unzip \
vim \
wget \
&& apt-get clean \
&& apt-get autoclean \
&& apt-get autoremove \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Install AWS CLI
RUN apt-get update \
&& apt-get -y --force-yes install \
python-pip \
&& pip install awscli==${AWSCLI_VERSION} \
&& apt-get clean \
&& apt-get autoclean \
&& apt-get autoremove \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Install Terraform
RUN wget -O terraform.zip https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip \
&& unzip terraform.zip \
&& mv terraform /usr/local/bin/terraform \
&& chmod +x /usr/local/bin/terraform \
&& rm terraform.zip
# Install kubectl
ADD https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl /usr/local/bin/kubectl
RUN chmod +x /usr/local/bin/kubectl
# Install Kops
ADD https://github.com/kubernetes/kops/releases/download/${KOPS_VERSION}/kops-linux-amd64 /usr/local/bin/kops
RUN chmod +x /usr/local/bin/kops
# Install Helm
RUN wget -O helm.tar.gz https://storage.googleapis.com/kubernetes-helm/helm-v${HELM_VERSION}-linux-amd64.tar.gz \
&& tar xfz helm.tar.gz \
&& mv linux-amd64/helm /usr/local/bin/helm \
&& chmod +x /usr/local/bin/helm \
&& rm -Rf linux-amd64 \
&& rm helm.tar.gz
# Create default user "kops"
RUN useradd -ms /bin/bash kops
WORKDIR /home/kops
USER kops
# Ensure the prompt doesn't break if we don't mount the ~/.kube directory
RUN mkdir /home/kops/.kube \
&& touch /home/kops/.kube/config
docker-compose.yml
version: '2.1'
services:
cluster-main:
container_name: cluster.com
image: cluster.com
user: root
stdin_open: true
volumes:
- ./data:/data
- ./.ssh:/root/.ssh
- ./.kube:/root/.kube
- ./.aws:/root/.aws
cluster-proxy:
container_name: cluster.com-kubectl-proxy
image: cluster.com
user: root
entrypoint: kubectl proxy --address='0.0.0.0' --port=8001 --accept-hosts='.*'
ports:
- "8001:8001"
stdin_open: true
volumes:
- ./data:/data
- ./.ssh:/root/.ssh
- ./.kube:/root/.kube
- ./.aws:/root/.aws
Makefile
docker.build:
docker build -t cluster.com .
docker.run:
docker-compose up -d
docker.bash:
docker exec -it cluster.com bash
Dockerfile — берем базовый образ ubuntu и устанавливаем весь софт. Makefile — просто для удобства, можно использовать и обычный механизм алиасов. Docker-compose.yml — мы добавили дополнительный контейнер, который нам пробросит в браузер K8S Dashboard, если нужно визуально что-то посмотреть.
Создаем папки data, .ssh, .kube, .aws в корне и кладем туда наш конфиг для aws, ssh ключи и можем собирать и запускать наш контейнер через make docker.build & make docker.run.
Ну и в папке data создаем папку, в которую положим yaml файлы k8s, а рядом вторую, в которой будем хранить состояние terraform кластера. Примерный результат этого этапа положил на гитхаб.
1. Поднимаем наш кластер.
Дальше будет вольный перевод этой заметки. Я опущу много теоретических моментов, постараюсь описать краткую выжимку. Все таки формат моей заметки — tldr.
В нашу папку data/aws-cluster-init-kops-terraform клонируем то, что лежит в этом репозитории и заходим в консоль контейнера через make docker.bash. Начинается россыпь скучных команд.
AWS CLI
Создаем пользователя kops , добавляем права доступа и переконфигурируем AWS CLI на него, чтобы не запускать команды от суперюзера.
aws iam create-group --group-name kops
# Политики доступа
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonEC2FullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonRoute53FullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/IAMFullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AWSCertificateManagerFullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonVPCFullAccess --group-name kops
aws iam create-user --user-name kops
aws iam add-user-to-group --user-name kops --group-name kops
aws iam create-access-key --user-name kops
aws configure
Инициализируем Terraform
Изменяем в файле data/aws-cluster-init-kops-terraform/variables.tf имя кластера на нужное. Не забываем взять из файла update.json наши dns сервера и обновить их там, где покупали свой домен.
# Переходим в папку
cd /data/aws-cluster-init-kops-terraform
# Экспортируем переменные от AWS CLI
export AWS_ACCESS_KEY_ID=$(aws configure get aws_access_key_id)
export AWS_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key)
# Инициализируем terraform
terraform init
terraform get
terraform apply
# Берем NS сервера
cat update-zone.json \
| jq ".Changes[].ResourceRecordSet.Name=\"$(terraform output name).\"" \
| jq ".Changes[].ResourceRecordSet.ResourceRecords=$(terraform output -json name_servers | jq '.value|[{"Value": .[]}]')" \
> update-zone.json
Kops
Создаем кластер через kops, экспортируя конфиг в .tf файл.
export NAME=$(terraform output cluster_name)
export KOPS_STATE_STORE=$(terraform output state_store)
export ZONES=$(terraform output -json availability_zones | jq -r '.value|join(",")')
kops create cluster \
--master-zones $ZONES \
--zones $ZONES \
--topology private \
--dns-zone $(terraform output public_zone_id) \
--networking calico \
--vpc $(terraform output vpc_id) \
--target=terraform \
--out=. \
${NAME}
Здесь нужна небольшая ремарка. Terraform создаст VPC, и нам необходимо будет немного подправить конфиг, который нам отдаст kops. Это делается достаточно просто, через вспомогательный образ ryane/gensubnets:0.1
# Внутри конейнера
terraform output -json > subnets.json
# На вашей машине хоста
echo subnets.json | docker run --rm -i ryane/gensubnets:0.1
Вы можете добавить сразу политики для route53.
additionalPolicies:
master: |
[
{
"Effect": "Allow",
"Action": ["route53:ListHostedZonesByName"],
"Resource": ["*"]
},
{
"Effect": "Allow",
"Action": ["elasticloadbalancing:DescribeLoadBalancers"],
"Resource": ["*"]
},
{
"Effect": "Allow",
"Action": ["route53:ChangeResourceRecordSets"],
"Resource": ["*"]
}
]
node: |
[
{
"Effect": "Allow",
"Action": ["route53:ListHostedZonesByName"],
"Resource": ["*"]
},
{
"Effect": "Allow",
"Action": ["elasticloadbalancing:DescribeLoadBalancers"],
"Resource": ["*"]
},
{
"Effect": "Allow",
"Action": ["route53:ChangeResourceRecordSets"],
"Resource": ["*"]
}
]
Редактируем через kops edit cluster ${NAME}.
Теперь мы можем поднимать сам кластер.
kops update cluster \
--out=. \
--target=terraform \
${NAME}
terraform apply
Все пройдет хорошо, контекст kubectl изменится. В папке data/aws-cluster-init-kops-terraform у нас будет хранится состояние кластера. Можно просто положить все в git и отправить в приватный репозиторий битбакета.
$ kubectl get nodes
NAME STATUS AGE
ip-10-20-101-252.ec2.internal Ready,master 7m
ip-10-20-103-232.ec2.internal Ready,master 7m
ip-10-20-103-75.ec2.internal Ready 5m
ip-10-20-104-127.ec2.internal Ready,master 6m
ip-10-20-104-6.ec2.internal Ready 5m
2. Поднимаем наше приложение
Теперь, когда у нас есть что-то, мы можем разворачивать в кластере наши сервисы. Примерные конфиги я положу в этот же репозиторий. Их можно пачкой положить в data/k8s.
Сервисные шутки
Начнем с сервисных вещей. Нам необходим helm, route53, storage-classes и доступ к нашему приватному registry на hub.docker.com. Ну или к любому другому, если есть такое желание.
# Init helm
kubectl create serviceaccount --namespace kube-system tiller
kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
kubectl patch deploy --namespace kube-system tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}'
helm init
kubectl apply -f default-namespace.yaml
kubectl apply -f storage-classes.yaml
kubectl apply -f route53.yaml
kubectl apply -f docker-hub-secret.yml
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/recommended/kubernetes-dashboard.yaml
PostgreSQL + Redis
Я очень много раз обжигался, используя докер не для stateless контейнеров, но последняя конфигурация показала пока себя наиболее подходящей. Использую Stolon, чтобы обеспечить масштабируемость. Около года полет нормальный.
Развертываем helm-charts и пару быстреньких конфигов Redis.
# Разворачиваем etcd для stolon
cd etcd-chart
helm install --name global-etcd .
# Разворачиваем сам stolon
cd stolon-chart
helm dep build
helm install --name global-postgres .
# Разворачиваем redis
kubectl apply -f redis
Nginx + PHP
Обычная связка. Nginx и php-fpm. Конфиги я особо не вычищал, но каждый сможет под себя настроить. Перед применением необходимо указать образ, из которого мы будем брать код + добавить строчку сертификата из AWS Certificate Manager. Сам php — можно брать из докерхаба, но я собрал свой приватный, добавив немного библиотек.
kubectl apply -f nginx
kubectl apply -f php
В нашем образе с кодом мы храним его в папке /crm-code. Подменяем на свой образ и оно вполне корректно заработает. Файл — nginx/deployment.yml.
Выводим наружу домен. Route53 сервис его подхватит, изменит/добавит DNS записи, сертификат подгрузится на ELB из AWS Certificate Manager. Файл — nginx/service.yml.
Пробрасываем env переменные в php, чтобы иметь их внутри и подключаться к PostgreSQL/Redis. Файл — php/deployment.yml.
Как итог, мы имеем K8S кластер, который на базовом уровне мы можем масштабировать, добавлять новые сервисы, новые сервера (ноды), изменять количество PostgreSQL, PHP, Nginx инстансов и прожить до того, как в команде появится отдельный человек, который будет этим заниматься.
В рамках этой небольшой заметки я не буду касаться вопросов бэкапов/мониторинга этого всего добра. На начальном этапе будет достаточно localhost:8001/ui от K8S Dashboard сервиса. Позже можно будет прикрутить Prometheus, Grafana, Barman, либо любые другие схожие решения.
Используя терминал, либо Teamcity, Jenkins обновление кода будет делаться примерно так.
# Собираем образ с кодом где-то на локальной машине или в Teamcity
docker build -t GROUP/crm-code:latest .
docker push GROUP/crm-code:latest
# Обновляем код (здесь немного лайфхаков)
kubectl set image deployment/php-fpm php-fpm=GROUP/php-fpm
kubectl rollout status deployment/php-fpm
kubectl set image deployment/php-fpm php-fpm=GROUP/php-fpm:latest
kubectl set image deployment/nginx nginx=danday74/nginx-lua
kubectl rollout status deployment/nginx
kubectl set image deployment/nginx nginx=danday74/nginx-lua:latest
kubectl rollout status deployment/php-fpm
kubectl rollout status deployment/nginx
Буду рад, если это будет кому-нибудь интересно и вдвойне рад, если это кому-нибудь поможет. Спасибо за ваше внимание. Еще раз прикладываю ссылку на репозиторий один и два.