В любые времена перед разработчиками и владельцами сайтов стояла непростая задача: доставить контент как можно быстрее, как можно большему количеству клиентов. Одно из самых правильных решений — использовать CDN (Content Delivery Network) для раздачи статичных файлов. В случае с динамическим рендером страниц на сервере мы должны были ограничиваться небольшим списком объектов, которые можно было разместить в CDN: таблицы стилей, файлы скриптов, изображения. Однако, фронтенд, написанный на Angular (React, Vue.js...), статичен целиком, включая индексную страницу. Вот тут и возникает мысль: а почему бы не организовать раздачу через CDN всего фронтенда?
В данной статье пойдет речь о настройке комплексного решения для разработки, контроля версий, автоматической сборки и доставки статического сайта с использованием Gitlab CI, Amazon S3 и Amazon CloudFront. Также речь пойдет о настройке сопутствующих вещей: git, безопасное соединение по протоколу HTTPS, доменная почта, DNS хостинг, бэкенд сервер…
Если вас заинтересовала эта тема, добро пожаловать под кат. Осторожно! Много скриншотов.
Что мы получим в результате выполнения пошагового руководства:
- Стартовый сайт на Angular.
- Версионный контроль (git).
- Автоматическая сборка и публикация фронтенда в корзине Amazon S3.
- Раздача фронтенда через CDN (Amazon CloudFront).
- Бесплатный SSL сертификат от Amazon с автопродлением (для работы сайта через HTTPS).
- Бесплатная доменная почта от Яндекса и удобный интерфейс управления записями DNS (и не только).
- Bash скрипт автоматической настройки бэкенд сервера.
- Бэкенд сервер, работающий на ОС Debain 9 (nginx + PHP7.1-FPM).
- Бесплатный SSL сертификат от Let's Encrypt с автопродлением (для работы корневой зоны домена и бэкенд сервисов через HTTPS).
Что для этого нам понадобится:
- Доменное имя с доступом к панели управления регистратора.
- Аккаунт на Gitlab.
- Аккаунт на AWS (Amazon Web Services).
- Аккаунт на Яндексе.
- VDS (Virtual Dedicated Server) с установленной ОС Debain 9.
Несколько слов о таком выборе инструментов
Я потратил немало времени на поиски подходящих сервисов по ряду критериев. Во первых, это цена — хотелось бы, чтобы сервис был или бесплатным, или не очень дорогим. Во вторых — надежность. Также хорошо, если сервис берет большинство обязанностей на себя, избавляя от необходимости что-то устанавливать и настраивать. Например, зачем настраивать собственный git сервер, использовать какой-то сторонний сервис CI (Continuous Integration), если Gitlab предоставляет все эти сервисы «из коробки», а также неограниченное количество приватных репозиториев, организаций и коворкеров? Зачем настраивать доменную почту на собственном сервере, если можно это предоставить Яндексу? По поводу CDN — я просто не нашел ничего дешевле Amazon CloudFront. Файловое хранилище Amazon S3 также не дорогое (а зачем хранить много файлов на VDS, если это дорого?).
Итак, поехали!
1. DNS хостинг и почта Яндекс
Для начала привяжем ваш домен к Яндекс почте. Делаем этот шаг в первую очередь для настройки доменной почты. Нам необходимо будет подтвердить владение доменом для получения SSL сертификата Amazon, а для этого мы должны будем получить письмо на адрес webmaster@yourdomain.com.
1.1. Регистрируемся / логинимся в Яндексе.
1.2. Переходим в доменную почту Яндекса и добавляем свой домен (пункт меню «Подключить домен»).

Тут хочу сразу отметить, что у Яндекса появился «ПДД 2.0» или «Яндекс.Коннект». Однако, никак не могу определиться — то ли интерфейс Яндекс.Коннект
1.3. Подтверждаем владение доменом.

Самый простой для нас способ, это делегировать домен на Яндекс. Для этого в панели управления регистратора устанавливаем для вашего домена следующие NS-серверы:
dns1.yandex.net
dns2.yandex.net
Внимание! Этот шаг должен быть выполнен строго после подключения домена к почте для домена (пункт 1.2).
1.4. Ждем до 72 часов (как повезет).
Проверка владения доменом должна пройти автоматически. После этого на странице ПДД в списке «Моих доменов» вы должны увидеть сообщение зеленого цвета о том, что «домен подключен и делегирован на Яндекс».

Теперь мы можем подключить почту для домена. Настраивать MX-записи для домена не нужно, т.к. мы делегировали домен на NS-серверы Яндекса.
На данном этапе мы уже получили доменную почту с возможностью добавлять до 1000 почтовых ящиков и с правильными почтовыми DNS записями.
1.5. Создаем первый почтовый ящик johndoe@yourdomain.com.
1.6. В верхнем меню переходим по ссылке «Мигрировать в Коннект».
Осуществляем перенос домена в Яндекс.Коннект, указываем название организации. После этого нам становится доступна страница https://connect.yandex.ru/portal/home.

