Skip to content

Containers

Containers are the heart of any workload. No matter what workload type are you dealing with (Deployment, Job, StatefulSet etc.), you'll have to define containers for it - but the good news is that container API object is the same for any workload, so you'll need to learn it once and use in any manifest.

All containers in K8S Framework must implement ContainerInterface. Containers returned from method containers() in workload. You may return array of them, or (which looks better) use yield keyword, for examle:

public function containers(): iterable
{
    yield new PhpFpmContainer();
    yield new NginxContainer();
}

Also every workload type has Container-prefixed abstract class, which is a recommended way of writing your manifest: for example, there is AbstractDeplyment class that you may inherit, but there is also Container-prefixed class AbstractContainerDeployment, which implements both DeploymentInterface and ContainerInterface - by inheriting this class, you'll make your manifest much more descriptive - define your "main" container methods in deployment class itself, and if you also need some auxiliary containers - rewrite containers() method amd return them as well.

For example, if your deployment ExampleDeployment has php-fpm and nginx containers - just extend AbstractContainerDeployment class, define php-fpm container method in ExampleDeployment itself (since php-fpm holds your application code and may be considered as "main" container), and that rewrite your containers() method as follows:

class ExampleDeployment extends AbstractContainerDeployment
{
    public function containers(): iterable
    {
        yield $this;
        yield new NginxContainer();
    }

    //...
}

Separate container classes, like NginxContainer from example above, should be defined in Container namespace inside your app.

Tip

App's directory structure is explained in Apps article.

Since every container class must implement ContainerInterface, there is an abstract class AbstractContainer for you, which helps you with that by defining most of the methods from ContainerInterface with reasonable defaults. You then just have to modify what should be modified:

namespace App\K8S\Hello\Container;

use Dealroadshow\K8S\Framework\Core\Container\AbstractContainer;
use Dealroadshow\K8S\Framework\Core\Container\Image\Image;

class NginxContainer extends AbstractContainer
{
    public function image(): Image
    {
        return Image::fromName('nginx');
    }
}

This is a perfectly valid container class. AbstractContainer requires from you to define only image() method.

Container images

All containers are created from images, so every container needs one. Defining image your container will use is as simple as defining image() method in your manifest, as shown in the example above. As you see, this method must return instance of Image class. Image is a pretty easey to use value-object. Basically, you can just use one of the static constructors: Image::fromName() or Image::fromString(). The difference between two is that Image::fromString expects full image information as parameter - along with repository url, image tag and so on. For example, if you image is stored in AWS ECR and you want to deploy tag 1.2.3 of your my-organization/cool-application image, your method image() may look as follows:

public function image(): Image
{
    return Image::fromString(
        '49367101234912.dkr.ecr.us-east-1.amazonaws.com/my-organization/cool-application:1.2.3'
    );
}

If you prefer using Image::fromName() constructor, this is how method image() would like if you want to return the same image as above:

public function image(): Image
{
    return Image::fromName('cool-application')
        ->setPrefix('my-organization')
        ->setTag('1.2.3')
        ->setRegistryUrl('49367101234912.dkr.ecr.us-east-1.amazonaws.com');
}

But the one thing that may come to your mind, is "It's not cool to modify image() method to change tag every time I need to deploy new version of my application". And you are absolutely right. This problem above is pretty easy to solve by using environment variable in code and passing this variable from whatever CI/CD tool you use. You may write setTag(getenv('MY_APPLICATION_TAG')) instead of setTag(1.2.3) in your code. But even after that - if you use Kubernetes, there are probably plenty of different images built in your organization, so you'll need to repeat setRegistryUrl() and setPrefix() parts many times. There is a solution in K8S Framework, specifically created to address issues like that. If you want to want to avoid such repetitiveness in your code - please read Images Middleware.

Defining environment variables for container

Passing environment variables to your containers are one of the most often used features in Kubernetes manifests. All environment variables for your container are defined in env() method. Just after your IDE have generated this method for you, it should look like follows:

public function env(EnvConfigurator $env): void
{
}

To define single env variable - use $env->var() method:

Simple env variable

public function env(EnvConfigurator $env): void
{
    $env
        ->var('VARIABLE_NAME', 'SOME VALUE')
        ->var('FOO', 'Bar!');
}

In Kubernetes manifests you can also define env variable that gets it's value from a plenty of different sources:

Env variable from ConfigMap or Secret

Env variable may get it's value from some key in ConfigMap or Secret. K8S Framework follows simple rule: You don't deal with manifest names in manifest classes, since simple typo in name of resource you want to use will lead to errors that's not immediately detectable. Instead, framework makes it easy to deal with connections between different Kubernetes resources by using their class names:

