Как сделать статический веб-сайт в 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