【CI/CD】tfnotify と GitHub App で Terraform の CI 機構を作ってみる【CodeBuild】

こんにちは。お久しぶりです。
新しい会社に移ってから、約半年が経過しました。
周りの皆様はとてもレベルが高く、大変素晴らしい環境でお仕事をさせていただいております。
本当に心の底から転職して良かったな、と感じる今日この頃です。

さて、本日は、とあるシステムの構築に携わらせていただき、Terraform の CI 機構を CodeBuild / GitHub App / tfnotify で作る機会があったので、その際の備忘録をツラツラと残しておこうと思います。

作るもの

構成

Terraform のコード自体は GitHub のリポジトリでホストされていることを前提とします。
Pull Request の作成、更新時に CodeBuild を Hook して Terraform を実行し、plan 結果を Pull Request のコメントとして通知します。
詳細なシーケンスは以下の通りです。

① PR の作成、更新をトリガーに CodeBuild を起動。
② CodeBuild が SSM Parameter Store に格納された GitHub App の秘密鍵を取得
③ GitHub App の秘密鍵から JWT を生成し、 API を叩いて Installation ID を生成
④ Installation ID を指定して API を叩き、Access Token を生成
⑤ terraform plan を実行
⑥ plan 結果を tfnotify に渡して Pull Request に通知
⑦ Access Token を無効化

ディレクトリ構成

.
├── deploy_scripts
│   └── ci
│       ├── .tfnotify.yml
│       ├── bin
│       │   ├── create_github_token.sh
│       │   ├── install_terraform.sh
│       │   ├── install_tfnotify.sh
│       │   ├── plan_tfnotify.sh
│       │   └── revoke_github_token.sh
│       └── buildspec.yml
└── env
    └── dev
        ├── resource1
        │   ├── 00_backend.tf
        │   ├── 00_provider.tf
        │   └── vpc.tf
        ├── resource2
        │   ├── 00_backend.tf
        │   ├── 00_provider.tf
        │   └── vpc.tf
        └── resource3
            ├── 00_backend.tf
            ├── 00_provider.tf
            └── vpc.tf

Terraform のコードは /env/dev/resource[1 – 3] 配下の 3 つをエントリーポイントとするディレクトリ構成を前提とします。

また、/deploy_scripts/ci/bin 配下に GitHub App より Token を発行するスクリプトや、Terraform の実行結果を通知するためのユーティリティスクリプトを配置します。

buildspec.yml

CodeBuild の処理を定義する buildspec.yml は以下の通りです。

---
version: 0.2
  
phases:
  install:
    runtime-versions:
      golang: latest
    commands:
      - /bin/bash ${CODEBUILD_SRC_DIR}/deploy_scripts/ci/bin/install_terraform.sh
      - /bin/bash ${CODEBUILD_SRC_DIR}/deploy_scripts/ci/bin/install_tfnotify.sh
  pre_build:
    commands:
      - export APP_ID=$(aws ssm get-parameter --name ${APP_ID_SSM_ARN} --with-decryption --query Parameter.Value --output text)
      - export APP_SECRET=$(aws ssm get-parameter --name ${APP_SECRET_SSM_ARN} --with-decryption --query Parameter.Value --output text)
      - export GITHUB_TOKEN=$(/bin/bash ${CODEBUILD_SRC_DIR}/deploy_scripts/ci/bin/create_github_token.sh)
      - export REPO_OWNER=$(echo $REPO_FULL_NAME | cut -d '/' -f 1)
      - export REPO_NAME=$(echo $REPO_FULL_NAME | cut -d '/' -f 2)
      - sed -i "s/<OWNER>/$REPO_OWNER/g; s/<NAME>/$REPO_NAME/g" ${CODEBUILD_SRC_DIR}/deploy_scripts/ci/.tfnotify.yml
      - cd ${CODEBUILD_SRC_DIR}
      - terraform fmt -no-color -check -diff -recursive
  build:
    commands:
      - |
        if git --no-pager diff origin/main..HEAD --name-only | grep -E '^env/dev/resource1/'; then
          /bin/bash ${CODEBUILD_SRC_DIR}/deploy_scripts/ci/bin/plan_tfnotify.sh /env/dev/resource1/
        fi
  
      - |
        if git --no-pager diff origin/main..HEAD --name-only | grep -E '^env/dev/resource2/'; then
          /bin/bash ${CODEBUILD_SRC_DIR}/deploy_scripts/ci/bin/plan_tfnotify.sh /env/dev/resource2/
        fi
        
      - |
        if git --no-pager diff origin/main..HEAD --name-only | grep -E '^env/dev/resource3/'; then
          /bin/bash ${CODEBUILD_SRC_DIR}/deploy_scripts/ci/bin/plan_tfnotify.sh /env/dev/resource3/
        fi
  post_build:
    commands:
      - /bin/bash ${CODEBUILD_SRC_DIR}/deploy_scripts/ci/bin/revoke_github_token.sh

