AWS Elastic Beanstalk with Harness Custom Deployment

Hey all!

This is a post from the Custom Deployments series, you must have seen the amazing posts that @rohan and @lukehertert did about Google Cloud Run and Google Cloud Functions, and today I will be walking you all through a AWS Elastic Beanstalk Deployment in Harness :smiley:

In this post you can learn how to create Custom Deployments step by step, but if you want to go fast, in my Github repo you can find everything ready to import to your Harness Account using our Config As Code via Git Sync and you can find a script that uploads the templates automatically for you as well :gear:

Yes, Custom Deployment is a cool feature that allow you to build your own deployment types that works as native, leveraging all the amazing features from Harness, such as automated rollbacks, reporting, security, verification and many others.

Let’s go!

Pre-Requisites

  • Permissions to the Template Library
  • Permissions to perform Workflow, Service, and Environment Creation
  • Permission to Manage Delegate Profiles
  • Custom Deployments need to be enabled (the feature is in Beta)
  • EC2 Delegate with AWS CLI and jq installed

1. Creating the Template

Navigate to the Harness Template Library, and Select “Add Template” and click and click on Custom Deployment Type

2. Configure the Custom Deployment Type Form

  • We will be configuring how Harness will map to the user’s infrastructure and query for deployed instances on the configured infrastructure. In this case we are fetching the EC2 instances for the environment we are going to deploy.

  • User should configure the Display Name
  • Any variables needed for Infrastructure, here we have the EB EnvironmentName, Region, and the STSRole if you want to assume other IAM Role to deploy to other account for example.
  • In the Fetch Instance field, we will provide the script to collect the instances deployed by Harness on the target infrastructure
  • The user will need to provide a Host Array path, this is the path in the JSON to the host object we are looking for.
  • Here we are fetching each EC2 instance active for the Beanstalk Environment. This helps not only to use it in the workflow, but also on the Service Instance reporting.
 export AWS_DEFAULT_REGION=${infra.custom.vars.Region}
 AWS_STS_ROLE=${infra.custom.vars.stsRole}
    NAME="Harness-Assume-Role"

    if [ ! -z "$AWS_STS_ROLE" ]; then
    echo "Assuming STS Role..."

    #Cleanup current sessions
    unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_SECURITY_TOKEN

    unset AWS_ACCESS_KEY AWS_SECRET_KEY AWS_DELEGATION_TOKEN

    KST=(`aws sts assume-role --role-arn "$AWS_STS_ROLE" \
                            --role-session-name "$NAME" \
                            --query '[Credentials.AccessKeyId,Credentials.SecretAccessKey,Credentials.SessionToken]' \
    --output text`)

    export AWS_ACCESS_KEY_ID=${KST[0]}
    export AWS_SECRET_ACCESS_KEY=${KST[1]}
    export AWS_SESSION_TOKEN=${KST[2]}

    else
     echo "Skipping STS AssumeRole..."
    fi

    OUTPUT=`aws elasticbeanstalk describe-instances-health --environment-name=${infra.custom.vars.EnvironmentName}`

    echo ${OUTPUT}

    cat > $INSTANCE_OUTPUT_PATH <<EOF
    ${OUTPUT}
    EOF

This is how the template spec looks like:

3. Configure the Service Commands

