16
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Wano GroupAdvent Calendar 2023

Day 2

aws-cli のラッパースクリプトを書いて作業を超効率化する

Posted at

この記事はWano Group Advent Calendar 2023の2日目の記事となります。
1日目は、@masafumi330 の 全エンジニアにおすすめする本「世界一流エンジニアの思考法」 です。そちらもどうぞ!

また、グループ会社のEDOCODE(メインで仕事してます)のAdvent Calendarもありますので、よろしければそちらもどうぞ。

この記事に書いていること

以前からAWSは使ってはいるのですが、そんなに突っ込んで使うことも少なく、aws-cliを使う機会もそこまで多くありませんでした。ですが、頻繁に使い出すと AWS の mangement console 触ってると、日が暮れる...とか、ストレスで死にそう...という心の声が聞こえたので、ラッパースクリプトを書いて、作業を効率化しましたよ、という話です。

注意: お使いの環境によって、多少変えないといけないところがあるかと思いますが、そのへんはよしなにしてください。また、フィルターとかもっとうまくかけるんじゃない?という話もあるかと思いますが、コメントで教えてもらえれば幸いです。

aws-cli のラッパースクリプトを書くメリット

  • 新しい人がチームに入ったときに設定等がスムーズに行える
  • AWS management console でクリックをたどっていくストレスがなくなる
  • 人間が覚えるには難しい内容をラッパースクリプト内に押し込める
  • 地味にめんどくさい単純計算をする必要がなくなる
  • 人間が覚えるには多いが、9割くらいは毎回同じ引数をラッパースクリプトに押し込める
  • 体感10-100倍くらいは効率化できる(かも)

こんな感じで、気分的にも体感でも作業効率がかなり上がること間違いないです。

準備: aws-cli のプロファイル名をチーム内で統一する

まず、これをやりましょう。統一することにはメリットしかないので、統一しましょう。統一しない場合、「俺の考えた最高に格好いいプロファイル名を使える」というメリットはあるかもしれませんが、格好いいプロファイル名をチームで共有すれば解決です!

統一するメリットとしては:

  • 統一していれば、slackでこのコマンド叩いてと、コピペで渡したものが、そのまま使えます。統一していないと、profileを書き換える必要があります
  • 複数のプロファイルを切り替える必要があるときに、ラッパーコマンド内で、デフォルトのプロファイルを決めることができます

「今回のプロジェクトはこの名前で統一する!」と独断と偏見で決めてしまえば良いので、簡単です。

  • project-name-dev
  • project-name-stg
  • project-name-prod

とか、こんな感じで良いと思います。

こんなんでもありですね(いや、ない)。

  • project-name-bronze
  • project-name-silver
  • project-name-gold

作ったラッパースクリプト

  • aws-sso ... aws sso のラッパー
  • aws-logs ... aws logs のラッパー
  • aws-pipelines ... aws codepipelines のラッパー
  • aws-ssm ... aws ssm のラッパー
  • aws-ecs ... aws ecs のラッパー
  • aws-ecr ... aws ecr のラッパー

と、まぁ、(だいたい)-でつないだだけです。
なお、ほぼすべてのコマンドはjqコマンドを内部で使っていますので、事前にjqをインストールしておいてください。

ラッパースクリプトの紹介

というわけで、作ったラッパースクリプトのなかみを紹介していきますが、最初に共通の挙動について説明します。

ラッパースクリプトの共通する挙動

あまり時間を掛けたくないですし、覚えやすいように、単純な引数にしました(順番は重要)。

コマンド [環境指定] サブコマンド [引数...]

という感じです。指定されなかったときは、helpを表示するというようにしています。
スクリプトの冒頭は、全部ほぼ一緒で、下記のようになっています。
環境指定がないときは、devが使われるようになっています。

#!/bin/bash -e

# project-name は適当に変更してください
if [[ "$1" =~ ^-(dev|stg|prod)$ ]]; then
  AWS_PROFILE="project-name-$1";
  shift
fi

# project-name は適当に変更してください
if [ "$AWS_PROFILE" = "" ]; then
  AWS_PROFILE=project-name-dev # 指定がなければdevを使う
fi

# クラスタ名のところは、適当に変更してください
if [[ "$AWS_PROFILE" =~ dev ]]; then
   CLUSTER_NAME=dev-cluster
elif [[ "$AWS_PROFILE" =~ stg ]]; then
   CLUSTER_NAME=stg-cluster
elif [[ "$AWS_PROFILE" =~ prod ]]; then
   CLUSTER_NAME=prod-cluster
