diff --git a/content/rendering-with-batch/_index.md b/content/rendering-with-batch/_index.md index 974dc64b..9e01f304 100644 --- a/content/rendering-with-batch/_index.md +++ b/content/rendering-with-batch/_index.md @@ -7,11 +7,11 @@ pre: "9. " --- {{% notice info %}} -The estimated completion time of this lab is **60 minutes**. Please note that rendering the animation presented below can incur in costs up to **$15**. +The estimated completion time of this lab is **90 minutes**. Please note that rendering the animation presented below can incur in costs up to **$15**. {{% /notice %}} ## Overview -In this workshop you will learn to submit jobs with [AWS Batch](https://aws.amazon.com/batch/) following Spot best practices to [render](https://en.wikipedia.org/wiki/Rendering_(computer_graphics)) a [Blender](https://www.blender.org/) file in a distributed way. You will be creating a docker container and publishing it in Amazon Elastic Container Registry (ECR). Then you will use that container in AWS Batch using a mix of EC2 On-Demand and Spot instances. [Spot instances](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-spot-instances.html) are EC2 spare capacity offered at steep discounts compared to On-Demand instances and are a cost-effective choice for applications that can be interrupted, what makes them well-suited for the batch processing that we will run. After going through all the sections, you will have the following pipeline created: +In this workshop you will learn to submit jobs with [AWS Batch](https://aws.amazon.com/batch/) following Spot best practices to [render](https://en.wikipedia.org/wiki/Rendering_(computer_graphics)) a [Blender](https://www.blender.org/) file in a distributed way. You will be creating a docker container and publishing it in Amazon Elastic Container Registry (ECR). Then you will use that container in AWS Batch using a mix of EC2 On-Demand and Spot instances. [Spot instances](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-spot-instances.html) are EC2 spare capacity offered at steep discounts compared to On-Demand instances and are a cost-effective choice for applications that can be interrupted, what makes them well-suited for the batch processing that we will run. After going through all the sections, you will have the following pipeline created, orchestrated by AWS Step Functions: 1. A python script downloads the Blender file from S3 to extract the number of frames from the Blender project. 2. The script submits a batch job using an `array job` with as many tasks as number of frames. It also submits a single stitching job using [FFmpeg](https://ffmpeg.org/) to create a final video file. diff --git a/content/rendering-with-batch/batch/batch.files/job_submission.py b/content/rendering-with-batch/batch/batch.files/job_submission.py deleted file mode 100644 index 7a000dc6..00000000 --- a/content/rendering-with-batch/batch/batch.files/job_submission.py +++ /dev/null @@ -1,257 +0,0 @@ -#!/usr/bin/env python3 - -import sys, getopt, math, boto3, argparse, json, struct, gzip - - -INPUT_URI = '' # S3 URI where the blender file is located -OUTPUT_URI = '' # S3 URI where to upload the rendered file -F_PER_JOB = 0 # Number of frames that each job has to render -JOB_NAME = '' # Name of the job that will be submitted to Batch -JOB_QUEUE = '' # Queue to which the job is submitted -JOB_DEFINITION = '' # Job definition used by the submitted job -FILE_NAME = '' # Name of the blender file - - -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# Extract from Blender's script library included in scripts/modules. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# ##### END GPL LICENSE BLOCK ##### -def read_blend_rend_chunk(path): - """Extract from Blender's script library included in scripts/modules. - Reads the header of a blend file and returns scenes' information. - - Keyword arguments: - path -- path where the blend file is located - """ - - blendfile = open(path, "rb") - - head = blendfile.read(7) - - if head[0:2] == b'\x1f\x8b': # gzip magic - blendfile.seek(0) - blendfile = gzip.open(blendfile, "rb") - head = blendfile.read(7) - - if head != b'BLENDER': - print("not a blend file:", path) - blendfile.close() - return [] - - is_64_bit = (blendfile.read(1) == b'-') - - # true for PPC, false for X86 - is_big_endian = (blendfile.read(1) == b'V') - - # Now read the bhead chunk!!! - blendfile.read(3) # skip the version - - scenes = [] - - sizeof_bhead = 24 if is_64_bit else 20 - - while blendfile.read(4) == b'REND': - sizeof_bhead_left = sizeof_bhead - 4 - - struct.unpack('>i' if is_big_endian else '2i' if is_big_endian else '<2i', blendfile.read(8)) - - scene_name = blendfile.read(64) - - scene_name = scene_name[:scene_name.index(b'\0')] - - try: - scene_name = str(scene_name, "utf8") - except TypeError: - pass - - scenes.append((start_frame, end_frame, scene_name)) - - blendfile.close() - - return scenes - -def parse_arguments(): - """Parses the command line arguments and stores the values in global variables. - """ - - parser = argparse.ArgumentParser(description='Submit an AWS Batch job that will render a Blender file in a distributed fashion.') - parser.add_argument('-i', dest='input_uri', type=str, required=True, help='S3 URI where the blender file is located') - parser.add_argument('-o', dest='output_uri', type=str, required=True, help='S3 URI where to upload the rendered file') - parser.add_argument('-f', dest='f_per_job', type=int, required=True, help='Number of frames that each job has to render') - parser.add_argument('-n', dest='job_name', type=str, required=True, help='Name of the job that will be submitted to Batch') - parser.add_argument('-q', dest='job_queue', type=str, required=True, help='Queue to which the job is submitted') - parser.add_argument('-d', dest='job_definition', type=str, required=True, help='Job definition used by the submitted job') - args = parser.parse_args() - - if args.f_per_job < 1: - print('F_PER_JOB must be a positive integer') - sys.exit(2) - - global INPUT_URI - INPUT_URI = args.input_uri - global OUTPUT_URI - OUTPUT_URI = args.output_uri - global F_PER_JOB - F_PER_JOB = args.f_per_job - global JOB_NAME - JOB_NAME = args.job_name - global JOB_QUEUE - JOB_QUEUE = args.job_queue - global JOB_DEFINITION - JOB_DEFINITION = args.job_definition - global FILE_NAME - FILE_NAME = INPUT_URI.split('/')[-1] - -def download_blender_file_from_s3(): - """Downloads the blend file from S3 and stores it locally. - """ - - bucket = INPUT_URI.split('s3://')[1].split('/')[0] - - s3 = boto3.resource('s3') - s3.meta.client.download_file(bucket, FILE_NAME, './{}'.format(FILE_NAME)) - -def get_number_of_frames(path): - """Reads the header of the blend file and calculates - the number of frames it has. - - Keyword arguments: - path -- path where the blend file is located - """ - - try: - frame_start, frame_end, scene = read_blend_rend_chunk(path)[0] - except FileNotFoundError as e: - print(e.args[1]) - sys.exit(2) - else: - return int(frame_end - frame_start + 1) - -def calculate_array_job_size(): - """Calculates the size of the job array - based on the number of frames of the blender file - and the number of frames that each job has to render - """ - - # Get the scene's number of frames by reading the header of the blend file - n_frames = get_number_of_frames('./{}'.format(FILE_NAME)) - - # Adjust the number of frames per job, if that value is greater than the number of frames in the scene - global F_PER_JOB - F_PER_JOB = min(F_PER_JOB, n_frames) - - # Calculate how many jobs need to be submitted - return n_frames, math.ceil(n_frames / F_PER_JOB) - -def build_job_kwargs(job_name, command): - """Returns a dictionary with properties for launching a job. - - Keyword arguments: - job_name -- name of the job - command -- command to be executed by the job - """ - - return { - 'jobName': job_name, - 'jobQueue': JOB_QUEUE, - 'jobDefinition': JOB_DEFINITION, - 'containerOverrides': { - 'command': command - }, - 'retryStrategy': { - 'attempts': 1 - } - } - -def submit_rendering_job(n_frames, array_size): - """Submits a Batch job that renders the frames. - Depending on the value of , it will submit either - a single job or an array job - - Keyword arguments: - n_frames -- total number of frames - array_size -- size of the array job - """ - - # Append the name of the job to the URI so that a folder is created in S3 - full_output_uri = '{}/{}'.format(OUTPUT_URI, JOB_NAME) - client = boto3.client('batch') - command = 'render -i {} -o {} -f {} -t {}'.format(INPUT_URI, full_output_uri, F_PER_JOB, n_frames) - kwargs = build_job_kwargs('{}_Rendering'.format(JOB_NAME), command.split()) - - # If this is not a single job, submit it as an array job - if array_size > 1: - kwargs['arrayProperties'] = { - 'size': array_size - } - - try: - return client.submit_job(**kwargs) - except Exception as e: - print(e.args[0]) - sys.exit(2) - -def submit_stitching_job(depends_on_job_id): - """Submits a Batch job that creates a mp4 video using the rendered frames. - - Keyword arguments: - depends_on_job_id -- identifier of the job that the stitching job depends on - """ - - # Append the name of the job to the URI so that a folder is created in S3 - full_output_uri = '{}/{}'.format(OUTPUT_URI, JOB_NAME) - client = boto3.client('batch') - command = 'stitch -i {} -o {}'.format(full_output_uri, full_output_uri) - kwargs = build_job_kwargs('{}_Stitching'.format(JOB_NAME), command.split()) - - # Add the job dependency - kwargs['dependsOn'] = [ - { - 'jobId': depends_on_job_id, - 'type': 'SEQUENTIAL' - } - ] - - try: - return client.submit_job(**kwargs) - except Exception as e: - print(e.args[0]) - sys.exit(2) - -if __name__ == "__main__": - job_results = [] - - # Gather command line arguments - parse_arguments() - - # Download the blend file from s3 and save it locally to work with it - download_blender_file_from_s3() - - # Calculate the size of the array job - n_frames, array_size = calculate_array_job_size() - - # Submit the rendering job - job_results.append(submit_rendering_job(n_frames, array_size)) - - # Submit the stitching job - job_results.append(submit_stitching_job(job_results[0]['jobId'])) - - print(json.dumps(job_results)) diff --git a/content/rendering-with-batch/batch/batch.files/pottery.blend b/content/rendering-with-batch/batch/batch.files/pottery.blend deleted file mode 100644 index 9629522d..00000000 Binary files a/content/rendering-with-batch/batch/batch.files/pottery.blend and /dev/null differ diff --git a/content/rendering-with-batch/batch/job_definition.md b/content/rendering-with-batch/batch/job_definition.md index 8606d1a3..167a05c3 100644 --- a/content/rendering-with-batch/batch/job_definition.md +++ b/content/rendering-with-batch/batch/job_definition.md @@ -18,7 +18,8 @@ cat < job-definition-config.json "containerProperties": { "image": "${IMAGE}", "vcpus": 1, - "memory": 8000 + "memory": 8000, + "command": ["Ref::action", "-i", "Ref::inputUri", "-o", "Ref::outputUri", "-f", "Ref::framesPerJob"] }, "retryStrategy": { "attempts": 3 @@ -36,6 +37,7 @@ Let's explore the configuration parameters in the structure: - **image**: the image used to start a container, this value is passed directly to the Docker daemon. - **vcpus**: The number of vCPUs reserved for the job. Each vCPU is equivalent to 1,024 CPU shares. - **memory**: hard limit (in MiB) for a container. If your container attempts to exceed the specified number, it's terminated. +- **command**: this is the command that will be executed in the container when the job is started. It has placeholders for some parameters that will be substituted when submitting the job using AWS Batch. - **platformCapabilities**: the platform capabilities required by the job definition. Either `EC2` or `FARGATE`. {{% notice info %}} diff --git a/content/rendering-with-batch/batch/job_queue.md b/content/rendering-with-batch/batch/job_queue.md index ddbe61b4..35c45c31 100644 --- a/content/rendering-with-batch/batch/job_queue.md +++ b/content/rendering-with-batch/batch/job_queue.md @@ -42,7 +42,8 @@ All the compute environments within a queue must be either (`SPOT` and/or `EC2`) Execute this command to create the job queue. To learn more about this API, see [create-job-queue CLI command reference](https://docs.aws.amazon.com/cli/latest/reference/batch/create-job-queue.html). ``` -aws batch create-job-queue --cli-input-json file://job-queue-config.json +export JOB_QUEUE_ARN=$(aws batch create-job-queue --cli-input-json file://job-queue-config.json | jq -r '.jobQueueArn') +echo "Job queue Arn: ${JOB_QUEUE_ARN}" ``` Next, you are going to create a **Job Definition** that will be used to submit jobs. diff --git a/content/rendering-with-batch/batch/submit_job.md b/content/rendering-with-batch/batch/submit_job.md index e237b538..af2b0537 100644 --- a/content/rendering-with-batch/batch/submit_job.md +++ b/content/rendering-with-batch/batch/submit_job.md @@ -1,92 +1,143 @@ --- -title: "Submitting the job" +title: "Workflow orchestration" date: 2021-09-06T08:51:33Z weight: 120 --- -You have now all the AWS Batch components in place, and are ready to start submitting jobs that will be placed in a queue and processed by a compute environment when AWS Batch's scheduler starts running them. The last step is to download a Python script that will take the bucket name, the rendering queue and the job definition as input parameters and will launch the two jobs of the rendering pipeline using the [AWS SDK for Python, Boto3](https://aws.amazon.com/sdk-for-python/). +You have now all the AWS Batch components in place, and are ready to start submitting jobs that will be placed in a queue and processed by a compute environment when AWS Batch's scheduler starts running them. We are going to use [AWS Step Functions](https://aws.amazon.com/step-functions/?nc1=h_ls&step-functions.sort-by=item.additionalFields.postDateTime&step-functions.sort-order=desc) to orchestrate the execution of our rendering pipeline, from the pre-processing of the Blender file to the stitching of the frames. -## Downloading the python script +AWS Step Functions helps you orchestrate your AWS Batch jobs using serverless workflows, called state machines. You can use Step Functions to orchestrate preprocessing of data and Batch to handle the large compute executions, providing an automated, scalable, and managed batch computing workflow. The CloudFormation template has created the following state machine: -To submit the jobs that will implement the rendering and stitching you are going to use a python script that has already been coded. Execute the following command to download it from GitHub. +![State machine](/images/rendering-with-batch/state_machine.png) -``` -wget https://raw.githubusercontent.com/awslabs/ec2-spot-workshops/master/content/rendering-with-batch/batch/batch.files/job_submission.py -``` - -## Reviewing the command line arguments - -This script needs a couple of command line arguments as input, execute the following to read the help documentation: +You can notice that each step in the rendering pipeline has been mapped to a state. In AWS Step Functions, you can create state machines using the [Amazon States Language](https://docs.aws.amazon.com/step-functions/latest/dg/concepts-amazon-states-language.html) or the [AWS Step Functions Workflow Studio](https://docs.aws.amazon.com/step-functions/latest/dg/tutorial-workflow-studio-using.html). -``` -python3 job_submission.py -h -``` The script needs: (a) the location of the blender file, (b) the location where results will be uploaded, \(c\) the Job Definition that will be used to submit the job, (d) the Job Queue where it will be placed and (e) the name that will be used to submit it. -Additionally, there's an extra argument `-f` that needs to be passed. The `-f` argument can be used to specify how many frames each job should render. This will have a direct impact on the size of the array job that is submitted. E.g.: if you want to render a file that has 250 frames and you specify a value of 1 for that argument, the size of the array job will be 250. If you specify a value of 5, the size will be 50 and so on. As you can imagine, the less the frames each job has to render, the less the time it will take for the job to complete. +The state machine will: -## Submitting the job +1. Download the Blender file from S3 to determine how many frames it has. +2. Submit a Batch array job that will run Blender to render the frames in parallel. +3. Submit a Batch job that will run FFmpeg to produce the final video. -To submit the rendering job, run the following block of code optionally replacing the value of the argument -f. It will launch the python script and export the identifiers of the two jobs to environment variables to be able to monitor them. +To start the process, perform the following api call to pass a payload to the state machine with the job name, input path, output path, ARNs of the Job Definition and Job queue for AWS Batch to use and the number of frames each job has to render: ``` -export JOB_NAME="RenderingWithBatch" -export JOB_IDS=$(python3 job_submission.py -i "s3://${BucketName}/${BlendFileName}" -o "s3://${BucketName}" -f 1 -n "${JOB_NAME}" -q "${RENDERING_QUEUE_NAME}" -d "${JOB_DEFINITION_NAME}") -export RENDERING_JOB_ID=$((echo $JOB_IDS) | jq -r '.[0].jobId') -export STITCHING_JOB_ID=$((echo $JOB_IDS) | jq -r '.[1].jobId') -echo "Job successfully submitted. Rendering job Id: ${RENDERING_JOB_ID}. Stitching job Id: ${STITCHING_JOB_ID}" +export JOB_NAME="Pottery" +export EXECUTION_ARN=$(aws stepfunctions start-execution --state-machine-arn "${StateMachineArn}" --input "{\"jobName\": \"${JOB_NAME}\", \"inputUri\": \"s3://${BucketName}/${BlendFileName}\", \"outputUri\": \"s3://${BucketName}/${JOB_NAME}\", \"jobDefinitionArn\": \"${JOB_DEFINITION_ARN}\", \"jobQueueArn\": \"${JOB_QUEUE_ARN}\", \"framesPerJob\": \"1\"}" | jq -r '.executionArn') +echo "State machine started. Execution Arn: ${EXECUTION_ARN}." ``` -At this point the jobs have been submitted and you are ready to monitor them. +To learn more about this API, see [start-execution CLI Command Reference](https://docs.aws.amazon.com/cli/latest/reference/stepfunctions/start-execution.html). At this point the state machine is started and you are ready to monitor the progress of the pipeline. -## Optional: understanding the script +## Optional: understanding the state machine -We have used a python program to submit the jobs to AWS Batch. Feel free to move to the next section and monitor the execution of your AWS Batch job. If at some point you are interested in knowing the details of how the `job_submission.py` python job submits the job, you can read the sections below. +We have orchestrated the workflow of the rendering pipeline using an AWS Step Functions state machine. Feel free to move to the next section and monitor its execution, but if at some point you are interested in knowing the details of its states, you can read the sections below. -### Method submit_rendering_job +### State *Number of frames extraction* -Two important things to highlight from this method: +This is the entry point of our state machine. In this state, a Lambda function is invoked to determine the number of frames of the Blender file. To do that, we are using a script included in Blender's script library that extracts information related to the scenes of the composition. In broad strokes, the Lambda function implements the following logic: -1. The command that is passed to the container is defined in the line 200. The arguments are preceded by the keyword `render`. These arguments will be received by the bash script that we have talked about in the section *Download image files* inside *Creating your Docker image*. - - {{< highlight go "linenos=table, hl_lines=3, linenostart=198" >}} -full_output_uri = '{}/{}'.format(OUTPUT_URI, JOB_NAME) -client = boto3.client('batch') -command = 'render -i {} -o {} -f {} -t {}'.format(INPUT_URI, full_output_uri, F_PER_JOB, n_frames) -kwargs = build_job_kwargs('{}_Rendering'.format(JOB_NAME), command.split()) -{{< / highlight >}} +1. Receives the payload that we passed on to the state machine when we started it. +2. Downloads the Blender file from S3. +3. Reads the number of frames of the file. +4. Calculates the dimension of the Batch array job taking into account the number of frames and how many frames each job has to render. -2. The array job is created by specifying the `arrayProperties` value. An array job is a job that shares common parameters, such as the job definition, vCPUs, and memory. It runs as a collection of related, yet separate, basic jobs that may be distributed across multiple hosts and may run concurrently. At runtime, the `AWS_BATCH_JOB_ARRAY_INDEX` environment variable is set to the container's corresponding job array index number. This is how the bash script is able to calculate the slice of frames that needs to render. +An array job is a job that shares common parameters, such as the job definition, vCPUs, and memory. It runs as a collection of related, yet separate, basic jobs that may be distributed across multiple hosts and may run concurrently. At runtime, the `AWS_BATCH_JOB_ARRAY_INDEX` environment variable is set to the container's corresponding job array index number. This is how the bash script is able to calculate the slice of frames that needs to render. To learn more about it, visit [Array jobs](https://docs.aws.amazon.com/batch/latest/userguide/array_jobs.html) and [Tutorial: Using the Array Job Index to Control Job Differentiation](https://docs.aws.amazon.com/batch/latest/userguide/array_index_example.html). - {{< highlight go "linenos=table, linenostart=204" >}} -if array_size > 1: - kwargs['arrayProperties'] = { - 'size': array_size - } -{{< / highlight >}} +You can view the Lambda function in the following URL: -### Method submit_stitching_job +``` +echo "https://console.aws.amazon.com/lambda/home?region=${AWS_DEFAULT_REGION}#/functions/${PreprocessingLambda}?tab=code" +``` -Two important things to highlight from this method: +### State *Rendering* + +Submits an AWS Batch array job of dimension *n*, where *n* is the number returned by the Lambda function of the previous state. Three important configurations are implemented in the definition of this state: + +1. **Extraction of the ARNs of the Job Definition and Job Queue** from the payload received by the state machine using a [JSONPath](https://docs.aws.amazon.com/kinesisanalytics/latest/dev/about-json-path.html) expression. Those are passed on to AWS Batch when submitting the job: + + {{< highlight go "linenos=inline, linenostart=1, hl_lines=15-16" >}} +"Rendering": { + "Type": "Task", + "Resource": "arn:aws:states:::batch:submitJob.sync", + "Parameters": { + "JobName": "Rendering", + "ArrayProperties": { + "Size.$": "$.output.Payload.body.arrayJobSize" + }, + "Parameters": { + "action": "render", + "inputUri.$": "$.inputUri", + "outputUri.$": "$.outputUri", + "framesPerJob.$": "$.framesPerJob" + }, + "JobDefinition.$": "$.jobDefinitionArn", + "JobQueue.$": "$.jobQueueArn" + }, + "Next": "Stitching", + "ResultPath": "$.output" +} +{{< / highlight >}} -1. The command that is passed to the container is defined in the line 225. The arguments are preceded by the keyword `stitch`. + Read [this blog post](https://aws.amazon.com/es/blogs/compute/using-jsonpath-effectively-in-aws-step-functions/) to learn how to effectively use JSONPath expressions in AWS Step Functions. + +2. **Setting the dimension of the array job** by specifying a value for the attribute `Size` inside the `ArrayProperties` structure. Now, we are taking that value from the output of the previous state: + + {{< highlight go "linenos=inline, linenostart=1, hl_lines=6-8" >}} +"Rendering": { + "Type": "Task", + "Resource": "arn:aws:states:::batch:submitJob.sync", + "Parameters": { + "JobName": "Rendering", + "ArrayProperties": { + "Size.$": "$.output.Payload.body.arrayJobSize" + }, + "Parameters": { + "action": "render", + "inputUri.$": "$.inputUri", + "outputUri.$": "$.outputUri", + "framesPerJob.$": "$.framesPerJob" + }, + "JobDefinition.$": "$.jobDefinitionArn", + "JobQueue.$": "$.jobQueueArn" + }, + "Next": "Stitching", + "ResultPath": "$.output" +} +{{< / highlight >}} - {{< highlight go "linenos=table, hl_lines=3, linenostart=225" >}} -full_output_uri = '{}/{}'.format(OUTPUT_URI, JOB_NAME) -client = boto3.client('batch') -command = 'stitch -i {} -o {}'.format(full_output_uri, full_output_uri) -kwargs = build_job_kwargs('{}_Stitching'.format(JOB_NAME), command.split()) +3. **Setting a value for the parameters defined in the Job Definition**. If you remember, we did specify a `command` attribute with the value `["Ref::action", "-i", "Ref::inputUri", "-o", "Ref::outputUri", "-f", "Ref::framesPerJob"]` when we created the Job Definition. Now it's time to give a value to the placeholders in that expression: + + {{< highlight go "linenos=inline, linenostart=1, hl_lines=9-14" >}} +"Rendering": { + "Type": "Task", + "Resource": "arn:aws:states:::batch:submitJob.sync", + "Parameters": { + "JobName": "Rendering", + "ArrayProperties": { + "Size.$": "$.output.Payload.body.arrayJobSize" + }, + "Parameters": { + "action": "render", + "inputUri.$": "$.inputUri", + "outputUri.$": "$.outputUri", + "framesPerJob.$": "$.framesPerJob" + }, + "JobDefinition.$": "$.jobDefinitionArn", + "JobQueue.$": "$.jobQueueArn" + }, + "Next": "Stitching", + "ResultPath": "$.output" +} {{< / highlight >}} -2. The job dependency is created by specifying the `dependsOn` value, which is a dictionary that contains the identifier of the job towards which create the dependency, and the type of dependency. In this case, the dependency is `SEQUENTIAL` because the stitching job must be launched **after** all the frames have been rendered. To learn more about job dependencies visit [Job Dependencies](https://docs.aws.amazon.com/batch/latest/userguide/job_dependencies.html). +As you can see, the action is set to `render`, since this state implements the rendering part of the pipeline. - {{< highlight go "linenos=table, linenostart=229" >}} -kwargs['dependsOn'] = [ - { - 'jobId': depends_on_job_id, - 'type': 'SEQUENTIAL' - } -] -{{< / highlight >}} +### State *Stitching* + +Similarly to the previous state, the *Stitching* state launches an AWS Batch job but, in this case, it is a single job. By the way we have defined the execution flow of the state machine, this state will be executed **after** the previous state has completed its execution. Optionally, in AWS Batch you can define job dependencies to manage the relationship of jobs and start them when others finish their execution. To learn more about job dependencies visit [Job Dependencies](https://docs.aws.amazon.com/batch/latest/userguide/job_dependencies.html) and to learn more about AWS Step Functions transitions visit [Transitions](https://docs.aws.amazon.com/step-functions/latest/dg/concepts-transitions.html). + +The only configuration of this state that differs from the previous is the value of the parameter `action`, that in this case is set to `stitch` so that our Docker container runs FFmpeg to produce the video when all the frames have been rendered. diff --git a/content/rendering-with-batch/ecr-repository.md b/content/rendering-with-batch/ecr-repository.md index 81c80ca9..f79318c9 100644 --- a/content/rendering-with-batch/ecr-repository.md +++ b/content/rendering-with-batch/ecr-repository.md @@ -61,7 +61,7 @@ You are now done with the container part. Next, you will configure some environm ## Optional: understanding the render.sh script When we send a batch job, the container that we just created will be executed. The entry point of the container is the bash script `render.sh`. The script just takes -a few arguments that AWS Batch will pass to each task and does run either blender when an environment variable named `ACTION` is set to `render` or or ffmpeg when is set to `stitch`. +a few arguments that AWS Batch will pass to each task and does run either blender when an environment variable named `ACTION` is set to `render` or ffmpeg when is set to `stitch`. The following section describes the `render.sh` script in more detail. You don't need to go through this to run this workshop, but if you are interested in fully understanding how Blender and FFmpeg are called it will give you a clear description. @@ -70,7 +70,12 @@ The following section describes the `render.sh` script in more detail. You don't Reads the environment variable `ACTION` and decides from it what's the type of job to run, either render or stitch. It also takes other arguments such as the the *input*, *output* -{{< highlight go "linenos=table, linenostart=6" >}} +{{< highlight go "linenos=inline,hl_lines=6-11,linenostart=1" >}} +#!/bin/bash + +parse_arguments() { + # Parses the command line arguments and stores the values in global variables. + ACTION=$1 if [ "${ACTION}" != "render" ] && [ "${ACTION}" != "stitch" ] ; then @@ -78,6 +83,8 @@ Reads the environment variable `ACTION` and decides from it what's the type of j exit 2 fi + while (( "$#" )); do +... {{< / highlight >}} @@ -85,53 +92,143 @@ Reads the environment variable `ACTION` and decides from it what's the type of j 1. Downloads the blender file from S3. - {{< highlight go "linenos=table, linenostart=57" >}} -aws s3 cp "${INPUT_URI}" file.blend -{{< / highlight >}} + {{< highlight go "linenos=inline,hl_lines=4-5,linenostart=53" >}} +render() { + # Pipeline that is executed when this script is told to render. + + # Download the blender file from S3 + aws s3 cp "${INPUT_URI}" file.blend + + # Calculate start frame and end frame + calculate_render_frame_range + + # Start the rendering process + mkdir frames + echo "Rendering frames ${start_frame} to ${end_frame}" + blender -b file.blend -E CYCLES -o "frames/" -s "${start_frame}" -e "${end_frame}" -a + + # Upload all the rendered frames to a folder in S3 + aws s3 cp --recursive "frames" "${OUTPUT_URI}/frames" +} -2. Calculates the slice of frames that has to render (we will se how in more detail when we talk about AWS Batch). +{{< / highlight >}} - {{< highlight go "linenos=table, linenostart=43" >}} -if [[ -z "${AWS_BATCH_JOB_ARRAY_INDEX}" ]]; then - start_frame=1 - end_frame="${F_PER_JOB}" -else - start_frame=$((AWS_BATCH_JOB_ARRAY_INDEX * F_PER_JOB + 1)) - end_frame=$((AWS_BATCH_JOB_ARRAY_INDEX * F_PER_JOB + F_PER_JOB)) -fi +2. Calculates the slice of frames that has to render (we will cover in more detail when we talk about AWS Batch). + + {{< highlight go "linenos=inline,hl_lines=6-13,linenostart=38" >}} +calculate_render_frame_range() { + # Calculates the start frame and end frame a job has to render + # using the value of the env var AWS_BATCH_JOB_ARRAY_INDEX + + # If the env var AWS_BATCH_JOB_ARRAY_INDEX is empty, this is a single job. Render from start to end + if [[ -z "${AWS_BATCH_JOB_ARRAY_INDEX}" ]]; then + start_frame=1 + end_frame="${F_PER_JOB}" + # Otherwise use the array index to calculate the corresponding frame slice + else + start_frame=$((AWS_BATCH_JOB_ARRAY_INDEX * F_PER_JOB + 1)) + end_frame=$((AWS_BATCH_JOB_ARRAY_INDEX * F_PER_JOB + F_PER_JOB)) + fi +} {{< / highlight >}} + 3. Executes Blender. - {{< highlight go "linenos=table, linenostart=63, hl_lines=3" >}} -mkdir frames -echo "Rendering frames ${start_frame} to ${end_frame}" -blender -b file.blend -E CYCLES -o "frames/" -s "${start_frame}" -e "${end_frame}" -a + {{< highlight go "linenos=inline,hl_lines=10-13,linenostart=53" >}} +render() { + # Pipeline that is executed when this script is told to render. + + # Download the blender file from S3 + aws s3 cp "${INPUT_URI}" file.blend + + # Calculate start frame and end frame + calculate_render_frame_range + + # Start the rendering process + mkdir frames + echo "Rendering frames ${start_frame} to ${end_frame}" + blender -b file.blend -E CYCLES -o "frames/" -s "${start_frame}" -e "${end_frame}" -a + + # Upload all the rendered frames to a folder in S3 + aws s3 cp --recursive "frames" "${OUTPUT_URI}/frames" +} {{< / highlight >}} 4. Uploads all the frames to S3. - {{< highlight go "linenos=table, linenostart=68" >}} -aws s3 cp --recursive "frames" "${OUTPUT_URI}/frames" + {{< highlight go "linenos=inline,hl_lines=15-16,linenostart=53" >}} +render() { + # Pipeline that is executed when this script is told to render. + + # Download the blender file from S3 + aws s3 cp "${INPUT_URI}" file.blend + + # Calculate start frame and end frame + calculate_render_frame_range + + # Start the rendering process + mkdir frames + echo "Rendering frames ${start_frame} to ${end_frame}" + blender -b file.blend -E CYCLES -o "frames/" -s "${start_frame}" -e "${end_frame}" -a + + # Upload all the rendered frames to a folder in S3 + aws s3 cp --recursive "frames" "${OUTPUT_URI}/frames" +} {{< / highlight >}} ### Method stitch: 1. Downloads all the frames from S3. - {{< highlight go "linenos=table, linenostart=75" >}} -mkdir frames -aws s3 cp --recursive "${INPUT_URI}/frames" frames/ + {{< highlight go "linenos=inline,hl_lines=4-6,linenostart=71" >}} +stitch() { + # Pipeline that is executed when this script is told to stitch. + + # Download the frames from S3 + mkdir frames + aws s3 cp --recursive "${INPUT_URI}/frames" frames/ + + # Start the stitching process + ffmpeg -i frames/%04d.png output.mp4 + + # Upload the output video to S3 + aws s3 cp output.mp4 "${OUTPUT_URI}/output.mp4" +} {{< / highlight >}} 2. Executes FFmpeg. - {{< highlight go "linenos=table, linenostart=79" >}} -ffmpeg -i frames/%04d.png output.mp4 + {{< highlight go "linenos=inline,hl_lines=8-9,linenostart=71" >}} +stitch() { + # Pipeline that is executed when this script is told to stitch. + + # Download the frames from S3 + mkdir frames + aws s3 cp --recursive "${INPUT_URI}/frames" frames/ + + # Start the stitching process + ffmpeg -i frames/%04d.png output.mp4 + + # Upload the output video to S3 + aws s3 cp output.mp4 "${OUTPUT_URI}/output.mp4" +} {{< / highlight >}} 3. Uploads the video to S3. - {{< highlight go "linenos=table, linenostart=82" >}} -aws s3 cp output.mp4 "${OUTPUT_URI}/output.mp4" + {{< highlight go "linenos=inline,hl_lines=11-12,linenostart=71" >}} +stitch() { + # Pipeline that is executed when this script is told to stitch. + + # Download the frames from S3 + mkdir frames + aws s3 cp --recursive "${INPUT_URI}/frames" frames/ + + # Start the stitching process + ffmpeg -i frames/%04d.png output.mp4 + + # Upload the output video to S3 + aws s3 cp output.mp4 "${OUTPUT_URI}/output.mp4" +} {{< / highlight >}} diff --git a/content/rendering-with-batch/final_thoughts.md b/content/rendering-with-batch/final_thoughts.md index c1b58055..42f6f769 100644 --- a/content/rendering-with-batch/final_thoughts.md +++ b/content/rendering-with-batch/final_thoughts.md @@ -10,7 +10,8 @@ weight: 160 2. Seen how easy it is to adopt Spot best practices when using AWS Batch, by defining several compute environments with the appropriate allocation strategies. 3. Learned the different types of jobs you find in AWS Batch (array and single). Also, you have learned how to define dependencies between them and how to work with the AWS_BATCH_JOB_ARRAY_INDEX environment variable. 4. Learned the concepts required to create a rendering pipeline using Blender and FFmpeg. -5. Built and published in ECR a Docker image that runs Blender and FFmpeg. +5. Used AWS Step Functions to orchestrate the workflow of the pipeline. +6. Built and published in ECR a Docker image that runs Blender and FFmpeg. ## Savings diff --git a/content/rendering-with-batch/monitor_job.md b/content/rendering-with-batch/monitor_job.md index dcbd9bd3..9d141176 100644 --- a/content/rendering-with-batch/monitor_job.md +++ b/content/rendering-with-batch/monitor_job.md @@ -11,6 +11,18 @@ You can check the rendering progress by running these commands in the Cloud9 ter ``` while [ true ] do + while [ true ] + do + export RENDERING_JOB_ID=$(aws batch list-jobs --job-queue "${RENDERING_QUEUE_NAME}" --filters name=JOB_NAME,values="${JOB_NAME}" --query 'jobSummaryList[*].jobId' | jq -r '.[0]') + + if [ ! -z "$RENDERING_JOB_ID" ] ; then + break + fi + + echo "Rendering not started yet" + sleep 5 + done + export RENDERING_PROGRESS=$(aws batch describe-jobs --jobs "${RENDERING_JOB_ID}") export RENDER_COUNT=$(echo $RENDERING_PROGRESS | jq -r '.jobs[0].arrayProperties.statusSummary.SUCCEEDED') export FRAMES_TO_RENDER=$(echo $RENDERING_PROGRESS | jq -r '.jobs[0].arrayProperties.size') @@ -21,12 +33,16 @@ do break fi - sleep 7 + sleep 5 done ``` +{{% notice info %}} +It is normal if the progress is stuck at 0% at the beginning and after it increases rapidly. The reason for this is that AWS Batch is provisioning capacity for the Compute environments as defined earlier, and jobs will remain in the `RUNNABLE` state until sufficient resources are available. You can read more about job states here: [Job States](https://docs.aws.amazon.com/batch/latest/userguide/job_states.html). +{{% /notice %}} + {{% notice tip %}} -This operation will take about 5 minutes. While it progresses, go to the AWS Batch Console, and explore the state of: (a) Compute Environments, (b) Jobs. You can also check in the EC2 Console the: \(c\) EC2 Instances and (d) Auto Scaling groups defined. +This operation will take about 10 minutes. While it progresses, go to the AWS Batch Console, and explore the state of: (a) Compute Environments, (b) Jobs. You can also check in the EC2 Console the: \(c\) EC2 Instances and (d) Auto Scaling groups defined. {{% /notice %}} When the progress reaches 100%, the output video will be available in the following URL: @@ -46,9 +62,20 @@ Explore also the rest of the S3 folders and check the frames that were created. {{% /notice %}} - ## Monitoring +### Viewing the execution of the state machine + +You can follow the progress of the rendering pipeline by navigating to the executions tab of the state machine. To do it: + +1. Navigate to the AWS Step Functions service page. +2. Select the state machine whose name begins with **RenderingPipeline**. +3. In the executions tab, select the first entry. +4. In the graph inspector, you can select a state to see the input it receives and the output it produces. + +![AWS Step Functions console](/images/rendering-with-batch/step-functions.png) + + ### Viewing the logs of a job To view the logs of a job using the console: @@ -65,7 +92,7 @@ To view the logs of a job using the console: You can monitor the status of a job using the following command: ``` -aws batch describe-jobs --jobs "${RENDERING_JOB_ID}" "${STITCHING_JOB_ID}" +aws batch describe-jobs --jobs "${RENDERING_JOB_ID}" ``` To learn more about this command, you can review the [describe-jobs CLI command reference](https://docs.aws.amazon.com/cli/latest/reference/batch/describe-jobs.html). diff --git a/content/rendering-with-batch/rendering-with-batch.files/diagrams.drawio b/content/rendering-with-batch/rendering-with-batch.files/diagrams.drawio index f8bc43ca..9430e57d 100644 --- a/content/rendering-with-batch/rendering-with-batch.files/diagrams.drawio +++ b/content/rendering-with-batch/rendering-with-batch.files/diagrams.drawio @@ -1 +1 @@ -7Vxtc6M4DP41+dgM5j0f89bdzu1Ne+3c7e196RBwElqCWeKk6f76s8EmxibvpEmzaTvTWBhBLOmRLAkaRney+JJ6yfhPFMCooWvBomH0Grquu6ZG/lHKe06xXCcnjNIwyElgSXgKf0FGZOeNZmEAp6WJGKEIh0mZ6KM4hj4u0bw0RW/laUMUla+aeCOoEJ58L1Kp38MAjxnVtszlga8wHI35pYHdyo9MPD6bfZXp2AvQm0Ay+g2jmyKE80+TRRdGdPX4wuTn3a44WtxZCmO8zQn/6eP2cydtWa37u9Ff9y/tu/DrjZFzmXvRjH3jfx667H7xO1+FBIUxzlbS6pA/cp2u1rDIkS4dNXVLIshjp0wA6ojyKBPksVMmAJk9kK4P5BsUCMqoxF6Trq8JN0j+jA6a4SiMYbfQOY0QR6kXhEQUXRShlNBiFJPV64zxJCIjQD6+jUMMnxLPp6v6RuyF0IYoxkzrgc7HbOEpV6I0Cf08WYyogTW9t6nZHKVolmSXvCN6X3n0eZ749HScolfIb6mhG7rpusCkFwqjSLrVOUxxSFS/HYUjyhUjehGPjSI4xJQjuf8wHn3LRj1DY/csXKLd7jgdl9ADbzqGAfsiqrYyBaZXhQuBxLT3C0QTiNN3MoUdvSHGxWyJoQlHl7eladouo40FqzS5DXoMDkYF86XBkA/MZqrt5+Ynvn3842mhPXYfRj/n/ZfX9OUGWIoBPcI4gClZoxyCPPoNXtBAMaoKCSjistp217XFtQQrBSWrjiSWglUdkmg5ZUE4qiB0rUIQhn00QShyeHjHY2IdZM39NEywsv7TV4j9MVssb5rkpjwMF3SdOxnkwbQ/hznygdwYcwTXZO0WrLwsQybYyBvA6AFNQxxmButDylwQ5TdpwgBhjCYbjbLgIyoTEbamAdftVeDHZOp7sJmdlaThFDZ94rKfydmwFgMtDI3pBbkTRTHMlqoX1rHUQlfU4i5OZkQZtAj5XrbYa/Wi0vNVer8qD1jpBVVPWJqW+aaKK8jEKpqjEoE6jbszlVhFq/Ld8tmg4mwgnb3ac0pARX5vqYQVj9qg8NE2DEc41gtTwii3mRilVMdkW9AdhziOKnQdZj+yLddlkgImbPLyCvpUuv0UTtEs9WHu9DtkWOX+p0Zd3tY0zKZVMmfLVc3ZcVVz5rTazVn1tor5woAE8GxIFQKNUOxF/SVVFAxx1W26S6BzE0hXFUaDbMiFnOmNl2I+jQE6OfE2pPeeR2mK3zatjk3DLBKLkWiAqxaRRvr+Lx2QhWXDH8WtkEFvIc7svYujBxJUkFWkupUTFyEWeJHRD+HIkhMdvAsDmc9KXcm1bTO4kuUZQbzZN1PJrNU8QasAjx5SGBGYnpe3Y1V6xdg9UMAWtdhwQFmHDcnX5F+TnSfunxRWuit5NzmszFdCYdVmUSCfxpyKbAzFSuxvH7ZiH/0FTj2fXk2LZ5MBkbquoSHdCqfeBE4rzSdDvrKhKOgmg+AkDILMuggshb+8QYFtZQ/aq1S4tbYuI1ax2WcXaYjb6Sok05pAc8tKwFXyYO0qcQV6mQEaDqcQH0POjiLn9vcnQuh4NHS5RjSfJ6K5ddy+Zu4W0fQ0qwuc3yaiGWRKXVcKAUgYDuzThjTu8UKaAdnjvO4T06j72DUxDQtDgBiEFCHJ9mHIMjYSIyMhUNolNto/puFJ5k0xjXPKmAZYK7bZe8Q0hsTKtbaKaeryZNwcBf3/5s1if0xjE7JSQi4tDhq6HdEk5IBEMfYoS0fikIBDPuEFDc45nuGGfnA8c0O8lcnrKAfqEjDKcZEuSf94IQxQM3b38U0PTirl3EWTZIYhtbh4HqYonsBYTeld6xSfrk4Bff05jIk7in34TGbhPN+qup+ea9vAOGrlorhE7ZWLlgTYQHN4akV0EDw7LgYewLRWG/VhKXM1OfqUIAoOV2u7WtsFWVtVeWqFrR2tPKUW2nskOM/SMVQOHtGcdP3G/aCd5qoCY3GCtMW8yI0k1/xpXXqmSahuqXvJKjU7XhHUvGrZxWuZAdTI4WO1TM0+Vm679klZ8FTEhowFz2xkxH2SFjzZUFRejpFsOOskgtzCoVtSnLl1DkF2t4bsRXcui+x4y0tlzlnWu09VE3T3M3ztIfi0GfdrD8HH9RC0tFbJYk/eQABal+O7dm0iOHm/AIfSzQ0D1ikdo9RkekDDAJAaBpQ+1JM3DPAbEuzh7yRCXkCz72ffIlBYcy05dWDqJWnx/pZDFUriCj4sxV4AtpDgE+olU/KP9n1eXHeyI3Whnr47mXuZa7Hjmn69+PSrI6fFzqHYoV8TsKffV9SdGrPlstqpE7C6moC9ltSumH55mH4GJTXeqnJF9AtG9JMXOwx1D1W5IT5mf+aqjNG5NGzulodaqSb15Y84x5Pkj1ywIemzdf7I2cSprsKKLW2YbZZYOWphxVD3xI+wnHjKez61WZGRmocBRJX2dx4JKWMV4u2VkLLt8uN3NeWjDLvEtajnHj8hZaibsNrA9DNWjpcpeU0rJeWbrmN8ZGLe2hZYT4irrZZc/d0XV91NnOrCVaBJFWvDNdffmnLGx0Cxum3lUEzgirbgU1DOXhKghfEQpZPqsvcZATFHmjqAGOi8277mygAw3bKDl1gcEYnVJ0V/ayReiZ0iJFa+QemkDzcr79vYu4kHaLa889oOFGvTSLXPpr+A/gyfdQWysKManlLW3Fb5aZwDUeao8d06WxBE2I3QLGipJS5FpNeOqRLtrDqmzJ5DDjZ26pgiP2anVQXnF5kH83M9r+khZfuEDylXGvYR37vyUdEC32rtkvXaM8JYqQOb36iybefwaTukZPV09t6JbeK0d9BBhstXa+bTl28oNfr/Aw==7VrbUuM4EP2aPCblaxIeSYCZ2ZqtpYqtvTxRsq3YWmQryDJJ5uu3ZUu+KOESsMmwC1QFq9VuSUenu9UiI3eZbr9wtE5+ZRGmI8eKtiP3YuQ4tutN4Y+U7CrJbDqrBDEnkVJqBDfkB1ZCS0kLEuG8oygYo4Ksu8KQZRkORUeGOGebrtqK0e6oaxTjPcFNiOi+9E8SiURJp77XdHzFJE700Pb0rOpJkdZWS8kTFLFNS+RejtwlZ0xUT+l2ialETwNz7ts7l34dx/T3+/QP7/uZ8JJxZezqmFfqNXCciX5NO5XpB0QLBdjImVIYZLFiMBYsWuwUlNP7gumOcV5u9Dko2N5623TCUyz/3rjQFRThHRbaIMyssllpKExr804OuqFE3IJeVghKMrysWWGpkZeMMl6+4MLvlVzqIuYoIrjpy1gGNhe54OwOt1648ue+60lDhNKWfFX+gDxCeYIjNdoD5oIAkb6jANNrlhNBWAZ9AROCpS2Fc0pi2SHYGqRItUKYEIYBFolIKbRttQDlIraj2woBOSTK19VqV2Qr57EAyq1lZ7qNpXtO0Cb3JhznrOAh/hbK+SygWT11tRT4GuhnyaNIJheFty3fUWT6glmKBd+Biup1PPXKTnu88pNN42ezeSVKWh6mZUh5dlxbbsgLD4q/R3B5foDLJsdKSuhA4HR3xwR7RdkmTBAXk1zA561U3yRE4Js1CqXiBtSGBHhmdQB29vHVoja+WtY7vjqkPwUwjiD6qibjImExyxC9bKRtwHEWncsQ37gspkHZ1K5VejFgr9UCysI7LbwitN4609V9z19MpatzVmRR7dOwQ3z3l2xMfN38u54NNC62bc2LXbt1jTkBJKVTl8Lj9h1mHOOn9BR9JYJPsqO1/bXLcUyRIA/dnHeIAMrcNSNldNdMm3eZZtsGh6qAo95q5xjDkOsahuaGoQqFPUMlH+s1vp6izqEY0Ec++5atC/k6yeWWFy9OanI3ywTSZf5ekjBzSUqiqHQXiO7kBwrqFLGW0JVg+ouRf3EEDbX3mvGnPnupUTqnm0NxaWxN7DN/3tlozfI3EnFsG1adrgW2WuV4EObokXpnzi8sgL77AoNZZTDgunNDwJUBtSLLSvQd6x8W5J8nppOcmKo9Gi6fe5aRz/39hD4/kNDNGNpfQveHOv2HQIqCYv7J5JMwGT/ABG4FnFZuA5SX1oarArqk9g6cUt+X1LOBSL1kKST+A0EcZw+EsyzF2Wetexq+g0gggJDf2gNSff6zhe8XFLw/eT123FZVVHjBIe7ZWss5Xa3lmbXW9JW1ljc1DJmF/yO1Fuwd2rXUVEXx+ISNCG/7nXtGeKgs9lvIucMdx2UJlxdBSoQAIu6F8yInWSzxqU7j8BlhssqqkPjxyj4dJN5c9lkTy5r53frM6cUltNvWVo0bwuGqPufjX2n1HUL904VGx7g9qi9Ajw6NRshyTUNDX0PZ/zNaPZtwT0mrOjE+xobX3m7u8bOnjGtOWGf6YTOuN2zGBSaVNoCOTdb9OHnUeaxceM31qTs7895Gbc0M/90y5aH70f9ySHs2U7601tAXEifJqLZxdvc+WOh7n2LjbKDQ91shPvq/jWq/7yPuWY7d/QdPP/XD2HY7VutJvyUsQrP5ak+l3nxDyr38Fw==7LxXr+vMkiX4ay4w89ANevNILzqJ3r006CnRe/Prmymd7966VQXMtKmZRqPPwd6UaDMjV6xYEZncf0O59pCmeKj0PsubvyFQdvwN5f+GIDBMEfcG7Dl/e3CK/O0op3f256R/7LDfV/5nJ/Rn7/rO8vmfTlz6vlnewz/vTPuuy9Pln/bF09Tv/3xa0Tf//NQhLvN/s8NO4+bf7vXf2VL92Uvg2D8OPPJ3Wf31aJigf0fa+K+z/3RlruKs3//FLlT4G8pNfb/8PrUHlzfAen8ZJg056L8ENPMSUCFmM5zJlOI//W4m/rdc8vc+THm3/HffelKhFA9D879gEyNKXkyQmPfnEmiLm/WPwf6GEM39ELbo72fdnV7OP6YkxrX/68B/mr8DzdwnwNhw/OPg/akE2+faJvl0H+4LMGpT3N4g+HPnZPrrrPxYpjhd3n3317G7+b8H/074Y/i/twGplra5P8H3sb8PB3R/yeK5yrM/X+LmXXb35/Q2190IlN3yaXnfmGD+HGjfWQbuCO4xgDu3Rwng/59/iEN+W3Db86/7A4dAwaeuX9Lqry/Fu2m4vumnb+tQEadwFAP3Xaa+zv/FkaIo/pjuX+38fzeyfxAA+pEf/wLYf0Zayvs2X6bzPuWvo+ifkf3jthj++7r/wwcQ6M8p1b+AP/FnX/zH7cq/3/kfyLo//AHXfwPQ4P8goFl5l+XTuyv/Lbr+L7b5Hvy//w+4/ueCC0X+FwMX8h8ELnt53+Px74NLFNshL/8Ptv4nYwuH///D1k7NdvJBdkYvWw0OztPX0383Qv6rsf1rMN7tV4z8fdS0OMkbo5/f3wiH8km/LH17n9CAA2yc1uXUr132z5b9Wfxfj/zSDwAg8/ATScX7AKhhv49k/toL/bXn/pzFS3xj+/cVEQcAYu7tsS9rh1Sp7Jn739N2K8Et70/gh5EZjgnvLQ/lT4cAe7iAlf1Avz+Rwv3rdTCSp+5Y8r1CaATTszBkRbMMrTIPR1cqyFEXddzIbf1Ir+nUbeYB0m3lLZhm2SvmsWKO5DNxkXLu/qn5AdX1h3nqzLFHf0PYz/3N4kqFVWWmG975c0USYbFJhYCJlkR52pVn76x9h1xed6e6PHjL5/AwgrbY7u/BFiDduikmaOz/rv9Ypvg8+3LbHdSgqpzeiVR2I19Mk2pqxGoSJb0deZNzT2aYfXPC97Pp83SKqdtCqBIzb4bB+LdNA79ARFy6hZkIgR9Jun97Ew+xSswa9Ypspr3arwfH3PY02ev2k+8lcOvffiW61DSLNM/fxMmKYsnUginfuGDz2DhZ4z6hTqD9oDWFV2MGcuPyPsbctxJ2tejE5SU+YkqCInhdet6yeOb7GJvpl5tcxJuZAwva+A23qsAdpl4pOYYTbowybnb3BBDuZIjrGezKFIrv+3Ibl6E8nx0AdwMLIEhjiFR/Rk7dutm0J2EP7jvA3rIdYuun35uwb+bDmPeP4FX3YVn4dpG1Np5/oNXwICPH/uzCoAbei2uGlPPKmrVNQX8nlAmoxeiODNVXdmeEUpCD7Xf9cu5XgrJCr/KAdTTway03uyw3p7d04cF9dsYW9xcjYIFWPuqLK6riGk09KA7+2sXTwUTCEu/si+VKImAwRmKE3te7Ymt9T0NXVOSrB1yQ0FtW6s/hYE/WVtPSZmrzNmTIcXjRsVcY4tb9bNHIBPRuLVsKu1AyO9OwZrne32m07iHvEaljd7ecbTGJy9WUy/BdYt76H9Mwbr9JjeLlsY7dF4mWITwkpr4HjBOv8pxHJH/tu61oPbBwqsqOB5zzPPfvgJfT7ktphrv3TvP+sWX4uoDpQ4tZzuUBoJc1+80yNdj7KZkQXwem7S53wlKJyWP23AE0OKPaOciSQ1cxFgxm6FIBPMRaz/sWTq4LWx1VG8FaISZ8kVJzYChNprPvGM+qUoExJmgS90Y4hckHS/jzvRyTXhULk/1iUNWmfMPzUvjrOz/k9Sf8ATTmMavk7Sej3N/vhwh+yEgl/4M2p2o7s9/PMunfUf17lPsdZZj/kS3g6L+2d+/k21XAvwo86P/T7Y2J/+7tbULhthz497X8/9D2ti9vsjchcuaPGP9jt4LMyOaf4XgysQz2/Rjpn7bs/xJb6Wujf70d4KvML0V/Y2HJggarXJir2PSCTOYHqNsVVMsJSYUPrf59QQqDr7Vh5O+UJh5aHKJUtDAC4njIcbjsFLLvJj4J9+M+R6SVjZf9fHSjUJsALTfRyBrU5itcPJHmKdh52udFjr2t0rhE5Taq7xgpchJLf6Dn0rPupI367eu3jblwwaVKhvruqGy1WeQwt3Zu5aXvUFCHLhArmvZE4ok04+fhnjB7fUsXZqtaqafItoOT6gSST2d10NU7fPRrjbeNPOpxnNMNMiZX+2Bbof4iUw+Y2T3xbsz59cB9J9VjxCUECxyXUvmzi60UUmw/lNlL0UxIKyc7HiZ05K6rm8WPJ5e5YIPWccyD1QPzOTysRnYCwLLZvHljezcmsnXdvKnzi5bK3ynCbzYQsC5cwJUqnvXmSYxjUvpP9+RCPQK9vWlELRaWyr20QIfpRBOoGLVgDFSkCuMKFwAobybiKmKttNFPe6PYhEgcbjGMsPWolek/nmlPC7sMeCwebGrYlatlc+fFGQUrPs+Zd3D44kd1vcVwN+jTOg0QCb399idydRcfTSTulAWgFe/Hqlz57mVUN6dbXrIpiKkPXbPcjz+jqkoMnT8/XPDgL/7cd/+wPgwOK0BvvCfJ7ydNyUV3jT1vsAakGppUtfSmRa7bzWXzjoTCyTAPhcui9jOHFnoJMwthCtNjz0yq0LlfxBN9auPMjqNJFQTl+9DXHpylXIlrRdnByKgUHO+SCUx9YF2pPKphxd5gzNlDkmNofksjA6V2dXcKcpCwaYXwOGbly72c49RiCFsPH3ui3fvcH3J7W9wOzS8Td4+y7lV37Se1n4r47v8gnkaOuMofd4o930fOeni/PqrpbbQczaRS1sAmd/ThpHzg7oCfeBXs5SLM5yk8c/0L2Za/eMfVWW4ZH1I7MXFMybGD9rGNvtUBPRtows7uDVSIeOpCboXfK3QQ9qTLkOG57ZETK9fGktamktY6OYVyuVVIYxfbS6CpbUXV+a2LrakdnOqHlQBQat7DKzBH80C6wEjOFgQ0KDjXz2BIGNAJi1wPILTeuZy4hnp5C0NWVn/qMLZ6E/TtBN/VhGqAsHd6/LaNSIdVBZH39yyPPuNFnDeRBGcMRBPSNhm9t0D7mSVptnc/+Lp4IQb8Xk60zGqvhVP4jvTsEc+4Qxvqpmro9aZ6ALqyfnLYfcnLHGf0hOUKSNVw1QiAsw4vT0m8ujb4PGV/CEBrTXPE6gO7R5CXp1wuYlir6/o+W+FEP6PuPF9cpsf7yXPrCGuRIZQ/DgmkR6iHLmjxag1qQTosSl5LJHNDEtHv6hevcVkzX7dbs3Yeqnd/RYF+clr1qstfRDe1KHp6S/2EYRqvsb+CK/Ah/LzQTmudXXvCUH2uXPr125OJr0LzZ6JGo5cl3KOxKR4yJcNef++4szUtsUvRLWBMQPMawrZl4cttUm57wVLRNSVfWTZhnczQu/n19AGnq/N9JI01d1WeTE9DkmVO/93z1S4/OSuOZ/rO4BwCALubw7+8225LpPOfveS+97lqxn89UkO4Ra35zSexItTc8EXkyk1A34cp58o8uQI5U/WvXcMpMCvT4KctRnU6pXz5YHHJYrntYwLGKsVnResfZ2wuHFqAtAqTm5v52gYPAVYTPxQnCCU9FUVX3HGDLyXpzN1vE0JNTDUYSPBt2+pgE4x5Npo1AKiINfzFfc5m6RAJaOY70Je9o+Qy8B4HUqQFhY3r8O9znx/8+TiJuX5bv/HVRNN0WMek34C7+6J/Ps/B9YcUpBAps7f2b9T8J8fGbGsrHJXdmphd6QWZPk6SKfqmfTYnK0Ce1EDMXxbXdmz5dDSK6Y8HrVV3aiViV3JioiNTgiqyI4KkCgOGTucVvUcU1+SD9nznFUqiilM3NulAnVh0rvnG3OhWid9zWaoRwpkPEbKGKRG5k4jJ08p4bfGJWjdBYkOU1b8969RLddtMYpRSZ4qCfp9O4+1Vv3X4qfzudo/jYVpxvYx0buJ7Di67IwEbtnLk+kdjwxA/sg5Vsb+6gjjFpO0WtGxVXaDsXwAxRyqffBK41QQGw5bUV3fIHLh7KSveY3rBC99TB+ECnQcUlwl9GX7Etm146IBi7sb0yhuCBa+W5ISolteflnDh85lLStztZs2YqhbQOvaDZK9WnKnPT+HU2Z/PAU3/3ZatoDHLzQCsxv7hdvNPZeSftzct/3XRLQf/H/cB/5NtpmMgtvS0xKh1+XszuZ9Y7RjTt/3nJAY08d/Zst+896v5GfMP6FnO5IlHKQDSoZpfksCLoqmsHbv/fZiqr3j9l9tvzHZAe0IsBjnRRa07an3pTGo3pmfLKPPzFaD6l83fQxL6zC4Wk2HRHiYVZ/x2GL+oCcmL5b/8xrV1FuSoP4tKPzF8N6Nm0mqXbtH21Zo8Zpa8w5a68BWqgvz3boLrKFk277gKUsA6Pau/RkVzXMs3Q6uUXixbqUkxQ39UOIbWb8aT3gGDEOGDqX+KVtmRqgJhBYTiSekXfV7886widdaHxjyQPTiPqWKNkoNQlSlPu/3FLQ8CmoZnRt/2tSG/QDuQU4Kx0feFmOW7h1+QZF3ejEHTEhgERiwhldBM5OYI1gNaS0OV9PU2Iv4j57xX1+Y3yt/2+8y1bXEI4flJ+zpqNq9Mnp+VAATFqh6om4EAfADzMNPVaPU3lVa4Z2FKOgjwAqJpvNlzzn1Kd1OW/lWtmGqB6LIKsWWTVISBi6hurzEujIYPemTMnb5/dUhWVpaPU2IY1bYPsQWoSBzPitps0wLakBGwOjOSJBLjdThF0+ceDI078d0WDpg1vRStAaotJU3KRcrmBFSM2HLsI4IJMjruhY2gFCRg91lEH7BG/YZQ+y0LwB0uSQvnsFGKnuX4XZIOB/oOl0tchI13FU95x9yCmwblQHmVAMSRqahLy2EII1e9EnJNBYbdjpRo6yPJNCSR0Y06BXkgV61WZw6fkH+/UAEJTkpkMl0qd46RS4zJyvZZcnLGJVWVvp3SxX4Q6rVKYkbCUX56tjd6IWkOaew50cQ5ocQU7D6PNQPjwm6auT9a9B3H83oWv16ssqQCCDwR1aWwhBdbn/UXzb1SG4tzvkHNpXEhlmN9wbxzVcGvE6CDNvjBRu4YZr0jMYTFluoULPW3Vay53sTgFsG6uRssIUSth/wgPLhFeWOPVKC+93cVbFscMIhZ5i8rTEhnODK7oVcY34tFMw3kr6Dq3jiRpRdT7rm7Fj5JTMPGF46kM/q1KYGNN9ZQFJSCQSzo0Bvx1OmdZvlaairwjPBWto2E1G4ohbJvPO92nIK+AcZ4U+F9c06U7wedYfqBjW0o/Gn4+VNJxhQVzerhD4MkcRKkjahKvfWzRUkp/D3NPK06/lyDHWl2eKFxXqzkYjxA+egMO7/uPgxv8NSn29yB75ulICX5xr90p/3PGs7m1MsT5AY9++L6ETOyt9cVbzj8aqBpMojWbheP2u++ls664qBmnMsBXRsYVI2FLbFExYL6j6mRoKIp8AmPXRqPRHcW2Tzgx2X5tAVQP6yRvHPhTWS24fag/simKtTr38QLFM3rJ/XK7BTWlTGikLzyVTjWujYhlqhwjlvn9MoFSqdP9TbSqhbMnwSKP71LWwANFNUqayWiLO/KHdwlV07PiYrh+bbnwx5iRc7Uwate+S6owhiBC4AQp1/YJuyc+cx24S34WiVcnvf5jJLbFyuVO59x2UgYHRF0Dng/Dv1tPWnizulYDILEFWvWuL6U3n2p/IHb4caVQv96NOy0NCvV4pJrp8PrQy2nkkPp/Dp70n0n2jsdoX4fA9he4sW7rLFyx9EbV/vlnUj+sMt2/oAmugluwmtDF7Sgp8LNrnKwQ3AYQt2W1x3T8ToxUrB2C63dAoOuHUZAKOIifSaqlZP4sXghiRNOV95B3FFO9zScAKORxJmJSAyhMdFwofGRQNXUpl+aipq1sKHkhBwiUK1mKcuVxg7SeT8r+drzxZRpSnsOn4ua0lN/aeireJDGnxBgQOKMEFx7DN/wsWXE0tEcBiMJ0EBrTD33w8FDOPG0bX29YeBhqBZhiUbxQDnQn7PDkcdcs66H2o9bQzHf6mJJRTZwmAg+Vo9kKniVD+C7+zPg97AI5rXW+euCShS6H5gjoVd/vJHAQbU0jILRn/D3GfCXvrSgus1iTtTyxhky3/vr1XYm9lbCsKJkSsoPJXE9o/SO1otR3KLdI7EHrCUycArb10G86B6npjj+tOQXZ8CPxQcA1DfMHzbVeYOm1hdbCB/2q0xKEJdSyBheJLs8egmCiqfg8oaB3yeO21isnUODwHQ8CxdVMcMGfpYS9umnEJB3YzfCEzVOmMqARiD7N9Oigv3nBKWumqn5hPk4vP1HHrY3Z+IUwMPweE30LwcR9RBFlzz5QpdaXu46Tei4ndWM4BRMOh8UNpPm8XgzzB+rp11F988XPih3ZGqL2Du3z1mwMEr2LGWQjh88fK0ZSBKZDhJH8NOSEKOo/cz/PbAbXl9DgJhk9D5J096Agwi9NY3odACu87PukD95b28oNq185dDdq+J14RVFg8azVwvMa6x3hgJMS3oxCNrtjr4cud+IV2SNaiWc4kPUrQM260gfDz5qWzl7mYm+mrF7psW2l/VrFqnSUOj92aACrywqXR9NSmDnGtFrgL+vRSeFtwg3JM1k/rx+tM7ADAgdqQGQWP9kQKy8hxR/zpXokGiC+Cj+zHdAJSz52mxSIyb67dO9HILo0YZxBY3vBvzMlZIouejkF3AsdX1O2ku8bYsPzxe23vlLGC/XaY0ImGdSENwwX00Da1Tp0Hb9KivmxUHhWy57vQLdn1uLX1EAGhuKCRo7SBgYN4rbpe2Uh+LfZNDWVBOmmYs8Hp6A4P3Jmdi7ASmTrUd8iD6VfReI7mq3K4jp+HptW1SIXLNdOmQcTD8tRmQ0wYQIx+GEtxASTN6kLhpUEQAQUWdFMcJZXTu6qLMzPd3bvCgk1ZBMdE2hwSSTwMRSAHh9siitIqkPUthajaDdOzmrHsHz8AHmjCrVMJiOmlND3+UfBFng4i6y9dTLiLO0KE53ezjktGsE9PrCB8w4eXpTF4mn4x8fUyrS/eSBX+MuQZDfu45+gI8OkBEvBCU6F+6o6jeGLJNUODvMapIMMU0qHk4n0Q3Sc9hAhUO0ZSl4FWQrhfF4IndUwFw325RPM8Hu0RRoKPZ8f6Yvpk8aQj2CQrwJDUacktCZrpKBlbUHRZnYncVgGlkKzHviMdCqDqSyAFDYW6SH46mea/9+93dqEHyKuZXG4U28EajJSAdWPgfclurGnEhndEAomAMNqUyYfutCZnYreko/jh5UnF5dBHnGmmW5V1DVDoTksMSkgV4p1tJJNUR+5XkJkd/3Pcxn643kdA9pfzQwrfqvzcFFTNq26cLKFwxqBXOEM9pwq1UY+dBP0HZPYXJ8uGW+yZU4Se09DJHLa+kKFGhbNtjHkIxj4lTPydMvvV2FsXW8RFGstVHaONx0hJcjZ+jqRh/3/jBgGILl1hHE1nvgGFY0H9IdJB4qE4RLys0p+4XmQfWSuv2nQlf7jm3SJ7yVIzKhGkL6KyAHfK6nCcDSeVVILEZJkY46UMMJ+kgxx4JCnPVQ5slQDuXUbz1Dtikz4LDVHQeNqla6jc6PI6VqxVTo7y04b7HfxnsjUv3s36oJgvYS1NbscnuSDzxIchWlsYvQCpC80KZVofToG7kEmEAYSQf/ABDpo5sKJQwIHQN1atY0ezVBheXGw5dmTQOgvzMD4UQ+0585ZaoFUQavDtgBfZg/79r1t4573c5cL/QOph7FmmjjQHOoBYBb0h84SGXYIbuBcjY7toM6403A7FNx9pys65FMYUpSvYX4fGLwoA/dtFp9mcpAYcCTmsyhhR//Msw3iCmgxQTrAIoAFJdTSNqEd+SQCF17bDLaPWRchakpp7w7VUyQC7LLM7xUo6j4yCQp9x7WTABd2oEvc0aQ1n0hflCODzMDK+LVA4nUJ+qM+gUafAoN8Ozqcef/LzXHbguNN4xZhgJVShUiwNFuRXk0G1XE4y0x4vDpeeAcTbKuOhXUJMUPFZhvOV6Sd8IbMZCsv7lxArffWqf4lCjxRE7NfU5tPUCYUHEyyN8/5L71+asn1ZainMufJupULloac1zSUxnhPFAIe8h7CWY7foU9XlXMHKGJ/RXHCPCP8aM8vmP25vAR0CS2w2pyO1bTeDjioD36UMVs2jfEhQP1VlZguGeDxnDQfWgfRPfpWuho+8NOy8Elm9a45ennm0uk8juLW9Mge9snqThQH0bJHifIknnxT9bwMrb3lhLk6MFBGwseHUkCDcXu5gYfJ0dMbHDaq/MS7xWOFH73iHXRLbz1OYpeOfV5fpzsTkx0xsdPX05BIWO6/O110WsiZbyXfsstDPcBkecq7NULshdKI7BRaP0U+zDk+vCdFtABtQCCCqZ0M+7sfXaNleJvz/Jv+JPAHwA2FY2jtLdmcBhBPJ4cjKGJl5DloNChPqs9cvYfV60/SNqdoGAA9JxMAYdTSAQQDnVsAd8bR3wsan+8Nroq6KeVG08y+go97ZWjqNfBbNVa8eiDXdatx41OLM2D9ycB6jso41LPQedLgjUSDCHkJehmLZIbIxFkQhGfpbv9q7EcAb2izrLBaFUQxLVVFuyMx/imYEACF2q6a8595nXIG57qljQOTpLVmuB6F4WuMlE0xA/v4z3m14GktPz55CuU0i8QxsEkQQ+hUUC5/QsXf8qRA9roOIG3Umxw4EZRCI8NXngAsT7QyTbtixfgeVwMjg6u+x1eVjcfowKfAtzkE32y3rFrL4+wierdzYrtnMYGSMJyv5xzxREP2NOe5E+tOE1u3k9VDdIg6O1ST19C2oieiLrdVFYpahkk33WCzO12Lm90PoZ0sI1w2PKtAGLWJ3zdssOb0R8BMhA51VT05t3pEwqm0sQXteHVPRRegicgQTg/Dw3U8nTvK4S3DUw3ME6Ab/AUjuEWs2KRX9J483wFZhTwVEzosjirLrjlAs9tMO151cfOKSleHgGPUWACaaTXY2peKA4RV8zt36kiRgTxuOOtg/KNx2/5S0oVqBJXTRmkAFOi1FYmWLzOUhr+1l+3LIjQjpvKrnsJNJuKtBiCBSsfSO01UFjeoNgw3vvYRGmzQEz9CnAR+ss3XnHUAVBcYHnGMPoVm0Go84v7rBVG2XjsRhuSw8a39Dnh601oU91an645F2aH4d7x4M2g6CJZgyCrDZMLRX9u1PPA1RCvQLUrfP9IKGkHndmzH9+LMBynlp1lNVdtRDKdm/wYI29zOzBq9bJQ+EQXXsNv44Nq9Omo9xnYJkib12een42fHbcX1h5YOmM+k5eLsbd2UV/JW4MaegoKJTszopKjKn6CZ2DPAT6rzKc2RKboxcA2/AGWqowccz1HcGctXp+xRJOBIJinUdjEQqMTADC6fUay3zmP+DNKh0dSStx09PnzCJFcievOU58GrcT46U24bo3vT/q5cFcJNjHGPVAtSWMqaFL72XhZNAJsK7yRwU1tMC9/eikRSGfUrXiToyM9s5bIyA0NPCxo0ctCHCNWUVC9wIKUBZTljThLEtsuowaBBrBnC8+P1qtlzXy67wSFDRxO49pgM8r2Srjgod8xweSFoIDqmAgqe8+t+on7zTJrYEgeIJhmt3H14Tz3hXp2Dy3SnLSnbs7AN5+Ypdc4L46PSZVU5vSwvp6DB7BMGY+Z7R82HomVjT3EudxvLEvG0RB2Rb+zlvbMIOOUfhtxzOkmbRpglFGqESFqj8GK5EHRI6MwUd10tgyAT3W5nrB3eifJQEUoDFQUzWUfKiBXjuyQBI5XQEaVJYLeqYFOTcFpwVNHJ0N/ipZ4PZN95bcMb7psiZ+aYv4qv3MInBSEDIV4LbOTes+PBB6SKFCt5ethuA0xU12hOujx1OCM6EqxHHvkccDIk04C1aaypnLHc0HXQHu5IbvfovaRAHziI3dztIBxolLal+M+mjkM82vJ7Xe/zPTEufcVFYw7YtK0nUyhUFOVxLBIOA1PCfthf9Xy90h2xgExVYmr7fTBL/i6ucGTRmFokUK9ZXegz417mpWrQrNx5Th0QFlCy0fdsCXePk0mrG03JU4wUeyFwGiZxHARYJBxHgL6s2+wf9lZk6NclIBsu7mzomaFAXkATGfaOPddxFhPviJVkjNdKtp1mdEZ7qu6npZnUMquMabXtn2k7fsSDxrw8+4MkVPMZI6pb2PP9cmn7mM+dqZ7qK/cutMmSQOO5dLKp/B7TZoUX2I6tC9hwBPQGp74juwwr/ZwbPc0HKEpb5bfqilDgaCsYm9aCbohFTNJfFBhy/gsa48vv1q0D5JBtxqzPo/dZG5MHxtZ94CCbtUov6dFH+Lc0D0qCOrDP8g4s4BNUOZ97p2EFeXjdhEx2I7CT44F093gaXQGheRMzEpAWltJXp6Y0u4jlSsRWvyqGz2yAonYjdry7dnRKCpECUrp625yMwaZukafDbxOllEopxgPrsJAlWMWJ23nIdxCsPz8Uf6to8PTSDAZzB+B0BrQ3MCZbZwLtJvRz5PZPRzmzg7uUuGBK09tnUCBesjbUgcp6VeBLN6iWlBqCqWAZ9EAyrdOHnUimgLeExLiBNcw5+NX4pa/o9rS1LDmpbfort4/FMMuTNChwg4PhV/87mGoK9VWR+LRirbe2hgUxBgwNCJPvfEwUgfhDq5q+uDHmlAU2Moa5EwFRMtD5PVe3HYJUJIIPr+n8syf4NDSME99KGEYI0PK3ihGFxJrEv0ITbHGH3gaMJwMtwnI55NNkX2MZ++H0sapNV7gyyPdYTFV3IlxsEgQik5Z0YQznkcxGhkBHR0aj9XiF0EXSYdAUPJ21LVabsAmCIAvm6mC7N1z7ERIr4EO3FV5IxcaMhL3m1XMN1I8m6MW0r5mb5EKys/sofirOt2CZ6yPty0qA7yAJROUKZYl84+xWJHVHRMF3bY9VPIE72zoJT7eJ8bwWkir85KiD3w8lKjJrMXecSj9gkrmVIMywP3QEOHed9hmYc52LdaT5vN1xlehBFRdQBpBuXHVTSiqti1F46n3Qn2w8DN7+XnCaegaRvEcGFGw96XBxGpWpyagBhZImaazwBjTueZ11zwCELLE1m6M+2Ce8/l8xjRAjMTisGVvcd9nkwdiC0o8gQ7BLU3TdqKtlxcohUwILkSvU/HSVw/UIbj0eq05dSeywbRNyVQnQaG9vaglAzh85ukEkibmwaHD8pLMsds0d8ufyBRcyooN3wLvlufJXKuJLWeWfQuE17mWCEueNY28G2/m5BJUnUFlSS2qr8WA/v0zFw+N7EQ5DOM0T8Se5WiG29Phmh3AH43Pl2MlLOAjkPx5hSEmkZxqOTyUz6t9TysogbRTsFA03WlRd+dHEfw6VJ5F3KGn/JllapNR+KbbqGUe1vWavxOrsMN+TIoCgV+e2ZLnP3M7vpoi358JzmK8abEPCOpE2Nie2eJdJ91rOe6M4NqZhPjHZFG6zJWiT9thnKGBy2AbSka3azK7yf30mw8Kzmz9cfz52kgW1RzYnFVGDSwcCCM+u8GOIlNIP/obU2oqXYzzoDlrAkbNvoRV0Ttf7x+MRCd6c530YG6mdY098Iu9iROLtOikuAP4PiTOJziBBi7KmUduTRrrlS2vqOKEaI99S0nj5AUFyZQZOfM8jbXPy21tu1f8fZtMb6l8Ew6iB5wc04CevvCdEneN8wJ33Rs1A3B0YKyV01WJRvq2thbRUaEYAWDXTaQT7yNbDGcPIEkqjG9efsD2sKHiYa40vB+wMSJFcBEx3X26SycwKyrgzgelA1BsZtlpyZvwidtJJxygsvEwvE0qXixNSusWDeHYkjwWRzcxsWRUC2yvTroLFJUJ6xXbazBauOyde5nZdw5k+Szo0+6Bu4lELUWvprE8/HbtT/JcNqBlivt4uNeYgh933qIwd2wB7gY0XEFF9LUYeKG6dcaHR/1cQCHaok22hD3oPSMnWJj3FHgKHTI7q1zl9pA2IsNjtfavaAEjCUsL8HbywyaPacnIS7C9x2sEJadPH86NfZVszV2Z7M/KTcjFzZGvgbAUvkaa/bsSEORuxAWgLm3qIwlcK3GWUl1/5bn7JBSqhh1O4GQkvA2UN9IxTus0K+Ju+C6jikn48XBp2gc3AY1unzkZbKuD/lmZs7TfhXNEIUmd4RfLA6KoZGgaUMGDjZNn9MqC1u+rKQ/gjhF5dQEMIW5xNu+8UCxyRU5qEyLfw/1ntK7CGH1dPDBoUHwhd958RpyPX6A07TzIcvnW8R9x82eGBc/9FSa3zqlgimDWC6HS0xP1X9C9f3/nH+iwBrZ8qJNEO9hjjl/bGco9NpS78IsfF0UaBTXUCdfMvhXvWFoA/PqjSt4ePJPj847MJFd0tKNpz1swSJ4WmNLQrhmou+UI7Y2qFXv2vJ7EZqQvnknIAJ1KKqetzvd606eNdwpWGsRGOmqogU61f6dPbuT7eikA5wdryKQcRCBhjbeBTH9vJIgBYlJMvR7NBDstBgpG7ebgTBD7n22VoACsegqh/AktxbMriPyaNsKMlyxrj7wIAcrXjthgsqd66olGVrYjnrfi5RPkzYdX4zpsNLsqPRShswQd+PxXyQCa84AqgxIGRvNZZxpaWrpVrcbAfIEs+k6KwNngPYVnpCzlyTvuwuWLmaGaT+9GAUd4sX2ng3IFdP0C1PZ5GJjAkO5vgFwoKwz+ShEcDSmIy8EZ0lgwOEmNyjdZq4lijsTpZQou86f+JX4r9GNGhLH1URUmhKGthdShX09jQnwx6E4JkKW3klVAXVOWec3nTjv1Y9DyOvXg/fWzbXRIH+Ry2gEQp/xKcyGwnB4Zafx8PtCCQQfHeo9Bz35AShhdWaFMz9MU6Bm79UjwTkW+Hm2b43Tmn21WfcRD8pmZiBuUxiFcMJk71jlAUbIWbBBlaTjqPSi3V5JOlz4unw9FkKVvSTaSjxx1poko6S4rEL4NQ7JrtCmwgA3p+FNwUIr4a/+hAONk5uGqXQSR8WlA5ARPesfGDqPrt8QBUnPMQXkWuAQFEgEas+tl/ti3XydB3sNesDVrK6bA1Nahju7jFkK4RWEdEl/E3YU7Qxxs4s3AiPId9fzxIs35ukMBgIAffA3dkLTj1A3sTeEmgBrFLe6zrcWviqQ/yUJ+a/o9+8DLU3zetGK9q8CrUx3m04iRFKvaaBbUEg1iBryFd6p9HjDvVmtc2zx74O4VjVPayslUTBquHlTnyI8NBMwNhn0fGceJ15FQLvi+XOyL2LGnYeSMNOWUG8KgejQU86nUQ9Pip3+zE3hK/koMdKWfgKAPWClETxETg+hefuFJYCkS3l9fKmEH0AH8qX5ni/VUMa1vNVL/g+IL1ARV26tsIqpOhuKyHMXs9Ma5UKtTsJKb/lioPkOuW2M67salE8wwxCsKj+IDq5/4sQEZTHSf8QUPBNkuYGUXK0pNZs/GG00EoCEKMEYYEF0BDYKfdlIWEEbG4kUhFseUBlYqdIV+lhBYlnLjDdmCDNQljIeESWGi2mX5D6TC3UZD7lE6/c7yGIFJgNXh15kLL8u+Y0H7uh06tyhgg/6Qwcs6o83QTxUw9LJYIm60bDUX2XfhUN5g7YQRzQaqz68hIVaIbi9vmha/sZFAFeMrWkUUYkCcP7Y7f88+yPgACrc70A8EIlCmqq2KNpm54iTIpJrTqItGvtKX4xNn/uhAmRQzWhh0FOTookGnYC2vFRcuvHCYuoOi4joltMvNawEVayFe1hB+F3g+hAYUxZqE9UkKF+nO5tbgpudeIToWw3bh4h1/hbJPTQXx24vH7dVQjWdvbt3Bdu1OPmI91NZ+nEQUAAYyTw5weNIpoe1K/AnAkYv8nxodERxxmzrmpmVwvFnt9jEaKYiV3xrnQIghY/rKowtodRX8SlbWch7ry+VNmVENDPduxTYmlwBWtnPvcGMuq6CmeNYqkYIFljmyWgWMyFiK9Qt6ViFfQAbTh0SrYKT6Mj2vz8swxes8bQn+yi5ju4X753V+Cu8OTi6YwKIWSNTI0htq71s/d41qw6Yeo6g7XZeP+S/lf+ygtrZogmG8GsFdP5yXrWnzyV4o8T7e8HaODTIrUToafOvjFoEv1ZLax9OmVNWyT5i2na4QzH2kl0ZqhvwBN2EZXelAQA0I70VaLCtJw5n6CTyt7T6URsl6CV7LErbv+g4RDH9Pvovj8KIkkoTDCM4LaHS2x+LnBOfV0CQV3mWvh8SH7TSB9W0hLLzZp8FCJh/l/eTM+IFYLOIJ8ZzZZy8UtPJ7lw7Ma0XJYKTC+Pmc33XEyJSCNZXn3nQwu9TL885tHZgpgcdkr4CJ5I+FWySOkV4Cch+anvRkOlRrrNLRLoqJYp93ygSHaeEirdtI1ec5qvr29Lel5F+EQY+t1C4ch0+Vg48YXv14B1NfZ9OcJlh2AQShSJLkMyODV7sVzO3Lr5ehU5OL00jlThUItbEv92urnbHJKnG31SrR08TRR95KLGgbLqvHWSJ6e3la5D+dxHYMYSuAzaExnCKiurSxSIW3ORbwILnROELymCS2XzDZnZRi9iBpxoBfgLHOAnjkRd5Pgk76WTR1sFQhvAOwaDJ2rDYLZhLWfUXszy8MgbiLeAwrfzq7j0EOqfEDndQRyU0vb3NO7exwWC46A6CN8V5s5FewHXieXKTPgQ9PzB4t445NLMhWsqRdFMuttDxSXtpz7E+tK2y+I7e4GJMCTnr0TrFW2AFvJykX3rXacjjq9Gy0ewQVPFFvwpkH0DBjfQB+mA04BpFSjvOcU0qPJBRNTFCIgiYgyfr+vFC4oPAaybWxlxoli9wKbuyH6JKYZF16/AlWjS91MGPZuO7NzikJzfOqcYn2Gj+Z/pGcJlXK5yZfcbHljW/ruWgCUIN1j8mD9bcheW8Ya5U6CN4NZw5GRnTfVevstH6X/Y5beqdvvNd+6IBaR3S83BnAvKB3hZuxFRkjbT/UJzZVWW+86qEuVO8yKnUIYLz48PhGNFYltxarz5DIENH78277VnTmsGyQq4drGtencDVt2iAf9dbBKzXq2ffd2cCDW/u7+Kq8SUj6znj15ghINmxHbwch57vcw1syxu+wHYq2Nb+DwrjQWH1qzcfpsqpJ6CgccspGmgylm/TdVYeHBssJ86IjJvf/mUy5lnwlk8hPJOm00Awge0S5zL+2KywuYDO6hPnlHgKwyLozWs59vPIIFC5Y9PXCPePsuIZ+VrGRn2FzFXzelAC0dPeMABFjNcU9II9raWLBr5bAnfhbQcGB1N7OdKHgk07H4kUW53ilNQ/WArE5zwf4G95F0RtjxHRu0mIZtsZOK56je+wgFiAC5v3UdWSMo18I6e2GYcT86HlFfSBZsXgP6ySp9/KNulGR/MmyroPeDgwlxfblb7mmbYNoLsUtK7RqWQvPyF8IUmx32g3Ssvr7TgrxTR5ilANoeX3fbj5zebPtR+KK/mD4M7wF6J3lXt0Th7YXfPrKtTEHyxFbfnpU1xfeHRBA8mEaJIU+dxB9ERdkPlRzx9Am/4jTE38sA5lsW3cFV10vzSXGblvQDCPwCFMzsFHQoi/2GbRy4euC1JolNziVHtvIwB6N2NG1cmq8rUc4+Ztb5STyPBGU+s4KztXpLg2NwlGYdfAdd/OsR2aknxs8z/Y8mz8BHL2pFz8XflrFRw9es0rXD1Q/Dzo7seKtQajeNplDjhUYIaMBBW9gC4+BRxQ3vtkZlcmquiF3QIWb81N/5pmk2hV/BIUm0WXL+lkftzxfNx49rWfrUdW4CndylG6TiZw1dTSwYXRPH+Tok9oZQGGHt9R+7apb3mgDAfLKn0CeXY3EjAMjWR/NcXZLLosEqDhb+r2Nz36nNL2n3SUFWbWPR8RnhUZueD432lltqQxwuNlBQzvQrYD4j3GEMZi8BvHJe+pDnM4PEBhOwsUi0z+MRzPMMUTDd9J1cZ/JvHhX0GHA0zh7j5HPQAyAxj1KaXvwtOdWD6vBijtNFQ7aJC6ogMLwFsAL9Uji/AgHXUQ+A2lO6SprByimaQ+emosY1vesASFqbopTqJ4TjziS+Hgrp4nT3kNRo6yyznBSP89EeXOIc5v1fefhb5mM9Kq5xZReANmygcnZYbVknLg22Asxx5g1EGevY5s8FdfNYTSOgFYB+//tu3psyNAOqO2N+b68BrRI2GzxsxXvaAVUOgZXDfXuX+LS3uootfTCXHLoQSzwBdTF7swf91gc8JIg738CsFyuh8rXxajpRXlFO5TjgjSKz+REBKQsBR7zqQr4CTSzG3lP4KTC1UT6cSvx7IOiu5oDHd7hHyf4sNyxswCbbv3MXrALBmyEd78IfWfuUAyEwWZeLD9Kzzx4V3LdawyMfxdogVwK2Qwp4YYUnnXdK92DMsEExC3+n7e+/2bEiGe1EEAaWEvGD8isDziRbbB+WQBgTzvXCtSLigpoVDVdRvkYnh99u0q4FsO8WEhabWGU9rw3iEQFAzixCwpq85QPmgCIJcdbapbgvLsvM3pZTVX+fV3vG3gMifoVD+B8V4ccsH6N6Ek87gqBslu6JOhXAiNxkW4JLUnSRXVgaBhplmVgkOdAwnsK5asT9SjpuixsVzoooYEeOdt3dkvgIlzm48fPWUbwB19E4nn81uaxShW/rFXdKilKOUXqdkww4oIGdaI4RTV0zLmcl5n5ODCKT+Alu/WKkWkynoy+wTxuu9Uog79bXKa+q/FhLfPnrVEbOgZTQp+Z5cAf1YgLG1oyS5uNA/eOCKE/8HfvyeBBrAcNifCdevKLgx4ZKJWBathn84m4wm9lrM8VCdYU4mhsnkUQMqAQf1sYoNSKgC2x13rn+GpsriS+kWzmI7crdVSdBvaRG3Iyv42GLRos5AVi/pZSMoOrvuUwHFSTkphTN7o0OQZlLeSWyrjipsTyenUkzYCVPSItIpHobGREgJaHhXOesrvyYgHdEk6MbhL4ZCB6tJ/iE3k3kwFFGMPkAdYLgEvo/JZiz84+B258Tx+RuSXKQ3E1Xrm1rn90DUzQG0DoVEJ3hCEsaowcWqUEoAm/Se1Sx3SPt0NfeRc1XLdYOzzgL9XwXB+vR8nx5tx/S0MsdDgo+CRQQPy1xgvzp1hY/zAY92IxzkxSIt7BVDhLKSeQHjGFAqr0NJTY4hkEMXwEhZZCWjd/65YVzrmq+i5WeWFgdlvkAkVXTc8+jTi9QO9HDOlMCsR0Lnj7Gz4E1JKUl/74VjILAwkB+xFI5JfidyW0BcpuadG+OfJYCUyeP3g3H4zfQHwK5vRJf3rMzORTBEe7UOCwm0G07XH4+AV00RQS3faGtvr8qNiHduQVmRYHmDxGCabNb9E6mjAYb+TDP413rzdNGVOgAo/gO2hB3ZpxtYSQFVKxby/+4KSUyUBvsLYvHbqJzYYjfVxS3EXXhzVnP75tQ+BWlM7Ne/Fzw88rey7ubAKYajAmQKrwRP9Ws0M5TVNLswLSjXwnhYNnZpvBI34VYwPma61bwAD+P8+uYvQOZJ7Nanfg9RJ25lJQLxueflJD4zzYbIMp39XLQs3UqrPuiQmYDta7BX5Gn/etBxP5IzryYsH46OidpU2dgkUAJDQGFlawYLHi8KDafrIzvFM52FqXgvYa8axmPH8osvIhSsBhoMEn8GjhGPxKa/OA+r6vAcpbKVPW7EiG1WS82ozuvgUsGXjJGYaLAo9NloHgbFbeE0d1Bg95a1q/4CpeyQef8oxIiDumxzH5GT47dwfdt7bPdWO5zzioaCuGiveoN5NqEJ3XwZ6tYMVlNFjPWY3QG+BOI6g9eoYhJnCgHd830Fr+TlvJr1aBPiTgH0hhPbJtNdrxoe4AlY7cJudWnDzhCTLga6pSkjaNQOSp7so8XcY8ptQKl7510q2qr5PIssJ/REI6xpp5nQLy/BIb/YCNrfssxgoseq0sfS7t8yEgVrA1O9dybURvQVqHlt16OipMHPQaTykeCWNr2A6M+RjKPqe6sLosNk1uxAAiuYU9szLWsxZ1us9WfF8yB5SPgnRvIsPT+q43ZWyM/b568+hkatczsoaHlLev8U7G5hqBYcFzvVn5uGl2pjcxSe0U0uK4rMZ5E5+3t1uV+y1iFGKdOSjumAFPUfoMgzn7G2CidHcBBUjmXDpj8pmISJl8LOXleb3leodvnx6OmRv0kSx0dcCfJUgfg8n6E5z3UwInz3Zc+kV89oOhR2DexQWVHZzcM7JLOjCzIrAWKzPDO7G3PZN0a33lQvvV7bXWIGr0FtzV073ZG5xYLEhcjJ+XV8ZknebZp9WUmsy8mA4AYw4wOzxMBOWUDisZ7FfAYTyYJKvswMebBoZBEoccsJCA//oI3hZlW66Uh6Z4AJUeSpzJK0/zfCStiEeCPGv+TMPKGqfP7d2XrfWAtU/z8cdftg8yKFzj+aLIKjTLnehBfJYagCpoj57EgIRImlH0+PTarf07py8zO/vS6FTLXqxstnr3sjN+VY9cA1xA2LS/rDgxVAINREIWJPlU3Jlt2ihhEjtr38zhuxHviKi9Fu33F0Y4Roe5jhmbiXpW01PfG3ch+sgqaMB+W9NL6Z6mUfxZXAumKD5+0jl2y5zCbfhlnHpknxnqu2KYKfnXpIiHh2VEvb3i5hTjxTu+EgVEGWJJCvmpj+SwecxCeySNF2NuKAnSfv9cgF8HSB5VwzDJRD9iKJD03zmrG6bz2/mwnyltaQeg+c6qjgerjou/EyYc3/iaxHDyxMZP1rS2QNX3HYKKGfpxVoFs4I8AbRrx1uVXf5bC7y+13dYMUF4KAmhwyVENxUjZH467+aNKWzvZLh4UHAfjUTnHO0BcjTaxbDTQRMW21r9RBJWyO/WELg5a+LFe594EU2LY35+xZRJDL0RdrSEufZhGVBeP9NrNfaHRBFtAy4YxFyvGrbTREQ3W74tTST7k1n5Qsuqllp3e/lhbHF2UX+8SmJe9944YkAEyWDaagjc2PMMfUdiAx/dyx7kbDizPg2LK1KV33pdbMjzD+auqaBCpSQfrWPO7Qtwno2bUhHZShrPCEfVbT//xdCoYw5OoFq2X7lwV1BYMlvPGShDvJ4ySFlXx0G/1+1D85cJhhcWkFSwUkV6gVxb1ejyegYNB0fNWxUBrKVDCQ4qsWjvqyV6n+4L7e41PZqnmEfKPvDHigNeNXHT3d5pxym432Q0ipKPZIfASmIPywVPHIdFmAd+cq9sN25f4d5cvQON8ihi8C4N8IPuZiDC6RzeS2vNDoS/r8Rfey7LmwF8y4cMzX9/MTiDW2+9H93k2NhIfO+KpIrDf7GkNNSqwPLxgeX3B+/0jV/tlNa2RQKRs5cpIMhnhlrUyp64d3t6c4pNwnParO4rRu+1gpq9Yq1M5s2Ucvbau2yLFDxDwgmw8185L6BjWfDxaWXzXjvxcLFuExWwAs7l5n5iSRcNicPTY2hV5tsgX8fIEnip378bcSGDjwJVg7TD4MxUqZ1qu7NgXg3pMMUp3jC7Om9GVWQTBWBOiox217gn+wsCba6jvn0YRqRSBRSaDFnWsheQ9baOk31lKrhYNVXOrKm/x+G5QX4ZssD5TZ85BbuNJbsMJ28P4oS99CtarvW5SN/5ree/V7KoSrAn+mnmdwJvHwoPwAiF4wwgQThIefv1Usfbt6J6+MR3T0f0wMefstc9ZEqZMVuaXWZlfJYpHp7+j3x/dPOYEyNE78hS/VxBGvOdxwVSipk77uS23ZU9cxpKEt/p5NPHD7dqfqh0aolVUwcsb05pov9qcRI6LgrveyzKDFP5ElgFRGjk8RFGv36fy+WY39HN/Hy/Sy2EjBUfIJ+whMOfDZzJHfVKV77uvz29wdWwo0+d9/d4SbOdukmV7hCSH4qP9iclyq1v4vNrnLvINfduktMXSH/xpEdIzt3c7S5MvzMby0bfnzSB3uuyFv51f5GhxVe82xS/MvSB3vDg88zchJnW/j9TAX7XGOBHojwnaCO1hV3sUV9rFTSJGqh6Hw/v84034xxT2X/5fudngqKv7m8Kk240+n7vYpl4oeop2rqJQR1sMUbmwD+XM7VGrgFbw7yyCawOBJ3TmydVNh8siEt3uJwBFCvcvwnSf5LbN8S8CioC2ZBf98fft61PcF+IDJ628y5V4u5dFldSvGbbukyJVnbmrKddL1NmIb3A1N37StDR+TnFn2qY474Wt1SxLts0oNPgsp0D/Cvv6YDukB1afoOJ3BOcq3mDfeMofaBY5Y6ODx6TvMjEOhLtU8cT3SbGydVKz54FPMSnDinYlCZe+Nif5bo0bRFpz5bhUa1uu6qChgKOn2CAl8rCrnDMzreGypdAvGV+TvRq/vrKBHqwhCpye9d7wSGfdn3KXpb6iwrYm/tfkGxQUWf/AwEm6Y7N/s6R72Kja/+czW9jYJE+TdNEP7gznqf7dwsFfaPankfT2d584jPyObLGvphBwXaEagvjEl0lEfwyEKba8XVvW14tBNnR3Sg3p4ZS52sGHKr8bNvCpZWfk0A5Ewf0w+64cuATf11vhMH5H5jOS2rSjqIqCRcXv+OJm6n9AHQmb7bpXkjjbc8+CkFS/l9Z17RrfYXYA3jNBt60RPIn99fBTHnHForIMJfQffWm/+yoB7jKVR7BzyG1biDUqx5xxjCt25tztTz9UXgVkyTTsJ3Ko5CSYX+4/asjWJNllsgcvOD2r0tgn/tVYDLuSqNGaGZBD+TnZquahEdfvSOD9O9cyyUG7H+4q8oSoOnmVV16lO/dQ0t8iWB2B2UphnSucp6aMdMtldJ5UDtuzQEBsyh7Y4qxvDZS/4n6e+MPnGTQMaA9pjg5iFcde3HTvRt2+8p6hCKBJ/MvzeKMcHDR3SGui2flELneBttaGlhLo0kAkOsE0bzdZ2S8erpfLNjBdw8ehj6JU38g5i0oEpCGhnT8Jn920K8k1leYFxSaUF7Owi2VLbZNaJdpw1YFQ1f3XuF08ovcDPkBZy9ecqnf+9Xzi/pNNt7EpGtOjoO+Id2onyCK4vU4kUjzt3c1ZmPFGzF6oJPlEbh2E2/+gLMKGK1tw4el8ezTHHq+rP9CJnhgTp7wKngC03v24XL7JhLSkhS2bNPWtPDl814oBVbHqV8IW/6AmTG4Ut/NghcufN7a2W7kOaewBI0xE4olyzBU8WDOuL0UNmnflZ2ShKbJPYFUl2wc1TSHxh1MdHUkWGrdePbcdSNXU8uHwQ2QICiN7wnb/hC0eUirwWRLQYMS+Dhq7B7VDYCSAW1EEyi35CobR5h2IXTUBTWO7z92VqZU2xKdixMTtUWLg/9P/yE8UW97sGyukERbLi7OhjwXDD2l5bI2qqhD1Mfoj/O8ilf6/8ZX/x6//Fac0/h+00/V/cxbE/y5S6f+MDf//h6TSUwD/MuVNBv13u1wl9ekrkeYHGZFgBaEciScIicq/k7tgZJEyJA+jiyOfzvOuc+H1N0UxfAjoX/Y4QefDeyqvAq1gwLJ7b9vCBy05QYwylORuya8zTZJW6UOQPCTFgAb5mdI36D9Q3Zstr6w4BPTFX3H/lceDcrHmkfZzEAnj/f5MKFF8JWMRN4CI/WNrokO0gMRFFYO/TU8+DqeiWZp5JvNtsCOrfWFp2mcMeek41kXRjN+3x9rmvF82/cud51UXibEmae85mLStujvsD+NYGsW+nycK8y9YYHhmwzNc2ZaNfiBDNVssy+JsqK7yDwvB22SllVkZ0eLI1Ff1jWWGAfsxDjs7Abbbz/dFvyGj0GCvYSXOcztS+683JuZeikiYPGBe0cjgAinLGLilVaxHx8tAQI2N8lpXmLn/SbmGQjV4fv2tkfyrfO7hBRm4x/g8SRzikzvHc6uM7A8bqUAWaWdlXOOWMVW4yJWZRfPXZd21f0TX98KO7DbP/pCycJn1LXRBxb7YZB4Vk55rKEuKyDXIxqVMb5e8j8xbnzQ/gfKAoIsffyVXzP8hVikQuozjMifO0m3JJ+si4LoJXlbQ3V9/lp3u1ytsNeue6K0XOGCdsnyQOBeqE0iTiX6xWYfbUlWS/PvEUcxr4mMPWDUqFERWCXPfQuAxVlMeMRrb9vMPfSQse6JMmNAVUWuEK9I7Z2yVpnLl8Dz/XUockZ3pKEZ91ZFehBCb0C9QEp2e1dDUVNSL/4ieCvYB+dRxjh7T0DHoewKzVmrRQ4hnpQvKsBHEL/zFE/a2mGdR0kBrXSCTy19/f0ckQ2c+WRGKn3I/cg9xqNtEy+aR3SxnXeTOA6JUnAMP53OQosrUJ1ZD5l+IFnmPwR0lYJzSyesvFuVQBrXTyzUC/FqZd9nqSWIdi6/xx5QrmysXmiPuIZisEWV2ss/Hl4L9SYXP7esEDMZZw8AKQQzH4Taf6xFDrfLlPrPZwOcKlR2hPLGZXITdTxeZjhHLYGQMrA0U71tLKI7+h8OUlV/wupLAtyAaMYaeRgznhfg309NZsuvcjHcZTHpAo9XGXFUFr/CYzmQ6cK1BK42tlkUWPDEqvnkPrM/vKeYT4igyVYY0KvGiJn9bF1MGsTIaVb0F96y2jCgxKH5ARiDMU6HrHFylgfd/GLJ4rKnXCpUYJD+vBz81FndEASUcL8t13a0wYct2i1LUVsD899qwUDIEXYrXMA/gy9VKRN3ks6hMq1aA8A3+S1kI14fsQLKYhm+DieysMQQb5j4LCJTXuycjH8xXk3c63JrrF0H4ik7zZRqWIYUXIogdr2tu3uFkGrE70leBawl4Ijj03X1o35e7sjcUmZv++pI8sjmOUehS3oanGNPP20yF1x132QUW0nMsi0AbnA/4serVlP3+Uht3A6jG21AT57kQBlxG+NbCRguyCkyxyA0RmQpP90XRbZYEVTELTxaKLBBAIlO2VLKadh4Uv9spItfTJVMLavJahP/GmSPddPsbn0GoOGjvGuk8Oe2OIfY7ry8uWTb/1oPy/PtP3gnmv3FqSzeYLyCMff6sQGWgWmLROF9D83HeCjJPFQYlb8z+LsB+neaudLUivQBE/bCCkLay5fwxzxo1HfFFc2LKfumL5rFUDJSm56soDCbIycA0GGorCqHpF0OerLY64ZbGyB8/+3o71FuR8zjGl4KCWpX1zoeAzOwuTi7tfSnEAY76XUUvfIhKNa/P+Pdy0Akdwvck2W+WooJ/2W+LISXoV+D1SKBQ3JgPLnKeq+bXr2Nw6qhARrnqNqUnS5fIe4ey8LihPAT6vCpQ1jYHf6N8hXhVcUJ6CLETviUg/xEAag/ZlEgZR/uYyMTDdlvBw19+VApWaOaAiAyNrEkNlA2uVK4p1RslUwUCUVbCsRC3O4AuuLtxRfkKGhRDiZGYe22pGe+UKFgf19a1TLINVcPBz9M+YUq2ngq17Br5akkuqAGGu0XibWgpA1/4IyuN1Fh3pJC7YoL8QI6YtCGaxkZnnX0ieCk+RLf8E6wNDfQXdVHYoYKLQs6WhgNnrgeJkni9SAaFHkkrYjdC9zw31Oda3JR3x33lN7S+0zXlPemqH8KbBij0tnJJvaxMILKdUUZC5ElAAsK3/oCBH048Q63SbkOJE6U/NR7mNPpKnlwVFyWSx8NuLQ/tL9/Jou/0QMeieihDDxPqLUS0yR2aak5tEoEXhUbPt+qqIS2cqD9OX6RF9BRRD5VKZZBIj8pP8PUXyP5p1tVfbaJFqE0FUaXvdoA8WZxeMCb24JQ7OvgI7y3PDaiF2muVe9Z9ITPjoWMQVsDXwXEYQOGJtkTJUHM5GjESl9bXhQRaQC+2rdPaSXttoWmDbXlKVZI7a/cgfrrQLVQw1sklYTdAiAx83I261kpr3sn5XqzrEjNSoclQmqor6Zvi0QYHwJ5dZYbg9FbB2HLXtZ5NtjsR/IZSwhrY1QOTztpZFLX2rrVqkppU9Lwn+QK4JtespR8IdT1V+RHbq06q0FyLq3xQ0Nvue5KXvcCjhKigYNchmlU9fZKcasluXNDBuVUFg0rrSk/F/L9eI7N12mc0JEaN9Gpu3U3gBK8bUziG+A+AvkMbC/3xxacWr/k4EsE7WporGn6H0FWgi+rpBdPq0iQL6A/bnmAZ75elBsqbyJVvRQikZ1QeFmS148DpQKMyiUZdcGBe2TWIQ1mRtkt+R3weXm2278wZOVJEE6JQlW4ZSGhT9+Q9iNArPUsI+1VmCF5L83L/HQytE90XNzDMAA870JDiyN112NIfFfCfjzCJvxkPkdJDFo9gaOczjthH/bUaaFvhNJVtWO0/3Rcar6WZ2FZKiqItDgTJQtKgSyyzw1rwkGshS/y0yfMYDms/s2gH4zdpbU1mwrSptiSjnUEXmK+/QZSQRvTICCsOP9v6ExTBcjeTaQn31asFaet5C8UuyI22t46Bnob+RAEWt18CJvNcg93oPGMKUNFEMTzIqMh1NP+JKEeiyhfrXUvyNSOIJBPJBXs1ti12f+0Ue4iD2BSLRN2rrMxuf6IEBUU7V+uSUKZHYIpubpWmQQMniObx6r+JunOm0Jc/0RvqLQ22QqmbQgcqn15HorCRxCPs+dkuzwo7X2eeKbfvwzBvtOwdduAUCBsYxOvnRQKTtFmTqPV80p/mbz3X0JD6xR+F7/e3zCPdUIRrnPZwkk8u3p0HLW+4BC1Zw8c3Ioci9O/UiZDCH/vXvs0lQrgUir6Ol4LYCLWZX5HxC99Op4Eb8cy0LvPQepC8wnQfcsnrc26503ZblSiWvKrSvbvMofzxg41i8yJqmAv7+KMoWR5q0t/eCBVPr8C/gLRTQPWZv+1JfVxVZwhhfU2nCQnrMqV4oOKDOXe5SzDr/K3hO+AseC0prh7JIoTAvIYfBCVo/vbTS+VoSyRZrdeUz6uLwQMihJLACF3YGtK+vT1kfVX9ntVDTthFGe30Zf+/H2acs0ekm++lYMjkssp2jRXPLLHeFUJZnUFkJhIDz9wn1Ud2BcKgSYw8qDErufl3HdijLYbqV3izLx927Lj7vg7ub35SX+eHkRt1h/0/0s6QIrTehY8zHhE+f61KJKTrEJDnFz8I8LlQuCguRCx4htCcXSsL6HoTdpJ1yG+T0pf60bUPTxakn/kt+rVykLSbiP4izDf1spcGg9ZgasZ2zVy+OoCjlciwedfZHUbi08c/H74H2IeiE1TiDbqfMv3rTVzF4VzRWDGgVW15rQIyYnzgT+Zfq0fbFDvYx7GnrpNY+grT/dgYX9VCJo8rYBDW8Fq0wm7WcY0aUG7YpIYb8pLym3hp/9uWBFCdvf6jRSA5SufvatOcUs1h/j1NPezl73NhfDCv5PPverv75HOFbIXkG4Skx1ULTvjBDUmur8rfj9BlQgWHynoLxDcT+JjMrrmThVdS2Wk5zp3lTO/r2ZreZ0Jr7Vs3LNHfaA0WpZoqErTcFDeE0NXNYwCh0tJxzRG0abqD/bVB8OUeDouA6GuMCvtrh+Dtdg+cMbcQVAhVcSQD/XOdSAJX0mP8dtdEGjcqttbKiD/y3/1DCh/1B3E/83LJ4E2UlTG2xmUuEAaup+pvGiO86HHG+JvHo6Eyy+Q7y9p01HKEDnQBrtuqIwoPQRgpy2Qp/hsF0fJCRCYN/sI8/3Z9xP8I/Fwk1/8z3/0vfFT1v+gh/+W7/9lHiV67Jsj5vKlu6Z5Cysyuw/Esr+RX6YNyAMN3C6J8LmHxfLA3IsFIHEtWDOm6NbBkMrKanMSZGilghv2FZDONDbNgpQfy11wSdDGxM++V0TEJnn9TgPgbnlRMFtyVyaKuOL9BbyuzJOWgmcxMPywxP2MrtD2oYZE9YunmKlw5vR1Y64Ock6jfVaqY5mjaDAhXpVO7kn/ed5QxApxPmFTua3HD1NoMWoLfI3yClfc9OEAiHdDxjKWiRgreT+02y0HokNyGclcVnf7cq9shqx85G+e5ju7ABRFCz7jJDVNOuZ4OhZ9nKL4YU6wSbPR+pLK+4lXCfmiUqBJz8BzYvaN0/Af2TwGcGBmWgPkynvDpZnYZ+kCbP0rLFj/8Hi2mR+DPGIvmx0JedY8NB6I3McXdjZvtcjXuuaNZTJ8cKS4KifCR6KvcVOqrA7jG+1Osu1YOGKb6UAF90l9D9xC/fjrWEzy2RGGJ72ppupFDpd+YRxlYyCi7xt1qYq6bSOdU36kNJAT16JWvA+8Bnq0LjgNtvaDxW4dj8eV8+H4w0wRWd2CidgVxeO7sqx6s9nKg+ZM+sqK9sOIKaqX2MzZHzVM/9kjXeJZ/qRaqF6P5YiycJSuKhCRGGId1Le0D0TxEx06Cd5XgKRXyAsXS5XUO6iGjBzJVLlnIkHydvalE9ZeZ9dN4CMJsUivQo7owhmTsL1Tfb4pyhhaOFxNeBL73zYSeIdNS+2P/2aXS1ZOwIG9vt1FiCQBP4vi1AUIjJH609+5Rc5sTYI/Mn6GVXypnverC1i+2K5NW3fSMMG9QSQXbNw4KsMwMfqTz+JSLW3lecTNUZpbtLTgWyTpc8Sgv8pRju800hUISG4+gyrOg4BoLBMpptnYMwZvq5uglcWossPIVDoTf6r9UExTEBSlMsl0lmte+Z0yktxdKKGH3B+xxQJWa/yhx0oJ9xYGJcuEN5ITh5HsECuix0zp/GyV4Sf8U/AQqcHa3K7VqrPjXoRNE0WSy+iIem1zZ5EuvxDAMsBM4OVvifSXZ42sZOu3nXoR8Zg5tWBj6C8kjJP27z3NKPEz2BcXOYvOm5iEIxU26n4gYNC4l3MzI6EROFH/y2wSt1VBUWHSIHl8sSOhadtIHBGpyykisQ17xj9fw3lprEpHu+gY5EebD8kZxCRuuFbN2f4DxwxzKsAZsaR2hvePvQzohziukOPapUirhDQr/0Uap+yLArXHp37QEbMonz/mgy3S+h8Wwo7JK5VG3yf06BYYZ6oOHHt8c5W8r4yZLavEogxDH3iiEIDursmeWwoknGzRvJF9rVBQZa86n5+A70/2A9lfhpvCugVktXFvUXeQmcfCJ4ig++Jr1TA59sdSQ8P4sQn+rpPmFFsxxplz2vW9PVcasgOayNtiPRUXZEIotTL90tdnuiNXleL3WDke38BbfhVqgzvgqGVsxkmsBVIy2g+DJzxtXrQ0wx1dEnLaE4YMH8YiEnLYTh23TByEaJ/vBzzvj+BXGL9tPRWiePeefVs5HXZb8v3JyOst7AXle2AO8jTPruspZhi/T0syLz9rrDL6QW4WyfLLOnXYKSyQpuoMGQ46FyHsLlDFowzTOuGpoD6KYa/N+l7j+4azju30F3oYiss+qk5s6RDK2LyPZzY+RwJq6fHjm484/PT7yu319dc11OOSPsxOZ56jXmi65sqgmSndW0EJxLbB4NREK4C5JqMBYWIiroJxX8eUUUzP5t0ZXNlN783IgcHJdGazOfLSsQgsakJ+bfgzkVyiHrk8Hyn0ezbSzDKlgUDmamG7Nugs5UAYfL94Rn/JxO95JO/KTD5/En2IqSaYq71lWht+feSZ2Oc9nOkn5DXsRb8lynDfH8OXLJsXTwtk/JzFwiDXPwJscareI1d8fgakgjKkdMbXnSlDRC/c+QRxTv8D6Tpjpozpm0tz0a5MaH133iW1xXTlPfmN5T3uwKAqEYzroeiKDQJInQ73vW6MIFDnrqUVNFwtPqTyDbqhXGeBob+8MjzU5GPNV3z4FVO8TsrBK9q2MRamVYsRFN3xhOs95gkRTCUMSepGWbFaW3Iu+irzTiIWIm+aOfEiHWamrRMXwnt6/59tAeNm41PZnctDCe5QKINA29r3k4+LHuM0tdIVnxoHb/Zxr+w2ID1T4rI+9Yuj2Dih26h+bLnrWeAXmW82puObdp9rHvGo/80fIv+K+/n64s8pQ6skksZ+uvYvybEbF/aQ1zbxJg/dRL0mZMZSyHPhUXdf2OMWTOKq4qBdcW0HsqhUFczONaS70d02Yz0m/X9mAqneVKyT8/rgdzzr2gG1eEnQKnUJR+a3+gvS9zozKaoTBZnEEdqRwywlnF8apDpGTlCQLJx2IaKcpd/Lq6PnkTUmd2f+IZzBjb7kKztnDliV2TjvhqeJxQOOJkoc+GbCw21mA2y+ldv8HVgNO4K8f7Yz/+PTXrNCDn4/R0Cjj8zgw71MkXevMn2FfweuWREqiS5sJlK16mmPA2UKNSKYFvieuHNI1A6HhP/d3aN/7ehmbOJDVWN6mA3ohEnd3j9v+yZ3PVbmEcPyX/Vi1YN1OFscQKzrtVvE9raDTZUmoJGn+GR0ocqHDAwqAqYBO9CoWZ17b0DI5CnqEg8Kgs1ZeJ7sScdLx00J0LsmPKM18v3aPlgWCsM+LijM5l/FTfgzkl6t+7W4bSfQ5ouzVzyT0K6HTC6+/7+CzGaDPEFikQ4IaoivujeQveqWkO0B04t8xq/4qtL4Zwk025sKtrYpEOv7eoyUIAcs7Hw3okwtsxE5AvtkeSi9WPpTOZUqT6SleUU9z6p1wX+sdeGzjol3etmnYYCYV8QMi9zVkcGUIzx+zfJ3X8KWVcd8jgAvvyCeMhWziRf5J12aVFD9vP6UKiPIlZ3gkUeUNaa7H/FbCbyBV0Yh0xkW/j2doj+Dr36FcZKl5h5ZKOZp5iRJwryPo5Z8e97dtojwZQk9Btcfg2ux7RYzI05/lHhEiVkHMn+sb24uFM/hIHhWZiKXhH+F9oSblur1ZbzJrP0L1TqsPUQOKvazE/d/dJyOzJbeOk+tq6Tj7n2oHSkMaeGxe69Wgy6+hQgXPqzpBaYGzSOXatB/hxYvJdUQI7vVP9jXM73AfyBAgrHaVztwI2vHk6uRvv8/EHNJb4NpbtvOhO2h82qfZsaE2SZ83lh9q/xxyYv6oy0UDjLsB2WxhXwHP8ySd65pqtkzl4oji92p9+VZ+OON7MKmCJFHALi4i92IxoCBgRHaR/wJSqnRApJXVMOkhGULXFm6PEFd+5ymZioILR8XYK5lFaPWfZDktP21pkdgJ/jWp31YGH1E8gdDOgLnjqHJKDIEtYBs0z4hYkxst1tDv/oxU/86OCH0ingM3e1L3eIOjLGnAEJTvk6KSJkXKr3CRPHMr/mxWtEtQbzN4HcxaslI53ZZ3ar7yDbp8lprf+2529cV8zchcKM4rlnDvUOaMZxj8KYYyfpBW4AEJwmr1Z8UYU4mHI4Qs0lo2H0Cfx2iv81BQiuyNwfx8Hk2gDjYQYdPEFbUklMHN9DiluvVFeaxXaeDB31iSJLJC2+6Aan6PbRM9vD9EdCCDQENfBMt4L2/WYUY9tir9mjnoqmKP9O2hoXViUrq7QkiNgYI8tpPipvnzIVL+wwm2v2EhX7hkea1tGRy6gwXT10Tbfyj9eNbRcrE9xzNJ4pndG0QlQXx5liOHKx/uGSFVfligNdAP31TBq7PX8SKHKRRVUoY8fZwWkVAF2pkTsquFUMGGPv4rEn/UwK9Athala/9jHu/2Z0IGdLgkQ39OddB9x3IqnfXN2VsoArf3HjUJtv69jl0rW5vYyR3rIj5wTfrS314rfXxEmrjXfiTXh2hf/5iGMKgeV1V6wUcTEq8a+UypP8SOKbJhzu8bQgN8x41hppaCmEdTiiacqSm0mlm5UgEpd2OQ3V4xXIZL13n8E2le0j1fyBT+XlbH3z50Qnmfyri2/C+8T2vP87hrGllDKKOASqAHoOgJ5j3cVZLuGv2gImW8z0g1mhcJN9q8yXR3P+4oRP54YDGRgkwqkSTcbgH+ycY0Js/AWVn0FhQDP2sxqkiEClF4Nb/c7NjeSixsvvONXKMHOy2kDcgHL13cSvvtW8i7vypv0UaUiawnpju2NC5nE92kZBfR3n7Ud5G+KlfLhINrlik6qS+5JC6ubHeXHZMCK0GMFijECaWVMSvF/PjWaG32MnIXSd6x5VCmxIh/M32Y7fSgS8+udlHR/pdFQkS5oBUwOJvfUhci92wkUf10+qUB+H95kUc1J0jdvcjyK+MsFUE1OXKzloyjldXMgiYUzWGHXVqoArLHiLTkWK1EqdNkg5sW3NM5JcEJsuuh57Cu5vwklWvvHxo03lQGsia9D/jslNKDDuUT/TD8g+iJmhPP1xFzL249tR+ieV1LHiKcJ1f8e6MADcqSC+PND5C8m8zzoOpT3jun/LgTLrLs+Mb/BkUo9YPP+C0XPkYjHuLDitOkXvII9Px1qAYW5cv5JUsG2TC/eWM/m2HqBkh8vjfX3rFYeYJH20pEUZQSjE51ogQlxQkuX/iqT0VkHkNYng10K3HpvSG9nGU8XqLk2zeBTstT7lF+QL/uVyoRtENon+dV0tDewq9K2CdNEu7md9+321Csx8ogiUNDsP265cWU/Asd1S183lIwApHGN5lPLaQ7tV52S/9iVmAEOEYeUGoMSlvBl95MMHz6Wu8lNar7eAafonkfSpjdSRJt3gvcVVXMUNlYzlJdb/JDFWsJSSr3Nk9PyPvCJNZTePOikmdOoWIthydiPAgWd0VHr2PUZgiLkfpPMOFw6ZiysO9QIjUTXgW0n+CiVtquNcyKPEdo/wryoXNjrstIuJfuxapPUaz1FvSO+ayb73rG4KsNF9x7XvPen/abfzi8oGr0D6W+QoTKsGy7s6gdIhA75Ryj18Qr7lne13OjZ7n63K0JYl2wDU8mHr22oYlkIrwTiWKhQ43513owgaNGgZfCcv6y2xTMOkfPAMw/X5OFWuSUeUqjojfWpy9sB2JyNtsXZ//Zlrd8SxHv2qLIVxzq+6SRS6bwCB8SPGHsnEi4QfiQDCxuQKLtnXmbnGZ5/CLxpZ30dXAEmhhmjXmsFHThUwtKFg0h6DnOBDf5Y93IpkSJcoq5P1/N/CIwWnMuhU/fSAbVAnD24XivSjBvlDbUwfs6gmws+dOKtNSloLH7S1oX+N+9M1AChi49xAsu1ZEjYOdlUi8Oc/frJ/dvw5CkZjbP67D60NKGswwEcW7FFMjYD5MWUU+081IwCpfa628t/smGqOfq96lsoeRwCOgRpFl24+dJN6V6x4qlfoq2c1N3GSyymBaehBrg6Uam5bJuAE5yNH+s9UmHfYRXWbWk/0ccg+LkFDdq2IYWGm3sHJgYhDVSdSzn3r7xvfuud9UlNaVqxMZSiZk4nyqowNde/csqtbNE54rL71xOTxWWxN9kvnyF6I6TbwkIomFLazbLSeevfH5qH+Ej0l/N+CEWQ6OWR3aSoExBjPx//LHyrdIm30AsWfSLlN0AWWvEHXFcR8gKB+Goqo4PdYjiEs9c+5Q/xgHI4iEOxujLiOwUUYkpTE+pWoyfGHI6cE8CHIpWeAKBAJ2Pc9QoKFFavmWPkiEgulJODEJyPmCzBuWt5jO77jT54rmfFthsVUng0oPbN/0xPTSD1+FKwVKsQjvkdSLwPBe/VYSttlp5IIvDOfjKJ58nmKfJ9Onn2k9pkU+fEGyq3nCO7f8tijbMZT9j880pe6QI81VC32QP1nzUeS92NFWtZSjtxq3b1Ad0giopWB7o0ofADihbUPh4zphtol7mtTR97a4sdwJ5QC9uz8g6WYB3zlS/2RCCLlws+/giZtDw/lqv+4oC/no5d9rVJG2s52CJTWqTbbZ4bVh9j0A7UWB36wFAt7AHj0/YFpOwofpkoZ02sX6pMxcB+YlOaJGJHKgNd0fAFXMhevKqvQf+l3uY1+6IIDtgRnLj8+hXd9qA7RoaWjYIR6JnLkkFUmUT8gDoUCGCgLqLD78mxnRFuw+g8vfxYV12Q8MA0E0dyBKyJTZtCQhB3UXXoUc0KhJssS/y64Pu1fbZU14TjGoM4q9GLNtYwxGThY903knwe+JrwJtQLgZ1qO6v9Uqj7b+lyDWSwVEtv5Lih4BXXbRnjgPn2QBbQIVYvc/aQO53NR3AL0bGIOAxHdre7b3/NN5gMj+nnNsAfd3b74WjtIFqE7y+Er3g/YqSJffcHjguvdrn66cN2LoyTPIYaenRTqWvUEzL098Q0P3ckmxUX1Fm2ZNuOum/0/ggGpRbKzxeUPMXvxM4yvhtensoUlp4tPMr9emxNaIqPtX7xcRC//xEB3eQD9hHuxEn2H7Faauhw2obT37/RTidb5mR2/YsP5TrfGeeW5l972oaBF1zHNxihUQjcVrh5E6qQBM8nkWJ3HEUuegLsOONm5ZLun9Pk35RD68Ns0E1GZLTSiteyAOz5BKHEii+5eE4nw/LsoNsn3u0X/Jc30m0kKz9whIksnmjm7kjmC88f4C/oyZR+MDCqUSicdf9iTaHNK3C3tmpYqFqLaNm/TV06HEFZ1tWIV1YjLrP2qN7rbLClg/6yELxA6Euflz8HGOUE9UW/jDySbIrW5cW46/5sCEC3vLpll9lWpff7avONs/qDF7DFY9J88oNKffgXr893LJsP3Le9PFs4K6IE46d64Y5TnJF6Ur3OSFVVsCndcMVkia/DI5HKL+f5FekhKKk4lKjPPn2d217rd/X+jtLjU7wcpU+5Rct+5uHQpe8juYKh744lmu/FydPoWMXa68GflO7B4fWIF+29JmIc4asK/JSkrKLjl/5EbBCS7js528oBD2gJ6UZFyvbdAH6beZWRfwp4lpNSBnhpuiTE//F2HzuCM+rPgRP5cQgjNj4okteRisYlzwifidiaZ4TyXabnT3r4r5NRx6W/MhjwUZKyrHKKNsByt9yQ4eJX/kh3rVz6f0dd339fbEBo78l/bEJVNBYIFSzYnhgaZzIwLpVCRDf59zFAfmZlEqDXkcMDoSpyQXz3ia0+4PtWSApMh9LZR89MJ6dRlgkHLwk5y9jV7kQ5JNW6A90fvXbH04RbgB+CryGSCG8Pvqm6EfEIacLlLWkAH7ELLgPzLoFZnOS4wztXHPXoK8YJFxS6asD9Q5rgxy+HeXezBOXAGvSAXpjgK47Yjif0CqHlVKVTQlk+iNXkvlJVR6du3Uk25W0eIYGUB/XOeSJrfYq/V+1/JXsMaik6/Sm5jXZJZKitu4BJcFXfyS/JDzYXpplN1oDf/3qvpVava0bs/IosYrp4RPWKaA3HYSykNkd0cuPOAMOc+1WsgIzW3xfvYA+9lzJIM5R9JglB4+K0grhjoruLu0rpH/GzMRPnbTJ0kfoStL3nbNsTybT+wdw71/OD2XxeOTD8MIK9ee+1Fg6WRkiiG0HTYfzu3+VJvetVrkdU5nT3Y9lQNvCo8PJekfhVwvah2iuskWpdGjU1yHj5rKuFGRdAL3zX6DVtqigXAEJatUY5AqlE0ZPQY8NCNUFpys7m5eRC0nJyGLw5LjmItHbY5SN3tcqGPUHAt9dUU5hBD0QSty0TYfrgCc6m3/eLBrkteg4mqsHHC1u0qmjE64V5/kyKTnHbp7whRqIfA6pAsHiqVaSI27m01jyRV5/1bd6VHMwpgQxNWW/8oLXImZE4KyMDjhhrCbAcxuHQ6g1/XrezngkR1e4iFmVpOlEGaxibLHG5oJxCrptEnwRn+ht1XiGHZ+PdeStqzqRhw80Ppusf9SCY7SqBCi2sGL8s2IAVw1Icfba+Hw3LVo95+enPbn3Cp0k6LukbgPa+6Osjf60uAEFebXqcANn/niC3gT9c1DnNUVzFiHEO5uvYWZ+pDr7MaIXluTv7cU6fE+tF7sK6zShipIVFJWPRGK+Iy8mLbIwqR6xAn0xHWjaN3V4FFtd18g4ycL1PxI6t3Oee0B+KXy+Sx4V6dalQ8Tqjrg53kb1hmpFrpS6DOToKl7nHnqLTpOWXWWCkNKPbxVKDxZ08VbDsa09A50VbYBa/SrVQNwgevGAY7DTY3UVr19+JxWKng7hxb2H/s35NJTqwZYq0kpv56dIWCSm5OdxXWP5OgQeKGtQVkArouoXNK4J9H0qBIxWP74OON/XRg7/3WH+p3f4/90dw//gjvt/d8f4P7gj+O/uWP8Hd4T/ST8e/493PP6Tfvw/3xH9J/34uwM8t1rM9PptIIhYnVuMe1kMW3zLv8yzMzYDuM0xiv7PQ9z0YA6sM8dQNp8fWDVJpB8N9c/0gw20x52TKyjnSI8WSGtI0e+Gkjpvgq6iiNRjAxaYPDuMRM/wTnAj5mabLvyvdEEBFRkUut468uHZM0lEL0G7trsrCjohk5v2mbHHgTLBDcKtY6uZ2N+pAmu4jnPd8ZUgYlW/CEVT3ezDw9O7i8yZkzjYRJoLBLetDkP43PY06j4v2Njje52iAdBlqzvgqE5CuGt6q1aZmvToY+oBXPNbtoTnUtCWJJdduOpdkb7jicGPAddMJqcWqY6KdnMDvEMo3MildL5DP2f69wFMDqx2i+YDjYao1jRF4ghrR9A88scOCEBTDZ3pXHQA9P78OnChm/iCk/r+/tQzsCYPFOaj71crM3SVBdRqvoK3oifBH428+Hqh/2/SX6zyDrNFZPsQBfU4d4fmphsnX/HiCI60pP9eWjRfhRC8ObqmQ6AH4z0Y8FdZVJOwp4ouW+/kzwRpKZZxIDQ2j1OP980U4r+5VbiXO0yks5em+6G/EXaTN1PMYnTP7x3d/ESuMHmbxxRhrGWQtgbidFYQt+Sk5AhQHxVqFs9C7KYKnUZXUG5br2ND5uW+LhtDvfTY1ewuKYvfc5F+VKtAzKPeZ2K0ysVhnJklx+vcJ02SwZ5shhjk0rILmeB1UFupHsej4TRY+5w0nP3NWwt2jxOyipt09HY5VVF1NbroRScpz3KVgVfizVLH8iXpbt5ffIHWJPp+mET6W03+Dpq6ElOHVmaWhotoM5v/9n2EueaW9BDfa/jZVVZEklEHP4iyPXWK2rsgfu7YagYc65weNaGbLBLg0ouIJfZH8eMsx/vN1+XHnJHzuTEWuRLksQ9Ex4GbxunM7YpZBVdNm7h7ngm7hvVd2La/drdSgDcHxbLpf4FjT/baybsckfXWQCMPFQD8/+BxFu840fxldD3YP3vliPnaNezvu05SClCoTLBunQdRe1fFtzucz8sEdrk5lrvHQk9fgEbgnAaD8xdrmTxTlXuIlLhMadPhe4sxod2aK8B/Rcv7NtPMDXjbJKH1dwfegLLq7vp0yeVbvmX076ps3BjYlif/PK/DS4ElHIDTkLOVv3Bu401h6HPpAZXd7TmyCoTjVTqWpXGaKdQt6FVQl3TM9/G87DyK1Ak+tGCf89NKmOjxf2XZ9/Dh+DdajHX9f28JM0tQ/00NM4n/n/R/V8WMKAf+70XM//HZ/4saZoRrPp/5v/pOHdNvbX2KF7ri/wI= \ No newline at end of file +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 \ No newline at end of file diff --git a/content/rendering-with-batch/rendering-with-batch.files/stack.yaml b/content/rendering-with-batch/rendering-with-batch.files/stack.yaml index c57b305b..642f7d1c 100644 --- a/content/rendering-with-batch/rendering-with-batch.files/stack.yaml +++ b/content/rendering-with-batch/rendering-with-batch.files/stack.yaml @@ -1,4 +1,320 @@ +Parameters: + C9InstanceType: + Description: Cloud9 instance type + Type: String + Default: m5.large + AllowedValues: + - t3.small + - t3.medium + - m4.large + - m5.large + ConstraintDescription: Must be a valid Cloud9 instance type + # Used only by Event Engine, if you are self-deploying the stack leave the default value to NONE + EETeamRoleArn: + Description: "ARN of the Team Role" + Default: NONE + Type: String + ConstraintDescription: This is ONLY used Event Engine, don't change this if you are self-deploying the stack Resources: + CustomCloud9SsmCloud9Ec2EnvironmentF47DD48C: + Type: AWS::Cloud9::EnvironmentEC2 + Properties: + InstanceType: + Ref: C9InstanceType + Tags: + - Key: stack-id + Value: + Ref: AWS::StackId + OwnerArn: !If [NotEventEngine , !Ref AWS::NoValue , !Sub 'arn:aws:sts::${AWS::AccountId}:assumed-role/TeamRole/MasterKey'] + Metadata: + aws:cdk:path: RenderingStack/CustomCloud9Ssm/Cloud9Ec2Environment + CustomCloud9SsmEc2RoleCE9ACBCB: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: + Fn::Join: + - "" + - - ec2. + - Ref: AWS::URLSuffix + Version: "2012-10-17" + ManagedPolicyArns: + - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore + RoleName: CustomCloud9Ssm-CustomCloud9SsmEc2Role + Metadata: + aws:cdk:path: RenderingStack/CustomCloud9Ssm/Ec2Role/Resource + CustomCloud9SsmEc2RoleDefaultPolicy92A07C4C: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - ec2:DescribeInstances + - ec2:ModifyVolume + - ec2:DescribeVolumesModifications + - batch:DescribeJobQueues + - batch:CreateComputeEnvironment + - batch:DeleteComputeEnvironment + - batch:SubmitJob + - batch:UpdateComputeEnvironment + - batch:ListJobs + - batch:ListJobs + - ecr:* + - batch:DescribeComputeEnvironments + - cloudformation:DescribeStacks + - batch:DeregisterJobDefinition + - s3:PutObject + - s3:ListBucket + - s3:DeleteObject + - batch:CreateJobQueue + - batch:DescribeJobs + - batch:RegisterJobDefinition + - states:StartExecution + - batch:DescribeJobDefinitions + - batch:DeleteJobQueue + - batch:UpdateJobQueue + - iam:PassRole + Effect: Allow + Resource: "*" + Version: "2012-10-17" + PolicyName: CustomCloud9SsmEc2RoleDefaultPolicy92A07C4C + Roles: + - Ref: CustomCloud9SsmEc2RoleCE9ACBCB + Metadata: + aws:cdk:path: RenderingStack/CustomCloud9Ssm/Ec2Role/DefaultPolicy/Resource + CustomCloud9SsmEc2InstanceProfile167B6BF8: + Type: AWS::IAM::InstanceProfile + Properties: + Roles: + - Ref: CustomCloud9SsmEc2RoleCE9ACBCB + DependsOn: + - CustomCloud9SsmEc2RoleDefaultPolicy92A07C4C + - CustomCloud9SsmEc2RoleCE9ACBCB + Metadata: + aws:cdk:path: RenderingStack/CustomCloud9Ssm/Ec2InstanceProfile + CustomCloud9SsmSsmDocumentD052D5F9: + Type: AWS::SSM::Document + Properties: + Content: + schemaVersion: "2.2" + description: Bootstrap Cloud9 EC2 instance + mainSteps: + - name: ResizeEBS + action: aws:runShellScript + inputs: + runCommand: + - "#!/bin/bash" + - echo '=== Installing packages ===' + - sudo yum -y install jq + - sudo pip install boto3 + - echo '=== Exporting current region ===' + - export AWS_DEFAULT_REGION=$(curl -s 169.254.169.254/latest/dynamic/instance-identity/document | jq -r '.region') + - echo 'Region is ${AWS_DEFAULT_REGION}' + - echo '=== Gathering instance Id ===' + - instanceId=$(curl -s 169.254.169.254/latest/dynamic/instance-identity/document | jq -r '.instanceId') + - echo 'Instance Id is ${instanceId}' + - echo '=== Resizing EBS volume ===' + - volumeId=$(aws ec2 describe-instances --instance-ids $instanceId | jq -r '.Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId') + - echo 'Volume Id is ${volumeId}' + - aws ec2 modify-volume --volume-id $volumeId --size 100 + - echo '=== Waiting for the volume to enter the optimizing state ===' + - while [ true ] + - do + - modificationState=$(aws ec2 describe-volumes-modifications --volume-ids $volumeId | jq -r '.VolumesModifications[0].ModificationState') + - if [ $modificationState == "optimizing" ] ; then + - break + - fi + - sleep 5 + - done + - echo '=== Resizing file system ===' + - sudo growpart /dev/xvda 1 + - sudo resize2fs $(df -h |awk '/^\/dev/{print $1}') + DocumentType: Command + Name: CustomCloud9Ssm-CustomCloudSsm-SsmDocument + Metadata: + aws:cdk:path: RenderingStack/CustomCloud9Ssm/SsmDocument + CustomCloud9SsmSsmAssociationABF443F9: + Type: AWS::SSM::Association + Properties: + Name: CustomCloud9Ssm-CustomCloudSsm-SsmDocument + Targets: + - Key: tag:stack-id + Values: + - Ref: AWS::StackId + DependsOn: + - CustomCloud9SsmCloud9Ec2EnvironmentF47DD48C + - CustomCloud9SsmSsmDocumentD052D5F9 + Metadata: + aws:cdk:path: RenderingStack/CustomCloud9Ssm/SsmAssociation + CustomCloud9SsmProfileAttachLambdaFunctionServiceRoleF610D074: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: lambda.amazonaws.com + Version: "2012-10-17" + ManagedPolicyArns: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Metadata: + aws:cdk:path: RenderingStack/CustomCloud9Ssm/ProfileAttachLambdaFunction/ServiceRole/Resource + CustomCloud9SsmProfileAttachLambdaFunctionServiceRoleDefaultPolicy9CC20561: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - ec2:DescribeInstances + - ec2:AssociateIamInstanceProfile + - ec2:ReplaceIamInstanceProfileAssociation + - ec2:RebootInstances + - iam:ListInstanceProfiles + - iam:PassRole + - ssm:DescribeAssociationExecutions + - ssm:DescribeAssociationExecutionTargets + Effect: Allow + Resource: "*" + Version: "2012-10-17" + PolicyName: CustomCloud9SsmProfileAttachLambdaFunctionServiceRoleDefaultPolicy9CC20561 + Roles: + - Ref: CustomCloud9SsmProfileAttachLambdaFunctionServiceRoleF610D074 + Metadata: + aws:cdk:path: RenderingStack/CustomCloud9Ssm/ProfileAttachLambdaFunction/ServiceRole/DefaultPolicy/Resource + CustomCloud9SsmProfileAttachLambdaFunction01DB4FFC: + Type: AWS::Lambda::Function + Properties: + Code: + ZipFile: | + import boto3 + import cfnresponse + import time + + + def is_association_applied(association_id): + client = boto3.client('ssm') + + # Retrieve the execution history of the association + response = client.describe_association_executions( + AssociationId=association_id, + Filters=[ + { + 'Key': 'Status', + 'Value': 'Success', + 'Type': 'EQUAL' + } + ] + ) + + # There are no executions yet + if 'AssociationExecutions' not in response or not response['AssociationExecutions']: + return False + + # Retrieve the targets of the execution to see if the SSM agent has picked up the EC2 instance yet + response = client.describe_association_execution_targets( + AssociationId=association_id, + ExecutionId=response['AssociationExecutions'][0]['ExecutionId'], + Filters=[ + { + 'Key': 'Status', + 'Value': 'Success' + } + ] + ) + + return 'AssociationExecutionTargets' in response and response['AssociationExecutionTargets'] + + + def handler(event, context): + if event['RequestType'] == 'Create': + # Extract context variables + stack_id = event['ResourceProperties']['stack_id'] + profile_arn = event['ResourceProperties']['profile_arn'] + association_id = event['ResourceProperties']['association_id'] + + try: + client = boto3.client('ec2') + + # Retrieve EC2 instance's identifier + print('Retrieving EC2 instance Id') + + instance_id = client.describe_instances( + Filters=[{'Name': 'tag:stack-id', 'Values': [stack_id]}] + )['Reservations'][0]['Instances'][0]['InstanceId'] + + # Associate the SSM instance profile + print('Associating the SSM instance profile to the instance') + + client.associate_iam_instance_profile( + IamInstanceProfile={'Arn': profile_arn}, + InstanceId=instance_id + ) + + # Reboot the instance to restart the SSM agent + print('Rebooting the instance so that the SSM agent picks up the association') + + client.reboot_instances( + InstanceIds=[instance_id] + ) + + # Wait for the SSM association to be applied + while True: + print('Waiting for the association to be applied') + + if is_association_applied(association_id): + break + else: + time.sleep(5) + except Exception as e: + cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': e.args[0]}) + return + + cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) + Role: + Fn::GetAtt: + - CustomCloud9SsmProfileAttachLambdaFunctionServiceRoleF610D074 + - Arn + Handler: index.handler + Runtime: python3.6 + Timeout: 800 + DependsOn: + - CustomCloud9SsmProfileAttachLambdaFunctionServiceRoleDefaultPolicy9CC20561 + - CustomCloud9SsmProfileAttachLambdaFunctionServiceRoleF610D074 + Metadata: + aws:cdk:path: RenderingStack/CustomCloud9Ssm/ProfileAttachLambdaFunction/Resource + CustomCloud9SsmCustomResourceCD940758: + Type: AWS::CloudFormation::CustomResource + Properties: + ServiceToken: + Fn::GetAtt: + - CustomCloud9SsmProfileAttachLambdaFunction01DB4FFC + - Arn + stack_id: + Ref: AWS::StackId + profile_arn: + Fn::GetAtt: + - CustomCloud9SsmEc2InstanceProfile167B6BF8 + - Arn + association_id: + Fn::GetAtt: + - CustomCloud9SsmSsmAssociationABF443F9 + - AssociationId + DependsOn: + - CustomCloud9SsmEc2InstanceProfile167B6BF8 + - CustomCloud9SsmSsmAssociationABF443F9 + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Metadata: + aws:cdk:path: RenderingStack/CustomCloud9Ssm/CustomResource/Default Vpc8378EB38: Type: AWS::EC2::VPC Properties: @@ -8,9 +324,9 @@ Resources: InstanceTenancy: default Tags: - Key: Name - Value: RenderingWithBatchStack/Vpc + Value: RenderingStack/Vpc Metadata: - aws:cdk:path: RenderingWithBatchStack/Vpc/Resource + aws:cdk:path: RenderingStack/Vpc/Resource VpcRenderingWithBatchSubnet1SubnetADE6DAA0: Type: AWS::EC2::Subnet Properties: @@ -28,9 +344,9 @@ Resources: - Key: aws-cdk:subnet-type Value: Public - Key: Name - Value: RenderingWithBatchStack/Vpc/RenderingWithBatchSubnet1 + Value: RenderingStack/Vpc/RenderingWithBatchSubnet1 Metadata: - aws:cdk:path: RenderingWithBatchStack/Vpc/RenderingWithBatchSubnet1/Subnet + aws:cdk:path: RenderingStack/Vpc/RenderingWithBatchSubnet1/Subnet VpcRenderingWithBatchSubnet1RouteTable1BCC3903: Type: AWS::EC2::RouteTable Properties: @@ -38,9 +354,9 @@ Resources: Ref: Vpc8378EB38 Tags: - Key: Name - Value: RenderingWithBatchStack/Vpc/RenderingWithBatchSubnet1 + Value: RenderingStack/Vpc/RenderingWithBatchSubnet1 Metadata: - aws:cdk:path: RenderingWithBatchStack/Vpc/RenderingWithBatchSubnet1/RouteTable + aws:cdk:path: RenderingStack/Vpc/RenderingWithBatchSubnet1/RouteTable VpcRenderingWithBatchSubnet1RouteTableAssociation9592115F: Type: AWS::EC2::SubnetRouteTableAssociation Properties: @@ -49,7 +365,7 @@ Resources: SubnetId: Ref: VpcRenderingWithBatchSubnet1SubnetADE6DAA0 Metadata: - aws:cdk:path: RenderingWithBatchStack/Vpc/RenderingWithBatchSubnet1/RouteTableAssociation + aws:cdk:path: RenderingStack/Vpc/RenderingWithBatchSubnet1/RouteTableAssociation VpcRenderingWithBatchSubnet1DefaultRoute58E16100: Type: AWS::EC2::Route Properties: @@ -61,7 +377,7 @@ Resources: DependsOn: - VpcVPCGWBF912B6E Metadata: - aws:cdk:path: RenderingWithBatchStack/Vpc/RenderingWithBatchSubnet1/DefaultRoute + aws:cdk:path: RenderingStack/Vpc/RenderingWithBatchSubnet1/DefaultRoute VpcRenderingWithBatchSubnet2SubnetDC61207B: Type: AWS::EC2::Subnet Properties: @@ -79,9 +395,9 @@ Resources: - Key: aws-cdk:subnet-type Value: Public - Key: Name - Value: RenderingWithBatchStack/Vpc/RenderingWithBatchSubnet2 + Value: RenderingStack/Vpc/RenderingWithBatchSubnet2 Metadata: - aws:cdk:path: RenderingWithBatchStack/Vpc/RenderingWithBatchSubnet2/Subnet + aws:cdk:path: RenderingStack/Vpc/RenderingWithBatchSubnet2/Subnet VpcRenderingWithBatchSubnet2RouteTable5DF00176: Type: AWS::EC2::RouteTable Properties: @@ -89,9 +405,9 @@ Resources: Ref: Vpc8378EB38 Tags: - Key: Name - Value: RenderingWithBatchStack/Vpc/RenderingWithBatchSubnet2 + Value: RenderingStack/Vpc/RenderingWithBatchSubnet2 Metadata: - aws:cdk:path: RenderingWithBatchStack/Vpc/RenderingWithBatchSubnet2/RouteTable + aws:cdk:path: RenderingStack/Vpc/RenderingWithBatchSubnet2/RouteTable VpcRenderingWithBatchSubnet2RouteTableAssociationE3297937: Type: AWS::EC2::SubnetRouteTableAssociation Properties: @@ -100,7 +416,7 @@ Resources: SubnetId: Ref: VpcRenderingWithBatchSubnet2SubnetDC61207B Metadata: - aws:cdk:path: RenderingWithBatchStack/Vpc/RenderingWithBatchSubnet2/RouteTableAssociation + aws:cdk:path: RenderingStack/Vpc/RenderingWithBatchSubnet2/RouteTableAssociation VpcRenderingWithBatchSubnet2DefaultRoute42CB5476: Type: AWS::EC2::Route Properties: @@ -112,15 +428,15 @@ Resources: DependsOn: - VpcVPCGWBF912B6E Metadata: - aws:cdk:path: RenderingWithBatchStack/Vpc/RenderingWithBatchSubnet2/DefaultRoute + aws:cdk:path: RenderingStack/Vpc/RenderingWithBatchSubnet2/DefaultRoute VpcIGWD7BA715C: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name - Value: RenderingWithBatchStack/Vpc + Value: RenderingStack/Vpc Metadata: - aws:cdk:path: RenderingWithBatchStack/Vpc/IGW + aws:cdk:path: RenderingStack/Vpc/IGW VpcVPCGWBF912B6E: Type: AWS::EC2::VPCGatewayAttachment Properties: @@ -129,11 +445,11 @@ Resources: InternetGatewayId: Ref: VpcIGWD7BA715C Metadata: - aws:cdk:path: RenderingWithBatchStack/Vpc/VPCGW + aws:cdk:path: RenderingStack/Vpc/VPCGW securityGroup32C48086: Type: AWS::EC2::SecurityGroup Properties: - GroupDescription: RenderingWithBatchStack/securityGroup + GroupDescription: RenderingStack/securityGroup GroupName: RenderingWithBatch SecurityGroupEgress: - CidrIp: 0.0.0.0/0 @@ -141,8 +457,20 @@ Resources: IpProtocol: "-1" VpcId: Ref: Vpc8378EB38 + DependsOn: + - VpcIGWD7BA715C + - VpcRenderingWithBatchSubnet1DefaultRoute58E16100 + - VpcRenderingWithBatchSubnet1RouteTable1BCC3903 + - VpcRenderingWithBatchSubnet1RouteTableAssociation9592115F + - VpcRenderingWithBatchSubnet1SubnetADE6DAA0 + - VpcRenderingWithBatchSubnet2DefaultRoute42CB5476 + - VpcRenderingWithBatchSubnet2RouteTable5DF00176 + - VpcRenderingWithBatchSubnet2RouteTableAssociationE3297937 + - VpcRenderingWithBatchSubnet2SubnetDC61207B + - Vpc8378EB38 + - VpcVPCGWBF912B6E Metadata: - aws:cdk:path: RenderingWithBatchStack/securityGroup/Resource + aws:cdk:path: RenderingStack/securityGroup/Resource launchTemplateDEE5742D: Type: AWS::EC2::LaunchTemplate Properties: @@ -155,11 +483,11 @@ Resources: - ResourceType: instance Tags: - Key: Name - Value: RenderingWithBatchStack/launchTemplate + Value: RenderingStack/launchTemplate - ResourceType: volume Tags: - Key: Name - Value: RenderingWithBatchStack/launchTemplate + Value: RenderingStack/launchTemplate UserData: Fn::Base64: |- MIME-Version: 1.0 @@ -176,14 +504,10 @@ Resources: --==MYBOUNDARY==-- LaunchTemplateName: RenderingWithBatch + DependsOn: + - securityGroup32C48086 Metadata: - aws:cdk:path: RenderingWithBatchStack/launchTemplate/Resource - bucket43879C71: - Type: AWS::S3::Bucket - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Metadata: - aws:cdk:path: RenderingWithBatchStack/bucket/Resource + aws:cdk:path: RenderingStack/launchTemplate/Resource repository9F1A3F0B: Type: AWS::ECR::Repository Properties: @@ -191,55 +515,8 @@ Resources: UpdateReplacePolicy: Delete DeletionPolicy: Delete Metadata: - aws:cdk:path: RenderingWithBatchStack/repository/Resource - cloud9envec2env8356485F: - Type: AWS::Cloud9::EnvironmentEC2 - Properties: - InstanceType: t2.micro - Name: RenderingWithBatch - SubnetId: - Ref: VpcRenderingWithBatchSubnet1SubnetADE6DAA0 - Tags: - - Key: SSMBootstrap - Value: RenderingWithBatch - Metadata: - aws:cdk:path: RenderingWithBatchStack/cloud9env/ec2env/Resource - cloud9envSSMDocument9E89DF0C: - Type: AWS::SSM::Document - Properties: - Content: - schemaVersion: '2.2' - description: Bootstrap Cloud9 Instance - mainSteps: - - action: aws:runShellScript - name: C9bootstrap - inputs: - runCommand: - - "#!/bin/bash" - - echo '=== Installing packages ===' - - sudo yum -y install jq - - sudo pip install boto3 - - echo '=== Resizing file system ===' - - sudo growpart /dev/xvda 1 - - sudo resize2fs /dev/xvda1 - DocumentType: Command - Name: BootstrapDocument - Metadata: - aws:cdk:path: RenderingWithBatchStack/cloud9env/SSMDocument - cloud9envSSMAssociation4FBBFEE5: - Type: AWS::SSM::Association - Properties: - Name: BootstrapDocument - Targets: - - Key: tag:SSMBootstrap - Values: - - RenderingWithBatch - DependsOn: - - cloud9envbootstrapLambdaCustomResourceD5142DA7 - - cloud9envSSMDocument9E89DF0C - Metadata: - aws:cdk:path: RenderingWithBatchStack/cloud9env/SSMAssociation - cloud9envFISRoleAD8CCA4E: + aws:cdk:path: RenderingStack/repository/Resource + ecsRole157644C0: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: @@ -254,18 +531,26 @@ Resources: - Ref: AWS::URLSuffix Version: "2012-10-17" ManagedPolicyArns: - - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore - RoleName: SSMInstanceProfile + - arn:aws:iam::aws:policy/AmazonS3FullAccess + - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role Metadata: - aws:cdk:path: RenderingWithBatchStack/cloud9env/FISRole/Resource - cloud9envfisinstanceprofile96D194A9: + aws:cdk:path: RenderingStack/ecsRole/Resource + ecsinstanceprofile: Type: AWS::IAM::InstanceProfile Properties: Roles: - - Ref: cloud9envFISRoleAD8CCA4E + - Ref: ecsRole157644C0 + DependsOn: + - ecsRole157644C0 Metadata: - aws:cdk:path: RenderingWithBatchStack/cloud9env/fisinstanceprofile - cloud9envbootstrapLambdaServiceRole520F9D06: + aws:cdk:path: RenderingStack/ecsinstanceprofile + bucket43879C71: + Type: AWS::S3::Bucket + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Metadata: + aws:cdk:path: RenderingStack/bucket/Resource + PreprocessingServiceRole532E6474: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: @@ -281,150 +566,190 @@ Resources: - - "arn:" - Ref: AWS::Partition - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + DependsOn: + - bucket43879C71 Metadata: - aws:cdk:path: RenderingWithBatchStack/cloud9env/bootstrapLambda/ServiceRole/Resource - cloud9envbootstrapLambdaServiceRoleDefaultPolicy474DC27A: + aws:cdk:path: RenderingStack/Preprocessing/ServiceRole/Resource + PreprocessingServiceRoleDefaultPolicyF8800D5C: Type: AWS::IAM::Policy Properties: PolicyDocument: Statement: - - Action: - - ec2:DescribeInstances - - ec2:ModifyVolume - - ec2:AssociateIamInstanceProfile - - ec2:ReplaceIamInstanceProfileAssociation - - ec2:RebootInstances - - iam:ListInstanceProfiles - - iam:PassRole - - ssm:SendCommand + - Action: "*" Effect: Allow - Resource: "*" + Resource: + - Fn::GetAtt: + - bucket43879C71 + - Arn + - Fn::Join: + - "" + - - Fn::GetAtt: + - bucket43879C71 + - Arn + - /* Version: "2012-10-17" - PolicyName: cloud9envbootstrapLambdaServiceRoleDefaultPolicy474DC27A + PolicyName: PreprocessingServiceRoleDefaultPolicyF8800D5C Roles: - - Ref: cloud9envbootstrapLambdaServiceRole520F9D06 + - Ref: PreprocessingServiceRole532E6474 + DependsOn: + - bucket43879C71 Metadata: - aws:cdk:path: RenderingWithBatchStack/cloud9env/bootstrapLambda/ServiceRole/DefaultPolicy/Resource - cloud9envbootstrapLambda02FC7F40: + aws:cdk:path: RenderingStack/Preprocessing/ServiceRole/DefaultPolicy/Resource + Preprocessing329E01E4: Type: AWS::Lambda::Function Properties: Code: ZipFile: | - import time - import boto3 - import cfnresponse + import json, gzip, struct, boto3, math, sys - def retrieve_cloud9_instance(env_id): - print("Retrieving environment's instance...") + # ##### BEGIN GPL LICENSE BLOCK ##### + # + # Extract from Blender's script library included in scripts/modules. + # + # This program is free software; you can redistribute it and/or + # modify it under the terms of the GNU General Public License + # as published by the Free Software Foundation; either version 2 + # of the License, or (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # ##### END GPL LICENSE BLOCK ##### + def read_blend_rend_chunk(file): + blendfile = open(file, "rb") - client = boto3.client('ec2') + head = blendfile.read(7) - return client.describe_instances( - Filters=[ - { - 'Name': 'tag:aws:cloud9:environment', - 'Values': [ - env_id, - ] - }, - ] - )['Reservations'][0]['Instances'][0] + if head[0:2] == b'\x1f\x8b': # gzip magic + blendfile.seek(0) + blendfile = gzip.open(blendfile, "rb") + head = blendfile.read(7) + if head != b'BLENDER': + print("not a blend file:", file) + blendfile.close() + return [] - def resize_volume(volume_id, new_size): - print('Resizing EBS volume...') + is_64_bit = (blendfile.read(1) == b'-') - client = boto3.client('ec2') + # true for PPC, false for X86 + is_big_endian = (blendfile.read(1) == b'V') - client.modify_volume( - VolumeId=volume_id, - Size=new_size - ) + # Now read the bhead chunk!!! + blendfile.read(3) # skip the version - print('EBS volume resized') + scenes = [] + sizeof_bhead = 24 if is_64_bit else 20 - def associate_ssm_instance_profile(c9_env_id, profile_arn): - instance_data = retrieve_cloud9_instance(c9_env_id) - client = boto3.client('ec2') + while blendfile.read(4) == b'REND': + sizeof_bhead_left = sizeof_bhead - 4 - while instance_data['State']['Name'] != 'running': - print('Waiting for the instance to be running to attach the instance profile...') - time.sleep(5) - instance_data = retrieve_cloud9_instance(c9_env_id) + struct.unpack('>i' if is_big_endian else '2i' if is_big_endian else '<2i', blendfile.read(8)) - print('Instance profile associated. Restarting SSM agent...') + scene_name = blendfile.read(64) - client.reboot_instances( - InstanceIds=[ - instance_data['InstanceId'] - ] - ) + scene_name = scene_name[:scene_name.index(b'\0')] - print('Instance rebooted') + try: + scene_name = str(scene_name, "utf8") + except TypeError: + pass + scenes.append((start_frame, end_frame, scene_name)) - def handler(event, context): - if event['RequestType'] == 'Create': - # Extract context variables - c9_env_id = event['ResourceProperties']['cloud9EnvId'] - ebs_size = int(event['ResourceProperties']['ebsSize']) - profile_arn = event['ResourceProperties']['profile_arn'] + blendfile.close() - try: - # Retrieve EC2 instance's identifier and its EBS volume's identifier - instance_data = retrieve_cloud9_instance(c9_env_id) - volume_id = instance_data['BlockDeviceMappings'][0]['Ebs']['VolumeId'] + return scenes - # Resize the EBS volume - resize_volume(volume_id, ebs_size) - # Associate the SSM instance profile - associate_ssm_instance_profile(c9_env_id, profile_arn) - except Exception as e: - cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': e.args[0]}) - return + def get_number_of_frames(file): + """Reads the header of the blend file and calculates + the number of frames it has. - cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) + Keyword arguments: + file -- Blender file to analyse + """ + + try: + frame_start, frame_end, scene = read_blend_rend_chunk(file)[0] + except FileNotFoundError as e: + print(e.args[1]) + sys.exit(2) + else: + return int(frame_end - frame_start + 1) + + + def download_blender_file_from_s3(uri): + """Downloads the blend file from S3 and stores it locally. + + Keyword arguments: + uri -- S3 URI of the file to download + """ + + uri_components = uri.split('s3://')[1].split('/') + bucket = uri_components[0] + file = uri_components[1] + + s3 = boto3.resource('s3') + s3.meta.client.download_file(bucket, file, '/tmp/{}'.format(file)) + + return '/tmp/{}'.format(file) + + + def calculate_array_job_size(file, frames_per_job): + """Calculates the size of the job array + + Keyword arguments: + file -- Blender file to analyse + frames_per_job -- Number of frames each Batch job has to render + """ + + # Get the scene's number of frames by reading the header of the blender file + n_frames = get_number_of_frames(file) + + # Adjust the number of frames per job if needed + frames_per_job = min(frames_per_job, n_frames) + + # Calculate how many jobs need to be submitted + return n_frames, math.ceil(n_frames / frames_per_job) + + + def lambda_handler(event, context): + # Download the blend file from s3 and save it locally to work with it + file = download_blender_file_from_s3(event['inputUri']) + + # Calculate the size of the array job and extract the number of frames + n_frames, array_job_size = calculate_array_job_size(file, int(event['framesPerJob'])) + + return { + 'statusCode': 200, + 'body': {'arrayJobSize': array_job_size} + } Role: Fn::GetAtt: - - cloud9envbootstrapLambdaServiceRole520F9D06 + - PreprocessingServiceRole532E6474 - Arn - Handler: index.handler - Runtime: python3.7 + Handler: index.lambda_handler + Runtime: python3.9 Timeout: 300 DependsOn: - - cloud9envbootstrapLambdaServiceRoleDefaultPolicy474DC27A - - cloud9envbootstrapLambdaServiceRole520F9D06 + - bucket43879C71 + - PreprocessingServiceRoleDefaultPolicyF8800D5C + - PreprocessingServiceRole532E6474 Metadata: - aws:cdk:path: RenderingWithBatchStack/cloud9env/bootstrapLambda/Resource - cloud9envbootstrapLambdaCustomResourceD5142DA7: - Type: AWS::CloudFormation::CustomResource - Properties: - ServiceToken: - Fn::GetAtt: - - cloud9envbootstrapLambda02FC7F40 - - Arn - cloud9EnvId: - Ref: cloud9envec2env8356485F - ebsSize: 40 - profile_arn: - Fn::GetAtt: - - cloud9envfisinstanceprofile96D194A9 - - Arn - UpdateReplacePolicy: Delete - DeletionPolicy: Delete - Metadata: - aws:cdk:path: RenderingWithBatchStack/cloud9env/bootstrapLambdaCustomResource/Default - ecsRole157644C0: + aws:cdk:path: RenderingStack/Preprocessing/Resource + RenderingPipelineRole4AAB9C07: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: @@ -435,27 +760,93 @@ Resources: Service: Fn::Join: - "" - - - ec2. - - Ref: AWS::URLSuffix + - - states. + - Ref: AWS::Region + - .amazonaws.com Version: "2012-10-17" - ManagedPolicyArns: - - arn:aws:iam::aws:policy/AmazonS3FullAccess - - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role Metadata: - aws:cdk:path: RenderingWithBatchStack/ecsRole/Resource - ecsinstanceprofile: - Type: AWS::IAM::InstanceProfile + aws:cdk:path: RenderingStack/RenderingPipeline/Role/Resource + RenderingPipelineRoleDefaultPolicy951F3306: + Type: AWS::IAM::Policy Properties: + PolicyDocument: + Statement: + - Action: lambda:InvokeFunction + Effect: Allow + Resource: + Fn::GetAtt: + - Preprocessing329E01E4 + - Arn + - Action: batch:SubmitJob + Effect: Allow + Resource: + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":batch:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - :job-definition/* + - Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":batch:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - :job-queue/* + - Action: + - events:PutTargets + - events:PutRule + - events:DescribeRule + Effect: Allow + Resource: + Fn::Join: + - "" + - - "arn:" + - Ref: AWS::Partition + - ":events:" + - Ref: AWS::Region + - ":" + - Ref: AWS::AccountId + - :rule/StepFunctionsGetEventsForBatchJobsRule + Version: "2012-10-17" + PolicyName: RenderingPipelineRoleDefaultPolicy951F3306 Roles: - - Ref: ecsRole157644C0 + - Ref: RenderingPipelineRole4AAB9C07 Metadata: - aws:cdk:path: RenderingWithBatchStack/ecsinstanceprofile + aws:cdk:path: RenderingStack/RenderingPipeline/Role/DefaultPolicy/Resource + RenderingPipeline477A29E4: + Type: AWS::StepFunctions::StateMachine + Properties: + RoleArn: + Fn::GetAtt: + - RenderingPipelineRole4AAB9C07 + - Arn + DefinitionString: + Fn::Join: + - "" + - - '{"Comment": "Workflow orchestration for a rendering pipeline using AWS Batch","StartAt":"Number of frames extraction","States":{"Number of frames extraction":{"Next":"Rendering","Retry":[{"ErrorEquals":["Lambda.ServiceException","Lambda.AWSLambdaException","Lambda.SdkClientException"],"IntervalSeconds":2,"MaxAttempts":6,"BackoffRate":2}],"Type":"Task","Resource":"arn:' + - Ref: AWS::Partition + - :states:::lambda:invoke","Parameters":{"FunctionName":" + - Fn::GetAtt: + - Preprocessing329E01E4 + - Arn + - '","Payload.$":"$"}, "ResultPath":"$.output"},"Rendering":{"Type":"Task","Resource":"arn:aws:states:::batch:submitJob.sync","Parameters":{"JobName.$":"$.jobName","ArrayProperties":{"Size.$":"$.output.Payload.body.arrayJobSize"},"Parameters":{"action":"render","inputUri.$":"$.inputUri","outputUri.$":"$.outputUri","framesPerJob.$":"$.framesPerJob"},"JobDefinition.$":"$.jobDefinitionArn","JobQueue.$":"$.jobQueueArn"},"Next":"Stitching","ResultPath":"$.output"},"Stitching":{"Type":"Task","Resource":"arn:aws:states:::batch:submitJob.sync","Parameters":{"JobName":"Stitching","Parameters":{"action":"stitch","inputUri.$":"$.outputUri","outputUri.$":"$.outputUri","framesPerJob.$":"$.framesPerJob"},"JobDefinition.$":"$.jobDefinitionArn","JobQueue.$":"$.jobQueueArn"},"End":true}}}' + DependsOn: + - RenderingPipelineRoleDefaultPolicy951F3306 + - RenderingPipelineRole4AAB9C07 + Metadata: + aws:cdk:path: RenderingStack/RenderingPipeline/Resource CDKMetadata: Type: AWS::CDK::Metadata Properties: - Analytics: v2:deflate64:H4sIAAAAAAAA/1VRwW7CMAz9lt1DGOXEbaxjCGnSqoK4B9eIjDauEmeoivLvSykdcPLzy9Oz/TKTs2whX1/e1MVNoDpPA5BFGbas4CxKdOQtoMiP5ttz61nkZBxbDwl5x9Q8Sh5x0lWaNZkoeuuAkMmwb6F/2xe5KPyh1rD1B4Pcc3dUkmfcqUONd/7OLZ0j0Kp3/hf3YGMYbVKuFeNFdbcxt27J6ZxTg4bFFsFbzd3akm+vA56IL+UNnHbYtLUajJ+ZKNxchncP52HZAUWBYGUosSWnmWw3pDF2UUBNvlrIsIJsZX61JXNdJqke2lWeJXvXyJD4DwI/ah5ujkKrJChpSGesm/QpygAWlo46UQWlcK9bDCiKWjWHSsnwma4ZwxtxjFEUHZ/ITOdyIWcvP07rifWGdYOyHOofxvWhZCoCAAA= + Analytics: v2:deflate64:H4sIAAAAAAAA/11RwW7CMAz9Fu4h2+DEkXUMMW1aVRBXlKZGzdrGVeKAqqr/vqRhK9sp9rPj9/y84Ev+OBNXO5dFNa9Vzvs9CVkxD516WaMrVjw5642+KIO6AU2bZMGUaHifYQ3M18Y3xVrJLqRTtNOWhJaQGjyrGgZmrf/mCy8oXRgVmtbWolSCFOqB1aLJC8H7V6dlQELDTzwwkAveH1sZ0GOasNTlnmrvcg3jqCnK0BEcRB4FRnzC7ih/m6NeAuM7t4LgKrobzS1bk/elHFXvQTqjqNsadO1I8Ad4F15xeYCmrUUc/BcJixhvH7RoFaEZiabM27Tk/bOTVdwlRh4laM83L+yJhK2sbxMkS79fo+gNc08d/NvpC1bw78d4WIIPv4TS0Za7fBhYBhadkb7kLGEzpUHcFH86at0oLEFdqHiZtKMS9cOSr/jT7MsqNTdOk2qAZ/H9BoXDYZRoAgAA Metadata: - aws:cdk:path: RenderingWithBatchStack/CDKMetadata/Default + aws:cdk:path: RenderingStack/CDKMetadata/Default Condition: CDKMetadataAvailable Outputs: Subnet1: @@ -466,11 +857,6 @@ Outputs: Ref: VpcRenderingWithBatchSubnet2SubnetDC61207B LaunchTemplateName: Value: RenderingWithBatch - BucketName: - Value: - Ref: bucket43879C71 - BlendFileName: - Value: blendfile.blend RepositoryName: Value: Ref: repository9F1A3F0B @@ -479,7 +865,19 @@ Outputs: Fn::GetAtt: - ecsinstanceprofile - Arn + BucketName: + Value: + Ref: bucket43879C71 + BlendFileName: + Value: blendfile.blend + PreprocessingLambda: + Value: + Ref: Preprocessing329E01E4 + StateMachineArn: + Value: + Ref: RenderingPipeline477A29E4 Conditions: + NotEventEngine: !Equals [!Ref EETeamRoleArn, NONE] CDKMetadataAvailable: Fn::Or: - Fn::Or: diff --git a/content/rendering-with-batch/rendering-with-batch.files/verifying_resilience.py b/content/rendering-with-batch/rendering-with-batch.files/verifying_resilience.py deleted file mode 100644 index 2fd6c708..00000000 --- a/content/rendering-with-batch/rendering-with-batch.files/verifying_resilience.py +++ /dev/null @@ -1,58 +0,0 @@ -import boto3 -import sys - - -def get_jobs_in_array(array_id): - """Returns the identifiers of the jobs inside an array job - - Keyword arguments: - array_id -- identifier of the array job - """ - - client = boto3.client('batch') - kwargs = {'arrayJobId': array_id, 'jobStatus': 'SUCCEEDED'} - job_ids = [] - - while True: - response = client.list_jobs(**kwargs) - job_ids += [job['jobId'] for job in response['jobSummaryList']] - - if 'nextToken' in response: - kwargs['nextToken'] = response['nextToken'] - else: - return job_ids - -def show_execution_attempts(job_ids): - """Shows the number of atemmps of the specified jobs, - if the attempt number is bigger than 1. - - Keyword arguments: - job_ids -- Identifiers of the target jobs - """ - - client = boto3.client('batch') - page = 0 - items_per_page = 100 - - while page * items_per_page < len(job_ids): - start_index = page * items_per_page - end_index = start_index + items_per_page - page += 1 - - response = client.describe_jobs( - jobs=job_ids[start_index:end_index] - ) - - for job in response['jobs']: - if len(job['attempts']) > 1: - print('Frame\t{}\twas attempted to render {} times'.format(job['arrayProperties']['index'], len(job['attempts']))) - - -if __name__ == "__main__": - array_id = sys.argv[1] - - # Get the identifiers of the jobs in the array job - job_ids = get_jobs_in_array(array_id) - - # Show the number of attempts per job - show_execution_attempts(job_ids) diff --git a/content/rendering-with-batch/rendering_pipeline.md b/content/rendering-with-batch/rendering_pipeline.md index b6b76992..4a7bf7c1 100644 --- a/content/rendering-with-batch/rendering_pipeline.md +++ b/content/rendering-with-batch/rendering_pipeline.md @@ -20,9 +20,7 @@ In this workshop we will use its [rendering capabilities](https://www.blender.or You can launch Blender's rendering capabilities from the command line. This allows to access Blender remotely and reduce compute resource consumption since it does not need to load a graphical interface. The Docker image that you will create will do exactly this; run a bash script that will execute Blender and pass to it some arguments needed to render a specific slice of frames. The command that will be executed is the following: -```bash -blender -b -E CYCLES -o -s -e -a -``` +``blender -b -E CYCLES -o -s -e -a`` The arguments mean the following: @@ -41,6 +39,7 @@ We will use a Blender file from [BlendSwap](https://blendswap.com/categories). * - The file must be configured to render the frames as .png files. - The file must be named **blendfile.blend**. +- The file size cannot exceed 512MB. - The more frames it has, the more compute resources you will need to render it thus impacting the costs of running the workshop. Run the following command to download the file and upload it to S3: @@ -58,6 +57,4 @@ FFmpeg is a free and open-source multimedia framework able to decode, encode, tr To concatenate multiple images and make a video out of them, you will use what in FFmpeg's wiki is referred to as [*SlideShow*](https://trac.ffmpeg.org/wiki/Slideshow). When you launch the stitching job, the Docker image that you create will execute ffmpeg from the command line and pass it some arguments needed to create the video. The command that will be executed is the following: -```bash -ffmpeg -i -``` +``ffmpeg -i `` diff --git a/content/rendering-with-batch/start/_index.md b/content/rendering-with-batch/start/_index.md index 6e2d7a9d..5cc28279 100644 --- a/content/rendering-with-batch/start/_index.md +++ b/content/rendering-with-batch/start/_index.md @@ -11,5 +11,5 @@ To start the workshop head to one of the following pages, depending whether you When you finish the workshop, don't forget to execute the commands described in [**Clean up**](/rendering-with-batch/cleanup.html) to prevent incurring in additional charges. {{% notice info %}} -The estimated completion time of this lab is **60 minutes**. Rendering the animation presented in the initial page of this workshop, you will incur in an estimated cost of up to **$15**. +The estimated completion time of this lab is **90 minutes**. Rendering the animation presented in the initial page of this workshop, you will incur in an estimated cost of up to **$15**. {{% /notice %}} diff --git a/content/rendering-with-batch/start/at-an-event.md b/content/rendering-with-batch/start/at-an-event.md index 08261cae..3c834e3c 100644 --- a/content/rendering-with-batch/start/at-an-event.md +++ b/content/rendering-with-batch/start/at-an-event.md @@ -17,9 +17,10 @@ You are now logged in to the AWS console in an account that was created for you, - An S3 bucket - An ECR repository - A Launch Template +- An AWS Step Functions state machine - An instance profile for AWS Batch compute environment - The Cloud9 environment where you will run all the commands - +You can check the CloudFormation stack by downloading the following file: [CloudFormation stack](https://raw.githubusercontent.com/awslabs/ec2-spot-workshops/master/content/rendering-with-batch/rendering-with-batch.files/stack.yaml) {{% insert-md-from-file file="rendering-with-batch/start/review-outputs.md" %}} diff --git a/content/rendering-with-batch/start/on-your-own.md b/content/rendering-with-batch/start/on-your-own.md index e9b15d11..bc5a8be2 100644 --- a/content/rendering-with-batch/start/on-your-own.md +++ b/content/rendering-with-batch/start/on-your-own.md @@ -12,6 +12,7 @@ As a first step, **download** a [CloudFormation stack](https://raw.githubusercon - An S3 bucket - An ECR repository - A Launch Template +- An AWS Step Functions state machine - An instance profile for AWS Batch compute environment - The Cloud9 environment where you will run all the commands @@ -22,6 +23,10 @@ After downloading the template, open the [CloudFormation console](https://consol 3. In the **Configure stack options** page, leave all the configuration as it is. Navigate to the bottom of the page and click on **Next**. 4. In the **Review** page, leave all the configuration as it is. Navigate to the bottom of the page, and click on **I acknowledge that AWS CloudFormation might create IAM resources** and finally on **Create stack**. +{{% notice warning %}} +It is important that you use **RenderingWithBatch** as the stack name, as later we will use that value to retrieve some outputs programmatically. +{{% /notice %}} + The stack creation process will begin. All the resources will be ready to use when the status of the stack is `CREATE_COMPLETE`. {{% insert-md-from-file file="rendering-with-batch/start/review-outputs.md" %}} diff --git a/static/images/rendering-with-batch/architecture.png b/static/images/rendering-with-batch/architecture.png index 2e9536e9..5b4295ae 100644 Binary files a/static/images/rendering-with-batch/architecture.png and b/static/images/rendering-with-batch/architecture.png differ diff --git a/static/images/rendering-with-batch/state_machine.png b/static/images/rendering-with-batch/state_machine.png new file mode 100644 index 00000000..b8121908 Binary files /dev/null and b/static/images/rendering-with-batch/state_machine.png differ diff --git a/static/images/rendering-with-batch/step-functions.png b/static/images/rendering-with-batch/step-functions.png new file mode 100644 index 00000000..c6905f11 Binary files /dev/null and b/static/images/rendering-with-batch/step-functions.png differ