Factories

Factories allow you to link multiple bots together and deploy them all from a single command. You can decide whether to deploy bots locally, or in the AWS account of the invoking user. qalx handles building the entire factory for you and deploying all the bots.

Note

A note on terminology:

Stage: A specific grouping of types (aws, local), sectors and bots that get deployed at the same time. A stage could have multiple sectors of different types, each with multiple bots.

Stack A stack is a collection of sectors of a given type. A single stack could contain multiple sectors with multiple bots. Sectors within a stack are grouped based on the given region_name.

Sector: The specific server that a bot will get deployed to. This could be local for bots that run local to where you run the factory commands, or aws for bots that run in the AWS account of the invoking user. A sector could define some or all of the bots defined in the top level bots key

Workflow: When a bot has finished processing a job you can configure workflows to define which bot(s) the job gets passed to next

A factory consists of multiple stages:

  • Plan - Codifying your factory in a factory-plan.yml file

  • Validate - Validating that your factory-plan.yml as the correct format

  • Pack - Downloading your bot code and preparing for build

  • Build - Building your factory

  • Demolish - Demolishing your factory

Stages can be interacted with either via the command line (recommended) or can be invoked via code.

Plan

The root of a factory is the factory-plan.yml file. Below is an example configuration. See validate for specific member level validation and description of members

