Last month I started writing about the DevOps pipeline that I built out for a PHP project. Today I plan on filling it out a bit more. What I described last week is what many people consider a full CI Pipeline, executing unit tests, code coverage, and static analysis. I threw in a little more with some dependency checking of third party tools. At Coveros, our SecureAgileTM process encourages us to do more before we hand the code over to QA. We want to smoke and acceptance test the application, along with run a brief security scan. This post will cover how I accomplished these tasks for our PHP application.

Like many applications, in order to execute any real Smoke or Acceptance tests, the application has to be deployed somewhere. I decided to use AWS and Chef to manage our deploys, mainly because it was so simple.

Deploy To DevInt

I chose to go with AWS and Chef, because of the excellent support that AWS has built in for launching instances, and executing chef recipes. I configured a special user on AWS with permissions to launch and terminate a new machine within ec2. I installed knife ec2 and aws cli onto our SecureCITM server, which Rich Mills went over in a previous post. I used the knife ec2 server command to launch an AMI with the specifics that I desired, and to install the chef recipes I wanted. I had previously setup a chef instance, and so I needed the Jenkins user to be connected to this server, and have those credentials installed in [jenkins-home]/.chef/knife.rb. This exercise is left up to you as the user.

To launch a new instance for our DevInt tests I executed the simple command that is below

knife ec2 server create \
    --config ~/.chef/knife.rb \
    --ssh-key jenkins-client \
    -i ~/.chef/jenkins-client.pem \
    --image $ami \
    --flavor $size \
    --winrm-user $user \
    --node-name $taggedName \
    --subnet subnet-b28a9e99 \
    --associate-public-ip \
    --ebs-size $Drive_Size \
    --security-group-ids sg-f6a59890 \
    --run-list $Run_List \
    --json-attributes $JSON \
    --tags "Jenkins Build Number"=$BUILD_NUMBER,"Jenkins Build Id"=$BUILD_ID,"Name"=$taggedName

You’ll notice there are several values being pulled in as variables, such as the size of the machine, what run-list (chef cookbooks/recipes to install), and special attribute). For our application in DevInt, I installed a few applications, including PHP, NGINX, and MySQL, and our actual application. The chef recipe for our application reached out to Jenkins to obtain the packaged file, as I described hosting it in our previous post. Our next post will talk a bit more about installations on different environments. We also used encrypted databags with a recipe to install users for each of our developers onto the system, including their public key for access.

Once this machine was launched, I waited until was running, and obtained some key information about the system.

private_ip=`aws ec2 describe-instances --filters "Name=tag-key,Values=Name" "Name=tag-value,Values=$taggedName" | grep PRIVATEIPADDRESSES | cut -f4`
public_ip=`aws ec2 describe-instances --filters "Name=tag-key,Values=Name" "Name=tag-value,Values=$taggedName" | grep ASSOCIATION | head -1 | cut -f4`
instance_id=`aws ec2 describe-instances --filters "Name=tag-key,Values=Name" "Name=tag-value,Values=$taggedName" | grep INSTANCES | cut -f8`
while [ -z "$private_ip" ]; do
  private_ip=`aws ec2 describe-instances --filters "Name=tag-key,Values=Name" "Name=tag-value,Values=$taggedName" | grep PRIVATEIPADDRESSES | cut -f4`
  public_ip=`aws ec2 describe-instances --filters "Name=tag-key,Values=Name" "Name=tag-value,Values=$taggedName" | grep ASSOCIATION | head -1 | cut -f4`
  instance_id=`aws ec2 describe-instances --filters "Name=tag-key,Values=Name" "Name=tag-value,Values=$taggedName" | grep INSTANCES | cut -f8`
done

This information was needed to pass to our future steps, so that we could access the machine, and eventually shut it down as well. I wrote this information out to a file, and passed this file to the downstream job as the paramters.

rm -rf latest_machine
echo "Private_IP=${private_ip}" >> "latest_machine"
echo "Public_IP=${public_ip}" >> "latest_machine"
echo "Instance_ID=${instance_id}" >> "latest_machine"
echo "Tag_Name=${taggedName}" >> "latest_machine"

Smoke Tests

I wanted some smoke tests that would run quickly for our application, and something that would give us some confidence that our application is up and ready to attempt some simple acceptance tests. I decided to go with exercising our API tests for this. We have about a dozen APIs for the system, which I decided to write some tests for. These tests were mostly non-destructive, and the ones that were destructive, I left out of the smoke test, as I wanted to leave the system in a good/clean state for additional testing. I decided to write these testing using the SecureCITM Testing Framework (expect an update to the framework shortly, with an accompanying post). I ended up with just shy of 100 tests that examined the system from a high level, and ensure that some basic functionality should be present.

In order to kick these tests off, I just ran a simple command gradle smoketest. This gradle command was tied to an ant task, that is defined with the included build.xml with the SecureCITM Testing Framework. Rather than simply kick the tests off, I wanted to ensure that Jenkins was ready with the proper/latest system setup. So my entire job looked like this (substituting gradle for the ant command).

#!/bin/bash

