使用 CDK 建置 AWS CodeDeploy 應用於 AutoScaling (二) – WaitCondition 和 CreationPolicy 使用方式

POSTED BY   Chris
2020 年 12 月 8 日
使用 CDK 建置 AWS CodeDeploy 應用於 AutoScaling (二) – WaitCondition 和 CreationPolicy 使用方式

第二篇最主要是敘述一下踩到的雷或是開發上卡住的部分,軟體開發只要有親手去做,基本上踩到雷都是正常的,只是每個人或許踩到的雷都不相同,這篇就這次實作中踩到的雷,做個說明,有些雷踩完後也覺得又學到了新的東西,也是一個不錯的經驗

 

項目

  • CDK 開發時,該選擇包成 construct 或 stack ?
  • CDK AWS IAM policy 的 service-role prefix 問題
  • 經由二個以上的 AWS SSM 去 ubuntu 安裝 package 時問題

 

1. CDK 開發時,該選擇包成 construct 或 stack ?

這個問題在用 CDK 開發時,困擾了我一陣子,在經過一陣摸索後,最後是決定把建立 IAM 的部分拆成一個 Construct,其他 CodeDeploy 和 Auto Scaling 的部分拆成另一個 Construct,但這樣或許還不夠細,拆成 Construct 有以下幾點好處

  • construct 是 CDK 最基礎的元件,拆出來後,可以方便不同 stack 間的共用
  • construct 可以像 stack constructor 的第 3 個參數 StackProps 一樣,自己設計不同的參數來建立 Resources
  • 把不同 construct 集合在相同的 stack 中,好處是一但失敗,CloudFormation Rollback 時,也會一併刪除

當然或許更往上一層來看,一個 CDK App 跑多個 stack 也合情合理,如果兩個以上的 Stack 彼此之間相依不大甚至沒有相依,是可以考慮包成 stack,因為有相依的話,我們總是希望運用 CloudFormaton 的特性,若失敗就能全部 Rollback,省去手動再去操作 AWS 後台

所以開發時選擇包成 construct 來看,更可以靈活組合運用,因為 construct 是 CDK 最基底的元件,供讀者參考

 

2. CDK AWS IAM policy 的 service-role prefix 問題

在寫 CDK 時,引用方法可以盡量去看 CDK 的 source code,因為裡面都會提到該怎麼使用,要帶入什麼,若是 cfn 的 L1 Construct,參數大都都會提供 CloudFormation 的連結供參數,非常方便

而會踩到這個問題,是因為沒有看清楚說明,在使用的時候是如下程式碼

TypeScript
iam.ManagedPolicy.fromAwsManagedPolicyName(
  'service-role/AWSCodeDeployRole'
)

 

很單純的呼叫了 fromAwsManagedPolicyName 這個 function,而參數是要帶 AWS IAM 官方已經提供的 policy,那我們再來看看 source code 中的 comment

TypeScript
 /**
 * Import a managed policy from one of the policies that AWS manages.
 *
 * For this managed policy, you only need to know the name to be able to use it.
 *
 * Some managed policy names start with "service-role/", some start with
 * "job-function/", and some don't start with anything. Do include the
 * prefix when constructing this object.
 */
static fromAwsManagedPolicyName(managedPolicyName: string): IManagedPolicy;

而當初沒好好看,以為都需要有 service-role/ 這個 prefix,但其實沒有,舉個例子,AmazonSSMManagedInstanceCore 這個可以使用 SSM 功能的 policy 就沒有這個 prefix,所以在設定 policy name 時就要特別注意一下

[2021/05/06 補充] – 直接在 AWS Management Console 中的 IAM Policies 中,觀看該 Policy 的 arn 最準,以 job-function 來說,並非如文件中介紹的每個 job-function policy 都需要這個 prefix,例如 DatabaseAdministrator 的 arn 為

arn:aws:iam::aws:policy/job-function/DatabaseAdministrator

而 PowerUserAccess 為

arn:aws:iam::aws:policy/PowerUserAccess

在寫 CDK 的時候還滿困擾的,因為 prefix 沒指定正確,Cfn 就直接噴錯了,所以看 policy arn 是最準的

 

