ISID テックブログ

ISIDが運営する技術ブログ

Oura Ringで就寝時間をレコメンドする

こんにちは。ISID 金融ソリューション事業部の若本です。

1ヶ月ほど前にOura Ringというスマートデバイスを購入しまして、生体データを色々取り溜めていました。せっかくデータを取るなら何かに使いたいということで、機械学習モデルと組み合わせて就寝時間をレコメンドしてみます。

Oura Ringとは?

Oura Ring指輪型の健康トラッカーです。多種のセンサーを内蔵しており、運動や睡眠、心拍数に関するデータを日々蓄積しています。完全に個人の感想ですが、充電持ちがよく着用の違和感もないので重宝しています。

なお、蓄積したデータはAPIを使ってアクセスできます。APIは2種類ありますが、今回はOura API V1のデータのみを使いました。より詳細なデータを取得できるOura API V2もありますが、こちらは運動や心拍のデータがメインとなっており、コンディションや睡眠に関するデータは記事執筆時点で提供されていません。今回はAPI V1で取得できるデータのうち、機械学習モデルへの入力として「前日の睡眠時間・起床時間・就寝時間」を、出力として「翌日のReadinessスコア」を使用しました。

※V1の機能はV2に順次移行していくようですので、仕様は変わる可能性があります。適宜APIリファレンスをご参照ください。

Oura Ringでやりたいこと

Oura Ringでは平均的な就寝時間の範囲を把握することはできますが、就寝時間を変えることによって体調がどれほど変化するのかはわかりません。そこで、Oura Ringのデータから「何時に寝ればいいのか」「それによってどれくらい次の日のコンディションがよくなるのか」を教えてくれるAIモデルを作成します。
Oura Ringでは体が準備できているかを表す指標としてReadinessスコアを算出しており、これを疑似的にコンディションとみなすことができそうです。このReadinessスコアを高くするために、前日の睡眠実績から「今日は何時に寝るべきか」を推論することがゴールです。

機械学習モデルによる予測

Oura Ringで取得している睡眠実績データを基に、同じくOura Ringで算出されるReadinessスコアを予測します。取り貯めた1ヶ月のデータをOura API V1で取得して得られた前日の睡眠時間、起床時間、就寝時間を基に、翌日のReadinessスコアを予測する機械学習モデルを作成しましょう。

ここで、Oura APIで取得したデータについて前処理を行います。取得した起床時間/就寝時間はタイムゾーンなのでそのまま機械学習モデルで扱うことができません。floatへ型変換を行いましょう。具体的には、「何時何分か」の情報を数値情報に変換します。一番簡単なのは1日を0~1の数値に落とし込むことですが、単純に24で除算してしまうと24時以降の値が不連続になってしまうので、14時までのデータは24を足すことで連続するようにしました。睡眠時間とスコアについても適当にスケーリングします。

def time_to_float_converter(x):
    time = pd.to_datetime(x)
    hour = (time.hour + 24 if time.hour < 14 else time.hour) / 24 - 0.5
    minute = time.minute / (24 * 60)
    return (hour+minute)
    
info_df['sleep_bedtime_start'] = info_df['sleep_bedtime_start'].apply(lambda x: time_to_float_converter(x))
info_df['sleep_bedtime_end'] = info_df['sleep_bedtime_end'].apply(lambda x: time_to_float_converter(x))
info_df['sleep_duration'] = info_df['sleep_duration'] / 3600 / 12
info_df['readiness_score'] /= 100

前処理後のデータは以下のようになります。全てのデータをfloat型に落とし込めていることが確認できました。

Readinessスコアの説明によると、Readinessスコアは運動・睡眠・心拍数の長期的な推移で算出されているそうですが、今回は睡眠データの一部しか使わないため、正確にReadinessスコアを予測することは不可能です。さらに期間も短いため、外れ値がモデルの学習に影響を及ぼしやすい条件になっています。そこで、予測モデルは外れ値の影響を受けづらいHuber Regressorを選定しました。なお、モデルの作成時にはPyCaretでチューニングしたパラメータを用いています。

X_train = info_df.drop(['readiness_score'], axis=1)
y_train = info_df['readiness_score']
model = HuberRegressor(**params)
model.fit(X_train, y_train)

DiCEで就寝時間をレコメンド

上記のモデルで翌日のReadinessスコアを予測したい場合、就寝時間の入力を使うことができません。なぜなら、予測時点でまだ就寝時間が決まっていないからです。

なので、モデルの理想の出力に合わせて就寝時間を計算し、就寝時間をレコメンドします。反実仮想説明(CE: Counterfactual Explanation)に用いられるDiCE1でこれを実現します。
DiCEの詳細な説明は省きますが、DiCEでは「もしも~の入力とその予測結果」(反実仮想サンプル)を生成します。詳細は原著論文をご参照ください。DiCEを使用することで、予測を変えるために入力をどれだけ変えるべきかがわかります。
まず先ほど学習済みの機械学習モデルをDiCEに取り込みます。

d = dice_ml.Data(dataframe=info_df,
                 continuous_features = list(info_df.drop(['readiness_score'], axis=1).columns), # 連続変数の指定(今回はすべて該当)
                 outcome_name = 'readiness_score')
m = dice_ml.Model(model=model, backend="sklearn", model_type='regressor')
exp = dice_ml.Dice(d, m, method="random")

次にDiCEを用いて、学習データから反実仮想サンプルを生成してみます。今回は2つ生成してみましょう。21時~27時の間に就寝するという制約を付け、Readinessスコアが80点以上となるようなサンプルを生成します。

index = 0
counterfactuals_num = 2
conf = exp.generate_counterfactuals(X_train.iloc[index:index+1, :], 
                                  total_CFs=counterfactuals_num, 
                                  features_to_vary=["sleep_bedtime_start"],
                                  permitted_range={'sleep_bedtime_start': [0.417, 0.625]}, # 21時~27時の間
                                  desired_range=[0.8, 1.0]) # スコアが80~100点ならOK
conf.visualize_as_dataframe(show_only_changes=True)

上記のような結果が得られました。sleep_duration(前日の睡眠時間)やsleep_bedtime_end(起床時間)は変わらず、sleep_bedtime_start(就寝時間)のみ変化していることが分かります。最後に、得られた反実仮想サンプルを時刻に戻して出力してみます。

def float_to_time_converter(x: float):
    time_tmp = (x + 0.5) * 24
    f, i = math.modf(time_tmp)
    pred_hour = int(i)
    pred_minites = int(f * 60)
    return pred_hour, pred_minites
    
def messenger(df):
    for i, row in df.iterrows():
        pred_hour, pred_minites = float_to_time_converter(row['sleep_bedtime_start'])
        score = row['readiness_score']
        print(f'<候補{i+1}> {pred_hour}:{pred_minites}に就寝すると{score*100:.1f}点のReadinessスコアが見込めます。')


conf_df = conf.cf_examples_list[0].final_cfs_df_sparse # DiCEの結果をpandasに変換
messenger(conf_df)

前日の睡眠状態から、「24時過ぎに寝ること」をレコメンドされました。現実的な数値なので納得感はあります。23時過ぎの就寝もレコメンドされていますが、1時間早く寝ても翌日の予測Readinessスコアは0.2点しか上がらないようです。
思ったよりも就寝時間の差による影響が少なかった印象ですが、より長い期間のデータを基にモデルを作ると顕著な傾向が見られるかもしれません。もしくは就寝時間だけでなく、今回使用しなかった運動データなどについてもレコメンド対象とすれば、より大幅にReadinessスコアをコントロールできそうです。

おわりに

今回は、Oura Ringで取得したデータを用いた就寝時間のレコメンドについて紹介しました。今回は簡易的な検証として睡眠データとReadinessスコアのみを用いましたが、好みに合わせて入力を変えるのも面白そうです。予測したい変数についても1日の総タイピング数、Gitのcommit行数などに変えてもいいかもしれません。
さらに、LINE NotifyHerokuなどのサービスを組み合わせることで就寝時間を毎日通知してくれるようにもできます。この指輪1つで色々と面白いことができそうですね。

おまけ

今回使用しているコードはこちらに公開しています。Oura Ringをお持ちの方はぜひお試しください。