CodeBuild の処理自体は、先のシーケンスと同等です。
差分のあったディレクトリの plan 結果のみを通知するように、build ステージで main ブランチとの diff を取ってから、検知したディレクトリのみ Terraform の実行と通知処理を行います。

.tfnotify.yml

GitHub への通知を行うため tfnotify を用います。
通知内容のカスタマイズも可能で、以下のように yaml ファイルへ通知内容を記述し、 CLI 実行時の引数として渡してあげます。
※ <OWNER> と <NAME> は、先の buildspec.yaml の処理 ( 16 – 18 行 ) にて適切なリポジトリ情報で置換されます。

---
ci: codebuild

notifier:
  github:
    token: $GITHUB_TOKEN
    repository:
      owner: "<OWNER>"
      name: "<NAME>"

terraform:
  plan:
    template: |
      {{ .Title }} for {{ .Message }} <sup>[CI link]( {{ .Link }} )</sup>
      {{if .Result}}
      <pre><code> {{ .Result }}
      </pre></code>
      {{end}}
      <details><summary>Details (Click me)</summary>

      <pre><code>
      {{ .Body }}
      </pre></code>
      </details>
    when_add_or_update_only:
      label: "add-or-update"
    when_destroy:
      label: "destroy"
      template: |
        ## :warning: WARNING: Resource Deletion will happen :warning:

        This plan contains **resource deletion**. Please check the plan result very carefully!
    when_plan_error:
      label: "error"

Utility scripts

CLI の導入、Token の発行、通知等々のスクリプトを以下のように用意します。
※ クリックで展開します。

install_terraform.sh
#!/bin/bash

####################################
# This script is used by CodeBuild #
####################################

set -eux

TFVERSION="1.7.3"

