のべラボ.blog

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

eksctl で Kubernetes 1.23 の Amazon EKS クラスターを作成する

2022年8月11日に、Amazon Elastic Kubernetes Service ( Amazon EKS ) で Kubernetes のバージョン 1.23 がサポートされました。

aws.amazon.com

そこで、早速 バージョン 1.23 の EKS クラスターを作成しようと思ったのですが、その時点ではまだ eksctl が対応していないことが判明しました。

1.23 のサポートがアナウンスされた後に、eksctl のバージョンはアップグレードしたのですが、次のように、eksctl のバージョンが 1.0.8 以下の場合、eksctl で EKS クラスターを作成しようとすると指定バージョンが対象外であるとエラーになります。

$ eksctl version
0.108.0
$ eksctl create cluster \
> --name test123-cluster \
> --vpc-public-subnets subnet-0b10715b2edf27dde,subnet-023105dfe3f0a2bdb  \
> --nodegroup-name test123-nodes \
> --node-type t3.small \
> --nodes 2 \
> --nodes-min 1 \
> --nodes-max 3 \
> --managed \
> --version 1.23 \
> --region ap-northeast-1
2022-08-26 13:28:34 []  eksctl version 0.108.0
2022-08-26 13:28:34 []  using region ap-northeast-1
Error: invalid version, supported values: 1.19, 1.20, 1.21, 1.22

EKS で サポートされる Kubernetes のバージョンが追加されたからといって、同時に そのバージョンに対応した eksctl がリリースされているわけではないんですね。

で、約 2週間ほど経って、 eksctl のリリース情報を確認すると、どうやら バージョン 1.109.0 で Kubernetes のバージョン 1.23 に対応したことがわかりました。

newreleases.io

この eksctl のリリース情報をみると、EKS で 1.23 のサポートがアナウンスされてから、約 1週間後 くらいにリリースされたようです。

AWSAmazon EKS のドキュメントで eksctl のインストールの説明部分をみると、英語版の方では、バージョンの表示例が 1.0.9 になっていました。(2022年8月27日現在)

Installing or updating eksctl - Amazon EKS

If you have eksctl installed in the path of your device, the example output is as follows. If you want to update the version that you currently have installed with a later version, complete the next step, making sure to install the new version in the same location that your current version is in. 0.109.0

ただし、日本語の方では、バージョンの表示例は まだ 1.0.5 になっていました。(2022年8月27日現在)いずれ更新されると思います。

eksctl のインストールまたは更新 - Amazon EKS

eksctl がデバイスのパスにインストールされている場合、出力例は次のようになります。現在インストールされているバージョンを新しいバージョンで更新する場合は、次の手順を完了し、新しいバージョンを現在のバージョンと同じ場所にインストールするようにします。 0.105.0

ともあれ、eksctl で バージョン 1.23 の EKS クラスターを作成できるようになったのは、嬉しい事です。

早速、eksctl のバージョンを 1.109.0 にアップグレードして、EKSクラスター 1.23 の作成を試してみました。