執筆:@wakamoto.ryosuke、レビュー:@higaShodoで執筆されました


  1. DiCE: Diverse Counterfactual Explanations(https://github.com/interpretml/DiCE

AWSインフラをCDKでIaC化したらcdk-nagでセキュリティスキャンしたくない?(特にサプレスの方法)

こんにちは。X(クロス)イノベーション本部 ソフトウェアデザインセンター セキュリティグループの耿です。

クラウドインフラをIaC化すると、静的セキュリティスキャンができるようになります。インフラをデプロイする前にセキュリティ上の問題や、ベストプラクティスに沿っていない構成を知ることができるため、ぜひスキャンしておきたいところです。
今までのスキャンツールとしては、以下のようなものがありました。

  • tfsec
    • OSS
    • Terraformに対応
  • cfn_nag
    • OSS
    • CloudFormationテンプレートに対応
  • terrascan
    • OSS
    • Terraform、CloudFormationテンプレート、Azure Resource Managerなどに対応
  • Snyk IaC
    • 有償(無償プランあり)
    • Terraform、CloudFormationテンプレート、Azure Resource Managerなどに対応

最近、AWS CDKで使いやすい、AWS開発のOSSツール cdk-nag の存在を知り、非常にテンションが上がっているためブログを書いております!
前半では従来のツールでCDKコードをスキャンするときの問題点とcdk-nagの導入方法、後半では実用する時に欠かせない、cdk-nagエラーのサプレス方法を書いていきます。

TL;DR

  • CDKのセキュリティスキャンはcdk-nagが使いやすい
  • 意味のあるCIにするためには正しいサプレスが重要。細かい粒度でサプレスするべき
  • appliesTo でリソースレベルよりもさらに細かい粒度でサプレスできる
  • CDKが勝手に作成するリソースについては、 applyToChildrentrue にするか、addResourceSuppressionsByPath でサプレスできる
  • サプレスはCDKコードの最後にまとめて書くのが良さそう
  • 既存のCDKに導入する時、コメントアウトして少しずつ対応すると良い

cfn_nagでCDKをスキャンするときの問題点

cdk-nagの存在を知る前までは、cfn_nagとterrascanをGitHub Actionsのワークフローに組み込んで使っていました。どちらのツールも直接CDKをサポートしていませんが、cdk synth で生成されるCloudFormationテンプレートに対してスキャンをかけることができます。

例として、cfn_nagのスキャン結果は次のようになります(一部抜粋、編集済)

------------------------------------------------------------
cdk.out/MyStack.template.json
------------------------------------------------------------
| WARN W89
|
| Resource: ["AWS679f53fac002430cb0da5b7982bd22872D164C4C", "SomeTrivialFunction4CC789DF"]
| Line Numbers: [647, 2395]
|
| Lambda functions should be deployed inside a VPC

これには次のような問題があることがわかりました。

問題点1:CDKコードとの関連が分かりにくい

スキャン結果では行番号を出力してくれていますが、CloudFormationテンプレート内の行番号なので、CDKで探す際の参考にはなりません。
またリソースの論理IDも出力されますが、こちらもCloudFormationテンプレートに変換した後のIDなので、CDKで記述したIDとは少し異なります。明示的にCDKで定義したリソースならば、IDの先頭の文字列を見ればどのリソースかなんとなく分かりますが、CDKが自動で作成するリソースについてはIDが勝手に割り振られるため、何によって作成されたリソースかは判断つきません。

問題点2:サプレスがイケてない

こちらが一番重要です。cfn_nagでは特定のリソースに対して特定のルールをサプレスしたい場合、CloudFromationテンプレートのMetadataに記載することになります。
CDKからMetadataを追加する場合、一度CloudFormationリソースとして取得する必要があり、コードが煩雑になります。(CDKが自動で作成するリソースのルールをサプレスする方法は、まだ試していませんがさらに大変そうです)

const cfnFunction = someTrivialFunction.node.defaultChild as lambda.CfnFunction;
cfnFunction.cfnOptions.metadata = {
  cfn_nag: { rules_to_suppress: [{ id: "W89", reason: "No need to deploy this function inside VPC" }] },
};

cdk-nagセットアップ

ここからはcdk-nagを使っていきます。AWSのブログcdk-nagのREADMEに従ってセットアップします。

cdk-nagパッケージをインストールします。

npm install -D cdk-nag

v2.15.32時点でルールパックは5種類ありますが、今回は AWS Solutions を利用します。またエラー詳細を出力するように verbose: true を指定します。

import * as cdk from "aws-cdk-lib";
import { AwsSolutionsChecks } from "cdk-nag";
import { ExampleStack } from "../lib/example-stack";

const app = new cdk.App();
new ExampleStack(app, "ExampleStack", {});

cdk.Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));

cfn_nagと異なり、cdk-nagはCDKと直接統合されており、CDKのAspects を利用し、Synthesize前のPrepareの段階でコードを実行する仕組みです。サプレスされていないルール違反がある場合はフローの途中でエラーが返るため、cdk deploy を実行しても後続のデプロイ処理がされません。CI/CDに組み込みやすいです。

スキャンしてみる

まずは簡単なS3バケットを作って、 cdk synth を実行します。

import { Stack, StackProps } from "aws-cdk-lib";
import * as s3 from "aws-cdk-lib/aws-s3";
import { Construct } from "constructs";

export class ExampleStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const myBucket = new s3.Bucket(this, "MyBucket", {});
  }
}

するとエラーが4つ出ました。

[Error at /ExampleStack/MyBucket/Resource] AwsSolutions-S1: The S3 Bucket has server access logs disabled. The bucket should have server access logging enabled to provide detailed records for the requests that are made to the bucket.

[Error at /ExampleStack/MyBucket/Resource] AwsSolutions-S2: The S3 Bucket does not have public access restricted and blocked. The bucket should have public access restricted and blocked to prevent unauthorized access.

[Error at /ExampleStack/MyBucket/Resource] AwsSolutions-S3: The S3 Bucket does not default encryption enabled. The bucket should minimally have SSE enabled to help protect data-at-rest.

[Error at /ExampleStack/MyBucket/Resource] AwsSolutions-S10: The S3 Bucket or bucket policy does not require requests to use SSL. You can use HTTPS (TLS) to help prevent potential attackers from eavesdropping on or manipulating network traffic using person-in-the-middle or similar attacks. You should allow only encrypted connections over HTTPS (TLS) using the aws:SecureTransport condition on Amazon S3 bucket policies.

内容は以下のとおりです

  • AwsSolutions-S1:サーバーアクセスログが有効でない
  • AwsSolutions-S2:ブロックパブリックアクセスが有効でない
  • AwsSolutions-S3:デフォルト暗号化が有効でない
  • AwsSolutions-S10:TLS通信の強制が有効でない

同時に cdk.out/AwsSolutions-ExampleStack-NagReport.csv にも結果レポートが出力されます。違反のあったルールだけではなく、クリアしたルールとサプレスしたルールについても出力されています。

サーバーアクセスログ以外の3つのエラーに対応してみます。

    const myBucket = new s3.Bucket(this, "MyBucket", {
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
      enforceSSL: true,
    });

再び cdk synth を実行すると、エラーが1つだけになりました。

[Error at /ExampleStack/MyBucket/Resource] AwsSolutions-S1: The S3 Bucket has server access logs disabled. The bucket should have server access logging enabled to provide detailed records for the requests that are made to the bucket.

今回、このバケットではサーバーアクセスログを取得しないものとして、このルールをサプレスしようと思います。

cdk-nagのサプレス方法

なぜサプレスが重要なのか

スキャン結果を必要に応じて簡単にサプレスできることは、意味のあるCIをするために非常に重要です。

スキャンルールはあくまでもベストプラクティス集であり、その全てをクリアできないケース(あるいはする必要がないケース)が多いでしょう。不要なルールがサプレスされずに結果に残り続けると、確認する際のノイズになり、差分も分かりにくくなります。CIに組み込んでいる場合、ノイズが多いとそのうち結果を確認しなくなります。

また、なるべく細かい粒度(リソースレベルかそれ以下)でサプレスすることも重要です。プロジェクト全体で絶対に対応しない方針としたものであれば良いのですが、不用意にグローバルでルールをサプレスしてしまうと、リソースを追加した際に気にかけておくべきベストプラクティスを見逃すことになります。

基本的なサプレス

サプレスは id にルールIDを渡し、次のように行います。

import { NagSuppressions } from "cdk-nag";

...

    NagSuppressions.addResourceSuppressions(myBucket, [
      { id: "AwsSolutions-S1", reason: "Bucket storing logs. No need to export logs itself" },
    ]);

cdk synth を実行するとエラーがなくなり、CloudFormationテンプレートが出力されました。

cdk-nagでは reason の記載が必須であり、さらには文字数が10文字未満だと次のようなエラーになります。将来読んでも理由がよくわかるように正確に書きましょう。

Error: MyBucket: 
        Error(s) detected in suppression with 'id' AwsSolutions-S1. The suppression must have a 'reason' of 10 characters or more.

サプレスのために記載したreason は、出力したCloudFormationテンプレートの Metadata に書き込まれます。CloudFormationテンプレートに入力されたマルチバイト文字は文字化けしてしまい、cdk diff として間違って検出されてしまうため、今のところ reason 欄は英語で記載するのが良さそうです。

スタック全体で特定のルールをサプレス

特定のルールをスタック全体でサプレスしたい場合は、次のように書けますが、あまり濫用しない方が良いでしょう。

    NagSuppressions.addStackSuppressions(this, [
      { id: "AwsSolutions-S1", reason: "Enabling server access logs is not requiered" },
    ]);

