Cloud, DevOps, Technical

Automated Moodle Deployment on the Oracle Cloud + DevOps

Was recently helping a customer deploy Moodle on the Oracle Cloud with a focus on how to do this in automated way with Developer Cloud Service. I thought I’d share my experience in case anyone else is trying to do something similar.

Goal

We want to deploy Moodle (which is a PHP application that uses MySQL) on the Oracle Cloud and we want the build to be automatically triggered every night (so we can test it fresh).

Step 1: Create the infrastructure

The first step is to create our infrastructure. As Moodle is a PHP application and since Application Container Cloud now supports PHP let’s use that. The alternative would be to use IaaS, but then we’d have to install Apache, PHP etc. As an added benefit ACC gives us built-in scaling, load balancer, backups etc.

I used the Oracle Cloud Stack to define our architecture. Oracle Cloud Stack allows you to define a deployment based on a number of PaaS Services (Java, Database, ACC etc).

You can use the UI inside your cloud service to create the deployment, or create the yaml file directly (which is what the UI builder does for you). In the code block below you can see my moodle-lmp.yaml file that defines a deployment container a MySQL Cloud Service instance (to host our database) and an Application Container using PHP.

---
  template:
    templateName: Moodle-LMP
    templateVersion: 1.0.1
    templateDescription: "Provisions a LMP Stack composed of Application Container Cloud Service and MySQL Cloud Service for a Moodle app"
    parameters:
      numberOfInstances:
        label: Number of Instances
        description: Enter the number of PHP Application instances to be created. The number must be between 1 and 16.
        type: Number
        default: 1
        minValue: 1
        maxValue: 16
      instanceMemory:
        label: Memory per Instance (GB)
        description: Enter the memory that should be allocated to each application instance being provisioned. The number must be between 1 and 20.
        type: Number
        default: 4
        minValue: 1
        maxValue: 20
      archiveURL:
        label: PHP Application Archive
        description: "Provide an optional PHP application archive path on Oracle Cloud Storage that you wish to deploy with the PHP container"
        type: String
        default: ""
      publicKeyText:
        label: SSH Public Key
        description: "Click Edit to generate a new key pair, enter an existing public keys value or upload a new public key."
        type: ssh
        mandatory: true
        sensitive: true
      computeShape:
        label: Compute Shape
        description: Select the compute shape for your MySQL service instance.
        type: ComputeShape
        default: oc3
        allowedValues: [ oc3, oc4, oc5, oc6, oc1m, oc2m, oc3m, oc4m ]
      mysqlUser:
        label: Administration User
        description: "Administration user for connecting to service via the MySQL protocol. User name should start with a letter, consist of letters and numbers, and be between 2 to 25 characters. (example: root)"
        type: String
        default: root
        allowedPattern: "^[a-zA-Z]+[a-zA-Z0-9_]{2,25}"
      mysqlPwd:
        label: Password
        description: "Password for the MySQL Administration user. Password must be atleast 8 characters long with at least one lower case letter, one upper case letter, one number and one special character. (example: Ach1z0#d)"
        type: String
        mandatory: true
        sensitive: true
        allowedPattern: "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[\\W_])[A-Za-z_\\d\\W]{8,}"
      mysqlPort:
        label: Port
        description: "Port to access the database"
        type: Number
        default: 3306
    parameterGroups:
      - label: MySQL Cloud Service Details
        parameters: [ computeShape, mysqlUser, mysqlPwd, publicKeyText ]
      - label: Application Cloud Service Details
        parameters: [ archiveURL, numberOfInstances, instanceMemory ]
    conditions:
        deploySampleApp: {"Fn::Equals" : ["Fn::GetParam" : archiveURL, ""]}
    resources:
      phpContainer:
        type: apaas
        parameters:
            name: { "Fn::Join": ["", ["Fn::GetParam": serviceName, Container]] }
            runtime: PHP
            subscription: HOURLY
            archiveURL: { "Fn::If" : [deploySampleApp, "OPC::NoValue", { "Fn::GetParam": archiveURL } ] }
            deployment:
                memory: { "Fn::Join": ["", ["Fn::GetParam": instanceMemory, G]] }
                instances: { "Fn::Join": ["", ["Fn::GetParam": numberOfInstances]]}
                services:
                      -
                        identifier: MySQLBinding
                        name: { "Fn::GetAtt": [mysqlDB, serviceName] }
                        type: MySQLCS
                        username: { "Fn::GetParam": mysqlUser }
                        password: { "Fn::GetParam": mysqlPwd }
      mysqlDB:
        type: MySQLCS
        parameters:
            serviceParameters:
                serviceName: { "Fn::Join": ["", ["Fn::GetParam": serviceName, db]] }
                serviceLevel: PAAS
                subscription: HOURLY
                serviceVersion: 5.7
                vmPublicKeyText: { "Fn::GetParam": publicKeyText }
                cloudStorageContainerAutoGenerate: false
            componentParameters:
                mysql:
                  shape: { "Fn::GetParam": computeShape }
                  mysqlPort: {"Fn::GetParam": mysqlPort }
                  mysqlUserName: { "Fn::GetParam": mysqlUser }
                  mysqlUserPassword: { "Fn::GetParam": mysqlPwd }
    attributes:
        AppURL:
            value: { "Fn::GetAtt": [phpContainer, attributes.webURL.value] }
            description: Open Application
            type: URL

