BioErrorLog Tech Blog

試行錯誤の記録

Private SubnetのCloud9からAWS APIが叩けない問題 | AWS managed temporary credentials

AWS managed temporary credentialsを使うと、Private Subnetに構築したCloud9からAWS APIが叩けない、ということの備忘録です。

はじめに

Private Subnetに構築したCloud9からAWS APIが叩けない、という事象に遭遇しました。

この現象の裏にこれまで知らなかった仕様があったので、備忘録にまとめます。

Private SubnetのCloud9からAWS APIが叩けない問題

起きた現象

まずは起きたことを整理します。

Private SubnetにCloud9を構築しました。

※ Private SubnetにCloud9を構築するやり方は別途記事参照:

www.bioerrorlog.work

そしてこのCloud9から適当なAWS APIを叩くと、権限エラーInvalidClientTokenIdが発生します。

$ aws sts get-caller-identity
An error occurred (InvalidClientTokenId) when calling the GetCallerIdentity operation: The security token included in the request is invalid

この時の条件は、

  • Cloud9の権限はAWS managed temporary credentialsで付与 (デフォルト)
  • 操作ユーザーの権限は、AWSへのフルアクセス可能なAdmin権限

です。

AWS managed temporary credentialsによってAdmin権限がCloud9に渡るので、上記のようなAPI callは成功する想定でした。

※ AWS managed temporary credentialsによって実行できるActionには元々制限がありますが、今回取り上げているのはAWS managed temporary credentialsでも実行できるはずのActionが実行できない、というケースです。

なぜAWS APIが叩けないのか

「Private Subnetに構築されたCloud9からは、AWS managed temporary credentialsによってAWSサービスにアクセスできないから」です。

これは現在ドキュメントに明記された仕様です。

Currently, if your environment’s EC2 instance is launched into a private subnet, you can't use AWS managed temporary credentials to allow the EC2 environment to access an AWS service on behalf of an AWS entity (for example, an IAM user).

Ref. Identity and access management in AWS Cloud9 - AWS Cloud9

訳:「現在、EC2インスタンスがprivate subnetに起動されている場合、AWS managed temporary credentialsを使ってAWSエンティティ(例: IAMユーザー)の代理としてAWSサービスにアクセスすることはできません。」

ということで、現時点ではprivate subnetでAWS managed temporary credentialsを使うことはできません。

対処法

AWS managed temporary credentialsではなくIAM Roleを使ってCloud9に権限を付与することで、private subnetのCloud9からAWS API呼び出しが可能です。

手順:

  1. Cloud9のEC2にIAM Roleをアタッチする
  2. Cloud9のAWS managed temporary credentialsを無効化する

AWS managed temporary credentialsを無効化は、Cloud9内の設定画面から可能です:

AWS managed temporary credentialsの無効化方法 | 画像はAWS workshopより引用

おわりに

以上、Private SubnetのCloud9からAWS APIが叩けない問題について整理しました。

知らない現象だったので少し戸惑いましたが、ドキュメントに明記された仕様なので仕方ないですね。

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

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Identity and access management in AWS Cloud9 - AWS Cloud9

VPC settings for AWS Cloud9 Development Environments - AWS Cloud9

Calling AWS services from an environment in AWS Cloud9 - AWS Cloud9

Identity and access management in AWS Cloud9 - AWS Cloud9

sts get-caller-identity doesn't work on Cloud9 instance deployed in Private Subnet | AWS re:Post

d. Temporary credentials on Cloud9 :: AWS HPC Workshops

Private SubnetにCloud9を構築する | AWS

AWS Cloud9をPrivate Subnetで作成する方法を整理します。

はじめに

Cloud9では、SSM接続方式を選択することでPrivate Subnetにインスタンスを建てることができますが、その際にも満たすべき一定のネットワーク要件があります。

毎回忘れて調べ直しているので、今回はそのやり方を整理します。

前提: Private SubnetでのCloud9の仕組み

前提として、Private Subnetに構築するCloud9の仕組みをおさらいしておきましょう。

Cloud9にはSSHとSSM(Systems Manager)2つのネットワーク接続方式が提供されており、Private SubnetにCloud9を構築するにはSSM接続方式の方を選択する必要があります。

Cloud9には2つのネットワーク接続方式がある

実際にSSM接続方式のCloud9に接続するときの通信は、

  • ユーザーがコンソール経由でSystems Managerにアクセスする
  • Cloud9インスタンスがSystems Managerにアクセスする

の2つの経路が通っている必要があります。 前者は普通にマネジメントコンソールにアクセスできていれば問題ないので、基本は後者の接続を意識することになります。

SSM接続方式のCloud9を利用する際の通信経路の例 | 画像はAWSブログより

では、通信要件の条件に応じて実際にPrivate SubnetにCloud9を構築していきます。

Private SubnetでCloud9を構築する

  • Internet Gateway / NATありの場合
    (NAT Gateway経由でInternet GatewayにアクセスできるPrivate Subnet)
  • Internet Gateway / NATなしの閉域Private Subnetの場合
    (NAT Gatewayもなく、Internet Gatewayに到達する経路がないPrivate Subnet)

のそれぞれで、Cloud9を構築していきます。

Internet Gateway / NATありの場合

NAT Gatewayがある場合は、何か特殊な設定をする必要はありません。

Cloud9を作成するPrivate Subnetから、NAT Gatewayを経由してInternet Gatewayへ出れる通信経路が開いていれば、Cloud9を作成することができます。

  • Private SubnetのルートテーブルでNAT Gatewayへの経路が通っているか
  • Public SubnetのルートテーブルでInternet Gatewayへの経路が通っているか

今回の検証で使ったNetworkリソースのTerraformコードをそのままこちらに配置していますので、ご参考までにどうぞ: github.com

Internet Gateway / NATなしの閉域Private Subnetの場合

Internet Gateway / NAT Gatewayのないno-egressなPrivate Subnetの場合は、最低限下記3つのVPC endpointを作成しておく必要があります。

  • com.amazonaws.[region].ec2messages
  • com.amazonaws.[region].ssm
  • com.amazonaws.[region].ssmmessages

※ 上記[region]部分はregion名に置き換えてください。

こちらも、上記構成のNetworkリソースを構築するTerraformコードをそのままGitHubに配置していますので、ご参考まで:

github.com

おわりに

以上、Private SubnetにCloud9を構築する方法を整理しました。

Cloud9をサクッと使いたい場面もしばしばあります。

そんなときの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Isolating network access to your AWS Cloud9 environments | AWS Security Blog

Manage private EC2 instances without internet access using Systems Manager | AWS re:Post

Accessing no-ingress EC2 instances with AWS Systems Manager - AWS Cloud9

GitHub - bioerrorlog/terraform-examples: My terraform example projects.

TerraformでAWSリージョンを取得する

TerraformでAWSリージョンを取得する方法の備忘録です。

はじめに

TerraformでAWSリソースを書いていると、ARNの指定などでアカウントidやリージョン名を取得したくなるときがあります。

以前、TerraformでAWSアカウントidを取得する方法を書きましたが、リージョンを取得する方法はいつも都度ググっていたので、備忘録を残します。

TerraformでAWSリージョンを取得する

やり方

aws_regionのdata sourceを使って、AWSリージョン名を取得できます。