より細かい粒度のサプレス

S3バケットへのアクセスを許可するIAMポリシーを作ってみます。

import * as iam from "aws-cdk-lib/aws-iam";

...

    const myIamPolicy = new iam.ManagedPolicy(this, "MyIAMPolicy", {
      statements: [
        new iam.PolicyStatement({
          resources: [myBucket.bucketArn, `${myBucket.bucketArn}/*`],
          actions: ["s3:Get*", "s3:ListBucket"],
          effect: iam.Effect.ALLOW,
        }),
      ],
    });

エラーが2つ出ました。

[Error at /ExampleStack/MyIAMPolicy/Resource] AwsSolutions-IAM5[Action::s3:Get*]: The IAM entity contains wildcard permissions and does not have a cdk-nag rule suppression with evidence for those permission. Metadata explaining the evidence (e.g. via supporting links) for wildcard permissions allows for transparency to operators. This is a granular rule that returns individual findings that can be suppressed with 'appliesTo'. The findings are in the format 'Action::<action>' for policy actions and 'Resource::<resource>' for resources. Example: appliesTo: ['Action::s3:*'].

[Error at /ExampleStack/MyIAMPolicy/Resource] AwsSolutions-IAM5[Resource::<MyBucketF68F3FF0.Arn>/*]: The IAM entity contains wildcard permissions and does not have a cdk-nag rule suppression with evidence for those permission. Metadata explaining the evidence (e.g. via supporting links) for wildcard permissions allows for transparency to operators. This is a granular rule that returns individual findings that can be suppressed with 'appliesTo'. The findings are in the format 'Action::<action>' for policy actions and 'Resource::<resource>' for resources. Example: appliesTo: ['Action::s3:*'].

IAMポリシーにワイルドカード(*)が使われており、広いアクセス権限を付与していることに対するエラーです。ActionとResourceの両方でワイルドカードを使っているため、両方について指摘されています。今回は問題ないこととしてサプレスするとします。

このIAMポリシー全体に対してルール AwsSolutions-IAM5 をサプレスすることも可能ですが、そうすると将来的に追加するあらゆるワイルドカードも許可してしまいます。そうではなく、特定のActionやResourceに対してのみワイルドカードを許可する方が良いでしょう。

エラーをよく見ると、今回はルールIDの後に括弧がついており([Action::s3:Get*] [Resource::<MyBucketF68F3FF0.Arn>/*])、問題となった具体的なActionやResourceがわかるようになっています。
appliesTo を使うことで、特定の記述のみをサプレスできます。

    NagSuppressions.addResourceSuppressions(myIamPolicy, [
      {
        id: "AwsSolutions-IAM5",
        reason: "Necessary to grant Get access to all objects in the bucket",
        appliesTo: ["Action::s3:Get*", "Resource::<MyBucketF68F3FF0.Arn>/*"],
      },
    ]);

Resourceは前に作成したS3バケットへの参照となっていますが、ルールIDの後の括弧にある Resource::<MyBucketF68F3FF0.Arn>/* をそのまま appliesTo に追加して構いません。これでサプレスされました。

ちなみに、appliesTo の中に正規表現を書くこともできます。

    NagSuppressions.addResourceSuppressions(myIamPolicy, [
      {
        id: "AwsSolutions-IAM5",
        reason: "Necessary to grant Get access to all objects in the bucket",
        appliesTo: [
          "Action::s3:Get*",
          {
            regex: "/^Resource::<MyBucketF68F3FF0.Arn>(.*)$/g",
          },
        ],
      },
    ]);

CDKが作成するリソースのサプレス

今度はLambda関数を作ります。

import { Runtime } from "aws-cdk-lib/aws-lambda";
import * as lambdaNodejs from "aws-cdk-lib/aws-lambda-nodejs";
import * as logs from "aws-cdk-lib/aws-logs";

...

    const myTrivialFunction = new lambdaNodejs.NodejsFunction(this, "MyTrivialFunction", {
      entry: "aws-cdk/functions/my-trivial-function.ts",
      runtime: Runtime.NODEJS_16_X,
      logRetention: logs.RetentionDays.SIX_MONTHS,
      bundling: { forceDockerBundling: false },
    });

3つのエラーが出ました。まずは1つ目です。

[Error at /ExampleStack/MyTrivialFunction/ServiceRole/Resource] AwsSolutions-IAM4[Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]: The IAM user, role, or group uses AWS managed policies. An AWS managed policy is a standalone policy that is created and administered by AWS. Currently, many AWS managed policies do not restrict resource scope. Replace AWS managed policies with system specific (customer) managed policies.This is a granular rule that returns individual findings that can be suppressed with 'appliesTo'. The findings are in the format 'Policy::<policy>' for AWS managed policies. Example: appliesTo: ['Policy::arn:<AWS::Partition>:iam::aws:policy/foo'].

このLambda関数が明示的に実行ロールを指定していないため、実行ロールが自動で作成されており、 AWSLambdaBasicExecutionRole ポリシーが付与されています。AWSマネージドポリシーはリソースレベルで権限を絞っていないため、独自のポリシーを使うべきという指摘です。今回は問題ないとし、サプレスします。

しかし今までに述べた方法ではうまくいきません。エラーになっているリソースは関数そのものではなく、関数が使用しているロールだからです。この場合、addResourceSuppressions の3つ目の引数の applyToChildrentrue を渡すことでサプレスできます。(合わせ技でappliesTo には具体的なポリシーを指定しています)

    NagSuppressions.addResourceSuppressions(
      myTrivialFunction,
      [
        {
          id: "AwsSolutions-IAM4",
          reason: "OK to use AWS managed AWSLambdaBasicExecutionRole",
          appliesTo: ["Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"],
        },
      ],
      true
    );

2つ目、3つ目のエラーは次の通りです。

[Error at /ExampleStack/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/Resource] AwsSolutions-IAM4[Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]: The IAM user, role, or group uses AWS managed policies. An AWS managed policy is a standalone policy that is created and administered by AWS. Currently, many AWS managed policies do not restrict resource scope. Replace AWS managed policies with system specific (customer) managed policies.This is a granular rule that returns individual findings that can be suppressed with 'appliesTo'. The findings are in the format 'Policy::<policy>' for AWS managed policies. Example: appliesTo: ['Policy::arn:<AWS::Partition>:iam::aws:policy/foo'].

[Error at /ExampleStack/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/DefaultPolicy/Resource] AwsSolutions-IAM5[Resource::*]: The IAM entity contains wildcard permissions and does not have a cdk-nag rule suppression with evidence for those permission. Metadata explaining the evidence (e.g. via supporting links) for wildcard permissions allows for transparency to operators. This is a granular rule that returns individual findings that can be suppressed with 'appliesTo'. The findings are in the format 'Action::<action>' for policy actions and 'Resource::<resource>' for resources. Example: appliesTo: ['Action::s3:*'].

リソースパスが長くランダム風の文字列になっており、CDKが自動で作成したリソースであることがわかります。Lambda関数を作る時に logRetention を指定した場合、ログリテンションポリシーを設定するためのLambda関数が1つ作られ、その実行ロールに対する指摘です。
明示的にCDKに記述したLambda関数の子リソースではないため、applyToChildrentrue にしてもサプレスできません。エラーメッセージに出力されているリソースパスを使い、addResourceSuppressionsByPath でスタックからこのリソースを見つけてもらいます。

    NagSuppressions.addResourceSuppressionsByPath(
      this,
      "/ExampleStack/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/Resource",
      [
        {
          id: "AwsSolutions-IAM4",
          reason: "CDK managed resource",
          appliesTo: ["Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"],
        },
      ]
    );

    NagSuppressions.addResourceSuppressionsByPath(
      this,
      "/ExampleStack/LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a/ServiceRole/DefaultPolicy/Resource",
      [
        {
          id: "AwsSolutions-IAM5",
          reason: "CDK managed resource",
          appliesTo: ["Resource::*"],
        },
      ]
    );

ちなみにCDKでカスタムリソースを作成する場合もLambda関数が作られますが、そのサプレスにもこの方法を利用できます。

interfaceEndpoints作成時のWarningのサプレス

VPCとセキュリティグループ、VPCエンドポイントを作ります。

import * as ec2 from "aws-cdk-lib/aws-ec2";

...
    const vpc = new ec2.Vpc(this, "VPC", {
      flowLogs: {
        s3: {
          destination: ec2.FlowLogDestination.toS3(myBucket, "vpc"),
          trafficType: ec2.FlowLogTrafficType.ALL,
        },
      },
    });

    const appSecurityGroup = new ec2.SecurityGroup(this, "appSecurityGroup", {
      vpc: vpc,
      allowAllOutbound: true,
    });

    const httpsSecurityGroup = new ec2.SecurityGroup(this, "httpsSecurityGroup", {
      vpc: vpc,
      allowAllOutbound: true,
    });
    httpsSecurityGroup.addIngressRule(appSecurityGroup, ec2.Port.tcp(443));

    vpc.addInterfaceEndpoint("SSMEndpoint", {
      service: ec2.InterfaceVpcEndpointAwsService.SSM,
      subnets: { subnets: vpc.publicSubnets },
      securityGroups: [httpsSecurityGroup],
      privateDnsEnabled: true,
    });

Cloudformationテンプレートは問題なく生成されますが、実はチェック失敗のWarningが出力されています。

[Warning at /ExampleStack/httpsSecurityGroup/Resource] CdkNagValidationFailure: 'AwsSolutions-EC23' threw an error during validation. This is generally caused by a parameter referencing an intrinsic function. For more details enable verbose logging.' The parameter resolved to to a non-primitive value "{"Fn::GetAtt":["VPCB9E5F0B4","CidrBlock"]}", therefore the rule could not be validated.

こちらのIssueに記載がある通り、想定通りの挙動でサプレスして良いとのことなので、次のように idCdkNagValidationFailure を渡すことでサプレスできます。

    NagSuppressions.addResourceSuppressions(httpsSecurityGroup, [
      { id: "CdkNagValidationFailure", reason: "https://github.com/cdklabs/cdk-nag/issues/817" },
    ]);

サプレスはどこに書くべきか

プロジェクトの対応方針によっては大量にサプレスが発生する場合があります。cdk-nagはCDKコード内にサプレスを記載するため、どこに書くべきかについて考えてみます。2つのパターンが考えられます。

  1. エラーが発生したリソース付近に書く
  2. サプレスのみをまとめて書く

サプレスをどこに書くべきか

「1. エラーが発生したリソース付近に書く」場合、どのリソースに対するサプレスかがわかりやすい一方で、インフラを定義するコードとセキュリティスキャンのサプレスコードが入り混じるため、見通しの悪いコードになってしまう印象があります。IaCコードを読む時は、サプレス済みのセキュリティスキャンルールは意識の外に置きたいです。
そこで個人的には「2. サプレスのみをまとめて書く」のが良いのではないかと思います。スタックの最後にサプレスをまとめて記述すれば、コードを読む際の邪魔にはなりません。それでもリソースの数が増えるとどこで何をサプレスしているのか探しにくくなるので、CDKを書く際には1つのスタックにリソースを詰め込みすぎず、適度な長さに分割するのが良いでしょう。

まとめ

さまざまなケースを取り上げてサプレスする方法を説明しました。ハードルは下がったと思うので、ぜひcdk-nagを導入してセキュリティチェックをしていきましょう。

既存のCDKコードにcdk-nagを入れる場合、たくさんのエラーが出る可能性があります。まずはコードを全てコメントアウトした上で少しずつコメントを外し、 cdk synth でエラーをステップ・バイ・ステップで対応していくのが良いと思います。

今回はAWS Solutionsルールパックのみでしたが、そのうち他のルールパックについても試してみたいと思います。ここまで読んでいただきありがとうございました。


私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。
- セキュリティエンジニア(セキュリティ設計)

執筆:@kou.kinyo2、レビュー:@kou.kinyo2Shodoで執筆されました

DistrolessイメージでPrismaを動かしてみた

こんにちは、X(クロス)イノベーション本部 ソフトウェアデザインセンター・セキュリティグループの大西です。現在、DockerとTypeScriptを使ってシステムを開発中です。DockerのDistrolessイメージの中で、ORMのPrismaを使おうとするとエラーが出てハマってしまったので、エラー解消の方法についてお話ししたいと思います。 まずは少し、DistrolessイメージとPrismaについて説明します。

Distrolessイメージとは

Googleが公開しているDistrolessイメージとは、アプリケーションの実行に必要な最小限のファイルのみが入っている超軽量なDockerイメージです。それゆえ、普通のOSに入っているようなパッケージマネージャーやシェルなどは入っていません。最も小さいサイズのものでgcr.io/distroless/static-debian11はたったの2MiBほどしかなく、alpine(5Mib)の約半分、debian(124MiB)の2%のサイズしかありません。メリットとしては、不要なファイルを含まないことで攻撃対象領域(Attack Surface)を最小限に抑えており、不要なバグや脆弱性を埋め込みにくいという点が挙げられます。また、イメージが軽いことでリポジトリの容量を抑えられ、起動時の読み込みが軽いのでスケーリングにかかる時間が減るという利点もあります。

Prismaとは

Prismaとは、Node.jsとTypeScriptの環境下で動くオープンソースのORMです。MySQLPostgreSQLSQLiteSQL Serverなどに対応しており、NextJS、NestJS、GraphQLなど多くのフレームワークにも対応しています。新しいリリースは約2週間に一度行われ、先日v4.0.0がリリースされました。Prismaでよく使われる機能は以下の2つです。 Prisma Client: 型安全なデータベースクライアントでアプリに合わせたタイプのPrismaスキーマから自動生成される Prisma Migrate: カスタマイズ可能なSQLデータベースマイグレーションを自動的に生成し、移行ファイルを生成せずにデータベースに変更を加えることができる

PrismaとTypeORMの比較

今回、開発を進める中でPrismaとTypeORMどちらも使用してみましたが、Prismaの方が直感的にDBを操作できるなと感じました。書くコードの量もTypeORMよりも少なくてすみ、使いやすい印象です。個人的に便利だと思ったのは、スキーマの自動生成機能です。npx prisma db pullコマンドを実行するだけで、すでにあるDBからスキーマを読み出しPrisma用のスキーマ(schema.prisma)を自動で作成してくれます。最初はTypeORMでモデルを実装していたので、TypeORMから出力されるデータベースのスキーマがすでに存在していました。そのため、Prismaスキーマを自動生成する機能によってTypeORMからPrismaに変更するときも割と簡単に移行できました。既存のプロジェクトにPrismaを導入するときの手順はこちらにあります。また、PrismaとTypeORMの比較はこちらです。今回は使用していませんが、TypeORMからのマイグレーションの方法もこちらで紹介されています。

今回ハマったところ

まず背景として、DockerのDistrolessイメージを使いたいという開発者の気持ちがありました。その理由は、やはりセキュアなイメージだからです。それゆえ、AlpineイメージでPrismaは動くけれど、頑張ってDistrolessで動かしたいという思いがありました。今回ハマったところは、Distroless上でPrismaを使ってDB操作を実施しようとするとき、PrismaClientInitializationError: Unable to load Node-API Library from /usr/app/node_modules/.prisma/client/libquery_engine-debian-openssl-1.1.x.so.node, Library may be corruptというエラーが出てPrismaを使えなくなったところです。ライブラリが壊れているかもしれない、と言われても・・・。このエラーメッセージで検索しても解決策は見つからず、どこに原因要素があるのか調べるためいろいろ試したところ、alpineイメージではPrismaが動くことが分かりました。

解決策

alpineイメージで動くことは分かったものの、alpineイメージにはあり、Distrolessイメージにはない何かを見つけることができません。そんなとき、同じ部の先輩が、Distrolessコンテナの中に入るためシェルのあるdistroless-debugイメージに変えることを思いつきました。そして、もう一度GitHubPrismaコードをよく見ると、Library may be corruptのエラーメッセージが出ている箇所のエラーがサプレスされていることに気づきました。Library may be corruptのエラーメッセージが出力されている部分のエラー このeを見れば何かわかるかもしれないということで、next startされた後のシェルに入ってトランスパイルされたJSを直接書き換えてみましたが、eの内容は表示されません。つまり、next startする前にソースコードを書き換えないといけないので、コンテナを作る時に書き換えるためDockerfile上でsedすることを思いつきます。

RUN sed -i -e '41147i console.log(e);' node_modules/@prisma/client/runtime/index.js

するとError: libz.so.1: cannot open shared object file: No such file or directoryというエラーが出ていることが分かりました!libzをDistrolessイメージに入れるため、マルチステージングビルドを使うことにしました。以下のように、libzの入っているalpineイメージからDistrolessイメージにlibzをコピーしてみるとエラーが変わり、Error: libc.musl-x86_64.so.1: cannot open shared object file: No such file or directoryとなったのでzlibと同様にlibc.musl-x86_64.so.1というCのライブラリもコピーしました。最終的なDockerfileは以下のようになりました。

# libzの入ったalpineイメージを作成
FROM alpine as lib

# 上記のalpineイメージからdistrolessにzlibとmusl clibを提供する
FROM gcr.io/distroless/nodejs:16
COPY --from=lib /lib/libz.so.1 /lib/libz.so.1
COPY --from=lib /lib/libc.musl-x86_64.so.1 /lib/libc.musl-x86_64.so.1

まとめ

セキュアで軽量なイメージだからこそ、必要なものは自分で入れていかないといけないDistrolessイメージ。その必要なものがなかなか分からず、今回は時間がかかってしまいました。エラーに遭遇したとき、とりあえずそのエラーを検索することはしますが、それでも方法が見つからないこともあります。そんな時はソースが公開されていればソースを眺めて、ここにはどんな値が入っているのだろう、ここはどんなエラー文が出ているのだろうと、自分でソースを開拓していくことも技の一つだと知りました。先輩に感謝です!


私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。
- セキュリティエンジニア(セキュリティ設計)

執筆:@onishi.mayu、レビュー:@handa.kentaShodoで執筆されました

Azure Managed Grafana について調べてみた

X(クロス)イノベーション本部 クラウドイノベーションセンターの田村です。 普段の業務では、Microsoft Azure のアップデートや新サービスの調査検証をしており、その一環として Azure Managed Grafana の調査を実施しました。

本記事では、Azure Managed Grafana の概要と機能について、調査結果を絡めてご紹介します。

Azure Managed Grafana とは

Azure Managed Grafana は、Microsoft と Grafana Lab 社が開発したフルマネージドの可視化サービスです。 Grafana は Grafana Lab 社が提供するサービスでしたが、このたび Microsoft とパートナーシップを結び、Azure のマネージドサービスとして公開されました。

もともと Grafana はオープンソースの可視化サービスとして知られており、Kibana や Metabase といったサービスとよく比較されている印象があります。

Azure Managed Grafana の特徴としては、下記の 4 点が紹介されています。

  • Azure に最適化
    • Azure Monitor や Azure Data Explorer などの Azure サービスの可視化を行うために最適化
  • ダッシュボード共有
    • 複数人で Grafana ダッシュボードを共有する際に、管理者/編集者/閲覧者ロールの設定が可能
    • ユーザーロールとロール管理は後述する Azure AD と統合されている
  • ID 管理
    • Azure AD(Active Directory)による ID 管理とユーザーロール管理の統合
    • マネージド ID による Azure サービスへのアクセス
  • 簡単な操作
    • Azure Portal 上からワンクリックで Grafana ダッシュボードへ移動
    • グラフのテンプレート等を活用することで直感的なダッシュボード作成が可能

詳細は下記のリンクをご参照ください。

可視化機能の検証

実際に Azure Managed Grafana でダッシュボードを作成し、可視化機能を検証してみました。 今回は可視化対象のデータソースとして、Azure の監視サービスである Azure Monitor を採用しました。

作業の流れは大まかに下記の通りとなります。

  1. Azure Managed Grafana のリソース作成
  2. データソースの登録
  3. ダッシュボードの作成

Azure Managed Grafana のリソース作成

Azure Portal と Azure CLI から作成できますが、今回は GUI の Azure Portal から作成しました。 作成時に設定を変更しない限り、デフォルトで管理者ロールの割り当てやマネージド ID の作成が実行されるので、権限周りの設定も簡単です。

作成が完了すると、リソース概要のエンドポイント項目に Grafana へのリンク先が表示されます。 こちらの URL を選択することで Grafana へアクセスでき、ダッシュボード作成が可能になります。

データソースの登録

今回可視化する Azure Monitor を登録します。

Grafana にアクセスし、左側のメニューから Configuration > Data sources と選択すると、データソースの設定画面に遷移します。 Azure Monitor がデフォルトで表示されているので選択します。 他のデータソースは Add data source から追加できます。

Authentication にて、認証方式とサブスクリプションを設定します。 認証方式はマネージド ID(Maneged Identity)またはサービスプリンシパル(App Registration)を選択できますが、今回はマネージド ID による認証を設定しました。 Save & test を実行し、成功すればデータソースの登録は完了です。

ダッシュボードの作成

データソースの登録が完了したので、Azure Monitor で監視しているデータを可視化するダッシュボードを作成します。 作成方法は大きく 2 種類あり、1 から自分で作成するパターンと、テンプレートを利用するパターンがあります。

まずは 1 から作成してみます。 左側のメニューから Create > Dashboard > Add panel と選択すると、ダッシュボードの編集画面に遷移します。 デフォルトで表示されているグラフに意味はありません。

データソースに先ほど登録した Azure Monitor を設定し、グラフを作成してみます。 今回は仮想マシンの CPU 使用率を可視化してみました。 作業は単純で、サブスクリプション > リソースグループ > リソース > 可視化するメトリクス(Percentage CPU)の順に選択するだけです。

今回は CPU 使用率を例にしましたが、Azure Monitor をデータソースにすると、各リソースのさまざまなメトリクスを可視化できます。 仮想マシンだけでも、メモリの稼働率やディスクの使用率、インバウンド/アウトバウンドフローなどに対応しています。

このようにしてどんどんダッシュボードを作りこんでいくのですが、慣れない人ではどうしても手間がかかります。 そこで、用意されているテンプレートによるダッシュボード作成も試してみました。

左側のメニューから Dashboard > Browse > Azure Monitor と選択すると、ダッシュボードのテンプレートが一覧で表示されます。 下記の 9 種類がありますが、今回はストレージアカウントの可視化テンプレートを選択しました。

ストレージアカウントのテンプレートを選択すると、構成済みのダッシュボードが表示されます。

上記画像では表示しきれていませんが、ストレージアカウントのテンプレートは 20 以上のパネル(グラフやカード)で構成されています。 これにより、リソースに関するほぼすべてのメトリクスを 1 枚のダッシュボードで確認できます。 ここから編集を加えたり、新たなテンプレートを追加することも可能なので、ユーザーにとって非常に便利な機能です。

サポートしているデータソース

Azure Managed Grafana では、Grafana Enterprise でサポートされているサービスをデータソースとして設定できます。 デフォルトで使用可能なデータソースは 17 種類あり、クラウドサービスや各種 DB サービスに対応しています。

上記以外にも、プラグインを設定することでさまざまなデータソースの可視化が可能です。 プラグインも含めると、サポートされているデータソースは 132 種類になります。

他の Grafana サービスとの違い

今回調査した Azure Managed Grafana の他に、Grafana には OSS/Grafana Cloud*1/Grafana Enterprise といった提供形態があります。 料金やサポートといった点の差異を調査し、下記の表にまとめました。[^4]

Azure Managed Grafana Grafana(OSS Grafana Cloud Grafana Enterprise
データソース 132 132 132 132
料金 従量課金*2 無料 従量課金*3 要問い合わせ
セキュリティ Azure による保護 なし 専用ツール内包 専用ツール内包
サポート Azure サポート なし チケット制 24 時間 365 日

Power BI との違い

Microsoft の代表的な可視化サービスとして Power BI が挙げられます。 Azure における可視化という点で比較すると、Azure Managed Grafana と Power BI の最大の違いは、サポートしている Azure サービスの種類です。

Azure Managed Grafana で可視化できる Azure サービスは、プラグイン設定込みでも 3 つ(Azure Monitor/Azure Data Explorer/Azure DevOps)のみです。 Azure Monitor など、Azure Managed Grafana がサポートしているデータソースで取得できるデータは、ログやメトリクスといった監視情報が多くを占めています。 Grafana ダッシュボード上ではアラートの設定が可能なため、Azure PaaS を可視化のデータソースとした場合は取得した監視情報の可視化に特化している印象です。

一方で、Power BI は Azure SQL Database や Azure Synapse Analytics をはじめ、Azure のデータサービスをほぼ網羅しています。 そのため、Azure 上に構築したデータウェアハウスやデータマートに蓄積したデータを可視化したい、というシナリオには Power BI の方が適しているといえます。

まとめ

本記事では、Azure Managed Grafana の概要と機能について、筆者の調査結果をもとにご紹介しました。 Azure Managed Grafana は 4 月にパブリックプレビューが開始されたばかりのサービスのため、今後もアップデートに注目したいと思います。


X(クロス)イノベーション本部 クラウドイノベーションセンターでは、共に働いてくれる仲間を探しています。 本記事でご紹介したような調査検証をはじめ、クラウドアーキテクトとしての業務に興味がある方のご応募お待ちしております。

執筆:@tamura.kohei、レビュー:@sato.taichiShodoで執筆されました

*1:Grafana Lab 社が独自のクラウドリソースとして提供しているサービス

*2:米国東部リージョンでの見積もり:リソース 1 つにつき $0.069/時間に加え、ユーザー 1人あたり $6/月(ただし 3 人まで無料)

*3:使用したデータ量による従量課金に加え、ユーザー 1人あたり $8/月(ただし 3 人まで無料)

Renovateを用いたKubernetesエコシステムの自動バージョンアップ

こんにちは。Xイノベーション本部クラウドイノベーションセンターの柴田です。

本記事では Renovate を使ったKubernetesエコシステムの自動バージョンアップを紹介します。

なお本記事の内容は Kubernetes Meetup Tokyo #48 で紹介された 個人運用k8sクラスタの構成要素の技術選定 - でこてっくろぐ ねお にインスパイアされています。

背景

Kubernetesでは、Kubernetesと連携して動作する様々なOSSやサービスを活用することで、アプリケーションの開発・運用に関する生産性向上や効率化を図ることができます。 本記事ではそれらをKubernetesエコシステムと呼びます。 多くのKubernetesエコシステムは開発元が提供しているコンテナイメージやマニフェストファイルを自分たちのKubernetesクラスタへデプロイして利用します。 開発が盛んなKubernetesエコシステムでは、頻繁に新機能やセキュリティ改善が実装され、新しいバージョンとしてリリースされます。 Kubernetesエコシステムの恩恵を最大限に享受するにはなるべく新しいバージョンを使用することが望ましいです。

またKubernetesでは、マニフェストファイルを GitOps と呼ばれる手法で管理・デプロイすることが多いです。 GitOpsはWeaveworks社が提唱した継続的デプロイの方法であり、以下の特徴があります。

  • システム全体が宣言的に記述されていること。
  • マニフェストがgitで管理され、それが信頼できる唯一の情報源(Single Source of Truth)であること。
  • 承認されたマニフェストの変更が自動的にデプロイされること。またその際Kubernetesクラスタへの認証情報を外部(例えばCIサーバなど)に持たせる必要がないこと。
  • マニフェストKubernetesクラスタの間に差分がある場合、それを検知したり、自動的に修正したりできること。

GitOpsを実現するツールには例えば Argo CDFluxPipeCD があります。

以上から、Kubernetesエコシステムの新しいバージョンがリリースされる度に、gitリポジトリで管理されたそれらのマニフェストを最新の内容に更新できることが望ましいです。 また、運用負荷がかからないよう、マニフェストの更新はなるべく自動的に行えることが望ましいです。

Renovateとは

Renovate はプロジェクトの依存関係の更新を自動化するツールです。 似たようなツールに Dependabot などがあります。 Renovateはアプリケーションのライブラリの自動バージョンアップに使われることが多いですが、アプリケーション開発以外の用途でも利用できます。

ここからはRenovateで何ができるかを簡単に説明します。 なお本記事では Renovate v32.97.0 を前提とします。

Platform

Renovateは以下に格納された依存関係ファイルを更新できます。

Manager

Renovateは以下の依存関係を更新できます。

Datasource

Renovateは以下の依存先の更新を検知できます。

adoptium-java, artifactory, aws-machine-image, bitbucket-tags, cdnjs, clojure, conan, conda, crate, dart, docker, flutter-version, galaxy, galaxy-collection, git-refs, git-tags, github-releases, github-tags, gitlab-packages, gitlab-releases, gitlab-tags, go, golang-version, gradle-version, helm, hex, jenkins-plugins, maven, node, npm, nuget, orb, packagist, pod, pypi, repology, ruby-version, rubygems, sbt-package, sbt-plugin, terraform-module, terraform-provider

Versioning

依存関係を更新するためにはバージョン表記を比較できる必要があります。 Renovateでは以下のフォーマットのバージョン表記を扱えます。

aws-machine-image, cargo, composer, conan, debian, docker, git, gradle, hashicorp, helm, hex, ivy, loose, maven, node, npm, nuget, pep440, poetry, regex, rez, ruby, semver, semver-coerced, swift, ubuntu

RenovateでKubernetesエコシステムを自動更新する

では実際にRenovateを使ってKubernetesエコシステムの更新作業を自動化したいと思います。

更新対象のKubernetesクラスタ

今回は私が検証用途で使用しているKubernetesクラスタを対象にします。 このKubernetesクラスタには以下の特徴があります。

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/v1.10.3/config/master/aws-k8s-cni.yaml
images:
  - name: 602401143452.dkr.ecr.us-west-2.amazonaws.com/amazon-k8s-cni
    newName: 602401143452.dkr.ecr.ap-northeast-1.amazonaws.com/amazon-k8s-cni
    newTag: v1.10.3
  - name: 602401143452.dkr.ecr.us-west-2.amazonaws.com/amazon-k8s-cni-init
    newName: 602401143452.dkr.ecr.ap-northeast-1.amazonaws.com/amazon-k8s-cni-init
    newTag: v1.10.3

Renovateを実行する

Renovateには大まかに2種類の実行方法があります。

  • GitHub App を使用する
  • 自分たちでRenovateを実行する(Self-Hosting)

今回はGitHub Actions renovatebot/github-action を使用して自分たちでRenovateを実行します。 その他のSelf-Hostingの実行方法は Self-Hosting Renovate を参照してください。

以下はGitHub Actionsのワークフローです。

# .github/workflows/renovate.yaml
name: Renovate
on:
  schedule:
    - cron: '0 0 * * *' # Run workflow at 09:00 AM JST every day
jobs:
  renovate:
    name: Renovate
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run renovate
        uses: renovatebot/github-action@v32.97.0
        with:
          token: ${{ secrets.GH_TOKEN }}
          configurationFile: renovate.json

GH_TOKENhttps://github.com/renovatebot/github-action#token に従って作成したGitHubのPersonal Access Tokenです。 GitHub Actionsでは GITHUB_TOKEN が利用できますが、 https://github.com/renovatebot/github-action#token

Note that the GITHUB_TOKEN secret can't be used for authenticating Renovate.

とあるためこのようにしています。

Renovateの設定

次にRenovateの設定ファイル renovate.json を作成します。 今回使用した設定は以下のとおりです。

{
    "repositories": [
        "ISID/renovate-sample"
    ],
    "extends": [
        "config:base",
        ":prHourlyLimitNone",
        ":prConcurrentLimitNone"
    ],
    "enabledManagers": [
        "argocd",
        "flux",
        "helmfile",
        "kustomize",
        "regex"
    ],
    "labels": [
        "renovate"
    ],
    "assignees": [
        "@ShibataTakao"
    ],
    "argocd": {
        "fileMatch": [
            "^manifests/argo-cd-apps/.*\\.ya?ml$"
        ]
    },
    "flux": {
        "fileMatch": [
            "^manifests/flux/.*\\.ya?ml$",
            "^manifests/flux-ks/.*\\.ya?ml$"
        ]
    },
    "regexManagers": [
        {
            "fileMatch": [
                "(^|/)kustomization\\.ya?ml$"
            ],
            "matchStrings": [
                "https://raw\\.githubusercontent\\.com/(?<depName>[^/]+/[^/]+)/(?<currentValue>[^/]+)/.*",
                "https://github\\.com/(?<depName>[^/]+/[^/]+)/releases/download/(?<currentValue>[^/]+)/.*"
            ],
            "datasourceTemplate": "github-releases",
            "versioningTemplate": "semver"
        }
    ],
    "packageRules": [
        {
            "matchPackageNames": [
                "kubernetes/autoscaler"
            ],
            "matchDatasources": [
                "github-tags",
                "github-releases"
            ],
            "versioning": "regex:^cluster-autoscaler-(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)?$",
            "allowedVersions": "< 1.23.0"
        },
        {
            "matchPackageNames": [
                "k8s.gcr.io/autoscaling/cluster-autoscaler"
            ],
            "matchDatasources": [
                "docker"
            ],
            "allowedVersions": "< 1.23.0"
        }
    ]
}

各設定を順に見ていきましょう。

"repositories": [
    "ISID/renovate-sample"
]

repositories にはRenovateの対象とするリポジトリを指定します。 リポジトリ名を直接指定するかわりに autodiscoverautodiscoverFilter を設定してRenovateの対象とするリポジトリを自動的に検出することもできます。

"extends": [
    "config:base",
    ":prHourlyLimitNone",
    ":prConcurrentLimitNone"
]

extends には Preset を指定します。 Presetは定義済みの設定の集合です。 デフォルトで定義されたPresetを使うだけでなく、独自のPresetを定義することもできます。 今回は以下のPresetを使用します。

"enabledManagers": [
    "argocd",
    "flux",
    "helmfile",
    "kustomize",
    "regex"
]

enabledManagers を設定して明示的に指定したManagerのみを有効化します。

多くのManagerはデフォルトで有効になっています。 各Managerの有効/無効は <manager>.enabled の値を true または false に設定することで変更できます。 または enabledManagers に有効にするManagerを明示的に指定することで、指定したManagerのみを有効にできます。 詳細は Enabling and disabling managers を参照してください。

今回は以下のManagerのみを有効化します。

今回の更新対象のうち

  • Helm: 公式や3rd partyが提供するHelm Chartを利用します。 具体的にはArgo CDの Application リソースやFluxの HelmRelease リソースとしてKubernetesクラスタへデプロイします。 またローカル環境でマニフェストを生成できるよう helmfile も使用しています。

argocdfluxhelmfile を使ってHelm ChartをバージョンアップするためのPRを自動成します。 また

  • Kustomize: kustomize を使って公式が提供するマニフェストhttps経由で参照します。 イメージタグを書き換えることもあります。

kustomizeregex を使って参照先のマニフェストのURLやイメージタグをバージョンアップするためのPRを自動作成します。 regex については後ほどもう少し詳しく説明します。

"labels": [
    "renovate"
],
"assignees": [
    "@ShibataTakao"
]

labelsassignees にはRenovateが依存関係を更新するために作成するPRのLabelsとAssigneesを設定します。

"argocd": {
    "fileMatch": [
        "^manifests/argo-cd-apps/.*\\.ya?ml$"
    ]
},
"flux": {
    "fileMatch": [
        "^manifests/flux/.*\\.ya?ml$",
        "^manifests/flux-ks/.*\\.ya?ml$"
    ]
}

fileMatch には各Managerの依存関係ファイルの追加パスを正規表現で記述します。 多くのManagerにはデフォルトの fileMatch が設定されていますが、デフォルトの fileMatch が設定されていない場合や独自のファイルパスを追加で設定したい場合には、 fileMatch にそれらを追加設定できます。

"regexManagers": [
    {
        "fileMatch": [
            "(^|/)kustomization\\.ya?ml$"
        ],
        "matchStrings": [
            "https://raw\\.githubusercontent\\.com/(?<depName>[^/]+/[^/]+)/(?<currentValue>[^/]+)/.*",
            "https://github\\.com/(?<depName>[^/]+/[^/]+)/releases/download/(?<currentValue>[^/]+)/.*"
        ],
        "datasourceTemplate": "github-releases",
        "versioningTemplate": "semver"
    }
]

regexManagers には regex Managerの設定を記述します。 regex を用いることで正規表現を使用して依存関係を検出・自動更新するユーザー独自の仕組みを構築できます。 今回は

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/v1.10.3/config/master/aws-k8s-cni.yaml
images:
  - name: 602401143452.dkr.ecr.us-west-2.amazonaws.com/amazon-k8s-cni
    newName: 602401143452.dkr.ecr.ap-northeast-1.amazonaws.com/amazon-k8s-cni
    newTag: v1.10.3
  - name: 602401143452.dkr.ecr.us-west-2.amazonaws.com/amazon-k8s-cni-init
    newName: 602401143452.dkr.ecr.ap-northeast-1.amazonaws.com/amazon-k8s-cni-init
    newTag: v1.10.3

で述べた参照先のマニフェストのURLを自動更新するためにこの仕組みを利用します。

fileMatch には更新対象のファイル kustomization.yaml のファイルパスを正規表現で設定します。

matchStrings には kustomization.yaml 内の更新対象の文字列を設定します。 今回は

  • https://raw.githubusercontent.com/<user>/<repo>/<tag>/<filepath>GitHubリポジトリのコンテンツ)
  • https://github.com/<user>/<repo>/releases/download/<release>/<filepath>GitHubリポジトリのリリースのアセット)

が更新対象です。

regex では更新対象の依存関係に関する以下の設定をする必要があります。 これらは、該当する設定項目に値を設定するか、 matchStrings正規表現に名前付きキャプチャグループを用いることで設定できます。

Capture Group Config Field 必須 説明
currentValue なし 必須 現在のバージョン。Renovateによって更新される。
depName depNameTemplate 必須 依存関係の名前。
packageName packageNameTemplate オプション パッケージ名。省略した場合 depName と同じになる。
datasource datasourceTemplate 必須 依存関係の参照先( Datasource )。
depType depTypeTemplate オプション 依存関係の種類。
versioning versioningTemplate オプション 依存関係のバージョン表記( Versioning )。デフォルトは semver
extractVersion extractVersionTemplate オプション キャプチャしたバージョンと Datasource のバージョンの表記が異なる場合に使用する。詳細は extractVersion を参照。
currentDigest なし オプション 現在のDigest。Renovateによって更新される。
registryUrl registryUrlTemplate オプション 依存関係の参照先のURL。詳細は registryUrls を参照。
"packageRules": [
    {
        "matchPackageNames": [
            "kubernetes/autoscaler"
        ],
        "matchDatasources": [
            "github-tags",
            "github-releases"
        ],
        "versioning": "regex:^cluster-autoscaler-(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)?$",
        "allowedVersions": "< 1.23.0"
    },
    {
        "matchPackageNames": [
            "k8s.gcr.io/autoscaling/cluster-autoscaler"
        ],
        "matchDatasources": [
            "docker"
        ],
        "allowedVersions": "< 1.23.0"
    }
]

packageRules には特定の依存関係の更新における追加ルールを設定します。 今回は Cluster Autoscaler に関する以下の追加ルールを設定します。

実行結果

設定に従いGitHub Actionsが毎朝9時にRenovateを実行します。

Helm Chartに更新がある場合は以下のようなPRが自動的に作成されます。

Kustomizeが参照するマニフェストのURLやイメージタグに更新がある場合は以下のようなPRが自動的に作成されます。

PRの説明文には公式GitHubリポジトリのReleaseからRelease Notesが転記されており、具体的な変更内容を知ることができます。

また Dependency Dashboard を有効にしておくと、Renovateが作成したPRを一覧して管理できるIssueが作成されます。 Dependency Dashboardを有効化する設定はPreset config:base に含まれています。

あとはこれらのPRをマージし、Argo CDやFluxを使ってKubernetesクラスタへデプロイするだけで、Kubernetesエコシステムの更新は完了です。

なお今回は全てのPRの承認・マージを手動で行う形にしましたが、特定の更新のみ承認・マージを必要にしてそれ以外はPRを自動的にマージするよう設定することも可能です。 特定の更新として、例えば特定のパッケージやメジャーバージョンの更新などが設定できます。

おわりに

本記事ではRenovateを使ったKubernetesエコシステムの自動バージョンアップを紹介しました。

開発が盛んなKubernetesエコシステムでは、頻繁に新機能やセキュリティ改善が実装され、新しいバージョンとしてリリースされます。 Renovateを活用し、Kubernetesエコシステムを頻繁かつ自動的にバージョンアップすることで、それらの恩恵を最大限に享受できます。

本記事が少しでもKubernetesクラスタを運用している方のお役に立てば幸いです。


私たちは同じチームで働いてくれる仲間を探しています。クラウドアーキテクトの業務に興味がある方のご応募をお待ちしています。

執筆:@shibata.takao、レビュー:@yamashita.tsuyoshiShodoで執筆されました

Storybook6(CSF)の基本実装

こんにちは。ISID コミュニケーションIT事業部 瀧川亮弘(あきひろ)です。
プロジェクトにてStorybookを利用しています。
Storybookの概要と基本的な実装方法をお伝えできればと思います。

Storybookとは?

一言でいうとUIのカタログです。また、それを作成するためのライブラリです。
Storybookを利用することで、GUI上でUIコンポーネントのデザインや振る舞いを簡単に確認できます。

百聞は一見にしかずということで、普段大変お世話になっているVSCodeStorybookのリンクを貼っておきます。良かったら触ってみてください!
Microsoft VSCode Webview UI Toolkit

Storybookの実装

UIコンポーネントStorybook上で表示するために.stories.jsという拡張子のファイルを作成する必要があります。
本章では、こちらのファイルの基本的な実装について記載します。

尚、いくつか存在する記法のうち、CSF(Component Story Format)という記法を採用します。以前まで主流だった、storiesOf関数やadd関数を利用するstoriesOf APIは現在非推奨となっています。

余談ですが、筆者はstoriesOf APIで記載された数十のファイルをCSFに書き換える作業をしたことがあります。個々の作業は簡単でしたが、数が数だけにとても苦労した懐かしい思い出があります。

対象のコンポーネント

対象とするコンポーネントとして、簡単なボタンを用意しました。
愛着が湧くように自身の名前をつけてAkiButtonと名付けました。
Vue3script setup構文を利用しています。

<template>
  <button class="button" :class="[getColor]" @click="handleClick">
    <slot />
  </button>
</template>

<script setup lang="ts">
import { computed } from "vue";
interface Props {
  /**
   * 色
   */
  color?: "basic" | "primary" | "error";
}
const props = withDefaults(defineProps<Props>(), {
  color: "basic",
});

