搭建AWS codebuild无服务器cicd接口

需求:用于提供对cicd部署有稳定性高要求的项目备选方案。
背景:自建cicd的流程,jenkins,虽然说在构建速度上优于codebuild,但是Jenkins会有偶然的宕机情况发生,导致一大片的服务出现无法发布的问题,所以需要有像codebuild一样高稳定性的无服务器构建方式。

整体架构
搭建AWS codebuild无服务器cicd接口

如图,配置了相应webhook的某个gitlab有push事件,将触发lambda中的gitpull函数去拉取该项目的项目信息,整个cicd流程为了安全都使用了密钥的方式去传输数据包,所有加密的行为均为lambda中的另一个函数createsshkey去请求kms服务创建并读取密钥,而后gitpull将拉取到的git代码以及改项目仓库的所有信息(包括分支,项目名等等)发送给codebuild来对项目进行构建,最后,由codebuild去拉取该项目的源代码,并将构建好的镜像push到ecr镜像仓库,并操作eks,将镜像仓库中最新版本的镜像发布到集群中对应的deployment。

创建流程

AWS官方提供了一个demo,一个能够实现将gitlab同步到s3桶中并且将源码打包的cloudformation(aws服务堆栈)。
具体创建流程参考官方文档:https://docs.aws.amazon.com/codepipeline/latest/userguide/tutorials-simple-s3.html
在创建的过程中注意对可用区进行修改。
搭建AWS codebuild无服务器cicd接口

等待整个堆栈创建完成后,对lambda和codebuild进行自定义的修改,首先,现在已有的条件是,配置有webhook的gitlab库的push事件会触发lambda的运行。lambda会将项目信息发送给codebuild,codebuild凭借信息去拉取正确分支的代码进行构建。这样一分析,其实,最需要修改的就是codebuild 的内容,关于lambda的修改可以根据后去项目构建的需要再去自定义。

下面是codebuild spec内容

version: 0.2
env:
  exported-variables:
    - GIT_COMMIT_ID
    - GIT_COMMIT_MSG
