Okay,… The pipeline has already two steps “Build” and “Deploy” running, but the last step “Test” is missing. In this part I will show a simple example with Python, Selenium and Docker (standalone-chrome) for test step.
Preconditions
Install additional packages on AWS EC2
There is a need to install additional packages on AWS EC2 Linux instance (Jenkins).
# start ssh connection $ ssh -i ~/.ssh/ExampleKeyPair.pem ec2-user@<EC2 IP|DNS> # change to root user $ sudo su - # install python pip $ easy_install pip # install virtualenv $ pip install virtualenv # exit root and go back to ec2-user $ exit # exit ec2-user (ssh connection) $ exit
Create new files and folder (Project/Repository)
You need to create a new directory called “test”. Inside that directory you will create a file “example.py” with following content.
#!/usr/bin/env python import unittest from selenium import webdriver class ExampleTest(unittest.TestCase): def setUp(self): """Start web driver""" options = webdriver.ChromeOptions() options.add_argument('--no-sandbox') options.add_argument('--headless') options.add_argument('--disable-gpu') self.driver = webdriver.Remote('http://0.0.0.0:4444/wd/hub', options.to_capabilities()) self.driver.get("APPLICATION_URL") def test_search_headline(self): """TestCase 1""" title = 'DemoPipeline' assert title in self.driver.title def test_search_text(self): """TestCase 2""" element = self.driver.find_element_by_tag_name('body') assert element.text == 'Hello world...' def tearDown(self): """Stop web driver""" self.driver.quit() if __name__ == "__main__": unittest.main(verbosity=2)
When you are done you have to modify the “Jenkinsfile” and the bash script “test.sh”.
pipeline { agent any parameters { string(name: 'REPONAME', defaultValue: 'example/nginx', description: 'AWS ECR Repository Name') string(name: 'ECR', defaultValue: '237724776192.dkr.ecr.eu-central-1.amazonaws.com/example/nginx', description: 'AWS ECR Registry URI') string(name: 'REGION', defaultValue: 'eu-central-1', description: 'AWS Region code') string(name: 'CLUSTER', defaultValue: 'ExampleCluster', description: 'AWS ECS Cluster name') string(name: 'TASK', defaultValue: 'ExampleTask', description: 'AWS ECS Task name') } stages { stage('BuildStage') { steps { sh "./cicd/build.sh -b ${env.BUILD_ID} -n ${params.REPONAME} -e ${params.ECR} -r ${params.REGION}" } } stage('DeployStage') { steps { sh "./cicd/deploy.sh -b ${env.BUILD_ID} -e ${params.ECR} -c ${params.CLUSTER} -t ${params.TASK}" } } stage('TestStage') { steps { sh "./cicd/test.sh -c ${params.CLUSTER} -t ${params.TASK}" } } } }
#!/usr/bin/env bash ## shell options set -e set -u set -f ## magic variables declare CLUSTER declare TASK declare TEST_URL declare -r -i SUCCESS=0 declare -r -i NO_ARGS=85 declare -r -i BAD_ARGS=86 declare -r -i MISSING_ARGS=87 ## script functions function usage() { local FILE_NAME FILE_NAME=$(basename "$0") printf "Usage: %s [options...]\n" "$FILE_NAME" printf " -h\tprint help\n" printf " -c\tset esc cluster name uri\n" printf " -t\tset esc task name\n" } function no_args() { printf "Error: No arguments were passed\n" usage exit "$NO_ARGS" } function bad_args() { printf "Error: Wrong arguments supplied\n" usage exit "$BAD_ARGS" } function missing_args() { printf "Error: Missing argument for: %s\n" "$1" usage exit "$MISSING_ARGS" } function get_test_url() { local TASK_ARN local TASK_ID local STATUS local HOST_PORT local CONTAINER_ARN local CONTAINER_ID local INSTANCE_ID local PUBLIC_IP # list running task TASK_ARN="$(aws ecs list-tasks --cluster "$CLUSTER" --desired-status RUNNING --family "$TASK" | jq -r .taskArns[0])" TASK_ID="${TASK_ARN#*:task/}" # wait for specific container status STATUS="PENDING" while [ "$STATUS" != "RUNNING" ]; do STATUS="$(aws ecs describe-tasks --cluster "$CLUSTER" --task "$TASK_ID" | jq -r .tasks[0].containers[0].lastStatus)" done # get container id CONTAINER_ARN="$(aws ecs describe-tasks --cluster "$CLUSTER" --tasks "$TASK_ID" | jq -r .tasks[0].containerInstanceArn)" CONTAINER_ID="${CONTAINER_ARN#*:container-instance/}" # get host port HOST_PORT="$(aws ecs describe-tasks --cluster "$CLUSTER" --tasks "$TASK_ID" | jq -r .tasks[0].containers[0].networkBindings[0].hostPort)" # get instance id INSTANCE_ID="$(aws ecs describe-container-instances --cluster "$CLUSTER" --container-instances "$CONTAINER_ID" | jq -r .containerInstances[0].ec2InstanceId)" # get public IP PUBLIC_IP="$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" | jq -r .Reservations[0].Instances[0].PublicIpAddress)" TEST_URL="$(printf "http://%s:%d" "$PUBLIC_IP" "$HOST_PORT")" } function clean_up() { # stop container if [ "$(docker inspect -f {{.State.Running}} ChromeBrowser)" == "true" ]; then docker rm -f ChromeBrowser fi # delete virtualenv if [ -d .env ]; then rm -fr .env fi } function run_selenium_test() { local TEST_TEMPLATE local TEST_FILE # clean up clean_up # pull image (standalone-chrome) docker pull selenium/standalone-chrome # run docker container (standalone-chrome) docker run -d -p 4444:4444 --name ChromeBrowser selenium/standalone-chrome # create and activate virtualenv virtualenv .env && source .env/bin/activate # install Selenium pip install -U selenium # read test template into variable TEST_TEMPLATE=$(cat ./test/example.py) # replace string with URL TEST_FILE="${TEST_TEMPLATE/APPLICATION_URL/$TEST_URL}" # save into final test file echo "$TEST_FILE" > ./test/suite.py # execute test python -B ./test/suite.py # deactivate virtualenv deactivate } ## check script arguments while getopts "hc:t:" OPTION; do case "$OPTION" in h) usage exit "$SUCCESS";; c) CLUSTER="$OPTARG";; t) TASK="$OPTARG";; *) bad_args;; esac done if [ "$OPTIND" -eq 1 ]; then no_args fi if [ -z "$CLUSTER" ]; then missing_args '-c' fi if [ -z "$TASK" ]; then missing_args '-t' fi ## run main function function main() { get_test_url printf "Test Application URL: %s\n" "$TEST_URL" run_selenium_test } main # exit exit "$SUCCESS"
Ensure that “example.py” has all needed permission rights. $ chmod +x example.py
Commit all changes now and wait that the Jenkins job gets triggered (or trigger manually).
That’s already all… your job should execute all steps. This part is done super fast.
Some last words
There is a lot of space for improvements here, but I think you learned already much and had some fun. Some hints now:
- you can add any other test methods by your self on this step (eq. Performance- and Security tests)
- Unit tests and Static Code Analysis could executed on build step (before create image)
- check out AWS ECS Services
- use a proxy for Jenkins and enable SSL
- create other pipelines and ECS clusters to enable staging
- create “Lifecycle policy rules” on ECR
- use Git Webhook’s to trigger the Jenkins jobs
- add a post step in your Jenkins pipeline to store metrics and/or inform about build status