9

I have around 30 Wordpress websites so the way Jenkins is configured is I have a Job for every website. The process of development is as follows (I don't know if it's optimal but this is how we have it):

  1. Since we have outsourcing developers, they have their own repository hosted in their own repository hosting provider. Whenever the code is ready for QA, they commit all their changes to our repository in the master branch.
  2. I then run the Jenkins job manually with the jenkinsfile like the one below.
  3. The workflow must be: deploy to dev environment, if and only if the previous deployment is successful, deploy to stage. Here we have to stop and let the QA people review the website for any broken links, errors, etc.
  4. If everything looks like we expected and no errors have been found, then deploy finally to production environment.

NOTE: Some people have suggested me to have only staging and production. The reason why we don't have that configuration is because the dev environment is not accessible online, and the reason for that is because I use this environment to test back-end configuration (e.g. apache conf, etc.).

Also, some other people have suggested to have a branch for each environment, which in theory makes sense but I think it will change the way that our outsourcing devs are committing code to the repository, I mean, they will always have to commit the code to the dev branch and then merge to the stage branch to get deployed to stage, which I don't think is pretty good.

Now, the steps 2-4 look like the following: In order to give you an example on how that process looks we are going to have an example website and job called "Bearitos":

enter image description here

Inside of that Job called "Bearitos" there is a project called "Bearitos to any"

enter image description here

which basically means inside of that project I have a pipeline configured with three stages: dev, staging and prod which are parameterized with the following parameters: DEPLOY_TO: Dev/staging/prod and DEPLOY_DB: Yes/No . So depending of what the user chooses, Jenkins will deploy to that specific environment which I don't think it's even necessary to have those options since the correct deployment flow should be dev -> staging -> prod, there shouldn't be a scenario where dev or staging would be skipped and then deploy right next to production, so in my opinion this should be updated better

enter image description here

Inside of the Jenkinsfile I have defined the three stages Dev, Staging or Prod and also the options if it was chosen to build a DB or not, following is the example of how my Jenkinsfile looks like:

