亚马逊AWS官方博客

Jenkins集成AWS开发者服务构建端到端CICD流水线

1.介绍

AWS推出的开发者服务,包括了CodePipeline, CodeCommit, CodeBuild, CodeDeploy, CodeStar等DevOps工具集,实现了建立自动化CI/CD流水线。但是一般企业用户或多或少都已经有部分自己正在使用的CI/CD工具,比较常用的如Jenkins。企业用户需要的是一条可自由定制的流水线,补全自己CI/CD流水线的不足。

本文基于AWS开发者服务包括CodeCommit、CodeDeploy,再结合中国企业客户常用的主流开源CI/CD工具比如Jenkins、 Sonar、Maven等,演示在AWS云上构建自动化CI/CD流水线。

2.环境说明

  • 支持Java语言、开发框架选择Spring Boot,应用程序示例为Java后端查询的API接口,可以从Github上下载
  • 代码使用亚马逊CodeCommit托管,代码静态检查使用Sonar,流水线管道采用了Jenkins,代码构建使用Maven,单元测试框架是Junit,部署工具使用AWS CodeDeploy
  • 支持将通过测试的Java应用程序部署到亚马逊云上Autoscaling组的EC2服务器中

 3.系统架构

此解决方案可在由西云数据运营的亚马逊云科技(宁夏)区域或由光环新网运营的亚马逊云科技(北京)区域中部署,也可以在海外区域部署。

当流水线创建成功后,用户就可以利用本地的代码编辑IDE比如Eclipse或者亚马逊云上提供的Cloud9完成Java代码的拉取,编辑和提交代码,从而触发流水线执行。CI/CD流水线中从Jenkins从CodeCommit拉取代码、编译代码、静态扫描、单元测试、集成测试、打包、把产物保存在S3、调用CodeDeploy执行部署到Auto Scaling组的EC2上。

 4. 部署步骤:

  • 终端环境准备
  • 创建存储桶
  • 创建CodeCommit存储库
  • 创建Jenkins
  • 配置CodeDeploy服务
  • 创建系统运行环境

4.1终端环境准备

请确保本地终端或能访问AWS服务的终端已安装AWS cli并配置的账号的访问凭证,使用 Linux 和有 root 权限,并且使用x86架构时,可以使用以下命令进行安装

curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

使用命令行aws configure配置访问凭证。

4.2创建存储桶

在终端中运行以下命令,创建S3存储桶用于存放编译后的包,其中<123456789000>替换为自己的aws账户id(控制台右上角可以找到):

aws s3 mb s3://devops-template-build-packages-<123456789000>

4.3创建CodeCommit存储库

  • 打开AWS管理控制台,进入CodeCommit服务

  • 选择右边菜单栏存储库,点击创建存储库, 输入存储库名字devops-template-project,及其他可选配置,点击创建

  • 默认用当前用户访问存储库,用户需要具有CodeCommit访问权限,也可以创建访问该存储库的新用户
  • 点击当前用户,进入用户配置详细页面,点击安全证书子页面

  • 安全证书设置页面中生成AWSCodeCommit的HTTPS Git凭证,保存下载用户和密码

  • 在本地终端命令行执行以下命令,将pipeline及脚手架工程下载到本地:

git clone https://github.com/JiangKeJacky/aws-cicd-pipeline.git

Aws-cicd-pipeline文件包含两部分:pipeline包含构建pipeline环境执行脚本和代码;spring-test是一个包含单元测试、部署规范、部署脚本的spring工程脚手架,用户可根据需要在脚手架的基础上构建工程。

IDE中配置存储库地址及访问用户名和密码,把示例代码提交到存储库。也可以使用命令行如下,其中<username>和<password>为上一步创建的用户名和密码:

cp -r aws-cicd-pipeline/spring-tests ./
cd spring-tests
git init
git add .
git commit -m "first commit"
git remote add origin https://<username>:<password>@git-codecommit.ap-southeast-1.amazonaws.com/v1/repos/devops-template-project
git push -u origin master
  • 在AWS控制台,进入CodeCommit,存储库devops-template-project中,查看脚手架代码是否上传成功

4.4创建Jenkins

为简化安装配置,本文将Jenkins/Sonar/maven环境配置完,打包成一个映像AMI,可通过AMI启动EC2快速完成配置,也可以自己擦考Jenkins、Sonar、Maven官网说明在EC2上安装配置。

  • 进入EC2服务页面,点击右侧菜单映像AMI,选择公有映像,筛选AMI ID: ami-09ee66d2d7dd20ad1(中国区),ami-0c60e8d2f933706e3(海外区),