1.7. Переходим в «Админку». Здесь нам доступны различные настройки.
Самое интересное для нас — пункт меню «Управление DNS». Вернемся к нему позже. Сейчас нам необходимо добавить алиас почтового ящика с названием webmaster@yourdomain.com. Этот ящик нам понадобится для получения письма с запросом подтверждения владения доменом от Amazon.
1.8. Переходим в пункт меню «Оргструктура» и выбираем единственного (пока) пользователя, который был автоматически создан при добавлении первого почтового ящика (пункт 1.5).

1.9. Нажимаем троеточие в правом верхнем углу карточки пользователя, в выпадающем меню выбираем пункт «Управление алиасами».
В появившемся окне нажимаем кнопку «Добавить новый».

Вводим «webmaster» и нажимаем кнопку «Добавить».
Теперь у нас появился алиас почтового ящика webmaster@yourdomain.com и мы готовы принимать письма на доменную почту.
2. Аккаунт Amazon Web Services
Для выполнения дальнейших действий данного руководства вам потребуется корневая учетная запись (root user) AWS.
Если у вас еще нет учетной записи, то вам сказочно повезло и вы можете воспользоваться уровнем бесплатного пользования AWS. Для этого необходимо пройти процедуру регистрации. В процессе регистрации у вас запросят данные банковской карты, откуда снимут (и не вернут!) 1$ для проверки платежеспособности. Также вам нужно будет указать свой номер телефона, на который позвонит робот из Америки.
В целом, процесс регистрации довольно прост и на мой взгляд не требует подробного описания.
3. Amazon Security Credentials
Для автоматического обмена данными с сервисами AWS мы будем использовать консольную утилиту aws cli (AWS Command Line Interface). Утилита авторизуется с помощью пары Access key ID и Secret access key. Создадим их.
3.1. Заходим в консоль AWS.
3.2. В верхнем меню справа нажимаем на свой логин. В выпадающем меню выбираем пункт «My Security Credentials».

3.3. Здесь может появиться окошко с предупреждением.

Можно его игнорировать и нажать кнопку «Continue to Security Credentials».
3.4. В меню слева выбираем пункт «Users». Нажимаем кнопку «Add user».
3.5. В поле «User name» пишем имя пользователя. Например, «cli-manager».
В пункте «Access type» ставим галочку «Programmatic access».

Нажимаем кнопку «Next: Permissions».
3.6. В следующем пункте выбираем «Attach existing policies directly». Выбираем галочку «AdministratorAccess».

Нажимаем кнопку «Next: Review».
3.7. В следующем пункте нажимаем кнопку «Create user».
3.8. В последнем пункте мы увидим только что созданного пользователя и его данные.

Внимание! Сразу запишите Access key ID и Secret access key (его можно увидеть, нажав «show») и/или скачайте файл .csv с данными пользователя (нажав кнопку «Download .csv»). Секретный ключ доступа вы больше нигде и никогда не увидите.
4. Сертификат SSL Amazon
Теперь нам нужно получить сертификат SSL для нашего домена.
4.1. В консоли AWS в верхнем меню выбираем «Services».
4.2. В поиске по сервисам набираем «Certificate manager».
4.3. Нажимаем кнопку «Request a certificate».
4.4. В поле «Domain name» пишем "*.yourdomain.com".

Обратите внимание на звездочку и точку перед названием домена. Таким образом мы получим wildcard certificate для домена и всех его поддоменов. Нажимаем кнопку «Review and request».
4.5. В следующем пункте нажимаем кнопку «Confirm and request».
4.6. В следующем пункте нажимаем кнопку «Continue».

Здесь мы увидим только что запрошенный сертификат и его статус: «Pending validation».
4.7. Теперь заходим в почту Яндекса.
Логинимся в ваш доменный почтовый ящик, который вы создали в пункте 1.5. настоящего руководства ( johndoe@yourdomain.com ). Вам должно было прийти письмо с запросом подтверждения владения доменом от Amazon.

4.8. Переходим по ссылке в письме.
Откроется новая вкладка браузера со страницей подтверждения владения доменом.

Нажимаем кнопку «I Approve».
Сертификат должен пройти успешное подтверждение.
Вернувшись в Certificate manager мы увидим наш сертификат и его статус: «Issued».
5. Корзина Amazon S3
Создадим корзину Amazon S3, в которой будут храниться статичные файлы нашего фронтенда.
5.1. В консоли AWS в верхнем меню выбираем «Services».
5.2. В поиске по сервисам набираем «S3».
5.3. Нажимаем кнопку «Create bucket».

В поле «Bucket name» указываем имя хоста (вместе с www). Оно должно полностью совпадать с названием вашего домена. Например: www.yourdomain.com.
В поле «Region» выбираем «US East (N. Virginia)». Во первых, это позволит избежать возможных проблем с перенаправлением на неправильные объекты (статья документации). Во вторых, это самый популярный регион и здесь самые недорогие расценки. В третьих, нам все равно, в каком регионе расположена корзина, т.к. раздавать контент будет CDN, а напрямую к файлам S3 клиенты обращаться не будут.
Нажимаем «Next» несколько раз, оставляем все поля как есть.
В списке ваших корзин появится новая корзина с названием www.yourdomain.com
5.4. Редактируем свойства корзины.
Кликаем на пустое место рядом с названием вашей корзины. В правой части окна появится всплывающее окно.