// Deployment template for CMS-based websites (Drupal or Wordpress)
// 
//
pipeline {
    agent any

    parameters {
        choice choices: ['Dev', 'Staging', 'Production'], description: "Choose which environment to push changes to.", name: "DEPLOY_TO"
        booleanParam defaultValue: true, "Choose whether to deploy the database.", name: "DEPLOY_DB"
    }

    environment {
         SITEID = "lb"
        NOFLAGS = "0"
        DBNAME = "wpress_myproject"
        DBSERVER = "dbserver"
        DBUSER = "WordpressUser"
        DBPASS = "hiddenpassword"
        EXCLUDE = "domain_commentmeta,domain_comments"  // separate multiple tables with commas
        DEPLOY_TO = "${params.DEPLOY_TO}"
        DEPLOY_DB = "${params.DEPLOY_DB}"
    }

    stages {
        stage("deploy-db-dev") {
            when {
                allOf { 
                    environment ignoreCase: true, name: "DEPLOY_TO", value: "dev"; 
                    environment ignoreCase: true, name: "DEPLOY_DB", value: "true"; 
                }
            }
            steps {
                // this stage only required until we make our dev the master DB
                // copy full dev database from bolwebdev1
                // import latest database dump to dev server
                script {
                    FILENM = sh(script: 'ls -t myproject-s-dump* | head -1', returnStdout: true)
                }
                //Fixing the problem with the collation existing in the sql dump file, refer to: https://stackoverflow.com/questions/42385099/1273-unknown-collation-utf8mb4-unicode-520-ci 
                //apparently, this is due to a version of mysql issue. Once the problem is fixed from the server side we can then remove the following lines. 

                sh """sed -i s/utf8mb4_unicode_520_ci/utf8mb4_unicode_ci/g ${FILENM}
                # The following line was added because the site is pointing to a staging server which we don't have control over, again, once this is fixed we can delete the following line of code. 
                sed -i s/myproject.staging.websites.3pth.com/myproject.example.net/g ${FILENM}
                mysql -h devserver2 -u ${env.DBUSER} --password='${env.DBPASS}' ${env.DBNAME}_dev < ${WORKSPACE}/${FILENM}
                rm -f ${WORKSPACE}/${FILENM}"""
        }
        }
        stage("deploy-dev") {
            when {
                environment ignoreCase: true, name: "DEPLOY_TO", value: "dev"
            }
            steps {
                // copy files to devserver2
                // NOTE: if we move the repo to SVN, we should change httpdocs/ to ${env.SITEID}docs/
                sh """sudo chown jenkins:jenkins *

                #Replace the wp-config.php file with our domain file with our information. 
        /bin/cp httpdocs/wp-config-domain.php httpdocs/wp-config.php

                # prepare the dev server to receive files by changing the owner
                ssh webadmin@devserver2 'sudo chown -R webadmin:webadmin /var/opt/httpd/${env.SITEID}docs/'
                # copy files from control server to dev
                rsync --exclude=Jenkinsfile -rav -e ssh --delete ${WORKSPACE}/httpdocs/ webadmin@devserver2:/var/opt/httpd/${env.SITEID}docs/
                # fix the owner/permissions on the dev server
        ssh webadmin@devserver2 'sudo chown -R apache:${env.SITEID}-web /var/opt/httpd/${env.SITEID}docs/ && sudo chmod -R g+w /var/opt/httpd/${env.SITEID}docs/ && sudo find /var/opt/httpd/${env.SITEID}docs/ -type d -exec chmod g+s {} \\;'"""
            }
        }
        stage("deploy-db-staging") {
            when {
                allOf { 
                    environment ignoreCase: true, name: "DEPLOY_TO", value: "staging"; 
                    environment ignoreCase: true, name: "DEPLOY_DB", value: "true"; 
                }
            }
            steps {
                script {
                    def myexcludes = env.EXCLUDE.split(',').toList()
                    MYFLAGS = "-Q -K -c -e --default-character-set=utf8 "
                    if (env.NOFLAGS == "0") {
                        myexcludes.each {
                            MYFLAGS = "${MYFLAGS} --ignore-table=${env.DBNAME}_dev.${it}"
                        }
                    }
                }
                // pull a backup of the current dev database (may exclude some tables)
                sh """mysqldump -h devserver2 -u ${env.DBUSER} --password='${env.DBPASS}' ${env.DBNAME}_dev ${MYFLAGS} > ${env.DBNAME}_dev.sql
        #Searching and replace for the URL to change from the dev sever to the staging server
                sed -i s/myproject.example.net/stage-myproject.example.net/g ${env.DBNAME}_dev.sql

        # create a backup copy of the current staging database (full backup)
                mysqldump -h ${env.DBSERVER} -u ${env.DBUSER} --password='${env.DBPASS}' ${env.DBNAME}_stage > ${env.DBNAME}_stage_bak.sql
                # upload the dev database dump to the staging database
                mysql -h ${env.DBSERVER} -u ${env.DBUSER} --password='${env.DBPASS}' ${env.DBNAME}_stage < ${WORKSPACE}/${env.DBNAME}_dev.sql
                rm -f ${WORKSPACE}/${env.DBNAME}_dev.sql"""
       }
        }
        stage("deploy-staging") {
            when {
                environment ignoreCase: true, name: "DEPLOY_TO", value: "staging"
            }
            steps {
                // copy files from dev to control server
                sh """rsync --exclude=.svn --exclude=.git -rav -e ssh webadmin@devserver2:/var/opt/httpd/${env.SITEID}docs/ /tmp/${env.SITEID}docs/

                #Replace the wp-config.php file with our domain file with our information. 
            /bin/cp httpdocs/wp-config-domain.php httpdocs/wp-config.php

                #prepare the staging server to receive files by changing the owner
                ssh webadmin@stageserver 'sudo chown -R webadmin:webadmin /var/opt/httpd/${env.SITEID}docs/'
                # copy files from control server to staging
                rsync --exclude=.svn --exclude=.git -rav -e ssh --delete /tmp/${env.SITEID}docs/ webadmin@stageserver:/var/opt/httpd/${env.SITEID}docs/
                # fix the owner/permissions on the staging server
                ssh webadmin@stageserver 'sudo chown -R apache:${env.SITEID}-web /var/opt/httpd/${env.SITEID}docs/ && sudo chmod -R g+w /var/opt/httpd/${env.SITEID}docs/ && sudo find /var/opt/httpd/${env.SITEID}docs/ -type d -exec chmod g+s {} \\;'

                #delete the temporary files on the control server
                rm -Rf /tmp/${env.SITEID}docs/
                # clear the Incapsula caches
                if [[ \$( curl -sS -X POST \"http://www.example.net/incapcache.php?api_key=asdaswwGR)feasdsdda&site_id=stage&resource_url=stage-myproject.example.net\" | jq -r .debug_info.id_info) != \"incapsula cache cleared successfuly\" ]]; then exit 255; fi"""
            }
        }
        stage("deploy-db-production") {
            when {
                allOf { 
                    environment ignoreCase: true, name: "DEPLOY_TO", value: "production"; 
                    environment ignoreCase: true, name: "DEPLOY_DB", value: "true"; 
                }
            }
            steps {
                script {
                    def myexcludes = env.EXCLUDE.split(',').toList()
                    MYFLAGS = "-Q -K -c -e --default-character-set=utf8 "
                    if (env.NOFLAGS == "0") {
                        myexcludes.each {
                            MYFLAGS = "${MYFLAGS} --ignore-table=${env.DBNAME}_stage.${it}"
                        }
                    }
                }
                sh """cd ${WORKSPACE}
                # pull a backup of the current staging database (may exclude some tables)
                mysqldump -h ${env.DBSERVER} -u ${env.DBUSER} --password='${env.DBPASS}' ${env.DBNAME}_stage ${MYFLAGS} > ${env.DBNAME}_stage.sql
        #Searching and replace for the URL to change from the stage sever to the prod server
                sed -i s/stage-myproject.example.net/www.myproject.com/g ${env.DBNAME}_stage.sql

                # create a backup copy of the current production database (full backup)
                mysqldump -h ${env.DBSERVER} -u ${env.DBUSER} --password='${env.DBPASS}' ${env.DBNAME}_prod > ${env.DBNAME}_prod_bak.sql
                # upload the staging database dump to the production database
                mysql -h ${env.DBSERVER} -u ${env.DBUSER} --password='${env.DBPASS}' ${env.DBNAME}_prod < ${WORKSPACE}/${env.DBNAME}_stage.sql
                rm -f ${WORKSPACE}/${env.DBNAME}_stage.sql"""
        }
        }
        stage("deploy-production") {
            when {
                environment ignoreCase: true, name: "DEPLOY_TO", value: "production"
            }
            steps {
                // copy files from staging to control server
                sh """rsync --exclude=.svn --exclude=.git -rav -e ssh webadmin@stageserver:/var/opt/httpd/${env.SITEID}docs/ /tmp/${env.SITEID}docs/

                # prepare the production server to receive files by changing the owner
                ssh webadmin@prodserver1 'sudo chown -R webadmin:webadmin /var/opt/httpd/${env.SITEID}docs'
                ssh webadmin@prodserver2 'sudo chown -R webadmin:webadmin /var/opt/httpd/${env.SITEID}docs'
                # copy files from control server to production
                rsync --exclude=.svn --exclude=.git -rav -e ssh --delete /tmp/${env.SITEID}docs/ webadmin@prodserver1:/var/opt/httpd/${env.SITEID}docs/
                rsync --exclude=.svn --exclude=.git -rav -e ssh --delete /tmp/${env.SITEID}docs/ webadmin@prodserver2:/var/opt/httpd/${env.SITEID}docs/
                # fix the owner/permissions on the production server
                ssh webadmin@prodserver1 'sudo chown -R apache:${env.SITEID}-web /var/opt/httpd/${env.SITEID}docs/'
                ssh webadmin@prodserver2 'sudo chown -R apache:${env.SITEID}-web /var/opt/httpd/${env.SITEID}docs/'
                ssh webadmin@prodserver1 'sudo chmod -R g+w /var/opt/httpd/${env.SITEID}docs/'
                ssh webadmin@prodserver2 'sudo chmod -R g+w /var/opt/httpd/${env.SITEID}docs/'
                ssh webadmin@prodserver1 'sudo find /var/opt/httpd/${env.SITEID}docs/ -type d -exec chmod g+s {} \\;'
                ssh webadmin@prodserver2 'sudo find /var/opt/httpd/${env.SITEID}docs/ -type d -exec chmod g+s {} \\;'

                # delete the temporary files on the control server
                rm -Rf /tmp/${env.SITEID}docs/
                # clear the Incapsula caches
                if [[ \$( curl -sS -X POST \"http://www.example.net/incapcache.php?api_key=asdaswwGR)feasdsdda&site_id=088&resource_url=www.myproject.com\" | jq -r .debug_info.id_info) != \"incapsula cache cleared successfuly\" ]]; then exit 255; fi"""
            }
        }
    }
}

