Deployment

Pipeline overview

AWS CodePipeline (trigger)
  └─► AWS CodeBuild  (buildspec.yml)
        install    : Python 3.9, remove .python-version
        pre_build  : settings_setup.py → pip install → config_setup.py
                     → openssl (prod only) → pytest → ECR login
        build      : docker-compose build → tag images → write deploy artifacts
        post_build : docker push both images to ECR
  └─► AWS CodeDeploy (ECS)
        register the new task definition and roll out via appspec.yaml

Environments and accounts

Account selection happens in buildspec.yml from ENV_TYPE:

if [ "$ENV_TYPE" = "dev" ]; then
  export AWS_ACCOUNT=605668041275
  export ROOT_PATH=/automatic-uw-tool-web-api
else
  export AWS_ACCOUNT=625663093496
  export ROOT_PATH=/automatic-uw-tool-web-api
fi
export SWF_IMAGE_URI=${AWS_ACCOUNT}.dkr.ecr.us-east-1.amazonaws.com/automatic-uw-tool-swf-worker
export API_IMAGE_URI=${AWS_ACCOUNT}.dkr.ecr.us-east-1.amazonaws.com/automatic-uw-tool-web-api
export APP_NAMES=${APP_NAMES:-"automatic-uw-tool-swf-worker automatic-uw-tool-web-api"}

APP_NAMES defaults to both apps. Each token must contain swf or api (that substring is what the build phase matches on) and doubles as the imagedefinitions artifact filename.

Environment-to-resource mapping: Configuration & secrets.

CodeBuild phases

Runtime: Python 3.9. GIT_TOKEN comes from the Secrets Manager entry aws-codebuild-token-eva and is needed to install the private pyb-sdk.

install

Removes .python-version. uv commits a bare 3.9 pin, but pyenv (which backs CodeBuild’s runtime-versions) needs an exact patch version and lets that file win — newer CodeBuild images otherwise fail with pyenv: version 3.9' is not installed. Local `uv workflows recreate the file.

