【マルチアカウント編】EC2 Image Builder で作られたイメージ(AMI)を自動的に SSMパラメータストアに格納する

2021.03.15

はじめに

EC2 Image Builder(以下 Image Builder)のパイプラインから EC2イメージ(AMI)を作成したときに、 そのAMI IDを Systems Manager(SSM)パラメータストアに自動的に格納するような構成を作っていきます。

前回は 1アカウント内で完結するケースを書きました。

今回は「マルチアカウント編」ということで、 イメージ配布(共有)先アカウントの SSMパラメータに格納する 構成を作ってみます。

図で表すと以下のとおりです。

img

  1. Image Builderパイプライン で AMI作成・配布時に SNSトピックを通知するように設定しておきます
  2. このSNSトピック通知をトリガーに Lambda関数を実行します
  3. このLambda関数で「各アカウントのSSMパラメータストア」に AMI IDの情報を登録( PutParameter )します

構成

大枠は 前回の1アカウント編のブログ と変わりません。 共通部分は簡単に説明します。

EC2 Image Builder

パイプラインの インフラストラクチャ設定 > SNS 部分を設定しておきます。

img

ディストリビューション設定 部分でイメージ共有の設定を行います。

img

このSNS設定/ディストリビューション設定により、AMI作成・配布が完了したときに、以下のようなメッセージを 通知することができます。

{
  "versionlessArn": "arn:aws:imagebuilder:ap-northeast-1:123456789012:image/sample-recipe",
  "semver": 1073741827,
  "arn": "arn:aws:imagebuilder:ap-northeast-1:123456789012:image/sample-recipe/0.0.1/3",
  "name": "sample-recipe",
  (略)
  "distributionConfiguration": {
    "arn": "arn:aws:imagebuilder:ap-northeast-1:123456789012:distribution-configuration/sample-al2-ami-xxxx",
    "name": "sample-al2-ami-xxxx",
    "dateCreated": "Mar 5, 2021 1:14:50 AM",
    "dateUpdated": "Mar 9, 2021 2:56:29 AM",
    "distributions": [
      {
        "region": "ap-northeast-1",
        "amiDistributionConfiguration": {
          "launchPermission": {
            "userIds": [
              "111111111111",
              "222222222222"
            ]
          }
        }
      }
    ],
    "tags": {
      "internalId": "6520c90f-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "resourceArn": "arn:aws:imagebuilder:ap-northeast-1:123456789012:distribution-configuration/sample-al2-ami-xxxx"
    },
    "accountId": "123456789012"
  },
  (略)
  "outputResources": {
    "amis": [
      {
        "region": "ap-northeast-1",
        "image": "ami-01xxxxxxxxxxxxxxx",
        "name": "sample-recipe 2021-03-05T01-16-05.489Z",
        "accountId": "123456789012"
      }
    ]
  },
  (略)
}

ハイライト部分 distributionConfiguration が配布設定です。後述の Lambda 関数で利用します。

IAMロール

img

上記のように IAMロールを 2種類 作成する必要があります。

  • ロールA
    • Lambdaの実行アカウントに作成する
    • AWSLambdaBasicExecutionRole (AWS管理ポリシー) を付与
    • ロールBに AssumeRole を行う権限を付与
  • ロールB
    • 配布先アカウントにそれぞれ作成する
    • ロールAが AssumeRole できるようする(信頼ポリシー)
    • SSMパラメータを作成/更新する権限を付与

今回のブログではロールBの名前を LambdaExecutionRoleForSSMParam としています。

Lambda 関数

ランタイム: Python3.7 で作成しています。

以下のようなコードを作成しました。

import json
import boto3
from boto3.session import Session
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

REGION = "ap-northeast-1"
ROLE_NAME = "LambdaExecutionRoleForSSMParam"

sts = boto3.client('sts')


def lambda_handler(event, context):
    message = json.loads(event["Records"][0]["Sns"]["Message"])
    process_sns_message(message)


def process_sns_message(message):
    logger.info("Printing message: {}".format(message))
    # state キーが無いとき、もしくは state.status が "AVAILABLE" でないときはパラメータストアに登録しない
    if message.get('state') == None or message['state'].get('status') != "AVAILABLE":
        return None

    # レシピ名, AMI ID, 配布情報を取得
    recipe_name = message['name']
    ami = message['outputResources']['amis'][0]
    dists = message['distributionConfiguration']['distributions']
    logger.info("recipe_name={}".format(recipe_name))
    logger.info("ami={}".format(ami))
    logger.info("dists={}".format(dists))

    # 配布先アカウントの SSMパラメータに登録
    for dist in filter(lambda d: d.get('region') == REGION, dists):
        for user_id in dist['amiDistributionConfiguration']['launchPermission']['userIds']:
            logger.info(
                "executing ssm:PutParameters to user:{}".format(user_id))
            put_param(recipe_name, ami, user_id)


