ロードバランサの流量調整を動的にやってみた！

# 初めに
こんにちは!
[CYBIRD Advent Calendar 2023](https://qiita.com/advent-calendar/2023/cybird)の20日目を担当します、インフラエンジニアの@ice_matcha3です。
19日目は@gongon282828さんの「外部ネットワークとAWSのVPCをSite-to-Site VPNとトランジェントゲートウェイで疎通する」でした。
# 実装したいこと
AWSのサービスを利用している下図のような構成において、動的にx,yの流量値を遷移させたい！
なお、Target Groupは以下「tg」、Auto Scailing Groupは以下「asg」と省略して表記します。

![flow_rate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298380/383672f9-a58d-2020-8fce-a22e63551b0a.png)


## 想定する台数
高負荷時前にオートスケールの台数を事前に4台にするアクションを設定し、それ以外のタイミングは1台で動いている状況を想定して作成していこうと思います。

### この想定で固定の流量調整だと何が問題なのか？
- 流量 x=30% y=70%の場合
  - asgサーバが1台の際
Instanec01には30%、Instance02及びasgサーバには35%の流量となり、5%程度の流量の差なので許容内です。
  - asgサーバが4台の際
Instanec01には30%、Instance02及びasgサーバには14%の流量となり、16%程度の流量の差になってしまうため、tg1側に負荷が掛りすぎています。
- 流量  x=20% y=80%の場合
  - asgサーバが1台の際
Instanec01には20%、Instance02及びasgサーバには40%の流量となり、20%程度の流量の差になってしまうため、tg2側に負荷が掛りすぎています。
  - asgサーバが4台の際 
 Instanec01には20%,Instance02及びasgサーバには16%の流量となり、4%程度の流量の差なので許容内です。

このように、台数によってtgに掛かる負荷の差があるため固定の流量だと少し問題があります(´・ω・`) ｼｮﾎﾞｰﾝ


# 実装フロー
下図のような流れで作成していきます(　´∀｀)bｸﾞｯ!

　<img width="500" alt="flow.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298380/b5ad7670-d65d-fbc5-63b1-e466f8b2ac9e.png">

 ## cloudwatchアラームの発火
 まずasgサーバが今1台なのか1台より多いのかを判定するためにcloudwatchアラームを作成していきます。
 しきい値はtg2の中に正常値のホストが何台存在しているかで判断させようと思います。
 1台かそれ以上で判定するアラームのため、常時どちらかがアラーム状態になっているイメージです。
 ### 1台の場合に発火するアラームのパラメータ
 - 名前 ： ```less_than_two```
 - メトリクス名 ： ```HealthyHostCount```
 - TargetGroup : ```tg2```
 - LoadBalancer : ```流量調整したいalb```
 - しきい値の種類 ： ```静的```
 - アラーム条件 ： ```以下```
 - しきい値 ： ```２``` (tg2にはinstance02も含まれているためasgサーバ+1台の値を設定しています)
### 1台より多い場合に発火するアラームのパラメータ
 - 名前 ： ```more_than_two```
 - メトリクス名 ： ```HealthyHostCount```
 - TargetGroup : ```tg2```
 - LoadBalancer : ```流量調整したいalb```
 - しきい値の種類 ： ```静的```
 - アラーム条件 ： ```より大きい```
 - しきい値 ： ```２``` (tg2にはinstance02も含まれているためasgサーバ+1台の値を設定しています)

 ## eventbridgeで検知
 cloudwatchアラームが発火した際にアラームを検知するためのサービスとしてeventbridgeを利用します。今回は2種類のアラームがあるため、eventbridgeも2種類作成していきます。

 ＊eventbridge作成する際、「検知した後のターゲット」を選択しないといけないです。
  そのため構成上後ろに記載している「lambda関数の作成」を事前に実施しておく必要がありますので同環境を作成する際はご注意ください(*_ _)人ｺﾞﾒﾝﾅｻｲ
 ### 1台の場合に検知するルールの作成
 ルール名は```detect_less_than_two```としておきます。
 イベントパターンは以下のような設定で作成しています。
 ```
{
  "source": ["aws.cloudwatch"],
  "detail-type": ["CloudWatch Alarm State Change"],
  "detail": {
    "alarmName": ["less_than_2"],
    "state": {
      "value": ["ALARM"]
    }
  }
}
```
 ### 1台より多い場合に検知するルールの作成
 ルール名は```detect_more_than_two```としておきます。
 イベントパターンは同様に以下のような設定で作成しています。
 ```
{
  "source": ["aws.cloudwatch"],
  "detail-type": ["CloudWatch Alarm State Change"],
  "detail": {
    "alarmName": ["more_than_2"],
    "state": {
      "value": ["ALARM"]
    }
  }
}
```

## lambdaの実行
eventbrideで検知したアラームの種類によって流量を調整するlambdaを作成していこうと思います。
### lambda関数の作成
＊eventbridgeの作成より前にこの部分だけ先に作る必要があります。

まずはコードを実行するための関数を作成していきます。
関数名は流量調整ということで```flow_rate_adjustment```としておきます。
今回はlambdaの中で```autoscale```や```loadbalancer```といったawsサービスの値を取得、変更してきたいため、awsコマンドが利用できるpythonをランタイムに設定します。現時点で最新の```python3.11```を利用して関数を作成します。

### 実行ロールの設定
コード内で```autoscale```と```loadbalancer```の情報を操作したいので、ロールにポリシーを追加しておきましょう。

### コードの実装の前の確認
albの流量設定をしているルールがデフォルトアクションで行われているのか否かを確認しておきましょう。デフォルトアクションを修正する場合と指定したルールを修正する場合で、利用するawsコマンドが異なりますので注意です。
指定したルールを修正する場合は```modify_rule```、デフォルトアクションを修正する場合は```modify_listener```を利用します。
https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_ModifyRule.html

### コードの実装
最終的に実施したいことが流量の調整のためそれに必要なものを順に取得していき、最終的にアラームの種類で判定を実施した後、流量をセットするようなコードとなっています。
また、動くことを優先としたコードのため、pythonのベストプラクティスに則った記述でないことを事前に断っておきますm(･ω･m)ｿｰﾘｨ
```
import boto3

def lambda_handler(event, context):

    #変数宣言
    scale_in_alarm  = "less_than_two"
    scale_out_alarm = "more_than_two"
    lb_name         = "test-alb"
    asg_name        = "test-asg"
    
    #イベントデータ取得
    alarm_name = event['detail']['alarmName']
    
    #各種arnの取得
    lb_arn = get_lb_arn(lb_name)
    listener_asg_tg = get_asg_target_groups_arn(asg_name)
    listener_default_tg = get_defaule_target_groups_arn(lb_arn,listener_asg_tg)
    listener_arn = get_lb_listeners_arn(lb_arn)
    rule_arn = get_lb_rule_arn(listener_arn)
   
    
    if alarm_name == scale_in_alarm:
      print('流量 3:7')
      #デフォルトアクションを利用するためdefault_ruleを利用
      default_rule_set_flow_rate_scale_in(listener_arn,listener_asg_tg,listener_default_tg)
    elif alarm_name == scale_out_alarm:
      print('流量 2:8')  
      #デフォルトアクションを修正するためdefault_ruleを利用
      default_rule_set_flow_rate_scale_out(listener_arn,listener_asg_tg,listener_default_tg)
    else:
      print('異常値')

#lbのarn取得
def get_lb_arn(lb_name):
   client  = boto3.client('elbv2')
   lb_info = client.describe_load_balancers(
     Names = [lb_name]
   )
   return lb_info['LoadBalancers'][0]['LoadBalancerArn']


#asg tgの取得
def get_asg_target_groups_arn(asg_name):
  client = boto3.client('autoscaling')
  asg_info = client.describe_auto_scaling_groups(AutoScalingGroupNames=[asg_name])
  print(asg_info)
  targetgroup_arns = asg_info['AutoScalingGroups'][0]['TargetGroupARNs'][0]
  return targetgroup_arns
    
    
#リスナーarnの取得  
def get_lb_listeners_arn(lb_arn):
    client  = boto3.client('elbv2')
    lb_arn_info = client.describe_listeners(
      LoadBalancerArn = lb_arn
    )
    for i, value in enumerate(lb_arn_info['Listeners']):
      if value['Port'] == 443:
        check_port = i
    return lb_arn_info['Listeners'][check_port]['ListenerArn']
    
    
#ルールarnの取得
def get_lb_rule_arn(listener_arn):
    client  = boto3.client('elbv2')
    rule_arn_info = client.describe_rules(
      ListenerArn = listener_arn
    )
    # 優先度1のものを取得他ルールの流量を変えたい場合はここの処理を変える必要あり 
    return rule_arn_info['Rules'][0]['RuleArn']
    
#指定したルールの変更
def rule_set_flow_rate_scale_out(rule_arn,asg_tg,listener_default_tg):
    client  = boto3.client('elbv2')
    client.modify_rule(
        RuleArn = rule_arn,
        Actions = [
            {
              'Type': 'forward',
              'ForwardConfig':{
                'TargetGroups':[
                  {
                    'TargetGroupArn' : listener_default_tg,
                    'Weight'         : 2
                  },
                  {
                    'TargetGroupArn' : asg_tg,
                    'Weight'         : 8
                  }
                ]
              }
            }
         ]
    )

#デフォルトのルールの変更の場合はmodify_ruleではなくmodify_listenerを使う必要あり
def default_rule_set_flow_rate_scale_out(listener_arn,asg_tg,listener_default_tg):
    client  = boto3.client('elbv2')
    client.modify_listener(
        ListenerArn = listener_arn,
        DefaultActions = [
            {
              'Type': 'forward',
              'ForwardConfig':{
                'TargetGroups':[
                  {
                    'TargetGroupArn' : listener_default_tg,
                    'Weight'         : 2
                  },
                  {
                    'TargetGroupArn' : asg_tg,
                    'Weight'         : 8
                  }
                ]
              }
            }
         ]
    )


#指定したルールの変更
def rule_set_flow_rate_scale_in(rule_arn,asg_tg,listener_default_tg):
    client  = boto3.client('elbv2')
    client.modify_rule(
        RuleArn = rule_arn,
        Actions = [
            {
              'Type': 'forward',
              'ForwardConfig':{
                'TargetGroups':[
                  {
                    'TargetGroupArn' : listener_default_tg,
                    'Weight'         : 3
                  },
                  {
                    'TargetGroupArn' : asg_tg,
                    'Weight'         : 7
                  }
                ]
              }
            }
         ]
    )
    
#デフォルトのルールの変更の場合はmodify_ruleではなくmodify_listenerを使う必要あり
def default_rule_set_flow_rate_scale_in(listener_arn,asg_tg,listener_default_tg):
    client  = boto3.client('elbv2')
    client.modify_listener(
        ListenerArn = listener_arn,
        DefaultActions = [
            {
              'Type': 'forward',
              'ForwardConfig':{
                'TargetGroups':[
                  {
                    'TargetGroupArn' : listener_default_tg,
                    'Weight'         : 3
                  },
                  {
                    'TargetGroupArn' : asg_tg,
                    'Weight'         : 7
                  }
                ]
              }
            }
         ]
    )

```

## 流量調整完了
アラームの発火からlambdaの実行までの流れを経て、動的に流量を変更することができましたヽ(*´∀`)ﾉｵﾒﾃﾞﾄ─ｯ♪

# まとめ
本来は1台4台と台数が固定するように考えず、台数の変化に応じて動的に流量が変化するような仕組みを考えたかったのですが、その前段階ということでこのような記事を書いてみました。流量を動的に変化させる記事がなかったので、誰かの助けになればと思います！

# 最後に
[CYBIRD Advent Calendar 2023](https://qiita.com/advent-calendar/2023/cybird)、 明日は@dave_cさんのさんの「ボーイスカウトルールのご紹介」です。

# 参考
https://boto3.amazonaws.com/v1/documentation/api/latest/index.html