LoginSignup
20
4

AWSのSSO導入において工夫したポイント5選

Last updated at Posted at 2023-12-07

※この記事は リンクアンドモチベーション Advent Calendar 2023 の9日目の記事です。

はじめに

こんにちは、リンクアンドモチベーションSREグループの久原です。
今回は念願だったAWSへのSingle Sign On (以下 SSO) 導入を行ったため、導入において工夫したポイントを5つ記載しました。

  • ① 手動ではなくコードベースでリソースを管理する
  • ② アカウント発行・削除のフローをPRで完結させる
  • ③ AWSコンソールのサインイン先アカウントが一目で分かる
  • ④ 簡単にAWS CLIのスイッチロールができる
  • ⑤ 誤った本番アカウントへのサインインに気づくことができる

それぞれを以下で詳しく説明していきます。

目次

SSO導入の全体像

SSO導入の背景についてはAWSの公式ドキュメントにも記載がある通りです。

SSO導入前後のアーキテクチャは以下のようになります。

SSO導入前のアーキテクチャ

image.png

SSO導入前はこちらの記事のように、IAMのスイッチロールを用いて構築を行っていました。
ユーザーはまず踏み台アカウントにログインし、付与されたroleにスイッチロールすることで各アカウントのリソースにアクセスすることができます。

SSO導入後のアーキテクチャ

image.png

SSO導入はAWS IAM アイデンティティセンターを用いて行いました。
IDプロバイダは既に弊社で導入していたOktaを採用しました。
管理アカウントでAWS IAM アイデンティティセンターのリソースを構築することで、AWS Organizationsで紐づく各アカウントに自動でroleが作成されます。
ユーザーはAWSアクセスポータルを介して各アカウントのリソースにアクセスすることができます。
image.png

その他の各要素に関してはこちらの記事が参考になるかと思います。
本記事ではOkta側の設定については割愛しますが、以下のドキュメントを参考にOkta側の設定を行いました。

SREの体験向上の工夫

上記の構築を行うSREとして、以下の観点で工夫を行いました。

  • ① 手動ではなくコードベースでリソースを管理する
  • ② アカウント発行・削除のフローをPRで完結させる

図示すると以下のようになります。
image.png

今までは全てAWSコンソールから手動でIAMに関するリソース群 (ユーザー, ユーザーグループ, ロール) を構築していました。
しかし、リソースの全体像を把握することが困難で、アカウント発行・削除の履歴を残しづらいという課題がありました。
そのため、Terraformを用いてIAMに関するリソース群を全てコードで管理することにしました。

① 手動ではなくコードベースでリソースを管理する

1. ユーザー・ユーザーグループ・ロールをシンプルに定義

ユーザー・権限・グループの追加や削除をする際に最小限の変更で済むように、シンプルに変数定義できるように記載方法を工夫しました。

terraform.tfvars
# アクセス権限セット
permission_sets = {
  "admin_role" = {
    description = "administrator role"
    aws_managed_policy_arns = [
      "arn:aws:iam::aws:policy/AdministratorAccess" # この配列に適用したいAWS管理ポリシーを追加する
    ]
  },
  ...
}

groups = {
  "prod_admin" = {
    description = "prod admin group"
    account_id = [
      "${ACCOUNT ID}" # この配列に適用したいアカウントIDを追加する
    ]
    permission_set_name = [
      "admin_role" # この配列に適用したいアクセス権限セットを追加する
    ]
  },
  ...
}

users = {
  "hogefuga@example.com" = {
    display_name = "hoge-fuga"
    last_name    = "hoge"
    first_name   = "fuga"
    assigned_groups = [
      "prod_admin" # この配列に適用したいグループを追加する
    ]
  },
  ...
}

2. ユーザー・ユーザーグループ・ロールの構築

上記の変数定義を用いて、ユーザー・ユーザーグループ・ロールの数だけfor_eachで繰り返しリソースを構築しています。