def put_param(recipe_name, ami, user_id):
    # 一時情報の取得
    remote = sts.assume_role(
        RoleArn="arn:aws:iam::{}:role/{}".format(user_id, ROLE_NAME),
        RoleSessionName="imagebuilder_master"
    )
    ssm = boto3.client(
        'ssm',
        aws_access_key_id=remote['Credentials']['AccessKeyId'],
        aws_secret_access_key=remote['Credentials']['SecretAccessKey'],
        aws_session_token=remote['Credentials']['SessionToken']
    )

    # SSMパラメータストアに登録
    response = ssm.put_parameter(
        Name="/ec2-imagebuilder/latest/{}".format(recipe_name),
        Description="Latest AMI ID:{}".format(recipe_name),
        Value=ami['image'],
        Type='String',
        Overwrite=True,
        Tier='Standard'
    )
    logger.info(
        "[put_param] ssm.put_parameter response: {}".format(response))

受け取った SNSメッセージをのステータス、AMI ID、レシピ名、配布情報を取得して、 配布先アカウントそれぞれに SSMパラメータ ( "/ec2-imagebuilder/latest/{レシピ名}" ) に登録する処理を行います。

▼ ほか設定: SNS

Image Builder のインフラストラクチャ設定で指定したSNSトピックをトリガーとします。

▼ ほか設定: IAMロール

前述の ロールA を付与します。 AssumeRole 部分のポリシーは以下のようになります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Resource": "arn:aws:iam::*:role/LambdaExecutionRoleForSSMParam",
            "Action": "sts:AssumeRole",
        }
    ]
}

構築

(事前準備) 配布先アカウントのIAMロール

前述の ロールB (LambdaExecutionRoleForSSMParam) を配布先アカウントそれぞれに作成します。 以下 CloudFormation(CFn)テンプレートを作成しました。

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  MainAccountId:
    Description: "Main AWS account ID to run Lambda function"
    Type: String
Resources:
  Role:
    Type: AWS::IAM::Role
    Properties: 
      RoleName: LambdaExecutionRoleForSSMParam
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              "AWS": !Sub "arn:aws:iam::${MainAccountId}:root"
      Policies: 
        - PolicyName: policy-for-ssm-action
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: SSMPutParameter
                Effect: "Allow"
                Action: "ssm:PutParameter"
                Resource: "*"

個別に展開、もしくは CFn StackSets 展開しておきます。

SAMで構築

SNSトピックとLambda関数(+ロールA)の部分AWS サーバーレスアプリケーションモデル (AWS SAM)を使って構築しました。 SAMのプロジェクト構成内容を記します。

▼ プロジェクト

sam init で新規プロジェクトを作成します。

sam init --runtime python3.7 --name tracking-latest-images-in-imagebuilder-cross-account

▼ template.yaml

必要なリソースを記述した template.yaml は以下のとおり。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: "tracking the latest AMI ID in EC2 Image Builder pipeline"
Resources:
  # SNS topic
  SnsTopic:
    Type: AWS::SNS::Topic
    Properties: 
      TopicName: topic-for-imagebuilder
  # Lambda function
  Function:
    Type: AWS::Serverless::Function
    Properties:
      Description: "Update SSM Parameter with the latest AMI ID"
      CodeUri: scripts/
      Handler: app.lambda_handler
      Runtime: python3.7
      Events:
        EventBridgeRule:
          Type: SNS
          Properties:
            Topic: !Ref SnsTopic
      Policies:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - Version: '2012-10-17'
          Statement:
            - Sid: STSAssumeRole
              Effect: "Allow"
              Action: "sts:AssumeRole"
              Resource: "arn:aws:iam::*:role/LambdaExecutionRoleForSSMParam"

▼ scripts/app.py

前述のLambda関数のコードを scripts/app.py に格納します。

▼ ビルド、デプロイ

sam build
sam deploy --guided

特にパラメータ指定していないので、ガイド通りに YES 選択でデプロイできます。

確認

事前に ImageBuilderのパイプラインの インフラストラクチャ設定 > SNS 部分 に、 作成した SNSトピックを指定しておきます。

パイプライン実行

Image Builder のパイプラインを実行します。 [使用可能] になるまで待ちます。

img

配布先アカウントの確認

まず、EC2の AMI画面を見てみます。Image Builder からのAMI共有を確認できました。

img

次に配布先のアカウントの SSMパラメータを見てみます。

img

AMI ID が登録されていること確認できました。

おわりに

EC2 Image Builder で作った最新AMIを「配布先アカウントの SSMパラメータストア」に 自動登録する仕組みを作ってみました。

マルチアカウント環境では AMIを他アカウントに共有する機会が多いと思います。 配布されたAMI および作成されたSSMパラメータを使って、 EC2インスタンスを作成する際の AMI ID 参照などに活用できると思います。

参考