のぴぴのメモ

クラウドやLinuxとかの技術メモ

テストでプロセスをハングアップさせる方法

テストケースでプロセスハングがあり、どうやろうかと調べたので簡単にメモします。

プロセスの一時停止

対象のプロセスにSIGSTOPを送るとプロセスが一時停止します。これでプロセスのハングアップ状況を作ります。
※ アプリ側でSIGSTOPを受け付けないよう実装されている場合は効かないです。

sudo kill -s SIGSTOP <対象プロセスのPID>

プロセスの再開

再開する場合は対象のプロセスにSIGCONTを送ります。ただ正常に再開してくれるかは該当アプリの実装によります。
なのでサービスであればsystemctl restart XXXXXで再起動してしまったほうが確実です。

sudo kill -s SIGCONT <対象プロセスのPID>

なんで調べたのか

kubernetes(EKS+ワーカーノードEC2構成)の障害テストで、kubeletのハング時を再現したくて調べたものです。
ちなみにテスト結果は以下のとおりです。

  • EC2インスタンスは正常稼働のママ(インスタンスやOSは正常なので、AutoScalingでは当然検知できない)
  • kubernetesで該当ノードはNotReady状態になる
  • 該当ノード上のPodは検証ではTerminatingになり、最終的に別のノードに再配置され復旧する
  • アプリ(ALB経由の簡易的なhttpdのPod)は一時的にエラー(50x)が発生する場合も

SSH接続型のCloud9をセットアップする手順

はじめに

AWS Cloud9は、ブラウザのみでコードを記述、実行、デバッグできるクラウドベースの統合開発環境 (IDE) です。IDEの画面はこんな感じです。
f:id:nopipi:20220413211500p:plain:w500


AWS Cloud9で、AWS Cloud9サービスからCloud9を動かすEC2インスタンス or VMへの接続方式は以下の3種類があります。

  1. ダイレクトアクセス
  2. Systems Manager経由
  3. SSH接続


この記事ではSSH接続方式の場合のCloud9のセットアップ手順を説明します。

結論としては面倒なので、EC2インスタンスタイプで特にこの方式を採用するメリットはないと思います。しいて言えばCloud9をEC2インスタンス以外で動作させたい場合はこの方式になるのではないでしょうか。

手順

インスタンス作成

前提
  • Public接続ができるVPCがあること(デフォルトVPCでOK)
  • CIDR0.0.0/0からSSH(TCP Port22)へ接続可能なセキュリティーグループがあること
インスタンス作成
#設定が必要なパラメータ
PROFILE="<対象アカウントのプロファイル(デフォルトプロファイル利用時はdefaultを指定)>"
REGION="<インスタンスを作成するリージョンのコード。東京リージョンならap-northeast-1>"
SUBNET_ID="<パブリックサブネットのID>"
SG_ID="<インバウンドでsshを許可しているセキュリティーグループのID>"
KEYNAME="<EC2インスタンスにSSHログインするためのキーペア名>"

#基本固定で良いパラメータ
INSTANCE_TYPE="t2.micro"

#AMI ID取得(ubuntuイメージ利用)
UBUNTU_AMI_ID=$(aws --profile ${PROFILE} --region ${REGION} --output text \
    ec2 describe-images \
        --filters 'Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-????????' \
                      'Name=state,Values=available' \
        --query 'reverse(sort_by(Images, &CreationDate))[:1].ImageId' );
echo "
UBUNTU_AMI_ID = ${UBUNTU_AMI_ID}
"

#タグ設定
TAGJSON='
[
    {
        "ResourceType": "instance",
        "Tags": [
            {
                "Key": "Name",
                "Value": "Cloud9-SshType"
            }
        ]
    }
]'

# サーバの起動
aws --profile ${PROFILE} --region ${REGION} \
    ec2 run-instances \
        --image-id ${UBUNTU_AMI_ID} \
        --instance-type ${INSTANCE_TYPE} \
        --key-name ${KEYNAME} \
        --subnet-id ${SUBNET_ID} \
        --security-group-ids ${SG_ID} \
        --associate-public-ip-address \
        --tag-specifications "${TAGJSON}" ;

Cloud9用ユーザ準備

インスタンスへのログイン
#作成したインスタンスのPublic IP取得
PUBLIC_IP=$( aws --profile ${PROFILE} --region ${REGION} --output text \
    ec2 describe-instances \
        --filters \
            "Name=instance-state-name,Values=running" \
            "Name=tag:Name,Values=Cloud9-SshType" \
    --query 'Reservations[].Instances[].PublicIpAddress' );
echo "PUBLIC_IP = ${PUBLIC_IP}"

#インスタンスへのssh接続
ssh "ubuntu@${PUBLIC_IP}"
Cloud9用ユーザ追加
sudo adduser --quiet --gid 100 --disabled-password cloud9


Changing the user information for cloud9
Enter the new value, or press ENTER for the default
	Full Name []:
	Room Number []:
	Work Phone []:
	Home Phone []:
	Other []:
Is the information correct? [Y/n] Y

cloud9ユーザでパスワード無しでのsudoでのroot権限利用が必要なため、sudo設定を追加します。

sudo -i
echo 'cloud9 ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/99-cloud9-user
exit
cloud9ユーザのSSH公開鍵用設定

.sshフォルダを作成します。

sudo -i -u cloud9

mkdir ~/.ssh
chmod 700 ~/.ssh

ls -la |grep ssh
drwx------ 2 cloud9 users 4096 Apr 13 11:11 .ssh

exit

Cloud9の前提パッケージセットアップ

Python2.7

デフォルトではPython3しかインストールされていないので、追加でのインストールが必要なはずです。

#pythonの確認
python2 --version

#インストールされていない場合は下記コマンドでインストール
sudo apt update
sudo apt install -y python2
Node.js

恐らくインストールされていないので、Node.jsのインストールが必要なはずです。

#Node.jsの確認(多分コマンドがないはず)
node --version

#Node.jsのインストール
sudo apt update
sudo apt install -y nodejs
ホームディレクトリの権限確認

Cloud9の要件で、Cloud9実行ユーザのホームディレクトリはrwxr-xr-xに設定されている必要があります。
ubuntuでは多分変更不要と思いますが、念の為確認しあってなければ変更します。

#ホームディレクトリの確認
ls -la /home |grep cloud9

