🌍

MFA認証を必須化した上で、Terraformをdevcontainerで開発する環境を構築する

2023/01/28に公開

1. はじめに

開発は絶対devcontainerでやりたいし、かつ、AWS CLI利用時もMFA認証を必須化したいよねという、ニッチ?なエンジニア(自分はその一人)向けに、Terraformの開発環境を構築する方法を記事にまとめてみました。

2. AWS CLI利用時にもMFA認証を必須化する

2-1. どうやって必須化するのか

簡単に言うと、IAMユーザーが所属するIAMグループに対して、MFA認証をしていない場合にActionに制限をかけるIAMポリシーをアタッチし、AWS CLI利用時はMFA認証を要求するIAMロールを引き受けて作業する運用にします[1]。こうすることで、MFA認証を通過していない状態ではAWS CLIで十分な作業ができないので、MFA認証を要求するIAMロールにスイッチしてから作業することを強制できます。(ちなみに、自分はSREでもなんでもなく、個人のAWSアカウントで趣味でこれをやっているw)

注意点としては、MFA認証をしていない場合でも全てのActionに制限をかけるのではなく、一部のActionは許可しておく必要があります。というのも、もし、MFA認証をしていない場合に全てのActionに制限をかけてしまうと、最初にパスワードを設定することも、MFA認証を有効化することも、クレデンシャルを発行することもできなくなってしまい、鶏卵問題に陥ってしまうからです。ですので、MFA認証していない場合でも、以下に列挙することだけは許可した方が良さそうです(あくまで、個人で趣味でやっていることなので、実運用に適うかはわかりませんw)。

  • パスワードの変更
  • MFAデバイスの有効化
  • クレデンシャルの発行と削除

こうすることで、新しいユーザーを発行して、それを開発者に連絡すれば、マネジメントコンソールからログインして自分でパスワードやMFAデバイスやクレデンシャルの設定をしてもらうことができます(という妄想をしている)。

そして、もう一つの注意点は、引き受けるIAMロールにもMFA認証を必須にした上で、特定のIAMユーザーのみがそのロールを引き受けられるように制限をかけておくことです。こうすることで、特定のIAMユーザーがMFA認証した場合しかAssumeRoleできないように制御できます。ここまでできれば、セキュリティ的には十分でしょうか(少なくとも個人アカウントのレベルでは)。本当は、ロールの引き受け元を特定のIAMグループに限定することができればなお良かったのですが、現時点ではそのような方法が見つけられなかったので、渋々IAMユーザー単位で信頼関係を制限しています。

2-2. 作業手順

  1. IAMユーザーの作成MFAデバイスの有効化アクセスキーの発行は既に行われていることを前提とします。
  2. 2-1節で説明したことをCloudFormation化すると以下のようになるので、このYAMLファイルをCloudFormationにツッコミます。(自身のIAMユーザー名はmy-nameとする。)
AWSTemplateFormatVersion: "2010-09-09"
Description: "Create IAM Policy, Role, and Group"
Resources:
  # MFA必須ポリシー
  MFAForcePolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      ManagedPolicyName: "MFAForcePolicy"
      PolicyDocument:
        {
          "Version": "2012-10-17",
          "Statement": [
	    # MFA認証をしていない場合、NotActionに列挙した以外のアクションを実行を拒否する
            {
              "Effect": "Deny",
              "NotAction": [
                "iam:CreateVirtualMFADevice",
                "iam:DeleteVirtualMFADevice",
                "iam:DeactivateMFADevice",
                "iam:EnableMFADevice",
                "iam:ResyncMFADevice",
                "iam:ListMFADevices",
                "iam:ChangePassword",
                "iam:CreateAccessKey",
                "iam:DeleteAccessKey",
                "iam:GetAccountPasswordPolicy",
                "iam:UpdateAccessKey",
                "iam:UpdateSigningCertificate",
                "iam:UploadSigningCertificate",
                "iam:UpdateLoginProfile",
              ],
              "Resource": "*",
              "Condition": {
                "BoolIfExists": {
                  "aws:MultiFactorAuthPresent": "false"
                }
              }
            },
	    # MFA認証の有無に依らず、Actionに列挙したアクションの実行を許可する
            {
              "Effect": "Allow",
              "Action": [
                "iam:ListUsers",
                "iam:ListVirtualMFADevices"
              ],
              "Resource": "*"
            }
          ]
        }

  # 管理者グループの定義
  AdminGroup:
    Type: "AWS::IAM::Group"
    DependsOn:
      - MFAForcePolicy
    Properties:
      GroupName: "Admins"
      ManagedPolicyArns:
        # 管理者ポリシー設定(AWS管理ポリシー)
        - "arn:aws:iam::aws:policy/AdministratorAccess"
        - Fn::Join: ["", ["arn:aws:iam::", !Ref "AWS::AccountId", ":policy/MFAForcePolicy"]]

  # スイッチ元のロールを制限した管理者ロールの定義
  AdminRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: "AdminRole"
      Path: "/"
      # 指定したIAMユーザーのみAssumeRoleを許可
      AssumeRolePolicyDocument:
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "AWS": [
                  { "Fn::Join" : [
                      "", [
                        "arn:aws:iam::",
                        { "Ref" : "AWS::AccountId" },
                        ":user/my-name"
                      ]
                    ]
                  }
                ]
              },
              "Action": "sts:AssumeRole",
              "Condition": {
                "Bool": {
                  "aws:MultiFactorAuthPresent": "true"
                }
              }
            }
          ]
        }
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/AdministratorAccess"
  1. そして、マネジメントコンソールでmy-name(自身のIAMユーザー)をAdmins(上記のテンプレートで作成したIAMグループ)に所属させます[2]