$ eksctl version
0.109.0
$ eksctl create cluster \
> --name test123-cluster \
> --vpc-public-subnets subnet-0b10715b2edf27dde,subnet-023105dfe3f0a2bdb  \
> --nodegroup-name test123-nodes \
> --node-type t3.small \
> --nodes 2 \
> --nodes-min 1 \
> --nodes-max 3 \
> --managed \
> --version 1.23 \
> --region ap-northeast-1
2022-08-26 13:15:16 []  eksctl version 0.109.0
2022-08-26 13:15:16 []  using region ap-northeast-1
2022-08-26 13:15:16 []  using existing VPC (vpc-0260526e8e00b9bb6) and subnets (private:map[] public:map[ap-northeast-1a:{subnet-0b10715b2edf27dde ap-northeast-1a 10.0.0.0/24 0} ap-northeast-1c:{subnet-023105dfe3f0a2bdb ap-northeast-1c 10.0.10.0/24 0}])
2022-08-26 13:15:16 [!]  custom VPC/subnets will be used; if resulting cluster doesn't function as expected,make sure to review the configuration of VPC/subnets
2022-08-26 13:15:16 [ℹ]  nodegroup "test123-nodes" will use "" [AmazonLinux2/1.23]
2022-08-26 13:15:16 [ℹ]  using Kubernetes version 1.23
2022-08-26 13:15:16 [ℹ]  creating EKS cluster "test123-cluster" in "ap-northeast-1" region with managed nodes
2022-08-26 13:15:16 [ℹ]  will create 2 separate CloudFormation stacks for cluster itself and the initial managed nodegroup
2022-08-26 13:15:16 [ℹ]  if you encounter any issues, check CloudFormation console or try 'eksctl utils describe-stacks --region=ap-northeast-1 --cluster=test123-cluster'
2022-08-26 13:15:16 [ℹ]  Kubernetes API endpoint access will use default of {publicAccess=true, privateAccess=false} for cluster "test123-cluster" in "ap-northeast-1"
2022-08-26 13:15:16 [ℹ]  CloudWatch logging will not be enabled for cluster "test123-cluster" in "ap-northeast-1"
2022-08-26 13:15:16 [ℹ]  you can enable it with 'eksctl utils update-cluster-logging --enable-types={SPECIFY-YOUR-LOG-TYPES-HERE (e.g. all)} --region=ap-northeast-1 --cluster=test123-cluster'
2022-08-26 13:15:16 [ℹ]
2 sequential tasks: { create cluster control plane "test123-cluster",
    2 sequential sub-tasks: {
        wait for control plane to become ready,
        create managed nodegroup "test123-nodes",
    }
}
2022-08-26 13:15:16 [ℹ]  building cluster stack "eksctl-test123-cluster-cluster"
2022-08-26 13:15:16 [ℹ]  deploying stack "eksctl-test123-cluster-cluster"
2022-08-26 13:15:46 [ℹ]  waiting for CloudFormation stack "eksctl-test123-cluster-cluster"
(中略)
2022-08-26 13:29:18 [ℹ]  building managed nodegroup stack "eksctl-test123-cluster-nodegroup-test123-nodes"
2022-08-26 13:29:18 [ℹ]  deploying stack "eksctl-test123-cluster-nodegroup-test123-nodes"
2022-08-26 13:29:18 [ℹ]  waiting for CloudFormation stack "eksctl-test123-cluster-nodegroup-test123-nodes"
(中略)
2022-08-26 13:33:01 [ℹ]  waiting for the control plane availability...
2022-08-26 13:33:03 [ℹ]  saved kubeconfig as "/home/ssm-user/.kube/config"
2022-08-26 13:33:03 [ℹ]  no tasks
2022-08-26 13:33:03 [ℹ]  all EKS cluster resources for "test123-cluster" have been created
2022-08-26 13:33:03 [ℹ]  nodegroup "test123-nodes" has 2 node(s)
2022-08-26 13:33:03 [ℹ]  node "ip-10-0-0-138.ap-northeast-1.compute.internal" is ready
2022-08-26 13:33:03 [ℹ]  node "ip-10-0-10-72.ap-northeast-1.compute.internal" is ready
2022-08-26 13:33:03 [ℹ]  waiting for at least 1 node(s) to become ready in "test123-nodes"
2022-08-26 13:33:03 [ℹ]  nodegroup "test123-nodes" has 2 node(s)
2022-08-26 13:33:03 [ℹ]  node "ip-10-0-0-138.ap-northeast-1.compute.internal" is ready
2022-08-26 13:33:03 [ℹ]  node "ip-10-0-10-72.ap-northeast-1.compute.internal" is ready
2022-08-26 13:33:05 [ℹ]  kubectl command should work with "/home/ssm-user/.kube/config", try 'kubectl get nodes'
2022-08-26 13:33:05 [ℹ]  EKS cluster "test123-cluster" in "ap-northeast-1" region is ready
$