The file itself is pretty easy, first we set a bunch of parameters. These are used so we don’t have to hard code names, passwords etc and so that we could use this file to deploy multiple instances of our architecture.

In the resources section you can see where we define our PHP and MySQL instances. Importantly you can see that we bind the MySQL instance to the PHP container (Line 74). This means that the URL and connection details for MySQL will be available as an environment variables inside our application container.

Having created our infrastructure file, let’s check it into a new git repository in Developer Cloud Service (create a new empty git repo in DevCS, check it out to your desktop, add the yaml file and do a push). If you don’t know how to do this check out the doco.

Step 2: Create the build

Now let’s have our infrastructure build every night. To do so log into your Developer Cloud Service project and create a new build job. Call it ‘infrastructure-create-stack’

MoodleBuild_1_Infra_1

Hook it up to our infrastructure repository in the Source Control tab

MoodleBuild_1_Infra_2

Now let’s trigger to run at midnight every night in the Trigger section. The Schedule field takes cron parameters but I’ll use the shortcut @midnight to run at midnight every night. We could also tick ‘Based on SCM polling schedule’ if we wanted it to rebuild whenever we modified the infrastructure file. But we’ll grab it at midnight anyway, so let’s leave it.

MoodleBuild_1_Infra_3

Now the let’s open the ‘Build Steps’ and add the steps we need to create the actual infrastructure. First, we need to load the PSMcli (the PaaS Stack Manager command line interface). This let’s us use psm commands to interact with the Oracle Cloud (which means we don’t need to deal with the REST APIs). To add it add the ‘Invoke PSMcli’ build step. Add your cloud authentication details in (as you can see it will save your password, but won’t reveal it to anyone. This is much safer than if we had to connect to the REST APIs directly, as we’d have to add the password in the clear into the command line steps).

MoodleBuild_1_Infra_4

Now we need to add 2 steps, one to delete the existing infrastructure and another to build a fresh new one. To do this add a ‘Execute Shell’ step to the build. Add the following code to the step:

# Delete existing
VALUE=`psm stack delete --name MdleNtly &2>1`
if [[ ${VALUE} != *"No data found"* ]]; then
  # If the stack doesn't exist this will prevent an error
  JOBID=$(echo  $VALUE | sed 's/.*jobId\":\"\([0-9]*\).*/\1/')
  echo "Job ID is "$JOBID  

  #Repeat check until SUCCEED is in the status
  PSMSTATUS=-1
  while [ $PSMSTATUS -ne 0 ]; do   

  CHECKSTATUS=`psm stack operation-status --job-id $JOBID`
    if [[ $CHECKSTATUS == *"SUCCEED"* ]]
    then
    PSMSTATUS=0
      echo "PSM operation Succeeded!"
    else
      echo "Waiting for PSM operation to complete"
      sleep 60
    fi
  done
fi

On Line 2 you can see our psm delete command to delete the entirety of the old stack.

We have to add a little extra bash scripting here, because PSMcli doesn’t have an ability to wait until the build completes. We add a check here to keep polling PSMcli for when the operation is complete (we can’t run the build step next without making sure the last one is deleted).

