Use AWS Lambda to create CI/CD dependencies with CodePipeline

June 19, 2020

Use AWS Lambda to create CI/CD dependencies with CodePipeline

At Sentia we like to make everybody’s lives easier, by trying to fully automate our solutions. We are using a lot of tools and services to do so, including AWS CDK and AWS CodePipeline. When we started to offer Kubernetes solutions to our customers, one CodePipeline pipeline was not enough to do the job, because one pipeline with multiple purposes will add complexity to the solution and it will create unwanted dependencies.

Separating the pipelines per purpose, will remove complexity from the pipeline design, will create separate processes and will decouple the components, will be easier to debug, and it will improve the change process by only changing the part you need.

In our case, we wanted to separate the infrastructure deployments, from the applications (pods) deployments for K8s, because we want to have all the infrastructure up to date with the latest changes, before we deploy the application. And, if the only modification made is at the application level, there is no need to update the infrastructure as well.

Although having different pipelines for different purposes brings many benefits, it does come with some challenges as well. The main challenge is managing all the pipelines. This is because some of them will require more time to run, and some of them will require less. And, usually when this happens, you might need them to run in a specific order, so the result of one pipeline can be used as an input for the next one, and so on. In order to obtain this, some pipeline dependencies need to be created. To do this in a professional and cost-effective manner, following the recommended best practices from AWS, leads us to use AWS Lambda functions.

Back to our use case, we have the main pipeline, the one responsible for deploying all the underlying infrastructure, and needs more time to complete than the K8s pipeline that is used for the pods deployment. The first one is responsible for updates of the underlying infrastructure, including K8s infrastructure and it should finish to update successfully all the stages before the second one starts running. This way, the K8s pipeline will have the latest updates on the infrastructure and can run safely.

The plan is to create a new stage in the K8s pipeline and add an action to trigger one lambda function. This first lambda function is the main lambda and will be responsible to check if the main pipeline is running, and if it isn’t running, let the process continue.

# put scheduled rule
cw_events.put_rule(
    Name=rule_name,
    Description='LambdaEventRule',
    ScheduleExpression=f'cron({scheduled_minute} {scheduled_hour} * * ? *)',
    State='ENABLED'
)

# put target for rule
cw_events.put_targets(
    Rule=rule_name,
    Targets=[
        {
            'Arn': k8s_lambda_arn,
            'Id': target_id,
        }
    ]
)

# put event
cw_events.put_events(
    Entries=[
        {
            'Detail': '{}',
            'DetailType': 'ScheduledEvent',
            'Resources': [k8s_lambda_arn]
        }
    ]
)
response = cicd.put_job_failure_result(
    jobId=event['CodePipeline.job']['id'],
    failureDetails={
        'type': 'JobFailed',
        'message': 'The main pipeline is running',
    }
)
return response

To control the ScheduledExpression parameter, a cron expression is being used to schedule an execution 5 minutes after the current time.

now = datetime.now()
current_hour = int(now.strftime("%H"))
current_minute = int(now.strftime("%M"))
scheduled_minute = current_minute + 5

If the main pipeline is running, it will trigger a CloudWatch Event that will have another lambda function as a target, let’s call it K8s Lambda. The CloudWatch Event will have a rule that is being triggered by the scheduled expression that we talked about above, and it has as target the K8s Lambda function. This function will have as scope to first delete the event, and then to trigger the K8s pipeline again. In order to delete the event, it must first delete the events targets. The event deletion will ensure that we can always trigger the K8s Lambda, 5 minutes after the event was created.

cw_events.remove_targets(
    Rule=rule_name,
    Ids=[target_id]
)

cw_events.delete_rule(
    Name=rule_name
)

cicd.start_pipeline_execution(
    name=os.getenv('K8S_CICD_NAME')
)

These checks will be done until the main pipeline has finished running (first scenario is being valid), and the Main Lambda will allow: the stage to be validated and the CodePipeline to move to the next stage in the K8s pipeline.

The decision tree looks like this:

CICD dependencies for AWS - Decision tree

Having separate pipelines for different purposes, simplifies any complex solution and brings automation and flexibility to it. You can now achieve decoupled infrastructure, simplified debug process, more control over the changes, to run only what you need, when you need it. As any change, this comes with some challenges. But when you have the right tools and some creativity, these challenges can be transformed in great solutions for the future.

Andra Glavan

Andra Glavan

Cloud Systems Consultant