おぎろぐはてブロ

なんだかんだエンジニアになって10年以上

Oktaのアプリケーション割り当て失敗をAmazon EventBridgeでSlackに通知する

Oktaのアプリケーションの割り当て処理が失敗したときにSlack通知を行い気付きやすくしました。 Okta logをAmazon EventBridgeにストリーミングし、EventBridgeから直接Slackにメッセージを送ります。

課題

Oktaでアプリケーションをアサインした後、OktaからSaaSに対してライセンス割り当ての処理が行われますが、SaaS側の問題で失敗している時があります。 アサインして即時エラーが出るわけではないのですぐに気づけないことがあります。 タスクには一覧表示されるけど常に見ているわけもなく。

失敗する理由としては、SaaS側でライセンスが不足している場合だったり、Microsoftの仕様でTeamsの試用版をセルフで開始している場合などがあります。

続きを読む

Windows 365でAndroid StudioのAndroid Emulatorを使えるようにIntuneで配布する

Windows 365のCloud PCの検証を進めていますが、業務として、アプリの検証のためAndroid Studioをインストールして、Android Emulatorを起動したいというものがありました。 ハマったので、まとめておきます。

tl;dr

  • Android Emulatorを起動するためには、Hyper-Vの有効化が必要
  • Hyper-Vを有効にするには4 vCPU以上が必要

普通にインストールして動かす

Windows 365のCloud PC上にAndroid Studioをインストールして、イメージをダウンロードしてきました。 これで▶️を押せば普通は起動するのですが、

Virtual Device

Install Android Emulator hypervisor driverとエラーが出て、OKを押すとインストールに進みます。

AEHDをインストールしろと言われる

インストール画面

なんだけど、StartService FAILED with error 4294967201というエラーでサービスの開始に失敗する。

StartService FAILED with error 4294967201

AEHDは、GitHubから別途インストールできるのですが、インストールしても、Install Android Emulator hypervisor driverのエラーは出てくるのが謎

github.com

Hyper-Vを有効にする

StartService FAILED with error 4294967201のエラーで調べてみると、Hyper-V云々の話が出てきたので、有効にしてみます。 Windows 365は仮想PCなので、nested virtualizationということになります。

ここに 4vCPU 以上のクラウド と書かれているので、Cloud PCのスペックを4vCPUに変更しました。

learn.microsoft.com

そして、管理者権限のPowershellでコマンドを実行します。

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All

learn.microsoft.com

再起動してHyper-Vが有効になります。

もう一度試す

Hyper-Vが有効になったCloud PCでAndroid Emulatorを起動します。 だいぶ待たされるけど無事Virtual Deviceが起動しました。やったね。

Virtual Deviceが起動する

ちなみにAEHDを削除してもAEHD云々の警告は出ず、起動します。 (Android Emulator hypervisor driver is a hypervisor to accelerate Android Emulator ということで、パフォーマンスに影響はあるかもしれない)

ということで、これをAppとして配布するように準備します。

Hyper-Vの有効化アプリ

必要な人だけ有効にするよう、アプリとして配布します。

こちらの記事を参考にさせてもらいました。

endpointcave.com

いつもの Microsoft Win32 Content Prep Tool でintunewinファイルを作成します。 以下のファイルを含めます。

enable.ps1

#Check if Hyper-V is enabled and enable it if necessary
if((Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Hypervisor).State -eq "Disabled")
{
    Write-host "Enabling Microsoft Hyper-V...."
    Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All -NoRestart
}
else
{
    Write-host "Microsoft Hyper-V was already succesfully installed"
}

disable.ps1

Disable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -NoRestart

Intune管理センターのアプリからWindowsのアプリを Windowsアプリ (Win32) で作成して intunewinファイルをアップロードします。

アプリ情報

プログラムの画面では以下を設定します。

  • インストール コマンド: Powershell.exe -NoProfile -ExecutionPolicy ByPass -File .\enable.ps1
  • アンインストールコマンド: Powershell.exe -NoProfile -ExecutionPolicy ByPass -File .\disable.ps1
  • デバイスの再起動: Intuneによってデバイスの必須の再起動が強制実行されるようにする

プログラム

必要条件では、必要な論理プロセッサの最小数を4に設定します。

必要条件

検出規則では、カスタム検出スクリプトを使用するようにし、以下のスクリプトをアップロードします。

$exit_code = -1

if((Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Hypervisor).State -eq "Enabled")
{
    $exit_code = 0
}
else
{
    $exit_code = 1
}

Write-Output "Exit Code: $exit_code"
exit $exit_code

検出規則

Android Studioの配布

Android StudioのexeファイルをWin32 Content Prep Toolでintunewinファイルに変換して、粛々と設定する流れですが、

  • プログラム
    • インストール コマンド: android-studio-2024.1.1.11-windows.exe /S
    • アンインストール コマンド: "C:\Program Files\Android\Android Studio\uninstall.exe" /S
  • 検出ルール
    • 検出規則: ファイル C:\Program Files\Android\Android Studio\bin