fi

また、それぞれのコマンド(aws-ssoを除く)には、list コマンドがあります。
サブコマンドの引数となるものをリストアップする役割です。listに引数を与えた場合、その引数でgrepした結果を出力するようにしています。

listに引数なしだと、全部出力されて、

% aws-ecs list 
a-ecs
b-ecs
c-ecs

b を引数に渡すと、bが含まれるものだけ出力されます。

% aws-ecs list b
b-ecs

それでは、各コマンドの詳細について説明します。以下の説明では、上記の共通部分の続きを書いていると思ってください。

aws-sso

これを用意して置くことのメリットとしては、「プロジェクトに新しく入る人に aws-cli の設定について、ごちゃごちゃ教える必要がない」 ところです。
アカウントを準備したら、このコマンドから「aws-sso login とやれば、cli 使えるようになるので」と言うだけですみます。
残念ながら、人間には長いURLやアカウントIDなどを覚える能力はないので、ラッパースクリプト内に書いてしまうのが良いです。

※ アスタリスクの部分は適当に変えてください

if [ "$1" = "" ] || [ "$1" != "login" ]; then
  echo "$0 ... login to AWS env or setup AWS profile if first time login"
  echo
  echo "$0 [-dev,-stg,-prod] login... setup profile"
  exit 1
fi

if grep -E "\[profile $AWS_PROFILE\]" ~/.aws/config > /dev/null; then
  echo "Login to $AWS_PROFILE"
  echo
  aws sso login --profile=$AWS_PROFILE
  exit 1;
fi

cat <<EOL >> ~/.aws/config
[profile $AWS_PROFILE]
sso_region = ap-northeast-1
region = ap-northeast-1
output = json
sso_role_name = ********
EOL

if [[ "$AWS_PROFILE" =~ dev ]]; then
  echo 'sso_start_url = https://********.awsapps.com/start#' >> ~/.aws/config
  echo 'sso_account_id = ************' >> ~/.aws/config
elif [[ "$AWS_PROFILE" =~ stg ]]; then
  echo 'sso_start_url = https://********.awsapps.com/start#' >> ~/.aws/config
  echo 'sso_account_id = ************' >> ~/.aws/config
elif [[ "$AWS_PROFILE" =~ prod ]]; then
  echo 'sso_start_url = https://********.awsapps.com/start#' >> ~/.aws/config
  echo 'sso_account_id = ************' >> ~/.aws/config
fi 

echo "Login to $AWS_PROFILE"
aws sso login --profile=$AWS_PROFILE

aws-logs

AWSの management console上の cloudwathで、リアルタイムのログ見るのしんどくないですか?僕はしんどいです。

list でロググループの一覧を取得します。
tail で対象のロググループのログの取得を開始します。引数に数字を渡すと、その数字分の、分数前からのログを取得します。これで、地味に現在日時から9時間と○分前を計算するのに毎回頭のリソースが奪われることがなくなり、より仕事に集中できるようになります。grepで絞ることもできますね。

※ アスタリスクの部分は適当に変えてください

COMMAND=$1
TARGET=$2;
SINCE=$3;

help() {
  echo
  echo $0 ... aws logs wrapper command;
  echo
  echo "$0 [-dev,-stg,-prod] list [\$GREP_KEYWORD] ... list log groups"
  echo "$0 [-dev,-stg,-prod] tail \$log_group_namepull [\$sicne_time_stamp|\$minutes] ... tail log of \$log_group_name. default \$since_time_stamp is now -9 hours."
  echo
  exit 1;
}

if [ "$COMMAND" != "list" ] && [ "$COMMAND" != "tail" ]; then
  echo "COMMAND is required as 1st arg: list/tail";
  help;
fi

if [ "$COMMAND" = "tail" ]; then
  if [ "$TARGET" = "" ]; then
    echo "tail requires second arg: log group name";
    help;
  fi
fi

if [ "$COMMAND" = "list" ]; then
  CMD="cat"
  if [ "$TARGET" != "" ]; then
    CMD="grep $TARGET"
  fi
  aws logs describe-log-groups --profile $AWS_PROFILE | \
    jq -r '.logGroups[] | .logGroupName' | eval $CMD
elif [ "$COMMAND" = "tail" ]; then
  if [ "$SINCE" != "" ]; then
     if [[ "$SINCE" =~ ^[0-9]+$ ]]; then
       DIFF=$((60 * 9 + $SINCE))
       SINCE=$(date +"%Y-%m-%dT%T" --date "$DIFF minutes ago")
     fi
  else
    SINCE=$(date +"%Y-%m-%dT%T" --date "9 hours ago")
  fi
  echo "tail since: $SINCE (UTC)";
  aws logs tail --follow --profile $AWS_PROFILE --since $SINCE $TARGET
