0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【CICD入門】AWSでシンプルなCICDを作る(第3回:運用編)

0
Posted at

Version・Alias・監視でリリースを安全にする(運用編)

🐈🐈🐈 作成した内容は👆こちら 🐈🐈🐈

1. はじめに

前回の記事では、Bootstrap StackとCI Stackを作り、pushするとLambdaが更新される状態まで作りました。権限を「作る役」と「指示する役」に分けたところが中心でした。

今回はその続きとして、リリースの仕組みと、失敗に気付くための監視を扱います。

CI/CDを作ると、まず「デプロイできた」がうれしい段階が来ます。その次に必要になるのは、次の2つです。

  • おかしくなったときに戻せる
  • おかしくなったことに気付ける

この2つをAWS標準機能でどう作るかを書いていきます。

今回扱うもの

項目 使う機能
リリースの単位を固定する LambdaのVersionとAlias
設定変更もリリースに含める SAMのAutoPublishAliasAllProperties
異常時に切り替えない CodeDeployとCloudWatch Alarmの連携
異常に気付く CloudWatch Alarm、SNS、EventBridge Rule
本番だけ人の判断を挟む CodePipelineの手動承認

2. VersionとAliasで呼び出し先を固定する

$LATESTを直接呼ぶと何が困るか

Lambdaは、コードを更新すると$LATESTが上書きされます。何も設定しなければ、呼び出し側は常に最新のコードを呼びます。

一見それで良さそうですが、2つ困ります。

困ること 内容
戻せない 前のコードが手元にしか残らない。戻すには再デプロイが必要
いつ切り替わったか分からない 更新した瞬間に切り替わる。切り替えのタイミングを制御できない

Versionとlive Alias

Lambdaには、そのときのコードと設定を固定して保存するVersionという仕組みがあります。Versionは番号(1、2、3…)で、あとから変更できません。

そして、Versionを指す名札としてAliasを付けられます。このテンプレートではliveという名前のAliasを使います。

呼び出し側(EventBridge Schedulerなど)
   │ 呼ぶ先は常に「関数名:live」
   ▼
live ──────────> Version 3   ← いまここを指している
                 Version 2
                 Version 1

デプロイすると Version 4 が発行され、live が Version 4 を指すように切り替わる

呼び出し側はAliasだけを見ます。 Versionの番号は知りません。これにより、切り替えは「Aliasの向き先を変える」という1つの操作に集約されます。

Templateではこう書きます。

  Function:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub '${AWS::StackName}'
      CodeUri: ../../../application/functions/daily_report/
      Handler: handler.lambda_handler
      Runtime: python3.12
      # デプロイのたびにVersionを発行し、live Aliasの向き先を切り替える
      AutoPublishAlias: live

この1行で、SAMがVersionとAlias、そして切り替えに使うCodeDeployのResourceまで作ります。自分でVersion番号を管理したり、Aliasを更新するスクリプトを書く必要はありません。

定期実行のScheduler側も、Aliasを指すようにします。

  Schedule:
    Type: AWS::Scheduler::Schedule
    Properties:
      Target:
        # 未修飾のARN($LATEST)ではなく、live Aliasを呼ぶ
        Arn: !Ref FunctionAliaslive
        RoleArn: !GetAtt SchedulerRole.Arn

FunctionAliasliveは、SAMが自動で作るAliasの論理IDです(<関数の論理ID> + Alias + <Alias名>)。

Schedulerに渡すRoleも、このAliasを呼ぶ権限だけにしています。

      Policies:
        - PolicyName: InvokeLiveAlias
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: lambda:InvokeFunction
                Resource: !Ref FunctionAliaslive

こうすると、$LATESTを直接呼ぶ経路が塞がれます。「呼び出しは必ずAlias経由」という状態を、権限の側からも担保できます。

3. 設定変更もリリースに含める

AutoPublishAliasだけでは、すべての変更がVersionになるわけではありません。