Example factory_plan.yml file
# Namespaced as factory to leave room for extra keys
# at the `bots` level in the future
factory:
  # factory names don't have to be unique
  name: my_factory_name
  # bots are a mapping from the name of the
  # bot to details about source/config
  bots:
    my_bot_1:
      source:
        url: ssh://fredsmith@bitbucket.org/my-bots-repo
        branch: develop
      # This will effectively call `bot-start -p 1 bots_package.the_bots:bot1`
      bot_path: bots_package.the_bots:bot1
    my_bot_2:
      source:
        path: path/to/local/code
      bot_path: bots:bot2
    my_bot_3:
      source:
        pypi: many-useful-bots
      bot_path: many_useful_bots:bot_3
    my_bot_4:
      source:
        url: https://github.com/some/repo.git
      # This will effectively call `bot-start -p 1 bots_package.the_bots:bot1`
      bot_path: bots_package.the_bots:bot4
    my_bot_5:
      source:
        pypi: my-bot-package
        version: 1.5.9 # An optional version can be included
      bot_path: many_useful_bots:bot_5
  # stages are mappings from a stage name to a sector.
  # Each sector then specifies the bots running and the arguments
  # to pass to `bot.start()
  stages:
    test:
      local_sector:
        type: local
        bots:
          my_bot_1:
            # There could be other arguments here that would get passed
            # to bot.start()
            queue-name: my_bot_1-test-local-queue-name
            processes: 1
          my_bot_2:
            queue-name: my_bot_2-test-local-queue-name
            processes: 1
      another_local_sector:
        type: local
        bots:
          my_bot_3:
            queue-name: my_bot_3-test-local-queue-name
            processes: 1
    dev:
      # Specify a workflow for how your bots interact on this stage
      workflow: flow_1
      local_sector:
        type: local
        bots:
          my_bot_1:
            queue-name: my_bot_1-dev-local-queue-name
            processes: 1
      another_local_sector:
        type: local
        bots:
          my_bot_3:
            queue-name: my_bot_3-dev-local-queue-name
            processes: 5
      ec2_box_1:
        type: aws
        # The optional alarm to use for this instance.
        alarm: alarm1
        parameters:
          # The parameters to use when setting up EC2 instances
          ImageId: ami-0242408af868
          InstanceType: t2.nano
          KeyName: keyname # name of key-pair to secure the instance
          NetworkInterfaces:
            - DeviceIndex: 0
              AssociatePublicIpAddress: true
              SubnetId: subnet-19d539c3545
              GroupSet:
                # This should be created manually in your AWS account
                - sg-8c422def392d
        bots:
          my_bot_2:
            queue-name: my_bot_2-dev-aws-queue-name
            processes: 4
      ec2_box_2:
        type: aws
        parameters:
          ImageId: ami-3f348b23ee98
          InstanceType: t2.micro
          KeyName: keyname
          NetworkInterfaces:
            - DeviceIndex: 0
              AssociatePublicIpAddress: true
              SubnetId: subnet-19d539c3545
              GroupSet:
                - sg-8c422def392d
        bots:
          my_bot_3:
            queue-name: my_bot_3-dev-aws-queue-name
            processes: 1
      ec2_box_3:
        type: aws
        region_name: eu-west-2
        parameters:
          ImageId: ami-3f348b23ee98
          InstanceType: t2.nano
          KeyName: keyname
          NetworkInterfaces:
            - DeviceIndex: 0
              AssociatePublicIpAddress: true
              SubnetId: subnet-19d539c3545
              GroupSet:
                - sg-8c422def392d
        bots:
          my_bot_3:
            queue-name: my_bot_3-dev-aws-queue-name
            processes: 1
      ec2_box_4:
        type: aws
        region_name: eu-west-1
        parameters:
          ImageId: ami-62cee09a7747
          InstanceType: t2.nano
          KeyName: keyname
          NetworkInterfaces:
            - DeviceIndex: 0
              AssociatePublicIpAddress: true
              SubnetId: subnet-45f96310c85a
              GroupSet:
                - sg-0c9c041e6559
        bots:
          # multiple bots can be started on a single instance
          my_bot_3:
            queue-name: my_bot_3-dev-aws-queue-name
            processes: 1
          my_bot_4:
            queue-name: my_bot_4-dev-aws-queue-name
            processes: 2
    prod:
      workflow: flow_1
      local_connection:
        type: local
        bots:
          my_bot_1:
            queue-name: my_bot_1-prod-local-queue-name
            processes: 1
      ec2_box:
        type: aws
        region_name: eu-west-2
        parameters:
          ImageId: ami-3f348b23ee98
          InstanceType: t2.micro
          KeyName: keyname
          NetworkInterfaces:
            - DeviceIndex: 0
              AssociatePublicIpAddress: true
              SubnetId: subnet-19d539c3545
              GroupSet:
                # This will have to be created manually by the user in their AWS account
                - sg-8c422def392d
        bots:
          my_bot_2:
            queue-name: my_bot_2-prod-aws-queue-name
            processes: 10
          my_bot_3:
            queue-name: my_bot_3-prod-aws-queue-name
            processes: 10
  meta:
    key: value
  # These get saved on the FactoryBuild object in the API and work like normal tags
  tags:
    key: value
  # The bots that an entity should be passed to once it has finished processing.
  # This is restricted to only bots defined on this factory.
  workflows:
    flow_1:
      my_bot_1:
        - my_bot_2
        - my_bot_3:
            my_bot_4
    flow_2:
      my_bot_1:
        my_bot_2:
          my_bot_3
  # A dictionary of alarms that are available to this factory.  Can only
  # be applied to AWS sectors.  Configuration options match the CloudFormation
  # options for a Cloudwatch Alarm.  The Dimensions key is not required
  # as this is built automatically by pyqalx
  alarms:
    alarm1:
      MetricName: CPUUtilization
      Statistic: Average
      Period: 60
      ComparisonOperator: LessThanThreshold
      EvaluationPeriods: 10
      Threshold: '5'
      Namespace: AWS/EC2
      AlarmActions:
        - terminate

Aliases

Factories use pyyaml under the hood. So you are able to build complex plans using all the power of yaml. A common use case is the ability to reuse certain sections of your plan in order to keep code dry.

Take for example the below:

Duplicated sectors without aliases
...snip...
stages:
   prod:
      ec2_box:
        type: aws
        region_name: eu-west-2
        parameters:
          ImageId: ami-3f348b23ee98
          InstanceType: t2.micro
      ec2_box2:
        type: aws
        region_name: eu-west-2
        parameters:
          ImageId: ami-3f348b23ee98
          InstanceType: t2.micro
...snip...

You could instead take advantage of yaml aliases to avoid having to duplicate the configuration for ec2_box_2. You can even override certain keys

