CloudFormationを使ったS3バケットの構築とIAM制限の実装

スポンサーリンク
ハンズオン
スポンサーリンク

はじめに

AWS認定試験ではCloudFormationに関する問題が多く出題されますが、これまでCloudFormationに触れたことがない方にとっては、「そもそもCloudFormationとは何か?」や「どのように実行・管理するのか?」といった具体的なイメージが湧かないかもしれません。

本ハンズオンでは、CloudFormationを使ってリソースの作成を実際に行いながら、その仕組みを理解することを目指します。

CloudFormationとは?

※CloudFormationについて概要を知っている方は読み飛ばしてもらっていいです。

簡単にいうとAWSのリソースの設定値をテンプレート化してリソースを配備・管理が可能なサービスです。

テンプレートすることで以下のメリットがあります。

  • 一貫性のある環境を迅速に構築できる
  • インフラストラクチャをコード(Infrastructure as Code, IaC)として管理できる
  • AMS内のリソースの作成、更新、削除といった運用作業を自動化し、効率敵に管理できる

参考:AWS CloudFormation とは

ハンズオン概要

このハンズオンの構成は以下です。

このハンズオンでは、CloudFormationを使用して、以下のS3バケットを構築します。

  • 特定のIAMロールを持つLambda関数のみが、バケットに対してオブジェクトの取得、登録、更新、削除が可能(バケットポリシーの設定)
  • アップロードされたファイルは1日後に自動削除(ライフサイクルポリシーの設定)

1. 前提条件

事前に以下を準備してください。

  • AWSアカウント
  • IAMロールとLambda関数の事前作成

2. CloudFormationテンプレートの作成

以下がS3バケットとIAMポリシーを作成するCloudFormationテンプレートの例です。