data "aws_region" "current" {}

このようにdata sourceを定義したら、

data.aws_region.current.name

の形式でリージョン名を取得できます。

# 使用例
source_arn = "arn:aws:events:${data.aws_region.current.name}:123456789012:rule/RunDaily"


必要に応じてリージョン名をlocal変数に格納しておくのも、可読性と変更容易性が上がるのでおすすめです。

locals {
  region = data.aws_region.current.name
}

おまけ: Terraformソースコードを読む

せっかくなので、このaws_region data sourceの実装をソースコードから読んでみます。

まず場所としては、下記のコードがaws_region data sourceを実装してそうです。

github.com

この中のRead関数で、aws_region data sourceの実態が実装されています。

プロバイダの現在のリージョンを取得する部分のコードを抜粋します。

   // Default to provider current region if no other filters matched
    if region == nil {
        matchingRegion, err := FindRegionByName(d.Meta().Region)

        if err != nil {
            response.Diagnostics.AddError("finding Region by name", err.Error())

            return
        }

        region = matchingRegion
    }

aws_region data sourceの引数に何も指定がない場合、メタデータからproviderの現在のリージョンが取得されているのがわかります。

おわりに

以上、TerraformでAWSリージョンを取得する方法の備忘録をまとめて、ついでにソースコードを読んでみました。

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

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Terraform Registry

terraform-provider-aws/internal/service/meta at be9d5ac322736b8c599459a755cb3d63be051e6c · hashicorp/terraform-provider-aws · GitHub

terraform-provider-aws/internal/service/meta/region_data_source.go at be9d5ac322736b8c599459a755cb3d63be051e6c · hashicorp/terraform-provider-aws · GitHub

terraform-provider-aws/internal/service/meta/region_data_source_test.go at be9d5ac322736b8c599459a755cb3d63be051e6c · hashicorp/terraform-provider-aws · GitHub

Retrieve instance metadata - Amazon Elastic Compute Cloud

Terraform backend(S3/DynamoDB)をCloudFormationでデプロイする

Terraformのstateを格納するbackend(S3/DynamoDB)を構築する、CloudFormationテンプレートの備忘録です。

はじめに

Terraformのstateを格納するbackend自身をどのように構築するか、はちょっとした考慮ポイントですね。

Terraform自身で実装すると少し厄介(そのTerraformのstateはどう管理するの?問題が発生する)なので、何かしらTerraform以外の手段で構築するのも手です。

AWSであれば、CloudFormationで構築するのが選択肢の一つになるでしょう。

今回は、Terraform backendを構築するCloudFormationテンプレートを備忘録にメモします。

Terraform backendを構築するCloudFormationテンプレート

実装例こちらです。 (GitHubに置いたコードはこちら)

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Create S3 and DynamoDB resources for Terraform backend'
Parameters:
  ResourceNamePrefix:
    Type: String
    Description: 'The prefix for the resource names (S3 bucket and DynamoDB table)'
    Default: 'demo'

Resources:
  TerraformStateBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Sub '${ResourceNamePrefix}-${AWS::AccountId}-terraform-state'
      VersioningConfiguration:
        Status: 'Enabled'
      LifecycleConfiguration:
        Rules:
        - Id: ExpireNoncurrentVersions
          NoncurrentVersionExpirationInDays: 90
          Status: Enabled
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: 'AES256'
      AccessControl: 'Private'
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        IgnorePublicAcls: true
        BlockPublicPolicy: true
        RestrictPublicBuckets: true

  DynamoDBLockTable:
    Type: 'AWS::DynamoDB::Table'
    Properties:
      TableName: !Sub '${ResourceNamePrefix}-terraform-lock'
      AttributeDefinitions:
        - AttributeName: 'LockID'
          AttributeType: 'S'
      KeySchema:
        - AttributeName: 'LockID'
          KeyType: 'HASH'
      BillingMode: 'PAY_PER_REQUEST'

Outputs:
  TerraformStateBucketName:
    Description: 'The name of the S3 bucket for storing Terraform state files'
    Value: !Ref TerraformStateBucket

  DynamoDBLockTableName:
    Description: 'The name of the DynamoDB table for Terraform state locking'
    Value: !Ref DynamoDBLockTable

なるべくシンプルな形で構成しました。

S3:

  • デフォルト暗号化 (SSE-S3)
  • バージョニング有効
  • ブロックパブリックアクセス有効

DynamoDB:

  • 必要なスキーマ構成
  • PAY_PER_REQUESTでの課金体系


ここを出発点にして、要件に応じて機能を加えていくのが良いでしょう。

例:

  • S3をCMKで暗号化
  • バケットポリシーによるアクセス制御/クロスアカウントアクセス許可
  • etc...

おわりに

Terraform backendを構築するCloudFormationテンプレートの実装例をメモしました。

Terraform Cloudとかを利用すればこの辺りも自動で用意できるのかも知れませんが、個人的にはまだOSSとしてしか利用したことがないのでbackendの用意が必要です。

以上、どなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

GitHub - thoughtbot/cloudformation-terraform-state-backend: Cloudformation template to create Terraform state S3 backend

GitHub - tiborhercz/tf-state-backend-s3-cloudformation: CloudFormation template to provision a state backend for Terraform

GitHub - bioerrorlog/terraform-examples: My terraform example projects.

Parquetファイルのダミーデータを生成する | Python

Pythonでparquetファイルのダミーデータを生成する方法の備忘録です。

はじめに

ちょっとした検証のために、ダミーデータのparquetファイルを用意する機会がありました。

Pythonスクリプトを書いたので、備忘録にメモします。

※ソースコードはこちらに置いています: github.com

Parquetファイルのダミーデータを生成する

Parquetファイルのダミーデータを生成するPythonスクリプト例です。 圧縮形式はここではgzipとしています。

from typing import Dict, List
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
import boto3


def create_dummy_data(rows: int = 1000) -> pd.DataFrame:
    data: Dict[str, List] = {
        'id': range(1, rows + 1),
        'name': [f'name_{i}' for i in range(1, rows + 1)],
        'value': [i * 100 for i in range(1, rows + 1)]
    }
    return pd.DataFrame(data)


def save_gz_parquet(df: pd.DataFrame, file_path: str) -> None:
    table: pa.Table = pa.Table.from_pandas(df)
    pq.write_table(table, file_path, compression='gzip')


def main() -> None:
    dummy_data: pd.DataFrame = create_dummy_data()

    file_path: str = 'dummy_data.parquet.gz'
    save_gz_parquet(dummy_data, file_path)


if __name__ == '__main__':
    main()

pyarrow.parquetを使うことで、pandas DataFrameから簡単にparquetファイルを書き出すことができます。

上記のコード例では、create_dummy_data関数でダミーのpandas DataFrameを作成し、save_gz_parquet関数でそのデータをparquetファイルで保存しています。

補足:S3に配置する

作成したparquetファイルをそのままS3に配置するには、下記のような形になります。

from typing import Dict, List
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
import boto3


def create_dummy_data(rows: int = 1000) -> pd.DataFrame:
    data: Dict[str, List] = {
        'id': range(1, rows + 1),
        'name': [f'name_{i}' for i in range(1, rows + 1)],
        'value': [i * 100 for i in range(1, rows + 1)]
    }
    return pd.DataFrame(data)


