The PaaSTA Contract

The PaaSTA Contract is similar to the 12 Factor App documented for Heroku. It specifies what kinds of apps are suitable for PaaSTA and what that app must do to run properly.

Basic requirements

Codebase

PaaSTA assumes that the source for a service is stored in a single Git repository, which can produce a single Docker image. The image MUST run that service, though the same image MAY support multiple use cases (worker daemon, web task, etc).

Dependencies

The Docker image MUST contain all the code necessary to run the service.

State

PaaSTA services SHOULD be stateless – that is, no irreplaceable information should be stored by your service. Generally, services should store their state in external datastores like MySQL, Cassandra, or S3. Services MAY do filesystem IO, but everything written to disk will be discarded whenever the container dies. (Containers are killed for many reasons: autoscaling, deploying a new version of your code, most changes to yelpsoa-configs, etc.)

If you want to run a system that stores important state on disk, we recommend running a Kubernetes Operator.

Logs

PaaSTA will log the stdout/stderr of your service, and this is accessible via the paasta logs command. Services that want structured logs should use an external log system: at Yelp we use Monk (usually accessed via the clog library).

Bouncing

PaaSTA reserves the right to cause a bounce (our term for when all of your service’s containers are replaced) on your service at any time. Reasons for bouncing your service include:

  • To deploy a new version of your service
  • To adjust the configuration of your service – most changes to yelpsoa-configs will require a bounce. Some changes to the Paasta codebase may also cause your service to be restarted, e.g. if the default values for configuration change.
  • paasta restart.

Please make sure your service can handle this. Generally, stateless HTTP services should have no trouble being bounced – Paasta will work with the service mesh to stop traffic going to a container before killing it. See the docs on bouncing for bounce settings that can help make bounces less impactful to your service.

Configuration

Each service must have one or more “instances” defined in yelpsoa-configs. Each instance is an independent configuration of your service, specifying things like environment variables, the command to run within your container, and autoscaling parameters.

See the yelpsoa-configs documentation.

HTTP/TCP services

  • MUST be discoverable by the service mesh:
    • This requires an entry in smartstack.yaml for the registration used by that instance (which defaults to the instance name, i.e. main).
  • MUST bind to the port specified by the $PAASTA_PORT environment variable. (This is almost always 8888, but it is wise to use the variable instead of hard-coding.).

Long-running tasks that don’t listen on a port

Some services (or some instances of some services) want to run indefinitely like a HTTP/TCP service, but don’t need accept incoming traffic. Examples include queue workers, chat bots, and other programs that make outbound connections to request work.

To do this:

  • Don’t create an entry in smartstack.yaml for this instance.

  • Don’t set registrations in kubernetes-{cluster}.yaml:

    # kubernetes-norcal-devc.yaml
    ---
    main:
      instances: 3
    worker:
      instances: 1
    # smartstack.yaml
    main:
      proxy_port: 12345
    # (no `worker` definition in smartstack.yaml!)
    
  • MAY set healthcheck_mode to cmd and specify a healthcheck_cmd in kubernetes-<cluster>.yaml to give Kubernetes better insight into the health of a task:

    # kubernetes-clustername.yaml
    ---
    queue_worker:
      healthcheck_mode = "cmd"
      healthcheck_cmd = "/some_status_command.py"
    

Without specifying a healthcheck command, each replica is considered healthy until it exits (crashes or otherwise stops running).

Deployment Workflow