diff --git a/pkg/environments/environment.go b/pkg/environments/environment.go
index 465a409129330506b5a7a9c9ebca9a14e5f10bee..3e0f7bb62a0be1d2fbef334322a6cb315060af8a 100644
--- a/pkg/environments/environment.go
+++ b/pkg/environments/environment.go
@@ -6,6 +6,18 @@ const (
 	DefaultEnvironment = "master"
 )
 
+var (
+	ReadAllowedStates = []State{
+		StateNew,
+		StateReady,
+	}
+
+	WriteAllowedStates = []State{
+		StateNew,
+		StateReady,
+	}
+)
+
 type State int
 
 const (
diff --git a/pkg/environments/service.go b/pkg/environments/service.go
index cd0a37f449819a4b92239f8d8b38a3a0036ec8d8..920fc043704f9d7dcf51d83032d98292be758550 100644
--- a/pkg/environments/service.go
+++ b/pkg/environments/service.go
@@ -2,6 +2,11 @@ package environments
 
 import (
 	"context"
+	"slices"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"github.com/avast/retry-go/v4"
 )
 
 // Environments
@@ -18,3 +23,51 @@ type Environments interface {
 	RemoveAlias(ctx context.Context, spaceId, envId, alias string) (err error)
 	Migrate(ctx context.Context, spaceId, envId string, options ...*MigrateOptions) (err error)
 }
+
+const (
+	RetryDelay         = 1 * time.Second
+	RetryAttempts uint = 1200
+)
+
+// IsAvailable проверяет доступность окружения.
+// Если окружение недоступно, возвращает ошибку ErrEnvironmentUnavailable
+func IsAvailable(ctx context.Context, svc Environments, spaceID string, environmentID string) error {
+	env, err := svc.Get(ctx, spaceID, environmentID)
+	if err != nil {
+		return err
+	}
+	if env.StateInfo == nil || slices.Contains(WriteAllowedStates, env.StateInfo.State) {
+		return nil
+	}
+	return errors.WithContext(ErrEnvironmentUnavailable, "state", env.StateInfo.State)
+}
+
+// WaitForAvailable периодически проверяет состояние окружения, пока оно не станет доступным.
+// Если окружение становится доступным, функция возвращает nil.
+// Если достигнуто максимальное количество попыток, функция возвращает ошибку.
+//
+// Через необязательные параметры можно, например, настроить задержку между попытками (по умолчанию равную RetryDelay) и максимальное количество попыток (по умолчанию равное RetryAttempts),
+// но опции retry.Context и retry.RetryIf будут проигнорированы.
+func WaitForAvailable(ctx context.Context, svc Environments, spaceID string, environmentID string, opts ...retry.Option) error {
+	opts = slices.Concat(
+		[]retry.Option{
+			retry.DelayType(retry.FixedDelay),
+			retry.Delay(RetryDelay),
+			retry.Attempts(RetryAttempts),
+		},
+		opts,
+		// Чтобы предотвратить изменение параметров пользователем, размещаем их в конце списка.
+		[]retry.Option{
+			retry.Context(ctx),
+			retry.RetryIf(func(err error) bool {
+				return errors.Is(err, ErrEnvironmentUnavailable)
+			}),
+		},
+	)
+	return retry.Do(
+		func() error {
+			return IsAvailable(ctx, svc, spaceID, environmentID)
+		},
+		opts...,
+	)
+}
diff --git a/pkg/environments/service_test.go b/pkg/environments/service_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..9a3e6b06ec81e3ab8e84cefdfb20aec22c7213af
--- /dev/null
+++ b/pkg/environments/service_test.go
@@ -0,0 +1,111 @@
+package environments
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/avast/retry-go/v4"
+	"github.com/stretchr/testify/assert"
+)
+
+type dummyEnvironments struct {
+	Environments
+	environment *Environment
+	sleep       time.Duration
+}
+
+func (t *dummyEnvironments) Get(ctx context.Context, _ string, _ string) (*Environment, error) {
+	if t.sleep != 0 {
+		time.Sleep(t.sleep)
+		if err := ctx.Err(); err != nil {
+			return nil, err
+		}
+	}
+	return t.environment, nil
+}
+
+func TestIsAvailable(t *testing.T) {
+	tests := []struct {
+		name        string
+		environment *Environment
+		wantErr     bool
+	}{
+		{
+			"Environment has nil StateInfo: available",
+			&Environment{ID: "env-id", SpaceID: "space-id"},
+			false,
+		},
+		{
+			"Environment state is StateReady: available",
+			&Environment{ID: "env-id", SpaceID: "space-id", StateInfo: &StateInfo{State: StateReady}},
+			false,
+		},
+		{
+			"Environment state is StatePreparing: not available",
+			&Environment{ID: "env-id", SpaceID: "space-id", StateInfo: &StateInfo{State: StatePreparing}},
+			true,
+		},
+		{
+			"Environment state is StateError: not available",
+			&Environment{ID: "env-id", SpaceID: "space-id", StateInfo: &StateInfo{State: StateError}},
+			true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			environments := &dummyEnvironments{environment: tt.environment}
+			if err := IsAvailable(context.Background(), environments, "space-id", "master"); (err != nil) != tt.wantErr {
+				t.Errorf("IsAvailable() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestWaitForAvailable(t *testing.T) {
+	t.Run("Success", func(t *testing.T) {
+		env := &Environment{ID: "env-id", SpaceID: "space-id", StateInfo: &StateInfo{State: StatePreparing}}
+		environments := &dummyEnvironments{environment: env}
+
+		// Имитируем завершение подготовки окружения через 1 секунду
+		go func() {
+			time.Sleep(1 * time.Second)
+			env.StateInfo = &StateInfo{State: StateReady}
+		}()
+
+		err := WaitForAvailable(context.Background(), environments, "space-id", "master")
+		assert.NoError(t, err)
+	})
+	t.Run("Overwritten retry options", func(t *testing.T) {
+		env := &Environment{ID: "env-id", SpaceID: "space-id", StateInfo: &StateInfo{State: StatePreparing}}
+		environments := &dummyEnvironments{environment: env}
+
+		err := WaitForAvailable(context.Background(), environments, "space-id", "master", retry.Attempts(1))
+		assert.Error(t, err, "За одну попытку окружение не стало доступным, получаем ошибку")
+	})
+	t.Run("External context was canceled", func(t *testing.T) {
+		env := &Environment{ID: "env-id", SpaceID: "space-id", StateInfo: &StateInfo{State: StatePreparing}}
+		environments := &dummyEnvironments{environment: env}
+
+		ctx, cancel := context.WithCancel(context.Background())
+
+		// Отменяем контекст через 1 секунду
+		go func() {
+			time.Sleep(1 * time.Second)
+			cancel()
+		}()
+
+		err := WaitForAvailable(ctx, environments, "space-id", "master")
+		assert.ErrorIs(t, err, context.Canceled)
+	})
+	t.Run("External context was deadline exceeded", func(t *testing.T) {
+		env := &Environment{ID: "env-id", SpaceID: "space-id", StateInfo: &StateInfo{State: StatePreparing}}
+		environments := &dummyEnvironments{environment: env}
+
+		ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+		defer cancel()
+
+		err := WaitForAvailable(ctx, environments, "space-id", "master")
+		assert.ErrorIs(t, err, context.DeadlineExceeded)
+	})
+}