Parameters:
  S3BucketName:
    Type: String
    Description: "Name of the S3 bucket."

  IAMRoleArn:
    Type: String
    Description: "ARN of the IAM role that will have access to the S3 bucket."

  ExpirationInDays:
    Type: Number
    Default: 1
    Description: "Number of days after which objects expire in the S3 bucket."
    
Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref S3BucketName
      LifecycleConfiguration:
        Rules:
          - Id: "ExpireFilesAfterSpecifiedDays"
            Status: "Enabled"
            ExpirationInDays: !Ref ExpirationInDays

  S3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Ref IAMRoleArn
            Action:
              - s3:GetObject
              - s3:ListBucket
              - s3:PutObject
              - s3:DeleteObject
            Resource:
              - !Sub arn:aws:s3:::${S3BucketName}/*
              - !Sub arn:aws:s3:::${S3BucketName}

3. CloudFormationスタックのデプロイ

  1. AWS管理コンソールのCloudFormationの画面を開きます。「スタックの作成」を選択してください。
  2. テンプレートの準備画面が表示されます。アップロードをします。以下指定の上、「次へ」を指定してください。
    テンプレートの準備:既存のテンプレートを選択
    テンプレートの指定:テンプレートファイルのアップロード
    ファイルの選択:2. CloudFormationテンプレートの作成のyamlファイルを選択
  1. スタックの詳細を選択画面が表示されます。以下を指定の上、「次へ」を選択してください。スタック名:任意の文字列を入力してください。パラメータ
パラメータ名 設定値 説明・注意事項
S3BucketName 【作成するS3バケット名】 S3の規約にあった文字列をしてしてください。
規約外の場合スタックの作成が失敗します。
IAMRoleArn 【Lambda関数に紐づけするロールのARN】 Lambda関数からのアクセスを許可します。
ExpirationInDays 1 オブジェクトの有効期限(日)
指定した日付が過ぎるとバケットから削除されます。
今回はハンズオンなので1日に設定します。

  1. スタックオプションの設定画面が表示されます。デフォルトのまま「次へ」を選択してください。 
  2. 確認して作成画面が表示されます。設定内容確認の上、「送信」を選択してください。
  3. スタックが作成されます。しばらくすると「CREATE_IN_PROGRESS」から「CREATE_COMPLETE」に変わります。 

4. バケットの確認

※ユーザの権限によっては閲覧ができない可能性があります。

  1. AWS管理コンソールのS3の画面を開きます。指定したバケットが作成されています。バケットを選択して詳細を確認します。
  2. アクセス許可を選択して設定したバケットポリシーが正しく設定されているか確認してみます。
  3. バケットポリシーも正しく設定されています。
  4. 管理を選択して、ライフサイクルポリシーが正しく設定されているか確認してみます。
  1. ライフサイクルポリシーも正しく設定されています。

5. Lambda関数の設定

Lambda関数が指定のIAMロールを持つことを確認し、S3バケットへのアクセスを実行します。
lambda関数の例は以下です。

ランタイム:python 3.12

環境変数

環境変数名 設定値
AWS_REGION S3バケットの配備先リージョン(「ap-northeast-1」など
BUCKET_NAME 作成したS3バケット名

コード

import boto3
import datetime
import json
import os

region = os.getenv('AWS_REGION', 'ap-northeast-1') 
s3_client = boto3.client('s3', region_name=region)
bucket_name = os.getenv('BUCKET_NAME', '') 

def lambda_handler(event, context):
    command = event.get('command')
    bucket_name = event.get('bucket_name')
    file_name = event.get('file_name')

    if not command or not bucket_name:
        return {
            'statusCode': 400,
            'body': json.dumps('Error: command and bucket_name are required parameters.')
        }

    try:
        if command == 'upload_file':
            return upload_file(bucket_name)
        elif command == 'get_file':
            if not file_name:
                return {
                    'statusCode': 400,
                    'body': json.dumps('Error: file_name is required for get_file command.')
                }
            return get_file(bucket_name, file_name)
        elif command == 'list_files':
            return list_files(bucket_name)
        elif command == 'delete_file':
            if not file_name:
                return {
                    'statusCode': 400,
                    'body': json.dumps('Error: file_name is required for delete_file command.')
                }
            return delete_file(bucket_name, file_name)
        else:
            return {
                'statusCode': 400,
                'body': json.dumps('Error: Invalid command.')
            }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps(f'Error: {str(e)}')
        }

def upload_file():
    current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    file_content = f"Current time: {current_time}"
    file_name = f"current_time_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"

    try:
        s3_client.put_object(
            Bucket=bucket_name,
            Key=file_name,
            Body=file_content
        )
        return {
            'statusCode': 200,
            'body': json.dumps(f'File {file_name} uploaded to {bucket_name}.')
        }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps(f'Error uploading file: {str(e)}')
        }

def get_file(file_name):
    try:
        response = s3_client.get_object(
            Bucket=bucket_name,
            Key=file_name
        )
        file_content = response['Body'].read().decode('utf-8')
        return {
            'statusCode': 200,
            'body': json.dumps(f'File content: {file_content}')
        }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps(f'Error retrieving file: {str(e)}')
        }

def list_files():
    try:
        response = s3_client.list_objects_v2(
            Bucket=bucket_name
        )
        files = [obj['Key'] for obj in response.get('Contents', [])]
        return {
            'statusCode': 200,
            'body': json.dumps(f'Files in bucket {bucket_name}: {files}')
        }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps(f'Error listing files: {str(e)}')
        }

def delete_file(file_name):
    try:
        s3_client.delete_object(
            Bucket=bucket_name,
            Key=file_name
        )
        return {
            'statusCode': 200,
            'body': json.dumps(f'File {file_name} deleted from {bucket_name}.')
        }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps(f'Error deleting file: {str(e)}')
        }

テストイベント

①ファイル登録

{
  "command": "upload_file"
}

②ファイル取得

{
  "command": "get_file",
  "file_name": "current_time_********_******.txt" // ファイル名を指定
}

③ファイル一覧取得

{
  "command": "list_files"
}

④ファイル削除

{
  "command": "delete_file",
  "file_name": "current_time_********_******.txt" // ファイル名を指定
}

6. Lambdaでアクセス検証

指定のIAMロールがLambdaに紐づいていない場合(バケット取得時の例)

{
  "statusCode": 500,
  "body": "\"Error retrieving file: An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::************:assumed-role/***************/handson-s3bucketaccess is not authorized to perform: s3:ListBucket on resource: \\\"arn:aws:s3:::20241020-handsonbucket\\\" because no identity-based policy allows the s3:ListBucket action\""
}

指定のIAMロールが紐づいている場合

①ファイル登録

{
  "statusCode": 200,
  "body": "\"File current_time_20241020_070609.txt uploaded to 20241020-handsonbucket.\""
}

S3バケットを確認するとファイルが登録されています。

②ファイル取得

{
    "statusCode": 200,
    "body": "\"File content: Current time: 2024-10-20 07:06:09\""
}

③ファイル一覧取得

S3バケットに以下が登録されている状態で実行してみます。

{
    "statusCode": 200,
    "body": "\"Files in bucket 20241020-handsonbucket: ['current_time_20241020_070609.txt', 'current_time_20241020_071216.txt', 'current_time_20241020_071229.txt']\""
}

④ファイル削除

S3バケット登録されている「current_time_20241020_070609.txt」を削除してみます。

{
    "statusCode": 200,
    "body": "\"File current_time_20241020_070609.txt deleted from 20241020-handsonbucket.\""
}

S3バケットを確認するとファイルが削除されています。

7. ライフサイクルポリシーの検証

こちらのハンズオンではライフサイクルポリシーの検証はハンズオンの時間内でできないため、省略します。

まとめ

このハンズオンでは、CloudFormationを使ってS3バケットの作成とIAM制限、ライフサイクルポリシーを実装する方法を紹介しました。

CloudFormationに初めて触れる方でも、実際の利用イメージが掴めたのではないでしょうか。

ぜひこのハンズオンを参考に、他のリソースの配備オプションの設定や、別のサービスリソースの配備にも挑戦してみてください。

タイトルとURLをコピーしました