【AWSリソース操作自動化】制限緩和申請を行う

#はじめに

皆さん、業務でAWSは使われているでしょうか。
AWS上でお客様ごとに環境を管理していたりすると、デフォルトの制限ではリソースが足りないということがよくあると思います。
そういう場合、今まではサポートに直接文章で連絡していたと思いますが、AWS Service Quotasを使用することでCLIで制限緩和を行えるようになりました。（2019/07）
https://aws.amazon.com/jp/blogs/news/aws-service-quotas/
自動化するうえでの注意点として、試しにcliでなげてみようとするとAWS運営側に本当に申請がいってしまうため、テストする場合は無駄にコマンドを実行しないようにしましょう。

#本題
そこで、今回は事前に1環境当たりの必要リソース数とリージョン当たりの必要環境数が明らかになっている前提の元、まとめてServiceQuotasで制限緩和を提出するスクリプトを書いてみました。
このスクリプトでは、現在のquotaを確認し、必要環境数に対して不足しているかどうかを判定、不足していれば必要数まで拡張を申請します。
ついでに、再実行しても問題ないように申請ステータスも確認し、処理されていないものがあればそのリソースについては申請を行いません。

#実装
リソース事に1環境当たり必要な数を記載したaws-resource.ymlを用意しておく

```yaml
quota_info:
  AutoScalingGroup:
    adjustable: true
    count: 4
    default: 200.0
    global: false
    need_increase: true
    quota_code: L-CD******
    quota_name: Auto Scaling groups per region
    service_code: autoscaling
  EIP:...
```

以下のジョブで制限緩和を実行

