AWS: миграция RTFM 2.1 – CloudFormation для EC2 c Jenkins

Автор: | 30/09/2017
 

Задача – развернуть 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

Теперь приступаем к созданию шаблона.

Шаблон очень простой:

  1. Security Group
  2. EC2
  3. подключить к нему созданный выше 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, добавляем в PropertyVolume:

...
  "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, часть #1: ручное создание инфраструктуры – VPC, подсети, IGW, NAT GW, маршруты и EC2

AWS: миграция RTFM, часть #3: CloudFormation – инфрастуктура

AWS: смонтировать EBS к EC2

Docker: AWS [China] – Jenkins в Docker

Azure: Azure Resource Manager provisioning и Jenkins в Docker