def save_gz_parquet(df: pd.DataFrame, file_path: str) -> None:
    table: pa.Table = pa.Table.from_pandas(df)
    pq.write_table(table, file_path, compression='gzip')


def upload_to_s3(bucket: str, s3_key: str, file_path: str) -> None:
    s3 = boto3.client('s3')
    s3.upload_file(file_path, bucket, s3_key)


def main() -> None:
    dummy_data: pd.DataFrame = create_dummy_data()

    file_path: str = 'dummy_data.parquet.gz'
    save_gz_parquet(dummy_data, file_path)

    bucket_name: str = 'your-bucket-name'
    # ".parquet.gz" doesn't work in S3 select.
    s3_key: str = 'path/to/dummy_data.gz.parquet'
    upload_to_s3(bucket_name, s3_key, file_path)


if __name__ == '__main__':
    main()

boto3のupload_fileでシンプルにファイルをS3にアップロードしています。

ちなみに、ファイルの拡張子を.parquet.gzとしてしまうと、S3 Selectがうまく機能しないので注意が必要です。 (ファイル全体をgzipされたものとしてS3に判定されてしまい、S3 Selectできない)

おわりに

以上、Pythonでparquetファイルのダミーデータを生成する方法の簡単な備忘録でした。

拡張子を.parquet.gzにするとS3 Selectdできない、というのは盲点でした。

参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

参考

raspberry-pi-examples/put_parquet_gz_to_s3 at main · bioerrorlog/raspberry-pi-examples · GitHub

GitHubのSecret scanningを有効化してアラート動作を実際に確認してみる

GitHubのSecret scanningを有効化し、実際にsecretsをpushしてみてアラート動作を確認します。

はじめに

先日、GitHubのSecret scanning機能が、無料で全てのパブリックレポジトリにて利用できるようになりました。

github.blog

今回はこのSecret scanningを有効化し、実際にsecretsを流出させてみることで動作を確認してみたいと思います。

GitHubのSecret scanningアラートの動作を確認する

GitHubのSecret scanningを有効化する

Secret scanningを有効化方法は、GitHub公式のアナウンスBlogにて丁寧に示されています:

Enable Secret Scanning

  1. レポジトリ上部タブから"Settings"を選択
  2. 左タブから"Code security and analysis"を選択
  3. "Secret scanning"にて"Enable"を選択

これだけで簡単に有効化することができます。

Secretキーをレポジトリにpushしてみる

では、ダミーのパブリックレポジトリを作成して、AWS IAMのアクセスキーとシークレットキー(無効化済み)をpushしてみます。

AWS secretsをpush

すると、数十秒後にはメールでアラートが届きました。

GitHubからのアラートメール

どのファイルの何行目、どのコミットで何のsecretsが流出しているのかまでちゃんと記載されています。 便利ですね。

おわりに

以上、GitHubのSecret scanningを有効化して実際にsecretsを流出させてみました。

数十秒後にはメールが届くというスピード感は流石ですね。

ぜひ活用していきたいところです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Secret scanning alerts are now available (and free) for all public repositories | The GitHub Blog

すべてのパブリックリポジトリに対してSecret scanningアラートの一般提供(GA)を開始 - GitHubブログ

リソース名に"リソース種名"を含めるべきか | AWS命名規則

含めるべきではない、と感じているので考えを整理します。

はじめに

AWSのリソース命名規則として、リソース名にリソース種名を含めることが多くあるように感じます。

例:

{任意文字列}-vpc # 末尾にリソース種名を置くパターン
iam-role-{任意文字列} # 冒頭にリソース種名を置くパターン

私もそのような命名規則のもと長らくシステムを構築運用していたのですが、この方法はデメリットが多いように感じています。

今回はその考えを整理します。

リソース名に"リソース種名"を含めるべきか

デメリット

リソース名に"リソース種名"を含める場合のデメリット

リソース名実装ロジックの複雑性が増す

リソース名にリソース種名を含める、とする場合、その命名規則が守られていることを保証する仕組みを追加する必要が出てきます。 この実装ロジックの追加は、チームにとって無視できない負担になり得ます。

例えばTerraformなど何かしらのIaCでリソースを書いている場合、prefix/suffixのパターンを強制する変数を用意するなどして、リソース命名が規則に沿うことを自動的に保証しているとします。 仮にprefixとして{sysname}-{env}-を挿入する、と定めたとしましょう ()。 ここにリソース種名を含むようにすると、prefixは{resource_type}-{sysname}-{env}-みたいな感じになります。

この{resource_type}を命名ルールに挿入する実装ロジックの増加は一見すると大したことありませんが、一定規模以上のシステムを長らく運用していると、チームにとってなかなかの負担になります。
(それぞれのリソース種に対応する{resource_type}をどう定義するか、module間でどう共通化するか...etc.)

リソース種名を含めるとする命名規則がそれ自身価値を生まないのであれば、これらの労力も実質的に価値を生みません。


逆に、命名規則を自動保証する仕組みを用意していない場合は、単純に人間の労力による確認が増えるので、これこそチームにとっての負担は増大します。


prefixに{sysname}-{env}-を含める、というのも個人的には不要であると考えています。 (これらはリソース名ではなくTagとして付与する方が使い勝手が良い)

リソース名の文字数上限を圧迫する

AWSのリソース名にはそれぞれ文字数上限があります。 リソースによってはこの文字数上限が結構低く設定されているものもあり、リソース種名に費やす文字数が惜しくなることがあります。

命名規則が成熟してくると自動で挿入される文字数も増えてくるので、ものによっては残された文字数が少ない、という状況に陥ります。 私も、もう少し文字数に余裕があればより自明で分かりやすいリソース名が書けるのに...という経験があります。

リソース種名を命名含める場合は単純にそれだけ文字数を消費し、リソース名の制約が増えます。

リソース種名の略語の決定に時間と労力を消費する

"リソース種名"は、ありのまま定義すれば結構な長さになるものも少なくありません。 上記のようにリソース名には文字数上限があるので、リソース種名の略語を使おう、という機運になるのは自然な圧力です。

この"略語を決める"という営みは、結構な時間と労力を使います。

もちろん不適切な略語になっては意味が伝わりません。 しかし、プルリクがその略語決定の議論を残して長らくmergeされない、というのはチームの生産性に影響します。

メリット

リソース名に"リソース種名"を含める場合のメリット

リソース種をリソース名から判断できる

これはそのまま、リソース種をリソース名から判断できるというのがメリットになるでしょう。


しかし、これはあまり意味がないと感じています。 リソース種をリソース名から判断したい、というケースがほぼ見当たらないからです。

例えば、マネジメントコンソール上でリソースを確認する際は、いま何のリソースを見ているのかはリソース名を確認するまでもなく自明です。

IaCのコード上でも、リソース名以前にリソース種定義があるので、そこからリソース種を判断できます。 (むしろリソース名にリソース種を含むのは、リソース種定義部分とのコード上の重複が起こっている、とも言えます)

ログやリソースtagの集計にクエリを投げる場合も、リソース名から正規表現でリソース種を取得する、みたいな方法を取ることは少ないでしょう。

アーキテクチャ図やドキュメント上ではどうでしょうか。 リソース名にリソース種が含まれていたとしても、読者にとっての情報量が増えることは少ないと私は思います。