既定では、コードやメモリサイズなどの一部の変更だけがVersion発行の対象です。たとえばTimeoutを30秒から60秒へ変えても、新しいVersionは発行されず、liveは古い設定のままになります。

これは混乱します。「Templateを直してデプロイしたのに反映されていない」という状態になるためです。

そこで、1行足します。

      AutoPublishAlias: live
      # コード以外の設定変更もVersion発行の対象にする
      AutoPublishAliasAllProperties: true

これで、関数のプロパティが変われば新しいVersionが発行され、liveが切り替わります。**「Templateに書いた内容 = liveの内容」**という関係が保たれます。

一方で、変更が無ければ何も起きません。

変更したもの Versionが増えるか
Lambdaのコード 増える
Timeout、環境変数などの設定 増える
テストコードだけ 増えない(関数の中身が変わらないため)
ドキュメントだけ 増えない

このテンプレートは毎回すべてのStackを配備しますが、変更が無いStackは「変更なし」で成功します。無駄なリリースは起きません。

4. Alarmが鳴っている間は切り替えない

VersionとAliasで切り替えの単位が決まりました。次は「切り替えてよいかどうか」です。

SAMは、Aliasの切り替えをCodeDeployに任せる設定を持っています。

      DeploymentPreference:
        # 一括で切り替える方式(段階的に移す方式も選べる)
        Type: AllAtOnce
        Role: !GetAtt CodeDeployRole.Arn
        # このAlarmが鳴っている間は切り替えない
        Alarms:
          - !Ref ErrorsAlarm

Alarmsに指定したCloudWatch AlarmがALARMの状態だと、CodeDeployは切り替えを行わずにデプロイを失敗させます。異常が出ている最中に新しいコードを重ねないための仕掛けです。

大事なのは、Alarmを作るだけでは連携されないことです。Alarmは「監視する」だけの存在で、デプロイと結びつけるにはこのAlarmsの指定が必要です。

実際にこうなります。

Errors Alarmが ALARM の状態でDeployが始まる
   ↓
CodeDeployが切り替えを中止 → デプロイ失敗
   ↓
CloudFormationがStackをロールバック
   ↓
live は前のVersionを指したまま(新しいVersionは発行済みだが誰も呼ばない)

呼び出し先が勝手に新しいコードへ移ることはありません。原因を直してから再実行します。

なお、Duration(実行時間)のAlarmはこの判定に含めていません。一時的に遅くなっただけでリリースが止まると運用しにくいためです。「止める基準」と「気付く基準」は分けて考えるのがよさそうです。

5. 気付ける状態を作る

監視の設計で決めることは、「何を見るか」と「どこへ知らせるか」の2つです。

何を見るか

このテンプレートが標準で作るAlarmは3種類です。

Alarm 何を見ているか いつ鳴るか
<Stack名>-errors live Alias経由の呼び出しのエラー数 60秒間に1件以上失敗した
<Stack名>-duration 実行時間の最大値 Timeout(30秒)の8割に達した
<Stack名>-dlq 処理できなかったメッセージの数 キュー処理で退避が発生した

Errors Alarmは、Aliasを経由した呼び出しだけを見るようにしています。

  ErrorsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      Namespace: AWS/Lambda
      MetricName: Errors
      Dimensions:
        - Name: FunctionName
          Value: !Ref Function
        - Name: Resource
          # 「関数名:live」を指定すると、Alias経由の呼び出しだけが対象になる
          Value: !Sub '${Function}:live'
      Statistic: Sum
      Period: 60
      Threshold: 1
      ComparisonOperator: GreaterThanOrEqualToThreshold
      # 実行がない期間を異常として扱わない
      TreatMissingData: notBreaching

最後のTreatMissingData: notBreachingは、定期実行のLambdaでは重要です。1日1回しか動かない関数では、ほとんどの時間帯にデータがありません。既定の扱いのままだと「データがない」ことで状態が揺れます。

ただし裏返すと、「動いていないこと」はこのAlarmでは検知できません。実行されたうえで失敗したことだけを見ています。実行そのものの欠落を見たい場合は、別の監視が必要です。