fi

aws-pipelines

コードパイプラインが、今どういう状況なんやろーというのが気になります。まぁ、Slackに通知もしてはいるのですが。watch aws-pipelines name1 name2 name3 のようにやって、terminalの片隅においておくと、いい感じです。
また、codepipelineを手動で開始したり、止めたりすることもできます。

COMMAND=$1
TARGET=$2
PIPELINE_ID=$3

help() {
  echo 
  echo "$0 ... aws codepipelines wrapper command;"
  echo
  echo "$0 [-dev,-stg,-prod] list [\$GREP_KEYWORD] ... list pipelines"
  echo "$0 [-dev,-stg,-prod] start \$pipeline_name ... start \$pipeline_name"
  echo "$0 [-dev,-stg,-prod] stop \$pipeline_name \$pipeline_id ... stop \$pipeline_name. \$pipeline_id is shown as a result of start command"
  echo "$0 [-dev,-stg,-prod] status \$pipeline_name [\$pipeline_name ...] ... show status of \$pipeline_name(s)"
  echo
  exit 1;
}

if [ "$COMMAND" != "list" ] && [ "$COMMAND" != "start" ] && [ "$COMMAND" != "status" ] && [ "$COMMAND" != "stop" ] ; then
  echo "COMMAND is required as 1st arg: list/start/stop/status";
  help;
fi

if [ "$COMMAND" = "start" ] || [ "$COMMAND" = "status" ] || [ "$COMMAND" = "stop" ] ; then
  if [ "$TARGET" = "" ]; then
    echo "start requires second arg: pipeline name";
    help;
  fi
  if [ "$COMMAND" = "stop" ] && [ "$PIPELINE_ID" = "" ]; then
    echo "stop requires third arg: pipeline id";
    help;
  fi
fi

if [ "$COMMAND" = "list" ]; then
  CMD="cat"
  if [ "$TARGET" != "" ]; then
    CMD="grep '$TARGET'"
  fi
  aws codepipeline --profile $AWS_PROFILE list-pipelines | jq -r '.pipelines[] | .name' | eval $CMD
elif [ "$COMMAND" = "start" ]; then
  echo "$COMMAND: $TARGET @ $AWS_PROFILE"
  aws codepipeline --profile $AWS_PROFILE start-pipeline-execution --name $TARGET | \
      jq -r '\"pipelineExecutionId: \" + .pipelineExecutionId'
elif [ "$COMMAND" = "stop" ]; then
  echo "$COMMAND: $TARGET @ $AWS_PROFILE"
  aws codepipeline --profile $AWS_PROFILE stop-pipeline-execution \
      --pipeline-name $TARGET --pipeline-execution-id $PIPELINE_ID | jq -r
elif [ "$COMMAND" = "status" ]; then
  shift
  for TARGET in $@; do
    echo "$COMMAND: $TARGET @ $AWS_PROFILE"
    aws codepipeline --profile $AWS_PROFILE  get-pipeline-state --name $TARGET | \
        jq -r '.stageStates[] | [.stageName, .latestExecution.status, .latestExecution.pipelineExecutionId, .inboundExecution.status, .inboundExecution.pipelineExecutionId ] | @tsv'
  done
fi

aws-ecs

コンテナにログインしたいときに使います。

login と言いながら、引数にコマンド渡せるので、exec とかのが良かったかもしれませんが、後付なので、loginのままになってます。exec だと引数ないのも違和感を感じますしね。

TASK_ARN="";
COMMAND=$1
CONTAINER_NAME=$2
CMD=$3

help() {
  echo 
  echo $0 ... aws ecs wrapper command;
  echo
  echo $0 [-dev,-stg,-prod] list [\$GREP_KEYWORD] ... list containers
  echo $0 [-dev,-stg,-prod] login \$container_name [CMD] ... login to \$container_name. if CMD is given, execute CMD instead of bash
  echo
  echo "-n flag is dry-run. just show aws cli command."
  exit 1;
}

if [ "$COMMAND" != "list" ] && [ "$COMMAND" != "login" ] ; then
  echo "COMMAND is required as 1st arg: list/login";
  help;
fi

if [ "$COMMAND" = "start" ] && [ "$CONTAINER_NAME" = "" ]; then
  echo "CONTAINER_NAME is required as 2st arg of login";
  help;