获取 Jenkins-Sonar 的 AMI 镜像

  • 从 Jenkins-Sonar 的 AMI 启动 EC2 新实例,点击操作->启动
  • 选择 t2.large 的类型,EC2类型可根据工作负载选择调整

  • 选择自动分配公网 IP,IAM角色选择具有S3存储桶读写权限、CodeCommit、CodeDeploy访问权限的角色,其他用默认选项

  • 安全组入口开放 22,8080 和 9000 端口,22 端口用于远程 SSL 连接,8080 端口用于 Jenkins 访问,9000 用于 Sonar 访问。来源为允许访问的地址。注:由于中国区互联网域名访问需要ICP备案,因此在中国区不能正常访问使用可限制内网地址访问或者通过IP地址访问。
  • 实例启动后,可在终端通过 ssh 连接到实例,ssh -i ee-default-keypiart.pem ec2-user@jenkinsserverip
  • 命令行 sudo lsof -i:8080sudo lsof -i:9000 检查 jenkins 和 sonar 是否启动
  • 如果 jenkins 未成功启动,命令行 sudo systemctl start jenkins 启动 jenkins
  • 如果 sonar 未成功启动,命令行 sudo docker start sonarqube999 启动 sonar
  • Jenkins 默认用户名密码为:root/abcd1234,登录后修改密码
  • Sonar 默认用户名密码为:admin/abcd1234,登录后修改密码
  • 在本地浏览器输入EC2 服务器的域名及 sonar 端口号 9000,如http://ec2-13-213-61-124.cn-northwest-1.compute.amazonaws.com:9000
  • 打开本地浏览器输入该 EC2 服务器的域名及 Jenkins 端口号 8080,如http://ec2-13-213-61-124.c-northwest-1.compute.amazonaws.com:8080,将进入Jenkins配置页面

  • 配置 CodeCommit访问凭证,通过 Manage Jenkins->Manage Credentials

选择codecommit-user

点击右边菜单的更新,输入devuser的用户名和密码,并保存。

  • 返回Jenkins DashBoard 中有 3 个预设的 pipeline,本文使用cicd-pipeline-codecommit-codedeploy,点击进入修改 cicd-pipeline-codecommit-codedeploy 配置

  • 点击流水线,修改AWS_S3_BUCKET_CODE_PACKAGE环境变量为准备阶段创建的S3存储桶devops-template-build-packages-<123456789000>

保存该 pipeline

  • cicd-pipeline-codecommit-codedeploy的pipeline脚本定义如下:
import groovy.json.JsonSlurper