まとめ

以上を踏まえ、

  • リソース名にリソース種名を含める場合のデメリットはメリットを上回る
  • -> リソース名にリソース種名を含めるべきではない

と考えています。

おわりに

リソース名にリソース種名を含めるべきか、について考えを整理してみました。

次にリソースを構築する際にまた考慮してみようと思います。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Terraform Development Guideline. Including Terraform recommended… | by Xin Cheng | Dev Genius

Naming conventions - Terraform Best Practices

SageMakerでlocal training jobが実行できない時の対処法 | Unable to locate credentials

Unable to locate credentials

のエラーで、SageMaker instanceのlocal training jobが実行できないときの対処法をまとめます。

はじめに

SageMakerでは、SageMaker Python SDKを利用して簡単にtraining job/学習ジョブを実行することができます。

SageMaker SDKのtraining jobには、

  • オンデマンドに別インスタンスが立ち上がってジョブを実行する通常ジョブ
  • 作業中のNotebookインスタンス上でジョブを実行するlocalジョブ

の2種類があります。

この後者、local training jobを実行する際に、Unable to locate credentialsエラーが発生してジョブが実行できない現象に遭遇しました。

対処法を見つけたので、備忘録としてまとめておきます。

SageMakerでlocal training jobが実行できない時の対処法

事象

  • SageMaker instanceでlocal training jobを実行した際に、ジョブコンテナでUnable to locate credentialsエラーが発生してジョブが異常終了する

ただし、私はまだ確実なエラー再現手順を特定できてません。
同様の現象は下記のようにいくつか報告があがっています:

原因

  • ジョブ実行コンテナからインスタンスメタデータエンドポイント(169.254.169.254)にアクセスできていないこと

インスタンスメタデータエンドポイント(169.254.169.254)は、インスタンスのメタデータを取得するためのリンクローカルアドレスです。

Localのジョブ実行コンテナがインスタンスに付与されたIAM権限を参照/Token取得する際に、このエンドポイントにアクセスする必要があります。

しかし、このエンドポイントにアクセスできていないことで、ジョブ実行コンテナがIAMのTokenを取得できない = 権限を取得できない = Unable to locate credentials、という状況です。

対処法

  • 下記のようなスクリプトを実行し、ジョブ実行コンテナから169.254.169.254へアクセスできるように設定変更する
#!/bin/bash

# check if we need to configure our docker interface
SAGEMAKER_NETWORK=`docker network ls | grep -c sagemaker-local`
if [ $SAGEMAKER_NETWORK -eq 0 ]; then
  docker network create --driver bridge sagemaker-local
fi

# Get the Docker Network CIDR and IP for the sagemaker-local docker interface.
SAGEMAKER_INTERFACE=br-`docker network ls | grep sagemaker-local | cut -d' ' -f1`
DOCKER_NET=`ip route | grep $SAGEMAKER_INTERFACE | cut -d" " -f1`
DOCKER_IP=`ip route | grep $SAGEMAKER_INTERFACE | cut -d" " -f9`

# check if both IPTables and the Route Table are OK.
IPTABLES_PATCHED=`sudo iptables -S PREROUTING -t nat | grep -c $SAGEMAKER_INTERFACE`
ROUTE_TABLE_PATCHED=`sudo ip route show table agent | grep -c $SAGEMAKER_INTERFACE`

if [ $ROUTE_TABLE_PATCHED -eq 0 ]; then
  # fix routing
  sudo ip route add $DOCKER_NET via $DOCKER_IP dev $SAGEMAKER_INTERFACE table agent
  echo "route tables for Docker setup done"
else
  echo "SageMaker instance route table setup is ok. We are good to go."
fi

if [ $IPTABLES_PATCHED -eq 0 ]; then
  # fix ip table
  sudo iptables -t nat -A PREROUTING  -i $SAGEMAKER_INTERFACE -d 169.254.169.254/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 169.254.0.2:9081
  echo "iptables for Docker setup done"
else
  echo "SageMaker instance routing for Docker is ok. We are good to go!"
fi

重要なのは下記2つの設定変更部分です。

# fix routing
sudo ip route add $DOCKER_NET via $DOCKER_IP dev $SAGEMAKER_INTERFACE table agent

# fix ip table
sudo iptables -t nat -A PREROUTING  -i $SAGEMAKER_INTERFACE -d 169.254.169.254/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 169.254.0.2:9081

SageMaker SDKのlocal training jobでは、sagemaker-localという名前のdocker networkが利用されます (存在しない場合は作成する処理をスクリプト冒頭で行っています)。

このsagemaker-localを変更(ip routeの追加とiptablesのdestination変更)することによって、ジョブ実行コンテナから169.254.169.254へアクセスできるようになる -> インスタンスメタデータ取得できるようになる -> IAM権限Tokenを取得できるようになる、で、エラーを解消できました。

※上記スクリプトは、AWS公式が提供しているSageMaker examples内で見つけたセットアップスクリプトから一部を抽出/改変したものです。

おわりに

以上、Unable to locate credentialsエラーでSageMakerのlocal training jobが実行できないときの対処法をまとめました。

再現方法が特定できていないなどモヤモヤが残る部分はありますが、取り急ぎ解決にたどり着くことができました。

思わぬハマりポイントだったので、この記事が同じ目に遭ったどなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

amazon web services - Assume Sagemaker Notebook instance role from Docker container with default network mode - Stack Overflow

Local mode: role chaining/assumed role on notebook instances does not forward correct credentials · Issue #3464 · aws/sagemaker-python-sdk · GitHub

amazon-sagemaker-examples/setup.sh at main · aws/amazon-sagemaker-examples · GitHub

GitHub - aws/sagemaker-python-sdk: A library for training and deploying machine learning models on Amazon SageMaker

Retrieve instance metadata - Amazon Elastic Compute Cloud

Link-local address - Wikipedia

Terraform module設計時に考慮すべき3つの観点

Terraform moduleスコープ設計時に役立つ3つの観点をまとめます。

はじめに

Terraformでインフラを書く際、いかにmoduleを設計するか、は最も重要なことのひとつと考えています。

インフラコードの設計は往々にして現場組織の文脈/歴史的背景を汲んだものになりがちだと思いますが、一般論としての設計プラクティスも押さえておきたいところです。

例えばHashicorpは、下記の記事でTerraform moduleの設計時の考え方をプラクティスとしてまとめています。

Module Creation - Recommended Pattern | Terraform | HashiCorp Developer

今回はこの記事をベースにしつつ、自分の理解と解釈でTerraform module設計時に考慮すべき観点をまとめていきます。

Terraform module設計の考え方

Moduleスコープ設計の3つの観点

Terraform moduleを設計する際は、下記の3つの観点を考慮します。

  • Encapsulation / カプセル化
  • Privileges / 特権性
  • Volatility /変動性

Encapsulation / カプセル化

関連するリソースをグルーピングして抽象度を上げます。

リソースをグルーピングしておくことで、moduleのユーザーは詳細を知り尽くさずとも多くのリソースを構築できるようになります。 逆に、あまりに多くのリソースをmoduleに含めすぎてしまうと、内部構造が複雑になったりmoudleの意図が不明瞭になってしまうので注意が必要です。