Duplicated sectors using aliases
...snip...
stages:
   prod:
      ec2_box: &my_alias_name
        type: aws
        region_name: eu-west-2
        parameters:
          ImageId: ami-3f348b23ee98
          InstanceType: t2.micro
      ec2_box2:
         <<: *my_alias_name
         # Start this sector in eu-west-1 but keep
         # all other settings the same
         region_name: eu-west-1
...snip...

Validate

qalx factory-validate --plan=<path-to-your-factory-plan.yml>

Validation is the first stage in building a factory. This ensures that the factory-plan.yml file is well formatted and that the required keys exist.

Why would I want to run this command?

You may want to run the validate command to check that the factories-plan.yml file is structured correctly before attempting to build your factory

Keys and validation requirements

Below is an overview of a sample factories-plan.yml.

factory (required): Root level key

bots (required): A mapping of bot name to source. At least one bot is required. The source could be a private repository, a path to local code or a package name on pypi

Required sub keys:
  • source

  • bot_path

Example bots
bots:
  my_bot_1: # The name of the bot that will be used on this factory.
     source:
         # A private repository.  Provide key:value arguments
         # here that should get passed to `git clone`.
         # Uses local ssh credentials to authenticate in the same
         # way that `git clone git@bitbucket.org/my-bots-repo` does
         url: git@bitbucket.org/my-bots-repo
         branch: develop
         bare: true
     # The path to the bot.  This should be in the same format
     # as when doing `qalx bot-start TARGET
     bot_path: bots_package.the_bots:bot1
  my_bot_2:
     source:
         # A bot stored locally
         path: path/to/local/code
     bot_path: bots:bot2
  my_bot_3:
     source:
         # A package name on pypi
         pypi: many-useful-bots
     bot_path: many_useful_bots:bot_3
  my_bot_4:
     source:
         url: https://github.com/some/repo.git
     bot_path: bots_package.the_bots:bot4

stages (required): A mapping of stages to sector details. You can have as many stages as you want. Stages are useful if you want to test bots on smaller hardware and with less resources or with test data before deploying them to production instances. At least one stage is required. A stage could consist of local and aws sectors.

Required sub keys for a sector:
  • type (valid choices local, aws)

  • bots

Example local stage
   # A local stage is one that will deploy bots to the same
   # machine as the factory build command is run

   stages: # A mapping of stage name to a sector
     test: # The name of this stage
         workflow: flow_1 # (optional) The name of the workflow this stage should use
         local_connection: # The name of the sector
           type: local # The type of the sector.
           bots: # A mapping of bots that should be active on this sector
             my_bot_1:
               # A mapping of arguments that get passed to `bot-start`
               queue-name: my_bot_1_test_queue
               processes: 1
               entity-class: dotted.path.to:MyQalxEntityClass
             my_bot_2:
               queue-name: my_bot_2_test_queue
               processes: 1
             my_bot_3:
               queue-name: my_bot_3_test_queue
               processes: 1
Example AWS stage
   # A stage that defines a single AWS sector.  This will deploy the bots
   # to the users AWS account who invoked the factory build command

   stages: # A mapping of stage name to a sector
      dev: # The name of this stage
        ec2_box_1:  # The name of the sector
           type: aws # The type of the sector.
           alarm: alarm1 # The optional name of the Cloudwatch Alarm to use for this instance
           region_name: eu-west-2 # An optional region to start this sector in
           parameters:
             # The parameters to use when setting up EC2 instances
             ImageId: ami-0242408af868
             InstanceType: t2.nano
             KeyName: keyname # name of key-pair to secure the instance
             NetworkInterfaces:
               - DeviceIndex: 0
                 AssociatePublicIpAddress: true
                 SubnetId: subnet-19d539c3545
                 GroupSet:
                   # This should be created manually in your AWS account
                   - sg-8c422def392d
           bots:
             my_bot_2:
               queue-name: my_bot_2-dev-aws-queue-name
               processes: 4

Warning

Ensure that any EC2 Security Groups are created manually before trying to build the factory

meta (not required): A mapping of key:value for the meta keys that should be saved on the factory

