【CloudFormation】S3のバケットの中身をCloudFormationで作成する

はじめに

CloudFormationでシステムのようなものを組んでいるうちに、本当で適当で良いから入力フォームみたいなものが必要になってきた。 こうしたときに便利なのがS3の静的ホスティング機能だろう。

qiita.com

ただ、CloudFormationは、S3の中身までは作成できないという欠点があった。

強力なS3Objectsマクロ

しかし、このS3Objectsを利用すれば、CloudFormationでS3内のファイルを作成することができる github.com

デプロイ

使い方は非常に簡単で、上記のサイトの手順に従ってマクロをデプロイする。適当なアーティファクトバケットを作成する。 次に、git cloneかファイルのダウンロードし*1、以下のコマンドを実行する。

aws cloudformation package \
    --template-file macro.template \
    --s3-bucket <your bucket name> \
    --output-template-file packaged.template

aws cloudformation deploy \
    --stack-name s3objects-macro \
    --template-file packaged.template \
    --capabilities CAPABILITY_IAM

これで、デプロイしたアカウント内ならばこのマクロを利用することができるようになった。 なお、私はコマンドをうつのが面倒だったため、CodeStarを利用した。

テスト

テスト用のコードも用意されているため、実行して有効性を確認する。

aws cloudformation deploy \
    --stack-name s3objects-macro-example \
    --template-file example.template \
    --capabilities CAPABILITY_IAM

使い方

CloudFormationが利用するIAMロールにS3Fullをアタッチし*2、CloudFormationテンプレートの頭に以下のブロックを置くことで、マクロが有効化される。

2019/12/21追記:マクロを動作させるためには、CloudFormationが利用するRoleにAWSLambdaExecuteポリシーをアタッチする必要がある。

Transform: S3Objects

サーバーレス変換など、他のマクロを合わせて利用する場合は、以下のようにリスト表記する。

Transform:
- S3Objects
- AWS::Serverless-2016-10-31

公式ドキュメントによると、以下の使い方があるようだ。 なお、いずれの項目でも

Target:
  Bucket: 
  Key:

は必須項目で、アップロード先のバケットとキー名を指定している。

テキストの内容をベタ書き

Transform: S3Objects
Resources:
  Bucket:
    Type: AWS::S3::Bucket

  Object:
    Type: AWS::S3::Object
    Properties:
      Target:
        Bucket: !Ref Bucket
        Key: README.md
        ContentType: text/plain
      Body: Hello, world!

Body以下に書かれた内容がそのままファイルの内部に反映される。 非常に短いコードならば問題にならないが、長文になると修正も非常に面倒になるほか、コードを変更した際にテストが行えない点に注意すること。

エンコードされた文字列を記載する

SinglePixel:
  Type: AWS::S3::Object
  Properties:
    Target:
      Bucket: !Ref TargetBucket
      Key: 1pixel.gif
    Base64Body: R0lGODdhAQABAIABAP///0qIbCwAAAAAAQABAAACAkQBADs=

ここでは、Base64の文字列をテンプレート中に記載する。

S3にアップロードされているファイルを指定する。

CopiedObject:
  Type: AWS::S3::Object
  Properties:
    Source:
      Bucket: !Ref SourceBucket
      Key: index.html
    Target:
      Bucket: !Ref TargetBucket
      Key: index.html
      ACL: public-read

予め他のS3にアップロードしておいたファイルを指定してコピーする。 aws-cliaws s3 cp ~~~をcloudFormationで実行していると考えて良いだろう。

結論

S3Objects変換を利用することで、S3バケットの中身をCloudFormationで作成することができるようになった。 工夫すれば、静的サイトをCloudFormationで作成することができるようになると考えられる。 ただし、アップロードする方法がテキストのベタ書き、または他のs3オブジェクトへのリンクのみという弱点もある。*3

*1:いずれも、他のマクロテンプレートなどが大量に入っているため時間がかかる点に注意

*2:何もフルアクセスでなくともよいが、とりあえず

*3:これはCodeBuildと組み合わせればゴリ押しで解決できる気がするのであまり気にしていない。

【AWS Athena】str型Timestampをクエリする

課題

以下のようなデータがあったとする。