3. aws-vaultを設定する

3-1. aws-vaultを利用する背景

2章で、AWS CLI利用時にもMFA認証を必須化することができましたが、実は、Terraformは対話的にMFA認証できるようになっていないので、このままではterraformコマンドを実行することはできません😅

ですので、aws-vaultを利用し、あらかじめ前章で作成したAdminRoleを引き受けて一時的セキュリティ認証情報を払い出しておき、その一時的な認証情報を使ってAWS CLIを実行する方式を採用します[3]

3-2. 作業手順

  1. aws-vaultが既にインストールされており、かつ、IAMユーザー(例ではmy-name)のアクセスキーが既に発行されていることを前提とします。
  2. まず、クレデンシャルをmy-nameというプロファイル名で保存します。
$ aws-vault add my-name
Enter Access Key ID: AKIAXXXXXXXXXXXXXXXX
Enter Secret Access Key:
  1. そして、以下のように、スイッチしたいプロファイル(ここではadmin_role)を.aws/configに設定します。
.aws/config
[default]
region=ap-northeast-1
output=json

[profile my-name]

[profile admin_role]
region=ap-northeast-1
output=json
mfa_serial=arn:aws:iam::123456789012:mfa/my-name
source_profile=my-name
role_arn=arn:aws:iam::123456789012:role/AdminRole
credential_process=aws-vault exec my-name --json --prompt=osascript

.aws/configの説明

  1. source_profile=my-namerole_arn=arn:aws:iam::123456789012:role/AdminRoleの記述によって、my-nameというIAMユーザーの認証情報を利用して、AdminRoleを引き受けています[4]
  2. mfa_serial=arn:aws:iam::123456789012:mfa/my-nameの記述によって、使用するMFAデバイスを指定しています。
  3. credential_process=aws-vault exec my-name --json --prompt=osascriptの記述によって、aws-vault(= AWS CLIが直接サポートしていない認証情報の生成ツール)が払い出す認証情報を参照させています。

4. devcontainerでのTerraformの開発環境を構築する

4-1. 概要

MFA認証が必須化された状態でTerraformの開発用コンテナを作成する時のキーポイントは、「どのようにしてAWSの認証情報をコンテナに埋め込むのか」です。ここでは以下の方法を採用しています。

開発用コンテナの起動にフックをかけてスイッチ先のIAMロール(ここではAdminRole)を引き受けるコマンドを実行し、その認証情報をファイルに書き出し、コンテナ起動時にそのファイルを環境変数として埋め込む[5]

実際の設定は次節以降で説明していきます。

4-2. ディレクトリ構成

ディレクトリ構成を晒しておきますが、お好みで良いと思います👍

.
├── .devcontainer
│   └── devcontainer.json
├── Dockerfile
├── docker-compose.yml
└── terraform
    ├── modules
    │   └── sample
    │       ├── main.tf
    │       ├── outputs.tf
    │       └── variables.tf
    └── main.tf

4-3. Dockerfile

Dockerfileもお好みで良いと思うのですが、一応、自分の設定を晒しておきます。M1 Mac用の良い感じのイメージが見つけられなかったので、pythonのベースイメージに手動でAWS CLIとTerraformをインストールしています。あ、そうそう、汎用的に作るモチベが湧かなかったので、以下はM1 Macじゃないと動かないです😅(←コンテナの意味...w)

Dockerfile
ARG PYTHON_VERSION
FROM python:${PYTHON_VERSION}-slim-bullseye as build
RUN apt update && apt install -y curl unzip wget

# Install aws-cli Manually
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip" && \
  unzip awscliv2.zip && \
  ./aws/install

# Install Terraform Manually
ARG TERRAFORM_VERSION
RUN wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_arm.zip && \
  unzip terraform_${TERRAFORM_VERSION}_linux_arm.zip