git clone https://github.com/tfutils/tfenv.git $HOME/.tfenv
ln -s $HOME/.tfenv/bin/* /usr/local/bin
tfenv install ${TFVERSION} && tfenv use ${TFVERSION}
terraform version
install_tfnotify.sh
#!/bin/bash

####################################
# This script is used by CodeBuild #
####################################

set -eux

TNVERSION="v0.8.0"

wget https://github.com/mercari/tfnotify/releases/download/${TNVERSION}/tfnotify_linux_amd64.tar.gz
tar xzf tfnotify_linux_amd64.tar.gz
cp tfnotify /usr/local/bin/
tfnotify
plan_tfnotify.sh
#!/bin/bash

####################################
# This script is used by CodeBuild #
####################################

set -eux

echo "Run Terraform plan in ${CODEBUILD_SRC_DIR}/${1}/."

cd ${CODEBUILD_SRC_DIR}/${1}/ && \
terraform init && \
terraform plan -no-color | tfnotify --config ${CODEBUILD_SRC_DIR}/deploy_scripts/ci/.tfnotify.yml plan --message "${1}"

echo "Terraform plan executed in ${CODEBUILD_SRC_DIR}/${1}/."
create_github_token.sh
#!/bin/bash

####################################
# This script is used by CodeBuild #
####################################

base64url() {
  openssl enc -base64 -A | tr '+/' '-_' | tr -d '='
}

sign() {
  openssl dgst -binary -sha256 -sign <(printf '%s' "${APP_SECRET}")
}

header="$(printf '{"alg":"RS256","typ":"JWT"}' | base64url)"

now="$(date '+%s')"
iat="$((now - 60))"
exp="$((now + (3 * 60)))"
template='{"iss":"%s","iat":%s,"exp":%s}'
payload="$(printf "${template}" "${APP_ID}" "${iat}" "${exp}" | base64url)"

signature="$(printf '%s' "${header}.${payload}" | sign | base64url)"

jwt="${header}.${payload}.${signature}"

installation_id="$(curl --location --silent --request GET \
  --url "https://api.github.com/repos/${REPO_FULL_NAME}/installation" \
  --header "Accept: application/vnd.github+json" \
  --header "X-GitHub-Api-Version: 2022-11-28" \
  --header "Authorization: Bearer ${jwt}" \
  | jq -r '.id'
)"

token="$(curl --location --silent --request POST \
  --url "https://api.github.com/app/installations/${installation_id}/access_tokens" \
  --header "Accept: application/vnd.github+json" \
  --header "X-GitHub-Api-Version: 2022-11-28" \
  --header "Authorization: Bearer ${jwt}" \
  | jq -r '.token'
)"

echo "${token}"
revoke_github_token.sh
#!/bin/bash

####################################
# This script is used by CodeBuild #
####################################

curl --location --silent --request DELETE \
  --url "https://api.github.com/installation/token" \
  --header "Accept: application/vnd.github+json" \
  --header "X-GitHub-Api-Version: 2022-11-28" \
  --header "Authorization: Bearer ${GITHUB_TOKEN}"

AWS Resources

構成図にある CodeBuild 関連のリソースですが、こちら に Terraform の module を用意しました。
以下のように呼び出すことで必要なリソースがプロビジョニングされます。
※ GitHub App の ID と 秘密鍵 は SSM Parameter Store に SecureString として手動で登録しておき、その ARN を CodeBuild の環境変数 ( APP_ID_SSM_ARN, APP_SECRET_SSM_ARN ) として設定しておきます。
※ クリックで展開します。

呼び出しサンプル
module "cicd_terraform_ci_github" {
  source = "git::https://github.com/snkk1210/tf-m-templates.git//modules/aws/cicd/terraform/ci/github"

  common = {
    "project"      = "sample"
    "environment"  = "sandbox"
    "service_name" = "hcl"
    "type"         = "ci"
  }

  source_info = {
    location        = "https://github.com/<ORGANIZATION_NAME>/<REPOSITORY_NAME>.git"
    git_clone_depth = 1
    buildspec       = "./deploy_scripts/ci/buildspec.yml"
  }

  environment_variable = {
    variables = [
      {
        name  = "APP_ID_SSM_ARN"
        value = "arn:aws:ssm:ap-northeast-1:xxxxxxxxxxxx:parameter/path/to/id"
        type  = "PLAINTEXT"
      },
      {
        name  = "APP_SECRET_SSM_ARN"
        value = "arn:aws:ssm:ap-northeast-1:xxxxxxxxxxxx:parameter/path/to/secret"
        type  = "PLAINTEXT"
      },
      {
        name  = "REPO_FULL_NAME"
        value = "<ORGANIZATION_NAME>/<REPOSITORY_NAME>"
        type  = "PLAINTEXT"
      }
    ]
  }

  filter_groups = [
    {
      event_pattern = "PULL_REQUEST_CREATED"
      file_pattern  = "^env/dev/*"
    },
    {
      event_pattern = "PULL_REQUEST_UPDATED"
      file_pattern  = "^env/dev/*"
    },
    {
      event_pattern = "PULL_REQUEST_REOPENED"
      file_pattern  = "^env/dev/*"
    }
  ]
}

やってみる!!

以下のように /env/dev/resource1 配下のリソースを変更 ( Name タグの更新 ) し、Pull Request を作成します。

diff --git a/env/dev/resource1/vpc.tf b/env/dev/resource1/vpc.tf
index 5360514..415e5ae 100644
--- a/env/dev/resource1/vpc.tf
+++ b/env/dev/resource1/vpc.tf
@@ -2,6 +2,6 @@ resource "aws_vpc" "this" {
   cidr_block = "10.10.0.0/16"

   tags = {
-    Name = "dev-resource1-vpc.0.0.11"
+    Name = "dev-resource1-vpc.0.0.12"
   }
 }

CodeBuild が起動しました!

コメントが通知されました!

Terraform の実行結果も確認できました!

と、いった形で Terraform の CI 機構が完成しました!

終わりに

当初は GitHub の PAT ( Personal Access Token ) をそのまま使って機構を作ろうとしておりました。
しかし、セキュリティリスクの懸念があるのではないか?との指摘をいただいて、GitHub App から一時トークンを生成して利用する構成に変更した経緯があります。
これも新しい職場に転職したおかげで気づけた事柄ですね!!

これからも新天地で頑張っていきます!!
※ 本当に転職して良かった (歓喜)。

【AWS】ECR 最新イメージの脆弱性スキャン結果を CSV ライクに出力する【Bash】

こんにちは。お久しぶりでございます。
昨年の秋に受験した 情報処理安全確保支援士試験 に合格してたようで大変浮足立っております。
ただ、支援士資格に関してはコストが高額なのもあり、未登録のままにしておこうかと考えている今日この頃でございます。

さて、話は変わりますが Inspector V2 で ECR のイメージを継続的にスキャンしている環境がございまして。
複数の ECR リポジトリに存在する最新イメージにて検知された脆弱性スキャン結果のみを抽出する必要がございまして。
ちょっとしたスクリプトを組む必要が出てきまして。
わりかし今後も似たようなスクリプトを組む気がしているので、自分用のイディオムとして残しておきます。

スクリプト

ロギング ( log.sh ) / credentials ( env.sh ) の処理は別ファイルから読み込む前提とします。
また、対象とする ECR リポジトリに関しても、リスト ( list.sh ) を作成し、外部ファイルから読み込む形とします。
※ list.sh については、引数を渡して ECR リポジトリを出力する処理である前提です。

#!/usr/bin/bash

PWD=`dirname ${0}`

source ${PWD}/../etc/env.sh
. ${PWD}/../lib/log.sh

if [ $# != 1 ]; then
    echo "Error: invalid argument."
    emitLog "Error: invalid argument."
    exit 1
fi

if !(type "aws" > /dev/null 2>&1); then
    echo "Error: aws command is not found."
    emitLog "Error: aws command is not found."
    exit 1
fi

## GET LATEST IMAGE TAG

container_array=($(/bin/bash ${PWD}/../etc/list.sh ${1}))

container_index=0
for i in ${container_array[@]}
do
     tag_array[${container_index}]=`aws ecr describe-images \
          --region ap-northeast-1 \
          --repository-name ${i} \
          --query 'sort_by(imageDetails,& imagePushedAt)[-1]' --output json \
          | jq -r '.imageTags[0]'`

     let container_index++
done

## SHOW SCAN FINDING

tag_index=0
for i in ${tag_array[@]}
do
     echo "/=================================================/"
     aws ecr describe-image-scan-findings \
          --region ap-northeast-1 \
          --repository-name ${container_array[$tag_index]} \
          --image-id imageTag=${i} \
          | jq ".repositoryName, .imageId.imageTag"

     element_num=`aws ecr describe-image-scan-findings \
          --region ap-northeast-1 \
          --repository-name ${container_array[$tag_index]} \
          --image-id imageTag=${i} \
          | jq '.imageScanFindings.enhancedFindings | length'`
     
     element_num=$(($element_num - 1))
     for j in `seq 0 ${element_num}`
     do
          echo "-------------------------------------------------"
          aws ecr describe-image-scan-findings \
               --region ap-northeast-1 \
               --repository-name ${container_array[$tag_index]} \
               --image-id imageTag=${i} \
               | jq -r "[.imageScanFindings.enhancedFindings[$j].packageVulnerabilityDetails.vulnerablePackages[0].name, .imageScanFindings.enhancedFindings[$j].packageVulnerabilityDetails.vulnerabilityId, .imageScanFindings.enhancedFindings[$j].severity, .imageScanFindings.enhancedFindings[$j].score] | @csv"
     done

     let tag_index++     
done

ネストしまくりで可読性がアレですね。
もっと上手な書き方あれば教えてください。

【SNMP】標準 MIB で CPU / メモリ / Disk の使用率を計算するスクリプトを作る

こんにちは。
とあるネットワーク機器をモニタリングする必要があり、SNMP を用いたリソース情報の取得諸々について調べてみたので備忘録を残しておきます。

※ というのも普段 Linux デバイスに定義されている UCD-SNMP-MIB が定義されていなかったため、どうすればいいんだろう?と悩んだ結果です。 SNMPv2 前提です。

メモリ/ディスク

メモリ / ディスク の情報については、hrStorageDescr の OID で一覧と割り当てられた index 番号を取得できるようです。

[sanuki@sanuki-wd01 ~]$ snmpwalk -v 2c -c <CommunityName> xxx.xxx.xxx.xxx hrStorageDescr
HOST-RESOURCES-MIB::hrStorageDescr.1 = STRING: Physical memory
HOST-RESOURCES-MIB::hrStorageDescr.3 = STRING: Virtual memory
HOST-RESOURCES-MIB::hrStorageDescr.6 = STRING: Memory buffers
HOST-RESOURCES-MIB::hrStorageDescr.7 = STRING: Cached memory
HOST-RESOURCES-MIB::hrStorageDescr.8 = STRING: Shared memory
HOST-RESOURCES-MIB::hrStorageDescr.10 = STRING: Swap space
HOST-RESOURCES-MIB::hrStorageDescr.35 = STRING: /dev/shm
HOST-RESOURCES-MIB::hrStorageDescr.37 = STRING: /run
HOST-RESOURCES-MIB::hrStorageDescr.38 = STRING: /sys/fs/cgroup
HOST-RESOURCES-MIB::hrStorageDescr.55 = STRING: /
HOST-RESOURCES-MIB::hrStorageDescr.63 = STRING: /run/user/1000

※ 上記だと、index 1 が物理メモリ、index 55 がディスクの ルートパーティション となります。

index を参考に下記の OID を取得することで、Total / 使用量 / 使用率 が割り出せます。

HOST-RESOURCES-MIB::hrStorageAllocationUnits
HOST-RESOURCES-MIB::hrStorageSize
HOST-RESOURCES-MIB::hrStorageUsed

※ 計算式は下記の通りです。
Total = HOST-RESOURCES-MIB::hrStorageSize * HOST-RESOURCES-MIB::hrStorageAllocationUnits
使用量 = HOST-RESOURCES-MIB::hrStorageUsed * HOST-RESOURCES-MIB::hrStorageAllocationUnits
使用率 = 使用量 / Total * 100

諸々を bash と awk でスクリプトに纏めると下記の通りとなります。
※ シェルスクリプトだと小数点が扱えないので、演算部分は awk で賄います。
※ 第一引数に求める値(Total/使用量/使用率)、第二引数に領域の index を指定します。

#!/usr/bin/bash

# MEMO: 引数チェック
if [ $# != 2 ]; then
    echo "ERR:invalid argument"
    exit 1
fi

COMMUNITY_NAME=<CommunityName>
IP_ADDR=xxx.xxx.xxx.xxx

ALLOCATIONUNITS=`snmpwalk -v 2c -c ${COMMUNITY_NAME} ${IP_ADDR} hrStorageAllocationUnits | grep "HOST-RESOURCES-MIB::hrStorageAllocationUnits.${2} = INTEGER:" | awk '{print $4}'`
HRSTORAGESIZE=`snmpwalk -v 2c -c ${COMMUNITY_NAME} ${IP_ADDR} hrStorageSize | grep "HOST-RESOURCES-MIB::hrStorageSize.${2} = INTEGER:" | awk '{print $4}'`
HRSTORAGEUSED=`snmpwalk -v 2c -c ${COMMUNITY_NAME} ${IP_ADDR} hrStorageUsed | grep "HOST-RESOURCES-MIB::hrStorageUsed.${2} = INTEGER:" | awk '{print $4}'`

USED_MEMORY=$(( HRSTORAGEUSED * ALLOCATIONUNITS ))
ALL_MEMORY=$(( HRSTORAGESIZE * ALLOCATIONUNITS ))

USED_RATIO=`awk "BEGIN { print $USED_MEMORY / $ALL_MEMORY * 100 }"`

case ${1} in
    # NOTE: Total
    "1")
        echo ${ALL_MEMORY}
        ;;
    # NOTE: Used
    "2")
        echo ${USED_MEMORY}
        ;;
    # NOTE: Ratio
    "3")
        echo ${USED_RATIO}
        ;;
    # NOTE: Ratio
    *)
        echo ${USED_RATIO}
        ;;
esac

CPU

CPU 使用率は hrProcessorLoad の OID で1コア辺りの 使用率を参照できます。

[sanuki@sanuki-wd01 ~]$ snmpwalk -v 2c -c <CommunityName> xxx.xxx.xxx.xxx hrProcessorLoad
HOST-RESOURCES-MIB::hrProcessorLoad.196608 = INTEGER: 2
HOST-RESOURCES-MIB::hrProcessorLoad.196609 = INTEGER: 2
HOST-RESOURCES-MIB::hrProcessorLoad.196610 = INTEGER: 2
HOST-RESOURCES-MIB::hrProcessorLoad.196611 = INTEGER: 2
HOST-RESOURCES-MIB::hrProcessorLoad.196612 = INTEGER: 2
HOST-RESOURCES-MIB::hrProcessorLoad.196613 = INTEGER: 2
HOST-RESOURCES-MIB::hrProcessorLoad.196614 = INTEGER: 2
HOST-RESOURCES-MIB::hrProcessorLoad.196615 = INTEGER: 2

※ 上記だとノードに 8コアの CPU が詰まれていて、1コア辺り 2% の使用率となる。

こちらも bash と awk でスクリプトに纏めると下記の通りとなります。
※ 1 コア辺りの平均値を求めます。

#!/usr/bin/bash

COMMUNITY_NAME=<CommunityName>
IP_ADDR=xxx.xxx.xxx.xxx

TOTAL_CPU_LOAD=`snmpwalk -v 2c -c ${COMMUNITY_NAME} ${IP_ADDR} hrProcessorLoad | awk '{print $4}' | awk '{s += $1} END {print s}'`
CPU_NUM=`snmpwalk -v 2c -c ${COMMUNITY_NAME} ${IP_ADDR} hrProcessorLoad | wc -l`

echo $(( TOTAL_CPU_LOAD / CPU_NUM ))

終わりに

上 2つのスクリプトで対象の機器に対してポーリング → DB にデータ格納 でモニタリングできますね。

【jq】AWS WAF で Block されたリクエストの一覧を CSV に整形して出力してみる

こんにちは。
AWS WAF により Block されたリクエストの一覧を月一でリスト化する必要がございました。
最初は API を叩いて Sample request を取得し、その後にログ解析を行おうと思ったのですが、どうも、この API には 5000req/3h の制限があるようで断念…。
調べてみると Kinesis Firehose を経由して S3 に JSON 形式で WAF のログを出力できるようです。
ついては出力された S3 のログを引っ張って jq でリスト化すればいいや、となりました。

シェルスクリプト

cron で月末にスクリプトが実行されるよう設定しておけば、あとは放置で OK ですね。

#!/bin/bash

WORK_DIR=""
THIS_YEAR=`date "+%Y"`
THIS_MONTH=`date "+%m"`
S3_BUCKET=""
PROFILE=""
OUTPUTDIR=""
OUTPUT=${OUTPUTDIR}block_requests-`date +'%FT%R'`.csv

# 作業ディレクトリの作成
mkdir -p ${WORK_DIR}/${THIS_YEAR}/${THIS_MONTH}

# S3 からログのダウンロード
aws s3 cp s3://${S3_BUCKET}/${THIS_YEAR}/${THIS_MONTH}/ ${WORK_DIR}/${THIS_YEAR}/${THIS_MONTH} --recursive --profile=${PROFILE}

# csv のフィールド名を出力
echo "Request.ClientIP,TerminatingRuleId,Request.Country,Request.URI,Request.httpVersion,Request.Method,Other" >> ${OUTPUT}

# ログの一覧を取得
SERCH_FILES=`find ${WORK_DIR}/${THIS_YEAR}/${THIS_MONTH} -type f -name aws-waf-logs*`

for SERCH_FILE in ${SERCH_FILES}
do
    # json -> csv の変換 / パース / 出力
    cat ${SERCH_FILE} | jq -r ' select( .action == "BLOCK") | [.httpRequest.clientIp, .terminatingRuleId, .httpRequest.country, .httpRequest.uri, .httpRequest.httpVersion, .httpRequest.httpMethod, "#HeadersName", .httpRequest.headers[].name, "#HeadersValue",.httpRequest.headers[].value, "#RuleGroup", .ruleGroupList[].ruleGroupId, "#RuleMatch", .ruleGroupList[].terminatingRule.ruleId, "#RuleAction", .ruleGroupList[].terminatingRule.action] |@csv' \
    >> ${OUTPUT}
done

jq って便利ですね。

Zabbix で SSL 証明書の有効期限を監視してみる

こんにちは。
このサイトは Let’s Encrypt で証明書を取得して cron で自動更新しているんですが、昨日にサイトを確認すると証明書の期限が切れておりました。
原因を調べたところ certbot が正常に動作しなくなっておりまして、どうも先日に yum update を行った際にパッケージのバージョン関連で不整合が発生していたようです。
※こちらは pyOpenSSL を yum 管理ではなく、pip 管理にすることで解決しました。

今後も同様のことが発生した時に気づけるように Zabbix で証明書の期限を監視してみます。

外部スクリプト

証明書の情報諸々は openssl s_client で取得できるので、これの出力を整形/計算して証明書の残り期限を出力させるスクリプトを用意します。

#!/bin/bash

DOMAIN=$1
TODAY=$(date "+%Y%m%d")

PREVALIDDAY=$(echo | openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null | openssl x509 -noout -enddate | awk  -F '=' '{print $2}')
expr \( `date -d"$PREVALIDDAY" +%s` - `date -d"$TODAY" +%s` \) / 86400

後で汎用的に利用できるZabbixテンプレートを作成するので、監視する証明書のコモンネームを第一引数に指定できるようにしてます。
※CLIから実行するとこんな感じ ↓
[root@sanuki-zab50 externalscripts]# ./ssl-check.sh fingerease.work
89

Zabbix には外部スクリプトの出力結果を取り込む機能があるので、こちらのスクリプトを Zabbix サーバの下記ディレクトリ以下に ssl-check.sh という名称で配置します。
[root@sanuki-zab50 externalscripts]# pwd && ls
/usr/lib/zabbix/externalscripts
ssl-check.sh

テンプレート

Zabbix の WEB UI から証明書期限監視用のテンプレートを作成します。
※今回は S_Template_ssl_check という名称のテンプレートを作成します。

アイテム

キーに先程作成したスクリプトの名称と第一引数(証明書のコモンネーム)をマクロで指定できるように記述しておきます。

名前:cert valid days
タイプ:外部チェック
キー:ssl-check.sh[“{$DOMAIN}”]
データ型:数値(整数)

トリガー

トリガーには証明書の期限が「何日以内」となった場合にアラートを飛ばすか汎用的に設定できるよう、{$THRES} というマクロを用意しておきます。

名前:{$DOMAIN} の証明書期限が残り{$THRES}日です
条件式:{S_Template_ssl_check:ssl-check.sh[“{$DOMAIN}”].last()}<{$THRES}

監視

あとはホストを作成してマクロに監視対象のコモンネーム、監視閾値を入力すればOKです。

↓こんな感じ

{$DOMAIN} → fingerease.work
{$THRES} → 7

Zabbixを使うと割となんでも監視/グラフ化することができますね。

リストファイルを分割して複数のサーバに配布するスクリプト

JMeter での負荷試験時に slave の個数分だけ CSVファイルを分割して、それぞれの slave サーバに scp で転送したかったのです。
そんな背景で取り急ぎ作成したよくわからない何かです。

スクリプトみたいな何か

#!/bin/bash

############################################
# 第一引数に配布先マシンのIPアドレス(9つまで)
# 第二引数に分割して配布するリストファイル
############################################

# ファイルの転送先ディレクトリ
DIR=/var/tmp/dlist.txt
# 接続ユーザ
USER=vagrant
# 接続に使用する秘密鍵
SECRET_KEY=/home/vagrant/.ssh/id_rsa

# 引数の数をチェック
if [ $# != 2 ]; then
    echo "引数の数が不正です"
    exit 1
fi

# 配布先マシンの数量を取得
TARGET_COLUMN=`cat $1 | wc -l`
# リストファイルの行数を取得
LIST_COLUMN=`cat $2 | wc -l`
# 1ターゲット辺りのリスト数を取得
ONE_TARGET_NUM=$[LIST_COLUMN / TARGET_COLUMN]

# 配布先マシンのIPアドレスが9つ以上あるかチェック
if [ $TARGET_COLUMN -gt 9 ]; then
    echo "配布先IPアドレスは9つまでしか設定できません"
    exit 1
fi

# 配布先マシンの数量分だけファイルを分割する
split -l $ONE_TARGET_NUM -a 1 --numeric-suffixes=1 $2 list.

# 分割したファイルを転送する
while read VAL;
do
  NUM=$((NUM+1))
  scp -i $SECRET_KEY ./list.$NUM $USER@$VAL:$DIR

  if [ $? -ne 0 ]; then
    echo "scp コマンドが正常に終了しませんでした"
    exit 1
  fi

  # 分割したファイルを削除
  rm -f ./list.$NUM
done < $1

スクリプトの第一引数に下記のような形で転送先サーバの IP アドレスを記載したファイルを渡せば OK です。
※ 第二引数に分割したいリストファイルを指定してください。
192.168.33.11
192.168.33.12
192.168.33.13
192.168.33.14
192.168.33.15
192.168.33.16

【Python】JMeter の実行結果をスプレッドシートに出力するスクリプトを書いてみた【gspread】

こんにちは。
JMeter が出力する結果を Google のスプレッドシートに出力してみようと考えまして、調べてみると Python に便利なライブラリがあったんで作ってみました。
※ 下記の JMeter デプロイ用 Playbook にも入れているのでよければご利用くださいませ。
https://github.com/keisukesanuki/jmeter-MS

Python スクリプト

#!/usr/bin/python3

import gspread
import json
import csv
import sys
import itertools

# シークレットキーを絶対パスで指定
SECRETJSON = "/usr/local/jmeter/bin/sacred-drive.json"
# スプレッドシートキーを定義
SPREADSHEET_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

############################################################################
## 関数

# 数字と文字コンバーター
def num2alpha(num):
    if num<=26:
        return chr(64+num)
    elif num%26==0:
        return num2alpha(num//26-1)+chr(90)
    else:
        return num2alpha(num//26)+chr(64+num%26)

#############################################################################
## 認証

# お約束
from oauth2client.service_account import ServiceAccountCredentials
scope = ['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive']

# ダウンロードした json ファイルを定義
credentials = ServiceAccountCredentials.from_json_keyfile_name(SECRETJSON, scope)

# Google API にログイン
gc = gspread.authorize(credentials)

# スプレッドシートのシート1を開く
worksheet = gc.open_by_key(SPREADSHEET_KEY).sheet1


##############################################################################
## 処理

# コマンドライン引数を取得
args = sys.argv
csvfile = args[1]

# CSVファイルの内容を配列に代入
with open(csvfile) as fp:
    results_list_ex = list(csv.reader(fp))

# 2次元配列を1次元配列に変換
results_list = list(itertools.chain.from_iterable(results_list_ex))

# カウント変数初期化
COUNT_NUM = 1
# 空白行探索
while str(len(worksheet.cell(COUNT_NUM, 1).value)) != "0":
        COUNT_NUM += 1

# 編集する範囲を指定
cell_list = worksheet.range('A'+str(COUNT_NUM)+':'+num2alpha(len(results_list))+str(COUNT_NUM))

# cell_listにresults_listの配列を代入
for i,cell in enumerate(cell_list):
    cell.value = results_list[i]

# 結果の保存
worksheet.update_cells(cell_list)

第一引数に csv ファイルを指定することで、結果をスプレッドシートに出力するスクリプトです。
このスクリプトを後述の JMeter 起動用スクリプトで利用します。

シェルスクリプト

#!/bin/sh

DATE=$(date +"%Y%m%d")
OPTIME=$(date +"%Y%m%d-%H%M%S")
# 結果の出力先ディレクトリを指定
LOGDIR=/var/www/html/${DATE}
# JMXファイルを指定
FILE_JMX=/usr/local/jmeter/bin/templates/build-web-test-plan.jmx

# 日付ディレクトリの作成
mkdir -p ${LOGDIR}

# JMeter 起動
/usr/local/jmeter/bin/jmeter -Dsun.net.inetaddr.ttl=0 -n -t ${FILE_JMX} -j ${LOGDIR}/${OPTIME}.log -l ${LOGDIR}/${OPTIME}.jtl -e -o ${LOGDIR}/${OPTIME}_th${JMETER_THREAD}${2}/ -r

# CSV ファイルの作成
cat ${LOGDIR}/${OPTIME}_th${JMETER_THREAD}${2}/statistics.json | jq  -r ". [] | [.transaction,.sampleCount,.errorCount,.errorPct,.meanResTime,.minResTime,.maxResTime,.pct1ResTime,.pct2ResTime,.pct3ResTime,.throughput,.receivedKBytesPerSec,.sentKBytesPerSec] | @csv" | grep "Total" > ${LOGDIR}/${OPTIME}_th${JMETER_THREAD}${2}/statistics.csv

# スプレッドシートに結果を出力
/usr/local/bin/main.py ${LOGDIR}/${OPTIME}_th/statistics.csv

JMeter は json で結果を出力するので jq で無理やり csv に変換してます。

Python だと簡単に実装できて楽ですね。

補足

スクリプトの実行に下記のパッケージ導入が必要です。

yum install python3 python-devel jq
pip3 install gspread
pip3 install oauth2client

【AWS】EBS のスナップショットを取得して世代管理してみる【シェルスクリプト】

こんにちは。
表題の通りです。
DLM でもよいのですが、最長のインターバルが 24 時間なので、要件が合わない時のために。

#!/bin/sh

# スナップショットの保持世代数を定義
LOTATE_NUM=3
# ホストネームを定義
HOSTNAME=
# スナップショットを取得するボリュームIDを定義
VOLID=

# スナップショットを取得
aws ec2 create-snapshot --volume-id $VOLID --tag-specification 'ResourceType="snapshot",Tags=[{Key="Name",Value="script-create"}]' --description "$HOSTNAME snapshot"

# 指定した世代数分になるようにスナップショットを削除
SNAPSHOTS_NUM=`aws ec2 describe-snapshots --output text | grep $VOLID | grep "$HOSTNAME snapshot" | wc -l`
while [ ${SNAPSHOTS_NUM} -gt ${LOTATE_NUM} ]
do
        aws ec2 delete-snapshot --snapshot-id `aws ec2 describe-snapshots --output text | grep $VOLID | grep "$HOSTNAME snapshot" | sort -k 8 | awk 'NR==1 {print $7}'`
        SNAPSHOTS_NUM=`aws ec2 describe-snapshots --output text | grep $VOLID | grep "$HOSTNAME snapshot" | wc -l`
done

# awscli の導入

curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
sudo python get-pip.py
sudo pip install awscli
aws configure

【シェルスクリプト】だいたいのサーバ情報を取得する便利スクリプト【サーバ移設】

取り敢えずこのスクリプトを実行すれば、ある程度のサーバ情報を取得できるよ、ってだけのスクリプトです。

#!/bin/sh

# 情報取得関数
function addinfo(){

	# 情報を出力するファイル名を定義
	OUTPUT=serverinfo-`hostname`.txt
	# 情報内容を出力
	echo "####### $1 #########" >> $OUTPUT
	# コマンド結果を出力
	$2 >> $OUTPUT
	
	return 0
}

# ホストネーム
addinfo hostname "hostname"
# プロセス一覧
addinfo process-list "ps auxwwf"
# Listenポート一覧
addinfo listen-port "sudo netstat -lntp"
# ルートテーブル
addinfo route-table "sudo netstat -rn"
# rpmパッケージ
addinfo rpm-package "sudo rpm -qa | sort"
# 自動起動リスト
addinfo chkconfig "sudo chkconfig --list"
# OSバージョン
addinfo os-version "cat /etc/redhat-release"
# CPU
addinfo cpu-info "cat /proc/cpuinfo"
# メモリ
addinfo memory-info "free -m"
# ディスク
addinfo disk-info "df -hT"
# fstab
addinfo fstab "cat /etc/fstab"
# iptables
addinfo iptables "sudo service iptables status"
# ifconfig
addinfo ifconfig "ifconfig"

【redash】Dockerコンテナ内部からdumpデータを取得【シェルスクリプト】

そういえば redash のバックアップを取得していなかったなと気づきまして。
どうやらコンテナ内部の PostgreSQL の dumpデータを取得するればいいようです。

↓こんな感じのシェルスクリプトをcronで定期実行すればいいですね!

#!/bin/sh

# バックアップディレクトリを定義
BACKUP_DIR=/var/backup
# バックアップファイルを保持する期間を定義
TIME=3

# DBのコンテナIDを取得
CID=$(docker container ls | grep postgres | awk '{print $1}')

# dumpを取得
docker container exec ${CID} /bin/bash -c 'pg_dump -U postgres postgres | gzip > /usr/local/redash-backup.gz'

# ホストにdumpを転送
docker container cp ${CID}:/usr/local/redash-backup.gz ${BACKUP_DIR}/redash-backup_$(date +"%Y%m%d").gz

# dumpのローテート設定
find ${BACKUP_DIR}/ -daystart -name '*.gz' -mtime +${TIME} -delete

↓redashの構築方法はこちら

【Docker】redashの構築【CentOS7】