phases:
  install:
      runtime-versions:
          python: 3.7
      # commands:
      # - pip3 install boto3
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com      
  build:
      commands:
      - echo "=======================Start-gitpull============================="
      - echo "Getting the SSH Private Key"
      - |
        python3 - << "EOF"
        from boto3 import client
        import os
        s3 = client('s3')
        kms = client('kms')
        enckey = s3.get_object(Bucket=os.getenv('KeyBucket'), Key=os.getenv('KeyObject'))['Body'].read()
        privkey = kms.decrypt(CiphertextBlob=enckey)['Plaintext']
        with open('enc_key.pem', 'w') as f:
            print(privkey.decode("utf-8"), file=f)
        EOF
      - mv ./enc_key.pem ~/.ssh/id_rsa
      - ls ~/.ssh/
      - echo "Setting SSH config profile"
      - | 
        cat > ~/.ssh/config <<EOF
        Host *
          AddKeysToAgent yes
          StrictHostKeyChecking no
          IdentityFile ~/.ssh/id_rsa
        EOF
      - chmod 600 ~/.ssh/id_rsa
      - echo "Cloning the repository $GitUrl on branch $Branch"
      - git clone --single-branch --depth=1 --branch $Branch $GitUrl .
      - ls -alh
      - export projectname=$(echo $outputbucketpath | tr '/' ' ' | awk '{print $2}')      
      - |
        if [ "$Branch" = "deploy-staging" ]; then
          export repo_name=$projectname"-staging"
          export env="stage"
        elif [ "$Branch" = "deploy-production" ]; then
          export repo_name=$projectname"-production"
          export env="prod"
        else
          return 1
        fi
      - |
        if [ "$projectname" = "export" ]; then
          export contextname="xxxx-bussiness"
        elif [ "$Branch" = "deploy-staging" ]; then
          export contextname="xxxx-bussiness-non-prod"
        elif [ "$Branch" = "deploy-profuction" ]; then
          export contextname="xxx-bussiness"
        else
          return 1
        fi
      - export GIT_COMMIT_ID=$(git rev-parse --short HEAD)
      - echo $GIT_COMMIT_ID
      - export GIT_COMMIT_MSG="$(git log -1 --pretty=%B)"
      - echo $GIT_COMMIT_MSG
      - pwd
      - aws s3 cp s3:/xxx/xxx/settings.xml /opt/maven/conf/
      - echo "=======================End-gitgull============================="
      - echo "=======================Start-Build============================="
      - |
        export isjava=$(find ./ -name pom.xml)
        if [ "$isjava" = "" ]; then
          echo "it's node project"
          echo "Start Npm install......................................"
          npm install
          echo "Start Npm run build...................................."
          npm run build:$env
        else
          echo "it's java project"
          mvn  clean package -Dmaven.test.skip=true
        fi
      - echo "=======================End-Build============================="
      - echo Build started on `date`
      - echo Building the Docker image...   
      - echo "=====================Start-Docker-build======================"
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$repo_name:$deploymentname_$GIT_COMMIT_ID
      - echo "=========================End-Docker-build======================"
      - docker image ls
      - mkdir ~/.aws
      - mkdir ~/.kube
      - export imageurl=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$repo_name:$deploymentname_$GIT_COMMIT_ID
      - echo $imageurl
      - aws s3 cp s3://xxx/xxx/kube/ ~/.kube/ --recursive
      - aws s3 cp s3://xxx/xxx/awscli/ ~/.aws/ --recursive
      - aws s3 cp s3://xxx/xxx/deploy.sh ./
      - aws s3 cp s3://xxx/xxx/image.json ./
      - sed -i 's#imageurl#'$imageurl'#g' image.json
      - sed -i 's#deploymentname#'$deploymentname'#g' image.json
      - ls -alh ~/
      - chmod +x deploy.sh
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$repo_name:$deploymentname_$GIT_COMMIT_ID
      - kubectl config get-contexts
      - echo $contextname
      - ./deploy.sh $contextname $deploymentname $repo_name

问题解决:
1.codebuild对ecr,eks等资源的权限需要先分配好。
2.在构建过程中如何判断,项目是node项目还是java项目。
3.根据不同的分支去确定发布的环境。
4.maven配置文件,如果有涉及到自定义maven仓库地址,可以从s3将配置文件拷贝到构建环境中。
5.kubectl以及aws的配置文件通过s3拷贝到本地。
6.将kubectl patch步骤文件化,脚本化,来解决codebuild spec对这一步骤cli的限制。

deploy.sh

#!/bin/bash
kubectl --context $1 patch deployment $2 --patch "$(cat image.json)" --namespace $3

image.json

{
	"spec": {
		"template": {
			"spec": {
				"containers": [{
					"name": "deploymentname",
					"image": "imageurl"
				}]
			}
		}
	}
}

gitlab权限配置

使用cloudformation输出的PublicSSHKey 来创建一个ssh key用于身份验证。

lambda代码内容

from boto3 import client
import os
import time
import stat
import shutil
from ipaddress import ip_network, ip_address
import logging
import hmac
import hashlib
import distutils.util

exclude_git = bool(distutils.util.strtobool(os.environ['ExcludeGit']))

cleanup = False

key = 'enc_key'

logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.handlers[0].setFormatter(logging.Formatter('[%(asctime)s][%(levelname)s] %(message)s'))
logging.getLogger('boto3').setLevel(logging.ERROR)
logging.getLogger('botocore').setLevel(logging.ERROR)

s3 = client('s3')
kms = client('kms')