pipeline {
   agent any

       options {
           timeout(time: 30, unit: 'MINUTES')
       }

    environment {
        PROJECT_NAME            = "spring-test-unit"
        JENKIN_ROLE_NAME        = 'role-Deploy'
        ROOT_BACKEND_PATH       = "${WORKSPACE}"

        RUN_ENV                 = "prd"
        VERSION_NUMBER          = "0.0.${BUILD_NUMBER}.${RUN_ENV}"
        MVN_PACKAGE_NAME        = "target/${PROJECT_NAME}-0.0.1-SNAPSHOT.zip"
        APPLICATION_DIR         = "${PROJECT_NAME}"

        //GIT repository
        GIT_REPOSITORY = "https://git-codecommit.ap-southeast-1.amazonaws.com/v1/repos/devops-template-project.git"

        AWS_DEFAULT_REGION="ap-southeast-1"

        //s3 bucket for store the code package, should be created before the pipeline first run
        AWS_S3_BUCKET_CODE_PACKAGE      = "devops-template-build-packages-123456789000"

        //initialize codedeploy for a pipeline should be done before the pipeline first run
        AWS_DEPLOYMENT_APPLICATION_NAME = "SpringBoot_Test"
        AWS_DEPLOYMENT_GROUP            = "SpringBoot_DepGroup"
        PATH = "/usr/local/lib/apache-maven-3.8.1/bin:$PATH"

        //setting for sonar
        SONAR_SERVER = "http://localhost:9000"
        SONAR_TOKEN = "d4c1d7d45ac038ccfcee734f1c20c9ef43a5d535"

        DEPLOY_ID = ""
    }

    stages {

        stage('PullSource') {
            steps {
                //credential should be created in jenkins global credential, with user and password generated by codecommit
                git credentialsId: 'codecommit-user', url: "${GIT_REPOSITORY}"
            }
        }

        stage('SourceScan'){
            steps{
                sh "mvn clean verify sonar:sonar  -Dsonar.host.url=${SONAR_SERVER} -Dsonar.login=${SONAR_TOKEN} -Pcoverage"
            }
        }

        stage('Compile') {
            steps {
                //代码编译
                sh "mvn clean compile"
            }
        }

        stage('UnitTest') {
            steps {
                //单元测试
                sh "mvn -Dtest=com.mgiglione.service.test.unit.UnitTests test"

            }
        }

        stage('IntegrationTest') {
            steps {
                //集成测试
                sh "mvn -Dtest=com.mgiglione.service.test.integration.IntegrationTests test"
            }

        }

        stage('Package') {
            steps {
                //代码打包
                sh "mvn -Dmaven.test.skip=true package"

            }
        }

        stage('PackageToS3' ) {
            steps{

                    script{

                        def UPLOAD_SCRIPT = '''
                        aws s3 ls
                        aws s3 cp ${MVN_PACKAGE_NAME} s3://${AWS_S3_BUCKET_CODE_PACKAGE}/${APPLICATION_DIR}_${VERSION_NUMBER}.zip
                        '''
                        sh UPLOAD_SCRIPT
                        echo "VERSION_NUMBER:  ${VERSION_NUMBER}"
                    }
            }
        }

       stage('Deploy') {
          steps{
            script{

               def DEPLOY_SCRIPT = '''
                        aws --region ${AWS_DEFAULT_REGION} deploy create-deployment \
                        --application-name ${AWS_DEPLOYMENT_APPLICATION_NAME} \
                        --deployment-group-name ${AWS_DEPLOYMENT_GROUP} \
                        --deployment-config-name CodeDeployDefault.OneAtATime \
                        --s3-location bucket=${AWS_S3_BUCKET_CODE_PACKAGE},key=${APPLICATION_DIR}_${VERSION_NUMBER}.zip,bundleType=zip
                        '''
              result = sh returnStdout: true, script: DEPLOY_SCRIPT
              result = result.trim()
              echo result
              def deployment = new JsonSlurper().parseText(result)
              if (deployment == null) error 'Deploy fail'
              DEPLOY_ID =  deployment.deploymentId
                //result = sh returnStdout: true, script: DEPLOY_STATUS
              echo DEPLOY_ID
              if (DEPLOY_ID == null || DEPLOY_ID == "") error 'Deploy fail'
            }

            timeout(2) {
                waitUntil {
                    script {
                        def DEPLOY_STATUS = '''aws deploy get-deployment --deployment-id '''.concat("${DEPLOY_ID}").concat(''' --query "deploymentInfo.status" --output text ''')
                        //sh DEPLOY_STATUS
                        result = sh returnStdout: true, script: DEPLOY_STATUS
                        echo "Deploy status: " + result
                        if (result.trim().contains("Failed")) {
                            error 'Deployment failed'
                            return true
                        }
                        if (result.trim().contains("Succeeded")) {
                            echo "Deployment succeeded"
                            return true
                        }

                        return false
                    }

                    }
              }
          }
       }

       stage('AutomaticTest') {
            steps {
                sleep 5
                script{
                    def TEST_SCRIPT = '''
                        cd /home/jenkins
                        python3 -m robot --outputdir results robot/ACT.robot
                    '''
                    sh TEST_SCRIPT
                }
            }
        }
   }

   post {
       success {
            emailext(to: 'kejian@amazon.com',
                subject: '[CICD] ${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS}!',
                body: '${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS}!. Check console output at ${BUILD_URL} to view the results.',
                compressLog: true, attachLog: true,
                attachmentsPattern: 'report.html')
           //mail body: 'Jenkins Pipeline Build #${env.BUILD_NUMBER} Finished. Check console output at $BUILD_URL to view the results.', from: 'kejian@amazon.com', replyTo: 'kejian@amazon.com', subject: 'mail ${env.PROJECT_NAME - Build # ${env.BUILD_NUMBER} - ${env.BUILD_STATUS}!', to: 'kejian@amazon.com'

       }
       failure {
            emailext(to: 'kejian@amazon.com',
                subject: '[CICD] ${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS}!',
                body: '${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS}!. Check console output at ${BUILD_URL} to view the results.',
                compressLog: true, attachLog: true,
                attachmentsPattern: 'report.html')
       }
   }

}

其中,与Codecommit服务集成代码如下:

git credentialsId: 'codecommit-user', url: "${GIT_REPOSITORY}"

与CodeDeploy服务集成调用aws deploy create-deployment命令,由于CodeDeploy服务是异步调用,采用waitUntil语句查询部署服务的状态,具体调用了aws deploy get-deployment。在AutomaticTest阶段集成的自动测试框架RobotFramework可以根据需要选择使用。在Pipeline执行完成时,Post阶段可以发送邮件,此功能需要配置Jenkins的smtp服务。

4.5配置CodeDeploy服务

