Slide 1

Slide 1 text

SaaSͷ Ϛϧνςφϯτ࣮ݱํ๏ @neko-neko

Slide 2

Slide 2 text

ࠓ೔࿩͢͜ͱ • SaaSͷιϑτ΢ΣΞఏڙํ๏ • Ϛϧνςφϯτͷ࣮ݱํ๏

Slide 3

Slide 3 text

ࠓ೔࿩͢͜ͱ • SaaSͷιϑτ΢ΣΞఏڙํ๏ • Ϛϧνςφϯτͷ࣮ݱํ๏

Slide 4

Slide 4 text

SaaSʹ͓͚Διϑτ΢ΣΞ ఏڙํ๏͸2ͭ

Slide 5

Slide 5 text

SaaSͷιϑτ΢ΣΞఏڙํ๏ • γϯάϧςφϯτ • Ϛϧνςφϯτ • ΞϓϦέʔγϣϯڞ༗/DB෼ׂܕ • ΞϓϦέʔγϣϯ/DBڞ༗ܕ

Slide 6

Slide 6 text

SaaSͷιϑτ΢ΣΞఏڙํ๏ • γϯάϧςφϯτ • Ϛϧνςφϯτ • ΞϓϦέʔγϣϯڞ༗/DB෼ׂܕ • ΞϓϦέʔγϣϯ/DBڞ༗ܕ

Slide 7

Slide 7 text

γϯάϧςφϯτͬͯʁ

Slide 8

Slide 8 text

ΞϓϦέʔγϣϯΛݸผͷ؀ڥ Ͱར༻͢ΔϞσϧ

Slide 9

Slide 9 text

• ސ٬ຖʹސ٬ઐ༻ͷαʔϏεͱDBΛ༻ҙ͢Δ • ઐ༗؀ڥΛ༻ҙ͢ΔͷͰߴίετ • ϥϯχϯάίετɺӡ༻ίετͰࢮΜͰ͠·͏

Slide 10

Slide 10 text

SaaSͷιϑτ΢ΣΞఏڙํ๏ • γϯάϧςφϯτ • Ϛϧνςφϯτ • ΞϓϦέʔγϣϯڞ༗/DB෼ׂܕ • ΞϓϦέʔγϣϯ/DBڞ༗ܕ

Slide 11

Slide 11 text

Ϛϧνςφϯτͬͯʁ

Slide 12

Slide 12 text

ΞϓϦέʔγϣϯΛಉ͡؀ڥͰڞ༗ར༻͢Δ Ϟσϧ

Slide 13

Slide 13 text

• ෳ਺ͷސ٬͕1ͭͷ؀ڥΛڞ༻͢Δ • γϯάϧςφϯτͱൺֱͯ͠ϥϯχϯάίε τ΋ӡ༻ίετ΋άοͱ཈͑ΒΕΔ • ੈͷSaaS͸΄ͱΜͲ͜Ε

Slide 14

Slide 14 text

ϚϧνςφϯτΞʔΩςΫνϟ Ҿ༻ݩ: https://www.ibm.com/developerworks/jp/cloud/library/cl-multitenantsaas/index.html

Slide 15

Slide 15 text

SaaSͷιϑτ΢ΣΞఏڙํ๏ • γϯάϧςφϯτ • Ϛϧνςφϯτ • ΞϓϦέʔγϣϯڞ༗/DB෼ׂܕ • ΞϓϦέʔγϣϯ/DBڞ༗ܕ

Slide 16

Slide 16 text

ΞϓϦέʔγϣϯڞ༗ɺDB෼ׂܕ

Slide 17

Slide 17 text

• ϝϦοτ • ؅ཧָ͕ • ༰қʹ৘ใ࿙͍͑Λ๷͙ͨΊͷϑΟϧλ৚݅Λ૊ΊΔ • σϝϦοτ • ࿦ཧతʹDBΛ෼ׂ͍ͯ͠Δέʔε(databaseΛސ٬ຖʹ༻ҙ͢Δํ๏΍εΩʔϚΛސ٬ ຖʹ༻ҙ͢Δํ๏) • ސ٬਺ͷ૿ՃͰςʔϒϧ਺͕૿͑ΔͨΊDBϚΠάϨʔγϣϯ࣮ߦ͕࣌ؒ૿͑Δ • ςφϯτຖʹεΩʔϚ͕ҟͳΔλΠϛϯά͕௕͍࣌ؒى͖ಘΔ • ສ͕ҰϚΠάϨʔγϣϯ͕ࣦഊͨ࣌͠ͷϦΧόϦ͕खؒ