tags (not required): A mapping of key:value for the tags that should be saved on the factory. See permissions for more info on tags

workflows (not required): Workflows enable you to define bot(s) that a job should get passed to when a bot has finished processing. Workflows are optional for each stage. Bots defined in workflows must exist on this plan.

Required sub keys:
  • bot name

Example workflow
workflows:
     flow_1:  # The name of the workflow
       -my_bot_1: # The bot that should pass the job
         # A list of bots that `my_bot_1` will pass the job to when it has
         # finished processing.  This enables you to fan a job out to multiple bots
         - my_bot_2
         - my_bot_3
     flow_2:
       -my_bot_1:
         # A mapping of bots that `my_bot_1` will pass the job to when it has
         # finished processing.
         # This enables you to have a job passed from my_bot_1 -> my_bot_2 -> my_bot_3
         -my_bot_2:
           - my_bot_3

Pack

qalx factory-pack --plan=<path-to-your-factory-plan.yml> (--stage=<stage>) (--no-delete)

Packing is the process by which all the code for the defined bots are downloaded from their various sources to the local computer.

The bots are packed into the value of the FACTORY_PACK_DIR configuration option.

Note

The pack command will automatically call the validate command for you.

--stage is optional - if provided it will only download the bots for the given stage. If --stage is not provided then all the bots will be downloaded.

--no-delete is optional - By default the code gets deleted after download. Provide this switch to prevent the code from being deleted

Why would I want to run this command?

You may want to run the pack command to check that the credentials are correct for the sources and that the bots get correctly downloaded.

Sources

A bot could come from various sources

Git Repository

A remote git repository. Uses your local ssh credentials if necessary to access private repositories.

Warning

A git executable must be available on your system path in order to download from git

# Will use the local ssh credentials to download from
# a private repository
source:
   url: git@bitbucket.org/my-bots-repo
# Will download from a public repository
source:
   url: https://github.com/some/repo.git

You can provide extra key: value pairs to pass to git in the same way you would when running git clone from the command line

source:
   # Functionally equivalent to
   # git clone https://github.com/some/repo.git --branch=develop --quiet --bare

   url: https://github.com/some/repo.git
   branch: develop
   bare: true
   quiet: true

Local Code

A path to code that is local to the computer the pack command runs on.

Windows path
source:
   path: C:\\Users\\path\\to\\local\\code

Note

Remember to use double slash on Windows.

Linux path
source:
   path: /home/user/path/to/local/code

PyPi

The name of a package on pypi

pypi package
source:
   pypi: a-package-name

You can optionally specify a version of a pypi package

pypi package with version
source:
   pypi: a-package-name
   version: 1.2.3

Build

qalx factory-build --plan=<path-to-your-factory-plan.yml> --stage=<stage> (--aws-profile=<aws_profile>)

Building is the process by which the packed code for the bots, defined on a specific stage, is uploaded to qalx, and the bots started on their specific sectors.

Note

Depending on the complexity of your stage and how many remote sectors you have, the build command may take some time to complete. You will be given detailed feedback via the console and log files.

Warning

All bots for a given sector will be started in the same virtual environment. Keep this in mind if certain bots have different dependencies as this could lead to dependency issues.

Note

pyqalx will attempt to install your bot into the virtual environment for all sector types. For bots with a pypi source it will just install the appropriate file that was downloaded from pypi during packing - doing the equivalent of pip install <package_name> –upgrade. For local paths/git sources pyqalx will attempt to install in the following order:

  • pyproject.toml

  • setup.py

  • requirements.txt

For the above it will attempt to do the equivalent of pip install . –upgrade if a pyproject.toml or setup.py is found, and a pip install -r requirements.txt if a requirements.txt file is found

If you don’t want to upgrade a particular package ensure that your dependencies are pinned to the correct version.

Sector Types

A sector could be one of the following types:

  • local - The defined bots will be started on the local machine to where you ran qalx factory-build

  • aws - The defined bots will be started on an AWS EC2 instance in your AWS account.

Note

You can have multiple bots defined on an individual sector.

Warning