在本地终端中创建IAM角色和策略。如果在中国区使用请将create-codedeploy-role.sh和create-codedeploy-project.sh中资源路径arn:aws修改为arn:aws-cn,例如:create-codedeploy-project.sh中 arn:aws:iam:: <123456789000>:role/CodeDeployServiceRole 在中国区使用修改arn为: arn:aws-cn:iam:: <123456789000>:role/CodeDeployServiceRole,  <123456789000> 修改为自己的 aws 账号

cd ../aws-cicd-pipeline/pipeline
vim create-codedeploy-project.sh

执行脚本:

./create-codedeploy-role.sh
./create-codedeploy-project.sh

4.6创建系统运行环境

  • 执行以下命令,将自动创建3台EC2服务器:
aws ec2 create-security-group --group-name CodeDeployDemo-SG --description "CodeDeployDemo test security group"

输出如下:

{
“GroupId”: “sg-0177142e880071b22
}

GroupId值”sg-0177142e880071b22”在后面命令中使用

aws ec2 authorize-security-group-ingress \
    --group-name CodeDeployDemo-SG \
    --protocol tcp \
    --port 22 \
    --cidr 0.0.0.0/0

aws ec2 authorize-security-group-ingress \
    --group-name CodeDeployDemo-SG \
    --protocol tcp \
    --port 8080 \
    --cidr 0.0.0.0/0

aws autoscaling create-launch-configuration \
  --launch-configuration-name CodeDeployDemo-AS-Configuration \
  --image-id ami-0e8e39877665a7c92 \
  --key-name ee-default-keypair \
  --security-groups <sg-0177142e880071b22> \
  --iam-instance-profile CodeDeployDemo-EC2-Instance-Profile \
  --instance-type t3.small
<sg-0177142e880071b22>为在第一条命令行中输出的“GroupId”值
aws autoscaling create-auto-scaling-group \
  --auto-scaling-group-name CodeDeployDemo-AS-Group \
  --launch-configuration-name CodeDeployDemo-AS-Configuration \
  --min-size 3 \
  --max-size 3 \
  --desired-capacity 3 \
  --vpc-zone-identifier "<subnet-f4fb7c92>,<subnet-3003b778>" \
  --tags Key=Name,Value=CodeDeployDemo,PropagateAtLaunch=true

aws ssm create-association \
  --name AWS-ConfigureAWSPackage \
  --targets Key=tag:Name,Values=CodeDeployDemo \
  --parameters action=Install,name=AWSCodeDeployAgent \
  --schedule-expression "cron(0 2 ? * SUN *)"

vpc-zone-identifier在VPC服务的子网页面可以找到

创建应用负载均衡器ALB监听80端口

创建目标组,目标组端口为8080(默认springboot启动的web应用端口),注册上面创建的3台EC2服务器。

5.运行使用Pipeline

  • 在jenkins的cicd-pipeline-codecommit-codedeploy中,点击build now开始新的构建

在浏览器中输入:http://:8080/manga/sync/ken  验证部署的应用是否返回关键字为ken的查询结果。

  • 修改spring-tests/src/main/java/com/example/controller/MangaController.java的代码

或者

cd ../../spring-tests
vim src/main/java/com/example/controller/MangaController.java

加入以下接口,并保存。

@RequestMapping(value = "/helloworld", method = RequestMethod.GET)
public @ResponseBody String sayHello()
{
    return "Hello world!";
}

提交到 CodeCommit, 将触发 Jenkins 的 CICD pipeline,在终端中执行以下命令

git commit -a -m "add new interface helloworld"
git push origin master 

在 Jenkins 中查看pipeline构建进度,pipeline完成后在浏览器中输入http://<serverip>:8080/manga/helloworld 验证部署的应用。

  • 点击某个 build,在 pipeline 的 Console Out 中查看下详细的输出,包括代码扫描、单元测试、集成测试、编译、打包等过程。

  • 由于 CodeDeploy 是调用异步执行,CodeDeploy 的详细执行情况在 CodeDeploy 控制台中查看。

6.总结

本文介绍了使用Jenkins,集成AWS CodeCommit、S3 和 CodeDeploy服务,构建自动化CI/CD流水线,支持Java应用发布到EC2服务器上。基于这个流水线有很多扩展空间,比如:

  • 集成CodeBuild服务
  • 蓝绿部署
  • 权限控制

本文提供AMI中预置的pipeline也包括了打包容器部署EKS及前端构建的示例。希望基于本文,读者可以快速集成AWS开发者服务建立起自己的CICD流水线。

本篇作者

姜可

亚马逊云科技资深解决方案架构师,负责协助客户业务系统上云的解决方案架构设计和咨询,现致力于DevOps、IoT、机器学习相关领域的研究。在加入亚马逊云科技之前,曾在金融、制造、政府等行业耕耘多年,对相关行业解决方案和架构有很深的理解。