私の戦闘力は53万です

awsとgcpについて書きます

SendgridのEvent WebhookログをAWSに連携する


sendgridを利用しており送信履歴をログに残したい要件がありました。

対応する機能としてSendgridではWebhook機能があり、
メールを送信や開封等のイベントを
指定のエンドポイントに送信できます。

sendgrid.kke.co.jp

サンプルの構成

実際の構成はどのようになるかと言うと、
公式のブログにサンプル構成があり、
こちらがとても参考になりました。
ただ、何点か構成を変更したい点がありました。

sendgrid.kke.co.jp

構築したい構成

上記サンプルとの違いは下記です

ログはS3に格納する

数量が膨大になりそうでしたので、より料金単価が安いS3にしました。

Lambdaを利用しない(apigatewayとs3を直接連携)

背景として、今回のケースではメールのイベントログを格納したいだけであり、
何か加工等の特別な処理をさせたい訳でなかったため、
Lambdaを利用する必要がありませんでした。
Lambda経由でS3に格納しても要件実現はできてしまうのですが、
特に、Lambdaのスケーラビリティを心配したくないことや、 ソース管理の必要があるといったデメリットを抱えてしまうため
利用しない形としました。

CDKで書きたい

サンプルで書いてみました。
apigateway周りでもう少し細かい設定を付与したいですが、
最低限のものとお考えください。