fi

if [ "$COMMAND" = "login" ]; then
  echo "$COMMAND: $CONTAINER_NAME @ $AWS_PROFILE"
  TASK_ARN=$(aws ecs --profile $AWS_PROFILE list-tasks --cluster $CLUSTER_NAME | \
		 jq -r '.taskArns[]' | xargs -L 100 \
         aws ecs --profile $AWS_PROFILE describe-tasks --cluster $CLUSTER_NAME --tasks | \
		 jq -r '.tasks[].containers[] | select(.lastStatus == "RUNNING") | select(.name == "'$CONTAINER_NAME'") | .taskArn');
  
  if [ "$CMD" = "" ]; then
    CMD='/bin/bash'
  fi

  aws ecs execute-command \
      --cluster $CLUSTER_NAME \
      --task $TASK_ARN \
      --profile $AWS_PROFILE \
      --container $CONTAINER_NAME \
      --interactive \
      --command "$CMD"
elif [ "$COMMAND" = "list" ]; then
  CMD="cat"
  if [ "$CONTAINER_NAME" != "" ]; then
    CMD="grep '$CONTAINER_NAME'"
  fi
  aws ecs --profile $AWS_PROFILE list-tasks --cluster $CLUSTER_NAME | \
      jq -r '.taskArns[]' | xargs -L 100 -r \
      aws ecs --profile $AWS_PROFILE  describe-tasks --cluster $CLUSTER_NAME --tasks  | \
      jq -r '.tasks[].containers[] | select(.lastStatus == \"RUNNING\") |.name' | \
      eval $CMD;
fi

aws-ssm

ssm でログインしたいのですが、残念ながら、instance のIDなどは人は覚えることができませんし、覚える意味もありません。

TASK_ARN="";
COMMAND=$1
TARGET=$2
CMD=$3

help() {
  echo 
  echo $0 ... aws ssm wrapper command;
  echo
  echo $0 [-dev,-stg,-prod] list [\$GREP_KEYWORD] ... list ssm target
  echo $0 [-dev,-stg,-prod] login \$instanceId ... login to \$instanceId
  echo
  exit 1;
}

if [ "$COMMAND" != "list" ] && [ "$COMMAND" != "login" ] ; then
  echo "COMMAND is required as 1st arg: list/login";
  help;
fi

if [ "$COMMAND" = "login" ] && [ "$TARGET" = "" ]; then
  echo "InstanceId is required as 2nd arg of login";
  help;
fi

if [ "$COMMAND" = "login" ]; then
  echo "$COMMAND: $TARGET @ $AWS_PROFILE"
  
  aws ssm --profile=$AWS_PROFILE start-session --target $TARGET
elif [ "$COMMAND" = "list" ]; then
  CMD="cat"
  if [ "$TARGET" != "" ]; then
    CMD="grep '$TARGET'"
  fi
  aws ec2 --profile=$AWS_PROFILE describe-instances | \
      jq -r '.Reservations[] | .Instances[] | [(.NetworkInterfaces[] | .Groups[0] | .GroupName), .InstanceId] | @tsv' | \
      eval $CMD
fi

aws-ecr

このプロジェクトでは、環境ごとにECRも用意されていたので、URLが異なります。人はECRのURLなんてものは覚えられないので、ラッパースクリプト内に書いておきます。

※ アスタリスクの部分は適当に変えてください

COMMAND=$1
TARGET=$2;
TAG=$3;

help() {
  echo 
  echo $0 ... aws ecr wrapper command;
  echo
  echo $0 [-dev,-stg,-prod] login... docker login to ecr
  echo $0 [-dev,-stg,-prod] list [\$GREP_KEYWORD] ... list repotisory name and URI
  echo $0 [-dev,-stg,-prod] pull \$repository_name [\$tag_name] ... docker pull latest image of \$repository_name
  echo $0 [-dev,-stg,-prod] tags \$repository_name ... list tags of \$repository_name
  echo
  exit 1;
}

if [ "$COMMAND" != "list" ] && [ "$COMMAND" != "tags" ] && [ "$COMMAND" != "pull" ] && [ "$COMMAND" != "login" ]; then
  echo "COMMAND is required as 1st arg: list/tags/pull/login";
  help;
fi

if [ "$COMMAND" = "pull" ] || [ "$COMMAND" = "tags" ]; then
  if [ "$TARGET" = "" ]; then
    echo "tags/pull requires second arg: repository name";
    help;
  fi
fi

