Как стать автором
Обновить

Как сделать статический веб-сайт в object storage на s3 aws sdk

Введение

Приветствую! Я сижу в крайне уставшем состоянии, но с чувством гордости за выполненную задачу. Задача: закинуть статический html сайт с css, js,images в object storage и выводить это добро в iframe. В object storage, потому что уже его используем, а чем больше зависимостей, как известно, тем хуже. Мне оказалось крайне сложно найти информацию по этому поводу. С немного подгоревшим стулом я решил написать информацию по этому вопросу, т.к. это должно быть в интернете.

Что это?

У s3 есть возможность сделать статический сайт, а не только положить туда-сюда файлы. Вообще можно сделать публичным файл, что подойдет с одним index.html, но не подойдет с прилинкованным к нему css, js и прочими товарищами html сайтов.

Статический сайт делается только на bucket, у которого ACL (такая схема прав у s3) установлен на публичный доступ как минимум (а лучше только, что логично) на чтение, при том для всех пользователей, даже не зарегистрированных. Называется он public-read.

Далее в созданном публичном bucket можно выбрать настройки "веб-сайт", у которого можно настроить "главную страницу" (например, index.html), "страницу ошибки" (например error.html), переадресацию (запрос идет к bucket, а он редиректит на указанный адрес).

После сохранение вышеописанных манипуляций будет доступна ссылка вида: https://<bucket_name>website.<static_cloud_prefix>. В этот bucket можно кидать свои файлы, по ссылке он будет работать как хостинг.

Конечно, раз это облачный сервис, с ним можно взаимодействовать с помощью api и sdk. Мне надо было использовать sdk, т.к. его уже начали использовать (ну логично его использовать), а чем больше зависимостей, как известно, тем хуже. Api идёт до лучших времен, юзаем sdk. Но как?

А как через sdk?

В данный момент жизни колдую в основном на go, проект на нём же, примеры будут тоже на нём. Однако название у методов одинаковые, sdk библиотеки идут на все популярные ЯП, принцип один.

У go есть библиотека aws-sdk-go-v2. Последняя версия v2, конечно же она отличается от v1. Используем конечно же v2. Смысл реализации ровно такой же, как и через клиент, только выполняем это методами, а именно:

  • Код с комментариями. Это создание и отправление объекта (файла) в обычный bucket. Таких примеров куча, будет еще один

package main

import (
	"bytes"
	"context"
	"fmt"
	"os"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/bearatol/lg"
)

const MyTestBucket = "my-test-bucket"

func main() {
	ctx := context.Background()

	// create client object for s3
	client, err := NewS3Client(ctx)
	if err != nil {
		lg.Fatal(err)
	}
	// list buckets in s3
	buckets, err := client.GetListBucket(ctx)
	if err != nil {
		lg.Fatal(err)
	}

	// checking bucket name in a total list
	// if bucket with name "MyTestBucket" doesn't exist, then to create it
	// if we call method PutBucket and bucket with name "MyTestBucket" was created, then we will get error
	if !client.existBucket(MyTestBucket, buckets) {
		if err := client.PutBucket(ctx, MyTestBucket); err != nil {
			lg.Fatal(err)
		}
	}

	// we need some test file for upload to s3
	someTestFile := "/some/path/to/test/file.some"
	// and we need data of the someTestFile
	someTestFileData, err := os.ReadFile(someTestFile)
	if err != nil {
		lg.Fatal(err)
	}

	// create the struct of data with params
	// the params are describe data of our someTestFile
	params := &s3.PutObjectInput{
		Bucket:      aws.String(MyTestBucket),
		Key:         aws.String(someTestFile),
		Body:        bytes.NewReader(someTestFileData),
		ContentType: aws.String("some"),
	}

	// to sent our file to bucket with name MyTestBucket
	if _, err := client.PutObjInS3(ctx, params); err != nil {
		lg.Fatal(err)
	}

	lg.Infof("CONGRATULATIONS! You have done it! The [%s] was send to your bucket!", someTestFile)
}


func (s *S3) existBucket(s3CloudBucket string, buckets []string) bool {
	for _, bucket := range buckets {
		if bucket == s3CloudBucket {
			return true
		}
	}
	return false
}

type S3 struct {
	client *s3.Client
}