この観点の確認に役立つ問い:

  • Moduleの命名がパッと思い浮かぶか
  • Moduleに含まれるリソースは一緒にデプロイされるべきものか

Privileges / 特権性

リソースの権限境界を揃えます。

リソースを変更できるのは誰か/どのチームなのかを考慮し、module内にあるリソースは同一チームによって管理されるようにします。 例えば、アプリチームが管理するmodule内に、本来であればアドミンチームが管理すべきリソースが含まれている場合は、責務がmoduleとして上手く分離できていません。

この観点の確認に役立つ問い:

  • 単一module内に複数チームの責務が混在していないか

Volatility /変動性

Moduleに含まれるリソースの更新頻度/ライフサイクルを揃えます。

同一module内に含まれたリソースは、基本的に同じタイミングでデプロイされます。 更新頻度/ライフサイクルの異なるリソースが混在すると、変更したくないものが意図せず削除/更新されてしまう (またはそのリスクを恐れて更新したいリソースの更新頻度が下がる) という状況が発生します。

この観点の確認に役立つ問い:

  • Moduleに含まれるリソースの更新頻度は揃っているか
  • Moduleに含まれるリソースのライフサイクルは揃っているか

Module設計例

例として、下記のような3層構造の典型的なWebアプリケーションのインフラをTerraformで構築するケースのmodule設計を考えてみます。

3層構造のアプリケーションを構築する | 画像はドキュメントより

Network module

Network moduleとして、VPCやSubnetなどのネットワーク関連リソースを含めます。

Network moduleの設計 | 画像はドキュメントより

Module設計の観点:高い特権性と低い変動性

  • ネットワークリソースは、責任を持つ単一のチームのみが作成/変更できるべきである。
  • ネットワークリソースは、一度作成されたら頻繁に変更されない。 (他のリソースが頻繁に更新される際も、ネットワークリソースは変更されないことを保証したい)

Web module

Web moduleとして、ロードバランサーやWebサーバーを含むWeb層のリソースを含めます。

Web moduleの設計 | 画像はドキュメントより

Module設計の観点:高いカプセル性と高い変動性

  • Web層のリソースという、概念的にも容易に分離できるリソースをmoduleとして束ねる。
  • これらリソースは頻繫に変更されるため、moduleとして分離することで他のリソースへの影響を抑える。

App module

App moduleとして、AppサーバーやS3を含むアプリケーション層のリソースを含めます。

App moduleの設計 | 画像はドキュメントより

Module設計の観点:高いカプセル性と高い変動性

  • アプリケーション層のリソースという、概念的にも容易に分離できるリソースをmoduleとして束ねる。
  • これらリソースは頻繫に変更されるため、moduleとして分離することで他のリソースへの影響を抑える。

Database module

Database moduleとしてデータベースをmoduleとして分離します。

Database moduleの設計 | 画像はドキュメントより

Module設計の観点:高い特権性と低い変動性

  • データベースインフラは、責任を持つ単一のチームのみが管理する。
  • 変更頻度が低いリソースのため、moduleとして分離することで他リソースの頻繫な変更からの影響を抑える。

Routing module

Route 53など、ネットワークルーティングに必要なリソースをRouting moduleに含めます。

Routing moduleの設計 | 画像はドキュメントより

Module設計の観点:高い特権性と低い変動性

  • ネットワークルーティングは、責任を持つ単一のチームのみが管理する。
  • 変更頻度が低いリソースのため、moduleとして分離することで他リソースの頻繫な変更からの影響を抑える。

Security module

IAMやsecurity grounp、MFAなどのセキュリティ関連リソースをmoduleとして分離します。

Security moduleの設計 | 画像はドキュメントより

Module設計の観点:高い特権性と低い変動性

  • IAMやセキュリティの変更は、責任を持つ単一のチームのみが管理する。
  • 変更頻度が低いため、moduleとして分離することで他リソースの頻繫な変更からの影響を抑える。

おわりに

以上、Terraform module設計時の観点とその適用例をまとめました。

インフラコードはアプリケーションコードに比べてそのプラクティスが語られる機会は多くないように感じますが、この記事が一つの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Module Creation - Recommended Pattern | Terraform | HashiCorp Developer

Module Composition | Terraform | HashiCorp Developer

Index - Terraform Recommended Practices | Terraform | HashiCorp Developer

Welcome - Terraform Best Practices

Best practices for using Terraform  |  Google Cloud

boto3とbotoの違い | AWS SDK for Pythonの歴史を調べる

boto3とbotoの違いや、そもそもの"boto"の由来など、AWS SDK for Pythonの歴史を調べてまとめます。

はじめに

AWS SDK for Pythonとしてboto3をいつも使っています。

ただ、私がAWSを触り始めたときには既に"boto3"になっていたので、"3"というからには無印botoやboto2もあるんだよね..?というモヤっとした疑問を抱えていました。 以前からAWSを知る人にとっては自明かもしれませんか、自分はまだ明確な答えを知りません。

そこで今回はboto3の歴史的背景を調べ、

  • boto3とbotoの違い
  • boto2はどこに行ったのか
  • そもそも"boto"の由来は?

をまとめます。

AWS SDK for Pythonの歴史

boto3とbotoの違い

少し調べると、boto3とは別にbotoがライブラリとして存在していたことが分かります。

このbotoとboto3の違いを調べてみると、stackoverflowにbotoの作者自身が回答を寄せていました:
python - What is the difference between the AWS boto and boto3 - Stack Overflow

ざっくり意訳抜粋すると、

  • botoは2006年から使われていたAWS公式のAWS Python SDK
  • しかしその設計から、AWSサービスが増えるにしたがってメンテが困難になった
  • そこで設計を刷新したboto3が開発された
    • botocoreをベースとする設計
    • AWSとの低レベルなインターフェースは自動生成される
    • これによりクライアント層の設計を綺麗に保てるようになった

ということのようです。
なるほど私が知らなかっただけで、botoは2006年から(boto3がGAする)2015年まで10年近くも活躍していたんですね。 時間の経過とともに設計刷新の必要性が出たのも納得できます。

各GitHub starsの推移からも、この背景が透けて見えてきます:

  • botoは元々人気のあったライブラリ
  • 2015頃からboto3/botocoreが登場
  • 以後boto3の人気は直線状に上昇/botoは役目を終える

boto, boto3, botocoreのGitHub star数推移

boto2はどこに行ったのか

botoとboto3登場の歴史は納得しました。
が、"boto2はどこに行ったのか"という疑問が残ります。

これは"boto2"という言葉がどの文脈で使われているのか、を過去の情報から調べることで推し量ろうと思います。

いろいろ調べて得た結論はこちらです:

「boto2 はbotoのversion2のことを指すらしい」


ドキュメント類の記述からいくつか例示します:

botoのドキュメントには、"これはbotoの古いバージョン(boto2)のドキュメントだから、boto3を見ることをお勧めします"という記述があります:

You are viewing the documentation for an older version of boto (boto2).
boto: A Python interface to Amazon Web Services — boto v2.49.0


boto3のドキュメントにも、"boto2からの移行"という文脈でbotoのversion2のことを"boto2"と呼んでいる様子が見られます:

The rest of this document will describe specific common usage scenarios of Boto 2 code and how to accomplish the same tasks with Boto3.
Migrating from Boto 2.x - Boto3 1.26.127 documentation


以上、歴史的推移をまとめると、

  • 2006年よりbotoが利用されてきた
  • botoのversion2のことをboto2と呼んでいた
  • 2015年、設計刷新したboto3がリリースされた

ということになりそうです。 スッキリしました。

"boto"の由来

ちなみに"boto"という言葉の由来については、boto3のREADMEに記載があります:

Boto (pronounced boh-toh) was named after the fresh water dolphin native to the Amazon river.
GitHub - boto/boto3: AWS SDK for Python

アマゾンカワイルカ/Amazon river dolphin/Inia geoffrensis/通称boto、が由来のようです。

また下記Issueでは、botoの作者自身がその由来をコメントされています:

It was named after the fresh water dolphin native to the Amazon river. I wanted something short, unusual, and with at least some kind of connection to Amazon. Boto seemed to fit the bill 8^)
Why is the project named boto? · Issue #1023 · boto/boto3 · GitHub

(意訳)
アマゾン川に生息する川イルカから名前を付けました。 短くて、特徴的で、何かしらAmazonに関係する名を付けたかったので、Botoはぴったりの名前でした。

Amazon river dolphin (boto) の写真 | 画像はWikipediaより

おわりに

以上、boto3とbotoの違いから、AWS SDK for Pythonの歴史を調べてまとめました。

ふと思い立って発作的に始めた調べものでしたが、なかなか面白いことが知れて満足です。

同じように不思議に思ったどなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

python - What is the difference between the AWS boto and boto3 - Stack Overflow

Elastician

What is the difference between the AWS boto and boto3? - Quora

boto: A Python interface to Amazon Web Services — boto v2.49.0

Migrating from Boto 2.x - Boto3 1.26.127 documentation

GitHub - boto/boto3: AWS SDK for Python

GitHub - boto/boto: For the latest version of boto, see https://github.com/boto/boto3 -- Python interface to Amazon Web Services

GitHub - boto/botocore: The low-level, core functionality of boto3 and the AWS CLI.

Why is the project named boto? · Issue #1023 · boto/boto3 · GitHub

Amazon river dolphin - Wikipedia

RustのAWS LambdaをTerraformで実装する

Rustで書いたAWS LambdaをTerraformでデプロイする方法の備忘録です。

はじめに

こんにちは、@bioerrorlogです。

最近、Rustの採用が増えている印象があります。 MetaでRustがCLIとサーバーサイドでの正式サポート言語に追加されたニュースも記憶に新しいですね。

AWS LambdaではRustランタイムは現在ビルトインでは提供されていませんが、カスタムランタイムを使えばRustで書かれたLambdaを作成できます。

Rustで書いたLambdaをTerraformでデプロイしてみたので、今回はその備忘録を残します。

RustのAWS LambdaをTerraformで実装する

作ったもの

最終的なコードはこちら: github.com

EventBridgeがLambdaをトリガーし、ログをCloudWatch Logsに吐き出すごくシンプルな構成です。

アーキテクチャ: lambda-rust Architecture

ではポイントとなる部分をメモします。

Terraformの実装

Lambda実装の重要な部分を抜粋します。

####################################################
# Lambda Function
####################################################

resource "aws_lambda_function" "this" {
  function_name = "${var.name_prefix}_rust_lambda_sample"
  description   = "Rust lambda sample."

  filename         = data.archive_file.zip.output_path
  source_code_hash = data.archive_file.zip.output_base64sha256

  role    = aws_iam_role.this.arn
  handler = local.lambda_bin_name
  runtime = "provided.al2"
}

resource "null_resource" "rust_build" {
  triggers = {
    code_diff = join("", [
      for file in fileset(local.lambda_local_path, "**.rs")
      : filebase64("${local.lambda_local_path}/${file}")
    ])
  }

  provisioner "local-exec" {
    working_dir = local.lambda_local_path
    command     = "cargo lambda build --release"
  }
}

data "archive_file" "zip" {
  type        = "zip"
  source_file = local.lambda_bin_local_path
  output_path = local.lambda_zip_local_path

  depends_on = [
    null_resource.rust_build
  ]
}

terraform-examples/main.tf at main · bioerrorlog/terraform-examples · GitHub

Lambdaが構築される全体的な流れ:

  1. null_resourceでRustパッケージをビルド
  2. archive_fileでzip化
  3. カスタムランタイム指定したaws_lambda_functionにzipファイルを直接指定

まずはnull_resourceでコマンドcargo lambda build --releaseを実行し、Rustパッケージをビルドしています。 ここで使っているcargo-lambdaは、RustをAWS Lambdaで使うための機能を持ったツールです。 インストール方法などは後の章で説明します。

null_resourceにはtriggersを指定することで、Rustコードに変更があった場合に再ビルドを実行するようにしています。

  triggers = {
    code_diff = join("", [
      for file in fileset(local.lambda_local_path, "**.rs")
      : filebase64("${local.lambda_local_path}/${file}")
    ])
  }

.rsのbase64エンコードに差分が出た場合に、null_resourceが再実行されます。

Rustコードがビルドされたら、そのアーティファクトをarchive_fileを使ってzip化します。 depends_onを使ってRustをビルドするnull_resourceに依存を張ることで、ビルド → zip化の順番で実行されることを保証しています。

そしてaws_lambda_functionでこのzipファイルのパスを直接指定しています。 (S3に配置してそのS3 URIを渡すやり方もありますが、今回はこちらの方式をとりました)

ランタイムには、runtime = "provided.al2"としてカスタムランタイム(Amazon Linux 2)を指定しています。


Terraform側の実装はざっとこんな感じです。
(詳細はコードをご覧ください)

次は、Rustパッケージの実装をみていきます。

Lambda用Rustパッケージの実装

Rustコード部分の最終的なコードはこちら: github.com

Lambda用Rustパッケージの実装は、aws-lambda-rust-runtimeというRustランタイムがAWS公式から提供されているのでこれを利用します。

大きな開発の流れは、

  1. cargo-lambdaのインストール
  2. 新規Lambdaパッケージを生成
  3. Rustコードの実装

です。

1. cargo-lambdaのインストール

まずはcargo-lambdaをインストールします。 先述の通り、cargo-lambdaはRustをAWS Lambdaで使うための機能を持ったツールで、cargoのsubcommandとして呼び出せます。

pip3 install cargo-lambda

cargo installなどのほかの手段でもインストールできます。 詳しくは公式ドキュメントをご覧ください。

cargo install cargo-lambda

2. 新規Lambdaパッケージを生成

インストールしたcargo-lambdaを使って、Rustパッケージを新規作成します。

cargo lambda new YOUR_FUNCTION_NAME

対話的にいろいろ聞かれますが、特に要件が無ければNo->Enterで新規Rustパッケージを生成できます。 ここの質問への答えによって生成されるテンプレートが変化するみたいですね。

$ cargo lambda new lambda
? Is this function an HTTP function? No
? AWS Event type that this function receives  
  activemq::ActiveMqEvent
  autoscaling::AutoScalingEvent
  chime_bot::ChimeBotEvent
  cloudwatch_events::CloudWatchEvent
  cloudwatch_logs::CloudwatchLogsEvent
  cloudwatch_logs::CloudwatchLogsLogEvent
