应用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
  • NetworkSecurity下抉择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/<key name>
  • 没有明码
  • 通过运行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 updatesudo apt install software-properties-commonsudo add-apt-repository --yes --update ppa:ansible/ansiblesudo apt install ansible

配置Ansible的Hosts

  1. 运行sudo vim /etc/ansible/hosts
  2. 在文件顶部插入以下内容:

    [hosts]localhost[hosts:vars]ansible_connection=localansible_python_interpreter=/usr/bin/python3
  3. 退出并保留。

    配置

    为防止提醒输出ECDSA密钥指纹,请按如下配置ansible.cfg文件。

  4. 运行sudo vim /etc/ansible/ansible.config
  5. 在“# host_key_checking = False”一行中去掉正文合乎“#”
  6. 退出并保留。

    Ansible Playbook: docker.yml

    与其手动运行命令来装置Docker,不如应用上面的docker.yml Ansible Playbook。Playbook分为几个工作,每个工作都有一个形容所采取行动的名称。须要留神的一些主题是应用“{{变量名称}}”表示法而不是创立多个工作来应用Ansible Playbook变量。另请留神 ansible.builtin.apt 或 apt 用于包治理。Ansible通过应用State来查看包是否存在于实例上。如果指标是删除一个包,那么State将从preset更改为absent。
    另请留神,hosts援用的是main而不是local的。

  7. name: Install Docker
    hosts: main
    become: yes

    tasks:

    • 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

Jenkins

Jenkins是一个独立的开源自动化服务器,用于自动化与构建、测试和交付/部署软件相干的工作。Jenkins Pipeline通过应用插件和Jenkinsfile将继续交付管道实现到Jenkins。Jenkinsfile能够是申明式的或脚本式的,并蕴含管道要遵循的步骤列表。

应用Ansible装置Jenkins

您能够在Cloud9实例上手动装置Jenkins,但为了取得更多练习,您也能够应用Ansible。
运行ansible-playbook playbook/jenkins.yml

凋谢8080端口

  1. 在AWS控制台中导航到您的Cloud9的Security Group。
  2. 凋谢8080端口,范畴0.0.0.0/0,面向全世界。这是容许咱们拜访Jenkins服务器所必须的,而且它解决了咱们稍后会遇到的GitHub Webhook问题。这就是为什么它向世界凋谢,而不仅仅是一个IP。
  3. 在浏览器中导航到 <Cloud9 Public IP>:8080进行测试。

配置Jenkins

  1. 依照屏幕上的阐明通过sudo cat /var/lib/jenkins/secrets/initialAdminPassword运行检索您的治理明码
  2. 将后果复制并粘贴到Unlock Jenkins字段中,而后单击Continue
  3. 单击Install suggested plugins
  4. 输出所需信息。保留并持续到Jenkins。
  5. 点击Manage Jenkins > Manage Plugins抉择Available标签,查找Ansible并点击install without restart
  6. 点击Manage Plugins抉择Available标签,查找Pipeline: AWS Steps并点击install without restart

治理Jenkins凭证