interface Emits {
  /**
   * クリックイベント
   */
  (e: "click"): void;
}
const emit = defineEmits<Emits>();
const handleClick = () => {
  emit("click");
};

const getColor = computed<string>(() => `button__color--${props.color}`);
</script>

<style lang="scss" scoped>
.button {
  height: 30px;
  padding: 0 16px;
  cursor: pointer;

  &__color {
    &--basic {
      background-color: $color__primary-light;
      border-color: $color__primary-light;
      color: $color__primary;
    }
    &--primary {
      background-color: $color__primary;
      border-color: $color__primary;
      color: $color__white;
    }
    &--error {
      background-color: $color__error;
      border-color: $color__error;
      color: $color__white;
    }
  }
}
</style>

実装

完成形

まずは完成形をお見せします。
次章から順を追って実装を確認します。

import AkiButton from "./AkiButton.vue";
export default {
  title: "Atoms/AkiButton",
  component: AkiButton,
  argTypes: {
    color: {
      control: {
        type: "select",
        options: ["basic", "primary", "error"],
      },
    },
    click: {
      action: "click",
    },
  },
};

const Template = (args, { argTypes }) => ({
  components: { AkiButton },
  setup() {
    return {
      ...args,
    };
  },
  template: `
    <AkiButton :color="color" @click="click">あきボタン</AkiButton>
  `,
});