#######

FROM debian:bullseye-slim as dev
RUN apt update && apt install -y git
COPY --from=build /usr/local/aws-cli /usr/local/aws-cli
COPY --from=build /usr/local/bin /usr/local/bin
COPY --from=build ./terraform /usr/bin/terraform

# Install the "auto-complete" Terraform Extension
RUN terraform -install-autocomplete

4-4. docker-compose.yml

docker-compose.ymlは大切なポイントが一つあります。それは、env_fileを設定することです。このenv_fileで指定したファイルにAWSの認証情報が格納される予定なので、このファイルをコンテナの環境変数として埋め込むようにしておきます。

docker-compose.yml
version: "3.9"

services:
  tf:
    build:
      context: .
      dockerfile: Dockerfile
      target: dev
      args:
        TERRAFORM_VERSION: 1.3.7
        PYTHON_VERSION: 3.11.1
    working_dir: "/home"
    command: /bin/bash
    stdin_open: true
    tty: true
    volumes:
      # 人によっては設定ファイルの置き場所が違うかもしれないが、多くは同じはず。
      - ~/.aws:/root/.aws
      - ~/.gitconfig:/root/.gitconfig:ro
      - ~/.ssh:/root/.ssh:ro
      - ./:/home
    env_file:
      # [後述]このファイルに一時的な認証情報を格納し、コンテナの環境変数として埋め込んでいる。
      - .env
    environment:
      AWS_DEFAULT_OUTPUT: json

4-5. devcontainer.json

devcontainer.jsonは以下のように設定します。

devcontainer.json
{
	"name": "Terraform",
	"dockerComposeFile": [
		"../docker-compose.yml"
	],
	"service": "tf",
	"workspaceFolder": "/home",
	"initializeCommand": "aws-vault exec admin_role -- env | grep AWS > .env",
	"settings": {
		"terminal.integrated.defaultProfile.linux": "/bin/bash",
		"editor.insertSpaces": true,
		"editor.tabSize": 2,
		"files.encoding": "utf8",
		"files.eol": "\n",
		"files.insertFinalNewline": true,
		"files.trimFinalNewlines": true,
		"files.trimTrailingWhitespace": true,
		"editor.formatOnSave": true,
		"[terraform]": {
			"editor.defaultFormatter": "hashicorp.terraform",
			"editor.formatOnSave": true,
			"editor.codeActionsOnSave": {
				"source.formatAll.terraform": true
			}
		},
		"[terraform-vars]": {
			"editor.defaultFormatter": "hashicorp.terraform",
			"editor.formatOnSave": true,
			"editor.codeActionsOnSave": {
				"source.formatAll.terraform": true
			}
		}
	},
	"extensions": [
		"hashicorp.terraform"
	]
}

ポイントはinitializeCommandを設定して、コンテナを起動する直前にaws-vault exec admin_role -- env | grep AWS > .envコマンドを実行させることです。こうすることで、VS Codeのリモートコンテナを起動するタイミングでAdminRoleを引き受けるコマンドが発動します。そして、MFA認証が要求され、認証に成功すると一時的な認証情報が返されるのでそれを受け取り、.envファイルに書き込んでファイル出力します。そしてさらに、コンテナ起動時に.envファイルの中身がコンテナの環境変数として書き込まれ、したがって、コンテナ内からterraformコマンドが実行できるようになります。

ちなみに、認証情報の有効期限が切れた場合は、リモートコンテナを再ビルドすることで再度MFA認証が要求され、再発行された一時的な認証情報がコンテナの環境変数に再設定されます👍

5. まとめ

この記事では、AWS CLI利用時にもMFA認証を必須化していて、かつ、Terraformの開発をdevcontainerでやりたいエンジニア向けに、その方法をまとめました。MFA認証の必須化はともかく、Terraformの開発をdevcontainer上でやりたいエンジニアは多くはないかもしれませんが、どこかの誰かのお役に立てれば幸いです😁

脚注
  1. AWS IAMのマニアックな話を参考にさせていただきました。 ↩︎

  2. 自分は、IAMユーザーにIAMグループを所属させる対応付けまでCloudFormationなどのIaCで管理するのは、逆に面倒なんじゃないかと思っています。 ↩︎

  3. aws-vaultは、そもそもが、永続的なクレデンシャルキーをOSのキーストアに暗号化して保存し、それを元に一時的なクレデンシャルを発行するツールです。日本語の記事だとこちらの記事が参考になりそうです。 ↩︎

  4. 詳細はIAM ロール使用の概要をご参照ください。 ↩︎

  5. VS Codeで作るAWS Vault付きのポータブルなTerraform環境
    を参考にさせていただきました。 ↩︎

Discussion