5.5. Выбираем пункт «Properties».

5.6. Нажимаем на «Static website hosting».

Сразу скопируйте «Endpoint» URL (указан в верхней части окна). Он понадобится для дальнейших настроек.
Выбираем первый пункт «Use this bucket to host a website».
В поле «Index document» пишем «index.html».
Нажимаем кнопку «Save».
5.7. Выбираем вкладку «Permissions».
Здесь нам нужно добавить www.yourdomain.com.s3-website-us-east-1.amazonaws.com и https://*.yourdomain.com в список разрешенных хостов для кроссдоменных запросов.

Нажимаем кнопку «CORS configuration».
Не будем разбирать формат XML файла. Для детального изучения данного вопроса, можно ознакомиться с документацией. Нам достаточно просто скопировать в текстовое поле вот это:
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>http://www.yourdomain.com.s3-website-us-east-1.amazonaws.com</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>HEAD</AllowedMethod>
<AllowedHeader>Content-*</AllowedHeader>
<AllowedHeader>Host</AllowedHeader>
<AllowedHeader>Origin</AllowedHeader>
</CORSRule>
<CORSRule>
<AllowedOrigin>https://*.yourdomain.com</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>HEAD</AllowedMethod>
<AllowedHeader>Content-*</AllowedHeader>
<AllowedHeader>Host</AllowedHeader>
<AllowedHeader>Origin</AllowedHeader>
</CORSRule>
</CORSConfiguration>
Замените значения тегов AllowedOrigin на ваши. Первый адрес — тот самый Static website endpoint, который вы скопировали в пункте 5.6 настоящего руководства.
Нажимаем кнопку «Save».
6. Раздача Amazon CloudFront
Теперь нам нужно организовать раздачу файлов через CDN из только что созданной корзины.
6.1. В консоли AWS в верхнем меню выбираем «Services».
6.2. В поиске по сервисам набираем «CloudFront».
6.3. Нажимаем кнопку «Create Distribution».

Выбираем метод доставки «Web», нажимаем соответствующую кнопку «Get Started».
Откроется очень большая форма (скрин делать не буду).
Пройдемся по нужным нам полям. Остальные поля не трогаем, оставляем как есть. В любом случае у нас есть возможность в дальнейшем менять все эти настройки.
Origin Settings
Origin Domain Name: здесь внимательно! Не нужно выбирать домен «www.yourdomain.com.s3.anazonaws.com» из подсказки к этому полю! Вставляем сюда Static website endpoint, который вы скопировали в пункте 5.6 настоящего руководства, без «http://» в начале.
Default Cache Behavior Settings
Viewer Protocol Policy: выбираем пункт «Redirect HTTP to HTTPS».
Allowed HTTP Methods: выбираем пункт «GET, HEAD, OPTIONS».
Cached HTTP Methods: ставим галочку напротив «OPTIONS».
Cache Based on Selected Request Headers: выбираем пункт «Whitelist». В появившемся пункте "Whitelist Headers" выбираем «Origin», нажимаем кнопку «Add».
Object Caching: Выбираем пункт «Customize».
Minimum TTL: пишем значение «300».
Compress Objects Automatically: выбираем «Yes».
Distribution Settings
Alternate Domain Names (CNAMEs): в текстовом поле пишем «www.yourdomain.com» и «static.yourdomain.com» — по одному на каждой строчке. По адресу static.yourdomain.com у нас будет доступно все то же самое, что и через www.yourdomain.com. Мы будем использовать его для получения статичных файлов, чтобы уменьшить количество запросов к основному домену.
SSL Certificate: выбираем пункт «Custom SSL Certificate». Ниже в выпадающем списке выбираем ранее полученный SSL сертификат "*.yourdomain.com".
Default Root Object: вводим «index.html» (без слеша в начале).
Нажимаем кнопку «Create Distribution».
Раздача создана. Она появится в списке со статусом «In Progress». В течении некоторого времени (обычно до 10 минут) она приобретет статус «Enabled».
Сразу скопируем ID и Domain Name нашей раздачи, они потребуются нам для дальнейших настроек.
6.4. Настроим страницы ошибок.
Нажимаем на ID раздачи в списке. Переходим во вкладку «Error Pages».

