三个最好的朋友:Terraform、Ansible 和 Jenkins

使用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的课程。
免责声明:虽然本文将带您完成该项目的步骤,但它并不是一个教程,而是我最近的一个副项目的文档。如果您收到任何错误,请不要发送仇恨邮件。

先决条件

开始设置环境

创建您的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
    三个最好的朋友:Terraform、Ansible 和 Jenkins_第1张图片
  • 实例:选择您的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
    三个最好的朋友:Terraform、Ansible 和 Jenkins_第2张图片
  • 命名您的workspace,然后创建workspace
  • 复制workspace提供的示例代码并将其添加到backends.tf以替换当前后端
  • 通过单击Remote并选择Local来设置Execution Mode
    image.png

创建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

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

    [hosts]
    localhost
    [hosts:vars]
    ansible_connection=local
    ansible_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
三个最好的朋友:Terraform、Ansible 和 Jenkins_第3张图片

开放8080端口

  1. 在AWS控制台中导航到您的Cloud9的Security Group。
  2. 开放8080端口,范围0.0.0.0/0,面向全世界。这是允许我们访问Jenkins服务器所必需的,而且它解决了我们稍后会遇到的GitHub Webhook问题。这就是为什么它向世界开放,而不仅仅是一个IP。
  3. 在浏览器中导航到 :8080进行测试。
    三个最好的朋友:Terraform、Ansible 和 Jenkins_第4张图片

配置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
    三个最好的朋友:Terraform、Ansible 和 Jenkins_第5张图片
  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来安装该应用程序
    三个最好的朋友:Terraform、Ansible 和 Jenkins_第6张图片
  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/
  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
    image.png
  2. 点击Webhooks
  3. 点击Add webhook
    三个最好的朋友:Terraform、Ansible 和 Jenkins_第7张图片
  4. Paylod 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
    三个最好的朋友:Terraform、Ansible 和 Jenkins_第8张图片
  3. 接下来配置pipeline:
  4. Display Name =
  5. Branches Sources = GitHub
  6. GitHub Credentials = 选择GitHub App Credentials
  7. 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文件。
三个最好的朋友:Terraform、Ansible 和 Jenkins_第9张图片

条件和输入

如果分支等于dev,则使用when的条件来确定是否运行输入。如果分支不等于dev,则跳过输入。
输入用于暂停pipeline并等待手动选择Apply planAbort
三个最好的朋友:Terraform、Ansible 和 Jenkins_第10张图片

EC2等待

这可能是我从课程中学到的最方便的物品之一。在Inventory和EC2等待阶段下方都使用shell脚本来获取Terraform输出,并使用jq来获取实例ID和实例的IP。Inventory阶段传递从Terraform代码创建的实例IP,并将它们附加到Ansible将使用的aws_hosts文件中,从而创建一个动态清单文件。EC2等待阶段执行相同的操作,但传递实例ID并执行等待命令。这将验证实例是否已完成初始化,然后再转到执行Ansible的下一步。
三个最好的朋友:Terraform、Ansible 和 Jenkins_第11张图片

整合所有

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
    三个最好的朋友:Terraform、Ansible 和 Jenkins_第12张图片
  7. 将鼠标悬停在Validate Apply步骤上,单击Apply plan
    三个最好的朋友:Terraform、Ansible 和 Jenkins_第13张图片
  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] ubuntu@[instance ip]
    image.png
  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的另一个分支。
    三个最好的朋友:Terraform、Ansible 和 Jenkins_第14张图片
  5. 单击main以查看运行中的pipeline。由于代码已经在开发pipeline中进行了测试,Jenkinsfile将根据设置的条件跳过验证步骤。pipeline应该一直移动到Validate Destroy阶段。
    三个最好的朋友:Terraform、Ansible 和 Jenkins_第15张图片
  6. 获取Docker实例IP,类似于测试开发pipeline的方式并验证Docker是否已安装。一旦确认返回pipeline并销毁。

附加测试

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

故障排除

注意:请再次查看本文开头的免责声明。如果您偏离上述步骤,那么您可能会得到不同的结果。例如,如果您在Cloud9上使用您最喜欢的IDE,或者如果您使用Amazon Linux而不是Ubuntu。所有这些都可能改变你的结果和应该采取的步骤。
我已尽力捕捉过程中的所有步骤。所有这些因素都会改变所需的步骤。如果您一直在跟进并遇到问题,请返回您的步骤并验证您是否修改了指定的变量。还要检查创建的所有凭据。当我最初经历这一切时,这似乎就是我所有的问题所在。如果有错误,我建议检查日志并查看错误以找到解决方案。我是人,我有可能错过了一步。

你可能感兴趣的:(三个最好的朋友:Terraform、Ansible 和 Jenkins)