From 451a3b6766a1e226deb4550b9c01b2297e5705fe Mon Sep 17 00:00:00 2001 From: Steve Jahl <636687+sjahl@users.noreply.github.com> Date: Fri, 20 Sep 2024 10:28:06 -0400 Subject: [PATCH] Export Reads deployment (#1613) --- .cloudbuild/reads.cloudbuild.yaml | 40 +++++ deploy/deployctl/__main__.py | 4 - .../subcommands/reads_deployments.py | 164 ------------------ deploy/deployctl/subcommands/reads_images.py | 71 -------- deploy/docs/Deployment.md | 18 +- .../manifests/reads/base/kustomization.yaml | 4 - .../reads/base/reads.deployment.yaml | 83 --------- reads/docker-build.sh | 33 ++++ 8 files changed, 80 insertions(+), 337 deletions(-) create mode 100644 .cloudbuild/reads.cloudbuild.yaml delete mode 100644 deploy/deployctl/subcommands/reads_deployments.py delete mode 100644 deploy/deployctl/subcommands/reads_images.py delete mode 100644 deploy/manifests/reads/base/kustomization.yaml delete mode 100644 deploy/manifests/reads/base/reads.deployment.yaml create mode 100755 reads/docker-build.sh diff --git a/.cloudbuild/reads.cloudbuild.yaml b/.cloudbuild/reads.cloudbuild.yaml new file mode 100644 index 000000000..fa6040a4a --- /dev/null +++ b/.cloudbuild/reads.cloudbuild.yaml @@ -0,0 +1,40 @@ +# Submitting a cloud build using this config from your local machine can be done as: +# gcloud builds submit --config .cloudbuild/reads.cloudbuild.yaml \ +# --substitutions=_BRANCH_FOR_IMAGE_NAME="my-branch-name",SHORT_SHA="6f3f419" . + +steps: + - name: 'gcr.io/cloud-builders/docker' + args: + [ + 'build', + '-t', + 'us-docker.pkg.dev/${PROJECT_ID}/gnomad/gnomad-reads-server:$SHORT_SHA', + '-t', + 'us-docker.pkg.dev/${PROJECT_ID}/gnomad/gnomad-reads-server:${_BUILD_TAG}', + '-f', + 'deploy/dockerfiles/reads/reads-server.dockerfile', + '.', + ] + - name: 'gcr.io/cloud-builders/docker' + args: + [ + 'build', + '-t', + 'us-docker.pkg.dev/${PROJECT_ID}/gnomad/gnomad-reads-api:$SHORT_SHA', + '-t', + 'us-docker.pkg.dev/${PROJECT_ID}/gnomad/gnomad-reads-api:${_BUILD_TAG}', + '-f', + 'deploy/dockerfiles/reads/reads-api.dockerfile', + '.', + ] +options: + dynamicSubstitutions: true +substitutions: + _BUILD_TAG: '${_BRANCH_FOR_IMAGE_NAME}-${BUILD_ID}' + +# push tag with the short sha, and also a branch-based UUID +images: + - 'us-docker.pkg.dev/${PROJECT_ID}/gnomad/gnomad-reads-server:$SHORT_SHA' + - 'us-docker.pkg.dev/${PROJECT_ID}/gnomad/gnomad-reads-server:${_BUILD_TAG}' + - 'us-docker.pkg.dev/${PROJECT_ID}/gnomad/gnomad-reads-api:$SHORT_SHA' + - 'us-docker.pkg.dev/${PROJECT_ID}/gnomad/gnomad-reads-api:${_BUILD_TAG}' diff --git a/deploy/deployctl/__main__.py b/deploy/deployctl/__main__.py index f48db9730..2b1742467 100755 --- a/deploy/deployctl/__main__.py +++ b/deploy/deployctl/__main__.py @@ -11,8 +11,6 @@ from deployctl.subcommands import elasticsearch from deployctl.subcommands import ingress_demo from deployctl.subcommands import ingress_production -from deployctl.subcommands import reads_deployments -from deployctl.subcommands import reads_images def main(): @@ -22,8 +20,6 @@ def main(): "config": config, "deployments": browser_deployments, "images": browser_images, - "reads-deployments": reads_deployments, - "reads-images": reads_images, "production": ingress_production, "demo": ingress_demo, "data-pipeline": data_pipeline, diff --git a/deploy/deployctl/subcommands/reads_deployments.py b/deploy/deployctl/subcommands/reads_deployments.py deleted file mode 100644 index 36eda3527..000000000 --- a/deploy/deployctl/subcommands/reads_deployments.py +++ /dev/null @@ -1,164 +0,0 @@ -import argparse -import datetime -import glob -import os -import string -import sys -import typing - -from deployctl.config import config -from deployctl.shell import kubectl, get_most_recent_tag, image_exists, get_k8s_deployments - - -KUSTOMIZATION_TEMPLATE = """--- -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ../../base -commonLabels: - deployment: '{deployment_name}' -nameSuffix: '-{deployment_name}' -images: - - name: gnomad-reads-server - newName: {reads_server_image_repository} - newTag: '{reads_server_tag}' - - name: gnomad-reads-api - newName: {reads_api_image_repository} - newTag: '{reads_api_tag}' -""" - - -def deployments_directory() -> str: - path = os.path.realpath(os.path.join(os.path.dirname(__file__), "../../manifests/reads/deployments")) - if not os.path.exists(path): - os.makedirs(path) - return path - - -def list_deployments() -> None: - print("Local configurations") - print("====================") - paths = reversed(sorted(glob.iglob(f"{deployments_directory()}/*/kustomization.yaml"), key=os.path.getmtime)) - for path in paths: - print(os.path.basename(os.path.dirname(path))) - - print() - - print("Cluster deployments") - print("===================") - for deployment in get_k8s_deployments("component=gnomad-reads"): - print(deployment[len("gnomad-reads-") :]) - - -def create_deployment(name: str, reads_server_tag: str = None, reads_api_tag: str = None) -> None: - if not name: - name = datetime.datetime.now().strftime("%Y%m%d-%H%M") - else: - allowed_characters = set(string.ascii_lowercase) | set(string.digits) | {"-"} - if set(name).difference(allowed_characters): - raise ValueError(f"invalid deployment name '{name}'") - - if name == "latest": - raise ValueError("'latest' cannot be used for a deployment name") - - deployment_directory = os.path.join(deployments_directory(), name) - - if os.path.exists(deployment_directory): - raise RuntimeError(f"deployment '{name}' already exists") - - if reads_server_tag: - if not image_exists(config.reads_server_image_repository, reads_server_tag): - raise RuntimeError(f"could not find image {config.reads_server_image_repository}:{reads_server_tag}") - else: - reads_server_tag = get_most_recent_tag(config.reads_server_image_repository) - print(f"No server tag provided, using most recent ({reads_server_tag})") - - if reads_api_tag: - if not image_exists(config.reads_api_image_repository, reads_api_tag): - raise RuntimeError(f"could not find image {config.reads_api_image_repository}:{reads_api_tag}") - else: - reads_api_tag = get_most_recent_tag(config.reads_api_image_repository) - print(f"No API tag provided, using most recent ({reads_api_tag})") - - os.makedirs(deployment_directory) - - with open(os.path.join(deployment_directory, "kustomization.yaml"), "w") as kustomization_file: - kustomization = KUSTOMIZATION_TEMPLATE.format( - deployment_name=name, - reads_server_image_repository=config.reads_server_image_repository, - reads_server_tag=reads_server_tag, - reads_api_image_repository=config.reads_api_image_repository, - reads_api_tag=reads_api_tag, - ) - - kustomization_file.write(kustomization) - - print(f"configured deployment '{name}'") - - -def apply_deployment(name: str) -> None: - deployment_directory = os.path.join(deployments_directory(), name) - - if not os.path.exists(deployment_directory): - raise RuntimeError(f"no configuration for deployment '{name}'") - - kubectl(["apply", "-k", deployment_directory]) - - -def delete_deployment(name: str, clean: bool = False) -> None: - deployment_directory = os.path.join(deployments_directory(), name) - - if os.path.exists(deployment_directory): - kubectl(["delete", "-k", deployment_directory]) - if clean: - clean_deployment(name) - else: - create_deployment(name) - delete_deployment(name, clean=True) - - -def clean_deployment(name: str) -> None: - deployment_directory = os.path.join(deployments_directory(), name) - os.remove(os.path.join(deployment_directory, "kustomization.yaml")) - os.rmdir(deployment_directory) - - -def main(argv: typing.List[str]) -> None: - parser = argparse.ArgumentParser(prog="deployctl") - subparsers = parser.add_subparsers() - - list_parser = subparsers.add_parser("list") - list_parser.set_defaults(action=list_deployments) - - create_parser = subparsers.add_parser("create") - create_parser.set_defaults(action=create_deployment) - create_parser.add_argument("--name") - create_parser.add_argument("--server-tag", dest="reads_server_tag") - create_parser.add_argument("--api-tag", dest="reads_api_tag") - - apply_parser = subparsers.add_parser("apply") - apply_parser.set_defaults(action=apply_deployment) - apply_parser.add_argument("name") - - delete_parser = subparsers.add_parser("delete") - delete_parser.set_defaults(action=delete_deployment) - delete_parser.add_argument("name") - delete_parser.add_argument("--clean", action="store_true") - - clean_parser = subparsers.add_parser("clean") - clean_parser.set_defaults(action=clean_deployment) - clean_parser.add_argument("name") - - args = parser.parse_args(argv) - - if "action" not in args: - parser.print_usage() - sys.exit(1) - - action = args.action - del args.action - try: - action(**vars(args)) - except Exception as err: # pylint: disable=broad-except - print(f"Error: {err}", file=sys.stderr) - sys.exit(1) diff --git a/deploy/deployctl/subcommands/reads_images.py b/deploy/deployctl/subcommands/reads_images.py deleted file mode 100644 index ebb2f8c2e..000000000 --- a/deploy/deployctl/subcommands/reads_images.py +++ /dev/null @@ -1,71 +0,0 @@ -import argparse -import os -import subprocess -import sys -import typing - -from deployctl.config import config -from deployctl.tag import get_tag_from_git_revision - - -def build_images(tag: str = None, push: bool = False) -> None: - repository_root = os.path.realpath(os.path.join(os.path.dirname(__file__), "../../..")) - - if not tag: - tag = get_tag_from_git_revision() - - images = [ - ("deploy/dockerfiles/reads/reads-server.dockerfile", config.reads_server_image_repository), - ("deploy/dockerfiles/reads/reads-api.dockerfile", config.reads_api_image_repository), - ] - - for dockerfile_path, image_repository in images: - subprocess.check_call( - [ - "docker", - "build", - "--pull", - f"--file={dockerfile_path}", - f"--tag={image_repository}:{tag}", - f"--tag={image_repository}:latest", - ".", - ], - cwd=repository_root, - env=dict(os.environ, DOCKER_BUILDKIT="1"), - ) - - if push: - for dockerfile_path, image_repository in images: - subprocess.check_call(["docker", "push", f"{image_repository}:{tag}"]) - - for _, image_repository in images: - print(f"Tagged {image_repository}:{tag}") - print(f"Tagged {image_repository}:latest") - - if push: - for _, image_repository in images: - print(f"Pushed {image_repository}:{tag}") - - -def main(argv: typing.List[str]) -> None: - parser = argparse.ArgumentParser(prog="deployctl") - subparsers = parser.add_subparsers() - - build_parser = subparsers.add_parser("build") - build_parser.set_defaults(action=build_images) - build_parser.add_argument("--tag") - build_parser.add_argument("--push", action="store_true") - - args = parser.parse_args(argv) - - if "action" not in args: - parser.print_usage() - sys.exit(1) - - action = args.action - del args.action - try: - action(**vars(args)) - except Exception as err: # pylint: disable=broad-except - print(f"Error: {err}", file=sys.stderr) - sys.exit(1) diff --git a/deploy/docs/Deployment.md b/deploy/docs/Deployment.md index 3aaa35f7a..3d5523d78 100644 --- a/deploy/docs/Deployment.md +++ b/deploy/docs/Deployment.md @@ -335,25 +335,21 @@ The production gnomad browser uses a [blue/green deployment](https://martinfowle ## Create and Update Reads Deployment -Create Reads Deployment +### Production -- Build Docker images and push to GCR. +Docker images are built after merging to main. Once the build succeeds, and you have a new tag, update the reads deployment in [gnomad-deployments/reads](https://github.com/broadinstitute/gnomad-deployments/tree/main/reads). See the instructions in the deployment repo to create a new blue/green version, and deploy it to the cluster. - ``` - ./deployctl reads-images build --push - ``` +### Development / Demo -- Create deployment manifests. +- Build Docker images and push to GCR. ``` - ./deployctl reads-deployments create + ./reads/docker-build.sh ``` -- Apply deployment. +- Create deployment manifests. - ``` - ./deployctl reads-deployments apply - ``` + Follow the instructions in the [gnomad-deployments/reads](https://github.com/broadinstitute/gnomad-deployments/tree/main/reads) repo to create and apply a new demo deployment. Update Reads Deployment diff --git a/deploy/manifests/reads/base/kustomization.yaml b/deploy/manifests/reads/base/kustomization.yaml deleted file mode 100644 index 91c508d59..000000000 --- a/deploy/manifests/reads/base/kustomization.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - ./reads.deployment.yaml diff --git a/deploy/manifests/reads/base/reads.deployment.yaml b/deploy/manifests/reads/base/reads.deployment.yaml deleted file mode 100644 index f533e08a6..000000000 --- a/deploy/manifests/reads/base/reads.deployment.yaml +++ /dev/null @@ -1,83 +0,0 @@ ---- -kind: Deployment -apiVersion: apps/v1 -metadata: - name: gnomad-reads - labels: - component: gnomad-reads -spec: - replicas: 1 - selector: - matchLabels: - name: gnomad-reads - template: - metadata: - labels: - name: gnomad-reads - spec: - containers: - - name: web - image: gnomad-reads-server - env: - - name: PROXY_IPS - valueFrom: - configMapKeyRef: - name: proxy-ips - key: ips - imagePullPolicy: Always - ports: - - name: http - containerPort: 80 - resources: - requests: - cpu: '50m' - memory: '4Gi' - limits: - cpu: '100m' - memory: '4Gi' - readinessProbe: - httpGet: - path: /health/ready - port: http - initialDelaySeconds: 3 - periodSeconds: 10 - volumeMounts: - - name: readviz - mountPath: /readviz - - name: app - image: gnomad-reads-api - env: - - name: PORT - value: '8000' - - name: TRUST_PROXY - valueFrom: - configMapKeyRef: - name: proxy-ips - key: ips - ports: - - name: http - containerPort: 8000 - resources: - requests: - cpu: '50m' - memory: '128Mi' - limits: - cpu: '100m' - memory: '256Mi' - readinessProbe: - httpGet: - path: /health/ready - port: http - initialDelaySeconds: 3 - periodSeconds: 10 - volumeMounts: - - name: readviz - mountPath: /readviz - nodeSelector: - cloud.google.com/gke-nodepool: 'main-pool' - volumes: - - name: readviz - gcePersistentDisk: - fsType: ext4 - pdName: readviz-data-v4-2023-10-28 - readOnly: true diff --git a/reads/docker-build.sh b/reads/docker-build.sh new file mode 100755 index 000000000..1984aef21 --- /dev/null +++ b/reads/docker-build.sh @@ -0,0 +1,33 @@ +#!/bin/sh -euo pipefail + +# This is kind of strange, but we do this to ensure the script runs with the right +# working directory without you having to worry about where you run the script from. +SCRIPT_DIR="$(dirname "$0")" +cd "$SCRIPT_DIR/.." + +# Tag image with git revision +COMMIT_HASH=$(git rev-parse --short HEAD) +IMAGE_TAG=${COMMIT_HASH} + +# Add current branch name to tag if not on main branch +BRANCH=$(git symbolic-ref --short -q HEAD) +if [ "$BRANCH" != "main" ]; then + TAG_BRANCH=$(echo "$BRANCH" | sed 's/[^A-Za-z0-9_\-\.]/_/g') + IMAGE_TAG="${IMAGE_TAG}-${TAG_BRANCH}" +fi + +# Add "-modified" to tag if there are uncommitted local changes +GIT_STATUS=$(git status --porcelain 2> /dev/null | tail -n1) +if [ -n "$GIT_STATUS" ]; then + IMAGE_TAG="${IMAGE_TAG}-modified" +fi + +AR_REPO="us-docker.pkg.dev/exac-gnomad/gnomad/gnomad-reads" +docker build -f deploy/dockerfiles/reads/reads-server.dockerfile . \ + --tag "${AR_REPO}-server:dev-${IMAGE_TAG}" + +docker build -f deploy/dockerfiles/reads/reads-api.dockerfile . \ + --tag "${AR_REPO}-api:dev-${IMAGE_TAG}" + +echo "${AR_REPO}-server:dev-${IMAGE_TAG}" +echo "${AR_REPO}-api:dev-${IMAGE_TAG}"