Slide 18

Slide 18 text

SaaSͷιϑτ΢ΣΞఏڙํ๏ • γϯάϧςφϯτ • Ϛϧνςφϯτ • ΞϓϦέʔγϣϯڞ༗/DB෼ׂܕ • ΞϓϦέʔγϣϯ/DBڞ༗ܕ

Slide 19

Slide 19 text

ΞϓϦέʔγϣϯɺDBڞ༗ܕ

Slide 20

Slide 20 text

• ϝϦοτ • γϯϓϧΏ͑ʹ؅ཧָ͕ • γεςϜશମͷςʔϒϧ૯਺΋཈͑ΒΕΔ • σϝϦοτ • DB෼ׂܕͱൺ΂ͯ৘ใ࿙͍͑Λ๷͙ͨΊͷϑΟϧλΛ૊ΉͷΛߟྀ͠ͳ͘ ͯ͸͍͚ͳ͍ • ୯७ʹݸʑͷॲཧʹରͯ͠ϑΟϧλ৚݅Λ૊ΈࠐΉͱɺ࿙Ε͕ى͖ͨ࣌ ʹॏେͳηΩϡϦςΟϗʔϧͱͳΓಘΔ • ސ٬਺ͷ૿ՃͰγεςϜશମͷϨΠςϯγ͕ѱԽ͢Δ

Slide 21

Slide 21 text

͔͜͜Β͕ࠓ೔ͷຊ୊

Slide 22

Slide 22 text

ࠓ೔࿩͢͜ͱ • SaaSͷιϑτ΢ΣΞఏڙํ๏ • Ϛϧνςφϯτͷ࣮ݱํ๏

Slide 23

Slide 23 text

ΞϓϦέʔγϣϯڞ༗/ DB෼ׂܕΛ࣮ݱ͢Δ৔߹

Slide 24

Slide 24 text

ΞϓϦέʔγϣϯڞ༗ɺDB෼ׂܕ • Ҋ1: ςφϯτຖʹεΩʔϚ͔databaseΛ෼ ׂ͢Δ • Ҋ2: ϝΠϯςʔϒϧΛ༻ҙ͠ɺςφϯτຖ ʹViewΛ༻ҙ͢Δ

Slide 25

Slide 25 text

ΞϓϦέʔγϣϯڞ༗ɺDB෼ׂܕ • Ҋ1: ςφϯτຖʹεΩʔϚ͔databaseΛ෼ ׂ͢Δ • Ҋ2: ϝΠϯςʔϒϧΛ༻ҙ͠ɺςφϯτຖ ʹViewΛ༻ҙ͢Δ

Slide 26

Slide 26 text

ςφϯτຖʹεΩʔϚ͔DB෼ׂ CREATE SCHEMA tenant1; CREATE TABLE tenant1.blogs ( id bigserial NOT NULL PRIMARY KEY, title text NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, );

Slide 27

Slide 27 text

ςφϯτຖʹεΩʔϚ͔DB෼ׂ CREATE SCHEMA tenant1; CREATE TABLE tenant1.blogs ( id bigserial NOT NULL PRIMARY KEY, title text NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, ); ςφϯτຖʹεΩʔϚΛ༻ҙͯ͠ ςʔϒϧΛ࡞੒

Slide 28

Slide 28 text

ΞϓϦέʔγϣϯڞ༗ɺDB෼ׂܕ • Ҋ1: ςφϯτຖʹεΩʔϚ͔databaseΛ෼ ׂ͢Δ • Ҋ2: ϝΠϯςʔϒϧΛ༻ҙ͠ɺςφϯτຖ ʹViewΛ༻ҙ͢Δ

Slide 29

Slide 29 text