export const Basic = Template.bind({});
Basic.args = {
  color: "basic",
};

export const Primary = Template.bind({});
Primary.args = {
  color: "primary",
};

export const Error = Template.bind({});
Error.args = {
  color: "error",
};

メタデータの定義

メタデータを定義します。
titleには、メニューに表示される名前を定義します。/(スラッシュ)で階層を切ると、GUI上ツリー表示されます。ちなみに、現在のプロジェクトではAtomic Designに基づいて階層を切っています。

componentには、対象のコンポーネントで用意したコンポーネントを指定しています。

これらを指定したオブジェクトをデフォルトエクスポートすることでメタデータとして認識してくれます。

import AkiButton from "./AkiButton.vue";
export default {
  title: "Atoms/AkiButton",
  component: AkiButton,
};

テンプレートの定義

テンプレートを定義します。
次章でいくつかのストーリーを定義しますが、それらのストーリーが共通して利用するテンプレートとなります。各ストーリーで利用する関数なので、テンプレート自体のexportは必要ありません。

テンプレートの関数は、引数として各ストーリーごとに定義されたプロパティを受け取ります。ここではargsという名前で受け取ったプロパティを対象コンポーネントに渡しています。

const Template = (args, { argTypes }) => ({
  components: { AkiButton },
  setup() {
    return {
      ...args,
    };
  },
  template: `
    <AkiButton :color="color" @click="click">あきボタン</AkiButton>
  `,
});

