From 92c1a4c28834085c0ca6b8c9e8b09ea16ddd236d Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Tue, 13 Feb 2024 14:32:13 +0000 Subject: [PATCH 01/20] add mlflow image module Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-image/README.md | 39 +++ modules/mlflow/mlflow-image/app.py | 54 ++++ modules/mlflow/mlflow-image/coverage.ini | 3 + modules/mlflow/mlflow-image/deployspec.yaml | 35 +++ modules/mlflow/mlflow-image/pyproject.toml | 35 +++ .../mlflow/mlflow-image/requirements-dev.in | 14 + .../mlflow/mlflow-image/requirements-dev.txt | 273 ++++++++++++++++++ modules/mlflow/mlflow-image/requirements.in | 4 + modules/mlflow/mlflow-image/requirements.txt | 91 ++++++ modules/mlflow/mlflow-image/setup.cfg | 28 ++ modules/mlflow/mlflow-image/src/Dockerfile | 15 + modules/mlflow/mlflow-image/stack.py | 65 +++++ 12 files changed, 656 insertions(+) create mode 100644 modules/mlflow/mlflow-image/README.md create mode 100644 modules/mlflow/mlflow-image/app.py create mode 100644 modules/mlflow/mlflow-image/coverage.ini create mode 100644 modules/mlflow/mlflow-image/deployspec.yaml create mode 100644 modules/mlflow/mlflow-image/pyproject.toml create mode 100644 modules/mlflow/mlflow-image/requirements-dev.in create mode 100644 modules/mlflow/mlflow-image/requirements-dev.txt create mode 100644 modules/mlflow/mlflow-image/requirements.in create mode 100644 modules/mlflow/mlflow-image/requirements.txt create mode 100644 modules/mlflow/mlflow-image/setup.cfg create mode 100644 modules/mlflow/mlflow-image/src/Dockerfile create mode 100644 modules/mlflow/mlflow-image/stack.py diff --git a/modules/mlflow/mlflow-image/README.md b/modules/mlflow/mlflow-image/README.md new file mode 100644 index 00000000..494d2c52 --- /dev/null +++ b/modules/mlflow/mlflow-image/README.md @@ -0,0 +1,39 @@ +# SageMaker Model Endpoint + +## Description + +This module creates and mlflow container image and pushes to the specified ECR. + +## Inputs/Outputs + +### Input Parameters + +#### Required + +- `ecr-repository-name`: The name of the ECR repository to push the image to. + +### Sample manifest declaration + +```yaml +name: mlflow-image +path: modules/mlflow/mlflow-image +parameters: + - name: ecr-repository-name + valueFrom: + moduleMetadata: + group: storage + name: ecr-mlflow + key: EcrRepositoryName +``` + +### Module Metadata Outputs + +- `MlflowImageUri`: Mlflow image URI + +#### Output Example + +```json +{ + "MlflowImageUri": "" +} +``` diff --git a/modules/mlflow/mlflow-image/app.py b/modules/mlflow/mlflow-image/app.py new file mode 100644 index 00000000..6d8cddfb --- /dev/null +++ b/modules/mlflow/mlflow-image/app.py @@ -0,0 +1,54 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os + +import aws_cdk + +from stack import MlflowImagePublishingStack + + +def _param(name: str) -> str: + return f"SEEDFARMER_PARAMETER_{name}" + + +project_name = os.getenv("SEEDFARMER_PROJECT_NAME", "") +deployment_name = os.getenv("SEEDFARMER_DEPLOYMENT_NAME", "") +module_name = os.getenv("SEEDFARMER_MODULE_NAME", "") +app_prefix = f"{project_name}-{deployment_name}-{module_name}" + +environment = aws_cdk.Environment( + account=os.environ["CDK_DEFAULT_ACCOUNT"], + region=os.environ["CDK_DEFAULT_REGION"], +) + +ecr_repo_name = os.getenv(_param("ECR_REPOSITORY_NAME")) + +if not ecr_repo_name: + raise ValueError("Missing input parameter ecr-repository-name") + + +app = aws_cdk.App() +stack = MlflowImagePublishingStack( + scope=app, + id=app_prefix, + app_prefix=app_prefix, + ecr_repo_name=ecr_repo_name, + env=aws_cdk.Environment( + account=os.environ["CDK_DEFAULT_ACCOUNT"], + region=os.environ["CDK_DEFAULT_REGION"], + ), +) + + +aws_cdk.CfnOutput( + scope=stack, + id="metadata", + value=stack.to_json_string( + { + "MlflowImageUri": stack.image_uri, + } + ), +) + +app.synth() diff --git a/modules/mlflow/mlflow-image/coverage.ini b/modules/mlflow/mlflow-image/coverage.ini new file mode 100644 index 00000000..c3878739 --- /dev/null +++ b/modules/mlflow/mlflow-image/coverage.ini @@ -0,0 +1,3 @@ +[run] +omit = + tests/* \ No newline at end of file diff --git a/modules/mlflow/mlflow-image/deployspec.yaml b/modules/mlflow/mlflow-image/deployspec.yaml new file mode 100644 index 00000000..dc6cd86f --- /dev/null +++ b/modules/mlflow/mlflow-image/deployspec.yaml @@ -0,0 +1,35 @@ +publishGenericEnvVariables: true +deploy: + phases: + install: + commands: + - npm install -g aws-cdk@2.126.0 + - pip install -r requirements.txt + pre_build: + commands: + - echo "Prebuild stage" + build: + commands: + - cdk deploy --require-approval never --progress events --app "python app.py" --outputs-file ./cdk-exports.json + # Export metadata + - seedfarmer metadata convert -f cdk-exports.json || true + post_build: + commands: + - echo "Build successful" +destroy: + phases: + install: + commands: + - npm install -g aws-cdk@2.126.0 + - pip install -r requirements.txt + pre_build: + commands: + - echo "Prebuild stage" + build: + commands: + - cdk destroy --force --app "python app.py" + post_build: + commands: + - echo "Destroy successful" + +build_type: BUILD_GENERAL1_SMALL \ No newline at end of file diff --git a/modules/mlflow/mlflow-image/pyproject.toml b/modules/mlflow/mlflow-image/pyproject.toml new file mode 100644 index 00000000..1f856efe --- /dev/null +++ b/modules/mlflow/mlflow-image/pyproject.toml @@ -0,0 +1,35 @@ +[tool.black] +line-length = 120 +target-version = ["py36", "py37", "py38"] +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | \.env + | _build + | buck-out + | build + | dist + | codeseeder.out +)/ +''' + +[tool.isort] +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +line_length = 120 +py_version = 36 +skip_gitignore = false + +[tool.pytest.ini_options] +addopts = "-v --cov=. --cov-report term --cov-config=coverage.ini --cov-fail-under=80" +pythonpath = [ + "." +] \ No newline at end of file diff --git a/modules/mlflow/mlflow-image/requirements-dev.in b/modules/mlflow/mlflow-image/requirements-dev.in new file mode 100644 index 00000000..6412f987 --- /dev/null +++ b/modules/mlflow/mlflow-image/requirements-dev.in @@ -0,0 +1,14 @@ +awscli +black +cdk-nag +cfn-lint +check-manifest +flake8 +isort +mypy +pip-tools +pydot +pyroma +pytest +types-PyYAML +types-setuptools diff --git a/modules/mlflow/mlflow-image/requirements-dev.txt b/modules/mlflow/mlflow-image/requirements-dev.txt new file mode 100644 index 00000000..59d38e3f --- /dev/null +++ b/modules/mlflow/mlflow-image/requirements-dev.txt @@ -0,0 +1,273 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile requirements-dev.in +# +aiohttp==3.9.2 + # via black +aiosignal==1.3.1 + # via aiohttp +annotated-types==0.6.0 + # via pydantic +async-timeout==4.0.3 + # via aiohttp +attrs==23.1.0 + # via + # aiohttp + # cattrs + # jschema-to-python + # jsii + # jsonschema + # referencing + # sarif-om +aws-cdk-asset-awscli-v1==2.2.201 + # via aws-cdk-lib +aws-cdk-asset-kubectl-v20==2.1.2 + # via aws-cdk-lib +aws-cdk-asset-node-proxy-agent-v6==2.0.1 + # via aws-cdk-lib +aws-cdk-lib==2.115.0 + # via cdk-nag +aws-sam-translator==1.82.0 + # via cfn-lint +awscli==1.32.0 + # via -r requirements-dev.in +black==23.12.0 + # via -r requirements-dev.in +boto3==1.34.0 + # via aws-sam-translator +botocore==1.34.0 + # via + # awscli + # boto3 + # s3transfer +build==1.0.3 + # via + # check-manifest + # pip-tools + # pyroma +cattrs==23.2.3 + # via jsii +cdk-nag==2.27.216 + # via -r requirements-dev.in +certifi==2023.11.17 + # via requests +cfn-lint==0.83.5 + # via -r requirements-dev.in +charset-normalizer==3.3.2 + # via requests +check-manifest==0.49 + # via -r requirements-dev.in +click==8.1.7 + # via + # black + # pip-tools +colorama==0.4.4 + # via awscli +constructs==10.3.0 + # via + # aws-cdk-lib + # cdk-nag +docutils==0.16 + # via + # awscli + # pyroma +exceptiongroup==1.2.0 + # via + # cattrs + # pytest +flake8==6.1.0 + # via -r requirements-dev.in +frozenlist==1.4.0 + # via + # aiohttp + # aiosignal +idna==3.6 + # via + # requests + # yarl +importlib-metadata==7.0.0 + # via build +importlib-resources==6.1.1 + # via jsii +iniconfig==2.0.0 + # via pytest +isort==5.13.2 + # via -r requirements-dev.in +jmespath==1.0.1 + # via + # boto3 + # botocore +jschema-to-python==1.2.3 + # via cfn-lint +jsii==1.93.0 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-nag + # constructs +jsonpatch==1.33 + # via cfn-lint +jsonpickle==3.0.2 + # via jschema-to-python +jsonpointer==2.4 + # via jsonpatch +jsonschema==4.20.0 + # via + # aws-sam-translator + # cfn-lint +jsonschema-specifications==2023.11.2 + # via jsonschema +junit-xml==1.9 + # via cfn-lint +mccabe==0.7.0 + # via flake8 +mpmath==1.3.0 + # via sympy +multidict==6.0.4 + # via + # aiohttp + # yarl +mypy==1.7.1 + # via -r requirements-dev.in +mypy-extensions==1.0.0 + # via + # black + # mypy +networkx==3.2.1 + # via cfn-lint +packaging==23.2 + # via + # black + # build + # pyroma + # pytest +pathspec==0.12.1 + # via black +pbr==6.0.0 + # via + # jschema-to-python + # sarif-om +pip-tools==7.3.0 + # via -r requirements-dev.in +platformdirs==4.1.0 + # via black +pluggy==1.3.0 + # via pytest +publication==0.0.3 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-nag + # constructs + # jsii +pyasn1==0.5.1 + # via rsa +pycodestyle==2.11.1 + # via flake8 +pydantic==2.5.2 + # via aws-sam-translator +pydantic-core==2.14.5 + # via pydantic +pydot==1.4.2 + # via -r requirements-dev.in +pyflakes==3.1.0 + # via flake8 +pygments==2.17.2 + # via pyroma +pyparsing==3.1.1 + # via pydot +pyproject-hooks==1.0.0 + # via build +pyroma==4.2 + # via -r requirements-dev.in +pytest==7.4.3 + # via -r requirements-dev.in +python-dateutil==2.8.2 + # via + # botocore + # jsii +pyyaml==6.0.1 + # via + # awscli + # cfn-lint +referencing==0.32.0 + # via + # jsonschema + # jsonschema-specifications +regex==2023.10.3 + # via cfn-lint +requests==2.31.0 + # via pyroma +rpds-py==0.13.2 + # via + # jsonschema + # referencing +rsa==4.7.2 + # via awscli +s3transfer==0.9.0 + # via + # awscli + # boto3 +sarif-om==1.0.4 + # via cfn-lint +six==1.16.0 + # via + # junit-xml + # python-dateutil +sympy==1.12 + # via cfn-lint +tomli==2.0.1 + # via + # black + # build + # check-manifest + # mypy + # pip-tools + # pyproject-hooks + # pytest +trove-classifiers==2023.11.29 + # via pyroma +typeguard==2.13.3 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-nag + # constructs + # jsii +types-pyyaml==6.0.12.12 + # via -r requirements-dev.in +types-setuptools==69.0.0.0 + # via -r requirements-dev.in +typing-extensions==4.9.0 + # via + # aws-sam-translator + # black + # cattrs + # jsii + # mypy + # pydantic + # pydantic-core +urllib3==1.26.18 + # via + # botocore + # requests +wheel==0.42.0 + # via pip-tools +yarl==1.9.4 + # via aiohttp +zipp==3.17.0 + # via + # importlib-metadata + # importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/modules/mlflow/mlflow-image/requirements.in b/modules/mlflow/mlflow-image/requirements.in new file mode 100644 index 00000000..369a6704 --- /dev/null +++ b/modules/mlflow/mlflow-image/requirements.in @@ -0,0 +1,4 @@ +aws-cdk-lib==2.126.0 +cdk-nag==2.28.27 +boto3==1.34.35 +cdk_ecr_deployment==2.5.30 \ No newline at end of file diff --git a/modules/mlflow/mlflow-image/requirements.txt b/modules/mlflow/mlflow-image/requirements.txt new file mode 100644 index 00000000..fa7b5c65 --- /dev/null +++ b/modules/mlflow/mlflow-image/requirements.txt @@ -0,0 +1,91 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --output-file=requirements.txt requirements.in +# +attrs==23.2.0 + # via + # cattrs + # jsii +aws-cdk-asset-awscli-v1==2.2.202 + # via aws-cdk-lib +aws-cdk-asset-kubectl-v20==2.1.2 + # via aws-cdk-lib +aws-cdk-asset-node-proxy-agent-v6==2.0.1 + # via aws-cdk-lib +aws-cdk-lib==2.126.0 + # via + # -r requirements.in + # cdk-ecr-deployment + # cdk-nag +boto3==1.34.35 + # via -r requirements.in +botocore==1.34.40 + # via + # boto3 + # s3transfer +cattrs==23.2.3 + # via jsii +cdk-ecr-deployment==2.5.30 + # via -r requirements.in +cdk-nag==2.28.27 + # via -r requirements.in +constructs==10.3.0 + # via + # aws-cdk-lib + # cdk-ecr-deployment + # cdk-nag +exceptiongroup==1.2.0 + # via cattrs +importlib-resources==6.1.1 + # via jsii +jmespath==1.0.1 + # via + # boto3 + # botocore +jsii==1.94.0 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-ecr-deployment + # cdk-nag + # constructs +publication==0.0.3 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-ecr-deployment + # cdk-nag + # constructs + # jsii +python-dateutil==2.8.2 + # via + # botocore + # jsii +s3transfer==0.10.0 + # via boto3 +six==1.16.0 + # via python-dateutil +typeguard==2.13.3 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-ecr-deployment + # cdk-nag + # constructs + # jsii +typing-extensions==4.9.0 + # via + # cattrs + # jsii +urllib3==1.26.18 + # via botocore +zipp==3.17.0 + # via importlib-resources diff --git a/modules/mlflow/mlflow-image/setup.cfg b/modules/mlflow/mlflow-image/setup.cfg new file mode 100644 index 00000000..6136e2bb --- /dev/null +++ b/modules/mlflow/mlflow-image/setup.cfg @@ -0,0 +1,28 @@ +[metadata] +license_files = + LICENSE + NOTICE + VERSION + +[flake8] +max-line-length = 120 +extend-ignore = E203, W503 +exclude = + .git, + __pycache__, + docs/source/conf.py, + old, + build, + dist, + .venv, + codeseeder.out, + bundle + +[mypy] +python_version = 3.7 +strict = True +ignore_missing_imports = True +allow_untyped_decorators = True +exclude = + codeseeder.out/|example/|tests/ +warn_unused_ignores = False \ No newline at end of file diff --git a/modules/mlflow/mlflow-image/src/Dockerfile b/modules/mlflow/mlflow-image/src/Dockerfile new file mode 100644 index 00000000..423ff904 --- /dev/null +++ b/modules/mlflow/mlflow-image/src/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.10.12 + +RUN pip install \ + mlflow==2.10.2 \ + pymysql==1.0.2 \ + boto3 && \ + mkdir /mlflow/ + +EXPOSE 5000 + +CMD mlflow server \ + --host 0.0.0.0 \ + --port 5000 \ + --default-artifact-root ${BUCKET} \ + --backend-store-uri mysql+pymysql://${USERNAME}:${PASSWORD}@${HOST}:${PORT}/${DATABASE} \ No newline at end of file diff --git a/modules/mlflow/mlflow-image/stack.py b/modules/mlflow/mlflow-image/stack.py new file mode 100644 index 00000000..fe7fd689 --- /dev/null +++ b/modules/mlflow/mlflow-image/stack.py @@ -0,0 +1,65 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os +from typing import Any, cast + +from aws_cdk import Stack, Tags +from aws_cdk import aws_ecr as ecr +from aws_cdk.aws_ecr_assets import DockerImageAsset +from cdk_ecr_deployment import DockerImageName, ECRDeployment +from cdk_nag import NagPackSuppression, NagSuppressions +from constructs import Construct, IConstruct + + +class MlflowImagePublishingStack(Stack): # type: ignore + def __init__( + self, + scope: Construct, + id: str, + app_prefix: str, + ecr_repo_name: str, + **kwargs: Any, + ) -> None: + super().__init__(scope, id, **kwargs) + + Tags.of(scope=cast(IConstruct, self)).add(key="Deployment", value=app_prefix[:64]) + + repo = ecr.Repository.from_repository_name( + self, + f"{app_prefix}-repo", + repository_name=ecr_repo_name + ) + + local_image = DockerImageAsset( + self, + f"{app_prefix}-image", + directory=os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"), + ) + + self.image_uri = f"{repo.repository_uri}:mlflow-latest" + ECRDeployment( + self, + f"{app_prefix}-img-deployment", + src=DockerImageName(local_image.image_uri), + dest=DockerImageName(self.image_uri), + ) + + NagSuppressions.add_stack_suppressions( + self, + apply_to_nested_stacks=True, + suppressions=[ + NagPackSuppression( + **{ + "id": "AwsSolutions-IAM4", + "reason": "Managed Policies are for src account roles only", + } + ), + NagPackSuppression( + **{ + "id": "AwsSolutions-IAM5", + "reason": "Resource access restricted to resources", + } + ), + ], + ) From f3373083403c37127b4d2cccba12e2d682c0dc8f Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Tue, 13 Feb 2024 15:31:07 +0000 Subject: [PATCH 02/20] formatting & readme fix Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-image/README.md | 2 +- modules/mlflow/mlflow-image/stack.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/mlflow/mlflow-image/README.md b/modules/mlflow/mlflow-image/README.md index 494d2c52..9f129985 100644 --- a/modules/mlflow/mlflow-image/README.md +++ b/modules/mlflow/mlflow-image/README.md @@ -1,4 +1,4 @@ -# SageMaker Model Endpoint +# Mlflow image module ## Description diff --git a/modules/mlflow/mlflow-image/stack.py b/modules/mlflow/mlflow-image/stack.py index fe7fd689..d129dfed 100644 --- a/modules/mlflow/mlflow-image/stack.py +++ b/modules/mlflow/mlflow-image/stack.py @@ -25,11 +25,7 @@ def __init__( Tags.of(scope=cast(IConstruct, self)).add(key="Deployment", value=app_prefix[:64]) - repo = ecr.Repository.from_repository_name( - self, - f"{app_prefix}-repo", - repository_name=ecr_repo_name - ) + repo = ecr.Repository.from_repository_name(self, f"{app_prefix}-repo", repository_name=ecr_repo_name) local_image = DockerImageAsset( self, From e0d771488fbb23f97584750c7610f1c5617c8644 Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Tue, 13 Feb 2024 16:50:57 +0000 Subject: [PATCH 03/20] update Dockerfile & minor fixes Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-image/README.md | 4 ++-- modules/mlflow/mlflow-image/requirements.in | 3 +-- modules/mlflow/mlflow-image/src/Dockerfile | 4 +--- modules/mlflow/mlflow-image/stack.py | 8 ++++---- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/modules/mlflow/mlflow-image/README.md b/modules/mlflow/mlflow-image/README.md index 9f129985..6adff988 100644 --- a/modules/mlflow/mlflow-image/README.md +++ b/modules/mlflow/mlflow-image/README.md @@ -2,7 +2,7 @@ ## Description -This module creates and mlflow container image and pushes to the specified ECR. +This module creates an mlflow container image and pushes to the specified ECR. ## Inputs/Outputs @@ -34,6 +34,6 @@ parameters: ```json { - "MlflowImageUri": "" + "MlflowImageUri": "xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/ecr-mlflow:latest" } ``` diff --git a/modules/mlflow/mlflow-image/requirements.in b/modules/mlflow/mlflow-image/requirements.in index 369a6704..fcacd9ac 100644 --- a/modules/mlflow/mlflow-image/requirements.in +++ b/modules/mlflow/mlflow-image/requirements.in @@ -1,4 +1,3 @@ aws-cdk-lib==2.126.0 cdk-nag==2.28.27 -boto3==1.34.35 -cdk_ecr_deployment==2.5.30 \ No newline at end of file +boto3==1.34.35 \ No newline at end of file diff --git a/modules/mlflow/mlflow-image/src/Dockerfile b/modules/mlflow/mlflow-image/src/Dockerfile index 423ff904..ce392be4 100644 --- a/modules/mlflow/mlflow-image/src/Dockerfile +++ b/modules/mlflow/mlflow-image/src/Dockerfile @@ -2,7 +2,6 @@ FROM python:3.10.12 RUN pip install \ mlflow==2.10.2 \ - pymysql==1.0.2 \ boto3 && \ mkdir /mlflow/ @@ -11,5 +10,4 @@ EXPOSE 5000 CMD mlflow server \ --host 0.0.0.0 \ --port 5000 \ - --default-artifact-root ${BUCKET} \ - --backend-store-uri mysql+pymysql://${USERNAME}:${PASSWORD}@${HOST}:${PORT}/${DATABASE} \ No newline at end of file + --default-artifact-root ${BUCKET} diff --git a/modules/mlflow/mlflow-image/stack.py b/modules/mlflow/mlflow-image/stack.py index d129dfed..47fb380d 100644 --- a/modules/mlflow/mlflow-image/stack.py +++ b/modules/mlflow/mlflow-image/stack.py @@ -25,18 +25,18 @@ def __init__( Tags.of(scope=cast(IConstruct, self)).add(key="Deployment", value=app_prefix[:64]) - repo = ecr.Repository.from_repository_name(self, f"{app_prefix}-repo", repository_name=ecr_repo_name) + repo = ecr.Repository.from_repository_name(self, "ECR", repository_name=ecr_repo_name) local_image = DockerImageAsset( self, - f"{app_prefix}-image", + "ImageAsset", directory=os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"), ) - self.image_uri = f"{repo.repository_uri}:mlflow-latest" + self.image_uri = f"{repo.repository_uri}:latest" ECRDeployment( self, - f"{app_prefix}-img-deployment", + "ECRDeployment", src=DockerImageName(local_image.image_uri), dest=DockerImageName(self.image_uri), ) From 6b4e2dbb16de36c93f5de16064675eb0d679a4f0 Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Tue, 13 Feb 2024 16:51:36 +0000 Subject: [PATCH 04/20] add mlflow fargate module - checkpoint Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-fargate/README.md | 21 ++++ modules/mlflow/mlflow-fargate/app.py | 72 ++++++++++++ modules/mlflow/mlflow-fargate/deployspec.yaml | 35 ++++++ modules/mlflow/mlflow-fargate/pyproject.toml | 35 ++++++ .../mlflow/mlflow-fargate/requirements-dev.in | 14 +++ modules/mlflow/mlflow-fargate/requirements.in | 4 + .../mlflow/mlflow-fargate/requirements.txt | 83 ++++++++++++++ modules/mlflow/mlflow-fargate/setup.cfg | 28 +++++ modules/mlflow/mlflow-fargate/stack.py | 108 ++++++++++++++++++ 9 files changed, 400 insertions(+) create mode 100644 modules/mlflow/mlflow-fargate/README.md create mode 100644 modules/mlflow/mlflow-fargate/app.py create mode 100644 modules/mlflow/mlflow-fargate/deployspec.yaml create mode 100644 modules/mlflow/mlflow-fargate/pyproject.toml create mode 100644 modules/mlflow/mlflow-fargate/requirements-dev.in create mode 100644 modules/mlflow/mlflow-fargate/requirements.in create mode 100644 modules/mlflow/mlflow-fargate/requirements.txt create mode 100644 modules/mlflow/mlflow-fargate/setup.cfg create mode 100644 modules/mlflow/mlflow-fargate/stack.py diff --git a/modules/mlflow/mlflow-fargate/README.md b/modules/mlflow/mlflow-fargate/README.md new file mode 100644 index 00000000..dff5dd2c --- /dev/null +++ b/modules/mlflow/mlflow-fargate/README.md @@ -0,0 +1,21 @@ +# Mlflow on Fargate module + +## Description + +This module runs Mlflow on Fargate. + +## Inputs/Outputs + +### Input Parameters + +#### Required + +- `vpc-id`: The VPC-ID that the cluster will be created in. +- `subnet-ids`: The subnets that the cluster will be created in. +- `ecr-repository-name`: The name of the ECR repository to pull the image from. +- `artifacts-bucket-name`: Name of the artifacts store bucket + +#### Optional + +- `ecs-cluster-name`: Name of the ECS cluster +- `service-name`: name of the service \ No newline at end of file diff --git a/modules/mlflow/mlflow-fargate/app.py b/modules/mlflow/mlflow-fargate/app.py new file mode 100644 index 00000000..1a27dbb7 --- /dev/null +++ b/modules/mlflow/mlflow-fargate/app.py @@ -0,0 +1,72 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import json +import os + +import aws_cdk + +from stack import MlflowFargateStack + + +def _param(name: str) -> str: + return f"SEEDFARMER_PARAMETER_{name}" + + +project_name = os.getenv("SEEDFARMER_PROJECT_NAME", "") +deployment_name = os.getenv("SEEDFARMER_DEPLOYMENT_NAME", "") +module_name = os.getenv("SEEDFARMER_MODULE_NAME", "") +app_prefix = f"{project_name}-{deployment_name}-{module_name}" + +DEFAULT_ECS_CLUSTER_NAME = "ecs-cluster" +DEFAULT_SERVICE_NAME = "service" + +environment = aws_cdk.Environment( + account=os.environ["CDK_DEFAULT_ACCOUNT"], + region=os.environ["CDK_DEFAULT_REGION"], +) + +# TODO: add ability to pull specific tag +ecr_repo_name = os.getenv(_param("ECR_REPOSITORY_NAME")) +vpc_id = os.getenv(_param("VPC_ID")) +subnet_ids = json.loads(os.getenv(_param("SUBNET_IDS"), "[]")) +cluster_name = os.getenv(_param("ECS_CLUSTER_NAME"), DEFAULT_ECS_CLUSTER_NAME) +service_name = os.getenv(_param("SERVICE_NAME"), DEFAULT_SERVICE_NAME) +artifacts_bucket_name = os.getenv(_param("ARTIFACTS_BUCKET_NAME")) +# TODO: add persistent backend store + +if not ecr_repo_name: + raise ValueError("Missing input parameter ecr-repository-name") + +if not vpc_id: + raise ValueError("Missing input parameter vpc-id") + +app = aws_cdk.App() +stack = MlflowFargateStack( + scope=app, + id=app_prefix, + app_prefix=app_prefix, + vpc_id=vpc_id, + subnet_ids=subnet_ids, + cluster_name=cluster_name, + service_name=service_name, + ecr_repo_name=ecr_repo_name, + artifacts_bucket_name=artifacts_bucket_name, + env=aws_cdk.Environment( + account=os.environ["CDK_DEFAULT_ACCOUNT"], + region=os.environ["CDK_DEFAULT_REGION"], + ), +) + + +aws_cdk.CfnOutput( + scope=stack, + id="metadata", + value=stack.to_json_string( + { + "LoadBalancerDNS": stack.fargate_service.load_balancer.load_balancer_dns_name, + } + ), +) + +app.synth() diff --git a/modules/mlflow/mlflow-fargate/deployspec.yaml b/modules/mlflow/mlflow-fargate/deployspec.yaml new file mode 100644 index 00000000..dc6cd86f --- /dev/null +++ b/modules/mlflow/mlflow-fargate/deployspec.yaml @@ -0,0 +1,35 @@ +publishGenericEnvVariables: true +deploy: + phases: + install: + commands: + - npm install -g aws-cdk@2.126.0 + - pip install -r requirements.txt + pre_build: + commands: + - echo "Prebuild stage" + build: + commands: + - cdk deploy --require-approval never --progress events --app "python app.py" --outputs-file ./cdk-exports.json + # Export metadata + - seedfarmer metadata convert -f cdk-exports.json || true + post_build: + commands: + - echo "Build successful" +destroy: + phases: + install: + commands: + - npm install -g aws-cdk@2.126.0 + - pip install -r requirements.txt + pre_build: + commands: + - echo "Prebuild stage" + build: + commands: + - cdk destroy --force --app "python app.py" + post_build: + commands: + - echo "Destroy successful" + +build_type: BUILD_GENERAL1_SMALL \ No newline at end of file diff --git a/modules/mlflow/mlflow-fargate/pyproject.toml b/modules/mlflow/mlflow-fargate/pyproject.toml new file mode 100644 index 00000000..1f856efe --- /dev/null +++ b/modules/mlflow/mlflow-fargate/pyproject.toml @@ -0,0 +1,35 @@ +[tool.black] +line-length = 120 +target-version = ["py36", "py37", "py38"] +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | \.env + | _build + | buck-out + | build + | dist + | codeseeder.out +)/ +''' + +[tool.isort] +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +line_length = 120 +py_version = 36 +skip_gitignore = false + +[tool.pytest.ini_options] +addopts = "-v --cov=. --cov-report term --cov-config=coverage.ini --cov-fail-under=80" +pythonpath = [ + "." +] \ No newline at end of file diff --git a/modules/mlflow/mlflow-fargate/requirements-dev.in b/modules/mlflow/mlflow-fargate/requirements-dev.in new file mode 100644 index 00000000..6412f987 --- /dev/null +++ b/modules/mlflow/mlflow-fargate/requirements-dev.in @@ -0,0 +1,14 @@ +awscli +black +cdk-nag +cfn-lint +check-manifest +flake8 +isort +mypy +pip-tools +pydot +pyroma +pytest +types-PyYAML +types-setuptools diff --git a/modules/mlflow/mlflow-fargate/requirements.in b/modules/mlflow/mlflow-fargate/requirements.in new file mode 100644 index 00000000..369a6704 --- /dev/null +++ b/modules/mlflow/mlflow-fargate/requirements.in @@ -0,0 +1,4 @@ +aws-cdk-lib==2.126.0 +cdk-nag==2.28.27 +boto3==1.34.35 +cdk_ecr_deployment==2.5.30 \ No newline at end of file diff --git a/modules/mlflow/mlflow-fargate/requirements.txt b/modules/mlflow/mlflow-fargate/requirements.txt new file mode 100644 index 00000000..a3405f80 --- /dev/null +++ b/modules/mlflow/mlflow-fargate/requirements.txt @@ -0,0 +1,83 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile requirements.in +# +attrs==23.1.0 + # via + # cattrs + # jsii +aws-cdk-asset-awscli-v1==2.2.200 + # via aws-cdk-lib +aws-cdk-asset-kubectl-v20==2.1.2 + # via aws-cdk-lib +aws-cdk-asset-node-proxy-agent-v5==2.0.166 + # via aws-cdk-lib +aws-cdk-lib==2.83.1 + # via + # -r requirements.in + # cdk-nag +boto3==1.21.46 + # via -r requirements.in +botocore==1.24.46 + # via + # boto3 + # s3transfer +cattrs==23.1.2 + # via jsii +cdk-nag==2.12.29 + # via -r requirements.in +constructs==10.0.91 + # via + # -r requirements.in + # aws-cdk-lib + # cdk-nag +exceptiongroup==1.1.3 + # via cattrs +importlib-resources==6.1.0 + # via jsii +jmespath==1.0.1 + # via + # boto3 + # botocore +jsii==1.90.0 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v5 + # aws-cdk-lib + # cdk-nag + # constructs +publication==0.0.3 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v5 + # aws-cdk-lib + # cdk-nag + # constructs + # jsii +python-dateutil==2.8.2 + # via + # botocore + # jsii +s3transfer==0.5.2 + # via boto3 +six==1.16.0 + # via python-dateutil +typeguard==2.13.3 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v5 + # aws-cdk-lib + # jsii +typing-extensions==4.8.0 + # via + # cattrs + # jsii +urllib3==1.26.18 + # via botocore +zipp==3.17.0 + # via importlib-resources diff --git a/modules/mlflow/mlflow-fargate/setup.cfg b/modules/mlflow/mlflow-fargate/setup.cfg new file mode 100644 index 00000000..6136e2bb --- /dev/null +++ b/modules/mlflow/mlflow-fargate/setup.cfg @@ -0,0 +1,28 @@ +[metadata] +license_files = + LICENSE + NOTICE + VERSION + +[flake8] +max-line-length = 120 +extend-ignore = E203, W503 +exclude = + .git, + __pycache__, + docs/source/conf.py, + old, + build, + dist, + .venv, + codeseeder.out, + bundle + +[mypy] +python_version = 3.7 +strict = True +ignore_missing_imports = True +allow_untyped_decorators = True +exclude = + codeseeder.out/|example/|tests/ +warn_unused_ignores = False \ No newline at end of file diff --git a/modules/mlflow/mlflow-fargate/stack.py b/modules/mlflow/mlflow-fargate/stack.py new file mode 100644 index 00000000..df5d3bd6 --- /dev/null +++ b/modules/mlflow/mlflow-fargate/stack.py @@ -0,0 +1,108 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from typing import Any, List, cast + +from aws_cdk import Duration, Stack, Tags +from aws_cdk import aws_ec2 as ec2 +from aws_cdk import aws_ecr as ecr +from aws_cdk import aws_ecs as ecs +from aws_cdk import aws_ecs_patterns as ecs_patterns +from aws_cdk import aws_iam as iam +from constructs import Construct, IConstruct + + +class MlflowFargateStack(Stack): # type: ignore + def __init__( + self, + scope: Construct, + id: str, + app_prefix: str, + vpc_id: str, + subnet_ids: List[str], + cluster_name: str, + service_name: str, + ecr_repo_name: str, + artifacts_bucket_name: str, + **kwargs: Any, + ) -> None: + super().__init__(scope, id, **kwargs) + + Tags.of(scope=cast(IConstruct, self)).add(key="Deployment", value=app_prefix[:64]) + + role = iam.Role( + self, + "TaskRole", + assumed_by=iam.ServicePrincipal(service="ecs-tasks.amazonaws.com"), + managed_policies=[ + iam.ManagedPolicy.from_aws_managed_policy_name("AmazonS3FullAccess"), + iam.ManagedPolicy.from_aws_managed_policy_name("AmazonECS_FullAccess"), + ], + ) + + vpc = ec2.Vpc.from_lookup(self, "vpc", vpc_id=vpc_id) + self.subnets = [ec2.Subnet.from_subnet_id(self, f"sub-{subnet_id}", subnet_id) for subnet_id in subnet_ids] + + cluster = ecs.Cluster( + self, + "EcsCluster", + cluster_name=cluster_name, + vpc=vpc, + ) + + task_definition = ecs.FargateTaskDefinition( + self, + "MlflowTask", + task_role=role, + cpu=4 * 1024, + memory_limit_mib=8 * 1024, + ) + + container = task_definition.add_container( + "ContainerDef", + # TODO: add ability to pull specific tag + image=ecs.ContainerImage.from_ecr_repository( + repository=ecr.Repository.from_repository_name( + self, + "ecr-repo", + repository_name=ecr_repo_name, + ), + ), + environment={ + "BUCKET": f"s3://{artifacts_bucket_name}", + # TODO: Add persistence + # "HOST": database.db_instance_endpoint_address, + # "PORT": str(port), + # "DATABASE": db_name, + # "USERNAME": username, + }, + # secrets={"PASSWORD": ecs.Secret.from_secrets_manager(db_password_secret)}, + logging=ecs.LogDriver.aws_logs(stream_prefix="mlflow"), + ) + port_mapping = ecs.PortMapping(container_port=5000, host_port=5000, protocol=ecs.Protocol.TCP) + container.add_port_mappings(port_mapping) + + fargate_service = ecs_patterns.NetworkLoadBalancedFargateService( + self, + "MlflowLBService", + service_name=service_name, + cluster=cluster, + task_definition=task_definition, + ) + + # Setup security group + fargate_service.service.connections.security_groups[0].add_ingress_rule( + peer=ec2.Peer.ipv4(vpc.vpc_cidr_block), + connection=ec2.Port.tcp(5000), + description="Allow inbound from VPC for mlflow", + ) + + # Setup autoscaling policy + scaling = fargate_service.service.auto_scale_task_count(max_capacity=2) + scaling.scale_on_cpu_utilization( + id="AutoscalingPolicy", + target_utilization_percent=70, + scale_in_cooldown=Duration.seconds(60), + scale_out_cooldown=Duration.seconds(60), + ) + self.fargate_service = fargate_service From 2f254086ada34be005d1e40fc0219ef995edb035 Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Tue, 13 Feb 2024 16:52:39 +0000 Subject: [PATCH 05/20] update manifests Signed-off-by: Anton Kukushkin --- manifests/deployment.yaml | 6 ++++++ manifests/images-modules.yaml | 9 +++++++++ manifests/mlflow-modules.yaml | 27 +++++++++++++++++++++++++ manifests/sagemaker-studio-modules.yaml | 2 +- manifests/storage-modules.yaml | 15 ++++++++++++++ 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 manifests/images-modules.yaml create mode 100644 manifests/mlflow-modules.yaml create mode 100644 manifests/storage-modules.yaml diff --git a/manifests/deployment.yaml b/manifests/deployment.yaml index 6806f3ef..33946fb4 100644 --- a/manifests/deployment.yaml +++ b/manifests/deployment.yaml @@ -4,8 +4,14 @@ forceDependencyRedeploy: true groups: - name: networking path: manifests/networking-modules.yaml + - name: storage + path: manifests/storage-modules.yaml - name: sagemaker-studio path: manifests/sagemaker-studio-modules.yaml + - name: images + path: manifests/images-modules.yaml + - name: mlflow + path: manifests/mlflow-modules.yaml targetAccountMappings: - alias: primary accountId: diff --git a/manifests/images-modules.yaml b/manifests/images-modules.yaml new file mode 100644 index 00000000..dc85af23 --- /dev/null +++ b/manifests/images-modules.yaml @@ -0,0 +1,9 @@ +name: mlflow-image +path: modules/mlflow/mlflow-image +parameters: + - name: ecr-repository-name + valueFrom: + moduleMetadata: + group: storage + name: ecr-mlflow + key: EcrRepositoryName \ No newline at end of file diff --git a/manifests/mlflow-modules.yaml b/manifests/mlflow-modules.yaml new file mode 100644 index 00000000..17d9c571 --- /dev/null +++ b/manifests/mlflow-modules.yaml @@ -0,0 +1,27 @@ +name: mlflow-fargate +path: modules/mlflow/mlflow-fargate +parameters: + - name: vpc-id + valueFrom: + moduleMetadata: + group: networking + name: networking + key: VpcId + - name: subnet-ids + valueFrom: + moduleMetadata: + group: networking + name: networking + key: PrivateSubnetIds + - name: ecr-repository-name + valueFrom: + moduleMetadata: + group: storage + name: ecr-mlflow + key: EcrRepositoryName + - name: artifacts-bucket-name + valueFrom: + moduleMetadata: + group: storage + name: buckets + key: ArtifactsBucketName \ No newline at end of file diff --git a/manifests/sagemaker-studio-modules.yaml b/manifests/sagemaker-studio-modules.yaml index 321fdf21..a87c13c0 100644 --- a/manifests/sagemaker-studio-modules.yaml +++ b/manifests/sagemaker-studio-modules.yaml @@ -3,7 +3,7 @@ path: git::https://github.com/awslabs/idf-modules.git//modules/ml/sagemaker-stud targetAccount: primary parameters: - name: studio_domain_name - value: mlops + value: mlops-studio - name: vpc_id valueFrom: moduleMetadata: diff --git a/manifests/storage-modules.yaml b/manifests/storage-modules.yaml new file mode 100644 index 00000000..88c18e64 --- /dev/null +++ b/manifests/storage-modules.yaml @@ -0,0 +1,15 @@ +name: ecr-mlflow +path: git::https://github.com/awslabs/idf-modules.git//modules/storage/ecr?ref=release/1.3.0&depth=1 +parameters: + - name: repository_name + value: "ecr-mlflow" +--- +name: buckets +path: git::https://github.com/awslabs/idf-modules.git//modules/storage/buckets?ref=release/1.3.0&depth=1 +targetAccount: primary +targetRegion: us-east-1 +parameters: + - name: encryption-type + value: SSE + - name: retention-type + value: RETAIN \ No newline at end of file From 2930b1c0bac1e241881d0fe711aa68cc77398cd2 Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Tue, 13 Feb 2024 18:30:51 +0000 Subject: [PATCH 06/20] update readme & housekeeping Signed-off-by: Anton Kukushkin --- README.md | 9 +++- modules/mlflow/mlflow-fargate/README.md | 60 ++++++++++++++++++++++--- modules/mlflow/mlflow-fargate/app.py | 21 ++++++--- modules/mlflow/mlflow-fargate/stack.py | 27 ++++++----- 4 files changed, 92 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index d7a05606..1f1eac28 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The modules in this repository are decoupled from each other and can be aggregat The modules in this repository are / must be generic for reuse without affiliation to any one particular project in Machine Learning Operations domain. -All modules in this repository adhere to the module strutucture defined in the the [SeedFarmer Guide](https://seed-farmer.readthedocs.io/en/latest) +All modules in this repository adhere to the module structure defined in the the [SeedFarmer Guide](https://seed-farmer.readthedocs.io/en/latest) - [Project Structure](https://seed-farmer.readthedocs.io/en/latest/project_development.html) - [Module Development](https://seed-farmer.readthedocs.io/en/latest/module_development.html) @@ -22,6 +22,13 @@ All modules in this repository adhere to the module strutucture defined in the t |-----------------------------------------------------------------------------|-------------------------------------------------| | [SageMaker Endpoint Module](modules/sagemaker/sagemaker-endpoint/README.md) | Creates SageMaker real-time inference endpoint. | +### Mlflow Modules + +| Type | Description | +|---------------------------------------------------------------------|---------------------------------| +| [Mlflow Image Module](modules/mlflow/mlflow-image/README.md) | Creates Mlflow container image. | +| [Mlflow on Fargate Module](modules/mlflow/mlflow-fargate/README.md) | Runs Mlflow on AWS Fargate. | + ### Industry Data Framework (IDF) Modules The modules in this repository are compatible with [Industry Data Framework (IDF) Modules](https://github.com/awslabs/idf-modules) and can be used together within the same deployment. Refer to `examples/manifests` for examples. diff --git a/modules/mlflow/mlflow-fargate/README.md b/modules/mlflow/mlflow-fargate/README.md index dff5dd2c..f322a394 100644 --- a/modules/mlflow/mlflow-fargate/README.md +++ b/modules/mlflow/mlflow-fargate/README.md @@ -2,7 +2,7 @@ ## Description -This module runs Mlflow on Fargate. +This module runs Mlflow on AWS Fargate. ## Inputs/Outputs @@ -10,12 +10,62 @@ This module runs Mlflow on Fargate. #### Required -- `vpc-id`: The VPC-ID that the cluster will be created in. -- `subnet-ids`: The subnets that the cluster will be created in. +- `vpc-id`: The VPC-ID that the ECS cluster will be created in. +- `subnet-ids`: The subnets that the Fargate task will use. - `ecr-repository-name`: The name of the ECR repository to pull the image from. - `artifacts-bucket-name`: Name of the artifacts store bucket #### Optional -- `ecs-cluster-name`: Name of the ECS cluster -- `service-name`: name of the service \ No newline at end of file +- `ecs-cluster-name`: Name of the ECS cluster. +- `service-name`: Name of the service. +- `task-cpu-units`: The number of cpu units used by the Fargate task. +- `task-memory-limit-mb`: The amount (in MiB) of memory used by the Fargate task. + +### Sample manifest declaration + +```yaml +name: mlflow-fargate +path: modules/mlflow/mlflow-fargate +parameters: + - name: vpc-id + valueFrom: + moduleMetadata: + group: networking + name: networking + key: VpcId + - name: subnet-ids + valueFrom: + moduleMetadata: + group: networking + name: networking + key: PrivateSubnetIds + - name: ecr-repository-name + valueFrom: + moduleMetadata: + group: storage + name: ecr-mlflow + key: EcrRepositoryName + - name: artifacts-bucket-name + valueFrom: + moduleMetadata: + group: storage + name: buckets + key: ArtifactsBucketName +``` + +### Module Metadata Outputs + +- `ECSClusterName`: Name of the ECS cluster. +- `ServiceName`: Name of the service. +- `LoadBalancerDNSName`: Load balancer DNS name. + +#### Output Example + +``` +{ + "ECSClusterName": "mlops-mlops-mlflow-mlflow-fargate-EcsCluster97242B84-xxxxxxxxxxxx", + "ServiceName": "mlops-mlops-mlflow-mlflow-fargate-MlflowLBServiceEBACC043-xxxxxxxxxxxx", + "LoadBalancerDNSName": "xxxxxxxxxxxx.elb.us-east-1.amazonaws.com" +} +``` diff --git a/modules/mlflow/mlflow-fargate/app.py b/modules/mlflow/mlflow-fargate/app.py index 1a27dbb7..b9b7eeda 100644 --- a/modules/mlflow/mlflow-fargate/app.py +++ b/modules/mlflow/mlflow-fargate/app.py @@ -18,20 +18,23 @@ def _param(name: str) -> str: module_name = os.getenv("SEEDFARMER_MODULE_NAME", "") app_prefix = f"{project_name}-{deployment_name}-{module_name}" -DEFAULT_ECS_CLUSTER_NAME = "ecs-cluster" -DEFAULT_SERVICE_NAME = "service" +DEFAULT_ECS_CLUSTER_NAME = None +DEFAULT_SERVICE_NAME = None +DEFAULT_TASK_CPU_UNITS = 4 * 1024 +DEFAULT_TASK_MEMORY_LIMIT_MB = 8 * 1024 environment = aws_cdk.Environment( account=os.environ["CDK_DEFAULT_ACCOUNT"], region=os.environ["CDK_DEFAULT_REGION"], ) -# TODO: add ability to pull specific tag -ecr_repo_name = os.getenv(_param("ECR_REPOSITORY_NAME")) vpc_id = os.getenv(_param("VPC_ID")) subnet_ids = json.loads(os.getenv(_param("SUBNET_IDS"), "[]")) -cluster_name = os.getenv(_param("ECS_CLUSTER_NAME"), DEFAULT_ECS_CLUSTER_NAME) +ecs_cluster_name = os.getenv(_param("ECS_CLUSTER_NAME"), DEFAULT_ECS_CLUSTER_NAME) service_name = os.getenv(_param("SERVICE_NAME"), DEFAULT_SERVICE_NAME) +ecr_repo_name = os.getenv(_param("ECR_REPOSITORY_NAME")) +task_cpu_units = os.getenv(_param("TASK_CPU_UNITS"), DEFAULT_TASK_CPU_UNITS) +task_memory_limit_mb = os.getenv(_param("TASK_MEMORY_LIMIT_MB"), DEFAULT_TASK_MEMORY_LIMIT_MB) artifacts_bucket_name = os.getenv(_param("ARTIFACTS_BUCKET_NAME")) # TODO: add persistent backend store @@ -48,9 +51,11 @@ def _param(name: str) -> str: app_prefix=app_prefix, vpc_id=vpc_id, subnet_ids=subnet_ids, - cluster_name=cluster_name, + ecs_cluster_name=ecs_cluster_name, service_name=service_name, ecr_repo_name=ecr_repo_name, + task_cpu_units=int(task_cpu_units), + task_memory_limit_mb=int(task_memory_limit_mb), artifacts_bucket_name=artifacts_bucket_name, env=aws_cdk.Environment( account=os.environ["CDK_DEFAULT_ACCOUNT"], @@ -64,7 +69,9 @@ def _param(name: str) -> str: id="metadata", value=stack.to_json_string( { - "LoadBalancerDNS": stack.fargate_service.load_balancer.load_balancer_dns_name, + "ECSClusterName": stack.cluster.cluster_name, + "ServiceName": stack.service.service.service_name, + "LoadBalancerDNSName": stack.service.load_balancer.load_balancer_dns_name, } ), ) diff --git a/modules/mlflow/mlflow-fargate/stack.py b/modules/mlflow/mlflow-fargate/stack.py index df5d3bd6..d43b29d0 100644 --- a/modules/mlflow/mlflow-fargate/stack.py +++ b/modules/mlflow/mlflow-fargate/stack.py @@ -1,7 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from typing import Any, List, cast +from typing import Any, List, Optional, cast from aws_cdk import Duration, Stack, Tags from aws_cdk import aws_ec2 as ec2 @@ -20,9 +20,11 @@ def __init__( app_prefix: str, vpc_id: str, subnet_ids: List[str], - cluster_name: str, - service_name: str, + ecs_cluster_name: Optional[str], + service_name: Optional[str], ecr_repo_name: str, + task_cpu_units: int, + task_memory_limit_mb: int, artifacts_bucket_name: str, **kwargs: Any, ) -> None: @@ -41,21 +43,22 @@ def __init__( ) vpc = ec2.Vpc.from_lookup(self, "vpc", vpc_id=vpc_id) - self.subnets = [ec2.Subnet.from_subnet_id(self, f"sub-{subnet_id}", subnet_id) for subnet_id in subnet_ids] + subnets = [ec2.Subnet.from_subnet_id(self, f"sub-{subnet_id}", subnet_id) for subnet_id in subnet_ids] cluster = ecs.Cluster( self, "EcsCluster", - cluster_name=cluster_name, + cluster_name=ecs_cluster_name, vpc=vpc, ) + self.cluster = cluster task_definition = ecs.FargateTaskDefinition( self, "MlflowTask", task_role=role, - cpu=4 * 1024, - memory_limit_mib=8 * 1024, + cpu=task_cpu_units, + memory_limit_mib=task_memory_limit_mb, ) container = task_definition.add_container( @@ -64,7 +67,7 @@ def __init__( image=ecs.ContainerImage.from_ecr_repository( repository=ecr.Repository.from_repository_name( self, - "ecr-repo", + "ECRRepo", repository_name=ecr_repo_name, ), ), @@ -82,7 +85,7 @@ def __init__( port_mapping = ecs.PortMapping(container_port=5000, host_port=5000, protocol=ecs.Protocol.TCP) container.add_port_mappings(port_mapping) - fargate_service = ecs_patterns.NetworkLoadBalancedFargateService( + service = ecs_patterns.NetworkLoadBalancedFargateService( self, "MlflowLBService", service_name=service_name, @@ -91,18 +94,18 @@ def __init__( ) # Setup security group - fargate_service.service.connections.security_groups[0].add_ingress_rule( + service.service.connections.security_groups[0].add_ingress_rule( peer=ec2.Peer.ipv4(vpc.vpc_cidr_block), connection=ec2.Port.tcp(5000), description="Allow inbound from VPC for mlflow", ) # Setup autoscaling policy - scaling = fargate_service.service.auto_scale_task_count(max_capacity=2) + scaling = service.service.auto_scale_task_count(max_capacity=2) scaling.scale_on_cpu_utilization( id="AutoscalingPolicy", target_utilization_percent=70, scale_in_cooldown=Duration.seconds(60), scale_out_cooldown=Duration.seconds(60), ) - self.fargate_service = fargate_service + self.service = service From 9a34e4007e969f7c023dafad5e128c3d8936c342 Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Wed, 14 Feb 2024 14:44:15 +0000 Subject: [PATCH 07/20] update docs Signed-off-by: Anton Kukushkin --- README.md | 8 ++++---- modules/mlflow/mlflow-fargate/README.md | 4 ++++ .../mlflow-fargate-module-architecture.png | Bin 0 -> 45054 bytes .../mlflow-fargate-module-architecture.xml | 1 + 4 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 modules/mlflow/mlflow-fargate/docs/_static/mlflow-fargate-module-architecture.png create mode 100644 modules/mlflow/mlflow-fargate/docs/_static/mlflow-fargate-module-architecture.xml diff --git a/README.md b/README.md index 1f1eac28..27d96bb5 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,10 @@ All modules in this repository adhere to the module structure defined in the the ### Mlflow Modules -| Type | Description | -|---------------------------------------------------------------------|---------------------------------| -| [Mlflow Image Module](modules/mlflow/mlflow-image/README.md) | Creates Mlflow container image. | -| [Mlflow on Fargate Module](modules/mlflow/mlflow-fargate/README.md) | Runs Mlflow on AWS Fargate. | +| Type | Description | +|-------------------------------------------------------------------------|---------------------------------| +| [Mlflow Image Module](modules/mlflow/mlflow-image/README.md) | Creates Mlflow container image. | +| [Mlflow on AWS Fargate Module](modules/mlflow/mlflow-fargate/README.md) | Runs Mlflow on AWS Fargate. | ### Industry Data Framework (IDF) Modules diff --git a/modules/mlflow/mlflow-fargate/README.md b/modules/mlflow/mlflow-fargate/README.md index f322a394..9f1dae68 100644 --- a/modules/mlflow/mlflow-fargate/README.md +++ b/modules/mlflow/mlflow-fargate/README.md @@ -4,6 +4,10 @@ This module runs Mlflow on AWS Fargate. +### Architecture + +![Mlflow on AWS Fargate Module Architecture](docs/_static/mlflow-fargate-module-architecture.png "Mlflow on AWS Fargate Module Architecture") + ## Inputs/Outputs ### Input Parameters diff --git a/modules/mlflow/mlflow-fargate/docs/_static/mlflow-fargate-module-architecture.png b/modules/mlflow/mlflow-fargate/docs/_static/mlflow-fargate-module-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..391a4826e24680fe1180838f4e358285be44ebb0 GIT binary patch literal 45054 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 \ No newline at end of file From f2f22698d442ed77f75caf1d564aa88570a57091 Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Wed, 14 Feb 2024 14:49:27 +0000 Subject: [PATCH 08/20] use provided task subnets, scope down s3 permissions Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-fargate/app.py | 8 ++++++-- modules/mlflow/mlflow-fargate/stack.py | 11 ++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/modules/mlflow/mlflow-fargate/app.py b/modules/mlflow/mlflow-fargate/app.py index b9b7eeda..2ba6f371 100644 --- a/modules/mlflow/mlflow-fargate/app.py +++ b/modules/mlflow/mlflow-fargate/app.py @@ -38,11 +38,15 @@ def _param(name: str) -> str: artifacts_bucket_name = os.getenv(_param("ARTIFACTS_BUCKET_NAME")) # TODO: add persistent backend store +if not vpc_id: + raise ValueError("Missing input parameter vpc-id") + if not ecr_repo_name: raise ValueError("Missing input parameter ecr-repository-name") -if not vpc_id: - raise ValueError("Missing input parameter vpc-id") +if not artifacts_bucket_name: + raise ValueError("Missing input parameter artifacts-bucket-name") + app = aws_cdk.App() stack = MlflowFargateStack( diff --git a/modules/mlflow/mlflow-fargate/stack.py b/modules/mlflow/mlflow-fargate/stack.py index d43b29d0..e442ab1e 100644 --- a/modules/mlflow/mlflow-fargate/stack.py +++ b/modules/mlflow/mlflow-fargate/stack.py @@ -9,6 +9,7 @@ from aws_cdk import aws_ecs as ecs from aws_cdk import aws_ecs_patterns as ecs_patterns from aws_cdk import aws_iam as iam +from aws_cdk import aws_s3 as s3 from constructs import Construct, IConstruct @@ -37,13 +38,16 @@ def __init__( "TaskRole", assumed_by=iam.ServicePrincipal(service="ecs-tasks.amazonaws.com"), managed_policies=[ - iam.ManagedPolicy.from_aws_managed_policy_name("AmazonS3FullAccess"), iam.ManagedPolicy.from_aws_managed_policy_name("AmazonECS_FullAccess"), ], ) - vpc = ec2.Vpc.from_lookup(self, "vpc", vpc_id=vpc_id) - subnets = [ec2.Subnet.from_subnet_id(self, f"sub-{subnet_id}", subnet_id) for subnet_id in subnet_ids] + # Grant artifacts bucket read-write permissions + model_bucket = s3.Bucket.from_bucket_name(self, "ArtifactsBucket", bucket_name=artifacts_bucket_name) + model_bucket.grant_read_write(role) + + vpc = ec2.Vpc.from_lookup(self, "Vpc", vpc_id=vpc_id) + subnets = [ec2.Subnet.from_subnet_id(self, f"Sub{subnet_id}", subnet_id) for subnet_id in subnet_ids] cluster = ecs.Cluster( self, @@ -91,6 +95,7 @@ def __init__( service_name=service_name, cluster=cluster, task_definition=task_definition, + task_subnets=ec2.SubnetSelection(subnets=subnets), ) # Setup security group From 42199f7282711bf40dcc7a4028665cf0de900207 Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Wed, 14 Feb 2024 18:14:37 +0000 Subject: [PATCH 09/20] add basic unit tests Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-fargate/coverage.ini | 3 + .../mlflow-fargate/requirements-dev.txt | 254 ++++++++++++++++++ .../mlflow/mlflow-fargate/tests/__init__.py | 0 .../mlflow/mlflow-fargate/tests/test_app.py | 46 ++++ .../mlflow/mlflow-fargate/tests/test_stack.py | 56 ++++ .../mlflow/mlflow-image/requirements-dev.txt | 2 +- modules/mlflow/mlflow-image/tests/__init__.py | 0 modules/mlflow/mlflow-image/tests/test_app.py | 30 +++ .../mlflow/mlflow-image/tests/test_stack.py | 43 +++ 9 files changed, 433 insertions(+), 1 deletion(-) create mode 100644 modules/mlflow/mlflow-fargate/coverage.ini create mode 100644 modules/mlflow/mlflow-fargate/requirements-dev.txt create mode 100644 modules/mlflow/mlflow-fargate/tests/__init__.py create mode 100644 modules/mlflow/mlflow-fargate/tests/test_app.py create mode 100644 modules/mlflow/mlflow-fargate/tests/test_stack.py create mode 100644 modules/mlflow/mlflow-image/tests/__init__.py create mode 100644 modules/mlflow/mlflow-image/tests/test_app.py create mode 100644 modules/mlflow/mlflow-image/tests/test_stack.py diff --git a/modules/mlflow/mlflow-fargate/coverage.ini b/modules/mlflow/mlflow-fargate/coverage.ini new file mode 100644 index 00000000..c3878739 --- /dev/null +++ b/modules/mlflow/mlflow-fargate/coverage.ini @@ -0,0 +1,3 @@ +[run] +omit = + tests/* \ No newline at end of file diff --git a/modules/mlflow/mlflow-fargate/requirements-dev.txt b/modules/mlflow/mlflow-fargate/requirements-dev.txt new file mode 100644 index 00000000..1864c0bb --- /dev/null +++ b/modules/mlflow/mlflow-fargate/requirements-dev.txt @@ -0,0 +1,254 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --output-file=requirements-dev.txt requirements-dev.in +# +annotated-types==0.6.0 + # via pydantic +attrs==23.2.0 + # via + # cattrs + # jschema-to-python + # jsii + # jsonschema + # referencing + # sarif-om +aws-cdk-asset-awscli-v1==2.2.202 + # via aws-cdk-lib +aws-cdk-asset-kubectl-v20==2.1.2 + # via aws-cdk-lib +aws-cdk-asset-node-proxy-agent-v6==2.0.1 + # via aws-cdk-lib +aws-cdk-lib==2.127.0 + # via cdk-nag +aws-sam-translator==1.84.0 + # via cfn-lint +awscli==1.32.41 + # via -r requirements-dev.in +black==24.2.0 + # via -r requirements-dev.in +boto3==1.34.41 + # via aws-sam-translator +botocore==1.34.41 + # via + # awscli + # boto3 + # s3transfer +build==1.0.3 + # via + # check-manifest + # pip-tools + # pyroma +cattrs==23.2.3 + # via jsii +cdk-nag==2.28.34 + # via -r requirements-dev.in +certifi==2024.2.2 + # via requests +cfn-lint==0.85.1 + # via -r requirements-dev.in +charset-normalizer==3.3.2 + # via requests +check-manifest==0.49 + # via -r requirements-dev.in +click==8.1.7 + # via + # black + # pip-tools +colorama==0.4.4 + # via awscli +constructs==10.3.0 + # via + # aws-cdk-lib + # cdk-nag +docutils==0.16 + # via + # awscli + # pyroma +exceptiongroup==1.2.0 + # via + # cattrs + # pytest +flake8==7.0.0 + # via -r requirements-dev.in +idna==3.6 + # via requests +importlib-metadata==7.0.1 + # via build +importlib-resources==6.1.1 + # via jsii +iniconfig==2.0.0 + # via pytest +isort==5.13.2 + # via -r requirements-dev.in +jmespath==1.0.1 + # via + # boto3 + # botocore +jschema-to-python==1.2.3 + # via cfn-lint +jsii==1.94.0 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-nag + # constructs +jsonpatch==1.33 + # via cfn-lint +jsonpickle==3.0.2 + # via jschema-to-python +jsonpointer==2.4 + # via jsonpatch +jsonschema==4.21.1 + # via + # aws-sam-translator + # cfn-lint +jsonschema-specifications==2023.12.1 + # via jsonschema +junit-xml==1.9 + # via cfn-lint +mccabe==0.7.0 + # via flake8 +mpmath==1.3.0 + # via sympy +mypy==1.8.0 + # via -r requirements-dev.in +mypy-extensions==1.0.0 + # via + # black + # mypy +networkx==3.2.1 + # via cfn-lint +packaging==23.2 + # via + # black + # build + # pyroma + # pytest +pathspec==0.12.1 + # via black +pbr==6.0.0 + # via + # jschema-to-python + # sarif-om +pip-tools==7.3.0 + # via -r requirements-dev.in +platformdirs==4.2.0 + # via black +pluggy==1.4.0 + # via pytest +publication==0.0.3 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-nag + # constructs + # jsii +pyasn1==0.5.1 + # via rsa +pycodestyle==2.11.1 + # via flake8 +pydantic==2.6.1 + # via aws-sam-translator +pydantic-core==2.16.2 + # via pydantic +pydot==2.0.0 + # via -r requirements-dev.in +pyflakes==3.2.0 + # via flake8 +pygments==2.17.2 + # via pyroma +pyparsing==3.1.1 + # via pydot +pyproject-hooks==1.0.0 + # via build +pyroma==4.2 + # via -r requirements-dev.in +pytest==8.0.0 + # via -r requirements-dev.in +python-dateutil==2.8.2 + # via + # botocore + # jsii +pyyaml==6.0.1 + # via + # awscli + # cfn-lint +referencing==0.33.0 + # via + # jsonschema + # jsonschema-specifications +regex==2023.12.25 + # via cfn-lint +requests==2.31.0 + # via pyroma +rpds-py==0.18.0 + # via + # jsonschema + # referencing +rsa==4.7.2 + # via awscli +s3transfer==0.10.0 + # via + # awscli + # boto3 +sarif-om==1.0.4 + # via cfn-lint +six==1.16.0 + # via + # junit-xml + # python-dateutil +sympy==1.12 + # via cfn-lint +tomli==2.0.1 + # via + # black + # build + # check-manifest + # mypy + # pip-tools + # pyproject-hooks + # pytest +trove-classifiers==2024.1.31 + # via pyroma +typeguard==2.13.3 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-nag + # constructs + # jsii +types-pyyaml==6.0.12.12 + # via -r requirements-dev.in +types-setuptools==69.0.0.20240125 + # via -r requirements-dev.in +typing-extensions==4.9.0 + # via + # aws-sam-translator + # black + # cattrs + # jsii + # mypy + # pydantic + # pydantic-core +urllib3==1.26.18 + # via + # botocore + # requests +wheel==0.42.0 + # via pip-tools +zipp==3.17.0 + # via + # importlib-metadata + # importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/modules/mlflow/mlflow-fargate/tests/__init__.py b/modules/mlflow/mlflow-fargate/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/mlflow/mlflow-fargate/tests/test_app.py b/modules/mlflow/mlflow-fargate/tests/test_app.py new file mode 100644 index 00000000..d0615f84 --- /dev/null +++ b/modules/mlflow/mlflow-fargate/tests/test_app.py @@ -0,0 +1,46 @@ +import os +import sys + +import pytest + + +@pytest.fixture(scope="function") +def stack_defaults(): + os.environ["SEEDFARMER_PROJECT_NAME"] = "test-project" + os.environ["SEEDFARMER_DEPLOYMENT_NAME"] = "test-deployment" + os.environ["SEEDFARMER_MODULE_NAME"] = "test-module" + os.environ["CDK_DEFAULT_ACCOUNT"] = "111111111111" + os.environ["CDK_DEFAULT_REGION"] = "us-east-1" + + os.environ["SEEDFARMER_PARAMETER_VPC_ID"] = "vpc-12345" + os.environ["SEEDFARMER_PARAMETER_ECR_REPOSITORY_NAME"] = "repo5" + os.environ["SEEDFARMER_PARAMETER_ARTIFACTS_BUCKET_NAME"] = "bucket" + + # Unload the app import so that subsequent tests don't reuse + if "app" in sys.modules: + del sys.modules["app"] + + +def test_app(stack_defaults): + import app # noqa: F401 + + +def test_vpc_id(stack_defaults): + del os.environ["SEEDFARMER_PARAMETER_VPC_ID"] + + with pytest.raises(Exception, match="Missing input parameter vpc-id"): + import app # noqa: F401 + + +def test_ecr_repository_name(stack_defaults): + del os.environ["SEEDFARMER_PARAMETER_ECR_REPOSITORY_NAME"] + + with pytest.raises(Exception, match="Missing input parameter ecr-repository-name"): + import app # noqa: F401 + + +def test_artifacts_bucket_name(stack_defaults): + del os.environ["SEEDFARMER_PARAMETER_ARTIFACTS_BUCKET_NAME"] + + with pytest.raises(Exception, match="Missing input parameter artifacts-bucket-name"): + import app # noqa: F401 diff --git a/modules/mlflow/mlflow-fargate/tests/test_stack.py b/modules/mlflow/mlflow-fargate/tests/test_stack.py new file mode 100644 index 00000000..ec05b2f1 --- /dev/null +++ b/modules/mlflow/mlflow-fargate/tests/test_stack.py @@ -0,0 +1,56 @@ +import os +import sys + +import aws_cdk as cdk +import pytest +from aws_cdk.assertions import Template + + +@pytest.fixture(scope="function") +def stack_defaults() -> None: + os.environ["CDK_DEFAULT_ACCOUNT"] = "111111111111" + os.environ["CDK_DEFAULT_REGION"] = "us-east-1" + + # Unload the app import so that subsequent tests don't reuse + if "stack" in sys.modules: + del sys.modules["stack"] + + +def test_synthesize_stack() -> None: + import stack + + app = cdk.App() + + project_name = "test-project" + dep_name = "test-deployment" + mod_name = "test-module" + app_prefix = f"{project_name}-{dep_name}-{mod_name}" + + vpc_id = "vpc-123" + subnet_ids = [] + ecr_repo_name = "repo" + task_cpu_units = 4 * 1024 + task_memory_limit_mb = 8 * 1024 + artifacts_bucket_name = "bucket" + + stack = stack.MlflowFargateStack( + scope=app, + id=app_prefix, + app_prefix=app_prefix, + vpc_id=vpc_id, + subnet_ids=subnet_ids, + ecs_cluster_name=None, + service_name=None, + ecr_repo_name=ecr_repo_name, + task_cpu_units=task_cpu_units, + task_memory_limit_mb=task_memory_limit_mb, + artifacts_bucket_name=artifacts_bucket_name, + env=cdk.Environment( + account=os.environ["CDK_DEFAULT_ACCOUNT"], + region=os.environ["CDK_DEFAULT_REGION"], + ), + ) + + template = Template.from_stack(stack) + template.resource_count_is("AWS::ECS::Cluster", 1) + template.resource_count_is("AWS::ECS::TaskDefinition", 1) diff --git a/modules/mlflow/mlflow-image/requirements-dev.txt b/modules/mlflow/mlflow-image/requirements-dev.txt index 59d38e3f..96ce3195 100644 --- a/modules/mlflow/mlflow-image/requirements-dev.txt +++ b/modules/mlflow/mlflow-image/requirements-dev.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.9 # by the following command: # -# pip-compile requirements-dev.in +# pip-compile --output-file=requirements-dev.txt requirements-dev.in # aiohttp==3.9.2 # via black diff --git a/modules/mlflow/mlflow-image/tests/__init__.py b/modules/mlflow/mlflow-image/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/mlflow/mlflow-image/tests/test_app.py b/modules/mlflow/mlflow-image/tests/test_app.py new file mode 100644 index 00000000..4a2d61c2 --- /dev/null +++ b/modules/mlflow/mlflow-image/tests/test_app.py @@ -0,0 +1,30 @@ +import os +import sys + +import pytest + + +@pytest.fixture(scope="function") +def stack_defaults(): + os.environ["SEEDFARMER_PROJECT_NAME"] = "test-project" + os.environ["SEEDFARMER_DEPLOYMENT_NAME"] = "test-deployment" + os.environ["SEEDFARMER_MODULE_NAME"] = "test-module" + os.environ["CDK_DEFAULT_ACCOUNT"] = "111111111111" + os.environ["CDK_DEFAULT_REGION"] = "us-east-1" + + os.environ["SEEDFARMER_PARAMETER_ECR_REPOSITORY_NAME"] = "repo" + + # Unload the app import so that subsequent tests don't reuse + if "app" in sys.modules: + del sys.modules["app"] + + +def test_app(stack_defaults): + import app # noqa: F401 + + +def test_vpc_id(stack_defaults): + del os.environ["SEEDFARMER_PARAMETER_ECR_REPOSITORY_NAME"] + + with pytest.raises(Exception, match="Missing input parameter ecr-repository-name"): + import app # noqa: F401 diff --git a/modules/mlflow/mlflow-image/tests/test_stack.py b/modules/mlflow/mlflow-image/tests/test_stack.py new file mode 100644 index 00000000..d60ac344 --- /dev/null +++ b/modules/mlflow/mlflow-image/tests/test_stack.py @@ -0,0 +1,43 @@ +import os +import sys + +import aws_cdk as cdk +import pytest +from aws_cdk.assertions import Template + + +@pytest.fixture(scope="function") +def stack_defaults() -> None: + os.environ["CDK_DEFAULT_ACCOUNT"] = "111111111111" + os.environ["CDK_DEFAULT_REGION"] = "us-east-1" + + # Unload the app import so that subsequent tests don't reuse + if "stack" in sys.modules: + del sys.modules["stack"] + + +def test_synthesize_stack() -> None: + import stack + + app = cdk.App() + + project_name = "test-project" + dep_name = "test-deployment" + mod_name = "test-module" + app_prefix = f"{project_name}-{dep_name}-{mod_name}" + + ecr_repo_name = "repo" + + stack = stack.MlflowImagePublishingStack( + scope=app, + id=app_prefix, + app_prefix=app_prefix, + ecr_repo_name=ecr_repo_name, + env=cdk.Environment( + account=os.environ["CDK_DEFAULT_ACCOUNT"], + region=os.environ["CDK_DEFAULT_REGION"], + ), + ) + + template = Template.from_stack(stack) + template.resource_count_is("Custom::CDKBucketDeployment", 1) From 634d0cbf51d08fcd94f486fd6bb117aab6b279b3 Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Thu, 15 Feb 2024 14:10:13 +0000 Subject: [PATCH 10/20] add cdk-nag & enable LB access logs Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-fargate/app.py | 1 + modules/mlflow/mlflow-fargate/stack.py | 47 +++++++++++++++++++++++++- modules/mlflow/mlflow-image/stack.py | 7 ++-- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/modules/mlflow/mlflow-fargate/app.py b/modules/mlflow/mlflow-fargate/app.py index 2ba6f371..5299921e 100644 --- a/modules/mlflow/mlflow-fargate/app.py +++ b/modules/mlflow/mlflow-fargate/app.py @@ -76,6 +76,7 @@ def _param(name: str) -> str: "ECSClusterName": stack.cluster.cluster_name, "ServiceName": stack.service.service.service_name, "LoadBalancerDNSName": stack.service.load_balancer.load_balancer_dns_name, + "LoadBalancerAccessLogsBucketArn": stack.lb_access_logs_bucket.bucket_arn, } ), ) diff --git a/modules/mlflow/mlflow-fargate/stack.py b/modules/mlflow/mlflow-fargate/stack.py index e442ab1e..e2707c68 100644 --- a/modules/mlflow/mlflow-fargate/stack.py +++ b/modules/mlflow/mlflow-fargate/stack.py @@ -3,13 +3,14 @@ from typing import Any, List, Optional, cast -from aws_cdk import Duration, Stack, Tags +from aws_cdk import Aspects, Duration, Stack, Tags from aws_cdk import aws_ec2 as ec2 from aws_cdk import aws_ecr as ecr from aws_cdk import aws_ecs as ecs from aws_cdk import aws_ecs_patterns as ecs_patterns from aws_cdk import aws_iam as iam from aws_cdk import aws_s3 as s3 +from cdk_nag import AwsSolutionsChecks, NagPackSuppression, NagSuppressions from constructs import Construct, IConstruct @@ -54,6 +55,7 @@ def __init__( "EcsCluster", cluster_name=ecs_cluster_name, vpc=vpc, + container_insights=True, ) self.cluster = cluster @@ -97,6 +99,15 @@ def __init__( task_definition=task_definition, task_subnets=ec2.SubnetSelection(subnets=subnets), ) + lb_access_logs_bucket = s3.Bucket( + self, + "LBAccessLogsBucket", + encryption=s3.BucketEncryption.S3_MANAGED, + block_public_access=s3.BlockPublicAccess.BLOCK_ALL, + enforce_ssl=True, + ) + service.load_balancer.log_access_logs(bucket=lb_access_logs_bucket) + self.lb_access_logs_bucket = lb_access_logs_bucket # Setup security group service.service.connections.security_groups[0].add_ingress_rule( @@ -114,3 +125,37 @@ def __init__( scale_out_cooldown=Duration.seconds(60), ) self.service = service + + # Add CDK nag solutions checks + Aspects.of(self).add(AwsSolutionsChecks()) + + NagSuppressions.add_stack_suppressions( + self, + apply_to_nested_stacks=True, + suppressions=[ + NagPackSuppression( + **{ + "id": "AwsSolutions-IAM4", + "reason": "Managed Policies are for src account roles only", + } + ), + NagPackSuppression( + **{ + "id": "AwsSolutions-IAM5", + "reason": "Resource access restricted to resources", + } + ), + NagPackSuppression( + **{ + "id": "AwsSolutions-ECS2", + "reason": "Not passing secrets via env variables", + } + ), + NagPackSuppression( + **{ + "id": "AwsSolutions-S1", + "reason": "Access logs not required for access logs bucket", + } + ), + ], + ) diff --git a/modules/mlflow/mlflow-image/stack.py b/modules/mlflow/mlflow-image/stack.py index 47fb380d..3557eb93 100644 --- a/modules/mlflow/mlflow-image/stack.py +++ b/modules/mlflow/mlflow-image/stack.py @@ -4,11 +4,11 @@ import os from typing import Any, cast -from aws_cdk import Stack, Tags +from aws_cdk import Aspects, Stack, Tags from aws_cdk import aws_ecr as ecr from aws_cdk.aws_ecr_assets import DockerImageAsset from cdk_ecr_deployment import DockerImageName, ECRDeployment -from cdk_nag import NagPackSuppression, NagSuppressions +from cdk_nag import AwsSolutionsChecks, NagPackSuppression, NagSuppressions from constructs import Construct, IConstruct @@ -41,6 +41,9 @@ def __init__( dest=DockerImageName(self.image_uri), ) + # Add CDK nag solutions checks + Aspects.of(self).add(AwsSolutionsChecks()) + NagSuppressions.add_stack_suppressions( self, apply_to_nested_stacks=True, From 66c69d269d2771efde5e48ed698bc810ab464b16 Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Thu, 15 Feb 2024 14:15:08 +0000 Subject: [PATCH 11/20] add autoscale_max_capacity param Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-fargate/app.py | 3 +++ modules/mlflow/mlflow-fargate/stack.py | 3 ++- modules/mlflow/mlflow-fargate/tests/test_stack.py | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/mlflow/mlflow-fargate/app.py b/modules/mlflow/mlflow-fargate/app.py index 5299921e..65af4cad 100644 --- a/modules/mlflow/mlflow-fargate/app.py +++ b/modules/mlflow/mlflow-fargate/app.py @@ -22,6 +22,7 @@ def _param(name: str) -> str: DEFAULT_SERVICE_NAME = None DEFAULT_TASK_CPU_UNITS = 4 * 1024 DEFAULT_TASK_MEMORY_LIMIT_MB = 8 * 1024 +DEFAULT_AUTOSCALE_MAX_CAPACITY = 2 environment = aws_cdk.Environment( account=os.environ["CDK_DEFAULT_ACCOUNT"], @@ -35,6 +36,7 @@ def _param(name: str) -> str: ecr_repo_name = os.getenv(_param("ECR_REPOSITORY_NAME")) task_cpu_units = os.getenv(_param("TASK_CPU_UNITS"), DEFAULT_TASK_CPU_UNITS) task_memory_limit_mb = os.getenv(_param("TASK_MEMORY_LIMIT_MB"), DEFAULT_TASK_MEMORY_LIMIT_MB) +autoscale_max_capacity = os.getenv(_param("AUTOSCALE_MAX_CAPACITY"), DEFAULT_AUTOSCALE_MAX_CAPACITY) artifacts_bucket_name = os.getenv(_param("ARTIFACTS_BUCKET_NAME")) # TODO: add persistent backend store @@ -60,6 +62,7 @@ def _param(name: str) -> str: ecr_repo_name=ecr_repo_name, task_cpu_units=int(task_cpu_units), task_memory_limit_mb=int(task_memory_limit_mb), + autoscale_max_capacity=int(autoscale_max_capacity), artifacts_bucket_name=artifacts_bucket_name, env=aws_cdk.Environment( account=os.environ["CDK_DEFAULT_ACCOUNT"], diff --git a/modules/mlflow/mlflow-fargate/stack.py b/modules/mlflow/mlflow-fargate/stack.py index e2707c68..31e5f07c 100644 --- a/modules/mlflow/mlflow-fargate/stack.py +++ b/modules/mlflow/mlflow-fargate/stack.py @@ -27,6 +27,7 @@ def __init__( ecr_repo_name: str, task_cpu_units: int, task_memory_limit_mb: int, + autoscale_max_capacity: int, artifacts_bucket_name: str, **kwargs: Any, ) -> None: @@ -117,7 +118,7 @@ def __init__( ) # Setup autoscaling policy - scaling = service.service.auto_scale_task_count(max_capacity=2) + scaling = service.service.auto_scale_task_count(max_capacity=autoscale_max_capacity) scaling.scale_on_cpu_utilization( id="AutoscalingPolicy", target_utilization_percent=70, diff --git a/modules/mlflow/mlflow-fargate/tests/test_stack.py b/modules/mlflow/mlflow-fargate/tests/test_stack.py index ec05b2f1..4f22980c 100644 --- a/modules/mlflow/mlflow-fargate/tests/test_stack.py +++ b/modules/mlflow/mlflow-fargate/tests/test_stack.py @@ -31,6 +31,7 @@ def test_synthesize_stack() -> None: ecr_repo_name = "repo" task_cpu_units = 4 * 1024 task_memory_limit_mb = 8 * 1024 + autoscale_max_capacity = 2 artifacts_bucket_name = "bucket" stack = stack.MlflowFargateStack( @@ -44,6 +45,7 @@ def test_synthesize_stack() -> None: ecr_repo_name=ecr_repo_name, task_cpu_units=task_cpu_units, task_memory_limit_mb=task_memory_limit_mb, + autoscale_max_capacity=autoscale_max_capacity, artifacts_bucket_name=artifacts_bucket_name, env=cdk.Environment( account=os.environ["CDK_DEFAULT_ACCOUNT"], From 8f3b9f75e2f610843e04ea49b011737f1a5d353d Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Thu, 15 Feb 2024 17:17:30 +0000 Subject: [PATCH 12/20] add CHANGELOG.MD Signed-off-by: Anton Kukushkin --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..293c613e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +======= + +## UNRELEASED + +### **Added** + +- added `mlflow-image` and `mlflow-fargate` modules +- added `sagemaker-endpoint` module + +### **Changed** + +### **Removed** + +======= From a2109fba0e1161cef1605266816cd9038878dbbc Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Fri, 16 Feb 2024 15:49:46 +0000 Subject: [PATCH 13/20] bump cdk version Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-fargate/requirements.in | 2 +- .../mlflow/mlflow-fargate/requirements.txt | 33 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/modules/mlflow/mlflow-fargate/requirements.in b/modules/mlflow/mlflow-fargate/requirements.in index 369a6704..14ffea02 100644 --- a/modules/mlflow/mlflow-fargate/requirements.in +++ b/modules/mlflow/mlflow-fargate/requirements.in @@ -1,4 +1,4 @@ -aws-cdk-lib==2.126.0 +aws-cdk-lib==2.128.0 cdk-nag==2.28.27 boto3==1.34.35 cdk_ecr_deployment==2.5.30 \ No newline at end of file diff --git a/modules/mlflow/mlflow-fargate/requirements.txt b/modules/mlflow/mlflow-fargate/requirements.txt index a3405f80..263a2cdf 100644 --- a/modules/mlflow/mlflow-fargate/requirements.txt +++ b/modules/mlflow/mlflow-fargate/requirements.txt @@ -2,36 +2,39 @@ # This file is autogenerated by pip-compile with Python 3.9 # by the following command: # -# pip-compile requirements.in +# pip-compile --output-file=requirements.txt requirements.in # attrs==23.1.0 # via # cattrs # jsii -aws-cdk-asset-awscli-v1==2.2.200 +aws-cdk-asset-awscli-v1==2.2.202 # via aws-cdk-lib aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib -aws-cdk-asset-node-proxy-agent-v5==2.0.166 +aws-cdk-asset-node-proxy-agent-v6==2.0.1 # via aws-cdk-lib -aws-cdk-lib==2.83.1 +aws-cdk-lib==2.128.0 # via # -r requirements.in + # cdk-ecr-deployment # cdk-nag -boto3==1.21.46 +boto3==1.34.35 # via -r requirements.in -botocore==1.24.46 +botocore==1.34.43 # via # boto3 # s3transfer cattrs==23.1.2 # via jsii -cdk-nag==2.12.29 +cdk-ecr-deployment==2.5.30 + # via -r requirements.in +cdk-nag==2.28.27 # via -r requirements.in constructs==10.0.91 # via - # -r requirements.in # aws-cdk-lib + # cdk-ecr-deployment # cdk-nag exceptiongroup==1.1.3 # via cattrs @@ -41,20 +44,22 @@ jmespath==1.0.1 # via # boto3 # botocore -jsii==1.90.0 +jsii==1.94.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-kubectl-v20 - # aws-cdk-asset-node-proxy-agent-v5 + # aws-cdk-asset-node-proxy-agent-v6 # aws-cdk-lib + # cdk-ecr-deployment # cdk-nag # constructs publication==0.0.3 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-kubectl-v20 - # aws-cdk-asset-node-proxy-agent-v5 + # aws-cdk-asset-node-proxy-agent-v6 # aws-cdk-lib + # cdk-ecr-deployment # cdk-nag # constructs # jsii @@ -62,7 +67,7 @@ python-dateutil==2.8.2 # via # botocore # jsii -s3transfer==0.5.2 +s3transfer==0.10.0 # via boto3 six==1.16.0 # via python-dateutil @@ -70,8 +75,10 @@ typeguard==2.13.3 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-kubectl-v20 - # aws-cdk-asset-node-proxy-agent-v5 + # aws-cdk-asset-node-proxy-agent-v6 # aws-cdk-lib + # cdk-ecr-deployment + # cdk-nag # jsii typing-extensions==4.8.0 # via From 16ae34292a5508dd2fe78a538320d5d0fcdb56b5 Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Fri, 16 Feb 2024 15:50:12 +0000 Subject: [PATCH 14/20] add EFS Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-fargate/stack.py | 59 ++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/modules/mlflow/mlflow-fargate/stack.py b/modules/mlflow/mlflow-fargate/stack.py index 31e5f07c..23eaf1b9 100644 --- a/modules/mlflow/mlflow-fargate/stack.py +++ b/modules/mlflow/mlflow-fargate/stack.py @@ -8,6 +8,7 @@ from aws_cdk import aws_ecr as ecr from aws_cdk import aws_ecs as ecs from aws_cdk import aws_ecs_patterns as ecs_patterns +from aws_cdk import aws_efs as efs from aws_cdk import aws_iam as iam from aws_cdk import aws_s3 as s3 from cdk_nag import AwsSolutionsChecks, NagPackSuppression, NagSuppressions @@ -70,7 +71,6 @@ def __init__( container = task_definition.add_container( "ContainerDef", - # TODO: add ability to pull specific tag image=ecs.ContainerImage.from_ecr_repository( repository=ecr.Repository.from_repository_name( self, @@ -80,18 +80,54 @@ def __init__( ), environment={ "BUCKET": f"s3://{artifacts_bucket_name}", - # TODO: Add persistence - # "HOST": database.db_instance_endpoint_address, - # "PORT": str(port), - # "DATABASE": db_name, - # "USERNAME": username, }, - # secrets={"PASSWORD": ecs.Secret.from_secrets_manager(db_password_secret)}, logging=ecs.LogDriver.aws_logs(stream_prefix="mlflow"), ) port_mapping = ecs.PortMapping(container_port=5000, host_port=5000, protocol=ecs.Protocol.TCP) container.add_port_mappings(port_mapping) + # Add EFS + fs = efs.FileSystem( + self, + "EfsFileSystem", + vpc=vpc, + encrypted=True, + throughput_mode=efs.ThroughputMode.ELASTIC, + performance_mode=efs.PerformanceMode.GENERAL_PURPOSE, + file_system_policy=iam.PolicyDocument( + statements=[ + iam.PolicyStatement( + actions=[ + "elasticfilesystem:ClientMount", + "elasticfilesystem:ClientWrite", + "elasticfilesystem:ClientRootAccess", + ], + principals=[iam.AnyPrincipal()], + resources=["*"], + conditions={"Bool": {"elasticfilesystem:AccessedViaMountTarget": "true"}}, + ), + ] + ), + ) + self.fs = fs + + # Attach and mount volume + task_definition.add_volume( + name="efs-volume", + efs_volume_configuration=ecs.EfsVolumeConfiguration( + file_system_id=fs.file_system_id, + transit_encryption="ENABLED", + ), + ) + container.add_mount_points( + ecs.MountPoint( + container_path="./mlruns", + source_volume="efs-volume", + read_only=False, + ) + ) + + # Create ECS Service service = ecs_patterns.NetworkLoadBalancedFargateService( self, "MlflowLBService", @@ -99,7 +135,11 @@ def __init__( cluster=cluster, task_definition=task_definition, task_subnets=ec2.SubnetSelection(subnets=subnets), + circuit_breaker=ecs.DeploymentCircuitBreaker(rollback=True), ) + self.service = service + + # Enable access logs lb_access_logs_bucket = s3.Bucket( self, "LBAccessLogsBucket", @@ -110,6 +150,10 @@ def __init__( service.load_balancer.log_access_logs(bucket=lb_access_logs_bucket) self.lb_access_logs_bucket = lb_access_logs_bucket + # Allow access to EFS from Fargate service + fs.grant_root_access(service.task_definition.task_role.grant_principal) + fs.connections.allow_default_port_from(service.service.connections) + # Setup security group service.service.connections.security_groups[0].add_ingress_rule( peer=ec2.Peer.ipv4(vpc.vpc_cidr_block), @@ -125,7 +169,6 @@ def __init__( scale_in_cooldown=Duration.seconds(60), scale_out_cooldown=Duration.seconds(60), ) - self.service = service # Add CDK nag solutions checks Aspects.of(self).add(AwsSolutionsChecks()) From c3fceed1a5f3e1826ed5a8bf3413c73c0ea54ab6 Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Fri, 16 Feb 2024 16:26:51 +0000 Subject: [PATCH 15/20] update docs Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-fargate/README.md | 6 +++++- modules/mlflow/mlflow-fargate/app.py | 2 +- .../mlflow-fargate-module-architecture.png | Bin 45054 -> 56069 bytes .../mlflow-fargate-module-architecture.xml | 2 +- modules/mlflow/mlflow-fargate/stack.py | 1 + 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/modules/mlflow/mlflow-fargate/README.md b/modules/mlflow/mlflow-fargate/README.md index 9f1dae68..203281a8 100644 --- a/modules/mlflow/mlflow-fargate/README.md +++ b/modules/mlflow/mlflow-fargate/README.md @@ -4,6 +4,8 @@ This module runs Mlflow on AWS Fargate. +By default, uses EFS for backend storage. + ### Architecture ![Mlflow on AWS Fargate Module Architecture](docs/_static/mlflow-fargate-module-architecture.png "Mlflow on AWS Fargate Module Architecture") @@ -63,6 +65,7 @@ parameters: - `ECSClusterName`: Name of the ECS cluster. - `ServiceName`: Name of the service. - `LoadBalancerDNSName`: Load balancer DNS name. +- `EFSFileSystemId`: EFS file system id. #### Output Example @@ -70,6 +73,7 @@ parameters: { "ECSClusterName": "mlops-mlops-mlflow-mlflow-fargate-EcsCluster97242B84-xxxxxxxxxxxx", "ServiceName": "mlops-mlops-mlflow-mlflow-fargate-MlflowLBServiceEBACC043-xxxxxxxxxxxx", - "LoadBalancerDNSName": "xxxxxxxxxxxx.elb.us-east-1.amazonaws.com" + "LoadBalancerDNSName": "xxxxxxxxxxxx.elb.us-east-1.amazonaws.com", + "EFSFileSystemId": "fs-xxxxxxxxxxx", } ``` diff --git a/modules/mlflow/mlflow-fargate/app.py b/modules/mlflow/mlflow-fargate/app.py index 65af4cad..cd52e6be 100644 --- a/modules/mlflow/mlflow-fargate/app.py +++ b/modules/mlflow/mlflow-fargate/app.py @@ -38,7 +38,6 @@ def _param(name: str) -> str: task_memory_limit_mb = os.getenv(_param("TASK_MEMORY_LIMIT_MB"), DEFAULT_TASK_MEMORY_LIMIT_MB) autoscale_max_capacity = os.getenv(_param("AUTOSCALE_MAX_CAPACITY"), DEFAULT_AUTOSCALE_MAX_CAPACITY) artifacts_bucket_name = os.getenv(_param("ARTIFACTS_BUCKET_NAME")) -# TODO: add persistent backend store if not vpc_id: raise ValueError("Missing input parameter vpc-id") @@ -80,6 +79,7 @@ def _param(name: str) -> str: "ServiceName": stack.service.service.service_name, "LoadBalancerDNSName": stack.service.load_balancer.load_balancer_dns_name, "LoadBalancerAccessLogsBucketArn": stack.lb_access_logs_bucket.bucket_arn, + "EFSFileSystemId": stack.fs.file_system_id, } ), ) diff --git a/modules/mlflow/mlflow-fargate/docs/_static/mlflow-fargate-module-architecture.png b/modules/mlflow/mlflow-fargate/docs/_static/mlflow-fargate-module-architecture.png index 391a4826e24680fe1180838f4e358285be44ebb0..c76a4f48c94f7cd1562093ef5d26c07ff21615bd 100644 GIT binary patch literal 56069 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 \ No newline at end of file +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 \ No newline at end of file diff --git a/modules/mlflow/mlflow-fargate/stack.py b/modules/mlflow/mlflow-fargate/stack.py index 23eaf1b9..7d2d5e51 100644 --- a/modules/mlflow/mlflow-fargate/stack.py +++ b/modules/mlflow/mlflow-fargate/stack.py @@ -80,6 +80,7 @@ def __init__( ), environment={ "BUCKET": f"s3://{artifacts_bucket_name}", + "DUMMY": "DUMMY", }, logging=ecs.LogDriver.aws_logs(stream_prefix="mlflow"), ) From 935a41ac08d2c898ae358ce8ec2cd5715144a062 Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Fri, 16 Feb 2024 16:27:22 +0000 Subject: [PATCH 16/20] fix cdk version Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-fargate/deployspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/mlflow/mlflow-fargate/deployspec.yaml b/modules/mlflow/mlflow-fargate/deployspec.yaml index dc6cd86f..2c6a998e 100644 --- a/modules/mlflow/mlflow-fargate/deployspec.yaml +++ b/modules/mlflow/mlflow-fargate/deployspec.yaml @@ -3,7 +3,7 @@ deploy: phases: install: commands: - - npm install -g aws-cdk@2.126.0 + - npm install -g aws-cdk@2.128.0 - pip install -r requirements.txt pre_build: commands: @@ -20,7 +20,7 @@ destroy: phases: install: commands: - - npm install -g aws-cdk@2.126.0 + - npm install -g aws-cdk@2.128.0 - pip install -r requirements.txt pre_build: commands: From aba7ca0038c86b32a44395f6105000148b528f04 Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Tue, 20 Feb 2024 15:43:48 +0000 Subject: [PATCH 17/20] add lb access logs bucket an prefix parameters Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-fargate/app.py | 6 ++++++ modules/mlflow/mlflow-fargate/stack.py | 24 +++++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/modules/mlflow/mlflow-fargate/app.py b/modules/mlflow/mlflow-fargate/app.py index cd52e6be..0596144b 100644 --- a/modules/mlflow/mlflow-fargate/app.py +++ b/modules/mlflow/mlflow-fargate/app.py @@ -23,6 +23,8 @@ def _param(name: str) -> str: DEFAULT_TASK_CPU_UNITS = 4 * 1024 DEFAULT_TASK_MEMORY_LIMIT_MB = 8 * 1024 DEFAULT_AUTOSCALE_MAX_CAPACITY = 2 +DEFAULT_LB_ACCESS_LOGS_BUCKET_NAME = None +DEFAULT_LB_ACCESS_LOGS_BUCKET_PREFIX = None environment = aws_cdk.Environment( account=os.environ["CDK_DEFAULT_ACCOUNT"], @@ -38,6 +40,8 @@ def _param(name: str) -> str: task_memory_limit_mb = os.getenv(_param("TASK_MEMORY_LIMIT_MB"), DEFAULT_TASK_MEMORY_LIMIT_MB) autoscale_max_capacity = os.getenv(_param("AUTOSCALE_MAX_CAPACITY"), DEFAULT_AUTOSCALE_MAX_CAPACITY) artifacts_bucket_name = os.getenv(_param("ARTIFACTS_BUCKET_NAME")) +lb_access_logs_bucket_name = os.getenv(_param("LB_ACCESS_LOGS_BUCKET_NAME"), DEFAULT_LB_ACCESS_LOGS_BUCKET_NAME) +lb_access_logs_bucket_prefix = os.getenv(_param("LB_ACCESS_LOGS_BUCKET_PREFIX"), DEFAULT_LB_ACCESS_LOGS_BUCKET_PREFIX) if not vpc_id: raise ValueError("Missing input parameter vpc-id") @@ -63,6 +67,8 @@ def _param(name: str) -> str: task_memory_limit_mb=int(task_memory_limit_mb), autoscale_max_capacity=int(autoscale_max_capacity), artifacts_bucket_name=artifacts_bucket_name, + lb_access_logs_bucket_name=lb_access_logs_bucket_name, + lb_access_logs_bucket_prefix=lb_access_logs_bucket_prefix, env=aws_cdk.Environment( account=os.environ["CDK_DEFAULT_ACCOUNT"], region=os.environ["CDK_DEFAULT_REGION"], diff --git a/modules/mlflow/mlflow-fargate/stack.py b/modules/mlflow/mlflow-fargate/stack.py index 7d2d5e51..6c4fe1ae 100644 --- a/modules/mlflow/mlflow-fargate/stack.py +++ b/modules/mlflow/mlflow-fargate/stack.py @@ -30,6 +30,8 @@ def __init__( task_memory_limit_mb: int, autoscale_max_capacity: int, artifacts_bucket_name: str, + lb_access_logs_bucket_name: Optional[str], + lb_access_logs_bucket_prefix: Optional[str], **kwargs: Any, ) -> None: super().__init__(scope, id, **kwargs) @@ -80,7 +82,6 @@ def __init__( ), environment={ "BUCKET": f"s3://{artifacts_bucket_name}", - "DUMMY": "DUMMY", }, logging=ecs.LogDriver.aws_logs(stream_prefix="mlflow"), ) @@ -141,14 +142,19 @@ def __init__( self.service = service # Enable access logs - lb_access_logs_bucket = s3.Bucket( - self, - "LBAccessLogsBucket", - encryption=s3.BucketEncryption.S3_MANAGED, - block_public_access=s3.BlockPublicAccess.BLOCK_ALL, - enforce_ssl=True, - ) - service.load_balancer.log_access_logs(bucket=lb_access_logs_bucket) + if lb_access_logs_bucket_name: + lb_access_logs_bucket = s3.Bucket.from_bucket_name( + self, "LBAccessLogsBucket", bucket_name=lb_access_logs_bucket_name + ) + else: + lb_access_logs_bucket = s3.Bucket( + self, + "LBAccessLogsBucket", + encryption=s3.BucketEncryption.S3_MANAGED, + block_public_access=s3.BlockPublicAccess.BLOCK_ALL, + enforce_ssl=True, + ) + service.load_balancer.log_access_logs(bucket=lb_access_logs_bucket, prefix=lb_access_logs_bucket_prefix) self.lb_access_logs_bucket = lb_access_logs_bucket # Allow access to EFS from Fargate service From 7e98fe792966ef2dde50f84e0253169205f4a73b Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Tue, 20 Feb 2024 15:44:19 +0000 Subject: [PATCH 18/20] update docs Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-fargate/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/mlflow/mlflow-fargate/README.md b/modules/mlflow/mlflow-fargate/README.md index 203281a8..c317e76c 100644 --- a/modules/mlflow/mlflow-fargate/README.md +++ b/modules/mlflow/mlflow-fargate/README.md @@ -27,6 +27,8 @@ By default, uses EFS for backend storage. - `service-name`: Name of the service. - `task-cpu-units`: The number of cpu units used by the Fargate task. - `task-memory-limit-mb`: The amount (in MiB) of memory used by the Fargate task. +- `lb-access-logs-bucket-name`: Name of the bucket to store load balancer access logs +- `lb-access-logs-bucket-prefix`: Prefix for load balancer access logs ### Sample manifest declaration @@ -65,6 +67,7 @@ parameters: - `ECSClusterName`: Name of the ECS cluster. - `ServiceName`: Name of the service. - `LoadBalancerDNSName`: Load balancer DNS name. +- `LoadBalancerAccessLogsBucketArn`: Load balancer access logs bucket arn - `EFSFileSystemId`: EFS file system id. #### Output Example @@ -74,6 +77,7 @@ parameters: "ECSClusterName": "mlops-mlops-mlflow-mlflow-fargate-EcsCluster97242B84-xxxxxxxxxxxx", "ServiceName": "mlops-mlops-mlflow-mlflow-fargate-MlflowLBServiceEBACC043-xxxxxxxxxxxx", "LoadBalancerDNSName": "xxxxxxxxxxxx.elb.us-east-1.amazonaws.com", + "LoadBalancerAccessLogsBucketArn": "arn:aws:s3:::xxxxxxxxxxxx", "EFSFileSystemId": "fs-xxxxxxxxxxx", } ``` From df59d380f98e05b4e34bff3f9c95f4f83a0a9c44 Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Tue, 20 Feb 2024 15:44:36 +0000 Subject: [PATCH 19/20] lock boto3 version Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-image/src/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/mlflow/mlflow-image/src/Dockerfile b/modules/mlflow/mlflow-image/src/Dockerfile index ce392be4..9b08a8b6 100644 --- a/modules/mlflow/mlflow-image/src/Dockerfile +++ b/modules/mlflow/mlflow-image/src/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.10.12 RUN pip install \ mlflow==2.10.2 \ - boto3 && \ + boto3==1.34.45 && \ mkdir /mlflow/ EXPOSE 5000 From c42d360e12fb4e3a46f06318262b77d4dbe6b3ad Mon Sep 17 00:00:00 2001 From: Anton Kukushkin Date: Tue, 20 Feb 2024 15:50:28 +0000 Subject: [PATCH 20/20] fix stack tests Signed-off-by: Anton Kukushkin --- modules/mlflow/mlflow-fargate/tests/test_stack.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/mlflow/mlflow-fargate/tests/test_stack.py b/modules/mlflow/mlflow-fargate/tests/test_stack.py index 4f22980c..88784514 100644 --- a/modules/mlflow/mlflow-fargate/tests/test_stack.py +++ b/modules/mlflow/mlflow-fargate/tests/test_stack.py @@ -47,6 +47,8 @@ def test_synthesize_stack() -> None: task_memory_limit_mb=task_memory_limit_mb, autoscale_max_capacity=autoscale_max_capacity, artifacts_bucket_name=artifacts_bucket_name, + lb_access_logs_bucket_name=None, + lb_access_logs_bucket_prefix=None, env=cdk.Environment( account=os.environ["CDK_DEFAULT_ACCOUNT"], region=os.environ["CDK_DEFAULT_REGION"],