Sectors with a type of aws will automatically create a cloudformation stack in your AWS account for each region_name defined on your factories-plan.yml. The profile used is the one specified in the aws-profile argument. See aws for more info on what gets created and the IAM permissions required by the user who calls qalx factory-build.

local

A local sector is one where the defined bots are started on the machine local to where you run qalx factory-build.

Why would I use a local sector

You may want to use a local sector if you only have a single bot or if you want to test a factory without having to incur additional charges from your cloud hosting provider.

Example local sector
local_sector: # the name of this sector for identification purposes
   type: local
   bots: # A list of the bots that should be started
     my_bot_1: # The bot name for identification purposes
       processes: 1
     my_bot_2:
       # You can pass any argument here that you can pass to `bot-start`
       processes: 1
     my_bot_3:
       processes: 1

aws

An AWS sector is one where the defined bots are created on brand new EC2 instances on your AWS account. The aws-profile optional CLI argument specifies the AWS profile to be used for remote sectors. The default profile is used if this argument is not provided

Why would I use an aws sector

You may want to use an AWS sector if you have multiple long running bots that require a lot of computing power.

Warning

In order for the factory to be able to build correctly you must specify an ImageId that is either one of the pyqalx AMIs or uses a pyqalx AMI as a base image. See here for a list of available pyqalx images.

Warning

You must specify a security group with an outbound rule of HTTPS to 0.0.0.0/0 otherwise the sector won’t be able to contact the API and the build will fail. You will also have to ensure that the sector is placed into a subnet that has access to the internet.

Example minimum aws sector
ec2_box_1:  # the name of this sector for identification purposes
  type: aws
  parameters:
    # Provider CloudFormation parameters to configure your instance however you wish
    ImageId: ami-0242408af868  # The ImageId to use - must be based on a pyqalx image
    InstanceType: t2.nano # The InstanceType to use
    SecurityGroupIds:
      # Security group ID with HTTPS open as an outbound rule
      - sg-8c422def392d
  bots: #  A list of the bots that should be started
    my_bot_1: # The bot name for identification purposes
       # You can pass any argument here that you can pass to `bot-start`
       processes: 4
    my_bot_2: # The bot name for identification purposes
       # You can pass any argument here that you can pass to `bot-start`
       processes: 4

Note

You can add any value to parameters that are defined in the Properties section on the EC2 cloudformation documentation

Important

You must specify an ImageId and an InstanceType otherwise the AWS sector will not be able to create the EC2 instances. Valid instance type can be found here

Created Resources

The following resources will be created in your AWS account for an aws sector.

  • A Cloudformation stack for each region_name defined on the stage

  • An EC2 instance for each named sector (ec2_box_1, ec2_box_2 etc) on each Cloudformation stack

  • A secret in Secrets Manager for each EC2 instance and each bot on the instance - this will contain the Qalx TOKEN from the config specified on each bot on the sector

  • A SSM Parameter for all other config values for each bot on the sector

  • An IAM role for each EC2 instance with permission to access the specific secrets and parameters for the bots on the EC2 instance

Required IAM Permissions

In order to create an aws sector you will need to have an IAM user with at least following permissions. These minimum permissions will be validated before pyqalx attempts to create anything in your AWS account. You may require additional permissions if you provide additional parameters in factories-plan.yaml