```groovy
pipeline {
    options {
        buildDiscarder(logRotator(daysToKeepStr: '30', numToKeepStr: '30'))
        disableConcurrentBuilds()
    }

    parameters {
        string(name: 'NODE_LABEL', defaultValue: "sre_slave", description: '')
        string(name: 'AWS_ACCESS_KEY', description: 'Specify the access key to be used.')
        password(name: 'AWS_SECRET_ACCESS_KEY', description: 'Specify the AWS Secret Access Key to be used.')
        booleanParam(name: 'TEST_FLAG', defaultValue: true, description: 'If set to true, no Request will be submitted.')
    }

    environment {
        AWS_ACCESS_KEY_ID      = "${params.AWS_ACCESS_KEY}"
        AWS_SECRET_ACCESS_KEY  = "${params.AWS_SECRET_ACCESS_KEY}"
        AWS_DEFAULT_REGION     = "ap-northeast-1"
        AWS_DEFAULT_OUTPUT     = "json"
    }

    agent {
        docker {
            label "${NODE_LABEL}"
            image 'amazon/aws-cli'//AWSの公式cliイメージ
            args "-t --entrypoint='' --init --cap-add=SYS_ADMIN -e LANG=C.UTF-8 -e AWS_ACCESS_KEY_ID=\$AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY=\$AWS_SECRET_ACCESS_KEY -e AWS_DEFAULT_REGION='ap-northeast-1' -e AWS_DEFAULT_OUTPUT='json'"
        }
    }

    stages {
        stage('Request limit increase') {
            steps {
                script{
                    def regions = ["ap-northeast-1":110,"ap-southeast-1":15]//region_id:environment_number
                    def environment_number = 0//想定環境数
                    def current_quota      = 0//現在の制限数
                    def need_quota = 0        //必要な制限数
                    def margin     = 0        //current_quota - need_quota
                    def count_all  = 0        //複数countがあった場合の合計値
                    def region_id             //作業対象リージョン
                    def resource_name         //aws_resources.yml quota_infoのどの要素か
                    def info                  //aws_resources.yml quota_infoの要素が持つ属性の集合
                    def execute_flg           //trueなら制限緩和申請を行う
                    def current_status        //現在の制限情報の集合
                    def requested_quotas      //提出された制限緩和申請の履歴の集合
                    def update_targets_region = [:] //TEST_FLAGがtrueの場合、ここに実行コマンドを格納 resource_name:command
                    def update_targets = []  //update_targets_regionの集合
                    def resources_yaml = readYaml(file: "./aws_resources.yml")
                    def quota_infos    = resources_yaml

                    //環境が立つリージョンそれぞれに実行
                    for (region in regions.entrySet()){
                        //aws-resourceからまずservice quotaで管理されているリソースの制限緩和申請を行う
                        for (quota_info in quota_infos.quota_info.entrySet()){
                            execute_flg   = true
                            resource_name = quota_info.getKey()
                            info          = quota_info.getValue()
                            print("${resource_name}:${info.quota_name}-${info.quota_code} need_increase: ${info.need_increase}")
                            //need_increase（環境の増加によって拡張が必要）がtrueの場合のみ制限緩和対象となる
                            if (info.need_increase){
                                //グローバルサービスはリージョンが固定
                                if (info.global){
                                    print("${resource_name}:${info.service_code} is grobal service")
                                    region_id          = "us-east-1"
                                    environment_number = 100
                                } else {
                                    region_id          = region.getKey()
                                    environment_number = region.getValue()
                                }

                                current_quota = 0
                                need_quota    = 0
                                margin        = 0
                                //まず対象リソースの現状を取得
                                //get-service-quotaできないものがあるので、その場合はDefaultの値を使用する。
                                def check_result = sh(
                                    script:"export AWS_DEFAULT_REGION='${region_id}' && aws service-quotas get-service-quota --service-code ${info.service_code} --quota-code ${info.quota_code}",
                                    returnStatus: true
                                    )
                                if (check_result){//exit 0以外はtrue
                                    print("can't get current quota. so use default value.")
                                    current_quota = info.default
                                } else {
                                    //ステータスをとるか標準出力を取るかしかできないので再実行
                                    def current_status_json = sh(
                                        script:"export AWS_DEFAULT_REGION='${region_id}' && aws service-quotas get-service-quota --service-code ${info.service_code} --quota-code ${info.quota_code}",
                                        returnStdout: true
                                    )
                                    current_status = readJSON text: current_status_json
                                    current_quota  = current_status["Quota"]["Value"]
                                }

                                //必要なquotaを計算
                                need_quota = environment_number * info.count
                                margin = current_quota - need_quota
                                print("current:${current_quota} - need:${need_quota} = ${margin}")

                                //現在の値が必要なリソース数未満の場合は更新リクエストを投げる
                                if (margin >= 0){
                                    print("There is enough margin ${resource_name}:${info.service_code}")
                                    execute_flg = false
                                } else {
                                    print("need update ${resource_name}:${info.quota_name} to ${need_quota} ${region_id}")
                                    //リクエスト一覧を参照し、CASE_OPENDかになっているものがあれば更新対象からはずす
                                    //リクエストより低い値で適用されると再実行時に排除できない。
                                     def requested_quotas_json = sh (
                                        script: "export AWS_DEFAULT_REGION='${region_id}' && aws service-quotas list-requested-service-quota-change-history --service-code ${info.service_code}",
                                        returnStdout: true,
                                    )
                                    requested_quotas = readJSON text: requested_quotas_json
                                    if (requested_quotas["RequestedQuotas"].size() > 0 ){
                                        for (requested_quota in requested_quotas["RequestedQuotas"]){
                                            if (requested_quota["QuotaCode"] == "${info.quota_code}"){
                                                if (requested_quota["Status"] == "CASE_OPENED" | requested_quota["Status"] == "PENDING"){
                                                    print("The request cannot be made because there are ${requested_quota["Status"]} requests. id:${requested_quota["ServiceName"]} CaseId:${requested_quota["CaseId"]}")
                                                    execute_flg = false
                                                    break
                                                }
                                            }
                                        }
                                    }
                                }
                            } else {
                                print("No update required ${resource_name}:${info.service_code}")
                                execute_flg = false
                            }

                            //更新flgがtrueなら更新
                            if (execute_flg){
                                if (params.TEST_FLAG){//TEST_FLAGがtrueなら更新せず配列に結果を入れていく
                                    update_targets_region.put("${resource_name}", "aws service-quotas request-service-quota-increase --service-code '${info.service_code}' --quota-code '${info.quota_code}' --desired-value ${need_quota}")
                                } else {
                                    sh "export AWS_DEFAULT_REGION='${region_id}' && aws service-quotas request-service-quota-increase --service-code '${info.service_code}' --quota-code '${info.quota_code}' --desired-value ${need_quota}"
                                    print("Requested ${resource_name}:${info.quota_name} to ${need_quota} ${region_id}")
                                }
                            }
                        }
                        if (params.TEST_FLAG){
                            sh "echo '{ \'${region_id}\' : \'${update_targets_region}\' }' >${region_id}_quota.json"
                            archiveArtifacts "${region_id}_quota.json"
                        }
                    }
                }
            }
        }
    }

    post {
        always {
            cleanWs()
        }
    }
}

```

#おわりに
今回も、すでにあるかなと思ったらなかったので書いてみたシリーズです。
正直、私はコーディングは体系的に学んできたわけでもなく、OJTのみでやってきたので、あまり格好いいコードはかけていないと思います。（ぶっちゃけ晒したくない。怖い）
なので、そういった観点からもコメントいただければありがたく思います！

以上、お疲れ様でした。