Нажимаем кнопку «Create Custom Error Response».
В поле «HTTP Error Code» выбираем «403: Forbidden».
«Customize Error Response» — выбираем «Yes».
В поле «Response Page Path» вводим "/index.html".
В поле «HTTP Response Code» выбираем «200: OK».
Повторяем эти же действия для ошибки 404.
Таким образом, не найденные или запрещенные адреса будут перенаправляться на index.html и обрабатываться Angular Router.
7. Бэкенд сервер
Настало время настроить бэкенд сервер.
Даже если у вас полностью статичный сайт, не требующий обращений к API на сервере, VDS не помешает. Дело в том, что корневая запись домена должна быть типа A (и/или AAAA, если у вас есть IPv6) и, соответственно, ссылаться на IP адрес. Мое мнение — самый простой и дешевый способ получить постоянный IP адрес в Интернете, это арендовать VDS. В придачу к этому мы получаем возможность разместить на этом IP адресе различные сервисы: API, базы данных, службу обмена сообщениями в реальном времени, и вообще что угодно. Некоторые DNS хостинги предоставляют возможность вместо записи типа A использовать ALIAS, где можно прописать не IP адрес, а доменное имя. Например, можно использовать Amazon Route 53, настроить корневую запись домена как ссылку на еще одну корзину S3, которая будет осуществлять переадресацию на вашу раздачу CloudFront.
В любом случае, это выбор каждого. Я же склоняюсь к аренде VDS, тем более, что цены на них сейчас довольно доступные. Например, Айхор Хостинг предоставляет VDS (1 СPU / 512 MB RAM / 10 GB HDD) всего за 1080 рублей в год. Это самый дешевый тариф, для начала нам он вполне подойдет. Хотя, я рекомендую приобрести VDS с диском SSD.
Далее я буду описывать процесс настройки VDS (или полноценного сервера) с root доступом и операционной системой Debian 9 на борту.
В своей работе я использую несколько однотипно настроенных VDS и не использую никаких панелей управления, типа ISPManager. Поэтому я автоматизировал процесс настройки сервера, написав несложный bash скрипт. Давайте поступим так же и создадим несколько файлов. Будьте внимательны! Переводы строк в файлах должны быть в стиле Unix (LF), а не Windows (CRLF).
nginx.conf:
user www-data;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
client_header_timeout 30;
client_body_timeout 30;
reset_timedout_connection on;
keepalive_timeout 30;
client_max_body_size 32m;
client_body_buffer_size 128k;
server_tokens off;
gzip on;
gzip_vary on;
gzip_disable "msie6";
gzip_proxied any;
gzip_min_length 1024;
gzip_comp_level 5;
gzip_types
application/atom+xml
application/javascript
application/json
application/ld+json
application/manifest+json
application/octet-stream
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy;
expires max;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Это основной конфигурационный файл nginx. Главное, что мы тут делаем — это включаем компрессию gzip и подключаем конфиги, которые будут находиться в каталоге /etc/nginx/sites-enabled.
ssl.conf:
ssi on;
ssl on;
ssl_certificate "/etc/letsencrypt/live/{{DOMAIN}}/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/{{DOMAIN}}/privkey.pem";
ssl_trusted_certificate "/etc/letsencrypt/live/{{DOMAIN}}/chain.pem";
ssl_ciphers AES256+EECDH:AES256+EDH;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1.2;
ssl_ecdh_curve secp384r1;
ssl_dhparam /etc/nginx/dhparam.pem;
ssl_stapling on;
ssl_stapling_verify on;
ssl_session_timeout 24h;
ssl_session_cache shared:SSL:24m;
ssl_buffer_size 1400;
Шаблон конфигурации nginx для SSL. Тут мы подключаем сертификат, который нам выдаст Let's Encrypt и устанавливаем необходимые параметры SSL, которые должны дать нам рейтинг A+ в Qualys SSL Server Test. Обратите внимание на подстроки {{DOMAIN}} — так и должно быть. Скрипт настройки сам заменит их на ваш домен.
site.conf:
server {
server_name {{DOMAIN}};
listen 80;
listen 443 ssl http2;
error_log off;
access_log off;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000;";
add_header Cache-Control public;
include /etc/nginx/ssl.conf;
location /.well-known/acme-challenge/ {
alias /var/www/.well-known/acme-challenge/;
}
location / {
return 301 https://www.$host:443$request_uri;
}
}
Шаблон конфигурации nginx для корневой зоны домена. Здесь мы подключаем конфигурацию SSL, делаем доступным извне каталог, в который Let's Encrypt будет складывать файлы для валидации доменов. Все запросы к yourdomain.com или yourdomain.com перенаправляем на www.yourdomain.com.
api.conf:
server {
server_name {{DOMAIN}};
listen 80;
error_log off;
access_log off;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
location /.well-known/acme-challenge/ {
alias /var/www/.well-known/acme-challenge/;
}
location / {
return 301 https://$host:443$request_uri;
}
}
server {
server_name {{DOMAIN}};
listen 443 ssl http2;
access_log /var/log/nginx/{{DOMAIN}}.access.log;
error_log /var/log/nginx/{{DOMAIN}}.error.log;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000;";
add_header Cache-Control public;
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, HEAD, OPTIONS, POST, PUT, DELETE, PATCH';
add_header 'Access-Control-Allow-Headers' 'Accept,Accept-Encoding,Accept-Language,Authorization,Cache-Control,Content-Length,Content-Type,Origin,If-Modified-Since,User-Agent,X-Requested-With';
add_header 'Access-Control-Expose-Headers' 'X-Powered-By';
set $root_path /var/www/{{DOMAIN}};
root $root_path;
disable_symlinks if_not_owner from=$root_path;
charset utf-8;
index index.php;
autoindex off;
include /etc/nginx/ssl.conf;
if ($request_method ~* ^(OPTIONS|HEAD)$) {
return 204;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Шаблон конфигурации nginx для поддомена api.yourdomain.com. Здесь мы также подключаем конфигурацию SSL, делаем доступным извне каталог, в который Let's Encrypt будет складывать файлы для валидации доменов. Все запросы к api.yourdomain.com перенаправляем на api.yourdomain.com. Подключаем PHP7.1-FPM.
setup.sh:
#!/usr/bin/env bash
time_start=`date +%s`
########################
# EDIT THESE VARIABLES:
########################
DOMAIN=yourdomain.com
API_DOMAIN="api.$DOMAIN"
SUPERUSER="inpassor"
SUPERUSER_PASSWORD="12341234"
USER="johndoe"
USER_EMAIL="johndoe@yourdomain.com"
USER_PASSWORD="12341234"
########################
apt update
echo y | apt install dialog apt-utils
echo y | apt install sed wget gnupg nano htop curl zip unzip apt-transport-https lsb-release ca-certificates debian-archive-keyring certbot
# add users
adduser --quiet --disabled-password --gecos "" $SUPERUSER
echo "$SUPERUSER:$SUPERUSER_PASSWORD" | chpasswd
adduser --quiet --disabled-password --gecos "" $USER
echo "$USER:$USER_PASSWORD" | chpasswd
sed -i "s/$SUPERUSER:x:1000:1000/$SUPERUSER:x:0:0/" /etc/passwd
# add external repositories and GPG keys
echo "deb http://nginx.org/packages/debian/ stretch nginx" > /etc/apt/sources.list.d/nginx.list
wget --quiet -O - https://nginx.org/packages/keys/nginx_signing.key | apt-key add -
echo "deb https://packages.sury.org/php/ stretch main" > /etc/apt/sources.list.d/php.list
wget --quiet -O - https://packages.sury.org/php/apt.gpg | apt-key add -
apt update
echo y | apt upgrade
echo y | apt install nginx php7.1-cli php7.1-fpm php7.1-mbstring php7.1-curl php7.1-xml
# nginx setup
mkdir /var/www
mkdir "/var/www/$API_DOMAIN"
chown -R $USER:www-data /var/www
rm /etc/nginx/conf.d/default.conf
cp nginx.conf /etc/nginx/nginx.conf
echo "" > /etc/nginx/ssl.conf
mkdir /etc/nginx/sites-available
mkdir /etc/nginx/sites-enabled
sed "s@{{DOMAIN}}@$DOMAIN@g" site.conf > /etc/nginx/sites-available/$DOMAIN.conf
ln -s /etc/nginx/sites-available/$DOMAIN.conf /etc/nginx/sites-enabled
sed "s@{{DOMAIN}}@$API_DOMAIN@g" api.conf > /etc/nginx/sites-available/$API_DOMAIN.conf
ln -s /etc/nginx/sites-available/$API_DOMAIN.conf /etc/nginx/sites-enabled
# ssl setup
service nginx restart
certbot register --agree-tos --email $USER_EMAIL
certbot certonly --webroot -w /var/www -d $DOMAIN -d $API_DOMAIN --rsa-key-size 4096
openssl dhparam -out /etc/nginx/dhparam.pem 4096
sed "s@{{DOMAIN}}@$DOMAIN@g" ssl.conf > /etc/nginx/ssl.conf
service nginx restart
chown -R nginx:$USER /var/log/nginx
chmod 664 /var/log/nginx/*
time_end=`date +%s`
echo Execution time: $((time_end-time_start)) sec.
Bash скрипт для автоматической настройки сервера. В начале файла объявляется несколько переменных:
DOMAIN=yourdomain.com # ваш домен (без www в начале)
API_DOMAIN=«api.$DOMAIN» # поддомен, на котором будет бэкенд API (здесь это api.yourdomain.com)
SUPERUSER=«inpassor» # имя суперпользователя, обладающего правами root
SUPERUSER_PASSWORD=«12341234» # пароль суперпользователя
USER=«johndoe» # имя пользователя
USER_EMAIL=«johndoe@yourdomain.com» # email пользователя — используется для регистрации в Let's Encrypt
USER_PASSWORD=«12341234» # пароль пользователя
Разумеется, необходимо их переназначить.
Далее последовательно выполняются следующие действия:
- Устанавливаются необходимые пакеты.
- Добавляются внешние репозитории и GPG ключи для доступа к более свежим пакетам nginx и PHP.
- Устанавливаются пакеты nginx, php7.1-cli, php7.1-fpm, php7.1-mbstring, php7.1-curl и php7.1-xml.
- Настраивается nginx.
- Запускается процесс получения SSL сертификата Let's Encrypt. Обратите внимание, что здесь же генерируется файл dhparam.pem. Этот процесс весьма долгий, поэтому можно сохранить сгенерированный файл и в дальнейшем копировать его.
Все, скрипт настройки сервера у нас готов.
Теперь мы можем зайти на наш VDS через SSH (по его IP адресу) под пользователем root, скопировать туда файлы *.conf и setup.sh, сделать setup.sh исполняемым (chmod 700 setup.sh) и запустить его.
После процесса настройки VDS готов к работе.
У нас установлен nginx, который слушает запросы к адресам: yourdomain.com, yourdomain.com, api.yourdomain.com, api.yourdomain.com. Запросы к http перенаправляются на https, запросы к yourdomain.com перенаправляются на www.yourdomain.com. По адресу api.yourdomain.com у нас работает API. Сейчас там пусто и чтобы он отвечал что-то на наши запросы, необходимо создать в каталоге /var/www/api.yourdomain.com/ файл index.php.
Но пока это все не доступно, по той причине, что мы не прописали необходимые настройки DNS.
8. Настройки DNS
После того, как мы настроили раздачу CloudFront и бэкенд сервер, мы готовы произвести окончательную настройку DNS записей.
8.1. Возвращаемся в Яндекс.Коннект.
8.2. Переходим в «Админку».
8.3. Выбираем пункт меню «Управление DNS».
Нам нужно добавить четыре новые DNS записи.
- Запись типа A: в поле «Хост» указываем "@", в поле «Значение записи» — IP адрес нашего VDS, в поле «TTL» — «3600».
- Запись A: «Хост» — «api», «Значение записи» — IP адрес VDS, «TTL» — «3600».
- Запись CNAME: «Хост» — «www», «Значение записи» — Domain Name раздачи CloudFront (см. пункт 6.3 настоящего руководства), «TTL» — «3600».
- Запись CNAME: «Хост» — «static», «Значение записи» — Domain Name раздачи CloudFront, «TTL» — «3600».
Только что мы направили запросы к yourdomain.com (без www) и api.yourdomain.com к нашей VDS, а запросы к www.yourdomain.com и static.yourdomain.com — к раздаче CloudFront.
9. Gitlab репозиторий
Далее нам нужно создать приватный git репозиторий для хранения и версифицирования исходных файлов нашего проекта на Angular.
9.1. Регистрирумся / логинимся на Gitlab.
9.2. Нажимаем кнопку «New project».

9.3. Далее выбираем путь к проекту и его название.

Устанавливаем «Visibility Level» в значение «Private» и нажимаем кнопку «Create project».
9.4. Добавляем ваш публичный SSH ключ в настройках.
Если у вас нет ключа, необходимо его создать. Как создать пару приватный ключ — публичный ключ в системе Windows можно почитать, например, тут.
В верхнем меню нажимаем на свой аватар, в выпадающем меню выбираем пункт «Settings».

В меню слева выбираем пункт «SSH Keys».
В поле «Key» вставляем содержимое файла публичного ключа. В системе Windows обычно он распололагается по пути: C:\Users\YourUsername\.ssh\id_rsa.pub.
В поле «Title» пишем название ключа (что угодно).
9.5. Клонируем репозиторий на локальный компьютер.
Для этого у нас на компьютере должен быть установлен git / git for Windows.
В командной строке заходим в локальную папку с вашими проектами, например, в C:\Projects.
Запускаем команду:
git clone git@gitlab.com:YourLogin/my-awesome-project.git MyAwesomeProject
Адрес репозитория «git@gitlab.com:YourLogin/my-awesome-project.git» можно посмотреть на главной странице проекта на сайте Gitlab. Здесь «MyAwesomeProject» — название локальной папки с проектом, которая будет создана автоматически.
Мы только что склонировали пустой репозиторий на локальный компьютер и у нас появилась папка проекта, в которой можно начинать создавать ваш проект.
10. Gitlab CI
Настраиваем Gitlab CI для автоматической сборки проекта, синхронизации собранных файлов с корзиной Amazon S3 и обновлении раздачи CloudFront.
10.1. Заходим в наш проект на сайте https://gitlab.com.
10.2. В меню слева выбираем «CI / CD» — «Environments».

Нажимаем кнопку «New environment».
10.3. В поле «Name» вводим «prod».
В поле «External URL» вводим ваш домен (вместе с https:// и www вначале). Например: www.yourdomain.com. Нажимаем кнопку «Save».
10.4. В меню слева выбираем «Settings» — «CI / CD».

10.5. Напротив пункта «Secret variables» нажимаем кнопку «Expand».
В поле «Key» вводим «AWS_ACCESS_KEY_ID».
В поле «Value» вводим ваш Access key ID, полученный в пункте 3.8 настоящего руководства.
В поле «Environment scope» оставляем звездочку.
Нажимаем кнопку «Add new variable».
Добавляем еще одну переменную «AWS_SECRET_ACCESS_KEY».
Повторяем действия, в поле «Value» вводим ваш Secret access key, полученный в пункте 3.8 настоящего руководства.
И еще одна переменная — «AWS_DISTRIBUTION_ID».
Повторяем действия, в поле «Value» вводим ваш Distribution ID, полученный в пункте 6.3 настоящего руководства.
На этот раз в поле «Environment scope» вводим «prod».
Должно получиться как на скриншоте:

Настройки среды окружения и переменных CI мы выполнили, остальное пропишем далее непосредственно в нашем проекте в файле .gitlab-ci.yml.
11. Стартовый сайт на Angular
Настало время создать наш проект на Angular.
11.1. В командной строке заходим в каталог нашего проекта — туда, куда мы склонировали пустой репозиторий (см. пункт 9.5 настоящего руководства).
В нашем примере это C:\Projects\MyAwesomeProject.
Выполняем следующие команды:
npm i -g @angular/cli
ng new yourdomain.com --style=scss --skip-git=true --directory=.
yourdomain.com заменяем на название вашего проекта (не обязательно должно совпадать с названием домена).
У нас глобально установился Angular cli и в каталоге проекта появились файлы, с которыми можно начинать работать.
11.2. Создадим файл .gitignore:
/.idea
/dist
/out-tsc
/node_modules
/e2e/*.js
/e2e/*.map
npm-debug.log
package-lock.json
11.3. Отредактируем файл .angular-cli.json.
В секции «apps» находим ключ «assets» и меняем его значение на:
[
{
"glob": "**/*",
"input": "./assets/",
"output": "./"
}
]
Таким образом все файлы и каталоги, которые будут находится в src/assets, попадут в корень нашей сборки.
В «styles» заменим «styles.scss» на «styles/styles.scss».
11.4. Создадим каталог src/styles и перенесем туда файл src/styles.scss.
Создадим файл src/styles/_variables.scss с таким содержимым:
$static-url: 'https://static.yourdomain.com';
В начале файла src/styles/styles.scss вставим строчку:
@import 'variables';
В дальнейшем скрипт сборки будет обновлять переменную $static-url в файле src/styles/_variables.scss.
Таким образом, у нас появится возможность прописывать в стилях пути к изображениям и шрифтам через эту переменную.
11.5. Добавим файл src/assets/favicon.ico. Как же мы без иконки то?
11.6. Создадим файл src/assets/robots.txt:
User-agent: *
Host: {{SERVER_URL}}
Sitemap: {{SERVER_URL}}/sitemap.xml
11.7. Создадим файл src/assets/sitemap.xml:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>{{SERVER_URL}}</loc>
</url>
</urlset>
11.8. Создадим файл .gitlab-ci.yml:
image: node:8.9
stages:
- deploy
cache:
paths:
- node_modules/
before_script:
- npm install --unsafe-perm --silent --global @angular/cli
- npm install --unsafe-perm --silent
- apt update
- echo y | apt install python-dev unzip
- curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "awscli-bundle.zip"
- unzip awscli-bundle.zip
- ./awscli-bundle/install -i /usr/local/aws -b /usr/local/bin/aws
deploy_prod:
stage: deploy
only:
- master
environment:
name: prod
artifacts:
paths:
- dist
script:
- DEPLOY_SERVER="${CI_ENVIRONMENT_URL/https:\/\/www./}"
- STATIC_URL="https://static.$DEPLOY_SERVER"
- sed -i "s@$static-url:.*;@$static-url:'$STATIC_URL';@g" src/styles/_variables.scss
- ng build --prod --aot --build-optimizer --no-progress --extract-licenses=false
- sed -i -e "s@\" href=\"@\" href=\"$STATIC_URL\/@g; s@href=\"styles@href=\"$STATIC_URL\/styles@g; s@src=\"@src=\"$STATIC_URL\/@g" dist/index.html
- SED_PATTERN="s@{{STATIC_URL}}@$STATIC_URL@g; s@{{SERVER_URL}}@$CI_ENVIRONMENT_URL@g"
- sed -i -e "$SED_PATTERN" dist/robots.txt
- sed -i -e "$SED_PATTERN" dist/sitemap.xml
- aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
- aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
- aws s3 rm s3://www.$DEPLOY_SERVER/ --recursive --exclude "*" --include "*.css" --include "*.js" --include "*.json" --include "*.html" --include "*.xml" --include "*.txt"
- aws s3 sync dist/ s3://www.$DEPLOY_SERVER/ --no-progress --delete --size-only --acl public-read
- aws cloudfront create-invalidation --distribution-id $AWS_DISTRIBUTION_ID --paths /*.html /*.xml /*.txt /*.json
Давайте посмотрим, что у нас тут.
Для работы скрипта используется Docker image «node:8.9».
Объявляется один этап сборки «deploy».
Каталог «node_modules» кешируется.
Перед выполнением скрипта сборки производится глобальная установка angular/cli, после этого устанавливаются все зависимости проекта.
Далее устанавливаются необходимые пакеты python-dev и unzip, скачивается утилита aws cli, распаковывается и устанавливается.
Объявляется одна задача с названием «deploy_prod», которая выполняется в окружении «prod», на этапе «deploy» и только в ветке «master».
В результате выполнения этой задачи будут созданы артефакты — все содержимое каталога «dist» (результат сборки проекта).
Процесс выполнения задачи:
- Объявляется переменная $DEPLOY_SERVER, которая формируется на основе $CI_ENVIRONMENT_URL (значение поля «External URL», см. пункт 10.3 настоящего руководства), без «www.» в начале (yourdomain.com).
- Объявляется переменная $STATIC_URL — static.$DEPLOY_SERVER (https://static.yourdomain.com).
- Значение переменной "$static-url" в файле src/styles/_variables.scss заменяется значением $STATIC_URL (https://static.yourdomain.com).
- Производится сборка проекта Angular (environment — production, AOT, используя build optimizer, не показывая прогресс на экране, не извлекая лицензии сторонних производителей).
- В файле index.html все пути к файлам таблиц стилей, скриптов и изображений заменяются на абсолютные пути к $STATIC_URL.
- В файлах robots.txt и sitemap.xml подстрока "{{SERVER_URL}}" заменяется на значение $CI_ENVIRONMENT_URL (https://www.yourdomain.com), подстрока "{{STATIC_URL}}" заменяется на значение $STATIC_URL (https://static.yourdomain.com).
- Устанавливаются ID ключа доступа и секретный ключ для безопасного соединения с AWS.
- Из корзины S3 рекурсивно удаляются все файлы *.css, *.js, *.json, *.html, *.xml и *.txt.
- Каталог «dist» синхронизируется с корзиной S3 (без вывода прогресса на экран, удаляя из корзины отсутствующие в «dist» файлы, сравнивая только размеры файлов, публикуя файлы для чтения).
- Для раздачи CloudFront создается invalidation (статья документации) с удалением файлов *.html, *.xml, *.txt и *.json из корневой папки.
11.9. Запускаем в каталоге проекта команды:
git add .
git commit -m "first commit"
git push
Всё! Если мы выполнили все пункты данного руководства правильно, осталось дождаться когда Gitlab выполнит задачу, и мы увидим наш стартовый сайт по адресу www.yourdomain.com.

Поздравляю!
12. Заключение
Итак, мы только что развернули удобную рабочую среду для создания сайта на Angular, настроили автоматическую сборку проекта и его доставку через CDN. Также мы настроили бэкенд сервер, SSL сертификаты, DNS записи и у нас работает доменная почта.
О чем я не упомянул в статье и что еще можно сделать?
- Если нам нужен бэкенд API, мы должны его создать (в нашем примере, на PHP). Скорее всего, понадобится усложнить bash скрипт настройки сервера, добавив туда, например, установку баз данных.
- Несмотря на то, что мы создали файл robots.txt и явно указали в нем Host, все равно желательно защититься от нежелательного доступа клиентов напрямую к корзине S3 и раздаче CloudFront.
- В Gitlab CI у нас настроено только одно окружение — «prod». В условиях, приближенных к боевым, вам может понадобиться тестовое окружение.
- Вероятно вам понадобится перед сборкой в Gitlab CI прогнать тесты.
- Ну и, конечно, вам нужно создать свой сайт на Angular. Не оставлять же стартовый?
Комментарии (12)
eshimischi
11.11.2017 12:02С приходом http/2 необходимость в cdn уже не является критической, но тем не менее спасибо за подробное описание.
Inpassor Автор
12.11.2017 15:51HTTP/2 улучшает время загрузки, но это не значит, что CDN больше не актуальны. В HTTP/2 реализовано множество улучшений в формате веб-контента, но это никак не приблизит контент к конечному пользователю. Только серверы и кеширование могут это сделать.
Так что не совсем правильно говорить о HTTP/2 как о замене CDN.
Почему бы не поговорить о CDN, работающем на HTTP/2?eshimischi
12.11.2017 16:17Я имел ввиду эту особенность, которая по сути и является средством доставки статического контента Server Push, но я соглашусь что http/2 не сможет полностью перекрыть значение cdn — Http2-need-for-CDN
Inpassor Автор
12.11.2017 16:22Да, я прекрасно понял, что вы имеете ввиду. Но ведь контент посредством Server Push будет идти до конечного пользователя от вашего сервера. А через CDN — от ближайшего сервера. Сюда сверху навесить HTTP/2 — и будет идеально.
setevoy4
11.11.2017 13:17Скажите, Inpassor — а вы про DevOps слышали? :-) Мы, собственно, вот как раз такой автоматизацией, как вы описали, занимаемся.
Пост замечательный, спасибо.
Только я бы предложил вам ещё посмотреть в сторону configuration management утилит, типа Ansible/Salt, как замена bash-скрипту. И хранить шаблоны в Gitlab, и обновлять конфигурацию бекендов и вообще всего и вся через билды в Jenkins CD :-) Но это уже из области Infrastructure as Code (которая, собственно, и входит в devops).
nevitas
11.11.2017 23:32Почему Amazon CloudFront, а не бесплатный CloudFlare?
Inpassor Автор
12.11.2017 10:43Вариантов может быть множество. Я описал один из них.
Что же касательно CloudFlare — был печальный опыт его использования. Одна из причин, почему я от него отказался, это то, что РосПотребНадзор блокировал многие IP CloudFlare.
KoMePcAHT
реферальная ссылка на сомнительный хостинг испортила все впечатление о статье
TecHMeaT
Да ладно вам, не на столько уж она и портит впечатление от хорошей статьи. К слову, хостинг вполне надежный.
Inpassor Автор
Удалил. А Айхор Хостинг я бы не стал называть сомнительным. Хотя бы прочитайте про них.