Задача – развернуть CloudFromation стек с одним EC2 интансом и примонтировать Elastic Block Store (EBS) с данными Jenkins.
Потом с помощью Ansible – установить там Docker и запустить Jenkins.
Содержание
Подготовка
Сначала – вручную создадим EBS, который будет хранить данные Jenkins и далее будет подключаться к создаваемому EC2 интансу.
Создаём именованный профиль AWS CLI для RTFM:
[simterm]
$ aws configure --profile rtfm AWS Access Key ID [None]: AKI***2PA AWS Secret Access Key [None]: P6o***2VR Default region name [None]: eu-west-1 Default output format [None]: json
[/simterm]
EBS
Создаём EBS раздел.
Тип раздела – стандартный Magnetic (standard), ибо дешёво и сердито – для Jenkins достаточно.
Стоимость EBS разделов – тут>>>, описание – тут>>>.
Создаём с помощью create-volume
:
[simterm]
$ aws ec2 --profile rtfm create-volume --availability-zone eu-west-1a --size 8 --volume-type standard --tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=jenkins-workspaces},{Key=Env,Value=production}]' { "AvailabilityZone": "eu-west-1a", "CreateTime": "2017-09-30T11:47:30.863Z", "Encrypted": false, "Size": 8, "SnapshotId": "", "State": "creating", "VolumeId": "vol-0085149b3a0a45d0c", "Tags": [ { "Key": "Name", "Value": "jenkins-workspaces" }, { "Key": "Env", "Value": "production" } ], "VolumeType": "standard" }
[/simterm]
Elastic IP (EIP) будет добавляться во время создания стека, что бы не тратить деньги, когда стек будет удаляться (держать Jenkins запущенным постоянно я не планирую, ибо деньги – проще обновить IN A
запись домена при обновлении стека и EIP).
Домены со временем тоже планируется вынести в AWS Route 53, пока управление – через панель регистратора.
Key pair
Создаём ключ для доступа с помощью create-key-pair
, сохраняем в файл ~/.ssh/rtfm_jenkins.pem
:
[simterm]
$ aws ec2 --profile rtfm create-key-pair --key-name rtfm_jenkins --query 'KeyMaterial' --output text > ~/.ssh/rtfm_jenkins.pem
[/simterm]
Права:
[simterm]
$ chmod 400 ~/.ssh/rtfm_jenkins.pem
[/simterm]
CloudFormation
Теперь приступаем к созданию шаблона.
Шаблон очень простой:
- Security Group
- EC2
- подключить к нему созданный выше EBS
Примеры шаблонов для создания EC2 есть тут>>> и тут>>>.
AMI ID
Находим Debian AMI ID с помощью describe-images
:
[simterm]
$ aws ec2 --profile rtfm describe-images --filters "Name=name,Values=debian-stretch*" "Name=root-device-type,Values=ebs" "Name=image-type,Values=machine" "Name=manifest-location,Values=aws-marketplace*" { "Images": [ { "Architecture": "x86_64", "CreationDate": "2017-09-01T05:55:19.000Z", "ImageId": "ami-cc5aa0b5", "ImageLocation": "aws-marketplace/debian-stretch-hvm-x86_64-gp2-2017-08-31-64407-572488bb-fc09-4638-8628-e1e1d26436f4-ami-ac5e55d7.4", ...
[/simterm]
Создание шаблона
Теперь – создаём шаблон.
Сначала – параметры:
- InstanceType: тип инстанса, по умолчанию самый мелкий –
t2.nano
, типы инстансов смотрим тут>>> - JenkinsAMIID: AMI ID Debian 9, который нашли выше
- KeyName: ключ, созданный ранее
- HomeAllowLocation: домашний IP, откуда разрешён доступ, позже можно будет добавить ещё один, для доступа с рабочей сети, передавать будем через параметры при создании стека
Ресурсы:
- EC2Instance: параметры EC2
- InstanceSecurityGroup: описание Security Group
Собственно – это всё, что требуется для минимального стека с EC2.
Полностью шаблон выглядит так:
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "AWS CloudFormation Template EC2", "Parameters" : { "InstanceType": { "Description": "Jenkins EC2 instance type", "Type": "String", "Default": "t2.nano", "AllowedValues": [ "t2.nano", "t2.micro", "t2.small" ], "ConstraintDescription": "Must be a valid EC2 instance type." }, "AMIID": { "Description": "Debian AMI ID", "Type": "String", "Default": "ami-cc5aa0b5" }, "KeyName": { "Description": "Name of an existing EC2 KeyPair to enable SSH access to the instance", "Type": "AWS::EC2::KeyPair::KeyName", "ConstraintDescription": "must be the name of an existing EC2 KeyPair.", "Default": "rtfm_jenkins" }, "HomeAllowLocation": { "Description": "The IP address range that can be used to SSH to the EC2 instances", "Type": "String", "MinLength": "9", "MaxLength": "18", "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x." } }, "Resources" : { "EC2Instance" : { "Type" : "AWS::EC2::Instance", "Properties" : { "Tags" : [ { "Key" : "Name", "Value" : "rtfm_jenkins" } ], "InstanceType" : { "Ref" : "InstanceType" }, "SecurityGroups" : [ { "Ref" : "InstanceSecurityGroup" } ], "KeyName" : { "Ref" : "KeyName" }, "ImageId" : { "Ref" : "AMIID" } } }, "InstanceSecurityGroup" : { "Type" : "AWS::EC2::SecurityGroup", "Properties" : { "Tags" : [ { "Key" : "Name", "Value" : "rtfm_jenkins" } ], "GroupDescription" : "Enable SSH access via port 22", "SecurityGroupIngress" : [ { "IpProtocol" : "tcp", "FromPort" : "22", "ToPort" : "22", "CidrIp" : { "Ref" : "HomeAllowLocation"} }, { "IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : { "Ref" : "HomeAllowLocation"} }, { "IpProtocol" : "tcp", "FromPort" : "443", "ToPort" : "443", "CidrIp" : { "Ref" : "HomeAllowLocation"} } ] } } }, "Outputs" : { "InstanceId" : { "Description" : "InstanceId of the newly created EC2 instance", "Value" : { "Ref" : "EC2Instance" } }, "AZ" : { "Description" : "Availability Zone of the newly created EC2 instance", "Value" : { "Fn::GetAtt" : [ "EC2Instance", "AvailabilityZone" ] } }, "PublicDNS" : { "Description" : "Public DNSName of the newly created EC2 instance", "Value" : { "Fn::GetAtt" : [ "EC2Instance", "PublicDnsName" ] } }, "PublicIP" : { "Description" : "Public IP address of the newly created EC2 instance", "Value" : { "Fn::GetAtt" : [ "EC2Instance", "PublicIp" ] } } } }
validate-template
Проверяем шаблон с помощью validate-template
:
[simterm]
$ cd PycharmProjects/rtfm_jenkins_stack/ $ ls -l total 4 -rw-r--r-- 1 setevoy setevoy 2988 Sep 30 16:09 jenkins_ec2.template $ aws cloudformation --profile rtfm validate-template --template-body file://jenkins_ec2.template { "Parameters": [ { "ParameterKey": "AMIID", "DefaultValue": "ami-cc5aa0b5", "NoEcho": false, "Description": "Debian AMI ID" }, { "ParameterKey": "KeyName", "DefaultValue": "rtfm_jenkins", "NoEcho": false, "Description": "Name of an existing EC2 KeyPair to enable SSH access to the instance" }, { "ParameterKey": "HomeAllowLocation", "NoEcho": false, "Description": "The IP address range that can be used to SSH to the EC2 instances" }, { "ParameterKey": "InstanceType", "DefaultValue": "t2.nano", "NoEcho": false, "Description": "Jenkins EC2 instance type" } ], "Description": "AWS CloudFormation Template EC2" }
[/simterm]
create-stack
И создаём стек с помощью create-stack
, передавая параметром IP:
[simterm]
aws cloudformation --profile rtfm create-stack --stack-name rtfm-jenkins --disable-rollback --template-body file://jenkins_ec2.template --parameters ParameterKey=HomeAllowLocation,ParameterValue=188.***.***.114/32 { "StackId": "arn:aws:cloudformation:eu-west-1:264418146286:stack/rtfm-jenkins/59633cf0-a5e1-11e7-b8cc-503ac9e74cc5" }
[/simterm]
Проверяем статус:
[simterm]
$ aws cloudformation --profile rtfm describe-stacks --stack-name rtfm-jenkins --query 'Stacks[*].StackStatus' [ "CREATE_COMPLETE" ]
[/simterm]
В консоли AWS:
update-stack
Надо было имя для EC и Security Group сразу задать, через теги…
Обновим шаблон, ресурс EC2Instance:
... "Resources" : { "EC2Instance" : { "Type" : "AWS::EC2::Instance", "Properties" : { "Tags" : [ { "Key" : "Name", "Value" : "rtfm_jenkins" } ], ...
И InstanceSecurityGroup:
... "InstanceSecurityGroup" : { "Type" : "AWS::EC2::SecurityGroup", "Properties" : { "Tags" : [ { "Key" : "Name", "Value" : "rtfm_jenkins" } ], ...
Выполняем update-stack
:
[simterm]
$ aws cloudformation --profile rtfm update-stack --stack-name rtfm-jenkins --template-body file://jenkins_ec2.template --parameters ParameterKey=HomeAllowLocation,ParameterValue=188.***.*** .114/32 { "StackId": "arn:aws:cloudformation:eu-west-1:264418146286:stack/rtfm-jenkins/59633cf0-a5e1-11e7-b8cc-503ac9e74cc5" }
[/simterm]
Проверяем статус:
[simterm]
$ aws cloudformation --profile rtfm describe-stacks --stack-name rtfm-jenkins --query 'Stacks[*].StackStatus' [ "UPDATE_COMPLETE" ]
[/simterm]
Проверяем инстанс:
[simterm]
$ aws ec2 --profile rtfm describe-instances --filters Name=tag-value,Values=rtfm_jenkins { "Reservations": [ { "Groups": [], "Instances": [ { "AmiLaunchIndex": 0, "ImageId": "ami-cc5aa0b5", "InstanceId": "i-06cb3e0a8ce7a8e59", "InstanceType": "t2.nano", "KeyName": "rtfm_jenkins", ...
[/simterm]
Замечательно.
Attach EBS
Следующим шагом – добавим подключение созданного в самом начале EBS раздела.
Обновляем шаблон, в параметры добавляем ID EBS раздела:
... "JenkinsWorkspacesEBSID": { "Description": "Existing EBS volume with Jenkins worspaces", "Type": "String", "Default": "vol-0085149b3a0a45d0c" } ...
В ресурсах – обновляем EC2, добавляем в Property – Volume
:
... "Resources" : { "EC2Instance" : { "Type" : "AWS::EC2::Instance", "Properties" : { "Tags" : [ { "Key" : "Name", "Value" : "rtfm_jenkins" } ], "InstanceType" : { "Ref" : "InstanceType" }, "SecurityGroups" : [ { "Ref" : "InstanceSecurityGroup" } ], "KeyName" : { "Ref" : "KeyName" }, "ImageId" : { "Ref" : "AMIID" }, "Volumes" : [ { "VolumeId" : { "Ref" : "JenkinsWorkspacesEBSID" }, "Device" : "/dev/xvdb" } ] } }, ...
Проверяем шаблон:
[simterm]
$ aws cloudformation --profile rtfm validate-template --template-body file://jenkins_ec2.template
[/simterm]
Обновляем стек:
[simterm]
$ aws cloudformation --profile rtfm update-stack --stack-name rtfm-jenkins --template-body file://jenkins_ec2.template --parameters ParameterKey=HomeAllowLocation,ParameterValue=188.***.***.114/32
[/simterm]
И апдейт падает с ошибкой “The volume ‘vol-0085149b3a0a45d0c’ is not in the same availability zone as instance ‘i-06cb3e0a8ce7a8e59’“.
ОК, забыл.
В параметры – добавляем Availability Zone:
... "JenkinsAvailabilityZone": { "Description": "AZ for Jenkins EC2, must be same as Workspaces EBS", "Type": "String", "Default": "eu-west-1a" } ...
В Properties
ресурса EC2 – добавляем указание “AvailabilityZone“:
... "Resources" : { "EC2Instance" : { "Type" : "AWS::EC2::Instance", "Properties" : { "AvailabilityZone" : { "Ref" : "JenkinsAvailabilityZone" }, "Tags" : [ { "Key" : "Name", "Value" : "rtfm_jenkins" } ], ...
Обновляем:
[simterm]
$ aws cloudformation --profile rtfm update-stack --stack-name rtfm-jenkins --template-body file://jenkins_ec2.template --parameters ParameterKey=HomeAllowLocation,ParameterValue=188.***.***.114/32
[/simterm]
В Консоли:
Проверяем разделы интанса:
[simterm]
$ aws ec2 --profile rtfm describe-instances --filters Name=tag-value,Values=rtfm_jenkins --query 'Reservations[*].Instances[*].BlockDeviceMappings' --output text xvda EBS 2017-09-30T13:52:50.000Z True attached vol-0edd79eb7741a3bd7 /dev/xvdb EBS 2017-09-30T13:56:12.000Z False attached vol-0085149b3a0a45d0c
[/simterm]
Логинимся на сервер, проверяем разделы:
[simterm]
$ ssh [email protected] -i ~/.ssh/rtfm_jenkins.pem admin@ip-172-31-20-149:~$ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT xvda 202:0 0 8G 0 disk └─xvda1 202:1 0 8G 0 part / xvdb 202:16 0 8G 0 disk
[/simterm]
Jenkins
Дальше установка Jenkins будет выполнятся с помощью Ansbile – а сейчас выполним это руками, что бы иметь готовый EBS с worskpaces
.
xvdb
Форматируем раздел xvdb
.
Будьте осторожны с sfdisk
– синтаксис немного… Неудобный.
Выглядит команда так:
sfdisk /dev/xvdb << EOF ; EOF
Выполняем:
[simterm]
root@ip-172-31-20-149:/home/admin# sfdisk /dev/xvdb << EOF > ; > EOF Checking that no-one is using this disk right now ... OK Disk /dev/xvdb: 8 GiB, 8589934592 bytes, 16777216 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0x7feabcd9 Old situation: >>> Created a new DOS disklabel with disk identifier 0x1f89d3bd. /dev/xvdb1: Created a new partition 1 of type 'Linux' and of size 8 GiB. /dev/xvdb2: Done. New situation: Device Boot Start End Sectors Size Id Type /dev/xvdb1 2048 16777215 16775168 8G 83 Linux The partition table has been altered. Calling ioctl() to re-read partition table. Syncing disks.
[/simterm]
Проверяем:
[simterm]
root@ip-172-31-20-149:/home/admin# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT xvda 202:0 0 8G 0 disk └─xvda1 202:1 0 8G 0 part / xvdb 202:16 0 8G 0 disk └─xvdb1 202:17 0 8G 0 part
[/simterm]
Создаём файловую систему:
[simterm]
root@ip-172-31-20-149:/home/admin# mkfs.ext4 /dev/xvdb1 mke2fs 1.43.4 (31-Jan-2017) Creating filesystem with 2096896 4k blocks and 524288 inodes Filesystem UUID: 3818cfb8-c1ac-46d1-87ac-55700dc0f473 Superblock backups stored on blocks: 32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632 Allocating group tables: done Writing inode tables: done Creating journal (16384 blocks): done Writing superblocks and filesystem accounting information: done
[/simterm]
Монтируем /dev/xvdb1
в каталог /jenkins
:
[simterm]
root@ip-172-31-20-149:/home/admin# mkdir /jenkins root@ip-172-31-20-149:/home/admin# mount /dev/xvdb1 /jenkins/
[/simterm]
Docker
Устанавливаем Docker:
[simterm]
root@ip-172-31-20-149:/home/admin# apt update && apt -y install curl root@ip-172-31-20-149:/home/admin# curl https://get.docker.com/ | bash
[/simterm]
Проверяем:
[simterm]
root@ip-172-31-20-149:/home/admin# docker --version Docker version 17.09.0-ce, build afdb6d4
[/simterm]
Запуск Jenkins
Запускаем Docker контейнер с Jenkins, монтируем каталог /jenkins
с хост-машины в каталог /jenkins
контейнера, и указываем переменную JENKINS_HOME=/jenkins
:
[simterm]
# docker run -ti -u 0 -p 80:8080 -v /jenkins/:/jenkins -v /var/run/docker.sock:/var/run/docker.sock -e JENKINS_HOME='/jenkins' jenkins ... Sep 30, 2017 2:38:18 PM hudson.model.AsyncPeriodicWork$1 run INFO: Finished Download metadata. 9,353 ms --> setting agent port for jnlp --> setting agent port for jnlp... done
[/simterm]
Собственно – всё.
Активируем Jenkins в UI:
Проверяем содержимое /dev/xvdb1
:
[simterm]
admin@ip-172-31-20-149:~$ ls -l /jenkins/ total 100 -rw-r--r-- 1 root root 1592 Sep 30 14:38 config.xml -rw-r--r-- 1 root root 159 Sep 30 14:38 hudson.model.UpdateCenter.xml -rw-r--r-- 1 root root 370 Sep 30 14:40 hudson.plugins.git.GitTool.xml -rw------- 1 root root 1712 Sep 30 14:38 identity.key.enc ... drwxr-xr-x 3 root root 4096 Sep 30 14:38 users drwxr-xr-x 10 root root 4096 Sep 30 14:38 war drwxr-xr-x 2 root root 4096 Sep 30 14:40 workflow-libs
[/simterm]
Пересоздание стека
Для полноты картины – теперь можно всё удалить – и создать заново:
[simterm]
$ aws cloudformation --profile rtfm delete-stack --stack-name rtfm-jenkins
[/simterm]
Проверяем состояние EBS:
[simterm]
$ aws ec2 --profile rtfm describe-volumes --volume-ids vol-0085149b3a0a45d0c --query 'Volumes[*].State' --output text available
[/simterm]
Пересоздаём стек:
[simterm]
$ aws cloudformation --profile rtfm create-stack --stack-name rtfm-jenkins --disable-rollback --template-body file://jenkins_ec2.template --parameters ParameterKey=HomeAllowLocation,ParameterValue=188.***.***.114/32
[/simterm]
Подключаемся к новому инстансу, проверяем разделы:
[simterm]
$ ssh [email protected] -i ~/.ssh/rtfm_jenkins.pem ... admin@ip-172-31-31-164:~$ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT xvda 202:0 0 8G 0 disk └─xvda1 202:1 0 8G 0 part / xvdb 202:16 0 8G 0 disk └─xvdb1 202:17 0 8G 0 part
[/simterm]
Монтируем, проверяем данные:
[simterm]
root@ip-172-31-31-164:/home/admin# mkdir /jenkins root@ip-172-31-31-164:/home/admin# mount /dev/xvdb1 /jenkins/ root@ip-172-31-31-164:/home/admin# ls -l /jenkins/ total 108 -rw-r--r-- 1 root root 1592 Sep 30 14:38 config.xml -rw-r--r-- 1 root root 159 Sep 30 14:38 hudson.model.UpdateCenter.xml -rw-r--r-- 1 root root 370 Sep 30 14:40 hudson.plugins.git.GitTool.xml ... drwxr-xr-x 10 root root 4096 Sep 30 14:38 war drwxr-xr-x 2 root root 4096 Sep 30 14:40 workflow-libs
[/simterm]
Устанавливаем Docker:
[simterm]
root@ip-172-31-31-164:/home/admin# apt update && apt -y install curl && curl https://get.docker.com/ | bash
[/simterm]
Повторяем запуск Jenkins:
[simterm]
# docker run -ti -u 0 -p 80:8080 -v /jenkins/:/jenkins -v /var/run/docker.sock:/var/run/docker.sock -e JENKINS_HOME='/jenkins' jenkins
[/simterm]
Всё на месте:
Готово.
Шаблон доступен в Github тут>>>.
Ссылки по теме
Собственно – все ссылки на этот же блог, ибо всё уже делалось ранее:
AWS: CloudFormation – создание шаблона для VPC, EC2, NAT и Internet Gateway
AWS: миграция RTFM, часть #3: CloudFormation – инфрастуктура
Docker: AWS [China] – Jenkins в Docker
Azure: Azure Resource Manager provisioning и Jenkins в Docker