public function env(EnvConfigurator $env): void
{
    $env
        ->varFromConfigMap(
            varName: 'MY_VAR',
            configMapClass: SomeConfigMap::class,
            configMapKey: 'someKey'
        )
        ->varFromSecret(
            varName: 'MY_VAR',
            secretClass: SomeSecret::class,
            secretKey: 'someKey'
        )
    ;
}

Env variable from container resources

Kubernetes allows you to pass your resources requests and limits as values of env variables:

public function env(EnvConfigurator $env): void
{
    $env
        ->varFromContainerResources(
            varName: 'MY_CPU_LIMITS',
            field: ContainerResourcesField::cpuLimits()
        )
        ->varFromContainerResources(
            varName: 'MY_MEMORY_REQUESTS',
            field: ContainerResourcesField::memoryRequests()
        )
    ;
}

Env variable from pod fields

Kubernetes also allows to pass some info about pod:

public function env(EnvConfigurator $env): void
{
    $env
        ->varFromPod(
            varName: 'MY_IP',
            podField: PodField::podIp()
        )
        ->varFromPod(
            varName: 'MY_METADATA_LABELS',
            podField: PodField::metadataLabels()
        )
    ;
}

Import all keys from ConfigMap or Secret as env variables

Sometimes you'll want to import an entire ConfigMap or Secret as env variables source. So all keys of ConfigMap or Secret will be names of environment variables:

public function env(EnvConfigurator $env): void
{
    $env
        ->addConfigMap(SomeConfigMap::class)
        ->addSecret(SomeSecret::class)
    ;
}

Or, even simpler:

public function env(EnvConfigurator $env): void
{
    $env
        ->addFrom(SomeConfigMap::class)
        ->addFromClasses(
            SomeOtherConfigMap::class,
            SomeSecret::class,
            SomeOtherSecret::class
        )
    ;
}

Using env ConfigMaps or Secrets that are in different app

Examples above will work only if ConfigMaps and Secrets you add belong to the same app as your container class.

For example, if you have container class K8S\Hello\Container\NginxContainer, where K8S\Hello is your app namespace, and you are trying to add variables from K8S\World\Manifest\SomeConfigMap as above - it will not work. Using env sources from other apps is considered a bad practice, since it will couple your apps, and it means that your apps are not structured correctly. But if you absolutely have to do this - here is how it's done:

public function env(EnvConfigurator $env): void
{
    $env
        ->withExternalApp(WorldApp::name())
            ->addFrom(SomeConfigMap::class)
        ->withExternalApp(SomeOtherApp::name())
            ->addFromClasses(YetOneConfigMap::class, YetOneSecret::class)
    ;
}

Method EnvConfigurator::withExternalApp() returns other EnvConfigurator instance, configured to look for env source classes in other app, which name you passed to this method as parameter.

If it's not absolutely clear to you, what does it mean for manifest "to belong to app" - please read Apps article.

Volumes and VolumeMounts

An issue with Volumes and VolumeMounts is that VolumeMounts is a part of container specification, while Volumes are defined on pod spec level, so volumes() method is not a part of ContainerInterface. This is one more reason to use Container-prefixed workloads, such as AbstractContainerDeployment, AbstractContainerCronjob etc. By extending this classes you'll have methods volumes() and volumeMounts() in one class. Here is an example using our WorldDeployment class from Getting Started:

WorldDeployment.php
class WorldDeployment extends AbstractContainerDeployment
{
    private const VOLUME_TEMP_DIR = 'temp-dir';
    private const VOLUME_PHP_CONF = 'php-conf';

    //...

    public function volumes(VolumesConfigurator $volumes): void
    {
        $volumes
            ->fromEmptyDir(self::VOLUME_TEMP_DIR)
            ->setSizeLimit(Memory::mebibytes(100))
            ->useRAM();

        $volumes->fromConfigMap(self::VOLUME_PHP_CONF, PhpConfigMap::class);
    }

    public function volumeMounts(VolumeMountsConfigurator $mounts): void
    {
        $mounts->add(self::VOLUME_TEMP_DIR, '/tmp');
        $mounts->add(self::VOLUME_PHP_CONF, '/etc/php/');
    }

    //...
}

Again, as with env sources, if ConfigMap you want to mount belongs to another app - use withExternalApp() method:

public function volumes(VolumesConfigurator $volumes): void
{
    $volumes
        ->withExternalApp(SomeOtherApp::name())
        ->fromConfigMap(self::VOLUME_PHP_CONF, PhpConfigMap::class);
}

Summary

Please see ContainerInterface in order to see what methods are available for you to define in this class.