drwxr-xr-x  3 cloud9 users  4096 Apr 13 11:11 cloud9  <= drwxr-xr-xならOK

#仮に上記パーミッションでない場合は以下で設定
sudo chmod u=rwx,g=rx,o=rx /home/cloud9

Cloud9のセットアップ

セットアップに必要なパッケージのインストール

パッケージインストールでgccとmakeを利用するため、先にパッケージをインストールします。

sudo apt update
sudo apt install -y make gcc
Cloud9セットアップ

Cのビルドが走り完了までに数分かかります。

curl -L https://raw.githubusercontent.com/c9/install/master/install.sh | bash

(マネコン)Cloud9サービスでの設定

  • マネージメントコンソールでAWS Cloud9を表示します。(リージョンはどこでもOK)
  • Create environmentでCloud9環境作成のウィザードを開きます
  • 下図を参照に必要項目を入力し公開鍵をコピーします(Next stepは進まない)

f:id:nopipi:20220413204647p:plain:w600

(ubuntu)Cloud9サービスからのSSH接続設定

コピーした公開鍵を、ubuntsのcloud9ユーザに設定します。

sudo -i -u cloud9

echo "[コピーした公開鍵]" >>  ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
(マネコン)environment作成の続き
  • Next stepを実行します
  • ssh接続が成功すると確認画面が表示されるので、Create environmentを実行します。

Pythonで署名付きURLを作成し S3オブジェクトをGET/PUTしてみた

はじめに

この記事は、S3の署名付きURLの学習のために署名付きURLを利用してS3のオブジェクトをPUT/GETしたときの手順をまとめたものです。
ポイントは以下のとおりです。

  • S3の署名付きURLは、AWS CLISDK、マネージメントコンソールで生成可能
  • ただしAWS CLIでは、オブジェクトのPUT(アップロード)用の署名付きURLは不可
  • boto3でS3の署名付きURLを生成する場合は、generate_presigned_urlで生成可能(引数でPUT用/GET用を指定)

手順

環境準備

プロファイル設定
#プロファイルとリージョン設定
PROFILE="<利用したいプロファイル名:デフォルトの場合はdefaultを指定>"
REGION="<実行したいリージョン:ap-northeast-1など>"

aws --profile ${PROFILE} --region ${REGION} sts get-caller-identity
S3バケット作成
#S3バケット作成
GET_BUCKET="xxxxx-get-bucket"
PUT_BUCKET="xxxxx-put-bucket"
OBJECT="hello-world.txt"


aws --profile ${PROFILE} --region ${REGION} s3api create-bucket --bucket "${GET_BUCKET}" --create-bucket-configuration "LocationConstraint=${REGION}"
aws --profile ${PROFILE} --region ${REGION} s3api create-bucket --bucket "${PUT_BUCKET}" --create-bucket-configuration "LocationConstraint=${REGION}"

#GET用バケットへのオブジェクト配置
echo 'Hello World!!' > ${OBJECT}
aws s3 --profile ${PROFILE} --region ${REGION} cp "${OBJECT}" "s3://${GET_BUCKET}"
aws s3 --profile ${PROFILE} --region ${REGION} ls "s3://${GET_BUCKET}"
EC2用のインスタンスロール作成
#S3アクセス用のインスタンスロール作成
#IAMロール作成
POLICY='{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}'
aws --profile ${PROFILE} \
    iam create-role \
        --role-name "EC2-PreSignS3Test" \
        --assume-role-policy-document "${POLICY}" \
        --max-session-duration 43200

# SSM用カスタマー管理ポリシーのアタッチ(SSMでインスタンスログインするためで検証自体としては不要)
aws --profile ${PROFILE} \
    iam attach-role-policy \
        --role-name "EC2-PreSignS3Test" \
        --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

#S3アクセス用のインラインポリシー追加
InlinePolicyDocument='{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ForGetBucket",
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetObject"
      ],
      "Resource": [
        "arn:aws:s3:::'"${GET_BUCKET}"'",
        "arn:aws:s3:::'"${GET_BUCKET}"'/*"
      ]
    },
    {
      "Sid": "ForPutBucket",
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::'"${PUT_BUCKET}"'",
        "arn:aws:s3:::'"${PUT_BUCKET}"'/*"
      ]
    }
  ]
}'
aws --profile ${PROFILE} \
    iam put-role-policy \
        --role-name "EC2-PreSignS3Test" \
        --policy-name "AllowS3Bucket" \
        --policy-document "${InlinePolicyDocument}"

#インスタンスプロファイルの作成
aws --profile ${PROFILE} \
    iam create-instance-profile \
        --instance-profile-name "EC2-PreSignS3Test";

aws --profile ${PROFILE} \
    iam add-role-to-instance-profile \
        --instance-profile-name "EC2-PreSignS3Test" \
        --role-name "EC2-PreSignS3Test" ;

|

EC2用インスタンス作成
PubSubnetID="<インスタンスを作成するパブリックサブネットのID: subnet-xxxx>"
SgId="<インスタンスに付与するサブネットID>"
KEYNAME="<SSH接続する場合はキーペア指定>"

#最新のAmazon Linux2のAMI IDを取得します。
AL2_AMIID=$(aws --profile ${PROFILE} --region ${REGION} --output text \
    ec2 describe-images \
        --owners amazon \
        --filters 'Name=name,Values=amzn2-ami-hvm-2.0.????????.?-x86_64-gp2' \
                  'Name=state,Values=available' \
        --query 'reverse(sort_by(Images, &CreationDate))[:1].ImageId' ) ;
echo -e "PubSubnetID = ${PubSubnetID}\nSgId        = ${SgId}\nKEYNAME     = ${KEYNAME}\nAL2_AMIID   = ${AL2_AMIID}"

#インスタンスの作成
#インスタンスタイプ設定
INSTANCE_TYPE="t2.micro"

#タグ設定
TAGJSON='
[
    {
        "ResourceType": "instance",
        "Tags": [
            {
                "Key": "Name",
                "Value": "PreSignS3Test"
            }
        ]
    }
]'

