のべラボ.blog

Tech Blog | AWS や サーバーレスやコンテナ などなど

AWS CloudFormation でスタック作成時 Rate exceeded が出て困った話

先日、Cloud9 の環境を複数作成する必要があったので、AWS CloudFormation のテンプレートを作成してスタックを作成しました。

次のテンプレートはあくまで抜粋ですが、実際使ったテンプレートでは 13 個もの AWS Cloud9 の環境を作成しました。

  Cloud901:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      Name: Cloud9box01
      ImageId: amazonlinux-2-x86_64
      InstanceType: t3.small
      OwnerArn: !GetAtt User01.Arn

  Cloud902:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      Name: Cloud9box02
      ImageId: amazonlinux-2-x86_64
      InstanceType: t3.small
      OwnerArn: !GetAtt User02.Arn

  Cloud903:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      Name: Cloud9box03
      ImageId: amazonlinux-2-x86_64
      InstanceType: t3.small
      OwnerArn: !GetAtt User03.Arn

テンプレート作成後、テストでスタック作成行い、問題がない事を確認したのですが、いざ本番環境で必要な時にスタックを作成してみると、Rate exceeded エラーが出てスタック作成に失敗してしまい、大いに慌ててしまうことになりました。

AWS CloudFormation を使用して多くのリソースを作成時に Rate exceeded エラーが出ることは確かにありうることで、次のドキュメントにも記載があります。

aws.amazon.com

このドキュメントにはいくつかの対処策が記載されていますが、今回は DependsOn 属性を使うことで対処しました。

DependsOn 属性は、本来はリソース間の依存性を考慮し、リソース作成の順番を制御するためのものですが、これを活用すれば スタック作成時に、一気にリソース作成の API を発行せず、順番に作成を行うことで、スロットリングによるRate exceeded エラーを回避できそうです。

docs.aws.amazon.com

また、この方法であればテンプレートの記述の変更だけで対処できます。

まずは、1つ1つ順番に作成するように記載してみました。

  Cloud901:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      Name: Cloud9box01
      ImageId: amazonlinux-2-x86_64
      InstanceType: t3.small
      OwnerArn: !GetAtt User01.Arn

  Cloud902:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      Name: Cloud9box02
      ImageId: amazonlinux-2-x86_64
      InstanceType: t3.small
      OwnerArn: !GetAtt User02.Arn
   DependsOn: Cloud901

  Cloud903:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      Name: Cloud9box03
      ImageId: amazonlinux-2-x86_64
      InstanceType: t3.small
      OwnerArn: !GetAtt User03.Arn
   DependsOn: Cloud902

これにより、Cloud9 の作成は完全に1つづつ行われるので、スロットリングによるRate exceeded エラーは確実に回避できそうです。

しかし、1つづつしか作成できないため、スタック作成が完了するまで時間もかかります。DependsOn 属性はスタック作成時だけでなく、削除時もその順番が考慮されるため、削除も1つづつ行われ、かなり時間がかかってしまいました。

私がテストしたところ、13個の Cloud9 の環境を作成するのに約14分、削除するのに 約20分かかりました。

そこで、DependsOn 属性の指定方法を次のように変更しました。

  Cloud901:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      Name: Cloud9box01
      ImageId: amazonlinux-2-x86_64
      InstanceType: t3.small
      OwnerArn: !GetAtt User01.Arn

  Cloud902:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      Name: Cloud9box02
      ImageId: amazonlinux-2-x86_64
      InstanceType: t3.small
      OwnerArn: !GetAtt User02.Arn

  Cloud903:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      Name: Cloud9box03
      ImageId: amazonlinux-2-x86_64
      InstanceType: t3.small
      OwnerArn: !GetAtt User03.Arn
   DependsOn: Cloud902

要するに、同時に2つづつ作成するように、DependsOn 属性の指定の方法を変えました。

こうすることで、スタック作成の時間は約 7分、削除の時間は約 9分に短縮することができ、現実的な運用として使用できることになりました。

時間短縮を目的に同時並行で作成する数を増やしていくと、またスロットリングによるRate exceeded エラーを引き起こす可能性があるため、同時並行作成数の決定には注意する必要がありますね。

また、並行して CloudTrail で証跡を S3 バケットに保存し、スロットリングのエラー発生後、その API コールの情報を Athenaで検索できるようにしました。機会があれば、その辺りについても、記事にしたいと思います。

AWS SAM を使用するための IAM ポリシー

AWS SAM (Serverless Application Model) を使用すると、AWS Lambda などを使用したサーバーレスアプリケーションのテストやデプロイで、様々な便利な機能を使用できます。

aws.amazon.com

今回は、この AWS SAM を使用するために必要な IAM ポリシーについて考えてみます。

どのような IAM ポリシーが必要でしょうか?

もちろん、構築するアプリケーション内容に依存するのですが、AWS のドキュメントにヒントが記載されています。

docs.aws.amazon.com

このドキュメントでは、非常にシンプルな Hello World レベルのアプリケーションだと、以下のポリシーがあれば十分だと記載されています。

  • AWSCloudFormationFullAccess
  • IAMFullAccess
  • AWSLambda_FullAccess
  • AmazonAPIGatewayAdministrator
  • AmazonS3FullAccess
  • AmazonEC2ContainerRegistryFullAccess

