EC2 Image Builderから生まれたAMIをSlackに通知してみた。

EC2 Image Builderから生まれたAMIをSlackに通知してみた。

Clock Icon2025.05.14

はじめに

皆様こんにちは、あかいけです。
突然ですが、EC2 Image Builder を使っていますでしょうか?

EC2 Image BuilderはAMIおよびコンテナイメージのビルドはもちろん、独自のカスタマイズやデプロイ、はたまたライフサイクルポリシーによる世代管理まで…。
自前で実装したら大変な処理を、パイプラインとして一元化して管理できるとても便利なサービスです。
また先日SSMパラメータストアとの統合がサポートされ、その便利さにますます磨きをかけています。

https://dev.classmethod.jp/articles/ec2-image-builder-ssm-parameter-store-integration

しかし私は気づいてしまいました、
日々生まれては消えていくAMIたちへの感謝ができていないなと…。

というわけで今回は、
EC2 Image Builderから生まれたAMIをSlackに通知してみました。

なおそもそも「EC2 Image Builderってなんやねん…。」という方は以下の記事をご参照ください。

https://dev.classmethod.jp/articles/introduction-2024-imagebuilder/

構成

大まかな構成は以下の通りです。

image.drawio

EC2 Image Builderのインフラストラクチャ設定にて、
通知先のSNS トピックを指定して、その後Lambdaへ連携して加工したメッセージをSlack Webhook URL(Parameters Storeに格納)を使ってSlackへ通知しています。

事前準備

作成方法

今回はTerraformで用意しました、以下の流れでデプロイできます。

リポジトリをcloneします。

git clone https://github.com/Lamaglama39/ami-birth-notify.git;

クレデンシャル情報を設定するため、terraform.tfvarsを作成します。
slack_webhook_urlは取得したURL、
slack_channelは送信先のチャネル名に置き換えてください。

cat <<EOF >> ./terraform/terraform.tfvars
slack_webhook_url = "https://hooks.slack.com/services/XXX/XXX/XXX" # 取得したSlack Webhook URL
slack_channel     = "#channel-name"                                # 通知先Slack Channel
app_name          = "ami-slack-notification"                       # アプリ名
aws_region        = "ap-northeast-1"                               # 作成リージョン
EOF

あとはapplyするだけです。

cd ./terraform;
terraform init;
terraform apply;

リソースの作成完了後、
outputに出力される以下のコマンドを実行するか、マネジメントコンソールからパイプラインを手動実行してください。

terraform output;
aws imagebuilder start-image-pipeline-execution --image-pipeline-arn ${aws_imagebuilder_image_pipeline.example.arn}

イメージの作成完了するとSlackに通知されます。
新しいAMIが生まれました、かわいいね。

notify-image

ポイント

いくつかポイントとなる箇所を説明します。

パイプライン実行タイミング

aws_imagebuilder_image_recipe.parent_imageにて、AWS の提供するイメージを指定しています。
またバージョンの指定をx.x.xとすることで、パイプライン実行時に最新バージョンのAMIが指定されるようになります。

https://docs.aws.amazon.com/ja_jp/imagebuilder/latest/userguide/ibhow-semantic-versioning.html

image-builder.tf
resource "aws_imagebuilder_image_recipe" "example" {
  name         = "${var.app_name}-recipe"
  version      = "1.0.0"
  parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2023-x86/x.x.x"
  component {
    component_arn = aws_imagebuilder_component.example.arn
  }
  block_device_mapping {
    device_name = "/dev/xvda"
    ebs {
      volume_size = 8
      volume_type = "gp3"
    }
  }
}

次にaws_imagebuilder_image_pipeline.pipeline_execution_start_conditionにて、
EXPRESSION_MATCH_AND_DEPENDENCY_UPDATES_AVAILABLEを指定します。
こうすることで、参照しているベースイメージ、およびコンポーネントが更新されている場合のみパイプラインが実行されるようになります。

image-builder.tf
resource "aws_imagebuilder_image_pipeline" "example" {
  name                             = "${var.app_name}-pipeline"
  image_recipe_arn                 = aws_imagebuilder_image_recipe.example.arn
  infrastructure_configuration_arn = aws_imagebuilder_infrastructure_configuration.example.arn

  schedule {
    schedule_expression                = "cron(* * * * ? *)"
    pipeline_execution_start_condition = "EXPRESSION_MATCH_AND_DEPENDENCY_UPDATES_AVAILABLE"
  }
  image_tests_configuration {
    image_tests_enabled = false
  }
  status = "ENABLED"
}

メッセージの加工

メッセージの加工はLambdaで行っております。