The problems that I'm currently facing with this approach are:

  1. I can't figure out how make deployments automated since it's a parameterized pipeline so I'm not sure how to make it automated. The desired process would be to make the deployment automated once Jenkins polling every X amount of minutes on the git repository, deploy to Dev > Stage (only if Dev deployment was successful) automatically and then stop there until we manually deploy to Prod after we do QA on Staging.

  2. The current Git configuration has configured only one branch (master) which is where the developers push the changes once they want to make a deployment to Dev -> Stage -> Prod. But I think the ideal scenario would have a dev branch for the dev deployments then stage branch for deploying to Stage environment and then master for deployment once we merge those dev and staging branches to the master branch. I'm not sure if this would be optimal, so I would appreciate any suggestions or ideas on this.

The desired approach will be to have mentioned problems resolved and also have an automated way to deploy and notify once the dev -> staging deployment was successful. As well as having an option to do the mentioned workflow manually, like we are doing right now (this is not that important, but would be a nice to have feature).

Thank you in advance for your help!

VaTo
  • 2,936
  • 7
  • 38
  • 77

4 Answers4

3
  1. Get rid of your parameters, they are not needed (would stop you from automation).
  2. Have a manual input in the deploy to prod stage
pipeline {
    agent any
    stages {
        stage('Deploy to prod') {
            input {
                message "Should we continue?"
                ok "Yes, we should."
            }
            steps {
                echo "Deploying."
            }
        }
    }
}
  1. This should be multibranch pipeline project in Jenkins (since you want to it on all branches)
  2. if you want to use different stages for different branches do use when
pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when {
                branch 'production'
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}

As for suggestions - I would say you need to match your git flow with your CI/CD flow. What's the lifecycle of given git branch type? What's the result of the given stage? Do you want to execute the stages for all the branches and deploy to prod just for one branch?

hakamairi
  • 4,464
  • 4
  • 30
  • 53
  • Thank you for your response! regarding point number 1, I would like even though is not that important, to be able to deploy manually as well. And regarding your last questions, I have updated my main post. Thank you again! – VaTo May 06 '19 at 16:20
  • By manually I mean like I'm currently doing it, go into jenkins select the job and then choose "deploy to dev", etc. and then click on the button to deploy, but like I was saying that's not super important, what I think it is more important for me to know is how the jenkins job would look like, I mean, can you tell me how do I have to create the job in jenkins this time and inside of that job create three folders or three piplines, or how would that look like? – VaTo May 06 '19 at 17:59
  • Good question. One option would be to create multibranch pipeline project in jenkins and have the configuration as code meaning a Jenkinsfile (you would need to commit this to git). You wouldn't have 3 separate jobs, just one job per project, inside there would be division per branch, inside of this you would have stage view. If you would have some stages available just for some branches then the stages might look different (for example no `deploy to prod` for development branch). But hey, that's almost what you have now. – hakamairi May 06 '19 at 18:48
  • when you mention this "there would be division per branch" I think you are thinking that I have a branch per environment, which I don't. I just have a master branch for now. And at this time I don't have plans to create a branch per environment. – VaTo May 06 '19 at 19:17
  • But you are using the Jenkinsfiles. In general you could have just a pipeline project in Jenkins and this would still follow the same design. The idea with branches assumes that you would use same Jenkinsfile, but you would have some conditions on including some of the branches. – hakamairi May 07 '19 at 06:06
