AWS CodePipelineの手動承認をカスタマイズして、匿名で承認出来るようにしてみた
いわさです。
CodePipilineでは手動承認のアクションが用意されていて、デプロイ前に管理者の承認を得たのちにデプロイを実施するなどを行うことが出来ます。
そんな手動承認アクションですが、CodePipelineでの主な設定先はSNSトピックを指定するくらいです。
一見カスタマイズ性があまり高くないように見えますが、実際にはSNSトピックから様々な拡張を行うことが出来ます。
本日は、カスタム例として以下のように手動承認フローをカスタマイズしてみました。
- メール通知内容のカスタマイズ
- 匿名で承認(AWSへのサインイン不要でメールから即承認)
手動承認のおさらい
手動承認アクションを設定すると、パイプラインの特定ステージで手動での承認が行われるまでパイプラインを待機させることが出来ます。
そして待機状態に入ったあとは、コンソールのレビューボタンで承認か拒否を選択することでパイプラインが再開されます。
また、手動承認アクションの追加のオプションとして大きなところとしてはSNSトピックを指定することが出来ます。
例えば、こちらにEメールを連携したSNSトピックを指定すると、以下のように手動承認が必要な旨のメールが送信されます。
受信者はメールに記載されているURLにアクセスし、AWSマネジメントコンソールへサインインします。
サインイン後はCodePipelineの対象パイプラインのフロー画面に遷移するので、そこでレビュー操作を行う形となっています。
デフォルトのSNS通知を使ったEメール承認の流れはこのような形となっています。
CodePipeline手動承認アクションの追加の情報
シンプルにEメール購読しているSNSトピックを設定した場合は上記のような動きとなりますが、実際にはSNSトピックは様々な使い方が出来ます。
SNSへのJSON出力を利用してSQSやLambdaで処理することで、通知する方法やフォーマットなどもカスタマイズ出来ます。
以下は手動承認アクションで発生するJSON出力の例です。
{ "region": "us-east-2", "consoleLink": "https://console.aws.amazon.com/codepipeline/home?region=us-east-2#/view/MyFirstPipeline", "approval": { "pipelineName": "MyFirstPipeline", "stageName": "MyApprovalStage", "actionName": "MyApprovalAction", "token": "1a2b3c4d-573f-4ea7-a67E-XAMPLETOKEN", "expires": "2016-07-07T20:22Z", "externalEntityLink": "http://example.com", "approvalReviewLink": "https://console.aws.amazon.com/codepipeline/home?region=us-east-2#/view/MyFirstPipeline/MyApprovalStage/MyApprovalAction/approve/1a2b3c4d-573f-4ea7-a67E-XAMPLETOKEN", "customData": "Review the latest changes and approve or reject within seven days." } }
CodePipeline でのマニュアルの承認通知の JSON データ形式 - AWS CodePipeline
次に承認の方法です。
マネジメントコンソールへサインインし、レビュー操作を行う以外に、上記出力から得られるトークンとcodepipeline:PutApprovalResult
権限さえあれば、CLIでもAPIからでも手動承認が可能です。
うまく組み合わせることで通知方法や承認方法はいくらでもカスタマイズできそうです。
カスタマイズしてみよう
この記事では以下のような形でカスタマイズしてみることにしました。
SNSで直接メールを送らずにLambdaで一度通知内容を加工します。
通知分には、別で用意するHTTPエンドポイントへリクエストが送信されるようなURLをトークン付きで構成します。
これによってメール通知内容をカスタマイズすることが出来ます。
承認のためにリクエストを受けるHTTPエンドポイントも用意しましょう。
ここでは、匿名アクセスが出来て、実行ロールが対象パイプラインへの承認権限を持っている、Lambdaの関数URLを使ってみます。
API Gatewayでも良いのですが、今回は検証用途なのでLambda関数URLをCloudFormationで構築してみることに。
CloudFormationテンプレートの全体は以下のリポジトリにおいておきます。
この記事ではポイントとなる部分だけ少し抜粋します。
以下は、別のSNSへ送信するLambda処理です。
前述のJSON出力にアクセスして、必要なパラメータを取得し、URLパラメータに利用しています。
SendNotifyFunction: Type: AWS::Lambda::Function Properties: FunctionName: !Sub ${AWS::StackName}-SendNotify-Function Description: !Sub ${AWS::StackName}-SendNotify-Function Code: ZipFile: !Sub - | import json import boto3 def handler(event, context): client = boto3.client('sns') for record in event['Records']: codePipelineMessage = json.loads(record['Sns']['Message']) pipelineName = codePipelineMessage['approval']['pipelineName'] stageName = codePipelineMessage['approval']['stageName'] actionName = codePipelineMessage['approval']['actionName'] token = codePipelineMessage['approval']['token'] url = '${approval_url}' + '?pipelinename=' + pipelineName + '&stagename=' + stageName + '&actionname=' + actionName + '&token=' + token + '&action=' request = { 'TopicArn': '${dest_topic_arn}', 'Message': '承認\n' + url + 'approve\n' + '拒否\n' + url + 'deny\n', 'Subject': 'hoge' } client.publish(**request) - { dest_topic_arn: !Ref snsTopicNotify, approval_url: !GetAtt ApprovalUrl.FunctionUrl } Handler: index.handler MemorySize: 128 Role: !GetAtt SendNotifyFunctionExecRole.Arn Runtime: python3.8 Timeout: 10
以下は、匿名での承認リクエストを受けるLambdaです。
今回初めて関数URL構築したのですが、AWS::Lambda::Url
を追加で設定するのですね。なるほど。
そして、関数URLを匿名アクセスで構築する場合は、AWS::Lambda::Permission
でFunctionUrlAuthType
などの設定も必要です。
ApprovalUrl: Type: AWS::Lambda::Url Properties: AuthType: NONE TargetFunctionArn: !GetAtt ApprovalFunction.Arn ApprovalFunction: Type: AWS::Lambda::Function Properties: FunctionName: !Sub ${AWS::StackName}-Approval-Function Description: !Sub ${AWS::StackName}-Approval-Function Code: ZipFile: | import json import boto3 def handler(event, context): client = boto3.client('codepipeline') hoge = 'Approved' if event['queryStringParameters']['action'] == 'approve' else 'Rejected' client.put_approval_result( pipelineName=event['queryStringParameters']['pipelinename'], stageName=event['queryStringParameters']['stagename'], actionName=event['queryStringParameters']['actionname'], result={ 'summary':'', 'status': hoge }, token=event['queryStringParameters']['token'] ) return hoge Handler: index.handler MemorySize: 128 Role: !GetAtt ApprovalFunctionExecRole.Arn Runtime: python3.8 Timeout: 10 ApprovalFunctionExecRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: !Sub ${AWS::StackName}-ApprovalFunction PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: "codepipeline:PutApprovalResult" Resource: - Fn::Join: - "" - - !Ref PipelineArn - "/*/*" ApprovalFunctionInvokePermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunctionUrl FunctionName: !GetAtt ApprovalFunction.Arn Principal: '*' FunctionUrlAuthType: NONE
メールのリンクにリクエストを送信してみる
これで、CodePipelineを時刻してみると、2つ目のSNSで設定した通知先に承認URLと拒否URLが通知されます。
承認 https://2n4oioxgaibaw663les6gpgi240ofzuz.lambda-url.ap-northeast-1.on.aws/?pipelinename=hoge-pipeline&stagename=hoge&actionname=hogehoge&token=b66dae12-c0ae-456e-9d1a-4e8266be6181&action=approve 拒否 https://2n4oioxgaibaw663les6gpgi240ofzuz.lambda-url.ap-northeast-1.on.aws/?pipelinename=hoge-pipeline&stagename=hoge&actionname=hogehoge&token=b66dae12-c0ae-456e-9d1a-4e8266be6181&action=deny
試してみましょう。
$ curl "https://2n4oioxgaibaw663les6gpgi240ofzuz.lambda-url.ap-northeast-1.on.aws/?pipelineName=hoge-pipeline&stageName=hoge&actionName=hogehoge&token=27db0033-2119-459a-91fa-07ae4c45d060&action=approve" Approved
承認されました。
$ curl "https://2n4oioxgaibaw663les6gpgi240ofzuz.lambda-url.ap-northeast-1.on.aws/?pipelinename=hoge-pipeline&stagename=hoge&actionname=hogehoge&token=b66dae12-c0ae-456e-9d1a-4e8266be6181&action=deny" Rejected
拒否されました。
さいごに
本日は、CodePipelineの手動承認アクションをカスタマイズしてみました。
ちなみに、トークンの期間は1週間で、変更は出来ないようです。
この記事では、匿名で承認出来るようにしてみましたが、あくまでもカスタマイズ例なので実際にご利用される際はセキュリティなど別途ご検討ください。