と、依存関係として、先に作成したHyper-V有効化のアプリの自動インストールを設定してください。

AWS SAM + Slack Bolt for PythonでSlack botをつくる

書いたこと

  • Lazy Listenerを利用し、ackが必要なイベントかつ3秒以上時間がかかる処理を実現する
  • Lambda Function URLを利用してHTTP Endpointをつくる

Slack Bolt for PythonでSlack botをつくる

このチュートリアルを参照しつつ、つくります。割愛。

slack.dev

ある程度、チュートリアルに沿って、ローカルでSocket Mode有効でつくってから、Lambdaで動かすように次のLazy Listernerを有効にするように自分はしています。

Lazy Listenerの利用

Boltで受けられるイベントは各種ありますが、アクション(action)、コマンド(command)、ショートカット(shortcut)、オプション(options)、およびモーダルからのデータ送信(view_submission)を処理する場合は、Slack側からのリクエストに対して3秒以内に ack() を返す必要があります。 ただ、Lambdaで動かす場合、HTTPレスポンスを返却したタイミングでプロセスが終了されるため、ackを返したあとに処理を継続させることができません。この対応として、Slack Bolt for Pythonでは、Lazy Listenerという機能が提供されています。 これは、HTTPでSlackからリクエストを受け取ったLambdaプロセスが、時間のかかる処理を実行するLambdaを非同期で呼び出して、ackレスポンスを返却することで実現されています。

例にあるように、ack と、 lazy の引数にそれぞれack処理と、時間のかかる処理を分けて書けます。lazyには複数の関数を渡せ、それぞれ並列に実行されます。 lazyに1つ処理を指定した場合、1つのイベントに対してack処理とlazy処理でLambdaが2度invokeされることになります。上に書いてあるackが必要なイベント以外をハンドルする場合はackが不要なので、その場合Lazy Listenerを使うと無駄にinvokeの回数が増えるので、この場合はLazy Listenerを有効にしないほうがよいです。

以下は、行頭にHelloもしくはhelloという文字列があったときにHello!を発言する場合です。 (messageイベントはackが不要なのでlazyを使う必要はないです)

import logging
import re

from slack_bolt import App, Say
from slack_bolt.adapter.aws_lambda import SlackRequestHandler

SlackRequestHandler.clear_all_log_handlers()
logging.basicConfig(level=logging.INFO)

app = App(process_before_response=True)


def handle_message(message, say: Say):
    print(message)

    say("Hello!")


rule = re.compile("^[Hh]ello")

app.message(rule)(
    ack=lambda ack: ack(),
    lazy=[handle_message]
)


def handler(event, context):
    print("invoked", event)
    slack_handler = SlackRequestHandler(app=app)
    return slack_handler.handle(event, context)

とにかくackだけ返したければ

ack=lambda ack: ack(),

と書けばOK

呼び出しをCloudWatch Metricsにputする

Slack botを作った場合、Slackからの呼び出しは同じLambda Functionが呼び出されるだけなので、複数のイベントをハンドリングする場合、どのイベントがどのくらい呼び出されているのかを調べるのが困難です。このときCloudWatch metricsにメトリックを記録すると便利です。 以下のように定義して、handle_shortcut ではackしつつmodalを開いたりし、do_something では時間のかかる処理を行い、step_handle_shortcut ではCloudWatch metricsにメトリックを記録します。 do_something と step_handle_shortcut は並列にinvokeされます。

app.shortcut("shortcut")(
    ack=handle_shortcut,
    lazy=[do_something, metric.step_handle_shortcut]
)

metric.py では

import boto3
import os
import datetime


def put(step: str):
    client = boto3.client("cloudwatch")

    env = os.getenv("ENV") # 別途定義する

    response = client.put_metric_data(
        Namespace=f"FooBarSlackApp/{env}",
        MetricData=[
            {
                "MetricName": "Request",
                "Dimensions": [
                    {
                        "Name": "Step",
                        "Value": step
                    },
                ],
                "Timestamp": datetime.datetime.utcnow(),
                "Value": 1,
            },
        ]
    )

    print(response)


def step_handle_shortcut():
    put("handle_shortcut")

SAMでデプロイ

最低限必要なのは Lambda Function と、HTTP EndpointとしてのLambda Function URLです。 HTTP EndpointとしてAPI Gateway (v1, v2) を利用している例があったりしますが、特に高度な定義も必要なければ、Lambda Function URLを使用するほうが、定義が簡単で、かつ、利用料金もLambda側に含まれるので安価になります。(大抵はまぁ誤差みたいな金額だと思いますが)

src/ 配下にコードを配置した場合、以下のようなコードで実現できます。

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Slack bot sample

