frab (https://github.com/frab/frab) is an opensource conference management system. A docker-ized version is already provided upstream. However I wanted to have it run on OpenShift without the need of any additional security context constraints (SCC).

Without any change to the container image, you would need the anyuid SCC added to the serviceaccount which runs the pod because the container is using a specific user. I wanted to deploy frab on a managed OpenShift Container Platform where I don’t have the permissions to do that. Furthermore it shouldn’t be necessary.

So, basically what I am doing is to build the container image from source code, staying as close as possible to the origial.

This is the Dockerfile I came up with (which probably can be enhanced, but for now it’s good enough for me):

FROM ruby:latest
  
RUN apt-get update && \
    DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs file imagemagick git && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

ENV FRAB_HOME=/opt/frab/app
ENV HOME=$FRAB_HOME

VOLUME $FRAB_HOME/public
COPY . $FRAB_HOME

RUN mkdir -p $FRAB_HOME
RUN chgrp -R 0 $FRAB_HOME && \
    chmod -R g=u $FRAB_HOME
RUN chmod g=u $FRAB_HOME /etc/passwd

USER 10001

WORKDIR $FRAB_HOME

RUN bundle install

RUN cp config/database.yml.template config/database.yml

EXPOSE 3000

ENV RACK_ENV=production \
    SECRET_KEY_BASE=asdkjf3245jsjfakjq435jadsgjlkq4j5jwj45jasdjvlj \
    FRAB_HOST=localhost \
    FRAB_PROTOCOL=http \
    RAILS_SERVE_STATIC_FILES=true \
    RAILS_LOG_TO_STDOUT=true \
    CAP_USER=frab \
    FROM_EMAIL=frab@localhost \
    SMTP_ADDRESS=172.17.0.1 \
    SMTP_PORT=25 \
    HOME=$FRAB_HOME \
    DATABASE_URL=sqlite3://localhost/opt/frab/app/data/database.db

ENV GEM_HOME="/usr/local/bundle"
ENV PATH $GEM_HOME/bin:$GEM_HOME/gems/bin:$PATH

CMD ["./docker-cmd.sh"]

Alongside with that, these are the OpenShift resources I am using:

  • in case you want to build the image in OpenShift - BuildConfig:
apiVersion: build.openshift.io/v1
kind: BuildConfig
metadata:
  labels:
    build: frab
  name: frab
  namespace: frab
spec:
  failedBuildsHistoryLimit: 5
  nodeSelector: null
  output:
    to:
      kind: ImageStreamTag
      name: 'frab:latest'
  postCommit: {}
  resources: {}
  runPolicy: Serial
  source:
    binary: {}
    type: Binary
  strategy:
    dockerStrategy:
      env:
        - name: BUNDLE_WITHOUT
          value: 'development:test:mysql:sqlite3'
    type: Docker
  successfulBuildsHistoryLimit: 5
  triggers: []
  • A slighlty modified postgres template since frab needs the admin password:
apiVersion: template.openshift.io/v1
kind: Template
labels:
  template: postgresql-persistent-template
message: |-
  The following service(s) have been created in your project: ${DATABASE_SERVICE_NAME}.

         Username: ${POSTGRESQL_USER}
         Password: ${POSTGRESQL_PASSWORD}
    Database Name: ${POSTGRESQL_DATABASE}
    Admin Password: ${POSTGRESQL_ADMIN_PASSWORD}
   Connection URL: postgresql://${DATABASE_SERVICE_NAME}:5432/

  For more information about using this template, including OpenShift considerations, see https://github.com/sclorg/postgresql-container/.
metadata:
  annotations:
    description: |-
      PostgreSQL database service, with persistent storage. For more information about using this template, including OpenShift considerations, see https://github.com/sclorg/postgresql-container/.

      NOTE: Scaling to more than one replica is not supported. You must have persistent volumes available in your cluster to use this template.
    iconClass: icon-postgresql
    openshift.io/display-name: PostgreSQL
    openshift.io/documentation-url: https://docs.openshift.org/latest/using_images/db_images/postgresql.html
    openshift.io/long-description: This template provides a standalone PostgreSQL
      server with a database created.  The database is stored on persistent storage.  The
      database name, username, and password are chosen via parameters when provisioning
      this service.
    openshift.io/provider-display-name: Red Hat, Inc.
    openshift.io/support-url: https://access.redhat.com
    tags: database,postgresql
  name: postgresql-persistent
  namespace: openshift
objects:
- apiVersion: v1
  kind: Secret
  metadata:
    annotations:
      template.openshift.io/expose-database_name: '{.data[''database-name'']}'
      template.openshift.io/expose-password: '{.data[''database-password'']}'
      template.openshift.io/expose-username: '{.data[''database-user'']}'
      template.openshift.io/expose-admin-password: '{.data[''database-admin-password'']}'
    name: ${DATABASE_SERVICE_NAME}
  stringData:
    database-name: ${POSTGRESQL_DATABASE}
    database-password: ${POSTGRESQL_PASSWORD}
    database-user: ${POSTGRESQL_USER}
    database-admin-password: ${POSTGRESQL_ADMIN_PASSWORD}
- apiVersion: v1
  kind: Service
  metadata:
    annotations:
      template.openshift.io/expose-uri: postgres://{.spec.clusterIP}:{.spec.ports[?(.name=="postgresql")].port}
    name: ${DATABASE_SERVICE_NAME}
  spec:
    ports:
    - name: postgresql
      nodePort: 0
      port: 5432
      protocol: TCP
      targetPort: 5432
    selector:
      name: ${DATABASE_SERVICE_NAME}
    sessionAffinity: None
    type: ClusterIP
  status:
    loadBalancer: {}
- apiVersion: v1
  kind: PersistentVolumeClaim
  metadata:
    name: ${DATABASE_SERVICE_NAME}
  spec:
    accessModes:
    - ReadWriteOnce
    resources:
      requests:
        storage: ${VOLUME_CAPACITY}
- apiVersion: v1
  kind: DeploymentConfig
  metadata:
    annotations:
      template.alpha.openshift.io/wait-for-ready: "true"
    name: ${DATABASE_SERVICE_NAME}
  spec:
    replicas: 1
    selector:
      name: ${DATABASE_SERVICE_NAME}
    strategy:
      type: Recreate
    template:
      metadata:
        labels:
          name: ${DATABASE_SERVICE_NAME}
      spec:
        containers:
        - capabilities: {}
          env:
          - name: POSTGRESQL_USER
            valueFrom:
              secretKeyRef:
                key: database-user
                name: ${DATABASE_SERVICE_NAME}
          - name: POSTGRESQL_PASSWORD
            valueFrom:
              secretKeyRef:
                key: database-password
                name: ${DATABASE_SERVICE_NAME}
          - name: POSTGRESQL_DATABASE
            valueFrom:
              secretKeyRef:
                key: database-name
                name: ${DATABASE_SERVICE_NAME}
          - name: POSTGRESQL_ADMIN_PASSWORD
            valueFrom:
              secretKeyRef:
                key: database-admin-password
                name: ${DATABASE_SERVICE_NAME}
          image: ' '
          imagePullPolicy: IfNotPresent
          livenessProbe:
            exec:
              command:
              - /usr/libexec/check-container
              - --live
            initialDelaySeconds: 120
            timeoutSeconds: 10
          name: postgresql
          ports:
          - containerPort: 5432
            protocol: TCP
          readinessProbe:
            exec:
              command:
              - /usr/libexec/check-container
            initialDelaySeconds: 5
            timeoutSeconds: 1
          resources:
            limits:
              memory: ${MEMORY_LIMIT}
          securityContext:
            capabilities: {}
            privileged: false
          terminationMessagePath: /dev/termination-log
          volumeMounts:
          - mountPath: /var/lib/pgsql/data
            name: ${DATABASE_SERVICE_NAME}-data
        dnsPolicy: ClusterFirst
        restartPolicy: Always
        volumes:
        - name: ${DATABASE_SERVICE_NAME}-data
          persistentVolumeClaim:
            claimName: ${DATABASE_SERVICE_NAME}
    triggers:
    - imageChangeParams:
        automatic: true
        containerNames:
        - postgresql
        from:
          kind: ImageStreamTag
          name: postgresql:${POSTGRESQL_VERSION}
          namespace: ${NAMESPACE}
        lastTriggeredImage: ""
      type: ImageChange
    - type: ConfigChange
  status: {}
parameters:
- description: Maximum amount of memory the container can use.
  displayName: Memory Limit
  name: MEMORY_LIMIT
  required: true
  value: 512Mi
- description: The OpenShift Namespace where the ImageStream resides.
  displayName: Namespace
  name: NAMESPACE
  value: openshift
- description: The name of the OpenShift Service exposed for the database.
  displayName: Database Service Name
  name: DATABASE_SERVICE_NAME
  required: true
  value: postgresql
- description: Username for PostgreSQL user that will be used for accessing the database.
  displayName: PostgreSQL Connection Username
  from: user[A-Z0-9]{3}
  generate: expression
  name: POSTGRESQL_USER
  required: true
- description: Password for the PostgreSQL connection user.
  displayName: PostgreSQL Connection Password
  from: '[a-zA-Z0-9]{16}'
  generate: expression
  name: POSTGRESQL_PASSWORD
  required: true
- description: Password for the PostgreSQL admin user.
  displayName: PostgreSQL Admin Password
  from: '[a-zA-Z0-9]{16}'
  generate: expression
  name: POSTGRESQL_ADMIN_PASSWORD
  required: true
- description: Name of the PostgreSQL database accessed.
  displayName: PostgreSQL Database Name
  name: POSTGRESQL_DATABASE
  required: true
  value: sampledb
- description: Volume space available for data, e.g. 512Mi, 2Gi.
  displayName: Volume Capacity
  name: VOLUME_CAPACITY
  required: true
  value: 1Gi
- description: Version of PostgreSQL image to be used (9.4, 9.5, 9.6 or latest).
  displayName: Version of PostgreSQL Image
  name: POSTGRESQL_VERSION
  required: true
  value: "9.6"
  • a PVC for the app data of frab, so that the assets like attachments and alike aren’t lost after a container restart:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: frab-appdata
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1G
  • the DeploymentConfig:
apiVersion: apps.openshift.io/v1
kind: DeploymentConfig
metadata:
  labels:
    app: frab
  name: frab
spec:
  replicas: 1
  selector:
    app: frab
    deploymentconfig: frab
  strategy:
    activeDeadlineSeconds: 21600
    recreateParams:
      timeoutSeconds: 600
    resources: {}
    type: Recreate
  template:
    metadata:
      labels:
        app: frab
        deploymentconfig: frab
    spec:
      containers:
        - env:
            - name: FRAB_DBPASSWORD
              valueFrom:
                secretKeyRef:
                  key: database-admin-password
                  name: postgresql
            - name: FRAB_DBNAME
              valueFrom:
                secretKeyRef:
                  key: database-name
                  name: postgresql
            - name: FRAB_DBHOST
              value: postgresql
            - name: TZ
              value: Europe/Berlin
            - name: SECRET_KEY_BASE
              valueFrom:
                secretKeyRef:
                  key: SECRET_KEY_BASE
                  name: frab-secret-key-base
            - name: FRAB_HOST
              value: localhost
            - name: FROM_EMAIL
              value: frab@localhost
            - name: SMTP_ADDRESS
              value: 172.17.0.1
            - name: SMTP_PORT
              value: '25'
            - name: BUNDLE_WITHOUT
              value: 'development:test:mysql:sqlite3'
          image: quay.io/jaeichle/frab:0.9
          imagePullPolicy: Always
          name: frab
          ports:
            - containerPort: 3000
              protocol: TCP
          readinessProbe:
            failureThreshold: 3
            httpGet:
              path: /
              port: 3000
              scheme: HTTP
            initialDelaySeconds: 30
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
          resources:
            limits:
              cpu: '1'
              memory: 512Mi
            requests:
              cpu: '1'
              memory: 512Mi
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
          volumeMounts:
            - mountPath: /home/frab/app/public
              name: appdata
      imagePullSecrets:
        - name: jaeichle-openpaas-pull-secret
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
      volumes:
        - name: appdata
          persistentVolumeClaim:
            claimName: frab-appdata
  test: false
  triggers:
  • A service:
apiVersion: v1
kind: Service
metadata:
  labels:
    app: frab
  name: frab
spec:
  ports:
    - port: 3000
      protocol: TCP
      name: 3000-tcp
      targetPort: 3000
  selector:
    app: frab
  sessionAffinity: None
  type: ClusterIP
  • A template to create frab secrets:
apiVersion: template.openshift.io/v1
kind: Template
labels:
  template: frab-secret-keybase-secret
message: |-
  The following secret has been created in your project: ${FRAB_SECRET_KEYBASE_SECRET}.
metadata:
  annotations:
    tags: frab
  name: frab-secret-key-base-secret-template
objects:
- apiVersion: v1
  kind: Secret
  metadata:
    annotations:
    name: ${FRAB_SECRET_KEYBASE_SECRET}
  stringData:
    SECRET_KEY_BASE: ${FRAB_SECRET_KEYBASE}
parameters:
- description: The name of the OpenShift secret for the frab secret key base.
  displayName: Frab Secret Key Base
  name: FRAB_SECRET_KEYBASE_SECRET
  required: true
  value: frab-secret-key-base
- description: Password for the PostgreSQL connection user.
  displayName: PostgreSQL Connection Password
  from: '[a-zA-Z0-9]{32}'
  generate: expression
  name: FRAB_SECRET_KEYBASE
  required: true
  • And the route to be able to access it:
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  labels:
    app: frab
  name: frab
  namespace: frab
spec:
  host: frab.openshift.example.com
  port:
    targetPort: 3000-tcp
  tls:
    insecureEdgeTerminationPolicy: Redirect
    termination: edge
  to:
    kind: Service
    name: frab
    weight: 100
  wildcardPolicy: None

After that’s all there I just did:

  1. Make sure quay secret is there
  2. oc process -f postgresql-template.yaml | oc apply -f -
  3. oc create -f frab-appdata-pvc.yaml
  4. oc process -f secret-template.yaml | oc create -f -
  5. oc create -f deploymentconfig.yaml
  6. oc create -f route.yaml

An easier way would be to create a template for the whole application, but that’s for another time when there’s more time.