使用Jenkins CI/CD Pipeline通过Terraform和Ansible创建AWS基础设施。
首先,我要感谢Derek Morgan和他的Terraform、Ansible and Jenkins课程。我最近完成了他的课程,再怎么推荐也不为过。它从小开始,到最后你有一个复杂的工作项目。完成每门课程后,我都会尝试创建自己的项目来强化我所学的知识,而这篇文章正是记录了这一点。
该项目的最终结果是Terraform代码,该代码创建了一个带有公共子网和EC2实例的AWS环境,以及一个在新创建的EC2实例上安装Docker的Ansible Playbook剧本脚本文件。当代码被推送到GitHub存储库时,GitHub Webhook将触发Jenkins CI/CD pipeline,该pipeline将执行一系列操作,具体取决于我们是将代码推送到开发分支还是主分支。
注意:本文显示了完成的项目,但希望您在构建它时测试每个部分以验证一切正常,而不是一下子就完全构建它。对于这个,我不打算详细说明我是如何做到的。例如,我不会详细介绍如何设置Terraform,然后在构建基础设施时逐步测试代码,以及如何在将playbook应用到Jenkins管道之前配置Ansible并在本地测试它们等。只是意识到这些步骤在开发过程中是典型的。按照我列出此文档的方式,如果您按照这种方式单独进行测试,您将遇到错误。如果您想了解更多详细信息,我再次建议您查看Derek的课程。
免责声明:虽然本文将带您完成该项目的步骤,但它并不是一个教程,而是我最近的一个副项目的文档。如果您收到任何错误,请不要发送仇恨邮件。
先决条件
- GitHub帐户
- AWS CLI
- 安装Terraform
- AWS账户
- 具有管理员权限的AWS用户
- AWS Cloud9(您可以使用其他 IDE,但要意识到某些步骤可能会有所不同)
- Terraform Cloud帐户
- 安装Git
开始设置环境
创建您的Amazon Cloud9环境
使用除平台部分之外的所有默认值。选择Ubuntu Server 18.04LTS
。
分配Elastic IP
为了防止Cloud9实例在每次关闭和重新启动时更改其公共IP地址,请为其分配一个弹性IP。这也将允许稍后将公共IP分配给安全组的Terraform变量。只需确保在删除Cloud9实例后删除弹性IP,否则将收取费用。
- 在AWS控制台中导航到EC2
- 在
Network
和Security
下选择Elastic IP
- 单击分配
Allocate Elastic IP address
- 点击
Allocate
- 从
Action
下拉列表中选择Associate Elastic IP Address
实例:选择您的Cloud9实例,私有IP地址:选择您的Cloud9私有IP地址。单击
Associate
。调整Cloud9实例的大小
Derek的课程提供了一个很棒的大小调整脚本来扩大Cloud9实例的存储空间,这将是必需的。
- 在Cloud9中创建一个名为resize.sh的文件
- 将Derek存储库中的代码复制到您的resize.sh文件中
- 运行
chmod +x resize.sh
- 运行
./resize.sh
创建SSH密钥
- 在您的终端中运行
ssh-keygen -t rsa
- 输入保存密钥的文件(我命名为 mykey):/home/ubuntu/.ssh/
- 没有密码
- 通过运行
ls ~/.ssh
验证您的密钥是否已创建
安装jq
运行sudo apt install jq
Fork Repo
如果您想fork并引用它,可以在这里找到我的代码:
https://github.com/troy-ingra...
Terraform Cloud
[Terraform Cloud]允许您将Terraform状态存储在远程安全位置,而不是将其存储在本地。这允许更好的安全性和更好的团队协作。
- 创建一个新的workspace
- 选择
CLI-driven workflow
- 命名您的workspace,然后创建workspace
- 复制workspace提供的示例代码并将其添加到backends.tf以替换当前后端
- 通过单击
Remote
并选择Local
来设置Execution Mode
创建Terraform Cloud令牌
- 单击浏览器右上角的
Profile
,然后选择User settings
- 在
User settings
下选择Token
- 单击创建
Create API token
- 输入描述并单击创建
Create API token
- 复制提供的令牌并将其保存在安全的地方(如果您丢失了它,您可以随时使令牌过期并创建另一个)
- 导航到Cloud9终端并运行
terraform login
- 键入
yes
,然后粘贴之前复制的Terraform Cloud令牌 - 运行
terraform init
这将我们的Terraform代码连接到我们的Terraform Cloud的workspace的替换了Terraform Cloud的本地后端。Jenkins未来在运行管道时也会访问Terraform Cloud。
Terraform
作为与云无关的产品,HashiCorp的Terraform支持多云和本地。它是一个开源工具,具有企业版和社区版,使用HashiCorp自己的HashiCorp配置语言 (HCL)。 Terraform的HCL允许开发人员学习一种语言以使用多种云产品和本地提供商,而不必为每种语言学习新的服务和语言。 HCL是一种声明性语言,专注于最终状态,而不是过程语言,其中所有命令都按编写的顺序执行。 Hashicorp为开发人员提供有关Terraform的文档,并且可以访问官方和社区模板的Terraform Registry。
更新variables.tf
- 将access_ip变量修改为您的个人公共IP CIDR
- 将cloud9_ip变量修改为您的Cloud9 IP CIDR
更新tfvars文件
- 将main.tfvars和dev.tfvars中的key_name变量更新为您的密钥名称
- 将main.tfvars和 dev.tfvars中的public_key_path变量更新为您的密钥名称
Terraform源码
Terraform代码可以在这GitHub的repo中找到: 所以我不打算在这里讨论所有内容。我将重点介绍一些有助于使文件更加通用和整洁的注意事项。
Data Source
使用一个AWS Available Zones的Data Source来确定将要在其中启动资源的Region中当前可用的AZ。使用Local Value来设置AZ的名称。然后使用length函数根据AZ的数量来确定计数。然后该计数用于索引cidr_block和availability_zones,而不是为每一个单独创建一个AZ定义块。
locals {
azs = data.aws_availability_zones.available.names
}
data "aws_availability_zones" "available" {}
resource "aws_subnet" "public_subnet" {
count = length(local.azs)
vpc_id = aws_vpc.vpc.id
cidr_block = var.public_cidr[count.index]
map_public_ip_on_launch = true
availability_zone = local.azs[count.index]
tags = {
Name = "docker-public"
}
}
Data Source还用于指定在该Region即将要启动的映像的AMI ID。这消除了对AMI ID进行硬编码或创建AMI和Region的映射的需要。请注意ami如何调用Data Source来确定Ubuntu AMI ID。
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
}
resource "aws_instance" "web" {
count = var.instance_count
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
vpc_security_group_ids = [aws_security_group.sg.id]
subnet_id = aws_subnet.public_subnet[count.index].id
key_name = aws_key_pair.docker_auth.id
tags = {
Name = "docker-instance"
}
}
Ansible
Ansible是一个简单的配置自动化工具,它使用SSH而不是在目标主机上安装代理。通常需要复杂脚本来自动化的事情现在可以使用Ansible使用Ansible Playbook在几行代码中完成。Ansible Playbooks使用YAML,与大多数脚本语言相比,它是一种易于阅读的纯英语语言。使用Ansible,您可以管理要跟踪或修改的主机的纯文本清单文件。这些主机可以组合在一起,也可以单独分组在不同的组标题下。在构建Ansible Playbook时,您可以区分哪些组应该运行哪些模块。
安装Ansible
在Cloud9实例上运行以下命令以安装Ansible。
sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install ansible
配置Ansible的Hosts
- 运行
sudo vim /etc/ansible/hosts
在文件顶部插入以下内容:
[hosts] localhost [hosts:vars] ansible_connection=local ansible_python_interpreter=/usr/bin/python3
退出并保存。
配置
为避免提示输入ECDSA密钥指纹,请按如下配置ansible.cfg文件。
- 运行
sudo vim /etc/ansible/ansible.config
- 在“# host_key_checking = False”一行中去掉注释符合“#”
退出并保存。
Ansible Playbook: docker.yml
与其手动运行命令来安装Docker,不如使用下面的docker.yml Ansible Playbook。Playbook分为几个任务,每个任务都有一个描述所采取行动的名称。需要注意的一些主题是使用“{{变量名称}}”表示法而不是创建多个任务来使用Ansible Playbook变量。另请注意 ansible.builtin.apt 或 apt 用于包管理。Ansible通过使用State来检查包是否存在于实例上。如果目标是删除一个包,那么State将从preset更改为absent。
另请注意,hosts引用的是main而不是local的。name: Install Docker
hosts: main
become: yestasks:
- name: Update apt cache
apt: update_cache=yes cache_valid_time=3600 - name: Upgrade all apt packages
apt: upgrade=dist name: Install dependencies
apt:
name: "{{ packages }}"
state: present
update_cache: yes
vars:
packages:- apt-transport-https
- ca-certificates
- curl
- software-properties-common
- gnupg-agent
- name: Add an apt signing key for Docker
apt_key:
url: https://download.docker.com/l...
state: present - name: Add apt repository for stable version
apt_repository:
repo: deb [arch=amd64] https://download.docker.com/l... focal stable
state: present name: Install Docker
apt:
name: "{{ packages }}"
state: present
update_cache: yes
vars:
packages:- docker-ce
- docker-ce-cli
- containerd.io
- name: Update apt cache
Jenkins
Jenkins是一个独立的开源自动化服务器,用于自动化与构建、测试和交付/部署软件相关的任务。Jenkins Pipeline通过使用插件和Jenkinsfile将持续交付管道实现到Jenkins。Jenkinsfile可以是声明式的或脚本式的,并包含管道要遵循的步骤列表。
使用Ansible安装Jenkins
您可以在Cloud9实例上手动安装Jenkins,但为了获得更多练习,您也可以使用Ansible。
运行ansible-playbook playbook/jenkins.yml
开放8080端口
- 在AWS控制台中导航到您的Cloud9的Security Group。
- 开放8080端口,范围0.0.0.0/0,面向全世界。这是允许我们访问Jenkins服务器所必需的,而且它解决了我们稍后会遇到的GitHub Webhook问题。这就是为什么它向世界开放,而不仅仅是一个IP。
- 在浏览器中导航到
:8080进行测试。
配置Jenkins
- 按照屏幕上的说明通过
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
运行检索您的管理密码 - 将结果复制并粘贴到Unlock Jenkins字段中,然后单击
Continue
- 单击
Install suggested plugins
- 输入所需信息。保存并继续到Jenkins。
- 点击
Manage Jenkins > Manage Plugins
选择Available
标签,查找Ansible
并点击install without restart
- 点击
Manage Plugins
选择Available
标签,查找Pipeline: AWS Steps
并点击install without restart
管理Jenkins凭证
GitHub App
- 在GitHub中单击浏览器右上角的个人资料图标,然后选择
Settings
- 点击左侧导航底部的
Development settings
- 单击
GitHub Apps
,然后单击New GitHub App
- 按照[GitHub App]()文档中
Creating GitHub App
部分的说明进行操作 - 将私钥下载到本地后,在下载文件夹中打开,然后拖放到Cloud9实例的顶层(这样可以确保您以后不会意外提交此文件)
在终端的顶层运行:
openssl pkcs8 -topk8 -inform PEM -outform PEM -in [key-in-your-downloads-folder-name].pem -out converted-github-app.pem -nocrypt
- 这将创建一个名为converted-github-app.pem的文件,该文件对Jenkins友好
- 导航到Jenkins点击
Manage Jenkins > Manage Credentials > Jenkins
- 点击
Global > Global credentials
- 点击
Add Credentials
- Kind = GitHub App
- ID = [名称]
- Description = GitHub App Credentials
- App ID =(这可以在GitHub App中找到。
`
Settings > Developer settings > GitHub Apps > [您的App名称]) - Key = 复制并粘贴之前转换的密钥的converted-github-app.pem文件内容,然后单击
OK
- 导航回您的GitHub App并通过单击
Install
来安装该应用程序 - 回到Jenkins,点击你的
GitHub App Credentials
。单击Update
,然后单击Test connection
以验证一切正常。
Terraform Cloud凭证
- 要获取Terraform Cloud凭据,请在终端中运行
cat /home/ubuntu/.terraform.d/credentials.tfrc.json
- 复制输出。创建一个本地txt文件并粘贴输出并保存
- 导航回Jenkins并点击
Add a new credentials
- Kind = Secret file
- File = (选择刚刚保存的txt文件)
- ID = tf-creds(在下面引用到)
- Description = Terraform Cloud Credentials
SSH Key
- 在Cloud9终端上运行
`
cat /home/ubuntu/.ssh/ - 复制输出
- 返回Jenkins并点击
Add a new credentials
- Kind = SSH Username with private key
- ID = ec2-ssh-key (在下面引用到)
- Description = SSH key for EC2 instances
- Username = ubuntu
- Private Key = 选择
Enter directly
,粘贴前面的复制 - 点击
OK
GitHub Webhook
- 导航到项目GitHub存储库并单击
Settings
- 点击
Webhooks
- 点击
Add webhook
Paylod URL =
/github-webhook/
注意:确保末尾有"/"。Content type = application/json
Which events would you like to trigger this webhook? = Just the push event
创建Jenkins Pipeline
- 导航到Jenkins的Dashboard并单击
New Item
- 命名pipeline并选择
Multi-Branch Pipeline
,然后单击OK
- 接下来配置pipeline:
- Display Name =
- Branches Sources = GitHub
- GitHub Credentials = 选择
GitHub App Credentials
- Repository HTTPS URL =
.git
注意:保存后,您将被带到扫描存储库日志屏幕,即使它已完成,它看起来也像是在进行中。在输出底部看到SUCCESS
后,您就可以返Dashbord了。
Jenkinsfile
Jenkins需要在我们代码的根目录下使用Jenkinsfile来创建Jenkins pipeline。下面是这个项目使用的Jenkinsfile。
pipeline {
agent any
environment {
TF_IN_AUTOMATION = 'true'
TF_CLI_CONFIG_FILE = credentials('tf-creds')
AWS_SHARED_CREDENTIALS_FILE='/home/ubuntu/.aws/credentials'
}
stages {
stage('Init') {
steps {
sh 'ls'
sh 'cat $BRANCH_NAME.tfvars'
sh 'terraform init -no-color'
}
}
stage('Plan') {
steps {
sh 'terraform plan -no-color -var-file="$BRANCH_NAME.tfvars"'
}
}
stage('Validate Apply') {
when {
beforeInput true
branch "dev"
}
input {
message "Do you want to apply this plan?"
ok "Apply plan"
}
steps {
echo 'Apply Accepted'
}
}
stage('Apply') {
steps {
sh 'terraform apply -auto-approve -no-color -var-file="$BRANCH_NAME.tfvars"'
}
}
stage('Inventory') {
steps {
sh '''printf \\
"\\n$(terraform output -json instance_ips | jq -r \'.[]\')" \\
>> aws_hosts'''
}
}
stage('EC2 Wait') {
steps {
sh '''aws ec2 wait instance-status-ok \\
--instance-ids $(terraform output -json instance_ids | jq -r \'.[]\') \\
--region us-east-1'''
}
}
stage('Validate Ansible') {
when {
beforeInput true
branch "dev"
}
input {
message "Do you want to run Ansible?"
ok "Run Ansible"
}
steps {
echo 'Ansible Approved'
}
}
stage('Ansible') {
steps {
ansiblePlaybook(credentialsId: 'ec2-ssh-key', inventory: 'aws_hosts', playbook: 'playbooks/docker.yml')
}
}
stage('Validate Destroy') {
input {
message "Do you want to destroy?"
ok "Destroy"
}
steps {
echo 'Destroy Approved'
}
}
stage('Destroy') {
steps {
sh 'terraform destroy -auto-approve -no-color -var-file="$BRANCH_NAME.tfvars"'
}
}
}
post {
success {
echo 'Success!'
}
failure {
sh 'terraform destroy -auto-approve -no-color -var-file="$BRANCH_NAME.tfvars"'
}
aborted {
sh 'terraform destroy -auto-approve -no-color -var-file="$BRANCH_NAME.tfvars"'
}
}
}
我不会介绍这个文件的所有内容,但会强调一些有趣的点。
$BRANCH_NAME环境变量
当我们执行Jenkinsfile时,使用这个环境变量可以区分我们的分支。例如,下面的示例执行shell脚本并输出启动GitHub Webhook的分支。对于计划阶段,这允许我们调用main.tfvars或dev.tfvars文件,这将覆盖我们的默认variables.tf文件。
条件和输入
如果分支等于dev,则使用when
的条件来确定是否运行输入。如果分支不等于dev,则跳过输入。
输入用于暂停pipeline并等待手动选择Apply plan
或Abort
。
EC2等待
这可能是我从课程中学到的最方便的物品之一。在Inventory和EC2等待阶段下方都使用shell脚本来获取Terraform输出,并使用jq来获取实例ID和实例的IP。Inventory阶段传递从Terraform代码创建的实例IP,并将它们附加到Ansible将使用的aws_hosts文件中,从而创建一个动态清单文件。EC2等待阶段执行相同的操作,但传递实例ID并执行等待命令。这将验证实例是否已完成初始化,然后再转到执行Ansible的下一步。
整合所有
dev分支
- 通过运行
git checkout -b dev
创建一个dev分支。 - 运行
git add
。 - 通过运行
git commit -m "initial commit"
提交文件。 - 通过运行
git push -u origin dev
将代码推送到dev分支并通过GitHub进行身份验证。 - 导航到您的Jenkins仪表板。
- 单击位于浏览器左下角的
Build Executor Status
下的running pipeline
。 - 将鼠标悬停在
Validate Apply
步骤上,单击Apply plan
。 - 然后pipeline将应用Terraform代码,运行清单步骤,并等待EC2实例初始化,这样Ansible就不会在尝试访问实例时出错。
- 一旦到达
Validate Ansible
步骤,再次将鼠标悬停在该步骤上并选择Run Ansible
。 - 等待时,单击
Build History
旁边的绿色复选标记以查看输出。您将看到Ansible正在运行,但您也可以向上滚动并查看整个pipeline的所有输出。 - 当pipeline在
Validate Destroy
步骤中等待时,让我们检查我们的实例以确保Docker安装正确。在输出中向上滚动并找到instance_ips
输出并向下复制实例的公共IP。 - 返回Cloud9终端并使用您的SSH密钥SSH进入实例。
ssh -i /home/ubuntu/.ssh/[key name] ubuntu@[instance ip]
- 验证运行
docker --version 13
. 返回您的Jenkinspipeline并选择Destroy
。
main分支
由于dev分支已经过验证,代码可以推送到主分支。
- 通过运行
git checkout main
切换到主分支。 - 通过运行
git merge dev
合并dev分支。 - 通过运行
git push -u origin main
将代码推送到main并通过GitHub进行身份验证。 - 切换回Jenkins并检查我们的pipeline。这次你应该看到main的另一个分支。
- 单击main以查看运行中的pipeline。由于代码已经在开发pipeline中进行了测试,Jenkinsfile将根据设置的条件跳过验证步骤。pipeline应该一直移动到
Validate Destroy
阶段。 - 获取Docker实例IP,类似于测试开发pipeline的方式并验证Docker是否已安装。一旦确认返回pipeline并销毁。
附加测试
转到dev.tfvars文件和main.tfvars文件并将实例计数更新为2或3,并验证pipeline是否仍在运行并且是否创建了多个实例。
故障排除
注意:请再次查看本文开头的免责声明。如果您偏离上述步骤,那么您可能会得到不同的结果。例如,如果您在Cloud9上使用您最喜欢的IDE,或者如果您使用Amazon Linux而不是Ubuntu。所有这些都可能改变你的结果和应该采取的步骤。
我已尽力捕捉过程中的所有步骤。所有这些因素都会改变所需的步骤。如果您一直在跟进并遇到问题,请返回您的步骤并验证您是否修改了指定的变量。还要检查创建的所有凭据。当我最初经历这一切时,这似乎就是我所有的问题所在。如果有错误,我建议检查日志并查看错误以找到解决方案。我是人,我有可能错过了一步。