ストーリーの定義

Storybookのメインであるストーリーを定義します。
コンポーネントを表示するパターン、テストするパターンなどを考慮してストーリーを表現します。ここでは、colorプロパティの値ごとに、ストーリーを作成しています。
テンプレートの定義で用意したテンプレートの関数をもとにいくつかのストーリーを作成するのが便利です。

ストーリーは関数で表現し、名前付きエクスポートします。
ここでつけた名前がデフォルトではストーリー名となります。

export const Basic = Template.bind({});
Basic.args = {
  color: "basic",
};

export const Primary = Template.bind({});
Primary.args = {
  color: "primary",
};

export const Error = Template.bind({});
Error.args = {
  color: "error",
};

アドオンを利用する

アドオンとはStorybookの拡張モジュールのことです。
今回はStorybookが公式でサポートしている3つのアドオンを利用してみます。

Controlsアドオン

Controlsアドオンにより、コンポーネントに対するプロパティをGUI上で動的に切り替えることができます。

  argTypes: {
    color: {
      control: {
        type: "select",
        options: ["basic", "primary", "error"],
      },
    },
  },

Actionsアドオン

Actionsアドオンにより、発火されたイベントをロギングします。
各イベントが発火するタイミングやイベントの内容を確認できます。

  argTypes: {
    click: {
      action: "click",
    },
  },