GitHub App

  1. 在GitHub中单击浏览器右上角的个人资料图标,而后抉择Settings
  2. 点击左侧导航底部的Development settings
  3. 单击GitHub Apps,而后单击New GitHub App
  4. 依照[GitHub App]()文档中Creating GitHub App局部的阐明进行操作
  5. 将私钥下载到本地后,在下载文件夹中关上,而后拖放到Cloud9实例的顶层(这样能够确保您当前不会意外提交此文件)
  6. 在终端的顶层运行:

    openssl pkcs8 -topk8 -inform PEM -outform PEM -in [key-in-your-downloads-folder-name].pem -out converted-github-app.pem -nocrypt
  7. 这将创立一个名为converted-github-app.pem的文件,该文件对Jenkins敌对
  8. 导航到Jenkins点击Manage Jenkins > Manage Credentials > Jenkins
  9. 点击Global > Global credentials
  10. 点击Add Credentials
  11. Kind = GitHub App
  12. ID = [名称]
  13. Description = GitHub App Credentials
  14. App ID =(这能够在GitHub App中找到。`Settings > Developer settings > GitHub Apps > [您的App名称])
  15. Key = 复制并粘贴之前转换的密钥的converted-github-app.pem文件内容,而后单击OK
  16. 导航回您的GitHub App并通过单击Install来装置该应用程序
  17. 回到Jenkins,点击你的GitHub App Credentials。单击Update,而后单击Test connection以验证一切正常。

Terraform Cloud凭证

  1. 要获取Terraform Cloud凭据,请在终端中运行cat /home/ubuntu/.terraform.d/credentials.tfrc.json
  2. 复制输入。创立一个本地txt文件并粘贴输入并保留
  3. 导航回Jenkins并点击Add a new credentials
  4. Kind = Secret file
  5. File = (抉择刚刚保留的txt文件)
  6. ID = tf-creds(在上面援用到)
  7. Description = Terraform Cloud Credentials

SSH Key

  1. 在Cloud9终端上运行` cat /home/ubuntu/.ssh/<key name>
  2. 复制输入
  3. 返回Jenkins并点击Add a new credentials
  4. Kind = SSH Username with private key
  5. ID = ec2-ssh-key (在上面援用到)
  6. Description = SSH key for EC2 instances
  7. Username = ubuntu
  8. Private Key = 抉择Enter directly,粘贴后面的复制
  9. 点击OK

GitHub Webhook

  1. 导航到我的项目GitHub存储库并单击Settings
  2. 点击Webhooks
  3. 点击Add webhook
  4. Paylod URL = <Jenkins URL>/github-webhook/
    留神:确保开端有"/"。
  5. Content type = application/json
  6. Which events would you like to trigger this webhook? = Just the push event

创立Jenkins Pipeline

  1. 导航到Jenkins的Dashboard并单击New Item
  2. 命名pipeline并抉择Multi-Branch Pipeline,而后单击OK
  3. 接下来配置pipeline:
  4. Display Name = <Name of pipeline>
  5. Branches Sources = GitHub
  6. GitHub Credentials = 抉择GitHub App Credentials
  7. Repository HTTPS URL = <Your repo name>.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 planAbort

EC2期待

这可能是我从课程中学到的最不便的物品之一。在Inventory和EC2期待阶段下方都应用shell脚本来获取Terraform输入,并应用jq来获取实例ID和实例的IP。Inventory阶段传递从Terraform代码创立的实例IP,并将它们附加到Ansible将应用的aws_hosts文件中,从而创立一个动静清单文件。EC2期待阶段执行雷同的操作,但传递实例ID并执行期待命令。这将验证实例是否已实现初始化,而后再转到执行Ansible的下一步。

整合所有

dev分支

  1. 通过运行git checkout -b dev创立一个dev分支。
  2. 运行git add
  3. 通过运行git commit -m "initial commit"提交文件。
  4. 通过运行git push -u origin dev将代码推送到dev分支并通过GitHub进行身份验证。
  5. 导航到您的Jenkins仪表板。
  6. 单击位于浏览器左下角的Build Executor Status下的running pipeline
  7. 将鼠标悬停在Validate Apply步骤上,单击Apply plan
  8. 而后pipeline将利用Terraform代码,运行清单步骤,并期待EC2实例初始化,这样Ansible就不会在尝试拜访实例时出错。
  9. 一旦达到Validate Ansible步骤,再次将鼠标悬停在该步骤上并抉择Run Ansible
  10. 期待时,单击Build History旁边的绿色复选标记以查看输入。您将看到Ansible正在运行,但您也能够向上滚动并查看整个pipeline的所有输入。
  11. 当pipeline在Validate Destroy步骤中期待时,让咱们查看咱们的实例以确保Docker装置正确。在输入中向上滚动并找到instance_ips输入并向下复制实例的公共IP。
  12. 返回Cloud9终端并应用您的SSH密钥SSH进入实例。ssh -i /home/ubuntu/.ssh/[key name] [email protected][instance ip]
  13. 验证运行docker --version 13. 返回您的Jenkinspipeline并抉择Destroy

main分支

因为dev分支曾经过验证,代码能够推送到主分支。

  1. 通过运行git checkout main切换到主分支。
  2. 通过运行git merge dev合并dev分支。
  3. 通过运行git push -u origin main将代码推送到main并通过GitHub进行身份验证。
  4. 切换回Jenkins并查看咱们的pipeline。这次你应该看到main的另一个分支。
  5. 单击main以查看运行中的pipeline。因为代码曾经在开发pipeline中进行了测试,Jenkinsfile将依据设置的条件跳过验证步骤。pipeline应该始终挪动到Validate Destroy阶段。
  6. 获取Docker实例IP,相似于测试开发pipeline的形式并验证Docker是否已装置。一旦确认返回pipeline并销毁。

附加测试

转到dev.tfvars文件和main.tfvars文件并将实例计数更新为2或3,并验证pipeline是否仍在运行并且是否创立了多个实例。

故障排除

留神:请再次查看本文结尾的免责申明。如果您偏离上述步骤,那么您可能会失去不同的后果。例如,如果您在Cloud9上应用您最喜爱的IDE,或者如果您应用Amazon Linux而不是Ubuntu。所有这些都可能扭转你的后果和应该采取的步骤。
我已尽力捕获过程中的所有步骤。所有这些因素都会扭转所需的步骤。如果您始终在跟进并遇到问题,请返回您的步骤并验证您是否批改了指定的变量。还要查看创立的所有凭据。当我最后经验这所有时,这仿佛就是我所有的问题所在。如果有谬误,我倡议查看日志并查看谬误以找到解决方案。我是人,我有可能错过了一步。