Minimum permissions to create AWS sector
  {
 "Version": "2012-10-17",
 "Statement": [
     {
         "Effect": "Allow",
         "Action": "iam:SimulatePrincipalPolicy",
         "Resource": "<your user ARN>"
     },
     {
         "Effect": "Allow",
         "Action": [
             "iam:AddRoleToInstanceProfile",
             "iam:CreateInstanceProfile",
             "iam:CreateRole",
             "iam:DeleteInstanceProfile",
             "iam:DeleteRole",
             "iam:DeleteRolePolicy",
             "iam:GetInstanceProfile",
             "iam:GetRole",
             "iam:GetRolePolicy",
             "iam:PassRole",
             "iam:PutRolePolicy",
             "iam:RemoveRoleFromInstanceProfile",
             "iam:TagRole",
         ],
         "Resource": [
             "arn:aws:iam::*:instance-profile/*",
             "arn:aws:iam::*:role/*"
         ]
     },
     {
         "Effect": "Allow",
         "Action": [
             "cloudformation:CreateStack",
             "cloudformation:DeleteStack",
             "cloudformation:DescribeStacks",
             "cloudformation:DescribeStackEvents",
             "cloudformation:UpdateTerminationProtection",
             "cloudwatch:PutMetricData"
             "ec2:CreateTags",
             "ec2:DescribeImages",
             "ec2:DescribeInstances",
             "ec2:TerminateInstances",
             "secretsmanager:CreateSecret",
             "secretsmanager:DeleteSecret",
             "secretsmanager:TagResource",
             "ssm:AddTagsToResource",
             "ssm:DeleteParameter",
             "ssm:PutParameter",
         ],
         "Resource": "*"
     },
     {
         "Effect": "Allow",
         "Action": [
            "ec2:RunInstances",
         ],
         "Resource": [
            "arn:aws:ec2:*:*:security-group/*",
            "arn:aws:ec2:*::image/*",
            "arn:aws:ec2:*:*:instance/*",
         ]
     },
     {
         "Effect": "Allow":,
         "Action": [
            "cloudwatch:EnableAlarmActions",
            "cloudwatch:DeleteAlarms",
            "cloudwatch:DisableAlarmActions",
            "cloudwatch:PutMetricAlarm"
         ],
         "Resource": [
            "arn:aws:cloudwatch:*:*:alarm:*"
         ]
     }
 ]
}

Extending images

While the provided base images will be useful in most circumstances, there may be times when you need to install additional software on them to enable your bots to run. You can easily do this by taking the base image, installing your custom software, creating a new image and then using the new image id in your plan. If you want to bake python dependencies into the image you will need to install them into the correct virtual environment that pyqalx uses to bootstrap the image when building the factory.

Note

The bootstrap script will automatically install your bot based on the installing rules when bootstrapping the sectors.

  • Linux
    • user: ec2-user

    • virtual environment path: /home/ec2-user/pyqalx

  • Windows
    • user: Administrator

    • virtual environment path: C:UsersAdministrator.venvspyqalxScriptsactivate.bat

Demolish

qalx factory-demolish --name=<factory_name>

Demolishing is the process by which your factory gets completely deleted.

Why would I want to run this command?

You may want to run this command if you are finished using your factory and want to remove the resources it is using in order to reduce costs or if you want to make updates to your factory.

The demolish command will initially list all factories matching the factory name as there could be multiple factories with the same name on different stages. Once you have selected the factory that you wish to delete the following will happen:

  • The factory status will be updated to DELETING

  • The bots will be warm terminated according to the specific workflow defined on the stage.
    • Local bot processes will shut down asynchronously as per normal termination.

  • Any remote stacks will be deleted (these will take time to delete and will complete asynchronously)

  • The factory entity will be deleted from the database - bot entities will remain for data integrity purposes

  • The local build paths will be deleted if possible
    • Deleting the local build paths may not be possible if the demolish command was issue from a different physical machine to what the factory was built on

Debug

qalx provides various optional debug settings for developing factories. Modifying these settings will help you debug more complex issues with your build

demolish_on_failure

Sometimes a factory build will fail. This could be for a number of reasons, perhaps the bot is misconfigured, or there is a problem with a remote stack. qalx makes every effort to inform you of what went wrong but sometimes you may need to investigate in more detail.

default value

By default, if an error occurs when building a factory, qalx will delete all resources that were created during the build process

example usage

Setting the demolish_on_failure flag to false will stop qalx from deleting the factory entity from the database or deleting any stacks. Any bots that successfully started will continue to run.

Example factory_plan.yml file with demolish_on_failure set to false
factory:
  debug:
    demolish_on_failure: false
  ...the rest of the factory plan here...

Warning

It is up to you to demolish any resources once you are done debugging. The easiest way to do this is with the demolish command, but depending on the reason for the build failure you may need to delete resources manually.