第二篇最主要是敘述一下踩到的雷或是開發上卡住的部分,軟體開發只要有親手去做,基本上踩到雷都是正常的,只是每個人或許踩到的雷都不相同,這篇就這次實作中踩到的雷,做個說明,有些雷踩完後也覺得又學到了新的東西,也是一個不錯的經驗
項目
- 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 的連結供參數,非常方便
而會踩到這個問題,是因為沒有看清楚說明,在使用的時候是如下程式碼
iam.ManagedPolicy.fromAwsManagedPolicyName(
'service-role/AWSCodeDeployRole'
)
很單純的呼叫了 fromAwsManagedPolicyName 這個 function,而參數是要帶 AWS IAM 官方已經提供的 policy,那我們再來看看 source code 中的 comment
/**
* 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 EC2 和 Auto 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 上使用
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 初始化就執行指令如下程式碼
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
先來看一張示意圖

這次實作的順序是先等 SSM (Install PHP) 安裝完畢後,再安裝 SSM (Install CodeDeploy Agent),右邊的圖示,是實作時一開始的錯誤,以為 WaitCondtion 是個計數器,不是 Resource,所以把第二個 SSM 也 DependsOn 在 第一個 SSM,想當然,這樣不會有效果,還是發生了衝突
再來看一下 CDK 程式碼的部分
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,如下程式碼
this._cfnSSMInstallCodeDeployAgent.addDependsOn(this._cfnWaitCondition);
到此設定結束後,WaitCondition 應該就能運作正常
這算是實作中額外遇到的問題,希望這樣的解釋對讀者能有所幫助