Resources:
  HandlerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/
      Handler: app.handler
      Runtime: python3.11
      Timeout: 30
      Policies:
        - AWSLambdaRole
      FunctionUrlConfig:
        AuthType: NONE

Outputs:
  HandlerFunctionUrl:
    Value: !GetAtt HandlerFunctionUrl.FunctionUrl

FunctionUrlConfig を定義してやると、Lambda Function URLが有効になります。SlackからのリクエストにIAM認証を利用することはできないので、AuthType: NONE になります。(bolt内でSlackの特定のアプリから来た正当なリクエストか signing_secret を利用して検証しています)

Slack appの Interactivity eventsのURL、Slash commandのURL、Event SubscriptionsのURL (それぞれ有効にするかは使うイベント次第) にこのLambda Function URLのURLを指定してやる必要があるので、Outputs で出力をしています。 SAMでリソースが勝手に作られて、その名前が Lambda Function名に Url を足したものになるので、Lambda Functionが HandlerFunction の場合、 HandlerFunctionUrl になります。

AWS CLIで存在するNode.js 10.xのLambda関数を一覧する

[要対応] AWS Lambda における Node.js 10 のサポート終了 | [Action Required] AWS Lambda end of support for Node.js 10 のメール来てるけど、どこにあるんだとおもったら

tl;dr

以下を実行

REGIONS=`aws ec2 describe-regions --query 'Regions[*].RegionName' --output text`
for region in $REGIONS; do
  aws lambda list-functions \
    --function-version ALL \
    --region $region \
    --query "Functions[?Runtime=='nodejs10.x']"
    --no-cli-pager
done
  • region一覧とって回そう
  • --function-version ALL で LATEST 以外のバージョンも取得する
  • 見つかったら、CloudWatch MetricでInvocationされているかを確認して、使われてなかったら削除しよう

AWS Configでリソースに紐付かないセキュリティグループを抽出

tl;dr

  • 使っていないセキュリティグループを列挙したい
  • セキュリティグループはENIに対して割り当て、ENIがEC2などのリソースにアタッチされている。使っていないセキュリティグループ = どのENIにも割り当てられていないセキュリティグループ、なのだが、取得するのはセキュリティグループを列挙し、ENIを列挙して、という作業が必要でEC2 API叩いて処理するの地味に面倒
  • AWS Configを有効にしているなら、クエリでいい感じにとれるし、リージョンもまたげるし、Aggregatorを使っているならマルチアカウントも一発

コード

import json

import boto3

client = boto3.client('config')

query = "SELECT resourceId, awsRegion, resourceName, " \
        "configuration.description, configuration.ipPermissions, " \
        "configuration.ipPermissionsEgress, relationships " \
        "WHERE resourceType = 'AWS::EC2::SecurityGroup'"

results = []

response = client.select_resource_config(Expression=query, 
                                         Limit=100)
results.extend(response['Results'])

while True:
    if 'NextToken' in response:
        response = client.select_resource_config(Expression=query,
                                                 NextToken=response['NextToken'], 
                                                 Limit=100)
        results.extend(response['Results'])
    else:
        break

for result in results:
    result = json.loads(result)

    for relationship in result['relationships']:
        if relationship['resourceType'] == 'AWS::EC2::NetworkInterface':
            #eni_id = relationship['resourceId']
            break
    else:
        # 関連するENIが無い = リソースに関連付けられていないセキュリティグループ
        print('\t'.join(
            [result['awsRegion'], result['resourceId'], result['resourceName'],
             result['configuration']['description']]))

マルチアカウント版コード

select_aggregate_resource_config() に差し替えるだけで、AWS Config Aggregatorに対してクエリを実行できます。便利。

import json

import boto3

client = boto3.client('config')

query = "SELECT accountId, resourceId, awsRegion, resourceName, " \
        "configuration.description, configuration.ipPermissions, " \
        "configuration.ipPermissionsEgress, relationships " \
        "WHERE resourceType = 'AWS::EC2::SecurityGroup'"

results = []

response = client.select_aggregate_resource_config(Expression=query, Limit=100,
                                                   ConfigurationAggregatorName='<aggregator name>')
results.extend(response['Results'])

while True:
    if 'NextToken' in response:
        response = client.select_aggregate_resource_config(Expression=query, 
                                                           NextToken=response['NextToken'], 
                                                           Limit=100,
                                                           ConfigurationAggregatorName='<aggregator name>')
        results.extend(response['Results'])
    else:
        break

for result in results:
    result = json.loads(result)

    for relationship in result['relationships']:
        if relationship['resourceType'] == 'AWS::EC2::NetworkInterface':
            #eni_id = relationship['resourceId']
            break
    else:
        # 関連するENIが無い = リソースに関連付けられていないセキュリティグループ
        print('\t'.join(
            ['"' + result['accountId'] + '"', result['awsRegion'], result['resourceId'], 
             result['resourceName'], result['configuration']['description']]))