func NewS3Client(ctx context.Context) (*S3, error) {

	// enter your data
	if _, ok := os.LookupEnv("AWS_ACCESS_KEY_ID"); !ok {
		return nil, fmt.Errorf("cannot set environment [AWS_ACCESS_KEY_ID]")
	}
	// enter your data
	if _, ok := os.LookupEnv("AWS_SECRET_ACCESS_KEY"); !ok {
		return nil, fmt.Errorf("cannot set environment [AWS_SECRET_ACCESS_KEY]")
	}
	// I need to set a value ru-central1, enter your data
	if _, ok := os.LookupEnv("AWS_REGION"); !ok {
		return nil, fmt.Errorf("cannot set environment [AWS_REGION]")
	}

	customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
		if service == s3.ServiceID && region == "ru-central1" { // <your data>
			return aws.Endpoint{
				PartitionID:   "<your data>",
				URL:           "<your data>",
				SigningRegion: "ru-central1", // <your data too>
			}, nil
		}
		return aws.Endpoint{}, fmt.Errorf("unknown endpoint requested")
	})

	// the method get data from environment variables
	cfg, err := config.LoadDefaultConfig(ctx, config.WithEndpointResolverWithOptions(customResolver))
	if err != nil {
		return nil, err
	}

	return &S3{s3.NewFromConfig(cfg)}, nil
}

func (s *S3) GetListBucket(ctx context.Context) ([]string, error) {
	buckets, err := s.client.ListBuckets(ctx, &s3.ListBucketsInput{})
	if err != nil {
		return nil, err
	}
	arrBuckets := make([]string, len(buckets.Buckets))
	for _, bucket := range buckets.Buckets {
		arrBuckets = append(arrBuckets, *bucket.Name)
	}
	return arrBuckets, nil
}

func (s *S3) PutBucket(ctx context.Context, bucket string) error {
	if _, err := s.client.CreateBucket(ctx, &s3.CreateBucketInput{
		Bucket: aws.String(bucket),
	}); err != nil {
		return err
	}

	return nil
}

func (s *S3) PutObjInS3(ctx context.Context, param *s3.PutObjectInput) (*s3.PutObjectOutput, error) {
	return s.client.PutObject(ctx, param)
}
  • Код с созданием bucket, даем ему ACL "public-read", тянем метод, который делает из bucket сайт, кидаем туда файлы. Готово, сайт доступен по адресу (посмотрите у себя в админке), который стриотся как https://<bucket_name>website.<static_cloud_prefix>

package main

import (
	"bytes"
	"context"
	"fmt"
	"os"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/aws/aws-sdk-go-v2/service/s3/types"
	"github.com/bearatol/lg"
)

const (
	MyTestBucket        = "my-test-bucket"
	MyTestBucketWebsite = "my-test-bucket-website"
)

func main() {
	ctx := context.Background()

	// create client object for s3
	client, err := NewS3Client(ctx)
	if err != nil {
		lg.Fatal(err)
	}
	// list buckets in s3
	buckets, err := client.GetListBucket(ctx)
	if err != nil {
		lg.Fatal(err)
	}
	// checking bucket name in a total list
	// if bucket with name "MyTestBucket" doesn't exist, then to create it
	// if we call method PutBucket and bucket with name "MyTestBucket" was created, then we will get error
	if !client.existBucket(MyTestBucket, buckets) {
		if err := client.PutBucket(ctx, MyTestBucket); err != nil {
			lg.Fatal(err)
		}
	}

	// we need some test file for upload to s3
	someTestFile := "/some/path/to/test/file.some"
	// and we need data of the someTestFile
	someTestFileData, err := os.ReadFile(someTestFile)
	if err != nil {
		lg.Fatal(err)
	}

	// create the struct of data with params
	// the params are describe data of our someTestFile
	params := &s3.PutObjectInput{
		Bucket:      aws.String(MyTestBucket),
		Key:         aws.String(someTestFile),
		Body:        bytes.NewReader(someTestFileData),
		ContentType: aws.String("some"),
	}

	// to sent our file to bucket with name MyTestBucket
	if _, err := client.PutObjInS3(ctx, params); err != nil {
		lg.Fatal(err)
	}

	lg.Infof("CONGRATULATIONS! You have done it! The [%s] was send to your bucket!", someTestFile)

	// CREATE BUCKET SITE

	// add a new bucket to the list that we created earlier
	buckets = append(buckets, MyTestBucket)
	// and to check one more time
	if !client.existBucket(MyTestBucketWebsite, buckets) {
		// add a new bucket with needed ACL permissions
		if err := client.PutPublicBucket(ctx, MyTestBucketWebsite); err != nil {
			lg.Fatal(err)
		}
		// make a public bucket a website
		if err := client.PutWebsiteForPublicBucket(ctx, MyTestBucketWebsite); err != nil {
			lg.Fatal(err)
		}
	}

	// get some files for a static website
	someIndexFile := "/some/file/index.html"
	someStyleFile := "/some/file/style.css"

	// get data of files
	someIndexFileData, err := os.ReadFile(someIndexFile)
	if err != nil {
		lg.Fatal(err)
	}
	someStyleFileData, err := os.ReadFile(someStyleFile)
	if err != nil {
		lg.Fatal(err)
	}

	// create a new params for index file
	paramsIndexFile := &s3.PutObjectInput{
		Bucket:      aws.String(MyTestBucketWebsite),
		Key:         aws.String(someIndexFile),
		Body:        bytes.NewReader(someIndexFileData),
		ContentType: aws.String("html"),
	}
	// create a new params for style file
	paramsStyleFile := &s3.PutObjectInput{
		Bucket:      aws.String(MyTestBucketWebsite),
		Key:         aws.String(someStyleFile),
		Body:        bytes.NewReader(someStyleFileData),
		ContentType: aws.String("css"),
	}
	// sent html file
	if _, err := client.PutObjInS3(ctx, paramsIndexFile); err != nil {
		lg.Error(err)
	}
	// sent css file
	if _, err := client.PutObjInS3(ctx, paramsStyleFile); err != nil {
		lg.Error(err)
	}

	url := fmt.Sprintf("https://%s.%s", MyTestBucketWebsite, "some-prefix")
	lg.Infof("CONGRATULATIONS! You have done it! You create a website bucket by url: [%s]!", url)
}