#get our framework if needed
if [ ! -f stf-1.4.0.tgz ]; then
  wget https://secureci.reprophet/nexus/service/local/repositories/thirdparty/content/coveros/stf/1.4.0/stf-1.4.0.tgz >/dev/null 2>&1
  tar zxf stf-1.4.0.tgz
  mv stf-1.4.0/* .
  rm -rf stf-1.4.0
fi

#reset our data
[SOME CUSTOM COMMAND]

#execute our tests
ant clean run replace junit-report -k -DappURL=https://${Private_IP} -Dtest-suite=api.xml

#link to results
echo ""
echo ""
echo "Tests are complete, view detailed reports at ${JOB_URL}/ws/test-output/index.html"

That private IP was the one he had provided from our previous Jenkins job to deploy the system. We are using private IPs, as we are working within a private VPC on AWS, and we are keeping all of our machine private from the world. These tests ran in under 1 minute, with the majority of that time (executing tests was under 15 seconds) actually going to converting our TestNG results to JUnit so that the Jenkins pipeline tool could nicely display them. When our tests completed, the results looked something like this.
Screenshot from 2016-03-08 12:32:06 Screenshot from 2016-03-08 12:33:08 Screenshot from 2016-03-08 12:32:43 Screenshot from 2016-03-08 12:32:52
While I should have all passing tests, you can see I have some passing ones, and some failing ones in this situation, so these need to get cleaned up.

Acceptance Tests

For our acceptance tests I decided to execute some basic Selenium tests. As these can be much slower, I broke up our Selenium tests into two groups: Acceptance and Regression. Our Acceptance tests were quick ones, that just looked at the happy path for each story/bug I was implementing/fixing, with full exercising of the story getting placed in the regression group. I setup groups and tags for each of these tests, again using the SecureCITM Testing Framework. Our results looked similar, and were executed similarly to the above, except instead of executing the api.xml test suite, I used the selenium.xml suite. These tests were also checked to be non-destructive, so that we would have a good system left after they were done. A sample test output file looks something like this.
Screenshot from 2016-03-08 13:16:09
These tests ran in about 6 minutes, which is a little longer than I wanted, but with 75 Selenium tests, it’s not to bad.

Quick Security Scan

For our last set of tests on DevInt, I wanted to run a quick security scan. I used OWASP’s Zed Attack Proxy, to setup a quick un-authenticated scan of our system. Because this was a quick scan, I simply started ZAP in cmd mode, and pointed it towards our application. The commands looks like below.

    #!/bin/bash

    #cleanup old results
    #rm -rf zapScan.log zapReport.jsp

    #execute the scan
    echo ""
    echo ""
    /opt/zap/zap.sh -cmd -quickurl https://${Private_IP} -quickout $WORKSPACE/zapScan.xml -port 9090 -quickprogress

    #format the results
    sed -i 's/<?xml version="1.0"?>/<?xml version="1.0"?>\n<?xml-stylesheet type="text\/xsl" href="report_html.xsl"?>\n/' $WORKSPACE/zapScan.xml

    #display the results
    echo ""
    echo ""
    echo "Results can be viewed at {JOB_URL}/ws/zapScan.xml"

You can see I are once again passing in the private IP of our app, otherwise everything is strictly out of the basic ZAP instructions. I also formatted the report using an xsl which is available at github. This gives us something nice/pretty to look at in Jenkins.
Screenshot from 2016-03-08 13:23:39
This scan takes just over a minute to execute, which isn’t too bad for timing.

Destroy DevInt

Once all of these tests have completed, we want to destroy our AWS instance. This is accomplished very simply using the aws cli commands, and again, we want to provide the parameters I had generated from the Deploy to DevInt job.

    knife ec2 server delete ${Instance_ID} \
        --config ~/.chef/knife.rb \
        --purge \
        --node-name ${Tag_Name} \
        --yes
  

Simple, right? The other thing that I did in this CI pipeline was actually change some of the job dependencies based on the outcome of the tests. If the Smoke test failed, I skipped all subsequent tests, and immediately destroyed the machine, as there wasn’t any point in running additional tests on a broken system. Similarly for the Acceptance tests.

Package A Release Candidate

Finally, if all of our tests worked, I want to push this packaged application out to Nexus, instead of keeping it in Jenkins. While I have a high degree of confidence that everything is working, I want to double check that the code on our development branch (remember that from last time?) can get properly merged into our master branch. So, I used the Jenkins plugin to merge our development code into the master. If this didn’t cause any problems, I re-ran our unit tests, code coverage, and static analysis, to ensure I didn’t miss anything. If all of these results came back the same as the results from the Developer Tests job then I know I have a good build. If any of the results are different, then I have a problem. This means someone checked in something directly to master, which I can’t have, so the build stops and fails. If I have a success, then I go ahead and publish our artifact to Nexus. I have another gradle command for this.

    apply plugin: "distribution"
    apply plugin: "maven"

    def nexusUser = System.getenv("USER") ?: ""
    def nexusPass = System.getenv("PASS") ?: ""

    artifacts {
      archives file(tarfile + ".tgz")
    }
    uploadArchives {
      repositories {
        mavenDeployer {
          repository(url: "https://secureci/nexus/content/repositories/snapshots") {
            authentication(userName: nexusUser, password: nexusPass)
          }
          pom.version = version
        }
      }
    }
  

Note that the Nexus username and password are stored in system variables, which I set/injected via Jenkins.

And that is it. This will fully test our application within our CI pipeline. And a final view of this looks like the below:
CI Pipeline  Jenkins

As a final note, I made certain that every task/job I run within this CI job is in a our gradle build script. This ensures that every developer can execute the exact same steps as the pipeline does, which means that none of the results should be a surprise to them.

Good luck coding, and stay tuned for the last step of this process, setting up the full CD portion of this pipeline.

One thought to “Filling out your CI Pipeline for Your PHP Project”

Leave a comment

Your email address will not be published. Required fields are marked *

X