pre_build

  1. python settings_setup.py → config/settings.json.

  2. pip install -r requirements.txt with PIP_CONSTRAINT pinning setuptools<81 + wheel, so pyb-sdk’s `pkg_resources import keeps working during build isolation.

  3. export ENV_FOR_DYNACONF=$ENV_NAME.

  4. python config_setup.py $CODEBUILD_INITIATOR → config/.secrets.json and the TR material.

  5. Production only — decode the base64 PFX and extract the PEMs:

    PFX_PASSWORD=$(cat config/thomson_reuters/pfx_password.txt)
    cat config/thomson_reuters/file.pfx.base64 | base64 --decode > config/thomson_reuters/file.pfx
    openssl pkcs12 -in config/thomson_reuters/file.pfx -nocerts -out config/thomson_reuters/key_pass.pem -passin pass:$PFX_PASSWORD -passout pass:$PFX_PASSWORD
    openssl pkcs12 -in config/thomson_reuters/file.pfx -clcerts -nokeys -out config/thomson_reuters/cert.pem -passin pass:$PFX_PASSWORD
    openssl rsa -in config/thomson_reuters/key_pass.pem -out config/thomson_reuters/key.pem -passin pass:$PFX_PASSWORD

    In dev, config_setup.py has already written cert.pem and key.pem directly.

  6. python -m pytest — unit tests and coverage. The build fails if coverage drops below 80% (pyproject.toml).

  7. aws ecr get-login-password | docker login for the target account.

build

docker-compose -f deployment/docker-compose.yml build, then per app in APP_NAMES:

App Tag and artifact

swf

Tag $SWF_IMAGE_URI:$ENV_NAME; write artifacts/<app>.json = [{"name":"app","imageUri":"…:<ENV_NAME>"}].

api, non-release

Tag $API_IMAGE_URI:$ENV_NAME; same imagedefinitions format.

api, release

Tag $API_IMAGE_URI:release; write artifacts/imageDetail.json = {"ImageURI":"…:release"} for the ECS blue/green CodeDeploy.

The whole block is skipped when ENV_NAME is unset.

post_build

docker push both images to ECR under the $ENV_NAME tag.

Reports and artifacts

  • Reports: report.xml (JUnit), coverage.xml (Cobertura).

  • Artifacts: everything in ./artifacts/* with discard-paths: yes.

Docker images

Both images start from public.ecr.aws/docker/library/python:3.9-slim, install git (for the git-based private dependencies), copy the repository, and pip install -r requirements.txt.

Image Dockerfile Workdir CMD

Web API

deployment/web_api/Dockerfile

/automatic-uw-tool-web-api

uvicorn web_api_app:app --reload --host 0.0.0.0 --port 80 --log-level debug

SWF worker

deployment/swf_worker/Dockerfile

/automatic-uw-tool-swf-worker

python -u swf_app.py

Build args wired by deployment/docker-compose.yml: ENV_FOR_DYNACONF (from ENV_NAME), AWS_ACCOUNT, GIT_TOKEN, and ROOT_PATH (Web API only).

The Web API image runs uvicorn with --reload and --log-level debug, which are development flags: the reloader spawns a watcher process and debug logging is verbose. Worth revisiting when hardening production.

Note also that the generated config/settings.json and config/.secrets.json are produced in pre_build and therefore baked into the image by COPY . .. The image is environment-specific, which is why it is tagged with $ENV_NAME.

ECS artifacts

artifacts/taskdef.json — Fargate task definition (Web API)

Family automatic-uw-tool, one container named api on port 80, 512 CPU / 2048 MB, awslogs to /ecs/automatic-uw-tool in us-east-1. <IMAGE1_NAME> and <TASK_DEFINITION> are substituted at deploy time.

"family": "automatic-uw-tool",
"taskRoleArn": "arn:aws:iam::625663093496:role/automatic-uw-tool-role",
"executionRoleArn": "arn:aws:iam::625663093496:role/ecsTaskExecutionRole",
"networkMode": "awsvpc",
"containerDefinitions": [
    {
        "name": "api",
        "image": "<IMAGE1_NAME>",

artifacts/appspec.yaml — CodeDeploy ECS spec

Blue/green deployment of the ECS service, routing the ALB to container api port 80.

{
  "version": 1,
  "Resources": [
    {
      "TargetService": {
        "Type": "AWS::ECS::Service",
        "Properties": {
          "TaskDefinition": "<TASK_DEFINITION>",
          "LoadBalancerInfo": { "ContainerName": "api", "ContainerPort": 80 }
        }
      }
    }
  ]
}

taskdef.json and appspec.yaml describe only the Web API service. The SWF worker is deployed as its own ECS service from its generated imagedefinitions artifact — it needs no load balancer, because it long-polls SWF.

Building locally

export ENV_NAME=<env> AWS_ACCOUNT=<acct> GIT_TOKEN=<pat> \
       ROOT_PATH=/automatic-uw-tool-web-api NAME_TAG=automatic-uw-tool
export ENV_FOR_DYNACONF=$ENV_NAME
python settings_setup.py
python config_setup.py            # needs AWS credentials
docker-compose -f deployment/docker-compose.yml build

Or make docker-build — see Local development.

Runtime dependencies

requirements.txt (Python 3.9): fastapi==0.110.0, uvicorn, boto3==1.33.2, dynaconf==3.1.8, requests==2.31.0, xmltodict, PyJWT, sentry-sdk, thefuzz (name matching), arrow, coolname (worker thread names), werkzeug (HTTP status phrases), hubspot-api-client, plus the private pyb-sdk and eventlogger installed from GitHub with GIT_TOKEN.

Rolling back

Images are tagged by environment (:release, :<env>), not by commit, so "roll back" means redeploying a previous task definition revision in ECS/CodeDeploy rather than re-pulling an older tag. Keep that in mind before triggering a rebuild on a bad commit — the rebuild overwrites the tag.