どこへ知らせるか

通知先は、環境ごとにSNS Topicを1つだけ作ります。Lambdaごとに増やしません。

Lambda Alarm(Errors / Duration)─┐
DLQ滞留のAlarm                     ├─> example-project-dev-operations ─> 登録した受け取り先
Pipelineの失敗                     ┘

Pipelineの失敗は、EventBridge Ruleで拾ってTopicへ流します。

  PipelineFailureRule:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - aws.codepipeline
        detail-type:
          - CodePipeline Pipeline Execution State Change
        detail:
          pipeline:
            - !Ref Pipeline
          state:
            - FAILED

成功は通知しません。成功の通知が毎回届くと、失敗の通知が埋もれるためです。

通知先はTemplateに書かない

1つだけ、意図的にTemplateに含めていないものがあります。**誰に通知するか(購読先)**です。

SNS Topic          → Templateで作る
Topicへの購読登録   → 作らない。構築後に手動で登録する

理由は、個人のメールアドレスをリポジトリに残さないためです。担当者が変わるたびにコードを変更してデプロイするのも、運用として重くなります。

そのぶん、通知が誰にも届いていない状態に気付けないという弱点があります。構築したら一度、意図的に失敗させて通知が届くことを確認しておくのが安全です。

6. 本番だけ人の判断を挟む

devやstgは、pushしたらそのまま配備されてかまいません。一方でprodは、配備の直前に人が確認したい場合があります。

CodePipelineには手動承認のActionがあるので、prodのときだけStageを追加します。

Conditions:
  # prod環境のときだけ真になる
  IsProd: !Equals [!Ref Environment, 'prod']
        - !If
          - IsProd
          - Name: Approval
            Actions:
              - Name: Approval
                ActionTypeId:
                  Category: Approval
                  Owner: AWS
                  Provider: Manual
                  Version: '1'
                Configuration:
                  NotificationArn: !Ref OperationsTopic
                  CustomData: !Sub '${ProjectName}-${Environment} へ配備します。Test / Buildは成功済みです。'
                RunOrder: 1
          # prod以外では、この要素そのものが消える
          - !Ref AWS::NoValue

!IfとAWS::NoValueの組み合わせで、環境によってStageの有無を変えられます。同じTemplateを3環境で使いながら、prodだけ承認が入る形になります。

dev / stg : Source → Test → Build ─────────────→ Deploy
prod      : Source → Test → Build → Approval → Deploy

承認のポイントを2つ書いておきます。

項目 内容
承認者の権限 codepipeline:PutApprovalResultが必要。Templateでは付与しないので、IAMで別途設定する
期限 既定で7日。承認しないまま過ぎるとPipelineは失敗する

承認画面に出る説明(CustomData)に環境名を入れているのは、どの環境への配備かを承認者が画面だけで判断できるようにするためです。

7. つまずいたところ

ここでも実際に踏んだものを2つ載せます。どちらも「仕組みは正しいのに動かない」種類のもので、原因が分かりにくいものでした。

タグの付与で権限が足りない

タグを整理していたときの話です。このテンプレートでは、タグをTemplateの各Resourceに書かず、Stackに付けてCloudFormationに伝播させる方式を採っています。

sam deploy ... --tags Project=example-project Environment=dev ManagedBy=CloudFormation

この方式は書き忘れがなくて良いのですが、新しい種類のResourceを追加したときに、その種類へのタグ付け権限が必要になります。

SQSを使うLambdaを追加したとき、こう失敗しました。

Resource handler returned message: "Encountered a permissions error performing a
tagging operation, please add required tag permissions."
User: .../AWSCloudFormation is not authorized to perform: lambda:TagResource
on resource: arn:aws:lambda:ap-northeast-1:123456789012:event-source-mapping:*
(HandlerErrorCode: UnauthorizedTaggingOperation)

lambda:TagResourceは関数に対しては許可していましたが、Event Source Mapping(キューと関数をつなぐResource)には許可していませんでした。