ここでいう、Hello World のアプリケーションというのは、hello world という簡単なメッセージを返す Lambda 関数を、Amazon API GatewayREST API から呼び出せるようにしているアプリケーションの事です。

さらに、このドキュメントには、Hello World アプリケーションに対して、きめ細かくポリシーを指定した場合の例も掲載されています。

ドキュメントの例

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "CloudFormationTemplate",
            "Effect": "Allow",
            "Action": [
                "cloudformation:CreateChangeSet"
            ],
            "Resource": [
                "arn:aws:cloudformation:*:aws:transform/Serverless-2016-10-31"
            ]
        },
        {
            "Sid": "CloudFormationStack",
            "Effect": "Allow",
            "Action": [
                "cloudformation:CreateChangeSet",
                "cloudformation:CreateStack",
                "cloudformation:DeleteStack",
                "cloudformation:DescribeChangeSet",
                "cloudformation:DescribeStackEvents",
                "cloudformation:DescribeStacks",
                "cloudformation:ExecuteChangeSet",
                "cloudformation:GetTemplateSummary",
                "cloudformation:ListStackResources",
                "cloudformation:UpdateStack"
            ],
            "Resource": [
                "arn:aws:cloudformation:*:111122223333:stack/*"
            ]
        },
        {
            "Sid": "S3",
            "Effect": "Allow",
            "Action": [
                "s3:CreateBucket",
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::*/*"
            ]
        },
        {
            "Sid": "ECRRepository",
            "Effect": "Allow",
            "Action": [
                "ecr:BatchCheckLayerAvailability",
                "ecr:BatchGetImage",
                "ecr:CompleteLayerUpload",
                "ecr:CreateRepository",
                "ecr:DeleteRepository",
                "ecr:DescribeImages",
                "ecr:DescribeRepositories",
                "ecr:GetDownloadUrlForLayer",
                "ecr:GetRepositoryPolicy",
                "ecr:InitiateLayerUpload",
                "ecr:ListImages",
                "ecr:PutImage",
                "ecr:SetRepositoryPolicy",
                "ecr:UploadLayerPart"
            ],
            "Resource": [
                "arn:aws:ecr:*:111122223333:repository/*"
            ]
        },
        {
            "Sid": "ECRAuthToken",
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "Lambda",
            "Effect": "Allow",
            "Action": [
                "lambda:AddPermission",
                "lambda:CreateFunction",
                "lambda:DeleteFunction",
                "lambda:GetFunction",
                "lambda:GetFunctionConfiguration",
                "lambda:ListTags",
                "lambda:RemovePermission",
                "lambda:TagResource",
                "lambda:UntagResource",
                "lambda:UpdateFunctionCode",
                "lambda:UpdateFunctionConfiguration"
            ],
            "Resource": [
                "arn:aws:lambda:*:111122223333:function:*"
            ]
        },
        {
            "Sid": "IAM",
            "Effect": "Allow",
            "Action": [
                "iam:CreateRole",
                "iam:AttachRolePolicy",
                "iam:DeleteRole",
                "iam:DetachRolePolicy",
                "iam:GetRole",
                "iam:TagRole"
            ],
            "Resource": [
                "arn:aws:iam::111122223333:role/*"
            ]
        },
        {
            "Sid": "IAMPassRole",
            "Effect": "Allow",
            "Action": "iam:PassRole",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "iam:PassedToService": "lambda.amazonaws.com"
                }
            }
        },
        {
            "Sid": "APIGateway",
            "Effect": "Allow",
            "Action": [
                "apigateway:DELETE",
                "apigateway:GET",
                "apigateway:PATCH",
                "apigateway:POST",
                "apigateway:PUT"
            ],
            "Resource": [
                "arn:aws:apigateway:*::*"
            ]
        }
    ]
}

ただし、結論からすると、このドキュメントの例のポリシーは、AWS SAM で Hello World レベルのアプリケーションを作成、更新、削除するには不十分です

例えば、AWS SAM で作成したアプリケーションは、 sam delete というコマンドで削除できますが、ドキュメントの例のポリシーでは、SAM のリソースを格納している S3 バケットからのオブジェクトの削除権限と、CloudFormation の操作である cloudformation:GetTemplate を実行する権限が足りません。

また、この AWS SAM アプリケーションを sam deploy --guided でデプロイする場合にも追加の考慮が必要です。

もし、sam deploy --guided の実行が、そのリージョンにおいて初めての場合、自動的に CloudFormation で aws-sam-cli-managed-default という名前のスタックが構築され、 SAM 用の S3 バケットが作成されますが、ドキュメントの例だと、その作成権限も足りません。

よって、(ドキュメントの冒頭にも記載はありましたが) S3の権限においては AmazonS3FullAccessポリシー またはそれに相当する権限を付与しておかないと、SAM アプリケーションのデプロイや削除が不意に失敗する可能性があります。

次に示すポリシーは、ドキュメントの例を変更したものです。

S3 の権限の範囲を広げており、cloudformation:GetTemplate の操作も許可するよう追加しています。

あくまで一つの例ですが、少なくとも Hello World レベルのアプリケーションの作成、削除は可能です。sam deploy --guided をリージョンで初めて実行する場合でも、権限不足でエラーにはなりません。)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "CloudFormationTemplate",
            "Effect": "Allow",
            "Action": [
                "cloudformation:CreateChangeSet"
            ],
            "Resource": [
                "arn:aws:cloudformation:*:aws:transform/Serverless-2016-10-31"
            ]
        },
        {
            "Sid": "CloudFormationStack",
            "Effect": "Allow",
            "Action": [
                "cloudformation:CreateChangeSet",
                "cloudformation:CreateStack",
                "cloudformation:DeleteStack",
                "cloudformation:DescribeChangeSet",
                "cloudformation:DescribeStackEvents",
                "cloudformation:DescribeStacks",
                "cloudformation:ExecuteChangeSet",
                "cloudformation:GetTemplateSummary",
                "cloudformation:GetTemplate",
                "cloudformation:ListStackResources",
                "cloudformation:UpdateStack"
            ],
            "Resource": [
                "arn:aws:cloudformation:*:111122223333:stack/*"
            ]
        },
        {
            "Sid": "S3",
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "ECRRepository",
            "Effect": "Allow",
            "Action": [
                "ecr:BatchCheckLayerAvailability",
                "ecr:BatchGetImage",
                "ecr:CompleteLayerUpload",
                "ecr:CreateRepository",
                "ecr:DeleteRepository",
                "ecr:DescribeImages",
                "ecr:DescribeRepositories",
                "ecr:GetDownloadUrlForLayer",
                "ecr:GetRepositoryPolicy",
                "ecr:InitiateLayerUpload",
                "ecr:ListImages",
                "ecr:PutImage",
                "ecr:SetRepositoryPolicy",
                "ecr:UploadLayerPart"
            ],
            "Resource": [
                "arn:aws:ecr:*:111122223333:repository/*"
            ]
        },
        {
            "Sid": "ECRAuthToken",
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "Lambda",
            "Effect": "Allow",
            "Action": [
                "lambda:AddPermission",
                "lambda:CreateFunction",
                "lambda:DeleteFunction",
                "lambda:GetFunction",
                "lambda:GetFunctionConfiguration",
                "lambda:ListTags",
                "lambda:RemovePermission",
                "lambda:TagResource",
                "lambda:UntagResource",
                "lambda:UpdateFunctionCode",
                "lambda:UpdateFunctionConfiguration"
            ],
            "Resource": [
                "arn:aws:lambda:*:111122223333:function:*"
            ]
        },
        {
            "Sid": "IAM",
            "Effect": "Allow",
            "Action": [
                "iam:CreateRole",
                "iam:AttachRolePolicy",
                "iam:DeleteRole",
                "iam:DetachRolePolicy",
                "iam:GetRole",
                "iam:TagRole"
            ],
            "Resource": [
                "arn:aws:iam::111122223333:role/*"
            ]
        },
        {
            "Sid": "IAMPassRole",
            "Effect": "Allow",
            "Action": "iam:PassRole",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "iam:PassedToService": "lambda.amazonaws.com"
                }
            }
        },
        {
            "Sid": "APIGateway",
            "Effect": "Allow",
            "Action": [
                "apigateway:DELETE",
                "apigateway:GET",
                "apigateway:PATCH",
                "apigateway:POST",
                "apigateway:PUT"
            ],
            "Resource": [
                "arn:aws:apigateway:*::*"
            ]
        }
    ]
}

例えば、すでにそのリージョンに sam deploy --guided によって SAM 用の S3 バケットが作成されている前提であれば、ポリシーで対象にしているリソースをそのバケットに限定することもできますので、使用する状況により、S3 バケットのポリシーで許可する範囲は絞りこむことをお薦めします。

どなたかの参考になれば幸いです!

Amazon Linux 2 に rbac-lookup をインストールする

Amazon Linux 2 に rbac-lookup をインストールする時に、やや試行錯誤が必要だったので正しくインストールする手順をメモしておきます。

rbac-lookupは、Kubernetes 環境の ロールやクラスターロールとバインドしているユーザーやサービスアカウントの情報をシンプルに表示してくれるコマンドラインツールです。

rbac-lookup.docs.fairwinds.com

rbac-lookup は、Linuxbrew コマンドが使用できる環境であれば容易にインストールできるはずなのですが、Amazon Linux 2 ではデフォルトでは brewが使えません。

よって、まず brew が使用できるようにします。

(実は、ここが一番試行錯誤したところです。)

まず前提として、Kubernetes クラスタに接続できる Amazon Linux 2 の環境にログインします。

以降は、OSユーザーが ssm-user という前提でコマンドを記載します。

まず、Development Tools グループを指定して必要なパッケージ群をインストールします。

sudo yum groupinstall 'Development Tools' -y

次に、Linuxbrew をインストールします。

sh -c "$(curl -fsSL https://raw.githubusercontent.com/Linuxbrew/install/master/install.sh)"

途中で次のように表示されるので、Enter キーを押します。

Press RETURN/ENTER to continue or any other key to abort:

この後、少し時間がかかりますが、brew のインストールが完了しますので、PATH を通しておきましょう。

PATH を通すためのコマンドは、brew のインストール完了時にも表示されています。

次のコマンドは、OSユーザーが ssm-user であることを前提にしています。

echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /home/ssm-user/.bash_profile
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"

念のため、brew doctor を実行して brew が正しく使用できるか確認しておきましょう。

brew doctor

次のように表示されれば OK です。

Your system is ready to brew.

これでようやく、rbac-lookup をインストールできます。

brew install FairwindsOps/tap/rbac-lookup

インストール完了後、さっそく rbac-lookup を試してみましょう。

次のコマンドでは、Kubernetes のグループにバインドされているロールを表示します。

rbac-lookup -k group

結果の例です。

SUBJECT                   SCOPE          ROLE
eks:kube-proxy-windows    cluster-wide   ClusterRole/system:node-proxier
system:authenticated      cluster-wide   ClusterRole/eks:podsecuritypolicy:privileged
system:authenticated      cluster-wide   ClusterRole/system:basic-user
system:authenticated      cluster-wide   ClusterRole/system:discovery
system:authenticated      cluster-wide   ClusterRole/system:public-info-viewer
system:bootstrappers      cluster-wide   ClusterRole/eks:node-bootstrapper
system:masters            cluster-wide   ClusterRole/cluster-admin
system:monitoring         cluster-wide   ClusterRole/system:monitoring
system:node-proxier       cluster-wide   ClusterRole/system:node-proxier
system:nodes              cluster-wide   ClusterRole/eks:node-bootstrapper
system:serviceaccounts    cluster-wide   ClusterRole/system:service-account-issuer-discovery
system:unauthenticated    cluster-wide   ClusterRole/system:public-info-viewer

無事に rbac-lookup を実行できましたね!

今回は、EC2 インスタンスAWS Systems Manager のセッションマネージャーを使用してアクセスしたので、OS ユーザーは ssm-user となっています。

再度セッションマネージャーで接続した後は、rbac-lookup コマンドの PATH を通すために、.bash_profile の内容を反映させる必要があります。

これを手っ取り早く行うには、 sudo su - ssm-user を実行するとよいでしょう。

rbac-lookup はすごく便利なのですが、如何せん、brew でのインストールが必要になります。

macOS 環境であれば簡単にインストールできるのですが、Amazon Linux 2 では、brew 自体のインストールが必要なため、少し手間取りました。

この記事がどなたかの参考になれば嬉しいです!


AWS Step Functions から Amazon ECS のタスクを実行する

今回は、AWS Step Functions で Amazon ECS のタスクを実行するシンプルなステートマシンを作成していきます。

すでに Fargate で動作可能な ECS クラスターや ECS タスク定義、 ECS タスク実行ロール、VPCのサブネットやセキュリティグループは用意している前提です。

ステートマシンで ECS クラスターやタスク定義の ARN の指定が必要になるので、メモしておきます。次に挙げているのは例です。

ECS クラスターの ARN

 arn:aws:ecs:ap-northeast-1:000000000000:cluster/test-cluster

ECS タスク定義の ARN

arn:aws:ecs:ap-northeast-1:000000000000:task-definition/python-web-hello:1

事前に ECS クラスターでタスクが実行できることを確認しておきましょう。

確認ができたら、AWS Step Functions のステートマシンを作成していきます。

今回は Workflow Builder を使用して、標準タイプのステートマシンを作成します。

この図で示している③の API パラメータ で、Cluster に ECS クラスターの ARN を、TaskDefinition に ECS タスク定義の ARN を指定するわけですが、実際は、それらの指定だけでは動作しません。

次の例に示すように、タスクを動作させる Fargate の配置先となる VPC サブネットや、セキュリティグループの ID も必要です。

それらを指定するための NetworkConfiguration 部分は Workflow Studio ではデフォルトで生成されないので注意しましょう。

{
  "LaunchType": "FARGATE",
  "Cluster": "arn:aws:ecs:ap-northeast-1:000000000000:cluster/test-cluster",
  "TaskDefinition": "arn:aws:ecs:ap-northeast-1:000000000000:task-definition/python-web-hello:1",
  "NetworkConfiguration": {
    "AwsvpcConfiguration": {
      "AssignPublicIp": "DISABLED",
      "SecurityGroups": [
        "sg-0c0abc379ab3abfab"
      ],
      "Subnets": [
        "subnet-0abc000ab00ab0",
        "subnet-1abc011ab10ab2",
      ]
    }
  }
}

API パラメータが入力出来たら、 次へ ボタンを 2回選択します。

アクセス許可 セクションでは、新しいロールの作成 を選びます。

これは、「ステートマシンの定義と詳細設定に基づいて、Step Fucntionsが 新しいロールを作成」とあるので良さそうです。

https://cdn-ak.f.st-hatena.com/images/fotolife/n/neob/20220626/20220626081703.png

しかし、実はそうではないのですが、敢えてこのまま続けます。

ステートマシンの作成を完了します。

その後、実行の開始 ボタンを選択して、ステートマシンを実行してみましょう。

ステートマシンの実行は失敗します!

RunTaskのステートの例外の情報を見てみましょう。

エラー

ECS.AccessDeniedException

原因

User: arn:aws:sts::000000000000:assumed-role/StepFunctions-MyStateMachine-role-e4865d7b/CPOWNuVYmrftNqvlpPKDpWuiEvWJfAsi is not authorized to perform: iam:PassRole on resource: arn:aws:iam::000000000000:role/ecsTaskExecutionRole because no identity-based policy allows the iam:PassRole action (Service: AmazonECS; Status Code: 400; Error Code: AccessDeniedException; Request ID: acbdd742-e0b2-4b53-937b-68be10c7c864; Proxy: null)

ECSのタスク実行ロールを passRole するポリシーが許可されていないことが原因ですね。

ステートマシン作成時に、自動的に新しいロールを作る設定の説明では「ステートマシンの定義と詳細設定に基づいて、Step Fucntionsが 新しいロールを作成」と記載されていたのですが、ECSのタスク実行ロールの passRole を許可するポリシーは、作成されるロールに設定してくれないのです。

そのため、このエラーメッセージで表示されている IAMロールに対して、次のポリシーを追加で許可しましょう。 (AWS アカウントID部分は、環境に合わせて変更して下さい。)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "iam:PassRole",
            "Resource": "arn:aws:iam::000000000000:role/ecsTaskExecutionRole"
        }
    ]
}

ポリシーを追加したら、再度ステートマシンを実行してみて下さい。

今度は、うまく動きましたね。

https://cdn-ak.f.st-hatena.com/images/fotolife/n/neob/20220716/20220716210806.png

ECSのコンソールでも動作していることが確認できます。

https://cdn-ak.f.st-hatena.com/images/fotolife/n/neob/20220717/20220717084808.png

成功を確認したら、ECSのコンソールからタスクを停止して下さい。(コストを考慮しての停止です。)

AWS Step Functions の ECS のタスクを実行するステートマシンを作成すること自体は非常に簡単に行えますが、IAMロールについては自動作成に頼るよりも、あらかじめ必要なIAMロールを作成しておく方が良さそうですね!

AWS Lambda関数の Init フェーズのタイムアウト

今回は、AWS Lambda関数の Init フェーズタイムアウトが発生した場合の動作を検証してみます。

AWS Lambda関数の実行環境のライフサイクルについては、次のドキュメントで説明されています。

docs.aws.amazon.com

このドキュメントにもあるように、Lambda関数は最初に呼び出されると Init フェーズが実行され初期化処理が行われれます。このフェーズでは、Lambda関数のハンドラー関数の外側にあるコードも実行されます。

次に Invoke フェーズでLambda関数のハンドラー関数が実行されます。また、しばらくLambda関数が呼び出されない状態になると、Shutdown フェーズがトリガーされ、Lambda関数の実行環境は停止、削除されます。

Init フェーズは、Lambda関数の実行環境が作成されたときに 1回だけ実行されます。 つまり、その実行環境が Shutdown フェーズで削除されない限りは、2回目以降のLambda関数の呼び出しでは Invoke フェーズから実行されます。

また、前述の AWS Lambda関数の実行環境のライフサイクルのドキュメントをみると、次のような記述があります。

Init フェーズは 10 秒に制限されています。3 つのタスクすべてが 10 秒以内に完了しない場合、Lambda は最初の関数呼び出し時に Init フェーズを再試行します。

これはLambda関数を扱う上では、重要な記述だと思います。

Lambda関数には、最大 15分のタイムアウトを設定できます。(2022年 7月現在)

しかし、この最大 15分のタイムアウトというのは Invokeフェース に対する設定であり、Init フェーズに対する設定ではないということになります。

Init フェーズには、固定で 10 秒というタイムアウトがあり、しかも、もしタイムアウトが発生した場合は、Init フェーズ の処理が Invokeフェーズとして再試行されます。

つまり、Lambda関数のハンドラー関数の外側にあるコードは10秒以内に完了しないと、再試行により再度同じコードが実行されることになります。

実際に試してみましょう。今回は、Python 3.9 のLambda関数を使って検証します。

(なお、次から紹介するコードは、動作検証を目的としたものであり本番環境を想定したものではないことはご了承ください。)


Init フェーズでタイムアウトしないケース

次のLambda関数のコードでは、ハンドラー関数の外側で1回だけ Amazon SQS の Queue に日時のデータを含めたメッセージを送信しています。 Lambda関数のタイムアウト、つまり Invoke フェーズのタイム後は 3分 に設定しています。

import json
import time
import datetime
import boto3
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# ハンドラー関数の外
logger.info('--- Init sleep start ---')
sqs = boto3.resource('sqs', region_name='ap-northeast-1') # SQSのリソースの取得
queue = sqs.get_queue_by_name(QueueName='DemoQ')          # Queueの取得
current = datetime.datetime.now()                         # 日時を取得
message = 'current: ' + str(current)                      # Queueに送信するメッセージを用意
queue.send_message(MessageBody=message)                   # Queueにメッセージを送信
time.sleep(5)                                             # 5秒 sleep
logger.info('--- Init sleep end ---')

# ハンドラー関数
def lambda_handler(event, context):
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

このコードは Init フェーズでは 5秒強ほどの時間はかかりますが、10秒のタイムアウトにはひっかからりません。 Amazon SQS の 対象のQueue 内のメッセージを確認すると、1つのメッセージしか送信されていませんでした。

メッセージの本文

current: 2022-07-09 04:41:38.200748

また、実行ログをみても、ハンドラー関数の外側のコードは 一度しか実行されていないことがわかります。

START RequestId: 2c1d6bb0-5be9-45e3-8905-ba9716b9e539 Version: $LATEST
[INFO]  2022-07-09T05:17:11.153Z        --- Init sleep start ---
[INFO]  2022-07-09T05:17:11.208Z        Found credentials in environment variables.
[INFO]  2022-07-09T05:17:16.301Z        --- Init sleep end ---
END RequestId: 2c1d6bb0-5be9-45e3-8905-ba9716b9e539
REPORT RequestId: 2c1d6bb0-5be9-45e3-8905-ba9716b9e539  Duration: 1.05 ms   Billed Duration: 2 ms   Memory Size: 128 MB Max Memory Used: 66 MB  Init Duration: 5387.97 ms   

Init フェーズでタイムアウトするケース

次は、Init フェーズで10秒以上実行されるようにして試してみます。 time.sleep(5)time.sleep(11) に変更するだけです。

import json
import time
import datetime
import boto3
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# ハンドラー関数の外
logger.info('--- Init sleep start ---')
sqs = boto3.resource('sqs', region_name='ap-northeast-1') # SQSのリソースの取得
queue = sqs.get_queue_by_name(QueueName='DemoQ')          # Queueの取得
current = datetime.datetime.now()                         # 日時を取得
message = 'current: ' + str(current)                      # Queueに送信するメッセージを用意
queue.send_message(MessageBody=message)                   # Queueにメッセージを送信
time.sleep(11)                                            # 11秒 sleep
logger.info('--- Init sleep end ---')

# ハンドラー関数
def lambda_handler(event, context):
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

このコードを実行すると、正常には修了しますが、Amazon SQS の 対象のQueue 内のメッセージを確認すると、2つのメッセージが送信されています。

1つ目のメッセージの本文

current: 2022-07-09 04:55:14.241178

2つ目のメッセージの本文

current: 2022-07-09 04:55:27.429081

2つのメッセージが送信されたということは、ハンドラー関数の外側のコードが 2回実行されたことになります。

また、実行ログをみても、ハンドラー関数の外側のコードが 再試行されていることがわかります。

[INFO]   2022-07-09T05:02:04.432Z        --- Init sleep start ---
[INFO]  2022-07-09T05:02:04.489Z        Found credentials in environment variables.
START RequestId: f5bf8835-42ba-4134-92e1-c75544cc3e42 Version: $LATEST
[INFO]  2022-07-09T05:02:16.883Z        --- Init sleep start ---
[INFO]  2022-07-09T05:02:17.183Z        Found credentials in environment variables.
[INFO]  2022-07-09T05:02:28.840Z        --- Init sleep end ---
END RequestId: f5bf8835-42ba-4134-92e1-c75544cc3e42
REPORT RequestId: f5bf8835-42ba-4134-92e1-c75544cc3e42  Duration: 14592.69 ms   Billed Duration: 14593 ms   Memory Size: 128 MB Max Memory Used: 25 MB  

ドキュメントの記載通りですね。


まとめ

Lambda関数では、ハンドラー関数のタイムアウトを意識することは多いと思いますが、Init フェーズ つまりハンドラー関数の外側のコードのタイムアウトについても、意識しておく必要があります。

Init フェーズのタイムアウトは、10秒固定で、変更することができません。

また、Init フェーズのタイムアウトが発生した場合は、Invoke フェーズで再試行されます。

もともとLambda関数のコードは冪等にする必要がありますが、この Init フェーズのコードの再試行という観点 でも冪等に留意しておく必要があります。

モダンなJavaScriptのお勉強:アロー関数編

ひさびさに モダンな JavaScript のお勉強ネタを書きます。

今回は、アロー関数について勉強しています。 アロー関数は、ES2015から追加された関数の記述方法の一つです。

まず、従来の関数の記述方法をおさらいしておきましょう。

次のように、function のあとに関数名、そして( ) で囲んだ引数名を指定し、関数の処理は { } で囲みます。

function sayHello(yourname) {
    return "Hello! " + yourname;
}

console.log(sayHello("Nobe"));

このコードを実行すると、次のようになります。

結果:

Hello! Nobe

また、次のように関数を変数に代入して扱うこともできます。

const hellofunc = function sayHello(yourname) {
    return "Hello! " + yourname;
}
console.log(hellofunc("Nobe"));

このコードの実行結果は、最初のコード例と同じです。

結果:

Hello! Nobe

では、アロー関数の記述方法をみていきます。

アロー関数では、関数自体に名前を付けず、いきなり引数から記述するのが特徴です。

次の例では、(yourname) が関数の引数を表しています。 その後に => と記載し、関数内の処理を { }内に記載します。

(yourname) => { 
    return "Hello! " + yourname;
}

つまり、yourname という引数を受け取り、それに Hello という文字列を連結して return するという関数になります。 関数の名前はありませんが、次の例のように変数に代入して、変数名で呼び出すことができます。

const hello_arrow1 = (yourname) => { 
    return "Hello! " + yourname;
}
console.log(hello_arrow1("Nobe1"));

結果:

Hello! Nobe1

まずは、(引数) => { 関数の処理 } という基本形を覚えておくとよいでしょう。

ただ、アロー関数では、省略した記述ができるケースがあります。 例えば、引数が1つの場合は、次のように引数を囲んでいる ( )を省略できます。

// 引数が1つの場合は、() を省略できる
const hello_arrow2 = yourname => { 
    return "Hello! " + yourname;
}
console.log(hello_arrow2("Nobe2"));

結果:

Hello! Nobe2

逆にいうと、引数が複数の場合は、引数を囲む ( )は必須になります。

// 引数が複数の場合は、() が必要
const hello_arrow3 = (yourname,youraddress) => { 
    return "Hello! " + yourname + " in " + youraddress;
}
console.log(hello_arrow3("Nobe3","Kyoto"));

結果:

Hello! Nobe3 in Kyoto

その他の省略可能なルールとして、次の例をみてみましょう。

// 関数の処理が単一行でその結果をreturnする場合は、{} と return 句を省略できる
const hello_arrow4 = yourname => 
  "Hello! " + yourname;
console.log(hello_arrow4("Nobe4"));

結果:

Hello! Nobe4

本来は関数の処理として { return "Hello! " + yourname; } と記述するのですが、処理が単一行であれば、関数の処理を囲む { }return 句を省略できるので、このような記述になるわけです。

逆にいうと、処理が複数行になる場合は、次のように 関数の処理を囲む{ }は必須で、関数が値を返す場合は return 句も必要になります。

// 関数の処理が複数行になる場合は、{}が必要。return 句は省略できない
const hello_arrow5 = yourname => {
  const greeting = "Hello! ";
  return greeting + yourname;
}
console.log(hello_arrow5("Nobe5"));

結果:

Hello! Nobe5

次に、オブジェクトのように複数行を return する場合をみてみましょう。 この場合、return する値を ( ) で囲むことで、そのオブジェクトを return することになります。

// returnする内容が複数行になる場合は、() で囲む
const hello_arrow6 = (yourname,youraddress) => (
     {
        name:    yourname,
        address: youraddress
     }
)
console.log(hello_arrow6("Nobe6","Kyoto"));

結果:

{ name: 'Nobe6', address: 'Kyoto' }

まとめ


JavaScript ではなく、Java 言語のラムダ式をご存じの場合は、このアロー関数がよく似ていると感じるでしょう。 JavaScript のアロー関数にせよ、Java 言語のラムダ式にせよ、基本的な記述方法がわかっていれば、コードを読み解くことは難しくありません。 アロー関数では、基本的に => があれば、その左にあるものは引数、右にあるものが関数の処理本体として読み解くと良いでしょう。

AWS Step Functions と Amazon EKS の連携でハマったこと

AWS Step Functions のステートマシンから Amazon EKS クラスターで Job を実行しようとしたときにハマったことをメモしておきます。

最初は、次の AWS Blog の記事がキッカケでした。

AWS Step FunctionsとAmazon EKSの統合のご紹介

この記事では、AWS Step Functions と Amazon EKS の統合について記載されてます。

これを、自分なりに試してみようと思ったわけです。

ただシンプルに試したかったので、EKS クラスターで Job の実行を行うだけのステートマシンを作成することにしました。

まず、自分が既に用意している EKS クラスターのエンドポイントや CertificateAuthority の情報を用意しておきます。

これは AWS マネジメントコンソール で EKSのクラスターの情報を表示すれば参照できますが、今回は AWS CLI で取得しています。

次のコマンドは dev-cluster というクラスターのエンドポイントを取得する例です。

aws eks describe-cluster --name dev-cluster | jq '.cluster.endpoint'

結果の例

"https://xxxxxxxxxxxxxxx.yz5.ap-northeast-1.eks.amazonaws.com"

次は CertificateAuthority を取得する例です。

aws eks describe-cluster --name dev-cluster | jq '.cluster.certificateAuthority.data'

結果の例 (実際は長い文字列ですが、省略しています。)

"LS0t6UFBwS29jU ...  0tCg=="

次に、AWS Step Functions で 標準タイプのステートマシンを作成していきます。

WorkFlow Builderで、次の図のように作成します。③ の APIパラメータに、EKS クラスター名やエンドポイント、CertificateAuthority を設定します。

③ の APIパラメータで、実行する Job のマニフェストを指定できます。デフォルトのままでもいいのですが、Job の実行時間を短縮したかったので、print bpi(2000) の個所を print bpi(20) に変更しました。

次の例は、APIパラメータで、Job のマニフェスト部分を抜粋したものです。

        "Job": {
          "apiVersion": "batch/v1",
          "kind": "Job",
          "metadata": {
            "name": "my-example-job20"
          },
          "spec": {
            "template": {
              "metadata": {
                "name": "my-example-job"
              },
              "spec": {
                "containers": [
                  {
                    "name": "my-function-name",
                    "image": "perl",
                    "command": [
                      "perl"
                    ],
                    "args": [
                      "-Mbignum=bpi",
                      "-wle",
                      "print bpi(20)"
                    ]
                  }
                ],
                "restartPolicy": "Never"
              }
            }
          }
        }
      },

次に、[ステートマシン設定を指定] のページの [アクセス許可] のセクションで、ステートマシンに設定するIAM ロールを指定します。 今回は、新しい IAM ロールを作成するように指定します。

そしてステートマシンの作成を完了させると、自動作成された IAM ロールの ARN が表示されるので、それをコピーします。

この IAM ロールは、 EKS クラスターで Job を実行する許可を与える必要があるため、EKS クラスターに接続できる環境から eksctl コマンドを使って次を実行します。

なお、AWSアカウント ID や IAM ロール名、EKS クラスター名は環境に応じて変更する必要があります。

実はこれがハマるポイントなのですが、説明は後にして、ひとまずこのまま実行します。

eksctl create iamidentitymapping --cluster dev-cluster --arn arn:aws:iam::000000000000:role/service-role/StepFunctions-EKS-RunJob-StateMachine-role-4b53263d --group system:masters --username my-statemachine

このコマンドは、あくまで例の一つですが、ステートマシンの IAM ロールと Kubernetes の system:masters グループをマッピングすることで、ステートマシンが EKS クラスターで Job を実行することを許可しています。

これで、ステートマシンから EKS クラスターに対して Job を実行する準備が整いました。

しかし、ステートマシンを実行すると、次のようなエラーが出てしまいます。

エラー

EKS.401
原因

{
  "ResponseBody": {
    "kind": "Status",
    "apiVersion": "v1",
    "metadata": {},
    "status": "Failure",
    "message": "Unauthorized",
    "reason": "Unauthorized",
    "code": 401
  },
  "StatusCode": 401,
  "StatusText": "Unauthorized"
}

さきほどの ekstctl コマンドで、IAM ロールを system:masters グループにマッピングしたはずなのに、なぜ Unauthorized になるのか理解できず、しばらく悩みました。

しかし、次の Step Functions のドキュメントをみると、この原因がわかりました。

Permissions | Call Amazon EKS with Step Functions

このドキュメントには、Note として次のような記載があります。

Note You may see the ARN for an IAM role displayed in a format that includes the path /service-role/, such as arn:aws:iam::123456789012:role/service-role/my-role. This service-role path token should not be included when listing the role in aws-auth.

つまり、ekstctl コマンドで、IAM ロールを system:masters グループにマッピングするときに、IAM ロールのARNに /service-role/ というパスを含めてはいけない! ということなんですね。

では、修正していきます。

まず、さきほどのマッピングを削除します。

eksctl delete iamidentitymapping --cluster dev-cluster --arn arn:aws:iam::000000000000:role/service-role/StepFunctions-EKS-RunJob-StateMachine-role-4b53263d  

次に、/service-role/ を含まない ARN を指定して、マッピングを作成します。

eksctl create iamidentitymapping --cluster dev-cluster --arn arn:aws:iam::000000000000:role/StepFunctions-EKS-RunJob-StateMachine-role-4b53263d --group system:masters --username my-statemachine

このあと、ステートマシンを実行すると問題なく Job を起動することができました。

また、EKS クラスター側でも Job が完了したことを確認できました。

$ kubectl get jobs
NAME               COMPLETIONS   DURATION   AGE
my-example-job     1/1           4s         4m21s

$ kubectl describe  job my-example-job
Name:           my-example-job
Namespace:      default
Selector:       controller-uid=cc291e67-8d12-4f64-b6c3-01e330957bed
Labels:         controller-uid=cc291e67-8d12-4f64-b6c3-01e330957bed
                job-name=my-example-job
Annotations:    <none>
Parallelism:    1
Completions:    1
Start Time:     Sun, 26 Jun 2022 00:24:58 +0000
Completed At:   Sun, 26 Jun 2022 00:25:02 +0000
Duration:       4s
Pods Statuses:  0 Running / 1 Succeeded / 0 Failed
Pod Template:
  Labels:  controller-uid=cc291e67-8d12-4f64-b6c3-01e330957bed
           job-name=my-example-job
  Containers:
   my-function-name:
    Image:      perl
    Port:       <none>
    Host Port:  <none>
    Command:
      perl
    Args:
      -Mbignum=bpi
      -wle
      print bpi(20)
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Events:
  Type    Reason            Age    From            Message
  ----    ------            ----   ----            -------
  Normal  SuccessfulCreate  4m33s  job-controller  Created pod: my-example-job-8k5xr
  Normal  Completed         4m29s  job-controller  Job completed

ハマったといっても実はドキュメントには Note として記載してあったので、うっかりのレベルではあります。

ただ、ステートマシン作成時に IAM ロールを作成して、そのままコンソールに表示されている ARN をコピーしてしまうということは、ありがちではないかと思うので注意したいと思います。

/* -----codeの行番号----- */