Building Cloud Foundry buildpacks in .NET

Cloud Foundry buildpacks offer a convenient way to bootstrap applications. At its core, it’s a code recipe that prepares the application to be launched on Cloud Foundry by doing things like injecting dependencies (runtime, middleware, etc), setting environmental variables, modifying application config parameters. If you’re a Cloud Foundry user, you probably have many buildpacks already available to you, which you can list with cf buildpacks command. These are the ones that are installed on the platform and are available to use out of the box. However, unless disabled by the platform operator, you can also specify buildpacks off the platform just by pointing to the buildpack package URL. On the one input, we have the original artifact that the developer gave the platform via cf push command, and the output of is tarball that contains the application and all the dependencies introduced by the buildpack, being similar in nature to a simplified docker container. Buildpacks can also be chained, essentially acting as steps in a pipeline of preparing your app to be run on the platform. The final buildpack in the chain is special as it determines the startup command for the application, while any intermediatory buildpacks are referred to as “supply buildpacks”.

So what exactly is in a buildpack and how does it work? Buildpack includes shell hooks that Cloud Foundry will invoke at different stages of the application lifecycle, which are as follows:

  • bin/detect determines whether or not to apply the buildpack to an app.
  • bin/supply provides dependencies for an app.
  • bin/finalize prepares the app for launch.
  • bin/release provides feedback metadata to Cloud Foundry indicating how the app should be executed.

Each hook will be called with a set of parameters, and expected to either return code to the platform or write it’s output to standard out. The actual implementation of each hook can be any valid executable on the platform, from shell script to compiled binary. More details on the buildpack contract can be found on the official Cloud Foundry buildpack documentation. Buildpacks are packaged up as zip file and be either installed on the platform by platform operator via cf create-buildpack command or referenced in the manifest by its published URL.

The good news is I’ve created a buildpack template in .NET, allowing you to build a new buildpack just by implementing a few abstract methods. The provided build script will build and package your project into the expected buildpack format so that you can use directly in your manifest – and it works on both Linux and Windows stacks!

Let’s see how easy it is to actually do this. Today I’m going to show you how to build a buildpack that will replace any values for configuration\appsettings inside web.config with matching values provided in environmental variables.

First, we going to create a new project via buildpack template:

dotnet new --install CloudFoundry.Buildpack.V2
dotnet new buildpack -n TransformBuildpack

Open up the solution created and look at TransformBuildpack class (name of the main class will match the name of your project).

public class TransformBuildpack : SupplyBuildpack
{
    protected override bool Detect(string buildPath)
    {
        return false;
    }
    protected override void Apply(string buildPath, string cachePath, string depsPath, int index)
    {
        Console.WriteLine("=== Applying TransformBuildpack ===");
    }
}

Depending on the type of buildpack you plan on building, inherit either from SupplyBuildpack or FinalBuildpack. The Detect method can be used to allow buildpack to automatically determine if it should be applied by examining the content of the application located at `buildPath` parameter. Detection will only run if the buildpack is installed on the platform. If it is explicitly specified in the manifest, it will be applied regardless of what is in the Detect method.

The detection phase for this is very simple – we just want to check if there’s a web.config in the application folder. We can implement that with a single line like this:

protected override bool Detect(string buildPath) => 
  File.Exists(Path.Combine(buildPath, "web.config"));

The meat of the buildpack happens in the Apply method. Depending on whether you inherit from SupplyBuildpack or FinalBuildpack, this method will be invoked either on /bin/supply or /bin/finalize hook. The parameters are as following:

  • buildPath – Directory path to the application
  • cachePath – Location the buildpack can use to store assets during the build process
  • depsPath – Directory where dependencies provided by all buildpacks are installed. New dependencies introduced by current buildpack should be stored inside subfolder named with index argument ({depsPath}/{index})
  • index – Number that represents the ordinal position of the buildpack

We want to open up web.config if it exists, read appSettings block and for every value check if there’s a matching environmental variable that will override the default setting. The implementation would look as follows:

protected override void Apply(string buildPath, string cachePath, string depsPath, int index)
{
    var webConfig = Path.Combine(buildPath, "web.config");
    if (!File.Exists(webConfig))
    {
        Console.WriteLine("Web.config not detected");
        Environment.Exit(0);
    }
    var doc = new XmlDocument();
    doc.Load(webConfig);
    var adds = doc.SelectNodes("/configuration/appSettings/add").OfType<XmlElement>();

    foreach(var add in adds)
    {	
        var key = add.GetAttribute("key");
        if(key == null)
            continue;
        var envVal = Environment.GetEnvironmentVariable(key);
        if(envVal != null)
            add.SetAttribute("value", envVal);
    }
    doc.Save(webConfig);
}

Now we just need to compile this into a package that Cloud Foundry understand. The template makes this super easy via the included build script (powered by Cake, see cake.build file for details). The build scripts accepts a stack parameter depending on which Cloud Foundry stack you’re targeting – in this case we want Windows. (Note, you can’t compile Windows buildpack from Linux or Mac because it requires .NET Framework – for Linux the buildpack will be compiled to use self contained .NET core)

.\build.ps1 -ScriptArgs '-stack=windows'
.\build.ps1 -ScriptArgs '-stack=linux'
./build.sh --stack=linux

You should find the output in the /publish directory. Simply publish it to a public URL (I recommend GitHub releases page if you’re hosting your buildpack on GitHub), and reference in your manifest

If you’re compiling for Linux stack, you must run `scripts/fix.sh` on a Linux or Mac box before it can be used on Cloud Foundry. This is necessary to properly repackage zip file with correct file permissions which cannot be set by the regular compression library used by the build script.
---
applications:
- name: my-config
  random-route: true
  memory: 512M
  stack: windows2016
  buildpacks: 
    - https://github.com/macsux/cf-buildpack-template/releases/download/1.0/TransformBuildpack-win-x64-1.0.609536966.zip
    - hwc_buildpack
  env:
    MyKey: Hello
<configuration>
  ...
  <appSettings>
    <add key="MyKey" value="default"/>
  </appSettings>
  ...
</configuration>
<configuration>
  ...
  <appSettings>
    <add key="MyKey" value="Hello"/>
  </appSettings>
  ...
</configuration>
 

Note that the buildpack format is changing from V2 to V3 which will introduce a lot of extra flexibility; however, the mechanics of the contract will change significantly. At this time of writing, V3 buildpack is not supported on Windows. The buildpack template used is targeting V2 format.

Leave a comment

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