2018-10-01

ecs-deployコードリーディング

ecs-deployのコードリーディングをしました。バージョンは3.4.0です。

以下のパターンで追ってみます。/ecs-deployのシェルスクリプトが実体になります。

$ ecs-deploy -c {cluster_name} -n {service_name} -i {image_uri}

最初の方は定数とメソッドが定義されていて、最後にメインの処理が書かれています。

if [ "$BASH_SOURCE" == "$0" ]; then
# ...
    # Check for AWS, AWS Command Line Interface
    require aws
    # Check for jq, Command-line JSON processor
    require jq

    # Loop through arguments, two at a time for key and value
    while [[ $# -gt 0 ]]
    do
        key="$1"

        case $key in
            -k|--aws-access-key)
                AWS_ACCESS_KEY_ID="$2"
                shift # past argument
                ;;
# ...            
            --version)
                echo ${VERSION}
                exit 0
                ;;
            *)
                usage
                exit 2
            ;;
        esac
        shift # past argument or value
    done

シェルからecs-deployのコマンドを実行した場合は$BASH_SOURCE = $0になり、分岐の中が実行されます。while, case文の中でoptionをパースしています。

requireではコマンドがインストールされていてパスが通っているかを確認しています。

function require() {
    command -v "$1" > /dev/null 2>&1 || {
        echo "Some of the required software is not installed:"
        echo "    please install $1" >&2;
        exit 4;
    }
}

ecs-deployはaws-cliとjq(JSONパースして属性値取り出す用)を使っているのでrequireで確認しています。

次にassertRequiredArgumentsSet関数で必須のオプションが指定されているかどうかをチェックしています。

    # Check that required arguments are provided
    assertRequiredArgumentsSet

    if [[ "$AWS_ASSUME_ROLE" != false ]]; then
        assumeRole
    fi

assumeRoleでは aws sts assume-roleでAssumeRoleした後、レスポンスのAPIクレデンシャルを各環境変数(AWS_ACCESS_KEY_IDなど)にセットします。

次にparseImageName関数で指定したイメージ名をパースしてecs-deploy用にイメージ名を再構築します。

    # Determine image name
    parseImageName
    echo "Using image name: $useImage"

    # Get current task definition
    getCurrentTaskDefinition
    echo "Current task definition: $TASK_DEFINITION_ARN";

    # create new task definition json
    createNewTaskDefJson

    # register new task definition
    registerNewTaskDefinition
    echo "New task definition: $NEW_TASKDEF";

getCurrentTaskDefinition関数ではaws ecs describe-servicesaws ecs describe-task-definitionでクラスタ・サービスに紐づくタスク定義を抽出します。

function getCurrentTaskDefinition() {
    if [ $SERVICE != false ]; then
      # Get current task definition arn from service
      TASK_DEFINITION_ARN=`$AWS_ECS describe-services --services $SERVICE --cluster $CLUSTER | jq -r .services[0].taskDefinition`
      TASK_DEFINITION=`$AWS_ECS describe-task-definition --task-def $TASK_DEFINITION_ARN`

      # For rollbacks
      LAST_USED_TASK_DEFINITION_ARN=$TASK_DEFINITION_ARN

      if [ $USE_MOST_RECENT_TASK_DEFINITION != false ]; then
        # Use the most recently created TD of the family; rather than the most recently used.
        TASK_DEFINITION_FAMILY=`$AWS_ECS describe-task-definition --task-def $TASK_DEFINITION_ARN | jq -r .taskDefinition.family`
        TASK_DEFINITION=`$AWS_ECS describe-task-definition --task-def $TASK_DEFINITION_FAMILY`
        TASK_DEFINITION_ARN=`$AWS_ECS describe-task-definition --task-def $TASK_DEFINITION_FAMILY | jq -r .taskDefinition.taskDefinitionArn`
      fi
    elif [ $TASK_DEFINITION != false ]; then
      # Get current task definition arn from family[:revision] (or arn)
      TASK_DEFINITION_ARN=`$AWS_ECS describe-task-definition --task-def $TASK_DEFINITION | jq -r .taskDefinition.taskDefinitionArn`
    fi
    TASK_DEFINITION=`$AWS_ECS describe-task-definition --task-def $TASK_DEFINITION_ARN`
}

createNewTaskDefJson関数では既存のタスク定義をsedで書き換えて更新用のタスク定義JSONを生成します。