期待通り、問題なく作成できました!

EKS クラスターは、eksctl 以外でも、AWS マネジメントコンソール や AWS CLI でも作成できますが、私は、EKS クラスターを作成する場合は、eksctl を使う派ですので、ようやく バージョン1.23 を eksctl で作成することができた、と感じています。

Amazon EKS でサポートされる バージョンが追加されたとき、どれくらいのタイムラグで、そのバージョンに対応した eksctl がリリースされるのかは、今後も注目していきたいと思います。

AWS SAM における Lambda 関数のエイリアスとバージョンの重みづけについて

以前から、AWS SAM を使用して AWS Lambda 関数のエイリアスとバージョンの重みづけを設定したいなーと思ってました。

ただ、結論から言うと、AWS SAM 仕様では重みづけの設定する方法は無く、AWS CloudFormation のプロパティとして設定する必要があることがわかりました。


エイリアスやバージョンの発行、という観点だと、AWS SAM では、AutoPublishAlias というプロパティを使用できます。

docs.aws.amazon.com

このプロパティの指定だけで、AWS Lambda 関数のエイリアスと、それに関連付けられた AWS Lambda 関数のバージョンが自動的に発行されます。

AutoPublishAlias を指定している場合、SAM でスタックが作成、更新される都度に新しいバージョンが発行され、エイリアスの重みづけは常に 100% に設定されます。

AutoPublishAlias を指定したうえで、さらに DeploymentPreference というプロパティを指定すると、 新バージョンへの段階的な移行が可能になり、例えば Canary や Linear などの手法で、時間の経過とともに新バージョンに振り分けるトラフィック量を自動的に変更したり、CloudWatch Alarm と連動させて、デプロイをロールバックすることもできます。 docs.aws.amazon.com

この AutoPublishAliasDeploymentPreference は、非常に強力かつ便利ではあるのですが、意外に単純なことができないことがわかりました。

それは、Canary や Linear などの手法ではなく、単に エイリアスと複数バージョンの重みづけを設定すること です。これができないのです。

例えば、次のような手順を AWS SAM AutoPublishAliasDeploymentPreference を指定して 行うことはできません。

  1. AWS Lambda関数の バージョン 1を発行する。エイリアスと関連付けて重みづけは 100% とする。
  2. AWS Lambda関数のコードを更新して、バージョン 2を発行する。エイリアスで、バージョン 1 は90%、バージョン 2は 10%の重みづけにする。
  3. AWS Lambda関数の バージョン 2の、エイリアスて重みづけを 100% とする。

DeploymentPreference で Canary や Linear を指定するのも便利ですが、その場合は、あくまであらかじめ用意されている、Canary10Percent15Minutes や Linear10PercentEvery10Minutes というタイプから選ぶしかありません。

トラフィックを移行する割合を手動で指定したい、自分でトラフィックを切り替えるタイミングを制御したいという場合は、AWS SAM 仕様のプロパティでは対応するものが無いのです。


では、どうすればいいかというと、AWS CloudFormation のプロパティで指定する、という方法があります。

まず、AWS Lambda 関数の バージョン 1を発行し、エイリアスと関連付けて重みづけは 100% とするための SAM テンプレートの例をみてみましょう。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
  Function:
    Timeout: 3
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function 
    Properties:
      FunctionName: HelloSAMAliasVersion
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
  HelloWorldFunctionVersion1:
      Type: AWS::Lambda::Version
      Properties:
        FunctionName: !Ref HelloWorldFunction
        Description: v1
  HelloWorldFunctionAlias:
      Type: AWS::Lambda::Alias
      Properties:
        FunctionName: !Ref HelloWorldFunction
        FunctionVersion: !GetAtt HelloWorldFunctionVersion1.Version
        Name: live