ςφϯτຖʹViewΛ༻ҙ͢Δ CREATE TABLE public.blogs ( id bigserial NOT NULL PRIMARY KEY, tenant_id bigint NOT NULL, title text NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, ); CREATE SCHEMA tenant1; CREATE VIEW tenant1.blogs AS SELECT * FROM public.blogs WHERE tenant_id::text = current_setting('application_name');

Slide 30

Slide 30 text

ςφϯτຖʹViewΛ༻ҙ͢Δ CREATE TABLE public.blogs ( id bigserial NOT NULL PRIMARY KEY, tenant_id bigint NOT NULL, title text NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, ); CREATE SCHEMA tenant1; CREATE VIEW tenant1.blogs AS SELECT * FROM public.blogs WHERE tenant_id::text = current_setting('application_name'); ςφϯτຖʹεΩʔϚΛ༻ҙ͢Δͷ͸มΘΒͳ ͍͕ɺ7JFXΛ࢖ͬͯՄࢹੑͷ੍ݶΛߦ͏ɻ ςφϯτͷૢ࡞͸7JFXͷΈΛ௨ͯ͡$36%͢Δ͜ ͱͰ҆શʹσʔλΛѻ͑Δ

Slide 31

Slide 31 text

ΞϓϦέʔγϣϯ/DBڞ༗ܕ Λ࣮ݱ͢Δ৔߹

Slide 32

Slide 32 text

ΞϓϦέʔγϣϯ/DBڞ༗ܕ • Ҋ1: ۪௚ʹςφϯτͷϦιʔε͔ϦΫΤετ ຖʹνΣοΫ • Ҋ2: ςφϯτຖͷViewΛ༻ҙ͢Δ • Ҋ3: RLS(Row Level Security)Λ࢖͏

Slide 33

Slide 33 text

ΞϓϦέʔγϣϯ/DBڞ༗ܕ • Ҋ1: ۪௚ʹςφϯτͷϦιʔε͔ϦΫΤετ ຖʹνΣοΫ • Ҋ2: ςφϯτຖͷViewΛ༻ҙ͢Δ • Ҋ3: RLS(Row Level Security)Λ࢖͏

Slide 34

Slide 34 text

ςφϯτຖʹཁٻϦΫΤετ͕ ਖ਼͍͠είʔϓ͔֬ೝ id := req.params(“id”) if !model.validScope(id) { return permissionDenied } // something to do…

Slide 35

Slide 35 text

• ͦͩ͘Δ͍ • ݱ࣮͸͜Μͳγϯϓϧʹ͸ऩ·Βͳ͍ • ສ͕Ұ࿙Ε͕͋ͬͨ৔߹ɺॏେͳηΩϡϦςΟ ϗʔϧʹͳΔ • ਓ͸ϛε͢Δ΋ͷɻݕग़࿙Ε͕ݕ஌ͮ͠Β͍(ςε τέʔε͔Β΋࿙ΕΔՄೳੑ͕͋Δ)

Slide 36

Slide 36 text

ΞϓϦέʔγϣϯ/DBڞ༗ܕ • Ҋ1: ۪௚ʹςφϯτͷϦιʔε͔ϦΫΤετ ຖʹνΣοΫ • Ҋ2: ςφϯτຖͷViewΛ༻ҙ͢Δ • Ҋ3: RLS(Row Level Security)Λ࢖͏

Slide 37

Slide 37 text

ςφϯτຖʹViewΛ༻ҙ͢Δ CREATE TABLE public.blogs ( id bigserial NOT NULL PRIMARY KEY, tenant_id bigint NOT NULL, title text NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, ); CREATE VIEW public.tenant1_blogs AS SELECT * FROM public.blogs WHERE tenant_id::text = current_setting('application_name');

Slide 38

Slide 38 text

• UPDATE/DELETE࣌͸Viewʹରͯ͠ߦ͏ • INSERT͸௚઀େݩͷςʔϒϧʹରͯ͠ߦ͏ • ސ٬ຖʹViewΛ༻ҙ͢Δ͜ͱʹ͸ͳΔ͕ɺ࣮ ૷͸ָͪΜ

Slide 39

Slide 39 text