func (s *S3) existBucket(s3CloudBucket string, buckets []string) bool {
	for _, bucket := range buckets {
		if bucket == s3CloudBucket {
			return true
		}
	}
	return false
}

type S3 struct {
	client *s3.Client
}

func NewS3Client(ctx context.Context) (*S3, error) {

	// enter your data
	if _, ok := os.LookupEnv("AWS_ACCESS_KEY_ID"); !ok {
		return nil, fmt.Errorf("cannot set environment [AWS_ACCESS_KEY_ID]")
	}
	// enter your data
	if _, ok := os.LookupEnv("AWS_SECRET_ACCESS_KEY"); !ok {
		return nil, fmt.Errorf("cannot set environment [AWS_SECRET_ACCESS_KEY]")
	}
	// I need to set a value ru-central1, enter your data
	if _, ok := os.LookupEnv("AWS_REGION"); !ok {
		return nil, fmt.Errorf("cannot set environment [AWS_REGION]")
	}

	customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
		if service == s3.ServiceID && region == "ru-central1" { // <your data>
			return aws.Endpoint{
				PartitionID:   "<your data>",
				URL:           "<your data>",
				SigningRegion: "ru-central1", // <your data too>
			}, nil
		}
		return aws.Endpoint{}, fmt.Errorf("unknown endpoint requested")
	})

	// the method get data from environment variables
	cfg, err := config.LoadDefaultConfig(ctx, config.WithEndpointResolverWithOptions(customResolver))
	if err != nil {
		return nil, err
	}

	return &S3{s3.NewFromConfig(cfg)}, nil
}

func (s *S3) GetListBucket(ctx context.Context) ([]string, error) {
	buckets, err := s.client.ListBuckets(ctx, &s3.ListBucketsInput{})
	if err != nil {
		return nil, err
	}
	arrBuckets := make([]string, len(buckets.Buckets))
	for _, bucket := range buckets.Buckets {
		arrBuckets = append(arrBuckets, *bucket.Name)
	}
	return arrBuckets, nil
}

func (s *S3) PutBucket(ctx context.Context, bucket string) error {
	if _, err := s.client.CreateBucket(ctx, &s3.CreateBucketInput{
		Bucket: aws.String(bucket),
	}); err != nil {
		return err
	}

	return nil
}

func (s *S3) PutPublicBucket(ctx context.Context, bucket string) error {
	if _, err := s.client.CreateBucket(ctx, &s3.CreateBucketInput{
		Bucket: aws.String(bucket),
		ACL:    types.BucketCannedACLPublicRead,
	}); err != nil {
		return err
	}
	return nil
}

func (s *S3) PutWebsiteForPublicBucket(ctx context.Context, bucket string) error {
	websiteConfiguration := &types.WebsiteConfiguration{
		ErrorDocument: &types.ErrorDocument{
			Key: aws.String("error.html"),
		},
		IndexDocument: &types.IndexDocument{
			Suffix: aws.String("index.html"),
		},
	}

	input := &s3.PutBucketWebsiteInput{
		Bucket:               aws.String(bucket),
		WebsiteConfiguration: websiteConfiguration,
	}

	_, err := s.client.PutBucketWebsite(ctx, input)
	if err != nil {
		return err
	}

	return nil
}

func (s *S3) PutObjInS3(ctx context.Context, param *s3.PutObjectInput) (*s3.PutObjectOutput, error) {
	return s.client.PutObject(ctx, param)
}

Принцип как и выше, натыкивая из админки.

Создали bucket; сделали его публичным; сказали, что он будет сайтом; указали домашнюю страницу в виде html файлика; закинули туда html файлик с его атрибутикой.

Надеюсь, я кому-то сэкономил часы их драгоценной жизни. Удачи!

Ссылки с документацией, примерами:
https://github.com/aws/aws-sdk-go-v2
https://github.com/awsdocs/aws-doc-sdk-examples
https://docs.aws.amazon.com/code-library/latest/ug/go_2_s3_code_examples.html

Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.