SAM テンプレートとして記載していますが、AWS Lambda 関数のエイリアスやバージョンを発行する指定は、Type: AWS::Lambda::VersionType: AWS::Lambda::Alias と、AWS CloudFormation のプロパティを指定しています。

このSAM テンプレートを使用して デプロイし、スタックを作成すると、エイリアス名 live で、最初のバージョンが重みづけ 100% で設定された状態になります。

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


次に、AWS Lambda 関数のコードを更新し、新しいバージョン 2として、エイリアス live に 10% の重みづけでトラフィックを振り分けたいとします。

その場合は、SAM テンプレートを次のように変更してデプロイします。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
  Function:
    Timeout: 3
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function 
    Properties:
      FunctionName: HelloSAMAliasVersion
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7

  HelloWorldFunctionVersion1:
      Type: AWS::Lambda::Version
      Properties:
        FunctionName: !Ref HelloWorldFunction
        Description: v1
  HelloWorldFunctionVersion2:
      Type: AWS::Lambda::Version
      Properties:
        FunctionName: !Ref HelloWorldFunction
        Description: v2
  HelloWorldFunctionAlias:
      Type: AWS::Lambda::Alias
      Properties:
        FunctionName: !Ref HelloWorldFunction
        FunctionVersion: !GetAtt HelloWorldFunctionVersion1.Version
        Name: live
        RoutingConfig:
          AdditionalVersionWeights:
            - FunctionVersion: !GetAtt HelloWorldFunctionVersion2.Version
              FunctionWeight: 0.1

デプロイが完了すると、バージョン 1が 90%、バージョン 2が 10% になったことを確認できます。

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


この後、バージョン 2 へ完全に切り替えたい場合は、このテンプレートの FunctionWeight: で指定する値を 1.0 変更してデプロイします。

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


これらの プロパティについては、次のドキュメントも参考にしてください。

docs.amazonaws.cn


AWS CloudFormation のプロパティを使うと、自分で重みづけや、切り替えタイミングを制御できるのはいいのですが、プロパティの記載量はどうしても増えます。

SAM であれば、次のように、AWS Lambda 関数 で AutoPublishAlias を指定しつつ、さらに API GatewayAPIとの統合もシンプルに指定できます。

 HelloWorldFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
      AutoPublishAlias: live
      Events:
        HelloWorld:
          Type: Api 
          Properties:
            Path: /hello
            Method: get

ただし、AWS CloudFormation のプロパティでエイリアスやバージョンを手動指定すると、このような書き方はできません。

なので、AutoPublishAlias で、自分でバージョンの重みづけができればいいのですが、それは現状ではサポートされていないようです。

(次の issue にも Feature request として挙げられていましたが、DeploymentPreferences を使いなさい、と close されてました。)

github.com

ちょっと残念ですね。

AWS SAM Accelerate と POSTMAN を使って AWS Lambda の Function URLs を手早く試す

AWS Lambda の Function URLs で、Auth タイプに AWS_IAM を指定した場合のアクセスを試そうと考えていたところ、AWS SAM Accelerate と POSTMAN を使えば、手早く試せるのではと思い立ったので、やってみました。

AWS Lambda の Function URLs については、次の AWS Blog の記事を参照してください。

aws.amazon.com

また、AWS SAM Accelerate については、次の AWS Blog の記事を参照してください。

aws.amazon.com

まず、SAM CLI が使用できる環境を用意します。

今回は SAM Accelerate を使うので、バージョン 1.34 以上が必要です。

SAM CLI のバージョンを確認します。

sam --version

1.34.1以上のバージョンでなければ、アップグレードします。

まず最新版をダウンロードします。

wget https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip
unzip aws-sam-cli-linux-x86_64.zip -d sam-installation

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

sudo ./sam-installation/install --update

再び SAM CLI のバージョンを確認します。

sam --version