さらに厄介なのは、ARNの末尾がevent-source-mapping:*になっている点です。このResourceのIDは作成時に決まるため、AWSはワイルドカードに対して権限を確認します。つまり名前で範囲を絞れません。

対処として、タグの読み書きだけを別のStatementに分けました。

              - Sid: TagRuntimeEventSourceMappings
                Effect: Allow
                Action:
                  - lambda:ListTags
                  - lambda:TagResource
                  - lambda:UntagResource
                # IDが作成時に決まるため、ここは範囲を絞れない
                Resource: !Sub 'arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:event-source-mapping:*'

範囲を絞れない場合は、せめて操作の種類を絞るという考え方です。タグの読み書き以外はこのStatementに含めていません。

コードを変えていないのにVersionを発行できない

もっと不思議だったのがこちらです。タグをTemplateから削除する変更をデプロイしたら、こう失敗しました。

Resource handler returned message: "A version for this Lambda function exists ( 11 ).
Modify the function to create a new version." (HandlerErrorCode: AlreadyExists)

原因は、2つの仕組みの噛み合わせでした。

見る側 Tagsの扱い
SAM(AutoPublishAliasAllProperties: true) 関数のプロパティ。変わったので新しいVersionを作ろうとする
Lambda 関数の設定ではない。Versionには含まれない

Lambdaの仕様では、コードと設定のどちらも変わっていない場合はVersionを発行しません(公式ドキュメントにも明記されています)。SAMは「プロパティが変わったから新Versionを作る」と判断し、Lambdaは「中身は同じだから作らない」と答えるため、デプロイが失敗します。

対処は、同じデプロイに実際の設定変更を含めることです。ダミーの変更を入れるのは避けたかったので、各関数にDescriptionを付けました。

      FunctionName: !Sub '${AWS::StackName}'
      # 一覧で役割が分かるようにする。Descriptionは関数の設定の一部なので、
      # 変更すると新しいVersionが発行される
      Description: 定期実行のサンプル。1回の実行をログへ記録します。

コンソールの一覧で役割が分かるようになり、実用も兼ねられました。

この現象は、タグのように「Templateでは関数のプロパティだが、Lambdaの設定ではない」項目だけを変えたときに起きます。滅多に起きないぶん、原因にたどり着くのに時間がかかる類のものです。

8. おわりに

3回に分けて、Lambda向けの最小構成のCI/CDを書いてきました。今回扱ったのは、デプロイの次に必要になる部分です。

  • 呼び出し先をAliasに固定し、切り替えを1つの操作にする
  • 設定変更もVersionに含めて、Templateとliveの内容を一致させる
  • Alarmが鳴っている間は切り替えない
  • Alarmと失敗通知を1つのTopicへ集約する
  • prodだけ人の判断を挟む

どれもAWSの標準機能で、自作したコードはほとんどありません。

向いている範囲

この構成は「標準機能の組み合わせで運用できる規模」を想定しています。Lambdaが10〜15個程度までなら、そのまま使えます。

一方で、次のような要求が出てきたら設計を足す必要があります。

必要になること 追加で決めること
Lambdaが増えて全件ビルドが重い 変更検出、キャッシュ、並列実行
段階的リリース(Canary) 切替方式、判定メトリクス、Rollback条件
同じ成果物を環境間で昇格させる Artifactの受け渡しと昇格の記録

「今は要らないものを作らない」と決めておくと、必要になったときに何を足すかが明確になります。

単独のLambda以外の形

記事では単独のLambda(定期実行)だけを扱いましたが、同じ枠組みで別の形も作れます。

形 構成
複数の関数で1つのジョブ Step FunctionsのState Machineと、3つのLambdaを1つのStackへ
キュー処理 SQS Queueとデッドレターキュー、消費するLambdaを1つのStackへ

どちらも「1つのStackに、まとめて配備したい範囲を入れる」という第1回の方針のままです。Stackの中身が変わるだけで、Pipelineも権限の考え方も共通です。

同じような小さなLambdaを何度も作る方に、設計の出発点として参考になればうれしいです。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?