function createNewTaskDefJson() {
    # Get a JSON representation of the current task definition
    # + Update definition to use new image name
    # + Filter the def
    if [[ "x$TAGONLY" == "x" ]]; then
      DEF=$( echo "$TASK_DEFINITION" \
            | sed -e "s|\"image\": *\"${imageWithoutTag}:.*\"|\"image\": \"${useImage}\"|g" \
            | sed -e "s|\"image\": *\"${imageWithoutTag}\"|\"image\": \"${useImage}\"|g" \
            | jq '.taskDefinition' )
    else
      DEF=$( echo "$TASK_DEFINITION" \
            | sed -e "s|\(\"image\": *\".*:\)\(.*\)\"|\1${useImage}\"|g" \
            | jq '.taskDefinition' )
    fi

    # Default JQ filter for new task definition
    NEW_DEF_JQ_FILTER="family: .family, volumes: .volumes, containerDefinitions: .containerDefinitions, placementConstraints: .placementConstraints"

# ...

    # Build new DEF with jq filter
    NEW_DEF=$(echo "$DEF" | jq "{${NEW_DEF_JQ_FILTER}}")

    # If in test mode output $NEW_DEF
    if [ "$BASH_SOURCE" != "$0" ]; then
      echo "$NEW_DEF"
    fi
}

 registerNewTaskDefinition関数でcreateNewTaskDefJsonで生成したJSONファイルを元に aws ecs register-task-definitionを使って新しいタスク定義を登録します。

function registerNewTaskDefinition() {
    # Register the new task definition, and store its ARN
    NEW_TASKDEF=`$AWS_ECS register-task-definition --cli-input-json "$NEW_DEF" | jq -r .taskDefinition.taskDefinitionArn`
}

-nオプションが指定されている場合は、updateServiceが実行されます。

    # update service if needed
    if [ $SERVICE == false ]; then
        if [ $RUN_TASK == true ]; then
            runTask
        fi
        echo "Task definition updated successfully"
    else
        updateService

        if [[ $SKIP_DEPLOYMENTS_CHECK != true ]]; then
          waitForGreenDeployment
        fi
    fi

    if [[ "$AWS_ASSUME_ROLE" != false ]]; then
        assumeRoleClean
    fi

updateService関数はaws ecs update-serviceでクラスター・サービス・タスク定義を引数に既存サービスを更新します。

function updateService() {
# ...
    # Update the service
    UPDATE=`$AWS_ECS update-service --cluster $CLUSTER --service $SERVICE $DESIRED_COUNT --task-definition $NEW_TASKDEF $DEPLOYMENT_CONFIG`

    # Only excepts RUNNING state from services whose desired-count > 0
    SERVICE_DESIREDCOUNT=`$AWS_ECS describe-services --cluster $CLUSTER --service $SERVICE | jq '.services[]|.desiredCount'`

# ...

}

最後にwaitForGreentDeploy関数を実行します。 aws ecs describe-servicesでサービスのデプロイ数を取得し、1であればループを抜け、そうでなければ2秒sleepしたのち再度デプロイ数をチェックします。

function waitForGreenDeployment {
  DEPLOYMENT_SUCCESS="false"
  every=2
  i=0
  echo "Waiting for service deployment to complete..."
  while [ $i -lt $TIMEOUT ]
  do
    NUM_DEPLOYMENTS=$($AWS_ECS describe-services --services $SERVICE --cluster $CLUSTER | jq "[.services[].deployments[]] | length")

    # Wait to see if more than 1 deployment stays running
    # If the wait time has passed, we need to roll back
    if [ $NUM_DEPLOYMENTS -eq 1 ]; then
      echo "Service deployment successful."
      DEPLOYMENT_SUCCESS="true"
      # Exit the loop.
      i=$TIMEOUT
    else
      sleep $every
      i=$(( $i + $every ))
    fi
  done

  if [[ "${DEPLOYMENT_SUCCESS}" != "true" ]]; then
    if [[ "${ENABLE_ROLLBACK}" != "false" ]]; then
      rollback
    fi
    exit 1
  fi
}

 タイムアウトした場合はrollbackが走ります。

rollbackは aws ecs update-serviceでtask-definitionに$LAST_USED_TASK_DEFINITION_ARNを指定します。

function rollback() {
    echo "Rolling back to ${LAST_USED_TASK_DEFINITION_ARN}"
    $AWS_ECS update-service --cluster $CLUSTER --service $SERVICE --task-definition $LAST_USED_TASK_DEFINITION_ARN > /dev/null
}

LAST_USED_TASK_DEFINITION_ARNはgetCurrentTaskDefinitionで設定されるデプロイ前の最新のタスク定義ARNになります。

このエントリーをはてなブックマークに追加