** I really wish they added something to PSMcli to handle this. My OracleCloud fog library can handle it, so shouldn’t be too hard to add **

And now add another ‘Execute Shell’ build step to create the nightly infrastructure.

# Create new one
VALUE=`psm stack create --name MdleNtly --template Moodle-LMP -p publicKeyText:'ssh-rsa AAAAB3...' mysqlPwd:"<PASSWORD>"`
JOBID=$(echo  $VALUE | sed 's/.*jobId\":\"\([0-9]*\).*/\1/')
echo "Job ID is "$JOBID
#Repeat check until SUCCEED is in the status
PSMSTATUS=-1
while [ $PSMSTATUS -ne 0 ]; do
  CHECKSTATUS=`psm stack operation-status --job-id $JOBID`
  if [[ $CHECKSTATUS == *"SUCCEED"* ]]
  then
  PSMSTATUS=0
    echo "PSM operation Succeeded!"
  else
    echo "Waiting for PSM operation to complete"
    sleep 60
  fi
done

The most important line is Line 2, where we call psm to create our stack. The –arguments are the parameters we made available in our infrastructure.

If you’d like you can now run the build step (go back to the job status page and click ‘Build Now’). Otherwise wait until midnight and the build should run automatically.

Step 3: Set up our database

Phew, we’ve got the infrastructure built, now we need to configure the database. We are going to use an open-source tool called flyway so that we can apply versioning to our database. This way if we make changes to the database structure (or add new default data) we can add them to a SQL script and have flyway handle the migration on our database.

Flyway works by providing it a set of versioned sql scripts and asking it to migrate the database. Flyway will create a special table within your database to record which scripts it has run and when you run migrate it will only apply any new scripts to the database. In the case of our nightly build this will initially be just one database script (the one that creates the entire database structure)

Unfortunately Moodle doesn’t provide it’s sql scripts separately as it relies on you running an install.php script. We won’t have access to this during the auto-deployment (and if you deploy Moodle straight to ACC without doing this you will get a 500 error and ACC will refuse to deploy it). So the solution is to install Moodle locally, run the install script and then dump the table structure into a script. To dump your table structure, run:

mysqldump -u [uname] -p[pass] db_name > db_backup.sql

Add the resulting sql file into a new git repository and name it V001_Initialise.sql. When you make changes to the moodle database you’ll need to add a new sql script (V002_something.sql) which contains the changes.

You’ll also need to add the following pom.xml file to the root of that directory. Flyway is available through maven, so we’ll use that to install and configure it in our DevCS environment.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.example</groupId>
	<artifactId>mysql-migration</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>
	<name>mysql-migration</name>
	<url>http://maven.apache.org</url>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>
	<dependencies>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>6.0.5</version>
		</dependency>
		<dependency>
			<groupId>org.flywaydb</groupId>
			<artifactId>flyway-maven-plugin</artifactId>
			<version>4.1.1</version>
		</dependency>
	</dependencies>
    <build>
	<plugins>
		<plugin>
			<groupId>org.flywaydb</groupId>
			<artifactId>flyway-maven-plugin</artifactId>
			<version>4.1.1</version>
			<configuration>
			    <skipDefaultCallbacks>true</skipDefaultCallbacks>
			</configuration>
			</plugin>
		</plugins>
	</build>
</project>

With those checked into a new repository, let’s create a build job to run the sql scripts. Go back to the build section and create a new build called ‘reset-nightly-database’. On the Source Control tab, link it to our moodle-db.git repository (or whatever you ended up calling it).

MoodleBuild_2_Infra_1

Now this job should be triggered when our infrastructure is rebuilt, so add that now:

MoodleBuild_2_Infra_2

And finally configure the build steps by adding a ‘Invoke Maven 3’ step. Match the settings as in the image (of course provide your password and connection details)

MoodleBuild_2_Infra_3

That’s it, either run this now or wait until midnight rolls around.

Note: There is currently a bug in Developer Cloud Service where it will not allow access to the MySQL instance over port 3306 (as it blocks this port from within DevCS). Until this is fixed, to get around this you’ll need to update the mysql port to something lower (like 1521, which is open). To do so see Step 7 on this page: https://github.com/oracle/cloud-native-devops-workshop/blob/master/db-devops/CloudNative200.md