ΞϓϦέʔγϣϯ/DBڞ༗ܕ • Ҋ1: ۪௚ʹςφϯτͷϦιʔε͔ϦΫΤετ ຖʹνΣοΫ • Ҋ2: ςφϯτຖͷViewΛ༻ҙ͢Δ • Ҋ3: RLS(Row Level Security)Λ࢖͏

Slide 40

Slide 40 text

RLSͬͯʁ

Slide 41

Slide 41 text

Row Level Security • ߦ୯ҐηΩϡϦςΟ • ߦຖʹϨίʔυͷࢀর/ૠೖ/ߋ৽/࡟আΛ੍ݶ͢Δ͜ͱ͕ग़དྷΔ • ϙϦγʔʹϚον͠ͳ͍৔߹
 ࢀর: ֘౰ͷϨίʔυ͕ฦ٫͞Εͳ͍
 ͦΕҎ֎: ࣮ߦग़དྷͳ͍ • ϙϦγʔ͸SUPERUSER΋͘͠͸BYPASSRLS͕Ξλον͞Εͯ ͍Δϩʔϧͷ৔߹ແࢹ͞ΕΔ

Slide 42

Slide 42 text

ࢼͯ͠ΈΔ CREATE TABLE public.blogs ( id bigserial NOT NULL PRIMARY KEY, tenant_id bigint NOT NULL, title text NOT NULL ); CREATE POLICY blogs_policy ON public.blogs FOR ALL TO app_user USING (tenant_id::text = current_setting(‘application_name', FALSE)); ALTER TABLE public.blogs ENABLE ROW LEVEL SECURITY; INSERT INTO public.blogs (tenant_id, title) VALUES (1, ‘TEST1’); INSERT INTO public.blogs (tenant_id, title) VALUES (1, ‘TEST2’); INSERT INTO public.blogs (tenant_id, title) VALUES (2, ‘TEST3’); INSERT INTO public.blogs (tenant_id, title) VALUES (2, ‘TEST4’); INSERT INTO public.blogs (tenant_id, title) VALUES (3, ‘TEST5’);

Slide 43

Slide 43 text

ಈ࡞ΛݟΔ(SELECT) -- superuserͷ৔߹ select * from blogs; id | tenant_id | title ----+-----------+------- 1 | 1 | TEST1 2 | 1 | TEST2 3 | 2 | TEST3 4 | 2 | TEST4 5 | 3 | TEST5 -- normal userͷ৔߹ select * from blogs; id | tenant_id | title ----+-----------+------- (0 rows) -- normal userͷ৔߹(application_name: 1) SET application_name = '1'; select * from blogs; id | tenant_id | title ----+-----------+------- 1 | 1 | TEST1 2 | 1 | TEST2 (2 rows)

Slide 44

Slide 44 text

ಈ࡞ΛݟΔ(INSERT) -- superuserͷ৔߹ ໰୊ແ͘ೖΔ -- normal userͷ৔߹ INSERT INTO public.blogs (tenant_id, title) VALUES (1, 'TEST1'); ERROR: new row violates row-level security policy for table "blogs" -- normal userͷ৔߹(application_name: 1) SET application_name = ‘1'; INSERT INTO public.blogs (tenant_id, title) VALUES (1, 'TEST1'); INSERT 0 1 -- normal userͷ৔߹(application_name: 2) INSERT INTO public.blogs (tenant_id, title) VALUES (1, 'TEST1'); ERROR: new row violates row-level security policy for table "blogs"

Slide 45

Slide 45 text

ಈ࡞ΛݟΔ(UPDATE) -- superuserͷ৔߹ UPDATE blogs SET title='UPDATE' WHERE id = 1; select * from blogs where id = 1; id | tenant_id | title ----+-----------+-------- 1 | 1 | UPDATE -- normal userͷ৔߹ UPDATE blogs SET title='UPDATE' WHERE id = 1; UPDATE 0 <- Ξοϓσʔτ͞Ε͍ͯͳ͍ -- normal userͷ৔߹(application_name: 1) SET application_name = '1'; UPDATE blogs SET title=‘UPDATE' WHERE id = 1; UPDATE 1 <- Ξοϓσʔτ͞Ε͍ͯΔ

Slide 46

Slide 46 text

ಈ࡞ΛݟΔ(DELETE) -- superuserͷ৔߹ ໰୊ͳ͘࡟আग़དྷΔ -- normal userͷ৔߹ DELETE FROM blogs WHERE id = 1; DELETE 0 <- DELETE͞Ε͍ͯͳ͍ -- normal userͷ৔߹(application_name: 1) SET application_name = ‘1’; DELETE FROM blogs WHERE id = 1; DELETE 1 <- DELETE͞Ε͍ͯΔ

Slide 47

Slide 47 text

RLS with go

Slide 48

Slide 48 text

લఏ • ֤ςʔϒϧʹtenant_idΛ࣋ͨͤΔӡ༻ʹ͢Δ • ސ٬ຖʹϩʔϧΛ࡞੒͢Δӡ༻ʹ͸͠ͳ͍ • 1ͭͷεΩʔϚͰαʔϏεͷςʔϒϧ؅ཧΛ ߦ͏

Slide 49

Slide 49 text

Table CREATE TABLE public.blogs ( id bigserial NOT NULL PRIMARY KEY, tenant_id bigint NOT NULL, title text NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, ); CREATE TABLE public.posts ( id bigserial NOT NULL PRIMARY KEY, tenant_id bigint NOT NULL, title text NOT NULL, content text NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, ); -- ֎෦Ωʔͷఆٛ͸লུ -- tenant has many groups͕͋ΔલఏͰάϧʔϓͷॴଐϝϯόʔΛ؅ཧ͢Δςʔϒϧ CREATE TABLE public.group_users ( id bigserial NOT NULL PRIMARY KEY, tenant_id bigint NOT NULL, group_id bigint NOT NULL, user_id bigint NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, );

Slide 50

Slide 50 text

Policy — ϙϦγʔͷఆٛ CREATE POLICY blogs_policy ON public.blogs FOR ALL TO app_user USING (tenant_id::text = current_setting(‘application_name', FALSE)); CREATE POLICY posts_policy ON public.posts FOR ALL TO app_user USING (tenant_id::text = current_setting('application_name', FALSE)); CREATE POLICY group_users_policy ON public.group_users FOR ALL TO app_user USING (tenant_id::text = current_setting('application_name', FALSE)); WITH CEHCK ( group_id = (SELECT id FROM groups WHERE id = group_id) AND user_id = (SELECT id FROM users WHERE id = user_id) ); —- ϙϦγʔͷ༗ޮԽ ALTER TABLE public.blogs ENABLE ROW LEVEL SECURITY; ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY; ALTER TABLE public.group_users ENABLE ROW LEVEL SECURITY;

Slide 51

Slide 51 text

Go

Slide 52

Slide 52 text

Middleware(Τϥʔൈ͖) func ExampleMultitenantMiddleware(db *sql.DB) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // context͔ΒݱࡏͷςφϯτIDΛऔಘͯ͠ // ίωΫγϣϯϓʔϧͷ઀ଓΛઐ༗͢Δ tenantID := context.CurrentTenantID(ctx) conn, _ := db.Conn(ctx) defer conn.Close() // ηογϣϯ͕ੜ͖͍ͯΔؒ༗ޮʹͳΔม਺ΛDB΁ηοτ // Ҏ߱ͷhandler͸ઐ༗தͷίωΫγϣϯΛ࢖ͬͯΫΤϦΛൃߦ͢Δ conn.ExecContext(ctx, fmt.Sprintf(“SET application_name=%s;”, tenantID)) next.ServeHTTP(w, r.WithContext(context.WithCurrentConnection(ctx, conn))) } return http.HandleFunc(fn) } }

Slide 53

Slide 53 text

·ͱΊ • RLSΛ࢖͏͜ͱͰDBͷ؅ཧ͕ͦ͜·Ͱ൥ࡶʹͳΒͣ Ϛϧνςφϯτͷ࣮ݱ͕ग़དྷΔ • ϓϩμΫτॳظϑΣʔζͰ͋Ε͹ɺ͜ΕͰे෼྇͛ Δ • Ϩίʔυ਺͕૿͍͑ͯͬͨΒ֤ςʔϒϧͷ tenant_idΛ࢖ͬͯγϟʔσΟϯάΛߦ͍ෛՙରࡦ