Pipeline

The Blender files are put in a S3 Bucket, a script controls the pipeline operation and EC2 machines are scaled inside a VPC with two subnets, with one set doing the rendering and the other set stitching the renders together to create the final video file. This output is then sent to another S3 bucket for use by the dashboards which are used by the end users.

The following Cloudformation file is inspired by the AWS EC2 Workshops and brings up the Cloud components required to run the AWS pipeline. I explained to the DevOps specialists for the client how Blender works and what it needs to have to render each image and how it generates its output. They replicated the architecture with the structure that would work for the client’s project. After a few test runs, we got it working!

Resources:
  BlenderVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
        - Key: Id
          Value: BlenderStack/Vpc
    Metadata:
      aws:cdk:path: BlenderStack/Vpc/Resource
  BlenderSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.0.0/24
      VpcId:
        Ref: BlenderVpc
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: true
      Tags:
        - Key: aws-cdk:subnet-name
          Value: BlenderSubnet
        - Key: aws-cdk:subnet-type
          Value: Public
        - Key: Id
          Value: BlenderStack/Vpc/BlenderSubnet1
    Metadata:
      aws:cdk:path: BlenderStack/Vpc/BlenderSubnet1/Subnet
  BlenderRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: BlenderVpc
      Tags:
        - Key: Id
          Value: BlenderStack/Vpc/BlenderSubnet1
    Metadata:
      aws:cdk:path: BlenderStack/Vpc/BlenderSubnet1/RouteTable
  BlenderRtAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: BlenderRouteTable
      SubnetId:
        Ref: BlenderSubnet
    Metadata:
      aws:cdk:path: BlenderStack/Vpc/BlenderSubnet1/RouteTableAssociation
  BlenderDefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: BlenderRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: BlenderGateway
    DependsOn:
      - BlenderGatewayAttachment
    Metadata:
      aws:cdk:path: BlenderStack/Vpc/BlenderSubnet1/DefaultRoute
  BlenderSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.1.0/24
      VpcId:
        Ref: BlenderVpc
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: true
      Tags:
        - Key: aws-cdk:subnet-name
          Value: BlenderSubnet
        - Key: aws-cdk:subnet-type
          Value: Public
        - Key: Id
          Value: BlenderStack/Vpc/BlenderSubnet2
    Metadata:
      aws:cdk:path: BlenderStack/Vpc/BlenderSubnet2/Subnet
  BlenderSubnet2RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: BlenderVpc
      Tags:
        - Key: Id
          Value: BlenderStack/Vpc/BlenderSubnet2
    Metadata:
      aws:cdk:path: BlenderStack/Vpc/BlenderSubnet2/RouteTable
  BlenderSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: BlenderSubnet2RouteTable
      SubnetId:
        Ref: BlenderSubnet2
    Metadata:
      aws:cdk:path: BlenderStack/Vpc/BlenderSubnet2/RouteTableAssociation
  BlenderGateway2Attachment:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: BlenderSubnet2RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: BlenderGateway
    DependsOn:
      - BlenderGatewayAttachment
    Metadata:
      aws:cdk:path: BlenderStack/Vpc/BlenderSubnet2/DefaultRoute
  BlenderGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Id
          Value: BlenderStack/Vpc
    Metadata:
      aws:cdk:path: BlenderStack/Vpc/IGW
  BlenderGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId:
        Ref: BlenderVpc
      InternetGatewayId:
        Ref: BlenderGateway
    Metadata:
      aws:cdk:path: BlenderStack/Vpc/VPCGW
  BlenderSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: BlenderStack/securityGroup
      GroupName: BlenderSubnet
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          Description: Allow all outbound traffic by default
          IpProtocol: "-1"
      VpcId:
        Ref: BlenderVpc
    Metadata:
      aws:cdk:path: BlenderStack/securityGroup/Resource
  BlenderLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        SecurityGroupIds:
          - Fn::GetAtt:
              - BlenderSecurityGroup
              - GroupId
        TagSpecifications:
          - ResourceType: instance
            Tags:
              - Key: Id
                Value: BlenderStack/launchTemplate
          - ResourceType: volume
            Tags:
              - Key: Id
                Value: BlenderStack/launchTemplate
        UserData:
          Fn::Base64: |-
            MIME-Version: 1.0
            Content-Type: multipart/mixed; boundary="==MYBOUNDARY=="

            --==MYBOUNDARY==
            Content-Type: text/x-shellscript; charset="us-ascii"

            #!/bin/bash
            echo "ECS_CLUSTER=BlenderSpotEC2" >> /etc/ecs/ecs.config
            echo "ECS_ENABLE_SPOT_INSTANCE_DRAINING=true" >> /etc/ecs/ecs.config
            echo "ECS_CONTAINER_STOP_TIMEOUT=90s" >> /etc/ecs/ecs.config
            echo "ECS_ENABLE_CONTAINER_METADATA=true" >> /etc/ecs/ecs.config

            --==MYBOUNDARY==--
      LaunchTemplateName: BlenderSubnet
    Metadata:
      aws:cdk:path: BlenderStack/launchTemplate/Resource
  BlenderBucket:
    Type: AWS::S3::Bucket
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Metadata:
      aws:cdk:path: BlenderStack/bucket/Resource
  BlenderRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: rendering-with-batch
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Metadata:
      aws:cdk:path: BlenderStack/repository/Resource
  BlenderCloud9:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      InstanceType: t2.micro
      Name: BlenderSubnet
      SubnetId:
        Ref: BlenderSubnet
      Tags:
        - Key: SSMBootstrap
          Value: BlenderSubnet
    Metadata:
      aws:cdk:path: BlenderStack/cloud9env/ec2env/Resource
  BlenderBootstrap:
    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: BlenderStack/cloud9env/SSMDocument
  BlenderCloud9Association:
    Type: AWS::SSM::Association
    Properties:
      Name: BootstrapDocument
      Targets:
        - Key: tag:SSMBootstrap
          Values:
            - BlenderSubnet
    DependsOn:
      - BlenderCloud9CustomResource
      - BlenderBootstrap
    Metadata:
      aws:cdk:path: BlenderStack/cloud9env/SSMAssociation
  BlenderCloud9Role:
    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: SSMInstanceProfile
    Metadata:
      aws:cdk:path: BlenderStack/cloud9env/FISRole/Resource
  BlenderCloud9Profile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - Ref: BlenderCloud9Role
    Metadata:
      aws:cdk:path: BlenderStack/cloud9env/fisinstanceprofile
  BlenderCloud9ServiceRole:
    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: BlenderStack/cloud9env/bootstrapLambda/ServiceRole/Resource
  BlenderCloud9Policy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action:
              - ec2:DescribeInstances
              - ec2:ModifyVolume
              - ec2:AssociateIamInstanceProfile
              - ec2:ReplaceIamInstanceProfileAssociation
              - ec2:RebootInstances
              - iam:ListInstanceProfiles
              - iam:PassRole
              - ssm:SendCommand
            Effect: Allow
            Resource: "*"
        Version: "2012-10-17"
      PolicyName: BlenderCloud9Policy
      Roles:
        - Ref: BlenderCloud9ServiceRole
    Metadata:
      aws:cdk:path: BlenderStack/cloud9env/bootstrapLambda/ServiceRole/DefaultPolicy/Resource
  BlenderCloud9Lambda:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import time
          import boto3
          import cfnresponse


          def retrieve_cloud9_instance(env_id):
              print("Retrieving environment's instance...")

              client = boto3.client('ec2')

              return client.describe_instances(
                  Filters=[
                      {
                          'Name': 'tag:aws:cloud9:environment',
                          'Values': [
                              env_id,
                          ]
                      },
                  ]
              )['Reservations'][0]['Instances'][0]


          def resize_volume(volume_id, new_size):
              print('Resizing EBS volume...')

              client = boto3.client('ec2')

              client.modify_volume(
                  VolumeId=volume_id,
                  Size=new_size
              )

              print('EBS volume resized')


          def associate_ssm_instance_profile(c9_env_id, profile_arn):
              instance_data = retrieve_cloud9_instance(c9_env_id)
              client = boto3.client('ec2')

              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)

              print('Attaching instance profile...')

              client.associate_iam_instance_profile(
                  IamInstanceProfile={'Arn': profile_arn},
                  InstanceId=instance_data['InstanceId']
              )

              print('Instance profile associated. Restarting SSM agent...')

              client.reboot_instances(
                  InstanceIds=[
                      instance_data['InstanceId']
                  ]
              )

              print('Instance rebooted')


          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']

                  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']

                      # 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

              cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
      Role:
        Fn::GetAtt:
          - BlenderCloud9ServiceRole
          - Arn
      Handler: index.handler
      Runtime: python3.7
      Timeout: 300
    DependsOn:
      - BlenderCloud9Policy
      - BlenderCloud9ServiceRole
    Metadata:
      aws:cdk:path: BlenderStack/cloud9env/bootstrapLambda/Resource
  BlenderCloud9CustomResource:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - BlenderCloud9Lambda
          - Arn
      cloud9EnvId:
        Ref: BlenderCloud9
      ebsSize: 40
      profile_arn:
        Fn::GetAtt:
          - BlenderCloud9Profile
          - Arn
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Metadata:
      aws:cdk:path: BlenderStack/cloud9env/bootstrapLambdaCustomResource/Default
  BlenderEcsRole:
    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/AmazonS3FullAccess
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
    Metadata:
      aws:cdk:path: BlenderStack/ecsRole/Resource
  ecsinstanceprofile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - Ref: BlenderEcsRole
    Metadata:
      aws:cdk:path: BlenderStack/ecsinstanceprofile
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Analytics: v2:deflate64:H4sIAAAAAAAA/1VRwW7CMAz9lt1DGOXEbaxjCGnSqoK4B9eIjDauEmeoivLvSykdcPLzy9Oz/TKTs2whX1/e1MVNoDpPA5BFGbas4CxKdOQtoMiP5ttz61nkZBxbDwl5x9Q8Sh5x0lWaNZkoeuuAkMmwb6F/2xe5KPyh1rD1B4Pcc3dUkmfcqUONd/7OLZ0j0Kp3/hf3YGMYbVKuFeNFdbcxt27J6ZxTg4bFFsFbzd3akm+vA56IL+UNnHbYtLUajJ+ZKNxchncP52HZAUWBYGUosSWnmWw3pDF2UUBNvlrIsIJsZX61JXNdJqke2lWeJXvXyJD4DwI/ah5ujkKrJChpSGesm/QpygAWlo46UQWlcK9bDCiKWjWHSsnwma4ZwxtxjFEUHZ/ITOdyIWcvP07rifWGdYOyHOofxvWhZCoCAAA=
    Metadata:
      aws:cdk:path: BlenderStack/CDKMetadata/Default
    Condition: CDKMetadataAvailable
Outputs:
  Subnet1:
    Value:
      Ref: BlenderSubnet
  Subnet2:
    Value:
      Ref: BlenderSubnet2
  LaunchTemplateName:
    Value: BlenderSubnet
  BucketName:
    Value:
      Ref: BlenderBucket
  BlendFileName:
    Value: blendfile.blend
  RepositoryName:
    Value:
      Ref: BlenderRepository
  ECSInstanceProfile:
    Value:
      Fn::GetAtt:
        - ecsinstanceprofile
        - Arn
Conditions:
  CDKMetadataAvailable:
    Fn::Or:
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-2
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-2