2

What abut implementing a separate deploy pipeline which is capable of deploying to all environments and is parametrized and implement another pipeline which is sheduled and triggers the pipeline to deploy to dev (stage dev) and when this job is succesfull then triggers the pipeline again to deploy to stage (stage qa). The deployment to prod can then be done manually.

https://jenkins.io/doc/pipeline/steps/pipeline-build-step/#-build-%20build%20a%20job

Thomas Herzog
  • 506
  • 2
  • 6
2
pipeline 
{
        stages 
        {
               stage('Build')
               {
                     steps
                     {
                        echo 'building the codes from the git'
                      }
                }
                stage('developer-branch-stuff')
                {
                   when
                   {
                       branch 'developer'
                   }
                   steps
                   {
                      echo 'run this stage - only if the branch = developer branch'
                   }
                }
        stage('Deliver for development') 
        {
            when 
            {
                branch 'developer'
            }
            steps 
            {
                sh 'your_filename_along_with_your_filepath'
                input message: 'shall we deploy it? (Click "Proceed" to continue)'
            }
        }
        stage('Deploy for production') 
        {
            when
            {
                branch 'developer'
            }
            steps
            {
                sh 'your_filename_along_with_your_filepath'
                input message: 'shall we proceed to production? (Click "Proceed" to continue)'
            }
        }
    }
}
suba mohan
  • 21
  • 1
  • Thank you for your answer, is this code assuming that I have a git branch per environment? – VaTo May 07 '19 at 21:17
  • Yes it is assumed that you have the separate specific branch for all the environment. – suba mohan May 08 '19 at 03:25
  • Like my post says in my case I on;y have master branch in this case. – VaTo May 08 '19 at 16:11
  • If you have master branch then you can change branch name as 'master' .once you changed that branch name as master then ,only the master branch will be permitted for the deployment as well as for the production... Branch name in conditional statement depends on the branch that you have created. – suba mohan May 10 '19 at 06:51
  • Just so you know I'm using the master branch to deploy to dev environment and then to staging and then to production, it's just a linear deployment, so what I want is using just the master branch, we can deploy to the dev environment then to the staging env automatically when there's a new commit made to the master branch in the repository, from there I would like to do a manual deployment to production if everything looks good in staging. My question here is, can I still use your code in this case? – VaTo May 10 '19 at 17:08
  • As per your question if you don't need to have the manual process then you can remove the steps in the development phase of my code and you can use it .Production stage is as same as, what you are in need. – suba mohan May 14 '19 at 08:04
2

I would use git tags here.

First, I would have the Dev build job which is configured to listen to my master branch for any commits and start building the code, running the unit tests, deploying your application to the dev environment etc. If and when the build succeeds, it will tag the git repository with a particular version number.

Now, the Staging build job would be configured as a downstream job to the Dev build job and would only execute if the Dev build job is successful. When it runs, it take the version number as an argument from the upstream Dev build job, and checkout code for that particular version. If this fails, it will remove that particular version tag from the repository.

If you want to do manual testing in any of the above 2 phases and mark the build as "Pass/Fail" according to that, there is a Fail the build Jenkins plugin for that. If you mark the build as "Fail" after doing manual testing, then also you should remove the particular version tag corresponding to the build.

After that, you can have a manual Release job which lists all the version tags in the git repository marking all the successful builds. You can select any one of the versions and launch the Release job which would deploy the code pertaining to that build into production.

WaughWaugh
  • 1,012
  • 10
  • 15