(クリックして展開)
main.tf
# userの作成
resource "aws_identitystore_user" "identitystore_user" {
  for_each = var.users

  identity_store_id = tolist(data.aws_ssoadmin_instances.ssoadmin_instances.identity_store_ids)[0]

  display_name = each.value["display_name"]
  user_name    = each.key

  name {
    given_name  = each.value["last_name"]
    family_name = each.value["first_name"]
  }

  emails {
    value   = each.key
    primary = true
  }
}

# groupの作成
resource "aws_identitystore_group" "identitystore_group" {
  for_each = var.groups

  identity_store_id = tolist(data.aws_ssoadmin_instances.ssoadmin_instances.identity_store_ids)[0]
  display_name      = replace(each.key, "_", "-")
  description       = each.value["description"]
}

# permission setの作成
resource "aws_ssoadmin_permission_set" "PermissionSet" {
  for_each = var.permission_sets

  name         = replace(each.key, "_", "-")
  description  = each.value["description"]
  instance_arn = tolist(data.aws_ssoadmin_instances.ssoadmin_instances.arns)[0]
}

3. アクセス権限セットへのAWS管理ポリシーのアタッチ

アクセス権限セット × AWS管理ポリシーの組み合わせの数だけアタッチリソースは存在するため、ローカル変数を用いて変形してからリソースを繰り返し構築しました。

(クリックして展開)
main.tf
locals {
  permission_set_aws_managed_policy_map = flatten([
    for ps_name, ps_values in var.permission_sets : [
      for policy_arn in ps_values.aws_managed_policy_arns : {
        permission_set_name    = ps_name
        aws_managed_policy_arn = policy_arn
      }
    ]
  ])
}

# AWS管理ポリシー・アクセス権限セットの関連付け
resource "aws_ssoadmin_managed_policy_attachment" "AwsManagedPolicyAttachment" {
  for_each = { for i in local.permission_set_aws_managed_policy_map : "${i.permission_set_name}-${i.aws_managed_policy_arn}" => i }

  instance_arn       = aws_ssoadmin_permission_set.PermissionSet[each.value.permission_set_name].instance_arn
  managed_policy_arn = each.value.aws_managed_policy_arn
  permission_set_arn = aws_ssoadmin_permission_set.PermissionSet[each.value.permission_set_name].arn
}

4. 3と同様に他の関連付けも構築

ユーザー・グループの関連付けはmodulesを用いて、terraform.tfvarsでの定義の仕方に沿った構築ができるように記載しました。

(クリックして展開)
main.tf
locals {
  permission_set_account_group_map = flatten([
    for group_name, group in var.groups : [
      for account_id in group.account_id : [
        for permission_set_name in group.permission_set_name : {
          permission_set_name = permission_set_name
          group_name          = group_name
          account_id          = account_id
        }
      ]
    ]
  ])
  group_ids_map = {
    for user, details in var.users : user => {
      group_ids = [for group_name in details.assigned_groups : aws_identitystore_group.identitystore_group[group_name].id]
    }
  }
}

# ユーザー・グループの関連付け
module "aws_identitystore_group_membership" {
  for_each = var.users

  source = "./modules/group_membership"

  identity_store_id = tolist(data.aws_ssoadmin_instances.ssoadmin_instances.identity_store_ids)[0]

  user_id   = aws_identitystore_user.identitystore_user[each.key].user_id
  group_ids = local.group_ids_map[each.key].group_ids
}

# アカウント・アクセス権限セット・グループの関連付け
resource "aws_ssoadmin_account_assignment" "AccountAssignment" {
  for_each = { for i in local.permission_set_account_group_map : "${i.permission_set_name}-${i.group_name}-${i.account_id}" => i }

  instance_arn       = tolist(data.aws_ssoadmin_instances.ssoadmin_instances.arns)[0]
  permission_set_arn = aws_ssoadmin_permission_set.PermissionSet[each.value.permission_set_name].arn

  principal_id   = aws_identitystore_group.identitystore_group[each.value.group_name].group_id
  principal_type = "GROUP"

  target_id   = each.value.account_id
  target_type = "AWS_ACCOUNT"
}
modules/group_membership/main.tf
resource "aws_identitystore_group_membership" "group_membership" {
  count = length(var.group_ids)

  identity_store_id = var.identity_store_id
  member_id         = var.user_id
  group_id          = replace(var.group_ids[count.index], "${var.identity_store_id}/", "")
}