EC2 Image BuilderからSNSトピックに通知されるメッセージ形式は以下の通りですので、
通知メッセージを変更したい場合はこちらをご参照の上カスタマイズしてください。

https://docs.aws.amazon.com/ja_jp/imagebuilder/latest/userguide/integ-sns.html#integ-sns-message

index.js

const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm');
const https = require('https');
const url = require('url');

exports.handler = async (event) => {
    console.log('Event:', JSON.stringify(event, null, 2));

    // SNSイベントをパース
    let imageBuilderEvent = event;
    if (event.Records && event.Records[0] && event.Records[0].Sns) {
        try {
            imageBuilderEvent = JSON.parse(event.Records[0].Sns.Message);
        } catch (error) {
            console.error('Error parsing SNS message:', error);
        }
    }

    // パラメータストアからSlackのWebhook URLとチャンネルを取得
    const ssmClient = new SSMClient();
    const webhookUrlParam = await ssmClient.send(
        new GetParameterCommand({
            Name: process.env.SLACK_WEBHOOK_PARAM_NAME,
            WithDecryption: true
        })
    );
    const slackChannelParam = await ssmClient.send(
        new GetParameterCommand({
            Name: process.env.SLACK_CHANNEL_PARAM_NAME,
            WithDecryption: true
        })
    );

    const webhookUrl = webhookUrlParam.Parameter.Value;
    const slackChannel = slackChannelParam.Parameter.Value;

    // イベントからイメージの詳細を抽出
    const imageArn = imageBuilderEvent.arn || 'N/A';
    const imageState = imageBuilderEvent.state?.status || 'N/A';
    const imageId = imageBuilderEvent.outputResources?.amis?.[0]?.image || 'N/A';
    const imageName = imageBuilderEvent.name || 'N/A';
    const recipeVersion = imageBuilderEvent.version || 'N/A';
    const buildVersion = imageBuilderEvent.buildVersion || 'N/A';
    const osVersion = imageBuilderEvent.osVersion || 'N/A';
    const region = imageBuilderEvent.outputResources?.amis?.[0]?.region || 'N/A';
    const recipeName = imageBuilderEvent.imageRecipe?.name || 'N/A';
    const parentImage = imageBuilderEvent.imageRecipe?.parentImage || 'N/A';

    // Slackメッセージを作成
    const message = {
        channel: slackChannel,
        username: 'AWS Image Builder',
        icon_emoji: ':aws:',
        attachments: [{
            color: '#36a64f',
            title: '👶 New AMI is Born 👶',
            fields: [
                {
                    title: 'Image Name',
                    value: imageName,
                    short: true
                },
                {
                    title: 'Image Version',
                    value: `${recipeVersion}/${buildVersion}`,
                    short: true
                },
                {
                    title: 'AMI ID',
                    value: imageId,
                    short: true
                },
                {
                    title: 'Status',
                    value: imageState,
                    short: true
                },
                {
                    title: 'OS Version',
                    value: osVersion,
                    short: true
                },
                {
                    title: 'Region',
                    value: region,
                    short: true
                },
                {
                    title: 'Recipe',
                    value: recipeName,
                    short: true
                },
                {
                    title: 'Parent Image',
                    value: parentImage,
                    short: true
                }
            ],
            footer: 'AWS Image Builder',
            ts: Math.floor(Date.now() / 1000)
        }]
    };

    // Slackにメッセージを送信
    try {
        await sendSlackMessage(webhookUrl, message);
        return { statusCode: 200, body: 'Notification sent to Slack' };
    } catch (error) {
        console.error('Error sending Slack message:', error);
        throw error;
    }
};

function sendSlackMessage(webhookUrl, message) {
    return new Promise((resolve, reject) => {
        const parsedUrl = url.parse(webhookUrl);
        const options = {
            hostname: parsedUrl.hostname,
            path: parsedUrl.path,
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            }
        };

        const req = https.request(options, (res) => {
            let responseBody = '';
            res.on('data', (chunk) => {
                responseBody += chunk;
            });

            res.on('end', () => {
                if (res.statusCode < 200 || res.statusCode >= 300) {
                    reject(new Error(`Status Code: ${res.statusCode} ${responseBody}`));
                } else {
                    resolve(responseBody);
                }
            });
        });

        req.on('error', (error) => {
            reject(error);
        });

        req.write(JSON.stringify(message));
        req.end();
    });
}

さいごに

以上、EC2 Image Builderから生まれたAMIをSlackに通知する方法でした。
無事に生まれてきてくれたAMIへの感謝を忘れないようにしましょう。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.