In the first post of the Exploring the Sling Feature Model series, I discussed the process of converting the Sling CMS app from the Sling Provisioning Model to the Sling Feature Model. So how does this apply to your custom applications?
To illustrate, let’s convert my personal site, danklco.com, which is currently managed via Sling CMS, to the Feature Model.
It’s worth noting that I could keep running my site the way it is, by using a pre-built Sling CMS Runnable Jar, but that my goal is to run my site in Kubernetes for simplicity of upgrades, deployment, and management.
Step 1: Refactor Project Structure
Currently, my personal website code is a single OSGi Bundle which I deploy with Github Actions. To support the Sling Feature Model, I’m going to convert the project into a multi-module project and add a new sub-project for my feature.
The new project structure will look like:
/mysite
/bundle
/feature
/images
Step 2: Generate Features
The custom feature is pretty simple, defining my custom code bundles and configurations. A number of parameters are supplied so they can be changed and I’m not putting secrets in code:
{ "bundles": [ { "id": "com.danklco:com.danklco.slingcms.plugins.disqus:1.1-SNAPSHOT", "start-order": "20" }, { "id": "com.danklco:com.danklco.slingcms.plugins.twitter:1.0", "start-order": "20" }, { "id": "com.danklco:com.danklco.site.cna.bundle:1.0.0-SNAPSHOT", "start-order": "20" } ], "configurations": { "org.apache.sling.cms.core.analytics.impl.GeoLocatorImpl": { "scheduler.expression": "0 0 0 ? * WED", "licenseKey": "${MAXMIND_LICENSE_KEY}" }, "org.apache.sling.cms.reference.impl.SearchServiceImpl": { "searchServiceUsername": "dklco-com-search-user" }, "org.apache.sling.commons.crypto.internal.FilePasswordProvider~default": { "names": [ "default" ], "path": "/opt/slingcms/passwd" }, "org.apache.sling.commons.crypto.jasypt.internal.JasyptRandomIvGeneratorRegistrar~default": { "algorithm": "SHA1PRNG" }, "org.apache.sling.commons.crypto.jasypt.internal.JasyptRandomSaltGeneratorRegistrar~default": { "algorithm": "SHA1PRNG" }, "org.apache.sling.commons.crypto.jasypt.internal.JasyptStandardPBEStringCryptoService~default": { "algorithm": "PBEWITHHMACSHA512ANDAES_256", "saltGenerator.target": "", "securityProviderName": "", "ivGenerator.target": "", "securityProvider.target": "", "keyObtentionIterations": 1000, "names": [ "default" ], "stringOutputType": "base64" }, "org.apache.sling.commons.messaging.mail.internal.SimpleMailService~default": { "connectionListeners.target": "", "transportListeners.target": "", "username": "${SMTP_USERNAME}", "mail.smtps.from": "${SMTP_USERNAME}", "messageIdProvider.target": "", "mail.smtps.host": "${SMTP_HOST}", "names": [ "default" ], "password": "${SMTP_ENC_PASSWORD}", "mail.smtps.port": 465, "cryptoService.target": "", "threadpool.name": "default" }, "org.apache.sling.commons.messaging.mail.internal.SimpleMessageIdProvider~default": { "host": "danklco.com", "names": [ "default" ] } } }
To create a usable model, I’ll need to combine the Sling CMS model and my custom model, which can be accomplished with the Sling Feature model. To support the Composite Node Store, I’ll want to generate two separate aggregates, one for seeding and one for running the instance.
Since the Sling Feature Model JSON will resolve dependencies at runtime from Apache Maven, we’ll also want to generate Feature Archives or FAR files which bundles the models with their dependencies.
<plugin> <groupid>org.apache.sling</groupid> <artifactid>slingfeature-maven-plugin</artifactid> <version>1.3.0</version> <extensions>true</extensions> <configuration> <framework> <groupid>org.apache.felix</groupid> <artifactid>org.apache.felix.framework</artifactid> <version>6.0.3</version> </framework> <aggregates> <aggregate> <classifier>danklco-com-seed</classifier> <filesinclude>**/*.json</filesinclude> <includeartifact> <groupid>org.apache.sling</groupid> <artifactid>org.apache.sling.cms.feature</artifactid> <version>0.16.3-SNAPSHOT</version> <classifier>slingcms-composite-seed</classifier> <type>slingosgifeature</type> </includeartifact> <includeartifact> <groupid>org.apache.sling</groupid> <artifactid>org.apache.sling.cms.feature</artifactid> <version>0.16.3-SNAPSHOT</version> <classifier>standalone</classifier> <type>slingosgifeature</type> </includeartifact> <title>DanKlco.com</title> </aggregate> <aggregate> <classifier>danklco-com-runtime</classifier> <filesinclude>**/*.json</filesinclude> <includeartifact> <groupid>org.apache.sling</groupid> <artifactid>org.apache.sling.cms.feature</artifactid> <version>0.16.3-SNAPSHOT</version> <classifier>slingcms-composite-runtime</classifier> <type>slingosgifeature</type> </includeartifact> <includeartifact> <groupid>org.apache.sling</groupid> <artifactid>org.apache.sling.cms.feature</artifactid> <version>0.16.3-SNAPSHOT</version> <classifier>standalone</classifier> <type>slingosgifeature</type> </includeartifact> <title>DanKlco.com</title> </aggregate> </aggregates> <scans> <scan> <includeclassifier>danklco-com-seed</includeclassifier> </scan> <scan> <includeclassifier>danklco-com-runtime</includeclassifier> </scan> </scans> <archives> <archive> <classifier>danklco-com-seed-far</classifier> <includeclassifier>danklco-com-seed</includeclassifier> </archive> <archive> <classifier>danklco-com-runtime-far</classifier> <includeclassifier>danklco-com-runtime</includeclassifier> </archive> </archives> </configuration> <executions> <execution> <id>aggregate-features</id> <phase>prepare-package</phase> <goals> <goal>aggregate-features</goal> <goal>analyse-features</goal> <goal>attach-features</goal> <goal>attach-featurearchives</goal> </goals> <configuration> <replacepropertyvariables>MAXMIND_LICENSE_KEY,SMTP_HOST,SMTP_USERNAME,SMTP_ENC_PASSWORD</replacepropertyvariables> </configuration> </execution> </executions> </plugin>
Step 3: Build Docker Images
Since the goal is to run this in Kubernetes, we’ll create Docker images for running Sling CMS and Apache web server. Since I’m running a lean server, I’ll want to run this as a standalone instance using the Composite Repository so the datastore persists between instances.
To populate variables into the images and coordinate the full build, we’ll use Apache Maven to process the Docker files and input files as Maven artifacts and kick off the docker build. Unlike the Sling CMS build, we’re not leveraging Apache Maven to download the artifacts within the Docker build, we’ll pre-fetch them during the maven build and supply them to the Docker build.
Side Note – Variables
One challenge to note when attempting to reproduce an actual instance, there are a quite a few variables required for the application to actually work. For my local testing I have a bash script to provide all of the required properties to Maven, but since they include secrets like passwords I’ve not put it in source control.
See it in Action!
Seeing something work is work a thousand words, so check out this GIF of the build process in action:
and check out the code on GitHub: https://github.com/klcodanr/danklco.com-site/tree/cloud-native-sling
What’s Next?
All of this is leading up to having a fully running Cloud Native Apache Sling CMS instance in Kubernetes, but before that my next post is going to talk about using Sling Content Distribution and Sling Discovery to support publishing content between Author and Renderer Apache Sling CMS instances. Check back soon!