Step 4: Add the moodle code

Phew! Almost there I promise. We now need to get our moodle code into the PHP container. To do, grab the latest moodle code and check it into a fresh git repository in DevCS. I actually grabbed the code and removed all the.git files and folders before I checked it in. This removes all the version history from the code base and makes it much smaller. Of course, you now can’t rollback to older moodle versions, but at this point I assume you just want the latest anyway.

I did make one change to the Moodle code base, by adding the DotEnv project. This allows us to reference environment variables from a .env file in your directory. All the configuration to connect to MySQL will be stored within environment variables by ACC, so we need to a way to reference those when we are developing. You could just add everything to your environment variables or use .env. To install DotEnv run:

curl -s http://getcomposer.org/installer | php
php composer.phar require vlucas/phpdotenv

Once you’ve done that update add a .env file into the root (don’t check this in, so add it to .gitignore) so that you can develop moodle locally.

APP_HOME="/Users/joelnation/Sites/moodle-dev"
ORA_APP_PUBLIC_URL="http://localhost/moodle-dev"
MYSQLCS_CONNECT_STRING="localhost"
MYSQLCS_USER_NAME="root"
MYSQLCS_USER_PASSWORD="PASSWORD"

Also update the config.php file so that you load all your connection details from environment variables. :

<?php  // Moodle configuration file   require_once 'vendor/autoload.php';   $dotenv = new Dotenv\Dotenv(__DIR__); $dotenv->load();

unset($CFG);
global $CFG;
$CFG = new stdClass();

$CFG->dbtype    = 'mysqli';
$CFG->dblibrary = 'native';
$CFG->dbhost    = strtok(getenv('MYSQLCS_CONNECT_STRING'),":");
$CFG->dbname    = 'moodle';
$CFG->dbuser    = getenv('MYSQLCS_USER_NAME');
$CFG->dbpass    = getenv('MYSQLCS_USER_PASSWORD');
$CFG->prefix    = 'mdl_';
$CFG->dboptions = array (
  'dbpersist' => 0,
  'dbport' => '',
  'dbsocket' => '',
  'dbcollation' => 'utf8_general_ci',
);

$CFG->wwwroot   = strtolower(getenv('ORA_APP_PUBLIC_URL')) . '/moodle';
$CFG->dataroot  = getenv('APP_HOME') . '/data';
$CFG->admin     = 'admin';

$CFG->sslproxy = true;

Super Important: Set $CFG->sslproxy to true otherwise moodle will not work! See ACC will provide you with a https:// url, however this is only to talk to the in-built load balancer. Between the load balancer and your application will be http:// but the ORA_APP_PUBLIC_URL will be http:// so you need to tell moodle it’s behind an ssl proxy.

Add Build Jobs

We need to create two new build jobs. One to grab the code, package it up and send it to Oracle Storage Cloud (this is where you put code before deploying to ACC). And then another that takes that code and deploys to ACC. We could make this one big build job, but I like to break things up a little, particularly while I’m testing.

So, let’s create a new build job ‘compile-moodle’ and attach it to the moodle-dev.git repo you created at the beginning of this step. This time though, set the Local Subdirectory to ‘moodle’. This will checkout the repo into a new folder called ‘moodle’.

MoodleBuild_3_Infra_2.png

For this one have it be triggered when the infrastructure is created and when the git repo is updated (this way it will run when we create new infrastructure and whenever we make changes to the code base).

MoodleBuild_3_Infra_1.png

Then add an ‘Execute Shell’ build step with the following code:

# Create the .env file (it can be blank as ACCS will add all the envs for us)
touch moodle/.env
mkdir data
echo '<?php header('moodle/index.php'); ?>' > index.php
# Zip it up
zip -r moodle.zip . -x *.git*

First we create a .env file (we need this so we don’t get an error, but the environment variables will be set by Application Container Cloud). Then moodle needs a data directory, so we create one. I could have put this in the same directory as the moodle code, but I read that this should be seperate so I’ve placed it here. Then I created an index.php that just redirects to the one in moodle/index.php. Finally we zip it up ready to send it to Storage Cloud.