v codebuild::CodeBuildEvent
[↑↓ to move, tab to auto-complete, enter to submit. Leave it blank if you don't want to use any event from the aws_lambda_events crate]

ちなみに生成される新規パッケージのディレクトリ構成は、↓のようにシンプルな形です。

$ tree -L 3
.
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files

3. Rustコードの実装

ここまで来たら、後はRustコードを実装していきます。
(ビルドおよびLambdaへのデプロイは、先述の通りTerraformで自動実行させます)

今回はあくまでRust & Lambdaをデプロイしてみるまでが目的なので、生成されているデフォルトのRustコードを眺めるのみに留めます。

あとは自由に実装していきましょう。

// main.rs
use lambda_runtime::{run, service_fn, Error, LambdaEvent};

use serde::{Deserialize, Serialize};

/// This is a made-up example. Requests come into the runtime as unicode
/// strings in json format, which can map to any structure that implements `serde::Deserialize`
/// The runtime pays no attention to the contents of the request payload.
#[derive(Deserialize)]
struct Request {
    command: String,
}

/// This is a made-up example of what a response structure may look like.
/// There is no restriction on what it can be. The runtime requires responses
/// to be serialized into json. The runtime pays no attention
/// to the contents of the response payload.
#[derive(Serialize)]
struct Response {
    req_id: String,
    msg: String,
}

/// This is the main body for the function.
/// Write your code inside it.
/// There are some code example in the following URLs:
/// - https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples
/// - https://github.com/aws-samples/serverless-rust-demo/
async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {
    // Extract some useful info from the request
    let command = event.payload.command;

    // Prepare the response
    let resp = Response {
        req_id: event.context.request_id,
        msg: format!("Command {}.", command),
    };

    // Return `Response` (it will be serialized to JSON automatically by the runtime)
    Ok(resp)
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        // disable printing the name of the module in every log line.
        .with_target(false)
        // disabling time is handy because CloudWatch will add the ingestion time.
        .without_time()
        .init();

    run(service_fn(function_handler)).await
}

おわりに

以上、Rust & LambdaをTerraformで実装する方法の備忘録でした。

AWSは近年Rustサポートを強化していく流れもありそうなので、今後はより手厚い機能が提供されていくかもしれませんね。

この記事がどなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

GitHub - awslabs/aws-lambda-rust-runtime: A Rust runtime for AWS Lambda

Rust Runtime for AWS Lambda | AWS Open Source Blog

Installation | Cargo Lambda

https://crates.io/crates/cargo-lambda

Why AWS loves Rust, and how we’d like to help | AWS Open Source Blog

terraform-provider-aws/examples/lambda at main · hashicorp/terraform-provider-aws · GitHub

AWS Lambda and Rust: Building AWS Lambda functions with Terraform pt. 1 | by Jakub Jantošík | Medium

AWSデータ分析基盤の良質な学習資料をまとめる

データ基盤領域について、AWS公式から出ている良質な学習資料/参考資料をまとめます。

はじめに

こんにちは、@bioerrorlogです。

データ分析基盤領域に関してAWS公式が発信している資料は豊富にあります。 これらは非常に参考になるものばかりですが、それぞれの資料は分散しており一覧で見渡せる場所がありません。

そこで今回は、それらAWSデータ分析基盤領域の良質な資料をまとめておこうと思います。

他にも良い資料がありましたらぜひ教えてください。

AWSデータ分析基盤の学習資料まとめ

AWS Well-Architected Framework - Data Analytics Lens

データ領域に特化したWell-Architected Framework。

アーキテクチャ設計の勘所を整理するのに最適。

docs.aws.amazon.com

AWS Whitepapers - Analytics & Big Data

データ分析/データ基盤領域に関するホワイトペーパー群。

各ユースケースのベストプラクティスを参照できる。

aws.amazon.com

AWS Reference Architecture - Analytics & Big Data

データ分析/データ基盤領域のリファレンスアーキテクチャ。

アーキテクチャ事例が一枚絵で俯瞰できる。

aws.amazon.com

This is My Architecture - Analytics

各企業のテックリードがアーキテクチャ事例をインタビュー形式で解説する動画シリーズの、データ分析/データ基盤カテゴリー。

どこが推しポイントなのか、生の声から臨場感を持って伝わってくるのも面白い。 インタビュアー側が何を聞くのかも、観点として参考になる。

aws.amazon.com

AWS Prescriptive Guidance - Analytics

各ユースケースについて、具体的なレベルで構築パターンをまとめた資料。

プラクティスを詳細に把握するのに良い。

docs.aws.amazon.com

AWS Solutions Library - Analytics

テンプレートとして提供されるSolutionのカタログ。

特に3rdパーティーから提供されているものが多いため、外部ツールとの連携周りも参考になる。

aws.amazon.com

AWS Black Belt - Analytics

各AWSサービスの基本概念から詳細まで、非常にわかりやすくまとまっているスライド/動画資料。

サービス単位で理解したいときに最適。

aws.amazon.com

AWS Big Data Blog

ビッグデータ/データ基盤領域についてのAWS Blog。

リリース情報も流れてくるので、最新情報のキャッチアップにも良い。

aws.amazon.com

The Amazon Builders' Library

Amazonのエンジニアによるシステム構築/運用ノウハウ集。

データ基盤領域に限った記事ではないが、読み物としても面白い。

aws.amazon.com

おわりに

以上、データ分析基盤領域に関する良さげなAWS公式資料をまとめました。

ざっと目を通すだけでも、その領域の土地勘が得られるので非常に重宝しています。

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

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

TerraformでAWSアカウントIDを取得する

Terraformで現在のAWSアカウントIDを取得する方法の備忘録です。

はじめに

Terraformを書いているとき、arnの指定等でAWSアカウントIDを取得&設定したくなることがあります。

今回はそのやり方の備忘録を残しつつ、ついでに関連部分のソースコードも読んでみます。

TerraformでAWSアカウントIDを取得する

やり方

aws_caller_identityのdata sourceを使って、AWSアカウントIDを取得できます。

data "aws_caller_identity" "current" {}

# data.aws_caller_identity.current.account_id
# でアカウントIDを取得できる

使用例としては、例えば↓のようにして、arn内にアカウントIDを埋め込みできます。

source_arn = "arn:aws:events:eu-west-1:${data.aws_caller_identity.current.account_id}:rule/RunDaily"

※ 参考: terraform-aws-lambda/examples/complete/main.tf at master · terraform-aws-modules/terraform-aws-lambda · GitHub

data.aws_caller_identity.current.account_idは毎回アクセスするには変数名が長いので、必要に応じてlocal変数に格納しておくのもおすすめです。

locals {
  account_id = data.aws_caller_identity.current.account_id
}

おまけ: Terraformソースコードを読む

せっかくなので、今回取り上げているaws_caller_identityのdata sourceが書かれてそうな部分のソースコードを覗いてみます。

aws_caller_identityはAWS STSのAPIを経由してるはずなので、terraform-provider-awsでSTS関連が定義されている場所を探します。

ここ↓ですね。

github.com

その中でも一番それっぽいところを抜粋します。

func dataSourceCallerIdentityRead(d *schema.ResourceData, meta interface{}) error {
    client := meta.(*conns.AWSClient).STSConn

    log.Printf("[DEBUG] Reading Caller Identity")
    res, err := client.GetCallerIdentity(&sts.GetCallerIdentityInput{})

    if err != nil {
        return fmt.Errorf("getting Caller Identity: %w", err)
    }

    log.Printf("[DEBUG] Received Caller Identity: %s", res)

    d.SetId(aws.StringValue(res.Account))
    d.Set("account_id", res.Account)
    d.Set("arn", res.Arn)
    d.Set("user_id", res.UserId)

    return nil
}

terraform-provider-aws/internal/service/sts/caller_identity_data_source.go at 41352c290416c86708236db2e62757bfebdb193d · hashicorp/terraform-provider-aws · GitHub

AWS ClientのSTS接続からGetCallerIdentityを叩いて、そこからCallerのaccount_id, arn, user_idが取得されてます。

おわりに

以上、Terraformで現在のAWSアカウントIDを取得する方法をメモしました。

Terraformは(CloudFormationと違って)AWS APIに基づいているので、この類の実装も直感的に辿れるのが良いですね。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Terraform Registry

GitHub - terraform-aws-modules/terraform-aws-lambda: Terraform module, which takes care of a lot of AWS Lambda/serverless tasks (build dependencies, packages, updates, deployments) in countless combinations 🇺🇦

エラー対処: YAML_FILE_ERROR Message: Expected Commands[0] to be of string type | CodeBuild

CodeBuildにおける下記エラーの原因と対処法を整理します。

YAML_FILE_ERROR Message: Expected Commands[0] to be of string type: found subkeys instead at line 6, value of the key tag on line 5 might be empty

はじめに

こんにちは、@bioerrorlogです。

CodeBuildはAWS上で手軽にPipelineのジョブを実行するのに便利なサービスですね。

先日、CodeBuildで下記のエラーに遭遇しました:

YAML_FILE_ERROR Message: Expected Commands[0] to be of string type: found subkeys instead at line 6, value of the key tag on line 5 might be empty

今回はこのエラーの原因と解決策を整理します。

原因は"コロン+スペース"

"コロン+スペース" : がコマンドに含まれていませんか?

例えば、下記のミニマルなbuildspecでこのエラーを再現することが出来ます。

version: 0.2

phases:
  build:
    commands:
     - echo "Hello: world"

結果:

Phase context status code: 
YAML_FILE_ERROR Message: Expected Commands[0] to be of string type: found subkeys instead at line 6, value of the key tag on line 5 might be empty

echo "Hello: world"に含まれる"コロン+スペース"によって、yamlの解釈に問題が生じた (subkeyとして解釈された) のがエラーの原因です。

解決策

コマンドをクオーテーションで囲う

一つ目の解決策は、コマンド全体をクオーテーションで囲うことです。

このやり方はドキュメントにも記載されています。

If a command contains a character, or a string of characters, that is not supported by YAML, you must enclose the command in quotation marks ("").

たとえば先の不正なbuildspecは、下記のように変更することでエラーを解消できます。

version: 0.2

phases:
  build:
    commands:
     - "echo Hello: world"

結果:

Running command echo Hello: world
Hello: world

"コロン+スペース"を使わない

もちろん、もしエラーの原因である"コロン+スペース"を使わないことが可能なら、単純に使わなければよいという話でもあります。

先述のクオーテーションで囲うやり方はコードの読み手を少し戸惑わせるかもしれないので、できればこちらでシンプルに対処したいところです。

コロンのみ、またはスペースのみ、であれば問題ありません。

※ コロンのみ

version: 0.2

phases:
  build:
    commands:
     - echo "Hello:world"

結果:

Running command echo "Hello:world"
Hello:world


※ スペースのみ

version: 0.2

phases:
  build:
    commands:
     - echo "Hello world"

結果:

Running command echo "Hello world"
Hello world

おわりに

以上、CodeBuildエラーの原因と対処法を整理しました。

yamlはjsonに比べ人間に優しいフォーマットですが、落とし穴には十分注意していきたいところです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Build specification reference for CodeBuild - AWS CodeBuild

amazon web services - YAML_FILE_ERROR Message: Expected Commands[0] to be of string type: - Stack Overflow

YAML syntax error when string contains a colon + space · Issue #2769 · ansible/ansible · GitHub

!FindInMapを!Subの中で使う | CloudFormation

AWS CloudFormationで!FindInMap!Sub内で使うやり方をまとめます。

はじめに

こんにちは、@bioerrorlogです。

CloudFormationの!Subは値を代入/置換 ("substitute")できる関数、!FindInMapMappingsから値を取得できる関数です。
(!SubFn::Subの短縮記法、!FindInMapFn::FindInMapの短縮記法)

典型的な使い方は、ざっくりこんな感じでしょうか:

# !Sub の使用例:
ExampleArn: !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/instance-id'
# -> ${AWS::Region}と${AWS::AccountId}が文字列に代入される

# !FindInMapの使用例:
Mappings: 
  RegionMap: 
    us-east-1: 
      "HVM64": "ami-0ff8a91507f77f867"
    us-west-1: 
      "HVM64": "ami-0bdb828fd58c52235"
Resources: 
  myEC2Instance: 
    Type: "AWS::EC2::Instance"
    Properties: 
      ImageId: !FindInMap [ RegionMap, !Ref 'AWS::Region', HVM64 ]
# -> Mappingsから該当の値が取得される


ここでこの二つの関数を組み合わせて、!FindInMapで取得した値を!Subで文字列に代入したくなるときがあります。

今回はそのやり方、!FindInMap!Subの中で使う方法をメモします。

!FindInMapを!Subの中で使う

下記のようにして、!FindInMapで取得した値を!Subで代入できます。

Mappings:
  LogGroupMapping:
    Test:
      Name: test_log_group
    Prod:
      Name: prod_log_group

Resources:
  myLogGroup:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Sub 
        - 'cloud_watch_${log_group_name}'
        - log_group_name: !FindInMap [ LogGroupMapping, Test, Name ]
# ${log_group_name} が!FindInMap結果で置換される

!Subは単純に文字列内の${...}を置換する以外にも、リストの第一要素内の${...}第二要素で置換する使い方ができます。

これを利用して、!Subのリスト第二要素に!FindInMapを定義すれば、これらを併用することができます。

該当部抜粋:

LogGroupName: !Sub 
  - 'cloud_watch_${log_group_name}'
  - log_group_name: !FindInMap [ LogGroupMapping, Test, Name ]
# ${log_group_name} が!FindInMap結果で置換される

おわりに

今回はCloudFormationで!FindInMap!Sub内で使う方法をまとめました。

CloudFormationはTerraform等ほかのIaCツールより癖のある書き方を強いられることも多いですが、AWSを使う以上は避けて通れないものだと思っています。

上手いこと使いこなしていきたいものです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Use the Fn::Sub function in AWS CloudFormation

aws-cloudformation-templates/FindInMap_Inside_Sub.yaml at master · awslabs/aws-cloudformation-templates · GitHub

amazon web services - How to use !FindInMap in !Sub | userdata section - Stack Overflow

Fn::FindInMap - AWS CloudFormation

Mappings - AWS CloudFormation

Fn::Sub - AWS CloudFormation