上記と変数・データソースの定義を行うことで、IAM アイデンティティセンターに関する一連のリソースをTerraformで管理することができました。

② アカウント発行・削除のフローをPRで完結させる

アカウント発行・削除の際、PRベースでスムーズに承認と適用ができるように以下の実装を行いました。

  • PR作成時に terraform plan を自動実行
  • PRをmainマージ時に terraform apply を自動実行

applyの自動実行については以下のようにGitHub Actionsのワークフローでterraformコマンド実行用のCodeBuildを呼び出しています。

image.png

これにより、SREは発行されるPRのレビューを行うだけでリソースの構築は自動で行われるようになりました。

SSOユーザー(開発者)の体験向上の工夫

SSOのユーザー側の体験を良くするために、以下の観点で工夫を行いました。

  • ③ AWSコンソールのサインイン先アカウントが一目で分かる
  • ④ 簡単にAWS CLIのスイッチロールができる
  • ⑤ 誤った本番アカウントへのサインインに気づくことができる

③ AWSコンソールのサインイン先アカウントが一目で分かる

デフォルトの状態だとバッジにスイッチロール先のアカウント名が表示されないため、現在どのアカウントにいるかがわかりづらいです。
image.png

上記の課題に対しては、既に提供されているAWS Peacock Management Console Chrome拡張機能を採用しました。
こちらを利用することで、アカウント名がバッジに表示されるだけでなく、アカウントごとに任意の色分けをすることができます。
image.png

④ 簡単にAWS CLIのスイッチロールができる

基本的に、SSOのサインイン画面から以下のCommand line or programmatic accessをクリックして一時的な認証情報を得ることでAWS CLIでの操作が可能となります。
image.png

しかし、アカウントを変更する度に毎回AWSアクセスポータルにアクセスして情報を取得するという手間がかかります。
こちらを解消するために、AWSumeaws-sso-utilを組み合わせることでAWS CLIのスイッチロールをスムーズに行えるようにしました。(参考記事)

AWSumeの導入はこちらの記事を参考に行いました。
次に、aws-sso-utilのインストールを行います。

$ pip install aws-sso-util 

最後に、以下のように.aws/configファイルに記載をします。これで設定は完了です。

[profile {プロファイル名}]
sso_start_url      = https://{ssoポータルのurl}.awsapps.com/start
sso_region         = ap-northeast-1
sso_account_id     = {アカウントid}
sso_role_name      = admin-role
credential_process = aws-sso-util credential-process --profile {プロファイル名}

実際にサインインする際は、まず以下のコマンドでSSOの認証を行います。

$ aws-sso-util login

以下の画面にリダイレクトするので、Confirm and continueをクリックします。次の画面でAllowをクリックし、認証は完了です。
image.png

あとは以下のコマンドを叩くだけで簡単にスイッチロールをすることができます。

$ awsume {プロファイル名}

⑤ 誤った本番アカウントへのサインインに気づくことができる

本番アカウントへのサインインは慎重に行いたいですが、権限を持っている場合、誤ってサインインして気づかず作業をしてしまう...という可能性があります。
これを防ぐために、本番アカウントへのサインインがあった際にSlackにメンション付きで通知をする機能を作成しました。
構成は以下のようになっています。

image.png

1. Amazon EventBridgeがユーザーのサインインイベントを取得してAWS Lambdaをトリガーする

ユーザーのサインインイベントはAWS CloudTrailにログが残ります。
そのため、Amazon EventBridgeでサインインイベントを検知し、AWS Lambdaをトリガーします。