次のように、1.34 以降のバージョンになったので、大丈夫ですね。

SAM CLI, version 1.53.0

では、SAM の初期化を行います。

今回は、Python の Lambda 関数を作成します。また、ここでは Cloud 9 を使っているため、Pythonのバージョンを敢えて3.7 にしています。

sam init --app-template hello-world --name sam-tutorial --package-type Zip --runtime python3.7

あっという間に、SAM のリソースが用意されました。

今回は、Function URLs でのアクセスを試したいだけなので、Lambda 関数のコードは編集せず、Hello World のレベルのままにしておきます。

編集が必要なのは、template.yaml です。これを次のように書き換えます。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.7
      FunctionUrlConfig:
        AuthType: AWS_IAM
Outputs:
  HelloWorldFunctionEndpoint:
      Description: "HelloWorldFunction Function URL Endpoint"
      Value:
        Fn::GetAtt: HelloWorldFunctionUrl.FunctionUrl

Lambda 関数のプロパティで FunctionUrlConfig を指定しています。

また、Outputs セクションで、Function URLs のエンドポイントを出力するように指定しています。

このとき、属性名として、 <Lambda関数のリソース名>に Url.FunctionUrl をつけます。

これで、SAM のテンプレートは完成です。ではリソースを作成します。

cd sam-app
sam sync  --stack-name sam-app

SAM Accelerate の sync を使っているので、SAM の build や package、deploy を行う必要がありません。

次のように確認を求められたら、Y で応答します。

Enter Y to proceed with the command, or enter N to cancel:
 [Y/n]: Y

スタックが作成され、Output セクションで指定したエンドポイントが表示されます。

--------------------
Key                 HelloWorldFunctionEndpoint                                                  
Description         HelloWorldFunction Function URL Endpoint                                    
Value               https://nbuyqvkie2c64v7mjdt7sni3qy0lbgym.lambda-url.ap-northeast-1.on.aws/

Function URLs の Auth タイプが NONE であれば、単純にこのエンドポイントにアクセスするだけでよいのですが、今回は Auth タイプは AWS_IAM なので、アクセスキー ID やシークレットアクセスキーをもとに作成した署名 (AWS の署名v4) が必要です。

docs.aws.amazon.com

AWS の署名v4 を作成する方法はいくつかありますが、今回は POSTMAN を使ってみます。

www.postman.com

POSTMAN は、RESTful API を発行するときに、AWS の 署名v4 を自動生成する機能を提供しています。

次の図のように、指定して下さい。

Service Namelambda と入力するのを忘れないようにしましょう。

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

POSTMAN で Send ボタンを選択して、 Function URL のエンドポイントにリクエストを発行すると、次のようなレスポンスが返ってきます。

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

正常にアクセスできましたね。

試しに、正しくないアクセスキー ID を指定してみると、ステータスコード 403で 次のようなメッセージが返されます。

{
    "message": "The security token included in the request is invalid."
}

では最後に、作成した Lambda 関数を削除します。

これも、 SAM CLI から削除できます。確認を求められたら、 y で応答します。

sam delete --stack-name sam-app
        Are you sure you want to delete the stack sam-app in the region ap-northeast-1 ? [y/N]: y
        Do you want to delete the template file a19a97a7b854a2609df1ae91982ee843.template in S3? [y/N]: y

以上です!

Lambda 関数の Function URLs を試すだけであれば、マネジメントコンソールから手作業で作成してもいいのですが、デモ環境として再利用することも考慮し、今回は SAM を使ってみました。

ただし、SAM においても、package や deploy で多くのパラメータを指定するような操作はしたくなかったので、SAM Accelerate を使いました。

また、AWS の 署名v4 の生成には、POSTMAN を活用しました。

うまくツールを活用することで、あまり時間をかけず効率的に動作検証できますね。(横着ともいわれそうですが...)

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ロールを作成しておく方が良さそうですね!

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