Note: This will mean that moodle will be available at /moodle. I could have done this in the root directory (ie: not checked out the moodle code into a moodle subfolder), but one of the features of ACC is that when it deploys your code it checks if index.php returns an error. If it does, it will cancel the deploy and rollback to the last version. This is prevents you from deploying a broken release over a working one, which is super helpful. However, moodle will return a 500 error until you’ve configured everything properly and during my development of this process it became really difficult to work out what the error was (as ACC wouldn’t report it and since it was never deployed you don’t get any logs). The solution was to put it in a sub-folder and then have an index.php file that returns something without error. I could then navigate to the sub-directory and see the error on the screen. Now that it’s working, I could come back here and set it back to normal. Your choice!

Finally we need to save the zip file into our archive (the archive is where output from build steps is saved and can be used by other build steps to grab code etc). And we need to trigger the next build step.

MoodleBuild_3_Infra_3.png

Now, let’s create our final build job – ‘build-nightly-moodle’. For this one, don’t attach it to source control, as we’ll use the output from the compile job here.

First, add a ‘Copy Artifacts’ step to the build.

MoodleBuild_4_1.png

Then add an ‘Invoke PSMcli’ step so we can use the PSM commands. (See above if you forgot how to do this). And then add an ‘Execute Shell’ command to add the following script:

# PSM needs to be updated to support archive-path. Until then use the Storage Cloud

ARCHIVE_FILE="moodle.zip"

ARCHIVE_FILE_NAME=$(basename $ARCHIVE_FILE)

if [ ! -e "$ARCHIVE_FILE" ]; then
  echo "Error: file not found $ARCHIVE_FILE"
  exit -1
fi

echo "Found artifact: $ARCHIVE_FILE"

# CREATE CONTAINER
echo '\n[info] Creating container\n'
curl -i -X PUT \
    --header 'authorization: Basic <PASSWORD>' \
    https://<IDENTITY_DOMAIN>.storage.oraclecloud.com/v1/Storage-<IDENTITY_DOMAIN>/moodle_archives

# PUT ARCHIVE IN STORAGE CONTAINER
echo '\n[info] Uploading application to storage\n'
curl -i -X PUT \
  --header 'authorization: Basic <PASSWORD>' \
  https://<IDENTITY DOMAIN>.storage.oraclecloud.com/v1/Storage-<IDENTITY_DOMAIN>/moodle_archives/$ARCHIVE_FILE_NAME \
      -T $ARCHIVE_FILE
# Need to allow time for replication
#sleep 30
VALUE=`psm accs push --name MdleNtlyContainer --archive-url moodle_archives/$ARCHIVE_FILE_NAME`

echo $VALUE
JOBID=$(echo $VALUE | sed 's/.*Job ID : \([0-9]*\).*/\1/')
echo "Job ID is $JOBID"

#Repeat check until SUCCEED is in the status
PSMSTATUS=-1
while [ $PSMSTATUS -ne 0 ]; do   

CHECKSTATUS=`psm accs operation-status --job-id $JOBID`
  if [[ $CHECKSTATUS == *"SUCCEED"* ]]
  then
  PSMSTATUS=0
    echo "PSM operation Succeeded!"
  else
    echo "Waiting for PSM operation to complete"
    sleep 60
  fi
done

There’s a lot to understand here. But in a nutshell:

  1. First we create a container in storage cloud to hold our archive. We run this each time, but if it already exists than it won’t create a new one. Note: we unfortunately have to use curl here (and hard code our authorisation string) as PSMcli only does PaaS, not IaaS and the equivalent tool from Oracle ComputeCLI isn’t supported in DevCS yet. Until that’s resolved we have to do it this way
  2. Then we upload the archive to our storage container
  3. And finally we use PSM to push the code to our application container. We also add the standard check code to wait until this is complete

And we’re done! With this the code should be successfully added to the container.

Conclusion

So there’s a lot here, but once you have it all set up and running it should run automatically everynight, giving you a fresh moodle instance to configure and develop on every day. Plus the yaml file we created to create the infrastructure can be used by others to create their own private instances of moodle for their development purposes.

As for ongoing development, Moodle is a little different as most of the actual configuration will take place by users within the UI. To have an automated deployment based on this you will have to capture the difference in the database and add that to the SQL scripts. As such, you may actually better off just taking a snapshot of some development database each night (before running this) and archiving that for use by the build scripts.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s