COLUMN
AWSのCodeシリーズの運用で躓いたお話
こんにちは、松田です。 |
CI/CDパイプラインのオーケストレーションサービスであるAWS CodePipelineのDeployフェーズで発生する、AWS CloudFormationのスロットリングエラーについて、原因の特定から解決までを
- IaC(Infrastructure as Codeの略)で管理されたAWSリソースをCodeシリーズでデプロイしている方
- AWS Codeシリーズを今後利用しようと思っている方
向けにご紹介します。
1. 課題
LambdaとPythonでAPIの開発を行う中でCI/CDとしてAWSのCodeシリーズを利用してデプロイを行っており、CI/CDが完成した当初は、スムーズなデプロイに非常に感動して、開発速度もかなり向上した。
がしかし、リポジトリサイズが増えるにあたって、何故かDeployフェーズでロールバックされ、正常にデプロイできない事象が発生するようになった。
この事象は、何度かDeployフェーズをやり直すと成功することもあって、それが所以で原因特定ができなかった。
本番環境へのデプロイ時にも発生することから、DevOpsの観点で早急な改善がチーム内の課題となった。
NTT東日本では、AWSやクラウドに関する情報をメールマガジンにて発信しています。ご購読を希望される方は、ぜひこちらからご登録ください。
2. 先に結論
2-1. 原因
CloudFormationで管理されたリソースを同時にデプロイしようとしたことによるAPIのスロットリングエラー。
2-2. 選択した解決策
各リソースにDependsOn(依存)属性を持たせて、一定の関数毎に順にデプロイさせることにした。
3. 結論に至るまでに検証したこと
- ChangeSetエラーでロールバックが発生していないか確認
- リポジトリサイズを小さくしてロールバックが発生するか確認
3-1. ChangeSetエラーでロールバックが発生していないか確認
3-1-1. 仮説立ての理由
IaC管理されているリソースの構成を外部から変更した場合、IaCに記載されている構成との差分が発生するため、その差分を検知すると自動でロールバックする仕組みを入れていた。(ChangeSet)
デプロイ後のデバック作業で我慢しきれず、手動で環境変数とかを変更してしまい、デプロイエラーになるケースは過去にも私自身がやらかしていてたため、まずこれの確認を行った。
3-1-2. 方法
「IaCと実リソースの差分がないか1つずつ目視で確認」した。(とても地道で辛い)
3-1-3. 結果
特に差分もなく、ChangeSetによるロールバックの可能性は低いと判断した。
3-2. リポジトリサイズを小さくしてロールバックが発生するか確認
3-2-1. 仮説立ての理由
開発の初期は1の事象以外でDeployフェーズでロールバックが発生することはなかったため、コード量に起因することを想定した。
3-2-2. 方法
デプロイ対象のリソースを最小1から段階的に増加させていき、Deployエラーが発生するリソース数を確認する
3-2-3. 結果
大体20リソース前後で本事象の発生を確認できた。
3-3. 検証のまとめ
2のIaC(CloudFormation)で管理しているリソース数に起因している可能性が高いという仮説が有力になった。
4. 詳細調査
4-1. ググる
CloudFormation起因であることは明白になったので、「CloudFormation 上限」で調べて出てきた以下のサイトでサービスクォートを確認。
Understand CloudFormation quotas
正直さっぱり分からなかったので、今回のリポジトリで管理しているリソース数(50前後)に近い数字がないか確認したが、その観点でのクォートエラーはなさそうと当時理解した。
4-2. 更にググる
CodePipeLineとCloudFormationの間に何かしらの関係性があることを想定し、以下の答えに近づくドキュメントに出会う
How do I prevent "Rate exceeded" errors in CloudFormation?
AWS公式曰く、
リソース間の依存関係が定義されていない限り、CloudFormation ではリソースの作成や更新が複数同時に行われます
引用:How do I prevent "Rate exceeded" errors in CloudFormation?
とのことなので、今回のCloudFormationで管理されたリソースはすべて同時にデプロイが行われて、それによるスロットリングエラーが発生していたことが判明した。
当時のtemplete.ymlの再現
#関数毎に独立して定義がなされていた。
####################################FunctionA#########################################
# FunctionA-1
#---------------------------------------------------------------------------
FunctionA_1:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub '${Env}-FunctionA-1'
CodeUri: ../../functions/FunctionA/1
Handler: FunctionA_1.lambda_handler
Environment:
Variables:
Region: !Ref Region
Role:
Fn::GetAtt:
- xxxxxxxxxxxxxxxxxx
# FunctionA-2
#---------------------------------------------------------------------------
FunctionA_2:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub '${Env}-FunctionA-2'
CodeUri: ../../functions/FunctionA/2
Handler: FunctionA_2.lambda_handler
Environment:
Variables:
Region: !Ref Region
Role:
Fn::GetAtt:
- xxxxxxxxxxxxxxxxxx
5. 検証・調査を踏まえて改善したこと
解決の選択肢としてはDependsOn属性を利用し、リソース間に依存関係(デプロイの前後関係)を持たせることで、一度にリソースがデプロイされないに考慮した。
ただし全リソースを直列に依存関係を設けてしまうと関数がデプロイ時間が無限に伸びていくため、リソースをグループに分け、グループ毎に並列にデプロイされるようにする工夫が重要!!!
####################################FunctionA#########################################
# FunctionA-1
#---------------------------------------------------------------------------
FunctionA_1:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub '${Env}-FunctionA-1'
CodeUri: ../../functions/FunctionA/1
Handler: FunctionA_1.lambda_handler
Environment:
Variables:
Region: !Ref Region
Role:
Fn::GetAtt:
- xxxxxxxxxxxxxxxxxx
# FunctionA-2
#---------------------------------------------------------------------------
FunctionA_2:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub '${Env}-FunctionA-2'
CodeUri: ../../functions/FunctionA/2
Handler: FunctionA_2.lambda_handler
Environment:
Variables:
Region: !Ref Region
Role:
Fn::GetAtt:
- xxxxxxxxxxxxxxxxxx
DependsOn: FunctionA_1 #依存関係を設定 FunctionA_1がデプロイされた後にFunctionA_2がデプロイされる
# FunctionBというリソースグループとFunctionAはグループ単位では並列にデプロイされる
####################################FunctionB#########################################
# FunctionB-1
#---------------------------------------------------------------------------
FunctionB_1:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub '${Env}-FunctionB-1'
CodeUri: ../../functions/FunctionB/1
Handler: FunctionB_1.lambda_handler
Environment:
Variables:
Region: !Ref Region
Role:
Fn::GetAtt:
- xxxxxxxxxxxxxxxxxx
# FunctionB-2
#---------------------------------------------------------------------------
FunctionB_2:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub '${Env}-FunctionB-2'
CodeUri: ../../functions/FunctionB/2
Handler: FunctionB_2.lambda_handler
Environment:
Variables:
Region: !Ref Region
Role:
Fn::GetAtt:
- xxxxxxxxxxxxxxxxxx
DependsOn: FunctionB_1
5-1. 改善の結果
実はこの事象はすごい昔に起こったことですが、この改善をしてから今日まで同様の事象は一度も発生していません。
NTT東日本では、AWSやクラウドに関する情報をメールマガジンにて発信しています。ご購読を希望される方は、ぜひこちらからご登録ください。
6. 得た学び(まとめ)
- CloudFormationを利用したCI/CDで、不規則にDeployが失敗する場合はAPIスロットリングエラー(一度にデプロイしようとしすぎ)を疑う。
- リポジトリ分割が難しい場合は、DependsOn属性を利用して、デプロイに順序性を持たせる。
- 公式ドキュメントは隅々まで読むべし。
参考記事
RECOMMEND
その他のコラム
相談無料!プロが中立的にアドバイスいたします
クラウド・AWS・Azureでお困りの方はお気軽にご相談ください。