# サーバの起動
aws --profile ${PROFILE} --region ${REGION} \
    ec2 run-instances \
        --image-id ${AL2_AMIID} \
        --instance-type "t2.micro" \
        --subnet-id ${PubSubnetID} \
        --security-group-ids ${SgId} \
        --associate-public-ip-address \
        --tag-specifications "${TAGJSON}" \
        --iam-instance-profile "Name=EC2-PreSignS3Test" \
        --key-name ${KEYNAME} ;  #SSH接続しない場合はこの行を削除する
インスタンス上でのAWS CLI設定
  • 作成したインスタンスにSSMまたはSSHで接続する
  • 下記コマンドを実行しAWS CLI環境をセットアップする
#AWS CLI環境の設定
REGION=$(TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"` \
              && curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed -e 's/.$//')
aws configure set region ${REGION}
aws configure set output json

#接続確認
aws sts get-caller-identity

S3署名付きURLでのオブジェクト取得(GET)

AWS CLIでのオブジェクト取得確認
#S3バケット情報の設定
GET_BUCKET="xxxxx-get-bucket"
OBJECT="hello-world.txt"

#AWS CLIでのオブジェクト確認(ファイル一覧にオブジェクトが表示されること)
aws s3 ls "s3://${GET_BUCKET}"

#AWS CLIでのオブジェクト取得確認
aws s3 cp "s3://${GET_BUCKET}/${OBJECT}" .
署名付きURLでのオブジェクト取得用のpythonスクリプト配置
  • python3とboto3のセットアップ
sudo yum -y install python3
sudo pip3 install boto3
  • get.pyというファイル名で以下のファイルを格納
#!/usr/bin/env python3
import sys
import boto3

#引数からバケットとオブジェクト情報を取得
BUCKET_NAME = sys.argv[1]
OBJECT_KEY  = sys.argv[2]
EXPIRESIN   = sys.argv[3]

url = boto3.client('s3').generate_presigned_url(
    ClientMethod='get_object',
    Params={'Bucket':  BUCKET_NAME, 'Key': OBJECT_KEY },
    ExpiresIn=EXPIRESIN)

print(url)
  • 以下のコマンドを実行して署名付きURLを生成
python3 get.py ${GET_BUCKET} ${OBJECT} 60
署名付きURLでのオブジェクトアップロードのpythonスクリプト配置

|

  • put.pyというファイル名で以下のファイルを格納
#!/usr/bin/env python3
import sys
import boto3

#引数からバケットとオブジェクト情報を取得
BUCKET_NAME = sys.argv[1]
OBJECT_KEY  = sys.argv[2]
EXPIRESIN   = sys.argv[3]

url = boto3.client('s3').generate_presigned_url(
    ClientMethod='put_object',
    Params={'Bucket':  BUCKET_NAME, 'Key': OBJECT_KEY },
    ExpiresIn=EXPIRESIN)

print(url)
python3 put.py ${PUT_BUCKET} ${OBJECT} 60
curl -X PUT --upload-file <ファイルパス> "<取得したPre-Signed URL>"
  • 補足事項
    • 上記検証でAmazon S3 から 307 Temporary Redirect レスポンスが返される場合: 作成したS3バケットバケット名がAWSの全リージョンに伝搬し切れてない場合に返されます。バケット名が伝搬するまでしばらく待ってから再実行して下さい。*1

IAM Policy Simulatorの結果をJSON/CSV形式で出力するツール

作成したIAMポリシーやPermissions BoundaryのテストのためにIAM Policy Simulatorを利用すると便利ですが、テスト用のIAMアクションをGUIで作成しないといけないとか、結果をJSONCSV形式の簡易な一覧で取得できない点が悩みでしたので、Pythonで簡単なツールを作ってみました。

f:id:nopipi:20220322233007p:plain

使い方

コードは下記のGitHubレポジトリを参照下さい。セットアップ方法も以下のGitHubレポジトリを参照下さい。

評価対象のアクション一覧を準備

IAM Policy Simulatorで評価したいアクション一覧を以下の形式のJSONで準備します。

{
    "Action": [
        "iam:CreateServiceSpecificCredential",
        "iam:DeactivateMFADevice",
        <中略>
        "iam:DeletePolicyVersion",
        "iam:ListUserTags"
    ]
}
{{<
** IAM Policy Simulatorでの評価
ツールの実行
>|sh|
./simulate_iampolicy.py [-h] [-P PROFILE] [-c] -p POLICY_SOURCE_ARN -a ACTION_LIST_FILE
  • 必須オプション
    • -p POLICY_SOURCE_ARN : IAMユーザ/グループ/ロールのARNを指定
    • -a ACTION_LIST_FILE : 評価したいアクション一覧のJSONファイルパスを指定
  • 任意オプション
    • -P PROFILE : AWS CLI/boto3のプロファイルを明示的に指定する
    • -c: 結果をCSV形式で出力したい場合に指定。未指定時はJSON形式で結果を出力する
(実行例1) IAMロールに対して、IAMのアクション一覧を評価しJSON形式で出力する
./simulate_iampolicy.py -p 'arn:aws:iam::999999999999:role/RoleName' -a 'actions/iam_all_actions.json'
(実行例2) Hogeプロファイルのアカウントの特定IAMユーザ対し、S3のアクション一覧を評価しCSV形式で出力する
./simulate_iampolicy.py -P Hoge -c -p 'arn:aws:iam::999999999999:user/UserName' -a 'actions/s3_all_actions.json'

実行結果

[
  {
    "EvalActionName": "iam:CreateServiceSpecificCredential",
    "EvalDecision": "allowed"
  },
  {
    "EvalActionName": "iam:DeactivateMFADevice",
    "EvalDecision": "explicitDeny"
  },
]
EvalActionName,EvalDecision
iam:CreateServiceSpecificCredential,allowed
iam:DeactivateMFADevice,explicitDeny
  • 結果内容
    • EvalActionName : 評価対象のアクション名
    • EvalDecision : 評価結果
      • allowed : アクションを許可
      • explicitDeny : アクションを明示的に拒否
      • implicitDeny : アクションを暗黙的に拒否

Amazon Linux2にterraformをセットアップする

Googleで検索すれば記事は多数出てくるのですが、勉強と備忘録のために記録します。

Terraformのセットアップ

Cloud9(Amazon Linux2ベース)はデフォルトでterraformが入ってるので、セットアップはスキップできます。

  • Cloud9 on Amazon Linux2 : デフォルトでインストールされているのでセットアップ不要(2021/6現在)
  • Amazon Linux2: セットアップが必要

セットアップする場合は、Terraformのドキュメントにインストール手順が記載されているので、その内容を見ながらセットアップすればOKです。
learn.hashicorp.com

TerraformのYumレポジトリ の登録

Terraformのrpmパッケージは、Terraform提供元のHashiCorpのオフィシャルのyumレポジトリ から取得することができます。(Amazon LinuxではTerraformのパッケージは配布していない)
そのため、まずHashiCorpのオフィシャルのyumレポジトリをyumに登録する必要があります。

  • yum-config-managerコマンドのインストール
sudo yum install -y yum-utils
  • HashiCorpのオフィシャルのyumレポジトリの追加
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo

Terraformのインストール

sudo yum -y install terraform

Terraformのテスト

terraform -help

フォワードプロキシ(NLB+EC2(AutoScaling))を作成するCloudFormation

概要

NLB + EC2(Squid)構成のフォワードプロキシを作成するCloudFormationのテンプレートです。
ソースコードこちらのGitHubにあります。

  • クライアントはNLBに対してTCPの3128ポートにプロキシ接続してアクセスして利用します。
  • プロキシはSquidを利用
  • SquidのログはLogsに出力(CloudWatchエージェントを利用)

利用方法

前提条件

  • パブリックサブネットを有するVPCがあること
  • CloudFormationのデプロイ用にAdministratorAccess権限のある実行環境があること(CLIまたはマネージメントコンソール)
  • CLIを利用する場合はAWS CLIがセットアップされ、上記のAdministratorAccessで実行可能なプロファイルが設定済みであること
  • gitコマンドが利用可能なこと

(オプション)AWS CLIのための準備

手順でAWS CLIを利用する場合は事前に以下の設定を行う

export PROFILE="default"       #Specify a profile with AdministratorAccess privileges
export REGION="ap-northeast-1" #Specify Region

#動作テスト
aws --profile ${PROFILE} --region ${REGION} sts get-caller-identity

#以下のようにUserID/Account/Arnが表示されればOK
{
    "UserId": "AROAZKIQKBDEIBOE2MGEJ:botocore-session-xxxxxxxxxx",
    "Account": "999999999999",
    "Arn": "arn:aws:sts::999999999999:assumed-role/OrganizationAccountAccessRole/botocore-session-xxxxxxxxxx"
}

コードのダウンロード

git clone https://github.com/Noppy/CFn_ForwardProxy.git
cd CFn_ForwardProxy

設定情報の取得

CloudFormationのスタック作成時のパラメーターに設定する以下の情報を事前に控えておく

  • VPC構成情報
    • VPC ID
    • フォワードプロキシのEC2インスタンスを配置するパブリックサブネットのID(マルチAZ構成の場合は複数確認)
    • NLBを配置するプライベートまたはパブリックサブネットのID(マルチAZ構成の場合は複数確認)
  • Amazon Linux2(x86_64)のAMI ID
AMZ2_AMIID=$(aws --profile ${PROFILE} --region ${REGION} --output text \
    ec2 describe-images \
        --owners amazon \
        --filters 'Name=name,Values=amzn2-ami-kernel-5.10-hvm-2.0.????????.?-x86_64-gp2' \
                      'Name=state,Values=available' \
    --query 'reverse(sort_by(Images, &CreationDate))[:1].ImageId' ) ;
echo "AMZ2_AMIID = ${AMZ2_AMIID}"

CloudFormationによるデプロイ

  • マネージメントコンソールの場合:
    • 具体的な操作手順は、CloudFormationのユーザーガイドを参照下さい。
    • テンプレートは、./src/forward_proxy.yaml
    • パラメータ: 上記の「設定情報の取得」で確認した情報を設定します。
  • CUIの場合:
# 「設定情報の取得」で確認した情報を設定
VPCID="<VPC ID>" 
PubSubIds="<PubSub1Id>,<PubSub2Id>,<Pubsub3Id>"
ElbSubIds="<ElbSub1Id>,<Elbsub2Id>,<ElbSub3Id>"
SrcCidr="<CIDR of VPC>"

# パラメータ用のJSON生成
CFN_STACK_PARAMETERS='
[
    {
        "ParameterKey": "VpcId",
        "ParameterValue": "'"${VPCID}"'"
    },
    {
        "ParameterKey": "PublicSubnets",
        "ParameterValue": "'"${PubSubIds}"'"
    },
    {
        "ParameterKey": "ElbSubnets",
        "ParameterValue": "'"${ElbSubIds}"'"
    },
    {
        "ParameterKey": "ProxyAmiId",
        "ParameterValue": "'"${AMZ2_AMIID}"'"
    },
    {
        "ParameterKey": "AllowCidrBlockForProxy",
        "ParameterValue": "'"${SrcCidr}"'"
    }
]'

# スタックの作成
aws --profile ${PROFILE} --region ${REGION} \
    cloudformation create-stack \
        --stack-name "ForwardProxy" \
        --template-body "file://./src/forward_proxy.yaml" \
--parameters "${CFN_STACK_PARAMETERS}" \
--capabilities CAPABILITY_IAM ;

NLBのDNS取得

  • マネージメントコンソールの場合:
    • 作成したスタックのOutputsで確認する
  • CUIの場合:
ProxyDns=$(aws --profile ${PROFILE} --region ${REGION} --output text \
    cloudformation describe-stacks \
        --stack-name "ForwardProxy" \
        --query 'Stacks[].Outputs[?OutputKey==`LoadBalancerDns`].[OutputValue]')
echo "ProxyDns = ${ProxyDns}"

テスト

Linuxサーバをクライアントとしてテストする場合の手順をいかに示します。

ProxyDns="<「NLBのDNS取得」で取得したDNS情報>"
export http_proxy="http://${ProxyDns}:3128"
export https_proxy="http://${ProxyDns}:3128"

# Test
# "https://www.google.co.jp" is allowed, "https://www.yahoo.co.jp" is denied.
curl https://www.google.co.jp
curl https://www.yahoo.co.jp

Amazon Linux2にCloudWatchエージェントを設定する手順(ユーザデータでの手順あり)

  • はじめに
  • CloudWatchエージェントとは*1
  • CloudWatchエージェント設定手順概要
  • CloudWatchエージェントの初期設定手順
    • (事前準備) IAMロール&EC2インスタンスの作成
    • CloudWatchエージェントのインストール
    • CloudWatch設定ファイル作成
    • CloudWatch設定ファイルのフェッチ
    • CloudWatchエージェントの起動*5
    • 補足
      • 設定フェッチと再起動を1コマンドで実行する
      • CloudWatchエージェントのログファイル
  • Systems Manager Parameter Storeで設定を配布する場合
    • Parameter Storeへの設定ファイル格納
    • CloudWatchエージェントを稼働させるAmazon Linux2サーバでの操作
  • ユーザデータでCloudWatchエージェントを設定する

はじめに

Amazon Linux2にCloudWatchエージェントをセットアップする手順をまとめました。ドキュメントではSSMから操作する手順をメインにしていますが、AutoScalingなどでUserDataにセットアップ手順を埋め込むことを考慮し全てコマンドラインでセットアップが完結するように手順を整理しています。

ポイントは以下の4点です。

  1. エージェントの操作はamazon-cloudwatch-agent-ctlを利用する
  2. Wizardや手動作成した設定ファイルはフェッチしないと有効化されない
  3. OS自動起動amazon-cloudwatch-agent-ctlからのエージェント起動時に自動設定される
  4. collectdを未使用時でも、Wizardではデフォルトcollectdを有効化するためcollectdパケージをインストールしておくほうが無難

なおCloudWatchエージェントの標準のカスタムメトリクスとログ取得の範囲で利用している場合は、collectdとStatsDは必要ありません。

CloudWatchエージェントとは*1

CloudWatchエージェントは、EC2やオンプレのLinux/Windowsで稼働し、OSやアプリケーションの情報をCloudWatchに送信するエージェントソフトウェアです。
具体的には以下の情報をCloudWatchに送信することができます。

  • OSの稼働情報(CPU/メモリ/ディスクなど)を取得し、カスタムメトリクスとしてCloudWatchへ送信する
  • (Option)CloudWatch エージェントで定義されていない稼働情報は、collectdプロトコルStatsDプロトコルを介して取得することでカスタムメトリクスとしてCloudWatchへ送信する事が可能
  • (Option)指定したログファイルをCloudWatch Logsへ送信する

f:id:nopipi:20220212130803p:plain:w600

なおcollectdStatsDの要否は以下の通りです。
CloudWatchエージェントの標準のカスタムメトリクスとログ取得の範囲で利用している場合は、どちらの機能も不要となります。

  • collectd: CloudWatchエージェントで準備されているメトリクス(CPU/メモリ/ディスク)以外に追加のメトリクスを独自取得したい場合に利用する
  • StatsD: ユーザアプリケーションから出力される稼働データをCloudWatchエージェントからカスタムメトリクスとして送信したい場合(ユーザアプリでStatsDプロトコルでの送信の実装が必要)

CloudWatchエージェント設定手順概要

CloudWatchエージェントは、Amazon Linux2の場合/opt/aws/amazon-cloudwatch-agentにインストールされ、操作用のコマンドは/opt/aws/amazon-cloudwatch-agent/binに配置されます。主に利用するコマンドは、(1)設定ファイル生成用ウィザードのamazon-cloudwatch-agent-config-wizardと、(2)操作用のamazon-cloudwatch-agent-ctl、の2つになります。

CloudWatchエージェントを利用するための手順概要は以下の通りです。

  1. CloudWatchエージェントのインストール
  2. 設定ファイルの作成(手動またはamazon-cloudwatch-agent-config-wizardによる作成)
  3. 設定ファイルのフェッチ
  4. CloudWatchエージェントの起動

f:id:nopipi:20220212135002p:plain:w600

続きを読む

AutoScaling Groupや起動テンプレートでEC2インスタンスの送信元/送信先チェックを変更する方法

EC2インスタンスの送信元/送信先チェック(source/destination checking)を、AutoScalingするインスタンスに設定しようとしても、Auto Scalingグループや起動テンプレートでは、送信元/送信先チェックの設定用のパラメータがないため設定できない。そのため、インスタンスのユーザデータでインスタンスIDとリージョン情報を取得して無理くり実行するしかない。

ということで起動サンプルが以下です。

設定

ユーザデータ

ユーザでは、EC2インスタンスメタデータからインスタンスIDとリージョン情報を取得して、AWS CLIを利用し自分自身のEC2インスタンスの設定変更を行います。
こちらでは、Instance Metadata Service Version 2 (IMDSv2)に対応した書き方になっています。

#!/bin/sh

# Disable Source/Destination Check 
yum -y install jq
INSTANCE_ID=$( TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"` \
    && curl -H "X-aws-ec2-metadata-token: $TOKEN" -v "http://169.254.169.254/latest/meta-data/instance-id");

REGION=$( TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"` \
    && curl -H "X-aws-ec2-metadata-token: $TOKEN" -v "http://169.254.169.254/latest/dynamic/instance-identity/document" | \
    jq -r '.region' )

aws --region "${REGION}" ec2 modify-instance-attribute --instance-id "${INSTANCE_ID}" --source-dest-check '{"Value": false}'

EC2インスタンスロールに付与するIAMポリシー

実行するEC2インスタンスには、以下のIAMポリシーが付与されたEC2インスタンスロール が必要です。こちらの権限がないとAccessDenyでawsコマンド実行時にアベンドします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "ec2:ModifyInstanceAttribute"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}

AWS CLIのAssumeRoleするプロファイル設定を使って、AssumeRoleのクレデンシャルを取得し環境変数に設定するシェル芸 ワンライナーを作ってみた

見やすいようにバックスラッシュで行を分けてますが、ワンライナーです。
terraformをローカルマシンで実行する時に、AWSプロバイダーの部分にローカルマシン固有のプロファイルとか埋め込みたくないけど、都度クレデンシャルを手で環境変数に設定するのは手間だということで作ってみたものです。

AWS_BASE_PROFILE="default"; \
AWS_ASSUME_PROFILE="<assume先のプロファイル>"; \
aws --profile ${AWS_BASE_PROFILE} \
    sts assume-role \
        --role-arn $(aws configure get ${AWS_ASSUME_PROFILE}.role_arn) \
        --role-session-name ${AWS_ASSUME_PROFILE}-session \
    --query 'Credentials.{
              AWS_ACCESS_KEY_ID:AccessKeyId,
              AWS_SECRET_ACCESS_KEY:SecretAccessKey,
              AWS_SESSION_TOKEN:SessionToken
    }' | \
jq -r 'to_entries | .[] | [.key + " " + .value]|@tsv' |
while read key value; do export $key=$value; done

1行にするとこんな感じ。

AWS_BASE_PROFILE="default"; AWS_ASSUME_PROFILE="<assume先のプロファイル>"; aws --profile ${AWS_BASE_PROFILE} sts assume-role --role-arn $(aws configure get ${AWS_ASSUME_PROFILE}.role_arn) --role-session-name ${AWS_ASSUME_PROFILE}-session --query 'Credentials.{AWS_ACCESS_KEY_ID:AccessKeyId,AWS_SECRET_ACCESS_KEY:SecretAccessKey,AWS_SESSION_TOKEN:SessionToken}' |jq -r 'to_entries | .[] | [.key + " " + .value]|@tsv' |while read key value; do export $key=$value; done

terraformでSlackチャンネルを作成するための手順

概要

terraformからpablovarela/slackプロバイダーを利用し、slackのchannelを作成するための手順です。

slackを外部のアプリケーションから操作するためには、slackでアプリケーション(App)を作成する必要があります。作成したAppに必要な権限を付与し、App操作のためのTOKENを利用しterrafomから実行します。今回利用したpablovarela/slackプロバイダーは、環境変数SLACK_TOKENでTOKENをterraformに渡せますのでその方式を利用します。(セキュリティーの面からコードへのTOKEN埋め込みは避けたいため)

あとはterraformでコーディングして実行して、Slackのチャンネルを作成します。

f:id:nopipi:20210921001409p:plain

手順

SlackのApp作成

terraformで操作を行うため、SlackのApp(ポット)を作成します。

f:id:nopipi:20210920224224p:plain:w400

  • Create an appFrom scratchを選択

f:id:nopipi:20210920224833p:plain:w400

  • Appの名前とワークスペースを選択する
    • App Name : 今回はexecute-terraformという名称を設定
    • Pick a workspace to develop your app in: Appをインストールしたいワークスペースを指定する

f:id:nopipi:20210920225258p:plain:w400

  • Appのボット用TOKENに権限を付与する
    • 左のバーからOAuth & Permissionsを選択する
    • Scopesで、 Bot Token ScopesAdd an OAuth Scopeから必要な権限を追加する
    • 権限は以下の6つを追加する
      • terraformのResourceslack_conversation用権限
        • channels:manage
        • channels:read
        • groups:write
        • groups:read
      • terraformのData sourceslack_user用権限
        • users:read
        • users:read.email

f:id:nopipi:20210920230936p:plain:w400

f:id:nopipi:20210920231856p:plain:w400

f:id:nopipi:20210920231950p:plain:w400

  • ワークスペースへのAppインストール
    • 同じ画面上部のOAuth Tokens for Your Workspaceに移動し、Install to Workspace>を実行
    • 選択先の画面で許可を行う
  • TOKENの取得
    • Bot User OAuth Tokenに表示されているトークンを取得する

f:id:nopipi:20210920233403p:plain

terrafomのコード

terrafomのセットアップは、こちらを参照ください。

SlackのTOKEN設定

terrafomでSlackを操作するSlack Providerには、環境変数を利用しTOKENを渡します。

export SLACK_TOKEN="<Appで生成したtoken>"

terrafomでのSlackチャンネル作成のサンプルコード

Slack チャンネル作成用のサンプルです。
terraformの実用的な実装を考慮しmodule構成にはしていますが、blogでの視認性の観点からファイル分割や環境毎のディレクトリ分割はしていません。

.
├── main.tf
└── modules
    └── slack_channel
        └── module_main.tf
  • ./main.tf: プロバイダー設定 & モジュールの呼び出し
#----------------------------
# slack provider
#----------------------------
terraform {
  required_providers {
    slack = {
      source  = "pablovarela/slack"
      version = "~> 1.0"
    }
  }
  required_version = ">= 0.13"
}

provider "slack" {}

#----------------------------
# main
#----------------------------
module "slack_channelt" {
  source = "./modules/slack_channel"

  name             = "sample-channel"
  topic            = "This is sample channel for terrafom demo."
  slack_users_mail = ["clausvalca813@gmail.com", "n.fujita74@gmail.com"]
}
  • ./modules/slack_channel/module_main.tf: 最小モジュールのサンプル
#---------------------------
# Variable設定
#---------------------------
variable "name" {
  type = string
}

variable "topic" {
  type = string
}

variable "slack_users_mail" {
  type = list(string)
}

#---------------------------
# Provider設定
#---------------------------
terraform {
  required_providers {
    slack = {
      source  = "pablovarela/slack"
      version = "~> 1.0"
    }
  }
  required_version = ">= 0.13"
}

#---------------------------
# channel設定
#---------------------------
# 追加するユーザのメール情報からユーザIDの配列を生成
data "slack_user" "users" {
  for_each = toset(var.slack_users_mail)

  email = each.key
}

locals {
  users_list = [for i in data.slack_user.users : i.id]
}


# Create a Slack channel
resource "slack_conversation" "individual_alert_channel" {
  name              = var.name
  topic             = var.topic
  permanent_members = local.users_list
  is_archived       = false
  is_private        = false
}

ポイントは以下の通りです。

  • provider設定
    • 今回利用したプロバイダーは、 required_providersを設定しないと動作しないので要設定
    • module側でも同じ設定が必要
    • 設定内容は、こちらのドキュメントを参照
  • チャンネルに参加させるユーザ
    • slack_userでメールアドレスからSlackユーザを特定し、localsでユーザIDを配列にする

terraformの実行

terraform init
terraform apply

実行時の注意点

  • terraform destroyなどでチャンネルを削除しても、チャンネルはアーカイブされるだけで削除はされない
    • そのため再度チャンネルを作成しようとすると、エラー(name_taken: A channel cannot be created with the given name)が発生する
    • 再作成する場合は、事前にチャンネル管理者によりアーカイブされた該当チャンネルを削除する必要がある

Red Hat Developer ProgramのサブスクリプションはRed Hat DeveloperからRHELイメージをダウンロードすると自動的に付与される件

Red Hat Developer Programのに参加すると、無料のRed Hat Enterprise Linuxサブスクリプションを取得することができます。RHELのを取得できるのも嬉しいですが、redhat
ナレッジベースを参照できるようになるのが嬉しいです。
ちなみに、この無償プログラムは2016/4から提供されています。
また2020/12には、16 システムまでのプロダクション環境での利用に拡張されています。
www.redhat.com

(1)Red Hat Developer Programに参加してサブスクリプションを取得する

こちらの記事にまとまっていたのでこちらは割愛します。
qiita.com

(2)サブスクリプションの更新

Red Hat Developer Programのサブスクリプション"Red Hat Developer Subscription"は、他のサブスクリプション同様有効期限が1年です。
有効期限切れた場合、Red Hat DeveloperからRHELイメージをダウンロードすると、自動的に新しいRed Hat Developer Subscriptionが付与されました。

更新

  • 2018/9/9 初版
  • 2021/9/19 Red Hat Developerの最新ページに合わせ更新

Lambda(python)で特定のロググループ & ログストリームにログ出力するサンプルコード

Lambdaで特定のロググループ & ログストリームにログ出力するコードのサンプルです。
この例では、eventの内容をLogsに出力しています。

コードは以下の記事の内容をベースに一部最適化してます。

import json
import time
import boto3

logGroupName = "security-alaert"
logStreamName = "development"
  
def lambda_handler(event, context):

    #Get Session
    client = boto3.client('logs')

    #Put Log Event
    put_logs(client, logGroupName, logStreamName, "Received event:{0}".format( json.dumps(event)))

    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }


def put_logs(client, group_name, stream_name_prefix, message):
    try:
        #Set Logs Event Data
        log_event = {
            'timestamp': int(time.time()) * 1000,
            'message': message
        }
        
        #Set Flags
        exist_log_stream = True
        sequence_token = None
        while True:
            break_loop = False
            try:
                if exist_log_stream == False:
                    #Create LogGroup
                    try:
                        client.create_log_group(logGroupName=group_name)
                    except client.exceptions.ResourceAlreadyExistsException:
                        pass
                    #Create LogStream
                    client.create_log_stream(
                        logGroupName = group_name,
                        logStreamName = stream_name_prefix)
                    exist_log_stream = True
                    #Write First event log
                    client.put_log_events(
                        logGroupName = group_name,
                        logStreamName = stream_name_prefix,
                        logEvents = [log_event])
                    break_loop = True
                elif sequence_token is None:
                    client.put_log_events(
                        logGroupName = group_name,
                        logStreamName = stream_name_prefix,
                        logEvents = [log_event])
                else:
                    client.put_log_events(
                        logGroupName = group_name,
                        logStreamName = stream_name_prefix,
                        logEvents = [log_event],
                        sequenceToken = sequence_token)
                    break_loop = True
            except client.exceptions.ResourceNotFoundException as e:
                exist_log_stream = False
            except client.exceptions.DataAlreadyAcceptedException as e:
                sequence_token = e.response.get('expectedSequenceToken')
            except client.exceptions.InvalidSequenceTokenException as e:
                sequence_token = e.response.get('expectedSequenceToken')
            except Exception as e:
                print(e)
            
            if break_loop:
                break
    except Exception as e:
        print(e)

macOS zshのプロンプトにgitブランチをカラー表示してみる

やりたいこと

macOSzshのプロンプトにgitのブランチを表示するようにします。
表示はカラー表示させます。

今回の環境は以下の通りです。

  • ProductName: Mac OS X
  • ProductVersion: 10.15.7
  • BuildVersion: 19H1323

gitセットアップに関する他の関連記事はこちら。

セットアップ手順

モジュールのダウンロード

cd
mkdir .zsh
curl https://raw.githubusercontent.com/git/git/master/contrib/completion/git-completion.zsh -o ~/.zsh/git-completion.zsh
curl https://raw.githubusercontent.com/git/git/master/contrib/completion/git-prompt.sh -o ~/.zsh/git-prompt.sh

git-completion.zshgit-prompt.shの2つのファイルが空でない状態であることを確認します。

ls  -l .zsh
total 56
-rw-r--r--  1 XXXXXX  XXXX   7133  9 12 15:48 git-completion.zsh
-rw-r--r--  1 XXXXXX  XXXX  17781  9 12 15:48 git-prompt.sh

ログインシェルへの追加

ログインプロンプトに表示するための設定を~/.zshrcに追加します。

cat >> ~/.zshrc << EOL

# For git
fpath=(~/.zsh $fpath)
if [ -f ${HOME}/.zsh/git-completion.zsh ]; then
       zstyle ':completion:*:*:git:*' script ~/.zsh/git-completion.zsh
fi
if [ -f ${HOME}/.zsh/git-prompt.sh ]; then
       source ${HOME}/.zsh/git-prompt.sh
fi
GIT_PS1_SHOWDIRTYSTATE=true
GIT_PS1_SHOWUNTRACKEDFILES=true
GIT_PS1_SHOWSTASHSTATE=true
GIT_PS1_SHOWUPSTREAM=auto
setopt PROMPT_SUBST ; PS1='%F{46}mac%f: %F{051}%c%F{198}\$(__git_ps1)%f\\$ '
EOL
  • 最後の行の\$(__git_ps1)と、\\$の冒頭のバックスラッシュ(\)は、catで入力するヒアドキュメントがシェルとして評価されないようにするためのエスケープで追加したも

テスト

別のterminalを立ち上げ、gitのリポジトリディレクトリでbranchが表示されることを確認します。

AWS Organizationsのメンバーアカウントで予算設定するためには、先に管理アカウントでAWS Cost Explorerを有効化しないといけない件

困ったこと

AWS Organizationsのメンバーアカウントで予算設定をしようとしたところ、先にpayer account(AWS Organizationsの管理アカウント)側でbudgetsを有効化するようにというメッセージが出てエラーになりました。

BUDGET_CONFIG_JSON='
{
   "BudgetName": "Example Budget",
   "BudgetType": "COST",
    "BudgetLimit": {
        "Amount": "100",
        "Unit": "USD"
    },
    "TimeUnit": "MONTHLY"
}'

aws budgets create-budget \
    --account-id 999999999999 \
    --budget "${BUDGET_CONFIG_JSON}"    

発生したエラー内容

An error occurred (AccessDeniedException) when calling the CreateBudget operation: Account 999999999999 is a linked account. To enable budgets for your account, ask the payer account to enable budgets first.

解決方法

エラーメッセージには、ask the payer account to enable budgets first.とありましたが、実機で確認したところ正確には管理アカウントでAWS Cost Explorerを有効化する必要がありました。

管理アカウントのAWS Cost Explorerの有効化方法

AWS Organizationsの管理アカウントにアクセスします。
AWS Cost Explorerは、マネコンでAWS Cost Explorerを起動することで有効化されます。CLI(API)によるAWS Cost Explorer有効化はできません。*1

  • AWS Organizationsの管理アカウントの管理者権限のあるユーザで、マネージメントコンソールにサインインして
  • Billing and Cost Management コンソールを開く https://console.aws.amazon.com/billing/
  • 左のナビゲーションペインから[Cost Explorer]を選択し、
  • Cost Explorer を起動
  • 有効化されるまで24時間待つ
  • AWS Organizationsの管理アカウントでCost Explorer が利用可能になったことを確認します

メンバーアカウントでのBudget作成

  • Budgetを作成したメンバーアカウントにアクセスできるようにします(マネコン/CLIどちらでも可)
  • Budgetを作成する
BUDGET_CONFIG_JSON='
{
   "BudgetName": "Example Budget",
   "BudgetType": "COST",
    "BudgetLimit": {
        "Amount": "100",
        "Unit": "USD"
    },
    "TimeUnit": "MONTHLY"
}'

aws budgets create-budget \
    --account-id 999999999999 \
    --budget "${BUDGET_CONFIG_JSON}"    
  • 作成したBudgetを確認する
aws budgets describe-budgets --account-id 999999999999
{
    "Budgets": [
        {
            "BudgetName": "Example Budget",
            "BudgetLimit": {
                "Amount": "100.0",
                "Unit": "USD"
            },
            "CostTypes": {
                "IncludeTax": true,
                "IncludeSubscription": true,
                "UseBlended": false,
                "IncludeRefund": true,
                "IncludeCredit": true,
                "IncludeUpfront": true,
                "IncludeRecurring": true,
                "IncludeOtherSubscription": true,
                "IncludeSupport": true,
                "IncludeDiscount": true,
                "UseAmortized": false
            },
            "TimeUnit": "MONTHLY",
            "TimePeriod": {
                "Start": "2021-09-01T09:00:00+09:00",
                "End": "2087-06-15T09:00:00+09:00"
            },
            "CalculatedSpend": {
                "ActualSpend": {
                    "Amount": "0.048",
                    "Unit": "USD"
                }
            },
            "BudgetType": "COST",
            "LastUpdatedTime": "2021-09-05T15:37:34.817000+09:00"
        }
    ]
}

gitのpull reqやpushで更新があったディレクトリを抽出しJOSNの配列にするシェル芸

やりたいこと

  • system毎にディレクトリが分かれているterraformがあり(下記イメージ)、パイプラインで更新があったプロジェクトのディレクトリを判別してterraformを実行したい。
  • 具体的には、git diffを使って差分があるディレクトリを抽出し、それをJOSNの配列にしたい。
  • 差分は、mainブランチとの差分で、1コミット分の差分抽出でOK
  • シェルで実現したい
.
├── README.md
├── modules
│   ├── network
│   ├── resource
│   └── security
└── projects
    ├── system-a
    │   ├── backend.tf
    │   ├── data.tf
    │   ├── local.tf
    │   ├── main.tf
    │   ├── provider.tf
    │   └── terraform.tf
    ├── system-b
    └── system-c

やりかた

mainへのpull requestの場合のmainコミットとの差分抽出

TARGET_BRANCH=main
git fetch --depth 1 origin ${TARGET_BRANCH}  > /dev/null 2>&1
git diff  ${TARGET_BRANCH} HEAD --name-only -- 'projects/' | \
  sed 's:\(^projects/[^/]*\)*.*$:\1:' | \
  sort | uniq | \
  jq -scR 'split("\n") | .[:-1]';

push時の一つ前のコミットとの差分抽出

TARGET_BRANCH=main
git fetch --depth 2 origin ${TARGET_BRANCH}  > /dev/null 2>&1
git diff HEAD^ HEAD --name-only -- 'projects/' | \
  sed 's:\(^projects/[^/]*\)*.*$:\1:' | \
  sort | uniq | \
  jq -scR 'split("\n") | .[:-1]';

実行例

TARGET_BRANCH=main
git fetch --depth 1 origin ${TARGET_BRANCH}  > /dev/null 2>&1
git diff  ${TARGET_BRANCH} HEAD --name-only -- 'projects/' | \
  sed 's:\(^projects/[^/]*\)*.*$:\1:' | \
  sort | uniq | \
  jq -scR 'split("\n") | .[:-1]';
["projects/system-a","projects/system-b","projects/system-c"]

解説

  • git fetchgit diff
TARGET_BRANCH=main
git fetch --depth 1 origin ${TARGET_BRANCH}  > /dev/null 2>&1
git diff  ${TARGET_BRANCH} HEAD --name-only -- 'projects/' 

projects/system-a/backend.tf
projects/system-a/data.tf
projects/system-a/local.tf
projects/system-a/main.tf
projects/system-a/provider.tf
projects/system-b/Makefile
projects/system-b/backend.tf
projects/system-c/dummy
  • sed 's:\(^projects/[^/]*\)*.*$:\1:' |
    • sedでprojects直下のフォルダまでのパスまでだけ抽出しています。
    • sedの書き方は、こちらを参照ください
    • /projects/xxxxxのパスまで絞り込めますが、この段階では同じディレクトリが重複した状態になります。
projects/system-a
projects/system-a
projects/system-a
projects/system-a
projects/system-a
projects/system-b
projects/system-b
projects/system-c
  • /sort | uniq/
    • uniqコマンドで重複している行を削除します。uniqの前提としてソートされている必要があるため、あらかじめsortコマンドで行をソートします。
    • ここまでの実行結果
projects/system-a
projects/system-b
projects/system-c
  • jq -scR 'split("\n") | .[:-1]'
    • jqコマンドでJSONの配列に変換しています。