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流水线。
本篇作者