3. 經由二個以上的 AWS SSM 去 ubuntu 安裝 package 時問題

這個其實在上一篇文章已經有提到,這邊再做個詳細說明,因為這部分牽扯到使用 CloudFormaion 中的 WaitCondition,前置作業和機制上相對比較複雜

問題的原因是用了兩個 AWS SSM 去安裝 CodeDeploy Agent 和 php,導致 ubuntu apt 在運作時會因為有另一個 process 在運作導致出錯,錯誤訊息如下


E: Could not get lock /var/lib/dpkg/lock-frontend – open (11: Resource temporarily unavailable) install errors: E: Could not get lock /var/lib/apt/lists/lock – open (11: Resource temporarily unavailable)


故必需等其中一方安裝完畢後,才能再跑另一個,避免這個問題

 

使用 WaitCondition 和 CreationPolicy

這兩個都是在做同一件事情,也就是等待接收到 信號 (signal) 時,再繼續執行,那兩者的不同在於應用的 Resources,在 WaitCondition 最上方已經很明顯的指出,若是使用 Amazon EC2Auto Scaling resources 則推薦使用 CreationPolicy

這次實作中為了解決兩個 SSM 執行時間衝突的問題,所以需要用 WaitCondition,但要用 WaitCondition 則需要先安裝 aws-cfn-bootstrap package,就變成在 Auto Scaling Group 啟動 EC2 時,就需要安裝此套件

而另外一方面,為了確保每台 EC2 都有安裝到此套件,所以使用 CreationPolicy 來確保每台 EC2 都正確安裝完 aws-cfn-bootstrap package 後,再繼續往下執行,避免部署失敗 (呼…,看來頗複雜)

 

建立 CreationPolicy

就來看看在 CDK 上是如何使用 CreationPolicy,這邊是在 Auto Scaling Group 上使用

TypeScript
protected createCfnAsg(
    launchConfigurationName: string | undefined,
    vpc: ec2.Vpc
  ): autoscaling.CfnAutoScalingGroup {
    const cfnAsg = new autoscaling.CfnAutoScalingGroup(this, 'ASG', {
      autoScalingGroupName: 'ASG-with-codedeploy',
      minSize: '6',
      maxSize: '8',
      launchConfigurationName,
      availabilityZones: cdk.Fn.getAzs(this._region),
      vpcZoneIdentifier: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PUBLIC,
      }).subnetIds,
      tags: [{ key: 'Name', value: 'CodeDeployDemo', propagateAtLaunch: true }],
    });

    cfnAsg.cfnOptions.creationPolicy = {
      resourceSignal: {
        count: 6,
        timeout: 'PT5M',
      },
    };

    return cfnAsg;
  }
  • count: 需要等到收到幾個 signal 後,才會繼續往下執行,這次 Auto Scaling Group 的 minSize 是設定 6,所以這邊也設定 6
  • timeout: PT5M 是說定 5 分鐘,可參考 aws-attribute-creationpolicy 裡面的說明

 

在 Auto Scaling Group 設定完 CreationPolicy 後,再來就是怎麼在每台 EC2 上面安裝套件來發送 signal,使用 UserData 在每台 EC2 初始化就執行指令如下程式碼

TypeScript
protected attachUserDataForCfnLaunchConfiguration(
    asgResourceId: string
  ): void {
    this._cfnLaunchConfiguration.userData = cdk.Fn.base64(
      cdk.Fn.join('\n', [
        '#!/bin/bash -xe',
        'apt-get update -y',
        'apt-get install -y python-setuptools',
        'mkdir -p /opt/aws/bin',
        'wget https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz',
        'python -m easy_install --script-dir /opt/aws/bin aws-cfn-bootstrap-latest.tar.gz',
        cdk.Fn.sub(
          '/opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource ' +
            asgResourceId +
            ' --region ${AWS::Region}'
        ),
      ])
    );
  }

由於 Auto Scaling Group 是用 LaunchConfiguration 去設定啟動,故 UserData 加在這邊

安裝 aws-cfn-bootstrap package 如下