main.tf
# AWSサインイン時の通知用AWS EventBridgeルール
resource "aws_cloudwatch_event_rule" "notify_event_rule" {
  name          = "notify_signin_to_slack"
  description   = "Trigger Lambda on signin event"
  event_pattern = jsonencode({
    "detail-type" : ["AWS Console Sign In via CloudTrail"]
  })
}

ポイントはイベントパターンの箇所で、 AWS Console Sign In via CloudTrail を指定することで、サインインイベントを検知できます。

2. AWS LambdaでSlack通知

Amazon EventBridgeから取得した userIdentity.arnの情報をもとに、 ROLE_NAME で指定したroleでのアクセスがあった時にSlackに通知をするようにしました。

lambda/notify_signin_to_slack.py
import json
import urllib.request
import os


def lambda_handler(event, context):
    print(json.dumps(event))

    print(event['detail']['userIdentity']['arn'])
    arn = event['detail']['userIdentity']['arn']
    webhook_url = os.environ['RELEASE_CH_WEBHOOK_URL']
    mention_ids = os.environ['SLACK_MENTION_IDS']
    role_name = os.environ['ROLE_NAME']
    if role_name in arn:
        post_slack(arn, mention_ids, webhook_url, role_name)


def post_slack(arn, mention, webhook_url, role_name):
    username = arn.split('/')[-1].split('@')[0]
    aws_account_alias = os.environ['AWS_ACCOUNT_ALIAS']
    message = {
        "attachments": [
            {
                "color": "#f9ee00",
                "pretext": "{}".format(mention),
                "author_name": f"【警告】{aws_account_alias}アカウントへ{role_name}権限でのアクセスが発生しました",
                "title": f"{role_name}権限のアクセス通知",
                "text": f"{username}{aws_account_alias}アカウントに{role_name}権限でアクセスしました。スレッドに用途を記載してください。",
                "fields": [
                    {
                        "title": "user_arn",
                        "value": arn,
                        "short": False
                    }
                ],
            }
        ]
    }

    send_text = "payload=" + json.dumps(message)

    request = urllib.request.Request(
        webhook_url,
        data=send_text.encode("utf-8"),
        method="POST"
    )

    with urllib.request.urlopen(request):
        pass
(クリックして展開) その他必要なリソース
main.tf
# AWSサインイン時の通知用Lambda
resource "aws_lambda_function" "lambda_notify_signin_to_slack" {
  function_name    = "notify_signin_to_slack"
  handler          = "notify_signin_to_slack.lambda_handler"
  runtime          = "python3.11"
  role             = aws_iam_role.lambda_execution_role.arn
  filename         = data.archive_file.lambda_code.output_path
  source_code_hash = data.archive_file.lambda_code.output_base64sha256

  environment {
    variables = {
      RELEASE_CH_WEBHOOK_URL = "${var.release_ch_webhook_url}"
      SLACK_MENTION_IDS      = "${var.slack_mention_ids}"
      ROLE_NAME              = "${var.role_name}"
      AWS_ACCOUNT_ALIAS      = "${var.aws_account_alias}"
    }
  }
}

resource "aws_iam_role" "lambda_execution_role" {
  name  = "notify_signin_to_slack_lambda_role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "lambda_execution_role_policy" {
  role       = aws_iam_role.lambda_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_lambda_permission" "allow_eventbridge" {
  statement_id  = "AllowExecutionFromEventBridge"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.lambda_notify_signin_to_slack.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.notify_event_rule.arn
}

resource "aws_cloudwatch_event_target" "notify_event_target" {
  rule  = aws_cloudwatch_event_rule.notify_event_rule.name
  arn   = aws_lambda_function.lambda_notify_signin_to_slack.arn
}

上記の構築によって、本番アカウントにサインインした際以下のような通知をSlackからユーザーへ送れるようになりました。
image.png

最後に

地味なところではありますが、信頼性の担保のためにSSO導入は重要です。
また、生産性という観点でも開発者が日々行うサインイン作業の体験の向上はヒットしていくはずです。
これからも信頼性・生産性の両立を意識した改善を行なっていこうと思います。

20
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
4