{
  "timestamp": "2019-02-01 00:00:00",
  "data": 100
}
{
  "timestamp": "2019-03-01 00:00:00",
  "data": 100
}
{
  "timestamp": "2019-04-01 00:00:00",
  "data": 100
}

このデータを年月ごとに集計するクエリを作成したい。。 正直に以下のSQLでクエリすると次のようになる

SELECT year(timestamp) as year,
            month(timestamp) as month,
            sum(data)
FROM <your_year(timestamp),month(timestamp)database>.<your_table> 
GROUP BY  year(timestamp),month(timestamp);
SYNTAX_ERROR: line 1:8: Unexpected parameters (varchar) for function year. Expected: year(timestamp) , year(timestamp with time zone) , year(date) , year(interval year to month) 

原因と対策

これは、str型のタイムスタンプがタイムスタンプとして認識されていない事が原因である。 したがって以下のように記述する必要がある。

SELECT year(date_parse(timestamp, \'%Y/%m/%d %H:%i:%s\')),
            month(date_parse(timestamp, \'%Y/%m/%d %H:%i:%s\')),
            sum(data)
FROM <your_year(timestamp),month(timestamp)database>.<your_table> 
GROUP BY  year(date_parse(timestamp, \'%Y/%m/%d %H:%i:%s\')),month(date_parse(timestamp, \'%Y/%m/%d %H:%i:%s\'));

参考

AWS AthenaはPrestoベースであることがわかったため、以下のサイトを参考にした * Prestoでの日付の扱い方 - ★データ解析備忘録★ * prestoの気持ち 時間関係(Date and Time Functions and Operators) - Qiita

【AWS Athena】Lambdaからクエリを実行した際のIAM由来のエラーと対策

Athenaをテストした際に遭遇したエラーを以下にまとめる。別のエラーに遭遇した場合は後日追加していく。

Unable to Verify/Create Output Bucket

Unable to Verify/Create Output Bucket

S3へのアクセス権の不足が原因。以下のポリシーがアタッチされているか確認する。

  • s3:ListBucket
  • s3:ListBucketMultipartUploads
  • s3:GetObject
  • s3:GetBucketLocation
  • s3:ListMultipartUploadParts
  • s3:AbortMultipartUpload
  • s3:PutObject

Access Denied

Access Denied (Service: Amazon S3; Status Code: 403; Error Code: AccessDenied; 

S3へのアクセス権の不足。 以下のポリシーがアタッチされているか確認する。

  • s3:PutObject
  • s3:ListObject

Insufficient permissions to execute the query.

Insufficient permissions to execute the query. User: ROLE_NAME is not authorized to perform: glue:GetTable on resource: arn:aws:glue:ap-northeast-1:UserId:catalog

エラーコードの通り、glue:GetTableのアクセス権が不足している。

HIVE_CANNOT_OPEN_SPLIT

Your query has the following errors:HIVE_CANNOT_OPEN_SPLIT: Error opening Hive split s3://BUCKET_NAME/part-00000-feda4fa5-d862-48ba-88ad-3a468e234a27-c000.snappy.parquet (offset=0, length=7791): com.amazonaws.services.s3.model.AmazonS3Exception: Forbidden (Service: Amazon S3; Status Code:403; Error Code: 403 Forbidden; Request ID: 028A0DA47697C2D7;(以下略)

この記事の通り、glue:GetTablesのアクセス権が不足している。

特に、自分でポリシーを作成した際に忘れやすい。

【CloudFormation】Error: The policy failed legacy parsing

やりたい事

以下のページを参考に、別アカウントのCodePipelineに、CodeCommitの変更を配信したい。また、これをCloudFormationを使ってできる限り省力化したい。

dev.classmethod.jp

発生したエラー

開発アカウント側で、IAM Policyを作成する際にエラーが発生した。以下がテンプレートである。

AllowTargetAccessPolicy:
    Type: AWS::IAM::Policy
    Properties: 
      Roles: 
        - !Ref AllowTargetAccessRole
      PolicyName: 
        !Join
          - ""
          - - !Ref AWS::Region
            - !Ref AWS::AccountId
            - To
            - !Ref TargetAccountID
            - CodeCommitAccessPolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement: 
          - 
            Effect: Allow
            Action: 
              - s3:PutObject
              - s3:PutObjectAcl
            
            Resource: !Sub arn:aws:s3:::artifact-bucket-for-${TargetAccountID}-from-${AWS::AccountId}
      
          - 
            Effect: Allow
            Action: 
              - kms:DescribeKey
              - "kms:GenerateDataKey*"
              - kms:Encrypt
              - "kms:ReEncrypt*"
              - kms:Decrypt
            
            Resource: !Sub "arn:aws:kms:${AWS::Region}:${TargetAccountID}:key/${KMS_ID}

          - 
            Effect: Allow
            Action: 
              - codecommit:GetBranch
              - codecommit:GetCommit
              - codecommit:UploadArchive
              - codecommit:GetUploadArchiveStatus
              - codecommit:CancelUploadArchive
            
            Resource:
              !Sub arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${RepositoryName}

すると、以下のエラーが発生した。

The policy failed legacy parsing (Service: AmazonIdentityManagement; Status Code: 400; Error Code: MalformedPolicyDocument;)

リソースをコメントアウトして、問題がKMSに関するポリシードキュメントブロックにあることがわかった。

解決方法

なぜか、!Sub の代わりに!Joinを使用することでエラーが回避できた。 (参考) tycoh.hatenablog.com

AllowTargetAccessPolicy:
    Type: AWS::IAM::Policy
    Properties: 
      Roles: 
        - !Ref AllowTargetAccessRole
      PolicyName: 
        !Join
          - ""
          - - !Ref AWS::Region
            - !Ref AWS::AccountId
            - To
            - !Ref TargetAccountID
            - CodeCommitAccessPolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement: 
          - 
            Effect: Allow
            Action: 
              - s3:PutObject
              - s3:PutObjectAcl
            
            Resource: !Sub arn:aws:s3:::artifact-bucket-for-${TargetAccountID}-from-${AWS::AccountId}
      
          - 
            Effect: Allow
            Action: 
              - kms:DescribeKey
              - "kms:GenerateDataKey*"
              - kms:Encrypt
              - "kms:ReEncrypt*"
              - kms:Decrypt
            
            Resource: |
              !Join
                - ""
                - - arn:aws:kms
                  - !Ref AWS::Region
                  - ":"
                  - !Ref TargetAccountID
                  - "key/"
                  - !Ref KMSArn

          - 
            Effect: Allow
            Action: 
              - codecommit:GetBranch
              - codecommit:GetCommit
              - codecommit:UploadArchive
              - codecommit:GetUploadArchiveStatus
              - codecommit:CancelUploadArchive
            
            Resource:
              !Sub arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${RepositoryName}

原因は後日AWS Forumに質問してみることにする。

【Error】CloudFormation:: Parameter is non alphaNumeric

現象

以下のCloudFormationテンプレートのパラメーターブロックでParameter is non alphaNumericというエラーが発生した。

---
Parameter:
  MY_MESSAGE: This is test
---

原因

パラメーターブロックでは、記号を使うことができない。そのため、アンダーバーを使うことができない。 そのため、キャメルケースでパラメーターを設定する。

Parameter:
  myMessage: This is test
  # または、MyMessage

参考

CloudFormation Parameter Template Error : Parameter is non alphanumeric

【CloudFormation】!Subと!Refの使い分け

CloudFormationのテンプレートでよく使う組み込み関数に!Refと!Subがあります。 これらの違いを整理します。

  • 基本的な使い方
    • 基本的な作用
    • 使い方
    • 誤った使い方
    • !Subの問題点
  • 文字列との結合
    • !Subの場合
    • !Refの場合
  • 結論
続きを読む

CloudFormationメモ SQSとLambdaの連携

Error

Queue visibility timeout: 120 seconds is less than Function timeout: 900 seconds

エラーが発生したときのコード

SensorDataQueue:
    Type: AWS::SQS::Queue
    Properties:
      VisibilityTimeout: 120 #ここがLambdaのタイムアウト時間より短い
calcData:
    Type: AWS::Serverless::Function      
    Properties:
      #中略
      Events:
        SQS:
          Type: SQS
          Timeout: 900
          Properties:
            Queue: !GetAtt SensorDataQueue.Arn
            BatchSize: 10
            Enabled: True

対策

SQSのVisiblityTimeOutを、LambdaのTimeoutで設定した値以上にする