Now that we have the Deployment Specification, we need to define how we deploy the services in AWS Beanstalk. Here we have a template with a few commands:

  • Prepare (Just a few validation commands)
  • Create Version (Create a new version in your EB Environment)
  • Update environment (Activate your new version)
  • Steady State Check (Check if your environment

Here are the step contents:

Prepare

    export AWS_DEFAULT_REGION=${infra.custom.vars.Region}
    AWS_STS_ROLE=${infra.custom.vars.stsRole}
    NAME="Harness-Assume-Role"
    export VERSION_LABEL=${VERSION}

    if [ ! -z "$AWS_STS_ROLE" ]; then
      echo "Assuming STS Role..."

    # Unset existing AWS session keys
      unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_SECURITY_TOKEN

      unset AWS_ACCESS_KEY AWS_SECRET_KEY AWS_DELEGATION_TOKEN

      KST=(`aws sts assume-role --role-arn "$AWS_STS_ROLE" \
                              --role-session-name "$NAME" \
                              --query '[Credentials.AccessKeyId,Credentials.SecretAccessKey,Credentials.SessionToken]' \
    --output text`)

      export AWS_ACCESS_KEY_ID=${KST[0]}
      export AWS_SECRET_ACCESS_KEY=${KST[1]}
      export AWS_SESSION_TOKEN=${KST[2]}

    else
       echo "Skipping STS AssumeRole..."
    fi

Create Version

    export AWS_DEFAULT_REGION=${infra.custom.vars.Region}
    AWS_STS_ROLE=${infra.custom.vars.stsRole}
    NAME="Harness-Assume-Role"
    export VERSION_LABEL=${VERSION}

    if [ ! -z "$AWS_STS_ROLE" ]; then
    echo "Assuming STS Role..."

    # Unset existing AWS session keys
    unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_SECURITY_TOKEN

    unset AWS_ACCESS_KEY AWS_SECRET_KEY AWS_DELEGATION_TOKEN

    KST=(`aws sts assume-role --role-arn "$AWS_STS_ROLE" \
                              --role-session-name "$NAME" \
                              --query '[Credentials.AccessKeyId,Credentials.SecretAccessKey,Credentials.SessionToken]' \
    --output text`)

    export AWS_ACCESS_KEY_ID=${KST[0]}
    export AWS_SECRET_ACCESS_KEY=${KST[1]}
    export AWS_SESSION_TOKEN=${KST[2]}

    else
    echo "Skipping STS AssumeRole... Will use the current IAM role."
    fi

    VERSION_EXISTS=`aws elasticbeanstalk describe-application-versions --application-name=${service.name} --version-labels=${VERSION_LABEL} | jq -r '.ApplicationVersions' | jq length`

    if [ $VERSION_EXISTS -gt 0 ]; then
      echo "Version already exists, Harness skipping this step..."
    else

    echo "Creating EB app version ${VERSION_LABEL} in EB app \"${service.name}\" on region ${AWS_DEFAULT_REGION}"

    aws elasticbeanstalk create-application-version --application-name "${service.name}" --description "Version created by ${deploymentTriggeredBy} on Harness." \
     --version-label "${VERSION_LABEL}" --source-bundle S3Bucket=${artifact.bucketName},S3Key=${artifact.artifactPath}
    fi

Update Environment

    export AWS_DEFAULT_REGION=${infra.custom.vars.Region}
    AWS_STS_ROLE=${infra.custom.vars.stsRole}
    NAME="Harness-Assume-Role"
    export VERSION_LABEL=${VERSION}

    if [ ! -z "$AWS_STS_ROLE" ]; then
    echo "Assuming STS Role..."

    # Unset existing AWS session keys
    unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_SECURITY_TOKEN

    unset AWS_ACCESS_KEY AWS_SECRET_KEY AWS_DELEGATION_TOKEN

    KST=(`aws sts assume-role --role-arn "$AWS_STS_ROLE" \
                              --role-session-name "$NAME" \
                              --query '[Credentials.AccessKeyId,Credentials.SecretAccessKey,Credentials.SessionToken]' \
    --output text`)

    export AWS_ACCESS_KEY_ID=${KST[0]}
    export AWS_SECRET_ACCESS_KEY=${KST[1]}
    export AWS_SESSION_TOKEN=${KST[2]}

    else
    echo "Skipping STS AssumeRole..."
    fi

    # See if EB_APP_VERSION is in the EB app
    NB_VERS=`aws elasticbeanstalk describe-applications --application-name "${service.name}" | jq '.Applications[] | .Versions[]' | grep -c "\"${VERSION_LABEL}\""`
    if [ ${NB_VERS} = 0 ];then
    	echo "No app version called \"${VERSION_LABEL}\" in EB application \"${EB_APP}\"."
    	exit 4
    fi

    aws elasticbeanstalk update-environment --environment-name ${infra.custom.vars.EnvironmentName} --version-label ${VERSION_LABEL}

Steady State Check

    export AWS_DEFAULT_REGION=${infra.custom.vars.Region}
    AWS_STS_ROLE=${infra.custom.vars.stsRole}
    NAME="Harness-Assume-Role"
    export VERSION_LABEL=${VERSION}

    if [ ! -z "$AWS_STS_ROLE" ]; then
    echo "Assuming STS Role..."

    # Unset existing AWS session keys
    unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_SECURITY_TOKEN

    unset AWS_ACCESS_KEY AWS_SECRET_KEY AWS_DELEGATION_TOKEN

    KST=(`aws sts assume-role --role-arn "$AWS_STS_ROLE" \
                              --role-session-name "$NAME" \
                              --query '[Credentials.AccessKeyId,Credentials.SecretAccessKey,Credentials.SessionToken]' \
    --output text`)

    export AWS_ACCESS_KEY_ID=${KST[0]}
    export AWS_SECRET_ACCESS_KEY=${KST[1]}
    export AWS_SESSION_TOKEN=${KST[2]}

    else
    echo "Skipping STS AssumeRole..."
    fi
    #######
    echo "Checking for Steady State..."
    APP_INFO=`aws elasticbeanstalk describe-environments --environment-name ${infra.custom.vars.EnvironmentName}`
    APP_STATUS=`echo ${APP_INFO}  | jq '.Environments[] | .Status' | sed -e 's/^"//' -e 's/"$//'`
    APP_HEALTHSTATUS=`echo ${APP_INFO}  | jq '.Environments[] | .HealthStatus' | sed -e 's/^"//' -e 's/"$//'`
    APP_HEALTH=`echo ${APP_INFO}  | jq '.Environments[] | .Health' | sed -e 's/^"//' -e 's/"$//'`

    echo "Current APP Status: " ${APP_STATUS}
    echo "Current APP Health Status" ${APP_HEALTHSTATUS}
    echo "Current APP Health" ${APP_HEALTH}

    while [ "$APP_STATUS" != "Ready" ] || [ "$APP_HEALTHSTATUS" != "Ok" ] || [ "$APP_HEALTH" != "Green" ]; do
      APP_INFO=`aws elasticbeanstalk describe-environments --environment-name ${infra.custom.vars.EnvironmentName}`
      APP_STATUS=`echo ${APP_INFO}  | jq '.Environments[] | .Status' | sed -e 's/^"//' -e 's/"$//'`
      APP_HEALTHSTATUS=`echo ${APP_INFO}  | jq '.Environments[] | .HealthStatus' | sed -e 's/^"//' -e 's/"$//'`
      APP_HEALTH=`echo ${APP_INFO}  | jq '.Environments[] | .Health' | sed -e 's/^"//' -e 's/"$//'`
      echo "---"
      echo "Checking for Steady State..."
      echo "Current APP Status: " ${APP_STATUS} " - Desired: Ready "
      echo "Current APP Health Status" ${APP_HEALTHSTATUS} " - Desired: Ok"
      echo "Current APP Health" ${APP_HEALTH} " - Desired: Green"
      sleep 2
    done

    echo "------"
    echo ${APP_INFO}

We also need to define a ShellScript template to our rollback step:

    export VERSION_LABEL=${VERSION}
    export AWS_DEFAULT_REGION=${infra.custom.vars.Region}
    AWS_STS_ROLE=${infra.custom.vars.stsRole}
    NAME="Harness-Assume-Role"

    if [ ! -z "$AWS_STS_ROLE" ]; then
      echo "Assuming STS Role..."

    # Clean existing sessions
      unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_SECURITY_TOKEN

      unset AWS_ACCESS_KEY AWS_SECRET_KEY AWS_DELEGATION_TOKEN

      KST=(`aws sts assume-role --role-arn "$AWS_STS_ROLE" \
                              --role-session-name "$NAME" \
                              --query '[Credentials.AccessKeyId,Credentials.SecretAccessKey,Credentials.SessionToken]' \
    --output text`)

      export AWS_ACCESS_KEY_ID=${KST[0]}
      export AWS_SECRET_ACCESS_KEY=${KST[1]}
      export AWS_SESSION_TOKEN=${KST[2]}

    else
       echo "Skipping STS AssumeRole..."
    fi

    APP_STATUS=`echo ${APP_INFO}  | jq '.Environments[] | .Status' | sed -e 's/^"//' -e 's/"$//'`
    APP_HEALTHSTATUS=`echo ${APP_INFO}  | jq '.Environments[] | .HealthStatus' | sed -e 's/^"//' -e 's/"$//'`
    APP_HEALTH=`echo ${APP_INFO}  | jq '.Environments[] | .Health' | sed -e 's/^"//' -e 's/"$//'`

    echo "Current APP Status: " ${APP_STATUS}
    echo "Current APP Health Status" ${APP_HEALTHSTATUS}
    echo "Current APP Health" ${APP_HEALTH}
       echo "---" 
       echo "Latest environment events ..."

    aws elasticbeanstalk describe-events  --environment-name ${infra.custom.vars.EnvironmentName} --output text --query  'Events[*].[EventDate,Severity,Message]' | head -20

    while [ "$APP_STATUS" != "Ready" ] ; do
      APP_INFO=`aws elasticbeanstalk describe-environments --environment-name ${infra.custom.vars.EnvironmentName}`
      APP_STATUS=`echo ${APP_INFO}  | jq '.Environments[] | .Status' | sed -e 's/^"//' -e 's/"$//'`
      echo "---"
      echo "Checking for Steady State..."
      echo "Current APP Status: " ${APP_STATUS} " - Desired: Ready "
      sleep 2
    done

    # See if EB_APP_VERSION is in the EB app
    NB_VERS=`aws elasticbeanstalk describe-applications --application-name "${service.name}" | jq '.Applications[] | .Versions[]' | grep -c "\"${VERSION_LABEL}\""`
    if [ ${NB_VERS} = 0 ];then
    	echo "No app version called \"${VERSION_LABEL}\" in EB application \"${app.name}\"."
    	exit 4
    fi

    aws elasticbeanstalk update-environment --environment-name ${infra.custom.vars.EnvironmentName} --version-label ${VERSION_LABEL}

4. Configure the Service

  • User will navigate to their Harness Application and create a new service. This service will be of the type of Custom Deployment. So since we called the template AWS Elastic Beanstalk, the Deployment type should appear like this:

AWS Elastic Beanstalk (Custom)

  • User should add the artifact in the service from a S3 source. This is the artifact that will be deployed to your Beanstalk Env.

If necessary you can customize the templates and leverage all Harness features like variables, secrets, etc.

5. Configure the Environment

  • Where are we going to deploy this service? You should navigate to your environment section and configure an Environment and an Infrastructure Definition.
  • The Infrastructure definition takes in the Infrastructure variables you configured in the Custom Deployment Template in step 1.

6. Configure the Workflow

  • We are going to deploy this new service to the target infrastructure we defined. In order to do so, we will create a basic workflow.

  • This workflow will already come with Fetch Instances, which allows the user to track and manage the deployed instance. It also lets the user perform CV on their deployed instances as well.

  • Add a new step and link the “Deploy AWS Beanstalk” service command from Template Library. If you have many delegates, pick your delegate selector and make sure that the Delegate have the AWS CLI and JQ installed.

  • In the rollback steps, link the “Rollback Elastic Beanstalk” template.

7. Time to Deploy

  • Finally, after the configuration, we can deploy our Elastic Beanstalk Service!
  • Hit Deploy and you should see something like this! :rocket: :ok_hand:

Conclusion

The new Harness Custom Deployments Type allows our users to bring in their own deployment styles and types and leverage Harness to manage the workloads and track the instances that are deployed. Users can now define how they want to deploy and how they want to monitor and verify it!

Check out the docs on Custom Deployments: https://docs.harness.io/article/g7m5a380kl-create-a-custom-deployment

2 Likes