def lambda_handler(event, context):
    print(event)
    keybucket = event['context']['key-bucket']
    outputbucket = event['context']['output-bucket']
    pubkey = event['context']['public-key']
    # Source IP ranges to allow requests from, if the IP is in one of these the request will not be checked for an api key
    ipranges = []
    if event['context']['allowed-ips']:
        for i in event['context']['allowed-ips'].split(','):
            ipranges.append(ip_network(u'%s' % i))
    # APIKeys, it is recommended to use a different API key for each repo that uses this function
    apikeys = event['context']['api-secrets'].split(',')
    ip = ip_address(event['context']['source-ip'])
    secure = False
    if ipranges:
        for net in ipranges:
            if ip in net:
                secure = True
    if 'X-Git-Token' in event['params']['header'].keys():
        print(event['params']['header']['X-Git-Token'])
        if event['params']['header']['X-Git-Token'] in apikeys:
            secure = True
    if 'X-Gitlab-Token' in event['params']['header'].keys():
        if event['params']['header']['X-Gitlab-Token'] in apikeys:
            secure = True
    if 'X-Hub-Signature' in event['params']['header'].keys():
        for k in apikeys:
            if 'use-sha256' in event['context']:
                k1 = hmac.new(str(k).encode('utf-8'), str(event['context']['raw-body']).encode('utf-8'),
                              hashlib.sha256).hexdigest()
                k2 = str(event['params']['header']['X-Hub-Signature'].replace('sha256=', ''))
            else:
                k1 = hmac.new(str(k).encode('utf-8'), str(event['context']['raw-body']).encode('utf-8'),
                              hashlib.sha1).hexdigest()
                k2 = str(event['params']['header']['X-Hub-Signature'].replace('sha1=', ''))
            if k1 == k2:
                secure = True
    # TODO: Add the ability to clone TFS repo using SSH keys
    try:
        # GitHub
        full_name = event['body-json']['repository']['full_name']
    except KeyError:
        try:
            # BitBucket #14
            full_name = event['body-json']['repository']['fullName']
        except KeyError:
            try:
                # GitLab
                full_name = event['body-json']['repository']['path_with_namespace']
            except KeyError:
                try:
                    # GitLab 8.5+
                    full_name = event['body-json']['project']['path_with_namespace']
                except KeyError:
                    try:
                        # BitBucket server
                        full_name = event['body-json']['repository']['name']
                    except KeyError:
                        # BitBucket pull-request
                        full_name = event['body-json']['pullRequest']['fromRef']['repository']['name']
    if not secure:
        logger.error('Source IP %s is not allowed' % event['context']['source-ip'])
        raise Exception('Source IP %s is not allowed' % event['context']['source-ip'])

    # GitHub publish event
    if ('action' in event['body-json'] and event['body-json']['action'] == 'published'):
        branch_name = 'tags/%s' % event['body-json']['release']['tag_name']
        repo_name = full_name + '/release'
    else:
        repo_name = full_name
        try:
            # branch names should contain [name] only, tag names - "tags/[name]"
            branch_name = event['body-json']['ref'].replace('refs/heads/', '').replace('refs/tags/', 'tags/')
        except KeyError:
            try:
                # Bibucket server
                branch_name = event['body-json']['push']['changes'][0]['new']['name']
            except:
                # Bitbucket Server v6.6.1
                try:
                    branch_name = event['body-json']['changes'][0]['ref']['displayId']
                except:
                    branch_name = 'master'
    try:
        # GitLab
        remote_url = event['body-json']['project']['git_ssh_url']
        deploymentname = event['body-json']['repository']['name']
        if deploymentname == 'gateway-api':
            deploymentname = 'ftl-gateway'
    except Exception:
        try:
            remote_url = 'git@' + event['body-json']['repository']['links']['html']['href'].replace('https://',
                                                                                                    '').replace('/',
                                                                                                                ':',
                                                                                                                1) + '.git'
        except:
            try:
                # GitHub
                remote_url = event['body-json']['repository']['ssh_url']
            except:
                # Bitbucket
                try:
                    for i, url in enumerate(event['body-json']['repository']['links']['clone']):
                        if url['name'] == 'ssh':
                            ssh_index = i
                    remote_url = event['body-json']['repository']['links']['clone'][ssh_index]['href']
                except:
                    # BitBucket pull-request
                    for i, url in enumerate(
                            event['body-json']['pullRequest']['fromRef']['repository']['links']['clone']):
                        if url['name'] == 'ssh':
                            ssh_index = i

                    remote_url = \
                    event['body-json']['pullRequest']['fromRef']['repository']['links']['clone'][ssh_index]['href']
    try:
        codebuild_client = client(service_name='codebuild')
        new_build = codebuild_client.start_build(projectName=os.getenv('GitPullCodeBuild'),
                                                 environmentVariablesOverride=[
                                                     {
                                                         'name': 'GitUrl',
                                                         'value': remote_url,
                                                         'type': 'PLAINTEXT'
                                                     },
                                                     {
                                                         'name': 'Branch',
                                                         'value': branch_name,
                                                         'type': 'PLAINTEXT'
                                                     },
                                                     {
                                                         'name': 'KeyBucket',
                                                         'value': keybucket,
                                                         'type': 'PLAINTEXT'
                                                     },
                                                     {
                                                         'name': 'KeyObject',
                                                         'value': key,
                                                         'type': 'PLAINTEXT'
                                                     },

                                                     {
                                                         'name': 'outputbucket',
                                                         'value': outputbucket,
                                                         'type': 'PLAINTEXT'
                                                     },
                                                     {
                                                         'name': 'outputbucketkey',
                                                         'value': '%s' % (repo_name.replace('/', '_')) + '.zip',
                                                         'type': 'PLAINTEXT'
                                                     },
                                                     {
                                                         'name': 'outputbucketpath',
                                                         'value': '%s/%s/' % (repo_name, branch_name),
                                                         'type': 'PLAINTEXT'
                                                     },
                                                     {
                                                         'name': 'exclude_git',
                                                         'value': '%s' % (exclude_git),
                                                         'type': 'PLAINTEXT'
                                                     }
                                                 ])
        buildId = new_build['build']['id']
        logger.info('CodeBuild Build Id is %s' % (buildId))
        buildStatus = 'NOT_KNOWN'
        counter = 0
        while (counter < 60 and buildStatus != 'SUCCEEDED'):  # capped this, so it just fails if it takes too long
            logger.info("Waiting for Codebuild to complete")
            time.sleep(5)
            logger.info(counter)
            counter = counter + 1
            theBuild = codebuild_client.batch_get_builds(ids=[buildId])
            print(theBuild)
            buildStatus = theBuild['builds'][0]['buildStatus']
            logger.info('CodeBuild Build Status is %s' % (buildStatus))
            if buildStatus == 'SUCCEEDED':
                EnvVariables = theBuild['builds'][0]['exportedEnvironmentVariables']
                commit_id = [env for env in EnvVariables if env['name'] == 'GIT_COMMIT_ID'][0]['value']
                commit_message = [env for env in EnvVariables if env['name'] == 'GIT_COMMIT_MSG'][0]['value']
                current_revision = {
                    'revision': "Git Commit Id:" + commit_id,
                    'changeIdentifier': 'GitLab',
                    'revisionSummary': "Git Commit Message:" + commit_message
                }
                outputVariables = {
                    'commit_id': "Git Commit Id:" + commit_id,
                    'commit_message': "Git Commit Message:" + commit_message
                }
                break
            elif buildStatus == 'FAILED' or buildStatus == 'FAULT' or buildStatus == 'STOPPED' or buildStatus == 'TIMED_OUT':
                break
    except Exception as e:
        logger.info("Error in Function: %s" % (e))

上一篇:jenkins在k8s中的CICD(第二版)


下一篇:CICD 流水线就该这么玩系列之一