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:8080和sudo 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创建系统运行环境
 
        
        
        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服务器上。基于这个流水线有很多扩展空间,比如:
 
        
       本文提供AMI中预置的pipeline也包括了打包容器部署EKS及前端构建的示例。希望基于本文,读者可以快速集成AWS开发者服务建立起自己的CICD流水线。
 
       本篇作者