Docsアドオン

Docsアドオンは、ソースコードを解析しドキュメントを自動生成してくれます。
Vueファイルに記載したコメントがドキュメントに反映されていることがわかります。

  /**
   * 色
   */
  color?: "basic" | "primary" | "error";

storybook実装方法は以上です。
必要に応じて、アドオンを追加するとよいでしょう。

SDD(Storybook Driven Development)のすゝめ

Storybookとは?ではStorybookをUIカタログとご紹介しましたが、StorybookをUIの開発環境と捉えることもできます。

UIコンポーネントをアプリケーションから切り離された環境(Storybook上)で開発することには以下のようなメリットがあると思います。

皆さんもよければ、Storybook上で開発してみてください。

最後に

いかがでしたか?
Storybookって面白いなと思っていただけたら嬉しいです!

では、よいStorybook生活を!

執筆:@takigawa.akihiro、レビュー:@shibata.takaoShodoで執筆されました

AWS Certified Security - Specialty(SCS-C01)の合格体験記

Xイノベーション本部 ソフトウェアデザインセンター セキュリティグループの福山です。
AWSセキュリティの認定資格である「AWS Certified Security - Specialty(SCS-C01)」を受験し、合格しました。
この資格を取得することで、AWSクラウドにおけるデータやワークロードのセキュリティ確保に関する
専門知識を保有することが認定されます。
今後受験する方のお役に立てればと思い、取り組んだことを共有します。

