- 投稿日:2020-12-08T23:25:37+09:00
「パブリック仮想インターフェイス」と「プライベート仮想インターフェイス」の違い
初めに
皆さんは「パブリック仮想インターフェイス」と「プライベート仮想インターフェイス」の違い理解していますか?
「パブリック仮想インターフェイス」と「プライベート仮想インターフェイス」
パブリック仮想インターフェイス(Public virtual interface)
パブリックリソース (非 VPC サービス) に接続するために使用する。
※パブリックリソース・・・AmazonS3, AmazonGlacier等プライベート仮想インターフェイス(Private virtual interface)
VPC に接続するために使用する。
終わりに
なるべくシンプルに理解したいと思い、公式ドキュメントを読み漁ったところ、とてもシンプルな説明が書いてありました。
読んでくださった皆さんの理解の一助となれば幸いです。公式ドキュメント
- 投稿日:2020-12-08T21:47:48+09:00
Amplify Mockingを利用してGraphQL API のユニットテストをする
どうも、じゃがです!
本記事は AWS Amplify Advent Calendar 2020 の9日目のエントリになります
2020年はAmplify iOS, AndroidのGA、Amplify JSのSSR対応やFlutterのDeveloper Preview、そしてAmplify Admin UIの登場など、Amplifyのアップデートが楽しい一年でしたね!
今回はGraphQL APIのユニットテストを書くノウハウについてまとめてみたいと思います!想定読者
- AmplifyでGraphQL APIのテストを書きたい人
- テストが要件に入ってくる開発でもAmplify使いたい人
Amplify の基本的な使い方については解説しませんのでご了承ください。手を動かしてAmplify学びたいとうかたは、以下のWorkshopもご参照ください
動作確認環境
- @aws-amplify/cli v4.34.0
- 本記事で作成したコード: https://github.com/jaga810/amplify-graphql-test
GraphQL APIのテストとは?
Amplify CLIでGraphQL APIを構築する際、Amplify Mockingを用いてAPIレスポンスが期待通り返ってくるか、手元で試しながら実装される方は多いのではないでしょうか。(Amplify Mockingを使ったことがないという方はぜひGraphQL API開発スピードを爆上げするAWS Amplify Mockingことはじめをご参照ください)
一方で開発中、継続的にユニットテストを実行したいというニーズもあるかと思います。ユニットテストがあれば今まで実装した部分が新たなコードの変更で崩れないことを担保でき、開発のスピードも質も向上します
Amplify CLIのGraphQL APIでユニットテストはどのように実施すればよいでしょうか?
本記事ではJavaScriptテスティングフレームワークのJestを用いて、Amplify Mockingで立ち上げたローカルサーバーへGraphQL Operationを行い、挙動をテストするという方針でユニットテストを行います
また、そのテストをAmplify Consoleを利用したCI/CDパイプラインで動かしますGraphQL APIの作成
Amplify プロジェクトの初期化
shell#適当な作業ディレクトリで以下のコマンドを実行します $ mkdir graphql-test; cd $_ $ amplify init #amplify initで聞かれる項目は全てデフォルトの選択肢で大丈夫ですGraphQL API の作成
shell$ amplify add api #amplify add apiで聞かれる項目は、認証方法でCognitoを選ぶこと以外は全てデフォルトの選択肢で回答しますシンプルなスキーマを持つ Todo モデルが出来上がりました
amplify/backend/api/graphqltest/schema.graphqltype Todo @model { id: ID! name: String! description: String }認可の仕組みを実装するために、このスキーマを以下のように書き換えます
amplify/backend/api/graphqltest/schema.graphqltype Todo @model @auth( rules: [ {allow: owner, ownerField: "owner", operations: [create, read, update, delete]}, ] ) { id: ID! name: String! description: String }これにより最初にTodoを作成した
owner
だけが、read/update/delete処理を実行できるようになりました動作確認
ユニットテストではローカル環境でGraphQL APIの動作確認が可能なAmplify Mockingを利用します。Amplify Mockingは内部的に、amplify-appsync-simulatorと、DynamoDB Localを利用しています
shell$ amplify mock api #amplify mock apiで聞かれる項目はすべてデフォルトの選択肢で回答します ... AppSync Mock endpoint is running at http://192.168.1.2:20002 (http://192.168.1.2:20002/)最後に表示されたエンドポイントでamplify-appsync-simulatorが立ち上がっており、アクセスすると以下のようなAmplify GraphiQL Explorerが表示されます
ユニットテストの実行
Jestの導入
まずは
package.json
を作成しますshell$ npm init #test commandのみ jest と入力しましょう。$ npm testコマンドでjestが走るようになりますJestをはじめとしたテストに必要なライブラリをインストールします
shell$ npm install —save-dev jest babel-jest babel-plugin-transform-es2015-modules-commonjsまた、プロジェクト直下に以下のファイルを作成します
.babelrc{ "env": { "test": { "plugins": [ "transform-es2015-modules-commonjs" ] } } }jest.config.jsmodule.exports = { transform: { '^.+\\.js$' : '<rootDir>/node_modules/babel-jest', }, moduleFileExtensions: ['js'] }@authのテストを書く
@authで指定した通りに、ownerしかupdateができないことを確認するユニットテストを書いていきます
テスト内で必要なライブラリをインストールします
shellnpm install —save-dev graphql-request graphql crypto base64url
Amplify JavaScriptのライブラリはAppSyncに渡すヘッダを自由に変更できないので、軽量なgraphql-requestをGraphQLクライアントとして利用します
cryptoとbase64urlはCognitoのJWTトークンを擬似的に生成するために利用しますプロジェクト直下に以下のファイルを作成します
auth.test.jsimport { GraphQLClient } from 'graphql-request'; import crypto from 'crypto'; import base64url from 'base64url'; import { createTodo, updateTodo } from './src/graphql/mutations'; const cognitoJwtGenerator = ({username}) => { const header = { 'alg': 'HS256', 'typ': 'JWT' } const payload = { 'sub': '7d8ca528-4931-4254-9273-ea5ee853f271', 'cognito:groups': [], 'email_verified': true, 'algorithm': 'HS256', 'iss': 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_fake_idp', 'phone_number_verified': true, 'cognito:username': username, 'cognito:roles': [], 'aud': '2hifa096b3a24mvm3phskuaqi3', 'event_id': '18f4067e-9985-4eae-9f33-f45f495470d0', 'token_use': 'id', 'phone_number': '+12062062016', 'exp': 16073469193, 'email': 'user@domain.com', 'auth_time': 1586740073, 'iat': 1586740073 } const encodedHeaderPlusPayload = base64url(JSON.stringify(header)) + '.' + base64url(JSON.stringify(payload)); const hmac = crypto.createHmac('sha256', 'secretKey') hmac.update(encodedHeaderPlusPayload) return encodedHeaderPlusPayload + '.' + hmac.digest('hex'); } //2ユーザーからリクエストを行えるよう2つのクライアントを作成 const testUsers = ['user_0', 'user_1']; const clients = []; clients.push(new GraphQLClient('http://localhost:20002/graphql', { headers: { Authorization: cognitoJwtGenerator({username: testUsers[0]}) }, })); clients.push(new GraphQLClient('http://localhost:20002/graphql', { headers: { Authorization: cognitoJwtGenerator({username: testUsers[1]}) }, })); describe('Todo Model', () => { test('Only owner can update their todos', async () => { const testTodo = { name: 'Test task', description: 'This is a test task for unit test', }; // Test用Todoの作成 const created = await clients[0].request(createTodo, {input: testTodo}); // Owner自身によるUpdateが成功することを確認 const updatedName = 'Updated Test Task by user_1'; const updatedByOwner = await clients[0].request(updateTodo, {input: {id: created.createTodo.id, name: updatedName}}); expect(updatedByOwner.updateTodo.name).toStrictEqual(updatedName); // Owner以外によるUpdateが失敗することを確認 const updatedByOthers = clients[1].request(updateTodo, {input: {id: created.createTodo.id, name: ''}}); await expect(updatedByOthers).rejects.toThrowError('ConditionalCheckFailedException'); }); });テストを実行してみましょう。( $ amplify mock api が動いていることを確認してください)
shell$ npm run test graphql-test@1.0.0 test /Users/daisnaga/Dev/amplify-playground/graphql-test > jest PASS ./auth.test.js Todo Model ✓ Only owner can update their todos (166 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 2.65 s Ran all test suites.テストが通りました!
CI/CDパイプライン (Amplify Console) でGraphQL APIのユニットテストを実行する
1コマンドでAppSync Simulatorの起動からテストの実行までを行う
これまでのように
$ amplify mock api
を起動しておきつつ、別のシェルで$ npm run test
を実行するのは、手元ではAppSync Simulatorの開始処理を何度も行わないで済むというメリットがある一方、Amplify ConsoleなどCI/CDパイプラインを利用する際は不便です。1コマンドでAppSync Simulatorの起動からテストの実行までを行うため、bahmutov/start-server-and-test を用います。shell$ npm install —save-dev start-server-and-testpackage.jsonのscriptsに以下を追記します。
package.json{ ... "scripts": { "test": "jest", "start-server": "amplify mock api", "ci": "start-server-and-test start-server http://localhost:20002 (http://localhost:20002/) test“ }, ... }これにより、
$ npm run ci
実行時に以下の処理が行われます
- 第一引数の
start-server
、すなわちamplify mock api
コマンドの実行によりAmplify Mockingを開始- 第二引数の
http://localhost:20002
を監視し、amplify-appsync-simulatorの起動を待機- 第三引数の
test
すなわちjest
を実行- テストの実行終了後、Amplify Mockingを停止
実際に試してみましょう。($ amplify mock api が停止していることをご確認ください)
$ npm run ciユニットテストが無事実行できたかと思います。(途中AppSync Simulatorのエラーが出力されますが、期待通りの振る舞いです)
Amplify Consoleのセットアップ
下ごしらえができたので、Amplify Console でテストを実行します。
ここではGitHubにこれまでのコードをpushする流れはSkipさせていただきます。
- AWSのマネージメントコンソールからAmplify Consoleを開きます
- 右上の New app > Host web app をクリックします
- GitHubを選択し、 Continueをクリックします
- リポジトリブランチの追加でpushしたブランチをrepoとブランチを選択します
- ここまで $ amplify pushを実行していないので、ビルド設定の追加ではCreate new environmentを選択し、env名には好きな名前(mainline など)を入力します。
- ビルド設定の項目で、テスト項目を追加します
- 「次へ」をクリックし、「保存してデプロイ」します
数分待つとビルドとテストがクリアされ、デプロイされたことを確認できます。
ビルド設定にGraphQL APIのユニットテストを足す
このままだと特にユニットテストもせずデプロイされてしまいます。テストの設定を追加してみましょう
アプリの設定 > ビルドの設定 > ビルド設定の追加で編集をクリック
以下のように書き換え、「保存」します
amplify.ymlversion: 1 backend: phases: # IMPORTANT - Please verify your build commands preBuild: commands: - amazon-linux-extras enable corretto8 - yum install -y java-1.8.0-amazon-corretto java-1.8.0-amazon-corretto-devel - npm ci - amplify pull --appId ${APP_ID} --envName main -y - # Amplify Mockingの実行に必要なAmplifyプロジェクトの情報をpull - # ${APP_ID}はご自身のIDに置き換えてください。Amplify ConsoleでAppを開いて、#/の次の文字列です。(例: d3j54ikssyzl4d) build: commands: - '# Execute Amplify CLI with the helper script' - npm run ci && amplifyPush --simple #ユニットテストが通った時のみデプロイ frontend: phases: preBuild: commands: - npm ci build: commands: [] artifacts: # IMPORTANT - Please verify your build output directory baseDirectory: / files: - '**/*' cache: paths: - node_modules/**/*(注1) Amplify Consoleのビルド環境はAmazon Linux2で、$ amplify mock api に必要なJavaランタイムが入っていません。そのため本記事ではAWSが提供する無償OpenJDK Distributionである Amazon Corretto をビルド時にインストールしています。ただしこれではCI/CDパイプラインが動くたびにJavaをインストールすることになり、ビルドの実行時間が伸びてしまいます。実際に利用するときはCustom build images機能を用いて、Javaをインストール済みのコンテナを利用することをおすすめします。
(注2) Add end-to-end tests to your appのように、
test
セクションを用いる事も可能ですが、test
セクションに書いた内容はbackend
とfrontend
のセクションが実行された後に実行されます。その場合$ npm run ci
を用いたユニットテストが通ろうが通るまいが、バックエンドリソースが更新されてしまいます。そのため本記事ではnpm run ci && amplifyPush --simple
とすることで、ユニットテストが通らない場合にバックエンドリソースの更新をしないようにしています。ビルドのページに戻って「このバージョンを再デプロイ」ボタンをクリックします。
数分待つとビルドとデプロイが実行されました!
ユニットテストが実行され、パスしてることが確認できます。
ユニットテストが通らなかった場合の挙動の確認
ユニットテストが通らなかった場合に、デプロイが実行されないことを確認しましょう
auth.test.js
に必ず失敗するテストを追加しますauth.test.js... describe('Todo Model', () => { //[追加部分]必ず失敗するテスト test('must fail', () => { expect(0).toStrictEqual(1); }) //以下は同じ test('Only owner can update their todos', async () => { const testTodo = { name: 'Test task', description: 'This is a test task for unit test', }; // Test用Todoの作成 const created = await clients[0].request(createTodo, {input: testTodo}); // Owner自身によるUpdateが成功することを確認 const updatedName = 'Updated Test Task by user_1'; const updatedByOwner = await clients[0].request(updateTodo, {input: {id: created.createTodo.id, name: updatedName}}); expect(updatedByOwner.updateTodo.name).toStrictEqual(updatedName); // Owner以外によるUpdateが失敗することを確認 const updatedByOthers = clients[1].request(updateTodo, {input: {id: created.createTodo.id, name: ''}}); await expect(updatedByOthers).rejects.toThrowError('ConditionalCheckFailedException'); }); });この内容をgitにpushし、再度デプロイの様子をみてみます。
確かにビルドフェーズで失敗しており、
amplifyPush --simple
が実行されないことが確認できました!Tips & etc
リクエストヘッダの生成
AmplifyのGraphQL APIで利用するAppSyncではCognito、IAM、API_KEY、OIDCの4つの認証方法が利用できます。
ここでは、IAM、API_KEY認証のリクエストヘッダの作り方をご紹介します。
(著者の環境ではAmplify MockingのOIDC認証がUnauthorizedException
になってしまい解決できず、、ここでは割愛させていただきます...><)IAMの場合
リクエストヘッダに以下を付与します
const iam_key_client = new GraphQLClient('http://localhost:20002/graphql', { headers: { 'Authorization': 'AWS4-HMAC-SH256 IAMAuthorized' } })API_KEYの場合
const api_key_client = new GraphQLClient('http://localhost:20002/graphql', { headers: { 'x-api-key': 'da2-fakeApiId123456' } })Seed Dataの作成
残念ながらAmplify CLIのAmplify MockingではSeed Dataの作成をサポートしていません。
PFRが上がっているので清き+1をぜひ!
https://github.com/aws-amplify/amplify-cli/issues/2563#issuecomment-541873258VTL単体のテスト
Amplify CLI を使っていると生VTLを書くことはほとんどありませんが、Custom VTL単体でtestしたい場合は、@G-awaさん著 Effective AppSync 〜 Serverless Framework を使用した AppSync の実践的な開発方法とテスト戦略 〜 の「VTLをテストする」の項目が非常に参考になります。
まとめ
Amplify Mockingを活用したGraphQL APIのユニットテストと、ユニットテストをAmplify Consoleでのデプロイ時に実行するところまでをご紹介しました。日々の開発にお役立ていただけましたら幸いです!!!
参考資料
- 投稿日:2020-12-08T20:51:38+09:00
Amazon Rekognitionでユーザーがアップロードしたアレな画像を<del>収集</del>ブロックする
AWSのML系のサービスにAmazon Rekognitionというのがあり、画像の認識に使えるAPIが用意されています。
ユーザーからアップロードされた画像をサイト上に表示する場合などに、まあ何というかいろいろとアウトな感じの画像をアップロードされるとよろしくないですね。そこでこのRekognitionを使うと、inappropriateな要素にタグ付けをしてくれます。
実装例
Node/TypeScriptでの実装例です。画像をBufferで受け取って、アウトっぽい画像なら
false
を返します。以下のドキュメントの内容を基にしています。https://docs.aws.amazon.com/rekognition/latest/dg/procedure-moderate-images.html
import * as AWS from 'aws-sdk'; AWS.config.update({ region: 'ap-northeast-1' }); export class RekognitionModerator { public async moderate(image: Buffer): Promise<boolean> { const rekognition = new AWS.Rekognition({apiVersion: '2016-06-27'}); const params: AWS.Rekognition.DetectModerationLabelsRequest = { Image: { Bytes: image, }, MinConfidence: 99.5 }; const res: AWS.Rekognition.DetectModerationLabelsResponse = await rekognition.detectModerationLabels(params).promise(); if (res.ModerationLabels && res.ModerationLabels.length > 0) { return false; } return true; } }ユーザーがアップロードしてきた画像をこのクラスでチェックすれば、いかがわしい画像をブロックできます。登録はさせないけどサーバーのどこかに保存しておく、みたいな邪悪なことをやるのはやめましょう。
いくらなんでもここにテスト用画像貼ると怒られるので貼りませんが、画像いくつか集めてきて試してみると
健全なしずかす絵
{ ModerationLabels: [], ModerationModelVersion: '4.0' }不健全なしずかす絵
{ ModerationLabels: [ { Confidence: 99.9688949584961, Name: 'Explicit Nudity', ParentName: '' }, { Confidence: 99.9688949584961, Name: 'Illustrated Explicit Nudity', ParentName: 'Explicit Nudity' } ], ModerationModelVersion: '4.0' }7ct Kindle版11ページ
{ ModerationLabels: [], ModerationModelVersion: '4.0' }Rekognitionからはこんな感じのレスポンスが返ってきます。
Confidence
は結果の信頼度、どのくらいの精度でアウトなのかを示す数値です。高いほど結果が信頼できるということになります。サンプルコードでは
MinConfidence: 99.5
という指定をしていました。この場合RekognitionはConfidence
が99.5以上の結果しか返さない、という動きになります。ちなみにデフォルト値は50なので相当緩く設定しています。いろいろ試してみた感じ、特に二次元にはわりと厳しいようで、別にR指定が付くような画像でなくても露出度高めの衣装とかだと98とか99とかそんな値が返ってくることが時々あります。二次元の画像を主に扱う場合は99.5あたりを機械検出の閾値にして、後は人間がモデレーションする(適宜巡回する、通報を受け付ける)のが現実的なところかな、というのが個人的な感覚です。
- 投稿日:2020-12-08T20:15:31+09:00
elasticが開発した公式のGo言語ElasticSearchクライアントについてまとめてみる
これはGo Advent Calendar 2020の8日目の記事です。
先日業務の中でElasticSearchを利用する機会があり、elasticがサポートしている公式のGoクライアントをその際にあまり日本語でまとまっていた情報がなかったので、これを機にまとめてみようと思います。
go-elasticsearch
https://github.com/elastic/go-elasticsearch概要
この公式ライブラリは2019年にリリースされた比較的新しいもので、Elasticの公式のクライアントとして認定され、メンテナンスされています。
https://www.elastic.co/guide/en/elasticsearch/client/index.htmlgo-elasticsearchクライアントはバージョン6系と7系がありますが、これはそれぞれElasticSearchの6系、7系に対応するものになっているので、使用するElasticSearchのバージョンに合わせて利用するライブラリのバージョンは決定してください。
使い方
Client作成
クライアント作成は2パターンあります。まずNewDefaultClientです。こちらは引数を取らないものですが、 ELASTICSEARCH_URLという環境変数にElasticSearchのエンドポイントURLを入れておくことで自動で設定してくれます。
elasticsearch.NewDefaultClient()elasticsearch.NewClient(Config)は色々とオプションを追加できるクライアントの作成方法になります。Elastic Cloudなどを利用する場合はアドレスではなく、IDでも接続することができます。この場合はELASTICSEARCH_URLに設定された環境変数は無視されます。
CACertで証明書、RetryOnStatusでリトライするStatusの定義なども盛り込むことが可能です。cert, _ := ioutil.ReadFile("path/to/ca.crt") cfg := elasticsearch.Config{ Addresses: []string{ "http://localhost:9200", "http://localhost:9201", }, Username: "foo", Password: "bar", RetryOnStatus: []int{429, 502, 503, 504}, CACert: cert, Transport: &http.Transport{ MaxIdleConnsPerHost: 10, ResponseHeaderTimeout: time.Second, DialContext: (&net.Dialer{Timeout: time.Second}).DialContext, TLSClientConfig: &tls.Config{ MinVersion: tls.VersionTLS11, }, }, } elasticsearch.NewClient(cfg)Search
検索サジェストなどで用いるSearchは以下のように使用します。
var buf bytes.Buffer query := map[string]interface{}{ "query": map[string]interface{}{ "match": map[string]interface{}{ "title": "test", }, }, } if err := json.NewEncoder(&buf).Encode(query); err != nil { log.Fatalf("Error encoding query: %s", err) } // Perform the search request. res, err = es.Search( es.Search.WithContext(context.Background()), es.Search.WithIndex("test"), es.Search.WithBody(&buf), es.Search.WithTrackTotalHits(true), es.Search.WithPretty(), ) if err != nil { log.Fatalf("Error getting response: %s", err) } defer res.Body.Close() if res.IsError() { var e map[string]interface{} if err := json.NewDecoder(res.Body).Decode(&e); err != nil { log.Fatalf("Error parsing the response body: %s", err) } else { // Print the response status and error information. log.Fatalf("[%s] %s: %s", res.Status(), e["error"].(map[string]interface{})["type"], e["error"].(map[string]interface{})["reason"], ) } }少し複雑ですが、queryに実際のリクエストで投げるJsonの構造体を参考にmap[string]interface()を定義して、検索する文字列を入れます。
HTTPでリクエスト送る際のJsonBodyがこんな感じだと
{ "size": 5, "query": { "bool": { "should": [{ "match": { "word.autocomplete": { "query": "え" } } }, { "match": { "word.readingform": { "query": "え", "fuzziness": "AUTO", "operator": "and" } } }] } }, }'Goで定義するクエリはこんな感じになります。なかなか複雑ですね・・・。
query := map[string]interface{}{ "query": map[string]interface{}{ "bool": map[string]interface{}{ "should": []map[string]interface{}{ { "match": map[string]interface{}{ "word.autocomplete": map[string]interface{}{ "query": normalized, }, }, }, { "match": map[string]interface{}{ "word.readingform": map[string]interface{}{ "query": normalized, "fuzziness": "AUTO", "operator": "and", }, }, }, }, }, }, }そしてその後それをjsonにエンコードし、Searchメソッドの引数にWithBody内に入れ、Searchメソッドを叩きます。
if err := json.NewEncoder(&buf).Encode(query); err != nil { log.Fatalf("Error encoding query: %s", err) } // Perform the search request. res, err = es.Search( es.Search.WithContext(context.Background()), es.Search.WithIndex("test"), es.Search.WithBody(&buf), es.Search.WithTrackTotalHits(true), es.Search.WithPretty(), )他にも多くの引数があることが見て取れます。withSortなどを用いるとSortなども可能となっています。
Searchメソッドのレスポンスはhttp.Responseのラッパーなようになっています。また、IsError()メソットで500エラーなどの判定をすることが可能です、
Searchなどとは異なり、IndexやCreate,Updateは比較的シンプルに記載することができます。基本的にそれぞれのXXRequestという型がgo-elasticのesapiパッケージに用意されているため、そこにリクエストする値を入れてDoメソッドを叩く形になります。
ここもレスポンスはIsErrorでチェックしてあげてください。
tag := Sample{ ID: id, Name: name, } reqByte, err := json.Marshal(tag) if err != nil { return err } requestReader := bytes.NewReader(reqByte) req := esapi.CreateRequest{ Body: requestReader, Pretty: true, } res, err := req.Do(ctx, r.client) if err != nil { return xerrors.Errorf("failed to update with elastic search. %w", err) } if res.IsError() { return xerrors.Errorf("failed to update with elastic search. Not ok. %s", res.Status()) } defer res.Body.Close()様々なオプションもその型の中で定義することが可能です。試しにUpdateRequest型を見てみましょう。基本的なリクエストのボディをBodyに格納する形になりますがHeaderやPrettyなど様々なオプションの定義ができることがみて取れますね。
type UpdateRequest struct { Index string DocumentType string DocumentID string Body io.Reader Fields []string IfPrimaryTerm *int IfSeqNo *int Lang string Parent string Refresh string RetryOnConflict *int Routing string Source []string SourceExcludes []string SourceIncludes []string Timeout time.Duration Version *int VersionType string WaitForActiveShards string Pretty bool Human bool ErrorTrace bool FilterPath []string Header http.Header ctx context.Context }以上がelasticがサポートするElasticSearchのGoクライアントの紹介になりました。
少しコード量が多くなってしまう場合もありますが、公式がメンテをしてくれることもあり安心して利用のできるライブラリなので使って損はないと思います。
- 投稿日:2020-12-08T20:08:15+09:00
AWS LambdaのコンテナサポートをJavaで試してみた。
AWS LambdaとServerless Advent Calendar 2020 12/8分の記事です。
COVID-19の影響で、オンライン開催となったAWS re:Invent 2020。
初日のキーノートで、AWS Lambdaがコンテナをサポートするという発表がありました。
New for AWS Lambda – Container Image Supportなんかやってみたなーと思って、昔Javaをやっていたので、
Java用のコンテナイメージ使って、Lambda関数を作ってみました。どんなコンテナイメージあるの?
コンテナイメージ(Base Image)にはどんなのがあるのかと、
DockerHubをみると、以下のものがあるようですね。
- amazon/aws-lambda-nodejs
- amazon/aws-lambda-python
- amazon/aws-lambda-java
- amazon/aws-lambda-dotnet
- amazon/aws-lambda-ruby
- amazon/aws-lambda-go
- amazon/aws-lambda-provided
現在標準のランタイムとして提供されている言語はすべてありそうです。
amazon/aws-lambda-providedはTagをみると、ALとかAL2があるようなので、
カスタムランタイム用のようです。各言語も、例えば、Javaでは、
バージョン8と11が用意されています。イメージの取得場所なんですが、
FROM public.ecr.aws/lambda/java:11
となっていて、
同じくKeyNoteで発表された
Announcing Amazon ECR Public and Amazon ECR Public Gallery
が早速使われているみたいですね。Base Imageを使うと、AWSがパッチ当てとかメンテナンスをしてくれると聞いております。
やってみた
コードをがっつり書くつもりはなく(すみません)。コード自体は、公式ドキュメントのAWS Lambda for Javaに記載のあったSample Appsのうち、java-basicのHander.javaを使わさせていただいてます。
ビルド自体はMavenを使ってます(昔に使ってたので)。DockerfileもDockerhub記載のものをベースにしてます。
FROM public.ecr.aws/lambda/java:11 # Copy function code COPY target/java-sample-1.0-SNAPSHOT.jar ${LAMBDA_TASK_ROOT} RUN ls -al RUN jar -xvf java-sample-1.0-SNAPSHOT.jar # Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile) CMD [ "example.Handler::handleRequest" ]最初は、jarファイルを${LAMBDA_TASK_ROOT}(/var/task)に配置すれば、
よしなにやってくれるかなと思ったんですが、
そんなわけもなく、
コンパイルされた状態で配置させる必要があるようで、
作成されたjarファイルを解凍してます(めっちゃ下手書きですみません)。とてもシンプルなJava("Hello Java")と返すようなプログラムだと、
ビルドしたClassファイルだけあれば行けそうだったんですが、
サンプルだと、Googleが提供するJSONデータとJavaオブジェクトを相互に変換するライブラリの**GSON**を使ってて、
それがClassNotFoundExceptionになっちゃうので、この形式にしてます。久々に使ったので、
もしかしたら、他にいい手段があるかもしれないので、
わかったらアップデートしようかなと思います。コンテナの中で全部できるかな?ってことでやってみたのがこちら
FROM public.ecr.aws/lambda/java:11 RUN yum -y update && yum -y install wget RUN wget http://repos.fedorapeople.org/repos/dchen/apache-maven/epel-apache-maven.repo -O /etc/yum.repos.d/epel-apache-maven.repo RUN sed -i s/\$releasever/6/g /etc/yum.repos.d/epel-apache-maven.repo RUN yum install -y apache-maven ENV JAVA_HOME=/usr/lib/jvm/java-11-amazon-corretto.x86_64/ # Copy function code COPY src/ ${LAMBDA_TASK_ROOT}/src/ COPY pom.xml ${LAMBDA_TASK_ROOT} RUN mvn package RUN cp -r target/java-sample-1.0-SNAPSHOT.jar ${LAMBDA_TASK_ROOT} RUN jar -xvf java-sample-1.0-SNAPSHOT.jar # Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile) CMD [ "example.Handler::handleRequest" ]こちらもjarファイルを解凍してます(同じく下手書きですみません)。
便利だなと思ったのは、
コンテナを起動して、cUrlなどでアクセスしてローカルテスト可能なのは便利かなと思いました。docker build -t java-sample-v11 . Successfully tagged java-sample-v11:latest docker run -p 9000:8080 java-sample-v11:latest別ターミナルで、
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"payload":"hello world!"}'
って感じで実行すると、問題なければレスポンスがあります。ログもちゃんと出力されます。
Init Duration: 0.22 ms Duration: 504.55 ms Billed Duration: 600 ms
となっていて、Billed Duration
が100ms単位だなーって思いましたが、ローカルなのでwENVIRONMENT VARIABLES: { "PATH": "/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin", "LAMBDA_TASK_ROOT": "/var/task", "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "3008", "TZ": ":/etc/localtime", "AWS_EXECUTION_ENV": "AWS_Lambda_java11", "AWS_LAMBDA_LOG_GROUP_NAME": "/aws/lambda/Functions", "AWS_LAMBDA_LOG_STREAM_NAME": "$LATEST", "LANG": "en_US.UTF-8", "_HANDLER": "example.Handler::handleRequest", "LAMBDA_RUNTIME_DIR": "/var/runtime", "HOSTNAME": "b10284f7ea25", "LD_LIBRARY_PATH": "/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib", "AWS_LAMBDA_FUNCTION_VERSION": "$LATEST", "PWD": "/var/task", "AWS_LAMBDA_RUNTIME_API": "127.0.0.1:9001", "SHLVL": "0", "HOME": "/root", "AWS_LAMBDA_FUNCTION_NAME": "test_function" }CONTEXT: { "memoryLimit": 3008, "awsRequestId": "fc64a0bd-890c-4339-a460-8ecfc5fa1686", "logGroupName": "/aws/lambda/Functions", "logStreamName": "$LATEST", "functionName": "test_function", "functionVersion": "$LATEST", "invokedFunctionArn": "", "deadlineTimeInMs": 3214833340286, "logger": {} }EVENT: { "payload": "hello world!" }EVENT TYPE: class java.util.LinkedHashMapEND RequestId: fc64a0bd-890c-4339-a460-8ecfc5fa1686 REPORT RequestId: fc64a0bd-890c-4339-a460-8ecfc5fa1686 Init Duration: 0.22 ms Duration: 504.55 ms Billed Duration: 600 ms Memory Size: 3008 MB Max Memory Used: 3008 MBローカルテストを実行してみて、問題なければ、
- ECRにレポジトリ作成
- ECRにログイン
- TAGづけ
- ECRにPush、
- ECRにPushしたイメージを使ってLambdaをデプロイ となります。 2〜4はコンソールでレポジトリ作成すると、ECR上でコマンド出してくれるので、その通りに実行すれば大丈夫そうです。
デプロイして、テスト実行なりして、動けばOKです。
違いあるのかな?
コンテナ利用したLambda関数と普通にjarファイルをアップしてデプロイしたLambda関数で違いあるのかな?っていうのをついでに確認してみました。
両方ともテストイベントのHello World
をベースにしたテストイベントを実行してます。コンテナ
jarファイルをデプロイ
直前にTwitterで流れてきて、知ったのですが、
コンテナ利用な場合は、Init処理の時間も課金単位に含まれるようです。
Lambda のコンテナイメージパッケージって初期化時間も課金されるのね。カスタムランタイムと同じ扱いか。 pic.twitter.com/xSrDwI2FPP
— hayao_k (@hayaok3) December 8, 2020カスタムランタイムも同じなんですね。それは知らなかったです。
Java2度目以降は速いですねー(今更感)。
そして、課金単位がちゃんと1ms単位になってる!まとめ
AWSJの西谷さんが発表後にブログAWS Lambdaがコンテナをサポートしたのでちょっと試してみたを書かれており、そこで、
「あくまでもLambdaの実行モデルであるファンクションモデルはそのままにランタイムの自由度が増した」
と述べられていますが、その通りかなと思います。Dockerイメージ用意して、そのままECRにPushして、デプロイできるのは、
Docker上で開発されている開発者にとっては便利なのかもしれませんね。
(普段Docker上で開発してないもので。。。今後の検討課題ではあります)以前、Amazon Linux2上でビルドしたモジュールを含んだLambdaのパッケージを作ってデプロイするという機会があったのですが、
そういう場合、
Docker上(Amazon Linux2ベースのはず)でモジュールをビルドして、そのままアップロードできるのかなと思ったりしてます。
※あとで試そうかなとは思っています。ただ、そういう用途でなければ、自分が定常的に使うか。。。と言われると、今のところ、積極的には使わないかなぁーっていうのが結論です。
おまけ
今年の7月に共著でAWS LambdaをはじめとするServerlessでの開発などについて記した基礎から学ぶ サーバーレス開発という本を書かせていただいたのですが、
コンテナサポート以外にも、Lambda Extensions(これre:Invent落選組だったんだな。。。)、課金単位の変更(100ms->1ms)、メモリ量の増加などのアップデートがあったので、本自体もアップデートしない(書いてた時から、すぐに変わっちゃうんだろうなとは思っていましたが)ダメだなぁ。。。なんて思った人です。
検証して一気に書き上げたので、至らぬ点があるかもしませんが、ご容赦ください。。。
- 投稿日:2020-12-08T19:54:08+09:00
FileMakerでS3にREST APIでアクセスする (AWS Signature Version 4)
FileMaker Advent Calendar 2020 8日目の記事です。
最近はAWSにFileMaker Serverをデプロイして利用することも増えてきており、他のAWSサービスと組み合わせてFileMakerを利用している現場も増えてきていると思います。
そのためAWSのREST APIをFileMakerから直接利用するシーンは少なくないはずですが、それに必要な"AWS Signature Version 4"のいい感じの計算式が見当たらなかったので、今回1から作ることにしました。
タイトルにS3とありますが、S3をサンプルの題材に使用しているだけで実際はだいたいのREST APIは同じ方法でアクセスできるはずです。
AWS Signature Version 4とは
AWS Signature Version 4とはAWSのREST APIで必要なSignature(署名)のことです。
具体的にはHTTPヘッダーに下記のようなAuthorizationヘッダー情報を加えてアクセスする必要があります。
Authorizationヘッダー例:
Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7※見やすくするために改行を挟んでいますが、実際には改行はされません。
Authorizationヘッダーの
Signature=
以降の部分がSignature(署名)です。Signatureの作成には色々ルールがあり、厳密にこれを守らないと正しいSignatureが作れないため、1から実装するのは結構面倒です。
なので、そのSignatureを含め、HTTPヘッダーをまとめて作る計算式を今回は作成してきました。
S3ダウンロードを実行してみる
まず、サンプルファイルを使ってS3のダウンロードを実行してみましょう。
Step 1. アクセスキーの発行
Signatureの作成にはまずAWS IAMユーザーのAccessKeyID, SecretAccessKeyが必要です。
IAMユーザーはFileMakerファイル毎に作成するのがオススメです。ファイルが多い場合は面倒ですが、ファイル毎にアクセス可能な範囲を細かくコントロールする方が後々便利だと思います。
ユーザー名はファイル名などにしておくとわかりやすくていいと思います(例:FM_FileStore
)。ルートユーザーのアクセスキーでも利用は可能ですが、セキュリティーの観点からルートユーザーのアクセスキーの発行は推奨されてません。
IAM ユーザーのアクセスキーの管理 - AWS Identity and Access Management
Step 2. サンプルファイルのダウンロード
GitHubにサンプルファイル計算式、サンプルファイルを置いておきました。
hazi/FileMaker-AwsSignature: AWS Signature Version 4 in FileMaker formula.こちらからサンプルファイルをダウンロードしてください。
Step 3. アクセスキーの設定
ファイルを開くと書いてありますが、カスタム関数にAccessKeyID, SecretAccessKeyを保存する形式をとっています(定数としてのカスタム関数の利用)。
AWS.SecretAccessKeyの場合はこんな感じです。
AWS.SecretAccessKeyLet([ ~yourSecretAccessKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; _=0]; Case( not IsEmpty($AWS_SecretAccessKey); $AWS_SecretAccessKey; ~yourSecretAccessKey ) )必要に応じて、スクリプト内でアクセスキーを書き換えられるようになっています。不要な場合は削除してもらっても構いません。
基本的には関数の
~yourAccessKeyID = "AKIDEXAMPLE";
や
~yourSecretAccessKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
の行を書き換えて使用してください。Step 4. スクリプト修正
S3 Download(path)
スクリプトにスクリプト引数を渡すことで、ファイルのダウンロードが可能です。まず、
S3 Download(path)
の設定値を変えます。# Setting 変数を設定 [ $host ; 値: "example.s3.amazonaws.com" ] 変数を設定 [ $region ; 値: "us-east-1" ]
$host
はバケット名.s3.amazonaws.com
の形式です。
$region
は東京の場合はap-northeast-1
です。すべて引数で実装してもいいのですが、ファイル内で複数のバケット、リージョンを利用することもあまりないと思うので、あえて固定値にしています。必要に応じて書き換えてください。
Step 5. スクリプト実行
次に、新規にスクリプトを作成し、引数にダウンロードパス(バケットルートからのパス)を指定します。
スクリプト実行 [ 指定: 一覧から ; 「S3 Download(path)」 ; 引数: "/path/to/file.txt" ] 変数を設定 [ $result ; 値: Get(スクリプトの結果) ]これで実行すればS3のファイルがダウンロードできるはずです。
XMLパース
ここで1つ注意点があります。
S3のAPIの戻り値は基本的XMLです。
XMLを手動でパースするのはなかなか面倒なので、今回はBaseElements Pluginを使用しています。ファイルのダウンロードだけならそのまま動くかもしれませんが、エラー処理などはBaseElements Pluginがないとうまく動作しません。
BaseElements Pluginを長いこと使っていますが、特段クラッシュしたりということはないので使用をオススメしています。不安な場合やクライアントの関係で難しい方パース部分の計算式を修正してください。
Downloads - BaseElements Plugin Support
サンプル以外のアクセス
S3のDownload, Upload, List など以外にも細かくいろんなアクションがあります。
それらのアクセス方法は公式ドキュメントと、下記の解説を参考に独自に作成してみてください。Amazon S3 REST API Introduction - Amazon Simple Storage Service
Amazon Simple Storage Service - Amazon Simple Storage Service実装・引数解説
サンプルファイルの
AWS REST API(JSON)
がコアなスクリプトとなっており、メインの計算式(#Main Logic
コメントの後ろ)の全文はGitHubに別途テキストファイルで上げてありますのでそちらと合わせてご覧ください。計算式: AWS Signature Version 4.fmfn
ポイントとしては、1つのJSON引数のみで動作するようになっています。
Signatureの作成にはヘッダーやURLの情報がキーになっているので、まずそれらを1つの引数にまとめました。
そうすることで、1ステップでSignatureを生成できるようになっています。引数の中身はこんな感じです。
基本的にはAWSのAPI解説に出てくる名前そのままなので、APIドキュメントを見ながら使用して頂ければ問題ないと思います。$AWS4Options{ # Required Items "service": "string", # "s3" "scheme": "https", # (default: "https") "host": "string", # "s3.amazonmws.com" "path": "string", # String starting with a "/" (default: "/") "query": "string", # "?" Not including (default: "") "region": "string", # "ap-northeast-1" "httpMethod": "GET", # (default: "GET") # Options "httpHeaderJSON": {...} # to curl `--head` option: `{"Content-MD5":"1B2M2Y8AsgTpgAmY7PhCfg==","Content-Type":"text/plain","x-amz-meta-mode":"33188"}` "file": null, # Base64Encoded Object: `Base64EncodeRFC(4648; $file)` (default: null) "createContentSha256HexHeader": true, # When true, "X-Amz-Content-Sha256" will be automatically generated and added to the header. (default: true) "timestampFaceOverride": null, # Force the timestamp to be rewritten For debugging (default: null) "curl--FM-return-container-variable": false, # Script always returns the value as an object (default: false) }Optionsの内容だけ独自仕様なので少し解説します。
- file
ファイルをアップロードする場合などに使用します。JSONにはオブジェクト形式のデータを混ぜられないので、Base64Encodeした文字列を受け付けるようになっています。BodyにJSONなどのテキストを渡す場合はJSONをTextEncode
などでエンコードしファイル化して渡してあげると動作すると思います。- curl--FM-return-container-variable
cURLオプションのFM-return-container-variable
を有効にします。 テキストファイルをダウンロードする際などにオブジェクトとして受け取らないと誤った文字コードで解釈されて文字化けします。ダウンロード時は基本的にはtrue
を指定し、戻り値を適切にTextEncode
を使ってエンコードした方が良いでしょう。デフォルトはfalse
です。- createContentSha256HexHeader
デフォルトでヘッダーにX-Amz-Content-Sha256
を追加する仕様になっています。利用したくない場合にfalse
を指定してください。- timestampFaceOverride
基本的には使うことはないと思いますが、計算式内で使うタイムスタンプを手動で指定したい場合に使用します。デフォルトでは実行時のタイムスタンプを使用します。- httpHeaderJSON
httpヘッダー情報もSignatureを作成する上で必要なため、httpヘッダーに情報を追加する必要がある場合はJSON形式で渡す必要があります。Content-MD5
,Content-Type
,x-amz-meta-mode
をヘッダーに追加する場合は下記のようなJSONをhttpHeaderJSON
に追加してください。$AWS4Options{ #... "httpHeaderJSON": { "Content-MD5":"1B2M2Y8AsgTpgAmY7PhCfg==", "Content-Type":"text/plain", "x-amz-meta-mode":"33188" } }実装の振り返り
細かい実装の解説は膨大になるので今回は省きます。
基本的にはAWSの指定するロジックをFileMakerに書き換えただけです。
「抽象化のためにヘッダー情報などを全部JSON引数で受け取る」と決めたら意外とスッキリ実装できました。細かいバグを潰すのに半日かかってしまいましたが、特別なことは何もないです。
改行コードの変更
改行区切りのデータは基本的には
¶
やList()
を使っていますが、これはAWSの改行コードとは一致しません。FileMakerの改行コードはCR(
Char(13)
)なのに対し、AWSはLF(Char(10)
)です。最初から
Char(10)
を使って改行コードを挟んでもいいのですが、見た目に何しているのかよくわからないのと改行コードが混在すると、バグになりやすいので必要時にだけTextEncode(string; "utf-8"; 3)
で改行コードを変更するようにしました。JSON引数
今更ですが、キーワード引数の代替えとしてJSONが使えるようになったのはとても大いいですね。
今回Base64Encodeを組み合わせることで、オブジェクトのやりとりも問題なく行えることが確認できました(デバッグ中データビュアーを表示して大きいファイルをやりとりすると死にますw)。
引数を作るのが面倒なのでできるだけ使いたくないとか、引数が無限に渡せるから何でもかんでも渡すようにした結果、誰もメンテできないブラックボックスになるなんてことも容易に想像できますが、上限個数をルール化するなどとすれば問題なさそうです。
終わりに
実はサンプルファイルはあまりテストしてません…ちょっと時間がありませんでした。
もしバグってたらコメントかissueください。あ、あと英語がダメダメなので変だったら教えてください!w
参考
- 投稿日:2020-12-08T19:03:34+09:00
短編映画「Hello, world!インセプション」
第1階層 Windows
ファイル名を指定して実行
cmd
Microsoft Windows [Version 10.0.19042.630] (c) 2020 Microsoft Corporation. All rights reserved. C:\Users\user>
>echo "Hello, world!"
C:\Users\user>echo "Hello, world!" "Hello, world!"第2階層 Ubuntu (WSL)
>wsl
C:\Users\user>wsl user@DESKTOP-EPGPMTG:/mnt/wsl/docker-desktop-bind-mounts/Ubuntu-20.04/7272****$
$ echo "Hello, world!"
user@DESKTOP-EPGPMTG:/mnt/wsl/docker-desktop-bind-mounts/Ubuntu-20.04/7272****$ echo "Hello, world!" Hello, world!第3階層 Amazon Linux (EC2)
$ ssh -i "****.pem" ec2-user@ec2-****.us-east-2.compute.amazonaws.com
user@DESKTOP-EPGPMTG:~$ ssh -i "****.pem" ec2-user@ec2-****.us-east-2.compute.amazonaws.com Last login: *** *** * **:**:** 2020 from ****.**.** __| __|_ ) _| ( / Amazon Linux 2 AMI ___|\___|___| https://aws.amazon.com/amazon-linux-2/ [ec2-user@ip-**** ~]$
$ echo Hello, world!
[ec2-user@ip-**** ~]$ echo Hello, world! Hello, world!第4階層 CentOS (Docker)
$ docker run -it centos /bin/bash
[ec2-user@ip-**** ~]$ docker run -it centos /bin/bash [root@**** /]#
# echo "Hello, world!"
[root@**** /]# echo "Hello, world!" Hello, world!第5階層 Python
# yum install python3
# python3
[root@**** /]# yum install python3 [root@**** /]# python3 Python 3.6.8 (default, Apr 16 2020, 01:36:27) [GCC 8.3.1 20191121 (Red Hat 8.3.1-5)] on linux Type "help", "copyright", "credits" or "license" for more information. >>>
>>> print ("Hello, world!")
>>> print ("Hello, world!") Hello, world!脱出
>>> exit() [root@**** /]# exit exit [ec2-user@ip-**** ~]$ exit logout Connection to ec2-****.us-east-2.compute.amazonaws.com closed. user@DESKTOP-EPGPMTG:~$ exit logout C:\Users\user>exitエピローグ
実際の映画(?)ではここで謎の男が出てきてexitと入力すると主人公の存在が消えてしまいます。
あと、一点つまづいた点として、wslがWindowsディレクトリのままいるとssh接続する時にPermissionエラーが出てしまうので、WSLのフォルダに入れた後に
chmod 400
しないといけませんでした。WSLからssh接続する時にLoad key "****.pem": bad permissions. Permission denied (publickey,gssapi-keyex,gssapi-with-mic).
というエラーが出てくる時は参考ページの通りにやると上手くいきます(検索用要約)。
- 投稿日:2020-12-08T18:57:09+09:00
S3のバケットの一部のフォルダの中のファイルを公開する
概要
アプリケーションを作成していて,S3に画像を置いて公開したかった.
既にユーザーからの入力などを保存しておく非公開のバケットを作成していた.
画像を配信するには公開用のバケットを別途作成しなければいけないかと思っていたが,バケットの一部分のファイルのみを公開することができるらしい
これにより1つのアプリケーションで複数のバケットを使用せずに済んだ.
また複数のバケットを作成,管理する必要がなくなった.手順をメモしておく
やったこと
バケットの一部のフォルダの中に入っているファイルのみパブリックアクセス可能にした.
その他のファイルは全て非公開になっている.
やり方
公開したいバケットのページにいき,アクセス許可からブロックパブリックアクセスを編集
デフォルトではパブリックアクセスを全てブロックにチェックがついているが,これを外して4つの項目のうち下2つのチェックを外した状態で変更の保存をする.
バケットポリシーを作成する
バケットのページのアクセス許可からバケットポリシーを編集する
バケットポリシーには以下のjsonを記入する{ "Version":"2012-10-17", "Statement":[ { "Sid":"AddPerm", "Effect":"Allow", "Principal": "*", "Action":["s3:GetObject"], "Resource":["arn:aws:s3:::DOC-EXAMPLE-BUCKET/publicprefix/*"] } ] }ここで
DOC-EXAMPLE-BUCKET
にはバケットの名前,publicprefix
には公開したいフォルダの名前を書く以上で一部のフォルダのみを公開することができる.
試しに公開設定をしたフォルダの中のオブジェクトのオブジェクトURL(https://bucket-name.s3-ap-northeast-1.amazonaws.com/hoge/hoge.png のようなurl.各オブジェクトのページに記載されている)を開くと公開されていることがわかる.
また公開設定していないフォルダの中のオブジェクトのオブジェクトURLを開くとアクセス拒否されるのでこちらはきちんと非公開になっていることがわかる.
参考
AWS公式
https://aws.amazon.com/jp/premiumsupport/knowledge-center/read-access-objects-s3-bucket/
こちらのページにはフォルダ以下を公開するやり方以外にもオブジェクトにタグをつけて特定のタグがついているオブジェクトのみ公開する方法なども紹介されている.
- 投稿日:2020-12-08T18:41:23+09:00
Infrastructure as Codeで実現するAWS MediaConvertによるHLS形式へ自動変換
XTechグループ Advent Calendar 2020の9日目の担当は、エキサイトのLife&Wellness事業部でエンジニアをしている坂本です。
はじめに
米アップルによって開発されたHLSのシェアが近年拡大されています。HLSとは、HTTP Live Streaming(ストリーミングプロトコル)の略で、アップルが自社iOS向けに開発しました。HTTPベースなので、CDNのキャッシュ技術を利用できます。Androidも対応しますので、個人的に動画配信(VOD)サービスを作りたいのなら、HLS技術採用が最適だと思います。
AWSコンソール上にMediaConvertで簡単に動画形式からhls形式に変換できます。今回は以下のような最低限な構成を作ってみたいと思います。
本稿の範囲で完全にInfrastructure as Codeの紹介が難しいので、自分自身が最も悩んでいたLambdaからMediaConvertのジョブ発行を紹介したいと思います。
事前準備(手動で作成)
S3バケット作成
コンソール上に以下の2つバケットを作成します
① input-excite-mediaconvert-bucket:入稿用のバケット
② converted-excite-mediaconvert-bucket:変換済コンテンツ保管バケットIAMロール作成
まず、MediaConvertでジョブ作成のため、ロール付与が必要です。コンソール上にExciteMediaConvertRoleという名前でロールを作成します。設置場所がMediaConvertなので、作成の際にMediaConvert選択になります。
次に、LambdaからMediaConvertのジョブを実行するために、Lambda用のExciteLambdaMediaConvertRoleという名前で作成します。MediaConvert用のロールからのPassRole設定が必要です。
ロール名 設置場所 設定ポリシー 意味 ExciteMediaConvertRole MediaConvert デフォルト値↓
AmazonS3FullAccess
AmazonAPIGatewayInvokeFullAccessジョブ作成時に必要なロール ExciteLambdaMediaConvertRole Lambda AmazonS3FullAccess
AWSElementalMediaConvertFullAccessポリシーにPassRole設定が必要 ExciteLambdaMediaConvertRoleの設定がこのようになります
MediaConvertのジョブ設定をコピーする
AWSコンソール上のMediaConvertからジョブを作成します。ジョブテンプレートを作成して、Lambda上からジョブテンプレートを呼ぶ出す方法もありますが、極力的に手作業を避けたいので、このやり方を使わない。
ジョブ作成の際に最低限で以下の4つ入力が必要です。
- 入力ファイルURL(入稿用バケットのURL)
- 送信先(変換済コンテンツ保管バケットのURL)
- ビットレート:例)「264000」を入力
- 名前修飾子:例)「_hls」を入力ジョブ作成が完了したら、変換済コンテンツ保管バケットに以下のようなファイルが作成されます。
これでHLS形式に変換が成功しましたので、このジョブの設定をコピーしたいと思います。
- AWSコンソール上の実行したジョブの画面から「JSONのエクスポート」ボタンを押して、JSONファイルをダウンロードしておきます。
- JSON内の「Settings」要素だけ抜き出して、AudioDuration要素削除とバケット情報(OutputGroupSettingsのDestinationとInputsのFileInput)の値を削除(その後Lambda側にこの値を書き換えします)して、「job_setting.json」という名前で保存します。AudioDuration要素は、任意の設定で動画とオーディオの再生時間の差がきわめて小さい再パッケージのダウンストリームワークフローで出力が消費される場合にのみ指定します。
例)ジョブ設定
{ "TimecodeConfig": { "Source": "ZEROBASED" }, "OutputGroups": [ { "Name": "Apple HLS", "Outputs": [ { "ContainerSettings": { "Container": "M3U8", "M3u8Settings": { "AudioFramesPerPes": 4, "PcrControl": "PCR_EVERY_PES_PACKET", "PmtPid": 480, "PrivateMetadataPid": 503, "ProgramNumber": 1, "PatInterval": 0, "PmtInterval": 0, "Scte35Source": "NONE", "NielsenId3": "NONE", "TimedMetadata": "NONE", "VideoPid": 481, "AudioPids": [ 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492 ] } }, "VideoDescription": { "ScalingBehavior": "DEFAULT", "TimecodeInsertion": "DISABLED", "AntiAlias": "ENABLED", "Sharpness": 50, "CodecSettings": { "Codec": "H_264", "H264Settings": { "InterlaceMode": "PROGRESSIVE", "NumberReferenceFrames": 3, "Syntax": "DEFAULT", "Softness": 0, "GopClosedCadence": 1, "GopSize": 90, "Slices": 1, "GopBReference": "DISABLED", "SlowPal": "DISABLED", "EntropyEncoding": "CABAC", "Bitrate": 264000, "FramerateControl": "INITIALIZE_FROM_SOURCE", "RateControlMode": "CBR", "CodecProfile": "MAIN", "Telecine": "NONE", "MinIInterval": 0, "AdaptiveQuantization": "AUTO", "CodecLevel": "AUTO", "FieldEncoding": "PAFF", "SceneChangeDetect": "ENABLED", "QualityTuningLevel": "SINGLE_PASS", "FramerateConversionAlgorithm": "DUPLICATE_DROP", "UnregisteredSeiTimecode": "DISABLED", "GopSizeUnits": "FRAMES", "ParControl": "INITIALIZE_FROM_SOURCE", "NumberBFramesBetweenReferenceFrames": 2, "RepeatPps": "DISABLED", "DynamicSubGop": "STATIC" } }, "AfdSignaling": "NONE", "DropFrameTimecode": "ENABLED", "RespondToAfd": "NONE", "ColorMetadata": "INSERT" }, "AudioDescriptions": [ { "AudioTypeControl": "FOLLOW_INPUT", "CodecSettings": { "Codec": "AAC", "AacSettings": { "AudioDescriptionBroadcasterMix": "NORMAL", "Bitrate": 96000, "RateControlMode": "CBR", "CodecProfile": "LC", "CodingMode": "CODING_MODE_2_0", "RawFormat": "NONE", "SampleRate": 48000, "Specification": "MPEG4" } }, "LanguageCodeControl": "FOLLOW_INPUT" } ], "OutputSettings": { "HlsSettings": { "AudioGroupId": "program_audio", "AudioOnlyContainer": "AUTOMATIC", "IFrameOnlyManifest": "EXCLUDE" } }, "NameModifier": "_hls" } ], "OutputGroupSettings": { "Type": "HLS_GROUP_SETTINGS", "HlsGroupSettings": { "ManifestDurationFormat": "INTEGER", "SegmentLength": 10, "TimedMetadataId3Period": 10, "CaptionLanguageSetting": "OMIT", "Destination": "", "TimedMetadataId3Frame": "PRIV", "CodecSpecification": "RFC_4281", "OutputSelection": "MANIFESTS_AND_SEGMENTS", "ProgramDateTimePeriod": 600, "MinSegmentLength": 0, "MinFinalSegmentLength": 0, "DirectoryStructure": "SINGLE_DIRECTORY", "ProgramDateTime": "EXCLUDE", "SegmentControl": "SEGMENTED_FILES", "ManifestCompression": "NONE", "ClientCache": "ENABLED", "AudioOnlyHeader": "INCLUDE", "StreamInfResolution": "INCLUDE" } } } ], "AdAvailOffset": 0, "Inputs": [ { "AudioSelectors": { "Audio Selector 1": { "Offset": 0, "DefaultSelection": "DEFAULT", "ProgramSelection": 1 } }, "VideoSelector": { "ColorSpace": "FOLLOW", "Rotate": "DEGREE_0", "AlphaBehavior": "DISCARD" }, "FilterEnable": "AUTO", "PsiControl": "USE_PSI", "FilterStrength": 0, "DeblockFilter": "DISABLED", "DenoiseFilter": "DISABLED", "InputScanType": "AUTO", "TimecodeSource": "ZEROBASED", "FileInput": "" } ] }Lambdaで変換バッチ作成
AWSコンソール上にLambda関数を作成して、job_setting.jsonも同じところに入れます。
excite-mediaconvert-project/ ├── job_setting.json └── lambda_function.pyLambdaのアクセス権限にExciteLambdaMediaConvertRoleを付与します。
例)Lambdaの中身
import json import boto3 region_name = "ap-northeast-1" endpoint_url = "https://xxxxxxxxx.mediaconvert.ap-northeast-1.amazonaws.com" convert_client = boto3.client("mediaconvert", region_name=region_name, endpoint_url=endpoint_url) input_file = "s3://input-excite-mediaconvert-bucket/input.mp4" output_destination = "s3://converted-excite-mediaconvert-bucket/" mediaconvert_job_role = "arn:aws:iam::xxxxxxxxx:role/ExciteMediaConvertRole" def lambda_handler(event, context): print("==start mediaconvert job==") # ジョブ設定を読み込み with open('job_setting.json') as json_data: job_settings = json.load(json_data) # ジョブ設定の中身を書き換え job_settings['Inputs'][0]['FileInput'] = input_file job_settings['OutputGroups'][0]['OutputGroupSettings']['HlsGroupSettings']['Destination'] = output_destination job = convert_client.create_job( Role=mediaconvert_job_role, Settings=job_settings ) print(f"job={str(job)}")これでLambdaを実行すると、上記と同様に出力用のフォルダーにHLS形式のファイルが作成されています。Printしていますので、興味があればジョブ作成のレスポンスも確認できます。
最後に
如何でしょうか。AWS MediaConvertで簡単にストリーミング形式に変換できます。CloudfrontのCookie認証と組み合わせしたら、簡単にプライベート動画配信サービスを実現できると思います。
今回の記事は単純に大きいな動画ファイルから小さいな動画ファイル(約1MB〜2MBが最適)に分割しますので、コンテンツ保護の観点からあまりよろしくないので、実際のプロジェクトでは暗号化など是非ご検討ください。HLSのいいところはシンプルですが、より強固なコンテンツ保護を実現したいのなら、MPEG-DASHが良いかもしれない。
エキサイト株式会社では随時に仲間を募集しております。
- 投稿日:2020-12-08T18:14:31+09:00
AWS CloufFormationのスタックをAWS CLIでリスト形式にして出力し、古いものを削除するコマンド
基本構文
aws cloudformation describe-stacks --region ap-northeast-1 --profile profile
変数化
REGION=ap-northeast-1 PROFILE=profile aws cloudformation describe-stacks --region ${REGION} --profile ${PROFILE}作成順で並び替え
REGION=ap-northeast-1 PROFILE=profile aws cloudformation describe-stacks --region ${REGION} --profile ${PROFILE} | jq -r '.Stacks | sort_by(.CreationTime)'スタック名と作成日を抜き出す
REGION=ap-northeast-1 PROFILE=profile aws cloudformation describe-stacks --region ${REGION} --profile ${PROFILE} | jq -r '.Stacks | .[] | [.StackName, .CreationTime]'スタック名で特定の文字列が含まれているもの(先頭がooで始まるもの)
下記を入れる
select(.StackName | test("^'${PROJECT_HEADER}'")コマンド
REGION=ap-northeast-1 PROFILE=profile PREFIX=my-stack- aws cloudformation describe-stacks --region ${REGION} --profile ${PROFILE} | jq -r '.Stacks | .[] | select(.StackName | test("^'${PREFIX}'")) | .StackName'スタック名が特定のスタック名以外(masterやdevelop以外のスタックを表示)
(contains("develop") | not)REGION=ap-northeast-1 PROFILE=profile aws cloudformation describe-stacks --region ${REGION} --profile ${PROFILE} | jq -r '.Stacks | .[] | select(.StackName |(contains("develop") | not) and (contains("master") | not) ) | .StackName'特定の件数を表示
下記のようにすると10件目以降を表示する。
.['10':][]
sort_by(.CreationTime)
と組み合わせると最新10件を表示する。(10件目以降の古いスタックを表示)REGION=ap-northeast-1 PROFILE=profile LIMIT=10 aws cloudformation describe-stacks --region ${REGION} --profile ${PROFILE} | jq -r '.Stacks | sort_by(.CreationTime) | .['${LIMIT}':][] | .StackName'古いスタックをすべて表示
これまでのものを組み合わせて利用。
REGION=ap-northeast-1 PROFILE=profile LIMIT=10 PREFIX=my-stack aws cloudformation describe-stacks --region ${REGION} --profile ${PROFILE} | jq -r '.Stacks | sort_by(.CreationTime) | .['${LIMIT}':][] | { 'StackName': .StackName, 'CreationTime': .CreationTime } | select(.StackName | test("^'${PREFIX}'") and (contains("develop") | not) and (contains("master") | not) ) | .StackName'古いスタックを削除する
delete.shstacks=$(aws cloudformation describe-stacks --region ${REGION} --profile ${PROFILE} | jq -r '.Stacks | sort_by(.CreationTime) | .['${LIMIT}':][] | { 'StackName': .StackName, 'CreationTime': .CreationTime } | select(.StackName | test("^'${PREFIX}'") and (contains("develop") | not) and (contains("master") | not) ) | .StackName') for STACK in ${stacks} do read -n1 -p "${STACK} : delete? (y/N):" yn echo "" case "$yn" in [yY]*) echo "Deleting ${BUCKET_NAME} ..."; aws cloudformation delete-stack --stack-name ${STACK} --region ${REGION} --profile ${PROFILE}; echo "done" ;; *) continue ;; esac done
- 投稿日:2020-12-08T18:13:33+09:00
【AWS】デプロイ手順備忘録
概要
- VPCの作成
- サブネットの作成(パブリックのみ)
- インターネットゲートウェイの設定
- ルートテーブルの設定
- セキュリティーグループの作成
- DBの設置(今回は設置しない)
- EC2インスタンスの作成
- Elastic IPの設定
- デプロイ作業
VPCの作成
- サイダーは推奨の/16で作成。
IAMロールの権限設定
とCloudWatchの料金アラート設定
を怠らないこと。サブネットの作成
- サイダーは推奨の/24で作成。
- AZを1aに設定。
インターネットゲートウェイの設定
- VPCへのアタッチを忘れずに!
ルートテーブルの設定
- フルオープン
- サブネットへのアタッチを忘れずに!
セキュリティグループの作成
- インバウンド:SSH, HTTP, HTTPS
- アウトバウンド:フルオープン
DBの設置
- RDBは設置ぜず、EC2インスタンス内に設置する。
- RDBは有料かつ
Auroraは非常に高額
なので安易に設置しない。EC2インスタンスの作成
- 無料枠内で作成
Elastic IPの設定
アタッチしていない場合には料金が発生
するので注意。デプロイ作業
EC2インスタンスの初期設定
- AWS CLIのダウンロード
- インスタンスへアクセス
% mv pemファイルのディレクトリを記述 ~/.ssh % cd .ssh/ .ssh % chmod 400 ~/.ssh/pemファイル名.pem .ssh % ssh -i pemファイル名.pem ec2-user@パブリックIPを記述 .ssh % ssh -i ~/.ssh/pemファイル名.pem
- 操作用ユーザーの作成(下記AWS公式ページ前段参照) https://aws.amazon.com/jp/premiumsupport/knowledge-center/new-user-accounts-linux-instance/
- インスタンスの初期設定
- いろいろな方々の資料を参考にしました。必要に応じて取捨選択可能なようです。
[ec2-user|~]$ sudo yum install \ git make gcc-c++ patch \openssl-devel \ libyaml-devel libffi-devel libicu-devel \libxml2 libxslt libxml2-devel libxslt-devel \zlib-devel readline-devel \ mysql mysql-server mysql-devel \ImageMagick ImageMagick-devel \epel-release
- sshを用いたインスタンスへのアクセス(下記AWS公式ページ後段参照) https://aws.amazon.com/jp/premiumsupport/knowledge-center/new-user-accounts-linux-instance/
- 以降、必要に応じてミドルウェア等をダウンロードする。
- secret_key_baseの設定
注意事項
- パブリック IPv4 DNSが表示されていない場合はVPCの設定を修正すること。
- インスタンスに繋がらない場合はセキュリティグループのSSHインバウンドに誤りがないか確認。
個人的なもの
- データベーステーブルのValueが
latin1
になっていないか特に注意。また、[client] default-character-set=utf8 [mysql] default-character-set=utf8の順番で書かないとなぜかDBにログインできないので注意。- DBはDokerとAWSで記述を変更する必要がある。 忘れずに。
default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password socket: /tmp/mysql.sock # docker用 # host: db
- 投稿日:2020-12-08T17:53:02+09:00
EFSをECS Fargateにマウントする定義をCFnで書きながら理解する
はじめに
2020年4月にAWS Fargateのプラットフォームバージョン1.4.0がリリースされ、それに伴い、ECS FargateからAmazon Elastic Filesystem(以下、EFS)が利用できるようになりました。今回これを必要とする要件があり、EFSを初めて利用することになりました。そのため、基本的な概念を整理するために記事にしました。
今回の進め方として、CloudFormation(以下、CFn)で設定が必要な要素を確認しつつ、不明点があればドキュメントを読んで理解していったので、本記事でもCFnの定義をみながらEFSの要素についてみていきたいと思います。最終的な構成
これから順を追って各コンポーネントについてみていきますが、今回の最終的な構成としては以下のようになります。
サブネットは二つで、それぞれのサブネットにECS Fargate上でタスクが稼働しています。そして、それぞれのタスクからEFSをマウントするという構成です。
CFnの定義
1. ECSの作成
まずはECS FargateをCloudFormationで定義していきます。構成図でいうと以下の赤枠の部分になります。
CloudFormationでは以下のようになります。この段階ではEFSに絡む定義はまだ出てきません。
尚、今回直接関連しないパラメータやサブネットなどの一部のリソースについては省略させていただいております。ECSService: Type: 'AWS::ECS::Service' Properties: Cluster: !Ref ECSCluster DesiredCount: 2 LaunchType: 'FARGATE' LoadBalancers: - ContainerName: 'api' ContainerPort: 80 TargetGroupArn: !Ref ElasticLoadBalancingV2TargetGroupExternal NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: 'DISABLED' SecurityGroups: - !Ref EC2SecurityGroupAPI Subnets: - !Ref EC2SubnetPrivateAppAZ1 - !Ref EC2SubnetPrivateAppAZ2 PlatformVersion: '1.4.0' TaskDefinition: !Ref ECSTaskDefinition ECSTaskDefinition: Type: 'AWS::ECS::TaskDefinition' Properties: Family: efs-test RequiresCompatibilities: - 'FARGATE' Cpu: 1024 Memory: 2048 NetworkMode: 'awsvpc' ExecutionRoleArn: !GetAtt IAMRoleECSTaskExecution.Arn TaskRoleArn: !GetAtt IAMRoleAPI.Arn ContainerDefinitions: - Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryAPI}:latest Name: 'api' Cpu: 512 MemoryReservation: 1024 - ContainerPort: 8080 HostPort: 80 Protocol: 'tcp' LogConfiguration: LogDriver: 'awslogs' Options: awslogs-group: '/ecs/efs-test' awslogs-region: !Ref AWS::Region awslogs-stream-prefix: 'ecs' awslogs-create-group: true Essential: true IAMRoleAPI: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: 'Allow' Principal: Service: 'ecs-tasks.amazonaws.com' Action: 'sts:AssumeRole' ManagedPolicyArns: - 'arn:aws:iam::aws:policy/AWSOpsWorksCloudWatchLogs'2. EFS Filesystemの作成
次にEFSファイルシステムを作成していきます。構成図では以下の箇所になります。
ここではバックアップや暗号化の有無、パフォーマンスモード、スループットモードを設定していきます。
EFSFileSystem: Type: 'AWS::EFS::FileSystem' Properties: BackupPolicy: Status: 'ENABLED' Encrypted: true FileSystemTags: - Key: 'Name' Value: !Sub 'efs-filesystem' FileSystemPolicy: Version: '2012-10-17' Statement: - Effect: 'Allow' Action: - 'elasticfilesystem:ClientWrite' - 'elasticfilesystem:ClientMount' Principal: AWS: !GetAtt IAMRoleAPI.Arn LifecyclePolicies: - TransitionToIA: AFTER_30_DAYS PerformanceMode: 'generalPurpose' ThroughputMode: 'bursting'注意点としては、パフォーマンスモードはgeneralPurpose、maxIOから選択可能なのですが、ファイルシステムの作成後は変更できない点です。一般的なファイルサービスとして利用する場合はgeneralPurpose、ビッグデータ解析などのワークロードの場合にmaxIOを選択すると良いと思います。
スループットモードはバーストかプロビジョニングを選択できます。バーストモードは短期間で高いスループットが必要になり、残りの時間は低いスループットで良いワークロードが想定されています。
バーストモードはベースラインとクレジットという概念を抑えておく必要があります。EFSのスループットにはベースラインが設定されており、これを超えるスループットを必要とする場合、クレジットを消費してスループットをバーストすることができます。クレジットを使い切った場合は当然バーストすることはできないため、ベースラインでは不足している場合でもベースラインのスループットのままとなります。クレジットは時間と共に蓄積されていきます。ベースラインのスループットはディスク容量を増やすことであげることができます。バーストモードではクレジットが常に枯渇するようなワークロードの場合はプロビジョニングを検討します。プロビジョニングは必要なスループットを指定することで、そのスループットを維持できます。ただし、ベースラインのスループットを超えてプロビジョニングされたスループット分に別途課金が発生します。ここで発生する課金は安くないため注意が必要です。
EFSのパフォーマンスについてはこちらのドキュメントにまとめられています。
また、ここで重要な設定としてFileSystemPolicyがあります。これはこのファイルシステムに対してリソースベースのアクセスを制御することが可能になります。今回はECS FargateのIAMRoleからマウント、読み込み、書き込みのアクセスを許可しています。
3. EFS MountTargetの作成
次にEFS マウントターゲットを作成していきます。構成図では以下の箇所になります。
マウントターゲットはEFSにアクセスためのリソースになります。これは耐障害性の観点からAZに一つずつ作成することが推奨されています。AZごとに作成できるマウントターゲットは一つだけのため、一つのAZに複数のサブネットが含まれる場合もマウントターゲットを作成するサブネットを一つ選択して作成します。他のサブネットからは同じAZ内のサブネットに作られたマウントターゲットを利用できます。
また、マウントターゲットにはセキュリティグループをアタッチできるため、EFSのファイアウォールとしての役割も担います。セキュリティグループはport 2049からのアクセスを許可することでアクセス可能になります。EFSMountTargetPrivateAppAZ1: Type: 'AWS::EFS::MountTarget' Properties: FileSystemId: !Ref EFSFileSystem SubnetId: !Ref EC2SubnetPrivateAppAZ1 SecurityGroups: - !Ref EC2SecurityGroupEFS EFSMountTargetPrivateAppAZ2: Type: 'AWS::EFS::MountTarget' Properties: FileSystemId: !Ref EFSFileSystem SubnetId: !Ref EC2SubnetPrivateAppAZ2 SecurityGroups: - !Ref EC2SecurityGroupEFS EC2SecurityGroupEFS: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: 'efs-filesystem securitygroup' SecurityGroupIngress: - IpProtocol: tcp FromPort: 2049 ToPort: 2049 SourceSecurityGroupId: !Ref EC2SecurityGroupAPI Tags: - Key: 'Name' Value: 'efs-filesystem' VpcId: !Ref EC2VPC4. EFS AccessPointの作成
次にアクセスポイントを作成していきます。構成図では以下の箇所になります。
CloudFormationの定義としては以下のようになります。
EFSAccessPoint: Type: 'AWS::EFS::AccessPoint' Properties: FileSystemId: !Ref EFSFileSystem PosixUser: Uid: "1001" Gid: "1001"私はこのアクセスポイントを間違って理解してしまい、少しハマりました。ドキュメントには以下のように記載されています。
アクセスポイントを使用すると、アクセスポイントを介したすべてのファイルシステム要求に対してユーザー ID (ユーザーの POSIX グループなど) を適用できます。
ここでいう「適用」が指す意味を「適用するために上書き」してくれるものと勘違いし、それに沿った設定をしたところ、EFSファイルシステムにアクセスする際に
Permission Deny
が発生しました。挙動を確認したところ、おそらくここは「適用するために指定されたUID、GID以外を弾く」ことを指すようです。
そのため、NFSクライアント側でUID、GIDを設定する必要があります。ECSのタスク定義 or Dockerで特にUID、GIDを指定しない場合はデフォルトでrootユーザーでのアクセスとなるため、同じくPermission Deny
が発生します。回避策としてはファイルシステムポリシーに「elasticfilesystem:ClientRootAccess」を設定する方法があります。ただしこれはno_root_squashを設定するのと同等のリスクが生まれるため、リスクを考慮して設定するか判断する必要があります。5. ECS FargateからEFSをマウントする設定
ここまででEFS側の設定は完了しているので、「 1. ECSの作成」で作成したECSのタスク定義を編集し、EFSをマウントしていきます。
設定箇所としては2箇所になります。
1. Volumeを定義してECSからEFSを認識させる設定
2. ボジュームをタスクからマウントする設定ECSTaskDefinition: Type: 'AWS::ECS::TaskDefinition' Properties: Family: efs-test RequiresCompatibilities: - 'FARGATE' Cpu: 1024 Memory: 2048 NetworkMode: 'awsvpc' ExecutionRoleArn: !GetAtt IAMRoleECSTaskExecution.Arn TaskRoleArn: !GetAtt IAMRoleAPI.Arn #### ここを追加する。1.に該当する。 Volumes: - Name: 'efs-filesystem' EFSVolumeConfiguration: AuthorizationConfig: AccessPointId: !Ref EFSAccessPoint IAM: 'ENABLED' FilesystemId: !Ref EFSFileSystem TransitEncryption: 'ENABLED' #### ここを追加する。1.に該当する。 ContainerDefinitions: - Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryAPI}:latest Name: 'api' Cpu: 512 MemoryReservation: 1024 - ContainerPort: 8080 HostPort: 80 Protocol: 'tcp' LogConfiguration: LogDriver: 'awslogs' Options: awslogs-group: '/ecs/efs-test' awslogs-region: !Ref AWS::Region awslogs-stream-prefix: 'ecs' awslogs-create-group: true #### ここを追加する。2.に該当する。 MountPoints: - SourceVolume: 'efs-filesystem' ContainerPath: '/working' #### ここを追加する。2.に該当する。 Essential: true以上の設定により、ECS FargateからEFSを利用可能になります。
最後に
EFSを初めて使ってみて、アクセス制御の選択肢が豊富だなという感想を抱きました。
EFSをECSにマウントするCloudFormationの定義は意外と落ちていなかったりするので、参考になれば幸いです。
- 投稿日:2020-12-08T17:01:09+09:00
Amazon SageMaker Pipelines を実際に使ってみた【前編】[re:Invent 2020]
はじめに
この記事は株式会社ナレッジコミュニケーションが運営する Amazon AI by ナレコム Advent Calendar 2020 の 8日目にあたる記事になります。
AWS が開催する re:Invent 2020 で発表された Amazon SageMaker の新機能である Amazon SageMaker Pipelines を実際に触ってみました。
Amazon SageMaker Pipelines
Amazon SageMaker Pipelines はエンドツーエンドの機械学習ワークフローを管理するための CI/CD サービスです。
Python SageMaker SDK を使用して JSON 形式のパイプラインを定義し、SageMaker studio で視覚的に管理することができます。
機械学習のワークフローには、探索的データ分析やアルゴリズム・パラメーターの設定を行って、トレーニングして本番環境にデプロイというステップがあります。パイプラインの定義部分はコーディングが必要ですが、一度作ってしまえば SageMaker studio 上で実行履歴なども確認できるため、パイプライン開発が爆速になること間違いなしです。
今回はパイプラインの定義の部分を解説していきます。
また本記事では UCI の アワビの年齢データセットを使って SageMaker Pipelines を実際に使ってみます。
こちらの github を参考にしてパイプラインを定義し、生成した JSON ファイルを SageMaker Studio 上で読み込んでいきます。パイプラインの定義
SageMaker Pipelines では JSON 形式の有効巡回グラフ(DAG)として定義します。定義するには SageMaker Python SDK を使用します。
定義する内容としては
- フィーチャーエンジニアリングステップ(AbaloneProcess)
- トレーニングステップ(AbaloneTrain)
- モデル評価ステップ(AbaloneEval)
- バッチ変換用モデル作成ステップ(AbaloneCreateModel)
- バッチ変換モデル実行ステップ(AbaloneTransform)
- モデルパッケージ作成ステップ(AbaloneRegisterModel)
- モデル検証条件定義ステップ(AbaloneMSECond)
をそれぞれ定義していきます。
完成する DAG は次のようになります。(最終的に SageMaker Studio 上で見れるものです)今回は SageMaker ノートブックインスタンスを使ってパイプライン定義を作っていきます。
セットアップとデータの準備
まず環境のセットアップを行います。SageMaker セッションを作成します。
import boto3 import sagemaker # SageMaker セッションの作成 region = boto3.Session().region_name sagemaker_session = sagemaker.session.Session() role = sagemaker.get_execution_role() default_bucket = sagemaker_session.default_bucket() model_package_group_name = f"AbaloneModelPackageGroupName"デフォルトのバケットにデータをアップロードします。
# ディレクトリ作成 !mkdir -p data # パス指定 local_path = "data/abalone-dataset.csv" # アワビデータのダウンロード s3 = boto3.resource("s3") s3.Bucket(f"sagemaker-servicecatalog-seedcode-{region}").download_file( "dataset/abalone-dataset.csv", local_path ) # データを自分のバケットにアップロード base_uri = f"s3://{default_bucket}/abalone" input_data_uri = sagemaker.s3.S3Uploader.upload( local_path=local_path, desired_s3_uri=base_uri, ) print(input_data_uri)次にモデル作成後のバッチ変換用のデータをアップロードします。
# パス指定 local_path = "data/abalone-dataset-batch" # バッチ用アワビデータのダウンロード s3 = boto3.resource("s3") s3.Bucket(f"sagemaker-servicecatalog-seedcode-{region}").download_file( "dataset/abalone-dataset-batch", local_path ) # データを自分のバケットにアップロード base_uri = f"s3://{default_bucket}/abalone" batch_data_uri = sagemaker.s3.S3Uploader.upload( local_path=local_path, desired_s3_uri=base_uri, ) print(batch_data_uri)ここまででパイプラインに流していくためのデータの準備が完了しました。ここからは実際にパイプラインの中身を作っていきます。
パイプラインパラメータの定義
パイプラインパラメータを定義します。これによってパイプライン定義を変更せずにカスタムパイプラインの実行とスケジュール実行ができるようになります。
from sagemaker.workflow.parameters import ( ParameterInteger, # python の int 型 ParameterString, # python の str 型 ) # 処理ジョブのインスタンス数 processing_instance_count = ParameterInteger( name="ProcessingInstanceCount", default_value=1 ) # 処理ジョブの ml.* インスタンスタイプ processing_instance_type = ParameterString( name="ProcessingInstanceType", default_value="ml.m5.xlarge" ) # トレーニングジョブの ml.* training_instance_type = ParameterString( name="TrainingInstanceType", default_value="ml.m5.xlarge" ) # 学習したモデルを CI/CD 目的のために登録する承認ステータス model_approval_status = ParameterString( name="ModelApprovalStatus", default_value="PendingManualApproval" ) # 入力データの S3 バケット URI input_data = ParameterString( name="InputData", default_value=input_data_uri, ) # バッチデータの S3 バケット URI batch_data = ParameterString( name="BatchData", default_value=batch_data_uri, )フィーチャーエンジニアリングステップ(AbaloneProcess)
パイプラインの最初の処理を定義していきます。こちらの処理の内容となる preprocessing.py というファイルを作成します。
流れとしては読み込んだデータに対して
- 連続値のカラム(「性別」カラム以外)に対する欠損値の処理、スケーリング処理
- 離散値のカラム(「性別」カラム)に対する欠損値の処理、エンコーディング処理
をそれぞれ行い、データをシャッフルして
- トレーニング用データ
- 検証用データ
- テスト用データ
にそれぞれ分割し、出力します。
# ディレクトリ作成 !mkdir -p abaloneファイルを作成して処理内容を書いていきます。
%%writefile abalone/preprocessing.py import argparse import os import requests import tempfile import numpy as np import pandas as pd from sklearn.compose import ColumnTransformer from sklearn.impute import SimpleImputer from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler, OneHotEncoder # csv ファイルのカラム指定 feature_columns_names = [ "sex", "length", "diameter", "height", "whole_weight", "shucked_weight", "viscera_weight", "shell_weight", ] label_column = "rings" # 説明変数のデータ型 feature_columns_dtype = { "sex": str, "length": np.float64, "diameter": np.float64, "height": np.float64, "whole_weight": np.float64, "shucked_weight": np.float64, "viscera_weight": np.float64, "shell_weight": np.float64 } # 目的変数のデータ型 label_column_dtype = {"rings": np.float64} # データ型更新用(説明変数、目的変数連結用)関数 def merge_two_dicts(x, y): z = x.copy() z.update(y) return z # 直接呼び出し時に実行 if __name__ == "__main__": base_dir = "/opt/ml/processing" # csv ファイルの読み込み df = pd.read_csv( f"{base_dir}/input/abalone-dataset.csv", header=None, names=feature_columns_names + [label_column], dtype=merge_two_dicts(feature_columns_dtype, label_column_dtype) ) # 連続値カラムに対する前処理 numeric_features = list(feature_columns_names) numeric_features.remove("sex") numeric_transformer = Pipeline( steps=[ ("imputer", SimpleImputer(strategy="median")), ("scaler", StandardScaler()) ] ) # 離散値カラムに対する前処理 categorical_features = ["sex"] categorical_transformer = Pipeline( steps=[ ("imputer", SimpleImputer(strategy="constant", fill_value="missing")), ("onehot", OneHotEncoder(handle_unknown="ignore")) ] ) # 前処理をまとめる preprocess = ColumnTransformer( transformers=[ ("num", numeric_transformer, numeric_features), ("cat", categorical_transformer, categorical_features) ] ) # ラベル用カラム(目的変数)を落として前処理を実行 y = df.pop("rings") X_pre = preprocess.fit_transform(df) y_pre = y.to_numpy().reshape(len(y), 1) # 処理したデータの連結 X = np.concatenate((y_pre, X_pre), axis=1) # シャッフルして分割 np.random.shuffle(X) train, validation, test = np.split(X, [int(.7*len(X)), int(.85*len(X))]) # データフレームの出力 pd.DataFrame(train).to_csv(f"{base_dir}/train/train.csv", header=False, index=False) pd.DataFrame(validation).to_csv(f"{base_dir}/validation/validation.csv", header=False, index=False) pd.DataFrame(test).to_csv(f"{base_dir}/test/test.csv", header=False, index=False)各ステップはそれぞれステップ定義用のインスタンスに内容を渡して定義します。フィーチャーエンジニアリングのステップは SKLearnProcessor インスタンスに記述して、ProcessingStep に渡します。(ProcessingStep インスタンスはこの後で作ります)
from sagemaker.sklearn.processing import SKLearnProcessor # フレームワークバージョンも指定できます framework_version = "0.23-1" # SKLearnProcessor インスタンスの作成 sklearn_processor = SKLearnProcessor( framework_version=framework_version, instance_type=processing_instance_type, instance_count=processing_instance_count, base_job_name="sklearn-abalone-process", role=role, )フィーチャーエンジニアリング用の ProcessingStep インスタンスを作成し、インプットデータやアウトプットデータや先ほど作成した前処理のコードなどを指定して、ステップを定義します。
from sagemaker.processing import ProcessingInput, ProcessingOutput from sagemaker.workflow.steps import ProcessingStep # フィーチャーエンジニアリングステップの定義 step_process = ProcessingStep( name="AbaloneProcess", processor=sklearn_processor, inputs=[ ProcessingInput(source=input_data, destination="/opt/ml/processing/input"), ], outputs=[ ProcessingOutput(output_name="train", source="/opt/ml/processing/train"), ProcessingOutput(output_name="validation", source="/opt/ml/processing/validation"), ProcessingOutput(output_name="test", source="/opt/ml/processing/test") ], code="abalone/preprocessing.py", )トレーニングステップ(AbaloneTrain)
XGBoost でモデルトレーニングを行うステップを作成していきます。
from sagemaker.estimator import Estimator # モデルパスとイメージ URI の指定 model_path = f"s3://{default_bucket}/AbaloneTrain" image_uri = sagemaker.image_uris.retrieve( framework="xgboost", region=region, version="1.0-1", py_version="py3", instance_type=training_instance_type, ) # XGBoost コンテナの呼びだし xgb_train = Estimator( image_uri=image_uri, instance_type=training_instance_type, instance_count=1, output_path=model_path, role=role, ) # ハイパーパラメータの設定 xgb_train.set_hyperparameters( objective="reg:linear", num_round=50, max_depth=5, eta=0.2, gamma=4, min_child_weight=6, subsample=0.7, silent=0 )処理内容を TrainingStep インスタンスに渡してトレーニングステップ(AbaloneTrain)を定義します。
from sagemaker.inputs import TrainingInput from sagemaker.workflow.steps import TrainingStep # トレーニングステップの定義 step_train = TrainingStep( name="AbaloneTrain", estimator=xgb_train, inputs={ "train": TrainingInput( s3_data=step_process.properties.ProcessingOutputConfig.Outputs[ "train" ].S3Output.S3Uri, content_type="text/csv" ), "validation": TrainingInput( s3_data=step_process.properties.ProcessingOutputConfig.Outputs[ "validation" ].S3Output.S3Uri, content_type="text/csv" ) }, )モデル評価ステップ(AbaloneEval)
モデル評価ステップを作成するためにモデル評価処理を記述する evaluation.py ファイルを作成します。XGBoost を使って
- モデルのロード
- テストデータの読み込み
- テストデータに対する予測の発行
- 評価指標(適合率・再現率・F1スコアなど)レポートの作成
- 評価レポートの保存
を行います。
%%writefile abalone/evaluation.py import json import pathlib import pickle import tarfile import joblib import numpy as np import pandas as pd import xgboost from sklearn.metrics import mean_squared_error # 直接読み込み時に実行 if __name__ == "__main__": # モデルパス指定 model_path = f"/opt/ml/processing/model/model.tar.gz" # tar ファイルの展開 with tarfile.open(model_path) as tar: tar.extractall(path=".") # モデルのロード model = pickle.load(open("xgboost-model", "rb")) # テストデータのデータフレーム作成 test_path = "/opt/ml/processing/test/test.csv" df = pd.read_csv(test_path, header=None) # DMatrix に整形 y_test = df.iloc[:, 0].to_numpy() df.drop(df.columns[0], axis=1, inplace=True) X_test = xgboost.DMatrix(df.values) # テスト実行 predictions = model.predict(X_test) # メトリクスの定義 mse = mean_squared_error(y_test, predictions) std = np.std(y_test - predictions) report_dict = { "regression_metrics": { "mse": { "value": mse, "standard_deviation": std }, }, } # アウトプット用ディレクトリの作成 output_dir = "/opt/ml/processing/evaluation" pathlib.Path(output_dir).mkdir(parents=True, exist_ok=True) # 評価レポートのファイルの作成 evaluation_path = f"{output_dir}/evaluation.json" with open(evaluation_path, "w") as f: f.write(json.dumps(report_dict))ScriptProcessor インスタンスを作成します。(後で ProcessingStep に渡します)
from sagemaker.processing import ScriptProcessor # ScriptProcessor インスタンスの作成 script_eval = ScriptProcessor( image_uri=image_uri, command=["python3"], instance_type=processing_instance_type, instance_count=1, base_job_name="script-abalone-eval", role=role, )処理内容を ProcessingStep に渡してモデル評価ステップを定義します。処理ステップの内容をレポートに格納できるようにもしておきます。
from sagemaker.workflow.properties import PropertyFile # 処理ステップの内容をレポートに格納するための設定 evaluation_report = PropertyFile( name="EvaluationReport", output_name="evaluation", path="evaluation.json" ) # モデル評価ステップの定義 step_eval = ProcessingStep( name="AbaloneEval", processor=script_eval, inputs=[ ProcessingInput( source=step_train.properties.ModelArtifacts.S3ModelArtifacts, destination="/opt/ml/processing/model" ), ProcessingInput( source=step_process.properties.ProcessingOutputConfig.Outputs[ "test" ].S3Output.S3Uri, destination="/opt/ml/processing/test" ) ], outputs=[ ProcessingOutput(output_name="evaluation", source="/opt/ml/processing/evaluation"), ], code="abalone/evaluation.py", property_files=[evaluation_report], )バッチ変換用モデル作成ステップ(AbaloneCreateModel)
バッチ変換を行うためのモデルを作成するステップを定義します。 S3ModelArtifacts から SageMaker モデルを作成します。
from sagemaker.model import Model # SageMaker モデルの作成 model = Model( image_uri=image_uri, model_data=step_train.properties.ModelArtifacts.S3ModelArtifacts, sagemaker_session=sagemaker_session, role=role, )モデル入力を定義して、作成した SageMaker モデルとともに CreateModelStep に渡してバッチ変換用モデル作成ステップ(AbaloneCreateModel)を定義します。
from sagemaker.inputs import CreateModelInput from sagemaker.workflow.steps import CreateModelStep # モデル入力の定義 inputs = CreateModelInput( instance_type="ml.m5.large", accelerator_type="ml.eia1.medium", ) # バッチ変換用モデル作成ステップの定義 step_create_model = CreateModelStep( name="AbaloneCreateModel", model=model, inputs=inputs, )バッチ変換モデル実行ステップ(AbaloneTransform)
モデルのトレーニング後にバッチ用データに対する変換処理を実行するステップを定義します。適切なコンピューティングインスタンス、インスタンスカウント及びアウトプット先の S3 バケットを指定して Transformer インスタンスを作成します。
from sagemaker.transformer import Transformer # Transformer インスタンスの定義 transformer = Transformer( model_name=step_create_model.properties.ModelName, instance_type="ml.m5.xlarge", instance_count=1, output_path=f"s3://{default_bucket}/AbaloneTransform" )TransformStep インスタンスに先ほど作成した Transformer インスタンスとバッチデータの S3 バケットの URI を渡してバッチ変換モデル実行ステップ(AbaloneTransform)を定義します。
from sagemaker.inputs import TransformInput from sagemaker.workflow.steps import TransformStep # バッチ変換モデル実行ステップ(AbaloneTransform) step_transform = TransformStep( name="AbaloneTransform", transformer=transformer, inputs=TransformInput(data=batch_data) )モデルパッケージ作成ステップ(AbaloneRegisterModel)
トレーニングステップで指定された Estimator インスタンスを使用して RegisterModel インスタンスを作成します。パイプラインで RegisterModel を実行した結果をモデルパッケージといいます。モデルパッケージには推論に必要な全ての要素をパッケージ化した再利用可能なモデルアーティファクトの抽象化されたもの、というイメージです。
ちなみに model_package_group というのはモデルパッケージの集合体で、パイプラインの実行ごとに新しいバージョンとモデルパッケージがこのグループに追加されていきます。from sagemaker.model_metrics import MetricsSource, ModelMetrics from sagemaker.workflow.step_collections import RegisterModel # モデルのメトリクスの読み込み model_metrics = ModelMetrics( model_statistics=MetricsSource( s3_uri="{}/evaluation.json".format( step_eval.arguments["ProcessingOutputConfig"]["Outputs"][0]["S3Output"]["S3Uri"] ), content_type="application/json" ) ) # RegisterModel の定義 step_register = RegisterModel( name="AbaloneRegisterModel", estimator=xgb_train, model_data=step_train.properties.ModelArtifacts.S3ModelArtifacts, content_types=["text/csv"], response_types=["text/csv"], inference_instances=["ml.t2.medium", "ml.m5.xlarge"], transform_instances=["ml.m5.xlarge"], model_package_group_name=model_package_group_name, approval_status=model_approval_status, model_metrics=model_metrics, )モデル検証条件定義ステップ(AbaloneMSECond)
SageMaker Pipelines では条件ステップを定義して、モデルの評価指標がある閾値を超えたときにモデル登録する、という動きも組み込むことができます。
from sagemaker.workflow.conditions import ConditionLessThanOrEqualTo from sagemaker.workflow.condition_step import ( ConditionStep, JsonGet, ) # モデル評価ステップの出力に対する条件定義と mse の読み込み cond_lte = ConditionLessThanOrEqualTo( left=JsonGet( step=step_eval, property_file=evaluation_report, json_path="regression_metrics.mse.value", ), right=6.0 ) # 条件通過した際の処理 step_cond = ConditionStep( name="AbaloneMSECond", conditions=[cond_lte], if_steps=[step_register, step_create_model, step_transform], else_steps=[], )パイプラインの作成
これまでに作成した全てのステップを結合します。
from sagemaker.workflow.pipeline import Pipeline # AbalonePipeline パイプラインの作成 pipeline_name = f"AbalonePipeline" pipeline = Pipeline( name=pipeline_name, parameters=[ processing_instance_type, processing_instance_count, training_instance_type, model_approval_status, input_data, batch_data, ], steps=[step_process, step_train, step_eval, step_cond], )おわりに
次回は定義したパイプラインを SageMaker Studio 上に表示したり実行履歴のトラッキングをしていきたいと思います!
- 投稿日:2020-12-08T16:02:24+09:00
AWSマルチアカウント運用時の脅威検出の導入
こんにちは、Hamee株式会社でSREとして働いています。大嶋です。
普段はNextEngineのクラウド化案件を担当しています。この記事はHamee Advent Calendar 2020の9日目になります。
また、前職でテックブログ執筆のために準備していた記事を修正し公開しています。
(公開のタイミングを見失っていたためこのタイミングで公開させてください)
そのためやってみた記事と、今後弊社で個人的に取り入れていきたい仕組みの紹介(提案)となります。それではAWSマルチアカウント運用時の脅威検出の導入の取り組みについて、紹介させて頂きます。
目次
- 概要
- 課題
- 利用AWSサービス説明
- CloudFormationStackSets
- GuardDuty
- 全体イメージ
CloudFormationStackSets
- 必要なリソース、用途
- 構築手順
GuardDuty
- 必要なリソース、用途
- 構築手順
Slack通知
- 必要なリソース、用途
- 構築手順
- AWS Chatbotとの比較
まとめ
概要
AWSアカウントを複数所有していて、プロダクト、組織、用途(Dev、Stg、Prod)などで分割され、AWS Organizationsを利用しマルチアカウントを管理しています。
以下の図はre:invent2018での資料(日本語版)ですが、用途ごとにアカウントを分割し管理することをベストプラクティスとしています。
https://d0.awsstatic.com/events/jp/2017/summit/slide/D4T2-2.pdf今後更にアカウントが増加していく可能性がある中で、いくつか課題がありました。
課題
1. すべてのアカウントの設定が必要な場合はそれぞれのアカウントに都度ログインする必要があり大変
2. アカウント担当者が適切に外部からの脅威を防げているのか不明であり、セキュリティホールが存在or今後発生する可能性がある使用AWSサービス概要
今回利用したサービスを紹介させていただきます
CloudFormation StackSets
AWS CloudFormationは弊社でもデフォルトで使われるサービスですが、CloudFormation StackSetsは複数のアカウント及びリージョンに対してスタックを作成、更新、削除できるサービスです。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html今回は大規模マルチアカウントを運用することになり、一つ一つのアカウントに対してCloudFormationを実施する必要があることが今後現実的に厳しいことからCloudFormation StackSetsを採用することにしました。
GuardDuty
悪意のある操作、不正な動作について脅威検出するサービスです。機械学習、異常検出等を利用し、潜在的な脅威なども識別することができます。AWSの複数のサービスを利用しイベントを分析することが可能です。
https://aws.amazon.com/jp/guardduty/CloudTrail、CloudWatchなどを利用し独自カスタマイズし脅威検出を導入することもできるのですが、今回は以下2つの理由からAWSのマネージド脅威検出サービスであるGuardDutyを採用しました。
- 脅威検出の初手ということでなるべく早く導入したかった
- アカウント数が多くすべてのユースケースに対応したカスタマイズすることが困難であった
全体イメージ
上記サービスを利用し、今回どのような仕組みを構築したのか紹介させて頂きます。
全体イメージとしては以下のようになります
今回はAWSアカウントを3パターンに分けています
1. マスターアカウント
AWS Organizationsを管理しているマスターアカウントです。
2. メンバーアカウント(セキュリティ用)
マスターアカウントに管理されているメンバーアカウントの中でセキュリティに関することを取り扱うアカウントです。
今回はGuardDutyのログ集約やSlack通知を実行します。
3. メンバー(一般)
マスターアカウントに管理されているメンバーアカウントです。また今回はマスターアカウントからCloudFormation StackSetsを実行できるようにするだけでなく、セキュリティに関するCloudFormation StackSetsはメンバーアカウント(セキュリティ用)から実行できる設計にしました。
そうすることでセキュリティ担当者がマスターアカウントにログインする権限が不要になり、マスターアカウントにIAMユーザーを作成する必要がなくなります。
特にマスターアカウントにはセキュリティ担当者など不要なIAMユーザーを作成することは避けたかったことが理由になります。次からは構築手順を順を追って紹介させて頂きます。
CloudFormationStackSets
必要なリソース、用途
- Administration IAM Role(管理用)
- マスターアカウントからExecution IAM Role(管理用)を保持しているアカウントに対してCloudFormationStackSetsを実行できるようにするRole
- Execution IAM Role(管理用)
- メンバーアカウント(セキュリティ用、一般)がCloudFormationStackSetsを実行できるようにするRole
- Administration IAM Role(セキュリティ用)
- メンバーアカウント(セキュリティ用)からExecution IAM Role(セキュリティ用)を保持しているアカウントに対してCloudFormationStackSetsを実行できるようにするRole
- Execution IAM Role(セキュリティ用)
- マスターアカウント、メンバーアカウント(一般)がCloudFormationStackSetsを実行できるようにするRole
先程の3パターンのアカウントに配置する各リソースの表はこちらです。
Administration IAM Role(管理用) Execution IAM Role(管理用) Administration IAM Role(セキュリティ用) Execution IAM Role(セキュリティ用) マスターアカウント ◯ ◯ メンバーアカウント(セキュリティ用) ◯ ◯ メンバーアカウント(一般) ◯ ◯ このように配置することで、先程記述したマスターアカウントからもメンバーアカウント(セキュリティ用)どちらからもCloudFormationStackSetsが実行できるようになります。
それぞれリソースのコードは以下です。
Administration IAM Role(管理用)
AWSCloudFormationStackSetAdministrationRole.ymlAWSTemplateFormatVersion: 2010-09-09 Description: Configure the AWSCloudFormationStackSetAdministrationRole to enable use of AWS CloudFormation StackSets. Resources: AdministrationRole: Type: AWS::IAM::Role Properties: RoleName: AWSCloudFormationStackSetAdministrationRole AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: cloudformation.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: AssumeRole-AWSCloudFormationStackSetExecutionRole PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - sts:AssumeRole Resource: - "arn:aws:iam::*:role/AWSCloudFormationStackSetExecutionRole"Execution IAM Role(管理用)
AWSCloudFormationStackSetExecutionRole.ymlAWSTemplateFormatVersion: 2010-09-09 Description: Configure the AWSCloudFormationStackSetExecutionRole to enable use of your account as a target account in AWS CloudFormation StackSets. Parameters: AdministratorAccountId: Type: String Description: AWS Account Id of the administrator account (the account in which StackSets will be created). MaxLength: 12 MinLength: 12 Resources: ExecutionRole: Type: AWS::IAM::Role Properties: RoleName: AWSCloudFormationStackSetExecutionRole AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: AWS: - !Ref AdministratorAccountId Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/AdministratorAccessAdministration IAM Role(セキュリティ用)
AWSCloudFormationStackSetAdministrationRoleForOtherAccount.ymlAWSTemplateFormatVersion: 2010-09-09 Description: Configure the AWSCloudFormationStackSetAdministrationRole to enable use of AWS CloudFormation StackSets. Parameters: RoleSuffix: Type: String Description: IAM Role Suffix.CamelCase highly reccomend. ex) When RoleSuffix is SecurityAccount, AWSCloudFormationStackSetAdministrationRoleForSecurityAccount Resources: AdministrationRole: Type: AWS::IAM::Role Properties: RoleName: !Sub AWSCloudFormationStackSetAdministrationRoleFor${RoleSuffix} AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: cloudformation.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: !Sub AssumeRole-AWSCloudFormationStackSetExecutionRoleFor${RoleSuffix} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - sts:AssumeRole Resource: - !Sub "arn:aws:iam::*:role/AWSCloudFormationStackSetExecutionRoleFor${RoleSuffix}"Execution IAM Role(セキュリティ用)
AWSCloudFormationStackSetExecutionRoleForOtherAccount.ymlAWSTemplateFormatVersion: 2010-09-09 Description: Configure the AWSCloudFormationStackSetExecutionRole to enable use of your account as a target account in AWS CloudFormation StackSets. Parameters: AdministratorAccountId: Type: String Description: AWS Account Id of the administrator account (the account in which StackSets will be created). MaxLength: 12 MinLength: 12 RoleSuffix: Type: String Description: IAM Role Suffix.CamelCase highly reccomend. ex) When RoleSuffix is SecurityAccount, AWSCloudFormationStackSetExecutionRoleForSecurityAccount Resources: ExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub AWSCloudFormationStackSetExecutionRoleFor${RoleSuffix} AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: AWS: - !Ref AdministratorAccountId Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/AdministratorAccess
構築手順
マルチアカウントを運用している方に読んで頂いていると思うので、具体的なAWSの操作は省略させていただきます。
手順1
概要 操作アカウント Administration IAM Role(管理用)を作成 マスターアカウント 手順2
概要 操作アカウント Exection IAM Role(管理用)を作成 メンバーアカウント(セキュリティ用、一般) (今回のみすべてのアカウントにログインが必要になります)
以下リンクから作成
※AdministratorAccountIdには親アカウントのアカウントIDを記入
(アカウントが多い場合はURLのXXX部分を修正すると楽になります)手順3
概要 操作アカウント Administration IAM Role(セキュリティ用)を作成 メンバーアカウント(セキュリティ用) CloudFormationにて必要なリソースで紹介したAdministration IAM Role(セキュリティ用)の
AWSCloudFormationStackSetAdministrationRoleForOtherAccount.yml
を実行し、メンバーアカウント(セキュリティ)にIAM Roleを作成
※RoleSuffixには任意で記入 例)SecurityAccount手順4
概要 操作アカウント Exection IAM Role(セキュリティ用)を作成 マスターアカウント CloudFormation StackSetsにて必要なリソースで紹介したExecution IAM Role(セキュリティ用)
AWSCloudFormationStackSetExecutionRoleForOtherAccount.yml
を実行し、マスターアカウントとメンバーアカウント(一般)にIAM Roleを作成
※ AdministratorAccountIdには子アカウント(セキュリティ)のアカウントIDを記入
※ RoleSuffixには任意で記入 例)SecurityAccount
※ StackSetsの対象はメンバーアカウント(セキュリティ用)を除くすべてアカウントを指定してください、任意のOUを作成しメンバーアカウント(セキュリティ用)以外を参加させるか、後述するstacksets_accounts.csv
を利用次にGuarDutyを設定していきます
GuardDuty
必要なリソース、用途
- マスターアカウント、メンバーアカウント(一般)のアカウントID一覧
- CloudFormation StackSets実行時に対象を指定するために利用します
stacksets_accounts.csv123456789012,123456789013,・・・・123456789099
- マスターアカウント、メンバーアカウント(一般)のアカウントID、メールアドレス一覧
- GuardDutyのメンバー招待用のAWSアカウントIDとルートユーザのメールアドレスのcsvファイルです。
※GuardDutyのメンバーアカウントとStackSetsのメンバーアカウントが混同しやすいので注意してください
guardduty_member_accounts.csv123456789012,aws+123456789012@example.com 123456789013,aws+123456789013@example.com ・・・・ 123456789099,aws+123456789099@example.com構築手順
こちらも同様に具体的な操作は省略させて頂きます。
手順1
概要 操作アカウント セキュリティアカウントでGuardDuty有効化 セキュリティアカウント コンソールにログインしサービスからGuardDutyを選択し、有効化します
手順2
概要 操作アカウント GuardDutyメンバー招待 セキュリティアカウント 準備しておいた
guardduty_member_accounts.csv
を利用しメンバーに招待します。手順3
概要 操作アカウント Stacksetsですべてアカウント、リージョンでGuardDuty有効化 セキュリティアカウント 次にSlackに通知を設定していきます。
Slack通知
必要なリソース、用途
- Slack通知用リソース
- CloudWatchEventRuleにて制御を入れることで脅威レベルがミディアム、高いものに絞って通知するようにしました。現時点で脅威レベルが低いを含めてしまうと通知が多く重要な脅威検出ができない可能性があったためです。今後少しずつ脅威レベルが低いものを修正した段階で脅威レベルが低いものも通知しようと思ってます。
notify-guardduty-alert.ymlAWSTemplateFormatVersion: 2010-09-09 Description: 'enable guardduty and set alert' Parameters: SlackWebhookUrl: Type: String Default: 'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXX/XXXXXXXXXXXXXXXXXXXXX' SlackMentionId: Type: String SlackMentionName: Type: String Resources: LambdaFunctionNotifyAlertFromGuardDuty: Type: 'AWS::Lambda::Function' Properties: Handler: 'index.lambda_handler' Runtime: 'python3.7' Code: ZipFile: | import json import os import urllib.request def get_severity_level(severity, sre_mention): # ref: http://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings.html#guardduty_findings-severity if severity == 0.0: level = {'label': 'Information', 'color': 'good', 'mention': ''} elif 0.1 <= severity <= 3.9: level = {'label': 'Low', 'color': 'warning', 'mention': ''} elif 4.0 <= severity <= 6.9: level = {'label': 'Medium', 'color': 'warning', 'mention': sre_mention} elif 7.0 <= severity <= 8.9: level = {'label': 'High', 'color': 'danger', 'mention': sre_mention} elif 9.0 <= severity <= 10.0: level = {'label': 'Critical', 'color': 'danger', 'mention': sre_mention} else: level = {'label': 'Unknown', 'color': '#666666', 'mention': ''} return level def format_message(data, sre_mention): account_id = data['detail']['accountId'] region = data['detail']['region'] severity = data['detail']['severity'] title = data['detail']['title'] description = data['detail']['description'] type = data['detail']['type'] severity_level = get_severity_level(severity, sre_mention) payload = { 'username': 'GuardDuty', 'text': '{} GuardDuty Finding in {}'.format(severity_level['mention'], region), 'icon_emoji': ':aws:', 'attachments': [ { 'fallback': 'Detailed information on GuardDuty Finding.', 'color': severity_level['color'], 'title': title, 'text': description, 'fields': [ { 'title': 'Account ID', 'value': account_id, 'short': True }, { 'title': 'Severity', 'value': severity_level['label'], 'short': True }, { 'title': 'Type', 'value': type, 'short': False } ] } ] } return payload def notify_slack(url, payload): data = json.dumps(payload).encode('utf-8') method = 'POST' headers = {'Content-Type': 'application/json'} request = urllib.request.Request(url, data = data, method = method, headers = headers) with urllib.request.urlopen(request) as response: return response.read().decode('utf-8') def lambda_handler(event, context): slack_webhook_url = os.getenv('SLACK_WEBHOOK_URL') slack_mention_id = os.getenv('SLACK_MENTION_ID') slack_mention_name = os.getenv('SLACK_MENTION_NAME') sre_mention = '<!subteam^%s|%s>' % (slack_mention_id, slack_mention_name) payload = format_message(event, sre_mention) response = notify_slack(slack_webhook_url, payload) return response MemorySize: 128 Timeout: 60 Environment: Variables: SLACK_WEBHOOK_URL: !Ref SlackWebhookUrl SLACK_MENTION_ID: !Ref SlackMentionId SLACK_MENTION_NAME: !Ref SlackMentionName Role: !GetAtt IAMRoleNotifyAlertFromGuardDuty.Arn IAMRoleNotifyAlertFromGuardDuty: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2008-10-17' Statement: - Effect: 'Allow' Principal: Service: 'lambda.amazonaws.com' Action: 'sts:AssumeRole' ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy LambdaPermissionNotifyAlertFromGuardDuty: Type: 'AWS::Lambda::Permission' Properties: FunctionName: !Ref LambdaFunctionNotifyAlertFromGuardDuty Action: 'lambda:InvokeFunction' Principal: 'events.amazonaws.com' SourceArn: !GetAtt EventsRuleNotifyAlertFromGuardDutySchedule.Arn EventsRuleNotifyAlertFromGuardDutySchedule: Type: 'AWS::Events::Rule' Properties: Description: 'Alert to slack when find threats by GuardDuty' EventPattern: | { 'source': [ 'aws.guardduty' ], 'detail-type': [ 'GuardDuty Finding' ], 'detail': { 'severity': [4.0,4.1,4.2,4.3,4.4,4.5,4.6,4.7,4.8,4.9,5.0,5.1,5.2,5.3,5.4,5.5,5.6,5.7,5.8,5.9,6.0,6.1,6.2,6.3,6.4,6.5,6.6,6.7,6.8,6.9,7.0,7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,8.0,8.1,8.2,8.3,8.4,8.5,8.6,8.7,8.8,8.9,9.0,9.1,9.2,9.3,9.4,9.5,9.6,9.7,9.8,9.9,10.0,4,5,6,7,8,9,10] } } Targets: - Arn: !GetAtt LambdaFunctionNotifyAlertFromGuardDuty.Arn Id: 'Slackbot' IAMRoleLambdaExecutionNotifyAlertFromGuardDuty: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: 'Allow' Principal: Service: 'events.amazonaws.com' Action: 'sts:AssumeRole'構築手順
こちらも同様に具体的な操作は省略させて頂きます。
手順1
概要 操作アカウント SlackにてWebフックURLの取得 - 手順2
概要 操作アカウント Slack通知を設定 セキュリティアカウント CloudFormationにて必要なリソースで紹介した
notify-guardduty-alert.yml
を実行し、Slack通知を設定AWS Chatbotとの比較
今回はLambdaにてSlack通知を実施しましたが、AWS Chatbotも利用を検討しました。
簡単にLambdaを選択した理由をまとめておきます。
Lambda AWS Chatbot Slack対応 可能 可能 GuarDuty対応 可能 可能 脅威レベル絞り込み 可能 可能 メンション 可能 不可能 表示アカウントID 問題のアカウント セキュリティ用アカウント(セキュリティアカウントにログインして詳細を確認することで問題のアカウントを特定する) 通知例 以上より、メンション可能であること、Slack通知を見た時にどのアカウントで脅威が検出されたのかわかるの2点から今回はLambdaにてSlack通知することとしました。
まとめ
今回は以上の手順にて、AWSマルチアカウント運用時の脅威検出の導入の取り組みについて紹介させて頂きました。
大量のAWSアカウントを運用している場合に、一つ一つのアカウントにログインして設定することは想像以上に大変のため、今回のCloudFormationStackSetsを利用することにより運用の負荷を下げることが可能です。
またGuardDutyを利用し脅威検出を可能にしたため、アカウント運用チームの知らないところでセキュリティインシデント発生も抑制することが可能になるのではと思っております。参考
https://dev.classmethod.jp/cloud/aws/create-stacksets-iam-role/
https://dev.classmethod.jp/cloud/aws/set-guardduty-all-region/
https://dev.classmethod.jp/cloud/aws/introducing-cloudformation-stacksets/
- 投稿日:2020-12-08T16:02:24+09:00
AWSマルチアカウント運用時の驚異検出の導入
こんにちは、Hamee株式会社でSREとして働いています。大嶋です。
普段はNextEngineのクラウド化案件を担当しています。この記事はHamee Advent Calendar 2020の9日目になります。
また、前職でテックブログ執筆のために準備していた記事を修正し公開しています。
(公開のタイミングを見失っていたためこのタイミングで公開させてください)
そのためやってみた記事と、今後弊社で個人的に取り入れていきたい仕組みの紹介(提案)となります。それではAWSマルチアカウント運用時の驚異検出の導入の取り組みについて、紹介させて頂きます。
目次
- 概要
- 課題
- 利用AWSサービス説明
- CloudFormationStackSets
- GuardDuty
- 全体イメージ
CloudFormationStackSets
- 必要なリソース、用途
- 構築手順
GuardDuty
- 必要なリソース、用途
- 構築手順
Slack通知
- 必要なリソース、用途
- 構築手順
- AWS Chatbotとの比較
まとめ
概要
AWSアカウントを複数所有していて、プロダクト、組織、用途(Dev、Stg、Prod)などで分割され、AWS Organizationsを利用しマルチアカウントを管理しています。
以下の図はre:invent2018での資料(日本語版)ですが、用途ごとにアカウントを分割し管理することをベストプラクティスとしています。
https://d0.awsstatic.com/events/jp/2017/summit/slide/D4T2-2.pdf今後更にアカウントが増加していく可能性がある中で、いくつか課題がありました。
課題
1. すべてのアカウントの設定が必要な場合はそれぞれのアカウントに都度ログインする必要があり大変
2. アカウント担当者が適切に外部からの脅威を防げているのか不明であり、セキュリティホールが存在or今後発生する可能性がある使用AWSサービス概要
今回利用したサービスを紹介させていただきます
CloudFormation StackSets
AWS CloudFormationは弊社でもデフォルトで使われるサービスですが、CloudFormation StackSetsは複数のアカウント及びリージョンに対してスタックを作成、更新、削除できるサービスです。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html今回は大規模マルチアカウントを運用することになり、一つ一つのアカウントに対してCloudFormationを実施する必要があることが今後現実的に厳しいことからCloudFormation StackSetsを採用することにしました。
GuardDuty
悪意のある操作、不正な動作について脅威検出するサービスです。機械学習、異常検出等を利用し、潜在的な脅威なども識別することができます。AWSの複数のサービスを利用しイベントを分析することが可能です。
https://aws.amazon.com/jp/guardduty/CloudTrail、CloudWatchなどを利用し独自カスタマイズし脅威検出を導入することもできるのですが、今回は以下2つの理由からAWSのマネージド脅威検出サービスであるGuardDutyを採用しました。
- 脅威検出の初手ということでなるべく早く導入したかった
- アカウント数が多くすべてのユースケースに対応したカスタマイズすることが困難であった
全体イメージ
上記サービスを利用し、今回どのような仕組みを構築したのか紹介させて頂きます。
全体イメージとしては以下のようになります
今回はAWSアカウントを3パターンに分けています
1. マスターアカウント
AWS Organizationsを管理しているマスターアカウントです。
2. メンバーアカウント(セキュリティ用)
マスターアカウントに管理されているメンバーアカウントの中でセキュリティに関することを取り扱うアカウントです。
今回はGuardDutyのログ集約やSlack通知を実行します。
3. メンバー(一般)
マスターアカウントに管理されているメンバーアカウントです。また今回はマスターアカウントからCloudFormation StackSetsを実行できるようにするだけでなく、セキュリティに関するCloudFormation StackSetsはメンバーアカウント(セキュリティ用)から実行できる設計にしました。
そうすることでセキュリティ担当者がマスターアカウントにログインする権限が不要になり、マスターアカウントにIAMユーザーを作成する必要がなくなります。
特にマスターアカウントにはセキュリティ担当者など不要なIAMユーザーを作成することは避けたかったことが理由になります。次からは構築手順を順を追って紹介させて頂きます。
CloudFormationStackSets
必要なリソース、用途
- Administration IAM Role(管理用)
- マスターアカウントからExecution IAM Role(管理用)を保持しているアカウントに対してCloudFormationStackSetsを実行できるようにするRole
- Execution IAM Role(管理用)
- メンバーアカウント(セキュリティ用、一般)がCloudFormationStackSetsを実行できるようにするRole
- Administration IAM Role(セキュリティ用)
- メンバーアカウント(セキュリティ用)からExecution IAM Role(セキュリティ用)を保持しているアカウントに対してCloudFormationStackSetsを実行できるようにするRole
- Execution IAM Role(セキュリティ用)
- マスターアカウント、メンバーアカウント(一般)がCloudFormationStackSetsを実行できるようにするRole
先程の3パターンのアカウントに配置する各リソースの表はこちらです。
Administration IAM Role(管理用) Execution IAM Role(管理用) Administration IAM Role(セキュリティ用) Execution IAM Role(セキュリティ用) マスターアカウント ◯ ◯ メンバーアカウント(セキュリティ用) ◯ ◯ メンバーアカウント(一般) ◯ ◯ このように配置することで、先程記述したマスターアカウントからもメンバーアカウント(セキュリティ用)どちらからもCloudFormationStackSetsが実行できるようになります。
それぞれリソースのコードは以下です。
Administration IAM Role(管理用)
AWSCloudFormationStackSetAdministrationRole.ymlAWSTemplateFormatVersion: 2010-09-09 Description: Configure the AWSCloudFormationStackSetAdministrationRole to enable use of AWS CloudFormation StackSets. Resources: AdministrationRole: Type: AWS::IAM::Role Properties: RoleName: AWSCloudFormationStackSetAdministrationRole AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: cloudformation.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: AssumeRole-AWSCloudFormationStackSetExecutionRole PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - sts:AssumeRole Resource: - "arn:aws:iam::*:role/AWSCloudFormationStackSetExecutionRole"Execution IAM Role(管理用)
AWSCloudFormationStackSetExecutionRole.ymlAWSTemplateFormatVersion: 2010-09-09 Description: Configure the AWSCloudFormationStackSetExecutionRole to enable use of your account as a target account in AWS CloudFormation StackSets. Parameters: AdministratorAccountId: Type: String Description: AWS Account Id of the administrator account (the account in which StackSets will be created). MaxLength: 12 MinLength: 12 Resources: ExecutionRole: Type: AWS::IAM::Role Properties: RoleName: AWSCloudFormationStackSetExecutionRole AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: AWS: - !Ref AdministratorAccountId Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/AdministratorAccessAdministration IAM Role(セキュリティ用)
AWSCloudFormationStackSetAdministrationRoleForOtherAccount.ymlAWSTemplateFormatVersion: 2010-09-09 Description: Configure the AWSCloudFormationStackSetAdministrationRole to enable use of AWS CloudFormation StackSets. Parameters: RoleSuffix: Type: String Description: IAM Role Suffix.CamelCase highly reccomend. ex) When RoleSuffix is SecurityAccount, AWSCloudFormationStackSetAdministrationRoleForSecurityAccount Resources: AdministrationRole: Type: AWS::IAM::Role Properties: RoleName: !Sub AWSCloudFormationStackSetAdministrationRoleFor${RoleSuffix} AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: cloudformation.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: !Sub AssumeRole-AWSCloudFormationStackSetExecutionRoleFor${RoleSuffix} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - sts:AssumeRole Resource: - !Sub "arn:aws:iam::*:role/AWSCloudFormationStackSetExecutionRoleFor${RoleSuffix}"Execution IAM Role(セキュリティ用)
AWSCloudFormationStackSetExecutionRoleForOtherAccount.ymlAWSTemplateFormatVersion: 2010-09-09 Description: Configure the AWSCloudFormationStackSetExecutionRole to enable use of your account as a target account in AWS CloudFormation StackSets. Parameters: AdministratorAccountId: Type: String Description: AWS Account Id of the administrator account (the account in which StackSets will be created). MaxLength: 12 MinLength: 12 RoleSuffix: Type: String Description: IAM Role Suffix.CamelCase highly reccomend. ex) When RoleSuffix is SecurityAccount, AWSCloudFormationStackSetExecutionRoleForSecurityAccount Resources: ExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub AWSCloudFormationStackSetExecutionRoleFor${RoleSuffix} AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: AWS: - !Ref AdministratorAccountId Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/AdministratorAccess
構築手順
マルチアカウントを運用している方に読んで頂いていると思うので、具体的なAWSの操作は省略させていただきます。
手順1
概要 操作アカウント Administration IAM Role(管理用)を作成 マスターアカウント 手順2
概要 操作アカウント Exection IAM Role(管理用)を作成 メンバーアカウント(セキュリティ用、一般) (今回のみすべてのアカウントにログインが必要になります)
以下リンクから作成
※AdministratorAccountIdには親アカウントのアカウントIDを記入
(アカウントが多い場合はURLのXXX部分を修正すると楽になります)手順3
概要 操作アカウント Administration IAM Role(セキュリティ用)を作成 メンバーアカウント(セキュリティ用) CloudFormationにて必要なリソースで紹介したAdministration IAM Role(セキュリティ用)の
AWSCloudFormationStackSetAdministrationRoleForOtherAccount.yml
を実行し、メンバーアカウント(セキュリティ)にIAM Roleを作成
※RoleSuffixには任意で記入 例)SecurityAccount手順4
概要 操作アカウント Exection IAM Role(セキュリティ用)を作成 マスターアカウント CloudFormation StackSetsにて必要なリソースで紹介したExecution IAM Role(セキュリティ用)
AWSCloudFormationStackSetExecutionRoleForOtherAccount.yml
を実行し、マスターアカウントとメンバーアカウント(一般)にIAM Roleを作成
※ AdministratorAccountIdには子アカウント(セキュリティ)のアカウントIDを記入
※ RoleSuffixには任意で記入 例)SecurityAccount
※ StackSetsの対象はメンバーアカウント(セキュリティ用)を除くすべてアカウントを指定してください、任意のOUを作成しメンバーアカウント(セキュリティ用)以外を参加させるか、後述するstacksets_accounts.csv
を利用次にGuarDutyを設定していきます
GuardDuty
必要なリソース、用途
- マスターアカウント、メンバーアカウント(一般)のアカウントID一覧
- CloudFormation StackSets実行時に対象を指定するために利用します
stacksets_accounts.csv123456789012,123456789013,・・・・123456789099
- マスターアカウント、メンバーアカウント(一般)のアカウントID、メールアドレス一覧
- GuardDutyのメンバー招待用のAWSアカウントIDとルートユーザのメールアドレスのcsvファイルです。
※GuardDutyのメンバーアカウントとStackSetsのメンバーアカウントが混同しやすいので注意してください
guardduty_member_accounts.csv123456789012,aws+123456789012@example.com 123456789013,aws+123456789013@example.com ・・・・ 123456789099,aws+123456789099@example.com構築手順
こちらも同様に具体的な操作は省略させて頂きます。
手順1
概要 操作アカウント セキュリティアカウントでGuardDuty有効化 セキュリティアカウント コンソールにログインしサービスからGuardDutyを選択し、有効化します
手順2
概要 操作アカウント GuardDutyメンバー招待 セキュリティアカウント 準備しておいた
guardduty_member_accounts.csv
を利用しメンバーに招待します。手順3
概要 操作アカウント Stacksetsですべてアカウント、リージョンでGuardDuty有効化 セキュリティアカウント 次にSlackに通知を設定していきます。
Slack通知
必要なリソース、用途
- Slack通知用リソース
- CloudWatchEventRuleにて制御を入れることで脅威レベルがミディアム、高いものに絞って通知するようにしました。現時点で脅威レベルが低いを含めてしまうと通知が多く重要な脅威検出ができない可能性があったためです。今後少しずつ脅威レベルが低いものを修正した段階で脅威レベルが低いものも通知しようと思ってます。
notify-guardduty-alert.ymlAWSTemplateFormatVersion: 2010-09-09 Description: 'enable guardduty and set alert' Parameters: SlackWebhookUrl: Type: String Default: 'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXX/XXXXXXXXXXXXXXXXXXXXX' SlackMentionId: Type: String SlackMentionName: Type: String Resources: LambdaFunctionNotifyAlertFromGuardDuty: Type: 'AWS::Lambda::Function' Properties: Handler: 'index.lambda_handler' Runtime: 'python3.7' Code: ZipFile: | import json import os import urllib.request def get_severity_level(severity, sre_mention): # ref: http://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings.html#guardduty_findings-severity if severity == 0.0: level = {'label': 'Information', 'color': 'good', 'mention': ''} elif 0.1 <= severity <= 3.9: level = {'label': 'Low', 'color': 'warning', 'mention': ''} elif 4.0 <= severity <= 6.9: level = {'label': 'Medium', 'color': 'warning', 'mention': sre_mention} elif 7.0 <= severity <= 8.9: level = {'label': 'High', 'color': 'danger', 'mention': sre_mention} elif 9.0 <= severity <= 10.0: level = {'label': 'Critical', 'color': 'danger', 'mention': sre_mention} else: level = {'label': 'Unknown', 'color': '#666666', 'mention': ''} return level def format_message(data, sre_mention): account_id = data['detail']['accountId'] region = data['detail']['region'] severity = data['detail']['severity'] title = data['detail']['title'] description = data['detail']['description'] type = data['detail']['type'] severity_level = get_severity_level(severity, sre_mention) payload = { 'username': 'GuardDuty', 'text': '{} GuardDuty Finding in {}'.format(severity_level['mention'], region), 'icon_emoji': ':aws:', 'attachments': [ { 'fallback': 'Detailed information on GuardDuty Finding.', 'color': severity_level['color'], 'title': title, 'text': description, 'fields': [ { 'title': 'Account ID', 'value': account_id, 'short': True }, { 'title': 'Severity', 'value': severity_level['label'], 'short': True }, { 'title': 'Type', 'value': type, 'short': False } ] } ] } return payload def notify_slack(url, payload): data = json.dumps(payload).encode('utf-8') method = 'POST' headers = {'Content-Type': 'application/json'} request = urllib.request.Request(url, data = data, method = method, headers = headers) with urllib.request.urlopen(request) as response: return response.read().decode('utf-8') def lambda_handler(event, context): slack_webhook_url = os.getenv('SLACK_WEBHOOK_URL') slack_mention_id = os.getenv('SLACK_MENTION_ID') slack_mention_name = os.getenv('SLACK_MENTION_NAME') sre_mention = '<!subteam^%s|%s>' % (slack_mention_id, slack_mention_name) payload = format_message(event, sre_mention) response = notify_slack(slack_webhook_url, payload) return response MemorySize: 128 Timeout: 60 Environment: Variables: SLACK_WEBHOOK_URL: !Ref SlackWebhookUrl SLACK_MENTION_ID: !Ref SlackMentionId SLACK_MENTION_NAME: !Ref SlackMentionName Role: !GetAtt IAMRoleNotifyAlertFromGuardDuty.Arn IAMRoleNotifyAlertFromGuardDuty: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2008-10-17' Statement: - Effect: 'Allow' Principal: Service: 'lambda.amazonaws.com' Action: 'sts:AssumeRole' ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy LambdaPermissionNotifyAlertFromGuardDuty: Type: 'AWS::Lambda::Permission' Properties: FunctionName: !Ref LambdaFunctionNotifyAlertFromGuardDuty Action: 'lambda:InvokeFunction' Principal: 'events.amazonaws.com' SourceArn: !GetAtt EventsRuleNotifyAlertFromGuardDutySchedule.Arn EventsRuleNotifyAlertFromGuardDutySchedule: Type: 'AWS::Events::Rule' Properties: Description: 'Alert to slack when find threats by GuardDuty' EventPattern: | { 'source': [ 'aws.guardduty' ], 'detail-type': [ 'GuardDuty Finding' ], 'detail': { 'severity': [4.0,4.1,4.2,4.3,4.4,4.5,4.6,4.7,4.8,4.9,5.0,5.1,5.2,5.3,5.4,5.5,5.6,5.7,5.8,5.9,6.0,6.1,6.2,6.3,6.4,6.5,6.6,6.7,6.8,6.9,7.0,7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,8.0,8.1,8.2,8.3,8.4,8.5,8.6,8.7,8.8,8.9,9.0,9.1,9.2,9.3,9.4,9.5,9.6,9.7,9.8,9.9,10.0,4,5,6,7,8,9,10] } } Targets: - Arn: !GetAtt LambdaFunctionNotifyAlertFromGuardDuty.Arn Id: 'Slackbot' IAMRoleLambdaExecutionNotifyAlertFromGuardDuty: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: 'Allow' Principal: Service: 'events.amazonaws.com' Action: 'sts:AssumeRole'構築手順
こちらも同様に具体的な操作は省略させて頂きます。
手順1
概要 操作アカウント SlackにてWebフックURLの取得 - 手順2
概要 操作アカウント Slack通知を設定 セキュリティアカウント CloudFormationにて必要なリソースで紹介した
notify-guardduty-alert.yml
を実行し、Slack通知を設定AWS Chatbotとの比較
今回はLambdaにてSlack通知を実施しましたが、AWS Chatbotも利用を検討しました。
簡単にLambdaを選択した理由をまとめておきます。
Lambda AWS Chatbot Slack対応 可能 可能 GuarDuty対応 可能 可能 脅威レベル絞り込み 可能 可能 メンション 可能 不可能 表示アカウントID 問題のアカウント セキュリティ用アカウント(セキュリティアカウントにログインして詳細を確認することで問題のアカウントを特定する) 通知例 以上より、メンション可能であること、Slack通知を見た時にどのアカウントで脅威が検出されたのかわかるの2点から今回はLambdaにてSlack通知することとしました。
まとめ
今回は以上の手順にて、AWSマルチアカウント運用時の驚異検出の導入の取り組みについて紹介させて頂きました。
大量のAWSアカウントを運用している場合に、一つ一つのアカウントにログインして設定することは想像以上に大変のため、今回のCloudFormationStackSetsを利用することにより運用の負荷を下げることが可能です。
またGuardDutyを利用し脅威検出を可能にしたため、アカウント運用チームの知らないところでセキュリティインシデント発生も抑制することが可能になるのではと思っております。参考
https://dev.classmethod.jp/cloud/aws/create-stacksets-iam-role/
https://dev.classmethod.jp/cloud/aws/set-guardduty-all-region/
https://dev.classmethod.jp/cloud/aws/introducing-cloudformation-stacksets/
- 投稿日:2020-12-08T15:41:22+09:00
チュートリアル: Amazon S3 で AWS Lambda を使用するで、Lambda Layersを使ってみた
やったこと
AWS公式のLambdaチュートリアルをベースに、画像変換ライブラリ"sharp"の依存関係を、Lambda Layersに切り出してみました。
Lambda Layersを使用することで、Lambda関数をマネジメントコンソール上で改修できるので便利ですね。
- チュートリアル: Amazon S3 で AWS Lambda を使用する
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-s3-example.html※補足
チュートリアルでは、Lambda関数と画像変換ライブラリ"sharp"をデプロイパッケージで管理していて、Lambda関数をマネジメントコンソール上で改修できません本記事の概要
説明すること
- Windowsで、画像変換ライブラリ"sharp"をzip圧縮する方法
- マネジメントコンソールで、Lambda Layersにライブラリを登録して使用する方法
説明しないこと
- Lambdaチュートリアルの内容
- Lambda関数をマネジメントコンソール上で作成する手順
環境
- Node.js 12.x
- AWS CLI 2
- Windows10
説明
Windowsで、画像変換ライブラリ"sharp"をzip圧縮する方法
1.コマンドプロンプトを起動し、任意のフォルダで
nodejs
フォルダを作成する。...> mkdir nodejs2.
nodejs
フォルダへ移動し、npm コマンドで 画像変換ライブラリ"sharp"を取得する。.../nodejs> npm install --arch=x64 --platform=linux --target=12.13.0 sharp3.
nodejs
フォルダを右クリックしてzip圧縮する。
※注意
powershellのcompress-archiveでzip圧縮した場合、Lambda Layer作成時に以下のエラーとなりました。...> powershell compress-archive nodejs sharp レイヤーバージョンを作成できませんでした: Layer conversion failed: Some directories do not have execute permissions;4.sharp.zipのフォルダ構成を確認する。以下のフォルダ構造になっていればOK。
sharp.zip └ nodejs |- package-lock.json |- /node_modules/sharp └ /node_modules/...マネジメントコンソールで、Lambda Layersにライブラリを登録して使用する方法
1.Lambda Layerに
sharp-layer
を作成し、sharp.zipをアップロードする。詳細は以下の画面イメージを参照下さい。2.Lambda関数にLambda Layerの
sharp-layer
を設定する。詳細は以下の画面イメージを参照下さい。
参考サイト
- チュートリアル: Amazon S3 で AWS Lambda を使用する
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-s3-example.html
- AWS Lambda レイヤー
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-layers.html- AWS Lambda Layersでnode_modulesを使う
https://xp-cloud.jp/blog/2019/01/12/4630/
- 投稿日:2020-12-08T15:35:09+09:00
【AWS機能紹介】有名人の認識やテキスト抽出ができる
本記事の概要
本記事はAWSをこれから学びたい、どんなものか体験してみたい、
そんな初心者さん向けに作成しております。Amazon Rekognition
今回AWSの機能として紹介するのは「Amazon Rekognition」になります。
導入するにあたり細かい設定を必要とせず手軽に始めることができ、
言語などの専門知識がなくても画像をアップなどするだけで使用できます。機能紹介
・テキストの検出
イメージおよびビデオ内のテキストを検出できる機能です。
その後、検出されたテキストを機械可読テキストに変換できます。・顔の比較
顔を比較する機能です。類似割合 (%) に基づいて類似度を確認します。その他にも様々な機能があります。
・安全でないコンテンツの検出
ダルトコンテンツや暴力的なコンテンツを検出でき不適切なコンテンツをフィルタできます。
規定についてはユーザの方で調節ができます。・有名人認識
アップした写真から政治、スポーツ、ビジネス、エンターテインメント、
メディアなどのさまざまな分野にわたる多数の有名人を認識することが可能です。使用するには
Amazon Rekognitionで検索し、AWSのサイトから使用を開始できます。
事前にアカウントの作成は必要です。参考元サイト
・公式AWSサイト
(https://aws.amazon.com/jp/rekognition/?blog-cards.sort-by=item.additionalFields.createdDate&blog-cards.sort-order=desc)
(https://docs.aws.amazon.com/ja_jp/rekognition/latest/dg/what-is.html)・紹介サイト
(https://recipe.kc-cloud.jp/archives/10911)まとめ
本記事はいかがでしたでしょうか。
AWSと聞くとよく分からない、難しそうといった印象があると思います。
実際サーバを立てたり、プログラミング言語を使用したりと、
便利で比較的簡単ではありますが、ある程度の知識がないと分からないものも多々あります。
ですがAWSは幅広いサービスを展開しており「Amazon Rekognition」のように
入りやすいサービスもいくつもあります。
もし興味をもたれましたら他サービス等も調べてみてはいかがでしょうか。
- 投稿日:2020-12-08T15:21:23+09:00
LambdaでDockerコンテナイメージ使えるってマジですか?(Python3でやってみる)
背景
自然言語処理関係でLambdaを使いたいと思っているけど、どうもライブラリのサイズが大きく、Lambdaのクオータに引っかかる。
ECSでAutoScalingかな?と思っていた所に、re:Invent2020で、Lambdaでコンテナイメージが使えるという発表があったという話を聞く。早速Developers.IO様で記事になってた。まさに大きいファイルを扱う必要があるML系処理向けらしい!
【速報】Lambdaのパッケージフォーマットとしてコンテナイメージがサポートされるようになりました!! #reinvent前提
今回は、Python3.8のランタイムが対象
Ubuntu18.04マシンで構築流れ確認
流れとしては以下の感じらしい。
- 指定エンドポイントにアクセスすると、LambdaフォーマットでのJsonオブジェクトを返す様なコンテナを作成
- このコンテナ作成際にはベースイメージがあるそうだが、フォーマットを守れば自作でも可能らしい
- ローカルでテストしつつDockerfileを作成
- ECRに登録
- Lambdaの関数作成時に、そのECRのDockerImageのURIを指定する
用語
Runtime interface clients
コンテナ内部に存在し、Lambdaとプログラムコードをつなぐ役割をする。このモジュールにhandlerが渡されて処理される形の模様。
デフォルトのAWS Lambdaベースイメージには既に入っているので、自分たちで独自にDockerImageを作る場合には個別対応が必要。Runtime Interface Emulater
ローカル環境でLambdaコンテナを試せるエミュレーター。多分、Lambdaテスト用のエンドポイントを提供するwebサーバーの様なものだと思う。公式DockerImageなら既に入っている模様。
ローカル開発環境セットアップ
RIE公式Github にインストールコマンドが記載されている。
全体概要がよく解らなくて苦戦していたら、日本語説明ページがあった。これをトレースしてみる。作業用フォルダ作成
mkdir locallambdatest cd locallambdatestaws-lambda-rieをダウンロード
https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie
へアクセスするとダウンロードが始まるので、先に作った作業用フォルダ直下に保存。Dockerfile作成
公式ページそのままから、aws-lambda-rieのコピー部分、entry.shのモード変更だけ修正
ファイル名:Dockerfile.python3.9test
Dockerfile.python3.9test# Define global args ARG FUNCTION_DIR="/home/app/" ARG RUNTIME_VERSION="3.9" ARG DISTRO_VERSION="3.12" # Stage 1 - bundle base image + runtime # Grab a fresh copy of the image and install GCC FROM python:${RUNTIME_VERSION}-alpine${DISTRO_VERSION} AS python-alpine # Install GCC (Alpine uses musl but we compile and link dependencies with GCC) RUN apk add --no-cache \ libstdc++ # Stage 2 - build function and dependencies FROM python-alpine AS build-image # Install aws-lambda-cpp build dependencies RUN apk add --no-cache \ build-base \ libtool \ autoconf \ automake \ libexecinfo-dev \ make \ cmake \ libcurl # Include global args in this stage of the build ARG FUNCTION_DIR ARG RUNTIME_VERSION # Create function directory RUN mkdir -p ${FUNCTION_DIR} # Copy handler function COPY app/* ${FUNCTION_DIR} # Optional – Install the function's dependencies # RUN python${RUNTIME_VERSION} -m pip install -r requirements.txt --target ${FUNCTION_DIR} # Install Lambda Runtime Interface Client for Python RUN python${RUNTIME_VERSION} -m pip install awslambdaric --target ${FUNCTION_DIR} # Stage 3 - final runtime image # Grab a fresh copy of the Python image FROM python-alpine # Include global arg in this stage of the build ARG FUNCTION_DIR # Set working directory to function root directory WORKDIR ${FUNCTION_DIR} # Copy in the built dependencies COPY --from=build-image ${FUNCTION_DIR} ${FUNCTION_DIR} # (Optional) Add Lambda Runtime Interface Emulator and use a script in the ENTRYPOINT for simpler local runs # COPY https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie COPY aws-lambda-rie /usr/bin/aws-lambda-rie RUN chmod 755 /usr/bin/aws-lambda-rie COPY entry.sh / RUN chmod 755 /entry.sh ENTRYPOINT [ "/entry.sh" ] CMD [ "app.handler" ]アプリソース(app.py)を作成して、appフォルダ以下へ配置
mkdir app vim app/app.py
app.pyimport sys def handler(event, context): return 'Hello from AWS Lambda using Python' + sys.version + '! test'entry.sh 作成
日本語ページだと$1が消えているので注意。その部分を修正。
entry.sh#!/bin/sh if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then exec /usr/bin/aws-lambda-rie /usr/local/bin/python -m awslambdaric $1 else exec /usr/local/bin/python -m awslambdaric $1 fiDockerImageをビルド
ここではイメージ名を「python3.9test:local」とする。
sudo docker build -t python3.9test:local -f Dockerfile.python3.9test .コンテナを起動する
ここではコンテナ名を「locallambdatest」とする。
sudo docker run -p 9000:8080 --name locallambdatest python3.9test:localコンテナへアクセスしてみる
$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}' "Hello from AWS Lambda using Python3.9.0 (default, Nov 25 2020, 02:36:55) \n[GCC 9.3.0]! test"成功!
自然言語処理ライブラリをインストールしてみる
Dockerfile.python3.9test の修正
モジュールインストール部分コメントイン+requirements.txtの配置
Dockerfile.python3.9testCOPY requirements.txt requirements.txt RUN python${RUNTIME_VERSION} -m pip install -r requirements.txt --target ${FUNCTION_DIR} # RUN python${RUNTIME_VERSION} -m pip install -r requirements.txt --target ${FUNCTION_DIR}requirements.txtを作成
作業用フォルダ直下に以下の内容で作成。ginzaを入れておく。
requirements.txtginza==4.0.5app.py の修正
ginzaライブラリで形態素解析をして、その中身をそのまま返す。
import sys import json import spacy import logging from ginza import * logger = logging.getLogger() def handler(event, context): logger.info(context) target_text = event['text'] nlp = spacy.load('ja_ginza') doc = nlp(target_text) morpheme_list = [] for sent_idx, sent in enumerate(doc.sents): for token_idx, tk in enumerate(sent): wk_morpheme = {} wk_morpheme['text'] = tk.text wk_morpheme['dep'] = tk.dep_ wk_morpheme['pos'] = tk.pos_ wk_morpheme['tag'] = tk.tag_ morpheme_list.append(wk_morpheme) return morpheme_list実行
$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"text":"テストしてみる"}' [{"text": "\u30c6\u30b9\u30c8", "dep": "ROOT", "pos": "VERB", "tag": "\u540d\u8a5e-\u666e\u901a\u540d\u8a5e-\u30b5\u5909\u53ef\u80fd"}, {"text": "\u3057", "dep": "advcl", "pos": "AUX", "tag": "\u52d5\u8a5e-\u975e\u81ea\u7acb\u53ef\u80fd"}, {"text": "\u3066", "dep": "mark", "pos": "SCONJ", "tag": "\u52a9\u8a5e-\u63a5\u7d9a\u52a9\u8a5e"}, {"text": "\u307f\u308b", "dep": "aux", "pos": "AUX", "tag": "\u52d5\u8a5e-\u975e\u81ea\u7acb\u53ef\u80fd"}]出力が文字コードの文字列になっちゃいましたが、形態素解析出来ている模様。
ECRへコンテナイメージを登録する
ここではレポジトリ名を「lambdacontainer-test」とする。123412341234はもちろんダミーです。実際にはECRリポジトリを作成するAWSアカウントIDEです。
aws ecr create-repository --repository-name lambdacontainer-test --image-scanning-configuration scanOnPush=true sudo docker tag python3.9test:local 123412341234.dkr.ecr.ap-northeast-1.amazonaws.com/lambdacontainer-test:latest aws ecr get-login-password | sudo docker login --username AWS --password-stdin 123412341234.dkr.ecr.ap-northeast-1.amazonaws.com sudo docker push 123412341234.dkr.ecr.ap-northeast-1.amazonaws.com/lambdacontainer-test:latestdocker login 実行時、「Error saving credentials: error storing credentials」なんてエラーが出てきたら以下実行
sudo apt install gnupg2 passここまでで、AWSコンソールのECRリポジトリリストに表示されているはず。
Lambda関数をコンテナを使って作成する。
- AWSコンソールからLambdaのページへ行き「関数の作成」
- オプションで「コンテナイメージ」を選択
- 関数名は適当に。今回は「my-lambda-container-test」
- コンテナイメージURIは「画像を選択」(多分 Select Image の日本語訳)ボタンを押してリポジトリ(lambdacontainer-test)とタグ(latest)を選択
- 「関数の作成」を押してしばらくすると「関数の作成が正常に終了しました」のポップアップが出てくる。
テストする
- 「テスト」ボタンを押す
- イベント名は適当に
{"text":"テストしてみる"}
をテスト用Bodyに指定- 「作成」を押す
設定変更
- 「基本設定」エリアの「編集」ボタンを押す
- メモリを1GB、実行時間制限を5分ぐらいにする
テスト実行
なんかエラー出た。ginzaが使用しているsudachiライブラリがsymlinkを実行しようとして失敗している模様。
このエラー、ローカルテスト時点で出るようにして欲しかった・・・・(公式イメージだとそうなったりするのかな?)
回避するためにはまたライブラリの設定とかが必要になってきそう(そもそもそれが可能なのかも)。
※Lambda上では、書き込み系ファイル操作は /tmp/フォルダ以下で行う必要がある。{ "errorMessage": "[Errno 30] Read-only file system: '/home/app/sudachidict_core' -> '/home/app/sudachidict'", "errorType": "OSError", "stackTrace": [ " File \"/home/app/app.py\", line 12, in handler\n nlp = spacy.load('ja_ginza')\n", " File \"/home/app/spacy/__init__.py\", line 30, in load\n return util.load_model(name, **overrides)\n", " File \"/home/app/spacy/util.py\", line 170, in load_model\n return load_model_from_package(name, **overrides)\n", " File \"/home/app/spacy/util.py\", line 191, in load_model_from_package\n return cls.load(**overrides)\n", " File \"/home/app/ja_ginza/__init__.py\", line 12, in load\n return load_model_from_init_py(__file__, **overrides)\n", " File \"/home/app/spacy/util.py\", line 239, in load_model_from_init_py\n return load_model_from_path(data_path, meta, **overrides)\n", " File \"/home/app/spacy/util.py\", line 203, in load_model_from_path\n nlp = cls(meta=meta, **overrides)\n", " File \"/home/app/spacy/language.py\", line 186, in __init__\n make_doc = factory(self, **meta.get(\"tokenizer\", {}))\n", " File \"/home/app/spacy/lang/ja/__init__.py\", line 274, in create_tokenizer\n return JapaneseTokenizer(cls, nlp, config)\n", " File \"/home/app/spacy/lang/ja/__init__.py\", line 139, in __init__\n self.tokenizer = try_sudachi_import(self.split_mode)\n", " File \"/home/app/spacy/lang/ja/__init__.py\", line 38, in try_sudachi_import\n tok = dictionary.Dictionary().create(\n", " File \"/home/app/sudachipy/dictionary.py\", line 37, in __init__\n self._read_system_dictionary(config.settings.system_dict_path())\n", " File \"/home/app/sudachipy/config.py\", line 107, in system_dict_path\n dict_path = create_default_link_for_sudachidict_core(output=f)\n", " File \"/home/app/sudachipy/config.py\", line 72, in create_default_link_for_sudachidict_core\n dict_path = set_default_dict_package('sudachidict_core', output=output)\n", " File \"/home/app/sudachipy/config.py\", line 48, in set_default_dict_package\n dst_path.symlink_to(src_path)\n", " File \"/usr/local/lib/python3.9/pathlib.py\", line 1398, in symlink_to\n self._accessor.symlink(target, self, target_is_directory)\n", " File \"/usr/local/lib/python3.9/pathlib.py\", line 445, in symlink\n return os.symlink(a, b)\n" ] }基本目的は達成したのと、この問題の解消はまた別問題になるので、今回はここまでに。
感想
今回は最後できれいな結果は出てこなかったけど、Lambdaをコンテナ指定で使うという部分は出来た。
所要時間は「2485.68 ms(連続実行時は数ms)」。
処理の最初でエラーが出ているのでほぼオーバーヘッドとみなしていいかと。warmup戦略を取れば低レイテンシーが求められる用途にも使えそう。
ただ、使用しているライブラリが書き込み系ファイル操作を行うかは十分にチェックする必要あり(コンテナ使用Lambdaでなくても)。参考にさせていただいたページ
正直まだ公表されたばかりのサービスで、公式ドキュメントも説明不足を感じました。ただ、今後改善されていくと思いますし、有志の方が情報整理を行ってくれたりもすると思います。
AWS公式基本DockerImage
RIE公式Github【速報】Lambdaのパッケージフォーマットとしてコンテナイメージがサポートされるようになりました!! #reinvent
CDKでAWS Lambdaのパッケージフォーマットにコンテナイメージを指定してデプロイしてみた
- 投稿日:2020-12-08T13:19:56+09:00
【Go】 AWS Athenaから取得したデータをVegaでグラフ化してみた
はじめに
GoからAthenaのクエリを叩き、Athenaで取得したデータをもとにVegaでグラフを作成します。今回は、下の画像のような散布図を作成します。作業は大きく分けて、Goでの作業とVegaでの作業に分かれます。
Vegaとは?
グラフ作成ツールで、json形式のデータを読み込ませ、簡単なグラフから複雑なインタラクティブなグラフまで、様々なグラフを作ることができます。
Goでの作業
準備
.envを用意
GoからAWSにアクセスするために、AWSクレデンシャルを記載するファイルを用意します。
- まずは、.envファイルを用意します。
- .envに
AWS_ACCESS_KEY_ID
,AWS_SECRET_ACCESS_KEY
を記載します。スイッチロールが必要な場合はROLE_ARN
を指定します。AWS_ACCESS_KEY_ID=XXXXX AWS_SECRET_ACCESS_KEY=XXXXX ROLE_ARN=XXXXXgo mod 初期化
Goのプログラムで必要なパッケージをダウンロードするために、下記のコマンドをターミナルで実行します。この時、
go mod init hoge
のhoge
の部分には、好きな文字列を指定することができ、ここで指定した文字列は、コンパイル後の実行ファイルの名前になります。$ go mod init hoge上記のコマンドを実行後、
go.mod
というファイルができていればOK。main.go
全体像
package main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "os" "strings" "log" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/athena" "github.com/joho/godotenv" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) type Plot_data struct { Unixtime string `json:"unixtime"` Duration string `json:"duration"` } const ( database = "aucfan_services_log_partition" output_location = "s3://output_bucket/2020/12/07" query = ` SELECT unixtime, duration FROM aucfan_paapi_nginx_errorlog WHERE year = 2020 AND month = 6 AND day = 1 AND status_code = 429 ORDER BY unixtime; ` ) func newSession() *session.Session { awscfg := &aws.Config{ Region: aws.String("ap-northeast-1"), Credentials: credentials.NewStaticCredentialsFromCreds(credentials.Value{ AccessKeyID: os.Getenv("AWS_ACCESS_KEY_ID"), SecretAccessKey: os.Getenv("AWS_SECRET_ACCESS_KEY"), }), } sess := session.Must(session.NewSession(awscfg)) if len(os.Getenv("ROLE_ARN")) == 0 { return sess } creds := stscreds.NewCredentials(sess, os.Getenv("ROLE_ARN")) config := aws.Config{Region: sess.Config.Region, Credentials: creds} sSess := session.New(&config) return sSess } //クエリ実行 func runQuery(query string) error { var ( err error r athena.ResultConfiguration ) sSess := newSession() svc := athena.New(sSess, aws.NewConfig().WithRegion("ap-northeast-1")) //クエリをセット var s athena.StartQueryExecutionInput s.SetQueryString(query) //データベースをセット var q athena.QueryExecutionContext q.SetDatabase(database) s.SetQueryExecutionContext(&q) //結果の吐き出し用のS3バケット fmt.Println(output_location) r.SetOutputLocation(output_location) s.SetResultConfiguration(&r) //クエリ実行 result, err := svc.StartQueryExecution(&s) if err != nil { return err } fmt.Println("StartQueryExecution result:") fmt.Println(result.GoString()) var qri athena.GetQueryExecutionInput qri.SetQueryExecutionId(*result.QueryExecutionId) var qrop *athena.GetQueryExecutionOutput fmt.Println("waiting...") for { qrop, err = svc.GetQueryExecution(&qri) if err != nil { return err } if *qrop.QueryExecution.Status.State == "SUCCEEDED" || *qrop.QueryExecution.Status.State == "FAILED" { break } } if *qrop.QueryExecution.Status.State == "SUCCEEDED" { err = plot(svc, result.QueryExecutionId) return err } else { fmt.Println(*qrop.QueryExecution.Status.State) } return err } // クエリ実行結果を取得 func getData(svc *athena.Athena, id *string, token *string) (*athena.GetQueryResultsOutput, *string, error) { var err error ip := &athena.GetQueryResultsInput{ QueryExecutionId: id, NextToken: token, } op, err := svc.GetQueryResults(ip) if err != nil { return nil, nil, err } return op, op.NextToken, err } // 読み込んだ結果をjsonに書き込む func plot(svc *athena.Athena, id *string) error { var ( token *string op *athena.GetQueryResultsOutput ) token = nil ioutil.WriteFile("data.json", []byte("["), os.ModePerm) f, err := os.OpenFile("data.json", os.O_APPEND|os.O_WRONLY, 0600) if err != nil { log.Print(err) return err } defer f.Close() for { //tokenを使い、全てのデータを取得する op, token, err = getData(svc, id, token) if err != nil { return err } var ( data Plot_data eol string //行末 ) //取得したデータをjsonファイルへ書き込む for i, s := range op.ResultSet.Rows { for j, t := range s.Data { if j == 0 { //Asia/Tokyoの部分を削除 data.Unixtime = strings.Replace(*t.VarCharValue, " Asia/Tokyo", "", 1) } else { data.Duration = *t.VarCharValue } } if i != 0 { if i == (len(op.ResultSet.Rows)-1) && token == nil{ eol = "]" } else { eol = "," } jsonBytes, err := json.Marshal(data) if err != nil { return err } out := new(bytes.Buffer) json.Indent(out, jsonBytes, "", " ") plot := out.String() + eol fmt.Fprint(f, "\n" + plot) } } if token == nil { break } } return err } func handler(c echo.Context) error { fmt.Println(query) err := runQuery(query) //クエリ実行 if err != nil { return err } return c.File("data.json") } func main() { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(middleware.CORS()) if err := godotenv.Load(".env"); err != nil { log.Fatal(err) } e.GET("/", handler) e.Start(":1323") }解説
定数
- database
- データを取得したいAthenaのデータベースを指定します。
- output_location
- クエリの結果の吐き出し先S3バケットを指定します。
- query
- 実行したいSQLをここに記述します 。今回のような散布図を作成する場合、
SELECT x座標, y座標
になるようにSQLを作ると良いでしょう。const ( database = "aucfan_services_log_partition" output_location = "s3://output_bucket/2020/12/07" query = ` SELECT unixtime, duration FROM aucfan_paapi_nginx_errorlog WHERE year = 2020 AND month = 6 AND day = 1 AND status_code = 429 ORDER BY unixtime; ` )main
Go echoを使って、取得したAthenaのデータをJSON形式で返すAPIサーバーを立てます。
また、.envの読み込みもここでしています。godotenv.Load
の部分には作成した.envファイルまでのファイルパスを記述します。func main() { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(middleware.CORS()) if err := godotenv.Load(".env"); err != nil { log.Fatal(err) } e.GET("/", handler) e.Start(":1323") }handler
http://localhost:1323/ にアクセスした時にこの関数が実行されます。ここでは、Athenaでクエリを実行後、取得したデータをまとめたjsonファイルを返しています。
func handler(c echo.Context) error { fmt.Println(query) err := runQuery(query) //クエリ実行 if err != nil { return err } return c.File("data.json") }runQuery
クエリ、データベース、S3バケットを設定後、Athenaでクエリを実行します。クエリ実行後、クエリの実行が終わるまで待ちます。クエリの実行が成功(SUCCEEDED)したら、データを取得する作業に移ります。クエリの実行が失敗(FAILED)したら、処理を終了します。
func runQuery(query string) error { var ( err error r athena.ResultConfiguration ) sSess := newSession() svc := athena.New(sSess, aws.NewConfig().WithRegion("ap-northeast-1")) //クエリをセット var s athena.StartQueryExecutionInput s.SetQueryString(query) //データベースをセット var q athena.QueryExecutionContext q.SetDatabase(database) s.SetQueryExecutionContext(&q) //結果の吐き出し用のS3バケット fmt.Println(output_location) r.SetOutputLocation(output_location) s.SetResultConfiguration(&r) //クエリ実行 result, err := svc.StartQueryExecution(&s) if err != nil { return err } fmt.Println("StartQueryExecution result:") fmt.Println(result.GoString()) var qri athena.GetQueryExecutionInput qri.SetQueryExecutionId(*result.QueryExecutionId) var qrop *athena.GetQueryExecutionOutput fmt.Println("waiting...") for { qrop, err = svc.GetQueryExecution(&qri) if err != nil { return err } if *qrop.QueryExecution.Status.State == "SUCCEEDED" || *qrop.QueryExecution.Status.State == "FAILED" { break } } if *qrop.QueryExecution.Status.State == "SUCCEEDED" { err = plot(svc, result.QueryExecutionId) return err } else { fmt.Println(*qrop.QueryExecution.Status.State) } return err }newSession
AWSに接続するためのSessionを作成します。ここで、.envに記載してクレデンシャル情報を利用しています。
func newSession() *session.Session { awscfg := &aws.Config{ Region: aws.String("ap-northeast-1"), Credentials: credentials.NewStaticCredentialsFromCreds(credentials.Value{ AccessKeyID: os.Getenv("AWS_ACCESS_KEY_ID"), SecretAccessKey: os.Getenv("AWS_SECRET_ACCESS_KEY"), }), } sess := session.Must(session.NewSession(awscfg)) if len(os.Getenv("ROLE_ARN")) == 0 { return sess } creds := stscreds.NewCredentials(sess, os.Getenv("ROLE_ARN")) config := aws.Config{Region: sess.Config.Region, Credentials: creds} sSess := session.New(&config) return sSess }plot
クエリの実行結果を読み込み、jsonファイルに書き込みます。forループの中では、1行ずつ結果を読み込み、jsonファイルに書き込んでいます。
func plot(svc *athena.Athena, id *string) error { var ( token *string op *athena.GetQueryResultsOutput ) token = nil ioutil.WriteFile("data.json", []byte("["), os.ModePerm) f, err := os.OpenFile("data.json", os.O_APPEND|os.O_WRONLY, 0600) if err != nil { log.Print(err) return err } defer f.Close() for { //tokenを使い、全てのデータを取得する op, token, err = getData(svc, id, token) if err != nil { return err } var ( data Plot_data eol string //行末 ) //取得したデータをjsonファイルへ書き込む for i, s := range op.ResultSet.Rows { for j, t := range s.Data { if j == 0 { //Asia/Tokyoの部分を削除 data.Unixtime = strings.Replace(*t.VarCharValue, " Asia/Tokyo", "", 1) } else { data.Duration = *t.VarCharValue } } if i != 0 { if i == (len(op.ResultSet.Rows)-1) && token == nil{ eol = "]" } else { eol = "," } jsonBytes, err := json.Marshal(data) if err != nil { return err } out := new(bytes.Buffer) json.Indent(out, jsonBytes, "", " ") plot := out.String() + eol fmt.Fprint(f, "\n" + plot) } } if token == nil { break } } return err }getData
Athenaのクエリ実行結果を取得している部分です。実行結果は一度に最大1000件までしか取得できないため、1000件を超える場合はtokenを利用して、ページングにより次のデータを取得することができます。ループとtokenを使えば、全てのデータを取得することができます。
func getData(svc *athena.Athena, id *string, token *string) (*athena.GetQueryResultsOutput, *string, error) { var err error ip := &athena.GetQueryResultsInput{ QueryExecutionId: id, NextToken: token, } op, err := svc.GetQueryResults(ip) if err != nil { return nil, nil, err } return op, op.NextToken, err }確認
1.コンパイルします
$ go build2.プログラムを実行します
$ ./hoge3.ブラウザで http://localhost:1323/ にアクセスし、jsonデータが表示されればOK。
Vegaでの作業
- https://vega.github.io/editor/ にアクセスします。
- エディタに下記のような内容を記述すれば、右側に空っぽのグラフが作成されます。
{ "$schema": "https://vega.github.io/schema/vega/v5.json", "width": 400, "height": 400, "padding": {"left": 25, "top": 20, "right": 5, "bottom": 80}, "title": {"text": "Graph"}, "data": [ { "name": "table", "format": { "type": "json", "parse": {"unixtime": "date", "duration": "number"} }, "async": true, "url": "http://localhost:1323/" } ], "scales": [ { "name": "xscale", "type": "time", "domain": {"data": "table", "field": "unixtime"}, "range": "width", "padding": 0.05, "round": true }, { "name": "yscale", "domain": {"data": "table", "field": "duration"}, "nice": true, "range": "height" } ], "axes": [ { "orient": "bottom", "scale": "xscale", "labelAngle": 90, "labelAlign": "left", "format": "%b %d %H:%M" }, {"orient": "left", "scale": "yscale", "title": "Response Time [sec]"} ], "marks": [ { "type": "symbol", "from": {"data": "table"}, "encode": { "enter": { "size": {"value": 5}, "x": {"scale": "xscale", "field": "unixtime"}, "y": {"scale": "yscale", "field": "duration"} } } } ] }3.先ほど作成したgoのプログラムを実行します
$ ./hoge4.ブラウザのページを更新し、数秒後にグラフが表示されればOK。
- 投稿日:2020-12-08T11:18:41+09:00
本当にあった、「何もしていないのにEC2が動かなくなった」話
こんにちは。
Web・iOSエンジニアの三浦です。突然ですが皆さんは、何もしていないのにEC2が動かなくなった経験はありませんか?
今回は実際に私が体験したそんな話と、その原因について紹介します。はじめに
オンプレからAWSへの移行というのは、金銭的なコストやマネージングコストの削減、可用性の向上など、様々なメリットをもたらしてくれます。
そのため近年では、色々なところで移行が行われているのを耳にする方も多いのではないでしょうか。
ただ、上記のメリットがある一方で、勝手知ったるオンプレサーバからEC2に移行したとき、その仕様の違いによって様々な不幸がもたらされてしまうのも事実。
今回はそのうちの一つ、「何もしていないのに突然EC2が動かなくなった!」について、実際に私の身に起きた実体験を話していきたいと思います。1. AWS移行の完了と恐怖の始まり
私が担当していたサービスは、長いことオンプレサーバでWebサービスを提供し続けてきた長寿なサービスでした。
ただ、オンプレサーバでの管理にも限界がやってきて、2019年にAWSへの移行が決定。
2019年の後半からAWSへの移行が開始され、2020年3月、いよいよ切り替えが実行されました。
結果、大きな障害が起きることもなく、Webサーバやバッチサーバ、DB等も含めすべてのオンプレサーバをAWSに移行することができました。
念の為しばらくオンプレサーバも残すことにしつつ、しかしもう使うことも無いだろうと高をくくっていました。その3日後、突然Webサーバが動かなくなるまでは。
2. 難航する調査
それは突然起こりました。
新しくコードをデプロイしたわけでも、アクセスが急増したわけでもなく、本当に突然EC2が機能しなくなったのです。
それも、複数台に分散処理させていたWebサーバのうち一部のサーバのみが機能しなくなった形でした。正確には、CPU使用率が急激に落ちた、というものでした。
幸いRoute53を使って取り急ぎサービスをオンプレサーバに戻したので、長期的なサービス障害を避けることはできました。
その後原因を調査し始めたのですが、皆さんならまず何を疑うでしょうか?
パッと思いつくものとしては、
- EC2にt系を用いていた場合は、バースト機能のCPUクレジットが枯渇してCPU使用率が制限された
- ストレージが枯渇した
- AWSで障害が起きた
- オンプレと仕様が変わったため、ネットワーク周りで何かしらが詰まった
- Webサービスのアプリケーションで重い処理に当たってしまい、プロセスが増えすぎて詰まった
- 使用したEC2サーバが偶然にも不具合のあるサーバだった
このあたりではないでしょうか。
少なくとも、当時まだまだインフラ周りの知識の少ない私では、このあたりを思いつくのが精一杯でした。
ただ、調査した結果、「1. CPUクレジット枯渇」「2. ストレージ枯渇」「3. AWS障害」が起きていないことは確認でき、「4. ネットワーク周りでなにか詰まった」形跡も発見できませんでした。
そこで、「5. プロセスが増えすぎた」「6. 偶然不具合のあるEC2に当たってしまった」可能性を考え、以下の対策を行いました。
- アクセスログについて、各アクセスに掛かった処理時間も出力するようにする
- Webサーバ用のEC2を総入れ替えする
そして対策終了後、祈りながらサーバをEC2に再度切り替えました。
3日後、祈り虚しく再びオンプレサーバに出番が出てくるまで、EC2サーバは正常に動いていました…。アクセスログを見ても長すぎる処理は確認できず(というより、機能しなくなった時間帯のアクセスログは出力されていませんでした)、Webサーバも入れ替えているはずなのに結局障害が起きる始末。
この、「障害発生 -> 原因調査 -> 対策 -> 再度切り替え -> 障害発生」のループは、その後何度か繰り返されつつ、原因が判明することは一向にありませんでした。3. 持たさられるヒント
このループが何度も繰り返されていたある時のこと、調査時に気になる点を見つけました。
それは、「ファイルIOが詰まっているように見える」こと。
確かにそのサービスは、ログやキャッシュなどでかなりファイルIOしているサービスだったのは確かでしたが、とはいえストレージには十分余裕がありました。
一縷の望みを掛けてなんとか不要なログ出力を減らしたりはしたものの、結局は障害は起き続けるばかり。
心の端には留めていたものの、しかしまさかこれが原因だとは思わず、他の調査に当たっていました。しかし、これこそがまさしく障害の原因だったのです。
4. 原因の判明と解決
皆さんは、「バースト・クレジット枯渇」と言ったら何を思い浮かべますか?
多くの人はt系のEC2のCPU使用率を思い浮かべると思います。
私もその一人だったわけですが、実はそれ以外にも、バースト・クレジットの形式を持っているサービスがあったのです。EBSです。
EBSといえばEC2等に使用するストレージなわけですが、そんなEBSの何にバーストがあり、何にクレジットがあるかというと、実はファイルのIO回数にバーストが設定されているのです!
EBS一覧のモニタリングでも確認できます。
バースト・クレジットはEBSのストレージタイプによって有無が分かれますが、今回使っていたgp2ボリュームはバースト・クレジットが存在しており、そしてこれが尽きたことによってファイルへのIOが急激に遅くなったことが、今回の障害の原因だったのです。
3日とはクレジットが枯渇するまでの時間であり、それによって切り替え後すぐではなく時間差で障害が起きていたのでした。
すべてのサーバが一度に機能を停止しなかったのは、サーバごとに微妙にファイルIO数にばらつきがあったためであり、またエラーログの削減だけで障害が止まらなかったことについては、行った対策だけでは十分ファイルIOを減らせなかったものだと思われます。ある日の調査で偶然にもこれに思い当たり、まさしく上図のメトリクスでクレジットが尽きていたことを確認した私は、大急ぎで対策を行いました。
EBSのファイルIOのバーストのベースライン(クレジットが減らない最大IO数)は、gp2ボリュームの場合EBSのストレージサイズが大きくなることに比例して高くなっていきます。
当初はオンプレの時に使われていたストレージサイズをもとに必要なストレージサイズを計算していましたが、クレジットが枯渇しないことを念頭に必要なストレージサイズを再計算して適用し、結果ようやくEC2の障害が止まることになったのでした。さいごに
EC2は、AWSのサービスの中でも特に使われることの多いサービスの1つだと思います。
そこに付属する形でEBSもかなり使われることとなるかと思いますが、どこか「EC2の付属サービス」という認識があり、そこに何かしらの障害の原因があったときに原因の究明に時間がかかることもあるのではないでしょうか。
オンプレからAWSへの移行時なども、基本的にはオンプレのときに使われていたストレージサイズをもとにEBSのストレージサイズを決めることも多いかと思います。この記事で、原因究明の時間が少しでも減少すれば、または障害の予防になれば幸いです。
- 投稿日:2020-12-08T11:14:51+09:00
コンテナイメージではない大きなコードをAWS Lambdaに上げる裏技
こんにちは。
Web・iOSエンジニアの三浦です。今回は、コンテナイメージにしていないサイズの大きなコードを、AWS Labmdaにデプロイする裏技を紹介します。
はじめに
今でこそAWS Lambda(以降Lambda)は2020年12月4日に10GBまでのコンテナイメージが使用できるようになりましたが、それまでは最大で250MBまでしかコードをデプロイすることができませんでした。
まだその制限があった際に250MBを超えるコードのデプロイが必要になったことがあり、とある裏技を使うことでなんとか回避してデプロイすることができたので、今回はその方法について紹介していきます。AWSとLambdaの紹介
AWSとは
AWSは、Amazonが運営するクラウドサービスです。
サーバやデータベース、その他開発に必要/有用な様々なサービスを提供してくれています。
使用するメリットとして、使った分だけ請求されることや、管理をAWS側に任せることができることが挙げられます。Lambdaとは
Lambdaは、AWSが提供しているサービスの一つで、コードをデプロイして動かす事のできるサービスです。
サーバそのものに加え、内部の設定や負荷分散してくれる機能なども含めて提供してくれているサービスであり、エンジニアは、コードのデプロイと多少のパラメータの設定をするだけで良いというものになっています。Lambdaの制限とその回避方法
Lambdaに存在する制限
上記で挙げたとおりLambdaは非常に便利なサービスですが、一方でいくつか制限も存在します。
その一つが、コンテナイメージでないコードだと、デプロイできるコードが最大250MBということです。
私が以前行っていたプロジェクトでは、以下のシステムをLambdaで実現しようとしたときに250MBを超えてしまいました。インターネット上のサイトをChromeのバイナリを使ってスクレイピングし、その結果をDBに保存するシステム。
Chromeのバイナリと、それを操作してデータを取得・DBに保存するPythonのコードをLambdaにデプロイする。特にChromeのバイナリが重く、全て合わせると250MBを超えてしまっていました。
ただ、いくつかの事情からこの状態でLambdaでの運用をする必要があり、色々検討した結果、以下の方法でこれをデプロイ・実行できることが判明しました。Lambdaのコードサイズ制限の回避方法
回避方法といってもシンプルなもので、以下のようにChromeのバイナリを圧縮した、というものです。
ChromeのバイナリのみZIPとして固めた状態でLambdaにデプロイし、Lambda実行時にコード上で解凍して使用します。
このときのプロジェクトでは、Chromeのバイナリを圧縮すれば250MBを下回ったので、この方法でなんとかデプロイすることができました。
また、解凍後に250MBを超えてしまいますが、現状(2020年11月23日現在)では問題なく動いています。まとめ
Lambdaには、コンテナイメージでないコードは250MBまでしかデプロイできないという制限がありますが、一部のコードやバイナリなどを圧縮してデプロイし、Lambda実行時に解凍して使用する、という裏技を使うことで、250MBを超えていてもデプロイできます。
ただし、以下の懸念点も存在します。
- 使用する言語・フレームワークによっては、実行時に解凍して使用できない可能性がある
- 圧縮しても250MBを上回る場合はデプロイできない
- 現状では、デプロイ時に250MBを下回っていさえすれば、実行時に解凍して250MBを超えていても問題ないが、将来的に問題ないという保証はない
以上の点から、250MBを上回ってしまう場合は、コンテナイメージを使用したり、EC2・ECS等を使用するほうが安全性は高いでしょう。
ただ、もしどうしようもない理由がある場合は、こちらの裏技を試してみてもいいかも知れません。参考になれば幸いです。
- 投稿日:2020-12-08T10:52:06+09:00
ECSのデプロイ方法を見直して任意のタイミングでコミットハッシュをタグに使ったイメージをデプロイできるようにした
こんにちは。
弁護士ドットコムでSREをやっている @t2ynkmr です。
この記事は 弁護士ドットコム Advent Calendar 2020 の9日目の記事です。TL;DR
- CodePipeline を使った ECS への自動デプロイをやめた
- CodePipeline のデプロイステージで行われている処理の一部をビルドステージで実行させた
- CodeDeploy にリビジョン登録する際にコミットハッシュをタグにしたイメージを紐付けることで latest タグ運用から卒業した
- いろいろやったけど1から作れるなら ecspresso でやるのがよさげ
はじめに
みなさん、ECS で動かすコンテナは latest タグを使わずにに運用していますか?
去年の7月には ECR でイミュータブルなコンテナタグがサポートされ、クラスメソッドさんのブロクでも ECS デプロイのアンチパターンとして紹介されています。私が主に担当しているクラウドサインというサービスでもばっちり latest タグ運用をやってました…
これは ECS のデプロイ方法の見直しに合わせて latest タグ運用を脱却し、コミットハッシュをタグに使ったイメージを指定してデプロイできるようにした記録です。
ECS デプロイの見直し
クラウドサインではサービスのコンテナ化と ECS への移行を進めています。
その際に terraform で作成した構成は以下のようになっています。リポジトリへの操作をトリガーにして CloudWatchEvents で CodePipeline を動かして latest タグでイメージを更新し ECS へのデプロイまで自動で行うものでした。
これはこれで便利だったのですが ECS を運用していく中でエンジニアから要望が上がってきました
- 今動いているイメージがどの時点でビルドされたのか把握したい
- エラーが発生したらなるべく早く切り戻したいので過去のコミットハッシュを指定したデプロイを行いたい
- revert コミットでの対応ではエラー発生時間が伸びてしまう
- 任意のタイミングでデプロイをしたい
ごもっともです。みんなえらい。
ECS のデプロイ方法を見直してこれらの要望を実現できないか検討することにしました。やること/やらないこと
デプロイの見直しにあたりまずは状況を整理します。
やること
要望にもあったとおり以下ですね。
- デプロイされたイメージがいつの時点でビルドされたものか表示
- 過去のコミットハッシュを指定したデプロイ
- 任意のタイミングでデプロイ
やらないこと
やらないことも整理しておきます。
- コンテナオーケストレーションサービスの変更
- k8s なら…等も頭をよぎりましたが、解決したい課題に対して影響範囲が大きいので ECS のままいい感じにできる方法を模索します
- ECS のデプロイタイプの変更
- ECS ではデプロイタイプとしてローリングデプロイではなく Blue/Green デプロイを利用しています。これはこれでツラミがあるのですがデプロイタイプの見直しも今回は行いません
- 3rd Party のデプロイツールの利用
- 既存の EC2 のデプロイにて CodeDeploy を利用しています。デプロイツールが乱立するのもツラいのでひとまずは既存の資産を活かせる形での変更を模索します
見直しの前に引き続き既存のデプロイでやっている処理を整理します。
既存のデプロイについて
EC2
EC2 へのデプロイはリポジトリへの操作をトリガーにして CI で CodeDeploy にリビジョンを登録しています。
リビジョンの description にgit show HEAD --oneline | head -n1
を登録することで、リビジョンを一覧表示した際にコミットハッシュで絞り込めるようにしてあります。
デプロイをする際は slack から bot 経由で CodeDeploy を操作して登録されたリビジョンの確認やデプロイを実施しています。
リビジョンの表示によりデプロイされたアプリケーションがいつビルドされたのか確認可能です。操作感的にはこれと同様な仕組みができればエンジニアが新しいデプロイ方法を学ぶことなくやりたいことが実現できそうです。
ECS
前述したとおり ECS への自動デプロイはリポジトリへの操作をトリガーにして CodePipeline 経由で実行されます。
ビルドステージで CodeBuild によるイメージのビルド、デプロイステージで CodeDeploy によるデプロイが行われています。デプロイの見直し方針
CodePipeline が実行しているデプロイステージを読み解いてやれば EC2 へのデプロイのように任意のタイミングでのデプロイが実現できそうです。
デプロイステージで行われる CodeDeploy を使った Blue/Green デプロイの処理をもう少し細かく見てみます。調査
デプロイステージの設定としてアプリケーション名やデプロイメントグループだけでなく以下を設定しています。
- ECS のタスク定義
- CodeDeploy の AppSpec ファイル
CodeDeploy にこれらを渡して create-deployment で終わり\(^o^)/
と思いきやそんなに簡単ではありませんでした…。create-deployment のオプションではリビジョンが指定可能ですがタスク定義は指定できません。
ECS の場合 appspec.yml がリビジョンにあたることは記載されていますが、単純にこの2つを設定することは難しそうです。
デプロイステージで設定されるタスク定義と AppSpec で何が行われているのか確認してみます。デプロイステージに設定するタスク定義
CodePipeline のチュートリアルを確認するとプレースホルダーを含んだタスク定義を設定しています。
taskdef.json{ "executionRoleArn": "arn:aws:iam::account_ID:role/ecsTaskExecutionRole", "containerDefinitions": [ { "name": "sample-website", "image": "<IMAGE1_NAME>", "essential": true, "portMappings": [ { "hostPort": 80, "protocol": "tcp", "containerPort": 80 } ] } ], "requiresCompatibilities": [ "FARGATE" ], "networkMode": "awsvpc", "cpu": "256", "memory": "512", "family": "ecs-demo" }プレースホルダーを利用したタスク定義を設定することで、イメージ名を動的に更新したタスク定義を生成し、タスク定義を更新(新しいリビジョンを発行)しているようです。
ちなみにプレースホルダーは
imageDetail.json
というファイルに記載されたイメージ名が参照されます。
自動的に作成される場合もありますが、ビルドステージでイメージをビルドするような場合には作成してやる必要があります。 1デプロイステージに設定する AppSpec
同じく CodePipeline のチュートリアルを確認するとこちらもプレースホルダーを含んだ AppSpec ファイルを設定しています。
appspec.ymlversion: 0.0 Resources: - TargetService: Type: AWS::ECS::Service Properties: TaskDefinition: <TASK_DEFINITION> LoadBalancerInfo: ContainerName: "sample-website" ContainerPort: 80CodeDeploy で ECS へデプロイする場合は AppSpec ファイルがリビジョンとして登録されます。
プレースホルダーを利用して前述の json を利用して発行されたリビジョンのタスク定義の ARN を動的に設定して、CodeDeploy が利用するリビジョンとして登録しているようです。デプロイステージでの処理
デプロイステージでは CodeDeploy の実行だけではなく CodePipeline により以下のような CodeDeploy 実行前段階の処理が行われていました。
- ビルドしたイメージを指定したタスク定義の更新
- タスク定義を反映した AppSpec の作成
- CodeDeploy へのリビジョン登録
そもそもこのデプロイステージでの設定を適切に実施してやることでコミットハッシュをタグにしたイメージをデプロイすることができます。
デプロイされたイメージがいつの時点でビルドされたものか表示
という部分は解決しそうです。見直し方針策定
デプロイステージの仕組みを準備してやることで CodeDeploy でのデプロイ前の状態を再現できることがわかりました。
次のような方針でやりたいことが実現できそうです。
- デプロイタイミングの制御のために CodePipeline でデプロイステージは設定しない
- デプロイステージで行われている CodeDeploy の前段階の処理はビルドステージで設定
- デプロイステージで行われている CodeDeploy 自体の処理は既存の EC2 のデプロイ処理を使いまわす
具体的には以下の様に設定します。
- リポジトリへの操作をトリガーに CodePipeline のソースステージが実行される
- CodePipeline のビルドステージで CodeBuild を実行する
- コミットハッシュをタグにしたイメージをビルドして ECR にプッシュ
- コミットハッシュをタグにしたイメージを参照するタスク定義を作成して ECS に登録
- ECS に登録したタスク定義を利用する appspec.yml をリビジョンとして CodeDeploy に登録
- CodePipeline 完了後に slackbot から CodeDeploy の情報を参照してデプロイを行う
これで複数人がリポジトリを更新していても 1〜2 の処理が自動で行われ、最終的に任意のタイミングでデプロイできるようになりそうです。
デプロイ見直し結果
見直し方針を踏まえて CodePipeline のステージ設定を変更し以下のような構成になりました。
ビルドステージまでで CodeBuild でのイメージビルド、CodeDeploy へのリビジョン登録のみ実施し、デプロイ自体は CodeDeploy に登録されたリビジョンの情報を利用して任意のタイミングで行います。
設定としては buildspec.yml を以下の様に更新してアプリケーションのリポジトリに登録しました。
(前提として ECS のクラスター、サービスが作成されていること(タスク定義が存在していること)が必要です)buiidspec.ymlversion: 0.2 env: variables: AWS_DEFAULT_REGION: ap-northeast-1 AWS_ECR_URL: 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com CONTAINER_NAME: app CONTAINER_PORT: 9999 DOCKER_BUILDKIT: 1 phases: pre_build: commands: - COMMIT_SHORT_SHA=$(git rev-parse --short HEAD) - DESC_REVISION=$(git show HEAD --oneline | head -n1) - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 426005763013.dkr.ecr.ap-northeast-1.amazonaws.com build: commands: # コミットハッシュをタグにしたイメージをビルドして ECR にプッシュ - docker build -t ${AWS_ECR_URL}/${CONTAINER_NAME}:${COMMIT_SHORT_SHA} . - docker push ${AWS_ECR_URL}/${CONTAINER_NAME}:${COMMIT_SHORT_SHA} post_build: commands: # コミットハッシュをタグにしたイメージを利用するタスク定義を作成して ECS に登録 ## タスク定義を取得 - |- aws ecs describe-task-definition \ --task-definition ${CONTAINER_NAME} | \ jq '.taskDefinition | { family: .family, taskRoleArn: .taskRoleArn, executionRoleArn: .executionRoleArn, networkMode: .networkMode, containerDefinitions: .containerDefinitions, volumes: .volumes, requiresCompatibilities: .requiresCompatibilities, cpu: .cpu, memory: .memory }' \ > ./taskdef.json - sed -i -e "s#\"${AWS_ECR_URL}/${CONTAINER_NAME}:.*\"#\"${AWS_ECR_URL}/${CONTAINER_NAME}:${COMMIT_SHORT_SHA}\"#" ./taskdef.json ## タスク定義を更新 - aws ecs register-task-definition --cli-input-json file://$(pwd)/taskdef.json # ECS に登録したタスク定義を利用する appspec.yml をリビジョンとして CodeDeploy に登録 ## タスク定義の ARN を取得 - |- TASK_DEFINITION_ARN=$(aws ecs describe-task-definition \ --task-definition ${CONTAINER_NAME} | \ jq -r '.taskDefinition.taskDefinitionArn') ## CodeDeploy のリビジョンを作成する - |- cat << EOF > revision.json { "applicationName": "${CONTAINER_NAME}", "description": "${DESC_REVISION}", "revision": { "revisionType": "AppSpecContent", "appSpecContent": { "content": "{\"version\": 1,\"Resources\": [{\"TargetService\": {\"Type\": \"AWS::ECS::Service\",\"Properties\": {\"TaskDefinition\": \"${TASK_DEFINITION_ARN}\",\"LoadBalancerInfo\": {\"ContainerName\": \"${CONTAINER_NAME}\",\"ContainerPort\": ${CONTAINER_PORT}},\"PlatformVersion\": \"1.4.0\"}}}]}" } } } EOF - aws deploy register-application-revision --cli-input-json file://$(pwd)/revision.jsonこれでリポジトリへの操作をトリガーに CodeDeploy にコミットハッシュ単位でリビジョンが登録されます。
あとは slack から呼び出す bot の処理に ECS のアプリケーション情報を追加してやれば既存のデプロイ処理と同様にリビジョンを呼び出してデプロイできそうです。bot 自体の説明は割愛しますがこのような感じで動いています。
デプロイできるリビジョンの確認
CodeDeploy の list-application-revisions と get-application-revision で取得した情報をregisterTime
でソートして整形して表示しています。デプロイしたリビジョンの確認
CodeDeploy のlist-application-revisions
とget-application-revision
で取得した情報をlastUsedTime
でソートして整形して表示しています。いい感じです。これでやりたかったことが実現できました。
開発者もデプロイへの抵抗を感じることがなく、作業できそうです。おわりに
回りくどいことをしたような気もしていますが、既存のデプロイと使用感をあわせて ECS にもデプロイできるようにできたので開発者フレンドリーな方法を採用できたのではないでしょうか。
また既存のデプロイへの影響を考慮し 3rdParty ツールを使わないという決意の元で実施したのですが、調べた限りでは ecspresso を利用すれば同じことがもっとシンプルにできそうでした。
1からデプロイの仕組みをつくるならこっちかな…。ともあれAWS上のサービスを利用してデプロイしたい状況とコンテナのタグ運用の見直しの参考になれば幸いです。
明日は弁護士ドットコムの技術顧問である @koriym さんです。お楽しみに
- 投稿日:2020-12-08T10:06:06+09:00
AWS Load Balancer ControllerのTargetGroupBindingを試す
はじめに
※HISYSのアドベントカレンダー12日目の記事です。
最近私はAWS EKSをよく触っているのですが、先日EKS周りのアップデートとして気になるトピックが挙がっていました。今まで「ALB Ingress Controller」と呼ばれていた、AWSのALBをKubernetesリソースとして操作するためのモジュールの後継(v2)という立ち位置で、「AWS Load Balancer Controller」というコントローラがリリースされたというアップデートです。
v2になって新しく追加された機能としては、下記の通りです。
- NLBをサポート
- 複数のIngressリソースでALBの共有が可能に
- TargetGroupBindingというカスタムリソースが新しく作成されるようになった
この中で、今回は3番目のTargetGroupBindingについて書こうと思います。
まだネット上でこの機能について言及されているそんなに記事が多くないように感じますが、私的にはまさに抱えていた課題の解決にぴったりの機能だったので、少しでも同じ悩みを抱えている方の助けになれば幸いです。従来のEKSとALB周りのパターンと課題
そもそも、冒頭で述べたALB Ingress Controllerとは、Kubernetes上でALB IngressというIngressリソースが作成されたときにAWS上のリソースであるALBをデプロイ、管理してくれるモジュールです。
Kubernetesでクラスター外からクラスター内のPodへのルーティングを提供する場合LoadBalancerのServiceを使うという方法もありますが、EKSでLoadBalancer Serviceを作成するとCLBがデプロイされます。
L7での制御が必要になるユースケースでは、できればCLBではなくALBを利用したいのですが、その際の選択肢として今回は以下の3つを考えてみます。①ALB Ingress Controllerを利用してALBもKubernetes側からデプロイ、管理する
②AWS側でALBを作成し、ServiceをNodePortとして公開する
③AWS側でALBを作成し、NGINX Ingress ControllerをNodePortとして公開する①ALB Ingress Controllerを利用してALBもKubernetes側からデプロイ、管理する
【メリット】
ALBの作成、Service(ClusterIPもしくはNodePort)へのルーティングをマニフェストで宣言的に管理することができる。
【デメリット】
ALBのライフサイクルがKubernetes側のライフサイクルと切り離せないため、その他AWSリソースとALBが連携されている場合などでは管理が複雑になる②AWS側でALBを作成し、ServiceをNodePortとして公開する
【メリット】
ALBとKubernetesの関係が、①よりは疎結合になっている
【デメリット】
TargetGroupとNodePortとして公開したポートの紐づけを行わなくてはいけないため、Service側で変更(例えばサービスの追加など)をしたときにALB側も修正しなくてはいけなくなる③AWS側でALBを作成し、NGINX Ingress ControllerをNodePortとして公開する
【メリット】
ALBとKubernetesの関係が完全に疎結合になっている
【デメリット】
NGINXをNodePortとして公開する必要があるが、その場合アクセス元IPの取得制限等がある
https://kubernetes.github.io/ingress-nginx/deploy/baremetal/#over-a-nodeport-service以上の3ケースからの考察をまとめると、
・ALBのライフサイクルとKubernetesのライフサイクルを切り離し疎結合にしながらも、
・Serviceへのルーティング部分の管理性はKubernetes側に持たせて、
・NGINX Ingress ControllerをNodePortとして運用しなくていい
ソリューションが理想だと思われます。
そんなソリューションあるのだろうか…(棒読みそれ、AWS Load Balancer TargetGroupBindingsでできます
予定調和感が否めませんが、AWS Load BalancerのTargetGroupBindingsを利用すれば、上記の課題が解決できると考えています。
ALBおよびTargetGroupはCloudFormationやTerraform等AWS側のツールでデプロイを行い、Kubernetes側ではCustomResourceDefinitionであるTargetGroupBindingsというリソースを作成することで、AWS Load Balancer ControllerはTargetGroupからKubernetes Serviceへのルーティングのみを制御することが可能です。
これにより、KubernetesからALB自体のリソースのライフサイクルを切り離しつつ、いい感じにServiceへのルーティング等の管理をKubernetes側に移譲することができます。
今のところ、この分け方が一番管理的にはきれいになるのではないかと思っています。いざ実装
※下記環境で確認しています。
OS : Amazon Linux2
aws cli : aws-cli/1.18.147
eksctl : 0.33.0
まずはサクッとEKSクラスターを作ってしまいましょう。eksctl create cluster --name=sample-cluster --nodes=2 --node-type=t2.medium30分弱ほどコーヒーでも飲みながら待ちましょう。
プロンプトが返ってきたら、kubectlで確認。kubectl get nodes NAME STATUS ROLES AGE VERSION ip-192-168-34-63.us-west-2.compute.internal Ready <none> 3m1s v1.18.9-eks-d1db3c ip-192-168-6-110.us-west-2.compute.internal Ready <none> 3m2s v1.18.9-eks-d1db3cおお、できてるできてる。eksctlで作成されたVPC-IDをメモっておきましょう。あとで使います。
いい感じですね。
ではALBを作っていきます。
まずはTargetGroupから
今回はClusterIPにルーティングするので、target typeをIP addressにします。Target group nameを入力して、VPCはeksctlで作成されたvpcを選択します。
※注意!NodePortでサービスを公開する場合は、instanceを選択します。ここが間違っているとTargetGroupから作り直しになります。
他はすべてデフォルトで作成します。出来上がったTargetGroupのArnもメモっておきましょう。これも後で使います。
次はALBです。
ALBを選びましょう。
名前を入力します。
VPCはeksctlで作成されたVPCを、サブネットはeksctlで作成されたPublicSubnetを選択。
次へ次へと押していき、手順3では一旦検証のために自分のマシンに対して80番を開けたSGを作成します。
手順4ルーティングの設定では先ほど作ったTargetGroupを指定しましょう。
あとは次へを連打します。
CLIに戻って、AWS Load Balancer Controllerを導入します。
今回はeksctlで作成したので、service account等の設定もeksctlだけで完結することもできるのですが、CloudFormationで作成した場合はそうもいきません。そんな人でも大丈夫なようにシェルを用意しました。
eksctlでやる場合はこちらAWSLoadBalancer.shCLUSTER_NAME=<your_cluster_name> AWS_DEFAULT_REGION=<your_region_name> VPC_ID=<cluster_vpc_id> # oidc providerの作成及びeksclusterとの紐づけ eksctl utils associate-iam-oidc-provider \ --cluster $CLUSTER_NAME \ --approve # ALB Controller用のPolicy作成 curl -o iam-policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json aws iam create-policy \ --policy-name $CLUSTER_NAME-ALB-Policy \ --policy-document file://iam-policy.json IAM_ARN=$(aws iam list-policies --query 'Policies[?PolicyName==`'$CLUSTER_NAME'-ALB-Policy`].Arn' --output text) # ALB Controller用のRole作成 ISSUER_URL=$(aws eks describe-cluster \ --name $CLUSTER_NAME \ --query cluster.identity.oidc.issuer \ --output text) ISSUER_HOSTPATH=$(echo $ISSUER_URL | cut -f 3- -d'/') ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) PROVIDER_ARN="arn:aws:iam::$ACCOUNT_ID:oidc-provider/$ISSUER_HOSTPATH" cat > irp-trust-policy.json << EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "$PROVIDER_ARN" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "${ISSUER_HOSTPATH}:sub": "system:serviceaccount:kube-system:aws-load-balancer-controller" } } } ] } EOF ROLE_NAME=$CLUSTER_NAME-ALB-IAM-Role aws iam create-role \ --role-name $ROLE_NAME \ --assume-role-policy-document file://irp-trust-policy.json aws iam update-assume-role-policy \ --role-name $ROLE_NAME \ --policy-document file://irp-trust-policy.json aws iam attach-role-policy \ --role-name $ROLE_NAME \ --policy-arn $IAM_ARN ALB_ROLE_ARN=$(aws iam get-role \ --role-name $ROLE_NAME \ --query Role.Arn --output text) # ALB Controller用のservice account作成 kubectl create sa aws-load-balancer-controller -n kube-system kubectl annotate sa aws-load-balancer-controller eks.amazonaws.com/role-arn=$ALB_ROLE_ARN -n kube-system # ALB Controller用にcert-managerを設定 kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.0.2/cert-manager.yaml echo '[MESSAGE] wait for cert-manager...' sleep 30 # ALB Controller用のManifestを持ってきて修正してapply curl -o v2_0_0_full.yaml https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/v2_0_0_full.yaml sed s/your-cluster-name/$CLUSTER_NAME/ v2_0_0_full.yaml > ALB_Controller_manifest.yaml sed -i -e "/ingress-class=alb$/a \ - --aws-region=$AWS_DEFAULT_REGION" ./ALB_Controller_manifest.yaml sed -i -e "/ingress-class=alb$/a \ - --aws-vpc-id=$VPC_ID" ./ALB_Controller_manifest.yaml kubectl apply -f ALB_Controller_manifest.yamlここまででAWS Load Balancer Controllerが作成されているはずです。
確認用のnginxサンプルをデプロイします。nginx.yamlapiVersion: v1 kind: Namespace metadata: name: sample-ns --- apiVersion: v1 kind: Service metadata: name: service-sample-nginx namespace: sample-ns labels: app: sample-nginx spec: selector: app: sample-nginx ports: - protocol: TCP port: 80 targetPort: 80 --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment namespace: sample-ns labels: app: sample-nginx spec: selector: matchLabels: app: sample-nginx replicas: 3 template: metadata: labels: app: sample-nginx spec: containers: - name: nginx image: nginx:1.19.2 ports: - containerPort: 80kubectl apply -f nginx.yaml
いよいよTargetGroupBindingを作成してみましょう。
にはTargetGroupを作成した際にメモしたARNを、にはalbに紐づいているSecuirtyGroupIDを入れましょう。target-group-binding.yamlapiVersion: elbv2.k8s.aws/v1alpha1 kind: TargetGroupBinding metadata: name: sample-tgb namespace: sample-ns spec: serviceRef: name: service-sample-nginx port: 80 targetGroupARN: <your-targetgroup-arn> targetType: ip networking: ingress: - from: - securityGroup: groupID: <your-alb-securitygroup> ports: - protocol: TCPデプロイ。
kubectl apply -f target-group-binding.yaml
うまくいっていそうですね。
TargetGroupにもちゃんとPodのIPアドレスが登録されています。
画像はdeploymentを更新したときのもので、Podが入れ替わってもちゃんとAWS Load Balancer Controllerが追いかけてくれているのがわかります。以上AWS Load Balancer ControllerのTargetGroupBindingを試してみましたが、どうだったでしょうか。実際にこの手順をなぞると1時間程度でできるのでぜひぜひやってみてください。
EKSに対するハードルが少しでも下がれば幸いです。記載されている会社名、製品名、サービス名、ロゴ等は各社の商標または登録商標です。
- 投稿日:2020-12-08T09:56:45+09:00
【AWS】ファーストタッチレイテンシーの現象を試してみた
Snapshotからリストアしたボリュームへの初回アクセスはパフォーマンスが落ちる
Snapshotからボリュームをリストアした場合、ブロックに初めてアクセスする時にS3にデータを取りに行きます。そのため、初回アクセスのパフォーマンスが落ちます。これをがファーストタッチレイテンシー(ファーストタッチペナルティ)と呼びます。
今回はこの現象を確認していきます。
検証環境とシナリオ
- Windowsサーバを一台たて、EBSボリュームでCドライブ(120GB:gp2)とDドライブ(120GB:gp2)の準備をします。
- Cドライブに10GBのデータを作成します。
- DドライブのSnapshotを作成します
- 既存のDドライブをデタッチし、取得したSnapshotから作成したボリュームを新しくDドライブとしてアタッチします
- ここで、CドライブからDドライブへデータを転送して時間を計測します
手順
環境構築のコマンドです。
Cドライべへダミーファイルを作成
fsutil file createnew dummy.data (10737418240)DドライブのEBSSnapshotを作成
aws ec2 describe-volumes aws ec2 create-snapshot --volume-id <voi_id>--tag-specification 'ResourceType=snapshot, Tags=[{Key=Name,Value=testsnapshot}]' --description "for test"https://docs.aws.amazon.com/cli/latest/reference/ec2/create-snapshot.html
:::message
空のボリュームだとファーストタッチレイテンシーが発生しないので、空である場合は何か適当にファイルを配置してください。
:::DドライブをEC2インスタンスからデタッチ
aws ec2 detach-volume --volume-id <vol_id>https://docs.aws.amazon.com/cli/latest/reference/ec2/detach-volume.html
DドライブをSnapshotからリストア
aws ec2 create-volume --snapshot-id <snapshot_id> --volume-type gp2 --availability-zone ap-northeast-1dhttps://docs.aws.amazon.com/cli/latest/reference/ec2/create-volume.html
DドライブをEC2にアタッチ
aws ec2 attach-volume --device <device> --instance-id <instance_id> --volume-id <vol_id>https://docs.aws.amazon.com/cli/latest/reference/ec2/attach-volume.html
CドライブからDドライブへ転送
$watch = New-Object System.Diagnostics.StopWatch $watch.Start() Copy-Item C:\Users\Administrator\Desktop\dummy.data D:\ $watch.Stop() $t = $watch.Elapsed "{0} min {1}.{2} sec" -f $t.Minutes,$t.Seconds,$t.Milliseconds計測まとめ
回数 経過時間 1回目 22 min 56.65 sec 2回目以降 1 min 19.341 sec まとめ
初回とそれ以降ではかなり差が開きました。ただ、本来であればデータを作成したボリュームのSnapshotを作成し、そのボリュームをリストアしてデータ転送を確認したかったのですが、それだとファーストタッチレイテンシーが発生しなかったので、そこは別で調べようと思います。
- 投稿日:2020-12-08T09:54:39+09:00
【AWS】PrivateHostedZoneを他のアカウントと関連付ける方法
Private Hosted Zone(PHZ)とは
PHZはDNSのレコードを格納するコンテナです。PrivateとPublicがありますが、Privateだと名前解決をVPC内のみで行うことができます。作成時にドメインを指定しますが、VPC内でそのドメインとサブドメインのDNSクエリに対応することができます。
アカウントAのPrivate Hosted Zoneに登録されたレコードをアカウントBから参照したい
まず、PHZはドメインとVPCを1:1で関連付けます。VPC内で完結するため、アカウントAのVPCからアカウントBのVPCの名前解決をすることはデフォルトではできません。方法として、PHZに別アカウントのVPCを関連付けるという操作を行います。これは、CLIから対応する必要があります。
今回の検証環境について
2つのアカウントを用意して、VPCAとVPCBを作成します。ちょっとわかりにくくなりましたが、黒点線での囲いはアカウント単位です(つまり、2つのアカウントが存在して、その中に環境を構築していることを表しています)
各VPCに紐づくPHZAとPHZBを作成し、各VPCに配置されたEC2インスタンスから別アカウントのEC2に対して、互いに名前解決ができるかを確認していきます。
検証を構築する
PHZを作成してレコード登録
アカウントAとBの両方でEC2とPHZを構築し、PHZにEC2のレコードを登録します。
アカウントAのレコードと現時点での名前解決の状況
同じVPC内の名前解決はできますが、別アカウントのVPCの名前解決はできません。
アカウントBのレコードと現時点での名前解決の状況
PHZと別アカウントのVPCの関連付け
まずは、アカウントAのPHZにアカウントBのVPCを関連付けます。
最初はこんな感じです。このVPCはアカウントAのVPCです。
- アカウントAで、実行IAMユーザの作成します。ユーザに
Route53FullAccess
を付与します。- 1.で作成したユーザで下記コマンドを実行する。つまり、アカウントA側で実行します。
aws route53 create-vpc-association-authorization --hosted-zone-id <AのPHZID> --vpc VPCRegion=ap-northeast-1,VPCId=<BのVPCID>
アカウントBで、実行IAMユーザの作成し、ユーザに
Route53FullAccess
、VPCFullAccess
を付与します。3.で作成したアカウントで下記コマンドを実行します。つまり、ここではアカウントB側で実行します。
aws route53 associate-vpc-with-hosted-zone --hosted-zone-id <AのPHZID> --vpc VPCRegion=ap-northeast-1,VPCId=<BのVPCID>
そうすると、こうなります。アカウントAのPHZにアカウントBのVPCを関連付けることができました。
次は、BのPHZにAのVPCを関連付けます。手順は同じです。
アカウントBで実行
aws route53 create-vpc-association-authorization --hosted-zone-id <BのPHZID> --vpc VPCRegion=ap-northeast-1,VPCId=<AのVPCID>
アカウントAで実行
aws route53 associate-vpc-with-hosted-zone --hosted-zone-id <BのPHZID> --vpc VPCRegion=ap-northeast-1,VPCId=<AのVPCID>
検証の結果をみてみる
検証結果を見てみます。先程はできなかった別アカウントの名前解決ができています。
- 投稿日:2020-12-08T09:52:38+09:00
AWS BakcupとSystemsManager AutomationでEC2バックアップを運用する方法
EC2バックアップ前に停止して、バックアップ取得後に起動したい
AWSではバックアップを取得する時に何かしらの方法で静止点を取得することを推奨されています。アプリケーションの機能で静止点を取る方法もありますが、やはりインスタンスを停止することは割と一般的だと思います。今までオンプレ使っていたりバックアップジョブを自前で実装していた人たちから見ると特に。
そこで、バックアップ前のEC2インスタンス停止からバックアップ取得、そしてEC2インスタンス起動までの処理をAWSのサービスを使用して実装していきます。
検証構成
EC2インスタンスを立てて、SystemManager AutomationとAWS Backupを利用します。処理の順番は、下記の通りです。
- SystemManager Automation の機能でEC2インスタンスを停止
- SystemManager Automation の機能でBakcupAPIを呼び出し、バックアップを取得
- Backupジョブが正常に投げられたらEC2インスタンスを起動
使用するサービスについて少し説明します
AWS Backup
下記にAWSドキュメントのリンクを張っておきます。
AWS Backupでは既存のサービスで提供されているバックアップ機能を利用して各サービスのバックアップを取得し、それを一元管理してくれるサービスです。取得時間を設定することもできるので、バックアップを取得するだけであればこのサービスを使用すれば問題ありません。
例えば、EC2インスタンスであれば
AMI
とEBS Snapshot
の取得・管理をしてくれます。:::message
EFSのバックアップを取得することもできます。対応サービスはリンクから確認できます。
:::バックアップを取得する
AWS Backupでは下記の方法を使用してバックアップを取得します。
- バックアッププランによる取得
- オンデマンドバックアップによる取得
既に書きましたが、今回はAutomationでBakcupAPIを実行します。そうすると、オンデマンドバックアップとしてジョブが投入されバックアップを取得します。
AWS System Manager Automation
Automationの機能をドキュメントから引用します。
Systems Manager オートメーションは、EC2 インスタンスおよび他の AWS リソースの一般的なメンテナンスとデプロイのタスクを簡素化します。
これだけだとわかりにくいのでもう少し見ていきます。Automationでは、ドキュメントという単位でタスクをまとめることができます。下記は
AWSによって管理されているドキュメント
です。これの
AWS-StartEC2Instance
を見てみます。このように、ドキュメントではStepという単位で実行されるアクションを指定します。このアクションはいくつか種類がありますのでリファレンスのURLを書いておきます。
Systems Manager オートメーションアクションのリファレンス
今回使用するのは下の2種類です。
- aws:changeInstanceState
- aws:executeAwsApi
では、実際に実装するところに入っていきます。
ドキュメント作成前に何をしておく必要があるか
BackupAPIを実行する時に、Roleを指定します。なので、BackupからEC2を操作できるように事前にロールを作成しておいてください。
私は、
ユースケースの選択
でAWS Backup
を指定し、ポリシーとしてAmazonEC2FullAccess
を付けました。そしたらロールARNをコピーしておいてください。
あと、対象となる
インスタンスIDのメモ
をしておいてください。StringList
で指定するため複数でも問題ありません。ドキュメントの作成しよう
AWS マネジメントコンソール画面で、
AWS SystemsManager
を選択します。左側のサービス一覧の一番したにある共有リソース
のドキュメント
を選択します。
選択した画面で上の方に
オートメーションを作成する
があるので選択してください。作成したドキュメントは自己所有タブ
の画面に表示されます。ドキュメント名などを入れたらステップの作成に入ります。今回は3つのステップを作成します。
- StopInstance(指定したEC2インスタンスを停止する)
- Run_API(指定したAPIを実行する)
- StartInstance(指定したEC2インスタンスを開始する)
まずは、①から見ていきます。アクションタイプにある
Change or assert instance State
がaws:changeInstanceState
に該当します。指定したインスタンスのステータスをDesired state
で指定したステータスに変更します。今回は停止のためstopped
を指定してます。対象のインスタンスは
StringList
で指定します。これは- i-xxx
という記述になります。インスタンスIDを書くだけだとエラーで作成できません。次に②について確認します。ここではアクションに
Call and run AWS API actions
を指定します。これがaws:executeAwsApi
に該当します。Service
にはAPIを叩くサービスを指定します。今回はAWS Bakupなのでbackup
になります。APIは実行するAPIコマンドを指定します。StartBakcupJob
となります。ここで追加の入力に引数となる値をいれます。上記リンクを読むと必須の引数は3つあります。
- BackupVaultName
- IamRoleArn
- ResourceArn
BackupVaultName
はバックアップ保存先の名前です。ここでは最初からあるDefault
を指定してます。次のIamRoleArn
はBackupからバックアップを取るサービスへのアクセス件げ付与されているロールARNを指定します。ここでさっき作成したロールARNを貼り付けます。3つ目はResourceArn
です。対象となるリソースのARNです。今回Backup対象となるEC2のIDのARNを指定してます。③は①の逆で、インスタンスの起動です。
Desired state
が変わるぐらいです。
これでドキュメントの作成は完了です。
ドキュメントの実行しよう
最後に実行する方法です。
SystemsManagerの画面の左側の真ん中あたりに
自動化
という項目があるので選択してください。
オートメーションの実行を選択し、その先で作成したドキュメントを選択してください。画面の一番下にいくと
次へ
というボタンがあるので選択し実行します。
以上で実行まで完了です。終わったらEC2からAMIとSnapshotを確認してもいいですし、AWS Backupにジョブが投入されていることを確認してもいいと思います。
最後に
今回Automationを使用して見ましたが、他にもLambdaを使用したりすることもできると思います。ただ、AWSのサービスかつコーディングなしで運用できるのはやはり便利なのではないかなと思います。
- 投稿日:2020-12-08T09:52:38+09:00
【AWS】AWS BakcupとSystemsManager AutomationでEC2バックアップを運用する方法
EC2バックアップ前に停止して、バックアップ取得後に起動したい
AWSではバックアップを取得する時に何かしらの方法で静止点を取得することを推奨されています。アプリケーションの機能で静止点を取る方法もありますが、やはりインスタンスを停止することは割と一般的だと思います。今までオンプレ使っていたりバックアップジョブを自前で実装していた人たちから見ると特に。
そこで、バックアップ前のEC2インスタンス停止からバックアップ取得、そしてEC2インスタンス起動までの処理をAWSのサービスを使用して実装していきます。
検証構成
EC2インスタンスを立てて、SystemManager AutomationとAWS Backupを利用します。処理の順番は、下記の通りです。
- SystemManager Automation の機能でEC2インスタンスを停止
- SystemManager Automation の機能でBakcupAPIを呼び出し、バックアップを取得
- Backupジョブが正常に投げられたらEC2インスタンスを起動
使用するサービスについて少し説明します
AWS Backup
下記にAWSドキュメントのリンクを張っておきます。
AWS Backupでは既存のサービスで提供されているバックアップ機能を利用して各サービスのバックアップを取得し、それを一元管理してくれるサービスです。取得時間を設定することもできるので、バックアップを取得するだけであればこのサービスを使用すれば問題ありません。
例えば、EC2インスタンスであれば
AMI
とEBS Snapshot
の取得・管理をしてくれます。:::message
EFSのバックアップを取得することもできます。対応サービスはリンクから確認できます。
:::バックアップを取得する
AWS Backupでは下記の方法を使用してバックアップを取得します。
- バックアッププランによる取得
- オンデマンドバックアップによる取得
既に書きましたが、今回はAutomationでBakcupAPIを実行します。そうすると、オンデマンドバックアップとしてジョブが投入されバックアップを取得します。
AWS System Manager Automation
Automationの機能をドキュメントから引用します。
Systems Manager オートメーションは、EC2 インスタンスおよび他の AWS リソースの一般的なメンテナンスとデプロイのタスクを簡素化します。
これだけだとわかりにくいのでもう少し見ていきます。Automationでは、ドキュメントという単位でタスクをまとめることができます。下記は
AWSによって管理されているドキュメント
です。これの
AWS-StartEC2Instance
を見てみます。このように、ドキュメントではStepという単位で実行されるアクションを指定します。このアクションはいくつか種類がありますのでリファレンスのURLを書いておきます。
Systems Manager オートメーションアクションのリファレンス
今回使用するのは下の2種類です。
- aws:changeInstanceState
- aws:executeAwsApi
では、実際に実装するところに入っていきます。
ドキュメント作成前に何をしておく必要があるか
BackupAPIを実行する時に、Roleを指定します。なので、BackupからEC2を操作できるように事前にロールを作成しておいてください。
私は、
ユースケースの選択
でAWS Backup
を指定し、ポリシーとしてAmazonEC2FullAccess
を付けました。そしたらロールARNをコピーしておいてください。
あと、対象となる
インスタンスIDのメモ
をしておいてください。StringList
で指定するため複数でも問題ありません。ドキュメントの作成しよう
AWS マネジメントコンソール画面で、
AWS SystemsManager
を選択します。左側のサービス一覧の一番したにある共有リソース
のドキュメント
を選択します。
選択した画面で上の方に
オートメーションを作成する
があるので選択してください。作成したドキュメントは自己所有タブ
の画面に表示されます。ドキュメント名などを入れたらステップの作成に入ります。今回は3つのステップを作成します。
- StopInstance(指定したEC2インスタンスを停止する)
- Run_API(指定したAPIを実行する)
- StartInstance(指定したEC2インスタンスを開始する)
まずは、①から見ていきます。アクションタイプにある
Change or assert instance State
がaws:changeInstanceState
に該当します。指定したインスタンスのステータスをDesired state
で指定したステータスに変更します。今回は停止のためstopped
を指定してます。対象のインスタンスは
StringList
で指定します。これは- i-xxx
という記述になります。インスタンスIDを書くだけだとエラーで作成できません。次に②について確認します。ここではアクションに
Call and run AWS API actions
を指定します。これがaws:executeAwsApi
に該当します。Service
にはAPIを叩くサービスを指定します。今回はAWS Bakupなのでbackup
になります。APIは実行するAPIコマンドを指定します。StartBakcupJob
となります。ここで追加の入力に引数となる値をいれます。上記リンクを読むと必須の引数は3つあります。
- BackupVaultName
- IamRoleArn
- ResourceArn
BackupVaultName
はバックアップ保存先の名前です。ここでは最初からあるDefault
を指定してます。次のIamRoleArn
はBackupからバックアップを取るサービスへのアクセス件げ付与されているロールARNを指定します。ここでさっき作成したロールARNを貼り付けます。3つ目はResourceArn
です。対象となるリソースのARNです。今回Backup対象となるEC2のIDのARNを指定してます。③は①の逆で、インスタンスの起動です。
Desired state
が変わるぐらいです。
これでドキュメントの作成は完了です。
ドキュメントの実行しよう
最後に実行する方法です。
SystemsManagerの画面の左側の真ん中あたりに
自動化
という項目があるので選択してください。
オートメーションの実行を選択し、その先で作成したドキュメントを選択してください。画面の一番下にいくと
次へ
というボタンがあるので選択し実行します。
以上で実行まで完了です。終わったらEC2からAMIとSnapshotを確認してもいいですし、AWS Backupにジョブが投入されていることを確認してもいいと思います。
最後に
今回Automationを使用して見ましたが、他にもLambdaを使用したりすることもできると思います。ただ、AWSのサービスかつコーディングなしで運用できるのはやはり便利なのではないかなと思います。
- 投稿日:2020-12-08T08:43:05+09:00
サーバーレスワークフローをTypeScriptで作成しよう 〜StepFunctionsとCDKによるLambdaの実行順序制御入門〜
この記事はAWS Advent Calendar 2020 - Qiitaの8日目の記事です。
ワークフロー型のアーキテクチャはAWSでよく見られるイベント駆動型のアーキテクチャと補完関係にあるアーキテクチャです。その考え方はシンプルで明示的に実行順序を記述することで処理の流れを表現します。本記事ではサーバーレスの中核を担う Lambda関数のワークフロー型の実行順序制御 を実現する方法、特に インフラ構築、ワークフロー作成、関数作成と呼び出しを全てTypeScriptで完結させる方法 について、その実現方法と利点を記載したいと思います。
イベント駆動とワークフロー
最初にイベント駆動型とワークフロー型のアーキテクチャについて簡単に説明します。すでにご存じの方は飛ばしていただいても構いません。
イベント駆動型のシンプルな構成は以下のとおりです。プロデューサがイベントを生成し、ブローカーがイベントを受け取り、コンシューマにイベントを渡します。
ここで重要なのはブローカはイベントを貯めてもいいし、フィルタして減らしてもいいし、逆に増やしても構いません。またイベントの宛先であるコンシューマを変えたり、複数のコンシューマにイベントを複製して配っても問題ありません。また、ブローカを多段に構成することもできます。要はプロデューサとコンシューマの間にイベントを自由に扱えるブローカをいくつも挟んで良い(挟まなくてももちろんOK)というのがこのアーキテクチャの肝であり、柔軟性やスケーラビリティの根源になっています。
対してワークフロー型は以下のように処理の順番が明白に決まっており処理順序は分かりやすいですが、イベント駆動型ほどの柔軟性はありません。
一般的には処理順序の分かりやすさやデバッグのしやすさを優先するならワークフロー型が適しており、柔軟性やスケーラビリティが必要であればイベント駆動型が適しておりお互いに補完関係にあるアーキテクチャだと言うことができます1。
AWSはアーキテクチャとしてスケーラブルな非同期分散処理に力を入れてきたのでイベント駆動型のアーキテクチャを支えるサービス(SQS, SNS, EventBridge, Kinesis, MQ, MSK等)が充実しています。しかし近年はStep Functionsの登場によりワークフロー型のアーキテクチャも広く使われるようになってきました2。
サーバーレスワークフローとは
サーバーレスは EC2のような仮想サーバを使わずにアプリケーションを開発するアーキテクチャ のことを指しますが、慣習的にはSaaSをLambda(FaaS)で連携させたり補完したりしてアプリケーションを構築するアーキテクチャを指します。サーバーレスワークフローは そのサーバーレスにワークフロー制御のSaaSであるStep Functionsを加えてワークフロー制御を実現したアーキテクチャ になります。
Step Functionsの弱点
Step Functionsの弱点は端的に言うとワークフローのビジュアルエディターが公式にはリリースされていないことです3。ワークフローを作成・編集するにはAmazon ステートメント言語(ASL)というJSONベースの言語で記述する必要があります。だたJSONで記述したワークフローの可視化は行えます。以下はAWS公式のサンプルのHelloWorldのステートマシンをStep Functionsのグラフインスペクターで可視化したものです。
上記のワークフローをASLで記述したものが以下になります。さすがにこれを記述するのは厳しいと感じる方が多でしょう。
{ "Comment": "A Hello World example demonstrating various state types of the Amazon States Language", "StartAt": "Pass", "States": { "Pass": { "Comment": "A Pass state passes its input to its output, without performing work. Pass states are useful when constructing and debugging state machines.", "Type": "Pass", "Next": "Hello World example?" }, "Hello World example?": { "Comment": "A Choice state adds branching logic to a state machine. Choice rules can implement 16 different comparison operators, and can be combined using And, Or, and Not", "Type": "Choice", "Choices": [ { "Variable": "$.IsHelloWorldExample", "BooleanEquals": true, "Next": "Yes" }, { "Variable": "$.IsHelloWorldExample", "BooleanEquals": false, "Next": "No" } ], "Default": "Yes" }, "Yes": { "Type": "Pass", "Next": "Wait 3 sec" }, "No": { "Type": "Fail", "Cause": "Not Hello World" }, "Wait 3 sec": { "Comment": "A Wait state delays the state machine from continuing for a specified time.", "Type": "Wait", "Seconds": 3, "Next": "Parallel State" }, "Parallel State": { "Comment": "A Parallel state can be used to create parallel branches of execution in your state machine.", "Type": "Parallel", "Next": "Hello World", "Branches": [ { "StartAt": "Hello", "States": { "Hello": { "Type": "Pass", "End": true } } }, { "StartAt": "World", "States": { "World": { "Type": "Pass", "End": true } } } ] }, "Hello World": { "Type": "Pass", "End": true } } }従ってASL使わずに楽にワークフローを構築したいのですが、その選択肢の一つが本記事で紹介したいAWS CDKを利用したTypeScriptによるワークフローの構築になります。TypeScriptで記述することにより型の補完が使えたり、CDKがワークフローを抽象化してくれていたりするので生のJSONよりも大分書きやすくなっています。
AWS CDKについて簡単に補足すると、AWS CDKは、使い慣れたプログラミング言語を使用してクラウドアプリケーションリソースを定義するためのソフトウェア開発フレームワークです。CDKはいくつかのプログラミング言語をサポートしていますが、本記事ではTypeScriptを用います。
サーバーレスワークフローをTypeScriptで作成しよう
セットアップ
前提条件としてAWS CLIのセットアップとNode.jsのインストールは済んでいるものとします。
まず、CDKをインストールします。
$ npm install -g aws-cdk次にTypeScriptをインストールします。
$ npm install -g typescriptアプリケーションのディレクトリを作成します。
$ mkdir cdk-sfn $ cd cdk-sfnTypeScriptで初期アプリを作成します。
$ cdk init app --language typescriptこれでセットアップは完了です。
初めてのワークフローの作成から実行まで
ワークフローはcdk-sfn-stack.tsに書いていきます。コード編集にはVisualStudio Code等を用います。
以下が初期のファイルで、とりあずはコンストラクタにガシガシ書いていきます。lib/cdk-sfn-stack.tsimport * as cdk from '@aws-cdk/core'; export class CdkSfnStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // The code that defines your stack goes here } }コードを書き始める前に依存関係のあるライブラリをインストールしておきます。
$ npm install @aws-cdk/aws-stepfunctions次のコードは空の処理を3つ逐次実行するワークフローです。空の処理は
sfn.Pass
で作成し、next
メソッドで繋いでいきます。import * as cdk from "@aws-cdk/core"; import * as sfn from "@aws-cdk/aws-stepfunctions"; export class CdkSfnStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const start = new sfn.Pass(this, "Start", {}); const step1 = new sfn.Pass(this, "Step 1", {}); const end = new sfn.Pass(this, "End", {}); const definition = start.next(step1).next(end); // 処理を順番に繋ぐ const stateMachine = new sfn.StateMachine(this, "cdk-sfn-state-machine", { stateMachineName: "cdk-sfn-state-machine", definition, }); } }
deploy
コマンドでAWS上にStep Functionsをデプロイします。以下ターミナルから実行するとしばらくしてデプロイが完了します。$ cdk deployAWSマネジメントコンソールから「cdk-sfn-state-machiene」ステートマシーンが作成されていることを確認します。
対象をクリックしてステートマシンを開いた後に以下の赤枠で囲った「実行の開始」ボタンを押して実行します。
以下の実行開始ダイアログが出るので
「実行の開始」
を押して実行開始します。実行が完了したら以下の画面が表示されます。「Start」 → 「Step 1」 → 「End」というワークフローが実行されていることがわかると思います。
Step1をLambda関数に変更してみる
次にStep1をLambda関数にしてみたいと思います。Lambda関数は受け取ったJSONに格納された名前に対して挨拶するものとします。
まずは関連モジュールをインストールします。インストールする「aws-lambda-nodejs」はTypeScriptをビルドしてLambda関数を作成してくれる便利なものですが、注意点が2つあってまだ実験的なモジュールであることと、利用にはDockerが必要になることです。
$ npm install @aws-cdk/aws-stepfunctions-tasks @aws-cdk/aws-lambda-nodejs @types/aws-lambdaLambda関数は
labmbda/hello
というディレクトリを作成し、その下にindex.ts
という名前で以下のLambda関数を作成します。lambda/hello/index.tsimport * as lambda from "aws-lambda"; export async function handler( event: Event, context: lambda.Context, callback: lambda.Callback ) { return `hello ${event.name}`; } type Event = { name: string; };そして、ワークフローの中身を以下のように書き換えます。変化している箇所はLambda関数をソースの場所を指定して作成していることと、そのLambda関数を呼び出すStep Functionsのタスクを作成しているところです。注目すべきは
Start
の出力結果としてJSONオブジェクトを生成しているところです。Step Functionsでは前の処理の出力結果を次の処理の入力として利用することができます。import * as cdk from "@aws-cdk/core"; import * as sfn from "@aws-cdk/aws-stepfunctions"; import * as tasks from "@aws-cdk/aws-stepfunctions-tasks"; import { NodejsFunction } from "@aws-cdk/aws-lambda-nodejs"; export class CdkSfnStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const start = new sfn.Pass(this, "Start", { result: sfn.Result.fromObject({ // 次のタスクのインプットとしてにJsonオブジェクト(`{"name": "hinastory"}`)を渡す name: "hinastory", }), }); const helloFunc = new NodejsFunction(this, "hello", { // Lambda関数の作成 entry: "lambda/hello/index.ts", handler: "handler", }); const helloTask = new tasks.LambdaInvoke(this, "helloTask", { // Lambda関数を呼び出すタスクの作成 lambdaFunction: helloFunc, payloadResponseOnly: true, }); const end = new sfn.Pass(this, "End", {}); const definition = start.next(helloTask).next(end); const stateMachine = new sfn.StateMachine(this, "cdk-sfn-state-machine", { stateMachineName: "cdk-sfn-state-machine", definition, }); } }ここまで定義は完成です。このあとは前回と同じように
cdk deplory
をしてステートマシンを実行してみてください。
以下のようにグラフインスペクターのビジュアルの「helloTask」をクリックして、「ステップ出力」のタブで「"hello hnastory"」が出力されていたら成功です。ここまでがStep Functionsの基本となります。あとはStep Functions分岐や繰り返し、並列処理等さまざま部品が用意されているのでそれらを用いて様々なワークフローが定義できます。
応用編
次はちょとした応用編です。S3にテスト用のzipファイルをアップロードして、lambda関数でs3内のディレクトリを探し、さらにそのディレクトリの中にあるzipファイルを並列に処理するサンプルです。ちょっと何行っているかわからないかもしれませんが、ワークフローは以下のようになります。
s3のバケット内に含まれるオブジェクトまたはディレクトリをリストするLambda関数は以下のとおりです。
lambda/list-s3/index.tsimport * as lambda from "aws-lambda"; import * as aws from "aws-sdk"; import { delimiter } from "path"; export async function handler( event: Event, context: lambda.Context, callback: lambda.Callback ) { console.log(event); const s3 = new aws.S3(); const params: aws.S3.ListObjectsV2Request = event.location; const res = await s3.listObjectsV2(params).promise(); console.log(res); return res; } type Event = { location: { Bucket: string; Prefix: string; }; };そしてテストデータをアップロードして実際のワークフローを構築するコードが以下です。ポイントはS3内のオブジェクトをラムダ関数で一覧化し、そのデータでMapを用いて動的な並列処理を実行しているところです。
import * as cdk from "@aws-cdk/core"; import * as sfn from "@aws-cdk/aws-stepfunctions"; import * as tasks from "@aws-cdk/aws-stepfunctions-tasks"; import { NodejsFunction } from "@aws-cdk/aws-lambda-nodejs"; import * as s3 from "@aws-cdk/aws-s3"; import { BlockPublicAccess } from "@aws-cdk/aws-s3"; import * as s3deploy from "@aws-cdk/aws-s3-deployment"; import { RemovalPolicy } from "@aws-cdk/core"; import * as lambda from "@aws-cdk/aws-lambda"; import * as iam from "@aws-cdk/aws-iam"; export class CdkSfnStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); this.createWorkLoad("sfn-s3-test"); } // S3にテスト用のZIPファイルをアップロードする。ローカルのassets/test/配下にはテスト用のZIPファイルをいくつか置いておく // ファイルは指定したバケットのdestinationKeyPrefix配下にアップロードされる // この機能は"@aws-cdk/aws-s3-deployment"を使っているがまだExperimentalなので留意すること private createTestData(bucketName: string) { const bucket = new s3.Bucket(this, bucketName, { bucketName: bucketName, removalPolicy: RemovalPolicy.DESTROY, blockPublicAccess: BlockPublicAccess.BLOCK_ALL, }); new s3deploy.BucketDeployment(this, "deploy1", { sources: [s3deploy.Source.asset("assets/test")], destinationBucket: bucket, retainOnDelete: false, destinationKeyPrefix: "private/aaa/001", }); new s3deploy.BucketDeployment(this, "deploy2", { sources: [s3deploy.Source.asset("assets/test")], destinationBucket: bucket, retainOnDelete: false, destinationKeyPrefix: "private/aaa/002", }); new s3deploy.BucketDeployment(this, "deploy3", { sources: [s3deploy.Source.asset("assets/test")], destinationBucket: bucket, retainOnDelete: false, destinationKeyPrefix: "private/bbb/023", }); } private createWorkLoad(stackPrefix: string) { const bucketName = `${stackPrefix}-sfn-test`; const first = new sfn.Pass(this, "First", { result: sfn.Result.fromObject({ Bucket: bucketName, Prefix: "private/", Delimiter: "/", }), resultPath: "$.location", }); this.createTestData(bucketName); const listObjects = new NodejsFunction(this, "list-s3", { entry: "lambda/list-s3/index.ts", handler: "handler", }); listObjects.addToRolePolicy( new iam.PolicyStatement({ resources: ["*"], actions: ["s3:*"], }) ); const listFirstDirTask = new tasks.LambdaInvoke(this, "listFirstDirTask", { lambdaFunction: listObjects, payloadResponseOnly: true, }); const firstDirMap = new sfn.Map(this, "firstDirMap", { // 動的な並列処理 maxConcurrency: 3, itemsPath: sfn.JsonPath.stringAt("$.CommonPrefixes"), }); const testLambda = lambda.Function.fromFunctionArn( this, "test-func", "arn:aws:lambda:ap-northeast-1:071000381825:function:cats-cats-cats" // 定義済みのLambda関数の呼び出し ); const listPayload = sfn.TaskInput.fromObject({ location: { Bucket: bucketName, Prefix: sfn.JsonPath.stringAt("$.Prefix"), Delimiter: "/", }, }); const testTask = new tasks.LambdaInvoke(this, "testLambda", { lambdaFunction: testLambda, payloadResponseOnly: true, }); const listSecondDirTask = new tasks.LambdaInvoke( this, "listSecondDirTask", { lambdaFunction: listObjects, payload: listPayload, payloadResponseOnly: true, } ); const secondDirMap = new sfn.Map(this, "secondDirMap", { // 動的な並列処理 maxConcurrency: 3, itemsPath: sfn.JsonPath.stringAt("$.CommonPrefixes"), }); const done = new sfn.Pass(this, "Done", {}); const definition = first .next(listFirstDirTask) .next(firstDirMap) .next(done); firstDirMap.iterator( listSecondDirTask.next(secondDirMap.iterator(testTask)) ); const stateMachine = new sfn.StateMachine( this, `${stackPrefix}-state-machine`, { stateMachineName: `${stackPrefix}-state-machine`, definition, } ); } }インフラ構築(CDK)とワークフロー作成(StepFunctins)と関数作成と呼び出し(Lmabda)をアイソモーフィックにする利点
アイソモーフィックとは「同型」という意味です。この記事ではインフラ構築(CDK)とワークフロー作成(StepFunctins)と関数作成と呼び出し(Lmabda)を同じ言語で作成することを指しています。ここではTypeScriptを使いましたが、CDKとLambdaがサポートしていれば同じ言語にしやすいと思います。今回はTypeScriptに統一しましたが、同じ言語だとストレスなく開発ができ開発体験がかなり向上するのでぜひ試してみてください。
まとめ
サーバレスワークフローの紹介とCDKとTypeScriptを用いたワークフローの構築方法を紹介しました。StepFunctionsとLambdaはとても相性が良く、サーバレスアプリケーションを簡単に実行制御できる便利な道具なので、色々な場で活躍できると思い紹介しました。
本記事がサーバレスワークフローに興味がある方の一助になれば幸いです。
おまけ
Rust 2 Advent Calendar 2020 - Qiitaの6日目で、RustとLambdaの相性が良い7つの理由 〜RustでLambdaをやっていく〜という記事も書いています。興味があれば御覧ください。
この観点はあくまで利用者側の視点です。実装的にはワークフロー型の方が複雑になりやすく、ワークフローの可視化も望まれるので色々と難しい面が多いです。 ↩
ワークフロー型を実現サービスとしてAWS SWFもありますが、新規の利用には推奨されていないので本記事では割愛します。 ↩
一応、公式外では次のようなdraw.ioを用いたワークフローのエディタもあるみたいです。 sakazuki/step-functions-draw.io ↩
- 投稿日:2020-12-08T08:43:05+09:00
サーバーレスワークフローをTypeScriptで作成しよう 〜Step FunctionsとCDKによるLambdaの実行順序制御入門〜
この記事はAWS Advent Calendar 2020 - Qiitaの8日目の記事です。
ワークフロー型のアーキテクチャはAWSでよく見られるイベント駆動型のアーキテクチャと補完関係にあるアーキテクチャです。その考え方はシンプルで明示的に実行順序を記述することで処理の流れを表現します。本記事ではサーバーレスの中核を担う Lambda関数のワークフロー型の実行順序制御 を実現する方法、特に インフラ構築、ワークフロー作成、関数作成と呼び出しを全てTypeScriptで完結させる方法 について、その実現方法と利点を記載したいと思います。
イベント駆動とワークフロー
最初にイベント駆動型とワークフロー型のアーキテクチャについて簡単に説明します。すでにご存じの方は飛ばしていただいても構いません。
イベント駆動型のシンプルな構成は以下のとおりです。プロデューサがイベントを生成し、ブローカーがイベントを受け取り、コンシューマにイベントを渡します。
ここで重要なのはブローカはイベントを貯めてもいいし、フィルタして減らしてもいいし、逆に増やしても構いません。またイベントの宛先であるコンシューマを変えたり、複数のコンシューマにイベントを複製して配っても問題ありません。また、ブローカを多段に構成することもできます。要はプロデューサとコンシューマの間にイベントを自由に扱えるブローカをいくつも挟んで良い(挟まなくてももちろんOK)というのがこのアーキテクチャの肝であり、柔軟性やスケーラビリティの根源になっています。
対してワークフロー型は以下のように処理の順番が明白に決まっており処理順序は分かりやすいですが、イベント駆動型ほどの柔軟性はありません。
一般的には処理順序の分かりやすさやデバッグのしやすさを優先するならワークフロー型が適しており、柔軟性やスケーラビリティが必要であればイベント駆動型が適しておりお互いに補完関係にあるアーキテクチャだと言うことができます1。
AWSはアーキテクチャとしてスケーラブルな非同期分散処理に力を入れてきたのでイベント駆動型のアーキテクチャを支えるサービス(SQS, SNS, EventBridge, Kinesis, MQ, MSK等)が充実しています。しかし近年はStep Functionsの登場によりワークフロー型のアーキテクチャも広く使われるようになってきました2。
サーバーレスワークフローとは
サーバーレスは EC2のような仮想サーバを使わずにアプリケーションを開発するアーキテクチャ のことを指しますが、慣習的にはSaaSをLambda(FaaS)で連携させたり補完したりしてアプリケーションを構築するアーキテクチャを指します。サーバーレスワークフローは そのサーバーレスにワークフロー制御のSaaSであるStep Functionsを加えてワークフロー制御を実現したアーキテクチャ になります。
Step Functionsの弱点
Step Functionsの弱点は端的に言うとワークフローのビジュアルエディターが公式にはリリースされていないことです3。ワークフローを作成・編集するにはAmazon ステートメント言語(ASL)というJSONベースの言語で記述する必要があります。だたJSONで記述したワークフローの可視化は行えます。以下はAWS公式のサンプルのHelloWorldのステートマシンをStep Functionsのグラフインスペクターで可視化したものです。
上記のワークフローをASLで記述したものが以下になります。さすがにこれを記述するのは厳しいと感じる方が多でしょう。
{ "Comment": "A Hello World example demonstrating various state types of the Amazon States Language", "StartAt": "Pass", "States": { "Pass": { "Comment": "A Pass state passes its input to its output, without performing work. Pass states are useful when constructing and debugging state machines.", "Type": "Pass", "Next": "Hello World example?" }, "Hello World example?": { "Comment": "A Choice state adds branching logic to a state machine. Choice rules can implement 16 different comparison operators, and can be combined using And, Or, and Not", "Type": "Choice", "Choices": [ { "Variable": "$.IsHelloWorldExample", "BooleanEquals": true, "Next": "Yes" }, { "Variable": "$.IsHelloWorldExample", "BooleanEquals": false, "Next": "No" } ], "Default": "Yes" }, "Yes": { "Type": "Pass", "Next": "Wait 3 sec" }, "No": { "Type": "Fail", "Cause": "Not Hello World" }, "Wait 3 sec": { "Comment": "A Wait state delays the state machine from continuing for a specified time.", "Type": "Wait", "Seconds": 3, "Next": "Parallel State" }, "Parallel State": { "Comment": "A Parallel state can be used to create parallel branches of execution in your state machine.", "Type": "Parallel", "Next": "Hello World", "Branches": [ { "StartAt": "Hello", "States": { "Hello": { "Type": "Pass", "End": true } } }, { "StartAt": "World", "States": { "World": { "Type": "Pass", "End": true } } } ] }, "Hello World": { "Type": "Pass", "End": true } } }従ってASL使わずに楽にワークフローを構築したいのですが、その選択肢の一つが本記事で紹介したいAWS CDKを利用したTypeScriptによるワークフローの構築になります。TypeScriptで記述することにより型の補完が使えたり、CDKがワークフローを抽象化してくれていたりするので生のJSONよりも大分書きやすくなっています。
AWS CDKについて簡単に補足すると、AWS CDKは、使い慣れたプログラミング言語を使用してクラウドアプリケーションリソースを定義するためのソフトウェア開発フレームワークです。CDKはいくつかのプログラミング言語をサポートしていますが、本記事ではTypeScriptを用います。
サーバーレスワークフローをTypeScriptで作成しよう
セットアップ
前提条件としてAWS CLIのセットアップとNode.jsのインストールは済んでいるものとします。
まず、CDKをインストールします。
$ npm install -g aws-cdk次にTypeScriptをインストールします。
$ npm install -g typescriptアプリケーションのディレクトリを作成します。
$ mkdir cdk-sfn $ cd cdk-sfnTypeScriptで初期アプリを作成します。
$ cdk init app --language typescriptこれでセットアップは完了です。
初めてのワークフローの作成から実行まで
ワークフローはcdk-sfn-stack.tsに書いていきます。コード編集にはVisualStudio Code等を用います。
以下が初期のファイルで、とりあずはコンストラクタにガシガシ書いていきます。lib/cdk-sfn-stack.tsimport * as cdk from '@aws-cdk/core'; export class CdkSfnStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // The code that defines your stack goes here } }コードを書き始める前に依存関係のあるライブラリをインストールしておきます。
$ npm install @aws-cdk/aws-stepfunctions次のコードは空の処理を3つ逐次実行するワークフローです。空の処理は
sfn.Pass
で作成し、next
メソッドで繋いでいきます。import * as cdk from "@aws-cdk/core"; import * as sfn from "@aws-cdk/aws-stepfunctions"; export class CdkSfnStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const start = new sfn.Pass(this, "Start", {}); const step1 = new sfn.Pass(this, "Step 1", {}); const end = new sfn.Pass(this, "End", {}); const definition = start.next(step1).next(end); // 処理を順番に繋ぐ const stateMachine = new sfn.StateMachine(this, "cdk-sfn-state-machine", { stateMachineName: "cdk-sfn-state-machine", definition, }); } }
deploy
コマンドでAWS上にStep Functionsをデプロイします。以下ターミナルから実行するとしばらくしてデプロイが完了します。$ cdk deployAWSマネジメントコンソールから「cdk-sfn-state-machiene」ステートマシーンが作成されていることを確認します。
対象をクリックしてステートマシンを開いた後に以下の赤枠で囲った「実行の開始」ボタンを押して実行します。
以下の実行開始ダイアログが出るので
「実行の開始」
を押して実行開始します。実行が完了したら以下の画面が表示されます。「Start」 → 「Step 1」 → 「End」というワークフローが実行されていることがわかると思います。
Step1をLambda関数に変更してみる
次にStep1をLambda関数にしてみたいと思います。Lambda関数は受け取ったJSONに格納された名前に対して挨拶するものとします。
まずは関連モジュールをインストールします。インストールする「aws-lambda-nodejs」はTypeScriptをビルドしてLambda関数を作成してくれる便利なものですが、注意点が2つあってまだ実験的なモジュールであることと、利用にはDockerが必要になることです。
$ npm install @aws-cdk/aws-stepfunctions-tasks @aws-cdk/aws-lambda-nodejs @types/aws-lambdaLambda関数は
labmbda/hello
というディレクトリを作成し、その下にindex.ts
という名前で以下のLambda関数を作成します。lambda/hello/index.tsimport * as lambda from "aws-lambda"; export async function handler( event: Event, context: lambda.Context, callback: lambda.Callback ) { return `hello ${event.name}`; } type Event = { name: string; };そして、ワークフローの中身を以下のように書き換えます。変化している箇所はLambda関数をソースの場所を指定して作成していることと、そのLambda関数を呼び出すStep Functionsのタスクを作成しているところです。注目すべきは
Start
の出力結果としてJSONオブジェクトを生成しているところです。Step Functionsでは前の処理の出力結果を次の処理の入力として利用することができます。import * as cdk from "@aws-cdk/core"; import * as sfn from "@aws-cdk/aws-stepfunctions"; import * as tasks from "@aws-cdk/aws-stepfunctions-tasks"; import { NodejsFunction } from "@aws-cdk/aws-lambda-nodejs"; export class CdkSfnStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const start = new sfn.Pass(this, "Start", { result: sfn.Result.fromObject({ // 次のタスクのインプットとしてにJsonオブジェクト(`{"name": "hinastory"}`)を渡す name: "hinastory", }), }); const helloFunc = new NodejsFunction(this, "hello", { // Lambda関数の作成 entry: "lambda/hello/index.ts", handler: "handler", }); const helloTask = new tasks.LambdaInvoke(this, "helloTask", { // Lambda関数を呼び出すタスクの作成 lambdaFunction: helloFunc, payloadResponseOnly: true, }); const end = new sfn.Pass(this, "End", {}); const definition = start.next(helloTask).next(end); const stateMachine = new sfn.StateMachine(this, "cdk-sfn-state-machine", { stateMachineName: "cdk-sfn-state-machine", definition, }); } }ここまで定義は完成です。このあとは前回と同じように
cdk deplory
をしてステートマシンを実行してみてください。
以下のようにグラフインスペクターのビジュアルの「helloTask」をクリックして、「ステップ出力」のタブで「"hello hnastory"」が出力されていたら成功です。ここまでがStep Functionsの基本となります。あとはStep Functions分岐や繰り返し、並列処理等さまざま部品が用意されているのでそれらを用いて様々なワークフローが定義できます。
応用編
次はちょとした応用編です。S3にテスト用のzipファイルをアップロードして、lambda関数でs3内のディレクトリを探し、さらにそのディレクトリの中にあるzipファイルを並列に処理するサンプルです。ちょっと何行っているかわからないかもしれませんが、ワークフローは以下のようになります。
s3のバケット内に含まれるオブジェクトまたはディレクトリをリストするLambda関数は以下のとおりです。
lambda/list-s3/index.tsimport * as lambda from "aws-lambda"; import * as aws from "aws-sdk"; import { delimiter } from "path"; export async function handler( event: Event, context: lambda.Context, callback: lambda.Callback ) { console.log(event); const s3 = new aws.S3(); const params: aws.S3.ListObjectsV2Request = event.location; const res = await s3.listObjectsV2(params).promise(); console.log(res); return res; } type Event = { location: { Bucket: string; Prefix: string; }; };そしてテストデータをアップロードして実際のワークフローを構築するコードが以下です。ポイントはS3内のオブジェクトをラムダ関数で一覧化し、そのデータでMapを用いて動的な並列処理を実行しているところです。
import * as cdk from "@aws-cdk/core"; import * as sfn from "@aws-cdk/aws-stepfunctions"; import * as tasks from "@aws-cdk/aws-stepfunctions-tasks"; import { NodejsFunction } from "@aws-cdk/aws-lambda-nodejs"; import * as s3 from "@aws-cdk/aws-s3"; import { BlockPublicAccess } from "@aws-cdk/aws-s3"; import * as s3deploy from "@aws-cdk/aws-s3-deployment"; import { RemovalPolicy } from "@aws-cdk/core"; import * as lambda from "@aws-cdk/aws-lambda"; import * as iam from "@aws-cdk/aws-iam"; export class CdkSfnStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); this.createWorkLoad("sfn-s3-test"); } // S3にテスト用のZIPファイルをアップロードする。ローカルのassets/test/配下にはテスト用のZIPファイルをいくつか置いておく // ファイルは指定したバケットのdestinationKeyPrefix配下にアップロードされる // この機能は"@aws-cdk/aws-s3-deployment"を使っているがまだExperimentalなので留意すること private createTestData(bucketName: string) { const bucket = new s3.Bucket(this, bucketName, { bucketName: bucketName, removalPolicy: RemovalPolicy.DESTROY, blockPublicAccess: BlockPublicAccess.BLOCK_ALL, }); new s3deploy.BucketDeployment(this, "deploy1", { sources: [s3deploy.Source.asset("assets/test")], destinationBucket: bucket, retainOnDelete: false, destinationKeyPrefix: "private/aaa/001", }); new s3deploy.BucketDeployment(this, "deploy2", { sources: [s3deploy.Source.asset("assets/test")], destinationBucket: bucket, retainOnDelete: false, destinationKeyPrefix: "private/aaa/002", }); new s3deploy.BucketDeployment(this, "deploy3", { sources: [s3deploy.Source.asset("assets/test")], destinationBucket: bucket, retainOnDelete: false, destinationKeyPrefix: "private/bbb/023", }); } private createWorkLoad(stackPrefix: string) { const bucketName = `${stackPrefix}-sfn-test`; const first = new sfn.Pass(this, "First", { result: sfn.Result.fromObject({ Bucket: bucketName, Prefix: "private/", Delimiter: "/", }), resultPath: "$.location", }); this.createTestData(bucketName); const listObjects = new NodejsFunction(this, "list-s3", { entry: "lambda/list-s3/index.ts", handler: "handler", }); listObjects.addToRolePolicy( new iam.PolicyStatement({ resources: ["*"], actions: ["s3:*"], }) ); const listFirstDirTask = new tasks.LambdaInvoke(this, "listFirstDirTask", { lambdaFunction: listObjects, payloadResponseOnly: true, }); const firstDirMap = new sfn.Map(this, "firstDirMap", { // 動的な並列処理 maxConcurrency: 3, itemsPath: sfn.JsonPath.stringAt("$.CommonPrefixes"), }); const testLambda = lambda.Function.fromFunctionArn( this, "test-func", "arn:aws:lambda:ap-northeast-1:071000381825:function:cats-cats-cats" // 定義済みのLambda関数の呼び出し ); const listPayload = sfn.TaskInput.fromObject({ location: { Bucket: bucketName, Prefix: sfn.JsonPath.stringAt("$.Prefix"), Delimiter: "/", }, }); const testTask = new tasks.LambdaInvoke(this, "testLambda", { lambdaFunction: testLambda, payloadResponseOnly: true, }); const listSecondDirTask = new tasks.LambdaInvoke( this, "listSecondDirTask", { lambdaFunction: listObjects, payload: listPayload, payloadResponseOnly: true, } ); const secondDirMap = new sfn.Map(this, "secondDirMap", { // 動的な並列処理 maxConcurrency: 3, itemsPath: sfn.JsonPath.stringAt("$.CommonPrefixes"), }); const done = new sfn.Pass(this, "Done", {}); const definition = first .next(listFirstDirTask) .next(firstDirMap) .next(done); firstDirMap.iterator( listSecondDirTask.next(secondDirMap.iterator(testTask)) ); const stateMachine = new sfn.StateMachine( this, `${stackPrefix}-state-machine`, { stateMachineName: `${stackPrefix}-state-machine`, definition, } ); } }インフラ構築(CDK)とワークフロー作成(StepFunctins)と関数作成と呼び出し(Lmabda)をアイソモーフィックにする利点
アイソモーフィックとは「同型」という意味です。この記事ではインフラ構築(CDK)とワークフロー作成(StepFunctins)と関数作成と呼び出し(Lmabda)を同じ言語で作成することを指しています。ここではTypeScriptを使いましたが、CDKとLambdaがサポートしていれば同じ言語にしやすいと思います。今回はTypeScriptに統一しましたが、同じ言語だとストレスなく開発ができ開発体験がかなり向上するのでぜひ試してみてください。
まとめ
サーバレスワークフローの紹介とCDKとTypeScriptを用いたワークフローの構築方法を紹介しました。StepFunctionsとLambdaはとても相性が良く、サーバレスアプリケーションを簡単に実行制御できる便利な道具なので、色々な場で活躍できると思い紹介しました。
本記事がサーバレスワークフローに興味がある方の一助になれば幸いです。
おまけ
Rust 2 Advent Calendar 2020 - Qiitaの6日目で、RustとLambdaの相性が良い7つの理由 〜RustでLambdaをやっていく〜という記事も書いています。興味があれば御覧ください。
この観点はあくまで利用者側の視点です。実装的にはワークフロー型の方が複雑になりやすく、ワークフローの可視化も望まれるので色々と難しい面が多いです。 ↩
ワークフロー型を実現サービスとしてAWS SWFもありますが、新規の利用には推奨されていないので本記事では割愛します。 ↩
一応、公式外では次のようなdraw.ioを用いたワークフローのエディタもあるみたいです。 sakazuki/step-functions-draw.io ↩
- 投稿日:2020-12-08T08:29:26+09:00
サーバCPUもArmの時代になりました
Supershipグループ Advent Calendar 2020 - Qiitaの12/8の記事です。
この記事は、2020年10月22日に行われたオンラインイベント Compute x AWS Graviton2 「Armプロセッサによるコスト最適化」の登壇内容を元に加筆・抜粋したものです。
ざっくりまとめ
- AWS EC2で64bit Arm CPUのインスタンスを使用できるようになりました
- 検証した結果、安くて概ね速くて非常にコスパに優れています
- 一部不向きなタスクもあります
- 積極的に使っていきます
AWS Gravitonプロセッサ
AWSが作ったArmアーキテクチャのCPUです。
サーバのCPUといえばIntelかAMDが作るx86_64 CPUという状態が10年以上続いてきました。AWSもずっとIntel Xeonを採用し続けてきましたが、2015年にAnnapurna Labsを買収してから独自にCPUの開発を進め、2018年11月にArmベースのGravitonプロセッサを使用したA1インスタンスを発表、2019年12月には性能を向上させたGraviton2プロセッサを発表しました。現在では一般用途のm6g、コンピュート最適化のc6g、メモリ最適化のr6g、バースト可能インスタンスのt4gと、よく使う一通りのインスタンスタイプでGraviton2プロセッサを利用できるようになっています。
価格
Gravtionプロセッサの利点は、なんといってもCPUあたりの価格が安いことです。詳細な価格は公式の価格表に譲るとして、メモリ量 = vCPU数 * 2 GB のインスンタンスの2020-12-07時点のオンデマンド価格をAsia Pacific(Tokyo)で比較してみましょう。
インスタンスタイプ CPU メモリ(GB) vCPU数 価格($/h) c5.large Intel Xeon (Skylake) 4 2 0.107 a1.large Graviton 4 2 0.0642 c6g.large Graviton2 4 2 0.0856 vCPUあたりの価格はGraviton, Graviton2のほうが安価です。しかも、c5の2vCPUは物理1core 2threadなのに対しa1とc6gは物理2coreとなっており、物理CPU単価では半額以下です。
性能
a1インスタンスは性能が低いという記事を見たため検証しなかったのですが、Graviton2はAWSが自信ありそうな推し方をしていたので、業務を想定して様々な処理の性能を計測してみました。色々計測した結論としては、
- 全CPUコアを使い切って処理する場面ではGraviton2インスタンスのほうがスループットが高い
- シングルスレッド性能は既存のIntel CPUインスタンスのほうが高い
です。詳細はこちらのスライドをご覧ください。ここでは特徴的な結果だけ抜粋して掲載します。
Go1.14.4で実装されたバッチをc6g、c5、c5aの12xlarge(48vCPU)インスタンスで実行し、並行処理のスレッド(goroutine)数を1から48まで変化させてデータ処理量(higher is better)を計測した結果がこちらです。全てのCPUを使い切る設定の時、最も安価なc6gが最も高いスループットを示しました。スレッド数を増やしていくと、c5とc5aはスレッド数がvCPU数の半分=物理core数に達するあたりから処理量が伸びづらくなりますが、vCPU=物理coreのc6gはスレッド数がvCPU数に達するまで処理量が伸び続けます。
上記のような12xlargeという大きなインスタンスだけでなく、largeのような比較的小さなインスタンスでも同様の結果となります。m6g、m5、c5aのlarge(2vCPU)インスタンスにJSONを返却するWebサーバとWebアプリケーションを構築し、捌ける限界リクエスト数(higher is better)を計測したところ、物理2coreを生かしてm6gが最も良い結果を示しました。
一方、シングルスレッドでいくつかの処理の実行時間(lower is better)を計測した時は、Intel CPUのインスタンスが良好な値を示しました。m系インスタンスどうしで比較すると差が小さいのですが、c系インスタンスで比較するとc5の速さが目立ちます。
Graviton2の良さが光るのは、「CPU性能が求められず、コスト削減を優先したい」「全CPUコアを使い切った時のスループットが重要」どちらかの場面だと言えるでしょう。シングルスレッド性能が重要な場面には不向きです。使用感
これまでサーバサイドコンピューティングの世界はx86_64を前提として回ってきたため、arm64では実行できないソフトウェアが続出するのでは…と懸念していました。実際に使ってみると、Linuxのディストリビューションが同じであればx86_64でもarm64でも同じ手順で構築でき、同じように使えて拍子抜けすることが多かったです。機械学習を除けば。
Graviton2上でPythonを使い機械学習をやってみた時は、ライブラリのインストールが失敗したりコンパイルに時間がかかったりと、環境構築面で苦労が多い印象でした。また、GPUインスタンスは用意されていません。Graviton2登場当初と比べるとライブラリの対応は進んできており、将来的には問題なく使えるようになることを期待しています。
なお、x86_64で構築済みのサーバのインスタンスタイプをarm64に変更することはできません。既にx86_64で作成済みのサーバをarm64に切り替えるには、サーバをarm64で一から作り直す必要があります。Dockerを使っている場合は、arm64向けにDocker Imageを作り直す必要があります。検証の際はサーバを新規作成して比較したので問題ありませんでしたが、実業務ではこの点がハードルになる場面があるかもしれません。
おわりに
検証結果を受けて、新しくサーバを構築する際はGraviton2を選ぶことが多くなりました。既存のサーバも積極的にGraviton2に置き換えてコストパフォーマンスを向上させるべく、準備を進めています。
Graviton2の登場はもちろん、macがArmベースのM1チップを採用したりArmを採用した富岳がスパコンランキングで1位になったりと、2020年はArm躍進の年でした。モバイルと組込機器はArm、PCとサーバはx86_64という棲み分けが一気に崩れた感があります。このまま世界のCPUがArmに支配されていくのか、IntelとAMDが奮起して巻き返すのか、競争による製品の改善に期待を持って見守っていきます。