diff --git a/.adr-dir b/.adr-dir new file mode 100644 index 0000000000..c73b64aed2 --- /dev/null +++ b/.adr-dir @@ -0,0 +1 @@ +docs/adr diff --git a/docs/adr/0017-python-anlaytics-processing.md b/docs/adr/0017-python-anlaytics-processing.md new file mode 100644 index 0000000000..fecf812460 --- /dev/null +++ b/docs/adr/0017-python-anlaytics-processing.md @@ -0,0 +1,213 @@ +# Python Analytics Processing + +## Status: Proposed + +## Context + +Our team has developed a natural language processing model for the identifying duplicate goals using python. We need to productionize this model and deploy it to our cloud infrastructure on cloud.gov, which has specific technical limitations. We had two design options to consider that can operate without considerable changes to infrastructure. Option1 - containerizing the Python code and deploying it using cloud.gov's underlining infrastructure technology with the use of Cloud Foundry's Docker support. Option 2(view raw markdown to see proposal): The second option would be to spawn a node child processes to execute the python code on the existing node workers. The first option would require additional effort to set up the Docker environment and ensure that the code runs correctly within the container, and data sources (S3 and Posrgres) are securely connected, but it would also encapsulate the code into its own runtime, making it easier to manage and deploy. The second option would be easier to implement, but it would also introduce additional complexity to the deployment process and increase the boundary between the application and the infrastructure. We have decided to go with the first option, as it is appears as a reliable and scalable option for our production environment and should add unnecessary complexity to the user experience. + +## Alternatives Considered + +We have considered several other alternatives, such as using a serverless architecture, deploying the code as a standalone service, and using Kubernetes. However, we have rejected these alternatives for reasons such as complexity, cost, and mainly lack of compatibility with cloud.gov and its technical limitations. There is also the option of using a different cloud provider which would reduce friction, but we have decided to stick with cloud.gov as the boundary would be too large to change at this point. We have also considered using a different programming language, such as R and node.js, but we have decided to stick with Python as it is the language that the model was developed in and offers the most flexibility in terms of deployment and analytics development options. + +## Stakeholders + +Stakeholders involved in the process include developers/operations teams, data scientists, and business stakeholders. Their requirements include scalability, reliability, ease of deployment, and compliance with cloud.gov technical limitations. + +## Pros and Cons + +### Containerization + +Cloud.gov does support application deploys there are two flavors to run applications outside the buildpack method "Docker as tasks" and" Docker as app" but we have not tested this yet. We will need to have tested the deployment of a containerized Python application using Cloud Foundry's Docker support to correctly weigh the options. "Docker as task" is the most likely option for us to use, static analytic processing can be run as a task and the results can be stored in a database or S3 bucket. These tasks can be run on a schedule or on demand either through the TTA Hub app's node workers or CircleCi's continuous integration with the cloud.gov api. The "Docker as app" option is more suited for a web application that needs to be available to users, which could be an option for the future if near-time analytics are needed to be asynchrously processed via user input and displayed to users back in the TTA Hub app. + + Containerization is a viable option for our use case, but it does have some drawbacks. + + +**Pros** + +* Encapsulates the code into its own runtime, making it easier to manage and deploy. +* Separates the code from other components of the system. +* Well-established practice for deploying applications in a cloud environment. +* Widely supported by most cloud providers. +* Reliable and scalable option for our production environment. +* Can build container images and run containers on local workstation. +Fine-grained control over compilation and root filesystem. +* Docker containers are portable and can be easily moved between environments. + +**Cons** + +* Requires additional effort to set up the Docker environment and ensure that the code runs correctly within the container. +* May require additional resources and expertise. +* Potential complexity introduced to the deployment process. +* Docker environment may not be properly configured, which could lead to security vulnerabilities or other issues. +* Added responsibility for all security updates and bug fixes. +* More compliance responsibility means more work. +* Increases boundary between the application and the infrastructure. +* May need additional ATO for Docker container. +* Non-standard deployment process for Docker containers (cloud.gov documents that "No Docker components are involved in this process - your applications are run under the garden-runc runtime") + +### Dockerfile +Dockerfile sets up a containerized environment with all the necessary dependencies for running a Python application. This example uses multi-stage builds to separate the build and install stages from the final stage. The final stage uses the distroless image, which is a minimal image that only contains the Python runtime and the application code, providing a secure and lightweight environment for running Python applications. This image is based on the Debian 11 (bullseye) image, which is the latest version of Debian. The `python_build` stage installs the necessary dependencies for building the Python application, such as gcc and python3-dev. The `python_install` stage installs the Python dependencies using pip. The final stage copies the application code and the Python dependencies from the previous stage and sets the environment variables. The final stage also sets the working directory and the command to run the application. + +```Dockerfile +DOCKERFILE + +ARG PYTHON_VERSION=3.9 + +# Python build stage +FROM python:${PYTHON_VERSION}-slim-bullseye as python_build +WORKDIR /opt/venv +RUN apt-get update && apt-get install -y gcc python3-dev --no-install-recommends +COPY ./ops/dev-stack/py_app/src/requirements.txt . +ENV VIRTUAL_ENV=/opt/venv +ENV PATH="$VIRTUAL_ENV/bin:$PATH" +RUN python3 -m venv $VIRTUAL_ENV && \ + $VIRTUAL_ENV/bin/python3 -m pip install -U --upgrade pip && \ + $VIRTUAL_ENV/bin/pip install --upgrade pip setuptools wheel psutil + +# Python install stage +FROM python_build as python_install +# Use buildkit to cache pip dependencies +# https://pythonspeed.com/articles/docker-cache-pip-downloads/ +RUN --mount=type=cache,target=/root/.cache \ + $VIRTUAL_ENV/bin/python3 -m pip install -U --no-cache-dir -r requirements.txt --prefer-binary -v + +# Final stage +# FROM gcr.io/distroless/python3-debian11:debug +FROM gcr.io/distroless/python3-debian11 +ENV PYTHON_VERSION=3.9 + +COPY ./ops/dev-stack/py_app/src /opt/venv +COPY --from=python_install /opt/venv/ /opt/venv/ +COPY --from=python_install /usr/lib/ /usr/lib/ + +ENV SPARK_HOME=/opt +ENV PATH=$PATH:/opt/bin +ENV PATH /opt/venv/bin:$PATH + +WORKDIR /opt/venv +CMD ["python3", "/app.py"] +``` + +### CI/CD +* Create a CircleCI Docker build of models REPO/IMAGE:TAG +* Deploy Data Analytics Model Container cloud.gov container registry with CircleCI +* Ensure that container images dependencies are up to date with Snyk + +Example of a CircleCI config.yml file for building and deploying a Docker image to cloud.gov + +```yaml +... +cf_dam_deploy: + description: "Login to cloud foundry space with service account credentials + and push application using deployment configuration file." + parameters: + app_name: + description: "Name of Cloud Foundry cloud.gov application; must match + application name specified in manifest" + type: string + auth_client_id: + description: "Name of CircleCi project environment variable that + holds authentication client id, a required application variable" + type: env_var_name + auth_client_secret: + description: "Name of CircleCi project environment variable that + holds authentication client secret, a required application variable" + type: env_var_name + cloudgov_username: + description: "Name of CircleCi project environment variable that + holds deployer username for cloudgov space" + type: env_var_name + cloudgov_password: + description: "Name of CircleCi project environment variable that + holds deployer password for cloudgov space" + type: env_var_name + cloudgov_space: + description: "Name of CircleCi project environment variable that + holds name of cloudgov space to target for application deployment" + type: env_var_name + deploy_config_file: + description: "Path to deployment configuration file" + type: string + session_secret: + description: "Name of CircleCi project environment variable that + holds session secret, a required application variable" + type: env_var_name + new_relic_license: + description: "Name of CircleCI project environment variable that + holds the New Relic License key, a required application variable" + type: env_var_name + steps: + - run: + name: Login with service account + command: | + cf login -a << pipeline.parameters.cg_api >> \ + -u ${<< parameters.cloudgov_username >>} \ + -p ${<< parameters.cloudgov_password >>} \ + -o << pipeline.parameters.cg_org >> \ + -s ${<< parameters.cloudgov_space >>} + - run: + name: Push Data Analytics Modeling Application with deployment vars + command: | + cf push --docker-image data-models REPO/IMAGE:TAG +--strategy rolling \ #not sure if strategy is needed for docker + --vars-file << parameters.deploy_config_file >> \ + --var AUTH_CLIENT_ID=${<< parameters.auth_client_id >>} \ + --var AUTH_CLIENT_SECRET=${<< parameters.auth_client_secret >>} \ + --var NEW_RELIC_LICENSE_KEY=${<< parameters.new_relic_license >>} \ + --var SESSION_SECRET=${<< parameters.session_secret >>} + ... +``` +### New Relic +* New Relic is a monitoring tool that can be used to monitor the performance of the application. It can be used to monitor the performance +* + + + +### Risks and Mitigation + +Risks associated with containerization could include compatibility issues with cloud.gov's technical limitations, difficulty in managing dependencies, and potential security vulnerabilities. Cost controls are not really transparent in using this method, as there is flat rate purchasing of resources. To mitigate these risks, we will conduct thorough testing and monitoring, as well as implement contingency plans for addressing potential issues. We will also consider the costs and resource requirements associated with each approach, including infrastructure costs, developer resources, and maintenance requirements. We aim to choose an approach that is cost-effective and feasible within our available resources. + + + +## Timeline + +We aim to complete the productionize process within four weeks, with key milestones including: + +* Setting up the development environment +* Testing and validation ( unit tests, integration tests, etc.) +* Deployment to the production environment +* Monitoring and maintenance + +## Cost and Resource Considerations + +We will consider the costs and resource requirements associated with each approach, including infrastructure costs, developer resources, and maintenance requirements. We aim to choose an approach that is cost-effective and feasible within our available resources. + +## Conclusion + +After documenting the pros and cons of each approach, we will carefully evaluate the trade-offs and make a decision that best suits our needs. We will consider factors such as resource availability, expertise, reliability, scalability, and ease of deployment. We will also ensure that our chosen approach complies with cloud.gov's technical limitations. + +To comply with cloud.gov's technical limitations, we have updated the proposal to reflect this requirement. We will consider these limitations when evaluating the pros and cons of each approach and when making our final decision. We will also ensure that our approach is compatible with cloud.gov. \ No newline at end of file diff --git a/docs/boundary_diagram.md b/docs/boundary_diagram.md index 6b1cf1c1a6..967a55006b 100644 --- a/docs/boundary_diagram.md +++ b/docs/boundary_diagram.md @@ -1,7 +1,7 @@ System Boundary Diagram ======================= -![rendered boundary diagram](http://www.plantuml.com/plantuml/png/jLVTRXkz4RtNKqnz4JX0PA5sUj6Y229BigF2QXIlabmaWS3bZbQiAN933gK8HTwzdDMANdlj0Cs73qYitH7ddCCXvqTlFGb7OQDxVr56wb0Wh8ag_u_1m8dTMQbe7VBWqKbh20sTIRiPYC8fOROwL64mUjsVQs7eu-nce0MXfsXQZk9geGowiugnIP56M2o6y2xaaDjW2k7si5MuwytHUMjEg_h3e7u59yU57oFlINmV6a1Jz5a6s1NGEc6Tl3pw5xX5RIDAotzyj9sqZDi8PHsazHw4AU1wCOjObusoKx7ppFivWskx7MaRYfENyDyUG5eXsLZQRLnMFvx5vsODGBRtX9lbv0SnqbBelBqCj1L5FnTQ61crDKJ3cykeuA5tObcAIZs06CwdlrZjRA0w4lzyeSKUdNy3zww7BptprZk3AzwJsmCviLef2HxTLabiyEzZGZPdhL8wB1G9KjQaRzFM0TBPd-vskoYyQkj8b-0pvZ2iAgra3SHg_cqB_5Vs2YO_AeS-7i0j2adypLZvAW8SZafQhL6Ihw4AGU8CPa6JgZI2CfuugXvSC4QPCcwYeygwEtIDi2IdDZwdw39epUzpT2lhDf0BULSwtZemMYsBbK3bR2JtJnDBBJQ2ByoLqWXUYbeX7-NXBCHc-8cVfeRG6Q51tAk1o7llKcT2gFWU-IxHEAyZN-Iis7eg7MOVRc0S1UR2u_72tOupVcpSF3GPtiJOh5GPNE9k9zspZFwYWSuk80_o3kaa6VWf8-lGW-Ux8s75KH3U8ps_1xvfd8ct_DdPmKGB7v64NDUc3m43WbsrZ-S_DlT0zd1CrZH4Ftly_sVla3kdxzvbososc2ypTDiwRoOReJGSNXawhe2MZSCHySirJ9qrS-ie0X_1iaaMFVYZthCofKR8L6dWlVbgPbT3s2e12xN1_gNmMF2b0-jWFlq4KhZYQroLsU1alPTh9mmXrdjlLwZIYwKNmZE545cJVwrYK5eRYPSaokL6657M-xgkZR0G9SmEHjROUeqKF_z_akYhzwIalwSO7arCmCcU35sA5HP7cYiiReUpULzKbRDRBE1fNeCxXrf9pb7SmIq_7o-pCi463pDh55d7gSY4SKcxHxG03Zk7wf3RN3tW1MXRbj6LCUj-2DUJnM3-Fej_FYu6uydDP361qzUlBrv6TCPEjQw5xB2oZZmGQjmWkJt3aQpQZcrHJnxPg4b5X6ZtppgrZ-2JmbsgvdMsSPeSVLEdVT8tGrDuCDRqvzDf0k7YrdNlwkq0iEPdlEyxuy4vrVDV1lqLU9GSZHtctdADyT27prcMWcei8JXLhWci0QtCNM-eDKYcbLpFeY2ib2GiG4GWDDBjAtxRALh3uYQ3lvwTGp0QlGTBQtGxvH4OjpZheIdgHzvEgdhjwQMmEyELkIbmH4AkDsZ8FzZNWpkGGj7ju9MpFnJtC_mUq9F_Wy5XjWv9khj_1bkxtQOEo9tCrm5gKJ0ydFxz6TzkaJQMr8hL1j_CCvnkSFwtnodPb6I7v1Hk4MvHgagXeUvrQqll08X0w-Z5BRbXPT_tmsW-xzxlnYbgLQizg88zQT-DQAAyL0z8KmztA0e_s3d5hApO8u53QLrnsBN_hfDx2jdl8xG5-8kdt8-p0ljkAydJUMMLeQwAtuNf23bE0cZYpGpekRD39PpOu76QhPSy9o415FWa0Bo0K4LHpNmIkeXFwnbPwmfbQiUK1-13xgLJrQ7Sv4WxH9EcqpPsweJ7ciwbujWDHzF1Q0npLQ5M1hjjyF3pZ9kkeZYODPDAdyFgU4hcGQv0u3xa5LaUqny2IEMaHgcOgtuSJH-rKHv6osOEpD8FWqu_JKhc8TVAhsjYXefVW8_lf-VdJLebrMrokzd4XMjhxo2bzL4HaELn7WxHg58qQh1xa4U_bSPOQRj_C4F7Vpv87j1NGJTm4JWYtOZzSloeEHsijyr0Lnk-f1NVeE_wR-1BSoU-zTwYAS96_my0) +![rendered boundary diagram](https://www.plantuml.com/plantuml/png/jLTVRniv3d_Ff-3beN21ECQbtOSx71Rhn6wQGtBrPjpsONTXQ3JqM1jPcagKNUFGxtxY_54c7IV05TY7n1gE-EEF54bnVl4a78MjFlcRCbA70c53LFb_JYPExCzBHPkG1uzEMaDew5pQxKGKJWcpqw4AawktuuKMXZxUtKwq8FGKHQkhk5ieW-wyOcnIf16Moocy3pdaDfX2k0Fi5EvF5kYyDQEg_Z6ep-2qs_ZHepkDprC3Q8enqm2x1jeahDFNdNw1EzGselJqPvtij2VSHIZh8Ett84m1rykxY7NPCXk9lMUxdpEujhihRKDn-Xh-UmAGTaWMbdONjzNByxXkzm1a1q-uNSs_4YEjXCxxsq1RKOnpeOMHpAqnDBszZ0oUQYShAr6f9n3JnSqBkvqDL4VYNw-qEA3pFyEZQlFIiztQCtZ7Fha3a1FhjPBWqUsKn1R_CIPaS-OgfSD2aI1bJNgRN0T8Pp_Qx_UHUDNdaPBWC-OmhIgjP0t4RFvZ2_ntTWRphvL3xy_W7eKaVZDJlee0pL59gpLAyZLK8KYSmrtGf2gDe8mdZge75unHfenE35XPzu2k9PRerCBd65q6hVbvWMvjtHPo8HzAnwu3ep6jcWbKpaRZ_hXXOOG-a99-HS8_S2XR0IoI6YpZgzhKWJRMdC7CoaYC9TSOGGHXOoc1WGrK1V9734ejje8pz9tI25wAEYISEytXY-tq4wzk3A6BU20UtOVewADANNgXuiGzOcYMrqTTvCpRUYeTPh_UmaoGo8N7BiFlPnalMpKFRKjf3zMiLHbSijslycSj-ZSjTFO6yX03HQT9mAkCh4CFdWH7USrH4DuZFUy3fpQN_Zt_3ZoOQ-4Zaf2RMlHhm81Wr_ry-DFc7iWsUM5DQ-ZR2VzzEscATVJ-VRRAxfQBLOPkLnVgV2kKXkQHeUCEwCk0DX6_tC2DisPX7KNW3YoRPr63VsgVbIarGgPA0n_Cx-RktHHsIi1IRN5yAJmMdELW7InkFe4KhlWzxifiSBBsvTe9GuYrx_sMM7gnya9uDY54rXPyh_kKraR3AnBbQYkCA6k_hciXB4K9TusGEVOUeubl_x-HbFsF9gQTY0rmTqc2eOar5fsPTrZSJ-yMOr5LpkwmWEDs3UuTQYMvAU0UxddT9RComGOFTzOeievhdmt6BVrBgGVGU0vLqqouNS4hqBOieofZrXSmNC-NayM7BFxxk9pCvhVpvHn6Rz--UHtH6Jirrnwomyewya2eSOla3Wn7ikehzaaTFRAhjXGHejmVKneSmIU5-tHzrDN6PTBffglzg6w6fl1Wh1alRcuI2FUueVgGxm20EJ-Z_NWLDyhf0danw6VWKN8qzfXxRiRaOGoViom5rLX2SAhS45W3Mfc7awdM8Dciv7xceS1AIS82X4D08zsXugUzeWqiRpFu-_a51AFHUx2qGRTN7e5nY_CJD4MzP7TIr-kFIuNT6-xCROCZ4dApHKF-YLzFSY25OZWoLCv-LUZ1uPU0dloF1oTP6nX9kVjNMElVx-a6v9lCrm7gcM1u6FtZ6TrXapQMr9hP1jyEK5nkSF5JexsQFT4VNFVAA5A2i3UBoUxw0Txpme6bYBNTtY4vXJk4UvIgKcXecCal0eX0cwZ5jtth4Ej-c4RnnR1qMgKTEhK-W2fOamx3mngOBF8ZWP3M-B15S68F9FBsJFuiSZdwwUsRYqx_UxqqOpGJpoCkZ1IUU-R4QGzUMeUl1v2Ngbz0Jy4JQ3ihzB3RddeaiWapnJOnT2WAFzaxHTXq8uy43YEneiaR_qKdzPGqFuwG2BJU3TMxQOjrTvK4D4LbbQ4XYn-56H3fPYaqiRO3UfwDe19ERB7x0ActF4SX0HHu501UGQWYgNR22qF4utokh7M5ChLYwYJWG-wbKrNJi7EaFQ997nHzx3HBTBSYPrqtJrpTJAvci50LQcLmE4WqNzGyjYZYqRQPzSOSLcUrHXw52woppYko_6Nr540g9pLAnRRgvTNDTuC8Z_DbEqbdwLjkC94a9ekGQ-KtjM66YY_0n-VHHIh5Rc_VkDrk5S76sWT8ZR5Z16J11GDDDAeKZHhi4UIxpziPLjeUddo5M0C-I8dUhuDku1Wu8jsAmshstVNUIE_RaRWM_9Ps_05ZD_uPVcjpue-JNz0KOQl_1m00) UML Source ---------- @@ -20,6 +20,7 @@ Boundary(aws, "AWS GovCloud") { Boundary(atob, "Accreditation Boundary") { Container(www_app, "<&layers> TTA Hub Web Application", "NodeJS, Express, React", "Displays and collects TTA data. Multiple instances running") Container(worker_app, "TTA Hub Worker Application", "NodeJS, Bull", "Perform background work and data processing") + Container(analytics_task, "Analytic Processing Task", "Python, Docker", "Generate data analytics outputs") Container(clamav, "File scanning API", "ClamAV", "Internal application for scanning user uploads") ContainerDb(www_db, "PostgreSQL Database", "AWS RDS", "Contains content and configuration for the TTA Hub") ContainerDb(www_s3, "AWS S3 bucket", "AWS S3", "Stores static file assets") @@ -53,9 +54,13 @@ Rel(aws_alb, cloudgov_router, "proxies requests", "https GET/POST/PUT/DELETE, se Rel(cloudgov_router, www_app, "proxies requests", "https GET/POST/PUT/DELETE, secure websockets - WSS (443)") Rel(worker_app, clamav, "scans files", "https POST (9443)") Rel(worker_app, HHS_SMTP_Server, "notifies users", "port 25") +Rel_D(worker_app, analytics_task, "initiate cloud.gov container tasks", "https GET (443)") Rel(www_app, HSES, "retrieve Recipient data", "https GET (443)") Rel(www_app, HSES, "authenticates user", "OAuth2") Rel(personnel, HSES, "verify identity", "https GET/POST (443)") +Rel(www_db, analytics_task, "consume raw tables", "jdbc(5432)") +Rel(analytics_task, www_db, "output analytics (ie.sentiment score)", "jdbc(5432)") +BiRel(www_s3, analytics_task, "output static analytics reports", "https GET (443)") BiRel(www_app, www_db, "reads/writes dataset records", "psql") BiRel(worker_app, www_db, "reads/writes dataset records", "psql") BiRel(www_app, www_s3, "reads/writes data content", "vpc endpoint") @@ -79,10 +84,10 @@ Lay_R(HSES, aws) Instructions ------------ -1. [Edit this diagram with plantuml.com](http://www.plantuml.com/plantuml/umla/jLVTRXkz4RtNKqnz4JX0PA5sUj6Y229BigF2QXIlabmaWS3bZbQiAN933gK8HTwzdDMANdlj0Cs73qYitH7ddCCXvqTlFGb7OQDxVr56wb0Wh8ag_u_1m8dTMQbe7VBWqKbh20sTIRiPYC8fOROwL64mUjsVQs7eu-nce0MXfsXQZk9geGowiugnIP56M2o6y2xaaDjW2k7si5MuwytHUMjEg_h3e7u59yU57oFlINmV6a1Jz5a6s1NGEc6Tl3pw5xX5RIDAotzyj9sqZDi8PHsazHw4AU1wCOjObusoKx7ppFivWskx7MaRYfENyDyUG5eXsLZQRLnMFvx5vsODGBRtX9lbv0SnqbBelBqCj1L5FnTQ61crDKJ3cykeuA5tObcAIZs06CwdlrZjRA0w4lzyeSKUdNy3zww7BptprZk3AzwJsmCviLef2HxTLabiyEzZGZPdhL8wB1G9KjQaRzFM0TBPd-vskoYyQkj8b-0pvZ2iAgra3SHg_cqB_5Vs2YO_AeS-7i0j2adypLZvAW8SZafQhL6Ihw4AGU8CPa6JgZI2CfuugXvSC4QPCcwYeygwEtIDi2IdDZwdw39epUzpT2lhDf0BULSwtZemMYsBbK3bR2JtJnDBBJQ2ByoLqWXUYbeX7-NXBCHc-8cVfeRG6Q51tAk1o7llKcT2gFWU-IxHEAyZN-Iis7eg7MOVRc0S1UR2u_72tOupVcpSF3GPtiJOh5GPNE9k9zspZFwYWSuk80_o3kaa6VWf8-lGW-Ux8s75KH3U8ps_1xvfd8ct_DdPmKGB7v64NDUc3m43WbsrZ-S_DlT0zd1CrZH4Ftly_sVla3kdxzvbososc2ypTDiwRoOReJGSNXawhe2MZSCHySirJ9qrS-ie0X_1iaaMFVYZthCofKR8L6dWlVbgPbT3s2e12xN1_gNmMF2b0-jWFlq4KhZYQroLsU1alPTh9mmXrdjlLwZIYwKNmZE545cJVwrYK5eRYPSaokL6657M-xgkZR0G9SmEHjROUeqKF_z_akYhzwIalwSO7arCmCcU35sA5HP7cYiiReUpULzKbRDRBE1fNeCxXrf9pb7SmIq_7o-pCi463pDh55d7gSY4SKcxHxG03Zk7wf3RN3tW1MXRbj6LCUj-2DUJnM3-Fej_FYu6uydDP361qzUlBrv6TCPEjQw5xB2oZZmGQjmWkJt3aQpQZcrHJnxPg4b5X6ZtppgrZ-2JmbsgvdMsSPeSVLEdVT8tGrDuCDRqvzDf0k7YrdNlwkq0iEPdlEyxuy4vrVDV1lqLU9GSZHtctdADyT27prcMWcei8JXLhWci0QtCNM-eDKYcbLpFeY2ib2GiG4GWDDBjAtxRALh3uYQ3lvwTGp0QlGTBQtGxvH4OjpZheIdgHzvEgdhjwQMmEyELkIbmH4AkDsZ8FzZNWpkGGj7ju9MpFnJtC_mUq9F_Wy5XjWv9khj_1bkxtQOEo9tCrm5gKJ0ydFxz6TzkaJQMr8hL1j_CCvnkSFwtnodPb6I7v1Hk4MvHgagXeUvrQqll08X0w-Z5BRbXPT_tmsW-xzxlnYbgLQizg88zQT-DQAAyL0z8KmztA0e_s3d5hApO8u53QLrnsBN_hfDx2jdl8xG5-8kdt8-p0ljkAydJUMMLeQwAtuNf23bE0cZYpGpekRD39PpOu76QhPSy9o415FWa0Bo0K4LHpNmIkeXFwnbPwmfbQiUK1-13xgLJrQ7Sv4WxH9EcqpPsweJ7ciwbujWDHzF1Q0npLQ5M1hjjyF3pZ9kkeZYODPDAdyFgU4hcGQv0u3xa5LaUqny2IEMaHgcOgtuSJH-rKHv6osOEpD8FWqu_JKhc8TVAhsjYXefVW8_lf-VdJLebrMrokzd4XMjhxo2bzL4HaELn7WxHg58qQh1xa4U_bSPOQRj_C4F7Vpv87j1NGJTm4JWYtOZzSloeEHsijyr0Lnk-f1NVeE_wR-1BSoU-zTwYAS96_my0) +1. [Edit this diagram with plantuml.com](https://www.plantuml.com/plantuml/png/jLTVRniv3d_Ff-3beN21ECQbtOSx71Rhn6wQGtBrPjpsONTXQ3JqM1jPcagKNUFGxtxY_54c7IV05TY7n1gE-EEF54bnVl4a78MjFlcRCbA70c53LFb_JYPExCzBHPkG1uzEMaDew5pQxKGKJWcpqw4AawktuuKMXZxUtKwq8FGKHQkhk5ieW-wyOcnIf16Moocy3pdaDfX2k0Fi5EvF5kYyDQEg_Z6ep-2qs_ZHepkDprC3Q8enqm2x1jeahDFNdNw1EzGselJqPvtij2VSHIZh8Ett84m1rykxY7NPCXk9lMUxdpEujhihRKDn-Xh-UmAGTaWMbdONjzNByxXkzm1a1q-uNSs_4YEjXCxxsq1RKOnpeOMHpAqnDBszZ0oUQYShAr6f9n3JnSqBkvqDL4VYNw-qEA3pFyEZQlFIiztQCtZ7Fha3a1FhjPBWqUsKn1R_CIPaS-OgfSD2aI1bJNgRN0T8Pp_Qx_UHUDNdaPBWC-OmhIgjP0t4RFvZ2_ntTWRphvL3xy_W7eKaVZDJlee0pL59gpLAyZLK8KYSmrtGf2gDe8mdZge75unHfenE35XPzu2k9PRerCBd65q6hVbvWMvjtHPo8HzAnwu3ep6jcWbKpaRZ_hXXOOG-a99-HS8_S2XR0IoI6YpZgzhKWJRMdC7CoaYC9TSOGGHXOoc1WGrK1V9734ejje8pz9tI25wAEYISEytXY-tq4wzk3A6BU20UtOVewADANNgXuiGzOcYMrqTTvCpRUYeTPh_UmaoGo8N7BiFlPnalMpKFRKjf3zMiLHbSijslycSj-ZSjTFO6yX03HQT9mAkCh4CFdWH7USrH4DuZFUy3fpQN_Zt_3ZoOQ-4Zaf2RMlHhm81Wr_ry-DFc7iWsUM5DQ-ZR2VzzEscATVJ-VRRAxfQBLOPkLnVgV2kKXkQHeUCEwCk0DX6_tC2DisPX7KNW3YoRPr63VsgVbIarGgPA0n_Cx-RktHHsIi1IRN5yAJmMdELW7InkFe4KhlWzxifiSBBsvTe9GuYrx_sMM7gnya9uDY54rXPyh_kKraR3AnBbQYkCA6k_hciXB4K9TusGEVOUeubl_x-HbFsF9gQTY0rmTqc2eOar5fsPTrZSJ-yMOr5LpkwmWEDs3UuTQYMvAU0UxddT9RComGOFTzOeievhdmt6BVrBgGVGU0vLqqouNS4hqBOieofZrXSmNC-NayM7BFxxk9pCvhVpvHn6Rz--UHtH6Jirrnwomyewya2eSOla3Wn7ikehzaaTFRAhjXGHejmVKneSmIU5-tHzrDN6PTBffglzg6w6fl1Wh1alRcuI2FUueVgGxm20EJ-Z_NWLDyhf0danw6VWKN8qzfXxRiRaOGoViom5rLX2SAhS45W3Mfc7awdM8Dciv7xceS1AIS82X4D08zsXugUzeWqiRpFu-_a51AFHUx2qGRTN7e5nY_CJD4MzP7TIr-kFIuNT6-xCROCZ4dApHKF-YLzFSY25OZWoLCv-LUZ1uPU0dloF1oTP6nX9kVjNMElVx-a6v9lCrm7gcM1u6FtZ6TrXapQMr9hP1jyEK5nkSF5JexsQFT4VNFVAA5A2i3UBoUxw0Txpme6bYBNTtY4vXJk4UvIgKcXecCal0eX0cwZ5jtth4Ej-c4RnnR1qMgKTEhK-W2fOamx3mngOBF8ZWP3M-B15S68F9FBsJFuiSZdwwUsRYqx_UxqqOpGJpoCkZ1IUU-R4QGzUMeUl1v2Ngbz0Jy4JQ3ihzB3RddeaiWapnJOnT2WAFzaxHTXq8uy43YEneiaR_qKdzPGqFuwG2BJU3TMxQOjrTvK4D4LbbQ4XYn-56H3fPYaqiRO3UfwDe19ERB7x0ActF4SX0HHu501UGQWYgNR22qF4utokh7M5ChLYwYJWG-wbKrNJi7EaFQ997nHzx3HBTBSYPrqtJrpTJAvci50LQcLmE4WqNzGyjYZYqRQPzSOSLcUrHXw52woppYko_6Nr540g9pLAnRRgvTNDTuC8Z_DbEqbdwLjkC94a9ekGQ-KtjM66YY_0n-VHHIh5Rc_VkDrk5S76sWT8ZR5Z16J11GDDDAeKZHhi4UIxpziPLjeUddo5M0C-I8dUhuDku1Wu8jsAmshstVNUIE_RaRWM_9Ps_05ZD_uPVcjpue-JNz0KOQl_1m00) 2. Copy and paste the final UML into the UML Source section 3. Update the img src and edit link target to the current values ### Notes -* See the help docs for [C4 variant of PlantUML](https://github.com/RicardoNiepel/C4-PlantUML) for syntax help. +* See the help docs for [C4 variant of PlantUML](https://github.com/plantuml-stdlib/C4-PlantUML) for syntax help.