受験するきっかけ


私は2022年3月にISIDに入社しました。全社横断的にセキュリティを推進していくチームで、
クラウド環境のSOC、IRという役割をいただいています。
まずは当資格を取得することで、AWSのセキュリティを網羅的に理解することを目標としました。

前提知識


  • 前職はインフラエンジニアで、AWSのインフラ構築・保守の経験が2~3年
  • AWS SAAは取得済み(1回目は不合格、2回目は732点で何とか合格)
  • 現在は業務で Security Hub、GuardDuty等のセキュリティ系サービスを頻繁に触っている

勉強方法


勉強期間は2ヶ月程度を要しました。以下に勉強の流れを記載します。

➀試験内容を把握する

②各AWSサービスとベストプラクティスを理解する

  • 参考書を購入する
     ⇒要点整理から攻略する『AWS認定 セキュリティ-専門知識』
     タイトルの通り、要点が整理されて、非常にわかりやすい内容になっております。
  • IAMとKMSのBlackbeltを閲覧する
     上記2サービスは頻出するとの情報が散見されるため、BlackBeltを閲覧しました。
     ⇒BlackBelt
  • 主なサービスをおさえる
     各サービスの概要、他サービスとの関連性、ベストプラクティスをおさえます。

    ■分野1:インシデントへの対応
    ・Security Hub、Config
    ・GuardDuty、Detective
    Amazon Macie
    ■分野2:ログ記録とモニタリング
    ・CloudWatch Events、SNS
    ・S3、CloudTrail
    KinesisOpenSearch Service
    ■分野3:インフラストラクチャのセキュリティ
    ・SG、NACL
    AWS WAF、Shield、CloudFront
    AWS Artifact
    ■分野4:アイデンティティ管理とアクセス管理
    ・IAM(頻出)
    ・Organizations
    ■分野5:データ保護
    ・KMS(頻出)
    AWS Config
    ・Secrets Manager、Parameter Store

 また、以下ドキュメントには、AWSの定めるSecurity Hubのセキュリティ基準が記載されており参考になります。
  ⇒AWS Foundational Security Best Practices

③試験を申し込む

  • 以下サイトからログインの上、試験を申し込む  試験の1ヶ月前に申し込みました。
     なお、ISIDの社内制度で、受験料は補償されました。
     ⇒試験のスケジュールを立てる

④問題を解く

  • Udemyを受講する
     以下のコースを受講しました。ISIDには、Udemy Businessを利用できる社内サービスがあるため、
     無償で受講することができました。
     なお、こちらのコースはボリュームが多いため、まずは各章の練習問題を実施し、
     間違えた箇所は本編を視聴して消化するスタイルで進めました。
     ⇒AWS Certified Security – Specialty SCS-C01
  • AWS公式の模擬試験を受ける
     こちら無償になっています(要ログイン)。
     ⇒AWS Certified Security - Specialty Official Practice Question Set (SCS-C01 - Japanese)
  • 時間の許す限り、①②③(AWS公式トレーニング、サンプル問題、参考書、Udemy、模擬試験)の練習問題を繰り返す

試験を終えて


結果は767点と合格ラインギリギリでした。。
点数を落とした要因は、IAMポリシーやKMSキーポリシーの記述方法を問われる問題が頻出し、
自信を持って解けなかったことが大きかったと思われます。
また、クロスアカウントやサービスへのアクセスに必要な記述内容なども理解しておくと良いと思いました。
次回更新時は上記を重点的に学習したいと思います。
最後に、参考書やブログで散見される、IAMユーザーにMFAを強制するIAMポリシーの記述方法を紹介して、
締めとさせていただきます。 それでは、行ってらっしゃい!!

{
   "Sid": "DenyAllExceptListedIfNoMFA",
   "Effect": "Deny",
   "NotAction": [
      "iam:CreateVirtualMFADevice",
      "iam:DeleteVirtualMFADevice",
      "iam:EnableMFADevice",
      "iam:GetUser",
      "iam:ListMFADevices",
      "iam:ListVirtualMFADevices",
      "iam:ResyncMFADevice",
      "sts:GetSessionToken"
   ],
   "Resource": "*",
   "Condition": {
      "BoolIfExists": {
         "aws:MultiFactorAuthPresent": "false"
      }
   }
}

参考:MFAで認証されたIAM ユーザーが「My Security Credentials」ページで自分の MFA デバイスを管理できるようにする
   IAMユーザにMFA設定を強制するにあたりiam:ListUsersが必須では無くなった話


私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募お待ちしています。
- セキュリティエンジニア(セキュリティ設計)

執筆:@fuku.dancho、レビュー:@sato.taichiShodoで執筆されました