import {
  Stack,
  StackProps,
  RemovalPolicy,
  aws_apigateway as apigateway,
  aws_s3 as s3,
  aws_iam as iam,
  aws_certificatemanager as acm,
  aws_route53 as route53,
  aws_cognito as cognito,
  aws_route53_targets as targets,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';

const scopeName = 'activity';

type Props = {
  hostedZoneId: string;
  domainName: string;
  domainPrefix: string;
  bucketName: string;
} & StackProps;

export class SendgridWebhookS3Stack extends Stack {
  constructor(scope: Construct, id: string, props: Props) {
    super(scope, id, props);

    const { hostedZoneId, domainName, domainPrefix, bucketName } = props;

    const bucket = new s3.Bucket(this, 'Bucket', {
      bucketName: bucketName,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    const fullAccessScope = new cognito.ResourceServerScope({ scopeName: '*', scopeDescription: 'Full access' });
    const pool = new cognito.UserPool(this, 'Pool', {
      removalPolicy: RemovalPolicy.DESTROY,
    });
    const resourceServer = pool.addResourceServer('ResourceServer', {
      identifier: scopeName,
      scopes: [fullAccessScope],
    });

    pool.addDomain('CognitoDomain', {
      cognitoDomain: {
        domainPrefix: domainPrefix,
      },
    });

    pool.addClient('appClient', {
      generateSecret: true,
      oAuth: {
        flows: {
          clientCredentials: true,
        },
        scopes: [cognito.OAuthScope.resourceServer(resourceServer, fullAccessScope)],
      },
    });

    const auth = new apigateway.CognitoUserPoolsAuthorizer(this, 'Authorizer', {
      cognitoUserPools: [pool],
    });

    const restApiRole = new iam.Role(this, 'Role', {
      assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
      path: '/',
    });
    bucket.grantReadWrite(restApiRole);

    const hostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'HostedZone', {
      hostedZoneId: hostedZoneId,
      zoneName: domainName,
    });

    const certificate = new acm.Certificate(this, 'Certificate', {
      domainName: `*.${domainName}`,
      validation: acm.CertificateValidation.fromDns(hostedZone),
    });

    const api = new apigateway.RestApi(this, 'RestAPI', {
      domainName: {
        domainName: `${domainPrefix}.${domainName}`,
        certificate: certificate,
      },
      // disableExecuteApiEndpoint: true,
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: ['POST', 'OPTIONS'],
        statusCode: 200,
      },
      endpointConfiguration: {
        types: [apigateway.EndpointType.REGIONAL],
      },
    });
    const items = api.root.addResource('logs');
    const prefix = items.addResource('{prefix}');

    // オブジェクトをアップロードするための PUT メソッドを作成する
    prefix.addMethod(
      'POST',
      new apigateway.AwsIntegration({
        service: 's3',
        integrationHttpMethod: 'PUT',
        // アップロード先を指定する
        path: `${bucket.bucketName}/{prefix}/{fileName}`,
        options: {
          credentialsRole: restApiRole,
          passthroughBehavior: apigateway.PassthroughBehavior.WHEN_NO_MATCH,
          requestParameters: {
            // メソッドリクエストのパスパラメータを統合リクエストのパスパラメータにマッピングする
            'integration.request.path.prefix': 'method.request.path.prefix',
            'integration.request.path.fileName': 'context.requestId',
          },
          integrationResponses: [
            {
              statusCode: '200',
              responseParameters: {
                'method.response.header.Timestamp': 'integration.response.header.Date',
                'method.response.header.Content-Length': 'integration.response.header.Content-Length',
                'method.response.header.Content-Type': 'integration.response.header.Content-Type',
                'method.response.header.Access-Control-Allow-Headers': "'Content-Type,Authorization'",
                'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,POST,PUT'",
                'method.response.header.Access-Control-Allow-Origin': "'*'",
              },
            },
            {
              statusCode: '400',
              selectionPattern: '4\\d{2}',
              responseParameters: {
                'method.response.header.Access-Control-Allow-Headers': "'Content-Type,Authorization'",
                'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,POST,PUT'",
                'method.response.header.Access-Control-Allow-Origin': "'*'",
              },
            },
            {
              statusCode: '500',
              selectionPattern: '5\\d{2}',
              responseParameters: {
                'method.response.header.Access-Control-Allow-Headers': "'Content-Type,Authorization'",
                'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,POST,PUT'",
                'method.response.header.Access-Control-Allow-Origin': "'*'",
              },
            },
          ],
        },
      }),
      {
        authorizer: auth,
        authorizationScopes: [`${scopeName}/*`],
        requestParameters: {
          'method.request.path.prefix': true,
          'method.request.path.fileName': true,
        },
        methodResponses: [
          {
            statusCode: '200',
            responseParameters: {
              'method.response.header.Timestamp': true,
              'method.response.header.Content-Length': true,
              'method.response.header.Content-Type': true,
              'method.response.header.Access-Control-Allow-Headers': true,
              'method.response.header.Access-Control-Allow-Methods': true,
              'method.response.header.Access-Control-Allow-Origin': true,
            },
          },
          {
            statusCode: '400',
            responseParameters: {
              'method.response.header.Access-Control-Allow-Headers': true,
              'method.response.header.Access-Control-Allow-Methods': true,
              'method.response.header.Access-Control-Allow-Origin': true,
            },
          },
          {
            statusCode: '500',
            responseParameters: {
              'method.response.header.Access-Control-Allow-Headers': true,
              'method.response.header.Access-Control-Allow-Methods': true,
              'method.response.header.Access-Control-Allow-Origin': true,
            },
          },
        ],
      }
    );
    new route53.ARecord(this, 'ARecod', {
      zone: hostedZone,
      recordName: `${domainPrefix}.${hostedZone.zoneName}`,
      target: route53.RecordTarget.fromAlias(new targets.ApiGateway(api)),
    });
  }
}

結果確認

作成されたcognitoからclientIdとsecretを取得し、
下記の要領でリクエスト実行します。
<>で囲んだ部分はcdkで指定した値に読み替えてください。

DOMAIN=https://<domainPrefix>.auth.ap-northeast-1.amazoncognito.com
APP_CLIENT_ID=xxxxxx
APP_CLIENT_SECRET=xxxxx
curl -s -X POST ${DOMAIN}/oauth2/token \
-H "Authorization: Basic $(echo -n ${APP_CLIENT_ID}:${APP_CLIENT_SECRET} | base64 )" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=${APP_CLIENT_ID}"


curl -X POST 'https://<domainPrefix>.<domainName>/logs/<prefix>/' \
--header "Authorization: Bearer xxxxxxxxxxx" \
--data '{"message":"Hello"}'

S3にログが格納されていることを確認できました。
S3内ではprefixで指定した文字列の配下にログが格納されます。
下記はprefixにsampleを指定した場合の例です。

おまけで、SendgridではSubuserという機能があり、
論理単位でログの出力を複数設定することができます。
そのため、特定用途のメールのみprefixを分けて
ログ出力するといったことも可能で下。 sendgrid.kke.co.jp

AWS configとsecurityhubの連携機能を試してみた

先月にsecurityhubとconfigの新しい連携機能が発表されており、
挙動が気になっていたので試してみました。

aws.amazon.com

何が変わったのか

config ruleで検知した違反をsecurityhubに自動連携してくれるようになりました。

これまでは、configで検知した違反をsecurityhubに連携する場合
下記のような形で、連携の仕組みを自作する必要がありました。
これからは自動的にconfigの結果をsecurityhubに取り込んでくれます。

aws.amazon.com

動作を確認してみる

configで検知した内容がSecurityhubにインポートされるときに、
どのように取り込まれるのか、各パターンを試してみました。

No 前提条件
workflow status
前提条件
compliance status
configチェック時の
リソース状態
検証結果
1 new PASSED PASSED 何も起きない
2 new PASSED FAILED compliance statsuがFAILEDになる
3 new FAILED PASSED compliance statsuがPASSEDになる
4 new FAILED FAILED 何も起きない
5 suppress PASSED PASSED 何も起きない
6 suppress PASSED FAILED compliance statsuがFAILEDになる
7 suppress FAILED PASSED compliance statsuがPASSEDになる
8 suppress FAILED FAILED 何も起きない
9 notified PASSED PASSED 何も起きない
10 notified PASSED FAILED compliance statsuがFAILEDになる
Workflow statusがNewになる
11 notified FAILED PASSED compliance statsuがPASSEDになる
12 notified FAILED FAILED 何も起きない
13 resoleved PASSED PASSED 何も起きない
14 resoleved PASSED FAILED compliance statusはFAILEDになる
Workflow statusがNewになる
15 resoleved FAILED PASSED compliance statsuがPASSEDになる
16 resoleved FAILED FAILED 何も起きない
17 なし なし FAILED Newとして新しいfindignsが登録される

表の見方補足

上記の表の見方が分かりにくいかもと思いましたので補足します。
例として、下記のsecurityhub findignsがあったとします。

sample
それぞれのステータスは下記です。
workflow status:New
compliance status:PASSED
この例は表のNo1,2の前提条件が該当します。

上記前提条件をもとに、configがリソースを検査し、
その結果がPASSEDの場合はNo1、
FAIEDの場合はNo2という見方となります

検証してみて

検証して見て、securityhub standardは
config ruleに置き換えて良いのではと思いました。
docs.aws.amazon.com

securityhub standardは便利なのですが
個人的には下記が不便な点でした。

securityhubのimportイベントの発生頻度が多い

securityhub standardでは検知内容が放置された(上記No4)場合に
importイベントが定期的に発生します。
これは良し悪しあるのですが、
イベントを通知条件によっては何度も通知が来ます。
configの場合は、ルールの評価結果に変動がない場合には
securityhubに連携が行われない(=importイベントが発生しない)形となります。
個人差あると思うのですが、私にとっては、
変更がある = イベント通知されるという流れが直感的でしたので
configの方がより馴染みました。

securityhub standardに新規追加がある場合、ルールが有効化された状態で追加される

securityhub standardは定期的に更新があり、
ルールが追加されることがあります。

aws.amazon.com このような形で、ある日ルールが有効化状態で追加されます。
そのため、追加されたルールが不要な場合は、 いちいち無効化作業が必要でした。 configの場合は、ルールを追加しない限り検知項目は増えないため、 ルール追加のタイミングを自分達で選択できます。

これから

上記の形でsecurityhub standardを利用せず、
configだけで運用する形が取れそうでしたらやってみて、
ブログネタ的なものがあれば継続して書いて見たいと思います。
Organizationやsecurityhub、config周り詳しい方、
より良い方法や発見がありましたらコメントで教えて頂けると嬉しいです。

serverlessテスト手法(エミュレータなし)について書いてみる

このブログはAWS LambdaとServerless Advent Calendar 2021の16日目です。

qiita.com

サーバレスのテスト手法は様々なパターンがあり、
私も勉強会で情報収集するようにしていました。
ただ、結局のところ何が良いのかよく悩んでいたところではありました。

そんな折、AWSAtsushi Fukui (@afukui) | Twitterさんが
以前勉強会で、ローカルでの検証方法を語られていたり、
(該当部分に時間合わせてますが、この勉強会面白かったので、ぜひ全体見てください)

youtu.be

また、本アドベントカレンダーの5日目の
kensh (@_kensh) | Twitterさんのyoutube動画での
エミュレーションなしのテスト方法が
割と普段自分が実施している方法に近いのではと思ったことから
私の理解まとめと、具体的なコードを公開してみようと思いました。

youtu.be

本カレンダーではサーバレスやlambdaに詳しい方が
たくさんおられると思うので私の理解が間違っていたり、
もっとこうした方が良いといった
フィードバックやツッコミが頂けるかもという期待もあります。
ぜひお待ちしています!

サンプル

本記事での説明をより分かりやすくするため
サンプルでSAMのpython3.7のソースを公開しました。 github.com

dockerfile等はvscoderemote container
動作確認した時のものですので、
remote containerをお使いの方は是非ご利用ください。

コードを説明

lambdaのコードから見ます。
lambda起動時に実行されるlambda_handlerとは別の関数を書きます。
サンプルコードではfunction_sampleが該当します。
ここでは単純に2つの引数を足し算するだけです。

lambdaのコード

app.py

import os
.......(略)

def function_sample(num1: int, num2: int) -> int:
    res = num1 + num2
    return res


def lambda_handler(event, context):

    print(function_sample(3,4))

    return {
        "statusCode": 200
    }

テストコード

テストコードでは、上記関数(function_sample)を呼び出します。

tests/unit/test_hello.py

from functions.hello_world import app as app_hello

.......(略)

def test_function_sample():
    assert app_hello.function_sample(2, 3) == 5

テスト実行

deployはせず、ローカルでpytestを実行する形になります。

pytest  -k test_function_sample

こちらが、kensh (@_kensh) | Twitterさんの下記スライドの
エミュレータなしの方法に近いのかと思います。 f:id:remmemento:20211216214853p:plain

samやcdkの場合、buildやdeployをすると時間がかかってしまいますが
ローカルでソース変更してpytest実行するだけであれば
すぐに実行できます。

こうしてfunction_sampleが完成したら、
lambda_handlerからfunction_sampleを呼び出します。
サンプルのこの部分です

メインのロジック部分はこの方法で書いて
おおよそのバグを潰しておいて、
単純なコードだけではテストが難しい部分
(他サービスとの連携テストや権限周りのテストなど)は
deployして実際の環境で実施する形となります。

補足と考え方

ソース修正からテスト実行までの時間が短いことの価値は
Takuto Wada (@t_wada) | Twitterさんの
講演が分かりやすいと思います。
youtu.be

また、テスタビリティ向上のため、
外部との接続部分と、処理のロジックを切り分けるような話は
下記が分かりやすいと思いました。
こちらもサンプルコードを公開されています。 qiita.com

実は、この記事を見て、その分かりやすさに感動し、
自分も何か同じカレンダーで記事書いてみたい思い、
本記事を書いてみようと思いました。

あと理想と現実のバランスの取り方という点で、
以前クラメソさんで開催されていた勉強会がありがたかったです。 記事こちらです。 logmi.jp

また、本カレンダー6日目でhotswap的な手法も紹介されており、
最近使えるようになった手法で、こちらも開発段階では重宝しそうと思いました。
qiita.com

さいごに

もっと良い方法があれば是非知りたいのでコメント頂けると嬉しいです。
またカレンダー後半も楽しみにしています!
ありがとうございました。

DevOps Guru for RDS を触ってみた

こちらJapan APN Ambassador Advent Calendar 2021の11日目の記事です。

qiita.com

f:id:remmemento:20211211231551p:plain

今年もre:Inventでたくさん発表がありましたね。
発表の中で気になった DevOps Guru for RDS を触ってみたので
ブログ書きたいと思います。

準備

こちらサポートされているのが現時点で
AuroraのみのようなのでAuroraを立ち上げます。 docs.aws.amazon.com

立ち上がってしばらくしてからAuroraに負荷をかけてみます。
※DevOps Guru for RDSは機械学習による異常検知なので、
作成後すぐに負荷をかけても異常検知されない可能性があります。
私も適当に負荷をかけていたのですが、 すぐには検知されず、
下記で異常を検知させることができました。

host="xxxxxxxxxxxxxx.rds.amazonaws.com"
mysqlslap --delimiter=";" -u admin -p -h ${host} -P 3306 mydb --auto-generate-sql --concurrency=200 --number-of-queries=10000 --auto-generate-sql-write-number=2000 --number-int-cols=2 --number-char-cols=3

検知結果を見てみる

上記方法でAuroraに負荷かけ、異常が検出されたので見てみました。
insightsの一覧に表示されます。
また、この検知はeventBridgeやSNSと連携が可能です。
f:id:remmemento:20211211232137p:plain

今回はwriteのlatencyが異常だと検知されました。
詳細を開くと、重要度や開始時間が書かています。
インシデント対応の時には、報告の際に、この種の情報が必要になるので
まとめてくれているのは嬉しいですね。 f:id:remmemento:20211211232156p:plain

関連するメトリクスも提案表示してくれる

グラフを確認すると、関連するメトリクスもまとめてくれていました。
今回はwriteLatencyで検知されていたのですが、
同タイミングでコネクション数も増やしていたため、
これも怪しいのでは?ということで並べて表示してくれているようでした。

f:id:remmemento:20211211233804p:plain

AWS関連の操作もまとめてくれる

また、Relevant eventsとして、
直近で実施したAWS関連の操作もピックアップしてくれていました。
例えば私は、この検証をする前に、
別の検証でcloudformation stackを削除していたのですが、
それも関連しそうなイベントとして一覧表示してくれていました。
f:id:remmemento:20211211234435p:plain

Performance insightも併用

また、こちらは以前からあった機能ですが、 RDSのPerformance insightと併せて、 原因となるqueryの候補を確認できます。 f:id:remmemento:20211211234657p:plain

試してみて

DevOps Guru for RDSを使ってみた感覚としては
RDSのメトリクスにanomaly detectionがかけられているような感覚でした。
また、パフォーマンス面で何かあった時に
簡単に見たいものが揃っていて、率直に嬉しいサービスと思いました。

私は前職でOracleで性能問題の対応をしたことがあったのですが、
その時こんなツールがあったら良かったなと昔を思い出してしまいました。
DBの理解には@odakeiji1さんの書籍に大変お世話になりました。
特にお気に入りはこの本です。
www.amazon.co.jp

今はAWSに所属されているようで、
これからも大いに学ばせて頂きます。 twitter.com

Ambassadorとして楽しく記事を書かせて頂きました。
ありがとうございました!

amplify studioを触ってみた

f:id:remmemento:20211204192448p:plain

re:invent 2021で発表のあったamplify studioを触ってみました。

概要としては、figmaというサービスと連携し
GUIベースでコンポーネント
作成できるようになったようです。
今回は試しにカードのcomponetを作ってみました
f:id:remmemento:20211204205503p:plain

figmaでアカウント作成

まずはfigmaでアカウント作成し UI figma kitを利用可能なよう設定します。 www.figma.com

f:id:remmemento:20211204205919p:plain

figmaで適当なコンポーネントを作成

figmaで適当なコンポーネントを作成してみます。
figma上でコンポーネント化しておくことで
amplify側にコンポーネントとして連携できるようになるようなので
自分が作成したいコンポーネントが出来上がったら
create componetしておくと良いと思います。 f:id:remmemento:20211204205602p:plain

figmaとamplifyを連携

コンポーネントを作成したら、
それをamplify側に持ってきます。 awsコンソールから、
amplify studio ( 旧名 amplify admin UI)を有効化し
amplify studioにログインしておきます

f:id:remmemento:20211204211203p:plain

UI Libraryが追加されていますので選択します。 f:id:remmemento:20211204210711p:plain そしてfigmaと連携するために、 figmaでURLを取得し、Amplify側に入力します。 f:id:remmemento:20211204210439p:plain

初期でサンプルで作成されているコンポーネントも含めて
連携するかどうかを聞かれますので、
必要に応じてRejectかAcceptを選択します f:id:remmemento:20211204211427p:plain

あとはamplifyでamplify pullすれば
上記で作成したコンポーネントのコードが
自動作成されダウンロードされます。

実際に作成されたコード

内部が気になったのでコードを見てみました。
@aws-amplify/ui-react自体がText等のコンポーネントを持っており、
propsで、位置等の情報を指定可能なよう作成されているようでした。

/***************************************************************************
 * The contents of this file were generated with Amplify FrontendManager.           *
 * Please refrain from making any modifications to this file.              *
 * Any changes to this file will be overwritten when running amplify pull. *
 **************************************************************************/

/* eslint-disable */
import React from "react";
import { getOverrideProps } from "@aws-amplify/ui-react/internal";
import { Icon, Text, View } from "@aws-amplify/ui-react";
export default function Component1(props) {
  const { Title, text, overrides: overridesProp, ...rest } = props;
  const overrides = { ...overridesProp };
  return (
    <View
      width="449px"
      padding="0px 0px 0px 0px"
      position="relative"
      height="264px"
      {...rest}
      {...getOverrideProps(overrides, "View")}
    >
      <View
        padding="0px 0px 0px 0px"
        backgroundColor="rgba(245.00000059604645,245.00000059604645,245.00000059604645,1)"
        top="0px"
        left="0px"
        width="449px"
        position="absolute"
        height="264px"
        {...getOverrideProps(overrides, "View.View[0]")}
      ></View>
      <View
        padding="0px 0px 0px 0px"
        backgroundColor="rgba(25.999998450279236,188.00003439188004,254.00000005960464,0.2199999988079071)"
        top="0px"
        left="0px"
        width="449px"
        position="absolute"
        height="63px"
        {...getOverrideProps(overrides, "View.View[1]")}
      ></View>
      <Icon
        pathData="M96 19C96 29.4934 74.5097 38 48 38C21.4903 38 0 29.4934 0 19C0 8.50659 21.4903 0 48 0C74.5097 0 96 8.50659 96 19Z"
        viewBox={{ minX: 0, minY: 0, width: 96, height: 38 }}
        color="rgba(174.00000482797623,179.000004529953,183.00000429153442,1)"
        top="12px"
        left="339px"
        width="96px"
        position="absolute"
        height="38px"
        {...getOverrideProps(overrides, "View.Icon[0]")}
      ></Icon>
      <Text
        padding="0px 0px 0px 0px"
        color="rgba(0,0,0,1)"
        textAlign="left"
        display="flex"
        justifyContent="flex-start"
        fontFamily="Roboto"
        top="15px"
        left="27px"
        fontSize="30px"
        lineHeight="35.15625px"
        position="absolute"
        fontWeight="400"
        direction="column"
        children="Title"
        {...getOverrideProps(overrides, "View.Text[0]")}
      ></Text>
      <Text
        padding="0px 0px 0px 0px"
        color="rgba(0,0,0,1)"
        textAlign="left"
        display="flex"
        justifyContent="flex-start"
        fontFamily="Roboto"
        top="89px"
        left="47px"
        fontSize="30px"
        lineHeight="35.15625px"
        position="absolute"
        fontWeight="400"
        direction="column"
        children="text...."
        {...getOverrideProps(overrides, "View.Text[1]")}
      ></Text>
    </View>
  );
}

こちらのコードは上書きしたとしても、
再度amplify pullをすると上書きされてしまうようです。
ドキュメントをみると、上書きされたくないなら、
適当にrenameして管理すればできると書かれていました。
https://docs.amplify.aws/console/uibuilder/override/

Amplify studioでコンポーネントを操作してみる

Amplify studioを利用してPropsを
指定できるようなので試してみました。
例えば、子要素のtextで、labelを与えることで
テキスト表示を変えることができました。 f:id:remmemento:20211204211827p:plain

また、条件によってpropsを変えることもできるようです。
例えば親コンポーネントに与えたプロパティtypeの値がnotificationの場合は backgroundColorを青にする f:id:remmemento:20211204212018p:plain

それ以外(alert)だったら赤にする f:id:remmemento:20211204212041p:plain

といった設定も可能なようでした。

やってみて

正直なところ私はfigmaが初めてで、若干GUI部分の操作に戸惑いました。
ただ、figmaに慣れれば、割と感覚的に
作業できるようになるのかと思いました。
またamplifyはreinvent前にもupdateが多くあったり
amplify studioにも、他にも機能があるようなので、
引き続き使ってみて記事書ければと思います。

Amazon CodeGuruハンズオン(JAWS-UG千葉)に参加しました

f:id:remmemento:20211112004547p:plain Amazon CodeGuruハンズオン(JAWS-UG千葉)に参加しました。
千葉と言いつつも、オンライン開催をされていて全国から参加し放題です。
今回はCodeGuruのハンズオンがあり、
なんとAWS所属の方々もスピーカー参加されておりました。
(しかもBlackbeltと同じkanasugiさん!)
またハンズオンのシナリオが公式のworkshopかと思うほど
きれいで学びになったのでブログでシェアできればと思い書いてみます。

jawsug-chiba.connpass.com

参考資料

ハンズオンの資料公開OKをもらったので
資料のリンクを貼っておきます。 この資料があればハンズオンできますので興味ある人はぜひ。 drive.google.com ハンズオン内で、コピペしたい箇所のテキストはこちら drive.google.com

資料前半の説明部分はBlackbeltに似た説明があるので
併せて聴くと良いと思います。 www.youtube.com

CodeGuru機能概要

今回のハンズオンではCodeGuruの下記2つの機能を使いました。

CodeGuru Reviewer

f:id:remmemento:20211111232449p:plain こちらはソースのレビューを実施し、改善箇所を指摘してくれる機能です。
今回のシナリオではCodeCommitでPRをして、
その際にCodeGuruがレビューをしてくれる流れになっています。
また、今回のハンズオンでは実施していませんが、
既存のGithub等のリポジトリをレビューをかけるようなこともできます。

CodeGuru Profiler

f:id:remmemento:20211111232617p:plain こちらは実稼働しているアプリケーションの実績をもとに、
どのコード部分がリソース消費に影響を与えているかを解析する機能です。
本ハンズオンでは、EC2上でAgentが動作し、 EC2の稼働状況のデータがCodeGuru Profilerに送られ、 その結果を見るシナリオがあります。

ハンズオンの概要

ハンズオンは2部構成で、Part1はCICDのパイプラインを作成します。
cloud9からcode系サービスを利用し、EC2にdeployできる環境を作ります。
こちらは前準備のようなもので、cfnを使って展開します。 f:id:remmemento:20211111230705p:plain

Part2で主役のCodeGuruを利用します。
初期のサンプルソースではCodeGuruから指摘が出るのですが、
その指摘部分を改善修正することで、
より指摘が少なくなる、といった具合で
ハンズオンのシナリオが組まれています。 f:id:remmemento:20211111230733p:plain

CodeGuru Reviewerを使ってみて

ハンズオン用のサンプルソースでは予め指摘が出るようになっていました。
例えば、AWSのベストプラクティスに反する箇所の指摘
(アクセスキー使わないでRole使って)だったり、
f:id:remmemento:20211111233815p:plain

一般的なJavaの指摘をしてくれます。
f:id:remmemento:20211111233843p:plain

指摘内容は理解しやすいよう
ほぼ全てリンクが付与されていました。
AWSの使い方に関する指摘は、AWSの公式ページのリンクが付与されています。

CodeGuru Profilerを使ってみて

Profilerは動作すると、下記のようなステータスが見れます。

f:id:remmemento:20211111234338p:plain リソースの利用状況を可視化でき、
例えば上記でVisualize CPUを押すと下記のような画面に遷移します。

f:id:remmemento:20211111234648p:plain 情報てんこ盛りですが、
libraryのコード(緑色)と自分のコード(青色)をひとめで区別できたり、
推奨事項を確認できたりと
実際の稼働状況から得られる情報が良い感じで集約されています。
便利ですね。

PythonでもCodeGuru Reviewerを使ってみた

ハンズオンは上記のような流れですが、
pythonでもやってみたかったので
ハンズオン終わった後に、CodeGuru Reviewerで
自分のgithub上のpythonのソースをレビューしてみました。

温度感を表すために、下記1個だけ指摘のケースを書きます。 f:id:remmemento:20211112000546p:plain learn moreのリンク先はpythonの公式ドキュメントで下記でした。 docs.python.org 指摘内容としてはget()の第二引数にNoneをわざわざ指定する必要ないよ、といった指摘でした。 修正してPRすると、指摘が解消されていることが確認できました。 このように一般的なベストプラクティスのような部分を指摘してくれるので良いですね。
これに慣れると、人間は業務ロジックのような
本来の部分に集中できるのかなと思います。

修正前

 eventArn = event.get('arn', None)

修正後

 eventArn = event.get('arn')

さいごに

今回ハンズオン実施頂いた主催者の方々、ありがとうございました!

JAWS-UG千葉支部のハンズオン、
いつも細部まで綺麗で、よく勉強させてもらっています、ありがとうございます。
今後も参加させてもらいます!

AWSの基礎を学ぼう Gateway Load Balancerに参加しました

AWSの基礎を学ぼう Gateway Load Balancerに参加しました
AWSの亀田さんが、ほぼ毎週開催されている
いろんなサービスを触ってみる勉強会です。
今週はGateway Load Balancer でした。
awsbasics.connpass.com

作成した環境

こんな感じの構成をハンズオンで構築できました。

f:id:remmemento:20210904152635p:plain
全体図

VPCが4つ存在し若干細々しているので、コメントを付けてみます f:id:remmemento:20210904154153p:plain

  • 左側のApplicationのVPCから通信がスタートするとします
  • Internet Out時にTransit Gatewayを経由し、右側のVPC(security用)に通信します
  • 右側のVPCでsecurityのcheckをします。このcheckをsecurity applianceのEC2が実施しており、
    そこへの負荷分散でGateway Load Balancerが登場します
  • securityのcheckを通過したら上のVPCを通じてinternet outします

最終的な通信のイメージは下記のような流れです。
f:id:remmemento:20210904155735p:plain

cloudformationを見てみる

実際のハンズオンではcloudformation templateをご提供頂きました。
Gateway Load Balancerや内部のネットワーク構成を
どう構築されているかcfnを見てみました。

※実際のハンズオンでは手順とcloudformation templateをご提供頂いたのですが、
全部公開はNGで、部分的には載せて良いですよ言って頂けたので一部だけ載せさせて頂きます。

Gateway Load Balancer

ALBとほぼ同じでした

  GWLB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: gwlb-lab
      Type: gateway
      Subnets:
        - !Ref SecurityApplianceSubnet1
        - !Ref SecurityApplianceSubnet2
      Tags:
      - Key: Name
        Value: gwlb-lab

ターゲットグループでEC2を登録しています

  GWLBTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: gwlb-target
      Port: 6081
      Protocol: GENEVE
      TargetGroupAttributes:
      - Key: deregistration_delay.timeout_seconds
        Value: 20
      VpcId: !Ref SecurityVPC
      HealthCheckPort: 22
      HealthCheckProtocol: TCP
      TargetType: instance
      Targets:
        - Id: !Ref ApplianceAZ1EC2Instance
        - Id: !Ref ApplianceAZ2EC2Instance
      Tags:
      - Key: Name
        Value: gwlb-target

VPC Endpoint

VPC Endpointを作成しています
ただ、VPC Endpointには直接loadbalancerの紐付けがされていませんでした。

  GWLBEAZ1:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref SecurityVPC
      ServiceName: !GetAtt GWLBESerivceName.Data
      VpcEndpointType: GatewayLoadBalancer
      SubnetIds:
       - !Ref SecurityApplianceSubnet1

  GWLBEAZ2:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref SecurityVPC
      ServiceName: !GetAtt GWLBESerivceName.Data
      VpcEndpointType: GatewayLoadBalancer
      SubnetIds:
       - !Ref SecurityApplianceSubnet2

代わりにVPC endpointサービスでGatewayLoadBalancerと紐付けをしていました。

  GWLBEService:
    Type: AWS::EC2::VPCEndpointService
    Properties:
      GatewayLoadBalancerArns:
        - !Ref GWLB
      AcceptanceRequired: false

そして、VPC endpointサービスと
VPC endopointを紐づけているようでした。
こちらにカスタムリソースが利用されておりました

  GWLBESerivceName:
    DependsOn: GWLBEService
    Type: Custom::DescribeVpcEndpointServiceConfigurations
    Properties:
      ServiceToken: !GetAtt DescribeGWLBEService.Arn
      Input: !Ref GWLBEService

customリソースを利用してvpc endpoitのサービス名を取得しているようです そして、取得したサービス名でVPC endpointとendpoint serviceを
紐づけているようです。

こうすることで2つのAZに存在するVPC endpointを1つのendpoint serviceに
まとめているようでした。
こんな作り方できるのかととても勉強になりました。

 DescribeGWLBEService:
    Type: AWS::Lambda::Function
    Properties:
      Handler: "index.handler"
      Role: !GetAtt
        - DescribeGWLBEServiceLambdaExecutionRole
        - Arn
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import json
          import logging
          import time
          def handler(event, context):
            time.sleep(600)
            logger = logging.getLogger()
            logger.setLevel(logging.INFO)
            responseData = {}
            responseStatus = cfnresponse.FAILED
            logger.info('Received event: {}'.format(json.dumps(event)))
            if event["RequestType"] == "Delete":
              responseStatus = cfnresponse.SUCCESS
              cfnresponse.send(event, context, responseStatus, responseData)
            if event["RequestType"] == "Create":
              try:
                VpceServiceId = event["ResourceProperties"]["Input"]
              except Exception as e:
                logger.info('VPC Endpoint Service Id retrival failure: {}'.format(e))
              try:
                ec2 = boto3.client('ec2')
              except Exception as e:
                logger.info('boto3.client failure: {}'.format(e))
              try:
                response = ec2.describe_vpc_endpoint_service_configurations(
                  Filters=[
                    {
                      'Name': 'service-id',
                      'Values': [VpceServiceId]
                    }
                  ]
                )
              except Exception as e:
                logger.info('ec2.describe_vpc_endpoint_service_configurations fa: {}'.format(e))
              ServiceName = response['ServiceConfigurations'][0]['ServiceName']
              logger.info('service name: {}'.format(ServiceName))
              responseData['Data'] = ServiceName
              responseStatus = cfnresponse.SUCCESS
              cfnresponse.send(event, context, responseStatus, responseData)
      Runtime: python3.7
      Timeout: 900

ありがとうございました!

Gateway load balanerは初めて触ったのですが、
それ以外にも上記のように普段触ることないNW構成を触ることができ
とても勉強になりました
ありがとうございました!