wget https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz
python -m easy_install --script-dir /opt/aws/bin aws-cfn-bootstrap-latest.tar.gz

而發送 signal 的指令就在最下方,用 fn.sub 來串接執行

cdk.Fn.sub(
'/opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource ' + asgResourceId + ' --region ${AWS::Region}'
)

會用 fn.sub 的原因是參考 Fn::Sub 的 UserData 命令 ,可以用變數來代入指令中

最後每一台 EC2 都會執行指令來發送信號,待 CreationPolicy 收到 6 個 signal 後,就會再繼續往下執行,若沒有收到 6 個,等待 5 分鐘 timeout 後,部署就會失敗,CloudFormation 會整個 rollback

 

建立 WaitCondition

先來看一張示意圖

Hint: 點圖片可放大

這次實作的順序是先等 SSM (Install PHP) 安裝完畢後,再安裝 SSM (Install CodeDeploy Agent),右邊的圖示,是實作時一開始的錯誤,以為 WaitCondtion 是個計數器,不是 Resource,所以把第二個 SSM 也 DependsOn 在 第一個 SSM,想當然,這樣不會有效果,還是發生了衝突

再來看一下 CDK 程式碼的部分

TypeScript
protected createCfnSSMInstallPHPAndWaitCondition(): ssm.CfnAssociation {
    const waitConditionHandler = new cdk.CfnWaitConditionHandle(
      this,
      'waitConditionHandler'
    );

    this._cfnWaitCondition = new cdk.CfnWaitCondition(this, 'waitCondition', {
      handle: waitConditionHandler.ref,
      timeout: '1200',
    });

    const cfnSSMInstallPHP = new ssm.CfnAssociation(this, 'ASG-SSM-PHP', {
      name: 'AWS-RunShellScript',
      targets: [{ key: 'tag:Name', values: ['CodeDeployDemo'] }],
      parameters: {
        commands: [
          'apt -y install dialog apt-utils',
          'apt -y install software-properties-common',
          'add-apt-repository -y ppa:ondrej/php',
          'apt-get update',
          'apt -y install php7.4',
          cdk.Fn.join('', [
            '/opt/aws/bin/cfn-signal -e $? -d "Install php7.4 completed" -r "Install php7.4 completed" ',
            waitConditionHandler.ref,
            '"',
          ]),
        ],
      },
    });

    this._cfnWaitCondition.addDependsOn(cfnSSMInstallPHP);

    return cfnSSMInstallPHP;
  }

WaitCondition 參數說明

  • handle: WaitCondition 需要一個 WaitConditionHandle 來接受 signal 用
  • timeout: 等待多久後 timeout,單位為秒

注意下方這行,也就是 WaitCondition 要作用在哪一個 Resource 上,作用在 SSM (Install PHP),跟示意圖所畫的一樣

this._cfnWaitCondition.addDependsOn(cfnSSMInstallPHP);

而 WaitCondition 的原理是在作用的 Resource 建立時,計數器就開始計算,故以此例子,在 SSM (Install PHP) 中必需在 timeout 前把 signal 送出,才能繼續執行下去,發送 signal 的指令如下

cdk.Fn.join('', [
  '/opt/aws/bin/cfn-signal -e $? -d "Install php7.4 completed" -r "Install php7.4 completed" ',
  waitConditionHandler.ref,
  '"',
]),

 

最後的關鍵,也就是圖示中所畫的第二個 SSM (Install CodeDeploy Agent) 必需 DependsOn WaitCondition,如下程式碼

TypeScript
this._cfnSSMInstallCodeDeployAgent.addDependsOn(this._cfnWaitCondition);

到此設定結束後,WaitCondition 應該就能運作正常

這算是實作中額外遇到的問題,希望這樣的解釋對讀者能有所幫助

歡迎留言
0

您可能也想看

Workaround for AWS Grafana alerting
2023 年 8 月 3 日
AWS, DevOps
AWS VPC Endpoint 使用場景
2022 年 3 月 14 日
AWS, CDK, Network
CDK 指定 Physical names 運作方式
2021 年 12 月 4 日
AWS, CDK, Cloudformation