if [ "$COMMAND" = "list" ]; then
  CMD="cat"
  if [ "$TARGET" != "" ]; then
    CMD="grep $TARGET"
  fi
  aws ecr describe-repositories --profile $AWS_PROFILE | jq -r '.repositories[] | [.repositoryName, .repositoryUri] | @tsv' | eval $CMD
elif [ "$COMMAND" = "login" ]; then
  if [[ "$AWS_PROFILE" =~ dev ]]; then
    aws ecr get-login-password --profile $AWS_PROFILE | docker login --username AWS --password-stdin *********.ecr.ap-northeast-1.amazonaws.com
  elif [[ $AWS_PROFILE =~ stg ]]; then
    aws ecr get-login-password --profile $AWS_PROFILE | docker login --username AWS --password-stdin *********.ecr.ap-northeast-1.amazonaws.com
  elif [[ $AWS_PROFILE =~ prod ]]; then
    aws ecr get-login-password --profile $AWS_PROFILE | docker login --username AWS --password-stdin *********.ecr.ap-northeast-1.amazonaws.com
  fi
elif [ "$COMMAND" = "pull" ]; then
  if [ "$TAG" = "" ]; then
    TAG=$(aws ecr list-images --profile $AWS_PROFILE --repository-name $TARGET | \
          jq -r '.[] |sort_by(.imageTag) | last.imageTag')  
  fi
  aws ecr describe-repositories --profile $AWS_PROFILE | \
      jq -r '.repositories[] | select(.repositoryName == \"'$TARGET'\") | .repositoryUri' | \
      xargs -i -r docker pull {}:$TAG
elif [ "$COMMAND" = "tags" ]; then
  aws ecr list-images --profile $AWS_PROFILE --repository-name $TARGET | \
      jq -r '.[] |sort_by(.imageTag) | .[].imageTag'
fi

実測

さて、実際どのくらい作業効率が良くなるのか、測ってみました。動画でとっても良かったのですが、編集スキルも時間もないので、結果だけですみません。

クラウドウォッチのログを見るのに要する時間です。

ブラウザ

  1. management console を開く(10秒)
  2. 検索にcloudwatchを入力してcloudwatchを開く(17秒)
  3. ロググループをクリックして開く(9秒)
  4. ロググループから目当てのログをインクリメンタル検索して開く(13秒)
  5. 最新のログをクリックして開く(3秒)

52秒かかっています。タブ開きすぎてブラウザ遅くなってるかもしれませんけども、いつもそんなもんなんで...。

aws-cli wrapper

  1. aws-logs list (2.5秒)
  2. 結果から目当てのものを見つける(0.5秒)
  3. aws-logs tail ↑で見つけたもの (1秒)

合計4秒くらいです。

ブラウザと比べると、当然ですが、だいぶ速いですね。

しかもですね。

terminalなら、ctrl+rとか使えますね!

1.ctrl + r + tail で検索して実行 (0.5秒)

ブラウザは2度目でもやること変わらないので、(運が良ければ)100倍以上速いですね!(強引)

結果

aws-cli wrapper(4秒) ctrl+r(0.5秒)
ブラウザ(52秒) 13倍 104倍

まぁ、ctrl+rは置いといて...、13倍?思ったより大したことないな?と思いましたかね?
でも、これ、100回繰り返すと、4800秒差が出るので、1.5時間以上の効率化となります。

それに、あなたのストレスもきっと軽減されているはずですよ。

おしまい

以上、aws-cliのラッパースクリプトの紹介でした。見ての通り、やってることはaws-clijqを使っているだけの超簡単なものです。helpとか、引数のチェックしてる行数のが多いくらいですね。

より改善するなら、list以外で引数を取るサブコマンドの実行時にエラーが出たら、listの結果から引数で絞って、一つしかなかったら、引っかかるもので実行を行うとか、2つ出たら、1 or 2 みたいな感じで、入力したら実行できるようにするとか...。

他にも1プロジェクト特化型じゃなくて、複数プロジェクトで使えるようにして、プロジェクトごとのプロファイルを作れるようにしてやれば、1コマンドできますね。これは、プロジェクト名と環境名をチーム間で統一すれば簡単にできそうです。

改善の余地はまだあるかとは思いますが、こんな簡単なものでも、体感的には10〜100倍、気分的には256倍くらいは効率良くなった気はします。ラッパースクリプトを書くのに1日もかからないので、継続的に関わるプロジェクトならかける価値はあるかと思います。

人材募集

現在、Wanoグループでは人材募集をしています。興味のある方は下記を参照してください。
JOBS | Wano Group

16
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?